From 0d5db351473cdf4d5d262242c4f1a8a7ff8a099c Mon Sep 17 00:00:00 2001 From: Danila Poyarkov Date: Sat, 23 May 2026 01:10:24 +0300 Subject: [PATCH] perf(canvas): bound blend and text mask layers --- packages/core/src/canvas/scene.ts | 21 ++++++++++++++++++--- tests/engine/render/canvas/blend.test.ts | 4 +++- 2 files changed, 21 insertions(+), 4 deletions(-) diff --git a/packages/core/src/canvas/scene.ts b/packages/core/src/canvas/scene.ts index 50fd658eb..056e6b57f 100644 --- a/packages/core/src/canvas/scene.ts +++ b/packages/core/src/canvas/scene.ts @@ -2,6 +2,7 @@ import type { Canvas, Path } from 'canvaskit-wasm' import { DROP_HIGHLIGHT_ALPHA, DROP_HIGHLIGHT_STROKE, SECTION_CORNER_RADIUS } from '#core/constants' +import { computeDescendantVisualBounds } from '#core/geometry' import type { SceneNode, SceneGraph, Fill } from '#core/scene-graph' import type { Color } from '#core/types' import { vectorNetworkToCenterlinePath } from '#core/vector' @@ -212,9 +213,22 @@ export function renderNode( const needsNodeLayer = node.opacity < 1 || needsIsolatedBlendLayer(node.blendMode) if (needsNodeLayer) { + const bounds = computeDescendantVisualBounds( + [nodeId], + (id) => graph.getNode(id) ?? undefined, + (id) => graph.getAbsolutePosition(id) + ) + const layerBounds = bounds + ? r.ck.LTRBRect( + bounds.minX - absX, + bounds.minY - absY, + bounds.maxX - absX, + bounds.maxY - absY + ) + : r.ck.LTRBRect(0, 0, node.width, node.height) r.opacityPaint.setAlphaf(node.opacity) r.opacityPaint.setBlendMode(figmaBlendModeToSkia(r.ck, node.blendMode)) - canvas.saveLayer(r.opacityPaint) + canvas.saveLayer(r.opacityPaint, layerBounds) } const layerBlur = node.effects.find( @@ -589,11 +603,12 @@ function drawGradientText( r.effectLayerPaint.setImageFilter(null) r.effectLayerPaint.setColorFilter(null) r.effectLayerPaint.setBlendMode(r.ck.BlendMode.SrcOver) - canvas.saveLayer(r.effectLayerPaint) + const bounds = r.ck.LTRBRect(0, paragraphY, node.width, paragraphY + node.height) + canvas.saveLayer(r.effectLayerPaint, bounds) canvas.drawParagraph(paragraph, 0, paragraphY) r.effectLayerPaint.setBlendMode(r.ck.BlendMode.SrcIn) - canvas.saveLayer(r.effectLayerPaint) + canvas.saveLayer(r.effectLayerPaint, bounds) canvas.drawRect(r.ck.LTRBRect(0, 0, node.width, node.height), r.fillPaint) canvas.restore() canvas.restore() diff --git a/tests/engine/render/canvas/blend.test.ts b/tests/engine/render/canvas/blend.test.ts index 326b059e5..11bb5a000 100644 --- a/tests/engine/render/canvas/blend.test.ts +++ b/tests/engine/render/canvas/blend.test.ts @@ -1,4 +1,5 @@ import { describe, expect, mock, test } from 'bun:test' + import type { Canvas } from 'canvaskit-wasm' import { figmaBlendModeToSkia } from '#core/canvas/blend' @@ -123,7 +124,8 @@ describe('canvas blend modes', () => { expect(renderer.opacityPaint.setAlphaf).toHaveBeenCalledWith(1) expect(renderer.opacityPaint.setBlendMode).toHaveBeenCalledWith('Multiply') - expect(canvas.saveLayer).toHaveBeenCalledWith(renderer.opacityPaint) + expect(canvas.saveLayer).toHaveBeenCalledWith(renderer.opacityPaint, expect.any(Float32Array)) + expect(renderer.ck.LTRBRect).toHaveBeenCalledWith(0, 0, 100, 100) expect(renderer.renderShape).toHaveBeenCalled() expect(renderer.opacityPaint.setBlendMode).toHaveBeenLastCalledWith('SrcOver') })