Files
hyperframes/registry/blocks/snow/snow.html
Miguel Ángel 590c192498 feat(catalog): add 3D objects, cursors, snow, starfield and paper transitions (#4349)
* feat(catalog): shelve 3D objects and cursors in their own catalog sections

Carries the generator grouping rules from #4310 and #4308.

* feat(registry): add a starfield ambient overlay

From #4313.

* feat(registry): add a falling snow ambient overlay

From #4331.

* feat(registry): add a torn-paper-rip transition

From #4321.

* feat(registry): add a paper-collage-reveal transition

From #4322.

* feat(registry): add a curtain-reveal transition

From #4323.

* feat(registry): add 3D objects: product turntable, glass hero, exploded stack, extruded logo

From #4310.

* feat(registry): add eight cursor animations and the dart cursor

From #4308.

* fix(catalog): find items by everyday editor words and accept a bare search word

From #4311.

* chore(registry): remove the simulated-cursor component

The Cursors section's new pointer animations replace it (from #4308).

* fix(registry): take cursor-zoom-follow's screenshot from runtime variables only

The declared default is empty, so page markup never becomes an image URL.

Clears the code-scanning alert raised on #4308.

* docs(cli): show a catalog search example that finds items

"smoke" matches nothing in the catalog; "crossfade" finds fade-through.

* chore(ci): allow the simulated-cursor removal in the deletion guard

* fix(ci): skip tracked files missing from disk in the player CDN pin check

Catalog regeneration removes a deleted item's pages before the publish PR untracks them,

so the check crashed on every catalog deletion PR.

* chore(cli): drop comments that narrate catalog search history

* fix(registry): drag-drop keeps the family arrow and shows the drop slot only while hovering

* fix(registry): agent cursor labels its steps in an accent tag, no glow or sparkles, in 4.8 s

* fix(registry): annotate cursor draws a tapered marker stroke and clears the circled value

* fix(registry): show real file thumbnails and end the context menu demo sooner

* fix(registry): give marquee select real photos, a Finder-blue marquee and a tighter loop

* fix(registry): centre the text-select toolbar on the selection and tighten its timing

* fix(registry): selection cursors default to the macOS system blue

* style(registry): format the agent and annotate cursor sources

* fix(registry): dart-cursor stops covering the choice it makes and shows the export finishing

* fix(registry): frame the product turntable fully and light it with two studio softboxes

* fix(registry): touch-indicator taps like a real phone and the post it taps gets liked

* fix(registry): press-ripple confirms with a check and small click rings at the cursor

* fix(registry): exploded stack separates at once and marks each layer in blue as it is labelled

* fix(registry): extruded logo settles facing the camera, set in Geist Bold with a neutral rim

* fix(registry): magnetic cursor docks its toolbar on the selection and ends after the press

* style(registry): format the exploded stack source

* fix(registry): glyph trail becomes a tutorial cursor path with numbered steps on a settings screen

* fix(registry): make glass-hero a clear lens that shows the headline through it

* fix(registry): snow reads as real snowfall with bokeh flakes over a winter title card

* fix(registry): oversized cursor is a 3.5x arrow that clicks a real editor toggle on

* fix(registry): starfield reads as a drifting night sky over a real title card

* style(registry): format the glass hero source

* fix(registry): make the paper tear show a white fibrous edge and lift the torn sheet away

* fix(registry): curtain reveal draws velvet folds that gather onto a title card

* fix(registry): make every zoom-follow click change the screen and read at zoom

* fix(registry): paper collage pieces stay opaque, tear with deckled grain and lift away on an arc

* refactor(registry): drop dead code and correct stale comments in reworked items

* fix(registry): agent cursor scrubs cleanly, docs match press-ripple, fictional demo names

* docs(registry): catalog notes say oversized-cursor exits and how it differs from press-ripple

* docs(registry): plain lowercase comments in oversized-cursor and touch-indicator

* docs(registry): oversized-cursor names CLICK_AT as the click cue
2026-09-28 22:03:59 +00:00

205 lines
8.0 KiB
HTML
Vendored

<!doctype html>
<!--
snow: HyperFrames video primitive (backgrounds / ambient overlay)
Concept: snowflakes fall from top to bottom in a continuous seamless loop, drifting
side to side, meant to sit on a track above real footage rather than as an opaque
background. Each flake draws one depth value that sets size and opacity
and buckets it into one of three fall-speed layers (near/mid/far); the loop invariant
already forces fall speed to an integer number of cycles per mount, so three
deliberately distinct per-layer counts keep near/far separation even on a short mount.
A radial mask keeps the frame center clear for a subject or caption underneath.
Variables:
- density (sparse | dense, default sparse): flake count across the frame.
- speed (calm | gust, default calm): how fast flakes fall and sway.
Envelope: IN = 0, HOLD = the full elastic duration (the host clip's data-duration), OUT = 0.
Each flake runs its own whole number of fall cycles (and, separately, sway cycles)
during HOLD, so a longer mount adds more falls instead of stretching a single one thin.
Loop invariant: a flake's vertical position is frac(integer * phase + const) and its
sway is sin(2*PI * integer * phase + const), so phase = 0 and phase = 1 (t = 0 and
t = duration) always match exactly.
Sound: none.
Mount contract: the runtime clones only this file's body. The root has no data-width or
data-height requirement beyond the block's own declared dimensions, and registers one
paused timeline under the hardcoded snow id.
-->
<html lang="en">
<head>
<meta charset="UTF-8" />
<script src="https://cdn.jsdelivr.net/npm/gsap@3.14.2/dist/gsap.min.js"></script>
<style>
*,
*::before,
*::after {
box-sizing: border-box;
}
html,
body {
width: 1920px;
height: 1080px;
margin: 0;
overflow: hidden;
background: transparent;
}
#hf-snow {
position: absolute;
inset: 0;
overflow: hidden;
pointer-events: none;
/* Center clearance: a subject placed mid-frame stays readable by default. */
-webkit-mask-image: radial-gradient(
circle 230px at 50% 50%,
transparent 0 230px,
#000 653px
);
mask-image: radial-gradient(circle 230px at 50% 50%, transparent 0 230px, #000 653px);
}
#hf-snow .hf-flake {
position: absolute;
top: 0;
left: 0;
border-radius: 50%;
background: #fff;
will-change: transform;
}
</style>
</head>
<body>
<div
id="snow"
data-composition-id="snow"
data-start="0"
data-duration="6"
data-width="1920"
data-height="1080"
data-composition-variables='[
{ "id": "density", "type": "enum", "role": "style", "label": "Density", "description": "Flake count across the frame.", "default": "sparse", "options": [{ "value": "sparse", "label": "Sparse" }, { "value": "dense", "label": "Dense" }] },
{ "id": "speed", "type": "enum", "role": "style", "label": "Fall speed", "description": "How fast flakes fall and sway.", "default": "calm", "options": [{ "value": "calm", "label": "Calm" }, { "value": "gust", "label": "Gust" }] }
]'
>
<div id="hf-snow" aria-hidden="true" data-layout-allow-overflow=""></div>
</div>
<script>
window.__timelines = window.__timelines || {};
(function () {
var field = document.getElementById("hf-snow");
// Mounted, the runtime strips data-duration off this file's root; the host clip carries it.
var host = field.closest("[data-composition-id]");
var vars =
window.__hyperframes && window.__hyperframes.getVariables
? window.__hyperframes.getVariables()
: {};
// The host's data-width/data-height are authoritative over this file's own root
// (compositionLoader.ts) — read them instead of a third hardcoded 1920x1080.
var W = Number(host.getAttribute("data-width")) || 1920;
var H = Number(host.getAttribute("data-height")) || 1080;
var duration = Number(host.getAttribute("data-duration")) || 6;
var COUNT = vars.density === "dense" ? 280 : 120;
var fallSeconds = vars.speed === "gust" ? 2.2 : 4.5;
var swaySeconds = vars.speed === "gust" ? 2.2 : 4;
// Even 2D coverage with no clumping and no seed to store (Weyl low-discrepancy
// sequence): successive multiples of an irrational constant, taken mod 1.
var PHI = 0.6180339887498949; // (sqrt(5) - 1) / 2
var RT2 = 0.4142135623730951; // sqrt(2) - 1
function frac(x) {
return x - Math.floor(x);
}
// Deterministic per-index hash (sin fold; no Math.random) for scalars that only
// need decorrelation, not even coverage. registry/components/stop-motion-cadence's
// rnd(n).
function rnd(n) {
var x = Math.sin(n * 127.1 + 311.7) * 43758.5453;
return x - Math.floor(x);
}
var TAU = 2 * Math.PI;
// A whole number of cycles over the mount for a given per-cycle duration; the
// loop invariant forces this to round to an integer, so callers that need distinct
// speeds must space their inputs, not just their multipliers (rounding a sub-1
// product always collapses to 1 on a short mount).
function wholeCycles(cycleSeconds) {
return Math.max(1, Math.round(duration / cycleSeconds));
}
// Flakes enter above the top edge and exit below the bottom before wrapping, so the
// loop seam never pops one into view mid-frame. It must clear the largest flake's
// radius plus its blur (9px + 4px at 1080 tall); a correctness bound, not taste.
var WRAP_MARGIN = 0.02;
var fallBase = wholeCycles(fallSeconds);
var swayBase = wholeCycles(swaySeconds);
var flakes = [];
var fragment = document.createDocumentFragment();
for (var i = 0; i < COUNT; i++) {
var laneX = frac(i * PHI + 0.07);
// One depth draw drives size and opacity, and buckets the flake into
// one of three fall-speed layers (far/mid/near). Layer cycle counts are offsets
// from the shared base, not products of it, so they stay distinct on any mount.
var depth = rnd(i);
var sizePx = 2 + depth * depth * 16;
// The nearest flakes sit in front of the focal plane: large, soft and half see-through.
var bokeh = depth > 0.85;
var opacity = bokeh ? 0.5 : 0.35 + depth * 0.55;
var layer = depth < 1 / 3 ? 0 : depth < 2 / 3 ? 1 : 2;
var el = document.createElement("div");
el.className = "hf-flake";
el.style.width = sizePx + "px";
el.style.height = sizePx + "px";
el.style.opacity = opacity;
if (bokeh) el.style.filter = "blur(2px)";
fragment.appendChild(el);
flakes.push({
el: el,
laneX: laneX,
fallCycles: fallBase + layer,
fallPhi: frac(i * RT2 + 0.37),
swayPx: 6 + depth * 24,
swayCycles: swayBase + Math.round(rnd(i + 5000) * 2),
swayPhi: TAU * rnd(i + 3000),
});
}
field.appendChild(fragment);
var state = { phase: 0 };
function render() {
var p = state.phase;
for (var s = 0; s < flakes.length; s++) {
var f = flakes[s];
var yFrac = frac(f.fallCycles * p + f.fallPhi);
var y = (-WRAP_MARGIN + yFrac * (1 + 2 * WRAP_MARGIN)) * H;
var x = f.laneX * W + f.swayPx * Math.sin(TAU * f.swayCycles * p + f.swayPhi);
f.el.style.transform =
"translate(" + Math.round(x) + "px, " + Math.round(y) + "px) translate(-50%, -50%)";
}
}
var tl = gsap.timeline({ paused: true });
tl.to(state, { phase: 1, duration: duration, ease: "none", onUpdate: render }, 0);
render();
window.__timelines["snow"] = tl;
})();
</script>
</body>
</html>