> ## Documentation Index
> Fetch the complete documentation index at: /llms.txt
> Use this file to discover all available pages before exploring further.

# Snow

> Seek-safe falling-snow overlay: sparse or dense flakes drift down with size, speed and opacity correlated per flake for depth, radial-masked so the frame center stays clear

# Snow

Seek-safe falling-snow overlay: sparse or dense flakes drift down with size, speed and opacity correlated per flake for depth, radial-masked so the frame center stays clear

**Snow**

```html snow.html
<!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>
```

Copy npx hyperframes add snow to the clipboard

That writes one file: `compositions/snow.html`.

## Add it to your video

It runs for 6 seconds at 1920×1080. Paste this into your composition:

```html index.html
<div
  data-composition-id="snow"
  data-composition-src="compositions/snow.html"
  data-start="0"
  data-duration="6"
  data-track-index="1"
  data-width="1920"
  data-height="1080"
></div>
```

Move it in time with `data-start`. Put it on a different timeline row with
`data-track-index`. See [data attributes](/concepts/data-attributes.md) for the rest.

## Variables

Every one of these has a default, so the piece works untouched. Set the ones you
want to change on the element:

| Variable | Default | Accepts | What it does |
| - | - | - | - |
| `density` | `sparse` | `sparse`, `dense` | Flake count across the frame. |
| `speed` | `calm` | `calm`, `gust` | How fast flakes fall and sway. |

Set them with `data-variable-values` on the element that mounts it. These are the
defaults, so this behaves exactly like the preview above until you change one:

```html wrap
<div
  data-composition-id="snow"
  data-composition-src="compositions/snow.html"
  data-variable-values='{"density":"sparse","speed":"calm"}'
></div>
```

{/* hf:generated-footer */}

Tagged `snow` `snowfall` `winter` `weather` `overlay` `ambient` `particles` `media-treatment-overlay` `loopable`.

## Related topics

* [Browse the complete Catalog](/catalog.md)
* [Add assets and Catalog items in the browser editor](/studio/assets-and-blocks.md)
* [Put blocks together into one video](/concepts/compositions.md)
