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
isPointInPath
and isPointInStroke
implementation with accurate geometric calculationglobalCompositeOperation
support with all 10 standard operations working correctlynpm run build # Build the library
npm run minify # Create minified version
npm run build:prod # Build + minify in one command
This generates:
dist/swcanvas.js
- Complete library for developmentdist/swcanvas.min.js
- Minified library for production (71% smaller)dist/swcanvas.min.js.map
- Source map for debuggingtests/dist/core-functionality-tests.js
from 36 individual test files in /tests/core/
tests/dist/visual-rendering-tests.js
from 138 individual test files in /tests/visual/
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);
<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>
Open examples/showcase.html
in a web browser for a comprehensive demonstration of SWCanvas capabilities:
swcanvas.min.js
) with automatic fallbackFeatures Demonstrated:
# View the example
open examples/showcase.html
See examples/README.md for additional examples and usage instructions.
npm test
This runs:
/tests/core/
)tests/output/
Open tests/browser/index.html
in a web browser for:
/tests/core/
- API correctness, edge cases, mathematical accuracy/tests/visual/
- Pixel-perfect rendering verification with PNG generationThe modular architecture allows individual test development while maintaining build-time concatenation for performance.
See tests/README.md for detailed test 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
// 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)'
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);
SWCanvas supports HTML5 Canvas-compatible gradients and patterns for advanced fill and stroke operations:
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
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
'repeat'
- Tile in both directions (default)'repeat-x'
- Tile horizontally only'repeat-y'
- Tile vertically only'no-repeat'
- Display once at pattern origincreateLinearGradient(x0, y0, x1, y1)
- Linear color transitioncreateRadialGradient(x0, y0, r0, x1, y1, r1)
- Radial color transitioncreateConicGradient(startAngle, centerX, centerY)
- Conic sweep transitionSWCanvas 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);
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);
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);
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());
Debug Utilities: See debug/README.md
for debugging scripts, templates, and investigation workflows.
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
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.
The build script (build.sh
) concatenates source files in dependency order, following OO architecture:
Phase 1: Foundation Classes
Phase 2: Service Classes
Phase 2.5: Paint Sources
Phase 3: Rendering Classes
MIT License - see LICENSE file for details.
npm run build
npm test
tests/browser/index.html
in browser/tests/core/
or /tests/visual/
(see renumbering utility for advanced organization)The comprehensive test suite ensures any changes maintain pixel-perfect compatibility with HTML5 Canvas.