From b759604d3a78802b50eb6cd9a59fe104d6f7c76c Mon Sep 17 00:00:00 2001 From: simien Date: Sun, 13 Sep 2026 22:59:12 -0400 Subject: [PATCH] feat: collide the layers, give each layer its own motion, and stretch Ripple Collide, under Direction in the Animation section, makes the two layers push on each other under whatever motion is running: each layer's field is bent along the ridges of the other layer's field as it is that frame, so under Loop, Drift, and Wind the two flows bend each other as they move. It rises and settles once per cycle like Tide, so the still is untouched and every cycle closes. The pusher's field is sampled on a two-cell lattice, since under a moving motion it is rebuilt every frame. Tide is now Collide at full with nothing else moving, and reads the same lattice. Each layer tab has a Motion of its own that follows General unless changed, so one layer can loop while the other pulses or holds still. Choosing a Motion on General puts both layers back to Same, and a layer's own menu names the layer in its toast. If General is None and a layer runs a kind, the preview plays and the Speed controls show. Ripple no longer touches the grid builder. The swell grows and shrinks each finished layer's shapes along the rings, and the pull moved into the painter, which reads every pixel from the cell under the refracted point, so blocks stretch as a ring passes rather than stepping a cell at a time. Drops fall on the canvas as a whole, and in the live preview each lands somewhere new every life; exports repeat their rain so the loop closes. Wind's broad mass now holds still while the gust bends it and the fine octaves stream through; it used to slide like Drift with the gust on top. Layer B circles the other way under Loop and in Wind's gust, so the layers sway against each other rather than as one. The three reroll example clips are removed, and the six motion clips are re-rendered from six distinct compositions, kaleidoscope on Loop only. Collide and the layers' motions travel in links as tail fields whose zero is off and Same; links grow from 83 to 84 characters. Co-Authored-By: Claude Fable 5.1 --- AGENTS.md | 62 +++- README.md | 42 +-- animated-backgrounds/index.html | 9 +- examples/brand-campaign.gif | Bin 599116 -> 0 bytes examples/drift.gif | Bin 808221 -> 1154497 bytes examples/full-random.gif | Bin 416721 -> 0 bytes examples/kaleidoscope-variations.gif | Bin 258987 -> 0 bytes examples/loop.gif | Bin 1105486 -> 1148705 bytes examples/pulse.gif | Bin 672425 -> 1386721 bytes examples/ripple.gif | Bin 1355369 -> 1403125 bytes examples/tide.gif | Bin 1261406 -> 1128399 bytes examples/wind.gif | Bin 717724 -> 1203375 bytes flow-fields/index.html | 2 +- generator.js | 507 +++++++++++++++++---------- guide/index.html | 65 ++-- index.html | 107 +++++- seamless-backgrounds/index.html | 2 +- 17 files changed, 502 insertions(+), 294 deletions(-) delete mode 100644 examples/brand-campaign.gif delete mode 100644 examples/full-random.gif delete mode 100644 examples/kaleidoscope-variations.gif diff --git a/AGENTS.md b/AGENTS.md index 8946176..4d5cf2c 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -175,7 +175,7 @@ change carries its speed as an index into that list, and it meant both durations at once. `checks.py` guards the constant the way it guarded the menu. -## Ripple moves cells and density, not the field +## Ripple is a post-pass and a painter, not a field motion Loop, Drift and Wind move where the field is sampled. Pulse and Ripple leave the field alone, which is why both work with no field strength @@ -186,20 +186,31 @@ Ripple went through the field first and was invisible. A radial displacement of the sampling coordinates, at any amplitude short of mush, changes almost nothing a dither shows, because the dither stays put and only the shading under it shifts. A lateral wave and a zoom -failed the same way. What reads is either changing how many cells fill -(the crest's swell, as Pulse does) or moving the finished cells -themselves (`rippleWarp`, which pulls each drawn cell along the -water's slope, dither and all). Ripple does both, and the warp is what -makes every texture move under it, traced ones included, so it needs -no per-texture answer. +failed the same way. What reads is changing the finished cells. Ripple +does that in two places, neither of them `buildGrid`: `rippleSwell` +grows and shrinks each finished layer's shapes by a cell along the +rings, and `renderWarped` paints every pixel from the cell under the +refracted point, at pixel resolution, so blocks stretch rather than +step. A cell-level warp was tried between the two and stepped whole +blocks, which is what the pixel painter replaced. + +Two things follow that a new consumer of `generate()` must know. It +returns `warp` alongside the grids, null for every motion but Ripple, +and `renderToCanvas` needs it passed through or a Ripple frame paints +with the swell and no pull; every call site in the app and the +explainers passes it. And the SVG exporter reads the grids only, which +is right, because an SVG is a still. Ripple is rain: several seeded drops, each born at its own moment and living one cycle, summed where the rings cross. Each is periodic on its own so the sum closes, but rings are in flight at phase 0, so Ripple's -first frame is not the still, as Drift's is not. The rings are -evaluated on a lattice `RIPPLE_STEP` cells apart and interpolated; -evaluating every drop at every cell put a frame far over the 80ms -budget. Both layers share A's drops, B at 70% of the swell and pull. +first frame is not the still, as Drift's is not. The preview passes +`loopCycle`, and with it each drop lands somewhere new at every +rebirth, when its ring is at zero; exports leave it out, so their rain +repeats and the loop closes. The rings are evaluated on a lattice +`RIPPLE_STEP` cells apart and interpolated; evaluating every drop at +every cell put a frame far over the 80ms budget. Both layers share the +one surface, B at 70% of the swell and pull. ## Tide is the one motion where the layers touch @@ -226,6 +237,32 @@ way, so the slider worked and looked dead. The edge is now dithered over `CONTOUR_FEATHER` of a level and no more, so the same shift moves the line. Don't widen it to soften the look without watching it move. +## Collide and per-layer motion + +Every motion is resolved per layer in `generate()`: a layer's `motion` +of "same" follows `loopMode`, "none" holds it still, and a kind runs +it, all on the one phase. Anything that reads "the" motion in the app +must say which: `generalMotion()` is what `generate()` gets as +`loopMode`, and `currentMotion()` is whether anything plays and what to +call it, which a layer's own kind can answer when General is None. + +Collide is Tide's push under any motion: `tidePush` reads the pusher's +field as it is that frame, on a lattice two cells apart, because under +Loop, Drift, and Wind it is rebuilt every frame and a full-resolution +gradient put a frame far over budget. Tide's own push is the still +one, cached. Both go through the same `deform` hook. + +## Wind stands, Drift slides, and B circles the other way + +Wind's first octave holds still and only the finer octaves travel +(`WIND_PARALLAX`), with the gust bending the lot; it used to slide +like Drift with the gust on top, and the two were hard to tell apart. +Under Loop, and in Wind's gust circle, layer B turns the opposite way +(`LAYER_B_SPIN`), so the layers slide against each other; same-way +circling read as one sway. Both are how a motion plays, not how a +still looks, so links are untouched, but the Loop and Wind example +clips were re-rendered for them. + ## Six orthogonal controls drive animation Don't re-entangle them. **Motion** (General tab) is what a pass looks like, @@ -272,7 +309,8 @@ field existed", because base64 pads the last byte with zeros and an old link's tail is read straight out of that padding. A tail that is all zeros is not written at all, though nothing leaves it empty today: the two durations are always written, so every new link is the same length -(83 characters since depth and direction joined the tail). +(84 characters since depth, direction, collide, and the layers' +own motions joined the tail). The escape hatch is `COMPACT_VERSION`. Bumping it says the break is deliberate, and both checks stand down: the decoder refuses a version it diff --git a/README.md b/README.md index fe35384..60f27f2 100644 --- a/README.md +++ b/README.md @@ -29,9 +29,9 @@ no cut. + scale to whatever each column ends up with. Two columns, so each + clip renders large, which these need since the motion is the whole + point of them. --> @@ -42,8 +42,8 @@ no cut. - - + + @@ -54,8 +54,8 @@ no cut. - - + + @@ -66,31 +66,8 @@ no cut. - - - -
LoopDrift
The flow field breathes around a small closed path and comes back where it started, so the stripes of this 8-way kaleidoscope sway in place.The green bands slide one way along their flow direction and wrap into themselves, so they never jump; the sparse purple layer, with almost no field strength, holds still behind them.Each layer's flow field breathes around a small closed path, the two turning opposite ways, so they sway against each other and come back where they started.Orange bands slide one way and a cream nebula behind them slides the other, each along its own flow direction, and both wrap into themselves so they never jump.
WindPulse
The yellow and magenta streaks travel the way Drift's do, but a much broader gust bends the field as they go, rising and settling once across the cycle, so the fine wisps run ahead of the bands they belong to.Density swings on one sine wave, Layer B at 70% of the depth, so the streaks swell and thin without moving.The broad streaks hold their ground while a much broader gust bends them, rising and settling once across the cycle, and their fine detail streams through along the flow.Density swings on one sine wave, Layer B at 70% of the depth, so the contour lines swell and thin in weight and the streaks behind them breathe, without moving.
RippleTide
Rain: seven drops land at their own moments, and each ring bends and swells what it crosses, thins as it spreads, and adds where it meets another, while the field beneath never moves.Each layer's field is pushed along the other's ridges, rising and settling once per cycle, so the blue and pink bands bend around each other and let go.
- -Three more, each thirteen Auto-randomize passes at a two-second interval, -showing three ways to use the locks. - - - - - - - - - - - - - - - - - + +
Full randomBrand campaignKaleidoscope variations
Full randomBrand campaignKaleidoscope variations
Only block size and shape mask locked; every other field rerolls each pass.Fine diagonal streaks fading toward the bottom, colors and flow locked to a brand palette; only density and stretch drift, for a set of on-brand variations.A chunky 8-way kaleidoscope with symmetry, block size, density, and flow locked; seeds and colors reroll each pass, so every frame is a new pattern in the same language.Rain on a marble: seven drops land at their own moments, and each ring bends and swells what it crosses, thins as it spreads, and adds where it meets another, while the field beneath never moves.Each layer's field is pushed along the other's ridges, rising and settling once per cycle, so the strands are swept along the nebula behind them and the nebula bends around the strands.
@@ -136,6 +113,7 @@ of the sidebar or the end of the settings panel on a phone. - Plays on the canvas the moment you pick one, which is exactly what a GIF records - Separate sliders for how fast it moves and how often it rerolls, both from one second to sixteen, with the reroll stepping in doublings - Depth scales how far a motion goes, and Direction runs it backwards, so Drift and Wind can travel against the flow +- Collide makes the layers push on each other under any motion, and each layer can run a motion of its own ### Exporting and sharing diff --git a/animated-backgrounds/index.html b/animated-backgrounds/index.html index 5127fa9..9226f30 100644 --- a/animated-backgrounds/index.html +++ b/animated-backgrounds/index.html @@ -179,7 +179,7 @@

