I am creating a header that acts like the Chrome for Android Address bar. The effect is that the header is a pseudo sticky header that scrolls out of view as you scroll down and then you you begin to scroll back up the header scrolls back into view.
Right now it works fine on the desktop (around 60fps) but on Chrome for Android (on Nexus 7 2013) it is full of jank.
Demo: jsFiddle
Both the header and content area are moved with transform translateY which are more performant than pos:top
I am also using requestAnimationFrame to debounce scrolling and only change properties when it is most convenient for the browser.
The header is position: fixed; top: 0; and then scrolled in and out of view with transform: translateY(...);. Also instead of using margin-top to get the content out from underneath the header, I am using transform: translateY(...);
The basic structure of my js looks like:
var latestScrollTop = 0;
var lastReactedScrollTop = 0;
var ticking = false;
function DoScroll()
{
var builtUpScrollTop = latestScrollTop - lastReactedScrollTop;
// Fold the top bar while we are scrolling (lock it to scrolling)
$('header.main-header').css('transform', 'translateY(' ... 'px)');
HeaderHeightChange();
lastReactedScrollTop = latestScrollTop;
ticking = false;
}
function HeaderHeightChange()
{
// We need to update the margin-top for the content so we don't overlap it
$('main.content-area').css('transform', 'translateY(' ... 'px)');
}
function requestTick() {
if(!ticking) {
requestAnimationFrame(function(){
DoScroll();
});
}
ticking = true;
}
$(window).on('scroll', function(e) {
latestScrollTop = $(window).scrollTop();
requestTick();
});
The effect is not complete as it needs to resolve the fold after you finish scrolling (and is coded) but I do not want to complicate the issue when just the scroll movement lock to header is causing jank. I see paint rectangles when scrolling up and down even though I am changing transform which I assume the gpu is handling and shouldn't be painting.
Edit: It seems when debugging with ADB that there is a a bunch of clear grey outlined time in each frame.
Turns out that even though I was using transform: translateY() that you still need to add translateZ(0) to see the benefit of layers and having it gpu accelerated.
But I did also update my code to use a object literal code style and got rid of the forced synchronous layout warning in the timeline by reading then writing. This is coupled along with requestAnimationFrame.
Demo: jsFiddle
var myUtils = {
clamp: function(min, max, value) {
return Math.min(Math.max(value, min), max);
},
getTranslateYFromTransform: function(rawTransform) {
return parseFloat(rawTransform.match(/^matrix\((([+-]?[0-9]*\.?[0-9]*),\s*?){5}([+-]?[0-9]*\.?[0-9]*)\)$/)[3])
}
};
var scrollHeader = {
latestScrollTop: 0,
lastReactedScrollTop: 0,
headerHeight: 0,
headerTransformTranslateY: 0,
ticking: false,
requestTick: function() {
if(!scrollHeader.ticking) {
requestAnimationFrame(function(){
scrollHeader.doHeaderFold();
});
}
scrollHeader.ticking = true;
},
doHeaderFold: function() {
var header = $('header.main-header');
var builtUpScrollTop = scrollHeader.latestScrollTop - scrollHeader.lastReactedScrollTop;
scrollHeader.headerHeight = header.outerHeight();
scrollHeader.headerTransformTranslateY = myUtils.clamp(-parseInt(scrollHeader.headerHeight), 0, (myUtils.getTranslateYFromTransform(header.css('transform')) - builtUpScrollTop));
// Fold the top bar while we are scrolling (lock it to scrolling)
header.css('transform', 'translateY(' + scrollHeader.headerTransformTranslateY + 'px) translateZ(0)');
scrollHeader.headerHeightChange();
scrollHeader.lastReactedScrollTop = scrollHeader.latestScrollTop;
scrollHeader.ticking = false;
},
headerHeightChange: function() {
// We need to update the margin-top for the content so we don't overlap it
$('main.content-area').css('transform', 'translateY(' + (scrollHeader.headerHeight + scrollHeader.headerTransformTranslateY) + 'px) translateZ(0)');
}
};
$(window).on('scroll', function(e) {
//console.log(e);
scrollHeader.latestScrollTop = $(window).scrollTop();
scrollHeader.requestTick();
});
This makes the timeline debugging on ADB (Nexus 7 2013) look like(very smooth):
Also to get rid of a small jump when first scrolling add transform: translateZ(0) to your element before animating it.
Related
I've got my side bar working almost as I want it, but am having issues at certain page heights:
The side bar should work as follows:
When the "menu" hits the top of the page, an image appears about the menu and the entire side is fixed - working!
If the footer collides with the bottom of the sidebar when fixed, it starts to scroll up - working!
If the window height is smaller than the sidebar height, it never fixes - not quite working.
For point 3, there is an area of about 170 pixels between 581px and 751px (height) where the sidebar starts to jump around. In the example, if the browser height is ~580 or less then it works fine, but if it falls into that zone it starts to become jittery.
I've mimicked the behaviour here:
https://jsbin.com/wokupacebu/edit?html,css,js,console
doesn't quite fit the window sizes in jsbin, so to see a full screen working copy, see here:
https://output.jsbin.com/wokupacebu
here's my JS - the CSS and HTML can be found in the jsbin.
$(function() {
$sideBarContainer = $("#side-bar-container");
$sideBar = $("#side-bar");
$sideImageContainer = $("#side-image-container");
$sideImage = $("#side-image")
$footer = $("#footer");
$window = $(window);
$(window).on("scroll.sidebar resize.sidebar",function() {
setSideBarFixable();
});
function setSideBarFixable() {
if ($window.height() < $sideBar.height() + $sideImage.outerHeight()) {
$sideBar.removeClass("fixed");
$sideBarContainer.css("width","auto");
$sideImageContainer.css("height","0");
return;
} else {
if ($window.scrollTop() >= $sideBarContainer.position().top) {
$sideBar.addClass("fixed");
$sideBarContainer.css("width",$sideBar.outerWidth() + "px");
$sideImageContainer.css("height",$sideImage.outerHeight() + +"px");
} else {
$sideBar.removeClass("fixed");
$sideBarContainer.css("width","auto");
$sideImageContainer.css("height","0");
}
var diff = $footer.position().top - $window.scrollTop() - $sideBar.outerHeight();
if (diff <= 0) {
$sideBar.css("top",diff+"px");
} else {
$sideBar.css("top","0");
}
}
}
I am trying to create a landing page that has a parallax effect. The problem is that when scrolling, the assets seem to move at less than 15 fps. In this performance, the parallax effect is not noticeable. The object inside the element bg_container is more than 10000px in height.
What is wrong with my code or is there a better implementation for this. This is the temporary site of my site: http://royvon.therookieblog.com/
Here the version 1 of my code in which the objects are moved within the window scroll event:
$(document).ready(function() {
var bg_speed = 0.7;
var tree_speed = 0.8;
var mid_speed = 0.8;
var fast_speed = 1.0;
$(window).scroll(function () {
var scrolled = $(window).scrollTop();
$('#bg_container').css('top',(0-(scrolled*bg_speed))+'px');
$('#map_1home_container').css('top',(0-(scrolled*bg_speed))+'px');
$('#map_2playGround_container').css('top',(0-(scrolled*bg_speed))+'px');
$('#map_3camp_container').css('top',(0-(scrolled*bg_speed))+'px');
//AND SOME 15 OBEJCTS HERE
});
});
Here is the version 2 of my code that uses setTimeout to move the assets:
$(document).ready(function() {
var scrollTimer = null;
$(window).scroll(function () {
if (scrollTimer) {
clearTimeout(scrollTimer); // clear any previous pending timer
}
scrollTimer = setTimeout(moveObjects, 25); // set new timer
});
});
function moveObjects() {
var bg_speed = 0.7;
var tree_speed = 0.8;
var mid_speed = 0.8;
var fast_speed = 1.0;
var scrolled = $(window).scrollTop();
$('#bg_container').stop(true, true).animate({'top':(0-(scrolled*bg_speed))+'px'}, 25);
$('#map_1home_container').stop(true, true).css({'top':(0-(scrolled*bg_speed))+'px'},25);
$('#map_2playGround_container').css('top',(0-(scrolled*bg_speed))+'px');
$('#map_3camp_container').css('top',(0-(scrolled*bg_speed))+'px');
//AND SOME other assets here
}
In version 2, I have tried changing the number in setTimeout. I also tried in version 2 changing the .css to .animate but the performance is still the same.
For version 1, Firefox and Chrome seems to display the parallax in less than 15fps but in internet explorer 9 and 10, it's very smooth.
I have tried viewing both versions in Chrome for android and the performance was catastrophic.
Is there something that can be done in this code? Or I just tell the client leave this idea?
I am building a full page slider that keeps the native scrollbar and allows the user to either free scroll, use the mouse wheel or navigation dots (on the left) to switch to a slide.
Once the user is on the last slide and tries to scroll down further, the whole slider moves up to reveal a simple scrollable section. If the user scrolls down and then tries to go back up, then this new section moves out of the way again and returns the slider back into view.
Fiddle: http://jsfiddle.net/3odc8zmx/
The parts I'm struggling with:
Only the first two navigation dots work. The third one DOES WORK if you area looking at the first slide. But doesn't do anything, if you are on slide 2. Note: the purple one is a short-cut to the second section of the page and not related to the slider.
When moving to the last slide (via the dots, if you're on the first slide) it causes the code to make the whole slider move upwards as it sees this as the user has slid past the last slide as per the description above. I have tried to combat this using a variable called listen to stop the scroll event listening when using the showSlide method... but it seems to be true even though I set it to false, and only reset it to true again after the animation...
When scrolling down using the mouse wheel, I can get to the second section and back up, but not to the first third section. I'm wondering if I could use the showSlide method to better handle this instead of the current dirty next and prev functions I have implemented.
Note: If the user has free-scrolled, when they use the mouse-wheel, I want the slider to snap to the nearest slide to correct itself... Any suggestions for how I could do this?
Can anyone offer some help?
Here's the JS:
var listen = true;
function nextSlide()
{
$('#section1').stop(true,false).animate({
scrollTop: $('#section1').scrollTop() + $(window).height()
});
}
function prevSlide()
{
$('#section1').stop(true,false).animate({
scrollTop: -$('#section1').scrollTop() + $(window).height()
});
}
function showSlide(index)
{
var offset = $('#section1 div').eq(index).offset();
offset = offset.top;
if(offset){
listen = false;
$('.slide-dot').removeClass('active');
$('.slide-dot').eq(index).addClass('active');
$('#section1').stop(true,false).animate({
scrollTop: offset
}, 500, function(){
listen = true;
});
} else {
alert('error');
}
}
$(document).ready(function(){
var fullHeight = 0;
$('#section1 div').each(function(){
fullHeight = fullHeight + $(this).height();
});
var lastScrollTop1 = 0;
$('#section1').on('scroll', function(e){
var st = $(this).scrollTop();
if (st > lastScrollTop1){
if( $('#section1').scrollTop() + $(window).height() == fullHeight) {
if(listen){
$('body').addClass('shifted');
}
}
}
lastScrollTop1 = st;
});
$('#section1').on('mousewheel', function(e){
e.preventDefault();
var st = $(this).scrollTop();
if (st > lastScrollTop1){
nextSlide();
} else {
prevSlide();
}
});
var lastScrollTop2 = 0;
$('#section2').on('scroll', function(e){
var st = $(this).scrollTop();
if (st > lastScrollTop1){
} else {
if( st == 0 ){
$('body').removeClass('shifted');
}
}
lastScrollTop1 = st;
});
$('.slide-dots').css({'margin-top':-$('.slide-dots').height() / 2});
$('.slide-dot').first().addClass('active');
$(document).on('click', '.slide-dot', function(e){
e.preventDefault();
showSlide( $(this).index() );
});
$(document).on('click', '.slide-dot-fake', function(e){
e.preventDefault();
$('body').addClass('shifted');
});
});
And for those wondering why I'm not using something like fullPage.js, it's because it can't handle the way I want to transition between the two areas and have two scrollbars (one for each area).
You can use:
e.originalEvent.wheelDelta
instead of:
st > lastScrollTop1
in the mousewheel event for your third problem to check if the user has scrolled up or down. And also change the +/- in prevSlide. I used dm4web's fiddle for your first problem. And I used:
scrollTop: offset - 1
instead of:
scrollTop: offset
for your second problem, because when the scroll reaches to the last pixel of the third element, it automatically goes to the next section, so 1 pixel is enough for it not to.
Here's the fiddle: http://jsfiddle.net/3odc8zmx/3/
As suggested by #chdltest, you could do it by using fullPage.js.
Here's an example. Go to the last section.
Code used for the example:
Javascript
$('#fullpage').fullpage({
sectionsColor: ['yellow', 'orange', '#C0C0C0', '#ADD8E6'],
scrollOverflow: true,
scrollBar: true,
afterLoad: function (anchor, index) {
//hiding the main scroll bar
if (index == 4) {
$('body, html').css('overflow', 'hidden');
}
//showing the main scroll bar
if (index == 3) {
$('body, html').css('overflow', 'visible');
}
}
});
CSS (in case you prefer to use the normal style for it)
/* Normal style scroll bar
* --------------------------------------- */
.slimScrollBar {
display: none !important;
}
.fp-scrollable {
overflow: auto !important;
}
Advantages of using fullPage.js instead to your own code:
Strongly tested in different devices and browsers. (IE, Opera, Safari, Chrome, Firefox..)
Prevent problems with trackpads, Apple laptops trackpads or Apple Magic Mouse.
Old browser's compatibility, such as IE 8, Opera 12...
Touch devices compatibility (IE Windows Phone, Android, Apple iOS, touch desktops...)
It provides many other useful options and callbacks.
I have this event:
$(window).scroll(function(e){
console.log(e);
})
I want to know, how much I have scroll value in pixels, because I think, scroll value depends from window size and screen resolution.
Function parameter e does not contains this information.
I can store $(window).scrollTop() after every scroll and calculate difference, but can I do it differently?
The "scroll value" does not depend on the window size or screen resolution. The "scroll value" is simply the number of pixels scrolled.
However, whether you are able to scroll at all, and the amount you can scroll is based on available real estate for the container and the dimensions of the content within the container (in this case the container is document.documentElement, or document.body for older browsers).
You are correct that the scroll event does not contain this information. It does not provide a delta property to indicate the number of pixels scrolled. This is true for the native scroll event and the jQuery scroll event. This seems like it would be a useful feature to have, similar to how mousewheel events provide properties for X and Y delta.
I do not know, and will not speculate upon, why the powers-that-be did not provide a delta property for scroll, but that is out of scope for this question (feel free to post a separate question about this).
The method you are using of storing scrollTop in a variable and comparing it to the current scrollTop is the best (and only) method I have found. However, you can simplify this a bit by extending jQuery to provide a new custom event, per this article: http://learn.jquery.com/events/event-extensions/
Here is an example extension I created that works with window / document scrolling. It is a custom event called scrolldelta that automatically tracks the X and Y delta (as scrollLeftDelta and scrollTopDelta, respectively). I have not tried it with other elements; leaving this as exercise for the reader. This works in currrent versions of Chrome and Firefox. It uses the trick for getting the sum of document.documentElement.scrollTop and document.body.scrollTop to handle the bug where Chrome updates body.scrollTop instead of documentElement.scrollTop (IE and FF update documentElement.scrollTop; see https://code.google.com/p/chromium/issues/detail?id=2891).
JSFiddle demo: http://jsfiddle.net/tew9zxc1/
Runnable Snippet (scroll down and click Run code snippet):
// custom 'scrolldelta' event extends 'scroll' event
jQuery.event.special.scrolldelta = {
delegateType: "scroll",
bindType: "scroll",
handle: function (event) {
var handleObj = event.handleObj;
var targetData = jQuery.data(event.target);
var ret = null;
var elem = event.target;
var isDoc = elem === document;
var oldTop = targetData.top || 0;
var oldLeft = targetData.left || 0;
targetData.top = isDoc ? elem.documentElement.scrollTop + elem.body.scrollTop : elem.scrollTop;
targetData.left = isDoc ? elem.documentElement.scrollLeft + elem.body.scrollLeft : elem.scrollLeft;
event.scrollTopDelta = targetData.top - oldTop;
event.scrollTop = targetData.top;
event.scrollLeftDelta = targetData.left - oldLeft;
event.scrollLeft = targetData.left;
event.type = handleObj.origType;
ret = handleObj.handler.apply(this, arguments);
event.type = handleObj.type;
return ret;
}
};
// bind to custom 'scrolldelta' event
$(window).on('scrolldelta', function (e) {
var top = e.scrollTop;
var topDelta = e.scrollTopDelta;
var left = e.scrollLeft;
var leftDelta = e.scrollLeftDelta;
// do stuff with the above info; for now just display it to user
var feedbackText = 'scrollTop: ' + top.toString() + 'px (' + (topDelta >= 0 ? '+' : '') + topDelta.toString() + 'px), scrollLeft: ' + left.toString() + 'px (' + (leftDelta >= 0 ? '+' : '') + leftDelta.toString() + 'px)';
document.getElementById('feedback').innerHTML = feedbackText;
});
#content {
/* make window tall enough for vertical scroll */
height: 2000px;
/* make window wide enough for horizontal scroll */
width: 2000px;
/* visualization of scrollable content */
background-color: blue;
}
#feedback {
border:2px solid red;
padding: 4px;
color: black;
position: fixed;
top: 0;
height: 20px;
background-color: #fff;
font-family:'Segoe UI', 'Arial';
}
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<div id='feedback'>scrollTop: 0px, scrollLeft: 0px</div>
<div id='content'></div>
Note that you may want debounce the event depending on what you are doing. You didn't provide very much context in your question, but if you give a better example of what you are actually using this info for we can provide a better answer. (Please show more of your code, and how you are using the "scroll value").
To detemine how many pixels were scrolled you have to keep in mind that the scroll event gets fired almost every pixel that you move. The way to accomplish it is to save the previous scrolled value and compare that in a timeout. Like this:
var scrollValue = 0;
var scrollTimeout = false
$(window).scroll(function(event){
/* Clear it so the function only triggers when scroll events have stopped firing*/
clearTimeout(scrollTimeout);
/* Set it so it fires after a second, but gets cleared after a new triggered event*/
scrollTimeout = setTimeout(function(){
var scrolled = $(document).scrollTop() - scrollValue;
scrollValue = $(document).scrollTop();
alert("The value scrolled was " + scrolled);
}, 1000);
});
This way you will get the amount of scrolled a second after scrolling (this is adjustable but you have to keep in mind that the smooth scrolling that is so prevalent today has some run-out time and you dont want to trigger before a full stop).
The other way to do this? Yes, possible, with jQuery Mobile
I do not appreciate this solution, because it is necessary to include heavy jQuery mobile. Solution:
var diff, top = 0;
$(document).on("scrollstart",function () {
// event fired when scrolling is started
top = $(window).scrollTop();
});
$(document).on("scrollstop",function () {
// event fired when scrolling is stopped
diff = Math.abs($(window).scrollTop() - top);
});
To reduce the used processing power by adding a timer to a Jquery scroll method is probably not a great idea. The visual effect is indeed quite bad.
The whole web browsing experience could be made much better by hiding the scrolling element just when the scroll begins and making it slide in (at the right position) some time after. The scrolling even can be checked with a delay too.
This solution works great.
$(document).ready(function() {
var element = $('.movable_div'),
originalY = element.offset().top;
element.css('position', 'relative');
$(window).on('scroll', function(event) {
var scrollTop = $(window).scrollTop();
element.hide();
element.stop(false, false).animate({
top: scrollTop < originalY
? 0
: scrollTop - originalY + 35
}, 2000,function(){element.slideDown(500,"swing");});
});
});
Live demo here
On my website, I have a sidebar DIV on the left and a text DIV on the right. I wanted to make the sidebar follow the reader as he or she scrolls down so I DuckDuckGo'ed a bit and found this then modified it slightly to my needs:
<script type='text/javascript'>//<![CDATA[
$(window).load(function(){
$(function(){
var $sidebar = $('#sidebar'),
sidebarOffset = $sidebar.offset(),
$window = $(window),
gap = $('#header').css('marginBottom').replace(/[^-\d\.]/g, ''),
distance = ($window.scrollTop()) - (sidebarOffset.top - gap),
footerHeight = $('#footer').outerHeight();
$window.scroll(function(){
distance = ($window.scrollTop()) - (sidebarOffset.top - gap);
if ( distance > 0 ) {
$sidebar.css({'top': gap + 'px', 'position' : 'fixed'});
} else {
$sidebar.css({'top': '0', 'position': 'relative'});
}
})
});
});//]]>
</script>
And it works just like I want it to. However, my website uses Skeleton framework to handle responsive design. I've designed it so that when it goes down to mobile devices (horizontal then vertical), sidebar moves from being to the left of the text to being above it so that text DIV can take 100% width. As you can probably imagine, this script causes the sidebar to cover parts of text as you scroll down.
I am completely new to jQuery and I am doing my best through trial-and-error but I've given up. What I need help with is to make this script not execute if a certain DIV has a certain CSS value (i.e. #header-logo is display: none).
Ideally, the script should check for this when user resizes the browser, not on website load, in case user resizes the browser window from normal size to mobile size.
I imagine it should be enough to wrap it in some IF-ELSE statement but I am starting to pull the hair out of my head by now. And since I don't have too much hair anyway, I need help!
Thanks a lot in advance!
This function will execute on window resize and will check if #header-logo is visible.
$(window).resize(function() {
if ($('#header-logo').is(':visible')) {
// Your code
}
});
I think you need to check this on load to, because you don't know if the user will start with mobile view or not. You could do something like this:
$(window).resize(function() {
if ($('#header-logo').is(':visible')) {
// Your code
}
}).resize();
This will get executed on load and on resize.
EDIT: You will probably need to turn off the scroll function if #header-logo is not visible. So, instead of create the function inside the scroll event, you need to create it outside:
$(window).resize(function() {
if ($('#header-logo').is(':visible')) {
var $sidebar = $('#sidebar'),
sidebarOffset = $sidebar.offset(),
$window = $(window),
gap = $('#header').css('marginBottom').replace(/[^-\d\.]/g, ''),
distance = ($window.scrollTop()) - (sidebarOffset.top - gap),
footerHeight = $('#footer').outerHeight();
function myScroll() {
distance = ($window.scrollTop()) - (sidebarOffset.top - gap);
if ( distance > 0 ) {
$sidebar.css({'top': gap + 'px', 'position' : 'fixed'});
} else {
$sidebar.css({'top': '0', 'position': 'relative'});
}
}
$window.on('scroll', myScroll);
} else {
$(window).off('scroll', myScroll);
}
});
Didn't test it, but you get the idea.
$("#headerLogo").css("display") will get you the value.
http://api.jquery.com/css/
I also see you only want this to happen on resize, so wrap it in jquery's resize() function:
https://api.jquery.com/resize/