リアルタイム波形
Canvasで合成正弦波を毎フレーム描画するオシロスコープ風モニタ。グロー発光と一時停止操作で、信号やストリーミングデータの可視化に。
ライブデモ
使用例(お題: アイドルグループ Sakura)
この技法を「アイドルグループ Sakura」というテーマのダミーサイトで実際に使った例です。
HTML
<!-- Sakura:ライブ配信画面。会場の歓声レベルをリアルタイム波形で可視化 -->
<section class="sw-stage">
<header class="sw-head">
<div class="sw-brand"><span class="sw-petal"></span> Sakura</div>
<div class="sw-live">
<span class="sw-led" id="swLed"></span>
<span class="sw-live__txt">LIVE 配信中</span>
</div>
</header>
<div class="sw-monitor">
<div class="sw-monitor__top">
<p class="sw-monitor__title">会場 盛り上がりメーター</p>
<span class="sw-hz" id="swHz">42.0 Hz</span>
</div>
<!-- リアルタイム波形(Canvasで合成正弦波を毎フレーム描画) -->
<canvas id="swWaveCanvas" class="sw-canvas" role="img" aria-label="会場の歓声波形"></canvas>
</div>
<footer class="sw-foot">
<span class="sw-now">♪ 再生中:「春風センセーション」</span>
<button class="sw-pause" id="swWavePause" type="button" aria-pressed="false">一時停止</button>
</footer>
</section>
CSS
/* Sakura:ライブ盛り上がりメーター(リアルタイム波形が主役) */
* { box-sizing: border-box; }
body {
margin: 0;
height: 400px;
font-family: "Yu Gothic", "Hiragino Kaku Gothic ProN", "Segoe UI", sans-serif;
background:
radial-gradient(600px 360px at 50% 18%, #3a1a2a 0%, transparent 65%),
linear-gradient(165deg, #1a0e16 0%, #12080f 100%);
color: #ffd1e0;
}
.sw-stage { height: 400px; padding: 18px 22px; display: flex; flex-direction: column; }
.sw-head { display: flex; align-items: center; justify-content: space-between; margin-bottom: 14px; }
.sw-brand { display: flex; align-items: center; gap: 9px; font-weight: 800; font-size: 16px; color: #ffd1e0; }
.sw-petal {
width: 14px; height: 14px; background: #ffd1e0;
border-radius: 50% 0 50% 50%; transform: rotate(45deg);
box-shadow: 0 0 10px #ff8fb5;
}
.sw-live { display: flex; align-items: center; gap: 7px; font-size: 12px; font-weight: 700; }
.sw-live__txt { color: #ff8fb5; letter-spacing: 0.08em; }
/* 録画中ランプ(点滅)。一時停止で消灯 */
.sw-led {
width: 9px; height: 9px; border-radius: 50%;
background: #ff5e98; box-shadow: 0 0 8px #ff5e98;
animation: swBlink 1.1s infinite;
}
.sw-led.paused { background: #6b5560; box-shadow: none; animation: none; }
@keyframes swBlink { 0%,100% { opacity: 1; } 50% { opacity: 0.35; } }
/* 波形モニタ */
.sw-monitor {
flex: 1;
background: rgba(20,10,18,0.6);
border: 1px solid rgba(255,143,181,0.22);
border-radius: 14px;
padding: 12px 14px 8px;
display: flex; flex-direction: column;
box-shadow: inset 0 0 40px rgba(255,94,152,0.06);
}
.sw-monitor__top { display: flex; align-items: center; justify-content: space-between; margin-bottom: 4px; }
.sw-monitor__title { margin: 0; font-size: 12px; color: #e0a8bd; letter-spacing: 0.06em; }
.sw-hz {
font-size: 12px; font-weight: 700; color: #ff8fb5;
font-variant-numeric: tabular-nums; letter-spacing: 0.04em;
}
.sw-canvas { flex: 1; width: 100%; min-height: 0; display: block; }
.sw-foot { display: flex; align-items: center; justify-content: space-between; margin-top: 12px; }
.sw-now { font-size: 12px; color: #e0a8bd; }
.sw-pause {
font: inherit; font-size: 12px; font-weight: 700;
padding: 8px 18px; border-radius: 20px; cursor: pointer;
color: #fff0f6; background: transparent;
border: 1.5px solid #ff5e98;
box-shadow: 0 0 12px rgba(255,94,152,0.4), inset 0 0 8px rgba(255,94,152,0.15);
transition: background 0.2s ease;
}
.sw-pause:hover { background: rgba(255,94,152,0.15); }
JavaScript
// Sakura:会場の歓声レベルを合成波形でリアルタイム描画(グロー・一時停止)
(() => {
const canvas = document.getElementById('swWaveCanvas');
const pauseBtn = document.getElementById('swWavePause');
const led = document.getElementById('swLed');
const hzEl = document.getElementById('swHz');
if (!canvas) return; // null安全
const ctx = canvas.getContext('2d');
if (!ctx) return;
let dpr = 1, W = 0, H = 0;
function resize() {
dpr = Math.max(1, window.devicePixelRatio || 1);
const rect = canvas.getBoundingClientRect();
W = rect.width; H = rect.height;
canvas.width = Math.round(W * dpr);
canvas.height = Math.round(H * dpr);
ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
}
resize();
window.addEventListener('resize', resize);
const reduceMotion = window.matchMedia('(prefers-reduced-motion: reduce)').matches;
let phase = 0;
let running = !reduceMotion; // reduced-motionなら静止波形
let lastTime = performance.now();
// 複数正弦波を合成して有機的な歓声波形を作る
function sample(x, t) {
const k = x * 0.018;
return (
Math.sin(k + t * 2.0) * 0.5 +
Math.sin(k * 2.3 + t * 1.3) * 0.28 +
Math.sin(k * 0.6 - t * 0.8) * 0.18
);
}
function drawGrid() {
ctx.strokeStyle = 'rgba(255, 143, 181, 0.1)';
ctx.lineWidth = 1;
const gx = 28, gy = 24;
for (let x = 0; x <= W; x += gx) {
ctx.beginPath(); ctx.moveTo(x, 0); ctx.lineTo(x, H); ctx.stroke();
}
for (let y = 0; y <= H; y += gy) {
ctx.beginPath(); ctx.moveTo(0, y); ctx.lineTo(W, y); ctx.stroke();
}
// 中央基準線を強調
ctx.strokeStyle = 'rgba(255, 143, 181, 0.22)';
ctx.beginPath(); ctx.moveTo(0, H / 2); ctx.lineTo(W, H / 2); ctx.stroke();
}
function drawWave(t) {
const mid = H / 2;
const amp = H * 0.34;
ctx.beginPath();
for (let x = 0; x <= W; x += 2) {
const y = mid + sample(x, t) * amp;
if (x === 0) ctx.moveTo(x, y); else ctx.lineTo(x, y);
}
// 桜ピンクのグロー付き発光ライン
ctx.lineJoin = 'round';
ctx.strokeStyle = 'rgba(255, 94, 152, 0.35)';
ctx.lineWidth = 6;
ctx.shadowColor = '#ff5e98';
ctx.shadowBlur = 14;
ctx.stroke();
ctx.strokeStyle = '#ffd1e0';
ctx.lineWidth = 2;
ctx.shadowBlur = 6;
ctx.stroke();
ctx.shadowBlur = 0;
}
function frame(now) {
const dt = Math.min(0.05, (now - lastTime) / 1000);
lastTime = now;
if (running) phase += dt;
ctx.clearRect(0, 0, W, H);
drawGrid();
drawWave(phase);
// 擬似的な周波数表示(位相速度から算出)
if (hzEl) {
const hz = (42 + Math.sin(phase * 0.5) * 6).toFixed(1);
hzEl.textContent = `${hz} Hz`;
}
requestAnimationFrame(frame);
}
// 一時停止トグル
if (pauseBtn) {
pauseBtn.addEventListener('click', () => {
running = !running;
pauseBtn.textContent = running ? '一時停止' : '再開';
pauseBtn.setAttribute('aria-pressed', String(!running));
if (led) led.classList.toggle('paused', !running);
});
}
if (reduceMotion && pauseBtn) {
pauseBtn.textContent = '再開';
pauseBtn.setAttribute('aria-pressed', 'true');
if (led) led.classList.add('paused');
}
requestAnimationFrame(frame);
})();
コード
HTML
<div class="dv-wrap">
<figure class="dv-card">
<figcaption class="dv-head">
<h2 class="dv-title">リアルタイム信号モニタ</h2>
<p class="dv-sub">Canvasで毎フレーム流れる波形(オシロスコープ風)</p>
</figcaption>
<div class="dv-screen">
<canvas id="waveCanvas" class="dv-canvas" role="img" aria-label="リアルタイムに流れる波形"></canvas>
<div class="dv-readout">
<span class="dv-led" id="waveLed"></span>
<span id="waveHz" class="dv-hz">— Hz</span>
</div>
</div>
<div class="dv-ctrl">
<button id="wavePause" class="dv-btn" type="button" aria-pressed="false">一時停止</button>
</div>
</figure>
</div>
CSS
:root {
--dv-radius: 18px;
--dv-ink: #d1fae5;
--dv-sub: #6ee7b7;
--dv-screen: #021410;
}
* { 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(800px 500px at 50% -10%, #064e3b 0%, transparent 55%),
linear-gradient(160deg, #03251c, #010d0a);
}
.dv-wrap { width: min(92vw, 720px); padding: 20px; }
.dv-card {
margin: 0;
padding: 20px 22px 18px;
border-radius: var(--dv-radius);
background: rgba(2, 20, 16, 0.6);
border: 1px solid rgba(16, 185, 129, 0.25);
box-shadow: 0 24px 60px -24px rgba(0, 0, 0, 0.8);
}
.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-screen {
position: relative;
border-radius: 12px;
overflow: hidden;
background: var(--dv-screen);
box-shadow: inset 0 0 40px rgba(0, 0, 0, 0.6), inset 0 0 0 1px rgba(16, 185, 129, 0.2);
}
.dv-canvas { display: block; width: 100%; height: 176px; }
.dv-readout {
position: absolute;
top: 10px;
right: 12px;
display: flex;
align-items: center;
gap: 8px;
font-variant-numeric: tabular-nums;
font-size: 13px;
font-weight: 700;
color: #6ee7b7;
text-shadow: 0 0 8px rgba(110, 231, 183, 0.6);
}
.dv-led {
width: 9px;
height: 9px;
border-radius: 50%;
background: #34d399;
box-shadow: 0 0 10px #34d399;
animation: dv-blink 1s steps(2, jump-none) infinite;
}
.dv-led.paused { background: #fbbf24; box-shadow: 0 0 10px #fbbf24; animation: none; }
@keyframes dv-blink { 50% { opacity: 0.25; } }
.dv-ctrl { margin-top: 12px; text-align: center; }
.dv-btn {
padding: 9px 22px;
border: 1px solid rgba(16, 185, 129, 0.45);
border-radius: 999px;
background: rgba(16, 185, 129, 0.12);
color: var(--dv-ink);
font-size: 13px;
font-weight: 600;
cursor: pointer;
transition: background .2s ease, transform .1s ease;
}
.dv-btn:hover { background: rgba(16, 185, 129, 0.25); }
.dv-btn:active { transform: scale(0.96); }
.dv-btn:focus-visible { outline: 2px solid #6ee7b7; outline-offset: 2px; }
@media (prefers-reduced-motion: reduce) {
.dv-led { animation: none; }
}
JavaScript
// Canvasで流れる合成波形をリアルタイム描画(グリッド・グロー・周波数表示付き)
(() => {
const canvas = document.getElementById('waveCanvas');
const pauseBtn = document.getElementById('wavePause');
const led = document.getElementById('waveLed');
const hzEl = document.getElementById('waveHz');
if (!canvas) return; // null安全
const ctx = canvas.getContext('2d');
if (!ctx) return;
let dpr = 1, W = 0, H = 0;
function resize() {
dpr = Math.max(1, window.devicePixelRatio || 1);
const rect = canvas.getBoundingClientRect();
W = rect.width; H = rect.height;
canvas.width = Math.round(W * dpr);
canvas.height = Math.round(H * dpr);
ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
}
resize();
window.addEventListener('resize', resize);
const reduceMotion = window.matchMedia('(prefers-reduced-motion: reduce)').matches;
let phase = 0;
let running = !reduceMotion; // reduced-motionなら静止波形
let lastTime = performance.now();
// 複数の正弦波を合成して有機的な波形を作る
function sample(x, t) {
const k = x * 0.018;
return (
Math.sin(k + t * 2.0) * 0.5 +
Math.sin(k * 2.3 + t * 1.3) * 0.28 +
Math.sin(k * 0.6 - t * 0.8) * 0.18
);
}
function drawGrid() {
ctx.strokeStyle = 'rgba(16, 185, 129, 0.12)';
ctx.lineWidth = 1;
const gx = 28, gy = 24;
for (let x = 0; x <= W; x += gx) {
ctx.beginPath(); ctx.moveTo(x, 0); ctx.lineTo(x, H); ctx.stroke();
}
for (let y = 0; y <= H; y += gy) {
ctx.beginPath(); ctx.moveTo(0, y); ctx.lineTo(W, y); ctx.stroke();
}
// 中央基準線を強調
ctx.strokeStyle = 'rgba(16, 185, 129, 0.25)';
ctx.beginPath(); ctx.moveTo(0, H / 2); ctx.lineTo(W, H / 2); ctx.stroke();
}
function drawWave(t) {
const mid = H / 2;
const amp = H * 0.34;
ctx.beginPath();
for (let x = 0; x <= W; x += 2) {
const y = mid + sample(x, t) * amp;
if (x === 0) ctx.moveTo(x, y); else ctx.lineTo(x, y);
}
// グロー付きの発光ライン
ctx.lineJoin = 'round';
ctx.strokeStyle = 'rgba(52, 211, 153, 0.35)';
ctx.lineWidth = 6;
ctx.shadowColor = '#34d399';
ctx.shadowBlur = 14;
ctx.stroke();
ctx.strokeStyle = '#6ee7b7';
ctx.lineWidth = 2;
ctx.shadowBlur = 6;
ctx.stroke();
ctx.shadowBlur = 0;
}
function frame(now) {
const dt = Math.min(0.05, (now - lastTime) / 1000);
lastTime = now;
if (running) phase += dt;
ctx.clearRect(0, 0, W, H);
drawGrid();
drawWave(phase);
// 擬似的な周波数表示(位相速度から算出)
if (hzEl) {
const hz = (42 + Math.sin(phase * 0.5) * 6).toFixed(1);
hzEl.textContent = `${hz} Hz`;
}
requestAnimationFrame(frame);
}
// 一時停止トグル
if (pauseBtn) {
pauseBtn.addEventListener('click', () => {
running = !running;
pauseBtn.textContent = running ? '一時停止' : '再開';
pauseBtn.setAttribute('aria-pressed', String(!running));
if (led) led.classList.toggle('paused', !running);
});
}
if (reduceMotion && pauseBtn) {
pauseBtn.textContent = '再開';
pauseBtn.setAttribute('aria-pressed', 'true');
if (led) led.classList.add('paused');
}
requestAnimationFrame(frame);
})();
🤖 AIエージェント用プロンプト
このままコピーしてAIに貼り付け「追加する場所」だけ書き換えればOK
あなたは熟練のフロントエンドエンジニアです。私のWebサイトに「リアルタイム波形」の効果を追加してください。
# 追加してほしい効果
リアルタイム波形(データ可視化)
Canvasで合成正弦波を毎フレーム描画するオシロスコープ風モニタ。グロー発光と一時停止操作で、信号やストリーミングデータの可視化に。
# 追加する場所
👉【ここに対象箇所を記入:例「トップのヒーローセクション」「お問い合わせボタン」「記事カードの一覧」など】
# 参考実装(この見た目・挙動を再現してください)
【HTML】
<div class="dv-wrap">
<figure class="dv-card">
<figcaption class="dv-head">
<h2 class="dv-title">リアルタイム信号モニタ</h2>
<p class="dv-sub">Canvasで毎フレーム流れる波形(オシロスコープ風)</p>
</figcaption>
<div class="dv-screen">
<canvas id="waveCanvas" class="dv-canvas" role="img" aria-label="リアルタイムに流れる波形"></canvas>
<div class="dv-readout">
<span class="dv-led" id="waveLed"></span>
<span id="waveHz" class="dv-hz">— Hz</span>
</div>
</div>
<div class="dv-ctrl">
<button id="wavePause" class="dv-btn" type="button" aria-pressed="false">一時停止</button>
</div>
</figure>
</div>
【CSS】
:root {
--dv-radius: 18px;
--dv-ink: #d1fae5;
--dv-sub: #6ee7b7;
--dv-screen: #021410;
}
* { 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(800px 500px at 50% -10%, #064e3b 0%, transparent 55%),
linear-gradient(160deg, #03251c, #010d0a);
}
.dv-wrap { width: min(92vw, 720px); padding: 20px; }
.dv-card {
margin: 0;
padding: 20px 22px 18px;
border-radius: var(--dv-radius);
background: rgba(2, 20, 16, 0.6);
border: 1px solid rgba(16, 185, 129, 0.25);
box-shadow: 0 24px 60px -24px rgba(0, 0, 0, 0.8);
}
.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-screen {
position: relative;
border-radius: 12px;
overflow: hidden;
background: var(--dv-screen);
box-shadow: inset 0 0 40px rgba(0, 0, 0, 0.6), inset 0 0 0 1px rgba(16, 185, 129, 0.2);
}
.dv-canvas { display: block; width: 100%; height: 176px; }
.dv-readout {
position: absolute;
top: 10px;
right: 12px;
display: flex;
align-items: center;
gap: 8px;
font-variant-numeric: tabular-nums;
font-size: 13px;
font-weight: 700;
color: #6ee7b7;
text-shadow: 0 0 8px rgba(110, 231, 183, 0.6);
}
.dv-led {
width: 9px;
height: 9px;
border-radius: 50%;
background: #34d399;
box-shadow: 0 0 10px #34d399;
animation: dv-blink 1s steps(2, jump-none) infinite;
}
.dv-led.paused { background: #fbbf24; box-shadow: 0 0 10px #fbbf24; animation: none; }
@keyframes dv-blink { 50% { opacity: 0.25; } }
.dv-ctrl { margin-top: 12px; text-align: center; }
.dv-btn {
padding: 9px 22px;
border: 1px solid rgba(16, 185, 129, 0.45);
border-radius: 999px;
background: rgba(16, 185, 129, 0.12);
color: var(--dv-ink);
font-size: 13px;
font-weight: 600;
cursor: pointer;
transition: background .2s ease, transform .1s ease;
}
.dv-btn:hover { background: rgba(16, 185, 129, 0.25); }
.dv-btn:active { transform: scale(0.96); }
.dv-btn:focus-visible { outline: 2px solid #6ee7b7; outline-offset: 2px; }
@media (prefers-reduced-motion: reduce) {
.dv-led { animation: none; }
}
【JavaScript】
// Canvasで流れる合成波形をリアルタイム描画(グリッド・グロー・周波数表示付き)
(() => {
const canvas = document.getElementById('waveCanvas');
const pauseBtn = document.getElementById('wavePause');
const led = document.getElementById('waveLed');
const hzEl = document.getElementById('waveHz');
if (!canvas) return; // null安全
const ctx = canvas.getContext('2d');
if (!ctx) return;
let dpr = 1, W = 0, H = 0;
function resize() {
dpr = Math.max(1, window.devicePixelRatio || 1);
const rect = canvas.getBoundingClientRect();
W = rect.width; H = rect.height;
canvas.width = Math.round(W * dpr);
canvas.height = Math.round(H * dpr);
ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
}
resize();
window.addEventListener('resize', resize);
const reduceMotion = window.matchMedia('(prefers-reduced-motion: reduce)').matches;
let phase = 0;
let running = !reduceMotion; // reduced-motionなら静止波形
let lastTime = performance.now();
// 複数の正弦波を合成して有機的な波形を作る
function sample(x, t) {
const k = x * 0.018;
return (
Math.sin(k + t * 2.0) * 0.5 +
Math.sin(k * 2.3 + t * 1.3) * 0.28 +
Math.sin(k * 0.6 - t * 0.8) * 0.18
);
}
function drawGrid() {
ctx.strokeStyle = 'rgba(16, 185, 129, 0.12)';
ctx.lineWidth = 1;
const gx = 28, gy = 24;
for (let x = 0; x <= W; x += gx) {
ctx.beginPath(); ctx.moveTo(x, 0); ctx.lineTo(x, H); ctx.stroke();
}
for (let y = 0; y <= H; y += gy) {
ctx.beginPath(); ctx.moveTo(0, y); ctx.lineTo(W, y); ctx.stroke();
}
// 中央基準線を強調
ctx.strokeStyle = 'rgba(16, 185, 129, 0.25)';
ctx.beginPath(); ctx.moveTo(0, H / 2); ctx.lineTo(W, H / 2); ctx.stroke();
}
function drawWave(t) {
const mid = H / 2;
const amp = H * 0.34;
ctx.beginPath();
for (let x = 0; x <= W; x += 2) {
const y = mid + sample(x, t) * amp;
if (x === 0) ctx.moveTo(x, y); else ctx.lineTo(x, y);
}
// グロー付きの発光ライン
ctx.lineJoin = 'round';
ctx.strokeStyle = 'rgba(52, 211, 153, 0.35)';
ctx.lineWidth = 6;
ctx.shadowColor = '#34d399';
ctx.shadowBlur = 14;
ctx.stroke();
ctx.strokeStyle = '#6ee7b7';
ctx.lineWidth = 2;
ctx.shadowBlur = 6;
ctx.stroke();
ctx.shadowBlur = 0;
}
function frame(now) {
const dt = Math.min(0.05, (now - lastTime) / 1000);
lastTime = now;
if (running) phase += dt;
ctx.clearRect(0, 0, W, H);
drawGrid();
drawWave(phase);
// 擬似的な周波数表示(位相速度から算出)
if (hzEl) {
const hz = (42 + Math.sin(phase * 0.5) * 6).toFixed(1);
hzEl.textContent = `${hz} Hz`;
}
requestAnimationFrame(frame);
}
// 一時停止トグル
if (pauseBtn) {
pauseBtn.addEventListener('click', () => {
running = !running;
pauseBtn.textContent = running ? '一時停止' : '再開';
pauseBtn.setAttribute('aria-pressed', String(!running));
if (led) led.classList.toggle('paused', !running);
});
}
if (reduceMotion && pauseBtn) {
pauseBtn.textContent = '再開';
pauseBtn.setAttribute('aria-pressed', 'true');
if (led) led.classList.add('paused');
}
requestAnimationFrame(frame);
})();
# 外部ライブラリ
なし(追加ライブラリ不要)
# 守ってほしいこと
- 既存のHTML構造・レイアウト・デザインを壊さないこと。必要に応じてクラス名・色・サイズを私のサイトに合わせて調整してよい。
- クラス名やidが既存と衝突しないよう、必要なら接頭辞で名前空間を分けること。
- レスポンシブ対応と prefers-reduced-motion への配慮を入れること。
- 私のサイトのフレームワーク(React / Vue / 素のHTML など)に合わせて実装すること。不明な場合は素のHTML/CSS/JSで提示し、組み込み手順も説明すること。
- 変更後の確認手順も簡潔に教えてください。