How to move object to target naturally and smoothly? - javascript

Can somebody fix it script to make it works properly?
What I expects:
Run script
Click at the canvas to set target (circle)
Object (triangle) starts to rotate and move towards to target (circle)
Change target at any time
How it works:
Sometimes object rotates correctly, sometimes isn't
Looks like one half sphere works well, another isn't
Thanks!
// prepare 2d context
const c = window.document.body.appendChild(window.document.createElement('canvas'))
.getContext('2d');
c.canvas.addEventListener('click', e => tgt = { x: e.offsetX, y: e.offsetY });
rate = 75 // updates delay
w = c.canvas.width;
h = c.canvas.height;
pi2 = Math.PI * 2;
// object that moves towards the target
obj = {
x: 20,
y: 20,
a: 0, // angle
};
// target
tgt = undefined;
// main loop
setInterval(() => {
c.fillStyle = 'black';
c.fillRect(0, 0, w, h);
// update object state
if (tgt) {
// draw target
c.beginPath();
c.arc(tgt.x, tgt.y, 2, 0, pi2);
c.closePath();
c.strokeStyle = 'red';
c.stroke();
// update object position
// vector from obj to tgt
dx = tgt.x - obj.x;
dy = tgt.y - obj.y;
// normalize
l = Math.sqrt(dx*dx + dy*dy);
dnx = (dx / l);// * 0.2;
dny = (dy / l);// * 0.2;
// update object position
obj.x += dnx;
obj.y += dny;
// angle between +x and tgt
a = Math.atan2(0 * dx - 1 * dy, 1 * dx + 0 * dy);
// update object angle
obj.a += -a * 0.04;
}
// draw object
c.translate(obj.x, obj.y);
c.rotate(obj.a);
c.beginPath();
c.moveTo(5, 0);
c.lineTo(-5, 4);
c.lineTo(-5, -4);
//c.lineTo(3, 0);
c.closePath();
c.strokeStyle = 'red';
c.stroke();
c.rotate(-obj.a);
c.translate(-obj.x, -obj.y);
}, rate);

This turned out to be a bit more challenging than I first thought and I ended up just re-writing the code.
The challenges:
Ensure the ship only rotated to the exact point of target. This required me to compare the two angle from the ship current position to where we want it to go.
Ensure the target did not rotate past the target and the ship did not translate past the target. This required some buffer space for each because when animating having this.x === this.x when an object is moving is very rare to happen so we need some room for the logic to work.
Ensure the ship turned in the shortest direction to the target.
I have added notes in the code to better explain. Hopefully you can implement this into yours or use it as is. Oh and you can change the movement speed and rotation speed as you see fit.
let canvas = document.getElementById("canvas");
let ctx = canvas.getContext("2d");
canvas.width = 400;
canvas.height = 400;
let mouse = { x: 20, y: 20 };
let canvasBounds = canvas.getBoundingClientRect();
let target;
canvas.addEventListener("mousedown", (e) => {
mouse.x = e.x - canvasBounds.x;
mouse.y = e.y - canvasBounds.y;
target = new Target();
});
class Ship {
constructor() {
this.x = 20;
this.y = 20;
this.ptA = { x: 15, y: 0 };
this.ptB = { x: -15, y: 10 };
this.ptC = { x: -15, y: -10 };
this.color = "red";
this.angle1 = 0;
this.angle2 = 0;
this.dir = 1;
}
draw() {
ctx.save();
//use translate to move the ship
ctx.translate(this.x, this.y);
//angle1 is the angle from the ship to the target point
//angle2 is the ships current rotation angle. Once they equal each other then the rotation stops. When you click somewhere else they are no longer equal and the ship will rotate again.
if (!this.direction(this.angle1, this.angle2)) {
//see direction() method for more info on this
if (this.dir == 1) {
this.angle2 += 0.05; //change rotation speed here
} else if (this.dir == 0) {
this.angle2 -= 0.05; //change rotation speed here
}
} else {
this.angle2 = this.angle1;
}
ctx.rotate(this.angle2);
ctx.beginPath();
ctx.strokeStyle = this.color;
ctx.moveTo(this.ptA.x, this.ptA.y);
ctx.lineTo(this.ptB.x, this.ptB.y);
ctx.lineTo(this.ptC.x, this.ptC.y);
ctx.closePath();
ctx.stroke();
ctx.restore();
}
driveToTarget() {
//get angle to mouse click
this.angle1 = Math.atan2(mouse.y - this.y, mouse.x - this.x);
//normalize vector
let vecX = mouse.x - this.x;
let vecY = mouse.y - this.y;
let dist = Math.hypot(vecX, vecY);
vecX /= dist;
vecY /= dist;
//Prevent continuous x and y increment by checking if either vec == 0
if (vecX != 0 || vecY != 0) {
//then also give the ship a little buffer incase it passes the given point it doesn't turn back around. This allows time for it to stop if you increase the speed.
if (
this.x >= mouse.x + 3 ||
this.x <= mouse.x - 3 ||
this.y >= mouse.y + 3 ||
this.y <= mouse.y - 3
) {
this.x += vecX; //multiple VecX by n to increase speed (vecX*2)
this.y += vecY; //multiple VecY by n to increase speed (vecY*2)
}
}
}
direction(ang1, ang2) {
//converts rads to degrees and ensures we get numbers from 0-359
let a1 = ang1 * (180 / Math.PI);
if (a1 < 0) {
a1 += 360;
}
let a2 = ang2 * (180 / Math.PI);
if (a2 < 0) {
a2 += 360;
}
//checks whether the target is on the right or left side of the ship.
//We use then to ensure it turns in the shortest direction
if ((360 + a1 - a2) % 360 > 180) {
this.dir = 0;
} else {
this.dir = 1;
}
//Because of animation timeframes there is a chance the ship could turn past the target if rotating too fast. This gives the ship a 1 degree buffer to either side of the target to determine if it is pointed in the right direction.
//We then correct it to the exact degrees in the draw() method above once the if statment defaults to 'else'
if (
Math.trunc(a2) <= Math.trunc(a1) + 1 &&
Math.trunc(a2) >= Math.trunc(a1) - 1
) {
return true;
}
return false;
}
}
let ship = new Ship();
class Target {
constructor() {
this.x = mouse.x;
this.y = mouse.y;
this.r = 3;
this.color = "red";
}
draw() {
ctx.strokeStyle = this.color;
ctx.beginPath();
ctx.arc(this.x, this.y, this.r, 0, Math.PI * 2, false);
ctx.stroke();
}
}
function animate() {
ctx.clearRect(0, 0, canvas.width, canvas.height);
ctx.fillRect(0, 0, canvas.width, canvas.height);
ship.draw();
ship.driveToTarget();
if (target) {
target.draw();
}
requestAnimationFrame(animate);
}
animate();
<canvas id="canvas"></canvas>

Related

Change speed of pong ball in realtime on input (JS)

