From 4dbb5832180787d3d51a7e526e49fd39f0f9acf8 Mon Sep 17 00:00:00 2001 From: Danila Poyarkov Date: Fri, 13 Mar 2026 19:05:48 +0300 Subject: [PATCH] Fix radial, angular, and diamond gradient rendering Use the full gradientTransform affine matrix via CanvasKit's localMatrix parameter instead of manually extracting center and radius. The previous approach ignored rotation and aspect ratio, producing circular gradients where Figma specifies rotated ellipses. Define gradients in Figma's unit gradient space (center 0.5,0.5, radius 0.5) and let the localMatrix (scale(w,h) * gradientTransform) handle the transform to pixel coordinates. Also merge the GRADIENT_DIAMOND branch with GRADIENT_RADIAL since they use the same shader. --- packages/core/src/renderer/fills.ts | 47 +++++++++++++---------------- 1 file changed, 21 insertions(+), 26 deletions(-) diff --git a/packages/core/src/renderer/fills.ts b/packages/core/src/renderer/fills.ts index d91975448..f7295c2b1 100644 --- a/packages/core/src/renderer/fills.ts +++ b/packages/core/src/renderer/fills.ts @@ -101,40 +101,35 @@ export function applyGradientFill(r: SkiaRenderer, fill: Fill, node: SceneNode): r.ck.TileMode.Clamp ) r.fillPaint.setShader(shader) - } else if (fill.type === 'GRADIENT_RADIAL') { - const cx = t.m02 * w - const cy = t.m12 * h - const radius = Math.sqrt(t.m00 * t.m00 + t.m10 * t.m10) * Math.max(w, h) - const shader = r.ck.Shader.MakeRadialGradient( - [cx, cy], - radius, - colors, - positions, - r.ck.TileMode.Clamp + } else if (fill.type === 'GRADIENT_RADIAL' || fill.type === 'GRADIENT_DIAMOND') { + // Figma's gradientTransform maps gradient space (center 0.5,0.5, radius 0.5) + // to the node's normalized [0,1] coordinate space. The full local matrix + // converts to pixel coordinates: scale(w, h) * gradientTransform. + const localMatrix = r.ck.Matrix.multiply( + r.ck.Matrix.scaled(w, h), + [t.m00, t.m01, t.m02, t.m10, t.m11, t.m12, 0, 0, 1] ) - r.fillPaint.setShader(shader) - } else if (fill.type === 'GRADIENT_ANGULAR') { - const cx = t.m02 * w - const cy = t.m12 * h - const shader = r.ck.Shader.MakeSweepGradient( - cx, - cy, + const shader = r.ck.Shader.MakeRadialGradient( + [0.5, 0.5], + 0.5, colors, positions, r.ck.TileMode.Clamp, - undefined + localMatrix ) r.fillPaint.setShader(shader) - } else if (fill.type === 'GRADIENT_DIAMOND') { - const cx = t.m02 * w - const cy = t.m12 * h - const radius = Math.sqrt(t.m00 * t.m00 + t.m10 * t.m10) * Math.max(w, h) - const shader = r.ck.Shader.MakeRadialGradient( - [cx, cy], - radius, + } else if (fill.type === 'GRADIENT_ANGULAR') { + const localMatrix = r.ck.Matrix.multiply( + r.ck.Matrix.scaled(w, h), + [t.m00, t.m01, t.m02, t.m10, t.m11, t.m12, 0, 0, 1] + ) + const shader = r.ck.Shader.MakeSweepGradient( + 0.5, + 0.5, colors, positions, - r.ck.TileMode.Clamp + r.ck.TileMode.Clamp, + localMatrix ) r.fillPaint.setShader(shader) }