How to animate tree in HTML5 canvas - javascript

I drew this tree in canvas and now i want to animate it and make it look like it is growing. I am a beginner with animations so i would be happy for any input and ideas! Also the tree is supposed to grow differently every time and not look the same if that is possible.
Thanks!
<!DOCTYPE html>
<html lang="en">
<head>
<title>tree</title>
</head>
<body onload="init();">
<canvas id="canvas" width="1200" height="600" ></canvas>
<script type="text/javascript" >
init = function() {
var x1 = 400;
var y1 = 300;
var x2 = 400;
var y2 = 200;
var angle = 0.1 * Math.PI;
var depth = 6;
drawTree( x1, y1, x2, y2, angle, depth );
}
drawTree = function( x1, y1, x2, y2, angle, depth ){
var context = document.getElementById('canvas').getContext('2d');
context.strokeStyle = 'rgb( 0, 0, 0 )';
context.lineWidth = depth;
context.beginPath();
context.moveTo( x1, y1 );
context.lineTo( x2, y2 );
context.stroke();
if( depth > 0 ){
var x = x2 - x1;
var y = y2 - y1;
var scale = 0.5 + Math.random() * 0.5;
x *= scale;
y *= scale;
var xLeft = x * Math.cos( -angle ) - y * Math.sin( -angle );
var yLeft = x * Math.sin( -angle ) + y * Math.cos( -angle );
var xRight = x * Math.cos( +angle ) - y * Math.sin( +angle );
var yRight = x * Math.sin( +angle ) + y * Math.cos( +angle );
xLeft += x2;
yLeft += y2;
xRight += x2;
yRight += y2;
drawTree( x2, y2, xLeft, yLeft, angle, depth - 1 );
drawTree( x2, y2, xRight, yRight, angle, depth - 1 );
}
}
</script>
</body>
</html>

You can animate your growing tree by separating your tree-line definitions and your drawings into 2 separate steps. That way you can animate the drawing of your tree-lines.
Define all the lines that make up your tree.
Use an animation loop to draw those lines starting with the main branch and ending with the smallest twigs.
Here's annotated code and a Demo:
var canvas=document.getElementById("canvas");
var context=canvas.getContext("2d");
var cw=canvas.width;
var ch=canvas.height;
// tree definitions
var x1 = 200;
var y1 = 500;
var x2 = 200;
var y2 = 400;
// growing definitions
var angle = 0.1 * Math.PI;
var depth = 6;
// save segments for later animation
var branches=[];
for(var i=0;i<=depth;i++){branches.push([]);}
var segments=[];
var segmentIndex=0;
// animation variables
var nextTime=0;
var delay=16*5;
///////////// Do stuff!
// define the tree
defineTree( x1, y1, x2, y2, angle, depth );
// create a combined array of segments to be drawn with animation
for(var i=branches.length-1;i>=0;i--){
segments=segments.concat(branches[i]);
}
// load leaf images and then start animating
var leaves=new Image();
leaves.onload=function(){
// animate drawing the tree
requestAnimationFrame(animate);
}
leaves.src='https://dl.dropboxusercontent.com/u/139992952/multple/leaves.png';
///////////// functions
// function to reiteratively define all segments of a tree
function defineTree( x1, y1, x2, y2, angle, depth ){
var segment={
x1:x1,y1:y1,
x2:x2,y2:y2,
linewidth:depth,
};
branches[depth].push(segment);
if( depth > 0 ){
var x = x2 - x1;
var y = y2 - y1;
var scale = 0.5 + Math.random() * 0.5;
x *= scale;
y *= scale;
var xLeft = x * Math.cos( -angle ) - y * Math.sin( -angle );
var yLeft = x * Math.sin( -angle ) + y * Math.cos( -angle );
var xRight = x * Math.cos( +angle ) - y * Math.sin( +angle );
var yRight = x * Math.sin( +angle ) + y * Math.cos( +angle );
xLeft += x2;
yLeft += y2;
xRight += x2;
yRight += y2;
defineTree( x2, y2, xLeft, yLeft, angle, depth - 1 );
defineTree( x2, y2, xRight, yRight, angle, depth - 1 );
}
}
// draw 1 segment of the tree
function drawSegment(segment){
context.strokeStyle = 'rgb( 0, 0, 0 )';
context.lineWidth = segment.linewidth;
context.beginPath();
context.moveTo( segment.x1, segment.y1 );
context.lineTo( segment.x2, segment.y2 );
context.stroke();
//
if(segment.linewidth==0){
var dx=segment.x2-segment.x1;
var dy=segment.y2-segment.y1;
var angle=Math.atan2(dy,dx)+Math.PI/2;
var i=parseInt(Math.random()*2.99);
var j=parseInt(Math.random()*1.99);
context.save();
context.translate(segment.x2,segment.y2);
context.rotate(angle);
context.scale(.25,.25);
context.drawImage(leaves,127*i,142*j,127,142,-127/2,-142/2,127,142);
context.restore();
}
}
// animate drawing each segment of the tree
function animate(currentTime){
// request another loop until all segments have been drawn
if(segmentIndex<segments.length){
requestAnimationFrame(animate);
}
// delay until nextTime
if(currentTime<nextTime){return;}
// set the new nextTime
nextTime=currentTime+delay;
// draw the current segment
drawSegment(segments[segmentIndex]);
// increment the segmentIndex for next loop
segmentIndex++;
}
body{ background-color: ivory; padding:10px; }
#canvas{border:1px solid red;}
<canvas id="canvas" width=400 height=500></canvas>

There's a whole science behind this, I am afraid noone will give you a simple, clean solution. Of course, I dare anyone to prove me wrong, I would like to see that myself. :)
But try to read something about L-Systems and fractal tree growing on the net. Here's a nice example, you can look into the source code and inspect the "grow.js" script there:
https://developer.cdn.mozilla.net/media/uploads/demos/c/o/codepo8/1a61b64545e9e7b1113eef901831ec61/fractal-trees_1313841385_demo_package/index.html

var canvas=document.getElementById("canvas");
var context=canvas.getContext("2d");
var cw=canvas.width;
var ch=canvas.height;
// tree definitions
var x1 = 200;
var y1 = 500;
var x2 = 200;
var y2 = 400;
// growing definitions
var angle = 0.1 * Math.PI;
var depth = 6;
// save segments for later animation
var branches=[];
for(var i=0;i<=depth;i++){branches.push([]);}
var segments=[];
var segmentIndex=0;
// animation variables
var nextTime=0;
var delay=16*5;
///////////// Do stuff!
// define the tree
defineTree( x1, y1, x2, y2, angle, depth );
// create a combined array of segments to be drawn with animation
for(var i=branches.length-1;i>=0;i--){
segments=segments.concat(branches[i]);
}
// load leaf images and then start animating
var leaves=new Image();
leaves.onload=function(){
// animate drawing the tree
requestAnimationFrame(animate);
}
leaves.src='https://dl.dropboxusercontent.com/u/139992952/multple/leaves.png';
///////////// functions
// function to reiteratively define all segments of a tree
function defineTree( x1, y1, x2, y2, angle, depth ){
var segment={
x1:x1,y1:y1,
x2:x2,y2:y2,
linewidth:depth,
};
branches[depth].push(segment);
if( depth > 0 ){
var x = x2 - x1;
var y = y2 - y1;
var scale = 0.5 + Math.random() * 0.5;
x *= scale;
y *= scale;
var xLeft = x * Math.cos( -angle ) - y * Math.sin( -angle );
var yLeft = x * Math.sin( -angle ) + y * Math.cos( -angle );
var xRight = x * Math.cos( +angle ) - y * Math.sin( +angle );
var yRight = x * Math.sin( +angle ) + y * Math.cos( +angle );
xLeft += x2;
yLeft += y2;
xRight += x2;
yRight += y2;
defineTree( x2, y2, xLeft, yLeft, angle, depth - 1 );
defineTree( x2, y2, xRight, yRight, angle, depth - 1 );
}
}
// draw 1 segment of the tree
function drawSegment(segment){
context.strokeStyle = 'rgb( 0, 0, 0 )';
context.lineWidth = segment.linewidth;
context.beginPath();
context.moveTo( segment.x1, segment.y1 );
context.lineTo( segment.x2, segment.y2 );
context.stroke();
//
if(segment.linewidth==0){
var dx=segment.x2-segment.x1;
var dy=segment.y2-segment.y1;
var angle=Math.atan2(dy,dx)+Math.PI/2;
var i=parseInt(Math.random()*2.99);
var j=parseInt(Math.random()*1.99);
context.save();
context.translate(segment.x2,segment.y2);
context.rotate(angle);
context.scale(.25,.25);
context.drawImage(leaves,127*i,142*j,127,142,-127/2,-142/2,127,142);
context.restore();
}
}
// animate drawing each segment of the tree
function animate(currentTime){
// request another loop until all segments have been drawn
if(segmentIndex<segments.length){
requestAnimationFrame(animate);
}
// delay until nextTime
if(currentTime<nextTime){return;}
// set the new nextTime
nextTime=currentTime+delay;
// draw the current segment
drawSegment(segments[segmentIndex]);
// increment the segmentIndex for next loop
segmentIndex++;
}
body{ background-color: ivory; padding:10px; }
#canvas{border:1px solid red;}
<canvas id="canvas" width=400 height=500></canvas>

