SWCanvas

A deterministic 2D raster engine with Canvas-like API. SWCanvas provides pixel-perfect, cross-platform 2D rendering that produces identical results on any system, making it ideal for testing, screenshots, and server-side graphics.

๐ŸŽจ Interactive Demo โ€ข ๐Ÿงช Visual Tests โ€ข ๐Ÿ“Š Simple Test

Features

Quick Start

Building

npm run build      # Build the library
npm run minify     # Create minified version
npm run build:prod # Build + minify in one command

This generates:

Node.js Usage

const SWCanvas = require('./dist/swcanvas.js');

// Create surface with immutable dimensions
const surface = SWCanvas.Core.Surface(800, 600);
const ctx = new SWCanvas.Core.Context2D(surface);

// Use Canvas 2D API
ctx.setFillStyle(255, 0, 0, 255); // Red
ctx.fillRect(10, 10, 100, 50);

// Advanced: Use OO classes directly
const transform = new SWCanvas.Core.Transform2D()
    .translate(100, 100)
    .rotate(Math.PI / 4);

const point = new SWCanvas.Point(50, 75);
const rect = new SWCanvas.Rectangle(0, 0, 200, 150);
const color = new SWCanvas.Core.Color(255, 128, 0, 200);

// Export as PNG (recommended - preserves transparency)
const pngData = SWCanvas.Core.PngEncoder.encode(surface);
// Or export as BMP (legacy - composites with white background)  
const bmpData = SWCanvas.Core.BitmapEncoder.encode(surface);

Browser Usage

<script src="dist/swcanvas.js"></script>
<script>
// Create surface and context
const surface = SWCanvas.Core.Surface(800, 600);
const ctx = new SWCanvas.Core.Context2D(surface);

// Standard Canvas 2D operations
ctx.setFillStyle(255, 0, 0, 255); // Red
ctx.fillRect(10, 10, 100, 50);

// Path operations including ellipses
ctx.beginPath();
ctx.ellipse(400, 200, 80, 40, Math.PI / 4, 0, 2 * Math.PI);
ctx.fill();

// Use immutable geometry classes
const center = new SWCanvas.Point(400, 300);
const bounds = new SWCanvas.Rectangle(100, 100, 600, 400);
if (bounds.contains(center)) {
    console.log('Center is within bounds');
}

// Transform operations
ctx.save();
const rotTransform = SWCanvas.Core.Transform2D.rotation(Math.PI / 6);
ctx.setTransform(rotTransform.a, rotTransform.b, rotTransform.c, rotTransform.d, rotTransform.e, rotTransform.f);
ctx.fillRect(50, 50, 100, 100);
ctx.restore();
</script>

Examples

Feature Showcase

Open examples/showcase.html in a web browser for a comprehensive demonstration of SWCanvas capabilities:

Features Demonstrated:

# View the example
open examples/showcase.html

See examples/README.md for additional examples and usage instructions.

Testing

Run All Tests

npm test

This runs:

Browser Tests

Open tests/browser/index.html in a web browser for:

Test Architecture

The modular architecture allows individual test development while maintaining build-time concatenation for performance.

See tests/README.md for detailed test documentation.

API Documentation

SWCanvas provides dual API architecture for maximum flexibility:

Drop-in replacement for HTML5 Canvas with familiar API:

// Create canvas element (works in Node.js and browsers)
const canvas = SWCanvas.createCanvas(800, 600);
const ctx = canvas.getContext('2d');

// Standard HTML5 Canvas API
ctx.fillStyle = 'red';
ctx.fillRect(10, 10, 100, 50);

ctx.strokeStyle = '#0066cc';
ctx.lineWidth = 2;
ctx.strokeRect(20, 20, 80, 30);

// Line dashing
ctx.setLineDash([5, 5]);       // Dashed line pattern
ctx.lineDashOffset = 0;        // Starting offset
ctx.beginPath();
ctx.moveTo(10, 70);
ctx.lineTo(100, 70);
ctx.stroke();

