Reputation: 22999
I'm trying to build a freehand pencil tool in HTML5 Canvas (using Paper.js as the Canvas wrapper).
I'd like to allow the user to draw straight lines (when Shift is pressed down for example) while drawing. This straight line should "snap", ideally, to an 8-directional snapping "radius".
I've tried a very simple solution where I snap the mouse point to a near rounded point. This works somehow fine but it's not exactly a snap-to-angle tool, it's more like a snap to an invisible grid kind-of-tool.
mousedrag: function(event) {
var snapped = {x: snap(event.point.x), y: snap(event.point.y)};
// add "snapped" drag point
path.add(snapped.x, snapped.y);
}
// convert a number to a rounded snap
function snap(x, div) {
return Math.round(x/div)*div;
};
Here's an interactive Sketch of what I'm currently doing (holding Shift snaps to a grid, releasing resumes regular freehand drawing)
Can anyone give me an indication on how to proceed for snapping to an angle instead of a grid?
Although I'm using Canvas/Paper.js, I understand that the solution to the problem is independent of the rendering tech I'm using, so any JS-based solution (either SVG or Canvas, wrapper or without) should give me some good foundations on how to proceed.
I'm having a feeling that the solution might involve Math.atan()
or something along those lines, instead of my solution where I snap to a rounded Math.round
point.
Upvotes: 3
Views: 1574
Reputation: 11669
If you juste change one or two lines in your code, you get something which might be close to what you want:
// ...
function onMouseDrag(event) {
// if shift is down we transform the mousepoint
// to a "snapped point", else add the mousepoint as it is.
if(shiftDown)
{
var snapPoint = new Point(snap(event.point.x), snap(event.point.y));
myPath.lastSegment.point = snapPoint;
}
else
{
var snapPoint = event.point;
myPath.add(snapPoint);
}
}
// ...
You can easily modify this to snap the angle instead of the position.
function onMouseDrag(event) {
// if shift is down we transform the mousepoint
// to a "snapped point", else add the mousepoint as it is.
if(shiftDown)
{
var vector = event.point - myPath.lastSegment.previous.point;
vector.angle = Math.round(vector.angle/angleSnap)*angleSnap;
myPath.lastSegment.point = myPath.lastSegment.previous.point + vector;
}
else
{
var snapPoint = event.point;
myPath.add(snapPoint);
}
}
Upvotes: 4
Reputation: 105035
The basis of limiting drawing to straight lines is fairly straightforward -- pun intended :-)
When the user drags the mouse, calculate the nearest point on an imaginary line running at your desired angle.
Here is how to calculate the point on a line that is closest to the mouse:
var canvas=document.getElementById("canvas");
var ctx=canvas.getContext("2d");
var cw=canvas.width;
var ch=canvas.height;
function reOffset(){
var BB=canvas.getBoundingClientRect();
offsetX=BB.left;
offsetY=BB.top;
}
var offsetX,offsetY;
reOffset();
window.onscroll=function(e){ reOffset(); }
window.onresize=function(e){ reOffset(); }
var lineStart={x:50,y:50};
var lineEnd={x:250,y:250};
var cr=15;
draw({x:-20,y:-20},'green');
$("#canvas").mousemove(function(e){handleMouseMove(e);});
//////////////////////////////////
function draw(pt,fill){
ctx.clearRect(0,0,cw,ch);
ctx.beginPath();
ctx.moveTo(lineStart.x,lineStart.y);
ctx.lineTo(lineEnd.x,lineEnd.y);
ctx.strokeStyle='black';
ctx.stroke();
ctx.beginPath();
ctx.arc(pt.x,pt.y,5,0,Math.PI*2);
ctx.closePath();
ctx.fillStyle=fill;
ctx.fill();
}
// Find line segment point closest to source point
// [x0,y0] to [x1,y1] define a line segment
// [cx,cy] is source point
function calcClosestPtOnSegment(x0,y0,x1,y1,cx,cy,cr){
// calc delta distance: source point to line start
var dx=cx-x0;
var dy=cy-y0;
// calc delta distance: line start to end
var dxx=x1-x0;
var dyy=y1-y0;
// Calc position on line normalized between 0.00 & 1.00
// == dot product divided by delta line distances squared
var t=(dx*dxx+dy*dyy)/(dxx*dxx+dyy*dyy);
// calc nearest pt on line
var x=x0+dxx*t;
var y=y0+dyy*t;
// clamp results to being on the segment
if(t<0){x=x0;y=y0;}
if(t>1){x=x1;y=y1;}
return({
x:x, y:y,
isColliding:((cx-x)*(cx-x)+(cy-y)*(cy-y)) < cr*cr,
isOnSegment:(t>=0 && t<=1),
});
}
function handleMouseMove(e){
// tell the browser we're handling this event
e.preventDefault();
e.stopPropagation();
mouseX=parseInt(e.clientX-offsetX);
mouseY=parseInt(e.clientY-offsetY);
var p=calcClosestPtOnSegment(
lineStart.x,lineStart.y,lineEnd.x,lineEnd.y,
mouseX,mouseY,cr);
var fill=(p.isOnSegment)?'green':'red';
draw(p,fill);
ctx.beginPath();
ctx.arc(mouseX,mouseY,cr,0,Math.PI*2);
ctx.closePath();
ctx.strokeStyle=p.isColliding?'green':'blue';
ctx.stroke();
}
body{ background-color: ivory; }
#canvas{border:1px solid red; }
<script src="https://ajax.googleapis.com/ajax/libs/jquery/1.9.1/jquery.min.js"></script>
<h4>Move mouse to see closest point on angled line segment.<br>Closest point is green (or red if beyond segment) </h4>
<canvas id="canvas" width=300 height=300></canvas>
Upvotes: 3