Fabricjs canvas reset after zooming - javascript

I have a fabricjs canvas that I need to be able to zoom in and out and also change the image/object inside several times.
For this I setup the canvas in the first time the page loads like this:
fabric.Object.prototype.hasBorders = false;
fabric.Object.prototype.hasControls = false;
canvas = new fabric.Canvas('my_canvas', {renderOnAddRemove: false, stateful: false});
canvas.defaultCursor = "pointer";
canvas.backgroundImageStretch = false;
canvas.selection = false;
canvas.clear();
var image = document.getElementById('my_image');
if (image != null) {
imageSrc = image.src;
if(imageSrc.length > 0){
fabric.Image.fromURL(imageSrc, function(img) {
img = scaleImage(canvas, img); //shrinks the image to fit the canvas
img.selectable = false;
canvas.centerObject(img);
canvas.setActiveObject(img);
canvas.add(img);
});
}
}
canvas.deactivateAll().renderAll();
Then when I need to change the image/object in the canvas or when the page reloads, I try to reset the canvas like this:
canvas.clear();
canvas.remove(canvas.getActiveObject());
var image = document.getElementById('my_image');
if (image != null) {
imageSrc = image.src;
if(imageSrc.length > 0){
fabric.Image.fromURL(imageSrc, function(img) {
img = scaleImage(canvas, img); //shrinks the image to fit the canvas
img.selectable = false;
canvas.centerObject(img);
canvas.setActiveObject(img);
canvas.add(img);
});
}
}
Not sure if it matters but the way I change the image is by changing the source in 'my_image' and reseting the canvas with the above method.
This works well until I use canvas.zoomToPoint, as per this thread, after this, the image/object starts changing position when I reset the zoom or click the canvas with the mouse while it is zoomed, seeming to jump at each change in the top left corner direction, eventually disappearing from view.
Reset Zoom:
canvas.setZoom(1);
resetCanvas(); //(above method)
How can I restore the image/object position?
I tried doing the initial setup instead of the reset and seamed to work visually but was in fact adding a new layer of upper canvas at each new setup so it is no good.
Is there a way to reset the canvas to original state without causing this behavior and still be able to zoom in/out correctly?

Although this question is very old, here is what I did using the current version of fabric.js 2.2.4:
canvas.setViewportTransform([1,0,0,1,0,0]);
For your information: zooming to a point is a recalculation of the viewport transformation. The upper matrix is this is the initial viewport transform matrix.

I eventually fixed the problems I was having.
To reset the zoom, instead of just setting the zoom to 1 with canvas.setZoom(1), I reapplied the canvas.zoomToPoint method to the same point but with zoom 1, to force the initial zoom but regarding the same point that was used to zoom in.
As for the problem of restoring the image position in canvas (after panning for instance) it is as simple as removing the image, centering it in the canvas and re-adding it to the canvas as was done when adding first time:
var img = canvas.getActiveObject();
canvas.remove(img);
canvas.centerObject(img);
canvas.setActiveObject(img);
canvas.add(img);
canvas.renderAll();

See below snippet - here I do the same - zooming together, but degrouping the objects in case somebody clicks on it.
The problem to get to original object properties can be solved, ungrouping the group and creating copies of them and reattaching - a bit annoying, but the only solution I found.
<script id="main">
// canvas and office background
var mainGroup;
var canvas = this.__canvas = new fabric.Canvas('c');
fabric.Object.prototype.transparentCorners = false;
fabric.Object.prototype.originX = fabric.Object.prototype.originY = 'center';
createOnjects(canvas);
// events - zoom
$(canvas.wrapperEl).on('mousewheel', function(e) {
var target = canvas.findTarget(e);
var delta = e.originalEvent.wheelDelta / 5000;
if (target) {
target.scaleX += delta;
target.scaleY += delta;
// constrain
if (target.scaleX < 0.1) {
target.scaleX = 0.1;
target.scaleY = 0.1;
}
// constrain
if (target.scaleX > 10) {
target.scaleX = 10;
target.scaleY = 10;
}
target.setCoords();
canvas.renderAll();
return false;
}
});
// mouse down
canvas.on('mouse:up', function(options) {
if (options.target) {
var thisTarget = options.target;
var mousePos = canvas.getPointer(options.e);
if (thisTarget.isType('group')) {
// unGroup
console.log(mousePos);
var clone = thisTarget._objects.slice(0);
thisTarget._restoreObjectsState();
for (var i = 0; i < thisTarget._objects.length; i++) {
var o = thisTarget._objects[i];
if (o._element.alt == "officeFloor")
continue;
else {
if (mousePos.x >= o.originalLeft - o.currentWidth / 2 && mousePos.x <= o.originalLeft + o.currentWidth / 2
&& mousePos.y >= o.originalTop - o.currentHeight / 2 && mousePos.y <= o.originalTop + o.currentHeight / 2)
console.log(o._element.alt);
}
}
// remove all objects and re-render
canvas.remove(thisTarget);
canvas.clear().renderAll();
var group = new fabric.Group();
for (var i = 0; i < clone.length; i++) {
group.addWithUpdate(clone[i]);
}
canvas.add(group);
canvas.renderAll();
}
}
});
// functions
function createOnjects(canvas) {
// ToDo: jQuery.parseJSON() for config file (or web service)
fabric.Image.fromURL('pics/OfficeFloor.jpg', function(img) {
var back = img.set({ left: 100, top: 100 });
back._element.alt = "officeFloor";
back.hasControls = false;
fabric.Image.fromURL('pics/me.png', function(img) {
var me = img.set({ left: -420, top: 275 });
me._element.alt = "me";
console.log(me);
var group = new fabric.Group([ back, me], { left: 700, top: 400, hasControls: false });
canvas.clear().renderAll();
canvas.add(group);
// remove all objects and re-render
});
});
}
</script>

