Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
62 changes: 50 additions & 12 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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

Expand All @@ -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,
Expand Down Expand Up @@ -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
Expand Down
42 changes: 10 additions & 32 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,9 +29,9 @@ no cut.

<!-- HTML rather than a markdown table so the columns can be given equal
widths; a markdown table sizes them by caption length, and the GIFs
scale to whatever each column ends up with. Two columns here against
the three below: each clip renders about half again as large, which
these need since the motion is the whole point of them. -->
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. -->
<table>
<tr>
<th width="50%">Loop</th>
Expand All @@ -42,8 +42,8 @@ no cut.
<td width="50%"><img src="examples/drift.gif" width="100%" alt="Drift"></td>
</tr>
<tr>
<td width="50%">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.</td>
<td width="50%">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.</td>
<td width="50%">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.</td>
<td width="50%">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.</td>
</tr>
<tr>
<th width="50%">Wind</th>
Expand All @@ -54,8 +54,8 @@ no cut.
<td width="50%"><img src="examples/pulse.gif" width="100%" alt="Pulse"></td>
</tr>
<tr>
<td width="50%">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.</td>
<td width="50%">Density swings on one sine wave, Layer B at 70% of the depth, so the streaks swell and thin without moving.</td>
<td width="50%">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.</td>
<td width="50%">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.</td>
</tr>
<tr>
<th width="50%">Ripple</th>
Expand All @@ -66,31 +66,8 @@ no cut.
<td width="50%"><img src="examples/tide.gif" width="100%" alt="Tide"></td>
</tr>
<tr>
<td width="50%">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.</td>
<td width="50%">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.</td>
</tr>
</table>

Three more, each thirteen Auto-randomize passes at a two-second interval,
showing three ways to use the locks.

<!-- Three equal columns, for the same reason as the table above: a
markdown table would size them by caption length instead. -->
<table>
<tr>
<th width="33%">Full random</th>
<th width="33%">Brand campaign</th>
<th width="33%">Kaleidoscope variations</th>
</tr>
<tr>
<td width="33%"><img src="examples/full-random.gif" width="100%" alt="Full random"></td>
<td width="33%"><img src="examples/brand-campaign.gif" width="100%" alt="Brand campaign"></td>
<td width="33%"><img src="examples/kaleidoscope-variations.gif" width="100%" alt="Kaleidoscope variations"></td>
</tr>
<tr>
<td width="33%">Only block size and shape mask locked; every other field rerolls each pass.</td>
<td width="33%">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.</td>
<td width="33%">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.</td>
<td width="50%">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.</td>
<td width="50%">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.</td>
</tr>
</table>

Expand Down Expand Up @@ -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

Expand Down
9 changes: 5 additions & 4 deletions animated-backgrounds/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -179,7 +179,7 @@ <h2 id="motions">The six motions</h2>
<button type="button" class="btn" id="playBtn">Pause</button>
<button type="button" class="btn" id="rerollBtn">Reroll</button>
</div>
<p class="demo-caption">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.</p>
<p class="demo-caption">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.</p>
</div>

<h2 id="length">How long a cycle lasts</h2>
Expand All @@ -188,7 +188,7 @@ <h2 id="length">How long a cycle lasts</h2>

<p>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.</p>

<p>Two more controls sit beside it. <strong>Depth</strong> is how far the motion goes, from a quarter of its usual reach to twice it. <strong>Direction</strong> runs the cycle backwards, which is how Drift and Wind travel against the flow without the still changing.</p>
<p>Three more controls sit beside it. <strong>Depth</strong> is how far the motion goes, from a quarter of its usual reach to twice it. <strong>Direction</strong> runs the cycle backwards, which is how Drift and Wind travel against the flow without the still changing. <strong>Collide</strong> 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.</p>

<p>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 <a href="https://www.w3.org/WAI/WCAG22/Understanding/three-flashes-or-below-threshold.html" target="_blank" rel="noopener">WCAG 2.3.1</a> allows. Worth keeping in mind for whatever you build with the file, too.</p>

Expand Down Expand Up @@ -295,7 +295,7 @@ <h2 id="try">Try it</h2>
</footer>
</main>

<script src="../generator.js?v=13"></script>
<script src="../generator.js?v=14"></script>
<script>
// Phosphor's own path data, the same two glyphs and the same
// flield-theme key the app and the guide use, so a choice made on any
Expand Down Expand Up @@ -368,7 +368,7 @@ <h2 id="try">Try it</h2>
loopMode: mode,
loopPhase,
};
const { gridA, gridB, cols, rows } = generate(options);
const { gridA, gridB, cols, rows, warp } = generate(options);
renderToCanvas(canvas, {
...options,
colorA: options.layerA.color,
Expand All @@ -377,6 +377,7 @@ <h2 id="try">Try it</h2>
gridB,
cols,
rows,
warp,
});
}

Expand Down
Binary file removed examples/brand-campaign.gif
Binary file not shown.
Binary file modified examples/drift.gif
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file removed examples/full-random.gif
Binary file not shown.
Binary file removed examples/kaleidoscope-variations.gif
Binary file not shown.
Binary file modified examples/loop.gif
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified examples/pulse.gif
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified examples/ripple.gif
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified examples/tide.gif
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified examples/wind.gif
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
2 changes: 1 addition & 1 deletion flow-fields/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -296,7 +296,7 @@ <h2 id="try">Try it</h2>
</footer>
</main>

<script src="../generator.js?v=13"></script>
<script src="../generator.js?v=14"></script>
<script>
// Phosphor's own path data, the same two glyphs and the same
// flield-theme key the app and the guide use, so a choice made on any
Expand Down
Loading
Loading