feat: add CSS Animation Editor tool
Comprehensive visual editor for CSS @keyframe animations: - AnimationSettings: name, duration, delay, easing (incl. cubic-bezier), iteration, direction, fill-mode - KeyframeTimeline: drag-to-reposition keyframe markers, click-track to add, delete selected - KeyframeProperties: per-keyframe transform (translate/rotate/scale/skew), opacity, background-color, border-radius, blur, brightness via sliders - AnimationPreview: live preview on box/circle/text element with play/pause/restart and speed control (0.25×–2×) - PresetLibrary: 22 presets across Entrance/Exit/Attention/Special categories with animated thumbnails - ExportPanel: plain CSS and Tailwind v4 @utility formats with copy and download Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
104
components/animate/AnimationEditor.tsx
Normal file
104
components/animate/AnimationEditor.tsx
Normal file
@@ -0,0 +1,104 @@
|
||||
'use client';
|
||||
|
||||
import { useState, useCallback } from 'react';
|
||||
import { AnimationSettings } from './AnimationSettings';
|
||||
import { AnimationPreview } from './AnimationPreview';
|
||||
import { KeyframeTimeline } from './KeyframeTimeline';
|
||||
import { KeyframeProperties } from './KeyframeProperties';
|
||||
import { PresetLibrary } from './PresetLibrary';
|
||||
import { ExportPanel } from './ExportPanel';
|
||||
import { DEFAULT_CONFIG, newKeyframe } from '@/lib/animate/defaults';
|
||||
import type { AnimationConfig, KeyframeProperties as KFProps, PreviewElement } from '@/types/animate';
|
||||
|
||||
export function AnimationEditor() {
|
||||
const [config, setConfig] = useState<AnimationConfig>(DEFAULT_CONFIG);
|
||||
const [selectedId, setSelectedId] = useState<string | null>(
|
||||
DEFAULT_CONFIG.keyframes[DEFAULT_CONFIG.keyframes.length - 1].id
|
||||
);
|
||||
const [previewElement, setPreviewElement] = useState<PreviewElement>('box');
|
||||
|
||||
const selectedKeyframe = config.keyframes.find((k) => k.id === selectedId) ?? null;
|
||||
|
||||
const updateKeyframeProps = useCallback((id: string, props: KFProps) => {
|
||||
setConfig((c) => ({
|
||||
...c,
|
||||
keyframes: c.keyframes.map((k) => k.id === id ? { ...k, properties: props } : k),
|
||||
}));
|
||||
}, []);
|
||||
|
||||
const addKeyframe = useCallback((offset: number) => {
|
||||
const kf = newKeyframe(offset);
|
||||
setConfig((c) => ({ ...c, keyframes: [...c.keyframes, kf] }));
|
||||
setSelectedId(kf.id);
|
||||
}, []);
|
||||
|
||||
const deleteKeyframe = useCallback((id: string) => {
|
||||
setConfig((c) => {
|
||||
if (c.keyframes.length <= 2) return c;
|
||||
const next = c.keyframes.filter((k) => k.id !== id);
|
||||
return { ...c, keyframes: next };
|
||||
});
|
||||
setSelectedId((prev) => {
|
||||
if (prev !== id) return prev;
|
||||
const remaining = config.keyframes.filter((k) => k.id !== id);
|
||||
return remaining[remaining.length - 1]?.id ?? null;
|
||||
});
|
||||
}, [config.keyframes]);
|
||||
|
||||
const moveKeyframe = useCallback((id: string, newOffset: number) => {
|
||||
const clamped = Math.min(100, Math.max(0, Math.round(newOffset)));
|
||||
setConfig((c) => ({
|
||||
...c,
|
||||
keyframes: c.keyframes.map((k) => k.id === id ? { ...k, offset: clamped } : k),
|
||||
}));
|
||||
}, []);
|
||||
|
||||
const loadPreset = useCallback((presetConfig: AnimationConfig) => {
|
||||
setConfig(presetConfig);
|
||||
setSelectedId(presetConfig.keyframes[presetConfig.keyframes.length - 1].id);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
{/* Row 1: Settings + Preview */}
|
||||
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6 items-stretch">
|
||||
<div className="lg:col-span-1">
|
||||
<AnimationSettings config={config} onChange={setConfig} />
|
||||
</div>
|
||||
<div className="lg:col-span-2">
|
||||
<AnimationPreview
|
||||
config={config}
|
||||
element={previewElement}
|
||||
onElementChange={setPreviewElement}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Row 2: Keyframe Timeline */}
|
||||
<KeyframeTimeline
|
||||
keyframes={config.keyframes}
|
||||
selectedId={selectedId}
|
||||
onSelect={setSelectedId}
|
||||
onAdd={addKeyframe}
|
||||
onDelete={deleteKeyframe}
|
||||
onMove={moveKeyframe}
|
||||
/>
|
||||
|
||||
{/* Row 3: Keyframe Properties + Export */}
|
||||
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6 items-stretch">
|
||||
<div className="lg:col-span-1">
|
||||
<KeyframeProperties
|
||||
keyframe={selectedKeyframe}
|
||||
onChange={updateKeyframeProps}
|
||||
/>
|
||||
</div>
|
||||
<div className="lg:col-span-2">
|
||||
<ExportPanel config={config} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Row 4: Preset Library */}
|
||||
<PresetLibrary currentName={config.name} onSelect={loadPreset} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user