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:
| Control | Live? | Becomes |
|---|---|---|
| text | yes | set-text |
| size | yes | resize |
| colour | yes | recolor |
| x / y | yes | move |
| in at | yes | retime |
| entrance | yes | swap-motion |
| weight, in dur | yes | a spec edit — no op covers these |
| palette roles | yes | an 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 editop array - Commands — apply, re-render, then
contrastandbrand_checkto 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.jsoninvocation to make it real, followed by thecontrastandbrand_checkcalls 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.