janpaepke / ScrollMagic

The javascript library for magical scroll interactions.
http://ScrollMagic.io
Other
14.88k stars 2.17k forks source link

Some insights on the flickering, jumping, jittering pinned elements bug (+ workaround) #660

Open DarioSoller opened 7 years ago

DarioSoller commented 7 years ago

First of all, I want to mention that I already used ScrollMagic in several onepagers, and want to say a big thank you for your effort & work, @janpaepke !

Nevertheless me and a few others (#288, #330, #316, #355, #424, #445, #497, #488, #504, #531, #538, #542, #553, #575, #595, #601) stumbled over some unexpected behavior with pinned elements. Some of the issues may be solved, or may have another cause, but I think it's good to bundle all related issues to get an overview of this bug. The example codepen by @DraganLibraFire ( #595 ) was what I experienced as well. So in the following, I just want to share, what I have found out and what I came up with to solve it in my case:

The Bug

The main cause was that the top value of the pinned element was jumping between two different values. Even though one was not scrolling anymore. Responsible event is triggered in line 1928:

.on("progress.internal", function (e) { updatePinState(); })

and within the updatePinState() function the topvalue for the pinned element is calculated. There the _progress variable switches between two different values. I hadn't had the time to further track down, where the _progress variable is updated or what causes the progress.internal event to be fired, without scrolling.

My Workaround

Somehow the described bug only happens for elements that are not at the top of the page. So somehow the scrollTop/offset top values seem to be corrupt (especially when using SVG elements as trigger elements). So what I did, was not using any triggerElement at all, but instead only using the durationand offset scene options and calculating the offset on my own:

  var SM = {
    sceneHeights: [], // has to be initialized with the scene heights
    scenePinLengths: [], // has to be initilized with "0"s
    mainPinEl: "#container",
    smController: new ScrollMagic.controller()
  }

  function _sceneOffset(sceneIndex){
    var total = 0;
    var sceneHeightsForOffset = SM.sceneHeights.slice(0, sceneIndex);
    var scenePinsForOffset = SM.scenePinLengths.slice(0, sceneIndex);

    for (var i = 0; i < sceneHeightsForOffset.length; i++) {
      total += sceneHeightsForOffset[i] + scenePinsForOffset[i];
    }

    return total;
  }

  var pinLength = 600;
  var sceneIndex = 0; // sceneIndex starting at 0
  var newSMScene = new ScrollMagic.Scene({
        duration: pinLength,
        offset: _sceneOffset(sceneIndex)
      }).setPin(SM.mainPinEl, {pushFollowers: false})
        .setTween(new TimelineMax())
        .addTo(SM.smController); // assign the scene to the controller

  SM.scenePinLengths[sceneIndex] = pinLength; // updating the scene pinning length

This solved the jitter while pinning elements bug for me. Hope it helps someone else implementing a quick fix and further I hope it helps @janpaepke gaining more ideas how to maybe tackle this.

nitrokevin commented 6 years ago

Hi I'm having this issue, are you able to explain how to implement your work around? I'm just getting errors,

I'm not sure what you are meant to do with this part

sceneHeights: [], // has to be initialized with the scene heights scenePinLengths: [], // has to be initilized with "0"s

DarioSoller commented 6 years ago

@nitrokevin you just have to init the sceneHeights and scenePinLengths variables ie. on the jQuery ready event like this:

$(document).ready(function ($) {
    var scenes = $('.scene'); // expecting scene wrapper elements with the class 'scene'

    for (var i = 0; i < scenes.length; i++) {
        SM.sceneHeights.push($(scenes[i]).height()); // init scene heights
        SM.scenePinLengths.push(0); // init scene pin duration array
    }
});

Didn't had the time right now to set up a proper codepen, but I hope this still helps you with your errors.

nitrokevin commented 6 years ago

great thanks, I'll take a look.

I was getting the issue where the top value of the spacer was flicking between two values which caused the wobble in safari only ... just spent hours going through it and noticed that I had used a negative margin-top value on the image I was pinning, as soon as I got rid of that the wobble went away

scermat commented 6 years ago

I was having the exact same issue, and happy to report that your workaround to remove the triggerElement and work with the duration and offset did the trick.

Thank you for reporting and sharing the insight!

TylerOlthuizen commented 6 years ago

I implemented your code and it doesn't seem to work , what am I doing wrong? Here's my code , let me know please :)

