Update index.html
Browse files- index.html +47 -169
index.html
CHANGED
|
@@ -2,7 +2,7 @@
|
|
| 2 |
<html lang="en">
|
| 3 |
<head>
|
| 4 |
<meta charset="UTF-8">
|
| 5 |
-
<title>One Piece Short: Nami's Fury (
|
| 6 |
<style>
|
| 7 |
:root {
|
| 8 |
--sky-top: #4FACFE;
|
|
@@ -63,7 +63,7 @@
|
|
| 63 |
padding: 20px;
|
| 64 |
text-align: center;
|
| 65 |
}
|
| 66 |
-
#guide-overlay { z-index: 110; display: none;
|
| 67 |
|
| 68 |
.btn-group { display: flex; gap: 15px; margin-top: 25px; }
|
| 69 |
|
|
@@ -81,22 +81,23 @@
|
|
| 81 |
}
|
| 82 |
#play-btn { background: #2ecc71; box-shadow: 0 5px 0 #27ae60; }
|
| 83 |
#play-btn:hover { transform: translateY(-2px); background: #2efc71; }
|
| 84 |
-
|
| 85 |
#rec-btn { background: #e74c3c; box-shadow: 0 5px 0 #c0392b; }
|
| 86 |
#rec-btn:hover { transform: translateY(-2px); background: #ff6b6b; }
|
| 87 |
|
| 88 |
.guide-step {
|
| 89 |
background: #333;
|
| 90 |
-
padding: 15px;
|
| 91 |
-
margin:
|
| 92 |
border-radius: 8px;
|
| 93 |
text-align: left;
|
| 94 |
-
width:
|
| 95 |
-
max-width:
|
| 96 |
font-size: 16px;
|
| 97 |
border-left: 5px solid #FFD700;
|
|
|
|
| 98 |
}
|
| 99 |
.highlight { color: #FFD700; font-weight: bold; }
|
|
|
|
| 100 |
|
| 101 |
#subtitle-box {
|
| 102 |
position: absolute;
|
|
@@ -124,24 +125,13 @@
|
|
| 124 |
|
| 125 |
#rec-status {
|
| 126 |
position: absolute;
|
| 127 |
-
top: 15px;
|
| 128 |
-
right: 15px;
|
| 129 |
background: rgba(231, 76, 60, 0.9);
|
| 130 |
-
color: white;
|
| 131 |
-
|
| 132 |
-
|
| 133 |
-
font-weight: bold;
|
| 134 |
-
font-size: 16px;
|
| 135 |
-
opacity: 0;
|
| 136 |
-
z-index: 95;
|
| 137 |
-
display: flex;
|
| 138 |
-
align-items: center;
|
| 139 |
-
gap: 10px;
|
| 140 |
-
}
|
| 141 |
-
.rec-dot {
|
| 142 |
-
width: 12px; height: 12px; background: #fff; border-radius: 50%;
|
| 143 |
-
animation: pulse 1s infinite;
|
| 144 |
}
|
|
|
|
| 145 |
@keyframes pulse { 0% { opacity: 1; } 50% { opacity: 0.3; } 100% { opacity: 1; } }
|
| 146 |
|
| 147 |
/* --- Animations --- */
|
|
@@ -172,8 +162,6 @@
|
|
| 172 |
|
| 173 |
<div id="main-container">
|
| 174 |
<div id="rec-status"><div class="rec-dot"></div> RECORDING...</div>
|
| 175 |
-
|
| 176 |
-
<!-- MAIN ANIMATION STAGE -->
|
| 177 |
<svg id="stage" viewBox="0 0 960 540" preserveAspectRatio="xMidYMid slice">
|
| 178 |
<defs>
|
| 179 |
<pattern id="wood-grain" width="100" height="20" patternUnits="userSpaceOnUse">
|
|
@@ -276,41 +264,35 @@
|
|
| 276 |
|
| 277 |
<div id="subtitle-box"> <div id="subtitle"></div> </div>
|
| 278 |
|
| 279 |
-
<!-- START OVERLAY -->
|
| 280 |
<div id="overlay">
|
| 281 |
<h1 style="color:#FFD700; text-shadow: 4px 4px #000; font-size: 48px; margin-bottom: 5px;">ONE PIECE SHORT</h1>
|
| 282 |
<h2 style="color:#fff; font-family: sans-serif; letter-spacing: 2px; margin-top: 0;">NAMI'S FURY</h2>
|
| 283 |
<div class="btn-group">
|
| 284 |
<button id="play-btn" class="main-btn">▶ JUST PLAY</button>
|
| 285 |
-
<button id="rec-btn" class="main-btn">🔴
|
| 286 |
</div>
|
| 287 |
<p style="color:#bbb; font-size: 14px; margin-top: 30px;">*Sound Required*</p>
|
| 288 |
</div>
|
| 289 |
|
| 290 |
-
<!-- RECORDING GUIDE OVERLAY -->
|
| 291 |
<div id="guide-overlay">
|
| 292 |
-
<h2 style="color:#FFD700;">HOW TO RECORD
|
| 293 |
-
<div class="guide-step">1.
|
| 294 |
-
<div class="guide-step">
|
| 295 |
-
<div class="guide-step">
|
| 296 |
-
<div class="guide-step">
|
| 297 |
-
<button id="confirm-rec-btn" class="main-btn" style="background:#FFD700; color:#000; margin-top:
|
| 298 |
-
<p style="color:#ccc; font-size:12px; margin-top:15px;">(If you click 'Cancel' on the browser popup, recording will fail)</p>
|
| 299 |
</div>
|
| 300 |
-
|
| 301 |
</div>
|
| 302 |
|
| 303 |
<script>
|
| 304 |
const el = (id) => document.getElementById(id);
|
| 305 |
const state = { audioCtx: null, voices: [], mediaRecorder: null, recordedChunks: [], isRecording: false };
|
| 306 |
|
| 307 |
-
// === AUDIO ENGINE ===
|
| 308 |
function initAudio() { if(!state.audioCtx) state.audioCtx = new (window.AudioContext || window.webkitAudioContext)(); }
|
| 309 |
function playSound(type) {
|
| 310 |
if (!state.audioCtx) return;
|
| 311 |
const ctx = state.audioCtx; const t = ctx.currentTime;
|
| 312 |
const mGain = ctx.createGain(); mGain.connect(ctx.destination);
|
| 313 |
-
|
| 314 |
if (type === 'ocean') {
|
| 315 |
const buf = ctx.createBuffer(1, ctx.sampleRate * 2, ctx.sampleRate);
|
| 316 |
const data = buf.getChannelData(0); for (let i = 0; i < ctx.sampleRate * 2; i++) data[i] = Math.random() * 2 - 1;
|
|
@@ -341,164 +323,60 @@
|
|
| 341 |
}
|
| 342 |
}
|
| 343 |
|
| 344 |
-
|
| 345 |
-
function
|
| 346 |
-
|
| 347 |
-
|
| 348 |
-
const v = window.speechSynthesis.getVoices();
|
| 349 |
-
if (v.length !== 0) { state.voices = v; clearInterval(id); resolve(); }
|
| 350 |
-
}, 50);
|
| 351 |
-
});
|
| 352 |
-
}
|
| 353 |
-
function getVoice(char) {
|
| 354 |
-
const v = state.voices; const en = v.filter(voice => voice.lang.startsWith('en')); const pool = en.length > 0 ? en : v;
|
| 355 |
-
if (char === 'nami') return pool.find(voice => voice.name.includes('Zira') || voice.name.includes('Female') || voice.name.includes('Google US English')) || pool[0];
|
| 356 |
-
else return pool.find(voice => voice.name.includes('David') || voice.name.includes('Male')) || pool[1] || pool[0];
|
| 357 |
-
}
|
| 358 |
-
function speak(text, char) {
|
| 359 |
-
return new Promise(resolve => {
|
| 360 |
-
const u = new SpeechSynthesisUtterance(text); u.voice = getVoice(char); u.lang = 'en-US';
|
| 361 |
-
if (char === 'luffy') { u.rate = 1.4; u.pitch = 1.5; } else { u.rate = 1.15; u.pitch = 1.1; }
|
| 362 |
-
u.volume = 1.0;
|
| 363 |
-
u.onstart = () => { el('subtitle').textContent = (char === 'nami' ? 'NAMI: ' : 'LUFFY: ') + text; el('subtitle').style.opacity = 1; }
|
| 364 |
-
u.onend = () => { el('subtitle').style.opacity = 0; setTimeout(resolve, 400); }
|
| 365 |
-
window.speechSynthesis.speak(u);
|
| 366 |
-
});
|
| 367 |
-
}
|
| 368 |
|
| 369 |
-
// === ANIMATION HELPERS ===
|
| 370 |
-
function setFace(char, type) {
|
| 371 |
-
if (char === 'luffy') { el('luffy-face-normal').classList.toggle('hidden', type === 'stupid'); el('luffy-face-stupid').classList.toggle('hidden', type !== 'stupid'); }
|
| 372 |
-
else { el('nami-face-normal').classList.toggle('hidden', type === 'angry'); el('nami-face-angry').classList.toggle('hidden', type !== 'angry'); el('nami-vein').classList.toggle('hidden', type !== 'angry'); }
|
| 373 |
-
}
|
| 374 |
-
|
| 375 |
-
// === RECORDER ===
|
| 376 |
async function startRecording() {
|
| 377 |
try {
|
| 378 |
-
//
|
| 379 |
-
const stream = await navigator.mediaDevices.getDisplayMedia({
|
| 380 |
-
video: { cursor: "never", displaySurface: "browser" },
|
| 381 |
-
audio: { echoCancellation: false, noiseSuppression: false, autoGainControl: false }
|
| 382 |
-
});
|
| 383 |
|
| 384 |
-
//
|
| 385 |
-
|
| 386 |
-
|
| 387 |
-
|
| 388 |
}
|
| 389 |
|
| 390 |
state.mediaRecorder = new MediaRecorder(stream, { mimeType: 'video/webm' });
|
| 391 |
state.recordedChunks = [];
|
| 392 |
state.mediaRecorder.ondataavailable = (e) => { if (e.data.size > 0) state.recordedChunks.push(e.data); };
|
| 393 |
state.mediaRecorder.onstop = downloadVideo;
|
| 394 |
-
|
| 395 |
state.mediaRecorder.start();
|
| 396 |
state.isRecording = true;
|
| 397 |
el('rec-status').style.opacity = 1;
|
| 398 |
-
|
| 399 |
-
// If user manually clicks "Stop Sharing" on the browser banner, stop recording cleanly
|
| 400 |
stream.getVideoTracks()[0].onended = () => { if (state.mediaRecorder && state.mediaRecorder.state !== 'inactive') stopRecording(); };
|
| 401 |
return true;
|
| 402 |
} catch (err) {
|
| 403 |
-
console.error("Recording failed:", err);
|
| 404 |
-
// Catch "Permission Denied" error if they clicked Cancel
|
| 405 |
-
alert("Recording failed or was cancelled. You must click 'Share' in the browser popup to record.");
|
| 406 |
return false;
|
| 407 |
}
|
| 408 |
}
|
|
|
|
|
|
|
| 409 |
|
| 410 |
-
function stopRecording() {
|
| 411 |
-
if (state.mediaRecorder && state.mediaRecorder.state !== 'inactive') {
|
| 412 |
-
state.mediaRecorder.stop();
|
| 413 |
-
if (state.mediaRecorder.stream) state.mediaRecorder.stream.getTracks().forEach(track => track.stop());
|
| 414 |
-
}
|
| 415 |
-
state.isRecording = false;
|
| 416 |
-
el('rec-status').style.opacity = 0;
|
| 417 |
-
}
|
| 418 |
-
|
| 419 |
-
function downloadVideo() {
|
| 420 |
-
if (state.recordedChunks.length === 0) return;
|
| 421 |
-
const blob = new Blob(state.recordedChunks, { type: 'video/webm' });
|
| 422 |
-
const url = URL.createObjectURL(blob);
|
| 423 |
-
const a = document.createElement('a');
|
| 424 |
-
a.style.display = 'none'; a.href = url; a.download = 'one_piece_nami_fury.webm';
|
| 425 |
-
document.body.appendChild(a); a.click();
|
| 426 |
-
setTimeout(() => { document.body.removeChild(a); window.URL.revokeObjectURL(url); }, 100);
|
| 427 |
-
}
|
| 428 |
-
|
| 429 |
-
// === TIMELINE ===
|
| 430 |
async function runEpisode() {
|
| 431 |
-
el('guide-overlay').style.display = 'none';
|
| 432 |
-
|
| 433 |
-
|
| 434 |
-
|
| 435 |
-
await new Promise(r => setTimeout(r,
|
| 436 |
-
el('luffy').
|
| 437 |
-
|
| 438 |
-
|
| 439 |
-
el('
|
| 440 |
-
await speak("Luffy, shut up for a second! I'm trying to read this map.", 'nami');
|
| 441 |
-
await speak("If you bother me one more time, I'm tripling your debt!", 'nami');
|
| 442 |
-
await speak("Ehhh? Maps are boring...", 'luffy');
|
| 443 |
-
setFace('luffy', 'stupid'); playSound('stretch');
|
| 444 |
-
el('luffy-arm-l').style.transition = "all 0.8s cubic-bezier(0.68, -0.55, 0.27, 1.55)";
|
| 445 |
-
el('luffy-arm-l').setAttribute('d', 'M-35,10 Q-200,80 -250,20');
|
| 446 |
-
await new Promise(r => setTimeout(r, 1200));
|
| 447 |
-
setFace('nami', 'angry');
|
| 448 |
-
await speak("I SAID SHUT UP!", 'nami');
|
| 449 |
-
el('luffy-arm-l').setAttribute('d', 'M-35,10 Q-60,50 -50,90');
|
| 450 |
-
el('nami').style.transition = "transform 0.3s cubic-bezier(0.18, 0.89, 0.32, 1.28)";
|
| 451 |
-
el('nami').style.transform = "translate(250px, 320px) rotate(-25deg) scale(1.1)";
|
| 452 |
-
await new Promise(r => setTimeout(r, 400));
|
| 453 |
-
playSound('punch'); playSound('spin');
|
| 454 |
-
el('stage').classList.add('shake-screen');
|
| 455 |
-
el('nami').style.transition = "transform 0.1s";
|
| 456 |
-
el('nami').style.transform = "translate(450px, 320px) rotate(15deg) scale(1.2)";
|
| 457 |
-
el('impact-star').classList.remove('hidden'); el('impact-star').style.transition = "all 0.15s ease-out";
|
| 458 |
-
el('impact-star').style.transform = "translate(580px,250px) scale(4) rotate(180deg)";
|
| 459 |
-
el('luffy').classList.remove('breathe'); el('luffy').classList.add('luffy-spin-crash');
|
| 460 |
-
await new Promise(r => setTimeout(r, 400));
|
| 461 |
-
el('stage').classList.remove('shake-screen'); el('impact-star').style.opacity = 0;
|
| 462 |
-
await new Promise(r => setTimeout(r, 2500));
|
| 463 |
-
el('nami').style.transition = "transform 0.8s ease-in-out";
|
| 464 |
-
el('nami').style.transform = "translate(300px, 320px) rotate(0deg)";
|
| 465 |
-
setFace('nami', 'normal');
|
| 466 |
-
await speak("*Sigh*... what an idiot captain. Such a pain.", 'nami');
|
| 467 |
-
await new Promise(r => setTimeout(r, 1000));
|
| 468 |
-
|
| 469 |
if (state.isRecording) stopRecording();
|
| 470 |
-
|
| 471 |
-
el('overlay').style.display = 'flex'; el('overlay').style.opacity = 1;
|
| 472 |
-
el('overlay').innerHTML = "<h1 style='color:white'>FINISH</h1><button onclick='location.reload()' style='padding:15px 30px; font-size:20px; cursor:pointer; border-radius:10px; border:none; background:#FFD700;'>REPLAY</button>";
|
| 473 |
}
|
| 474 |
|
| 475 |
-
|
| 476 |
-
el('
|
| 477 |
-
el('play-btn').textContent = "LOADING...";
|
| 478 |
-
initAudio(); await initVoices();
|
| 479 |
-
runEpisode();
|
| 480 |
-
};
|
| 481 |
-
|
| 482 |
-
el('rec-btn').onclick = () => {
|
| 483 |
-
el('overlay').style.opacity = 0; // Hide main overlay briefly to show guide clearly
|
| 484 |
-
setTimeout(() => { el('overlay').style.display = 'none'; el('guide-overlay').style.display = 'flex'; }, 300);
|
| 485 |
-
};
|
| 486 |
-
|
| 487 |
el('confirm-rec-btn').onclick = async () => {
|
| 488 |
-
el('confirm-rec-btn').disabled = true;
|
| 489 |
-
el('confirm-rec-btn').textContent = "WAITING FOR PERMISSION...";
|
| 490 |
initAudio(); await initVoices();
|
| 491 |
const success = await startRecording();
|
| 492 |
-
if (success)
|
| 493 |
-
|
| 494 |
-
} else {
|
| 495 |
-
// Reset if failed/cancelled
|
| 496 |
-
el('guide-overlay').style.display = 'none';
|
| 497 |
-
el('overlay').style.display = 'flex';
|
| 498 |
-
el('overlay').style.opacity = 1;
|
| 499 |
-
el('confirm-rec-btn').disabled = false;
|
| 500 |
-
el('confirm-rec-btn').textContent = "I UNDERSTAND - LET'S GO";
|
| 501 |
-
}
|
| 502 |
};
|
| 503 |
</script>
|
| 504 |
</body>
|
|
|
|
| 2 |
<html lang="en">
|
| 3 |
<head>
|
| 4 |
<meta charset="UTF-8">
|
| 5 |
+
<title>One Piece Short: Nami's Fury (Universal Record)</title>
|
| 6 |
<style>
|
| 7 |
:root {
|
| 8 |
--sky-top: #4FACFE;
|
|
|
|
| 63 |
padding: 20px;
|
| 64 |
text-align: center;
|
| 65 |
}
|
| 66 |
+
#guide-overlay { z-index: 110; display: none; }
|
| 67 |
|
| 68 |
.btn-group { display: flex; gap: 15px; margin-top: 25px; }
|
| 69 |
|
|
|
|
| 81 |
}
|
| 82 |
#play-btn { background: #2ecc71; box-shadow: 0 5px 0 #27ae60; }
|
| 83 |
#play-btn:hover { transform: translateY(-2px); background: #2efc71; }
|
|
|
|
| 84 |
#rec-btn { background: #e74c3c; box-shadow: 0 5px 0 #c0392b; }
|
| 85 |
#rec-btn:hover { transform: translateY(-2px); background: #ff6b6b; }
|
| 86 |
|
| 87 |
.guide-step {
|
| 88 |
background: #333;
|
| 89 |
+
padding: 12px 15px;
|
| 90 |
+
margin: 8px;
|
| 91 |
border-radius: 8px;
|
| 92 |
text-align: left;
|
| 93 |
+
width: 85%;
|
| 94 |
+
max-width: 550px;
|
| 95 |
font-size: 16px;
|
| 96 |
border-left: 5px solid #FFD700;
|
| 97 |
+
line-height: 1.4;
|
| 98 |
}
|
| 99 |
.highlight { color: #FFD700; font-weight: bold; }
|
| 100 |
+
.alt-option { color: #3498db; font-weight: bold; }
|
| 101 |
|
| 102 |
#subtitle-box {
|
| 103 |
position: absolute;
|
|
|
|
| 125 |
|
| 126 |
#rec-status {
|
| 127 |
position: absolute;
|
| 128 |
+
top: 15px; right: 15px;
|
|
|
|
| 129 |
background: rgba(231, 76, 60, 0.9);
|
| 130 |
+
color: white; padding: 8px 20px; border-radius: 30px;
|
| 131 |
+
font-weight: bold; font-size: 16px; opacity: 0; z-index: 95;
|
| 132 |
+
display: flex; align-items: center; gap: 10px;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 133 |
}
|
| 134 |
+
.rec-dot { width: 12px; height: 12px; background: #fff; border-radius: 50%; animation: pulse 1s infinite; }
|
| 135 |
@keyframes pulse { 0% { opacity: 1; } 50% { opacity: 0.3; } 100% { opacity: 1; } }
|
| 136 |
|
| 137 |
/* --- Animations --- */
|
|
|
|
| 162 |
|
| 163 |
<div id="main-container">
|
| 164 |
<div id="rec-status"><div class="rec-dot"></div> RECORDING...</div>
|
|
|
|
|
|
|
| 165 |
<svg id="stage" viewBox="0 0 960 540" preserveAspectRatio="xMidYMid slice">
|
| 166 |
<defs>
|
| 167 |
<pattern id="wood-grain" width="100" height="20" patternUnits="userSpaceOnUse">
|
|
|
|
| 264 |
|
| 265 |
<div id="subtitle-box"> <div id="subtitle"></div> </div>
|
| 266 |
|
|
|
|
| 267 |
<div id="overlay">
|
| 268 |
<h1 style="color:#FFD700; text-shadow: 4px 4px #000; font-size: 48px; margin-bottom: 5px;">ONE PIECE SHORT</h1>
|
| 269 |
<h2 style="color:#fff; font-family: sans-serif; letter-spacing: 2px; margin-top: 0;">NAMI'S FURY</h2>
|
| 270 |
<div class="btn-group">
|
| 271 |
<button id="play-btn" class="main-btn">▶ JUST PLAY</button>
|
| 272 |
+
<button id="rec-btn" class="main-btn">🔴 RECORD VIDEO</button>
|
| 273 |
</div>
|
| 274 |
<p style="color:#bbb; font-size: 14px; margin-top: 30px;">*Sound Required*</p>
|
| 275 |
</div>
|
| 276 |
|
|
|
|
| 277 |
<div id="guide-overlay">
|
| 278 |
+
<h2 style="color:#FFD700;">HOW TO RECORD (READ CAREFULLY)</h2>
|
| 279 |
+
<div class="guide-step">1. OPTION A: If <span class="highlight">'Chrome Tab'</span> is available, select it and select THIS tab name.</div>
|
| 280 |
+
<div class="guide-step alt-option">1. OPTION B (FALLBACK): If 'Tab' is missing, select <span class="highlight">'Entire Screen'</span> (Seluruh Layar).</div>
|
| 281 |
+
<div class="guide-step">2. <span class="highlight" style="color:#ff6b6b">CRUCIAL:</span> You MUST check the box <b>"Share system audio"</b> (or 'Bagikan audio') at the bottom of the popup. Without this, video will be silent.</div>
|
| 282 |
+
<div class="guide-step">3. Click <b>Share</b> to begin.</div>
|
| 283 |
+
<button id="confirm-rec-btn" class="main-btn" style="background:#FFD700; color:#000; margin-top:15px;">I UNDERSTAND - START</button>
|
|
|
|
| 284 |
</div>
|
|
|
|
| 285 |
</div>
|
| 286 |
|
| 287 |
<script>
|
| 288 |
const el = (id) => document.getElementById(id);
|
| 289 |
const state = { audioCtx: null, voices: [], mediaRecorder: null, recordedChunks: [], isRecording: false };
|
| 290 |
|
|
|
|
| 291 |
function initAudio() { if(!state.audioCtx) state.audioCtx = new (window.AudioContext || window.webkitAudioContext)(); }
|
| 292 |
function playSound(type) {
|
| 293 |
if (!state.audioCtx) return;
|
| 294 |
const ctx = state.audioCtx; const t = ctx.currentTime;
|
| 295 |
const mGain = ctx.createGain(); mGain.connect(ctx.destination);
|
|
|
|
| 296 |
if (type === 'ocean') {
|
| 297 |
const buf = ctx.createBuffer(1, ctx.sampleRate * 2, ctx.sampleRate);
|
| 298 |
const data = buf.getChannelData(0); for (let i = 0; i < ctx.sampleRate * 2; i++) data[i] = Math.random() * 2 - 1;
|
|
|
|
| 323 |
}
|
| 324 |
}
|
| 325 |
|
| 326 |
+
function initVoices() { return new Promise(r => { let id = setInterval(() => { const v = window.speechSynthesis.getVoices(); if (v.length !== 0) { state.voices = v; clearInterval(id); r(); } }, 50); }); }
|
| 327 |
+
function getVoice(char) { const v = state.voices; const en = v.filter(vc => vc.lang.startsWith('en')); const pool = en.length > 0 ? en : v; if (char === 'nami') return pool.find(vc => vc.name.includes('Zira') || vc.name.includes('Female')) || pool[0]; else return pool.find(vc => vc.name.includes('Male')) || pool[1] || pool[0]; }
|
| 328 |
+
function speak(text, char) { return new Promise(r => { const u = new SpeechSynthesisUtterance(text); u.voice = getVoice(char); u.lang = 'en-US'; if (char === 'luffy') { u.rate = 1.4; u.pitch = 1.5; } else { u.rate = 1.15; u.pitch = 1.1; } u.volume = 1.0; u.onstart = () => { el('subtitle').textContent = (char === 'nami' ? 'NAMI: ' : 'LUFFY: ') + text; el('subtitle').style.opacity = 1; }; u.onend = () => { el('subtitle').style.opacity = 0; setTimeout(r, 400); }; window.speechSynthesis.speak(u); }); }
|
| 329 |
+
function setFace(char, type) { if (char === 'luffy') { el('luffy-face-normal').classList.toggle('hidden', type === 'stupid'); el('luffy-face-stupid').classList.toggle('hidden', type !== 'stupid'); } else { el('nami-face-normal').classList.toggle('hidden', type === 'angry'); el('nami-face-angry').classList.toggle('hidden', type !== 'angry'); el('nami-vein').classList.toggle('hidden', type !== 'angry'); } }
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 330 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 331 |
async function startRecording() {
|
| 332 |
try {
|
| 333 |
+
// Removed strict constraints to allow browser fallbacks more easily
|
| 334 |
+
const stream = await navigator.mediaDevices.getDisplayMedia({ video: { cursor: "never" }, audio: true });
|
|
|
|
|
|
|
|
|
|
| 335 |
|
| 336 |
+
// Basic check if audio track exists (browser dependent if it works)
|
| 337 |
+
if (stream.getAudioTracks().length === 0) {
|
| 338 |
+
// Optional: alert user here if you want strictly enforce audio, but let's let it slide as fallback.
|
| 339 |
+
console.warn("Audio track might be missing if 'Share Audio' wasn't checked.");
|
| 340 |
}
|
| 341 |
|
| 342 |
state.mediaRecorder = new MediaRecorder(stream, { mimeType: 'video/webm' });
|
| 343 |
state.recordedChunks = [];
|
| 344 |
state.mediaRecorder.ondataavailable = (e) => { if (e.data.size > 0) state.recordedChunks.push(e.data); };
|
| 345 |
state.mediaRecorder.onstop = downloadVideo;
|
|
|
|
| 346 |
state.mediaRecorder.start();
|
| 347 |
state.isRecording = true;
|
| 348 |
el('rec-status').style.opacity = 1;
|
|
|
|
|
|
|
| 349 |
stream.getVideoTracks()[0].onended = () => { if (state.mediaRecorder && state.mediaRecorder.state !== 'inactive') stopRecording(); };
|
| 350 |
return true;
|
| 351 |
} catch (err) {
|
|
|
|
|
|
|
|
|
|
| 352 |
return false;
|
| 353 |
}
|
| 354 |
}
|
| 355 |
+
function stopRecording() { if (state.mediaRecorder && state.mediaRecorder.state !== 'inactive') { state.mediaRecorder.stop(); if (state.mediaRecorder.stream) state.mediaRecorder.stream.getTracks().forEach(track => track.stop()); } state.isRecording = false; el('rec-status').style.opacity = 0; }
|
| 356 |
+
function downloadVideo() { if (state.recordedChunks.length === 0) return; const blob = new Blob(state.recordedChunks, { type: 'video/webm' }); const url = URL.createObjectURL(blob); const a = document.createElement('a'); a.style.display = 'none'; a.href = url; a.download = 'one_piece_nami_fury.webm'; document.body.appendChild(a); a.click(); setTimeout(() => { document.body.removeChild(a); window.URL.revokeObjectURL(url); }, 100); }
|
| 357 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 358 |
async function runEpisode() {
|
| 359 |
+
el('guide-overlay').style.display = 'none'; el('overlay').style.opacity = 0; setTimeout(() => el('overlay').style.display = 'none', 500);
|
| 360 |
+
playSound('ocean'); await new Promise(r => setTimeout(r, 1500));
|
| 361 |
+
el('luffy').classList.add('fidget'); await speak("Nami! I'm so booored! When's the next island?", 'luffy'); await speak("I'm hungry! Sanji locked the fridge again!", 'luffy'); el('luffy').classList.remove('fidget');
|
| 362 |
+
await speak("Luffy, shut up for a second! I'm trying to read this map.", 'nami'); await speak("If you bother me one more time, I'm tripling your debt!", 'nami');
|
| 363 |
+
await speak("Ehhh? Maps are boring...", 'luffy'); setFace('luffy', 'stupid'); playSound('stretch'); el('luffy-arm-l').style.transition = "all 0.8s cubic-bezier(0.68, -0.55, 0.27, 1.55)"; el('luffy-arm-l').setAttribute('d', 'M-35,10 Q-200,80 -250,20'); await new Promise(r => setTimeout(r, 1200));
|
| 364 |
+
setFace('nami', 'angry'); await speak("I SAID SHUT UP!", 'nami'); el('luffy-arm-l').setAttribute('d', 'M-35,10 Q-60,50 -50,90'); el('nami').style.transition = "transform 0.3s cubic-bezier(0.18, 0.89, 0.32, 1.28)"; el('nami').style.transform = "translate(250px, 320px) rotate(-25deg) scale(1.1)"; await new Promise(r => setTimeout(r, 400));
|
| 365 |
+
playSound('punch'); playSound('spin'); el('stage').classList.add('shake-screen'); el('nami').style.transition = "transform 0.1s"; el('nami').style.transform = "translate(450px, 320px) rotate(15deg) scale(1.2)"; el('impact-star').classList.remove('hidden'); el('impact-star').style.transition = "all 0.15s ease-out"; el('impact-star').style.transform = "translate(580px,250px) scale(4) rotate(180deg)"; el('luffy').classList.remove('breathe'); el('luffy').classList.add('luffy-spin-crash'); await new Promise(r => setTimeout(r, 400));
|
| 366 |
+
el('stage').classList.remove('shake-screen'); el('impact-star').style.opacity = 0; await new Promise(r => setTimeout(r, 2500));
|
| 367 |
+
el('nami').style.transition = "transform 0.8s ease-in-out"; el('nami').style.transform = "translate(300px, 320px) rotate(0deg)"; setFace('nami', 'normal'); await speak("*Sigh*... what an idiot captain. Such a pain.", 'nami'); await new Promise(r => setTimeout(r, 1000));
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 368 |
if (state.isRecording) stopRecording();
|
| 369 |
+
el('overlay').style.display = 'flex'; el('overlay').style.opacity = 1; el('overlay').innerHTML = "<h1 style='color:white'>FINISH</h1><button onclick='location.reload()' style='padding:15px 30px; font-size:20px; cursor:pointer; border-radius:10px; border:none; background:#FFD700;'>REPLAY</button>";
|
|
|
|
|
|
|
| 370 |
}
|
| 371 |
|
| 372 |
+
el('play-btn').onclick = async () => { el('play-btn').textContent = "LOADING..."; initAudio(); await initVoices(); runEpisode(); };
|
| 373 |
+
el('rec-btn').onclick = () => { el('overlay').style.opacity = 0; setTimeout(() => { el('overlay').style.display = 'none'; el('guide-overlay').style.display = 'flex'; }, 300); };
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 374 |
el('confirm-rec-btn').onclick = async () => {
|
| 375 |
+
el('confirm-rec-btn').disabled = true; el('confirm-rec-btn').textContent = "WAITING FOR PERMISSION...";
|
|
|
|
| 376 |
initAudio(); await initVoices();
|
| 377 |
const success = await startRecording();
|
| 378 |
+
if (success) runEpisode();
|
| 379 |
+
else { el('guide-overlay').style.display = 'none'; el('overlay').style.display = 'flex'; el('overlay').style.opacity = 1; el('confirm-rec-btn').disabled = false; el('confirm-rec-btn').textContent = "I UNDERSTAND - START"; }
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 380 |
};
|
| 381 |
</script>
|
| 382 |
</body>
|