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:
1 parent
03b1147a93
commit
a05396e734
12 files changed
+611
-173
No files matched your search
+36
-2
@@ -47,6 +47,14 @@ const ENGINE_SOURCE = `(function(){
|
||||
if(a.fill&&b.fill)out.fill=lerpColor(a.fill,b.fill,t);
|
||||
else if(b.fill)out.fill=b.fill;
|
||||
else if(a.fill)out.fill=a.fill;
|
||||
if(a.points&&b.points&&a.points.length===b.points.length){
|
||||
var lp=[];
|
||||
for(var lpi=0;lpi<a.points.length;lpi++){
|
||||
lp.push({x:lerp(a.points[lpi].x,b.points[lpi].x,t),y:lerp(a.points[lpi].y,b.points[lpi].y,t)});
|
||||
}
|
||||
out.points=lp;
|
||||
} else if(b.points) out.points=b.points;
|
||||
else if(a.points) out.points=a.points;
|
||||
return out;
|
||||
}
|
||||
function sample(layer,time){
|
||||
@@ -143,7 +151,7 @@ const ENGINE_SOURCE = `(function(){
|
||||
function drawMaskShape(ctxm,layer,time){
|
||||
var asset=assetMap[layer.assetId];
|
||||
if(!asset) return;
|
||||
if(asset.kind!=='rect' && asset.kind!=='circle' && asset.kind!=='text') return;
|
||||
if(asset.kind!=='rect' && asset.kind!=='circle' && asset.kind!=='text' && asset.kind!=='polygon') return;
|
||||
var p=sample(layer,time);
|
||||
ctxm.save();
|
||||
ctxm.translate(p.x,p.y);
|
||||
@@ -160,12 +168,23 @@ const ENGINE_SOURCE = `(function(){
|
||||
ctxm.beginPath();
|
||||
ctxm.arc(-mcOff,-mcOffY,asset.radius,0,Math.PI*2);
|
||||
ctxm.fill();
|
||||
} else {
|
||||
} else if(asset.kind==='text'){
|
||||
var mtOff=layer.offsetX!=null?layer.offsetX:0;
|
||||
var mtOffY=layer.offsetY!=null?layer.offsetY:0;
|
||||
ctxm.font=asset.fontSize+'px sans-serif';
|
||||
ctxm.textBaseline='top';
|
||||
ctxm.fillText(asset.text,-mtOff,-mtOffY);
|
||||
} else {
|
||||
var mpOffX=layer.offsetX!=null?layer.offsetX:0;
|
||||
var mpOffY=layer.offsetY!=null?layer.offsetY:0;
|
||||
var mpPts=p.points||asset.points||[];
|
||||
if(mpPts.length>=3){
|
||||
ctxm.beginPath();
|
||||
ctxm.moveTo(mpPts[0].x-mpOffX,mpPts[0].y-mpOffY);
|
||||
for(var mpI=1;mpI<mpPts.length;mpI++)ctxm.lineTo(mpPts[mpI].x-mpOffX,mpPts[mpI].y-mpOffY);
|
||||
ctxm.closePath();
|
||||
ctxm.fill();
|
||||
}
|
||||
}
|
||||
ctxm.restore();
|
||||
}
|
||||
@@ -217,6 +236,21 @@ const ENGINE_SOURCE = `(function(){
|
||||
ctx.font=asset.fontSize+'px sans-serif';
|
||||
ctx.textBaseline='top';
|
||||
ctx.fillText(asset.text,-tOffX,-tOffY);
|
||||
}else if(asset.kind==='polygon'){
|
||||
var pOffX=layer.offsetX!=null?layer.offsetX:0;
|
||||
var pOffY=layer.offsetY!=null?layer.offsetY:0;
|
||||
var pBlur=(p.blur!=null?p.blur:(asset.blur||0));
|
||||
var pPts=p.points||asset.points||[];
|
||||
if(pPts.length>=3){
|
||||
ctx.fillStyle=p.fill||asset.fill;
|
||||
if(pBlur>0) ctx.filter='blur('+pBlur+'px)';
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(pPts[0].x-pOffX,pPts[0].y-pOffY);
|
||||
for(var pI=1;pI<pPts.length;pI++)ctx.lineTo(pPts[pI].x-pOffX,pPts[pI].y-pOffY);
|
||||
ctx.closePath();
|
||||
ctx.fill();
|
||||
ctx.filter='none';
|
||||
}
|
||||
}else if(asset.kind==='image'){
|
||||
var img=imgs[asset.id];
|
||||
if(img){
|
||||
|
||||
@@ -62,6 +62,16 @@ function lerpProps(a: AnimatableProps, b: AnimatableProps, t: number): Animatabl
|
||||
if (a.shadowColor && b.shadowColor) out.shadowColor = lerpColor(a.shadowColor, b.shadowColor, t)
|
||||
else if (b.shadowColor) out.shadowColor = b.shadowColor
|
||||
else if (a.shadowColor) out.shadowColor = a.shadowColor
|
||||
if (a.points && b.points && a.points.length === b.points.length) {
|
||||
out.points = a.points.map((pa, i) => {
|
||||
const pb = b.points![i]
|
||||
return { x: lerp(pa.x, pb.x, t), y: lerp(pa.y, pb.y, t) }
|
||||
})
|
||||
} else if (b.points) {
|
||||
out.points = b.points.map((p) => ({ ...p }))
|
||||
} else if (a.points) {
|
||||
out.points = a.points.map((p) => ({ ...p }))
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
|
||||
+35
-2
@@ -130,6 +130,22 @@ function drawLayers(
|
||||
ctx.font = `${asset.fontSize}px sans-serif`
|
||||
ctx.textBaseline = 'top'
|
||||
ctx.fillText(asset.text, -offX, -offY)
|
||||
} else if (asset.kind === 'polygon') {
|
||||
const offX = layer.offsetX ?? 0
|
||||
const offY = layer.offsetY ?? 0
|
||||
const fill = p.fill ?? asset.fill
|
||||
const blur = p.blur ?? asset.blur ?? 0
|
||||
const pts = p.points ?? asset.points
|
||||
if (pts.length >= 3) {
|
||||
ctx.fillStyle = fill
|
||||
if (blur > 0) ctx.filter = `blur(${blur}px)`
|
||||
ctx.beginPath()
|
||||
ctx.moveTo(pts[0].x - offX, pts[0].y - offY)
|
||||
for (let j = 1; j < pts.length; j++) ctx.lineTo(pts[j].x - offX, pts[j].y - offY)
|
||||
ctx.closePath()
|
||||
ctx.fill()
|
||||
ctx.filter = 'none'
|
||||
}
|
||||
} else if (asset.kind === 'image') {
|
||||
const img = images[asset.id]
|
||||
if (img) {
|
||||
@@ -152,7 +168,13 @@ function drawMaskShape(
|
||||
) {
|
||||
const asset = assetMap.get(layer.assetId)
|
||||
if (!asset) return
|
||||
if (asset.kind !== 'rect' && asset.kind !== 'circle' && asset.kind !== 'text') return
|
||||
if (
|
||||
asset.kind !== 'rect' &&
|
||||
asset.kind !== 'circle' &&
|
||||
asset.kind !== 'text' &&
|
||||
asset.kind !== 'polygon'
|
||||
)
|
||||
return
|
||||
const p = sampleLayer(layer, time)
|
||||
ctx.save()
|
||||
ctx.translate(p.x, p.y)
|
||||
@@ -169,12 +191,23 @@ function drawMaskShape(
|
||||
ctx.beginPath()
|
||||
ctx.arc(-offX, -offY, asset.radius, 0, Math.PI * 2)
|
||||
ctx.fill()
|
||||
} else {
|
||||
} else if (asset.kind === 'text') {
|
||||
const offX = layer.offsetX ?? 0
|
||||
const offY = layer.offsetY ?? 0
|
||||
ctx.font = `${asset.fontSize}px sans-serif`
|
||||
ctx.textBaseline = 'top'
|
||||
ctx.fillText(asset.text, -offX, -offY)
|
||||
} else {
|
||||
const offX = layer.offsetX ?? 0
|
||||
const offY = layer.offsetY ?? 0
|
||||
const pts = p.points ?? asset.points
|
||||
if (pts.length >= 3) {
|
||||
ctx.beginPath()
|
||||
ctx.moveTo(pts[0].x - offX, pts[0].y - offY)
|
||||
for (let j = 1; j < pts.length; j++) ctx.lineTo(pts[j].x - offX, pts[j].y - offY)
|
||||
ctx.closePath()
|
||||
ctx.fill()
|
||||
}
|
||||
}
|
||||
ctx.restore()
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user