// Shadows
ctx.shadowColor = 'rgba(0, 0, 0, 0.5)';  // Semi-transparent black
ctx.shadowBlur = 5;                      // 5px blur radius
ctx.shadowOffsetX = 3;                   // 3px right offset
ctx.shadowOffsetY = 3;                   // 3px down offset
ctx.fillStyle = 'red';
ctx.fillRect(120, 10, 50, 30);          // Rectangle with shadow

// Rounded corners with arcTo
ctx.beginPath();
ctx.moveTo(50, 100);
ctx.lineTo(150, 100);
ctx.arcTo(200, 100, 200, 150, 25); // 25px radius rounded corner
ctx.lineTo(200, 200);
ctx.stroke();

// Path hit testing
ctx.beginPath();
ctx.rect(10, 120, 100, 60);
ctx.fillStyle = 'blue';
ctx.fill();

// Test if points are inside the filled rectangle
if (ctx.isPointInPath(60, 150)) {
    console.log('Point (60, 150) is inside the rectangle');
}
if (ctx.isPointInPath(60, 150, 'evenodd')) {
    console.log('Point is inside using evenodd fill rule');
}

// Test if points are on the stroke outline
ctx.lineWidth = 5;
ctx.stroke();
if (ctx.isPointInStroke(48, 150)) { // On stroke edge
    console.log('Point (48, 150) is on the stroke outline');
}

// Composite operations (Porter-Duff blending)
ctx.fillStyle = 'red';
ctx.fillRect(30, 30, 40, 40);

ctx.globalCompositeOperation = 'destination-over'; // Draw behind existing content
ctx.fillStyle = 'blue';
ctx.fillRect(50, 50, 40, 40);

// All Porter-Duff operations supported:
// Source-bounded operations: source-over (default), destination-over, destination-out, xor
// Canvas-wide operations: destination-atop, destination-in, source-atop, source-in, source-out, copy

// ImageData API for pixel manipulation
const imageData = ctx.createImageData(100, 100);
// ... modify imageData.data ...
ctx.putImageData(imageData, 50, 50);

// Extract pixel data
const pixelData = ctx.getImageData(60, 60, 10, 10);

// Factory method for ImageData objects
const blankImage = SWCanvas.createImageData(50, 50);

Direct access to core classes with explicit RGBA values:

// Create surface and context directly  
const surface = SWCanvas.Core.Surface(width, height);
const ctx = new SWCanvas.Core.Context2D(surface);

// Explicit RGBA values (0-255)
ctx.setFillStyle(255, 0, 0, 255);    // Red
ctx.setStrokeStyle(0, 102, 204, 255); // Blue

Drawing Operations

// Rectangle filling
ctx.fillRect(x, y, width, height);

// Path operations
ctx.beginPath();
ctx.moveTo(x, y);
ctx.lineTo(x2, y2);
ctx.arc(cx, cy, radius, startAngle, endAngle, counterclockwise);
ctx.ellipse(cx, cy, radiusX, radiusY, rotation, startAngle, endAngle, counterclockwise);
ctx.arcTo(x1, y1, x2, y2, radius); // Rounded corners between lines
ctx.fill();
ctx.stroke();

// Path testing  
const isInside = ctx.isPointInPath(x, y, fillRule); // Test if point is inside current path
const isOnStroke = ctx.isPointInStroke(x, y); // Test if point is on stroke outline

// Transforms
ctx.translate(x, y);
ctx.scale(x, y);
ctx.rotate(angle);

// Clipping
ctx.clip();

// State management
ctx.save();
ctx.restore();

// Image rendering
ctx.drawImage(imagelike, dx, dy);                    // Basic positioning
ctx.drawImage(imagelike, dx, dy, dw, dh);            // With scaling
ctx.drawImage(imagelike, sx, sy, sw, sh, dx, dy, dw, dh); // With source rectangle