Related

Draw rotated rounded corner rect in canvas without using canvas.rotate()

I used quadraticCurveTo to draw the rounded corner rect in canvas
function roundRect(x0, y0, x1, y1, r, color) {
var w = x1 - x0;
var h = y1 - y0;
if (r > w/2) r = w/2;
if (r > h/2) r = h/2;
context.beginPath();
context.moveTo(x1 - r, y0);
context.quadraticCurveTo(x1, y0, x1, y0 + r);
context.lineTo(x1, y1-r);
context.quadraticCurveTo(x1, y1, x1 - r, y1);
context.lineTo(x0 + r, y1);
context.quadraticCurveTo(x0, y1, x0, y1 - r);
context.lineTo(x0, y0 + r);
context.quadraticCurveTo(x0, y0, x0 + r, y0);
context.closePath();
context.fillStyle = color;
context.fill();
}
Now I got the (x1,y1), (x2,y2), (x3,y3), (x4,y4) four points of the rect,
and want to draw the rotated rounded corner rect in canvas without using canvas.rotate()
function roundRect(x1, y1, x2, y2, x3, y3, x4, y4, r, color) {
var w = x4 - x1;
var h = y4 - y1;
if (r > w/2) r = w/2;
if (r > h/2) r = h/2;
context.beginPath();
context.moveTo(x2 - r, y2);
context.quadraticCurveTo(x2, y2, x2, y2 + r);
context.lineTo(x4, y4-r);
context.quadraticCurveTo(x4, y4, x4 - r, y4);
context.lineTo(x3 + r, y3);
context.quadraticCurveTo(x3, y3, x3, y3 - r);
context.lineTo(x1, y1 + r);
context.quadraticCurveTo(x1, y1, x1 + r, y1);
context.closePath();
context.fillStyle = color;
context.fill();
}
The corner was wrong position of this code, any solution to draw the rotated rounded rect using x1-x4, y1-y4 without using canvas.rotate()? I sure that my x1-x4, y1-y4 is works.
Transforming coordinates
To rotate the box you need to apply a rotation matrix on each of the points.
The matrix
The matrix defines the x axis, (top) and y axis, (right side of a pixel, including scale, or how big a pixel is), and where the origin is (coordinate {x:0, y:0})
const xAx = Math.cos(angle) * scale; // scale is the size of a pixel
const xAy = Math.sin(angle) * scale;
const yAx = Math.cos(angle + Math.PI / 2) * scale; // Y axis 90 deg CW from x axis
const yAy = Math.sin(angle + Math.PI / 2) * scale;
matrix[0] = xAx; // x part of x axis
matrix[1] = xAy; // y part of x axis
matrix[2] = yAx; // x part of y axis
matrix[3] = yAy; // y part of y axis
matrix[4] = 0; // origin x
matrix[5] = 0; // origin y
The transformation
When you transform a coordinate x, y to tx, ty...
const x = ?
const y = ?
var tx, ty;
...you first move it alone the x axis...
tx = x * matrix[0]
ty = x * matrix[1]
... which scales it along the x axis at the same time. Then move and scale along the y axis.
tx += y * matrix[2]
ty += y * matrix[3]
Then move to the origin
tx += matrix[4]
ty += matrix[5]
This transformation moves a coordinate from local space to world space (or in 2D world space is often called the view)
Local space
When you rotate a shape you need to pick a point around which you want to rotate it, for example the center, or at one corner.
To do that you define the shape relative to the rotation point (in the shape's local space). If for example you want to rotate the box around the center you define top left and bottom right points to be equal distance from zero eg [-100, -50], [100, 50]
To rotate at a corner you position the box relative to that corner. eg top left the box is [0, 0], [200, 100]
You position the shape in world space, by setting the origin of the matrix (where on the canvas the rotation center will be)
Example
The above matrix calculations can be simplified if we know that the scale is uniform (x and y axis scale the same amount), and that the x, and y axis are always 90 degree from each other.
The example uses an array to hold the matrix, the functions
transformPoint applies the matrix to a point
setOrigin sets the transform origin (where on canvas the rotation point is)
setRotation sets the directions of the x and y axis
setScale not used in example. Sets the scale of the transform. NOTE must call setScale after setRotation in this example
setTransform not used in example. Does the above 3 in one call
roundRect draws the shape, it is given the top left and bottom right coordinates of the box in local space. It constrains the corner radius to the min size that will fit and still maintain (near as beziers are never round) round corners
There are two boxes to demonstrate changing the center of rotation. One rotates about its center the other around the top left corner.
A third box (red) demonstrates that there is no reason to manually transform the box, that using the 2D API transformations is identical but far simpler and a lot quicker
requestAnimationFrame(update);
const ctx = canvas.getContext("2d");
const matrix = [1,0,0,1,0,0];
function transfromPoint(x, y) {
const m = matrix;
return [x * m[0] + y * m[2] + m[4], x * m[1] + y * m[3] + m[5]];
}
function setOrigin(x, y) {
matrix[4] = x;
matrix[5] = y;
}
function setRotation(angle) {
const ax = Math.cos(angle);
const ay = Math.sin(angle);
matrix[0] = ax;
matrix[1] = ay;
matrix[2] = -ay;
matrix[3] = ax;
}
function setScale(scale) {
matrix[0] *= scale;
matrix[1] *= scale;
matrix[2] *= scale;
matrix[3] *= scale;
}
function setTransform(ox, oy, rot, scale) {
const ax = Math.cos(rot) * scale;
const ay = Math.sin(rot) * scale;
matrix[0] = ax;
matrix[1] = ay;
matrix[2] = -ay;
matrix[3] = ax;
matrix[4] = ox;
matrix[5] = oy;
}
function roundRect(x1, y1, x2, y2, r, color = "#000", lineWidth = 2) {
ctx.strokeStyle = color;
ctx.lineWidth = lineWidth;
const min = Math.min(Math.abs(x1 - x2), Math.abs(y1 - y2));
r = r > min ? min / 2 : r;
ctx.beginPath();
ctx.moveTo(...transfromPoint(x2 - r, y1));
ctx.quadraticCurveTo(...transfromPoint(x2, y1), ...transfromPoint(x2, y1 + r));
ctx.lineTo(...transfromPoint(x2, y2 - r));
ctx.quadraticCurveTo(...transfromPoint(x2, y2), ...transfromPoint(x2 - r, y2));
ctx.lineTo(...transfromPoint(x1 + r, y2));
ctx.quadraticCurveTo(...transfromPoint(x1, y2), ...transfromPoint(x1 , y2 - r));
ctx.lineTo(...transfromPoint(x1, y1 + r));
ctx.quadraticCurveTo(...transfromPoint(x1, y1), ...transfromPoint(x1 + r, y1));
ctx.closePath();
ctx.stroke();
}
function roundRectAPITransform(x1, y1, x2, y2, r, color = "#F00", lineWidth = 2) {
ctx.strokeStyle = color;
ctx.lineWidth = lineWidth;
const min = Math.min(Math.abs(x1 - x2), Math.abs(y1 - y2));
r = r > min ? min / 2 : r;
ctx.beginPath();
ctx.moveTo(x2 - r, y1);
ctx.quadraticCurveTo(x2, y1, x2, y1 + r);
ctx.lineTo(x2, y2 - r);
ctx.quadraticCurveTo(x2, y2, x2 - r, y2);
ctx.lineTo(x1 + r, y2);
ctx.quadraticCurveTo(x1, y2, x1 , y2 - r);
ctx.lineTo(x1, y1 + r);
ctx.quadraticCurveTo(x1, y1, x1 + r, y1);
ctx.closePath();
ctx.stroke();
}
function update(time) {
ctx.clearRect(0,0,ctx.canvas.width,ctx.canvas.height);
// around center
setOrigin(100, 100);
setRotation(time * Math.PI / 2000); // one rotation every 4 seconds
roundRect(-60, -35, 60, 35, 15);
// around top right corner
setOrigin(300, 100);
setRotation(-time * Math.PI / 2000); // one rotation every 4 seconds
roundRect(-60, 0, 0, 35, 5);
// red box using API
ctx.setTransform(1,0,0,1,200,100);
ctx.rotate(-time * Math.PI / 4000) // once every 8 seconds;
roundRectAPITransform(-30, -15, 30, 15, 12);
ctx.setTransform(1,0,0,1,0,0); // restore default
requestAnimationFrame(update);
}
<canvas id="canvas" width="400" height="200"></canvas>
Update
Re comments
You don't want to rotate a rectangle, you already have it rotated.
The following function adds rounded corners to a rotate rectangle
// MUST BE RECTANGULAR!!
function roundRectangle(x1, y1, x2, y2, x3, y3, x4, y4, r, color) {
// get top and left edge vectors and lengths
var tx = x2 - x1;
var ty = y2 - y1;
const td = (tx * tx + ty * ty) ** 0.5;
var lx = x3 - x1;
var ly = y3 - y1;
const ld = (lx * lx + ly * ly) ** 0.5;
// Constrain corner radius
const min = Math.min(td, ld) / 2;
r = r > min ? min : r;
// Normalize vectors to length of corner radius
tx *= r / td;
ty *= r / td;
lx *= r / ld;
ly *= r / ld;
// draw rotated retangle
ctx.fillStyle = color;
ctx.beginPath();
ctx.lineTo(x2 - tx, y2 - ty);
ctx.quadraticCurveTo(x2, y2, x2 + lx, y2 + ly);
ctx.lineTo(x4 - lx, y4 - ly);
ctx.quadraticCurveTo(x4, y4, x4 - tx, y4 - ty);
ctx.lineTo(x3 + tx, y3 + ty);
ctx.quadraticCurveTo(x3, y3, x3 - lx, y3 - ly);
ctx.lineTo(x1 + lx, y1 + ly);
ctx.quadraticCurveTo(x1, y1, x1 + tx, y1 + ty);
ctx.fill();
}

How to draw triangle pointers inside of circle

I realize this is a simple Trigonometry question, but my high school is failing me right now.
Given an angle, that I have converted into radians to get the first point. How do I figure the next two points of the triangle to draw on the canvas, so as to make a small triangle always point outwards to the circle. So lets say Ive drawn a circle of a given radius already. Now I want a function to plot a triangle that sits on the edge of the circle inside of it, that points outwards no matter the angle. (follows the edge, so to speak)
function drawPointerTriangle(ctx, angle){
var radians = angle * (Math.PI/180)
var startX = this.radius + this.radius/1.34 * Math.cos(radians)
var startY = this.radius - this.radius/1.34 * Math.sin(radians)
// This gives me my starting point on the outer edge of the circle, plotted at the angle I need
ctx.moveTo(startX, startY);
// HOW DO I THEN CALCULATE x1,y1 and x2, y2. So that no matter what angle I enter into this function, the arrow/triangle always points outwards to the circle.
ctx.lineTo(x1, y1);
ctx.lineTo(x2, y2);
}
Example
You don't say what type of triangle you want to draw so I suppose that it is an equilateral triangle.
Take a look at this image (credit here)
I will call 3 points p1, p2, p3 from top right to bottom right, counterclockwise.
You can easily calculate the coordinate of three points of the triangle in the coordinate system with the origin is coincident with the triangle's centroid.
Given a point belongs to the edge of the circle and the point p1 that we just calculated, we can calculate parameters of the translation from our main coordinate system to the triangle's coordinate system. Then, we just have to translate the coordinate of two other points back to our main coordinate system. That is (x1,y1) and (x2,y2).
You can take a look at the demo below that is based on your code.
const w = 300;
const h = 300;
function calculateTrianglePoints(angle, width) {
let r = width / Math.sqrt(3);
let firstPoint = [
r * Math.cos(angle),
r * Math.sin(angle),
]
let secondPoint = [
r * Math.cos(angle + 2 * Math.PI / 3),
r * Math.sin(angle + 2 * Math.PI / 3),
]
let thirdPoint = [
r * Math.cos(angle + 4 * Math.PI / 3),
r * Math.sin(angle + 4 * Math.PI / 3),
]
return [firstPoint, secondPoint, thirdPoint]
}
const radius = 100
const triangleWidth = 20;
function drawPointerTriangle(ctx, angle) {
var radians = angle * (Math.PI / 180)
var startX = radius * Math.cos(radians)
var startY = radius * Math.sin(radians)
var [pt0, pt1, pt2] = calculateTrianglePoints(radians, triangleWidth);
var delta = [
startX - pt0[0],
startY - pt0[1],
]
pt1[0] = pt1[0] + delta[0]
pt1[1] = pt1[1] + delta[1]
pt2[0] = pt2[0] + delta[0]
pt2[1] = pt2[1] + delta[1]
ctx.beginPath();
// This gives me my starting point on the outer edge of the circle, plotted at the angle I need
ctx.moveTo(startX, startY);
[x1, y1] = pt1;
[x2, y2] = pt2;
// HOW DO I THEN CALCULATE x1,y1 and x2, y2. So that no matter what angle I enter into this function, the arrow/triangle always points outwards to the circle.
ctx.lineTo(x1, y1);
ctx.lineTo(x2, y2);
ctx.closePath();
ctx.fillStyle = '#FF0000';
ctx.fill();
}
function drawCircle(ctx, radius) {
ctx.beginPath();
ctx.arc(0, 0, radius, 0, 2 * Math.PI);
ctx.closePath();
ctx.fillStyle = '#000';
ctx.fill();
}
function clear(ctx) {
ctx.fillStyle = '#fff';
ctx.fillRect(-w / 2, -h / 2, w, h);
}
function normalizeAngle(pointCoordinate, angle) {
const [x, y] = pointCoordinate;
if (x > 0 && y > 0) return angle;
else if (x > 0 && y < 0) return 360 + angle;
else if (x < 0 && y < 0) return 180 - angle;
else if (x < 0 && y > 0) return 180 - angle;
}
function getAngleFromPoint(point) {
const [x, y] = point;
if (x == 0 && y == 0) return 0;
else if (x == 0) return 90 * (y > 0 ? 1 : -1);
else if (y == 0) return 180 * (x >= 0 ? 0: 1);
const radians = Math.asin(y / Math.sqrt(
x ** 2 + y ** 2
))
return normalizeAngle(point, radians / (Math.PI / 180))
}
document.addEventListener('DOMContentLoaded', function() {
const canvas = document.querySelector('canvas');
const angleText = document.querySelector('.angle');
const ctx = canvas.getContext('2d');
ctx.translate(w / 2, h / 2);
drawCircle(ctx, radius);
drawPointerTriangle(ctx, 0);
canvas.addEventListener('mousemove', _.throttle(function(ev) {
let mouseCoordinate = [
ev.clientX - w / 2,
ev.clientY - h / 2
]
let degAngle = getAngleFromPoint(mouseCoordinate)
clear(ctx);
drawCircle(ctx, radius);
drawPointerTriangle(ctx, degAngle)
angleText.innerText = Math.floor((360 - degAngle)*100)/100;
}, 15))
})
<script src="https://cdnjs.cloudflare.com/ajax/libs/underscore.js/1.9.1/underscore-min.js"></script>
<canvas width=300 height=300></canvas>
<div class="angle">0</div>
reduce the radius, change the angle and call again cos/sin:
function drawPointerTriangle(ctx, angle)
{
var radians = angle * (Math.PI/180);
var radius = this.radius/1.34;
var startX = this.center.x + radius * Math.cos(radians);
var startY = this.center.y + radius * Math.sin(radians);
ctx.moveTo(startX, startY);
radius *= 0.9;
radians += 0.1;
var x1 = this.center.x + radius * Math.cos(radians);
var y1 = this.center.y + radius * Math.sin(radians);
radians -= 0.2;
var x1 = this.center.x + radius * Math.cos(radians);
var y1 = this.center.y + radius * Math.sin(radians);
ctx.lineTo(x1, y1);
ctx.lineTo(x2, y2);
ctx.lineTo(startX, startY);
}
the resulting triangle's size is proportional to the size of the circle.
in case you need an equilateral, fixed size triangle, use this:
//get h by pythagoras
h = sqrt( a^2 - (a/2)^2 );)
//get phi using arcustangens:
phi = atan( a/2, radius-h );
//reduced radius h by pythagoras:
radius = sqrt( (radius-h)^2 + (a/2)^2 );
radians += phi;
...
radians -= 2*phi;
...

