Restructure. Fix vendor script versions.
This commit is contained in:
@@ -0,0 +1,436 @@
|
||||
<!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>
|
||||
Reference in New Issue
Block a user