dream-museum / static /app.js
AitorConS's picture
Initial Gradio Space β€” Dream Museum
b1deecb verified
Raw
History Blame Contribute Delete
14.3 kB
import { init as initScene, setPaintings } from '/static/scene.js';
// ── State ─────────────────────────────────────────────────────────────────────
const state = {
userId: localStorage.getItem('dm_user') || '',
lastSketch: null,
lastDream: null,
activePanel: null,
};
// ── API helper ────────────────────────────────────────────────────────────────
async function api(path, body) {
const r = await fetch(path, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(body),
});
return r.json();
}
// ── Boot ──────────────────────────────────────────────────────────────────────
document.addEventListener('DOMContentLoaded', async () => {
initScene(document.getElementById('museum-canvas'), openDreamModal);
if (state.userId) _applyAuth(state.userId);
await _refreshMuseum();
_initHUD();
_initAuth();
_initDrawCanvas();
_initControls();
_initModal();
_initSaveBtns();
});
// ── Museum ────────────────────────────────────────────────────────────────────
async function _refreshMuseum() {
const data = await api('/gallery/public', { limit: 12, offset: 0 });
if (data.ok) setPaintings(data.dreams);
}
// ── HUD / panels ──────────────────────────────────────────────────────────────
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');
}
}
// Hide panel after slide-out transition ends
document.addEventListener('transitionend', e => {
const el = e.target;
if (el.classList.contains('side-panel') && !el.classList.contains('open')) {
el.classList.add('hidden');
}
});
// ── Auth ──────────────────────────────────────────────────────────────────────
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();
}
// ── Drawing canvas ─────────────────────────────────────────────────────────────
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;
// Brush preview dot
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);
// Tool buttons
document.getElementById('t-pen').addEventListener('click', () => {
tool = 'pen'; _setActiveTool('t-pen'); _updatePreview();
});
document.getElementById('t-eraser').addEventListener('click', () => {
tool = 'eraser'; _setActiveTool('t-eraser'); _updatePreview();
});
// Color picker
document.getElementById('t-color').addEventListener('input', e => {
color = e.target.value;
tool = 'pen'; _setActiveTool('t-pen'); _updatePreview();
});
// Color swatches
document.querySelectorAll('.swatch').forEach(s => {
s.addEventListener('click', () => {
color = s.dataset.color;
document.getElementById('t-color').value = color;
tool = 'pen'; _setActiveTool('t-pen'); _updatePreview();
});
});
// Size slider
document.getElementById('t-size').addEventListener('input', e => {
size = +e.target.value; _updatePreview();
});
// Clear
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');
}
// ── Generation ─────────────────────────────────────────────────────────────────
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');
}
}
// ── Save buttons ───────────────────────────────────────────────────────────────
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();
}
// ── My Gallery ─────────────────────────────────────────────────────────────────
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;
}
// ── Dream modal ────────────────────────────────────────────────────────────────
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');
}