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

+58 -11
View File
@@ -55,6 +55,12 @@ const ENGINE_SOURCE = `(function(){
out.points=lp;
} else if(b.points) out.points=b.points;
else if(a.points) out.points=a.points;
if(a.stroke&&b.stroke) out.stroke=lerpColor(a.stroke,b.stroke,t);
else if(b.stroke) out.stroke=b.stroke;
else if(a.stroke) out.stroke=a.stroke;
if(a.strokeWidth!=null&&b.strokeWidth!=null) out.strokeWidth=lerp(a.strokeWidth,b.strokeWidth,t);
else if(b.strokeWidth!=null) out.strokeWidth=b.strokeWidth;
else if(a.strokeWidth!=null) out.strokeWidth=a.strokeWidth;
return out;
}
function sample(layer,time){
@@ -178,7 +184,8 @@ const ENGINE_SOURCE = `(function(){
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){
var mpClosed=asset.closed!==false;
if(mpClosed && 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);
@@ -211,44 +218,84 @@ const ENGINE_SOURCE = `(function(){
ctx.shadowOffsetX=Math.cos(sAn)*sDi;
ctx.shadowOffsetY=Math.sin(sAn)*sDi;
}
var lRawStroke=p.stroke||asset.stroke;
var lSW=(p.strokeWidth!=null?p.strokeWidth:(asset.strokeWidth||0));
var lStroke=lSW>0?(lRawStroke||'#000000'):null;
var lHasStroke=!!lStroke && lStroke!=='transparent' && lSW>0;
if(asset.kind==='rect'){
var rOffX=layer.offsetX!=null?layer.offsetX:asset.width/2;
var rOffY=layer.offsetY!=null?layer.offsetY:asset.height/2;
var rBlur=(p.blur!=null?p.blur:(asset.blur||0));
ctx.fillStyle=p.fill||asset.fill;
if(rBlur>0) ctx.filter='blur('+rBlur+'px)';
ctx.fillRect(-rOffX,-rOffY,asset.width,asset.height);
var rFill=p.fill||asset.fill;
if(rFill && rFill!=='transparent'){
ctx.fillStyle=rFill;
ctx.fillRect(-rOffX,-rOffY,asset.width,asset.height);
}
if(lHasStroke){
ctx.strokeStyle=lStroke;
ctx.lineWidth=lSW;
ctx.strokeRect(-rOffX,-rOffY,asset.width,asset.height);
}
ctx.filter='none';
}else if(asset.kind==='circle'){
var cOffX=layer.offsetX!=null?layer.offsetX:0;
var cOffY=layer.offsetY!=null?layer.offsetY:0;
var cBlur=(p.blur!=null?p.blur:(asset.blur||0));
ctx.fillStyle=p.fill||asset.fill;
if(cBlur>0) ctx.filter='blur('+cBlur+'px)';
ctx.beginPath();
ctx.arc(-cOffX,-cOffY,asset.radius,0,Math.PI*2);
ctx.fill();
var cFill=p.fill||asset.fill;
if(cFill && cFill!=='transparent'){
ctx.fillStyle=cFill;
ctx.fill();
}
if(lHasStroke){
ctx.strokeStyle=lStroke;
ctx.lineWidth=lSW;
ctx.stroke();
}
ctx.filter='none';
}else if(asset.kind==='text'){
var tOffX=layer.offsetX!=null?layer.offsetX:0;
var tOffY=layer.offsetY!=null?layer.offsetY:0;
ctx.fillStyle=p.fill||asset.fill;
ctx.font=asset.fontSize+'px sans-serif';
ctx.textBaseline='top';
ctx.fillText(asset.text,-tOffX,-tOffY);
var tFill=p.fill||asset.fill;
if(tFill && tFill!=='transparent'){
ctx.fillStyle=tFill;
ctx.fillText(asset.text,-tOffX,-tOffY);
}
if(lHasStroke){
ctx.strokeStyle=lStroke;
ctx.lineWidth=lSW;
ctx.strokeText(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;
var pClosed=asset.closed!==false;
var pNeeded=pClosed?3:2;
if(pPts.length>=pNeeded){
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();
if(pClosed) ctx.closePath();
var pFill=p.fill||asset.fill;
if(pClosed && pFill && pFill!=='transparent'){
ctx.fillStyle=pFill;
ctx.fill();
}
if(lHasStroke){
ctx.strokeStyle=lStroke;
ctx.lineWidth=lSW;
ctx.lineJoin='round';
ctx.lineCap='round';
ctx.stroke();
}
ctx.filter='none';
}
}else if(asset.kind==='image'){