Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix(): abort concurrent rendering #8218

Closed
wants to merge 11 commits into from
1 change: 0 additions & 1 deletion src/canvas.class.ts
Original file line number Diff line number Diff line change
Expand Up @@ -112,7 +112,6 @@ import { Point } from './point.class';
*/
initialize: function(el, options) {
options || (options = { });
this.renderAndResetBound = this.renderAndReset.bind(this);
this.requestRenderAllBound = this.requestRenderAll.bind(this);
this._initStatic(el, options);
this._initInteractive();
Expand Down
114 changes: 64 additions & 50 deletions src/static_canvas.class.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,9 +38,8 @@ import { removeFromArray } from './util/internals';
* @param {Object} [options] Options object
* @return {Object} thisArg
*/
initialize: function(el, options) {
options || (options = { });
this.renderAndResetBound = this.renderAndReset.bind(this);
initialize: function (el, options) {
options || (options = {});
this.requestRenderAllBound = this.requestRenderAll.bind(this);
this._initStatic(el, options);
},
Expand Down Expand Up @@ -171,7 +170,7 @@ import { removeFromArray } from './util/internals';
* The coordinates get updated with @method calcViewportBoundaries.
* @memberOf fabric.StaticCanvas.prototype
*/
vptCoords: { },
vptCoords: {},

/**
* Based on vptCoords and object.aCoords, skip rendering of objects that
Expand Down Expand Up @@ -199,8 +198,10 @@ import { removeFromArray } from './util/internals';
* @param {HTMLElement | String} el <canvas> element to initialize instance on
* @param {Object} [options] Options object
*/
_initStatic: function(el, options) {
_initStatic: function (el, options) {
this._objects = [];
// holds refs to active abort controllers that control rendering
this.__abortControllers = {};
this._createLowerCanvas(el);
this._initOptions(options);
// only initialize retina scaling once
Expand All @@ -213,22 +214,22 @@ import { removeFromArray } from './util/internals';
/**
* @private
*/
_isRetinaScaling: function() {
_isRetinaScaling: function () {
return (config.devicePixelRatio > 1 && this.enableRetinaScaling);
},

/**
* @private
* @return {Number} retinaScaling if applied, otherwise 1;
*/
getRetinaScaling: function() {
getRetinaScaling: function () {
return this._isRetinaScaling() ? Math.max(1, config.devicePixelRatio) : 1;
},

/**
* @private
*/
_initRetinaScaling: function() {
_initRetinaScaling: function () {
if (!this._isRetinaScaling()) {
return;
}
Expand All @@ -239,7 +240,7 @@ import { removeFromArray } from './util/internals';
}
},

__initRetinaScaling: function(scaleRatio, canvas, context) {
__initRetinaScaling: function (scaleRatio, canvas, context) {
canvas.setAttribute('width', this.width * scaleRatio);
canvas.setAttribute('height', this.height * scaleRatio);
context.scale(scaleRatio, scaleRatio);
Expand All @@ -260,13 +261,13 @@ import { removeFromArray } from './util/internals';
/**
* @private
*/
_createCanvasElement: function() {
_createCanvasElement: function () {
var element = createCanvasElement();
if (!element) {
throw CANVAS_INIT_ERROR;
}
if (!element.style) {
element.style = { };
element.style = {};
}
if (typeof element.getContext === 'undefined') {
throw CANVAS_INIT_ERROR;
Expand Down Expand Up @@ -472,9 +473,9 @@ import { removeFromArray } from './util/internals';
*/
setViewportTransform: function (vpt) {
var activeObject = this._activeObject,
backgroundObject = this.backgroundImage,
overlayObject = this.overlayImage,
object, i, len;
backgroundObject = this.backgroundImage,
overlayObject = this.overlayImage,
object, i, len;
this.viewportTransform = vpt;
for (i = 0, len = this._objects.length; i < len; i++) {
object = this._objects[i];
Expand Down Expand Up @@ -602,7 +603,7 @@ import { removeFromArray } from './util/internals';
* @private
* @param {fabric.Object} obj Object that was added
*/
_onObjectAdded: function(obj) {
_onObjectAdded: function (obj) {
this.stateful && obj.setupState();
if (obj.canvas && obj.canvas !== this) {
/* _DEV_MODE_START_ */
Expand All @@ -621,7 +622,7 @@ import { removeFromArray } from './util/internals';
* @private
* @param {fabric.Object} obj Object that was removed
*/
_onObjectRemoved: function(obj) {
_onObjectRemoved: function (obj) {
obj._set('canvas', undefined);
this.fire('object:removed', { target: obj });
obj.fire('removed', { target: this });
Expand All @@ -633,7 +634,7 @@ import { removeFromArray } from './util/internals';
* @return {fabric.Canvas} thisArg
* @chainable
*/
clearContext: function(ctx) {
clearContext: function (ctx) {
ctx.clearRect(0, 0, this.width, this.height);
return this;
},
Expand Down Expand Up @@ -674,26 +675,10 @@ import { removeFromArray } from './util/internals';
* @chainable
*/
renderAll: function () {
var canvasToDrawOn = this.contextContainer;
this.renderCanvas(canvasToDrawOn, this._objects);
this.renderCanvas(this.contextContainer, this._objects);
return this;
},

/**
* Function created to be instance bound at initialization
* used in requestAnimationFrame rendering
* Let the fabricJS call it. If you call it manually you could have more
* animationFrame stacking on to of each other
* for an imperative rendering, use canvas.renderAll
* @private
* @return {fabric.Canvas} instance
* @chainable
*/
renderAndReset: function() {
this.isRendering = 0;
this.renderAll();
},

/**
* Append a renderAll request to next animation frame.
* unless one is already in progress, in that case nothing is done
Expand All @@ -703,7 +688,25 @@ import { removeFromArray } from './util/internals';
*/
requestRenderAll: function () {
if (!this.isRendering) {
this.isRendering = fabric.util.requestAnimFrame(this.renderAndResetBound);
let handle;
new Promise((resolve, reject) => {
const controller = new AbortController();
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

polyfill needed for node<16

handle = this.isRendering = fabric.util.requestAnimFrame(resolve);
controller.signal.addEventListener('abort', (e) => {
this.cancelRequestedRender();
reject(e);
}, { once: true });
this.__abortControllers[handle] = controller;
}).then(() => {
this.isRendering = 0;
this.renderAll();
}).catch(error => {
if (error.type !== 'abort') {
throw error;
}
}).finally(() => {
delete this.__abortControllers[handle];
})
}
return this;
},
Expand All @@ -715,15 +718,15 @@ import { removeFromArray } from './util/internals';
* @return {Object} points.tl
* @chainable
*/
calcViewportBoundaries: function() {
calcViewportBoundaries: function () {
var width = this.width, height = this.height,
iVpt = invertTransform(this.viewportTransform),
a = transformPoint({ x: 0, y: 0 }, iVpt),
b = transformPoint({ x: width, y: height }, iVpt),
// we don't support vpt flipping
// but the code is robust enough to mostly work with flipping
min = a.min(b),
max = a.max(b);
iVpt = invertTransform(this.viewportTransform),
a = transformPoint({ x: 0, y: 0 }, iVpt),
b = transformPoint({ x: width, y: height }, iVpt),
// we don't support vpt flipping
// but the code is robust enough to mostly work with flipping
min = a.min(b),
max = a.max(b);
return this.vptCoords = {
tl: min,
tr: new Point(max.x, min.y),
Expand All @@ -732,13 +735,29 @@ import { removeFromArray } from './util/internals';
};
},

cancelRequestedRender: function() {
/**
* consider using {@link abortRendering} to abort concurrent rendering
*/
cancelRequestedRender: function () {
if (this.isRendering) {
fabric.util.cancelAnimFrame(this.isRendering);
this.isRendering = 0;
}
},

/**
* abort concurrent and requested rendering
*/
abortRendering: function () {
// first cancel requested rendering
this.cancelRequestedRender();
Object.values(this.__abortControllers)
.forEach(controller => {
controller.abort();
});
this.__abortControllers = {};
},

/**
* Renders background, objects, overlay and controls.
* @param {CanvasRenderingContext2D} ctx
Expand All @@ -748,7 +767,6 @@ import { removeFromArray } from './util/internals';
*/
renderCanvas: function(ctx, objects) {
var v = this.viewportTransform, path = this.clipPath;
this.cancelRequestedRender();
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is an important fix
Before, if you would called Canvas#toCanvasElement it would have cancelled requested renders. Or was it intentional.
I guess now there could be some nasty race condition.

this.calcViewportBoundaries();
this.clearContext(ctx);
fabric.util.setImageSmoothing(ctx, this.imageSmoothingEnabled);
Expand Down Expand Up @@ -1624,11 +1642,7 @@ import { removeFromArray } from './util/internals';
* @chainable
*/
dispose: function () {
// cancel eventually ongoing renders
if (this.isRendering) {
fabric.util.cancelAnimFrame(this.isRendering);
this.isRendering = 0;
}
this.abortRendering();
this.forEachObject(function(object) {
object.dispose && object.dispose();
});
Expand Down
2 changes: 1 addition & 1 deletion test/unit/brushes.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
var parsePath = fabric.util.parsePath;
QUnit.module('fabric.BaseBrush', function(hooks) {
hooks.afterEach(function() {
canvas.cancelRequestedRender();
canvas.abortRendering();
});

QUnit.test('fabric brush constructor', function(assert) {
Expand Down
6 changes: 3 additions & 3 deletions test/unit/canvas.js
Original file line number Diff line number Diff line change
Expand Up @@ -128,13 +128,13 @@
fabric.config.configure({ devicePixelRatio: ORIGINAL_DPR });
canvas.viewportTransform = [1, 0, 0, 1, 0, 0];
canvas.clear();
canvas.cancelRequestedRender();
canvas.abortRendering();
canvas.backgroundColor = fabric.Canvas.prototype.backgroundColor;
canvas.overlayColor = fabric.Canvas.prototype.overlayColor;
canvas._collectObjects = fabric.Canvas.prototype._collectObjects;
canvas.off();
canvas.calcOffset();
canvas.cancelRequestedRender();
canvas.abortRendering();
upperCanvasEl.style.display = 'none';
}
});
Expand Down Expand Up @@ -2127,7 +2127,7 @@
canvas.item(0).animate('scaleX', 10);
assert.equal(fabric.runningAnimations.length, 1, 'should have a running animation');
canvas.dispose();
canvas.cancelRequestedRender();
canvas.abortRendering();
assert.equal(fabric.runningAnimations.length, 0, 'dispose should clear running animations');
assert.equal(canvas.getObjects().length, 0, 'dispose should clear canvas');
assert.equal(parentEl.childNodes.length, 1, 'parent has always 1 child');
Expand Down
6 changes: 3 additions & 3 deletions test/unit/canvas_events.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@

QUnit.module('fabric.Canvas events mixin', {
beforeEach: function() {
canvas.cancelRequestedRender();
canvas.abortRendering();
canvas.viewportTransform = [1, 0, 0, 1, 0, 0];
upperCanvasEl.style.display = '';
canvas.controlsAboveOverlay = fabric.Canvas.prototype.controlsAboveOverlay;
Expand All @@ -29,7 +29,7 @@
canvas.setDimensions({ width: 600, height: 600 });
canvas.calcOffset();
upperCanvasEl.style.display = 'none';
canvas.cancelRequestedRender();
canvas.abortRendering();
}
});

Expand Down Expand Up @@ -855,7 +855,7 @@
fabric.document.dispatchEvent(event);
assert.equal(counter, 1, 'listener executed once');
fabric.Canvas.prototype._onMouseUp = originalMouseUp;
c.cancelRequestedRender();
c.abortRendering();
done();
}, 200);
});
Expand Down
Loading