From b49060d59fec0062cbbad13e26aace2f46176dcc Mon Sep 17 00:00:00 2001 From: Fini Date: Sun, 10 May 2026 22:53:58 +0800 Subject: [PATCH] fix(renderer): drop-shadow follows cornerRadius / ellipse outline MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit User-reported 2026-05-10 "圆角元素的尖角阴影" — rounded cards / hero images had visibly square-cornered drop shadows poking out from under the rounded shape. Forensic root cause: applyShadowDirect at node-renderer.ts:447 was always drawing the shadow as a plain `canvas.drawRect(...)`, completely ignoring the node's cornerRadius. A frame with cornerRadius=24 + a subtle drop shadow would render the rounded body cleanly but stamp a sharp-cornered shadow rectangle just behind it, with the rectangle corners visible past the rounded outline. Fix: pass the node's cornerRadius into applyShadowDirect; when > 0 use `drawRRect` with `RRectXY(rect, cornerRadius+spread, ...)` so the shadow's rounding stays parallel to the node's rounding (the +spread correction keeps the visible curve aligned when spread expands / contracts the bounds). Ellipse / circle nodes (avatars, status dots) get cornerRadius = min(w,h)/2 from the call site so their shadows render as stadium / circle. Asymmetric-aspect ellipses get a stadium approximation rather than a true ellipse — accepted simplification, the common case is symmetric (avatar / dot). Path / line / polygon nodes have no cornerRadius and fall through with cr=0 — rectangular shadow stays correct for them. This is a renderer-layer fix that detector-only paths can't reach; ships in the same session as the typography / spacing detectors so the user sees end-to-end aesthetic improvement on the next rebuild. --- packages/pen-renderer/src/node-renderer.ts | 47 +++++++++++++++++----- 1 file changed, 38 insertions(+), 9 deletions(-) diff --git a/packages/pen-renderer/src/node-renderer.ts b/packages/pen-renderer/src/node-renderer.ts index f9a25592e..0b69c14cf 100644 --- a/packages/pen-renderer/src/node-renderer.ts +++ b/packages/pen-renderer/src/node-renderer.ts @@ -451,6 +451,7 @@ export class SkiaNodeRenderer { y: number, w: number, h: number, + cornerRadius: number = 0, ): boolean { if (!effects) return false; const shadow = effects.find((e): e is ShadowEffect => e.type === 'shadow'); @@ -462,15 +463,28 @@ export class SkiaNodeRenderer { paint.setAntiAlias(true); paint.setColor(parseColor(ck, shadow.color)); paint.setMaskFilter(ck.MaskFilter.MakeBlur(ck.BlurStyle.Normal, shadow.blur / 2, true)); - canvas.drawRect( - ck.LTRBRect( - x + shadow.offsetX - shadow.spread, - y + shadow.offsetY - shadow.spread, - x + w + shadow.offsetX + shadow.spread, - y + h + shadow.offsetY + shadow.spread, - ), - paint, + const rect = ck.LTRBRect( + x + shadow.offsetX - shadow.spread, + y + shadow.offsetY - shadow.spread, + x + w + shadow.offsetX + shadow.spread, + y + h + shadow.offsetY + shadow.spread, ); + if (cornerRadius > 0) { + // 2026-05-10 user-reported "圆角元素的尖角阴影" — drawRect was producing + // a sharp-cornered shadow under rounded frames / cards because the + // shadow geometry never inherited the node's cornerRadius. Drop + // shadows MUST follow the node's outline, otherwise the corners poke + // out from under the rounded shape. + // + // Spread expands (or contracts) the visible shadow edge by `spread` + // px on each side, so the visible corner radius needs the same + // offset to stay parallel to the node — clamp to ≥ 0 so a negative + // spread doesn't invert the curve. + const shadowR = Math.max(0, cornerRadius + shadow.spread); + canvas.drawRRect(ck.RRectXY(rect, shadowR, shadowR), paint); + } else { + canvas.drawRect(rect, paint); + } paint.delete(); return true; } @@ -541,7 +555,22 @@ export class SkiaNodeRenderer { const effects = 'effects' in node ? (node as PenNode & { effects?: PenEffect[] }).effects : undefined; if (node.type !== 'text') { - this.applyShadowDirect(canvas, effects, absX, absY, absW, absH); + // Pass the node's cornerRadius so the drop-shadow follows the rounded + // outline. Path / line / polygon don't carry cornerRadius and fall + // through with cr=0 (rectangular shadow, which is fine for them). + // Ellipse maps to cornerRadius = min(w,h)/2 — RRect with rx=ry= + // half-min produces a stadium / circle shadow that matches a + // symmetric ellipse outline; asymmetric ellipses get a stadium + // approximation rather than a true ellipse, accepted as known + // simplification (avatar / status-dot use circles). + const cr = + node.type === 'ellipse' + ? Math.min(absW, absH) / 2 + : cornerRadiusValue( + (node as PenNode & { cornerRadius?: number | [number, number, number, number] }) + .cornerRadius, + ); + this.applyShadowDirect(canvas, effects, absX, absY, absW, absH, cr); } switch (node.type) {