SVG折れ線グラフ
Catmull-Rom補間で滑らかにしたSVG折れ線と面グラデ。stroke-dashによる描画アニメで時系列データの推移を魅せます。
ライブデモ
使用例(お題: SaaS FlowDesk)
この技法を「SaaS FlowDesk」というテーマのダミーサイトで実際に使った例です。
HTML
<!-- FlowDesk:利用状況ダッシュボード。週次アクティブ数の推移をSVG折れ線で表示 -->
<section class="fd-stage">
<header class="fd-head">
<div class="fd-brand"><span class="fd-mark">◆</span> FlowDesk</div>
<nav class="fd-tabs">
<span class="is-active">利用状況</span>
<span>請求</span>
<span>設定</span>
</nav>
</header>
<div class="fd-card">
<div class="fd-card__top">
<div>
<p class="fd-card__label">週次アクティブユーザー</p>
<p class="fd-card__value">8,642 <span class="fd-up">+18.7%</span></p>
</div>
<span class="fd-card__range">過去8週</span>
</div>
<!-- SVG折れ線グラフ(滑らか曲線+面グラデ+描画アニメ) -->
<svg id="fdLineChart" class="fd-chart" viewBox="0 0 600 240" preserveAspectRatio="none" role="img" aria-label="週次アクティブユーザーの推移">
<defs>
<linearGradient id="fdAreaGrad" x1="0" y1="0" x2="0" y2="1">
<stop offset="0%" stop-color="#4f7cff" stop-opacity="0.35" />
<stop offset="100%" stop-color="#4f7cff" stop-opacity="0" />
</linearGradient>
</defs>
<g id="fdGrid" class="fd-grid"></g>
<path id="fdAreaPath" class="fd-area" />
<path id="fdLinePath" class="fd-line" />
<g id="fdDots" class="fd-dots"></g>
</svg>
</div>
</section>
CSS
/* FlowDesk:利用状況ダッシュボード(SVG折れ線が主役) */
:root {
--navy: #0f1b34;
--blue: #4f7cff;
}
* { box-sizing: border-box; }
body {
margin: 0;
height: 400px;
font-family: "Segoe UI", "Hiragino Kaku Gothic ProN", system-ui, sans-serif;
background: radial-gradient(120% 120% at 50% -10%, #1a2c50 0%, #0f1b34 60%, #0a1228 100%);
color: #eef2ff;
}
.fd-stage { height: 400px; padding: 18px 22px; display: flex; flex-direction: column; }
.fd-head { display: flex; align-items: center; justify-content: space-between; margin-bottom: 14px; }
.fd-brand { font-size: 15px; font-weight: 800; letter-spacing: 0.04em; }
.fd-mark { color: var(--blue); }
.fd-tabs { display: flex; gap: 14px; font-size: 12px; color: rgba(255,255,255,0.5); }
.fd-tabs .is-active { color: #fff; font-weight: 700; border-bottom: 2px solid var(--blue); padding-bottom: 3px; }
/* チャートカード */
.fd-card {
flex: 1;
background: rgba(255,255,255,0.05);
border: 1px solid rgba(255,255,255,0.09);
border-radius: 14px;
padding: 16px 18px 8px;
display: flex; flex-direction: column;
}
.fd-card__top { display: flex; align-items: flex-start; justify-content: space-between; margin-bottom: 6px; }
.fd-card__label { margin: 0; font-size: 11px; letter-spacing: 0.06em; color: #9db4ff; }
.fd-card__value { margin: 4px 0 0; font-size: 24px; font-weight: 800; }
.fd-up { font-size: 12px; font-weight: 700; color: #6fe0a8; margin-left: 4px; }
.fd-card__range { font-size: 11px; color: rgba(255,255,255,0.5); }
/* SVGグラフ本体 */
.fd-chart { flex: 1; width: 100%; min-height: 0; overflow: visible; }
.fd-grid line { stroke: rgba(255,255,255,0.08); stroke-width: 1; }
.fd-grid text { fill: rgba(255,255,255,0.4); font-size: 11px; }
.fd-area { fill: url(#fdAreaGrad); }
.fd-line { fill: none; stroke: var(--blue); stroke-width: 3; stroke-linecap: round; stroke-linejoin: round; }
.fd-dots circle {
fill: #fff; stroke: var(--blue); stroke-width: 2.5; r: 0;
transition: r 0.25s ease;
}
.fd-dots circle.show { r: 4.5px; }
@media (prefers-reduced-motion: reduce) {
.fd-dots circle { transition: none; }
}
JavaScript
// FlowDesk:週次アクティブ数をSVG折れ線+面で描画(滑らか曲線・描画アニメ)
(() => {
const svg = document.getElementById('fdLineChart');
const linePath = document.getElementById('fdLinePath');
const areaPath = document.getElementById('fdAreaPath');
const grid = document.getElementById('fdGrid');
const dots = document.getElementById('fdDots');
if (!svg || !linePath || !areaPath || !grid || !dots) return; // null安全
const NS = 'http://www.w3.org/2000/svg';
const VBW = 600, VBH = 240;
const pad = { top: 20, right: 18, bottom: 28, left: 40 };
// 週次アクティブユーザー(百単位のダミー)
const values = [52, 58, 55, 64, 61, 73, 70, 86];
const labels = ['W1', 'W2', 'W3', 'W4', 'W5', 'W6', 'W7', 'W8'];
const maxV = Math.max(...values);
const plotW = VBW - pad.left - pad.right;
const plotH = VBH - pad.top - pad.bottom;
// データ点を画面座標へ
const pts = values.map((v, i) => ({
x: pad.left + (plotW / (values.length - 1)) * i,
y: pad.top + plotH - (v / maxV) * plotH,
}));
// Catmull-Rom→ベジェで滑らかに
function smoothPath(p) {
if (p.length < 2) return '';
let d = `M ${p[0].x} ${p[0].y}`;
for (let i = 0; i < p.length - 1; i++) {
const p0 = p[i - 1] || p[i];
const p1 = p[i];
const p2 = p[i + 1];
const p3 = p[i + 2] || p2;
const c1x = p1.x + (p2.x - p0.x) / 6;
const c1y = p1.y + (p2.y - p0.y) / 6;
const c2x = p2.x - (p3.x - p1.x) / 6;
const c2y = p2.y - (p3.y - p1.y) / 6;
d += ` C ${c1x} ${c1y}, ${c2x} ${c2y}, ${p2.x} ${p2.y}`;
}
return d;
}
// グリッドと目盛り(千単位表示)
const steps = 4;
for (let i = 0; i <= steps; i++) {
const y = pad.top + (plotH / steps) * i;
const line = document.createElementNS(NS, 'line');
line.setAttribute('x1', pad.left);
line.setAttribute('y1', y);
line.setAttribute('x2', VBW - pad.right);
line.setAttribute('y2', y);
grid.appendChild(line);
const t = document.createElementNS(NS, 'text');
t.setAttribute('x', pad.left - 8);
t.setAttribute('y', y + 4);
t.setAttribute('text-anchor', 'end');
t.textContent = `${Math.round((maxV / steps) * (steps - i)) / 10}k`;
grid.appendChild(t);
}
labels.forEach((lb, i) => {
const t = document.createElementNS(NS, 'text');
t.setAttribute('x', pts[i].x);
t.setAttribute('y', VBH - 8);
t.setAttribute('text-anchor', 'middle');
t.textContent = lb;
grid.appendChild(t);
});
const lineD = smoothPath(pts);
const areaD = `${lineD} L ${pts[pts.length - 1].x} ${pad.top + plotH} L ${pts[0].x} ${pad.top + plotH} Z`;
linePath.setAttribute('d', lineD);
areaPath.setAttribute('d', areaD);
// データ点(ツールチップ付き)
pts.forEach((p, i) => {
const c = document.createElementNS(NS, 'circle');
c.setAttribute('cx', p.x);
c.setAttribute('cy', p.y);
const title = document.createElementNS(NS, 'title');
title.textContent = `${labels[i]}: ${(values[i] * 100).toLocaleString()}人`;
c.appendChild(title);
dots.appendChild(c);
});
const reduceMotion = window.matchMedia('(prefers-reduced-motion: reduce)').matches;
const dotEls = Array.from(dots.children);
if (reduceMotion) {
areaPath.style.opacity = '1';
dotEls.forEach((d) => d.classList.add('show'));
} else {
// stroke-dashで線を描画
const len = linePath.getTotalLength();
linePath.style.strokeDasharray = String(len);
linePath.style.strokeDashoffset = String(len);
areaPath.style.opacity = '0';
areaPath.style.transition = 'opacity .8s ease .3s';
requestAnimationFrame(() => {
linePath.style.transition = 'stroke-dashoffset 1.4s cubic-bezier(.65,0,.35,1)';
linePath.style.strokeDashoffset = '0';
areaPath.style.opacity = '1';
});
dotEls.forEach((d, i) => {
setTimeout(() => d.classList.add('show'), 300 + i * 150);
});
}
})();
コード
HTML
<div class="dv-wrap">
<figure class="dv-card">
<figcaption class="dv-head">
<h2 class="dv-title">週次セッション推移</h2>
<p class="dv-sub">SVGパスのストローク・アニメーションと面グラデ</p>
</figcaption>
<!-- viewBoxで内部座標を固定し、レスポンシブに伸縮させる -->
<svg id="lineChart" class="dv-svg" viewBox="0 0 600 240"
role="img" aria-label="週次セッション数の折れ線グラフ">
<defs>
<linearGradient id="areaFill" x1="0" y1="0" x2="0" y2="1">
<stop offset="0%" stop-color="#a78bfa" stop-opacity="0.45" />
<stop offset="100%" stop-color="#a78bfa" stop-opacity="0" />
</linearGradient>
<linearGradient id="lineStroke" x1="0" y1="0" x2="1" y2="0">
<stop offset="0%" stop-color="#c084fc" />
<stop offset="100%" stop-color="#60a5fa" />
</linearGradient>
</defs>
<g id="grid" class="dv-grid"></g>
<path id="areaPath" class="dv-area" fill="url(#areaFill)"></path>
<path id="linePath" class="dv-line" fill="none" stroke="url(#lineStroke)"></path>
<g id="dots" class="dv-dots"></g>
</svg>
</figure>
</div>
CSS
:root {
--dv-radius: 18px;
--dv-ink: #ede9fe;
--dv-sub: #c4b5fd;
}
* { box-sizing: border-box; }
body {
margin: 0;
min-height: 100vh;
display: grid;
place-items: center;
font-family: "Segoe UI", "Hiragino Kaku Gothic ProN", system-ui, sans-serif;
color: var(--dv-ink);
background:
radial-gradient(900px 500px at 80% -20%, #312e81 0%, transparent 60%),
linear-gradient(160deg, #1e1b4b, #0f0c29);
}
.dv-wrap {
width: min(92vw, 720px);
padding: 20px;
}
.dv-card {
margin: 0;
padding: 22px 24px 18px;
border-radius: var(--dv-radius);
background: rgba(30, 27, 75, 0.45);
border: 1px solid rgba(167, 139, 250, 0.25);
box-shadow: 0 24px 60px -24px rgba(0, 0, 0, 0.75);
backdrop-filter: blur(6px);
}
.dv-head { margin-bottom: 12px; }
.dv-title { margin: 0; font-size: clamp(18px, 3.4vw, 22px); }
.dv-sub { margin: 4px 0 0; font-size: 13px; color: var(--dv-sub); }
.dv-svg {
display: block;
width: 100%;
height: auto;
aspect-ratio: 600 / 240;
max-height: 220px;
overflow: visible;
}
.dv-grid line { stroke: rgba(196, 181, 253, 0.16); stroke-width: 1; }
.dv-grid text { fill: rgba(196, 181, 253, 0.7); font-size: 11px; }
.dv-line {
stroke-width: 3;
stroke-linecap: round;
stroke-linejoin: round;
filter: drop-shadow(0 4px 10px rgba(96, 165, 250, 0.4));
}
.dv-dots circle {
fill: #0f0c29;
stroke: #c084fc;
stroke-width: 2.5;
opacity: 0;
transition: opacity .3s ease, r .2s ease;
}
.dv-dots circle.show { opacity: 1; }
.dv-dots circle:hover { r: 7; }
/* 動きを抑える設定では即時表示 */
@media (prefers-reduced-motion: reduce) {
.dv-line, .dv-area { transition: none !important; }
}
JavaScript
// SVGで折れ線+面グラフを生成。滑らかなCatmull-Rom曲線とライン描画アニメ
(() => {
const svg = document.getElementById('lineChart');
const linePath = document.getElementById('linePath');
const areaPath = document.getElementById('areaPath');
const grid = document.getElementById('grid');
const dots = document.getElementById('dots');
if (!svg || !linePath || !areaPath || !grid || !dots) return; // null安全
const NS = 'http://www.w3.org/2000/svg';
// viewBox基準の座標系(0..600 x 0..240)
const VBW = 600, VBH = 240;
const pad = { top: 20, right: 18, bottom: 28, left: 36 };
const values = [38, 52, 47, 65, 58, 80, 72, 96]; // 週次データ
const labels = ['W1', 'W2', 'W3', 'W4', 'W5', 'W6', 'W7', 'W8'];
const maxV = Math.max(...values);
const plotW = VBW - pad.left - pad.right;
const plotH = VBH - pad.top - pad.bottom;
// データ点を画面座標へ変換
const pts = values.map((v, i) => ({
x: pad.left + (plotW / (values.length - 1)) * i,
y: pad.top + plotH - (v / maxV) * plotH,
}));
// Catmull-Rom→ベジェ変換で滑らかな線を作る
function smoothPath(p) {
if (p.length < 2) return '';
let d = `M ${p[0].x} ${p[0].y}`;
for (let i = 0; i < p.length - 1; i++) {
const p0 = p[i - 1] || p[i];
const p1 = p[i];
const p2 = p[i + 1];
const p3 = p[i + 2] || p2;
const c1x = p1.x + (p2.x - p0.x) / 6;
const c1y = p1.y + (p2.y - p0.y) / 6;
const c2x = p2.x - (p3.x - p1.x) / 6;
const c2y = p2.y - (p3.y - p1.y) / 6;
d += ` C ${c1x} ${c1y}, ${c2x} ${c2y}, ${p2.x} ${p2.y}`;
}
return d;
}
// グリッドと目盛りラベル
const steps = 4;
for (let i = 0; i <= steps; i++) {
const y = pad.top + (plotH / steps) * i;
const line = document.createElementNS(NS, 'line');
line.setAttribute('x1', pad.left);
line.setAttribute('y1', y);
line.setAttribute('x2', VBW - pad.right);
line.setAttribute('y2', y);
grid.appendChild(line);
const t = document.createElementNS(NS, 'text');
t.setAttribute('x', pad.left - 8);
t.setAttribute('y', y + 4);
t.setAttribute('text-anchor', 'end');
t.textContent = Math.round((maxV / steps) * (steps - i));
grid.appendChild(t);
}
labels.forEach((lb, i) => {
const t = document.createElementNS(NS, 'text');
t.setAttribute('x', pts[i].x);
t.setAttribute('y', VBH - 8);
t.setAttribute('text-anchor', 'middle');
t.textContent = lb;
grid.appendChild(t);
});
const lineD = smoothPath(pts);
const areaD = `${lineD} L ${pts[pts.length - 1].x} ${pad.top + plotH} L ${pts[0].x} ${pad.top + plotH} Z`;
linePath.setAttribute('d', lineD);
areaPath.setAttribute('d', areaD);
// データ点(ホバーで強調)
pts.forEach((p, i) => {
const c = document.createElementNS(NS, 'circle');
c.setAttribute('cx', p.x);
c.setAttribute('cy', p.y);
c.setAttribute('r', 4.5);
const title = document.createElementNS(NS, 'title');
title.textContent = `${labels[i]}: ${values[i]}`;
c.appendChild(title);
dots.appendChild(c);
});
const reduceMotion = window.matchMedia('(prefers-reduced-motion: reduce)').matches;
const dotEls = Array.from(dots.children);
if (reduceMotion) {
// アニメ無し:即表示
areaPath.style.opacity = '1';
dotEls.forEach((d) => d.classList.add('show'));
} else {
// ストローク長を使った描画アニメ
const len = linePath.getTotalLength();
linePath.style.strokeDasharray = String(len);
linePath.style.strokeDashoffset = String(len);
areaPath.style.opacity = '0';
areaPath.style.transition = 'opacity .8s ease .3s';
requestAnimationFrame(() => {
linePath.style.transition = 'stroke-dashoffset 1.4s cubic-bezier(.65,0,.35,1)';
linePath.style.strokeDashoffset = '0';
areaPath.style.opacity = '1';
});
// 線の描画進行に合わせて点を順番に表示
dotEls.forEach((d, i) => {
setTimeout(() => d.classList.add('show'), 300 + i * 150);
});
}
})();
🤖 AIエージェント用プロンプト
このままコピーしてAIに貼り付け「追加する場所」だけ書き換えればOK
あなたは熟練のフロントエンドエンジニアです。私のWebサイトに「SVG折れ線グラフ」の効果を追加してください。
# 追加してほしい効果
SVG折れ線グラフ(データ可視化)
Catmull-Rom補間で滑らかにしたSVG折れ線と面グラデ。stroke-dashによる描画アニメで時系列データの推移を魅せます。
# 追加する場所
👉【ここに対象箇所を記入:例「トップのヒーローセクション」「お問い合わせボタン」「記事カードの一覧」など】
# 参考実装(この見た目・挙動を再現してください)
【HTML】
<div class="dv-wrap">
<figure class="dv-card">
<figcaption class="dv-head">
<h2 class="dv-title">週次セッション推移</h2>
<p class="dv-sub">SVGパスのストローク・アニメーションと面グラデ</p>
</figcaption>
<!-- viewBoxで内部座標を固定し、レスポンシブに伸縮させる -->
<svg id="lineChart" class="dv-svg" viewBox="0 0 600 240"
role="img" aria-label="週次セッション数の折れ線グラフ">
<defs>
<linearGradient id="areaFill" x1="0" y1="0" x2="0" y2="1">
<stop offset="0%" stop-color="#a78bfa" stop-opacity="0.45" />
<stop offset="100%" stop-color="#a78bfa" stop-opacity="0" />
</linearGradient>
<linearGradient id="lineStroke" x1="0" y1="0" x2="1" y2="0">
<stop offset="0%" stop-color="#c084fc" />
<stop offset="100%" stop-color="#60a5fa" />
</linearGradient>
</defs>
<g id="grid" class="dv-grid"></g>
<path id="areaPath" class="dv-area" fill="url(#areaFill)"></path>
<path id="linePath" class="dv-line" fill="none" stroke="url(#lineStroke)"></path>
<g id="dots" class="dv-dots"></g>
</svg>
</figure>
</div>
【CSS】
:root {
--dv-radius: 18px;
--dv-ink: #ede9fe;
--dv-sub: #c4b5fd;
}
* { box-sizing: border-box; }
body {
margin: 0;
min-height: 100vh;
display: grid;
place-items: center;
font-family: "Segoe UI", "Hiragino Kaku Gothic ProN", system-ui, sans-serif;
color: var(--dv-ink);
background:
radial-gradient(900px 500px at 80% -20%, #312e81 0%, transparent 60%),
linear-gradient(160deg, #1e1b4b, #0f0c29);
}
.dv-wrap {
width: min(92vw, 720px);
padding: 20px;
}
.dv-card {
margin: 0;
padding: 22px 24px 18px;
border-radius: var(--dv-radius);
background: rgba(30, 27, 75, 0.45);
border: 1px solid rgba(167, 139, 250, 0.25);
box-shadow: 0 24px 60px -24px rgba(0, 0, 0, 0.75);
backdrop-filter: blur(6px);
}
.dv-head { margin-bottom: 12px; }
.dv-title { margin: 0; font-size: clamp(18px, 3.4vw, 22px); }
.dv-sub { margin: 4px 0 0; font-size: 13px; color: var(--dv-sub); }
.dv-svg {
display: block;
width: 100%;
height: auto;
aspect-ratio: 600 / 240;
max-height: 220px;
overflow: visible;
}
.dv-grid line { stroke: rgba(196, 181, 253, 0.16); stroke-width: 1; }
.dv-grid text { fill: rgba(196, 181, 253, 0.7); font-size: 11px; }
.dv-line {
stroke-width: 3;
stroke-linecap: round;
stroke-linejoin: round;
filter: drop-shadow(0 4px 10px rgba(96, 165, 250, 0.4));
}
.dv-dots circle {
fill: #0f0c29;
stroke: #c084fc;
stroke-width: 2.5;
opacity: 0;
transition: opacity .3s ease, r .2s ease;
}
.dv-dots circle.show { opacity: 1; }
.dv-dots circle:hover { r: 7; }
/* 動きを抑える設定では即時表示 */
@media (prefers-reduced-motion: reduce) {
.dv-line, .dv-area { transition: none !important; }
}
【JavaScript】
// SVGで折れ線+面グラフを生成。滑らかなCatmull-Rom曲線とライン描画アニメ
(() => {
const svg = document.getElementById('lineChart');
const linePath = document.getElementById('linePath');
const areaPath = document.getElementById('areaPath');
const grid = document.getElementById('grid');
const dots = document.getElementById('dots');
if (!svg || !linePath || !areaPath || !grid || !dots) return; // null安全
const NS = 'http://www.w3.org/2000/svg';
// viewBox基準の座標系(0..600 x 0..240)
const VBW = 600, VBH = 240;
const pad = { top: 20, right: 18, bottom: 28, left: 36 };
const values = [38, 52, 47, 65, 58, 80, 72, 96]; // 週次データ
const labels = ['W1', 'W2', 'W3', 'W4', 'W5', 'W6', 'W7', 'W8'];
const maxV = Math.max(...values);
const plotW = VBW - pad.left - pad.right;
const plotH = VBH - pad.top - pad.bottom;
// データ点を画面座標へ変換
const pts = values.map((v, i) => ({
x: pad.left + (plotW / (values.length - 1)) * i,
y: pad.top + plotH - (v / maxV) * plotH,
}));
// Catmull-Rom→ベジェ変換で滑らかな線を作る
function smoothPath(p) {
if (p.length < 2) return '';
let d = `M ${p[0].x} ${p[0].y}`;
for (let i = 0; i < p.length - 1; i++) {
const p0 = p[i - 1] || p[i];
const p1 = p[i];
const p2 = p[i + 1];
const p3 = p[i + 2] || p2;
const c1x = p1.x + (p2.x - p0.x) / 6;
const c1y = p1.y + (p2.y - p0.y) / 6;
const c2x = p2.x - (p3.x - p1.x) / 6;
const c2y = p2.y - (p3.y - p1.y) / 6;
d += ` C ${c1x} ${c1y}, ${c2x} ${c2y}, ${p2.x} ${p2.y}`;
}
return d;
}
// グリッドと目盛りラベル
const steps = 4;
for (let i = 0; i <= steps; i++) {
const y = pad.top + (plotH / steps) * i;
const line = document.createElementNS(NS, 'line');
line.setAttribute('x1', pad.left);
line.setAttribute('y1', y);
line.setAttribute('x2', VBW - pad.right);
line.setAttribute('y2', y);
grid.appendChild(line);
const t = document.createElementNS(NS, 'text');
t.setAttribute('x', pad.left - 8);
t.setAttribute('y', y + 4);
t.setAttribute('text-anchor', 'end');
t.textContent = Math.round((maxV / steps) * (steps - i));
grid.appendChild(t);
}
labels.forEach((lb, i) => {
const t = document.createElementNS(NS, 'text');
t.setAttribute('x', pts[i].x);
t.setAttribute('y', VBH - 8);
t.setAttribute('text-anchor', 'middle');
t.textContent = lb;
grid.appendChild(t);
});
const lineD = smoothPath(pts);
const areaD = `${lineD} L ${pts[pts.length - 1].x} ${pad.top + plotH} L ${pts[0].x} ${pad.top + plotH} Z`;
linePath.setAttribute('d', lineD);
areaPath.setAttribute('d', areaD);
// データ点(ホバーで強調)
pts.forEach((p, i) => {
const c = document.createElementNS(NS, 'circle');
c.setAttribute('cx', p.x);
c.setAttribute('cy', p.y);
c.setAttribute('r', 4.5);
const title = document.createElementNS(NS, 'title');
title.textContent = `${labels[i]}: ${values[i]}`;
c.appendChild(title);
dots.appendChild(c);
});
const reduceMotion = window.matchMedia('(prefers-reduced-motion: reduce)').matches;
const dotEls = Array.from(dots.children);
if (reduceMotion) {
// アニメ無し:即表示
areaPath.style.opacity = '1';
dotEls.forEach((d) => d.classList.add('show'));
} else {
// ストローク長を使った描画アニメ
const len = linePath.getTotalLength();
linePath.style.strokeDasharray = String(len);
linePath.style.strokeDashoffset = String(len);
areaPath.style.opacity = '0';
areaPath.style.transition = 'opacity .8s ease .3s';
requestAnimationFrame(() => {
linePath.style.transition = 'stroke-dashoffset 1.4s cubic-bezier(.65,0,.35,1)';
linePath.style.strokeDashoffset = '0';
areaPath.style.opacity = '1';
});
// 線の描画進行に合わせて点を順番に表示
dotEls.forEach((d, i) => {
setTimeout(() => d.classList.add('show'), 300 + i * 150);
});
}
})();
# 外部ライブラリ
なし(追加ライブラリ不要)
# 守ってほしいこと
- 既存のHTML構造・レイアウト・デザインを壊さないこと。必要に応じてクラス名・色・サイズを私のサイトに合わせて調整してよい。
- クラス名やidが既存と衝突しないよう、必要なら接頭辞で名前空間を分けること。
- レスポンシブ対応と prefers-reduced-motion への配慮を入れること。
- 私のサイトのフレームワーク(React / Vue / 素のHTML など)に合わせて実装すること。不明な場合は素のHTML/CSS/JSで提示し、組み込み手順も説明すること。
- 変更後の確認手順も簡潔に教えてください。