Jump to content
Search Community

Performance optimization with draggable paralax effect

teejay_hh test
Moderator Tag

Recommended Posts

Hello I have a question performance. I wrote small horizontal slider which also moves 2 background images.  The following code shows my slider

implementation. I wrote this for my team so there are few more comments which dont assume gsap knowledge. 

 

define(function (require) {
  const gsap = require('gsap/gsap.min').gsap
  const Draggable = require('gsap/Draggable.min').Draggable
  const InertiaPlugin = require('gsap/InertiaPlugin.min').InertiaPlugin
  gsap.registerPlugin(Draggable, InertiaPlugin)

  /**
   * The slider class creates a draggable slider which uses gsap's draggable plugin
   * The slider requires a certain dom structure to function and therefore is not reusable
   * The class requires `gsap` as wells as the 2 plugins `Draggable, InertiaPlugin` to be registered
   *
   * The draggable will start an auto rotation by default, which can be interrupted via interacting with it.
   *
   * @class Slider
   *
   *
   *
   */
  return class Slider {
    constructor(delay = 8, endCallback = () => {}) {
      this.slideDelay = delay
      this.slideDuration = 1
      this.slider = document.getElementById('slider')
      this.slides = document.querySelectorAll('.slide')
      this.slidesInner = document.querySelectorAll('.slides-inner')
      this.grass = document.getElementById('bg-three')
      this.hills = document.getElementById('bg-two')
      this.clouds = document.getElementById('bg-one')
      this.numSlides = this.slides.length
      this.snapX = 0
      this.animationCounter = 1
      this.slideWidth = this.slides[0].offsetWidth
      this.endCallback = endCallback
      this.hillsXSetter = gsap.quickSetter(this.hills, 'css')
      this.cloudsXSetter = gsap.quickSetter(this.clouds, 'css')

      this.createResizeListener()
      this.createDraggable()
      this.initiateSliderAnimation()
    }

    /**
     * Creates the draggable component with a few defaults. Please check out the
     * documentation if you would like to know more about what this plugin can do
     *
     * @see https://greensock.com/docs/v3/Plugins/Draggable
     */
    createDraggable() {
      this.draggable = new Draggable(this.slidesInner, {
        type: 'x', // only horizontal scrolling
        bounds: this.slider, // bounds the draggable to the container so it cant be dragged out of it
        inertia: true, // momentum - throwing behaviour (magic)
        edgeResistance: 0.8,
        dragResistance: 0.3,
        throwProps: true,
        onDragStart: this.killAnimation, // callback which gets executed on every tick of dragging
        onDrag: this.onDrag, // callback which gets executed on every tick of dragging
        onThrowUpdate: this.onDrag, // callback which gets executed when the draggable is thrown
        snap: this.snapX, // is set to a function which calculates a snapping position
        callbackScope: this, // sets the callback scope to be the class instead of the draggable instance
        // implement some cool live snapping
      })
    }

    /**
     * We start a delayed call which initiated the animation cycle to run through all slides until the end
     * This will set a timer variable which can be used to either kill the call or to restart it.
     */
    initiateSliderAnimation() {
      this.timer = gsap.delayedCall(this.slideDelay, this.autoPlay, null, this)
    }

    /**
     * Stop the animation loop
     */
    killAnimation() {
      if (this.timer) this.timer.kill()
    }

    /**
     * The method get called by the delayed call `initiateSliderAnimation` which starts the slide animation
     * When the draggable is pressed, dragged or thrown at the time of execution then we will kill the next call.
     * We also stop at the end of the last slide which executes a callback which is used to redirect to a different page.
     */
    autoPlay() {
      if (this.animationCounter >= this.numSlides) {
        this.killAnimation()
        this.endCallback()
        return
      }

      if (this.draggable.isPressed || this.draggable.isDragging || this.draggable.isThrowing) {
        this.killAnimation()
      } else {
        this.animateSlides()
      }
    }

    /**
     * The animateSlides method runs an animation which slides the draggable one screen further
     * If the function is called multiple times it will progress through all slides sequentially
     * We use the timer variable to create a loop which calls the delayed call from `initiateSliderAnimation`  method
     * again.
     */
    animateSlides() {
      const _this = this // callback functions require us to use a scope var

      _this.animationCounter++
      _this.timer.restart(true) // create a loop

      // slides the draggable one screen further
      gsap.to(_this.slidesInner, {
        x: '-=' + _this.slideWidth,
        duration: _this.slideDuration,
        ease: 'quad.inOut',
        onUpdate: function (s) {
          const drag = _this.draggable.update()
          _this.onDrag(null, drag.x)
        },
      })
    }

    /**
     * This method is called when the we animate or drag the draggable. Its used to create the paralax effect
     * on the different backgrounds.
     *
     * @param pointer
     * @param s
     */
    onDrag(pointer = null, s = null) {
      if (s === null) s = this.draggable.x
      // quicksetter (defined in the constructor) have a massive performance boost
      // to the normal set method at least 50% - https://greensock.com/docs/v3/GSAP/gsap.quickSetter()
      this.hillsXSetter({ x: s / 5 })
      this.cloudsXSetter({ x: s / 8 })
    }

    /**
     * The resizeListener listens to the window resize event and sets the Draggable width programmatically
     */
    createResizeListener() {
      window.addEventListener('resize', this.setDraggableWidth.bind(this))
      this.setDraggableWidth()
    }

    /**
     * We use the window width set the slide width to stretch the whole screen
     * The slides are setup as flex colums of 1, which means they will adjust to the size of its parent container.
     * To make these slides fullscreen we need to set the container the size of x times the screen width.
     * We also adjust the snaping based on the current screen width.
     */
    setDraggableWidth() {
      this.slideWidth = document.documentElement.clientWidth || document.body.clientWidth
      gsap.set(this.slidesInner, { width: this.numSlides * this.slideWidth + 'px' })
      gsap.set(this.slides, { width: 100 / this.numSlides + '%' })

      // make sure we show the inner container after the slides are repositioned.
      gsap.to(this.slidesInner, { delay: 0.5, duration: 0.5, autoAlpha: 1 })

      this.snapX = function (endValue) {
        const slideWidth = document.body.offsetWidth
        return Math.round(endValue / slideWidth) * slideWidth
      }

      if (this.draggable) this.draggable.update(true)
    }
  }
})

