스크롤 효과
1. 스크롤 효과를 만들어보자
섹션 제목: “1. 스크롤 효과를 만들어보자”1.1. 1단계-섹션1
섹션 제목: “1.1. 1단계-섹션1”1.1.1. 좌우이동 - css 메서드 활용
섹션 제목: “1.1.1. 좌우이동 - css 메서드 활용”<!DOCTYPE html><html lang="ko"> <head> <meta charset="UTF-8" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" /> <title>Document</title> <link rel="stylesheet" href="css/jq-01.css" /> <script src="https://code.jquery.com/jquery-3.7.1.min.js"></script> <script src="js/jq-01.js" defer></script> </head>
<body> <section class="section1"> <h2>section1</h2> <div class="container"> <div class="box box1 bg2"></div> <div class="box box2 bg3"></div> </div> </section> <section class="section2"> <h2>section2</h2> <div class="box box1 bg4"></div> <div class="box box2 bg5"></div> </section> <section class="section3"> <h2>section3</h2> <div class="box box1 bg2"></div> <div class="box box2 bg1"></div> </section> <section class="section4"> <h2>section4</h2> <div class="box box1 bg2"></div> <div class="box box2 bg1"></div> </section> <section class="section5"> <h2>section5</h2> <div class="box box1 bg2"></div> <div class="box box2 bg3"></div> </section> </body></html>* { margin: 0; padding: 0;}:root { --bg1: #285dfb; --bg2: #537dfb; --bg3: #7e9efc; --bg4: #a9befd; --bg5: #d4dffe;}.bg1 { background-color: var(--bg1); color: var(--bg5);}.bg2 { background-color: var(--bg2); color: var(--bg4);}.bg3 { background-color: var(--bg3); color: var(--bg3);}.bg4 { background-color: var(--bg4); color: var(--bg2);}.bg5 { background-color: var(--bg5); color: var(--bg1);}section { overflow: hidden; text-align: center; width: 100%; height: 100vh;}
section h2 { padding: 12vw 6vw;}.box { display: inline-block; /* clamp (최소,기본,최대)최소, 최대가 명확한 경우 사용가능 */ width: clamp(100px, 30%, 100%); height: 300px; transition: all 2s;}.box1 { transform: translateX(-200%);}.box2 { transform: translateX(200%);}.box.in { transform: translateX(-200%);}$(window).on('scroll', () => { let winSCT; const sections = $('section'); winSCT = $(window).scrollTop(); sections.each(function (idx, o) { $(o).addClass(`bg${idx + 1}`); const tg = $(this); const tgtop = tg.offset().top; if (winSCT > tgtop) { tg.find('.box').css('transform', 'translateX(0%)'); } else if (winSCT > tgtop) { tg.find('.box').css('transform', 'translateX(0%)'); } else if (winSCT > tgtop) { tg.find('.box').css('transform', 'translateX(0%)'); } });});1.2. 2단계-섹션2
섹션 제목: “1.2. 2단계-섹션2”1.2.1. 상하이동 시간차-animate() 메서드 활용
섹션 제목: “1.2.1. 상하이동 시간차-animate() 메서드 활용”<section class="section2"> <h2>section2</h2> <div class="gallery"> <div class="box bg3"></div> <div class="box bg4"></div> <div class="box bg5"></div> </div></section>.section1 .box { display: inline-block; /* 최소, 최대가 명확한 경우 사용가능 */ width: clamp(100px, 30%, 100%); height: 300px; transition: all 2s;}
.section1 .box1 { transform: translateX(-200%);}
.section1 .box2 { transform: translateX(200%);}
.section1 .box.in { transform: translateX(-200%);}
.section2 { position: relative;}.section2 .gallery { position: relative;}.section2 .gallery .box { width: 15vw; height: 200px; position: absolute; opacity: 0; top: 100vw;}
.section2 .bg3 { left: 10vw;}
.section2 .bg4 { left: 40vw;}
.section2 .bg5 { right: 10vw;}const sections = $('section');let speed = Math.floor(sections.outerHeight() * 0.2);let topArr = [];let winSCT;
sections.each((idx, section) => { $(section).addClass(`bg${idx + 1}`); const sectionTop = $(section).offset().top; topArr.push(sectionTop);});
$(window).on('scroll', () => { winSCT = $(window).scrollTop(); if (winSCT > topArr[0] winSCT < topArr[1] - speed) { sections.eq(0).find('.box').css('transform', 'translateX(0%)'); } if (winSCT > topArr[1] winSCT < topArr[2]) { sections.eq(1).find('.bg3').stop().delay(0).animate({ top: '5vw', opacity: 1 }, 500, 'swing'); sections.eq(1).find('.bg4').stop().delay(100).animate({ top: '0vw', opacity: 1 }, 800, 'swing'); sections.eq(1).find('.bg5').stop().delay(200).animate({ top: '-5vw', opacity: 1 }, 1100, 'swing'); }});:::note 1단계의 코드는 크게 4가지의 버그가 발생할수 있다.
- 중복된 조건: if (winSCT > tgtop)같은 조건을 세번 반복 하여 비교하고 있다. 같은 조건이 성립되면 .box 요소의 스타일 또한 여러번 설정될 수 있다.
- 섹션별 offsetTop 값을 배열로 저장하여 이벤트 핸들러에 전달한다.
- 부적절한 비교 연산자: 모든 else if 문의 조건이 winSCT > tgtop으로 설정되어 있으므로 첫 번째 if 문과 동일한 조건이다. 결국 두 번째와 세 번째 else if 문은 실행되지 않는다.
- 배열의 인덱스 번호를 활용하여 조건을 명확하게 지정한다.
- 변수 범위(scope): winSCT, sections, tg, 그리고 tgtop 변수는 모두 함수 내부에서 선언되었다. 각 섹션 반복문마다 새로운 변수 인스턴스가 생성되므로 원하는 결과를 얻을 수 없을 수 있습니다.
- 전역변수로 수정한다.
- 중첩된 스크롤 이벤트 핸들러: 이벤트 핸들러 함수를 최상위 레벨로 작성하게 될경우 추후 하위에 다른 이벤트핸들러를 포함하게 되어 예기치 못한 동작을 초래할수 있다.
- 반복문과 이벤트 핸들러를 분리한다. :::
1.3. 3단계-섹션3
섹션 제목: “1.3. 3단계-섹션3”1.3.1. 리빌효과-addClass() 메서드 활용
섹션 제목: “1.3.1. 리빌효과-addClass() 메서드 활용”마스크효과를 구현해보자
<section class="section3"> <div class="item"> <h2>section3</h2> <figure> <img src="http://qwerew.cafe24.com/images/1.jpg" alt="" /> <figcaption>yum yum</figcaption> </figure> </div> <div class="item"> <p>Lorem ipsum dolor sit amet consectetur adipisicing elit. Saepe est nostrum amet eligendi quas fugit libero cumque deserunt voluptate placeat dolorum culpa praesentium reiciendis, aliquid ad illum laborum, harum ratione.</p> </div></section>.section3 { display: flex; color: #333; gap: 2rem;}
.section3 .item:nth-child(1) { flex-basis: 60%;}
.section3 .item:nth-child(2) { flex-basis: 40%; align-self: center;}
.section3 figure { position: relative; box-shadow: -1rem 1rem 3rem -2rem rgba(0, 0, 0, 0.5);}
.section3 figure:before { position: absolute; top: 0; right: 0; bottom: 0; left: 0; background: var(--bg1); transition: clip-path 0.8s cubic-bezier(0.18, 0.89, 0.32, 1.28);}
.section3 figure img { width: 100%; display: block; clip-path: inset(0 100% 0 0); /* duration 0.6 delay 0.3 */ transition: clip-path 0.6s 0.3s cubic-bezier(0.18, 0.89, 0.32, 1.28);}
.section3 figure figcaption { position: absolute; top: 20px; right: 20px; padding: 10px; font-weight: bold; text-transform: uppercase; color: #fff; background: var(--bg1); mix-blend-mode: difference; transition: clip-path 0.3s 0.9s cubic-bezier(0.18, 0.89, 0.32, 1.28);}
.section3 figure::before,.section3 figure figcaption { clip-path: inset(0 0 0 100%);}
.section3.is-animated figure::before,.section3.is-animated figure img,.section3.is-animated figure figcaption { clip-path: inset(0);}...생략if (winSCT > topArr[2] && winSCT < topArr[3]-speed) { sections.eq(2).addClass('is-animated');}1.4. 4단계-섹션4
섹션 제목: “1.4. 4단계-섹션4”1.4.1. PIP스크롤
섹션 제목: “1.4.1. PIP스크롤”화면안의 화면이 스크롤 되는 효과를 만들어보자
<section class="section4"> <h2>section4</h2> <div class="container"> <div class="item left pa"> <div class="mockup pc"> <div class="mask"><img src="image/project1_pc.png" alt="" class="screen" /></div> <img src="image/desktop.png" alt="" class="device" /> </div> <div class="mockup mobile"> <div class="mask"><img src="image/project1_mobile.png" alt="" class="screen" /></div> <img src="image/mobile.png" alt="" class="device" /> </div> </div> <div class="item right bg1 pa"></div> </div></section>.section4 .container { display: flex; position: relative;}.pa { position: absolute; top: 0;}
.item { height: 30vw;}.left { width: 60vw; transition: left 1s ease-in-out; left: -100%;}.right { width: 40vw; right: 0;}.is-animated .left { left: 0;}.left .mockup img,.left .mockup .mask { position: absolute; top: 0; left: 0;}.left .mockup.pc { margin-left: clamp(5%, 100px, 10%); position: relative; width: 60%; height: 100%;}
.left .mockup.pc .mask { z-index: 3; width: 32.3vw; height: 61.8%; overflow: hidden; top: 6%; left: 5.2%;}.left .mockup.pc img.screen { z-index: 1; width: 100%;}.left .mockup.pc img.device { z-index: 2; width: 100%;}/* mobile */.left .mockup.mobile { z-index: 99; position: relative; top: -76%; left: 58%; width: 20%; height: 60%;}
.left .mockup.mobile .mask { z-index: 999; width: 10.5vw; height: 100%; overflow: hidden; top: 10.8%; left: 7%; border-radius: 16px 16px 0 0;}.left .mockup.mobile img.screen { z-index: 3; width: 100%;}.left .mockup.mobile img.device { z-index: 4; width: 100%;}$(() => { const sections = $('section'); let speed = Math.floor(sections.outerHeight() * 0.3); let topArr = []; let winSCT;
sections.each((idx, section) => { $(section).addClass(`bg${idx + 1}`); const sectionTop = $(section).offset().top; topArr.push(sectionTop); });
/* 스크롤함수 */ $(window).on('scroll', () => { winSCT = $(window).scrollTop(); if (winSCT > topArr[0] && winSCT < topArr[1] - speed) { sections.eq(0).find('.box').css('transform', 'translateX(0%)'); }
if (winSCT > topArr[1] && winSCT < topArr[2] - speed) { sections.eq(1).find('.bg3').stop().delay(100).animate({ top: 0, opacity: 1 }, 500, 'swing'); sections.eq(1).find('.bg4').stop().delay(200).animate({ top: -100, opacity: 1 }, 800, 'swing'); sections.eq(1).find('.bg5').stop().delay(300).animate({ top: -200, opacity: 1 }, 1100, 'swing'); }
if (winSCT > topArr[2] && winSCT < topArr[3] - speed) { console.log(winSCT > topArr[2] && winSCT < topArr[3]); sections.eq(2).addClass('is-animated'); } if (winSCT > topArr[3] && winSCT < topArr[4]) { sections.eq(3).addClass('is-animated'); } }); pipScroll(); function pipScroll() { const section = sections.eq(3); const devices = ['.mockup.pc', '.mockup.mobile'];
$.each(devices, function (i, deviceEl) { const device = section.find(deviceEl); const screen = device.find('.mask>img'); const mask = device.find('.mask'); const heightDifference = screen.innerHeight() - mask.innerHeight(); console.log(device.innerHeight()); console.log(screen.innerHeight());
device.on({ mouseenter: function () { if (section.hasClass('is-animated')) { screen.stop().animate({ top: -heightDifference }, 1000); } }, mouseleave: function () { if (section.hasClass('is-animated')) { screen.stop().animate({ top: 0 }, 1000); } }, }); }); }}); //jQuery// 윈도우 크기가 변경될 때 heightDifference를 다시 계산.function pipScroll() {...생략$(window).on('resize', function () { $.each(devices, function (i, deviceEl) { let device = section.find(deviceEl); let screen = device.find('.mask>img'); let mask = device.find('.mask'); let heightDifference = screen.innerHeight() - mask.innerHeight();
// heightDifference를 다시 설정. device.data('heightDifference', heightDifference); console.log(heightDifference); }); }); }})//jQuery1.4.2. PIP스크롤 발전형
섹션 제목: “1.4.2. PIP스크롤 발전형”여러 섹션에서 사용할수 사용할수 있도록 함수를 수정한다.
<section class="section5"> <div class="container"> <div class="item left"> <div class="mockup pc"> <div class="mask"> <img src="image/project1_pc.png" alt="" class="screen" /> </div> <img src="image/desktop.png" alt="" class="device" /> </div> <!-- //.mockup.pc --> <div class="mockup mobile"> <div class="mask"><img src="image/project1_mobile.png" alt="" class="screen" /></div> <img src="image/mobile.png" alt="" class="device" /> </div> <!-- //.mockup.mobile --> </div> <!-- //.left --> <div class="item right bg1"></div> </div> <!-- //.right --></section>섹션추가
.section5,.section4 { position: relative;}.section5 .container,.section4 .container { position: relative; /* absolute를 제어하는 relative 는 꼭 크기를 넣을것 */ width: 100%; height: 100%;}.section5 .item,.section4 .item { position: absolute; top: 0; height: 30vw;}.section5 .item.left,.section4 .item.left { width: 60%; /* */ transition: left 1s ease-in-out; left: -100%;}.section5.is-animated .item.left,.section4.is-animated .item.left { left: 0%; transition: left 2s ease-in-out;}
.section5 .item.right,.section4 .item.right { width: 40%; right: 0;}.section5 .mockup.pc .mask,.section4 .mockup.pc .mask { z-index: 8; width: 32.3vw; height: 61.8%; overflow: hidden; left: 5.2%; top: 6%;}.section4 선택자에 .section5를 추가
const win = $(window);const sections = $('section');let speed = Math.floor(win.height() * 0.5);let topArr = [];let winSCT;console.log(speed);//sections.offsetTopsections.each(function (i, o) { const sectionTop = $(o).offset().top; topArr.push(sectionTop);});win.on('scroll', () => { winSCT = win.scrollTop(); if (winSCT > topArr[0] && winSCT < topArr[1]) { sections.eq(0).addClass('is-animated').siblings().removeClass('is-animated'); } if (winSCT > topArr[1] - speed && winSCT < topArr[2]) { sections.eq(1).addClass('is-animated').siblings().removeClass('is-animated'); } if (winSCT > topArr[2] - speed && winSCT < topArr[3]) { sections.eq(2).addClass('is-animated').siblings().removeClass('is-animated'); } if (winSCT > topArr[3] - speed && winSCT < topArr[4]) { sections.eq(3).addClass('is-animated').siblings().removeClass('is-animated'); pipScroll(); console.log(topArr[4], winSCT); } if (winSCT > topArr[4] - speed) { sections.eq(4).addClass('is-animated').siblings().removeClass('is-animated'); pipScroll(); }});
function pipScroll() { const devices = ['.mockup.pc', '.mockup.mobile']; $.each(devices, function (i, deviceEl) { const device = $(deviceEl); const screen = device.find('.screen'); const mask = device.find('.mask'); const hightDifference = screen.innerHeight() - mask.innerHeight(); console.log('hightDifference', hightDifference); device.on({ mouseenter: function () { screen.stop().animate({ top: -hightDifference }, 1000); }, mouseleave: function () { screen.stop().animate({ top: 0 }, 1000); }, }); });}win.on('resize', function () { pipScroll();});위의 코드는 화면에는 보이지 않지만 버그가 있다.
['.mockup.pc', '.mockup.mobile']에는 단일 요소가 할당 되었으므로두개의 mockup.pc 가 추가 될 경우 각각 동작하지 않는다. 이것을 제이쿼리 객체로 변경하면 여러개 요소를 알아서 반복 처리 하기 때문에 각각 다른 이벤트를 처리할수 있다. 아래처럼 수정하자
function pipScroll() { //const devices = ['.mockup.pc', '.mockup.mobile']; const devices = $('.mockup.pc, .mockup.mobile'); //$.each(devices, function (i, deviceEl) { devices.each(function (i, deviceEl) { let device = $(this); let screen = device.find('.mask>img');1.5. 가로 스크롤 효과
섹션 제목: “1.5. 가로 스크롤 효과”<!DOCTYPE html><html lang="en"> <head> <meta charset="UTF-8" /> <meta http-equiv="X-UA-Compatible" content="IE=edge" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" /> <title></title> </head>
<body> <main class="container"> <section id="section1" class="item"> <h2 class="num">01</h2> </section> <section id="section2" class="item"> <h2 class="num">02</h2> </section> <section id="section3" class="item"> <h2 class="num">03</h2> </section> <section id="section4" class="item"> <h2 class="num">04</h2> </section> <section id="section5" class="item"> <h2 class="num">05</h2> </section> <section id="section6" class="item"> <h2 class="num">06</h2> </section> <section id="section7" class="item"> <h2 class="num">07</h2> </section> <section id="section8" class="item"> <h2 class="num">08</h2> </section> <section id="section9" class="item"> <h2 class="num">09</h2> </section> </main> </body></html>.container { position: fixed; left: 0; top: 0; display: flex;}.item { width: 100vw; height: 100vh; position: relative;}#section1 { background-color: #111;}#section2 { background-color: #222;}#section3 { background-color: #333;}#section4 { background-color: #444;}#section5 { background-color: #555;}#section6 { background-color: #666;}#section7 { background-color: #777;}#section8 { background-color: #888;}#section9 { background-color: #999;}
.num { position: absolute; bottom: 20px; right: 20px; color: #fff; font-size: 20vw; z-index: 10000;}자바스크립트는 gsap 을 사용
<script src="https://unpkg.com/gsap@3/dist/gsap.min.js"></script>
const cont = document.querySelector('.container');const item = document.querySelector('.item');
function scroll() { let scrollTop = window.scrollY; let offsetLeft = cont.offsetWidth; document.body.style.height = offsetLeft + 'px';
let viewWidth = offsetLeft - window.innerWidth; let viewHeight = offsetLeft - window.innerHeight; let goLeft = scrollTop * (viewWidth / viewHeight);
gsap.to(cont, { left: -goLeft });
requestAnimationFrame(scroll);}scroll();
window.addEventListener('resize', scroll);const cont = $('.container');function scroll() { let scrollTop = $(window).scrollTop(); let offsetLeft = cont.outerWidth(); $('body').css('height', offsetLeft + 'px');
let viewWidth = offsetLeft - $(window).innerWidth(); let viewHeight = offsetLeft - $(window).innerHeight(); let goLeft = scrollTop * (viewWidth / viewHeight);
cont.css('left', -goLeft); console.log(scrollTop, offsetLeft, viewWidth, goLeft, viewHeight);}scroll();$(window).on({ resize: function () { scroll(); }, scroll: function () { scroll(); },});