波形プレイヤー

棒状の波形をクリックした位置から再生できるプレイヤー。再生済みの部分はアンバー色に塗り分けられ、時間経過に応じて色分けの境界が右へ進みます。

#css#js#media

ライブデモ

コード

HTML
<!-- 波形プレイヤー: クリック位置でシークできる棒状波形。再生済み部分を色分けして擬似再生 -->
<div class="stage">
  <section class="mwf-player" data-duration="154" aria-label="波形プレイヤー">
    <div class="mwf-player__head">
      <img class="mwf-player__avatar" src="https://picsum.photos/id/1069/120/120" alt="アーティスト画像: Night Bloom" width="40" height="40">
      <div class="mwf-player__meta">
        <p class="mwf-player__title">夜のドライブ (Waveform Mix)</p>
        <p class="mwf-player__artist">Night Bloom</p>
      </div>
      <button type="button" class="mwf-player__play" aria-label="再生と一時停止を切り替え" aria-pressed="false">
        <svg class="mwf-icon mwf-icon--play" viewBox="0 0 24 24" width="16" height="16" aria-hidden="true"><path d="M8 5v14l11-7z" fill="currentColor"/></svg>
        <svg class="mwf-icon mwf-icon--pause" viewBox="0 0 24 24" width="16" height="16" aria-hidden="true" hidden><path d="M7 5h4v14H7zM13 5h4v14h-4z" fill="currentColor"/></svg>
      </button>
    </div>

    <div class="mwf-player__wave" tabindex="0" role="slider" aria-label="再生位置(波形をクリックしてシーク)" aria-valuemin="0" aria-valuemax="154" aria-valuenow="0" aria-valuetext="0分00秒 / 2分34秒">
      <div class="mwf-player__wave-bg" aria-hidden="true"></div>
      <div class="mwf-player__wave-fg" aria-hidden="true"></div>
    </div>

    <div class="mwf-player__time">
      <span class="mwf-player__current">0:00</span>
      <span class="mwf-player__dur">2:34</span>
    </div>
  </section>
</div>
CSS
/* 波形プレイヤー: 深いティール地にアンバーの波形アクセント */
* { box-sizing: border-box; }

