I'm trying to make an animation inside a canvas: here, a numbered circle must be drawn and move from left to right one single time, disappearing as soon as it reaches the end of animation.
For now I managed to animate it in loop, but I need to animate at the same time (or with a set delay) multiple numbered circles, strating in different rows (changing y position) so they wont overlap.
Any idea how can I manage this? my JS code is the following:
// Single Animated Circle - Get Canvas element by Id
var canvas = document.getElementById("canvas");
// Set Canvas dimensions
canvas.width = 300;
canvas.height = 900;
// Get drawing context
var ctx = canvas.getContext("2d");
// Radius
var radius = 13;
// Starting Position
var x = radius;
var y = radius;
// Speed in x and y direction
var dx = 1;
var dy = 0;
// Generate random number
var randomNumber = Math.floor(Math.random() * 60) + 1;
if (randomNumber > 0 && randomNumber <= 10) {
ctx.strokeStyle = "#0b0bf1";
} else if (randomNumber > 10 && randomNumber <= 20) {
ctx.strokeStyle = "#f10b0b";
} else if (randomNumber > 20 && randomNumber <= 30) {
ctx.strokeStyle = "#0bf163";
} else if (randomNumber > 30 && randomNumber <= 40) {
ctx.strokeStyle = "#f1da0b";
} else if (randomNumber > 40 && randomNumber <= 50) {
ctx.strokeStyle = "#950bf1";
} else if (randomNumber > 50 && randomNumber <= 60) {
ctx.strokeStyle = "#0bf1e5";
}
function animate3() {
requestAnimationFrame(animate3);
ctx.clearRect(0, 0, 300, 900);
if (x + radius > 300 || x - radius < 0) {
x = radius;
}
x += dx;
ctx.beginPath();
ctx.arc(x, y, 12, 0, Math.PI * 2, false);
ctx.stroke();
ctx.fillText(randomNumber, x - 5, y + 3);
}
// Animate the Circle
animate3();
<canvas id="canvas"></canvas>
Here is a solution which doesn't use classes as such and separates the animation logic from the updating - which can be useful if you want more precise control over timing.
// Some helper functions
const clamp = (number, min, max) => Math.min(Math.max(number, min), max);
// Choose and remove random member of arr with equal probability
const takeRandom = arr => arr.splice(parseInt(Math.random() * arr.length), 1)[0]
// Call a function at an interval, passing the amount of time that has passed since the last call
function update(callBack, interval) {
let now = performance.now();
let last;
setInterval(function() {
last = now;
now = performance.now();
callBack((now - last) / 1000);
})
}
const settings = {
width: 300,
height: 150,
radius: 13,
gap: 5,
circles: 5,
maxSpeed: 100,
colors: ["#0b0bf1", "#f10b0b", "#0bf163", "#f1da0b", "#950bf1", "#0bf1e5"]
};
const canvas = document.getElementById("canvas");
canvas.width = settings.width;
canvas.height = settings.height;
const ctx = canvas.getContext("2d");
// Set circle properties
const circles = [...Array(settings.circles).keys()].map(i => ({
number: i + 1,
x: settings.radius,
y: settings.radius + (settings.radius * 2 + settings.gap) * i,
radius: settings.radius,
dx: settings.maxSpeed * Math.random(), // This is the speed in pixels per second
dy: 0,
color: takeRandom(settings.colors)
}));
function drawCircle(circle) {
ctx.strokeStyle = circle.color;
ctx.beginPath();
ctx.arc(circle.x, circle.y, circle.radius, 0, Math.PI * 2, false);
ctx.stroke();
ctx.fillText(circle.number, circle.x - 5, circle.y + 3);
}
function updateCircle(circle, dt) {
// Update a circle's position after dt seconds
circle.x = clamp(circle.x + circle.dx * dt, circle.radius + 1, settings.width - circle.radius - 1);
circle.y = clamp(circle.y + circle.dy * dt, circle.radius + 1, settings.height - circle.radius - 1);
}
function animate() {
ctx.clearRect(0, 0, settings.width, settings.height);
circles.forEach(drawCircle);
requestAnimationFrame(animate);
}
update(dt => circles.forEach(circle => updateCircle(circle, dt)), 50);
animate();
<canvas id="canvas" style="border: solid 1px black"></canvas>
Here I transformed your sample code into a class ...
We pass all the data as a parameter, you can see that in the constructor, I simplified a lot of your code to keep it really short, but all the same drawing you did is there in the draw function
Then all we need to do is create instances of this class and call the draw function inside that animate3 loop you already have.
You had a hardcoded value on the radius:
ctx.arc(x, y, 12, 0, Math.PI * 2, false)
I assume that was a mistake and fix it on my code
var canvas = document.getElementById("canvas");
canvas.width = canvas.height = 300;
var ctx = canvas.getContext("2d");
class Circle {
constructor(data) {
this.data = data
}
draw() {
if (this.data.x + this.data.radius > 300 || this.data.x - this.data.radius < 0) {
this.data.x = this.data.radius;
}
this.data.x += this.data.dx;
ctx.beginPath();
ctx.arc(this.data.x, this.data.y, this.data.radius, 0, Math.PI * 2, false);
ctx.stroke();
ctx.fillText(this.data.number, this.data.x - 5, this.data.y + 3);
}
}
circles = []
circles.push(new Circle({radius:13, x: 10, y: 15, dx: 1, dy: 0, number: "1"}))
circles.push(new Circle({radius:10, x: 10, y: 50, dx: 2, dy: 0, number: "2"}))
function animate3() {
requestAnimationFrame(animate3);
ctx.clearRect(0, 0, canvas.width, canvas.height);
circles.forEach(item => item.draw());
}
animate3();
<canvas id="canvas"></canvas>
Code should be easy to follow let me know if you have any questions
Related
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);
});
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>
I am trying to get alert when any of the balls turn red. Every time when the balls turn red, alert should come. Anyone can you help me out to get this thing?
And i want the canvas outline stroke as black color. How to do that? I tried adding > canvas.stokestyle also but it not showing anything.
function getBall(xVal, yVal, dxVal, dyVal, rVal, colorVal) {
var ball = {
x: xVal,
y: yVal,
dx: dxVal,
dy: dyVal,
r: rVal,
color: colorVal
};
return ball;
}
var canvas = document.getElementById("Canvas");
var ctx = canvas.getContext("2d");
ctx.globalAlpha = 0.8
var containerR = 150;
canvas.width = canvas.height = containerR * 2;
canvas.style["border-radius"] = containerR + "px";
var balls = [
getBall(canvas.width / 2, canvas.height - 30, 2, 2, 8, "#32CD32"),
getBall(canvas.width / 3, canvas.height - 50, 2, 2, 8, "#32CD32"),
getBall(canvas.width / 4, canvas.height - 60, 2, 2, 8, "#32CD32"),
getBall(canvas.width / 2, canvas.height / 5, 2, 2, 8, "#32CD32")
];
function drawBall(curBall) {
ctx.beginPath();
ctx.arc(curBall.x, curBall.y, curBall.r, 0, Math.PI * 2, false);
ctx.fillStyle = curBall.color;
ctx.fill();
ctx.closePath();
}
function updatePos(curBall) {
curBall.x += curBall.dx;
curBall.y += curBall.dy;
var dx = curBall.x - containerR;
var dy = curBall.y - containerR;
if (Math.sqrt(dx * dx + dy * dy) >= containerR - curBall.r) {
var v = Math.sqrt(curBall.dx * curBall.dx + curBall.dy * curBall.dy);
var angleToCollisionPoint = Math.atan2(-dy, dx);
var oldAngle = Math.atan2(-curBall.dy, curBall.dx);
var newAngle = 2 * angleToCollisionPoint - oldAngle;
curBall.dx = -v * Math.cos(newAngle);
curBall.dy = v * Math.sin(newAngle);
}
}
function ballsMeet(b1, b2) {
return (Math.hypot(Math.abs(b1.x - b2.x), Math.abs(b1.y - b2.y)) < (b1.r + b2.r))
}
function draw() {
ctx.clearRect(0, 0, canvas.width, canvas.height);
for (var i = 0; i < balls.length; i++) {
drawBall(balls[i])
updatePos(balls[i])
meet = false
for (var j = 0; j < balls.length; j++) {
if (ballsMeet(balls[i], balls[j]) && i != j) {
meet = true
balls[j].color = "red"
break
}
}
balls[i].color = (meet) ? "red" : "green"
}
requestAnimationFrame(draw);
}
draw();
canvas {
background: #eee;
margin: 0 auto;
}
<html>
<canvas id="Canvas"></canvas>
<div id="x"></div>
<div id="y"></div>
<div id="dx"></div>
<div id="dy"></div>
</html>
Thanks in advance
Don't use alert() when working with RequestAnimationFrame
Don't color balls inside draw (BTW call it engine since it's a RAF recursion), use the drawBall instead.
Given the above, on collision, set the ball property collider to be the colliding ball - and paint accordingly inside drawBall - that way you can restore to the original color once the ball exits collision: ctx.fillStyle = ball.collider ? "red" : ball.color;
You could use CSS radius and box-shadow on the CANVAS element
const ctx = document.getElementById("Canvas").getContext("2d");
const containerR = 90;
const size = containerR * 2
ctx.canvas.width = ctx.canvas.height = size;
ctx.globalAlpha = 0.8
const getBall = (x, y, dx, dy, r, color) => ({x, y, dx, dy, r, color});
const balls = [
getBall(size / 2, size - 30, 1, 1, 30, "Blue"),
getBall(size / 3, size - 50, 1, 1, 20, "Yellow"),
getBall(size / 4, size - 60, 1, 1, 10, "Fuchsia"),
getBall(size / 2, size / 5, 1, 1, 8, "Green"),
];
const drawBall = (ball) => {
ctx.beginPath();
ctx.arc(ball.x, ball.y, ball.r, 0, Math.PI * 2, false);
ctx.fillStyle = ball.collider ? "red" : ball.color;
ctx.fill();
ctx.closePath();
}
const updatePos = (ball) => {
ball.x += ball.dx;
ball.y += ball.dy;
const dx = ball.x - containerR;
const dy = ball.y - containerR;
if (Math.sqrt(dx * dx + dy * dy) >= containerR - ball.r) {
const v = Math.sqrt(ball.dx * ball.dx + ball.dy * ball.dy);
const angleToCollisionPoint = Math.atan2(-dy, dx);
const oldAngle = Math.atan2(-ball.dy, ball.dx);
const newAngle = 2 * angleToCollisionPoint - oldAngle;
ball.dx = -v * Math.cos(newAngle);
ball.dy = v * Math.sin(newAngle);
}
}
const collides = (a, b) => (Math.hypot(Math.abs(a.x - b.x), Math.abs(a.y - b.y)) < (a.r + b.r));
function engine() {
console.clear(); // Clear console test messages
ctx.clearRect(0, 0, ctx.canvas.width, ctx.canvas.height);
balls.forEach((a, ai) => {
a.collider = undefined;
balls.forEach((b, bi) => {
if (bi === ai) return; // Don't look at self
if (collides(a, b)) a.collider = b; // Store the colliding B ball
});
if (a.collider) { // If ball has a collider:
console.log(`${a.color[0]} → ← ${a.collider.color[0]}`);
}
updatePos(a);
drawBall(a);
});
requestAnimationFrame(engine);
}
engine();
canvas {
background: #eee;
margin: 0 auto;
border-radius: 50%;
box-shadow: 0 0 0 4px #000;
}
<canvas id="Canvas"></canvas>
In the Below code link HTML5 canvas spin wheel game. I want to stop this canvas at a user-defined position as if the user wants to stop always at 200 texts or 100 texts like that.
Currently, it is stopping at random points I want to control where to stop as in if I want to stop circle at 100 or 200 or 0 whenever I want.
How can we achieve that??? Can anyone Help!!!!!
Attached Codepen link also.
Html file
<div>
<canvas class="spin-wheel" id="canvas" width="300" height="300"></canvas>
</div>
JS file
var color = ['#ca7','#7ac','#77c','#aac','#a7c','#ac7', "#caa"];
var label = ['10', '200','50','100','5','500',"0"];
var slices = color.length;
var sliceDeg = 360/slices;
var deg = 270;
var speed = 5;
var slowDownRand = 0;
var ctx = canvas.getContext('2d');
var width = canvas.width; // size
var center = width/2; // center
var isStopped = false;
var lock = false;
function rand(min, max) {
return Math.random() * (max - min) + min;
}
function deg2rad(deg){ return deg * Math.PI/180; }
function drawSlice(deg, color){
ctx.beginPath();
ctx.fillStyle = color;
ctx.moveTo(center, center);
ctx.arc(center, center, width/2, deg2rad(deg), deg2rad(deg+sliceDeg));
console.log(center, center, width/2, deg2rad(deg), deg2rad(deg+sliceDeg))
ctx.lineTo(center, center);
ctx.fill();
}
function drawText(deg, text) {
ctx.save();
ctx.translate(center, center);
ctx.rotate(deg2rad(deg));
ctx.textAlign = "right";
ctx.fillStyle = "#fff";
ctx.font = 'bold 30px sans-serif';
ctx.fillText(text, 130, 10);
ctx.restore();
}
function drawImg() {
ctx.clearRect(0, 0, width, width);
for(var i=0; i<slices; i++){
drawSlice(deg, color[i]);
drawText(deg+sliceDeg/2, label[i]);
deg += sliceDeg;
}
}
// ctx.rotate(360);
function anim() {
isStopped = true;
deg += speed;
deg %= 360;
// Increment speed
if(!isStopped && speed<3){
speed = speed+1 * 0.1;
}
// Decrement Speed
if(isStopped){
if(!lock){
lock = true;
slowDownRand = rand(0.994, 0.998);
}
speed = speed>0.2 ? speed*=slowDownRand : 0;
}
// Stopped!
if(lock && !speed){
var ai = Math.floor(((360 - deg - 90) % 360) / sliceDeg); // deg 2 Array Index
console.log(slices)
ai = (slices+ai)%slices; // Fix negative index
return alert("You got:\n"+ label[ai] ); // Get Array Item from end Degree
// ctx.arc(150,150,150,8.302780584487312,9.200378485512967);
// ctx.fill();
}
drawImg();
window.requestAnimationFrame(anim);
}
function start() {
anim()
}
drawImg();
Spin wheel codepen
Ease curves
If you where to plot the wheel position over time as it slows to a stop you would see a curve, a curve that looks like half a parabola.
You can get the very same curve if you plot the value of x squared in the range 0 to 1 as in the next snippet, the red line shows the plot of f(x) => x * x where 0 <= x <= 1
Unfortunately the plot is the wrong way round and needs to be mirrored in x and y. That is simple by changing the function to f(x) => 1 - (1 - x) ** 2 (Click the canvas to get the yellow line)
const size = 200;
const ctx = Object.assign(document.createElement("canvas"),{width: size, height: size / 2}).getContext("2d");
document.body.appendChild(ctx.canvas);
ctx.canvas.style.border = "2px solid black";
plot(getData());
plot(unitCurve(x => x * x), "#F00");
ctx.canvas.addEventListener("click",()=>plot(unitCurve(x => 1 - (1 - x) ** 2), "#FF0"), {once: true});
function getData(chart = []) {
var pos = 0, speed = 9, deceleration = 0.1;
while(speed > 0) {
chart.push(pos);
pos += speed;
speed -= deceleration;
}
return chart;
}
function unitCurve(f,chart = []) {
const step = 1 / 100;
var x = 0;
while(x <= 1) {
chart.push(f(x));
x += step
}
return chart;
}
function plot(chart, col = "#000") {
const xScale = size / chart.length, yScale = size / 2 / Math.max(...chart);
ctx.setTransform(xScale, 0, 0, yScale, 0, 0);
ctx.strokeStyle = col;
ctx.beginPath();
chart.forEach((y,x) => ctx.lineTo(x,y));
ctx.setTransform(1, 0, 0, 1, 0, 0);
ctx.stroke();
}
In animation this curve is an ease in.
We can create function that uses the ease function, takes the time and returns the position of the wheel. We can provide some additional values that controls how long the wheel will take to stop, the starting position and the all important stop position.
function wheelPos(currentTime, startTime, endTime, startPos, endPos) {
// first scale the current time to a value from 0 to 1
const x = (currentTime - startTime) / (endTime - startTime);
// rather than the square, we will use the square root (this flips the curve)
const xx = x ** (1 / 2);
// convert the value to a wheel position
return xx * (endPos - startPos) + startPos;
}
Demo
The demo puts it in action. Rather than using the square root the function in the demo defines the root as the constant slowDownRate = 2.6. The smaller this value the greater start speed and the slower the end speed. A value of 1 means it will move at a constant speed and then stop. The value must be > 0 and < 1
requestAnimationFrame(mainLoop);
Math.TAU = Math.PI * 2;
const size = 160;
const ctx = Object.assign(document.createElement("canvas"),{width: size, height: size}).getContext("2d");
document.body.appendChild(ctx.canvas);
const stopAt = document.createElement("div")
document.body.appendChild(stopAt);
ctx.canvas.style.border = "2px solid black";
var gTime; // global time
const colors = ["#F00","#F80","#FF0","#0C0","#08F","#00F","#F0F"];
const wheelSteps = 12;
const minSpins = 3 * Math.TAU; // min number of spins before stopping
const spinTime = 6000; // in ms
const slowDownRate = 1 / 1.8; // smaller this value the greater the ease in.
// Must be > 0
var startSpin = false;
var readyTime = 0;
ctx.canvas.addEventListener("click",() => { startSpin = !wheel.spinning });
stopAt.textContent = "Click wheel to spin";
const wheel = { // hold wheel related variables
img: createWheel(wheelSteps),
endTime: performance.now() - 2000,
startPos: 0,
endPos: 0,
speed: 0,
pos: 0,
spinning: false,
set currentPos(val) {
this.speed = (val - this.pos) / 2; // for the wobble at stop
this.pos = val;
},
set endAt(pos) {
this.endPos = (Math.TAU - (pos / wheelSteps) * Math.TAU) + minSpins;
this.endTime = gTime + spinTime;
this.startTime = gTime;
stopAt.textContent = "Spin to: "+(pos + 1);
}
};
function wheelPos(currentTime, startTime, endTime, startPos, endPos) {
const x = ((currentTime - startTime) / (endTime - startTime)) ** slowDownRate;
return x * (endPos - startPos) + startPos;
}
function mainLoop(time) {
gTime = time;
ctx.setTransform(1,0,0,1,0,0);
ctx.clearRect(0, 0, size, size);
if (startSpin && !wheel.spinning) {
startSpin = false;
wheel.spinning = true;
wheel.startPos = (wheel.pos % Math.TAU + Math.TAU) % Math.TAU;
wheel.endAt = Math.random() * wheelSteps | 0;
} else if (gTime <= wheel.endTime) { // wheel is spinning get pos
wheel.currentPos = wheelPos(gTime, wheel.startTime, wheel.endTime, wheel.startPos, wheel.endPos);
readyTime = gTime + 1500;
} else { // wobble at stop
wheel.speed += (wheel.endPos - wheel.pos) * 0.0125;
wheel.speed *= 0.95;
wheel.pos += wheel.speed;
if (wheel.spinning && gTime > readyTime) {
wheel.spinning = false;
stopAt.textContent = "Click wheel to spin";
}
}
// draw wheel
ctx.setTransform(1,0,0,1,size / 2, size / 2);
ctx.rotate(wheel.pos);
ctx.drawImage(wheel.img, -size / 2 , - size / 2);
// draw marker shadow
ctx.setTransform(1,0,0,1,1,4);
ctx.fillStyle = "#0004";
ctx.beginPath();
ctx.lineTo(size - 13, size / 2);
ctx.lineTo(size, size / 2 - 7);
ctx.lineTo(size, size / 2 + 7);
ctx.fill();
// draw marker
ctx.setTransform(1,0,0,1,0,0);
ctx.fillStyle = "#F00";
ctx.beginPath();
ctx.lineTo(size - 13, size / 2);
ctx.lineTo(size, size / 2 - 7);
ctx.lineTo(size, size / 2 + 7);
ctx.fill();
requestAnimationFrame(mainLoop);
}
function createWheel(steps) {
const ctx = Object.assign(document.createElement("canvas"),{width: size, height: size}).getContext("2d");
const s = size, s2 = s / 2, r = s2 - 4;
var colIdx = 0;
for (let a = 0; a < Math.TAU; a += Math.TAU / steps) {
const aa = a - Math.PI / steps;
ctx.fillStyle = colors[colIdx++ % colors.length];
ctx.beginPath();
ctx.moveTo(s2, s2);
ctx.arc(s2, s2, r, aa, aa + Math.TAU / steps);
ctx.fill();
}
ctx.fillStyle = "#FFF";
ctx.beginPath();
ctx.arc(s2, s2, 12, 0, Math.TAU);
ctx.fill();
ctx.beginPath();
ctx.lineWidth = 2;
ctx.arc(s2, s2, r, 0, Math.TAU);
ctx.moveTo(s2 + 12, s2);
ctx.arc(s2, s2, 12, 0, Math.TAU);
for (let a = 0; a < Math.TAU; a += Math.TAU / steps) {
const aa = a - Math.PI / steps;
ctx.moveTo(Math.cos(aa) * 12 + s2, Math.sin(aa) * 12 + s2);
ctx.lineTo(Math.cos(aa) * r + s2, Math.sin(aa) * r + s2);
}
//ctx.fill("evenodd");
ctx.stroke();
ctx.fillStyle = "#000";
ctx.font = "13px arial black";
ctx.textAlign = "center";
ctx.textBaseline = "middle";
const tr = r - 8;
var idx = 1;
for (let a = 0; a < Math.TAU; a += Math.TAU / steps) {
const dx = Math.cos(a);
const dy = Math.sin(a);
ctx.setTransform(dy, -dx, dx, dy, dx * (tr - 4) + s2, dy * (tr - 4) + s2);
ctx.fillText(""+ (idx ++), 0, 0);
}
return ctx.canvas;
}
body { font-family: arial }
I am aiming to incorporate easing into time based move in my project and may need some help doing so.
For the time being i am using simple formula of x pixels per second.
...
speed: 100,
now: undefined,
delta: undefined,
then: undefined,
setDelta: function() {
this.now = Date.now();
this.delta = (this.now - this.then) / 1000;
this.then = this.now;
},
...
var slice = this.speed * this.delta;
this.x += Math.cos(rad) * slice;
this.y += Math.sin(rad) * slice;
By doing so my object is moving with 100 pixels per second.
The animation however is very boring, therefore an idea to make it more interesting by making it to start slow, accelerate to half of the distance and then start slowing again until it reaches destination.
I found this list of easing functions for javascript click.
I think that the one that seems accurate would be some smooth sine, like this one:
easeInOutSin: function (t) {
return (1 + Math.sin(Math.PI * t - Math.PI / 2)) / 2;
}
the problem is however that i cant figure out how to "connect" this formula to my code (presented above).
On the link the guys claim that t is parameter from 0 to 1 and i am thinking that probably what needs to vary is the speed. Maybe someone could help out.
this is a demo snippet for experimenting:
let distance = (p) => Math.sqrt((p.x - p.dx) * (p.x - p.dx) + (p.y - p.dy) * (p.y - p.dy)),
rftv = (p) => Math.atan2(p.dy - p.y, p.dx - p.x);
let cvs = document.createElement('canvas'),
ctx = cvs.getContext('2d'),
w = cvs.width = 700,
h = cvs.height = 200,
cx = w / 2,
cy = h / 2;
let obj = {
x: 100,
y: cy,
speed: 100,
dx: 600,
dy: cy,
run: function() {
if(!this.moving) { return; }
let d = distance(this);
if(d < 1) {
this.end();
}
this.setDelta();
var slice = this.speed * this.delta;
let rad = rftv(this);
this.x += Math.cos(rad) * slice;
this.y += Math.sin(rad) * slice;
},
now: undefined,
delta: undefined,
then: undefined,
setDelta: function() {
this.now = Date.now();
this.delta = (this.now - this.then) / 1000;
this.then = this.now;
},
moving: false,
start: function() {
this._started_ = Date.now();
this.then = Date.now();
this.moving = true;
},
end: function() {
this.moving = false;
console.log( Date.now() - this._started_, 'should be close to 5000' );
}
};
let render = () => {
ctx.fillStyle = '#ccc';
ctx.fillRect(0, 0, w, h);
ctx.beginPath();
ctx.arc(obj.x, obj.y, 10, 0, Math.PI * 2);
ctx.closePath();
ctx.strokeStyle = 'red';
ctx.stroke();
obj.run();
requestAnimationFrame(render);
};
document.body.appendChild(cvs);
render();
obj.start();
The easing function you picked is just in function of time. It means it returns a ratio from 0 to 1 depending on a time that is also in a range from 0 to 1. It means you have to calculate the ratio of time elapsed compared to the total animation time you want. Then to calculate the position you need to apply the returned ratio to the total distance you want to go (this.dx - this.startX) and add it to start position.
Note that in the following examples I ignored your rad and this.then calculations, i didn't really see what you meant with rad, and as you see easing must be in function of a total distance to go and total animation time. So there is no notion of speed either, or you have to apply it to the total distance/animation time instead.
let distance = (p) => Math.sqrt((p.x - p.dx) * (p.x - p.dx) + (p.y - p.dy) * (p.y - p.dy)),
rftv = (p) => Math.atan2(p.dy - p.y, p.dx - p.x),
easeInOutSin = function (t) {
return (1 + Math.sin(Math.PI * t - Math.PI / 2)) / 2;
};
let cvs = document.createElement('canvas'),
ctx = cvs.getContext('2d'),
w = cvs.width = 700,
h = cvs.height = 200,
cx = w / 2,
cy = h / 2;
let obj = {
x: 100,
startX: 100,
y: cy,
//speed: 100,
dx: 600,
dy: cy,
run: function() {
if(!this.moving) { return; }
let d = distance(this);
if(d < 1) {
this.end();
}
this.setDelta();
/*var slice = this.speed * this.delta;
let rad = rftv(this);
this.x += Math.cos(rad) * slice;*/
this.x = this.startX + (this.delta * (this.dx - this.startX));
//this.y += Math.sin(rad) * slice;
},
now: undefined,
delta: undefined,
//then: undefined,
setDelta: function() {
this.now = Date.now();
this.delta = easeInOutSin( (this.now - this._started_) / 5000 ); //(this.now - this.then) / 1000;
//this.then = this.now;
},
moving: false,
start: function() {
this._started_ = Date.now();
this.then = Date.now();
this.moving = true;
},
end: function() {
this.moving = false;
console.log( Date.now() - this._started_, 'should be close to 5000' );
}
};
let render = () => {
ctx.fillStyle = '#ccc';
ctx.fillRect(0, 0, w, h);
ctx.beginPath();
ctx.arc(obj.x, obj.y, 10, 0, Math.PI * 2);
ctx.closePath();
ctx.strokeStyle = 'red';
ctx.stroke();
obj.run();
if(obj.moving){ requestAnimationFrame(render); }
};
document.body.appendChild(cvs);
obj.start();
render();
Here is a second example with a more advanced easing function adapted from this answer that takes 4 parameters: time elapsed, starting and ending values and total animation time. Returned value is directly your x position.
EDIT: fixed applied parameters, should be 0 and total distance, and then you add it to starting position.
let distance = (p) => Math.sqrt((p.x - p.dx) * (p.x - p.dx) + (p.y - p.dy) * (p.y - p.dy)),
rftv = (p) => Math.atan2(p.dy - p.y, p.dx - p.x),
easeInOutSine = (t, startVal, endVal, totalTime) => (-endVal/2 * (Math.cos(Math.PI*t/totalTime) - 1) + startVal);
let cvs = document.createElement('canvas'),
ctx = cvs.getContext('2d'),
w = cvs.width = 700,
h = cvs.height = 200,
cx = w / 2,
cy = h / 2;
let obj = {
x: 100,
startX: 100,
y: cy,
//speed: 100,
dx: 600,
dy: cy,
run: function() {
if(!this.moving) { return; }
let d = distance(this);
if(d < 1) {
this.end();
}
this.setDelta();
/*var slice = this.speed * this.delta;
let rad = rftv(this);
this.x += Math.cos(rad) * slice;*/
this.x = this.startX + this.delta;
//this.y += Math.sin(rad) * slice;
},
now: undefined,
delta: undefined,
//then: undefined,
setDelta: function() {
this.now = Date.now();
this.delta = easeInOutSine((this.now - this._started_), 0, (this.dx - this.startX), 5000);//(this.now - this.then) / 1000;
//this.then = this.now;
},
moving: false,
start: function() {
this._started_ = Date.now();
this.then = Date.now();
this.moving = true;
},
end: function() {
this.moving = false;
console.log( Date.now() - this._started_, 'should be close to 5000' );
}
};
let render = () => {
ctx.fillStyle = '#ccc';
ctx.fillRect(0, 0, w, h);
ctx.beginPath();
ctx.arc(obj.x, obj.y, 10, 0, Math.PI * 2);
ctx.closePath();
ctx.strokeStyle = 'red';
ctx.stroke();
obj.run();
if(obj.moving){ requestAnimationFrame(render); }
};
document.body.appendChild(cvs);
obj.start();
render();
Hope you understand better how it works, good luck!
Additional note: I also inversed obj.start(); and render(); and added a condition to requestAnimationFrame to avoid endless loop.