canvas-website/src/lib/screenshotService.ts

156 lines
4.5 KiB
TypeScript

import { Editor } from 'tldraw';
import { exportToBlob } from 'tldraw';
export interface BoardScreenshot {
slug: string;
dataUrl: string;
timestamp: number;
}
/**
* Generates a screenshot of the current canvas state
*/
export const generateCanvasScreenshot = async (editor: Editor): Promise<string | null> => {
try {
// Get all shapes on the current page
const shapes = editor.getCurrentPageShapes();
console.log('Found shapes:', shapes.length);
if (shapes.length === 0) {
console.log('No shapes found, no screenshot generated');
return null;
}
// Get all shape IDs for export
const allShapeIds = shapes.map(shape => shape.id);
console.log('Exporting all shapes:', allShapeIds.length);
// Calculate bounds of all shapes to fit everything in view
const bounds = editor.getCurrentPageBounds();
console.log('Canvas bounds:', bounds);
// Use Tldraw's export functionality to get a blob with all content
const blob = await exportToBlob({
editor,
ids: allShapeIds,
format: "png",
opts: {
scale: 0.5, // Reduced scale to make image smaller
background: true,
padding: 20, // Increased padding to show full canvas
preserveAspectRatio: "xMidYMid meet",
bounds: bounds, // Export the entire canvas bounds
},
});
if (!blob) {
console.warn('Failed to export blob, no screenshot generated');
return null;
}
// Convert blob to data URL with compression
const reader = new FileReader();
const dataUrl = await new Promise<string>((resolve, reject) => {
reader.onload = () => {
// Create a canvas to compress the image
const img = new Image();
img.onload = () => {
const canvas = document.createElement('canvas');
const ctx = canvas.getContext('2d');
if (!ctx) {
reject(new Error('Could not get 2D context'));
return;
}
// Set canvas size for compression (max 400x300 for dashboard)
canvas.width = 400;
canvas.height = 300;
// Draw and compress the image
ctx.drawImage(img, 0, 0, 400, 300);
const compressedDataUrl = canvas.toDataURL('image/jpeg', 0.6); // Use JPEG with 60% quality
resolve(compressedDataUrl);
};
img.onerror = reject;
img.src = reader.result as string;
};
reader.onerror = reject;
reader.readAsDataURL(blob);
});
console.log('Successfully exported board to data URL');
console.log('Screenshot data URL:', dataUrl);
return dataUrl;
} catch (error) {
console.error('Error generating screenshot:', error);
return null;
}
};
/**
* Stores a screenshot for a board
*/
export const storeBoardScreenshot = (slug: string, dataUrl: string): void => {
try {
const screenshot: BoardScreenshot = {
slug,
dataUrl,
timestamp: Date.now(),
};
localStorage.setItem(`board_screenshot_${slug}`, JSON.stringify(screenshot));
} catch (error) {
console.error('Error storing screenshot:', error);
}
};
/**
* Retrieves a stored screenshot for a board
*/
export const getBoardScreenshot = (slug: string): BoardScreenshot | null => {
try {
const stored = localStorage.getItem(`board_screenshot_${slug}`);
if (!stored) return null;
return JSON.parse(stored);
} catch (error) {
console.error('Error retrieving screenshot:', error);
return null;
}
};
/**
* Removes a stored screenshot for a board
*/
export const removeBoardScreenshot = (slug: string): void => {
try {
localStorage.removeItem(`board_screenshot_${slug}`);
} catch (error) {
console.error('Error removing screenshot:', error);
}
};
/**
* Checks if a screenshot exists for a board
*/
export const hasBoardScreenshot = (slug: string): boolean => {
return getBoardScreenshot(slug) !== null;
};
/**
* Generates and stores a screenshot for the current board
* This should be called when the board content changes significantly
*/
export const captureBoardScreenshot = async (editor: Editor, slug: string): Promise<void> => {
console.log('Starting screenshot capture for:', slug);
const dataUrl = await generateCanvasScreenshot(editor);
if (dataUrl) {
console.log('Screenshot generated successfully for:', slug);
storeBoardScreenshot(slug, dataUrl);
console.log('Screenshot stored for:', slug);
} else {
console.warn('Failed to generate screenshot for:', slug);
}
};