// Line dashing
ctx.setLineDash([10, 5]);        // Set dash pattern: 10px dash, 5px gap
ctx.lineDashOffset = 2;          // Starting offset into pattern
const pattern = ctx.getLineDash(); // Get current pattern: [10, 5]

// Shadow properties
ctx.shadowColor = 'rgba(0, 0, 0, 0.3)'; // Shadow color with transparency
ctx.shadowBlur = 8;                     // Blur radius in pixels
ctx.shadowOffsetX = 4;                  // Horizontal shadow offset
ctx.shadowOffsetY = 4;                  // Vertical shadow offset

// Drawing with shadows (works with all drawing operations)
ctx.fillStyle = 'blue';
ctx.fillRect(50, 50, 100, 60);          // Rectangle with shadow
ctx.strokeStyle = 'green';
ctx.lineWidth = 3;
ctx.strokeRect(50, 120, 100, 60);       // Stroked rectangle with shadow

// Shadows work with paths and complex shapes
ctx.beginPath();
ctx.arc(300, 100, 40, 0, Math.PI * 2);
ctx.fillStyle = 'orange';
ctx.fill();                             // Circle with shadow

// Turn off shadows
ctx.shadowColor = 'transparent';        // Or set to 'rgba(0,0,0,0)'

Color Setting

ctx.setFillStyle(r, g, b, a);    // 0-255 values
ctx.setStrokeStyle(r, g, b, a);  // 0-255 values

// Shadow properties (Core API uses explicit RGBA values)
ctx.setShadowColor(0, 0, 0, 128);    // Semi-transparent black shadow
ctx.shadowBlur = 5;                  // 5px blur radius
ctx.shadowOffsetX = 3;               // 3px horizontal offset
ctx.shadowOffsetY = 3;               // 3px vertical offset

// Or use Color objects directly
const color = new SWCanvas.Core.Color(255, 128, 0, 200);
ctx.setFillStyle(color.r, color.g, color.b, color.a);

const shadowColor = new SWCanvas.Core.Color(0, 0, 0, 100);
ctx.setShadowColor(shadowColor.r, shadowColor.g, shadowColor.b, shadowColor.a);

Gradients and Patterns

SWCanvas supports HTML5 Canvas-compatible gradients and patterns for advanced fill and stroke operations:

HTML5 Canvas-Compatible API

const canvas = SWCanvas.createCanvas(400, 300);
const ctx = canvas.getContext('2d');

// Linear gradients
const linearGrad = ctx.createLinearGradient(0, 0, 200, 0);
linearGrad.addColorStop(0, 'red');
linearGrad.addColorStop(0.5, 'yellow');
linearGrad.addColorStop(1, 'blue');
ctx.fillStyle = linearGrad;
ctx.fillRect(10, 10, 200, 100);

// Radial gradients  
const radialGrad = ctx.createRadialGradient(150, 75, 0, 150, 75, 50);
radialGrad.addColorStop(0, '#ff0000');
radialGrad.addColorStop(1, '#0000ff');
ctx.fillStyle = radialGrad;
ctx.fillRect(100, 50, 100, 100);

// Conic gradients (CSS conic-gradient equivalent)
const conicGrad = ctx.createConicGradient(Math.PI / 4, 200, 150);
conicGrad.addColorStop(0, 'red');
conicGrad.addColorStop(0.25, 'yellow');
conicGrad.addColorStop(0.5, 'lime');
conicGrad.addColorStop(0.75, 'aqua');
conicGrad.addColorStop(1, 'red');
ctx.fillStyle = conicGrad;
ctx.fillRect(150, 100, 100, 100);

// Patterns with ImageLike objects
const patternImage = ctx.createImageData(20, 20);
// ... fill patternImage.data with pattern ...
const pattern = ctx.createPattern(patternImage, 'repeat');
ctx.fillStyle = pattern;
ctx.fillRect(50, 150, 150, 100);

