diff --git a/src/lib/ai/mutations.ts b/src/lib/ai/mutations.ts
index a249b65..7c71a80 100644
--- a/src/lib/ai/mutations.ts
+++ b/src/lib/ai/mutations.ts
@@ -131,6 +131,26 @@ export function mutateCreateLayer(
layer.contentOffset = input.contentOffset;
}
+ // Set enter/exit transitions if provided
+ if (input.enterTransition) {
+ const preset = getPresetById(input.enterTransition.presetId);
+ if (preset) {
+ layer.enterTransition = {
+ presetId: input.enterTransition.presetId,
+ duration: input.enterTransition.duration
+ };
+ }
+ }
+ if (input.exitTransition) {
+ const preset = getPresetById(input.exitTransition.presetId);
+ if (preset) {
+ layer.exitTransition = {
+ presetId: input.exitTransition.presetId,
+ duration: input.exitTransition.duration
+ };
+ }
+ }
+
// Mutate project
ctx.project.layers.push(layer);
@@ -252,6 +272,34 @@ export function mutateEditLayer(ctx: MutationContext, input: EditLayerInput): Ed
layer.contentOffset = Math.max(0, input.contentOffset);
}
+ // Update enter/exit transitions
+ if (input.enterTransition !== undefined) {
+ if (input.enterTransition) {
+ const preset = getPresetById(input.enterTransition.presetId);
+ if (preset) {
+ layer.enterTransition = {
+ presetId: input.enterTransition.presetId,
+ duration: input.enterTransition.duration
+ };
+ }
+ } else {
+ layer.enterTransition = undefined;
+ }
+ }
+ if (input.exitTransition !== undefined) {
+ if (input.exitTransition) {
+ const preset = getPresetById(input.exitTransition.presetId);
+ if (preset) {
+ layer.exitTransition = {
+ presetId: input.exitTransition.presetId,
+ duration: input.exitTransition.duration
+ };
+ }
+ } else {
+ layer.exitTransition = undefined;
+ }
+ }
+
return {
success: true,
layerId: resolvedId,
diff --git a/src/lib/ai/schemas.ts b/src/lib/ai/schemas.ts
index 21bd425..8ea98f7 100644
--- a/src/lib/ai/schemas.ts
+++ b/src/lib/ai/schemas.ts
@@ -45,6 +45,19 @@ const TimingFieldsSchema = z.object({
.describe('Start offset for trimming media content (seconds)')
});
+// ============================================
+// Layer Transition (enter/exit preset)
+// ============================================
+
+const LayerTransitionFieldSchema = z
+ .object({
+ presetId: AnimationPresetIdSchema.describe('Animation preset ID to apply as transition'),
+ duration: z.number().positive().describe('Transition duration in seconds (typically 0.3-0.8)')
+ })
+ .describe(
+ 'Automatic animation applied at layer enter/exit. Position presets are relative offsets from the layer base position. Scale/opacity are factors (0→1 means invisible→visible).'
+ );
+
// ============================================
// Layer Type + Props Union
// ============================================
@@ -123,9 +136,17 @@ export const CreateLayerInputSchema = z
// Layer type and properties
layer: LayerTypePropsUnion,
- // Animation (preset OR custom keyframes)
+ // Transitions (automatic enter/exit animations, no keyframes created)
+ enterTransition: LayerTransitionFieldSchema.optional().describe(
+ 'Auto-play animation when layer enters (e.g., fade-in, slide-in-left). Applied as offset/factor on base transform.'
+ ),
+ exitTransition: LayerTransitionFieldSchema.optional().describe(
+ 'Auto-play animation when layer exits (e.g., fade-out, slide-out-right). Applied as offset/factor on base transform.'
+ ),
+
+ // Animation (preset OR custom keyframes - baked as keyframes)
animation: CreateLayerAnimationSchema.optional().describe(
- 'Animation: preset OR custom keyframes'
+ 'Animation as keyframes: preset (baked at startTime) OR custom keyframes'
)
})
.refine(
@@ -199,7 +220,15 @@ export const EditLayerInputSchema = z
.describe('Layer-specific properties to update (merged with existing)'),
// Timing fields
- ...TimingFieldsSchema.shape
+ ...TimingFieldsSchema.shape,
+
+ // Transitions (automatic enter/exit animations)
+ enterTransition: LayerTransitionFieldSchema.optional().describe(
+ 'Set enter transition preset. Omit to keep existing, set to null to remove.'
+ ),
+ exitTransition: LayerTransitionFieldSchema.optional().describe(
+ 'Set exit transition preset. Omit to keep existing, set to null to remove.'
+ )
})
.refine(
(data) => {
@@ -295,11 +324,12 @@ export const animationTools = {
- Position, rotation, scale, anchor point
- Layer-specific props (text content, colors, sizes, etc.)
- Style (opacity, blur, filters, drop shadow)
-- Animation via preset OR custom keyframes
+- Enter/exit transitions (automatic preset animations at layer boundaries)
+- Animation via preset (baked as keyframes) OR custom keyframes
- Timing (enter/exit times, content duration/offset)
-Example: Create a text layer with animation:
-{ "layer": { "type": "text", "props": { "content": "Hello World", "fontSize": 48, "fill": "#ffffff" } }, "transform": { "position": { "x": 0, "y": -200 } }, "animation": { "preset": { "id": "fade-in", "startTime": 0, "duration": 0.5 } } }`,
+Example with enter transition:
+{ "layer": { "type": "text", "props": { "content": "Hello", "fontSize": 48, "fill": "#fff" } }, "transform": { "position": { "x": 0, "y": -200 } }, "enterTransition": { "presetId": "fade-in", "duration": 0.5 }, "exitTransition": { "presetId": "fade-out", "duration": 0.3 } }`,
inputSchema: CreateLayerInputSchema
}),
@@ -308,6 +338,7 @@ Example: Create a text layer with animation:
- Provide only the fields you want to change
- transform/style sections replace entire object if provided
- props are merged with existing props
+- enterTransition/exitTransition: set automatic enter/exit animations
- Use layer ID from create_layer response or layer name`,
inputSchema: EditLayerInputSchema
}),
diff --git a/src/lib/ai/system-prompt.ts b/src/lib/ai/system-prompt.ts
index 8e0eb37..dc7587a 100644
--- a/src/lib/ai/system-prompt.ts
+++ b/src/lib/ai/system-prompt.ts
@@ -3,9 +3,11 @@
*
* Tools: create_layer, edit_layer, remove_layer, configure_project, group_layers, ungroup_layers
* Animation: can use preset AND/OR custom keyframes in create_layer.
+ * Transitions: can set enterTransition/exitTransition on layers via edit_layer.
*/
import type { Project } from '$lib/types/animation';
import { projectDataSchema } from '$lib/schemas/animation';
+import { getPresetsSummaryForAI } from '$lib/engine/presets';
import goodExampleRaw from '$lib/good-example.json';
const exampleProject = projectDataSchema.parse(goodExampleRaw);
@@ -29,12 +31,47 @@ Steps: present plan (if full video) → execute tool calls → conclude with a f
## Tools
-- **create_layer**: Layer type + props + transform + style + animation + timing
-- **edit_layer**: Modify existing layer (provide fields to change)
+- **create_layer**: Layer type + props + transform + style + animation + timing + enterTransition/exitTransition
+- **edit_layer**: Modify existing layer (provide fields to change, including enterTransition/exitTransition)
- **remove_layer**: Delete layer by ID or name
- **configure_project**: Project settings (name, dimensions, duration, background)
- **group_layers** / **ungroup_layers**: Group/ungroup layers
+## Animation System
+
+Two complementary ways to animate layers:
+
+### 1. Transitions (enterTransition / exitTransition)
+Automatic enter/exit animations applied at runtime (no keyframes created).
+Set via create_layer or edit_layer. Position presets are relative offsets from the layer's base position.
+Scale and opacity presets are applied as factors (e.g., scale 0→1 means from invisible to full size).
+When the transition finishes, the layer returns to its base values.
+
+\`\`\`json
+{ "enterTransition": { "presetId": "fade-in", "duration": 0.5 } }
+{ "exitTransition": { "presetId": "slide-out-left", "duration": 0.3 } }
+\`\`\`
+
+### 2. Keyframe Presets (animation.preset in create_layer)
+Bakes preset keyframes onto the layer at a specific time. Good for emphasis effects or custom timing.
+Position values are added as offsets to the layer's current base position.
+
+\`\`\`json
+{ "animation": { "preset": { "id": "pulse", "startTime": 1.0, "duration": 0.5 } } }
+\`\`\`
+
+### 3. Custom Keyframes (animation.keyframes in create_layer)
+Full control over individual property animations. Can be combined with presets.
+
+### When to use which:
+- **Transitions**: Best for entrance/exit effects. No keyframes cluttering the timeline. Automatically tied to layer enter/exit time.
+- **Keyframe presets**: Best for emphasis effects (pulse, shake, bounce) at specific moments.
+- **Custom keyframes**: Best for unique, hand-crafted animations.
+
+## Available Presets
+
+${getPresetsSummaryForAI()}
+
## Graphic Style - MANDATORY DIRECTIVES
### Backgrounds
@@ -106,8 +143,8 @@ ${JSON.stringify(exampleProject)}
When the user gives no specific animation directions, apply at minimum:
-- **Entrances/Exits**: Fade in from opacity:0 + slight scale (0.95→1) + blur (filter.blur 8→0). Add a gentle slide (20-40px) from a direction. Reverse for exits.
-- **Text layers**: At minimum, appear from opacity:0 with a subtle slide-up on Y (~20px). Titles can add scale+blur for more impact.
+- **Entrances/Exits**: Use enterTransition/exitTransition with appropriate presets (fade-in, slide-in-*, scale-in for entrances; fade-out, slide-out-*, scale-out for exits). Default duration 0.3-0.6s.
+- **Text layers**: At minimum, use enterTransition with fade-in or slide-in-bottom (duration 0.4s). Titles can use pop or bounce-in for more impact.
- **Flat backgrounds**: If a background feels too plain, add 1-2 decorative circle shapes (≈200x200, high blur ≈150, low opacity ≈0.15-0.25) as soft ambient blobs with gentle position drift.
These are sensible defaults — override freely when the user provides specific creative direction.
diff --git a/src/lib/components/ai/model-selector.svelte b/src/lib/components/ai/model-selector.svelte
index 8db0d44..e366f1d 100644
--- a/src/lib/components/ai/model-selector.svelte
+++ b/src/lib/components/ai/model-selector.svelte
@@ -1,5 +1,5 @@
+
+
= { + time: number; + property: P; + value: P extends TransformProperty + ? number + : P extends + | 'opacity' + | 'blur' + | 'brightness' + | 'contrast' + | 'saturate' + | 'dropShadowX' + | 'dropShadowY' + | 'dropShadowBlur' + ? number + : P extends 'dropShadowColor' + ? string + : never; + interpolation?: Interpolation; +}; + +/** + * Type-safe animation preset with category and description + */ +export interface TypedAnimationPreset { + id: string; + name: string; + description: string; + category: PresetCategory; + keyframes: PresetKeyframe[]; +} + +/** + * Helper to create type-safe preset keyframes + */ +function kf
( + time: number, + property: P, + value: PresetKeyframe
['value'], + interpolation?: Interpolation +): PresetKeyframe
{ + return { time, property, value, interpolation } as PresetKeyframe
;
+}
+
+/**
+ * Helper to create a type-safe preset
+ */
+function preset(
+ id: string,
+ name: string,
+ description: string,
+ category: PresetCategory,
+ keyframes: PresetKeyframe[]
+): TypedAnimationPreset {
+ return { id, name, description, category, keyframes };
+}
/**
* All available animation presets
* Keyframe times are normalized (0-1) and will be scaled by duration when applied
*/
-export const animationPresets: AnimationPreset[] = [
+export const animationPresets: TypedAnimationPreset[] = [
// ============================================
// Fade animations
// ============================================
- {
- id: 'fade-in',
- name: 'Fade In',
- keyframes: [
- {
- time: 0,
- property: 'opacity',
- value: 0,
- interpolation: { family: 'continuous', strategy: 'ease-out' }
- },
- {
- time: 1,
- property: 'opacity',
- value: 1,
- interpolation: { family: 'continuous', strategy: 'linear' }
- }
- ]
- },
- {
- id: 'fade-out',
- name: 'Fade Out',
- keyframes: [
- {
- time: 0,
- property: 'opacity',
- value: 1,
- interpolation: { family: 'continuous', strategy: 'ease-in' }
- },
- {
- time: 1,
- property: 'opacity',
- value: 0,
- interpolation: { family: 'continuous', strategy: 'linear' }
- }
- ]
- },
+ preset('fade-in', 'Fade In', 'Gradually appears from transparent to fully visible', 'enter', [
+ kf(0, 'opacity', 0, { family: 'continuous', strategy: 'ease-out' }),
+ kf(1, 'opacity', 1, { family: 'continuous', strategy: 'linear' })
+ ]),
+ preset('fade-out', 'Fade Out', 'Gradually disappears from visible to transparent', 'exit', [
+ kf(0, 'opacity', 1, { family: 'continuous', strategy: 'ease-in' }),
+ kf(1, 'opacity', 0, { family: 'continuous', strategy: 'linear' })
+ ]),
// ============================================
// Slide animations
// ============================================
- {
- id: 'slide-in-left',
- name: 'Slide In from Left',
- keyframes: [
- {
- time: 0,
- property: 'position.x',
- value: -500,
- interpolation: { family: 'continuous', strategy: 'ease-out' }
- },
- {
- time: 0,
- property: 'opacity',
- value: 0,
- interpolation: { family: 'continuous', strategy: 'ease-out' }
- },
- {
- time: 1,
- property: 'position.x',
- value: 0,
- interpolation: { family: 'continuous', strategy: 'linear' }
- },
- {
- time: 1,
- property: 'opacity',
- value: 1,
- interpolation: { family: 'continuous', strategy: 'linear' }
- }
+ preset(
+ 'slide-in-left',
+ 'Slide In from Left',
+ 'Slides in from 500px left with fade, ending at original position',
+ 'enter',
+ [
+ kf(0, 'position.x', -500, { family: 'continuous', strategy: 'ease-out' }),
+ kf(0, 'opacity', 0, { family: 'continuous', strategy: 'ease-out' }),
+ kf(1, 'position.x', 0, { family: 'continuous', strategy: 'linear' }),
+ kf(1, 'opacity', 1, { family: 'continuous', strategy: 'linear' })
]
- },
- {
- id: 'slide-in-right',
- name: 'Slide In from Right',
- keyframes: [
- {
- time: 0,
- property: 'position.x',
- value: 500,
- interpolation: { family: 'continuous', strategy: 'ease-out' }
- },
- {
- time: 0,
- property: 'opacity',
- value: 0,
- interpolation: { family: 'continuous', strategy: 'ease-out' }
- },
- {
- time: 1,
- property: 'position.x',
- value: 0,
- interpolation: { family: 'continuous', strategy: 'linear' }
- },
- {
- time: 1,
- property: 'opacity',
- value: 1,
- interpolation: { family: 'continuous', strategy: 'linear' }
- }
+ ),
+ preset(
+ 'slide-in-right',
+ 'Slide In from Right',
+ 'Slides in from 500px right with fade, ending at original position',
+ 'enter',
+ [
+ kf(0, 'position.x', 500, { family: 'continuous', strategy: 'ease-out' }),
+ kf(0, 'opacity', 0, { family: 'continuous', strategy: 'ease-out' }),
+ kf(1, 'position.x', 0, { family: 'continuous', strategy: 'linear' }),
+ kf(1, 'opacity', 1, { family: 'continuous', strategy: 'linear' })
]
- },
- {
- id: 'slide-in-top',
- name: 'Slide In from Top',
- keyframes: [
- {
- time: 0,
- property: 'position.y',
- value: -400,
- interpolation: { family: 'continuous', strategy: 'ease-out' }
- },
- {
- time: 0,
- property: 'opacity',
- value: 0,
- interpolation: { family: 'continuous', strategy: 'ease-out' }
- },
- {
- time: 1,
- property: 'position.y',
- value: 0,
- interpolation: { family: 'continuous', strategy: 'linear' }
- },
- {
- time: 1,
- property: 'opacity',
- value: 1,
- interpolation: { family: 'continuous', strategy: 'linear' }
- }
+ ),
+ preset(
+ 'slide-in-top',
+ 'Slide In from Top',
+ 'Slides in from 400px above with fade, ending at original position',
+ 'enter',
+ [
+ kf(0, 'position.y', -400, { family: 'continuous', strategy: 'ease-out' }),
+ kf(0, 'opacity', 0, { family: 'continuous', strategy: 'ease-out' }),
+ kf(1, 'position.y', 0, { family: 'continuous', strategy: 'linear' }),
+ kf(1, 'opacity', 1, { family: 'continuous', strategy: 'linear' })
]
- },
- {
- id: 'slide-in-bottom',
- name: 'Slide In from Bottom',
- keyframes: [
- {
- time: 0,
- property: 'position.y',
- value: 400,
- interpolation: { family: 'continuous', strategy: 'ease-out' }
- },
- {
- time: 0,
- property: 'opacity',
- value: 0,
- interpolation: { family: 'continuous', strategy: 'ease-out' }
- },
- {
- time: 1,
- property: 'position.y',
- value: 0,
- interpolation: { family: 'continuous', strategy: 'linear' }
- },
- {
- time: 1,
- property: 'opacity',
- value: 1,
- interpolation: { family: 'continuous', strategy: 'linear' }
- }
+ ),
+ preset(
+ 'slide-in-bottom',
+ 'Slide In from Bottom',
+ 'Slides in from 400px below with fade, ending at original position',
+ 'enter',
+ [
+ kf(0, 'position.y', 400, { family: 'continuous', strategy: 'ease-out' }),
+ kf(0, 'opacity', 0, { family: 'continuous', strategy: 'ease-out' }),
+ kf(1, 'position.y', 0, { family: 'continuous', strategy: 'linear' }),
+ kf(1, 'opacity', 1, { family: 'continuous', strategy: 'linear' })
]
- },
- {
- id: 'slide-out-left',
- name: 'Slide Out to Left',
- keyframes: [
- {
- time: 0,
- property: 'position.x',
- value: 0,
- interpolation: { family: 'continuous', strategy: 'ease-in' }
- },
- {
- time: 0,
- property: 'opacity',
- value: 1,
- interpolation: { family: 'continuous', strategy: 'ease-in' }
- },
- {
- time: 1,
- property: 'position.x',
- value: -500,
- interpolation: { family: 'continuous', strategy: 'linear' }
- },
- {
- time: 1,
- property: 'opacity',
- value: 0,
- interpolation: { family: 'continuous', strategy: 'linear' }
- }
+ ),
+ preset(
+ 'slide-out-left',
+ 'Slide Out to Left',
+ 'Slides out 500px to the left with fade, from original position',
+ 'exit',
+ [
+ kf(0, 'position.x', 0, { family: 'continuous', strategy: 'ease-in' }),
+ kf(0, 'opacity', 1, { family: 'continuous', strategy: 'ease-in' }),
+ kf(1, 'position.x', -500, { family: 'continuous', strategy: 'linear' }),
+ kf(1, 'opacity', 0, { family: 'continuous', strategy: 'linear' })
]
- },
- {
- id: 'slide-out-right',
- name: 'Slide Out to Right',
- keyframes: [
- {
- time: 0,
- property: 'position.x',
- value: 0,
- interpolation: { family: 'continuous', strategy: 'ease-in' }
- },
- {
- time: 0,
- property: 'opacity',
- value: 1,
- interpolation: { family: 'continuous', strategy: 'ease-in' }
- },
- {
- time: 1,
- property: 'position.x',
- value: 500,
- interpolation: { family: 'continuous', strategy: 'linear' }
- },
- {
- time: 1,
- property: 'opacity',
- value: 0,
- interpolation: { family: 'continuous', strategy: 'linear' }
- }
+ ),
+ preset(
+ 'slide-out-right',
+ 'Slide Out to Right',
+ 'Slides out 500px to the right with fade, from original position',
+ 'exit',
+ [
+ kf(0, 'position.x', 0, { family: 'continuous', strategy: 'ease-in' }),
+ kf(0, 'opacity', 1, { family: 'continuous', strategy: 'ease-in' }),
+ kf(1, 'position.x', 500, { family: 'continuous', strategy: 'linear' }),
+ kf(1, 'opacity', 0, { family: 'continuous', strategy: 'linear' })
]
- },
+ ),
+ preset(
+ 'slide-out-top',
+ 'Slide Out to Top',
+ 'Slides out 400px upward with fade, from original position',
+ 'exit',
+ [
+ kf(0, 'position.y', 0, { family: 'continuous', strategy: 'ease-in' }),
+ kf(0, 'opacity', 1, { family: 'continuous', strategy: 'ease-in' }),
+ kf(1, 'position.y', -400, { family: 'continuous', strategy: 'linear' }),
+ kf(1, 'opacity', 0, { family: 'continuous', strategy: 'linear' })
+ ]
+ ),
+ preset(
+ 'slide-out-bottom',
+ 'Slide Out to Bottom',
+ 'Slides out 400px downward with fade, from original position',
+ 'exit',
+ [
+ kf(0, 'position.y', 0, { family: 'continuous', strategy: 'ease-in' }),
+ kf(0, 'opacity', 1, { family: 'continuous', strategy: 'ease-in' }),
+ kf(1, 'position.y', 400, { family: 'continuous', strategy: 'linear' }),
+ kf(1, 'opacity', 0, { family: 'continuous', strategy: 'linear' })
+ ]
+ ),
// ============================================
// Scale animations
// ============================================
- {
- id: 'scale-in',
- name: 'Scale In',
- keyframes: [
- {
- time: 0,
- property: 'scale.x',
- value: 0,
- interpolation: { family: 'continuous', strategy: 'ease-out' }
- },
- {
- time: 0,
- property: 'scale.y',
- value: 0,
- interpolation: { family: 'continuous', strategy: 'ease-out' }
- },
- {
- time: 0,
- property: 'opacity',
- value: 0,
- interpolation: { family: 'continuous', strategy: 'ease-out' }
- },
- {
- time: 1,
- property: 'scale.x',
- value: 1,
- interpolation: { family: 'continuous', strategy: 'linear' }
- },
- {
- time: 1,
- property: 'scale.y',
- value: 1,
- interpolation: { family: 'continuous', strategy: 'linear' }
- },
- {
- time: 1,
- property: 'opacity',
- value: 1,
- interpolation: { family: 'continuous', strategy: 'linear' }
- }
+ preset(
+ 'scale-in',
+ 'Scale In',
+ 'Grows from 0 to full size with fade, uniform scale on both axes',
+ 'enter',
+ [
+ kf(0, 'scale.x', 0, { family: 'continuous', strategy: 'ease-out' }),
+ kf(0, 'scale.y', 0, { family: 'continuous', strategy: 'ease-out' }),
+ kf(0, 'opacity', 0, { family: 'continuous', strategy: 'ease-out' }),
+ kf(1, 'scale.x', 1, { family: 'continuous', strategy: 'linear' }),
+ kf(1, 'scale.y', 1, { family: 'continuous', strategy: 'linear' }),
+ kf(1, 'opacity', 1, { family: 'continuous', strategy: 'linear' })
]
- },
- {
- id: 'scale-out',
- name: 'Scale Out',
- keyframes: [
- {
- time: 0,
- property: 'scale.x',
- value: 1,
- interpolation: { family: 'continuous', strategy: 'ease-in' }
- },
- {
- time: 0,
- property: 'scale.y',
- value: 1,
- interpolation: { family: 'continuous', strategy: 'ease-in' }
- },
- {
- time: 0,
- property: 'opacity',
- value: 1,
- interpolation: { family: 'continuous', strategy: 'ease-in' }
- },
- {
- time: 1,
- property: 'scale.x',
- value: 0,
- interpolation: { family: 'continuous', strategy: 'linear' }
- },
- {
- time: 1,
- property: 'scale.y',
- value: 0,
- interpolation: { family: 'continuous', strategy: 'linear' }
- },
- {
- time: 1,
- property: 'opacity',
- value: 0,
- interpolation: { family: 'continuous', strategy: 'linear' }
- }
+ ),
+ preset(
+ 'scale-out',
+ 'Scale Out',
+ 'Shrinks from full size to 0 with fade, uniform scale on both axes',
+ 'exit',
+ [
+ kf(0, 'scale.x', 1, { family: 'continuous', strategy: 'ease-in' }),
+ kf(0, 'scale.y', 1, { family: 'continuous', strategy: 'ease-in' }),
+ kf(0, 'opacity', 1, { family: 'continuous', strategy: 'ease-in' }),
+ kf(1, 'scale.x', 0, { family: 'continuous', strategy: 'linear' }),
+ kf(1, 'scale.y', 0, { family: 'continuous', strategy: 'linear' }),
+ kf(1, 'opacity', 0, { family: 'continuous', strategy: 'linear' })
]
- },
- {
- id: 'pop',
- name: 'Pop (Scale with Overshoot)',
- keyframes: [
- {
- time: 0,
- property: 'scale.x',
- value: 0,
- interpolation: { family: 'continuous', strategy: 'ease-out' }
- },
- {
- time: 0,
- property: 'scale.y',
- value: 0,
- interpolation: { family: 'continuous', strategy: 'ease-out' }
- },
- {
- time: 0,
- property: 'opacity',
- value: 0,
- interpolation: { family: 'continuous', strategy: 'ease-out' }
- },
- {
- time: 0.6,
- property: 'scale.x',
- value: 1.15,
- interpolation: { family: 'continuous', strategy: 'ease-out' }
- },
- {
- time: 0.6,
- property: 'scale.y',
- value: 1.15,
- interpolation: { family: 'continuous', strategy: 'ease-out' }
- },
- {
- time: 0.6,
- property: 'opacity',
- value: 1,
- interpolation: { family: 'continuous', strategy: 'ease-out' }
- },
- {
- time: 1,
- property: 'scale.x',
- value: 1,
- interpolation: { family: 'continuous', strategy: 'ease-in-out' }
- },
- {
- time: 1,
- property: 'scale.y',
- value: 1,
- interpolation: { family: 'continuous', strategy: 'ease-in-out' }
- }
+ ),
+ preset(
+ 'pop',
+ 'Pop (Scale with Overshoot)',
+ 'Scales up with a bouncy overshoot to 115% then settles at 100%',
+ 'enter',
+ [
+ kf(0, 'scale.x', 0, { family: 'continuous', strategy: 'ease-out' }),
+ kf(0, 'scale.y', 0, { family: 'continuous', strategy: 'ease-out' }),
+ kf(0, 'opacity', 0, { family: 'continuous', strategy: 'ease-out' }),
+ kf(0.6, 'scale.x', 1.15, { family: 'continuous', strategy: 'ease-out' }),
+ kf(0.6, 'scale.y', 1.15, { family: 'continuous', strategy: 'ease-out' }),
+ kf(0.6, 'opacity', 1, { family: 'continuous', strategy: 'ease-out' }),
+ kf(1, 'scale.x', 1, { family: 'continuous', strategy: 'ease-in-out' }),
+ kf(1, 'scale.y', 1, { family: 'continuous', strategy: 'ease-in-out' })
]
- },
+ ),
// ============================================
// Bounce animations
// ============================================
- {
- id: 'bounce',
- name: 'Bounce',
- keyframes: [
- {
- time: 0,
- property: 'scale.y',
- value: 1,
- interpolation: { family: 'continuous', strategy: 'ease-in-out' }
- },
- {
- time: 0.25,
- property: 'scale.y',
- value: 1.2,
- interpolation: { family: 'continuous', strategy: 'ease-in-out' }
- },
- {
- time: 0.5,
- property: 'scale.y',
- value: 0.9,
- interpolation: { family: 'continuous', strategy: 'ease-in-out' }
- },
- {
- time: 0.75,
- property: 'scale.y',
- value: 1.05,
- interpolation: { family: 'continuous', strategy: 'ease-in-out' }
- },
- {
- time: 1,
- property: 'scale.y',
- value: 1,
- interpolation: { family: 'continuous', strategy: 'linear' }
- }
+ preset(
+ 'bounce',
+ 'Bounce',
+ 'Bounces vertically with decreasing amplitude (scale.y: 1 → 1.2 → 0.9 → 1.05 → 1)',
+ 'emphasis',
+ [
+ kf(0, 'scale.y', 1, { family: 'continuous', strategy: 'ease-in-out' }),
+ kf(0.25, 'scale.y', 1.2, { family: 'continuous', strategy: 'ease-in-out' }),
+ kf(0.5, 'scale.y', 0.9, { family: 'continuous', strategy: 'ease-in-out' }),
+ kf(0.75, 'scale.y', 1.05, { family: 'continuous', strategy: 'ease-in-out' }),
+ kf(1, 'scale.y', 1, { family: 'continuous', strategy: 'linear' })
]
- },
- {
- id: 'bounce-in',
- name: 'Bounce In',
- keyframes: [
- {
- time: 0,
- property: 'scale.x',
- value: 0,
- interpolation: { family: 'continuous', strategy: 'ease-out' }
- },
- {
- time: 0,
- property: 'scale.y',
- value: 0,
- interpolation: { family: 'continuous', strategy: 'ease-out' }
- },
- {
- time: 0,
- property: 'opacity',
- value: 0,
- interpolation: { family: 'continuous', strategy: 'ease-out' }
- },
- {
- time: 0.4,
- property: 'scale.x',
- value: 1.2,
- interpolation: { family: 'continuous', strategy: 'ease-out' }
- },
- {
- time: 0.4,
- property: 'scale.y',
- value: 1.2,
- interpolation: { family: 'continuous', strategy: 'ease-out' }
- },
- {
- time: 0.4,
- property: 'opacity',
- value: 1,
- interpolation: { family: 'continuous', strategy: 'linear' }
- },
- {
- time: 0.6,
- property: 'scale.x',
- value: 0.9,
- interpolation: { family: 'continuous', strategy: 'ease-in-out' }
- },
- {
- time: 0.6,
- property: 'scale.y',
- value: 0.9,
- interpolation: { family: 'continuous', strategy: 'ease-in-out' }
- },
- {
- time: 0.8,
- property: 'scale.x',
- value: 1.05,
- interpolation: { family: 'continuous', strategy: 'ease-in-out' }
- },
- {
- time: 0.8,
- property: 'scale.y',
- value: 1.05,
- interpolation: { family: 'continuous', strategy: 'ease-in-out' }
- },
- {
- time: 1,
- property: 'scale.x',
- value: 1,
- interpolation: { family: 'continuous', strategy: 'ease-in-out' }
- },
- {
- time: 1,
- property: 'scale.y',
- value: 1,
- interpolation: { family: 'continuous', strategy: 'ease-in-out' }
- }
+ ),
+ preset(
+ 'bounce-in',
+ 'Bounce In',
+ 'Enters with bouncing scale overshoot (0 → 1.2 → 0.9 → 1.05 → 1)',
+ 'enter',
+ [
+ kf(0, 'scale.x', 0, { family: 'continuous', strategy: 'ease-out' }),
+ kf(0, 'scale.y', 0, { family: 'continuous', strategy: 'ease-out' }),
+ kf(0, 'opacity', 0, { family: 'continuous', strategy: 'ease-out' }),
+ kf(0.4, 'scale.x', 1.2, { family: 'continuous', strategy: 'ease-out' }),
+ kf(0.4, 'scale.y', 1.2, { family: 'continuous', strategy: 'ease-out' }),
+ kf(0.4, 'opacity', 1, { family: 'continuous', strategy: 'linear' }),
+ kf(0.6, 'scale.x', 0.9, { family: 'continuous', strategy: 'ease-in-out' }),
+ kf(0.6, 'scale.y', 0.9, { family: 'continuous', strategy: 'ease-in-out' }),
+ kf(0.8, 'scale.x', 1.05, { family: 'continuous', strategy: 'ease-in-out' }),
+ kf(0.8, 'scale.y', 1.05, { family: 'continuous', strategy: 'ease-in-out' }),
+ kf(1, 'scale.x', 1, { family: 'continuous', strategy: 'ease-in-out' }),
+ kf(1, 'scale.y', 1, { family: 'continuous', strategy: 'ease-in-out' })
]
- },
+ ),
// ============================================
// Rotation animations
// ============================================
- {
- id: 'rotate-in',
- name: 'Rotate In',
- keyframes: [
- {
- time: 0,
- property: 'rotation.z',
- value: -Math.PI,
- interpolation: { family: 'continuous', strategy: 'ease-out' }
- },
- {
- time: 0,
- property: 'opacity',
- value: 0,
- interpolation: { family: 'continuous', strategy: 'ease-out' }
- },
- {
- time: 0,
- property: 'scale.x',
- value: 0.5,
- interpolation: { family: 'continuous', strategy: 'ease-out' }
- },
- {
- time: 0,
- property: 'scale.y',
- value: 0.5,
- interpolation: { family: 'continuous', strategy: 'ease-out' }
- },
- {
- time: 1,
- property: 'rotation.z',
- value: 0,
- interpolation: { family: 'continuous', strategy: 'linear' }
- },
- {
- time: 1,
- property: 'opacity',
- value: 1,
- interpolation: { family: 'continuous', strategy: 'linear' }
- },
- {
- time: 1,
- property: 'scale.x',
- value: 1,
- interpolation: { family: 'continuous', strategy: 'linear' }
- },
- {
- time: 1,
- property: 'scale.y',
- value: 1,
- interpolation: { family: 'continuous', strategy: 'linear' }
- }
- ]
- },
- {
- id: 'spin',
- name: 'Spin (Full Rotation)',
- keyframes: [
- {
- time: 0,
- property: 'rotation.z',
- value: 0,
- interpolation: { family: 'continuous', strategy: 'linear' }
- },
- {
- time: 1,
- property: 'rotation.z',
- value: Math.PI * 2,
- interpolation: { family: 'continuous', strategy: 'linear' }
- }
+ preset(
+ 'rotate-in',
+ 'Rotate In',
+ 'Rotates 180° while scaling from 50% to 100% with fade',
+ 'enter',
+ [
+ kf(0, 'rotation.z', -Math.PI, { family: 'continuous', strategy: 'ease-out' }),
+ kf(0, 'opacity', 0, { family: 'continuous', strategy: 'ease-out' }),
+ kf(0, 'scale.x', 0.5, { family: 'continuous', strategy: 'ease-out' }),
+ kf(0, 'scale.y', 0.5, { family: 'continuous', strategy: 'ease-out' }),
+ kf(1, 'rotation.z', 0, { family: 'continuous', strategy: 'linear' }),
+ kf(1, 'opacity', 1, { family: 'continuous', strategy: 'linear' }),
+ kf(1, 'scale.x', 1, { family: 'continuous', strategy: 'linear' }),
+ kf(1, 'scale.y', 1, { family: 'continuous', strategy: 'linear' })
]
- },
+ ),
+ preset('spin', 'Spin (Full Rotation)', 'Completes one full 360° clockwise rotation', 'emphasis', [
+ kf(0, 'rotation.z', 0, { family: 'continuous', strategy: 'linear' }),
+ kf(1, 'rotation.z', Math.PI * 2, { family: 'continuous', strategy: 'linear' })
+ ]),
// ============================================
// Attention/emphasis animations
// ============================================
- {
- id: 'pulse',
- name: 'Pulse',
- keyframes: [
- {
- time: 0,
- property: 'scale.x',
- value: 1,
- interpolation: { family: 'continuous', strategy: 'ease-in-out' }
- },
- {
- time: 0,
- property: 'scale.y',
- value: 1,
- interpolation: { family: 'continuous', strategy: 'ease-in-out' }
- },
- {
- time: 0.5,
- property: 'scale.x',
- value: 1.1,
- interpolation: { family: 'continuous', strategy: 'ease-in-out' }
- },
- {
- time: 0.5,
- property: 'scale.y',
- value: 1.1,
- interpolation: { family: 'continuous', strategy: 'ease-in-out' }
- },
- {
- time: 1,
- property: 'scale.x',
- value: 1,
- interpolation: { family: 'continuous', strategy: 'ease-in-out' }
- },
- {
- time: 1,
- property: 'scale.y',
- value: 1,
- interpolation: { family: 'continuous', strategy: 'ease-in-out' }
- }
+ preset(
+ 'pulse',
+ 'Pulse',
+ 'Gently scales up to 110% and back to 100%, drawing attention',
+ 'emphasis',
+ [
+ kf(0, 'scale.x', 1, { family: 'continuous', strategy: 'ease-in-out' }),
+ kf(0, 'scale.y', 1, { family: 'continuous', strategy: 'ease-in-out' }),
+ kf(0.5, 'scale.x', 1.1, { family: 'continuous', strategy: 'ease-in-out' }),
+ kf(0.5, 'scale.y', 1.1, { family: 'continuous', strategy: 'ease-in-out' }),
+ kf(1, 'scale.x', 1, { family: 'continuous', strategy: 'ease-in-out' }),
+ kf(1, 'scale.y', 1, { family: 'continuous', strategy: 'ease-in-out' })
]
- },
- {
- id: 'shake',
- name: 'Shake',
- keyframes: [
- {
- time: 0,
- property: 'position.x',
- value: 0,
- interpolation: { family: 'continuous', strategy: 'linear' }
- },
- {
- time: 0.1,
- property: 'position.x',
- value: -10,
- interpolation: { family: 'continuous', strategy: 'linear' }
- },
- {
- time: 0.2,
- property: 'position.x',
- value: 10,
- interpolation: { family: 'continuous', strategy: 'linear' }
- },
- {
- time: 0.3,
- property: 'position.x',
- value: -10,
- interpolation: { family: 'continuous', strategy: 'linear' }
- },
- {
- time: 0.4,
- property: 'position.x',
- value: 10,
- interpolation: { family: 'continuous', strategy: 'linear' }
- },
- {
- time: 0.5,
- property: 'position.x',
- value: -5,
- interpolation: { family: 'continuous', strategy: 'linear' }
- },
- {
- time: 0.6,
- property: 'position.x',
- value: 5,
- interpolation: { family: 'continuous', strategy: 'linear' }
- },
- {
- time: 0.7,
- property: 'position.x',
- value: -2,
- interpolation: { family: 'continuous', strategy: 'linear' }
- },
- {
- time: 0.8,
- property: 'position.x',
- value: 2,
- interpolation: { family: 'continuous', strategy: 'linear' }
- },
- {
- time: 1,
- property: 'position.x',
- value: 0,
- interpolation: { family: 'continuous', strategy: 'linear' }
- }
+ ),
+ preset(
+ 'shake',
+ 'Shake',
+ 'Rapid horizontal shaking with decreasing intensity (±10px → ±2px)',
+ 'emphasis',
+ [
+ kf(0, 'position.x', 0, { family: 'continuous', strategy: 'linear' }),
+ kf(0.1, 'position.x', -10, { family: 'continuous', strategy: 'linear' }),
+ kf(0.2, 'position.x', 10, { family: 'continuous', strategy: 'linear' }),
+ kf(0.3, 'position.x', -10, { family: 'continuous', strategy: 'linear' }),
+ kf(0.4, 'position.x', 10, { family: 'continuous', strategy: 'linear' }),
+ kf(0.5, 'position.x', -5, { family: 'continuous', strategy: 'linear' }),
+ kf(0.6, 'position.x', 5, { family: 'continuous', strategy: 'linear' }),
+ kf(0.7, 'position.x', -2, { family: 'continuous', strategy: 'linear' }),
+ kf(0.8, 'position.x', 2, { family: 'continuous', strategy: 'linear' }),
+ kf(1, 'position.x', 0, { family: 'continuous', strategy: 'linear' })
]
- },
- {
- id: 'float',
- name: 'Float (Subtle Up/Down)',
- keyframes: [
- {
- time: 0,
- property: 'position.y',
- value: 0,
- interpolation: { family: 'continuous', strategy: 'ease-in-out' }
- },
- {
- time: 0.5,
- property: 'position.y',
- value: -15,
- interpolation: { family: 'continuous', strategy: 'ease-in-out' }
- },
- {
- time: 1,
- property: 'position.y',
- value: 0,
- interpolation: { family: 'continuous', strategy: 'ease-in-out' }
- }
+ ),
+ preset(
+ 'float',
+ 'Float (Subtle Up/Down)',
+ 'Gentle vertical drift of 15px up and back, loopable',
+ 'emphasis',
+ [
+ kf(0, 'position.y', 0, { family: 'continuous', strategy: 'ease-in-out' }),
+ kf(0.5, 'position.y', -15, { family: 'continuous', strategy: 'ease-in-out' }),
+ kf(1, 'position.y', 0, { family: 'continuous', strategy: 'ease-in-out' })
]
- },
- {
- id: 'glow',
- name: 'Glow (Opacity Pulse)',
- keyframes: [
- {
- time: 0,
- property: 'opacity',
- value: 1,
- interpolation: { family: 'continuous', strategy: 'ease-in-out' }
- },
- {
- time: 0.5,
- property: 'opacity',
- value: 0.6,
- interpolation: { family: 'continuous', strategy: 'ease-in-out' }
- },
- {
- time: 1,
- property: 'opacity',
- value: 1,
- interpolation: { family: 'continuous', strategy: 'ease-in-out' }
- }
+ ),
+ preset(
+ 'glow',
+ 'Glow (Opacity Pulse)',
+ 'Fades opacity down to 60% and back, creating a glowing effect',
+ 'emphasis',
+ [
+ kf(0, 'opacity', 1, { family: 'continuous', strategy: 'ease-in-out' }),
+ kf(0.5, 'opacity', 0.6, { family: 'continuous', strategy: 'ease-in-out' }),
+ kf(1, 'opacity', 1, { family: 'continuous', strategy: 'ease-in-out' })
]
- },
+ ),
// ============================================
// Zoom animations
// ============================================
- {
- id: 'zoom-in',
- name: 'Zoom In (from far)',
- keyframes: [
- {
- time: 0,
- property: 'scale.x',
- value: 0.3,
- interpolation: { family: 'continuous', strategy: 'ease-out' }
- },
- {
- time: 0,
- property: 'scale.y',
- value: 0.3,
- interpolation: { family: 'continuous', strategy: 'ease-out' }
- },
- {
- time: 0,
- property: 'opacity',
- value: 0,
- interpolation: { family: 'continuous', strategy: 'ease-out' }
- },
- {
- time: 1,
- property: 'scale.x',
- value: 1,
- interpolation: { family: 'continuous', strategy: 'linear' }
- },
- {
- time: 1,
- property: 'scale.y',
- value: 1,
- interpolation: { family: 'continuous', strategy: 'linear' }
- },
- {
- time: 1,
- property: 'opacity',
- value: 1,
- interpolation: { family: 'continuous', strategy: 'linear' }
- }
+ preset(
+ 'zoom-in',
+ 'Zoom In (from far)',
+ 'Scales from 30% to 100% with fade, simulating approach from distance',
+ 'enter',
+ [
+ kf(0, 'scale.x', 0.3, { family: 'continuous', strategy: 'ease-out' }),
+ kf(0, 'scale.y', 0.3, { family: 'continuous', strategy: 'ease-out' }),
+ kf(0, 'opacity', 0, { family: 'continuous', strategy: 'ease-out' }),
+ kf(1, 'scale.x', 1, { family: 'continuous', strategy: 'linear' }),
+ kf(1, 'scale.y', 1, { family: 'continuous', strategy: 'linear' }),
+ kf(1, 'opacity', 1, { family: 'continuous', strategy: 'linear' })
]
- },
- {
- id: 'zoom-out',
- name: 'Zoom Out (to far)',
- keyframes: [
- {
- time: 0,
- property: 'scale.x',
- value: 1,
- interpolation: { family: 'continuous', strategy: 'ease-in' }
- },
- {
- time: 0,
- property: 'scale.y',
- value: 1,
- interpolation: { family: 'continuous', strategy: 'ease-in' }
- },
- {
- time: 0,
- property: 'opacity',
- value: 1,
- interpolation: { family: 'continuous', strategy: 'ease-in' }
- },
- {
- time: 1,
- property: 'scale.x',
- value: 0.3,
- interpolation: { family: 'continuous', strategy: 'linear' }
- },
- {
- time: 1,
- property: 'scale.y',
- value: 0.3,
- interpolation: { family: 'continuous', strategy: 'linear' }
- },
- {
- time: 1,
- property: 'opacity',
- value: 0,
- interpolation: { family: 'continuous', strategy: 'linear' }
- }
+ ),
+ preset(
+ 'zoom-out',
+ 'Zoom Out (to far)',
+ 'Scales from 100% to 30% with fade, simulating retreat to distance',
+ 'exit',
+ [
+ kf(0, 'scale.x', 1, { family: 'continuous', strategy: 'ease-in' }),
+ kf(0, 'scale.y', 1, { family: 'continuous', strategy: 'ease-in' }),
+ kf(0, 'opacity', 1, { family: 'continuous', strategy: 'ease-in' }),
+ kf(1, 'scale.x', 0.3, { family: 'continuous', strategy: 'linear' }),
+ kf(1, 'scale.y', 0.3, { family: 'continuous', strategy: 'linear' }),
+ kf(1, 'opacity', 0, { family: 'continuous', strategy: 'linear' })
]
- },
+ ),
// ============================================
// Special effects
// ============================================
- {
- id: 'flip-in-x',
- name: 'Flip In (Horizontal)',
- keyframes: [
- {
- time: 0,
- property: 'rotation.y',
- value: Math.PI / 2,
- interpolation: { family: 'continuous', strategy: 'ease-out' }
- },
- {
- time: 0,
- property: 'opacity',
- value: 0,
- interpolation: { family: 'continuous', strategy: 'ease-out' }
- },
- {
- time: 1,
- property: 'rotation.y',
- value: 0,
- interpolation: { family: 'continuous', strategy: 'linear' }
- },
- {
- time: 1,
- property: 'opacity',
- value: 1,
- interpolation: { family: 'continuous', strategy: 'linear' }
- }
+ preset(
+ 'flip-in-x',
+ 'Flip In (Horizontal)',
+ 'Flips 90° around Y axis into view with fade',
+ 'enter',
+ [
+ kf(0, 'rotation.y', Math.PI / 2, { family: 'continuous', strategy: 'ease-out' }),
+ kf(0, 'opacity', 0, { family: 'continuous', strategy: 'ease-out' }),
+ kf(1, 'rotation.y', 0, { family: 'continuous', strategy: 'linear' }),
+ kf(1, 'opacity', 1, { family: 'continuous', strategy: 'linear' })
]
- },
- {
- id: 'flip-in-y',
- name: 'Flip In (Vertical)',
- keyframes: [
- {
- time: 0,
- property: 'rotation.x',
- value: Math.PI / 2,
- interpolation: { family: 'continuous', strategy: 'ease-out' }
- },
- {
- time: 0,
- property: 'opacity',
- value: 0,
- interpolation: { family: 'continuous', strategy: 'ease-out' }
- },
- {
- time: 1,
- property: 'rotation.x',
- value: 0,
- interpolation: { family: 'continuous', strategy: 'linear' }
- },
- {
- time: 1,
- property: 'opacity',
- value: 1,
- interpolation: { family: 'continuous', strategy: 'linear' }
- }
+ ),
+ preset(
+ 'flip-in-y',
+ 'Flip In (Vertical)',
+ 'Flips 90° around X axis into view with fade',
+ 'enter',
+ [
+ kf(0, 'rotation.x', Math.PI / 2, { family: 'continuous', strategy: 'ease-out' }),
+ kf(0, 'opacity', 0, { family: 'continuous', strategy: 'ease-out' }),
+ kf(1, 'rotation.x', 0, { family: 'continuous', strategy: 'linear' }),
+ kf(1, 'opacity', 1, { family: 'continuous', strategy: 'linear' })
]
- },
- {
- id: 'swing',
- name: 'Swing',
- keyframes: [
- {
- time: 0,
- property: 'rotation.z',
- value: 0,
- interpolation: { family: 'continuous', strategy: 'ease-in-out' }
- },
- {
- time: 0.2,
- property: 'rotation.z',
- value: 0.26,
- interpolation: { family: 'continuous', strategy: 'ease-in-out' }
- }, // ~15deg
- {
- time: 0.4,
- property: 'rotation.z',
- value: -0.17,
- interpolation: { family: 'continuous', strategy: 'ease-in-out' }
- }, // ~-10deg
- {
- time: 0.6,
- property: 'rotation.z',
- value: 0.09,
- interpolation: { family: 'continuous', strategy: 'ease-in-out' }
- }, // ~5deg
- {
- time: 0.8,
- property: 'rotation.z',
- value: -0.05,
- interpolation: { family: 'continuous', strategy: 'ease-in-out' }
- }, // ~-3deg
- {
- time: 1,
- property: 'rotation.z',
- value: 0,
- interpolation: { family: 'continuous', strategy: 'ease-in-out' }
- }
+ ),
+ preset(
+ 'swing',
+ 'Swing',
+ 'Pendulum-like rotation swinging ±15° with decreasing amplitude',
+ 'emphasis',
+ [
+ kf(0, 'rotation.z', 0, { family: 'continuous', strategy: 'ease-in-out' }),
+ kf(0.2, 'rotation.z', 0.26, { family: 'continuous', strategy: 'ease-in-out' }),
+ kf(0.4, 'rotation.z', -0.17, { family: 'continuous', strategy: 'ease-in-out' }),
+ kf(0.6, 'rotation.z', 0.09, { family: 'continuous', strategy: 'ease-in-out' }),
+ kf(0.8, 'rotation.z', -0.05, { family: 'continuous', strategy: 'ease-in-out' }),
+ kf(1, 'rotation.z', 0, { family: 'continuous', strategy: 'ease-in-out' })
]
- }
+ )
];
/**
@@ -933,10 +460,11 @@ export const AnimationPresetIdSchema = z.enum(
);
export type AnimationPresetId = z.infer