Files
hyperframes/docs/public/catalog/blocks/halftone-field.json
Miguel Ángel 9734578e60 feat(registry): bring back the video-primitive moves (#3169)
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.
2026-08-10 18:46:03 -04:00

1 line
13 KiB
JSON

{"html":"<!doctype html>\n<html lang=\"en\">\n <head>\n <meta charset=\"utf-8\" />\n <meta name=\"viewport\" content=\"width=1920, height=1080\" />\n <title>Halftone Field</title>\n <script src=\"https://cdn.jsdelivr.net/npm/gsap@3.14.2/dist/gsap.min.js\"></script>\n <style>\n *,\n *::before,\n *::after {\n margin: 0;\n padding: 0;\n box-sizing: border-box;\n }\n body {\n background: #000;\n overflow: hidden;\n }\n #hlf-root {\n position: relative;\n width: 1920px;\n height: 1080px;\n overflow: hidden;\n }\n #hlf-backdrop {\n position: absolute;\n inset: 0;\n background: #000000;\n }\n #hlf-canvas {\n position: absolute;\n top: 0;\n left: 0;\n width: 1920px;\n height: 1080px;\n }\n </style>\n </head>\n <body>\n <div\n id=\"hlf-root\"\n data-composition-id=\"halftone-field\"\n data-root=\"true\"\n data-width=\"1920\"\n data-height=\"1080\"\n data-start=\"0\"\n data-duration=\"10\"\n data-composition-variables='[\n {\"id\":\"frequency\",\"type\":\"number\",\"label\":\"Field frequency\",\"default\":2,\"min\":1,\"max\":10,\"step\":0.1},\n {\"id\":\"speed\",\"type\":\"number\",\"label\":\"Flow speed\",\"default\":5,\"min\":0,\"max\":10,\"step\":0.1},\n {\"id\":\"cellSize\",\"type\":\"number\",\"label\":\"Cell size\",\"default\":54,\"min\":1,\"max\":100,\"step\":1},\n {\"id\":\"gamma\",\"type\":\"number\",\"label\":\"Gamma (midtone crush)\",\"default\":12,\"min\":1,\"max\":20,\"step\":0.1},\n {\"id\":\"paletteBias\",\"type\":\"number\",\"label\":\"Palette bias\",\"default\":2,\"min\":0,\"max\":20,\"step\":0.5},\n {\"id\":\"palette1\",\"type\":\"color\",\"label\":\"Palette stop 1 (lows)\",\"default\":\"#00AAFF\"},\n {\"id\":\"palette2\",\"type\":\"color\",\"label\":\"Palette stop 2\",\"default\":\"#C2FF67\"},\n {\"id\":\"palette3\",\"type\":\"color\",\"label\":\"Palette stop 3\",\"default\":\"#6600FF\"},\n {\"id\":\"palette4\",\"type\":\"color\",\"label\":\"Palette stop 4 (crests)\",\"default\":\"#000000\"},\n {\"id\":\"background\",\"type\":\"color\",\"label\":\"Background\",\"default\":\"#000000\"},\n {\"id\":\"quantize\",\"type\":\"boolean\",\"label\":\"Quantise to 30fps (stepped)\",\"default\":false}\n ]'\n >\n <div id=\"hlf-backdrop\"></div>\n <canvas id=\"hlf-canvas\" width=\"1920\" height=\"1080\"></canvas>\n\n <!-- Driver clip: gives HyperFrames a timed element to own on track 0. -->\n <div\n id=\"hlf-drv\"\n class=\"clip\"\n data-start=\"0\"\n data-duration=\"10\"\n data-track-index=\"0\"\n style=\"position: absolute; width: 1px; height: 1px; opacity: 0; pointer-events: none\"\n ></div>\n </div>\n\n <script>\n (function () {\n var DUR = 10;\n var W = 1920;\n var H = 1080;\n\n var V = (window.__hyperframes && window.__hyperframes.getVariables()) || {};\n var CS = getComputedStyle(document.getElementById(\"hlf-root\"));\n\n // The runtime defines every declared variable as `--<id>` on the root,\n // so a host stylesheet can override one there too. Read the custom\n // property first, fall back to the declared value when it is unset.\n function raw(id) {\n var css = CS.getPropertyValue(\"--\" + id.toLowerCase()).trim();\n return css !== \"\" ? css : V[id];\n }\n function num(id, fallback) {\n var n = parseFloat(raw(id));\n return isFinite(n) ? n : fallback;\n }\n function bool(id) {\n var v = raw(id);\n return v === true || /^(true|1|on|yes)$/i.test(String(v));\n }\n // #RGB / #RRGGBB -> [r, g, b] in 0..1. Anything else falls back.\n function rgb(id, fallback) {\n var s = String(raw(id) || \"\").trim();\n var m = /^#([0-9a-f]{3}|[0-9a-f]{6})$/i.exec(s);\n if (!m) s = fallback;\n else s = \"#\" + m[1];\n var hex = s.slice(1);\n if (hex.length === 3) {\n hex = hex[0] + hex[0] + hex[1] + hex[1] + hex[2] + hex[2];\n }\n var n = parseInt(hex, 16);\n return [((n >> 16) & 255) / 255, ((n >> 8) & 255) / 255, (n & 255) / 255];\n }\n // The reference exposes 1-10 / 1-100 / 1-20 dials and maps them onto\n // the ranges the shader actually wants.\n function mapLinear(x, a1, a2, b1, b2) {\n return b1 + ((x - a1) * (b2 - b1)) / (a2 - a1);\n }\n\n var FREQ = mapLinear(num(\"frequency\", 2), 1, 10, 0.3, 6);\n var SPEED = num(\"speed\", 5) * 0.05;\n var CELL = mapLinear(num(\"cellSize\", 54), 1, 100, 6, 60);\n var GAMMA = mapLinear(num(\"gamma\", 12), 1, 20, 0.5, 8);\n var BIAS = num(\"paletteBias\", 2) * 0.05;\n var PAL = [\n rgb(\"palette1\", \"#00AAFF\"),\n rgb(\"palette2\", \"#C2FF67\"),\n rgb(\"palette3\", \"#6600FF\"),\n rgb(\"palette4\", \"#000000\"),\n ];\n var BG = rgb(\"background\", \"#000000\");\n var QUANTIZE = bool(\"quantize\");\n\n document.getElementById(\"hlf-backdrop\").style.background =\n \"rgb(\" +\n BG.map(function (c) {\n return Math.round(c * 255);\n }).join(\",\") +\n \")\";\n\n var canvas = document.getElementById(\"hlf-canvas\");\n var gl =\n canvas.getContext(\"webgl\", {\n alpha: false,\n antialias: false,\n depth: false,\n stencil: false,\n preserveDrawingBuffer: true,\n powerPreference: \"high-performance\",\n }) ||\n canvas.getContext(\"experimental-webgl\", {\n alpha: false,\n preserveDrawingBuffer: true,\n });\n\n var VERT = \"attribute vec2 aPos;\\nvoid main() { gl_Position = vec4(aPos, 0.0, 1.0); }\";\n\n // Single pass. The reference renders the colour field into a target and\n // samples it at each cell centre, but the field is analytic, so\n // evaluating it at the cell centre here is the same value without the\n // round trip through a texture.\n var FRAG = `\nprecision highp float;\nuniform vec2 uRes;\nuniform float uTime;\nuniform float uFreq;\nuniform float uSpeed;\nuniform float uCell;\nuniform float uGamma;\nuniform float uBias;\nuniform vec3 uPal0;\nuniform vec3 uPal1;\nuniform vec3 uPal2;\nuniform vec3 uPal3;\nuniform vec3 uBg;\n\n// 3D simplex noise — Ashima Arts / Stefan Gustavson (MIT).\nvec3 mod289(vec3 x) { return x - floor(x * (1.0 / 289.0)) * 289.0; }\nvec4 mod289(vec4 x) { return x - floor(x * (1.0 / 289.0)) * 289.0; }\nvec4 permute(vec4 x) { return mod289(((x * 34.0) + 1.0) * x); }\nvec4 taylorInvSqrt(vec4 r) { return 1.79284291400159 - 0.85373472095314 * r; }\n\nfloat snoise(vec3 v) {\n const vec2 C = vec2(1.0 / 6.0, 1.0 / 3.0);\n const vec4 D = vec4(0.0, 0.5, 1.0, 2.0);\n\n vec3 i = floor(v + dot(v, C.yyy));\n vec3 x0 = v - i + dot(i, C.xxx);\n\n vec3 g = step(x0.yzx, x0.xyz);\n vec3 l = 1.0 - g;\n vec3 i1 = min(g.xyz, l.zxy);\n vec3 i2 = max(g.xyz, l.zxy);\n\n vec3 x1 = x0 - i1 + C.xxx;\n vec3 x2 = x0 - i2 + C.yyy;\n vec3 x3 = x0 - D.yyy;\n\n i = mod289(i);\n vec4 p = permute(permute(permute(\n i.z + vec4(0.0, i1.z, i2.z, 1.0)) +\n i.y + vec4(0.0, i1.y, i2.y, 1.0)) +\n i.x + vec4(0.0, i1.x, i2.x, 1.0));\n\n float n_ = 0.142857142857;\n vec3 ns = n_ * D.wyz - D.xzx;\n\n vec4 j = p - 49.0 * floor(p * ns.z * ns.z);\n\n vec4 x_ = floor(j * ns.z);\n vec4 y_ = floor(j - 7.0 * x_);\n\n vec4 x = x_ * ns.x + ns.yyyy;\n vec4 y = y_ * ns.x + ns.yyyy;\n vec4 h = 1.0 - abs(x) - abs(y);\n\n vec4 b0 = vec4(x.xy, y.xy);\n vec4 b1 = vec4(x.zw, y.zw);\n\n vec4 s0 = floor(b0) * 2.0 + 1.0;\n vec4 s1 = floor(b1) * 2.0 + 1.0;\n vec4 sh = -step(h, vec4(0.0));\n\n vec4 a0 = b0.xzyw + s0.xzyw * sh.xxyy;\n vec4 a1 = b1.xzyw + s1.xzyw * sh.zzww;\n\n vec3 p0 = vec3(a0.xy, h.x);\n vec3 p1 = vec3(a0.zw, h.y);\n vec3 p2 = vec3(a1.xy, h.z);\n vec3 p3 = vec3(a1.zw, h.w);\n\n vec4 norm = taylorInvSqrt(vec4(dot(p0, p0), dot(p1, p1), dot(p2, p2), dot(p3, p3)));\n p0 *= norm.x;\n p1 *= norm.y;\n p2 *= norm.z;\n p3 *= norm.w;\n\n vec4 m = max(0.6 - vec4(dot(x0, x0), dot(x1, x1), dot(x2, x2), dot(x3, x3)), 0.0);\n m = m * m;\n return 42.0 * dot(m * m, vec4(dot(p0, x0), dot(p1, x1), dot(p2, x2), dot(p3, x3)));\n}\n\n// Full-saturation, full-value HSV: only the hue ramp survives.\nvec3 hue2rgb(float h) {\n return clamp(abs(mod(h * 6.0 + vec3(0.0, 4.0, 2.0), 6.0) - 3.0) - 1.0, 0.0, 1.0);\n}\n\n// Piecewise-linear ramp across the four palette stops.\nvec3 ramp(float v) {\n float x = clamp(v, 0.0, 1.0) * 3.0;\n vec3 c = mix(uPal0, uPal1, clamp(x, 0.0, 1.0));\n c = mix(c, uPal2, clamp(x - 1.0, 0.0, 1.0));\n c = mix(c, uPal3, clamp(x - 2.0, 0.0, 1.0));\n return c;\n}\n\nvoid main() {\n // One sample per cell, taken at the cell centre.\n vec2 cell = floor(gl_FragCoord.xy / uCell);\n vec2 centre = (cell + 0.5) * uCell;\n\n vec2 st = centre / uRes;\n st.x *= uRes.x / uRes.y;\n\n float hue = abs(snoise(vec3(st * uFreq, uTime * uSpeed)));\n vec3 field = hue2rgb(hue);\n\n // The gamma is the whole look: it crushes the midtones so most of the frame\n // stays background and only the crests of the field open up.\n float gray = pow(dot(field, vec3(0.2126, 0.7152, 0.0722)), uGamma);\n float v = clamp(gray + uBias, 0.0, 1.0);\n\n float radius = v * 0.5 * uCell;\n float d = length(gl_FragCoord.xy - centre);\n float mask = 1.0 - smoothstep(radius - 1.0, radius + 1.0, d);\n\n gl_FragColor = vec4(mix(uBg, ramp(v), mask), 1.0);\n}\n`;\n\n var uni = {};\n var ready = false;\n\n function compile(type, src) {\n var sh = gl.createShader(type);\n gl.shaderSource(sh, src);\n gl.compileShader(sh);\n if (!gl.getShaderParameter(sh, gl.COMPILE_STATUS)) {\n throw new Error(\"halftone-field shader: \" + gl.getShaderInfoLog(sh));\n }\n return sh;\n }\n\n if (gl) {\n var prog = gl.createProgram();\n gl.attachShader(prog, compile(gl.VERTEX_SHADER, VERT));\n gl.attachShader(prog, compile(gl.FRAGMENT_SHADER, FRAG));\n gl.linkProgram(prog);\n if (!gl.getProgramParameter(prog, gl.LINK_STATUS)) {\n throw new Error(\"halftone-field link: \" + gl.getProgramInfoLog(prog));\n }\n gl.useProgram(prog);\n\n var buf = gl.createBuffer();\n gl.bindBuffer(gl.ARRAY_BUFFER, buf);\n gl.bufferData(gl.ARRAY_BUFFER, new Float32Array([-1, -1, 3, -1, -1, 3]), gl.STATIC_DRAW);\n var loc = gl.getAttribLocation(prog, \"aPos\");\n gl.enableVertexAttribArray(loc);\n gl.vertexAttribPointer(loc, 2, gl.FLOAT, false, 0, 0);\n\n [\n \"uRes\",\n \"uTime\",\n \"uFreq\",\n \"uSpeed\",\n \"uCell\",\n \"uGamma\",\n \"uBias\",\n \"uPal0\",\n \"uPal1\",\n \"uPal2\",\n \"uPal3\",\n \"uBg\",\n ].forEach(function (n) {\n uni[n] = gl.getUniformLocation(prog, n);\n });\n\n gl.viewport(0, 0, W, H);\n gl.uniform2f(uni.uRes, W, H);\n gl.uniform1f(uni.uFreq, FREQ);\n gl.uniform1f(uni.uSpeed, SPEED);\n gl.uniform1f(uni.uCell, CELL);\n gl.uniform1f(uni.uGamma, GAMMA);\n gl.uniform1f(uni.uBias, BIAS);\n gl.uniform3f(uni.uPal0, PAL[0][0], PAL[0][1], PAL[0][2]);\n gl.uniform3f(uni.uPal1, PAL[1][0], PAL[1][1], PAL[1][2]);\n gl.uniform3f(uni.uPal2, PAL[2][0], PAL[2][1], PAL[2][2]);\n gl.uniform3f(uni.uPal3, PAL[3][0], PAL[3][1], PAL[3][2]);\n gl.uniform3f(uni.uBg, BG[0], BG[1], BG[2]);\n ready = true;\n }\n\n // Every frame is computed from t alone — the field is a closed-form\n // function of position and time. No accumulator, no clock, no PRNG.\n // `quantize` snaps t to a 30fps grid for the stepped LED-wall look.\n function draw(t) {\n if (!ready) return;\n gl.uniform1f(uni.uTime, QUANTIZE ? Math.floor(t * 30) / 30 : t);\n gl.drawArrays(gl.TRIANGLES, 0, 3);\n gl.flush();\n }\n\n window.__timelines = window.__timelines || {};\n var tl = gsap.timeline({ paused: true });\n\n // The canvas is repainted from a property SETTER, not from onUpdate:\n // gsap's seek(t) suppresses events by default, so an onUpdate callback\n // silently never fires on a scrub and the canvas freezes on frame 0.\n // Tweened values are always written during render, suppressed or not,\n // so this fires on every seek — and hands us the frame time directly.\n var driver = { _t: 0 };\n Object.defineProperty(driver, \"t\", {\n get: function () {\n return this._t;\n },\n set: function (v) {\n this._t = v;\n draw(v);\n },\n });\n tl.to(driver, { t: DUR, duration: DUR, ease: \"none\" }, 0);\n window.__timelines[\"halftone-field\"] = tl;\n\n draw(0);\n })();\n </script>\n </body>\n</html>\n"}