diff --git a/public/app.js b/public/app.js index 5293338..16b1cc4 100644 --- a/public/app.js +++ b/public/app.js @@ -7601,13 +7601,335 @@ document.addEventListener('DOMContentLoaded', () => { let dragStartWidth, dragStartHeight; let activeAssetTab = 'postit'; - // --- ESTÚDIO DE CENAS EM CAMADAS --- + // --- ESTÚDIO DE CENAS EM CAMADAS (novo layer manager) --- const btnDecomposeScene = document.getElementById('btnDecomposeScene'); - const sceneDecomposePrompt = document.getElementById('sceneDecomposePrompt'); + const btnAddSceneLayer = document.getElementById('btnAddSceneLayer'); + const sceneLayerManagerList = document.getElementById('sceneLayerManagerList'); const sceneDecomposeLoader = document.getElementById('sceneDecomposeLoader'); const sceneDecomposeStatus = document.getElementById('sceneDecomposeStatus'); - const sceneLayersList = document.getElementById('sceneLayersList'); - const sceneLayersItems = document.getElementById('sceneLayersItems'); + + // Estado das camadas + let sceneLayers = []; + let sceneLayerCounter = 0; + + // Cria uma linha de camada no painel + function createSceneLayerRow(id, isBackground) { + const row = document.createElement('div'); + row.dataset.layerId = id; + row.style.cssText = `display:flex; align-items:center; gap:6px; padding:6px 8px; + background:rgba(255,255,255,0.04); border:1px solid rgba(139,92,246,0.2); + border-radius:8px; transition: border-color 0.2s;`; + + const badge = document.createElement('div'); + badge.style.cssText = `min-width:22px; height:22px; border-radius:50%; + background:${isBackground ? 'rgba(16,163,127,0.3)' : 'rgba(139,92,246,0.3)'}; + display:flex; align-items:center; justify-content:center; font-size:0.7rem; font-weight:700; + color:${isBackground ? '#10a37f' : '#a78bfa'}; flex-shrink:0;`; + badge.textContent = isBackground ? '🌄' : sceneLayers.filter(l => !l.isBackground).length; + + const input = document.createElement('input'); + input.type = 'text'; + input.placeholder = isBackground + ? 'Cenário de fundo (Ex: parque de diversões)' + : 'Elemento (Ex: menino de 6 anos)'; + input.style.cssText = `flex:1; background:transparent; border:none; outline:none; + color:var(--text-primary); font-size:0.82rem; padding:2px 0; min-width:0;`; + input.addEventListener('input', () => { + const layer = sceneLayers.find(l => l.id === id); + if (layer) layer.description = input.value; + }); + + // Botões de ação + const btnVisibility = document.createElement('button'); + btnVisibility.title = 'Ocultar/Mostrar camada no canvas'; + btnVisibility.innerHTML = '👁️'; + btnVisibility.style.cssText = `background:none; border:none; cursor:pointer; font-size:0.9rem; + padding:2px; opacity:1; transition:opacity 0.2s; flex-shrink:0;`; + let visible = true; + btnVisibility.addEventListener('click', () => { + const layer = sceneLayers.find(l => l.id === id); + if (!layer || !layer.canvasItem) return; + visible = !visible; + layer.visible = visible; + layer.canvasItem.style.display = visible ? '' : 'none'; + btnVisibility.style.opacity = visible ? '1' : '0.3'; + btnVisibility.title = visible ? 'Ocultar camada' : 'Mostrar camada'; + }); + + const btnPin = document.createElement('button'); + btnPin.title = 'Fixar posição (impede arrastar)'; + btnPin.innerHTML = '📌'; + btnPin.style.cssText = `background:none; border:none; cursor:pointer; font-size:0.9rem; + padding:2px; opacity:0.4; transition:opacity 0.2s; flex-shrink:0;`; + let pinned = false; + btnPin.addEventListener('click', () => { + const layer = sceneLayers.find(l => l.id === id); + if (!layer || !layer.canvasItem) return; + pinned = !pinned; + layer.pinned = pinned; + layer.canvasItem.style.pointerEvents = pinned ? 'none' : ''; + layer.canvasItem.style.cursor = pinned ? 'default' : 'move'; + btnPin.style.opacity = pinned ? '1' : '0.4'; + btnPin.title = pinned ? 'Desafixar posição' : 'Fixar posição'; + }); + + const btnDelete = document.createElement('button'); + btnDelete.title = 'Remover camada'; + btnDelete.innerHTML = '🗑️'; + btnDelete.style.cssText = `background:none; border:none; cursor:pointer; font-size:0.9rem; + padding:2px; opacity:0.6; transition:opacity 0.2s; flex-shrink:0;`; + btnDelete.addEventListener('mouseenter', () => btnDelete.style.opacity = '1'); + btnDelete.addEventListener('mouseleave', () => btnDelete.style.opacity = '0.6'); + btnDelete.addEventListener('click', () => { + // Remove do canvas se já gerado + const layer = sceneLayers.find(l => l.id === id); + if (layer?.canvasItem) layer.canvasItem.remove(); + // Remove do state + sceneLayers = sceneLayers.filter(l => l.id !== id); + // Remove do DOM + row.remove(); + // Reindexar badges dos elementos + reindexLayerBadges(); + }); + + row.appendChild(badge); + row.appendChild(input); + row.appendChild(btnVisibility); + row.appendChild(btnPin); + if (!isBackground) row.appendChild(btnDelete); // Fundo não pode ser deletado da lista + + return { row, input, badge }; + } + + function reindexLayerBadges() { + let elIdx = 0; + sceneLayers.forEach(l => { + if (!l.isBackground && l.rowBadge) { + l.rowBadge.textContent = ++elIdx; + } + }); + } + + function addSceneLayerEntry(isBackground = false) { + const id = `sl_${++sceneLayerCounter}`; + const layerObj = { id, description: '', isBackground, visible: true, pinned: false, canvasItem: null }; + sceneLayers.push(layerObj); + + const { row, input, badge } = createSceneLayerRow(id, isBackground); + layerObj.rowInput = input; + layerObj.rowBadge = badge; + if (sceneLayerManagerList) sceneLayerManagerList.appendChild(row); + input.focus(); + return layerObj; + } + + // Inicializa com camada de fundo padrão + addSceneLayerEntry(true); + + if (btnAddSceneLayer) { + btnAddSceneLayer.addEventListener('click', () => { + addSceneLayerEntry(false); + reindexLayerBadges(); + }); + } + + // ===== REMOÇÃO DE FUNDO BRANCO VIA CANVAS API ===== + async function removeWhiteBackground(imgUrl, threshold = 235) { + return new Promise((resolve) => { + const img = new Image(); + img.crossOrigin = 'anonymous'; + img.onload = () => { + const offscreen = document.createElement('canvas'); + offscreen.width = img.naturalWidth; + offscreen.height = img.naturalHeight; + const ctx = offscreen.getContext('2d', { willReadFrequently: true }); + ctx.drawImage(img, 0, 0); + + const imageData = ctx.getImageData(0, 0, offscreen.width, offscreen.height); + const data = imageData.data; + const softRange = 20; + + for (let i = 0; i < data.length; i += 4) { + const r = data[i], g = data[i + 1], b = data[i + 2]; + const minChannel = Math.min(r, g, b); + + if (minChannel >= threshold) { + data[i + 3] = 0; + } else if (minChannel >= threshold - softRange) { + const ratio = (minChannel - (threshold - softRange)) / softRange; + data[i + 3] = Math.round((1 - ratio) * data[i + 3]); + } + } + + ctx.putImageData(imageData, 0, 0); + resolve(offscreen.toDataURL('image/png')); + }; + img.onerror = () => resolve(imgUrl); + img.src = imgUrl; + }); + } + + // ===== ADICIONA ITEM DE CAMADA NO CANVAS ===== + function addSceneLayerToCanvas(layerData) { + if (!cartazPrintableArea) return null; + + const item = document.createElement('div'); + item.className = 'canvas-item canvas-scene-layer'; + item.setAttribute('data-layer-name', layerData.name || ''); + item.setAttribute('data-rotation', '0'); + item.style.cssText = `position:absolute; background:transparent; box-sizing:border-box;`; + item.style.zIndex = layerData.isBackground ? '0' : String(10 + (layerData.zOffset || 0)); + + // Cria a imagem SEM usar innerHTML (preserva o handle de resize) + const imgEl = document.createElement('img'); + imgEl.style.cssText = `width:100%; height:100%; object-fit:${layerData.isBackground ? 'cover' : 'contain'}; pointer-events:none; display:block;`; + imgEl.src = layerData.url; + item.appendChild(imgEl); + + // Handle de resize — criado uma única vez e permanece + const handle = document.createElement('div'); + handle.className = 'canvas-resize-handle'; + item.appendChild(handle); + + if (layerData.isBackground) { + item.style.left = '0'; + item.style.top = '0'; + item.style.width = '100%'; + item.style.height = '100%'; + item.title = '🖼️ Cenário de Fundo'; + } else { + item.style.left = (layerData.left || 10) + '%'; + item.style.top = (layerData.top || 10) + '%'; + item.style.width = (layerData.width || 30) + '%'; + item.style.height = (layerData.height || 30) + '%'; + item.title = '🎬 ' + (layerData.name || 'Elemento'); + + // Remove fundo branco: atualiza apenas o src da img (preserva o handle) + removeWhiteBackground(layerData.url).then(transparentUrl => { + imgEl.src = transparentUrl; + }); + } + + cartazPrintableArea.appendChild(item); + setupCanvasItemEvents(item); + return item; + } + + // ===== BOTÃO CRIAR CENA ===== + if (btnDecomposeScene) { + btnDecomposeScene.addEventListener('click', async () => { + // Validar que há pelo menos fundo + 1 elemento com descrição + const bgLayer = sceneLayers.find(l => l.isBackground); + if (!bgLayer?.description?.trim()) { + showToast('Descreva o cenário de fundo (Camada 🌄)!', 'warning'); + if (bgLayer?.rowInput) bgLayer.rowInput.focus(); + return; + } + const elementLayers = sceneLayers.filter(l => !l.isBackground && l.description?.trim()); + if (elementLayers.length === 0) { + showToast('Adicione pelo menos um elemento (➕ Adicionar Camada)!', 'warning'); + return; + } + + if (!canvasModeActive) { + if (cartazEditArea && cartazEditArea.style.display !== 'none') { + btnEditCartazToggle && btnEditCartazToggle.click(); + } + enterCanvasMode(); + } + + btnDecomposeScene.disabled = true; + btnDecomposeScene.style.opacity = '0.6'; + if (sceneDecomposeLoader) sceneDecomposeLoader.style.display = 'flex'; + + // Remover camadas anteriores do canvas + cartazPrintableArea.querySelectorAll('.canvas-scene-layer').forEach(el => el.remove()); + sceneLayers.forEach(l => { l.canvasItem = null; }); + + // Gerar cada camada individualmente (uma por uma para acompanhar progresso) + const totalLayers = 1 + elementLayers.length; // fundo + elementos + let done = 0; + + const updateStatus = () => { + done++; + if (sceneDecomposeStatus) sceneDecomposeStatus.textContent = `Gerando camada ${done}/${totalLayers}...`; + }; + + try { + const titulo = activeCartazData?.titulo || ''; + const token = localStorage.getItem('token'); + + // Gera todas as imagens em paralelo via API (usando os prompts diretos das camadas) + const allPrompts = [ + { isBackground: true, description: bgLayer.description, layerRef: bgLayer }, + ...elementLayers.map(l => ({ isBackground: false, description: l.description, layerRef: l })) + ]; + + if (sceneDecomposeStatus) sceneDecomposeStatus.textContent = `Enviando para a IA...`; + + const response = await fetch('/api/cartazes/decompose-scene', { + method: 'POST', + headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${token}` }, + body: JSON.stringify({ + titulo, + // Mandamos a estrutura de camadas diretamente (novo campo layers_manual) + layers_manual: allPrompts.map(p => ({ + isBackground: p.isBackground, + description: p.description + })) + }) + }); + + if (!response.ok) { + const errData = await response.json().catch(() => ({})); + throw new Error(errData.error || 'Erro na geração da cena.'); + } + + const data = await response.json(); + if (!data.success) throw new Error('Resposta inválida do servidor.'); + + if (sceneDecomposeStatus) sceneDecomposeStatus.textContent = 'Montando as camadas no canvas...'; + + // Fundo + if (data.backgroundUrl && bgLayer) { + bgLayer.canvasItem = addSceneLayerToCanvas({ + url: data.backgroundUrl, name: bgLayer.description, isBackground: true + }); + } + + // Elementos + const spread = [ + { left: 8, top: 32 }, { left: 42, top: 38 }, + { left: 64, top: 28 }, { left: 22, top: 56 } + ]; + if (Array.isArray(data.layers)) { + data.layers.forEach((layer, idx) => { + if (!layer.url) return; + const elLayerRef = elementLayers[idx]; + const pos = spread[idx % spread.length]; + const canvasItem = addSceneLayerToCanvas({ + url: layer.url, + name: layer.name || elLayerRef?.description || `Elemento ${idx + 1}`, + isBackground: false, + left: pos.left, top: pos.top, width: 30, height: 30, + zOffset: idx + 1 + }); + if (elLayerRef) elLayerRef.canvasItem = canvasItem; + }); + } + + showToast('🎬 Cena montada! Use os botões 👁️ 📌 🗑️ para controlar cada camada.', 'success'); + + } catch (err) { + console.error('[Cenas em Camadas]', err); + showToast('Erro ao criar a cena: ' + err.message, 'error'); + } finally { + btnDecomposeScene.disabled = false; + btnDecomposeScene.style.opacity = '1'; + if (sceneDecomposeLoader) sceneDecomposeLoader.style.display = 'none'; + } + }); + } // Cria um item de camada de imagem direto no canvas (background ou sticker) // Remoção de fundo branco via Canvas API — gera PNG com transparência real diff --git a/public/index.html b/public/index.html index 340afd6..dacca9a 100644 --- a/public/index.html +++ b/public/index.html @@ -1885,17 +1885,31 @@