class BannerDesigner { constructor() { this.canvas = document.getElementById('bannerCanvas'); this.ctx = this.canvas.getContext('2d'); this.overlay = document.getElementById('overlay'); this.elements = []; this.selectedElement = null; this.isDragging = false; this.isResizing = false; this.resizeHandle = null; this.dragOffset = {x: 0, y: 0}; this.backgroundImage = null; this.backgroundColor = '#ffffff'; this.backgroundOpacity = 1; // เพิ่ม History สำหรับ Undo/Redo this.history = []; this.historyStep = -1; this.maxHistorySteps = 50; this.init(); } init() { this.setupEventListeners(); this.setupKeyboardShortcuts(); this.updateCanvasSize(1200, 630); this.saveState(); // บันทึกสถานะเริ่มต้น this.render(); } setupEventListeners() { // Size preset dropdown document.getElementById('sizePreset').addEventListener('change', (e) => { const value = e.target.value; const customInputs = document.getElementById('customSizeInputs'); if (value === 'custom') { customInputs.style.display = 'flex'; } else { customInputs.style.display = 'none'; const [width, height] = value.split('x').map(num => parseInt(num)); this.updateCanvasSize(width, height); } }); // Custom size document.getElementById('applyCustomSize').addEventListener('click', () => { const width = parseInt(document.getElementById('customWidth').value); const height = parseInt(document.getElementById('customHeight').value); if (width > 0 && height > 0) { this.updateCanvasSize(width, height); } }); // Add Enter key support for custom size inputs document.getElementById('customWidth').addEventListener('keypress', (e) => { if (e.key === 'Enter') { document.getElementById('applyCustomSize').click(); } }); document.getElementById('customHeight').addEventListener('keypress', (e) => { if (e.key === 'Enter') { document.getElementById('applyCustomSize').click(); } }); // Background controls document.getElementById('bgColor').addEventListener('change', (e) => { this.backgroundColor = e.target.value; this.render(); }); document.getElementById('bgImage').addEventListener('change', (e) => { this.handleBackgroundImage(e.target.files[0]); }); document.getElementById('removeBgImage').addEventListener('click', () => { this.backgroundImage = null; this.render(); }); document.getElementById('bgOpacity').addEventListener('input', (e) => { this.backgroundOpacity = e.target.value / 100; document.getElementById('opacityValue').textContent = e.target.value + '%'; this.render(); }); // Add text button document.getElementById('addText').addEventListener('click', () => { this.addTextElement(); }); // Add logo document.getElementById('logoImage').addEventListener('change', (e) => { if (e.target.files[0]) { this.addImageElement(e.target.files[0]); } }); // Text controls this.setupTextControls(); // Export document.getElementById('exportBtn').addEventListener('click', () => { this.exportBanner(); }); // Canvas mouse events this.overlay.style.pointerEvents = 'all'; this.overlay.addEventListener('mousedown', (e) => this.handleMouseDown(e)); this.overlay.addEventListener('mousemove', (e) => this.handleMouseMove(e)); this.overlay.addEventListener('mouseup', (e) => this.handleMouseUp(e)); // Prevent context menu this.overlay.addEventListener('contextmenu', (e) => e.preventDefault()); } // เพิ่มฟังก์ชัน Keyboard Shortcuts setupKeyboardShortcuts() { document.addEventListener('keydown', (e) => { // ป้องกันไม่ให้ทำงานเมื่อพิมพ์ในช่อง input if (e.target.tagName === 'INPUT' || e.target.tagName === 'TEXTAREA') return; if (e.ctrlKey || e.metaKey) { switch (e.key.toLowerCase()) { case 'z': e.preventDefault(); if (e.shiftKey) { this.redo(); } else { this.undo(); } break; case 'y': e.preventDefault(); this.redo(); break; case 'c': e.preventDefault(); this.copyElement(); break; case 'v': e.preventDefault(); this.pasteElement(); break; case 'd': e.preventDefault(); this.duplicateElement(); break; } } else { switch (e.key) { case 'Delete': case 'Backspace': e.preventDefault(); if (this.selectedElement) { this.deleteElement(this.selectedElement); } break; case 'Escape': this.selectedElement = null; this.updateTextControls(); this.render(); break; } } }); } // ฟังก์ชันจัดการ History saveState() { // ลบ history ที่เกินขั้นตอนปัจจุบัน this.history = this.history.slice(0, this.historyStep + 1); // เพิ่มสถานะใหม่ const state = { elements: JSON.parse(JSON.stringify(this.elements.map(el => { if (el.type === 'image') { // สำหรับรูปภาพ เก็บเฉพาะข้อมูลที่จำเป็น (ไม่รวม image object) return { ...el, imageData: el.image ? el.image.src : null, image: null }; } return el; }))), backgroundColor: this.backgroundColor, backgroundOpacity: this.backgroundOpacity, backgroundImageData: this.backgroundImage ? this.backgroundImage.src : null, canvasWidth: this.canvas.width, canvasHeight: this.canvas.height }; this.history.push(state); this.historyStep++; // จำกัดขนาด history if (this.history.length > this.maxHistorySteps) { this.history.shift(); this.historyStep--; } } undo() { if (this.historyStep > 0) { this.historyStep--; this.restoreState(); } } redo() { if (this.historyStep < this.history.length - 1) { this.historyStep++; this.restoreState(); } } restoreState() { const state = this.history[this.historyStep]; if (!state) return; this.backgroundColor = state.backgroundColor; this.backgroundOpacity = state.backgroundOpacity; // อัพเดต Canvas size if (state.canvasWidth && state.canvasHeight) { this.updateCanvasSize(state.canvasWidth, state.canvasHeight); } // กู้คืนพื้นหลัง if (state.backgroundImageData) { const img = new Image(); img.onload = () => { this.backgroundImage = img; this.render(); }; img.src = state.backgroundImageData; } else { this.backgroundImage = null; } // กู้คืน Elements this.elements = []; state.elements.forEach(elementData => { if (elementData.type === 'image' && elementData.imageData) { const img = new Image(); img.onload = () => { elementData.image = img; this.elements.push(elementData); this.render(); }; img.src = elementData.imageData; } else { this.elements.push(elementData); } }); this.selectedElement = null; this.updateTextControls(); this.render(); } // ฟังก์ชัน Copy & Paste copyElement() { if (this.selectedElement) { this.copiedElement = JSON.parse(JSON.stringify(this.selectedElement)); // สำหรับรูปภาพ เก็บ imageData แทน image object if (this.copiedElement.type === 'image' && this.selectedElement.image) { this.copiedElement.imageData = this.selectedElement.image.src; delete this.copiedElement.image; } } } pasteElement() { if (this.copiedElement) { const element = JSON.parse(JSON.stringify(this.copiedElement)); element.id = Date.now(); element.x += 20; // เลื่อนตำแหน่งนิดหน่อย element.y += 20; if (element.type === 'image' && element.imageData) { const img = new Image(); img.onload = () => { element.image = img; delete element.imageData; this.elements.push(element); this.selectElement(element); this.saveState(); this.render(); }; img.src = element.imageData; } else { this.elements.push(element); this.selectElement(element); this.saveState(); this.render(); } } } duplicateElement() { if (this.selectedElement) { this.copyElement(); this.pasteElement(); } } updateCanvasSize(width, height) { this.canvas.width = width; this.canvas.height = height; this.canvas.style.maxWidth = '100%'; this.canvas.style.height = 'auto'; document.getElementById('canvasSize').textContent = `${width} × ${height} px`; // Update custom size inputs document.getElementById('customWidth').value = width; document.getElementById('customHeight').value = height; // Update size preset dropdown const sizePreset = document.getElementById('sizePreset'); const sizeValue = `${width}x${height}`; const customInputs = document.getElementById('customSizeInputs'); // Check if this is a preset size const presetOption = Array.from(sizePreset.options).find(option => option.value === sizeValue); if (presetOption) { sizePreset.value = sizeValue; customInputs.style.display = 'none'; } else { sizePreset.value = 'custom'; customInputs.style.display = 'block'; } this.render(); } handleBackgroundImage(file) { if (!file) return; const reader = new FileReader(); reader.onload = (e) => { const img = new Image(); img.onload = () => { this.backgroundImage = img; this.render(); }; img.src = e.target.result; }; reader.readAsDataURL(file); } addTextElement() { const element = { type: 'text', id: Date.now(), x: this.canvas.width / 2 - 100, y: this.canvas.height / 2 - 15, text: 'ข้อความใหม่', fontSize: 24, fontFamily: 'Kanit', color: '#000000', textAlign: 'left', bold: false, italic: false, underline: false, width: 200, height: 30, manuallyResized: false, minWidth: 50, minHeight: 20, // เพิ่มคุณสมบัติใหม่ opacity: 1, rotation: 0, shadow: { enabled: false, color: '#000000', blur: 5, offsetX: 2, offsetY: 2 }, stroke: { enabled: false, color: '#000000', width: 1 } }; // Calculate initial text dimensions this.ctx.font = `${element.fontSize}px ${element.fontFamily}`; const textMetrics = this.ctx.measureText(element.text); element.width = Math.max(textMetrics.width + 10, element.minWidth); element.height = Math.max(element.fontSize * 1.2, element.minHeight); this.elements.push(element); this.selectElement(element); this.saveState(); // บันทึกสถานะหลังเพิ่มองค์ประกอบ this.render(); } addImageElement(file) { const reader = new FileReader(); reader.onload = (e) => { const img = new Image(); img.onload = () => { const maxSize = 200; let width = Math.min(img.width, maxSize); let height = Math.min(img.height, maxSize); // Maintain aspect ratio const aspectRatio = img.width / img.height; if (width / height > aspectRatio) { width = height * aspectRatio; } else { height = width / aspectRatio; } const element = { type: 'image', id: Date.now(), x: this.canvas.width / 2 - width / 2, y: this.canvas.height / 2 - height / 2, image: img, width: width, height: height, originalWidth: img.width, originalHeight: img.height, aspectRatio: aspectRatio }; this.elements.push(element); this.selectElement(element); this.render(); }; img.src = e.target.result; }; reader.readAsDataURL(file); } selectElement(element) { this.selectedElement = element; this.updateTextControls(); } setupTextControls() { const textContent = document.getElementById('textContent'); const fontFamily = document.getElementById('fontFamily'); const fontSize = document.getElementById('fontSize'); const textColor = document.getElementById('textColor'); const textAlign = document.getElementById('textAlign'); const boldBtn = document.getElementById('boldBtn'); const italicBtn = document.getElementById('italicBtn'); const underlineBtn = document.getElementById('underlineBtn'); const deleteBtn = document.getElementById('deleteElement'); textContent.addEventListener('input', () => { if (this.selectedElement && this.selectedElement.type === 'text') { this.selectedElement.text = textContent.value; // Reset manual resize flag when text changes if (!this.selectedElement.manuallyResized) { this.selectedElement.manuallyResized = false; } this.render(); } }); fontFamily.addEventListener('change', () => { if (this.selectedElement && this.selectedElement.type === 'text') { this.selectedElement.fontFamily = fontFamily.value; this.render(); } }); fontSize.addEventListener('input', () => { if (this.selectedElement && this.selectedElement.type === 'text') { this.selectedElement.fontSize = parseInt(fontSize.value); this.selectedElement.manuallyResized = true; // Mark as manually resized when font size changes document.getElementById('fontSizeValue').textContent = fontSize.value + 'px'; this.render(); } }); textColor.addEventListener('change', () => { if (this.selectedElement && this.selectedElement.type === 'text') { this.selectedElement.color = textColor.value; this.render(); } }); textAlign.addEventListener('change', () => { if (this.selectedElement && this.selectedElement.type === 'text') { this.selectedElement.textAlign = textAlign.value; this.render(); } }); boldBtn.addEventListener('click', () => { if (this.selectedElement && this.selectedElement.type === 'text') { this.selectedElement.bold = !this.selectedElement.bold; boldBtn.classList.toggle('active', this.selectedElement.bold); this.render(); } }); italicBtn.addEventListener('click', () => { if (this.selectedElement && this.selectedElement.type === 'text') { this.selectedElement.italic = !this.selectedElement.italic; italicBtn.classList.toggle('active', this.selectedElement.italic); this.render(); } }); underlineBtn.addEventListener('click', () => { if (this.selectedElement && this.selectedElement.type === 'text') { this.selectedElement.underline = !this.selectedElement.underline; underlineBtn.classList.toggle('active', this.selectedElement.underline); this.render(); } }); deleteBtn.addEventListener('click', () => { if (this.selectedElement) { this.deleteElement(this.selectedElement); } }); } updateTextControls() { const textControls = document.getElementById('textControls'); if (this.selectedElement && this.selectedElement.type === 'text') { textControls.style.display = 'block'; document.getElementById('textContent').value = this.selectedElement.text; document.getElementById('fontFamily').value = this.selectedElement.fontFamily; document.getElementById('fontSize').value = this.selectedElement.fontSize; document.getElementById('fontSizeValue').textContent = this.selectedElement.fontSize + 'px'; document.getElementById('textColor').value = this.selectedElement.color; document.getElementById('textAlign').value = this.selectedElement.textAlign; document.getElementById('boldBtn').classList.toggle('active', this.selectedElement.bold); document.getElementById('italicBtn').classList.toggle('active', this.selectedElement.italic); document.getElementById('underlineBtn').classList.toggle('active', this.selectedElement.underline); } else { textControls.style.display = this.selectedElement ? 'block' : 'none'; if (this.selectedElement) { // Show only delete button for non-text elements textControls.innerHTML = '

✏️ แก้ไของค์ประกอบ

'; document.getElementById('deleteElement').addEventListener('click', () => { if (this.selectedElement) { this.deleteElement(this.selectedElement); } }); } } } deleteElement(element) { const index = this.elements.indexOf(element); if (index > -1) { this.elements.splice(index, 1); this.selectedElement = null; this.updateTextControls(); this.render(); } } getCanvasRect() { return this.canvas.getBoundingClientRect(); } getMousePos(e) { const rect = this.getCanvasRect(); const scaleX = this.canvas.width / rect.width; const scaleY = this.canvas.height / rect.height; return { x: (e.clientX - rect.left) * scaleX, y: (e.clientY - rect.top) * scaleY }; } handleMouseDown(e) { const mousePos = this.getMousePos(e); let elementClicked = false; // Check for resize handle first if (this.selectedElement) { const handle = this.getResizeHandle(mousePos, this.selectedElement); if (handle) { this.isResizing = true; this.resizeHandle = handle; this.dragOffset = {x: mousePos.x, y: mousePos.y}; return; } } // Check if clicking on an element (reverse order to check top elements first) for (let i = this.elements.length - 1; i >= 0; i--) { const element = this.elements[i]; if (this.isPointInElement(mousePos, element)) { this.selectElement(element); this.isDragging = true; this.dragOffset = { x: mousePos.x - element.x, y: mousePos.y - element.y }; elementClicked = true; break; } } if (!elementClicked) { this.selectedElement = null; this.updateTextControls(); this.render(); } } handleMouseMove(e) { const mousePos = this.getMousePos(e); if (this.isResizing && this.selectedElement) { this.handleResize(mousePos); } else if (this.isDragging && this.selectedElement) { this.selectedElement.x = mousePos.x - this.dragOffset.x; this.selectedElement.y = mousePos.y - this.dragOffset.y; // Keep elements within canvas bounds this.selectedElement.x = Math.max(0, Math.min(this.canvas.width - this.selectedElement.width, this.selectedElement.x)); this.selectedElement.y = Math.max(0, Math.min(this.canvas.height - this.selectedElement.height, this.selectedElement.y)); this.render(); } else if (this.selectedElement) { // Update cursor based on resize handle hover const handle = this.getResizeHandle(mousePos, this.selectedElement); if (handle) { this.overlay.style.cursor = this.getResizeCursor(handle); } else if (this.isPointInElement(mousePos, this.selectedElement)) { this.overlay.style.cursor = 'move'; } else { this.overlay.style.cursor = 'default'; } } } handleMouseUp(e) { this.isDragging = false; this.isResizing = false; this.resizeHandle = null; this.overlay.style.cursor = 'default'; } isPointInElement(point, element) { return point.x >= element.x && point.x <= element.x + element.width && point.y >= element.y && point.y <= element.y + element.height; } render() { // Clear canvas this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height); // Draw background this.drawBackground(); // Draw all elements this.elements.forEach(element => { this.drawElement(element); }); // Draw selection border if (this.selectedElement) { this.drawSelectionBorder(this.selectedElement); } } drawBackground() { // Fill with background color this.ctx.save(); this.ctx.globalAlpha = this.backgroundOpacity; this.ctx.fillStyle = this.backgroundColor; this.ctx.fillRect(0, 0, this.canvas.width, this.canvas.height); this.ctx.restore(); // Draw background image if exists if (this.backgroundImage) { this.ctx.save(); this.ctx.globalAlpha = this.backgroundOpacity; // Calculate scale to cover entire canvas const scale = Math.max( this.canvas.width / this.backgroundImage.width, this.canvas.height / this.backgroundImage.height ); const scaledWidth = this.backgroundImage.width * scale; const scaledHeight = this.backgroundImage.height * scale; const x = (this.canvas.width - scaledWidth) / 2; const y = (this.canvas.height - scaledHeight) / 2; this.ctx.drawImage(this.backgroundImage, x, y, scaledWidth, scaledHeight); this.ctx.restore(); } } drawElement(element) { this.ctx.save(); if (element.type === 'text') { this.drawTextElement(element); } else if (element.type === 'image') { this.drawImageElement(element); } this.ctx.restore(); } drawTextElement(element) { // Set font properties let fontStyle = ''; if (element.italic) fontStyle += 'italic '; if (element.bold) fontStyle += 'bold '; this.ctx.font = `${fontStyle}${element.fontSize}px ${element.fontFamily}`; this.ctx.fillStyle = element.color; this.ctx.textBaseline = 'top'; // Calculate text metrics const textMetrics = this.ctx.measureText(element.text); const textWidth = textMetrics.width; const textHeight = element.fontSize * 1.2; // Update element dimensions if not manually resized if (!element.manuallyResized) { element.width = Math.max(textWidth + 10, element.minWidth || 50); element.height = Math.max(textHeight, element.minHeight || 20); } // Calculate text position based on alignment and bounding box let textX, textY; switch (element.textAlign) { case 'center': textX = element.x + element.width / 2; this.ctx.textAlign = 'center'; break; case 'right': textX = element.x + element.width - 5; this.ctx.textAlign = 'right'; break; default: // left textX = element.x + 5; this.ctx.textAlign = 'left'; break; } // Center text vertically in the bounding box textY = element.y + (element.height - textHeight) / 2; // Draw text this.ctx.fillText(element.text, textX, textY); // Draw underline if needed if (element.underline) { this.ctx.beginPath(); this.ctx.moveTo(element.x + 5, textY + textHeight - 2); this.ctx.lineTo(element.x + element.width - 5, textY + textHeight - 2); this.ctx.strokeStyle = element.color; this.ctx.lineWidth = 1; this.ctx.stroke(); } } drawImageElement(element) { this.ctx.drawImage( element.image, element.x, element.y, element.width, element.height ); } drawSelectionBorder(element) { this.ctx.save(); this.ctx.strokeStyle = '#667eea'; this.ctx.lineWidth = 2; this.ctx.setLineDash([5, 5]); // Draw different border styles for different element types if (element.type === 'text') { // Draw background for text elements to show the text area this.ctx.fillStyle = 'rgba(102, 126, 234, 0.1)'; this.ctx.fillRect(element.x, element.y, element.width, element.height); } this.ctx.strokeRect(element.x - 2, element.y - 2, element.width + 4, element.height + 4); // Draw resize handles this.drawResizeHandles(element); this.ctx.restore(); } drawResizeHandles(element) { const handleSize = 8; const handles = [ {x: element.x - handleSize / 2, y: element.y - handleSize / 2}, {x: element.x + element.width - handleSize / 2, y: element.y - handleSize / 2}, {x: element.x - handleSize / 2, y: element.y + element.height - handleSize / 2}, {x: element.x + element.width - handleSize / 2, y: element.y + element.height - handleSize / 2}, {x: element.x + element.width / 2 - handleSize / 2, y: element.y - handleSize / 2}, {x: element.x + element.width / 2 - handleSize / 2, y: element.y + element.height - handleSize / 2}, {x: element.x - handleSize / 2, y: element.y + element.height / 2 - handleSize / 2}, {x: element.x + element.width - handleSize / 2, y: element.y + element.height / 2 - handleSize / 2} ]; this.ctx.fillStyle = '#667eea'; this.ctx.strokeStyle = 'white'; this.ctx.lineWidth = 2; this.ctx.setLineDash([]); handles.forEach(handle => { this.ctx.beginPath(); this.ctx.arc(handle.x + handleSize / 2, handle.y + handleSize / 2, handleSize / 2, 0, 2 * Math.PI); this.ctx.fill(); this.ctx.stroke(); }); } exportBanner() { const format = document.getElementById('exportFormat').value; let mimeType; let filename; switch (format) { case 'png': mimeType = 'image/png'; filename = 'banner.png'; break; case 'jpeg': mimeType = 'image/jpeg'; filename = 'banner.jpg'; break; case 'webp': mimeType = 'image/webp'; filename = 'banner.webp'; break; } // Create temporary canvas for export (without selection borders) const tempCanvas = document.createElement('canvas'); tempCanvas.width = this.canvas.width; tempCanvas.height = this.canvas.height; const tempCtx = tempCanvas.getContext('2d'); // Draw background tempCtx.save(); tempCtx.globalAlpha = this.backgroundOpacity; tempCtx.fillStyle = this.backgroundColor; tempCtx.fillRect(0, 0, tempCanvas.width, tempCanvas.height); tempCtx.restore(); if (this.backgroundImage) { tempCtx.save(); tempCtx.globalAlpha = this.backgroundOpacity; const scale = Math.max( tempCanvas.width / this.backgroundImage.width, tempCanvas.height / this.backgroundImage.height ); const scaledWidth = this.backgroundImage.width * scale; const scaledHeight = this.backgroundImage.height * scale; const x = (tempCanvas.width - scaledWidth) / 2; const y = (tempCanvas.height - scaledHeight) / 2; tempCtx.drawImage(this.backgroundImage, x, y, scaledWidth, scaledHeight); tempCtx.restore(); } // Draw elements this.elements.forEach(element => { tempCtx.save(); if (element.type === 'text') { let fontStyle = ''; if (element.italic) fontStyle += 'italic '; if (element.bold) fontStyle += 'bold '; tempCtx.font = `${fontStyle}${element.fontSize}px ${element.fontFamily}`; tempCtx.fillStyle = element.color; tempCtx.textAlign = element.textAlign; tempCtx.textBaseline = 'top'; let textX = element.x; if (element.textAlign === 'center') { textX = element.x + element.width / 2; } else if (element.textAlign === 'right') { textX = element.x + element.width; } tempCtx.fillText(element.text, textX, element.y); if (element.underline) { tempCtx.beginPath(); tempCtx.moveTo(element.x, element.y + element.height - 2); tempCtx.lineTo(element.x + element.width, element.y + element.height - 2); tempCtx.strokeStyle = element.color; tempCtx.lineWidth = 1; tempCtx.stroke(); } } else if (element.type === 'image') { tempCtx.drawImage( element.image, element.x, element.y, element.width, element.height ); } tempCtx.restore(); }); // Convert to blob and download tempCanvas.toBlob((blob) => { const url = URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; a.download = filename; document.body.appendChild(a); a.click(); document.body.removeChild(a); URL.revokeObjectURL(url); }, mimeType, 0.9); } getResizeHandle(mousePos, element) { const handleSize = 8; const handles = [ {name: 'nw', x: element.x - handleSize / 2, y: element.y - handleSize / 2}, {name: 'ne', x: element.x + element.width - handleSize / 2, y: element.y - handleSize / 2}, {name: 'sw', x: element.x - handleSize / 2, y: element.y + element.height - handleSize / 2}, {name: 'se', x: element.x + element.width - handleSize / 2, y: element.y + element.height - handleSize / 2}, {name: 'n', x: element.x + element.width / 2 - handleSize / 2, y: element.y - handleSize / 2}, {name: 's', x: element.x + element.width / 2 - handleSize / 2, y: element.y + element.height - handleSize / 2}, {name: 'w', x: element.x - handleSize / 2, y: element.y + element.height / 2 - handleSize / 2}, {name: 'e', x: element.x + element.width - handleSize / 2, y: element.y + element.height / 2 - handleSize / 2} ]; for (const handle of handles) { if (mousePos.x >= handle.x && mousePos.x <= handle.x + handleSize && mousePos.y >= handle.y && mousePos.y <= handle.y + handleSize) { return handle.name; } } return null; } getResizeCursor(handle) { const cursors = { 'nw': 'nw-resize', 'ne': 'ne-resize', 'sw': 'sw-resize', 'se': 'se-resize', 'n': 'n-resize', 's': 's-resize', 'w': 'w-resize', 'e': 'e-resize' }; return cursors[handle] || 'default'; } handleResize(mousePos) { const element = this.selectedElement; const startX = this.dragOffset.x; const startY = this.dragOffset.y; const deltaX = mousePos.x - startX; const deltaY = mousePos.y - startY; const minSize = element.type === 'text' ? (element.minWidth || 50) : 20; const minHeight = element.type === 'text' ? (element.minHeight || 20) : 20; const originalX = element.x; const originalY = element.y; const originalWidth = element.width; const originalHeight = element.height; // Mark text elements as manually resized if (element.type === 'text') { element.manuallyResized = true; } // For images, maintain aspect ratio when using corner handles const isCornerHandle = ['nw', 'ne', 'sw', 'se'].includes(this.resizeHandle); const maintainAspectRatio = element.type === 'image' && isCornerHandle; // Store original font size for text elements const originalFontSize = element.type === 'text' ? element.fontSize : null; switch (this.resizeHandle) { case 'se': if (maintainAspectRatio) { const scale = Math.max( (originalWidth + deltaX) / originalWidth, (originalHeight + deltaY) / originalHeight ); element.width = Math.max(minSize, originalWidth * scale); element.height = Math.max(minHeight, originalHeight * scale); } else { element.width = Math.max(minSize, originalWidth + deltaX); element.height = Math.max(minHeight, originalHeight + deltaY); } break; case 'sw': if (maintainAspectRatio) { const scale = Math.max( (originalWidth - deltaX) / originalWidth, (originalHeight + deltaY) / originalHeight ); const newWidth = Math.max(minSize, originalWidth * scale); const newHeight = Math.max(minHeight, originalHeight * scale); element.x = originalX + originalWidth - newWidth; element.width = newWidth; element.height = newHeight; } else { const newWidth = Math.max(minSize, originalWidth - deltaX); element.x = originalX + originalWidth - newWidth; element.width = newWidth; element.height = Math.max(minHeight, originalHeight + deltaY); } break; case 'ne': if (maintainAspectRatio) { const scale = Math.max( (originalWidth + deltaX) / originalWidth, (originalHeight - deltaY) / originalHeight ); const newWidth = Math.max(minSize, originalWidth * scale); const newHeight = Math.max(minHeight, originalHeight * scale); element.y = originalY + originalHeight - newHeight; element.width = newWidth; element.height = newHeight; } else { element.width = Math.max(minSize, originalWidth + deltaX); const newHeight = Math.max(minHeight, originalHeight - deltaY); element.y = originalY + originalHeight - newHeight; element.height = newHeight; } break; case 'nw': if (maintainAspectRatio) { const scale = Math.max( (originalWidth - deltaX) / originalWidth, (originalHeight - deltaY) / originalHeight ); const newW = Math.max(minSize, originalWidth * scale); const newH = Math.max(minHeight, originalHeight * scale); element.x = originalX + originalWidth - newW; element.y = originalY + originalHeight - newH; element.width = newW; element.height = newH; } else { const newW = Math.max(minSize, originalWidth - deltaX); const newH = Math.max(minHeight, originalHeight - deltaY); element.x = originalX + originalWidth - newW; element.y = originalY + originalHeight - newH; element.width = newW; element.height = newH; } break; case 'n': const newHeightN = Math.max(minHeight, originalHeight - deltaY); element.y = originalY + originalHeight - newHeightN; element.height = newHeightN; break; case 's': element.height = Math.max(minHeight, originalHeight + deltaY); break; case 'w': const newWidthW = Math.max(minSize, originalWidth - deltaX); element.x = originalX + originalWidth - newWidthW; element.width = newWidthW; break; case 'e': element.width = Math.max(minSize, originalWidth + deltaX); break; } // Keep element within canvas bounds if (element.x < 0) { element.width += element.x; element.x = 0; } if (element.y < 0) { element.height += element.y; element.y = 0; } if (element.x + element.width > this.canvas.width) { element.width = this.canvas.width - element.x; } if (element.y + element.height > this.canvas.height) { element.height = this.canvas.height - element.y; } // For text elements, update font size proportionally when using corner resize if (element.type === 'text' && isCornerHandle && originalFontSize) { const widthScale = element.width / originalWidth; const heightScale = element.height / originalHeight; const scale = Math.min(widthScale, heightScale); // Calculate new font size const newFontSize = Math.max(12, Math.min(200, Math.round(originalFontSize * scale))); element.fontSize = newFontSize; // Update UI controls const fontSizeSlider = document.getElementById('fontSize'); const fontSizeValue = document.getElementById('fontSizeValue'); if (fontSizeSlider && fontSizeValue) { fontSizeSlider.value = newFontSize; fontSizeValue.textContent = newFontSize + 'px'; } } // Update drag offset for smooth resizing this.dragOffset.x = mousePos.x; this.dragOffset.y = mousePos.y; this.render(); } } // Initialize the application when DOM is loaded document.addEventListener('DOMContentLoaded', () => { new BannerDesigner(); });