From 3e89d2998588f8e35c524e523ba2ee853f0f6475 Mon Sep 17 00:00:00 2001 From: Maze Winther Date: Mon, 20 Apr 2026 11:31:17 +0200 Subject: refactor: restructure files to their domains, new preview overlay system, and dep graph --- apps/web/src/speed/components/speed-tab.tsx | 156 ++++++++++++++++++++++++++++ 1 file changed, 156 insertions(+) create mode 100644 apps/web/src/speed/components/speed-tab.tsx (limited to 'apps/web/src/speed/components') diff --git a/apps/web/src/speed/components/speed-tab.tsx b/apps/web/src/speed/components/speed-tab.tsx new file mode 100644 index 00000000..2f9231d6 --- /dev/null +++ b/apps/web/src/speed/components/speed-tab.tsx @@ -0,0 +1,156 @@ +import { useRef } from "react"; +import { useEditor } from "@/editor/use-editor"; +import { NumberField } from "@/components/ui/number-field"; +import { Switch } from "@/components/ui/switch"; +import { HugeiconsIcon } from "@hugeicons/react"; +import { DashboardSpeed02Icon } from "@hugeicons/core-free-icons"; +import { buildConstantRetime } from "@/retime"; +import { + DEFAULT_RETIME_RATE, + MIN_RETIME_RATE, + MAX_RETIME_RATE, + clampRetimeRate, + canMaintainPitch, +} from "@/retime/rate"; +import type { AudioElement, VideoElement } from "@/timeline"; +import { + Section, + SectionContent, + SectionField, + SectionFields, + SectionHeader, + SectionTitle, +} from "@/components/section"; +import { usePropertyDraft } from "@/components/editor/panels/properties/hooks/use-property-draft"; +import { + formatNumberForDisplay, + getFractionDigitsForStep, + snapToStep, +} from "@/utils/math"; + +const SPEED_STEP = 0.01; +const SPEED_FRACTION_DIGITS = getFractionDigitsForStep({ step: SPEED_STEP }); + +function rateToDisplay({ rate }: { rate: number }): string { + return formatNumberForDisplay({ + value: rate, + fractionDigits: SPEED_FRACTION_DIGITS, + }); +} + +function parseSpeedInput({ input }: { input: string }): number | null { + const parsed = parseFloat(input); + if (Number.isNaN(parsed)) return null; + return clampRetimeRate({ + rate: snapToStep({ value: parsed, step: SPEED_STEP }), + }); +} + +function buildRetime({ + rate, + maintainPitch, +}: { + rate: number; + maintainPitch: boolean; +}) { + if (rate === DEFAULT_RETIME_RATE && !maintainPitch) return undefined; + return buildConstantRetime({ rate, maintainPitch }); +} + +export function SpeedTab({ + element, + trackId, +}: { + element: AudioElement | VideoElement; + trackId: string; +}) { + const editor = useEditor(); + const rate = clampRetimeRate({ + rate: element.retime?.rate ?? DEFAULT_RETIME_RATE, + }); + const isPitchPreserveAvailable = canMaintainPitch({ rate }); + const maintainPitch = element.retime?.maintainPitch ?? false; + const pendingRateRef = useRef(rate); + + const commitRetime = ({ + rate: nextRate, + maintainPitch: nextMaintainPitch, + }: { + rate: number; + maintainPitch: boolean; + }) => { + editor.timeline.updateElementRetime({ + trackId, + elementId: element.id, + retime: buildRetime({ rate: nextRate, maintainPitch: nextMaintainPitch }), + }); + }; + + const speedDraft = usePropertyDraft({ + displayValue: rateToDisplay({ rate }), + parse: (input) => parseSpeedInput({ input }), + onPreview: (nextRate) => { + pendingRateRef.current = nextRate; + editor.timeline.previewElements({ + updates: [ + { + trackId, + elementId: element.id, + updates: { + retime: buildRetime({ rate: nextRate, maintainPitch }), + }, + }, + ], + }); + }, + onCommit: () => { + commitRetime({ rate: pendingRateRef.current, maintainPitch }); + }, + }); + + return ( +
+ + Speed + + + + + } + value={speedDraft.displayValue} + suffix="x" + scrubRanges={[ + { from: 0.01, to: 1, pixelsPerUnit: 160 }, + { from: 1, to: 5, pixelsPerUnit: 48 }, + ]} + scrubClamp={{ min: MIN_RETIME_RATE, max: MAX_RETIME_RATE }} + onFocus={() => { + pendingRateRef.current = rate; + speedDraft.onFocus(); + }} + onChange={speedDraft.onChange} + onBlur={speedDraft.onBlur} + onScrub={speedDraft.scrubTo} + onScrubEnd={speedDraft.commitScrub} + onReset={() => + commitRetime({ rate: DEFAULT_RETIME_RATE, maintainPitch }) + } + isDefault={rate === DEFAULT_RETIME_RATE} + /> + +
+ Change pitch + + commitRetime({ rate, maintainPitch: !checked }) + } + /> +
+
+
+
+ ); +} -- cgit v1.3.1