Andromeda provides a comprehensive, GPU-accelerated Canvas API for creating 2D graphics, drawings, and visualizations. The API is powered by the WGPU backend for hardware acceleration and is based on the standard HTML5 Canvas specification.
Overview
The Canvas API allows you to:
- Draw shapes, lines, and text with hardware acceleration
- Render images and apply transformations
- Create complex graphics and visualizations with linear gradients
- Export graphics to PNG format
- Leverage GPU performance for intensive drawing operations
Creating a Canvas
OffscreenCanvas
Create a canvas that renders off-screen:
const canvas = new OffscreenCanvas(width, height);
const ctx = canvas.getContext("2d");
Parameters:
width
- Canvas width in pixelsheight
- Canvas height in pixels
Example:
// Create a 800x600 canvas
const canvas = new OffscreenCanvas(800, 600);
const ctx = canvas.getContext("2d");
if (!ctx) {
throw new Error("Failed to get 2D context");
}
Drawing Context
Getting the Context
const ctx = canvas.getContext("2d");
The context provides all drawing methods and properties.
Basic Shapes
Rectangles
// Filled rectangle
ctx.fillStyle = "#ff0000";
ctx.fillRect(x, y, width, height);
// Outlined rectangle
ctx.strokeStyle = "#00ff00";
ctx.lineWidth = 2;
ctx.strokeRect(x, y, width, height);
// Clear rectangle (make transparent)
ctx.clearRect(x, y, width, height);
Circles and Arcs
// Circle
ctx.beginPath();
ctx.arc(centerX, centerY, radius, 0, Math.PI * 2);
ctx.fillStyle = "#0000ff";
ctx.fill();
// Arc
ctx.beginPath();
ctx.arc(centerX, centerY, radius, startAngle, endAngle);
ctx.strokeStyle = "#ff00ff";
ctx.stroke();
Custom Paths
ctx.beginPath();
ctx.moveTo(50, 50); // Move to starting point
ctx.lineTo(150, 50); // Draw line to point
ctx.lineTo(100, 150); // Draw line to another point
ctx.closePath(); // Close the path
ctx.fillStyle = "#ffff00";
ctx.fill();
Advanced Path Methods
Quadratic Curves
// Quadratic Bézier curve
ctx.beginPath();
ctx.moveTo(20, 20);
ctx.quadraticCurveTo(100, 100, 200, 20); // Control point and end point
ctx.strokeStyle = "#ff0000";
ctx.stroke();
// Multiple curves for smooth paths
ctx.beginPath();
ctx.moveTo(50, 100);
ctx.quadraticCurveTo(100, 50, 150, 100);
ctx.quadraticCurveTo(200, 150, 250, 100);
ctx.strokeStyle = "#00ff00";
ctx.lineWidth = 3;
ctx.stroke();
Ellipses
// Complete ellipse
ctx.beginPath();
ctx.ellipse(100, 100, 50, 30, 0, 0, Math.PI * 2); // x, y, radiusX, radiusY, rotation, startAngle, endAngle
ctx.fillStyle = "#0000ff";
ctx.fill();
// Rotated ellipse
ctx.beginPath();
ctx.ellipse(200, 200, 60, 20, Math.PI / 4, 0, Math.PI * 2); // 45-degree rotation
ctx.strokeStyle = "#ff00ff";
ctx.lineWidth = 2;
ctx.stroke();
// Elliptical arc
ctx.beginPath();
ctx.ellipse(300, 100, 40, 25, 0, 0, Math.PI); // Half ellipse
ctx.fillStyle = "#ffff00";
ctx.fill();
Rounded Rectangles
// Basic rounded rectangle
ctx.beginPath();
ctx.roundRect(50, 50, 100, 80, 10); // x, y, width, height, radius
ctx.fillStyle = "#ff6b6b";
ctx.fill();
// Rounded rectangle with different corner radii
ctx.beginPath();
ctx.roundRect(200, 50, 120, 80, [20, 10, 5, 15]); // Different radius for each corner
ctx.strokeStyle = "#4ecdc4";
ctx.lineWidth = 3;
ctx.stroke();
// Rounded rectangle with stroke and fill
ctx.beginPath();
ctx.roundRect(50, 200, 150, 60, 25);
ctx.fillStyle = "#45b7d1";
ctx.fill();
ctx.strokeStyle = "#2c3e50";
ctx.lineWidth = 2;
ctx.stroke();
Colors and Styles
Fill and Stroke Colors
// Solid colors
ctx.fillStyle = "#ff0000"; // Hex
ctx.fillStyle = "rgb(255, 0, 0)"; // RGB
ctx.fillStyle = "rgba(255, 0, 0, 0.5)"; // RGBA
ctx.fillStyle = "red"; // Named color
// Apply to shapes
ctx.fillRect(10, 10, 100, 100);
Gradients
Andromeda supports both linear and radial gradients with hardware acceleration:
// Linear gradient
const gradient = ctx.createLinearGradient(x0, y0, x1, y1);
gradient.addColorStop(0, "#ff0000");
gradient.addColorStop(0.5, "#00ff00");
gradient.addColorStop(1, "#0000ff");
ctx.fillStyle = gradient;
ctx.fillRect(0, 0, 200, 100);
// Advanced linear gradient with multiple stops
const advancedGradient = ctx.createLinearGradient(0, 0, 300, 0);
advancedGradient.addColorStop(0, "rgba(255, 0, 0, 1)");
advancedGradient.addColorStop(0.25, "rgba(255, 255, 0, 0.8)");
advancedGradient.addColorStop(0.5, "rgba(0, 255, 0, 0.6)");
advancedGradient.addColorStop(0.75, "rgba(0, 255, 255, 0.8)");
advancedGradient.addColorStop(1, "rgba(0, 0, 255, 1)");
ctx.fillStyle = advancedGradient;
ctx.fillRect(0, 100, 300, 50);
// Radial gradient
const radialGradient = ctx.createRadialGradient(100, 100, 0, 100, 100, 100);
radialGradient.addColorStop(0, "#fff");
radialGradient.addColorStop(1, "#000");
ctx.fillStyle = radialGradient;
ctx.fillRect(0, 0, 200, 200);
// Gradient for text
const textGradient = ctx.createLinearGradient(0, 0, 200, 0);
textGradient.addColorStop(0, "#ff6b6b");
textGradient.addColorStop(1, "#4ecdc4");
ctx.fillStyle = textGradient;
ctx.font = "48px Arial";
ctx.fillText("Gradient Text", 10, 50);
Hardware Acceleration: Linear gradients in Andromeda are GPU-accelerated using the WGPU backend, providing superior performance for complex gradient operations.
Line Styles
ctx.lineWidth = 5;
ctx.lineCap = "round"; // "butt", "round", "square"
ctx.lineJoin = "round"; // "miter", "round", "bevel"
ctx.strokeStyle = "#000";
Text Rendering
Basic Text
ctx.font = "24px Arial";
ctx.fillStyle = "#000";
ctx.fillText("Hello, World!", x, y);
// Outlined text
ctx.strokeStyle = "#ff0000";
ctx.strokeText("Outlined Text", x, y);
Text Properties
ctx.font = "bold 32px 'Times New Roman'";
ctx.textAlign = "center"; // "start", "end", "left", "right", "center"
ctx.textBaseline = "middle"; // "top", "hanging", "middle", "alphabetic", "ideographic", "bottom"
ctx.fillText("Centered Text", canvas.width / 2, canvas.height / 2);
Text Measurements
const text = "Measure me!";
const metrics = ctx.measureText(text);
console.log(`Text width: ${metrics.width}px`);
Transformations
Translation
ctx.save(); // Save current state
ctx.translate(100, 50); // Move origin
ctx.fillRect(0, 0, 50, 50); // Draw at new origin
ctx.restore(); // Restore previous state
Rotation
ctx.save();
ctx.translate(canvas.width / 2, canvas.height / 2); // Move to center
ctx.rotate(Math.PI / 4); // Rotate 45 degrees
ctx.fillRect(-25, -25, 50, 50); // Draw centered square
ctx.restore();
Scaling
ctx.save();
ctx.scale(2, 2); // Scale 2x
ctx.fillRect(10, 10, 50, 50); // Will appear as 100x100
ctx.restore();
Custom Transforms
// Matrix transformation: transform(a, b, c, d, e, f)
ctx.transform(1, 0.5, -0.5, 1, 30, 10);
ctx.fillRect(0, 0, 50, 50);
Clipping and Compositing
Clipping Paths
// Create clipping region
ctx.beginPath();
ctx.arc(100, 100, 50, 0, Math.PI * 2);
ctx.clip();
// Everything drawn now will be clipped to the circle
ctx.fillStyle = "red";
ctx.fillRect(0, 0, 200, 200); // Only the part inside circle shows
Global Composite Operations
ctx.globalCompositeOperation = "multiply";
// Other options: "source-over", "source-in", "source-out", "destination-over", etc.
ctx.fillStyle = "red";
ctx.fillRect(50, 50, 100, 100);
ctx.fillStyle = "blue";
ctx.fillRect(100, 100, 100, 100); // Will multiply with red
Global Alpha
ctx.globalAlpha = 0.5; // 50% transparency
ctx.fillStyle = "red";
ctx.fillRect(10, 10, 100, 100); // Semi-transparent
ctx.globalAlpha = 1.0; // Reset to opaque
Advanced Features
Shadows
ctx.shadowColor = "rgba(0, 0, 0, 0.5)";
ctx.shadowOffsetX = 5;
ctx.shadowOffsetY = 5;
ctx.shadowBlur = 10;
ctx.fillStyle = "blue";
ctx.fillRect(50, 50, 100, 100); // Rectangle with shadow
Image Data Manipulation
// Get image data
const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);
const data = imageData.data; // Uint8ClampedArray (RGBA values)
// Manipulate pixels (invert colors)
for (let i = 0; i < data.length; i += 4) {
data[i] = 255 - data[i]; // Red
data[i + 1] = 255 - data[i + 1]; // Green
data[i + 2] = 255 - data[i + 2]; // Blue
// data[i + 3] is alpha, leave unchanged
}
// Put modified data back
ctx.putImageData(imageData, 0, 0);
Patterns
// Create a pattern from another canvas
const patternCanvas = new OffscreenCanvas(20, 20);
const patternCtx = patternCanvas.getContext("2d")!;
// Draw pattern
patternCtx.fillStyle = "#ff0000";
patternCtx.fillRect(0, 0, 10, 10);
patternCtx.fillStyle = "#0000ff";
patternCtx.fillRect(10, 10, 10, 10);
// Use pattern
const pattern = ctx.createPattern(patternCanvas, "repeat");
ctx.fillStyle = pattern;
ctx.fillRect(0, 0, 200, 200);
Exporting Graphics
Save as PNG
// Render the canvas
canvas.render();
// Save to file
canvas.saveAsPng("output.png");
console.log("✅ Image saved as output.png");
Get Image Data
// Get raw RGBA pixel data
const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);
console.log(`Image data: ${imageData.width}x${imageData.height}`);
Practical Examples
Drawing a Chart
function drawBarChart(canvas: OffscreenCanvas, data: number[]) {
const ctx = canvas.getContext("2d")!;
// Clear canvas
ctx.fillStyle = "#ffffff";
ctx.fillRect(0, 0, canvas.width, canvas.height);
// Chart settings
const padding = 40;
const chartWidth = canvas.width - padding * 2;
const chartHeight = canvas.height - padding * 2;
const barWidth = chartWidth / data.length;
const maxValue = Math.max(...data);
// Draw bars
data.forEach((value, index) => {
const barHeight = (value / maxValue) * chartHeight;
const x = padding + index * barWidth;
const y = canvas.height - padding - barHeight;
// Bar
ctx.fillStyle = `hsl(${index * 40}, 70%, 50%)`;
ctx.fillRect(x, y, barWidth - 2, barHeight);
// Value label
ctx.fillStyle = "#000";
ctx.font = "12px Arial";
ctx.textAlign = "center";
ctx.fillText(value.toString(), x + barWidth / 2, y - 5);
});
// Title
ctx.fillStyle = "#000";
ctx.font = "bold 18px Arial";
ctx.textAlign = "center";
ctx.fillText("Sales Data", canvas.width / 2, 25);
}
// Usage
const canvas = new OffscreenCanvas(600, 400);
drawBarChart(canvas, [25, 40, 35, 60, 45, 55]);
canvas.render();
canvas.saveAsPng("chart.png");
Creating a Logo
function createLogo(canvas: OffscreenCanvas) {
const ctx = canvas.getContext("2d")!;
// Background
const gradient = ctx.createLinearGradient(0, 0, canvas.width, canvas.height);
gradient.addColorStop(0, "#667eea");
gradient.addColorStop(1, "#764ba2");
ctx.fillStyle = gradient;
ctx.fillRect(0, 0, canvas.width, canvas.height);
// Center circle
const centerX = canvas.width / 2;
const centerY = canvas.height / 2;
ctx.beginPath();
ctx.arc(centerX, centerY, 80, 0, Math.PI * 2);
ctx.fillStyle = "#ffffff";
ctx.fill();
// Inner design
ctx.beginPath();
ctx.arc(centerX, centerY, 60, 0, Math.PI * 2);
ctx.fillStyle = "#4c51bf";
ctx.fill();
// Text
ctx.fillStyle = "#ffffff";
ctx.font = "bold 24px Arial";
ctx.textAlign = "center";
ctx.textBaseline = "middle";
ctx.fillText("LOGO", centerX, centerY);
}
// Usage
const canvas = new OffscreenCanvas(300, 300);
createLogo(canvas);
canvas.render();
canvas.saveAsPng("logo.png");
Animated Visualization
function drawFrame(canvas: OffscreenCanvas, frame: number) {
const ctx = canvas.getContext("2d")!;
// Clear canvas
ctx.fillStyle = "#000";
ctx.fillRect(0, 0, canvas.width, canvas.height);
// Draw rotating elements
const centerX = canvas.width / 2;
const centerY = canvas.height / 2;
for (let i = 0; i < 8; i++) {
ctx.save();
// Position and rotate
ctx.translate(centerX, centerY);
ctx.rotate((frame * 0.02) + (i * Math.PI / 4));
ctx.translate(60, 0);
// Draw shape
ctx.beginPath();
ctx.arc(0, 0, 20, 0, Math.PI * 2);
ctx.fillStyle = `hsl(${(frame + i * 45) % 360}, 70%, 50%)`;
ctx.fill();
ctx.restore();
}
}
// Create animation frames
const canvas = new OffscreenCanvas(400, 400);
for (let frame = 0; frame < 60; frame++) {
drawFrame(canvas, frame);
canvas.render();
canvas.saveAsPng(`animation-${frame.toString().padStart(3, "0")}.png`);
}
Data Visualization
interface DataPoint {
x: number;
y: number;
label: string;
}
function drawScatterPlot(canvas: OffscreenCanvas, data: DataPoint[]) {
const ctx = canvas.getContext("2d")!;
// Settings
const padding = 60;
const plotWidth = canvas.width - padding * 2;
const plotHeight = canvas.height - padding * 2;
// Find data ranges
const xMin = Math.min(...data.map((d) => d.x));
const xMax = Math.max(...data.map((d) => d.x));
const yMin = Math.min(...data.map((d) => d.y));
const yMax = Math.max(...data.map((d) => d.y));
// Helper function to convert data coordinates to canvas coordinates
const toCanvasX = (x: number) =>
padding + ((x - xMin) / (xMax - xMin)) * plotWidth;
const toCanvasY = (y: number) =>
canvas.height - padding - ((y - yMin) / (yMax - yMin)) * plotHeight;
// Clear background
ctx.fillStyle = "#fff";
ctx.fillRect(0, 0, canvas.width, canvas.height);
// Draw axes
ctx.strokeStyle = "#000";
ctx.lineWidth = 2;
ctx.beginPath();
ctx.moveTo(padding, padding);
ctx.lineTo(padding, canvas.height - padding);
ctx.lineTo(canvas.width - padding, canvas.height - padding);
ctx.stroke();
// Draw data points
data.forEach((point, index) => {
const x = toCanvasX(point.x);
const y = toCanvasY(point.y);
// Point
ctx.beginPath();
ctx.arc(x, y, 6, 0, Math.PI * 2);
ctx.fillStyle = `hsl(${index * 30}, 70%, 50%)`;
ctx.fill();
// Label
ctx.fillStyle = "#000";
ctx.font = "10px Arial";
ctx.textAlign = "center";
ctx.fillText(point.label, x, y - 10);
});
// Axis labels
ctx.fillStyle = "#000";
ctx.font = "14px Arial";
ctx.textAlign = "center";
ctx.fillText("X Axis", canvas.width / 2, canvas.height - 10);
ctx.save();
ctx.translate(15, canvas.height / 2);
ctx.rotate(-Math.PI / 2);
ctx.fillText("Y Axis", 0, 0);
ctx.restore();
}
// Usage
const data: DataPoint[] = [
{ x: 1, y: 2, label: "A" },
{ x: 2, y: 4, label: "B" },
{ x: 3, y: 3, label: "C" },
{ x: 4, y: 6, label: "D" },
{ x: 5, y: 5, label: "E" },
];
const canvas = new OffscreenCanvas(500, 400);
drawScatterPlot(canvas, data);
canvas.render();
canvas.saveAsPng("scatter-plot.png");
Performance Tips
- Batch drawing operations - Group similar operations together
- Use save/restore carefully - Only when necessary, as they have overhead
- Avoid frequent context switches - Set styles once for multiple operations
- Use appropriate canvas size - Larger canvases require more memory
- Clear efficiently - Use
clearRect
instead of drawing over with white
Best Practices
- Always check context - Ensure
getContext("2d")
returns a valid context - Handle errors gracefully - Wrap canvas operations in try-catch blocks
- Use meaningful coordinates - Define coordinate systems that make sense
- Document complex drawings - Comment your drawing logic
- Test output - Always verify that images are saved correctly
// Good error handling
function safeDrawing(canvas: OffscreenCanvas) {
const ctx = canvas.getContext("2d");
if (!ctx) {
throw new Error("Could not get 2D context");
}
try {
// Drawing code here
ctx.fillRect(0, 0, 100, 100);
canvas.render();
canvas.saveAsPng("output.png");
console.log("✅ Drawing completed successfully");
} catch (error) {
console.error("❌ Drawing failed:", error.message);
}
}
Found an issue with this page?Edit on GitHub
Last updated: