Skip to content

Studio Zoom Rendering And Text Jitter

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.

SymptomNode labels appeared to bob vertically while zooming.
CauseFractional font metrics changed the text baseline during reflow.
FixStable node-local layout with local node scaling.
Fractionaltext reflow
Stablenode layout
Lines and backgrounds can still render in screen space. The important part is that node text is not reflowed at a new fractional size on every zoom tick.

What Happened

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.

Step 1Zoom changes

The user scrolls or clicks zoom controls.

Step 2CSS sizes change

Font size, line height, padding, borders, and badges become zoom-scaled.

Step 3Fractions appear

Values land on numbers like 15.4px or 18.2px.

Step 4Text reflows

The browser recomputes line boxes, glyph positions, and baselines.

Step 5Jitter shows up

Different rounding frame to frame looks like vertical label movement.

Why Text Was Special

Rectangles and lines can tolerate fractional geometry better than text. Text rendering has extra constraints: glyph hinting, baseline snapping, antialiasing, and line box rounding.

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.

Stable approach

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.

Background

Screen-space pattern rendering

Grid and dot backgrounds should account for zoom and pan directly, so pattern spacing and offsets remain visually crisp.

Connections

Screen-space SVG paths

Convert canvas points with canvasToScreen and scale stroke widths and dashes intentionally. This avoids blurry whole-SVG scaling.

Nodes

Stable node-local layout

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.

Overlays

Viewport-native UI

Toolbars, popovers, minimaps, and command surfaces should stay outside the zoomed node geometry so they remain usable at every zoom level.

Implementation Shape

The core idea is small: only the node's outer visual shell receives the zoom scale.

tsx
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>

Takeaway

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.