My JavaScript book is out! Don't miss the opportunity to upgrade your beginner or average dev skills.

Monday, November 21, 2011

Differential Background Scrolling

A quick one about this technique quite common in Flash sites but rarely seen on Web.
Have a look at the example first so you can understand what I am talking about ... got it ?

What Is This About

Let's say we have a background, a big massive graphic background surely not suitable for mobile phones, due data roaming, but maybe cool for desktop and fast ADSL.
The background-size CSS property is able to let us decide if the image used as background should fit the whole element or only a portion of it.
In this case the image should fit, by default, the whole height of the document with an auto width in order to let the browser adjust the scale.
A differential scrolling is visible the moment we scroll the page ... please resize the window into a smaller one if you are in an HD monitor and start scrolling the page.
At the very beginning, the default height of the image is 100%, as body background, and with some padding in order to let space for few important image parts such the header, with clouds and enough space for a H1 tag, and the bottom, with the stylish logo of this game from, the one which page inspired this little experiment. Bear in mind no code has been even read from that website ... I have seen the effect, I have used it many times ages ago via ActionScript, I decided to do something similar for most advanced browsers, and here is ...

The Code

(function (document) {
// (C) WebReflection - Mit Style Licence
ratio = .85, // 0 to 1 where 1 is 100%

// shortcuts
styleSheets = document.styleSheets,
documentElement = document.documentElement,
ceil = Math.ceil,
scroll = "scroll",
scrollHeight = scroll + "Height",
scrollTop = scroll + "Top",
body, sHeight, sTop, y, last
styleSheets = styleSheets[styleSheets.length - 1];
// redefine the rule for the height
"body{background-size:auto " + ceil(
ratio * 100
) + "%;}",
// get the rest of the ratio
ratio = 1 - ratio;
// attach a scroll listener
addEventListener(scroll, function (e) {
if (body || (body = document.body)) {
sHeight = documentElement[scrollHeight] ||
sTop = documentElement[scrollTop] ||
y = ceil(
ratio * sHeight * sTop / (sHeight - innerHeight)
// this avoid some redundant assignment
// hopefully creating less flicking effect
if (last != y) { = "center " + (last = y) + "px";
}, false);
// you may want to try this for Chrome Browsers
// = "translateZ(0)";

The Problem

Many of them ... to start with the fact this technique does not scale as showed in this example since for mobile phones, or generally speaking smaller screens, it does not make sense to use such big image: use media queries for this.
Opera 12 is almost there but something goes terribly wrong during background reposition ... it's screwed up by N pixels even if the rest of the logic works and no error is shown on console.
Firefox Nightly goes quite well but it is still flicking a bit while Safari, and even better WebKit Nightly, are the smoothest in this Mac.
The disaster is Chrome Canary, which is not able to handle this background repositioning.
You can see the effect if you scroll fast in both inspiration site and my experiment and, as commented out in the code, the only way to make it better is to force HW acceleration in the whole document 'cause in the body only the background looks broken ... it's really cool to see how DOM is able to mess up with GPUs, isn't it?

As Summary

Nothing much to add to this post, it was just a quick example over a cool effect but as it is, since ever, in this Web field, almost everything went terribly wrong :D
Have fun with CSS and graceful JS enhancements!


kangax said...

Interesting technique!

Your example is unfortunately pretty jerky on my Chrome dev (17) on Macbook Pro :( but more or less smooth on nightly webkit.

Did you notice that original skyrim page doesn't have this scrolling effect in nightly Firefox? Are they using some unsupported CSS for the effect? Sniffing and excluding FF?

I wonder what's going on...

Andrea Giammarchi said...

to remove a bit of "jerkyness" the whole HTML has to be pushed into the GPU, kinda heavy, but it was making Chrome smoother.

I really did not read the code there but I might investigate to see what's going on ... thing is that they are probably not using the background image and the background size but some container behind.

I was rather thinking to file a bug in Chrome because it is clear to me that they repaint before the scroll event is fired plus their repaint is asynchronous or no flicking would be visible.

This is weird for many reasons, first of all the position fixed that works during that repaint but always before the scroll which means many offset() libraries method out there could produce the wrong result.

Andrea Giammarchi said...

P.S. Chrome flicks in their website too here, a bit less, but still flicks :-/

Andrea Giammarchi said...

actually .. webkit does repaint too before scroll event but the background is positioned from last value, the problem with Chrome is that the background is always repositioned from the top, then repaint, then scroll, then repaint again ... so the bug is in the CSS background-size, not in the repaint on scroll ( or better, the bug is different from what I thought and we have two bugs now )