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.
Related
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
I've got an animation that runs great the first few times on safari. But after each time the loop is triggered it slows down slightly. On chrome I don't experience the slow down. Is there some trick I'm needing to utilize for safari?
External link: Codepen
Here is my JS example:
let canvas = document.querySelector('.canvas');
let ctx = canvas.getContext('2d');
let scratch = document.createElement('canvas');
let ctxS = scratch.getContext('2d', { alpha: false });
let vw = window.innerWidth;
let vh = window.innerHeight;
let circleRadius = 50;
let circleSpacing = 3;
let stepDistanceX;
let stepDistanceY;
let originCircle;
let clickNum = 0;
let circles = [];
// Transition vars.
let frame;
let isZooming = false;
let destination;
let dx;
let dy;
let ds;
let dt = 0;
let zoomingImage;
// For matrix circles.
function setCircleSizes() {
if (vw < 600) {
circleRadius = 20;
circleSpacing = 2.5;
}
else if (vw < 900) {
circleRadius = 40;
circleSpacing = 3;
}
}
// Easing funciton for animation (linear)
function easing(t) {
return t
}
// On window resize.
function resize() {
canvas.width = vw;
canvas.height = vh;
scratch.width = Math.max(vw, vh);
scratch.height = Math.max(vw, vh);
}
// Set matrix for circles.
function setCircleMatrix() {
stepDistanceX = (circleRadius * circleSpacing) + ((vw % (circleRadius * circleSpacing)) / 2);
stepDistanceY = (circleRadius * circleSpacing) + ((vh % (circleRadius * circleSpacing)) / 2);
const circlesAcross = Math.floor(vw / stepDistanceX);
const circlesDown = Math.floor(vh / stepDistanceY);
let circlesToAdd = circlesAcross * circlesDown;
circles = new Array(circlesToAdd);
while (circlesToAdd) {
const i = circles.length - circlesToAdd;
const column = ((i + 1) + circlesAcross) % circlesAcross || circlesAcross;
const row = Math.floor(i / circlesAcross) + 1;
circles[i] = {
x: ((vw - (stepDistanceX * (circlesAcross - 1))) / 2) + (stepDistanceX * (column - 1)),
y: ((vh - (stepDistanceY * (circlesDown - 1))) / 2) + (stepDistanceY * (row - 1)),
drawn: false
};
circlesToAdd--;
}
}
// Gets the closest circle.
function getClosestCircle(x, y) {
return circles[circles.map((circle, i) => {
return {dist: Math.abs(circle.x - x) + Math.abs(circle.y - y), index: i };
}).sort((a, b) => {
return a.dist - b.dist;
})[0].index]
}
// Gets the closest circles by range.
function getClosestCircles(x, y, range) {
return circles.filter(circle => {
return Math.abs(circle.x - x) + Math.abs(circle.y - y) < range;
})
}
// Handle click event.
function getPosition(event){
if (event.srcElement.tagName === "A" || isZooming) {
return true;
}
const rect = canvas.getBoundingClientRect();
const x = event.clientX - rect.left; // x == the location of the click in the document - the location (relative to the left) of the canvas in the document
const y = event.clientY - rect.top; // y == the location of the click in the document - the location (relative to the top) of the canvas in the document
if (clickNum < 1) {
// First click.
originCircle = getClosestCircle(x,y);
drawStuff([originCircle], x, y);
}
else {
// Add from origin.
drawStuff(getClosestCircles(originCircle.x, originCircle.y, Math.max(clickNum * stepDistanceX, clickNum * stepDistanceY)), x, y);
}
clickNum++;
}
// This is the zoom animation.
function zoomReset() {
// break loop if no canvas.
if (!canvas) {
return true;
}
frame = requestAnimationFrame(zoomReset);
// Loop it.
if (dt < 1 && isZooming) {
dt += 0.08; //determines speed
// Do alot of stuff in the scratch pad.
ctxS.clearRect(0, 0, scratch.width, scratch.height);
const tx = easing(dt) * dx - (((scratch.width - canvas.width) / 2) * (1 - dt));
const ty = easing(dt) * dy - (((scratch.height - canvas.height) / 2) * (1 - dt));
const ts = 1 - ds * (easing(dt) * 1);
// set elements by tx
ctxS.putImageData(zoomingImage, (scratch.width - canvas.width) / 2, (scratch.height - canvas.height) / 2);
ctxS.beginPath();
ctxS.arc(scratch.width / 2, scratch.height / 2, Math.max(scratch.width / 2, scratch.height / 2), 0, Math.PI * 2);
ctxS.clip();
ctxS.fillStyle = `rgba(255, 79, 23, ${(1 * dt) - (0.2 / (1 * (dt * 2)))})`;
ctxS.fillRect(0, 0, scratch.width, scratch.height);
// Update on main canvas.
ctx.clearRect(0, 0, vw, vh);
ctx.drawImage(scratch, Math.floor(tx), Math.floor(ty), Math.floor(scratch.width * ts), Math.floor(scratch.height * ts));
}
else if (isZooming) {
isZooming = false;
drawStuff([getClosestCircle(...destination)]);
}
}
// Draw stuff on the canvas.
function drawStuff(stuffToDraw = [], x, y) {
// Do circles.
ctx.clearRect(0, 0, vw, vh);
stuffToDraw.forEach(circle => {
ctx.fillStyle = "#FF4F17";
ctx.beginPath(); //Start path
ctx.arc(circle.x, circle.y, circleRadius, 0, Math.PI * 2, true); // Draw a point using the arc function of the canvas with a point structure.
ctx.fill(); // Close the path and fill.
circle.drawn = true;
});
// Do our zoom.
if (!circles.filter(circle => !circle.drawn).length && isZooming === false) {
originCircle = getClosestCircle(x,y);
const {x:nx, y:ny} = originCircle;
destination = [nx,ny];
ds = Math.min(1 - (circleRadius / vw), 1 - (circleRadius / vh));
dx = nx - ((scratch.width * (1 - ds)) / 2);
dy = ny - ((scratch.height * (1 - ds)) / 2);
zoomingImage = zoomingImage ? zoomingImage : ctx.getImageData(0, 0, canvas.width, canvas.height);
clickNum = 1;
dt = 0;
circles.forEach(circle => {
circle.drawn = false;
});
isZooming = true;
}
}
// Start.
canvas.addEventListener("click", getPosition);
resize();
setCircleSizes();
setCircleMatrix();
frame = requestAnimationFrame(zoomReset);
<canvas class="canvas"></canvas>
UPDATE: I've found that if I reset the scratch element after using the loop scratch = document.createElement('canvas'); resize(); ctxS = scratch.getContext('2d', { alpha: false });, the animation works as fast each time like the first time. Any ideas as to why that is the case?
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 trying to build a function that moves bullets in mini game. For the moment i am doing it in very simple way.
Have function to calculate radian angle between two points:
this.rftv = (p1, p2) => Math.atan2(p2.y - p1.y, p2.x - p1.x);
Have to different points and calculate angle between them:
var x = objects[this.destination].x,
y = objects[this.destination].y,
rad = tools.rftv( { x: this.x, y: this.y }, { x: x, y: y } );
Define speed boost:
this.attack_speed = 2.5;
Move bullet using angle and speed boost:
this.x += Math.cos(rad) * this.attack_speed;
this.y += Math.sin(rad) * this.attack_speed;
What i am trying to do is to not move bullets in linear way, i am rather trying to move bullets using sinus wave, to achieve something like this:
I have no idea how to start to build it, maybe someone could help and write a function that would take two points and make object move sinusoidal between them.
I suggest you add a few more variables to each instance:
// initialization
this.distance = 0;
// have a max amplitude of about 30-50 depending on how you want it to look
this.amplitude = (Math.random() * 2 - 1) * MAX_AMPLITUDE;
// have a fixed period somewhere between 10-50 depending on how you want it to look
this.period = 30;
this.initial = { x: this.x, y: this.y };
// on each frame
this.distance += this.attack_speed;
this.x = this.initial.x + Math.cos(this.rad) * this.distance;
this.y = this.initial.y + Math.sin(this.rad) * this.distance;
const deviation = Math.sin(this.distance * Math.PI / this.period) * this.amplitude;
this.x += Math.sin(this.rad) * deviation;
this.y -= Math.cos(this.rad) * deviation;
Turns out I had a slight error with the math, it's corrected now, along with a very basic demo below.
A positive amplitude should cause the initial trajectory of the bullet to go at an angle slightly counter-clockwise compared to the angle from point A to point B, then oscillate back and forth on the way to B.
class Bullet {
constructor({initial = {}, destination = {}, amplitude = 50, period = 30, speed = 2.5} = {}) {
let { x: ix, y: iy } = this.initial = initial;
let { x: dx, y: dy } = this.destination = destination;
this.amplitude = (Math.random() * 2 - 1) * amplitude;
this.period = period;
this.speed = speed;
this.distance = 0;
this.x = ix;
this.y = iy;
this.rad = Math.atan2(dy - iy, dx - ix);
}
update() {
this.distance += this.speed;
this.x = this.initial.x + Math.cos(this.rad) * this.distance;
this.y = this.initial.y + Math.sin(this.rad) * this.distance;
const deviation = Math.sin(this.distance * Math.PI / this.period) * this.amplitude;
this.x += Math.sin(this.rad) * deviation;
this.y -= Math.cos(this.rad) * deviation;
}
}
const canvas = document.querySelector('canvas');
const ctx = canvas.getContext('2d');
let initial = {
x: canvas.width / 4,
y: canvas.height * 3 / 4
};
let destination = {
x: canvas.width * 3 / 4,
y: canvas.height / 4
};
let bullet = new Bullet({initial, destination});
console.log(bullet.amplitude);
function draw() {
requestAnimationFrame(draw);
// ctx.clearRect(0, 0, canvas.width, canvas.height);
bullet.update();
ctx.fillStyle = '#0000FF';
ctx.fillRect(bullet.x, bullet.y, 1, 1);
ctx.fillStyle = '#FF0000';
ctx.fillRect(canvas.width / 4, canvas.height * 3 / 4, 1, 1);
ctx.fillStyle = '#00FF00';
ctx.fillRect(canvas.width *3 / 4, canvas.height / 4, 1, 1);
}
draw();
<canvas width="500" height="200"></canvas>
I have a sprite animation where I have set a stopping distance to it and want to calculate how much i have to slow the object down in that stopping distance to reach its new target speed. But at the moment I am not getting the correct result.
My code looks like this:
function updatePosition(obj,brake){
var delta = new Date().getTime() - obj.timer; //time since last frame
if(brake){
obj.velocity -= (Math.pow(obj.velocity,2) - Math.pow(obj.targetSpeed,2)) / (2 * obj.stopDist);
if(obj.velocity < obj.targetSpeed){
obj.velocity = obj.targetSpeed;
}
}
}
My problem is the sprite goes far past the stopping distance with a velocity well above the target speed.
I created a fiddle with a red dot travelling to a destination here: http://jsfiddle.net/4tLmz3ch/1/
When it travels the distance set by obj.stopDist it should be going the target speed which should be well before it reaches its destination. But i am obviously getting something incorrect with the math here.
Hope you can help explain my misunderstanding.
This problem is a lot simpler if you determine the desired acceleration ahead of time, and use that during each refresh. Then the entire code for each frame (excluding the drawing logic and assuming one dimension) just becomes:
function frame() {
var t = new Date().getTime();
var tDelta = t - obj.lastTime;
obj.lastTime = t;
obj.pos += obj.velocity * tDelta;
if (obj.velocity > obj.destVelocity) {
obj.velocity += obj.acceleration * tDelta;
}
draw();
setTimeout(frame, 1);
}
Given a starting and ending position and velocity, the formula for the acceleration required (assuming constant acceleration) is:
So initializing the object like this:
var obj = {
start: 10,
height: 200,
stopDist: 300,
dest: 500,
lastTime: new Date().getTime(),
velocity: 0.05,
destVelocity: 0.01,
pos: undefined,
acceleration: undefined
};
here is how we can kick this all off:
function start(){
var v0 = obj.velocity,
vf = obj.destVelocity,
x0 = obj.start,
xf = x0 + x.stopDist,
vDelta = vf - v0;
obj.pos = x0;
obj.acceleration = (2 * v0 * vDelta + vDelta * vDelta) / (2 * (xf - x0));
frame();
}
As I've done above, it's helpful to solve the 1d case first. Here is that, all put together.
var canvas = document.getElementById('canvas');
var test = document.getElementById('test');
var ctx = canvas.getContext('2d');
function drawDot(color, x, y) {
ctx.fillStyle = color;
ctx.fillRect(x - 2, y - 2, 4, 4);
}
function draw() {
ctx.clearRect(0, 0, canvas.width, canvas.height);
drawDot("red", obj.pos, obj.height);
drawDot("white", obj.start, obj.height);
drawDot("green", obj.dest, obj.height);
drawDot("yellow", obj.start + obj.stopDist, obj.height);
ctx.fillText("x = " + obj.pos.toFixed(5), 20, 400);
ctx.fillText("v = " + obj.velocity.toFixed(5), 20, 420);
ctx.fillText("distance traveled: " + (obj.pos - obj.start).toFixed(2), 20, 440);
}
var obj = {
start: 10,
height: 200,
stopDist: 300,
dest: 500,
lastTime: new Date().getTime(),
velocity: 0.05,
destVelocity: 0.01,
pos: undefined,
acceleration: undefined
};
function frame() {
var t = new Date().getTime(),
tDelta = t - obj.lastTime;
obj.lastTime = t;
obj.pos += obj.velocity * tDelta;
if (obj.velocity > obj.destVelocity) {
obj.velocity += obj.acceleration * tDelta;
}
draw();
setTimeout(frame, 1);
}
function start() {
var v0 = obj.velocity,
vf = obj.destVelocity,
x0 = obj.start,
xf = x0 + obj.stopDist,
vDelta = vf - v0;
obj.pos = x0;
obj.acceleration = (2 * v0 * vDelta + vDelta * vDelta) / (2 * (xf - x0));
frame();
}
start();
#canvas{
background-color:black;
}
<canvas id="canvas" width="700" height="700"></canvas>
http://jsfiddle.net/x7842xcb/3/
And here is the 2d version (brace yourself):
var canvas = document.getElementById('canvas');
var test = document.getElementById('test');
var ctx = canvas.getContext('2d');
function drawDot(color, x, y) {
ctx.fillStyle = color;
ctx.fillRect(x - 2, y - 2, 4, 4);
}
function draw() {
ctx.clearRect(0, 0, canvas.width, canvas.height);
drawDot("red", obj.pos.x, obj.pos.y);
drawDot("white", obj.start.x, obj.start.y);
drawDot("green", obj.dest.x, obj.dest.y);
drawDot("yellow", obj.stopLocation.x, obj.stopLocation.y);
var dx = obj.pos.x - obj.start.x,
dy = obj.pos.y - obj.start.y,
dist = Math.sqrt(dx * dx + dy *dy),
v = obj.velocity,
speed = Math.sqrt(v.x * v.x + v.y * v.y);
ctx.fillText("distance traveled: " + dist.toFixed(5), 20, 400);
ctx.fillText("speed: " + speed.toFixed(5), 20, 420);
}
var obj = {
start: { x: 400, y: 230 },
stopDist: 350,
dest: { x: 50, y: 330 },
lastTime: new Date().getTime(),
startSpeed: 0.05,
destSpeed: 0.1,
pos: null,
velocity: null,
destVelocity: null,
acceleration: null
};
function sign(value) {
return value > 0 ? 1 : (value < 0 ? -1 : 0);
}
function reached(start, current, dest) {
return current === dest ||
sign(current - dest) === sign(dest - start);
}
function frame() {
var t = new Date().getTime(),
tDelta = t - obj.lastTime,
v = obj.velocity,
destv = obj.destVelocity,
startv = obj.startVelocity;
obj.lastTime = t;
obj.pos.x += v.x * tDelta;
obj.pos.y += v.y * tDelta;
if (!reached(startv.x, v.x, destv.x) ||
!reached(startv.y, v.y, destv.y)) {
v.x += obj.acceleration.x * tDelta;
v.y += obj.acceleration.y * tDelta;
}
draw();
setTimeout(frame, 1);
}
function calcAcceleration(p0, pf, v0, vf) {
var vDelta = vf - v0;
return pf === p0
? 0
: (2 * v0 * vDelta + vDelta * vDelta) / (2 * (pf - p0));
}
function start() {
// positions and deltas
var start = obj.start,
dest = obj.dest,
dx = dest.x - start.x,
dy = dest.y - start.y,
totalDistance = Math.sqrt(dx * dx + dy * dy);
// x and y component ratio
var cx = dx / totalDistance,
cy = dy / totalDistance;
var stopLocation = { x: cx * obj.stopDist + start.x,
y: cy * obj.stopDist + start.y };
// velocities
var startSpeed = obj.startSpeed,
destSpeed = obj.destSpeed,
startVelocity = { x: cx * startSpeed, y: cy * startSpeed },
endVelocity = { x: cx * destSpeed, y: cy * destSpeed };
console.log(startVelocity);
console.log(endVelocity);
// acceleration
var acceleration = {
x: calcAcceleration(start.x, stopLocation.x, startVelocity.x, endVelocity.x),
y: calcAcceleration(start.y, stopLocation.y, startVelocity.y, endVelocity.y)
};
obj.pos = Object.create(start);
obj.startVelocity = startVelocity;
obj.velocity = Object.create(startVelocity);
obj.stopLocation = stopLocation;
obj.destVelocity = endVelocity;
obj.acceleration = acceleration;
frame();
}
start();
#canvas{
background-color:black;
}
<canvas id="canvas" width="700" height="700"></canvas>
http://jsfiddle.net/1r3q4oob/3/
Edit Regarding the fix I made after the fact:
The problem with my original implementation was that it would only update the velocity if both the X and Y components of the current velocity were greater than the target velocity. This would prevent the correct behavior if:
The X or Y component of both the start and end velocity were 0 (i.e. if it were travelling perfectly horizontally or vertically)
The X or Y component of the start and end velocity were negative (i.e. if it were travelling up and to the left)
The velocity needed to increase rather than decrease (i.e. the dot were speeding up to a target velocity)
I resolved this with the addition of the reached() function, which basically returns true if (a) the destination velocity is between the current velocity and the start velocity (i.e. the current velocity has gone past the destination velocity), or (b) the current velocity is equal to the destination velocity.