I have a number input which i want to it to function as a real-time update of the speed of the pong ball, however every method I've tried hasn't worked it all comes back as "NaN". The speed is originally set to 7 but once the ball resets and a player scores id like the ball to change to the number of the input. Thank you
// select canvas element
const canvas = document.getElementById("pong");
// getContext of canvas = methods and properties to draw and do a lot of thing to the canvas
const ctx = canvas.getContext("2d");
// get the value of the user input
var userSpeedInput = parseInt(document.getElementById("user-speed").value);
// speed btn
const speedBtn = document.getElementById("speed-btn");
// load sounds
let hit = new Audio();
let wall = new Audio();
let userScore = new Audio();
let comScore = new Audio();
hit.src = "sounds/hit.mp3";
wall.src = "sounds/wall.mp3";
comScore.src = "sounds/comScore.mp3";
userScore.src = "sounds/userScore.mp3";
// Ball object
const ball = {
x: canvas.width / 2,
y: canvas.height / 2,
radius: 10,
velocityX: 5,
velocityY: 5,
speed: userSpeedInput, // speed: userSpeedInput;
color: "WHITE",
};
// User Paddle
const user = {
x: 0, // left side of canvas
y: (canvas.height - 100) / 2, // -100 the height of paddle
width: 10,
height: 100,
score: 0,
color: "WHITE",
};
// COM Paddle
const com = {
x: canvas.width - 10, // - width of paddle
y: (canvas.height - 100) / 2, // -100 the height of paddle
width: 10,
height: 100,
score: 0,
color: "WHITE",
};
// NET
const net = {
x: (canvas.width - 2) / 2,
y: 0,
height: 10,
width: 2,
color: "WHITE",
};
// draw a rectangle, will be used to draw paddles
function drawRect(x, y, w, h, color) {
ctx.fillStyle = color;
ctx.fillRect(x, y, w, h);
}
// draw circle, will be used to draw the ball
function drawArc(x, y, r, color) {
ctx.fillStyle = color;
ctx.beginPath();
ctx.arc(x, y, r, 0, Math.PI * 2, true);
ctx.closePath();
ctx.fill();
}
// listening to the mouse
canvas.addEventListener("mousemove", getMousePos);
function getMousePos(evt) {
let rect = canvas.getBoundingClientRect();
user.y = evt.clientY - rect.top - user.height / 2;
}
// when COM or USER scores, we reset the ball
function resetBall() {
if (!ball) return;
ball.x = canvas.width / 2;
ball.y = canvas.height / 2;
ball.velocityX = -ball.velocityX;
ball.speed = userSpeedInput;
alert(ball.speed);
}
// draw the net
function drawNet() {
for (let i = 0; i <= canvas.height; i += 15) {
drawRect(net.x, net.y + i, net.width, net.height, net.color);
}
}
// draw text
function drawText(text, x, y) {
ctx.fillStyle = "#FFF";
ctx.font = "75px fantasy";
ctx.fillText(text, x, y);
}
// collision detection
function collision(b, p) {
p.top = p.y;
p.bottom = p.y + p.height;
p.left = p.x;
p.right = p.x + p.width;
b.top = b.y - b.radius;
b.bottom = b.y + b.radius;
b.left = b.x - b.radius;
b.right = b.x + b.radius;
return (
p.left < b.right && p.top < b.bottom && p.right > b.left && p.bottom > b.top
);
}
// update function, the function that does all calculations
function update() {
// change the score of players, if the ball goes to the left "ball.x<0" computer win, else if "ball.x > canvas.width" the user win
if (ball.x - ball.radius < 0) {
com.score++;
comScore.play();
resetBall();
} else if (ball.x + ball.radius > canvas.width) {
user.score++;
userScore.play();
resetBall();
}
// the ball has a velocity
ball.x += ball.velocityX;
ball.y += ball.velocityY;
// computer plays for itself, and we must be able to beat it
// simple AI
com.y += (ball.y - (com.y + com.height / 2)) * 0.1;
// when the ball collides with bottom and top walls we inverse the y velocity.
if (ball.y - ball.radius < 0 || ball.y + ball.radius > canvas.height) {
ball.velocityY = -ball.velocityY;
wall.play();
}
// we check if the paddle hit the user or the com paddle
let player = ball.x + ball.radius < canvas.width / 2 ? user : com;
// if the ball hits a paddle
if (collision(ball, player)) {
// play sound
hit.play();
// we check where the ball hits the paddle
let collidePoint = ball.y - (player.y + player.height / 2);
// normalize the value of collidePoint, we need to get numbers between -1 and 1.
// -player.height/2 < collide Point < player.height/2
collidePoint = collidePoint / (player.height / 2);
// when the ball hits the top of a paddle we want the ball, to take a -45degees angle
// when the ball hits the center of the paddle we want the ball to take a 0degrees angle
// when the ball hits the bottom of the paddle we want the ball to take a 45degrees
// Math.PI/4 = 45degrees
let angleRad = (Math.PI / 4) * collidePoint;
// change the X and Y velocity direction
let direction = ball.x + ball.radius < canvas.width / 2 ? 1 : -1;
ball.velocityX = direction * ball.speed * Math.cos(angleRad);
ball.velocityY = ball.speed * Math.sin(angleRad);
// speed up the ball everytime a paddle hits it.
ball.speed += 0.1;
}
}
// render function, the function that does al the drawing
function render() {
// clear the canvas
drawRect(0, 0, canvas.width, canvas.height, "#000");
// draw the user score to the left
drawText(user.score, canvas.width / 4, canvas.height / 5);
// draw the COM score to the right
drawText(com.score, (3 * canvas.width) / 4, canvas.height / 5);
// draw the net
drawNet();
// draw the user's paddle
drawRect(user.x, user.y, user.width, user.height, user.color);
// draw the COM's paddle
drawRect(com.x, com.y, com.width, com.height, com.color);
// draw the ball
drawArc(ball.x, ball.y, ball.radius, ball.color);
}
function game() {
update();
render();
}
// number of frames per second
let framePerSecond = 50;
//call the game function 50 times every 1 Sec
let loop = setInterval(game, 1000 / framePerSecond);
You'll need an event listener on the user-speed textbox that changes the value of the global variable userSpeedInput when the user types something. Right now that variable is only set once, when the page first loads.
document.getElementById("user-speed").addEventListener("change", (e) => {
userSpeedInput = parseInt(e.target.value);
});

Determine if a Selection Marquee is over a Rotated Rectangle

