Feature: Scrolling song name in standby mode

This commit is contained in:
Dejvino 2026-01-04 20:27:26 +00:00
parent 31debfe151
commit 767460e08c

View File

@ -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() {