In short, I'm making an app where people can collaborate and draw over an image.
I have a proof-of-concept right now, but I've noticed that the drawings show up with incorrect scale on the other person's browser if the two people collaborating are using different sized windows.
The solution is to normalize the drawing input so that points are expressed as a percentage of the canvas that they span, instead of absolute pixel values. However, I don't know how to do this in FabricJS. Finally, I need to be certain that the solution works when zoomed as well.
Any advice for normalizing drawing input would be appreciated! For reference, here is my code so far.
Be warned: I've never used FabricJS before, so this code sample is a mashup of several blog posts and SO answers. This is not good code and will be refactored entirely if FabricJS is the library I decide to go with
The important lines have been commented
document.addEventListener('DOMContentLoaded', () => {
const room = window.location.href.split('/').pop();
const socket = io.connect();
socket.on('connect', () => {
socket.emit('room', room);
});
var canvas = new fabric.Canvas('map', {
isDrawingMode: true
});
let size = Math.min(window.innerWidth, window.innerHeight);
canvas.setHeight(size);
canvas.setWidth(size);
var img = new Image();
img.onload = function () {
canvas.setBackgroundImage(img.src, canvas.renderAll.bind(canvas), {
width: canvas.width,
height: canvas.height,
});
};
img.src = "/images/map.jpg";
canvas.wrapperEl.addEventListener('wheel', (e) => {
if (e.deltaY <= 0) {
canvas.zoomToPoint({
x: e.offsetX,
y: e.offsetY
}, canvas.getZoom() * 1.1);
} else {
canvas.zoomToPoint({
x: e.offsetX,
y: e.offsetY
}, canvas.getZoom() * 0.9);
}
});
canvas.on('path:created', function (e) {
// This is where I need to normalize the path data
canvas.remove(fabric.Path.fromObject(JSON.stringify(e.path)));
socket.emit('draw_line', {
line: e.path.toJSON(),
room: room
});
});
socket.on('draw_line', function (path) {
// This is where I need to convert the path data from percentages to real size
fabric.util.enlivenObjects([path], function (objects) {
objects.forEach(function (o) {
canvas.add(o);
});
});
});
var panning = false;
canvas.on('mouse:up', function (e) {
panning = false;
});
canvas.on('mouse:out', function (e) {
panning = false;
});
canvas.on('mouse:down', function (e) {
panning = true;
});
canvas.on('mouse:move', function (e) {
//allowing pan only if the image is zoomed.
if (panning && e && e.e && e.e.shiftKey) {
var delta = new fabric.Point(e.e.movementX, e.e.movementY);
canvas.relativePan(delta);
}
});
});
A general working solution is that you give your drawing board a virtual fixed dimension:
15.000 x 15.000 for example.
Then in each client app you make the canvas as big as you want, for example 1000 x 1000 and another will get 800 x 800.
Setting the canvas zoom to each one will make possible to see the drawing as big as needed mantaining the same point values on the 15.000 space coordinates.
The drawing thickness will probably change with zoom level.
canvas.setZoom(1000/15000) and canvas.setZoom(800/15000) should be enough.
If it does not work, there is a bug somewhere, that should be fixed.
Related
I do not know how to add local storage to save the drawing made on the canvas so when the page is reloaded the existing drawing is loaded onto the canvas through local storafe. I do not have must experience so would appreciate if someone could edit my code with the local storage addition. Many thanks in advance!
Here is my JS:
var canvas, ctx,
brush = {
x: 0,
y: 0,
color: '#000000',
size: 10,
down: false,
},
strokes = [],
currentStroke = null;
function redraw () {
ctx.clearRect(0, 0, canvas.width(), canvas.height());
ctx.lineCap = 'round';
for (var i = 0; i < strokes.length; i++) {
var s =strokes[i];
ctx.strokeStyle = s.color;
ctx.lineWidth = s.size;
ctx.beginPath();
ctx.moveTo(s.points[0].x, s.points[0].y);
for (var j = 0; j < s.points.length; j++){
var p = s.points[j];
ctx.lineTo(p.x, p.y);
}
ctx.stroke();
}
}
function init () {
canvas = $('#draw');
canvas.attr({
width: window.innerWidth,
height: window.innerHeight,
});
ctx = canvas[0].getContext('2d');
function mouseEvent (e){
brush.x = e.pageX;
brush.y = e.pageY;
currentStroke.points.push({
x: brush.x,
y: brush.y,
});
redraw();
}
canvas.mousedown(function (e){
brush.down = true;
currentStroke = {
color: brush.color,
size: brush.size,
points: [],
};
strokes.push(currentStroke);
mouseEvent(e);
}) .mouseup(function (e) {
brush.down = false;
mouseEvent(e);
currentStroke = null;
}) .mousemove(function (e) {
if (brush.down)
mouseEvent(e);
});
$('#save-btn').click(function () {
window.open(canvas[0].toDataURL());
});
$('#undo-btn').click(function (){
strokes.pop();
redraw();
});
$('#clear-btn').click(function (){
strokes = [];
redraw();
});
$('#color-picker').on('input', function () {
brush.color = this.value;
});
$('#brush-size').on('input', function () {
brush.size = this.value;
});
}
$(init);
Canvas.js will save the canvas as an image to the localStorage which is not helpful in your case as you're storing the mouse events in an array.
If you're looking for a solution which lets continue the drawing on canvas along with restoring old (saved) elements, what you would need is storing of the canvas elements (strokes array in your case) in the localStorage and restoring, redrawing the canvas.
Here's a demo doing that:
JS FIDDLE DEMO
Draw anything on the canvas.
Click on the "Save to local storage" button at the bottom.
Refresh the page.
To clear localStorage, clear the browser cache.
Relevant code changes:
Added a button in HTML to save to local storage
<button id="save-to-local-storage">
Save to local storage
</button>
Saving the strokes array to the localStorage on above button click.
$('#save-to-local-storage').click(function () {
localStorage.setItem('canvas_strokes', JSON.stringify(strokes));
});
On page refresh, check if localStorage has items set and if YES, redraw the canvas:
// check if localstorage has an array of strokes saved
if(localStorage.getItem('canvas_strokes')) {
strokes = JSON.parse(localStorage.getItem('canvas_strokes'));
redraw();
}
Let me know if you have any questions. Hope this helps. :)
Using the Canvas.js helper you can simply do:
const canvas = new Canvas( 'my-canvas' );
canvas.saveToStorage( 'balls' );
where
my-canvas is the canvas id
balls is the key to save it as
To later restore a canvas state:
const canvas = new Canvas( 'my-canvas' );
canvas.restoreFromStorage( 'balls' );
Load Canvas.js:
<script type="text/javascript" src="https://gustavgenberg.github.io/handy-front-end/Canvas.js">
EDIT
After reading the below post (Thank you #Shashank), ive made a jsfiddle with the complete code to achieve continous drawing. It automatically saves the last stroke on mouseup and loads it in on refresh. Check it out!
It uses Canvas.js and Pointer.js:
https://jsfiddle.net/wk5ttqa2/
EDIT 2
Just for fun... this is as simple as it can get really:
https://jsfiddle.net/GustavGenberg/1929f15t/1/
Note that it does not draw complete lines when moving fast (depends on framerate)...
I am using paperjs, when the user draws a line I want it to stop when it crosses a previously drawn line. This my current test code:
tool.minDistance = 1;
var path;
var drawing = false;
function onMouseDown(event) {
if (!drawing) {
startLine(event);
drawing = true;
}
}
function onMouseDrag(event) {
if (drawing) {
path.add(event.point);
}
}
function onMouseUp(event) {
if (drawing) {
endLine(event);
}
}
function startLine(event) {
path = new Path();
path.strokeColor = 'black';
path.strokeWidth = 10;
path.strokeCap = 'round';
path.add(event.point);
}
function endLine(event) {
path.add(event.point);
path.smooth();
path.simplify();
path.onMouseDrag = function(event) {
if (drawing) {
console.log('ending line')
endLine(event);
}
}
drawing = false;
}
I have added an onMouseDrag event to detect when the user crosses an existing path. This works most of the time, but sometimes it is not triggered, especially when drawing slowly.
My question is why is the onMouseDrag event not triggered in that situation? And/or is there a better way to accomplish what I am trying to do?
A better way is to use Path.getIntersections
When you draw use it to get intersections with drawn paths. If it return somethings, stop to draw at the point of intersection.
See also Path.getCrossings
I am trying to free draw rectangle on canvas.
Here's my JSFiddle.
Code:
var canvas1 = new fabric.Canvas("canvas2");
var freeDrawing = true;
var divPos = {};
var offset = $("#canvas2").offset();
$(document).mousemove(function(e) {
divPos = {
left : e.pageX - offset.left,
top : e.pageY - offset.top
};
});
$('#2').click(function() {
console.log("Button 2 cilcked");
// Declaring the variables
var isMouseDown = false;
var refRect;
// Setting the mouse events
canvas1.on('mouse:down', function(event) {
// Defining the procedure
isMouseDown = true;
// Getting yhe mouse Co-ordinates
// Creating the rectangle object
if (freeDrawing) {
var rect = new fabric.Rect({
left : divPos.left,
top : divPos.top,
width : 0,
height : 0,
stroke : 'red',
strokeWidth : 3,
fill : ''
});
canvas1.add(rect);
refRect = rect; // **Reference of rectangle object
}
});
canvas1.on('mouse:move', function(event) {
// Defining the procedure
if (!isMouseDown) {
return;
}
// Getting yhe mouse Co-ordinates
if (freeDrawing) {
var posX = divPos.left;
var posY = divPos.top;
refRect.setWidth(Math.abs((posX - refRect.get('left'))));
refRect.setHeight(Math.abs((posY - refRect.get('top'))));
canvas1.renderAll();
}
});
canvas1.on('mouse:up', function() {
// alert("mouse up!");
isMouseDown = false;
// freeDrawing=false; // **Disables line drawing
});
});
The problem that I am facing is after drawing a rectangle I am unable to move, resize or at least select the drawn rectangle.
Mistake is you are not adding the object finally when mouse is up. Just change the mouse:up event function like this:
canvas1.on('mouse:up', function() {
// alert("mouse up!");
canvas1.add(refRect);
isMouseDown = false;
// freeDrawing=false; // **Disables line drawing
});
It will work fine. :)
I am also facing the same issue, thanks for the solution provided. If you noticed in this fiddle, duplicate object is creating when moving the shape.
How to solve the issue.
$(document).ready(function(){
//Getting the canvas
var canvas1 = new fabric.Canvas("canvas2");
var freeDrawing = true;
var divPos = {};
var offset = $("#canvas2").offset();
$(document).mousemove(function(e){
divPos = {
left: e.pageX - offset.left,
top: e.pageY - offset.top
};
});
$('#2').click(function(){
console.log("Button 2 cilcked");
//Declaring the variables
var isMouseDown=false;
var refRect;
//Setting the mouse events
canvas1.on('mouse:down',function(event){
//Defining the procedure
isMouseDown=true;
//Getting yhe mouse Co-ordinates
//Creating the rectangle object
if(freeDrawing) {
var rect=new fabric.Rect({
left:divPos.left,
top:divPos.top,
width:0,
height:0,
stroke:'red',
strokeWidth:3,
fill:''
});
canvas1.add(rect);
refRect=rect; //**Reference of rectangle object
}
});
canvas1.on('mouse:move', function(event){
// Defining the procedure
if(!isMouseDown)
{
return;
}
//Getting yhe mouse Co-ordinates
if(freeDrawing) {
var posX=divPos.left;
var posY=divPos.top;
refRect.setWidth(Math.abs((posX-refRect.get('left'))));
refRect.setHeight(Math.abs((posY-refRect.get('top'))));
canvas1.renderAll();
}
});
canvas1.on('mouse:up',function(){
//alert("mouse up!");
canvas1.add(refRect);
isMouseDown=false;
//freeDrawing=false; // **Disables line drawing
});
});
});
JS Fiddle Link here.
http://jsfiddle.net/PrakashS/8u1cqasa/75/
None of the other answer's implementations worked for me. All seem to be un compatible with latest fabric.js versions or have important issues.
So I implemented my own (check the JS in the HTML sources)
https://cancerberosgx.github.io/demos/misc/fabricRectangleFreeDrawing.html?d=9
supports two modes: one that will create and update a rectangle onmousedown and another that won't and only create the rectangle onmouseup
doesn't depend on any other library other than fabric.js (no jquery, no react no nothing)
Doesn't use offsets or event.clientX, etc to track the coordinates. It just uses fabric event.pointer API to compute it. This is safe and compatible since it doesn't depend on the DOM and just on a fabric.js public API
carefully manages event listeners
TODO:
I probably will also add throttle support for mousemove
Thanks
I've been trying to develop a scratch card in EaselJS.
So far, I've managed to get a Shape instance above a Bitmap one and enabled erasing it with click and drag events, so the image below becomes visible.
I've used the updateCache() with the compositeOperation approach and it was easy enough, but here is my issue:
How can I find out how much the user has already erased from the Shape instance, so I can setup a callback function when, say, 90% of the image below is visible?
Here is a functioning example of what I'm pursuing: http://codecanyon.net/item/html5-scratch-card/full_screen_preview/8721110?ref=jqueryrain&ref=jqueryrain&clickthrough_id=471288428&redirect_back=true
This is my code so far:
function Lottery(stageId) {
this.Stage_constructor(stageId);
var self = this;
var isDrawing = false;
var x, y;
this.autoClear = true;
this.enableMouseOver();
self.on("stagemousedown", startDrawing);
self.on("stagemouseup", stopDrawing);
self.on("stagemousemove", draw);
var rectWidth = self.canvas.width;
var rectHeight = self.canvas.height;
// Image
var background = new createjs.Bitmap("http://www.taxjusticeblog.org/lottery.jpg");
self.addChild(background);
// Layer above image
var overlay = new createjs.Shape();
overlay.graphics
.f("#55BB55")
.r(0, 0, rectWidth, rectHeight);
self.addChild(overlay);
overlay.cache(0, 0, self.canvas.width, self.canvas.height);
// Cursor
self.brush = new createjs.Shape();
self.brush.graphics
.f("#DD1111")
.dc(0, 0, 5);
self.brush.cache(-10, -10, 25, 25);
self.cursor = "none";
self.addChild(self.brush);
function startDrawing(evt) {
x = evt.stageX-0.001;
y = evt.stageY-0.001;
isDrawing = true;
draw(evt);
};
function stopDrawing() {
isDrawing = false;
};
function draw(evt) {
self.brush.x = self.mouseX;
self.brush.y = self.mouseY;
if (!isDrawing) {
self.update();
return;
}
overlay.graphics.clear();
// Eraser line
overlay.graphics
.ss(15, 1)
.s("rgba(30,30,30,1)")
.mt(x, y)
.lt(evt.stageX, evt.stageY);
overlay.updateCache("destination-out");
x = evt.stageX;
y = evt.stageY;
self.update();
$rootScope.$broadcast("LotteryChangeEvent");
};
}
Any ideas?
That's a tricky one, regardless of the language. The naive solution would simply be to track the length of the paths the user "draws" within the active area, and then reveal when they scratch long enough. That's obviously not very accurate, but is fairly simple and might be good enough.
The more accurate approach would be to get the pixel data of the cacheCanvas, then check the alpha value of each pixel to get an idea of how many pixels are transparent (have low alpha). You could optimize this significantly by only checking every N pixel (ex. every 5th pixel in every 5th row would run 25X faster).
I am trying to make an image object rotate in accordance to the mouse being clicked down on that image and the angle of the mouse to the center of the object.
Think of a unit circle and having the image being rotated about the circle based on where the mouse is on that circle.
I currently have
var stage = new Kinetic.Stage({
container: "container",
width: 800,
height: 500,
id: "myCanvas",
name: "myCanvas"
});
var layer = new Kinetic.Layer();
//gets the canvas context
var canvas = stage.getContainer();
var mousePosX;
var mousePosY;
var mouseStartAngle;
var selectedImage;
var mouseAngle;
var mouseStartAngle;
console.log(canvas)
var shiftPressed
window.addEventListener('keydown', function (e) {
if (e.keyCode == "16") {
shiftPressed = true;
}
console.log(shiftPressed)
}, true);
window.addEventListener('keyup', function (e) {
if (e.keyCode == "16") {
shiftPressed = false;
}
console.log(shiftPressed)
}, true);
function drawImage(imageObj) {
var dynamicImg = new Kinetic.Image({
image: imageObj,
x: stage.getWidth() / 2 - 200 / 2,
y: stage.getHeight() / 2 - 137 / 2,
width: 100, //imageObj.width,
height: 100, // imageObj.height,
draggable: true,
offset: [50,50] //[(imageObj.width/2), (imageObj.height/2)]
});
dynamicImg.on('mousedown', function () {
selectedImage = this;
console.log("x: " + this.getX())
console.log("y: " + this.getY())
var mouseStartXFromCenter = mousePosX - (this.getX() + (this.getWidth() / 2));
var mouseStartYFromCenter = mousePosY - (this.getY() + (this.getHeight() / 2));
mouseStartAngle = Math.atan2(mouseStartYFromCenter, mouseStartXFromCenter);
if (shiftPressed) {
//console.log("trying to switch draggable to false");
//console.log(this)
this.setDraggable(false);
}
});
dynamicImg.on('mouseup mouseout', function () {
//console.log('mouseup mouseout')
this.setDraggable(true);
});
dynamicImg.on('mouseover', function () {
document.body.style.cursor = 'pointer';
});
dynamicImg.on('mouseout', function () {
document.body.style.cursor = 'default';
});
imageArray.push(dynamicImg);
layer.add(dynamicImg);
stage.add(layer);
}
var imageObj = new Image();
imageObj.onload = function () {
drawImage(this);
};
imageObj.src = 'http://localhost:60145/Images/orderedList8.png';
function getMousePos(evt) {
var rect = canvas.getBoundingClientRect();
return {
x: evt.clientX - rect.left,
y: evt.clientY - rect.top
};
}
canvas.addEventListener('mouseup', function () {
selectedImage = undefined;
});
canvas.addEventListener('mousemove', function (evt) {
mousePos = getMousePos(evt);
//console.log('Mouse position: ' + mousePos.x + ',' + mousePos.y)
mousePosX = mousePos.x;
mousePosY = mousePos.y;
if (selectedImage != undefined) {
mouseXFromCenter = mousePosX - selectedImage.getX();
mouseYFromCenter = mousePosY - selectedImage.getY();
mouseAngle = Math.atan2(mouseYFromCenter, mouseXFromCenter);
var rotateAngle = mouseAngle - mouseStartAngle;
selectedImage.setRotation(rotateAngle);
}
}, false);
the code has a couple things to it.
-It should only allow a rotate if 'shift' is pressed and mousedown event happens on an image.
-it needs to maintain the dynamic image drawing as they will be populating the canvas dynamically over the life of the page.
here is a good example of something similar i want to happen, but just simply cannot get it to work in canvas/kineticjs.
http://jsfiddle.net/22Feh/5/
I would go simpler way using dragBoundFunc. http://jsfiddle.net/bighostkim/vqGmL/
dragBoundFunc: function (pos, evt) {
if (evt.shiftKey) {
var x = this.getX() - pos.x;
var y = this.getY() - pos.y;
var radian = Math.PI + Math.atan(y/x);
this.setRotation(radian);
return {
x: this.getX(),
y: this.getY()
}
} else {
return pos;
}
}
My mistake, I misread your question. You were essentially missing one line:
layer.draw();
Hold shift and move the mouse, you'll see it rotates nicely.
http://jsfiddle.net/WXHe6/2/
canvas.addEventListener('mousemove', function (evt) {
mousePos = getMousePos(evt);
//console.log('Mouse position: ' + mousePos.x + ',' + mousePos.y)
mousePosX = mousePos.x;
mousePosY = mousePos.y;
if (selectedImage != undefined) {
mouseXFromCenter = mousePosX - selectedImage.getX();
mouseYFromCenter = mousePosY - selectedImage.getY();
mouseAngle = Math.atan2(mouseYFromCenter, mouseXFromCenter);
var rotateAngle = mouseAngle - mouseStartAngle;
selectedImage.setRotation(rotateAngle);
layer.draw(); // <--------------- right here
}
}, false);
You should be more clear what your .on() events do. In your code, the mousedown on the shape doesn't do anything other than calculate an mouseStartAngle, but doesn't do anything. And your mouseUp on the shape event doesn't do much either. It's your browser/client mousedown/mouseup that do all the work, and that's why it rotates properly on some clicks and not others.
For setting the rotation you have many options, here are two:
Option one: set the radians manually on mousedown
dynamicImg.on('mousedown', function () {
dynamicImg.setRotation(mouseStartAngle); //set the rotation degree
layer.draw(); // redraw the layer
}
Option two: let kineticJS animate the rotation for you
dynamicImg.on('mousedown', function () {
dynamicImg.transitionTo({
duration: 1, // length of animation in seconds
rotation: mouseStartAngle // your angle, in radians
});
}
your updated jsfiddle: http://jsfiddle.net/WXHe6/1/
Here are some additional notes:
The reason you are having trouble is because your code is structured is a bit of a messy manner, sorry to say. You are mixing browser events with and adding listeners to the canvas rather than using built-in kineticJS functionality, for example, you could use stage.getUserPosition() to get the mouse coordinates. Unless you have some dire need to structure your project this way, try to avoid it.
What I do like is that you have created functions to break up your code, but on the down-side you have multiple functions doing the same thing, like calculating and updating the rotation angle. Why not just make one function to get the angle?
Some Tips:
Try using mainly KineticJS for functionality (I know you'll need event listeners for buttons like SHIFT). What I mean is, use things like stage.getUserPosition(); to get the mouse/touch coordinates, rather than evt.clientX, it's cleaner, and the work has been done for you already, no need to re-invent the wheel.
Make one function for setting/getting the rotation for a shape. Then you can call it whenever you want. (use less repetitive code)
If you don't want the shape to be draggable when shift is clicked, you should set that before an item is clicked. You have button listeners so you can either disable rotation for all shapes when shift is pressed down, or just for a shape on that is under the cursor.
imageArray.push(dynamicImg); not sure if you really need this, if you need to access all the elements as an array, you could alway do layer.getChildren(); to get all the images in the layer, or access them numerically like layer.getChildren()[0]; (in creation order)