-
Notifications
You must be signed in to change notification settings - Fork 0
wip #28
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
wip #28
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,157 @@ | ||||||||||||||||||||||||||||||
| <script lang="ts"> | ||||||||||||||||||||||||||||||
| import * as Popover from '$lib/components/ui/popover'; | ||||||||||||||||||||||||||||||
| import { Button } from '$lib/components/ui/button'; | ||||||||||||||||||||||||||||||
| import { ChevronDown } from '@lucide/svelte'; | ||||||||||||||||||||||||||||||
| import type { TypedAnimationPreset } from '$lib/engine/presets'; | ||||||||||||||||||||||||||||||
| import { getLayerTransform, getLayerStyle } from '$lib/engine/layer-rendering'; | ||||||||||||||||||||||||||||||
| import { generateTransformCSS, generateFilterCSS } from '$lib/layers/base'; | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| const { | ||||||||||||||||||||||||||||||
| value, | ||||||||||||||||||||||||||||||
| options, | ||||||||||||||||||||||||||||||
| placeholder = 'Select animation', | ||||||||||||||||||||||||||||||
| onchange | ||||||||||||||||||||||||||||||
| }: { | ||||||||||||||||||||||||||||||
| value: string; | ||||||||||||||||||||||||||||||
| options: TypedAnimationPreset[]; | ||||||||||||||||||||||||||||||
| placeholder?: string; | ||||||||||||||||||||||||||||||
| onchange: (value: string) => void; | ||||||||||||||||||||||||||||||
| } = $props(); | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| let open = $state(false); | ||||||||||||||||||||||||||||||
| let hoveredPresetId = $state<string | null>(null); | ||||||||||||||||||||||||||||||
| let previewTime = $state(0); | ||||||||||||||||||||||||||||||
| let animationFrameId = $state<number | null>(null); | ||||||||||||||||||||||||||||||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
⚡ Proposed fix- let animationFrameId = $state<number | null>(null);
+ let animationFrameId: number | null = null;📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| const selectedPreset = $derived(options.find((opt) => opt.id === value)); | ||||||||||||||||||||||||||||||
| const hoveredPreset = $derived(options.find((opt) => opt.id === hoveredPresetId)); | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| // Create a dummy layer for preview | ||||||||||||||||||||||||||||||
| const previewLayer = $derived( | ||||||||||||||||||||||||||||||
| createLayer('rectangle', { | ||||||||||||||||||||||||||||||
| name: 'Preview', | ||||||||||||||||||||||||||||||
| transform: { | ||||||||||||||||||||||||||||||
| position: { x: 0, y: 0, z: 0 }, | ||||||||||||||||||||||||||||||
| rotation: { x: 0, y: 0, z: 0 }, | ||||||||||||||||||||||||||||||
| scale: { x: 1, y: 1, z: 1 }, | ||||||||||||||||||||||||||||||
| anchor: 'center' | ||||||||||||||||||||||||||||||
| }, | ||||||||||||||||||||||||||||||
| style: { opacity: 1 }, | ||||||||||||||||||||||||||||||
| props: { | ||||||||||||||||||||||||||||||
| width: 60, | ||||||||||||||||||||||||||||||
| height: 60, | ||||||||||||||||||||||||||||||
| fill: '#3b82f6', | ||||||||||||||||||||||||||||||
| stroke: '#2563eb', | ||||||||||||||||||||||||||||||
| strokeWidth: 2, | ||||||||||||||||||||||||||||||
| borderRadius: 8 | ||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||
| }) | ||||||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| // Apply preset keyframes to preview layer | ||||||||||||||||||||||||||||||
| const previewLayerWithKeyframes = $derived.by(() => { | ||||||||||||||||||||||||||||||
| if (!hoveredPreset) return previewLayer; | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| const keyframes = hoveredPreset.keyframes.map((kf) => ({ | ||||||||||||||||||||||||||||||
| id: crypto.randomUUID(), | ||||||||||||||||||||||||||||||
| time: kf.time, | ||||||||||||||||||||||||||||||
| property: kf.property as any, | ||||||||||||||||||||||||||||||
| value: kf.value, | ||||||||||||||||||||||||||||||
| interpolation: kf.interpolation | ||||||||||||||||||||||||||||||
| })); | ||||||||||||||||||||||||||||||
|
Comment on lines
+55
to
+61
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
The ESLint error flags this. The 🛡️ Proposed fix (adjust type based on actual TypedAnimationPreset keyframe type)- property: kf.property as any,
+ property: kf.property as import('$lib/schemas/animation').AnimatableProperty,As per coding guidelines: "Enable TypeScript strict mode and do not use 📝 Committable suggestion
Suggested change
🧰 Tools🪛 ESLint[error] 58-58: Unexpected any. Specify a different type. ( 🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| return { ...previewLayer, keyframes }; | ||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| // Get current transform and style for preview at current time | ||||||||||||||||||||||||||||||
| const previewTransform = $derived(getLayerTransform(previewLayerWithKeyframes, previewTime, 1)); | ||||||||||||||||||||||||||||||
| const previewStyle = $derived(getLayerStyle(previewLayerWithKeyframes, previewTime, 1)); | ||||||||||||||||||||||||||||||
| const previewTransformCSS = $derived(generateTransformCSS(previewTransform)); | ||||||||||||||||||||||||||||||
| const previewFilterCSS = $derived(generateFilterCSS(previewStyle)); | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| function startPreview(presetId: string) { | ||||||||||||||||||||||||||||||
| hoveredPresetId = presetId; | ||||||||||||||||||||||||||||||
| previewTime = 0; | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| // Animate preview | ||||||||||||||||||||||||||||||
| const startTime = performance.now(); | ||||||||||||||||||||||||||||||
| const duration = 1000; // 1 second loop | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| function animate(currentTime: number) { | ||||||||||||||||||||||||||||||
| const elapsed = currentTime - startTime; | ||||||||||||||||||||||||||||||
| previewTime = (elapsed % duration) / duration; | ||||||||||||||||||||||||||||||
| animationFrameId = requestAnimationFrame(animate); | ||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| animationFrameId = requestAnimationFrame(animate); | ||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| function stopPreview() { | ||||||||||||||||||||||||||||||
| hoveredPresetId = null; | ||||||||||||||||||||||||||||||
| previewTime = 0; | ||||||||||||||||||||||||||||||
| if (animationFrameId !== null) { | ||||||||||||||||||||||||||||||
| cancelAnimationFrame(animationFrameId); | ||||||||||||||||||||||||||||||
| animationFrameId = null; | ||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| function selectPreset(presetId: string) { | ||||||||||||||||||||||||||||||
| onchange(presetId); | ||||||||||||||||||||||||||||||
| open = false; | ||||||||||||||||||||||||||||||
| stopPreview(); | ||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| $effect(() => { | ||||||||||||||||||||||||||||||
| if (!open) { | ||||||||||||||||||||||||||||||
| stopPreview(); | ||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||
|
Comment on lines
+104
to
+108
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
If you return a function from the effect, it will be called right before the effect is run again, or when the component is unmounted. Since no cleanup function is returned here, if the component is destroyed while 🔒 Proposed fix $effect(() => {
if (!open) {
stopPreview();
}
+ return () => stopPreview();
});📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||
| </script> | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| <Popover.Root bind:open> | ||||||||||||||||||||||||||||||
| <Popover.Trigger asChild let:props> | ||||||||||||||||||||||||||||||
| <Button variant="outline" class="w-full justify-between" {...props}> | ||||||||||||||||||||||||||||||
| {selectedPreset?.name ?? placeholder} | ||||||||||||||||||||||||||||||
| <ChevronDown class="ml-2 h-4 w-4 opacity-50" /> | ||||||||||||||||||||||||||||||
| </Button> | ||||||||||||||||||||||||||||||
| </Popover.Trigger> | ||||||||||||||||||||||||||||||
| <Popover.Content class="w-80 p-2" align="start"> | ||||||||||||||||||||||||||||||
| <div class="grid grid-cols-1 gap-1"> | ||||||||||||||||||||||||||||||
| {#each options as preset (preset.id)} | ||||||||||||||||||||||||||||||
| {@const isSelected = value === preset.id} | ||||||||||||||||||||||||||||||
| {@const isHovered = hoveredPresetId === preset.id} | ||||||||||||||||||||||||||||||
| <button | ||||||||||||||||||||||||||||||
| class="flex items-center justify-between rounded px-3 py-2 text-sm transition-colors hover:bg-accent" | ||||||||||||||||||||||||||||||
| class:bg-accent={isSelected} | ||||||||||||||||||||||||||||||
| onmouseenter={() => startPreview(preset.id)} | ||||||||||||||||||||||||||||||
| onmouseleave={stopPreview} | ||||||||||||||||||||||||||||||
| onclick={() => selectPreset(preset.id)} | ||||||||||||||||||||||||||||||
| > | ||||||||||||||||||||||||||||||
| <span class="flex-1 text-left">{preset.name}</span> | ||||||||||||||||||||||||||||||
| {#if isHovered} | ||||||||||||||||||||||||||||||
| <div | ||||||||||||||||||||||||||||||
| class="relative ml-2 flex h-16 w-16 items-center justify-center overflow-hidden rounded border border-border bg-background" | ||||||||||||||||||||||||||||||
| > | ||||||||||||||||||||||||||||||
| <div | ||||||||||||||||||||||||||||||
| class="absolute top-1/2 left-1/2" | ||||||||||||||||||||||||||||||
| style:transform={previewTransformCSS} | ||||||||||||||||||||||||||||||
| style:filter={previewFilterCSS} | ||||||||||||||||||||||||||||||
| style:opacity={previewStyle.opacity} | ||||||||||||||||||||||||||||||
| > | ||||||||||||||||||||||||||||||
| <div | ||||||||||||||||||||||||||||||
| class="rounded" | ||||||||||||||||||||||||||||||
| style:width="{previewLayer.props.width}px" | ||||||||||||||||||||||||||||||
| style:height="{previewLayer.props.height}px" | ||||||||||||||||||||||||||||||
| style:background={previewLayer.props.fill} | ||||||||||||||||||||||||||||||
| style:border="{previewLayer.props.strokeWidth}px solid {previewLayer.props | ||||||||||||||||||||||||||||||
| .stroke}" | ||||||||||||||||||||||||||||||
| style:border-radius="{previewLayer.props.borderRadius}px" | ||||||||||||||||||||||||||||||
| ></div> | ||||||||||||||||||||||||||||||
| </div> | ||||||||||||||||||||||||||||||
| </div> | ||||||||||||||||||||||||||||||
| {/if} | ||||||||||||||||||||||||||||||
| </button> | ||||||||||||||||||||||||||||||
| {/each} | ||||||||||||||||||||||||||||||
| </div> | ||||||||||||||||||||||||||||||
| </Popover.Content> | ||||||||||||||||||||||||||||||
| </Popover.Root> | ||||||||||||||||||||||||||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,19 +1,48 @@ | ||
| <script lang="ts"> | ||
| import { Label } from '$lib/components/ui/label'; | ||
| import { Button } from '$lib/components/ui/button'; | ||
| import ScrubInput from '../../scrub-input.svelte'; | ||
| import { Label } from '$lib/components/ui/label'; | ||
| import { getEditorState } from '$lib/contexts/editor.svelte'; | ||
| import type { TypedLayer } from '$lib/layers/typed-registry'; | ||
| import { getEnterPresets, getExitPresets } from '$lib/engine/presets'; | ||
| import type { LayerTransition } from '$lib/schemas/animation'; | ||
| import InputsWrapper from '../../inputs-wrapper.svelte'; | ||
| import ScrubInput from '../../scrub-input.svelte'; | ||
| import AnimationPresetSelect from '../animation-preset-select.svelte'; | ||
|
|
||
| const { layer }: { layer: TypedLayer } = $props(); | ||
|
|
||
| const editorState = $derived(getEditorState()); | ||
| const projectStore = $derived(editorState.project); | ||
|
|
||
| const enterPresets = $derived([ | ||
| { id: '', name: 'None', category: 'enter' as const, keyframes: [] }, | ||
| ...getEnterPresets() | ||
| ]); | ||
|
|
||
| const exitPresets = $derived([ | ||
| { id: '', name: 'None', category: 'exit' as const, keyframes: [] }, | ||
| ...getExitPresets() | ||
| ]); | ||
|
|
||
| let enterPresetId = $derived<string>(layer.enterTransition?.presetId ?? ''); | ||
| let enterDuration = $derived<number>(layer.enterTransition?.duration ?? 0.5); | ||
| let exitPresetId = $derived<string>(layer.exitTransition?.presetId ?? ''); | ||
| let exitDuration = $derived<number>(layer.exitTransition?.duration ?? 0.5); | ||
|
|
||
| function updateTransition(type: 'enter' | 'exit', presetId: string, duration: number) { | ||
| const transition: LayerTransition | undefined = presetId ? { presetId, duration } : undefined; | ||
| const key = type === 'enter' ? 'enterTransition' : 'exitTransition'; | ||
| projectStore.updateLayer(layer.id, { [key]: transition }); | ||
| } | ||
| </script> | ||
|
|
||
| <div class="grid grid-cols-2 gap-2"> | ||
| <div class="space-y-1"> | ||
| <Label class="text-xs text-muted-foreground">Enter (s)</Label> | ||
| <div class="space-y-3"> | ||
| <InputsWrapper | ||
| fields={[ | ||
| { for: 'enter-time', labels: 'Enter (s)' }, | ||
| { for: 'exit-time', labels: 'Exit (s)' } | ||
| ]} | ||
| > | ||
| <ScrubInput | ||
| id="enter-time" | ||
| value={layer.enterTime ?? 0} | ||
|
|
@@ -27,9 +56,6 @@ | |
| step={0.1} | ||
| onchange={(v) => projectStore.setLayerEnterTime(layer.id, v)} | ||
| /> | ||
| </div> | ||
| <div class="space-y-1"> | ||
| <Label class="text-xs text-muted-foreground">Exit (s)</Label> | ||
| <ScrubInput | ||
| id="exit-time" | ||
| value={layer.exitTime ?? projectStore.state.duration} | ||
|
|
@@ -38,6 +64,72 @@ | |
| step={0.1} | ||
| onchange={(v) => projectStore.setLayerExitTime(layer.id, v)} | ||
| /> | ||
| </InputsWrapper> | ||
|
|
||
| <div class="space-y-2"> | ||
| <Label class="text-xs text-muted-foreground">Enter Transition</Label> | ||
| <InputsWrapper | ||
| fields={[ | ||
| { for: 'enter-preset', labels: 'Animation' }, | ||
| { for: 'enter-duration', labels: 'Duration (s)' } | ||
| ]} | ||
| > | ||
| <AnimationPresetSelect | ||
| value={enterPresetId} | ||
| options={enterPresets} | ||
| placeholder="None" | ||
| onchange={(v) => { | ||
| enterPresetId = v; | ||
| updateTransition('enter', v, enterDuration); | ||
| }} | ||
| /> | ||
| {#if enterPresetId} | ||
| <ScrubInput | ||
| id="enter-duration" | ||
| value={enterDuration} | ||
| min={0.1} | ||
| max={5} | ||
| step={0.1} | ||
| onchange={(v) => { | ||
| enterDuration = v; | ||
| updateTransition('enter', enterPresetId, v); | ||
| }} | ||
| /> | ||
| {/if} | ||
| </InputsWrapper> | ||
|
Comment on lines
+71
to
+99
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧩 Analysis chain🏁 Script executed: fd -e svelte "inputs-wrapper" -HRepository: epavanello/devmotion Length of output: 118 🏁 Script executed: cat -n src/lib/components/editor/panels/inputs-wrapper.svelteRepository: epavanello/devmotion Length of output: 2427 Fix orphaned "Duration (s)" label when no animation preset is selected. The 🤖 Prompt for AI Agents |
||
| </div> | ||
|
|
||
| <div class="space-y-2"> | ||
| <Label class="text-xs text-muted-foreground">Exit Transition</Label> | ||
| <InputsWrapper | ||
| fields={[ | ||
| { for: 'exit-preset', labels: 'Animation' }, | ||
| { for: 'exit-duration', labels: 'Duration (s)' } | ||
| ]} | ||
| > | ||
| <AnimationPresetSelect | ||
| value={exitPresetId} | ||
| options={exitPresets} | ||
| placeholder="None" | ||
| onchange={(v) => { | ||
| exitPresetId = v; | ||
| updateTransition('exit', v, exitDuration); | ||
| }} | ||
| /> | ||
| {#if exitPresetId} | ||
| <ScrubInput | ||
| id="exit-duration" | ||
| value={exitDuration} | ||
| min={0.1} | ||
| max={5} | ||
| step={0.1} | ||
| onchange={(v) => { | ||
| exitDuration = v; | ||
| updateTransition('exit', exitPresetId, v); | ||
| }} | ||
| /> | ||
| {/if} | ||
| </InputsWrapper> | ||
| </div> | ||
| </div> | ||
|
|
||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🧩 Analysis chain
🏁 Script executed:
Repository: epavanello/devmotion
Length of output: 91
🏁 Script executed:
Repository: epavanello/devmotion
Length of output: 3812
🏁 Script executed:
Repository: epavanello/devmotion
Length of output: 91
🏁 Script executed:
Repository: epavanello/devmotion
Length of output: 94
🏁 Script executed:
Repository: epavanello/devmotion
Length of output: 94
🏁 Script executed:
# Check the rest of the animation-preset-select.svelte file to see all imports cat -n src/lib/components/editor/panels/properties/animation-preset-select.svelteRepository: epavanello/devmotion
Length of output: 6243
🏁 Script executed:
Repository: epavanello/devmotion
Length of output: 151
🏁 Script executed:
Repository: epavanello/devmotion
Length of output: 123
Add missing
createLayerimport — build will fail without it.createLayer('rectangle', ...)is called at line 31 but is not imported. Add the following import with the other$lib/engineimports:This import should be added after line 6 to maintain proper import order (engine imports grouped together).
🤖 Prompt for AI Agents