mirror of
https://github.com/heygen-com/hyperframes.git
synced 2026-09-10 22:20:14 +00:00
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
This commit is contained in:
@@ -128,9 +128,33 @@ export function usePlaybackKeyboard({
|
||||
return;
|
||||
}
|
||||
shuttle("forward");
|
||||
return;
|
||||
}
|
||||
if (e.code === "KeyI") {
|
||||
e.preventDefault();
|
||||
const t = getAdapter()?.getTime() ?? usePlayerStore.getState().currentTime;
|
||||
usePlayerStore.getState().setInPoint(e.shiftKey ? null : t);
|
||||
return;
|
||||
}
|
||||
if (e.code === "KeyO") {
|
||||
e.preventDefault();
|
||||
const t = getAdapter()?.getTime() ?? usePlayerStore.getState().currentTime;
|
||||
usePlayerStore.getState().setOutPoint(e.shiftKey ? null : t);
|
||||
return;
|
||||
}
|
||||
if (e.code === "KeyA") {
|
||||
e.preventDefault();
|
||||
seek(usePlayerStore.getState().inPoint ?? 0);
|
||||
return;
|
||||
}
|
||||
if (e.code === "KeyE") {
|
||||
e.preventDefault();
|
||||
const { outPoint } = usePlayerStore.getState();
|
||||
seek(outPoint ?? getAdapter()?.getDuration() ?? usePlayerStore.getState().duration);
|
||||
return;
|
||||
}
|
||||
},
|
||||
[pause, shuttle, stepFrames, togglePlay],
|
||||
[pause, shuttle, stepFrames, togglePlay, getAdapter, seek],
|
||||
);
|
||||
|
||||
const handlePlaybackKeyUp = useCallback((e: KeyboardEvent) => {
|
||||
|
||||
@@ -185,15 +185,21 @@ export function useTimelinePlayer() {
|
||||
const time = adapter.getTime();
|
||||
const dur = adapter.getDuration();
|
||||
liveTime.notify(time); // direct DOM updates, no React re-render
|
||||
if (time >= dur && !adapter.isPlaying()) {
|
||||
const { inPoint, outPoint } = usePlayerStore.getState();
|
||||
const rawLoopEnd = outPoint !== null ? outPoint : dur;
|
||||
const rawLoopStart = inPoint !== null ? inPoint : 0;
|
||||
const loopEnd = rawLoopStart < rawLoopEnd ? rawLoopEnd : dur;
|
||||
const loopStart = rawLoopStart < rawLoopEnd ? rawLoopStart : 0;
|
||||
if (time >= loopEnd) {
|
||||
if (usePlayerStore.getState().loopEnabled && dur > 0) {
|
||||
adapter.seek(0);
|
||||
liveTime.notify(0);
|
||||
adapter.seek(loopStart);
|
||||
liveTime.notify(loopStart);
|
||||
adapter.play();
|
||||
setIsPlaying(true);
|
||||
rafRef.current = requestAnimationFrame(tick);
|
||||
return;
|
||||
}
|
||||
if (adapter.isPlaying()) adapter.pause();
|
||||
setCurrentTime(time); // sync Zustand once at end
|
||||
setIsPlaying(false);
|
||||
cancelAnimationFrame(rafRef.current);
|
||||
@@ -241,7 +247,7 @@ export function useTimelinePlayer() {
|
||||
const adapter = getAdapter();
|
||||
if (!adapter) return;
|
||||
if (adapter.getTime() >= adapter.getDuration()) {
|
||||
adapter.seek(0);
|
||||
adapter.seek(usePlayerStore.getState().inPoint ?? 0);
|
||||
}
|
||||
unmutePreviewMedia(iframeRef.current);
|
||||
applyPlaybackRate(usePlayerStore.getState().playbackRate);
|
||||
@@ -269,15 +275,20 @@ export function useTimelinePlayer() {
|
||||
const tick = (now: number) => {
|
||||
const elapsed = ((now - startedAt) / 1000) * speed;
|
||||
let nextTime = startTime - elapsed;
|
||||
if (nextTime <= 0) {
|
||||
const { inPoint, outPoint } = usePlayerStore.getState();
|
||||
const rawLoopEnd = outPoint !== null ? outPoint : duration;
|
||||
const rawLoopStart = inPoint !== null ? inPoint : 0;
|
||||
const loopEnd = rawLoopStart < rawLoopEnd ? rawLoopEnd : duration;
|
||||
const loopStart = rawLoopStart < rawLoopEnd ? rawLoopStart : 0;
|
||||
if (nextTime <= loopStart) {
|
||||
if (usePlayerStore.getState().loopEnabled && duration > 0) {
|
||||
startTime = duration;
|
||||
startTime = loopEnd;
|
||||
startedAt = now;
|
||||
nextTime = duration;
|
||||
nextTime = loopEnd;
|
||||
} else {
|
||||
adapter.seek(0);
|
||||
liveTime.notify(0);
|
||||
setCurrentTime(0);
|
||||
adapter.seek(loopStart);
|
||||
liveTime.notify(loopStart);
|
||||
setCurrentTime(loopStart);
|
||||
setIsPlaying(false);
|
||||
shuttleDirectionRef.current = null;
|
||||
reverseRafRef.current = 0;
|
||||
|
||||
Reference in New Issue
Block a user