mirror of
https://github.com/heygen-com/hyperframes.git
synced 2026-09-13 07:40:06 +00:00
Restores the 208 catalog items reverted after their previews 404'd in production, this time on the payload mechanism rather than the .html files that caused the outage. The generator no longer writes a preview document to docs/public. That writer, and the machinery under it, existed only to produce files the docs host discards, so it is gone rather than bypassed. Items now embed the composition itself via a payload, which is what the previous change already does for the items that were already in the catalog. The variables explorer is parked, not restored: it drove its preview through the same unpublished .html path, so it would have shown an empty frame. Items that declare variables get the live player plus the static variables table, and reconnecting the explorer to payloads is a follow-up.
482 lines
17 KiB
Plaintext
482 lines
17 KiB
Plaintext
---
|
||
title: "Halftone Field"
|
||
description: "A full-bleed halftone light field: a grid of dots whose size and colour track a slow flowing simplex-noise field, so broad bands of light sweep across a mostly-black frame like a giant LED wall showing a plasma. Built to run under type."
|
||
---
|
||
|
||
import { InstallCommand } from "/snippets/install-command.jsx";
|
||
|
||
<iframe
|
||
className="w-full aspect-video rounded-xl border-0 bg-zinc-100 dark:bg-zinc-800"
|
||
title="halftone-field preview"
|
||
loading="lazy"
|
||
srcDoc={`<!doctype html><html><head><meta charset="utf-8"><style>html,body{margin:0;height:100%;overflow:hidden;background:transparent}hyperframes-player{display:block;width:100%;height:100%}</style><script src="https://cdn.jsdelivr.net/npm/@hyperframes/player@0.7/dist/hyperframes-player.global.js"><\/script></head><body><script>fetch("/public/catalog/blocks/halftone-field.json").then(function(r){return r.json()}).then(function(d){var p=document.createElement("hyperframes-player");p.setAttribute("srcdoc",d.html);p.setAttribute("controls","");p.setAttribute("autoplay","");p.setAttribute("loop","");p.setAttribute("muted","");p.setAttribute("poster","https://static.heygen.ai/hyperframes-oss/docs/images/catalog/blocks/halftone-field.png");document.body.appendChild(p)});<\/script></body></html>`}
|
||
/>
|
||
|
||
## Install
|
||
|
||
<InstallCommand command="npx hyperframes add halftone-field" />
|
||
|
||
That writes one file: `compositions/halftone-field.html`.
|
||
|
||
## Add it to your video
|
||
|
||
It runs for 10 seconds at 1920×1080. Paste this into your composition:
|
||
|
||
```html index.html
|
||
<div
|
||
data-composition-id="halftone-field"
|
||
data-composition-src="compositions/halftone-field.html"
|
||
data-start="0"
|
||
data-duration="10"
|
||
data-track-index="1"
|
||
data-width="1920"
|
||
data-height="1080"
|
||
></div>
|
||
```
|
||
|
||
Move it in time with `data-start`. Put it on a different timeline row with
|
||
`data-track-index`. See [data attributes](/concepts/data-attributes) for the rest.
|
||
|
||
## Change how it looks
|
||
|
||
Set these CSS variables on the block:
|
||
|
||
- `--frequency` — Field frequency (1 = broad swells, 10 = fine mottle). Defaults to `2`.
|
||
- `--speed` — Flow speed (0 freezes the field). Defaults to `5`.
|
||
- `--cellsize` — Cell size (dot pitch, 1 = 6px, 100 = 60px). Defaults to `54`.
|
||
- `--gamma` — Gamma: midtone crush, the whole look. Low values flatten it to an even grey dot field. Defaults to `12`.
|
||
- `--palettebias` — Palette bias: floor under every dot's size and palette position. 0 lets the darks go fully black. Defaults to `2`.
|
||
- `--palette1` — Palette stop 1 (lows). Defaults to `#00AAFF`.
|
||
- `--palette2` — Palette stop 2. Defaults to `#C2FF67`.
|
||
- `--palette3` — Palette stop 3. Defaults to `#6600FF`.
|
||
- `--palette4` — Palette stop 4 (crests). Defaults to `#000000`.
|
||
- `--background` — Background. Defaults to `#000000`.
|
||
- `--quantize` — Quantise to 30fps for a stepped LED-wall look. Defaults to `false`. Options: `false`, `true`.
|
||
|
||
## Variables
|
||
|
||
Every one of these has a default, so the piece works untouched. Set the ones you
|
||
want to change on the element:
|
||
|
||
| Variable | Default | Accepts | What it does |
|
||
| --- | --- | --- | --- |
|
||
| `frequency` | `2` | 1 to 10, step 0.1 | |
|
||
| `speed` | `5` | 0 to 10, step 0.1 | |
|
||
| `cellSize` | `54` | 1 to 100, step 1 | |
|
||
| `gamma` | `12` | 1 to 20, step 0.1 | |
|
||
| `paletteBias` | `2` | 0 to 20, step 0.5 | |
|
||
| `palette1` | `#00AAFF` | color | |
|
||
| `palette2` | `#C2FF67` | color | |
|
||
| `palette3` | `#6600FF` | color | |
|
||
| `palette4` | `#000000` | color | |
|
||
| `background` | `#000000` | color | |
|
||
| `quantize` | `false` | boolean | |
|
||
|
||
Set them with `data-variable-values` on the element that mounts it. These are the
|
||
defaults, so this behaves exactly like the preview above until you change one:
|
||
|
||
```html wrap
|
||
<div
|
||
data-composition-id="halftone-field"
|
||
data-composition-src="compositions/halftone-field.html"
|
||
data-variable-values='{"frequency":2,"speed":5,"cellSize":54,"gamma":12,"paletteBias":2,"palette1":"#00AAFF","palette2":"#C2FF67","palette3":"#6600FF","palette4":"#000000","background":"#000000","quantize":false}'
|
||
></div>
|
||
```
|
||
|
||
## Source
|
||
|
||
<Accordion title={`halftone-field.html`}>
|
||
|
||
```html
|
||
<!doctype html>
|
||
<html lang="en">
|
||
<head>
|
||
<meta charset="utf-8" />
|
||
<meta name="viewport" content="width=1920, height=1080" />
|
||
<title>Halftone Field</title>
|
||
<script src="https://cdn.jsdelivr.net/npm/gsap@3.14.2/dist/gsap.min.js"></script>
|
||
<style>
|
||
*,
|
||
*::before,
|
||
*::after {
|
||
margin: 0;
|
||
padding: 0;
|
||
box-sizing: border-box;
|
||
}
|
||
body {
|
||
background: #000;
|
||
overflow: hidden;
|
||
}
|
||
#hlf-root {
|
||
position: relative;
|
||
width: 1920px;
|
||
height: 1080px;
|
||
overflow: hidden;
|
||
}
|
||
#hlf-backdrop {
|
||
position: absolute;
|
||
inset: 0;
|
||
background: #000000;
|
||
}
|
||
#hlf-canvas {
|
||
position: absolute;
|
||
top: 0;
|
||
left: 0;
|
||
width: 1920px;
|
||
height: 1080px;
|
||
}
|
||
</style>
|
||
</head>
|
||
<body>
|
||
<div
|
||
id="hlf-root"
|
||
data-composition-id="halftone-field"
|
||
data-root="true"
|
||
data-width="1920"
|
||
data-height="1080"
|
||
data-start="0"
|
||
data-duration="10"
|
||
data-composition-variables='[
|
||
{"id":"frequency","type":"number","label":"Field frequency","default":2,"min":1,"max":10,"step":0.1},
|
||
{"id":"speed","type":"number","label":"Flow speed","default":5,"min":0,"max":10,"step":0.1},
|
||
{"id":"cellSize","type":"number","label":"Cell size","default":54,"min":1,"max":100,"step":1},
|
||
{"id":"gamma","type":"number","label":"Gamma (midtone crush)","default":12,"min":1,"max":20,"step":0.1},
|
||
{"id":"paletteBias","type":"number","label":"Palette bias","default":2,"min":0,"max":20,"step":0.5},
|
||
{"id":"palette1","type":"color","label":"Palette stop 1 (lows)","default":"#00AAFF"},
|
||
{"id":"palette2","type":"color","label":"Palette stop 2","default":"#C2FF67"},
|
||
{"id":"palette3","type":"color","label":"Palette stop 3","default":"#6600FF"},
|
||
{"id":"palette4","type":"color","label":"Palette stop 4 (crests)","default":"#000000"},
|
||
{"id":"background","type":"color","label":"Background","default":"#000000"},
|
||
{"id":"quantize","type":"boolean","label":"Quantise to 30fps (stepped)","default":false}
|
||
]'
|
||
>
|
||
<div id="hlf-backdrop"></div>
|
||
<canvas id="hlf-canvas" width="1920" height="1080"></canvas>
|
||
|
||
<!-- Driver clip: gives HyperFrames a timed element to own on track 0. -->
|
||
<div
|
||
id="hlf-drv"
|
||
class="clip"
|
||
data-start="0"
|
||
data-duration="10"
|
||
data-track-index="0"
|
||
style="position: absolute; width: 1px; height: 1px; opacity: 0; pointer-events: none"
|
||
></div>
|
||
</div>
|
||
|
||
<script>
|
||
(function () {
|
||
var DUR = 10;
|
||
var W = 1920;
|
||
var H = 1080;
|
||
|
||
var V = (window.__hyperframes && window.__hyperframes.getVariables()) || {};
|
||
var CS = getComputedStyle(document.getElementById("hlf-root"));
|
||
|
||
// The runtime defines every declared variable as `--<id>` on the root,
|
||
// so a host stylesheet can override one there too. Read the custom
|
||
// property first, fall back to the declared value when it is unset.
|
||
function raw(id) {
|
||
var css = CS.getPropertyValue("--" + id.toLowerCase()).trim();
|
||
return css !== "" ? css : V[id];
|
||
}
|
||
function num(id, fallback) {
|
||
var n = parseFloat(raw(id));
|
||
return isFinite(n) ? n : fallback;
|
||
}
|
||
function bool(id) {
|
||
var v = raw(id);
|
||
return v === true || /^(true|1|on|yes)$/i.test(String(v));
|
||
}
|
||
// #RGB / #RRGGBB -> [r, g, b] in 0..1. Anything else falls back.
|
||
function rgb(id, fallback) {
|
||
var s = String(raw(id) || "").trim();
|
||
var m = /^#([0-9a-f]{3}|[0-9a-f]{6})$/i.exec(s);
|
||
if (!m) s = fallback;
|
||
else s = "#" + m[1];
|
||
var hex = s.slice(1);
|
||
if (hex.length === 3) {
|
||
hex = hex[0] + hex[0] + hex[1] + hex[1] + hex[2] + hex[2];
|
||
}
|
||
var n = parseInt(hex, 16);
|
||
return [((n >> 16) & 255) / 255, ((n >> 8) & 255) / 255, (n & 255) / 255];
|
||
}
|
||
// The reference exposes 1-10 / 1-100 / 1-20 dials and maps them onto
|
||
// the ranges the shader actually wants.
|
||
function mapLinear(x, a1, a2, b1, b2) {
|
||
return b1 + ((x - a1) * (b2 - b1)) / (a2 - a1);
|
||
}
|
||
|
||
var FREQ = mapLinear(num("frequency", 2), 1, 10, 0.3, 6);
|
||
var SPEED = num("speed", 5) * 0.05;
|
||
var CELL = mapLinear(num("cellSize", 54), 1, 100, 6, 60);
|
||
var GAMMA = mapLinear(num("gamma", 12), 1, 20, 0.5, 8);
|
||
var BIAS = num("paletteBias", 2) * 0.05;
|
||
var PAL = [
|
||
rgb("palette1", "#00AAFF"),
|
||
rgb("palette2", "#C2FF67"),
|
||
rgb("palette3", "#6600FF"),
|
||
rgb("palette4", "#000000"),
|
||
];
|
||
var BG = rgb("background", "#000000");
|
||
var QUANTIZE = bool("quantize");
|
||
|
||
document.getElementById("hlf-backdrop").style.background =
|
||
"rgb(" +
|
||
BG.map(function (c) {
|
||
return Math.round(c * 255);
|
||
}).join(",") +
|
||
")";
|
||
|
||
var canvas = document.getElementById("hlf-canvas");
|
||
var gl =
|
||
canvas.getContext("webgl", {
|
||
alpha: false,
|
||
antialias: false,
|
||
depth: false,
|
||
stencil: false,
|
||
preserveDrawingBuffer: true,
|
||
powerPreference: "high-performance",
|
||
}) ||
|
||
canvas.getContext("experimental-webgl", {
|
||
alpha: false,
|
||
preserveDrawingBuffer: true,
|
||
});
|
||
|
||
var VERT = "attribute vec2 aPos;\nvoid main() { gl_Position = vec4(aPos, 0.0, 1.0); }";
|
||
|
||
// Single pass. The reference renders the colour field into a target and
|
||
// samples it at each cell centre, but the field is analytic, so
|
||
// evaluating it at the cell centre here is the same value without the
|
||
// round trip through a texture.
|
||
var FRAG = `
|
||
precision highp float;
|
||
uniform vec2 uRes;
|
||
uniform float uTime;
|
||
uniform float uFreq;
|
||
uniform float uSpeed;
|
||
uniform float uCell;
|
||
uniform float uGamma;
|
||
uniform float uBias;
|
||
uniform vec3 uPal0;
|
||
uniform vec3 uPal1;
|
||
uniform vec3 uPal2;
|
||
uniform vec3 uPal3;
|
||
uniform vec3 uBg;
|
||
|
||
// 3D simplex noise — Ashima Arts / Stefan Gustavson (MIT).
|
||
vec3 mod289(vec3 x) { return x - floor(x * (1.0 / 289.0)) * 289.0; }
|
||
vec4 mod289(vec4 x) { return x - floor(x * (1.0 / 289.0)) * 289.0; }
|
||
vec4 permute(vec4 x) { return mod289(((x * 34.0) + 1.0) * x); }
|
||
vec4 taylorInvSqrt(vec4 r) { return 1.79284291400159 - 0.85373472095314 * r; }
|
||
|
||
float snoise(vec3 v) {
|
||
const vec2 C = vec2(1.0 / 6.0, 1.0 / 3.0);
|
||
const vec4 D = vec4(0.0, 0.5, 1.0, 2.0);
|
||
|
||
vec3 i = floor(v + dot(v, C.yyy));
|
||
vec3 x0 = v - i + dot(i, C.xxx);
|
||
|
||
vec3 g = step(x0.yzx, x0.xyz);
|
||
vec3 l = 1.0 - g;
|
||
vec3 i1 = min(g.xyz, l.zxy);
|
||
vec3 i2 = max(g.xyz, l.zxy);
|
||
|
||
vec3 x1 = x0 - i1 + C.xxx;
|
||
vec3 x2 = x0 - i2 + C.yyy;
|
||
vec3 x3 = x0 - D.yyy;
|
||
|
||
i = mod289(i);
|
||
vec4 p = permute(permute(permute(
|
||
i.z + vec4(0.0, i1.z, i2.z, 1.0)) +
|
||
i.y + vec4(0.0, i1.y, i2.y, 1.0)) +
|
||
i.x + vec4(0.0, i1.x, i2.x, 1.0));
|
||
|
||
float n_ = 0.142857142857;
|
||
vec3 ns = n_ * D.wyz - D.xzx;
|
||
|
||
vec4 j = p - 49.0 * floor(p * ns.z * ns.z);
|
||
|
||
vec4 x_ = floor(j * ns.z);
|
||
vec4 y_ = floor(j - 7.0 * x_);
|
||
|
||
vec4 x = x_ * ns.x + ns.yyyy;
|
||
vec4 y = y_ * ns.x + ns.yyyy;
|
||
vec4 h = 1.0 - abs(x) - abs(y);
|
||
|
||
vec4 b0 = vec4(x.xy, y.xy);
|
||
vec4 b1 = vec4(x.zw, y.zw);
|
||
|
||
vec4 s0 = floor(b0) * 2.0 + 1.0;
|
||
vec4 s1 = floor(b1) * 2.0 + 1.0;
|
||
vec4 sh = -step(h, vec4(0.0));
|
||
|
||
vec4 a0 = b0.xzyw + s0.xzyw * sh.xxyy;
|
||
vec4 a1 = b1.xzyw + s1.xzyw * sh.zzww;
|
||
|
||
vec3 p0 = vec3(a0.xy, h.x);
|
||
vec3 p1 = vec3(a0.zw, h.y);
|
||
vec3 p2 = vec3(a1.xy, h.z);
|
||
vec3 p3 = vec3(a1.zw, h.w);
|
||
|
||
vec4 norm = taylorInvSqrt(vec4(dot(p0, p0), dot(p1, p1), dot(p2, p2), dot(p3, p3)));
|
||
p0 *= norm.x;
|
||
p1 *= norm.y;
|
||
p2 *= norm.z;
|
||
p3 *= norm.w;
|
||
|
||
vec4 m = max(0.6 - vec4(dot(x0, x0), dot(x1, x1), dot(x2, x2), dot(x3, x3)), 0.0);
|
||
m = m * m;
|
||
return 42.0 * dot(m * m, vec4(dot(p0, x0), dot(p1, x1), dot(p2, x2), dot(p3, x3)));
|
||
}
|
||
|
||
// Full-saturation, full-value HSV: only the hue ramp survives.
|
||
vec3 hue2rgb(float h) {
|
||
return clamp(abs(mod(h * 6.0 + vec3(0.0, 4.0, 2.0), 6.0) - 3.0) - 1.0, 0.0, 1.0);
|
||
}
|
||
|
||
// Piecewise-linear ramp across the four palette stops.
|
||
vec3 ramp(float v) {
|
||
float x = clamp(v, 0.0, 1.0) * 3.0;
|
||
vec3 c = mix(uPal0, uPal1, clamp(x, 0.0, 1.0));
|
||
c = mix(c, uPal2, clamp(x - 1.0, 0.0, 1.0));
|
||
c = mix(c, uPal3, clamp(x - 2.0, 0.0, 1.0));
|
||
return c;
|
||
}
|
||
|
||
void main() {
|
||
// One sample per cell, taken at the cell centre.
|
||
vec2 cell = floor(gl_FragCoord.xy / uCell);
|
||
vec2 centre = (cell + 0.5) * uCell;
|
||
|
||
vec2 st = centre / uRes;
|
||
st.x *= uRes.x / uRes.y;
|
||
|
||
float hue = abs(snoise(vec3(st * uFreq, uTime * uSpeed)));
|
||
vec3 field = hue2rgb(hue);
|
||
|
||
// The gamma is the whole look: it crushes the midtones so most of the frame
|
||
// stays background and only the crests of the field open up.
|
||
float gray = pow(dot(field, vec3(0.2126, 0.7152, 0.0722)), uGamma);
|
||
float v = clamp(gray + uBias, 0.0, 1.0);
|
||
|
||
float radius = v * 0.5 * uCell;
|
||
float d = length(gl_FragCoord.xy - centre);
|
||
float mask = 1.0 - smoothstep(radius - 1.0, radius + 1.0, d);
|
||
|
||
gl_FragColor = vec4(mix(uBg, ramp(v), mask), 1.0);
|
||
}
|
||
`;
|
||
|
||
var uni = {};
|
||
var ready = false;
|
||
|
||
function compile(type, src) {
|
||
var sh = gl.createShader(type);
|
||
gl.shaderSource(sh, src);
|
||
gl.compileShader(sh);
|
||
if (!gl.getShaderParameter(sh, gl.COMPILE_STATUS)) {
|
||
throw new Error("halftone-field shader: " + gl.getShaderInfoLog(sh));
|
||
}
|
||
return sh;
|
||
}
|
||
|
||
if (gl) {
|
||
var prog = gl.createProgram();
|
||
gl.attachShader(prog, compile(gl.VERTEX_SHADER, VERT));
|
||
gl.attachShader(prog, compile(gl.FRAGMENT_SHADER, FRAG));
|
||
gl.linkProgram(prog);
|
||
if (!gl.getProgramParameter(prog, gl.LINK_STATUS)) {
|
||
throw new Error("halftone-field link: " + gl.getProgramInfoLog(prog));
|
||
}
|
||
gl.useProgram(prog);
|
||
|
||
var buf = gl.createBuffer();
|
||
gl.bindBuffer(gl.ARRAY_BUFFER, buf);
|
||
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array([-1, -1, 3, -1, -1, 3]), gl.STATIC_DRAW);
|
||
var loc = gl.getAttribLocation(prog, "aPos");
|
||
gl.enableVertexAttribArray(loc);
|
||
gl.vertexAttribPointer(loc, 2, gl.FLOAT, false, 0, 0);
|
||
|
||
[
|
||
"uRes",
|
||
"uTime",
|
||
"uFreq",
|
||
"uSpeed",
|
||
"uCell",
|
||
"uGamma",
|
||
"uBias",
|
||
"uPal0",
|
||
"uPal1",
|
||
"uPal2",
|
||
"uPal3",
|
||
"uBg",
|
||
].forEach(function (n) {
|
||
uni[n] = gl.getUniformLocation(prog, n);
|
||
});
|
||
|
||
gl.viewport(0, 0, W, H);
|
||
gl.uniform2f(uni.uRes, W, H);
|
||
gl.uniform1f(uni.uFreq, FREQ);
|
||
gl.uniform1f(uni.uSpeed, SPEED);
|
||
gl.uniform1f(uni.uCell, CELL);
|
||
gl.uniform1f(uni.uGamma, GAMMA);
|
||
gl.uniform1f(uni.uBias, BIAS);
|
||
gl.uniform3f(uni.uPal0, PAL[0][0], PAL[0][1], PAL[0][2]);
|
||
gl.uniform3f(uni.uPal1, PAL[1][0], PAL[1][1], PAL[1][2]);
|
||
gl.uniform3f(uni.uPal2, PAL[2][0], PAL[2][1], PAL[2][2]);
|
||
gl.uniform3f(uni.uPal3, PAL[3][0], PAL[3][1], PAL[3][2]);
|
||
gl.uniform3f(uni.uBg, BG[0], BG[1], BG[2]);
|
||
ready = true;
|
||
}
|
||
|
||
// Every frame is computed from t alone — the field is a closed-form
|
||
// function of position and time. No accumulator, no clock, no PRNG.
|
||
// `quantize` snaps t to a 30fps grid for the stepped LED-wall look.
|
||
function draw(t) {
|
||
if (!ready) return;
|
||
gl.uniform1f(uni.uTime, QUANTIZE ? Math.floor(t * 30) / 30 : t);
|
||
gl.drawArrays(gl.TRIANGLES, 0, 3);
|
||
gl.flush();
|
||
}
|
||
|
||
window.__timelines = window.__timelines || {};
|
||
var tl = gsap.timeline({ paused: true });
|
||
|
||
// The canvas is repainted from a property SETTER, not from onUpdate:
|
||
// gsap's seek(t) suppresses events by default, so an onUpdate callback
|
||
// silently never fires on a scrub and the canvas freezes on frame 0.
|
||
// Tweened values are always written during render, suppressed or not,
|
||
// so this fires on every seek — and hands us the frame time directly.
|
||
var driver = { _t: 0 };
|
||
Object.defineProperty(driver, "t", {
|
||
get: function () {
|
||
return this._t;
|
||
},
|
||
set: function (v) {
|
||
this._t = v;
|
||
draw(v);
|
||
},
|
||
});
|
||
tl.to(driver, { t: DUR, duration: DUR, ease: "none" }, 0);
|
||
window.__timelines["halftone-field"] = tl;
|
||
|
||
draw(0);
|
||
})();
|
||
</script>
|
||
</body>
|
||
</html>
|
||
```
|
||
|
||
</Accordion>
|
||
|
||
{/* hf:generated-footer */}
|
||
|
||
Tagged `webgl` `shader` `background` `texture` `halftone`.
|
||
|
||
## Related topics
|
||
|
||
- [Browse the complete Catalog](/catalog)
|
||
- [Add assets and Catalog items in Studio](/studio/assets-and-blocks)
|
||
- [Build a richer composition](/go-further)
|