docs: rebuild the hype reel around one visual idea per scene - #84
Open
sjquant wants to merge 12 commits into
Open
docs: rebuild the hype reel around one visual idea per scene#84sjquant wants to merge 12 commits into
sjquant wants to merge 12 commits into
Conversation
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.
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.
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.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Why
examples/product_hype_reel.pyadvertised "eight distinct visual ideas instead of one repeated card template" and shipped the opposite: the same eyebrow → headline → body stack at the sameyon 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 — noAnimationSpec, no animated counters — and left alow-contrastwarning 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 throughbar_grow, and the playhead crosses on aPositionTrack— the canonical motion contract rather than legacy effects aloneA
RadialGradientstage light per act replaces flat black and warms from steel to product blue at the narrative turnEvery 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 reportFixes two library defects the rewrite surfaced. A layer carrying an
AnimationSpeccrashed PPTX export with anAttributeError, because the timing pipeline only reads legacyAnimationfields; 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 reportsMotion 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
LIVEkeep 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.77sEvery 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 clickA 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.pydoes would take it to 0.68 MB, if a whole-film GIF is not worth the size.