Spaces:
Running
Running
File size: 7,447 Bytes
ffcebc2 |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 |
document.addEventListener('DOMContentLoaded', () => {
const gridContainer = document.getElementById('character-grid');
const audioElement = document.getElementById('main-audio');
// 新しい右カラムの要素
const mainPlayBtn = document.getElementById('main-play-btn');
const progressBar = document.getElementById('progress-bar');
const currentTimeEl = document.getElementById('current-time');
const durationEl = document.getElementById('duration');
const previewImage = document.getElementById('preview-image');
const previewName = document.getElementById('preview-name');
const previewNameReading = document.getElementById('preview-name-reading');
const previewMeta = document.getElementById('preview-meta');
let voiceActors = [];
// ファイル名の指定
const jsonFile = 'voice_actors_local.json';
// JSONデータの読み込み
fetch(jsonFile)
.then(response => {
if (!response.ok) {
return fetch('voice_actors.json').then(res => res.json());
}
return response.json();
})
.then(data => {
voiceActors = data;
renderGrid();
// 初期選択
if (voiceActors.length > 0) {
selectActor(0);
}
})
.catch(error => {
console.error('Error:', error);
gridContainer.innerHTML = '<div class="col-span-full text-center text-red-500 py-10">データの読み込みに失敗しました。</div>';
});
// グリッドの描画
function renderGrid() {
gridContainer.innerHTML = '';
voiceActors.forEach((actor, index) => {
const card = document.createElement('div');
card.className = `voice-card bg-white rounded-2xl overflow-hidden border-2 border-transparent transition-all duration-200 cursor-pointer hover:shadow-lg`;
card.dataset.index = index;
const tagsHtml = actor.tags ? actor.tags.slice(0, 3).map(tag =>
`<span class="bg-black/20 backdrop-blur-sm text-white text-[10px] font-bold px-2 py-0.5 rounded">${tag}</span>`
).join('') : '';
card.innerHTML = `
<div class="card-image-wrapper bg-gray-200 relative pt-[150%]">
<img src="${actor.thumbnail_url || actor.image_url}" alt="${actor.name}" loading="lazy" class="absolute top-0 left-0 w-full h-full object-cover transition-transform duration-300 hover:scale-105">
<div class="absolute inset-0 bg-gradient-to-t from-black/80 via-black/20 to-transparent flex flex-col justify-end p-3 md:p-4 pointer-events-none">
<p class="text-[10px] text-gray-300 leading-tight mb-0.5">${actor.nameReading || ''}</p>
<h3 class="text-base md:text-lg font-bold text-white leading-tight mb-1">${actor.name}</h3>
<p class="text-[10px] md:text-xs text-gray-200 mb-2 font-medium">${actor.age}歳</p>
<div class="flex flex-wrap gap-1">
${tagsHtml}
</div>
</div>
</div>
<div class="p-3 border-t border-gray-100 flex justify-between items-center bg-white">
<span class="text-xs font-bold text-gray-500 flex items-center gap-1">
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M14.752 11.168l-3.197-2.132A1 1 0 0010 9.87v4.263a1 1 0 001.555.832l3.197-2.132a1 1 0 000-1.664z"></path><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 12a9 9 0 11-18 0 9 9 0 0118 0z"></path></svg>
試聴
</span>
</div>
`;
// クリックイベント
card.addEventListener('click', () => {
selectActor(index);
// クリック時に再生したい場合はここを有効化
playAudio(actor.audio_url);
});
gridContainer.appendChild(card);
});
}
// キャラクター選択処理
function selectActor(index) {
const actor = voiceActors[index];
// グリッドの選択状態更新
document.querySelectorAll('.voice-card').forEach(el => el.classList.remove('border-indigo-500', 'shadow-xl'));
const selectedCard = gridContainer.children[index];
if(selectedCard) selectedCard.classList.add('border-indigo-500', 'shadow-xl');
// 右カラムの更新
previewImage.src = actor.image_url;
previewName.textContent = actor.name;
previewNameReading.textContent = actor.nameReading || '';
previewMeta.textContent = `${actor.age}歳`;
// 音声ソースの更新
if (audioElement.src !== new URL(actor.audio_url, document.baseURI).href) {
audioElement.src = actor.audio_url;
resetPlayerUI();
}
}
// オーディオ制御
function playAudio(url) {
const absoluteUrl = new URL(url, document.baseURI).href;
if(audioElement.src !== absoluteUrl) {
audioElement.src = url;
}
audioElement.play().catch(e => console.log("再生エラー:", e));
updateBtnState(true);
}
// メインボタンクリック
mainPlayBtn.addEventListener('click', () => {
if (audioElement.paused) {
audioElement.play();
} else {
audioElement.pause();
}
});
audioElement.addEventListener('play', () => updateBtnState(true));
audioElement.addEventListener('pause', () => updateBtnState(false));
audioElement.addEventListener('ended', () => updateBtnState(false));
audioElement.addEventListener('timeupdate', () => {
if (audioElement.duration) {
const percent = (audioElement.currentTime / audioElement.duration) * 100;
progressBar.style.width = `${percent}%`;
currentTimeEl.textContent = formatTime(audioElement.currentTime);
durationEl.textContent = formatTime(audioElement.duration);
}
});
audioElement.addEventListener('loadedmetadata', () => {
durationEl.textContent = formatTime(audioElement.duration);
});
function updateBtnState(isPlaying) {
const textSpan = mainPlayBtn.lastChild; // ボタンのテキストノード
if (isPlaying) {
textSpan.textContent = " 停止する";
mainPlayBtn.classList.add('bg-indigo-600');
mainPlayBtn.classList.remove('bg-gray-900');
} else {
textSpan.textContent = " サンプルボイスを再生";
mainPlayBtn.classList.remove('bg-indigo-600');
mainPlayBtn.classList.add('bg-gray-900');
}
}
function resetPlayerUI() {
progressBar.style.width = '0%';
currentTimeEl.textContent = '0:00';
durationEl.textContent = '0:00';
updateBtnState(false);
}
function formatTime(seconds) {
if (!seconds || isNaN(seconds)) return "0:00";
const m = Math.floor(seconds / 60);
const s = Math.floor(seconds % 60);
return `${m}:${s.toString().padStart(2, '0')}`;
}
}); |