Javascript animation goes wrong when tab is inactive - javascript

Sample:
http://codepen.io/anon/pen/xgZMVd/
html file:
<div class="game-page">
</div>
css file:
.game-page {
width: 1024px;
height: 768px;
position: absolute;
background-color: grey;
}
.stick {
position: absolute;
left: 50%;
margin-left: -94px;
top: -60px;
width: 188px;
height: 50px;
background-color: black;
}
js file:
$(document).ready(function() {
init();
});
var CSS_CLASS = { STICK: "stick" },
changeSpeedTime = 1000, //in milliseconds
gameTime = 60, //in seconds
timer = 1, //in seconds
windowWidth = 1024,
windowHeight = 768,
stickTop = -60,
restStickTime = 0, //in milliseconds
initialStickInterval = 1000, //in milliseconds
stickInterval = null, //in milliseconds
initialStickDuration = 7, //in seconds
stickDuration = null, //in seconds
stickTweensArray = [],
changeSpeedInterval = null,
countdownTimerInterval = null,
generateSticksInterval = null,
generateSticksTimeout = null,
$gamePage = null;
function init() {
initVariables();
initGamePage();
}
function changingSpeedFunction(x){
var y = Math.pow(2, (x / 20));
return y;
}
function initVariables() {
$gamePage = $(".game-page");
stickDuration = initialStickDuration;
stickInterval = initialStickInterval;
}
function initGamePage() {
TweenMax.ticker.useRAF(false);
TweenMax.lagSmoothing(0);
initGamePageAnimation();
}
function initGamePageAnimation () {
generateSticks();
changeSpeedInterval = setInterval(function () {
changeSpeed();
}, changeSpeedTime);
countdownTimerInterval = setInterval(function () {
updateCountdown();
}, 1000);
}
function changeSpeed () {
var x = timer;
var y = changingSpeedFunction(x); //change speed function
stickDuration = initialStickDuration / y;
stickInterval = initialStickInterval / y;
changeCurrentSticksSpeed();
generateSticks();
}
function changeCurrentSticksSpeed () {
stickTweensArray.forEach(function(item, i, arr) {
var tween = item.tween;
var $stick = item.$stick;
var oldTime = tween._time;
var oldDuration = tween._duration;
var newDuration = stickDuration;
var oldPosition = stickTop;
var newPosition = $stick.position().top;
var oldStartTime = tween._startTime;
var distance = newPosition - oldPosition;
var oldSpeed = distance / oldTime;
var newSpeed = oldSpeed * oldDuration / newDuration;
var newTime = distance / newSpeed;
var currentTime = oldStartTime + oldTime;
var newStartTime = currentTime - newTime;
item.tween._duration = newDuration;
item.tween._startTime = newStartTime;
});
}
function generateSticks () {
if (restStickTime >= changeSpeedTime) {
restStickTime -= changeSpeedTime;
restStickTime = Math.abs(restStickTime);
} else {
generateSticksTimeout = setTimeout(function () {
generateSticksInterval = setInterval(function () {
generateStick();
restStickTime -= stickInterval;
if (restStickTime <= 0) {
clearInterval(generateSticksInterval);
restStickTime = Math.abs(restStickTime);
}
}, stickInterval);
generateStick();
restStickTime = changeSpeedTime - Math.abs(restStickTime) - stickInterval;
if (restStickTime <= 0) {
clearInterval(generateSticksInterval);
restStickTime = Math.abs(restStickTime);
}
}, restStickTime);
}
}
function generateStick () {
var $stick = $("<div class='" + CSS_CLASS.STICK + "'></div>").appendTo($gamePage);
animateStick($stick);
}
function animateStick ($stick) {
var translateYValue = windowHeight + -stickTop;
var tween = new TweenMax($stick, stickDuration, {
y: translateYValue, ease: Power0.easeNone, onComplete: function () {
$stick.remove();
stickTweensArray.shift();
}
});
stickTweensArray.push({tween:tween, $stick:$stick});
}
function updateCountdown () {
timer++;
if (timer >= gameTime) {
onGameEnd();
clearInterval(changeSpeedInterval);
clearInterval(countdownTimerInterval);
clearInterval(generateSticksInterval);
clearTimeout(generateSticksTimeout);
}
}
function onGameEnd () {
var $sticks = $gamePage.find(".stick");
TweenMax.killTweensOf($sticks);
}
So, as I researched, I have next situation:
TweenMax (as it uses requestAnimationFrame) freezes when tab is inactive.
setInterval keep going when tab is inactive (also it's delay may change when tab is inactive, depends on browser)
Is there any other javascript functionality that changes when tab is inactive?
Then I have 2 solutions:
Freeze whole game, when tab is inactive.
Keep going, when tab is inactive.
With first solution I have next problem: as TweenMax uses requestAnimationFrame, it works correct according to this solution (freezes animation), but how can I freeze intervals and timeouts when tab is inactive and then resume intervals and timeouts?
With second solution I can use TweenMax.lagSmoothing(0) and TweenMax.ticker.useRAF(false) for animation and it works, but anyway something goes wrong with intervals and/or timeouts. I expected that animation goes wrong because of change of interval delay to 1000+ ms when tab is inactive (according to http://stackoverflow...w-is-not-active), but I disabled acceleration and set delays to 2000ms and it didn't help.
Please help me with at least one solution. Better with both to have some variety.

Resolved problem with first solution: freeze whole game. Just used TweenMax.delayedCall() function instead of setTimeout and setInterval and whole animation synchronized pretty well.

Related

How to fix setTimeout/requestAnimationFrame accuracy

For demonstration of the issue please see here:
https://gyazo.com/06e423d07afecfa2fbdb06a6da77f66a
I'm getting a jumping behavior on un-pausing the notification. This is also influenced by how long the mouse stays on the notification and how close the progress is to the end.
I've tried so many things, I'm not sure anymore if the problem is truly with setTimeout.
It is like as if since the calculation of this.timerFinishesAt to the first iteration of requestAnimationFrame the progress jumps due to waiting on cpu time? But then again, why would it be influenced by the hover time and progress.
How do I mitigate the jumping behavior?
I read/tried to implement the fix from the following resources amongst looking at other stackoverflow questions:
https://gist.github.com/tanepiper/4215634
How to create an accurate timer in javascript?
What is the reason JavaScript setTimeout is so inaccurate?
https://www.sitepoint.com/creating-accurate-timers-in-javascript/
https://codepen.io/sayes2x/embed/GYdLqL?default-tabs=js%2Cresult&height=600&host=https%3A%2F%2Fcodepen.io&referrer=https%3A%2F%2Fmedium.com%2Fmedia%2Fb90251c55fe9ac7717ae8451081f6366%3FpostId%3D255f3f5cf50c&slug-hash=GYdLqL
https://github.com/Falc/Tock.js/tree/master
https://github.com/philipyoungg/timer
https://github.com/Aaronik/accurate_timer
https://github.com/husa/timer.js
timerStart(){
// new future date = future date + elapsed time since pausing
this.timerFinishesAt = new Date( this.timerFinishesAt.getTime() + (Date.now() - this.timerPausedAt.getTime()) );
// set new timeout
this.timerId = window.setTimeout(this.toggleVisibility, (this.timerFinishesAt.getTime() - Date.now()));
// animation start
this.progressId = requestAnimationFrame(this.progressBar);
},
timerPause(){
// stop notification from closing
window.clearTimeout(this.timerId);
// set to null so animation won't stay in a loop
this.timerId = null;
// stop loader animation from progressing
cancelAnimationFrame(this.progressId);
this.progressId = null;
this.timerPausedAt = new Date();
},
progressBar(){
if (this.progress < 100) {
let elapsed = Date.now() - this.timerStarted.getTime();
let wholeTime = this.timerFinishesAt.getTime() - this.timerStarted.getTime();
this.progress = Math.ceil((elapsed / wholeTime) * 100);
if (this.timerId) {
this.progressId = requestAnimationFrame(this.progressBar);
}
} else {
this.progressId = cancelAnimationFrame(this.progressId);
}
}
When you do calculate the current progress of your timer, you are not taking the pause time into consideration. Hence the jumps: This part of your code is only aware of the startTime and currentTime, it won't be affected by the pauses.
To circumvent it, you can either accumulate all this pause times in the startTimer function
class Timer {
constructor() {
this.progress = 0;
this.totalPauseDuration = 0;
const d = this.timerFinishesAt = new Date(Date.now() + 10000);
this.timerStarted = new Date();
this.timerPausedAt = new Date();
}
timerStart() {
const pauseDuration = (Date.now() - this.timerPausedAt.getTime())
this.totalPauseDuration += pauseDuration;
// new future date = future date + elapsed time since pausing
this.timerFinishesAt = new Date(this.timerFinishesAt.getTime() + pauseDuration);
// set new timeout
this.timerId = window.setTimeout(this.toggleVisibility.bind(this), (this.timerFinishesAt.getTime() - Date.now()));
// animation start
this.progressId = requestAnimationFrame(this.progressBar.bind(this));
}
timerPause() {
// stop notification from closing
window.clearTimeout(this.timerId);
// set to null so animation won't stay in a loop
this.timerId = null;
// stop loader animation from progressing
cancelAnimationFrame(this.progressId);
this.progressId = null;
this.timerPausedAt = new Date();
}
progressBar() {
if (this.progress < 100) {
let elapsed = (Date.now() - this.timerStarted.getTime()) - this.totalPauseDuration;
let wholeTime = this.timerFinishesAt.getTime() - this.timerStarted.getTime();
this.progress = Math.ceil((elapsed / wholeTime) * 100);
log.textContent = this.progress;
if (this.timerId) {
this.progressId = requestAnimationFrame(this.progressBar.bind(this));
}
} else {
this.progressId = cancelAnimationFrame(this.progressId);
}
}
toggleVisibility() {
console.log("done");
}
};
const timer = new Timer();
btn.onclick = e => {
if (timer.timerId) timer.timerPause();
else timer.timerStart();
};
<pre id="log"></pre>
<button id="btn">toggle</button>
or update the startTime, which seems to be more reliable:
class Timer {
constructor() {
this.progress = 0;
const d = this.timerFinishesAt = new Date(Date.now() + 10000);
this.timerStarted = new Date();
this.timerPausedAt = new Date();
}
timerStart() {
const pauseDuration = (Date.now() - this.timerPausedAt.getTime())
// update timerStarted
this.timerStarted = new Date(this.timerStarted.getTime() + pauseDuration);
// new future date = future date + elapsed time since pausing
this.timerFinishesAt = new Date(this.timerFinishesAt.getTime() + pauseDuration);
// set new timeout
this.timerId = window.setTimeout(this.toggleVisibility.bind(this), (this.timerFinishesAt.getTime() - Date.now()));
// animation start
this.progressId = requestAnimationFrame(this.progressBar.bind(this));
}
timerPause() {
// stop notification from closing
window.clearTimeout(this.timerId);
// set to null so animation won't stay in a loop
this.timerId = null;
// stop loader animation from progressing
cancelAnimationFrame(this.progressId);
this.progressId = null;
this.timerPausedAt = new Date();
}
progressBar() {
if (this.progress < 100) {
let elapsed = Date.now() - this.timerStarted.getTime();
let wholeTime = this.timerFinishesAt.getTime() - this.timerStarted.getTime();
this.progress = Math.ceil((elapsed / wholeTime) * 100);
log.textContent = this.progress;
if (this.timerId) {
this.progressId = requestAnimationFrame(this.progressBar.bind(this));
}
} else {
this.progressId = cancelAnimationFrame(this.progressId);
}
}
toggleVisibility() {
console.log("done");
}
};
const timer = new Timer();
btn.onclick = e => {
if (timer.timerId) timer.timerPause();
else timer.timerStart();
};
<pre id="log"></pre>
<button id="btn">toggle</button>
As to the final gap, not seeing how this code is linked with your UI, it's hard to tell what happens.
I think simply using SetInterval is enough :
const progressBar = {
MsgBox : document.querySelector('#Message'),
Info : document.querySelector('#Message h1'),
barr : document.querySelector('#Message progress'),
interV : 0,
DTime : 0,
D_Max : 0,
Init() {
this.MsgBox.onmouseover=_=> { // pause
clearInterval( this.interV )
}
this.MsgBox.onmouseout=_=>{ // restart
this._run()
}
},
Start(t,txt)
{
this.DTime = this.D_Max = t * 1000
this.barr.value = 0
this.barr.max = this.D_Max
this.Info.textContent = txt
this._run()
},
_run()
{
let D_End = new Date(Date.now() + this.DTime )
this.interV = setInterval(_=>{
this.DTime = D_End - (new Date(Date.now()))
if (this.DTime > 0) { this.barr.value = this.D_Max - this.DTime }
else { clearInterval( this.interV ); console.clear(); console.log( "finish" ) }
}, 100);
}
}
progressBar.Init()
progressBar.Start(10, 'Hello!') // 10 seconds
#Message {
box-sizing: border-box;
display: block;
float: right;
width: 200px;
height: 80px;
background-color: darkslategrey;
padding: 0 1em;
color:#e4a8b4;
cursor: pointer;
margin-right:1.5em;
}
#Message h1 { margin: .3em 0 0 0}
#Message progress { height: .1em; margin: 0; width:100%; background-color:black; }
#Message progress::-moz-progress-bar,
#Message progress::-webkit-progress-value { background-color:greenyellow; }
<div id="Message">
<progress value="50" max="100" ></progress>
<h1> </h1>
</div>

Measure scroll velocity Javascript and trigger event

I would like to trigger an event when the window scroll velocity goes above a certain value. I found some code that will help measure the velocity but I can't work out why the if statement is not triggered when the speed goes above 150. Any help would be greatly appreciated.
const checkScrollSpeed = (function(settings){
settings = settings || {};
let lastPos, newPos, timer, delta,
delay = settings.delay || 50;
function clear() {
lastPos = null;
delta = 0;
}
clear();
return function(){
newPos = window.scrollY;
if ( lastPos != null ){ // && newPos < maxScroll
delta = newPos - lastPos;
}
lastPos = newPos;
clearTimeout(timer);
timer = setTimeout(clear, delay);
return delta;
};
})();
const container = document.querySelector('#container');
window.addEventListener('scroll', function() {
console.log(checkScrollSpeed());
if (checkScrollSpeed() > 150) {
console.log('150+')
container.classList.add('red');
}
});
#container {
width: 75%;
height: 170vh;
background-color: yellow;
}
#container.red {
background-color: red !important;
}
<div id="container"></div>
You are calling checkScrollSpeed in succession and when it's called the second time delta is 0. Either remove the console.log or assign delta to some variable and use that for logging and the condition.
I think it's because you have a little delay between the first time you call checkScrollSpeed() in your console.log and then in your if statement. You can try to call it only once and save the value for your console.log and if statement. It works for me with this solution:
const checkScrollSpeed = (function(settings) {
settings = settings || {};
let lastPos, newPos, timer, delta,
delay = settings.delay || 50;
function clear() {
lastPos = null;
delta = 0;
}
clear();
return function() {
newPos = window.scrollY;
if (lastPos != null) { // && newPos < maxScroll
delta = newPos - lastPos;
}
lastPos = newPos;
clearTimeout(timer);
timer = setTimeout(clear, delay);
return delta;
};
})();
const container = document.querySelector('#container');
window.addEventListener('scroll', function() {
var speed = checkScrollSpeed();
console.log(speed);
if (speed > 150) {
console.log('150+');
container.classList.add('red');
}
});
#container {
width: 75%;
height: 170vh;
background-color: yellow;
}
#container.red {
background-color: red !important;
}
<div id="container"></div>