I have a Rectangle class for drawing to HTML Canvas. It has a rotation property that gets applied in its draw method. If the user drags within the canvas, a selection marquee is being drawn. How can I set the Rectangle's active attribute to true when the Rectangle is within the selection marquee using math? This is a problem I'm having in another language & context so I do not have all of Canvas' methods available to me there (e.g. isPointInPath).
I found a StackOverflow post about finding Mouse position within rotated rectangle in HTML5 Canvas, which I am implementing in the Rectangle method checkHit. It doesn't account for the selection marquee, however. It's just looking at the mouse X & Y, which is still off. The light blue dot is the origin around which the rectangle is being rotated. Please let me know if anything is unclear. Thank you.
class Rectangle
{
constructor(x, y, width, height, rotation) {
this.x = x;
this.y = y;
this.height = height;
this.width = width;
this.xOffset = this.x + this.width/2;
this.yOffset = this.y + ((this.y+this.height)/2);
this.rotation = rotation;
this.active = false;
}
checkHit()
{
// translate mouse point values to origin
let originX = this.xOffset;
let originY = this.yOffset;
let dx = marquee[2] - originX;
let dy = marquee[3] - originY;
// distance between the point and the center of the rectangle
let h1 = Math.sqrt(dx*dx + dy*dy);
let currA = Math.atan2(dy,dx);
// Angle of point rotated around origin of rectangle in opposition
let newA = currA - this.rotation;
// New position of mouse point when rotated
let x2 = Math.cos(newA) * h1;
let y2 = Math.sin(newA) * h1;
// Check relative to center of rectangle
if (x2 > -0.5 * this.width && x2 < 0.5 * this.width && y2 > -0.5 * this.height && y2 < 0.5 * this.height){
this.active = true;
} else {
this.active = false;
}
}
draw()
{
ctx.save();
ctx.translate(this.xOffset, this.yOffset);
ctx.fillStyle = 'rgba(255,255,255,1)';
ctx.beginPath();
ctx.arc(0, 0, 3, 0, 2 * Math.PI, true);
ctx.fill();
ctx.rotate(this.rotation * Math.PI / 180);
ctx.translate(-this.xOffset, -this.yOffset);
if (this.active)
{
ctx.fillStyle = 'rgba(255,0,0,0.5)';
} else {
ctx.fillStyle = 'rgba(0,0,255,0.5)';
}
ctx.beginPath();
ctx.fillRect(this.x, this.y, this.width, this.y+this.height);
ctx.closePath();
ctx.stroke();
ctx.restore();
}
}
var canvas = document.getElementById("canvas");
var ctx = canvas.getContext("2d");
var raf;
var rect = new Rectangle(50,50,90,30,45);
var marquee = [-3,-3,-3,-3];
var BB=canvas.getBoundingClientRect();
var offsetX=BB.left;
var offsetY=BB.top;
var start_x,start_y;
let draw = () => {
ctx.clearRect(0,0, canvas.width, canvas.height);
//rect.rotation+=1;
rect.draw();
ctx.fillStyle = "rgba(200, 200, 255, 0.5)";
ctx.fillRect(parseInt(marquee[0]),parseInt(marquee[1]),parseInt(marquee[2]),parseInt(marquee[3]))
ctx.strokeStyle = "white"
ctx.lineWidth = 1;
ctx.rect(parseInt(marquee[0]),parseInt(marquee[1]),parseInt(marquee[2]),parseInt(marquee[3]))
ctx.stroke()
raf = window.requestAnimationFrame(draw);
}
let dragStart = (e) =>
{
start_x = parseInt(e.clientX-offsetX);
start_y = parseInt(e.clientY-offsetY);
marquee = [start_x,start_y,0,0];
canvas.addEventListener("mousemove", drag);
}
let drag = (e) =>
{
let mouseX = parseInt(e.clientX-offsetX);
let mouseY = parseInt(e.clientY-offsetY);
marquee[2] = mouseX - start_x;
marquee[3] = mouseY - start_y;
rect.checkHit();
}
let dragEnd = (e) =>
{
marquee = [-10,-10,-10,-10];
canvas.removeEventListener("mousemove", drag);
}
canvas.addEventListener('mousedown', dragStart);
canvas.addEventListener('mouseup', dragEnd);
raf = window.requestAnimationFrame(draw);
body
{
margin:0;
}
#canvas
{
width: 360px;
height: 180px;
border: 1px solid grey;
background-color: grey;
}
<canvas id="canvas" width="360" height="180"></canvas>
Do convex polygons overlap
Rectangles are convex polygons.
Rectangle and marquee each have 4 points (corners) that define 4 edges (lines segments) connecting the points.
This solution works for all convex irregular polygons with 3 or more sides.
Points and edges must be sequential either Clockwise CW of Count Clockwise CCW
Test points
If any point of one poly is inside the other polygon then they must overlap. See example function isInside
To check if point is inside a polygon, get cross product of, edge start to the point as vector, and the edge as a vector.
If all cross products are >= 0 (to the left of) then there is overlap (for CW polygon). If polygon is CCW then if all cross products are <= 0 (to the right of) there is overlap.
It is possible to overlap without any points inside the other poly.
Test Edges
If any of the edges from one poly crosses any of the edges from the other then there must be overlap. The function doLinesIntercept returns true if two line segments intercept.
Complete test
Function isPolyOver(poly1, poly2) will return true if there is overlap of the two polys.
A polygon is defined by a set of Point's and Lines's connecting the points.
The polygon can be irregular, meaning that each edge can be any length > 0
Do not pass polygons with an edge of length === 0 or will not work.
Added
I added the function Rectangle.toPoints that transforms the rectangle and returning a set of 4 points (corners).
Example
Example is a copy of your code working using the above methods.
canvas.addEventListener('mousedown', dragStart);
canvas.addEventListener('mouseup', dragEnd);
requestAnimationFrame(draw);
const Point = (x = 0, y = 0) => ({x, y, set(x,y){ this.x = x; this.y = y }});
const Line = (p1, p2) => ({p1, p2});
const selector = { points: [Point(), Point(), Point(), Point()] }
selector.lines = [
Line(selector.points[0], selector.points[1]),
Line(selector.points[1], selector.points[2]),
Line(selector.points[2], selector.points[3]),
Line(selector.points[3], selector.points[0])
];
const rectangle = { points: [Point(), Point(), Point(), Point()] }
rectangle.lines = [
Line(rectangle.points[0], rectangle.points[1]),
Line(rectangle.points[1], rectangle.points[2]),
Line(rectangle.points[2], rectangle.points[3]),
Line(rectangle.points[3], rectangle.points[0])
];
function isInside(point, points) {
var i = 0, p1 = points[points.length - 1];
while (i < points.length) {
const p2 = points[i++];
if ((p2.x - p1.x) * (point.y - p1.y) - (p2.y - p1.y) * (point.x - p1.x) < 0) { return false }
p1 = p2;
}
return true;
}
function doLinesIntercept(l1, l2) {
const v1x = l1.p2.x - l1.p1.x;
const v1y = l1.p2.y - l1.p1.y;
const v2x = l2.p2.x - l2.p1.x;
const v2y = l2.p2.y - l2.p1.y;
const c = v1x * v2y - v1y * v2x;
if(c !== 0){
const u = (v2x * (l1.p1.y - l2.p1.y) - v2y * (l1.p1.x - l2.p1.x)) / c;
if(u >= 0 && u <= 1){
const u = (v1x * (l1.p1.y - l2.p1.y) - v1y * (l1.p1.x - l2.p1.x)) / c;
return u >= 0 && u <= 1;
}
}
return false;
}
function isPolyOver(p1, p2) { // is poly p2 under any part of poly p1
if (p2.points.some(p => isInside(p, p1.points))) { return true };
if (p1.points.some(p => isInside(p, p2.points))) { return true };
return p1.lines.some(l1 => p2.lines.some(l2 => doLinesIntercept(l1, l2)));
}
const ctx = canvas.getContext("2d");
var dragging = false;
const marquee = [0,0,0,0];
const rotate = 0.01;
var startX, startY, hasSize = false;
const BB = canvas.getBoundingClientRect();
const offsetX = BB.left;
const offsetY = BB.top;
class Rectangle {
constructor(x, y, width, height, rotation) {
this.x = x;
this.y = y;
this.height = height;
this.width = width;
this.rotation = rotation;
this.active = false;
}
toPoints(points = [Point(), Point(), Point(), Point()]) {
const xAx = Math.cos(this.rotation) / 2;
const xAy = Math.sin(this.rotation) / 2;
const x = this.x, y = this.y;
const w = this.width, h = this.height;
points[0].set(-w * xAx + h * xAy + x, -w * xAy - h * xAx + y);
points[1].set( w * xAx + h * xAy + x, w * xAy - h * xAx + y);
points[2].set( w * xAx - h * xAy + x, w * xAy + h * xAx + y);
points[3].set(-w * xAx - h * xAy + x, -w * xAy + h * xAx + y);
}
draw() {
ctx.setTransform(1, 0, 0, 1, this.x, this.y);
ctx.fillStyle = 'rgba(255,255,255,1)';
ctx.strokeStyle = this.active ? 'rgba(255,0,0,1)' : 'rgba(0,0,255,1)';
ctx.lineWidth = this.active ? 3 : 1;
ctx.beginPath();
ctx.arc(0, 0, 3, 0, 2 * Math.PI, true);
ctx.fill();
ctx.rotate(this.rotation);
ctx.beginPath();
ctx.rect(-this.width / 2, - this.height / 2, this.width, this.height);
ctx.stroke();
}
}
function draw(){
rect.rotation += rotate;
ctx.setTransform(1, 0, 0, 1, 0, 0);
ctx.clearRect(0, 0, canvas.width, canvas.height);
rect.draw();
drawSelector();
requestAnimationFrame(draw);
}
function drawSelector() {
if (dragging && hasSize) {
rect.toPoints(rectangle.points);
rect.active = isPolyOver(selector, rectangle);
ctx.setTransform(1, 0, 0, 1, 0, 0);
ctx.fillStyle = "rgba(200, 200, 255, 0.5)";
ctx.strokeStyle = "white";
ctx.lineWidth = 1;
ctx.beginPath();
ctx.rect(...marquee);
ctx.fill();
ctx.stroke();
} else {
rect.active = false;
}
}
function dragStart(e) {
startX = e.clientX - offsetX;
startY = e.clientY - offsetY;
drag(e);
canvas.addEventListener("mousemove", drag);
}
function drag(e) {
dragging = true;
const x = e.clientX - offsetX;
const y = e.clientY - offsetY;
const left = Math.min(startX, x);
const top = Math.min(startY, y);
const w = Math.max(startX, x) - left;
const h = Math.max(startY, y) - top;
marquee[0] = left;
marquee[1] = top;
marquee[2] = w;
marquee[3] = h;
if (w > 0 || h > 0) {
hasSize = true;
selector.points[0].set(left, top);
selector.points[1].set(left + w, top);
selector.points[2].set(left + w, top + h);
selector.points[3].set(left , top + h);
} else {
hasSize = false;
}
}
function dragEnd(e) {
dragging = false;
rect.active = false;
canvas.removeEventListener("mousemove", drag);
}
const rect = new Rectangle(canvas.width / 2, canvas.height / 2, 90, 90, Math.PI / 4);
body
{
margin:0;
}
#canvas
{
width: 360px;
height: 180px;
border: 1px solid grey;
background-color: grey;
}
<canvas id="canvas" width="360" height="180"></canvas>

