how to draw a curve that passes three points in canvas? - javascript

What I am going to do is to change the curve of a circle.
If I click one point in a circle and drag it to other point, that arc of the circle should be extended or contracted accordingly.
I was going to use beizer curve but there's no guarantee that the new beizer curve will pass the dragged point.
Attached is the image showing a new curve when mouse dragged which I can not solve.
can anyone help me on this matter?
I will be looking forward to your reply

Fit circle to points
Maybe this will help.
The function at the top of the example fitCircleToPoints(x1, y1, x2, y2, x3, y3) will fit a circle to 3 points.
It returns an object
{
x, y, // center of circle
radius, // radius of circle
CCW, // true if circle segment is counter clockwise
}
If the 3 points are all on the same line then there is no circle that can fit (radius Infinity is not valid) so the function returns undefined.
function fitCircleToPoints(x1, y1, x2, y2, x3, y3) {
var x, y, u;
const slopeA = (x2 - x1) / (y1 - y2); // slope of vector from point 1 to 2
const slopeB = (x3 - x2) / (y2 - y3); // slope of vector from point 2 to 3
if (slopeA === slopeB) { return } // Slopes are same thus 3 points form striaght line. No circle can fit.
if (y1 === y2) { // special case with points 1 and 2 have same y
x = ((x1 + x2) / 2);
y = slopeB * x + (((y2 + y3) / 2) - slopeB * ((x2 + x3) / 2));
} else if(y2 === y3) { // special case with points 2 and 3 have same y
x = ((x2 + x3) / 2);
y = slopeA * x + (((y1 + y2) / 2) - slopeA * ((x1 + x2) / 2));
} else {
x = ((((y2 + y3) / 2) - slopeB * ((x2 + x3) / 2)) - (u = ((y1 + y2) / 2) - slopeA * ((x1 + x2) / 2))) / (slopeA - slopeB);
y = slopeA * x + u;
}
return {
x, y,
radius: ((x1 - x) ** 2 + (y1 - y) ** 2) ** 0.5,
CCW: ((x3 - x1) * (y2 - y1) - (y3 - y1) * (x2 - x1)) >= 0,
};
}
requestAnimationFrame(update);
Math.TAU = Math.PI * 2;
const ctx = canvas.getContext("2d");
const mouse = {x : 0, y : 0, button : false}
function mouseEvents(e){
const bounds = canvas.getBoundingClientRect();
mouse.x = e.pageX - bounds.left - scrollX;
mouse.y = e.pageY - bounds.top - scrollY;
mouse.button = e.type === "mousedown" ? true : e.type === "mouseup" ? false : mouse.button;
}
["down","up","move"].forEach(name => document.addEventListener("mouse" + name, mouseEvents));
var w = canvas.width, h = canvas.height, cw = w / 2, ch = h / 2;
var nearest, ox, oy, dragging, dragIdx;
const points = [10,110,200,100,400,110];
function drawPoint(x, y, rad, col = "black") {
ctx.strokeStyle = col;
ctx.beginPath();
ctx.arc(x, y, rad, 0, Math.TAU);
ctx.stroke();
}
function drawLines(idx, col = "black") {
ctx.strokeStyle = col;
ctx.beginPath();
ctx.lineTo(points[idx++], points[idx++]);
ctx.lineTo(points[idx++], points[idx++]);
ctx.lineTo(points[idx++], points[idx++]);
ctx.stroke();
}
function drawPoints() {
var i = 0, x, y;
nearest = - 1;
var minDist = 20;
while (i < points.length) {
drawPoint(x = points[i++], y = points[i++], 4);
const dist = (x - mouse.x) ** 2 + (y - mouse.y) ** 2;
if (dist < minDist) {
minDist = dist;
nearest = i - 2;
}
}
}
function update(){
ctx.setTransform(1,0,0,1,0,0); // reset transform
if (w !== innerWidth || h !== innerHeight) {
cw = (w = canvas.width = innerWidth) / 2;
ch = (h = canvas.height = innerHeight) / 2;
} else {
ctx.clearRect(0,0,w,h);
}
canvas.style.cursor = "default";
drawPoints();
if (nearest > -1) {
if (mouse.button) {
if (!dragging) {
dragging = true;
ox = points[nearest] - mouse.x;
oy = points[nearest+1] - mouse.y;
dragIdx = nearest;
}
} else {
canvas.style.cursor = "move";
}
drawPoint(points[nearest], points[nearest + 1], 6, "red")
}
if (dragging) {
if (!mouse.button) {
dragging = false;
} else {
points[dragIdx] = mouse.x + ox;
points[dragIdx + 1] = mouse.y + oy
canvas.style.cursor = "none";
}
}
drawLines(0, "#0002");
const circle = fitCircleToPoints(points[0], points[1], points[2], points[3], points[4], points[5]);
if (circle) {
ctx.strokeStyle = "#000";
const ang1 = Math.atan2(points[1] - circle.y, points[0]- circle.x);
const ang2 = Math.atan2(points[5] - circle.y, points[4]- circle.x);
ctx.beginPath();
ctx.arc(circle.x, circle.y, circle.radius, ang1, ang2, circle.CCW);
ctx.stroke();
}
requestAnimationFrame(update);
}
canvas { position : absolute; top : 0px; left : 0px; }
<canvas id="canvas"></canvas>
Use mouse to move points.

