diff options
| author | Maze Winther <mazewinther@gmail.com> | 2026-04-06 12:40:47 +0200 |
|---|---|---|
| committer | Maze Winther <mazewinther@gmail.com> | 2026-04-06 12:40:47 +0200 |
| commit | 964ac82d5e1ab2774e917338f7cbcbd11c323e94 (patch) | |
| tree | 4d39e3edfc86ddba746d3e64ce544868d013bbea /apps/web/src/components/editor | |
| parent | 05243616eaf597c8e81b9401ba09b425928ebf95 (diff) | |
| download | opencut-964ac82d5e1ab2774e917338f7cbcbd11c323e94.tar.gz | |
refactor: split presets into data definitions and storage store
Diffstat (limited to 'apps/web/src/components/editor')
5 files changed, 129 insertions, 103 deletions
diff --git a/apps/web/src/components/editor/panels/timeline/graph-editor/custom-presets-store.ts b/apps/web/src/components/editor/panels/timeline/graph-editor/custom-presets-store.ts new file mode 100644 index 00000000..54722ec2 --- /dev/null +++ b/apps/web/src/components/editor/panels/timeline/graph-editor/custom-presets-store.ts @@ -0,0 +1,101 @@ +"use client"; + +import { useSyncExternalStore } from "react"; +import { generateUUID } from "@/utils/id"; +import type { NormalizedCubicBezier } from "@/lib/animation/types"; +import type { EasingPreset } from "./easing-presets"; + +const STORAGE_KEY = "opencut:graph-editor-presets"; + +let cachedPresets: EasingPreset[] | null = null; +const listeners = new Set<() => void>(); + +function isValidPresetArray(value: unknown): value is EasingPreset[] { + return ( + Array.isArray(value) && + value.every( + (item) => + typeof item === "object" && + item !== null && + typeof item.id === "string" && + typeof item.label === "string" && + Array.isArray(item.value) && + item.value.length === 4 && + item.value.every((number: unknown) => typeof number === "number"), + ) + ); +} + +function readFromStorage(): EasingPreset[] { + try { + const raw = localStorage.getItem(STORAGE_KEY); + if (!raw) return []; + const parsed: unknown = JSON.parse(raw); + return isValidPresetArray(parsed) ? parsed : []; + } catch { + // Silently recover — corrupted localStorage shouldn't crash the editor + return []; + } +} + +function writeToStorage({ presets }: { presets: EasingPreset[] }): void { + localStorage.setItem(STORAGE_KEY, JSON.stringify(presets)); +} + +function getSnapshot(): EasingPreset[] { + cachedPresets ??= readFromStorage(); + return cachedPresets; +} + +function getServerSnapshot(): EasingPreset[] { + return []; +} + +function notify(): void { + cachedPresets = null; + for (const listener of listeners) { + listener(); + } +} + +function onStorageChange(event: StorageEvent): void { + if (event.key === STORAGE_KEY) notify(); +} + +function subscribe(listener: () => void): () => void { + if (listeners.size === 0 && typeof window !== "undefined") { + window.addEventListener("storage", onStorageChange); + } + listeners.add(listener); + return () => { + listeners.delete(listener); + if (listeners.size === 0 && typeof window !== "undefined") { + window.removeEventListener("storage", onStorageChange); + } + }; +} + +export function useCustomPresets(): EasingPreset[] { + return useSyncExternalStore(subscribe, getSnapshot, getServerSnapshot); +} + +export function savePreset({ value }: { value: NormalizedCubicBezier }): void { + const current = getSnapshot(); + writeToStorage({ + presets: [ + ...current, + { + id: generateUUID(), + label: `Custom ${current.length + 1}`, + value, + isCustom: true, + }, + ], + }); + notify(); +} + +export function removePreset({ id }: { id: string }): void { + writeToStorage({ presets: getSnapshot().filter((preset) => preset.id !== id) }); + notify(); +} diff --git a/apps/web/src/components/editor/panels/timeline/graph-editor/easing-presets.ts b/apps/web/src/components/editor/panels/timeline/graph-editor/easing-presets.ts new file mode 100644 index 00000000..6ccc9071 --- /dev/null +++ b/apps/web/src/components/editor/panels/timeline/graph-editor/easing-presets.ts @@ -0,0 +1,19 @@ +import type { NormalizedCubicBezier } from "@/lib/animation/types"; + +export const PRESET_MATCH_TOLERANCE = 0.02; + +export interface EasingPreset { + id: string; + label: string; + value: NormalizedCubicBezier; + isCustom?: boolean; +} + +export const BUILTIN_PRESETS: EasingPreset[] = [ + { id: "smooth", label: "Smooth", value: [0.25, 0.1, 0.25, 1] }, + { id: "ease-out", label: "Ease out", value: [0, 0, 0.2, 1] }, + { id: "ease-in", label: "Ease in", value: [0.8, 0, 1, 1] }, + { id: "ease-in-out", label: "In out", value: [0.4, 0, 0.2, 1] }, + { id: "pop", label: "Pop", value: [0.175, 0.885, 0.32, 1.275] }, + { id: "linear", label: "Linear", value: [0, 0, 1, 1] }, +]; diff --git a/apps/web/src/components/editor/panels/timeline/graph-editor/popover.tsx b/apps/web/src/components/editor/panels/timeline/graph-editor/popover.tsx index 86665392..00348a60 100644 --- a/apps/web/src/components/editor/panels/timeline/graph-editor/popover.tsx +++ b/apps/web/src/components/editor/panels/timeline/graph-editor/popover.tsx @@ -17,11 +17,9 @@ import type { GraphEditorComponentOption } from "./session"; import { BUILTIN_PRESETS, PRESET_MATCH_TOLERANCE, - removePreset, - savePreset, type EasingPreset, - useCustomPresets, -} from "./presets"; +} from "./easing-presets"; +import { removePreset, savePreset, useCustomPresets } from "./custom-presets-store"; import { BezierGraph, BEZIER_GRAPH_MIN_HEIGHT } from "./bezier-graph"; const COLLAPSED_MAX = 6; @@ -160,12 +158,12 @@ export function GraphEditorPopover({ isActive={activePresetId === preset.id} disabled={!canEdit} onSelect={() => onCommitValue?.(preset.value)} - onDelete={() => removePreset(preset.id)} + onDelete={() => removePreset({ id: preset.id })} /> ))} <button type="button" - onClick={() => value && savePreset(value)} + onClick={() => value && savePreset({ value })} disabled={!canEdit} className={cn( "text-muted-foreground flex flex-col items-center justify-center gap-1 rounded-sm px-1 py-1", diff --git a/apps/web/src/components/editor/panels/timeline/graph-editor/presets.ts b/apps/web/src/components/editor/panels/timeline/graph-editor/presets.ts deleted file mode 100644 index cbcdf566..00000000 --- a/apps/web/src/components/editor/panels/timeline/graph-editor/presets.ts +++ /dev/null @@ -1,90 +0,0 @@ -"use client"; - -import { useSyncExternalStore } from "react"; -import type { NormalizedCubicBezier } from "@/lib/animation/types"; - -const STORAGE_KEY = "opencut:graph-editor-presets"; -export const PRESET_MATCH_TOLERANCE = 0.02; - -export interface EasingPreset { - id: string; - label: string; - value: NormalizedCubicBezier; - isCustom?: boolean; -} - -export const BUILTIN_PRESETS: EasingPreset[] = [ - { id: "smooth", label: "Smooth", value: [0.25, 0.1, 0.25, 1] }, - { id: "ease-out", label: "Ease out", value: [0, 0, 0.2, 1] }, - { id: "ease-in", label: "Ease in", value: [0.8, 0, 1, 1] }, - { id: "ease-in-out", label: "In out", value: [0.4, 0, 0.2, 1] }, - { id: "pop", label: "Pop", value: [0.175, 0.885, 0.32, 1.275] }, - { id: "linear", label: "Linear", value: [0, 0, 1, 1] }, -]; - -let cachedPresets: EasingPreset[] | null = null; -const listeners = new Set<() => void>(); - -function readFromStorage(): EasingPreset[] { - try { - const raw = localStorage.getItem(STORAGE_KEY); - // JSON.parse can throw if the stored value is corrupted - return raw ? (JSON.parse(raw) as EasingPreset[]) : []; - } catch { - return []; - } -} - -function writeToStorage(presets: EasingPreset[]): void { - localStorage.setItem(STORAGE_KEY, JSON.stringify(presets)); -} - -function getSnapshot(): EasingPreset[] { - cachedPresets ??= readFromStorage(); - return cachedPresets; -} - -function getServerSnapshot(): EasingPreset[] { - return []; -} - -function notify(): void { - cachedPresets = null; - for (const listener of listeners) { - listener(); - } -} - -function subscribe(listener: () => void): () => void { - listeners.add(listener); - return () => listeners.delete(listener); -} - -if (typeof window !== "undefined") { - window.addEventListener("storage", (event) => { - if (event.key === STORAGE_KEY) notify(); - }); -} - -export function useCustomPresets(): EasingPreset[] { - return useSyncExternalStore(subscribe, getSnapshot, getServerSnapshot); -} - -export function savePreset(value: NormalizedCubicBezier): void { - const current = getSnapshot(); - writeToStorage([ - ...current, - { - id: `custom-${Date.now()}`, - label: `Custom ${current.length + 1}`, - value, - isCustom: true, - }, - ]); - notify(); -} - -export function removePreset(id: string): void { - writeToStorage(getSnapshot().filter((preset) => preset.id !== id)); - notify(); -} diff --git a/apps/web/src/components/editor/panels/timeline/graph-editor/session.ts b/apps/web/src/components/editor/panels/timeline/graph-editor/session.ts index 148fa7b4..9c81215d 100644 --- a/apps/web/src/components/editor/panels/timeline/graph-editor/session.ts +++ b/apps/web/src/components/editor/panels/timeline/graph-editor/session.ts @@ -1,5 +1,3 @@ -"use client"; - import { getCurveHandlesForNormalizedCubicBezier, getEditableScalarChannels, @@ -22,7 +20,7 @@ const FLAT_VALUE_EPSILON = 1e-6; const LINEAR_CURVE_EPSILON = 1e-6; export type GraphEditorUnavailableReason = - | "no-keyframe-selected" + | "no-keyframe-selected" | "multiple-keyframes-selected" | "selected-element-missing" | "selected-element-has-no-animations" @@ -201,11 +199,11 @@ export function resolveGraphEditorSelectionState({ } if (selectedKeyframes.length === 2) { - const [kf1, kf2] = selectedKeyframes; + const [firstKeyframe, secondKeyframe] = selectedKeyframes; if ( - kf1.trackId !== kf2.trackId || - kf1.elementId !== kf2.elementId || - kf1.propertyPath !== kf2.propertyPath + firstKeyframe.trackId !== secondKeyframe.trackId || + firstKeyframe.elementId !== secondKeyframe.elementId || + firstKeyframe.propertyPath !== secondKeyframe.propertyPath ) { return createUnavailableState({ reason: "multiple-keyframes-selected", |
