demo · v130

predicted events forecast

Sister API of getCoalescedEvents(): getPredictedEvents(). The spec change applies to both — predicted events on JS-constructed PointerEvents now keep their own offsetX/Y instead of being recomputed against the parent target. The pad below draws the actual pointer trail in black and the predicted (extrapolated) trail in blue.

trusted browser pad — draw on me with mouse/pen/touch
untrusted (JS-dispatched) pad — replay
actual pointer trail predicted continuation (1 frame ahead) coalesced sub-event positions
draw on the left pad — coalesced and predicted counts will show here
offsetX/Y preservation probe

Compares the coordinates supplied to JS-constructed sub-events with the values returned after untrusted dispatch.

list # recorded offsetX/Y replayed offsetX/Y status
No offset comparison has run yet.
Replay a drawing or run the deterministic probe to compare offsets.

why this matters

Latency-sensitive drawing apps (ink, signature, freehand annotation) use getPredictedEvents() to render ahead of where the user has actually moved, masking the input lag between OS-level pointer dispatch and the next paint. Pre-Chrome 130, dispatching a synthetic PointerEvent with your own predicted list would lose the per-event coordinates — the engine recomputed offsetX/Y against the parent's target. Frameworks that replay or fan out pointer streams (record/replay, accessibility-driven input, cross-document handoff) could not faithfully reconstruct the original geometry.

The spec text now reads: "For untrusted events, the populated entries in the coalesced or predicted events list remain unchanged" — engine doesn't touch your synthetic target or offsetX/Y.

the code

// inside a real pointermove handler
canvas.addEventListener("pointermove", (ev) => {
  // 1. trusted coalesced sub-events: smooth ink
  for (const c of ev.getCoalescedEvents()) drawDot(c.offsetX, c.offsetY);

  // 2. predicted continuation: draw a few frames ahead, then erase
  //    when the real pointer catches up
  if (ev.getPredictedEvents) {
    for (const p of ev.getPredictedEvents()) drawForecast(p.offsetX, p.offsetY);
  }
});

// later, replay the recorded pointer track as untrusted events
const replayPad = document.getElementById("replay");
for (const sample of recorded) {
  const sub = new PointerEvent("pointermove", { ...sample });
  // In Chrome 130+, the dispatched event's predicted sub-events keep their
  // offsetX/Y. Pre-130, they were recomputed against replayPad's bbox.
  const parent = new PointerEvent("pointermove", {
    bubbles: true,
    coalescedEvents: sample.coalesced,
    predictedEvents: sample.predicted,
  });
  replayPad.dispatchEvent(parent);
}

see also