You could draw two curves but make sure the control points are in line so you get a smooth transition. Using this tool I've made an example. His is however not a circle and not an ellipse.

Related

How to detect collision between circle and rotating polygon with JavaScript?

My program begins by drawing an n-sided polygon with circles inside it, like this: initial output. The polygon is continuously rotating around its center, and the circles have incredibly simple physics to fall down with gravity and bounce when they hit a wall.
However, I am having trouble detecting when the circles have hit a wall. Here is my current collision detection method, which involves finding the minimum distance from the line to the circle and comparing it to the circle's radius:
function collisionDetector(ball){
//calculate distance from all sides of the polygon
for(var i = 0; i < currentPolyPointsX.length -1; i++){
//get coordinates of line end points
let x1 = currentPolyPointsX[i];
let x2 = currentPolyPointsX[i+1];
let y1 = currentPolyPointsY[i];
let y2 = currentPolyPointsY[i+1];
//calculate length of line
let distanceX = x1 - x2;
let distanceY = y1 - y2;
let length = Math.sqrt( (distanceX *distanceX) + (distanceY*distanceY) );
//calculate dot product of vectors from line ends and ball
let dot = ( ((ball.x - x1) * (x2 - x1)) + ((ball.y - y1) * (y2-y1)) )
/ Math.pow(length, 2);
//calculate x and y coordinate on line (extends to infinity) closest to ball
let closestX = x1 + (dot * (x2 - x1));
let closestY = y1 + (dot * (y2 - y1));
//if those coordinates are not currently on our line, return false
if(!onLine(x1, y1, x2, y2, closestX, closestY)){
return false;
}
//calculate distance from closest coordinates to ball
distanceX = closestX - ball.x;
distanceY = closestY - ball.y;
let distance = Math.sqrt( (distanceX * distanceX) + (distanceY * distanceY) );
//if the ball is less than/equal to one radius away, it has collided
if( distance <= ball.returnRadius() ){
return true;
}else{
return false;
}
}
}
The points of the polygon are pushed into parallel arrays by this code:
function drawPolygon() { //x&y are positions, side is side number, r is size
//get current values of canvas center
var x = canvas.width /2;
var y = canvas.height /2;
var r = canvas.width /2;
//draw {sides} sided polygon
ctx.beginPath();
ctx.moveTo (x + r * Math.cos(0), y + r * Math.sin(0));
//clear currently stored points
currentPolyPointsX.length = 0;
currentPolyPointsY.length = 0;
//draw and save new polygon points in array
for (var i = 1; i <= sides; i ++) {
ctx.lineTo (x + r * Math.cos(i * 2 * Math.PI / sides),
y + r * Math.sin(i * 2 * Math.PI / sides));
currentPolyPointsX.push(x + r * Math.cos(i * 2 * Math.PI / sides)); // <----
currentPolyPointsY.push(y + r * Math.sin(i * 2 * Math.PI / sides)); // <----
}
//draw the polygon
ctx.strokeStyle = "black";
ctx.lineWidth = 2;
ctx.stroke();
ctx.save();
}
And then modified to reflect current rotation by this code:
function rotatePolygon(x){
let centerX = canvas.height/2;
let centerY = canvas.height/2;
let angle = x * Math.PI / 180
//clear canvas
ctx.clearRect(0, 0, canvas.width, canvas.height);
//move rotation point to center of canvas
ctx.translate(centerX, centerY);
//rotate around center of canvas
ctx.rotate(angle);
//move back to origin
ctx.translate(-centerX, -centerY);
//draw rotated polygon
drawPolygon();
//adjust coordinates to match rotation
let cos = Math.cos(x * Math.PI / 180);
let sin = Math.sin(x * Math.PI / 180);
for(let i = 0; i < currentPolyPointsX.length; i++){ // <----
currentPolyPointsX[i] = ((cos * (currentPolyPointsX[i] - centerX)) // <----
- (sin * (currentPolyPointsY[i] - centerY)) + centerX); // <----
currentPolyPointsY[i] = ((cos * (currentPolyPointsY[i] - centerY)) // <----
+ (sin * (currentPolyPointsX[i] - centerX)) + centerY); // <----
}
//un-rotate
ctx.translate(canvas.width/2, canvas.height/2);
ctx.rotate(-x * Math.PI / 180);
ctx.translate(-canvas.width/2, -canvas.width/2);
}
For whatever reason, this only detects collision between one of the polygon's walls, which doesn't make sense to me because it is capturing all the polygon's points and should be comparing them all to the circle's positions. This is a snippet of the console output for the parallel arrays:
X values: 100.26987463346667,2.871334680692655,162.69628391854758,358.87207475054254,320.2904320167505
Y values: 250.03751023155084,132.28082275846333,92.90811041924998,186.33112343743124,283.44243315330453
What am I doing wrong? No matter how I fiddle with the code, it only ever detects one wall. Even when rotation is turned off. Even when the number of sides is changed. Even when the speed of rotation as well as the speed of gravity are adjusted. It is driving me nuts! Any help you all provide will be greatly appreciated.
Here is a full snippet of the code in question. If you play with rotation speed, it helps to see which side is operational.
let canvas = document.getElementById("mainCanvas");
let ctx = canvas.getContext("2d");
let rotationSlider = document.getElementById("rotationSlider");
let sidesSlider = document.getElementById("sidesSlider");
//# sided polygon to draw
let sides = 5;
let speed = 33;
let rotationIncrement = 1;
let sideLength = 0;
//stores current corner coordinates for polygon
let currentPolyPointsX = [];
let currentPolyPointsY = [];
const GRAVITY = 1;
const Ball = {
x: 0,
y: 0,
vector: [0, 0],
speed: 0,
gravity: 1,
//returns the ball's radius, which is set according to canvas height
returnRadius: function() {
return canvas.height * 0.01;
}
}
let ballHolder = [];
function startup() {
resizeCanvas();
main();
ballInit(10);
sideLengthInit();
}
function main() {
//get center coordinates of canvas
var center = canvas.width / 2;
//pass desired size, and center coords (x,y) to drawHexagon
rotationLoop();
ballLoop();
setTimeout(main, speed);
}
function ballInit(amount) {
//clear any residual balls
ballHolder = [];
//initialize array of objects with {amount} of balls and give each a random jitter
for (var i = 0; i < amount; i++) {
ballHolder.push(Object.create(Ball));
ballHolder[i].x = canvas.height / 2 + (Math.floor(Math.random() * 50)) * (Math.random() < 0.5 ? -1 : 1);
ballHolder[i].y = canvas.height / 2 + (Math.floor(Math.random() * 50)) * (Math.random() < 0.5 ? -1 : 1);
}
}
//calculated distance between two points via distance formula
function distanceCalc(x1, y1, x2, y2) {
return Math.sqrt(((x2 - x1) * (x2 - x1)) + ((y2 - y1) * (y2 - y1)));
}
function ballLoop() {
//draw each ball on the canvas, and calculate bounce + collision with container
ballHolder.forEach(ball => {
//calculate gravity
ball.y = gravityCalc(ball);
ctx.beginPath();
ctx.arc((ball.x), (ball.y), canvas.height * 0.01, 0, 2 * Math.PI);
ctx.stroke();
ctx.fillStyle = "blue";
ctx.fill();
//check for collision
if (collisionDetector(ball)) {
//calculate bounce if collision occurs
ball.gravity = ball.gravity * -1;
}
//console.log("ball " + ball.x + " " + ball.y);
});
}
function gravityCalc(ball) {
return ball.y + (GRAVITY * ball.gravity);
}
function sideLengthInit() {
sideLength = distanceCalc(currentPolyPointsX[0], currentPolyPointsY[0],
currentPolyPointsX[1], currentPolyPointsY[1])
}
function collisionDetector(ball) {
//calculate distance from all sides of the polygon
for (var i = 0; i < currentPolyPointsX.length - 1; i++) {
//get coordinates of line end points
let x1 = currentPolyPointsX[i];
let x2 = currentPolyPointsX[i + 1];
let y1 = currentPolyPointsY[i];
let y2 = currentPolyPointsY[i + 1];
//calculate length of line
let distanceX = x1 - x2;
let distanceY = y1 - y2;
let length = Math.sqrt((distanceX * distanceX) + (distanceY * distanceY));
//calculate dot product of vectors from line ends and ball
let dot = (((ball.x - x1) * (x2 - x1)) + ((ball.y - y1) * (y2 - y1))) /
Math.pow(length, 2);
//calculate x and y coordinate on line (extends to infinity) closest to ball
let closestX = x1 + (dot * (x2 - x1));
let closestY = y1 + (dot * (y2 - y1));
//if those coordinates are not currently on our line, return false
if (!onLine(x1, y1, x2, y2, closestX, closestY)) {
return false;
}
//calculate distance from closest coordinates to ball
distanceX = closestX - ball.x;
distanceY = closestY - ball.y;
let distance = Math.sqrt((distanceX * distanceX) + (distanceY * distanceY));
//if the ball is less than/equal to one radius away, it has collided
if (distance <= ball.returnRadius()) {
/*console.log("COLLISION: " + ball.x + " " + ball.y + " " + closestX + " " + closestY + " " +
"\n " + distance + "\n " + currentPolyPointsX + "\n " + currentPolyPointsY);*/
return true;
} else {
return false;
}
}
}
function onLine(x1, y1, x2, y2, px, py) {
let length = distanceCalc(x1, y1, x2, y2);
let distance1 = distanceCalc(px, py, x1, y1);
let distance2 = distanceCalc(px, py, x2, y2);
let buffer = 1;
return (distance1 + distance2 >= length - buffer && distance1 + distance2 <= length + buffer);
}
let rotationAmount = 0;
function rotationLoop() {
if (rotationAmount < 360) {
rotationAmount += parseInt(rotationIncrement);
}
if (rotationAmount >= 360) {
rotationAmount = 0;
}
rotatePolygon(rotationAmount);
}
function drawPolygon() { //x&y are positions, side is side number, r is size, color is to fill
//get current values of canvas center
var x = canvas.width / 2;
var y = canvas.height / 2;
var r = canvas.width / 2;
//draw {sides} sided polygon
ctx.beginPath();
ctx.moveTo(x + r * Math.cos(0), y + r * Math.sin(0));
//clear currently stored points
currentPolyPointsX.length = 0;
currentPolyPointsY.length = 0;
//draw and save new polygon points in array
for (var i = 1; i <= sides; i++) {
ctx.lineTo(x + r * Math.cos(i * 2 * Math.PI / sides), y + r * Math.sin(i * 2 * Math.PI / sides));
currentPolyPointsX.push(x + r * Math.cos(i * 2 * Math.PI / sides));
currentPolyPointsY.push(y + r * Math.sin(i * 2 * Math.PI / sides));
}
//draw the polygon
ctx.strokeStyle = "black";
ctx.lineWidth = 2;
ctx.stroke();
ctx.save();
//console.log("drawn " + x + " " + y + " " + sides + " " + r);
}
function rotatePolygon(x) {
let centerX = canvas.height / 2;
let centerY = canvas.height / 2;
let angle = x * Math.PI / 180
//clear canvas
ctx.clearRect(0, 0, canvas.width, canvas.height);
//move rotation point to center of canvas
ctx.translate(centerX, centerY);
//rotate around center of canvas
ctx.rotate(angle);
//move back to origin
ctx.translate(-centerX, -centerY);
//draw rotated polygon
drawPolygon();
//adjust coordinates to match rotation
let cos = Math.cos(x * Math.PI / 180);
let sin = Math.sin(x * Math.PI / 180);
for (let i = 0; i < currentPolyPointsX.length; i++) {
currentPolyPointsX[i] = ((cos * (currentPolyPointsX[i] - centerX)) -
(sin * (currentPolyPointsY[i] - centerY)) + centerX);
currentPolyPointsY[i] = ((cos * (currentPolyPointsY[i] - centerY)) +
(sin * (currentPolyPointsX[i] - centerX)) + centerY);
}
/*console.log("X values: " + currentPolyPointsX + "\n" +
"Y values: " + currentPolyPointsY);*/
//un-rotate
ctx.translate(canvas.width / 2, canvas.height / 2);
ctx.rotate(-x * Math.PI / 180);
ctx.translate(-canvas.width / 2, -canvas.width / 2);
//console.log("rotated " + x);
}
function resizeCanvas() {
//gather window dimensions
var height = window.innerHeight;
var width = window.innerWidth;
//if dimensions are portrait, resize canvas based on height
if (height < width) {
var canvasSquared = window.innerHeight * .8 + "px";
} else { //if dimensions are landscape/square, resize canvas based on width
var canvasSquared = window.innerWidth * .8 + "px";
}
//commit canvas dimensions
canvas.height = parseFloat(canvasSquared);
canvas.style.height = canvasSquared;
canvas.width = parseFloat(canvasSquared);
canvas.style.width = canvasSquared;
//draw new hexagon of appropriate size
ctx.clearRect(0, 0, canvas.width, canvas.height);
drawPolygon();
console.log("resized: " + canvasSquared + " width : " + canvas.width + " height: " + canvas.height);
}
rotationSlider.oninput = () => rotationIncrement = rotationSlider.value;
sidesSlider.oninput = () => sides = sidesSlider.value;
window.onresize = resizeCanvas;
body {
display: flex;
flex-flow: column nowrap;
justify-content: center;
align-items: center;
}
.mainCanvas {
width: 600px;
height: 600px;
border: 1px solid black;
box-shadow: 0em 0em 0.5em grey;
}
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Gravity Synth</title>
<meta name="description" content="Sounds Go Bonk">
<meta name="author" content="D.A.">
<meta property="og:title" content="Sounds Go Bonk">
<meta property="og:type" content="website">
<meta property="og:description" content="Sounds Go Bonk">
<!--
<link rel="icon" href="/favicon.ico">
<link rel="icon" href="/favicon.svg" type="image/svg+xml">
<link rel="apple-touch-icon" href="/apple-touch-icon.png">
-->
<link rel="stylesheet" href="styles.css">
</head>
<body onload="startup()">
<canvas id="mainCanvas" class="mainCanvas"></canvas>
<div>Speed</div>
<input type="range" min=0 max=5 value=1 class="slider" id="rotationSlider">
<div>Sides</div>
<input type="range" min=3 max=9 value=5 class="slider" id="sidesSlider">
<script src="scripts.js"></script>
</body>
</html>

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;
...

