mirror of
https://github.com/heygen-com/hyperframes.git
synced 2026-09-12 23:29:50 +00:00
B5: mute and solo, on groups and tracks (track mute already shipped by A2 —
nothing to build there).
Group mute — persisted as data-hidden on the <hf-audio-group> element itself
(never written onto members, per design doc §2.1's state-restoration
warning). Studio action reuses B7's generic setAudioGroupAttribute
(setQuiet/setLive split) rather than duplicating toggleTimelineTrackHidden's
shape — same one-atomic-patch/one-undo-entry contract, already built for
exactly this purpose. Render: B4 already drops every member of a
data-hidden group (confirmed by a new audioMixer.test.ts case — no
production change needed there). Preview: a dedicated muteGain node
(groupInput -> [fx] -> muteGain -> output -> master) so a mute toggle
never fights scheduleVolumeLane's ramps on the same param — the same
hazard B7's volume fader was split out to avoid. Mid-playback toggles
sync via a new syncAudioGroupMute pass in init.ts (a group carries no
data-start, so it's invisible to the existing visibility-node query).
Members of a muted group render the strikethrough label treatment
(TimelineTrackPlainHeader's isGroupMuted, sourced from
TimelineElement.audioGroupHidden) — display only, no attribute touched.
Solo — "Hear only this": a new session-only store slice (audioSoloSlice,
soloed: ReadonlySet<string> of clip/group ids, never track numbers, never
serialized). Predicate (isAudibleUnderSolo, packages/core/src/audioGroups.ts
so both the store and the preview transport share one definition): an
element is audible while any solo is active only if it or its own group is
soloed. "Siblings, never ancestors" lives in the graph, not the predicate —
solo gain is a per-element stage only; group buses are never attenuated by
solo, so a soloed member's path through its group stays open by
construction. Preview: a dedicated per-element soloGain in
webAudioTransport.ts (parallel to the mute mechanics), pushed via
window.__hf.setAudioSolo — a direct call, not an attribute write, so it
can't ride the visibility-diff path mute uses. media.ts's HTMLMedia
fallback folds the same predicate into its per-tick volume computation
(the same seam A2 used for data-hidden). Half-lit group indicator
(isGroupHalfLitUnderSolo) for "not soloed itself, but a member is".
Exclusive-by-default toggle, ⌘/Ctrl-click to add/remove, TimelineSoloButton
(⌗) beside mute on both track and group headers. Transport-bar banner
("Hearing only <label> — your export is not affected", Clear button) added
in PlayerControls.tsx, reading labels straight off the live preview DOM.
Export-safety, the most important property here: toggling/adding/clearing
solo never calls setAttribute/removeAttribute on any element and never
invokes the project save path (both asserted directly via spies in
audioSoloSlice.test.ts) — solo cannot reach an export by construction, not
by convention.
Also: extracted useHydrateActiveCompPathFromUrl out of App.tsx (a
pre-existing, unrelated effect) to stay under the 600-line filesize cap
after wiring useAudioSoloBridge in; and fixed a circular dependency the
solo-banner wiring introduced (useAudioSoloBridge.ts now imports
usePlayerStore from its concrete module instead of the player/ barrel,
which re-exports PlayerControls.tsx — the barrel path is what closed the
cycle).
Gates: bun run build clean; packages/core full suite 2379/2379; packages/
studio full suite 4276/4294 (18 pre-existing todo); packages/engine
audioMixer.grouping.test.ts 5/5; oxfmt/oxlint clean on all 23 touched
files; fallow clean (0 new circular deps, 0 new filesize/complexity
findings).
Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
204 lines
7.6 KiB
TypeScript
204 lines
7.6 KiB
TypeScript
import type { RuntimeSeekOptions, RuntimeTimelineMessage, RuntimeTimelineLike } from "./types";
|
|
import type { RuntimeColorGradingApi } from "./colorGrading";
|
|
import type { HyperframePickerApi } from "../inline-scripts/pickerApi";
|
|
import type { PlayerAPI } from "../core.types";
|
|
import type { ClipTree } from "./clipTree";
|
|
|
|
type ThreeClockLike = {
|
|
elapsedTime: number;
|
|
oldTime: number;
|
|
startTime: number;
|
|
getElapsedTime: () => number;
|
|
getDelta: () => number;
|
|
};
|
|
|
|
type ThreeAnimationMixerLike = {
|
|
setTime?: (time: number) => void;
|
|
update: (deltaTime: number) => ThreeAnimationMixerLike;
|
|
};
|
|
|
|
type ThreeLike = {
|
|
Clock?: {
|
|
prototype: ThreeClockLike;
|
|
};
|
|
AnimationMixer?: {
|
|
prototype: ThreeAnimationMixerLike;
|
|
};
|
|
};
|
|
|
|
declare global {
|
|
interface Window {
|
|
__timelines: Record<string, RuntimeTimelineLike>;
|
|
__player?: PlayerAPI;
|
|
__clipManifest?: RuntimeTimelineMessage;
|
|
__clipTree?: ClipTree;
|
|
__hf?: {
|
|
colorGrading?: RuntimeColorGradingApi;
|
|
onSwallowed?: (label: string, err: unknown) => void;
|
|
seek?: (timeSeconds: number, options?: RuntimeSeekOptions) => void;
|
|
duration?: number;
|
|
/**
|
|
* Studio's "Hear only this" push: the full set of soloed clip/group ids,
|
|
* replaced wholesale on every change. Session-only by design — never
|
|
* read from or written to any document attribute.
|
|
*/
|
|
setAudioSolo?: (ids: readonly string[]) => void;
|
|
};
|
|
__playerReady?: boolean;
|
|
__renderReady?: boolean;
|
|
__hfRuntimeTeardown?: (() => void) | null;
|
|
__HF_EXPORT_RENDER_SEEK_CONFIG?: {
|
|
mode?: string;
|
|
diagnostics?: boolean;
|
|
step?: number;
|
|
offsetFraction?: number;
|
|
fps?: number;
|
|
fpsSource?: "render-options" | "default";
|
|
fpsFallbackReason?: "missing" | "invalid";
|
|
owner?: string;
|
|
};
|
|
__HF_PARITY_MODE?: boolean;
|
|
/** Legacy debug-only fps hint. Render-mode runtime fps uses __HF_EXPORT_RENDER_SEEK_CONFIG.fps. */
|
|
__HF_FPS?: number;
|
|
__HF_MAX_DURATION_SEC?: number;
|
|
__hfThreeTime?: number;
|
|
/**
|
|
* Current seek position in seconds, set by the TypeGPU/WebGPU adapter.
|
|
* Poll this from your WebGPU render loop instead of `performance.now()`
|
|
* to get the deterministic seek position.
|
|
*
|
|
* Also listen for the `"hf-seek"` CustomEvent on `window` for an
|
|
* imperative push signal: `window.addEventListener("hf-seek", e => render(e.detail.time))`.
|
|
*/
|
|
__hfTypegpuTime?: number;
|
|
/**
|
|
* Re-render GPU adapters (Three.js / WebGPU) at the given time, bypassing
|
|
* the `"hf-seek"` dedup. Called by the engine after injecting decoded
|
|
* video frames so GPU compositions re-upload their video textures from the
|
|
* freshly-injected `__render_frame__` images. See `forceDispatchSeekEvent`.
|
|
*/
|
|
__hfReseekGpu?: (time: number) => void;
|
|
/**
|
|
* Await GPU work registered synchronously by `hf-seek` listeners through
|
|
* `event.detail.waitUntil(...)`.
|
|
*/
|
|
__hfWaitForSeekCompletion?: () => Promise<void>;
|
|
/**
|
|
* Canonical root-timeline start for a media element. Snapshot capture uses
|
|
* this runtime-owned resolver so reference expressions, authored timing
|
|
* restoration, and arbitrary composition nesting cannot drift.
|
|
*/
|
|
__hfResolveMediaStartSeconds?: (element: Element) => number;
|
|
__HF_PICKER_API?: HyperframePickerApi;
|
|
gsap?: {
|
|
timeline: (params?: { paused?: boolean }) => RuntimeTimelineLike;
|
|
parseEase?: (
|
|
ease: string | ((progress: number) => number),
|
|
...args: unknown[]
|
|
) => ((progress: number) => number) | null;
|
|
registerPlugin?: (plugin: unknown) => void;
|
|
ticker?: {
|
|
tick: () => void;
|
|
};
|
|
};
|
|
THREE?: ThreeLike;
|
|
/**
|
|
* Global Anime.js v4 namespace (set by the UMD or IIFE bundle).
|
|
* Register returned instances on `window.__hfAnime`; v4 has no
|
|
* `anime.running` auto-discovery registry.
|
|
*/
|
|
anime?: {
|
|
animate?: (targets: unknown, params?: unknown) => unknown;
|
|
createTimeline?: (params?: unknown) => unknown;
|
|
/** Legacy v3 registry retained for backward-compatible discovery. */
|
|
running?: unknown[];
|
|
};
|
|
/**
|
|
* anime.js instances registered by compositions.
|
|
* The adapter seeks all instances when the player is seeked.
|
|
*
|
|
* Push your animation or timeline instance here:
|
|
* window.__hfAnime = window.__hfAnime || [];
|
|
* window.__hfAnime.push(anim);
|
|
*/
|
|
__hfAnime?: unknown[];
|
|
/**
|
|
* Global lottie-web instance (set by including the lottie.min.js script).
|
|
* The adapter uses `lottie.getRegisteredAnimations()` for auto-discovery.
|
|
*/
|
|
lottie?: {
|
|
loadAnimation: (params: unknown) => unknown;
|
|
getRegisteredAnimations: () => unknown[];
|
|
};
|
|
/**
|
|
* Lottie animation instances registered by compositions.
|
|
* The adapter seeks all instances when the player is seeked.
|
|
*
|
|
* Push your animation instance here after calling `lottie.loadAnimation()`:
|
|
* window.__hfLottie = window.__hfLottie || [];
|
|
* window.__hfLottie.push(anim);
|
|
*/
|
|
__hfLottie?: unknown[];
|
|
/**
|
|
* Mapbox GL JS map instances. Push your map here after creating it:
|
|
* window.__hfMapbox = window.__hfMapbox || [];
|
|
* window.__hfMapbox.push(map);
|
|
*/
|
|
__hfMapbox?: unknown[];
|
|
/**
|
|
* Leaflet map instances. Push your map here after creating it:
|
|
* window.__hfLeaflet = window.__hfLeaflet || [];
|
|
* window.__hfLeaflet.push(map);
|
|
*/
|
|
__hfLeaflet?: unknown[];
|
|
/**
|
|
* Google Maps instances. Push your map here after creating it:
|
|
* window.__hfGoogleMaps = window.__hfGoogleMaps || [];
|
|
* window.__hfGoogleMaps.push(map);
|
|
*/
|
|
__hfGoogleMaps?: unknown[];
|
|
/**
|
|
* MapLibre GL JS map instances. Push your map here after creating it:
|
|
* window.__hfMaplibre = window.__hfMaplibre || [];
|
|
* window.__hfMaplibre.push(map);
|
|
*/
|
|
__hfMaplibre?: unknown[];
|
|
/**
|
|
* D3 transition instances. Push your transition here after creating it:
|
|
* window.__hfD3 = window.__hfD3 || [];
|
|
* window.__hfD3.push(transition);
|
|
*/
|
|
__hfD3?: unknown[];
|
|
/**
|
|
* Render-time variable overrides injected by the engine when the user
|
|
* passes `hyperframes render --variables '<json>'`. Read indirectly via
|
|
* `window.__hyperframes.getVariables()` (or the named `getVariables`
|
|
* export from `@hyperframes/core`), which merges these over the
|
|
* declared defaults from `<html data-composition-variables="...">`.
|
|
*/
|
|
__hfVariables?: Record<string, unknown>;
|
|
/**
|
|
* Per-instance, pre-merged variables for sub-compositions. Keyed by the
|
|
* sub-composition's `data-composition-id`. Populated by the runtime
|
|
* composition loader at mount time: layers the host element's
|
|
* `data-variable-values` over the sub-comp's declared defaults so the
|
|
* scoped `getVariables()` exposed by `compositionScoping.ts` returns the
|
|
* resolved values for the instance currently executing.
|
|
*/
|
|
__hfVariablesByComp?: Record<string, Record<string, unknown>>;
|
|
/**
|
|
* Set to `true` while the GSAP tween-batching interceptor (injected via
|
|
* HF_EARLY_STUB in fileServer.ts) is still draining queued tween calls
|
|
* through requestAnimationFrame batches. Cleared and the "hf-timelines-built"
|
|
* CustomEvent is dispatched when all queues are empty.
|
|
*
|
|
* init.ts uses this to decide whether to defer `bindRootTimelineIfAvailable`:
|
|
* if true at DOMContentLoaded time, it adds a one-shot event listener and
|
|
* rebinds after the event fires.
|
|
*/
|
|
__hfTimelinesBuilding?: boolean;
|
|
}
|
|
}
|
|
|
|
export {};
|