I am working on simple script that should animate given value (for example 6345.23) to 0 by counting it down, it should also end up at 0 if specified amount of time have passed (for example 2 seconds.
I started by simple logic:
given config: initial value, time in sec, interval
time is given in seconds so convert it to milliseconds
calculate amount of ticks by dividing time in ms by interval
calculate amount of decreased value per tick by dividing initial value by amount of ticks
once above are known we can simply do: (simple model, not actual code)
intId = setInterval(function() {
if(ticks_made === amount_of_ticks) {
clearInterval(intId);
} else {
value -= amount_per_tick;
// update view
}
}, interval);
actual code:
var value = 212.45,
time = 2, // in seconds
interval = 20; // in milliseconds
var time_to_ms = time * 1000,
amount_of_ticks = time_to_ms / interval,
amount_per_tick = (value / amount_of_ticks).toFixed(5);
var start_time = new Date();
var ticks_made = 0;
var intId = setInterval(function() {
if(ticks_made === amount_of_ticks) {
console.log('start time', start_time);
console.log('end time', new Date());
console.log('total ticks: ', amount_of_ticks, 'decresed by tick: ', amount_per_tick);
clearInterval(intId);
} else {
value = (value - amount_per_tick).toFixed(5);
console.log('running', ticks_made, value);
}
ticks_made++;
}, interval);
Link do fiddle (in console you can observe how it works)
If you set time to 2 (2 seconds) its ok, but if you set time to for example 2.55 (2.55 seconds) it doesnt stop at all at 0, its passing by and going indefinitely in negative values.
How i can fix it so no matter what is set in seconds its always go precisly one by one until reaches perfectly 0?
var value = 212.45,
time = 2, // in seconds
interval = 20; // in milliseconds
var time_to_ms = time * 1000,
amount_of_ticks = time_to_ms / interval,
amount_per_tick = (value / amount_of_ticks).toFixed(5);
var start_time = new Date();
var ticks_made = 0;
var intId = setInterval(function() {
if(ticks_made === amount_of_ticks) {
console.log('start time', start_time);
console.log('end time', new Date());
console.log('total ticks: ', amount_of_ticks, 'decresed by tick: ', amount_per_tick);
clearInterval(intId);
} else {
value = (value - amount_per_tick).toFixed(5);
console.log('running', ticks_made, value);
}
ticks_made++;
}, interval);
You're relying on ticks_made === amount_of_ticks being an exact match. Chances are, due to rounding, you won't get an exact match, so you'd be better off doing:
if(ticks_made >= amount_of_ticks) {
kshetline's answer correctly addresses why you get into negative values. When dealing with fractional IEEE-754 double-precision binary numbers (in the normal range, or even whole numbers in very high ranges), == and === can be problematic (for instance, 0.1 + 0.2 == 0.3 is false). Dealing with values as small as the fractional values here are, accumulated imprecision is also a factor. It's inevitable to have to fudge the final step.
But there's a larger issue: You can't rely on timers firing on a precise schedule. Many, many things can prevent their doing so — other UI rendering work, other scripts, CPU load, the tab being inactive, etc.
Instead, the fundamental technique for animation on browsers is:
Update when you can
Update based on where you should be in the animation based on time, not based on how many times you've animated
Use requestAnimationFrame so your update synchronizes with the browser's refresh
Here's your code updated to do that, see comments:
// Tell in-snippet console to keep all lines (rather than limiting to 50)
console.config({maxEntries: Infinity});
var value = 212.45,
time = 2.55, // in seconds
time_in_ms = time * 1000,
amount_per_ms = value / time_in_ms,
interval = 100 / 6, // in milliseconds, ~16.66ms is a better fit for browser's natural refresh than 20ms
ticks_made = 0;
// A precise way to get relative milliseconds timings
var now = typeof performance !== "undefined" && performance.now
? performance.now.bind(performance)
: Date.now.bind(Date);
// Remember when we started
var started = now();
// Because of the delay between the interval timer and requestAnimationFrame,
// we need to flag when we're done
var done = false;
// Use the interval to request rendering on the next frame
var intId = setInterval(function() {
requestAnimationFrame(render);
}, interval);
// About half-way in, an artificial 200ms delay outside your control interrupts things
setTimeout(function() {
console.log("************DELAY************");
var stop = now() + 200;
while (now() < stop) {
// Busy-loop, preventing anything else from happening
}
}, time_in_ms / 2);
// Our "render" function (okay, so we just call console.log in this example, but
// in your real code you'd be doing a DOM update)
function render() {
if (done) {
return;
}
++ticks_made;
var elapsed = now() - started;
if (elapsed >= time_in_ms) {
console.log(ticks_made, "done");
done = true;
clearInterval(intId);
} else {
var current_value = value - (amount_per_ms * elapsed);
console.log(ticks_made, current_value);
}
}
/* Maximize in-snippet console */
.as-console-wrapper {
max-height: 100% !important;
}
If you run that, then scroll up to the "************DELAY************" line, you'll see that even though rendering was held up by "another process", we continue with the appropriate next value to render.
It would make sense to convert the result of .toFixed() to a number right away:
let amount_per_tick = +(value / amount_of_ticks).toFixed(5);
let value = +(value - amount_per_tick).toFixed(5);
(note the + signs)
Then you will never have to worry about type coercion or anything, and instead just focus on math.
Related
I am looking to increment the value of "time" with 0.01 each 10 miliseconds until it gets to the desired value. Right now it just increases it instantly to the conditioned value.
var time = 0;
function animate() {
decreaseIncrement = -0.78;
increaseIncrement = 0.78;
if (
(document.getElementById("but5").onclick = function () {
if (time < increaseIncrement) {
do {
time += 0.01;
} while (time < increaseIncrement);
}
})
)
if (
(document.getElementById("but3").onclick = function () {
if (decreaseIncrement < time) {
do {
time -= 0.01;
} while (decreaseIncrement < time);
}
})
)
increaseIncrement = time + increaseIncrement;
decreaseIncrement = time + decreaseIncrement;
}
https://jsfiddle.net/2epqg1wc/1/
You can solve that problem using setInterval which repeatedly runs a task every x milliseconds until you cancel it. Below code reduces the value to 0 in 0.01 steps with a step performed every 10 milliseconds.
var value = 1.0;
var decrement = 0.01;
function decreaseAnimation() {
var interval = setInterval(() => {
value -= decrement;
console.log(value);
if (value <= 0) {
clearInterval(interval);
}
}, 10);
}
decreaseAnimation();
You have 3 options:
requestAnimationFrame (rAF)
setTimeout/setInterval (sTo)
messageChannel
The first 2 options are more straightforward but they will lack the precision, because rAF fires every 17 milliseconds (assuming 60Hz) and sTO will fire at most 4ms after 4 successive recursions. Usually rAF is preferred over sTo because of better reliability in timing of firing these callbacks. Use sTO as a fallback if rAF is not supported.
Here is an implementation from a library for similar purposes:
var rafx = require("rafx");
rafx.async({ //create a ledger object to store values
curr_time:0,
desired:Math.random(),
frames:0
}).animate(function(obj){
//obj is the ledger above
//increment obj.frames here if you want to
return obj;
},).until(function(obj){
obj.frames++;
obj.curr_time = obj.frames * 17 / 10 * 0.01;
return obj.curr_time >= obj.desired;
}).then(function(obj){
console.log("sequence ended with values:" + JSON.stringify(obj));
});
You can copy paste the code above here and test it.
The last option uses MessageChannel to post message between ports, which gives extremely high precision because it is fired at the next event loop. You can combine this with performance.now to determine whether to increment your time or not.
Disclosure: I am the author of the aforementioned lib.
I wrote some JavaScript code to animate CSS properties of elements. I pass the following arguments to the function: amount, interval, and duration; amount being the change in the property (for example 200 could mean add 200 pixels to the element's width), interval being the time between two consecutive changes, and duration being the total duration of the animation.
The code works fine unless I pass the arguments in a way that the change in each interval becomes very small (like a tiny fraction of a pixel).
I know the code is working fine theoretically, as I get the change in console.
Any ideas about the problem?
Cheers.
UPDATE: the code:
function handleTimer (amount, interval, duration, execute, element) {
let i = 0;
let current = 0;
let stepsCount = countSteps(interval, duration);
let stepLength = calcStepLength(stepsCount, amount);
let count = setTimeout(function addOneMore () {
if ( i < stepsCount -1 ){
i++;
current += stepLength;
execute(stepLength, element);
if (current < amount) {
count = setTimeout(addOneMore, interval)
}
} else {
current = amount;
execute(amount - (stepsCount -1) * stepLength, element);
}
}, interval)
}
function countSteps (interval, duration) {
let remainder = duration % interval;
let stepsCount;
if (remainder) {
stepsCount = Math.floor(duration / interval) + 1;
} else {
stepsCount = duration / interval;
}
return stepsCount;
}
function calcStepLength(stepsCount, amount) {
return amount / stepsCount;
}
function resizeWidth (amount, element) {
let widthSTR = $(element).css('width');
let width = parseInt( widthSTR.substr( 0 , widthSTR.length - 2 ) );
$(element).css('width', `${width + amount}px`);
}
So this:
handleTimer(218, 5, 200, resizeWidth, '.box');
works fine, but this:
handleTimer(218, 5, 2000, resizeWidth, '.box');
doesn't.
UPDATE 2:
I know browsers are super accurate with pixels, like when you use percentages. Of course the value will be rounded before rendering since displays cant display half pixels, but the value is still calculated accurately.
I don't know at what decimal the rounding occurs.
This happens because parseInt is rounding your number up.
Pay attention to this line:
let width = parseInt( widthSTR.substr( 0 , widthSTR.length - 2 ) );
if width is a decimal number, like 22.5px, it will be rounded up to 22.
If amount is less than 1, it won't reach 23 and when you round up the number again, you'll get 22 again and it becomes a loop.
You have two solutions:
Use another variable to save the width value, avoiding to writing and reading it from CSS:
let initialWidth = $(element).css('width');
let savedWidth = widthSTR.substr(0, initialWidth, initialWidth.length - 2 ) );
function resizeWidth (amount, element) {
savedWidth += amount;
$(element).css('width', `${savedWidth}px`);
}
Just use parseFloat in place of parseInt to don't round your number up:
let width = parseFloat( widthSTR.substr( 0 , widthSTR.length - 2 ) );
I'm working on a game loop and can't get past a specific issue: A bunch of objects start with an incremented delay and should move a certain distance.
The expected behaviour is that all objects should move in an even diagonal line, yet they move in uneven groups.
I realize the issue lies in 16.667ms interval updates which "groups" objects in update cycles. Is it possible to achieve sub-17ms precision?
I have tried separating update and render methods and run the update inside a delta while loop - all to no avail.
Here's the relevant part from the tick function:
function tick() {
if (this._stopped) return;
let now = performance.now();
if (now < this._lastTick + this._interval - 1) {
this._rafId = requestAnimationFrame(this.tick);
return;
}
this._rafId = requestAnimationFrame(this.tick);
let frameTime = (now - this._lastTick);
this._lastTick = now;
this._delta += frameTime;
let acc = 0;
while (this._delta >= this._interval) {
this._delta -= this._interval;
//this.update(this._interval);
acc++;
}
this.update(acc * this._interval);
//this.render(time);
this.count++;
}
Here's the codepen.
Would really appreciate any input.
When I use the following code to fade in a file it doesn't work as I expect. I expect a gradual fade in from 0 to 1 over the course of 5 seconds, instead I get an abrupt change five seconds into playing the file where the gain instantly goes from 0 to 1. What am I not understanding ?
soundObj.play = function() {
playSound.buffer = soundObj.soundToPlay;
playSound.connect(gainNode);
gainNode.gain.value = 0;
gainNode.gain.exponentialRampToValueAtTime(1, audioContext.currentTime + 5);
gainNode.connect(audioContext.destination);
playSound.start(audioContext.currentTime);
}
Update/Edit
I changed the above code to the following and it seems to work, now I am researching why. I've added a few comments. Mainly inquiring as to if adding a setValueAtTime method is necessary and if a non zero value is necessary for the gain.value properties default value.
soundObj.play = function() {
playSound.buffer = soundObj.soundToPlay;
playSound.connect(gainNode);
gainNode.gain.value = 0.001; // If set to 0 it doesn't fade in
gainNode.gain.setValueAtTime(gainNode.gain.value, audioContext.currentTime); // Is this needed to have the other RampToValue methods work ?
gainNode.gain.exponentialRampToValueAtTime(1, audioContext.currentTime + 7);
gainNode.connect(audioContext.destination);
playSound.start(audioContext.currentTime);
}
A non-zero positive value is necessary for exponentialRampToValueAtTime. This isn't a Web Audio thing as much as it's just a math thing.
There's really no way to exponentially grow a value of 0.
Here's a rough version of the algorithm Chrome uses (rewritten in JS):
// start value
var value1 = 0.1;
// target value
var value2 = 1;
// start time (in seconds)
var time1 = 0;
// end time (in seconds)
var time2 = 2;
// duration
var deltaTime = time2 - time1;
// AudioContext sample rate
var sampleRate = 44100;
// total number of samples
var numSampleFrames = deltaTime * sampleRate;
// time incrementer
var sampleFrameTimeIncr = 1 / sampleRate;
// current time (in seconds)
var currentTime = 0;
// per-sample multiplier
var multiplier = Math.pow( value2 / value1, 1 / numSampleFrames );
// output gain values
var values = new Array( numSampleFrames );
// set up first value
var value = value1 * Math.pow( value2 / value1, ( ( currentTime - time1 ) * sampleRate ) / numSampleFrames );
for ( var i = 0; i < numSampleFrames; ++i ) {
values[ i ] = value;
value *= multiplier;
currentTime += sampleFrameTimeIncr;
}
If you change value1 to zero, you'll see that the output array is basically full of NaN. But Chrome also adds a bit of extra code to save you from that by special-casing instances where your value is <= 0 so that you don't actually end up with gain values of NaN.
If none of that makes sense, let me put it this way. In order to exponentially grow a value, you basically need a loop that looks like this:
for ( var i = 0; i < length; ++i ) {
values[ i ] = value;
value *= multiplier;
}
But if your initial value is 0, well, 0 multiplied by any other number is always 0.
Oh, and if you're interested (and can read C++), here's a link to the code that Chrome uses: https://chromium.googlesource.com/chromium/blink/+/master/Source/modules/webaudio/AudioParamTimeline.cpp
Relevant stuff is on line 316.
Edit
Apologies for a Chrome-centric explanation. But the underlying math concept of not being able to exponentially grow a value of zero will hold with any implementation.
This is probably basic math that I don't seem to remember.
I'm trying to get from 0 to 5,000,000 in 10 seconds while having all the numbers ticking. I don't have to have the number reach exactly 5,000,000 because I can just do a conditional for it to stop when it's over.
Right now I have this:
count+= 123456
if (count > 5000000) {
count = 5000000;
}
It gives the sense of number moving you know? But It really starts off too high. I wanted to gradually climb up.
You could do something like this:
function timedCounter(finalValue, seconds, callback){
var startTime = (new Date).getTime();
var milliseconds = seconds*1000;
(function update(){
var currentTime = (new Date).getTime();
var value = finalValue*(currentTime - startTime)/milliseconds;
if(value >= finalValue)
value = finalValue;
else
setTimeout(update, 0);
callback && callback(value);
})();
}
timedCounter(5000000, 10, function(value){
// Do something with value
});
Demo
Note that with a number as big as 5000000 you won't see the last couple digits change. You would only see that with a small number like 5000. You could fix that; perhaps by adding in some randomness:
value += Math.floor(Math.random()*(finalValue/10000 + 1));
Demo with randomness
You can tween:
import fl.transitions.Tween;
import fl.transitions.easing.Regular;
var count = 0;
var tween:Tween = new Tween(this, "count", Regular.easeInOut,0,5000000,10, true);
This will tween you variable count from 0 to 5000000 in 10 seconds. Read about these classes if you want to expand on this code.
Tween
TweenEvent
Good luck!