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 pixels
  • height - 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");
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

  1. Batch drawing operations - Group similar operations together
  2. Use save/restore carefully - Only when necessary, as they have overhead
  3. Avoid frequent context switches - Set styles once for multiple operations
  4. Use appropriate canvas size - Larger canvases require more memory
  5. Clear efficiently - Use clearRect instead of drawing over with white

Best Practices

  1. Always check context - Ensure getContext("2d") returns a valid context
  2. Handle errors gracefully - Wrap canvas operations in try-catch blocks
  3. Use meaningful coordinates - Define coordinate systems that make sense
  4. Document complex drawings - Comment your drawing logic
  5. 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: