Skip to content

WIP. DrawingCanvas API: Replace imperative extension methods with stateful canvas-based drawing model#377

Draft
JimBobSquarePants wants to merge 101 commits intomainfrom
js/canvas-api
Draft

WIP. DrawingCanvas API: Replace imperative extension methods with stateful canvas-based drawing model#377
JimBobSquarePants wants to merge 101 commits intomainfrom
js/canvas-api

Conversation

@JimBobSquarePants
Copy link
Member

@JimBobSquarePants JimBobSquarePants commented Mar 1, 2026

Prerequisites

  • I have written a descriptive pull-request title
  • I have verified that there are no overlapping pull-requests open
  • I have verified that I am following matches the existing coding patterns and practice as demonstrated in the repository. These follow strict Stylecop rules 👮.
  • I have provided test coverage for my change (where applicable)

Breaking Changes: DrawingCanvas API

Fix #106
Fix #244
Fix #344
Fix #367

This is a major breaking change. The library's public API has been completely redesigned around a canvas-based drawing model, replacing the previous collection of imperative extension methods.

What changed

The old API surface — dozens of IImageProcessingContext extension methods like DrawLine(), DrawPolygon(), FillPolygon(), DrawBeziers(), DrawImage(), DrawText(), etc. — has been removed entirely. These methods were individually simple but suffered from several architectural limitations:

  • Each call was an independent image processor that rasterized and composited in isolation, making it impossible to batch or reorder operations.
  • State (blending mode, clip paths, transforms) had to be passed to every single call.
  • There was no way for an alternate rendering backend to intercept or accelerate a sequence of draw calls.

The new model: DrawingCanvas

All drawing now goes through IDrawingCanvas / DrawingCanvas<TPixel>, a stateful canvas that queues draw commands and flushes them as a batch.

Via Image.Mutate() (most common)

using SixLabors.ImageSharp.Drawing;
using SixLabors.ImageSharp.Drawing.Processing;

image.Mutate(ctx => ctx.ProcessWithCanvas(canvas =>
{
    // Fill a path
    canvas.Fill(new EllipsePolygon(200, 200, 100), Brushes.Solid(Color.Red));

    // Stroke a path
    canvas.Draw(Pens.Solid(Color.Blue, 3), new RectangularPolygon(50, 50, 200, 100));

    // Draw a polyline
    canvas.DrawLine(Pens.Solid(Color.Green, 2), new PointF(0, 0), new PointF(100, 100));

    // Draw text
    canvas.DrawText(
        new RichTextOptions(font) { Origin = new PointF(10, 10) },
        "Hello, World!",
        Brushes.Solid(Color.Black),
        pen: null);

    // Draw an image
    canvas.DrawImage(sourceImage, sourceRect, destinationRect);

    // Save/Restore state (options, clip paths)
    canvas.Save(new DrawingOptions
    {
        GraphicsOptions = new GraphicsOptions { BlendPercentage = 0.5f }
    });
    canvas.Fill(path, brush);
    canvas.Restore();

    // Apply arbitrary image processing to a path region
    canvas.Process(path, inner => inner.Brightness(0.5f));

    // Commands are flushed on Dispose (or call canvas.Flush() explicitly)
}));

Standalone usage (without Image.Mutate)

DrawingCanvas<TPixel> can be constructed directly against an image frame, a Buffer2DRegion<TPixel>, or any ICanvasFrame<TPixel> implementation:

using var canvas = DrawingCanvas<Rgba32>.FromRootFrame(image, new DrawingOptions());

canvas.Fill(path, brush);
canvas.Draw(pen, path);
canvas.Flush();
using var canvas = DrawingCanvas<Rgba32>.FromImage(image, frameIndex: 0, new DrawingOptions());
// ...
using var canvas = DrawingCanvas<Rgba32>.FromFrame(frame, new DrawingOptions());
// ...
using var canvas = new DrawingCanvas<Rgba32>(configuration, cpuBufferRegion, new DrawingOptions());
// ...

Canvas state management

The canvas supports a save/restore stack (similar to HTML Canvas or SkCanvas):

canvas.Save(); // push current state
canvas.Save(options, clipPath1, clipPath2); // push and replace state

canvas.Restore();           // pop one level
canvas.RestoreTo(saveCount); // pop to a specific level

State includes DrawingOptions (graphics options, shape options, transform) and clip paths. CreateRegion() creates a child canvas over a sub-rectangle.

IDrawingBackend — bring your own renderer

