217 lines
7.4 KiB
TypeScript
217 lines
7.4 KiB
TypeScript
'use client';
|
|
|
|
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
|
|
import { Input } from '@/components/ui/input';
|
|
import { Label } from '@/components/ui/label';
|
|
import {
|
|
Select,
|
|
SelectContent,
|
|
SelectItem,
|
|
SelectTrigger,
|
|
SelectValue,
|
|
} from '@/components/ui/select';
|
|
import { Button } from '@/components/ui/button';
|
|
import { Infinity } from 'lucide-react';
|
|
import type { AnimationConfig } from '@/types/animate';
|
|
|
|
interface Props {
|
|
config: AnimationConfig;
|
|
onChange: (config: AnimationConfig) => void;
|
|
}
|
|
|
|
const EASINGS = [
|
|
{ value: 'linear', label: 'Linear' },
|
|
{ value: 'ease', label: 'Ease' },
|
|
{ value: 'ease-in', label: 'Ease In' },
|
|
{ value: 'ease-out', label: 'Ease Out' },
|
|
{ value: 'ease-in-out', label: 'Ease In Out' },
|
|
{ value: 'cubic-bezier', label: 'Cubic Bézier' },
|
|
{ value: 'steps(4, end)', label: 'Steps (4)' },
|
|
{ value: 'steps(8, end)', label: 'Steps (8)' },
|
|
];
|
|
|
|
export function AnimationSettings({ config, onChange }: Props) {
|
|
const set = <K extends keyof AnimationConfig>(key: K, value: AnimationConfig[K]) =>
|
|
onChange({ ...config, [key]: value });
|
|
|
|
const isInfinite = config.iterationCount === 'infinite';
|
|
const isCubic = config.easing === 'cubic-bezier';
|
|
|
|
// Parse cubic-bezier values from string like "cubic-bezier(x1,y1,x2,y2)"
|
|
const cubicValues = (() => {
|
|
const m = config.easing.match(/cubic-bezier\(([^)]+)\)/);
|
|
if (!m) return [0.25, 0.1, 0.25, 1.0];
|
|
return m[1].split(',').map(Number);
|
|
})();
|
|
|
|
const setCubic = (index: number, val: number) => {
|
|
const v = [...cubicValues];
|
|
v[index] = val;
|
|
set('easing', `cubic-bezier(${v.join(',')})`);
|
|
};
|
|
|
|
return (
|
|
<Card className="h-full">
|
|
<CardHeader>
|
|
<CardTitle>Settings</CardTitle>
|
|
</CardHeader>
|
|
<CardContent className="space-y-4">
|
|
{/* Name */}
|
|
<div className="space-y-1.5">
|
|
<Label className="text-xs">Name</Label>
|
|
<Input
|
|
value={config.name}
|
|
onChange={(e) => {
|
|
const val = e.target.value.replace(/\s+/g, '-').replace(/[^a-zA-Z0-9-_]/g, '');
|
|
set('name', val || 'myAnimation');
|
|
}}
|
|
className="font-mono text-xs"
|
|
/>
|
|
</div>
|
|
|
|
{/* Duration + Delay */}
|
|
<div className="grid grid-cols-2 gap-3">
|
|
<div className="space-y-1.5">
|
|
<Label className="text-xs">Duration</Label>
|
|
<div className="flex items-center gap-1">
|
|
<Input
|
|
type="number"
|
|
min={50}
|
|
max={10000}
|
|
step={50}
|
|
value={config.duration}
|
|
onChange={(e) => set('duration', Math.max(50, Number(e.target.value)))}
|
|
|
|
/>
|
|
<span className="text-xs text-muted-foreground shrink-0">ms</span>
|
|
</div>
|
|
</div>
|
|
<div className="space-y-1.5">
|
|
<Label className="text-xs">Delay</Label>
|
|
<div className="flex items-center gap-1">
|
|
<Input
|
|
type="number"
|
|
min={0}
|
|
max={5000}
|
|
step={50}
|
|
value={config.delay}
|
|
onChange={(e) => set('delay', Math.max(0, Number(e.target.value)))}
|
|
|
|
/>
|
|
<span className="text-xs text-muted-foreground shrink-0">ms</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Easing */}
|
|
<div className="space-y-1.5">
|
|
<Label className="text-xs">Easing</Label>
|
|
<Select
|
|
value={isCubic ? 'cubic-bezier' : config.easing}
|
|
onValueChange={(v) => {
|
|
if (v === 'cubic-bezier') {
|
|
set('easing', 'cubic-bezier(0.25,0.1,0.25,1)');
|
|
} else {
|
|
set('easing', v);
|
|
}
|
|
}}
|
|
>
|
|
<SelectTrigger className="w-full">
|
|
<SelectValue />
|
|
</SelectTrigger>
|
|
<SelectContent>
|
|
{EASINGS.map((e) => (
|
|
<SelectItem key={e.value} value={e.value}>
|
|
{e.label}
|
|
</SelectItem>
|
|
))}
|
|
</SelectContent>
|
|
</Select>
|
|
</div>
|
|
|
|
{/* Cubic-bezier inputs */}
|
|
{isCubic && (
|
|
<div className="space-y-1.5">
|
|
<Label className="text-xs text-muted-foreground">cubic-bezier(P1x, P1y, P2x, P2y)</Label>
|
|
<div className="grid grid-cols-4 gap-1.5">
|
|
{(['P1x', 'P1y', 'P2x', 'P2y'] as const).map((label, i) => (
|
|
<div key={label} className="space-y-0.5">
|
|
<Label className="text-[10px] text-muted-foreground">{label}</Label>
|
|
<Input
|
|
type="number"
|
|
min={i % 2 === 0 ? 0 : -1}
|
|
max={i % 2 === 0 ? 1 : 2}
|
|
step={0.01}
|
|
value={cubicValues[i] ?? 0}
|
|
onChange={(e) => setCubic(i, Number(e.target.value))}
|
|
className="text-xs px-1.5"
|
|
/>
|
|
</div>
|
|
))}
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{/* Iteration */}
|
|
<div className="space-y-1.5">
|
|
<Label className="text-xs">Iterations</Label>
|
|
<div className="flex items-center gap-2">
|
|
<Input
|
|
type="number"
|
|
min={1}
|
|
max={999}
|
|
value={isInfinite ? '' : config.iterationCount}
|
|
disabled={isInfinite}
|
|
onChange={(e) => set('iterationCount', Math.max(1, Number(e.target.value)))}
|
|
className="text-xs flex-1"
|
|
placeholder="1"
|
|
/>
|
|
<Button
|
|
size="icon-xs"
|
|
variant={isInfinite ? 'default' : 'outline'}
|
|
onClick={() =>
|
|
set('iterationCount', isInfinite ? 1 : 'infinite')
|
|
}
|
|
title="Toggle infinite"
|
|
>
|
|
<Infinity className="h-3 w-3" />
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Direction */}
|
|
<div className="space-y-1.5">
|
|
<Label className="text-xs">Direction</Label>
|
|
<Select value={config.direction} onValueChange={(v) => set('direction', v as AnimationConfig['direction'])}>
|
|
<SelectTrigger className="w-full">
|
|
<SelectValue />
|
|
</SelectTrigger>
|
|
<SelectContent>
|
|
<SelectItem value="normal">Normal</SelectItem>
|
|
<SelectItem value="reverse">Reverse</SelectItem>
|
|
<SelectItem value="alternate">Alternate</SelectItem>
|
|
<SelectItem value="alternate-reverse">Alternate Reverse</SelectItem>
|
|
</SelectContent>
|
|
</Select>
|
|
</div>
|
|
|
|
{/* Fill Mode */}
|
|
<div className="space-y-1.5">
|
|
<Label className="text-xs">Fill Mode</Label>
|
|
<Select value={config.fillMode} onValueChange={(v) => set('fillMode', v as AnimationConfig['fillMode'])}>
|
|
<SelectTrigger className="w-full">
|
|
<SelectValue />
|
|
</SelectTrigger>
|
|
<SelectContent>
|
|
<SelectItem value="none">None</SelectItem>
|
|
<SelectItem value="forwards">Forwards</SelectItem>
|
|
<SelectItem value="backwards">Backwards</SelectItem>
|
|
<SelectItem value="both">Both</SelectItem>
|
|
</SelectContent>
|
|
</Select>
|
|
</div>
|
|
</CardContent>
|
|
</Card>
|
|
);
|
|
}
|