DEV Community

cnlnr
cnlnr

Posted on

I Solved Text Position Offset During Pinch-to-Zoom in Mobile Code Editors

When developing a mobile code editor, two-finger pinch-to-zoom is an essential interaction feature. Almost all mobile code editors implement zooming by adjusting font sizes, which causes character layouts and line heights to shift away from their original positions, forcing developers to perform dynamic automatic scrolling corrections to fix the offset.

I solved this by changing the core approach: scaling the editor box itself while dynamically updating its width and height. This completely eliminates text position offset at the source.


Core Principle

  1. The Standard Approach Problem: Almost all mobile code editors implement zooming by altering font sizes. Because changing font size recalculates character widths and line wrapping, the text position inevitably shifts away during gestures, requiring developers to write complex logic that calculates scroll deltas to trigger dynamic automatic scrolling corrections.
  2. Direct Editor Box Scaling Solution: Instead of modifying font sizes, the editor box bounds (width and height) scale directly with the pinch gesture. The container frame and its internal content coordinate space scale as a single unified unit. Because relative text placement remains untouched, no text position offset occurs during zooming, eliminating the need for automatic scroll correction entirely.

Demo Implementation

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no, interactive-widget=resizes-content">
  <title>Code Editor Pinch Demo</title>
  <style>
    * { box-sizing: border-box; margin: 0; padding: 0; }
    html, body { width: 100vw; height: 100dvh; overflow: hidden; background: #0d1117; color: #c9d1d9; font-family: monospace; }
    #app { display: flex; flex-direction: column; height: 100dvh; }
    header { height: 40px; padding: 0 12px; display: flex; align-items: center; justify-content: space-between; background: #161b22; font-size: 13px; }
    main { flex: 1; overflow: hidden; padding: 8px; position: relative; }
    #editor { width: 100%; height: 100%; background: #010409; color: #e6edf3; border: 1px solid #30363d; border-radius: 6px; padding: 8px; font-size: 14px; line-height: 1.5; resize: none; outline: none; white-space: pre; overflow: auto; zoom: 1; }
  </style>
</head>
<body>
  <div id="app">
    <header><span>Code Editor</span><span id="info">100%</span></header>
    <main>
      <textarea id="editor" spellcheck="false"></textarea>
    </main>
  </div>

  <script>
    const editor = document.getElementById('editor');
    const info = document.getElementById('info');

    editor.value = `// Mobile Code Editor - Direct Box Resizing Demo
// Direct editor box scaling without text offset or manual scroll correction.

function calculateVectorZoom(baseZoom, currentDistance, startDistance) {
  const scaleFactor = currentDistance / startDistance;
  const targetZoom = Math.max(0.6, Math.min(2.5, baseZoom * scaleFactor));
  return parseFloat(targetZoom.toFixed(2));
}

` + Array.from({ length: 40 }, (_, i) => `console.log("Trace line ${i + 1}");`).join('\n');

    let zoom = 1.0, baseZoom = 1.0, startDist = 0, isPinching = false;

    const getDist = (t) => Math.hypot(t[0].clientX - t[1].clientX, t[0].clientY - t[1].clientY);

    editor.addEventListener('touchstart', (e) => {
      if (e.touches.length === 2) {
        isPinching = true;
        baseZoom = zoom;
        startDist = getDist(e.touches);
      }
    }, { passive: true });

    editor.addEventListener('touchmove', (e) => {
      if (isPinching && e.touches.length === 2) {
        if (e.cancelable) e.preventDefault();
        zoom = Math.max(0.6, Math.min(2.5, baseZoom * (getDist(e.touches) / startDist)));
        requestAnimationFrame(() => {
          editor.style.zoom = zoom.toFixed(2);
          info.innerText = `${Math.round(zoom * 100)}%`;
        });
      }
    }, { passive: false });

    editor.addEventListener('touchend', (e) => { if (e.touches.length < 2) isPinching = false; });
  </script>
</body>
</html>
Enter fullscreen mode Exit fullscreen mode

Top comments (0)