diff --git a/party-stage/src/core/media-storage.js b/party-stage/src/core/media-storage.js index e37d357..32c6685 100644 --- a/party-stage/src/core/media-storage.js +++ b/party-stage/src/core/media-storage.js @@ -1,5 +1,5 @@ const DB_NAME = 'PartyMediaDB'; -const DB_VERSION = 1; +const DB_VERSION = 2; export const MediaStorage = { open: () => { @@ -9,6 +9,7 @@ export const MediaStorage = { const db = e.target.result; if (!db.objectStoreNames.contains('music')) db.createObjectStore('music'); if (!db.objectStoreNames.contains('tapes')) db.createObjectStore('tapes'); + if (!db.objectStoreNames.contains('poster')) db.createObjectStore('poster'); }; request.onsuccess = (e) => resolve(e.target.result); request.onerror = (e) => reject(e); @@ -42,10 +43,24 @@ export const MediaStorage = { req.onerror = () => resolve([]); }); }, + savePoster: async (file) => { + const db = await MediaStorage.open(); + const tx = db.transaction('poster', 'readwrite'); + tx.objectStore('poster').put(file, 'currentPoster'); + }, + getPoster: async () => { + const db = await MediaStorage.open(); + return new Promise((resolve) => { + const req = db.transaction('poster', 'readonly').objectStore('poster').get('currentPoster'); + req.onsuccess = () => resolve(req.result); + req.onerror = () => resolve(null); + }); + }, clear: async () => { const db = await MediaStorage.open(); - const tx = db.transaction(['music', 'tapes'], 'readwrite'); + const tx = db.transaction(['music', 'tapes', 'poster'], 'readwrite'); tx.objectStore('music').clear(); tx.objectStore('tapes').clear(); + tx.objectStore('poster').clear(); } }; \ No newline at end of file diff --git a/party-stage/src/scene/config-ui.js b/party-stage/src/scene/config-ui.js index 280f334..8a7b20e 100644 --- a/party-stage/src/scene/config-ui.js +++ b/party-stage/src/scene/config-ui.js @@ -136,6 +136,40 @@ export class ConfigUI extends SceneFeature { }); statusContainer.appendChild(this.tapeList); + // Load Poster Button + const loadPosterBtn = document.createElement('button'); + loadPosterBtn.innerText = 'Load Poster'; + Object.assign(loadPosterBtn.style, { + marginTop: '10px', + padding: '8px', + cursor: 'pointer', + backgroundColor: '#555', + color: 'white', + border: 'none', + borderRadius: '4px', + fontSize: '14px' + }); + + const posterInput = document.createElement('input'); + posterInput.type = 'file'; + posterInput.accept = 'image/*'; + posterInput.style.display = 'none'; + posterInput.onchange = (e) => { + const file = e.target.files[0]; + if (file) { + if (state.posterImage) URL.revokeObjectURL(state.posterImage); + state.posterImage = URL.createObjectURL(file); + MediaStorage.savePoster(file); + showStandbyScreen(); + } + }; + document.body.appendChild(posterInput); + + loadPosterBtn.onclick = () => { + posterInput.click(); + }; + statusContainer.appendChild(loadPosterBtn); + // Load Tapes Button const loadTapesBtn = document.createElement('button'); loadTapesBtn.id = 'loadTapeButton'; @@ -235,6 +269,11 @@ export class ConfigUI extends SceneFeature { } } + if (state.posterImage) { + URL.revokeObjectURL(state.posterImage); + state.posterImage = null; + } + showStandbyScreen(); const defaults = { @@ -259,6 +298,14 @@ export class ConfigUI extends SceneFeature { document.body.appendChild(container); this.container = container; this.updateStatus(); + + // Restore poster + MediaStorage.getPoster().then(file => { + if (file) { + state.posterImage = URL.createObjectURL(file); + showStandbyScreen(); + } + }); } updateStatus() { diff --git a/party-stage/src/scene/projection-screen.js b/party-stage/src/scene/projection-screen.js index 6fe5277..20aa25d 100644 --- a/party-stage/src/scene/projection-screen.js +++ b/party-stage/src/scene/projection-screen.js @@ -298,47 +298,53 @@ export class ProjectionScreen extends SceneFeature { export function showStandbyScreen() { if (projectionScreenInstance) projectionScreenInstance.isVisualizerActive = false; - const canvas = document.createElement('canvas'); - canvas.width = 1024; - canvas.height = 576; - const ctx = canvas.getContext('2d'); + let texture; - // 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); - }); + 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'); - // Semi-transparent overlay - ctx.fillStyle = 'rgba(0, 0, 0, 0.7)'; - ctx.fillRect(0, 0, canvas.width, canvas.height); + // 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); + }); - // Text settings - ctx.fillStyle = '#ffffff'; - ctx.textAlign = 'center'; - ctx.textBaseline = 'middle'; + // Semi-transparent overlay + ctx.fillStyle = 'rgba(0, 0, 0, 0.7)'; + ctx.fillRect(0, 0, canvas.width, canvas.height); - // Song Title - let text = "PLEASE STAND BY"; - if (state.music && state.music.songTitle) { - text = state.music.songTitle.toUpperCase(); + // 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(); + } + + 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); } - 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); - - const texture = new THREE.CanvasTexture(canvas); - const material = new THREE.MeshBasicMaterial({ map: texture, side: THREE.DoubleSide diff --git a/party-stage/src/state.js b/party-stage/src/state.js index b4d4b08..70c42ec 100644 --- a/party-stage/src/state.js +++ b/party-stage/src/state.js @@ -44,6 +44,7 @@ export function initState() { videoUrls: [], videoFilenames: [], currentVideoIndex: -1, + posterImage: null, // Scene constants originalLampIntensity: 0.3,