デモ
デモを読み込めませんでした。
仕様
- 用途
- セクション
- 動作
- DOM
- 種類
- レシピ
- 使うもの
- ScrollTrigger・SplitText
- 動作確認
- three 0.186.0・gsap 3.15.0
- 公開日
しくみと調整
SplitText で文章を行に分け、行ごとに、塗る色のグラデーションを背景に敷いて文字の形で切り抜きます(background-clip の text)。各行の幅は文字に合わせておきます。背景の幅を 0% から広げていくと、文字が左から塗られていきます。
背景の幅を 0% から 100% へ、イーズ無しで伸ばします。行ごとに 1 つずつ順に始めると、上の行から順に塗られます。結びつけるのは ScrollTrigger で、文章の上端が画面の高さの 80% に来たら始まり、下端が 45% に来たら塗り終わる範囲を、スクロール量にそのまま連動させます(scrub)。1 文字ずつ色を変える方式と違い、行の途中まで塗られた状態を出せます。
background の一括指定は切り抜きの指定を初期値に戻すので、切り抜きは一括指定の後に書きます。行の幅は字形で決まるので、行に分けるのは、書体の読み込みが終わってからにします。
和文は空白で語に分かれないため、そのままでは全体がひとかたまりの行になります。文節の切れ目に <br> を入れて、行を決めてください。改行の文字や、改行を活かす white-space の指定では、行がまとまってしまいます。
コード
<section class="fill-section">
<p class="fill-eyebrow">Our approach</p>
<!--
和文は空白で語に分かれないので、文節の切れ目に <br> を入れて行を決める。
SplitText は <br> で行を分ける(英文なら <br> は要らず、幅で折り返した行に分かれる)。
-->
<p class="fill-text">気づくより先に、心で感じる。<br>
そんな動きをデザインしています。<br>
静かに、正確に、そしていつも<br>
伝えたい物語のために。</p>
</section>
.fill-section {
--dim: #343433; /* 塗る前の文字の色(地に文字の色を少し混ぜた暗い灰色) */
--fill: #eeece6; /* 塗った後の文字の色 */
padding: 40vh 6vw 22vh;
background-color: #111111;
}
.fill-eyebrow {
margin: 0 0 24px;
font-size: 13px;
font-weight: 700;
line-height: 1;
letter-spacing: 0.14em;
text-transform: uppercase;
color: #a8a6a0;
}
/* JS が動かないときは、ふつうの読める文字のまま(切り抜きは分割した後にだけ効かせる) */
.fill-text {
margin: 0;
font-size: clamp(28px, 4.4vw, 56px);
font-weight: 700;
line-height: 1.5;
letter-spacing: 0.04em;
color: var(--fill);
}
/* SplitText が作る行。塗る範囲を文字の幅にそろえ、背景を文字の形で切り抜く */
.fill-text.is-split .line {
width: fit-content;
color: transparent;
background: linear-gradient(var(--fill), var(--fill)) 0 0 / 0% 100% no-repeat, var(--dim);
-webkit-background-clip: text;
/* 一括指定(background)は切り抜きを初期値に戻すので、必ずその後に書く */
background-clip: text;
}
import { gsap } from "gsap";
import { ScrollTrigger } from "gsap/ScrollTrigger";
import { SplitText } from "gsap/SplitText";
gsap.registerPlugin(ScrollTrigger, SplitText);
const reduceMotion = window.matchMedia("(prefers-reduced-motion: reduce)").matches;
// 行の幅は字形で決まるので、書体の読み込みが終わってから行に分ける
document.fonts.ready.then(() => {
document.querySelectorAll(".fill-text").forEach((text) => {
const split = SplitText.create(text, { type: "lines", linesClass: "line" });
text.classList.add("is-split"); // ここで初めて、文字の形で切り抜く見た目にする
// 動きを減らす設定では、塗り終わった状態で止めておく
if (reduceMotion) {
gsap.set(split.lines, { backgroundSize: "100% 100%" });
return;
}
gsap.fromTo(
split.lines,
{ backgroundSize: "0% 100%" },
{
backgroundSize: "100% 100%",
ease: "none",
duration: 1,
stagger: 1, // 1 行ずつ順に塗る
scrollTrigger: {
trigger: text,
start: "top 80%", // 文章の上端が画面の高さの 80% に来たら塗り始める
end: "bottom 45%", // 下端が 45% に来たら塗り終わる
scrub: true, // スクロール量にそのまま連動させる(数値の scrub は実時間で追うので使わない)
},
}
);
});
});