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:
1 parent
a05396e734
commit
276d621d20
13 files changed
+944
-64
No files matched your search
+58
-11
@@ -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'){
|
||||
|
||||
@@ -62,6 +62,12 @@ 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.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 !== undefined && b.strokeWidth !== undefined) out.strokeWidth = lerp(a.strokeWidth, b.strokeWidth, t)
|
||||
else if (b.strokeWidth !== undefined) out.strokeWidth = b.strokeWidth
|
||||
else if (a.strokeWidth !== undefined) out.strokeWidth = a.strokeWidth
|
||||
if (a.points && b.points && a.points.length === b.points.length) {
|
||||
out.points = a.points.map((pa, i) => {
|
||||
const pb = b.points![i]
|
||||
|
||||
+51
-11
@@ -103,47 +103,86 @@ function drawLayers(
|
||||
const shadow = computeShadow(p)
|
||||
applyShadow(ctx, shadow)
|
||||
|
||||
const rawStroke = p.stroke ?? ('stroke' in asset ? asset.stroke : undefined)
|
||||
const strokeWidth =
|
||||
p.strokeWidth ?? ('strokeWidth' in asset ? asset.strokeWidth : undefined) ?? 0
|
||||
const stroke = strokeWidth > 0 ? rawStroke || '#000000' : undefined
|
||||
const hasStroke = !!stroke && stroke !== 'transparent' && strokeWidth > 0
|
||||
|
||||
if (asset.kind === 'rect') {
|
||||
const offX = layer.offsetX ?? asset.width / 2
|
||||
const offY = layer.offsetY ?? asset.height / 2
|
||||
const fill = p.fill ?? asset.fill
|
||||
const blur = p.blur ?? asset.blur ?? 0
|
||||
ctx.fillStyle = fill
|
||||
if (blur > 0) ctx.filter = `blur(${blur}px)`
|
||||
ctx.fillRect(-offX, -offY, asset.width, asset.height)
|
||||
if (fill && fill !== 'transparent') {
|
||||
ctx.fillStyle = fill
|
||||
ctx.fillRect(-offX, -offY, asset.width, asset.height)
|
||||
}
|
||||
if (hasStroke) {
|
||||
ctx.strokeStyle = stroke!
|
||||
ctx.lineWidth = strokeWidth
|
||||
ctx.strokeRect(-offX, -offY, asset.width, asset.height)
|
||||
}
|
||||
ctx.filter = 'none'
|
||||
} else if (asset.kind === 'circle') {
|
||||
const offX = layer.offsetX ?? 0
|
||||
const offY = layer.offsetY ?? 0
|
||||
const fill = p.fill ?? asset.fill
|
||||
const blur = p.blur ?? asset.blur ?? 0
|
||||
ctx.fillStyle = fill
|
||||
if (blur > 0) ctx.filter = `blur(${blur}px)`
|
||||
ctx.beginPath()
|
||||
ctx.arc(-offX, -offY, asset.radius, 0, Math.PI * 2)
|
||||
ctx.fill()
|
||||
if (fill && fill !== 'transparent') {
|
||||
ctx.fillStyle = fill
|
||||
ctx.fill()
|
||||
}
|
||||
if (hasStroke) {
|
||||
ctx.strokeStyle = stroke!
|
||||
ctx.lineWidth = strokeWidth
|
||||
ctx.stroke()
|
||||
}
|
||||
ctx.filter = 'none'
|
||||
} else if (asset.kind === 'text') {
|
||||
const offX = layer.offsetX ?? 0
|
||||
const offY = layer.offsetY ?? 0
|
||||
ctx.fillStyle = p.fill ?? asset.fill
|
||||
const fill = p.fill ?? asset.fill
|
||||
ctx.font = `${asset.fontSize}px sans-serif`
|
||||
ctx.textBaseline = 'top'
|
||||
ctx.fillText(asset.text, -offX, -offY)
|
||||
if (fill && fill !== 'transparent') {
|
||||
ctx.fillStyle = fill
|
||||
ctx.fillText(asset.text, -offX, -offY)
|
||||
}
|
||||
if (hasStroke) {
|
||||
ctx.strokeStyle = stroke!
|
||||
ctx.lineWidth = strokeWidth
|
||||
ctx.strokeText(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
|
||||
const closed = asset.closed !== false
|
||||
const needed = closed ? 3 : 2
|
||||
if (pts.length >= needed) {
|
||||
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()
|
||||
if (closed) ctx.closePath()
|
||||
if (closed && fill && fill !== 'transparent') {
|
||||
ctx.fillStyle = fill
|
||||
ctx.fill()
|
||||
}
|
||||
if (hasStroke) {
|
||||
ctx.strokeStyle = stroke!
|
||||
ctx.lineWidth = strokeWidth
|
||||
ctx.lineJoin = 'round'
|
||||
ctx.lineCap = 'round'
|
||||
ctx.stroke()
|
||||
}
|
||||
ctx.filter = 'none'
|
||||
}
|
||||
} else if (asset.kind === 'image') {
|
||||
@@ -201,7 +240,8 @@ function drawMaskShape(
|
||||
const offX = layer.offsetX ?? 0
|
||||
const offY = layer.offsetY ?? 0
|
||||
const pts = p.points ?? asset.points
|
||||
if (pts.length >= 3) {
|
||||
const closed = asset.closed !== false
|
||||
if (closed && 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)
|
||||
|
||||
Reference in new issue
Block a user