Commit Graph
6 Commits
Author SHA1 Message Date
Vance IngallsandClaude Fable 5 7a99ccec6d fix(core): honor root data-duration when GSAP timeline ends short (#1378)
* fix(core): honor root data-duration when GSAP timeline ends short

The authored-duration floor only counted child composition clips, never
the root element's own data-duration. A composition whose GSAP timeline
ended even 0.1s short of its declared data-duration reported the shorter
timeline length from player.getDuration() — and the studio's adapter
selection (docDuration <= adapterDur) then silently rejected the
audio-capable runtime player, downgrading preview playback to the
seek-scrubbing adapter, which never starts media elements or WebAudio.
Result: total audio silence with zero errors anywhere.

- include the root's declared data-duration in
  resolveAuthoredCompositionDurationFloorSeconds, making data-duration
  the source of truth for playable length (per the documented contract)
- console.warn in the studio when playback falls back to the
  seek-driven adapter, since the downgrade loses audio invisibly

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(studio): release static-seek adapter on native win, warn once on downgrade

Review findings on the previous commit, all in the static-seek fallback
path of useTimelinePlayer.getAdapter:

- A cached static-seek adapter was never paused when adapter selection
  later resolved a native adapter (the early returns bypass the fallback
  branch entirely), leaving its private rAF loop seeking the player while
  the native transport also drives it. The core data-duration fix makes
  this switch path much more common. releaseStaticSeekCache() now runs
  at every native-adapter return and at unmount.
- The downgrade warning fired on every cache miss — and the cache key can
  never hold for __timelines compositions because wrapTimeline() returns
  a fresh object per call, so it fired every rAF tick. It now warns once
  per downgrade streak (re-armed when a native adapter takes over).
- The warning interpolated adapterDur (the native __player duration,
  0 when absent) instead of the selected adapter's duration, and used a
  one-off "[hyperframes-studio]" prefix instead of the file's
  "[useTimelinePlayer]" convention.

The fallback cache logic moved to playbackAdapter.ts (with unit tests for
warn-once, cache identity, and pause-on-replace/release), which also
keeps useTimelinePlayer.ts inside the studio 600-line limit. Also
corrected a stale "no DOM reads" comment on the runtime transport tick —
the duration floor has always queried the DOM per call, and now also
reads the root's declared data-duration.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-06-12 11:01:39 -07:00
Carlos Alcaraz GregorandCarlos Alcaraz 8ecef4b939 fix(studio): make static-seek adapter honor keepPlaying option (#1089)
createStaticSeekPlaybackAdapter.seek now accepts the same options as the
PlaybackAdapter contract and aligns the default-pause semantics with
wrapTimeline (hardened in 3e7b464b). Without keepPlaying the adapter
clears its `playing` flag and cancels the RAF ticker, so on non-GSAP
compositions a scrub during playback no longer leaves the iframe
silently advancing while the public seek wrapper marks isPlaying=false.

Follow-up to #863 review: jrusso called out the type drift and invited
a separate PR; this also closes the asymmetry with wrapTimeline.

Co-authored-by: Carlos Alcaraz <193642530+calcarazgre646@users.noreply.github.com>
2026-05-27 00:50:30 -04:00
Miguel Ángel 3e7b464b3b fix(studio): ensure GSAP timeline stays paused after seek
Call tl.pause() both before AND after tl.seek() in the adapter.
GSAP's seek() can reactivate a timeline depending on internal state;
the second pause() guarantees it stays frozen at the seeked position.
2026-05-21 21:26:32 -04:00
James 7e0a447325 refactor: drop unused exports detected by fallow auto-fix
Run `fallow fix --auto-fixable` to remove `export` keywords from symbols
fallow's reachability analysis identifies as unused. Keeps only the cases
where the symbol is still referenced internally in its own file (so
removing `export` doesn't surface a new oxlint `no-unused-vars` error).

Result: fallow dead-code findings drop from 276 → 208 (68 fewer unused
exports), with no behavior change — each symbol is still defined and used
exactly the same way within its file.

Reverted ~20 files where fallow's auto-fix would have created cascading
"declared but never used" lint errors — those are cases where the symbol
isn't used at all, and properly cleaning them up means deleting the
declaration, not just dropping `export`. Better to land that as a
separate, narrower PR rather than mixing it into a mechanical de-export.

Also reverted four false positives where fallow missed real consumers:
- `captureCost.ts` (renderOrchestrator has two separate import blocks
  from the same module; fallow only saw the first)
- `propertyPanelHelpers.ts`, `domEditingLayers.ts` (real internal uses
  fallow's reachability missed)
- `render.ts` (functions imported via `await import()` dynamic import,
  which fallow's static analysis doesn't follow)

Test plan: bun run --filter '*' typecheck (clean), oxlint + oxfmt clean,
cli/core/studio/engine vitest suites pass (335 + 917 + 576 + 605 tests).
2026-05-19 00:51:56 +00:00
Carlos Alcaraz GregorandCarlos Alcaraz 0f2a705259 fix(studio): preserve playback state on Jump-to-in/out shortcuts (#842)
When the user has the timeline playing and presses A (Jump to in-point)
or E (Jump to out-point), the seek seeks to the marker as expected but
also pauses the playback. The reporter (and the natural UX) expects
playback to keep going from the marker.

Root cause sits in two layers:

1. The `seek` callback in `useTimelinePlayer.ts` unconditionally calls
   `setIsPlaying(false)` and `stopRAFLoop()` whenever the store reports
   playing. That path is shared with timeline clicks, LayersPanel
   navigation, and frame stepping — flipping the default would change
   behavior the rest of the app expects.

2. `wrapTimeline` (the GSAP-timeline-backed adapter) calls `tl.pause()`
   before `tl.seek(t)`, so even if the callback above stopped pausing,
   GSAP-driven compositions would still get paused inside the adapter.

The fix is opt-in at both layers:

- Extend `PlaybackAdapter.seek` with `options?: { keepPlaying?: boolean }`.
  Default is omitted/false, preserving existing behavior for every
  caller that doesn't pass the option.
- `wrapTimeline.seek` skips the implicit `tl.pause()` when keepPlaying
  is set. `createStaticSeekPlaybackAdapter` accepts the new signature
  but is a no-op for the flag (it never paused internally).
- `useTimelinePlayer` seek callback grows the same option and forwards
  it to adapter.seek(time, options). The reset block (stopRAFLoop,
  setIsPlaying(false), shuttle refs) is gated behind !options.keepPlaying.
- Reverse shuttle is always stopped on seek (the RAF reverse tick
  cannot survive a seek), so keepPlaying is overridden when the
  shuttle was running backward. Documented with an inline comment.
- usePlaybackKeyboard updates its seek param type to match and passes
  { keepPlaying: true } on the A and E handlers only. Frame stepping
  (Arrow keys, J/L with K held) keeps the default.

Tests (happy-dom):

- useTimelinePlayer.seek.test.ts covers the callback in three cases:
  default seek clears isPlaying, seek with keepPlaying preserves
  isPlaying=true, and the option from paused state stays paused.
- playbackAdapter.test.ts (new) covers wrapTimeline: default seek
  pauses the GSAP timeline, keepPlaying: true skips the pause,
  keepPlaying: false is the explicit default.

Closes part of #834 (sub-bug #2). Sub-bug #1 (playhead should loop
to in-point when exceeding out-point) lives in the RAF tick and is
left for a follow-up PR.

Co-authored-by: Carlos Alcaraz <193642530+calcarazgre646@users.noreply.github.com>
2026-05-14 22:37:32 +02:00
Miguel Ángel 91bdffffe6 fix(ci): scope LOC check to studio, split useTimelinePlayer + hyperframes-player under 500 LOC (#750)
* refactor: split useTimelinePlayer.ts and hyperframes-player.ts into focused modules (<500 LOC each)

* fix(ci): scope 500 LOC check to packages/studio, add allowlist for grandfathered files

* feat(cli): Linux ARM64 support — auto-install Chromium on DGX Spark / GB10 / Jetson

Chrome Headless Shell has no Linux ARM64 binary. On arm64 Linux:
- Detects the platform automatically
- Tries to auto-install system Chromium via apt-get (works on Ubuntu/Debian ARM)
- Falls back to clear manual instructions with exact commands
- 'hyperframes browser ensure' guides through the setup interactively
- After setup, all render commands work without any flags

* fix(ci): disable Windows Defender real-time monitoring to prevent EPERM builds

Path exclusions are insufficient — Defender re-scans new files created
during bun install before the exclusion takes effect. Disable real-time
monitoring for the entire job duration instead (standard CI practice).

* refactor(studio): split all files >500 LOC + extract useToast, delete allowlist

All 11 large files split into focused modules under 500 LOC.
App.tsx extracted toast logic into useToast hook (493 LOC now).
.filesize-allowlist deleted — no longer needed.

* fix: remove unused imports from split files, extract useToast from App.tsx

App.tsx: 504 → 493 lines (toast logic extracted to useToast hook)
timelineDOM.ts: remove unused imports from re-export pattern
MotionPanel.tsx: remove unused clampStudioCustomEasePoints import
studioMotionOps.ts: remove unused StudioGsapMotionDirection import

* fix(ci): use Set-MpPreference to fully disable Windows Defender (both jobs)

* fix(producer): use node --experimental-strip-types instead of tsx for build:fonts

Eliminates the tsx binary dependency that Windows Defender locks during
bun install, causing EPERM errors. Node 22.6+ strips TypeScript types
natively with no external binary.

* chore: remove .filesize-allowlist — App.tsx is now 493 lines (<500)

* fix(ci): disable Windows Defender before checkout to prevent all EPERM races

* fix(producer): skip build:fonts if fontData.generated.ts already exists

The generated file is tracked in git, so CI doesn't need to regenerate
it. This avoids @fontsource/inter node_modules access on Windows which
triggers EPERM from Defender scanning during bun install.
2026-05-13 01:48:12 +02:00