Jump to content
GreenSock

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

AirPods image sequence animation using ScrollTrigger

Recommended Posts

I'm trying to recreate Apple's Airpods Pro presentation page with ScrollTrigger.

This is what I'm trying to make: 

See the Pen ZEbGzyv by j-v-w (@j-v-w) on CodePen


My idea is to use an array which holds all the images and then make use of ScrollTrigger.update() to update the img src  based on the scrolling position.

See the Pen poybrBd by make96 (@make96) on CodePen

Link to comment
Share on other sites

Hey marius. Switching out the srcs is guaranteed to be slow because it will have to load the images every time you switch it. It's better to use a sprite or display/hide images. These threads talk more about this sort of thing:

 

  • Like 2
Link to comment
Share on other sites

 

Thank you for responding, @ZachSaucier.

Sorry for  the late response, but after a few tries I think I started to get the hang of this.

See the Pen yLOaVpE by make96 (@make96) on CodePen

As @sbest58 said in this topic, this is a process that takes a lot of trial and error.

For desktop, the sweet spot for me is 15 rows and 10 columns for a total of 147 images.

Testing this on my iPhones, it crashes, so I think I need to create a new grid.

Switching from png to webp did wonders, the hero-section image file went from 21mb to around 4mb. The Codepen link provided by me, has a .png file since imgur doesn't support .webp

I have a few questions:

  • If you check Apple's page, they are doing some sort of scaling with the images when you start scrolling: is this possible with sprites?
  • I want to split this into a few sections because if I create only one image grid with all the images, that image will take forever to load. I tried copying and pasting code for the second section, but that doesn't work. Can explain to me how can I create a new section? I tried of copying and pasting that code for every new section, with the trigger modified accordingly, but that doesn't work.
  • And the last question, how can I do what Apple does animate their text: translate - fade in - translate - fade out the text in a pinned section? I'm referring to the "Active Noise Cancellation for immersive sound.".
  • Like 1
Link to comment
Share on other sites

3 hours ago, marius96 said:

If you check Apple's page, they are doing some sort of scaling with the images when you start scrolling: is this possible with sprites?

You can have different size sprites or you could scale the image and still change the position of the sprite so yes.

 

3 hours ago, marius96 said:

I want to split this into a few sections because if I create only one image grid with all the images, that image will take forever to load. I tried copying and pasting code for the second section, but that doesn't work.

I'm guessing by "one image grid" you mean one sprite?

 

In terms of doing multiple we'd have to see what you're doing wrong. You'll need to have a different target, ScrollTriggers, variables, etc. for each section.

Link to comment
Share on other sites

For what it's worth, I did something similar to the Apple site for a client. I tried two approaches, both using individual img elements (not a sprite) positioned fixed behind the normal content.

 

The first I tried to create animations and ScrollTriggers for each section of the page and then sync those to the correct timing with the background images. While this worked (and was easier to get pinning working), it proved to be quite difficult to keep things synced on different viewports. I had a lot of conditional values that depended on breakpoints. And when I updated the height of one section, the timings of the other sections would get thrown off. Not optimal.

 

