Skip to content
Blog

Browser Canvas 2D from Flutter Web: Paths, Pixels, and DPR

Use the browser's Canvas 2D API from Flutter web with JS interop: paths, Path2D, devicePixelRatio transforms, getImageData pitfalls, OffscreenCanvas workers, and how it differs from dart:ui Canvas.

Published on • November 11, 2026

AI Assistant

Flutter’s own drawing stack — CustomPaint, the dart:ui Canvas, and fragment shaders — is covered in the CustomPainter and GPU rendering posts. This one is about the other canvas: the browser’s own Canvas 2D API, reached with package:web from inside an HtmlElementView. You want it when a JavaScript library already draws to a <canvas>, when you need pixel-level access, or when you are embedding an HTML-canvas component.

Get the context and draw

canvas.getContext('2d') hands back a CanvasRenderingContext2D, an imperative surface where every call mutates the bitmap immediately:

const ctx = canvas.getContext('2d');
if (ctx === null) throw new Error('no 2d context');

ctx.beginPath();
ctx.moveTo(20, 20);
ctx.lineTo(120, 60);
ctx.lineTo(20, 100);
ctx.closePath();
ctx.strokeStyle = '#e11d48';
ctx.lineWidth = 3;
ctx.stroke();

Reusable shapes come from Path2D, which you can stroke, fill, or transform independently of the current path. Styles are assignments: fillStyle, strokeStyle, lineWidth, shadowColor, shadowBlur, and gradients from createLinearGradient. Images go in with drawImage(image, dx, dy, dw, dh).

Transforms, devicePixelRatio, and crisp lines

The <canvas> width and height attributes define the backing store in pixels and are independent of CSS: the element can be sized arbitrarily, and the bitmap is stretched to fit, so a mismatched ratio distorts your drawing. Setting width also resets the entire rendering context and clears the buffer, so resize once per layout change, never per frame. Scale by devicePixelRatio and draw in CSS pixels:

void configure(web.HTMLCanvasElement canvas, int cssWidth, int cssHeight) {
  final dpr = web.window.devicePixelRatio;
  canvas.width = (cssWidth * dpr).round();
  canvas.height = (cssHeight * dpr).round();
  final ctx = canvas.getContext('2d') as web.CanvasRenderingContext2D;
  ctx.setTransform(dpr, 0, 0, dpr, 0, 0); // draw in CSS pixels from here on
}

The rest of the transform API is translate, scale, rotate, and setTransform(a, b, c, d, e, f) to reset the matrix outright — exactly the calls you already know from dart:ui, minus Flutter’s automatic device-pixel-ratio handling.

Pixels: read, write, and know when to stop

getImageData returns an ImageData whose data is a Uint8ClampedArray of RGBA bytes; putImageData writes one straight back:

const { data, width, height } = ctx.getImageData(0, 0, canvas.width, canvas.height);
for (let i = 0; i < data.length; i += 4) {
  data[i] = 255 - data[i]; // invert red
}
ctx.putImageData(new ImageData(data, width, height), 0, 0);

Every iteration of that loop crosses the JS↔Dart boundary if you write it from Dart, and read-backs force a GPU sync, so keep per-pixel work out of the hot path. For real pixel processing, move it to an OffscreenCanvas inside a worker: it decouples rendering from the DOM and lets the heavy loop run on another thread. If you only read occasionally, pass willReadFrequently: true to getContext() so the browser picks a software context that tolerates read-backs. For export, canvas.toDataURL('image/png') is synchronous and fine for thumbnails; canvas.toBlob() is async and the right call for anything large.

Redraw the way the browser wants

A Flutter painter runs when shouldRepaint or a Listenable says so. A browser canvas never repaints on its own — you clear and redraw it yourself, normally from requestAnimationFrame:

void startDrawLoop(web.CanvasRenderingContext2D ctx) {
  late final web.FrameRequestCallback cb;
  cb = ((double _) {
    ctx.clearRect(0, 0, 640, 480);
    drawFrame(ctx);
    web.window.requestAnimationFrame(cb);
  }).toJS;
  web.window.requestAnimationFrame(cb);
}

That loop is your repaint policy: throttle it, pause it when the tab is hidden, and never allocate fresh Paint-like objects inside it.

dart:ui Canvas versus browser Canvas 2D

dart:ui CanvasBrowser Canvas 2D
ModelDrawing calls inside paint()Stateful API you mutate yourself
When it drawsFlutter repaints the layerOnly when you call it from a frame loop
Where it livesIn the widget tree, composited by Flutter’s engineAn <canvas> in an HtmlElementView
Device pixelsHandled by FlutterYou set width/height and setTransform
InputFlutter gestures and HitTestDOM events on the element
Pixel accessNo public read-backgetImageData / putImageData

Pick the browser API when you must share the canvas with a JavaScript library, run pixel-level effects, or embed a canvas-based component. Otherwise stay in Flutter’s canvas and keep one rendering pipeline.

Takeaway: Use the browser’s Canvas 2D from Flutter web when a <canvas> you do not fully control is involved — set the backing store with devicePixelRatio, keep per-pixel loops off the main thread, and let requestAnimationFrame be your repaint policy.

Sources