Related

How to start function at specific scroll position on page?

I'm experimenting with the following code where images flip through in a canvas.
If I put the canvas, let's say at the middle of my page, I was wondering if there's a way to only make the images flip through once the viewer gets to that specific section of the page.
Right now, from the looks of it, the images start flipping through right at the top of the page, immediately when you scroll down. By the time you get to the middle of the page where the actual canvas is, the images have already finished flipping through, and it's stopped on the last frame.
I assume I have to set the function to only trigger after the user scrolls to a specific number of pixels on the Y-axis? What's the best way to do so?
Please see code below.
Thanks!
var images = new Array();
var currentLocation = 0;
var totalImages = 200;
for (var i = 1; i < totalImages; i++) {
var img = new Image;
var slug = '000' + i;
img.src = 'https://s3.amazonaws.com/clearmotion/hero/high-min/frame' + slug.slice(-3) + '-low.jpg'
images.push(img);
}
var c = document.getElementById("background");
var ctx = c.getContext("2d");
var mouseWheel = function() {
var newLocation = null;
window.addEventListener('wheel', function(e) {
e.preventDefault(); // No scroll
// update our variable at high frequency
var delta = Math.max(-1, Math.min(1, e.deltaY));
if (delta == -1) currentLocation += 1;
if (delta == 1) currentLocation -= 1;
if (currentLocation < 0) currentLocation = 0;
if (currentLocation >= (totalImages - 1)) currentLocation = (totalImages - 1);
if (newLocation === null) { // if set, we already are waiting to draw
requestAnimationFrame(setImage);
}
newLocation = currentLocation;
});
function setImage() {
if (images[newLocation]) {
ctx.fillRect(0, 0, c.width, c.height);
ctx.drawImage(images[newLocation], 0, 0, 1000, 1000);
}
newLocation = null; // so the throttler knows we can draw again
}
}
images[0].onload = function() {
ctx.fillRect(0, 0, c.width, c.height);
ctx.drawImage(images[currentLocation], 0, 0, 1000, 1000);
mouseWheel();
};
<canvas id="background" width="1000" height="1000"></canvas>
You can actually start make the images flip through once the viewer gets to that specific section of the page by using The Intersection Observer API
So you have to detect when your element in the the viewport, the canvas in this case.
To do this you have multiple ways.
By using Observer API
const element = document.querySelector("#background")
const Ob = new IntersectionObserver((entries) => {
if (entries[0].intersectionRatio <= 0) {
// Not in the viewport
} else {
// In the viewport
// You're code here
}
});
Ob.observe(element);
Or if you want to do it in you're own.
You can use this and adapt it if needed
function elementInViewport(el) {
var top = el.offsetTop
var height = el.offsetHeight;
while(el.offsetParent) {
el = el.offsetParent;
top += el.offsetTop;
}
return (
top >= window.pageYOffset &&
(top + height) <= (window.pageYOffset + window.innerHeight)
);
}

Fabric.JS and Fabric-Brush - Can't add to lower canvas