Drawing animated curves in canvas

A few days ago I came to stackoverflow asking about how to draw an arrow slowly into a canvas. No one was able to give me the correct answer... So I hope this helps someone.
Basically, i wanted to animate the progress of an invasion from one country to another country in a map. To do that I should use canvas and draw an arrow that moved from country A to country B, but not a fixed arrow... An arrow that grows progressively.
The code below draws an arrow, but not progressively. So, I needed to draw this curve like a CSS animation with a 5s transition.
function drawCurve (ctx, x0, y0, x1, y1, x2, y2){
ctx.beginPath();
ctx.moveTo( x0, y0 );
ctx.quadraticCurveTo( x1, y1, x2, y2 );
ctx.stroke();
ctx.closePath();
}
var docCanvas = document.getElementById('canvas');
var ctx = docCanvas.getContext('2d');
drawCurve(ctx, 0, 100, 150, -50, 300, 100);
<canvas id="canvas" width="480" height="320"></canvas>
After some digging I came to this solution that gives me all I wanted.
Basically drawBezierSplit() allow you to draw a section of a quadratic bezier curve.
All the credit to Patrick Galbraith.
/**
* Animates bezier-curve
*
* #param ctx The canvas context to draw to
* #param x0 The x-coord of the start point
* #param y0 The y-coord of the start point
* #param x1 The x-coord of the control point
* #param y1 The y-coord of the control point
* #param x2 The x-coord of the end point
* #param y2 The y-coord of the end point
* #param duration The duration in milliseconds
*/
function animatePathDrawing(ctx, x0, y0, x1, y1, x2, y2, duration) {
var start = null;
var step = function animatePathDrawingStep(timestamp) {
if (start === null)
start = timestamp;
var delta = timestamp - start,
progress = Math.min(delta / duration, 1);
// Clear canvas
ctx.clearRect(0, 0, ctx.canvas.width, ctx.canvas.height);
// Draw curve
drawBezierSplit(ctx, x0, y0, x1, y1, x2, y2, 0, progress);
if (progress < 1) {
window.requestAnimationFrame(step);
}
};
window.requestAnimationFrame(step);
}
/**
* Draws a splitted bezier-curve
*
* #param ctx The canvas context to draw to
* #param x0 The x-coord of the start point
* #param y0 The y-coord of the start point
* #param x1 The x-coord of the control point
* #param y1 The y-coord of the control point
* #param x2 The x-coord of the end point
* #param y2 The y-coord of the end point
* #param t0 The start ratio of the splitted bezier from 0.0 to 1.0
* #param t1 The start ratio of the splitted bezier from 0.0 to 1.0
*/
function drawBezierSplit(ctx, x0, y0, x1, y1, x2, y2, t0, t1) {
ctx.beginPath();
if( 0.0 == t0 && t1 == 1.0 ) {
ctx.moveTo( x0, y0 );
ctx.quadraticCurveTo( x1, y1, x2, y2 );
} else if( t0 != t1 ) {
var t00 = t0 * t0,
t01 = 1.0 - t0,
t02 = t01 * t01,
t03 = 2.0 * t0 * t01;
var nx0 = t02 * x0 + t03 * x1 + t00 * x2,
ny0 = t02 * y0 + t03 * y1 + t00 * y2;
t00 = t1 * t1;
t01 = 1.0 - t1;
t02 = t01 * t01;
t03 = 2.0 * t1 * t01;
var nx2 = t02 * x0 + t03 * x1 + t00 * x2,
ny2 = t02 * y0 + t03 * y1 + t00 * y2;
var nx1 = lerp ( lerp ( x0 , x1 , t0 ) , lerp ( x1 , x2 , t0 ) , t1 ),
ny1 = lerp ( lerp ( y0 , y1 , t0 ) , lerp ( y1 , y2 , t0 ) , t1 );
ctx.moveTo( nx0, ny0 );
ctx.quadraticCurveTo( nx1, ny1, nx2, ny2 );
}
ctx.stroke();
ctx.closePath();
}
/**
* Linearly interpolates between two numbers
*/
function lerp(v0, v1, t) {
return ( 1.0 - t ) * v0 + t * v1;
}
var docCanvas = document.getElementById('canvas');
var ctx = docCanvas.getContext('2d');
animatePathDrawing(ctx, 0, 100, 150, -50, 300, 100, 5000);
<canvas id="canvas" width="480" height="320"></canvas>
EDIT:
And if you need a polyfill, you can use this code:
(function() {
var lastTime = 0;
var vendors = ['webkit', 'moz'];
for(var x = 0; x < vendors.length && !window.requestAnimationFrame; ++x) {
window.requestAnimationFrame = window[vendors[x]+'RequestAnimationFrame'];
window.cancelAnimationFrame =
window[vendors[x]+'CancelAnimationFrame'] || window[vendors[x]+'CancelRequestAnimationFrame'];
}
if (!window.requestAnimationFrame)
window.requestAnimationFrame = function(callback, element) {
var currTime = new Date().getTime();
var timeToCall = Math.max(0, 16 - (currTime - lastTime));
var id = window.setTimeout(function() { callback(currTime + timeToCall); },
timeToCall);
lastTime = currTime + timeToCall;
return id;
};
if (!window.cancelAnimationFrame)
window.cancelAnimationFrame = function(id) {
clearTimeout(id);
};
}());
Link: http://www.pjgalbraith.com/drawing-animated-curves-javascript/