Using a line to divide a canvas into two new canvases

I'm looking to allow users to slice an existing canvas into two canvases in whatever direction they would like.
I know how to allow the user to draw a line and I also know how to copy the image data of one canvas onto two new ones, but how can I copy only the relevant color data on either side of the user-drawn line to its respective canvas?
For example, in the following demo I'd like the canvas to be "cut" where the white line is:
const canvas = document.querySelector("canvas"),
ctx = canvas.getContext("2d");
const red = "rgb(104, 0, 0)",
lb = "rgb(126, 139, 185)",
db = "rgb(20, 64, 87)";
var width,
height,
centerX,
centerY,
smallerDimen;
var canvasData,
inCoords;
function sizeCanvas() {
width = canvas.width = window.innerWidth;
height = canvas.height = window.innerHeight;
centerX = width / 2;
centerY = height / 2;
smallerDimen = Math.min(width, height);
}
function drawNormalState() {
// Color the bg
ctx.fillStyle = db;
ctx.fillRect(0, 0, width, height);
// Color the circle
ctx.arc(centerX, centerY, smallerDimen / 4, 0, Math.PI * 2, true);
ctx.fillStyle = red;
ctx.fill();
ctx.lineWidth = 3;
ctx.strokeStyle = lb;
ctx.stroke();
// Color the triangle
ctx.beginPath();
ctx.moveTo(centerX + smallerDimen / 17, centerY - smallerDimen / 10);
ctx.lineTo(centerX + smallerDimen / 17, centerY + smallerDimen / 10);
ctx.lineTo(centerX - smallerDimen / 9, centerY);
ctx.fillStyle = lb;
ctx.fill();
ctx.closePath();
screenshot();
ctx.beginPath();
ctx.strokeStyle = "rgb(255, 255, 255)";
ctx.moveTo(width - 20, 0);
ctx.lineTo(20, height);
ctx.stroke();
ctx.closePath();
}
function screenshot() {
canvasData = ctx.getImageData(0, 0, width, height).data;
}
function init() {
sizeCanvas();
drawNormalState();
}
init();
body {
margin: 0;
}
<canvas></canvas>
TL;DR the demo.
The best way I've found to do this is to 1) calculate "end points" for the line at the edge of (or outside) the canvas' bounds, 2) create two* polygons using the end points of the line generated in step 1 and the canvas' four corners, and 3) divide up the original canvas' image data into two new canvases based on the polygons we create.
* We actually create one, but the "second" is the remaining part of the original canvas.
1) Calculate the end points
You can use a very cheap algorithm to calculate some end points given a start coordinate, x and y difference (i.e. slope), and the bounds for the canvas. I used the following:
function getEndPoints(startX, startY, xDiff, yDiff, maxX, maxY) {
let currX = startX,
currY = startY;
while(currX > 0 && currY > 0 && currX < maxX && currY < maxY) {
currX += xDiff;
currY += yDiff;
}
let points = {
firstPoint: [currX, currY]
};
currX = startX;
currY = startY;
while(currX > 0 && currY > 0 && currX < maxX && currY < maxY) {
currX -= xDiff;
currY -= yDiff;
}
points.secondPoint = [currX, currY];
return points;
}
where
let xDiff = firstPoint.x - secondPoint.x,
yDiff = firstPoint.y - secondPoint.y;
2) Create two polygons
To create the polygons, I make use of Paul Bourke's Javascript line intersection:
function intersect(point1, point2, point3, point4) {
let x1 = point1[0],
y1 = point1[1],
x2 = point2[0],
y2 = point2[1],
x3 = point3[0],
y3 = point3[1],
x4 = point4[0],
y4 = point4[1];
// Check if none of the lines are of length 0
if((x1 === x2 && y1 === y2) || (x3 === x4 && y3 === y4)) {
return false;
}
let denominator = ((y4 - y3) * (x2 - x1) - (x4 - x3) * (y2 - y1));
// Lines are parallel
if(denominator === 0) {
return false;;
}
let ua = ((x4 - x3) * (y1 - y3) - (y4 - y3) * (x1 - x3)) / denominator;
let ub = ((x2 - x1) * (y1 - y3) - (y2 - y1) * (x1 - x3)) / denominator;
// is the intersection along the segments
if(ua < 0 || ua > 1 || ub < 0 || ub > 1) {
return false;
}
// Return a object with the x and y coordinates of the intersection
let x = x1 + ua * (x2 - x1);
let y = y1 + ua * (y2 - y1);
return [x, y];
}
Along with some of my own logic:
let origin = [0, 0],
xBound = [width, 0],
xyBound = [width, height],
yBound = [0, height];
let polygon = [origin];
// Work clockwise from 0,0, adding points to our polygon as appropriate
// Check intersect with top bound
let topIntersect = intersect(origin, xBound, points.firstPoint, points.secondPoint);
if(topIntersect) {
polygon.push(topIntersect);
}
if(!topIntersect) {
polygon.push(xBound);
}
// Check intersect with right
let rightIntersect = intersect(xBound, xyBound, points.firstPoint, points.secondPoint);
if(rightIntersect) {
polygon.push(rightIntersect);
}
if((!topIntersect && !rightIntersect)
|| (topIntersect && rightIntersect)) {
polygon.push(xyBound);
}
// Check intersect with bottom
let bottomIntersect = intersect(xyBound, yBound, points.firstPoint, points.secondPoint);
if(bottomIntersect) {
polygon.push(bottomIntersect);
}
if((topIntersect && bottomIntersect)
|| (topIntersect && rightIntersect)) {
polygon.push(yBound);
}
// Check intersect with left
let leftIntersect = intersect(yBound, origin, points.firstPoint, points.secondPoint);
if(leftIntersect) {
polygon.push(leftIntersect);
}
3) Divide up the original canvas' image data
Now that we have our polygon, all that's left is putting this data into new canvases. The easiest way to do this is to use canvas' ctx.drawImage and ctx.globalCompositeOperation.
// Use or create 2 new canvases with the split original canvas
let newCanvas1 = document.querySelector("#newCanvas1");
if(newCanvas1 == null) {
newCanvas1 = document.createElement("canvas");
newCanvas1.id = "newCanvas1";
newCanvas1.width = width;
newCanvas1.height = height;
document.body.appendChild(newCanvas1);
}
let newCtx1 = newCanvas1.getContext("2d");
newCtx1.globalCompositeOperation = 'source-over';
newCtx1.drawImage(canvas, 0, 0);
newCtx1.globalCompositeOperation = 'destination-in';
newCtx1.beginPath();
newCtx1.moveTo(polygon[0][0], polygon[0][1]);
for(let item = 1; item < polygon.length; item++) {
newCtx1.lineTo(polygon[item][0], polygon[item][1]);
}
newCtx1.closePath();
newCtx1.fill();
let newCanvas2 = document.querySelector("#newCanvas2");
if(newCanvas2 == null) {
newCanvas2 = document.createElement("canvas");
newCanvas2.id = "newCanvas2";
newCanvas2.width = width;
newCanvas2.height = height;
document.body.appendChild(newCanvas2);
}
let newCtx2 = newCanvas2.getContext("2d");
newCtx2.globalCompositeOperation = 'source-over';
newCtx2.drawImage(canvas, 0, 0);
newCtx2.globalCompositeOperation = 'destination-out';
newCtx2.beginPath();
newCtx2.moveTo(polygon[0][0], polygon[0][1]);
for(let item = 1; item < polygon.length; item++) {
newCtx2.lineTo(polygon[item][0], polygon[item][1]);
}
newCtx2.closePath();
newCtx2.fill();
All of that put together gives us this demo!

