From 0177dd1eb03766d54ee06920ce3a3c899c0204e8 Mon Sep 17 00:00:00 2001 From: AMAGI / Jun Yuri Date: Sat, 17 Oct 2026 11:39:53 -0700 Subject: [PATCH 01/42] feat(effects): add intra refresh params to DatamoshEffect Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_018attAihospyjqebPaUwNkn --- .changeset/datamosh-intra-refresh.md | 8 ++++ packages/effects/src/datamosh.ts | 62 ++++++++++++++++++++++++++-- packages/storybook/src/utils.ts | 11 +++++ 3 files changed, 77 insertions(+), 4 deletions(-) create mode 100644 .changeset/datamosh-intra-refresh.md diff --git a/.changeset/datamosh-intra-refresh.md b/.changeset/datamosh-intra-refresh.md new file mode 100644 index 000000000..b980cdd82 --- /dev/null +++ b/.changeset/datamosh-intra-refresh.md @@ -0,0 +1,8 @@ +--- +"@vfx-js/effects": minor +--- + +Add intra refresh params to `DatamoshEffect` so the mosh can heal block by +block like a real codec: `intraRate` (random refresh chance per block per +frame), `intraThreshold` (refresh blocks with bad motion matches), and +`intraPatchSize` (refresh in blobs of blocks instead of single blocks). diff --git a/packages/effects/src/datamosh.ts b/packages/effects/src/datamosh.ts index b780cd805..88e8d19e6 100644 --- a/packages/effects/src/datamosh.ts +++ b/packages/effects/src/datamosh.ts @@ -15,6 +15,7 @@ // ME cur vs prev -> mvRT (one fragment per block) // residual cur - mc(prev) -> resRT // decode intra ? cur : mc(accum, mv) + res -> accRT (persistent) +// (per-block intra refresh heals the mosh back to the source) // display accRT -> ctx.target // blit cur -> prevRT (persistent; next frame's reference) // @@ -40,6 +41,8 @@ void main() { // (px) that best matches the current block, into .rg of a float RT. // A small length bias breaks ties toward (0,0) so flat blocks don't latch // onto spurious large vectors. +// Also picks the block mode into .b (1 = intra): random refresh, or a +// poor best match, like an encoder falling back to intra. const FRAG_ME = `#version 300 es precision highp float; out vec4 outMV; @@ -48,6 +51,25 @@ uniform vec2 uResolution; // [w, h] in px uniform float uBlock; // block size in px uniform float uSearch; // search radius, in steps uniform float uStep; // px per step (coverage = uSearch * uStep px) +uniform float uIntraRate; +uniform float uIntraThreshold; +uniform float uIntraPatch; // refresh patch size, in blocks +uniform float uFrame; +float hash(vec3 p) { + uvec3 q = uvec3(p) * uvec3(1597334673u, 3812015801u, 2798796415u); + return float((q.x ^ q.y ^ q.z) * 1597334673u) / 4294967295.0; +} +// Value noise, new field each frame. Patch size 1 = per-block hash. +float patchNoise(vec2 p, float seed) { + vec2 i = floor(p); + vec2 f = fract(p); + f = f * f * (3.0 - 2.0 * f); + float a = hash(vec3(i, seed)); + float b = hash(vec3(i + vec2(1, 0), seed)); + float c = hash(vec3(i + vec2(0, 1), seed)); + float d = hash(vec3(i + vec2(1, 1), seed)); + return mix(mix(a, b, f.x), mix(c, d, f.x), f.y); +} void main() { vec2 block = floor(gl_FragCoord.xy); vec2 base = block * uBlock; // top-left pixel of this block @@ -66,6 +88,7 @@ void main() { vec2 move = vec2(0.0); float bestSad = 1e9; + float bestRaw = 0.0; for (float by = -uSearch; by <= uSearch; by++) { for (float bx = -uSearch; bx <= uSearch; bx++) { vec2 cand = vec2(bx, by) * uStep; // candidate displacement, px @@ -74,15 +97,23 @@ void main() { vec3 cRef = texture(uRef, (base + off[k] + cand + 0.5) / uResolution).rgb; sad += dot(abs(cs[k] - cRef), vec3(1.0)); } + float raw = sad; sad += length(cand) * 0.0025; // prefer small motion on ties if (sad < bestSad) { bestSad = sad; + bestRaw = raw; move = cand; } } } - outMV = vec4(move, 0.0, 1.0); + // Mean abs diff per channel, 0..1. + float err = bestRaw / 27.0; + float n = patchNoise(block / max(uIntraPatch, 1.0), uFrame); + // Smooth noise clusters near 0.5; smoothstep makes it about uniform. + if (uIntraPatch > 1.0) n = smoothstep(0.0, 1.0, n); + bool intra = n < uIntraRate || err > uIntraThreshold; + outMV = vec4(move, intra ? 1.0 : 0.0, 1.0); } `; @@ -112,7 +143,7 @@ uniform vec2 uResolution; uniform bool uIntra; uniform bool uUseResidual; void main() { - if (uIntra) { + if (uIntra || texture(uMV, uv).b > 0.5) { outColor = vec4(texture(uVideo, uv).rgb, 1.0); return; } @@ -195,7 +226,7 @@ uniform bool uIntra; uniform bool uUseResidual; void main() { // I-frame: passthrough the input - if (uIntra) { + if (uIntra || texture(uMV, uv).b > 0.5) { outColor = vec4(luma(texture(uVideo, uv).rgb), 0.0, 0.0, 1.0); return; } @@ -225,7 +256,7 @@ uniform bool uIntra; uniform bool uUseResidual; void main() { // I-frame: passthrough the input - if (uIntra) { + if (uIntra || texture(uMV, uv).b > 0.5) { outColor = vec4(chroma(texture(uVideo, uv).rgb), 0.0, 1.0); return; } @@ -294,6 +325,21 @@ export type DatamoshParams = { colorSpace: DatamoshColorSpace; /** Chroma saturation boost (YCbCr output). 1 = unchanged. */ chromaGain: number; + /** + * Chance per block per frame to refresh from the clean frame (0..1). + * Ramp up to heal the mosh gradually. + */ + intraRate: number; + /** + * Blocks whose best motion match is worse than this go intra. + * Mean abs diff per channel (0..1); 1 = never. + */ + intraThreshold: number; + /** + * Size of the random refresh patches, in blocks. + * 1 = each block refreshes on its own; larger = blobs of blocks. + */ + intraPatchSize: number; /** Stage shown on the canvas. */ view: DatamoshView; }; @@ -306,6 +352,9 @@ const DEFAULT_PARAMS: DatamoshParams = { dup: 0, colorSpace: "ycbcr", chromaGain: 1, + intraRate: 0, + intraThreshold: 1, + intraPatchSize: 1, view: "output", }; @@ -336,6 +385,7 @@ export class DatamoshEffect implements Effect { #cw = 0; #ch = 0; #block = 0; + #frame = 0; // Re-seed when the color model flips (accumulators change meaning). #lastColorSpace: DatamoshColorSpace; @@ -472,6 +522,10 @@ export class DatamoshEffect implements Effect { uBlock: this.#block, uSearch: this.params.searchRange, uStep: this.params.searchStep, + uIntraRate: this.params.intraRate, + uIntraThreshold: this.params.intraThreshold, + uIntraPatch: this.params.intraPatchSize, + uFrame: this.#frame++ % 65536, }, target: mv, }); diff --git a/packages/storybook/src/utils.ts b/packages/storybook/src/utils.ts index 4420a789d..7a5aded62 100644 --- a/packages/storybook/src/utils.ts +++ b/packages/storybook/src/utils.ts @@ -371,6 +371,17 @@ export function attachDatamoshPane( options: { rgb: "rgb", ycbcr: "ycbcr" }, }); pane.addBinding(effect.params, "chromaGain", { min: 0, max: 2, step: 0.1 }); + pane.addBinding(effect.params, "intraRate", { min: 0, max: 1, step: 0.01 }); + pane.addBinding(effect.params, "intraThreshold", { + min: 0, + max: 1, + step: 0.01, + }); + pane.addBinding(effect.params, "intraPatchSize", { + min: 1, + max: 16, + step: 1, + }); pane.addBinding(effect.params, "view", { options: { From 2baff6de57e115b2dec939916bd702a93d3dbd56 Mon Sep 17 00:00:00 2001 From: AMAGI / Jun Yuri Date: Sat, 17 Oct 2026 11:39:55 -0700 Subject: [PATCH 02/42] docs(examples): add Datamosh Scroll demo Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_018attAihospyjqebPaUwNkn --- packages/examples/src/works.ts | 11 + packages/examples/works/datamosh-scroll.html | 428 +++++++++++++++++++ 2 files changed, 439 insertions(+) create mode 100644 packages/examples/works/datamosh-scroll.html diff --git a/packages/examples/src/works.ts b/packages/examples/src/works.ts index d39f797f2..e18f4e486 100644 --- a/packages/examples/src/works.ts +++ b/packages/examples/src/works.ts @@ -179,4 +179,15 @@ export const works: Work[] = [ "A playground for the Figma shader effect ports: warp, slice shift, pixel stretch, pattern refraction, pixelate, dither, and colored edges. Pick an image and tweak every parameter live with Tweakpane.", url: "./works/figma-effects.html", }, + { + id: "datamosh-scroll", + index: "16", + title: "Datamosh Scroll", + tags: ["effect-api", "Glitch", "Transition"], + year: "2026", + author: "AMAGI", + description: + "A day of skateboarding, told as a one-page scroll site. Every section change drops the keyframe: the next scene's motion drags the last one along until intra refresh heals it block by block.", + url: "./works/datamosh-scroll.html", + }, ]; diff --git a/packages/examples/works/datamosh-scroll.html b/packages/examples/works/datamosh-scroll.html new file mode 100644 index 000000000..2f1a1f69b --- /dev/null +++ b/packages/examples/works/datamosh-scroll.html @@ -0,0 +1,428 @@ + + + +Datamosh Scroll + + + + + +
+
+
+ SidewalkIssue 04 · Autumn 2026p. 012 +
+
01
+
+ Morning / Park path +

The plan is no plan.

+

We met at ten with one board each, half a bottle of water and nowhere to be.

+
+

The path along the park is the best warm-up in the city: smooth, a little downhill, and empty before lunch. Nobody talks much. You listen to the wheels and find your stance again.

+

Somebody says the ramp is open today. Somebody else says it was closed last week. We go anyway. That is how most good days start.

+
+
“You don't pick where to go. The pavement does.”
+
1Kick, push, coast. The first kilometre is always the quietest.
+
+
+ 10:00 — Park path +

Push.

+

Morning. Wheels on warm concrete, no plan yet.

+
+
+
+
+ SidewalkIssue 04 · Autumn 2026p. 018 +
+
02
+
+ Afternoon / Ramp +

Drop in or go home.

+

The quarter pipe at the old yard is steeper than it looks, and everyone knows it.

+
+

The first drop is all in the head. Lean forward, put weight on the front foot, and trust the wheels. Lean back and the board leaves without you.

+

We count falls instead of tricks. Eleven before three o'clock. The coping is chipped from years of grinds, and so are our elbows.

+
+
“Commit. The ramp can tell when you don't.”
+
2Coping, chalk and a borrowed helmet. Nobody films the falls.
+
+
+ 14:30 — Ramp +

Drop.

+

Afternoon at the ramp. Lean in, don't look back.

+
+
+
+
+ SidewalkIssue 04 · Autumn 2026p. 024 +
+
03
+
+ Golden hour / Flat ground +

Everything looks like a photo now.

+

Around six the light goes low and orange, and even the easy tricks look expensive.

+
+

This is when the cameras come out. Phones lean against bags, a few frames per try. The kickflip lands on the fourth go, and the shadow lands with it.

+

Shadows stretch twice as long as we are. The ground is still warm. For twenty minutes nobody is tired, and nobody checks the time.

+
+
“Golden hour forgives everything except bad footwork.”
+
3Flip, catch, roll away. Shot at 18:10 with the sun behind the fence.
+
+
+ 18:10 — Golden hour +

Flip.

+

Low sun. Every trick looks better in this light.

+
+
+
+
+ SidewalkIssue 04 · Autumn 2026p. 030 +
+
04
+
+ Dusk / The way home +

One more line.

+

Streetlights in four minutes. Someone always says one more.

+
+

The last run is never the best one, but it is the one you remember. Same path, same cracks, slower now, legs heavy and happy.

+

Boards under arms, we walk the last block home. Next issue: rain, a parking garage, and whoever shows up.

+
+
“See you tomorrow. Same time, no plan.”
+
4Roll out. The lights come on behind us.
+
+
+ 19:02 — Dusk +

Roll.

+

One more line before the lights come on.

