| import { init as initScene, setPaintings } from '/static/scene.js'; |
|
|
| |
| const state = { |
| userId: localStorage.getItem('dm_user') || '', |
| lastSketch: null, |
| lastDream: null, |
| activePanel: null, |
| }; |
|
|
| |
| async function api(path, body) { |
| const r = await fetch(path, { |
| method: 'POST', |
| headers: { 'Content-Type': 'application/json' }, |
| body: JSON.stringify(body), |
| }); |
| return r.json(); |
| } |
|
|
| |
| document.addEventListener('DOMContentLoaded', async () => { |
| initScene(document.getElementById('museum-canvas'), openDreamModal); |
|
|
| if (state.userId) _applyAuth(state.userId); |
|
|
| await _refreshMuseum(); |
|
|
| _initHUD(); |
| _initAuth(); |
| _initDrawCanvas(); |
| _initControls(); |
| _initModal(); |
| _initSaveBtns(); |
| }); |
|
|
| |
| async function _refreshMuseum() { |
| const data = await api('/gallery/public', { limit: 12, offset: 0 }); |
| if (data.ok) setPaintings(data.dreams); |
| } |
|
|
| |
| function _initHUD() { |
| document.getElementById('btn-create').addEventListener('click', () => _openPanel('create')); |
| document.getElementById('btn-my-gallery').addEventListener('click', () => _openPanel('gallery')); |
| document.getElementById('close-create').addEventListener('click', () => _openPanel(null)); |
| document.getElementById('close-gallery').addEventListener('click', () => _openPanel(null)); |
| } |
|
|
| function _openPanel(name) { |
| if (state.activePanel) { |
| document.getElementById(`panel-${state.activePanel}`).classList.remove('open'); |
| } |
| state.activePanel = name; |
| const canvas = document.getElementById('museum-canvas'); |
|
|
| if (name) { |
| const panel = document.getElementById(`panel-${name}`); |
| panel.classList.remove('hidden'); |
| requestAnimationFrame(() => panel.classList.add('open')); |
| canvas.classList.add('dimmed'); |
| document.getElementById('room-hint').classList.add('hidden'); |
| if (name === 'gallery') _loadUserGallery(); |
| } else { |
| canvas.classList.remove('dimmed'); |
| document.getElementById('room-hint').classList.remove('hidden'); |
| } |
| } |
|
|
| |
| document.addEventListener('transitionend', e => { |
| const el = e.target; |
| if (el.classList.contains('side-panel') && !el.classList.contains('open')) { |
| el.classList.add('hidden'); |
| } |
| }); |
|
|
| |
| function _initAuth() { |
| const input = document.getElementById('auth-input'); |
| const btn = document.getElementById('auth-btn'); |
| btn.addEventListener('click', () => { const v = input.value.trim(); if (v) _applyAuth(v); }); |
| input.addEventListener('keydown', e => { if (e.key === 'Enter') btn.click(); }); |
| } |
|
|
| function _applyAuth(name) { |
| state.userId = name; |
| localStorage.setItem('dm_user', name); |
|
|
| document.getElementById('auth-input').classList.add('hidden'); |
| document.getElementById('auth-btn').classList.add('hidden'); |
| const nameEl = document.getElementById('auth-name'); |
| nameEl.textContent = `β¦ ${name}`; |
| nameEl.classList.remove('hidden'); |
|
|
| _updateSaveArea(); |
| } |
|
|
| |
| function _initDrawCanvas() { |
| const canvas = document.getElementById('draw-canvas'); |
| const ctx = canvas.getContext('2d'); |
| ctx.fillStyle = '#0f0d1e'; |
| ctx.fillRect(0, 0, canvas.width, canvas.height); |
|
|
| let drawing = false, tool = 'pen', color = '#ffffff', size = 6; |
|
|
| |
| const preview = document.getElementById('brush-preview'); |
| function _updatePreview() { |
| preview.style.width = Math.min(size, 24) + 'px'; |
| preview.style.height = Math.min(size, 24) + 'px'; |
| preview.style.background = tool === 'eraser' ? 'rgba(255,255,255,.3)' : color; |
| } |
| _updatePreview(); |
|
|
| function pos(e) { |
| const r = canvas.getBoundingClientRect(); |
| const sx = canvas.width / r.width; |
| const sy = canvas.height / r.height; |
| const s = e.touches ? e.touches[0] : e; |
| return { x: (s.clientX - r.left) * sx, y: (s.clientY - r.top) * sy }; |
| } |
|
|
| function down(e) { |
| e.preventDefault(); |
| drawing = true; |
| const { x, y } = pos(e); |
| ctx.beginPath(); ctx.moveTo(x, y); |
| } |
|
|
| function move(e) { |
| e.preventDefault(); |
| if (!drawing) return; |
| const { x, y } = pos(e); |
| ctx.lineWidth = tool === 'eraser' ? size * 3 : size; |
| ctx.strokeStyle = tool === 'eraser' ? '#0f0d1e' : color; |
| ctx.lineCap = ctx.lineJoin = 'round'; |
| ctx.globalAlpha = 1; |
| ctx.lineTo(x, y); ctx.stroke(); |
| ctx.beginPath(); ctx.moveTo(x, y); |
| } |
|
|
| function up() { drawing = false; ctx.beginPath(); } |
|
|
| canvas.addEventListener('mousedown', down); |
| canvas.addEventListener('mousemove', move); |
| canvas.addEventListener('mouseup', up); |
| canvas.addEventListener('mouseleave', up); |
| canvas.addEventListener('touchstart', down, { passive: false }); |
| canvas.addEventListener('touchmove', move, { passive: false }); |
| canvas.addEventListener('touchend', up); |
|
|
| |
| document.getElementById('t-pen').addEventListener('click', () => { |
| tool = 'pen'; _setActiveTool('t-pen'); _updatePreview(); |
| }); |
| document.getElementById('t-eraser').addEventListener('click', () => { |
| tool = 'eraser'; _setActiveTool('t-eraser'); _updatePreview(); |
| }); |
|
|
| |
| document.getElementById('t-color').addEventListener('input', e => { |
| color = e.target.value; |
| tool = 'pen'; _setActiveTool('t-pen'); _updatePreview(); |
| }); |
|
|
| |
| document.querySelectorAll('.swatch').forEach(s => { |
| s.addEventListener('click', () => { |
| color = s.dataset.color; |
| document.getElementById('t-color').value = color; |
| tool = 'pen'; _setActiveTool('t-pen'); _updatePreview(); |
| }); |
| }); |
|
|
| |
| document.getElementById('t-size').addEventListener('input', e => { |
| size = +e.target.value; _updatePreview(); |
| }); |
|
|
| |
| document.getElementById('t-clear').addEventListener('click', () => { |
| ctx.fillStyle = '#0f0d1e'; |
| ctx.fillRect(0, 0, canvas.width, canvas.height); |
| }); |
| } |
|
|
| function _setActiveTool(id) { |
| document.querySelectorAll('.tool').forEach(t => t.classList.remove('active')); |
| document.getElementById(id).classList.add('active'); |
| } |
|
|
| |
| function _initControls() { |
| const slider = document.getElementById('strength'); |
| const valEl = document.getElementById('strength-val'); |
| slider.addEventListener('input', () => { valEl.textContent = slider.value; }); |
| document.getElementById('generate-btn').addEventListener('click', _generate); |
| } |
|
|
| async function _generate() { |
| const desc = document.getElementById('dream-desc').value.trim(); |
| if (!desc) { alert('Please describe your dream first.'); return; } |
|
|
| const canvas = document.getElementById('draw-canvas'); |
| const sketch = canvas.toDataURL('image/png'); |
| const strength = parseFloat(document.getElementById('strength').value); |
|
|
| const btn = document.getElementById('generate-btn'); |
| const label = document.getElementById('gen-label'); |
| const spin = document.getElementById('gen-spin'); |
| btn.disabled = true; |
| label.textContent = 'β¦ Materializingβ¦'; |
| spin.classList.remove('hidden'); |
|
|
| try { |
| const data = await api('/generate', { sketch_b64: sketch, description: desc, strength }); |
| if (!data.ok) { alert('Error: ' + data.error); return; } |
|
|
| state.lastSketch = sketch; |
| state.lastDream = 'data:image/png;base64,' + data.image; |
|
|
| document.getElementById('res-sketch').src = state.lastSketch; |
| document.getElementById('res-dream').src = state.lastDream; |
| document.getElementById('result-area').classList.remove('hidden'); |
| _updateSaveArea(); |
| } finally { |
| btn.disabled = false; |
| label.textContent = 'β¦ Materialize Dream'; |
| spin.classList.add('hidden'); |
| } |
| } |
|
|
| |
| function _initSaveBtns() { |
| document.getElementById('btn-publish').addEventListener('click', () => _save('public')); |
| document.getElementById('btn-private').addEventListener('click', () => _save('private')); |
| document.getElementById('btn-download').addEventListener('click', _download); |
| } |
|
|
| function _updateSaveArea() { |
| if (!state.lastDream) return; |
| document.getElementById('need-auth').classList.toggle('hidden', !!state.userId); |
| document.getElementById('save-btns').classList.toggle('hidden', !state.userId); |
| } |
|
|
| async function _save(visibility) { |
| const desc = document.getElementById('dream-desc').value.trim(); |
| const data = await api('/gallery/save', { |
| user_id: state.userId, |
| sketch_b64: state.lastSketch, |
| image_b64: state.lastDream, |
| description: desc, |
| visibility, |
| }); |
| if (data.ok) { |
| alert(visibility === 'public' |
| ? 'β Dream exhibited in the Museum.' |
| : 'Dream saved to your private gallery.'); |
| if (visibility === 'public') _refreshMuseum(); |
| } else { |
| alert('Could not save: ' + data.error); |
| } |
| } |
|
|
| function _download() { |
| if (!state.lastDream) return; |
| const a = document.createElement('a'); |
| a.href = state.lastDream; |
| a.download = 'sueno.png'; |
| a.click(); |
| } |
|
|
| |
| async function _loadUserGallery() { |
| const grid = document.getElementById('my-grid'); |
|
|
| if (!state.userId) { |
| grid.innerHTML = '<div class="empty-state">Enter your name to see your saved dreams.</div>'; |
| return; |
| } |
|
|
| document.getElementById('gallery-subtitle').textContent = `${state.userId}'s archive`; |
| grid.innerHTML = '<div class="empty-state">Loadingβ¦</div>'; |
|
|
| const data = await api('/gallery/user', { user_id: state.userId }); |
| if (!data.ok || !data.dreams.length) { |
| grid.innerHTML = '<div class="empty-state">No dreams saved yet.</div>'; |
| return; |
| } |
|
|
| grid.innerHTML = ''; |
| data.dreams.forEach(d => grid.appendChild(_makeCard(d, true))); |
| } |
|
|
| function _makeCard(dream, isOwner = false) { |
| const card = document.createElement('div'); |
| card.className = 'dream-card'; |
| card.innerHTML = ` |
| <div class="card-diptych"> |
| <img src="${dream.sketch_url}" loading="lazy" alt="boceto" /> |
| <img src="${dream.image_url}" loading="lazy" alt="sueΓ±o" /> |
| </div> |
| <div class="card-info"> |
| <div class="card-desc">${dream.description}</div> |
| <div class="card-vis">${dream.visibility === 'public' ? 'β Public' : 'β Private'}</div> |
| </div> |
| `; |
| card.addEventListener('click', () => openDreamModal(dream, isOwner)); |
| return card; |
| } |
|
|
| |
| function _initModal() { |
| document.querySelector('.modal-close').addEventListener('click', _closeModal); |
| document.querySelector('.modal-backdrop').addEventListener('click', _closeModal); |
| document.addEventListener('keydown', e => { if (e.key === 'Escape') _closeModal(); }); |
| } |
|
|
| function openDreamModal(dream, isOwner = false) { |
| document.getElementById('modal-sketch').src = dream.sketch_url; |
| document.getElementById('modal-dream').src = dream.image_url; |
| document.getElementById('modal-desc').textContent = `"${dream.description}"`; |
| document.getElementById('modal-meta').textContent = |
| `${dream.user_id} Β· ${dream.timestamp?.slice(0, 10) ?? ''}`; |
|
|
| const ownerArea = document.getElementById('modal-owner'); |
| const isMyDream = state.userId && dream.user_id === state.userId; |
| ownerArea.classList.toggle('hidden', !isMyDream); |
|
|
| if (isMyDream) { |
| const toggleBtn = document.getElementById('modal-toggle'); |
| toggleBtn.textContent = dream.visibility === 'public' ? 'Make private' : 'Exhibit publicly'; |
| toggleBtn.onclick = async () => { |
| const r = await api('/gallery/toggle', { dream_id: dream.id, user_id: state.userId }); |
| if (r.ok) { |
| dream.visibility = r.visibility; |
| toggleBtn.textContent = dream.visibility === 'public' ? 'Make private' : 'Exhibit publicly'; |
| _refreshMuseum(); |
| _loadUserGallery(); |
| } |
| }; |
| document.getElementById('modal-delete').onclick = async () => { |
| if (!confirm('Delete this dream permanently?')) return; |
| const r = await api('/gallery/delete', { dream_id: dream.id, user_id: state.userId }); |
| if (r.ok) { _closeModal(); _loadUserGallery(); _refreshMuseum(); } |
| }; |
| } |
|
|
| document.getElementById('dream-modal').classList.remove('hidden'); |
| } |
|
|
| function _closeModal() { |
| document.getElementById('dream-modal').classList.add('hidden'); |
| } |
|
|