ロンドンをベースに、ウェブデザイナーやウェブディベロッパーとして活動する、Jason Harvey氏のポートフォリオサイトがとてもクールなので紹介します。
ポートフォリオサイトのsomefolkがイケているのはもちろんですが、サイトに掲載されているいくつかの制作実績に目を通すと、全ての実績に共通してアニメーション及びイージングのセンスの良さが見受けられます。
ここにJason Harvey氏へのリスペクトを示しつつ、彼のポートフォリオサイトで用いられている表現技法を学びましょう。
用件定義
PC版
◼︎ 惰性スクロール
◼︎ メインビジュアルのパララックス表示
◼︎ 要素の途中固定
◼︎ 固定した要素内の画像のスケールを変更
Mobile版
◼︎ 惰性スクロール
◼︎ メインビジュアルのパララックス表示
◼︎ 要素の途中固定
◼︎ 固定した要素内の画像のスケールを変更
結論
サンプルページはこちら
1.惰性スクロールには lenis を使います。
2.パララックス、要素の固定表示、画像のスケール変更には gsap と scroll trigger を使います。
3.それぞれのライブラリー専用のcssは不用です。
コード
index.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>Document</title>
<meta name="description" content="Description" />
<link rel="stylesheet" href="https://portfolio-technics.com/sample/0-css/style.css" />
<!-- style -->
<style>
.mv {
height: 50vh;
margin-bottom: 4rem;
overflow: hidden;
position: relative;
width: 100%;
}
.mv img {
-webkit-transform: translateX(-50%) scale(1.2);
-webkit-transform-origin: 50% 50%;
display: block;
height: auto;
left: 50%;
position: absolute;
top: 0;
transform: translateX(-50%) scale(1.2);
transform-origin: 50% 50%;
width: 100%;
}
.fix {
margin: 0 auto;
max-width: 96rem;
}
.fix__list {
display: flex;
flex-direction: column;
margin-bottom: 4rem;
row-gap: 4rem;
}
.fix__item {
aspect-ratio: 1080/608;
overflow: hidden;
}
.fix__item img {
display: block;
height: auto;
width: 100%;
}
.fix__unfix {
align-items: center;
background-color: black;
color: #fff;
display: flex;
height: 300rem;
justify-content: center;
margin-bottom: 4rem;
position: relative;
}
.footer {
align-items: center;
background-color: black;
color: #fff;
display: flex;
height: 400rem;
justify-content: center;
}
</style>
<!-- style -->
</head>
<body>
<div id="js-page" class="page">
<main class="main page__main">
<div class="mv js-parallax">
<img loading="lazy" src="https://dl.dropbox.com/s/4hrgv4272zvctj6/sample20.jpg?dl=0" alt="" />
</div>
<div class="fix">
<ul class="fix__list">
<li class="fix__item js-fix --1">
<img loading="lazy" src="https://dl.dropbox.com/s/oq7so2vc6j7wgk6/sample1.jpg?dl=0" alt="" />
</li>
<li class="fix__item js-fix --2">
<img loading="lazy" src="https://dl.dropbox.com/s/03sa966wy301vt1/sample2.jpg?dl=0" alt="" />
</li>
<li class="fix__item js-fix --3">
<img loading="lazy" src="https://dl.dropbox.com/s/aa7uk8vh5hhermx/sample3.jpg?dl=0" alt="" />
</li>
<li class="fix__item js-fix --4">
<img loading="lazy" src="https://dl.dropbox.com/s/9h64k0vppliw3m9/sample4.jpg?dl=0" alt="" />
</li>
</ul>
<div class="fix__unfix js-unfix">
<p>
The upper items will be unfixed when this content reaches the top of the screen.
</p>
</div>
</div>
</main>
<footer class="footer page__footer">
<p>
footer
</p>
</footer>
</div>
<!-- CDN -->
<script src="https://cdn.jsdelivr.net/gh/studio-freight/lenis@1.0.25/bundled/lenis.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/gsap/3.12.2/gsap.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/gsap/3.12.2/ScrollTrigger.min.js"></script>
<!-- js -->
<script>
class MomentumLenis {
constructor() {
this._lenisInit();
}
_lenisInit() {
gsap.registerPlugin(ScrollTrigger);
const lenis = new Lenis({
lerp: 0.1,
});
lenis.on("scroll", ScrollTrigger.update);
gsap.ticker.add((time) => {
lenis.raf(time * 1000);
});
// Images parallax
gsap.utils.toArray(".js-parallax").forEach((parallaxBoxes) => {
const parallaxImages = parallaxBoxes.querySelector("img");
const tl = gsap.timeline({
scrollTrigger: {
trigger: parallaxBoxes,
scrub: true,
pin: false,
// markers: true,
},
});
tl.fromTo(
parallaxImages,
{
yPercent: -20,
ease: "none",
},
{
yPercent: 20,
ease: "none",
}
);
});
// Fix the li elements and gradually scale the images on scroll
gsap.utils.toArray(".js-fix").forEach((fixItem) => {
const image = fixItem.querySelector("img");
const tl = gsap.timeline({
scrollTrigger: {
trigger: fixItem,
start: "top 10px",
endTrigger: ".js-unfix",
end: "top 10px", // Change the end condition to keep the item pinned until it reaches the top
scrub: 1,
pin: true,
pinSpacing: false,
// markers: true,
},
});
tl.to(fixItem, { y: 0, ease: "none" }).to(
image,
{
scale: 1.2,
ease: "none",
},
0
);
});
}
}
new MomentumLenis();
</script>
<!-- js -->
</body>
</html>
実践
❶lenisで惰性スクロールを実装する。
lenisの使い方|公式サイトに基づく
基本のコード
main.js
// smooth scroll setting
const lenis = new Lenis({
lerp: 0.2, // Linear interpolation (lerp) intensity (between 0 and 1)
duration: 1, // The duration of scroll animation (in seconds). Useless if lerp defined
});
function raf(time) {
lenis.raf(time);
requestAnimationFrame(raf);
}
requestAnimationFrame(raf);
❷1にgsap + scroll triggerで画像のparallax効果と拡大アニメーションを実装する。
lenisにgsap & scroll triggerを組み合わせる|公式サイトに基づく
main.js
gsap.registerPlugin(ScrollTrigger);
const lenis = new Lenis({
lerp: 0.1,
});
lenis.on("scroll", ScrollTrigger.update);
gsap.ticker.add((time) => {
lenis.raf(time * 1000);
});
// Images parallax
gsap.utils.toArray(".js-parallax").forEach((parallaxBoxes) => {
const parallaxImages = parallaxBoxes.querySelector("img");
const tl = gsap.timeline({
scrollTrigger: {
trigger: parallaxBoxes,
scrub: true,
pin: false,
// markers: true,
},
});
tl.fromTo(
parallaxImages,
{
yPercent: -20,
ease: "none",
},
{
yPercent: 20,
ease: "none",
}
);
});
❸2にスクロール途中で固定、スケールを変更。
main.js
const lenis = new Lenis({
lerp: 0.1,
});
gsap.registerPlugin(ScrollTrigger);
gsap.ticker.add((time) => {
lenis.raf(time * 1000);
});
lenis.on("scroll", ScrollTrigger.update);
// Images parallax
gsap.utils.toArray(".js-parallax").forEach((parallaxBoxes) => {
const parallaxImages = parallaxBoxes.querySelector("img");
const tl = gsap.timeline({
scrollTrigger: {
trigger: parallaxBoxes,
scrub: true,
pin: false,
// markers: true,
},
});
tl.fromTo(
parallaxImages,
{
yPercent: -20,
ease: "none",
},
{
yPercent: 20,
ease: "none",
}
);
});
// Fix the li elements and gradually scale the images on scroll
gsap.utils.toArray(".js-fix").forEach((fixItem) => {
const image = fixItem.querySelector("img");
const tl = gsap.timeline({
scrollTrigger: {
trigger: fixItem,
start: "top 10px",
endTrigger: ".js-unfix",
end: "top 10px", // Change the end condition to keep the item pinned until it reaches the top
scrub: 1,
pin: true,
pinSpacing: false,
// markers: true,
},
});
tl.to(fixItem, { y: 0, ease: "none" }).to(
image,
{
scale: 1.2,
ease: "none",
},
0
);
});