Related
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;
...
I'm working on a flow-network visualization with Javascript.
Vertices are represented as circles and edges are represented as arrows.
Here is my Edge class:
function Edge(u, v) {
this.u = u; // start vertex
this.v = v; // end vertex
this.draw = function() {
var x1 = u.x;
var y1 = u.y;
var x2 = v.x;
var y2 = v.y;
context.beginPath();
context.moveTo(x1, y1);
context.lineTo(x2, y2);
context.stroke();
var dx = x1 - x2;
var dy = y1 - y2;
var length = Math.sqrt(dx * dx + dy * dy);
x1 = x1 - Math.round(dx / ((length / (radius))));
y1 = y1 - Math.round(dy / ((length / (radius))));
x2 = x2 + Math.round(dx / ((length / (radius))));
y2 = y2 + Math.round(dy / ((length / (radius))));
// calculate the angle of the edge
var deg = (Math.atan(dy / dx)) * 180.0 / Math.PI;
if (dx < 0) {
deg += 180.0;
}
if (deg < 0) {
deg += 360.0;
}
// calculate the angle for the two triangle points
var deg1 = ((deg + 25 + 90) % 360) * Math.PI * 2 / 360.0;
var deg2 = ((deg + 335 + 90) % 360) * Math.PI * 2 / 360.0;
// calculate the triangle points
var arrowx = [];
var arrowy = [];
arrowx[0] = x2;
arrowy[0] = y2;
arrowx[1] = Math.round(x2 + 12 * Math.sin(deg1));
arrowy[1] = Math.round(y2 - 12 * Math.cos(deg1));
arrowx[2] = Math.round(x2 + 12 * Math.sin(deg2));
arrowy[2] = Math.round(y2 - 12 * Math.cos(deg2));
context.beginPath();
context.moveTo(arrowx[0], arrowy[0]);
context.lineTo(arrowx[1], arrowy[1]);
context.lineTo(arrowx[2], arrowy[2]);
context.closePath();
context.stroke();
context.fillStyle = "black";
context.fill();
};
}
Given the code
var canvas = document.getElementById('canvas'); // canvas element
var context = canvas.getContext("2d");
context.lineWidth = 1;
context.strokeStyle = "black";
var radius = 20; // vertex radius
var u = {
x: 50,
y: 80
};
var v = {
x: 150,
y: 200
};
var e = new Edge(u, v);
e.draw();
The draw() function will draw an edge between two vertices like this:
If we add the code
var k = new Edge(v, u);
k.draw();
We will get:
but I want to draw edges both directions as following:
(sorry for my bad paint skills)
Of course the vertices and the edge directions are not fixed.
A working example (with drawing vertex fucntion) on JSFiddle:
https://jsfiddle.net/Romansko/0fu01oec/18/
Aligning axis to a line.
It can make everything a little easier if you rotate the rendering to align with the line. Once you do that it is then easy to draw above or below the line as that is just in the y direction and along the line is the x direction.
Thus if you have a line
const line = {
p1 : { x : ? , y : ? },
p2 : { x : ? , y : ? },
};
Convert it to a vector and normalise that vector
// as vector from p1 to p2
var nx = line.p2.x - line.p1.x;
var ny = line.p2.y - line.p1.y;
// then get length
const len = Math.sqrt(nx * nx + ny * ny);
// use the length to normalise the vector
nx /= len;
ny /= len;
The normalised vector represents the new x axis we want to render along, and the y axis is at 90 deg to that. We can use setTransform to set both axis and the origin (0,0) point at the start of the line.
ctx.setTransform(
nx, ny, // the x axis
-ny, nx, // the y axis at 90 deg to the x axis
line.p1.x, line.p1.y // the origin (0,0)
)
Now rendering the line and arrow heads is easy as they are axis aligned
ctx.beginPath();
ctx.lineTo(0,0); // start of line
ctx.lineTo(len,0); // end of line
ctx.stroke();
// add the arrow head
ctx.beginPath();
ctx.lineTo(len,0); // tip of arrow
ctx.lineTo(len - 10, 10);
ctx.lineTo(len - 10, -10);
ctx.fill();
To render two lines offset from the center
var offset = 10;
ctx.beginPath();
ctx.lineTo(0,offset); // start of line
ctx.lineTo(len,offset); // end of line
ctx.moveTo(0,-offset); // start of second line
ctx.lineTo(len,-offset); // end of second line
ctx.stroke();
// add the arrow head
ctx.beginPath();
ctx.lineTo(len,offset); // tip of arrow
ctx.lineTo(len - 10, offset+10);
ctx.lineTo(len - 10, offset-10);
ctx.fill();
offset = -10;
// add second arrow head
ctx.beginPath();
ctx.lineTo(0,offset); // tip of arrow
ctx.lineTo(10, offset+10);
ctx.lineTo(10, offset-10);
ctx.fill();
And you can reset the transform with
ctx.setTransform(1,0,0,1,0,0); // restore default transform
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);
I have an arc which is rather large in size with a stroke that uses rgba values. It has a 50% alpha value and because of that, it is causing a big hit on my cpu profile for my browser.
So i want to find a way to optimize this so that where ever the arc is drawn in a canvas, it will only draw from one angle to another of which is visible on screen.
What i am having difficulty with, is working out the correct angle range.
Here is a visual example:
The top image is what the canvas actually does even if you don't see it, and the bottom one is what I am trying to do to save processing time.
I created a JSFiddle where you can click and drag the circle, though, the two angles are currently fixed:
https://jsfiddle.net/44tawd81/
Here is the draw code:
var canvas = document.getElementById('canvas');
var ctx = canvas.getContext('2d');
ctx.strokeStyle = 'red';
var radius = 50;
var pos = {
'x': canvas.width - 20,
'y': canvas.height /2
};
function draw(){
ctx.clearRect(0,0,canvas.width,canvas.height);
ctx.beginPath();
ctx.arc(pos.x,pos.y,radius,0,2*Math.PI); //need to adjust angle range
ctx.stroke();
requestAnimationFrame(draw);
}
draw();
What is the simplest way to find the angle range to draw based on it's position and size in a canvas?
Clipping a Circle
This is how to clip a circle to a rectangular region aligned to the x and y axis.
To clip the circle I search for the list of points where the circle intersects the clipping region. Starting from one side I go in a clockwise direction adding clip points as they are found. When all 4 sides are tested I then draw the arc segments that join the points found.
To find if a point has intercepted a clipping edge you find the distance the circle center is from that edge. Knowing the radius and the distance you can complete the right triangle to find the coordinates of the intercept.
For the left edge
// define the clip edge and circle
var clipLeftX = 100;
var radius = 200;
var centerX = 200;
var centerY = 200;
var dist = centerX - clipLeftX;
if(dist > radius) { // circle inside }
if(dist < -radius) {// circle completely outside}
// we now know the circle is clipped
Now calculate the distance from the circle y that the two clip points will be
// the right triangle with hypotenuse and one side know can be solved with
var clipDist = Math.sqrt(radius * radius - dist * dist);
So the points where the circle intercept the clipping line
var clipPointY1 = centerY - clipDist;
var clipPointY2 = centerY + clipDist;
With that you can work out if the two points are inside or outside the left side top or bottom by testing the two points against the top and bottom of the left line.
You will end up with either 0,1 or 2 clipping points.
Because arc requires angles to draw you need to calculate the angle from the circle center to the found points. You already have all the info needed
// dist is the x distance from the clip
var angle = Math.acos(radius/dist); // for left and right side
The hard part is making sure all the angles to the clipping point are in the correct order. The is a little fiddling about with flags to ensure that the arcs are in the correct order.
After checking all four sides you will end up with 0,2,4,6, or 8 clipping points representing the start and ends of the various clipped arcs. It is then simply iterating the arc segments and rendering them.
// Helper functions are not part of the answer
var canvas;
var ctx;
var mouse;
var resize = function(){
/** fullScreenCanvas.js begin **/
canvas = (function(){
var canvas = document.getElementById("canv");
if(canvas !== null){
document.body.removeChild(canvas);
}
// creates a blank image with 2d context
canvas = document.createElement("canvas");
canvas.id = "canv";
canvas.width = window.innerWidth;
canvas.height = window.innerHeight;
canvas.style.position = "absolute";
canvas.style.top = "0px";
canvas.style.left = "0px";
canvas.style.zIndex = 1000;
canvas.ctx = canvas.getContext("2d");
document.body.appendChild(canvas);
return canvas;
})();
ctx = canvas.ctx;
/** fullScreenCanvas.js end **/
/** MouseFull.js begin **/
var canvasMouseCallBack = undefined; // if needed
mouse = (function(){
var mouse = {
x : 0, y : 0, w : 0, alt : false, shift : false, ctrl : false,
interfaceId : 0, buttonLastRaw : 0, buttonRaw : 0,
over : false, // mouse is over the element
bm : [1, 2, 4, 6, 5, 3], // masks for setting and clearing button raw bits;
getInterfaceId : function () { return this.interfaceId++; }, // For UI functions
startMouse:undefined,
};
function mouseMove(e) {
var t = e.type, m = mouse;
m.x = e.offsetX; m.y = e.offsetY;
if (m.x === undefined) { m.x = e.clientX; m.y = e.clientY; }
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 (canvasMouseCallBack) { canvasMouseCallBack(m.x, m.y); }
e.preventDefault();
}
function startMouse(element){
if(element === undefined){
element = document;
}
"mousemove,mousedown,mouseup,mouseout,mouseover,mousewheel,DOMMouseScroll".split(",").forEach(
function(n){element.addEventListener(n, mouseMove);});
element.addEventListener("contextmenu", function (e) {e.preventDefault();}, false);
}
mouse.mouseStart = startMouse;
return mouse;
})();
if(typeof canvas === "undefined"){
mouse.mouseStart(canvas);
}else{
mouse.mouseStart();
}
}
/** MouseFull.js end **/
resize();
// Answer starts here
var w = canvas.width;
var h = canvas.height;
var d = Math.sqrt(w * w + h * h); // diagnal size
var cirLWidth = d * (1 / 100);
var rectCol = "black";
var rectLWidth = d * (1 / 100);
const PI2 = Math.PI * 2;
const D45_LEN = 0.70710678;
var angles = [0, 0, 0, 0, 0, 0, 0, 0, 0, 0]; // declared outside to stop GC
// create a clipArea
function rectArea(x, y, x1, y1) {
return {
left : x,
top : y,
width : x1 - x,
height : y1 - y
};
}
// create a arc
function arc(x, y, radius, start, end, col) {
return {
x : x,
y : y,
r : radius,
s : start,
e : end,
c : col
};
}
// draws an arc
function drawArc(arc, dir) {
ctx.strokeStyle = arc.c;
ctx.lineWidth = cirLWidth;
ctx.beginPath();
ctx.arc(arc.x, arc.y, arc.r, arc.s, arc.e, dir);
ctx.stroke();
}
// draws a clip area
function drawRect(r) {
ctx.strokeStyle = rectCol;
ctx.lineWidth = rectLWidth;
ctx.strokeRect(r.left, r.top, r.width, r.height);
}
// clip and draw an arc
// arc is the arc to clip
// clip is the clip area
function clipArc(arc, clip){
var count, distTop, distLeft, distBot, distRight, dist, swap, radSq, bot,right;
// cir1 is used to draw the clipped circle
cir1.x = arc.x;
cir1.y = arc.y;
count = 0; // number of clip points found;
bot = clip.top + clip.height; // no point adding these two over and over
right = clip.left + clip.width;
// get distance from all edges
distTop = arc.y - clip.top;
distBot = bot - arc.y;
distLeft = arc.x - clip.left;
distRight = right - arc.x;
radSq = arc.r * arc.r; // get the radius squared
// check if outside
if(Math.min(distTop, distBot, distRight, distLeft) < -arc.r){
return; // nothing to see so go home
}
// check inside
if(Math.min(distTop, distBot, distRight, distLeft) > arc.r){
drawArc(cir1);
return;
}
swap = true;
if(distLeft < arc.r){
// get the distance up and down to clip
dist = Math.sqrt(radSq - distLeft * distLeft);
// check the point is in the clip area
if(dist + arc.y < bot && arc.y + dist > clip.top){
// get the angel
angles[count] = Math.acos(distLeft / -arc.r);
count += 1;
}
if(arc.y - dist < bot && arc.y - dist > clip.top){
angles[count] = PI2 - Math.acos(distLeft / -arc.r); // get the angle
if(count === 0){ // if first point then set direction swap
swap = false;
}
count += 1;
}
}
if(distTop < arc.r){
dist = Math.sqrt(radSq - distTop * distTop);
if(arc.x - dist < right && arc.x - dist > clip.left){
angles[count] = Math.PI + Math.asin(distTop / arc.r);
count += 1;
}
if(arc.x+dist < right && arc.x+dist > clip.left){
angles[count] = PI2-Math.asin(distTop/arc.r);
if(count === 0){
swap = false;
}
count += 1;
}
}
if(distRight < arc.r){
dist = Math.sqrt(radSq - distRight * distRight);
if(arc.y - dist < bot && arc.y - dist > clip.top){
angles[count] = PI2 - Math.acos(distRight / arc.r);
count += 1;
}
if(dist + arc.y < bot && arc.y + dist > clip.top){
angles[count] = Math.acos(distRight / arc.r);
if(count === 0){
swap = false;
}
count += 1;
}
}
if(distBot < arc.r){
dist = Math.sqrt(radSq - distBot * distBot);
if(arc.x + dist < right && arc.x + dist > clip.left){
angles[count] = Math.asin(distBot / arc.r);
count += 1;
}
if(arc.x - dist < right && arc.x - dist > clip.left){
angles[count] = Math.PI + Math.asin(distBot / -arc.r);
if(count === 0){
swap = false;
}
count += 1;
}
}
// now draw all the arc segments
if(count === 0){
return;
}
if(count === 2){
cir1.s = angles[0];
cir1.e = angles[1];
drawArc(cir1,swap);
}else
if(count === 4){
if(swap){
cir1.s = angles[1];
cir1.e = angles[2];
drawArc(cir1);
cir1.s = angles[3];
cir1.e = angles[0];
drawArc(cir1);
}else{
cir1.s = angles[2];
cir1.e = angles[3];
drawArc(cir1);
cir1.s = angles[0];
cir1.e = angles[1];
drawArc(cir1);
}
}else
if(count === 6){
cir1.s = angles[1];
cir1.e = angles[2];
drawArc(cir1);
cir1.s = angles[3];
cir1.e = angles[4];
drawArc(cir1);
cir1.s = angles[5];
cir1.e = angles[0];
drawArc(cir1);
}else
if(count === 8){
cir1.s = angles[1];
cir1.e = angles[2];
drawArc(cir1);
cir1.s = angles[3];
cir1.e = angles[4];
drawArc(cir1);
cir1.s = angles[5];
cir1.e = angles[6];
drawArc(cir1);
cir1.s = angles[7];
cir1.e = angles[0];
drawArc(cir1);
}
return;
}
var rect = rectArea(50, 50, w - 50, h - 50);
var circle = arc(w * (1 / 2), h * (1 / 2), w * (1 / 5), 0, Math.PI * 2, "#AAA");
var cir1 = arc(w * (1 / 2), h * (1 / 2), w * (1 / 5), 0, Math.PI * 2, "red");
var counter = 0;
var countStep = 0.03;
function update() {
var x, y;
ctx.clearRect(0, 0, w, h);
circle.x = mouse.x;
circle.y = mouse.y;
drawArc(circle, "#888"); // draw unclipped arc
x = Math.cos(counter * 0.1);
y = Math.sin(counter * 0.3);
rect.top = h / 2 - Math.abs(y * (h * 0.4)) - 5;
rect.left = w / 2 - Math.abs(x * (w * 0.4)) - 5;
rect.width = Math.abs(x * w * 0.8) + 10;
rect.height = Math.abs(y * h * 0.8) + 10;
cir1.col = "RED";
clipArc(circle, rect); // draw the clipped arc
drawRect(rect); // draw the clip area. To find out why this method
// sucks move this to before drawing the clipped arc.
requestAnimationFrame(update);
if(mouse.buttonRaw !== 1){
counter += countStep;
}
ctx.font = Math.floor(w * (1 / 50)) + "px verdana";
ctx.fillStyle = "white";
ctx.strokeStyle = "black";
ctx.lineWidth = Math.ceil(w * (1 / 300));
ctx.textAlign = "center";
ctx.lineJoin = "round";
ctx.strokeText("Left click and hold to pause", w/ 2, w * (1 / 40));
ctx.fillText("Left click and hold to pause", w/ 2, w * (1 / 40));
}
update();
window.addEventListener("resize",function(){
resize();
w = canvas.width;
h = canvas.height;
rect = rectArea(50, 50, w - 50, h - 50);
circle = arc(w * (1 / 2), h * (1 / 2), w * (1 / 5), 0, Math.PI * 2, "#AAA");
cir1 = arc(w * (1 / 2), h * (1 / 2), w * (1 / 5), 0, Math.PI * 2, "red");
});
The quickest way to clip a circle.
That is the quickest I could manage to do it in code. There is some room for optimization but not that much in the agorithum.
The best solution is of course to use the canvas 2D context API clip() method.
ctx.save();
ctx.rect(10,10,200,200); // define the clip region
ctx.clip(); // activate the clip.
//draw your circles
ctx.restore(); // remove the clip.
This is much quicker than the method I showed above and should be used unless you have a real need to know the clip points and arcs segments that are inside or outside the clip region.
To find the angle to draw based on circle position, canvas position, circle size, and canvas size:
Determine intersection of circle and canvas
Calculate points on the circle at which intersection occurs
You then have an isosceles triangle.
You can use cosine formula for calculation of the angle.
c^2=a^2+b^2−2abcos(α)
a and b are sides adjacent to the angle α, which are the radius of the center r. c is the distance between the two points P1 and P2. So we get:
|P1−P2|^2=2r^2−2r^2cos(α)
2r^2−|P1−P2|^2/2r2=cos(α)
α=cos−1(2r^2−|P1−P2|^2/2r^2)
I have to move the small rectangle on the path. The rectangle moves after a click inside the canvas.
I am not able to animate it as the object just jumps to the required point.
Please find the code on Fiddle.
HTML
<canvas id="myCanvas" width=578 height=200></canvas>
CSS
#myCanvas {
width:578px;
height:200px;
border:2px thin;
}
JavaScript
var myRectangle = {
x: 100,
y: 20,
width: 25,
height: 10,
borderWidth: 1
};
$(document).ready(function () {
$('#myCanvas').css("border", "2px solid black");
var canvas = document.getElementById('myCanvas');
var context = canvas.getContext('2d');
var cntxt = canvas.getContext('2d');
drawPath(context);
drawRect(myRectangle, cntxt);
$('#myCanvas').click(function () {
function animate(myRectangle, canvas, cntxt, startTime) {
var time = (new Date()).getTime() - startTime;
var linearSpeed = 10;
var newX = Math.round(Math.sqrt((100 * 100) + (160 * 160)));
if (newX < canvas.width - myRectangle.width - myRectangle.borderWidth / 2) {
myRectangle.x = newX;
}
context.clearRect(0, 0, canvas.width, canvas.height);
drawPath(context);
drawRect(myRectangle, cntxt);
// request new frame
requestAnimFrame(function () {
animate(myRectangle, canvas, cntxt, startTime);
});
}
drawRect(myRectangle, cntxt);
myRectangle.x = 100;
myRectangle.y = 121;
setTimeout(function () {
var startTime = (new Date()).getTime();
animate(myRectangle, canvas, cntxt, startTime);
}, 1000);
});
});
$(document).keypress(function (e) {
if (e.which == 13) {
$('#myCanvas').click();
}
});
function drawRect(myRectangle, cntxt) {
cntxt.beginPath();
cntxt.rect(myRectangle.x, myRectangle.y, myRectangle.width, myRectangle.height);
cntxt.fillStyle = 'cyan';
cntxt.fill();
cntxt.strokeStyle = 'black';
cntxt.stroke();
};
function drawPath(context) {
context.beginPath();
context.moveTo(100, 20);
// line 1
context.lineTo(200, 160);
// quadratic curve
context.quadraticCurveTo(230, 200, 250, 120);
// bezier curve
context.bezierCurveTo(290, -40, 300, 200, 400, 150);
// line 2
context.lineTo(500, 90);
context.lineWidth = 5;
context.strokeStyle = 'blue';
context.stroke();
};
Here is how to move an object along a particular path
Animation involves movement over time. So for each “frame” of your animation you need to know the XY coordinate where to draw your moving object (rectangle).
This code takes in a percent-complete (0.00 to 1.00) and returns the XY coordinate which is that percentage along the path segment. For example:
0.00 will return the XY at the beginning of the line (or curve).
0.50 will return the XY at the middle of the line (or curve).
1.00 will return the XY at the end of the line (or curve).
Here is the code to get the XY at the specified percentage along a line:
// line: percent is 0-1
function getLineXYatPercent(startPt,endPt,percent) {
var dx = endPt.x-startPt.x;
var dy = endPt.y-startPt.y;
var X = startPt.x + dx*percent;
var Y = startPt.y + dy*percent;
return( {x:X,y:Y} );
}
Here is the code to get the XY at the specified percentage along a quadratic bezier curve:
// quadratic bezier: percent is 0-1
function getQuadraticBezierXYatPercent(startPt,controlPt,endPt,percent) {
var x = Math.pow(1-percent,2) * startPt.x + 2 * (1-percent) * percent * controlPt.x + Math.pow(percent,2) * endPt.x;
var y = Math.pow(1-percent,2) * startPt.y + 2 * (1-percent) * percent * controlPt.y + Math.pow(percent,2) * endPt.y;
return( {x:x,y:y} );
}
Here is the code to get the XY at the specified percentage along a cubic bezier curve:
// cubic bezier percent is 0-1
function getCubicBezierXYatPercent(startPt,controlPt1,controlPt2,endPt,percent){
var x=CubicN(percent,startPt.x,controlPt1.x,controlPt2.x,endPt.x);
var y=CubicN(percent,startPt.y,controlPt1.y,controlPt2.y,endPt.y);
return({x:x,y:y});
}
// cubic helper formula at percent distance
function CubicN(pct, a,b,c,d) {
var t2 = pct * pct;
var t3 = t2 * pct;
return a + (-a * 3 + pct * (3 * a - a * pct)) * pct
+ (3 * b + pct * (-6 * b + b * 3 * pct)) * pct
+ (c * 3 - c * 3 * pct) * t2
+ d * t3;
}
And here is how you put it all together to animate the various segments of your path
// calculate the XY where the tracking will be drawn
if(pathPercent<25){
var line1percent=pathPercent/24;
xy=getLineXYatPercent({x:100,y:20},{x:200,y:160},line1percent);
}
else if(pathPercent<50){
var quadPercent=(pathPercent-25)/24
xy=getQuadraticBezierXYatPercent({x:200,y:160},{x:230,y:200},{x:250,y:120},quadPercent);
}
else if(pathPercent<75){
var cubicPercent=(pathPercent-50)/24
xy=getCubicBezierXYatPercent({x:250,y:120},{x:290,y:-40},{x:300,y:200},{x:400,y:150},cubicPercent);
}
else {
var line2percent=(pathPercent-75)/25
xy=getLineXYatPercent({x:400,y:150},{x:500,y:90},line2percent);
}
// draw the tracking rectangle
drawRect(xy);
Here is working code and a Fiddle: http://jsfiddle.net/m1erickson/LumMX/
<!doctype html>
<html lang="en">
<head>
<style>
body{ background-color: ivory; }
canvas{border:1px solid red;}
</style>
<link rel="stylesheet" href="http://code.jquery.com/ui/1.10.3/themes/smoothness/jquery-ui.css" />
<script src="http://code.jquery.com/jquery-1.9.1.js"></script>
<script src="http://code.jquery.com/ui/1.10.3/jquery-ui.js"></script>
<script>
$(function() {
var canvas=document.getElementById("canvas");
var ctx=canvas.getContext("2d");
// set starting values
var fps = 60;
var percent=0
var direction=1;
// start the animation
animate();
function animate() {
// set the animation position (0-100)
percent+=direction;
if(percent<0){ percent=0; direction=1; };
if(percent>100){ percent=100; direction=-1; };
draw(percent);
// request another frame
setTimeout(function() {
requestAnimationFrame(animate);
}, 1000 / fps);
}
// draw the current frame based on sliderValue
function draw(sliderValue){
// redraw path
ctx.clearRect(0,0,canvas.width,canvas.height);
ctx.lineWidth = 5;
ctx.beginPath();
ctx.moveTo(100, 20);
ctx.lineTo(200, 160);
ctx.strokeStyle = 'red';
ctx.stroke();
ctx.beginPath();
ctx.moveTo(200, 160);
ctx.quadraticCurveTo(230, 200, 250, 120);
ctx.strokeStyle = 'green';
ctx.stroke();
ctx.beginPath();
ctx.moveTo(250,120);
ctx.bezierCurveTo(290, -40, 300, 200, 400, 150);
ctx.strokeStyle = 'blue';
ctx.stroke();
ctx.beginPath();
ctx.moveTo(400, 150);
ctx.lineTo(500, 90);
ctx.strokeStyle = 'gold';
ctx.stroke();
// draw the tracking rectangle
var xy;
if(sliderValue<25){
var percent=sliderValue/24;
xy=getLineXYatPercent({x:100,y:20},{x:200,y:160},percent);
}
else if(sliderValue<50){
var percent=(sliderValue-25)/24
xy=getQuadraticBezierXYatPercent({x:200,y:160},{x:230,y:200},{x:250,y:120},percent);
}
else if(sliderValue<75){
var percent=(sliderValue-50)/24
xy=getCubicBezierXYatPercent({x:250,y:120},{x:290,y:-40},{x:300,y:200},{x:400,y:150},percent);
}
else {
var percent=(sliderValue-75)/25
xy=getLineXYatPercent({x:400,y:150},{x:500,y:90},percent);
}
drawRect(xy,"red");
}
// draw tracking rect at xy
function drawRect(point,color){
ctx.fillStyle="cyan";
ctx.strokeStyle="gray";
ctx.lineWidth=3;
ctx.beginPath();
ctx.rect(point.x-13,point.y-8,25,15);
ctx.fill();
ctx.stroke();
}
// draw tracking dot at xy
function drawDot(point,color){
ctx.fillStyle=color;
ctx.strokeStyle="black";
ctx.lineWidth=3;
ctx.beginPath();
ctx.arc(point.x,point.y,8,0,Math.PI*2,false);
ctx.closePath();
ctx.fill();
ctx.stroke();
}
// line: percent is 0-1
function getLineXYatPercent(startPt,endPt,percent) {
var dx = endPt.x-startPt.x;
var dy = endPt.y-startPt.y;
var X = startPt.x + dx*percent;
var Y = startPt.y + dy*percent;
return( {x:X,y:Y} );
}
// quadratic bezier: percent is 0-1
function getQuadraticBezierXYatPercent(startPt,controlPt,endPt,percent) {
var x = Math.pow(1-percent,2) * startPt.x + 2 * (1-percent) * percent * controlPt.x + Math.pow(percent,2) * endPt.x;
var y = Math.pow(1-percent,2) * startPt.y + 2 * (1-percent) * percent * controlPt.y + Math.pow(percent,2) * endPt.y;
return( {x:x,y:y} );
}
// cubic bezier percent is 0-1
function getCubicBezierXYatPercent(startPt,controlPt1,controlPt2,endPt,percent){
var x=CubicN(percent,startPt.x,controlPt1.x,controlPt2.x,endPt.x);
var y=CubicN(percent,startPt.y,controlPt1.y,controlPt2.y,endPt.y);
return({x:x,y:y});
}
// cubic helper formula at percent distance
function CubicN(pct, a,b,c,d) {
var t2 = pct * pct;
var t3 = t2 * pct;
return a + (-a * 3 + pct * (3 * a - a * pct)) * pct
+ (3 * b + pct * (-6 * b + b * 3 * pct)) * pct
+ (c * 3 - c * 3 * pct) * t2
+ d * t3;
}
}); // end $(function(){});
</script>
</head>
<body>
<canvas id="canvas" width=600 height=300></canvas>
</body>
</html>
If you're gonna use the built-in Bezier curves of the canvas, you would still need to do the math yourself.
You can use this implementation of a cardinal spline and have all the points returned for you pre-calculated.
An example of usage is this little sausage-mobile moving along the slope (generated with the above cardinal spline):
Full demo here (cut-and-copy as you please).
The main things you need is when you have the point array is to find two points you want to use for the object. This will give us the angle of the object:
cPoints = quantX(pointsFromCardinalSpline); //see below
//get points from array (dx = current array position)
x1 = cPoints[dx];
y1 = cPoints[dx + 1];
//get end-points from array (dlt=length, must be an even number)
x2 = cPoints[dx + dlt];
y2 = cPoints[dx + dlt + 1];
To avoid stretching in steeper slopes we recalculate the length based on angle. To get an approximate angle we use the original end-point to get an angle, then we calculate a new length of the line based on wanted length and this angle:
var dg = getLineAngle(x1, y1, x2, y2);
var l = ((((lineToAngle(x1, y2, dlt, dg).x - x1) / 2) |0) * 2);
x2 = cPoints[dx + l];
y2 = cPoints[dx + l + 1];
Now we can plot the "car" along the slope by subtracting it's vertical height from the y positions.
What you will notice doing just this is that the "car" moves at variable speed. This is due to the interpolation of the cardinal spline.
We can smooth it out so the speed look more even by quantize the x axis. It will still not be perfect as in steep slopes the y-distance between to points will be greater than on a flat surface - we would really need a quadratic quantization, but for this purpose we do only the x-axis.
This gives us a new array with new points for each x-position:
function quantX(pts) {
var min = 99999999,
max = -99999999,
x, y, i, p = pts.length,
res = [];
//find min and max of x axis
for (i = 0; i < pts.length - 1; i += 2) {
if (pts[i] > max) max = pts[i];
if (pts[i] < min) min = pts[i];
}
max = max - min;
//this will quantize non-existng points
function _getY(x) {
var t = p,
ptX1, ptX2, ptY1, ptY2, f, y;
for (; t >= 0; t -= 2) {
ptX1 = pts[t];
ptY1 = pts[t + 1];
if (x >= ptX1) {
//p = t + 2;
ptX2 = pts[t + 2];
ptY2 = pts[t + 3];
f = (ptY2 - ptY1) / (ptX2 - ptX1);
y = (ptX1 - x) * f;
return ptY1 - y;
}
}
}
//generate new array per-pixel on the x-axis
//note: will not work if curve suddenly goes backwards
for (i = 0; i < max; i++) {
res.push(i);
res.push(_getY(i));
}
return res;
}
The other two functions we need is the one calculating the angle for a line, and the one calculating end-points based on angle and length:
function getLineAngle(x1, y1, x2, y2) {
var dx = x2 - x1,
dy = y2 - y1,
th = Math.atan2(dy, dx);
return th * 180 / Math.PI;
}
function lineToAngle(x1, y1, length, angle) {
angle *= Math.PI / 180;
var x2 = x1 + length * Math.cos(angle),
y2 = y1 + length * Math.sin(angle);
return {x: x2, y: y2};
}