From 869b67458a270dc5eb802d22550df9302c8be19a Mon Sep 17 00:00:00 2001
From: th
Date: Mon, 20 Apr 2026 10:35:43 +0200
Subject: [PATCH] 0.4.0: clipart drawer, animation paths, bezier keyframes
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
- 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)
---
.gitignore | 3 +
package.json | 2 +-
server/render.html | 10 +-
src/App.vue | 19 +-
src/components/CanvasStage.vue | 266 ++++++++++++++++++++++-
src/components/ClipartDrawer.vue | 304 ++++++++++++++++++++++++++
src/components/DocumentSettings.vue | 133 +++++++++++-
src/devtools/tests.ts | 319 ++++++++++++++++++++++++++++
src/i18n/messages.ts | 18 ++
src/lib/clipartLibrary.ts | 129 +++++++++++
src/lib/exportPlayer.ts | 10 +-
src/lib/interpolate.ts | 11 +-
src/stores/clipart.ts | 113 ++++++++++
src/stores/scene.ts | 51 +++++
src/types/scene.ts | 4 +
src/version.ts | 2 +-
16 files changed, 1379 insertions(+), 15 deletions(-)
create mode 100644 src/components/ClipartDrawer.vue
create mode 100644 src/lib/clipartLibrary.ts
create mode 100644 src/stores/clipart.ts
diff --git a/.gitignore b/.gitignore
index 89f1a15..2c3ce18 100644
--- a/.gitignore
+++ b/.gitignore
@@ -6,3 +6,6 @@ dist-ssr
.DS_Store
*.log
+
+# Local clipart working folder
+/clipart/
diff --git a/package.json b/package.json
index 216a043..0d394ff 100644
--- a/package.json
+++ b/package.json
@@ -1,7 +1,7 @@
{
"name": "animatr",
"private": true,
- "version": "0.3.0",
+ "version": "0.4.0",
"type": "module",
"scripts": {
"dev": "vite",
diff --git a/server/render.html b/server/render.html
index d62c1c6..7f91efa 100644
--- a/server/render.html
+++ b/server/render.html
@@ -80,7 +80,15 @@
}
var raw=(time-a.time)/(b.time-a.time);
var t=easing(b.easing,raw);
- return lerpProps(merge(layer.base,a.props), merge(layer.base,b.props), t);
+ var pa=merge(layer.base,a.props);
+ var pb=merge(layer.base,b.props);
+ var outS=lerpProps(pa,pb,t);
+ if(b.ctrlX!=null && b.ctrlY!=null){
+ var om=1-t;
+ outS.x=om*om*pa.x+2*om*t*b.ctrlX+t*t*pb.x;
+ outS.y=om*om*pa.y+2*om*t*b.ctrlY+t*t*pb.y;
+ }
+ return outS;
}
}
return merge(layer.base);
diff --git a/src/App.vue b/src/App.vue
index f839ec2..be8cc26 100644
--- a/src/App.vue
+++ b/src/App.vue
@@ -18,6 +18,7 @@ import {
import AboutDialog from './components/AboutDialog.vue'
import AssetSidebar from './components/AssetSidebar.vue'
import CanvasStage from './components/CanvasStage.vue'
+import ClipartDrawer from './components/ClipartDrawer.vue'
import DocumentSettings from './components/DocumentSettings.vue'
import ExportDialog from './components/ExportDialog.vue'
import FullscreenPreview from './components/FullscreenPreview.vue'
@@ -290,7 +291,9 @@ onBeforeUnmount(() => {
-