Commit Graph
4 Commits
Author SHA1 Message Date
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
Carlos Alcaraz eac8808425 fix(studio): use e.key for playback shortcuts so non-QWERTY layouts work
The 7 letter shortcuts (J/K/L/I/O/A/E) in usePlaybackKeyboard were gated
on `e.code === "Key*"`, which is the physical key position on a US-QWERTY
layout. On AZERTY (and other layouts) the physical "KeyA" slot produces
e.key="q", so "Jump to in-point" and the rest of the letter shortcuts
either fired on the wrong character or not at all.

Switch the 7 letter shortcuts to compare `e.key.toLowerCase()` and rename
`pressedCodesRef` → `pressedKeysRef` so the K-hold combo (K+J / K+L for
frame stepping) is also keyed off the typed character. `Space` and
`Arrow*` keep using `e.code` since those codes are layout-independent.

Adds a happy-dom test covering QWERTY happy path, AZERTY (physical KeyQ
produces e.key="a" → in-point seek fires), AZERTY contrapositive (physical
KeyA producing e.key="q" no longer triggers in-point), Shift+I clears
in-point, K-hold combo for frame stepping, K release returning the set
to clean state, and Space passthrough.

Addresses bug #3 in #834. Bugs #1 (loop at out-point) and #2 (Jump to
in-point forcing pause) live outside this hook (player loop and adapter
`seek` respectively) and are left for follow-up PRs.
2026-05-14 13:48:55 -03:00
Miguel Ángel 1caeb28658 feat(studio): header logo, playbar cleanup, and I/O work-area markers (#811)
* feat(studio): header logo, playbar cleanup, and I/O work-area markers

- Add Hyperframes icon mark to the studio header (left of project name)
- Remove m:ss toggle button — click the timecode directly to switch modes
- Remove frame jump input from controls bar — moved into ⌨ shortcuts panel
- Replace Loop text button with a repeat icon
- Collapse J/K/L shortcut badges into a single ⌨ icon that opens a panel
- Shortcuts panel: Jump to frame, Work area I/O display, shortcuts reference
- Implement I/O work-area markers (closes #807):
  - I / Shift+I: set / clear in-point at playhead
  - O / Shift+O: set / clear out-point at playhead
  - A: jump to in-point (or start); E: jump to out-point (or end)
  - Loop respects in/out boundaries for both forward and backward playback
  - Teal work-area band + tick markers rendered on the seek bar

* fix(studio): guard against inverted in/out work-area points in loop ticks

If the user sets out-point before in-point (outPoint < inPoint), rawLoopStart
>= rawLoopEnd caused the loop guard to fire immediately on every tick, creating
a tight infinite seek loop. Both the forward RAF tick and the reverse RAF tick
now fall back to the full composition range when the work area is invalid.

* fix(studio): address work-area edge cases from review

- setInPoint/setOutPoint now cross-clear the opposite marker when setting one
  would produce an inverted range (in >= out), preventing the invalid state
  rather than correcting it at tick time
- Forward tick no longer gates on !adapter.isPlaying() — outPoint crossing
  fires even while the adapter is running; explicitly pauses on the non-loop
  path so playback stops at out-point rather than sailing to dur
- play() end-of-stream reset seeks to inPoint (if set) instead of hardcoded 0

* feat(studio): use full Hyperframes wordmark logo in header

Replace the standalone icon mark with the complete logo from logo-dark.svg
(icon mark + Hyperframes wordmark), with all black text fills inverted to
white for the dark header background. Project name is shown next to the logo
separated by a middot.

* Revert "feat(studio): use full Hyperframes wordmark logo in header"

This reverts commit a2815fc7d0.

* feat(studio): show full HeyGen/Hyperframes logo in header

Replace the standalone chevron icon with the complete logo from logo-dark.svg:
heygen label + gradient mark + hyperframes wordmark, all white fills on dark
background. Project name follows after a middot separator.

* fix(studio): use | instead of · as logo/project separator
2026-05-14 01:06:55 +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