Performance drop when fading element based on scrollTop - javascript

seen afew websites with this effect, however it seems to drop the framerate in my attempt. I basically want to change the opacity of an element the more the user scrolls.
$(window).scroll(function(event){
$("#responsive-slider-with-blocks-1").css("opacity", 1 - $(window).scrollTop() / 1500);
}
Is there a better way to do this? (would be ace just CSS, but not possible).
I'm really not a fan of binding to the scroll event.
Edit:
Due to changing the opacity on an element which covers the entire viewport could be why the framerate drops so much. Would fading in black div covering the element maybe not drop the framerate so much?

Scroll events fire so fast, you're right, every little optimization will help. The docs for the scroll event have advice along those lines:
Since scroll events can fire at a high rate, the event handler shouldn't execute computationally expensive operations such as DOM modifications. Instead, it is recommended to throttle the event using requestAnimationFrame, setTimeout or customEvent...
You can adapt the example they have there to your purposes (and I'm trying to leave out jquery on purpose to remove the overhead):
var last_known_scroll_position = 0;
var ticking = false;
var responsiveSlider = document.getElementById('responsive-slider-with-blocks-1');
function doSomething(scroll_pos) {
responsiveSlider.style.opacity = 1 - scroll_pos / 1500;
}
window.addEventListener('scroll', function(e) {
last_known_scroll_position = window.scrollY;
if (!ticking) {
window.requestAnimationFrame(function() {
doSomething(last_known_scroll_position);
ticking = false;
});
}
ticking = true;
});
This is certainly longer, and there are some global scope messes to consider, but something like this may make the performance difference you are looking for.

Scroll event I believe will be triggered very often during scrolling. When scroll event triggered, jQuery needs to find DOM element based on the selector. This operation alone is quite expensive.
Changing the opacity make it worse as more pixels had to be processed.
Move code to select DOM using jQuery selector outside scroll event handler. That way you can avoid jQuery to lookup DOM element each time scroll event fires.
Limit size of element to reduce number of pixels need to be compute when opacity changed.
Change opacity at certain time interval helps reduce number of paint operations that browser need to do during scrolling operation. So instead of changing opacity everytime event fires, you wait until certain time has elapsed and then change opacity.

Related

jquery: a better way to tie an animation to scrollbar position

var AddFootnoteScrollIndicator = function(){
$('.mobileFootnote').on('scroll touchmove', function (event) {
var scrollTop = that.$mobileFootnote.scrollTop();
if (scrollTop <= 20){
var opacity = 1 - (scrollTop/20);
$('.scroll-down-indicator').css({'opacity': opacity });
}
});
};
As the user scrolls down, the indicator slowly fades out until it is gone. They scroll back up, the indicator slowly re-appears. They stop in the middle, the indicator is half-visible.
Code works fine, but modifying the opacity via .css() seems expensive. Is there a more clever way of doing this via css or...
I don't want to delay the .on() polling because the animation needs to respond quickly to the scroll.
Any ideas?
When it comes to scroll events, modifying the css via javascript is the only way to go. There is not a way with pure CSS to detect scroll positions like you can with media queries and screen sizes.
The jquery css() function is setting the element.style.opacity property under the hood. You are only one short abstraction layer from the actual element property, so it is not "expensive".
The most costly part of that call would be the $('.scroll-down-indicator') selector, as it has to perform a DOM traversal to find elements with the class name.

Scroll snapping to a page section

So I have two sections of content near the top of my page and I’d like for users who have scrolled down to near the top of the second section to get “scroll snapped” to the top of the second one once they have stopped scrolling.
I think it should be possible using jQuery but I haven’t been able to figure it out. Here are my examples:
Without my attempt: http://codepen.io/jifarris/pen/gaVgBp
With my broken attempt: http://codepen.io/jifarris/pen/gaVgQp
Basically I can’t figure out how to make it try scrolling to the spot only once, after scrolling has stopped. It’s kind of just freaking out.
I love how the recently introduced scroll snap points CSS feature handles scroll snapping and I’d almost prefer to use it – for the browsers that support it, at least – but it seems like it only works for items that take up 100% of the viewport height or width, and it seems like it’s for scrolling within an element, not the page itself.
The top section has a fixed height, so this really can be handled with pixel numbers.
And for reference, here’s the heart of the code from my attempt:
$(function() {
$(document).on('scroll', function() {
var top = $(document).scrollTop();
if (top > 255 && top < 455) {
$('html, body').animate({scrollTop: '356'}, 500);
$('body').addClass('hotzone');
} else {
$('body').removeClass('hotzone');
}
});
});
KQI's answer contains most of the steps required to create a well functioning section-scroll for use in your application/webpage.
However, if you'd just want to experiment yourself, developing your script further, the first thing you'll have to do is add a timeout handler. Otherwise your logic, and therefor scrollAnimation, will trigger every single pixel scrolled and create a buggy bouncing effect.
I have provided a working example based on your script here:
http://codepen.io/anon/pen/QjepRZ?editors=001
$(function() {
var timeout;
$(document).on('scroll', function() {
clearTimeout(timeout);
timeout = setTimeout(function() {
var top = $(document).scrollTop();
if (top > 255 && top < 455) {
$('body').animate({
scrollTop: '356'
}, 500);
$('body').addClass('hotzone');
} else {
$('body').removeClass('hotzone');
}
}, 50);
});
});
Good luck!
All right, there are couple of things you gonna have to deal with to get a good result: which are performance, call stack queue, easing.
Performance wise you should drop jQuery animate and use VelocityJs which gives a smoother transition, better frame per second (fps) to avoid screen glitches especially on mobiles.
Call stack: you should wrap whatever logic you have to animate the scrolltop with 'debounce' function, set the delay for let say 500mm and check the scrolling behavior. Just so you know, the 'scroll' listener your using is firing on each pixel change and your script will go crazy and erratic. (It is just gonna be a moment of so many calc at the same time. Debounce will fix that for you)
Easing: make the transition looks cool not just dry snappy movement.
Remember, 'easing' with Velocity starts with 'mina.' i.e.
'Mina.easingFnName'
Finally, your logic could be right, i am in my phone now cannot debug it but try to simplify it and work with a single problem at once, be like i.e.
If ( top > 380 ) // debounce(...)

Flickering with jQuery animate scrollTop

My problem
I am making a vertical website for a client who wishes to have the window "snap" to the nearest page when most of the element is visible in the viewport. So, if the page is 85% visible, it should scroll to be 100% visible.
My problem is that occasionally when scrolling all the way to the top or bottom of the viewport, the viewport will "stick" to the first or last element, preventing a few scroll events and causing a highly noticeable flicker.
A working fiddle is here: http://jsfiddle.net/RTzu8/1/
To reproduce the error, use the scrollbar to scroll to the bottom of the page. Then, scroll up with your mousewheel. You should see the flicker. Sometimes it takes a few refreshes or attempts, but the issue is highly reproducible.
I'm at a loss as to what could be causing this issue. See below for a run-down of my code and what I have tried to prevent it so far.
My code
To accomplish my snapping, I needed to detect whether an element was a certain percentage visible. So, I added a jQuery function, isNearScreen, below. I have thoroughly tested this function, and as far as I can tell it returns accurate results.
//Modification of http://upshots.org/javascript/jquery-test-if-element-is-in-viewport-visible-on-screen
//Returns "true" if element is percent visible within the viewport
$.fn.isNearScreen = function(percent){
var offset = 1 - percent;
var win = $(window);
var viewport = {
top : win.scrollTop()
};
viewport.bottom = viewport.top + win.height();
var bounds = this.offset();
bounds.bottom = bounds.top + this.outerHeight();
bounds.top = bounds.top;
//If the element is visible
if(!(viewport.bottom < bounds.top || viewport.top > bounds.bottom)){
//Get the percentage of the element that's visible
var percentage = (viewport.bottom - bounds.top) / this.height();
//If it's greater than percent, but less than 1 + (1 - percent), return true;
return (percentage > (1 - offset) && percentage < (1 + offset));
}
return false;
};
I then created a snap function, which makes use of Underscore.js's _.debounce function, to only fire on the trailing end of continuous scroll events. It fires after a 500ms timeout, and I am fairly (though not 100%) convinced that it is firing correctly. I have not been able to reproduce console logs that would indicate multiple concurrent firings.
//Snaps the screen to a page after scroll input has stopped arriving.
var snap = _.debounce(function(event){
//Check each page view
$.each($('.page-contents'), function(index, element){
//If the page view is 70% of the screen and we are allowed to snap, snap into view
if($(element).isNearScreen(0.7)){
$('html,body').animate({
scrollTop: $(element).offset().top
}, 300);
}
});
}, 500);
Finally, I bind to the window's scroll event
$(window).on('scroll', snap});
The (extremely simplified) HTML:
<div class="page">
<div class="page-contents"></div>
</div>
<div class="page">
<div class="page-contents"></div>
</div>
<div class="page">
<div class="page-contents"></div>
</div>
<div class="page">
<div class="page-contents"></div>
</div>
and CSS:
.page{
height: 750px;
width: 100%;
margin: 10px 0;
background: gray;
}
.page-contents{
height: 100%;
width: 100%;
}
What I've tried
I have tried the following, with no success:
Setting a boolean, 'preventSnap', on the window, checking its state, and only firing the animate portion of snap if it is set to false. After animation, set it to true, then set it to false after 500ms (which should in theory prevent double firings).
Calling .stop() on the element before running the snap animation.
Calling event.preventDefault() on the scroll event before running the animation.
Reducing and increasing my _.debounce delay. Interestingly, a lower _.debounce delay (200-300ms) seems to aggravate the problem and a higher _.debounce delay (1000ms) seems to fix it. This is not an acceptable solution, however, as it feels "long" waiting 1sec for the page to "snap".
Changing the heights of the elements
If there is any other information I can provide, please let me know. I'm at a loss!
I think this is a combination of events and how _.debounce works. I noticed in the fiddle (in Chrome) that the elements were 'jitterring' long after the snap finished. If you put a console log in the snap event handler you can see it's constantly being called after a snap even with no scroll inputs.
This must be the scroll animation itself setting off the snap, I tried to set a flag to prevent dual snapping and clearing the flag after the animation was finished -- however that didn't work I think because _.debounce is queuing the event to happen later (after the animation finishes and clears the flag).
So what does work is to add this as the start of the snap handler:
var nosnap = false;
var snap = _.debounce(function(event){
// Don't snap if already animating ...
if (nosnap) { nosnap = false; return; }
nosnap = true;
Fiddle
That prevents the animation directly firing the next snap event -- however that's going to cause issues if you scroll again during the animation.
So, that's a bit of a hack. Ideally you want to be able to tell what's causing the scroll event and react accordingly but there's no easy way to do that.
I absolutely think you need to stop the animation when handling a second scroll event as well.

Any options for position during webkit-overflow-scrolling: touch event

I have a div with webkit-overflow-scrolling set to touch. On iOS this then gives me an updated position during the touchmove event, but once the user lets go this event ends and a final call to touchend is made before events all stop, but the div continues to momentum scroll.
This is the behaviour I want, but I also want to update the page during this momentum scrolling.
I trigger a call to requestAnimationFrame when the touchend event happens, and I can loop this while the momentum scroll occurs. But when I get DOM information, it's frozen until after the mometnum scroll ends.
I've tried using both the scroll position of the scrolling element and elementFromPoint, but both just have the position the scrolled div was in at the time touchend was triggered, and don't update until the momentum scroll ends.
Does anyone know of any way to get real time DOM information on iOS (6+, not worried about 5)
Here's some code I'm using:
var glideStart;
var bird_scanner = document.getElementById('bird-scanner');
bird_scanner.addEventListener('touchend',function()
{
glideStart = null;
requestAnimationFrame(glide);
});
function glide(timestamp)
{
// if we need to reset the timestamp
if( glideStart === null )
{
glideStart = timestamp;
}
// determine if we've moved
var bird_scanner = document.getElementById('bird-scanner');
console.log( document.elementFromPoint(337,568) );
// calculate progress (keep running for a very long time so we see what happens when momentum ends)
var progress = timestamp - glideStart;
if( progress < 10000 )
{
requestAnimationFrame(App.Controller.bird.glide);
}
}
Update
After a lot of attempts at this, I think it really is impossible without using some library to try and mimic the momentum scroll instead of using the built in option (something I find never really gives as smooth results). Apple are clearly very worried about things interfering with their momentum scroll animation and preventing it rendering properly.
I ended up removing the momentum scroll and just detecting swipes and moving through a bunch of elements at once when that's triggered.
I did notice some particularly strange behaviour. When I had webkit-overflow-scrolling: touch set on an element that was scrolling the page up/down and added a setTimeout(some_func,0) to the touchend event, the function wasn't triggered until the momentum scroll ended. When I tried the same thing on a scroll going left/right it triggered the function straight away. No clue why this happens, decided it must just be some strange webkit quirk.

javascript bind an event handler to horizontal scroll

Is there a way in javascript to bind an event handler to a horizontal scroll as opposed to the generic scroll event which is fired when the user scrolls horizontally and vertically? I want to trigger an event only when the user scrolls horizontally.
I searched around for an answer to this question, but couldn't seem to find anything.
Thanks!
P.S. My apologies if I'm using some terminology incorrectly. I'm fairly new to javascript.
UPDATE
Thanks so much for all your answers! In summary, it looks like you are all saying that this isn't supported in javascript, but I that I can accomplish the functionality with something like this (using jQuery) (jsFiddle):
var oldScrollTop = $(window).scrollTop();
$(window).bind('scroll', function () {
if (oldScrollTop == $(window).scrollTop())
//scrolled horizontally
else {
//scrolled vertically
oldScrollTop = $(window).scrollTop();
}
});​
That's all I needed to know. Thanks again!
Answering from my phone, so unable to provide code at the moment.
What you'll need to do is subscribe to the scroll event. There isn't a specific one for vertical/horizontal.
Next, you'll need to get some measurements about the current display area. You'll need to measure the window.clientHeight and window.clientWidth.
Next, get window.top and window.left. This will tell you where position of the viewport is, ie if it's greater than 0 then scroll bars have been used.
It's pretty simple math from here to get what you need. If no one else has provided a code example in the next few hours I'll try to do so.
Edit:
A bit further information.
You must capture the scroll event. You also need to store the initial window.top and window.left properties somewhere. Whenever the scroll event happens, do a simple check to see if the current top/left values differ from the stores value.
At this point, if either are different you can trigger your own custom events to indicate vertical or horizontal scrolling. If you are using jQuery, this is very easy. If you are writing js without library assistance, it's easy too but a little more involved.
Do some searches for event dispatching in js.
You can then write any other code you want to subscribe to your custom events without needing to tie them together with method calls.
I wrote a jQuery plugin for you that lets you attach functions to the scrollh event.
See it in action at jsfiddle.net.
/* Enable "scrollh" event jQuery plugin */
(function ($) {
$.fn.enableHScroll = function() {
function handler(el) {
var lastPos = el
.on('scroll', function() {
var newPos = $(this).scrollLeft();
if (newPos !== lastPos) {
$(this).trigger('scrollh', newPos - lastPos);
lastPos = newPos;
}
})
.scrollLeft();
}
return this.each(function() {
var el = $(this);
if (!el.data('hScrollEnabled')) {
el.data('hScrollEnabled', true);
handler(el);
}
});
}
}(jQuery));
It's this easy to use:
$('#container')
.enableHScroll()
.on('scrollh', function(obj, offset) {
$('#info').val(offset);
});
Please note that scroll events come very fast. Even if you click in the scrollbar to jump to a new position, many scroll events are generated. You may want to adjust this code to wait a short time and accumulate all the changes in position during that time before firing the hscroll event.
You can use the same scroll event, but within your handler use the scrollLeft function to see if the scrollbar moved horizontally from the last time the event was fired. If the scrollbar did not move then just return from your handler. Otherwise update your variable to the new position and take action.
You can check if the the x value of the page changes and ignore your y value.
If the x value changes: There is your horizontal scroll.
With page-load, store the initial scrollbar positions for both in two variables (presumably both will be 0). Next, whenever a scroll event occurs, find the scrollleft and scrolltop properties. If the scrollleft property's value is different and scrolltop's value is same as compared to their earlier values, that's a horizontal scroll. Then set the values of the variables to the new scroll values.
No, there is no special event for scroll horizontal (it is for global scroll), but you can try to check the position of content by property .scrollLeft and if it's different from the previous value it means that the user scrolled content horizontally.

Categories

Resources