192 lines
5.4 KiB
TypeScript
192 lines
5.4 KiB
TypeScript
/**
|
|
* Capture Canvas Preview
|
|
*
|
|
* Captures the real simulator canvas as a preview image for examples
|
|
*/
|
|
|
|
import type { ExampleProject } from '../data/examples';
|
|
|
|
/**
|
|
* Wait for all wokwi-elements components to be defined
|
|
*/
|
|
async function waitForComponents(): Promise<void> {
|
|
const componentTags = [
|
|
'wokwi-arduino-uno',
|
|
'wokwi-led',
|
|
'wokwi-rgb-led',
|
|
'wokwi-pushbutton',
|
|
];
|
|
|
|
const promises = componentTags.map((tag) => {
|
|
if (customElements.get(tag)) {
|
|
return Promise.resolve();
|
|
}
|
|
return customElements.whenDefined(tag);
|
|
});
|
|
|
|
await Promise.all(promises);
|
|
}
|
|
|
|
/**
|
|
* Create a temporary canvas with the example circuit
|
|
*/
|
|
async function createPreviewCanvas(example: ExampleProject): Promise<HTMLCanvasElement> {
|
|
// Wait for components to be loaded
|
|
await waitForComponents();
|
|
|
|
// Create temporary container
|
|
const container = document.createElement('div');
|
|
container.style.position = 'absolute';
|
|
container.style.left = '-10000px';
|
|
container.style.width = '800px';
|
|
container.style.height = '600px';
|
|
container.style.backgroundColor = '#1e1e1e';
|
|
document.body.appendChild(container);
|
|
|
|
// Create SVG for rendering
|
|
const svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
|
|
svg.setAttribute('width', '800');
|
|
svg.setAttribute('height', '600');
|
|
svg.style.backgroundColor = '#1e1e1e';
|
|
container.appendChild(svg);
|
|
|
|
// Calculate bounds for all components
|
|
let minX = Infinity,
|
|
maxX = -Infinity,
|
|
minY = Infinity,
|
|
maxY = -Infinity;
|
|
|
|
example.components.forEach((comp) => {
|
|
minX = Math.min(minX, comp.x);
|
|
maxX = Math.max(maxX, comp.x + 150);
|
|
minY = Math.min(minY, comp.y);
|
|
maxY = Math.max(maxY, comp.y + 150);
|
|
});
|
|
|
|
// Add padding
|
|
const padding = 40;
|
|
minX -= padding;
|
|
minY -= padding;
|
|
maxX += padding;
|
|
maxY += padding;
|
|
|
|
// Calculate scale to fit
|
|
const contentWidth = maxX - minX;
|
|
const contentHeight = maxY - minY;
|
|
const scaleX = 760 / contentWidth;
|
|
const scaleY = 560 / contentHeight;
|
|
const scale = Math.min(scaleX, scaleY, 1);
|
|
|
|
// Center offset
|
|
const offsetX = (800 - contentWidth * scale) / 2 - minX * scale;
|
|
const offsetY = (600 - contentHeight * scale) / 2 - minY * scale;
|
|
|
|
// Create group for all components
|
|
const group = document.createElementNS('http://www.w3.org/2000/svg', 'g');
|
|
group.setAttribute('transform', `translate(${offsetX}, ${offsetY}) scale(${scale})`);
|
|
svg.appendChild(group);
|
|
|
|
// Render each component as foreignObject
|
|
for (const comp of example.components) {
|
|
const foreignObject = document.createElementNS('http://www.w3.org/2000/svg', 'foreignObject');
|
|
foreignObject.setAttribute('x', comp.x.toString());
|
|
foreignObject.setAttribute('y', comp.y.toString());
|
|
foreignObject.setAttribute('width', '150');
|
|
foreignObject.setAttribute('height', '150');
|
|
|
|
// Create the actual wokwi element
|
|
const element = document.createElement(comp.type);
|
|
|
|
// Set properties
|
|
Object.entries(comp.properties).forEach(([key, value]) => {
|
|
if (key !== 'state') {
|
|
element.setAttribute(key, String(value));
|
|
}
|
|
});
|
|
|
|
foreignObject.appendChild(element);
|
|
group.appendChild(foreignObject);
|
|
}
|
|
|
|
// Draw wires
|
|
example.wires.forEach((wire) => {
|
|
const startComp = example.components.find((c) => c.id === wire.start.componentId);
|
|
const endComp = example.components.find((c) => c.id === wire.end.componentId);
|
|
|
|
if (startComp && endComp) {
|
|
const line = document.createElementNS('http://www.w3.org/2000/svg', 'line');
|
|
line.setAttribute('x1', (startComp.x + 75).toString());
|
|
line.setAttribute('y1', (startComp.y + 75).toString());
|
|
line.setAttribute('x2', (endComp.x + 75).toString());
|
|
line.setAttribute('y2', (endComp.y + 75).toString());
|
|
line.setAttribute('stroke', wire.color);
|
|
line.setAttribute('stroke-width', '3');
|
|
group.appendChild(line);
|
|
}
|
|
});
|
|
|
|
// Wait for rendering
|
|
await new Promise((resolve) => setTimeout(resolve, 500));
|
|
|
|
// Convert SVG to canvas
|
|
const canvas = document.createElement('canvas');
|
|
canvas.width = 800;
|
|
canvas.height = 600;
|
|
const ctx = canvas.getContext('2d')!;
|
|
|
|
// Draw background
|
|
ctx.fillStyle = '#1e1e1e';
|
|
ctx.fillRect(0, 0, 800, 600);
|
|
|
|
// Convert SVG to image
|
|
const svgData = new XMLSerializer().serializeToString(svg);
|
|
const img = new Image();
|
|
const svgBlob = new Blob([svgData], { type: 'image/svg+xml;charset=utf-8' });
|
|
const url = URL.createObjectURL(svgBlob);
|
|
|
|
await new Promise((resolve, reject) => {
|
|
img.onload = resolve;
|
|
img.onerror = reject;
|
|
img.src = url;
|
|
});
|
|
|
|
ctx.drawImage(img, 0, 0);
|
|
URL.revokeObjectURL(url);
|
|
|
|
// Cleanup
|
|
document.body.removeChild(container);
|
|
|
|
return canvas;
|
|
}
|
|
|
|
/**
|
|
* Generate a preview image data URL for an example
|
|
*/
|
|
export async function generateCanvasPreview(example: ExampleProject): Promise<string> {
|
|
try {
|
|
const canvas = await createPreviewCanvas(example);
|
|
return canvas.toDataURL('image/png');
|
|
} catch (error) {
|
|
console.error('Failed to generate preview for', example.id, error);
|
|
return '';
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Generate previews for all examples and return a map
|
|
*/
|
|
export async function generateAllPreviews(
|
|
examples: ExampleProject[]
|
|
): Promise<Map<string, string>> {
|
|
const previews = new Map<string, string>();
|
|
|
|
for (const example of examples) {
|
|
const preview = await generateCanvasPreview(example);
|
|
if (preview) {
|
|
previews.set(example.id, preview);
|
|
}
|
|
}
|
|
|
|
return previews;
|
|
}
|