14 스크롤 효과
1. 스크롤 효과를 만들어보자
1.1. 1단계-섹션1
1.1.1. 좌우이동 - css 메서드 활용
- 예제
- HTML
- CSS
- JS
참고
이 코드는 잠재적인 버그가 있다. 작성후 어떤 버그가 발생할수 있을지 예상해보자
1<!DOCTYPE html>2<html lang="ko">3 <head>4 <meta charset="UTF-8" />5 <meta name="viewport" content="width=device-width, initial-scale=1.0" />6 <title>Document</title>7 <link rel="stylesheet" href="css/jq-01.css" />8 <script src="https://code.jquery.com/jquery-3.7.1.min.js"></script>9 <script src="js/jq-01.js" defer></script>10 </head>11
12 <body>13 <section class="section1">14 <h2>section1</h2>15 <div class="container">16 <div class="box box1 bg2"></div>17 <div class="box box2 bg3"></div>18 </div>19 </section>20 <section class="section2">21 <h2>section2</h2>22 <div class="box box1 bg4"></div>23 <div class="box box2 bg5"></div>24 </section>25 <section class="section3">26 <h2>section3</h2>27 <div class="box box1 bg2"></div>28 <div class="box box2 bg1"></div>29 </section>30 <section class="section4">31 <h2>section4</h2>32 <div class="box box1 bg2"></div>33 <div class="box box2 bg1"></div>34 </section>35 <section class="section5">36 <h2>section5</h2>37 <div class="box box1 bg2"></div>38 <div class="box box2 bg3"></div>39 </section>40 </body>41</html>1* {2 margin: 0;3 padding: 0;4}5:root {6 --bg1: #285dfb;7 --bg2: #537dfb;8 --bg3: #7e9efc;9 --bg4: #a9befd;10 --bg5: #d4dffe;11}12.bg1 {13 background-color: var(--bg1);14 color: var(--bg5);15}16.bg2 {17 background-color: var(--bg2);18 color: var(--bg4);19}20.bg3 {21 background-color: var(--bg3);22 color: var(--bg3);23}24.bg4 {25 background-color: var(--bg4);26 color: var(--bg2);27}28.bg5 {29 background-color: var(--bg5);30 color: var(--bg1);31}32section {33 overflow: hidden;34 text-align: center;35 width: 100%;36 height: 100vh;37}38
39section h2 {40 padding: 12vw 6vw;41}1.box {2 display: inline-block; /* clamp (최소,기본,최대)최소, 최대가 명확한 경우 사용가능 */3 width: clamp(100px, 30%, 100%);4 height: 300px;5 transition: all 2s;6}7.box1 {8 transform: translateX(-200%);9}10.box2 {11 transform: translateX(200%);12}13.box.in {14 transform: translateX(-200%);15}1$(window).on('scroll', () => {2 let winSCT;3 const sections = $('section');4 winSCT = $(window).scrollTop();5 sections.each(function (idx, o) {6 $(o).addClass(`bg${idx + 1}`);7 const tg = $(this);8 const tgtop = tg.offset().top;9 if (winSCT > tgtop) {10 tg.find('.box').css('transform', 'translateX(0%)');11 } else if (winSCT > tgtop) {12 tg.find('.box').css('transform', 'translateX(0%)');13 } else if (winSCT > tgtop) {14 tg.find('.box').css('transform', 'translateX(0%)');15 }16 });17});참고
첫번째 섹션에 도달시 애니메이트가 실행된다. 반복문 내에서 순회하는 this 의 거리를 비교하고 있으므로 조건 추가시 다음 섹션의 거리를 비교하여 애니메이트가 실행된다.
1.2. 2단계-섹션2
1.2.1. 상하이동 시간차-animate() 메서드 활용
- 예제
- HTML
- CSS
- JQ
1<section class="section2">2 <h2>section2</h2>3 <div class="gallery">4 <div class="box bg3"></div>5 <div class="box bg4"></div>6 <div class="box bg5"></div>7 </div>8</section>참고
section2의 구조를 수정한다.
1.section1 .box {2 display: inline-block;3 /* 최소, 최대가 명확한 경우 사용가능 */4 width: clamp(100px, 30%, 100%);5 height: 300px;6 transition: all 2s;7}8
9.section1 .box1 {10 transform: translateX(-200%);11}12
13.section1 .box2 {14 transform: translateX(200%);15}16
17.section1 .box.in {18 transform: translateX(-200%);19}20
21.section2 {22 position: relative;23}24.section2 .gallery {25 position: relative;26}27.section2 .gallery .box {28 width: 15vw;29 height: 200px;30 position: absolute;31 opacity: 0;32 top: 100vw;33}34
35.section2 .bg3 {36 left: 10vw;37}38
39.section2 .bg4 {40 left: 40vw;41}42
43.section2 .bg5 {44 right: 10vw;45}참고
css를 추가한다.
1const sections = $('section');2let speed = Math.floor(sections.outerHeight() * 0.2);3let topArr = [];4let winSCT;5
6sections.each((idx, section) => {7 $(section).addClass(`bg${idx + 1}`);8 const sectionTop = $(section).offset().top;9 topArr.push(sectionTop);10});11
12$(window).on('scroll', () => {13 winSCT = $(window).scrollTop();14 if (winSCT > topArr[0] winSCT < topArr[1] - speed) {15 sections.eq(0).find('.box').css('transform', 'translateX(0%)');16 }17 if (winSCT > topArr[1] winSCT < topArr[2]) {18 sections.eq(1).find('.bg3').stop().delay(0).animate({ top: '5vw', opacity: 1 }, 500, 'swing');19 sections.eq(1).find('.bg4').stop().delay(100).animate({ top: '0vw', opacity: 1 }, 800, 'swing');20 sections.eq(1).find('.bg5').stop().delay(200).animate({ top: '-5vw', opacity: 1 }, 1100, 'swing');21 }22});:::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.1. 리빌효과-addClass() 메서드 활용
마스크효과를 구현해보자
- 예제
- HTML
- CSS
- JQ
1<section class="section3">2 <div class="item">3 <h2>section3</h2>4 <figure>5 <img src="http://qwerew.cafe24.com/images/1.jpg" alt="" />6 <figcaption>yum yum</figcaption>7 </figure>8 </div>9 <div class="item">10 <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>11 </div>12</section>1.section3 {2 display: flex;3 color: #333;4 gap: 2rem;5}6
7.section3 .item:nth-child(1) {8 flex-basis: 60%;9}10
11.section3 .item:nth-child(2) {12 flex-basis: 40%;13 align-self: center;14}15
16.section3 figure {17 position: relative;18 box-shadow: -1rem 1rem 3rem -2rem rgba(0, 0, 0, 0.5);19}20
21.section3 figure:before {22 position: absolute;23 top: 0;24 right: 0;25 bottom: 0;26 left: 0;27 background: var(--bg1);28 transition: clip-path 0.8s cubic-bezier(0.18, 0.89, 0.32, 1.28);29}30
31.section3 figure img {32 width: 100%;33 display: block;34 clip-path: inset(0 100% 0 0);35 /* duration 0.6 delay 0.3 */36 transition: clip-path 0.6s 0.3s cubic-bezier(0.18, 0.89, 0.32, 1.28);37}38
39.section3 figure figcaption {40 position: absolute;41 top: 20px;42 right: 20px;43 padding: 10px;44 font-weight: bold;45 text-transform: uppercase;46 color: #fff;47 background: var(--bg1);48 mix-blend-mode: difference;49 transition: clip-path 0.3s 0.9s cubic-bezier(0.18, 0.89, 0.32, 1.28);50}51
52.section3 figure::before,53.section3 figure figcaption {54 clip-path: inset(0 0 0 100%);55}56
57.section3.is-animated figure::before,58.section3.is-animated figure img,59.section3.is-animated figure figcaption {60 clip-path: inset(0);61}1...생략2if (winSCT > topArr[2] && winSCT < topArr[3]-speed) {3 sections.eq(2).addClass('is-animated');4}참고
조건문을 추가한다.
1.4. 4단계-섹션4
1.4.1. PIP스크롤
화면안의 화면이 스크롤 되는 효과를 만들어보자
- 예제
- HTML
- CSS
- JQ
참고
아래의 이미지 파일을 다운로드 한다
1<section class="section4">2 <h2>section4</h2>3 <div class="container">4 <div class="item left pa">5 <div class="mockup pc">6 <div class="mask"><img src="image/project1_pc.png" alt="" class="screen" /></div>7 <img src="image/desktop.png" alt="" class="device" />8 </div>9 <div class="mockup mobile">10 <div class="mask"><img src="image/project1_mobile.png" alt="" class="screen" /></div>11 <img src="image/mobile.png" alt="" class="device" />12 </div>13 </div>14 <div class="item right bg1 pa"></div>15 </div>16</section>1.section4 .container {2 display: flex;3 position: relative;4}5.pa {6 position: absolute;7 top: 0;8}9
10.item {11 height: 30vw;12}13.left {14 width: 60vw;15 transition: left 1s ease-in-out;16 left: -100%;17}18.right {19 width: 40vw;20 right: 0;21}22.is-animated .left {23 left: 0;24}25.left .mockup img,26.left .mockup .mask {27 position: absolute;28 top: 0;29 left: 0;30}31.left .mockup.pc {32 margin-left: clamp(5%, 100px, 10%);33 position: relative;34 width: 60%;35 height: 100%;36}37
38.left .mockup.pc .mask {39 z-index: 3;40 width: 32.3vw;41 height: 61.8%;42 overflow: hidden;43 top: 6%;44 left: 5.2%;45}46.left .mockup.pc img.screen {47 z-index: 1;48 width: 100%;49}50.left .mockup.pc img.device {51 z-index: 2;52 width: 100%;53}54/* mobile */55.left .mockup.mobile {56 z-index: 99;57 position: relative;58 top: -76%;59 left: 58%;60 width: 20%;61 height: 60%;62}63
64.left .mockup.mobile .mask {65 z-index: 999;66 width: 10.5vw;67 height: 100%;68 overflow: hidden;69 top: 10.8%;70 left: 7%;71 border-radius: 16px 16px 0 0;72}73.left .mockup.mobile img.screen {74 z-index: 3;75 width: 100%;76}77.left .mockup.mobile img.device {78 z-index: 4;79 width: 100%;80}1$(() => {2 const sections = $('section');3 let speed = Math.floor(sections.outerHeight() * 0.3);4 let topArr = [];5 let winSCT;6
7 sections.each((idx, section) => {8 $(section).addClass(`bg${idx + 1}`);9 const sectionTop = $(section).offset().top;10 topArr.push(sectionTop);11 });12
13 /* 스크롤함수 */14 $(window).on('scroll', () => {15 winSCT = $(window).scrollTop();16 if (winSCT > topArr[0] && winSCT < topArr[1] - speed) {17 sections.eq(0).find('.box').css('transform', 'translateX(0%)');18 }19
20 if (winSCT > topArr[1] && winSCT < topArr[2] - speed) {21 sections.eq(1).find('.bg3').stop().delay(100).animate({ top: 0, opacity: 1 }, 500, 'swing');22 sections.eq(1).find('.bg4').stop().delay(200).animate({ top: -100, opacity: 1 }, 800, 'swing');23 sections.eq(1).find('.bg5').stop().delay(300).animate({ top: -200, opacity: 1 }, 1100, 'swing');24 }25
26 if (winSCT > topArr[2] && winSCT < topArr[3] - speed) {27 console.log(winSCT > topArr[2] && winSCT < topArr[3]);28 sections.eq(2).addClass('is-animated');29 }30 if (winSCT > topArr[3] && winSCT < topArr[4]) {31 sections.eq(3).addClass('is-animated');32 }33 });34 pipScroll();35 function pipScroll() {36 const section = sections.eq(3);37 const devices = ['.mockup.pc', '.mockup.mobile'];38
39 $.each(devices, function (i, deviceEl) {40 const device = section.find(deviceEl);41 const screen = device.find('.mask>img');42 const mask = device.find('.mask');43 const heightDifference = screen.innerHeight() - mask.innerHeight();44 console.log(device.innerHeight());45 console.log(screen.innerHeight());46
47 device.on({48 mouseenter: function () {49 if (section.hasClass('is-animated')) {50 screen.stop().animate({ top: -heightDifference }, 1000);51 }52 },53 mouseleave: function () {54 if (section.hasClass('is-animated')) {55 screen.stop().animate({ top: 0 }, 1000);56 }57 },58 });59 });60 }61}); //jQuery참고
창크기 변경시 스크롤값을 재계산하는 함수 추가 function pipScroll() 내에 작성한다.
1// 윈도우 크기가 변경될 때 heightDifference를 다시 계산.2function pipScroll() {3...생략4$(window).on('resize', function () {5 $.each(devices, function (i, deviceEl) {6 let device = section.find(deviceEl);7 let screen = device.find('.mask>img');8 let mask = device.find('.mask');9 let heightDifference = screen.innerHeight() - mask.innerHeight();10
11 // heightDifference를 다시 설정.12 device.data('heightDifference', heightDifference);13 console.log(heightDifference);14 });15 });16 }17})//jQuery1.4.2. PIP스크롤 발전형
여러 섹션에서 사용할수 사용할수 있도록 함수를 수정한다.
- 예제
- HTML
- CSS
- JS
1<section class="section5">2 <div class="container">3 <div class="item left">4 <div class="mockup pc">5 <div class="mask">6 <img src="image/project1_pc.png" alt="" class="screen" />7 </div>8 <img src="image/desktop.png" alt="" class="device" />9 </div>10 <!-- //.mockup.pc -->11 <div class="mockup mobile">12 <div class="mask"><img src="image/project1_mobile.png" alt="" class="screen" /></div>13 <img src="image/mobile.png" alt="" class="device" />14 </div>15 <!-- //.mockup.mobile -->16 </div>17 <!-- //.left -->18 <div class="item right bg1"></div>19 </div>20 <!-- //.right -->21</section>섹션추가
1.section5,2.section4 {3 position: relative;4}5.section5 .container,6.section4 .container {7 position: relative; /* absolute를 제어하는 relative 는 꼭 크기를 넣을것 */8 width: 100%;9 height: 100%;10}11.section5 .item,12.section4 .item {13 position: absolute;14 top: 0;15 height: 30vw;16}17.section5 .item.left,18.section4 .item.left {19 width: 60%; /* */20 transition: left 1s ease-in-out;21 left: -100%;22}23.section5.is-animated .item.left,24.section4.is-animated .item.left {25 left: 0%;26 transition: left 2s ease-in-out;27}28
29.section5 .item.right,30.section4 .item.right {31 width: 40%;32 right: 0;33}34.section5 .mockup.pc .mask,35.section4 .mockup.pc .mask {36 z-index: 8;37 width: 32.3vw;38 height: 61.8%;39 overflow: hidden;40 left: 5.2%;41 top: 6%;42}.section4 선택자에 .section5를 추가
1const win = $(window);2const sections = $('section');3let speed = Math.floor(win.height() * 0.5);4let topArr = [];5let winSCT;6console.log(speed);7//sections.offsetTop8sections.each(function (i, o) {9 const sectionTop = $(o).offset().top;10 topArr.push(sectionTop);11});12win.on('scroll', () => {13 winSCT = win.scrollTop();14 if (winSCT > topArr[0] && winSCT < topArr[1]) {15 sections.eq(0).addClass('is-animated').siblings().removeClass('is-animated');16 }17 if (winSCT > topArr[1] - speed && winSCT < topArr[2]) {18 sections.eq(1).addClass('is-animated').siblings().removeClass('is-animated');19 }20 if (winSCT > topArr[2] - speed && winSCT < topArr[3]) {21 sections.eq(2).addClass('is-animated').siblings().removeClass('is-animated');22 }23 if (winSCT > topArr[3] - speed && winSCT < topArr[4]) {24 sections.eq(3).addClass('is-animated').siblings().removeClass('is-animated');25 pipScroll();26 console.log(topArr[4], winSCT);27 }28 if (winSCT > topArr[4] - speed) {29 sections.eq(4).addClass('is-animated').siblings().removeClass('is-animated');30 pipScroll();31 }32});33
34function pipScroll() {35 const devices = ['.mockup.pc', '.mockup.mobile'];36 $.each(devices, function (i, deviceEl) {37 const device = $(deviceEl);38 const screen = device.find('.screen');39 const mask = device.find('.mask');40 const hightDifference = screen.innerHeight() - mask.innerHeight();41 console.log('hightDifference', hightDifference);42 device.on({43 mouseenter: function () {44 screen.stop().animate({ top: -hightDifference }, 1000);45 },46 mouseleave: function () {47 screen.stop().animate({ top: 0 }, 1000);48 },49 });50 });51}52win.on('resize', function () {53 pipScroll();54});위의 코드는 화면에는 보이지 않지만 버그가 있다.
['.mockup.pc', '.mockup.mobile']에는 단일 요소가 할당 되었으므로두개의 mockup.pc 가 추가 될 경우 각각 동작하지 않는다. 이것을 제이쿼리 객체로 변경하면 여러개 요소를 알아서 반복 처리 하기 때문에 각각 다른 이벤트를 처리할수 있다. 아래처럼 수정하자
1 function pipScroll() {2 //const devices = ['.mockup.pc', '.mockup.mobile'];3 const devices = $('.mockup.pc, .mockup.mobile');4 //$.each(devices, function (i, deviceEl) {5 devices.each(function (i, deviceEl) {6 let device = $(this);7 let screen = device.find('.mask>img');1.5. 가로 스크롤 효과
- 예제
- HTML
- CSS
- JS
- JQ
1<!DOCTYPE html>2<html lang="en">3 <head>4 <meta charset="UTF-8" />5 <meta http-equiv="X-UA-Compatible" content="IE=edge" />6 <meta name="viewport" content="width=device-width, initial-scale=1.0" />7 <title></title>8 </head>9
10 <body>11 <main class="container">12 <section id="section1" class="item">13 <h2 class="num">01</h2>14 </section>15 <section id="section2" class="item">16 <h2 class="num">02</h2>17 </section>18 <section id="section3" class="item">19 <h2 class="num">03</h2>20 </section>21 <section id="section4" class="item">22 <h2 class="num">04</h2>23 </section>24 <section id="section5" class="item">25 <h2 class="num">05</h2>26 </section>27 <section id="section6" class="item">28 <h2 class="num">06</h2>29 </section>30 <section id="section7" class="item">31 <h2 class="num">07</h2>32 </section>33 <section id="section8" class="item">34 <h2 class="num">08</h2>35 </section>36 <section id="section9" class="item">37 <h2 class="num">09</h2>38 </section>39 </main>40 </body>41</html>1.container {2 position: fixed;3 left: 0;4 top: 0;5 display: flex;6}7.item {8 width: 100vw;9 height: 100vh;10 position: relative;11}12#section1 {13 background-color: #111;14}15#section2 {16 background-color: #222;17}18#section3 {19 background-color: #333;20}21#section4 {22 background-color: #444;23}24#section5 {25 background-color: #555;26}27#section6 {28 background-color: #666;29}30#section7 {31 background-color: #777;32}33#section8 {34 background-color: #888;35}36#section9 {37 background-color: #999;38}39
40.num {41 position: absolute;42 bottom: 20px;43 right: 20px;44 color: #fff;45 font-size: 20vw;46 z-index: 10000;47}자바스크립트는 gsap 을 사용
<script src="https://unpkg.com/gsap@3/dist/gsap.min.js"></script>
1const cont = document.querySelector('.container');2const item = document.querySelector('.item');3
4function scroll() {5 let scrollTop = window.scrollY;6 let offsetLeft = cont.offsetWidth;7 document.body.style.height = offsetLeft + 'px';8
9 let viewWidth = offsetLeft - window.innerWidth;10 let viewHeight = offsetLeft - window.innerHeight;11 let goLeft = scrollTop * (viewWidth / viewHeight);12
13 gsap.to(cont, { left: -goLeft });14
15 requestAnimationFrame(scroll);16}17scroll();18
19window.addEventListener('resize', scroll);1const cont = $('.container');2function scroll() {3 let scrollTop = $(window).scrollTop();4 let offsetLeft = cont.outerWidth();5 $('body').css('height', offsetLeft + 'px');6
7 let viewWidth = offsetLeft - $(window).innerWidth();8 let viewHeight = offsetLeft - $(window).innerHeight();9 let goLeft = scrollTop * (viewWidth / viewHeight);10
11 cont.css('left', -goLeft);12 console.log(scrollTop, offsetLeft, viewWidth, goLeft, viewHeight);13}14scroll();15$(window).on({16 resize: function () {17 scroll();18 },19 scroll: function () {20 scroll();21 },22});