d3 / d3-zoom

Pan and zoom SVG, HTML or Canvas using mouse or touch input.
https://d3js.org/d3-zoom
ISC License
501 stars 144 forks source link

for touch interactions, zoom end events have no pointers #228

Closed cambecc closed 3 years ago

cambecc commented 3 years ago

For d3-zoom 2.0.0, passing a zoom "end" event to d3.pointers returns an empty array for touch interactions (on at least iOS), whereas mouse interactions return valid pointer locations.

To repro:

  1. Visit https://bl.ocks.org/cambecc/5c6c9d41ff770abf53b1f104d8d5ad83
  2. Tap in the green box and observe the text output showing each event type and corresponding array of pointers

Expected: start:[[250,88]] end:[[250,88]]

Actual: start:[[250,88]] end:[]

Mouse interactions have the expected behavior.

And because it's likely related, I'll mention that the zoom-on-double-tap behavior exhibits the same problem for all the transitional events:

Expected: start:[[250,88]] end:[[250,88]] start:[[250,88]] end:[[250,88]] start:[[250,88]] zoom:[[250,88]] ... zoom:[[250,88]] end:[[250,88]]

Actual: start:[[250,88]] end:[] start:[[250,88]] end:[] start:[] zoom:[] ... zoom:[] end:[]

Again, mouse zoom-on-double-click interactions have the expected behavior.

cambecc commented 3 years ago

For comparison with d3-zoom 1.8.3 (d3 v5), the call to d3.mouse would return the expected touch position in "end" event handlers. (And interestingly, d3.mouse would return [NaN, NaN] for all double-tap/click transitional events.)

See https://bl.ocks.org/cambecc/cb4db072497444e05804dfc8d237d47b

mbostock commented 3 years ago

I’m pretty sure that this is the expected behavior of d3.pointers: for the touchend event, event.touches is defined to not include the touches that were removed, and thus you would expect it to return the empty array if they are no remaining touches.

https://www.w3.org/TR/touch-events/#event-touchend

If you want the touches that ended, you’ll have to pass event.sourceEvent.changedTouches to d3.pointers instead. d3.mouse would implicitly pull out the event.sourceEvent.changedTouches[0] for you.

mbostock commented 3 years ago

I could see maybe having a d3.changedPointers helper:

function changedPointers(event, node) {
  event = sourceEvent(event);
  if (node === undefined) node = event.currentTarget;
  return Array.from(event.changedTouches || [event], event => pointer(event, node));
}

This is similar to d3.pointers, except you must pass an Event rather than a TouchList or iterable, and in the case of a TouchEvent it uses event.changedTouches instead of event.touches.

cambecc commented 3 years ago

Ahh, I see now. I had a (wrong) mental model that the interaction device is hidden behind the "zoom" abstraction, i.e., that the start, zoom, and end events occur at a location which is exposed by either d3.mouse (v5) or d3.pointers (v6) without needing to know the type of device used for the interaction.

Assuming it was broadly applicable, a helper might be nice. It's not super obvious how to finesse the event to replicate the behavior of d3.mouse. Though it had implicit behavior, d3.mouse was doing useful work for some contexts (like zoom).