0.4.0: clipart drawer, animation paths, bezier keyframes

- Clipart library: new left-edge drawer (5px idle, expands on hover,
  400px when open). User picks a local folder via the File System Access
  API in settings; directory handle is persisted in IndexedDB so the
  library survives reloads. Dragging a thumbnail onto the stage creates
  an image asset + layer at the drop point.
- Animation paths on the stage: selecting a keyframe draws the tween
  path from the previous keyframe to the selected one as a solid
  polyline with per-frame draggable dots. All other segments on the
  same position-track render as dashed ghost lines so the full curve
  progression is visible.
- Path dots grow on hover (2.5 → 6 px), show a grab cursor, and have a
  wider hit area so they're easier to grab.
- Bezier-curved position interpolation: keyframes gain optional
  ctrlX/ctrlY fields. When set, x/y tween along a quadratic bezier
  through the control point instead of a straight line. Dragging a
  path dot solves for the control so the curve passes through the
  drag point at its frame progress. Mirrored across all four render
  pipelines.
- Tests: new suites for polygon actions, freeform drawing lifecycle,
  polygon + stroke rendering (15 new tests). Fixed the mask-render
  tests that relied on a default-transparent canvas background.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
thandClaude Opus 4.7 committed 2026-04-20 10:35:43 +02:00
1 parent 276d621d20
commit 869b67458a
16 files changed
+1379 -15

No files matched your search

+113
View File
@@ -0,0 +1,113 @@
import { defineStore } from 'pinia'
import {
clearStoredClipartFolder,
disposeClipartItems,
isFileSystemAccessSupported,
listClipart,
loadStoredClipartFolder,
pickClipartFolder,
type ClipartItem,
} from '@/lib/clipartLibrary'
interface State {
handle: FileSystemDirectoryHandle | null
folderName: string | null
items: ClipartItem[]
open: boolean
loading: boolean
error: string | null
supported: boolean
initialized: boolean
}
const DRAWER_OPEN_KEY = 'animatr:clipartDrawerOpen'
function readDrawerOpen(): boolean {
try {
return localStorage.getItem(DRAWER_OPEN_KEY) === '1'
} catch {
return false
}
}
function writeDrawerOpen(open: boolean) {
try {
localStorage.setItem(DRAWER_OPEN_KEY, open ? '1' : '0')
} catch {
/* noop */
}
}
export const useClipartStore = defineStore('clipart', {
state: (): State => ({
handle: null,
folderName: null,
items: [],
open: readDrawerOpen(),
loading: false,
error: null,
supported: isFileSystemAccessSupported(),
initialized: false,
}),
actions: {
async initialize() {
if (this.initialized) return
this.initialized = true
if (!this.supported) return
const handle = await loadStoredClipartFolder()
if (!handle) return
this.handle = handle
this.folderName = handle.name
await this.refresh()
},
async pickFolder() {
this.error = null
try {
const handle = await pickClipartFolder()
if (!handle) return
this.setHandle(handle)
await this.refresh()
} catch (err) {
this.error = (err as Error).message ?? 'Failed to pick folder'
}
},
setHandle(handle: FileSystemDirectoryHandle) {
this.disposeItems()
this.handle = handle
this.folderName = handle.name
this.items = []
},
async clearFolder() {
this.disposeItems()
this.handle = null
this.folderName = null
this.items = []
this.error = null
await clearStoredClipartFolder()
},
async refresh() {
if (!this.handle) return
this.loading = true
this.error = null
try {
const next = await listClipart(this.handle)
this.disposeItems()
this.items = next
} catch (err) {
this.error = (err as Error).message ?? 'Failed to read folder'
} finally {
this.loading = false
}
},
disposeItems() {
disposeClipartItems(this.items)
},
setOpen(v: boolean) {
this.open = v
writeDrawerOpen(v)
},
toggle() {
this.setOpen(!this.open)
},
},
})