Line of sight from point

Need to create simple line of sight from point. Length of this line would be adapt to the size of canvas. If line directed to any object (circle, rectangle etc) it must be interrupted after this. I don't know exactly how to describe this, but behavior should be something like this. It's like laser aim in video-games.
Demo jsfiddle. Target line has red color. I think that line must have dynamic length depending on where I will direct it.
var canvas = document.querySelector("canvas");
canvas.width = 500;
canvas.height = 300;
var ctx = canvas.getContext("2d"),
line = {
x1: 190, y1: 170,
x2: 0, y2: 0,
x3: 0, y3: 0
};
var length = 100;
var circle = {
x: 400,
y: 70
};
window.onmousemove = function(e) {
//get correct mouse pos
var rect = ctx.canvas.getBoundingClientRect(),
x = e.clientX - rect.left,
y = e.clientY - rect.top;
// calc line angle
var dx = x - line.x1,
dy = y - line.y1,
angle = Math.atan2(dy, dx);
//Then render the line using 100 pixel radius:
line.x2 = line.x1 - length * Math.cos(angle);
line.y2 = line.y1 - length * Math.sin(angle);
line.x3 = line.x1 + canvas.width * Math.cos(angle);
line.y3 = line.y1 + canvas.width * Math.sin(angle);
// render
ctx.clearRect(0, 0, canvas.width, canvas.height);
ctx.beginPath();
ctx.moveTo(line.x1, line.y1);
ctx.lineTo(line.x2, line.y2);
ctx.strokeStyle = '#333';
ctx.stroke();
ctx.beginPath();
ctx.moveTo(line.x1, line.y1);
ctx.lineTo(line.x3, line.y3);
ctx.strokeStyle = 'red';
ctx.stroke();
ctx.beginPath();
ctx.arc(circle.x, circle.y, 20, 0, Math.PI * 2, true);
ctx.fillStyle = '#333';
ctx.fill();
}
<canvas></canvas>
Ray casting
The given answer is a good answer but this problem is better suited to a ray casting like solution where we are only interested in the distance to an intercept rather than the actual point of interception. We only need one point per cast ray so not calculating points will reduce the math and hence the CPU load giving more rays and objects per second.
A ray is a point that defines the start and a normalised vector that represents the direction of the ray. Because the ray uses a normalised vector that is a unit length many calculations are simplified because 1 * anything changes nothing.
Also the problem is about looking for the closest intercept so the intercept functions return a distance from the ray's origin. If no intercept is found then Infinity is returned to allow a valid distance comparison to be made. Every number is less than Infinity.
A nice feature of JavaScript is that it allows divide by zero and returns Infinity if that happens, this further reduces the complexity of the solution. Also if the intercept finds a negative intercept that means the object is behind that raycast origin and thus will return infinity as well.
So first let's define our objects by creating functions to make them. They are all ad hoc objects.
The Ray
// Ad Hoc method for ray to set the direction vector
var updateRayDir = function(dir){
this.nx = Math.cos(dir);
this.ny = Math.sin(dir);
return this;
}
// Creates a ray objects from
// x,y start location
// dir the direction in radians
// len the rays length
var createRay = function(x,y,dir,len){
return ({
x : x,
y : y,
len : len,
setDir : updateRayDir, // add function to set direction
}).setDir(dir);
}
A circle
// returns a circle object
// x,y is the center
// radius is the you know what..
// Note r2 is radius squared if you change the radius remember to set r2 as well
var createCircle = function(x , y, radius){
return {
x : x,
y : y,
rayDist : rayDist2Circle, // add ray cast method
radius : radius,
r2 : radius * radius, // ray caster needs square of radius may as well do it here
};
}
A wall
Note I changed the wall code in the demo
// Ad Hoc function to change the wall position
// x1,y1 are the start coords
// x2,y2 are the end coords
changeWallPosition = function(x1, y1, x2, y2){
this.x = x1;
this.y = y1;
this.vx = x2 - x1;
this.vy = y2 - y1;
this.len = Math.hypot(this.vx,this.vy);
this.nx = this.vx / this.len;
this.ny = this.vy / this.len;
return this;
}
// returns a wall object
// x1,y1 are the star coords
// x2,y2 are the end coords
var createWall = function(x1, y1, x2, y2){
return({
x : x1, y : y1,
vx : x2 - x1,
vy : y2 - y1,
rayDist : rayDist2Wall, // add ray cast method
setPos : changeWallPosition,
}).setPos(x1, y1, x2, y2);
}
So those are the objects, they can be static or moving through the circle should have a setRadius function because I have added a property that holds the square of the radius but I will leave that up to you if you use that code.
Now the intercept functions.
Ray Intercepts
The stuff that matters. In the demo these functions are bound to the objects so that the ray casting code need not have to know what type of object it is checking.
Distance to circle.
// Self evident
// returns a distance or infinity if no valid solution
var rayDist2Circle = function(ray){
var vcx, vcy, v;
vcx = ray.x - this.x; // vector from ray to circle
vcy = ray.y - this.y;
v = -2 * (vcx * ray.nx + vcy * ray.ny);
v -= Math.sqrt(v * v - 4 * (vcx * vcx + vcy * vcy - this.r2)); // this.r2 is the radius squared
// If there is no solution then Math.sqrt returns NaN we should return Infinity
// Not interested in intercepts in the negative direction so return infinity
return isNaN(v) || v < 0 ? Infinity : v / 2;
}
Distance to wall
// returns the distance to the wall
// if no valid solution then return Infinity
var rayDist2Wall = function(ray){
var x,y,u;
rWCross = ray.nx * this.ny - ray.ny * this.nx;
if(!rWCross) { return Infinity; } // Not really needed.
x = ray.x - this.x; // vector from ray to wall start
y = ray.y - this.y;
u = (ray.nx * y - ray.ny * x) / rWCross; // unit distance along normalised wall
// does the ray hit the wall segment
if(u < 0 || u > this.len){ return Infinity;} /// no
// as we use the wall normal and ray normal the unit distance is the same as the
u = (this.nx * y - this.ny * x) / rWCross;
return u < 0 ? Infinity : u; // if behind ray return Infinity else the dist
}
That covers the objects. If you need to have a circle that is inside out (you want the inside surface then change the second last line of the circle ray function to v += rather than v -=
The ray casting
Now it is just a matter of iterating all the objects against the ray and keeping the distant to the closest object. Set the ray to that distance and you are done.
// Does a ray cast.
// ray the ray to cast
// objects an array of objects
var castRay = function(ray,objects)
var i,minDist;
minDist = ray.len; // set the min dist to the rays length
i = objects.length; // number of objects to check
while(i > 0){
i -= 1;
minDist = Math.min(objects[i].rayDist(ray),minDist);
}
ray.len = minDist;
}
A demo
And a demo of all the above in action. THere are some minor changes (drawing). The important stuff is the two intercept functions. The demo creates a random scene each time it is resized and cast 16 rays from the mouse position. I can see in your code you know how to get the direction of a line so I made the demo show how to cast multiple rays that you most likely will end up doing
const COLOUR = "BLACK";
const RAY_COLOUR = "RED";
const LINE_WIDTH = 4;
const RAY_LINE_WIDTH = 2;
const OBJ_COUNT = 20; // number of object in the scene;
const NUMBER_RAYS = 16; // number of rays
const RAY_DIR_SPACING = Math.PI / (NUMBER_RAYS / 2);
const RAY_ROTATE_SPEED = Math.PI * 2 / 31000;
if(typeof Math.hypot === "undefined"){ // poly fill for Math.hypot
Math.hypot = function(x, y){
return Math.sqrt(x * x + y * y);
}
}
var ctx, canvas, objects, ray, w, h, mouse, rand, ray, rayMaxLen, screenDiagonal;
// create a canvas and add to the dom
var canvas = document.createElement("canvas");
canvas.width = w = window.innerWidth;
canvas.height = h = window.innerHeight;
canvas.style.position = "absolute";
canvas.style.left = "0px";
canvas.style.top = "0px";
document.body.appendChild(canvas);
// objects to ray cast
objects = [];
// mouse object
mouse = {x :0, y: 0};
//========================================================================
// random helper
rand = function(min, max){
return Math.random() * (max - min) + min;
}
//========================================================================
// Ad Hoc draw line method
// col is the stroke style
// width is the storke width
var drawLine = function(col,width){
ctx.strokeStyle = col;
ctx.lineWidth = width;
ctx.beginPath();
ctx.moveTo(this.x,this.y);
ctx.lineTo(this.x + this.nx * this.len, this.y + this.ny * this.len);
ctx.stroke();
}
//========================================================================
// Ad Hoc draw circle method
// col is the stroke style
// width is the storke width
var drawCircle = function(col,width){
ctx.strokeStyle = col;
ctx.lineWidth = width;
ctx.beginPath();
ctx.arc(this.x , this.y, this.radius, 0 , Math.PI * 2);
ctx.stroke();
}
//========================================================================
// Ad Hoc method for ray to set the direction vector
var updateRayDir = function(dir){
this.nx = Math.cos(dir);
this.ny = Math.sin(dir);
return this;
}
//========================================================================
// Creates a ray objects from
// x,y start location
// dir the direction in radians
// len the rays length
var createRay = function(x,y,dir,len){
return ({
x : x,
y : y,
len : len,
draw : drawLine,
setDir : updateRayDir, // add function to set direction
}).setDir(dir);
}
//========================================================================
// returns a circle object
// x,y is the center
// radius is the you know what..
// Note r2 is radius squared if you change the radius remember to set r2 as well
var createCircle = function(x , y, radius){
return {
x : x,
y : y,
draw : drawCircle, // draw function
rayDist : rayDist2Circle, // add ray cast method
radius : radius,
r2 : radius * radius, // ray caster needs square of radius may as well do it here
};
}
//========================================================================
// Ad Hoc function to change the wall position
// x1,y1 are the start coords
// x2,y2 are the end coords
changeWallPosition = function(x1, y1, len, dir){
this.x = x1;
this.y = y1;
this.len = len;
this.nx = Math.cos(dir);
this.ny = Math.sin(dir);
return this;
}
//========================================================================
// returns a wall object
// x1,y1 are the star coords
// len is the length
// dir is the direction
var createWall = function(x1, y1, len, dir){
return({
x : x1, y : y1,
rayDist : rayDist2Wall, // add ray cast method
draw : drawLine,
setPos : changeWallPosition,
}).setPos(x1, y1, len, dir);
}
//========================================================================
// Self evident
// returns a distance or infinity if no valid solution
var rayDist2Circle = function(ray){
var vcx, vcy, v;
vcx = ray.x - this.x; // vector from ray to circle
vcy = ray.y - this.y;
v = -2 * (vcx * ray.nx + vcy * ray.ny);
v -= Math.sqrt(v * v - 4 * (vcx * vcx + vcy * vcy - this.r2)); // this.r2 is the radius squared
// If there is no solution then Math.sqrt returns NaN we should return Infinity
// Not interested in intercepts in the negative direction so return infinity
return isNaN(v) || v < 0 ? Infinity : v / 2;
}
//========================================================================
// returns the distance to the wall
// if no valid solution then return Infinity
var rayDist2Wall = function(ray){
var x,y,u;
rWCross = ray.nx * this.ny - ray.ny * this.nx;
if(!rWCross) { return Infinity; } // Not really needed.
x = ray.x - this.x; // vector from ray to wall start
y = ray.y - this.y;
u = (ray.nx * y - ray.ny * x) / rWCross; // unit distance along normal of wall
// does the ray hit the wall segment
if(u < 0 || u > this.len){ return Infinity;} /// no
// as we use the wall normal and ray normal the unit distance is the same as the
u = (this.nx * y - this.ny * x) / rWCross;
return u < 0 ? Infinity : u; // if behind ray return Infinity else the dist
}
//========================================================================
// does a ray cast
// ray the ray to cast
// objects an array of objects
var castRay = function(ray,objects){
var i,minDist;
minDist = ray.len; // set the min dist to the rays length
i = objects.length; // number of objects to check
while(i > 0){
i -= 1;
minDist = Math.min(objects[i].rayDist(ray), minDist);
}
ray.len = minDist;
}
//========================================================================
// Draws all objects
// objects an array of objects
var drawObjects = function(objects){
var i = objects.length; // number of objects to check
while(i > 0){
objects[--i].draw(COLOUR, LINE_WIDTH);
}
}
//========================================================================
// called on start and resize
// creats a new scene each time
// fits the canvas to the avalible realestate
function reMakeAll(){
w = canvas.width = window.innerWidth;
h = canvas.height = window.innerHeight;
ctx = canvas.getContext("2d");
screenDiagonal = Math.hypot(window.innerWidth,window.innerHeight);
if(ray === undefined){
ray = createRay(0,0,0,screenDiagonal);
}
objects.length = 0;
var i = OBJ_COUNT;
while( i > 0 ){
if(Math.random() < 0.5){ // half circles half walls
objects.push(createWall(rand(0, w), rand(0, h), rand(screenDiagonal * 0.1, screenDiagonal * 0.2), rand(0, Math.PI * 2)));
}else{
objects.push(createCircle(rand(0, w), rand(0, h), rand(screenDiagonal * 0.02, screenDiagonal * 0.05)));
}
i -= 1;
}
}
//========================================================================
function mouseMoveEvent(event){
mouse.x = event.clientX;
mouse.y = event.clientY;
}
//========================================================================
// updates all that is needed when needed
function updateAll(time){
var i;
ctx.clearRect(0,0,w,h);
ray.x = mouse.x;
ray.y = mouse.y;
drawObjects(objects);
i = 0;
while(i < NUMBER_RAYS){
ray.setDir(i * RAY_DIR_SPACING + time * RAY_ROTATE_SPEED);
ray.len = screenDiagonal;
castRay(ray,objects);
ray.draw(RAY_COLOUR, RAY_LINE_WIDTH);
i ++;
}
requestAnimationFrame(updateAll);
}
// add listeners
window.addEventListener("resize",reMakeAll);
canvas.addEventListener("mousemove",mouseMoveEvent);
// set it all up
reMakeAll();
// start the ball rolling
requestAnimationFrame(updateAll);
An alternative use of above draws a polygon using the end points of the cast rays can be seen at codepen
For this you would need a line to circle intersection algorithm for the balls as well as line to line intersection for the walls.
For the ball you can use this function - I made this to return arrays being empty if no intersection, one point if tangent or two points if secant.
Simply feed it start of line, line of sight end-point as well as the ball's center position and radius. In your case you will probably only need the first point:
function lineIntersectsCircle(x1, y1, x2, y2, cx, cy, r) {
x1 -= cx;
y1 -= cy;
x2 -= cx;
y2 -= cy;
// solve quadrant
var a = (x2 - x1) * (x2 - x1) + (y2 - y1) * (y2 - y1),
b = 2 * ((x2 - x1) * x1 + (y2 - y1) * y1),
c = x1 * x1 + y1 * y1 - r * r,
d = b * b - 4 * a * c,
dq, p1, p2, t1, t2;
if (d <= 0 || !a) return [];
dq = Math.sqrt(d);
t1 = (-b - dq) / (2 * a);
t2 = (-b + dq) / (2 * a);
// calculate actual intersection points
if (t1 >= 0 && t1 <= 1)
p1 = {
x: x1 + t1 * (x2 - x1) + cx,
y: y1 + t1 * (y2 - y1) + cy
};
if (t2 >= 0 && t2 <= 1)
p2 = {
x: x1 + t2 * (x2 - x1) + cx,
y: y1 + t2 * (y2 - y1) + cy
};
return p1 && p2 ? [p1, p2] : p1 ? [p1] : [p2]
};
Then for the walls you would need a line to line intersection - define one line for each side of the rectangle. If there is line overlap you may get hit for two intersection, just ignore the second.
This will return a single point for the intersection or null if no intersection:
function getLineIntersection(p0x, p0y, p1x, p1y, p2x, p2y, p3x, p3y) {
var d1x = p1x - p0x,
d1y = p1y - p0y,
d2x = p3x - p2x,
d2y = p3y - p2y,
d = d1x * d2y - d2x * d1y,
px, py, s, t;
if (Math.abs(d) < 1e-14) return null;
px = p0x - p2x;
py = p0y - p2y;
s = (d1x * py - d1y * px) / d;
if (s >= 0 && s <= 1) {
t = (d2x * py - d2y * px) / d;
if (t >= 0 && t <= 1) {
return {
x: p0x + (t * d1x),
y: p0y + (t * d1y)
}
}
}
return null
}
Then just iterate with the line through the ball array, if no hit, iterate through the wall array.
Modified fiddle
To utilize these you will have to run the line through these each time it is moved (or per frame update).
Tip: You can make the function recursive so that you can find the intersection point, calculate reflected vector based on the hit angle, then find next intersection for n number of times (or total length the shot can move) using the last intersecting point and new angle as start of next line. This way you can build the path the shot will follow.
var canvas = document.querySelector("canvas");
canvas.width = 500;
canvas.height = 300;
var ctx = canvas.getContext("2d"),
line = {
x1: 190, y1: 170,
x2: 0, y2: 0,
x3: 0, y3: 0
};
var length = 100;
var circle = {
x: 400,
y: 70
};
var wall = {
x1: 440, y1: 0,
x2: 440, y2: 100
};
window.onmousemove = function(e) {
//get correct mouse pos
var rect = ctx.canvas.getBoundingClientRect(),
x = e.clientX - rect.left,
y = e.clientY - rect.top;
// calc line angle
var dx = x - line.x1,
dy = y - line.y1,
angle = Math.atan2(dy, dx);
//Then render the line using length as pixel radius:
line.x2 = line.x1 - length * Math.cos(angle);
line.y2 = line.y1 - length * Math.sin(angle);
line.x3 = line.x1 + canvas.width * Math.cos(angle);
line.y3 = line.y1 + canvas.width * Math.sin(angle);
// does it intersect?
var pts = lineIntersectsCircle(line.x1, line.y1, line.x3, line.y3, circle.x, circle.y, 20);
if (pts.length) {
line.x3 = pts[0].x;
line.y3 = pts[0].y
}
else {
pts = getLineIntersection(line.x1, line.y1, line.x3, line.y3, wall.x1, wall.y1, wall.x2, wall.y2);
if (pts) {
line.x3 = pts.x;
line.y3 = pts.y
}
}
// render
ctx.clearRect(0, 0, canvas.width, canvas.height);
ctx.beginPath();
ctx.moveTo(line.x1, line.y1);
ctx.lineTo(line.x2, line.y2);
ctx.strokeStyle = '#333';
ctx.stroke();
ctx.beginPath();
ctx.moveTo(line.x1, line.y1);
ctx.lineTo(line.x3, line.y3);
ctx.strokeStyle = 'red';
ctx.stroke();
ctx.beginPath();
ctx.arc(circle.x, circle.y, 20, 0, Math.PI * 2, true);
ctx.fillStyle = '#333';
ctx.fill();
// render example wall:
ctx.fillRect(wall.x1, wall.y1, 4, wall.y2-wall.y1);
}
function lineIntersectsCircle(x1, y1, x2, y2, cx, cy, r) {
x1 -= cx;
y1 -= cy;
x2 -= cx;
y2 -= cy;
// solve quadrant
var a = (x2 - x1) * (x2 - x1) + (y2 - y1) * (y2 - y1),
b = 2 * ((x2 - x1) * x1 + (y2 - y1) * y1),
c = x1 * x1 + y1 * y1 - r * r,
d = b * b - 4 * a * c,
dq, p1, p2, t1, t2;
if (d <= 0 || !a) return [];
dq = Math.sqrt(d);
t1 = (-b - dq) / (2 * a);
t2 = (-b + dq) / (2 * a);
// calculate actual intersection points
if (t1 >= 0 && t1 <= 1)
p1 = {
x: x1 + t1 * (x2 - x1) + cx,
y: y1 + t1 * (y2 - y1) + cy
};
if (t2 >= 0 && t2 <= 1)
p2 = {
x: x1 + t2 * (x2 - x1) + cx,
y: y1 + t2 * (y2 - y1) + cy
};
return p1 && p2 ? [p1, p2] : p1 ? [p1] : [p2]
};
function getLineIntersection(p0x, p0y, p1x, p1y, p2x, p2y, p3x, p3y) {
var d1x = p1x - p0x,
d1y = p1y - p0y,
d2x = p3x - p2x,
d2y = p3y - p2y,
d = d1x * d2y - d2x * d1y,
px, py, s, t;
if (Math.abs(d) < 1e-14) return null;
px = p0x - p2x;
py = p0y - p2y;
s = (d1x * py - d1y * px) / d;
if (s >= 0 && s <= 1) {
t = (d2x * py - d2y * px) / d;
if (t >= 0 && t <= 1) {
return {
x: p0x + (t * d1x),
y: p0y + (t * d1y)
}
}
}
return null
}
<canvas></canvas>
I don't have enough reputation to add this as a comment to Blindman67's solution, so i have to resort to adding this as an answer.
Blindman67's answer is great, but i needed support for polygons as well.
I am no math wizard so there may be a much better solution for polygons than this, but what i did was loop over all pairs of points from a polygon (so all sides of a polygon, really) and treat them as walls based on the code from Blindman67, then check the ray distance in the new rayDist2Polygon:
var rayDist2Polygon = function(ray){
let u,lineU;
const polLength = this.points.length;
const startX = this.x;
const startY = this.y;
// Loop over all lines of the polygon
for (i = 0; i < polLength; i++) {
const nextPoint = i === polLength - 1 ? this.points[0] : this.points[i + 1];
const x1 = startX + this.points[i].x;
const x2 = startX + nextPoint.x;
const y1 = startY + this.points[i].y;
const y2 = startY + nextPoint.y;
this.setupWall(x1, y1, x2, y2);
lineU = rayDist2Wall.bind(this)(ray);
if (!u) {
// If it's the first hit, assign it to `u`
u = lineU;
} else if (lineU < u) {
// If the current hit is smaller than anything we have so far, then this is the closest one, assign it to `u`
u = lineU;
}
}
// Reset positions after running this.setupWall;
this.x = startX;
this.y = startY;
return (!u || u < 0) ? Infinity : u; // if behind ray return Infinity else the dist
}
Then used the same logic to also support squares by converting a square's dimension/shape to points.
You can view it below, or fiddle with it at my codepen.
// Forked from https://stackoverflow.com/a/36566360/16956030
// All credits go to Blindman67
// All i did was add support for Polygons and Squares based on code from
// Blindman67, by treating each side of a polyon/square as a line/wall,
// then loop over each side and get the smallest result in rayDist2Polygon.
// I'm no math wizard and there may be a much better solution for these shapes,
// but this'll do for now.
console.clear();
const COLOUR = "BLACK";
const RAY_COLOUR = "RED";
const LINE_WIDTH = 4;
const RAY_LINE_WIDTH = 2;
const OBJ_COUNT = 20; // number of object in the scene;
const NUMBER_RAYS = 16; // number of rays
const RAY_DIR_SPACING = Math.PI / (NUMBER_RAYS / 2);
const RAY_ROTATE_SPEED = Math.PI * 2 / 31000;
if(typeof Math.hypot === "undefined"){ // poly fill for Math.hypot
Math.hypot = function(x, y){
return Math.sqrt(x * x + y * y);
}
}
var ctx, canvas, objects, ray, w, h, mouse, rand, ray, rayMaxLen, screenDiagonal;
// create a canvas and add to the dom
var canvas = document.createElement("canvas");
canvas.width = w = window.innerWidth;
canvas.height = h = window.innerHeight;
canvas.style.position = "absolute";
canvas.style.left = "0px";
canvas.style.top = "0px";
document.body.appendChild(canvas);
// objects to ray cast
objects = [];
// mouse object
mouse = {x :0, y: 0};
//========================================================================
// random helper
rand = function(min, max){
return Math.random() * (max - min) + min;
}
//========================================================================
// Ad Hoc draw line method
// col is the stroke style
// width is the storke width
var drawLine = function(col,width){
ctx.strokeStyle = col;
ctx.lineWidth = width;
ctx.beginPath();
ctx.moveTo(this.x,this.y);
ctx.lineTo(this.x + this.nx * this.len, this.y + this.ny * this.len);
ctx.stroke();
}
//========================================================================
// Ad Hoc draw circle method
// col is the stroke style
// width is the storke width
var drawCircle = function(col,width){
ctx.strokeStyle = col;
ctx.lineWidth = width;
ctx.beginPath();
ctx.arc(this.x , this.y, this.radius, 0 , Math.PI * 2);
ctx.stroke();
}
//========================================================================
// Ad Hoc draw square method
var drawSquare = function(){
ctx.beginPath();
ctx.rect(this.x, this.y, this.width, this.height);
ctx.stroke();
// Create array of points like a polygon based on the position & dimensions
// from this square, necessary for rayDist2Polygon
this.points = [
{ x: 0, y: 0},
{ x: this.width, y: 0},
{ x: this.width, y: this.height},
{ x: 0, y: this.height}
];
}
//========================================================================
// Ad Hoc draw [poligon] method
var drawPolygon = function(){
ctx.beginPath();
ctx.moveTo(this.x,this.y);
var polLength = this.points.length;
for(var i=0; i < polLength; ++i) {
ctx.lineTo(this.x + this.points[i].x, this.y + this.points[i].y);
}
ctx.closePath();
ctx.stroke();
}
//========================================================================
// Ad Hoc method for ray to set the direction vector
var updateRayDir = function(dir){
this.nx = Math.cos(dir);
this.ny = Math.sin(dir);
return this;
}
//========================================================================
// Creates a ray objects from
// x,y start location
// dir the direction in radians
// len the rays length
var createRay = function(x,y,dir,len){
return ({
x : x,
y : y,
len : len,
draw : drawLine,
setDir : updateRayDir, // add function to set direction
}).setDir(dir);
}
//========================================================================
// returns a circle object
// x,y is the center
// radius is the you know what..
// Note r2 is radius squared if you change the radius remember to set r2 as well
var createCircle = function(x , y, radius){
return {
x : x,
y : y,
draw : drawCircle, // draw function
rayDist : rayDist2Circle, // add ray cast method
radius : radius,
r2 : radius * radius, // ray caster needs square of radius may as well do it here
};
}
// Ad Hoc function to set the wall information
// x1,y1 are the start coords
// x2,y2 are the end coords
setupWallInformation = function(x1, y1, x2, y2){
this.x = x1;
this.y = y1;
this.vx = x2 - x1;
this.vy = y2 - y1;
this.len = Math.hypot(this.vx,this.vy);
this.nx = this.vx / this.len;
this.ny = this.vy / this.len;
return this;
}
//========================================================================
// returns a polygon object
// x,y are the start coords
// In this example the polygon always has the same shape
var createPolygon = function(x , y){
return {
x : x,
y : y,
points: [
{ x: 0, y: 0},
{ x: 100, y: 50},
{ x: 50, y: 100},
{ x: 0, y: 90}
],
draw : drawPolygon, // draw function
setupWall : setupWallInformation,
rayDist : rayDist2Polygon, // add ray cast method
};
}
//========================================================================
// returns a square object
// x,y are the start coords
// In this example the polygon always has the same shape
var createSquare = function(x , y, width, height){
return {
x : x,
y : y,
width: width,
height: height,
draw : drawSquare, // draw function
setupWall : setupWallInformation,
rayDist : rayDist2Polygon, // add ray cast method
};
}
//========================================================================
// Ad Hoc function to change the wall position
// x1,y1 are the start coords
// x2,y2 are the end coords
changeWallPosition = function(x1, y1, len, dir){
this.x = x1;
this.y = y1;
this.len = len;
this.nx = Math.cos(dir);
this.ny = Math.sin(dir);
return this;
}
//========================================================================
// returns a wall object
// x1,y1 are the star coords
// len is the length
// dir is the direction
var createWall = function(x1, y1, len, dir){
return({
x : x1, y : y1,
rayDist : rayDist2Wall, // add ray cast method
draw : drawLine,
setPos : changeWallPosition,
}).setPos(x1, y1, len, dir);
}
//========================================================================
// Self evident
// returns a distance or infinity if no valid solution
var rayDist2Circle = function(ray){
var vcx, vcy, v;
vcx = ray.x - this.x; // vector from ray to circle
vcy = ray.y - this.y;
v = -2 * (vcx * ray.nx + vcy * ray.ny);
v -= Math.sqrt(v * v - 4 * (vcx * vcx + vcy * vcy - this.r2)); // this.r2 is the radius squared
// If there is no solution then Math.sqrt returns NaN we should return Infinity
// Not interested in intercepts in the negative direction so return infinity
return isNaN(v) || v < 0 ? Infinity : v / 2;
}
//========================================================================
// returns the distance to the wall
// if no valid solution then return Infinity
var rayDist2Wall = function(ray){
var x,y,u;
rWCross = ray.nx * this.ny - ray.ny * this.nx;
if(!rWCross) { return Infinity; } // Not really needed.
x = ray.x - this.x; // vector from ray to wall start
y = ray.y - this.y;
u = (ray.nx * y - ray.ny * x) / rWCross; // unit distance along normal of wall
// does the ray hit the wall segment
if(u < 0 || u > this.len){ return Infinity;} /// no
// as we use the wall normal and ray normal the unit distance is the same as the
u = (this.nx * y - this.ny * x) / rWCross;
return u < 0 ? Infinity : u; // if behind ray return Infinity else the dist
}
//========================================================================
// returns the distance to the polygon
// if no valid solution then return Infinity
var rayDist2Polygon = function(ray){
let u,lineU;
const polLength = this.points.length;
const startX = this.x;
const startY = this.y;
// Loop over all lines of the polygon
for (i = 0; i < polLength; i++) {
const nextPoint = i === polLength - 1 ? this.points[0] : this.points[i + 1];
const x1 = startX + this.points[i].x;
const x2 = startX + nextPoint.x;
const y1 = startY + this.points[i].y;
const y2 = startY + nextPoint.y;
this.setupWall(x1, y1, x2, y2);
lineU = rayDist2Wall.bind(this)(ray);
if (!u) {
// If it's the first hit, assign it to `u`
u = lineU;
} else if (lineU < u) {
// If the current hit is smaller than anything we have so far, then this is the closest one, assign it to `u`
u = lineU;
}
}
// Reset positions after running this.setupWall;
this.x = startX;
this.y = startY;
return (!u || u < 0) ? Infinity : u; // if behind ray return Infinity else the dist
}
//========================================================================
// does a ray cast
// ray the ray to cast
// objects an array of objects
var castRay = function(ray,objects){
var i,minDist;
minDist = ray.len; // set the min dist to the rays length
i = objects.length; // number of objects to check
while(i > 0){
i -= 1;
minDist = Math.min(objects[i].rayDist(ray), minDist);
}
ray.len = minDist;
}
//========================================================================
// Draws all objects
// objects an array of objects
var drawObjects = function(objects){
var i = objects.length; // number of objects to check
while(i > 0){
objects[--i].draw(COLOUR, LINE_WIDTH);
}
}
//========================================================================
// called on start and resize
// creats a new scene each time
// fits the canvas to the avalible realestate
function reMakeAll(){
w = canvas.width = window.innerWidth;
h = canvas.height = window.innerHeight;
ctx = canvas.getContext("2d");
screenDiagonal = Math.hypot(window.innerWidth,window.innerHeight);
if(ray === undefined){
ray = createRay(0,0,0,screenDiagonal);
}
objects.length = 0;
var i = OBJ_COUNT;
while( i > 0 ){
var objectRandom = Math.floor(rand(0, 4));
if(objectRandom === 1){
objects.push(createWall(rand(0, w), rand(0, h), rand(screenDiagonal * 0.1, screenDiagonal * 0.2), rand(0, Math.PI * 2)));
}else if(objectRandom === 2){
objects.push(createPolygon(rand(0, w), rand(0, h)));
}else if(objectRandom === 3){
objects.push(createSquare(rand(0, w), rand(0, h), rand(screenDiagonal * 0.02, screenDiagonal * 0.05), rand(screenDiagonal * 0.02, screenDiagonal * 0.05)));
}else{
objects.push(createCircle(rand(0, w), rand(0, h), rand(screenDiagonal * 0.02, screenDiagonal * 0.05)));
}
i -= 1;
}
}
//========================================================================
function mouseMoveEvent(event){
mouse.x = event.clientX;
mouse.y = event.clientY;
}
//========================================================================
// updates all that is needed when needed
function updateAll(time){
var i;
ctx.clearRect(0,0,w,h);
ray.x = mouse.x;
ray.y = mouse.y;
drawObjects(objects);
i = 0;
while(i < NUMBER_RAYS){
ray.setDir(i * RAY_DIR_SPACING + time * RAY_ROTATE_SPEED);
ray.len = screenDiagonal;
castRay(ray,objects);
ray.draw(RAY_COLOUR, RAY_LINE_WIDTH);
i ++;
}
requestAnimationFrame(updateAll);
}
// add listeners
window.addEventListener("resize",reMakeAll);
canvas.addEventListener("mousemove",mouseMoveEvent);
// set it all up
reMakeAll();
// start the ball rolling
requestAnimationFrame(updateAll);