+
+ Videos from Pexels by RDNE Stock project, cottonbro studio, and Marc + Espejo. +
+
+ + From 271967070218f40ba8e3b3dadf0f83c27fd7c815 Mon Sep 17 00:00:00 2001 From: AMAGI / Jun Yuri Date: Sat, 17 Oct 2026 13:25:59 -0700 Subject: [PATCH 03/42] feat(effects): add heal param to DatamoshEffect Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_018attAihospyjqebPaUwNkn --- .changeset/datamosh-intra-refresh.md | 3 ++ packages/effects/src/datamosh.ts | 31 +++++++++++++++-- packages/examples/works/datamosh-scroll.html | 35 +++----------------- packages/storybook/src/utils.ts | 1 + 4 files changed, 37 insertions(+), 33 deletions(-) diff --git a/.changeset/datamosh-intra-refresh.md b/.changeset/datamosh-intra-refresh.md index b980cdd82..3dd919cb0 100644 --- a/.changeset/datamosh-intra-refresh.md +++ b/.changeset/datamosh-intra-refresh.md @@ -6,3 +6,6 @@ Add intra refresh params to `DatamoshEffect` so the mosh can heal block by block like a real codec: `intraRate` (random refresh chance per block per frame), `intraThreshold` (refresh blocks with bad motion matches), and `intraPatchSize` (refresh in blobs of blocks instead of single blocks). + +Add `heal` (ms): after `enable()`, the mosh heals by itself over this time +and then disables. 0 keeps the old behavior. diff --git a/packages/effects/src/datamosh.ts b/packages/effects/src/datamosh.ts index 88e8d19e6..996ead0a0 100644 --- a/packages/effects/src/datamosh.ts +++ b/packages/effects/src/datamosh.ts @@ -340,10 +340,19 @@ export type DatamoshParams = { * 1 = each block refreshes on its own; larger = blobs of blocks. */ intraPatchSize: number; + /** + * Auto-heal time in ms after `enable()`, then auto-disable. + * Overrides `intraRate` / `intraThreshold` while healing. + * 0 = mosh until `disable()`. + */ + heal: number; /** Stage shown on the canvas. */ view: DatamoshView; }; +// Peak random refresh rate while healing, per block per second. +const HEAL_PER_SEC = 60; + const DEFAULT_PARAMS: DatamoshParams = { blockSize: 16, searchRange: 5, @@ -355,6 +364,7 @@ const DEFAULT_PARAMS: DatamoshParams = { intraRate: 0, intraThreshold: 1, intraPatchSize: 1, + heal: 0, view: "output", }; @@ -370,6 +380,8 @@ export class DatamoshEffect implements Effect { #enabled = false; // Re-seed the accumulator from a clean frame on the next mosh frame. #seed = true; + // Seconds since `enable()`, for `heal`. + #elapsed = 0; #curRT: EffectRenderTarget | null = null; #prevRT: EffectRenderTarget | null = null; @@ -400,6 +412,7 @@ export class DatamoshEffect implements Effect { enable(): void { this.#enabled = true; + this.#elapsed = 0; } disable(): void { @@ -504,6 +517,20 @@ export class DatamoshEffect implements Effect { this.#seed = true; } + let intraRate = this.params.intraRate; + let intraThreshold = this.params.intraThreshold; + if (this.#enabled && this.params.heal > 0) { + this.#elapsed += ctx.deltaTime; + const t = (this.#elapsed * 1000) / this.params.heal; + if (t >= 1) { + this.disable(); + } else { + // Random refresh ramps up; hard-to-predict blocks heal first. + intraRate = Math.min(1, HEAL_PER_SEC * t ** 3 * ctx.deltaTime); + intraThreshold = 0.02 + 0.23 * (1 - t); + } + } + const resolution: [number, number] = [this.#w, this.#h]; // Capture the current frame into uv-space. @@ -522,8 +549,8 @@ export class DatamoshEffect implements Effect { uBlock: this.#block, uSearch: this.params.searchRange, uStep: this.params.searchStep, - uIntraRate: this.params.intraRate, - uIntraThreshold: this.params.intraThreshold, + uIntraRate: intraRate, + uIntraThreshold: intraThreshold, uIntraPatch: this.params.intraPatchSize, uFrame: this.#frame++ % 65536, }, diff --git a/packages/examples/works/datamosh-scroll.html b/packages/examples/works/datamosh-scroll.html index 2f1a1f69b..0180c2032 100644 --- a/packages/examples/works/datamosh-scroll.html +++ b/packages/examples/works/datamosh-scroll.html @@ -256,13 +256,6 @@

Roll.

import { DatamoshEffect } from "@vfx-js/effects"; import { Pane } from "tweakpane"; - // Pure-motion smear, then intra refresh heals it block by block. - const timing = { - smearMs: 100, - healMs: 1500, - // Peak refresh chance per block per second. - healPerSec: 60, - }; const ZOOM_MS = 1200; const sections = [...document.querySelectorAll("section")]; @@ -358,6 +351,8 @@

Roll.

blockSize: 32, intraPatchSize: 6, chromaGain: 3, + // Heal block by block over 1.5s after each switch. + heal: 1500, }); const pane = new Pane({ title: "Datamosh", expanded: false }); @@ -365,15 +360,12 @@

Roll.

pane.addBinding(effect.params, "blockSize", { min: 4, max: 64, step: 1 }); pane.addBinding(effect.params, "chromaGain", { min: 0, max: 3 }); pane.addBinding(effect.params, "intraPatchSize", { min: 1, max: 16, step: 1 }); - pane.addBinding(timing, "smearMs", { min: 0, max: 3000, step: 10 }); - pane.addBinding(timing, "healMs", { min: 100, max: 5000, step: 10 }); - pane.addBinding(timing, "healPerSec", { min: 1, max: 60 }); + pane.addBinding(effect.params, "heal", { min: 100, max: 5000, step: 10 }); const vfx = new VFX({ pixelRatio: 1, zIndex: 0 }); await vfx.add(canvas, { effect }); let shown = 0; let switchedAt = -Infinity; - let lastNow = performance.now(); const transition = (next) => { effect.enable(); @@ -387,22 +379,6 @@

Roll.

}); }; - // Smear: no residual, no refresh. Heal: ramp intra refresh up. - const updateMosh = (elapsed, dt) => { - if (!effect.enabled) return; - const t = Math.max(0, (elapsed - timing.smearMs) / timing.healMs); - if (t >= 1) { - effect.disable(); - return; - } - effect.setParams({ - useResidual: t > 0, - intraRate: Math.min(1, (timing.healPerSec * t ** 3 * dt) / 1000), - // Hard-to-predict blocks heal first. - intraThreshold: t > 0 ? 0.02 + 0.23 * (1 - t) : 1, - }); - }; - const loop = () => { const active = Math.min( sections.length - 1, @@ -410,10 +386,7 @@

Roll.

); if (active !== shown) transition(active); - const now = performance.now(); - updateMosh(now - switchedAt, now - lastNow); - lastNow = now; - const elapsed = now - switchedAt; + const elapsed = performance.now() - switchedAt; // Zoom-out kick on switch, plus scroll parallax. const t = Math.min(1, elapsed / ZOOM_MS); diff --git a/packages/storybook/src/utils.ts b/packages/storybook/src/utils.ts index 7a5aded62..d00764897 100644 --- a/packages/storybook/src/utils.ts +++ b/packages/storybook/src/utils.ts @@ -382,6 +382,7 @@ export function attachDatamoshPane( max: 16, step: 1, }); + pane.addBinding(effect.params, "heal", { min: 0, max: 5000, step: 10 }); pane.addBinding(effect.params, "view", { options: { From 334cf664e37e3a3adbb09a6af24fc7c26c9d775d Mon Sep 17 00:00:00 2001 From: AMAGI / Jun Yuri Date: Sat, 17 Oct 2026 13:35:51 -0700 Subject: [PATCH 04/42] refactor(effects): rename intraPatchSize to intraClusterSize Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_018attAihospyjqebPaUwNkn --- .changeset/datamosh-intra-refresh.md | 2 +- packages/effects/src/datamosh.ts | 18 +++++++++--------- packages/examples/works/datamosh-scroll.html | 4 ++-- packages/storybook/src/utils.ts | 2 +- 4 files changed, 13 insertions(+), 13 deletions(-) diff --git a/.changeset/datamosh-intra-refresh.md b/.changeset/datamosh-intra-refresh.md index 3dd919cb0..3e9fe5b1e 100644 --- a/.changeset/datamosh-intra-refresh.md +++ b/.changeset/datamosh-intra-refresh.md @@ -5,7 +5,7 @@ Add intra refresh params to `DatamoshEffect` so the mosh can heal block by block like a real codec: `intraRate` (random refresh chance per block per frame), `intraThreshold` (refresh blocks with bad motion matches), and -`intraPatchSize` (refresh in blobs of blocks instead of single blocks). +`intraClusterSize` (refresh in blobs of blocks instead of single blocks). Add `heal` (ms): after `enable()`, the mosh heals by itself over this time and then disables. 0 keeps the old behavior. diff --git a/packages/effects/src/datamosh.ts b/packages/effects/src/datamosh.ts index 996ead0a0..63c0eea3b 100644 --- a/packages/effects/src/datamosh.ts +++ b/packages/effects/src/datamosh.ts @@ -53,14 +53,14 @@ uniform float uSearch; // search radius, in steps uniform float uStep; // px per step (coverage = uSearch * uStep px) uniform float uIntraRate; uniform float uIntraThreshold; -uniform float uIntraPatch; // refresh patch size, in blocks +uniform float uIntraCluster; // refresh cluster size, in blocks uniform float uFrame; float hash(vec3 p) { uvec3 q = uvec3(p) * uvec3(1597334673u, 3812015801u, 2798796415u); return float((q.x ^ q.y ^ q.z) * 1597334673u) / 4294967295.0; } -// Value noise, new field each frame. Patch size 1 = per-block hash. -float patchNoise(vec2 p, float seed) { +// Value noise, new field each frame. Cluster size 1 = per-block hash. +float clusterNoise(vec2 p, float seed) { vec2 i = floor(p); vec2 f = fract(p); f = f * f * (3.0 - 2.0 * f); @@ -109,9 +109,9 @@ void main() { // Mean abs diff per channel, 0..1. float err = bestRaw / 27.0; - float n = patchNoise(block / max(uIntraPatch, 1.0), uFrame); + float n = clusterNoise(block / max(uIntraCluster, 1.0), uFrame); // Smooth noise clusters near 0.5; smoothstep makes it about uniform. - if (uIntraPatch > 1.0) n = smoothstep(0.0, 1.0, n); + if (uIntraCluster > 1.0) n = smoothstep(0.0, 1.0, n); bool intra = n < uIntraRate || err > uIntraThreshold; outMV = vec4(move, intra ? 1.0 : 0.0, 1.0); } @@ -336,10 +336,10 @@ export type DatamoshParams = { */ intraThreshold: number; /** - * Size of the random refresh patches, in blocks. + * Size of the random refresh clusters, in blocks. * 1 = each block refreshes on its own; larger = blobs of blocks. */ - intraPatchSize: number; + intraClusterSize: number; /** * Auto-heal time in ms after `enable()`, then auto-disable. * Overrides `intraRate` / `intraThreshold` while healing. @@ -363,7 +363,7 @@ const DEFAULT_PARAMS: DatamoshParams = { chromaGain: 1, intraRate: 0, intraThreshold: 1, - intraPatchSize: 1, + intraClusterSize: 1, heal: 0, view: "output", }; @@ -551,7 +551,7 @@ export class DatamoshEffect implements Effect { uStep: this.params.searchStep, uIntraRate: intraRate, uIntraThreshold: intraThreshold, - uIntraPatch: this.params.intraPatchSize, + uIntraCluster: this.params.intraClusterSize, uFrame: this.#frame++ % 65536, }, target: mv, diff --git a/packages/examples/works/datamosh-scroll.html b/packages/examples/works/datamosh-scroll.html index 0180c2032..530bb38dc 100644 --- a/packages/examples/works/datamosh-scroll.html +++ b/packages/examples/works/datamosh-scroll.html @@ -349,7 +349,7 @@

Roll.

