Skip to content

Commit

Permalink
Upper canvas retina scaling (fabricjs#5938)
Browse files Browse the repository at this point in the history
  • Loading branch information
cabaret authored and Hristo Chakarov committed Jul 27, 2021
1 parent a84e699 commit 54125cd
Show file tree
Hide file tree
Showing 6 changed files with 1,063 additions and 944 deletions.
17 changes: 11 additions & 6 deletions src/brushes/base_brush.class.js
Original file line number Diff line number Diff line change
Expand Up @@ -103,13 +103,18 @@ fabric.BaseBrush = fabric.util.createClass(/** @lends fabric.BaseBrush.prototype
return;
}

var ctx = this.canvas.contextTop,
zoom = this.canvas.getZoom();
var canvas = this.canvas,
shadow = this.shadow,
ctx = canvas.contextTop,
zoom = canvas.getZoom();
if (canvas && canvas._isRetinaScaling()) {
zoom *= fabric.devicePixelRatio;
}

ctx.shadowColor = this.shadow.color;
ctx.shadowBlur = this.shadow.blur * zoom;
ctx.shadowOffsetX = this.shadow.offsetX * zoom;
ctx.shadowOffsetY = this.shadow.offsetY * zoom;
ctx.shadowColor = shadow.color;
ctx.shadowBlur = shadow.blur * zoom;
ctx.shadowOffsetX = shadow.offsetX * zoom;
ctx.shadowOffsetY = shadow.offsetY * zoom;
},

needsFullRender: function() {
Expand Down
26 changes: 17 additions & 9 deletions src/canvas.class.js
Original file line number Diff line number Diff line change
Expand Up @@ -1347,6 +1347,12 @@
pointer = this.restorePointerVpt(pointer);
}

var retinaScaling = this.getRetinaScaling();
if (retinaScaling !== 1) {
pointer.x /= retinaScaling;
pointer.y /= retinaScaling;
}

if (boundsWidth === 0 || boundsHeight === 0) {
// If bounds are not available (i.e. not visible), do not apply scale.
cssScale = { width: 1, height: 1 };
Expand All @@ -1369,22 +1375,24 @@
* @throws {CANVAS_INIT_ERROR} If canvas can not be initialized
*/
_createUpperCanvas: function () {
var lowerCanvasClass = this.lowerCanvasEl.className.replace(/\s*lower-canvas\s*/, '');
var lowerCanvasClass = this.lowerCanvasEl.className.replace(/\s*lower-canvas\s*/, ''),
lowerCanvasEl = this.lowerCanvasEl, upperCanvasEl = this.upperCanvasEl;

// there is no need to create a new upperCanvas element if we have already one.
if (this.upperCanvasEl) {
this.upperCanvasEl.className = '';
if (upperCanvasEl) {
upperCanvasEl.className = '';
}
else {
this.upperCanvasEl = this._createCanvasElement();
upperCanvasEl = this._createCanvasElement();
this.upperCanvasEl = upperCanvasEl;
}
fabric.util.addClass(this.upperCanvasEl, 'upper-canvas ' + lowerCanvasClass);
fabric.util.addClass(upperCanvasEl, 'upper-canvas ' + lowerCanvasClass);

this.wrapperEl.appendChild(this.upperCanvasEl);
this.wrapperEl.appendChild(upperCanvasEl);

this._copyCanvasStyle(this.lowerCanvasEl, this.upperCanvasEl);
this._applyCanvasStyle(this.upperCanvasEl);
this.contextTop = this.upperCanvasEl.getContext('2d');
this._copyCanvasStyle(lowerCanvasEl, upperCanvasEl);
this._applyCanvasStyle(upperCanvasEl);
this.contextTop = upperCanvasEl.getContext('2d');
},

/**
Expand Down
14 changes: 11 additions & 3 deletions src/static_canvas.class.js
Original file line number Diff line number Diff line change
Expand Up @@ -276,12 +276,20 @@
if (!this._isRetinaScaling()) {
return;
}
this.lowerCanvasEl.setAttribute('width', this.width * fabric.devicePixelRatio);
this.lowerCanvasEl.setAttribute('height', this.height * fabric.devicePixelRatio);
var scaleRatio = fabric.devicePixelRatio;
this.__initRetinaScaling(scaleRatio, this.lowerCanvasEl, this.contextContainer);
if (this.upperCanvasEl) {
this.__initRetinaScaling(scaleRatio, this.upperCanvasEl, this.contextTop);
}
},

this.contextContainer.scale(fabric.devicePixelRatio, fabric.devicePixelRatio);
__initRetinaScaling: function(scaleRatio, canvas, context) {
canvas.setAttribute('width', this.width * scaleRatio);
canvas.setAttribute('height', this.height * scaleRatio);
context.scale(scaleRatio, scaleRatio);
},


/**
* Calculates canvas element offset relative to the document
* This method is also attached as "resize" event handler of window
Expand Down
151 changes: 80 additions & 71 deletions test/unit/brushes.js
Original file line number Diff line number Diff line change
@@ -1,79 +1,88 @@
(function() {
var canvas = new fabric.Canvas();
QUnit.module('fabric.BaseBrush', {
afterEach: function() {
QUnit.module('fabric.BaseBrush', function(hooks) {
hooks.afterEach(function() {
canvas.cancelRequestedRender();
}
});
QUnit.test('fabric brush constructor', function(assert) {
assert.ok(fabric.BaseBrush);
});

var brush = new fabric.BaseBrush();
QUnit.test('fabric brush constructor', function(assert) {
assert.ok(fabric.BaseBrush);

assert.ok(brush instanceof fabric.BaseBrush, 'should inherit from fabric.BaseBrush');
assert.equal(brush.color, 'rgb(0, 0, 0)', 'default color is black');
assert.equal(brush.width, 1, 'default width is 1');
assert.ok(typeof brush.setShadow === 'function', 'setShadow is a method');
});
QUnit.test('fabric pencil brush constructor', function(assert) {
assert.ok(fabric.PencilBrush);
var brush = new fabric.PencilBrush(canvas);
assert.equal(brush.canvas, canvas, 'assigns canvas');
assert.deepEqual(brush._points, [], 'points is an empty array');
});
QUnit.test('fabric pencil brush draw point', function(assert) {
var brush = new fabric.PencilBrush(canvas);
var pointer = canvas.getPointer({ clientX: 10, clientY: 10});
brush.onMouseDown(pointer, { e: {} });
var pathData = brush.convertPointsToSVGPath(brush._points).join('');
assert.equal(pathData, 'M 9.999 10 L 10.001 10', 'path data create a small line that looks like a point');
});
QUnit.test('fabric pencil brush multiple points', function(assert) {
var brush = new fabric.PencilBrush(canvas);
var pointer = canvas.getPointer({ clientX: 10, clientY: 10});
brush.onMouseDown(pointer, { e: {} });
brush.onMouseMove(pointer, { e: {} });
brush.onMouseMove(pointer, { e: {} });
brush.onMouseMove(pointer, { e: {} });
brush.onMouseMove(pointer, { e: {} });
var pathData = brush.convertPointsToSVGPath(brush._points).join('');
assert.equal(pathData, 'M 9.999 10 L 10.001 10', 'path data create a small line that looks like a point');
assert.equal(brush._points.length, 2, 'concident points are discarded');
});
QUnit.test('fabric pencil brush multiple points not discarded', function(assert) {
var brush = new fabric.PencilBrush(canvas);
var pointer = canvas.getPointer({ clientX: 10, clientY: 10});
var pointer2 = canvas.getPointer({ clientX: 15, clientY: 15});
var pointer3 = canvas.getPointer({ clientX: 20, clientY: 20});
brush.onMouseDown(pointer, { e: {} });
brush.onMouseMove(pointer2, { e: {} });
brush.onMouseMove(pointer3, { e: {} });
brush.onMouseMove(pointer2, { e: {} });
brush.onMouseMove(pointer3, { e: {} });
var pathData = brush.convertPointsToSVGPath(brush._points).join('');
assert.equal(pathData, 'M 9.999 9.999 Q 10 10 12.5 12.5 Q 15 15 17.5 17.5 Q 20 20 17.5 17.5 Q 15 15 17.5 17.5 L 20.001 20.001', 'path data create a complex path');
assert.equal(brush._points.length, 6, 'concident points are discarded');
});
QUnit.test('fabric pencil brush multiple points not discarded', function(assert) {
var fired = false;
var added = null;
canvas.on('path:created', function(opt) {
fired = true;
added = opt.path;
var brush = new fabric.BaseBrush();

assert.ok(brush instanceof fabric.BaseBrush, 'should inherit from fabric.BaseBrush');
assert.equal(brush.color, 'rgb(0, 0, 0)', 'default color is black');
assert.equal(brush.width, 1, 'default width is 1');
assert.ok(typeof brush.setShadow === 'function', 'setShadow is a method');
});
QUnit.test('fabric pencil brush constructor', function(assert) {
assert.ok(fabric.PencilBrush);
var brush = new fabric.PencilBrush(canvas);
assert.equal(brush.canvas, canvas, 'assigns canvas');
assert.deepEqual(brush._points, [], 'points is an empty array');
});

[true, false].forEach(function(val) {
QUnit.module('fabric.BaseBrush with canvas.enableRetinaScaling = ' + val, function(hooks) {
hooks.beforeEach(function() {
canvas.enableRetinaScaling = val;
});
QUnit.test('fabric pencil brush draw point', function(assert) {
var brush = new fabric.PencilBrush(canvas);
var pointer = canvas.getPointer({ clientX: 10, clientY: 10});
brush.onMouseDown(pointer, { e: {} });
var pathData = brush.convertPointsToSVGPath(brush._points).join('');
assert.equal(pathData, 'M 9.999 10 L 10.001 10', 'path data create a small line that looks like a point');
});
QUnit.test('fabric pencil brush multiple points', function(assert) {
var brush = new fabric.PencilBrush(canvas);
var pointer = canvas.getPointer({ clientX: 10, clientY: 10});
brush.onMouseDown(pointer, { e: {} });
brush.onMouseMove(pointer, { e: {} });
brush.onMouseMove(pointer, { e: {} });
brush.onMouseMove(pointer, { e: {} });
brush.onMouseMove(pointer, { e: {} });
var pathData = brush.convertPointsToSVGPath(brush._points).join('');
assert.equal(pathData, 'M 9.999 10 L 10.001 10', 'path data create a small line that looks like a point');
assert.equal(brush._points.length, 2, 'concident points are discarded');
});
QUnit.test('fabric pencil brush multiple points not discarded', function(assert) {
var brush = new fabric.PencilBrush(canvas);
var pointer = canvas.getPointer({ clientX: 10, clientY: 10});
var pointer2 = canvas.getPointer({ clientX: 15, clientY: 15});
var pointer3 = canvas.getPointer({ clientX: 20, clientY: 20});
brush.onMouseDown(pointer, { e: {} });
brush.onMouseMove(pointer2, { e: {} });
brush.onMouseMove(pointer3, { e: {} });
brush.onMouseMove(pointer2, { e: {} });
brush.onMouseMove(pointer3, { e: {} });
var pathData = brush.convertPointsToSVGPath(brush._points).join('');
assert.equal(pathData, 'M 9.999 9.999 Q 10 10 12.5 12.5 Q 15 15 17.5 17.5 Q 20 20 17.5 17.5 Q 15 15 17.5 17.5 L 20.001 20.001', 'path data create a complex path');
assert.equal(brush._points.length, 6, 'concident points are discarded');
});
QUnit.test('fabric pencil brush multiple points not discarded', function(assert) {
var fired = false;
var added = null;
canvas.on('path:created', function(opt) {
fired = true;
added = opt.path;
});
var brush = new fabric.PencilBrush(canvas);
var pointer = canvas.getPointer({ clientX: 10, clientY: 10});
var pointer2 = canvas.getPointer({ clientX: 15, clientY: 15});
var pointer3 = canvas.getPointer({ clientX: 20, clientY: 20});
brush.onMouseDown(pointer, { e: {} });
brush.onMouseMove(pointer2, { e: {} });
brush.onMouseMove(pointer3, { e: {} });
brush.onMouseMove(pointer2, { e: {} });
brush.onMouseMove(pointer3, { e: {} });
brush.onMouseUp({ e: {} });
assert.equal(fired, true, 'event is fired');
assert.ok(added instanceof fabric.Path, 'a path is added');
assert.ok(added.path.length, 6, 'path has 6 steps');
canvas.off();
});
});
});
var brush = new fabric.PencilBrush(canvas);
var pointer = canvas.getPointer({ clientX: 10, clientY: 10});
var pointer2 = canvas.getPointer({ clientX: 15, clientY: 15});
var pointer3 = canvas.getPointer({ clientX: 20, clientY: 20});
brush.onMouseDown(pointer, { e: {} });
brush.onMouseMove(pointer2, { e: {} });
brush.onMouseMove(pointer3, { e: {} });
brush.onMouseMove(pointer2, { e: {} });
brush.onMouseMove(pointer3, { e: {} });
brush.onMouseUp({ e: {} });
assert.equal(fired, true, 'event is fired');
assert.ok(added instanceof fabric.Path, 'a path is added');
assert.ok(added.path.length, 6, 'path has 6 steps');
canvas.off();
});
})();
Loading

0 comments on commit 54125cd

Please sign in to comment.