HTML Canvas Drawing Works on Desktop But Not Mobile

Summary

A signature‑capture canvas worked flawlessly on desktop but failed to register touch drawing on mobile Safari. The canvas rendered correctly, but touch events never produced usable coordinates, resulting in no visible strokes.

Root Cause

The underlying issue was incorrect handling of touch events on iOS Safari, specifically:

  • Using touches instead of changedTouches during touchend and sometimes touchmove, which results in missing coordinates.
  • Event listeners bound to class methods without proper this context, causing this.ctx or this.drawing to be undefined at runtime.
  • Canvas scaling mismatch between CSS size and actual pixel size, causing touches to map to the wrong coordinates.
  • iOS Safari requiring non‑passive listeners for preventDefault() to work, but the code only partially satisfies this.

Why This Happens in Real Systems

Real production systems frequently hit this problem because:

  • iOS Safari has unique touch‑event quirks not present on desktop browsers.
  • Developers often assume mouse and touch events behave similarly, but they differ in coordinate models and event lifecycles.
  • Angular component methods lose their this binding unless explicitly preserved.
  • High‑DPI scaling introduces coordinate distortion if not handled consistently.

Real-World Impact

When this bug appears in production:

  • Users cannot sign forms, blocking onboarding or checkout flows.
  • Support teams receive reports of “the signature pad is broken.”
  • Conversion rates drop because mobile users represent the majority of traffic.
  • QA teams struggle because the issue does not reproduce on desktop.

Example or Code (if necessary and relevant)

Below is a corrected event‑coordinate handler using changedTouches and ensuring proper binding:

getXY(event: MouseEvent | TouchEvent) {
  const rect = this.signatureCanvas.nativeElement.getBoundingClientRect();
  if (event instanceof MouseEvent) {
    return {
      x: event.clientX - rect.left,
      y: event.clientY - rect.top
    };
  }
  const touch = event.changedTouches[0];
  return {
    x: touch.clientX - rect.left,
    y: touch.clientY - rect.top
  };
}

How Senior Engineers Fix It

Experienced engineers typically resolve this by:

  • Normalizing all pointer input using pointerdown, pointermove, pointerup instead of mixing mouse and touch events.
  • Ensuring all event handlers are bound (this.startDraw = this.startDraw.bind(this) or using arrow functions everywhere).
  • Using changedTouches for reliable touch coordinates.
  • Applying consistent scaling between CSS pixels and canvas pixels.
  • Adding { passive: false } to all touch listeners so preventDefault() works on iOS.

Why Juniors Miss It

Less experienced developers often overlook this because:

  • They assume touches[0] always exists, but it disappears on touchend.
  • They don’t realize that iOS Safari blocks preventDefault() unless listeners are explicitly non‑passive.
  • They rely on desktop testing, missing mobile‑specific behavior.
  • They are unfamiliar with pointer events, which simplify cross‑device input handling.
  • They don’t notice that method references lose their class context when passed directly to event listeners.

This combination makes the bug subtle, frustrating, and easy to miss without deep experience in mobile browser behavior.

Leave a Comment