#
14.스크롤 효과
#
1. 스크롤 효과를 만들어보자
#
1.1. 1단계-섹션1
#
1.1.1. 좌우이동 - css 메서드 활용
이 코드는 잠재적인 버그가 있다. 작성후 어떤 버그가 발생할수 있을지 예상해보자
html
<!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>
css
* {
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;
}
애니메이트 효과 css
.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%);
}
jQuery
$(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%)');
}
});
});
첫번째 섹션에 도달시 애니메이트가 실행된다. 반복문 내에서 순회하는 this 의 거리를 비교하고 있으므로 조건 추가시 다음 섹션의 거리를 비교하여 애니메이트가 실행된다.
#
1.2. 2단계-섹션2
#
1.2.1. 상하이동 시간차-animate() 메서드 활용
html
<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>
section2의 구조를 수정한다.
css
.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;
}
css를 추가한다.
jQuery
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');
}
});
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
<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>
css
.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);
}
clip-path: inset(0 100% 0 0);
inset() 함수를 사용하여 요소의 보이는 부분을 설정. inset() 함수는 사각형 형태의 클리핑 영역을 만들며, 인수는 각각 상단, 오른쪽, 하단, 왼쪽 변에서 얼마나 안쪽으로 들어가는지를 나타냄. 결과적으로 상단에서는 내부로 들어가지 않고(0), 오른쪽에서는 요소 너비 전체만큼 내부로 들어간다(100%).- 참조링크
- 참조링크
.section3
에.is-animated
클래스가 추가되면 clip-path의 좌표를 변경한다.
javascript
...생략
if (winSCT > topArr[2] && winSCT < topArr[3]-speed) {
sections.eq(2).addClass('is-animated');
}
조건문을 추가한다.
#
1.4. 4단계-섹션4
#
1.4.1. PIP스크롤
화면안의 화면이 스크롤 되는 효과를 만들어보자
아래의 이미지 파일을 다운로드 한다
html
<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>
css
.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%;
}
javascript
$(() => {
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
창크기 변경시 스크롤값을 재계산하는 함수 추가 function pipScroll() 내에 작성한다.
// 윈도우 크기가 변경될 때 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);
});
});
}
})//jQuery
#
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.offsetTop
sections.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. 가로 스크롤 효과
html
<!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>
css
.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>
javascript
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);
jQuery
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();
},
});