Its fairly straight forward and pretty pretty fast on my mbpro but thats not the measure.  (I am not sure I can create a pen with the business plugins and I would have to change all assets (I guess placeholder would work))

 

Unfortunately this is not running smooth (like 60fps smooth) on an ipad of the 7th gen which I consider pretty fast (A10 Fusion chip). 

When I take out the dragging x setter (no paralax effect) everything is really nice, but moving the images is the problem.  What steps can I take to optimize it? 

 

Perhaps:

- dont resize images, keep them in their original size (no with 100% stretch)?

- try different way of setting the x pos on those images ?

- reduce size of images (remove transparent portion)

- I have implemented the paralax effect wrong ?

 

Please let me know if you have any tips.

 

Cheers Thomas

 

Link to comment
Share on other sites

I replaced the - quickSetter with normal to tweens -> 

gsap.to(this.hills, {duration: 0.1, x: s/5 })
gsap.to(this.clouds, {duration: 0.1, x: s/8 })

which seems to make a big difference. Not perfect but closer to what I want.

Link to comment
Share on other sites

Hard to say what if any performance problems there might be without seeing a demo.

 

The quickSetter just set values, so maybe you are mistaking that for performance problems. 🤷‍♂️ If you need a smooth transition, then you can interpolate changes like in this demo.

 

 

See the Pen WNNNBpo by GreenSock (@GreenSock) on CodePen

 

And the most performant way to use quickSetter is to specify the property.

this.hillsXSetter = gsap.quickSetter(this.hills, 'x', 'px')

this.hillsXSetter(s / 5)

 

Also, setting will-change: transform in your css for the images can improve performance.

 

  • Like 4
Link to comment
Share on other sites

Create an account or sign in to comment

You need to be a member in order to leave a comment

Create an account

Sign up for a new account in our community. It's easy!

Register a new account

Sign in

Already have an account? Sign in here.

Sign In Now
  • Recently Browsing   0 members

    • No registered users viewing this page.
×
×
  • Create New...