0.2.0: polygon tool, mask editor preview, collapsible properties

- Polygon asset type with per-vertex animation: new sidebar button inserts a
  hexagon; vertex handles in the editor write per-keyframe `points` arrays
  that interpolate between keyframes (requires matching point counts).
- Canvas mask preview: toggle in the zoom toolbar switches the stage
  between the editable ghost view and a rendered view that applies the
  actual masking via the shared canvas 2D path.
- Properties panel is grouped into collapsible sections (Transform, Text,
  Appearance, Shadow, Mask) with per-section open/closed state persisted
  to localStorage.
- Sidebar shape buttons are icon-only to reclaim width for the Polygon
  entry.

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 01:29:27 +02:00
1 parent 03b1147a93
commit a05396e734
12 files changed
+611 -173

No files matched your search

+30 -9
View File
@@ -8,6 +8,7 @@ import {
PhListBullets,
PhPencilSimple,
PhPlus,
PhPolygon,
PhSquare,
PhTextT,
PhTrash,
@@ -56,6 +57,23 @@ function addSampleCircle() {
store.addLayer(asset.id)
}
function addSamplePolygon() {
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
points.push({ x: Math.cos(a) * radius, y: Math.sin(a) * radius })
}
const asset = store.addAsset({
kind: 'polygon',
name: 'Polygon',
points,
fill: '#9b7ede',
})
store.addLayer(asset.id)
}
async function addSampleText() {
const input = await promptDialog({
title: t('sidebar.newTextDialog.title'),
@@ -169,6 +187,7 @@ const assetIconMap: Record<Asset['kind'], typeof PhSquare> = {
circle: PhCircle,
image: PhImage,
text: PhTextT,
polygon: PhPolygon,
}
const imageAssets = computed(() =>
@@ -218,17 +237,17 @@ function onAssetDragStart(e: DragEvent, asset: Extract<Asset, { kind: 'image' }>
</button>
<div class="actions">
<button class="shape-btn" @click="addSampleRect">
<PhSquare :size="14" />
{{ t('sidebar.shape.rect') }}
<button class="shape-btn" :title="t('sidebar.shape.rect')" @click="addSampleRect">
<PhSquare :size="16" />
</button>
<button class="shape-btn" @click="addSampleCircle">
<PhCircle :size="14" />
{{ t('sidebar.shape.circle') }}
<button class="shape-btn" :title="t('sidebar.shape.circle')" @click="addSampleCircle">
<PhCircle :size="16" />
</button>
<button class="shape-btn" @click="addSampleText">
<PhTextT :size="14" />
{{ t('sidebar.shape.text') }}
<button class="shape-btn" :title="t('sidebar.shape.text')" @click="addSampleText">
<PhTextT :size="16" />
</button>
<button class="shape-btn" :title="t('sidebar.shape.polygon')" @click="addSamplePolygon">
<PhPolygon :size="16" />
</button>
</div>
@@ -375,6 +394,8 @@ h4 {
.shape-btn {
flex: 1;
justify-content: center;
align-items: center;
padding: 6px 0;
font-size: 12px;
}