mirror of
https://github.com/heygen-com/hyperframes.git
synced 2026-09-12 23:29:50 +00:00
Vertical clip moves committed in the store but never survived: two persist
bugs plus a runtime renumber all fought the stable-track-lanes model.
- timelineMoveAdapter deliberately stripped the track from lane-reorder
persists ('z-only reorder path' — the old z-driven lane model). Lane =
authored data-track-index now: lane-reorder and track-insert both persist
the track; plain timing moves omit it to stay SDK-fast-path eligible.
- Display lanes and file tracks are different coordinate spaces:
normalizeToZones packs sparse authored tracks (1,2,... or gaps, or DOM-index
fallbacks) onto contiguous display lanes, and lane edits persisted the LANE
number — silently re-targeting the wrong row in any non-0-contiguous file.
Elements now record their authoredTrack when remapped; a lane change
persists the target lane's authored track (store stays in lane space).
- The runtime split same-track clips of different kinds (video vs caption
div) onto separate renumbered tracks at discovery, so authored indices
never round-tripped ('drop onto an existing track' bounced back). Removed:
data-track-index is honored verbatim (render never reads it); kind-based
row presentation belongs in the display layer if ever wanted.
Adversarial review fixes on the same pipeline:
- runtime: parseInt(attr) || fallback dropped authored track 0 for GSAP and
overlay clips (parseAuthoredTrack helper honors 0)
- single-clip move fallback persisted only data-start — lane changes snapped
back on reload (now passes the track to the patch builder)
- lane-change z-sync candidate ignored a multi-selection's time shift, so
patches were computed against stale overlap sets
- track insert around a locked clip persisted a colliding renumber (the next
normalize merged lanes); the insert is now refused with a warning
- computeStackingPatches compared leaf z across CSS stacking contexts, where
ancestor z decides paint order; the sync now partitions by
stackingContextId and never patches across contexts
Timeline geometry (user-reported):
- fit zoom leaves 20% trailing headroom (FIT_ZOOM_HEADROOM in
timelineLayout.ts; single fit-pps source, so ruler/lanes/playhead/drag all
inherit it)
- playhead line center now sits exactly on GUTTER + t*pps at every zoom
(wrapper had shrink-wrapped to the 9px diamond, off-centering the line);
ruler ticks center on their timestamp
- ruler: frame-mode steps snap to whole frames (no duplicate labels), hour
steps added for far zoom-out, tick positions computed as exact multiples
(no float drift)
104 lines
3.8 KiB
TypeScript
104 lines
3.8 KiB
TypeScript
// fallow-ignore-file dead-code
|
|
/**
|
|
* Shared playhead visual used by TimelineCanvas (real playhead) and
|
|
* TimelineEditorNotice (animated illustration).
|
|
*
|
|
* The vertical line + glow span the full track height; the grab-handle HEAD is
|
|
* `position: sticky; top: 0` so it pins to the top of the (vertically) scrolling
|
|
* track area — the ruler is sticky too, so the head stays visible and grabbable
|
|
* no matter how far the tracks are scrolled. The head is OUTLINE-only at rest and
|
|
* FILLED while the playhead is actively held/scrubbed (`scrubbing`).
|
|
*/
|
|
import { PLAYHEAD_HEAD_W } from "./timelineLayout";
|
|
|
|
interface PlayheadIndicatorProps {
|
|
/** CSS color, defaults to the HF accent variable */
|
|
color?: string;
|
|
/** Glow shadow color, defaults to translucent accent */
|
|
glowColor?: string;
|
|
/** Whether the playhead is being actively scrubbed — fills the head. */
|
|
scrubbing?: boolean;
|
|
/**
|
|
* When false, the head chip is rendered in normal flow (top:1) instead of the
|
|
* sticky pin — used by the static illustration where there is no scroll area.
|
|
*/
|
|
stickyHead?: boolean;
|
|
}
|
|
|
|
export function PlayheadIndicator({
|
|
color = "var(--hf-accent, #3CE6AC)",
|
|
glowColor = "rgba(60,230,172,0.14)",
|
|
scrubbing = false,
|
|
stickyHead = true,
|
|
}: PlayheadIndicatorProps) {
|
|
// Head chip dimensions — used to compute the centering offset and the
|
|
// point where the vertical line starts (so it begins at the head's bottom
|
|
// edge rather than running through the hollow diamond center). The width is
|
|
// the shared PLAYHEAD_HEAD_W constant: getTimelinePlayheadLeft shifts the
|
|
// wrapper by -PLAYHEAD_HEAD_W/2 so the 1px line (centered at 50% of the
|
|
// wrapper) lands exactly on GUTTER + time * pps — the ruler ticks' center x.
|
|
const HEAD_W = PLAYHEAD_HEAD_W;
|
|
const HEAD_H = 9;
|
|
// marginTop(1) + HEAD_H = where the line should start.
|
|
const HEAD_TOTAL_H = 1 + HEAD_H;
|
|
|
|
return (
|
|
<>
|
|
{/* Glow — spans full height, centered on the line. */}
|
|
<div
|
|
aria-hidden="true"
|
|
className="absolute top-0 bottom-0"
|
|
style={{
|
|
left: "50%",
|
|
width: 13,
|
|
transform: "translateX(-50%)",
|
|
background: `radial-gradient(closest-side, ${glowColor}, transparent)`,
|
|
}}
|
|
/>
|
|
{/* Vertical line — starts at the bottom edge of the head chip so nothing
|
|
shows through the hollow diamond center. */}
|
|
<div
|
|
className="absolute bottom-0"
|
|
style={{
|
|
left: "50%",
|
|
top: HEAD_TOTAL_H,
|
|
width: 1,
|
|
marginLeft: -0.5,
|
|
background: color,
|
|
boxShadow: `0 0 6px ${glowColor}`,
|
|
}}
|
|
/>
|
|
{/* Head chip — sticky so it pins to the ruler while tracks scroll.
|
|
Centering logic: wrapper width = HEAD_W (chip forces it). The line sits
|
|
at wrapper.left + HEAD_W/2 (left:"50%" of wrapper). The sticky element's
|
|
natural flow position is wrapper.left; so placing it there with no
|
|
horizontal translate puts its LEFT edge at wrapper.left and its CENTER
|
|
at wrapper.left + HEAD_W/2 — exactly on the line. */}
|
|
<div
|
|
className={stickyHead ? "sticky" : "absolute"}
|
|
style={{
|
|
left: 0,
|
|
top: stickyHead ? 0 : 1,
|
|
// Zero height keeps it from covering rows (sticky strip trick).
|
|
height: stickyHead ? 0 : undefined,
|
|
}}
|
|
>
|
|
<div
|
|
style={{
|
|
width: HEAD_W,
|
|
height: HEAD_H,
|
|
borderRadius: 2,
|
|
marginTop: 1,
|
|
// Outline-only at rest, filled while scrubbing.
|
|
background: scrubbing ? color : "transparent",
|
|
border: `1.5px solid ${color}`,
|
|
boxSizing: "border-box",
|
|
boxShadow: `0 1px 3px rgba(0,0,0,0.55), 0 0 5px ${glowColor}`,
|
|
transform: "rotate(45deg)",
|
|
}}
|
|
/>
|
|
</div>
|
|
</>
|
|
);
|
|
}
|