jonkwheeler / ScrollScene

ScrollScene is an extra layer on top of ScrollMagic as well as using IntersectionObserver to achieve similar effects.
https://scrollscene.jonkwheeler.now.sh/
MIT License
136 stars 9 forks source link

ScrollMagic loop - scene duration the same as the video duration that's part of the scene #45

Closed kiwwwipl closed 3 years ago

kiwwwipl commented 3 years ago

I have a loop of scenes with videos that play with the scroll (loop because it's integrated with WordPress ACF Flexible fields).

What I'm trying to do is to make the duration of the whole scene same as duration of the video of the scene (on the front-end there'll be a presentation with different videos). I tried simply video.duration, but it behaves in a weird way: sometimes it works, sometimes not (NaN). Is there a solution for this? Here is my code:

const startVideoFullScreenOnScrollFunction = () => {

const startVideoFullScreenOnScroll = document.querySelectorAll('.video-fullscreen-text-on-scroll');

for (let i = 0; i < startVideoFullScreenOnScroll.length; i += 1) {
  if (typeof (startVideoFullScreenOnScroll[i]) !== 'undefined' && startVideoFullScreenOnScroll[i] != null) {

    const controllerVideoFullScreenOnScroll = new ScrollMagic.Controller();

    const videoFullScreen = startVideoFullScreenOnScroll[i].querySelector('video');
    const textsOnVideo = startVideoFullScreenOnScroll[i].querySelectorAll('.video-fullscreen-text-on-scroll__copy');

    const timelineVideoFullScreenOnScroll = new TimelineMax();

    // fade in & fade out text
    for (let j = 0; j < textsOnVideo.length; j += 1) {
      timelineVideoFullScreenOnScroll
        .to(textsOnVideo[j], {
          opacity: 1,
          duration: 3,
        })
        .to(textsOnVideo[j], {
          opacity: 0,
          duration: 3,
        });
    }

    // ScrollMagic scene
    let sceneVideoFullScreenOnScroll = new ScrollMagic.Scene({
        duration: ???,
        triggerElement: startVideoFullScreenOnScroll[i],
        triggerHook: 0
      })
      .setTween(timelineVideoFullScreenOnScroll)
      .setPin(startVideoFullScreenOnScroll[i])
      .addTo(controllerVideoFullScreenOnScroll);

    let scrollpos = 0;
    let startpos = 0;

    sceneVideoFullScreenOnScroll.on('update', e => {

      startpos = e.startPos / 1000;
      scrollpos = e.scrollPos / 1000;

      if (scrollpos >= startpos) {
        videoFullScreen.currentTime = scrollpos - startpos;
      };
    });
  }
}
jonkwheeler commented 3 years ago

I'm gonna go out on a hutch here... this sounds like a race condition.

It could be the video is not loaded yet, but you're asking for the duration, thus it's like NaN because it doesn't know what to tell you.

I'd try first testing to see if all the video.duration work listening for an event.

https://developer.mozilla.org/en-US/docs/Web/API/HTMLMediaElement/canplay_event

const video = document.querySelector('video');

video.addEventListener('canplay', (event) => {
  console.log(video.duration);
});

If you can prove the durations are all valid, then you need to setup a listener to wait for the video to be ready, before doing anything with the video.duration.

https://developer.mozilla.org/en-US/docs/Web/API/HTMLMediaElement/duration

kiwwwipl commented 3 years ago

Wow, thank you for answering so quickly - appreciate it!

The problem is that the video.duration doesn't work only when put inside a ScrollMagic scene as a parameter. Outside the scene it returns correct value even without listener.

jonkwheeler commented 3 years ago

What's your actual code, since your example says duration: ????

FYI, I didn't write ScrollMagic, but I'm happy to help.

kiwwwipl commented 3 years ago

Sure, appreciate your help.

Well, first I triedvideoFullScreen.duration and then I just put some fixed value just to make the scene work in some part at least.

jonkwheeler commented 3 years ago

When you console.log(videoFullScreen.duration) it doesn't show the duration after it's loaded? That's weird.

A work around is to add a data attribute to the video tag output from the cms. Like <video data-duration="25" /> or something like that. You could then parse the attribute.

jonkwheeler commented 3 years ago

By the way, I know you're trying to scrub the video timeline, and I'll just put out there that I don't recommend this. It comes out quite choppy trying to constantly set the video current time. I'd recommend looking into doing this with a canvas animation animation.

kiwwwipl commented 3 years ago

Ok, I'll try the data attribute workaround - thanks!

About the video timeline - you're right, I'm not really happy with the end result. Any chance you could give me a bit more specific tip/link to some resources?

jonkwheeler commented 3 years ago

You're trying to do the Apple video tricks? Last I saw they use a sequence of images. See this url https://www.apple.com/105/media/us/airpods-pro/2019/1299e2f5_9206_4470_b28e_08307a42f19b/anim/sequence/large/10-fall-into-case/0000.jpg. Try changing the 0000.jpg to other numbers, like 0010.jpg. It's not a video they are scrubbing, it's an image sequence.

They then probably preload the images, or a certain number of them, and then on scroll change, update the image written to the canvas.

I actually made this as a feature of ScrollScene here but removed it as it was far too beta and buggy to be released. I dug up my old code I was working with. Maybe it'll help you.


  const canvas = {
    element: null,
    extension: 'jpg',
    frames: null,
    height: 820,
    width: 1458,
    source: null,
    ...options,
  }

  canvas.element.width = canvas.width
  canvas.element.height = canvas.height

  const ctx = canvas.element.getContext('2d')
  ctx.imageSmoothingEnabled = true
  ctx.imageSmoothingQuality = 'high'
  let prevProgress = 0

  const getSeqNum = progress =>
    padNumber(Math.min(canvas.frames, Math.round(lerp(prevProgress, progress, 0.1) * 100)), 4)

  const images = new Array()
  for (let index = 0; index < canvas.frames; index++) {
    images.push(canvas.source + padNumber(index + 1, 4) + '.' + canvas.extension)
  }

  const preloadedImages = new Array()
  const preload = imageArr => {
    for (let index = 0; index < imageArr.length; index++) {
      preloadedImages[index] = new Image()
      preloadedImages[index].src = imageArr[index]
    }
  }
  preload(images)

  const updateCanvas = progress => {
    /* Get the image */
    const img = new Image()

    img.src = `https://www.apple.com/105/media/us/airpods-pro/2019/1299e2f5_9206_4470_b28e_08307a42f19b/anim/sequence/large/02-head-bob-turn/${getSeqNum(
      (progress * 100) / canvas.frames,
    )}.jpg`

    prevProgress = progress

    /* Wait til the image is loaded */
    img.onload = function() {
      ctx.drawImage(img, 0, 0) /* Draw the image to the canvas */
    }
  }

  Scene.on('start', function(event) {
    if (event.state === 'BEFORE') {
      /* Set to the first frame when entering scene */
      updateCanvas('0001')
    }
  })

  Scene.on('progress', function(event) {
    if (event.state === 'DURING') {
      updateCanvas(event.progress)
    }
  })

  Scene.on('end', function(event) {
    if (event.state === 'AFTER') {
      /* Set to the last frame when leaving scene */
      updateCanvas(padNumber(canvas.frames, 4))
    }
  })
}```
kiwwwipl commented 3 years ago

The data attribute workaround worked like a charm :)

About the canvas way: if I understand correctly first you have to export A LOT of individual images from a video/render, right?

jonkwheeler commented 3 years ago

That's right. It's an image sequence, not a video.

Happy the data attribute worked. 🤟