I've uploaded a couple of images to act as sprites for a 2D HTML5 Canvas car game. I've been trying to do collision detection with just using the coordinates of the sprites, but it does not work smoothly. I've heard mention of bounding rectangles before, and to my knowledge, they are invisible rectangles under sprites that aid in collision detection (correct me if I'm wrong).
I've seen some things online like Element.getBoundingClientRect().
Can anyone help me put some bounding rectangles under my sprites because I am clueless and I can't find any basic tutorial online.
Js Code: Jsbin link: http://jsbin.com/muzulutaci/2/edit?
var canvas = document.getElementById('background');
var context = canvas.getContext('2d');
//================
//ENTER: USER CAR
//================
//Uploading car sprite
var usercar = new Image();
usercar.src = "http://www.iconshock.com/img_jpg/BETA/communications/jpg/128/car_icon.jpg";
//Setting properties of car
var x = 450;
var y = 730;
var speed = 10;
var angle = -90;
var mod = 0;
function drawUserCar() {
context.clearRect(0, 0, canvas.width, canvas.height);
context.save();
context.translate(x, y);
context.rotate(Math.PI / 180 * angle);
context.drawImage(usercar, -(usercar.width / 2), -(usercar.height / 2));
context.restore();
obstacleCar1();
}
//Interval for animation
var moveInterval = setInterval(function () {
drawUserCar();
}, 30);
//=====================
//ENTER: OBSTACLE CAR 1
//=====================
//Uploading obstacle car
var obstcar = new Image();
obstcar.src = "http://www.iconshock.com/img_jpg/BETA/communications/jpg/128/car_icon.jpg";
//Setting properties of obstacle car
var x1 = 450;
var y1 = 300;
var speed1 = 5;
var angle1 = 90;
var mod1 = 0;
function obstacleCar1() {
x1 += (speed1 * mod1) * Math.cos(Math.PI / 180 * angle1);
y1 += (speed1 * mod1) * Math.sin(Math.PI / 180 * angle1);
context.save();
context.translate(x1, y1);
context.rotate(Math.PI / 180 * angle1);
context.drawImage(obstcar, -(obstcar.width / 1), -(obstcar.height / 1));
context.restore();
}
Collision detection between rotated rectangles is mathematically complicated and comes in several flavors:
Detect if the 2 rotated rectangles are intersecting (colliding)
To detect if 2 rotated rectangles are colliding (but not detect where they are colliding), you can use the Separating Axis Theorm. A nice explanation is here: http://gamedevelopment.tutsplus.com/tutorials/collision-detection-using-the-separating-axis-theorem--gamedev-169
Detect where the rotated rectangles are colliding and apply rebound to the rectangles
To apply rebound when 2 rotated rectangles are colliding requires some complicated physics. Perhaps the easiest path to do this is to use a physics library like Box2dJS. Here's a nice demo of Box2dJS showing colliding rectangles: http://box2d-js.sourceforge.net/index2.html
Example code and a Demo:
var canvas=document.getElementById("canvas");
var ctx=canvas.getContext("2d");
var cw=canvas.width;
var ch=canvas.height;
var BB=canvas.getBoundingClientRect();
var offsetX=BB.left;
var offsetY=BB.top;
var isDown=false;
var startX;
var startY;
var PI=Math.PI;
var car1Rect,car2Rect;
var cars=[];
var Closure=(function(){
// ctor
function Closure(x,y,imageObject){
var iw=imageObject.width;
var ih=imageObject.height;
this.img=imageObject;
this.x=x;
this.y=y;
this.w=iw;
this.h=iw;
this.cx=x+iw/2;
this.cy=y+ih/2;
this.radius=Math.sqrt(iw*iw+ih*ih)/2;
this.rotation=0;
this.corners=[];
this.isDragging=false;
this.collisionType=0;
// corner angles
var w2=iw/2;
var h2=ih/2;
this.negHalfWidth=-w2;
this.negHalfHeight=-h2;
this.cornerAngles=[
Math.atan2(-h2,-w2), // top-left
Math.atan2(h2,-w2), // top-right
Math.atan2(h2,w2), // bottom-right
Math.atan2(-h2,w2) // bottom-left
];
this.rotateTo(0);
}
//
Closure.prototype.draw=function(){
this.drawImage();
this.drawBB(true);
this.drawBoundingCircle(true);
};
Closure.prototype.moveBy=function(dx,dy){
this.cx+=dx;
this.cy+=dy;
};
Closure.prototype.rotateTo=function(angle){
this.rotation=angle;
this.setCorners();
};
Closure.prototype.setCorners=function(){
this.corners.length=0;
for(var i=0;i<this.cornerAngles.length;i++){
var a=this.cornerAngles[i]+this.rotation;
var x=this.radius*Math.cos(a);
var y=this.radius*Math.sin(a);
this.corners.push({x:x,y:y});
}
};
Closure.prototype.drawBB=function(withStroke){
var p=this.corners;
var cx=this.cx;
var cy=this.cy;
ctx.beginPath();
ctx.moveTo(cx+p[0].x,cy+p[0].y);
for(var i=1;i<p.length;i++){
ctx.lineTo(cx+p[i].x,cy+p[i].y);
}
ctx.closePath();
if(withStroke){
switch(this.collisionType){
case 0:ctx.strokeStyle='gray';break;
case 1:ctx.strokeStyle='green';break;
case 2:ctx.strokeStyle='red';break;
}
ctx.stroke();
}
};
Closure.prototype.drawBoundingCircle=function(withStroke){
var p=this.corners;
var cx=this.cx;
var cy=this.cy;
ctx.beginPath();
ctx.arc(this.cx,this.cy,this.radius,0,PI*2);
ctx.closePath();
if(withStroke){
switch(this.collisionType){
case 0:ctx.strokeStyle='gray';break;
case 1:ctx.strokeStyle='red';break;
case 2:ctx.strokeStyle='red';break;
}
ctx.stroke();
}
};
Closure.prototype.drawImage=function(){
ctx.globalAlpha=0.50;
ctx.translate(this.cx,this.cy);
ctx.rotate(this.rotation);
ctx.drawImage(this.img,this.negHalfWidth,this.negHalfHeight);
ctx.setTransform(1,0,0,1,0,0);
ctx.globalAlpha=1.00;
};
// Closure.prototype.=function(){};
return(Closure);
})();
function calculateCollisionType(r1,r2){
// rough but fast circular bounds hit-test
var dx=r2.cx-r1.cx;
var dy=r2.cy-r1.cy;
var rr=r1.radius+r2.radius;
if(dx*dx+dy*dy>rr*rr){
r1.collisionType=0; // no collision
r2.collisionType=0; // no collision
return(false);
}
// hit-test the bounding rectangles
if(RectanglesIntersect(r1,r2)){
r1.collisionType=2; // bounding rectangles collide
r2.collisionType=2;
}else{
r1.collisionType=1; // circular bounds collide
r2.collisionType=1;
}
return(true);
}
$car1Angle=$('#car1Angle');
$car2Angle=$('#car2Angle');
$car1Angle.val(0);
$car2Angle.val(0);
var carCount=2;
var car1=new Image();
car1.onload=start;
car1.src="https://dl.dropboxusercontent.com/u/139992952/multple/car1.png";
var car2=new Image();
car2.onload=start;
car2.src="https://dl.dropboxusercontent.com/u/139992952/multple/car2.png";
function start(){
if(--carCount>0){return;}
car1Rect=new Closure(50,100,car1);
cars.push(car1Rect);
car2Rect=new Closure(50,250,car2);
cars.push(car2Rect);
$("#canvas").mousedown(function(e){handleMouseDown(e);});
$("#canvas").mousemove(function(e){handleMouseMove(e);});
$("#canvas").mouseup(function(e){handleMouseUpOut(e);});
$("#canvas").mouseout(function(e){handleMouseUpOut(e);});
$car1Angle.change(function(){
car1Rect.rotateTo($(this).val()*PI/180);
draw();
});
$car2Angle.change(function(){
car2Rect.rotateTo($(this).val()*PI/180);
draw();
});
calculateCollisionType(car1Rect,car2Rect);
draw();
$('#testCollision').click(function(){
log(RectanglesIntersect(car1Rect,car2Rect));
});
}
function draw(){
ctx.clearRect(0,0,cw,ch);
car2Rect.draw();
car1Rect.draw();
}
function handleMouseDown(e){
e.preventDefault();
e.stopPropagation();
startX=parseInt(e.clientX-offsetX);
startY=parseInt(e.clientY-offsetY);
isDown=false;
for(var i=0;i<cars.length;i++){
var c=cars[i];
c.drawBB(false);
if(ctx.isPointInPath(startX,startY)){
c.isDragging=true;
isDown=true;
}
}
}
function handleMouseUpOut(e){
e.preventDefault();
e.stopPropagation();
isDown=false;
for(var i=0;i<cars.length;i++){
cars[i].isDragging=false;
}
}
function handleMouseMove(e){
if(!isDown){return;}
e.preventDefault();
e.stopPropagation();
mouseX=parseInt(e.clientX-offsetX);
mouseY=parseInt(e.clientY-offsetY);
var dx=mouseX-startX;
var dy=mouseY-startY;
startX=mouseX;
startY=mouseY;
for(var i=0;i<cars.length;i++){
var c=cars[i];
if(c.isDragging){ c.moveBy(dx,dy); }
}
calculateCollisionType(car1Rect,car2Rect);
draw();
}
///////////////////////////////////////
// Attribution for RectanglesIntersect() & isProjectedAxisCollision()
// https://github.com/jozefchutka/YCanvas/blob/master/YCanvasLibrary/libs/yoz/sk/yoz/math/FastCollisions.as
//
function RectanglesIntersect(r1,r2){
// rotated rectangle hit-test
var cx,cy,c;
//
cx=r1.cx;
cy=r1.cy;
c=r1.corners;
//
var r1p1x=cx+c[0].x;
var r1p2x=cx+c[1].x;
var r1p3x=cx+c[2].x;
var r1p4x=cx+c[3].x;
//
var r1p1y=cy+c[0].y;
var r1p2y=cy+c[1].y;
var r1p3y=cy+c[2].y;
var r1p4y=cy+c[3].y;
//
cx=r2.cx;
cy=r2.cy;
c=r2.corners;
//
var r2p1x=cx+c[0].x;
var r2p2x=cx+c[1].x;
var r2p3x=cx+c[2].x;
var r2p4x=cx+c[3].x;
//
var r2p1y=cy+c[0].y;
var r2p2y=cy+c[1].y;
var r2p3y=cy+c[2].y;
var r2p4y=cy+c[3].y;
//
if(!isProjectedAxisCollision(r1p1x, r1p1y, r1p2x, r1p2y,
r2p1x, r2p1y, r2p2x, r2p2y, r2p3x, r2p3y, r2p4x, r2p4y))
return false;
if(!isProjectedAxisCollision(r1p2x, r1p2y, r1p3x, r1p3y,
r2p1x, r2p1y, r2p2x, r2p2y, r2p3x, r2p3y, r2p4x, r2p4y))
return false;
if(!isProjectedAxisCollision(r2p1x, r2p1y, r2p2x, r2p2y,
r1p1x, r1p1y, r1p2x, r1p2y, r1p3x, r1p3y, r1p4x, r1p4y))
return false;
if(!isProjectedAxisCollision(r2p2x, r2p2y, r2p3x, r2p3y,
r1p1x, r1p1y, r1p2x, r1p2y, r1p3x, r1p3y, r1p4x, r1p4y))
return false;
//
return true;
}
function isProjectedAxisCollision( b1x, b1y, b2x, b2y, p1x, p1y, p2x, p2y, p3x, p3y, p4x, p4y){
var x1, x2, x3, x4;
var y1, y2, y3, y4;
if(b1x == b2x){
x1 = x2 = x3 = x4 = b1x;
y1 = p1y;
y2 = p2y;
y3 = p3y;
y4 = p4y;
if(b1y > b2y)
{
if((y1 > b1y && y2 > b1y && y3 > b1y && y4 > b1y) ||
(y1 < b2y && y2 < b2y && y3 < b2y && y4 < b2y))
return false;
}
else
{
if((y1 > b2y && y2 > b2y && y3 > b2y && y4 > b2y) ||
(y1 < b1y && y2 < b1y && y3 < b1y && y4 < b1y))
return false;
}
return true;
}
else if(b1y == b2y){
x1 = p1x;
x2 = p2x;
x3 = p3x;
x4 = p4x;
y1 = y2 = y3 = y4 = b1y;
}else{
var a = (b1y - b2y) / (b1x - b2x);
var ia = 1 / a;
var t1 = b2x * a - b2y;
var t2 = 1 / (a + ia);
x1 = (p1y + t1 + p1x * ia) * t2;
x2 = (p2y + t1 + p2x * ia) * t2;
x3 = (p3y + t1 + p3x * ia) * t2;
x4 = (p4y + t1 + p4x * ia) * t2;
y1 = p1y + (p1x - x1) * ia;
y2 = p2y + (p2x - x2) * ia;
y3 = p3y + (p3x - x3) * ia;
y4 = p4y + (p4x - x4) * ia;
}
if(b1x > b2x){
if((x1 > b1x && x2 > b1x && x3 > b1x && x4 > b1x) ||
(x1 < b2x && x2 < b2x && x3 < b2x && x4 < b2x))
return false;
}else{
if((x1 > b2x && x2 > b2x && x3 > b2x && x4 > b2x) ||
(x1 < b1x && x2 < b1x && x3 < b1x && x4 < b1x))
return false;
}
return true;
}
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>
Red car angle: <input id=car1Angle type=range min=0 max=360 value=0><br>
Gold car angle: <input id=car2Angle type=range min=0 max=360 value=0><br>
<h4>Use sliders above to rotate cars.<br>Drag cars closer.<br>Bounding Circles turn green if they collide.<br>Bounding Rectangles turn green if they collide.</h4>
<br>
<canvas id="canvas" width=400 height=500></canvas>
The demo above is best viewed in full-screen mode
Related
I am trying to make a website to show visually the working of Midpoint Line Algorithm. I want to display the dots after a specific time interval but the setTimeout() function is not working properly. It is simply showing the end result after waiting for 3 seconds.
<canvas id="ltpcanvas" style="border: 1px solid #000000;">
</canvas>
<script>
window.onload = function(){
var ltpc = document.getElementById('ltpcanvas');
ltpc.width = window.innerWidth;
ltpc.height = window.innerHeight;
var context = ltpc.getContext('2d');
var cx1,cx2,cy1,cy2;
var count=0;
ltpc.addEventListener('mousedown',onDown,false);
function onDown(event){
count++;
cx = event.pageX;
cy = event.pageY;
cy-=9;
cx-=9;
if(count == 1){
cx1=cx;
cy1=cy;
}
else{
cx2=cx;
cy2=cy;
}
var ltpc = document.getElementById('ltpcanvas');
var context = ltpc.getContext('2d');
context.fillStyle = 'black';
context.beginPath();
context.arc(cx,cy,2,0*Math.PI,2*Math.PI,true);
context.closePath();
context.fill();
if(cx1!=undefined && cx2!=undefined && cy1!=undefined && cy2!=undefined){
if(cx1<cx2&&cy1<cy2)
midpoint(cx1,cy1,cx2,cy2);
else{
alert('cx1 = '+cx1+' cy1 = '+cy1+'\ncx2 = '+cx2+' cy2 = '+cy2);
}
}
}
}
function midpoint(X1,Y1,X2,Y2){
var dx = X2 - X1;
var dy = Y2 - Y1;
var d = dy - (dx/2);
var x = X1
var y = Y1;
while (x < X2)
{
x++;
if (d < 0)
d = d + dy;
else
{
d += (dy - dx);
y++;
}
myvar = setTimeout(Dotfunction,3000,x,y);
console.log(x);
console.log(y);
}
}
function Dotfunction(x,y){
var ltpc = document.getElementById('ltpcanvas');
var context = ltpc.getContext('2d');
context.fillStyle = 'black';
context.beginPath();
context.arc(x,y,1,0*Math.PI,2*Math.PI,true);
context.closePath();
context.fill();
}
</script>
I am trying to delay the execution of Dotfunction() every time but it is delaying for 3 seconds and then showing the entire line. I want it to delay after displaying every dot.
I want to display the dots after a specific time interval
The important thing here is that we want to show not "the dots", but "each dot" after an interval. This implies that each dot will be drawn on its own. I assume you want the dots to appear one after another in an animation.
The problem is that in this code we are calling setTimeout() multiple times with the same target delay. I.e. we tell the engine "do this in 3 seconds" many times within a single instant. The easiest solution to get an animation is to pass increasingly higher timeouts. Since we call the function within a loop and since we have a counting variable in it already, we can just reuse that and ask the JS engine to draw each point 10 milliseconds after the previous one.
The following code draws a line with a speed of 100 pixels per second.
PS: I removed the body margin, so that there is no longer the need to subtract that from the cursor position.
window.onload = function(){
var ltpc = document.getElementById('ltpcanvas');
ltpc.width = window.innerWidth;
ltpc.height = window.innerHeight;
var context = ltpc.getContext('2d');
var cx1, cx2, cy1, cy2;
var count = 0;
ltpc.addEventListener('mousedown', onDown, false);
function onDown(event) {
count++;
cx = event.pageX;
cy = event.pageY;
if (count == 1) {
cx1 = cx;
cy1 = cy;
}
else {
cx2 = cx;
cy2 = cy;
}
var ltpc = document.getElementById('ltpcanvas');
var context = ltpc.getContext('2d');
context.fillStyle = 'black';
context.beginPath();
context.arc(cx, cy, 2, 0 * Math.PI, 2 * Math.PI, true);
context.closePath();
context.fill();
if (cx1 != undefined && cx2 != undefined && cy1 != undefined && cy2 != undefined) {
if (cx1 < cx2 && cy1 < cy2)
midpoint(cx1, cy1, cx2, cy2);
else {
// alert('cx1 = '+cx1+' cy1 = '+cy1+'\ncx2 = '+cx2+' cy2 = '+cy2);
}
}
}
}
function midpoint(X1, Y1, X2, Y2){
var dx = X2 - X1;
var dy = Y2 - Y1;
var d = dy - (dx / 2);
var x = X1
var y = Y1;
while (x < X2)
{
x++;
if (d < 0)
d = d + dy;
else {
d += (dy - dx);
y++;
}
setTimeout(Dotfunction, x * 10, x, y);
//console.log(x);
//console.log(y);
}
}
function Dotfunction(x, y) {
var ltpc = document.getElementById('ltpcanvas');
var context = ltpc.getContext('2d');
context.fillStyle = 'black';
context.beginPath();
context.arc(x, y, 1,0 * Math.PI, 2 * Math.PI, true);
context.closePath();
context.fill();
}
body {
margin: 0;
}
<canvas id="ltpcanvas" style="border: 1px solid #000000;"></canvas>
Sorry for the confusing title, I don't know how to succinctly describe my question.
I'm drawing an ellipse on a canvas element using javascript and I'm trying to figure out how to detect if the mouse is clicked inside of the ellipse or not. The way I'm trying to do this is by comparing the distance from the center of the ellipse to the mouse to the radius of the ellipse at the same angle as the mouse click. Here's a terrible picture representing what I just said if it's still confusing:
Obviously this isn't working, otherwise I wouldn't be asking this, so below is a picture of the computed radius line (in red) and the mouse line (in blue). In this picture, the mouse has been clicked at a 45° angle to the center of the ellipse and I've calculated that the radius line is being drawn at about a 34.99° angle.
And below is the calculation code:
//This would be the blue line in the picture above
var mouseToCenterDistance = distanceTo(centerX, centerY, mouseX, mouseY);
var angle = Math.acos((mouseX - centerX) / mouseToCenterDistance);
var radiusPointX = (radiusX * Math.cos(angle)) + centerX;
var radiusPointY = (radiusY * Math.sin(-angle)) + centerY;
//This would be the red line in the picture above
var radius = distanceTo(centerX, centerY, radiusPointX, radiusPointY);
var clickedInside = mouseToCenterDistance <= radius;
I'm really not sure why this isn't working, I've been staring at this math forever and it seems correct. Is it correct and there's something about drawing on the canvas that's making it not work? Please help!
Ellipse line intercept
Finding the intercept includes solving if the point is inside.
If it is the ellipse draw via the 2D context the solution is as follows
// defines the ellipse
var cx = 100; // center
var cy = 100;
var r1 = 20; // radius 1
var r2 = 100; // radius 2
var ang = 1; // angle in radians
// rendered with
ctx.beginPath();
ctx.ellipse(cx,cy,r1,r2,ang,0,Math.PI * 2,true)
ctx.stroke()
To find the point on the ellipse that intersects the line from the center to x,y. To solve I normalise the ellipse so that it is a circle (well the line is moved so that the ellipse is a circle in its coordinate space).
var x = 200;
var y = 200;
var ratio = r1 / r2; // need the ratio between the two radius
// get the vector from the ellipse center to end of line
var dx = x - cx;
var dy = y - cy;
// get the vector that will normalise the ellipse rotation
var vx = Math.cos(-ang);
var vy = Math.sin(-ang);
// use that vector to rotate the line
var ddx = dx * vx - dy * vy;
var ddy = (dx * vy + dy * vx) * ratio; // lengthen or shorten dy
// get the angle to the line in normalise circle space.
var c = Math.atan2(ddy,ddx);
// get the vector along the ellipse x axis
var eAx = Math.cos(ang);
var eAy = Math.sin(ang);
// get the intercept of the line and the normalised ellipse
var nx = Math.cos(c) * r1;
var ny = Math.sin(c) * r2;
// rotate the intercept to the ellipse space
var ix = nx * eAx - ny * eAy
var iy = nx * eAy + ny * eAx
// cx,cy to ix ,iy is from the center to the ellipse circumference
The procedure can be optimised but for now that will solve the problem as presented.
Is point inside
Then to determine if the point is inside just compare the distances of the mouse and the intercept point.
var x = 200; // point to test
var y = 200;
// get the vector from the ellipse center to point to test
var dx = x - cx;
var dy = y - cy;
// get the vector that will normalise the ellipse rotation
var vx = Math.cos(ang);
var vy = Math.sin(ang);
// use that vector to rotate the line
var ddx = dx * vx + dy * vy;
var ddy = -dx * vy + dy * vx;
if( 1 >= (ddx * ddx) / (r1 * r1) + (ddy * ddy) / (r2 * r2)){
// point on circumference or inside ellipse
}
Example use of method.
function path(path){
ctx.beginPath();
var i = 0;
ctx.moveTo(path[i][0],path[i++][1]);
while(i < path.length){
ctx.lineTo(path[i][0],path[i++][1]);
}
if(close){
ctx.closePath();
}
ctx.stroke();
}
function strokeCircle(x,y,r){
ctx.beginPath();
ctx.moveTo(x + r,y);
ctx.arc(x,y,r,0,Math.PI * 2);
ctx.stroke();
}
function display() {
ctx.setTransform(1, 0, 0, 1, 0, 0); // reset transform
ctx.globalAlpha = 1; // reset alpha
ctx.clearRect(0, 0, w, h);
var cx = w/2;
var cy = h/2;
var r1 = Math.abs(Math.sin(globalTime/ 4000) * w / 4);
var r2 = Math.abs(Math.sin(globalTime/ 4300) * h / 4);
var ang = globalTime / 1500;
// find the intercept from ellipse center to mouse on the ellipse
var ratio = r1 / r2
var dx = mouse.x - cx;
var dy = mouse.y - cy;
var dist = Math.hypot(dx,dy);
var ex = Math.cos(-ang);
var ey = Math.sin(-ang);
var c = Math.atan2((dx * ey + dy * ex) * ratio, dx * ex - dy * ey);
var nx = Math.cos(c) * r1;
var ny = Math.sin(c) * r2;
var ix = nx * ex + ny * ey;
var iy = -nx * ey + ny * ex;
var dist = Math.hypot(dx,dy);
var dist2Inter = Math.hypot(ix,iy);
ctx.strokeStyle = "Blue";
ctx.lineWidth = 4;
ctx.beginPath();
ctx.ellipse(cx,cy,r1,r2,ang,0,Math.PI * 2,true)
ctx.stroke();
if(dist2Inter > dist){
ctx.fillStyle = "#7F7";
ctx.globalAlpha = 0.5;
ctx.fill();
ctx.globalAlpha = 1;
}
// Display the intercept
ctx.strokeStyle = "black";
ctx.lineWidth = 2;
path([[cx,cy],[mouse.x,mouse.y]])
ctx.strokeStyle = "red";
ctx.lineWidth = 5;
path([[cx,cy],[cx + ix,cy+iy]])
ctx.strokeStyle = "red";
ctx.lineWidth = 4;
strokeCircle(cx + ix, cy + iy, 6)
ctx.fillStyle = "white";
ctx.fill();
ctx.strokeStyle = "red";
ctx.lineWidth = 4;
strokeCircle(cx, cy, 6)
ctx.fillStyle = "white";
ctx.fill();
ctx.strokeStyle = "black";
ctx.lineWidth = 2;
strokeCircle(mouse.x, mouse.y, 4)
ctx.fillStyle = "white";
ctx.fill();
}
/** SimpleFullCanvasMouse.js begin **/
//==============================================================================
// Boilerplate code from here down and not related to the answer
//==============================================================================
var w, h, cw, ch, canvas, ctx, mouse, globalTime = 0, firstRun = true;
;(function(){
const RESIZE_DEBOUNCE_TIME = 100;
var createCanvas, resizeCanvas, setGlobals, resizeCount = 0;
createCanvas = function () {
var c,
cs;
cs = (c = document.createElement("canvas")).style;
cs.position = "absolute";
cs.top = cs.left = "0px";
cs.zIndex = 1000;
document.body.appendChild(c);
return c;
}
resizeCanvas = function () {
if (canvas === undefined) {
canvas = createCanvas();
}
canvas.width = innerWidth;
canvas.height = innerHeight;
ctx = canvas.getContext("2d");
if (typeof setGlobals === "function") {
setGlobals();
}
if (typeof onResize === "function") {
if(firstRun){
onResize();
firstRun = false;
}else{
resizeCount += 1;
setTimeout(debounceResize, RESIZE_DEBOUNCE_TIME);
}
}
}
function debounceResize() {
resizeCount -= 1;
if (resizeCount <= 0) {
onResize();
}
}
setGlobals = function () {
cw = (w = canvas.width) / 2;
ch = (h = canvas.height) / 2;
}
mouse = (function () {
function preventDefault(e) {
e.preventDefault();
}
var mouse = {
x : 0,
y : 0,
w : 0,
alt : false,
shift : false,
ctrl : false,
buttonRaw : 0,
over : false,
bm : [1, 2, 4, 6, 5, 3],
active : false,
bounds : null,
crashRecover : null,
mouseEvents : "mousemove,mousedown,mouseup,mouseout,mouseover,mousewheel,DOMMouseScroll".split(",")
};
var m = mouse;
function mouseMove(e) {
var t = e.type;
m.bounds = m.element.getBoundingClientRect();
m.x = e.pageX - m.bounds.left;
m.y = e.pageY - m.bounds.top;
m.alt = e.altKey;
m.shift = e.shiftKey;
m.ctrl = e.ctrlKey;
if (t === "mousedown") {
m.buttonRaw |= m.bm[e.which - 1];
} else if (t === "mouseup") {
m.buttonRaw &= m.bm[e.which + 2];
} else if (t === "mouseout") {
m.buttonRaw = 0;
m.over = false;
} else if (t === "mouseover") {
m.over = true;
} else if (t === "mousewheel") {
m.w = e.wheelDelta;
} else if (t === "DOMMouseScroll") {
m.w = -e.detail;
}
if (m.callbacks) {
m.callbacks.forEach(c => c(e));
}
if ((m.buttonRaw & 2) && m.crashRecover !== null) {
if (typeof m.crashRecover === "function") {
setTimeout(m.crashRecover, 0);
}
}
e.preventDefault();
}
m.addCallback = function (callback) {
if (typeof callback === "function") {
if (m.callbacks === undefined) {
m.callbacks = [callback];
} else {
m.callbacks.push(callback);
}
}
}
m.start = function (element) {
if (m.element !== undefined) {
m.removeMouse();
}
m.element = element === undefined ? document : element;
m.mouseEvents.forEach(n => {
m.element.addEventListener(n, mouseMove);
});
m.element.addEventListener("contextmenu", preventDefault, false);
m.active = true;
}
m.remove = function () {
if (m.element !== undefined) {
m.mouseEvents.forEach(n => {
m.element.removeEventListener(n, mouseMove);
});
m.element.removeEventListener("contextmenu", preventDefault);
m.element = m.callbacks = undefined;
m.active = false;
}
}
return mouse;
})();
// Clean up. Used where the IDE is on the same page.
var done = function () {
window.removeEventListener("resize", resizeCanvas)
mouse.remove();
document.body.removeChild(canvas);
canvas = ctx = mouse = undefined;
}
function update(timer) { // Main update loop
if(ctx === undefined){ return; }
globalTime = timer;
display(); // call demo code
requestAnimationFrame(update);
}
setTimeout(function(){
resizeCanvas();
mouse.start(canvas, true);
//mouse.crashRecover = done;
window.addEventListener("resize", resizeCanvas);
requestAnimationFrame(update);
},0);
})();
/** SimpleFullCanvasMouse.js end **/
If you have an ellipse of the form (x-x0)2/a2 + (y-y0)2/b2 = 1, then a point (x, y) is inside the ellipse if and only if (x-x0)2/a2 + (y-y0)2/b2 < 1. You can just test that inequality to see if the mouse is inside the ellipse.
To be able to draw a line to the edge of the ellipse: get the theta of the mouse with atan2 (don't use acos, you'll get incorrect results in quadrants III & IV), use the polar equation of the ellipse to solve for r, then convert back to rectangular coordinates and draw.
I'm attempting to create a simple draw/paint programme using html5 canvas and plain javascript. I've got it working ok, but when drawing and moving the mouse too fast the line disconnects and I just end up with a line of dots - how can I make this a smooth continuous line?
Advice would be much appreciated! I'm quite new to JS so code examples would be really useful, thanks in advance.
Current JS is:
var canvas, ctx
var mouseX, mouseY, mouseDown = 0
function draw(ctx,x,y,size) {
ctx.fillStyle = "#000000"
ctx.beginPath()
ctx.arc(x, y, size, 0, Math.PI*2, true)
ctx.closePath()
ctx.fill()
}
function clearCanvas(canvas,ctx) {
ctx.clearRect(0, 0, canvas.width, canvas.height)
}
function onMouseDown() {
mouseDown = 1
draw(ctx, mouseX, mouseY, 2)
}
function onMouseUp() {
mouseDown = 0
}
function onMouseMove(e) {
getMousePos(e)
if (mouseDown == 1) {
draw(ctx, mouseX, mouseY, 2)
}
}
function getMousePos(e) {
if (!e)
var e = event
if (e.offsetX) {
mouseX = e.offsetX
mouseY = e.offsetY
}
else if (e.layerX) {
mouseX = e.layerX
mouseY = e.layerY
}
}
function init() {
canvas = document.getElementById('sketchpad')
ctx = canvas.getContext('2d')
canvas.addEventListener('mousedown', onMouseDown, false)
canvas.addEventListener('mousemove', onMouseMove, false)
window.addEventListener('mouseup', onMouseUp, false)
}
init();
<canvas id="sketchpad" width="500" height="500"></canvas>
Drawing a smooth curve with the mouse.
Sadly it is not that easy if you wish to stay true to the artists intended line.
It involves recording the whole mouse stroke. When the stroke is complete, reduce the number of points to the detail limit (set by artist) then apply a bezier smoothing function on the remaining points.
It can be done as the stroke is drawn but for some devices this can become too much if the line becomes very long. As the line detail reduction looks at all points when showing the smoothed line live some people dont like the way it slightly changes as the line gets longer.
Demo
The code below demonstrates a solution I have found useful.
Use the left button to draw with smoothing done one button release.
Use the right button to draw with live smoothing (blue line).
Middle mouse button click to clear.
Use the two sliders at the top to set the amount of smoothing, and the amount of detail. Left click to drag out a stroke, the raw line is shown. When the mouse is released the line is then simplified, smoothed, and added to the background image.
var canvas = document.getElementById("canV");
var ctx = canvas.getContext("2d");
// mouse stuff
var mouse = {
x:0,
y:0,
buttonLastRaw:0, // user modified value
buttonRaw:0,
buttons:[1,2,4,6,5,3], // masks for setting and clearing button raw bits;
};
function mouseMove(event){
mouse.x = event.offsetX; mouse.y = event.offsetY;
if(mouse.x === undefined){ mouse.x = event.clientX; mouse.y = event.clientY;}
if(event.type === "mousedown"){ mouse.buttonRaw |= mouse.buttons[event.which-1];
}else if(event.type === "mouseup"){mouse.buttonRaw &= mouse.buttons[event.which+2];
}else if(event.type === "mouseout"){ mouse.buttonRaw = 0; mouse.over = false;
}else if(event.type === "mouseover"){ mouse.over = true; }
event.preventDefault();
}
canvas.addEventListener('mousemove',mouseMove);
canvas.addEventListener('mousedown',mouseMove);
canvas.addEventListener('mouseup' ,mouseMove);
canvas.addEventListener('mouseout' ,mouseMove);
canvas.addEventListener('mouseover' ,mouseMove);
canvas.addEventListener("contextmenu", function(e){ e.preventDefault();}, false);
// Line simplification based on
// the Ramer–Douglas–Peucker algorithm
// referance https://en.wikipedia.org/wiki/Ramer%E2%80%93Douglas%E2%80%93Peucker_algorithm
// points are and array of arrays consisting of [[x,y],[x,y],...,[x,y]]
// length is in pixels and is the square of the actual distance.
// returns array of points of the same form as the input argument points.
var simplifyLineRDP = function(points, length) {
var simplify = function(start, end) { // recursize simplifies points from start to end
var maxDist, index, i, xx , yy, dx, dy, ddx, ddy, p1, p2, p, t, dist, dist1;
p1 = points[start];
p2 = points[end];
xx = p1[0];
yy = p1[1];
ddx = p2[0] - xx;
ddy = p2[1] - yy;
dist1 = (ddx * ddx + ddy * ddy);
maxDist = length;
for (var i = start + 1; i < end; i++) {
p = points[i];
if (ddx !== 0 || ddy !== 0) {
t = ((p[0] - xx) * ddx + (p[1] - yy) * ddy) / dist1;
if (t > 1) {
dx = p[0] - p2[0];
dy = p[1] - p2[1];
} else
if (t > 0) {
dx = p[0] - (xx + ddx * t);
dy = p[1] - (yy + ddy * t);
} else {
dx = p[0] - xx;
dy = p[1] - yy;
}
}else{
dx = p[0] - xx;
dy = p[1] - yy;
}
dist = dx * dx + dy * dy
if (dist > maxDist) {
index = i;
maxDist = dist;
}
}
if (maxDist > length) { // continue simplification while maxDist > length
if (index - start > 1){
simplify(start, index);
}
newLine.push(points[index]);
if (end - index > 1){
simplify(index, end);
}
}
}
var end = points.length - 1;
var newLine = [points[0]];
simplify(0, end);
newLine.push(points[end]);
return newLine;
}
// This is my own smoothing method
// It creates a set of bezier control points either 2nd order or third order
// bezier curves.
// points: list of points
// cornerThres: when to smooth corners and represents the angle between to lines.
// When the angle is smaller than the cornerThres then smooth.
// match: if true then the control points will be balanced.
// Function will make a copy of the points
var smoothLine = function(points,cornerThres,match){ // adds bezier control points at points if lines have angle less than thres
var p1, p2, p3, dist1, dist2, x, y, endP, len, angle, i, newPoints, aLen, closed, bal, cont1, nx1, nx2, ny1, ny2, np;
function dot(x, y, xx, yy) { // get do product
// dist1,dist2,nx1,nx2,ny1,ny2 are the length and normals and used outside function
// normalise both vectors
dist1 = Math.sqrt(x * x + y * y); // get length
if (dist1 > 0) { // normalise
nx1 = x / dist1 ;
ny1 = y / dist1 ;
}else {
nx1 = 1; // need to have something so this will do as good as anything
ny1 = 0;
}
dist2 = Math.sqrt(xx * xx + yy * yy);
if (dist2 > 0) {
nx2 = xx / dist2;
ny2 = yy / dist2;
}else {
nx2 = 1;
ny2 = 0;
}
return Math.acos(nx1 * nx2 + ny1 * ny2 ); // dot product
}
newPoints = []; // array for new points
aLen = points.length;
if(aLen <= 2){ // nothing to if line too short
for(i = 0; i < aLen; i ++){ // ensure that the points are copied
newPoints.push([points[i][0],points[i][1]]);
}
return newPoints;
}
p1 = points[0];
endP =points[aLen-1];
i = 0; // start from second poitn if line not closed
closed = false;
len = Math.hypot(p1[0]- endP[0], p1[1]-endP[1]);
if(len < Math.SQRT2){ // end points are the same. Join them in coordinate space
endP = p1;
i = 0; // start from first point if line closed
p1 = points[aLen-2];
closed = true;
}
newPoints.push([points[i][0],points[i][1]])
for(; i < aLen-1; i++){
p2 = points[i];
p3 = points[i + 1];
angle = Math.abs(dot(p2[0] - p1[0], p2[1] - p1[1], p3[0] - p2[0], p3[1] - p2[1]));
if(dist1 !== 0){ // dist1 and dist2 come from dot function
if( angle < cornerThres*3.14){ // bend it if angle between lines is small
if(match){
dist1 = Math.min(dist1,dist2);
dist2 = dist1;
}
// use the two normalized vectors along the lines to create the tangent vector
x = (nx1 + nx2) / 2;
y = (ny1 + ny2) / 2;
len = Math.sqrt(x * x + y * y); // normalise the tangent
if(len === 0){
newPoints.push([p2[0],p2[1]]);
}else{
x /= len;
y /= len;
if(newPoints.length > 0){
var np = newPoints[newPoints.length-1];
np.push(p2[0]-x*dist1*0.25);
np.push(p2[1]-y*dist1*0.25);
}
newPoints.push([ // create the new point with the new bezier control points.
p2[0],
p2[1],
p2[0]+x*dist2*0.25,
p2[1]+y*dist2*0.25
]);
}
}else{
newPoints.push([p2[0],p2[1]]);
}
}
p1 = p2;
}
if(closed){ // if closed then copy first point to last.
p1 = [];
for(i = 0; i < newPoints[0].length; i++){
p1.push(newPoints[0][i]);
}
newPoints.push(p1);
}else{
newPoints.push([points[points.length-1][0],points[points.length-1][1]]);
}
return newPoints;
}
// creates a drawable image
var createImage = function(w,h){
var image = document.createElement("canvas");
image.width = w;
image.height =h;
image.ctx = image.getContext("2d");
return image;
}
// draws the smoothed line with bezier control points.
var drawSmoothedLine = function(line){
var i,p;
ctx.beginPath()
ctx.moveTo(line[0][0],line[0][1])
for(i = 0; i < line.length-1; i++){
p = line[i];
p1 = line[i+1]
if(p.length === 2){ // linear
ctx.lineTo(p[0],p[1])
}else
if(p.length === 4){ // bezier 2nd order
ctx.quadraticCurveTo(p[2],p[3],p1[0],p1[1]);
}else{ // bezier 3rd order
ctx.bezierCurveTo(p[2],p[3],p[4],p[5],p1[0],p1[1]);
}
}
if(p.length === 2){
ctx.lineTo(p1[0],p1[1])
}
ctx.stroke();
}
// smoothing settings
var liveSmooth;
var lineSmooth = {};
lineSmooth.lengthMin = 8; // square of the pixel length
lineSmooth.angle = 0.8; // angle threshold
lineSmooth.match = false; // not working.
// back buffer to save the canvas allowing the new line to be erased
var backBuffer = createImage(canvas.width,canvas.height);
var currentLine = [];
mouse.lastButtonRaw = 0; // add mouse last incase not there
ctx.lineWidth = 3;
ctx.lineJoin = "round";
ctx.lineCap = "round";
ctx.strokeStyle = "black";
ctx.clearRect(0,0,canvas.width,canvas.height);
var drawing = false; // if drawing
var input = false; // if menu input
var smoothIt = false; // flag to allow feedback that smoothing is happening as it takes some time.
function draw(){
// if not drawing test for menu interaction and draw the menus
if(!drawing){
if(mouse.x < 203 && mouse.y < 24){
if(mouse.y < 13){
if(mouse.buttonRaw === 1){
ctx.clearRect(3,3,200,10);
lineSmooth.angle = (mouse.x-3)/200;
input = true;
}
}else
if(mouse.buttonRaw === 1){
ctx.clearRect(3,14,200,10);
lineSmooth.lengthMin = (mouse.x-3)/10;
input = true;
}
canvas.style.cursor = "pointer";
}else{
canvas.style.cursor = "crosshair";
}
if(mouse.buttonRaw === 0 && input){
input = false;
mouse.lastButtonRaw = 0;
}
ctx.lineWidth = 0.5;
ctx.fillStyle = "red";
ctx.clearRect(3,3,200,10);
ctx.clearRect(3,14,200,10);
ctx.fillRect(3,3,lineSmooth.angle*200,10);
ctx.fillRect(3,14,lineSmooth.lengthMin*10,10);
ctx.textAlign = "left";
ctx.textBaseline = "top";
ctx.fillStyle = "#000"
ctx.strokeRect(3,3,200,10);
ctx.fillText("Smooth "+(lineSmooth.angle * (180 / Math.PI)).toFixed(0)+"deg",5,2)
ctx.strokeRect(3,14,200,10);
ctx.fillText("Detail "+lineSmooth.lengthMin.toFixed(0) + "pixels",5,13);
}else{
canvas.style.cursor = "crosshair";
}
if(!input){
ctx.lineWidth = 3;
if(mouse.buttonRaw === 4 && mouse.lastButtonRaw === 0){
currentLine = [];
drawing = true;
backBuffer.ctx.clearRect(0,0,canvas.width,canvas.height);
backBuffer.ctx.drawImage(canvas,0,0);
currentLine.push([mouse.x,mouse.y])
}else
if(mouse.buttonRaw === 4){
var lp = currentLine[currentLine.length-1]; // get last point
// dont record point if no movement
if(mouse.x !== lp[0] || mouse.y !== lp[1] ){
currentLine.push([mouse.x,mouse.y]);
ctx.beginPath();
ctx.moveTo(lp[0],lp[1])
ctx.lineTo(mouse.x,mouse.y);
ctx.stroke();
liveSmooth = smoothLine(
simplifyLineRDP(
currentLine,
lineSmooth.lengthMin
),
lineSmooth.angle,
lineSmooth.match
);
ctx.clearRect(0,0,canvas.width,canvas.height);
ctx.drawImage(backBuffer,0,0);
ctx.strokeStyle = "Blue";
drawSmoothedLine(liveSmooth );
ctx.strokeStyle = "black";
}
}else
if(mouse.buttonRaw === 0 && mouse.lastButtonRaw === 4){
ctx.textAlign = "center"
ctx.fillStyle = "red"
ctx.fillText("Smoothing...",canvas.width/2,canvas.height/5);
smoothIt = true;
}else
if(smoothIt){
smoothIt = false;
var newLine = smoothLine(
simplifyLineRDP(
currentLine,
lineSmooth.lengthMin
),
lineSmooth.angle,
lineSmooth.match
);
ctx.clearRect(0,0,canvas.width,canvas.height);
ctx.drawImage(backBuffer,0,0);
drawSmoothedLine(newLine);
drawing = false;
}
if(mouse.buttonRaw === 1 && mouse.lastButtonRaw === 0){
currentLine = [];
drawing = true;
backBuffer.ctx.clearRect(0,0,canvas.width,canvas.height);
backBuffer.ctx.drawImage(canvas,0,0);
currentLine.push([mouse.x,mouse.y])
}else
if(mouse.buttonRaw === 1){
var lp = currentLine[currentLine.length-1]; // get last point
// dont record point if no movement
if(mouse.x !== lp[0] || mouse.y !== lp[1] ){
currentLine.push([mouse.x,mouse.y]);
ctx.beginPath();
ctx.moveTo(lp[0],lp[1])
ctx.lineTo(mouse.x,mouse.y);
ctx.stroke();
}
}else
if(mouse.buttonRaw === 0 && mouse.lastButtonRaw === 1){
ctx.textAlign = "center"
ctx.fillStyle = "red"
ctx.fillText("Smoothing...",canvas.width/2,canvas.height/5);
smoothIt = true;
}else
if(smoothIt){
smoothIt = false;
var newLine = smoothLine(
simplifyLineRDP(
currentLine,
lineSmooth.lengthMin
),
lineSmooth.angle,
lineSmooth.match
);
ctx.clearRect(0,0,canvas.width,canvas.height);
ctx.drawImage(backBuffer,0,0);
drawSmoothedLine(newLine);
drawing = false;
}
}
// middle button clear
if(mouse.buttonRaw === 2){
ctx.clearRect(0,0,canvas.width,canvas.height);
}
mouse.lastButtonRaw = mouse.buttonRaw;
requestAnimationFrame(draw);
}
draw();
.canC { width:1000px; height:500px; border:1px black solid;}
<canvas class="canC" id="canV" width=1000 height=500></canvas>
You could save the last position and draw a line between the last point and the actual point.
if (lastX && lastY && (x !== lastX || y !== lastY)) {
ctx.fillStyle = "#000000";
ctx.lineWidth = 2 * size;
ctx.beginPath();
ctx.moveTo(lastX, lastY);
ctx.lineTo(x, y);
ctx.stroke();
// ...
lastX = x;
lastY = y;
}
On mouseup event set the two variables to zero.
var canvas, ctx
var mouseX, mouseY, mouseDown = 0,
lastX, lastY;
function draw(ctx,x,y,size) {
if (lastX && lastY && (x !== lastX || y !== lastY)) {
ctx.fillStyle = "#000000";
ctx.lineWidth = 2 * size;
ctx.beginPath();
ctx.moveTo(lastX, lastY);
ctx.lineTo(x, y);
ctx.stroke();
}
ctx.fillStyle = "#000000";
ctx.beginPath();
ctx.arc(x, y, size, 0, Math.PI*2, true);
ctx.closePath();
ctx.fill();
lastX = x;
lastY = y;
}
function clearCanvas(canvas,ctx) {
ctx.clearRect(0, 0, canvas.width, canvas.height)
}
function onMouseDown() {
mouseDown = 1
draw(ctx, mouseX, mouseY, 2)
}
function onMouseUp() {
mouseDown = 0;
lastX = 0;
lastY = 0;
}
function onMouseMove(e) {
getMousePos(e)
if (mouseDown == 1) {
draw(ctx, mouseX, mouseY, 2)
}
}
function getMousePos(e) {
if (!e)
var e = event
if (e.offsetX) {
mouseX = e.offsetX
mouseY = e.offsetY
}
else if (e.layerX) {
mouseX = e.layerX
mouseY = e.layerY
}
}
function init() {
canvas = document.getElementById('sketchpad')
ctx = canvas.getContext('2d')
canvas.addEventListener('mousedown', onMouseDown, false)
canvas.addEventListener('mousemove', onMouseMove, false)
window.addEventListener('mouseup', onMouseUp, false)
}
init();
<canvas id="sketchpad" width="600" height="300"></canvas>
Good question! And I recommend you a site https://developer.mozilla.org/zh-CN/docs/Web/API/Canvas_API to learn more canvas API.
I think using lineTo is better than arc.So I hope this code will help you.
var canvas, ctx;
var mouseDown = 0, lastX, lastY;
function draw(ctx,x,y) {
ctx.beginPath();
ctx.moveTo(lastX,lastY);
ctx.lineTo(x,y);
ctx.closePath();
ctx.stroke();
}
function clearCanvas(canvas,ctx) {
ctx.clearRect(0, 0, canvas.width, canvas.height)
}
function onMouseDown(e) {
var xy = getMousePos(e);
lastX = xy.mouseX;
lastY = xy.mouseY;
mouseDown = 1;
}
function onMouseUp() {
mouseDown = 0
}
function onMouseMove(e) {
if (mouseDown == 1) {
var xy = getMousePos(e);
draw(ctx, xy.mouseX, xy.mouseY);
lastX = xy.mouseX, lastY = xy.mouseY;
}
}
function getMousePos(e) {
var o = {};
if (!e)
var e = event
if (e.offsetX) {
o.mouseX = e.offsetX
o.mouseY = e.offsetY
}
else if (e.layerX) {
o.mouseX = e.layerX
o.mouseY = e.layerY
}
return o;
}
function init() {
canvas = document.getElementById('sketchpad')
ctx = canvas.getContext('2d')
canvas.addEventListener('mousedown', onMouseDown, false)
canvas.addEventListener('mousemove', onMouseMove, false)
canvas.addEventListener('mouseup', onMouseUp, false)
}
init();
I want to get the rendered size (width/height) of a bézier curve in HTML5 canvas
context.bezierCurveTo(cp1x, cp1y, cp2x, cp2y, x, y);
with this code, for instance
// control points
var cp1x = 200,
cp1y = 150,
cp2x = 260,
cp2y = 10;
var x = 0,
y = 0;
// calculation
var curveWidth = cp1x > x ? cp1x - x : x - cp1x,
curveHeight = cp1y > y ? cp1y - y : y - cp1y;
However, the cp2 point can increase the curve distance (length, size). I.e., suppose cp2 point is the red point in this image and its x coordinate is bigger than cp1's x, which looks to be the end point of the bézier curve:
So, how can I consider the length of cp2 point in curveWidth and in curveHeight to be exact?
To get extent of a quadratic bezier
The points
var x1 = ? // start
var y1 = ?
var x2 = ? // control
var y2 = ?
var x3 = ? // end
var y3 = ?
The extent
extent = {
minx : null,
miny : null,
maxx : null,
maxy : null,
}
The Math.
These equation apply for the x and y axis (thus two equations)
For quadratic bezier
F(u) = a(1-u)^2+2b(1-u)u+cu^2
which is more familiar in the form of a quadratic equation
Ax^2 + Bx + C = 0
so the bezier rearranged
F(u) = (a-2b+c)u^2+2(-a+b)u+a
We need the derivative so that becomes
2(a-2b+c)u-2a+2b
simplify divide common factor 2 to give
(a-2b+c)u + b - a = 0
separate out u
b-a = (a-2b + c)u
(b-a) / (a - 2b + c) = u
Then algorithm optimised for the fact the (b-a) part of (a-2b-c)
function solveB2(a,b,c){
var ba = b-a;
return ba / (ba - (c-b)); // the position on the curve of the maxima
}
var ux = solveB2(x1,x2,x3);
var uy = solveB2(y1,y2,y3);
These two values are positions along the curve so we now just have to find the coordinates of these two points. We need a function that finds a point on a quadratic bezier
function findPoint(u,x1,y1,x2,y2,x3,y3){ // returns array with x, and y at u
var xx1 = x1 + (x2 - x1) * u;
var yy1 = y1 + (y2 - y1) * u;
var xx2 = x2 + (x3 - x2) * u;
var yy2 = y2 + (y3 - y2) * u;
return [
xx1 + (xx2 - xx1) * u,
yy1 + (yy2 - yy1) * u
]
}
First check that they are on the curve and find the point at ux,uy
if(ux >= 0 && ux <= 1){
var px = findPoint(ux,x1,y1,x2,y2,x3,y3);
}
if(uy >= 0 && uy <= 1){
var py = findPoint(uy,x1,y1,x2,y2,x3,y3);
}
Now test against the extent
extent.minx = Math.min(x1,x3,px[0],py[0]);
extent.miny = Math.min(y1,y3,px[1],py[1]);
extent.maxx = Math.max(x1,x3,px[0],py[0]);
extent.maxy = Math.max(y1,y3,px[1],py[1]);
And you are done
extent has the coordinates of the box around the bezier. Top left (min) and bottom right (max)
You can also get the minimum bounding box if you rotate the bezier so that the start and end points fall on the x axis. Then do the above and the resulting rectangle is the minimum sized rectangle that can be placed around the bezier.
Cubics are much the same but just a lot more typing.
And a demo, just to make sure I got it all correct.
var canvas = document.createElement("canvas");
canvas.width = 800;
canvas.height = 400;
var ctx = canvas.getContext("2d");
document.body.appendChild(canvas);
var x1,y1,x2,y2,x3,y3;
var ux,uy,px,py;
var extent = {
minx : null,
miny : null,
maxx : null,
maxy : null,
}
function solveB2(a,b,c){
var ba = b-a;
return ba / (ba - (c-b)); // the position on the curve of the maxima
}
function findPoint(u,x1,y1,x2,y2,x3,y3){ // returns array with x, and y at u
var xx1 = x1 + (x2 - x1) * u;
var yy1 = y1 + (y2 - y1) * u;
var xx2 = x2 + (x3 - x2) * u;
var yy2 = y2 + (y3 - y2) * u;
return [
xx1 + (xx2 - xx1) * u,
yy1 + (yy2 - yy1) * u
]
}
function update(time){
ctx.clearRect(0,0,800,400);
// create random bezier
x1 = Math.cos(time / 1000) * 300 + 400;
y1 = Math.sin(time / 2100) * 150 + 200;
x2 = Math.cos((time + 3000) / 1200) * 300 + 400;
y2 = Math.sin(time / 2300) * 150 + 200;
x3 = Math.cos(time / 1400) * 300 + 400;
y3 = Math.sin(time / 2500) * 150 + 200;
// solve for bounds
var ux = solveB2(x1,x2,x3);
var uy = solveB2(y1,y2,y3);
if(ux >= 0 && ux <= 1){
px = findPoint(ux,x1,y1,x2,y2,x3,y3);
}else{
px = [x1,y1]; // a bit of a cheat but saves having to put in extra conditions
}
if(uy >= 0 && uy <= 1){
py = findPoint(uy,x1,y1,x2,y2,x3,y3);
}else{
py = [x3,y3]; // a bit of a cheat but saves having to put in extra conditions
}
extent.minx = Math.min(x1,x3,px[0],py[0]);
extent.miny = Math.min(y1,y3,px[1],py[1]);
extent.maxx = Math.max(x1,x3,px[0],py[0]);
extent.maxy = Math.max(y1,y3,px[1],py[1]);
// draw the rectangle
ctx.strokeStyle = "red";
ctx.lineWidth = 2;
ctx.strokeRect(extent.minx,extent.miny,extent.maxx-extent.minx,extent.maxy-extent.miny);
ctx.fillStyle = "rgba(255,200,0,0.2)";
ctx.fillRect(extent.minx,extent.miny,extent.maxx-extent.minx,extent.maxy-extent.miny);
// show points
ctx.fillStyle = "blue";
ctx.fillRect(x1-3,y1-3,6,6);
ctx.fillRect(x3-3,y3-3,6,6);
ctx.fillStyle = "black";
ctx.fillRect(px[0]-4,px[1]-4,8,8);
ctx.fillRect(py[0]-4,py[1]-4,8,8);
ctx.lineWidth = 3;
ctx.strokeStyle = "black";
ctx.beginPath();
ctx.moveTo(x1,y1);
ctx.quadraticCurveTo(x2,y2,x3,y3);
ctx.stroke();
// control point
ctx.lineWidth = 1;
ctx.strokeStyle = "#0a0";
ctx.strokeRect(x2-3,y2-3,6,6);
ctx.beginPath();
ctx.moveTo(x1,y1);
ctx.lineTo(x2,y2);
ctx.lineTo(x3,y3);
ctx.stroke();
// do it all again
requestAnimationFrame(update);
}
requestAnimationFrame(update);
UPDATE
While musing over the bezier I realised that I could remove a lot of code if I assumed that the bezier was normalised (the end points start at (0,0) and end at (1,1)) because the zeros can be removed and the ones simplified.
While changing the code I also realized that I had needlessly calculated the x and y for both the x and y extent coordinates. Giving 4 values while I only need 2.
The resulting code is much simpler. I remove the function solveB2 and findPoint as the calculations seam too trivial to bother with functions.
To find the x and y maxima from quadratic bezier defined with x1, y1, x2, y2, x3, y3
// solve quadratic for bounds by normalizing equation
var brx = x3 - x1; // get x range
var bx = x2 - x1; // get x control point offset
var x = bx / brx; // normalise control point which is used to check if maxima is in range
// do the same for the y points
var bry = y3 - y1;
var by = y2 - y1
var y = by / bry;
var px = [x1,y1]; // set defaults in case maximas outside range
if(x < 0 || x > 1){ // check if x maxima is on the curve
px[0] = bx * bx / (2 * bx - brx) + x1; // get the x maxima
}
if(y < 0 || y > 1){ // same as x
px[1] = by * by / (2 * by - bry) + y1;
}
// now only need to check the x and y maxima not the coordinates of the maxima
extent.minx = Math.min(x1,x3,px[0]);
extent.miny = Math.min(y1,y3,px[1]);
extent.maxx = Math.max(x1,x3,px[0]);
extent.maxy = Math.max(y1,y3,px[1]);
And the example code which has far better performance but unlike the previous demo this version does not calculate the actual coordinates of the x and y maximas.
var canvas = document.createElement("canvas");
canvas.width = 800;
canvas.height = 400;
var ctx = canvas.getContext("2d");
document.body.appendChild(canvas);
var x1,y1,x2,y2,x3,y3;
var ux,uy,px,py;
var extent = {
minx : null,
miny : null,
maxx : null,
maxy : null,
}
function update(time){
ctx.clearRect(0,0,800,400);
// create random bezier
x1 = Math.cos(time / 1000) * 300 + 400;
y1 = Math.sin(time / 2100) * 150 + 200;
x2 = Math.cos((time + 3000) / 1200) * 300 + 400;
y2 = Math.sin(time / 2300) * 150 + 200;
x3 = Math.cos(time / 1400) * 300 + 400;
y3 = Math.sin(time / 2500) * 150 + 200;
// solve quadratic for bounds by normalizing equation
var brx = x3 - x1; // get x range
var bx = x2 - x1; // get x control point offset
var x = bx / brx; // normalise control point which is used to check if maxima is in range
// do the same for the y points
var bry = y3 - y1;
var by = y2 - y1
var y = by / bry;
var px = [x1,y1]; // set defaults in case maximas outside range
if(x < 0 || x > 1){ // check if x maxima is on the curve
px[0] = bx * bx / (2 * bx - brx) + x1; // get the x maxima
}
if(y < 0 || y > 1){ // same as x
px[1] = by * by / (2 * by - bry) + y1;
}
// now only need to check the x and y maxima not the coordinates of the maxima
extent.minx = Math.min(x1,x3,px[0]);
extent.miny = Math.min(y1,y3,px[1]);
extent.maxx = Math.max(x1,x3,px[0]);
extent.maxy = Math.max(y1,y3,px[1]);
// draw the rectangle
ctx.strokeStyle = "red";
ctx.lineWidth = 2;
ctx.strokeRect(extent.minx,extent.miny,extent.maxx-extent.minx,extent.maxy-extent.miny);
ctx.fillStyle = "rgba(255,200,0,0.2)";
ctx.fillRect(extent.minx,extent.miny,extent.maxx-extent.minx,extent.maxy-extent.miny);
// show points
ctx.fillStyle = "blue";
ctx.fillRect(x1-3,y1-3,6,6);
ctx.fillRect(x3-3,y3-3,6,6);
ctx.fillStyle = "black";
ctx.fillRect(px[0]-4,px[1]-4,8,8);
ctx.lineWidth = 3;
ctx.strokeStyle = "black";
ctx.beginPath();
ctx.moveTo(x1,y1);
ctx.quadraticCurveTo(x2,y2,x3,y3);
ctx.stroke();
// control point
ctx.lineWidth = 1;
ctx.strokeStyle = "#0a0";
ctx.strokeRect(x2-3,y2-3,6,6);
ctx.beginPath();
ctx.moveTo(x1,y1);
ctx.lineTo(x2,y2);
ctx.lineTo(x3,y3);
ctx.stroke();
// do it all again
requestAnimationFrame(update);
}
requestAnimationFrame(update);
I found this in a stackoverflow question on how to draw in canvas http://jsfiddle.net/ArtBIT/kneDX/ but I want it to draw lines continuously without the circles but smooth lines. The code to be changed is below:
ctx.fillCircle = function(x, y, radius, fillColor) {
this.fillStyle = fillColor;
this.beginPath();
this.moveTo(x, y);
this.arc(x, y, radius, 0, Math.PI * 2, false);
this.fill();
};
You can use Bresenham's line algorithm to find all the points between the mouse start and end, and then fill the points between using fillRect. The reason you need to use the line algorithm is because if a user moves the mouse too fast you wont get solid lines, but lines with gaps. I modified your function quite a bit to do this. You can also pass a line thickness value to change how large you want the stroke to be. Note the same could be applied using arcs I just prefer rect
Live Demo
(function() {
// Creates a new canvas element and appends it as a child
// to the parent element, and returns the reference to
// the newly created canvas element
function createCanvas(parent, width, height) {
var canvas = {};
canvas.node = document.createElement('canvas');
canvas.context = canvas.node.getContext('2d');
canvas.node.width = width || 100;
canvas.node.height = height || 100;
parent.appendChild(canvas.node);
canvas.lastX = 0;
canvas.lastY = 0;
return canvas;
}
function init(container, width, height, fillColor) {
var canvas = createCanvas(container, width, height);
var ctx = canvas.context;
// define a custom fillCircle method
ctx.fillCircle = function(x1, y1, x2, y2, fillColor, lineThickness) {
this.fillStyle = fillColor;
var steep = (Math.abs(y2 - y1) > Math.abs(x2 - x1));
if (steep) {
var x = x1;
x1 = y1;
y1 = x;
var y = y2;
y2 = x2;
x2 = y;
}
if (x1 > x2) {
var x = x1;
x1 = x2;
x2 = x;
var y = y1;
y1 = y2;
y2 = y;
}
var dx = x2 - x1,
dy = Math.abs(y2 - y1),
error = 0,
de = dy / dx,
yStep = -1,
y = y1;
if (y1 < y2) {
yStep = 1;
}
for (var x = x1; x < x2; x++) {
if (steep) {
this.fillRect(y, x, lineThickness, lineThickness);
} else {
this.fillRect(x, y, lineThickness, lineThickness);
}
error += de;
if (error >= 0.5) {
y += yStep;
error -= 1.0;
}
}
};
ctx.clearTo = function(fillColor) {
ctx.fillStyle = fillColor;
ctx.fillRect(0, 0, width, height);
};
ctx.clearTo(fillColor || "#ddd");
// bind mouse events
canvas.node.onmousemove = function(e) {
if (!canvas.isDrawing) {
return;
}
mouseX = e.pageX - this.offsetLeft;
mouseY = e.pageY - this.offsetTop;
ctx.fillCircle(mouseX,mouseY,canvas.lastX,canvas.lastY,"#000",1);
canvas.lastX = mouseX;
canvas.lastY = mouseY;
};
canvas.node.onmousedown = function(e) {
canvas.isDrawing = true;
canvas.lastX = e.pageX - this.offsetLeft;
canvas.lastY = e.pageY - this.offsetTop;
};
canvas.node.onmouseup = function(e) {
canvas.isDrawing = false;
};
}
var container = document.getElementById('canvas');
init(container, 200, 200, '#ddd');
})();
If i understand,you need this: http://www.html5canvastutorials.com/tutorials/html5-canvas-lines/
This script draw a line from 0,0 to mouse.
window.event.clientX = mouse x coordinate
window.event.clientY = mouse y coordinate
<script>
context.beginPath();
context.moveTo(0,0);
context.lineTo(window.event.clientX,window.event.clientY);
context.stroke();
</script>