사이트명: 애플 에어팟 프로
작업 기간: 10일 소요
라이브러리: gsap, jquery
유형: PC 적응형, 클론 코딩
특징: gsap를 활용해 다양한 스크롤 이벤트를 구현한 동적인 페이지입니다.
Canvas태그
ScrollTrigger & forEach
sticky & GSAP timeline
setInterval
class
<div id="img_sequence">
<canvas width="1440" height="810" id="screen"></canvas>
</div>
var canvas = document.getElementById('screen');
var context = canvas.getContext('2d');
var scrollYPos = 0;
var img = new Image();
img.src = "./assets/images/canvas/0.png";//스크롤 전 첫 이미지
window.addEventListener('scroll', function(e){
scrollYPos = Math.round(window.scrollY/12);
if(scrollYPos > 64) {
scrollYPos = 64;
}
player(scrollYPos);
})
function player(num) {
img.src = "./assets/images/canvas/" + num + ".png";
}
img.addEventListener('load', function(e) {
context.clearRect(0, 0, context.canvas.width, context.canvas.height);
context.drawImage(img, 0, 0);
});
var scrollYPos = 0;
스크롤 내렸을때 변하는 scrolly포지션 값의 비율을 조절하기 위해 변수를 선언하였습니다.scrollYPos = Math.round(window.scrollY/12);
64장의 이미지를 스크롤 하였을때 부드러운 모션이 되게 하기 위해 scrollY값을 20분의 1로 나누었고 이때Math.round()
를 사용하여 정수가 되도록 설정해주었습니다.function player(num){~}
scrollYPos의 값에 따라 이미지를 불러오기 위해 이미지 명을 불러오는 함수를 사용했습니다.- 마지막으로
addEventListener()
메서드를 사용하여 'load'이벤트가 발생하면 캔버스가 빠르게 지워지고 다시 그려져 이미지가 움직이는 듯한 애니메이션을 구현하였습니다.
<ul class="content_list">
<li class="content_item">AirPods Pro가 더욱 풍부한 오디오 경험을 선사하도록 새롭게 설계되었습니다.</li>
<li class="content_item">한 차원 더 강력해진 액티브 노이즈 캔슬링 및 외부 소음을 선별적으로 줄여주는 적응형 주변음 허용 모드.</li>
<li class="content_item">놀라운 수준의 개인 맞춤형 몰입감을 선사하는 공간 음향.</li>
<li class="content_item">스와이프로 음량을 조절할 수 있게 해주는 터치 제어.</li>
<li class="content_item">그리고 한 번의 충전으로 6시간까지 사용 가능한 배터리 성능의 도약까지.</li>
</ul>
/**
* @i = 인덱스
* @l = 엘리먼트
*/
valueContentEl = document.querySelectorAll('.sc_value .content_item');
let i = 0;
valueContentEl.forEach(l => {
const tl4 = gsap.timeline({scrollTrigger: {
trigger: l,
start: "0% 70%",
end: "130% 70%",
scrub: 1,
}});
if(i === 4){//마지막 엘리먼트는 opacity:1 유지
tl4.to(l, {opacity:1},"-=0.2")
}else{
tl4.to(l, {opacity:1},"-=0.2")
.to(l, {duration:0.2, opacity:0.15})
}
i++;
});
📋 처음엔 gsap 타임라인에서 딜레이를 주어 각 리스트 오파시티를 조절 했으나 복잡해지고 반복되는 부분이 많아 foreach 반복문으로 변경해 좀 더 간결한 코드를 작성했습니다.
- 반복이 될 여러 요소를 가져오기 위해
querySelectorAll()
로 불러오고, 태그 갯수 만큼 각각 반복을 하기 위해foreach()
를 사용해 주었습니다.- 마지막 엘리먼트는 스크롤 해도 opacity:1을 유지하기 위해
i++;
i는 증가하도록 설정해주고if(i === 4){~}
라는 조건을 주어 i가 4일 경우에만 opacity를 유지시켰습니다.❔ foreach문
forEach() 메서드는 배열에 활용이 가능한 메서드로, 주어진 함수를 배열 요소 각각에 대해 실행하는 메서드이다.
.sc_audio .group_xray {
position: relative;
height: 300vh;
}
.sc_audio .group_xray .sticky_inner {
position: sticky;
height: 100vh;
top: 30px;
}
const xrayScrollAni = gsap.timeline({scrollTrigger: {
defaults:{
ease:'none'
},
trigger: ".group_xray",
start: "0% top",
end: "100% 150%",
scrub: true,
}});
gsap.set('.group_xray .desc01,.group_xray .desc02,.group_xray .desc03',{y:150, opacity:0})
xrayScrollAni
.addLabel('a')
.to(".group_xray .desc01",{y:-50, opacity:1},'a')
.to(".group_xray .desc01", {y:-200,opacity:0},'a+1')
.to(".group_xray .desc02",{y:-50, opacity:1},'a+1.5')
.to(".group_xray .desc02", {y:-200, opacity:0},'a+2')
.to(".group_xray .lockup_left img", {duration:2,scale:0.9},'a')
.to(".group_xray .lockup_left img", {opacity:0},'a+2')
.addLabel('b')
.to(".group_xray .lockup_right img", {opacity:1, scale:0.95},'b')
.to(".group_xray .lockup_right img", {scale:0.86},'b+0.6')
.to(".group_xray .lockup_right img", {opacity:0},'b+0.6')
.to(".group_xray .desc03",{y:-50, opacity:1},'b')
.to(".group_xray .desc03", {y:-200,opacity:0},'b+0.6')
.addLabel('c')
.to(".group_xray .glitter_area img", {opacity:1},'c-=0.6')
.to(".group_xray .glitter_area img", {duration:2, scale:0.78},'c')
.to(".group_xray .glitter_area video", {opacity:1, scale:1},'c-=0.1')
.to(".group_xray .glitter_area .btn_control", {opacity:1},'c-=0.1');
- 정해진 위치까지 fixed처럼 고정된 효과를 내주는 position: sticky 속성을 이용해 '.sticky_inner'가 30px에 도달하면 고정되도록 하였습니다.
- addlabel 속성을 이용해 크게 모션을 3그룹으로 나누었고, 그 안에서도 디테일하게 딜레이를 주었는데 'img'의 사이즈가 작아지는 동안 'desc'요소 들이 움직이는 애니메이션으로 'img'에만
duration:2
를 주어 움직임 속도를 맞추었습니다.- 스크롤 했을때 애니메이션이 빠르게 움직이는 느낌이 있어
end값을 150%
로 길게 주었습니다.
ㅤ💡 sticky 속성 이용시 주의점
상위 요소에 oveflow 속성이 있으면 동작하지 않음
top, left 등 임계값을 설정해야 함
📋 스크롤에 따라 동영상이 재생되도록 구현은 했으나 동영상이 시작되어야 할 위치가 아닌 페이지 제일 상단부터 동영상이 재생되는 이슈를 해결하기 위해 ScrollTrigger 이용해 코드 수정을 하였습니다.
const vid = document.getElementById('case_video');
window.onscroll = function(){
vid.pause();
};
setInterval(function(){
vid.currentTime = window.pageYOffset/400;
}, 40);
const vid2 = document.getElementById('case_video');
ScrollTrigger.create({
trigger:".sc_case .group_case",
start:"0% 0%",
end:"70% 50%",
scrub:1,
onEnter:function(self){
setInterval(function(){
vid2.currentTime = self.progress.toFixed(3)*5.5;
}, 40);
},
});
getElementById()
메서드로 id가case_video
인 요소를 가져와ScrollTrigger.create()
로 강제 스크롤 이벤트를 생성하였습니다.- 위치에 도달 했을때 동영상을 실행시키기 위해
onEnter
를 사용하였고,setInterval()
함수로 0.04초 간격으로 진행되도록 하였습니다.vid2.currentTime = self.progress.toFixed(3)*5.5;
기본은 1초의 애니메이션 진행이기 때문에 동영상 총길이인 5.5초를 곱해주었습니다.
ㅤ💡 toFixed(): 소수자리 표현
toFixed(3) -> 소수점 3자리수 까지
📋 페이지 내 대부분의 동영상 우측 하단에 동영상 재생/일시정지 버튼이 있습니다. 공통 영역으로 코드를 작성해보았습니다.
<div class="video_inner video_frame">
<div class="video_area">
<video src="./assets/images/value_video.mp4" poster="./assets/images/value_video.jpg" autoplay muted loop></video>
</div>
<button class="btn_control" aria-label="동영상 일시정지"></button>
</div>
.btn_control::after {
content: "";
font-family: "SF Pro Icons";
display: inline-block;
}
.btn_control.on::after {
content: "";
}
$('.btn_control').click(function(){
if($(this).hasClass('on')){
$(this).parents('.video_frame').find('video').get(0).play();
$(this).attr('aria-label','정지').removeClass('on')
}else{
$(this).parents('.video_frame').find('video').get(0).pause();
$(this).attr('aria-label','재생').addClass('on')
}
});
- 버튼의 부모 요소에
video_frame
클래스를 주었고, 해당 클래스를 가지고 있는 부모 안에video
를 찾아play 또는 pause
를 시킨다.
hasClass(),removeClass()
로 'on'이라는 클래스를 재생, 일시정지에 맞춰 가상요소 after선택자에 넣어준 content를 바꾸어 준다.- 텍스트가 없는 버튼이라
aria-label
을 사용하여 버튼의 동작을 간략하게 설명하여 웹 접근성을 고려하였고, 위의 조건문에 함께 작성하여 버튼 클릭시 "정지"/"재생"으로 설명이 바뀔수 있도록 하였다.
ㅤ💡 ::after 사용시 주의점
'content' 속성에 값을 꼭 지정해 주어야 한다.
안녕하세요 혹시 이미지 나 영상들은 어떻게 구하셨나요?