// Gradients work with strokes too, including sub-pixel strokes
ctx.strokeStyle = linearGrad;
ctx.lineWidth = 5;
ctx.strokeRect(250, 50, 100, 100);

// Sub-pixel strokes work with all paint sources
ctx.strokeStyle = radialGrad;
ctx.lineWidth = 0.5; // 50% opacity stroke
ctx.strokeRect(250, 150, 100, 100);

// Shadows work with all paint sources
ctx.shadowColor = 'rgba(0, 0, 0, 0.4)';
ctx.shadowBlur = 6;
ctx.shadowOffsetX = 4;
ctx.shadowOffsetY = 4;
ctx.fillStyle = conicGrad;
ctx.fillRect(300, 10, 80, 80);           // Gradient fill with shadow

Core API (Performance)

const surface = SWCanvas.Core.Surface(400, 300);
const ctx = new SWCanvas.Core.Context2D(surface);

// Linear gradients
const linearGrad = ctx.createLinearGradient(0, 0, 200, 0);
linearGrad.addColorStop(0, new SWCanvas.Core.Color(255, 0, 0, 255));
linearGrad.addColorStop(1, new SWCanvas.Core.Color(0, 0, 255, 255));
ctx.setFillStyle(linearGrad);
ctx.fillRect(10, 10, 200, 100);

// Radial gradients
const radialGrad = ctx.createRadialGradient(150, 75, 0, 150, 75, 50);
radialGrad.addColorStop(0, new SWCanvas.Core.Color(255, 255, 0, 255));
radialGrad.addColorStop(1, new SWCanvas.Core.Color(255, 0, 255, 255));
ctx.setStrokeStyle(radialGrad);
ctx.lineWidth = 8;
ctx.beginPath();
ctx.arc(150, 75, 40);
ctx.stroke();

// Conic gradients
const conicGrad = ctx.createConicGradient(0, 200, 150);
conicGrad.addColorStop(0, new SWCanvas.Core.Color(255, 0, 0, 255));
conicGrad.addColorStop(0.33, new SWCanvas.Core.Color(0, 255, 0, 255));
conicGrad.addColorStop(0.66, new SWCanvas.Core.Color(0, 0, 255, 255));
conicGrad.addColorStop(1, new SWCanvas.Core.Color(255, 0, 0, 255));
ctx.setFillStyle(conicGrad);
ctx.fillRect(150, 100, 100, 100);

// Patterns with sub-pixel strokes
const imagelike = { width: 10, height: 10, data: new Uint8ClampedArray(400) };
// ... fill imagelike.data ...
const pattern = ctx.createPattern(imagelike, 'repeat-x');
ctx.setStrokeStyle(pattern);
ctx.lineWidth = 0.25; // 25% opacity stroke  
ctx.strokeRect(50, 200, 200, 50);

// Shadows work with all paint sources (Core API)
ctx.setShadowColor(0, 0, 0, 100);        // RGBA shadow color
ctx.shadowBlur = 4;
ctx.shadowOffsetX = 2;
ctx.shadowOffsetY = 2;
ctx.setFillStyle(conicGrad);
ctx.fillRect(300, 200, 80, 80);          // Conic gradient fill with shadow

Pattern Repetition Modes

Gradient Types

Core API Classes

SWCanvas provides rich OO classes for advanced operations through the Core API:

// Immutable geometry classes
const point = new SWCanvas.Core.Point(100, 50);
const rect = new SWCanvas.Core.Rectangle(10, 20, 100, 80);
const center = rect.center; // Returns Point(60, 60)

// Immutable transformation matrix
const transform = new SWCanvas.Core.Transform2D()
    .translate(100, 100)
    .scale(2, 2)
    .rotate(Math.PI / 4);

// Apply to points
const transformed = transform.transformPoint(point);

