60fps: How to use requestAnimationFrame the right way? - javascript

On my website, a related content box should be animated into the viewport when it gets visible.
I’m trying to make my animation as efficient as possible through CSS and JavaScript, so that it doesn’t affects scroll performance negatively.
While the CSS part was simple (using transform, will-change, contain), I’m struggling a bit with when to use window.requestAnimationFrame.
Should I use it only when the class is added to the element or also when the function isScrolledIntoView is called or even inside isScrolledIntoView, when the elements position is measured?
var percentVisible = 0.25;
window.addEventListener('scroll', function(){
relatedContent(related, percentVisible);
}
)
function relatedContent(r, pV){
window.requestAnimationFrame(function() {
if(isScrolledIntoView(r, pV)){
window.requestAnimationFrame(function(){
r.classList.add("visible");
}, r)
}
}, r)
}
function isScrolledIntoView(el, percentV) {
var elemTop, elemBottom, elemHeight, overhang, isVisible;
/*window.requestAnimationFrame(
function(){*/
elemTop = el.getBoundingClientRect().top;
elemBottom = el.getBoundingClientRect().bottom;
elemHeight = el.getBoundingClientRect().height;
/*}
);*/
overhang = elemHeight * (1 - percentV);
isVisible = (elemTop >= -overhang) && (elemBottom <= window.innerHeight + overhang);
return isVisible;
}

requestAnimationFrame returns a non-zero long that can be used to cancel your request, so instead of writing your own throttle implementation, you can use the following simpler approach to prevent multiple handlers stacking up:
let currentRequest;
document.addEventListener('scroll', function () {
cancelAnimationFrame(currentRequest);
currentRequest = requestAnimationFrame(handleScroll);
});

No don't use it like that...
requestAnimationFrame (rAF) is a timing function that does synchronize with the screen refresh rate (generally 60fps).
Scroll event may fire more often than 60 events per second.
Each call to rAF will stack all the functions passed as its parameter in some kind of a big function called just before the next screen refresh.
Combine all of this and what you get is multiple calls to the same function in a stack, just before the next screen refresh.
Instead, what you seem to want is to prevent your scroll event to fire when not useful. This is called a throttle function, and you're a bit far from it.
Here is a simple throttle implementation using rAF :
var throttle = function(callback) {
var active = false; // a simple flag
var evt; // to keep track of the last event
var handler = function(){ // fired only when screen has refreshed
active = false; // release our flag
callback(evt);
}
return function handleEvent(e) { // the actual event handler
evt = e; // save our event at each call
if (!active) { // only if we weren't already doing it
active = true; // raise the flag
requestAnimationFrame(handler); // wait for next screen refresh
};
}
}
That you could use like this :
window.addEventListener('scroll', throttle(yourScrollCallback));

Related

Synchronize scrollbars between two elements - jQuery

How can you synchronize scrollbars between two elements without recursively calling each event?
Usually you would expect the following code:
$div1.scroll(function() {
$div2.scrollTop($div1.scrollTop());
});
$div2.scroll(function(){
$div1.scrollTop($div2.scrollTop());
});
But in this case, if you scroll $div1 1px, it will scroll $div2 1px as well, which will prompt the $div2 scroll event to fire and re-apply the scroll position to $div1. While this might not seem like an issue, when this code is applied to a page and you naturally scroll with a mouse, it scrolls in 1px increments because the handlers call eachother and don't allow the scroll to flow.
So, how would you solve this issue?
Example: https://jsfiddle.net/axtn/a91fsar3/2
Found out a good solution. Debouncing does the trick.
You can use a combination of timers and bools to make sure the element is being scrolled by the user. Thus, when the scroll event is rapidly and consecutively fired (like when a user scrolls down), it prevents the handlers from recursively calling eachother. The following code does the trick:
var userScroll1 = true;
var userScroll2 = true;
var timer;
$div1.scroll(function() {
if(userScroll2) {
userScroll1 = false;
clearTimeout(timer);
$div2.scrollTop($div1.scrollTop());
timer = setTimeout(function() {
userScroll1 = true;
}, 100);
}
});
$div2.scroll(function(){
if(userScroll1) {
userScroll2 = false;
clearTimeout(timer);
$div1.scrollTop($div2.scrollTop());
timer = setTimeout(function() {
userScroll2 = true;
}, 100);
}
});
Check out the properly functioning jsbin: https://jsfiddle.net/axtn/a91fsar3

Only setTimeout executes function. JS

