Jump to content
GreenSock

Search In
  • More options...
Find results that contain...
Find results in...
creativeocean

Parallax math help

Go to solution Solved by GreenSock,

Recommended Posts

I hope it's okay to ask for help here, even though my problem has nothing to do with an actual GSAP issue.

 

I'm working on this horizontal slider, and right now the parallax image movement (line 89) is just based on the draggable element's total progress. That works fine when the carousel has just a few images, but when there are many images, the amount of movement that is visible as each box crosses the stage becomes too small. And lots of the image is never actually seen. 

 

At first I thought this would be pretty simple, but I've been fruitlessly struggling to figure out an equation/setup that will make any number of slides have the same amount of parallax motion, and still be responsive to the full browser width.

See the Pen 44639788dd72f4e26b8c8b52cb096480?editors=0110 by creativeocean (@creativeocean) on CodePen

  • Like 1
Link to comment
Share on other sites

  • Solution

Well, CodePen has been down for a while so I can't provide a forked demo but here's how I'd do the JS: 

 

let n = 15;
let parallax = []; // we'll store the animations in here.
let clamp = gsap.utils.clamp(0, 1);
let currentX = 0;
let snap = gsap.utils.pipe(gsap.utils.snap(450), gsap.utils.clamp(n * -450, 0));

// Set #slides width for draggable bounds
gsap.set('#slides', {width:n*450});

// Populate slide boxes
for (var i=0; i<n; i++){

  var box = document.createElement('div'),
      img = new Image(),
      link = document.createElement('a');

  gsap.set(box, {
    width:400,
    height:600,
    overflow:'hidden',
    position:'absolute',
    top:50,
    left:i*450,
    attr:{ class:'box b'+i },
    background:'#333'
  });

  gsap.set(img, {
    position:'absolute',
    left:-300,//-i*50,
    attr:{src:'https://picsum.photos/id/'+(i+10)+'/700/600/'}
  });

  parallax[i] = gsap.to(img, {x: 300, ease: "none", paused: true});

  gsap.set(link, {
    position:'absolute',
    textAlign:'center',
    width:105,
    height:70,
    paddingTop:'7px',
    top:490,
    left:-25,
    rotation:90,
    fontSize:'45px',
    color:'#000',
    background:'#fff',
    mixBlendMode:'lighten',
    textDecoration:'none',
    innerHTML:'<span style="font-size:20px">IMG </span>'+(i+1),
    attr:{
      class:'imgLink',
      href:'https://picsum.photos/id/'+(i+10)+'/700/600/',
      target:'_blank'
    },
  });

  box.appendChild(img);
  box.appendChild(link);
  slides.appendChild(box);
}

// Make #slides draggable
Draggable.create('#slides', {
  type:'x',
  bounds: {left: innerWidth/2, width:1},
  zIndexBoost: false,
  onDrag:updateParallax,
  inertia: true,
  onRelease: function() { currentX = this.endX },
  onThrowUpdate: updateParallax,
  snap: snap
})

function updateParallax() {
  // parallax should start from the right edge of the screen and we know that the #slides starts with its left edge centered, thus we add innerWidth/2
  let x = gsap.getProperty('#slides', 'x') + window.innerWidth / 2,
      // convert the position in the viewport (right edge of viewport to -400 because that's when the right edge of the element would go off-screen to the left) into a progress value where it's 0 at the right edge and 1 when it reaches the left edge
      normalize = gsap.utils.mapRange(window.innerWidth, -400, 0, 1);
  // apply the clamped value to each animation
  parallax.forEach((animation, i) => animation.progress(clamp(normalize(x + i * 450))));
}

updateParallax();

// Update draggable bounds onResize
window.addEventListener('resize', ()=>{
  Draggable.get("#slides").applyBounds({left: innerWidth/2, width:1})
});

// Previous & next buttons
$('#prev, #next').on('click', function(e) {

  let nextX = snap(currentX + (e.currentTarget.id === "next" ? -450 : 450));
  if (nextX !== currentX) {
    gsap.to("#slides", {x: nextX, duration: 0.3, onUpdate: updateParallax, overwrite: true})
    currentX = nextX;
  }

});

$('#prev, #next').on('mouseenter', (e)=>{
  gsap.to('#'+e.currentTarget.id + ' circle', {attr:{r:22}, ease:'expo', overwrite: true})
});

$('#prev, #next').on('mouseleave', (e)=>{
  gsap.to('#'+e.currentTarget.id + ' circle', {attr:{r:20}, ease:'expo', overwrite: true})
});

// Img Link rollover/out behavior
$('.imgLink').on('mouseenter', (e)=>{
  gsap.to(e.currentTarget, {x:10, duration:0.3, ease:'power3', overwrite: true})
});

$('.imgLink').on('mouseleave', (e)=>{
  gsap.to(e.currentTarget, {x:0, duration:0.3, ease:'power4.inOut', overwrite: true})
});

 

The general idea is: 

  • You only want the parallax effect to exist while each individual element is inside the viewport (not the entire movement of the #slides).
  • I created a simple linear animation of x from 0 to 300 for EACH element. Paused. Dumped them into an Array.
  • The updateParallax() function loops through each one and sets the progress() according to its position (which we know because they're 450px apart). It's all based on the viewport so that progress would be 0 when it's on the far right edge of the screen and 1 when the element's right edge reaches the left edge of the viewport.

I also made the following improvements: 

  • I applied inertia with snapping directly on the draggable so it's super smooth and users can flick it.
  • The logic in the next/previous buttons allows users to click the buttons quickly and it still works (instead of ignoring clicks while animation is running). Sorry, that's my pet peeve when the interface ignores user clicks. :)

I hope that helps, Tom! 

  • Like 6
Link to comment
Share on other sites

Wow! Jack, I didn't expect this much support and improvement. There's a lot to review/digest/learn here, but I'm very grateful for the help and guidance. Codepen is now back online and I've forked the previous pen (and made it public), now using your JS: 

See the Pen OJgNyVm?editors=0010 by creativeocean (@creativeocean) on CodePen

 

Turned out pretty slick, if I do say so myself! Go team!

  • Like 5
Link to comment
Share on other sites

Happy to help, @creativeocean. It's the least I could do for all the cool stuff you did for our home page (and for those who don't know, Tom is the mind behind the animations at the top of the home page). 🙌

  • Like 2
Link to comment
Share on other sites

Really cool Tom and great assist by Jack. 👍

 

4 hours ago, GreenSock said:

The logic in the next/previous buttons allows users to click the buttons quickly and it still works (instead of ignoring clicks while animation is running). Sorry, that's my pet peeve when the interface ignores user clicks.

I had to laugh at this. I made a slider once that ignored clicks while the animation was running and Jack gave me an assist on part of it and said the same thing. "I want the buttons to work no matter what." Busted. 🤣

  • Haha 3
Link to comment
Share on other sites

Ha. Sorry, maybe it's just a personal pet peeve of mind that nobody else really cares about. If I click 5 times quickly, I want it to advance 5 chunks, not 1. :) 

 

Nobody has ever accused me of being picky, stubborn, or opinionated...ever🤥

  • Haha 5
Link to comment
Share on other sites

I was actually glad you did that.  Most of my slider type work since then listens for clicks even if there's action on the screen. :)

 

  • Like 3
Link to comment
Share on other sites

This is a wonderful thread and a great result. Nice job gang.

  • Like 2
Link to comment
Share on other sites

  • 4 weeks later...

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.
×