if body has class Fade in mp3 sound else fade out

I am trying to fade the volume of an mp3 in to 1 if the body has the class fp-viewing-0
How ever this isn't working and the volume doesn't change how can I fix this?
Code:
var audio0 = document.getElementById('audio-0');
audio0.volume = 0;
setInterval( function(){
if ($("body").hasClass("fp-viewing-0")) {
audio0.animate({volume: 1}, 1000);
}
else {
audio0.animate({volume: 0}, 1000);
}
}, 100);
HTML
<audio id="audio-0" src="1.mp3" autoplay="autoplay"></audio>
I've also tried:
$("#audio-0").prop("volume", 0);
setInterval( function(){
if ($("body").hasClass("fp-viewing-0")) {
$("#audio-0").animate({volume: 1}, 3000);
}
else {
$("#audio-0").animate({volume: 0}, 3000);
}
}, 100);
Kind Regards!
I have changed the jquery animate part to a fade made by hand. For that i created a fade time and steps count to manipulate the fade effect.
var audio0 = document.getElementById('audio-0');
audio0.volume = 0;
if ($("body").hasClass("fp-viewing-0")) {
audio0.volume = 1; //max volume
var fadeTime = 1500; //in milliseconds
var steps = 150; //increasing makes the fade smoother
var stepTime = fadeTime/steps;
var audioDecrement = audio0.volume/steps;
var timer = setInterval(function(){
audio0.volume -= audioDecrement; //fading out
if (audio0.volume <= 0.03){ //if its already inaudible stop it
audio0.volume = 0;
clearInterval(timer); //clearing the timer so that it doesn't keep getting called
}
}, stepTime);
}
Better would be to place all of this in a function that receives these values a fades accordingly so that it gets organized:
function fadeAudio(audio, fadeTime, steps){
audio.volume = 1; //max
steps = steps || 150; //turning steps into an optional parameter that defaults to 150
var stepTime = fadeTime/steps;
var audioDecrement = audio.volume/steps;
var timer = setInterval(function(){
audio.volume -= audioDecrement;
if (audio.volume <= 0.03){ //if its already inaudible stop it
audio.volume = 0;
clearInterval(timer);
}
}, stepTime);
}
Which would make your code a lot more compact and readable:
var audio0 = document.getElementById('audio-0');
audio0.volume = 0;
if ($("body").hasClass("fp-viewing-0")) {
fadeAudio(audio0, 1500);
}

