Reputation: 2190
How can I pan and zoom using fabricjs? I've tried using the methods zoomToPoint and setZoom but they do not work for panning. Once I start using different zoom points I start having trouble.
$('#zoomIn').click(function(){
canvas.setZoom(canvas.getZoom() * 1.1 ) ;
}) ;
$('#zoomOut').click(function(){
canvas.setZoom(canvas.getZoom() / 1.1 ) ;
}) ;
$('#goRight').click(function(){
//Need to implement
}) ;
$('#goLeft').click(function(){
//Need to implement
}) ;
http://jsfiddle.net/hdramos/ux16013L/
Upvotes: 23
Views: 28876
Reputation: 56
I solved this using CSS transform. I made a demo comparing drag and zoom using Fabric.js versus using CSS transform: https://codepen.io/Fjonan/pen/azoWXWJ The demo also includes working code four touch drag an pinch gesture.
Tl;dr: CSS transform ist vastly more performant no matter the complexity of the canvas.
I had serious performance problems when implementing drag and zoom using Fabric.js on canvases with lots of elements. I solved it by using CSS transform: translate()
and scale()
instead. For my purpose it was fine to move the canvas inside a wrapper element with overflow: hidden
, so I would not need to re-draw the content when moving the canvas around.
However when you scale
the canvas, the image gets blurry so after zoom you need to re-draw the content so the image gets sharp again.
Since the CSS transform solution is much better in my opinion I will go with that here. If you insist on doing with with Fabric.js you can see the code for that in my CodePen demo.
Setup something like this:
<section class="canvas-wrapper overflow-hidden position-relative">
<canvas id=canvas>
</canvas>
</section>
Fabric.js will create its own wrapper element canvas-container
which I access here using canvas.wrapperEl
.
This code handles dragging:
const wrapper = document.querySelector('.canvas-wrapper')
const canvas = new fabric.Canvas("canvas",{
allowTouchScrolling: false,
defaultCursor: 'grab',
selection: false,
// …
})
let lastPosX,
lastPosY
canvas.on("mouse:down", dragCanvasStart)
canvas.on("mouse:move", dragCanvas)
/**
* Save reference point from which the interaction started
*/
function dragCanvasStart(event) {
const evt = event.e // retrieving original event from fabricJS event
// save thew position you started dragging from
lastPosX = evt.clientX
lastPosY = evt.clientY
}
/**
* Start Dragging the Canvas using Fabric JS Events
*/
function dragCanvas(event) {
const evt = event.e // retrieving original event from fabricJS event
if (1 !== evt.buttons) { // left mouse button is pressed
return
}
translateCanvas(evt)
}
/**
* Convert movement to CSS translate which visually moves the canvas
*/
function translateCanvas(event) {
const transform = getTransformVals(canvas.wrapperEl)
let offsetX = transform.translateX + (event.clientX - (lastPosX || 0))
let offsetY = transform.translateY + (event.clientY - (lastPosY || 0))
const viewBox = wrapper.getBoundingClientRect()
canvas.wrapperEl.style.transform = `translate(${tVals.translateX}px, ${tVals.translateY}px) scale(${transform.scaleX})`
lastPosX = event.clientX
lastPosY = event.clientY
}
/**
* Get relevant style values for the given element
* @see https://stackoverflow.com/a/64654744/13221239
*/
function getTransformVals(element) {
const style = window.getComputedStyle(element)
const matrix = new DOMMatrixReadOnly(style.transform)
return {
scaleX: matrix.m11,
scaleY: matrix.m22,
translateX: matrix.m41,
translateY: matrix.m42,
width: element.getBoundingClientRect().width,
height: element.getBoundingClientRect().height,
}
}
And this code will handle zoom:
let touchZoom
canvas.on('mouse:wheel', zoomCanvasMouseWheel)
// after scaling transform the CSS to canvas zoom so it does not stay blurry
// @see https://lodash.com/docs/4.17.15#debounce
const debouncedScale2Zoom = _.debounce(canvasScaleToZoom, 1000)
/**
* Zoom canvas when user used mouse wheel
*/
function zoomCanvasMouseWheel(event) {
const delta = event.e.deltaY
let zoom = touchZoom
zoom *= 0.999 ** delta
const point = {x: event.e.offsetX, y: event.e.offsetY}
scaleCanvas(zoom, point)
debouncedScale2Zoom()
}
/**
* Convert zoom to CSS scale which visually zooms the canvas
*/
function scaleCanvas(zoom, aroundPoint) {
const tVals = getTransformVals(canvas.wrapperEl)
const scaleFactor = tVals.scaleX / touchZoom * zoom
// see CSS: transform-origin: var(--tOriginX) var(--tOriginY);
canvas.wrapperEl.style.setProperty('--tOriginX', `${aroundPoint.x}px`)
canvas.wrapperEl.style.setProperty('--tOriginY', `${aroundPoint.y}px`)
canvas.wrapperEl.style.transform = `translate(${tVals.translateX}px, ${tVals.translateY}px) scale(${scaleFactor})`
touchZoom = zoom
}
/**
* Converts CSS transform to Fabric.js zoom so the blurry image gets sharp
*/
function canvasScaleToZoom() {
const transform = getTransformVals(canvas.wrapperEl)
const canvasBox = canvas.wrapperEl.getBoundingClientRect()
const viewBox = wrapper.getBoundingClientRect()
// calculate the offset of the canvas inside the wrapper
const offsetX = canvasBox.x - viewBox.x
const offsetY = canvasBox.y - viewBox.y
// we resizen the canvas to the scaled values
canvas.setHeight(transform.height)
canvas.setWidth(transform.width)
canvas.setZoom(touchZoom)
// and reset the transform values
canvas.wrapperEl.style.transform = `translate(${offsetX}px, ${offsetY}px) scale(1)`
canvas.wrapperEl.style.setProperty('--tOriginX', '0px')
canvas.wrapperEl.style.setProperty('--tOriginY', '0px')
canvas.renderAll()
}
Additional note on performance: When working on this I came across an issue with performance when zooming on mobile devices. After a helpful discussion I could solve it by setting these two properties on the objects in the canvas:
new Rect({
// …
objectCaching: false, // important for mobile performance
noScaleCache: true, // important for mobile performance
})
I want to highlight this quote from Asturur, a maintainer of Fabric.js:
if you are using simple shapes that are a single draw call ( rect, circle, small polygon, ellipse, triangle ) and you are using only fill or only stroke, caching is at loss
So consider turning off caching for these kind of shapes to optimize performance. Strongly depends on your circtumstances.
I created a more extensive write up with extra code and reasoning if anybody needs more information.
Upvotes: 0
Reputation: 96
Zoom to center for button with percentage steps and limiter with the method :
Canvas.zoomToPoint(point: Point, value: number, skipSetCoords?: boolean): void
const zoomCanvas = (cmd: "+" | "-") => {
let: percent = Math.round(canvas.getZoom()*100) //starting zoom level in percentage
const step = 5 // zoom step in percentage
const max = 500 // max zoom level in percentage
const min = 5 // min zoom level in percentage
let zoom = 5
if(cmd === "+" && percent <= max){
zoom = Math.min(percent + step, max) // if command is '+' and percentage is lower than max then zoom in
}
else if (cmd === "-" && percent >= min){
zoom = Math.max(percent - step, min) // if command is '-' and percentage is greater than min then zoom out
} else { return }
const point = canvas.getCenterPoint() // get canvas centerPoint or any fabric.Point
canvas.zoomToPoint(point, zoom / 100) // zoom to point
}
Set a custom zoom percentage with limiter
const setCanvasZoomPercentage = (value: number) => {
const center = canvas.getCenterPoint(); // get canvas centerPoint or any fabric.Point
const max = 500; // max zoom level in percentage
const min = 5; // min zoom level in percentage
let zoom = value;
if (zoom > max) v = max; // if value is greater than max
if (zoom < min) v = min; // if value is lower than min
canvas.zoomToPoint(center, zoom / 100);// center or any fabric.Point
}
You can adapt min, max and step
Upvotes: 0
Reputation: 389
I have an example on Github using fabric.js Canvas panning and zooming: https://sabatinomasala.github.io/fabric-clipping-demo/
The code responsible for the panning behaviour is the following: https://github.com/SabatinoMasala/fabric-clipping-demo/blob/master/src/classes/Panning.js
It's a simple extension on the fabric.Canvas.prototype
, which enables you to toggle 'drag mode' on the canvas as follows:
canvas.toggleDragMode(true); // Start panning
canvas.toggleDragMode(false); // Stop panning
Take a look at the following snippet, documentation is available throughout the code.
const STATE_IDLE = 'idle';
const STATE_PANNING = 'panning';
fabric.Canvas.prototype.toggleDragMode = function(dragMode) {
// Remember the previous X and Y coordinates for delta calculations
let lastClientX;
let lastClientY;
// Keep track of the state
let state = STATE_IDLE;
// We're entering dragmode
if (dragMode) {
// Discard any active object
this.discardActiveObject();
// Set the cursor to 'move'
this.defaultCursor = 'move';
// Loop over all objects and disable events / selectable. We remember its value in a temp variable stored on each object
this.forEachObject(function(object) {
object.prevEvented = object.evented;
object.prevSelectable = object.selectable;
object.evented = false;
object.selectable = false;
});
// Remove selection ability on the canvas
this.selection = false;
// When MouseUp fires, we set the state to idle
this.on('mouse:up', function(e) {
state = STATE_IDLE;
});
// When MouseDown fires, we set the state to panning
this.on('mouse:down', (e) => {
state = STATE_PANNING;
lastClientX = e.e.clientX;
lastClientY = e.e.clientY;
});
// When the mouse moves, and we're panning (mouse down), we continue
this.on('mouse:move', (e) => {
if (state === STATE_PANNING && e && e.e) {
// let delta = new fabric.Point(e.e.movementX, e.e.movementY); // No Safari support for movementX and movementY
// For cross-browser compatibility, I had to manually keep track of the delta
// Calculate deltas
let deltaX = 0;
let deltaY = 0;
if (lastClientX) {
deltaX = e.e.clientX - lastClientX;
}
if (lastClientY) {
deltaY = e.e.clientY - lastClientY;
}
// Update the last X and Y values
lastClientX = e.e.clientX;
lastClientY = e.e.clientY;
let delta = new fabric.Point(deltaX, deltaY);
this.relativePan(delta);
this.trigger('moved');
}
});
} else {
// When we exit dragmode, we restore the previous values on all objects
this.forEachObject(function(object) {
object.evented = (object.prevEvented !== undefined) ? object.prevEvented : object.evented;
object.selectable = (object.prevSelectable !== undefined) ? object.prevSelectable : object.selectable;
});
// Reset the cursor
this.defaultCursor = 'default';
// Remove the event listeners
this.off('mouse:up');
this.off('mouse:down');
this.off('mouse:move');
// Restore selection ability on the canvas
this.selection = true;
}
};
// Create the canvas
let canvas = new fabric.Canvas('fabric')
canvas.backgroundColor = '#f1f1f1';
// Add a couple of rects
let rect = new fabric.Rect({
width: 100,
height: 100,
fill: '#f00'
});
canvas.add(rect)
rect = new fabric.Rect({
width: 200,
height: 200,
top: 200,
left: 200,
fill: '#f00'
});
canvas.add(rect)
// Handle dragmode change
let dragMode = false;
$('#dragmode').change(_ => {
dragMode = !dragMode;
canvas.toggleDragMode(dragMode);
});
<div>
<label for="dragmode">
Enable panning
<input type="checkbox" id="dragmode" name="dragmode" />
</label>
</div>
<canvas width="300" height="300" id="fabric"></canvas>
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/fabric.js/1.7.15/fabric.min.js"></script>
Upvotes: 4
Reputation: 4420
Here is my solution for canvas zoom (using mouse wheel) and pan (using left/up/right /down keys or shift key + mouse left down + mouse move).
https://jsfiddle.net/milanhlinak/7s4w0uLy/8/
<!DOCTYPE html>
<html>
<head>
<script type="text/javascript" src="lib/jquery-3.1.1.min.js"></script>
<script type="text/javascript" src="lib/fabric.min.js"></script>
</head>
<body>
<canvas id="canvas" style="border: 1px solid #cccccc"></canvas>
<script>
var Direction = {
LEFT: 0,
UP: 1,
RIGHT: 2,
DOWN: 3
};
var zoomLevel = 0;
var zoomLevelMin = 0;
var zoomLevelMax = 3;
var shiftKeyDown = false;
var mouseDownPoint = null;
var canvas = new fabric.Canvas('canvas', {
width: 500,
height: 500,
selectionKey: 'ctrlKey'
});
canvas.add(new fabric.Rect({
left: 100,
top: 100,
width: 50,
height: 50,
fill: '#faa'
}));
canvas.add(new fabric.Rect({
left: 300,
top: 300,
width: 50,
height: 50,
fill: '#afa'
}));
canvas.on('mouse:down', function (options) {
var pointer = canvas.getPointer(options.e, true);
mouseDownPoint = new fabric.Point(pointer.x, pointer.y);
});
canvas.on('mouse:up', function (options) {
mouseDownPoint = null;
});
canvas.on('mouse:move', function (options) {
if (shiftKeyDown && mouseDownPoint) {
var pointer = canvas.getPointer(options.e, true);
var mouseMovePoint = new fabric.Point(pointer.x, pointer.y);
canvas.relativePan(mouseMovePoint.subtract(mouseDownPoint));
mouseDownPoint = mouseMovePoint;
keepPositionInBounds(canvas);
}
});
fabric.util.addListener(document.body, 'keydown', function (options) {
if (options.repeat) {
return;
}
var key = options.which || options.keyCode; // key detection
if (key == 16) { // handle Shift key
canvas.defaultCursor = 'move';
canvas.selection = false;
shiftKeyDown = true;
} else if (key === 37) { // handle Left key
move(Direction.LEFT);
} else if (key === 38) { // handle Up key
move(Direction.UP);
} else if (key === 39) { // handle Right key
move(Direction.RIGHT);
} else if (key === 40) { // handle Down key
move(Direction.DOWN);
}
});
fabric.util.addListener(document.body, 'keyup', function (options) {
var key = options.which || options.keyCode; // key detection
if (key == 16) { // handle Shift key
canvas.defaultCursor = 'default';
canvas.selection = true;
shiftKeyDown = false;
}
});
jQuery('.canvas-container').on('mousewheel', function (options) {
var delta = options.originalEvent.wheelDelta;
if (delta != 0) {
var pointer = canvas.getPointer(options.e, true);
var point = new fabric.Point(pointer.x, pointer.y);
if (delta > 0) {
zoomIn(point);
} else if (delta < 0) {
zoomOut(point);
}
}
});
function move(direction) {
switch (direction) {
case Direction.LEFT:
canvas.relativePan(new fabric.Point(-10 * canvas.getZoom(), 0));
break;
case Direction.UP:
canvas.relativePan(new fabric.Point(0, -10 * canvas.getZoom()));
break;
case Direction.RIGHT:
canvas.relativePan(new fabric.Point(10 * canvas.getZoom(), 0));
break;
case Direction.DOWN:
canvas.relativePan(new fabric.Point(0, 10 * canvas.getZoom()));
break;
}
keepPositionInBounds(canvas);
}
function zoomIn(point) {
if (zoomLevel < zoomLevelMax) {
zoomLevel++;
canvas.zoomToPoint(point, Math.pow(2, zoomLevel));
keepPositionInBounds(canvas);
}
}
function zoomOut(point) {
if (zoomLevel > zoomLevelMin) {
zoomLevel--;
canvas.zoomToPoint(point, Math.pow(2, zoomLevel));
keepPositionInBounds(canvas);
}
}
function keepPositionInBounds() {
var zoom = canvas.getZoom();
var xMin = (2 - zoom) * canvas.getWidth() / 2;
var xMax = zoom * canvas.getWidth() / 2;
var yMin = (2 - zoom) * canvas.getHeight() / 2;
var yMax = zoom * canvas.getHeight() / 2;
var point = new fabric.Point(canvas.getWidth() / 2, canvas.getHeight() / 2);
var center = fabric.util.transformPoint(point, canvas.viewportTransform);
var clampedCenterX = clamp(center.x, xMin, xMax);
var clampedCenterY = clamp(center.y, yMin, yMax);
var diffX = clampedCenterX - center.x;
var diffY = clampedCenterY - center.y;
if (diffX != 0 || diffY != 0) {
canvas.relativePan(new fabric.Point(diffX, diffY));
}
}
function clamp(value, min, max) {
return Math.max(min, Math.min(value, max));
}
</script>
</body>
</html>
Upvotes: 9
Reputation: 3079
If you just want to pan the canvas in the display, and not to change the position of the elements, you can use this solution.
The idea is to have a fixed size container with css of overflow: hidden
that has an expended canvas inside it. the pan will move the canvas inside the container so the user will see different areas of the expended canvas every time.
Upvotes: 0
Reputation: 645
I know it is already answered, but I had to do a mouse panning, so I adapted the fiddle of the accepted answer to do so. I post it here for anyone who has to do something like this. This is just the main idea:
var panning = false;
canvas.on('mouse:up', function (e) {
panning = false;
});
canvas.on('mouse:down', function (e) {
panning = true;
});
canvas.on('mouse:move', function (e) {
if (panning && e && e.e) {
var units = 10;
var delta = new fabric.Point(e.e.movementX, e.e.movementY);
canvas.relativePan(delta);
}
});
Here is the fiddle: http://jsfiddle.net/gncabrera/hkee5L6d/5/
Upvotes: 19
Reputation: 2190
Solved it using:
relativePan() absolutePan()
[Update]
$('#goRight').click(function(){
var units = 10 ;
var delta = new fabric.Point(units,0) ;
canvas.relativePan(delta) ;
}) ;
$('#goLeft').click(function(){
var units = 10 ;
var delta = new fabric.Point(-units,0) ;
canvas.relativePan(delta) ;
}) ;
$('#goUp').click(function(){
var units = 10 ;
var delta = new fabric.Point(0,-units) ;
canvas.relativePan(delta) ;
}) ;
$('#goDown').click(function(){
var units = 10 ;
var delta = new fabric.Point(0,units) ;
canvas.relativePan(delta) ;
});
http://jsfiddle.net/ux16013L/2/
Upvotes: 23