Delaying the start of an animation on canvas

With the help of this forum I wrote some code to make an animation of a ball that goes into a basket.
My idea is that the user has to write the result of a mathematical operation within a time limit. If he gets it right, the ball goes into the basket.
The only problem is that I can't find out how to delay the start of the animation. My goal is to have a fixed canvas with a ball and a basket, and then at the end of the time limit I want the ball to start moving. I've tried to use a sleep() function that I got from the internet, but soon I learnt that it gave me more trouble than anything else. Here is a part of my code:
const tabx = 800, taby = 100; //backboard position
var canvas, ctx, i=0;
function init(z) { //z is the state of the function, at the start is 0 then goes to 1
canvas = document.getElementById("mycanvas");
ctx = canvas.getContext("2d");
//backboard
ctx.beginPath();
ctx.strokeStyle = "black";
ctx.rect(tabx, taby, 180, 100);
ctx.stroke();
//basket
ctx.scale(1, 0.5);
ctx.beginPath();
ctx.arc(890, 320, 35, 0, 2 * Math.PI, false);
ctx.stroke();
ctx.moveTo(855, 320);
ctx.lineTo(865, 445);
ctx.stroke();
ctx.moveTo(925, 320);
ctx.lineTo(915, 445);
ctx.stroke();
ctx.scale(1, 0.15);
ctx.beginPath();
ctx.arc(890, 2980, 25, 0, 2 * Math.PI, false);
ctx.stroke();
ctx.setTransform(1, 0, 0, 1, 0, 0); //setting transformation to default
if (z == 0){
start();
}
}
function start() {
var x1, y1, a, b, c, denom,x3,y3;
//generating ball position
x1 = (Math.random() * 500) + 100;
y1 = (Math.random() * 200) + 300;
canvas = document.getElementById("mycanvas");
ctx = canvas.getContext("2d");
//setting the end point of the parabola
x3 = tabx + 90;
y3 = taby + 90;
//setting the medium point of the parabola
x2 = (x1 + (x3+90))/ 2;
y2 = ((y1 + (y3+220)) / 2) - 300;
//calculating the equation of the parabola
denom = (x1 - x2) * (x1 - x3) * (x2 - x3);
a = (x3 * (y2 - y1) + x2 * (y1 - y3) + x1 * (y3 - y2)) / denom;
b = (x3 * x3 * (y1 - y2) + x2 * x2 * (y3 - y1) + x1 * x1 * (y2 - y3)) / denom;
c = (x2 * x3 * (x2 - x3) * y1 + x3 * x1 * (x3 - x1) * y2 + x1 * x2 * (x1 - x2) * y3) / denom;
drawball(a, b, c, x1, y1,x3);
}
function drawball(a, b, c, x1, y1, x3){
var ris;
canvas = document.getElementById("mycanvas");
ctx = canvas.getContext("2d");
//ball drawing
ctx.beginPath();
ctx.arc(x1, y1, 25, 0, 2 * Math.PI);
ctx.stroke();
if(i==1){
sleep(3000);
ris=genmat();
}
i++;
if (x1 < x3) {
window.requestAnimationFrame(function() {
ctx.clearRect(0, 0, 1000, 600);
init(1); //calling the init function to redraw everything
y1 = a * (x1 * x1) + b * x1 + c;
x1 += 10;
//drawing the next ball
drawpalla(a, b, c, x1, y1,x3)
});
}
}
function sleep(milliseconds) {
var start = new Date().getTime();
for (var i = 0; i < 1e7; i++) {
if ((new Date().getTime() - start) > milliseconds){
break;
}
}
}
//function that generates the operation
function genmat(){
var n1,n2,op,ris;
n1=Math.trunc(Math.random()*10)
n2=Math.trunc(Math.random()*10)
op=Math.trunc((Math.random()*(4-1))+1)
if(op==1){
op="+";
ris=n1+n2;
}
if(op==2){
op="-";
ris=n1-n2;
}
if(op==3){
op="*";
ris=n1*n2;
}
if(op==4){
op="/";
ris=n1/n2;
}
document.getElementById("mate").innerHTML=n1+op+n2;
return ris;
}
Thank you for your help.

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);

Categories