デモ
デモを読み込めませんでした。
仕様
- 用途
- ヒーロー
- 動作
- DOM
- 種類
- レシピ
- 使うもの
- DrawSVGPlugin
- 動作確認
- three 0.186.0・gsap 3.15.0
- 公開日
しくみと調整
手描きの筆記体を、書き順どおりに並べた SVG の path として持ちます。DrawSVGPlugin で 0% から 100% へ線を伸ばすと、ペンで書いているように見えます。画面に入ったら 1 回だけ描きます。
描く時間は線の長さをペンの速さ(1 秒に 1150 進む想定)で割って決めるので、線を差し替えても時間を直さずに済みます。短い線でも 0.2 秒は確保し、線と線のあいだは 0.12 秒ほど間をあけます。
描き始め(0% 0%)は長さ 0 のダッシュが始点の外へずれるため、丸い線端でも点が出ません。逆に縮めて消す描き方だと終点に点が残るので、読ませたあとは線ごとフェードして消すようにします。
書き終えたあとに差し色の下線ときらめきを重ねると、書き終えたことが伝わりやすくなります。動きを減らす設定では、描き終えた状態のまま止めて見せます。見出しのアクセントや、手紙風の演出に向いています。
コード
<section class="handwrite" aria-labelledby="handwrite-title">
<!-- data-ease: 描くイージング。data-gap: 次の線までの間隔(秒。負なら重ねて描く) -->
<svg class="handwrite__ink" viewBox="9 20 660 280" role="img" aria-label="Hello">
<path class="handwrite__stroke" data-ease="sine.inOut" data-gap="0.12"
d="M157.4 44 C137.9 110 115 186 98 234 C91 254 77 262 67.5 252"></path>
<path class="handwrite__stroke" data-ease="sine.inOut" data-gap="0.12"
d="M80.4 160 C120.9 150 177.4 140 223.9 138 C250.4 136 265.9 90 263.4 60 C262.4 40 242.9 38 235.9 58
C220.9 110 196.5 200 191.5 236 C190.5 256 212.5 256 235 238 C257 222 291 206 307.5 188 C321.4 172 314.4 160 297.9 162
C277.4 164 259 190 255 214 C249.5 244 275 254 309.5 236 C338 218 376.9 150 397.4 100 C412.4 64 410.4 40 395.9 42
C381.4 44 368.4 80 358.4 120 C345.9 170 330 226 337.5 244 C342.5 256 363.5 252 384 234 C412.5 216 450.9 150 471.4 100
C486.4 64 484.4 40 469.9 42 C455.4 44 442.4 80 432.4 120 C419.9 170 404 226 411.5 244 C416.5 256 437.5 252 456 234
C469.2 221 498.5 176 522.9 166 C507.9 170 491 190 485.5 212 C479 238 491.5 252 508 250 C526.5 248 541.5 228 545.5 204
C548.5 184 540.9 166 524.9 166 C514.9 166 512 178 526 178 C548 178 580.4 168 614.9 150"></path>
<path class="handwrite__stroke handwrite__stroke--accent" data-ease="power2.inOut" data-gap="-0.1"
d="M55.5 284 C174.5 272 376 266 515.5 268 C555.3 269 589 262 614.5 248"></path>
<path class="handwrite__stroke handwrite__stroke--accent" data-ease="power2.out" data-gap="-0.13" d="M622 108 L616 84"></path>
<path class="handwrite__stroke handwrite__stroke--accent" data-ease="power2.out" data-gap="-0.13" d="M632 116 L650 98"></path>
<path class="handwrite__stroke handwrite__stroke--accent" data-ease="power2.out" data-gap="-0.13" d="M638 130 L662 128"></path>
</svg>
<h2 class="handwrite__title" id="handwrite-title">静かで、<wbr>使いやすいWebサイトを。</h2>
</section>
.handwrite {
--bg: #f2f1ed;
--ink: #262626;
--accent: #b27771; /* 下線ときらめきの色(くすみローズ) */
display: grid;
place-items: center;
align-content: center; /* これが無いと、2 段の行が高さいっぱいに伸びて間に大きな隙間ができる */
gap: 24px;
min-height: 100vh;
padding: 96px 6vw;
background: var(--bg);
color: var(--ink);
text-align: center;
}
.handwrite__title {
margin: 0;
font-size: clamp(24px, 4vw, 32px);
font-weight: 700;
line-height: 1.4;
letter-spacing: 0.02em;
word-break: keep-all; /* 狭い画面では <wbr> の所(文節の切れ目)で折り返す */
}
.handwrite__ink {
width: min(560px, 100%);
height: auto;
overflow: visible;
}
.handwrite__stroke {
fill: none;
stroke: var(--ink);
stroke-width: 9;
stroke-linecap: round;
stroke-linejoin: round;
}
.handwrite__stroke--accent {
stroke: var(--accent);
}
import { gsap } from "gsap";
import { DrawSVGPlugin } from "gsap/DrawSVGPlugin";
gsap.registerPlugin(DrawSVGPlugin);
const SPEED = 1150; // ペンの速さ(1 秒に描く長さ。viewBox の単位)
const MIN = 0.2; // 短い線でも、これより短い時間では描かない
const reduceMotion = window.matchMedia("(prefers-reduced-motion: reduce)").matches;
document.querySelectorAll(".handwrite__ink").forEach((svg) => {
const paths = [...svg.querySelectorAll(".handwrite__stroke")];
// 動きを減らす設定では、描き終えた状態のまま置く
if (reduceMotion) {
gsap.set(paths, { drawSVG: "0% 100%" });
return;
}
// 線の長さはページに入ってから測る。描く時間 = 長さ ÷ ペンの速さ
const tl = gsap.timeline({ paused: true });
let at = 0;
for (const path of paths) {
const duration = Math.max(MIN, path.getTotalLength() / SPEED);
tl.fromTo(path, { drawSVG: "0% 0%" }, { drawSVG: "0% 100%", duration, ease: path.dataset.ease }, at);
at += duration + Number(path.dataset.gap);
}
// 画面に入ったら 1 回だけ描く
const io = new IntersectionObserver(
(entries, self) => {
for (const entry of entries) {
if (!entry.isIntersecting) continue;
self.unobserve(entry.target);
tl.play();
}
},
{ rootMargin: "0px 0px -15%" }
);
io.observe(svg);
});