body {
  margin: 0;
  min-height: 100vh;
  padding: 24px;
  display: grid;
  place-items: center;
  font-family: "Hiragino Sans", "Segoe UI", system-ui, -apple-system, sans-serif;
  background:
    radial-gradient(circle at 12% 12%, #123a3a 0%, transparent 46%),
    radial-gradient(circle at 90% 88%, #22343a 0%, transparent 50%),
    #0b1a1c;
  color: #eef6f2;
}

.stage { display: grid; place-items: center; width: 100%; }

.mwf-player {
  width: min(460px, 92vw);
  padding: 22px 24px 20px;
  border-radius: 20px;
  background: linear-gradient(180deg, #12292b, #0e2224);
  box-shadow: 0 22px 52px rgba(0, 0, 0, .5), 0 0 0 1px rgba(255, 255, 255, .04) inset;
}

.mwf-player__head {
  display: flex;
  align-items: center;
  gap: 12px;
  margin-bottom: 18px;
}
.mwf-player__avatar {
  width: 40px;
  height: 40px;
  border-radius: 999px;
  object-fit: cover;
  flex: none;
  box-shadow: 0 6px 16px rgba(0, 0, 0, .4);
}
.mwf-player__meta { min-width: 0; flex: 1; }
.mwf-player__title {
  margin: 0 0 2px;
  font-size: 13.5px;
  font-weight: 700;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.mwf-player__artist { margin: 0; font-size: 11.5px; color: #86a49e; }

.mwf-player__play {
  flex: none;
  width: 36px;
  height: 36px;
  display: grid;
  place-items: center;
  border: none;
  border-radius: 999px;
  color: #0e2224;
  background: linear-gradient(135deg, #ffcf7a, #ff9f5a);
  cursor: pointer;
  box-shadow: 0 8px 18px rgba(255, 159, 90, .3);
  transition: transform .18s cubic-bezier(.22, 1, .36, 1), box-shadow .18s ease;
}
.mwf-player__play svg { transform: translateX(1px); }
.mwf-player__play:hover { transform: translateY(-2px); box-shadow: 0 12px 24px rgba(255, 159, 90, .4); }
.mwf-player__play:active { transform: translateY(0) scale(.94); }
.mwf-icon--pause[hidden] { display: none; }

.mwf-player__wave {
  position: relative;
  height: 64px;
  cursor: pointer;
  outline-offset: 6px;
}
.mwf-player__wave-bg,
.mwf-player__wave-fg {
  position: absolute;
  inset: 0;
  display: flex;
  align-items: center;
  gap: 2.5px;
  pointer-events: none;
}
.mwf-player__wave-fg {
  width: 0%;
  overflow: hidden;
}
.mwf-bar {
  flex: none;
  width: 3px;
  border-radius: 2px;
}
.mwf-player__wave-bg .mwf-bar { background: rgba(238, 246, 242, .22); }
.mwf-player__wave-fg .mwf-bar { background: linear-gradient(180deg, #ffcf7a, #ff9f5a); }

.mwf-player__time {
  display: flex;
  justify-content: space-between;
  margin-top: 8px;
  font-size: 11px;
  font-variant-numeric: tabular-nums;
  color: #86a49e;
}

@media (prefers-reduced-motion: reduce) {
  .mwf-player__play { transition: none; }
}
JavaScript
// 波形プレイヤー: 実音声は使わず、決定的な波形バーを生成しクリック位置でシーク、タイマーで擬似再生する
(() => {
  const player = document.querySelector('.mwf-player');
  if (!player) return; // null安全

  const wave = player.querySelector('.mwf-player__wave');
  const waveBg = player.querySelector('.mwf-player__wave-bg');
  const waveFg = player.querySelector('.mwf-player__wave-fg');
  const playBtn = player.querySelector('.mwf-player__play');
  const curEl = player.querySelector('.mwf-player__current');
  const durEl = player.querySelector('.mwf-player__dur');
  const iconPlay = playBtn ? playBtn.querySelector('.mwf-icon--play') : null;
  const iconPause = playBtn ? playBtn.querySelector('.mwf-icon--pause') : null;

  if (!wave || !waveBg || !waveFg || !curEl || !durEl) return;

  const reduceMotion = window.matchMedia ? window.matchMedia('(prefers-reduced-motion: reduce)').matches : false;

  const DURATION = parseFloat(player.dataset.duration) || 154;
  const TICK_MS = 200;
  const SPEED = 2.4; // デモとして体感しやすい速さで擬似進行させる倍率
  const BAR_COUNT = 48;

  // 決定的な波形(毎回同じ形になるよう、乱数でなく合成サインカーブから高さを算出)
  const HEIGHTS = Array.from({ length: BAR_COUNT }, (_, i) => {
    const a = Math.sin(i * 0.42) * 0.5 + 0.5;
    const b = Math.sin(i * 0.13 + 1.4) * 0.5 + 0.5;
    const v = a * 0.6 + b * 0.4;
    return Math.max(0.14, Math.min(1, v));
  });

  function buildBars(container) {
    const frag = document.createDocumentFragment();
    HEIGHTS.forEach((h) => {
      const bar = document.createElement('span');
      bar.className = 'mwf-bar';
      bar.style.height = `${Math.round(h * 100)}%`;
      frag.appendChild(bar);
    });
    container.appendChild(frag);
  }
  buildBars(waveBg);
  buildBars(waveFg);

  const state = { current: 0, playing: false };

  const fmt = (s) => {
    const t = Math.max(0, Math.round(s));
    const m = Math.floor(t / 60);
    const sec = t % 60;
    return `${m}:${String(sec).padStart(2, '0')}`;
  };

  if (durEl) durEl.textContent = fmt(DURATION);

  let timer = null;

  function render() {
    const frac = Math.min(1, state.current / DURATION);
    waveFg.style.width = `${frac * 100}%`;
    wave.setAttribute('aria-valuenow', String(Math.round(state.current)));
    wave.setAttribute('aria-valuetext', `${fmt(state.current)} / ${fmt(DURATION)}`);
    curEl.textContent = fmt(state.current);
  }

  function setPlaying(next) {
    state.playing = next;
    player.classList.toggle('is-playing', next);
    if (playBtn) playBtn.setAttribute('aria-pressed', String(next));
    if (iconPlay) iconPlay.hidden = next;
    if (iconPause) iconPause.hidden = !next;

    clearInterval(timer);
    if (next) {
      timer = setInterval(() => {
        state.current += (TICK_MS / 1000) * SPEED;
        if (state.current >= DURATION) state.current = 0; // カードとして常に生きて見えるようループ再生
        render();
      }, TICK_MS);
    }
  }

  function togglePlay() { setPlaying(!state.playing); }

  function seekToFraction(frac) {
    state.current = Math.min(DURATION, Math.max(0, frac * DURATION));
    render();
  }
  function fracFromEvent(e) {
    const r = wave.getBoundingClientRect();
    const x = (e.touches && e.touches[0] ? e.touches[0].clientX : e.clientX) - r.left;
    return r.width ? Math.min(1, Math.max(0, x / r.width)) : 0;
  }

  if (playBtn) playBtn.addEventListener('click', togglePlay);

  let dragging = false;
  wave.addEventListener('pointerdown', (e) => {
    dragging = true;
    seekToFraction(fracFromEvent(e));
    wave.setPointerCapture && e.pointerId != null && wave.setPointerCapture(e.pointerId);
  });
  wave.addEventListener('pointermove', (e) => { if (dragging) seekToFraction(fracFromEvent(e)); });
  window.addEventListener('pointerup', () => { dragging = false; });
  wave.addEventListener('keydown', (e) => {
    if (e.key === 'ArrowRight') { state.current = Math.min(DURATION, state.current + 5); render(); }
    else if (e.key === 'ArrowLeft') { state.current = Math.max(0, state.current - 5); render(); }
    else if (e.key === 'Home') { state.current = 0; render(); }
    else if (e.key === 'End') { state.current = DURATION; render(); }
    else return;
    e.preventDefault();
  });

  render();
  // カード上でも「生きている」ことが伝わるよう既定で再生。モーション低減設定時は静止状態から開始
  if (!reduceMotion) setPlaying(true);
})();

🤖 AIエージェント用プロンプト

このままコピーしてAIに貼り付け「追加する場所」だけ書き換えればOK
あなたは熟練のフロントエンドエンジニアです。私のWebサイトに「波形プレイヤー」の効果を追加してください。

# 追加してほしい効果
波形プレイヤー(チャット & メディア)
棒状の波形をクリックした位置から再生できるプレイヤー。再生済みの部分はアンバー色に塗り分けられ、時間経過に応じて色分けの境界が右へ進みます。

# 追加する場所
👉【ここに対象箇所を記入:例「トップのヒーローセクション」「お問い合わせボタン」「記事カードの一覧」など】

# 参考実装(この見た目・挙動を再現してください)
【HTML】
<!-- 波形プレイヤー: クリック位置でシークできる棒状波形。再生済み部分を色分けして擬似再生 -->
<div class="stage">
  <section class="mwf-player" data-duration="154" aria-label="波形プレイヤー">
    <div class="mwf-player__head">
      <img class="mwf-player__avatar" src="https://picsum.photos/id/1069/120/120" alt="アーティスト画像: Night Bloom" width="40" height="40">
      <div class="mwf-player__meta">
        <p class="mwf-player__title">夜のドライブ (Waveform Mix)</p>
        <p class="mwf-player__artist">Night Bloom</p>
      </div>
      <button type="button" class="mwf-player__play" aria-label="再生と一時停止を切り替え" aria-pressed="false">
        <svg class="mwf-icon mwf-icon--play" viewBox="0 0 24 24" width="16" height="16" aria-hidden="true"><path d="M8 5v14l11-7z" fill="currentColor"/></svg>
        <svg class="mwf-icon mwf-icon--pause" viewBox="0 0 24 24" width="16" height="16" aria-hidden="true" hidden><path d="M7 5h4v14H7zM13 5h4v14h-4z" fill="currentColor"/></svg>
      </button>
    </div>

    <div class="mwf-player__wave" tabindex="0" role="slider" aria-label="再生位置(波形をクリックしてシーク)" aria-valuemin="0" aria-valuemax="154" aria-valuenow="0" aria-valuetext="0分00秒 / 2分34秒">
      <div class="mwf-player__wave-bg" aria-hidden="true"></div>
      <div class="mwf-player__wave-fg" aria-hidden="true"></div>
    </div>

    <div class="mwf-player__time">
      <span class="mwf-player__current">0:00</span>
      <span class="mwf-player__dur">2:34</span>
    </div>
  </section>
</div>

【CSS】
/* 波形プレイヤー: 深いティール地にアンバーの波形アクセント */
* { box-sizing: border-box; }

body {
  margin: 0;
  min-height: 100vh;
  padding: 24px;
  display: grid;
  place-items: center;
  font-family: "Hiragino Sans", "Segoe UI", system-ui, -apple-system, sans-serif;
  background:
    radial-gradient(circle at 12% 12%, #123a3a 0%, transparent 46%),
    radial-gradient(circle at 90% 88%, #22343a 0%, transparent 50%),
    #0b1a1c;
  color: #eef6f2;
}

.stage { display: grid; place-items: center; width: 100%; }

.mwf-player {
  width: min(460px, 92vw);
  padding: 22px 24px 20px;
  border-radius: 20px;
  background: linear-gradient(180deg, #12292b, #0e2224);
  box-shadow: 0 22px 52px rgba(0, 0, 0, .5), 0 0 0 1px rgba(255, 255, 255, .04) inset;
}

.mwf-player__head {
  display: flex;
  align-items: center;
  gap: 12px;
  margin-bottom: 18px;
}
.mwf-player__avatar {
  width: 40px;
  height: 40px;
  border-radius: 999px;
  object-fit: cover;
  flex: none;
  box-shadow: 0 6px 16px rgba(0, 0, 0, .4);
}
.mwf-player__meta { min-width: 0; flex: 1; }
.mwf-player__title {
  margin: 0 0 2px;
  font-size: 13.5px;
  font-weight: 700;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.mwf-player__artist { margin: 0; font-size: 11.5px; color: #86a49e; }

.mwf-player__play {
  flex: none;
  width: 36px;
  height: 36px;
  display: grid;
  place-items: center;
  border: none;
  border-radius: 999px;
  color: #0e2224;
  background: linear-gradient(135deg, #ffcf7a, #ff9f5a);
  cursor: pointer;
  box-shadow: 0 8px 18px rgba(255, 159, 90, .3);
  transition: transform .18s cubic-bezier(.22, 1, .36, 1), box-shadow .18s ease;
}
.mwf-player__play svg { transform: translateX(1px); }
.mwf-player__play:hover { transform: translateY(-2px); box-shadow: 0 12px 24px rgba(255, 159, 90, .4); }
.mwf-player__play:active { transform: translateY(0) scale(.94); }
.mwf-icon--pause[hidden] { display: none; }

.mwf-player__wave {
  position: relative;
  height: 64px;
  cursor: pointer;
  outline-offset: 6px;
}
.mwf-player__wave-bg,
.mwf-player__wave-fg {
  position: absolute;
  inset: 0;
  display: flex;
  align-items: center;
  gap: 2.5px;
  pointer-events: none;
}
.mwf-player__wave-fg {
  width: 0%;
  overflow: hidden;
}
.mwf-bar {
  flex: none;
  width: 3px;
  border-radius: 2px;
}
.mwf-player__wave-bg .mwf-bar { background: rgba(238, 246, 242, .22); }
.mwf-player__wave-fg .mwf-bar { background: linear-gradient(180deg, #ffcf7a, #ff9f5a); }

.mwf-player__time {
  display: flex;
  justify-content: space-between;
  margin-top: 8px;
  font-size: 11px;
  font-variant-numeric: tabular-nums;
  color: #86a49e;
}

@media (prefers-reduced-motion: reduce) {
  .mwf-player__play { transition: none; }
}

【JavaScript】
// 波形プレイヤー: 実音声は使わず、決定的な波形バーを生成しクリック位置でシーク、タイマーで擬似再生する
(() => {
  const player = document.querySelector('.mwf-player');
  if (!player) return; // null安全

  const wave = player.querySelector('.mwf-player__wave');
  const waveBg = player.querySelector('.mwf-player__wave-bg');
  const waveFg = player.querySelector('.mwf-player__wave-fg');
  const playBtn = player.querySelector('.mwf-player__play');
  const curEl = player.querySelector('.mwf-player__current');
  const durEl = player.querySelector('.mwf-player__dur');
  const iconPlay = playBtn ? playBtn.querySelector('.mwf-icon--play') : null;
  const iconPause = playBtn ? playBtn.querySelector('.mwf-icon--pause') : null;

  if (!wave || !waveBg || !waveFg || !curEl || !durEl) return;

  const reduceMotion = window.matchMedia ? window.matchMedia('(prefers-reduced-motion: reduce)').matches : false;

  const DURATION = parseFloat(player.dataset.duration) || 154;
  const TICK_MS = 200;
  const SPEED = 2.4; // デモとして体感しやすい速さで擬似進行させる倍率
  const BAR_COUNT = 48;

  // 決定的な波形(毎回同じ形になるよう、乱数でなく合成サインカーブから高さを算出)
  const HEIGHTS = Array.from({ length: BAR_COUNT }, (_, i) => {
    const a = Math.sin(i * 0.42) * 0.5 + 0.5;
    const b = Math.sin(i * 0.13 + 1.4) * 0.5 + 0.5;
    const v = a * 0.6 + b * 0.4;
    return Math.max(0.14, Math.min(1, v));
  });

  function buildBars(container) {
    const frag = document.createDocumentFragment();
    HEIGHTS.forEach((h) => {
      const bar = document.createElement('span');
      bar.className = 'mwf-bar';
      bar.style.height = `${Math.round(h * 100)}%`;
      frag.appendChild(bar);
    });
    container.appendChild(frag);
  }
  buildBars(waveBg);
  buildBars(waveFg);

  const state = { current: 0, playing: false };

  const fmt = (s) => {
    const t = Math.max(0, Math.round(s));
    const m = Math.floor(t / 60);
    const sec = t % 60;
    return `${m}:${String(sec).padStart(2, '0')}`;
  };

  if (durEl) durEl.textContent = fmt(DURATION);

  let timer = null;

  function render() {
    const frac = Math.min(1, state.current / DURATION);
    waveFg.style.width = `${frac * 100}%`;
    wave.setAttribute('aria-valuenow', String(Math.round(state.current)));
    wave.setAttribute('aria-valuetext', `${fmt(state.current)} / ${fmt(DURATION)}`);
    curEl.textContent = fmt(state.current);
  }

  function setPlaying(next) {
    state.playing = next;
    player.classList.toggle('is-playing', next);
    if (playBtn) playBtn.setAttribute('aria-pressed', String(next));
    if (iconPlay) iconPlay.hidden = next;
    if (iconPause) iconPause.hidden = !next;

    clearInterval(timer);
    if (next) {
      timer = setInterval(() => {
        state.current += (TICK_MS / 1000) * SPEED;
        if (state.current >= DURATION) state.current = 0; // カードとして常に生きて見えるようループ再生
        render();
      }, TICK_MS);
    }
  }

  function togglePlay() { setPlaying(!state.playing); }

  function seekToFraction(frac) {
    state.current = Math.min(DURATION, Math.max(0, frac * DURATION));
    render();
  }
  function fracFromEvent(e) {
    const r = wave.getBoundingClientRect();
    const x = (e.touches && e.touches[0] ? e.touches[0].clientX : e.clientX) - r.left;
    return r.width ? Math.min(1, Math.max(0, x / r.width)) : 0;
  }

  if (playBtn) playBtn.addEventListener('click', togglePlay);

  let dragging = false;
  wave.addEventListener('pointerdown', (e) => {
    dragging = true;
    seekToFraction(fracFromEvent(e));
    wave.setPointerCapture && e.pointerId != null && wave.setPointerCapture(e.pointerId);
  });
  wave.addEventListener('pointermove', (e) => { if (dragging) seekToFraction(fracFromEvent(e)); });
  window.addEventListener('pointerup', () => { dragging = false; });
  wave.addEventListener('keydown', (e) => {
    if (e.key === 'ArrowRight') { state.current = Math.min(DURATION, state.current + 5); render(); }
    else if (e.key === 'ArrowLeft') { state.current = Math.max(0, state.current - 5); render(); }
    else if (e.key === 'Home') { state.current = 0; render(); }
    else if (e.key === 'End') { state.current = DURATION; render(); }
    else return;
    e.preventDefault();
  });

  render();
  // カード上でも「生きている」ことが伝わるよう既定で再生。モーション低減設定時は静止状態から開始
  if (!reduceMotion) setPlaying(true);
})();

# 外部ライブラリ
なし(追加ライブラリ不要)

# 守ってほしいこと
- 既存のHTML構造・レイアウト・デザインを壊さないこと。必要に応じてクラス名・色・サイズを私のサイトに合わせて調整してよい。
- クラス名やidが既存と衝突しないよう、必要なら接頭辞で名前空間を分けること。
- レスポンシブ対応と prefers-reduced-motion への配慮を入れること。
- 私のサイトのフレームワーク(React / Vue / 素のHTML など)に合わせて実装すること。不明な場合は素のHTML/CSS/JSで提示し、組み込み手順も説明すること。
- 変更後の確認手順も簡潔に教えてください。