From 2e98ab04a8ffc7ef794966a7a6efc471fa6ebc28 Mon Sep 17 00:00:00 2001 From: Danila Poyarkov Date: Mon, 31 Aug 2026 21:32:19 +0300 Subject: [PATCH] fix(canvas): normalize imported dash patterns --- packages/core/src/canvas/scene.ts | 7 ++++--- packages/core/src/canvas/strokes.ts | 11 ++++++++-- .../canvas/normalize-dash-pattern.test.ts | 20 +++++++++++++++++++ 3 files changed, 33 insertions(+), 5 deletions(-) create mode 100644 tests/engine/render/canvas/normalize-dash-pattern.test.ts diff --git a/packages/core/src/canvas/scene.ts b/packages/core/src/canvas/scene.ts index 9ba9db79a..f7f38fa48 100644 --- a/packages/core/src/canvas/scene.ts +++ b/packages/core/src/canvas/scene.ts @@ -44,7 +44,8 @@ import { drawDashedRRectWithSolidCorners, drawStyledRRectStroke, getStrokeCapEntity, - getStrokeJoinEntity + getStrokeJoinEntity, + normalizeDashPattern } from './strokes' import { drawDerivedText, @@ -672,8 +673,8 @@ function drawVectorPathStrokes( miterLimit: number, outlineCacheKey?: string ): void { - const dash = stroke.dashPattern - if (dash && dash.length > 0) { + const dash = normalizeDashPattern(stroke.dashPattern) + if (dash.length > 0) { r.strokePaint.setColor(r.ck.Color4f(sc.r, sc.g, sc.b, sc.a)) r.strokePaint.setAlphaf(stroke.opacity) r.strokePaint.setStrokeWidth(stroke.weight) diff --git a/packages/core/src/canvas/strokes.ts b/packages/core/src/canvas/strokes.ts index 4e50d8f15..3fe8df0ee 100644 --- a/packages/core/src/canvas/strokes.ts +++ b/packages/core/src/canvas/strokes.ts @@ -30,6 +30,13 @@ export function getStrokeJoinEntity(r: SkiaRenderer, join: string | undefined): } } +export function normalizeDashPattern(dash: readonly number[] | undefined): number[] { + if (!dash || dash.length === 0) return [] + // Figma permits odd-length alternating patterns; CanvasKit requires the + // on/off interval list to contain a pair for every cycle. + return dash.length % 2 === 0 ? [...dash] : [...dash, ...dash] +} + function strokeInset(stroke: Stroke): number { if (stroke.align === 'INSIDE') return stroke.weight / 2 if (stroke.align === 'OUTSIDE') return -stroke.weight / 2 @@ -45,7 +52,7 @@ export function drawDashedRRectWithSolidCorners( cornerRadius: number, dashPhase = 0 ): void { - const dash = stroke.dashPattern ?? [] + const dash = normalizeDashPattern(stroke.dashPattern) const inset = strokeInset(stroke) const left = inset const top = inset @@ -166,7 +173,7 @@ export function drawStyledRRectStroke( color: Color, dashPhase = 0 ): void { - const dash = stroke.dashPattern ?? [] + const dash = normalizeDashPattern(stroke.dashPattern) configureStrokePaint(r, node, stroke, color) r.strokePaint.setPathEffect(dash.length > 0 ? r.ck.PathEffect.MakeDash(dash, dashPhase) : null) r.drawRRectStrokeWithAlign(canvas, rrect, node, stroke) diff --git a/tests/engine/render/canvas/normalize-dash-pattern.test.ts b/tests/engine/render/canvas/normalize-dash-pattern.test.ts new file mode 100644 index 000000000..5a851ec84 --- /dev/null +++ b/tests/engine/render/canvas/normalize-dash-pattern.test.ts @@ -0,0 +1,20 @@ +import { describe, expect, test } from 'bun:test' + +import { normalizeDashPattern } from '#core/canvas/strokes' + +describe('normalizeDashPattern', () => { + test('preserves valid values and mirrors odd-length patterns', () => { + expect(normalizeDashPattern([])).toEqual([]) + expect(normalizeDashPattern([8, 4])).toEqual([8, 4]) + expect(normalizeDashPattern([8])).toEqual([8, 8]) + expect(normalizeDashPattern([8, 4, 2])).toEqual([8, 4, 2, 8, 4, 2]) + expect(normalizeDashPattern([0, -1, Number.NaN])).toEqual([ + 0, + -1, + Number.NaN, + 0, + -1, + Number.NaN + ]) + }) +})