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 @@
-
-
-
+
+
+
-
+
-
Item:
-