Files
hyperframes/packages/core/src/runtime/window.d.ts
T
Vance IngallsandClaude Sonnet 5 0d26072e6c feat(studio,core): mute groups, and hear-only-this that cannot reach the export (#3291)
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>
2026-08-21 11:19:53 -07:00

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 {};