The library's rasterization and composition pipeline is abstracted behind IDrawingBackend (currently internal, will be made public). This interface has four methods:

Method Purpose
IsCompositionBrushSupported<TPixel> Capability check — can this backend compose the given brush type?
FillPath<TPixel> Rasterize a path and queue composition commands via the batcher
FlushCompositions<TPixel> Execute all queued composition commands against the target
TryReadRegion<TPixel> Read pixels back from the target (needed for Process() and DrawImage())

The library ships with DefaultDrawingBackend (CPU, tiled fixed-point rasterizer). An experimental WebGPU compute-shader backend (ImageSharp.Drawing.WebGPU) is also available, demonstrating how alternate backends plug in. When IDrawingBackend is made public, users will be able to provide their own implementations — for example, GPU-accelerated backends, SVG emitters, or recording/replay layers.

Backends are registered on Configuration:

// Will be possible once IDrawingBackend is public:
configuration.SetDrawingBackend(myCustomBackend);

Migration guide

Old API New API
ctx.Fill(color, path) ctx.ProcessWithCanvas(c => c.Fill(path, Brushes.Solid(color)))
ctx.Fill(brush, path) ctx.ProcessWithCanvas(c => c.Fill(path, brush))
ctx.Draw(pen, path) ctx.ProcessWithCanvas(c => c.Draw(pen, path))
ctx.DrawLine(pen, points) ctx.ProcessWithCanvas(c => c.DrawLine(pen, points))
ctx.DrawPolygon(pen, points) ctx.ProcessWithCanvas(c => c.Draw(pen, new Polygon(new LinearLineSegment(points))))
ctx.FillPolygon(brush, points) ctx.ProcessWithCanvas(c => c.Fill(new Polygon(new LinearLineSegment(points)), brush))
ctx.DrawText(text, font, color, origin) ctx.ProcessWithCanvas(c => c.DrawText(new RichTextOptions(font) { Origin = origin }, text, Brushes.Solid(color), null))
ctx.DrawImage(overlay, opacity) ctx.ProcessWithCanvas(c => c.DrawImage(overlay, sourceRect, destRect))
Multiple independent draw calls Single ProcessWithCanvas block — commands are batched and flushed together

Other breaking changes in this PR

  • AntialiasSubpixelDepth removed — The rasterizer now uses a fixed 256-step (8-bit) subpixel depth. The old AntialiasSubpixelDepth property (default: 16) controlled how many vertical subpixel steps the rasterizer used per pixel row. The new fixed-point scanline rasterizer integrates area/cover analytically per cell rather than sampling at discrete subpixel rows, so the "depth" is a property of the coordinate precision (24.8 fixed-point), not a tunable sample count. 256 steps gives ~0.4% coverage granularity — more than sufficient for all practical use cases. The old default of 16 (~6.25% granularity) could produce visible banding on gentle slopes.
  • GraphicsOptions.Antialias — now controls RasterizationMode (antialiased vs aliased). When false, coverage is snapped to binary using AntialiasThreshold.
  • GraphicsOptions.AntialiasThreshold — new property (0–1, default 0.5) controlling the coverage cutoff in aliased mode. Pixels with coverage at or above this value become fully opaque; pixels below are discarded. For users who previously set AntialiasSubpixelDepth = 1 to get aliased output, the equivalent is now Antialias = false.

@antonfirsov
Copy link
Member

antonfirsov commented Mar 4, 2026

Target 2, then 1.

This means that it would be of key importance to validate the API with a simple GUI app that truly challenges the rendering engine! I just made this thing with Claude*. We need an equivalent C# app. It can of course do something else (as long as it does mass text rendering!), but we need to make sure we have an HTML5 reference app, and that its' functionality is kept in sync with our C# app's, so we can compare them.

the WebGPU IDrawingBackend implementation is purely experimental

We don't need it to be production ready, but if we find performance bottlenecks, we should identify where is the root cause: suboptimal backend implementation VS the API shape VS our library code.

I can chime in into this work later, but not right now.

*disclaimer: didn't spend time on deslopification

@JimBobSquarePants JimBobSquarePants changed the title WIP. Expand DrawingBackend API and Canvas. WIP. DrawingCanvas API: Replace imperative extension methods with stateful canvas-based drawing model/ Mar 7, 2026
@JimBobSquarePants JimBobSquarePants changed the title WIP. DrawingCanvas API: Replace imperative extension methods with stateful canvas-based drawing model/ WIP. DrawingCanvas API: Replace imperative extension methods with stateful canvas-based drawing model Mar 7, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

2 participants