From 767460e08c405881de138e9873f419a8ee13de80 Mon Sep 17 00:00:00 2001 From: Dejvino Date: Sun, 4 Jan 2026 20:27:26 +0000 Subject: [PATCH] Feature: Scrolling song name in standby mode --- party-stage/src/scene/projection-screen.js | 179 +++++++++++++++------ 1 file changed, 128 insertions(+), 51 deletions(-) diff --git a/party-stage/src/scene/projection-screen.js b/party-stage/src/scene/projection-screen.js index 5c282fa..c8a6777 100644 --- a/party-stage/src/scene/projection-screen.js +++ b/party-stage/src/scene/projection-screen.js @@ -130,6 +130,18 @@ export class ProjectionScreen extends SceneFeature { this.isVisualizerActive = false; this.screens = []; this.colorBuffer = new Float32Array(16 * 3); + + this.standbyState = { + active: false, + scrollX: 0, + text: '', + header: '', + subtext: '' + }; + this.standbyCanvas = null; + this.standbyCtx = null; + this.standbyTexture = null; + sceneFeatureManager.register(this); } @@ -232,6 +244,10 @@ export class ProjectionScreen extends SceneFeature { update(deltaTime) { updateScreenEffect(); + + if (this.standbyState.active) { + this.renderStandbyFrame(); + } // Wobble Logic const time = state.clock.getElapsedTime(); @@ -337,68 +353,129 @@ export class ProjectionScreen extends SceneFeature { this.isVisualizerActive = false; turnTvScreenOff(); } + + activateStandby() { + this.isVisualizerActive = false; + + if (!this.standbyCanvas) { + this.standbyCanvas = document.createElement('canvas'); + this.standbyCanvas.width = 1024; + this.standbyCanvas.height = 576; + this.standbyCtx = this.standbyCanvas.getContext('2d'); + this.standbyTexture = new THREE.CanvasTexture(this.standbyCanvas); + this.standbyTexture.minFilter = THREE.LinearFilter; + this.standbyTexture.magFilter = THREE.LinearFilter; + } + + const songTitle = (state.music && state.music.songTitle) ? state.music.songTitle.toUpperCase() : ""; + + this.standbyState = { + active: true, + scrollX: 0, + text: songTitle, + header: "PLEASE STAND BY", + subtext: "WAITING FOR PARTY START" + }; + + const material = new THREE.MeshBasicMaterial({ + map: this.standbyTexture, + side: THREE.DoubleSide + }); + + this.applyMaterialToAll(material); + this.setAllVisible(true); + state.screenLight.intensity = 0.5; + + // Measure text to set initial scroll + this.standbyCtx.font = 'bold 80px monospace'; + const textWidth = this.standbyCtx.measureText(this.standbyState.text).width; + if (textWidth > this.standbyCanvas.width * 0.9) { + this.standbyState.scrollX = this.standbyCanvas.width; + } + + this.renderStandbyFrame(); + } + + renderStandbyFrame() { + if (!this.standbyState.active || !this.standbyCtx) return; + + const ctx = this.standbyCtx; + const width = this.standbyCanvas.width; + const height = this.standbyCanvas.height; + + // Draw Background + const colors = ['#ffffff', '#ffff00', '#00ffff', '#00ff00', '#ff00ff', '#ff0000', '#0000ff']; + const barWidth = width / colors.length; + colors.forEach((color, i) => { + ctx.fillStyle = color; + ctx.fillRect(i * barWidth, 0, barWidth, height); + }); + ctx.fillStyle = 'rgba(0, 0, 0, 0.7)'; + ctx.fillRect(0, 0, width, height); + + // Header + ctx.fillStyle = '#ffffff'; + ctx.textAlign = 'center'; + ctx.textBaseline = 'middle'; + ctx.font = 'bold 40px monospace'; + ctx.fillText(this.standbyState.header, width / 2, height * 0.2); + + // Song Title + const text = this.standbyState.text; + ctx.font = 'bold 80px monospace'; + const textMetrics = ctx.measureText(text); + const textWidth = textMetrics.width; + const centerY = height * 0.5; + + if (textWidth > width * 0.9) { + // Scroll + this.standbyState.scrollX -= 3; // Speed + if (this.standbyState.scrollX < -textWidth) { + this.standbyState.scrollX = width; + } + ctx.textAlign = 'left'; + ctx.fillText(text, this.standbyState.scrollX, centerY); + } else { + // Center + ctx.textAlign = 'center'; + ctx.fillText(text, width / 2, centerY); + } + + // Subtext + ctx.font = '30px monospace'; + ctx.fillStyle = '#cccccc'; + ctx.textAlign = 'center'; + ctx.fillText(this.standbyState.subtext, width / 2, height * 0.8); + + if (this.standbyTexture) this.standbyTexture.needsUpdate = true; + } } // --- Exported Control Functions --- export function showStandbyScreen() { - if (projectionScreenInstance) projectionScreenInstance.isVisualizerActive = false; - - let texture; + if (projectionScreenInstance) { + projectionScreenInstance.isVisualizerActive = false; + projectionScreenInstance.standbyState.active = false; + } if (state.posterImage) { - texture = new THREE.TextureLoader().load(state.posterImage); - } else { - const canvas = document.createElement('canvas'); - canvas.width = 1024; - canvas.height = 576; - const ctx = canvas.getContext('2d'); - - // Draw Color Bars - const colors = ['#ffffff', '#ffff00', '#00ffff', '#00ff00', '#ff00ff', '#ff0000', '#0000ff']; - const barWidth = canvas.width / colors.length; - colors.forEach((color, i) => { - ctx.fillStyle = color; - ctx.fillRect(i * barWidth, 0, barWidth, canvas.height); + const texture = new THREE.TextureLoader().load(state.posterImage); + const material = new THREE.MeshBasicMaterial({ + map: texture, + side: THREE.DoubleSide }); - - // Semi-transparent overlay - ctx.fillStyle = 'rgba(0, 0, 0, 0.7)'; - ctx.fillRect(0, 0, canvas.width, canvas.height); - - // Text settings - ctx.fillStyle = '#ffffff'; - ctx.textAlign = 'center'; - ctx.textBaseline = 'middle'; - - // Song Title - let text = "PLEASE STAND BY"; - if (state.music && state.music.songTitle) { - text = state.music.songTitle.toUpperCase(); + if (projectionScreenInstance) { + projectionScreenInstance.applyMaterialToAll(material); + projectionScreenInstance.setAllVisible(true); } - - ctx.font = 'bold 60px monospace'; - if (ctx.measureText(text).width > canvas.width * 0.9) { - ctx.font = 'bold 40px monospace'; - } - ctx.fillText(text, canvas.width / 2, canvas.height / 2 - 20); - - // Subtext - ctx.font = '30px monospace'; - ctx.fillStyle = '#cccccc'; - ctx.fillText("WAITING FOR PARTY START", canvas.width / 2, canvas.height / 2 + 50); - - texture = new THREE.CanvasTexture(canvas); + state.screenLight.intensity = 0.5; + return; } - const material = new THREE.MeshBasicMaterial({ - map: texture, - side: THREE.DoubleSide - }); - if (projectionScreenInstance) projectionScreenInstance.applyMaterialToAll(material); - if (projectionScreenInstance) projectionScreenInstance.setAllVisible(true); - - state.screenLight.intensity = 0.5; + if (projectionScreenInstance) { + projectionScreenInstance.activateStandby(); + } } export function turnTvScreenOn() {