music-video-gen/tv-player/src/scene/vcr-display.js
2025-11-17 14:56:41 +01:00

215 lines
7.6 KiB
JavaScript

import * as THREE from 'three';
import { state } from '../state.js';
import { formatTime } from '../utils.js';
// --- 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 (state.isVideoLoaded && state.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, state.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 ---
export 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 = '#030303';
ctx.fillRect(0, 0, canvas.width, canvas.height);
state.vcrDisplayTexture = new THREE.CanvasTexture(canvas);
state.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: state.vcrDisplayTexture,
side: THREE.FrontSide,
color: 0x105a10,
});
const displayMesh = new THREE.Mesh(displayGeometry, displayMaterial);
return displayMesh;
}
export function updateVcrDisplay(time) {
if (!state.vcrDisplayTexture) return;
const canvas = state.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);
state.vcrDisplayTexture.needsUpdate = true;
}