diff --git a/apps/web/src/components/editor/panels/properties/components/property-param-field.tsx b/apps/web/src/components/editor/panels/properties/components/property-param-field.tsx index e69c6918..9cc2b02e 100644 --- a/apps/web/src/components/editor/panels/properties/components/property-param-field.tsx +++ b/apps/web/src/components/editor/panels/properties/components/property-param-field.tsx @@ -1,7 +1,11 @@ "use client"; import type { ParamDefinition, NumberParamDefinition } from "@/lib/params"; -import { formatNumberForDisplay, snapToStep } from "@/utils/math"; +import { + formatNumberForDisplay, + getFractionDigitsForStep, + snapToStep, +} from "@/utils/math"; import { SectionField } from "@/components/section"; import { NumberField } from "@/components/ui/number-field"; import { Switch } from "@/components/ui/switch"; @@ -156,7 +160,7 @@ function NumberParamField({ onPreview(clamped / displayMultiplier); }; - const maxFractionDigits = step >= 1 ? 0 : Math.round(-Math.log10(step)); + const maxFractionDigits = getFractionDigitsForStep({ step }); const draft = usePropertyDraft({ displayValue: formatNumberForDisplay({ diff --git a/apps/web/src/components/editor/panels/properties/tabs/audio-tab.tsx b/apps/web/src/components/editor/panels/properties/tabs/audio-tab.tsx index a92f7be2..4dbc1a17 100644 --- a/apps/web/src/components/editor/panels/properties/tabs/audio-tab.tsx +++ b/apps/web/src/components/editor/panels/properties/tabs/audio-tab.tsx @@ -1,7 +1,13 @@ import { NumberField } from "@/components/ui/number-field"; import { VOLUME_DB_MAX, VOLUME_DB_MIN } from "@/lib/timeline/audio-constants"; import { DEFAULTS } from "@/lib/timeline/defaults"; -import { clamp, isNearlyEqual } from "@/utils/math"; +import { + clamp, + formatNumberForDisplay, + getFractionDigitsForStep, + isNearlyEqual, + snapToStep, +} from "@/utils/math"; import type { AudioElement, VideoElement } from "@/lib/timeline"; import { resolveNumberAtTime } from "@/lib/animation"; import { useElementPlayhead } from "../hooks/use-element-playhead"; @@ -18,6 +24,9 @@ import { SectionTitle, } from "@/components/section"; +const VOLUME_STEP = 0.1; +const VOLUME_FRACTION_DIGITS = getFractionDigitsForStep({ step: VOLUME_STEP }); + export function AudioTab({ element, trackId, @@ -42,17 +51,24 @@ export function AudioTab({ propertyPath: "volume", localTime, isPlayheadWithinElementRange, - displayValue: resolvedVolume.toFixed(1), + displayValue: formatNumberForDisplay({ + value: resolvedVolume, + fractionDigits: VOLUME_FRACTION_DIGITS, + }), parse: (input) => { const parsed = parseFloat(input); if (Number.isNaN(parsed)) { return null; } - return clamp({ value: parsed, min: VOLUME_DB_MIN, max: VOLUME_DB_MAX }); + return clamp({ + value: snapToStep({ value: parsed, step: VOLUME_STEP }), + min: VOLUME_DB_MIN, + max: VOLUME_DB_MAX, + }); }, valueAtPlayhead: resolvedVolume, - step: 0.1, + step: VOLUME_STEP, buildBaseUpdates: ({ value }) => ({ volume: value, }), diff --git a/apps/web/src/components/editor/panels/properties/tabs/masks-tab.tsx b/apps/web/src/components/editor/panels/properties/tabs/masks-tab.tsx index 6a5d0d0b..db8bd728 100644 --- a/apps/web/src/components/editor/panels/properties/tabs/masks-tab.tsx +++ b/apps/web/src/components/editor/panels/properties/tabs/masks-tab.tsx @@ -38,7 +38,12 @@ import { TooltipContent, TooltipTrigger, } from "@/components/ui/tooltip"; -import { clamp, formatNumberForDisplay, snapToStep } from "@/utils/math"; +import { + clamp, + formatNumberForDisplay, + getFractionDigitsForStep, + snapToStep, +} from "@/utils/math"; import { Section, SectionContent, @@ -599,7 +604,7 @@ function MaskNumberField({ const max = isPercent ? 100 : param.max; const step = isPercent ? 1 : param.step; const displayValue = value * displayMultiplier; - const maxFractionDigits = step >= 1 ? 0 : Math.round(-Math.log10(step)); + const maxFractionDigits = getFractionDigitsForStep({ step }); const clampDisplay = (nextDisplayValue: number) => max !== undefined diff --git a/apps/web/src/components/editor/panels/properties/tabs/speed-tab.tsx b/apps/web/src/components/editor/panels/properties/tabs/speed-tab.tsx index 4434890e..da86c626 100644 --- a/apps/web/src/components/editor/panels/properties/tabs/speed-tab.tsx +++ b/apps/web/src/components/editor/panels/properties/tabs/speed-tab.tsx @@ -22,16 +22,28 @@ import { SectionTitle, } from "@/components/section"; import { usePropertyDraft } from "../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 rate.toFixed(2); + 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; - const rounded = Math.round(parsed * 100) / 100; - return clampRetimeRate({ rate: rounded }); + return clampRetimeRate({ + rate: snapToStep({ value: parsed, step: SPEED_STEP }), + }); } function buildRetime({ @@ -56,24 +68,21 @@ export function SpeedTab({ const rate = clampRetimeRate({ rate: element.retime?.rate ?? DEFAULT_RETIME_RATE, }); - const maintainPitchAvailable = canMaintainPitch({ rate }); + const isPitchPreserveAvailable = canMaintainPitch({ rate }); const maintainPitch = element.retime?.maintainPitch ?? false; const pendingRateRef = useRef(rate); - const applyRetime = ({ + const commitRetime = ({ rate: nextRate, maintainPitch: nextMaintainPitch, - pushHistory = true, }: { rate: number; maintainPitch: boolean; - pushHistory?: boolean; }) => { editor.timeline.updateElementRetime({ trackId, elementId: element.id, retime: buildRetime({ rate: nextRate, maintainPitch: nextMaintainPitch }), - pushHistory, }); }; @@ -82,10 +91,20 @@ export function SpeedTab({ parse: (input) => parseSpeedInput({ input }), onPreview: (nextRate) => { pendingRateRef.current = nextRate; - applyRetime({ rate: nextRate, maintainPitch, pushHistory: false }); + editor.timeline.previewElements({ + updates: [ + { + trackId, + elementId: element.id, + updates: { + retime: buildRetime({ rate: nextRate, maintainPitch }), + }, + }, + ], + }); }, onCommit: () => { - applyRetime({ rate: pendingRateRef.current, maintainPitch }); + commitRetime({ rate: pendingRateRef.current, maintainPitch }); }, }); @@ -115,20 +134,20 @@ export function SpeedTab({ onScrub={speedDraft.scrubTo} onScrubEnd={speedDraft.commitScrub} onReset={() => - applyRetime({ rate: DEFAULT_RETIME_RATE, maintainPitch }) + commitRetime({ rate: DEFAULT_RETIME_RATE, maintainPitch }) } isDefault={rate === DEFAULT_RETIME_RATE} />