From e17e358e35e33bf6a4eedb75d65b7bd479d88172 Mon Sep 17 00:00:00 2001 From: admtracksteel Date: Thu, 3 Sep 2026 11:28:55 +0000 Subject: [PATCH] =?UTF-8?q?Ajuste=20de=20Formas=20(Espessura,=20Cor=20de?= =?UTF-8?q?=20Linha=20e=20Preenchimento),=20Al=C3=A7a=20de=20Redimensionam?= =?UTF-8?q?ento=20e=20Ordenamento=20de=20Camadas=20(Z-Index)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- public/app.js | 991 +++++++++++++++++++++------------------------- public/index.html | 63 ++- public/style.css | 45 ++- 3 files changed, 530 insertions(+), 569 deletions(-) diff --git a/public/app.js b/public/app.js index d82775d..f5dfa88 100644 --- a/public/app.js +++ b/public/app.js @@ -10046,14 +10046,21 @@ document.addEventListener('DOMContentLoaded', () => { const canvasFontFamily = document.getElementById('canvasFontFamily'); const canvasFontSize = document.getElementById('canvasFontSize'); const canvasTextColor = document.getElementById('canvasTextColor'); + const canvasBorderColor = document.getElementById('canvasBorderColor'); + const canvasBorderWidth = document.getElementById('canvasBorderWidth'); const canvasBgColorInput = document.getElementById('canvasBgColor'); + const btnCanvasBgTransparent = document.getElementById('btnCanvasBgTransparent'); const canvasRotation = document.getElementById('canvasRotation'); const canvasOpacity = document.getElementById('canvasOpacity'); + + const btnCanvasZIndexFront = document.getElementById('btnCanvasZIndexFront'); const btnCanvasZIndexUp = document.getElementById('btnCanvasZIndexUp'); const btnCanvasZIndexDown = document.getElementById('btnCanvasZIndexDown'); + const btnCanvasZIndexBack = document.getElementById('btnCanvasZIndexBack'); + const canvasItemZIndexBadge = document.getElementById('canvasItemZIndexBadge'); const btnCanvasDelete = document.getElementById('btnCanvasDelete'); - // Novos controles do Painel de Assets Lateral + // Controles do Painel de Assets Lateral const canvasAssetSearch = document.getElementById('canvasAssetSearch'); const canvasAssetsContainer = document.getElementById('canvasAssetsContainer'); const btnTabAssetPostits = document.getElementById('btnTabAssetPostits'); @@ -10064,42 +10071,51 @@ document.addEventListener('DOMContentLoaded', () => { let selectedCanvasItem = null; let isDraggingCanvas = false; let isResizingCanvas = false; - let dragStartX, dragStartY; - let dragStartLeft, dragStartTop; - let dragStartWidth, dragStartHeight; + let dragStartX = 0, dragStartY = 0; + let dragStartLeft = 0, dragStartTop = 0; + let dragStartWidth = 0, dragStartHeight = 0; let activeAssetTab = 'postit'; - // --- ESTÚDIO DE CENAS EM CAMADAS (novo layer manager) --- + // --- ESTÚDIO DE CENAS EM CAMADAS (LAYER MANAGER) --- const btnDecomposeScene = document.getElementById('btnDecomposeScene'); const btnAddSceneLayer = document.getElementById('btnAddSceneLayer'); const sceneLayerManagerList = document.getElementById('sceneLayerManagerList'); const sceneDecomposeLoader = document.getElementById('sceneDecomposeLoader'); const sceneDecomposeStatus = document.getElementById('sceneDecomposeStatus'); - // Estado das camadas let sceneLayers = []; let sceneLayerCounter = 0; - // Cria uma linha de camada no painel + // Cria uma linha de camada no painel lateral 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;`; + background:rgba(255,255,255,0.04); border:1px solid rgba(139,92,246,0.25); + border-radius:8px; transition: border-color 0.2s, background 0.2s; cursor:pointer;`; + + // Clique na linha foca/seleciona o elemento no canvas + row.addEventListener('click', (e) => { + if (e.target.tagName === 'INPUT' || e.target.tagName === 'BUTTON') return; + const layer = sceneLayers.find(l => l.id === id); + if (layer && layer.canvasItem) { + layer.canvasItem.click(); + layer.canvasItem.scrollIntoView({ behavior: 'smooth', block: 'nearest' }); + } + }); 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.style.cssText = `min-width:24px; height:24px; border-radius:50%; + background:${isBackground ? 'rgba(16,185,129,0.25)' : 'rgba(139,92,246,0.25)'}; + display:flex; align-items:center; justify-content:center; font-size:0.72rem; font-weight:700; + color:${isBackground ? '#34d399' : '#c4b5fd'}; 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)'; + ? 'Cenário de fundo (Ex: sala de aula acolhedora)' + : 'Elemento (Ex: professora com 3 crianças)'; 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', () => { @@ -10107,68 +10123,128 @@ document.addEventListener('DOMContentLoaded', () => { if (layer) layer.description = input.value; }); - // Botões de ação + // Botões de reordenação de camada (Mover para Cima / Baixo) + const btnUp = document.createElement('button'); + btnUp.title = 'Trazer camada para a frente (subir na ordem)'; + btnUp.innerHTML = '▲'; + btnUp.style.cssText = `background:none; border:none; cursor:pointer; font-size:0.72rem; color:var(--text-secondary); padding:2px 4px; flex-shrink:0; border-radius:4px;`; + btnUp.addEventListener('click', (e) => { + e.stopPropagation(); + reorderSceneLayer(id, -1); + }); + + const btnDown = document.createElement('button'); + btnDown.title = 'Enviar camada para trás (descer na ordem)'; + btnDown.innerHTML = '▼'; + btnDown.style.cssText = `background:none; border:none; cursor:pointer; font-size:0.72rem; color:var(--text-secondary); padding:2px 4px; flex-shrink:0; border-radius:4px;`; + btnDown.addEventListener('click', (e) => { + e.stopPropagation(); + reorderSceneLayer(id, 1); + }); + + // Botão Visibilidade (Olho) const btnVisibility = document.createElement('button'); - btnVisibility.title = 'Ocultar/Mostrar camada no canvas'; + 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;`; + btnVisibility.style.cssText = `background:none; border:none; cursor:pointer; font-size:0.85rem; padding:2px; flex-shrink:0; transition:opacity 0.2s;`; let visible = true; - btnVisibility.addEventListener('click', () => { + btnVisibility.addEventListener('click', (e) => { + e.stopPropagation(); 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.style.opacity = visible ? '1' : '0.35'; btnVisibility.title = visible ? 'Ocultar camada' : 'Mostrar camada'; + showToast(visible ? `Camada visível.` : `Camada ocultada.`, 'info'); }); + // Botão Fixar / Desafixar posição const btnPin = document.createElement('button'); - btnPin.title = 'Fixar posição (impede arrastar)'; + btnPin.title = 'Fixar posição da camada'; 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;`; + btnPin.style.cssText = `background:none; border:none; cursor:pointer; font-size:0.85rem; padding:2px; opacity:0.4; flex-shrink:0; transition:opacity 0.2s;`; let pinned = false; - btnPin.addEventListener('click', () => { + btnPin.addEventListener('click', (e) => { + e.stopPropagation(); 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'; }); + // Botão Excluir 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 + btnDelete.style.cssText = `background:none; border:none; cursor:pointer; font-size:0.85rem; padding:2px; opacity:0.6; flex-shrink:0;`; + btnDelete.addEventListener('click', (e) => { + e.stopPropagation(); 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(); + syncSceneLayersZIndex(); }); row.appendChild(badge); row.appendChild(input); + if (!isBackground) { + row.appendChild(btnUp); + row.appendChild(btnDown); + } row.appendChild(btnVisibility); row.appendChild(btnPin); - if (!isBackground) row.appendChild(btnDelete); // Fundo não pode ser deletado da lista + if (!isBackground) row.appendChild(btnDelete); return { row, input, badge }; } + function reorderSceneLayer(id, direction) { + const idx = sceneLayers.findIndex(l => l.id === id); + if (idx === -1) return; + const targetIdx = idx + direction; + // O fundo sempre permanece no índice 0 + if (targetIdx < 1 || targetIdx >= sceneLayers.length) return; + + const [moved] = sceneLayers.splice(idx, 1); + sceneLayers.splice(targetIdx, 0, moved); + + // Reordenar no DOM do painel lateral + if (sceneLayerManagerList) { + sceneLayerManagerList.innerHTML = ''; + sceneLayers.forEach(l => { + const { row, input, badge } = createSceneLayerRow(l.id, l.isBackground); + input.value = l.description || ''; + l.rowInput = input; + l.rowBadge = badge; + sceneLayerManagerList.appendChild(row); + }); + } + + reindexLayerBadges(); + syncSceneLayersZIndex(); + } + + function syncSceneLayersZIndex() { + let z = 10; + sceneLayers.forEach(l => { + if (l.isBackground) { + if (l.canvasItem) l.canvasItem.style.zIndex = '1'; + } else { + if (l.canvasItem) { + l.canvasItem.style.zIndex = String(z++); + } + } + }); + if (selectedCanvasItem) updateToolbarForSelected(); + } + function reindexLayerBadges() { let elIdx = 0; sceneLayers.forEach(l => { @@ -10201,7 +10277,7 @@ document.addEventListener('DOMContentLoaded', () => { }); } - // ===== REMOÇÃO DE FUNDO BRANCO VIA CANVAS API ===== + // Remoção de fundo branco via Canvas API (PNG com transparência real) async function removeWhiteBackground(imgUrl, threshold = 235) { return new Promise((resolve) => { const img = new Image(); @@ -10237,7 +10313,7 @@ document.addEventListener('DOMContentLoaded', () => { }); } - // ===== ADICIONA ITEM DE CAMADA NO CANVAS ===== + // ADICIONA ITEM DE CAMADA NO CANVAS (Preservando alça de resize e eventos) function addSceneLayerToCanvas(layerData) { if (!cartazPrintableArea) return null; @@ -10245,34 +10321,39 @@ document.addEventListener('DOMContentLoaded', () => { 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)); + item.setAttribute('data-is-bg', layerData.isBackground ? 'true' : 'false'); + item.style.position = 'absolute'; + item.style.background = 'transparent'; + item.style.boxSizing = 'border-box'; - // Cria a imagem SEM usar innerHTML (preserva o handle de resize) + // Fundo fica na camada 1; elementos ficam de 10 em diante + item.style.zIndex = layerData.isBackground ? '1' : String(10 + (layerData.zOffset || 0)); + + // Elemento de imagem com transparência 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 + // Alça de redimensionamento (Bolinha azul no canto inferior direito) 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.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.style.left = (layerData.left || 15) + '%'; + item.style.top = (layerData.top || 20) + '%'; + item.style.width = (layerData.width || 32) + '%'; + item.style.height = (layerData.height || 32) + '%'; item.title = '🎬 ' + (layerData.name || 'Elemento'); - // Remove fundo branco: atualiza apenas o src da img (preserva o handle) + // Atualiza apenas a imagem de forma assíncrona SEM recriar o HTML ou destruir o handle removeWhiteBackground(layerData.url).then(transparentUrl => { imgEl.src = transparentUrl; }); @@ -10283,10 +10364,9 @@ document.addEventListener('DOMContentLoaded', () => { return item; } - // ===== BOTÃO CRIAR CENA ===== + // BOTÃO CRIAR CENA (IA) 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'); @@ -10310,37 +10390,26 @@ document.addEventListener('DOMContentLoaded', () => { btnDecomposeScene.style.opacity = '0.6'; if (sceneDecomposeLoader) sceneDecomposeLoader.style.display = 'flex'; - // Remover camadas anteriores do canvas + // Limpar camadas anteriores de cena 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 titulo = cartazTituloInput?.value || 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...`; + if (sceneDecomposeStatus) sceneDecomposeStatus.textContent = 'Gerando elementos da cena com 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 @@ -10358,17 +10427,17 @@ document.addEventListener('DOMContentLoaded', () => { if (sceneDecomposeStatus) sceneDecomposeStatus.textContent = 'Montando as camadas no canvas...'; - // Fundo + // Montar Fundo if (data.backgroundUrl && bgLayer) { bgLayer.canvasItem = addSceneLayerToCanvas({ url: data.backgroundUrl, name: bgLayer.description, isBackground: true }); } - // Elementos + // Montar Elementos individuais const spread = [ - { left: 8, top: 32 }, { left: 42, top: 38 }, - { left: 64, top: 28 }, { left: 22, top: 56 } + { left: 10, top: 30 }, { left: 45, top: 35 }, + { left: 65, top: 25 }, { left: 25, top: 55 } ]; if (Array.isArray(data.layers)) { data.layers.forEach((layer, idx) => { @@ -10379,14 +10448,14 @@ document.addEventListener('DOMContentLoaded', () => { url: layer.url, name: layer.name || elLayerRef?.description || `Elemento ${idx + 1}`, isBackground: false, - left: pos.left, top: pos.top, width: 30, height: 30, + left: pos.left, top: pos.top, width: 32, height: 32, zOffset: idx + 1 }); if (elLayerRef) elLayerRef.canvasItem = canvasItem; }); } - showToast('🎬 Cena montada! Use os botões 👁️ 📌 🗑️ para controlar cada camada.', 'success'); + showToast('🎬 Cena montada com sucesso! Arraste e redimensione os elementos livremente.', 'success'); } catch (err) { console.error('[Cenas em Camadas]', err); @@ -10399,252 +10468,69 @@ document.addEventListener('DOMContentLoaded', () => { }); } - // 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 - async function removeWhiteBackground(imgUrl, threshold = 235) { - return new Promise((resolve) => { - const img = new Image(); - img.crossOrigin = 'anonymous'; // necessário para getImageData funcionar - 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; - // threshold: pixels com R,G,B >= threshold são considerados "fundos a remover" - // softRange: faixa de suavização para bordas anti-aliased - const softRange = 20; - - for (let i = 0; i < data.length; i += 4) { - const r = data[i]; - const g = data[i + 1]; - const b = data[i + 2]; - - // Pega o mínimo dos canais (garante que só pixels "esbranquiçados" sejam afetados) - const minChannel = Math.min(r, g, b); - - if (minChannel >= threshold) { - // Área central do branco: totalmente transparente - data[i + 3] = 0; - } else if (minChannel >= threshold - softRange) { - // Zona de borda: transição linear suave para preservar contornos - const ratio = (minChannel - (threshold - softRange)) / softRange; - data[i + 3] = Math.round((1 - ratio) * data[i + 3]); - } - // Pixels abaixo do threshold - softRange: mantém opacidade original - } - - ctx.putImageData(imageData, 0, 0); - resolve(offscreen.toDataURL('image/png')); - }; - img.onerror = () => { - console.warn('[removeWhiteBackground] Falha ao carregar imagem, usando original:', imgUrl); - resolve(imgUrl); - }; - img.src = imgUrl; - }); + // --- GERENCIAMENTO DE CAMADAS E PROFUNDIDADE (Z-INDEX) --- + function getAllCanvasItems() { + if (!cartazPrintableArea) return []; + return Array.from(cartazPrintableArea.querySelectorAll('.canvas-item')); } - function addSceneLayerToCanvas(layerData) { - if (!canvasModeActive || !cartazPrintableArea) return; + function getSortedZIndexItems() { + const items = getAllCanvasItems(); + return items.map(el => ({ + el, + z: parseInt(el.style.zIndex) || 1 + })).sort((a, b) => a.z - b.z); + } - 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.position = 'absolute'; - item.style.left = (layerData.left || 10) + '%'; - item.style.top = (layerData.top || 10) + '%'; - item.style.width = (layerData.width || 35) + '%'; - item.style.height = (layerData.height || 35) + '%'; - item.style.zIndex = layerData.isBackground ? '0' : String(10 + (layerData.zOffset || 0)); - item.style.background = 'transparent'; - - if (layerData.isBackground) { - item.style.left = '0'; - item.style.top = '0'; - item.style.width = '100%'; - item.style.height = '100%'; - item.style.zIndex = '0'; - item.style.pointerEvents = 'none'; - item.innerHTML = ``; - item.title = '🖼️ Cenário de Fundo'; - - const handle = document.createElement('div'); - handle.className = 'canvas-resize-handle'; - item.appendChild(handle); - cartazPrintableArea.appendChild(item); - item.style.pointerEvents = 'auto'; - setupCanvasItemEvents(item); + function bringCanvasItemToFront(item) { + if (!item) return; + const sorted = getSortedZIndexItems(); + const maxZ = sorted.length > 0 ? Math.max(...sorted.map(s => s.z)) : 1; + const newZ = Math.max(maxZ + 1, 35); + item.style.zIndex = String(newZ); + updateToolbarForSelected(); + showToast(`Elemento trazido para a frente de tudo (Nível ${newZ})`, 'info'); + } + function sendCanvasItemToBack(item) { + if (!item) return; + if (item.getAttribute('data-is-bg') === 'true') { + item.style.zIndex = '1'; } else { - // Adiciona o item ao canvas com um placeholder de loading - item.innerHTML = `
- -
`; - item.title = '🎬 ' + (layerData.name || 'Elemento'); - - const handle = document.createElement('div'); - handle.className = 'canvas-resize-handle'; - item.appendChild(handle); - cartazPrintableArea.appendChild(item); - setupCanvasItemEvents(item); - - // Remove o fundo branco de forma assíncrona via Canvas API - removeWhiteBackground(layerData.url).then(transparentDataUrl => { - item.innerHTML = ``; - const newHandle = document.createElement('div'); - newHandle.className = 'canvas-resize-handle'; - item.appendChild(newHandle); - }); + item.style.zIndex = '2'; // Diretamente acima do fundo } - - return item; + updateToolbarForSelected(); + showToast(`Elemento enviado para trás (Nível ${item.style.zIndex})`, 'info'); } - // Renderiza a lista de camadas no painel lateral - function renderSceneLayers(layers) { - if (!sceneLayersItems) return; - sceneLayersItems.innerHTML = ''; - - const allLayers = [{ name: '🖼️ Cenário de Fundo', isBackground: true }, ...layers.map(l => ({ ...l }))]; - allLayers.forEach((layer, index) => { - const chip = document.createElement('div'); - chip.style.cssText = `display:flex; align-items:center; gap:8px; padding:5px 8px; - background:rgba(255,255,255,0.05); border:1px solid rgba(139,92,246,0.2); - border-radius:6px; font-size:0.78rem; color:var(--text-primary); cursor:pointer;`; - chip.innerHTML = ` - ${layer.isBackground ? '🌄' : '✨'} - ${layer.name} - Camada ${index} - `; - chip.title = `${layer.isBackground ? 'Fundo da cena' : 'Elemento: ' + layer.name}`; - sceneLayersItems.appendChild(chip); - }); + function stepCanvasItemZIndexUp(item) { + if (!item) return; + const sorted = getSortedZIndexItems(); + const currentZ = parseInt(item.style.zIndex) || 1; + const higher = sorted.find(i => i.z > currentZ && i.el !== item); + const newZ = higher ? higher.z + 1 : currentZ + 1; + item.style.zIndex = String(newZ); + updateToolbarForSelected(); + showToast(`Nível avançado para ${newZ}`, 'info'); } - if (btnDecomposeScene) { - btnDecomposeScene.addEventListener('click', async () => { - const prompt = sceneDecomposePrompt ? sceneDecomposePrompt.value.trim() : ''; - if (!prompt) { - showToast('Descreva a cena antes de gerar!', 'warning'); - return; - } - - // Garantir que o modo canvas está ativo - if (!canvasModeActive) { - if (cartazEditArea && cartazEditArea.style.display !== 'none') { - btnEditCartazToggle && btnEditCartazToggle.click(); - } - enterCanvasMode(); - } - - // Mostrar loading - btnDecomposeScene.disabled = true; - btnDecomposeScene.style.opacity = '0.6'; - if (sceneDecomposeLoader) { - sceneDecomposeLoader.style.display = 'flex'; - } - if (sceneLayersList) sceneLayersList.style.display = 'none'; - - const statusSteps = [ - 'Planejando a cena com IA...', - 'Identificando elementos e cenário...', - 'Gerando imagem de fundo...', - 'Gerando elementos individuais...', - 'Montando as camadas no canvas...' - ]; - let stepIdx = 0; - const statusInterval = setInterval(() => { - if (sceneDecomposeStatus && stepIdx < statusSteps.length) { - sceneDecomposeStatus.textContent = statusSteps[stepIdx++]; - } - }, 3500); - - try { - const titulo = activeCartazData?.titulo || ''; - const response = await fetch('/api/cartazes/decompose-scene', { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - 'Authorization': `Bearer ${localStorage.getItem('token')}` - }, - body: JSON.stringify({ tema: prompt, titulo }) - }); - - clearInterval(statusInterval); - - 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...'; - - // Limpar canvas antes de montar (remover outros items de cena se houver) - const oldLayers = cartazPrintableArea.querySelectorAll('.canvas-scene-layer'); - oldLayers.forEach(l => l.remove()); - - // Adicionar background - if (data.backgroundUrl) { - addSceneLayerToCanvas({ url: data.backgroundUrl, name: 'Cenário de Fundo', isBackground: true }); - } - - // Adicionar cada elemento com posição escalonada - if (Array.isArray(data.layers)) { - data.layers.forEach((layer, idx) => { - if (layer.url) { - const spread = [ - { left: 10, top: 30 }, - { left: 38, top: 35 }, - { left: 62, top: 28 }, - { left: 25, top: 55 } - ]; - const pos = spread[idx % spread.length]; - addSceneLayerToCanvas({ - url: layer.url, - name: layer.name || `Elemento ${idx + 1}`, - isBackground: false, - left: pos.left, - top: pos.top, - width: 28, - height: 28, - zOffset: idx + 1 - }); - } - }); - - // Renderizar lista de camadas no painel - renderSceneLayers(data.layers); - if (sceneLayersList) sceneLayersList.style.display = 'flex'; - } - - showToast('🎬 Cena montada em camadas! Clique nos elementos para mover, girar e redimensionar.', 'success'); - - } catch (err) { - clearInterval(statusInterval); - console.error('[Decompose Scene]', err); - showToast('Erro ao criar a cena em camadas: ' + err.message, 'error'); - } finally { - btnDecomposeScene.disabled = false; - btnDecomposeScene.style.opacity = '1'; - if (sceneDecomposeLoader) sceneDecomposeLoader.style.display = 'none'; - } - }); + function stepCanvasItemZIndexDown(item) { + if (!item) return; + const sorted = getSortedZIndexItems().reverse(); + const currentZ = parseInt(item.style.zIndex) || 1; + const minAllowed = item.getAttribute('data-is-bg') === 'true' ? 1 : 2; + const lower = sorted.find(i => i.z < currentZ && i.el !== item); + const newZ = lower ? Math.max(minAllowed, lower.z - 1) : Math.max(minAllowed, currentZ - 1); + item.style.zIndex = String(newZ); + updateToolbarForSelected(); + showToast(`Nível recuado para ${newZ}`, 'info'); } + // --- ATIVAR / DESATIVAR MODO CANVAS --- function enterCanvasMode() { canvasModeActive = true; if (btnCanvasModeToggle) { - btnCanvasModeToggle.textContent = '💾 Salvar Canvas'; + btnCanvasModeToggle.textContent = '💾 Concluir Design'; btnCanvasModeToggle.style.background = '#10a37f'; btnCanvasModeToggle.style.color = 'white'; } @@ -10656,22 +10542,20 @@ document.addEventListener('DOMContentLoaded', () => { if (configPanel) configPanel.style.display = 'none'; if (canvasPanel) canvasPanel.style.display = 'flex'; - // Iniciar na aba correspondente de assets selectAssetTab(activeAssetTab); cartazPrintableArea.style.position = 'relative'; const rectParent = cartazPrintableArea.getBoundingClientRect(); const items = []; + if (cartazPreviewBadge && cartazPreviewBadge.style.display !== 'none') items.push(cartazPreviewBadge); if (cartazPreviewTitle) items.push(cartazPreviewTitle); if (cartazPreviewImageContainer && cartazPreviewImageContainer.style.display !== 'none') { items.push(cartazPreviewImageContainer); } const cards = Array.from(cartazPreviewContent.children); - cards.forEach(card => { - items.push(card); - }); + cards.forEach(card => items.push(card)); const clientRects = items.map(item => item.getBoundingClientRect()); @@ -10685,12 +10569,15 @@ document.addEventListener('DOMContentLoaded', () => { item.classList.add('canvas-item'); item.style.position = 'absolute'; - item.style.left = `${leftPercent}%`; - item.style.top = `${topPercent}%`; - item.style.width = `${widthPercent}%`; - item.style.height = `${heightPercent}%`; + item.style.left = `${Math.max(0, leftPercent).toFixed(2)}%`; + item.style.top = `${Math.max(0, topPercent).toFixed(2)}%`; + item.style.width = `${Math.min(100, widthPercent).toFixed(2)}%`; + item.style.height = `${Math.min(100, heightPercent).toFixed(2)}%`; item.style.margin = '0'; + // CRÍTICO: Textos, títulos e cartões iniciam na frente do fundo (1) e das camadas (10+) + item.style.zIndex = String(30 + index); + item.setAttribute('data-rotation', '0'); item.setAttribute('contenteditable', 'true'); if (!item.querySelector('.canvas-resize-handle')) { @@ -10707,19 +10594,18 @@ document.addEventListener('DOMContentLoaded', () => { }); cartazPreviewContent.style.display = 'none'; - showToast('Modo Design Canvas ativado! Adicione elementos e ajuste o mural livremente.', 'info'); + showToast('Modo Design Canvas ativado! Clique nos itens para editar, mover ou redimensionar.', 'info'); } function exitCanvasMode() { canvasModeActive = false; if (btnCanvasModeToggle) { - btnCanvasModeToggle.textContent = '🎨 Design Canvas'; + btnCanvasModeToggle.textContent = '🎨 Canvas'; btnCanvasModeToggle.style.background = 'transparent'; btnCanvasModeToggle.style.color = 'var(--text-secondary)'; } if (canvasControlPanel) canvasControlPanel.style.display = 'none'; - // Alternar painéis de volta const configPanel = document.getElementById('cartazLeftConfigPanel'); const canvasPanel = document.getElementById('cartazLeftCanvasPanel'); if (configPanel) configPanel.style.display = 'flex'; @@ -10735,92 +10621,52 @@ document.addEventListener('DOMContentLoaded', () => { item.removeAttribute('contenteditable'); }); - // Limpar o HTML para persistência (remover seleções/handles) - const tempContainer = document.createElement('div'); - tempContainer.innerHTML = cartazPrintableArea.innerHTML; - tempContainer.querySelectorAll('.canvas-resize-handle').forEach(h => h.remove()); - tempContainer.querySelectorAll('.canvas-item').forEach(el => { - el.classList.remove('active-item'); - el.removeAttribute('contenteditable'); - if (el.style.border && el.style.border.includes('dashed')) { - el.style.border = 'none'; - } - }); - const cleanHtml = tempContainer.innerHTML; - - // Persistir alterações de canvas no banco - if (activeCartazData && activeCartazData.id) { - fetch(`/api/cartazes/${activeCartazData.id}`, { - method: 'PUT', - headers: { - 'Content-Type': 'application/json', - 'Authorization': `Bearer ${localStorage.getItem('token')}` - }, - body: JSON.stringify({ - conteudo: cleanHtml, - corFundo: cartazBgColor.value, - corTexto: cartazTextColor.value - }) - }).then(res => { - if (res.ok) { - showToast('Layout do Canvas salvo no servidor!', 'success'); - loadCartazesHistory(); - } else { - showToast('Erro ao salvar layout no servidor.', 'error'); - } - }).catch(e => { - console.error(e); - showToast('Erro de conexão ao salvar canvas.', 'error'); - }); - } else { - showToast('Layout salvo temporariamente. Crie o cartaz para salvar definitivamente.', 'warning'); - } + showToast('Design do Canvas pronto para exportação!', 'success'); } + // CONFIGURAÇÃO COMPLETA DE EVENTOS DE DRAG, RESIZE E SELEÇÃO function setupCanvasItemEvents(item) { + // Clique: Seleciona o item item.addEventListener('click', (e) => { if (!canvasModeActive) return; e.stopPropagation(); - if (selectedCanvasItem) { + if (selectedCanvasItem && selectedCanvasItem !== item) { selectedCanvasItem.classList.remove('active-item'); } selectedCanvasItem = item; item.classList.add('active-item'); - updateToolbarForSelected(); }); - // Clique duplo para focar e editar texto + // Clique duplo: Edição de texto item.addEventListener('dblclick', (e) => { if (!canvasModeActive) return; - if (item.querySelector('img')) return; // Evita editar imagens + if (item.querySelector('img')) return; e.stopPropagation(); item.setAttribute('contenteditable', 'true'); item.focus(); - - // Coloca o cursor de digitação no texto - const range = document.createRange(); - range.selectNodeContents(item); - range.collapse(false); - const sel = window.getSelection(); - sel.removeAllRanges(); - sel.addRange(range); }); + // Mousedown para Drag do elemento item.addEventListener('mousedown', (e) => { if (!canvasModeActive) return; if (e.target.classList.contains('canvas-resize-handle')) return; - // Se o item já estiver focado/editando, permite seleção de texto livre sem arrastar + // Se for fundo bloqueado, não arrasta + if (item.getAttribute('data-is-bg') === 'true') return; + + // Se já estiver editando texto com cursor ativo, permite digitação if (item.getAttribute('contenteditable') === 'true' && document.activeElement === item) { return; } e.preventDefault(); + if (selectedCanvasItem && selectedCanvasItem !== item) { + selectedCanvasItem.classList.remove('active-item'); + } selectedCanvasItem = item; - document.querySelectorAll('.canvas-item').forEach(el => el.classList.remove('active-item')); item.classList.add('active-item'); updateToolbarForSelected(); @@ -10833,24 +10679,22 @@ document.addEventListener('DOMContentLoaded', () => { dragStartLeft = ((itemRect.left - parentRect.left) / parentRect.width) * 100; dragStartTop = ((itemRect.top - parentRect.top) / parentRect.height) * 100; - - // Previne o highlight azul do browser durante o drag - document.body.style.userSelect = 'none'; + document.body.style.userSelect = 'none'; document.addEventListener('mousemove', handleCanvasDrag); document.addEventListener('mouseup', stopCanvasDrag); }); + // Touchstart para Mobile / Touchscreen Drag item.addEventListener('touchstart', (e) => { if (!canvasModeActive) return; if (e.target.classList.contains('canvas-resize-handle')) return; + if (item.getAttribute('data-is-bg') === 'true') return; - if (item.getAttribute('contenteditable') === 'true' && document.activeElement === item) { - return; + if (selectedCanvasItem && selectedCanvasItem !== item) { + selectedCanvasItem.classList.remove('active-item'); } - selectedCanvasItem = item; - document.querySelectorAll('.canvas-item').forEach(el => el.classList.remove('active-item')); item.classList.add('active-item'); updateToolbarForSelected(); @@ -10869,59 +10713,44 @@ document.addEventListener('DOMContentLoaded', () => { document.addEventListener('touchend', stopCanvasDrag); }); + // CONFIGURAÇÃO DA ALÇA DE REDIMENSIONAMENTO (BOLINHA AZUL) const handle = item.querySelector('.canvas-resize-handle'); if (handle) { - handle.addEventListener('mousedown', (e) => { + const initResize = (clientX, clientY, e) => { if (!canvasModeActive) return; e.stopPropagation(); e.preventDefault(); - // Garante que o item clicado seja o selecionado - document.querySelectorAll('.canvas-item').forEach(el => el.classList.remove('active-item')); + if (selectedCanvasItem && selectedCanvasItem !== item) { + selectedCanvasItem.classList.remove('active-item'); + } selectedCanvasItem = item; item.classList.add('active-item'); updateToolbarForSelected(); isResizingCanvas = true; - dragStartX = e.clientX; - dragStartY = e.clientY; + dragStartX = clientX; + dragStartY = clientY; const parentRect = cartazPrintableArea.getBoundingClientRect(); const itemRect = item.getBoundingClientRect(); + dragStartLeft = ((itemRect.left - parentRect.left) / parentRect.width) * 100; + dragStartTop = ((itemRect.top - parentRect.top) / parentRect.height) * 100; dragStartWidth = (itemRect.width / parentRect.width) * 100; dragStartHeight = (itemRect.height / parentRect.height) * 100; - // Previne seleção de texto durante o drag de resize document.body.style.userSelect = 'none'; - document.addEventListener('mousemove', handleCanvasResize); document.addEventListener('mouseup', stopCanvasResize); - }); - - handle.addEventListener('touchstart', (e) => { - if (!canvasModeActive) return; - e.stopPropagation(); - e.preventDefault(); - - document.querySelectorAll('.canvas-item').forEach(el => el.classList.remove('active-item')); - selectedCanvasItem = item; - item.classList.add('active-item'); - updateToolbarForSelected(); - - isResizingCanvas = true; - const touch = e.touches[0]; - dragStartX = touch.clientX; - dragStartY = touch.clientY; - - const parentRect = cartazPrintableArea.getBoundingClientRect(); - const itemRect = item.getBoundingClientRect(); - - dragStartWidth = (itemRect.width / parentRect.width) * 100; - dragStartHeight = (itemRect.height / parentRect.height) * 100; - document.addEventListener('touchmove', handleCanvasTouchResize, { passive: false }); document.addEventListener('touchend', stopCanvasResize); + }; + + handle.addEventListener('mousedown', (e) => initResize(e.clientX, e.clientY, e)); + handle.addEventListener('touchstart', (e) => { + const touch = e.touches[0]; + initResize(touch.clientX, touch.clientY, e); }); } } @@ -10931,23 +10760,20 @@ document.addEventListener('DOMContentLoaded', () => { const dx = e.clientX - dragStartX; const dy = e.clientY - dragStartY; const parentRect = cartazPrintableArea.getBoundingClientRect(); + if (!parentRect.width || !parentRect.height) return; + const dxPercent = (dx / parentRect.width) * 100; const dyPercent = (dy / parentRect.height) * 100; - let newLeft = dragStartLeft + dxPercent; - let newTop = dragStartTop + dyPercent; - const itemRect = selectedCanvasItem.getBoundingClientRect(); const itemWPercent = (itemRect.width / parentRect.width) * 100; const itemHPercent = (itemRect.height / parentRect.height) * 100; - if (newLeft < 0) newLeft = 0; - if (newTop < 0) newTop = 0; - if (newLeft + itemWPercent > 100) newLeft = 100 - itemWPercent; - if (newTop + itemHPercent > 100) newTop = 100 - itemHPercent; + let newLeft = Math.max(0, Math.min(100 - itemWPercent, dragStartLeft + dxPercent)); + let newTop = Math.max(0, Math.min(100 - itemHPercent, dragStartTop + dyPercent)); - selectedCanvasItem.style.left = `${newLeft}%`; - selectedCanvasItem.style.top = `${newTop}%`; + selectedCanvasItem.style.left = `${newLeft.toFixed(2)}%`; + selectedCanvasItem.style.top = `${newTop.toFixed(2)}%`; } function handleCanvasTouchDrag(e) { @@ -10957,56 +10783,50 @@ document.addEventListener('DOMContentLoaded', () => { const dx = touch.clientX - dragStartX; const dy = touch.clientY - dragStartY; const parentRect = cartazPrintableArea.getBoundingClientRect(); + if (!parentRect.width || !parentRect.height) return; + const dxPercent = (dx / parentRect.width) * 100; const dyPercent = (dy / parentRect.height) * 100; - let newLeft = dragStartLeft + dxPercent; - let newTop = dragStartTop + dyPercent; - const itemRect = selectedCanvasItem.getBoundingClientRect(); const itemWPercent = (itemRect.width / parentRect.width) * 100; const itemHPercent = (itemRect.height / parentRect.height) * 100; - if (newLeft < 0) newLeft = 0; - if (newTop < 0) newTop = 0; - if (newLeft + itemWPercent > 100) newLeft = 100 - itemWPercent; - if (newTop + itemHPercent > 100) newTop = 100 - itemHPercent; + let newLeft = Math.max(0, Math.min(100 - itemWPercent, dragStartLeft + dxPercent)); + let newTop = Math.max(0, Math.min(100 - itemHPercent, dragStartTop + dyPercent)); - selectedCanvasItem.style.left = `${newLeft}%`; - selectedCanvasItem.style.top = `${newTop}%`; + selectedCanvasItem.style.left = `${newLeft.toFixed(2)}%`; + selectedCanvasItem.style.top = `${newTop.toFixed(2)}%`; } function stopCanvasDrag() { isDraggingCanvas = false; - document.body.style.userSelect = ''; // Restaura seleção de texto + document.body.style.userSelect = ''; document.removeEventListener('mousemove', handleCanvasDrag); document.removeEventListener('mouseup', stopCanvasDrag); document.removeEventListener('touchmove', handleCanvasTouchDrag); document.removeEventListener('touchend', stopCanvasDrag); } + // REDIMENSIONAMENTO PRECISO (AMPLIAR / REDUZIR PELA BOLINHA AZUL) function handleCanvasResize(e) { if (!isResizingCanvas || !selectedCanvasItem) return; const dx = e.clientX - dragStartX; const dy = e.clientY - dragStartY; const parentRect = cartazPrintableArea.getBoundingClientRect(); + if (!parentRect.width || !parentRect.height) return; + const dxPercent = (dx / parentRect.width) * 100; const dyPercent = (dy / parentRect.height) * 100; - let newWidth = dragStartWidth + dxPercent; - let newHeight = dragStartHeight + dyPercent; + const maxAvailableWidth = 100 - dragStartLeft; + const maxAvailableHeight = 100 - dragStartTop; - if (newWidth < 10) newWidth = 10; - if (newHeight < 5) newHeight = 5; + let newWidth = Math.min(maxAvailableWidth, Math.max(4, dragStartWidth + dxPercent)); + let newHeight = Math.min(maxAvailableHeight, Math.max(3, dragStartHeight + dyPercent)); - const itemLeft = parseFloat(selectedCanvasItem.style.left) || 0; - const itemTop = parseFloat(selectedCanvasItem.style.top) || 0; - - if (itemLeft + newWidth > 100) newWidth = 100 - itemLeft; - if (itemTop + newHeight > 100) newHeight = 100 - itemTop; - - selectedCanvasItem.style.width = `${newWidth}%`; - selectedCanvasItem.style.height = `${newHeight}%`; + selectedCanvasItem.style.width = `${newWidth.toFixed(2)}%`; + selectedCanvasItem.style.height = `${newHeight.toFixed(2)}%`; } function handleCanvasTouchResize(e) { @@ -11016,28 +10836,24 @@ document.addEventListener('DOMContentLoaded', () => { const dx = touch.clientX - dragStartX; const dy = touch.clientY - dragStartY; const parentRect = cartazPrintableArea.getBoundingClientRect(); + if (!parentRect.width || !parentRect.height) return; + const dxPercent = (dx / parentRect.width) * 100; const dyPercent = (dy / parentRect.height) * 100; - let newWidth = dragStartWidth + dxPercent; - let newHeight = dragStartHeight + dyPercent; + const maxAvailableWidth = 100 - dragStartLeft; + const maxAvailableHeight = 100 - dragStartTop; - if (newWidth < 10) newWidth = 10; - if (newHeight < 5) newHeight = 5; + let newWidth = Math.min(maxAvailableWidth, Math.max(4, dragStartWidth + dxPercent)); + let newHeight = Math.min(maxAvailableHeight, Math.max(3, dragStartHeight + dyPercent)); - const itemLeft = parseFloat(selectedCanvasItem.style.left) || 0; - const itemTop = parseFloat(selectedCanvasItem.style.top) || 0; - - if (itemLeft + newWidth > 100) newWidth = 100 - itemLeft; - if (itemTop + newHeight > 100) newHeight = 100 - itemTop; - - selectedCanvasItem.style.width = `${newWidth}%`; - selectedCanvasItem.style.height = `${newHeight}%`; + selectedCanvasItem.style.width = `${newWidth.toFixed(2)}%`; + selectedCanvasItem.style.height = `${newHeight.toFixed(2)}%`; } function stopCanvasResize() { isResizingCanvas = false; - document.body.style.userSelect = ''; // Restaura seleção de texto + document.body.style.userSelect = ''; document.removeEventListener('mousemove', handleCanvasResize); document.removeEventListener('mouseup', stopCanvasResize); document.removeEventListener('touchmove', handleCanvasTouchResize); @@ -11045,7 +10861,7 @@ document.addEventListener('DOMContentLoaded', () => { } function rgbToHex(rgb) { - if (!rgb || rgb === 'transparent') return '#ffffff'; + if (!rgb || rgb === 'transparent' || rgb === 'rgba(0, 0, 0, 0)') return '#ffffff'; if (rgb.startsWith('#')) return rgb; const match = rgb.match(/^rgba?\((\d+),\s*(\d+),\s*(\d+)(?:,\s*(\d+(?:\.\d+)?))?\)$/); if (!match) return '#ffffff'; @@ -11055,28 +10871,107 @@ document.addEventListener('DOMContentLoaded', () => { return "#" + ((1 << 24) + (r << 16) + (g << 8) + b).toString(16).slice(1); } + // SINCRONIZA A TOOLBAR COM O ITEM SELECIONADO function updateToolbarForSelected() { - if (!selectedCanvasItem) return; + if (!selectedCanvasItem) { + if (canvasItemZIndexBadge) canvasItemZIndexBadge.textContent = 'Camada: --'; + return; + } - const ff = window.getComputedStyle(selectedCanvasItem).fontFamily.replace(/['"]/g, '').split(',')[0].trim(); + const computed = window.getComputedStyle(selectedCanvasItem); + + // Indicador da Camada (Z-Index) + const zVal = selectedCanvasItem.style.zIndex || computed.zIndex || '1'; + if (canvasItemZIndexBadge) { + canvasItemZIndexBadge.textContent = `Camada: ${zVal}`; + } + + // Tipografia + const ff = computed.fontFamily.replace(/['"]/g, '').split(',')[0].trim(); if (canvasFontFamily) canvasFontFamily.value = ff; - const fs = parseFloat(window.getComputedStyle(selectedCanvasItem).fontSize); - if (canvasFontSize) canvasFontSize.value = Math.round(fs); + const fs = parseFloat(computed.fontSize); + if (canvasFontSize && !isNaN(fs)) canvasFontSize.value = Math.round(fs); - const col = rgbToHex(window.getComputedStyle(selectedCanvasItem).color); + // Cores de Texto e Preenchimento + const col = rgbToHex(computed.color); if (canvasTextColor) canvasTextColor.value = col; - const bg = rgbToHex(window.getComputedStyle(selectedCanvasItem).backgroundColor); + const bg = rgbToHex(selectedCanvasItem.style.backgroundColor || computed.backgroundColor); if (canvasBgColorInput) canvasBgColorInput.value = bg; + // Contorno / Borda (Linha) e Espessura + const borderCol = rgbToHex(selectedCanvasItem.style.borderColor || computed.borderColor || computed.borderTopColor); + if (canvasBorderColor) canvasBorderColor.value = borderCol; + + const bWidth = parseFloat(selectedCanvasItem.style.borderWidth || computed.borderWidth || computed.borderTopWidth) || 0; + if (canvasBorderWidth) canvasBorderWidth.value = Math.round(bWidth); + + // Rotação e Opacidade const rot = selectedCanvasItem.getAttribute('data-rotation') || '0'; if (canvasRotation) canvasRotation.value = rot; - const op = window.getComputedStyle(selectedCanvasItem).opacity || '1'; + const op = computed.opacity || '1'; if (canvasOpacity) canvasOpacity.value = Math.round(parseFloat(op) * 100); } + // --- OUVINTES DOS 3 RECURSOS PEDIDOS PELO USUÁRIO (LINHA, ESPESSURA E PREENCHIMENTO) --- + + // 1. Cor da Linha de Contorno + if (canvasBorderColor) { + canvasBorderColor.addEventListener('input', () => { + if (!selectedCanvasItem) return; + const color = canvasBorderColor.value; + selectedCanvasItem.style.borderColor = color; + if (selectedCanvasItem.style.borderTopColor) selectedCanvasItem.style.borderTopColor = color; + // Se não havia borda ativa, define estilo solid + if (!selectedCanvasItem.style.borderStyle || selectedCanvasItem.style.borderStyle === 'none') { + selectedCanvasItem.style.borderStyle = 'solid'; + } + }); + } + + // 2. Alargar / Afinar Linhas (Espessura do Contorno em Pixels) + if (canvasBorderWidth) { + canvasBorderWidth.addEventListener('input', () => { + if (!selectedCanvasItem) return; + const widthVal = parseInt(canvasBorderWidth.value) || 0; + if (widthVal <= 0) { + selectedCanvasItem.style.borderWidth = '0px'; + selectedCanvasItem.style.border = 'none'; + } else { + const curColor = canvasBorderColor ? canvasBorderColor.value : '#2d3748'; + selectedCanvasItem.style.border = `${widthVal}px solid ${curColor}`; + selectedCanvasItem.style.borderWidth = `${widthVal}px`; + } + }); + } + + // 3. Preenchimento de Cor (Fundo da Forma) + if (canvasBgColorInput) { + canvasBgColorInput.addEventListener('input', () => { + if (!selectedCanvasItem) return; + selectedCanvasItem.style.backgroundColor = canvasBgColorInput.value; + }); + } + + // Botão Sem Fundo (Preenchimento Transparente) + if (btnCanvasBgTransparent) { + btnCanvasBgTransparent.addEventListener('click', () => { + if (!selectedCanvasItem) return; + selectedCanvasItem.style.backgroundColor = 'transparent'; + showToast('Preenchimento transparente aplicado.', 'info'); + }); + } + + // Cor do Texto + if (canvasTextColor) { + canvasTextColor.addEventListener('input', () => { + if (selectedCanvasItem) selectedCanvasItem.style.color = canvasTextColor.value; + }); + } + + // Tipografia e Tamanho da Fonte if (canvasFontFamily) { canvasFontFamily.addEventListener('change', () => { if (selectedCanvasItem) selectedCanvasItem.style.fontFamily = canvasFontFamily.value; @@ -11087,16 +10982,8 @@ document.addEventListener('DOMContentLoaded', () => { if (selectedCanvasItem) selectedCanvasItem.style.fontSize = `${canvasFontSize.value}px`; }); } - if (canvasTextColor) { - canvasTextColor.addEventListener('input', () => { - if (selectedCanvasItem) selectedCanvasItem.style.color = canvasTextColor.value; - }); - } - if (canvasBgColorInput) { - canvasBgColorInput.addEventListener('input', () => { - if (selectedCanvasItem) selectedCanvasItem.style.backgroundColor = canvasBgColorInput.value; - }); - } + + // Giro / Rotação if (canvasRotation) { canvasRotation.addEventListener('input', () => { if (selectedCanvasItem) { @@ -11106,52 +10993,65 @@ document.addEventListener('DOMContentLoaded', () => { } }); } + + // Opacidade if (canvasOpacity) { canvasOpacity.addEventListener('input', () => { if (selectedCanvasItem) { - const val = canvasOpacity.value; - selectedCanvasItem.style.opacity = parseFloat(val) / 100; + selectedCanvasItem.style.opacity = parseFloat(canvasOpacity.value) / 100; } }); } + + // BOTÕES DE CONTROLE DE CAMADA (Z-INDEX) + if (btnCanvasZIndexFront) { + btnCanvasZIndexFront.addEventListener('click', () => { + if (selectedCanvasItem) bringCanvasItemToFront(selectedCanvasItem); + }); + } if (btnCanvasZIndexUp) { btnCanvasZIndexUp.addEventListener('click', () => { - if (selectedCanvasItem) { - const currentZ = parseInt(selectedCanvasItem.style.zIndex) || 1; - selectedCanvasItem.style.zIndex = currentZ + 1; - } + if (selectedCanvasItem) stepCanvasItemZIndexUp(selectedCanvasItem); }); } if (btnCanvasZIndexDown) { btnCanvasZIndexDown.addEventListener('click', () => { - if (selectedCanvasItem) { - const currentZ = parseInt(selectedCanvasItem.style.zIndex) || 1; - selectedCanvasItem.style.zIndex = Math.max(1, currentZ - 1); - } + if (selectedCanvasItem) stepCanvasItemZIndexDown(selectedCanvasItem); }); } + if (btnCanvasZIndexBack) { + btnCanvasZIndexBack.addEventListener('click', () => { + if (selectedCanvasItem) sendCanvasItemToBack(selectedCanvasItem); + }); + } + + // Excluir Elemento if (btnCanvasDelete) { btnCanvasDelete.addEventListener('click', () => { if (selectedCanvasItem) { selectedCanvasItem.remove(); selectedCanvasItem = null; + updateToolbarForSelected(); + showToast('Elemento removido.', 'info'); } }); } + // Adicionar Texto Rápido if (btnCanvasAddText) { btnCanvasAddText.addEventListener('click', () => { if (!canvasModeActive) return; const textItem = document.createElement('div'); textItem.className = 'canvas-item'; textItem.innerHTML = 'Novo Texto (Clique duplo para editar)'; - textItem.style.left = '10%'; - textItem.style.top = '15%'; - textItem.style.width = '50%'; - textItem.style.height = '8%'; - textItem.style.fontSize = '20px'; - textItem.style.color = cartazTextColor.value || '#2d3748'; - textItem.style.fontFamily = cartazFontSelect.value || 'Fredoka'; + textItem.style.left = '15%'; + textItem.style.top = '20%'; + textItem.style.width = '55%'; + textItem.style.height = '10%'; + textItem.style.fontSize = '22px'; + textItem.style.color = cartazTextColor ? cartazTextColor.value : '#2d3748'; + textItem.style.fontFamily = cartazFontSelect ? cartazFontSelect.value : 'Fredoka'; + textItem.style.zIndex = '35'; textItem.setAttribute('contenteditable', 'true'); const handle = document.createElement('div'); @@ -11164,21 +11064,24 @@ document.addEventListener('DOMContentLoaded', () => { }); } + // Adicionar Card Rápido if (btnCanvasAddCard) { btnCanvasAddCard.addEventListener('click', () => { if (!canvasModeActive) return; const cardItem = document.createElement('div'); cardItem.className = 'canvas-item'; - cardItem.style.background = 'rgba(255, 255, 255, 0.4)'; - cardItem.style.border = '2px dashed ' + (cartazTextColor.value || '#2d3748'); + cardItem.style.background = 'rgba(255, 255, 255, 0.6)'; + cardItem.style.backdropFilter = 'blur(6px)'; + cardItem.style.border = '2px dashed ' + (cartazTextColor ? cartazTextColor.value : '#2d3748'); cardItem.style.borderRadius = '12px'; cardItem.style.left = '20%'; cardItem.style.top = '25%'; - cardItem.style.width = '45%'; - cardItem.style.height = '20%'; - cardItem.style.padding = '15px'; - cardItem.innerHTML = '

Novo Card

Edite este texto...

'; - cardItem.style.fontFamily = cartazFontSelect.value || 'Fredoka'; + cardItem.style.width = '50%'; + cardItem.style.height = '22%'; + cardItem.style.padding = '14px'; + cardItem.style.zIndex = '35'; + cardItem.innerHTML = '

Novo Card

Edite este texto aqui...

'; + cardItem.style.fontFamily = cartazFontSelect ? cartazFontSelect.value : 'Fredoka'; cardItem.setAttribute('contenteditable', 'true'); const handle = document.createElement('div'); @@ -11191,19 +11094,21 @@ document.addEventListener('DOMContentLoaded', () => { }); } + // Toggle do Modo Canvas if (btnCanvasModeToggle) { btnCanvasModeToggle.addEventListener('click', () => { if (canvasModeActive) { exitCanvasMode(); } else { if (cartazEditArea && cartazEditArea.style.display !== 'none') { - btnEditCartazToggle.click(); + btnEditCartazToggle && btnEditCartazToggle.click(); } enterCanvasMode(); } }); } + // Clique no fundo do cartaz desmarca item if (cartazPrintableArea) { cartazPrintableArea.addEventListener('click', (e) => { if (!canvasModeActive) return; @@ -11211,11 +11116,13 @@ document.addEventListener('DOMContentLoaded', () => { if (selectedCanvasItem) { selectedCanvasItem.classList.remove('active-item'); selectedCanvasItem = null; + updateToolbarForSelected(); } } }); } + // Abas de Assets do Canvas (Post-its, Stickers, Formas) function selectAssetTab(tab) { activeAssetTab = tab; [btnTabAssetPostits, btnTabAssetStickers, btnTabAssetFormas].forEach(btn => { @@ -11223,8 +11130,8 @@ document.addEventListener('DOMContentLoaded', () => { const tabIdName = `btnTabAsset${tab.charAt(0).toUpperCase() + tab.slice(1)}s`; if (btn.id === tabIdName) { btn.classList.add('active'); - btn.style.background = 'rgba(59, 130, 246, 0.1)'; - btn.style.color = 'var(--text-primary)'; + btn.style.background = 'rgba(59, 130, 246, 0.15)'; + btn.style.color = '#3b82f6'; btn.style.fontWeight = 'bold'; } else { btn.classList.remove('active'); @@ -11246,9 +11153,10 @@ document.addEventListener('DOMContentLoaded', () => { }); } + // Carrega a galeria de assets async function loadCanvasAssets(tipo, busca = '') { if (!canvasAssetsContainer) return; - canvasAssetsContainer.innerHTML = '
Carregando...
'; + canvasAssetsContainer.innerHTML = '
Carregando elementos...
'; try { const res = await fetch(`/api/biblioteca-assets?tipo=${tipo}&busca=${encodeURIComponent(busca)}`, { @@ -11258,7 +11166,7 @@ document.addEventListener('DOMContentLoaded', () => { canvasAssetsContainer.innerHTML = ''; if (!data || data.length === 0) { - canvasAssetsContainer.innerHTML = '
Nenhum item encontrado.
'; + canvasAssetsContainer.innerHTML = '
Nenhum elemento encontrado.
'; return; } @@ -11266,7 +11174,7 @@ document.addEventListener('DOMContentLoaded', () => { const item = document.createElement('div'); item.style.cursor = 'pointer'; item.style.padding = '8px'; - item.style.borderRadius = '6px'; + item.style.borderRadius = '8px'; item.style.background = 'var(--bg-tertiary)'; item.style.border = '1px solid var(--border-light)'; item.style.display = 'flex'; @@ -11274,13 +11182,13 @@ document.addEventListener('DOMContentLoaded', () => { item.style.alignItems = 'center'; item.style.justifyContent = 'center'; item.style.gap = '6px'; - item.style.transition = 'all 0.2s'; + item.style.transition = 'transform 0.15s ease, border-color 0.15s ease'; item.style.aspectRatio = '1'; item.title = asset.nome; item.addEventListener('mouseenter', () => { - item.style.transform = 'scale(1.05)'; - item.style.borderColor = 'rgba(59, 130, 246, 0.5)'; + item.style.transform = 'scale(1.06)'; + item.style.borderColor = '#3b82f6'; }); item.addEventListener('mouseleave', () => { item.style.transform = 'scale(1)'; @@ -11294,21 +11202,21 @@ document.addEventListener('DOMContentLoaded', () => { previewHtml = ``; } else if (asset.tipo === 'forma') { if (asset.url === 'rect') { - previewHtml = `
`; + previewHtml = `
`; } else if (asset.url === 'square') { - previewHtml = `
`; + previewHtml = `
`; } else if (asset.url === 'circle') { - previewHtml = `
`; + previewHtml = `
`; } else if (asset.url === 'ellipse') { previewHtml = `
`; } else if (asset.url === 'line') { - previewHtml = `
`; + previewHtml = `
`; } else if (asset.url === 'triangle') { previewHtml = `
`; } else if (asset.url === 'star') { - previewHtml = `
`; + previewHtml = `
`; } else if (asset.url === 'arrow') { - previewHtml = `
➡️
`; + previewHtml = `
➡️
`; } } @@ -11326,22 +11234,27 @@ document.addEventListener('DOMContentLoaded', () => { } } + // ADICIONA ASSET (POST-IT, STICKER OU FORMA) NO CANVAS COM OS NOVOS CONTROLES DE LINHA E PREENCHIMENTO function addAssetToCanvas(asset) { if (!canvasModeActive) return; const item = document.createElement('div'); item.className = 'canvas-item'; item.style.position = 'absolute'; - item.style.left = '35%'; - item.style.top = '35%'; + item.style.left = '32%'; + item.style.top = '30%'; item.style.width = '30%'; - item.style.height = '15%'; - item.style.zIndex = '10'; + item.style.height = '18%'; + item.style.zIndex = '35'; // Sempre em cima do fundo e das camadas + const curBorderColor = canvasBorderColor ? canvasBorderColor.value : '#2d3748'; + const curBorderWidth = canvasBorderWidth ? parseInt(canvasBorderWidth.value) || 3 : 3; + const curBg = canvasBgColorInput ? canvasBgColorInput.value : 'transparent'; + if (asset.tipo === 'postit') { item.style.background = asset.url; item.style.color = '#2d3748'; - item.style.boxShadow = '3px 5px 10px rgba(0,0,0,0.15)'; + item.style.boxShadow = '3px 5px 12px rgba(0,0,0,0.15)'; item.style.borderRadius = '4px'; const randomRot = Math.round(Math.random() * 8 - 4); item.style.transform = `rotate(${randomRot}deg)`; @@ -11351,62 +11264,71 @@ document.addEventListener('DOMContentLoaded', () => { item.style.fontSize = '22px'; item.innerHTML = 'Anotação...'; item.setAttribute('contenteditable', 'true'); - item.style.width = '35%'; + item.style.width = '34%'; item.style.height = '20%'; } else if (asset.tipo === 'sticker') { item.style.background = 'transparent'; - item.innerHTML = ``; - item.style.width = '20%'; - item.style.height = '20%'; + item.innerHTML = ``; + item.style.width = '22%'; + item.style.height = '22%'; item.setAttribute('data-rotation', '0'); } else if (asset.tipo === 'forma') { - item.style.background = 'transparent'; - item.style.border = '3px solid ' + (cartazTextColor.value || '#2d3748'); + item.setAttribute('data-is-shape', 'true'); item.setAttribute('data-rotation', '0'); + item.style.background = 'transparent'; + item.style.border = `${curBorderWidth}px solid ${curBorderColor}`; + if (asset.url === 'rect') { - item.style.borderRadius = '4px'; + item.style.borderRadius = '6px'; + item.style.width = '36%'; + item.style.height = '22%'; } else if (asset.url === 'square') { - item.style.borderRadius = '4px'; - item.style.width = '20%'; - item.style.height = '20%'; - } else if (asset.url === 'circle' || asset.url === 'ellipse') { + item.style.borderRadius = '6px'; + item.style.width = '24%'; + item.style.height = '24%'; + } else if (asset.url === 'circle') { item.style.borderRadius = '50%'; - item.style.width = '20%'; - item.style.height = '20%'; + item.style.width = '24%'; + item.style.height = '24%'; + } else if (asset.url === 'ellipse') { + item.style.borderRadius = '50%'; + item.style.width = '35%'; + item.style.height = '22%'; } else if (asset.url === 'line') { item.style.border = 'none'; - item.style.borderTop = '4px solid ' + (cartazTextColor.value || '#2d3748'); - item.style.height = '10px'; + item.style.borderTop = `${curBorderWidth}px solid ${curBorderColor}`; + item.style.height = '8px'; item.style.width = '40%'; } else if (asset.url === 'triangle') { item.style.border = 'none'; - item.style.background = cartazTextColor.value || '#2d3748'; + item.style.background = curBorderColor; item.style.clipPath = 'polygon(50% 0%, 0% 100%, 100% 100%)'; - item.style.width = '20%'; - item.style.height = '20%'; + item.style.width = '24%'; + item.style.height = '24%'; } else if (asset.url === 'star') { item.style.border = 'none'; - item.style.color = cartazTextColor.value || '#2d3748'; + item.style.color = curBorderColor; item.style.fontSize = '48px'; item.style.display = 'flex'; item.style.alignItems = 'center'; item.style.justifyContent = 'center'; item.innerHTML = '⭐'; - item.style.width = '20%'; - item.style.height = '20%'; + item.style.width = '22%'; + item.style.height = '22%'; } else if (asset.url === 'arrow') { item.style.border = 'none'; - item.style.color = cartazTextColor.value || '#2d3748'; + item.style.color = curBorderColor; item.style.fontSize = '48px'; item.style.display = 'flex'; item.style.alignItems = 'center'; item.style.justifyContent = 'center'; item.innerHTML = '➡️'; - item.style.width = '20%'; - item.style.height = '20%'; + item.style.width = '22%'; + item.style.height = '22%'; } } + // Alça de redimensionamento (Bolinha azul) const handle = document.createElement('div'); handle.className = 'canvas-resize-handle'; item.appendChild(handle); @@ -11414,6 +11336,7 @@ document.addEventListener('DOMContentLoaded', () => { cartazPrintableArea.appendChild(item); setupCanvasItemEvents(item); item.click(); + showToast(`Elemento adicionado! Use a barra superior para ajustar contorno, espessura e preenchimento.`, 'success'); } }); diff --git a/public/index.html b/public/index.html index c3b5df8..c1fb508 100644 --- a/public/index.html +++ b/public/index.html @@ -2770,15 +2770,15 @@ -