The six motions

-

Loop breathes around a closed path and returns where it started. Drift travels one way and wraps into itself. Wind travels and bends as it goes, like a gust crossing the frame. Pulse holds still and swells. Ripple drops rain on the surface and lets the rings cross. Tide bends each layer around the other's ridges and lets go.

+

Loop breathes around a closed path and returns where it started, the two layers turning opposite ways. Drift travels one way and wraps into itself. Wind holds its ground while a gust bends it and fine detail streams through. Pulse holds still and swells. Ripple drops rain on the surface and lets the rings cross. Tide bends each layer around the other's ridges and lets go.

How long a cycle lasts

@@ -188,7 +188,7 @@

How long a cycle lasts

An export uses that same number. A GIF records whole cycles. Set four seconds and you get a four second file. Drop it into a page and let it repeat.

-

Two more controls sit beside it. Depth is how far the motion goes, from a quarter of its usual reach to twice it. Direction runs the cycle backwards, which is how Drift and Wind travel against the flow without the still changing.

+

Three more controls sit beside it. Depth is how far the motion goes, from a quarter of its usual reach to twice it. Direction runs the cycle backwards, which is how Drift and Wind travel against the flow without the still changing. Collide makes the two layers push on each other while they move. And each layer tab has a Motion of its own, so one layer can loop while the other drifts.

The slider starts at one second rather than lower. A cycle is one change of brightness across the whole frame, and one a second stays well under the three that WCAG 2.3.1 allows. Worth keeping in mind for whatever you build with the file, too.

@@ -295,7 +295,7 @@

Try it

- + + - + +