The second approach, and the one we went with in the end, is making use of one big timeline for both the background images and and animations of the content. We fixed the position of the content as well and just used the timeline to reveal, "pin" (it's a fake pin just positioning things in the same place for a bit), and hide the content. I used set percentages (hand picked) for each animation so that it stays perfectly synced with the background images. I also allowed other configuration parameters (like ease, distance of translation, etc.) to be set via data attributes and used for the animations for that element. CustomEase was a big help. For some reason it helped certain browsers to use really short tweens for the background image displaying vs .set()s. The basic setup is as follows:

const tl = gsap.timeline({
  defaults: { duration: 0.0001 }, 
  paused: true,
  scrollTrigger: { 
    // ... 
  }
});

// Create the background image animation - this needs to come first
for (let i = 0; i < frameCount; i++) {
  // Show the image briefly
  tl.to(frameImages[i], {opacity: 1}, i);

  // Hide the image after a bit
  if(i !== frameCount - 1) {
    tl.to(frameImages[i], {opacity: 0}, i + 1);
  }
}

// Get the duration of the timeline to use for our positioning
const TLDur = tl.duration();

// Create the animations for each section 
myElems.forEach((elem, i) => {
  // Set things up
  const myStartTime = elem.dataset.startpercent/100 * TLDur;
  const myDur = (elem.dataset.endpercent - elem.dataset.startpercent)/100 * TLDur;

  // Get other parameters here

  gsap.set(elem, {
    position: 'fixed',
    // Other styles set here
  });

  // Animate the position and autoAlpha separately for more fine control
  startScrollTL.fromTo(elem, {
    autoAlpha: 1
  }, {
    autoAlpha: 0,
    duration: myDur,
    // I used a modified slow ease with yoyoMode: true to go in and out in one tween for this ease
    // https://greensock.com/docs/v3/Eases/SlowMo
    ease: myAlphaEase
  }, myStartTime)
  .to(elem, {
    // I only animated y here but you can do whatever
    y: () => `-${elem.dataset.endy - elem.dataset.starty}vh`,
    duration: myDur,
    ease: myYEase
  }, myStartTime)
});

 

  • Like 4
Link to comment
Share on other sites

On 8/17/2020 at 5:14 PM, marius96 said:

My idea is to use an array which holds all the images and then make use of ScrollTrigger.update() to update the img src  based on the scrolling position.

 

Changing the image source isn't a good idea. It takes 1 line of canvas code to draw an image in canvas.

function render() {
  context.drawImage(images[airpods.frame], 0, 0); 
}

 

If the images have a transparent background, then it would be only 2 lines.

function render() {
  context.clearRect(0, 0, canvas.width, canvas.height);
  context.drawImage(images[airpods.frame], 0, 0); 
}

 

See the Pen 2152a28cffe2c2c0cca8a3e47f7b21c6 by osublake (@osublake) on CodePen

 

 

  • Like 8
Link to comment
Share on other sites

Thanks a lot for your suggestions.
I tested @OSUblake your first two examples and, ideed, they are working. Sadly, there are quite a few problems:

  • Microsoft Edge has a weird problem where if I start scrolling back up from the end to the top, it shows one random image and then suddenly starts the sprite process.
  • I tested quite a ton of grids for mobile, and the webpage can't properly load up. I'm sure this is because the file is too large or the grid is too big for mobile. 

@ZachSaucier I'm afraid I don't understand your code without seeing a CodePen.

@OSUblake Your last Pen is the best solution so far. It works across every browser, the performance of mobile is great and I don't have to play around too much with media queries to optimize this on mobile.

Sadly, I've encountered other problem.
Apple's way of fading in and fading out is pretty neat: immediately after first text element finishes fading out, the second element starts to fade in.

I managed to do that on desktop, but on mobile I can't do it.

I'm using ScrollTrigger.matchMedia to achieve the desired animation on mobile, but it doesn't work.

If I replace end: "bottom top" with end: "bottom -50%" in the all : function() { } , the effect works on mobile, but If I replace it in the "(max-width: 799px)" : function() { } , it doesn't. I think I'm doing something wrong with the media queries.

scrollTrigger: {
          trigger: target,
          markers: true,
          scrub: true,
          start: "center 50%",
          end: "bottom top",
          pin: true
        }

Here is a new Pen:

See the Pen LYNRrJY by make96 (@make96) on CodePen

Link to comment
Share on other sites

Nobody has any idea on how can I fix this for mobile devices?

Link to comment
Share on other sites

If you start stripping things out (to focus on the issue at hand) you will find that when you delete the "all" section it works just fine. Looking inside of that part, there's a competing ScrollTrigger that is never removed since it's within the "all". Did you mean to put it inside of the desktop breakpoint instead?

See the Pen yLOVJxd?editors=0010 by GreenSock (@GreenSock) on CodePen

  • Like 2
Link to comment
Share on other sites

Yes, that's what I was trying to do.

I'm sorry, I misunderstood how match media works. I thought, when the mobile viwerport would get triggered, the code in all section would not run and would get replaced by the mobile viewport code.

Thanks again!

Link to comment
Share on other sites

4 hours ago, marius96 said:

when the mobile viwerport would get triggered, the code in all section would not run and would get replaced by the mobile viewport code.

That's a testable hypothesis :) 

 

All means all ;) 

  • Like 1
Link to comment
Share on other sites

  • 2 months later...

Another option for this sort of effect is to use carefully encoded videos, potentially also using data blobs. See this demo by Shaw for more info.

  • Like 2
Link to comment
Share on other sites

  • 1 month later...

Hey @ZachSaucier,

I am using your example from above (canvas + keyframes) and I would like to know how you would enhance the code to pause the keyframe animation on specific keyframes while the text is displayed and then continue playing the animation? Apple is doing this on their AirPods Pro site right here (see screenshot for exact section): Pausing Animation on Apple Website

I would be very glad for any input on how to achieve this effect.

Bildschirmfoto 2020-12-20 um 14.28.04.jpg

Link to comment
Share on other sites

Hey @4nz and welcome to the GreenSock forums. Are you saying that you're using the frame object approach that I posted a demo of above? If so, to leave a particular frame up for longer you'd need to animate the frame object differently. Perhaps it'd make more sense to have different tweens in a timeline to animate that object. 

Link to comment
Share on other sites

Hey @ZachSaucier thanks for the welcome. Yes I am using the frame object approach and you described perfectly what I would like to achieve: leaving a particular frame up for longer (so I have time to fade in + fade out some text to describe a detail shown in that frame). Could you elaborate a little more the "different tweens in a timeline" approach? That sounds kind of doable, although I am unsure on how or where to start.

Thank you!

Link to comment
Share on other sites

In the demo look at the tween that affects the airpods object's frame value. That's what's controlling which image is being shown. Instead of using a single tween for that animation, you likely want to replace it with a timeline of different tweens to give you more control.

Link to comment
Share on other sites

  • 1 month later...
On 8/19/2020 at 6:41 PM, ZachSaucier said:

For what it's worth, I did something similar to the Apple site for a client. I tried two approaches, both using individual img elements (not a sprite) positioned fixed behind the normal content.

 

The first I tried to create animations and ScrollTriggers for each section of the page and then sync those to the correct timing with the background images. While this worked (and was easier to get pinning working), it proved to be quite difficult to keep things synced on different viewports. I had a lot of conditional values that depended on breakpoints. And when I updated the height of one section, the timings of the other sections would get thrown off. Not optimal.

 

The second approach, and the one we went with in the end, is making use of one big timeline for both the background images and and animations of the content. We fixed the position of the content as well and just used the timeline to reveal, "pin" (it's a fake pin just positioning things in the same place for a bit), and hide the content. I used set percentages (hand picked) for each animation so that it stays perfectly synced with the background images. I also allowed other configuration parameters (like ease, distance of translation, etc.) to be set via data attributes and used for the animations for that element. CustomEase was a big help. For some reason it helped certain browsers to use really short tweens for the background image displaying vs .set()s. The basic setup is as follows:


const tl = gsap.timeline({
  defaults: { duration: 0.0001 }, 
  paused: true,
  scrollTrigger: { 
    // ... 
  }
});

// Create the background image animation - this needs to come first
for (let i = 0; i < frameCount; i++) {
  // Show the image briefly
  tl.to(frameImages[i], {opacity: 1}, i);

  // Hide the image after a bit
  if(i !== frameCount - 1) {
    tl.to(frameImages[i], {opacity: 0}, i + 1);
  }
}

// Get the duration of the timeline to use for our positioning
const TLDur = tl.duration();

// Create the animations for each section 
myElems.forEach((elem, i) => {
  // Set things up
  const myStartTime = elem.dataset.startpercent/100 * TLDur;
  const myDur = (elem.dataset.endpercent - elem.dataset.startpercent)/100 * TLDur;

  // Get other parameters here

  gsap.set(elem, {
    position: 'fixed',
    // Other styles set here
  });

  // Animate the position and autoAlpha separately for more fine control
  startScrollTL.fromTo(elem, {
    autoAlpha: 1
  }, {
    autoAlpha: 0,
    duration: myDur,
    // I used a modified slow ease with yoyoMode: true to go in and out in one tween for this ease
    // https://greensock.com/docs/v3/Eases/SlowMo
    ease: myAlphaEase
  }, myStartTime)
  .to(elem, {
    // I only animated y here but you can do whatever
    y: () => `-${elem.dataset.endy - elem.dataset.starty}vh`,
    duration: myDur,
    ease: myYEase
  }, myStartTime)
});

 

Is there any demo related to this code

Link to comment
Share on other sites

3 hours ago, nikhiltyagicse said:

Is there any demo related to this code

Sorry, no. The psuedo-code that I provided should help you get started.

Link to comment
Share on other sites

See the Pen NWbXwPE?editors=1001 by nikhiltyagicse (@nikhiltyagicse) on CodePen

1)