// Bit manipulation utility (used by mask classes)
const bitBuffer = new SWCanvas.Core.BitBuffer(100, 100, 0); // Default to 0s
bitBuffer.setPixel(50, 50, true);
console.log(bitBuffer.getPixel(50, 50)); // true

// Mask classes (use BitBuffer composition internally)
const clipMask = new SWCanvas.Core.ClipMask(800, 600);
const sourceMask = new SWCanvas.Core.SourceMask(800, 600);

// Image processing utilities
const validImage = SWCanvas.Core.ImageProcessor.validateAndConvert(imageData);

// Composite operations utilities
const supportedOps = SWCanvas.Core.CompositeOperations.getSupportedOperations();
const isSupported = SWCanvas.Core.CompositeOperations.isSupported('xor');

// Path processing utilities
const polygons = SWCanvas.Core.PathFlattener.flattenPath(path2d);
const strokePolys = SWCanvas.Core.StrokeGenerator.generateStrokePolygons(path2d, strokeProps);

// Color parsing utilities (for CSS color strings)
const color = SWCanvas.Core.ColorParser.parse('#FF0000');

// BMP encoding configuration
const encodingOptions = SWCanvas.Core.BitmapEncodingOptions.withGrayBackground(128);

Image Rendering

SWCanvas supports drawing ImageLike objects with nearest-neighbor sampling:

// ImageLike interface: { width, height, data: Uint8ClampedArray }
const imagelike = {
    width: 10,
    height: 10,
    data: new Uint8ClampedArray(10 * 10 * 4) // RGBA
};

// RGB images auto-convert to RGBA
const rgbImage = {
    width: 5,
    height: 5, 
    data: new Uint8ClampedArray(5 * 5 * 3) // RGB โ†’ RGBA with alpha=255
};

// Basic usage
ctx.drawImage(imagelike, 10, 10);                    // Draw at position
ctx.drawImage(imagelike, 10, 10, 20, 20);            // Draw with scaling
ctx.drawImage(imagelike, 0, 0, 5, 5, 10, 10, 10, 10); // Source rectangle

// Works with transforms and clipping
ctx.translate(50, 50);
ctx.rotate(Math.PI / 4);
ctx.drawImage(imagelike, 0, 0);

Image Export

const pngData = SWCanvas.Core.PngEncoder.encode(surface);
// Returns ArrayBuffer containing PNG file data
// Preserves transparency without background compositing

// PNG with custom options
const pngOptions = SWCanvas.Core.PngEncodingOptions.withTransparency();
const pngData = SWCanvas.Core.PngEncoder.encode(surface, pngOptions);

BMP Export (Legacy - Background Compositing)

const bmpData = SWCanvas.Core.BitmapEncoder.encode(surface);
// Returns ArrayBuffer containing BMP file data
// Transparent pixels composited with white background (default)

// Custom background colors for transparent pixel compositing
const grayOptions = SWCanvas.Core.BitmapEncodingOptions.withGrayBackground(128);
const bmpData = SWCanvas.Core.BitmapEncoder.encode(surface, grayOptions);

// Pre-defined background options
const blackBmp = SWCanvas.Core.BitmapEncoder.encode(surface, 
    SWCanvas.Core.BitmapEncodingOptions.withBlackBackground());

Architecture

Core Components (Object-Oriented Design)

Key Features

Stencil-Based Clipping

Deterministic Rendering

Premultiplied sRGB

Development

Debug Utilities: See debug/README.md for debugging scripts, templates, and investigation workflows.

Project Structure (Object-Oriented Architecture)

