Skip to content

docs: rebuild the hype reel around one visual idea per scene - #84

Merged
sjquant merged 12 commits into
mainfrom
hype-reel-quality
Aug 7, 2026
Merged

docs: rebuild the hype reel around one visual idea per scene#84
sjquant merged 12 commits into
mainfrom
hype-reel-quality

Conversation

@sjquant

@sjquant sjquant commented Aug 6, 2026

Copy link
Copy Markdown
Owner

Why

examples/product_hype_reel.py advertised "eight distinct visual ideas instead of one repeated card template" and shipped the opposite: the same eyebrow → headline → body stack at the same y on every scene, a giant static number under five of them, a dead band through the middle of every frame, and every supporting label clamped to the same 48px. As the flagship animated-export example it also demonstrated none of the motion API it exists to sell — no AnimationSpec, no animated counters — and left a low-contrast warning that its own test asserted was acceptable.

Changes

  • Each scene is now built on its own idea: a live trace, a timeline whose playhead runs past the days it lit, three signals converging into one score, a full instrument, a plan that shortens on screen, an accumulating field, an eight-week distance chart, and one clear action

  • Readings count to their value through Canvas.counter(...), headlines rise line by line, comparisons grow through bar_grow, and the playhead crosses on a PositionTrack — the canonical motion contract rather than legacy effects alone

  • A RadialGradient stage light per act replaces flat black and warms from steel to product blue at the narrative turn

  • Every on-screen comparison is drawn to one scale: the planned and adjusted sessions share one pixels-per-minute rule, and the proof chart rises in all eight columns. Tests hold both

  • Fixes the defects behind the old look — day labels hand-spaced out of alignment with their dots, a strikethrough rule overshooting the value it crossed, signal rows colliding with the field behind them, and text blocks measured under the Reels action rail. deck.diagnose() now returns nothing to report

  • Fixes two library defects the rewrite surfaced. A layer carrying an AnimationSpec crashed PPTX export with an AttributeError, because the timing pipeline only reads legacy Animation fields; the exporter now lowers each preset onto the PowerPoint effect it most nearly is and drops the ones PowerPoint cannot express. And the worst-tile contrast rule took a tile's own maximum alpha as the opacity of the text it was measuring, so a 32px window clipping only a glyph's antialiased edge scored a solid 150px headline as 27% opaque and failed it at 1.57 — a verdict that depended on which rasteriser placed the glyph edge. Opacity now comes from the whole rendered layer; text genuinely drawn faint still reports

  • Motion now fills each scene instead of finishing in the first second and holding a still frame for the next three, which is what read as stuttering: entrances take a beat, no stagger is finer than a frame at 30fps, and the two scenes labelled LIVE keep reading for their whole length. Longest frozen stretch per scene, before to after: hook 3.00s → 0.07s, live sync 2.17s → 0.07s, streak 2.67s → 1.77s

  • Every scene's first animation triggers on arrival rather than on the default on_click, so the HTML slideshow plays a scene when it reaches it instead of waiting for a click

  • A counter rendered without a timeline showed the value it started from, so PNG, SVG, PDF and the PPTX handoff all printed 96, 0 and 32 where the film lands on 148, 84 and 24. A still shows the state a composition settles into

  • The closing field lit from a blue brighter than the accent, taking white type on it to 3.37; it now deepens outward from the accent and the film's lowest reading is 3.82

  • The outcome scene no longer quotes a beta user who does not exist or call that quote verified — the eight-week chart carries the claim and the figures are labelled illustrative

Narration, scene order, and the 34.69-second beat grid are unchanged — the eight bundled voiceovers have no transcript, so every scene keeps its topic and its slot.

Note: the checked-in preview GIF is 3.4 MB, up from 1.8 MB. Continuous motion means almost every frame is now distinct, so the encoder has far less to reuse; 432x768 at 64 colours is where it lands without visible damage. Cutting the GIF to a single scene the way ordinary_moments.py does would take it to 0.68 MB, if a whole-film GIF is not worth the size.

sjquant added 6 commits August 6, 2026 13:23
A layer carrying an AnimationSpec crashed PPTX export with an
AttributeError: the timing pipeline reads legacy Animation fields, and
nothing lowered a canonical spec onto them. The exporter now maps each
preset onto the PowerPoint effect it most nearly is, and drops the ones
PowerPoint cannot express — a keyframed timeline, a continuous or
per-glyph preset — so those layers ship in their settled state instead
of taking the whole export down with them.

Also renames the module-local Animation alias, which shadowed the
AnimationSpec model it was hiding a gap in.
The example claimed eight distinct visual ideas and shipped one card
template repeated eight times: the same eyebrow/headline/body stack at
the same y on every scene, a giant static number under five of them, a
dead band through the middle of the frame, and every supporting label
clamped to the same 48px. It also demonstrated none of the motion API it
exists to sell — no AnimationSpec, no animated counters — while carrying
a low-contrast warning its own test asserted was acceptable.

Each scene is now built on its own idea: a live trace, a timeline whose
playhead runs past the days it lit, three signals converging into one
score, a full instrument, a plan that shortens on screen, an
accumulating field, an eight-week chart, and one clear action. Readings
count to their value through Canvas.counter, headlines rise line by
line, comparisons grow through bar_grow, and the playhead crosses on a
PositionTrack. A radial stage light per act replaces flat black and
warms from steel to product blue at the turn.

