One-time play through and VCR timing
This commit is contained in:
parent
c870b7e5f3
commit
4784d5ee26
@ -51,13 +51,7 @@
|
|||||||
<!-- Load Tapes Button --><button id="loadTapeButton" class="tape-button px-8 py-3 bg-tape-red text-white font-bold text-lg uppercase tracking-wider rounded-lg hover:bg-red-700 transition duration-150">
|
<!-- Load Tapes Button --><button id="loadTapeButton" class="tape-button px-8 py-3 bg-tape-red text-white font-bold text-lg uppercase tracking-wider rounded-lg hover:bg-red-700 transition duration-150">
|
||||||
Load tapes
|
Load tapes
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<!-- Next Tape Button (still allows manual skip) --><button id="nextTapeButton" class="tape-button px-6 py-3 bg-gray-600 text-white font-bold text-lg uppercase tracking-wider rounded-lg opacity-50 cursor-not-allowed" disabled>
|
|
||||||
Next (0/0)
|
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Status message area --><p id="status" class="text-sm text-yellow-300 text-center font-mono opacity-80">Ready.</p>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 3D Canvas will be injected here by Three.js --><script>
|
<!-- 3D Canvas will be injected here by Three.js --><script>
|
||||||
@ -77,9 +71,7 @@
|
|||||||
const container = document.body;
|
const container = document.body;
|
||||||
const videoElement = document.getElementById('video');
|
const videoElement = document.getElementById('video');
|
||||||
const fileInput = document.getElementById('fileInput');
|
const fileInput = document.getElementById('fileInput');
|
||||||
const statusText = document.getElementById('status');
|
|
||||||
const loadTapeButton = document.getElementById('loadTapeButton');
|
const loadTapeButton = document.getElementById('loadTapeButton');
|
||||||
const nextTapeButton = document.getElementById('nextTapeButton');
|
|
||||||
const loader = new THREE.TextureLoader();
|
const loader = new THREE.TextureLoader();
|
||||||
|
|
||||||
const debugLight = false;
|
const debugLight = false;
|
||||||
@ -181,7 +173,6 @@
|
|||||||
loadTapeButton.addEventListener('click', () => {
|
loadTapeButton.addEventListener('click', () => {
|
||||||
fileInput.click();
|
fileInput.click();
|
||||||
});
|
});
|
||||||
nextTapeButton.addEventListener('click', playNextVideo);
|
|
||||||
|
|
||||||
// Auto-advance to the next video when the current one finishes.
|
// Auto-advance to the next video when the current one finishes.
|
||||||
videoElement.addEventListener('ended', playNextVideo);
|
videoElement.addEventListener('ended', playNextVideo);
|
||||||
@ -576,6 +567,12 @@
|
|||||||
|
|
||||||
// Position the curved screen
|
// Position the curved screen
|
||||||
tvScreen.position.set(0.0, 1.5, -2.1);
|
tvScreen.position.set(0.0, 1.5, -2.1);
|
||||||
|
tvScreen.material = new THREE.MeshPhongMaterial({
|
||||||
|
color: 0x0a0a0a, // Deep black
|
||||||
|
shininess: 5,
|
||||||
|
specular: 0x111111
|
||||||
|
});
|
||||||
|
tvScreen.material.needsUpdate = true;
|
||||||
tvGroup.add(tvScreen);
|
tvGroup.add(tvScreen);
|
||||||
|
|
||||||
tvGroup.position.set(x, 0, z);
|
tvGroup.position.set(x, 0, z);
|
||||||
@ -592,6 +589,11 @@
|
|||||||
screenLight.shadow.camera.far = 5;
|
screenLight.shadow.camera.far = 5;
|
||||||
tvGroup.add(screenLight);
|
tvGroup.add(screenLight);
|
||||||
|
|
||||||
|
// -- VCR --
|
||||||
|
const vcr = createVcr();
|
||||||
|
vcr.position.set(-0.3, 0.6, 0.05);
|
||||||
|
tvGroup.add(vcr);
|
||||||
|
|
||||||
scene.add(tvGroup);
|
scene.add(tvGroup);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -748,33 +750,8 @@
|
|||||||
return `${paddedMinutes}:${paddedSeconds}`;
|
return `${paddedMinutes}:${paddedSeconds}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- Helper function to update the control buttons' state and text ---
|
|
||||||
function updateControls() {
|
|
||||||
const total = videoUrls.length;
|
|
||||||
const current = currentVideoIndex + 1;
|
|
||||||
|
|
||||||
if (total > 1) {
|
|
||||||
nextTapeButton.disabled = false;
|
|
||||||
nextTapeButton.classList.remove('opacity-50', 'cursor-not-allowed', 'bg-gray-600');
|
|
||||||
nextTapeButton.classList.add('bg-tape-red', 'hover:bg-red-700');
|
|
||||||
} else {
|
|
||||||
nextTapeButton.disabled = true;
|
|
||||||
nextTapeButton.classList.add('opacity-50', 'cursor-not-allowed', 'bg-gray-600');
|
|
||||||
nextTapeButton.classList.remove('bg-tape-red', 'hover:bg-red-700');
|
|
||||||
}
|
|
||||||
// Always show the current tape count
|
|
||||||
nextTapeButton.textContent = `Next (${current}/${total})`;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
// --- Play video by index ---
|
// --- Play video by index ---
|
||||||
function playVideoByIndex(index) {
|
function playVideoByIndex(index) {
|
||||||
if (index < 0 || index >= videoUrls.length) {
|
|
||||||
statusText.textContent = 'End of playlist reached. Reload tapes to start again.';
|
|
||||||
screenLight.intensity = 0.0;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
currentVideoIndex = index;
|
currentVideoIndex = index;
|
||||||
const url = videoUrls[index];
|
const url = videoUrls[index];
|
||||||
|
|
||||||
@ -784,12 +761,27 @@
|
|||||||
videoTexture = null;
|
videoTexture = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (index < 0 || index >= videoUrls.length) {
|
||||||
|
console.info('End of playlist reached. Reload tapes to start again.');
|
||||||
|
screenLight.intensity = 0.0;
|
||||||
|
tvScreen.material.dispose();
|
||||||
|
tvScreen.material = new THREE.MeshPhongMaterial({
|
||||||
|
color: 0x0a0a0a, // Deep black
|
||||||
|
shininess: 5,
|
||||||
|
specular: 0x111111
|
||||||
|
});
|
||||||
|
tvScreen.material.needsUpdate = true;
|
||||||
|
isVideoLoaded = false;
|
||||||
|
lastUpdateTime = -1; // force VCR to redraw
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
videoElement.src = url;
|
videoElement.src = url;
|
||||||
videoElement.muted = true;
|
videoElement.muted = true;
|
||||||
videoElement.load();
|
videoElement.load();
|
||||||
|
|
||||||
// Set loop property: only loop if it's the only video loaded
|
// Set loop property: only loop if it's the only video loaded
|
||||||
videoElement.loop = videoUrls.length === 1;
|
videoElement.loop = false; //videoUrls.length === 1;
|
||||||
|
|
||||||
|
|
||||||
videoElement.onloadeddata = () => {
|
videoElement.onloadeddata = () => {
|
||||||
@ -811,29 +803,29 @@
|
|||||||
// Use the defined base intensity for screen glow
|
// Use the defined base intensity for screen glow
|
||||||
screenLight.intensity = originalScreenIntensity;
|
screenLight.intensity = originalScreenIntensity;
|
||||||
// Initial status message with tape count
|
// Initial status message with tape count
|
||||||
statusText.textContent = `Playing tape ${currentVideoIndex + 1} of ${videoUrls.length}.`;
|
console.info(`Playing tape ${currentVideoIndex + 1} of ${videoUrls.length}.`);
|
||||||
updateControls();
|
|
||||||
}).catch(error => {
|
}).catch(error => {
|
||||||
screenLight.intensity = originalScreenIntensity * 0.5; // Dim the light if playback fails
|
screenLight.intensity = originalScreenIntensity * 0.5; // Dim the light if playback fails
|
||||||
statusText.textContent = `Playback blocked for tape ${currentVideoIndex + 1}. Click Next Tape to try again.`;
|
console.error(`Playback blocked for tape ${currentVideoIndex + 1}. Click Next Tape to try again.`);
|
||||||
console.error('Playback Error: Could not start video playback.', error);
|
console.error('Playback Error: Could not start video playback.', error);
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
videoElement.onerror = (e) => {
|
videoElement.onerror = (e) => {
|
||||||
screenLight.intensity = 0.1; // Keep minimum intensity for shadow map
|
screenLight.intensity = 0.1; // Keep minimum intensity for shadow map
|
||||||
statusText.textContent = `Error loading tape ${currentVideoIndex + 1}.`;
|
console.error(`Error loading tape ${currentVideoIndex + 1}.`);
|
||||||
console.error('Video Load Error:', e);
|
console.error('Video Load Error:', e);
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- Cycle to the next video ---
|
// --- Cycle to the next video ---
|
||||||
function playNextVideo() {
|
function playNextVideo() {
|
||||||
if (videoUrls.length > 0) {
|
// Determine the next index, cycling back to 0 if we reach the end
|
||||||
// Determine the next index, cycling back to 0 if we reach the end
|
let nextIndex = currentVideoIndex + 1;
|
||||||
let nextIndex = (currentVideoIndex + 1) % videoUrls.length;
|
if (nextIndex < videoUrls.length) {
|
||||||
playVideoByIndex(nextIndex);
|
baseTime += videoElement.duration;
|
||||||
}
|
}
|
||||||
|
playVideoByIndex(nextIndex);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@ -841,7 +833,7 @@
|
|||||||
function loadVideoFile(event) {
|
function loadVideoFile(event) {
|
||||||
const files = event.target.files;
|
const files = event.target.files;
|
||||||
if (files.length === 0) {
|
if (files.length === 0) {
|
||||||
statusText.textContent = 'File selection cancelled.';
|
console.info('File selection cancelled.');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -858,14 +850,300 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (videoUrls.length === 0) {
|
if (videoUrls.length === 0) {
|
||||||
statusText.textContent = 'No valid video files selected.';
|
console.info('No valid video files selected.');
|
||||||
updateControls();
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 3. Start playback of the first video
|
// 3. Start playback of the first video
|
||||||
statusText.textContent = `Loaded ${videoUrls.length} tapes. Starting playback...`;
|
console.info(`Loaded ${videoUrls.length} tapes. Starting playback...`);
|
||||||
playVideoByIndex(0);
|
loadTapeButton.classList.add("hidden");
|
||||||
|
|
||||||
|
const startDelay = 5;
|
||||||
|
console.info(`Video will start in ${startDelay} seconds.`);
|
||||||
|
setTimeout(() => { playVideoByIndex(0); }, startDelay * 1000);
|
||||||
|
}
|
||||||
|
|
||||||
|
let vcrDisplayLight;
|
||||||
|
let simulatedPlaybackTime = 0;
|
||||||
|
let lastUpdateTime = -1;
|
||||||
|
let baseTime = 0;
|
||||||
|
let blinkState = false; // For blinking colon
|
||||||
|
let lastBlinkToggleTime = 0;
|
||||||
|
|
||||||
|
// --- Segment Display Definitions ---
|
||||||
|
|
||||||
|
// Define which segments (indexed 0-6: A, B, C, D, E, F, G) are active for each digit
|
||||||
|
// A=Top, B=TR, C=BR, D=Bottom, E=BL, F=TL, G=Middle
|
||||||
|
const SEGMENTS = {
|
||||||
|
'0': [1, 1, 1, 1, 1, 1, 0],
|
||||||
|
'1': [0, 1, 1, 0, 0, 0, 0],
|
||||||
|
'2': [1, 1, 0, 1, 1, 0, 1],
|
||||||
|
'3': [1, 1, 1, 1, 0, 0, 1],
|
||||||
|
'4': [0, 1, 1, 0, 0, 1, 1],
|
||||||
|
'5': [1, 0, 1, 1, 0, 1, 1],
|
||||||
|
'6': [1, 0, 1, 1, 1, 1, 1],
|
||||||
|
'7': [1, 1, 1, 0, 0, 0, 0],
|
||||||
|
'8': [1, 1, 1, 1, 1, 1, 1],
|
||||||
|
'9': [1, 1, 1, 1, 0, 1, 1],
|
||||||
|
' ': [0, 0, 0, 0, 0, 0, 0]
|
||||||
|
};
|
||||||
|
|
||||||
|
const SEG_THICKNESS = 3; // Thickness of the segment line in canvas pixels
|
||||||
|
const SEG_PADDING = 2; // Padding within a digit segment's box
|
||||||
|
|
||||||
|
// Colors for active and inactive segments
|
||||||
|
const COLOR_ACTIVE = '#00ff44'; // Bright Fluorescent Green
|
||||||
|
const COLOR_INACTIVE = '#1a1a1a'; // Dim dark gray for 'ghost' segments
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Draws a single 7-segment digit by drawing active segments.
|
||||||
|
* Now includes drawing of inactive (ghost) segments for better readability.
|
||||||
|
* @param {CanvasRenderingContext2D} ctx
|
||||||
|
* @param {string} digit The digit character (0-9).
|
||||||
|
* @param {number} x Left position of the digit area.
|
||||||
|
* @param {number} y Top position of the digit area.
|
||||||
|
* @param {number} H Total height of the digit area.
|
||||||
|
*/
|
||||||
|
function drawSegmentDigit(ctx, digit, x, y, H) {
|
||||||
|
const segments = SEGMENTS[digit] || SEGMENTS[' '];
|
||||||
|
const W = H / 2; // Width is half the height for standard aspect ratio
|
||||||
|
|
||||||
|
// Segment dimensions relative to W and H
|
||||||
|
const hLength = W - 2 * SEG_PADDING;
|
||||||
|
// Vertical length calculation: (Total height - 2 paddings - 3 horizontal thicknesses) / 2
|
||||||
|
const vLength = (H - (2 * SEG_PADDING) - (3 * SEG_THICKNESS)) / 2;
|
||||||
|
|
||||||
|
// Helper to draw horizontal segment (A, G, D)
|
||||||
|
const drawH = (index, x_start, y_start) => {
|
||||||
|
ctx.fillStyle = segments[index] ? COLOR_ACTIVE : COLOR_INACTIVE;
|
||||||
|
ctx.fillRect(x_start + SEG_PADDING, y_start, hLength, SEG_THICKNESS);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Helper to draw vertical segment (F, B, E, C)
|
||||||
|
const drawV = (index, x_start, y_start) => {
|
||||||
|
ctx.fillStyle = segments[index] ? COLOR_ACTIVE : COLOR_INACTIVE;
|
||||||
|
ctx.fillRect(x_start, y_start, SEG_THICKNESS, vLength);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Define segment positions
|
||||||
|
|
||||||
|
// Horizontal segments
|
||||||
|
// A (Top) - index 0
|
||||||
|
drawH(0, x, y + SEG_PADDING);
|
||||||
|
// G (Middle) - index 6
|
||||||
|
drawH(6, x, y + H/2 - SEG_THICKNESS/2);
|
||||||
|
// D (Bottom) - index 3
|
||||||
|
drawH(3, x, y + H - SEG_PADDING - SEG_THICKNESS);
|
||||||
|
|
||||||
|
// Vertical segments (Top Half)
|
||||||
|
const topVStart = y + SEG_PADDING + SEG_THICKNESS;
|
||||||
|
const rightVStart = x + W - SEG_PADDING - SEG_THICKNESS;
|
||||||
|
|
||||||
|
// F (Top-Left) - index 5
|
||||||
|
drawV(5, x + SEG_PADDING, topVStart);
|
||||||
|
|
||||||
|
// B (Top-Right) - index 1
|
||||||
|
drawV(1, rightVStart, topVStart);
|
||||||
|
|
||||||
|
// Vertical segments (Bottom Half)
|
||||||
|
const bottomVStart = y + H/2 + SEG_THICKNESS/2;
|
||||||
|
|
||||||
|
// E (Bottom-Left) - index 4
|
||||||
|
drawV(4, x + SEG_PADDING, bottomVStart);
|
||||||
|
|
||||||
|
// C (Bottom-Right) - index 2
|
||||||
|
drawV(2, rightVStart, bottomVStart);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Function to draw the colon (two dots), now with blinking logic
|
||||||
|
function drawColon(ctx, x, y, H, isVisible) {
|
||||||
|
const dotSize = 4;
|
||||||
|
ctx.fillStyle = COLOR_ACTIVE;
|
||||||
|
|
||||||
|
if (isVisible) {
|
||||||
|
// Top dot
|
||||||
|
ctx.fillRect(x, y + H * 0.3 - dotSize / 2, dotSize, dotSize);
|
||||||
|
// Bottom dot
|
||||||
|
ctx.fillRect(x, y + H * 0.7 - dotSize / 2, dotSize, dotSize);
|
||||||
|
} else {
|
||||||
|
// Draw inactive colon if not visible, for consistency
|
||||||
|
ctx.fillStyle = COLOR_INACTIVE;
|
||||||
|
ctx.fillRect(x, y + H * 0.3 - dotSize / 2, dotSize, dotSize);
|
||||||
|
ctx.fillRect(x, y + H * 0.7 - dotSize / 2, dotSize, dotSize);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Draws a simple playback arrow (triangle)
|
||||||
|
* @param {CanvasRenderingContext2D} ctx
|
||||||
|
* @param {number} x Left position of the arrow area.
|
||||||
|
* @param {number} y Top position of the arrow area.
|
||||||
|
* @param {number} H Total height of the arrow area.
|
||||||
|
*/
|
||||||
|
function drawPlaybackArrow(ctx, x, y, H) {
|
||||||
|
const arrowWidth = H * 0.4; // Arrow width relative to digit height
|
||||||
|
const arrowHeight = H * 0.4; // Arrow height relative to digit height
|
||||||
|
|
||||||
|
ctx.fillStyle = COLOR_ACTIVE;
|
||||||
|
ctx.beginPath();
|
||||||
|
ctx.moveTo(x, y + H * 0.5 - arrowHeight / 2); // Top point
|
||||||
|
ctx.lineTo(x + arrowWidth, y + H * 0.5); // Right point (center)
|
||||||
|
ctx.lineTo(x, y + H * 0.5 + arrowHeight / 2); // Bottom point
|
||||||
|
ctx.closePath();
|
||||||
|
ctx.fill();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// Main function to render the entire time string using segments
|
||||||
|
function drawSegmentDisplay(ctx, timeString) {
|
||||||
|
const canvasWidth = ctx.canvas.width;
|
||||||
|
const canvasHeight = ctx.canvas.height;
|
||||||
|
const timeStringLength = timeString.length;
|
||||||
|
|
||||||
|
// Clear display to dark background
|
||||||
|
ctx.fillStyle = '#0a0a0a';
|
||||||
|
ctx.fillRect(0, 0, canvasWidth, canvasHeight);
|
||||||
|
|
||||||
|
// Constants for layout
|
||||||
|
const charSpacing = 8; // Spacing between digits
|
||||||
|
const digitHeight = canvasHeight - 2 * SEG_PADDING;
|
||||||
|
const digitWidth = digitHeight / 2 + SEG_PADDING; // Total width slot for one digit
|
||||||
|
const colonWidth = 6;
|
||||||
|
const arrowWidth = digitHeight * 0.7; // Approx width for the arrow
|
||||||
|
const arrowPadding = 10; // Space between arrow and first digit
|
||||||
|
|
||||||
|
// Calculate total display width including arrow and spaces
|
||||||
|
const totalDisplayWidth = arrowWidth + arrowPadding + (4 * digitWidth) + colonWidth + ((timeStringLength - 1) * charSpacing);
|
||||||
|
|
||||||
|
// Calculate starting X to center the display
|
||||||
|
let currentX = (canvasWidth - totalDisplayWidth) / 2;
|
||||||
|
const currentY = SEG_PADDING;
|
||||||
|
|
||||||
|
// Draw Playback Arrow
|
||||||
|
if (isVideoLoaded && videoElement.readyState >= 3) {
|
||||||
|
drawPlaybackArrow(ctx, currentX, currentY, digitHeight);
|
||||||
|
}
|
||||||
|
currentX += arrowWidth + arrowPadding; // Move X after arrow and its padding
|
||||||
|
|
||||||
|
for (let i = 0; i < timeStringLength; i++) {
|
||||||
|
const char = timeString[i];
|
||||||
|
|
||||||
|
if (char === ':') {
|
||||||
|
drawColon(ctx, currentX, currentY, digitHeight, blinkState); // Pass blinkState
|
||||||
|
currentX += colonWidth;
|
||||||
|
} else if (char >= '0' && char <= '9') {
|
||||||
|
drawSegmentDigit(ctx, char, currentX, currentY, digitHeight);
|
||||||
|
currentX += digitWidth;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add spacing only if it's not the last element
|
||||||
|
if (i < timeStringLength - 1) {
|
||||||
|
currentX += charSpacing;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- VCR Display Functions ---
|
||||||
|
function createVcrDisplay() {
|
||||||
|
const canvas = document.createElement('canvas');
|
||||||
|
canvas.width = 160; // Increased width for arrow and better spacing
|
||||||
|
canvas.height = 32;
|
||||||
|
const ctx = canvas.getContext('2d');
|
||||||
|
|
||||||
|
ctx.fillStyle = '#0a0a0a';
|
||||||
|
ctx.fillRect(0, 0, canvas.width, canvas.height);
|
||||||
|
|
||||||
|
vcrDisplayTexture = new THREE.CanvasTexture(canvas);
|
||||||
|
vcrDisplayTexture.needsUpdate = true;
|
||||||
|
|
||||||
|
const displayGeometry = new THREE.PlaneGeometry(0.45, 0.1); // Adjust geometry width for new canvas size
|
||||||
|
const displayMaterial = new THREE.MeshBasicMaterial({
|
||||||
|
map: vcrDisplayTexture,
|
||||||
|
side: THREE.FrontSide,
|
||||||
|
color: 0xffffff,
|
||||||
|
transparent: true,
|
||||||
|
emissive: 0x00ff44,
|
||||||
|
emissiveIntensity: 0.1
|
||||||
|
});
|
||||||
|
|
||||||
|
const displayMesh = new THREE.Mesh(displayGeometry, displayMaterial);
|
||||||
|
return displayMesh;
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateVcrDisplay(time) {
|
||||||
|
if (!vcrDisplayTexture) return;
|
||||||
|
|
||||||
|
const canvas = vcrDisplayTexture.image;
|
||||||
|
const ctx = canvas.getContext('2d');
|
||||||
|
|
||||||
|
const timeString = formatTime(time);
|
||||||
|
|
||||||
|
// Uses the new segment drawing function with ghosting, including blinkState for colon
|
||||||
|
drawSegmentDisplay(ctx, timeString);
|
||||||
|
|
||||||
|
vcrDisplayTexture.needsUpdate = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- VCR Model Function ---
|
||||||
|
function createVcr() {
|
||||||
|
// Materials
|
||||||
|
const vcrBodyMaterial = new THREE.MeshPhongMaterial({
|
||||||
|
color: 0x222222, // Dark metallic gray
|
||||||
|
shininess: 70,
|
||||||
|
specular: 0x444444
|
||||||
|
});
|
||||||
|
const slotMaterial = new THREE.MeshPhongMaterial({
|
||||||
|
color: 0x0a0a0a, // Deep black
|
||||||
|
shininess: 5,
|
||||||
|
specular: 0x111111
|
||||||
|
});
|
||||||
|
const floorMaterial = new THREE.MeshPhongMaterial({ color: 0x1a1a1a, shininess: 5 });
|
||||||
|
|
||||||
|
// VCR Body
|
||||||
|
const vcrBodyGeometry = new THREE.BoxGeometry(1.0, 0.2, 0.7);
|
||||||
|
const vcrBody = new THREE.Mesh(vcrBodyGeometry, vcrBodyMaterial);
|
||||||
|
vcrBody.position.y = 0; // Centered
|
||||||
|
vcrBody.castShadow = true;
|
||||||
|
vcrBody.receiveShadow = true;
|
||||||
|
|
||||||
|
// Cassette Slot / Front Face
|
||||||
|
const slotGeometry = new THREE.BoxGeometry(0.9, 0.05, 0.01);
|
||||||
|
const slotMesh = new THREE.Mesh(slotGeometry, slotMaterial);
|
||||||
|
slotMesh.position.set(0, -0.05, 0.35 + 0.005);
|
||||||
|
slotMesh.castShadow = true;
|
||||||
|
slotMesh.receiveShadow = true;
|
||||||
|
|
||||||
|
// VCR Display
|
||||||
|
const displayMesh = createVcrDisplay();
|
||||||
|
displayMesh.position.z = 0.35 + 0.005;
|
||||||
|
displayMesh.position.x = 0.2; // Adjusted X for arrow
|
||||||
|
displayMesh.position.y = 0.03;
|
||||||
|
|
||||||
|
// VCR Group
|
||||||
|
const vcrGroup = new THREE.Group();
|
||||||
|
vcrGroup.add(vcrBody, slotMesh, displayMesh);
|
||||||
|
vcrGroup.position.set(0, 0.1, 0); // Position the whole VCR slightly above the floor
|
||||||
|
|
||||||
|
// Light from the VCR display itself
|
||||||
|
vcrDisplayLight = new THREE.PointLight(0x00ff44, 0.5, 1);
|
||||||
|
vcrDisplayLight.position.set(0.3, 0.03, 0.35 + 0.05); // Move light slightly closer to VCR surface
|
||||||
|
vcrDisplayLight.castShadow = true;
|
||||||
|
vcrDisplayLight.shadow.mapSize.width = 256;
|
||||||
|
vcrDisplayLight.shadow.mapSize.height = 256;
|
||||||
|
vcrGroup.add(vcrDisplayLight);
|
||||||
|
|
||||||
|
return vcrGroup;
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Helper function to format seconds into MM:SS ---
|
||||||
|
function formatTime(seconds) {
|
||||||
|
if (isNaN(seconds) || seconds < 0) return '00:00';
|
||||||
|
const totalSeconds = Math.floor(seconds);
|
||||||
|
const minutes = Math.floor(totalSeconds / 60);
|
||||||
|
const remainingSeconds = totalSeconds % 60;
|
||||||
|
const paddedMinutes = String(minutes).padStart(2, '0');
|
||||||
|
const paddedSeconds = String(remainingSeconds).padStart(2, '0');
|
||||||
|
return `${paddedMinutes}:${paddedSeconds}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
function randomFlyTarget() {
|
function randomFlyTarget() {
|
||||||
@ -1140,13 +1418,26 @@
|
|||||||
if (isVideoLoaded && videoElement.readyState >= 3) {
|
if (isVideoLoaded && videoElement.readyState >= 3) {
|
||||||
const currentTime = formatTime(videoElement.currentTime);
|
const currentTime = formatTime(videoElement.currentTime);
|
||||||
const duration = formatTime(videoElement.duration);
|
const duration = formatTime(videoElement.duration);
|
||||||
statusText.textContent =
|
console.info(`Tape ${currentVideoIndex + 1} of ${videoUrls.length}. Time: ${currentTime} / ${duration}`);
|
||||||
`Tape ${currentVideoIndex + 1} of ${videoUrls.length}. Time: ${currentTime} / ${duration}`;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
updateFlies();
|
updateFlies();
|
||||||
|
|
||||||
|
const currentTime = baseTime + videoElement.currentTime;
|
||||||
|
|
||||||
|
// Simulate playback time
|
||||||
|
if (Math.abs(currentTime - lastUpdateTime) > 0.1) {
|
||||||
|
updateVcrDisplay(currentTime);
|
||||||
|
lastUpdateTime = currentTime;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Blink the colon every second
|
||||||
|
if (currentTime - lastBlinkToggleTime > 0.5) { // Blink every 0.5 seconds
|
||||||
|
blinkState = !blinkState;
|
||||||
|
lastBlinkToggleTime = currentTime;
|
||||||
|
}
|
||||||
|
|
||||||
// RENDER!
|
// RENDER!
|
||||||
renderer.render(scene, camera);
|
renderer.render(scene, camera);
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user