I'm trying to use Fabric.js with Fabric Brush This issue that I'm running into is that Fabric Brush only puts the brush strokes onto the Top Canvas and not the lower canvas. (The stock brushes in fabric.js save to the bottom canvas) I think I need to convert "this.canvas.contextTop.canvas" to an object and add that object to the the lower canvas. Any ideas?
I've tried running:
this.canvas.add(this.canvas.contextTop)
in
onMouseUp: function (pointer) {this.canvas.add(this.canvas.contextTop)}
But I'm getting the error
Uncaught TypeError: obj._set is not a function
So the contextTop is CanvasHTMLElement context. You cannot add it.
You can add to the fabricJS canvas just fabric.Object derived classes.
Look like is not possible for now.
They draw as pixel effect and then they allow you to export as an image.
Would be nice to extend fabricJS brush interface to create redrawable objects.
As of now with fabricJS and that particular version of fabric brush, the only thing you can do is:
var canvas = new fabric.Canvas(document.getElementById('c'))
canvas.freeDrawingBrush = new fabric.CrayonBrush(canvas, {
width: 70,
opacity: 0.6,
color: "#ff0000"
});
canvas.isDrawingMode = true
canvas.on('mouse:up', function(opt) {
if (canvas.isDrawingMode) {
var c = fabric.util.copyCanvasElement(canvas.upperCanvasEl);
var img = new fabric.Image(c);
canvas.contextTopDirty = true;
canvas.add(img);
canvas.isDrawingMode = false;
}
})
<script src="https://cdnjs.cloudflare.com/ajax/libs/fabric.js/2.4.1/fabric.min.js"></script>
<script src="https://tennisonchan.github.io/fabric-brush/bower_components/fabric-brush/dist/fabric-brush.min.js"></script>
<button>Enter free drawing</button>
<canvas id="c" width="500" height="500" ></canvas>
That is just creating an image from the contextTop and add as an object.
I have taken the approach suggested by AndreaBogazzi and modified the Fabric Brush so that it does the transfer from upper to lower canvas (as an image) internal to Fabric Brush. I also used some code I found which crops the image to a smaller bounding box so that is smaller than the full size of the canvas. Each of the brushes in Fabric Brush has an onMouseUp function where the code should be placed. Using the case of the SprayBrush, the original code here was:
onMouseUp: function(pointer) {
},
And it is replaced with this code:
onMouseUp: function(pointer){
function trimbrushandcopytocanvas() {
let ctx = this.canvas.contextTop;
let pixels = ctx.getImageData(0, 0, canvas.upperCanvasEl.width, canvas.upperCanvasEl.height),
l = pixels.data.length,
bound = {
top: null,
left: null,
right: null,
bottom: null
},
x, y;
// Iterate over every pixel to find the highest
// and where it ends on every axis ()
for (let i = 0; i < l; i += 4) {
if (pixels.data[i + 3] !== 0) {
x = (i / 4) % canvas.upperCanvasEl.width;
y = ~~((i / 4) / canvas.upperCanvasEl.width);
if (bound.top === null) {
bound.top = y;
}
if (bound.left === null) {
bound.left = x;
} else if (x < bound.left) {
bound.left = x;
}
if (bound.right === null) {
bound.right = x;
} else if (bound.right < x) {
bound.right = x;
}
if (bound.bottom === null) {
bound.bottom = y;
} else if (bound.bottom < y) {
bound.bottom = y;
}
}
}
// Calculate the height and width of the content
var trimHeight = bound.bottom - bound.top,
trimWidth = bound.right - bound.left,
trimmed = ctx.getImageData(bound.left, bound.top, trimWidth, trimHeight);
// generate a second canvas
var renderer = document.createElement('canvas');
renderer.width = trimWidth;
renderer.height = trimHeight;
// render our ImageData on this canvas
renderer.getContext('2d').putImageData(trimmed, 0, 0);
var img = new fabric.Image(renderer,{
scaleY: 1./fabric.devicePixelRatio,
scaleX: 1./fabric.devicePixelRatio,
left: bound.left/fabric.devicePixelRatio,
top:bound.top/fabric.devicePixelRatio
});
this.canvas.clearContext(ctx);
canvas.add(img);
}
setTimeout(trimbrushandcopytocanvas, this._interval); // added delay because last spray was on delay and may not have finished
},
The setTimeout function was used because Fabric Brush could still be drawing to the upper canvas after the mouseup event occurred, and there were occasions where the brush would continue painting the upper canvas after its context was cleared.

Update canvas on mouse events

