diff options
Diffstat (limited to 'apps/web/src/components/editor/panels/properties/hooks')
4 files changed, 358 insertions, 10 deletions
diff --git a/apps/web/src/components/editor/panels/properties/hooks/use-keyframed-color-property.ts b/apps/web/src/components/editor/panels/properties/hooks/use-keyframed-color-property.ts index dbd19e4c..957ee1c9 100644 --- a/apps/web/src/components/editor/panels/properties/hooks/use-keyframed-color-property.ts +++ b/apps/web/src/components/editor/panels/properties/hooks/use-keyframed-color-property.ts @@ -4,8 +4,8 @@ import { hasKeyframesForPath, upsertElementKeyframe, } from "@/lib/animation"; -import type { AnimationPropertyPath, ElementAnimations } from "@/types/animation"; -import type { TimelineElement } from "@/types/timeline"; +import type { AnimationPropertyPath, ElementAnimations } from "@/lib/animation/types"; +import type { TimelineElement } from "@/lib/timeline"; export function useKeyframedColorProperty({ trackId, diff --git a/apps/web/src/components/editor/panels/properties/hooks/use-keyframed-number-property.ts b/apps/web/src/components/editor/panels/properties/hooks/use-keyframed-number-property.ts index 11aa049c..0f0168f8 100644 --- a/apps/web/src/components/editor/panels/properties/hooks/use-keyframed-number-property.ts +++ b/apps/web/src/components/editor/panels/properties/hooks/use-keyframed-number-property.ts @@ -4,8 +4,9 @@ import { hasKeyframesForPath, upsertElementKeyframe, } from "@/lib/animation"; -import type { AnimationPropertyPath, ElementAnimations } from "@/types/animation"; -import type { TimelineElement } from "@/types/timeline"; +import type { AnimationPropertyPath, ElementAnimations } from "@/lib/animation/types"; +import type { TimelineElement } from "@/lib/timeline"; +import { snapToStep } from "@/utils/math"; import { usePropertyDraft } from "./use-property-draft"; export function useKeyframedNumberProperty({ @@ -18,6 +19,7 @@ export function useKeyframedNumberProperty({ displayValue, parse, valueAtPlayhead, + step, buildBaseUpdates, }: { trackId: string; @@ -29,9 +31,12 @@ export function useKeyframedNumberProperty({ displayValue: string; parse: (input: string) => number | null; valueAtPlayhead: number; + step?: number; buildBaseUpdates: ({ value }: { value: number }) => Partial<TimelineElement>; }) { const editor = useEditor(); + const snapValue = (value: number) => + step != null ? snapToStep({ value, step }) : value; const hasAnimatedKeyframes = hasKeyframesForPath({ animations, propertyPath }); const keyframeAtTime = isPlayheadWithinElementRange @@ -43,6 +48,7 @@ export function useKeyframedNumberProperty({ hasAnimatedKeyframes && isPlayheadWithinElementRange; const previewValue = ({ value }: { value: number }) => { + const nextValue = snapValue(value); if (shouldUseAnimatedChannel) { editor.timeline.previewElements({ updates: [ @@ -54,7 +60,7 @@ export function useKeyframedNumberProperty({ animations, propertyPath, time: localTime, - value, + value: nextValue, }), }, }, @@ -68,7 +74,7 @@ export function useKeyframedNumberProperty({ { trackId, elementId, - updates: buildBaseUpdates({ value }), + updates: buildBaseUpdates({ value: nextValue }), }, ], }); @@ -76,7 +82,10 @@ export function useKeyframedNumberProperty({ const propertyDraft = usePropertyDraft({ displayValue, - parse, + parse: (input) => { + const parsedValue = parse(input); + return parsedValue === null ? null : snapValue(parsedValue); + }, onPreview: (value) => previewValue({ value }), onCommit: () => editor.timeline.commitPreview(), }); @@ -107,13 +116,14 @@ export function useKeyframedNumberProperty({ elementId, propertyPath, time: localTime, - value: valueAtPlayhead, + value: snapValue(valueAtPlayhead), }, ], }); }; const commitValue = ({ value }: { value: number }) => { + const nextValue = snapValue(value); if (shouldUseAnimatedChannel) { editor.timeline.upsertKeyframes({ keyframes: [ @@ -122,7 +132,7 @@ export function useKeyframedNumberProperty({ elementId, propertyPath, time: localTime, - value, + value: nextValue, }, ], }); @@ -134,7 +144,7 @@ export function useKeyframedNumberProperty({ { trackId, elementId, - updates: buildBaseUpdates({ value }), + updates: buildBaseUpdates({ value: nextValue }), }, ], }); diff --git a/apps/web/src/components/editor/panels/properties/hooks/use-keyframed-param-property.ts b/apps/web/src/components/editor/panels/properties/hooks/use-keyframed-param-property.ts new file mode 100644 index 00000000..335e3fb6 --- /dev/null +++ b/apps/web/src/components/editor/panels/properties/hooks/use-keyframed-param-property.ts @@ -0,0 +1,148 @@ +"use client"; + +import { useEditor } from "@/hooks/use-editor"; +import { + buildGraphicParamPath, + getKeyframeAtTime, + getParamDefaultInterpolation, + getParamValueKind, + hasKeyframesForPath, + upsertPathKeyframe, +} from "@/lib/animation"; +import type { + ElementAnimations, +} from "@/lib/animation/types"; +import type { ParamDefinition } from "@/lib/params"; +import type { TimelineElement } from "@/lib/timeline"; + +export interface KeyframedParamPropertyResult { + hasAnimatedKeyframes: boolean; + isKeyframedAtTime: boolean; + keyframeIdAtTime: string | null; + onPreview: (value: number | string | boolean) => void; + onCommit: () => void; + toggleKeyframe: () => void; +} + +export function useKeyframedParamProperty({ + param, + trackId, + elementId, + animations, + localTime, + isPlayheadWithinElementRange, + resolvedValue, + buildBaseUpdates, +}: { + param: ParamDefinition; + trackId: string; + elementId: string; + animations: ElementAnimations | undefined; + localTime: number; + isPlayheadWithinElementRange: boolean; + resolvedValue: number | string | boolean; + buildBaseUpdates: ({ + value, + }: { + value: number | string | boolean; + }) => Partial<TimelineElement>; +}): KeyframedParamPropertyResult { + const editor = useEditor(); + const propertyPath = buildGraphicParamPath({ paramKey: param.key }); + const hasAnimatedKeyframes = hasKeyframesForPath({ + animations, + propertyPath, + }); + const keyframeAtTime = isPlayheadWithinElementRange + ? getKeyframeAtTime({ + animations, + propertyPath, + time: localTime, + }) + : null; + const keyframeIdAtTime = keyframeAtTime?.id ?? null; + const isKeyframedAtTime = keyframeAtTime !== null; + const shouldUseAnimatedChannel = + hasAnimatedKeyframes && isPlayheadWithinElementRange; + + const previewValue: KeyframedParamPropertyResult["onPreview"] = (value) => { + if (shouldUseAnimatedChannel) { + editor.timeline.previewElements({ + updates: [ + { + trackId, + elementId, + updates: { + animations: upsertPathKeyframe({ + animations, + propertyPath, + time: localTime, + value, + valueKind: getParamValueKind({ param }), + defaultInterpolation: getParamDefaultInterpolation({ + param, + }), + numericRange: + param.type === "number" + ? { min: param.min, max: param.max, step: param.step } + : undefined, + }), + }, + }, + ], + }); + return; + } + + editor.timeline.previewElements({ + updates: [ + { + trackId, + elementId, + updates: buildBaseUpdates({ value }), + }, + ], + }); + }; + + const toggleKeyframe = () => { + if (!isPlayheadWithinElementRange) { + return; + } + + if (keyframeIdAtTime) { + editor.timeline.removeKeyframes({ + keyframes: [ + { + trackId, + elementId, + propertyPath, + keyframeId: keyframeIdAtTime, + }, + ], + }); + return; + } + + editor.timeline.upsertKeyframes({ + keyframes: [ + { + trackId, + elementId, + propertyPath, + time: localTime, + value: resolvedValue, + }, + ], + }); + }; + + return { + hasAnimatedKeyframes, + isKeyframedAtTime, + keyframeIdAtTime, + onPreview: previewValue, + onCommit: () => editor.timeline.commitPreview(), + toggleKeyframe, + }; +} diff --git a/apps/web/src/components/editor/panels/properties/hooks/use-keyframed-vector-property.ts b/apps/web/src/components/editor/panels/properties/hooks/use-keyframed-vector-property.ts new file mode 100644 index 00000000..188f05cb --- /dev/null +++ b/apps/web/src/components/editor/panels/properties/hooks/use-keyframed-vector-property.ts @@ -0,0 +1,190 @@ +import { useEditor } from "@/hooks/use-editor"; +import { + getKeyframeAtTime, + hasKeyframesForPath, + upsertElementKeyframe, +} from "@/lib/animation"; +import type { + AnimationPropertyPath, + ElementAnimations, + VectorValue, +} from "@/lib/animation/types"; +import type { TimelineElement } from "@/lib/timeline"; +import { snapToStep } from "@/utils/math"; +import { usePropertyDraft } from "./use-property-draft"; + +export function useKeyframedVectorProperty({ + trackId, + elementId, + animations, + propertyPath, + localTime, + isPlayheadWithinElementRange, + resolvedValue, + displayX, + displayY, + parseComponent, + step, + buildBaseUpdates, +}: { + trackId: string; + elementId: string; + animations: ElementAnimations | undefined; + propertyPath: AnimationPropertyPath; + localTime: number; + isPlayheadWithinElementRange: boolean; + resolvedValue: VectorValue; + displayX: string; + displayY: string; + parseComponent: (input: string) => number | null; + step?: number; + buildBaseUpdates: ({ + value, + }: { + value: VectorValue; + }) => Partial<TimelineElement>; +}) { + const editor = useEditor(); + const snapComponentValue = (value: number) => + step != null ? snapToStep({ value, step }) : value; + + const hasAnimatedKeyframes = hasKeyframesForPath({ + animations, + propertyPath, + }); + const keyframeAtTime = isPlayheadWithinElementRange + ? getKeyframeAtTime({ animations, propertyPath, time: localTime }) + : null; + const keyframeIdAtTime = keyframeAtTime?.id ?? null; + const isKeyframedAtTime = keyframeAtTime !== null; + const shouldUseAnimatedChannel = + hasAnimatedKeyframes && isPlayheadWithinElementRange; + + const previewVector = ({ value }: { value: VectorValue }) => { + const nextValue = { + x: snapComponentValue(value.x), + y: snapComponentValue(value.y), + }; + if (shouldUseAnimatedChannel) { + editor.timeline.previewElements({ + updates: [ + { + trackId, + elementId, + updates: { + animations: upsertElementKeyframe({ + animations, + propertyPath, + time: localTime, + value: nextValue, + }), + }, + }, + ], + }); + return; + } + + editor.timeline.previewElements({ + updates: [{ trackId, elementId, updates: buildBaseUpdates({ value: nextValue }) }], + }); + }; + + const x = usePropertyDraft({ + displayValue: displayX, + parse: (input) => { + const parsedValue = parseComponent(input); + return parsedValue === null ? null : snapComponentValue(parsedValue); + }, + onPreview: (xVal) => + previewVector({ value: { x: xVal, y: resolvedValue.y } }), + onCommit: () => editor.timeline.commitPreview(), + }); + + const y = usePropertyDraft({ + displayValue: displayY, + parse: (input) => { + const parsedValue = parseComponent(input); + return parsedValue === null ? null : snapComponentValue(parsedValue); + }, + onPreview: (yVal) => + previewVector({ value: { x: resolvedValue.x, y: yVal } }), + onCommit: () => editor.timeline.commitPreview(), + }); + + const toggleKeyframe = () => { + if (!isPlayheadWithinElementRange) return; + + if (keyframeIdAtTime) { + editor.timeline.removeKeyframes({ + keyframes: [ + { trackId, elementId, propertyPath, keyframeId: keyframeIdAtTime }, + ], + }); + return; + } + + editor.timeline.upsertKeyframes({ + keyframes: [ + { + trackId, + elementId, + propertyPath, + time: localTime, + value: resolvedValue, + }, + ], + }); + }; + + const commitX = ({ value }: { value: number }) => { + const vector: VectorValue = { + x: snapComponentValue(value), + y: snapComponentValue(resolvedValue.y), + }; + if (shouldUseAnimatedChannel) { + editor.timeline.upsertKeyframes({ + keyframes: [ + { trackId, elementId, propertyPath, time: localTime, value: vector }, + ], + }); + return; + } + editor.timeline.updateElements({ + updates: [ + { trackId, elementId, updates: buildBaseUpdates({ value: vector }) }, + ], + }); + }; + + const commitY = ({ value }: { value: number }) => { + const vector: VectorValue = { + x: snapComponentValue(resolvedValue.x), + y: snapComponentValue(value), + }; + if (shouldUseAnimatedChannel) { + editor.timeline.upsertKeyframes({ + keyframes: [ + { trackId, elementId, propertyPath, time: localTime, value: vector }, + ], + }); + return; + } + editor.timeline.updateElements({ + updates: [ + { trackId, elementId, updates: buildBaseUpdates({ value: vector }) }, + ], + }); + }; + + return { + x, + y, + hasAnimatedKeyframes, + isKeyframedAtTime, + keyframeIdAtTime, + toggleKeyframe, + commitX, + commitY, + }; +} |
