パスドローイング(手書きアニメ)
stroke-dasharrayとdashoffsetでSVGの線が描かれていく演出。グラフやロゴ、署名の登場アニメに使えます。
ライブデモ
使用例(お題: アイドルグループ Sakura)
この技法を「アイドルグループ Sakura」というテーマのダミーサイトで実際に使った例です。
HTML
<!-- Sakura:メンバー直筆サインが描かれるグッズ紹介 -->
<section class="sk-sign">
<div class="sk-sign__photo" aria-hidden="true"></div>
<div class="sk-sign__panel">
<span class="sk-sign__tag">GOODS / 限定</span>
<h2 class="sk-sign__title">直筆サイン入り<br>チェキ風ブロマイド</h2>
<!-- 主役:手書き風に描かれるサインSVG -->
<svg class="sk-draw" viewBox="0 0 280 140" role="img" aria-label="手書き風に描かれるメンバーのサイン">
<path class="sk-name" d="M20 95 q12 -55 26 -18 t26 -14 q14 38 30 4 t26 -26" />
<path class="sk-name2" d="M120 100 q18 -48 34 -8 t34 -22 q12 30 30 2" />
<path class="sk-heart" d="M210 70 c-9 -12 -26 -2 -16 12 c4 6 16 14 16 14 c0 0 12 -8 16 -14 c10 -14 -7 -24 -16 -12 Z" />
<path class="sk-under" d="M20 122 H250" />
</svg>
<button class="sk-sign__btn" type="button">▶ サインを再生</button>
<p class="sk-sign__note">※ 全7種ランダム封入。¥800(税込)</p>
</div>
</section>
CSS
/* Sakura:直筆サイン入りグッズ紹介 */
:root {
--pink: #ffd1e0;
--pink-deep: #ff7aa8;
--gray: #f3f4f7;
--ink: #4a4453;
}
* { box-sizing: border-box; }
body {
margin: 0;
height: 400px;
display: grid;
place-items: center;
background: linear-gradient(135deg, #fff 0%, var(--pink) 100%);
font-family: "Hiragino Kaku Gothic ProN", "Yu Gothic", system-ui, sans-serif;
color: var(--ink);
overflow: hidden;
}
.sk-sign {
display: grid;
grid-template-columns: 150px 1fr;
width: min(620px, 94vw);
background: #fff;
border-radius: 22px;
overflow: hidden;
box-shadow: 0 22px 50px -22px rgba(255, 122, 168, 0.55);
}
/* メンバー写真 */
.sk-sign__photo {
background:
linear-gradient(180deg, rgba(255,122,168,0.12), rgba(255,209,224,0.35)),
url("https://picsum.photos/300/500?random=61") center/cover no-repeat;
}
.sk-sign__panel { padding: 22px 26px 20px; }
.sk-sign__tag {
display: inline-block;
font-size: 10px;
letter-spacing: 0.18em;
font-weight: 700;
color: var(--pink-deep);
background: var(--pink);
padding: 4px 10px;
border-radius: 999px;
}
.sk-sign__title {
margin: 12px 0 6px;
font-size: 20px;
line-height: 1.45;
font-weight: 800;
}
/* 主役のサインSVG:線は最初隠し、--len分のdashを流す */
.sk-draw { width: 100%; height: auto; display: block; margin: 6px 0 4px; }
.sk-draw path {
fill: none;
stroke-linecap: round;
stroke-linejoin: round;
}
.sk-name {
stroke: var(--pink-deep);
stroke-width: 3.5;
stroke-dasharray: var(--len, 400);
stroke-dashoffset: var(--len, 400);
animation: skDash 1.6s ease forwards;
}
.sk-name2 {
stroke: var(--pink-deep);
stroke-width: 3.5;
stroke-dasharray: var(--len, 400);
stroke-dashoffset: var(--len, 400);
animation: skDash 1.4s ease 0.6s forwards;
}
.sk-heart {
stroke: #ff4f8b;
stroke-width: 3;
fill: rgba(255, 79, 139, 0);
stroke-dasharray: var(--len, 200);
stroke-dashoffset: var(--len, 200);
animation: skDash 0.9s ease 1.4s forwards, skFill 0.4s ease 2.2s forwards;
}
.sk-under {
stroke: rgba(74, 68, 83, 0.18);
stroke-width: 1.5;
stroke-dasharray: var(--len, 240);
stroke-dashoffset: var(--len, 240);
animation: skDash 0.8s ease 0.2s forwards;
}
@keyframes skDash { to { stroke-dashoffset: 0; } }
@keyframes skFill { to { fill: rgba(255, 79, 139, 0.9); } }
.sk-sign__btn {
font: 700 12px/1 "Hiragino Kaku Gothic ProN", sans-serif;
color: var(--pink-deep);
background: var(--gray);
border: 1px solid rgba(255, 122, 168, 0.4);
padding: 8px 16px;
border-radius: 999px;
cursor: pointer;
transition: background 0.2s, transform 0.1s;
}
.sk-sign__btn:hover { background: var(--pink); }
.sk-sign__btn:active { transform: scale(0.96); }
.sk-sign__note { margin: 12px 0 0; font-size: 11px; color: #9b94a5; }
@media (prefers-reduced-motion: reduce) {
.sk-draw path { animation: none; stroke-dashoffset: 0; }
.sk-heart { fill: rgba(255, 79, 139, 0.9); }
}
JavaScript
// 各パスの実長を測って dasharray/offset に反映(サインがきれいに描かれる)
const draw = document.querySelector(".sk-draw");
if (draw) {
const paths = draw.querySelectorAll("path");
// パス長を CSS 変数 --len へ渡す
const measure = (el) => {
if (typeof el.getTotalLength === "function") {
el.style.setProperty("--len", Math.ceil(el.getTotalLength()));
}
};
paths.forEach(measure);
// 再生ボタン:アニメをリスタート
const btn = document.querySelector(".sk-sign__btn");
const replay = () => {
paths.forEach((el) => {
el.style.animation = "none";
void el.offsetWidth; // リフローでリセット
el.style.animation = "";
});
};
btn?.addEventListener("click", replay);
}
コード
HTML
<!-- パスドローイング: stroke-dasharray/offset で線が描かれるアニメ -->
<div class="stage">
<svg class="draw" viewBox="0 0 320 200" role="img" aria-label="手書き風に描かれるグラフと署名">
<!-- 折れ線グラフ -->
<polyline class="line" points="20,160 80,110 130,135 185,60 250,90 300,40" />
<!-- 下線 -->
<path class="underline" d="M20 178 H300" />
<!-- 署名風の筆記体パス -->
<path class="sign" d="M40 150 q15 -50 30 -10 t30 -10 q20 40 40 0 t40 -20" />
<!-- 円(ハイライト) -->
<circle class="dot" cx="185" cy="60" r="7" />
</svg>
<button class="replay" type="button" aria-label="もう一度再生">▶ 再描画</button>
</div>
CSS
:root {
--ink: #f4f7ff;
--accent: #5eead4;
--accent2: #818cf8;
}
* { box-sizing: border-box; }
body {
margin: 0;
min-height: 100vh;
display: grid;
place-items: center;
font-family: "Segoe UI", system-ui, sans-serif;
/* 斜めグラデの落ち着いた背景 */
background:
radial-gradient(120% 120% at 80% 0%, #1e293b 0%, #0f172a 55%, #020617 100%);
}
.stage {
position: relative;
width: min(92%, 460px);
padding: 22px 26px 30px;
border-radius: 18px;
background: rgba(255, 255, 255, .03);
border: 1px solid rgba(255, 255, 255, .08);
box-shadow: 0 30px 60px -25px rgba(0, 0, 0, .8);
}
.draw { width: 100%; height: auto; display: block; }
/* 共通: 線は最初フルに隠し、--len分のdashを流す */
.draw [class] {
fill: none;
stroke-linecap: round;
stroke-linejoin: round;
}
.line {
stroke: var(--accent);
stroke-width: 3.5;
stroke-dasharray: var(--len, 400);
stroke-dashoffset: var(--len, 400);
animation: dash 1.6s ease forwards;
}
.underline {
stroke: rgba(255, 255, 255, .25);
stroke-width: 1.5;
stroke-dasharray: var(--len, 300);
stroke-dashoffset: var(--len, 300);
animation: dash 1s ease .2s forwards;
}
.sign {
stroke: var(--accent2);
stroke-width: 2.5;
stroke-dasharray: var(--len, 400);
stroke-dashoffset: var(--len, 400);
animation: dash 1.8s ease .6s forwards;
}
.dot {
fill: var(--accent);
stroke: #0f172a;
stroke-width: 2;
transform-box: fill-box;
transform-origin: center;
transform: scale(0);
animation: pop .4s cubic-bezier(.34, 1.56, .64, 1) 1.4s forwards;
}
@keyframes dash { to { stroke-dashoffset: 0; } }
@keyframes pop { to { transform: scale(1); } }
.replay {
position: absolute;
right: 14px;
bottom: 10px;
font: 600 12px/1 "Segoe UI", sans-serif;
color: var(--ink);
background: rgba(129, 140, 248, .18);
border: 1px solid rgba(129, 140, 248, .5);
padding: 6px 12px;
border-radius: 999px;
cursor: pointer;
transition: background .2s, transform .1s;
}
.replay:hover { background: rgba(129, 140, 248, .35); }
.replay:active { transform: scale(.96); }
@media (prefers-reduced-motion: reduce) {
.line, .underline, .sign { animation: none; stroke-dashoffset: 0; }
.dot { animation: none; transform: scale(1); }
}
JavaScript
// 各パスの実長を取得して dasharray/offset に反映(線がきれいに描かれる)
const svg = document.querySelector(".draw");
if (svg) {
const shapes = svg.querySelectorAll(".line, .underline, .sign");
// パスの長さを測って CSS 変数に渡す
const measure = (el) => {
if (typeof el.getTotalLength === "function") {
const len = Math.ceil(el.getTotalLength());
el.style.setProperty("--len", len);
}
};
shapes.forEach(measure);
// 再描画ボタン: アニメをリスタート
const replay = document.querySelector(".replay");
const restart = () => {
svg.querySelectorAll(".line, .underline, .sign, .dot").forEach((el) => {
el.style.animation = "none";
void el.offsetWidth; // リフローでアニメをリセット
el.style.animation = "";
});
};
replay?.addEventListener("click", restart);
}
🤖 AIエージェント用プロンプト
このままコピーしてAIに貼り付け「追加する場所」だけ書き換えればOK
あなたは熟練のフロントエンドエンジニアです。私のWebサイトに「パスドローイング(手書きアニメ)」の効果を追加してください。
# 追加してほしい効果
パスドローイング(手書きアニメ)(SVG エフェクト)
stroke-dasharrayとdashoffsetでSVGの線が描かれていく演出。グラフやロゴ、署名の登場アニメに使えます。
# 追加する場所
👉【ここに対象箇所を記入:例「トップのヒーローセクション」「お問い合わせボタン」「記事カードの一覧」など】
# 参考実装(この見た目・挙動を再現してください)
【HTML】
<!-- パスドローイング: stroke-dasharray/offset で線が描かれるアニメ -->
<div class="stage">
<svg class="draw" viewBox="0 0 320 200" role="img" aria-label="手書き風に描かれるグラフと署名">
<!-- 折れ線グラフ -->
<polyline class="line" points="20,160 80,110 130,135 185,60 250,90 300,40" />
<!-- 下線 -->
<path class="underline" d="M20 178 H300" />
<!-- 署名風の筆記体パス -->
<path class="sign" d="M40 150 q15 -50 30 -10 t30 -10 q20 40 40 0 t40 -20" />
<!-- 円(ハイライト) -->
<circle class="dot" cx="185" cy="60" r="7" />
</svg>
<button class="replay" type="button" aria-label="もう一度再生">▶ 再描画</button>
</div>
【CSS】
:root {
--ink: #f4f7ff;
--accent: #5eead4;
--accent2: #818cf8;
}
* { box-sizing: border-box; }
body {
margin: 0;
min-height: 100vh;
display: grid;
place-items: center;
font-family: "Segoe UI", system-ui, sans-serif;
/* 斜めグラデの落ち着いた背景 */
background:
radial-gradient(120% 120% at 80% 0%, #1e293b 0%, #0f172a 55%, #020617 100%);
}
.stage {
position: relative;
width: min(92%, 460px);
padding: 22px 26px 30px;
border-radius: 18px;
background: rgba(255, 255, 255, .03);
border: 1px solid rgba(255, 255, 255, .08);
box-shadow: 0 30px 60px -25px rgba(0, 0, 0, .8);
}
.draw { width: 100%; height: auto; display: block; }
/* 共通: 線は最初フルに隠し、--len分のdashを流す */
.draw [class] {
fill: none;
stroke-linecap: round;
stroke-linejoin: round;
}
.line {
stroke: var(--accent);
stroke-width: 3.5;
stroke-dasharray: var(--len, 400);
stroke-dashoffset: var(--len, 400);
animation: dash 1.6s ease forwards;
}
.underline {
stroke: rgba(255, 255, 255, .25);
stroke-width: 1.5;
stroke-dasharray: var(--len, 300);
stroke-dashoffset: var(--len, 300);
animation: dash 1s ease .2s forwards;
}
.sign {
stroke: var(--accent2);
stroke-width: 2.5;
stroke-dasharray: var(--len, 400);
stroke-dashoffset: var(--len, 400);
animation: dash 1.8s ease .6s forwards;
}
.dot {
fill: var(--accent);
stroke: #0f172a;
stroke-width: 2;
transform-box: fill-box;
transform-origin: center;
transform: scale(0);
animation: pop .4s cubic-bezier(.34, 1.56, .64, 1) 1.4s forwards;
}
@keyframes dash { to { stroke-dashoffset: 0; } }
@keyframes pop { to { transform: scale(1); } }
.replay {
position: absolute;
right: 14px;
bottom: 10px;
font: 600 12px/1 "Segoe UI", sans-serif;
color: var(--ink);
background: rgba(129, 140, 248, .18);
border: 1px solid rgba(129, 140, 248, .5);
padding: 6px 12px;
border-radius: 999px;
cursor: pointer;
transition: background .2s, transform .1s;
}
.replay:hover { background: rgba(129, 140, 248, .35); }
.replay:active { transform: scale(.96); }
@media (prefers-reduced-motion: reduce) {
.line, .underline, .sign { animation: none; stroke-dashoffset: 0; }
.dot { animation: none; transform: scale(1); }
}
【JavaScript】
// 各パスの実長を取得して dasharray/offset に反映(線がきれいに描かれる)
const svg = document.querySelector(".draw");
if (svg) {
const shapes = svg.querySelectorAll(".line, .underline, .sign");
// パスの長さを測って CSS 変数に渡す
const measure = (el) => {
if (typeof el.getTotalLength === "function") {
const len = Math.ceil(el.getTotalLength());
el.style.setProperty("--len", len);
}
};
shapes.forEach(measure);
// 再描画ボタン: アニメをリスタート
const replay = document.querySelector(".replay");
const restart = () => {
svg.querySelectorAll(".line, .underline, .sign, .dot").forEach((el) => {
el.style.animation = "none";
void el.offsetWidth; // リフローでアニメをリセット
el.style.animation = "";
});
};
replay?.addEventListener("click", restart);
}
# 外部ライブラリ
なし(追加ライブラリ不要)
# 守ってほしいこと
- 既存のHTML構造・レイアウト・デザインを壊さないこと。必要に応じてクラス名・色・サイズを私のサイトに合わせて調整してよい。
- クラス名やidが既存と衝突しないよう、必要なら接頭辞で名前空間を分けること。
- レスポンシブ対応と prefers-reduced-motion への配慮を入れること。
- 私のサイトのフレームワーク(React / Vue / 素のHTML など)に合わせて実装すること。不明な場合は素のHTML/CSS/JSで提示し、組み込み手順も説明すること。
- 変更後の確認手順も簡潔に教えてください。