SVG viewBox to Canvas device-pixel coordinate mismatch during incremental rect migration
0 reputation · 14 Jan 2024, 17:10 UTC
0 reputation · 14 Jan 2024, 17:10 UTC
Migrating a graphics layer from SVG <rect> to Canvas rect() without downtime requires running both renderers in parallel during transition. SVG rectangles express geometry in user units defined by the viewBox, while Canvas operations target drawing-buffer pixels that already incorporate devicePixelRatio. When a parent container applies a CSS transform or the viewBox scales the SVG viewport, the two coordinate systems diverge: an SVG x="10.5" maps to a different physical position than a Canvas x=10.5 after the context scales for high-DPI displays.
Stroke rendering adds another dimension of drift. SVG stroke-width scales with the current transform unless vector-effect: non-scaling-stroke is set, whereas Canvas lineWidth remains in device pixels unless the context transform is explicitly compensated. Hit-testing behaves differently as well: SVG exposes pointer-events declaratively, while Canvas demands manual point-in-rectangle checks against transformed coordinates.
Which strategy minimizes visual drift during the overlap period: a shared user-space abstraction layer that both renderers consume, or per-component dual rendering with automated pixel-diff parity checks?
31275 reputation · 14 Jan 2024, 21:26 UTC
Use a shared user-space abstraction layer as the primary drift-control strategy. Automated pixel-diff parity checks belong in CI as a validation gate, not as the runtime mechanism that keeps the two renderers aligned.
The core mismatch is that SVG geometry lives in viewBox user units while Canvas draws into a backing store already scaled by devicePixelRatio. A CSS transform on a common ancestor compounds the divergence. Defining every rectangle once in a single logical coordinate system (CSS pixels or viewBox units) and projecting to device pixels once per renderer eliminates per-primitive drift at the source.
Pixel-diff checks only detect symptoms after the fact. They cannot prevent a frame where the SVG rect lands at 10.5 CSS px and the Canvas rect at 10.5 device px because the context scale was applied at the wrong level.
{x, y, w, h, strokeWidth} and a Canvas 2D context, then returns device-pixel values for rect(), strokeRect(), and hit-test math:function toDeviceCoords(logicalRect, ctx) {
const t = ctx.getTransform();
const scale = t.a; // assumes uniform scale
return {
x: logicalRect.x * scale,
y: logicalRect.y * scale,
w: logicalRect.w * scale,
h: logicalRect.h * scale,
strokeWidth: logicalRect.strokeWidth * scale
};
}viewBox scaling.vector-effect: non-scaling-stroke in SVG unless the design requires it. If non-scaling strokes are required, apply the inverse scale to ctx.lineWidth in Canvas.clientToLogical(evt)), then run identical pointInRect(logicalPt, logicalRect) for both renderers. Keep pointer-events on the SVG overlay; give the Canvas layer pointer-events: none.resize or devicePixelRatio change, recompute the logical-to-device scale factor for both renderers simultaneously.If your container uses non-uniform CSS transforms (skew, anisotropic scale), the simple ctx.getTransform().a stroke compensation breaks. In that case you must either decompose the transform matrix in the projection function or restrict the migration to uniform-scale containers. Which transform profile does your container use?
canvas.toDataURL() comparison.devicePixelRatio values (1, 1.5, 2, 3) and container CSS scales (0.5, 1, 1.5, 2) using headless Chrome with --force-device-scale-factor.Use comments to ask for clarification. Post a solution as an answer.
No question comments on this page.