|
<!DOCTYPE html> |
|
<html lang="ja"> |
|
|
|
<head> |
|
<meta charset="UTF-8"> |
|
<script src="https://soiz1-eruda3.hf.space/eruda.js"></script> |
|
<script> |
|
eruda.init(); |
|
</script> |
|
<script> |
|
function isAdmin() { |
|
return document.cookie.includes("admin_access=true"); |
|
} |
|
|
|
if (!isAdmin()) { |
|
|
|
(function(w, d, s, l, i) { |
|
w[l] = w[l] || []; |
|
w[l].push({ |
|
'gtm.start': new Date().getTime(), |
|
event: 'gtm.js' |
|
}); |
|
var f = d.getElementsByTagName(s)[0], |
|
j = d.createElement(s), |
|
dl = l != 'dataLayer' ? '&l=' + l : ''; |
|
j.async = true; |
|
j.src = 'https://www.googletagmanager.com/gtm.js?id=' + i + dl; |
|
f.parentNode.insertBefore(j, f); |
|
})(window, document, 'script', 'dataLayer', 'GTM-NDL2LKLQ'); |
|
|
|
|
|
var gtagScript = document.createElement('script'); |
|
gtagScript.async = true; |
|
gtagScript.src = "https://www.googletagmanager.com/gtag/js?id=G-2M35HBEEVH"; |
|
document.head.appendChild(gtagScript); |
|
|
|
window.dataLayer = window.dataLayer || []; |
|
|
|
function gtag() { |
|
dataLayer.push(arguments); |
|
} |
|
gtag('js', new Date()); |
|
gtag('config', 'G-2M35HBEEVH'); |
|
} else { |
|
console.log("管理者のため、Googleタグはスキップされました。"); |
|
} |
|
</script> |
|
|
|
<script src="https://unpkg.com/draggabilly@2/dist/draggabilly.pkgd.min.js"></script> |
|
|
|
|
|
<script> |
|
document.addEventListener('DOMContentLoaded', function() { |
|
(function() { |
|
const UPDATE_URL = '/update.txt'; |
|
const CHECK_INTERVAL_MS = 30000; |
|
let initialVersion = null; |
|
|
|
|
|
const style = document.createElement('style'); |
|
style.textContent = ` |
|
#update-popup { |
|
position: fixed; |
|
top: 20px; |
|
right: 20px; |
|
background: #fff3cd; |
|
color: #856304; |
|
border: 1px solid #ffeeba; |
|
border-radius: 8px; |
|
padding: 20px 16px 16px 16px; |
|
box-shadow: 0 2px 10px rgba(0,0,0,0.1); |
|
display: none; |
|
z-index: 1000; |
|
font-family: sans-serif; |
|
width: 240px; |
|
} |
|
|
|
#update-popup-close { |
|
position: absolute; |
|
top: 6px; |
|
right: 8px; |
|
background: none; |
|
border: none; |
|
font-size: 16px; |
|
font-weight: bold; |
|
color: #856404; |
|
cursor: pointer; |
|
} |
|
|
|
#update-popup-close:hover { |
|
color: #000; |
|
} |
|
|
|
#update-popup button.reload { |
|
margin-top: 10px; |
|
padding: 6px 12px; |
|
border: none; |
|
background-color: #ffc107; |
|
color: #000; |
|
cursor: pointer; |
|
border-radius: 4px; |
|
width: 100%; |
|
} |
|
|
|
#update-popup button.reload:hover { |
|
background-color: #e0a800; |
|
} |
|
`; |
|
document.head.appendChild(style); |
|
|
|
|
|
const popup = document.createElement('div'); |
|
popup.id = 'update-popup'; |
|
popup.innerHTML = ` |
|
<button id="update-popup-close" aria-label="閉じる">×</button> |
|
<p>ページが新しくなりました。<br>タブの再読み込みが必要です。</p> |
|
<button class="reload">再読み込み</button> |
|
`; |
|
document.body.appendChild(popup); |
|
|
|
|
|
document.getElementById('update-popup-close').addEventListener('click', () => { |
|
popup.style.display = 'none'; |
|
}); |
|
|
|
|
|
popup.querySelector('.reload').addEventListener('click', () => { |
|
location.reload(); |
|
}); |
|
|
|
|
|
async function fetchVersion() { |
|
try { |
|
const res = await fetch(UPDATE_URL + '?t=' + Date.now()); |
|
if (res.ok) return await res.text(); |
|
} catch (err) { |
|
console.warn('update.txt の取得に失敗:', err); |
|
} |
|
return null; |
|
} |
|
|
|
|
|
async function checkForUpdate() { |
|
const latest = await fetchVersion(); |
|
if (initialVersion && latest && latest !== initialVersion) { |
|
popup.style.display = 'block'; |
|
} |
|
} |
|
|
|
|
|
(async () => { |
|
initialVersion = await fetchVersion(); |
|
if (initialVersion) { |
|
setInterval(checkForUpdate, CHECK_INTERVAL_MS); |
|
} |
|
})(); |
|
})(); |
|
}) |
|
</script> |
|
<link rel="icon" type="image/svg+xml" href='data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMjAwIiBoZWlnaHQ9IjIwMCIgdmlld0JveD0iMCAwIDIwMCAyMDAiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgbGFuZz0iamEiPjxkZWZzPiA8bGluZWFyR3JhZGllbnQgaWQ9ImdyYWQxIiB4MT0iMCUiIHkxPSIwJSIgeDI9IjAlIiB5Mj0iMTAwJSI+IDxzdG9wIG9mZnNldD0iMCUiIHN0b3AtY29sb3I9IiMzNDc0ZWIiIC8+IDxzdG9wIG9mZnNldD0iMTAwJSIgc3RvcC1jb2xvcj0iIzM0YThlYiIgLz4gPC9saW5lYXJHcmFkaWVudD4gPC9kZWZzPiA8IS0tIOWQhOS6uuWbvuWkjeS9nOWumuS7tuWkjeaWsOWIl+OBjOaWsOWIl+OBq+OBhOOBq+OBvuOBq+OCi+OCieWbvuWkjeOBjOOCjOOCieOCk+ODq+ODvOODrOODs+ODg+OCiwotLT4gPHBvbHlnb24gcG9pbnRzPSI1MCw1MCA1MCwxNTAgMTUwLDEwMCIgZmlsbD0idXJsKCNncmFkMSkiIHN0cm9rZT0iYmxhY2siIHN0cm9rZS13aWR0aD0iMiIgLz48L3N2Zz4=' /> |
|
<meta charset="UTF-8"> |
|
|
|
<script> |
|
|
|
document.addEventListener("DOMContentLoaded", () => { |
|
document.querySelectorAll("input[type='range']").forEach((slider) => { |
|
const bubble = document.createElement("div"); |
|
bubble.style.position = "absolute"; |
|
bubble.style.background = "#333a"; |
|
bubble.style.color = "#fff"; |
|
bubble.style.padding = "4px 8px"; |
|
bubble.style.borderRadius = "4px"; |
|
bubble.style.fontSize = "12px"; |
|
bubble.style.pointerEvents = "none"; |
|
bubble.style.whiteSpace = "nowrap"; |
|
bubble.style.transform = "translate(-50%, -120%)"; |
|
bubble.style.transition = "opacity 0.1s"; |
|
bubble.style.opacity = "0"; |
|
bubble.style.zIndex = "1000"; |
|
document.body.appendChild(bubble); |
|
|
|
let rect = null; |
|
|
|
slider.addEventListener("mouseenter", () => { |
|
rect = slider.getBoundingClientRect(); |
|
bubble.style.opacity = "1"; |
|
}); |
|
|
|
slider.addEventListener("mouseleave", () => { |
|
bubble.style.opacity = "0"; |
|
}); |
|
|
|
slider.addEventListener("mousemove", (e) => { |
|
if (!rect) rect = slider.getBoundingClientRect(); |
|
const min = parseFloat(slider.min || 0); |
|
const max = parseFloat(slider.max || 100); |
|
const step = parseFloat(slider.step || 1); |
|
const relativeX = e.clientX - rect.left; |
|
const percent = Math.min(Math.max(relativeX / rect.width, 0), 1); |
|
let value = min + (max - min) * percent; |
|
value = Math.round(value / step) * step; |
|
value = Number(value.toFixed(4)); |
|
|
|
|
|
bubble.textContent = value; |
|
|
|
|
|
const pageX = e.pageX; |
|
const pageY = window.scrollY + rect.top; |
|
bubble.style.left = `${pageX}px`; |
|
bubble.style.top = `${pageY}px`; |
|
}); |
|
|
|
window.addEventListener("scroll", () => { |
|
rect = slider.getBoundingClientRect(); |
|
}); |
|
|
|
window.addEventListener("resize", () => { |
|
rect = slider.getBoundingClientRect(); |
|
}); |
|
}); |
|
}); |
|
</script> |
|
<link rel="icon" type="image/svg+xml" href="data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMjAwIiBoZWlnaHQ9IjIwMCIgdmlld0JveD0iMCAwIDIwMCAyMDAiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgbGFuZz0iamEiPjxkZWZzPiA8bGluZWFyR3JhZGllbnQgaWQ9ImdyYWQxIiB4MT0iMCUiIHkxPSIwJSIgeDI9IjAlIiB5Mj0iMTAwJSI+IDxzdG9wIG9mZnNldD0iMCUiIHN0b3AtY29sb3I9IiMzNDc0ZWIiIC8+IDxzdG9wIG9mZnNldD0iMTAwJSIgc3RvcC1jb2xvcj0iIzM0YThlYiIgLz4gPC9saW5lYXJHcmFkaWVudD4gPC9kZWZzPiA8IS0tIOWQhOS6uuWbvuWkjeS9nOWumuS7tuWkjeaWsOWIl+OBjOaWsOWIl+OBq+OBhOOBq+OBvuOBq+OCi+OCieWbvuWkjeOBjOOCjOOCieOCk+ODq+ODvOODrOODs+ODg+OCiwotLT4gPHBvbHlnb24gcG9pbnRzPSI1MCw1MCA1MCwxNTAgMTUwLDEwMCIgZmlsbD0idXJsKCNncmFkMSkiIHN0cm9rZT0iYmxhY2siIHN0cm9rZS13aWR0aD0iMiIgLz48L3N2Zz4="> |
|
<meta name="viewport" content="width=device-width, initial-scale=1.0"> |
|
<link rel="preconnect" href="https://fonts.googleapis.com"> |
|
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin> |
|
<link href="https://fonts.googleapis.com/css2?family=M+PLUS+Rounded+1c&display=swap" rel="stylesheet"> |
|
<title>文化発表会動画プレイヤー</title> |
|
<style> |
|
|
|
.tech-background { |
|
position: fixed; |
|
top: 0; |
|
left: 0; |
|
width: 100%; |
|
height: 100%; |
|
z-index: -2; |
|
background: linear-gradient(135deg, #0f0c29, #302b63, #24243e); |
|
overflow: hidden; |
|
} |
|
|
|
.circuit-line { |
|
position: absolute; |
|
background: rgba(0, 255, 255, 0.1); |
|
box-shadow: 0 0 10px rgba(0, 255, 255, 0.3); |
|
} |
|
|
|
.grid-dot { |
|
position: absolute; |
|
width: 2px; |
|
height: 2px; |
|
background: rgba(0, 255, 255, 0.3); |
|
border-radius: 50%; |
|
} |
|
|
|
.hexagon { |
|
position: absolute; |
|
width: 40px; |
|
height: 23px; |
|
background: rgba(0, 255, 255, 0.05); |
|
border: 1px solid rgba(0, 255, 255, 0.1); |
|
box-shadow: 0 0 5px rgba(0, 255, 255, 0.2); |
|
} |
|
|
|
.hexagon:before, |
|
.hexagon:after { |
|
content: ""; |
|
position: absolute; |
|
width: 0; |
|
border-left: 20px solid transparent; |
|
border-right: 20px solid transparent; |
|
} |
|
|
|
.hexagon:before { |
|
bottom: 100%; |
|
border-bottom: 11.5px solid rgba(0, 255, 255, 0.05); |
|
} |
|
|
|
.hexagon:after { |
|
top: 100%; |
|
width: 0; |
|
border-top: 11.5px solid rgba(0, 255, 255, 0.05); |
|
} |
|
|
|
.pulse { |
|
position: absolute; |
|
width: 10px; |
|
height: 10px; |
|
background: rgba(0, 255, 255, 0.7); |
|
border-radius: 50%; |
|
box-shadow: 0 0 10px 5px rgba(0, 255, 255, 0.5); |
|
animation: pulse 2s infinite; |
|
} |
|
|
|
@keyframes pulse { |
|
0% { |
|
transform: scale(0.8); |
|
opacity: 0.7; |
|
} |
|
|
|
50% { |
|
transform: scale(1.2); |
|
opacity: 1; |
|
} |
|
|
|
100% { |
|
transform: scale(0.8); |
|
opacity: 0.7; |
|
} |
|
} |
|
|
|
@keyframes float { |
|
0% { |
|
transform: translateY(0) rotate(0deg); |
|
} |
|
|
|
50% { |
|
transform: translateY(-20px) rotate(5deg); |
|
} |
|
|
|
100% { |
|
transform: translateY(0) rotate(0deg); |
|
} |
|
} |
|
|
|
|
|
body { |
|
font-family: "M PLUS Rounded 1c", 'Arial', sans-serif; |
|
background-color: rgba(10, 25, 47, 0.8); |
|
color: #e6f1ff; |
|
margin: 0; |
|
padding: 20px; |
|
display: flex; |
|
flex-direction: column; |
|
align-items: center; |
|
min-height: 100vh; |
|
} |
|
|
|
h1 { |
|
color: #a2c2e8; |
|
text-align: center; |
|
margin-bottom: 30px; |
|
border-bottom: 1px solid #64ffda; |
|
padding-bottom: 10px; |
|
width: 100%; |
|
} |
|
|
|
.container { |
|
display: flex; |
|
flex-direction: column; |
|
width: 100%; |
|
max-width: 1000px; |
|
background-color: rgba(17, 34, 64, 0.3); |
|
border-radius: 10px; |
|
padding: 20px; |
|
box-shadow: 0 0 20px rgba(100, 255, 218, 0.2); |
|
backdrop-filter: blur(5px); |
|
border: 1px solid rgba(100, 255, 218, 0.1); |
|
} |
|
|
|
.video-container { |
|
position: relative; |
|
width: 100%; |
|
margin-bottom: 20px; |
|
} |
|
|
|
video { |
|
width: 100%; |
|
border-radius: 5px; |
|
background-color: #000; |
|
display: block; |
|
cursor: pointer; |
|
} |
|
|
|
.video-controls { |
|
background-color: rgba(17, 34, 64, 0.2); |
|
padding: 10px; |
|
border-radius: 0 0 5px 5px; |
|
display: flex; |
|
flex-direction: column; |
|
gap: 10px; |
|
transition: opacity 0.3s; |
|
} |
|
|
|
.progress-container { |
|
width: 100%; |
|
height: 10px; |
|
background-color: #1e2a47; |
|
border-radius: 5px; |
|
cursor: pointer; |
|
position: relative; |
|
} |
|
|
|
.progress-bar { |
|
height: 100%; |
|
background-color: #64ffda; |
|
border-radius: 5px; |
|
width: 0%; |
|
position: relative; |
|
} |
|
|
|
.progress-time { |
|
position: absolute; |
|
top: -25px; |
|
transform: translateX(-50%); |
|
background-color: rgba(30, 42, 71, 0.9); |
|
padding: 3px 6px; |
|
border-radius: 3px; |
|
font-size: 12px; |
|
display: none; |
|
white-space: nowrap; |
|
} |
|
|
|
|
|
.progress-marker { |
|
position: absolute; |
|
bottom: 0px; |
|
width: 0; |
|
height: 0; |
|
border-left: 5px solid transparent; |
|
border-right: 5px solid transparent; |
|
border-top: 10px solid #ff5555; |
|
transform: translateX(-50%); |
|
z-index: 2; |
|
} |
|
|
|
.main-controls { |
|
display: flex; |
|
align-items: center; |
|
gap: 15px; |
|
} |
|
|
|
.control-button { |
|
background: none; |
|
border: none; |
|
color: #e6f1ff; |
|
font-size: 18px; |
|
cursor: pointer; |
|
padding: 5px; |
|
border-radius: 50%; |
|
width: 36px; |
|
height: 36px; |
|
display: flex; |
|
align-items: center; |
|
justify-content: center; |
|
transition: background-color 0.3s; |
|
} |
|
|
|
.control-button:hover { |
|
background-color: rgba(100, 255, 218, 0.2); |
|
} |
|
|
|
.time-display { |
|
font-size: 14px; |
|
color: #ccd6f6; |
|
white-space: nowrap; |
|
} |
|
|
|
.volume-control { |
|
display: flex; |
|
align-items: center; |
|
gap: 5px; |
|
margin-left: auto; |
|
} |
|
|
|
.volume-button { |
|
background: none; |
|
border: none; |
|
color: #e6f1ff; |
|
font-size: 18px; |
|
cursor: pointer; |
|
padding: 5px; |
|
} |
|
|
|
.volume-slider { |
|
width: 80px; |
|
height: 6px; |
|
-webkit-appearance: none; |
|
background: #1e2a47; |
|
border-radius: 3px; |
|
outline: none; |
|
opacity: 0; |
|
transition: opacity 0.3s, width 0.3s; |
|
background-image: linear-gradient(#6aebfc, #6aebfc); |
|
background-size: 100% 100%; |
|
background-repeat: no-repeat; |
|
} |
|
|
|
.volume-control:hover .volume-slider { |
|
opacity: 1; |
|
width: 100px; |
|
} |
|
|
|
.volume-slider::-webkit-slider-thumb { |
|
-webkit-appearance: none; |
|
width: 12px; |
|
height: 12px; |
|
background: #6aebfc; |
|
border-radius: 50%; |
|
cursor: pointer; |
|
} |
|
|
|
.speed-control { |
|
display: flex; |
|
align-items: center; |
|
gap: 5px; |
|
} |
|
|
|
.speed-slider { |
|
width: 120px; |
|
height: 6px; |
|
-webkit-appearance: none; |
|
background: #1e2a47; |
|
border-radius: 3px; |
|
outline: none; |
|
background-image: linear-gradient(#64d1ff, #64d1ff); |
|
background-size: 100% 100%; |
|
background-repeat: no-repeat; |
|
} |
|
|
|
.speed-slider::-webkit-slider-thumb { |
|
-webkit-appearance: none; |
|
width: 12px; |
|
height: 12px; |
|
background: #5bb7de; |
|
border-radius: 50%; |
|
cursor: pointer; |
|
} |
|
|
|
.speed-value { |
|
font-size: 14px; |
|
min-width: 30px; |
|
text-align: center; |
|
} |
|
|
|
.fullscreen-button { |
|
margin-left: 10px; |
|
} |
|
|
|
.audio-controls { |
|
display: flex; |
|
flex-direction: column; |
|
gap: 10px; |
|
margin-bottom: 15px; |
|
} |
|
|
|
.audio-item { |
|
display: flex; |
|
align-items: center; |
|
gap: 10px; |
|
} |
|
|
|
.audio-item label { |
|
min-width: 50px; |
|
color: #9ab3d9; |
|
} |
|
|
|
.audio-slider { |
|
flex-grow: 1; |
|
height: 8px; |
|
-webkit-appearance: none; |
|
background: #1e2a47; |
|
border-radius: 5px; |
|
outline: none; |
|
background-image: linear-gradient(#64ffda, #64ffda); |
|
background-size: 100% 100%; |
|
background-repeat: no-repeat; |
|
} |
|
|
|
.audio-slider::-webkit-slider-thumb { |
|
-webkit-appearance: none; |
|
width: 18px; |
|
height: 18px; |
|
background: #64ffda; |
|
border-radius: 50%; |
|
cursor: pointer; |
|
} |
|
|
|
.settings { |
|
background-color: rbga(30, 42, 71, 0.3); |
|
padding: 15px; |
|
border-radius: 5px; |
|
margin-bottom: 20px; |
|
} |
|
|
|
.setting-item { |
|
display: flex; |
|
justify-content: space-between; |
|
align-items: center; |
|
margin-bottom: 10px; |
|
} |
|
|
|
.setting-item:last-child { |
|
margin-bottom: 0; |
|
} |
|
|
|
.setting-item label { |
|
color: #ccd6f6; |
|
} |
|
|
|
.global-volume-container, |
|
.playback-speed-container { |
|
display: flex; |
|
align-items: center; |
|
gap: 10px; |
|
width: 100%; |
|
} |
|
|
|
.global-volume-slider, |
|
.playback-speed-slider { |
|
flex-grow: 1; |
|
height: 8px; |
|
-webkit-appearance: none; |
|
background: #1e2a47; |
|
border-radius: 5px; |
|
outline: none; |
|
background-image: linear-gradient(#64ffda, #64ffda); |
|
background-size: 100% 100%; |
|
background-repeat: no-repeat; |
|
} |
|
|
|
.global-volume-slider::-webkit-slider-thumb, |
|
.playback-speed-slider::-webkit-slider-thumb { |
|
-webkit-appearance: none; |
|
width: 16px; |
|
height: 16px; |
|
background: #64ffda; |
|
border-radius: 50%; |
|
cursor: pointer; |
|
} |
|
|
|
.slider-value { |
|
min-width: 40px; |
|
text-align: right; |
|
} |
|
|
|
input[type="number"], |
|
input[type="checkbox"], |
|
select { |
|
background-color: #112240; |
|
border: 1px solid #64ffda; |
|
color: #e6f1ff; |
|
padding: 5px; |
|
border-radius: 3px; |
|
} |
|
|
|
.tech-decoration { |
|
width: 100%; |
|
height: 2px; |
|
background: linear-gradient(90deg, transparent, #64ffda, transparent); |
|
margin: 20px 0; |
|
} |
|
|
|
|
|
.video-container:-webkit-full-screen { |
|
width: 100%; |
|
height: 100%; |
|
background-color: black; |
|
} |
|
|
|
.video-container:-webkit-full-screen video { |
|
width: 100%; |
|
height: 100%; |
|
} |
|
|
|
.video-container:-webkit-full-screen .video-controls { |
|
position: fixed; |
|
bottom: 0; |
|
left: 0; |
|
right: 0; |
|
width: 100%; |
|
border-radius: 0; |
|
} |
|
|
|
|
|
.loading-overlay { |
|
position: fixed; |
|
top: 0; |
|
left: 0; |
|
width: 100%; |
|
height: 100%; |
|
background-color: rgba(0, 0, 0, 0.8); |
|
display: flex; |
|
justify-content: center; |
|
align-items: center; |
|
z-index: 9998; |
|
transition: opacity 1s ease-out; |
|
} |
|
|
|
.spinner-box { |
|
width: 300px; |
|
height: 300px; |
|
display: flex; |
|
justify-content: center; |
|
align-items: center; |
|
background-color: transparent; |
|
} |
|
|
|
|
|
.leo { |
|
position: absolute; |
|
display: flex; |
|
justify-content: center; |
|
align-items: center; |
|
border-radius: 50%; |
|
} |
|
|
|
.blue-orbit { |
|
width: 165px; |
|
height: 165px; |
|
border: 1px solid #91daffa5; |
|
animation: spin3D 3s linear .2s infinite; |
|
} |
|
|
|
.green-orbit { |
|
width: 120px; |
|
height: 120px; |
|
border: 1px solid #91ffbfa5; |
|
animation: spin3D 2s linear 0s infinite; |
|
} |
|
|
|
.red-orbit { |
|
width: 90px; |
|
height: 90px; |
|
border: 1px solid #ffca91a5; |
|
animation: spin3D 1s linear 0s infinite; |
|
} |
|
|
|
.white-orbit { |
|
width: 60px; |
|
height: 60px; |
|
border: 2px solid #ffffff; |
|
animation: spin3D 10s linear 0s infinite; |
|
} |
|
|
|
.w1 { |
|
transform: rotate3D(1, 1, 1, 90deg); |
|
} |
|
|
|
.w2 { |
|
transform: rotate3D(1, 2, .5, 90deg); |
|
} |
|
|
|
.w3 { |
|
transform: rotate3D(.5, 1, 2, 90deg); |
|
} |
|
|
|
|
|
@keyframes spin3D { |
|
from { |
|
transform: rotate3d(.5, .5, .5, 360deg); |
|
} |
|
|
|
to { |
|
transform: rotate3d(0, 0, 0, 0deg); |
|
} |
|
} |
|
|
|
@keyframes spin { |
|
from { |
|
transform: rotate(0deg); |
|
} |
|
|
|
to { |
|
transform: rotate(360deg); |
|
} |
|
} |
|
|
|
.time-set-button { |
|
background-color: #112240; |
|
border: 1px solid #64ffda; |
|
color: #e6f1ff; |
|
padding: 5px 10px; |
|
border-radius: 3px; |
|
cursor: pointer; |
|
font-size: 12px; |
|
margin-left: 5px; |
|
transition: background-color 0.3s; |
|
} |
|
|
|
.time-set-button:hover { |
|
background-color: rgba(100, 255, 218, 0.2); |
|
} |
|
|
|
|
|
.combine-button { |
|
background-color: #97c2f0; |
|
color: #0a192f; |
|
border: none; |
|
padding: 10px 20px; |
|
border-radius: 5px; |
|
font-size: 16px; |
|
cursor: pointer; |
|
margin-top: 20px; |
|
transition: all 0.3s; |
|
font-weight: bold; |
|
} |
|
|
|
.combine-button:hover { |
|
background-color: #52e0c4; |
|
transform: translateY(-2px); |
|
box-shadow: 0 5px 15px rgba(100, 255, 218, 0.4); |
|
} |
|
|
|
.combine-button:disabled { |
|
background-color: #3a5a78; |
|
cursor: not-allowed; |
|
transform: none; |
|
box-shadow: none; |
|
} |
|
|
|
|
|
.combine-status { |
|
margin-top: 10px; |
|
color: #97c2f0; |
|
font-size: 14px; |
|
height: 20px; |
|
} |
|
|
|
|
|
.preview-section { |
|
margin-top: 20px; |
|
padding: 15px; |
|
background-color: rgba(17, 34, 64, 0.7); |
|
border-radius: 5px; |
|
display: none; |
|
} |
|
|
|
.preview-section h3 { |
|
margin-top: 0; |
|
color: #97c2f0; |
|
border-bottom: 1px solid #64ffda; |
|
padding-bottom: 5px; |
|
} |
|
|
|
|
|
.disabled-overlay { |
|
position: absolute; |
|
top: 0; |
|
left: 0; |
|
width: 100%; |
|
height: 100%; |
|
background-color: rgba(10, 25, 47, 0.7); |
|
display: flex; |
|
justify-content: center; |
|
align-items: center; |
|
z-index: 10; |
|
border-radius: 5px; |
|
} |
|
|
|
.disabled-message { |
|
background-color: rgba(30, 42, 71, 0.9); |
|
padding: 20px; |
|
border-radius: 5px; |
|
text-align: center; |
|
max-width: 80%; |
|
} |
|
|
|
.disabled-message p { |
|
margin-bottom: 15px; |
|
} |
|
|
|
.loader { |
|
width: 80px; |
|
aspect-ratio: 1; |
|
border: 10px solid #000; |
|
box-sizing: border-box; |
|
background: |
|
radial-gradient(farthest-side, #fff 98%, #0000) 50%/20px 20px, |
|
radial-gradient(farthest-side, #fff 98%, #0000) 50%/20px 20px, |
|
radial-gradient(farthest-side, #fff 98%, #0000) 50%/20px 20px, |
|
radial-gradient(farthest-side, #fff 98%, #0000) 50%/20px 20px, |
|
radial-gradient(farthest-side, #fff 98%, #0000) 50%/80% 80%, |
|
#000; |
|
background-repeat: no-repeat; |
|
filter: blur(4px) contrast(10); |
|
animation: squarePulse 1s infinite alternate; |
|
} |
|
|
|
@keyframes squarePulse { |
|
0% { |
|
background-position: |
|
50% 50%, 50% 50%, 50% 50%, 50% 50%, 50% 50%, 50% 50%; |
|
} |
|
|
|
25% { |
|
background-position: |
|
50% 0, 50% 50%, 50% 50%, 50% 50%, 50% 50%, 50% 50%; |
|
} |
|
|
|
50% { |
|
background-position: |
|
50% 0, 50% 100%, 50% 50%, 50% 50%, 50% 50%, 50% 50%; |
|
} |
|
|
|
75% { |
|
background-position: |
|
50% 0, 50% 100%, 0 50%, 50% 50%, 50% 50%, 50% 50%; |
|
} |
|
|
|
100% { |
|
background-position: |
|
50% 0, 50% 100%, 0 50%, 100% 50%, 50% 50%, 50% 50%; |
|
} |
|
} |
|
|
|
#buffering-indicator { |
|
position: absolute; |
|
top: 50%; |
|
left: 50%; |
|
transform: translate(-50%, -50%); |
|
z-index: 10; |
|
display: none; |
|
} |
|
|
|
.sync-status { |
|
position: absolute; |
|
bottom: 100px; |
|
left: 10px; |
|
width: 150px; |
|
|
|
height: 30px; |
|
|
|
background-color: rgba(0, 0, 0, 0.7); |
|
color: #97c2f0; |
|
padding: 5px 10px; |
|
border-radius: 3px; |
|
font-size: 12px; |
|
z-index: 5; |
|
display: flex; |
|
align-items: center; |
|
gap: 5px; |
|
white-space: nowrap; |
|
|
|
overflow: hidden; |
|
|
|
text-overflow: ellipsis; |
|
|
|
user-select: none; |
|
|
|
|
|
contain: strict; |
|
|
|
} |
|
|
|
.sync-status button { |
|
background: none; |
|
border: none; |
|
color: #fff; |
|
cursor: pointer; |
|
font-size: 12px; |
|
} |
|
|
|
.lock-controls-btn { |
|
position: fixed; |
|
bottom: 20px; |
|
right: 20px; |
|
background-color: rgba(0, 0, 0, 0.7); |
|
border: none; |
|
color: #fff; |
|
width: 36px; |
|
height: 36px; |
|
border-radius: 50%; |
|
display: flex; |
|
align-items: center; |
|
justify-content: center; |
|
cursor: pointer; |
|
z-index: 100; |
|
display: none; |
|
} |
|
|
|
.lock-controls-btn.locked { |
|
color: #97c2f0; |
|
} |
|
|
|
.time-markers-container { |
|
display: flex; |
|
flex-wrap: wrap; |
|
gap: 10px; |
|
margin: 15px 0; |
|
padding: 15px; |
|
background-color: rgba(17, 34, 64, 0.8); |
|
border-radius: 5px; |
|
} |
|
|
|
.time-marker { |
|
font-size: 80%; |
|
padding: 6px 8px; |
|
background-color: rgba(100, 255, 218, 0.05); |
|
border: 1px solid #64ffda; |
|
border-radius: 4px; |
|
cursor: grab; |
|
user-select: none; |
|
transition: background-color 0.3s; |
|
} |
|
|
|
.time-marker:hover { |
|
background-color: rgba(100, 255, 218, 0.1); |
|
} |
|
|
|
.time-marker.dragging { |
|
opacity: 0.7; |
|
cursor: grabbing; |
|
} |
|
|
|
.time-input-container { |
|
display: flex; |
|
align-items: center; |
|
gap: 5px; |
|
} |
|
|
|
.time-input-container input { |
|
width: 80px; |
|
} |
|
|
|
#hide-video:checked~.video-container video { |
|
background-color: black; |
|
opacity: 0; |
|
} |
|
</style> |
|
</head> |
|
|
|
<body> |
|
|
|
<noscript><iframe src="https://www.googletagmanager.com/ns.html?id=GTM-NDL2LKLQ" height="0" width="0" style="display:none;visibility:hidden"></iframe></noscript> |
|
|
|
<script> |
|
|
|
document.addEventListener('DOMContentLoaded', function() { |
|
const urlParams = new URLSearchParams(window.location.search); |
|
const isTMode = urlParams.has('mode') && urlParams.get('mode') === 't'; |
|
const videoSource = document.querySelector('#video source'); |
|
|
|
if (isTMode && videoSource) { |
|
videoSource.src = '/t/v.mp4'; |
|
const video = document.getElementById('video'); |
|
video.load(); |
|
} |
|
}); |
|
</script> |
|
|
|
<div class="tech-background" id="techBg"> |
|
</div> |
|
|
|
<div class="loading-overlay" id="loadingOverlay"> |
|
<div class="spinner-box"> |
|
<div class="blue-orbit leo"> |
|
</div> |
|
<div class="green-orbit leo"> |
|
</div> |
|
<div class="red-orbit leo"> |
|
</div> |
|
<div class="white-orbit w1 leo"> |
|
</div> |
|
<div class="white-orbit w2 leo"> |
|
</div> |
|
<div class="white-orbit w3 leo"> |
|
</div> |
|
</div> |
|
</div> |
|
<h1 id="title-name">文化発表会音声動画プレイヤー</h1> |
|
<div class="settings"> |
|
<h2>サービスワーカー設定</h2> |
|
<div hidden> |
|
|
|
<div class="setting-item"> |
|
<label> |
|
<input type="checkbox" id="sw-video" checked>動画ファイル (/v.mp4)</label> |
|
</div> |
|
<div class="setting-item"> |
|
<label> |
|
<input type="checkbox" id="sw-t-video" checked>動画ファイル (/t/v.mp4)</label> |
|
</div> |
|
<br> |
|
<div class="setting-item"> |
|
<label> |
|
<input type="checkbox" id="sw-piano" checked>ピアノ音声 (/p.mp3)</label> |
|
</div> |
|
<div class="setting-item"> |
|
<label> |
|
<input type="checkbox" id="sw-soprano" checked>ソプラノ音声 (/s.mp3)</label> |
|
</div> |
|
<div class="setting-item"> |
|
<label> |
|
<input type="checkbox" id="sw-alto" checked>アルト音声 (/a.mp3)</label> |
|
</div> |
|
<div class="setting-item"> |
|
<label> |
|
<input type="checkbox" id="sw-tenor" checked>テノール音声 (/t.mp3)</label> |
|
</div> |
|
<div class="setting-item"> |
|
<label> |
|
<input type="checkbox" id="sw-combined" checked>全体音声 (/k.mp3)</label> |
|
</div> |
|
<br> |
|
<div class="setting-item"> |
|
<label> |
|
<input type="checkbox" id="sw-t-piano" checked>ピアノ音声 (/t/p.mp3)</label> |
|
</div> |
|
<div class="setting-item"> |
|
<label> |
|
<input type="checkbox" id="sw-t-soprano" checked>ソプラノ音声 (/t/s.mp3)</label> |
|
</div> |
|
<div class="setting-item"> |
|
<label> |
|
<input type="checkbox" id="sw-t-alto" checked>アルト音声 (/t/a.mp3)</label> |
|
</div> |
|
<div class="setting-item"> |
|
<label> |
|
<input type="checkbox" id="sw-t-tenor" checked>テノール音声 (/t/t.mp3)</label> |
|
</div> |
|
<div class="setting-item"> |
|
<label> |
|
<input type="checkbox" id="sw-t-combined" checked>全体音声 (/t/k.mp3)</label> |
|
</div> |
|
<br> |
|
<div class="setting-item"> |
|
<label> |
|
<input type="checkbox" id="sw-index" checked>インデックスファイル (/index.html)</label> |
|
</div> |
|
<div class="setting-item"> |
|
<label> |
|
<input type="checkbox" id="sw-root" checked>ルートファイル (/)</label> |
|
</div> |
|
</div> |
|
<a>下記のボタンを押すと、サービスワーカーを使用してページをオフラインで使用できるようにします。<br> |
|
<b>これはテスト的な機能で、ページの更新などができなくなるバグが発生する可能性があります。<br>(更新機能は実装していますが安定して動作するとは限りません。)<br> |
|
ページが更新されない場合、動画の更新ができなかったり既存のバグが残り続けたりする可能性があります。</b></a> |
|
<br> |
|
<button class="combine-button" id="sw-register-btn" disabled>登録を開始</button> |
|
<div class="combine-status" id="sw-status"> |
|
</div> |
|
<script> |
|
window.addEventListener('load', async () => { |
|
const statusElem = document.getElementById('sw-status'); |
|
const registerBtn = document.getElementById('sw-register-btn'); |
|
|
|
function updateStatus(message) { |
|
console.log(message); |
|
statusElem.textContent = message; |
|
} |
|
|
|
function updateError(message, error) { |
|
const errorMsg = `${message}\nエラー詳細: ${error?.message || error}`; |
|
console.error(errorMsg, error); |
|
statusElem.textContent = errorMsg; |
|
} |
|
|
|
if (!('serviceWorker' in navigator)) { |
|
updateError('このブラウザはService Workerに対応していません。'); |
|
return; |
|
} |
|
|
|
|
|
registerBtn.disabled = false; |
|
|
|
try { |
|
const reg = await navigator.serviceWorker.getRegistration(); |
|
|
|
if (reg) { |
|
updateStatus('Service Workerは既に登録されています。更新を確認中...'); |
|
registerBtn.innerText = "更新を開始"; |
|
await reg.update(); |
|
|
|
reg.onupdatefound = () => { |
|
const newSW = reg.installing; |
|
if (newSW) { |
|
newSW.onstatechange = () => { |
|
if (newSW.state === 'installed' && navigator.serviceWorker.controller) { |
|
updateStatus('更新があります。新しいService Workerがインストールされました。'); |
|
|
|
} |
|
}; |
|
} |
|
}; |
|
} else { |
|
updateStatus('Service Workerは未登録です。'); |
|
|
|
} |
|
} catch (err) { |
|
updateError('Service Workerの確認中にエラーが発生しました。', err); |
|
} |
|
|
|
registerBtn.addEventListener('click', async () => { |
|
registerBtn.disabled = true; |
|
try { |
|
updateStatus('既存のService Workerをアンレジスター中...'); |
|
const regs = await navigator.serviceWorker.getRegistrations(); |
|
for (const reg of regs) { |
|
await reg.unregister(); |
|
} |
|
|
|
updateStatus('新しいService Workerを登録中...'); |
|
await navigator.serviceWorker.register('/sw.js'); |
|
|
|
updateStatus('登録完了。ページをリロードしてキャッシュを有効化します。'); |
|
registerBtn.textContent = 'ページをリロード'; |
|
registerBtn.disabled = false; |
|
|
|
registerBtn.onclick = () => location.reload(); |
|
} catch (err) { |
|
updateError('Service Workerの登録に失敗しました。', err); |
|
} |
|
}); |
|
}); |
|
</script> |
|
<style> |
|
#app { |
|
position: fixed; |
|
top: 50%; |
|
left: 50%; |
|
width: 200px; |
|
height: 265px; |
|
background: rgba(0, 0, 0, 0.5); |
|
color: #fff; |
|
border-radius: 10px; |
|
box-sizing: border-box; |
|
box-shadow: 0 0 20px rgba(0, 0, 0, 0.7); |
|
display: flex; |
|
flex-direction: column; |
|
align-items: center; |
|
justify-content: center; |
|
padding: 10px; |
|
overflow: hidden; |
|
z-index: 999999; |
|
} |
|
|
|
#header { |
|
position: absolute; |
|
top: 5px; |
|
right: 5px; |
|
cursor: pointer; |
|
user-select: none; |
|
font-size: 24px; |
|
color: #fff; |
|
z-index: 10; |
|
width: 30px; |
|
height: 30px; |
|
display: flex; |
|
align-items: center; |
|
justify-content: center; |
|
} |
|
|
|
svg { |
|
overflow: visible; |
|
} |
|
|
|
#toggle { |
|
cursor: pointer; |
|
} |
|
|
|
#bpmInput { |
|
margin-top: 10px; |
|
background: #222; |
|
border: none; |
|
color: #fff; |
|
font-size: 16px; |
|
text-align: center; |
|
width: 80px; |
|
padding: 4px; |
|
border-radius: 4px; |
|
} |
|
|
|
#content { |
|
flex: 1; |
|
display: flex; |
|
flex-direction: column; |
|
align-items: center; |
|
justify-content: center; |
|
width: 100%; |
|
user-select: none; |
|
} |
|
</style> |
|
<script src="https://cdnjs.cloudflare.com/ajax/libs/gsap/3.12.2/gsap.min.js"></script> |
|
<script src="https://cdnjs.cloudflare.com/ajax/libs/gsap/3.12.2/CustomEase.min.js"></script> |
|
|
|
<script> |
|
document.addEventListener('DOMContentLoaded', function() { |
|
const urlParams = new URLSearchParams(window.location.search); |
|
|
|
let metronomeState = { |
|
isActive: false, |
|
bpm: urlParams.get('mode') === 't' ? 66 : 92 |
|
}; |
|
|
|
|
|
const tl = gsap.timeline({ |
|
repeat: -1, |
|
paused: true |
|
}) |
|
.to('#arm', { |
|
duration: 1, |
|
rotate: 90, |
|
ease: 'power2.inOut', |
|
yoyo: true, |
|
repeat: 1, |
|
transformOrigin: '100% 100%' |
|
}, 0) |
|
.add(() => { |
|
swing(-1); |
|
}, 0.5) |
|
.add(() => { |
|
swing(1); |
|
}, 1.5); |
|
|
|
|
|
function setMetronomeState(active, bpm) { |
|
metronomeState.isActive = active; |
|
metronomeState.bpm = bpm; |
|
|
|
if (active) { |
|
if (!tl.isActive()) { |
|
gsap.to('#toggle path', { |
|
attr: { |
|
stroke: '#fff' |
|
} |
|
}); |
|
gsap.to('#toggle circle', { |
|
duration: 0.3, |
|
x: 15, |
|
ease: 'power3.inOut' |
|
}); |
|
tl.play(); |
|
} |
|
} else { |
|
if (tl.isActive()) { |
|
gsap.to('#toggle path', { |
|
attr: { |
|
stroke: '#333' |
|
} |
|
}); |
|
gsap.to('#toggle circle', { |
|
duration: 0.3, |
|
x: 0, |
|
ease: 'power3.inOut' |
|
}); |
|
tl.pause(); |
|
} |
|
} |
|
} |
|
|
|
|
|
function swing(dir) { |
|
gsap.timeline() |
|
.to('#tip', { |
|
duration: 0.25, |
|
rotate: 40 * dir, |
|
transformOrigin: '50% 19%', |
|
ease: 'power3' |
|
}) |
|
.to('#tip', { |
|
duration: 0.35, |
|
rotate: 0, |
|
ease: CustomEase.create('ez', 'M0,0 C0.06,0.002 0.148,0.026 0.186,0.17 0.211,0.274 0.27,1.074 0.552,1.086 0.635,1.089 0.736,0.991 0.81,0.976 0.894,0.957 0.938,1.012 1,1') |
|
}, '-=0.1'); |
|
|
|
const snd = document.getElementById('snd'); |
|
const sound = snd.cloneNode(); |
|
sound.volume = 0.33; |
|
sound.play().catch(e => console.error('メトロノーム音再生エラー:', e)); |
|
} |
|
|
|
|
|
document.querySelector('#toggle').onclick = () => { |
|
setMetronomeState(!tl.isActive(), metronomeState.bpm); |
|
}; |
|
|
|
|
|
function setTempoFromBPM() { |
|
const bpmInput = document.getElementById('bpmInput'); |
|
let bpm = parseInt(bpmInput.value); |
|
|
|
if (isNaN(bpm)) bpm = 60; |
|
bpm = Math.max(0, Math.min(bpm, 10000)); |
|
bpmInput.value = bpm; |
|
metronomeState.bpm = bpm; |
|
|
|
const tempoLevel = Math.round(gsap.utils.mapRange(0, 10000, 1, 10, bpm)); |
|
gsap.set('#lvl', { |
|
attr: { |
|
'stroke-dasharray': tempoLevel * 10 + ' ' + (100 - tempoLevel * 10) |
|
} |
|
}); |
|
|
|
const secondsPerBeat = 60 / bpm; |
|
const durationPerHalf = secondsPerBeat; |
|
|
|
const wasActive = tl.isActive(); |
|
tl.pause(0).clear(); |
|
|
|
tl.to('#arm', { |
|
duration: durationPerHalf, |
|
rotate: 90, |
|
ease: 'power2.inOut', |
|
yoyo: true, |
|
repeat: 1, |
|
transformOrigin: '100% 100%' |
|
}, 0) |
|
.add(() => { |
|
swing(-1); |
|
}, durationPerHalf / 2) |
|
.add(() => { |
|
swing(1); |
|
}, durationPerHalf * 1.5); |
|
|
|
if (wasActive) { |
|
tl.play(0); |
|
} |
|
} |
|
|
|
|
|
document.getElementById('bpmInput').addEventListener('input', setTempoFromBPM); |
|
|
|
|
|
setTempoFromBPM(); |
|
|
|
|
|
async function combineAudio() { |
|
|
|
const wasMetronomeActive = tl.isActive(); |
|
if (wasMetronomeActive) { |
|
setMetronomeState(false, metronomeState.bpm); |
|
} |
|
|
|
const combineButton = document.getElementById('combine-button'); |
|
const combineStatus = document.getElementById('combine-status'); |
|
const audioFiles = ['p', 'a', 't', 's', 'k']; |
|
const basePath = new URLSearchParams(window.location.search).has('mode') ? '/t/' : '/'; |
|
|
|
|
|
const currentVolumes = {}; |
|
audioFiles.forEach(file => { |
|
const slider = document.querySelector(`.audio-slider[data-audio="${file}"]`); |
|
currentVolumes[file] = parseFloat(slider.value); |
|
}); |
|
|
|
combineButton.disabled = true; |
|
combineStatus.textContent = "音声を合成中..."; |
|
|
|
try { |
|
|
|
const audioBufferPromises = audioFiles.map(async file => { |
|
if (currentVolumes[file] === 0) return null; |
|
|
|
try { |
|
const response = await fetch(`${basePath}${file}.mp3`); |
|
if (!response.ok) throw new Error(`HTTP error! status: ${response.status}`); |
|
const arrayBuffer = await response.arrayBuffer(); |
|
return await audioContext.decodeAudioData(arrayBuffer); |
|
} catch (error) { |
|
console.error(`音声ファイル読み込みエラー (${file}.mp3):`, error); |
|
return null; |
|
} |
|
}); |
|
|
|
|
|
const buffers = await Promise.all(audioBufferPromises); |
|
const validBuffers = buffers.filter(Boolean); |
|
|
|
if (validBuffers.length === 0) { |
|
throw new Error("有効な音声ファイルが見つかりませんでした"); |
|
} |
|
|
|
|
|
const maxDuration = Math.max(...validBuffers.map(b => b.duration)); |
|
|
|
|
|
const combinedAudioBuffer = audioContext.createBuffer( |
|
2, |
|
audioContext.sampleRate * maxDuration, |
|
audioContext.sampleRate |
|
); |
|
|
|
|
|
audioFiles.forEach((file, index) => { |
|
if (!buffers[index] || currentVolumes[file] === 0) return; |
|
|
|
const buffer = buffers[index]; |
|
const volume = currentVolumes[file]; |
|
|
|
|
|
for (let channel = 0; channel < 2; channel++) { |
|
const inputData = buffer.getChannelData(channel % buffer.numberOfChannels); |
|
const outputData = combinedAudioBuffer.getChannelData(channel); |
|
|
|
for (let i = 0; i < inputData.length; i++) { |
|
outputData[i] += inputData[i] * volume; |
|
} |
|
} |
|
}); |
|
|
|
|
|
for (let channel = 0; channel < 2; channel++) { |
|
const outputData = combinedAudioBuffer.getChannelData(channel); |
|
let max = 0; |
|
|
|
for (let i = 0; i < outputData.length; i++) { |
|
if (Math.abs(outputData[i]) > max) { |
|
max = Math.abs(outputData[i]); |
|
} |
|
} |
|
|
|
if (max > 1) { |
|
for (let i = 0; i < outputData.length; i++) { |
|
outputData[i] /= max; |
|
} |
|
} |
|
} |
|
|
|
|
|
const blob = bufferToWave(combinedAudioBuffer, audioContext); |
|
const url = URL.createObjectURL(blob); |
|
|
|
|
|
if (window.combinedAudioElement) { |
|
window.combinedAudioElement.pause(); |
|
URL.revokeObjectURL(window.combinedAudioElement.src); |
|
} |
|
|
|
|
|
window.combinedAudioElement = new Audio(url); |
|
window.combinedAudioElement.preservesPitch = true; |
|
window.combinedAudioElement.mozPreservesPitch = true; |
|
window.combinedAudioElement.webkitPreservesPitch = true; |
|
window.combinedAudioElement.playbackRate = currentPlaybackRate; |
|
|
|
|
|
combineStatus.textContent = "音声の合成が完了しました"; |
|
isAudioCombined = true; |
|
enablePlayerControls(); |
|
|
|
|
|
if (wasMetronomeActive) { |
|
setTimeout(() => { |
|
setMetronomeState(true, metronomeState.bpm); |
|
}, 500); |
|
} |
|
} catch (error) { |
|
console.error('音声合成エラー:', error); |
|
combineStatus.textContent = `音声の合成に失敗しました: ${error.message}`; |
|
|
|
|
|
if (wasMetronomeActive) { |
|
setMetronomeState(true, metronomeState.bpm); |
|
} |
|
} finally { |
|
combineButton.disabled = false; |
|
} |
|
} |
|
|
|
|
|
function bufferToWave(abuffer, audioContext) { |
|
const numOfChan = abuffer.numberOfChannels; |
|
const length = abuffer.length * numOfChan * 2 + 44; |
|
const buffer = new ArrayBuffer(length); |
|
const view = new DataView(buffer); |
|
let pos = 0; |
|
|
|
|
|
setUint32(0x46464952); |
|
setUint32(length - 8); |
|
setUint32(0x45564157); |
|
setUint32(0x20746d66); |
|
setUint32(16); |
|
setUint16(1); |
|
setUint16(numOfChan); |
|
setUint32(audioContext.sampleRate); |
|
setUint32(audioContext.sampleRate * 2 * numOfChan); |
|
setUint16(numOfChan * 2); |
|
setUint16(16); |
|
setUint32(0x61746164); |
|
setUint32(length - pos - 4); |
|
|
|
|
|
for (let i = 0; i < abuffer.length; i++) { |
|
for (let channel = 0; channel < numOfChan; channel++) { |
|
let sample = abuffer.getChannelData(channel)[i] * 0x7fff; |
|
sample = Math.max(-32768, Math.min(32767, sample)); |
|
view.setInt16(pos, sample, true); |
|
pos += 2; |
|
} |
|
} |
|
|
|
function setUint16(data) { |
|
view.setUint16(pos, data, true); |
|
pos += 2; |
|
} |
|
|
|
function setUint32(data) { |
|
view.setUint32(pos, data, true); |
|
pos += 4; |
|
} |
|
|
|
return new Blob([buffer], { |
|
type: 'audio/wav' |
|
}); |
|
} |
|
|
|
|
|
const app = document.getElementById('app'); |
|
new Draggabilly(app, { |
|
containment: 'body' |
|
}); |
|
|
|
|
|
const header = document.getElementById('header'); |
|
const content = document.getElementById('content'); |
|
let isOpen = false; |
|
|
|
|
|
gsap.set(app, { |
|
width: 60, |
|
height: 40, |
|
padding: 0 |
|
}); |
|
content.style.display = 'none'; |
|
header.textContent = '▼'; |
|
|
|
header.addEventListener('click', () => { |
|
if (isOpen) { |
|
|
|
gsap.to(app, { |
|
duration: 0.4, |
|
width: 60, |
|
height: 40, |
|
padding: 0, |
|
ease: 'power3.inOut', |
|
onComplete: () => { |
|
content.style.display = 'none'; |
|
header.textContent = '▼'; |
|
} |
|
}); |
|
} else { |
|
|
|
content.style.display = 'flex'; |
|
gsap.to(app, { |
|
duration: 0.4, |
|
width: 300, |
|
height: 400, |
|
padding: 10, |
|
ease: 'power3.inOut', |
|
onComplete: () => { |
|
header.textContent = '▲'; |
|
} |
|
}); |
|
} |
|
isOpen = !isOpen; |
|
}); |
|
}); |
|
</script> |
|
</div> |
|
<div class="container"> |
|
<div class="audio-controls"> |
|
<h2>音声コントロール</h2> |
|
<span>各パートの音の大きさを設定できます、自分のパートの音量を1、それ以外のパートの音量を0.4くらいにすると他のパートのタイミングを確認しながら練習できます。<br> |
|
※「ピアノ」と「全体」は他のパートと音がずれるため、組み合わせて使用しないでください。</span> |
|
<div class="audio-item"> |
|
<label>ソプラノ</label> |
|
<input type="range" class="audio-slider" data-audio="s" min="0" max="1" step="0.01" value="1"> |
|
<span class="slider-value volume-value">1.00</span> |
|
</div> |
|
<div class="audio-item"> |
|
<label>アルト </label> |
|
<input type="range" class="audio-slider" data-audio="a" min="0" max="1" step="0.01" value="1"> |
|
<span class="slider-value volume-value">1.00</span> |
|
</div> |
|
<div class="audio-item"> |
|
<label>テノール</label> |
|
<input type="range" class="audio-slider" data-audio="t" min="0" max="1" step="0.01" value="1"> |
|
<span class="slider-value volume-value">1.00</span> |
|
</div> |
|
<div class="audio-item"> |
|
<label>ピアノ </label> |
|
<input type="range" class="audio-slider" data-audio="p" min="0" max="1" step="0.01" value="0"> |
|
<span class="slider-value volume-value">0.00</span> |
|
</div> |
|
<div class="audio-item"> |
|
<label>全体(非推奨)</label> |
|
<input type="range" class="audio-slider" data-audio="k" min="0" max="1" step="0.01" value="0"> |
|
<span class="slider-value volume-value">0.00</span> |
|
</div> |
|
|
|
<button class="combine-button" id="combine-button">音声を合成</button> |
|
<div class="combine-status" id="combine-status"> |
|
</div> |
|
</div> |
|
<div class="tech-decoration"> |
|
</div> |
|
|
|
<div class="preview-section" id="preview-section" hidden> |
|
<h3 hidden>プレビュー</h3> |
|
<p hidden>合成された音声をプレビューできます。再生ボタンをクリックして確認してください。</p> |
|
<button class="control-button" id="preview-button" hidden>▶</button> |
|
<span id="preview-time" hidden>00:00 / 00:00</span> |
|
</div> |
|
<div class="video-container" id="video-container"> |
|
|
|
<div id="buffering-indicator"> |
|
<div class="loader"> |
|
</div> |
|
</div> |
|
|
|
<div class="sync-status" id="sync-status"> |
|
<span id="sync-status-text"> |
|
</span> |
|
<button id="sync-status-close">×</button> |
|
</div> |
|
<script> |
|
new Draggabilly('#sync-status'); |
|
</script> |
|
|
|
<div class="disabled-overlay" id="disabledOverlay"> |
|
<div class="disabled-message"> |
|
<p>音声の合成が完了していません</p> |
|
<p>上の音声コントロールで各パートの音量を調整し、「合成」ボタンを押してください</p> |
|
</div> |
|
</div> |
|
<video id="video" muted> |
|
<source src="v.mp4" type="video/mp4" allowpictureinpicture> |
|
</source> |
|
</video> |
|
<div id="app"> |
|
<div id="header">▼</div> |
|
<div id="content"> |
|
<svg width="108" height="150" fill="none" stroke="#fff" stroke-width="4" stroke-linecap="round"> |
|
<path id="tip" d="M55,1c22,0,0,48,0,48S33,1,55,1z" fill="none" /> |
|
<path d="M21.3,117.5c0-17.4,14.1-31.5,31.5-31.5s31.5,14.1,31.5,31.5" stroke="#333" /> |
|
<path id="lvl" d="M21.3,117.5c0-17.4,14.1-31.5,31.5-31.5s31.5,14.1,31.5,31.5" /> |
|
<g id="arm"> |
|
<path d="M2,65 55,117" /> |
|
<circle cx="2" cy="65" r="2" /> |
|
</g> |
|
<g id="toggle"> |
|
<path d="M45.5,145 60.5,145" stroke-width="20" stroke="#444" /> |
|
<circle cx="45.5" cy="145" r="8" stroke="none" fill="#000" /> |
|
</g> |
|
</svg> |
|
<input id="bpmInput" type="number" min="0" value="66" /> BPM |
|
</div> |
|
<audio id="snd" src="data:audio/mpeg;base64,SUQzBAAAAAAAIlRTU0UAAAAOAAADTGF2ZjYxLjEuMTAwAAAAAAAAAAAAAAD/+1QAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABJbmZvAAAADwAAAAUAAASAAFVVVVVVVVVVVVVVVVVVVVVVVVWAgICAgICAgICAgICAgICAgICAgKqqqqqqqqqqqqqqqqqqqqqqqqqq1dXV1dXV1dXV1dXV1dXV1dXV1dX//////////////////////////wAAAABMYXZjNjEuMy4AAAAAAAAAAAAAAAAkAkAAAAAAAAAEgFJnjCP/+1RkAA/wDQCAAeAACAAADSAAAAEASAIADQAAIAAANIAAAAQD+c9Ur/f39/KLpNtVUpRoKhWoMy4UxKDYGAlmATReypqYqNWo+bVVKUZuovOVsYm4fiQntX6bb+vS6FVUoptu/tmZzvpdCqrM0iW1UxiThsDAPzC/K1BmhWAwOCkgrLbHh29sDrznj/qFIc6kdIe+OiLrGtwIkCn/9NMc8TLx48muiDgf2/RBODoZIFX9JUPc6f41SqLFvEPMuf7/+1RkTA/y3TsugYVlEgAADSAAAAENJQz0FGeACAAANIKAAARb1XDj0vAruHuBEgOH1tjT87z6h3U6xTqQghLKv46ncz8OhQaY2f7xm9PAiEICgUDAV0irYCAMBAAADwt5A73wJGAN/ijDg8BqgCONoHkwtMA3ED3PsQcPSAUAzJoVmZw0LhmF1ZKHgIm/RuyZuB+wFrBExSY0wbL/9buwGMYYXC2ZtLpX//1pmgNggOhBsWFyxOhfACv///i3uMz/+1RkN4ADhEjMbkpgAAAADSDAAAAOEPt9uPeAEAAANIMAAAAef19jsZigQCAUBgMCkABIwuN3LMqhG36c3GNxNIZDg1VL9VQbZP8cCxnN3tt0yr76xnGIDneOOSlM7gXlo9O41VQQhkXC/q+s59Lb+byPMel73vTO4mpa4ta2buHVkmsMn/znO9YtrGfjwb4eROGX8i22d2VUVDbcYTAIImcXTQlEE8LqR19UYqMXXuVcZb9xd9XU8ces0s3df3f/+1RkFQDx2x5jfzEABAAADSDgAAEHnHVxx6RmwAAANIAAAAQOAiaTAUBpMqhxt///7m+Bot83TS7MiyMMDaXyOT0eJuHofDzYXiwaHmlm2DHQRrKjGv9//I3tpEf9hgRHfEQ4gR1KEQdQepGsvt/JZWSqyXqniHQ1SSSwMAsGgVBFwlLwRPrWOicfLlyGus0DQNQ6dPA0JgaeIhKFRjyoLR534h8sDRUqC1MS//8qEnifewZ1n+So1zXK1P9MrP3/+1RkJoDx7QrZeYlgEAAADSAAAAEGROUOAIxHQAAANIAAAAQskwY5djO3+UKzpqz1KGOJM7br5SkFBSGXzBSCg0/2gyCQakxBTUUzLjEwMKqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqo="></audio> |
|
</div> |
|
<script> |
|
window.addEventListener('DOMContentLoaded', () => { |
|
const urlParams = new URLSearchParams(window.location.search); |
|
|
|
document.getElementById('bpmInput').value = urlParams.get('mode') === 't' ? 66 : 92; |
|
document.getElementById('title-name').textContent = urlParams.get('mode') === 't' ? "地球星歌" : "時の旅人"; |
|
}); |
|
</script> |
|
<div class="video-controls"> |
|
<div class="progress-container" id="progress-container"> |
|
<div class="progress-bar" id="progress-bar"> |
|
</div> |
|
<div class="progress-time" id="progress-time">00:00</div> |
|
<div class="progress-marker" id="start-marker" style="left: 0%; display: none;"> |
|
</div> |
|
<div class="progress-marker" id="end-marker" style="left: 100%; display: none;"> |
|
</div> |
|
</div> |
|
<div class="main-controls"> |
|
<button class="control-button" id="play-pause-btn" disabled>▶</button> |
|
<button class="control-button" id="reset-btn" disabled title="最初から再生">↺</button> |
|
<div class="time-display" id="time-display">00:00.00 / 00:00.00</div> |
|
<div class="volume-control"> |
|
<button class="volume-button" id="volume-btn" disabled>🔊</button> |
|
<input type="range" class="volume-slider" id="volume-slider" min="0" max="1" step="0.01" value="1" disabled> |
|
</div> |
|
<div class="speed-control"> |
|
<span class="speed-value" id="speed-value">1.00x</span> |
|
<input type="range" class="speed-slider" id="speed-slider" min="0.01" max="5" step="0.01" value="1" disabled> |
|
</div> |
|
<button class="control-button" id="pip-btn" disabled title="ピクチャーインピクチャー">⇲</button> |
|
<button class="control-button fullscreen-button" id="fullscreen-btn" disabled>⛶</button> |
|
</div> |
|
</div> |
|
</div> |
|
<div class="tech-decoration"> |
|
</div> |
|
<div class="settings"> |
|
<div class="time-markers-container" id="time-markers-container"> |
|
<span>このボックスをドラッグして「再生開始秒数」や「再生終了秒数」の入力ボックスの上で離すと秒数を簡単に変更できます。各秒数は楽譜に合わせています。<br> |
|
時の旅人はAからE、地球星歌は①から⑤で区切っていて、地球星歌はリピート記号があるので①と②は1番と2番があります。</span> |
|
|
|
</div> |
|
<div class="setting-item"> |
|
<label for="start-time">再生開始秒数:</label> |
|
<div class="time-input-container"> |
|
<input type="number" id="start-time" min="0" value="0" step="0.01" disabled> |
|
<button class="time-set-button" id="set-start-time" disabled>現在の秒数に設定</button> |
|
</div> |
|
</div> |
|
<div class="setting-item"> |
|
<label for="end-time">再生終了秒数:</label> |
|
<div class="time-input-container"> |
|
<input type="number" id="end-time" min="0" value="0" step="0.01" disabled> |
|
<button class="time-set-button" id="set-end-time" disabled>現在の秒数に設定</button> |
|
<button class="time-set-button" id="reset-end-time" disabled>動画の長さに戻す</button> |
|
</div> |
|
</div> |
|
<div class="setting-item"> |
|
<label for="loop">ループ再生:</label> |
|
<input type="checkbox" id="loop" disabled> |
|
</div> |
|
<div class="setting-item"> |
|
<button class="combine-button" id="apply-time-btn" disabled>時間設定を適用</button> |
|
</div> |
|
<h2>設定</h2> |
|
|
|
<div class="setting-item"> |
|
<label for="hide-video">消画モード:</label> |
|
<input type="checkbox" id="hide-video"> |
|
</div> |
|
<div class="setting-item"> |
|
<div class="global-volume-container"> |
|
<label>全体音量係数:</label> |
|
<input type="range" class="global-volume-slider" id="global-volume" min="0" max="10" step="0.01" value="5" disabled> |
|
<span class="slider-value" id="global-volume-value">0.5</span> |
|
</div> |
|
</div> |
|
<div class="setting-item"> |
|
<div class="playback-speed-container"> |
|
<label>再生速度:</label> |
|
<input type="range" class="playback-speed-slider" id="playback-speed" min="0.01" max="5" step="0.001" value="1" disabled> |
|
<span class="slider-value" id="playback-speed-value">1.00x</span> |
|
</div> |
|
</div> |
|
<div class="setting-item"> |
|
<label for="tempo">テンポ (BPM):</label> |
|
<input type="number" id="tempo" min="40" max="200" value="92" step="0.1"> |
|
<span id="tempo-speed-value">1.00x</span> |
|
</div> |
|
<span>↑練習したいテンポを入力するとそのテンポに合わせて再生速度スライダーを変更できます。 |
|
<br>楽譜に記載された「♪=」の横のテンポ(時の旅人は約92、地球星歌は約66)を基準に計算します。</span> |
|
</div> |
|
</div> |
|
|
|
<button class="lock-controls-btn" id="lock-controls-btn" title="コントロールバーを固定">🔒</button> |
|
<script> |
|
document.addEventListener('DOMContentLoaded', function() { |
|
|
|
let lastSyncTime = 0; |
|
let isBuffering = false; |
|
let syncDriftLog = []; |
|
let syncCheckInterval; |
|
let audioContext; |
|
let controlsHideTimeout; |
|
let isControlsLocked = false; |
|
let controlsVisible = true; |
|
let isCheckingSync = false; |
|
let isInBackgroundTab = false; |
|
|
|
try { |
|
audioContext = new(window.AudioContext || window.webkitAudioContext) |
|
(); |
|
} catch (e) { |
|
console.error('Web Audio APIがサポートされていません:', e); |
|
} |
|
|
|
|
|
function createTechBackground() { |
|
const bg = document.getElementById('techBg'); |
|
|
|
for (let i = 0; i < 200; i++) { |
|
const line = document.createElement('div'); |
|
line.className = 'circuit-line'; |
|
|
|
const isHorizontal = Math.random() > 0.5; |
|
if (isHorizontal) { |
|
line.style.width = `${Math.random() * 300 + 100}px`; |
|
line.style.height = '1px'; |
|
} else { |
|
line.style.width = '1px'; |
|
line.style.height = `${Math.random() * 300 + 100}px`; |
|
} |
|
|
|
line.style.left = `${Math.random() * 100}%`; |
|
line.style.top = `${Math.random() * 100}%`; |
|
line.style.opacity = Math.random() * 0.5 + 0.1; |
|
bg.appendChild(line); |
|
} |
|
|
|
for (let i = 0; i < 200; i++) { |
|
const dot = document.createElement('div'); |
|
dot.className = 'grid-dot'; |
|
dot.style.left = `${Math.random() * 100}%`; |
|
dot.style.top = `${Math.random() * 100}%`; |
|
bg.appendChild(dot); |
|
} |
|
|
|
for (let i = 0; i < 15; i++) { |
|
const hex = document.createElement('div'); |
|
hex.className = 'hexagon'; |
|
hex.style.left = `${Math.random() * 100}%`; |
|
hex.style.top = `${Math.random() * 100}%`; |
|
hex.style.transform = `rotate(${Math.random() * 360}deg)`; |
|
hex.style.animation = |
|
`float ${Math.random() * 10 + 5}s infinite ease-in-out`; |
|
bg.appendChild(hex); |
|
} |
|
|
|
for (let i = 0; i < 8; i++) { |
|
const pulse = document.createElement('div'); |
|
pulse.className = 'pulse'; |
|
pulse.style.left = `${Math.random() * 100}%`; |
|
pulse.style.top = `${Math.random() * 100}%`; |
|
pulse.style.animationDelay = `${Math.random() * 2}s`; |
|
bg.appendChild(pulse); |
|
} |
|
} |
|
|
|
createTechBackground(); |
|
|
|
const urlParams = new URLSearchParams(window.location.search); |
|
const isTMode = urlParams.has('mode') && urlParams.get('mode') === |
|
't'; |
|
const basePath = isTMode ? '/t/' : '/'; |
|
|
|
|
|
let loadingCount = 0; |
|
let totalToLoad = 6; |
|
let lastUpdateTime = 0; |
|
const updateInterval = 1; |
|
|
|
function checkLoadingComplete() { |
|
loadingCount++; |
|
if (loadingCount >= totalToLoad) { |
|
setTimeout(function() { |
|
const loadingOverlay = document.getElementById( |
|
'loadingOverlay'); |
|
loadingOverlay.style.opacity = '0'; |
|
setTimeout(function() { |
|
loadingOverlay.style.display = 'none'; |
|
}, 1000); |
|
}, 500); |
|
} |
|
} |
|
|
|
function handleError(error, message) { |
|
console.error(message, error); |
|
window.alert(`${message}\n\nエラー詳細: ${error.message}`); |
|
} |
|
|
|
|
|
const video = document.getElementById('video'); |
|
video.preservesPitch = true; |
|
video.mozPreservesPitch = true; |
|
video.webkitPreservesPitch = true; |
|
const videoContainer = document.getElementById('video-container'); |
|
const playPauseBtn = document.getElementById('play-pause-btn'); |
|
const timeDisplay = document.getElementById('time-display'); |
|
const progressContainer = document.getElementById( |
|
'progress-container'); |
|
const progressBar = document.getElementById('progress-bar'); |
|
const progressTime = document.getElementById('progress-time'); |
|
const volumeBtn = document.getElementById('volume-btn'); |
|
const volumeSlider = document.getElementById('volume-slider'); |
|
const speedSlider = document.getElementById('speed-slider'); |
|
const speedValue = document.getElementById('speed-value'); |
|
const playbackSpeedSlider = document.getElementById( |
|
'playback-speed'); |
|
const playbackSpeedValue = document.getElementById( |
|
'playback-speed-value'); |
|
const fullscreenBtn = document.getElementById('fullscreen-btn'); |
|
const startTimeInput = document.getElementById('start-time'); |
|
const endTimeInput = document.getElementById('end-time'); |
|
const loopCheckbox = document.getElementById('loop'); |
|
const globalVolumeSlider = document.getElementById( |
|
'global-volume'); |
|
const globalVolumeValue = document.getElementById( |
|
'global-volume-value'); |
|
const audioSliders = document.querySelectorAll('.audio-slider'); |
|
const volumeValues = document.querySelectorAll('.volume-value'); |
|
const setStartTimeBtn = document.getElementById('set-start-time'); |
|
const setEndTimeBtn = document.getElementById('set-end-time'); |
|
const resetEndTimeBtn = document.getElementById('reset-end-time'); |
|
const disabledOverlay = document.getElementById( |
|
'disabledOverlay'); |
|
const combineButton = document.getElementById('combine-button'); |
|
const combineStatus = document.getElementById('combine-status'); |
|
const previewSection = document.getElementById('preview-section'); |
|
const previewButton = document.getElementById('preview-button'); |
|
const previewTime = document.getElementById('preview-time'); |
|
const bufferingIndicator = document.getElementById( |
|
'buffering-indicator'); |
|
const syncStatus = document.getElementById('sync-status'); |
|
const syncStatusText = document.getElementById( |
|
'sync-status-text'); |
|
const syncStatusClose = document.getElementById( |
|
'sync-status-close'); |
|
const lockControlsBtn = document.getElementById( |
|
'lock-controls-btn'); |
|
const startMarker = document.getElementById('start-marker'); |
|
const endMarker = document.getElementById('end-marker'); |
|
const tempoInput = document.getElementById('tempo'); |
|
const tempoSpeedValue = document.getElementById( |
|
'tempo-speed-value'); |
|
const videoControls = document.querySelector('.video-controls'); |
|
const applyTimeBtn = document.getElementById('apply-time-btn'); |
|
const pipBtn = document.getElementById('pip-btn'); |
|
const resetBtn = document.getElementById('reset-btn'); |
|
|
|
|
|
const audioElements = {}; |
|
const audioBuffers = {}; |
|
const audioFiles = ['p', 'a', 't', 's', 'k']; |
|
let combinedAudioElement = null; |
|
let isAudioCombined = false; |
|
let currentVolumes = { |
|
p: 0, |
|
a: 1, |
|
t: 1, |
|
s: 1, |
|
k: 0 |
|
}; |
|
|
|
|
|
let videoDuration = 0; |
|
let isPlaying = false; |
|
let lastVolume = 1; |
|
let currentPlaybackRate = 1; |
|
let isFullscreen = false; |
|
|
|
async function enterPiP() { |
|
if (!document.pictureInPictureElement && !video.paused) { |
|
try { |
|
await video.requestPictureInPicture(); |
|
} catch (err) { |
|
console.warn('PiP開始失敗:', err); |
|
} |
|
} |
|
} |
|
|
|
async function exitPiP() { |
|
if (document.pictureInPictureElement) { |
|
try { |
|
await document.exitPictureInPicture(); |
|
} catch (err) { |
|
console.warn('PiP終了失敗:', err); |
|
} |
|
} |
|
} |
|
const hideVideoCheckbox = document.getElementById('hide-video'); |
|
hideVideoCheckbox.addEventListener('change', function() { |
|
video.style.backgroundColor = this.checked ? 'black' : ''; |
|
video.style.opacity = this.checked ? '0' : '1'; |
|
}); |
|
|
|
|
|
video.addEventListener('waiting', function() { |
|
isBuffering = true; |
|
bufferingIndicator.style.display = 'block'; |
|
if (combinedAudioElement) { |
|
combinedAudioElement.pause(); |
|
} |
|
}); |
|
|
|
video.addEventListener('playing', function() { |
|
isBuffering = false; |
|
bufferingIndicator.style.display = 'none'; |
|
if (combinedAudioElement && isPlaying) { |
|
syncAudioWithVideo(); |
|
} |
|
}); |
|
|
|
video.addEventListener('suspend', function() { |
|
console.log('動画の読み込みが一時停止しました'); |
|
}); |
|
|
|
video.addEventListener('stalled', function() { |
|
console.log('動画の読み込みが停滞しました'); |
|
if (isPlaying) { |
|
pauseMedia(); |
|
} |
|
}); |
|
|
|
function updatePipButton() { |
|
if (document.pictureInPictureElement) { |
|
pipBtn.textContent = '↸'; |
|
pipBtn.title = 'ピクチャーインピクチャーを終了'; |
|
} else { |
|
pipBtn.textContent = '⇲'; |
|
pipBtn.title = 'ピクチャーインピクチャーで再生'; |
|
} |
|
} |
|
|
|
pipBtn.addEventListener('click', async () => { |
|
try { |
|
if (document.pictureInPictureElement) { |
|
await document.exitPictureInPicture(); |
|
} else { |
|
await video.requestPictureInPicture(); |
|
} |
|
updatePipButton(); |
|
} catch (err) { |
|
console.error('PiP操作エラー:', err); |
|
} |
|
}); |
|
video.addEventListener('enterpictureinpicture', updatePipButton); |
|
video.addEventListener('leavepictureinpicture', async () => { |
|
updatePipButton(); |
|
if (isPlaying && !document.hidden) { |
|
try { |
|
await video.play(); |
|
} catch (err) { |
|
console.error('PiP終了後の再生エラー:', err); |
|
} |
|
} |
|
}); |
|
|
|
|
|
resetBtn.addEventListener('click', () => { |
|
const startTime = parseFloat(startTimeInput.value) || 0; |
|
seekMedia(startTime); |
|
if (isPlaying) { |
|
playMedia(); |
|
} |
|
}); |
|
|
|
|
|
updatePipButton(); |
|
|
|
|
|
function startSyncCheck() { |
|
if (isCheckingSync) return; |
|
isCheckingSync = true; |
|
if (syncCheckInterval) clearInterval(syncCheckInterval); |
|
syncCheckInterval = setInterval(checkSync, 1000); |
|
} |
|
|
|
function stopSyncCheck() { |
|
isCheckingSync = false; |
|
if (syncCheckInterval) clearInterval(syncCheckInterval); |
|
} |
|
|
|
function checkSync() { |
|
if (!isAudioCombined || !isPlaying || isBuffering || |
|
isInBackgroundTab) return; |
|
|
|
const videoTime = video.currentTime; |
|
const audioTime = combinedAudioElement.currentTime; |
|
const drift = videoTime - audioTime; |
|
|
|
syncDriftLog.push(drift); |
|
if (syncDriftLog.length > 5) syncDriftLog.shift(); |
|
|
|
const avgDrift = syncDriftLog.reduce((a, b) => a + b, 0) / |
|
syncDriftLog.length; |
|
syncStatusText.textContent = `同期ズレ: ${avgDrift.toFixed(3)}秒`; |
|
|
|
if (Math.abs(avgDrift) > 0.1) { |
|
console.log(`同期ズレを修正: ${avgDrift.toFixed(3)}秒`); |
|
syncAudioWithVideo(); |
|
} |
|
} |
|
|
|
applyTimeBtn.addEventListener('click', function() { |
|
|
|
const wasPlaying = isPlaying; |
|
if (isPlaying) { |
|
pauseMedia(); |
|
} |
|
|
|
|
|
const startTime = parseFloat(startTimeInput.value) || 0; |
|
const endTime = parseFloat(endTimeInput.value) || video.duration; |
|
|
|
|
|
if (video.currentTime < startTime) { |
|
video.currentTime = startTime; |
|
if (combinedAudioElement) { |
|
combinedAudioElement.currentTime = startTime; |
|
} |
|
} |
|
|
|
else if (video.currentTime > endTime) { |
|
video.currentTime = startTime; |
|
if (combinedAudioElement) { |
|
combinedAudioElement.currentTime = startTime; |
|
} |
|
} |
|
|
|
|
|
if (wasPlaying) { |
|
playMedia(); |
|
} |
|
|
|
|
|
updateProgressMarkers(); |
|
}); |
|
|
|
|
|
function syncAudioWithVideo() { |
|
if (!isAudioCombined || !isPlaying) return; |
|
|
|
const currentTime = video.currentTime; |
|
|
|
if (combinedAudioElement) { |
|
combinedAudioElement.currentTime = currentTime; |
|
if (isPlaying) { |
|
combinedAudioElement.play().catch(e => console.error( |
|
'音声再生エラー:', e)); |
|
} |
|
} |
|
|
|
|
|
syncDriftLog = []; |
|
lastSyncTime = performance.now(); |
|
} |
|
|
|
|
|
function loadAudioFiles() { |
|
audioFiles.forEach(file => { |
|
try { |
|
const audio = new Audio(`${basePath}${file}.mp3`); |
|
audio.preload = 'auto'; |
|
audio.loop = false; |
|
audioElements[file] = audio; |
|
|
|
audio.addEventListener('loadedmetadata', function() { |
|
console.log(`${basePath}${file}.mp3 loaded`); |
|
checkLoadingComplete(); |
|
}); |
|
|
|
audio.addEventListener('error', function() { |
|
console.error(`音声ファイル読み込みエラー (${basePath}${file}.mp3):`, |
|
audio.error); |
|
checkLoadingComplete(); |
|
}); |
|
} catch (error) { |
|
console.error(`音声ファイル初期化エラー (${basePath}${file}.mp3):`, |
|
error); |
|
checkLoadingComplete(); |
|
} |
|
}); |
|
} |
|
|
|
|
|
async function combineAudio() { |
|
if (combinedAudioElement) { |
|
combinedAudioElement.pause(); |
|
combinedAudioElement.src = ''; |
|
URL.revokeObjectURL(combinedAudioElement.src); |
|
} |
|
combineButton.disabled = true; |
|
combineStatus.textContent = "音声を合成中..."; |
|
|
|
try { |
|
|
|
audioFiles.forEach(file => { |
|
currentVolumes[file] = parseFloat(document.querySelector( |
|
`.audio-slider[data-audio="${file}"]`).value); |
|
}); |
|
|
|
|
|
const audioBufferPromises = audioFiles.map(async file => { |
|
const audio = audioElements[file]; |
|
if (!audio) return null; |
|
|
|
|
|
if (currentVolumes[file] === 0) return null; |
|
|
|
const response = await fetch(`${basePath}${file}.mp3`); |
|
const arrayBuffer = await response.arrayBuffer(); |
|
return await audioContext.decodeAudioData(arrayBuffer); |
|
}); |
|
|
|
|
|
const buffers = await Promise.all(audioBufferPromises); |
|
audioFiles.forEach((file, index) => { |
|
audioBuffers[file] = buffers[index]; |
|
}); |
|
|
|
|
|
const maxDuration = Math.max(...buffers.filter(b => b).map(b => |
|
b.duration)); |
|
|
|
|
|
const combinedAudioBuffer = audioContext.createBuffer( |
|
2, |
|
audioContext.sampleRate * maxDuration, |
|
audioContext.sampleRate |
|
); |
|
|
|
|
|
for (let file of audioFiles) { |
|
if (!audioBuffers[file] || currentVolumes[file] === 0) |
|
continue; |
|
|
|
const buffer = audioBuffers[file]; |
|
const volume = currentVolumes[file]; |
|
|
|
|
|
for (let channel = 0; channel < 2; channel++) { |
|
const inputData = buffer.getChannelData(channel % buffer.numberOfChannels); |
|
const outputData = combinedAudioBuffer.getChannelData( |
|
channel); |
|
|
|
for (let i = 0; i < inputData.length; i++) { |
|
outputData[i] += inputData[i] * volume; |
|
} |
|
} |
|
} |
|
|
|
|
|
for (let channel = 0; channel < 2; channel++) { |
|
const outputData = combinedAudioBuffer.getChannelData(channel); |
|
let max = 0; |
|
|
|
for (let i = 0; i < outputData.length; i++) { |
|
if (Math.abs(outputData[i]) > max) { |
|
max = Math.abs(outputData[i]); |
|
} |
|
} |
|
|
|
if (max > 1) { |
|
for (let i = 0; i < outputData.length; i++) { |
|
outputData[i] /= max; |
|
} |
|
} |
|
} |
|
|
|
|
|
const blob = bufferToWave(combinedAudioBuffer); |
|
const url = URL.createObjectURL(blob); |
|
|
|
|
|
combinedAudioElement = new Audio(url); |
|
combinedAudioElement.preservesPitch = true; |
|
combinedAudioElement.mozPreservesPitch = true; |
|
combinedAudioElement.webkitPreservesPitch = true; |
|
combinedAudioElement.playbackRate = currentPlaybackRate; |
|
|
|
isAudioCombined = true; |
|
combineStatus.textContent = "音声の合成が完了しました"; |
|
enablePlayerControls(); |
|
|
|
combineButton.disabled = false; |
|
|
|
document.addEventListener('visibilitychange', async () => { |
|
if (document.hidden) { |
|
isInBackgroundTab = true; |
|
|
|
if (!document.pictureInPictureElement) { |
|
video.pause(); |
|
} |
|
|
|
if (combinedAudioElement && !combinedAudioElement.paused) { |
|
await combinedAudioElement.play(); |
|
} |
|
if (tl.isActive()) { |
|
tl.play(); |
|
} |
|
} else { |
|
isInBackgroundTab = false; |
|
if (combinedAudioElement && !combinedAudioElement.paused) { |
|
video.currentTime = combinedAudioElement.currentTime; |
|
if (isPlaying) { |
|
await video.play(); |
|
} |
|
} |
|
} |
|
}); |
|
|
|
|
|
|
|
video.addEventListener('ended', exitPiP); |
|
|
|
applyVolume(); |
|
applyPlaybackRate(); |
|
|
|
} catch (error) { |
|
console.error('音声合成エラー:', error); |
|
combineStatus.textContent = "音声の合成に失敗しました"; |
|
combineButton.disabled = false; |
|
} |
|
} |
|
|
|
function bufferToWave(abuffer) { |
|
const numOfChan = abuffer.numberOfChannels, |
|
length = abuffer.length * numOfChan * 2 + 44, |
|
buffer = new ArrayBuffer(length), |
|
view = new DataView(buffer), |
|
channels = [], |
|
sampleRate = abuffer.sampleRate; |
|
|
|
|
|
let pos = 0; |
|
|
|
|
|
setUint32(0x46464952); |
|
setUint32(length - 8); |
|
setUint32(0x45564157); |
|
|
|
setUint32(0x20746d66); |
|
setUint32(16); |
|
setUint16(1); |
|
setUint16(numOfChan); |
|
setUint32(sampleRate); |
|
setUint32(sampleRate * 2 * numOfChan); |
|
setUint16(numOfChan * 2); |
|
setUint16(16); |
|
|
|
setUint32(0x61746164); |
|
setUint32(length - pos - 4); |
|
|
|
|
|
for (let i = 0; i < abuffer.length; i++) { |
|
for (let channel = 0; channel < numOfChan; channel++) { |
|
let sample = abuffer.getChannelData(channel)[i] * 0x7fff; |
|
if (sample < -32768) sample = -32768; |
|
if (sample > 32767) sample = 32767; |
|
view.setInt16(pos, sample, true); |
|
pos += 2; |
|
} |
|
} |
|
|
|
function setUint16(data) { |
|
view.setUint16(pos, data, true); |
|
pos += 2; |
|
} |
|
|
|
function setUint32(data) { |
|
view.setUint32(pos, data, true); |
|
pos += 4; |
|
} |
|
|
|
return new Blob([buffer], { |
|
type: 'audio/wav' |
|
}); |
|
} |
|
|
|
function applyVolume() { |
|
if (!isAudioCombined) return; |
|
|
|
|
|
const baseVolume = parseFloat(volumeSlider.value); |
|
|
|
const globalVolume = parseFloat(globalVolumeSlider.value) / 10; |
|
|
|
|
|
const finalVolume = Math.max(0, Math.min(1, baseVolume * |
|
globalVolume)); |
|
|
|
|
|
video.volume = finalVolume; |
|
if (combinedAudioElement) { |
|
combinedAudioElement.volume = finalVolume; |
|
} |
|
|
|
|
|
updateVolumeIcon(); |
|
} |
|
|
|
function applyPlaybackRate() { |
|
if (!isAudioCombined) return; |
|
|
|
const speed = parseFloat(playbackSpeedSlider.value); |
|
currentPlaybackRate = speed; |
|
video.playbackRate = speed; |
|
|
|
if (combinedAudioElement) { |
|
combinedAudioElement.playbackRate = speed; |
|
} |
|
|
|
speedValue.textContent = speed.toFixed(2) + 'x'; |
|
playbackSpeedValue.textContent = speed.toFixed(2) + 'x'; |
|
speedSlider.value = speed; |
|
} |
|
|
|
function enablePlayerControls() { |
|
disabledOverlay.style.display = 'none'; |
|
playPauseBtn.disabled = false; |
|
volumeBtn.disabled = false; |
|
volumeSlider.disabled = false; |
|
speedSlider.disabled = false; |
|
fullscreenBtn.disabled = false; |
|
startTimeInput.disabled = false; |
|
endTimeInput.disabled = false; |
|
resetEndTimeBtn.disabled = false; |
|
loopCheckbox.disabled = false; |
|
globalVolumeSlider.disabled = false; |
|
setStartTimeBtn.disabled = false; |
|
setEndTimeBtn.disabled = false; |
|
playbackSpeedSlider.disabled = false; |
|
applyTimeBtn.disabled = false; |
|
resetBtn.disabled = false; |
|
pipBtn.disabled = false; |
|
} |
|
|
|
|
|
function togglePreview() { |
|
if (!isAudioCombined || !combinedAudioElement) return; |
|
|
|
if (previewButton.textContent === '▶') { |
|
|
|
combinedAudioElement.currentTime = 0; |
|
combinedAudioElement.play() |
|
.then(() => { |
|
previewButton.innerHTML = '<svg xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 -960 960 960" width="24px" fill="#1f1f1f"><path d="M520-200v-560h240v560H520Zm-320 0v-560h240v560H200Zm400-80h80v-400h-80v400Zm-320 0h80v-400h-80v400Zm0-400v400-400Zm320 0v400-400Z"/></svg>'; |
|
|
|
|
|
const updatePreviewTime = () => { |
|
if (!combinedAudioElement || !isAudioCombined) return; |
|
|
|
const currentTime = combinedAudioElement.currentTime; |
|
const duration = combinedAudioElement.duration; |
|
|
|
if (currentTime >= duration) { |
|
previewButton.textContent = '▶'; |
|
previewTime.textContent = |
|
`00:00 / ${formatTime(duration)}`; |
|
return; |
|
} |
|
|
|
previewTime.textContent = |
|
`${formatTime(currentTime)} / ${formatTime(duration)}`; |
|
requestAnimationFrame(updatePreviewTime); |
|
}; |
|
|
|
updatePreviewTime(); |
|
}) |
|
.catch(e => console.error('プレビュー再生エラー:', e)); |
|
} else { |
|
|
|
combinedAudioElement.pause(); |
|
previewButton.textContent = '▶'; |
|
} |
|
} |
|
|
|
|
|
function formatTime(seconds) { |
|
const mins = Math.floor(seconds / 60); |
|
const secs = Math.floor(seconds % 60); |
|
return `${String(mins).padStart(2, '0')}:${String(secs).padStart(2, '0')}`; |
|
} |
|
|
|
|
|
video.addEventListener('loadedmetadata', function() { |
|
try { |
|
videoDuration = video.duration; |
|
endTimeInput.value = videoDuration.toFixed(2); |
|
endTimeInput.max = videoDuration; |
|
startTimeInput.max = videoDuration - 0.1; |
|
updateTimeDisplay(); |
|
checkLoadingComplete(); |
|
} catch (error) { |
|
handleError(error, '動画メタデータ読み込み中にエラーが発生しました'); |
|
} |
|
}); |
|
|
|
|
|
video.addEventListener('error', function() { |
|
handleError(video.error, '動画読み込み中にエラーが発生しました'); |
|
}); |
|
|
|
|
|
playPauseBtn.addEventListener('click', function() { |
|
const endTime = parseFloat(endTimeInput.value) || |
|
videoDuration; |
|
if (video.currentTime >= endTime) { |
|
const startTime = parseFloat(startTimeInput.value) || 0; |
|
seekMedia(startTime); |
|
} |
|
togglePlayPause(); |
|
}); |
|
|
|
|
|
function updateTimeDisplay() { |
|
const now = performance.now(); |
|
if (now - lastUpdateTime < updateInterval && !isFullscreen) |
|
return; |
|
lastUpdateTime = now; |
|
|
|
try { |
|
const currentTime = video.currentTime; |
|
const duration = video.duration || videoDuration; |
|
|
|
const currentMinutes = Math.floor(currentTime / 60); |
|
const currentSeconds = Math.floor(currentTime % 60); |
|
const currentMilliseconds = Math.floor((currentTime % 1) * 100); |
|
const durationMinutes = Math.floor(duration / 60); |
|
const durationSeconds = Math.floor(duration % 60); |
|
const durationMilliseconds = Math.floor((duration % 1) * 100); |
|
|
|
timeDisplay.textContent = |
|
`${String(currentMinutes).padStart(2, '0')}:${String(currentSeconds).padStart(2, '0')}.${String(currentMilliseconds).padStart(2, '0')} / ` + |
|
`${String(durationMinutes).padStart(2, '0')}:${String(durationSeconds).padStart(2, '0')}.${String(durationMilliseconds).padStart(2, '0')}`; |
|
|
|
const progressPercent = (currentTime / duration) * 100; |
|
progressBar.style.width = `${progressPercent}%`; |
|
} catch (error) { |
|
console.error('時間表示更新エラー:', error); |
|
} |
|
} |
|
|
|
|
|
function togglePlayPause() { |
|
if (isPlaying) { |
|
pauseMedia(); |
|
} else { |
|
playMedia(); |
|
} |
|
} |
|
|
|
function playMedia() { |
|
try { |
|
const duration = video.duration || videoDuration; |
|
const startTime = parseFloat(startTimeInput.value) || 0; |
|
const endTime = parseFloat(endTimeInput.value) || duration; |
|
|
|
if (video.currentTime >= endTime) { |
|
video.currentTime = startTime; |
|
} |
|
|
|
const playPromise = video.play(); |
|
|
|
if (playPromise !== undefined) { |
|
playPromise.then(() => { |
|
isPlaying = true; |
|
playPauseBtn.textContent = '⏸'; |
|
|
|
|
|
if (isAudioCombined) { |
|
combinedAudioElement.currentTime = video.currentTime; |
|
combinedAudioElement.play().catch(e => console.error( |
|
'音声再生エラー:', e)); |
|
} |
|
|
|
startSyncCheck(); |
|
|
|
video.playbackRate = currentPlaybackRate; |
|
}).catch(error => { |
|
console.error('動画再生エラー:', error); |
|
isPlaying = false; |
|
playPauseBtn.textContent = '▶'; |
|
}); |
|
} |
|
} catch (error) { |
|
console.error('メディア再生エラー:', error); |
|
isPlaying = false; |
|
playPauseBtn.textContent = '▶'; |
|
} |
|
} |
|
|
|
function pauseMedia() { |
|
try { |
|
video.pause(); |
|
isPlaying = false; |
|
playPauseBtn.textContent = '▶'; |
|
stopSyncCheck(); |
|
|
|
if (combinedAudioElement) { |
|
combinedAudioElement.pause(); |
|
} |
|
} catch (error) { |
|
console.error('メディア一時停止エラー:', error); |
|
} |
|
} |
|
|
|
|
|
video.addEventListener('timeupdate', function() { |
|
updateTimeDisplay(); |
|
}); |
|
|
|
|
|
progressContainer.addEventListener('click', function(e) { |
|
if (!video.duration) return; |
|
|
|
const rect = this.getBoundingClientRect(); |
|
const pos = (e.clientX - rect.left) / rect.width; |
|
const seekTime = pos * video.duration; |
|
|
|
seekMedia(seekTime); |
|
}); |
|
|
|
|
|
function seekMedia(time) { |
|
try { |
|
const duration = video.duration || videoDuration; |
|
const startTime = parseFloat(startTimeInput.value) || 0; |
|
const endTime = parseFloat(endTimeInput.value) || duration; |
|
|
|
const seekTime = Math.max(startTime, Math.min(time, endTime)); |
|
video.currentTime = seekTime; |
|
|
|
if (combinedAudioElement) { |
|
combinedAudioElement.currentTime = seekTime; |
|
if (isPlaying) { |
|
combinedAudioElement.play().catch(e => console.error( |
|
'音声再生エラー:', e)); |
|
} |
|
} |
|
} catch (error) { |
|
console.error('メディアシークエラー:', error); |
|
} |
|
} |
|
|
|
|
|
progressContainer.addEventListener('mousemove', function(e) { |
|
if (!video.duration) return; |
|
|
|
const rect = this.getBoundingClientRect(); |
|
const pos = (e.clientX - rect.left) / rect.width; |
|
const hoverTime = pos * video.duration; |
|
|
|
const minutes = Math.floor(hoverTime / 60); |
|
const seconds = Math.floor(hoverTime % 60); |
|
const milliseconds = Math.floor((hoverTime % 1) * 100); |
|
|
|
progressTime.textContent = |
|
`${String(minutes).padStart(2, '0')}:${String(seconds).padStart(2, '0')}.${String(milliseconds).padStart(2, '0')}`; |
|
progressTime.style.display = 'block'; |
|
progressTime.style.left = `${pos * 100}%`; |
|
}); |
|
|
|
progressContainer.addEventListener('mouseleave', function() { |
|
progressTime.style.display = 'none'; |
|
}); |
|
|
|
|
|
video.addEventListener('click', function() { |
|
togglePlayPause(); |
|
}); |
|
|
|
volumeSlider.addEventListener('input', function() { |
|
if (!isAudioCombined) return; |
|
|
|
lastVolume = parseFloat(this.value); |
|
applyVolume(); |
|
}); |
|
|
|
volumeBtn.addEventListener('click', function() { |
|
if (!isAudioCombined) return; |
|
|
|
if (video.volume > 0) { |
|
lastVolume = parseFloat(volumeSlider.value); |
|
volumeSlider.value = 0; |
|
} else { |
|
volumeSlider.value = lastVolume; |
|
} |
|
|
|
applyVolume(); |
|
}); |
|
|
|
|
|
function updateVolumeIcon() { |
|
if (video.volume === 0) { |
|
volumeBtn.textContent = '🔇'; |
|
} else if (video.volume < 3) { |
|
volumeBtn.textContent = '🔈'; |
|
} else { |
|
volumeBtn.textContent = '🔊'; |
|
} |
|
} |
|
|
|
|
|
speedSlider.addEventListener('input', function() { |
|
if (!isAudioCombined) return; |
|
|
|
const speed = parseFloat(this.value); |
|
speedValue.textContent = speed.toFixed(2) + 'x'; |
|
playbackSpeedSlider.value = speed; |
|
playbackSpeedValue.textContent = speed.toFixed(2) + 'x'; |
|
updatePlaybackRate(speed); |
|
}); |
|
|
|
|
|
playbackSpeedSlider.addEventListener('input', function() { |
|
if (!isAudioCombined) return; |
|
|
|
const speed = parseFloat(this.value); |
|
playbackSpeedValue.textContent = speed.toFixed(2) + 'x'; |
|
speedSlider.value = speed; |
|
speedValue.textContent = speed.toFixed(2) + 'x'; |
|
updatePlaybackRate(speed); |
|
}); |
|
|
|
|
|
tempoInput.addEventListener('input', function() { |
|
const tempo = parseFloat(this.value); |
|
const baseTempo = isTMode ? 66 : 92; |
|
const speed = tempo / baseTempo; |
|
|
|
const clampedSpeed = Math.max(0.001, Math.min(5.0, speed)); |
|
|
|
playbackSpeedSlider.value = clampedSpeed; |
|
playbackSpeedValue.textContent = clampedSpeed.toFixed(2) + |
|
'x'; |
|
speedSlider.value = clampedSpeed; |
|
speedValue.textContent = clampedSpeed.toFixed(2) + 'x'; |
|
tempoSpeedValue.textContent = clampedSpeed.toFixed(2) + 'x'; |
|
|
|
updatePlaybackRate(clampedSpeed); |
|
}); |
|
|
|
function updatePlaybackRate(speed) { |
|
if (!isAudioCombined) return; |
|
|
|
currentPlaybackRate = speed; |
|
video.playbackRate = speed; |
|
|
|
|
|
video.preservesPitch = true; |
|
video.mozPreservesPitch = true; |
|
video.webkitPreservesPitch = true; |
|
|
|
if (combinedAudioElement) { |
|
combinedAudioElement.playbackRate = speed; |
|
|
|
|
|
combinedAudioElement.preservesPitch = true; |
|
combinedAudioElement.mozPreservesPitch = true; |
|
combinedAudioElement.webkitPreservesPitch = true; |
|
} |
|
} |
|
|
|
|
|
fullscreenBtn.addEventListener('click', function() { |
|
if (!isFullscreen) { |
|
if (videoContainer.requestFullscreen) { |
|
videoContainer.requestFullscreen(); |
|
} else if (videoContainer.webkitRequestFullscreen) { |
|
videoContainer.webkitRequestFullscreen(); |
|
} else if (videoContainer.msRequestFullscreen) { |
|
videoContainer.msRequestFullscreen(); |
|
} |
|
} else { |
|
if (document.exitFullscreen) { |
|
document.exitFullscreen(); |
|
} else if (document.webkitExitFullscreen) { |
|
document.webkitExitFullscreen(); |
|
} else if (document.msExitFullscreen) { |
|
document.msExitFullscreen(); |
|
} |
|
} |
|
}); |
|
|
|
|
|
document.addEventListener('fullscreenchange', |
|
handleFullscreenChange); |
|
document.addEventListener('webkitfullscreenchange', |
|
handleFullscreenChange); |
|
document.addEventListener('msfullscreenchange', |
|
handleFullscreenChange); |
|
|
|
function handleFullscreenChange() { |
|
isFullscreen = !!(document.fullscreenElement || document.webkitFullscreenElement || |
|
document.msFullscreenElement); |
|
fullscreenBtn.textContent = isFullscreen ? '⛶' : '⛶'; |
|
video.controls = false; |
|
|
|
|
|
lockControlsBtn.style.display = isFullscreen ? 'flex' : 'none'; |
|
|
|
|
|
if (isFullscreen) { |
|
resetControlsHideTimer(); |
|
document.addEventListener('mousemove', |
|
handleFullscreenMouseMove); |
|
} else { |
|
document.removeEventListener('mousemove', |
|
handleFullscreenMouseMove); |
|
clearTimeout(controlsHideTimeout); |
|
showControls(); |
|
} |
|
} |
|
|
|
|
|
function handleFullscreenMouseMove() { |
|
if (!isControlsLocked) { |
|
showControls(); |
|
resetControlsHideTimer(); |
|
} |
|
} |
|
|
|
|
|
function showControls() { |
|
if (!controlsVisible) { |
|
videoControls.style.opacity = '1'; |
|
controlsVisible = true; |
|
} |
|
} |
|
|
|
|
|
function hideControls() { |
|
if (!isControlsLocked && controlsVisible) { |
|
videoControls.style.opacity = '0'; |
|
controlsVisible = false; |
|
} |
|
} |
|
|
|
|
|
function resetControlsHideTimer() { |
|
clearTimeout(controlsHideTimeout); |
|
if (!isControlsLocked) { |
|
controlsHideTimeout = setTimeout(hideControls, 1500); |
|
} |
|
} |
|
|
|
|
|
lockControlsBtn.addEventListener('click', function() { |
|
isControlsLocked = !isControlsLocked; |
|
this.classList.toggle('locked', isControlsLocked); |
|
|
|
if (isControlsLocked) { |
|
showControls(); |
|
clearTimeout(controlsHideTimeout); |
|
} else { |
|
resetControlsHideTimer(); |
|
} |
|
}); |
|
|
|
|
|
document.addEventListener('keydown', function(e) { |
|
if (e.key === 'Escape' && isFullscreen) { |
|
if (document.exitFullscreen) { |
|
document.exitFullscreen(); |
|
} else if (document.webkitExitFullscreen) { |
|
document.webkitExitFullscreen(); |
|
} else if (document.msExitFullscreen) { |
|
document.msExitFullscreen(); |
|
} |
|
} |
|
}); |
|
|
|
|
|
audioSliders.forEach((slider, index) => { |
|
slider.addEventListener('input', function() { |
|
const value = parseFloat(this.value); |
|
volumeValues[index].textContent = value.toFixed(2); |
|
|
|
const percent = value * 100; |
|
this.style.backgroundSize = `${percent}% 100%`; |
|
}); |
|
}); |
|
|
|
globalVolumeSlider.addEventListener('input', function() { |
|
const value = parseFloat(this.value); |
|
globalVolumeValue.textContent = value.toFixed(1); |
|
|
|
const percent = (value - this.min) / (this.max - this.min) * |
|
100; |
|
this.style.backgroundSize = `${percent}% 100%`; |
|
|
|
applyVolume(); |
|
}); |
|
|
|
|
|
loopCheckbox.addEventListener('change', function() { |
|
|
|
}); |
|
|
|
|
|
setStartTimeBtn.addEventListener('click', function() { |
|
startTimeInput.value = video.currentTime.toFixed(2); |
|
updateProgressMarkers(); |
|
}); |
|
|
|
|
|
setEndTimeBtn.addEventListener('click', function() { |
|
endTimeInput.value = video.currentTime.toFixed(2); |
|
updateProgressMarkers(); |
|
}); |
|
|
|
|
|
resetEndTimeBtn.addEventListener('click', function() { |
|
endTimeInput.value = video.duration.toFixed(2); |
|
updateProgressMarkers(); |
|
}); |
|
|
|
|
|
function updateProgressMarkers() { |
|
const duration = video.duration || 0; |
|
const startTime = parseFloat(startTimeInput.value) || 0; |
|
const endTime = parseFloat(endTimeInput.value) || duration; |
|
|
|
if (duration > 0) { |
|
if (startTime > 0) { |
|
startMarker.style.left = `${(startTime / duration) * 100}%`; |
|
startMarker.style.display = 'block'; |
|
} else { |
|
startMarker.style.display = 'none'; |
|
} |
|
|
|
if (endTime < duration) { |
|
endMarker.style.left = `${(endTime / duration) * 100}%`; |
|
endMarker.style.display = 'block'; |
|
} else { |
|
endMarker.style.display = 'none'; |
|
} |
|
} |
|
} |
|
|
|
|
|
startTimeInput.addEventListener('input', updateProgressMarkers); |
|
endTimeInput.addEventListener('input', updateProgressMarkers); |
|
|
|
|
|
combineButton.addEventListener('click', combineAudio); |
|
|
|
|
|
previewButton.addEventListener('click', togglePreview); |
|
|
|
|
|
syncStatusClose.addEventListener('click', function() { |
|
syncStatus.style.display = 'none'; |
|
}); |
|
|
|
|
|
loadAudioFiles(); |
|
updateVolumeIcon(); |
|
volumeSlider.value = video.volume; |
|
video.controls = false; |
|
|
|
|
|
function initSliderBackgrounds() { |
|
const sliders = [ |
|
volumeSlider, |
|
speedSlider, |
|
globalVolumeSlider, |
|
playbackSpeedSlider, |
|
...audioSliders |
|
]; |
|
|
|
sliders.forEach(slider => { |
|
if (slider) { |
|
slider.style.backgroundImage = |
|
'linear-gradient(#64ffda, #64ffda)'; |
|
slider.style.backgroundRepeat = 'no-repeat'; |
|
|
|
if (slider === globalVolumeSlider) { |
|
const percent = (slider.value - slider.min) / (slider.max - |
|
slider.min) * 100; |
|
slider.style.backgroundSize = `${percent}% 100%`; |
|
globalVolumeValue.textContent = slider.value; |
|
} else { |
|
slider.style.backgroundSize = |
|
`${slider.value * 100}% 100%`; |
|
} |
|
} |
|
}); |
|
} |
|
|
|
initSliderBackgrounds(); |
|
startSyncCheck(); |
|
|
|
|
|
tempoInput.value = isTMode ? 66 : 92; |
|
tempoInput.dispatchEvent(new Event('input')); |
|
}); |
|
|
|
document.addEventListener('DOMContentLoaded', function() { |
|
|
|
const startMarker = document.getElementById('start-marker'); |
|
const endMarker = document.getElementById('end-marker'); |
|
const video = document.getElementById('video'); |
|
const startTimeInput = document.getElementById('start-time'); |
|
const endTimeInput = document.getElementById('end-time'); |
|
|
|
|
|
const urlParams = new URLSearchParams(window.location.search); |
|
const isTMode = urlParams.has('mode') && urlParams.get('mode') === |
|
't'; |
|
|
|
|
|
const timeMarkers = isTMode ? [{ |
|
label: '①[1番]', |
|
time: 15.2 |
|
}, { |
|
label: '②[1番]', |
|
time: 43.8 |
|
}, { |
|
label: '①[2番]', |
|
time: 79.0 |
|
}, { |
|
label: '②[2番]', |
|
time: 108.0 |
|
}, { |
|
label: '③', |
|
time: 137.25 |
|
}, { |
|
label: '④', |
|
time: 162.55 |
|
}, { |
|
label: '⑤', |
|
time: 191.65 |
|
}] : [{ |
|
label: 'A', |
|
time: 8.35 |
|
}, { |
|
label: 'B', |
|
time: 72.1 |
|
}, { |
|
label: 'C', |
|
time: 98.67 |
|
}, { |
|
label: 'D', |
|
time: 155.83 |
|
}, { |
|
label: 'E', |
|
time: 192.55 |
|
}]; |
|
|
|
|
|
const timeMarkersContainer = document.getElementById( |
|
'time-markers-container'); |
|
|
|
timeMarkers.forEach(marker => { |
|
const markerElement = document.createElement('div'); |
|
markerElement.className = 'time-marker'; |
|
markerElement.innerHTML = |
|
`<b>${marker.label}</b>(${marker.time}秒)`; |
|
markerElement.dataset.time = marker.time; |
|
|
|
|
|
markerElement.addEventListener('dragstart', function(e) { |
|
e.dataTransfer.setData('text/plain', marker.time); |
|
this.classList.add('dragging'); |
|
}); |
|
|
|
|
|
markerElement.addEventListener('dragend', function() { |
|
this.classList.remove('dragging'); |
|
}); |
|
|
|
|
|
markerElement.draggable = true; |
|
|
|
|
|
markerElement.addEventListener('click', function() { |
|
startTimeInput.value = marker.time; |
|
updateProgressMarkers(); |
|
}); |
|
|
|
timeMarkersContainer.appendChild(markerElement); |
|
}); |
|
|
|
|
|
[startTimeInput, endTimeInput].forEach(input => { |
|
|
|
input.addEventListener('dragover', function(e) { |
|
e.preventDefault(); |
|
this.style.backgroundColor = 'rgba(100, 255, 218, 0.2)'; |
|
}); |
|
|
|
|
|
input.addEventListener('dragleave', function() { |
|
this.style.backgroundColor = ''; |
|
}); |
|
|
|
|
|
input.addEventListener('drop', function(e) { |
|
e.preventDefault(); |
|
this.style.backgroundColor = ''; |
|
|
|
const time = parseFloat(e.dataTransfer.getData( |
|
'text/plain')); |
|
if (!isNaN(time)) { |
|
this.value = time; |
|
updateProgressMarkers(); |
|
} |
|
}); |
|
}); |
|
|
|
|
|
startTimeInput.addEventListener('input', updateProgressMarkers); |
|
endTimeInput.addEventListener('input', updateProgressMarkers); |
|
|
|
|
|
function updateProgressMarkers() { |
|
const duration = video.duration || 0; |
|
const startTime = parseFloat(startTimeInput.value) || 0; |
|
const endTime = parseFloat(endTimeInput.value) || duration; |
|
|
|
if (duration > 0) { |
|
if (startMarker) startMarker.style.left = |
|
`${(startTime / duration) * 100}%`; |
|
if (endMarker) endMarker.style.left = |
|
`${(endTime / duration) * 100}%`; |
|
|
|
if (startMarker) startMarker.style.display = 'block'; |
|
if (endMarker) endMarker.style.display = 'block'; |
|
} |
|
} |
|
}); |
|
</script> |
|
</body> |
|
|
|
</html> |