Execute function IF another function is complete NOT when

I am having trouble creating a slider that pauses on hover, because I execute the animation function again on mouse off, if I flick the mouse over it rapidly (thereby calling the function multiple times) it starts to play up, I would like it so that the function is only called if the other function is complete, otherwise it does not call at all (to avoid queue build up and messy animations)
What's the easiest/best way to do this?
$(document).ready(function() {
//get variables
var slide_width = $('.slider_container').width();
var number_of_slides = $('.slider_container .slide').length;
var slider_width = slide_width*number_of_slides;
//set element dimensions
$('.slide').width(slide_width);
$('.slider').width(slider_width);
var n = 1;
$('.slider_container').hover(function() {
//Mouse on
n = 0;
$('.slider').stop(true, false);
}, function() {
//Mouse off
n = 1;
if (fnct == 0) sliderLoop();
});
//Called in Slide Loop
function animateSlider() {
$('.slider').delay(3000).animate({ marginLeft: -(slide_width * i) }, function() {
i++;
sliderLoop();
});
}
var i = 0;
var fnct = 0
//Called in Doc Load
function sliderLoop() {
fnct = 1
if(n == 1) {
if (i < number_of_slides) {
animateSlider();
}
else
{
i = 0;
sliderLoop();
}
}
fnct = 0
}
sliderLoop();
});
The slider works fine normally, but if I quickly move my mouse on and off it, then the slider starts jolting back and forth rapidly...been trying to come up with a solution for this for hours now..
Here's what fixed it, works a charm!
$(document).ready(function() {
//get variables
var slide_width = $('.slider_container').width();
var number_of_slides = $('.slider_container .slide').length;
var slider_width = slide_width*number_of_slides;
//set element dimensions
$('.slide').width(slide_width);
$('.slider').width(slider_width);
var n = 1;
var t = 0;
$('.slider_container').hover(function() {
clearInterval(t);
}, function() {
t = setInterval(sliderLoop,3000);
});
var marginSize = i = 1;
var fnctcmp = 0;
//Called in Doc Load
function sliderLoop() {
if (i < number_of_slides) {
marginSize = -(slide_width * i++);
}
else
{
marginSize = i = 1;
}
$('.slider').animate({ marginLeft: marginSize });
}
t = setInterval(sliderLoop,3000);
});

