I'm playing around with a progress ring, and I can't seem to get it to run on a timer. I am trying to make the progress ring propagate automatically and take, say, 0.5 seconds to go from 0% to whatever percent I set (65% in this case).
I used this progress ring as a base: http://llinares.github.io/ring-progress-bar/
This is my fiddle: http://jsfiddle.net/gTtGW/
I tried using a timer function, but I may not have been integrating that properly. In the fiddle, I have added:
for (var i = 0; i< 65; i++){
range += i;
setTimeout(timer,800);
}
However, this breaks the progress ring. I thought that any time the range is updated (with the += i), the draw function would be called. What am I doing wrong? Thank you so much in advance.
If you're not planning to use the input[type=range] element, you can change your code to this:
(function (window) {
'use strict';
var document = window.document,
ring = document.getElementsByTagName('path')[0],
range = 0,
text = document.getElementsByTagName('text')[0],
Math = window.Math,
toRadians = Math.PI / 180,
r = 100;
function draw() {
// Update the wheel giving to it a value in degrees, getted from the percentage of the input value a.k.a. (value * 360) / 100
var degrees = range * 3.5999,
// Convert the degrees value to radians
rad = degrees * toRadians,
// Determine X and cut to 2 decimals
x = (Math.sin(rad) * r).toFixed(2),
// Determine Y and cut to 2 decimals
y = -(Math.cos(rad) * r).toFixed(2),
// The another half ring. Same as (deg > 180) ? 1 : 0
lenghty = window.Number(degrees > 180),
// Moveto + Arcto
descriptions = ['M', 0, 0, 'v', -r, 'A', r, r, 1, lenghty, 1, x, y, 'z'];
// Apply changes to the path
ring.setAttribute('d', descriptions.join(' '));
// Update the numeric display
text.textContent = range;
range++;
if(range > 100) {
clearInterval(timer);
}
}
// Translate the center axis to a half of total size
ring.setAttribute('transform', 'translate(' + r + ', ' + r + ')');
var timer = setInterval(draw,100);
}(this));
Basically changing range to a simple variable starting at 0, and increasing its value every time draw() is called. Creating an interval (named timer) to run every 0.1 seconds in this case (of course it's up to you), and clearing that interval from draw() when appropriate...
JSFiddle Demo
I think you want something like:
function inc() {
var val = parseInt(range.value, 10)
range.value = val + 1;
draw(); // updating the value doesn't cause `onchange`.
if (val < 100) { // don't go over 100
setTimeout(inc, 100);
}
}
inc();
Related
I am using svg.js to create an animation of a bicyle rider. Semi-complete version here: https://pedalfuriously.neocities.org/. I'm running in to a bit of a problem with moving and rotating svg elements during animation created with requestAnimationFrame (rather than the svg.js built in animation).
If you take a look at the link, and use the cadence slider to make the rider pedal very fast, and then flip the slider quickly all the way back to zero, you can see that his lower leg "jiggles" in a disconnected way. What's really doing my head in is that the postion of the legs are determined in each frame based on an absolute relation to the rotation of the cranks (rather than taking some delta time value to determine movement over that frame).
I think I've been able to confirm what aspect of my code is causing the problem. Here is a minimal example that doesn't exhibit the exact behaviour, but I think illustrates the kind of thing I think is responsible:
var draw = SVG("drawing").viewbox(0, 0, 400, 400)
var origin = {
x: 70,
y: 70
}
var length = 60
var blueLine = draw.group()
blueLine.line(0, 0, 0 + length, 0).move(origin.x, origin.y)
.stroke({
color: "#00f",
width: 4
})
blueLine.angle = 0
var greenLine = draw.group()
greenLine.line(0, 0, 0 + length, 0).move(origin.x, origin.y)
.stroke({
color: "#0f0",
width: 4
})
greenLine.angle = 0
var previous = 0
var dt = 0
var step = function(timestamp) {
dt = timestamp - previous
previous = timestamp
blueLine.angle += 0.18 * dt
blueLine.rotate(blueLine.angle, origin.x, origin.y)
var endX = Math.cos(toRad(blueLine.angle)) * length
var endY = Math.sin(toRad(blueLine.angle)) * length
// Comment out this line, and rotation works fine
greenLine.move(endX, endY)
greenLine.angle = blueLine.angle - 10
// Comment out this line, and movement works fine
greenLine.rotate(greenLine.angle, origin.x, origin.y)
// But they don't work together. If I both move and rotate
// the green line, it goes in this crazy huge arc, rather
// than rotating neatly around the end of the blue line
// as expected.
window.requestAnimationFrame(step)
}
window.requestAnimationFrame(step)
function toRad(deg) {
return deg * (Math.PI / 180)
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/svg.js/2.6.4/svg.js"></script>
<div id="drawing"></div>
Something else I noticed with my actual code is that if I move the position of the legs, it changes the severity of the problem, or even stops it altogether. If the hips are positioned all the way near the front of the bicycle, the problem is not nearly as bad. Also, if I disable rotation on the lower legs, there is no jiggling. In some positions, the lower leg will just rotate out of the screen instantly on load, even before any motion has been started.
I'm hoping for some guidance on wether I'm misunderstanding the way manipulating elements works, either in svg.js in particular, or SVG in general.
Thank you kind vector graphics experts!
Here is the actual code for the legs. The step() function would probably be the most relevant. Not sure if it will be helpful:
Rider.Leg = function(foot, front, xOffset, yOffset) {
var upper = front ? SVGE.upperLeg : SVGE.upperLegBack
var lower = front ? SVGE.lowerLeg : SVGE.lowerLegBack
this.foot = foot
this.draw = foot.draw
this.geo = {
upper: {
x: this.foot.pedal.gear.x + 150,
y: this.foot.pedal.gear.y - 750,
length: 396
},
lower: {
length: 390
}
}
this.upper = this.draw.group().svg(upper).move(this.geo.upper.x, this.geo.upper.y)
.transform({ scale: 0.95, cx: 0, cy: 0 })
this.lower = this.draw.group().svg(lower).move(this.geo.upper.x, this.geo.upper.y)
}
// Step function does not take in a time argument. Positioning of legs is based only on
// the absolute position of other elements, none of which jiggle.
Rider.Leg.prototype.step = function () {
var angle = this.pedalAngle() - Math.PI
var ha = this.scaleneAngle(this.geo.lower.length, this.geo.upper.length, this.pedalDistance())
var ka = this.scaleneAngle(this.pedalDistance(), this.geo.lower.length, this.geo.upper.length)
var x = this.geo.upper.length * Math.cos(ha + angle)
var y = this.geo.upper.length * Math.sin(ha + angle)
this.upper.rotate(Drive.toDeg(angle + ha), 0, 0)
this.lower.move(this.geo.upper.x + x, + this.geo.upper.y + y)
this.lower.rotate(Drive.toDeg(angle + ha + ka - Math.PI), 0, 0)
}
// Gets the distance between the hip joint and the pedal
Rider.Leg.prototype.pedalDistance = function () {
var pos = this.foot.getPos()
var xDist = this.geo.upper.x - pos.x
var yDist = this.geo.upper.y - pos.y
return Math.hypot(xDist, yDist)
}
// Gets the angle between the hip joint and the pedal
Rider.Leg.prototype.pedalAngle = function () {
var pos = this.foot.getPos()
var xDist = this.geo.upper.x - pos.x
var yDist = this.geo.upper.y - pos.y
return Math.atan2(yDist, xDist)
}
Rider.Leg.prototype.scaleneAngle = function (a, b, c) {
return Math.acos(((b * b) + (c * c) - (a * a)) / (2 * b * c))
}
When you call move() on a group it is internally represented as a translation. svg.js figures out crazy ways to translate the object to the new place without changing any other transformations. That often does not work out. Especially not, when you rotate.
Thats why you should avoid these absolute transformations and go with relative ones. Just call untransform before every move and go from zero. Then you can do:
greenLine.transform({x:endX, y:endY, relative: true})
To move the line by a certain amount. That should work way better.
Edited : Thanks to all for valuable time and effort. Finally I made this )) JSfiddle
I was just playing with canvas and made this. Fiddle link here.
... some code here ...
var cords = [];
for(var i = 50; i <= width; i += 100) {
for(var j = 50; j <= height; j += 100) {
cords.push({ cor: i+','+j});
}
}
console.log(cords);
var offset = 15,
speed = 0.01,
angle = 0.01;
cords.forEach(function(e1) {
e1.base = parseInt(Math.random()*25);
e1.rgb = 'rgb('+parseInt(Math.random()*255)+','+parseInt(Math.random()*255)+','+parseInt(Math.random()*255)+')';
});
setInterval(function() {
cords.forEach(function(e1) {
e1.base = parseInt(Math.random()*25);
e1.rgb = 'rgb('+parseInt(Math.random()*255)+','+parseInt(Math.random()*255)+','+parseInt(Math.random()*255)+')';
});
},5000);
function render() {
ctx.clearRect(0,0,width,height);
cords.forEach(function(e1) {
//console.log(e1);
ctx.fillStyle = e1.rgb;
ctx.beginPath();
var r = e1.base + Math.abs(Math.sin(angle)) * offset;
var v = e1.cor.split(',');
ctx.arc(v[0],v[1],r,0,Math.PI * 2, false);
ctx.fill();
});
angle += speed;
requestAnimationFrame(render);
}
render();
Was wondering if -
Coordinates can be made random, now they are fixed as you can see. After 5000 mil, balls will show up in various random cords but even at their fullest they won't touch each other.
Every ball has same speed for changing size, I want that to be different too. Meaning, After 5000 mil, they show up with different animation speeds as well.
Also any suggestion on improving code and making it better/quicker/lighter is much appreciated. Thank you !
TL;DR - See it running here.
Making the coordinates random:
This requires you to add some random displacement to the x and y coordinates. So I added a random value to the coordinates. But then a displacement of less than 1 is not noticeable. So you'd need to magnify that random number by a multiplier. That's where the randomizationFactor comes in. I have set it to 100 since that is the value by which you shift the coordinates in each iteration. So that gives a truly random look to the animation.
Making Speed Random:
This one took me a while to figure out, but the ideal way is to push a value of speed into the array of coordinates. This let's you ensure that for the duration of animation, the speed will remain constant and that gives you a smoother feel. But again multiplying the radius r with a value between 0 and 1 reduces the speed significantly for some of the circles. So I have added a multiplier to 3 to compensate slightly for that.
Ideally I'd put a 2, as the average value of Math.random() is 0.5, so a multiplier of 2 would be adequate to compensate for that. But a little experimentation showed that the multiplier of 3 was much better. You can choose the value as per your preference.
Your logic of generating the coordinates changes as follows:
for(var i = 50; i <= width;i += 100) {
for(var j = 51; j <= height;j += 100) {
var x = i + (Math.random() - 0.5)*randomizationFactor;
var y = j + (Math.random() - 0.5)*randomizationFactor;
cords.push({ cor: x+','+y, speed: Math.random()});
}
}
Your logic of enlarging the circles changes as follows:
function render() {
ctx.clearRect(0,0,width,height);
cords.forEach(function(e1) {
//console.log(e1);
ctx.fillStyle = e1.rgb;
ctx.beginPath();
var r = e1.base + Math.abs(Math.sin(angle)) * offset * e1.speed * 3;
var v = e1.cor.split(',');
ctx.arc(v[0],v[1],r,0,Math.PI * 2, false);
ctx.fill();
});
angle += speed ;
requestAnimationFrame(render);
}
Suggestion: Update the coordinates with color
I'd probably also update the location of circles every 5 seconds along with the colors. It's pretty simple to do as well. Here I've just created a function resetCoordinates that runs every 5 seconds along with the setBaseRgb function.
var cords = [];
function resetCoordinates() {
cords = [];
for(var i = 50; i <= width;i += 100) {
for(var j = 51; j <= height;j += 100) {
var x = i + (Math.random() - 0.5)*randomizationFactor;
var y = j + (Math.random() - 0.5)*randomizationFactor;
cords.push({ cor: x+','+y, speed: Math.random()});
}
}
}
UPDATE I did some fixes in your code that can make your animation more dynamic. Totally rewritten sample.
(sorry for variable name changing, imo now better)
Built in Math.random not really random, and becomes obvious when you meet animations. Try to use this random-js lib.
var randEngine = Random.engines.mt19937().autoSeed();
var rand = function(from, to){
return Random.integer(from, to)(randEngine)
}
Internal base properties to each circle would be better(more dynamic).
var circles = [];
// better to save coords as object neither as string
for(var i = 50; i <= width; i += 100)
for(var j = 50; j <= height; j += 100)
circles.push({
coords: {x:i,y:j}
});
We can adjust animation with new bouncing property.
var offset = 15,
speed = 0.005,
angle = 0.01,
bouncing = 25;
This is how setBaseRgb function may look like
function setBaseRgb(el){
el.base = rand(-bouncing, bouncing);
el.speed = rand(5, 10) * speed;
el.angle = 0;
el.rgb = 'rgb('+rand(0, 255)+','+rand(0, 255)+','+rand(0, 255)+')';
}
All your animations had fixed setInterval timeout. Better with random timeout.
cords.forEach(function(el){
// random timeout for each circle
setInterval(setBaseRgb.bind(null,el), rand(3000, 5000));
})
You forgot to add your base to your circle position
function render() {
ctx.clearRect(0,0,width,height);
circles.forEach(function(el) {
ctx.fillStyle = el.rgb;
ctx.beginPath();
var r = bouncing + el.base + Math.abs(Math.sin(el.angle)) * offset;
var coords = el.coords;
ctx.arc(
coords.x + el.base,
coords.y + el.base,
r, 0, Math.PI * 2, false
);
ctx.fill();
el.angle += el.speed;
});
requestAnimationFrame(render);
}
render();
Effect 1 JSFiddle
Adding this
if(el.angle > 1)
el.angle=0;
Results bubling effect
Effect 2 JSFiddle
Playing with formulas results this
Effect 3 JSFiddle
I am working on some logic for point-to-point spaceship travel across a cartesian map using force, acceleration and mass. The ship will accelerate and burn at 1G towards its destination, flip 180 degrees at the half-way mark, and decelerate at 1G to arrive at a relative stop at its destination.
The problem I am having is determining the (x, y) coordinate using the time traveled while the ship is either under acceleration or deceleration.
Here are the specs on the ship:
ship = {
mass: 135000, // kg
force: 1324350, // Newtons
p: { x: -1, y: -5 } // (x,y) coordinates
}
dest: {
p: { x: 15, y: 30 } // (x,y) coordinates
}
For the first part of the problem I calculate the time to destination:
var timeToDestination = function(ship, dest) {
// calculate acceleration (F = ma)
var acceleration = ship.force / ship.mass; // ~9.81 (1G)
// calculate distance between 2 points (Math.sqrt((a - x)^2+(b - y)^2))
var totalDistance = Math.sqrt(
Math.pow(dest.p.x - ship.p.x, 2) +
Math.pow(dest.p.y - ship.p.y, 2)
); // 38.48376280978771
// multiply grid system to galactic scale
var actualDistance = totalDistance * 1e9; // 1 = 1Mkm (38,483,763km) Earth -> Venus
// determine the mid-point where ship should flip and burn to decelerate
var midPoint = actualDistance / 2;
// calculate acceleration + deceleration time by solving t for each: (Math.sqrt(2d/a))
return Math.sqrt( 2 * midPoint / acceleration ) * 2; // 125,266s or 34h or 1d 10h
}
The second part is a little trickier, getting the (x, y) coordinate after delta time. This is where I get stuck, but here is what I have so far:
var plotCurrentTimeDistance = function(ship, dest, time) {
// recalculate acceleration (F = ma)
var acc = ship.force / ship.mass; //~9.81m/s^2
// recalculate total distance
var distance = Math.sqrt(
Math.pow(dest.p.x - ship.p.x, 2) +
Math.pow(dest.p.y - ship.p.y, 2)
) * 1e9; // 38,483,762,810m
// get distance traveled (d = (1/2) at^2)
var traveled = (acc * Math.pow(time, 2)) / 2;
// get ratio of distance traveled to actualDistance
var ratio = traveled / distance;
// midpoint formula to test # 50% time ((x+a)/2,(y+b)/2)
console.log({ x: (ship.p.x+dest.p.x)/2, y: (ship.p.y+dest.p.y)/2})
// get the point using this formula (((1−t)a+tx),((1−t)b+ty))
return {
x: (( 1 - ratio ) * ship.p.x) + (ratio * dest.p.x),
y: (( 1 - ratio ) * ship.p.y) + (ratio * dest.p.y)
};
}
# 50% time, 62,633s point returns as (~7, ~12.5) which matches the midpoint formula which returns as (~7, ~12.5). However, any other distance/time you input will be wildly wrong. My guess is that acceleration is messing up the calculations but I can't figure out how to change the formula to make it work. Thank you for your time.
First, you say that distance is the total distance, but it really is the distance left from the ship to the destination. I don't fully understand your plan on how to do the calculations, so I will suggest something different below.
Lets call the start position start, and it has a x, and y coordinate: start.x and start.y. Similarly with end.
Now, for a simulation like this to work we need velocity as a property on the ship as well, so
ship = {
...
v : {x : 0, y : 0}
}
It will start from rest, and it should reach rest. Ideally it should have acceleration a for a general movement, but we can skip that right now. We will have two different behaviours
The ship starts from rest, accelerates with 9.8 m/s^2 towards the goal until the half point is reached.
The ship starts at speed v at the midpoint, decelerates with -9.8 m/s^2 towards the goal until the speed is 0. Now we should have reached the goal.
To get velocity from accelerations we use v = v_start + a*time, and to get position from velocity we use x = x_start + v*time. The current position of the ship is then for the two cases
// Case 1
current = start + a*time*time
// the above is due to the fact that
// current = start + v*time
// and the fact that v = v_start + a*time as I wrote previously,
// with v_start.x = v_start.y = 0
//Case 2
current = midpoint + (v_at_midpoint - a*time_since_midpoint)*time_since_midpoint
Note here that start, current and a here are vectors, so they will have a x and y (and potentially z) coordinate each.
The acceleration you get by the following algorithm
a = (start - end)/totalDistance * 9.81 // 9.81 is the desired acceleration -- change it to whatever you want
If you want to understand what the above actually means, it calculates what is called a unit vector, which tells us what direction the force points at.
What you will need to do now is as follows:
Calculate the total distance and the acceleration
Determine if you're in case 1 or 2 given the input time in the function.
Once you've reached the midpoint, store the velocity and how long it took to get there and use it to determine the motion in case 2.
Stop once you've reached the destination, or you will go back to the start eventually!
Good luck!
P.S I should also note here that this does not take into account special relativity, so if your distances are too far apart you will get non-physical speeds. It gets a lot messier if you want to take this into account, however.
So thanks to #pingul I was able to get the answer using insights from his suggestions.
var ship = {
mass: 135000,
force: 1324350,
accel: 0,
nav: {
startPos: { x: 0, y: 0 },
endPos: { x: 0, y: 0 },
distanceToDest: 0,
distanceTraveled: 0,
departTime: 0,
timeToDest: 0,
arriveTime: 0,
startDeceleration: 0
}
};
var log = [];
var start = { x: -1, y: -5 };
var end = { x: 15, y: 30 };
var updateLog = function() {
document.getElementById('ship').textContent = JSON.stringify(ship, null, 2);
document.getElementById('pos').textContent = JSON.stringify(log, null, 2);
}
var plotCourse = function(ship, start, end) {
// calculate acceleration (F = ma)
var acceleration = ship.force / ship.mass; // ~9.81 (1G)
// calculate distance between 2 points (Math.sqrt((a - x)^2+(b - y)^2))
var totalDistance = Math.sqrt(
Math.pow(end.x - start.x, 2) +
Math.pow(end.y - start.y, 2)
); // 38.48376280978771
// multiply grid system to galactic scale
var actualDistance = totalDistance * 1e9; // 1 = 1Mkm (38,483,763km) Earth -> Venus
// determine the mid-point where ship should flip and burn to decelerate
var midpoint = actualDistance / 2;
// calculate acceleration + deceleration time by solving t for each: (Math.sqrt(2d/a))
var time = Math.sqrt( 2 * midpoint / acceleration ) * 2; // 125,266s or 34h or 1d 10h
// load data into ship nav
ship.nav = {
startPos: start,
endPos: end,
distanceToDest: actualDistance,
timeToDest: time,
startDeceleration: time / 2
}
ship.accel = acceleration
//log it
updateLog();
};
var goUnderway = function(ship) {
var arrivalEl = document.getElementById('arrivalTime');
// set depart and arrive times
var timeToDest = ship.nav.timeToDest * 1000; // convert to ms
ship.nav['departTime'] = moment().valueOf(); // returns now as unix ms
ship.nav['arriveTime'] = moment(ship.nav.departTime).add(timeToDest).valueOf();
//log it
arrivalEl.textContent = 'Your ship will arrive ' + moment(ship.nav.arriveTime).calendar();
updateLog();
};
var getPosition = function(ship, date) {
var currentTime = date ? moment(date).valueOf() : moment().valueOf() // unix ms
var elapsedTime = (currentTime - ship.nav.departTime) / 1000; // convert to s
var remainingTime = (ship.nav.arriveTime - currentTime) / 1000; // convert to s
var distanceAtMidpoint = 0;
var timeSinceMidpoint = 0;
var pos = { x: 0, y: 0 };
// calculate velocity from elapsed time
if (elapsedTime < ship.nav.startDeceleration) {
// if ship is accelerating use this function
ship.nav.distanceTraveled = 0 + ship.accel * Math.pow(elapsedTime, 2) / 2;
} else if (elapsedTime > ship.nav.startDeceleration) {
// else if ship is decelerating use this function
distanceAtMidpoint = 0 + ship.accel * Math.pow(ship.nav.startDeceleration, 2) / 2; // distance at midpoint
timeSinceMidpoint = elapsedTime - ship.nav.startDeceleration;
ship.nav.distanceTraveled = distanceAtMidpoint + ship.accel * Math.pow(timeSinceMidpoint, 2) / 2;
}
if (remainingTime <= 0) {
ship.force = ship.vel = ship.accel = 0;
pos = ship.nav.endPos;
} else {
// get ratio of distance traveled to actualDistance
var ratio = ship.nav.distanceTraveled / ship.nav.distanceToDest;
// get the point using this formula (((1−t)a+tx),((1−t)b+ty))
pos = {
x: (( 1 - ratio ) * start.x) + (ratio * end.x),
y: (( 1 - ratio ) * start.y) + (ratio * end.y)
};
}
log.push({
timestamp: moment(currentTime),
pos: pos
})
//log it
updateLog();
};
plotCourse(ship, start, end);
goUnderway(ship);
for (var i = 0; i < 35; i++) {
getPosition(ship, moment().add(i, 'hour'));
}
pre {
outline: 1px solid #ccc;
padding: 5px;
margin: 5px;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/moment.js/2.15.1/moment.min.js"></script>
Ship: <span id="arrivalTime"></span>
<pre id="ship"></pre> Last Known Positions:
<pre id="pos"></pre>
Using setInterval or RequestAnimationFrame, I'd like to get the progression value from lerping between X and Y. Assuming that X is 0 and Y is 1, I want to have 0 when it starts, 0.5 in the half, and 1 when finished.
I'd like this to happen in a given timeframe, let's say 5 seconds. Meaning that the half value 0.5 would happen when the setInterval/RequestAnimationFrame reaches 2.5seconds.
Finally, I'd like it to pingPong, so when it reaches the 5 seconds the values are decreasing and not increasing, such as 0.9, 0.8, 0.7, etc and then start again from 0, 0.1, 0.2...
Here is my code so far:
/*
function lerp(start, end, time) {
return start * (1.0 - time) + end * time;
}
*/
function lerp (start, end, amt){
return (1-amt)*start+amt*end;
}
function repeat(t, len) {
console.log('t: ' + t + ', len: ' + len);
return t - Math.floor(t / len) * len;
}
function pingPong(t, len) {
t = repeat(t, len * 2);
return len - Math.abs(t-len);
}
var transitionDuration = 1;
var startTime = Date.now()/1000;
var startPos = 0;
var endPos = 1;
setInterval(function () {
var currentTime = Date.now()/1000;
console.log('currentTime:', currentTime);
var adjustedTime = pingPong(currentTime-startTime, transitionDuration);
var x = lerp(startPos, endPos, adjustedTime);
console.log(Math.abs(x.toFixed(2)));
}, 100);
How can I do this?
The basic formula for linear interpolation would be something like
InterpolatedValue = X*t + Y*(1-t)
where X and Y are the values to be interpolated between and t is a parameter between 0 and 1 determining the degree of interpolation; 0 yields X and 1 yields Y. Furthermore, you would like to have some periodic movement with a period length of 5, alternating the direction of interpolation; this can be achieved as follows. If t is a nonnegative number growing over time, calculate
t' = t - t / 10
to remove all previous periods which have occured and
t'' = t' : t' in [0,5)
5 - t' : t' in [5,10)
and afterwards set
t''' = t' / 5
to normalize the parameter into [0,1] and use the basic interpolation formula from the beginning.
Note that linear interpolation and various other methods are collected here.
From your description, at any given frame there are 6 pieces of state:
Start time of current lerp
Lerp timespan
Current direction
Current time
Start value
End value
From these you can calculate the required progress value, say:
function progressValue(startTime, lerpSpanSeconds, dir,
currentTime X, Y, dir, currentTime) {
// lerp
return 0.42;
}
For requestAnimationFrame, you need a simple callback to pass in. That is, the function has to know everything it needs except what it can acquire when it runs. Here, when it runs it just needs to get the current time and work the rest out from there.
function animableThing() {
var startTime = 7;
var lerpSpanSeconds = 3;
var dir = +1;
var X = 0;
var Y = 1;
var currentTime = GetCurrentUnicornTime();
var thingToAnimate = document.getElementById('myAnimableElement');
var progress = progressValue(startTime, lerpSpanSeconds, dir,
currentTime, X, Y, dir, currentTime);
// reverse when we hit the end
if(progress > Y) {
dir *= -1;
startTime = currentTime;
progress = Y;
}
DrawAnimationThing(thingToAnimate, progress);
// continue the animation
window.requestAnimationFrame(animableThing);
}
But there's a problem; if you want to be able to set up the animation using values from the script or inputs from the screen, or up-to-date information about the elements on the screen, then you need to be able to make an animableThing callback fresh when you have new values. Behold, the mother:
function MotherOfAnimableThings(startTime, lerpSpanSeconds, dir, X, Y,
thingToAnimate)
{
// Passed in variables have scope outside the animableThing, these
// will be private to the animableThing function.
// Consider defaulting or validation here
// Construct a new function freshly each time the Mother is called,
// and return it to the caller. Note that we assign a variable here
// so that we can re-call RequestAnimationFrame to continue the loop
var callback = (function() {
var currentTime = GetCurrentUnicornTime();
var progress = progressValue(startTime, lerpSpanSeconds, dir,
currentTime, X, Y, dir, currentTime);
// reverse when we hit the end
if(progress > Y) {
dir *= -1;
startTime = currentTime;
progress = Y;
}
DrawAnimationThing(thingToAnimate, progress);
window.requestAnimationFrame(callback);
});
return callback;
}
We could go further, and make this general for other types of thing by letting the caller pass in a progressValue function to call, or in fact a callback, so that you could take any element, Draw function and setup function and make a thing that animates, but this is a reasonable starting point.
With the above, we just need to call Mother to create an animableThing function and call RequestAnimationFrame with that. From then on, it calls RequestAnimationFrame internally to continue the cycle.
Now, having done that, you will want to make it stop, so add in a variable in the callback which it can check, so that you can do
var animableThing = MotherOfAnimableThings(...);
window.requestAnimationFrame(animableThing);
// ... later
animableThing.stop = true; // it should stop on the next frame
I have been wrestling with rendering an animation that fires a projectile accurately from an "enemy" node to a "player" node in a 2D 11:11 grid (0:0 = top-left) in JS/Canvas. After a lot of reading up I've managed to get the shots close, but not quite bang on. I think my velocity function is a little out but I really don't know why. This is the trigonometric function:
this.getVelocityComponents = function(speed){
// loc (location of enemy actor) = array(2) [X_coord, Y_coord]
// des (destination (ie. player in this instance)) = array(2) [X_coord, Y_coord]
var i, sum, hyp, output = [], dis = [];
var higher = false;
for (i in loc) {
sum = 0;
if (loc[i] > des[i])
sum = loc[i] - des[i];
if (loc[i] < des[i])
sum = des[i] - loc[i];
dis.push(sum);
}
hyp = Math.sqrt(Math.pow(dis[X], 2) + Math.pow(dis[Y], 2));
if (dis[X] > dis[Y]) {
output[X] = (speed * Math.cos(dis[X]/hyp))
output[Y] = (speed * Math.sin(dis[Y]/hyp))
} else if (dis[X] < dis[Y]) {
output[X] = (speed * Math.cos(dis[Y]/hyp))
output[Y] = (speed * Math.sin(dis[X]/hyp))
}
return output;
}
and this is the instruction that tells the X and the Y of the projectile frame to advance:
var distance = [];
for (i in loc) {
var sum = 0;
if (loc[i] > des[i])
sum = loc[i] - des[i];
if (loc[i] < des[i])
sum = des[i] - loc[i];
distance.push(sum);
}
if (distance[X] > distance[Y]) {
frm[X] += (loc[X] < des[X]) ? v[X] : -v[X];
frm[Y] += (loc[Y] < des[Y]) ? v[Y] : -v[Y];
} else {
frm[Y] += (loc[Y] < des[Y]) ? v[X] : -v[X];
frm[X] += (loc[X] < des[X]) ? v[Y] : -v[Y];
}
Below is a screenshot. Blue is player, pink enemy and the yellow circles are projectiles
as you can see, it's almost on the mark.
Have I done something wrong? what do I need to do?
To calculate the direction from enemy to player you can simplify the calculations a little.
Find direction angle
var diffX = Player.x - Enemy.x, // difference in position
diffY = Player.y - Enemy.y,
angle = Math.atan2(diffY, diffX); // atan2 will give the angle in radians
Notice also difference for Y comes first for atan2 as canvas is oriented 0° pointing right.
Velocity vector
Then calculate the velocity vector using angle and speed:
// calculate velocity vector
var speed = 8,
vx = Math.cos(angle) * speed, // angle x speed
vy = Math.sin(angle) * speed;
You might want to consider using time as a factor if that is important. You can see my answer from a while back here for an example on this.
Demo
Using these calculations you will be able to always "hit" the player with the projectile (reload demo to change enemy position to random y):
var ctx = document.querySelector("canvas").getContext("2d"),
Player = {
x: 470,
y: 75
},
Enemy = {
x: 100,
y: Math.random() * 150 // reload demo to change y-position
};
// calculate angle
var diffX = Player.x - Enemy.x,
diffY = Player.y - Enemy.y,
angle = Math.atan2(diffY, diffX);
// calculate velocity vector
var speed = 8,
vx = Math.cos(angle) * speed, // angle x speed
vy = Math.sin(angle) * speed,
x = Enemy.x, // projectil start
y = Enemy.y + 50;
// render
(function loop() {
ctx.clearRect(0, 0, 500, 300);
ctx.fillRect(Player.x, Player.y, 30, 100);
ctx.fillRect(Enemy.x, Enemy.y, 30, 100);
ctx.fillRect(x - 3, y -3, 6, 6);
x += vx;
y += vy;
if (x < 500) requestAnimationFrame(loop);
})();
<canvas width=500 height=300></canvas>
The solution is much simpler than that.
What should you do ?
1) compute the vector that leads from you enemy to the player. That will be the shooting direction.
2) normalize the vector : meaning you build a vector that has a length of 1, with the same direction.
3) multiply that vector by your speed : now you have a correct speed vector, with the right norm, aimed at the player.
Below some code to help you understand :
function spawnBullet(enemy, player) {
var shootVector = [];
shootVector[0] = player[0] - enemy[0];
shootVector[1] = player[1] - enemy[1];
var shootVectorLength = Math.sqrt(Math.pow(shootVector[0], 2) + Math.pow(shootVector[1],2));
shootVector[0]/=shootVectorLength;
shootVector[1]/=shootVectorLength;
shootVector[0]*=bulletSpeed;
shootVector[1]*=bulletSpeed;
// ... here return an object that has the enemy's coordinate
// and shootVector as speed
}
Then, since you don't use time in your computations (!! wrooong !! ;-) ) you will make the bullet move with the straightforward :
bullet[0] += bullet.speed[0];
bullet[1] += bullet.speed[1];
Now the issue with fixed-step is that your game will run, say, twice slower on a 30fps device than on a 60fps device. The solution is to compute how much time elapsed since the last refresh, let's call this time 'dt'. Using that time will lead you to an update like :
bullet[0] += dt * bullet.speed[0];
bullet[1] += dt * bullet.speed[1];
and now you'll be framerate-agnostic, your game will feel the same on any device.