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 = '<h3>✏️ แก้ไของค์ประกอบ</h3><button id="deleteElement" class="delete-btn">🗑️ ลบองค์ประกอบ</button>';
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();
});