src/              # Source files (ES6 Classes)
โ”œโ”€โ”€ Context2D.js     # Main drawing API (class)
โ”œโ”€โ”€ Surface.js       # Memory management (ES6 class) 
โ”œโ”€โ”€ Transform2D.js   # Transform mathematics (immutable class)
โ”œโ”€โ”€ Rasterizer.js    # Low-level rendering (ES6 class)
โ”œโ”€โ”€ Color.js         # Immutable color handling (class)
โ”œโ”€โ”€ Point.js         # Immutable 2D point operations (class)
โ”œโ”€โ”€ Rectangle.js     # Immutable rectangle operations (class)
โ”œโ”€โ”€ Gradient.js      # Gradient paint sources (linear, radial, conic)
โ”œโ”€โ”€ Pattern.js       # Pattern paint sources with repetition modes
โ”œโ”€โ”€ ClipMask.js      # 1-bit clipping buffer (class)
โ”œโ”€โ”€ ImageProcessor.js # ImageLike validation and conversion (static methods)
โ”œโ”€โ”€ PolygonFiller.js # Scanline polygon filling with paint sources (static methods)
โ”œโ”€โ”€ StrokeGenerator.js # Stroke generation (static methods)
โ”œโ”€โ”€ PathFlattener.js # Path to polygon conversion (static methods)
โ”œโ”€โ”€ BitmapEncoder.js # BMP file encoding (static methods)
โ”œโ”€โ”€ BitmapEncodingOptions.js # BMP encoding configuration (immutable options)
โ”œโ”€โ”€ ColorParser.js   # CSS color string parsing (static methods)
โ””โ”€โ”€ SWPath2D.js      # Path definition (class)

tests/            # Test suite
โ”œโ”€โ”€ core-functionality-tests.js # Core functionality tests
โ”œโ”€โ”€ visual-rendering-tests.js    # 138+ visual tests
โ””โ”€โ”€ run-tests.js            # Node.js test runner

tests/browser/    # Browser tests
โ”œโ”€โ”€ index.html       # Main visual comparison tool (moved from examples/)
โ”œโ”€โ”€ simple-test.html # Simple visual test
โ””โ”€โ”€ browser-test-helpers.js # Interactive test utilities

dist/             # Built library
โ””โ”€โ”€ swcanvas.js      # Concatenated distribution file

Test Architecture

SWCanvas uses a comprehensive dual test system:

See tests/README.md for complete test documentation, adding tests, and build utilities.

Test Count Maintenance: The npm run update-test-counts command automatically updates test count references across all documentation files to match the actual filesystem. This ensures documentation accuracy as tests are added or removed.

Build Process

The build script (build.sh) concatenates source files in dependency order, following OO architecture:

Phase 1: Foundation Classes

  1. Color - Immutable color handling
  2. Point - Immutable 2D point operations
  3. Rectangle - Immutable rectangle operations
  4. Transform2D - Transformation mathematics
  5. SWPath2D - Path definitions
  6. Surface - Memory buffer management

Phase 2: Service Classes

  1. BitmapEncodingOptions - BMP encoding configuration (immutable options)
  2. BitmapEncoder - BMP file encoding (static methods)
  3. PathFlattener - Path-to-polygon conversion (static methods)
  4. PolygonFiller - Scanline filling with paint sources (static methods)
  5. StrokeGenerator - Stroke generation (static methods)
  6. ClipMask - 1-bit stencil buffer management (class)
  7. ImageProcessor - ImageLike validation and conversion (static methods)
  8. ColorParser - CSS color string parsing (static methods)

Phase 2.5: Paint Sources

  1. Gradient - Linear, radial, and conic gradient paint sources
  2. Pattern - Repeating image pattern paint sources

Phase 3: Rendering Classes

  1. Rasterizer - Rendering pipeline (class)
  2. Context2D - Main drawing API (class)

License

MIT License - see LICENSE file for details.

Contributing

  1. Build: npm run build
  2. Test: npm test
  3. Visual Test: Open tests/browser/index.html in browser
  4. Add Tests: Create individual test files in /tests/core/ or /tests/visual/ (see renumbering utility for advanced organization)
  5. Verify: Ensure identical results in both Node.js and browser

The comprehensive test suite ensures any changes maintain pixel-perfect compatibility with HTML5 Canvas.