screen shot 2018-01-03 at 4 17 09 pm

screen shot 2018-01-03 at 4 20 28 pm

DarioSoller commented 6 years ago

@TylerOlthuizen: I have prepared a quick codepen that demos the workaround. I admit that it maybe was not very clear, that you have to repeat the code around the new ScrollMagic.Scene(...) for every scene of yours. Seems bulky with a lot of code duplication, but keep in mind that you probably end up with individual TweenMax animations for every scene.

TylerOlthuizen commented 6 years ago

@DarioSoller Thanks for the code pen! Yeah I was hoping I could just loop through all of my image I want to pin and use data attributes to get the x and y values that need to be set. Have you tried making it dynamic and class based with data attributes? I see now that the for loop in the document ready just sets the height of the mainPinEl ? Thanks for your help!

DarioSoller commented 6 years ago

@TylerOlthuizen : You can easily do something like this, expecting to have i.e. a data-pin-length="400" data attribute on your scene element.

for (var i = 0; i < scenes.length; i++) {
  initPinScene(i, $(scenes[i].data('pin-length')));
}

// Scene
function initPinScene(sceneIndex, pinLength) {
  console.log("Scene Offset for sceneIndex "+ sceneIndex +" is: "+ _sceneOffset(sceneIndex));
  var newSMScene = new ScrollMagic.Scene({
    triggerHook: 0,
    duration: pinLength,
    offset: _sceneOffset(sceneIndex)
  }).setPin(SM.mainPinEl, {pushFollowers: false})
      .addIndicators({name: "pin scene "+sceneIndex})
      .addTo(SM.smController); // assign the scene to the controller

  SM.scenePinLengths[sceneIndex] = pinLength; // updating the scene pinning length
  return newSMScene;
}

Hope that helps!

TheFlashYoda commented 6 years ago

I fixed this issue for our project in a much simpler way. I added _tfyFirstTop variable to Scene and instantiated at -1. Then I put this code in the updatePinState function after fixedPos is calculated in the 'pinned state' section:

fixedPos[containerInfo.vertical ? "top" : "left"] += scrollDistance;

if (_tfyFirstTop === -1) { _tfyFirstTop = fixedPos.top; }

if (fixedPos.top !== _tfyFirstTop) { fixedPos.top = _tfyFirstTop; }

_// set new values _util.css(pinOptions.spacer.firstChild, {

The idea is to only set that value once per scene and keep it locked down. Worked great with minimal code. Hope this helps! And thanks @DarioSoller for your research into what the issue was and where it was happening at. I was able to easily trace the code from there!

DarioSoller commented 6 years ago

Great news, I hoped there is such a simple fix. Thanks for sharing!

Kiefna commented 6 years ago

@TheFlashYoda 's solution doesn't work for me, the same corrupt values that cause the jitter in the first place can cause the first value for the scene to be set incorrectly. For the pages I'm building, it seems about 1 in 5 scenes.

sebastianjung commented 6 years ago

I was about to rip my hairs off, when i realized the jumping of the pinned section on IE was triggered by the addIndicators Plugin of Scrollmagic.

Disabling those might work.

Hope this helps!

zurnet commented 6 years ago

I've tried disabling the plugin, same issue. If anyone has any real concrete solutions or perhaps a fix to the library?

DarioSoller commented 6 years ago

@sebbler1337: I am aware that the addIndicator plugin does cause some extra jitter, but in a lot of pinning cases disabling the plugin, alone will not solve the jitter completly.