In Your Code

// Create the background image animation - this needs to come first
for (let i = 0; i < frameCount; i++) {
  // Show the image briefly
  tl.to(frameImages[i], {opacity: 1}, i);

  // Hide the image after a bit
  if(i !== frameCount - 1) {
    tl.to(frameImages[i], {opacity: 0}, i + 1);
  }
}

I want to use canvas to display hide images, how can i do that,

 

2) startpercent, endpercent

Small Description, how to declare these value correctly;

Example: i have a animation of duration :75,  If i want to start first animation at 0.3 sec and end at 0.5 sec.

What is the best way to calculate. 

 

3) Declare startScrollTL

I need help in declaring startScrollTL

 

What am i doing wrong?

 

Pls help, i am trying this from many days, aligning text and bg images, Any help/ guidance would be appreciated

Link to comment
Share on other sites

9 hours ago, Nikhil Tyagi said:

I want to use canvas to display hide images, how can i do that,

I'd probably keep an object that represents what image to show. Then animate that object with the timeline (making sure to snap to the nearest whole number). Then inside of the onUpdate of the animation, if the image number changed, I would have the logic for drawing the new image over the old one.

 

9 hours ago, Nikhil Tyagi said:

2) startpercent, endpercent

Small Description, how to declare these value correctly;

Example: i have a animation of duration :75,  If i want to start first animation at 0.3 sec and end at 0.5 sec.

What is the best way to calculate. 

Sorry, I don't really understand your question. I think it would help you if you read about how duration works with a scrub, covered in the ScrollTrigger docs.

Link to comment
Share on other sites

  • 1 month later...

Any chance someone can show how you could play a part of the image sequence on page load? So for example, in the Airpod example, once the page loaded it would autoplay the first 10-15 frames?

See the Pen ZEbGzyv by j-v-w (@j-v-w) on CodePen

Link to comment
Share on other sites

18 hours ago, errrrs said:

Any chance someone can show how you could play a part of the image sequence on page load? So for example, in the Airpod example, once the page loaded it would autoplay the first 10-15 frames?

You're saying that the first 10-15 frames would play after they load, and then the scroll-linked animation would only show the portion of the image sequence AFTER those frames? In other words, the only time users would see the first 10-15 frames would be the auto-play onload and they'd never be able to scroll up to see those again? You can't really have it both ways unless you literally force the page to scroll initially to go through those frames. See what I mean? 

 

It's all very doable, but unfortunately we don't have the resources to provide free consulting services to build effects like this but we'd be happy to answer any GSAP-specific questions about the API or ScrollTrigger functionality, etc. 

  • Like 1
Link to comment
Share on other sites

Hey thanks for getting back to me, I actually figured out a way to do it by tweening the first 30 frames on load in one render, and then having the scroll-linked animation  take over after that starting at 30th frame in another render.  

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.

×