setTimeout am I doing it wrong? for fireworks

So I am trying to get the clearInterval to work. This is from: https://codepen.io/whqet/pen/Auzch (I modified it a bit to make it more towards what I needed. Essentially, I want the animation to cease after 10000 ms. Excuse the messy coding, I threw in a timer at the bottom so I could see whether or not it would work.. Any assistance would be appreciated. Thanks!
var canvas = document.getElementById( 'canvas' ),
ctx = canvas.getContext( '2d' ),
// full screen dimensions
cw = window.innerWidth,
ch = window.innerHeight,
// firework collection
fireworks = [],
// particle collection
particles = [],
// starting hue
hue = 120,
// when launching fireworks with a click, too many get launched at once without a limiter, one launch per 5 loop ticks
limiterTotal = 5,
limiterTick = 0,
// this will time the auto launches of fireworks, one launch per 60 loop ticks
timerTotal = 60,
timerTick = 0,
mousedown = false,
// mouse x coordinate,
mx,
// mouse y coordinate
my;
// set canvas dimensions
canvas.width = cw;
canvas.height = ch;
// now we are going to setup our function placeholders for the entire demo
// get a random number within a range
function random( min, max ) {
return Math.random() * ( max - min ) + min;
}
// calculate the distance between two points
function calculateDistance( p1x, p1y, p2x, p2y ) {
var xDistance = p1x - p2x,
yDistance = p1y - p2y;
return Math.sqrt( Math.pow( xDistance, 2 ) + Math.pow( yDistance, 2 ) );
}
// create firework
function Firework( sx, sy, tx, ty ) {
// actual coordinates
this.x = sx;
this.y = sy;
// starting coordinates
this.sx = sx;
this.sy = sy;
// target coordinates
this.tx = tx;
this.ty = ty;
// distance from starting point to target
this.distanceToTarget = calculateDistance( sx, sy, tx, ty );
this.distanceTraveled = 0;
// track the past coordinates of each firework to create a trail effect, increase the coordinate count to create more prominent trails
this.coordinates = [];
this.coordinateCount = 3;
// populate initial coordinate collection with the current coordinates
while( this.coordinateCount-- ) {
this.coordinates.push( [ this.x, this.y ] );
}
this.angle = Math.atan2( ty - sy, tx - sx );
this.speed = 2;
this.acceleration = 1.05;
this.brightness = random( 50, 70 );
// circle target indicator radius
this.targetRadius = 1;
}
// update firework
Firework.prototype.update = function( index ) {
// remove last item in coordinates array
this.coordinates.pop();
// add current coordinates to the start of the array
this.coordinates.unshift( [ this.x, this.y ] );
// cycle the circle target indicator radius
if( this.targetRadius < 8 ) {
this.targetRadius += 0.3;
} else {
this.targetRadius = 1;
}
// speed up the firework
this.speed *= this.acceleration;
// get the current velocities based on angle and speed
var vx = Math.cos( this.angle ) * this.speed,
vy = Math.sin( this.angle ) * this.speed;
// how far will the firework have traveled with velocities applied?
this.distanceTraveled = calculateDistance( this.sx, this.sy, this.x + vx, this.y + vy );
// if the distance traveled, including velocities, is greater than the initial distance to the target, then the target has been reached
if( this.distanceTraveled >= this.distanceToTarget ) {
createParticles( this.tx, this.ty );
// remove the firework, use the index passed into the update function to determine which to remove
fireworks.splice( index, 1 );
} else {
// target not reached, keep traveling
this.x += vx;
this.y += vy;
}
}
// draw firework
Firework.prototype.draw = function() {
ctx.beginPath();
// move to the last tracked coordinate in the set, then draw a line to the current x and y
ctx.moveTo( this.coordinates[ this.coordinates.length - 1][ 0 ], this.coordinates[ this.coordinates.length - 1][ 1 ] );
ctx.lineTo( this.x, this.y );
ctx.strokeStyle = 'hsl(' + hue + ', 100%, ' + this.brightness + '%)';
ctx.stroke();
ctx.beginPath();
// draw the target for this firework with a pulsing circle
ctx.arc( this.tx, this.ty, this.targetRadius, 0, Math.PI * 2 );
ctx.stroke();
}
// create particle
function Particle( x, y ) {
this.x = x;
this.y = y;
// track the past coordinates of each particle to create a trail effect, increase the coordinate count to create more prominent trails
this.coordinates = [];
this.coordinateCount = 5;
while( this.coordinateCount-- ) {
this.coordinates.push( [ this.x, this.y ] );
}
// set a random angle in all possible directions, in radians
this.angle = random( 0, Math.PI * 2 );
this.speed = random( 1, 10 );
// friction will slow the particle down
this.friction = 0.95;
// gravity will be applied and pull the particle down
this.gravity = 1;
// set the hue to a random number +-50 of the overall hue variable
this.hue = random( hue - 50, hue + 50 );
this.brightness = random( 50, 80 );
this.alpha = 1;
// set how fast the particle fades out
this.decay = random( 0.015, 0.03 );
}
// update particle
Particle.prototype.update = function( index ) {
// remove last item in coordinates array
this.coordinates.pop();
// add current coordinates to the start of the array
this.coordinates.unshift( [ this.x, this.y ] );
// slow down the particle
this.speed *= this.friction;
// apply velocity
this.x += Math.cos( this.angle ) * this.speed;
this.y += Math.sin( this.angle ) * this.speed + this.gravity;
// fade out the particle
this.alpha -= this.decay;
// remove the particle once the alpha is low enough, based on the passed in index
if( this.alpha <= this.decay ) {
particles.splice( index, 1 );
}
}
// draw particle
Particle.prototype.draw = function() {
ctx. beginPath();
// move to the last tracked coordinates in the set, then draw a line to the current x and y
ctx.moveTo( this.coordinates[ this.coordinates.length - 1 ][ 0 ], this.coordinates[ this.coordinates.length - 1 ][ 1 ] );
ctx.lineTo( this.x, this.y );
ctx.strokeStyle = 'hsla(' + this.hue + ', 100%, ' + this.brightness + '%, ' + this.alpha + ')';
ctx.stroke();
}
// create particle group/explosion
function createParticles( x, y ) {
// increase the particle count for a bigger explosion, beware of the canvas performance hit with the increased particles though
var particleCount = 300;
while( particleCount-- ) {
particles.push( new Particle( x, y ) );
}
}
// main demo loop
function loop() {
// this function will run endlessly with requestAnimationFrame
requestAnimFrame( loop );
// increase the hue to get different colored fireworks over time
//hue += 0.5;
// create random color
hue= random(0, 360 );
// normally, clearRect() would be used to clear the canvas
// we want to create a trailing effect though
// setting the composite operation to destination-out will allow us to clear the canvas at a specific opacity, rather than wiping it entirely
ctx.globalCompositeOperation = 'destination-out';
// decrease the alpha property to create more prominent trails
ctx.fillStyle = 'rgba(0, 0, 0, 0.5)';
ctx.fillRect( 0, 0, cw, ch );
// change the composite operation back to our main mode
// lighter creates bright highlight points as the fireworks and particles overlap each other
ctx.globalCompositeOperation = 'lighter';
// loop over each firework, draw it, update it
var i = fireworks.length;
while( i-- ) {
fireworks[ i ].draw();
fireworks[ i ].update( i );
}
// loop over each particle, draw it, update it
var i = particles.length;
while( i-- ) {
particles[ i ].draw();
particles[ i ].update( i );
}
// launch fireworks automatically to random coordinates, when the mouse isn't down
if( timerTick >= timerTotal ) {
if( !mousedown ) {
// start the firework at the bottom middle of the screen, then set the random target coordinates, the random y coordinates will be set within the range of the top half of the screen
fireworks.push( new Firework( cw / 2, ch, random( 0, cw ), random( 0, ch / 2 ) ) );
timerTick = 0;
}
} else {
timerTick++;
}
// limit the rate at which fireworks get launched when mouse is down
if( limiterTick >= limiterTotal ) {
if( mousedown ) {
// start the firework at the bottom middle of the screen, then set the current mouse coordinates as the target
fireworks.push( new Firework( cw / 2, ch, mx, my ) );
limiterTick = 0;
}
} else {
limiterTick++;
}
}
// mouse event bindings
// update the mouse coordinates on mousemove
canvas.addEventListener( 'mousemove', function( e ) {
mx = e.pageX - canvas.offsetLeft;
my = e.pageY - canvas.offsetTop;
});
// toggle mousedown state and prevent canvas from being selected
canvas.addEventListener( 'mousedown', function( e ) {
e.preventDefault();
mousedown = true;
});
canvas.addEventListener( 'mouseup', function( e ) {
e.preventDefault();
mousedown = false;
});
// once the window loads, we are ready for some fireworks!
window.onload = loop;
// when animating on canvas, it is best to use requestAnimationFrame instead of setTimeout or setInterval
window.requestAnimFrame = ( function() {
return window.requestAnimationFrame ||
window.webkitRequestAnimationFrame ||
window.mozRequestAnimationFrame ||
function( ) {
window.setTimeout( timePeriodms );
};
function stopFireworks () {
var timePeriodms = 10000;
window.clearTimeout(timePeriodms);
};
})();
// now we will setup our basic variables for the demo
var minute = 0;
var sec = 00;
var zeroPholder = 0;
var counterIdv2 = setInterval(function(){
countUp2();
}, 1000);
function countUp2 () {
sec++;
if(sec == 60){
sec = 00;
minute = minute + 1;
}
if (minute == 0 && sec == 1) {
document.getElementById('count-up-2').style.color = 'red';
}
if (minute == 0 && sec == 59) {
document.getElementById('count-up-2').style.color = 'blue';
}
if (minute == 10 && sec == 00) {
document.getElementById('count-up-2').style.color = 'red';
}
if(sec == 10){
zeroPholder = '';
}else
if(sec == 00){
zeroPholder = 0;
}
document.getElementById("count-up-2").innerText = minute+':'+zeroPholder+sec;
}
body {
background: #000;
margin: 0;
}
canvas {
display: block;
}
<canvas id="canvas">Canvas is not supported in your browser.</canvas>
<table border="1" style="border-color:black;">
<tbody>
<tr>
<td style="background-color: #fff; padding: 5px;"><span>Time spent on page: </span><span id="count-up-2">0:00</span>
</td>
</tr>
</tbody>
</table>
requestAnimationFrame callback's argument
When requestAnimationFrame calls the callback function it supplies a high precision time in ms. You can use this time to control the timing of your animations.
For example the following animation loop will stop 10 seconds after the first frame is called.
requestAnimationFrame(mainLoop);
var startTime;
const endTime = 10000; // in ms
function mainLoop(time) {
if (startTime === undefined) { startTime = time }
if (time - startTime > endTime) {
console.log("Animation end.");
return;
}
// draw animated content
requestAnimationFrame(mainLoop);
}
This gives you better timing control in animations than using setTimeout especially if you have more than one thing to control the timing of (see demo)
Demo
The demo uses the same method to count down the time and control the fireworks. A few seconds before the end the fireworks are held back and then a second before a group are fired to explode in time with zero (Well there about!!!)
// Code based loosely on OPs code in so far as it animates fireworks.
// There is no control of fireworks via mouse.
// Uses reusable buffers to avoid GC overhead in low end devices.
const ctx = canvas.getContext( '2d' );
const GRAVITY = 0.2; // per frame squared
var cw = canvas.width = innerWidth - 40;
var ch = canvas.height = 500;
const iH = innerHeight;
var startTime;
const DISPLAY_TIME = 10000; // in ms. Time to countdown
const SHELL_X = cw / 2; // Location that shells are fired from in px
const SHELL_Y = ch;
const SHELL_TIME = 100; // in frames
const MAX_SHELLS = 8;
const MAX_PARTICLES = 1000; // Approx max particles.
const SHELL_RANDOM_RATE = 0.01; // cof of shell random fire control
const SHELL_FIRE_CURVE = 3; // Highest power of fire control exponents
var randomFire = 0; // holds the odds of a random shell being fired
Math.TAU = Math.PI * 2;
Math.PI90 = Math.PI / 2;
Math.PI270 = Math.PI + Math.PI90;
Math.rand = (m, M) => Math.random() * (M - m) + m;
Math.distance = (x1, y1, x2, y2) => ((x1 - x2) ** 2 + (y1 - y2) ** 2) ** 0.5;
requestAnimationFrame(mainLoop);
function mainLoop(time) {
if (startTime === undefined) { startTime = time }
const timer = DISPLAY_TIME - (time - startTime);
const displayTime = timer / 1000 | 0;
if (timer <= 0) {
countdown.textContent = "HAPPY NEW YEAR 2020";
if(particles.size === 0 && shells.size === 0) {
ctx.clearRect(0,0,cw,ch);
shells.clear();
startTime = undefined;
countdown.textContent = "Click to restart";
canvas.addEventListener("click",() => requestAnimationFrame(mainLoop), {once: true});
return; // ends update
} else {
randomFire = 0; // pervent shells firing after zero time
}
} else {
countdown.textContent !== displayTime && (countdown.textContent = displayTime);
}
ctx.lineCap = "round";
ctx.globalCompositeOperation = 'destination-out';
ctx.globalAlpha = 0.2;
ctx.fillStyle = "#000"
ctx.fillRect( 0, 0, cw, ch );
ctx.globalCompositeOperation = 'lighter';
shells.update();
particles.update();
ctx.lineWidth = 2;
shells.draw();
ctx.lineWidth = 3;
particles.draw();
if (timer < 2500 && timer > 1000) { randomFire = 0 }
else if(timer <= 1000 && timer > 0) { randomFire = 1 }
if(shells.size < MAX_SHELLS && particles.size < MAX_PARTICLES) {
if(Math.random() < randomFire ** SHELL_FIRE_CURVE) {
randomFire = 0;
shells.fire(
Math.rand(cw * (1/3), cw *(2/3)),
Math.rand(iH * (3/4), iH *(4/4)),
SHELL_TIME
);
}
randomFire += SHELL_RANDOM_RATE;
}
requestAnimationFrame(mainLoop);
}
function Trail() {}
function Particle() { }
function Shell( sx, sy, tx, ty ) {
this.trail = new Trail();
this.init(sx, sy,tx,sy);
}
Trail.prototype = {
init(x, y) {
this.x1 = this.x2 = this.x3 = x;
this.y1 = this.y2 = this.y3 = y;
},
update(x, y) {
this.x3 = this.x2
this.y3 = this.y2
this.x2 = this.x1
this.y2 = this.y1
this.x1 = x;
this.y1 = y;
},
draw() {
ctx.moveTo(this.x1, this.y1);
ctx.lineTo(this.x2, this.y2);
ctx.lineTo(this.x3, this.y3);
}
};
Shell.prototype = {
init(x, y, time) {
this.x = SHELL_X;
this.y = SHELL_Y;
this.sx = (x - this.x) / (time / 2);
this.sy = ((y - this.y) * (GRAVITY / ((time) ** 0.5)))* 2;
this.power = (-this.sy * 10) | 0;
this.hue = Math.rand(360, 720) % 360 | 0;
this.active = true;
this.trail.init(this.x, this.y);
this.time = time / 2;
this.life = time / 2;
},
explode() {
this.active = false;
particles.explode(this, this.power);
},
update() {
this.time -= 1;
if (this.time <= 0) { this.explode() }
this.sy += GRAVITY;
this.x += this.sx;
this.y += this.sy;
this.trail.update(this.x, this.y);
return this.active;
},
draw() {
ctx.strokeStyle = `hsl(${this.hue},100%,${(this.time / this.life) * 100}%)`;
ctx.beginPath();
this.trail.draw();
ctx.stroke();
},
};
Particle.prototype = {
init(shell) {
this.x2 = this.x1 = this.x = shell.x;
this.y2 = this.y1 = this.y = shell.y;
this.dx = shell.sx;
this.dy = shell.sy;
this.angle = Math.rand(0, Math.TAU);
const zAng = Math.cos(Math.random() ** 2 * Math.PI)
this.speed = zAng * shell.power / 30;
this.friction = 0.95;
this.gravity = GRAVITY;
this.hue = (Math.rand(shell.hue - 5, shell.hue + 5) + 360) % 360;
this.brightness = Math.rand( 25, 50 );
this.alpha = shell.power / 10;
this.decay = Math.rand( 0.2, 0.5);
this.active = true;
},
update() {
const dx = Math.cos(this.angle);
const dy = Math.sin(this.angle);
this.x2 = this.x1;
this.y2 = this.y1;
this.x1 = this.x - dx;
this.y1 = this.y + dy;
this.speed *= this.friction;
this.x += (this.dx *= 0.9);
this.y += (this.dy *= 0.9);
this.dy += GRAVITY / 100;
this.x += dx * this.speed;
this.y += dy * this.speed;
this.alpha -= this.decay;
if( this.alpha <= 0 || this.x < 0 || this.y < 0 || this.x > cw) {
this.active = false;
}
return this.active;
},
draw() {
const alpha = this.alpha / 5 > 1 ? 1 : this.alpha / 5;
const lum = this.brightness + this.alpha
ctx.strokeStyle = `hsla(${this.hue},100%,${lum<100 ? lum : 100}%,${alpha})`;
ctx. beginPath();
ctx.moveTo( this.x2, this.y2);
ctx.lineTo( this.x, this.y );
ctx.stroke();
}
};
function BubbleArray(extension) {
return Object.assign([], {
size: 0,
update() {
var read = 0, write = 0;
while (read < this.size) {
const item = this[read];
if(read !== write) {
const temp = this[write]
this[write] = item;
this[read] = temp;
}
item.update() === true && (write ++);
read++;
}
this.size = write;
},
draw() {
var i = 0,len = this.size;
while(i < len) { this[i++].draw() }
},
add(item) {
this.size ++;
this.push(item);
},
clear() { this.length = this.size = 0 },
getInactive() { return this.size < this.length ? this[this.size++] : undefined },
},
extension,
);
}
const particles = BubbleArray({
explode(shell, count) {
var item;
while(count-- > 0) {
!(item = this.getInactive()) && this.add(item = new Particle());
item.init(shell);
}
},
});
const shells = BubbleArray({
fire(tx = mx, ty = my) {
var item;
!(item = this.getInactive()) && this.add(item = new Shell());
item.init(tx, ty, 100);
}
});
body {
padding: 0px;
}
canvas {
background: #000;
position: absolute;
top: 0px;
left: 0px;
}
#countdown {
position: absolute;
top: 20px;
left: 20px;
font-family: arial;
font-size: xx-large;
color: white;
}
<canvas id="canvas"></canvas>
<div id="countdown"></div>