@zurnet: Have a look in the comments above. There some workarounds, you could try.

zurnet commented 6 years ago

@TheFlashYoda are you able to expand more on this part

I added _tfyFirstTop variable to Scene

are you simply putting a variable inside a function that contains the new scene?

TheFlashYoda commented 6 years ago

@zurnet in my case, the function updatePinState was causing the jitter by jumping between two values for some reason. My variable captures the very first value that is set for the top and from there on, holds the top to that value, essentially negating what updatePinState does. It's been awhile since I've looked at the code but I'm not sure why top would need to be changed once it is initially set for a horizontally scrolling situation - that should always be the same value, shouldn't it? Why this didn't work for @Kiefna is because the value it first grabbed was wrong - that didn't happen for me and I suppose in that case if I couldn't find a solution I would have added code to hardwire the values that are needed? I am by no means an expert in this module - just shared what worked for me in hopes that it would help others.

888jacklee commented 5 years ago

@DarioSoller this does help, keep up the good work.

grayayer commented 5 years ago

@DarioSoller - In order to test your codepen workaround on iPad in full screen view I had to fork it, because it says "The owner of this Pen needs to verify their email address to enable Full Page View.". That forked version is at https://codepen.io/grayayer/pen/PVeEbP.

It's a shame that one of the fundamental actions of ScrollMagic, pinning an object just doesn't out-of-the-box seem to work on touch devices.

As far as I can tell this jitter is caused by an improper top position value being set and then reset. You can address this with a single css rule that overides the inline scrolMajic Value.

.scrollmagic-pin-spacer{
   top:0 !important
}
DarioSoller commented 5 years ago

@grayayer sry I was not aware that I haven't verified my email yet. So I have also done that now. Good that the CSS top: 0 !important rule works for you. Much simpler than my approach ;)

jeroenbraspenning commented 5 years ago

I made an alternative solution, might be suitable for someone.

var FixScrollMagicJumpingPin = (scene, pinnedElement) => {
  let pinned = false
  let maxPos = 0

  // overwrite the top on every update
  // not ideal but does the job.
  scene.on('update', function(event) {
    if (!pinned) return

    let updatedTop = Math.max(maxPos, parseInt(pinnedElement.style.top))
    setTimeout(() => {
      if (!pinned) return
      pinnedElement.style.top = updatedTop + 'px'
    }, 0)
    maxPos = updatedTop
  })

  scene.on('enter', function(event) {
    pinned = true
  })

  scene.on('leave', function(event) {
    pinned = false
  })
}

export default FixScrollMagicJumpingPin
tmcepa commented 4 years ago

I made an alternative solution, might be suitable for someone.

var FixScrollMagicJumpingPin = (scene, pinnedElement) => {
  let pinned = false
  let maxPos = 0

  // overwrite the top on every update
  // not ideal but does the job.
  scene.on('update', function(event) {
    if (!pinned) return

    let updatedTop = Math.max(maxPos, parseInt(pinnedElement.style.top))
    setTimeout(() => {
      if (!pinned) return
      pinnedElement.style.top = updatedTop + 'px'
    }, 0)
    maxPos = updatedTop
  })

  scene.on('enter', function(event) {
    pinned = true
  })

  scene.on('leave', function(event) {
    pinned = false
  })
}

export default FixScrollMagicJumpingPin

how i implement this?

whartonweb commented 4 years ago

Great solution, @DarioSoller

RaviGill247 commented 3 years ago

Is there any way to implement this workaround in the "react-scrollmagic" version?

DarioSoller commented 3 years ago

@RaviGill247 I don't know the react-scrollmagic implementation, but I have done some GSAP stuff in React and found it to be playing really nicely together. GSAP recently also released a Scroll Trigger Plugin, which might be a fresh alternative for the unfortunately poorly maintained ScrollMagic lib.