I am trying to implement kind of player on my website.
If press 'Play' button, the music starts and the page smoothly scrolls down.
But when you press 'Mute' button (function(){music.volume=0}) I am not sure why the page appears at the top again. window.scroll() doesn't do anything without delay. So i am using setTimeout function to scroll the page on the current place. The problem is that in Opera and IE setTimeout takes about 10 ms, so when i click 'Mute' button i see like ticks to top and back. In chrome it takes only 2 ms and there is no problems.
Now when i decide to create my own timeout function the window.scroll() does not work again.
Here is my code:
var isMuted = false;
muteButton.onclick = function() { ////This function works with big delay.
if (!isMuted) {
mainAudio.volume = 0;
isMuted = true;
} else {
mainAudio.volume = bgAudioTrackVolume;
isMuted = false;
}
setTimeout(function() {
window.scroll(0, offset); /// Works
}, 0)
};
Change setTimeout with:
i = 9542155.873; /// I have tried delay time from 1ms - 250ms by changing this value.
while (i > 0.00001) {
i = i / 1.0001234567;
if (i < 0.00001) {
window.scroll(0, offset); /// Does not do anything. Strange! Have tried to change variable with a number.
}
}
Every time i check offset value, it is always available before calling scroll function.
I know that my problem is not usual and i am realy need your help.
The reason that the page scrolls to the top is that you are using a link with the empty bookmark #, which represents the top of the page. The reason that the scroll method doesn't work without a timeout is that jumping to the bookmark happens after the event handler.
Instead of trying to scroll the page back to where it was, just stop the default action of the link by returning false from the event handler:
var isMuted = false;
muteButton.onclick = function() {
if (!isMuted) {
mainAudio.volume = 0;
isMuted = true;
} else {
mainAudio.volume = bgAudioTrackVolume;
isMuted = false;
}
return false;
};
Alternatively, use some other element than a link.

Differentiating between mouseup mousedown and click

I know that mousedown happens when a user depresses the mouse button, mouseup happens when the release the mouse and click is of course two events mousedown and mouseup. I have three different events each dealing with these three events mouseup down and click. My question is how to differentiate between the three, now my mouse down has a timer, so I was thinking of adding a boolean in that timer and testing it within the click I tried this and it didn't work to my standards.
Mousedown- timer checks for certain classes then if none of these classes exist within the targeted element proceed
Mouseup- clear the timer
Click- open a module
I may have not made the boolean a global variable that each can read or not, or I am missing something completely. Here is an example quick code of my full code:
var isDown = false;
ee[i].addEventListener('click',function(){
if(isDown===false){
openModule();
}
},false);
ee[i].addEventListener('mousedown',function(){
var timer;
var $this = this;
timer = setTimeout(function(){
if($this.className == "class"){
isDown=true;
createActive();
}
},500);
},true);
ee[i].addEventListener('mouseup',function(){
clearTimeout(timer);
},false);
That is just a quick example. I may have missed some coding but I hope you catch my drift in the code above. Anyone know of a good way to differentiate between the three events?
I've rewritten your code utilizing jQuery...
var isDown = false;
var timer;
$('.class').mousedown(function(){
isDown = false;
timer = setTimeout(function(){
isDown = true;
//createActive();
console.log('MOUSE DOWN');
}, 500);
}).mouseup(function(){
if(isDown === false){
//openModule();
console.log('CLICK');
}else{
console.log('MOUSE UP');
}
clearTimeout(timer);
});
If you simply add jQuery to your page, my code will automatically attach itself to any element in your document with a class of 'class'.
I've commented out your createActive(); and openModule(); calls so that you can play around with it (viewing your javascript console at runtime will show you the script in action - remove the console.log() stuff when you're done playing). This code could be optimised a bit more but it will give you the general idea.
Your timer variable needed to be created globally (I moved it out of the function).
In this case (declaring a mousedown time barrier) the click function will be rendered useless so I've improvised it into the mouseup function.
It's good to know core javascript, but jQuery is just too easy and powerful to ignore.
Try this:
const isDown = ref(false)
const timer = ref(0)
const mouseDown = () => {
isDown.value = true
timer.value = setTimeout(() => {
isDown.value = false
}, 120)
}
const mouseUp = () => {
if (isDown.value === true) {
hideModal()
} else {
return
}
clearTimeout(timer.value)
}

Is it possible to listen for keydown and keyup while resizing window