smoother lineWidth changes in canvas lineTo

so i'm trying to create a drawing tool in HTML5 canvas where the weight of the stroke increases the faster you move the mouse and decreases the slower you move. I'm using ctx.lineTo() but on my first attempt noticed that if i move too quickly the change in thickness is registered as obvious square increments ( rather than a smooth increase in weight )
so i changed the ctx.lineJoin and ctx.lineCap to "round" and it got a little better
but this is still not as smooth as i'd like. i'm shooting for something like this
any advice on how to make the change in weight a bit smoother would be great! here's a working demo: http://jsfiddle.net/0fhag522/1/
and here' a preview of my "dot" object ( the pen ) and my draw function:
var dot = {
start: false,
weight: 1,
open: function(x,y){
ctx.lineJoin = "round";
ctx.lineCap = "round";
ctx.beginPath();
ctx.moveTo(x,y);
},
connect: function(x,y){
ctx.lineWidth = this.weight;
ctx.lineTo(x,y);
ctx.stroke();
ctx.closePath();
ctx.beginPath();
ctx.moveTo(x,y);
},
close: function(){
ctx.closePath();
}
}
function draw(){
if(down){
if(!dot.start){
dot.close();
prevx = mx; prevy = my;
dot.open(mx,my);
dot.start=true;
}
else {
var dx = (prevx>mx) ? prevx-mx : mx-prevx;
var dy = (prevy>my) ? prevy-my : my-prevy;
dot.weight = Math.abs(dx-dy)/2;
dot.connect( mx,my );
prevx = mx; prevy = my;
}
}
}
Here is a simple function to create growing lines with a round line cap:
/*
* this function returns a Path2D object
* the path represents a growing line between two given points
*/
function createGrowingLine (x1, y1, x2, y2, startWidth, endWidth) {
// calculate direction vector of point 1 and 2
const directionVectorX = x2 - x1,
directionVectorY = y2 - y1;
// calculate angle of perpendicular vector
const perpendicularVectorAngle = Math.atan2(directionVectorY, directionVectorX) + Math.PI/2;
// construct shape
const path = new Path2D();
path.arc(x1, y1, startWidth/2, perpendicularVectorAngle, perpendicularVectorAngle + Math.PI);
path.arc(x2, y2, endWidth/2, perpendicularVectorAngle + Math.PI, perpendicularVectorAngle);
path.closePath();
return path;
}
const ctx = myCanvas.getContext('2d');
// create a growing line between P1(10, 10) and P2(250, 100)
// with a start line width of 10 and an end line width of 50
let line1 = createGrowingLine(10, 10, 250, 100, 10, 50);
ctx.fillStyle = 'green';
// draw growing line
ctx.fill(line1);
<canvas width="300" height="150" id="myCanvas"></canvas>
Explanation:
The function createGrowingLine constructs a shape between two given points by:
calculating the direction vector of the two points
calculating the angle in radians of the perpendicular vector
creating a semi circle path from the calculated angle to the calculated angle + 180 degree with the center and radius of the start point
creating another semi circle path from the calculated angle + 180 degree to the calculated angle with the center and radius of the end point
closing the path by connecting the start point of the first circle with the end point of the second circle
In case you do not want to have the rounded line cap use the following function:
/*
* this function returns a Path2D object
* the path represents a growing line between two given points
*/
function createGrowingLine (x1, y1, x2, y2, startWidth, endWidth) {
const startRadius = startWidth/2;
const endRadius = endWidth/2;
// calculate direction vector of point 1 and 2
let directionVectorX = x2 - x1,
directionVectorY = y2 - y1;
// calculate vector length
const directionVectorLength = Math.hypot(directionVectorX, directionVectorY);
// normalize direction vector (and therefore also the perpendicular vector)
directionVectorX = 1/directionVectorLength * directionVectorX;
directionVectorY = 1/directionVectorLength * directionVectorY;
// construct perpendicular vector
const perpendicularVectorX = -directionVectorY,
perpendicularVectorY = directionVectorX;
// construct shape
const path = new Path2D();
path.moveTo(x1 + perpendicularVectorX * startRadius, y1 + perpendicularVectorY * startRadius);
path.lineTo(x1 - perpendicularVectorX * startRadius, y1 - perpendicularVectorY * startRadius);
path.lineTo(x2 - perpendicularVectorX * endRadius, y2 - perpendicularVectorY * endRadius);
path.lineTo(x2 + perpendicularVectorX * endRadius, y2 + perpendicularVectorY * endRadius);
path.closePath();
return path;
}
const ctx = myCanvas.getContext('2d');
// create a growing line between P1(10, 10) and P2(250, 100)
// with a start line width of 10 and an end line width of 50
let line1 = createGrowingLine(10, 10, 250, 100, 10, 50);
ctx.fillStyle = 'green';
// draw growing line
ctx.fill(line1);
<canvas width="300" height="150" id="myCanvas"></canvas>
Since canvas does not have a variable width line you must draw closed paths between your line points.
However, this leaves a visible butt-joint.
To smooth the butt-joint, you can draw a circle at each joint.
Here is example code and a Demo:
var canvas = document.getElementById("canvas");
var ctx = canvas.getContext("2d");
var cw = canvas.width;
var ch = canvas.height;
var $canvas = $("#canvas");
var canvasOffset = $canvas.offset();
var offsetX = canvasOffset.left;
var offsetY = canvasOffset.top;
var scrollX = $canvas.scrollLeft();
var scrollY = $canvas.scrollTop();
var isDown = false;
var startX;
var startY;
var PI = Math.PI;
var halfPI = PI / 2;
var points = [];
$("#canvas").mousedown(function(e) {
handleMouseDown(e);
});
function handleMouseDown(e) {
e.preventDefault();
e.stopPropagation();
mx = parseInt(e.clientX - offsetX);
my = parseInt(e.clientY - offsetY);
var pointsLength = points.length;
if (pointsLength == 0) {
points.push({
x: mx,
y: my,
width: Math.random() * 5 + 2
});
} else {
var p0 = points[pointsLength - 1];
var p1 = {
x: mx,
y: my,
width: Math.random() * 5 + 2
};
addAngle(p0, p1);
p0.angle = p1.angle;
addEndcap(p0);
addEndcap(p1);
points.push(p1);
extendLine(p0, p1);
}
}
function addAngle(p0, p1) {
var dx = p1.x - p0.x;
var dy = p1.y - p0.y;
p1.angle = Math.atan2(dy, dx);
}
function addEndcap(p) {
p.x0 = p.x + p.width * Math.cos(p.angle - halfPI);
p.y0 = p.y + p.width * Math.sin(p.angle - halfPI);
p.x1 = p.x + p.width * Math.cos(p.angle + halfPI);
p.y1 = p.y + p.width * Math.sin(p.angle + halfPI);
}
function extendLine(p0, p1) {
ctx.beginPath();
ctx.moveTo(p0.x0, p0.y0);
ctx.lineTo(p0.x1, p0.y1);
ctx.lineTo(p1.x1, p1.y1);
ctx.lineTo(p1.x0, p1.y0);
ctx.closePath();
ctx.fillStyle = 'blue';
ctx.fill();
// draw a circle to cover the butt-joint
ctx.beginPath();
ctx.moveTo(p1.x, p1.y);
ctx.arc(p1.x, p1.y, p1.width, 0, Math.PI * 2);
ctx.closePath();
ctx.fill();
}
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>Click to add line segments.</h4>
<canvas id="canvas" width=300 height=300></canvas>

Categories