import { Canvas, Image, Rect, Line, IText } from 'fabric';
export class ScreenshotAnnotator {
constructor() {
this.canvas = null;
this.currentTool = 'brush';
this.currentColor = '#ff0000';
this.currentSize = 5;
this.drawingMode = true;
}
initialize(imageUrl, container) {
console.log('Initializing annotator with container:', container);
return new Promise((resolve, reject) => {
try {
// Create canvas container
const canvasContainer = document.createElement('div');
canvasContainer.className = 'canvas-container';
// Create toolbar
const toolbar = document.createElement('div');
toolbar.className = 'annotation-toolbar';
toolbar.innerHTML = `
`;
// Create canvas wrapper for scrolling
const canvasWrapper = document.createElement('div');
canvasWrapper.className = 'canvas-wrapper';
// Create canvas
const canvas = document.createElement('canvas');
canvas.style.margin = 'auto';
canvas.style.boxShadow = '0 2px 8px rgba(0,0,0,0.1)';
// Add elements to DOM
canvasWrapper.appendChild(canvas);
canvasContainer.appendChild(toolbar);
canvasContainer.appendChild(canvasWrapper);
container.appendChild(canvasContainer);
console.log('Creating Fabric canvas');
// Initialize Fabric canvas
this.canvas = new Canvas(canvas);
// Load image
console.log('Loading image:', imageUrl.substring(0, 100) + '...');
const img = new window.Image();
img.onerror = (error) => {
console.error('Error loading image:', error);
reject(error);
};
img.onload = () => {
console.log('Image loaded:', img.width, 'x', img.height);
try {
// Set canvas size to match window size while maintaining aspect ratio
const maxWidth = window.innerWidth - 40;
const maxHeight = window.innerHeight - 100;
const scale = Math.min(
maxWidth / img.width,
maxHeight / img.height
);
const width = img.width * scale;
const height = img.height * scale;
console.log('Setting canvas size:', width, 'x', height);
this.canvas.setWidth(width);
this.canvas.setHeight(height);
// Create Fabric image
console.log('Creating Fabric image');
Image.fromURL(imageUrl, (fabricImg) => {
try {
fabricImg.scale(scale);
fabricImg.selectable = false;
// Add image to canvas
this.canvas.add(fabricImg);
this.canvas.renderAll();
// Set up event listeners
this.setupEventListeners(toolbar);
console.log('Setup complete');
// Return promise that resolves when done button is clicked
document.getElementById('done-btn').addEventListener('click', () => {
try {
const dataUrl = this.canvas.toDataURL();
this.cleanup();
resolve(dataUrl);
} catch (error) {
console.error('Error getting data URL:', error);
reject(error);
}
});
} catch (error) {
console.error('Error setting up Fabric image:', error);
reject(error);
}
});
} catch (error) {
console.error('Error in image onload:', error);
reject(error);
}
};
img.src = imageUrl;
} catch (error) {
console.error('Error in initialize:', error);
reject(error);
}
});
}
setupEventListeners(toolbar) {
console.log('Setting up event listeners');
try {
// Tool buttons
const toolButtons = toolbar.querySelectorAll('.tool-btn');
toolButtons.forEach(button => {
button.addEventListener('click', () => {
toolButtons.forEach(btn => btn.classList.remove('active'));
button.classList.add('active');
this.currentTool = button.dataset.tool;
this.canvas.isDrawingMode = this.currentTool === 'brush';
});
});
// Color picker
const colorPicker = document.getElementById('color-picker');
colorPicker.addEventListener('change', (e) => {
this.currentColor = e.target.value;
if (this.canvas.isDrawingMode) {
this.canvas.freeDrawingBrush.color = this.currentColor;
}
});
// Size slider
const sizeSlider = document.getElementById('size-slider');
sizeSlider.addEventListener('input', (e) => {
this.currentSize = parseInt(e.target.value);
if (this.canvas.isDrawingMode) {
this.canvas.freeDrawingBrush.width = this.currentSize;
}
});
// Undo button
document.getElementById('undo-btn').addEventListener('click', () => {
const objects = this.canvas.getObjects();
if (objects.length > 1) { // Keep background image
this.canvas.remove(objects[objects.length - 1]);
}
});
// Clear button
document.getElementById('clear-btn').addEventListener('click', () => {
const objects = this.canvas.getObjects();
// Remove all objects except background image
for (let i = objects.length - 1; i > 0; i--) {
this.canvas.remove(objects[i]);
}
});
// Set initial brush settings
this.canvas.freeDrawingBrush.color = this.currentColor;
this.canvas.freeDrawingBrush.width = this.currentSize;
// Mouse down handler for shapes and text
this.canvas.on('mouse:down', (options) => {
if (this.canvas.isDrawingMode) return;
const pointer = this.canvas.getPointer(options.e);
const startX = pointer.x;
const startY = pointer.y;
switch (this.currentTool) {
case 'rectangle':
const rect = new Rect({
left: startX,
top: startY,
width: 0,
height: 0,
fill: 'transparent',
stroke: this.currentColor,
strokeWidth: this.currentSize
});
this.canvas.add(rect);
this.canvas.setActiveObject(rect);
break;
case 'arrow':
const line = new Line([startX, startY, startX, startY], {
stroke: this.currentColor,
strokeWidth: this.currentSize
});
this.canvas.add(line);
this.canvas.setActiveObject(line);
break;
case 'text':
const text = new IText('Type here...', {
left: startX,
top: startY,
fill: this.currentColor,
fontSize: this.currentSize * 3
});
this.canvas.add(text);
text.enterEditing();
text.selectAll();
break;
}
});
// Mouse move handler for shapes
this.canvas.on('mouse:move', (options) => {
if (!this.canvas.isDrawingMode && this.canvas.getActiveObject()) {
const pointer = this.canvas.getPointer(options.e);
const activeObj = this.canvas.getActiveObject();
if (this.currentTool === 'rectangle') {
const width = pointer.x - activeObj.left;
const height = pointer.y - activeObj.top;
activeObj.set({ width, height });
} else if (this.currentTool === 'arrow') {
const points = [activeObj.x1, activeObj.y1, pointer.x, pointer.y];
activeObj.set({ x2: pointer.x, y2: pointer.y });
}
this.canvas.renderAll();
}
});
// Mouse up handler
this.canvas.on('mouse:up', () => {
this.canvas.setActiveObject(null);
});
} catch (error) {
console.error('Error in setupEventListeners:', error);
throw error;
}
}
cleanup() {
try {
// Remove event listeners and clean up resources
if (this.canvas) {
this.canvas.dispose();
this.canvas = null;
}
} catch (error) {
console.error('Error in cleanup:', error);
}
}
}