Skip to content

Commit 0a8d3c9

Browse files
committed
fix: keep descendant caches on clearCache() and release the previous canvases on re-cache
clearCache() on any container wiped the canvas cache of every descendant, even when the container itself was never cached, leaving filtered children unfiltered and their canvases unreleased. Re-caching a node also dropped its previous scene, filter and hit canvases without releasing them. The canvas cache now lives in its own field instead of the generic cache map, and one _releaseCanvasCache() serves clearCache() and cache().
1 parent ad94d9d commit 0a8d3c9

3 files changed

Lines changed: 94 additions & 52 deletions

File tree

src/Node.ts

Lines changed: 30 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -167,7 +167,6 @@ export type NodeConfig = {
167167
// CONSTANTS
168168
const ABSOLUTE_OPACITY = 'absoluteOpacity',
169169
ABSOLUTE_TRANSFORM = 'absoluteTransform',
170-
CANVAS = 'canvas',
171170
CHANGE = 'Change',
172171
CHILDREN = 'children',
173172
KONVA = 'konva',
@@ -274,6 +273,15 @@ export abstract class Node<Config extends NodeConfig = NodeConfig> {
274273
index = 0;
275274
parent: Container | null = null;
276275
_cache: Map<string, any> = new Map<string, any>();
276+
// canvases of cache(), see _getCanvasCache()
277+
_canvasCache: {
278+
scene: SceneCanvas;
279+
filter: SceneCanvas;
280+
hit: HitCanvas | null;
281+
hitConfig: { pixelRatio: number; width: number; height: number };
282+
x: number;
283+
y: number;
284+
} | null = null;
277285
_attachedDepsListeners: Map<string, boolean> = new Map<string, boolean>();
278286
_lastPos: Vector2d | null = null;
279287
_attrsAffectingSize!: string[];
@@ -317,6 +325,18 @@ export abstract class Node<Config extends NodeConfig = NodeConfig> {
317325
this._cache.clear();
318326
}
319327
}
328+
// drop the cached canvases and give their memory back
329+
_releaseCanvasCache() {
330+
const cache = this._canvasCache;
331+
if (cache) {
332+
Util.releaseCanvas(
333+
cache.scene._canvas,
334+
cache.filter._canvas,
335+
...(cache.hit ? [cache.hit._canvas] : [])
336+
);
337+
this._canvasCache = null;
338+
}
339+
}
320340
_getCache(attr: string, privateGetter: Function) {
321341
let cache = this._cache.get(attr);
322342

@@ -350,7 +370,7 @@ export abstract class Node<Config extends NodeConfig = NodeConfig> {
350370
}
351371

352372
_getCanvasCache() {
353-
return this._cache.get(CANVAS);
373+
return this._canvasCache;
354374
}
355375
/*
356376
* when the logic for a cached result depends on ancestor propagation, use this
@@ -386,16 +406,7 @@ export abstract class Node<Config extends NodeConfig = NodeConfig> {
386406
* node.clearCache();
387407
*/
388408
clearCache() {
389-
if (this._cache.has(CANVAS)) {
390-
const { scene, filter, hit } = this._cache.get(CANVAS);
391-
Util.releaseCanvas(
392-
scene._canvas,
393-
filter._canvas,
394-
...(hit ? [hit._canvas] : [])
395-
);
396-
this._cache.delete(CANVAS);
397-
}
398-
409+
this._releaseCanvasCache();
399410
this._clearSelfAndDescendantCache();
400411
this._requestDraw();
401412
return this;
@@ -522,7 +533,7 @@ export abstract class Node<Config extends NodeConfig = NodeConfig> {
522533

523534
cachedSceneCanvas.isCache = true;
524535

525-
this._cache.delete(CANVAS);
536+
this._releaseCanvasCache();
526537
this._filterUpToDate = false;
527538

528539
if (conf.imageSmoothingEnabled === false) {
@@ -558,7 +569,7 @@ export abstract class Node<Config extends NodeConfig = NodeConfig> {
558569
// the buffer is only needed while drawing
559570
Util.releaseCanvas(bufferCanvas._canvas);
560571

561-
this._cache.set(CANVAS, {
572+
this._canvasCache = {
562573
scene: cachedSceneCanvas,
563574
filter: cachedFilterCanvas,
564575
// the hit canvas is built on demand (see _getCachedHitCanvas)
@@ -570,7 +581,7 @@ export abstract class Node<Config extends NodeConfig = NodeConfig> {
570581
},
571582
x: x,
572583
y: y,
573-
});
584+
};
574585

575586
this._requestDraw();
576587

@@ -611,7 +622,7 @@ export abstract class Node<Config extends NodeConfig = NodeConfig> {
611622
* @returns {Boolean}
612623
*/
613624
isCached() {
614-
return this._cache.has(CANVAS);
625+
return !!this._canvasCache;
615626
}
616627

617628
abstract drawScene(canvas?: Canvas, top?: Node, bufferCanvas?: Canvas): void;
@@ -697,7 +708,7 @@ export abstract class Node<Config extends NodeConfig = NodeConfig> {
697708
context._applyOpacity(this);
698709
context._applyGlobalCompositeOperation(this);
699710

700-
const canvasCache = this._getCanvasCache();
711+
const canvasCache = this._getCanvasCache()!;
701712
context.translate(canvasCache.x, canvasCache.y);
702713

703714
const cacheCanvas = this._getCachedSceneCanvas();
@@ -713,7 +724,7 @@ export abstract class Node<Config extends NodeConfig = NodeConfig> {
713724
context.restore();
714725
}
715726
_drawCachedHitCanvas(context: Context, hitCanvas: HitCanvas) {
716-
const canvasCache = this._getCanvasCache();
727+
const canvasCache = this._getCanvasCache()!;
717728
context.save();
718729
context.translate(canvasCache.x, canvasCache.y);
719730
context.drawImage(
@@ -727,7 +738,7 @@ export abstract class Node<Config extends NodeConfig = NodeConfig> {
727738
}
728739
_getCachedSceneCanvas() {
729740
let filters = this.filters(),
730-
cachedCanvas = this._getCanvasCache(),
741+
cachedCanvas = this._getCanvasCache()!,
731742
sceneCanvas = cachedCanvas.scene as Canvas,
732743
filterCanvas = cachedCanvas.filter as Canvas,
733744
filterContext = filterCanvas.getContext(),

test/unit/Node-cache-test.ts

Lines changed: 46 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import {
99
createCanvasAndContext,
1010
loadImage,
1111
getPixelRatio,
12+
collectCanvasAllocations,
1213
} from './test-utils.ts';
1314

1415
describe('Caching', function () {
@@ -820,7 +821,7 @@ describe('Caching', function () {
820821
group.add(circle);
821822
group.cache();
822823

823-
const canvas = group._cache.get('canvas').scene;
824+
const canvas = group._getCanvasCache().scene;
824825
assert.equal(canvas.width, 106 * canvas.pixelRatio);
825826
});
826827

@@ -1279,13 +1280,18 @@ describe('Caching', function () {
12791280
maskgroup.add(mask);
12801281

12811282
maskgroup.cache();
1282-
var canvasBefore = maskgroup._cache.get('canvas').scene._canvas;
1283+
// re-caching releases the previous canvas, so keep a copy of it
1284+
var canvasBefore = maskgroup._getCanvasCache().scene._canvas;
1285+
const copyBefore = Konva.Util.createCanvasElement();
1286+
copyBefore.width = canvasBefore.width;
1287+
copyBefore.height = canvasBefore.height;
1288+
copyBefore.getContext('2d')!.drawImage(canvasBefore, 0, 0);
12831289

12841290
maskgroup.globalCompositeOperation('destination-in');
12851291
maskgroup.cache();
1286-
var canvasAfter = maskgroup._cache.get('canvas').scene._canvas;
1292+
var canvasAfter = maskgroup._getCanvasCache().scene._canvas;
12871293

1288-
compareCanvases(canvasBefore, canvasAfter);
1294+
compareCanvases(copyBefore, canvasAfter);
12891295

12901296
maskgroup.clearCache();
12911297

@@ -1395,7 +1401,7 @@ describe('Caching', function () {
13951401

13961402
layer.draw();
13971403
assert.equal(
1398-
bigCircle._cache.get('canvas').scene.getContext()._context
1404+
bigCircle._getCanvasCache().scene.getContext()._context
13991405
.imageSmoothingEnabled,
14001406
false
14011407
);
@@ -1449,16 +1455,16 @@ describe('Caching', function () {
14491455
layer.add(circle);
14501456
circle.cache();
14511457

1452-
assert.equal(circle._cache.get('canvas').filter.width, 0);
1458+
assert.equal(circle._getCanvasCache().filter.width, 0);
14531459
circle.filters([Konva.Filters.Blur]);
14541460
layer.draw();
14551461
assert.equal(
1456-
circle._cache.get('canvas').filter.width,
1457-
20 * circle._cache.get('canvas').filter.pixelRatio
1462+
circle._getCanvasCache().filter.width,
1463+
20 * circle._getCanvasCache().filter.pixelRatio
14581464
);
14591465
circle.filters([]);
14601466
// TODO: should we clear cache canvas?
1461-
// assert.equal(circle._cache.get('canvas').filter.width, 0);
1467+
// assert.equal(circle._getCanvasCache().filter.width, 0);
14621468
});
14631469

14641470
it('hit from cache + global composite', function (done) {
@@ -1516,7 +1522,7 @@ describe('Caching', function () {
15161522
});
15171523
layer.draw();
15181524

1519-
var hitCanvas = rect._cache.get('canvas').hit;
1525+
var hitCanvas = rect._getCanvasCache().hit;
15201526
assert.equal(hitCanvas._canvas.width, rect.width() * 0.2);
15211527
assert.equal(hitCanvas._canvas.height, rect.height() * 0.2);
15221528
assert.equal(hitCanvas.pixelRatio, 0.2);
@@ -1710,27 +1716,6 @@ describe('Caching', function () {
17101716
assert.equal(stage.getIntersection({ x: 20, y: 20 }), null);
17111717
});
17121718

1713-
// records the bitmap size of every canvas allocation made while fn runs
1714-
function collectCanvasAllocations(fn: () => void) {
1715-
const allocations: Array<{ canvas: any; width: number; height: number }> =
1716-
[];
1717-
const originalSetSize = Konva.Canvas.prototype.setSize;
1718-
Konva.Canvas.prototype.setSize = function (width, height) {
1719-
originalSetSize.call(this, width, height);
1720-
allocations.push({
1721-
canvas: this,
1722-
width: this.width,
1723-
height: this.height,
1724-
});
1725-
};
1726-
try {
1727-
fn();
1728-
} finally {
1729-
Konva.Canvas.prototype.setSize = originalSetSize;
1730-
}
1731-
return allocations.filter(({ width }) => width > 0);
1732-
}
1733-
17341719
it('cache() allocates only the cache canvas when no shape needs the buffer', function () {
17351720
// geometry far from the node's own origin used to inflate the buffer
17361721
// canvas by the offset (5100x5100 for a 102x102 cache)
@@ -1807,4 +1792,34 @@ describe('Caching', function () {
18071792
rect.position({ x: 0, y: 0 });
18081793
compareCanvases(exported, rect.toCanvas(), 10);
18091794
});
1795+
1796+
it('clearCache() on a parent keeps the caches of its children', function () {
1797+
var stage = addStage();
1798+
var layer = new Konva.Layer();
1799+
var group = new Konva.Group();
1800+
var rect = new Konva.Rect({
1801+
width: 50,
1802+
height: 50,
1803+
fill: 'red',
1804+
filters: [Konva.Filters.Invert],
1805+
});
1806+
group.add(rect);
1807+
layer.add(group);
1808+
stage.add(layer);
1809+
rect.cache();
1810+
1811+
group.clearCache();
1812+
1813+
assert.equal(rect.isCached(), true);
1814+
});
1815+
1816+
it('cache() releases the canvases of the previous cache', function () {
1817+
var rect = new Konva.Rect({ width: 50, height: 50, fill: 'red' });
1818+
rect.cache();
1819+
const previous = rect._getCanvasCache().scene;
1820+
1821+
rect.cache();
1822+
1823+
assert.equal(previous._canvas.width, 0, 'previous scene canvas released');
1824+
});
18101825
});

test/unit/test-utils.ts

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -169,6 +169,22 @@ export function compareLayers(layer1: Layer, layer2: Layer, tol?, secondTol?) {
169169
);
170170
}
171171

172+
// records the bitmap size of every canvas allocation made while fn runs
173+
export function collectCanvasAllocations(fn: () => void) {
174+
const allocations: Array<{ canvas: any; width: number; height: number }> = [];
175+
const originalSetSize = Konva.Canvas.prototype.setSize;
176+
Konva.Canvas.prototype.setSize = function (width, height) {
177+
originalSetSize.call(this, width, height);
178+
allocations.push({ canvas: this, width: this.width, height: this.height });
179+
};
180+
try {
181+
fn();
182+
} finally {
183+
Konva.Canvas.prototype.setSize = originalSetSize;
184+
}
185+
return allocations.filter(({ width }) => width > 0);
186+
}
187+
172188
export function createCanvasAndContext() {
173189
const canvas = Konva.Util.createCanvasElement();
174190
canvas.width = 578 * Konva.pixelRatio;
@@ -405,8 +421,8 @@ export const assertAlmostEqual = function (val1, val2, tol = 0.000001) {
405421
}
406422
};
407423

408-
export const assertAlmostDeepEqual = function (obj1, obj2) {
424+
export const assertAlmostDeepEqual = function (obj1, obj2, tol?) {
409425
for (var key1 in obj1) {
410-
assertAlmostEqual(obj1[key1], obj2[key1]);
426+
assertAlmostEqual(obj1[key1], obj2[key1], tol);
411427
}
412428
};

0 commit comments

Comments
 (0)