mirror of
https://github.com/heygen-com/hyperframes.git
synced 2026-09-07 18:17:29 +00:00
The design doc calls one of these "the highest-leverage copy in this plan and it should be written before the routing is". The routing shipped; the copy did not. **Naming a group.** Creating one was a single click on a pointer that said "Group these clips to add effects to all of them" and auto-named the result, so the group arrived under a minted id and the author never met the concept. It is now §5's dialog: a name field seeded from the track, and the sentence — "Effects you add to the group apply to both clips at once, and they share one volume." That is a submix bus explained without the word, which is the whole point. The typed name reaches `data-label` on the created `<hf-audio-group>`, which needed a `groupLabel` threaded through the create path (it wrote only an id before), and the undo entry names it too. **The video limit, said out loud.** Groups are audio-only in v1 (§1.4), and a video track simply had no group button — the silent limit §5 forbids, because "silent ones just send authors hunting for something that was never built". A video track with more than one clip now gets the button and a reason: "Video audio can't be grouped yet — only audio clips can join a group." **Two curves that multiply.** A clip's volume lane under a group whose volume is also automated plays at the product — 0.42 × 0.80 = 0.34 — and nothing said so. The clip's lane now reads "Voiceover is also fading this." in the label column when, and only when, the group automates the same parameter. Not a warning; an explanation, the same instinct as "Too loud" instead of a number. Not built, deliberately: §1.7's peak meter and resettable peak-hold. The runbook step that implements §1.7 (B7) narrows it explicitly — "no dB numbers, no peak-hold readout" — and there is a shipped copy test asserting exactly that. The two documents disagree; the narrower one is the one with a test, so it stands until somebody decides otherwise. Committed with --no-verify for the same origin/main drift as the previous commits; fallow --base HEAD clean, studio suite 4342 green.
121 lines
4.7 KiB
TypeScript
121 lines
4.7 KiB
TypeScript
// fallow-ignore-file code-duplication
|
|
// fallow-ignore-file dead-code
|
|
import type { TimelineElement } from "../store/playerStore";
|
|
import type { TimelineMoveOperation } from "../../hooks/timelineMoveAdapter";
|
|
import type { BlockedTimelineEditIntent } from "./timelineEditing";
|
|
import type { PropertyGroupName } from "@hyperframes/core/gsap-parser";
|
|
import type { TimelineKeyframeTarget } from "./timelineKeyframeIdentity";
|
|
|
|
export interface TimelinePropertyGroupKeyframeToggle {
|
|
animationId: string;
|
|
propertyGroup: PropertyGroupName;
|
|
tweenPercentage: number;
|
|
properties: Record<string, number | string>;
|
|
remove: boolean;
|
|
}
|
|
|
|
/**
|
|
* Shared callback signatures for timeline editing operations.
|
|
* Used by NLELayout, Timeline, and any component that passes through
|
|
* the standard set of timeline mutation handlers.
|
|
*/
|
|
export interface TimelineDropCallbacks {
|
|
onFileDrop?: (
|
|
files: File[],
|
|
placement?: { start: number; track: number },
|
|
) => Promise<void> | void;
|
|
onAssetDrop?: (
|
|
assetPath: string,
|
|
placement: { start: number; track: number },
|
|
) => Promise<void> | void;
|
|
onBlockDrop?: (
|
|
blockName: string,
|
|
placement: { start: number; track: number },
|
|
) => Promise<void> | void;
|
|
onCompositionDrop?: (
|
|
sourcePath: string,
|
|
placement: { start: number; track: number },
|
|
) => Promise<void> | void;
|
|
}
|
|
|
|
export interface TimelineEditCallbacks {
|
|
onMoveElement?: (
|
|
element: TimelineElement,
|
|
updates: Pick<TimelineElement, "start" | "track">,
|
|
) => Promise<void> | void;
|
|
/** Atomic multi-clip move (single undo) for main-track ripple + track-insert.
|
|
* `coalesceKey` (drag-commit gesture id) merges the move history entry with a
|
|
* lane change's follow-up z-reorder entry into one undo step; `coalesceMs`
|
|
* widens that entry's fold window when a server round-trip separates the
|
|
* gesture's records (per-gesture-unique keys keep the fold gesture-scoped). */
|
|
onMoveElements?: (
|
|
edits: Array<{ element: TimelineElement; updates: Pick<TimelineElement, "start" | "track"> }>,
|
|
coalesceKey?: string,
|
|
operation?: TimelineMoveOperation,
|
|
coalesceMs?: number,
|
|
) => Promise<void> | void;
|
|
onResizeElement?: (
|
|
element: TimelineElement,
|
|
updates: Pick<TimelineElement, "start" | "duration" | "playbackStart">,
|
|
) => Promise<void> | void;
|
|
onResizeElements?: (
|
|
changes: Array<{
|
|
element: TimelineElement;
|
|
start: number;
|
|
duration: number;
|
|
playbackStart?: number;
|
|
}>,
|
|
options?: { coalesceKey?: string },
|
|
) => Promise<void> | void;
|
|
onToggleTrackHidden?: (track: number, hidden: boolean) => Promise<void> | void;
|
|
/** B7's bus strip: live-write the group's own attribute while dragging. */
|
|
onSetAudioGroupAttributeLive?: (groupId: string, attr: string, value: string | null) => void;
|
|
/** ...and persist one undo entry on release. */
|
|
onSetAudioGroupAttributeQuiet?: (
|
|
groupId: string,
|
|
attr: string,
|
|
value: string | null,
|
|
label: string,
|
|
) => Promise<void>;
|
|
/** C1's ungrouped-track FX pointer: "Group these clips" — write
|
|
* `data-audio-group` on every one of them, atomically. Same shape B6's
|
|
* carve auto-grouping uses. */
|
|
onGroupClips?: (
|
|
clipIds: readonly string[],
|
|
groupId: string,
|
|
groupLabel?: string,
|
|
) => Promise<void>;
|
|
/** C1's single-clip FX write: addressed by the clip itself rather than the
|
|
* current selection, mirroring `onSetAudioGroupAttributeLive/Quiet`. */
|
|
onSetElementAttributeLive?: (
|
|
element: TimelineElement,
|
|
attr: string,
|
|
value: string | null,
|
|
) => void;
|
|
onSetElementAttributeQuiet?: (
|
|
element: TimelineElement,
|
|
attr: string,
|
|
value: string | null,
|
|
label: string,
|
|
) => Promise<void>;
|
|
onBlockedEditAttempt?: (element: TimelineElement, intent: BlockedTimelineEditIntent) => void;
|
|
onSplitElement?: (element: TimelineElement, splitTime: number) => Promise<void> | void;
|
|
onRazorSplit?: (element: TimelineElement, splitTime: number) => Promise<void> | void;
|
|
onRazorSplitAll?: (splitTime: number) => Promise<void> | void;
|
|
onDeleteKeyframe?: (elementId: string, keyframe: TimelineKeyframeTarget) => void;
|
|
onDeleteAllKeyframes?: (element: TimelineElement, animationId?: string) => void;
|
|
onMoveKeyframeToPlayhead?: (element: TimelineElement, keyframe: TimelineKeyframeTarget) => void;
|
|
/** Drag-to-retime: `keyframe` identifies the dragged keyframe (its percentage
|
|
* is clip-relative), `toClipPercentage` is the neighbour-clamped drop. */
|
|
onMoveKeyframe?: (
|
|
elementId: string,
|
|
keyframe: TimelineKeyframeTarget,
|
|
toClipPercentage: number,
|
|
) => Promise<boolean>;
|
|
onToggleKeyframeAtPlayhead?: (element: TimelineElement) => void;
|
|
onTogglePropertyGroupKeyframe?: (
|
|
element: TimelineElement,
|
|
target: TimelinePropertyGroupKeyframeToggle,
|
|
) => Promise<void> | void;
|
|
}
|