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>
2026-02-28 14:17:04 +01:00
|
|
|
'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 */}
|
2026-02-28 17:08:22 +01:00
|
|
|
<PresetLibrary onSelect={loadPreset} />
|
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>
2026-02-28 14:17:04 +01:00
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
}
|