I've created a hexagonal grid using this JS library. The grid does get properly painted onto the Canvas. My issue is with trying to introduce events on mouse events. For instance, when I hover over a certain hexagon I want it's background color to change.
At the moment, only the last created hexagon will change on mouseover regardless of which hexagon the cursor is hovering over. How can I make the event listener update the specific hexagon over which the cursor exists?
If this is not possible due to painted objects becoming "rasterized" into the canvas, what alternate approach would be recommended?
The code is below:
<canvas id="stage"></canvas>
<script>
var element = document.getElementById("stage");
element.height = window.innerHeight;
element.width = window.innerWidth;
var stage = new createjs.Stage("stage");
stage.x = window.innerWidth/2;
stage.y = window.innerHeight/2;
stage.enableMouseOver();
var grid = new Grid();
grid.tileSize = 55;
var stageTransformer = new StageTransformer().initialize({ element: element, stage: stage });
stageTransformer.addEventListeners();
var tick = function (event) { stage.update(); };
var colorHexagon = function(hexagon, fill) {
hexagon.graphics
.beginFill(fill)
.beginStroke("rgba(50,50,50,1)")
.drawPolyStar(0, 0, grid.tileSize, 6, 0, 0);
};
var coordinates = grid.hexagon(0, 0, 3, true)
for (var i = 0; i < coordinates.length; i++) {
var q = coordinates[i].q,
r = coordinates[i].r,
center = grid.getCenterXY(q, r),
hexagon = new createjs.Shape();
hexagon.cursor = "pointer";
hexagon.addEventListener("mouseover", function() {
colorHexagon(hexagon, "rgba(50,150,0,1)")
});
hexagon.addEventListener("mouseout", function() {
colorHexagon(hexagon, "rgba(150,150,150,1)")
});
hexagon.q = q;
hexagon.r = r;
hexagon.x = center.x;
hexagon.y = center.y;
colorHexagon(hexagon, "rgba(150,150,150,1)");
stage.addChild(hexagon);
stage.update();
}
tick();
createjs.Ticker.setFPS(30);
createjs.Ticker.addEventListener("tick", tick);
</script>
This should work.
stage.addEventListener("mouseover", function(evt) {
colorHexagon(evt.target, "rgba(50,150,0,1)")
});
stage.addEventListener("mouseout", function(evt) {
colorHexagon(evt.target, "rgba(150,150,150,1)")
});
you can put them at the very bottom of your script. They don't have to be in a loop.
Bind your event handlers to the container of the hexagons instead of to each hexagon seperately and find the hexagon hovered over by using the coordinates of the mouse. And check the docs of the library, maybe they have a method for that included.

Rotating the whole canvas in fabricjs

Is there a way to rotate the canvas in fabric.js?
I am not looking to rotate each element, that can be achieved easily, but rather a way to rotate the whole canvas similar to what is achieved with canvas.rotate() on a native canvas element:
var c=document.getElementById("myCanvas");
var ctx=c.getContext("2d");
ctx.rotate(20*Math.PI/180);
Accessing the canvas element from fabric.js with getContext() is possible, but if I do that and then rotate it, only one of the two canvases is being rotate and the selection/drawing is severely off and drawing/selecting/etc is not working anymore either.
I am somewhat at a loss here. If this is something that's currently not possible with fabric.js I will create a ticket on github, but somehow it feels like it should be possible ...
[edit]
After the input from Ian I've figured a few things out and am at a point where I can rotate the canvas and get some results. However, objects are very far off from the correct position. However, this might be because, while rotating, I am also zooming and absolute paning the canvas (with canvas.setZoom() and canvas.absolutePan()). I think I'll create a ticket on GitHub and see what the devs think. Somewhat stuck here ... Just for reference here's the code snippet:
setAngle: function(angle) {
var self = this;
var canvas = self.getFabricCanvas();
var group = new fabric.Group();
var origItems = canvas._objects;
var size = self.getSize();
group.set({width: size.width, height: size.height, left: size.width / 2, top: size.height / 2, originX: 'center', originY: 'center', centeredRotation: true})
for (var i = 0; i < origItems.length; i++) {
group.add(origItems[i]);
}
canvas.add(group);
group.set({angle: (-1 * self.getOldAngle())});
canvas.renderAll();
group.set({angle: angle});
canvas.renderAll();
items = group._objects;
group._restoreObjectsState();
canvas.remove(group);
for (var i = 0; i < items.length; i++) {
canvas.add(items[i]);
canvas.remove(origItems[i]);
}
canvas.renderAll();
self.setOldAngle(angle);
},
As stated above, this function is called with two other functions:
setPosition: function(left, top) {
var self = this;
if (left < 0) {
left = 0;
}
if (top < 0) {
top = 0;
}
var point = new fabric.Point(left, top);
self.getFabricCanvas().absolutePan(point);
},
setZoom: function(zoom) {
var self = this;
self.getFabricCanvas().setZoom(zoom);
},
The functions are called through the following code:
MyClass.setZoom(1);
MyClass.setPosition(left, top);
MyClass.setZoom(zoom);
MyClass.setAngle(angle);
As you can see, I try to set the angle last, but it doesn't make a difference (at least not visually) when I do that. The zoom set to 1 at the beginning is important as otherwise the panning won't work properly.
Maybe someone has an idea ...
Here is how I did this (code based on this js fiddle).
rotate (degrees) {
let canvasCenter = new fabric.Point(canvas.getWidth() / 2, canvas.getHeight() / 2) // center of canvas
let radians = fabric.util.degreesToRadians(degrees)
canvas.getObjects().forEach((obj) => {
let objectOrigin = new fabric.Point(obj.left, obj.top)
let new_loc = fabric.util.rotatePoint(objectOrigin, canvasCenter, radians)
obj.top = new_loc.y
obj.left = new_loc.x
obj.angle += degrees //rotate each object by the same angle
obj.setCoords()
});
canvas.renderAll()
},
After doing this I also had to adjust the canvas background and size so objects wouldn't go off the canvas.

