Image Sequence 스크롤 애니메이션 with GSAP

jellykelly·2023년 6월 19일
4

UI Interaction

목록 보기
2/3
post-thumbnail
post-custom-banner

ScrollMagic을 활용한 이미지 시퀀스 스크롤 애니메이션 구현하기 with GSAP

애플 제품 소개 페이지에서 자주 사용되는 사용자 경험 중 하나인 이미지 시퀀스 애니메이션은, 스크롤 이동 정도에 따라 이미지의 프레임이 움직이면서 마치 영상이 재생되는 듯한 효과를 보여줍니다.

이번 포스팅에서는 ScrollMagicGSAP 라이브러리를 사용하여 스크롤 애니메이션을 구현해 보겠습니다 🤗

준비

설치

TweenMaxScrollMagic 라이브러리를 CDN으로 설치합니다.
Tween을 ScrollMagic으로 제어하기 위해 animation.gsap 플러그인도 함께 설치할게요

<script src="https://cdnjs.cloudflare.com/ajax/libs/gsap/2.1.3/TweenMax.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/ScrollMagic/2.0.7/ScrollMagic.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/ScrollMagic/2.0.7/plugins/animation.gsap.min.js"></script>

그리고 디버깅용 플러그인을 설치합니다. 애니메이션의 시작과 끝 지점을 화면에 표시해 주기 때문에 작업할 때 매우 편리합니다!

<script src="https://cdnjs.cloudflare.com/ajax/libs/ScrollMagic/2.0.7/plugins/debug.addIndicators.min.js"></script>

시퀀스 이미지

라이브러리 설치가 완료되었으면 우리에게 필요한 것은 컨트롤할 이미지입니다!
이미지 시퀀스 애니메이션은 영상이 재생되는 것처럼 보이는 효과라고 말씀드렸는데요, 그래서 우리는 영상의 한 프레임을 한 장의 이미지로 쪼개서 준비해야 합니다. 프레임 수가 높은 영상일수록 끊기지 않고 재생 (스크롤링) 되겠죠?
영상을 이미지로 변환하는 사이트에서 간단하게 이미지를 추출해서 준비해 봅시다.

파일명은 avata-x로 지정할게요.
시작 이미지 avata-1에서 스크롤을 내리면 avata-2, avata-3, avata-4와 같이 avata- 뒤의 숫자가 커지고, avata-4에서 스크롤을 올리면 avata-3, avata-2, avata-1처럼 숫자가 작아질 거예요.
그렇기 때문에 모든 연속된 숫자로 지정해야 합니다.

Structure

HTML

<div class="wrap">
  	...
    <div id="trigger" class="pinned">
        <img class="avata" src="images/avata-1.png" alt="아바타 캐릭터 이미지 시퀀스">
        <h2 class="text">Hello world!</h2>
    </div>
</div>

스크롤을 움직일 때에도 아래 이미지의 초록색 부분은 고정되어 있습니다.
div.wrap이 배경 영역이고 div.pinned이 초록색 부분이 됩니다.

Javascript

시퀀싱 이미지 배열 생성

이미지의 파일명을 for 문으로 이미지 개수만큼 반복해서 배열을 생성하여 이미지를 정의합니다.

const $avataImgSqc = new Array();

for (let i = 1; i < 48; i++) {
    $avataImgSqc.push(`images/avata-${i}.png`);
}

애니메이션 오브젝트 생성

TweenMax.to( target:Object, duration:Number, vars:Object ) 문법에 따라 애니메이션 오브젝트를 생성해 보겠습니다 😉

이번 포스팅에서는 .to() 메서드를 사용하겠습니다. 더 많은 메서드는 GreenSock Docs 를 참고하세요.

const $img = { crntImg: 0 },
      $imgTag = document.querySelector('.avata');

let $tween_avata = TweenMax.to($img, 1, {
    crntImg: $avataImgSqc.length - 1, // crntImg(0)부터 전체 이미지 개수 animate 속성
    roundProps: 'crntImg', // 배열 인덱스로 사용되기 때문에 정수만 입력
    immediateRender: true, // 첫번째 이미지 자동로딩 여부
    onUpdate: function () {
        $imgTag.setAttribute('src', $avataImgSqc[$img.crntImg]) // 이미지 소스 세팅
    }
});

