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
2 changes: 1 addition & 1 deletion animated-backgrounds/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -258,7 +258,7 @@ <h2 id="try">Try it</h2>
</footer>
</main>

<script src="../generator.js?v=11"></script>
<script src="../generator.js?v=12"></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
2 changes: 1 addition & 1 deletion flow-fields/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -276,7 +276,7 @@ <h2 id="try">Try it</h2>
</footer>
</main>

<script src="../generator.js?v=11"></script>
<script src="../generator.js?v=12"></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
43 changes: 38 additions & 5 deletions generator.js
Original file line number Diff line number Diff line change
Expand Up @@ -1122,20 +1122,32 @@ function maskKeepProbability(shape, direction, x, y, cols, rows) {
return 1;
}

function applyShapeMask(grid, cols, rows, shape, seed, direction) {
if (!shape || shape === "none") return;
// A layer's mask is a pure function of the shape, its direction, the grid
// size and the layer's seed: nothing in it moves with the phase. It used
// to be recomputed for every cell on every frame, and for the radial
// shapes each cell allocated a corner list and took four square roots,
// which doubled the cost of an animated frame. It is built once here as a
// keep/drop byte per cell and reused. The random stream is read in the
// same cell order as before, so a masked grid comes out bit-identical to
// what the per-frame version drew. Same shape of cache as the field's: a
// few entries cover both layers, the tutorial demos and the favicon.
const SHAPE_MASK_CACHE_LIMIT = 6;
const shapeMaskCache = new Map();

function buildShapeMask(cols, rows, shape, seed, direction) {
const keep = new Uint8Array(cols * rows).fill(1);
const rand = seededRandom(seed + "|mask");

if (GRADIENT_SHAPE_MASKS.has(shape)) {
for (let y = 0; y < rows; y++) {
const row = y * cols;
for (let x = 0; x < cols; x++) {
if (rand() > maskKeepProbability(shape, direction, x, y, cols, rows)) {
grid[row + x] = 0;
keep[row + x] = 0;
}
}
}
return;
return keep;
}

const inner = 1 - SHAPE_MASK_FEATHER;
Expand All @@ -1146,10 +1158,31 @@ function applyShapeMask(grid, cols, rows, shape, seed, direction) {
const d = shapeDistance(shape, x, y, cols, rows);
if (d <= inner) continue;
if (d >= outer || rand() > 1 - (d - inner) / (outer - inner)) {
grid[row + x] = 0;
keep[row + x] = 0;
}
}
}
return keep;
}

function shapeMaskFor(cols, rows, shape, seed, direction) {
const key = `${cols}|${rows}|${shape}|${direction || ""}|${seed}`;
const cached = shapeMaskCache.get(key);
if (cached) return cached;
const keep = buildShapeMask(cols, rows, shape, seed, direction);
if (shapeMaskCache.size >= SHAPE_MASK_CACHE_LIMIT) {
shapeMaskCache.delete(shapeMaskCache.keys().next().value);
}
shapeMaskCache.set(key, keep);
return keep;
}

function applyShapeMask(grid, cols, rows, shape, seed, direction) {
if (!shape || shape === "none") return;
const keep = shapeMaskFor(cols, rows, shape, seed, direction);
for (let i = 0; i < grid.length; i++) {
if (!keep[i]) grid[i] = 0;
}
}

function layerOptionsToField(layer) {
Expand Down
2 changes: 1 addition & 1 deletion guide/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -178,7 +178,7 @@ <h3>Tabs</h3>
<tbody>
<tr>
<td><strong>General</strong></td>
<td>Canvas width and height, with Web, Tablet, Mobile and Tile presets. A preset's chip stays lit while the values still match it. Also background color, grid resolution (block size), shape mask, motion, speed and the Auto-randomize interval</td>
<td>Canvas width and height, with Web, Tablet, Mobile and Tile presets. A preset's chip stays lit while the values still match it. Also background color, grid resolution (block size) and shape mask, then an Animation section holding Motion and its Speed slider, the Auto-randomize interval, and three save slots</td>
</tr>
<tr>
<td><strong>Layer A</strong> / <strong>Layer B</strong></td>
Expand Down
18 changes: 14 additions & 4 deletions index.html
Original file line number Diff line number Diff line change
Expand Up @@ -548,6 +548,16 @@ <h2 class="section-title">Settings</h2>
<select id="shapeMaskDirection"></select>
</div>

<!-- Its own section rather than the last field under Settings,
headed Animation so it does not repeat the field's own label:
motion is what the site leads with, so it should be findable
as a heading, and it is one of the four controls that drive
animation (see AGENTS.md) rather than a canvas setting. Second
in the tab, not first: size, colors and mask come before how
it moves, a still is the default, and the flash warning below
should not be the first line a new visitor reads. -->
<h2 class="section-title">Animation</h2>

<div class="field">
<label for="motion">Motion</label>
<p class="hint">Plays on the canvas as soon as you pick one.</p>
Expand All @@ -563,9 +573,9 @@ <h2 class="section-title">Settings</h2>

<!-- Revealed with a motion, the same way the shape mask's
direction is revealed with a shape that has one. A cycle
length means nothing without a cycle, and holding the pair
together is what keeps it from reading as part of the
Auto-randomize interval below. -->
length means nothing without a cycle, and keeping the pair
under one heading is what keeps it from reading as part of
the Auto-randomize interval in the next section. -->
<div class="field" id="speedField" hidden>
<label for="speed">Speed <span class="field-value" id="speedVal"></span></label>
<p class="hint">How long a cycle takes on screen and in GIFs.</p>
Expand Down Expand Up @@ -909,7 +919,7 @@ <h1 class="brand"><a href="./">Flield<span class="sr-only">: seamless animated b
</script>
</div>

<script src="generator.js?v=11"></script>
<script src="generator.js?v=12"></script>
<script>
// Inline replacements for the icons that used to come from the
// Phosphor webfont (224KB for the ~19 glyphs this app actually uses).
Expand Down
2 changes: 1 addition & 1 deletion seamless-backgrounds/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -198,7 +198,7 @@ <h2 id="try">Try it</h2>
</footer>
</main>

<script src="../generator.js?v=11"></script>
<script src="../generator.js?v=12"></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