Animating Multiple Custom Shapes in html5 Canvas

Very first post so I apologize if I format my question wrong.
I have had some experience animating multiple circles with canvas prior to this project but currently am trying to animate a custom made letter Z.
Currently in the process of trying to make 100 Z's do the same thing, for some reason my constructor object is only drawing one.
So far I have:
console logged in my draw function to see if its getting that far and it is
console logged the array of Z objects, and all 100 are there.
in that array of objects I've checked all their x-coordinates and they are different, so I don't think all 100 are just sitting on top of each other.
My suspicions are that update function is some how wrong with my use of "this"
I guess ill just dump my whole javascript file incase its something else. Sorry i dont know typical protocol.
var canvas = document.getElementById('myCanvas');
var context = canvas.getContext('2d');
canvas.height = window.innerHeight;
canvas.width = window.innerWidth;
canvasHeight = canvas.height;
canvasWidth = canvas.width
var dx = 0
var dy = 0;
var direction = true
function Z(xCoord, yCoord, dx, dy, direction) {
this.x = xCoord
this.y = yCoord
this.dy = dy
this.dx = dx
this.direction = direction
this.update = function(){
//making the Z wiggle
if (this.dx < 20 && this.direction) {
this.dx++
}
else if (this.dx === 20 && this.direction) {
this.direction = false;
}
if (this.dx > -20 && !this.direction) {
this.dx--
}
else if (this.dx === -20 && !this.direction) {
this.direction = true;
}
//if Z gets to top of page it resets down at the bottom
if (this.dy === -canvasHeight - this.y) {
this.dy = 0
}
this.dy--;
this.draw()
}
this.draw = function() {
context.clearRect(0, 0, context.canvas.width, context.canvas.height);
context.translate(this.dx, this.dy); /// translate (move)
//The Letter Z
context.beginPath();
context.moveTo(this.x, this.y + canvasHeight); // x+0
context.lineTo(this.x+10, this.y + canvasHeight); // x+10
context.lineTo(this.x+2, this.y + canvasHeight-9); // x+2
context.lineTo(this.x+5, this.y + canvasHeight-9); // x+5
context.lineTo(this.x+10, this.y + canvasHeight-9); // x+10
context.lineTo(this.x+10, this.y + canvasHeight-10); // x+10
context.lineTo(this.x, this.y + canvasHeight-10); // x+0
context.lineTo(this.x+8, this.y + canvasHeight-1); // x+8
context.lineTo(this.x, this.y + canvasHeight-1); // x+0
context.lineTo(this.x, this.y + canvasHeight); // x+0
// complete custom shape
context.closePath();
context.lineWidth = 4;
context.fillStyle = 'white';
context.fill();
context.strokeStyle = 'black';
context.stroke();
context.translate(-this.dx, -this.dy); /// translate (move)
}
}
var ZArr = []
//for loop to make all the Z's
for (var index = 0; index < 100; index++) {
var randomX = Math.random() * canvasWidth;
var randomY = Math.floor(Math.random() * 500) + 1
var newZ = new Z(randomX, randomY, dx, dy, direction);
ZArr.push(newZ)
}
// console.log(ZArr)
function executeFrame() {
for (let index = 0; index < ZArr.length; index++) {
ZArr[index].update();
}
requestAnimationFrame(executeFrame);
}
//start animation
executeFrame();
Move context.clearRect(0, 0, context.canvas.width, context.canvas.height); to your render loop. Your erasing each drawing every loop of your array.
function executeFrame() {
context.clearRect(0, 0, context.canvas.width, context.canvas.height);
for (let index = 0; index < ZArr.length; index++) {
ZArr[index].update();
}
requestAnimationFrame(executeFrame);
}
This is because your clear canvas line is inside of your Z object's draw function, so the field will be cleared everytime you try to draw a new Z. This means only the last one in the array will be visible. Just move this line.
context.clearRect(0, 0, context.canvas.width, context.canvas.height);
To your executeFrame function just above the for loop.
Good luck!

