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
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)...
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.
I'm curious to know how applications such as Adobe Photoshop implement their drawing history with the ability to go back or undo strokes on rasterized graphics without having to redraw each stroke from the beginning...
I'm wanting to implement a similar history function on an HTML5 drawing application I'm working on but duplicating the canvas after every stoke seems like it'd use too much memory to be a practical approach, especially on larger canvas'...
Any suggestions on how this might be implemented in a practical and efficient manner?
I may have a solution.....
var ctx = document.getElementById("canvasId").getContext("2d");
var DrawnSaves = new Array();
var Undo = new Array();
var FigureNumber = 0;
var deletingTimer;
function drawLine(startX, startY, destX, destY) {
ctx.beginPath();
ctx.moveTo(startX, startY);
ctx.lineTo(destX, destY);
ctx.stroke();
var Para = new Array();
Para["type"] = "line";
Para["fromX"] = startX;
Para["fromY"] = startY;
Para["toX"] = destX;
Para["toY"] = destY;
DrawnSaves.push(Para);
FigureNumber++;
}
function undo() {
ctx.beginPath();
ctx.clearRect(0, 0, 500, 500);
Undo[FigureNumber] = DrawnSaves[FigureNumber];
DrawnSaves[FigureNumber] = "deleted";
FigureNumber--;
drawEverything();
startTimeoutOfDeleting();
}
function undoTheUndo() {
FigureNumber++;
DrawnSaves[FigureNumber] = Undo[FigureNumber];
drawEverything();
clearTimeout(deletingTimer);
}
function drawEverything() {
for (i = 0; i < DrawnSaves.length; i++) {
if (DrawnSaves[i].type == "line") {
ctx.beginPath();
ctx.moveTo(DrawnSaves[i].fromX, DrawnSaves[i].fromY);
ctx.lineTo(DrawnSaves[i].toX, DrawnSaves[i].toY);
ctx.stroke();
}
}
}
function startTimeoutOfDeleting() {
setTimeout(function() {Undo[FigureNumber] = "deleted";}, 5000);
}
This is really simple, first I draw a line when the function is called and save all his parameters in an array. Then , in the undo function I just start a timer do delete the figure drawn i 2000 miliseconds, clears the whole canvas and makes it can't be redrawn. in the undoTheUndo function, it stops the timer to delete the figure and makes that the figure can be redrawn. In the drawEverything function, it draws everything in the array based on it's type ("line here"). That's it... :-)
Here is an example working : This, after 2sec UNDOs then after 1sec UNDOTHEUNDO
I want to create a mobile web page where a shape appears on the screen, the user can only traces over the outline of the shape with his/her finger and then a new shape will appear. This library has a few good examples of what I am looking to do, just with more shapes. I have already found a couple of good examples for drawing on the canvas on a touch device here and here. The thing I don't know is how to constrain the line so you are only drawing on the path with a single continuous line. Is there something built in that will let me specify the only path you can draw, or do I have to write that logic by hand?
We can split the issue into two parts :
1) knowing if the user is on the path.
2) knowing if the user went on all path parts.
For 1), we can use the isPointInPath context2D method to know if the mouse/touch point (x,y) is on the curve. The constraint here is that you must build a closed surface, meaning a surface drawn by a fill(), not one built with a stroke(). So in case you are stroking thick lines, you have to do some little math to build the corresponding figures out of moveTo+lineTo+fill.
For 2) : build a list of 'check-points' for your shape. You might have, for instance 8 control points for a circle. Then decide of a distance at which the user will 'activate' the check point. Now the algorithm is, in pseudo-code:
mouseDown => check()
mouseMove => if mouse is down, check()
checkPointList = [ [ 10, 40, false ] , [ centerX, centerY, isChecked], ... ] ;
checked = 0;
function check() {
clear screen
draw the path
if (mouse down and mouse point on path) {
for ( checkPoint in CheckPointList) {
if (checkPoint near enough of mouse) {
checkPoint[3]=true;
checked++;
}
}
if (checked == checkPointList.length) ==>>> User DID draw all the shape.
} else
clear the flags of the checkPointList;
checked=0;
}
I did a moooost simple demo here, which quites work.
The control points are shown in red when disactivated, green when activated :
http://jsbin.com/wekaxiguwiyo/1/edit?js,output
// boilerplate
var cv = document.getElementById('cv');
var ctx = cv.getContext('2d');
function draw() {
ctx.clearRect(0,0,300,300);
drawShape();
drawCP();
}
// Shape
function drawShape() {
ctx.beginPath();
ctx.moveTo(30,5);
ctx.lineTo(80,5);
ctx.lineTo(80, 300);
ctx.lineTo(30,300);
ctx.closePath();
ctx.lineWidth= 16;
ctx.fillStyle='#000';
ctx.fill();
}
// Control points
var points = [ [50, 50, false], [50,120, false], [50, 190, false],[50,260, false ] ];
var pointsCount = 0;
function drawCP() {
for (var i=0; i<points.length; i++) {
var p = points[i];
ctx.fillStyle=p[2]?'#0F0':'#F00';
ctx.fillRect(p[0]-1, p[1]-1, 2, 2);
}
}
function resetCP() {
for (var i=0; i<points.length; i++) {
points[i][2]=false;
}
pointsCount=0;
}
function testCP(x,y) {
var d=30;
d=sq(d);
for (var i=0; i<points.length; i++) {
if (sq(points[i][0]-x)+sq(points[i][1]-y)<d) {
if (!points[i][2]) pointsCount++;
points[i][2]=true
};
}
}
function sq(x) { return x*x; }
//
draw();
// most simple event handling
addEventListener('mousemove', mouseMove);
var r = cv.getBoundingClientRect();
function mouseMove(e) {
var x = e.pageX-r.left;
var y = e.pageY-r.top;
draw();
ctx.fillStyle='#000';
if (ctx.isPointInPath(x,y)) {
ctx.fillStyle='#F00';
testCP(x,y);
} else {
resetCP();
}
ctx.fillRect(x-3,y-3,6,6);
var pathDrawn = (pointsCount == points.length);
if (pathDrawn) ctx.fillText('Shape drawn!!', 150, 150);
}
I am trying to achieve something similar to the code below. When user is on the edge of a rectangle, the cursor points a pointer, otherwise cursor is an arrow.
shape.graphics.beginStroke("#000").beginFill("#daa").drawRect(50, 150, 250, 250);
shape.on("mousemove", function(evt) {
if (isOnEdges(evt)) {
evt.target.cursor = "pointer";
} else {
evt.target.cursor = "arrow";
}
});
Problems with the above code is:
Shape doesn't have a mousemove handler
how do i calculate if mouse is on the edge of a shape (isOnEdges function)
You can simply set the cursor on the shape, and ensure that you enableMouseOver on the stage:
var shape = new Shape();
shape.graphics.beginStroke("#000").beginFill("#daa").drawRect(50, 150, 250, 250);
shape.cursor = "pointer";
stage.enableMouseOver();
EaselJS will automatically determine when you are over the shape's bounds.
I have a similar problem:
container.on("pressmove", function (evt) {
this.x = evt.stageX + this.offset.x;
this.y = evt.stageY + this.offset.y;
if (this.allowDrop(board)) {
this.cursor = "pointer";
}
else {
this.cursor = "no-drop";
}
});
This code doesn't change my cursor in runtime.. I update the stage with a ticker.. so this is not the problem.