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>
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 using Konva library to draw some stuff on HTML5 canvas.
I have given 2 points from user interaction by mouse click:
var A={x:'',y:''};
var B={x:'',y:''};
1) How to draw line line this?
My question is:
1) How to get perpendicular lines on each interval?
2) How to get distance from A to B point?
3) How to get all points on line from A to B?
4) How to get red points?
You have not explained what your line is so I am assuming it is a sin wave (though the image looks like circles stuck together???)
As MBo has given the basics this is just applying it to the wavy line.
// normalize a vector
function normalize(vec){
var length = Math.sqrt(vec.x * vec.x + vec.y * vec.y);
vec.x /= length;
vec.y /= length;
return vec;
}
// creates a wavy line
function wavyLine(start, end, waves, amplitude){
return ({
start,
end,
waves,
amplitude,
update(){
if(this.vec === undefined){
this.vec = {};
this.norm = {};
}
this.vec.x = this.end.x - this.start.x;
this.vec.y = this.end.y - this.start.y;
this.length = Math.sqrt(this.vec.x * this.vec.x + this.vec.y * this.vec.y);
this.norm.x = this.vec.x / this.length;
this.norm.y = this.vec.y / this.length;
return this;
}
}).update();
}
// draws a wavy line
function drawWavyLine(line) {
var x, stepSize, i, y, phase, dist;
ctx.beginPath();
stepSize = ctx.lineWidth;
ctx.moveTo(line.start.x, line.start.y);
for (i = stepSize; i < line.length; i+= stepSize) {
x = line.start.x + line.norm.x * i; // get point i pixels from start
y = line.start.y + line.norm.y * i; // get point i pixels from start
phase = (i / (line.length / line.waves)) * Math.PI * 2; // get the wave phase at this point
dist = Math.sin(phase) * line.amplitude; // get the distance from the line to the point on the wavy curve
x -= line.norm.y * dist;
y += line.norm.x * dist;
ctx.lineTo(x, y);
}
phase = line.waves * Math.PI * 2; // get the wave phase at this point
dist = Math.sin(phase) * line.amplitude; // get the distance from the line to the point on the wavy curve
ctx.lineTo(line.end.x - line.norm.y * dist, line.end.y + line.norm.x * dist);
ctx.stroke();
}
// find the closest point on a wavy line to a point returns the pos on the wave, tangent and point on the linear line
function closestPointOnLine(point,line){
var x = point.x - line.start.x;
var y = point.y - line.start.y;
// get the amount the line vec needs to be scaled so tat point is perpendicular to the line
var l = (line.vec.x * x + line.vec.y * y) / (line.length * line.length);
x = line.vec.x * l; // scale the vec
y = line.vec.y * l;
return pointAtDistance(Math.sqrt(x * x + y * y), line);
}
// find the point at (linear) distance along wavy line and return coordinate, coordinate on wave, and tangent
function pointAtDistance(distance,line){
var lenScale = line.length / line.waves; // scales the length into radians
var phase = distance * Math.PI * 2 / lenScale; // get the wave phase at this point
var dist = Math.sin(phase) * line.amplitude; // get the distance from the line to the point on the wavy curve
var slope = Math.cos(phase ) * Math.PI * 2 * line.amplitude / lenScale; // derivitive of sin(a*x) is -a*cos(a*x)
// transform tangent (slope) into a vector along the line. This vector is not a unit vector so normalize it
var tangent = normalize({
x : line.norm.x - line.norm.y * slope,
y : line.norm.y + line.norm.x * slope
});
// move from the line start to the point on the linear line at distance
var linear = {
x : line.start.x + line.norm.x * distance,
y : line.start.y + line.norm.y * distance
}
// move out perpendicular to the wavy part
return {
x : linear.x - line.norm.y * dist,
y : linear.y + line.norm.x * dist,
tangent,linear
};
}
// create a wavy line
var wLine = wavyLine({x:10,y:100},{x:300,y:100},3,50);
// draw the wavy line and show some points on it
function display(timer){
globalTime = timer;
ctx.setTransform(1,0,0,1,0,0); // reset transform
ctx.globalAlpha = 1; // reset alpha
ctx.clearRect(0,0,w,h);
var radius = Math.max(ch,cw);
// set up the wavy line
wLine.waves = Math.sin(timer / 10000) * 6;
wLine.start.x = Math.cos(timer / 50000) * radius + cw;
wLine.start.y = Math.sin(timer / 50000) * radius + ch;
wLine.end.x = -Math.cos(timer / 50000) * radius + cw;
wLine.end.y = -Math.sin(timer / 50000) * radius + ch ;
wLine.update();
// draw the linear line
ctx.lineWidth = 0.5;
ctx.strokeStyle = "blue";
ctx.beginPath();
ctx.moveTo(wLine.start.x, wLine.start.y);
ctx.lineTo(wLine.end.x, wLine.end.y);
ctx.stroke();
// draw the wavy line
ctx.lineWidth = 2;
ctx.strokeStyle = "black";
drawWavyLine(wLine);
// find point nearest mouse
var p = closestPointOnLine(mouse,wLine);
ctx.lineWidth = 1;
ctx.strokeStyle = "red";
ctx.beginPath();
ctx.arc(p.x,p.y,5,0,Math.PI * 2);
ctx.moveTo(p.x + p.tangent.x * 20,p.y + p.tangent.y * 20);
ctx.lineTo(p.x - p.tangent.y * 10,p.y + p.tangent.x * 10);
ctx.lineTo(p.x + p.tangent.y * 10,p.y - p.tangent.x * 10);
ctx.closePath();
ctx.stroke();
// find points at equal distance along line
ctx.lineWidth = 1;
ctx.strokeStyle = "blue";
ctx.beginPath();
for(var i = 0; i < w; i += w / 10){
var p = pointAtDistance(i,wLine);
ctx.moveTo(p.x + 5,p.y);
ctx.arc(p.x,p.y,5,0,Math.PI * 2);
ctx.moveTo(p.x,p.y);
ctx.lineTo(p.linear.x,p.linear.y);
ctx.moveTo(p.x + p.tangent.x * 40, p.y + p.tangent.y * 40);
ctx.lineTo(p.x - p.tangent.x * 40, p.y - p.tangent.y * 40);
}
ctx.stroke();
}
/******************************************************************************
The code from here down is generic full page mouse and canvas boiler plate
code. As I do many examples which all require the same mouse and canvas
functionality I have created this code to keep a consistent interface. The
Code may or may not be part of the answer.
This code may or may not have ES6 only sections so will require a transpiler
such as babel.js to run on legacy browsers.
*****************************************************************************/
// V2.0 ES6 version for Stackoverflow and Groover QuickRun
var w, h, cw, ch, canvas, ctx, mouse, globalTime = 0;
// You can declare onResize (Note the capital R) as a callback that is also
// called once at start up. Warning on first call canvas may not be at full
// size.
;(function(){
const RESIZE_DEBOUNCE_TIME = 100;
var resizeTimeoutHandle;
var firstRun = true;
function createCanvas () {
var c,cs;
cs = (c = document.createElement("canvas")).style;
cs.position = "absolute";
cs.top = cs.left = "0px";
cs.zIndex = 1000;
document.body.appendChild(c);
return c;
}
function resizeCanvas () {
if (canvas === undefined) { canvas = createCanvas() }
canvas.width = innerWidth;
canvas.height = innerHeight;
ctx = canvas.getContext("2d");
if (typeof setGlobals === "function") { setGlobals() }
if (typeof onResize === "function") {
clearTimeout(resizeTimeoutHandle);
if (firstRun) { onResize() }
else { resizeTimeoutHandle = setTimeout(onResize, RESIZE_DEBOUNCE_TIME) }
firstRun = false;
}
}
function setGlobals () {
cw = (w = canvas.width) / 2;
ch = (h = canvas.height) / 2;
}
mouse = (function () {
var m; // alias for mouse
var mouse = {
x : 0, y : 0, // mouse position and wheel
buttonRaw : 0,
buttonOnMasks : [0b1, 0b10, 0b100], // mouse button on masks
buttonOffMasks : [0b110, 0b101, 0b011], // mouse button off masks
bounds : null,
eventNames : "mousemove,mousedown,mouseup".split(","),
event(e) {
var t = e.type;
m.bounds = m.element.getBoundingClientRect();
m.x = e.pageX - m.bounds.left - scrollX;
m.y = e.pageY - m.bounds.top - scrollY;
if (t === "mousedown") { m.buttonRaw |= m.buttonOnMasks[e.which - 1] }
else if (t === "mouseup") { m.buttonRaw &= m.buttonOffMasks[e.which - 1] }
},
start(element) {
m.element = element === undefined ? document : element;
m.eventNames.forEach(name => document.addEventListener(name, mouse.event) );
},
}
m = mouse;
return mouse;
})();
function update(timer) { // Main update loop
globalTime = timer;
display(timer); // call demo code
requestAnimationFrame(update);
}
setTimeout(function(){
canvas = createCanvas();
mouse.start(canvas);
resizeCanvas();
window.addEventListener("resize", resizeCanvas);
requestAnimationFrame(update);
},0);
})();
We have points A and B. Difference vector
D.X = B.X - A.X
D.Y = B.Y - A.Y
Length = Sqrt(D.X * D.X + D.Y * D.Y)
normalized (unit) vector
uD.X = D.X / Length
uD.Y = D.Y / Length
perpendicular unit vector
P.X = - uD.Y
P.Y = uD.X
some red point:
R.X = A.X + uD.X * Dist + P.X * SideDist * SideSign
R.Y = A.Y + uD.Y * Dist + P.Y * SideDist * SideSign
where Dist is in range 0..Length
Dist = i / N * Length for N equidistant points
SideSign is +/- 1 for left and right side
I want to simulate a box of random moving balls.
I have created a canvas with just 1 moving ball.
I want to replicate the ball into other balls so that they could start from a specific direction and move in random directions independently.
The code below is used to create the canvas of 1 moving ball.
Can you guys help me to replicate the balls and have them move in random directions?
I am using Javascript, HTML5 and PHP.emphasized text
Thank you.
<html>
<head>
<style>
canvas { margin:0 20px 10px 200px; width:700; height:400; border:solid #CC0000; border-radius:15px;}
</style>
<canvas id="move"></canvas>
<script type="text/javascript">
var canvas = document.getElementById('move');
canvas.width = window.innerWidth;
canvas.height = window.innerHeight;
var x = canvas.width / 2 + 100; //starting position
var y = canvas.height / 2 + 100;
var ball = canvas.getContext('2d');
ball.fillStyle = '#FF0000'; //color
var radius = 30;
var dx = 10;
var dy = 10;
var delta = 55; // range (from 0) of possible dx or dy change
var max = 215; // maximum dx or dy values
canvas.addEventListener("click", togglestart);
function togglestart() {
if (interval == undefined) interval = window.setInterval(animate, 10000 / 60); // 60 FPS
else {
interval = clearInterval(interval);
console.log(interval);
}
}
var interval = window.setInterval(animate, 1000 / 60);
function animate() {
var d2x = (Math.random() * delta - delta / 2); //change dx and dy by random value
var d2y = (Math.random() * delta - delta / 2);
if (Math.abs(d2x + dx) > max) // start slowing down if going too fast
d2x *= -1;
if (Math.abs(d2y + dy) > max) d2y *= -1;
dx += d2x;
dy += d2y;
if ((x + dx) < 0 || (x + dx) > canvas.width) // bounce off walls
dx *= -1;
if ((y + dy) < 0 || (y + dy) > canvas.height) dy *= -1;
x += dx;
y += dy;
ball.beginPath(); //drawing ball
ball.arc(x, y, radius, 0, 2 * Math.PI, false);
ball.clearRect(0, 0, canvas.width, canvas.height); // CLS clear the canvas
ball.fill();
}
</script>
</head>
</html>
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};
}
Being relatively new to the HTML5 game, just wanted to ask if anyone knew if it was possible to animate a dashed line along a path? Think snake from Nokia days, just with a dashed line...
I've got a dashed line (which represents electrical current flow), which I'd like to animate as 'moving' to show that current is flowing to something.
Thanks to Rod's answer on this post, I've got the dashed line going, but am not sure where to start to get it moving. Anyone know where to begin?
Thanks!
Got it working here, based on this post by phrogz.
What i did:
Add a start parameter which is a number between 0 and 99
Calculate the dashSize summing the contents of the dash array
Calculate dashOffSet as a fraction of dashSize based on start percent
Subtracted the offset from x, y and added to dx, dy
Only started drawying after the offset been gone (it´s negative, remember)
Added a setInterval to update the start from 0 to 99, step of 10
Update
The original algorithm wasn't working for vertical or negative inclined lines. Added a check to use the inclination based on the y slope on those cases, and not on the x slope.
Demo here
Updated code:
if (window.CanvasRenderingContext2D && CanvasRenderingContext2D.prototype.lineTo) {
CanvasRenderingContext2D.prototype.dashedLine = function(x, y, x2, y2, dashArray, start) {
if (!dashArray) dashArray = [10, 5];
var dashCount = dashArray.length;
var dashSize = 0;
for (i = 0; i < dashCount; i++) dashSize += parseInt(dashArray[i]);
var dx = (x2 - x),
dy = (y2 - y);
var slopex = (dy < dx);
var slope = (slopex) ? dy / dx : dx / dy;
var dashOffSet = dashSize * (1 - (start / 100))
if (slopex) {
var xOffsetStep = Math.sqrt(dashOffSet * dashOffSet / (1 + slope * slope));
x -= xOffsetStep;
dx += xOffsetStep;
y -= slope * xOffsetStep;
dy += slope * xOffsetStep;
} else {
var yOffsetStep = Math.sqrt(dashOffSet * dashOffSet / (1 + slope * slope));
y -= yOffsetStep;
dy += yOffsetStep;
x -= slope * yOffsetStep;
dx += slope * yOffsetStep;
}
this.moveTo(x, y);
var distRemaining = Math.sqrt(dx * dx + dy * dy);
var dashIndex = 0,
draw = true;
while (distRemaining >= 0.1 && dashIndex < 10000) {
var dashLength = dashArray[dashIndex++ % dashCount];
if (dashLength > distRemaining) dashLength = distRemaining;
if (slopex) {
var xStep = Math.sqrt(dashLength * dashLength / (1 + slope * slope));
x += xStep
y += slope * xStep;
} else {
var yStep = Math.sqrt(dashLength * dashLength / (1 + slope * slope));
y += yStep
x += slope * yStep;
}
if (dashOffSet > 0) {
dashOffSet -= dashLength;
this.moveTo(x, y);
} else {
this[draw ? 'lineTo' : 'moveTo'](x, y);
}
distRemaining -= dashLength;
draw = !draw;
}
// Ensure that the last segment is closed for proper stroking
this.moveTo(0, 0);
}
}
var dashes = '10 20 2 20'
var c = document.getElementsByTagName('canvas')[0];
c.width = 300;
c.height = 400;
var ctx = c.getContext('2d');
ctx.strokeStyle = 'black';
var drawDashes = function() {
ctx.clearRect(0, 0, c.width, c.height);
var dashGapArray = dashes.replace(/^\s+|\s+$/g, '').split(/\s+/);
if (!dashGapArray[0] || (dashGapArray.length == 1 && dashGapArray[0] == 0)) return;
ctx.lineWidth = 4;
ctx.lineCap = 'round';
ctx.beginPath();
ctx.dashedLine(10, 0, 10, c.height, dashGapArray, currentOffset);
ctx.dashedLine(0, 10, c.width, 10, dashGapArray, currentOffset);
ctx.dashedLine(0, 0, c.width, c.height, dashGapArray, currentOffset);
ctx.dashedLine(0, c.height, c.width, 0, dashGapArray, currentOffset);
ctx.closePath();
ctx.stroke();
};
window.setInterval(dashInterval, 500);
var currentOffset = 0;
function dashInterval() {
drawDashes();
currentOffset += 10;
if (currentOffset >= 100) currentOffset = 0;
}
You can create the dashed line animation using SNAPSVG library.
Please check the tutorial here DEMO