From 4606e89846745b6eadc9e16522d95cb308b76235 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miguel=20=C3=81ngel?= Date: Wed, 23 Sep 2026 12:37:00 -0400 Subject: [PATCH 1/7] feat(registry): cocoa powder dissolving in milk, with an optional maths overlay --- .../powder-dissolve/powder-dissolve.html | 578 ++++++++++++++++++ .../blocks/powder-dissolve/registry-item.json | 68 +++ 2 files changed, 646 insertions(+) create mode 100644 registry/blocks/powder-dissolve/powder-dissolve.html create mode 100644 registry/blocks/powder-dissolve/registry-item.json diff --git a/registry/blocks/powder-dissolve/powder-dissolve.html b/registry/blocks/powder-dissolve/powder-dissolve.html new file mode 100644 index 0000000000..626df0a5f5 --- /dev/null +++ b/registry/blocks/powder-dissolve/powder-dissolve.html @@ -0,0 +1,578 @@ + + + + + + + + + + +
+ + + + +
+
+ + + + diff --git a/registry/blocks/powder-dissolve/registry-item.json b/registry/blocks/powder-dissolve/registry-item.json new file mode 100644 index 0000000000..6ff3cc909e --- /dev/null +++ b/registry/blocks/powder-dissolve/registry-item.json @@ -0,0 +1,68 @@ +{ + "$schema": "https://hyperframes.heygen.com/schema/registry-item.json", + "name": "powder-dissolve", + "type": "hyperframes:block", + "title": "Powder Dissolve", + "description": "A spoonful of cocoa powder lands on milk seen from above, floats dry, wets through from the edges and along cracks, then blooms out in fingered, marbled clouds until the milk is one even chocolate tint. Calm and slow, easing to stillness. Turn on maths to show the equations behind each phase with live values. Every frame is solved from time alone, so any frame renders on its own.", + "stability": "experimental", + "dimensions": { + "width": 1920, + "height": 1080 + }, + "duration": 10, + "tags": [ + "powder", + "dissolve", + "milk", + "cocoa", + "diffusion", + "ink", + "liquid", + "relaxing", + "asmr", + "webgl", + "background" + ], + "variables": [ + { + "id": "powder", + "type": "color", + "label": "Powder colour", + "default": "#7a4a2e" + }, + { + "id": "milk", + "type": "color", + "label": "Milk colour", + "default": "#f4efe6" + }, + { + "id": "dissolve", + "type": "number", + "label": "Dissolve duration", + "default": 8, + "min": 3, + "max": 30, + "step": 0.5, + "unit": "s" + }, + { + "id": "maths", + "type": "boolean", + "label": "Show the maths", + "description": "A whiteboard panel of the equations driving each phase, with live values.", + "default": false + } + ], + "files": [ + { + "path": "powder-dissolve.html", + "target": "compositions/powder-dissolve.html", + "type": "hyperframes:composition" + } + ], + "preview": { + "video": "https://static.heygen.ai/hyperframes-oss/docs/images/catalog/blocks/powder-dissolve.mp4", + "poster": "https://static.heygen.ai/hyperframes-oss/docs/images/catalog/blocks/powder-dissolve.png" + } +} From d51dc2aaee93e36e1473553373d8cc4ec64028c6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miguel=20=C3=81ngel?= Date: Wed, 23 Sep 2026 12:38:46 -0400 Subject: [PATCH 2/7] refactor(registry): powder-dissolve maths text shows exact landing time and crack warp --- registry/blocks/powder-dissolve/powder-dissolve.html | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/registry/blocks/powder-dissolve/powder-dissolve.html b/registry/blocks/powder-dissolve/powder-dissolve.html index 626df0a5f5..bf63b6f6f6 100644 --- a/registry/blocks/powder-dissolve/powder-dissolve.html +++ b/registry/blocks/powder-dissolve/powder-dissolve.html @@ -54,9 +54,6 @@ line-height: 1.3; font-variant-numeric: tabular-nums; } - #pd-maths[hidden] { - display: none; - } #pd-maths .pd-clock { font-family: Inter, sans-serif; font-size: 19px; @@ -408,6 +405,7 @@ // Phase curves, all pure functions of t. Landing takes under a second whatever the // dissolve length; everything after it is a fraction of the dissolve. var LAND = Math.min(0.9, DISSOLVE * 0.12); + var LAND_TXT = String(+LAND.toFixed(2)); // exact value the curves use, no trailing zeros var BATCH_AT = [0.08, 0.3, 0.55]; // release start of each batch, fraction of the dissolve var mass = new Float32Array(3); var spread = new Float32Array(3); @@ -424,7 +422,7 @@ { label: "Landing", from: 0, - tex: "L(t) = 1 − (1 − t / " + LAND.toFixed(1) + ")3", + tex: "L(t) = 1 − (1 − t / " + LAND_TXT + ")3", val: function (ph) { return ph.land.toFixed(2); }, @@ -447,7 +445,7 @@ { label: "Crust cracks", from: atS(0.13), // W(s) reaches 0.15 here - tex: "crack ⇔ F2 − F1 < 0.06 (Worley cells of 10x, once W > 0.15)", + tex: "crack ⇔ F2 − F1 < 0.06 (Worley cells of 10x + 1.5 fbm(8x), once W > 0.15)", }, { label: "Diffusion (exact Gaussian spread)", @@ -504,11 +502,11 @@ "t = " + ph.t.toFixed(2) + " s · s = (t − " + - LAND.toFixed(1) + + LAND_TXT + ") / (" + DISSOLVE + " − " + - LAND.toFixed(1) + + LAND_TXT + ") = " + ph.s.toFixed(2); EQS.forEach(function (eq, i) { From 0d21b6a36fffdef0c8add1bb1fed40d8b897320d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miguel=20=C3=81ngel?= Date: Wed, 23 Sep 2026 13:06:26 -0400 Subject: [PATCH 3/7] fix(registry): powder-dissolve maths panel is readable at 1080p and follows the live phase --- .../powder-dissolve/powder-dissolve.html | 62 +++++++++++-------- 1 file changed, 35 insertions(+), 27 deletions(-) diff --git a/registry/blocks/powder-dissolve/powder-dissolve.html b/registry/blocks/powder-dissolve/powder-dissolve.html index bf63b6f6f6..fe0dcbf685 100644 --- a/registry/blocks/powder-dissolve/powder-dissolve.html +++ b/registry/blocks/powder-dissolve/powder-dissolve.html @@ -42,38 +42,39 @@ } #pd-maths { position: absolute; - left: 64px; - top: 72px; - width: 700px; - padding: 28px 34px 30px; + left: 56px; + top: 56px; + width: 800px; + padding: 26px 34px 28px; border-radius: 18px; - background: rgba(250, 247, 241, 0.78); + background: rgba(250, 247, 241, 0.92); color: #2a1a10; font-family: "STIX Two Text", Georgia, serif; - font-size: 25px; - line-height: 1.3; + font-size: 28px; + line-height: 1.28; font-variant-numeric: tabular-nums; } #pd-maths .pd-clock { font-family: Inter, sans-serif; - font-size: 19px; + font-size: 20px; font-weight: 600; - letter-spacing: 0.02em; - margin-bottom: 14px; + margin-bottom: 8px; } #pd-maths .pd-eq { - margin-top: 13px; + margin-top: 14px; } #pd-maths .pd-label { font-family: Inter, sans-serif; - font-size: 13px; + font-size: 18px; font-weight: 600; - letter-spacing: 0.12em; + letter-spacing: 0.08em; text-transform: uppercase; - color: #8a5a3a; + color: #7a3a18; } #pd-maths .pd-val { - color: #a0461e; + color: #6e2f12; + letter-spacing: 0; + text-transform: none; } @@ -134,7 +135,7 @@ var MILK = hexRgb(vars.milk, "#f4efe6"); var DISSOLVE = clampNum(vars.dissolve, 3, 30, 8); var MATHS = vars.maths === true || vars.maths === "true"; - var OFFSET_X = MATHS ? 0.3 : 0; // clear the left of the frame for the panel + var OFFSET_X = MATHS ? 0.4 : 0; // clear the left of the frame for the panel function mulberry32(seed) { return function () { @@ -411,7 +412,8 @@ var spread = new Float32Array(3); // ── Maths panel ──────────────────────────────────────────────────── // Every line is an expression the shader or draw() above evaluates, with its live - // value from the same phase curves. Each fades in as its phase starts (seconds). + // value from the same phase curves. Listed in phase order: the live phase is full ink, + // finished phases dim, future phases stay hidden (their space is reserved, so no reflow). function atS(sv) { return LAND + sv * (DISSOLVE - LAND); } @@ -437,16 +439,11 @@ { label: "Wetting front", from: atS(0), - tex: "W(s) = smoothstep(0, 0.55, s), d = h − erosion
wet = smoothstep(0, 0.08, 1.2W − 0.12 − 1.1(d − 0.45) + 0.4(fbm(9x) − ½))", + tex: "W(s) = smoothstep(0, 0.55, s), d = h − erosion
wet = smoothstep(0, 0.08, 1.2W − 0.12
  − 1.1(d − 0.45) + 0.4(fbm(9x) − ½))", val: function (ph) { return "W = " + pct(ph.wet); }, }, - { - label: "Crust cracks", - from: atS(0.13), // W(s) reaches 0.15 here - tex: "crack ⇔ F2 − F1 < 0.06 (Worley cells of 10x + 1.5 fbm(8x), once W > 0.15)", - }, { label: "Diffusion (exact Gaussian spread)", from: atS(BATCH_AT[0]), @@ -457,6 +454,11 @@ return "r = " + Math.round(ph.radius * H) + " px"; }, }, + { + label: "Crust cracks", + from: atS(0.13), // W(s) reaches 0.15 here + tex: "crack ⇔ F2 − F1 < 0.06, once W > 0.15
