mirror of
https://github.com/heygen-com/hyperframes.git
synced 2026-11-14 13:27:13 +00:00
* feat(core,studio): the character presets pitch shift unlocks Chipmunk, Giant, and Monster ship as presets on the pitchshift worklet P1 added: Chipmunk pitches up and adds sparkle, Giant pitches down with weight and a compressor to hold the extra low end together, Monster pitches down further with saturation growl and a close, tight reverb. Every param verified against the live effect registry rather than sketched — the compressor/reverb/saturate/shelf keys all match exactly. Each gets its own title treatment (font, size, tracking, hue) so the FX rack's per-preset styling coverage and hue-distance/background-uniqueness tests extend cleanly to the three new entries, and complaint-line copy in the non-voice vocabulary the audit test enforces (no speech words — "Giant" over CapCut's "Deep Voice", as the design doc records). Updates plans/audio-fx-presets.md's two limits paragraphs to record that pitch shift landed and this half of the character list now ships; Robot and Alien stay out of scope (ring modulation, still unbuilt). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> * feat(core): the audio group model — element, membership, helpers Introduces <hf-audio-group> and data-audio-group as the group model B2–B7 and C1 build on: a non-rendering group element carries a label and (later) an FX chain, membership lives on the member's own data-audio-group attribute rather than DOM nesting, so a track removed from the document simply drops out of the group on the next resolve — nothing dangles. Groups do not nest: data-audio-group on the group element itself is ignored. A group with members but no <hf-audio-group> element still resolves, label falling back to the id, so hand-authored HTML degrades gracefully. Audio only in v1 — video members are ignored. Parse-only: nothing routes or sums audio yet (B3/B4). Adds the audio-groups canary at percentage: 0 gating the future Studio UI; the element and attribute parse and play regardless of enrollment. Verified rather than assumed per this plan's standing rule: the timeline's clip-collection selector ([data-start], [data-track-index], [data-composition-id], video, audio, img) already excludes the group element with zero changes, and no lint rule flags unknown elements or data-* attributes, so neither needed touching — confirmed by grep and by running `hyperframes lint` against a fixture containing the element (0 findings referencing it). The step doc's suggested display:none injection point (an existing base stylesheet in the runtime) does not exist in this codebase; skipped rather than inventing new infrastructure, since an empty, childless custom element already renders as a zero-size inline box with no visible output — the same reasoning the lint check above confirms empirically. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> * feat(studio): group rows in the timeline, and a split disclosure A group renders as its own row with member rows beneath it, and disclosure splits into two independent controls: caret shows/hides a group's member rows (structural), `∿` shows/hides any row's automation-lane rows. Plain tracks lose their caret (nothing to disclose structurally) and keep only `∿`. `expandedClipIds` keeps its existing keyframe-lane-state job; `expandedGroupIds`/`expandedLaneOwnerIds` are new, independent sets. Groups get a real position in the row/geometry pipeline rather than a visual-only overlay: `useTimelineTrackDerivations` re-emits a group's member tracks contiguously under a synthetic fractional anchor key (firstMember - 0.5, the same fractional-key convention sub-composition expansion already uses), so `rowGeometry`/keyboard-nav/virtualization treat a group row as a first-class row without widening their key type away from number. `TimelineLogicalRow.level` widens `1 | 2` to `1 | 2 | 3` (group / member-under-group / lane), lanes always `owner.level + 1`. All of it — grouped row emission, the header, the new expansion state — is gated behind `isCanaryEnabled("audio-groups")`; disabled, `groups` resolves empty and every new code path no-ops. `TimelineElement.audioGroup` (+ `audioGroupLabel`, resolved once per document via `resolveAudioGroups` from B1) is parsed unconditionally, mirroring how `hidden`/`fxChain` already flow DOM → manifest → TimelineElement — inert without the canary. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
82 lines
3.2 KiB
TypeScript
82 lines
3.2 KiB
TypeScript
import { TRACK_H } from "./timelineLayout";
|
|
import { TrackClipCount } from "./TrackClipCount";
|
|
|
|
// Layer row (Figma order: disclosure ∿, diamond, name) — the disclosure lives
|
|
// here, not on the clip bar, and re-expands a collapsed layer. `∿` (not a
|
|
// caret) because a group's own row keeps the caret for its structural
|
|
// disclosure (member rows) — this button only ever means "show this row's
|
|
// lanes", so it needs its own distinct glyph.
|
|
export function LayerDisclosureRow({
|
|
name,
|
|
clipCount,
|
|
isExpanded,
|
|
gutterBackground,
|
|
columnWidth,
|
|
lanesId,
|
|
onToggleClipExpanded,
|
|
children,
|
|
}: {
|
|
/** What this row is called. The active clip's own name when it is alone on the
|
|
* track; the track itself once it holds several, since naming a shared row
|
|
* after one of its clips reads as if the rows under it were that clip's. */
|
|
name: string;
|
|
clipCount: number;
|
|
isExpanded: boolean;
|
|
gutterBackground: string;
|
|
/** Same adaptive width the lane rows use: a narrowed header column must not
|
|
* leave this row hanging over the clips it labels. */
|
|
columnWidth: number;
|
|
/** Id of the CANVAS-side element holding the diamond lanes this row's caret
|
|
* expands (see TimelinePropertyLanes). The caret also reveals the per-lane
|
|
* control rows in this column, but the diamonds are what following the
|
|
* reference should land on. */
|
|
lanesId: string;
|
|
onToggleClipExpanded: () => void;
|
|
/** Trailing controls that act on the LAYER (the visibility eye), not on a lane. */
|
|
children?: React.ReactNode;
|
|
}) {
|
|
return (
|
|
<div
|
|
className="absolute left-0 top-0 flex items-center gap-1.5 overflow-hidden px-1.5 text-[11px]"
|
|
style={{
|
|
width: columnWidth,
|
|
height: TRACK_H,
|
|
color: "#ffffff",
|
|
background: gutterBackground,
|
|
}}
|
|
>
|
|
<button
|
|
type="button"
|
|
// ponytail: No focus id here; keyboard routing belongs to the enclosing logical row.
|
|
tabIndex={-1}
|
|
aria-expanded={isExpanded}
|
|
aria-controls={lanesId}
|
|
aria-label={`${isExpanded ? "Hide" : "Show"} ${name} lanes`}
|
|
title={`${isExpanded ? "Hide" : "Show"} lanes`}
|
|
// h-6 w-6 = the 24x24 WCAG 2.2 minimum target. The glyph stays 11px;
|
|
// only the hit box grows.
|
|
className={`flex h-6 w-6 shrink-0 items-center justify-center rounded border-0 bg-transparent p-0 text-[11px] leading-none focus-visible:outline focus-visible:outline-1 focus-visible:outline-[#3CE6AC] ${
|
|
isExpanded ? "text-[#3CE6AC]" : "text-white/55 hover:text-white"
|
|
}`}
|
|
onPointerDown={(event) => event.stopPropagation()}
|
|
onClick={(event) => {
|
|
event.stopPropagation();
|
|
onToggleClipExpanded();
|
|
}}
|
|
>
|
|
<span aria-hidden="true">∿</span>
|
|
</button>
|
|
{/* Decorative: the disclosure button above already names the row's keyframe
|
|
state, and aria-label on a plain span is not exposed reliably anyway. */}
|
|
<span aria-hidden="true" className="shrink-0 text-[13px] leading-none text-white/40">
|
|
◇
|
|
</span>
|
|
<span className="min-w-0 flex-1 truncate font-medium" title={name}>
|
|
{name}
|
|
</span>
|
|
<TrackClipCount clipCount={clipCount} />
|
|
{children}
|
|
</div>
|
|
);
|
|
}
|