0.3.0: freeform drawing, stroke properties, polygon edit gestures

- Freeform drawing tool: new sidebar button enters a click-to-draw mode
  that appends points, shows a live ghost preview line, and exits via
  Enter or double-click (near the start point closes the path). Works
  end-to-end through all four render pipelines.
- Polygon authoring gestures: double-click on a path inserts a vertex on
  the nearest edge; double-click on an open polygon's endpoint resumes
  the drawing session from that end, reversing point order when needed.
- Stroke + closed-path properties: all shape types (rect, circle, text,
  polygon) expose stroke color, stroke width, and (for polygons) a
  closed toggle. Fill and stroke each support a "transparent" switch so
  shapes can render as outline-only or fill-only.
- Mask preview no longer inherits the user's stroke — the dashed ghost
  keeps its own outline regardless of stroke-width changes.
- Playhead no longer dims on inactive timeline tracks; only keyframes,
  segments and clip blocks fade.
- Properties panel point-count control for polygons, plus per-keyframe
  points animation that correctly interpolates once a baseline exists.
- Pivot drag hides the rotation handle until release.
- Transformer attach retries on a short timer to avoid races where the
  Konva node hasn't mounted yet when selection changes.

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 09:53:01 +02:00
1 parent a05396e734
commit 276d621d20
13 files changed
+944 -64

No files matched your search

+30 -3
View File
@@ -7,6 +7,7 @@ import {
PhImageSquare,
PhListBullets,
PhPencilSimple,
PhPencilSimpleLine,
PhPlus,
PhPolygon,
PhSquare,
@@ -57,9 +58,21 @@ function addSampleCircle() {
store.addLayer(asset.id)
}
function addSamplePolygon() {
function startFreeformDrawing() {
store.startFreeformDrawing()
}
async function addSamplePolygon() {
const input = await promptDialog({
title: t('sidebar.newPolygonDialog.title'),
message: t('sidebar.newPolygonDialog.message'),
defaultValue: '6',
confirmLabel: t('sidebar.newPolygonDialog.confirm'),
})
if (input === null) return
const parsed = parseInt(input.trim(), 10)
const sides = Number.isFinite(parsed) ? Math.max(3, Math.min(24, parsed)) : 6
const radius = 60
const sides = 6
const points: { x: number; y: number }[] = []
for (let i = 0; i < sides; i++) {
const a = (i / sides) * Math.PI * 2 - Math.PI / 2
@@ -67,7 +80,7 @@ function addSamplePolygon() {
}
const asset = store.addAsset({
kind: 'polygon',
name: 'Polygon',
name: `Polygon ${sides}`,
points,
fill: '#9b7ede',
})
@@ -249,6 +262,14 @@ function onAssetDragStart(e: DragEvent, asset: Extract<Asset, { kind: 'image' }>
<button class="shape-btn" :title="t('sidebar.shape.polygon')" @click="addSamplePolygon">
<PhPolygon :size="16" />
</button>
<button
class="shape-btn"
:class="{ active: store.drawingMode === 'freeform' }"
:title="store.drawingMode === 'freeform' ? t('sidebar.shape.freeformActive') : t('sidebar.shape.freeform')"
@click="startFreeformDrawing"
>
<PhPencilSimpleLine :size="16" />
</button>
</div>
<input
@@ -399,6 +420,12 @@ h4 {
font-size: 12px;
}
.shape-btn.active {
background: var(--accent-soft);
color: var(--accent);
border-color: var(--accent);
}
.error {
color: var(--danger);
font-size: 11px;