const effect = new DatamoshEffect({ dup: 1, blockSize: 32, - intraPatchSize: 6, + intraClusterSize: 6, chromaGain: 3, // Heal block by block over 1.5s after each switch. heal: 1500, @@ -359,7 +359,7 @@

Roll.

pane.addBinding(effect.params, "dup", { min: 0, max: 8, step: 1 }); pane.addBinding(effect.params, "blockSize", { min: 4, max: 64, step: 1 }); pane.addBinding(effect.params, "chromaGain", { min: 0, max: 3 }); - pane.addBinding(effect.params, "intraPatchSize", { min: 1, max: 16, step: 1 }); + pane.addBinding(effect.params, "intraClusterSize", { min: 1, max: 16, step: 1 }); pane.addBinding(effect.params, "heal", { min: 100, max: 5000, step: 10 }); const vfx = new VFX({ pixelRatio: 1, zIndex: 0 }); await vfx.add(canvas, { effect }); diff --git a/packages/storybook/src/utils.ts b/packages/storybook/src/utils.ts index d00764897..79625889b 100644 --- a/packages/storybook/src/utils.ts +++ b/packages/storybook/src/utils.ts @@ -377,7 +377,7 @@ export function attachDatamoshPane( max: 1, step: 0.01, }); - pane.addBinding(effect.params, "intraPatchSize", { + pane.addBinding(effect.params, "intraClusterSize", { min: 1, max: 16, step: 1, From 39264827490f07f439111d8782c377549558ad1f Mon Sep 17 00:00:00 2001 From: AMAGI / Jun Yuri Date: Sat, 17 Oct 2026 13:38:24 -0700 Subject: [PATCH 05/42] docs(examples): pin magazine layer to the screen in Datamosh Scroll Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_018attAihospyjqebPaUwNkn --- packages/examples/works/datamosh-scroll.html | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/packages/examples/works/datamosh-scroll.html b/packages/examples/works/datamosh-scroll.html index 530bb38dc..ca7f395c4 100644 --- a/packages/examples/works/datamosh-scroll.html +++ b/packages/examples/works/datamosh-scroll.html @@ -34,7 +34,7 @@ /* Magazine layer. Drawn into the canvas so it gets moshed. */ /* The DOM copy is invisible and only gives layout. */ .mosh { - position: absolute; + position: fixed; inset: 5vh 6vw auto; opacity: 0; text-shadow: none; @@ -288,7 +288,10 @@

Roll.

}; // Draw the .mosh layer from its DOM layout: fills, top/bottom rules, text. - const moshEls = [...document.querySelectorAll(".mosh *")]; + // Fixed to the screen; only the shown section's layer is drawn. + const moshLayers = [...document.querySelectorAll(".mosh")].map((el) => [ + ...el.querySelectorAll("*"), + ]); const fill = (color, x, y, w, h) => { ctx2d.fillStyle = color; ctx2d.fillRect(x, y, w, h); @@ -322,11 +325,11 @@

Roll.

} ctx2d.fillText(line, x, y); }; - const drawMosh = () => { + const drawMosh = (index) => { ctx2d.textBaseline = "middle"; ctx2d.shadowColor = "rgba(0, 0, 0, 0.5)"; ctx2d.shadowBlur = 16; - for (const el of moshEls) { + for (const el of moshLayers[index]) { const r = el.getBoundingClientRect(); if (!r.width || r.bottom < 0 || r.top > innerHeight) continue; const cs = getComputedStyle(el); @@ -393,7 +396,7 @@

Roll.

const scale = 1.1 + 0.3 * (1 - t) ** 3; const dy = (scrollY - shown * innerHeight) * -0.3; draw(media[shown], scale, dy); - drawMosh(); + drawMosh(shown); vfx.update(canvas); requestAnimationFrame(loop); }; From d896155dd86110e871a157cca2e6731904029a11 Mon Sep 17 00:00:00 2001 From: AMAGI / Jun Yuri Date: Sat, 17 Oct 2026 13:42:06 -0700 Subject: [PATCH 06/42] docs(examples): mosh the old scene before swapping in Datamosh Scroll Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_018attAihospyjqebPaUwNkn --- packages/examples/works/datamosh-scroll.html | 28 +++++++++++++------- 1 file changed, 19 insertions(+), 9 deletions(-) diff --git a/packages/examples/works/datamosh-scroll.html b/packages/examples/works/datamosh-scroll.html index ca7f395c4..989349947 100644 --- a/packages/examples/works/datamosh-scroll.html +++ b/packages/examples/works/datamosh-scroll.html @@ -364,22 +364,32 @@

Roll.

pane.addBinding(effect.params, "chromaGain", { min: 0, max: 3 }); pane.addBinding(effect.params, "intraClusterSize", { min: 1, max: 16, step: 1 }); pane.addBinding(effect.params, "heal", { min: 100, max: 5000, step: 10 }); + // How long the old scene moshes before the swap. + const timing = { leadMs: 200 }; + pane.addBinding(timing, "leadMs", { min: 0, max: 1000, step: 10 }); const vfx = new VFX({ pixelRatio: 1, zIndex: 0 }); await vfx.add(canvas, { effect }); let shown = 0; + let target = 0; let switchedAt = -Infinity; + let swapTimer; + // Mosh the old scene first, then swap video and text under the mosh. const transition = (next) => { + target = next; effect.enable(); - switchedAt = performance.now(); - // Wait a frame so the mosh seeds from the old scene. - requestAnimationFrame(() => { - media[next].play(); - media[shown].pause(); - shown = next; - switchedAt = performance.now(); - }); + clearTimeout(swapTimer); + swapTimer = setTimeout(() => { + if (next !== shown) { + media[next].play(); + media[shown].pause(); + shown = next; + switchedAt = performance.now(); + } + // Restart the heal from the swap. + effect.enable(); + }, timing.leadMs); }; const loop = () => { @@ -387,7 +397,7 @@

Roll.

sections.length - 1, Math.max(0, Math.round(scrollY / innerHeight)), ); - if (active !== shown) transition(active); + if (active !== target) transition(active); const elapsed = performance.now() - switchedAt; From 9e8948ff4d65870fe50064a6f2fe9ab2e049a3c1 Mon Sep 17 00:00:00 2001 From: AMAGI / Jun Yuri Date: Sat, 17 Oct 2026 13:42:53 -0700 Subject: [PATCH 07/42] Revert "docs(examples): pin magazine layer to the screen in Datamosh Scroll" Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_018attAihospyjqebPaUwNkn --- packages/examples/works/datamosh-scroll.html | 13 +++++-------- 1 file changed, 5 insertions(+), 8 deletions(-) diff --git a/packages/examples/works/datamosh-scroll.html b/packages/examples/works/datamosh-scroll.html index 989349947..f5d17d3c2 100644 --- a/packages/examples/works/datamosh-scroll.html +++ b/packages/examples/works/datamosh-scroll.html @@ -34,7 +34,7 @@ /* Magazine layer. Drawn into the canvas so it gets moshed. */ /* The DOM copy is invisible and only gives layout. */ .mosh { - position: fixed; + position: absolute; inset: 5vh 6vw auto; opacity: 0; text-shadow: none; @@ -288,10 +288,7 @@

Roll.

}; // Draw the .mosh layer from its DOM layout: fills, top/bottom rules, text. - // Fixed to the screen; only the shown section's layer is drawn. - const moshLayers = [...document.querySelectorAll(".mosh")].map((el) => [ - ...el.querySelectorAll("*"), - ]); + const moshEls = [...document.querySelectorAll(".mosh *")]; const fill = (color, x, y, w, h) => { ctx2d.fillStyle = color; ctx2d.fillRect(x, y, w, h); @@ -325,11 +322,11 @@

Roll.

} ctx2d.fillText(line, x, y); }; - const drawMosh = (index) => { + const drawMosh = () => { ctx2d.textBaseline = "middle"; ctx2d.shadowColor = "rgba(0, 0, 0, 0.5)"; ctx2d.shadowBlur = 16; - for (const el of moshLayers[index]) { + for (const el of moshEls) { const r = el.getBoundingClientRect(); if (!r.width || r.bottom < 0 || r.top > innerHeight) continue; const cs = getComputedStyle(el); @@ -406,7 +403,7 @@

Roll.

const scale = 1.1 + 0.3 * (1 - t) ** 3; const dy = (scrollY - shown * innerHeight) * -0.3; draw(media[shown], scale, dy); - drawMosh(shown); + drawMosh(); vfx.update(canvas); requestAnimationFrame(loop); }; From 2ae4e71044bf32a948046996df7d592851431b16 Mon Sep 17 00:00:00 2001 From: AMAGI / Jun Yuri Date: Sat, 17 Oct 2026 13:45:02 -0700 Subject: [PATCH 08/42] docs(examples): fade chroma gain back to 1 over the heal in Datamosh Scroll Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_018attAihospyjqebPaUwNkn --- packages/examples/works/datamosh-scroll.html | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/packages/examples/works/datamosh-scroll.html b/packages/examples/works/datamosh-scroll.html index f5d17d3c2..cda9f2d13 100644 --- a/packages/examples/works/datamosh-scroll.html +++ b/packages/examples/works/datamosh-scroll.html @@ -358,7 +358,9 @@

Roll.

const pane = new Pane({ title: "Datamosh", expanded: false }); pane.addBinding(effect.params, "dup", { min: 0, max: 8, step: 1 }); pane.addBinding(effect.params, "blockSize", { min: 4, max: 64, step: 1 }); - pane.addBinding(effect.params, "chromaGain", { min: 0, max: 3 }); + // Peak chroma gain; fades back to 1 over the heal. + const look = { chromaGain: effect.params.chromaGain }; + pane.addBinding(look, "chromaGain", { min: 0, max: 3 }); pane.addBinding(effect.params, "intraClusterSize", { min: 1, max: 16, step: 1 }); pane.addBinding(effect.params, "heal", { min: 100, max: 5000, step: 10 }); // How long the old scene moshes before the swap. @@ -370,12 +372,14 @@

Roll.

let shown = 0; let target = 0; let switchedAt = -Infinity; + let healFrom = -Infinity; let swapTimer; // Mosh the old scene first, then swap video and text under the mosh. const transition = (next) => { target = next; effect.enable(); + healFrom = Infinity; clearTimeout(swapTimer); swapTimer = setTimeout(() => { if (next !== shown) { @@ -386,6 +390,7 @@

Roll.

} // Restart the heal from the swap. effect.enable(); + healFrom = performance.now(); }, timing.leadMs); }; @@ -396,7 +401,10 @@

Roll.

); if (active !== target) transition(active); - const elapsed = performance.now() - switchedAt; + const now = performance.now(); + const elapsed = now - switchedAt; + const heal = Math.min(1, Math.max(0, (now - healFrom) / effect.params.heal)); + effect.params.chromaGain = look.chromaGain + (1 - look.chromaGain) * heal; // Zoom-out kick on switch, plus scroll parallax. const t = Math.min(1, elapsed / ZOOM_MS); From 0eb412e31b0f9bf4d68f2a4aec93295b8111f0eb Mon Sep 17 00:00:00 2001 From: AMAGI / Jun Yuri Date: Sat, 17 Oct 2026 13:46:37 -0700 Subject: [PATCH 09/42] docs(examples): vary the magazine layout per section in Datamosh Scroll Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_018attAihospyjqebPaUwNkn --- packages/examples/works/datamosh-scroll.html | 47 ++++++++++++++++++-- 1 file changed, 44 insertions(+), 3 deletions(-) diff --git a/packages/examples/works/datamosh-scroll.html b/packages/examples/works/datamosh-scroll.html index cda9f2d13..8311d4b6e 100644 --- a/packages/examples/works/datamosh-scroll.html +++ b/packages/examples/works/datamosh-scroll.html @@ -120,12 +120,53 @@ font-weight: bold; text-align: center; } + /* Article left, number right. */ + .left .num { + left: auto; + right: 0; + } + .left article { + margin: 28px auto 0 0; + } + /* Full-width spread: headline across, text in two columns below. */ + .wide .num { + position: static; + margin-top: 16px; + font-size: clamp(48px, 6vw, 80px); + } + .wide article { + display: grid; + grid-template-columns: 1fr 1fr; + column-gap: 32px; + align-items: start; + width: 100%; + margin-top: 8px; + } + .wide .kicker, + .wide h3 { + grid-column: 1 / -1; + } + .wide h3 { + font-size: clamp(40px, 6.4vw, 96px); + } + /* Article at the bottom right. */ + .low { + bottom: 8vh; + display: flex; + flex-direction: column; + } + .low article { + margin-top: auto; + } @media (max-width: 700px) { .num, .cols, blockquote { display: none; } + .wide article { + display: block; + } } h2 { margin: 0; @@ -181,7 +222,7 @@

Push.

Morning. Wheels on warm concrete, no plan yet.

-
+
SidewalkIssue 04 · Autumn 2026p. 018
@@ -203,7 +244,7 @@

Drop.

Afternoon at the ramp. Lean in, don't look back.

-
+
SidewalkIssue 04 · Autumn 2026p. 024
@@ -225,7 +266,7 @@

Flip.

Low sun. Every trick looks better in this light.

-
+
SidewalkIssue 04 · Autumn 2026p. 030
From 9c5fa7a0e88547bbdb43ec38c094b497843d5104 Mon Sep 17 00:00:00 2001 From: AMAGI / Jun Yuri Date: Sat, 17 Oct 2026 13:48:15 -0700 Subject: [PATCH 10/42] docs(examples): move Drop and Flip headings in Datamosh Scroll Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_018attAihospyjqebPaUwNkn --- packages/examples/works/datamosh-scroll.html | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/packages/examples/works/datamosh-scroll.html b/packages/examples/works/datamosh-scroll.html index 8311d4b6e..11e0be3c7 100644 --- a/packages/examples/works/datamosh-scroll.html +++ b/packages/examples/works/datamosh-scroll.html @@ -37,6 +37,7 @@ position: absolute; inset: 5vh 6vw auto; opacity: 0; + text-align: left; text-shadow: none; --accent: #ffd23f; } @@ -186,6 +187,15 @@ text-transform: uppercase; opacity: 0.8; } + /* Section heading position. Default is bottom left. */ + section.end { + align-items: flex-end; + text-align: right; + } + section.center { + align-items: center; + text-align: center; + } .tp-dfwv { position: fixed !important; z-index: 2; @@ -221,7 +231,7 @@

The plan is no plan.

Push.

Morning. Wheels on warm concrete, no plan yet.

-
+
SidewalkIssue 04 · Autumn 2026p. 018 @@ -243,7 +253,7 @@

Drop in or go home.

Drop.

Afternoon at the ramp. Lean in, don't look back.

-
+
SidewalkIssue 04 · Autumn 2026p. 024 From 857f44b81e6882819d85edd38faddb592d100984 Mon Sep 17 00:00:00 2001 From: AMAGI / Jun Yuri Date: Sat, 17 Oct 2026 13:49:03 -0700 Subject: [PATCH 11/42] docs(examples): move Flip heading to the top right in Datamosh Scroll Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_018attAihospyjqebPaUwNkn --- packages/examples/works/datamosh-scroll.html | 19 +++++++++++++------ 1 file changed, 13 insertions(+), 6 deletions(-) diff --git a/packages/examples/works/datamosh-scroll.html b/packages/examples/works/datamosh-scroll.html index 11e0be3c7..a7c9fcb0e 100644 --- a/packages/examples/works/datamosh-scroll.html +++ b/packages/examples/works/datamosh-scroll.html @@ -129,7 +129,12 @@ .left article { margin: 28px auto 0 0; } - /* Full-width spread: headline across, text in two columns below. */ + /* Full-width spread at the bottom: headline across, two columns below. */ + .wide { + bottom: 8vh; + display: flex; + flex-direction: column; + } .wide .num { position: static; margin-top: 16px; @@ -141,7 +146,7 @@ column-gap: 32px; align-items: start; width: 100%; - margin-top: 8px; + margin-top: auto; } .wide .kicker, .wide h3 { @@ -192,9 +197,11 @@ align-items: flex-end; text-align: right; } - section.center { - align-items: center; - text-align: center; + section.top-end { + justify-content: flex-start; + align-items: flex-end; + padding-top: 14vh; + text-align: right; } .tp-dfwv { position: fixed !important; @@ -253,7 +260,7 @@

Drop in or go home.

Drop.

Afternoon at the ramp. Lean in, don't look back.

-
+
SidewalkIssue 04 · Autumn 2026p. 024 From cdf450d1163d0da3451ea6576c1adcf6ab5f120a Mon Sep 17 00:00:00 2001 From: AMAGI / Jun Yuri Date: Sat, 17 Oct 2026 13:55:05 -0700 Subject: [PATCH 12/42] docs(examples): mosh only video and headings in Datamosh Scroll Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_018attAihospyjqebPaUwNkn --- packages/examples/works/datamosh-scroll.html | 58 ++++++++++++-------- 1 file changed, 34 insertions(+), 24 deletions(-) diff --git a/packages/examples/works/datamosh-scroll.html b/packages/examples/works/datamosh-scroll.html index a7c9fcb0e..39b91664d 100644 --- a/packages/examples/works/datamosh-scroll.html +++ b/packages/examples/works/datamosh-scroll.html @@ -31,14 +31,11 @@ box-sizing: border-box; text-shadow: 0 0 24px rgba(0, 0, 0, 0.6); } - /* Magazine layer. Drawn into the canvas so it gets moshed. */ - /* The DOM copy is invisible and only gives layout. */ - .mosh { + /* Magazine layer. */ + .mag { position: absolute; inset: 5vh 6vw auto; - opacity: 0; text-align: left; - text-shadow: none; --accent: #ffd23f; } .masthead { @@ -185,7 +182,12 @@ max-width: 32em; font-size: clamp(14px, 1.4vw, 20px); } + /* Headings are drawn into the canvas; the DOM copy only gives layout. */ + .head { + opacity: 0; + } small { + display: block; margin-bottom: 12px; font-size: 13px; letter-spacing: 0.2em; @@ -217,7 +219,7 @@
-
+
SidewalkIssue 04 · Autumn 2026p. 012
@@ -234,12 +236,14 @@

The plan is no plan.

1Kick, push, coast. The first kilometre is always the quietest.
- 10:00 — Park path -

Push.

-

Morning. Wheels on warm concrete, no plan yet.

+
+ 10:00 — Park path +

Push.

+

Morning. Wheels on warm concrete, no plan yet.

+
-
+
SidewalkIssue 04 · Autumn 2026p. 018
@@ -256,12 +260,14 @@

Drop in or go home.

2Coping, chalk and a borrowed helmet. Nobody films the falls.
- 14:30 — Ramp -

Drop.

-

Afternoon at the ramp. Lean in, don't look back.

+
+ 14:30 — Ramp +

Drop.

+

Afternoon at the ramp. Lean in, don't look back.

+
-
+
SidewalkIssue 04 · Autumn 2026p. 024
@@ -278,12 +284,14 @@

Everything looks like a photo now.

3Flip, catch, roll away. Shot at 18:10 with the sun behind the fence.
- 18:10 — Golden hour -

Flip.

-

Low sun. Every trick looks better in this light.

+
+ 18:10 — Golden hour +

Flip.

+

Low sun. Every trick looks better in this light.

+
-
+
SidewalkIssue 04 · Autumn 2026p. 030
@@ -300,9 +308,11 @@

One more line.

4Roll out. The lights come on behind us.
- 19:02 — Dusk -

Roll.

-

One more line before the lights come on.

+
+ 19:02 — Dusk +

Roll.

+

One more line before the lights come on.

+
Videos from Pexels by RDNE Stock project, cottonbro studio, and Marc Espejo. @@ -345,8 +355,8 @@

Roll.

ctx2d.drawImage(el, (cw - w * s) / 2, (ch - h * s) / 2 + dy, w * s, h * s); }; - // Draw the .mosh layer from its DOM layout: fills, top/bottom rules, text. - const moshEls = [...document.querySelectorAll(".mosh *")]; + // Draw the headings into the canvas from their DOM layout, so they get moshed. + const moshEls = [...document.querySelectorAll(".head *")]; const fill = (color, x, y, w, h) => { ctx2d.fillStyle = color; ctx2d.fillRect(x, y, w, h); @@ -433,7 +443,7 @@

Roll.

let healFrom = -Infinity; let swapTimer; - // Mosh the old scene first, then swap video and text under the mosh. + // Mosh the old scene first, then swap video and headings under the mosh. const transition = (next) => { target = next; effect.enable(); From 42f921f55931e4381648437a31343dfb96da6451 Mon Sep 17 00:00:00 2001 From: AMAGI / Jun Yuri Date: Sat, 17 Oct 2026 13:55:50 -0700 Subject: [PATCH 13/42] docs(examples): halve body copy in Datamosh Scroll Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_018attAihospyjqebPaUwNkn --- packages/examples/works/datamosh-scroll.html | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/packages/examples/works/datamosh-scroll.html b/packages/examples/works/datamosh-scroll.html index 39b91664d..11b156300 100644 --- a/packages/examples/works/datamosh-scroll.html +++ b/packages/examples/works/datamosh-scroll.html @@ -229,8 +229,8 @@

The plan is no plan.

We met at ten with one board each, half a bottle of water and nowhere to be.

-

The path along the park is the best warm-up in the city: smooth, a little downhill, and empty before lunch. Nobody talks much. You listen to the wheels and find your stance again.

-

Somebody says the ramp is open today. Somebody else says it was closed last week. We go anyway. That is how most good days start.

+

The park path is the best warm-up in the city: smooth and empty before lunch.

+

Somebody says the ramp is open today. We go anyway.

“You don't pick where to go. The pavement does.”
1Kick, push, coast. The first kilometre is always the quietest.
@@ -253,8 +253,8 @@

Push.

Drop in or go home.

The quarter pipe at the old yard is steeper than it looks, and everyone knows it.

-

The first drop is all in the head. Lean forward, put weight on the front foot, and trust the wheels. Lean back and the board leaves without you.

-

We count falls instead of tricks. Eleven before three o'clock. The coping is chipped from years of grinds, and so are our elbows.

+

The first drop is all in the head. Lean forward and trust the wheels.

+

We count falls instead of tricks. Eleven before three o'clock.

“Commit. The ramp can tell when you don't.”
2Coping, chalk and a borrowed helmet. Nobody films the falls.
@@ -277,8 +277,8 @@

Drop.

Everything looks like a photo now.

Around six the light goes low and orange, and even the easy tricks look expensive.

-

This is when the cameras come out. Phones lean against bags, a few frames per try. The kickflip lands on the fourth go, and the shadow lands with it.

-

Shadows stretch twice as long as we are. The ground is still warm. For twenty minutes nobody is tired, and nobody checks the time.

+

The cameras come out. The kickflip lands on the fourth go.

+

Shadows stretch twice as long as we are. Nobody checks the time.

“Golden hour forgives everything except bad footwork.”
3Flip, catch, roll away. Shot at 18:10 with the sun behind the fence.
@@ -301,8 +301,8 @@

Flip.

One more line.

Streetlights in four minutes. Someone always says one more.

-

The last run is never the best one, but it is the one you remember. Same path, same cracks, slower now, legs heavy and happy.

-

Boards under arms, we walk the last block home. Next issue: rain, a parking garage, and whoever shows up.

+

The last run is never the best, but it is the one you remember.

+

Boards under arms, we walk home. Next issue: rain.

“See you tomorrow. Same time, no plan.”
4Roll out. The lights come on behind us.
From 5a34a6544a61b90477f2abcdc0ecd1cecbe09490 Mon Sep 17 00:00:00 2001 From: AMAGI / Jun Yuri Date: Sat, 17 Oct 2026 13:57:08 -0700 Subject: [PATCH 14/42] docs(examples): compact bottom-left layout for Flip in Datamosh Scroll Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_018attAihospyjqebPaUwNkn --- packages/examples/works/datamosh-scroll.html | 39 ++++++++++---------- 1 file changed, 19 insertions(+), 20 deletions(-) diff --git a/packages/examples/works/datamosh-scroll.html b/packages/examples/works/datamosh-scroll.html index 11b156300..0961e8e19 100644 --- a/packages/examples/works/datamosh-scroll.html +++ b/packages/examples/works/datamosh-scroll.html @@ -126,31 +126,33 @@ .left article { margin: 28px auto 0 0; } - /* Full-width spread at the bottom: headline across, two columns below. */ - .wide { + /* Compact block in the bottom-left corner, clear of the subject. */ + .corner { bottom: 8vh; display: flex; flex-direction: column; } - .wide .num { + .corner .num { position: static; - margin-top: 16px; + margin-top: auto; font-size: clamp(48px, 6vw, 80px); } - .wide article { - display: grid; - grid-template-columns: 1fr 1fr; - column-gap: 32px; - align-items: start; - width: 100%; - margin-top: auto; + .corner article { + width: min(22em, 100%); + margin: 0 auto 0 0; + } + .corner h3 { + font-size: clamp(28px, 3vw, 40px); } - .wide .kicker, - .wide h3 { - grid-column: 1 / -1; + .corner .deck { + font-size: 18px; } - .wide h3 { - font-size: clamp(40px, 6.4vw, 96px); + .corner .cols { + grid-template-columns: 1fr; + gap: 6px; + } + .corner blockquote { + font-size: 18px; } /* Article at the bottom right. */ .low { @@ -167,9 +169,6 @@ blockquote { display: none; } - .wide article { - display: block; - } } h2 { margin: 0; @@ -267,7 +266,7 @@

Drop.

-
+
SidewalkIssue 04 · Autumn 2026p. 024
From a0f2121f5c68752351822b30fffaa49cb3049a1e Mon Sep 17 00:00:00 2001 From: AMAGI / Jun Yuri Date: Sat, 17 Oct 2026 13:58:32 -0700 Subject: [PATCH 15/42] docs(examples): use smaller magazine type in all sections of Datamosh Scroll Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_018attAihospyjqebPaUwNkn --- packages/examples/works/datamosh-scroll.html | 15 +++------------ 1 file changed, 3 insertions(+), 12 deletions(-) diff --git a/packages/examples/works/datamosh-scroll.html b/packages/examples/works/datamosh-scroll.html index 0961e8e19..5d711fd12 100644 --- a/packages/examples/works/datamosh-scroll.html +++ b/packages/examples/works/datamosh-scroll.html @@ -73,13 +73,13 @@ } h3 { margin: 8px 0 0; - font: italic 400 clamp(36px, 4.4vw, 64px) / 1 Georgia, "Times New Roman", + font: italic 400 clamp(28px, 3vw, 40px) / 1 Georgia, "Times New Roman", serif; letter-spacing: -0.02em; } .deck { margin: 12px 0 0; - font: italic 22px / 1.3 Georgia, "Times New Roman", serif; + font: italic 18px / 1.3 Georgia, "Times New Roman", serif; max-width: none; } .cols { @@ -99,7 +99,7 @@ padding: 10px 0; border-top: 2px solid var(--accent); border-bottom: 2px solid var(--accent); - font: bold 24px / 1.2 "Helvetica Neue", Arial, sans-serif; + font: bold 18px / 1.2 "Helvetica Neue", Arial, sans-serif; letter-spacing: -0.01em; } .cap { @@ -141,19 +141,10 @@ width: min(22em, 100%); margin: 0 auto 0 0; } - .corner h3 { - font-size: clamp(28px, 3vw, 40px); - } - .corner .deck { - font-size: 18px; - } .corner .cols { grid-template-columns: 1fr; gap: 6px; } - .corner blockquote { - font-size: 18px; - } /* Article at the bottom right. */ .low { bottom: 8vh; From 5e334c207ea940ff8cc477828b015b5e06cd27cc Mon Sep 17 00:00:00 2001 From: AMAGI / Jun Yuri Date: Sat, 17 Oct 2026 13:59:50 -0700 Subject: [PATCH 16/42] docs(examples): shrink section numbers in Datamosh Scroll Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_018attAihospyjqebPaUwNkn --- packages/examples/works/datamosh-scroll.html | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/packages/examples/works/datamosh-scroll.html b/packages/examples/works/datamosh-scroll.html index 5d711fd12..75dd8ae15 100644 --- a/packages/examples/works/datamosh-scroll.html +++ b/packages/examples/works/datamosh-scroll.html @@ -52,7 +52,7 @@ position: absolute; top: 40px; left: -0.04em; - font-size: clamp(120px, 22vw, 320px); + font-size: clamp(48px, 6vw, 80px); font-weight: bold; line-height: 1; letter-spacing: -0.06em; @@ -135,7 +135,6 @@ .corner .num { position: static; margin-top: auto; - font-size: clamp(48px, 6vw, 80px); } .corner article { width: min(22em, 100%); From 49e7b3a7613529ccf96fa0410507350db04236e7 Mon Sep 17 00:00:00 2001 From: AMAGI / Jun Yuri Date: Sat, 17 Oct 2026 14:03:26 -0700 Subject: [PATCH 17/42] docs(examples): fix mobile layout in Datamosh Scroll Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_018attAihospyjqebPaUwNkn --- packages/examples/works/datamosh-scroll.html | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/packages/examples/works/datamosh-scroll.html b/packages/examples/works/datamosh-scroll.html index 75dd8ae15..f69b61875 100644 --- a/packages/examples/works/datamosh-scroll.html +++ b/packages/examples/works/datamosh-scroll.html @@ -159,6 +159,25 @@ blockquote { display: none; } + .masthead { + font-size: 11px; + letter-spacing: 0.15em; + } + .masthead span:nth-child(2) { + display: none; + } + /* Keep Flip's article at the bottom, clear of its top heading. */ + .corner article { + margin-top: auto; + } + /* Move Roll's article to the top, clear of its bottom heading. */ + .low { + bottom: auto; + display: block; + } + .low article { + margin-top: 28px; + } } h2 { margin: 0; From 33e774c8ec2bfddeaf5478346b5de5dd5fce8e27 Mon Sep 17 00:00:00 2001 From: AMAGI / Jun Yuri Date: Sat, 17 Oct 2026 14:06:07 -0700 Subject: [PATCH 18/42] docs(examples): enlarge main headings on mobile in Datamosh Scroll Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_018attAihospyjqebPaUwNkn --- packages/examples/works/datamosh-scroll.html | 3 +++ 1 file changed, 3 insertions(+) diff --git a/packages/examples/works/datamosh-scroll.html b/packages/examples/works/datamosh-scroll.html index f69b61875..ba20d8d31 100644 --- a/packages/examples/works/datamosh-scroll.html +++ b/packages/examples/works/datamosh-scroll.html @@ -159,6 +159,9 @@ blockquote { display: none; } + section h2 { + font-size: clamp(72px, 18vw, 126px); + } .masthead { font-size: 11px; letter-spacing: 0.15em; From 4d63a2a8561385b193fe654ebf6689b75a02e929 Mon Sep 17 00:00:00 2001 From: AMAGI / Jun Yuri Date: Sat, 17 Oct 2026 17:33:48 -0700 Subject: [PATCH 19/42] feat(effects): rename intraRate/intraThreshold to healRate/healThreshold Storybook pane binds only healRate for healing. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_018attAihospyjqebPaUwNkn --- .changeset/datamosh-intra-refresh.md | 6 +++--- packages/effects/src/datamosh.ts | 28 ++++++++++++++-------------- packages/storybook/src/utils.ts | 8 +------- 3 files changed, 18 insertions(+), 24 deletions(-) diff --git a/.changeset/datamosh-intra-refresh.md b/.changeset/datamosh-intra-refresh.md index 3e9fe5b1e..055e3604a 100644 --- a/.changeset/datamosh-intra-refresh.md +++ b/.changeset/datamosh-intra-refresh.md @@ -2,9 +2,9 @@ "@vfx-js/effects": minor --- -Add intra refresh params to `DatamoshEffect` so the mosh can heal block by -block like a real codec: `intraRate` (random refresh chance per block per -frame), `intraThreshold` (refresh blocks with bad motion matches), and +Add heal params to `DatamoshEffect` so the mosh can heal block by +block like a real codec: `healRate` (random refresh chance per block per +frame), `healThreshold` (refresh blocks with bad motion matches), and `intraClusterSize` (refresh in blobs of blocks instead of single blocks). Add `heal` (ms): after `enable()`, the mosh heals by itself over this time diff --git a/packages/effects/src/datamosh.ts b/packages/effects/src/datamosh.ts index 63c0eea3b..4e11b9d6f 100644 --- a/packages/effects/src/datamosh.ts +++ b/packages/effects/src/datamosh.ts @@ -51,8 +51,8 @@ uniform vec2 uResolution; // [w, h] in px uniform float uBlock; // block size in px uniform float uSearch; // search radius, in steps uniform float uStep; // px per step (coverage = uSearch * uStep px) -uniform float uIntraRate; -uniform float uIntraThreshold; +uniform float uHealRate; +uniform float uHealThreshold; uniform float uIntraCluster; // refresh cluster size, in blocks uniform float uFrame; float hash(vec3 p) { @@ -112,7 +112,7 @@ void main() { float n = clusterNoise(block / max(uIntraCluster, 1.0), uFrame); // Smooth noise clusters near 0.5; smoothstep makes it about uniform. if (uIntraCluster > 1.0) n = smoothstep(0.0, 1.0, n); - bool intra = n < uIntraRate || err > uIntraThreshold; + bool intra = n < uHealRate || err > uHealThreshold; outMV = vec4(move, intra ? 1.0 : 0.0, 1.0); } `; @@ -329,12 +329,12 @@ export type DatamoshParams = { * Chance per block per frame to refresh from the clean frame (0..1). * Ramp up to heal the mosh gradually. */ - intraRate: number; + healRate: number; /** * Blocks whose best motion match is worse than this go intra. * Mean abs diff per channel (0..1); 1 = never. */ - intraThreshold: number; + healThreshold: number; /** * Size of the random refresh clusters, in blocks. * 1 = each block refreshes on its own; larger = blobs of blocks. @@ -342,7 +342,7 @@ export type DatamoshParams = { intraClusterSize: number; /** * Auto-heal time in ms after `enable()`, then auto-disable. - * Overrides `intraRate` / `intraThreshold` while healing. + * Overrides `healRate` / `healThreshold` while healing. * 0 = mosh until `disable()`. */ heal: number; @@ -361,8 +361,8 @@ const DEFAULT_PARAMS: DatamoshParams = { dup: 0, colorSpace: "ycbcr", chromaGain: 1, - intraRate: 0, - intraThreshold: 1, + healRate: 0, + healThreshold: 1, intraClusterSize: 1, heal: 0, view: "output", @@ -517,8 +517,8 @@ export class DatamoshEffect implements Effect { this.#seed = true; } - let intraRate = this.params.intraRate; - let intraThreshold = this.params.intraThreshold; + let healRate = this.params.healRate; + let healThreshold = this.params.healThreshold; if (this.#enabled && this.params.heal > 0) { this.#elapsed += ctx.deltaTime; const t = (this.#elapsed * 1000) / this.params.heal; @@ -526,8 +526,8 @@ export class DatamoshEffect implements Effect { this.disable(); } else { // Random refresh ramps up; hard-to-predict blocks heal first. - intraRate = Math.min(1, HEAL_PER_SEC * t ** 3 * ctx.deltaTime); - intraThreshold = 0.02 + 0.23 * (1 - t); + healRate = Math.min(1, HEAL_PER_SEC * t ** 3 * ctx.deltaTime); + healThreshold = 0.02 + 0.23 * (1 - t); } } @@ -549,8 +549,8 @@ export class DatamoshEffect implements Effect { uBlock: this.#block, uSearch: this.params.searchRange, uStep: this.params.searchStep, - uIntraRate: intraRate, - uIntraThreshold: intraThreshold, + uHealRate: healRate, + uHealThreshold: healThreshold, uIntraCluster: this.params.intraClusterSize, uFrame: this.#frame++ % 65536, }, diff --git a/packages/storybook/src/utils.ts b/packages/storybook/src/utils.ts index 79625889b..ca7503541 100644 --- a/packages/storybook/src/utils.ts +++ b/packages/storybook/src/utils.ts @@ -371,18 +371,12 @@ export function attachDatamoshPane( options: { rgb: "rgb", ycbcr: "ycbcr" }, }); pane.addBinding(effect.params, "chromaGain", { min: 0, max: 2, step: 0.1 }); - pane.addBinding(effect.params, "intraRate", { min: 0, max: 1, step: 0.01 }); - pane.addBinding(effect.params, "intraThreshold", { - min: 0, - max: 1, - step: 0.01, - }); + pane.addBinding(effect.params, "healRate", { min: 0, max: 1, step: 0.01 }); pane.addBinding(effect.params, "intraClusterSize", { min: 1, max: 16, step: 1, }); - pane.addBinding(effect.params, "heal", { min: 0, max: 5000, step: 10 }); pane.addBinding(effect.params, "view", { options: { From 0018e88cebe3aa5cc4d284753caf23685f6e63f3 Mon Sep 17 00:00:00 2001 From: AMAGI / Jun Yuri Date: Sat, 17 Oct 2026 17:37:10 -0700 Subject: [PATCH 20/42] feat(effects): rename intraClusterSize to healClusterSize Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_018attAihospyjqebPaUwNkn --- .changeset/datamosh-intra-refresh.md | 2 +- packages/effects/src/datamosh.ts | 12 ++++++------ packages/examples/works/datamosh-scroll.html | 4 ++-- packages/storybook/src/utils.ts | 2 +- 4 files changed, 10 insertions(+), 10 deletions(-) diff --git a/.changeset/datamosh-intra-refresh.md b/.changeset/datamosh-intra-refresh.md index 055e3604a..897be3394 100644 --- a/.changeset/datamosh-intra-refresh.md +++ b/.changeset/datamosh-intra-refresh.md @@ -5,7 +5,7 @@ Add heal params to `DatamoshEffect` so the mosh can heal block by block like a real codec: `healRate` (random refresh chance per block per frame), `healThreshold` (refresh blocks with bad motion matches), and -`intraClusterSize` (refresh in blobs of blocks instead of single blocks). +`healClusterSize` (refresh in blobs of blocks instead of single blocks). Add `heal` (ms): after `enable()`, the mosh heals by itself over this time and then disables. 0 keeps the old behavior. diff --git a/packages/effects/src/datamosh.ts b/packages/effects/src/datamosh.ts index 4e11b9d6f..8312e902d 100644 --- a/packages/effects/src/datamosh.ts +++ b/packages/effects/src/datamosh.ts @@ -53,7 +53,7 @@ uniform float uSearch; // search radius, in steps uniform float uStep; // px per step (coverage = uSearch * uStep px) uniform float uHealRate; uniform float uHealThreshold; -uniform float uIntraCluster; // refresh cluster size, in blocks +uniform float uHealCluster; // refresh cluster size, in blocks uniform float uFrame; float hash(vec3 p) { uvec3 q = uvec3(p) * uvec3(1597334673u, 3812015801u, 2798796415u); @@ -109,9 +109,9 @@ void main() { // Mean abs diff per channel, 0..1. float err = bestRaw / 27.0; - float n = clusterNoise(block / max(uIntraCluster, 1.0), uFrame); + float n = clusterNoise(block / max(uHealCluster, 1.0), uFrame); // Smooth noise clusters near 0.5; smoothstep makes it about uniform. - if (uIntraCluster > 1.0) n = smoothstep(0.0, 1.0, n); + if (uHealCluster > 1.0) n = smoothstep(0.0, 1.0, n); bool intra = n < uHealRate || err > uHealThreshold; outMV = vec4(move, intra ? 1.0 : 0.0, 1.0); } @@ -339,7 +339,7 @@ export type DatamoshParams = { * Size of the random refresh clusters, in blocks. * 1 = each block refreshes on its own; larger = blobs of blocks. */ - intraClusterSize: number; + healClusterSize: number; /** * Auto-heal time in ms after `enable()`, then auto-disable. * Overrides `healRate` / `healThreshold` while healing. @@ -363,7 +363,7 @@ const DEFAULT_PARAMS: DatamoshParams = { chromaGain: 1, healRate: 0, healThreshold: 1, - intraClusterSize: 1, + healClusterSize: 1, heal: 0, view: "output", }; @@ -551,7 +551,7 @@ export class DatamoshEffect implements Effect { uStep: this.params.searchStep, uHealRate: healRate, uHealThreshold: healThreshold, - uIntraCluster: this.params.intraClusterSize, + uHealCluster: this.params.healClusterSize, uFrame: this.#frame++ % 65536, }, target: mv, diff --git a/packages/examples/works/datamosh-scroll.html b/packages/examples/works/datamosh-scroll.html index ba20d8d31..5b317aeeb 100644 --- a/packages/examples/works/datamosh-scroll.html +++ b/packages/examples/works/datamosh-scroll.html @@ -428,7 +428,7 @@

Roll.

const effect = new DatamoshEffect({ dup: 1, blockSize: 32, - intraClusterSize: 6, + healClusterSize: 6, chromaGain: 3, // Heal block by block over 1.5s after each switch. heal: 1500, @@ -440,7 +440,7 @@

Roll.

// Peak chroma gain; fades back to 1 over the heal. const look = { chromaGain: effect.params.chromaGain }; pane.addBinding(look, "chromaGain", { min: 0, max: 3 }); - pane.addBinding(effect.params, "intraClusterSize", { min: 1, max: 16, step: 1 }); + pane.addBinding(effect.params, "healClusterSize", { min: 1, max: 16, step: 1 }); pane.addBinding(effect.params, "heal", { min: 100, max: 5000, step: 10 }); // How long the old scene moshes before the swap. const timing = { leadMs: 200 }; diff --git a/packages/storybook/src/utils.ts b/packages/storybook/src/utils.ts index ca7503541..aa806bfaf 100644 --- a/packages/storybook/src/utils.ts +++ b/packages/storybook/src/utils.ts @@ -372,7 +372,7 @@ export function attachDatamoshPane( }); pane.addBinding(effect.params, "chromaGain", { min: 0, max: 2, step: 0.1 }); pane.addBinding(effect.params, "healRate", { min: 0, max: 1, step: 0.01 }); - pane.addBinding(effect.params, "intraClusterSize", { + pane.addBinding(effect.params, "healClusterSize", { min: 1, max: 16, step: 1, From 559a8e7974e3dea0240e492d5137e9b11a5f0436 Mon Sep 17 00:00:00 2001 From: AMAGI / Jun Yuri Date: Sat, 17 Oct 2026 17:41:46 -0700 Subject: [PATCH 21/42] feat(effects): remove heal param from DatamoshEffect Datamosh Scroll drives healRate itself and binds its slider to healRate. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_018attAihospyjqebPaUwNkn --- .changeset/datamosh-intra-refresh.md | 3 -- packages/effects/src/datamosh.ts | 31 ++------------------ packages/examples/works/datamosh-scroll.html | 15 +++++----- packages/storybook/src/utils.ts | 5 ++++ 4 files changed, 15 insertions(+), 39 deletions(-) diff --git a/.changeset/datamosh-intra-refresh.md b/.changeset/datamosh-intra-refresh.md index 897be3394..a132fb232 100644 --- a/.changeset/datamosh-intra-refresh.md +++ b/.changeset/datamosh-intra-refresh.md @@ -6,6 +6,3 @@ Add heal params to `DatamoshEffect` so the mosh can heal block by block like a real codec: `healRate` (random refresh chance per block per frame), `healThreshold` (refresh blocks with bad motion matches), and `healClusterSize` (refresh in blobs of blocks instead of single blocks). - -Add `heal` (ms): after `enable()`, the mosh heals by itself over this time -and then disables. 0 keeps the old behavior. diff --git a/packages/effects/src/datamosh.ts b/packages/effects/src/datamosh.ts index 8312e902d..e49767b81 100644 --- a/packages/effects/src/datamosh.ts +++ b/packages/effects/src/datamosh.ts @@ -340,19 +340,10 @@ export type DatamoshParams = { * 1 = each block refreshes on its own; larger = blobs of blocks. */ healClusterSize: number; - /** - * Auto-heal time in ms after `enable()`, then auto-disable. - * Overrides `healRate` / `healThreshold` while healing. - * 0 = mosh until `disable()`. - */ - heal: number; /** Stage shown on the canvas. */ view: DatamoshView; }; -// Peak random refresh rate while healing, per block per second. -const HEAL_PER_SEC = 60; - const DEFAULT_PARAMS: DatamoshParams = { blockSize: 16, searchRange: 5, @@ -364,7 +355,6 @@ const DEFAULT_PARAMS: DatamoshParams = { healRate: 0, healThreshold: 1, healClusterSize: 1, - heal: 0, view: "output", }; @@ -380,8 +370,6 @@ export class DatamoshEffect implements Effect { #enabled = false; // Re-seed the accumulator from a clean frame on the next mosh frame. #seed = true; - // Seconds since `enable()`, for `heal`. - #elapsed = 0; #curRT: EffectRenderTarget | null = null; #prevRT: EffectRenderTarget | null = null; @@ -412,7 +400,6 @@ export class DatamoshEffect implements Effect { enable(): void { this.#enabled = true; - this.#elapsed = 0; } disable(): void { @@ -517,20 +504,6 @@ export class DatamoshEffect implements Effect { this.#seed = true; } - let healRate = this.params.healRate; - let healThreshold = this.params.healThreshold; - if (this.#enabled && this.params.heal > 0) { - this.#elapsed += ctx.deltaTime; - const t = (this.#elapsed * 1000) / this.params.heal; - if (t >= 1) { - this.disable(); - } else { - // Random refresh ramps up; hard-to-predict blocks heal first. - healRate = Math.min(1, HEAL_PER_SEC * t ** 3 * ctx.deltaTime); - healThreshold = 0.02 + 0.23 * (1 - t); - } - } - const resolution: [number, number] = [this.#w, this.#h]; // Capture the current frame into uv-space. @@ -549,8 +522,8 @@ export class DatamoshEffect implements Effect { uBlock: this.#block, uSearch: this.params.searchRange, uStep: this.params.searchStep, - uHealRate: healRate, - uHealThreshold: healThreshold, + uHealRate: this.params.healRate, + uHealThreshold: this.params.healThreshold, uHealCluster: this.params.healClusterSize, uFrame: this.#frame++ % 65536, }, diff --git a/packages/examples/works/datamosh-scroll.html b/packages/examples/works/datamosh-scroll.html index 5b317aeeb..5e9962f81 100644 --- a/packages/examples/works/datamosh-scroll.html +++ b/packages/examples/works/datamosh-scroll.html @@ -336,6 +336,8 @@

Roll.

import { Pane } from "tweakpane"; const ZOOM_MS = 1200; + // Heal time after each switch; chroma fades back over it too. + const HEAL_MS = 1500; const sections = [...document.querySelectorAll("section")]; const media = sections.map(({ dataset: { src } }) => { @@ -430,18 +432,17 @@

Roll.

blockSize: 32, healClusterSize: 6, chromaGain: 3, - // Heal block by block over 1.5s after each switch. - heal: 1500, }); const pane = new Pane({ title: "Datamosh", expanded: false }); pane.addBinding(effect.params, "dup", { min: 0, max: 8, step: 1 }); pane.addBinding(effect.params, "blockSize", { min: 4, max: 64, step: 1 }); // Peak chroma gain; fades back to 1 over the heal. - const look = { chromaGain: effect.params.chromaGain }; + // healRate applies only after the swap. + const look = { chromaGain: effect.params.chromaGain, healRate: 0.03 }; pane.addBinding(look, "chromaGain", { min: 0, max: 3 }); + pane.addBinding(look, "healRate", { min: 0, max: 0.3, step: 0.01 }); pane.addBinding(effect.params, "healClusterSize", { min: 1, max: 16, step: 1 }); - pane.addBinding(effect.params, "heal", { min: 100, max: 5000, step: 10 }); // How long the old scene moshes before the swap. const timing = { leadMs: 200 }; pane.addBinding(timing, "leadMs", { min: 0, max: 1000, step: 10 }); @@ -467,8 +468,6 @@

Roll.

shown = next; switchedAt = performance.now(); } - // Restart the heal from the swap. - effect.enable(); healFrom = performance.now(); }, timing.leadMs); }; @@ -482,8 +481,10 @@

Roll.

const now = performance.now(); const elapsed = now - switchedAt; - const heal = Math.min(1, Math.max(0, (now - healFrom) / effect.params.heal)); + const heal = Math.min(1, Math.max(0, (now - healFrom) / HEAL_MS)); effect.params.chromaGain = look.chromaGain + (1 - look.chromaGain) * heal; + effect.params.healRate = now < healFrom ? 0 : look.healRate; + if (heal >= 1 && effect.enabled) effect.disable(); // Zoom-out kick on switch, plus scroll parallax. const t = Math.min(1, elapsed / ZOOM_MS); diff --git a/packages/storybook/src/utils.ts b/packages/storybook/src/utils.ts index aa806bfaf..1d26fd0ca 100644 --- a/packages/storybook/src/utils.ts +++ b/packages/storybook/src/utils.ts @@ -372,6 +372,11 @@ export function attachDatamoshPane( }); pane.addBinding(effect.params, "chromaGain", { min: 0, max: 2, step: 0.1 }); pane.addBinding(effect.params, "healRate", { min: 0, max: 1, step: 0.01 }); + pane.addBinding(effect.params, "healThreshold", { + min: 0, + max: 1, + step: 0.01, + }); pane.addBinding(effect.params, "healClusterSize", { min: 1, max: 16, From 5869214a04a1181de5be54b93a4d5fc53d562b1a Mon Sep 17 00:00:00 2001 From: AMAGI / Jun Yuri Date: Sat, 17 Oct 2026 22:32:42 -0700 Subject: [PATCH 22/42] docs(examples): drop the cut frame's residual in Datamosh Scroll Keeps the old scene's picture moving with the new scene's motion. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_018attAihospyjqebPaUwNkn --- packages/examples/works/datamosh-scroll.html | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/packages/examples/works/datamosh-scroll.html b/packages/examples/works/datamosh-scroll.html index 5e9962f81..531c04585 100644 --- a/packages/examples/works/datamosh-scroll.html +++ b/packages/examples/works/datamosh-scroll.html @@ -338,6 +338,8 @@

Roll.

const ZOOM_MS = 1200; // Heal time after each switch; chroma fades back over it too. const HEAL_MS = 1500; + // Covers the first frame of the new scene. + const CUT_MS = 50; const sections = [...document.querySelectorAll("section")]; const media = sections.map(({ dataset: { src } }) => { @@ -484,6 +486,8 @@

Roll.

const heal = Math.min(1, Math.max(0, (now - healFrom) / HEAL_MS)); effect.params.chromaGain = look.chromaGain + (1 - look.chromaGain) * heal; effect.params.healRate = now < healFrom ? 0 : look.healRate; + // Drop the cut frame's residual so the old picture stays (no I-frame). + effect.params.useResidual = elapsed > CUT_MS; if (heal >= 1 && effect.enabled) effect.disable(); // Zoom-out kick on switch, plus scroll parallax. From cb3c04f881f61f71218f554d0d597fefa4e50d51 Mon Sep 17 00:00:00 2001 From: AMAGI / Jun Yuri Date: Sat, 17 Oct 2026 22:37:52 -0700 Subject: [PATCH 23/42] docs(examples): swap one frame after enable in Datamosh Scroll Removes leadMs; the mosh only needs one frame of the old scene. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_018attAihospyjqebPaUwNkn --- packages/examples/works/datamosh-scroll.html | 47 +++++++------------- 1 file changed, 17 insertions(+), 30 deletions(-) diff --git a/packages/examples/works/datamosh-scroll.html b/packages/examples/works/datamosh-scroll.html index 531c04585..516349751 100644 --- a/packages/examples/works/datamosh-scroll.html +++ b/packages/examples/works/datamosh-scroll.html @@ -434,61 +434,48 @@

Roll.

blockSize: 32, healClusterSize: 6, chromaGain: 3, + healRate: 0.03, }); const pane = new Pane({ title: "Datamosh", expanded: false }); pane.addBinding(effect.params, "dup", { min: 0, max: 8, step: 1 }); pane.addBinding(effect.params, "blockSize", { min: 4, max: 64, step: 1 }); // Peak chroma gain; fades back to 1 over the heal. - // healRate applies only after the swap. - const look = { chromaGain: effect.params.chromaGain, healRate: 0.03 }; + const look = { chromaGain: effect.params.chromaGain }; pane.addBinding(look, "chromaGain", { min: 0, max: 3 }); - pane.addBinding(look, "healRate", { min: 0, max: 0.3, step: 0.01 }); + pane.addBinding(effect.params, "healRate", { min: 0, max: 0.3, step: 0.01 }); pane.addBinding(effect.params, "healClusterSize", { min: 1, max: 16, step: 1 }); - // How long the old scene moshes before the swap. - const timing = { leadMs: 200 }; - pane.addBinding(timing, "leadMs", { min: 0, max: 1000, step: 10 }); const vfx = new VFX({ pixelRatio: 1, zIndex: 0 }); await vfx.add(canvas, { effect }); let shown = 0; let target = 0; let switchedAt = -Infinity; - let healFrom = -Infinity; - let swapTimer; - - // Mosh the old scene first, then swap video and headings under the mosh. - const transition = (next) => { - target = next; - effect.enable(); - healFrom = Infinity; - clearTimeout(swapTimer); - swapTimer = setTimeout(() => { - if (next !== shown) { - media[next].play(); - media[shown].pause(); - shown = next; - switchedAt = performance.now(); - } - healFrom = performance.now(); - }, timing.leadMs); - }; const loop = () => { const active = Math.min( sections.length - 1, Math.max(0, Math.round(scrollY / innerHeight)), ); - if (active !== target) transition(active); - const now = performance.now(); + // Swap one frame after enable(), so the mosh starts from the old scene. + if (target !== shown) { + media[target].play(); + media[shown].pause(); + shown = target; + switchedAt = now; + } + if (active !== target) { + target = active; + effect.enable(); + } + const elapsed = now - switchedAt; - const heal = Math.min(1, Math.max(0, (now - healFrom) / HEAL_MS)); + const heal = Math.min(1, elapsed / HEAL_MS); effect.params.chromaGain = look.chromaGain + (1 - look.chromaGain) * heal; - effect.params.healRate = now < healFrom ? 0 : look.healRate; // Drop the cut frame's residual so the old picture stays (no I-frame). effect.params.useResidual = elapsed > CUT_MS; - if (heal >= 1 && effect.enabled) effect.disable(); + if (heal >= 1 && target === shown && effect.enabled) effect.disable(); // Zoom-out kick on switch, plus scroll parallax. const t = Math.min(1, elapsed / ZOOM_MS); From c052f28b28dba86bf320184ff344b125d005f3dc Mon Sep 17 00:00:00 2001 From: AMAGI / Jun Yuri Date: Sat, 17 Oct 2026 23:21:09 -0700 Subject: [PATCH 24/42] docs(examples): hold the mosh before healing in Datamosh Scroll Hold 1s with healRate 0, then heal and fade chroma over 0.5s. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_018attAihospyjqebPaUwNkn --- packages/examples/works/datamosh-scroll.html | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/packages/examples/works/datamosh-scroll.html b/packages/examples/works/datamosh-scroll.html index 516349751..17d57bdc7 100644 --- a/packages/examples/works/datamosh-scroll.html +++ b/packages/examples/works/datamosh-scroll.html @@ -336,8 +336,6 @@

Roll.

import { Pane } from "tweakpane"; const ZOOM_MS = 1200; - // Heal time after each switch; chroma fades back over it too. - const HEAL_MS = 1500; // Covers the first frame of the new scene. const CUT_MS = 50; @@ -433,17 +431,17 @@

Roll.

dup: 1, blockSize: 32, healClusterSize: 6, - chromaGain: 3, - healRate: 0.03, }); const pane = new Pane({ title: "Datamosh", expanded: false }); pane.addBinding(effect.params, "dup", { min: 0, max: 8, step: 1 }); pane.addBinding(effect.params, "blockSize", { min: 4, max: 64, step: 1 }); - // Peak chroma gain; fades back to 1 over the heal. - const look = { chromaGain: effect.params.chromaGain }; + // After a switch: hold the mosh, then heal and fade chroma back to 1. + const look = { chromaGain: 2, healRate: 0.05, holdMs: 1000, healMs: 500 }; pane.addBinding(look, "chromaGain", { min: 0, max: 3 }); - pane.addBinding(effect.params, "healRate", { min: 0, max: 0.3, step: 0.01 }); + pane.addBinding(look, "healRate", { min: 0, max: 0.3, step: 0.01 }); + pane.addBinding(look, "holdMs", { min: 0, max: 3000, step: 10 }); + pane.addBinding(look, "healMs", { min: 10, max: 3000, step: 10 }); pane.addBinding(effect.params, "healClusterSize", { min: 1, max: 16, step: 1 }); const vfx = new VFX({ pixelRatio: 1, zIndex: 0 }); await vfx.add(canvas, { effect }); @@ -471,8 +469,10 @@

Roll.

} const elapsed = now - switchedAt; - const heal = Math.min(1, elapsed / HEAL_MS); + const healing = elapsed - look.holdMs; + const heal = Math.min(1, Math.max(0, healing / look.healMs)); effect.params.chromaGain = look.chromaGain + (1 - look.chromaGain) * heal; + effect.params.healRate = healing < 0 ? 0 : look.healRate; // Drop the cut frame's residual so the old picture stays (no I-frame). effect.params.useResidual = elapsed > CUT_MS; if (heal >= 1 && target === shown && effect.enabled) effect.disable(); From bad823cb7eecaeb41be8dc3382c86448cf23549a Mon Sep 17 00:00:00 2001 From: AMAGI / Jun Yuri Date: Sat, 17 Oct 2026 23:23:16 -0700 Subject: [PATCH 25/42] docs(examples): expose zoom time in Datamosh Scroll pane Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_018attAihospyjqebPaUwNkn --- packages/examples/works/datamosh-scroll.html | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/packages/examples/works/datamosh-scroll.html b/packages/examples/works/datamosh-scroll.html index 17d57bdc7..3466443ca 100644 --- a/packages/examples/works/datamosh-scroll.html +++ b/packages/examples/works/datamosh-scroll.html @@ -335,7 +335,6 @@

Roll.

import { DatamoshEffect } from "@vfx-js/effects"; import { Pane } from "tweakpane"; - const ZOOM_MS = 1200; // Covers the first frame of the new scene. const CUT_MS = 50; @@ -437,11 +436,18 @@

Roll.

pane.addBinding(effect.params, "dup", { min: 0, max: 8, step: 1 }); pane.addBinding(effect.params, "blockSize", { min: 4, max: 64, step: 1 }); // After a switch: hold the mosh, then heal and fade chroma back to 1. - const look = { chromaGain: 2, healRate: 0.05, holdMs: 1000, healMs: 500 }; + const look = { + chromaGain: 2, + healRate: 0.05, + holdMs: 1000, + healMs: 500, + zoomMs: 1200, + }; pane.addBinding(look, "chromaGain", { min: 0, max: 3 }); pane.addBinding(look, "healRate", { min: 0, max: 0.3, step: 0.01 }); pane.addBinding(look, "holdMs", { min: 0, max: 3000, step: 10 }); pane.addBinding(look, "healMs", { min: 10, max: 3000, step: 10 }); + pane.addBinding(look, "zoomMs", { min: 10, max: 3000, step: 10 }); pane.addBinding(effect.params, "healClusterSize", { min: 1, max: 16, step: 1 }); const vfx = new VFX({ pixelRatio: 1, zIndex: 0 }); await vfx.add(canvas, { effect }); @@ -478,7 +484,7 @@

Roll.

if (heal >= 1 && target === shown && effect.enabled) effect.disable(); // Zoom-out kick on switch, plus scroll parallax. - const t = Math.min(1, elapsed / ZOOM_MS); + const t = Math.min(1, elapsed / look.zoomMs); const scale = 1.1 + 0.3 * (1 - t) ** 3; const dy = (scrollY - shown * innerHeight) * -0.3; draw(media[shown], scale, dy); From 05f637efce270c76a252ac456a98a9085aeb4f50 Mon Sep 17 00:00:00 2001 From: AMAGI / Jun Yuri Date: Sat, 17 Oct 2026 23:25:47 -0700 Subject: [PATCH 26/42] docs(examples): derive healRate from healMs in Datamosh Scroll Leaves 1% of blocks moshed at the end of the heal. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_018attAihospyjqebPaUwNkn --- packages/examples/works/datamosh-scroll.html | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/packages/examples/works/datamosh-scroll.html b/packages/examples/works/datamosh-scroll.html index 3466443ca..8e5d01587 100644 --- a/packages/examples/works/datamosh-scroll.html +++ b/packages/examples/works/datamosh-scroll.html @@ -438,13 +438,11 @@

Roll.

// After a switch: hold the mosh, then heal and fade chroma back to 1. const look = { chromaGain: 2, - healRate: 0.05, holdMs: 1000, healMs: 500, zoomMs: 1200, }; pane.addBinding(look, "chromaGain", { min: 0, max: 3 }); - pane.addBinding(look, "healRate", { min: 0, max: 0.3, step: 0.01 }); pane.addBinding(look, "holdMs", { min: 0, max: 3000, step: 10 }); pane.addBinding(look, "healMs", { min: 10, max: 3000, step: 10 }); pane.addBinding(look, "zoomMs", { min: 10, max: 3000, step: 10 }); @@ -455,6 +453,7 @@

Roll.

let shown = 0; let target = 0; let switchedAt = -Infinity; + let last = performance.now(); const loop = () => { const active = Math.min( @@ -462,6 +461,8 @@

Roll.

Math.max(0, Math.round(scrollY / innerHeight)), ); const now = performance.now(); + const dt = now - last; + last = now; // Swap one frame after enable(), so the mosh starts from the old scene. if (target !== shown) { media[target].play(); @@ -478,7 +479,8 @@

Roll.

const healing = elapsed - look.holdMs; const heal = Math.min(1, Math.max(0, healing / look.healMs)); effect.params.chromaGain = look.chromaGain + (1 - look.chromaGain) * heal; - effect.params.healRate = healing < 0 ? 0 : look.healRate; + // Refresh rate that leaves 1% of blocks moshed at the end of the heal. + effect.params.healRate = healing < 0 ? 0 : 1 - 0.01 ** (dt / look.healMs); // Drop the cut frame's residual so the old picture stays (no I-frame). effect.params.useResidual = elapsed > CUT_MS; if (heal >= 1 && target === shown && effect.enabled) effect.disable(); From ffa1fc2b0a36bd5243c0e532abf9a154d3052d98 Mon Sep 17 00:00:00 2001 From: AMAGI / Jun Yuri Date: Sat, 17 Oct 2026 23:25:57 -0700 Subject: [PATCH 27/42] docs(examples): tune Datamosh Scroll defaults Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_018attAihospyjqebPaUwNkn --- packages/examples/works/datamosh-scroll.html | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/examples/works/datamosh-scroll.html b/packages/examples/works/datamosh-scroll.html index 8e5d01587..424d855aa 100644 --- a/packages/examples/works/datamosh-scroll.html +++ b/packages/examples/works/datamosh-scroll.html @@ -427,9 +427,9 @@

Roll.

}; const effect = new DatamoshEffect({ - dup: 1, - blockSize: 32, - healClusterSize: 6, + dup: 0, + blockSize: 16, + healClusterSize: 2, }); const pane = new Pane({ title: "Datamosh", expanded: false }); From b4bce6eebca0c005af4f0f606c6fd256c8333d5a Mon Sep 17 00:00:00 2001 From: AMAGI / Jun Yuri Date: Sun, 18 Oct 2026 00:31:14 -0700 Subject: [PATCH 28/42] docs(examples): remove zoom kick from Datamosh Scroll Also set blockSize back to 32 and start the hold clock on enable. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_018attAihospyjqebPaUwNkn --- packages/examples/works/datamosh-scroll.html | 20 +++++++++----------- 1 file changed, 9 insertions(+), 11 deletions(-) diff --git a/packages/examples/works/datamosh-scroll.html b/packages/examples/works/datamosh-scroll.html index 424d855aa..f3e76649c 100644 --- a/packages/examples/works/datamosh-scroll.html +++ b/packages/examples/works/datamosh-scroll.html @@ -357,13 +357,13 @@

Roll.

resize(); addEventListener("resize", resize); - // Cover-fit with scale and vertical offset for extra motion on switch. - const draw = (el, scale, dy) => { + // Cover-fit, scaled up a bit so the parallax offset never shows an edge. + const draw = (el, dy) => { const w = el.videoWidth; const h = el.videoHeight; if (!w || !h) return; const { width: cw, height: ch } = canvas; - const s = Math.max(cw / w, ch / h) * scale; + const s = Math.max(cw / w, ch / h) * 1.1; ctx2d.drawImage(el, (cw - w * s) / 2, (ch - h * s) / 2 + dy, w * s, h * s); }; @@ -428,7 +428,7 @@

Roll.

const effect = new DatamoshEffect({ dup: 0, - blockSize: 16, + blockSize: 32, healClusterSize: 2, }); @@ -440,12 +440,10 @@

Roll.

chromaGain: 2, holdMs: 1000, healMs: 500, - zoomMs: 1200, }; pane.addBinding(look, "chromaGain", { min: 0, max: 3 }); pane.addBinding(look, "holdMs", { min: 0, max: 3000, step: 10 }); pane.addBinding(look, "healMs", { min: 10, max: 3000, step: 10 }); - pane.addBinding(look, "zoomMs", { min: 10, max: 3000, step: 10 }); pane.addBinding(effect.params, "healClusterSize", { min: 1, max: 16, step: 1 }); const vfx = new VFX({ pixelRatio: 1, zIndex: 0 }); await vfx.add(canvas, { effect }); @@ -472,6 +470,8 @@

Roll.

} if (active !== target) { target = active; + // Start the clock here too, so this frame already holds the mosh. + switchedAt = now; effect.enable(); } @@ -483,13 +483,11 @@

Roll.

effect.params.healRate = healing < 0 ? 0 : 1 - 0.01 ** (dt / look.healMs); // Drop the cut frame's residual so the old picture stays (no I-frame). effect.params.useResidual = elapsed > CUT_MS; - if (heal >= 1 && target === shown && effect.enabled) effect.disable(); + if (heal >= 1 && effect.enabled) effect.disable(); - // Zoom-out kick on switch, plus scroll parallax. - const t = Math.min(1, elapsed / look.zoomMs); - const scale = 1.1 + 0.3 * (1 - t) ** 3; + // Scroll parallax. const dy = (scrollY - shown * innerHeight) * -0.3; - draw(media[shown], scale, dy); + draw(media[shown], dy); drawMosh(); vfx.update(canvas); requestAnimationFrame(loop); From 6a250bfc7128d4e03ac6c897131e58f975a2f6ba Mon Sep 17 00:00:00 2001 From: AMAGI / Jun Yuri Date: Sun, 18 Oct 2026 09:10:07 -0700 Subject: [PATCH 29/42] fix: address review findings in Datamosh Scroll - Preload videos and swap only once the new video has a frame. - Cut parallax so it stays inside the 10% cover margin. - Size the canvas from its own box so mobile URL bar changes don't stretch it. - Fix stale datamosh header comment about clean refresh. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_018attAihospyjqebPaUwNkn --- packages/effects/src/datamosh.ts | 4 +-- packages/examples/works/datamosh-scroll.html | 27 +++++++++++++------- 2 files changed, 20 insertions(+), 11 deletions(-) diff --git a/packages/effects/src/datamosh.ts b/packages/effects/src/datamosh.ts index e49767b81..7ac3ddccd 100644 --- a/packages/effects/src/datamosh.ts +++ b/packages/effects/src/datamosh.ts @@ -7,8 +7,8 @@ // // - disabled: every frame decoded as intra -> straight passthrough. // - enabled: frames decoded as inter -> motion-compensate the held -// accumulator + residual, never refreshing it from a clean frame, so -// motion keeps dragging stale texture around (the mosh). +// accumulator + residual, so motion keeps dragging stale texture around +// (the mosh). Only heal-refreshed blocks go back to the clean frame. // // Pipeline per frame (RGB path): // capture ctx.src -> curRT diff --git a/packages/examples/works/datamosh-scroll.html b/packages/examples/works/datamosh-scroll.html index f3e76649c..1606533b8 100644 --- a/packages/examples/works/datamosh-scroll.html +++ b/packages/examples/works/datamosh-scroll.html @@ -16,8 +16,9 @@ #bg { position: fixed; inset: 0; - width: 100vw; - height: 100vh; + /* % of the viewport, not 100vh, so the bitmap matches on mobile. */ + width: 100%; + height: 100%; } section { position: relative; @@ -343,7 +344,14 @@

Roll.

const v = document.createElement("video"); // CORS-enabled so WebGL can read the frames. v.crossOrigin = "anonymous"; - Object.assign(v, { src, muted: true, loop: true, playsInline: true }); + Object.assign(v, { + src, + muted: true, + loop: true, + playsInline: true, + // Buffer every section up front, so a switch has a frame to cut to. + preload: "auto", + }); return v; }); media[0].play(); @@ -351,13 +359,13 @@

Roll.

const canvas = document.getElementById("bg"); const ctx2d = canvas.getContext("2d"); const resize = () => { - canvas.width = innerWidth; - canvas.height = innerHeight; + canvas.width = canvas.clientWidth; + canvas.height = canvas.clientHeight; }; resize(); addEventListener("resize", resize); - // Cover-fit, scaled up a bit so the parallax offset never shows an edge. + // Cover-fit, scaled up 10% so the parallax offset (up to 5%) never shows an edge. const draw = (el, dy) => { const w = el.videoWidth; const h = el.videoHeight; @@ -462,7 +470,8 @@

Roll.

const dt = now - last; last = now; // Swap one frame after enable(), so the mosh starts from the old scene. - if (target !== shown) { + // Wait for the new video's first frame, so the cut lands in CUT_MS. + if (target !== shown && media[target].readyState >= 2) { media[target].play(); media[shown].pause(); shown = target; @@ -483,10 +492,10 @@

Roll.

effect.params.healRate = healing < 0 ? 0 : 1 - 0.01 ** (dt / look.healMs); // Drop the cut frame's residual so the old picture stays (no I-frame). effect.params.useResidual = elapsed > CUT_MS; - if (heal >= 1 && effect.enabled) effect.disable(); + if (heal >= 1 && target === shown && effect.enabled) effect.disable(); // Scroll parallax. - const dy = (scrollY - shown * innerHeight) * -0.3; + const dy = (scrollY - shown * innerHeight) * -0.08; draw(media[shown], dy); drawMosh(); vfx.update(canvas); From 6a76cf7fe08938354d605975004b2f4985f62377 Mon Sep 17 00:00:00 2001 From: AMAGI / Jun Yuri Date: Sun, 18 Oct 2026 18:32:43 -0700 Subject: [PATCH 30/42] feat(effects): remove healClusterSize from DatamoshEffect Blocks heal one by one at exactly healRate. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_018attAihospyjqebPaUwNkn --- .changeset/datamosh-intra-refresh.md | 3 +-- packages/effects/src/datamosh.ts | 24 +------------------- packages/examples/works/datamosh-scroll.html | 2 -- packages/storybook/src/utils.ts | 5 ---- 4 files changed, 2 insertions(+), 32 deletions(-) diff --git a/.changeset/datamosh-intra-refresh.md b/.changeset/datamosh-intra-refresh.md index a132fb232..c2bb5af60 100644 --- a/.changeset/datamosh-intra-refresh.md +++ b/.changeset/datamosh-intra-refresh.md @@ -4,5 +4,4 @@ Add heal params to `DatamoshEffect` so the mosh can heal block by block like a real codec: `healRate` (random refresh chance per block per -frame), `healThreshold` (refresh blocks with bad motion matches), and -`healClusterSize` (refresh in blobs of blocks instead of single blocks). +frame) and `healThreshold` (refresh blocks with bad motion matches). diff --git a/packages/effects/src/datamosh.ts b/packages/effects/src/datamosh.ts index 7ac3ddccd..b7b2ee376 100644 --- a/packages/effects/src/datamosh.ts +++ b/packages/effects/src/datamosh.ts @@ -53,23 +53,11 @@ uniform float uSearch; // search radius, in steps uniform float uStep; // px per step (coverage = uSearch * uStep px) uniform float uHealRate; uniform float uHealThreshold; -uniform float uHealCluster; // refresh cluster size, in blocks uniform float uFrame; float hash(vec3 p) { uvec3 q = uvec3(p) * uvec3(1597334673u, 3812015801u, 2798796415u); return float((q.x ^ q.y ^ q.z) * 1597334673u) / 4294967295.0; } -// Value noise, new field each frame. Cluster size 1 = per-block hash. -float clusterNoise(vec2 p, float seed) { - vec2 i = floor(p); - vec2 f = fract(p); - f = f * f * (3.0 - 2.0 * f); - float a = hash(vec3(i, seed)); - float b = hash(vec3(i + vec2(1, 0), seed)); - float c = hash(vec3(i + vec2(0, 1), seed)); - float d = hash(vec3(i + vec2(1, 1), seed)); - return mix(mix(a, b, f.x), mix(c, d, f.x), f.y); -} void main() { vec2 block = floor(gl_FragCoord.xy); vec2 base = block * uBlock; // top-left pixel of this block @@ -109,10 +97,7 @@ void main() { // Mean abs diff per channel, 0..1. float err = bestRaw / 27.0; - float n = clusterNoise(block / max(uHealCluster, 1.0), uFrame); - // Smooth noise clusters near 0.5; smoothstep makes it about uniform. - if (uHealCluster > 1.0) n = smoothstep(0.0, 1.0, n); - bool intra = n < uHealRate || err > uHealThreshold; + bool intra = hash(vec3(block, uFrame)) < uHealRate || err > uHealThreshold; outMV = vec4(move, intra ? 1.0 : 0.0, 1.0); } `; @@ -335,11 +320,6 @@ export type DatamoshParams = { * Mean abs diff per channel (0..1); 1 = never. */ healThreshold: number; - /** - * Size of the random refresh clusters, in blocks. - * 1 = each block refreshes on its own; larger = blobs of blocks. - */ - healClusterSize: number; /** Stage shown on the canvas. */ view: DatamoshView; }; @@ -354,7 +334,6 @@ const DEFAULT_PARAMS: DatamoshParams = { chromaGain: 1, healRate: 0, healThreshold: 1, - healClusterSize: 1, view: "output", }; @@ -524,7 +503,6 @@ export class DatamoshEffect implements Effect { uStep: this.params.searchStep, uHealRate: this.params.healRate, uHealThreshold: this.params.healThreshold, - uHealCluster: this.params.healClusterSize, uFrame: this.#frame++ % 65536, }, target: mv, diff --git a/packages/examples/works/datamosh-scroll.html b/packages/examples/works/datamosh-scroll.html index 1606533b8..e9ae6353e 100644 --- a/packages/examples/works/datamosh-scroll.html +++ b/packages/examples/works/datamosh-scroll.html @@ -437,7 +437,6 @@

Roll.

const effect = new DatamoshEffect({ dup: 0, blockSize: 32, - healClusterSize: 2, }); const pane = new Pane({ title: "Datamosh", expanded: false }); @@ -452,7 +451,6 @@

Roll.

pane.addBinding(look, "chromaGain", { min: 0, max: 3 }); pane.addBinding(look, "holdMs", { min: 0, max: 3000, step: 10 }); pane.addBinding(look, "healMs", { min: 10, max: 3000, step: 10 }); - pane.addBinding(effect.params, "healClusterSize", { min: 1, max: 16, step: 1 }); const vfx = new VFX({ pixelRatio: 1, zIndex: 0 }); await vfx.add(canvas, { effect }); diff --git a/packages/storybook/src/utils.ts b/packages/storybook/src/utils.ts index 1d26fd0ca..4720a5b30 100644 --- a/packages/storybook/src/utils.ts +++ b/packages/storybook/src/utils.ts @@ -377,11 +377,6 @@ export function attachDatamoshPane( max: 1, step: 0.01, }); - pane.addBinding(effect.params, "healClusterSize", { - min: 1, - max: 16, - step: 1, - }); pane.addBinding(effect.params, "view", { options: { From f82a89aa80773e995c8bfe65197cbecdf16d5b83 Mon Sep 17 00:00:00 2001 From: AMAGI / Jun Yuri Date: Sun, 18 Oct 2026 18:38:40 -0700 Subject: [PATCH 31/42] docs(examples): toggle heading mosh in Datamosh Scroll pane Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_018attAihospyjqebPaUwNkn --- packages/examples/works/datamosh-scroll.html | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/packages/examples/works/datamosh-scroll.html b/packages/examples/works/datamosh-scroll.html index e9ae6353e..f97e5b86e 100644 --- a/packages/examples/works/datamosh-scroll.html +++ b/packages/examples/works/datamosh-scroll.html @@ -198,6 +198,9 @@ .head { opacity: 0; } + .plain-text .head { + opacity: 1; + } small { display: block; margin-bottom: 12px; @@ -447,10 +450,15 @@

Roll.

chromaGain: 2, holdMs: 1000, healMs: 500, + moshText: true, }; pane.addBinding(look, "chromaGain", { min: 0, max: 3 }); pane.addBinding(look, "holdMs", { min: 0, max: 3000, step: 10 }); pane.addBinding(look, "healMs", { min: 10, max: 3000, step: 10 }); + // Off: show the DOM headings instead of drawing them into the canvas. + pane.addBinding(look, "moshText").on("change", ({ value }) => { + document.body.classList.toggle("plain-text", !value); + }); const vfx = new VFX({ pixelRatio: 1, zIndex: 0 }); await vfx.add(canvas, { effect }); @@ -495,7 +503,7 @@

Roll.

// Scroll parallax. const dy = (scrollY - shown * innerHeight) * -0.08; draw(media[shown], dy); - drawMosh(); + if (look.moshText) drawMosh(); vfx.update(canvas); requestAnimationFrame(loop); }; From bcf039f03bc7ef58dc27dcc0f2771a51bc900015 Mon Sep 17 00:00:00 2001 From: AMAGI / Jun Yuri Date: Sun, 18 Oct 2026 18:42:16 -0700 Subject: [PATCH 32/42] docs(examples): toggle magazine text mosh instead of headings Headings are always moshed; moshText also draws the magazine text into the canvas. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_018attAihospyjqebPaUwNkn --- packages/examples/works/datamosh-scroll.html | 22 +++++++++++--------- 1 file changed, 12 insertions(+), 10 deletions(-) diff --git a/packages/examples/works/datamosh-scroll.html b/packages/examples/works/datamosh-scroll.html index f97e5b86e..e55bf0cd6 100644 --- a/packages/examples/works/datamosh-scroll.html +++ b/packages/examples/works/datamosh-scroll.html @@ -198,8 +198,9 @@ .head { opacity: 0; } - .plain-text .head { - opacity: 1; + /* Same for the magazine text while it is moshed too. */ + .mosh-text .mag { + opacity: 0; } small { display: block; @@ -378,8 +379,9 @@

Roll.

ctx2d.drawImage(el, (cw - w * s) / 2, (ch - h * s) / 2 + dy, w * s, h * s); }; - // Draw the headings into the canvas from their DOM layout, so they get moshed. - const moshEls = [...document.querySelectorAll(".head *")]; + // Draw text into the canvas from its DOM layout, so it gets moshed. + const headEls = [...document.querySelectorAll(".head *")]; + const magEls = [...document.querySelectorAll(".mag *")]; const fill = (color, x, y, w, h) => { ctx2d.fillStyle = color; ctx2d.fillRect(x, y, w, h); @@ -413,11 +415,11 @@

Roll.

} ctx2d.fillText(line, x, y); }; - const drawMosh = () => { + const drawMosh = (els) => { ctx2d.textBaseline = "middle"; ctx2d.shadowColor = "rgba(0, 0, 0, 0.5)"; ctx2d.shadowBlur = 16; - for (const el of moshEls) { + for (const el of els) { const r = el.getBoundingClientRect(); if (!r.width || r.bottom < 0 || r.top > innerHeight) continue; const cs = getComputedStyle(el); @@ -450,14 +452,14 @@

Roll.

chromaGain: 2, holdMs: 1000, healMs: 500, - moshText: true, + moshText: false, }; pane.addBinding(look, "chromaGain", { min: 0, max: 3 }); pane.addBinding(look, "holdMs", { min: 0, max: 3000, step: 10 }); pane.addBinding(look, "healMs", { min: 10, max: 3000, step: 10 }); - // Off: show the DOM headings instead of drawing them into the canvas. + // Also draw the magazine text into the canvas, so it gets moshed. pane.addBinding(look, "moshText").on("change", ({ value }) => { - document.body.classList.toggle("plain-text", !value); + document.body.classList.toggle("mosh-text", value); }); const vfx = new VFX({ pixelRatio: 1, zIndex: 0 }); await vfx.add(canvas, { effect }); @@ -503,7 +505,7 @@

Roll.

// Scroll parallax. const dy = (scrollY - shown * innerHeight) * -0.08; draw(media[shown], dy); - if (look.moshText) drawMosh(); + drawMosh(look.moshText ? [...magEls, ...headEls] : headEls); vfx.update(canvas); requestAnimationFrame(loop); }; From 1e159ed650cb8e5d017c5c8c5078563ac3b1cfc4 Mon Sep 17 00:00:00 2001 From: AMAGI / Jun Yuri Date: Sun, 18 Oct 2026 18:53:51 -0700 Subject: [PATCH 33/42] docs(examples): swap last video in Datamosh Scroll Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_018attAihospyjqebPaUwNkn --- packages/examples/works/datamosh-scroll.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/examples/works/datamosh-scroll.html b/packages/examples/works/datamosh-scroll.html index e55bf0cd6..3cf9c143c 100644 --- a/packages/examples/works/datamosh-scroll.html +++ b/packages/examples/works/datamosh-scroll.html @@ -306,7 +306,7 @@

Flip.

Low sun. Every trick looks better in this light.

-
+
SidewalkIssue 04 · Autumn 2026p. 030 From 4d4e5755cd417d76376d2e322c4de2a640dd3708 Mon Sep 17 00:00:00 2001 From: AMAGI / Jun Yuri Date: Sun, 18 Oct 2026 18:56:21 -0700 Subject: [PATCH 34/42] docs(examples): pin headings to the screen in Datamosh Scroll Only the shown section's heading is drawn; it swaps with the video. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_018attAihospyjqebPaUwNkn --- packages/examples/works/datamosh-scroll.html | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/packages/examples/works/datamosh-scroll.html b/packages/examples/works/datamosh-scroll.html index 3cf9c143c..3d5f193f0 100644 --- a/packages/examples/works/datamosh-scroll.html +++ b/packages/examples/works/datamosh-scroll.html @@ -380,7 +380,7 @@

Roll.

}; // Draw text into the canvas from its DOM layout, so it gets moshed. - const headEls = [...document.querySelectorAll(".head *")]; + const headEls = sections.map((s) => [...s.querySelectorAll(".head *")]); const magEls = [...document.querySelectorAll(".mag *")]; const fill = (color, x, y, w, h) => { ctx2d.fillStyle = color; @@ -415,12 +415,14 @@

Roll.

} ctx2d.fillText(line, x, y); }; - const drawMosh = (els) => { + // dy shifts the DOM layout, e.g. to pin text to the screen. + const drawMosh = (els, dy = 0) => { ctx2d.textBaseline = "middle"; ctx2d.shadowColor = "rgba(0, 0, 0, 0.5)"; ctx2d.shadowBlur = 16; for (const el of els) { - const r = el.getBoundingClientRect(); + const { x, y, width, height } = el.getBoundingClientRect(); + const r = new DOMRect(x, y + dy, width, height); if (!r.width || r.bottom < 0 || r.top > innerHeight) continue; const cs = getComputedStyle(el); ctx2d.globalAlpha = Number(cs.opacity); @@ -505,7 +507,9 @@

Roll.

// Scroll parallax. const dy = (scrollY - shown * innerHeight) * -0.08; draw(media[shown], dy); - drawMosh(look.moshText ? [...magEls, ...headEls] : headEls); + if (look.moshText) drawMosh(magEls); + // Pin the shown heading to the screen; it swaps with the video. + drawMosh(headEls[shown], -sections[shown].getBoundingClientRect().top); vfx.update(canvas); requestAnimationFrame(loop); }; From b9ce4092e8f2e81bbe9004c60e88cf03135bf95d Mon Sep 17 00:00:00 2001 From: AMAGI / Jun Yuri Date: Sun, 18 Oct 2026 18:57:08 -0700 Subject: [PATCH 35/42] docs(examples): use a fixed VFX canvas in Datamosh Scroll The scroll-synced canvas lagged the page scroll, so pinned headings shook. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_018attAihospyjqebPaUwNkn --- packages/examples/works/datamosh-scroll.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/examples/works/datamosh-scroll.html b/packages/examples/works/datamosh-scroll.html index 3d5f193f0..2755dfb2f 100644 --- a/packages/examples/works/datamosh-scroll.html +++ b/packages/examples/works/datamosh-scroll.html @@ -463,7 +463,7 @@

Roll.

pane.addBinding(look, "moshText").on("change", ({ value }) => { document.body.classList.toggle("mosh-text", value); }); - const vfx = new VFX({ pixelRatio: 1, zIndex: 0 }); + const vfx = new VFX({ pixelRatio: 1, zIndex: 0, scrollPadding: false }); await vfx.add(canvas, { effect }); let shown = 0; From 671aa07b2a0991b19cdd04868b3336bb4e015c37 Mon Sep 17 00:00:00 2001 From: AMAGI / Jun Yuri Date: Sun, 18 Oct 2026 19:01:11 -0700 Subject: [PATCH 36/42] docs(examples): pin magazine text and add scroll progress in Datamosh Scroll Only the shown section's text is visible; it swaps with the video. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_018attAihospyjqebPaUwNkn --- packages/examples/works/datamosh-scroll.html | 36 ++++++++++++++++++-- 1 file changed, 33 insertions(+), 3 deletions(-) diff --git a/packages/examples/works/datamosh-scroll.html b/packages/examples/works/datamosh-scroll.html index 2755dfb2f..0a9d22ba6 100644 --- a/packages/examples/works/datamosh-scroll.html +++ b/packages/examples/works/datamosh-scroll.html @@ -33,8 +33,10 @@ text-shadow: 0 0 24px rgba(0, 0, 0, 0.6); } /* Magazine layer. */ + /* Pinned like the headings; only the shown section's copy is visible. */ .mag { - position: absolute; + position: fixed; + visibility: hidden; inset: 5vh 6vw auto; text-align: left; --accent: #ffd23f; @@ -202,6 +204,26 @@ .mosh-text .mag { opacity: 0; } + .shown .mag { + visibility: visible; + } + /* Scroll progress, since the text no longer moves. */ + .progress { + position: fixed; + z-index: 2; + right: 16px; + top: 35vh; + width: 2px; + height: 30vh; + background: rgba(255, 255, 255, 0.25); + } + .progress i { + display: block; + height: 100%; + background: white; + transform-origin: top; + transform: scaleY(0); + } small { display: block; margin-bottom: 12px; @@ -334,6 +356,7 @@

Roll.

Espejo.
+