music-video-gen/tv-player/index_v0.html
2025-11-08 16:05:55 +01:00

436 lines
17 KiB
HTML

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>3D CRT Television Model (Video Player)</title>
<!-- Load Tailwind CSS -->
<script src="https://cdn.tailwindcss.com"></script>
<!-- Load Three.js -->
<script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/r128/three.min.js"></script>
<style>
/* Apply Inter font and ensure full viewport size */
html, body {
margin: 0;
padding: 0;
height: 100%;
font-family: 'Inter', sans-serif;
overflow: hidden; /* Prevent scrolling */
}
/* Style for the canvas container */
#tv-container {
width: 100%;
height: 100%;
display: flex;
justify-content: center;
align-items: center;
/* The canvas itself will be appended here by three.js */
}
/* Custom styling for the file input button */
#controls input[type="file"] {
display: none;
}
#controls label {
cursor: pointer;
padding: 0.5rem 1rem;
background-color: #ef4444; /* red-500 */
color: white;
border-radius: 0.375rem; /* rounded-md */
font-weight: 600;
transition: background-color 0.2s;
box-shadow: 0 4px #b91c1c; /* darker red for 3D press effect */
user-select: none;
}
#controls label:hover {
background-color: #dc2626; /* red-600 */
}
#controls label:active {
box-shadow: 0 1px #b91c1c;
transform: translateY(3px);
}
</style>
</head>
<body class="bg-gray-900 text-gray-100 antialiased">
<!-- Header -->
<header class="absolute top-0 left-0 right-0 p-4 text-center z-10">
<h1 class="text-3xl font-extrabold text-red-500">Nostalgic CRT Display (Video Player)</h1>
<p class="text-sm text-gray-400 mt-1">Click and drag to rotate the bulky set. Load a video to replace the static!</p>
</header>
<!-- Controls (Load Button) -->
<div id="controls" class="absolute bottom-4 left-0 right-0 p-4 flex justify-center z-10">
<input type="file" id="videoFileInput" accept="video/*">
<label for="videoFileInput" id="loadVideoLabel">
Load Video File
</label>
</div>
<!-- Hidden Video Element for decoding the video file -->
<video id="videoElement" class="hidden" loop muted playsinline></video>
<!-- 3D Canvas Container -->
<div id="tv-container">
<!-- Three.js will inject the canvas here -->
</div>
<script>
// --- Global Variables ---
let scene, camera, renderer;
let tvModel;
let isDragging = false;
let previousMousePosition = { x: 0, y: 0 };
let screenMesh; // Reference to the screen mesh for texture updates
// Dimensions for a bulky CRT
const TV_WIDTH = 12;
const TV_HEIGHT = 9;
const TV_DEPTH = 10;
// Variables for textures
let staticTexture;
let videoTexture;
let isStaticActive = true; // Flag to control rendering loop
// --- Utility Functions (Kept for environment compatibility) ---
function pcmToWav(pcm16, sampleRate) { /* ... omitted for brevity ... */ }
function base64ToArrayBuffer(base64) { /* ... omitted for brevity ... */ }
// --- Core 3D Functions ---
function init() {
const container = document.getElementById('tv-container');
// 1. Scene Setup
scene = new THREE.Scene();
scene.background = new THREE.Color(0x111827); // Dark background matching Tailwind bg-gray-900
// 2. Camera Setup
camera = new THREE.PerspectiveCamera(50, container.clientWidth / container.clientHeight, 0.1, 100);
camera.position.z = 25;
camera.position.y = 0;
// 3. Renderer Setup
renderer = new THREE.WebGLRenderer({ antialias: true });
renderer.setSize(container.clientWidth, container.clientHeight);
renderer.setPixelRatio(window.devicePixelRatio);
container.appendChild(renderer.domElement);
// 4. Create TV Model
tvModel = createTV();
scene.add(tvModel);
// 5. Setup controls
setupVideoControls();
// 6. Add Lighting
addLights();
// 7. Event Listeners for Interaction and Resizing
window.addEventListener('resize', onWindowResize, false);
renderer.domElement.addEventListener('mousedown', onMouseDown, false);
renderer.domElement.addEventListener('mousemove', onMouseMove, false);
renderer.domElement.addEventListener('mouseup', onMouseUp, false);
renderer.domElement.addEventListener('touchstart', onTouchStart, false);
renderer.domElement.addEventListener('touchmove', onTouchMove, false);
renderer.domElement.addEventListener('touchend', onMouseUp, false);
// Initial view rotation
tvModel.rotation.y = Math.PI / 4;
tvModel.rotation.x = -Math.PI / 16;
}
function createTV() {
const tvGroup = new THREE.Group();
// --- 1. CRT Body Casing (Bulky Box) ---
const bodyGeometry = new THREE.BoxGeometry(TV_WIDTH, TV_HEIGHT, TV_DEPTH);
const bodyMaterial = new THREE.MeshPhongMaterial({
color: 0x554433, // Brown/Beige plastic casing
specular: 0x111111,
shininess: 10
});
const bodyMesh = new THREE.Mesh(bodyGeometry, bodyMaterial);
tvGroup.add(bodyMesh);
// --- 2. Screen (Dynamic Canvas Texture for initial static) ---
// Create the initial dynamic static canvas (512x512 resolution for texture)
const screenCanvas = document.createElement('canvas');
screenCanvas.width = 512;
screenCanvas.height = 512;
const screenContext = screenCanvas.getContext('2d');
staticTexture = new THREE.CanvasTexture(screenCanvas); // Store global reference to static texture
// Parameters for CRT screen curvature
const screenRadius = 25; // Large radius for a subtle curve
const screenWidth = TV_WIDTH - 1.5;
const screenHeight = TV_HEIGHT - 1.5;
const screenOffset = TV_DEPTH / 2 + 0.01;
// Calculate the angle needed to span the screen width on the curved surface
const screenArcAngle = screenWidth / screenRadius;
// Geometry: Segment of a cylinder to create a concave screen
const screenGeometry = new THREE.CylinderGeometry(
screenRadius, screenRadius,
screenHeight, // Cylinder height corresponds to screen height (Y axis)
32, // Radial segments (for smoothness)
1,
true, // Open ended
-screenArcAngle / 2, // Start angle
screenArcAngle // Total angle (arc length)
);
// Use a Basic Material with the static texture initially
const screenMaterial = new THREE.MeshBasicMaterial({
map: staticTexture,
// Setting DoubleSide ensures the concave side of the geometry renders.
side: THREE.DoubleSide
});
screenMesh = new THREE.Mesh(screenGeometry, screenMaterial); // Store global reference
screenMesh.userData.context = screenContext; // Store context for static update
screenMesh.name = 'crtScreen'; // Give it a name for easy lookup
// Rotation and Positioning:
// Rotate 180 degrees around Y to flip the mesh so the CONCAVE side faces the camera (+Z).
screenMesh.rotation.y = Math.PI;
// Position: Move back by the radius so the front-center aligns with the front of the TV
screenMesh.position.z = screenOffset - screenRadius;
tvGroup.add(screenMesh);
// --- 3. Knobs/Dials on the side ---
const knobGeometry = new THREE.CylinderGeometry(0.3, 0.3, 0.5, 16);
const knobMaterial = new THREE.MeshPhongMaterial({ color: 0x333333, shininess: 30 });
// Channel Knob
const channelKnob = new THREE.Mesh(knobGeometry, knobMaterial);
channelKnob.rotation.z = Math.PI / 2; // Rotate to stick out from the side
channelKnob.position.set(TV_WIDTH / 2 + 0.25, TV_HEIGHT / 2 - 2, 0);
tvGroup.add(channelKnob);
// Volume Knob
const volumeKnob = new THREE.Mesh(knobGeometry, knobMaterial);
volumeKnob.rotation.z = Math.PI / 2;
volumeKnob.position.set(TV_WIDTH / 2 + 0.25, TV_HEIGHT / 2 - 3.5, 0);
tvGroup.add(volumeKnob);
// --- 4. Antenna (Rabbit Ears) ---
const antennaMaterial = new THREE.MeshPhongMaterial({ color: 0xaaaaaa, shininess: 80 });
const antennaGeometry = new THREE.CylinderGeometry(0.05, 0.05, 5, 8);
// Base connection point
const antennaBase = new THREE.Mesh(new THREE.CylinderGeometry(0.2, 0.2, 0.3, 16), knobMaterial);
antennaBase.position.set(0, TV_HEIGHT / 2 + 0.15 + TV_HEIGHT / 2, 0);
tvGroup.add(antennaBase);
// Left Ear
const leftEar = new THREE.Mesh(antennaGeometry, antennaMaterial);
leftEar.rotation.z = Math.PI / 4;
leftEar.position.set(-1.5, TV_HEIGHT / 2 + 2.5 + TV_HEIGHT / 2, 0);
tvGroup.add(leftEar);
// Right Ear
const rightEar = new THREE.Mesh(antennaGeometry, antennaMaterial);
rightEar.rotation.z = -Math.PI / 4;
rightEar.position.set(1.5, TV_HEIGHT / 2 + 2.5 + TV_HEIGHT / 2, 0);
tvGroup.add(rightEar);
return tvGroup;
}
function setupVideoControls() {
const input = document.getElementById('videoFileInput');
const video = document.getElementById('videoElement');
const label = document.getElementById('loadVideoLabel');
input.addEventListener('change', (event) => {
const file = event.target.files[0];
if (file) {
label.textContent = "Loading...";
loadVideoTexture(file, video);
}
});
}
function loadVideoTexture(file, video) {
// Stop any existing video playback and revoke old URL
if (video.src) {
video.pause();
URL.revokeObjectURL(video.src);
}
// Create new blob URL for the selected file
const videoUrl = URL.createObjectURL(file);
video.src = videoUrl;
// When video metadata is loaded, create the texture and assign it
video.onloadeddata = () => {
isStaticActive = false; // Stop static rendering
// Dispose of previous video texture if it exists
if (videoTexture) {
videoTexture.dispose();
}
// Create new VideoTexture
videoTexture = new THREE.VideoTexture(video);
videoTexture.minFilter = THREE.LinearFilter;
videoTexture.magFilter = THREE.LinearFilter;
videoTexture.format = THREE.RGBAFormat;
// Update the screen mesh material map
if (screenMesh) {
screenMesh.material.map = videoTexture;
screenMesh.material.needsUpdate = true;
document.getElementById('loadVideoLabel').textContent = "Video Playing (Click to change)";
// Attempt to play the video (it must be muted/playsinline for auto-play in most browsers)
video.play().catch(e => {
console.error("Video playback failed (User interaction required):", e);
document.getElementById('loadVideoLabel').textContent = "Playback blocked (Click screen to play)";
// Handle browsers blocking auto-play by waiting for user interaction
renderer.domElement.addEventListener('click', () => {
video.play().then(() => {
document.getElementById('loadVideoLabel').textContent = "Video Playing (Click to change)";
}).catch(err => console.error("Manual playback failed:", err));
}, { once: true });
});
}
};
}
function updateStaticTexture() {
if (!screenMesh || !isStaticActive) return;
const screenContext = screenMesh.userData.context;
const screenCanvas = staticTexture.image;
const w = screenCanvas.width;
const h = screenCanvas.height;
const imageData = screenContext.getImageData(0, 0, w, h);
const data = imageData.data;
const numPixels = w * h;
// Time-based shift for "color bloom/misalignment" effect
const time = Date.now() * 0.005;
const rShift = Math.sin(time * 0.5) * 5;
const gShift = Math.sin(time * 0.5 + 2) * 5;
const bShift = Math.sin(time * 0.5 + 4) * 5;
// Generate random noise
for (let i = 0; i < numPixels; i++) {
const noise = Math.random() * 200 + 55;
const dataIndex = i * 4;
// Apply color shift
data[dataIndex] = Math.min(255, noise + rShift + Math.random() * 10);
data[dataIndex + 1] = Math.min(255, noise + gShift + Math.random() * 10);
data[dataIndex + 2] = Math.min(255, noise + bShift + Math.random() * 10);
data[dataIndex + 3] = 255; // Alpha
}
screenContext.putImageData(imageData, 0, 0);
staticTexture.needsUpdate = true; // Update the static texture
}
function addLights() {
// Ambient light for general scene illumination
const ambientLight = new THREE.AmbientLight(0xffffff, 0.4);
scene.add(ambientLight);
// Directional light from the front-top-right
const directionalLight1 = new THREE.DirectionalLight(0xffffff, 0.8);
directionalLight1.position.set(5, 10, 10);
scene.add(directionalLight1);
}
function animate() {
requestAnimationFrame(animate);
if (isStaticActive) {
updateStaticTexture();
} else if (videoTexture) {
// Ensure video texture updates when playing
videoTexture.needsUpdate = true;
}
renderer.render(scene, camera);
}
// --- Event Handlers (Drag/Resize Logic) ---
function onWindowResize() {
const container = document.getElementById('tv-container');
camera.aspect = container.clientWidth / container.clientHeight;
camera.updateProjectionMatrix();
renderer.setSize(container.clientWidth, container.clientHeight);
}
function onMouseDown(event) {
isDragging = true;
previousMousePosition.x = event.clientX;
previousMousePosition.y = event.clientY;
}
function onTouchStart(event) {
if (event.touches.length === 1) {
isDragging = true;
previousMousePosition.x = event.touches[0].clientX;
previousMousePosition.y = event.touches[0].clientY;
}
}
function onMouseMove(event) {
if (!isDragging) return;
const deltaX = event.clientX - previousMousePosition.x;
const deltaY = event.clientY - previousMousePosition.y;
const rotationSpeed = 0.005;
tvModel.rotation.y += deltaX * rotationSpeed;
tvModel.rotation.x += deltaY * rotationSpeed;
tvModel.rotation.x = Math.max(-Math.PI / 4, Math.min(Math.PI / 4, tvModel.rotation.x));
previousMousePosition.x = event.clientX;
previousMousePosition.y = event.clientY;
}
function onTouchMove(event) {
if (!isDragging || event.touches.length !== 1) return;
const touch = event.touches[0];
const deltaX = touch.clientX - previousMousePosition.x;
const deltaY = touch.clientY - previousMousePosition.y;
const rotationSpeed = 0.007;
tvModel.rotation.y += deltaX * rotationSpeed;
tvModel.rotation.x += deltaY * rotationSpeed;
tvModel.rotation.x = Math.max(-Math.PI / 4, Math.min(Math.PI / 4, tvModel.rotation.x));
previousMousePosition.x = touch.clientX;
previousMousePosition.y = touch.clientY;
event.preventDefault();
}
function onMouseUp(event) {
isDragging = false;
}
// Start the application when the window loads
window.onload = function () {
init();
animate();
};
</script>
</body>
</html>