0

I have 3 elements with background images that I want to scale from x to 1. I want it to begin when the top of the element is just inside the viewport (this may vary a little).

I have achieved this effect the long way:

function animatePanelBackgrounds() {
    var $toolsBG   = $('#js-tools-bg'),
        $dennysBG  = $('#js-dennys-bg'),
        $verizonBG = $('#js-verizon-bg'),
        $llbeanBG  = $('#js-llbean-bg');
 
    var dennysTop = Math.floor( $("#js-dennys").offset().top );
    var dennysGo = dennysTop - window.innerHeight;
 
    var llbeanTop = Math.floor( $("#js-llbean").offset().top );
    var llbeanGo = llbeanTop - window.innerHeight;
 
    var verizonTop = Math.floor( $("#js-verizon").offset().top );
    var verizonGo = verizonTop - window.innerHeight;
 
    var toolsTop = Math.floor($toolsBG.offset().top);
    var toolsGo = 0;
 
    var ratio, $that;
 
    if ( thisWindows.offsetY() >= toolsGo ) {
        ratio = toolsTop/(thisWindows.offsetY()*10);
        $that = $toolsBG;
 
        $that.css({
            "transform": "scale(" + (1.0 + thisWindows.offsetY()*.0002) + ")",
            "-webkit-transform": "scale(" + (1.0 + thisWindows.offsetY()*.0002) + ")",
            "-moz-transform": "scale(" + (1.0 + thisWindows.offsetY()*.0002) + ")"
        })
    }
 
    if ( thisWindows.offsetY() >= dennysGo ) {
        ratio = dennysTop/thisWindows.offsetY()*.8;
        $that = $dennysBG;
 
        if ( ratio <= 1 ) {
            $that.css({
                "transform": "scale(1)",
                "-webkit-transform": "scale(1)",
                "-moz-transform": "scale(1)"
            })
        } else {
            $that.css({
                "transform": "scale(" + ratio + ")",
                "-webkit-transform": "scale(" + ratio + ")",
                "-moz-transform": "scale(" + ratio + ")"
            })
        }
    }
 
    if ( thisWindows.offsetY() >= verizonGo ) {
        ratio = verizonTop/thisWindows.offsetY()*.8;
        $that = $verizonBG;
 
        if ( ratio <= 1 ) {
            $that.css({
                "transform": "scale(1)",
                "-webkit-transform": "scale(1)",
                "-moz-transform": "scale(1)"
            })
        } else {
            $that.css({
                "transform": "scale(" + ratio + ")",
                "-webkit-transform": "scale(" + ratio + ")",
                "-moz-transform": "scale(" + ratio + ")"
            })
        }
    }
 
    if ( thisWindows.offsetY() >= llbeanGo ) {
        ratio = llbeanTop/thisWindows.offsetY()*.8;
        $that = $llbeanBG;
 
        if ( ratio <= 1 ) {
            $that.css({
                "transform": "scale(1)",
                "-webkit-transform": "scale(1)",
                "-moz-transform": "scale(1)"
            })
        } else {
            $that.css({
                "transform": "scale(" + ratio + ")",
                "-webkit-transform": "scale(" + ratio + ")",
                "-moz-transform": "scale(" + ratio + ")"
            })
        }
    }
}