Drag and drop on a canvas HTML5

I am trying to implement a drag and drop on a canvas representing 3 disks.
I would like to change with mouse the position of each mass. My main problem is that I am constrained by the length of axe for each of these 3 spheres.
For the moment, I have implemented the following function when mouse is moving inside the canvas (value of indexMass indicates which mass is moved: 1, 2 or 3 and t1, t2, t3 represents respectively the angle of mass 1, 2, 3):
// Happens when the mouse is moving inside the canvas
function myMove(event) {
if (isDrag) {
var x = event.offsetX;
var y = event.offsetY;
if (indexMass == 1)
{ // Update theta1 value
t1 = t1 + 0.1*Math.atan(y/x);
}
else if (indexMass == 2)
{ // Update theta2 value
t2 = t2 + 0.1*Math.atan(y/x);
}
else if (indexMass == 3)
{ // Update theta3 value
t3 = t3 + 0.1*Math.atan(y/x);
}
// Update drawing
DrawPend(canvas);
}
}
As you can see, I did for each angle:
t = t + 0.1*Math.atan(y/x);
with:
var x = event.offsetX;
var y = event.offsetY;
But this effect is not very nice. Once the sphere is selected with mouse (on mouse click), I would like the cursor to be stuck with this sphere or the sphere to follow the "delta" of the mouse coordinates when I am not on sphere any more.
Update 1
#Blindman67: thanks for your help, your code snippet is pretty complex for me, I didn't understand it all. But I am on the right way.
I am starting by the first issue: make rotate the selected disk with mouse staying very closed to it or over it, when dragging.
For the moment, I have modified my function myMove (which is called when I have clicked down and move the mouse for dragging) like:
// Happens when the mouse is moving inside the canvas
function myMove(event) {
// If dragging
if (isDrag) {
// Compute dx and dy before calling DrawPend
var lastX = parseInt(event.offsetX - mx);
var lastY = parseInt(event.offsetY - my);
var dx = lastX - window['x'+indexMass];
var dy = lastY - window['y'+indexMass];
// Change angle when dragging
window['t'+indexMass] = Math.atan2(dy, dx);
// Update drawing
DrawPend(canvas);
// Highlight dragging disk
fillDisk(indexMass, 'pink');
}
}
where indexMass is the index of dragged disk and window['x'+indexMass] , window['y'+indexMass] are the current coordinates of the selected disk center.
After, I compute the dx, dy respectively from coordinates mouse clicked when starting drag (mx, my returned by getMousePos function) and mouse coordinates with moving.
Finally, I change the angle of disk by set, for global variable (theta of selected disk), i.e window['t'+indexMass]:
// Change angle when dragging
window['t'+indexMass] = Math.atan2(dy, dx);
I have took your part of code with Math.atan2.
But the result of this function doesn't make a good animation with mouse dragging, I would like to know where this could come from.
Right now, I would like to implement only the dragging without modifying the length of axis, I will see more later for this functionality.
Update 2
I keep going on to find a solution about the dragging of a selected mass with mouse.
For trying a synthesis of what I have done previously, I believe the following method is good but this dragging method is not working very well: the selected disk doesn't follow correctly the mouse and I don't know why.
In myMove function (function called when I start dragging), I decided to:
Compute the dx, dy between the mouse coordinates and the selected disk coordinates, i.e:
var dx = parseInt(event.offsetX - window['x'+indexMass]);
var dy = parseInt(event.offsetY - window['y'+indexMass]);
indexMass represents the index of the selected disk.
Increment the position of selected disk (stored in temporary variables tmpX, tmpY) by dx, dy.
Compute the new angle theta (identified in code by global variable window['t'+indexMass]
Compute the new positions of selected disk with this new value of theta, i.e for example with disk1 (indexMass=1 and theta = t1):
x1= x0 +l1 * sin(t1)
y1= y0 +l1 * sin(t1)
I want to draw readers' attention to the fact that I want dragging with mouse not to modify the lengths of axes with mouse, this is a constraint.
Here's the entire myMove function (called when drag is starting) :
// Happens when the mouse is moving inside the canvas
function myMove(event) {
// If dragging
if (isDrag) {
console.log('offsetX', event.offsetX);
console.log('offsetY', event.offsetY);
var dx = parseInt(event.offsetX - window['x'+indexMass]);
var dy = parseInt(event.offsetY - window['y'+indexMass]);
console.log('dx', dx);
console.log('dy', dy);
// Temp variables
var tmpX = window['x'+indexMass];
var tmpY = window['y'+indexMass];
// Increment temp positions
tmpX += dx;
tmpY += dy;
// Compute new angle for indexMass
window['t'+indexMass] = Math.atan2(tmpX, tmpY);
console.log('printf', window['t'+indexMass]);
// Compute new positions of disks
dragComputePositions();
// Update drawing
DrawPend(canvas);
// Highlight dragging disk
fillDisk(indexMass, 'pink');
}
}
You can not move the OS mouse position. You can hide the mouse canvas.style.cursor = "none"; and then draw a mouse on the canvas your self but it will lag behind by one frame because when you get the mouse coordinates the OS has already placed the mouse at that position, and if you use requestAnimationFrame (RAF) the next presentation of the canvas will be at the next display refresh interval. If you don't use RAF you may or may not present the canvas on the current display refresh, but you will get occasional flicker and shearing.
To solve the problem (which is subjective) draw a line from the rotation point through the ball to the mouse position this will at least give the user some feedback as to what is happening.
I would also add some handles to the balls so you could change the mass (volume of sphere * density) and the length of axis.. The resize cursors are a problem as the will not match the direction of required movement when the angles have changes. You would need to find one closest to the correct angle or render a cursor to a canvas and use that.
Example code shows what I mean. (does not include sim) Move mouse over balls to move, when over you will also see two circles appear to change distance and radius (mass)
/*-------------------------------------------------------------------------------------
answer code
---------------------------------------------------------------------------------------*/
var balls = [];
var startX,startY;
var mouseOverBallIndex = -1;
var mouseOverDist = false;
var mouseOverMass = false;
const DRAG_CURSOR = "move";
const MASS_CURSOR = "ew-resize";
const DIST_CURSOR = "ns-resize";
var dragging = false;
var dragStartX = 0;
var dragStartY = 0;
function addBall(dist,radius){
balls.push({
dist : dist,
radius : Math.max(10,radius),
angle : -Math.PI / 2,
x : 0,
y : 0,
mass : (4/3) * radius * radius * radius * Math.PI,
});
}
function drawBalls(){
var i = 0;
var len = balls.length;
var x,y,dist,b,minDist,index,cursor;
ctx.lineWidth = 2;
ctx.strokeStyle = "black";
ctx.fillStyle = "blue"
ctx.beginPath();
x = startX;
y = startY;
ctx.moveTo(x, y)
for(; i < len; i += 1){
b = balls[i];
x += Math.cos(b.angle) * b.dist;
y += Math.sin(b.angle) * b.dist;
ctx.lineTo(x, y);
b.x = x;
b.y = y;
}
ctx.stroke();
minDist = Infinity;
index = -1;
for(i = 0; i < len; i += 1){
b = balls[i];
ctx.beginPath();
ctx.arc(b.x, b.y, b.radius, 0, Math.PI * 2);
ctx.fill();
if(!dragging){
x = b.x - mouse.x;
y = b.y - mouse.y;
dist = Math.sqrt(x * x + y * y);
if(dist < b.radius + 5 && dist < minDist){
minDist = dist;
index = i;
}
}
}
if(!dragging){
mouseOverBallIndex = index;
if(index !== -1){
cursor = DRAG_CURSOR;
b = balls[index];
ctx.fillStyle = "Red"
ctx.beginPath();
ctx.arc(b.x, b.y, b.radius, 0, Math.PI * 2);
ctx.fill();
dx = b.x - Math.cos(b.angle) * b.radius;
dy = b.y - Math.sin(b.angle) * b.radius;
x = dx - mouse.x;
y = dy - mouse.y;
dist = Math.sqrt(x * x + y * y);
ctx.beginPath();
if(dist < 6){
ctx.strokeStyle = "Yellow"
mouseOverDist = true;
ctx.arc(dx, dy, 12, 0, Math.PI * 2);
cursor = DIST_CURSOR;
}else{
ctx.strokeStyle = "black"
mouseOverDist = false;
ctx.arc(dx, dy, 5, 0, Math.PI * 2);
}
ctx.stroke();MASS_CURSOR
dx = b.x - Math.cos(b.angle + Math.PI/2) * b.radius;
dy = b.y - Math.sin(b.angle + Math.PI/2) * b.radius;
x = dx - mouse.x;
y = dy - mouse.y;
dist = Math.sqrt(x * x + y * y);
ctx.beginPath();
if(dist < 6){
ctx.strokeStyle = "Yellow"
mouseOverMass = true;
ctx.arc(dx, dy, 12, 0, Math.PI * 2);
cursor = MASS_CURSOR;
}else{
ctx.strokeStyle = "black"
mouseOverMass = false;
ctx.arc(dx, dy, 5, 0, Math.PI * 2);
}
ctx.stroke();
canvas.style.cursor = cursor;
}else{
canvas.style.cursor = "default";
}
}else{
b = balls[mouseOverBallIndex];
ctx.fillStyle = "Yellow"
ctx.beginPath();
ctx.arc(b.x, b.y, b.radius, 0, Math.PI * 2);
ctx.fill();
}
}
function display(){ // put code in here
var x,y,b
if(balls.length === 0){
startX = canvas.width/2;
startY = canvas.height/2;
addBall((startY * 0.8) * (1/4), startY * 0.04);
addBall((startY * 0.8) * (1/3), startY * 0.04);
addBall((startY * 0.8) * (1/2), startY * 0.04);
}
ctx.setTransform(1,0,0,1,0,0); // reset transform
ctx.globalAlpha = 1; // reset alpha
ctx.clearRect(0,0,w,h);
if((mouse.buttonRaw & 1) && mouseOverBallIndex > -1){
b = balls[mouseOverBallIndex];
if(dragging === false){
dragging = true;
dragStartX = balls[mouseOverBallIndex].x;
dragStartY = balls[mouseOverBallIndex].y;
}else{
b = balls[mouseOverBallIndex];
if(mouseOverBallIndex === 0){
x = startX;
y = startY;
}else{
x = balls[mouseOverBallIndex-1].x
y = balls[mouseOverBallIndex-1].y
}
if(mouseOverDist){
var dist = Math.sqrt(Math.pow(x-mouse.x,2)+Math.pow(y-mouse.y,2));
b.dist = dist + b.radius;
}else
if(mouseOverMass){
var dist = Math.sqrt(Math.pow(dragStartX-mouse.x,2)+Math.pow(dragStartY-mouse.y,2));
b.radius = Math.max(10,dist);
b.mass = dist * dist * dist * (4/3) * Math.PI;
}else{
b.angle = Math.atan2(mouse.y - y, mouse.x - x);
ctx.beginPath();
ctx.lineWidth = 1;
ctx.strokeStyle = "grey";
ctx.moveTo(x,y);
ctx.lineTo(mouse.x, mouse.y);
ctx.stroke();
}
}
}else if(dragging){
dragging = false;
}
drawBalls();
}
/*-------------------------------------------------------------------------------------
answer code END
---------------------------------------------------------------------------------------*/
/** SimpleFullCanvasMouse.js begin **/
const CANVAS_ELEMENT_ID = "canv";
const U = undefined;
var w, h, cw, ch; // short cut vars
var canvas, ctx, mouse;
var globalTime = 0;
var createCanvas, resizeCanvas, setGlobals;
var L = typeof log === "function" ? log : function(d){ console.log(d); }
createCanvas = function () {
var c,cs;
cs = (c = document.createElement("canvas")).style;
c.id = CANVAS_ELEMENT_ID;
cs.position = "absolute";
cs.top = cs.left = "0px";
cs.zIndex = 1000;
document.body.appendChild(c);
return c;
}
resizeCanvas = function () {
if (canvas === U) { canvas = createCanvas(); }
canvas.width = window.innerWidth;
canvas.height = window.innerHeight;
ctx = canvas.getContext("2d");
if (typeof setGlobals === "function") { setGlobals(); }
}
setGlobals = function(){ cw = (w = canvas.width) / 2; ch = (h = canvas.height) / 2; balls.length = 0; }
mouse = (function(){
function preventDefault(e) { e.preventDefault(); }
var mouse = {
x : 0, y : 0, w : 0, alt : false, shift : false, ctrl : false, buttonRaw : 0,
over : false, // mouse is over the element
bm : [1, 2, 4, 6, 5, 3], // masks for setting and clearing button raw bits;
mouseEvents : "mousemove,mousedown,mouseup,mouseout,mouseover,mousewheel,DOMMouseScroll".split(",")
};
var m = mouse;
function mouseMove(e) {
var t = e.type;
m.x = e.offsetX; m.y = e.offsetY;
if (m.x === U) { m.x = e.clientX; m.y = e.clientY; }
m.alt = e.altKey; m.shift = e.shiftKey; m.ctrl = e.ctrlKey;
if (t === "mousedown") { m.buttonRaw |= m.bm[e.which-1]; }
else if (t === "mouseup") { m.buttonRaw &= m.bm[e.which + 2]; }
else if (t === "mouseout") { m.buttonRaw = 0; m.over = false; }
else if (t === "mouseover") { m.over = true; }
else if (t === "mousewheel") { m.w = e.wheelDelta; }
else if (t === "DOMMouseScroll") { m.w = -e.detail; }
if (m.callbacks) { m.callbacks.forEach(c => c(e)); }
e.preventDefault();
}
m.addCallback = function (callback) {
if (typeof callback === "function") {
if (m.callbacks === U) { m.callbacks = [callback]; }
else { m.callbacks.push(callback); }
} else { throw new TypeError("mouse.addCallback argument must be a function"); }
}
m.start = function (element, blockContextMenu) {
if (m.element !== U) { m.removeMouse(); }
m.element = element === U ? document : element;
m.blockContextMenu = blockContextMenu === U ? false : blockContextMenu;
m.mouseEvents.forEach( n => { m.element.addEventListener(n, mouseMove); } );
if (m.blockContextMenu === true) { m.element.addEventListener("contextmenu", preventDefault, false); }
}
m.remove = function () {
if (m.element !== U) {
m.mouseEvents.forEach(n => { m.element.removeEventListener(n, mouseMove); } );
if (m.contextMenuBlocked === true) { m.element.removeEventListener("contextmenu", preventDefault);}
m.element = m.callbacks = m.contextMenuBlocked = U;
}
}
return mouse;
})();
var done = function(){
window.removeEventListener("resize",resizeCanvas)
mouse.remove();
document.body.removeChild(canvas);
canvas = ctx = mouse = U;
L("All done!")
}
resizeCanvas(); // create and size canvas
mouse.start(canvas,true); // start mouse on canvas and block context menu
window.addEventListener("resize",resizeCanvas); // add resize event
function update(timer){ // Main update loop
globalTime = timer;
display(); // call demo code
// continue until mouse right down
if (!(mouse.buttonRaw & 2)) { requestAnimationFrame(update); } else { done(); }
}
requestAnimationFrame(update);
/** SimpleFullCanvasMouse.js end **/
(Posted a solution from the question author to move it to the answer space).
Problem solved! I forgot to take into account the position of "indexMass-1" disk to compute the new angle with Math.atan2 function.

Categories

Resources