Controller 생성

이제 ScrollMagic에 대입해 봅시다. 먼저 new 키워드로 ScrollMagic Controller 인스턴스를 생성합니다.

const controller = new ScrollMagic.Controller();

Scene 생성

Scene 오브젝트를 생성합니다. Scrollmagic Docs 참고하시면 다양한 메서드와 옵션을 확인하실 수 있습니다!

new ScrollMagic.Scene();

이제 만들어 둔 애니메이션 오브젝트를 Scene에 추가 후 마지막으로 Controller에 추가합니다.

new ScrollMagic.Scene()
    .setTween($tween_avata)
	.addTo($controller);

스크롤 이동시 고정될 영역과 Scene이 시작될 트리거를 삽입해 봅시다! addIndicators()를 추가해서 어떤 지점에서 애니메이션이 시작되는지 확인해 볼게요

new ScrollMagic.Scene({
    triggerElement: '#trigger'
})
    .setTween($tween_avata)
    .setPin('.pinned')
    .addTo($controller)
    .addIndicators();

애니메이션은 start 지점이 trigger를 지나는 순간 실행됩니다.

그러나 지금은 start 지점 = end 지점이기 때문에, start 지점을 지나는 순간 앞서 배열로 만들어뒀던 for 문이 돌면서 이미지 시퀀싱이 한 번에 실행되어버립니다 🤯

Duration 지정

end 지점을 정하기 위해 Scene의 옵션 중 duration을 사용해서 .pinned의 길이를 지정하겠습니다. 임의로 5000으로 지정해 볼게요.
triggerHook으로 trigger가 일어날 지점의 위치를 지정할 수 있는데요. 0 = viewport 기준 가장 상단, 1= 가장 하단입니다.

new ScrollMagic.Scene({
    duration: 5000,
    triggerElement: '#trigger',
    triggerHook: 0,
})
    .setTween($tween_avata)
    .setPin('.pinned')
    .addTo($controller);

자 이제 #trigger를 지나 5000만큼 스크롤 하는 동안 .pinned 영역이 고정되면서 img 태그의 src 속성값이 바뀌게 됩니다.

<img class="avata" src="images/avata-1.png">
<img class="avata" src="images/avata-2.png">
<img class="avata" src="images/avata-3.png">
...

image preload

이미지의 로딩이 느리다면 프레임이 뚝뚝 끊기는 것처럼 보일 수 있겠죠?
이미지를 미리 불러오는 방법으로 해결할 수 있습니다 🤟

function preloading(preImgs) {
    let imgTotal = preImgs.length;
    for (let i = 0; i < imgTotal; i++) {
        let img = new Image();
        img.src = preImgs[i];
    }
}

전체코드 🗝

const $avataImgSqc = new Array();

for (let i = 1; i < 48; i++) {
    $avataImgSqc.push(`images/avata-${i}.png`);
}

function preloading(preImgs) {
    let imgTotal = preImgs.length;
    for (let i = 0; i < imgTotal; i++) {
        let img = new Image();
        img.src = preImgs[i];
    }
}

preloading($avataImgSqc)


const $img = { crntImg: 0 },
      $imgTag = document.querySelector('.avata');

let $tween_avata = TweenMax.to($img, 1, {
    crntImg: $avataImgSqc.length - 1,
    roundProps: 'crntImg',
    immediateRender: true,
    onUpdate: function () {
        $imgTag.setAttribute('src', $avataImgSqc[$img.crntImg])
    }
});

new ScrollMagic.Scene({
    duration: 5000,
    triggerElement: '#trigger',
    triggerHook: 0
})
    .setTween($tween_avata)
    .setPin('.pinned')
    .addTo($controller);

참고 : https://scrollmagic.io/examples/expert/image_sequence.html

profile
Hawaiian pizza with extra pineapples please! 🥤
post-custom-banner

0개의 댓글