$(window).on('scroll', function() {
  animatePanelBackgrounds();
}

I have also achieved this with a function that take a couple simple parameters:

function scaleBackground(element, multiplier) {
 
    var $el           = $(element),
        elTop         = Math.floor( $el.offset().top),
        startPosition = elTop - window.innerHeight;
 
    $win.on('scroll', function() {
 
        if(thisWindows.offsetY() >= startPosition) {
 
            var ratio = elTop/thisWindows.offsetY()*multiplier;
 
            if ( ratio <= 1 ) {
                $el.css({
                    "transform": "scale(1)",
                    "-webkit-transform": "scale(1)",
                    "-moz-transform": "scale(1)"
                })
            } else {
                $el.css({
                    "transform": "scale(" + ratio + ")",
                    "-webkit-transform": "scale(" + ratio + ")",
                    "-moz-transform": "scale(" + ratio + ")"
                })
            }
        }
    })
 
}
 
scaleBackground('#js-dennys-bg', '.8');
scaleBackground('#js-llbean-bg', '.8');
scaleBackground('#js-verizon-bg', '.8');

I feel like this should be handled in a loop of some sorts, but I haven't had any luck. Here's my basic attempt, I've tried tweaking little things in it along the way with 0 success:

var panels = $('.panel__bg');
 
for ( i = 0; i < panels.length; i++ ) {
    var $that = $(panels[i]),
        begin = $that.offset().top;
 
    if ( begin <= window.scrollY ) {
        var ratio = begin/(window.scrollY * 10);
 
        if ( ratio <= 1 ) {
            $that.css({
                "transform": "scale(1)",
                "-webkit-transform": "scale(1)",
                "-moz-transform": "scale(1)"
            })
        } else {
            $that.css({
                "transform": "scale(" + ratio + ")",
                "-webkit-transform": "scale(" + ratio + ")",
                "-moz-transform": "scale(" + ratio + ")"
            })
        }
    }
}

My question, finally, is simply: What is the best way to do this. By "best way", I am extremely concerned with performance and secondly concerned with readability/fewest lines of code.

Tyler Sloan
  • 246
  • 1
  • 2
  • 9
  • Could you add HTML to the snippets so they can be run? – bjb568 Apr 09 '15 at 21:19
  • It's kind of difficult to tell what the problem is here, and there's a ton of code to sift through. It would be much easier to help you if there is a jsfiddle. – posit labs Apr 09 '15 at 21:26
  • from performance point of view could cut down number of actual manipulations by checking state of elements first. Keep in mind that scoll event fires many times a second so there is no need to be constantly updating elements with same properties if they already exist. Also toggling classes is a good suggestion over inline css – charlietfl Apr 09 '15 at 21:46

2 Answers2

0

Here is some critique for your code. It's untested, but hopefully it addresses some of your concerns.

// .each is easier to understand, and cleaner looking than a for loop
$('.panel__bg').each(function(i, panel){

    // name this something that makes more sense than "$that", e.g. "$panel"
    var $panel = $(panel), 
        begin = $panel.offset().top;

    if ( begin <= window.scrollY ) {
        // caps to ratio=1. no need for if/else block
        var ratio = Math.min(1, begin/(window.scrollY * 10)); 

        // on SO, it helps to only show code relevant to the issue
        $panel.css({"transform": "scale(" + ratio + ")" });
    }
});

I assume this code is being run every time the scroll event is fired. You might run into trouble on ios because js execution is blocked during scroll. See this issue.

Community
  • 1
  • 1
posit labs
  • 8,951
  • 4
  • 36
  • 66
0

Not sure if this is really THE BEST way to do it, but here are some of the things I would do:

1 - as suggested already, using CSS and toggle class is probably better than using jQuery CSS method. And since all your elements share same styles instead of IDs you can use common class (as you already did in second snippet), so we have something like this:

.panel__bg {
    transform: scale(0.8);
}

.panel__bg.is-visible {
    transform: scale(1);
}

2 - This is small jQuery plugin used to check elements visibility:

(function($) {

  /**
   * Copyright 2012, Digital Fusion
   * Licensed under the MIT license.
   * http://teamdf.com/jquery-plugins/license/
   *
   * @author Sam Sehnert
   * @desc A small plugin that checks whether elements are within
   *     the user visible viewport of a web browser.
   *     only accounts for vertical position, not horizontal.
   */

  $.fn.visible = function(partial) {

      var $t            = $(this),
          $w            = $(window),
          viewTop       = $w.scrollTop(),
          viewBottom    = viewTop + $w.height(),
          _top          = $t.offset().top,
          _bottom       = _top + $t.height(),
          // if partial === false, visible will be true when 100% of element is shown
          compareTop    = partial === true ? _bottom : _top,
          compareBottom = partial === true ? _top : _bottom;

    return ((compareBottom <= viewBottom) && (compareTop >= viewTop));

  };

})(jQuery);

3 - since you are listening for scroll events you should probably look into requestAnimationFrame and include this polyfill if you need support for IE9 or less and Android 4.3 or less.

(function() {
    var lastTime = 0;
    var vendors = ['ms', 'moz', 'webkit', 'o'];
    for(var x = 0; x < vendors.length && !window.requestAnimationFrame; ++x) {
        window.requestAnimationFrame = window[vendors[x]+'RequestAnimationFrame'];
        window.cancelAnimationFrame = window[vendors[x]+'CancelAnimationFrame'] 
                                   || window[vendors[x]+'CancelRequestAnimationFrame'];
    }

    if (!window.requestAnimationFrame)
        window.requestAnimationFrame = function(callback, element) {
            var currTime = new Date().getTime();
            var timeToCall = Math.max(0, 16 - (currTime - lastTime));
            var id = window.setTimeout(function() { callback(currTime + timeToCall); }, 
              timeToCall);
            lastTime = currTime + timeToCall;
            return id;
        };

    if (!window.cancelAnimationFrame)
        window.cancelAnimationFrame = function(id) {
            clearTimeout(id);
        };
}());

4 - utilizing rAF you can optimize scroll events or even avoid them. I didn't test for performance difference between two linked examples, but I choose latter for brevity

5 - finally, your scaleBackground function is now basically just class toggler thingy that accepts element class and modifier class

(parts 4 & 5 are wrapped inside IIFE to avoid global variables)

(function() {

    var scaleBackground = function(element, triggerClass) {

        var $element = $(element);

        $element.each(function(i, el) {

            var el = $(el);
            // check if it's shown
            if (!el.hasClass(triggerClass) && el.visible(true)) {
                el.addClass(triggerClass);
            }

        });

    };

    var lastPosition = -1;

    var loop = function() {

        // Avoid calculations if not needed
        if (lastPosition == window.pageYOffset) {
            requestAnimationFrame(loop);
            return false;
        } else {
            lastPosition = window.pageYOffset;
        }

        // do stuff you need to do
        scaleBackground(".panel__bg", "is-visible");

        requestAnimationFrame(loop);

    };

    loop();

})();

// http://paulirish.com/2011/requestanimationframe-for-smart-animating/
// http://my.opera.com/emoller/blog/2011/12/20/requestanimationframe-for-smart-er-animating
 
// requestAnimationFrame polyfill by Erik Möller. fixes from Paul Irish and Tino Zijdel
 
// MIT license
 
(function() {
    var lastTime = 0;
    var vendors = ['ms', 'moz', 'webkit', 'o'];
    for(var x = 0; x < vendors.length && !window.requestAnimationFrame; ++x) {
        window.requestAnimationFrame = window[vendors[x]+'RequestAnimationFrame'];
        window.cancelAnimationFrame = window[vendors[x]+'CancelAnimationFrame'] 
                                   || window[vendors[x]+'CancelRequestAnimationFrame'];
    }
 
    if (!window.requestAnimationFrame)
        window.requestAnimationFrame = function(callback, element) {
            var currTime = new Date().getTime();
            var timeToCall = Math.max(0, 16 - (currTime - lastTime));
            var id = window.setTimeout(function() { callback(currTime + timeToCall); }, 
              timeToCall);
            lastTime = currTime + timeToCall;
            return id;
        };
 
    if (!window.cancelAnimationFrame)
        window.cancelAnimationFrame = function(id) {
            clearTimeout(id);
        };
})();


(function($) {

  /**
   * Copyright 2012, Digital Fusion
   * Licensed under the MIT license.
   * http://teamdf.com/jquery-plugins/license/
   *
   * @author Sam Sehnert
   * @desc A small plugin that checks whether elements are within
   *     the user visible viewport of a web browser.
   *     only accounts for vertical position, not horizontal.
   */

  $.fn.visible = function(partial) {
    
      var $t            = $(this),
          $w            = $(window),
          viewTop       = $w.scrollTop(),
          viewBottom    = viewTop + $w.height(),
          _top          = $t.offset().top,
          _bottom       = _top + $t.height(),
          // if partial === false, visible will be true when 100% of element is shown
          compareTop    = partial === true ? _bottom : _top,
          compareBottom = partial === true ? _top : _bottom;
    
    return ((compareBottom <= viewBottom) && (compareTop >= viewTop));

  };
    
})(jQuery);


(function() {

    var scaleBackground = function(element, triggerClass) {
        
        var $element = $(element);
        
        $element.each(function(i, el) {
            
            var el = $(el);
            // check if it's shown
            if (!el.hasClass(triggerClass) && el.visible(true)) {
                el.addClass(triggerClass);
            }

        });

    };

    var lastPosition = -1;

    var loop = function() {

        // Avoid calculations if not needed
        if (lastPosition == window.pageYOffset) {
            requestAnimationFrame(loop);
            return false;
        } else {
            lastPosition = window.pageYOffset;
        }

        // do stuff you need to do
        scaleBackground(".panel__bg", "is-visible");

        requestAnimationFrame(loop);

    };

    loop();

})();
.panel__bg {
    transform: scale(0.8);
    
    transition: transform .3s ease;
    width: 200px;
    height: 200px;
    position: absolute;
    background: hotpink;
}

.panel__bg.is-visible {
    transform: scale(1);
}

.one { top: 800px; }
.two { top: 1600px; }
.three { top: 3200px; }
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>

<div class="panel__bg one">test 1</div>
<div class="panel__bg two">test 2</div>
<div class="panel__bg three">test 3</div>

references:

Teo Dragovic
  • 3,438
  • 20
  • 34