F1,2: nearest Worley distances of 10x + 1.5 fbm(8x)", + }, { label: "Fingers", from: atS(0.16), @@ -467,7 +469,7 @@ }, { label: "Tint mix (Beer–Lambert)", - from: atS(0.4), + from: atS(0.55), // M passes 5 % here; the curve starts at s = 0.4 tex: "M = smoothstep(0.4, 1, s)1.6, c ← (1 − M)c + 0.6M" + "
colour = milk · e−0.55 c (−ln powder)", @@ -476,6 +478,12 @@ }, }, ]; + EQS.forEach(function (eq) { + eq.until = Infinity; // a phase stays live until a later one starts + EQS.forEach(function (other) { + if (other.from > eq.from) eq.until = Math.min(eq.until, other.from); + }); + }); var mathsEl = document.getElementById("pd-maths"); var clockEl, rows; if (MATHS) { @@ -510,9 +518,9 @@ ") = " + ph.s.toFixed(2); EQS.forEach(function (eq, i) { - rows[i].el.style.opacity = (0.08 + 0.92 * smooth(eq.from, eq.from + 0.5, ph.t)).toFixed( - 3, - ); + var shown = smooth(eq.from, eq.from + 0.4, ph.t); + var done = eq.until === Infinity ? 0 : smooth(eq.until, eq.until + 0.4, ph.t); + rows[i].el.style.opacity = (shown * (1 - 0.62 * done)).toFixed(3); if (eq.val) rows[i].val.textContent = eq.val(ph); }); } From 2ac625467473ae27cbf39e25b5c37368f1c695af Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miguel=20=C3=81ngel?= Date: Wed, 23 Sep 2026 13:46:42 -0400 Subject: [PATCH 4/7] fix(registry): powder-dissolve maths uses only glyphs the render font has --- registry/blocks/powder-dissolve/powder-dissolve.html | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/registry/blocks/powder-dissolve/powder-dissolve.html b/registry/blocks/powder-dissolve/powder-dissolve.html index fe0dcbf685..e387a8f9c3 100644 --- a/registry/blocks/powder-dissolve/powder-dissolve.html +++ b/registry/blocks/powder-dissolve/powder-dissolve.html @@ -449,7 +449,7 @@ from: atS(BATCH_AT[0]), tex: "c(x, t) = 5 Σb mb Σi wi σi² / (σi² + vb) e−|x−ci|² / 2(σi² + vb)" + - "
front radius r = √(σ² + v0)", + "
front radius r = (σ² + v0)½", val: function (ph) { return "r = " + Math.round(ph.radius * H) + " px"; }, @@ -457,7 +457,7 @@ { label: "Crust cracks", from: atS(0.13), // W(s) reaches 0.15 here - tex: "crack ⇔ F2 − F1 < 0.06, once W > 0.15
F1,2: nearest Worley distances of 10x + 1.5 fbm(8x)", + tex: "crack where F2 − F1 < 0.06, once W > 0.15
F1,2: nearest Worley distances of 10x + 1.5 fbm(8x)", }, { label: "Fingers", @@ -471,7 +471,7 @@ label: "Tint mix (Beer–Lambert)", from: atS(0.55), // M passes 5 % here; the curve starts at s = 0.4 tex: - "M = smoothstep(0.4, 1, s)1.6, c ← (1 − M)c + 0.6M" + + "M = smoothstep(0.4, 1, s)1.6, c := (1 − M)c + 0.6M" + "
colour = milk · e−0.55 c (−ln powder)", val: function (ph) { return "M = " + pct(ph.mix); From be93b48883ded1df131fad39dd0d2ca19f9072a7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miguel=20=C3=81ngel?= Date: Wed, 23 Sep 2026 15:22:17 -0400 Subject: [PATCH 5/7] fix(registry): powder-dissolve renders its maths panel the same on every worker --- .../powder-dissolve/powder-dissolve.html | 66 ++++++++++++------- .../blocks/powder-dissolve/registry-item.json | 3 +- 2 files changed, 46 insertions(+), 23 deletions(-) diff --git a/registry/blocks/powder-dissolve/powder-dissolve.html b/registry/blocks/powder-dissolve/powder-dissolve.html index e387a8f9c3..4c8292503e 100644 --- a/registry/blocks/powder-dissolve/powder-dissolve.html +++ b/registry/blocks/powder-dissolve/powder-dissolve.html @@ -12,6 +12,10 @@ its live value, appearing as its phase starts). The host clip may run longer than `dissolve`; the tail holds the evenly tinted milk. + Fonts: no web-font link on purpose. The renderer and Studio inject deterministic faces for + every CSS font-family; a network stylesheet declared after them would win whenever it + arrived in time, so early frames could differ between renders. + Determinism: every frame is a pure function of time. Clump layout comes from a fixed seed, grain and noise from position hashes, and tint spread from the closed-form solution of diffusion (a Gaussian convolved with a Gaussian), so frame N never reads frame N-1. @@ -20,10 +24,6 @@ -