nijivoice_sample / script.js
litagin's picture
init
ffcebc2
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')}`;
}
});