音楽プレイヤー
回転するジャケット写真と曲名・アーティスト表示、シークバーと前後の曲送りを備えた音楽プレイヤー。再生中はイコライザー風のバーが小さく揺れて動きます。
ライブデモ
コード
HTML
<!-- 音楽プレイヤー: ジャケット+曲情報+シークバー+前後送り。3曲を内部で送り再生する -->
<div class="stage">
<section class="ma-player" aria-label="音楽プレイヤー">
<div class="ma-player__art-wrap">
<img class="ma-player__art" src="https://picsum.photos/id/1062/300/300" alt="アルバムアート: 花明かりの午後" width="150" height="150">
<span class="ma-player__eq" aria-hidden="true"><i></i><i></i><i></i><i></i></span>
</div>
<div class="ma-player__info">
<p class="ma-player__track">花明かりの午後</p>
<p class="ma-player__artist">yura</p>
</div>
<div class="ma-player__bar" tabindex="0" role="slider" aria-label="再生位置" aria-valuemin="0" aria-valuemax="195" aria-valuenow="0" aria-valuetext="0分00秒 / 3分15秒">
<div class="ma-player__bar-track">
<div class="ma-player__bar-fill"></div>
<div class="ma-player__bar-handle"></div>
</div>
</div>
<div class="ma-player__time">
<span class="ma-player__current">0:00</span>
<span class="ma-player__dur">3:15</span>
</div>
<div class="ma-player__controls">
<button type="button" class="ma-player__btn ma-player__prev" aria-label="前の曲">
<svg viewBox="0 0 24 24" width="18" height="18" aria-hidden="true"><path d="M6 6h2v12H6zM19 6l-9 6 9 6z" fill="currentColor"/></svg>
</button>
<button type="button" class="ma-player__btn ma-player__play" aria-label="再生と一時停止を切り替え" aria-pressed="false">
<svg class="ma-icon ma-icon--play" viewBox="0 0 24 24" width="22" height="22" aria-hidden="true"><path d="M8 5v14l11-7z" fill="currentColor"/></svg>
<svg class="ma-icon ma-icon--pause" viewBox="0 0 24 24" width="22" height="22" aria-hidden="true" hidden><path d="M7 5h4v14H7zM13 5h4v14h-4z" fill="currentColor"/></svg>
</button>
<button type="button" class="ma-player__btn ma-player__next" aria-label="次の曲">
<svg viewBox="0 0 24 24" width="18" height="18" aria-hidden="true"><path d="M16 6h2v12h-2zM5 6l9 6-9 6z" fill="currentColor"/></svg>
</button>
</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 14% 12%, #ffe7d1 0%, transparent 46%),
radial-gradient(circle at 88% 18%, #f4dcec 0%, transparent 48%),
radial-gradient(circle at 50% 100%, #e7f0e6 0%, transparent 55%),
#fbf7f2;
color: #2c2620;
}
.stage { display: grid; place-items: center; width: 100%; }
.ma-player {
width: min(320px, 90vw);
padding: 26px 26px 22px;
border-radius: 26px;
background: #fffdfb;
box-shadow: 0 20px 50px rgba(90, 62, 35, .14);
display: grid;
justify-items: center;
gap: 4px;
}
.ma-player__art-wrap {
position: relative;
width: 150px;
height: 150px;
margin-bottom: 14px;
}
.ma-player__art {
width: 150px;
height: 150px;
border-radius: 999px;
object-fit: cover;
display: block;
box-shadow: 0 14px 34px rgba(90, 62, 35, .28), 0 0 0 6px #fffdfb, 0 0 0 8px rgba(90, 62, 35, .1);
animation: ma-spin 9s linear infinite;
animation-play-state: paused;
}
.ma-player.is-playing .ma-player__art { animation-play-state: running; }
@keyframes ma-spin { to { transform: rotate(360deg); } }
.ma-player__eq {
position: absolute;
right: -2px;
bottom: 2px;
display: flex;
align-items: flex-end;
gap: 3px;
height: 18px;
padding: 5px 6px 4px;
border-radius: 999px;
background: rgba(44, 38, 32, .82);
opacity: 0;
transition: opacity .25s ease;
}
.ma-player.is-playing .ma-player__eq { opacity: 1; }
.ma-player__eq i {
display: block;
width: 2.5px;
border-radius: 2px;
background: #ffd9a0;
animation: ma-bounce 1s ease-in-out infinite;
}
.ma-player__eq i:nth-child(1) { height: 5px; animation-delay: -.6s; }
.ma-player__eq i:nth-child(2) { height: 9px; animation-delay: -.3s; }
.ma-player__eq i:nth-child(3) { height: 6px; animation-delay: -.9s; }
.ma-player__eq i:nth-child(4) { height: 8px; animation-delay: -.15s; }
@keyframes ma-bounce { 0%, 100% { transform: scaleY(.4); } 50% { transform: scaleY(1.2); } }
.ma-player__info { text-align: center; margin-bottom: 14px; }
.ma-player__track { margin: 0 0 3px; font-size: 16px; font-weight: 800; letter-spacing: .01em; }
.ma-player__artist { margin: 0; font-size: 12.5px; color: #9a8c78; }
.ma-player__bar {
width: 100%;
padding: 8px 0;
cursor: pointer;
outline-offset: 4px;
}
.ma-player__bar-track {
position: relative;
height: 4px;
border-radius: 999px;
background: #efe6da;
}
.ma-player__bar-fill {
position: absolute;
inset: 0 auto 0 0;
width: 0%;
border-radius: 999px;
background: linear-gradient(90deg, #f7b071, #ef8a6b);
}
.ma-player__bar-handle {
position: absolute;
top: 50%;
left: 0%;
width: 12px;
height: 12px;
margin-left: -6px;
border-radius: 999px;
background: #fff;
box-shadow: 0 2px 6px rgba(90, 62, 35, .35), 0 0 0 2px #ef8a6b inset;
transform: translateY(-50%) scale(0);
transition: transform .15s ease;
}
.ma-player__bar:hover .ma-player__bar-handle,
.ma-player__bar:focus-visible .ma-player__bar-handle { transform: translateY(-50%) scale(1); }
.ma-player__time {
width: 100%;
display: flex;
justify-content: space-between;
font-size: 11px;
font-variant-numeric: tabular-nums;
color: #9a8c78;
margin-bottom: 16px;
}
.ma-player__controls {
display: flex;
align-items: center;
gap: 20px;
}
.ma-player__btn {
display: grid;
place-items: center;
border: none;
background: transparent;
color: #6b5c47;
cursor: pointer;
border-radius: 999px;
transition: color .18s ease, transform .12s ease, background .18s ease;
}
.ma-player__prev, .ma-player__next { width: 34px; height: 34px; }
.ma-player__prev:hover, .ma-player__next:hover { color: #ef8a6b; background: rgba(239, 138, 107, .1); }
.ma-player__play {
width: 54px;
height: 54px;
color: #fff;
background: linear-gradient(135deg, #f7b071, #ef8a6b);
box-shadow: 0 12px 26px rgba(239, 138, 107, .4);
}
.ma-player__play:hover { transform: translateY(-2px); box-shadow: 0 16px 32px rgba(239, 138, 107, .5); }
.ma-player__btn:active { transform: scale(.92); }
.ma-icon--pause[hidden] { display: none; }
@media (prefers-reduced-motion: reduce) {
.ma-player__art { animation: none; }
.ma-player__eq i { animation: none; }
.ma-player__btn, .ma-player__bar-handle { transition: none; }
}
JavaScript
// 音楽プレイヤー: 実音声は使わず、タイマーで進捗を擬似的に進める。3曲を内部で送り再生
(() => {
const player = document.querySelector('.ma-player');
if (!player) return; // null安全
const art = player.querySelector('.ma-player__art');
const trackEl = player.querySelector('.ma-player__track');
const artistEl = player.querySelector('.ma-player__artist');
const bar = player.querySelector('.ma-player__bar');
const barFill = player.querySelector('.ma-player__bar-fill');
const barHandle = player.querySelector('.ma-player__bar-handle');
const curEl = player.querySelector('.ma-player__current');
const durEl = player.querySelector('.ma-player__dur');
const playBtn = player.querySelector('.ma-player__play');
const prevBtn = player.querySelector('.ma-player__prev');
const nextBtn = player.querySelector('.ma-player__next');
const iconPlay = playBtn ? playBtn.querySelector('.ma-icon--play') : null;
const iconPause = playBtn ? playBtn.querySelector('.ma-icon--pause') : null;
if (!bar || !barFill || !barHandle || !curEl || !durEl) return;
const reduceMotion = window.matchMedia ? window.matchMedia('(prefers-reduced-motion: reduce)').matches : false;
const TRACKS = [
{ title: '花明かりの午後', artist: 'yura', art: 'https://picsum.photos/id/1062/300/300', duration: 195 },
{ title: '夜行バスのブルー', artist: 'Night Owl Parade', art: 'https://picsum.photos/id/1074/300/300', duration: 221 },
{ title: 'そよ風レコード', artist: 'Sunday Milk', art: 'https://picsum.photos/id/1080/300/300', duration: 168 },
];
const TICK_MS = 200;
const SPEED = 3.6; // デモとして体感しやすい速さで擬似進行させる倍率
const state = { index: 0, 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')}`;
};
let timer = null;
function duration() { return TRACKS[state.index].duration; }
function renderTrack() {
const t = TRACKS[state.index];
if (trackEl) trackEl.textContent = t.title;
if (artistEl) artistEl.textContent = t.artist;
if (art) { art.src = t.art; art.alt = `アルバムアート: ${t.title}`; }
durEl.textContent = fmt(t.duration);
bar.setAttribute('aria-valuemax', String(t.duration));
}
function renderProgress() {
const d = duration();
const frac = d ? Math.min(1, state.current / d) : 0;
barFill.style.width = `${frac * 100}%`;
barHandle.style.left = `${frac * 100}%`;
bar.setAttribute('aria-valuenow', String(Math.round(state.current)));
bar.setAttribute('aria-valuetext', `${fmt(state.current)} / ${fmt(d)}`);
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()) { goTo(state.index + 1); return; }
renderProgress();
}, TICK_MS);
}
}
function goTo(index) {
const len = TRACKS.length;
state.index = ((index % len) + len) % len;
state.current = 0;
renderTrack();
renderProgress();
}
function togglePlay() { setPlaying(!state.playing); }
function seekToFraction(frac) {
state.current = Math.min(duration(), Math.max(0, frac * duration()));
renderProgress();
}
function fracFromEvent(e) {
const r = bar.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);
if (prevBtn) prevBtn.addEventListener('click', () => goTo(state.index - 1));
if (nextBtn) nextBtn.addEventListener('click', () => goTo(state.index + 1));
let dragging = false;
bar.addEventListener('pointerdown', (e) => {
dragging = true;
seekToFraction(fracFromEvent(e));
bar.setPointerCapture && e.pointerId != null && bar.setPointerCapture(e.pointerId);
});
bar.addEventListener('pointermove', (e) => { if (dragging) seekToFraction(fracFromEvent(e)); });
window.addEventListener('pointerup', () => { dragging = false; });
bar.addEventListener('keydown', (e) => {
if (e.key === 'ArrowRight') { state.current = Math.min(duration(), state.current + 5); renderProgress(); }
else if (e.key === 'ArrowLeft') { state.current = Math.max(0, state.current - 5); renderProgress(); }
else if (e.key === 'Home') { state.current = 0; renderProgress(); }
else if (e.key === 'End') { state.current = duration(); renderProgress(); }
else return;
e.preventDefault();
});
renderTrack();
renderProgress();
// カード上でも「生きている」ことが伝わるよう既定で再生。モーション低減設定時は静止状態から開始
if (!reduceMotion) setPlaying(true);
})();
🤖 AIエージェント用プロンプト
このままコピーしてAIに貼り付け「追加する場所」だけ書き換えればOK
あなたは熟練のフロントエンドエンジニアです。私のWebサイトに「音楽プレイヤー」の効果を追加してください。
# 追加してほしい効果
音楽プレイヤー(チャット & メディア)
回転するジャケット写真と曲名・アーティスト表示、シークバーと前後の曲送りを備えた音楽プレイヤー。再生中はイコライザー風のバーが小さく揺れて動きます。
# 追加する場所
👉【ここに対象箇所を記入:例「トップのヒーローセクション」「お問い合わせボタン」「記事カードの一覧」など】
# 参考実装(この見た目・挙動を再現してください)
【HTML】
<!-- 音楽プレイヤー: ジャケット+曲情報+シークバー+前後送り。3曲を内部で送り再生する -->
<div class="stage">
<section class="ma-player" aria-label="音楽プレイヤー">
<div class="ma-player__art-wrap">
<img class="ma-player__art" src="https://picsum.photos/id/1062/300/300" alt="アルバムアート: 花明かりの午後" width="150" height="150">
<span class="ma-player__eq" aria-hidden="true"><i></i><i></i><i></i><i></i></span>
</div>
<div class="ma-player__info">
<p class="ma-player__track">花明かりの午後</p>
<p class="ma-player__artist">yura</p>
</div>
<div class="ma-player__bar" tabindex="0" role="slider" aria-label="再生位置" aria-valuemin="0" aria-valuemax="195" aria-valuenow="0" aria-valuetext="0分00秒 / 3分15秒">
<div class="ma-player__bar-track">
<div class="ma-player__bar-fill"></div>
<div class="ma-player__bar-handle"></div>
</div>
</div>
<div class="ma-player__time">
<span class="ma-player__current">0:00</span>
<span class="ma-player__dur">3:15</span>
</div>
<div class="ma-player__controls">
<button type="button" class="ma-player__btn ma-player__prev" aria-label="前の曲">
<svg viewBox="0 0 24 24" width="18" height="18" aria-hidden="true"><path d="M6 6h2v12H6zM19 6l-9 6 9 6z" fill="currentColor"/></svg>
</button>
<button type="button" class="ma-player__btn ma-player__play" aria-label="再生と一時停止を切り替え" aria-pressed="false">
<svg class="ma-icon ma-icon--play" viewBox="0 0 24 24" width="22" height="22" aria-hidden="true"><path d="M8 5v14l11-7z" fill="currentColor"/></svg>
<svg class="ma-icon ma-icon--pause" viewBox="0 0 24 24" width="22" height="22" aria-hidden="true" hidden><path d="M7 5h4v14H7zM13 5h4v14h-4z" fill="currentColor"/></svg>
</button>
<button type="button" class="ma-player__btn ma-player__next" aria-label="次の曲">
<svg viewBox="0 0 24 24" width="18" height="18" aria-hidden="true"><path d="M16 6h2v12h-2zM5 6l9 6-9 6z" fill="currentColor"/></svg>
</button>
</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 14% 12%, #ffe7d1 0%, transparent 46%),
radial-gradient(circle at 88% 18%, #f4dcec 0%, transparent 48%),
radial-gradient(circle at 50% 100%, #e7f0e6 0%, transparent 55%),
#fbf7f2;
color: #2c2620;
}
.stage { display: grid; place-items: center; width: 100%; }
.ma-player {
width: min(320px, 90vw);
padding: 26px 26px 22px;
border-radius: 26px;
background: #fffdfb;
box-shadow: 0 20px 50px rgba(90, 62, 35, .14);
display: grid;
justify-items: center;
gap: 4px;
}
.ma-player__art-wrap {
position: relative;
width: 150px;
height: 150px;
margin-bottom: 14px;
}
.ma-player__art {
width: 150px;
height: 150px;
border-radius: 999px;
object-fit: cover;
display: block;
box-shadow: 0 14px 34px rgba(90, 62, 35, .28), 0 0 0 6px #fffdfb, 0 0 0 8px rgba(90, 62, 35, .1);
animation: ma-spin 9s linear infinite;
animation-play-state: paused;
}
.ma-player.is-playing .ma-player__art { animation-play-state: running; }
@keyframes ma-spin { to { transform: rotate(360deg); } }
.ma-player__eq {
position: absolute;
right: -2px;
bottom: 2px;
display: flex;
align-items: flex-end;
gap: 3px;
height: 18px;
padding: 5px 6px 4px;
border-radius: 999px;
background: rgba(44, 38, 32, .82);
opacity: 0;
transition: opacity .25s ease;
}
.ma-player.is-playing .ma-player__eq { opacity: 1; }
.ma-player__eq i {
display: block;
width: 2.5px;
border-radius: 2px;
background: #ffd9a0;
animation: ma-bounce 1s ease-in-out infinite;
}
.ma-player__eq i:nth-child(1) { height: 5px; animation-delay: -.6s; }
.ma-player__eq i:nth-child(2) { height: 9px; animation-delay: -.3s; }
.ma-player__eq i:nth-child(3) { height: 6px; animation-delay: -.9s; }
.ma-player__eq i:nth-child(4) { height: 8px; animation-delay: -.15s; }
@keyframes ma-bounce { 0%, 100% { transform: scaleY(.4); } 50% { transform: scaleY(1.2); } }
.ma-player__info { text-align: center; margin-bottom: 14px; }
.ma-player__track { margin: 0 0 3px; font-size: 16px; font-weight: 800; letter-spacing: .01em; }
.ma-player__artist { margin: 0; font-size: 12.5px; color: #9a8c78; }
.ma-player__bar {
width: 100%;
padding: 8px 0;
cursor: pointer;
outline-offset: 4px;
}
.ma-player__bar-track {
position: relative;
height: 4px;
border-radius: 999px;
background: #efe6da;
}
.ma-player__bar-fill {
position: absolute;
inset: 0 auto 0 0;
width: 0%;
border-radius: 999px;
background: linear-gradient(90deg, #f7b071, #ef8a6b);
}
.ma-player__bar-handle {
position: absolute;
top: 50%;
left: 0%;
width: 12px;
height: 12px;
margin-left: -6px;
border-radius: 999px;
background: #fff;
box-shadow: 0 2px 6px rgba(90, 62, 35, .35), 0 0 0 2px #ef8a6b inset;
transform: translateY(-50%) scale(0);
transition: transform .15s ease;
}
.ma-player__bar:hover .ma-player__bar-handle,
.ma-player__bar:focus-visible .ma-player__bar-handle { transform: translateY(-50%) scale(1); }
.ma-player__time {
width: 100%;
display: flex;
justify-content: space-between;
font-size: 11px;
font-variant-numeric: tabular-nums;
color: #9a8c78;
margin-bottom: 16px;
}
.ma-player__controls {
display: flex;
align-items: center;
gap: 20px;
}
.ma-player__btn {
display: grid;
place-items: center;
border: none;
background: transparent;
color: #6b5c47;
cursor: pointer;
border-radius: 999px;
transition: color .18s ease, transform .12s ease, background .18s ease;
}
.ma-player__prev, .ma-player__next { width: 34px; height: 34px; }
.ma-player__prev:hover, .ma-player__next:hover { color: #ef8a6b; background: rgba(239, 138, 107, .1); }
.ma-player__play {
width: 54px;
height: 54px;
color: #fff;
background: linear-gradient(135deg, #f7b071, #ef8a6b);
box-shadow: 0 12px 26px rgba(239, 138, 107, .4);
}
.ma-player__play:hover { transform: translateY(-2px); box-shadow: 0 16px 32px rgba(239, 138, 107, .5); }
.ma-player__btn:active { transform: scale(.92); }
.ma-icon--pause[hidden] { display: none; }
@media (prefers-reduced-motion: reduce) {
.ma-player__art { animation: none; }
.ma-player__eq i { animation: none; }
.ma-player__btn, .ma-player__bar-handle { transition: none; }
}
【JavaScript】
// 音楽プレイヤー: 実音声は使わず、タイマーで進捗を擬似的に進める。3曲を内部で送り再生
(() => {
const player = document.querySelector('.ma-player');
if (!player) return; // null安全
const art = player.querySelector('.ma-player__art');
const trackEl = player.querySelector('.ma-player__track');
const artistEl = player.querySelector('.ma-player__artist');
const bar = player.querySelector('.ma-player__bar');
const barFill = player.querySelector('.ma-player__bar-fill');
const barHandle = player.querySelector('.ma-player__bar-handle');
const curEl = player.querySelector('.ma-player__current');
const durEl = player.querySelector('.ma-player__dur');
const playBtn = player.querySelector('.ma-player__play');
const prevBtn = player.querySelector('.ma-player__prev');
const nextBtn = player.querySelector('.ma-player__next');
const iconPlay = playBtn ? playBtn.querySelector('.ma-icon--play') : null;
const iconPause = playBtn ? playBtn.querySelector('.ma-icon--pause') : null;
if (!bar || !barFill || !barHandle || !curEl || !durEl) return;
const reduceMotion = window.matchMedia ? window.matchMedia('(prefers-reduced-motion: reduce)').matches : false;
const TRACKS = [
{ title: '花明かりの午後', artist: 'yura', art: 'https://picsum.photos/id/1062/300/300', duration: 195 },
{ title: '夜行バスのブルー', artist: 'Night Owl Parade', art: 'https://picsum.photos/id/1074/300/300', duration: 221 },
{ title: 'そよ風レコード', artist: 'Sunday Milk', art: 'https://picsum.photos/id/1080/300/300', duration: 168 },
];
const TICK_MS = 200;
const SPEED = 3.6; // デモとして体感しやすい速さで擬似進行させる倍率
const state = { index: 0, 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')}`;
};
let timer = null;
function duration() { return TRACKS[state.index].duration; }
function renderTrack() {
const t = TRACKS[state.index];
if (trackEl) trackEl.textContent = t.title;
if (artistEl) artistEl.textContent = t.artist;
if (art) { art.src = t.art; art.alt = `アルバムアート: ${t.title}`; }
durEl.textContent = fmt(t.duration);
bar.setAttribute('aria-valuemax', String(t.duration));
}
function renderProgress() {
const d = duration();
const frac = d ? Math.min(1, state.current / d) : 0;
barFill.style.width = `${frac * 100}%`;
barHandle.style.left = `${frac * 100}%`;
bar.setAttribute('aria-valuenow', String(Math.round(state.current)));
bar.setAttribute('aria-valuetext', `${fmt(state.current)} / ${fmt(d)}`);
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()) { goTo(state.index + 1); return; }
renderProgress();
}, TICK_MS);
}
}
function goTo(index) {
const len = TRACKS.length;
state.index = ((index % len) + len) % len;
state.current = 0;
renderTrack();
renderProgress();
}
function togglePlay() { setPlaying(!state.playing); }
function seekToFraction(frac) {
state.current = Math.min(duration(), Math.max(0, frac * duration()));
renderProgress();
}
function fracFromEvent(e) {
const r = bar.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);
if (prevBtn) prevBtn.addEventListener('click', () => goTo(state.index - 1));
if (nextBtn) nextBtn.addEventListener('click', () => goTo(state.index + 1));
let dragging = false;
bar.addEventListener('pointerdown', (e) => {
dragging = true;
seekToFraction(fracFromEvent(e));
bar.setPointerCapture && e.pointerId != null && bar.setPointerCapture(e.pointerId);
});
bar.addEventListener('pointermove', (e) => { if (dragging) seekToFraction(fracFromEvent(e)); });
window.addEventListener('pointerup', () => { dragging = false; });
bar.addEventListener('keydown', (e) => {
if (e.key === 'ArrowRight') { state.current = Math.min(duration(), state.current + 5); renderProgress(); }
else if (e.key === 'ArrowLeft') { state.current = Math.max(0, state.current - 5); renderProgress(); }
else if (e.key === 'Home') { state.current = 0; renderProgress(); }
else if (e.key === 'End') { state.current = duration(); renderProgress(); }
else return;
e.preventDefault();
});
renderTrack();
renderProgress();
// カード上でも「生きている」ことが伝わるよう既定で再生。モーション低減設定時は静止状態から開始
if (!reduceMotion) setPlaying(true);
})();
# 外部ライブラリ
なし(追加ライブラリ不要)
# 守ってほしいこと
- 既存のHTML構造・レイアウト・デザインを壊さないこと。必要に応じてクラス名・色・サイズを私のサイトに合わせて調整してよい。
- クラス名やidが既存と衝突しないよう、必要なら接頭辞で名前空間を分けること。
- レスポンシブ対応と prefers-reduced-motion への配慮を入れること。
- 私のサイトのフレームワーク(React / Vue / 素のHTML など)に合わせて実装すること。不明な場合は素のHTML/CSS/JSで提示し、組み込み手順も説明すること。
- 変更後の確認手順も簡潔に教えてください。