デモ
デモを読み込めませんでした。
仕様
- 用途
- セクション
- 動作
- DOM
- 種類
- レシピ
- 動作確認
- three 0.186.0・gsap 3.15.0
- 公開日
しくみと調整
強調したい語を <mark> で囲み、背景に 1 色のグラデーションを敷いて帯にします。帯の幅を 0% から 100% へ伸ばすと、左から蛍光ペンで引いたように見えます。語が行をまたいでも、背景は行の断片をつないだ 1 本として扱われるので、1 行目の終わりから 2 行目の頭へ続けて引かれます。
1 語を引く時間は 0.45 秒に、語の幅 1em ごとに 0.04 秒を足したもので、次の語は 0.55 秒後に引き始めます。長い語ほど少し長く引くので、ペンの速さがそろって見えます。字数で決めると、同じ幅でも和文は字数が欧文の半分ほどしかないので、和文だけ速く引かれて見えます。ease は power2.inOut で、引き始めと引き終わりにペンの重さを出します。
帯の縦の位置を、<mark> の箱に対する割合(下から 1 割など)で決めると、和文では帯が字の下へずれます。箱の高さは先頭に並べた欧文の書体で決まり、その下のほうは g や p のはみ出しのための余白です。和文の字はそこまで下りてこないので、箱の下寄りに置いた帯は字から外れて見えます。書体を替えると、またずれます。
そこで、語の先頭に高さ 0 の inline-block を差し込み、ベースラインの位置を測ります。中身の無い inline-block は、下端をベースラインにそろえて置かれるからです。帯の下端をベースラインの 0.14em 下に置くと、和文の字の下端の少し下から、字の高さの半分ほどを覆います。測った値を字の大きさとの比にして CSS 変数に入れておけば、画面の幅で字の大きさが変わっても帯はずれません。
帯の色・太さ・下げる量は、CSS の変数で変えられます。引くのは段落が画面に入った 1 回だけで、動きを減らす設定では、引き終えた状態のまま見せます。記事の本文や LP のリード文で、読んでほしい語を強調するのに向いています。
コード
<section class="statement">
<p class="statement__eyebrow">Approach</p>
<!-- 引きたい語を <mark class="marker"> で囲む。行は文節の切れ目に <br> を入れて決める -->
<p class="statement__text">私たちは、<mark class="marker">使いやすさ</mark>をいちばんに考えて、<br>
Webサイトをつくっています。<br>
<mark class="marker">はじめての人でも迷わない</mark>こと、<br>
そして<mark class="marker">長く使い続けられる</mark>こと。</p>
</section>
.statement {
--ink: #262626;
--muted: #6b6b6b;
--marker: #c8bf83; /* マーカーの色 */
--band: 0.5em; /* マーカーの太さ */
--drop: 0.14em; /* 帯の下端を、ベースラインからどれだけ下げるか */
padding: 120px 6vw;
background-color: #f2f1ed;
color: var(--ink);
}
.statement__eyebrow {
margin: 0 0 32px;
font-size: 13px;
font-weight: 700;
letter-spacing: 0.14em;
text-transform: uppercase;
color: var(--muted);
}
.statement__text {
margin: 0;
font-size: clamp(22px, 2.8vw, 36px);
line-height: 1.8;
letter-spacing: 0.04em;
word-break: auto-phrase; /* 狭い画面で折り返すとき、文節の切れ目で折る(対応するブラウザのみ) */
}
/* 帯は背景の単色のグラデーション。引いた長さ(--draw)を、スクリプトが 0% から 100% へ伸ばす。
縦の位置は、スクリプトが測ったベースライン(--baseline。箱の上端からの距離)から決める */
.marker {
--draw: 100%;
padding: 0 0.08em; /* 字組みを変えずに、帯だけ左右へ少しはみ出させる */
margin: 0 -0.08em;
background: linear-gradient(var(--marker), var(--marker)) no-repeat;
background-size: var(--draw) var(--band);
background-position: 0 calc(var(--baseline, 0.95em) + var(--drop) - var(--band));
color: inherit;
}
import { gsap } from "gsap";
const reduceMotion = window.matchMedia("(prefers-reduced-motion: reduce)").matches;
// <mark> の箱の上端からベースラインまでを、字の大きさに対する比で返す。
// 高さ 0 の inline-block は、下端をベースラインにそろえて置かれる
function baselineOf(mark) {
const probe = document.createElement("span");
probe.style.cssText = "display:inline-block;width:0;height:0;";
mark.prepend(probe);
const y = probe.getBoundingClientRect().bottom - mark.getClientRects()[0].top;
probe.remove();
return y / parseFloat(getComputedStyle(mark).fontSize);
}
// ベースラインの位置と語の幅は書体で決まるので、読み込みが終わってから測る
document.fonts.ready.then(() => {
document.querySelectorAll(".statement__text").forEach((text) => {
const marks = [...text.querySelectorAll(".marker")];
// 箱に対する %(下から 10% など)で帯を置くと、和文では字の下へずれるので、測ったベースラインから置く
for (const mark of marks) mark.style.setProperty("--baseline", `${baselineOf(mark).toFixed(3)}em`);
// 動きを減らす設定では、引き終えた状態のまま見せる
if (reduceMotion) return;
const size = parseFloat(getComputedStyle(text).fontSize);
const draw = gsap.timeline({ paused: true });
marks.forEach((mark, i) => {
// 長い語ほど少しだけ長く引く(幅 1em ごとに 0.04 秒。和文でも欧文でも、同じ幅なら同じ速さ)
const width = [...mark.getClientRects()].reduce((sum, rect) => sum + rect.width, 0);
draw.fromTo(
mark,
{ "--draw": "0%" },
{ "--draw": "100%", duration: 0.45 + (width / size) * 0.04, ease: "power2.inOut" },
i * 0.55 // 前の語を引き終える少し前に、次の語を引き始める
);
});
// 段落が画面の下から 4 分の 1 の所まで入ったら、1 回だけ引く
const io = new IntersectionObserver(
([entry]) => {
if (!entry.isIntersecting) return;
io.disconnect();
draw.play();
},
{ rootMargin: "0px 0px -25%" }
);
io.observe(text);
});
});