Make logo movement silky-smooth (no jank)

Symptoms

Drag is jumpy/janky.

Likely causes: re-rendering the whole SVG on every move, multiple competing listeners, or layout thrash from repeated reads/writes.

Non-negotiables

Mount the composed SVG once.

During drag: update only the transform on #logo-root via a ref.

Do all drag math in a single requestAnimationFrame (rAF) loop.

Commit React state only on pointerup (not every move).

1) One render, then mutate the DOM

Render the composed SVG once.

Keep a ref to #logo-root: rootRef.

During drag: rootRef.current!.setAttribute('transform', ...)

On pointerup: sync React state with the final {tx, ty} so sliders/exports stay correct.

Rebuilding the SVG string or setState on every pointermove = jank. Don’t do it.

2) Single rAF loop, no redundant work

On pointerdown:

setPointerCapture(e.pointerId), preventDefault()

Cache once: previewRect, startTx, startTy, originX, originY, current scale, rotationDeg.

Set a flag dragging = true.

On pointermove: store the latest clientX/Y in refs only.

Start one rAF loop while dragging:

Read the latest clientX/Y once.

Compute candidate tx, ty = startTx + dx, startTy + dy.

Clamp (see Step 3).

Compose string translate(tx,ty) rotate(rotationDeg) scale(scale) and set it on #logo-root.

On pointerup/cancel: dragging=false, commit {tx,ty} to state, release capture.

3) Clamp cheaply (screen-space, once per frame)

Use the cached previewRect from pointerdown.

For clamp, read one transformed rect per frame:

Temporarily set the candidate transform on the root, then:

const r = rootRef.current!.getBoundingClientRect();


Compute corrections:

let dx = 0, dy = 0;
if (r.left   < previewRect.left)   dx = previewRect.left - r.left;
if (r.right  > previewRect.right)  dx = Math.min(dx, previewRect.right - r.right);
if (r.top    < previewRect.top)    dy = previewRect.top - r.top;
if (r.bottom > previewRect.bottom) dy = Math.min(dy, previewRect.bottom - r.bottom);
tx += dx; ty += dy;


Set the final transform once per frame.

Don’t compute half-sizes in %, don’t mix units. Screen-space rect vs rect is simple and smooth.

4) Hygiene: kill thrash + conflicts

No setState inside pointermove (only on pointerup).

Ensure there is one drag system for the logo (no legacy listeners).

Preview element:

touch-action: none; user-select: none;

No CSS transition on [data-logo-stage] svg or on #logo-root (transitions cause lag).

Any overlays used for guides: pointer-events: none.

Add will-change: transform; to the stage container (helps Chrome).

5) Keep scale/rotation independent

Maintain four refs/state values: tx, ty, scale, rotationDeg.

Always compose the transform in this order:

translate(tx, ty) rotate(rotationDeg) scale(scale)


Drag updates tx/ty only.

Sliders update scale or rotationDeg only.

6) Sanity / quick test

Drag should track the cursor exactly, no hitching.

CPU profile should show one rAF callback, not dozens of re-renders.

Export uses the same transform values (since you commit them on pointerup).

7) If it’s still janky, log these once
console.log('multiListeners?', getEventListeners(previewEl)?.pointermove?.length);
console.log('hasTransition?', getComputedStyle(rootRef.current!).transition);


If multiple pointermove listeners or any transition is present → remove.

Acceptance

Drag is smooth in both Replit and normal Chrome tab.

No frame drops while moving.

Scale/rotation remain stable during drag.

Exported SVG/PNG reflect final position.

If they want a tiny fallback while they refactor: keep overlays and toolbar alive, but temporarily bypass clamp in the rAF loop to confirm the lag disappears (it should). Then reintroduce the screen-space clamp above — it adds almost zero overhead when done once per frame.