mirror of
https://github.com/heygen-com/hyperframes.git
synced 2026-09-14 18:01:20 +08:00
feat/instant-patch-helper
1914 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
2a0df600d9 |
fix(studio): address #1612 review — channel-aware set resolution + decline dynamic-expression patches
- resolveRuntimeTween gains an optional channels[] hint; for kind:set it prefers the set whose
vars carry one of the patched channels and never returns a disjoint-only set (e.g. won't write
{x,y} into a co-located {rotation} set). patchRuntimeTweenInPlace derives channels from the props.
- patchSet declines (returns false → soft reload) when overwriting a string/dynamic vars[ch],
instead of silently dropping the computed expression.
|
||
|
|
03d79278d3 |
feat(studio): patchRuntimeTweenInPlace — update a tween's values in place
Defensive runtime helper: locate the element's tween in window.__timelines via the shared resolveRuntimeTween scan, update its set/keyframe vars, invalidate, and re-seek the playhead — without re-running the whole composition. Returns false (caller falls back to soft reload) for any shape it can't safely patch (no tween, dynamic/computed keyframes, motionPath arc, channel mismatch, or any error). Foundation for instant, flicker-free manual edits. |
||
|
|
5ef68d01ec |
fix(studio): address #1567 review — drop drag-intercept flag, harden softReload onerror, tighten runtime ladder, per-group gestures
- DROP STUDIO_GSAP_DRAG_INTERCEPT_ENABLED: single-source GSAP intercept is the only position/rotation channel; the false branch silently killed drag+rotate (and let GSAP elements into the keyframe-corrupting CSS path). Removed flag + dead branch + env def + tests. - gsapSoftReload: plugin onerror no longer fakes success — signals onAsyncFailure so the caller full-reloads; honors __hfMotionPathPluginLoading so a concurrent reload can't queue a dup script. - gsapDragCommit: resolveDragRuntime narrows the as-any ladder; a mid-seek throw logs + drops partial reads (no phantom identity) and re-applies the drag override in finally. - MotionPathOverlay: park-timer cleanup keyed on animId change. - useGestureCommit: partitionKeyframesByGroup wraps the add-with-keyframes sites (per #1611 review). |
||
|
|
275994748a |
feat(studio): single-source manual offset + rotation via the GSAP timeline
Dragging or rotating an element writes into the GSAP timeline (the single source of truth) instead of a parallel --hf-studio-offset / --hf-studio-rotation CSS var: static elements commit a tl.set (idempotent on re-edit), tweened elements edit keyframes, and the live preview moves via gsap.set so what you see equals what is written and renders. Removes the dual-channel CSS-var/transform reconciliation behind the fling / disappear / runaway / double-stack / wrong-start bug class — for BOTH position and rotation (gesture base read from the gsap transform, gsap.set live preview, tl.set/ keyframe commit, dropped the handleDom*Commit CSS fallbacks). Subcompositions edit the same single-source way, which surfaced and fixes: - resolve a subcomp element's source file via the composition-id map (the runtime drops the source linkage when inlining the subcomposition); - a selected element's selection box AND motion path use basic visibility, not the occlusion heuristic (a backgroundless opacity-1 scene above it is not an opaque cover); - soft reload rebuilds ONLY the committed composition's timeline, leaving other compositions' timelines intact (no cross-composition revert); - read keyframes from the element's OWN composition timeline (scan all timelines, not the first unstable key); - delete-all uses a soft reload too, so editing no longer hard-reloads the iframe. |
||
|
|
151f36b5f4 |
fix(studio): address #1611 review — fetch-first keyframe path, gated hydration, dev-gated debug + gesture warn, per-group gesture tweens
- useEnableKeyframes: parse current source first (null-vs-[] distinction) so a delete-all's empty parse isn't overridden by a stale selectedGsapAnimations cache. - useStudioUrlState: freeze the hydration effect's time dep once hydrated (was re-running every tick). - useGestureRecording: dev-gated console.warn when the live-preview runtime throws (was silent). - playerStore: gate window.__playerStore behind dev (guarded import.meta.env.DEV). - useGestureCommit: partition recorded keyframes by property group → one add-with-keyframes per group, so a mixed gesture no longer yields an untagged legacy tween. |
||
|
|
5ab1ce7acc | feat(studio): keyframes flag, gesture recording + timeline/selection refinements | ||
|
|
0392a98ee5 | fix(studio): address #1610 review — scope dblclick to pan-surface, kind-aware geometry guard, gate createMode, screen-space drag threshold | ||
|
|
6953ebb95c | feat(studio): on-canvas motion-path overlay | ||
|
|
395c6f7295 | docs(studio): address #1609 review — document occlusion fade-in invariant, donut limit, nearestPointOnPath t-semantics | ||
|
|
a72c70ea11 | feat(studio): motion-path geometry + commit helpers | ||
|
|
c66e82a63b |
fix(studio): address #1608 review — facade awaits commit, strict stale-parse guard, clearProps restore
BLOCKER: useSafeGsapCommitMutation now RETURNS the (.catch-chained) commit promise and the commitMutation facade awaits it — so await session.commitMutation(...) resolves AFTER the server save, fixing both consumers (useEnableKeyframes + useGestureCommit's showToast/requestSeek/idle, which were firing before the save landed). SafeGsapCommitMutation return type widened void→Promise<void> (fire-and-forget consumers ignore it). - stale-parse guard uses hasNonHoldTweenForElement (a leftover hold set no longer counts as live). - commitFlatViaKeyframes snapshots dragged gsap values before clearProps + restores after seek, so a failed commit leaves the dropped pose, not a cleared element. |
||
|
|
0631e78371 | feat(studio): GSAP drag/commit/bridge editing infra | ||
|
|
e28b30549c |
fix(studio): address #1607 review — cold-parse vs fetch-error budgets, isZeroDurationSet, array-ease tests
- useGsapAnimationFetchFallback: discriminate resolved/fetch-error/cold; only the cold (warm-but-zero) race gets the full ~600ms retry budget — a hard fetch error retries once. - Extract isZeroDurationSet (was !(duration>0) duplicated); rejects NaN, documents intent. - parsePercentageKeyframes: cite GSAP even-index spread; tests that a per-entry/interior ease is stripped without shifting the other keyframes' percentages. |
||
|
|
105c86520d | feat(studio): GSAP runtime read layer + shared helpers | ||
|
|
1612463b5b |
fix(core): address #1555 review — complete hold-sync, invalidate clip cache, strip rotation channel
- HOLD_SYNC_MUTATION_TYPES: add add-motion-path (load-bearing — addMotionPathToScript authors past t=0 → first-frame snap-to-(0,0) without the hold), update-meta, shift-positions, scale-positions, split-animations. (add stays out: flat tweens only, syncPositionHoldsBeforeKeyframes is a no-op for non-keyframed tweens.) - init.ts: timedClip in-flow/leaf WeakMaps now invalidate on clipTreeSignature change; visible/hidden branches both go through isTimedClipInFlow (was .get() by accident). - keyframesWriteRotation mirrors keyframesWritePosition so a rotation-only keyframe set strips the stale --hf-studio-rotation channel. |
||
|
|
343dbcfc0a |
feat(core): strip legacy path-offset/rotation + drop obsolete studio lint rule
A position or rotation add/set mutation makes the GSAP timeline the single source of truth for that channel, so any lingering --hf-studio-offset / --hf-studio-rotation CSS var must be cleared to avoid double-applying. stripStudioEditsFromTarget now clears both channels, and the add-strip fires for the position AND rotation property groups. Also removes the obsolete `gsap_studio_edit_blocked` lint rule: it warned that Studio cannot save drag/resize edits to elements in a registered timeline — the exact premise the single-source work inverts (the timeline is now the edit target). Removed the rule, its now-unused TIMELINE_REGISTRY_ASSIGN_PATTERN import, and its 5 tests. |
||
|
|
663955d05d |
feat(core): route motion-path mutations through studio-api + fix clip stamping
Wire the new mutations into the file save route. Only authored clips suppress descendant stamping, so auto-stamped animated scenes can inline-expand. Hide in-flow timed clips with `display:none` only when they are LEAF clips (no nested timed clips). `display:none` on a container removes its whole subtree, hiding descendants that are still inside their own visibility window — e.g. an in-flow composition root whose effective window clamps to the timeline end would black out a child video that should still show (the hdr-hlg regression). Containers keep `visibility:hidden`, which a visible descendant can override; only leaves leave the flow, which is all the split-overlap case needs. |
||
|
|
fd0a5054f4 |
fix(core): address #1554 review — data-exclusion test, split-fix doc, motion-path sentinel, parity blocks
- Regression test for the `data` GSAP-key exclusion (parallel to _auto). - splitAnimationsInScript: documented that .fromTo()/.to() correctly stay out of the from-branch (only .from() reverts) and the <= boundary; added mid-flight straddle tests. - addMotionPathToScript failure path returns id: null (was empty-string sentinel); caller updated. - Parity blocks for addKeyframeToScript array-form + updateKeyframeInScript (mirroring removeKeyframeFromScript). Surfaced a latent acorn array-form partial-props merge bug — documented as it.skip with a ready assertion (acorn cutover follow-up). |
||
|
|
7d97fa6ee7 |
feat(core): add GSAP keyframe + motion-path source mutations
Array-form keyframe removal in both the recast and acorn writers, plus update/add/remove-motion-path-point and add-motion-path. Exclude _auto and data from tween property-group classification. |
||
|
|
31fcadf602 | chore(producer): use a template literal for the CJS banner (review nit) | ||
|
|
cd4308b0f2 |
chore(producer): shim __filename/__dirname in the CJS banner
Bundled CJS deps like wawoff2 call __dirname; without the shim they throw "__dirname is not defined in ES module" at render time. Also ignore .zed/. |
||
|
|
82b6ccde79 |
fix(core): import cssSelector test from vitest, not bun:test (#1599)
core runs its suite via `vitest run` (include: src/**/*.test.ts), so the bun:test
import broke vitest collection ("Module bun:test has been externalized"), turning
core CI red on main and on every open PR (which build against the merge ref).
vitest's describe/it/expect are API-compatible — no test changes needed.
|
||
|
|
c0ffdc0fb0 |
fix(core,studio): escape user values in querySelector attribute selectors
Extract cssAttrSelector to packages/core/src/utils/cssSelector.ts and use it (or CSS.escape for browser-side code) at all 12 sites that previously interpolated raw user-authored values into querySelector attribute selectors. A " in a composition ID, script src, or data-start value would produce a malformed selector that throws. Node-side (core compiler/parser): uses the shared cssAttrSelector. Browser-side (runtime, studio): uses native CSS.escape(). Supersedes #1568 which fixed only the 3 bundler sites. |
||
|
|
758eda995c |
fix(sdk,studio): restore DOM edit cutover parity (#1565)
- Add splitStyleDeclarations with quote/paren-aware CSS parsing - Fix backslash escape handling inside quoted CSS string values - Close html-attribute safety gap in SDK cutover (event handlers, dangerous URIs) - Consolidate HTML attribute safety constants to core/utils/htmlAttrSafety.ts - Extract NON_HTML_CHILD_TAGS set for foreign-content decline gate - Add sdkCutoverParity test corpus (shorthand/longhand, mixed batches) |
||
|
|
4e32c5e0fe | chore: release v0.6.114 v0.6.114 | ||
|
|
f05b3f9c7c |
fix(slideshow): finish remaining split-PR review findings (#1594)
* fix(slideshow): address split-PR review findings on #1585 Genuinely-open findings from the #1580/#1590/#1591/#1592 reviews (the rest were already fixed on this branch: CSP handlers, manifest version, UUID ids, float keys, presenter 1s-timer): core (#1580): - isManifest rejects a non-object/array manifest (e.g. [42,null]) explicitly - resolveSlideshow flags duplicate slideSequence ids instead of silent overwrite player (#1590): - present() window.open uses noopener,noreferrer (audience syncs via channel) - BroadcastChannel name is per-deck (keyed on pathname) to avoid same-origin cross-talk between decks - add observedAttributes + attributeChangedCallback so runtime sound/mode toggles re-render studio (#1591/#1592): - persistSlideshowManifest no-op gate (skip write when HTML is unchanged) - surface persist failures (console.error) instead of silent .catch(()=>{}) - confirm before deleting a branch sequence (data-loss + dangling hotspots) + tests for the collision + non-object-manifest rejection. 20 core / 106 player / 53 studio pass; tsc/lint/fmt/fallow clean; deck still renders. * fix(slideshow): finish remaining split-PR review findings The larger items from the #1580/#1590/#1591/#1592 reviews (the rest landed in #1585): core (#1580): - dedup isSceneLikeCompositionId — shared slideshow/sceneId.ts, used by both the lint rule and the runtime scene-window computation (no more mirror-and-drift) player (#1590 / #1592): - onKey: when multiple decks share a page, drop the unfocused-convenience so a key drives only the focused deck - slow-iframe recovery: if the scene timeline posts after the wait times out (empty scenes), re-init once so sceneId slides resolve instead of being dropped studio (#1591): - persistSlideshowManifest validates the built island round-trips before writing - reorderBranchSlide helper + BranchTree up/down controls (parallel to main-line reorder), with a branch-position indicator + tests for reorderBranchSlide. core 228 / player 106 / studio (panel) 46 pass; tsc/lint/fmt/fallow clean. * fix(player,cli): use fileURLToPath for path resolution (Windows CI) new URL(...).pathname yields a leading-slash drive path ("/D:/...") on Windows, which broke: - packages/player/vitest.config.ts — the @hyperframes/core/slideshow alias resolved to a nonexistent path, failing the player slideshow tests on the Windows render-verification CI (passed on macOS/Linux where pathname is clean) - packages/cli/src/utils/compositionServer.ts helperDir — same bug in the play/present bundle-path resolution fileURLToPath converts file:// URLs to correct OS paths on all platforms. Player slideshow tests pass; present serves + resolves bundles. * fix(producer): fileURLToPath for the renders dir (Windows) DEFAULT_RENDERS_DIR used new URL(import.meta.url).pathname, which is "/D:/..." on Windows and resolves to a bogus path — affects the Windows render pipeline. Last of the .pathname -> fileURLToPath fixes (repo-wide src sweep now clean). |
||
|
|
cc2220e59e |
fix(slideshow): address code-review findings #1580-1584 (#1585)
* fix(slideshow): address code-review findings #1580-1584
- player: bundle @hyperframes/core into the IIFE/global build (noExternal)
- player: resolve audience mode from ?mode=audience URL query, not just attr
- player: event-driven waitForScenes + loud failure when no slides resolve
- player: scope window keydown so Space/Backspace don't hijack the host page
- player: audience mirrors full position (branch + fragment) via syncTo
- player: next() reveals remaining fragments even at slide end; enterBranch ignores empty sequences
- core: harden extractScenes against null/non-object scene entries
- core: strict manifest validation; error on inverted ranges & empty hotspot targets; dedup fragments
- core/lint: accept data-end/timeline-derived scene durations (match runtime)
- core+studio: share ISLAND_TYPE + island regex from @hyperframes/core/slideshow
- studio: SlideList reflects manifest slide order; branch-slide authoring (notes/fragments/hotspots)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* feat(player): slideshow fullscreen + presenter-view rework
- fullscreen toggle in the nav chrome (button + 'F' key); standard Fullscreen
API on the <hyperframes-slideshow> element, icon reflects state
- presenter console: live slide on top, speaker-notes panel below, with the nav
controls shown in-view; Present button hides once presenting (harness)
- audience (viewer) window: chrome reduced to a fullscreen-only control, no nav
- fix: audience / back() / backToMain() mirror stayed frozen on the first frame —
a bare paused seek does not repaint some compositions. resumeSlide now plays a
brief render-nudge (RENDER_NUDGE) past the target so the composition paints,
then onTime pauses at the hold
- refactor: extract reusable buildNavCluster() + wireChromeButtons(); rework
buildPresenterLayout into the bottom notes panel
- example: airbnb-deck presenter-test.html harness (Present button + 'F')
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(player): slideshow no auto-progress + presenter slide fits/pins
- navigation jumps to a static frame instead of auto-playing the timeline:
playTo() seeks to the hold (+ a brief RENDER_NUDGE to repaint) rather than
sustaining playback, so slides hold until the user advances
- presenter view: pin the live slide to the top and confine the player to the
region above the notes panel, so the player CONTAINS the composition — the
full slide stays visible (letterboxed) at any width and re-fits on resize;
its bottom is no longer cut off by the notes panel
- tests: seek targets updated for the render-nudge offset
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(slideshow): presenter nav flash, slide-1 boundary, branch buttons
Three presenter-mode fixes from testing the airbnb deck: (1) navigation flash — seek to the exact target then play forward to repaint, instead of seeking backward (t-0.2) which painted the previous scene at boundaries; split hold into holdTarget (logical) and holdAt (target+nudge, clamped to slide.end). (2) slide-1 boundary — no-fragment slides rest at the slide midpoint, not slide.end. (3) presenter branch buttons — surface hotspots as buttons in the presenter console (the on-slide pill is lost in the letterboxed view). Also extract paintChrome() to dedupe the three chrome-render sites.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(slideshow): stop presenter nav buttons flickering / dropping clicks
The presenter elapsed clock called render() every second, which rebuilt the
entire chrome (innerHTML) including the nav buttons — they flickered and any
click landing mid-rebuild was lost. The 1s tick now updates only the elapsed
text node; the nav buttons are rebuilt only on actual navigation.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(slideshow): CSP-safe nav hover, UUID editor ids, manifest version
Addresses review feedback on the split stack:
- CSP: replace the 8 inline onmouseover/onmouseout handlers on the nav
buttons with a [data-hf-nav-cluster] button:hover CSS rule (injected once
per document). No inline event handlers → works under strict CSP.
- IDs: studio sequence/hotspot id generation used Date.now() (sub-ms
collision on rapid clicks) — now crypto.randomUUID().
- Versioning: stamp version on the persisted manifest island (preserving an
existing one); add the optional version field + SLIDESHOW_MANIFEST_VERSION
to the core schema so future schema changes can migrate older islands.
These live on the review-fixes tip (consistent with the stack's fixup-on-tip
model); the touched code belongs to ss-player-b (#1590), ss-studio-a/b
(#1591/#1592), and ss-core (#1580).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* chore(ci): fix format + fallow gates for slideshow stack
- .prettierignore: exclude generated demo compositions (registry/examples/**/*.html)
from oxfmt — large video-pipeline output (GSAP/Three/WebGL), not hand-authored
source. Was failing 'Format' repo-wide (pre-existing on main via #1584).
- .fallowrc: exempt SlideshowPanel.tsx (health/complexity — section fan-out) and
the slideshowPanelHelpers.ts / SlideshowPanel.test.ts parallel-structure clones
(duplicates.ignore). File-level config, not inline comments — inline shifts line
numbers and breaks fallow's inherited-finding fingerprint (per existing rc note).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(slideshow): address PR review + CodeQL findings
- CodeQL #638 (parseSlideshow): complete the regex metachar escape in
slideshowIslandRegex (was missing backslash); add JSDoc on the factory +
lastIndex caveat (reviewer 5a/16).
- CodeQL #639/#640 + review items 13/17: remove registry/examples/airbnb-deck/
presenter-test.html — a generated test harness (postMessage w/o origin check,
proto-pollution) that was scope-creep into a fix PR and a 3rd duplicate island.
Regenerate locally via the scratchpad script when testing.
- Review item 15 (docs drift in skills/slideshow/SKILL.md): lint resolves scenes
by data-composition-id only (not .clip[id]); fragments are valid INCLUSIVE of
[start,end], not 'strictly inside'.
IIFE bundles core confirmed (0 external @hyperframes/core refs in the slideshow
global build). format/lint/fallow green.
* feat(cli): add 'present' command — serve a deck in presenter mode
hyperframes present [dir] starts a lightweight HTTP server, wraps the
composition in <hyperframes-slideshow> with its island inlined, and opens
the browser. A real HTTP origin is required for presenter mode: present()
opens the audience window via window.open(?mode=audience) and the two sync
over BroadcastChannel — neither works from file://.
- New utils/compositionServer.ts factors the server scaffolding shared with
'play' (resolve runtime/player/slideshow bundles, inject runtime, asset
content-types, bind to a free port); play.ts now uses it too.
- Errors clearly if the deck has no slideshow island.
- .fallowrc: exempt the play/present command entrypoints (validation + server
wiring) and the per-command startup/logging block from the complexity /
duplication gates.
Verified end-to-end against registry/examples/airbnb-deck: server serves the
wrapper + assets, the component binds and renders (counter 1 / 11).
* fix(cli): present renders the deck (player sizing + self-driving serve)
Two bugs caused a black slide area:
- The <hyperframes-player> had no positioning, so its iframe collapsed to
zero size — the (absolutely-positioned) chrome showed but the composition
didn't. Add position:absolute; inset:0 (matches demo.html).
- The composition was served with the engine runtime injected, which leaves
its timelines engine-paused (blank). Slideshow decks self-drive their own
timelines (like demo.html / the standalone harness), so serve them raw.
Verified end-to-end on registry/examples/airbnb-deck: cover renders, Next
advances 1/11 -> 2/11 and slide 2 paints.
* fix(cli): present plays slideshow sound effects
The composition (in the player's sandboxed iframe) posts
{ type: 'hf-sfx', name } to the parent on nav, but the iframe is
autoplay-blocked — audio must play in the parent that owns the user gesture.
Add the parent-side hf-sfx handler (the 4 standard clips advance/fragment/
branch-enter/back, served from the deck's sfx/ under /composition/sfx/),
gesture-unlocked and mute-aware, in both presenter and audience windows.
Verified: sfx serve 200 (audio/mpeg) and Next delivers [advance, fragment]
to the parent handler.
* feat(examples): softer mellow slideshow sfx for airbnb-deck
Replace the aggressive percussive pops with gentle sine-tone cues (warm
pitches C5/G4/E5/F4, 12ms attack + exponential decay, lowpassed) — advance/
fragment/branch-enter/back. Much lighter; fragment is the most subtle.
* feat(examples): whoosh + sparkle slideshow sfx for airbnb-deck
Replace the sine-tone cues with airy, designed sounds:
- advance: a soft whoosh (band-limited pink noise, bell-shaped swell)
- back: that whoosh reversed and darkened
- fragment: a light sparkle (staggered high chime blips)
- branch-enter: whoosh + a trailing sparkle (magical entry)
* feat(examples): directional whoosh + richer branch-enter cue (airbnb-deck)
- Going backward a slide now plays the reverse whoosh (back), not advance —
the sfx logic detects nav direction by scene order instead of firing advance
for every scene change.
- branch-enter is now a more interesting magical cue: a faint whoosh + an
ascending C5-E5-G5-C6 chime arpeggio + a trailing sparkle.
Verified: next then prev fires [advance, fragment, back]; no page errors.
* fix(cli): harden present sfx handler + mute-hover affordance (R2 review)
Addresses Rames R2 items 19-21:
- 20: the present audio handler reintroduced the CodeQL classes removed with
presenter-test.html — add an origin check (same-origin composition iframe)
and an own-property guard so a 'name' like __proto__ can't resolve to and
mutate Object.prototype.
- 21: assetContentType used a bare index lookup (ext='__proto__' -> prototype);
guard with Object.hasOwn.
- 19: the CSP hover rule erased the speaker button's muted color; add a
higher-specificity [data-hf-muted] [data-hf-mute]:hover override.
Verified: hf-sfx origin matches location.origin (guard passes), advance/fragment
still fire, deck renders + advances. Items 14/18/22 deferred (minor, pre-existing).
* fix(slideshow): address remaining R2 items (14/18/22) + re-remove harness
- 14: resumeSlide now mirrors enterSlide — a no-fragment slide resumes at its
midpoint (visible-at-rest), not frame-0; fragmented slides still resume to the
saved fragment or slide.start. Added a dedicated test naming the heuristic.
- 18: fullscreenchange swaps only the fullscreen glyph + aria (hoisted SVGs to
module consts) instead of re-rendering the whole chrome.
- 22: .prettierignore lists the specific generated demo compositions instead of
blanket registry/examples/**/*.html, so hand-authored example HTML still formats.
- presenter-test.html: a stray
|
||
|
|
ae40498433 |
test(examples): slideshow demos — airbnb deck, startup pitch, fixture (#1584)
* feat(player): slideshow controller state machine Stack-split from the original ss-player PR (#1581): the SlideshowController state machine (stack-based slide/fragment/branch navigation) and its tests. The <hyperframes-slideshow> web component follows in the next PR. * feat(player): <hyperframes-slideshow> web component + presenter Stack-split from the original ss-player PR (#1581): the <hyperframes-slideshow> custom element (wraps <hyperframes-player>, drives the controller), presenter/audience BroadcastChannel sync, nav chrome, and the player scenes hook. * feat(studio): slideshow manifest persistence + panel helpers Stack-split from the original ss-studio PR (#1582): the data layer — setSlideshowManifest, the useSlideshowPersist hook, and panel helpers. The editor panel UI follows in the next PR. * feat(studio): slideshow branching editor panel UI Stack-split from the original ss-studio PR (#1582): the SlideshowPanel / SlideshowSubPanels editor UI, right-panel wiring, and app integration. * docs(skill): slideshow authoring guidance + standalone harness reference New /slideshow skill (island schema, slide rules, fragments, branching, validation) + a standalone-harness reference doc, and a router entry in the /hyperframes skill. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * test(examples): slideshow demos — airbnb deck, startup pitch, fixture Three runnable slideshow compositions: a current-Airbnb-branded remake of the 2009 seed deck (Three.js backgrounds, GSAP entrances, hotspot branch, HeyGen SFX), an animated startup pitch, and a minimal fixture. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
b7a1163753 |
docs(skill): slideshow authoring guidance + standalone harness reference (#1583)
* feat(player): slideshow controller state machine Stack-split from the original ss-player PR (#1581): the SlideshowController state machine (stack-based slide/fragment/branch navigation) and its tests. The <hyperframes-slideshow> web component follows in the next PR. * feat(player): <hyperframes-slideshow> web component + presenter Stack-split from the original ss-player PR (#1581): the <hyperframes-slideshow> custom element (wraps <hyperframes-player>, drives the controller), presenter/audience BroadcastChannel sync, nav chrome, and the player scenes hook. * feat(studio): slideshow manifest persistence + panel helpers Stack-split from the original ss-studio PR (#1582): the data layer — setSlideshowManifest, the useSlideshowPersist hook, and panel helpers. The editor panel UI follows in the next PR. * feat(studio): slideshow branching editor panel UI Stack-split from the original ss-studio PR (#1582): the SlideshowPanel / SlideshowSubPanels editor UI, right-panel wiring, and app integration. * docs(skill): slideshow authoring guidance + standalone harness reference New /slideshow skill (island schema, slide rules, fragments, branching, validation) + a standalone-harness reference doc, and a router entry in the /hyperframes skill. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
04a775b58a |
feat(studio): slideshow branching editor panel UI (#1592)
* feat(player): slideshow controller state machine Stack-split from the original ss-player PR (#1581): the SlideshowController state machine (stack-based slide/fragment/branch navigation) and its tests. The <hyperframes-slideshow> web component follows in the next PR. * feat(player): <hyperframes-slideshow> web component + presenter Stack-split from the original ss-player PR (#1581): the <hyperframes-slideshow> custom element (wraps <hyperframes-player>, drives the controller), presenter/audience BroadcastChannel sync, nav chrome, and the player scenes hook. * feat(studio): slideshow manifest persistence + panel helpers Stack-split from the original ss-studio PR (#1582): the data layer — setSlideshowManifest, the useSlideshowPersist hook, and panel helpers. The editor panel UI follows in the next PR. * feat(studio): slideshow branching editor panel UI Stack-split from the original ss-studio PR (#1582): the SlideshowPanel / SlideshowSubPanels editor UI, right-panel wiring, and app integration. |
||
|
|
b74020fa56 |
feat(studio): slideshow manifest persistence + panel helpers (#1591)
* feat(player): slideshow controller state machine Stack-split from the original ss-player PR (#1581): the SlideshowController state machine (stack-based slide/fragment/branch navigation) and its tests. The <hyperframes-slideshow> web component follows in the next PR. * feat(player): <hyperframes-slideshow> web component + presenter Stack-split from the original ss-player PR (#1581): the <hyperframes-slideshow> custom element (wraps <hyperframes-player>, drives the controller), presenter/audience BroadcastChannel sync, nav chrome, and the player scenes hook. * feat(studio): slideshow manifest persistence + panel helpers Stack-split from the original ss-studio PR (#1582): the data layer — setSlideshowManifest, the useSlideshowPersist hook, and panel helpers. The editor panel UI follows in the next PR. |
||
|
|
075302cd03 |
feat(player): <hyperframes-slideshow> web component + presenter (#1590)
* feat(player): slideshow controller state machine Stack-split from the original ss-player PR (#1581): the SlideshowController state machine (stack-based slide/fragment/branch navigation) and its tests. The <hyperframes-slideshow> web component follows in the next PR. * feat(player): <hyperframes-slideshow> web component + presenter Stack-split from the original ss-player PR (#1581): the <hyperframes-slideshow> custom element (wraps <hyperframes-player>, drives the controller), presenter/audience BroadcastChannel sync, nav chrome, and the player scenes hook. |
||
|
|
6938d6acf2 |
feat(player): slideshow controller state machine (#1589)
Stack-split from the original ss-player PR (#1581): the SlideshowController state machine (stack-based slide/fragment/branch navigation) and its tests. The <hyperframes-slideshow> web component follows in the next PR. |
||
|
|
3861e8e9fc |
feat(studio): slideshow branching editor panel (#1582)
New Slideshow right-panel tab — slide list, inspector (notes + fragment hold-points), branch tree, hotspot tool — backed by pure manifest-transform helpers and a debounced SDK persist that writes the JSON island. Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
7af3eb8f80 |
feat(player): slideshow controller + <hyperframes-slideshow> component (#1581)
DOM-free SlideshowController (discrete nav, fragment holds, branch stack) driving the existing player; <hyperframes-slideshow> web component with a unified mute+nav capsule (conditional prev/next), floating hotspot overlays, presenter mode (BroadcastChannel), keyboard/touch, and a scenes getter fed via the runtime message handler. Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
a38fc4e778 | chore: release v0.6.113 v0.6.113 | ||
|
|
3a28d3f6b8 |
fix(release): scope tag-monotonicity guard to tags reachable from HEAD
The guard blocked on any semver-higher v* tag, including orphan tags on dead branches (e.g. a stray `chore: release v1.0.3` never merged or published). Such tags can't appear in the release history and shouldn't block a legitimate release. Now only tags that are BOTH higher AND reachable from HEAD block; extracted `findBlockingTags` with unit coverage for orphan/reachable/lower cases. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
8376457989 |
feat(core): slideshow schema, parser, and lint rule (#1580)
## Slideshow mode — 1/5: core schema, parser & lint
Foundation for slideshow mode: a composition can declare an embedded **slideshow manifest** that turns its continuous timeline into a discrete, navigable deck. This PR adds the data model, parser, and validation — no runtime/UI yet.
### What & why
A slide is just an existing scene (`data-composition-id` + `data-start`/`data-duration`) plus metadata declared in one embedded `<script type="application/hyperframes-slideshow+json">` island. Keeping the manifest *in the composition* means no new file format and no build step — slides are additive metadata over a normal composition.
### Key changes
- `slideshow/slideshow.types.ts` — `SlideshowManifest`, `SlideRef`, `SlideHotspot`, `SlideSequence` and their resolved counterparts. TTS fields (`ttsScript`/`ttsAudioUrl`/`ttsDurationMs`) are present but **reserved** (playback not built).
- `slideshow/parseSlideshow.ts` — `parseSlideshowManifest(html)` extracts the island; `resolveSlideshow(manifest, scenes)` resolves each `sceneId` to a `{start,end}` range (honouring optional `startTime`/`endTime` overrides) and returns validation errors for: unresolved sceneId, fragment outside a slide's range, hotspot targeting an unknown sequence, and overlapping main-line slides.
- `lint/rules/slideshow.ts` — surfaces those resolve errors under `hyperframes lint`; derives scenes from `data-composition-id` (matching the runtime's scene source).
- `lint/rules/core.ts` — exempts the slideshow island MIME type from the inline-script-syntax check.
- `./slideshow` subpath export (dev + publishConfig) so downstream packages import only the lightweight parser, keeping core's Node-only barrel out of their typecheck graph.
### Testing
`parseSlideshow.test.ts` + `slideshow.test.ts` (vitest) cover parse, resolution, every error path, and the lint rule.
### Stack
Bottom of a 5-PR stack: **core** → player (#1581) → studio (#1582) → skill (#1583) → examples (#1584).
🤖 Generated with [Claude Code](https://claude.com/claude-code)
|
||
|
|
967bf9f9ed |
refactor(core): gate acorn GSAP writer behind cutover flag; keep recast default (WS-3F) (#1573)
* refactor(core): retire recast/babel, route all GSAP mutations to acorn (WS-E/3.F) - Delete gsapParser.ts (2595-line recast-based parser/writer) - Delete gsapParser.test.ts, gsapParser.stress.test.ts, gsapParser.test-helpers.ts - Add gsapParserExports.ts: re-export umbrella for gsap-parser subpath - Move SplitAnimationsOptions/SplitAnimationsResult to gsapSerialize.ts - executeGsapMutation: async->sync, static acorn imports replace loadGsapParser() - Fix 3 function name mismatches in files.ts switch cases - generators/hyperframes.ts: imports from gsapSerialize (blocker resolved) - gsapWriterAcorn.ts: SplitAnimationsOptions from gsapSerialize - Parity tests: recast oracle removed; acorn-only regression (14 pass) - Remove recast and @babel/parser from core/package.json Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * fix(sdk): harden mutation handlers + widen variable API (code-review) Self-contained review fixes for the SDK-hotspot stack (#1569–#1573). The dispatch path (_dispatch → applyOp) never runs validateOp, so the new WS-D/WS-3.C guards were advisory-only; re-enforce them in the handlers. - addElement: null-guard the resolved parent (no more `as Element` masking a null → crash on unknown parent id); reject <script> and multi-root fragments via parseInsertableFragment instead of inserting raw markup / silently dropping extra roots. - addWithKeyframes / replaceWithKeyframes: bail on empty keyframes (no degenerate `keyframes: {}` tween) and when the animationId resolves to nothing (no silent degrade-to-add leaving a duplicate tween). - isObjectVariableValue: exclude arrays so an array override value can't be misclassified as a font/image object and written into the variable model. - Composition.setVariableValue: widen the public interface signature to `… | FontValue | ImageValue` to match the impl + EditOp (B2 object-valued variables were unreachable via the typed API). - mutate.gsap.test.ts: import addKeyframeToScript from gsap-writer-acorn — the gsap-parser subpath no longer re-exports write fns after recast retire, so the test threw at runtime (red suite). - Dedup: export EXCLUDED_TAGS from hfIds.ts and drop the verbatim HF_EXCLUDED_TAGS copy in mutate.ts. Adds guard regression tests. SDK 340/340, core hfIds 13/13, build green, fallow --gate new-only clean. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(sdk): variable-model dedup + undo/scoped-parent correctness; test honesty (code-review) Second batch of review fixes for the SDK-hotspot stack. - Variable model (#7, #13): extract readVariableDefault/writeVariableDefault into a shared engine/variableModel.ts used by both mutate.ts (forward) and apply-patches.ts (replay), so the model shape can't diverge. Add clearVariableDefault and make a `variable` remove patch DELETE the decl's `default` key — the exact inverse of a first-set on a default-less variable. Previously undo of such a set no-op'd and stranded the value. - addElement scoped parent (#8): record the caller's id verbatim (scoped "hf-host/hf-leaf" path or composition id) as the patch parentId instead of the bare data-hf-id, so redo/replay re-resolves the SAME parent via resolveScoped rather than the canonical top-level dup (or document.body). - resolveTimings honesty (#5): correct the header + test that claimed a live "preview == render" parity — neither path consumes the resolver yet (anchor inputs are Pacific/backend-deferred). It's a pure-function property, not a current guarantee. - GSAP writer parity (#12): the recast oracle was deleted in WS-3.F, leaving the WS-3.C keyframe ops comparing acorn output to itself. Pin them as golden inline snapshots and drop the now-dead recast scaffolding (replaceWithKfRecast, removeAnimRecast alias). Remaining pre-WS-3.C parity blocks noted as follow-up. Adds regression tests (undo of default-less variable; scoped-parent redo). SDK 342/342, core timingResolver+parity green, build + fallow --gate new-only clean. Not changed (need design / out of scope): #9 pre-#1569 persisted-override CSS replay (moot for unreleased data; proper fix is render-time CSS derivation), #11 replaceWithKeyframes stale positional id (mitigated by the missing-id no-op guard + type doc; full fix needs non-positional ids). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(sdk): replay CSS-prop derivation for legacy var overrides; stale-id selector guard (code-review) Final review-fix batch — the two items deferred from the prior pass. - #9 legacy variable-override CSS: applyOverrideSet now derives the `--{id}` CSS custom prop from any scalar `var.{id}` override on replay (and removes it for a null override). Sets written before the model/CSS split carried only `var.{id}`; without this, replaying them updated the JSON model but left `var(--{id})` bindings rendering the schema default. Replay-path only — the undo path (applyOne) is untouched, so #1569's separate-patch undo correctness is preserved. Object (font/image) values are never CSS, so they are skipped. - #11 stale positional id: replaceWithKeyframes now requires the located animation to still target the caller's `targetSelector`. Position-derived ids re-point after structural edits; a stale id resolving to a DIFFERENT element's tween previously got silently replaced. It now bails (no-op) unless the id still points at the expected selector. Adds regression tests (legacy var.{id}-only override restores CSS; object override writes no CSS; stale-id-wrong-selector replace is a no-op). SDK 345/345, build + fallow --gate new-only clean. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * refactor(core): gate acorn GSAP writer behind cutover flag; keep recast default (WS-3F) Product decision pivot: acorn no longer replaces recast as the GSAP writer. Recast remains the default server writer; acorn runs only when STUDIO_SDK_CUTOVER_ENABLED=true (or =1) is set server-side — the same env flag name as the client Vite var, so a single switch flips both sides. Changes: - Restore gsapParser.ts (recast writer) + test/stress/helper files deleted by 3F - Restore @babel/parser + recast deps in packages/core/package.json - Add isAcornGsapWriterEnabled() + loadGsapParser() to files.ts (lines 59-82) - Split executeGsapMutation into async dispatcher + executeGsapMutationRecast (recast, async via loadGsapParser) + executeGsapMutationAcorn (acorn, sync) - Dispatcher defaults to recast; acorn branch taken only when flag is on - Restore gsapWriter.parity.test.ts, gsapWriterParity.acorn.test.ts, and gsapWriterParity.corpus.test.ts to true recast-vs-acorn differential suites (not acorn-vs-itself) - Exempt gsapParser.ts in .fallowrc.jsonc health.ignore + ignoreExports (pre-existing complexity + barrel re-exports consumed outside diff scope) - Add fallow-ignore-file code-duplication to files.ts (intentional parallel switch bodies for two writers) Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com> |
||
|
|
37efbcb955 |
feat(sdk): image-alpha hit-test phase 1 (WS-G) (#1574)
* feat(sdk): image-alpha hit-test phase 1 (WS-G) Extends the WS-A1 iframe adapter with image-alpha hit-testing: - Replace `elementFromPoint` with `elementsFromPoint` (z-stack) so a transparent-image hit falls through to the layer behind. - For `<img>` hits: map client point → natural-pixel coords via a pure `mapPointToImagePixel` fn (object-fit cover/contain/fill aware); draw to an offscreen canvas once (cached by `currentSrc`); sample alpha via pure `alphaIsOpaque`. Transparent pixel → miss, continue the stack. - Cross-origin images that taint the canvas → SecurityError fallback → treat pixel as opaque (never drop an unverifiable hit). - Phase 2 (per-pixel alpha via `drawElement`) NOT built; gated on a perf spike per plan. Tests: alphaIsOpaque thresholds, mapPointToImagePixel (fill/cover/contain + out-of-box→null), z-stack fallthrough, taint→opaque fallback, non-image WS-A1 regression. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(sdk): correct image-alpha hit-test edge cases (WS-G review) - map within the content box, not the border box (object-fit positions the image inside border+padding; getBoundingClientRect was off for a bordered/padded <img>) - normalize vertical-first object-position keyword pairs ("bottom left") - guard natural.width/height===0 in cover/contain (Infinity/NaN scale) - fall back to elementFromPoint when elementsFromPoint is unavailable - guard `instanceof win.HTMLImageElement` when the constructor is absent - bound _imgCanvasCache with a FIFO cap so it can't leak one canvas per src - drop the redundant taint-probe getImageData; the real pixel read already surfaces lazy taint - one opacity walk per candidate: the hf node is on the already-checked ancestor chain, so the resolver no longer re-walks for visibility - remove dead `fit==="fill"||` clause and unused resolveToken param - tests: none-fit, object-position keyword/px/reversed-pair, zero-natural, no-throw-without-HTMLImageElement + elementsFromPoint fallback Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(sdk): image-alpha review fixes (WS-G) — transform/taint/cache-key/memory Addresses the phase-1 gaps flagged in review (was documentation-only): - CSS rotation/skew on the image or an ancestor now fails safe to opaque instead of sampling the wrong pixel (getBoundingClientRect is axis-aligned). Full transform-inverse mapping stays phase 2. No-op where DOMMatrix is unavailable. - Cross-origin canvas taint now warns once per src (was silent) so the fall-back-to-opaque path is visible, not "hit-test feels wrong". - Canvas cache keyed on src + natural dimensions (was src only) so a srcset/responsive re-render of the same URL doesn't reuse a stale canvas. - Pathological-size guard: images above a pixel budget skip alpha-testing (opaque) to bound OffscreenCanvas memory. - Docs: border/padding clicks fall through (intentional) noted on imageAlphaOpaqueAt. Tests: removed the duplicate a=0 threshold case; added transparent-over- transparent-over-div fallthrough. iframe.test.ts 59/59. tsc/oxlint/oxfmt green. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
dd6fad6bb2 |
fix(sdk): code-review follow-ups (WS-B/C/3.C, #1569/#1570/#1572) (#1588)
Addresses the still-outstanding review concerns from merged PRs #1569 / #1570 / #1572 not already hoisted into #1573. WS-B (#1569): - validateVariables requires discriminant fields for object-valued font/image ({name,source} / {url}); a {name:42} font or {foo:42} image previously passed runtime validation and surfaced as a bogus font-family / missing image. - Dropped ImageValue's [key:string]:unknown index signature (let any {url}-shaped object through, swallowed typos); explicit alt?/fit? instead. - Documented the OverrideSet widening for SDK consumers. WS-C (#1570): - getElementTimings caches parsed GSAP labels by exact script text (avoids a full acorn re-parse per read; content-key invalidates on edit). - Documented end-inclusive label window + best-effort extractGsapLabels catch. WS-3.C (#1572): - Added typed Composition.addWithKeyframes / replaceWithKeyframes (was asymmetric with addGsapTween; Studio had to use raw dispatch). - Extracted shared KeyframeSpec type; documented position as seconds/number-only. Gates: build + core 18/18 + sdk 19/19 + oxlint + oxfmt + fallow + typecheck all green. Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
418f198b33 |
feat(sdk): ws-3c — addWithKeyframes + replaceWithKeyframes SDK ops (acorn writer) (#1572)
* feat(sdk): ws-b variables/brand — object-valued font/image + B1 JSON model
B1 — setVariableValue now drives the runtime JSON model
(data-composition-variables) so preview == render. CSS custom prop is
kept as a secondary compat write for compositions that CSS-bind directly
to --{id}.
B2 — object-valued font ({name, source}) and image ({url}) variable
types added core-to-SDK. Object values write to the JSON model only;
scalars write both model + explicit CSS style patches. Explicit style-path
patches in forward/inverse ensure apply-patches.ts handles each path type
purely (model vs CSS), so inverse patches restore exact pre-call state
without ambiguity.
Changed files:
packages/core/src/core.types.ts — font/image to CompositionVariableType + interfaces
packages/core/src/lint/rules/composition.ts — accept font/image in lint message
packages/core/src/parsers/htmlParser.ts — validate font/image variable declarations
packages/core/src/parsers/htmlParser.test.ts — tests for new variable types
packages/core/src/runtime/validateVariables.ts — checkType for font/image
packages/sdk/src/types.ts — FontValue/ImageValue; widen OverrideSet + EditOp
packages/sdk/src/index.ts — re-export FontValue/ImageValue
packages/sdk/src/session.ts — widen setVariableValue signature
packages/sdk/src/engine/patches.ts — valueChange helper for object-valued patches
packages/sdk/src/engine/mutate.ts — handleSetVariableValue: B1+B2 with explicit CSS patches
packages/sdk/src/engine/apply-patches.ts — variable case: model-only (CSS via explicit patch)
packages/sdk/src/engine/mutate.test.ts — B1+B2 round-trip + inverse tests
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* feat(sdk): ws-c elastic timing + word-alignment resolver (WS-C)
C1: getElementTimings/setElementTiming typed session methods + setHold typed
wrapper. getElementTimings reads data-duration (preferred) or data-end−data-start
(fallback) — same attr-preference as handleSetTiming. setElementTiming dispatches
a sparse map as one batch → one patch event → one undo step. setHold mirrors
setVariableValue pattern.
Also fixes a pre-existing apply-patches.ts gap: the timing/duration patch case was
absent, causing undo of duration changes to silently no-op. Added the duration
branch so inverse patches restore data-duration correctly.
C2: packages/core/src/compiler/timingResolver.ts — shared pure resolveTimings()
consumed by BOTH preview (sdk session) and render (timingCompiler) paths. Word-
anchored elements get enterAt = wordTimings[k].start + offset; elastic hold =
max(0, slotEnd − (enterAt + enterDuration + exitDuration)), clamped ≥ 0; never
timescales animated content. Un-anchored elements keep authored timing (align-on-
adjust). Deterministic + pure: no Date.now, no Math.random, no DOM.
extractGsapLabels() added to gsapParserAcorn.ts to parse tl.addLabel() calls for
the getElementTimings labels field.
Tests: timingResolver.test.ts (10 pure-function tests including preview==render
parity golden test); session.timings.test.ts (15 session-layer tests covering
duration-authored, end-authored, label extraction, batching, undo, and setHold
regression).
Gates: build ✓ · bun test (sdk+core/compiler) 434/434 ✓ · oxlint 0 warnings ✓ ·
oxfmt --check ✓ · fallow --gate new-only ✓ (complexity suppressed on 2 new
inline functions, duplication warn-only pre-existing)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* feat(sdk): addElement forward op — mint hf-id, inverse = removeElement (WS-D)
Implements WS-D: the addElement EditOp and session.addElement() typed method.
- types.ts: addElement op (parent/index/html) added to EditOp union;
addElement(parent, index, html): HfId added to Composition interface
- mutate.ts: handleAddElement inserts a single-root HTML fragment at
parent+index, minting ids against the LIVE document's existing id set
(not a fresh fragment set) via collectDocumentHfIds + mintFragmentIds;
forward = patchAdd, inverse = patchRemove; MutationResult.meta.newId
carries the minted root id
- mutate.ts: validateOp case rejects missing parent, negative index,
empty html, zero-element html, and <script> in html
- session.ts: typed addElement(parent, index, html) returns minted id
via result.meta.newId
- mutate.test.ts: 16 tests covering insert position, append semantics,
id uniqueness, content-collision rehash, nested fragments, forward/
inverse symmetry, undo, add/undo/redo stability, parent:null body
insertion, serialize round-trip, and all five validateOp rejection codes
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* feat(sdk): ws-3c — addWithKeyframes + replaceWithKeyframes SDK ops (acorn writer)
Port add-with-keyframes / replace-with-keyframes from the server recast path
to the acorn/magic-string writer and expose them as typed SDK EditOps.
- gsapWriterAcorn.ts: extend buildKeyframeObjectCode and
addAnimationWithKeyframesToScript to accept `auto?: boolean` on keyframes
(emits `_auto: 1` matching the recast writer)
- types.ts: add `addWithKeyframes` and `replaceWithKeyframes` to EditOp union
- mutate.ts: add handleAddWithKeyframes, handleReplaceWithKeyframes, and
applyGsapWithKeyframesOp dispatch sub-function; validateOp cases for both
- sdkCutover.ts: add sdkAddWithKeyframesPersist + sdkReplaceWithKeyframesPersist
(shared via dispatchWithKeyframes to eliminate clone)
- useGsapAnimationOps.ts: wire addWithKeyframes + replaceWithKeyframes
callbacks with SDK-first / server fallback pattern
- gsapWriter.parity.test.ts: add parity tests for _auto endpoint round-trip and
replaceWithKeyframes (remove + addWithKeyframes) differential golden harness;
import removeAnimationFromScript from both writers
Landmine note: tween IDs are position-derived — replaceWithKeyframes removes the
old tween (renumbering survivors) then inserts the replacement at the end; the
MutationResult patch pair restores the whole GSAP script on undo, not a per-ID
inverse, so ID-held references in callers must re-parse after structural edits.
Gate: WS-3.F (retire recast / executeGsapMutation) — NOT started here.
Remaining Studio callers (gsapDragCommit, gsapRuntimeBridge, useGestureCommit,
useEnableKeyframes) remain on the server commitMutation path until WS-3.F.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
||
|
|
0ca01c88cf |
feat(sdk): addElement forward op — mint hf-id, inverse = removeElement (WS-D) (#1571)
* feat(sdk): ws-c elastic timing + word-alignment resolver (WS-C) C1: getElementTimings/setElementTiming typed session methods + setHold typed wrapper. getElementTimings reads data-duration (preferred) or data-end−data-start (fallback) — same attr-preference as handleSetTiming. setElementTiming dispatches a sparse map as one batch → one patch event → one undo step. setHold mirrors setVariableValue pattern. Also fixes a pre-existing apply-patches.ts gap: the timing/duration patch case was absent, causing undo of duration changes to silently no-op. Added the duration branch so inverse patches restore data-duration correctly. C2: packages/core/src/compiler/timingResolver.ts — shared pure resolveTimings() consumed by BOTH preview (sdk session) and render (timingCompiler) paths. Word- anchored elements get enterAt = wordTimings[k].start + offset; elastic hold = max(0, slotEnd − (enterAt + enterDuration + exitDuration)), clamped ≥ 0; never timescales animated content. Un-anchored elements keep authored timing (align-on- adjust). Deterministic + pure: no Date.now, no Math.random, no DOM. extractGsapLabels() added to gsapParserAcorn.ts to parse tl.addLabel() calls for the getElementTimings labels field. Tests: timingResolver.test.ts (10 pure-function tests including preview==render parity golden test); session.timings.test.ts (15 session-layer tests covering duration-authored, end-authored, label extraction, batching, undo, and setHold regression). Gates: build ✓ · bun test (sdk+core/compiler) 434/434 ✓ · oxlint 0 warnings ✓ · oxfmt --check ✓ · fallow --gate new-only ✓ (complexity suppressed on 2 new inline functions, duplication warn-only pre-existing) Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(sdk): addElement forward op — mint hf-id, inverse = removeElement (WS-D) Implements WS-D: the addElement EditOp and session.addElement() typed method. - types.ts: addElement op (parent/index/html) added to EditOp union; addElement(parent, index, html): HfId added to Composition interface - mutate.ts: handleAddElement inserts a single-root HTML fragment at parent+index, minting ids against the LIVE document's existing id set (not a fresh fragment set) via collectDocumentHfIds + mintFragmentIds; forward = patchAdd, inverse = patchRemove; MutationResult.meta.newId carries the minted root id - mutate.ts: validateOp case rejects missing parent, negative index, empty html, zero-element html, and <script> in html - session.ts: typed addElement(parent, index, html) returns minted id via result.meta.newId - mutate.test.ts: 16 tests covering insert position, append semantics, id uniqueness, content-collision rehash, nested fragments, forward/ inverse symmetry, undo, add/undo/redo stability, parent:null body insertion, serialize round-trip, and all five validateOp rejection codes Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
f65e229663 |
feat(sdk): ws-c elastic timing + word-alignment resolver (WS-C) (#1570)
C1: getElementTimings/setElementTiming typed session methods + setHold typed wrapper. getElementTimings reads data-duration (preferred) or data-end−data-start (fallback) — same attr-preference as handleSetTiming. setElementTiming dispatches a sparse map as one batch → one patch event → one undo step. setHold mirrors setVariableValue pattern. Also fixes a pre-existing apply-patches.ts gap: the timing/duration patch case was absent, causing undo of duration changes to silently no-op. Added the duration branch so inverse patches restore data-duration correctly. C2: packages/core/src/compiler/timingResolver.ts — shared pure resolveTimings() consumed by BOTH preview (sdk session) and render (timingCompiler) paths. Word- anchored elements get enterAt = wordTimings[k].start + offset; elastic hold = max(0, slotEnd − (enterAt + enterDuration + exitDuration)), clamped ≥ 0; never timescales animated content. Un-anchored elements keep authored timing (align-on- adjust). Deterministic + pure: no Date.now, no Math.random, no DOM. extractGsapLabels() added to gsapParserAcorn.ts to parse tl.addLabel() calls for the getElementTimings labels field. Tests: timingResolver.test.ts (10 pure-function tests including preview==render parity golden test); session.timings.test.ts (15 session-layer tests covering duration-authored, end-authored, label extraction, batching, undo, and setHold regression). Gates: build ✓ · bun test (sdk+core/compiler) 434/434 ✓ · oxlint 0 warnings ✓ · oxfmt --check ✓ · fallow --gate new-only ✓ (complexity suppressed on 2 new inline functions, duplication warn-only pre-existing) Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
d0e520dbd9 |
feat(sdk): ws-b variables/brand — object-valued font/image + B1 JSON model (#1569)
## WS-B — variables / brand, object-valued (end-to-end)
Part of the AI Studio (Pacific) SDK integration. **Base of the SDK-hotspot stack** (`main → ws-b → ws-c → ws-d → ws-3c → ws-3f`).
### Problem
The variable system was split-brained: SDK `setVariableValue` wrote a `--{id}` CSS custom prop, while the runtime `getVariables()` read a separate JSON model (`data-composition-variables` / `__hfVariables`). The two never connected, and there was no `--brand-*` convention. Variables were scalar-only.
### What this does
- **B1 — one source of truth.** `setVariableValue` now drives the runtime variable model (`data-composition-variables` / `__hfVariables`), with CSS compatibility emitted as explicit `stylePath`-based patches alongside the model patch. A brand kit is a variables JSON; a batch of `setVariableValue` re-skins in one frame.
- **B2 — object-valued variables.** The `CompositionVariable` union extends from scalar-only to typed objects: `font` (`{name, source}`) and `image` (`{url, …}`), end-to-end (core union → SDK op → runtime merge). Colors stay scalar (per §7 LOCKED decision).
### Implementation notes
CSS compatibility was moved out of `apply-patches.ts` (where it was incorrectly writing CSS props as a side-effect of model patches, breaking inverse/undo) and into explicit patches emitted in `mutate.ts`. Forward emits `[modelPatch, cssPatch]` for scalars; inverse correctly generates `patchRemove` for the CSS prop when there was no prior CSS prop. Font/image variables never become CSS props.
### Files (12 changed, +441 −32)
- `packages/core`: `core.types.ts`, `lint/rules/composition.ts`, `parsers/htmlParser.ts` (+test), `runtime/validateVariables.ts`
- `packages/sdk`: `engine/mutate.ts` (+test), `engine/apply-patches.ts`, `engine/patches.ts`, `index.ts`, `types.ts`
### Gates
- `bun run build` ✅
- `bun test` SDK 304/0 ✅ · `validateVariables.test.ts` 13/0 ✅
- `bunx oxlint` 0/0 ✅ · `bunx oxfmt --check` ✅
- `fallow audit --gate new-only` ✅ (complexity inherited only)
> The +8 new `htmlParser.test.ts` font/image tests fail under the pre-existing `DOMParser is not defined` happy-dom limitation (main already carries 425 such failures) — not a logic bug; the pure runtime logic is covered by `validateVariables.test.ts`.
### Deferred
Brand-kit picker UI and `batch(setVariableValue × N)` wiring are Pacific-side; per-composition variable scoping beyond `__hfVariablesByComp`.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
|
||
|
|
7607a714a6 |
ci(publish): publish @hyperframes/sdk to npm (#1587)
The SDK is version-bumped by scripts/set-version.ts (it's in the PACKAGES list) but was never added to the publish_pkg list in publish.yml — so @hyperframes/sdk@0.6.112 sits on the version line yet is absent from npm (404), while core/player/engine/etc. all shipped at 0.6.112. Add the missing publish call so the SDK ships with every release. Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
fdb8f33fc0 |
fix(engine): hold the last video frame at the inclusive clip end (#1564)
* fix(engine): hold the last video frame at the inclusive clip end The frame-lookup active set deactivated a video on an exclusive end-bound (globalTime < end), while the runtime keeps an element visible through currentTime <= end (core/runtime init.ts). The rendered frame landing exactly on a clip's end went blank even though the runtime still showed the element on its final frame: one blank frame at the end of every clip whose end lands on a frame boundary. Make the active window inclusive of the end to match the runtime, and at t === end serve the last extracted frame (the runtime holds the element's final frame there too). Mid-clip source exhaustion (t < end) stays blank, unchanged. * fix(engine): align getFrame boundary to match refreshActiveSet Make getFrame's end-bound inclusive (> instead of >=) for consistency with the refreshActiveSet changes. getFrame is currently unused externally but should match the same contract. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Carlos Alcaraz <193642530+calcarazgre646@users.noreply.github.com> Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com> |
||
|
|
1bab79ef4c | chore: release v0.6.112 v0.6.112 | ||
|
|
7310223b66 |
feat(engine): static-frame dedup default-on + render telemetry (#1549)
* feat(engine): static-frame dedup for screenshot capture (opt-in) Skip re-seeking + re-screenshotting frames byte-identical to their predecessor. A frame is dedupable iff no GSAP tween or clip cut is active in it or its predecessor (predicted from window.__timelines + clip schedule) AND an empirical anchor-compare confirms it. Opt-in HF_STATIC_DEDUP=true, default off. Correctness (designed for the multi-worker / distributed render paths): - Reuse is keyed by the ABSOLUTE composition frame (derived from the frame's time), NOT the captureFrameCore frameIndex arg — chunked/parallel callers pass a chunk- relative index. Validated lossless (PSNR=inf) on both single- and multi-worker renders of a static-hold comp. - verifyStaticFramesSafe checks EVERY run (no longest-first budget truncation that left runs armed-but-unverified), and samples each run's FIRST reused frame, its END, and interior points at a stride; a hard cap disables dedup rather than trust an unverified set. - Conservative arming: skipped when capture mode != screenshot (BeginFrame tick semantics + the verifier's screenshot path wouldn't transfer), when a before-capture hook is set (per-frame video injection), when page-side compositing is active (shader / drawElement composite the plain verification screenshot can't reproduce), and when any data-start is a non-numeric reference expression the clip-boundary parser can't protect, or duration is unknown/zero. - Session reuse (prepareCaptureSessionForReuse) resets lastFrameBuffer + dedup counter so a probe/prior-render buffer can't bleed into the first static frame; the armed set is kept (same-composition reuse). Cost calibration bypasses dedup for its sparse, non-contiguous sample sweep, then restores the armed set. - HF_STATIC_DEDUP_SAMPLES is NaN-guarded. Disqualifies on signals the GSAP predictor can't see: video, canvas/webgl, zero tweens, running CSS/WAAPI animation. Pays on static-hold content (title cards, slideshow/kiosk loops, data-viz pauses); no-op on continuously-animated comps. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(engine): static-frame dedup default-on + render telemetry Flip dedup from opt-in (HF_STATIC_DEDUP=true) to default-on (opt-out HF_STATIC_DEDUP=false). Verification (verifyStaticFramesSafe) is the safety net that keeps reuse sound at scale. Add end-to-end dedup observability. The capture session records enabled / armed / skipReason / predicted; these surface via CapturePerfSummary -> a dedupPerfs accumulator (disk sequential + parallel AND streaming sequential + parallel) -> aggregated into RenderPerfSummary.staticDedup (OR armed, SUM frames across workers) -> render_complete props static_dedup_{enabled,armed,skip_reason, predicted_frames,reused_frames}. skip_reason is a low-cardinality code: capture_mode | video_injection | page_composite | ineligible | verification_failed. Distributed chunks run on Linux/beginframe where dedup never arms, so they pass a throwaway dedupPerfs sink (no per-chunk reporting). Tests: aggregation logic (OR/SUM/skip-reason) + opt-out passthrough. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(engine): address review on dedup default-on + telemetry Review feedback (miga-heygen) + self-review fixes: - Retry double-count: executeDiskCaptureWithAdaptiveRetry pushed worker dedup perf inside the retry loop, so an adaptive retry counted frames twice (reused/predicted could exceed totalFrames). Reset dedupPerfs at the start of each attempt — retry now REPLACES rather than accumulates; common no-retry path is unchanged. - Opt-out parsing: HF_STATIC_DEDUP now disables on {false,0,off} case/space-insensitive (was strict !== "false", so `False`/`0` silently kept dedup on — the kill-switch could no-op). - Verification budget vs drift: verifyStaticFramesSafe returns {badFrame, budgetExhausted}; armStaticDedup reports a distinct `verification_budget` skip reason so a telemetry spike means "raise HF_STATIC_DEDUP_SAMPLES", not "compositions are non-static". - Index idiom: captureFrameCore now uses Math.floor(time*fps + 1e-9) (matches quantizeTimeToFrame) so the dedup lookup agrees with the frame the seek lands on even for non-exact times. - Stale "opt-in HF_STATIC_DEDUP=true" comments -> "opt-out HF_STATIC_DEDUP=false" across frameCapture.ts + types.ts. - Extract pushWorkerDedupPerfs helper (perfSummary.ts), used by the disk and streaming parallel paths — removes the duplicated push loop and drops captureStreamingStage back under the complexity threshold. - dedupPerfs is now required (not optional) on executeDiskCaptureWithAdaptiveRetry — a missing arg silently dropped telemetry. - Test: captureStreamingStage createInput() now provides the required dedupPerfs field. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * refactor(engine): address deferred dedup-review items - Derivable state: drop session.staticDedupArmed/staticDedupPredicted; derive both from session.staticFrames in getCapturePerfSummary (armed ⟺ non-empty set, predicted === size) so they can't desync. - Config altitude: HF_STATIC_DEDUP now resolves into EngineConfig.staticFrameDedup (resolveConfig, opt-out on {false,0,off}), alongside forceScreenshot/browserGpuMode — armStaticDedup reads config instead of process.env. Default-on preserved (missing config → enabled). - Lossy aggregation: aggregateDedup now reports DISTINCT skip reasons (sorted, `|`-joined) across diverging unarmed workers instead of just the first. - discardWarmupCapture: also snapshot/restore staticDedupCount and lastFrameBuffer so a warmup capture can't leak a phantom reuse or a stale buffer anchor into the real summary. - Convention: perfSummary-dedup.test builds its job via createRenderJob instead of `as unknown as RenderJob`. - Docs: verification_budget added to skip-reason lists. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
5fb5153e4e |
fix(studio): shadow resolves bare leaf via dispatch path, not getElement (#1552)
The resolver-parity shadow tripwire decided element_not_found via Composition.getElement, which is canonical-only for a bare id by design (removeElement/getElement must agree on the same instance — the session.subcomp "ambiguous bare id" suite). But the cutover persist path dispatches the studio's bare data-hf-id and resolves it via resolveScoped, which locates the leaf anywhere in the document (canonical preferred, else first match). So getElement under-resolved a bare leaf living inside an inlined sub-composition (scopedId "host/leaf"), and the shadow emitted a false element_not_found the real dispatch path never hits — ~445 such events in PostHog, all one user editing an inlined yt-lower-third. Add resolveSnapshot in the shadow mirroring resolveScoped, used at all three element_not_found sites. getElement is unchanged (its contract is correct). Regression tests cover the inlined-sub-comp leaf case. Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |