デモ
デモを読み込めませんでした。
仕様
- 用途
- ヒーロー
- 動作
- DOM
- 種類
- レシピ
- 使うもの
- SplitText
- 動作確認
- three 0.186.0・gsap 3.15.0
- 公開日
しくみと調整
何もないところから、ぼやけた文字が右から少し寄りながら現れ、順にピントが合って見出しになります。SplitText で文字を1字ずつに分け、それぞれにかけたぼかしと透明度、右へのずれを、元の状態へ戻しているだけです。再生は見出しが画面に入ったときの1回だけにしました。
出始めのぼかしは14px、右へのずれは12pxで、隣の文字へ0.035秒ずつずらして始まります。イージングは power2.out なので、終わりにかけて速さがゆるみ、目で文字を追う速さに近い出方になります。ずらす間隔を広げると、文字が追いかけっこをするように現れます。暗い背景に明るい文字を置くと、ピントが合っていく変化がいちばんよくわかります。
文字を分けるのは、書体の読み込みが終わってからです。仮の書体の幅で分けると、本来の書体に替わったところで文字の位置がずれます。もうひとつ、filter は要素ごとに描き直しが起きるので、文字単位のまま長い文章に使うと重くなります。本文のような長さなら、語や行ごとに分けたほうが軽く済みます。
コード
<section class="blur-reveal">
<h2 class="blur-reveal__heading">見えなかったものが、<br>見えてくる。</h2>
</section>
.blur-reveal {
--bg: #111111;
--ink: #eeece6;
display: grid;
place-items: center;
min-height: 100vh;
padding: 96px 6vw;
background: var(--bg);
color: var(--ink);
text-align: center;
}
.blur-reveal__heading {
margin: 0;
font-size: clamp(26px, 7vw, 88px);
font-weight: 800;
line-height: 1.05;
letter-spacing: -0.02em;
}
import { gsap } from "gsap";
import { SplitText } from "gsap/SplitText";
gsap.registerPlugin(SplitText);
const BLUR = 14; // 最初のぼかし(px)
const SHIFT = 12; // 最初の横ずれ(px)
const reduceMotion = window.matchMedia("(prefers-reduced-motion: reduce)").matches;
// フォントが読み込まれてから分ける。フォールバックの書体の幅で分けると、書体が差し替わったところで字の位置がずれる
document.fonts.ready.then(() => {
document.querySelectorAll(".blur-reveal__heading").forEach((heading) => {
// 長い文は words 単位にすると要素数が抑えられる。ここでは 1 字ずつ現したいので chars も足す
const split = SplitText.create(heading, { type: "words,chars" });
const reveal = gsap.timeline({ paused: true }).from(split.chars, {
opacity: 0,
filter: `blur(${BLUR}px)`,
x: SHIFT,
duration: 1.1,
ease: "power2.out",
stagger: 0.035,
});
// 動きを減らす設定では、表示し終えた状態のまま置く
if (reduceMotion) {
reveal.progress(1);
return;
}
// 画面に入ったら 1 回だけ再生する。見るたびに繰り返すと、読み返す人の邪魔になる
const io = new IntersectionObserver(
(entries, self) => {
for (const entry of entries) {
if (!entry.isIntersecting) continue;
self.unobserve(entry.target);
reveal.play();
}
},
{ rootMargin: "0px 0px -15%" }
);
io.observe(heading);
});
});