Unstable approach
Recalculate real node internals on every zoom tick: font-size, line-height, padding, border width, badge dimensions, and handle positions. This asks the browser to lay out the label again at each fractional size.
The visible up-and-down text movement during zoom was caused by repeatedly re-laying out node text at fractional CSS sizes. The durable fix is to keep node internals stable and apply zoom as a local visual transform.
The first sharp-rendering pass removed the global canvas scale. That improved line and background rendering, but it also meant node visual values were recalculated on every zoom step.
The user scrolls or clicks zoom controls.
Font size, line height, padding, borders, and badges become zoom-scaled.
Values land on numbers like 15.4px or 18.2px.
The browser recomputes line boxes, glyph positions, and baselines.
Different rounding frame to frame looks like vertical label movement.
Rectangles and lines can tolerate fractional geometry better than text. Text rendering has extra constraints: glyph hinting, baseline snapping, antialiasing, and line box rounding.
Recalculate real node internals on every zoom tick: font-size, line-height, padding, border width, badge dimensions, and handle positions. This asks the browser to lay out the label again at each fractional size.
Keep the node's internal layout in logical canvas units. Then position the wrapper in screen space and scale the visible node container with transform: scale(zoom). The compositor scales the finished node instead of re-deciding its text metrics.
Different canvas layers have different rendering needs. The goal is not to force every layer into the same zoom strategy.
Grid and dot backgrounds should account for zoom and pan directly, so pattern spacing and offsets remain visually crisp.
Convert canvas points with canvasToScreen and scale stroke widths and dashes intentionally. This avoids blurry whole-SVG scaling.
Position the node wrapper in screen space, but keep text, padding, badges, and controls in local node units. Apply zoom to the node visual container as one local transform.
Toolbars, popovers, minimaps, and command surfaces should stay outside the zoomed node geometry so they remain usable at every zoom level.
The core idea is small: only the node's outer visual shell receives the zoom scale.
const screenPosition = canvasToScreen({ x: node.x, y: node.y }, viewOffset, zoom)
<div style={{ transform: `translate(${screenPosition.x}px, ${screenPosition.y}px)` }}>
<div
style={{
...nodeThemeStyle,
transform: `scale(${zoom})`,
transformOrigin: 'top left',
}}
>
Node text and controls stay in stable local layout.
</div>
</div>If zooming makes text appear to move inside a node, check whether the browser is being asked to reflow the text at fractional sizes. For Kemma Studio, keep node internals stable and scale the composed node locally; keep lines and canvas patterns screen-space for sharp geometry.