class ScreenshotEditor { constructor(canvas) { console.log('Initializing ScreenshotEditor'); this.canvas = canvas; this.ctx = canvas.getContext('2d', { alpha: false }); this.isDrawing = false; this.drawingMode = 'pen'; this.drawColor = '#ff0000'; this.lineWidth = 2; this.startX = 0; this.startY = 0; this.lastX = 0; this.lastY = 0; this.undoStack = []; this.redoStack = []; this.shapes = []; this.selectedShape = null; this.isDragging = false; this.originalImage = null; this.currentState = null; this.scale = 1; this.paths = []; this.currentPath = []; this.setupEventListeners(); this.setupTools(); console.log('ScreenshotEditor initialized'); } async loadImage(dataUrl) { console.log('Loading image...'); return new Promise((resolve, reject) => { if (!dataUrl || typeof dataUrl !== 'string') { console.error('Invalid dataUrl:', dataUrl); reject(new Error('Invalid image data')); return; } const img = new Image(); img.onload = () => { console.log('Image loaded, dimensions:', img.width, 'x', img.height); this.originalImage = img; // Use device pixel ratio for better resolution const dpr = window.devicePixelRatio || 1; // Calculate dimensions to maintain aspect ratio and high resolution const maxWidth = Math.min(window.innerWidth * 0.98, img.width); // 98% of window width or original width const maxHeight = Math.min(window.innerHeight * 0.9, img.height); // 90% of window height or original height const aspectRatio = img.width / img.height; let newWidth = img.width; let newHeight = img.height; // Scale down if necessary while maintaining aspect ratio if (newWidth > maxWidth || newHeight > maxHeight) { if (maxWidth / aspectRatio <= maxHeight) { newWidth = maxWidth; newHeight = maxWidth / aspectRatio; } else { newHeight = maxHeight; newWidth = maxHeight * aspectRatio; } } // Set canvas size accounting for device pixel ratio this.canvas.style.width = newWidth + 'px'; this.canvas.style.height = newHeight + 'px'; this.canvas.width = newWidth * dpr; this.canvas.height = newHeight * dpr; // Scale context for high DPI display this.ctx.scale(dpr, dpr); // Calculate scale for proper coordinate mapping this.scale = newWidth / img.width; // Enable image smoothing this.ctx.imageSmoothingEnabled = true; this.ctx.imageSmoothingQuality = 'high'; // Clear canvas and draw image with crisp rendering this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height); this.ctx.drawImage(img, 0, 0, newWidth, newHeight); // Save initial state this.saveState(); resolve(); }; img.onerror = (error) => { console.error('Error loading image:', error); reject(new Error('Failed to load image')); }; console.log('Setting image source...'); img.src = dataUrl; }); } setupEventListeners() { this.canvas.addEventListener('mousedown', this.startDrawing.bind(this)); this.canvas.addEventListener('mousemove', this.draw.bind(this)); this.canvas.addEventListener('mouseup', this.stopDrawing.bind(this)); this.canvas.addEventListener('mouseleave', this.stopDrawing.bind(this)); // Setup keyboard shortcuts document.addEventListener('keydown', (e) => { if (e.ctrlKey && e.key === 'z') { this.undo(); } else if (e.ctrlKey && e.key === 'y') { this.redo(); } }); } setupTools() { const tools = ['select', 'move', 'pen', 'rectangle', 'arrow', 'crop']; tools.forEach(tool => { const button = document.getElementById(`${tool}-tool`); if (button) { button.addEventListener('click', () => { this.setDrawingMode(tool); // Remove active class from all tools tools.forEach(t => { document.getElementById(`${t}-tool`)?.classList.remove('active'); }); // Add active class to selected tool button.classList.add('active'); }); } }); // Setup color picker const colorPicker = document.getElementById('color-picker'); if (colorPicker) { colorPicker.addEventListener('change', (e) => { this.drawColor = e.target.value; }); } // Setup line width const lineWidth = document.getElementById('line-width'); if (lineWidth) { lineWidth.addEventListener('change', (e) => { this.lineWidth = parseInt(e.target.value); }); } // Setup undo/redo buttons const undoBtn = document.getElementById('undo-btn'); const redoBtn = document.getElementById('redo-btn'); if (undoBtn) undoBtn.addEventListener('click', () => this.undo()); if (redoBtn) redoBtn.addEventListener('click', () => this.redo()); } startDrawing(e) { const rect = this.canvas.getBoundingClientRect(); const dpr = window.devicePixelRatio || 1; this.isDrawing = true; // Calculate coordinates taking into account DPR and canvas scaling this.startX = (e.clientX - rect.left) * (this.canvas.width / (rect.width * dpr)); this.startY = (e.clientY - rect.top) * (this.canvas.height / (rect.height * dpr)); this.lastX = this.startX; this.lastY = this.startY; if (this.drawingMode === 'pen') { // Start a new path array for the current drawing this.currentPath = []; this.currentPath.push({ x: this.startX, y: this.startY }); this.paths.push(this.currentPath); } } draw(e) { if (!this.isDrawing) return; const rect = this.canvas.getBoundingClientRect(); const dpr = window.devicePixelRatio || 1; const currentX = (e.clientX - rect.left) * (this.canvas.width / (rect.width * dpr)); const currentY = (e.clientY - rect.top) * (this.canvas.height / (rect.height * dpr)); // Get the current state as an image const baseImage = new Image(); baseImage.src = this.currentState || this.canvas.toDataURL(); // Clear the canvas this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height); // Draw the current state if (this.currentState) { const img = new Image(); img.src = this.currentState; this.ctx.drawImage(img, 0, 0); } else { this.ctx.drawImage(this.originalImage, 0, 0, this.canvas.width / dpr, this.canvas.height / dpr); } // Set drawing styles this.ctx.strokeStyle = this.drawColor; this.ctx.lineWidth = this.lineWidth; this.ctx.lineCap = 'round'; this.ctx.lineJoin = 'round'; switch (this.drawingMode) { case 'pen': // Add point to current path this.currentPath.push({ x: currentX, y: currentY }); // Draw the entire current path this.ctx.beginPath(); this.ctx.moveTo(this.currentPath[0].x, this.currentPath[0].y); for (let i = 1; i < this.currentPath.length; i++) { const point = this.currentPath[i]; this.ctx.lineTo(point.x, point.y); } this.ctx.stroke(); break; case 'rectangle': this.ctx.beginPath(); this.ctx.rect( this.startX, this.startY, currentX - this.startX, currentY - this.startY ); this.ctx.stroke(); break; case 'arrow': this.drawArrow(this.ctx, this.startX, this.startY, currentX, currentY); break; } } drawArrow(ctx, fromX, fromY, toX, toY) { const headLength = 20; const angle = Math.atan2(toY - fromY, toX - fromX); // Draw the line ctx.beginPath(); ctx.moveTo(fromX, fromY); ctx.lineTo(toX, toY); ctx.stroke(); // Draw the arrow head ctx.beginPath(); ctx.moveTo(toX, toY); ctx.lineTo( toX - headLength * Math.cos(angle - Math.PI / 6), toY - headLength * Math.sin(angle - Math.PI / 6) ); ctx.moveTo(toX, toY); ctx.lineTo( toX - headLength * Math.cos(angle + Math.PI / 6), toY - headLength * Math.sin(angle + Math.PI / 6) ); ctx.stroke(); } stopDrawing() { if (this.isDrawing) { this.isDrawing = false; // Save the current state with the completed drawing this.currentState = this.canvas.toDataURL('image/png'); this.saveState(); // Reset current path this.currentPath = []; } } undo() { if (this.undoStack.length > 1) { this.redoStack.push(this.undoStack.pop()); // Move current state to redo stack const previousState = this.undoStack[this.undoStack.length - 1]; this.loadStateImage(previousState); } } redo() { if (this.redoStack.length > 0) { const nextState = this.redoStack.pop(); this.undoStack.push(nextState); this.loadStateImage(nextState); } } loadStateImage(dataUrl) { const img = new Image(); img.onload = () => { this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height); this.ctx.drawImage(img, 0, 0); }; img.src = dataUrl; } getImageData() { // Return the current state with drawings return this.currentState || this.canvas.toDataURL('image/png'); } setDrawingMode(mode) { this.drawingMode = mode; this.canvas.style.cursor = mode === 'crop' ? 'crosshair' : 'default'; } saveState() { const imageData = this.canvas.toDataURL('image/png'); this.undoStack.push(imageData); this.redoStack = []; // Clear redo stack when new state is saved } redrawCanvas() { if (!this.canvas.width || !this.canvas.height) { console.warn('Canvas has no dimensions, skipping redraw'); return; } // Clear the canvas this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height); // Draw the current state if it exists, otherwise draw the original image if (this.currentState) { const img = new Image(); img.src = this.currentState; this.ctx.drawImage(img, 0, 0); } else if (this.originalImage) { this.ctx.drawImage( this.originalImage, 0, 0, this.canvas.width, this.canvas.height ); } } } window.ScreenshotEditor = ScreenshotEditor;