デモ
デモを読み込めませんでした。
仕様
- 用途
- ヒーロー
- 動作
- DOM
- 種類
- レシピ
- 使うもの
- SplitText
- 動作確認
- three 0.186.0・gsap 3.15.0
- 公開日
しくみと調整
見出しが画面に入ってくると、字が一斉に下から跳ね上がり、そろって静かに止まります。見出しが半分ほど出そろったころ、少し遅れて下の説明文が浮かび上がります。字ごとにマスクを付け、その下からせり上げているので、上がってくる途中の字が行の外へはみ出して見えることはありません。
入りは0.9秒の expo.out で、文字ごとに0.04秒ずつずらしました。出だしが速く、終わりにかけて長く減速するカーブなので、動き始めは一斉に飛び出し、止まるときはそろって見えます。ずらす間隔は0.08秒までが目安で、それより広げると読む速さに動きが追いつかず、見出しが波打って見えます。
分けるのは fonts.ready を待ってから。待たずに分けると、仮の書体の字幅でマスクが作られ、本来の書体に替わったところで字とマスクがずれます。欧文を混ぜるなら、行の高さは1.1より詰めないでください。g や y のように下へ伸びる部分が、せり上がる途中でマスクに切られ、別の字のように読めてしまいます。
再生は、見出しが画面に入った最初の1回だけにしています。見えるたびに繰り返すと、スクロールを戻すたびに見出しが消えて出直し、読み返している人の邪魔になります。デモでは出そろったあとに字を上へ抜けさせていますが、これは0.6秒の expo.in で入りより短くした、デモだけの動きです。公開用のコードには入れていません。
ヒーローや節の見出しのような、短くて大きな字で使う形です。和文の長い見出しなら、文字ごとではなく行ごとに分けたほうが、字がばらけずに読めます。
コード
<section class="rise">
<h2 class="rise__heading">動きで伝える</h2>
<p class="rise__lead">気づくより<wbr>先に、<wbr>心で感じる。<wbr>そんな動きを<wbr>デザインしています。</p>
</section>
.rise {
--heading: clamp(40px, 9vw, 112px); /* 見出しの大きさ */
--ink: #262626; /* 文字の色 */
--ground: #f2f1ed; /* 背景色 */
display: grid;
align-content: center;
justify-items: center;
gap: 24px;
min-height: 100vh;
padding: 96px 6vw;
background: var(--ground);
color: var(--ink);
text-align: center;
}
/* 行の高さは 1.1 まで。これより詰めると、せり上がる途中で g の下がマスクで切れる */
.rise__heading {
margin: 0;
font-size: var(--heading);
font-weight: 800;
line-height: 1.1;
letter-spacing: -0.02em;
}
.rise__lead {
max-width: 34em;
margin: 0;
font-size: 18px;
line-height: 1.8;
letter-spacing: 0.04em;
word-break: keep-all; /* 和文は <wbr> を書いた文節の切れ目でだけ折り返す */
}
import { gsap } from "gsap";
import { SplitText } from "gsap/SplitText";
gsap.registerPlugin(SplitText);
const reduceMotion = window.matchMedia("(prefers-reduced-motion: reduce)").matches;
// 字形が決まってから分ける。仮の書体の幅でマスクを作ると、書体が差し替わったときに字がずれる
document.fonts.ready.then(() => {
document.querySelectorAll(".rise").forEach((section) => {
const heading = section.querySelector(".rise__heading");
const lead = section.querySelector(".rise__lead");
// mask: "chars" で、文字ごとにはみ出しを隠すマスク要素が付く(自分で overflow を切らなくてよい)
const split = SplitText.create(heading, { type: "chars", mask: "chars" });
const rise = gsap
.timeline({ paused: true })
.from(split.chars, { yPercent: 110, duration: 0.9, ease: "expo.out", stagger: 0.04 })
.from(lead, { autoAlpha: 0, y: 16, duration: 0.6, ease: "power2.out" }, 0.3);
// 動きを減らす設定では、せり上がりきった状態で見せる
if (reduceMotion) {
rise.progress(1);
return;
}
// 画面に入ったら 1 回だけ再生する。見るたびに繰り返すと、読み返す人の邪魔になる
const io = new IntersectionObserver(
(entries, self) => {
for (const entry of entries) {
if (!entry.isIntersecting) continue;
self.unobserve(entry.target);
rise.play();
}
},
{ rootMargin: "0px 0px -15%" } // 下から 15% 入ったら始める
);
io.observe(section);
});
});