From 0a923cf185528f1a2e0cbea85be3e4d246866d00 Mon Sep 17 00:00:00 2001 From: EmaDev Date: Mon, 16 Feb 2026 09:07:39 +0100 Subject: [PATCH 1/3] more --- src/lib/ai/mutations.ts | 10 +- .../components/editor/panels/add-layer.svelte | 8 +- .../editor/panels/properties-panel.svelte | 161 ++++++++++++++--- src/lib/engine/interpolation.ts | 163 +++++++++++++++++- src/lib/engine/layer-factory.ts | 10 +- src/lib/engine/layer-rendering.ts | 10 +- src/lib/layers/LayerWrapper.svelte | 16 +- src/lib/layers/components/ShapeLayer.svelte | 22 ++- src/lib/layers/components/TextLayer.svelte | 31 +++- src/lib/layers/project-layer.ts | 21 +-- src/lib/schemas/animation.ts | 50 +++++- src/lib/schemas/base.ts | 37 +++- src/lib/stores/project.svelte.ts | 22 +-- src/lib/utils/interpolation-utils.ts | 42 ++++- 14 files changed, 512 insertions(+), 91 deletions(-) diff --git a/src/lib/ai/mutations.ts b/src/lib/ai/mutations.ts index b48a04d..223e03e 100644 --- a/src/lib/ai/mutations.ts +++ b/src/lib/ai/mutations.ts @@ -35,6 +35,7 @@ import type { RemoveKeyframeOutput } from './schemas'; import type { Layer, ProjectData } from '$lib/schemas/animation'; +import { defaultLayerStyle, defaultTransform } from '$lib/schemas/base'; /** * Context for resolving layer IDs (e.g. "layer_0" -> "actual-uuid") @@ -436,13 +437,8 @@ export function mutateGroupLayers( id: groupId, name: input.name ?? 'Group', type: 'group' as const, - transform: { - position: { x: 0, y: 0, z: 0 }, - rotation: { x: 0, y: 0, z: 0 }, - scale: { x: 1, y: 1 }, - anchor: 'center' as const - }, - style: { opacity: 1 }, + transform: defaultTransform(), + style: defaultLayerStyle(), visible: true, locked: false, keyframes: [], diff --git a/src/lib/components/editor/panels/add-layer.svelte b/src/lib/components/editor/panels/add-layer.svelte index 0cbc288..7fb6289 100644 --- a/src/lib/components/editor/panels/add-layer.svelte +++ b/src/lib/components/editor/panels/add-layer.svelte @@ -10,18 +10,14 @@ import type { LayerCategory } from '$lib/layers/base'; import { SvelteMap } from 'svelte/reactivity'; import type { Component } from 'svelte'; + import { defaultTransform } from '$lib/schemas/base'; const editorState = $derived(getEditorState()); const projectStore = $derived(editorState.project); function addLayer(type: LiteralUnion) { const layer = createLayer(type, { - transform: { - position: { x: 0, y: 0, z: 0 }, - rotation: { x: 0, y: 0, z: 0 }, - scale: { x: 1, y: 1 }, - anchor: 'center' - }, + transform: defaultTransform(), projectDimensions: { width: projectStore.state.width, height: projectStore.state.height diff --git a/src/lib/components/editor/panels/properties-panel.svelte b/src/lib/components/editor/panels/properties-panel.svelte index d7c66a4..c6ad79a 100644 --- a/src/lib/components/editor/panels/properties-panel.svelte +++ b/src/lib/components/editor/panels/properties-panel.svelte @@ -51,6 +51,7 @@ import { isProjectLayer, mapProjectLayerPropsToProject } from '$lib/layers/project-layer'; import type { Layer } from '$lib/schemas/animation'; import { getDefaultInterpolationForProperty } from '$lib/utils/interpolation-utils'; + import { defaultLayerStyle, defaultTransform } from '$lib/schemas/base'; const editorState = $derived(getEditorState()); const projectStore = $derived(editorState.project); @@ -115,24 +116,8 @@ // For the project settings layer, there are no keyframes, so just use static props if (isProjectSettings) { return { - transform: { - position: { - x: 0, - y: 0, - z: 0 - }, - rotation: { - x: 0, - y: 0, - z: 0 - }, - scale: { - x: 1, - y: 1 - }, - anchor: 'center' - }, - style: { opacity: 1 }, + transform: defaultTransform(), + style: defaultLayerStyle(), props: { ...selectedLayer.props } }; } @@ -162,7 +147,16 @@ } }, style: { - opacity: animatedStyle.opacity ?? selectedLayer.style.opacity + opacity: animatedStyle.opacity ?? selectedLayer.style.opacity, + blur: animatedStyle.blur ?? selectedLayer.style.blur ?? 0, + brightness: animatedStyle.brightness ?? selectedLayer.style.brightness ?? 1, + contrast: animatedStyle.contrast ?? selectedLayer.style.contrast ?? 1, + saturate: animatedStyle.saturate ?? selectedLayer.style.saturate ?? 1, + dropShadowX: animatedStyle.dropShadowX ?? selectedLayer.style.dropShadowX ?? 0, + dropShadowY: animatedStyle.dropShadowY ?? selectedLayer.style.dropShadowY ?? 0, + dropShadowBlur: animatedStyle.dropShadowBlur ?? selectedLayer.style.dropShadowBlur ?? 0, + dropShadowColor: + animatedStyle.dropShadowColor ?? selectedLayer.style.dropShadowColor ?? 'transparent' }, props: getAnimatedProps( selectedLayer.keyframes, @@ -357,7 +351,7 @@ function addKeyframe(property: AnimatableProperty) { if (!selectedLayer || !currentValues) return; - const propertyValueMap: Record = { + const propertyValueMap: Record = { 'position.x': currentValues.transform.position.x, 'position.y': currentValues.transform.position.y, 'position.z': currentValues.transform.position.z, @@ -366,7 +360,15 @@ 'rotation.z': currentValues.transform.rotation.z, 'scale.x': currentValues.transform.scale.x, 'scale.y': currentValues.transform.scale.y, - opacity: currentValues.style.opacity + opacity: currentValues.style.opacity, + blur: currentValues.style.blur, + brightness: currentValues.style.brightness, + contrast: currentValues.style.contrast, + saturate: currentValues.style.saturate, + dropShadowX: currentValues.style.dropShadowX, + dropShadowY: currentValues.style.dropShadowY, + dropShadowBlur: currentValues.style.dropShadowBlur, + dropShadowColor: currentValues.style.dropShadowColor }; let currentValue: number | string | boolean; @@ -734,6 +736,123 @@ /> + + + + + updateProperty('blur', v ?? 0, 'style')} + /> + + + updateProperty('brightness', v ?? 1, 'style')} + /> + + + updateProperty('contrast', v ?? 1, 'style')} + /> + + + updateProperty('saturate', v ?? 1, 'style')} + /> + + + + + + k.property === 'dropShadowX') + }, + { + for: 'style.dropShadowY', + labels: 'Y', + property: 'dropShadowY', + addKeyframe, + hasKeyframes: selectedLayer.keyframes.some((k) => k.property === 'dropShadowY') + } + ]} + > + updateProperty('dropShadowX', v ?? 0, 'style')} + /> + updateProperty('dropShadowY', v ?? 0, 'style')} + /> + + + updateProperty('dropShadowBlur', v ?? 0, 'style')} + /> + + + updateProperty('dropShadowColor', e.currentTarget.value, 'style')} + class="h-8 w-full cursor-pointer rounded border" + /> + + {/if} diff --git a/src/lib/engine/interpolation.ts b/src/lib/engine/interpolation.ts index ce293a3..2824466 100644 --- a/src/lib/engine/interpolation.ts +++ b/src/lib/engine/interpolation.ts @@ -2,7 +2,7 @@ * Animation interpolation and easing functions */ import BezierEasing from 'bezier-easing'; -import type { Interpolation, Keyframe, AnimatableProperty } from '$lib/types/animation'; +import type { Interpolation, Keyframe, AnimatableProperty, LayerStyle } from '$lib/types/animation'; import type { PropertyMetadata } from '$lib/layers/base'; import type { ContinuousInterpolationStrategy } from '$lib/schemas/animation'; @@ -169,23 +169,126 @@ function interpolateText( } } +/** + * Bounce easing helper (easeOutBounce is the base, others derive from it) + */ +function easeOutBounce(t: number): number { + const n1 = 7.5625; + const d1 = 2.75; + if (t < 1 / d1) return n1 * t * t; + if (t < 2 / d1) return n1 * (t -= 1.5 / d1) * t + 0.75; + if (t < 2.5 / d1) return n1 * (t -= 2.25 / d1) * t + 0.9375; + return n1 * (t -= 2.625 / d1) * t + 0.984375; +} + /** * Get easing function for continuous interpolation strategies */ function getEasingFunction(strategy: ContinuousInterpolationStrategy): (t: number) => number { switch (strategy) { + // CSS standard case 'linear': return (t) => t; - case 'ease-in': return BezierEasing(0.42, 0, 1.0, 1.0); - case 'ease-out': return BezierEasing(0, 0, 0.58, 1.0); - case 'ease-in-out': return BezierEasing(0.42, 0, 0.58, 1.0); + // Quad (power of 2) + case 'ease-in-quad': + return BezierEasing(0.55, 0.085, 0.68, 0.53); + case 'ease-out-quad': + return BezierEasing(0.25, 0.46, 0.45, 0.94); + case 'ease-in-out-quad': + return BezierEasing(0.455, 0.03, 0.515, 0.955); + + // Cubic (power of 3) + case 'ease-in-cubic': + return BezierEasing(0.55, 0.055, 0.675, 0.19); + case 'ease-out-cubic': + return BezierEasing(0.215, 0.61, 0.355, 1); + case 'ease-in-out-cubic': + return BezierEasing(0.645, 0.045, 0.355, 1); + + // Quart (power of 4) + case 'ease-in-quart': + return BezierEasing(0.895, 0.03, 0.685, 0.22); + case 'ease-out-quart': + return BezierEasing(0.165, 0.84, 0.44, 1); + case 'ease-in-out-quart': + return BezierEasing(0.77, 0, 0.175, 1); + + // Quint (power of 5) + case 'ease-in-quint': + return BezierEasing(0.755, 0.05, 0.855, 0.06); + case 'ease-out-quint': + return BezierEasing(0.23, 1, 0.32, 1); + case 'ease-in-out-quint': + return BezierEasing(0.86, 0, 0.07, 1); + + // Sine + case 'ease-in-sine': + return BezierEasing(0.47, 0, 0.745, 0.715); + case 'ease-out-sine': + return BezierEasing(0.39, 0.575, 0.565, 1); + case 'ease-in-out-sine': + return BezierEasing(0.445, 0.05, 0.55, 0.95); + + // Expo + case 'ease-in-expo': + return BezierEasing(0.95, 0.05, 0.795, 0.035); + case 'ease-out-expo': + return BezierEasing(0.19, 1, 0.22, 1); + case 'ease-in-out-expo': + return BezierEasing(1, 0, 0, 1); + + // Circ + case 'ease-in-circ': + return BezierEasing(0.6, 0.04, 0.98, 0.335); + case 'ease-out-circ': + return BezierEasing(0.075, 0.82, 0.165, 1); + case 'ease-in-out-circ': + return BezierEasing(0.785, 0.135, 0.15, 0.86); + + // Back (overshoots) + case 'ease-in-back': + return BezierEasing(0.6, -0.28, 0.735, 0.045); + case 'ease-out-back': + return BezierEasing(0.175, 0.885, 0.32, 1.275); + case 'ease-in-out-back': + return BezierEasing(0.68, -0.55, 0.265, 1.55); + + // Bounce (custom math — cannot be represented as cubic bezier) + case 'ease-out-bounce': + return easeOutBounce; + case 'ease-in-bounce': + return (t) => 1 - easeOutBounce(1 - t); + case 'ease-in-out-bounce': + return (t) => + t < 0.5 ? (1 - easeOutBounce(1 - 2 * t)) / 2 : (1 + easeOutBounce(2 * t - 1)) / 2; + + // Elastic (custom math — cannot be represented as cubic bezier) + case 'ease-in-elastic': + return (t) => { + if (t === 0 || t === 1) return t; + return -Math.pow(2, 10 * t - 10) * Math.sin((t * 10 - 10.75) * ((2 * Math.PI) / 3)); + }; + case 'ease-out-elastic': + return (t) => { + if (t === 0 || t === 1) return t; + return Math.pow(2, -10 * t) * Math.sin((t * 10 - 0.75) * ((2 * Math.PI) / 3)) + 1; + }; + case 'ease-in-out-elastic': + return (t) => { + if (t === 0 || t === 1) return t; + const c5 = (2 * Math.PI) / 4.5; + return t < 0.5 + ? -(Math.pow(2, 20 * t - 10) * Math.sin((20 * t - 11.125) * c5)) / 2 + : (Math.pow(2, -20 * t + 10) * Math.sin((20 * t - 11.125) * c5)) / 2 + 1; + }; + default: return (t) => t; } @@ -259,18 +362,60 @@ export function getAnimatedStyle( currentTime: number ): { opacity?: number; - color?: string; + blur?: number; + brightness?: number; + contrast?: number; + saturate?: number; + dropShadowX?: number; + dropShadowY?: number; + dropShadowBlur?: number; + dropShadowColor?: string; } { - const style: { opacity?: number; color?: string } = {}; + const style: Partial = {}; const opacity = getPropertyValue(keyframes, 'opacity', currentTime); if (opacity !== null) { style.opacity = opacity as number; } - const color = getPropertyValue(keyframes, 'color', currentTime); - if (color !== null) { - style.color = color as string; + // CSS filter properties + const blur = getPropertyValue(keyframes, 'blur', currentTime); + if (blur !== null) { + style.blur = blur as number; + } + + const brightness = getPropertyValue(keyframes, 'brightness', currentTime); + if (brightness !== null) { + style.brightness = brightness as number; + } + const contrast = getPropertyValue(keyframes, 'contrast', currentTime); + if (contrast !== null) { + style.contrast = contrast as number; + } + + const saturate = getPropertyValue(keyframes, 'saturate', currentTime); + if (saturate !== null) { + style.saturate = saturate as number; + } + + const dropShadowX = getPropertyValue(keyframes, 'dropShadowX', currentTime); + if (dropShadowX !== null) { + style.dropShadowX = dropShadowX as number; + } + + const dropShadowY = getPropertyValue(keyframes, 'dropShadowY', currentTime); + if (dropShadowY !== null) { + style.dropShadowY = dropShadowY as number; + } + + const dropShadowBlur = getPropertyValue(keyframes, 'dropShadowBlur', currentTime); + if (dropShadowBlur !== null) { + style.dropShadowBlur = dropShadowBlur as number; + } + + const dropShadowColor = getPropertyValue(keyframes, 'dropShadowColor', currentTime); + if (dropShadowColor !== null) { + style.dropShadowColor = dropShadowColor as string; } return style; diff --git a/src/lib/engine/layer-factory.ts b/src/lib/engine/layer-factory.ts index b856487..0ef339a 100644 --- a/src/lib/engine/layer-factory.ts +++ b/src/lib/engine/layer-factory.ts @@ -8,6 +8,7 @@ import { extractDefaultValues } from '$lib/layers/base'; import type { LayerProps, LayerTypeString } from '$lib/layers/layer-types'; import type { TypedLayer } from '$lib/layers/typed-registry'; import { calculateCoverDimensions, ASPECT_RATIOS } from '$lib/utils/media'; +import { defaultLayerStyle, defaultTransform } from '$lib/schemas/base'; /** * Default interpolation for initial keyframes @@ -71,15 +72,10 @@ export function createLayer( name: definition.label, type, transform: { - position: { x, y, z: 0 }, - rotation: { x: 0, y: 0, z: 0 }, - scale: { x: 1, y: 1 }, - anchor: 'center', + ...defaultTransform(), ...override?.transform }, - style: { - opacity: 1 - }, + style: defaultLayerStyle(), visible: true, locked: false, keyframes: initialKeyframes, diff --git a/src/lib/engine/layer-rendering.ts b/src/lib/engine/layer-rendering.ts index 3d45c5e..c222b26 100644 --- a/src/lib/engine/layer-rendering.ts +++ b/src/lib/engine/layer-rendering.ts @@ -40,7 +40,15 @@ export function getLayerStyle(layer: TypedLayer, currentTime: number) { const animatedStyle = getAnimatedStyle(layer.keyframes, currentTime); return { - opacity: animatedStyle.opacity ?? layer.style.opacity + opacity: animatedStyle.opacity ?? layer.style.opacity, + blur: animatedStyle.blur ?? layer.style.blur ?? 0, + brightness: animatedStyle.brightness ?? layer.style.brightness ?? 1, + contrast: animatedStyle.contrast ?? layer.style.contrast ?? 1, + saturate: animatedStyle.saturate ?? layer.style.saturate ?? 1, + dropShadowX: animatedStyle.dropShadowX ?? layer.style.dropShadowX ?? 0, + dropShadowY: animatedStyle.dropShadowY ?? layer.style.dropShadowY ?? 0, + dropShadowBlur: animatedStyle.dropShadowBlur ?? layer.style.dropShadowBlur ?? 0, + dropShadowColor: animatedStyle.dropShadowColor ?? layer.style.dropShadowColor ?? 'transparent' }; } diff --git a/src/lib/layers/LayerWrapper.svelte b/src/lib/layers/LayerWrapper.svelte index 26fc461..a4494fb 100644 --- a/src/lib/layers/LayerWrapper.svelte +++ b/src/lib/layers/LayerWrapper.svelte @@ -40,6 +40,20 @@ const transformCSS = $derived(generateTransformCSS(transform)); + const filterCSS = $derived.by(() => { + const parts: string[] = []; + if (style.blur > 0) parts.push(`blur(${style.blur}px)`); + if (style.brightness !== 1) parts.push(`brightness(${style.brightness})`); + if (style.contrast !== 1) parts.push(`contrast(${style.contrast})`); + if (style.saturate !== 1) parts.push(`saturate(${style.saturate})`); + if (style.dropShadowBlur > 0 || style.dropShadowX !== 0 || style.dropShadowY !== 0) { + parts.push( + `drop-shadow(${style.dropShadowX}px ${style.dropShadowY}px ${style.dropShadowBlur}px ${style.dropShadowColor})` + ); + } + return parts.length > 0 ? parts.join(' ') : undefined; + }); + // Drag state let isDragging = $state(false); let dragStart = $state({ x: 0, y: 0 }); @@ -241,7 +255,7 @@ style:--primary-color={BRAND_COLORS.blue} style:visibility={visible ? 'visible' : 'hidden'} > -
+
diff --git a/src/lib/layers/components/ShapeLayer.svelte b/src/lib/layers/components/ShapeLayer.svelte index 273bccf..47b9f11 100644 --- a/src/lib/layers/components/ShapeLayer.svelte +++ b/src/lib/layers/components/ShapeLayer.svelte @@ -18,7 +18,7 @@ */ const schema = SizeWithAspectRatioSchema.extend({ shapeType: z - .enum(['rectangle', 'circle', 'triangle', 'polygon']) + .enum(['rectangle', 'ellipse', 'circle', 'triangle', 'polygon']) .default('rectangle') .describe('Shape type') .register(fieldRegistry, { interpolationFamily: 'discrete' }), @@ -39,6 +39,14 @@ .default(2) .describe('Stroke width (px)') .register(fieldRegistry, { group: 'stroke', interpolationFamily: 'continuous' }), + borderRadius: z + .number() + .min(0) + .max(500) + .default(0) + .optional() + .describe('Corner radius (px)') + .register(fieldRegistry, { interpolationFamily: 'continuous' }), radius: z .number() .min(0) @@ -85,6 +93,7 @@ background, stroke, strokeWidth, + borderRadius = 0, radius = 100, sides = 6 }: Props = $props(); @@ -124,7 +133,16 @@ return { ...base, width: `${width}px`, - height: `${height}px` + height: `${height}px`, + borderRadius: borderRadius > 0 ? `${borderRadius}px` : undefined + }; + + case 'ellipse': + return { + ...base, + width: `${width}px`, + height: `${height}px`, + borderRadius: '50%' }; case 'circle': diff --git a/src/lib/layers/components/TextLayer.svelte b/src/lib/layers/components/TextLayer.svelte index a42d3e1..923ad02 100644 --- a/src/lib/layers/components/TextLayer.svelte +++ b/src/lib/layers/components/TextLayer.svelte @@ -42,6 +42,20 @@ interpolationFamily: 'continuous', widget: 'color' }), + letterSpacing: z + .number() + .min(-10) + .max(100) + .default(0) + .describe('Letter spacing (px)') + .register(fieldRegistry, { group: 'spacing', interpolationFamily: 'continuous' }), + lineHeight: z + .number() + .min(0.5) + .max(5) + .default(1.4) + .describe('Line height') + .register(fieldRegistry, { group: 'spacing', interpolationFamily: 'continuous' }), autoWidth: z .boolean() .default(true) @@ -71,6 +85,7 @@ propertyGroups: [ { id: 'typography', label: 'Typography' }, + { id: 'spacing', label: 'Spacing' }, { id: 'layout', label: 'Layout' } ] } as const satisfies LayerMeta; @@ -79,8 +94,18 @@ @@ -90,6 +115,8 @@ style:font-size="{fontSize}px" style:font-weight={fontWeight} style:color + style:letter-spacing="{letterSpacing}px" + style:line-height={lineHeight} style:width={autoWidth ? 'auto' : `${width}px`} style:text-align={textAlign} > diff --git a/src/lib/layers/project-layer.ts b/src/lib/layers/project-layer.ts index afb6d58..a7cde33 100644 --- a/src/lib/layers/project-layer.ts +++ b/src/lib/layers/project-layer.ts @@ -8,6 +8,7 @@ */ import type { TypedLayer } from './typed-registry'; import type { Project } from '$lib/schemas/animation'; +import { defaultLayerStyle, defaultTransform } from '$lib/schemas/base'; /** Well-known layer ID for the project settings layer */ export const PROJECT_LAYER_ID = '__project__'; @@ -29,24 +30,8 @@ export function createVirtualProjectLayer(project: Project): TypedLayer { id: PROJECT_LAYER_ID, name: project.name, type: PROJECT_LAYER_TYPE, - transform: { - position: { - x: 0, - y: 0, - z: 0 - }, - rotation: { - x: 0, - y: 0, - z: 0 - }, - scale: { - x: 1, - y: 1 - }, - anchor: 'center' - }, - style: { opacity: 1 }, + transform: defaultTransform(), + style: defaultLayerStyle(), visible: true, locked: false, keyframes: [], diff --git a/src/lib/schemas/animation.ts b/src/lib/schemas/animation.ts index 6b71491..367d7bc 100644 --- a/src/lib/schemas/animation.ts +++ b/src/lib/schemas/animation.ts @@ -16,7 +16,52 @@ import type { LiteralUnion } from 'type-fest'; // Continuous interpolation (smooth numeric transitions with easing) const ContinuousInterpolationSchema = z.object({ family: z.literal('continuous'), - strategy: z.enum(['linear', 'ease-in', 'ease-out', 'ease-in-out']) + strategy: z.enum([ + 'linear', + 'ease-in', + 'ease-out', + 'ease-in-out', + // Quad + 'ease-in-quad', + 'ease-out-quad', + 'ease-in-out-quad', + // Cubic + 'ease-in-cubic', + 'ease-out-cubic', + 'ease-in-out-cubic', + // Quart + 'ease-in-quart', + 'ease-out-quart', + 'ease-in-out-quart', + // Quint + 'ease-in-quint', + 'ease-out-quint', + 'ease-in-out-quint', + // Sine + 'ease-in-sine', + 'ease-out-sine', + 'ease-in-out-sine', + // Expo + 'ease-in-expo', + 'ease-out-expo', + 'ease-in-out-expo', + // Circ + 'ease-in-circ', + 'ease-out-circ', + 'ease-in-out-circ', + // Back (overshoots) + 'ease-in-back', + 'ease-out-back', + 'ease-in-out-back', + // Bounce + 'ease-in-bounce', + 'ease-out-bounce', + 'ease-in-out-bounce', + // Elastic + 'ease-in-elastic', + 'ease-out-elastic', + 'ease-in-out-elastic' + ]) }); export type ContinuousInterpolation = z.infer; @@ -75,8 +120,7 @@ export const BuiltInAnimatablePropertySchema = z.enum([ 'rotation.x', 'rotation.y', 'rotation.z', - 'opacity', - 'color' + ...LayerStyleSchema.keyof().options ]); // For props properties like props.fontSize, props.fill diff --git a/src/lib/schemas/base.ts b/src/lib/schemas/base.ts index 8623bfd..44e9fea 100644 --- a/src/lib/schemas/base.ts +++ b/src/lib/schemas/base.ts @@ -131,20 +131,53 @@ export const TransformSchema = z export type Transform = z.infer; +export function defaultTransform(): Transform { + return { + position: { x: 0, y: 0, z: 0 }, + rotation: { x: 0, y: 0, z: 0 }, + scale: { x: 1, y: 1 }, + anchor: 'center' + }; +} + // ============================================ // Style // ============================================ /** * Base style properties shared by all layers. - * Currently contains opacity, but may expand to include other visual properties. + * Contains opacity and CSS filter properties (blur, brightness, contrast, etc.) */ export const LayerStyleSchema = z.object({ - opacity: z.number().min(0).max(1).describe('Opacity (0-1)') + opacity: z.number().min(0).max(1).default(1).describe('Opacity (0-1)'), + // CSS filter properties + blur: z.number().min(0).default(0).describe('Blur (px)'), + brightness: z.number().min(0).max(10).default(1).describe('Brightness'), + contrast: z.number().min(0).max(10).default(1).describe('Contrast'), + saturate: z.number().min(0).max(10).default(1).describe('Saturate'), + // CSS drop-shadow filter + dropShadowX: z.number().default(0).describe('Shadow offset X (px)'), + dropShadowY: z.number().default(0).describe('Shadow offset Y (px)'), + dropShadowBlur: z.number().min(0).default(0).describe('Shadow blur (px)'), + dropShadowColor: z.string().default('transparent').describe('Shadow color') }); export type LayerStyle = z.infer; +export function defaultLayerStyle(): LayerStyle { + return { + opacity: 1, + blur: 0, + brightness: 1, + contrast: 1, + saturate: 1, + dropShadowX: 0, + dropShadowY: 0, + dropShadowBlur: 0, + dropShadowColor: 'transparent' + }; +} + // ============================================ // Base Layer Fields // ============================================ diff --git a/src/lib/stores/project.svelte.ts b/src/lib/stores/project.svelte.ts index 785f5bf..4ada644 100644 --- a/src/lib/stores/project.svelte.ts +++ b/src/lib/stores/project.svelte.ts @@ -3,7 +3,13 @@ * Manages project state and operations without persistence logic */ import { getPresetById } from '$lib/engine/presets'; -import type { Project, Keyframe, ViewportSettings, Transform } from '$lib/types/animation'; +import { + type Project, + type Keyframe, + type ViewportSettings, + type Transform, + type LayerStyle +} from '$lib/types/animation'; import { nanoid } from 'nanoid'; import { SvelteSet } from 'svelte/reactivity'; import { getLayerTransform, getLayerStyle, getLayerProps } from '$lib/engine/layer-rendering'; @@ -14,15 +20,14 @@ import { isProjectLayer, createVirtualProjectLayer } from '$lib/layers/project-layer'; +import { defaultLayerStyle, defaultTransform } from '$lib/schemas/base'; /** * Cached layer data for a single frame */ interface LayerFrameCache { transform: Transform; - style: { - opacity: number; - }; + style: LayerStyle; customProps: Record; } @@ -188,13 +193,8 @@ export class ProjectStore { id: groupId, name: 'Group', type: 'group', - transform: { - position: { x: 0, y: 0, z: 0 }, - rotation: { x: 0, y: 0, z: 0 }, - scale: { x: 1, y: 1 }, - anchor: 'center' - }, - style: { opacity: 1 }, + transform: defaultTransform(), + style: defaultLayerStyle(), visible: true, locked: false, keyframes: [], diff --git a/src/lib/utils/interpolation-utils.ts b/src/lib/utils/interpolation-utils.ts index f36e1ed..d8e94c9 100644 --- a/src/lib/utils/interpolation-utils.ts +++ b/src/lib/utils/interpolation-utils.ts @@ -16,7 +16,47 @@ export const interpolationLabels = { linear: 'Linear', 'ease-in': 'Ease In', 'ease-out': 'Ease Out', - 'ease-in-out': 'Ease In-Out' + 'ease-in-out': 'Ease In-Out', + // Quad + 'ease-in-quad': 'Ease In (Quad)', + 'ease-out-quad': 'Ease Out (Quad)', + 'ease-in-out-quad': 'Ease In-Out (Quad)', + // Cubic + 'ease-in-cubic': 'Ease In (Cubic)', + 'ease-out-cubic': 'Ease Out (Cubic)', + 'ease-in-out-cubic': 'Ease In-Out (Cubic)', + // Quart + 'ease-in-quart': 'Ease In (Quart)', + 'ease-out-quart': 'Ease Out (Quart)', + 'ease-in-out-quart': 'Ease In-Out (Quart)', + // Quint + 'ease-in-quint': 'Ease In (Quint)', + 'ease-out-quint': 'Ease Out (Quint)', + 'ease-in-out-quint': 'Ease In-Out (Quint)', + // Sine + 'ease-in-sine': 'Ease In (Sine)', + 'ease-out-sine': 'Ease Out (Sine)', + 'ease-in-out-sine': 'Ease In-Out (Sine)', + // Expo + 'ease-in-expo': 'Ease In (Expo)', + 'ease-out-expo': 'Ease Out (Expo)', + 'ease-in-out-expo': 'Ease In-Out (Expo)', + // Circ + 'ease-in-circ': 'Ease In (Circ)', + 'ease-out-circ': 'Ease Out (Circ)', + 'ease-in-out-circ': 'Ease In-Out (Circ)', + // Back + 'ease-in-back': 'Ease In (Back)', + 'ease-out-back': 'Ease Out (Back)', + 'ease-in-out-back': 'Ease In-Out (Back)', + // Bounce + 'ease-in-bounce': 'Ease In (Bounce)', + 'ease-out-bounce': 'Ease Out (Bounce)', + 'ease-in-out-bounce': 'Ease In-Out (Bounce)', + // Elastic + 'ease-in-elastic': 'Ease In (Elastic)', + 'ease-out-elastic': 'Ease Out (Elastic)', + 'ease-in-out-elastic': 'Ease In-Out (Elastic)' }, discrete: { 'step-end': 'Step End', From 5e1256705f0722918f011b651580941646da5bca Mon Sep 17 00:00:00 2001 From: EmaDev Date: Mon, 16 Feb 2026 21:19:54 +0100 Subject: [PATCH 2/3] refactor: Implement grouped properties and a field registry for layer property management in the editor panel. --- .gitignore | 3 +- .../editor/panels/input-property.svelte | 17 +- .../editor/panels/properties-group.svelte | 34 +- .../editor/panels/properties-panel.svelte | 671 +----------------- .../groups/animation-presets-group.svelte | 60 ++ .../properties/groups/keyframes-group.svelte | 8 + .../groups/layer-properties-group.svelte | 38 + .../groups/property-group-renderer.svelte | 151 ++++ .../properties/groups/style-group.svelte | 22 + .../properties/groups/time-range-group.svelte | 95 +++ .../properties/groups/transform-group.svelte | 240 +++++++ .../editor/panels/properties/groups/types.ts | 24 + src/lib/layers/base.ts | 42 +- src/lib/layers/components/AudioLayer.svelte | 3 +- src/lib/layers/components/BrowserLayer.svelte | 2 +- src/lib/layers/components/ButtonLayer.svelte | 2 +- .../layers/components/CaptionsLayer.svelte | 2 +- src/lib/layers/components/HtmlLayer.svelte | 2 +- src/lib/layers/components/ImageLayer.svelte | 2 +- src/lib/layers/components/MouseLayer.svelte | 2 +- src/lib/layers/components/PhoneLayer.svelte | 2 +- .../components/ProjectSettingsLayer.svelte | 2 +- src/lib/layers/components/ShapeLayer.svelte | 2 +- .../layers/components/TerminalLayer.svelte | 2 +- src/lib/layers/components/TextLayer.svelte | 2 +- src/lib/layers/components/VideoLayer.svelte | 2 +- src/lib/layers/layer-schemas.ts | 20 - src/lib/layers/properties/FontProperty.svelte | 2 +- .../layers/properties/SourceLayerRef.svelte | 2 +- src/lib/layers/properties/field-registry.ts | 43 ++ src/lib/layers/registry.ts | 19 +- src/lib/schemas/base.ts | 92 ++- src/lib/schemas/size.ts | 2 +- 33 files changed, 860 insertions(+), 752 deletions(-) create mode 100644 src/lib/components/editor/panels/properties/groups/animation-presets-group.svelte create mode 100644 src/lib/components/editor/panels/properties/groups/keyframes-group.svelte create mode 100644 src/lib/components/editor/panels/properties/groups/layer-properties-group.svelte create mode 100644 src/lib/components/editor/panels/properties/groups/property-group-renderer.svelte create mode 100644 src/lib/components/editor/panels/properties/groups/style-group.svelte create mode 100644 src/lib/components/editor/panels/properties/groups/time-range-group.svelte create mode 100644 src/lib/components/editor/panels/properties/groups/transform-group.svelte create mode 100644 src/lib/components/editor/panels/properties/groups/types.ts delete mode 100644 src/lib/layers/layer-schemas.ts create mode 100644 src/lib/layers/properties/field-registry.ts diff --git a/.gitignore b/.gitignore index c0e6322..03aee31 100644 --- a/.gitignore +++ b/.gitignore @@ -27,4 +27,5 @@ src/lib/paraglide # AI Debug Logs logs/ -.vscode/chrome/ \ No newline at end of file +.vscode/chrome/ +demo.json diff --git a/src/lib/components/editor/panels/input-property.svelte b/src/lib/components/editor/panels/input-property.svelte index 52ba6d2..5252381 100644 --- a/src/lib/components/editor/panels/input-property.svelte +++ b/src/lib/components/editor/panels/input-property.svelte @@ -16,12 +16,15 @@ metadata, value, layer, - onUpdateProp + onUpdateProp, + targetPath = 'props' }: { metadata: PropertyMetadata; value: unknown; layer: TypedLayer; onUpdateProp: (prop: string, value: unknown) => void; + /** The path prefix for the property (e.g., 'props' or 'style') */ + targetPath?: 'transform' | 'props' | 'style'; } = $props(); @@ -66,7 +69,7 @@ /> {:else if metadata.meta?.widget === 'textarea'}