From 2e08f7319f2961843cc2e00f146f7afa49fd8c74 Mon Sep 17 00:00:00 2001 From: Anton Lavrevov Date: Thu, 3 Apr 2025 13:02:58 -0500 Subject: [PATCH] fix caching when buffer is used. close #1886 fix svg length calculation. close #1869 --- src/Canvas.ts | 2 -- src/Node.ts | 26 +++++++++++++++++++++++--- src/Shape.ts | 10 ++++++++-- src/shapes/Path.ts | 18 +++++++++++++----- test/unit/Path-test.ts | 39 ++++++++++++++++++++++++++++++++++++++- test/unit/Shape-test.ts | 30 ++++++++++++++++++++++++++++++ test/unit/test-utils.ts | 8 ++++++++ 7 files changed, 120 insertions(+), 13 deletions(-) diff --git a/src/Canvas.ts b/src/Canvas.ts index 96002cca..b5d6a1ed 100644 --- a/src/Canvas.ts +++ b/src/Canvas.ts @@ -1,8 +1,6 @@ import { Util } from './Util'; import { SceneContext, HitContext, Context } from './Context'; import { Konva } from './Global'; -import { Factory } from './Factory'; -import { getNumberValidator } from './Validators'; // calculate pixel ratio let _pixelRatio; diff --git a/src/Node.ts b/src/Node.ts index 86ee06f7..c955fc8e 100644 --- a/src/Node.ts +++ b/src/Node.ts @@ -248,8 +248,8 @@ export abstract class Node { */ clearCache() { if (this._cache.has(CANVAS)) { - const { scene, filter, hit } = this._cache.get(CANVAS); - Util.releaseCanvas(scene, filter, hit); + const { scene, filter, hit, buffer } = this._cache.get(CANVAS); + Util.releaseCanvas(scene, filter, hit, buffer); this._cache.delete(CANVAS); } @@ -385,6 +385,18 @@ export abstract class Node { sceneContext = cachedSceneCanvas.getContext(), hitContext = cachedHitCanvas.getContext(); + const bufferCanvas = new SceneCanvas({ + // width and height already multiplied by pixelRatio + // so we need to revert that + // also increase size by x nd y offset to make sure content fits canvas + width: + cachedSceneCanvas.width / cachedSceneCanvas.pixelRatio + Math.abs(x), + height: + cachedSceneCanvas.height / cachedSceneCanvas.pixelRatio + Math.abs(y), + pixelRatio: cachedSceneCanvas.pixelRatio, + }), + bufferContext = bufferCanvas.getContext(); + cachedHitCanvas.isCache = true; cachedSceneCanvas.isCache = true; @@ -398,16 +410,23 @@ export abstract class Node { sceneContext.save(); hitContext.save(); + bufferContext.save(); sceneContext.translate(-x, -y); hitContext.translate(-x, -y); + bufferContext.translate(-x, -y); + // hard-code offset to make sure content fits canvas + // @ts-ignore + bufferCanvas.x = x; + // @ts-ignore + bufferCanvas.y = y; // extra flag to skip on getAbsolute opacity calc this._isUnderCache = true; this._clearSelfAndDescendantCache(ABSOLUTE_OPACITY); this._clearSelfAndDescendantCache(ABSOLUTE_SCALE); - this.drawScene(cachedSceneCanvas, this); + this.drawScene(cachedSceneCanvas, this, bufferCanvas); this.drawHit(cachedHitCanvas, this); this._isUnderCache = false; @@ -431,6 +450,7 @@ export abstract class Node { scene: cachedSceneCanvas, filter: cachedFilterCanvas, hit: cachedHitCanvas, + buffer: bufferCanvas, x: x, y: y, }); diff --git a/src/Shape.ts b/src/Shape.ts index 70305ec2..f567fe4b 100644 --- a/src/Shape.ts +++ b/src/Shape.ts @@ -602,7 +602,7 @@ export class Shape< stage, bufferContext; - const skipBuffer = canvas.isCache; + const skipBuffer = false; const cachingSelf = top === this; if (!this.isVisible() && !cachingSelf) { @@ -646,7 +646,13 @@ export class Shape< } context._applyOpacity(this); context._applyGlobalCompositeOperation(this); - context.drawImage(bc._canvas, 0, 0, bc.width / ratio, bc.height / ratio); + context.drawImage( + bc._canvas, + bc.x || 0, + bc.y || 0, + bc.width / ratio, + bc.height / ratio + ); } else { context._applyLineJoin(this); diff --git a/src/shapes/Path.ts b/src/shapes/Path.ts index e9e63493..d1a4f319 100644 --- a/src/shapes/Path.ts +++ b/src/shapes/Path.ts @@ -258,11 +258,19 @@ export class Path extends Shape { } if (length < 0.01) { - points = dataArray[i].points.slice(0, 2); - return { - x: points[0], - y: points[1], - }; + const cmd = dataArray[i].command; + if (cmd === 'M') { + points = dataArray[i].points.slice(0, 2); + return { + x: points[0], + y: points[1], + }; + } else { + return { + x: dataArray[i].start.x, + y: dataArray[i].start.y, + }; + } } const cp = dataArray[i]; diff --git a/test/unit/Path-test.ts b/test/unit/Path-test.ts index 4b3501f6..a90467ff 100644 --- a/test/unit/Path-test.ts +++ b/test/unit/Path-test.ts @@ -1083,6 +1083,7 @@ describe('Path', function () { it('get point at path', function () { var stage = addStage(); var layer = new Konva.Layer(); + stage.add(layer); const data = 'M 300,10 L 250,100 A 100 40 30 1 0 150 150 C 160,100, 290,100, 300,150'; var path = new Konva.Path({ @@ -1102,7 +1103,7 @@ describe('Path', function () { var circle = new Konva.Circle({ x: p.x, y: p.y, - radius: 2, + radius: 0.1, fill: 'black', stroke: 'black', }); @@ -1164,13 +1165,49 @@ describe('Path', function () { { x: 299.87435436448743, y: 149.4028482225714 }, ]); } + }); + it('get point at vertical path', function () { + var stage = addStage(); + var layer = new Konva.Layer(); + const data = 'M 614.96002,7.5147864 611.20262,429.59529'; + var path = new Konva.Path({ + stroke: 'red', + strokeWidth: 3, + data, + x: -600, + }); + layer.add(path); + if (isBrowser) { + const SVGPath = document.createElementNS( + 'http://www.w3.org/2000/svg', + 'path' + ) as SVGPathElement; + SVGPath.setAttribute('d', data); + for (var i = 0.001; i < path.getLength(); i += 1) { + var p = path.getPointAtLength(i); + console.log(p); + var circle = new Konva.Circle({ + x: p.x + path.x(), + y: p.y + path.y(), + radius: 2, + fill: 'black', + stroke: 'black', + }); + layer.add(circle); + const position = SVGPath.getPointAtLength(i); + console.log(position); + assert(Math.abs(p.x - position.x) <= 1); + assert(Math.abs(p.y - position.y) <= 1); + } + } stage.add(layer); }); it('get point at path with float attrs', function () { var stage = addStage(); var layer = new Konva.Layer(); + stage.add(layer); const data = 'M419.0000314094981 342.88624187900973 L419.00003140949804 577.0038889378335 L465.014001082264 577.0038889378336 Z'; diff --git a/test/unit/Shape-test.ts b/test/unit/Shape-test.ts index 70b1929d..cb866073 100644 --- a/test/unit/Shape-test.ts +++ b/test/unit/Shape-test.ts @@ -1597,6 +1597,36 @@ describe('Shape', function () { compareCanvases(canvas1, canvas2, 240, 110); }); + it('check stroke rendering on buffer canvas', async function () { + var stage = addStage(); + + var layer = new Konva.Layer(); + stage.add(layer); + + const rect = new Konva.Rect({ + x: 150, + y: 50, + width: 50, + height: 50, + fill: '#039BE5', + stroke: 'yellow', + strokeWidth: 5, + shadowColor: 'black', + shadowOffset: { x: 10, y: 10 }, + shadowOpacity: 0.5, + }); + + layer.add(rect); + + const canvas1 = layer.toCanvas(); + rect.cache(); + const canvas2 = layer.toCanvas(); + + // throw new Error('stop'); + + compareCanvases(canvas1, canvas2); + }); + // ====================================================== it('optional disable shadow for stroke', function () { var stage = addStage(); diff --git a/test/unit/test-utils.ts b/test/unit/test-utils.ts index 3738df2e..12e4df84 100644 --- a/test/unit/test-utils.ts +++ b/test/unit/test-utils.ts @@ -131,6 +131,14 @@ export function compareCanvases(canvas1, canvas2, tol?, secondTol?) { b.appendChild(canvas2); c.appendChild(diffCanvas); div.appendChild(b); + if (!canvas1.parentNode) { + const d = get('div', '
Original:
'); + canvas1.style.position = ''; + canvas1.style.display = ''; + d.style.float = 'left'; + d.appendChild(canvas1); + div.appendChild(d); + } div.appendChild(c); getContainer().appendChild(div); }