cassette-player/no-damage.html
2026-01-17 08:54:21 -05:00

1608 lines
56 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>16-Bit Cassette Player</title>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
display: flex;
justify-content: center;
align-items: center;
min-height: 100vh;
background: linear-gradient(180deg, #0a0a0a 0%, #1a1a1a 100%);
font-family: 'Courier New', monospace;
image-rendering: pixelated;
}
.player {
width: 600px;
background: linear-gradient(145deg, #2a2a2a 0%, #1a1a1a 50%, #0f0f0f 100%);
border: 8px solid #0a0a0a;
box-shadow:
inset 0 0 0 2px #3a3a3a,
inset 0 0 50px rgba(0,0,0,0.9),
0 30px 60px rgba(0,0,0,0.8),
5px 5px 0 rgba(0,0,0,0.3);
/* Extra top padding to accommodate eject/lightning buttons */
padding: 60px 30px 30px 30px;
position: relative;
transform: perspective(1000px) rotateX(1deg);
}
/* Heavy damage and scratches */
.player::before {
content: '';
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-image:
linear-gradient(45deg, transparent 48%, rgba(255,255,255,0.03) 49%, rgba(255,255,255,0.03) 51%, transparent 52%),
linear-gradient(-45deg, transparent 48%, rgba(0,0,0,0.4) 49%, rgba(0,0,0,0.4) 51%, transparent 52%),
repeating-linear-gradient(90deg, transparent, transparent 3px, rgba(0,0,0,0.5) 3px, rgba(0,0,0,0.5) 4px),
url('data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" width="200" height="200"><filter id="noise"><feTurbulence type="fractalNoise" baseFrequency="2.5" numOctaves="6" /></filter><rect width="200" height="200" filter="url(%23noise)" opacity="0.4"/></svg>');
pointer-events: none;
opacity: 0.8;
mix-blend-mode: overlay;
}
/* Rust and corrosion patches */
.player::after {
content: '';
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-image:
radial-gradient(ellipse at 20% 30%, rgba(139, 69, 19, 0.3) 0%, transparent 40%),
radial-gradient(ellipse at 80% 70%, rgba(101, 67, 33, 0.2) 0%, transparent 35%),
radial-gradient(ellipse at 60% 10%, rgba(120, 81, 45, 0.25) 0%, transparent 30%),
radial-gradient(ellipse at 10% 90%, rgba(139, 90, 43, 0.2) 0%, transparent 40%);
pointer-events: none;
}
/* Eject button - positioned top-left of player */
.eject-btn {
position: absolute;
top: 15px;
left: 30px;
width: 50px;
height: 35px;
background: linear-gradient(180deg, #3a3a3a 0%, #1a1a1a 100%);
border: 4px solid #0a0a0a;
cursor: pointer;
box-shadow:
inset 0 -3px 0 rgba(0,0,0,0.5),
inset 2px 0 0 rgba(80, 50, 20, 0.3),
0 3px 6px rgba(0,0,0,0.6);
transition: all 0.1s;
display: flex;
align-items: center;
justify-content: center;
}
/* Lightning button - positioned top-right of player */
.lightning-btn {
position: absolute;
top: 15px;
right: 30px;
width: 50px;
height: 35px;
background: linear-gradient(180deg, #3a3a3a 0%, #1a1a1a 100%);
border: 4px solid #0a0a0a;
cursor: pointer;
box-shadow:
inset 0 -3px 0 rgba(0,0,0,0.5),
inset 2px 0 0 rgba(80, 50, 20, 0.3),
0 3px 6px rgba(0,0,0,0.6);
transition: all 0.1s;
display: flex;
align-items: center;
justify-content: center;
font-size: 18px;
color: #cc8800;
text-shadow: 0 0 3px rgba(255, 170, 0, 0.3);
}
.lightning-btn::before,
.lightning-btn::after {
content: none;
}
.lightning-btn:active,
.eject-btn:active {
transform: translateY(2px);
box-shadow:
inset 0 -1px 0 rgba(0,0,0,0.5),
0 1px 3px rgba(0,0,0,0.6);
}
/* Eject icon: triangle pointing up */
.eject-btn::before {
content: '';
width: 0;
height: 0;
border-left: 8px solid transparent;
border-right: 8px solid transparent;
border-bottom: 10px solid #666;
position: absolute;
top: 6px;
filter: drop-shadow(0 0 2px rgba(255, 0, 255, 0.3));
}
/* Eject icon: bar underneath triangle */
.eject-btn::after {
content: '';
position: absolute;
bottom: 6px;
width: 18px;
height: 3px;
background: #666;
}
.eject-btn:active {
transform: translateY(2px);
box-shadow:
inset 0 -1px 0 rgba(0,0,0,0.5),
0 1px 3px rgba(0,0,0,0.6);
}
.display {
background: linear-gradient(180deg, #0a1a0a 0%, #050f05 100%);
border: 4px solid #000;
padding: 15px;
/* Spacing: top margin matches bottom margin for symmetry with buttons above */
margin-top: 15px;
margin-bottom: 25px;
box-shadow:
inset 0 0 30px rgba(0,0,0,0.9),
inset 0 0 5px rgba(0,255,0,0.1);
position: relative;
overflow: hidden;
}
/* Cracked display effect */
.display::before {
content: '';
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-image:
linear-gradient(120deg, transparent 0%, transparent 48%, rgba(255,255,255,0.05) 48.5%, transparent 49%),
linear-gradient(60deg, transparent 0%, transparent 68%, rgba(255,255,255,0.03) 68.5%, transparent 69%),
repeating-linear-gradient(0deg, transparent, transparent 2px, rgba(0,0,0,0.6) 2px, rgba(0,0,0,0.6) 3px);
pointer-events: none;
}
/* Flickering scanlines */
.display::after {
content: '';
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: repeating-linear-gradient(0deg, transparent, transparent 1px, rgba(0,0,0,0.5) 1px, rgba(0,0,0,0.5) 2px);
pointer-events: none;
animation: flicker 4s infinite;
}
@keyframes flicker {
0%, 100% { opacity: 0.8; }
50% { opacity: 0.6; }
51% { opacity: 1; }
52% { opacity: 0.7; }
}
/* Track name text - scrolls when playing (JS controlled) */
.display-text {
color: #00ff00;
font-size: 18px;
text-shadow:
0 0 10px #00ff00,
0 0 20px #00ff00,
0 0 30px #00ff00;
letter-spacing: 2px;
margin-bottom: 10px;
position: relative;
z-index: 1;
white-space: nowrap;
overflow: hidden;
}
/* Inner span - position controlled by JavaScript for bounce effect */
.display-text-inner {
display: inline-block;
position: relative;
left: 0;
}
.time-display {
color: #00ff00;
font-size: 24px;
font-weight: bold;
text-shadow:
0 0 10px #00ff00,
0 0 20px #00ff00;
letter-spacing: 3px;
position: relative;
z-index: 1;
}
.cassette {
background: linear-gradient(180deg, #2a2a2a 0%, #1a1a1a 50%, #0f0f0f 100%);
border: 6px solid #000;
border-radius: 4px;
padding: 20px;
margin-bottom: 30px;
position: relative;
box-shadow:
inset 0 0 40px rgba(0,0,0,0.9),
inset 5px 5px 20px rgba(0,0,0,0.8);
transform: perspective(800px) rotateX(2deg);
/* Prevent text selection during reel drag */
-webkit-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
user-select: none;
}
/* Damage on cassette housing */
/* commented out for this version
.cassette::before {
content: '';
position: absolute;
top: 10px;
right: 20px;
width: 40px;
height: 30px;
background: radial-gradient(ellipse, rgba(255, 0, 255, 0.2) 0%, transparent 70%);
border: 2px solid rgba(255, 0, 255, 0.3);
border-radius: 3px;
pointer-events: none;
}
*/
.cassette::after {
content: '';
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-image:
url('data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" width="100" height="100"><filter id="grime"><feTurbulence type="fractalNoise" baseFrequency="3" numOctaves="4" /></filter><rect width="100" height="100" filter="url(%23grime)" opacity="0.15"/></svg>');
pointer-events: none;
border-radius: 4px;
mix-blend-mode: multiply;
}
.cassette-label {
background: linear-gradient(180deg, #d0d0d0 0%, #a0a0a0 100%);
border: 3px solid #000;
padding: 15px;
text-align: center;
margin-bottom: 20px;
font-size: 14px;
color: #1a1a1a;
font-weight: bold;
box-shadow:
inset 0 0 30px rgba(0,0,0,0.3),
inset 3px 3px 10px rgba(0,0,0,0.4);
position: relative;
overflow: hidden;
/* Prevent text selection during reel drag */
-webkit-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
user-select: none;
}
/* Faded neon cyan showing through wear */
.cassette-label::before {
content: '';
position: absolute;
top: 5px;
left: 10px;
width: 50px;
height: 20px;
background: linear-gradient(90deg, transparent, rgba(0, 255, 255, 0.4), transparent);
pointer-events: none;
filter: blur(2px);
}
/* Faded magenta showing through wear */
.cassette-label::after {
content: '';
position: absolute;
bottom: 8px;
right: 15px;
width: 40px;
height: 15px;
background: linear-gradient(90deg, transparent, rgba(255, 0, 255, 0.3), transparent);
pointer-events: none;
filter: blur(2px);
}
.reel {
width: 80px;
height: 80px;
border: 1px solid #000;
border-radius: 50%;
background: #0a0a0a;
position: absolute;
top: 50%;
transform: translateY(-50%);
box-shadow:
inset 0 0 20px rgba(0,0,0,0.9),
inset 0 0 5px rgba(139, 69, 19, 0.3);
/* Prevent text selection during drag */
-webkit-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
user-select: none;
cursor: ns-resize;
}
.reel-left {
left: 30px;
}
.reel-right {
right: 30px;
}
.tape-wound {
position: absolute;
/* Center using top/left 50% with transform */
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
border-radius: 50%;
/* Darker tape color */
background: linear-gradient(135deg, #1a1815 0%, #0f0d0a 50%, #050403 100%);
box-shadow:
inset 0 0 10px rgba(0,0,0,0.9),
0 0 3px rgba(0,0,0,0.7);
z-index: 1;
/* Size controlled by JavaScript via CSS custom property */
width: var(--tape-size, 72px);
height: var(--tape-size, 72px);
/* Prevent text selection during drag */
-webkit-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
user-select: none;
cursor: ns-resize;
}
.tape-wound.spinning {
animation: spinTape 2s linear infinite;
}
@keyframes spinTape {
from { transform: translate(-50%, -50%) rotate(0deg); }
to { transform: translate(-50%, -50%) rotate(360deg); }
}
.reel-inner {
width: 60px;
height: 60px;
border-radius: 50%;
background: conic-gradient(
from 0deg,
#2a2a2a 0deg 45deg,
#1a1a1a 45deg 90deg,
#2a2a2a 90deg 135deg,
#1a1a1a 135deg 180deg,
#2a2a2a 180deg 225deg,
#1a1a1a 225deg 270deg,
#2a2a2a 270deg 315deg,
#1a1a1a 315deg 360deg
);
box-shadow: inset 0 0 10px rgba(0,0,0,0.8);
z-index: 2;
/* Center the spool within the reel container */
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
}
.reel-inner.spinning {
animation: spinSpool 2s linear infinite;
}
@keyframes spinSpool {
from { transform: translate(-50%, -50%) rotate(0deg); }
to { transform: translate(-50%, -50%) rotate(360deg); }
}
.reel-inner.spinning {
animation: spinSpool 2s linear infinite;
}
.tape-window {
position: relative;
width: 380px;
height: 80px;
margin: 0 auto;
background: rgba(60, 30, 10, 0.4);
border: 3px solid #000;
box-shadow: inset 0 0 15px rgba(0,0,0,0.9);
overflow: hidden;
}
.controls {
display: flex;
justify-content: center;
gap: 15px;
margin-bottom: 20px;
}
.btn {
width: 70px;
height: 70px;
border: 5px solid #000;
background: linear-gradient(180deg, #2a2a2a 0%, #1a1a1a 100%);
cursor: pointer;
position: relative;
box-shadow:
inset 0 -4px 0 rgba(0,0,0,0.5),
inset 0 0 10px rgba(0,0,0,0.6),
0 4px 8px rgba(0,0,0,0.6);
transition: all 0.1s;
}
.btn:active {
transform: translateY(2px);
box-shadow:
inset 0 -2px 0 rgba(0,0,0,0.5),
0 2px 4px rgba(0,0,0,0.6);
}
.btn::before {
content: '';
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
}
/* Worn colored icons on buttons */
.btn-prev::before {
width: 0;
height: 0;
border-right: 12px solid #00a0a0;
border-top: 8px solid transparent;
border-bottom: 8px solid transparent;
margin-right: 8px;
filter: drop-shadow(0 0 3px rgba(0, 255, 255, 0.4));
opacity: 0.7;
}
.btn-prev::after {
content: '';
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
width: 3px;
height: 16px;
background: #00a0a0;
margin-left: -12px;
opacity: 0.7;
}
.btn-play::before {
width: 0;
height: 0;
border-left: 20px solid #00aa00;
border-top: 12px solid transparent;
border-bottom: 12px solid transparent;
margin-left: 3px;
filter: drop-shadow(0 0 3px rgba(0, 255, 0, 0.3));
opacity: 0.8;
}
.btn-pause::before {
width: 20px;
height: 24px;
border-left: 7px solid #cc8800;
border-right: 7px solid #cc8800;
filter: drop-shadow(0 0 3px rgba(255, 170, 0, 0.3));
opacity: 0.7;
}
.btn-stop::before {
width: 20px;
height: 20px;
background: #aa0000;
filter: drop-shadow(0 0 3px rgba(255, 0, 0, 0.3));
opacity: 0.7;
}
.btn-next::before {
width: 0;
height: 0;
border-left: 12px solid #00a0a0;
border-top: 8px solid transparent;
border-bottom: 8px solid transparent;
margin-left: -8px;
filter: drop-shadow(0 0 3px rgba(0, 255, 255, 0.4));
opacity: 0.7;
}
.btn-next::after {
content: '';
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
width: 3px;
height: 16px;
background: #00a0a0;
margin-left: 12px;
opacity: 0.7;
}
.volume-control {
display: flex;
align-items: center;
justify-content: center;
gap: 15px;
}
.volume-label {
color: #00ff00;
font-size: 16px;
text-shadow:
0 0 5px #00ff00,
0 0 10px #00ff00;
opacity: 0.9;
}
input[type="range"] {
-webkit-appearance: none;
appearance: none;
width: 200px;
height: 12px;
background: linear-gradient(180deg, #1a1a1a 0%, #0a0a0a 100%);
border: 3px solid #000;
box-shadow:
inset 0 0 10px rgba(0,0,0,0.8),
inset 0 0 5px rgba(139, 69, 19, 0.2);
}
input[type="range"]::-webkit-slider-thumb {
-webkit-appearance: none;
width: 24px;
height: 24px;
background: linear-gradient(145deg, #00ff00 0%, #00aa00 100%);
border: 3px solid #000;
cursor: pointer;
box-shadow:
0 0 10px rgba(0, 255, 0, 0.5),
inset 0 -2px 5px rgba(0,0,0,0.4);
}
input[type="range"]::-moz-range-thumb {
width: 24px;
height: 24px;
background: linear-gradient(145deg, #00ff00 0%, #00aa00 100%);
border: 3px solid #000;
cursor: pointer;
box-shadow:
0 0 10px rgba(0, 255, 0, 0.5),
inset 0 -2px 5px rgba(0,0,0,0.4);
}
.video-overlay {
display: none;
position: fixed;
top: 0;
left: 0;
width: 100vw;
height: 100vh;
background: #000;
z-index: 1000;
justify-content: center;
align-items: center;
}
.video-overlay.active {
display: flex;
}
.video-overlay video {
width: 100%;
height: 100%;
object-fit: contain;
}
.close-video {
position: absolute;
top: 20px;
right: 20px;
width: 40px;
height: 40px;
background: rgba(0,0,0,0.8);
border: 2px solid #fff;
color: #fff;
font-size: 24px;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
z-index: 1003;
text-shadow: none;
}
.close-video:hover {
background: rgba(0,0,0,0.9);
border-color: #fff;
box-shadow: 0 0 20px rgba(255, 255, 255, 0.5);
}
/* ========================================
ALBY LIGHTNING PANEL STYLES - START
Panel overlay and slide-in panel for Lightning payments
Styled to match the cassette player's retro 16-bit aesthetic
======================================== */
/* Semi-transparent backdrop overlay - click to close panel */
.alby-overlay {
display: none;
position: fixed;
top: 0;
left: 0;
width: 100vw;
height: 100vh;
background: rgba(0, 0, 0, 0.7);
z-index: 1000;
}
.alby-overlay.active {
display: block;
}
/* Main slide-in panel container */
.alby-panel {
position: fixed;
top: 0;
right: -400px;
width: 380px;
height: 100vh;
background: linear-gradient(145deg, #2a2a2a 0%, #1a1a1a 50%, #0f0f0f 100%);
border-left: 6px solid #0a0a0a;
box-shadow:
inset 0 0 0 2px #3a3a3a,
inset 0 0 50px rgba(0,0,0,0.9),
-10px 0 30px rgba(0,0,0,0.8);
z-index: 1001;
transition: right 0.3s ease-out;
overflow-y: auto;
font-family: 'Courier New', monospace;
}
.alby-panel.active {
right: 0;
}
/* Noise texture overlay for worn appearance */
.alby-panel::before {
content: '';
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-image:
url('data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" width="200" height="200"><filter id="noise"><feTurbulence type="fractalNoise" baseFrequency="2.5" numOctaves="6" /></filter><rect width="200" height="200" filter="url(%23noise)" opacity="0.4"/></svg>');
pointer-events: none;
opacity: 0.5;
mix-blend-mode: overlay;
}
/* Panel header with amber Lightning accent */
.alby-panel-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 20px;
background: linear-gradient(180deg, #1a1a1a 0%, #0f0f0f 100%);
border-bottom: 4px solid #000;
position: relative;
}
.alby-panel-title {
color: #cc8800;
font-size: 16px;
font-weight: bold;
text-shadow:
0 0 10px rgba(255, 170, 0, 0.5),
0 0 20px rgba(255, 170, 0, 0.3);
letter-spacing: 2px;
display: flex;
align-items: center;
gap: 10px;
}
.alby-panel-title .lightning-icon {
font-size: 20px;
}
/* Close button styled like cassette buttons */
.alby-close-btn {
width: 36px;
height: 36px;
background: linear-gradient(180deg, #3a3a3a 0%, #1a1a1a 100%);
border: 3px solid #000;
color: #666;
font-size: 18px;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
box-shadow:
inset 0 -2px 0 rgba(0,0,0,0.5),
0 2px 4px rgba(0,0,0,0.6);
transition: all 0.1s;
}
.alby-close-btn:hover {
color: #aa0000;
}
.alby-close-btn:active {
transform: translateY(2px);
box-shadow:
inset 0 -1px 0 rgba(0,0,0,0.5),
0 1px 2px rgba(0,0,0,0.6);
}
/* Panel body content area */
.alby-panel-body {
padding: 20px;
position: relative;
}
/* Section labels with green CRT glow */
.alby-label {
color: #00ff00;
font-size: 12px;
text-shadow:
0 0 5px #00ff00,
0 0 10px #00ff00;
letter-spacing: 2px;
margin-bottom: 8px;
opacity: 0.9;
}
/* Lightning address display - styled like cassette display */
.alby-address-box {
background: linear-gradient(180deg, #0a1a0a 0%, #050f05 100%);
border: 3px solid #000;
padding: 12px;
margin-bottom: 15px;
box-shadow:
inset 0 0 20px rgba(0,0,0,0.9),
inset 0 0 5px rgba(255, 170, 0, 0.1);
position: relative;
}
/* Scanlines effect on address box */
.alby-address-box::after {
content: '';
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: repeating-linear-gradient(0deg, transparent, transparent 1px, rgba(0,0,0,0.4) 1px, rgba(0,0,0,0.4) 2px);
pointer-events: none;
}
.alby-address {
color: #cc8800;
font-size: 13px;
text-shadow: 0 0 8px rgba(255, 170, 0, 0.5);
word-break: break-all;
position: relative;
z-index: 1;
}
/* Node ID styled as terminal text */
.alby-node-id {
color: #00ff00;
font-size: 9px;
text-shadow: 0 0 5px rgba(0, 255, 0, 0.3);
word-break: break-all;
margin-top: 10px;
opacity: 0.7;
position: relative;
z-index: 1;
}
/* Divider line */
.alby-divider {
height: 2px;
background: linear-gradient(90deg, transparent, #333, transparent);
margin: 20px 0;
}
/* Amount controls container */
.alby-amount-controls {
display: flex;
align-items: center;
gap: 10px;
margin-bottom: 15px;
}
/* Increment/decrement buttons styled like cassette buttons */
.alby-btn {
width: 40px;
height: 40px;
background: linear-gradient(180deg, #2a2a2a 0%, #1a1a1a 100%);
border: 3px solid #000;
color: #00ff00;
font-size: 20px;
font-weight: bold;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
box-shadow:
inset 0 -3px 0 rgba(0,0,0,0.5),
0 3px 6px rgba(0,0,0,0.6);
transition: all 0.1s;
text-shadow: 0 0 5px rgba(0, 255, 0, 0.5);
}
.alby-btn:hover {
text-shadow: 0 0 10px rgba(0, 255, 0, 0.8);
}
.alby-btn:active {
transform: translateY(2px);
box-shadow:
inset 0 -1px 0 rgba(0,0,0,0.5),
0 1px 3px rgba(0,0,0,0.6);
}
/* Amount input styled like CRT display */
.alby-input {
flex: 1;
background: linear-gradient(180deg, #0a1a0a 0%, #050f05 100%);
border: 3px solid #000;
padding: 10px;
color: #00ff00;
font-family: 'Courier New', monospace;
font-size: 18px;
text-align: center;
text-shadow: 0 0 10px #00ff00;
box-shadow: inset 0 0 15px rgba(0,0,0,0.9);
outline: none;
}
.alby-input:focus {
box-shadow:
inset 0 0 15px rgba(0,0,0,0.9),
0 0 5px rgba(0, 255, 0, 0.3);
}
/* Remove spinner arrows from number input */
.alby-input::-webkit-outer-spin-button,
.alby-input::-webkit-inner-spin-button {
-webkit-appearance: none;
margin: 0;
}
.alby-input[type=number] {
-moz-appearance: textfield;
appearance: textfield;
}
/* Memo textarea styled like CRT */
.alby-textarea {
width: 100%;
background: linear-gradient(180deg, #0a1a0a 0%, #050f05 100%);
border: 3px solid #000;
padding: 12px;
color: #00ff00;
font-family: 'Courier New', monospace;
font-size: 14px;
text-shadow: 0 0 5px rgba(0, 255, 0, 0.5);
box-shadow: inset 0 0 15px rgba(0,0,0,0.9);
resize: none;
outline: none;
min-height: 80px;
}
.alby-textarea:focus {
box-shadow:
inset 0 0 15px rgba(0,0,0,0.9),
0 0 5px rgba(0, 255, 0, 0.3);
}
/* Character count display */
.alby-char-count {
color: #00ff00;
font-size: 10px;
text-shadow: 0 0 3px rgba(0, 255, 0, 0.3);
text-align: right;
margin-top: 5px;
opacity: 0.7;
}
/* Main boost button - amber Lightning theme */
.alby-boost-btn {
width: 100%;
padding: 15px 20px;
margin-top: 20px;
background: linear-gradient(180deg, #cc8800 0%, #996600 100%);
border: 4px solid #000;
color: #000;
font-family: 'Courier New', monospace;
font-size: 16px;
font-weight: bold;
letter-spacing: 2px;
cursor: pointer;
box-shadow:
inset 0 -4px 0 rgba(0,0,0,0.3),
0 0 15px rgba(255, 170, 0, 0.3),
0 4px 8px rgba(0,0,0,0.6);
transition: all 0.1s;
display: flex;
align-items: center;
justify-content: center;
gap: 10px;
}
.alby-boost-btn:hover {
box-shadow:
inset 0 -4px 0 rgba(0,0,0,0.3),
0 0 25px rgba(255, 170, 0, 0.5),
0 4px 8px rgba(0,0,0,0.6);
}
.alby-boost-btn:active {
transform: translateY(2px);
box-shadow:
inset 0 -2px 0 rgba(0,0,0,0.3),
0 0 15px rgba(255, 170, 0, 0.3),
0 2px 4px rgba(0,0,0,0.6);
}
/* Hide the simple-boost default styling, we use our own button */
simple-boost {
position: absolute !important;
width: 1px !important;
height: 1px !important;
opacity: 0 !important;
overflow: hidden !important;
pointer-events: none !important;
}
/* ========================================
ALBY LIGHTNING PANEL STYLES - END
======================================== */
</style>
</head>
<body>
<div class="player">
<button class="eject-btn" id="ejectBtn" title="Eject"></button>
<button class="lightning-btn" id="lightningBtn" title="Menu"></button>
<div class="display">
<!-- Track name with inner span for marquee scrolling -->
<div class="display-text" id="trackName"><span class="display-text-inner" id="trackNameInner">TRACK 1</span></div>
<div class="time-display" id="timeDisplay">00:00 / 00:00</div>
</div>
<div class="cassette">
<div class="cassette-label">ECHO REALITY</div>
<div class="tape-window">
<div class="reel reel-left" id="reelContainerLeft">
<div class="tape-wound" id="tapeLeft"></div>
<div class="reel-inner" id="reelLeft"></div>
</div>
<div class="reel reel-right" id="reelContainerRight">
<div class="tape-wound" id="tapeRight"></div>
<div class="reel-inner" id="reelRight"></div>
</div>
</div>
</div>
<div class="controls">
<button class="btn btn-prev" id="prevBtn"></button>
<button class="btn btn-play" id="playBtn"></button>
<button class="btn btn-pause" id="pauseBtn"></button>
<button class="btn btn-stop" id="stopBtn"></button>
<button class="btn btn-next" id="nextBtn"></button>
</div>
<div class="volume-control">
<span class="volume-label">VOL</span>
<input type="range" id="volumeSlider" min="0" max="100" value="70">
</div>
</div>
<div class="video-overlay" id="videoOverlay">
<button class="close-video" id="closeVideo">×</button>
<video id="videoPlayer" autoplay></video>
</div>
<!-- ========================================
ALBY LIGHTNING PANEL - START
Slide-out panel for Lightning payments/boosts
Triggered by the lightning button (⚡) on the player
======================================== -->
<!-- Backdrop overlay - clicking closes the panel -->
<div class="alby-overlay" id="albyOverlay"></div>
<!-- Main panel container -->
<div class="alby-panel" id="albyPanel">
<!-- Panel header with title and close button -->
<div class="alby-panel-header">
<div class="alby-panel-title">
<span class="lightning-icon"></span>
<span>LIGHTNING BOOST</span>
</div>
<button class="alby-close-btn" id="albyCloseBtn" title="Close">×</button>
</div>
<!-- Panel body content -->
<div class="alby-panel-body">
<!-- Lightning address display section -->
<div class="alby-label">LIGHTNING ADDRESS</div>
<div class="alby-address-box">
<div class="alby-address">hyperspaceout@getalby.com</div>
<div class="alby-node-id">NODE: 0325ab94e785e40877fe7421ec9a523bbb6021663dfb5c18987f40e17d5d507921</div>
</div>
<div class="alby-divider"></div>
<!-- Amount input with increment/decrement controls -->
<div class="alby-label">AMOUNT (USD)</div>
<div class="alby-amount-controls">
<button class="alby-btn" id="albyDecrementBtn" title="Decrease"></button>
<input type="number" class="alby-input" id="albyAmount" value="1.0" min="0.1" step="0.1">
<button class="alby-btn" id="albyIncrementBtn" title="Increase">+</button>
</div>
<!-- Memo textarea -->
<div class="alby-label">MEMO</div>
<textarea class="alby-textarea" id="albyMemo" maxlength="400" placeholder="Your message..."></textarea>
<div class="alby-char-count"><span id="albyCharCount">0</span>/400</div>
<!-- Boost button - triggers simple-boost payment -->
<button class="alby-boost-btn" id="albyBoostBtn">
<span></span>
<span>BOOST $<span id="albyDisplayAmount">1.00</span></span>
</button>
<!-- Hidden simple-boost component - handles actual Lightning payment -->
<simple-boost
id="albySimpleBoost"
noconfetti
memo=""
currency="usd"
amount="1.0"
address="hyperspaceout@getalby.com">
</simple-boost>
</div>
</div>
<!-- Load simple-boost web component for Lightning payments -->
<script type="module" src="https://esm.sh/simple-boost@latest" defer></script>
<!-- ALBY LIGHTNING PANEL - END -->
<audio id="audio"></audio>
<script>
const playlist = [
{ url: 'https://feed.falsefinish.club/Echo%20Reality/PINK%20FLIGHT/MP3%20BOUNCE/01.%20PINK%20FLIGHT%20ATTENDANT.mp3', name: 'TRACK 1 - PINK FLIGHT ATTENDANT' },
{ url: 'https://feed.falsefinish.club/Echo%20Reality/PINK%20FLIGHT/MP3%20BOUNCE/02.%20NOW.mp3', name: 'TRACK 2 - NOW' },
{ url: 'https://feed.falsefinish.club/Echo%20Reality/PINK%20FLIGHT/MP3%20BOUNCE/03.%20MAZES.mp3', name: 'TRACK 3 - MAZES' },
{ url: 'https://feed.falsefinish.club/Echo%20Reality/PINK%20FLIGHT/MP3%20BOUNCE/04.%20FAMILY%20MAN.mp3', name: 'TRACK 4 - FAMILY MAN' },
{ url: 'https://feed.falsefinish.club/Echo%20Reality/PINK%20FLIGHT/MP3%20BOUNCE/05.%20TOLLBOOTH%20SAINTS.mp3', name: 'TRACK 5 - TOLLBOOTH SAINTS' }
];
let currentTrack = 0;
const videoUrl = 'https://feed.falsefinish.club/Echo%20Reality/PINK%20FLIGHT/MAZES%20HB.mp4';
const audio = document.getElementById('audio');
const playBtn = document.getElementById('playBtn');
const pauseBtn = document.getElementById('pauseBtn');
const stopBtn = document.getElementById('stopBtn');
const prevBtn = document.getElementById('prevBtn');
const nextBtn = document.getElementById('nextBtn');
const volumeSlider = document.getElementById('volumeSlider');
const timeDisplay = document.getElementById('timeDisplay');
const trackName = document.getElementById('trackName');
const trackNameInner = document.getElementById('trackNameInner');
const reelLeft = document.getElementById('reelLeft');
const reelRight = document.getElementById('reelRight');
const tapeLeft = document.getElementById('tapeLeft');
const tapeRight = document.getElementById('tapeRight');
const reelContainerLeft = document.getElementById('reelContainerLeft');
const reelContainerRight = document.getElementById('reelContainerRight');
// Tape size constants (in pixels)
const TAPE_MIN_SIZE = 62; // Minimum tape size (just larger than reel-inner)
const TAPE_MAX_SIZE = 166; // Maximum tape size (fills most of reel)
const ejectBtn = document.getElementById('ejectBtn');
const lightningBtn = document.getElementById('lightningBtn');
const videoOverlay = document.getElementById('videoOverlay');
const videoPlayer = document.getElementById('videoPlayer');
const closeVideo = document.getElementById('closeVideo');
// Lightning button - opens Alby Lightning panel
// (toggleAlbyPanel function defined in ALBY PANEL FUNCTIONALITY section below)
lightningBtn.addEventListener('click', () => {
toggleAlbyPanel();
});
// ========================================
// TITLE SCROLL ANIMATION (JavaScript controlled bounce)
// Moves 1px at a time, bounces at edges, pauses with player
// ========================================
let titleScrollPosition = 0;
let titleScrollDirection = 1; // 1 = moving left (text shifts left), -1 = moving right
let titleScrollInterval = null;
const SCROLL_SPEED = 333; // milliseconds between 1px moves (1 second per ~3 pixels)
function startTitleScroll() {
if (titleScrollInterval) return; // Already scrolling
titleScrollInterval = setInterval(updateTitleScroll, SCROLL_SPEED);
}
function stopTitleScroll() {
if (titleScrollInterval) {
clearInterval(titleScrollInterval);
titleScrollInterval = null;
}
}
function resetTitleScroll() {
stopTitleScroll();
titleScrollPosition = 0;
titleScrollDirection = 1;
trackNameInner.style.left = '0px';
}
function updateTitleScroll() {
const containerWidth = trackName.offsetWidth;
const textWidth = trackNameInner.offsetWidth;
// Calculate max scroll - text scrolls until its right edge hits container's right edge
// If text is shorter than container, scroll until text's left edge hits container's right edge
let maxScroll;
if (textWidth > containerWidth) {
// Text is longer - scroll until right edge of text reaches right edge of container
maxScroll = textWidth - containerWidth;
} else {
// Text is shorter - scroll across the container width but keep text visible
// Text starts at left (0), scrolls right until left edge is at (containerWidth - textWidth)
maxScroll = containerWidth - textWidth;
}
// Move 1px in current direction
titleScrollPosition += titleScrollDirection;
// Bounce at edges
if (titleScrollPosition >= maxScroll) {
titleScrollPosition = maxScroll;
titleScrollDirection = -1; // Start moving back
} else if (titleScrollPosition <= 0) {
titleScrollPosition = 0;
titleScrollDirection = 1; // Start moving forward
}
// Apply position
// If text is longer: shift text LEFT (negative value) so we see the end
// If text is shorter: shift text RIGHT (positive value) to scroll across display
if (textWidth > containerWidth) {
trackNameInner.style.left = -titleScrollPosition + 'px';
} else {
trackNameInner.style.left = titleScrollPosition + 'px';
}
}
// Load initial track
function loadTrack(index) {
currentTrack = index;
audio.src = playlist[index].url;
// Update the inner span for marquee scrolling
trackNameInner.textContent = playlist[index].name;
// Reset scroll position for new track
resetTitleScroll();
}
loadTrack(0);
// Set initial volume
audio.volume = 0.7;
// Play button
playBtn.addEventListener('click', () => {
audio.play();
reelLeft.classList.add('spinning');
reelRight.classList.add('spinning');
tapeLeft.classList.add('spinning');
tapeRight.classList.add('spinning');
startTitleScroll();
});
// Pause button - keeps title at current position
pauseBtn.addEventListener('click', () => {
audio.pause();
reelLeft.classList.remove('spinning');
reelRight.classList.remove('spinning');
tapeLeft.classList.remove('spinning');
tapeRight.classList.remove('spinning');
stopTitleScroll();
});
// Stop button - resets title to start position
stopBtn.addEventListener('click', () => {
audio.pause();
audio.currentTime = 0;
reelLeft.classList.remove('spinning');
reelRight.classList.remove('spinning');
tapeLeft.classList.remove('spinning');
tapeRight.classList.remove('spinning');
resetTitleScroll();
resetTapeSizes();
});
// Previous track (prev button with bar)
prevBtn.addEventListener('click', () => {
console.log('Prev button clicked');
currentTrack = (currentTrack - 1 + playlist.length) % playlist.length;
audio.src = playlist[currentTrack].url;
trackNameInner.textContent = playlist[currentTrack].name;
resetTitleScroll(); // Reset title for new track
resetTapeSizes(); // Reset tape for new track
audio.load();
audio.oncanplay = function() {
const playPromise = audio.play();
if (playPromise !== undefined) {
playPromise.then(() => {
reelLeft.classList.add('spinning');
reelRight.classList.add('spinning');
tapeLeft.classList.add('spinning');
tapeRight.classList.add('spinning');
startTitleScroll();
}).catch(e => console.log('Play failed:', e));
}
audio.oncanplay = null;
};
});
// Next track (next button with bar)
nextBtn.addEventListener('click', () => {
console.log('Next button clicked');
currentTrack = (currentTrack + 1) % playlist.length;
audio.src = playlist[currentTrack].url;
trackNameInner.textContent = playlist[currentTrack].name;
resetTitleScroll(); // Reset title for new track
resetTapeSizes(); // Reset tape for new track
audio.load();
audio.oncanplay = function() {
const playPromise = audio.play();
if (playPromise !== undefined) {
playPromise.then(() => {
reelLeft.classList.add('spinning');
reelRight.classList.add('spinning');
tapeLeft.classList.add('spinning');
tapeRight.classList.add('spinning');
startTitleScroll();
}).catch(e => console.log('Play failed:', e));
}
audio.oncanplay = null;
};
});
// Drag on reels to scrub audio
let isDragging = false;
let startY = 0;
let startTime = 0;
let wasPlayingBeforeDrag = false;
function startDrag(e) {
if (audio.duration) {
e.preventDefault(); // Prevent text selection during drag
isDragging = true;
startY = e.clientY || e.touches[0].clientY;
startTime = audio.currentTime;
wasPlayingBeforeDrag = !audio.paused;
// Don't pause - let audio continue playing while scrubbing
}
}
function drag(e) {
if (isDragging && audio.duration) {
const currentY = e.clientY || e.touches[0].clientY;
const deltaY = startY - currentY;
const scrubAmount = (deltaY / 100) * audio.duration * 0.1;
const newTime = Math.max(0, Math.min(audio.duration, startTime + scrubAmount));
audio.currentTime = newTime;
// Keep playing during scrub
if (audio.paused && wasPlayingBeforeDrag) {
audio.play();
}
}
}
function endDrag() {
if (isDragging) {
isDragging = false;
// Continue playing if it was playing before
if (wasPlayingBeforeDrag && audio.paused) {
audio.play();
reelLeft.classList.add('spinning');
reelRight.classList.add('spinning');
tapeLeft.classList.add('spinning');
tapeRight.classList.add('spinning');
}
}
}
// Add drag listeners to all reel elements (containers, tape, and inner spools)
[reelLeft, reelRight, tapeLeft, tapeRight, reelContainerLeft, reelContainerRight].forEach(el => {
el.addEventListener('mousedown', startDrag);
el.addEventListener('touchstart', startDrag, { passive: false });
});
document.addEventListener('mousemove', drag);
document.addEventListener('touchmove', drag);
document.addEventListener('mouseup', endDrag);
document.addEventListener('touchend', endDrag);
// Auto-play next track when current ends
audio.addEventListener('ended', () => {
currentTrack = (currentTrack + 1) % playlist.length;
loadTrack(currentTrack);
resetTapeSizes();
audio.play();
reelLeft.classList.add('spinning');
reelRight.classList.add('spinning');
tapeLeft.classList.add('spinning');
tapeRight.classList.add('spinning');
startTitleScroll();
});
// Volume control
volumeSlider.addEventListener('input', (e) => {
audio.volume = e.target.value / 100;
});
// Update tape wound sizes based on playback progress
function updateTapeSizes() {
if (!audio.duration) return;
const progress = audio.currentTime / audio.duration;
const tapeRange = TAPE_MAX_SIZE - TAPE_MIN_SIZE;
// Left spool: starts full, decreases as track plays
const leftSize = TAPE_MAX_SIZE - (progress * tapeRange);
// Right spool: starts empty, increases as track plays
const rightSize = TAPE_MIN_SIZE + (progress * tapeRange);
tapeLeft.style.setProperty('--tape-size', leftSize + 'px');
tapeRight.style.setProperty('--tape-size', rightSize + 'px');
}
// Initialize tape sizes
function resetTapeSizes() {
tapeLeft.style.setProperty('--tape-size', TAPE_MAX_SIZE + 'px');
tapeRight.style.setProperty('--tape-size', TAPE_MIN_SIZE + 'px');
}
resetTapeSizes();
// Update time display and tape sizes
audio.addEventListener('timeupdate', () => {
const current = formatTime(audio.currentTime);
const duration = formatTime(audio.duration);
timeDisplay.textContent = `${current} / ${duration}`;
updateTapeSizes();
});
// Format time helper
function formatTime(seconds) {
if (isNaN(seconds)) return '00:00';
const mins = Math.floor(seconds / 60);
const secs = Math.floor(seconds % 60);
return `${String(mins).padStart(2, '0')}:${String(secs).padStart(2, '0')}`;
}
// Stop spinning when audio pauses
// Stop animations when audio pauses (title stays at current position)
audio.addEventListener('pause', () => {
reelLeft.classList.remove('spinning');
reelRight.classList.remove('spinning');
tapeLeft.classList.remove('spinning');
tapeRight.classList.remove('spinning');
stopTitleScroll();
});
// Eject button - opens fullscreen video
ejectBtn.addEventListener('click', () => {
audio.pause();
reelLeft.classList.remove('spinning');
reelRight.classList.remove('spinning');
tapeLeft.classList.remove('spinning');
tapeRight.classList.remove('spinning');
videoPlayer.src = videoUrl;
videoOverlay.classList.add('active');
videoPlayer.play();
});
// Close video
closeVideo.addEventListener('click', () => {
videoPlayer.pause();
videoPlayer.src = '';
videoOverlay.classList.remove('active');
});
// Close video on Escape key
document.addEventListener('keydown', (e) => {
if (e.key === 'Escape' && videoOverlay.classList.contains('active')) {
closeVideo.click();
}
});
// ========================================
// ALBY LIGHTNING PANEL FUNCTIONALITY - START
// Handles panel open/close, amount controls, and boost button
// ========================================
// DOM element references for Alby panel
const albyOverlay = document.getElementById('albyOverlay');
const albyPanel = document.getElementById('albyPanel');
const albyCloseBtn = document.getElementById('albyCloseBtn');
const albyAmount = document.getElementById('albyAmount');
const albyMemo = document.getElementById('albyMemo');
const albyIncrementBtn = document.getElementById('albyIncrementBtn');
const albyDecrementBtn = document.getElementById('albyDecrementBtn');
const albyBoostBtn = document.getElementById('albyBoostBtn');
const albyDisplayAmount = document.getElementById('albyDisplayAmount');
const albyCharCount = document.getElementById('albyCharCount');
const albySimpleBoost = document.getElementById('albySimpleBoost');
/**
* Toggle the Alby Lightning panel open/closed
* Also handles the backdrop overlay visibility
*/
function toggleAlbyPanel() {
const isActive = albyPanel.classList.contains('active');
if (isActive) {
// Close panel
albyPanel.classList.remove('active');
albyOverlay.classList.remove('active');
} else {
// Open panel
albyPanel.classList.add('active');
albyOverlay.classList.add('active');
}
}
/**
* Update the simple-boost component attributes and display amount
* Called whenever amount or memo values change
*/
function updateAlbyBoostButton() {
const amount = parseFloat(albyAmount.value).toFixed(1);
const memo = albyMemo.value;
// Update simple-boost component attributes
albySimpleBoost.setAttribute('amount', amount);
albySimpleBoost.setAttribute('memo', memo);
// Update displayed amount on our custom button
albyDisplayAmount.textContent = parseFloat(amount).toFixed(2);
}
/**
* Update the memo character count display
*/
function updateAlbyCharCount() {
albyCharCount.textContent = albyMemo.value.length;
}
// Close button click handler
albyCloseBtn.addEventListener('click', toggleAlbyPanel);
// Overlay click handler - close panel when clicking backdrop
albyOverlay.addEventListener('click', toggleAlbyPanel);
// Increment amount button (+$1.00)
albyIncrementBtn.addEventListener('click', () => {
const currentValue = parseFloat(albyAmount.value);
albyAmount.value = (currentValue + 1.0).toFixed(1);
updateAlbyBoostButton();
});
// Decrement amount button (-$1.00, minimum $0.10)
albyDecrementBtn.addEventListener('click', () => {
const currentValue = parseFloat(albyAmount.value);
const newValue = Math.max(0.1, currentValue - 1.0);
albyAmount.value = newValue.toFixed(1);
updateAlbyBoostButton();
});
// Amount input change handler
albyAmount.addEventListener('input', () => {
if (albyAmount.value && albyAmount.value >= 0.1) {
updateAlbyBoostButton();
}
});
// Amount input blur handler - validate minimum value
albyAmount.addEventListener('blur', () => {
if (!albyAmount.value || albyAmount.value < 0.1) {
albyAmount.value = '0.1';
}
updateAlbyBoostButton();
});
// Memo textarea input handler
albyMemo.addEventListener('input', () => {
updateAlbyCharCount();
updateAlbyBoostButton();
});
// Boost button click - trigger the simple-boost component
albyBoostBtn.addEventListener('click', () => {
// Ensure latest form values are synced to simple-boost before triggering
updateAlbyBoostButton();
// Click the inner button div inside the shadow DOM
const innerButton = albySimpleBoost.shadowRoot?.querySelector('.simple-boost-button');
if (innerButton) {
innerButton.click();
}
});
// Listen for successful payment and reset form fields
albySimpleBoost.addEventListener('success', (e) => {
// Reset form to default values
albyAmount.value = '1.0';
albyMemo.value = '';
updateAlbyCharCount();
updateAlbyBoostButton();
});
// Escape key handler for Alby panel
// (Added to existing keydown listener scope)
document.addEventListener('keydown', (e) => {
if (e.key === 'Escape' && albyPanel.classList.contains('active')) {
toggleAlbyPanel();
}
});
// Initialize character count and sync simple-boost attributes on page load
updateAlbyCharCount();
updateAlbyBoostButton();
// ========================================
// ALBY LIGHTNING PANEL FUNCTIONALITY - END
// ========================================
</script>
</body>
</html>