import { MaskManager } from './mask_manager.js'; import { BrushManager } from './brush_manager.js'; import { RemoveBgManager } from './removebg.js'; import { MagicWandManager } from './magic_wand_manager.js'; import { MaskPainterManager } from './mask_painter_manager.js'; const textIconPath = new Path2D("M5 4v2h5v12h4V6h5V4H5z"); const maskIconPath = new Path2D("M2 2 H22 V22 H2 Z M12 12 m-6 0 a6 6 0 1 0 12 0 a6 6 0 1 0 -12 0"); const FONT_LIST = [ // Sans-serif "Arial", "Verdana", "Tahoma", "Trebuchet MS", "Impact", "Lucida Sans Unicode", // Serif "Georgia", "Times New Roman", "Garamond", // Monospace "Courier New", "Lucida Console" ]; function ensureToolbarStyles() { const styleId = 'contextual-toolbar-styles'; if (document.getElementById(styleId)) return; const style = document.createElement('style'); style.id = styleId; style.innerHTML = ` #contextual-text-toolbar { /* MODIFIÉ : Fond blanc avec 85% d'opacité */ background-color: rgba(255, 255, 255, 0.5) !important; backdrop-filter: blur(8px); -webkit-backdrop-filter: blur(8px); /* MODIFIÉ : Bordure grise subtile */ border: 1px solid rgba(0, 0, 0, 0.1); border-radius: 8px; box-shadow: 0 4px H12px rgba(0,0,0,0.2); padding: 2px; display: flex; gap: 4px; } #contextual-text-toolbar button { background-color: transparent; border: none; /* MODIFIÉ : Icônes sombres pour la lisibilité sur fond blanc */ color: #333; font-size: 18px; cursor: pointer; padding: 4px; border-radius: 4px; transition: background-color 0.2s; } #contextual-text-toolbar button:hover { /* MODIFIÉ : Effet de survol gris très clair */ background-color: rgba(0, 0, 0, 0.05); } `; document.head.appendChild(style); } export class Toolbar { constructor(node) { this.node = node; this.width = 40; this.activeTool = null; this.textElements = []; this.activeTextarea = null; this.lastClickTime = 0; this.lastClickTarget = null; this.clickTimeout = null; this.tools = [ { name: 'text', icon: textIconPath, y: 9 }, { name: 'mask', icon: '🎭', y: 45 }, { name: 'magic_wand', icon: '☯️', y: 81 }, { name: 'brush', icon: '🖌️', y: 117 } ]; this.toolBounds = {}; this.selectedTextObject = null; this.textEditTool = 'move'; this.contextualToolbar = null; this.isTextDragging = false; this.dragStart = { x: 0, y: 0 }; this.initialTextPos = { x: 0, y: 0 }; this.dragOffset = { x: 0, y: 0 }; this.boundDragMove = this.handleDragMove.bind(this); this.boundDragEnd = this.handleDragEnd.bind(this); this.isResizing = false; this.resizeSensitivity = 0.5; this.colorPicker = null; this.maskManager = new MaskManager(this.node); this.removeBgManager = new RemoveBgManager(this.node, this.maskManager); this.magicWandManager = new MagicWandManager(this.node); this.brushManager = new BrushManager(this.node); this.maskPainterManager = new MaskPainterManager(this.node); this.setupColorPicker(); this.createContextualToolbar(); this.selectionSubMenu = null; this.createSelectionSubMenu(); } createContextualToolbar() { ensureToolbarStyles(); if (this.contextualToolbar) this.contextualToolbar.remove(); const toolbar = document.createElement("div"); toolbar.id = 'contextual-text-toolbar'; this.contextualToolbar = toolbar; Object.assign(toolbar.style, { position: 'fixed', display: 'none', zIndex: '10001', }); const fontSelect = document.createElement("select"); fontSelect.className = 'font-select'; Object.assign(fontSelect.style, { backgroundColor: 'transparent', border: '1px solid rgba(0, 0, 0, 0.1)', borderRadius: '4px', color: '#333', padding: '3px', margin: '0 2px' }); FONT_LIST.forEach(fontName => { const option = document.createElement("option"); option.value = fontName; option.textContent = fontName; option.style.fontFamily = fontName; fontSelect.appendChild(option); }); fontSelect.addEventListener('change', () => { if (this.selectedTextObject) { this.selectedTextObject.fontFamily = fontSelect.value; this.node.redrawPreviewCanvas(); this.node.updatePropertiesJSON(); } }); toolbar.appendChild(fontSelect); const icons = { 'move': '↔️', 'edit': '✏️', 'resize': '🔍', 'color': '🎨', 'delete': '🗑️', 'close': '❌' }; for (const action in icons) { const button = document.createElement("button"); button.innerHTML = icons[action]; button.addEventListener('click', () => { if (!this.selectedTextObject) return; this.isTextDragging = false; this.isResizing = false; this.node.previewCanvas.style.setProperty('cursor', 'default', 'important'); Array.from(event.currentTarget.parentElement.children).forEach(btn => { btn.style.backgroundColor = 'transparent'; }); switch(action) { case 'resize': this.isResizing = true; this.node.previewCanvas.style.setProperty('cursor', 'ns-resize', 'important'); event.currentTarget.style.backgroundColor = 'rgba(0, 0, 0, 0.1)'; break; case 'move': this.isTextDragging = true; this.node.previewCanvas.style.setProperty('cursor', 'move', 'important'); event.currentTarget.style.backgroundColor = 'rgba(0, 0, 0, 0.1)'; break; case 'edit': this.node.editTextElement(this.selectedTextObject); event.currentTarget.style.backgroundColor = 'rgba(0, 0, 0, 0.1)'; break; case 'delete': const index = this.textElements.findIndex(el => el.id === this.selectedTextObject.id); if (index > -1) { this.textElements.splice(index, 1); this.hideContextualToolbar(); this.node.updatePropertiesJSON(); } break; case 'close': this.hideContextualToolbar(); break; case 'color': event.currentTarget.style.backgroundColor = 'rgba(0, 0, 0, 0.1)'; if (this.selectedTextObject && this.colorPicker) { this.colorPicker.value = this.selectedTextObject.color || '#FFFFFF'; this.colorPicker.click(); } break; } }); toolbar.appendChild(button); } document.body.appendChild(toolbar); } getTexts() { return this.textElements; } draw(ctx) { const canvas = this.node.previewCanvas; if (!canvas) return; const x = 0; ctx.save(); ctx.fillStyle = '#282828'; ctx.fillRect(x, 0, this.width, canvas.height); ctx.strokeStyle = '#111111'; ctx.lineWidth = 1; ctx.strokeRect(x, 0, this.width, canvas.height); ctx.restore(); this.tools.forEach(tool => { const iconSize = 24; const iconX_start = (this.width - iconSize) / 2; const iconY_start = tool.y; this.toolBounds[tool.name] = { x: iconX_start, y: iconY_start, size: iconSize }; const isActive = this.activeTool === tool.name || (tool.name === 'magic_wand' && this.selectionSubMenu?.style.display === 'flex'); if (isActive) { ctx.fillStyle = "rgba(255, 221, 255, 0.6)"; const padding = 2; ctx.fillRect(iconX_start - padding, iconY_start - padding, iconSize + (padding * 2), iconSize + (padding * 2)); } ctx.save(); if (tool.icon instanceof Path2D) { ctx.translate(iconX_start, iconY_start); ctx.scale(iconSize / 24, iconSize / 24); ctx.strokeStyle = (this.activeTool === tool.name) ? "#FFD700" : "#FFFFFF"; ctx.lineWidth = 2; ctx.stroke(tool.icon); } else { ctx.font = `${iconSize}px sans-serif`; ctx.textAlign = 'center'; ctx.textBaseline = 'middle'; ctx.fillText(tool.icon, iconX_start + iconSize / 2, iconY_start + iconSize / 2); } ctx.restore(); }); } getConversionRatio() { if (!this.node.basePreviewImage || !this.node.previewCanvas || (this.node.previewCanvas.width - this.width) <= 0) { return 1.0; } const baseImageWidth = this.node.basePreviewImage.naturalWidth; const previewImageAreaWidth = this.node.previewCanvas.width - this.width; return baseImageWidth / previewImageAreaWidth; } drawTextElements(ctx) { if (!this.node.previewCanvas || !this.node.basePreviewImage) return; const ratio = this.getConversionRatio(); if (ratio === 1.0 && this.textElements.length > 0) return; const inverseRatio = 1 / ratio; const previewImageAreaWidth = this.node.previewCanvas.width - this.width; const previewCenterX = previewImageAreaWidth / 2; const previewCenterY = this.node.previewCanvas.height / 2; ctx.save(); this.textElements.forEach(textEl => { const preview_offset_x = textEl.offset_x * inverseRatio; const preview_offset_y = textEl.offset_y * inverseRatio; const preview_size = textEl.size * inverseRatio; const preview_x = this.width + previewCenterX + preview_offset_x; const preview_y = previewCenterY + preview_offset_y; ctx.fillStyle = textEl.color; ctx.font = `${preview_size}px ${textEl.fontFamily || 'Arial'}`; ctx.textBaseline = 'top'; ctx.fillText(textEl.text, preview_x, preview_y); }); ctx.restore(); } handleClick(e, mouseX, mouseY) { const clickedTool = this.tools.find(tool => { const bounds = this.toolBounds[tool.name]; if (!bounds) return false; return mouseX >= bounds.x && mouseX < (bounds.x + bounds.size) && mouseY >= bounds.y && mouseY < (bounds.y + bounds.size); }); if (clickedTool) { const toolName = clickedTool.name; const isDeactivating = this.activeTool === toolName || (toolName === 'magic_wand' && this.selectionSubMenu?.style.display === 'flex'); this.activeTool = null; this.maskManager.hide(); this.magicWandManager.hide(); this.brushManager.hide(); this.maskPainterManager.hide(); if (this.selectionSubMenu) { this.selectionSubMenu.style.display = 'none'; } if (!isDeactivating) { if (toolName !== 'magic_wand') { this.activeTool = toolName; } switch (toolName) { case 'mask': this.maskManager.show(); break; case 'magic_wand': if (this.selectionSubMenu) { this.selectionSubMenu.style.display = 'flex'; this.positionSelectionSubMenu(); } break; case 'brush': this.brushManager.show(); break; case 'text': break; } } if (clickedTool || this.activeTool) { this.node.movingLayer = null; } this.node.refreshUI(); } } handleCanvasClick(e) { if (this.activeTool !== 'text') { return; } if (this.selectedTextObject) { const clickedText = this.node.findTextElementAtPos(e.offsetX, e.offsetY); if (clickedText && clickedText.id === this.selectedTextObject.id) { this.handleDragStart(e); } else { this.hideContextualToolbar(); } return; } const now = Date.now(); const clickedTextForSelection = this.node.findTextElementAtPos(e.offsetX, e.offsetY); if (clickedTextForSelection && (now - this.lastClickTime < 300) && this.lastClickTarget === clickedTextForSelection.id) { if (this.clickTimeout) { clearTimeout(this.clickTimeout); this.clickTimeout = null; } this.showContextualToolbar(clickedTextForSelection, e); this.lastClickTime = 0; return; } this.lastClickTime = now; this.lastClickTarget = clickedTextForSelection ? clickedTextForSelection.id : null; if (!clickedTextForSelection) { this.clickTimeout = setTimeout(() => { if (this.activeTextarea) this.activeTextarea.remove(); const textInput = document.createElement("div"); this.activeTextarea = textInput; textInput.contentEditable = true; Object.assign(textInput.style, { position: 'fixed', left: `${e.clientX}px`, top: `${e.clientY}px`, border: '2px solid #FFD700', background: 'rgba(20,20,20,0.9)', color: 'white', zIndex: '9999', fontFamily: 'Arial', fontSize: '16px', padding: '5px', minWidth: '100px', resize: 'both', overflow: 'auto', whiteSpace: 'pre-wrap' }); document.body.appendChild(textInput); textInput.focus(); document.execCommand('selectAll', false, null); const onFinish = () => { if (textInput.innerText.trim() !== "") { const ratio = this.getConversionRatio(); const previewImageAreaWidth = this.node.previewCanvas.width - this.width; const previewCenterX = previewImageAreaWidth / 2; const previewCenterY = this.node.previewCanvas.height / 2; const click_x_in_preview_area = e.offsetX - this.width; const click_y_in_preview_area = e.offsetY; const newTextElement = { id: `text_${Date.now()}`, text: textInput.innerText, offset_x: (click_x_in_preview_area - previewCenterX) * ratio, offset_y: (click_y_in_preview_area - previewCenterY) * ratio, size: 24 * ratio, color: '#FFFFFF', fontFamily: 'Arial', }; this.textElements.push(newTextElement); this.node.updatePropertiesJSON(); this.node.redrawPreviewCanvas(); } if (textInput.parentElement) textInput.parentElement.removeChild(textInput); this.activeTextarea = null; }; textInput.addEventListener('blur', onFinish); textInput.addEventListener('keydown', (evt) => { evt.stopPropagation(); if (evt.key === 'Enter' && !evt.shiftKey) { evt.preventDefault(); onFinish(); } }); }, 250); } } showContextualToolbar(textElement, event) { this.selectedTextObject = textElement; this.contextualToolbar.style.display = 'flex'; this.node.redrawPreviewCanvas(); this.setDefaultMode(); const fontSelect = this.contextualToolbar.querySelector('.font-select'); if (fontSelect && this.selectedTextObject.fontFamily) { fontSelect.value = this.selectedTextObject.fontFamily; } this.updateContextualToolbarPosition(); } handleDragStart(e) { if (!this.selectedTextObject) return; this.initialTextData = { offset_x: this.selectedTextObject.offset_x, offset_y: this.selectedTextObject.offset_y, size: this.selectedTextObject.size }; this.dragStart = { x: e.clientX, y: e.clientY }; if (this.isResizing) { window.addEventListener('mousemove', this.boundDragMove, true); window.addEventListener('mouseup', this.boundDragEnd, true); return; } this.isTextDragging = true; window.addEventListener('mousemove', this.boundDragMove, true); window.addEventListener('mouseup', this.boundDragEnd, true); } updateContextualToolbarPosition() { if (!this.selectedTextObject || !this.node.previewCanvas || !this.node.getTextPreviewMetrics) return; const metrics = this.node.getTextPreviewMetrics(this.selectedTextObject); if (!metrics) return; const canvasRect = this.node.previewCanvas.getBoundingClientRect(); let zoom = 1.0; const unscaledWidth = this.node.previewCanvas.width; if (unscaledWidth > 0 && canvasRect.width > 0) { zoom = canvasRect.width / unscaledWidth; } const anchorX = canvasRect.left + (metrics.x + metrics.width / 2) * zoom; const anchorY = canvasRect.top + metrics.y * zoom; const toolbarEl = this.contextualToolbar; if (!toolbarEl) return; toolbarEl.style.left = `${anchorX}px`; toolbarEl.style.top = `${anchorY}px`; const margin = 60; toolbarEl.style.transform = `translate(-50%, -100%) translateY(-${margin}px)`; } handleDragMove(e) { if (!this.selectedTextObject) return; const dx = e.clientX - this.dragStart.x; const dy = e.clientY - this.dragStart.y; const ratio = this.getConversionRatio(); if (this.isResizing) { const delta_size_final = -(dy * this.resizeSensitivity) * ratio; const newSize = this.initialTextData.size + delta_size_final; this.selectedTextObject.size = Math.max(5 * ratio, newSize); } else if (this.isTextDragging) { e.preventDefault(); e.stopPropagation(); const delta_x_final = dx * ratio; const delta_y_final = dy * ratio; this.selectedTextObject.offset_x = this.initialTextData.offset_x + delta_x_final; this.selectedTextObject.offset_y = this.initialTextData.offset_y + delta_y_final; } this.node.redrawPreviewCanvas(); } handleDragEnd(e) { window.removeEventListener('mousemove', this.boundDragMove, true); window.removeEventListener('mouseup', this.boundDragEnd, true); this.node.updatePropertiesJSON(); this.node.redrawPreviewCanvas(); if (this.isResizing) { } else if (this.isTextDragging) { this.isTextDragging = false; this.updateContextualToolbarPosition(); } } createSelectionSubMenu() { this.selectionSubMenu = document.createElement("div"); Object.assign(this.selectionSubMenu.style, { position: 'fixed', display: 'none', zIndex: '10002', backgroundColor: 'rgba(30, 30, 30, 0.9)', border: '1px solid #555', borderRadius: '8px', padding: '4px', flexDirection: 'column', gap: '4px', }); const createEmojiButton = (emoji, title) => { const button = document.createElement("button"); button.innerText = emoji; button.title = title; Object.assign(button.style, { backgroundColor: '#444', color: 'white', border: '1px solid #666', padding: '0', borderRadius: '5px', cursor: 'pointer', width: '26px', height: '26px', fontSize: '16px', textAlign: 'center', lineHeight: '26px', }); button.onmouseover = () => button.style.backgroundColor = '#5E5E5E'; button.onmouseout = () => button.style.backgroundColor = '#444'; return button; }; const wandButton = createEmojiButton("🪄", "Baguette Magique"); wandButton.onclick = () => { this.activeTool = 'magic_wand'; this.magicWandManager.show(); this.selectionSubMenu.style.display = 'none'; }; const paintMaskButton = createEmojiButton("🖌️", "Paint Mask"); if (this.maskPainterManager) { this.maskPainterManager.show(); } this.selectionSubMenu.style.display = 'none'; }; this.selectionSubMenu.append(wandButton, paintMaskButton); document.body.appendChild(this.selectionSubMenu); } positionSelectionSubMenu() { if (!this.selectionSubMenu || this.selectionSubMenu.style.display === 'none') { return; } const bounds = this.toolBounds['magic_wand']; if (!bounds || !this.node.previewCanvas) return; const canvasRect = this.node.previewCanvas.getBoundingClientRect(); this.selectionSubMenu.style.left = `${canvasRect.left + bounds.x + bounds.size}px`; this.selectionSubMenu.style.top = `${canvasRect.top + bounds.y - 40}px`; } hideContextualToolbar() { this.selectedTextObject = null; this.contextualToolbar.style.display = 'none'; this.isResizing = false; this.isTextDragging = false; if (this.node.previewCanvas) { this.node.previewCanvas.style.setProperty('cursor', 'default', 'important'); } this.node.redrawPreviewCanvas(); } setupColorPicker() { const picker = document.createElement('input'); picker.type = 'color'; Object.assign(picker.style, { position: 'fixed', opacity: 0, pointerEvents: 'none', left: '-100px', top: '-100px' }); picker.addEventListener('input', () => { if (this.selectedTextObject) { this.selectedTextObject.color = picker.value; this.node.redrawPreviewCanvas(); } }); picker.addEventListener('change', () => { if (this.selectedTextObject) { this.setDefaultMode(); this.node.updatePropertiesJSON(); } }); document.body.appendChild(picker); this.colorPicker = picker; } setDefaultMode() { this.isTextDragging = true; this.isResizing = false; this.node.previewCanvas.style.setProperty('cursor', 'move', 'important'); if (!this.contextualToolbar) return; Array.from(this.contextualToolbar.children).forEach(button => { button.style.backgroundColor = 'transparent'; if (button.innerHTML === '↔️') { button.style.backgroundColor = 'rgba(0, 0, 0, 0.1)'; } }); } isClickOnToolbar(mouseX, mouseY) { const canvas = this.node.previewCanvas; return canvas && mouseX < this.width; } }