Multiple JavaScript timeouts at the same moment

I'm developing an HTML5 application and I'm drawing a sequence of images on the canvas with a certain speed and a certain timeout between every animation.
Able to use it multiple times I've made a function for it.
var imgNumber = 0;
var lastImgNumber = 0;
var animationDone = true;
function startUpAnimation(context, imageArray, x, y, timeOut, refreshFreq) {
if (lastImgNumber == 0) lastImgNumber = imageArray.length-1;
if (animationDone) {
animationDone = false;
setTimeout(playAnimationSequence, timeOut, refreshFreq);
}
context.drawImage(imageArray[imgNumber], x, y);
}
function playAnimationSequence(interval) {
var timer = setInterval(function () {
if (imgNumber >= lastImgNumber) {
imgNumber = 0;
clearInterval(timer);
animationDone = true;
} else imgNumber++;
}, interval);
}
Now in my main code, every time startUpAnimation with the right parameters it works just fine. But when I want to draw multiple animations on the screen at the same time with each of them at a different interval and speed it doesnt work!
startUpAnimation(context, world.mushrooms, canvas.width / 3, canvas.height - (canvas.height / 5.3), 5000, 300);
startUpAnimation(context, world.clouds, 100, 200, 3000, 100);
It now displays both animations at the right location but they animate both at the interval and timeout of the first one called, so in my case 5000 timeout and 300 interval.
How do I fix this so that they all play independently? I think I need to make it a class or something but I have no idea how to fix this. In some cases I even need to display maybe 5 animations at the same time with this function.
Any help would be appreciated!
try something like this
var Class = function () {
this.imgNumber = 0;
this.lastImgNumber = 0;
this.animationDone = true;
}
Class.prototype.startUpAnimation = function(context, imageArray, x, y, timeOut, refreshFreq) {
// if (lastImgNumber == 0) lastImgNumber = imageArray.length-1;
if (this.animationDone) {
this.animationDone = false;
setTimeout(this.playAnimationSequence, timeOut, refreshFreq, this);
}
// context.drawImage(imageArray[imgNumber], x, y);
};
Class.prototype.playAnimationSequence = function (interval, object) {
var that = object; // to avoid flobal this in below function
var timer = setInterval(function () {
if (that.imgNumber >= that.lastImgNumber) {
that.imgNumber = 0;
clearInterval(timer);
that.animationDone = true;
console.log("xxx"+interval);
} else that.imgNumber++;
}, interval);
};
var d = new Class();
var d1 = new Class();
d.startUpAnimation(null, [], 300, 300, 5000, 50);
d1.startUpAnimation(null,[], 100, 200, 3000, 60);
It should work,
As far as I can see, each call to startUpAnimation will draw immediately to the canvas because this execution (your drawImage(..) call) hasn't been included in the callback of the setTimeout or setInterval.

Categories

Resources