From 20c3ecc50fc034d1c5b69d37eea3624a4f26b8bc Mon Sep 17 00:00:00 2001 From: simien Date: Sun, 13 Sep 2026 20:38:58 -0400 Subject: [PATCH] feat: add Ripple and Tide motions, motion depth and direction, crisp Contours Two motions join the four. Ripple is rain: seven seeded drops, each born at its own moment in the cycle, whose rings swell the density along their crests and pull the drawn cells the way water refracts what is under it, so every texture bulges and settles as a ring passes. Layer B is pulled less than A, so the two slide against each other. Tide is the one motion in which the layers act on each other: each layer's field is read at coordinates pushed along the gradient of the other layer's field, rising and settling once per cycle. Both close exactly; Tide opens on the still, Ripple's first frame has rings in flight, and the guide says which. Depth and Direction sit under Speed. Depth scales how far every motion but Drift goes, from a quarter to twice its reach; Drift's travel is its period and cannot change. Direction runs the cycle backwards, which Drift and Wind show as travel against the flow. Both travel in links as tail fields whose zero is today's behaviour, so every existing link opens as it did. Contours' line now has a crisp edge with a thin dithered rim. The linear falloff made it sparkle under motion at every speed, since a quarter-cell shift re-rolled the dither across the whole line; the thin edge lets the lines crawl instead. Coverage equals the density slider all the way to solid, and the level count never drops below one, which is what the readout said. Also: Pulse reads the field at phase 0 and hits the cache rather than resampling an identical field every frame, a moving frame's field is no longer cached (it was only evicting the stills), the Layer B offset comment says eighth of a turn as the constant does, and the guide no longer calls Pulse the one kind that moves without field strength. Co-Authored-By: Claude Fable 5.1 --- AGENTS.md | 68 ++++- README.md | 24 +- animated-backgrounds/index.html | 49 ++-- examples/ripple.gif | Bin 0 -> 1355369 bytes examples/tide.gif | Bin 0 -> 1261406 bytes flow-fields/index.html | 2 +- generator.js | 470 +++++++++++++++++++++++++++++--- guide/index.html | 46 +++- index.html | 85 +++++- seamless-backgrounds/index.html | 2 +- 10 files changed, 657 insertions(+), 89 deletions(-) create mode 100644 examples/ripple.gif create mode 100644 examples/tide.gif diff --git a/AGENTS.md b/AGENTS.md index 239041a..8946176 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -175,12 +175,69 @@ 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. -## Four orthogonal controls drive animation +## Ripple moves cells and density, not the field + +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 +and why `buildGrid` reads the field at phase 0 for them: an identical +field sampled every frame was most of a Pulse frame's cost before that. + +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. + +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. + +## Tide is the one motion where the layers touch + +Every other motion builds a layer from its own settings alone. Tide +reads the other layer too: `generate()` hands each `buildGrid` a +`deform`, the other layer's normalised field gradient (`tidePush`, +cached per composition, symmetry applied so it is the field the canvas +shows), scaled by a raised-cosine gate, and `getFlowField` reads the +field from the pushed coordinates. The hook is null for every other +motion and every still, which is what keeps those bit-identical; a +field read with a deform is never cached, since its phase is in the +scale. A layer with no field strength has a flat field and pushes +nothing, so the other layer holds still under it. The push is away +from the other's ridges; toward was tried and gathers both layers onto +the same lines. + +## Contours' edge is thin on purpose + +The line was a linear falloff across its whole width, and under any +motion it sparkled at every speed: a line shifting a quarter of a cell +per frame re-rolled the dither across its whole width, and the flips +landed as scatter. Cells flipping per frame scale with speed either +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. + +## Six orthogonal controls drive animation Don't re-entangle them. **Motion** (General tab) is what a pass looks like, -**Speed** beneath it is how long an animated pass lasts, **Reroll every** is -how long a still one is held, and the **GIF** menu is how many passes to -record. A GIF export is capped at `GIF_FRAME_BUDGET` (240) frames; smoothness +**Speed** beneath it is how long an animated pass lasts, **Depth** is how +far it goes (every kind but Drift, whose travel is its period and cannot +change), **Direction** runs the phase backwards, **Reroll every** is how +long a still one is held, and the **GIF** menu is how many passes to +record. Depth is applied inside the generator as `options.depth`, a +multiplier whose 1 leaves every constant's arithmetic exactly as it was, +and Direction as `options.direction`, which maps the phase to one minus +itself. Both are tail fields whose zero is the default, so a link from +before them opens as it did. A GIF export is capped at `GIF_FRAME_BUDGET` (240) frames; smoothness gives way before file size does, and at the default settings that cap is not what binds, the per-pass smoothness is. @@ -214,7 +271,8 @@ only, and every tail field's zero must mean "what this was before the 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 82 characters. +two durations are always written, so every new link is the same length +(83 characters since depth and direction 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 1c160e2..fe35384 100644 --- a/README.md +++ b/README.md @@ -22,7 +22,7 @@ mechanic it runs on. ## Examples -Four exported GIFs, one per motion, each a single 3-second cycle at half the +Six exported GIFs, one per motion, each a single 3-second cycle at half the app's own frame rate to keep the files small. Every setting and both seeds hold; only the phase moves, and the last frame runs back into the first with no cut. @@ -30,9 +30,8 @@ no cut. + the three below: each clip renders about half again as large, which + these need since the motion is the whole point of them. --> @@ -58,6 +57,18 @@ no cut. + + + + + + + + + + + +
LoopThe 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.
RippleTide
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, @@ -121,9 +132,10 @@ of the sidebar or the end of the settings panel on a phone. ### Motion -- Loop, Drift, Wind, or Pulse move a composition instead of replacing it, cycling back with no cut +- Loop, Drift, Wind, Pulse, Ripple, or Tide move a composition instead of replacing it, cycling back with no cut - 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 ### Exporting and sharing @@ -153,7 +165,7 @@ rather than showing a screenshot of it: on why a 4-way mirror guarantees a tile repeats with no seam, and what breaks it. - [How to make a looping animated background](https://flield.com/animated-backgrounds/), - on the four motions, why they return to their first frame exactly, and + on the six motions, why they return to their first frame exactly, and which file format a hero background actually wants. ## Running locally diff --git a/animated-backgrounds/index.html b/animated-backgrounds/index.html index 235d094..5127fa9 100644 --- a/animated-backgrounds/index.html +++ b/animated-backgrounds/index.html @@ -16,7 +16,7 @@ } How to make a looping animated background for a website - + @@ -26,14 +26,14 @@ - + - + @@ -46,7 +46,7 @@ "@type": "TechArticle", "@id": "https://flield.com/animated-backgrounds/#article", "headline": "How to make a looping animated background for a website", - "description": "Four kinds of motion that return to their first frame with no cut, when a seamless loop is worth it, and which file format a hero background wants.", + "description": "Six kinds of motion that return to their first frame with no cut, when a seamless loop is worth it, and which file format a hero background wants.", "url": "https://flield.com/animated-backgrounds/", "mainEntityOfPage": "https://flield.com/animated-backgrounds/", "about": { "@id": "https://flield.com/#app" }, @@ -87,9 +87,12 @@