I'm trying to detect keydown and keyup, while the window is being resized. What I've tried so far, only fires the key events after the resize is finished.
$(window).resize(function(){
console.log("resizing");
});
$(window).keydown(function(e){
$("#key").css("background","green");
});
$(window).keyup(function(e){
$("#key").css("background","red");
});
Okay, so part of the problem you may be running into here is that keydown isn't an on or off thing, it's a fire-constantly thing.
The same is true of onresize.
As you resize the window the event gets called over and over.
Also, because JS isn't multithreaded, only one of these events is going to happen at one time.
The other event is going to be queued up to run immediately after the other event finishes.
So what you actually want is a state machine that one event sets, and the other event checks.
Quick examples:
var BrowserWindow = { width : 0, height : 0, prevWidth : 0, prevHeight : 0 };
window.onresize = function (e) {
/* set prevWidth/prevHeight, get new width/height */
};
window.onkeydown = function (e) {
if (BrowserWindow.prevWidth !== BrowserWindow.width) { /*...*/ }
};
That would work (except that it would only work when the screen was actively being stretched... so it wouldn't happen in the case where the key was down and the edge of the window was being held but not dragged (which might lead to flickering if the browser fires keydown events more-frequently than resize).
The more appropriate answer would likely be to go the other way:
var Keyboard = {
currentKeys : {},
previousKeys : {}
};
window.onkeydown = function (e) { Keyboard.currentKeys[e.keyCode] = true; };
window.onkeyup = function (e) { delete Keyboard.currentKeys[e.keyCode]; };
window.onresize = function (e) {
var key = <X>,
state = "";
state = Keyboard.currentKeys[key] && !Keyboard.previousKeys[key]
? "pressed"
: !Keyboard.currentKeys[key] && Keyboard.previousKeys[key]
? "released"
: Keyboard.currentKeys[key] && Keyboard.previousKeys[key]
? "held"
: "off";
Keyboard.previousKeys = Keyboard.currentKeys;
doSomething(state);
};
This is less than perfect from an architecture standpoint, but is more along the idea of what you'd have to do in another environment.

Problem with function within $(window).resize - using jQuery

I want to use a simple function to hide/show content only on mobile devices.
The function itself is pretty straightforward. I use this code here for that:
$('.toggleMobile').click(function() {
var hideableContent = $(this).parent().find('.hideable');
hideableContent.slideToggle('medium');
});
So... nothing fancy, i know.
It gets more complicated as i try to detect the browser viewport.
I think I took care of that by using the following lines (you probably will find ways to improve it):
function whatMedia() {
var viewport = $(window).width();
var mediaType;
if ( viewport < 767 )
mediaType = 'mobile';
else if ( (viewport >= 767) && (viewport < 991) )
mediaType = 'tablet';
else
mediaType = 'desktop';
return mediaType;
}
Now i just need a function that gets triggered only when the viewport is mobile (maybe the problem is here?):
function toggleMobile(mediaType) {
if ( mediaType === 'mobile' ) {
$('.toggleMobile').click(function() {
var hideableContent = $(this).parent().find('.hideable');
hideableContent.slideToggle('medium');
});
}
}
I have no problem checking for the viewport the first time the page is loaded.
I just use this (very simple bit of code):
// Check media type and activate accordingly
var mT = whatMedia();
toggleMobile(mT);
So far so good. Now comes the fun part:
I want to be able to detect if a user resizes the browser window and activate/deactive the toggleMobile() function accordingly..
I could do this:
$(window).resize(function() {
var mT = whatMedia();
toggleMobile(mT);
}
As you perhaps already know, this $(window).resize thing makes Webkit and other browsers go a bit crazy, and repeat the function as long as the user resizes the window.
This is good or bad depending on your take on it.
I personally don't want this to happen, so i use this function i found on the forums:
var waitForFinalEvent = (function () {
var timers = {};
return function (callback, ms, uniqueId) {
if (!uniqueId) {
uniqueId = "Don't call this twice without a uniqueId";
}
if (timers[uniqueId]) {
clearTimeout (timers[uniqueId]);
}
timers[uniqueId] = setTimeout(callback, ms);
}
})();
My resize event looks like this:
$(window).resize(function() {
waitForFinalEvent(function() {
var mT = whatMedia();
toggleMobile(mT);
}, 500, '1');
}
This certainly does delay the calculation of the browser window on resize but i can't make the function inside it work.
I don't know what the problem is :(
Th function gets triggered two or more times, and even when the viewport is recognized as desktopor tablet.
In the togglemobile function, you just register the click event but nothing else, if you want to trigger it you could do
$('.toggleMobile').click(function() {
var hideableContent = $(this).parent().find('.hideable');
hideableContent.slideToggle('medium');
}).trigger('click');
this would trigger the click event and run the code, alternatively you could hide the element immediately instead.
EDIT: I'll revise my answer a bit.
First we register the click event for the element that while slideToggle the content:
$('.toggleMobile').click(function() {
if(whatMedia() === "mobile") {
var hideableContent = $(this).parent().find('.hideable');
hideableContent.slideToggle('medium');
}
});
then in the toggleMobile(mt); function we now hide the content if the size goes over to mobile media
function toggleMobile(mediaType) {
if ( mediaType === 'mobile' ) {
var hideableContent = $(".toggleMobile").parent().find('.hideable');
hideableContent.slideToggle('medium');
}
}
this is if i understand, what you want?
I see that this is probably what #sje397 meant.

Categories

Resources