Skip to main content

Scrubbing a take

Instruments answer "is this correct?". They cannot answer "is this good?". scrub is the handoff to human judgement.

cleya scrub --scene promo.json --brand brand.json
cleya scrub --html film.html

It writes one self-contained HTML page next to the film and opens it. No rendering, no server, no Chrome process, no build step.

What you get

  • A timeline with hover-as-scrub — preview while paused by moving across it, click or drag to pin
  • Play, pause, loop, speed control
  • ±1 and ±10 frame stepping, with time and frame readouts
  • Full keyboard control — space, arrows, home/end, 0–9 to jump

Why it is honest

The film runs live in a same-origin srcdoc iframe, driven through the same window.CLEYA.seekTo contract the render backends use. What you scrub is exactly what render captures — it is not a preview approximation.

The page waits for CLEYA.ready and document.fonts.ready before arming, and reports a plain error if a film never defines seekTo.

Films are self-describing: compiled scenes carry their length as CLEYA.duration, so scrub --html needs no --duration. Pass --no-open to write the file without launching a browser.

The edit rail

With --scene, the page carries a right-hand panel of controls — one collapsible section for the brand palette, one per layer:

ControlLive?Becomes
textyesset-text
sizeyesresize
colouryesrecolor
x / yyesmove
in atyesretime
entranceyesswap-motion
weight, in duryesa spec edit — no op covers these
palette rolesyesan updated brand.json

Everything previews immediately. Colour drives the CSS custom properties the compiler emits; timing and entrance mutate window.CLEYA.scene and re-seek, which the runtime reads on every frame. Nothing recompiles, so nothing you see here can disagree with what render will produce from the same op.

Three buttons turn your changes into something to act on:

  • Edit ops — the cleya edit op array
  • Commands — apply, re-render, then contrast and brand_check to re-measure
  • Agent prompt — a before/after brief to paste to an agent

Each copies to your clipboard. The page never writes to disk: cleya edit validates before it writes, and that stays the only thing that touches your scene.

Gradients

Anything that sets a background can be a two-stop linear gradient — pick both stops, drag the angle, and the film updates as you go.

Each stop carries its own opacity; below 100% it becomes rgba(), which is exactly what goes in the spec.

Gradients work on text as well as boxes. color: linear-gradient(...) is not a colour, so each palette role compiles to a pair — the role variable holds the colour (transparent when a gradient is in play) and --cleya-<role>-img holds the gradient, with background-clip: text on text layers so it fills the glyphs. That means a gradient role in brand.json renders as gradient text, not just in preview.

Layer fills offer the same editor and become a recolor op with property: "bg".

Session memory

The page is written fresh on every run, so it keeps its memory outside the file — in localStorage, keyed by the scene path.

  • Save iteration (left rail) snapshots the current set of edits. Click a saved iteration to restore it: every control and the film roll back together.
  • Your working state persists as you go, so reopening a regenerated page picks up where you left off rather than starting blank.

Because re-running cleya scrub opens a new tab, the cheaper loop once a page is open is to keep the tab and reload it (the file is overwritten in place), or pass --no-open when regenerating.

Re-tinting without rendering

Pass --brand and the page gains a palette bar — one swatch per brand role:

cleya scrub --scene promo.json --brand brand.json

Drag a swatch and the film re-tints instantly, on every seek, with no recompile and no render. This works because a $palette.<role> token compiles to var(--cleya-<role>, <hex>) rather than a bare hex, so the role survives into the rendered document and the page can set one custom property to change every use of it at once. The hex fallback means the film is unchanged when nothing drives the variable.

The page can't write to your disk — it is a file:// document with no server — and it shouldn't: cleya's write path validates and measures. So an edit leaves as an artifact instead:

  • Download brand.json — the BrandProfile with your edited palette
  • Copy command — the cleya render --scene … --brand brand.json invocation to make it real, followed by the contrast and brand_check calls to re-measure

That last part matters. Changing a colour is precisely the edit that quietly breaks a WCAG ratio, so the loop closes back on the instruments rather than trusting your eye.

Raw --html films can be tintable too

The bar is gated on the film, not on which flag produced it. Author your colours as var(--cleya-<role>, <hex>) — the same indirection the compiler emits — and pass --brand so the roles have names and values:

cleya scrub --html film.html --runtime gsap --brand brand.json

The hex fallbacks mean the film still renders identically on its own.

Only roles the film actually references get a swatch, so every control does something: a role defined in brand.json but never used in the film is left out, and a film whose colours are baked hex gets no bar at all — with a note on the result explaining why, rather than a row of controls that quietly do nothing.

Limits. This covers colour. Anything structural — motion, timing, layout — needs the scene spec and a recompile; use cleya edit for those, which is deterministic and validated.