Yet Another Parallax Effect, but This One Wastes Fewer Pixels and Bytes.

Everyone loves the parallax effect, and I’ve been swiping snippets of code to do it for a long time, but never really thinking about it too much. You load it, set some parameters, and that’s it. I was having a little glitch with one, and was tweaking it, and I started to wonder why the effect didn’t just start at one end of my image, and end at the other end.

After all, if I don’t show all the pixels, I’ve “wasted” all those network bytes to load the data, and all the memory to hold the data.

This parallax effect is stingy with network bytes.

It’s also simpler. I find it tedious, and a little confusing, to set the parameters to adjust the speed. This effect that will vertically pan over the entire image as the DIV that reveals it scrolls upward. You don’t actually see the entire image, because some of it is revealed below the bottom of the viewport, and it scrolls up off the top of the viewport.

Resize the image to adjust the scroll speed. If you want a slow scroll, use a background image that’s only slightly larger than the DIV.

There are three versions below: a Javascript first draft, a jQuery version, and a link to a version that automatically attaches itself to elements based on an attribute.

This first draft has all the necessary features: it loads the image, and then gets its dimensions. It figures out the extent of the motion, and then sets handlers to tweak the background-position.

It’s in plain old Javascript, and works only in Firefox.

<!doctype html>
<html>
    <body>
        <style>
            #header {
                height: 1000px;
                background-color: #eef;
            }
            #main {
                height: 400px;
            }
            #footer {
                height: 1000px;
                background-color: #eef;
            }
        </style>
        <div id="header">
        </div>
        <div id="main">
        </div>
        <div id="footer">
        </div>
        <script src=""></script>
        <script>
            // load an Image
            var image = new Image();
            var main = document.getElementById('main');
            var mainOffsetTop;
            var viewportHeight;
            var minPos;
            var maxPos;
            var backgroundHeight;


            var loadImage = new Promise(function(resolve, reject) {
                image.addEventListener("loadend", function() {
                    resolve(image);
                });
                image.src = "http://localhost/wp-content/uploads/2016/09/snow-on-pine.jpg";
            });

            function init() {
                mainOffsetTop = main.offsetTop;
                viewportHeight = Math.max(document.documentElement.clientHeight, window.innerHeight || 0);
                // min and max positions are calculated relative to the top of main.
                minPos = Math.max(-main.scrollHeight, mainOffsetTop - (document.documentElement.scrollHeight - viewportHeight) );
                maxPos = Math.max(main.offsetTop, viewportHeight); 
                backgroundHeight = image.height;
                setPosition(document.documentElement.scrollTop);
            }

            loadImage.then(function(i) {
                main.style.backgroundImage = 'url(' + i.src + ')';
                init();
                document.addEventListener("scroll", parallaxScrollHandler);
                window.addEventListener("resize", init);
            });

            function parallaxScrollHandler(evt) {
                setPosition(evt.pageY);
            }

            function setPosition(pageY) {
                var mainOffsetY = mainOffsetTop - pageY;
                if (minPos < 0) {
                    mainOffsetY -= minPos;
                }
                var scaled = mainOffsetY / (maxPos - minPos);
                var bgY = -Math.floor( scaled * (backgroundHeight - main.scrollHeight) );
                main.style.backgroundPosition = "0px " + bgY + "px";
            }

        </script>
    </body>
</html>

There are a lot of adjustments in there to calculate the right distances, and it’s hard to keep straight. jQuery would help out here, because it reorients things more sensibly.

The odd logic is there to deal with two basic scenarios: tall pages where you can scroll the parallax effect DIV completely off the viewport, and short pages where the DIV cannot scroll off the viewport. In the former situation, you scale your motions across a range taller than the viewport, and in the latter, you scale it within the viewport. I couldn’t find an easy, general way to deal with this. (It’s really bothering me, and I think it’s a bug. It seems wrong.)

There you go. Not too much code, and maximum pixel-stingyness.

jQuery Version

Here is the jQuery version of the Javascript parts. It now works in Chrome:

        <script>
        (function($) {
            var $image = $('<img src="http://localhost/wp-content/uploads/2016/09/snow-on-pine.jpg">');
            var $main = $('#main');
            var mainHeight;
            var mainOffsetTop;
            var viewportHeight;
            var minPos;
            var maxPos;
            var backgroundHeight;

            (new Promise( (resolve, reject) => {

                $image.on("load", () => { resolve($image); });

            })).then( ($image) => {
                $main.css('backgroundImage','url(' + $image.attr('src') + ')');
                init();
                $(document).on("scroll", parallaxScrollHandler);
                $(window).on("resize", init);
            });

            function init() {
                mainOffsetTop = $main.offset().top;
                mainHeight = $main.height();
                viewportHeight = $(window).height();
                // min and max positions are calculated relative to the top of main.
                minPos = Math.max(-mainHeight, mainOffsetTop - ($(document).height() - viewportHeight) );
                maxPos = Math.max(mainOffsetTop, viewportHeight); 
                backgroundHeight = $image[0].height;
                parallaxScrollHandler();
            }

            function parallaxScrollHandler() {
                var mainOffsetY = mainOffsetTop - $(document).scrollTop();
                if (minPos < 0) {
                    mainOffsetY -= minPos;
                }
                var bgY = -Math.floor( (backgroundHeight - mainHeight) * mainOffsetY / (maxPos - minPos) );
                $main.css('backgroundPosition',"50% " + bgY + "px");
            }

        })(jQuery);
        </script>

Attribute-based Version

Linked is a version that will apply the parallax scrolling effect to any element with a data-parallax-image=”[someurl]” attribute. It also doesn’t pollute the global namespace in the way that the above two do.

Demo: Parallax Behavior that Attaches to an Attribute

Leave a Reply