HTML5 canvas zoom my drawings

Hello i create program like a paint on HTML5 canvas. I have problem i need create few tools drawing and zoom. I don't have idea how to create zoom without delay. Drawing example: http://jsfiddle.net/x5rrvcr0/
How i can zooming my drawings?
drawing code:
<style>
canvas {
background-color: #CECECE;
}
html, body {
background-color: #FFFFFF;
}
</style>
<script>
$(document).ready(function () {
var paintCanvas = document.getElementById("paintCanvas");
var paintCtx = paintCanvas.getContext("2d");
var size = 500;
paintCanvas.width = size;
paintCanvas.height = size;
var draw = false;
var prevMouseX = 0;
var prevMouseY = 0;
function getMousePos(canvas, evt) {
evt = evt.originalEvent || window.event || evt;
var rect = canvas.getBoundingClientRect();
if (evt.clientX !== undefined && evt.clientY !== undefined) {
return {
x: evt.clientX - rect.left,
y: evt.clientY - rect.top
};
}
}
$("#paintCanvas").on("mousedown", function(e) {
draw = true;
var coords = getMousePos(paintCanvas);
prevMouseX = coords.x;
prevMouseY = coords.y;
});
$("#paintCanvas").on("mousemove", function(e) {
if(draw) {
var coords = getMousePos(paintCanvas, e);
paintCtx.beginPath();
paintCtx.lineWidth = 10;
paintCtx.strokeStyle = "#000000";
paintCtx.moveTo(prevMouseX, prevMouseY);
paintCtx.lineTo(coords.x, coords.y);
paintCtx.stroke();
prevMouseX = coords.x;
prevMouseY = coords.y;
}
});
$("#paintCanvas").on("mouseup", function(e) {
draw = false;
});
});
</script>
<body>
<canvas id="paintCanvas"></canvas>
</body>
If you want to keep the pixelated effect in the zoom, you need to draw on a temp canvas, then only after copy that temp canvas to the main screen.
You no longer need to zoom in the temp canvas, just draw on 1:1 scale always. When copying to the view canvas, then you apply the zoom (and maybe translate) that you want.
Keep in mind that drawings are anti-aliased, so you when zooming you will see some shades of grey when drawing in black, for instance.
I kept the recording code of #FurqanZafar since it is a good idea to record things in case you want to perform undo : in that case just delete the last record entry and redraw everything.
http://jsfiddle.net/gamealchemist/x5rrvcr0/4/
function updatePaintCanvas() {
paintContext.clearRect(0, 0, paintContext.canvas.width, paintContext.canvas.height);
paintContext.save();
paintContext.translate(cvSize * 0.5, cvSize * 0.5);
paintContext.scale(scale, scale);
paintContext.drawImage(drawCanvas, -cvSize * 0.5, -cvSize * 0.5);
paintContext.restore();
}
Heres the updated fiddle: http://jsfiddle.net/x5rrvcr0/2/ with basic zooming functionality
If you draw multiple paths on mouse move then your sketch will appear broken or disconnected, instead you should only stroke a single path until "mouseup" event.
You can then store these paths in an array and later redraw them at different zoom levels:
function zoom(context, paths, styles, scale) {
context.clearRect(0, 0, context.canvas.width, context.canvas.height);
context.save();
applyStyles(context, styles);
scaleFromCenter(context, scale);
for (var i = 0; i < paths.length; i++) {
context.beginPath();
context.moveTo(paths[i][0].x, paths[i][0].y);
for (var j = 1; j < paths[i].length; j++)
context.lineTo(paths[i][j].x, paths[i][j].y);
context.stroke();
}
context.restore();
};

Categories

Resources