Fixes the defects behind the old look: day labels hand-spaced out of
alignment with their dots, a strikethrough rule overshooting the value
it crossed, signal rows colliding with the field behind them, and text
blocks measured under the Reels action rail. diagnose() now returns
nothing to report, and the tests hold it there.

Narration, scene order and the 34.69s beat grid are unchanged.
The dimmed "planned" reading sat at a 2.73 worst-tile ratio against its
panel — enough on macOS, under the 2.0 floor once Linux rasterised the
same glyphs. It moves one step up, still below MUTED so it reads as
superseded, now at 4.5 with room for either renderer.

The layout assertion also reports the scene and layer of anything it
finds, so the next such failure names itself.
The worst-tile contrast rule took a tile's own maximum alpha as the
opacity of the text it was measuring. A 32px window that clips only a
glyph's antialiased edge therefore reported that fringe as the text
colour: a 150px headline painted at full opacity was scored as if it
were 27% opaque and failed at 1.57 against a background it actually
clears by a factor of seven. Which tiles catch only an edge depends on
the rasteriser, so the same composition passed on one machine and failed
on another.

How opaque text is drawn is a property of the treatment, not of a window
onto it, so the opacity now comes from the whole rendered layer. Text
genuinely drawn faint still reports, because then the layer's own
maximum alpha is low everywhere.
getchannel returns a single-band image, but getextrema's type admits the
multiband tuple-of-tuples form, so the type check rejected passing the
maximum straight through.
Every scene animated for about a second and then held a still frame for
the next three. Eight of those in a row is what reads as stuttering; the
frames were all there, the film simply stopped moving.

Entrances now take a beat rather than half of one, so the choreography
occupies the scene instead of clearing out of it, and the two staggers
that were finer than a frame at 30fps no longer land two elements on the
same frame. The scenes that call their reading LIVE keep reading: each
trace bar carries one timeline that grows it in on its own beat and then
undulates for the rest of the scene, only ever shortening so the tallest
bars cannot reach the label above them. The timeline scene's playhead
was always moving but was too thin to see, so it gained weight.

Arrival and life are one curve because a unit in animated export carries
either legacy effects or one canonical timeline, never both: a layer
given a list of the two loses its canonical spec entirely.

Longest frozen stretch per scene, before to after: hook 3.00s to 0.07s,
live sync 2.17s to 0.07s, streak 2.67s to 1.77s, proof 1.97s to 1.07s.
@sjquant sjquant self-assigned this Aug 6, 2026
sjquant added 2 commits August 7, 2026 01:56
A counter rendered without a timeline showed the number it started from,
so every still surface — PNG, SVG, PDF and the PPTX handoff — printed 96,
0 and 32 where the film lands on 148, 84 and 24. A still shows the state
a composition settles into, which is the value the count finished on.

The closing field lit from a blue brighter than the accent, taking the
white type on it down to 3.37 against a 2.0 floor with no headroom; it
now deepens outward from the accent instead, and the secondary lines are
white rather than pale tints. The film's lowest reading is 3.82.

The outcome scene quoted a beta user who does not exist and called the
quote verified. The eight-week chart already carries that claim, so the
copy states the outcome and labels the figures illustrative.

Every scene's first animation took the default on_click trigger, so the
HTML slideshow sat on each slide waiting for a click before anything
moved, and the whole chain hung off it. A scene starts when it arrives.

The preview GIF is cut to 432x768 at 64 colours.
The exported HTML carries a content-derived state id, and this one's hex
opened with "ba", which the typo hook read as a misspelling of "by".
Nothing in that file is written by a person, and the id changes with the
composition, so the failure would return at random.
@sjquant sjquant closed this Aug 6, 2026
@sjquant sjquant reopened this Aug 6, 2026
sjquant added 4 commits August 7, 2026 08:19
The HTML runtime advances a slide by chaining groups: a node marked
after_previous continues the chain and with_previous joins the open
group. A canonical animation was emitted with no trigger at all, which
is neither, so it stalled the chain and became a click of its own. The
reel's heart-rate trace is sixteen layers, so a viewer had to click
sixteen times to get through one scene, and the same was true of every
canonical animation in every deck.

A canonical event carries its own place on the slide clock, so it now
runs alongside the group it belongs to, keeping any trigger the author
declared.

The reel stops asking for that place in absolute time, which the runtime
reads relative to the group a node joins rather than to the slide. Its
copy stack is also measured with the line stagger it actually has, the
shortest scene arrives its two labels beside the trace rather than
behind it, and a trace bar can no longer schedule an arrival longer than
the window holding it.
The trace's window is sized against where the copy stack settles, and
that figure is kept by hand — it was already wrong once, by exactly the
line stagger the headline adds. Nothing caught it, because a scene whose
animation outruns its beat still renders; the layers scheduled past the
cut simply never appear.

The slideshow timing now asserts what the runtime needs: no node waits
for a click, and every chain lands inside the beat its scene was cut to.
The stagger is chosen in frames, so the rate those frames are counted at
cannot be left to the exporter's default.
The HTML runtime counts a node's delay from the moment its group starts,
so an animation that named an absolute place on the slide clock landed
that far after its group instead of at the time it asked for. Walking the
chain gives each group's own start, which is what the absolute figure has
to be measured against; a moment already past by then plays as soon as it
can.
@sjquant
sjquant merged commit 1c65e58 into main Aug 7, 2026
3 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant