387 lines
13 KiB
JavaScript
387 lines
13 KiB
JavaScript
/******/ (() => { // webpackBootstrap
|
|
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.originalImage = null;
|
|
this.currentState = null;
|
|
this.scale = 1;
|
|
this.paths = [];
|
|
this.currentPath = [];
|
|
this.cropStart = null;
|
|
this.cropEnd = null;
|
|
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 = ['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 => {
|
|
var _document$getElementB;
|
|
(_document$getElementB = document.getElementById(`${t}-tool`)) === null || _document$getElementB === void 0 || _document$getElementB.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 scale and DPR
|
|
const x = (e.clientX - rect.left) / rect.width * this.canvas.width / dpr;
|
|
const y = (e.clientY - rect.top) / rect.height * this.canvas.height / dpr;
|
|
this.startX = x;
|
|
this.startY = y;
|
|
this.lastX = x;
|
|
this.lastY = y;
|
|
if (this.drawingMode === 'crop') {
|
|
this.cropStart = {
|
|
x,
|
|
y
|
|
};
|
|
this.cropEnd = null;
|
|
} else if (this.drawingMode === 'pen') {
|
|
this.currentPath = [];
|
|
this.currentPath.push({
|
|
x,
|
|
y
|
|
});
|
|
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) / rect.width * this.canvas.width / dpr;
|
|
const currentY = (e.clientY - rect.top) / rect.height * this.canvas.height / dpr;
|
|
|
|
// 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, this.canvas.width / dpr, this.canvas.height / dpr);
|
|
} 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':
|
|
this.currentPath.push({
|
|
x: currentX,
|
|
y: currentY
|
|
});
|
|
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;
|
|
case 'crop':
|
|
// Draw crop rectangle
|
|
this.ctx.save();
|
|
this.ctx.strokeStyle = '#ffffff';
|
|
this.ctx.lineWidth = 2;
|
|
this.ctx.setLineDash([5, 5]);
|
|
this.ctx.beginPath();
|
|
this.ctx.rect(this.startX, this.startY, currentX - this.startX, currentY - this.startY);
|
|
this.ctx.stroke();
|
|
|
|
// Draw semi-transparent overlay
|
|
this.ctx.fillStyle = 'rgba(0, 0, 0, 0.5)';
|
|
this.ctx.fillRect(0, 0, this.canvas.width, this.startY); // Top
|
|
this.ctx.fillRect(0, currentY, this.canvas.width, this.canvas.height - currentY); // Bottom
|
|
this.ctx.fillRect(0, this.startY, this.startX, currentY - this.startY); // Left
|
|
this.ctx.fillRect(currentX, this.startY, this.canvas.width - currentX, currentY - this.startY); // Right
|
|
|
|
this.ctx.restore();
|
|
this.cropEnd = {
|
|
x: currentX,
|
|
y: 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) return;
|
|
this.isDrawing = false;
|
|
if (this.drawingMode === 'crop' && this.cropStart && this.cropEnd) {
|
|
// Ensure positive width and height
|
|
const x = Math.min(this.cropStart.x, this.cropEnd.x);
|
|
const y = Math.min(this.cropStart.y, this.cropEnd.y);
|
|
const width = Math.abs(this.cropEnd.x - this.cropStart.x);
|
|
const height = Math.abs(this.cropEnd.y - this.cropStart.y);
|
|
|
|
// Create a temporary canvas for cropping
|
|
const tempCanvas = document.createElement('canvas');
|
|
const tempCtx = tempCanvas.getContext('2d');
|
|
tempCanvas.width = width;
|
|
tempCanvas.height = height;
|
|
|
|
// Draw the cropped portion
|
|
if (this.currentState) {
|
|
const img = new Image();
|
|
img.onload = () => {
|
|
tempCtx.drawImage(img, -x, -y);
|
|
|
|
// Update canvas size
|
|
this.canvas.width = width;
|
|
this.canvas.height = height;
|
|
|
|
// Draw cropped image
|
|
this.ctx.drawImage(tempCanvas, 0, 0);
|
|
|
|
// Save the cropped state
|
|
this.currentState = this.canvas.toDataURL('image/png');
|
|
this.saveState();
|
|
};
|
|
img.src = this.currentState;
|
|
} else if (this.originalImage) {
|
|
tempCtx.drawImage(this.originalImage, -x, -y);
|
|
|
|
// Update canvas size
|
|
this.canvas.width = width;
|
|
this.canvas.height = height;
|
|
|
|
// Draw cropped image
|
|
this.ctx.drawImage(tempCanvas, 0, 0);
|
|
|
|
// Save the cropped state
|
|
this.currentState = this.canvas.toDataURL('image/png');
|
|
this.saveState();
|
|
}
|
|
|
|
// Reset crop points
|
|
this.cropStart = null;
|
|
this.cropEnd = null;
|
|
} else {
|
|
// For other tools, save the current state
|
|
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();
|
|
const dpr = window.devicePixelRatio || 1;
|
|
img.onload = () => {
|
|
this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height);
|
|
this.ctx.drawImage(img, 0, 0, this.canvas.width / dpr, this.canvas.height / dpr);
|
|
this.currentState = dataUrl;
|
|
};
|
|
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;
|
|
}
|
|
const dpr = window.devicePixelRatio || 1;
|
|
|
|
// 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, this.canvas.width / dpr, this.canvas.height / dpr);
|
|
} else if (this.originalImage) {
|
|
this.ctx.drawImage(this.originalImage, 0, 0, this.canvas.width / dpr, this.canvas.height / dpr);
|
|
}
|
|
}
|
|
}
|
|
window.ScreenshotEditor = ScreenshotEditor;
|
|
/******/ })()
|
|
; |