From 2d434ae233f42e6aa2ccc3ce0819d243adb8e726 Mon Sep 17 00:00:00 2001 From: Estr Date: Thu, 6 Feb 2025 18:13:01 +0100 Subject: [PATCH 1/3] Implement momentum panning --- src/zoom.js | 65 ++++++++++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 62 insertions(+), 3 deletions(-) diff --git a/src/zoom.js b/src/zoom.js index d5643882..44c325e4 100644 --- a/src/zoom.js +++ b/src/zoom.js @@ -67,7 +67,8 @@ export default function() { touchDelay = 500, wheelDelay = 150, clickDistance2 = 0, - tapDistance = 10; + tapDistance = 10, + momentum = false; function zoom(selection) { selection @@ -269,6 +270,17 @@ export default function() { function mousedowned(event, ...args) { if (touchending || !filter.apply(this, arguments)) return; + + var isDown; + var startX, startY; + var velocityX = 0, velocityY = 0; + var velocityDecay = 0.75; + var velocityLimit = 0.5; + var momentumID; + var translation; + + isDown = true; + var currentTarget = event.currentTarget, g = gesture(this, args, true).event(event), v = select(event.view).on("mousemove.zoom", mousemoved, true).on("mouseup.zoom", mouseupped, true), @@ -282,21 +294,64 @@ export default function() { interrupt(this); g.start(); + startX = x0; + startY = y0; + + cancelMomentumTracking(); + + function beginMomentumTracking() { + cancelMomentumTracking(); + momentumID = requestAnimationFrame(momentumLoop); + } + + function cancelMomentumTracking() { + cancelAnimationFrame(momentumID); + } + + function momentumLoop() { + velocityX *= velocityDecay; + velocityY *= velocityDecay; + + if (translation) { + translation.x += velocityX; + translation.y += velocityY; + + g.event(event) + .zoom("mouse", constrain(translation, g.extent, translateExtent)); + } + + if (Math.abs(velocityX) > velocityLimit || Math.abs(velocityY) > velocityLimit) { + momentumID = requestAnimationFrame(momentumLoop); + } + } + function mousemoved(event) { noevent(event); if (!g.moved) { var dx = event.clientX - x0, dy = event.clientY - y0; g.moved = dx * dx + dy * dy > clickDistance2; } + + translation = translate(g.that.__zoom, g.mouse[0] = pointer(event, currentTarget), g.mouse[1]); + g.event(event) - .zoom("mouse", constrain(translate(g.that.__zoom, g.mouse[0] = pointer(event, currentTarget), g.mouse[1]), g.extent, translateExtent)); + .zoom("mouse", constrain(translation, g.extent, translateExtent)); + + if (momentum) { + if (!isDown) return; + velocityX = event.clientX - startX; + velocityY = event.clientY - startY; + } } function mouseupped(event) { v.on("mousemove.zoom mouseup.zoom", null); + isDown = false; dragEnable(event.view, g.moved); noevent(event); g.event(event).end(); + + momentum && beginMomentumTracking(); } } @@ -387,7 +442,7 @@ export default function() { if (g.taps === 2) { t = pointer(t, this); if (Math.hypot(touchfirst[0] - t[0], touchfirst[1] - t[1]) < tapDistance) { - var p = select(this).on("dblclick.zoom"); + var p = select(this).on('dblclick.zoom'); if (p) p.apply(this, arguments); } } @@ -406,6 +461,10 @@ export default function() { return arguments.length ? (touchable = typeof _ === "function" ? _ : constant(!!_), zoom) : touchable; }; + zoom.momentum = function(_) { + return arguments.length ? (momentum = _, zoom) : momentum; + }; + zoom.extent = function(_) { return arguments.length ? (extent = typeof _ === "function" ? _ : constant([[+_[0][0], +_[0][1]], [+_[1][0], +_[1][1]]]), zoom) : extent; }; From 65b0d2a493a326a1015921f4be95090b49ad20e3 Mon Sep 17 00:00:00 2001 From: Estr Date: Thu, 6 Feb 2025 22:50:27 +0100 Subject: [PATCH 2/3] Implement momentum panning for touch events --- src/zoom.js | 137 +++++++++++++++++++++++++++++----------------------- 1 file changed, 77 insertions(+), 60 deletions(-) diff --git a/src/zoom.js b/src/zoom.js index 44c325e4..35d925c0 100644 --- a/src/zoom.js +++ b/src/zoom.js @@ -11,7 +11,7 @@ import noevent, {nopropagation} from "./noevent.js"; // Ignore right-click, since that should open the context menu. // except for pinch-to-zoom, which is sent as a wheel+ctrlKey event function defaultFilter(event) { - return (!event.ctrlKey || event.type === 'wheel') && !event.button; + return (!event.ctrlKey || event.type === "wheel") && !event.button; } function defaultExtent() { @@ -70,6 +70,57 @@ export default function() { tapDistance = 10, momentum = false; + var isDown; + + var startX, + startY; + + var velocityX = 0, + velocityY = 0; + + var velocityDecay = 0.75; + var velocityLimit = 0.5; + + var momentumID; + var translation; + + function beginMomentumTracking(g) { + cancelMomentumTracking(); + momentumID = requestAnimationFrame(() => momentumLoop(g)); + } + + function cancelMomentumTracking() { + cancelAnimationFrame(momentumID); + } + + function momentumLoop(g) { + velocityX *= velocityDecay; + velocityY *= velocityDecay; + + if (translation) { + translation.x += velocityX; + translation.y += velocityY; + + g.zoom("mouse", constrain(translation, g.extent, translateExtent)); + } + + if (Math.abs(velocityX) > velocityLimit || Math.abs(velocityY) > velocityLimit) { + momentumID = requestAnimationFrame(() => momentumLoop(g)); + } + } + + function setStart(event) { + isDown = true; + startX = event.clientX; + startY = event.clientY; + } + + function setVelocity(event) { + if (!isDown) return; + velocityX = event.clientX - startX; + velocityY = event.clientY - startY; + } + function zoom(selection) { selection .property("__zoom", defaultTransform) @@ -271,16 +322,6 @@ export default function() { function mousedowned(event, ...args) { if (touchending || !filter.apply(this, arguments)) return; - var isDown; - var startX, startY; - var velocityX = 0, velocityY = 0; - var velocityDecay = 0.75; - var velocityLimit = 0.5; - var momentumID; - var translation; - - isDown = true; - var currentTarget = event.currentTarget, g = gesture(this, args, true).event(event), v = select(event.view).on("mousemove.zoom", mousemoved, true).on("mouseup.zoom", mouseupped, true), @@ -294,36 +335,8 @@ export default function() { interrupt(this); g.start(); - startX = x0; - startY = y0; - - cancelMomentumTracking(); - - function beginMomentumTracking() { - cancelMomentumTracking(); - momentumID = requestAnimationFrame(momentumLoop); - } - - function cancelMomentumTracking() { - cancelAnimationFrame(momentumID); - } - - function momentumLoop() { - velocityX *= velocityDecay; - velocityY *= velocityDecay; - - if (translation) { - translation.x += velocityX; - translation.y += velocityY; - - g.event(event) - .zoom("mouse", constrain(translation, g.extent, translateExtent)); - } - - if (Math.abs(velocityX) > velocityLimit || Math.abs(velocityY) > velocityLimit) { - momentumID = requestAnimationFrame(momentumLoop); - } - } + setStart(event); + cancelMomentumTracking(); function mousemoved(event) { noevent(event); @@ -333,25 +346,17 @@ export default function() { } translation = translate(g.that.__zoom, g.mouse[0] = pointer(event, currentTarget), g.mouse[1]); - - g.event(event) - .zoom("mouse", constrain(translation, g.extent, translateExtent)); - - if (momentum) { - if (!isDown) return; - velocityX = event.clientX - startX; - velocityY = event.clientY - startY; - } + g.event(event).zoom("mouse", constrain(translation, g.extent, translateExtent)); + momentum && setVelocity(event); } function mouseupped(event) { v.on("mousemove.zoom mouseup.zoom", null); - isDown = false; dragEnable(event.view, g.moved); noevent(event); g.event(event).end(); - - momentum && beginMomentumTracking(); + isDown = false; + momentum && beginMomentumTracking(g); } } @@ -385,8 +390,13 @@ export default function() { if (touchstarting) touchstarting = clearTimeout(touchstarting); + setStart(t); + cancelMomentumTracking(); + if (started) { - if (g.taps < 2) touchfirst = p[0], touchstarting = setTimeout(function() { touchstarting = null; }, touchDelay); + if (g.taps < 2) touchfirst = p[0], touchstarting = setTimeout(function() { + touchstarting = null; + }, touchDelay); interrupt(this); g.start(); } @@ -404,20 +414,22 @@ export default function() { if (g.touch0 && g.touch0[2] === t.identifier) g.touch0[0] = p; else if (g.touch1 && g.touch1[2] === t.identifier) g.touch1[0] = p; } - t = g.that.__zoom; + + var z = g.that.__zoom; if (g.touch1) { var p0 = g.touch0[0], l0 = g.touch0[1], p1 = g.touch1[0], l1 = g.touch1[1], dp = (dp = p1[0] - p0[0]) * dp + (dp = p1[1] - p0[1]) * dp, dl = (dl = l1[0] - l0[0]) * dl + (dl = l1[1] - l0[1]) * dl; - t = scale(t, Math.sqrt(dp / dl)); + z = scale(z, Math.sqrt(dp / dl)); p = [(p0[0] + p1[0]) / 2, (p0[1] + p1[1]) / 2]; l = [(l0[0] + l1[0]) / 2, (l0[1] + l1[1]) / 2]; - } - else if (g.touch0) p = g.touch0[0], l = g.touch0[1]; + } else if (g.touch0) p = g.touch0[0], l = g.touch0[1]; else return; - g.zoom("touch", constrain(translate(t, p, l), g.extent, translateExtent)); + translation = translate(z, p, l); + g.zoom("touch", constrain(translation, g.extent, translateExtent)); + momentum && setVelocity(t); } function touchended(event, ...args) { @@ -428,7 +440,9 @@ export default function() { nopropagation(event); if (touchending) clearTimeout(touchending); - touchending = setTimeout(function() { touchending = null; }, touchDelay); + touchending = setTimeout(function() { + touchending = null; + }, touchDelay); for (i = 0; i < n; ++i) { t = touches[i]; if (g.touch0 && g.touch0[2] === t.identifier) delete g.touch0; @@ -447,6 +461,9 @@ export default function() { } } } + + isDown = false; + momentum && beginMomentumTracking(g); } zoom.wheelDelta = function(_) { From b6853570f988a3babfc29fef0e264563052cddc0 Mon Sep 17 00:00:00 2001 From: Estr Date: Fri, 14 Feb 2025 09:29:50 +0100 Subject: [PATCH 3/3] Calculate average mousemove velocity --- src/zoom.js | 267 +++++++++++++++++++++++++++++----------------------- 1 file changed, 151 insertions(+), 116 deletions(-) diff --git a/src/zoom.js b/src/zoom.js index 35d925c0..73ef3c4e 100644 --- a/src/zoom.js +++ b/src/zoom.js @@ -1,12 +1,12 @@ -import {dispatch} from "d3-dispatch"; -import {dragDisable, dragEnable} from "d3-drag"; -import {interpolateZoom} from "d3-interpolate"; -import {select, pointer} from "d3-selection"; -import {interrupt} from "d3-transition"; +import { dispatch } from "d3-dispatch"; +import { dragDisable, dragEnable } from "d3-drag"; +import { interpolateZoom } from "d3-interpolate"; +import { select, pointer } from "d3-selection"; +import { interrupt } from "d3-transition"; import constant from "./constant.js"; import ZoomEvent from "./event.js"; -import {Transform, identity} from "./transform.js"; -import noevent, {nopropagation} from "./noevent.js"; +import { Transform, identity } from "./transform.js"; +import noevent, { nopropagation } from "./noevent.js"; // Ignore right-click, since that should open the context menu. // except for pinch-to-zoom, which is sent as a wheel+ctrlKey event @@ -41,9 +41,9 @@ function defaultTouchable() { function defaultConstrain(transform, extent, translateExtent) { var dx0 = transform.invertX(extent[0][0]) - translateExtent[0][0], - dx1 = transform.invertX(extent[1][0]) - translateExtent[1][0], - dy0 = transform.invertY(extent[0][1]) - translateExtent[0][1], - dy1 = transform.invertY(extent[1][1]) - translateExtent[1][1]; + dx1 = transform.invertX(extent[1][0]) - translateExtent[1][0], + dy0 = transform.invertY(extent[0][1]) - translateExtent[0][1], + dy1 = transform.invertY(extent[1][1]) - translateExtent[1][1]; return transform.translate( dx1 > dx0 ? (dx0 + dx1) / 2 : Math.min(0, dx0) || Math.max(0, dx1), dy1 > dy0 ? (dy0 + dy1) / 2 : Math.min(0, dy0) || Math.max(0, dy1) @@ -52,34 +52,37 @@ function defaultConstrain(transform, extent, translateExtent) { export default function() { var filter = defaultFilter, - extent = defaultExtent, - constrain = defaultConstrain, - wheelDelta = defaultWheelDelta, - touchable = defaultTouchable, - scaleExtent = [0, Infinity], - translateExtent = [[-Infinity, -Infinity], [Infinity, Infinity]], - duration = 250, - interpolate = interpolateZoom, - listeners = dispatch("start", "zoom", "end"), - touchstarting, - touchfirst, - touchending, - touchDelay = 500, - wheelDelay = 150, - clickDistance2 = 0, - tapDistance = 10, - momentum = false; - - var isDown; - - var startX, - startY; - - var velocityX = 0, - velocityY = 0; - - var velocityDecay = 0.75; - var velocityLimit = 0.5; + extent = defaultExtent, + constrain = defaultConstrain, + wheelDelta = defaultWheelDelta, + touchable = defaultTouchable, + scaleExtent = [0, Infinity], + translateExtent = [[-Infinity, -Infinity], [Infinity, Infinity]], + duration = 250, + interpolate = interpolateZoom, + listeners = dispatch("start", "zoom", "end"), + touchstarting, + touchfirst, + touchending, + touchDelay = 500, + wheelDelay = 150, + clickDistance2 = 0, + tapDistance = 10, + momentumEnabled = false; + + var momentum = { + isMoving: false, + x: 0, + y: 0, + prevX: 0, + prevY: 0, + time: 0, + velocityX: 0, + velocityY: 0, + velocityDecay: 0.7, + velocityLimit: 0.5, + interval: null, + } var momentumID; var translation; @@ -94,44 +97,62 @@ export default function() { } function momentumLoop(g) { - velocityX *= velocityDecay; - velocityY *= velocityDecay; + var decay = momentum.velocityDecay; + var limit = momentum.velocityLimit; + + momentum.velocityX *= decay; + momentum.velocityY *= decay; if (translation) { - translation.x += velocityX; - translation.y += velocityY; + translation.x += momentum.velocityX; + translation.y += momentum.velocityY; g.zoom("mouse", constrain(translation, g.extent, translateExtent)); } - if (Math.abs(velocityX) > velocityLimit || Math.abs(velocityY) > velocityLimit) { + if (Math.abs(momentum.velocityX) > limit || Math.abs(momentum.velocityY) > limit) { momentumID = requestAnimationFrame(() => momentumLoop(g)); } } - function setStart(event) { - isDown = true; - startX = event.clientX; - startY = event.clientY; + function onStart({ clientX, clientY }) { + momentum.isMoving = true; + momentum.prevX = clientX; + momentum.prevY = clientY; + momentum.x = clientX; + momentum.y = clientY; + momentum.time = performance.now(); + } + + function onMove({ clientX, clientY, movementX, movementY }) { + if (!momentum.isMoving) return; + + momentum.x = clientX; + momentum.y = clientY; + + var time = performance.now(); + var deltaTime = time - momentum.time; + + momentum.velocityX = movementX / deltaTime * 10; + momentum.velocityY = movementY / deltaTime * 10; + momentum.time = time; } - function setVelocity(event) { - if (!isDown) return; - velocityX = event.clientX - startX; - velocityY = event.clientY - startY; + function onEnd() { + clearInterval(momentum.interval); } function zoom(selection) { selection - .property("__zoom", defaultTransform) - .on("wheel.zoom", wheeled, {passive: false}) - .on("mousedown.zoom", mousedowned) - .on("dblclick.zoom", dblclicked) + .property("__zoom", defaultTransform) + .on("wheel.zoom", wheeled, { passive: false }) + .on("mousedown.zoom", mousedowned) + .on("dblclick.zoom", dblclicked) .filter(touchable) - .on("touchstart.zoom", touchstarted) - .on("touchmove.zoom", touchmoved) - .on("touchend.zoom touchcancel.zoom", touchended) - .style("-webkit-tap-highlight-color", "rgba(0,0,0,0)"); + .on("touchstart.zoom", touchstarted) + .on("touchmove.zoom", touchmoved) + .on("touchend.zoom touchcancel.zoom", touchended) + .style("-webkit-tap-highlight-color", "rgba(0,0,0,0)"); } zoom.transform = function(collection, transform, point, event) { @@ -153,7 +174,7 @@ export default function() { zoom.scaleBy = function(selection, k, p, event) { zoom.scaleTo(selection, function() { var k0 = this.__zoom.k, - k1 = typeof k === "function" ? k.apply(this, arguments) : k; + k1 = typeof k === "function" ? k.apply(this, arguments) : k; return k0 * k1; }, p, event); }; @@ -161,10 +182,10 @@ export default function() { zoom.scaleTo = function(selection, k, p, event) { zoom.transform(selection, function() { var e = extent.apply(this, arguments), - t0 = this.__zoom, - p0 = p == null ? centroid(e) : typeof p === "function" ? p.apply(this, arguments) : p, - p1 = t0.invert(p0), - k1 = typeof k === "function" ? k.apply(this, arguments) : k; + t0 = this.__zoom, + p0 = p == null ? centroid(e) : typeof p === "function" ? p.apply(this, arguments) : p, + p1 = t0.invert(p0), + k1 = typeof k === "function" ? k.apply(this, arguments) : k; return constrain(translate(scale(t0, k1), p0, p1), e, translateExtent); }, p, event); }; @@ -181,8 +202,8 @@ export default function() { zoom.translateTo = function(selection, x, y, p, event) { zoom.transform(selection, function() { var e = extent.apply(this, arguments), - t = this.__zoom, - p0 = p == null ? centroid(e) : typeof p === "function" ? p.apply(this, arguments) : p; + t = this.__zoom, + p0 = p == null ? centroid(e) : typeof p === "function" ? p.apply(this, arguments) : p; return constrain(identity.translate(p0[0], p0[1]).scale(t.k).translate( typeof x === "function" ? -x.apply(this, arguments) : -x, typeof y === "function" ? -y.apply(this, arguments) : -y @@ -206,24 +227,31 @@ export default function() { function schedule(transition, transform, point, event) { transition - .on("start.zoom", function() { gesture(this, arguments).event(event).start(); }) - .on("interrupt.zoom end.zoom", function() { gesture(this, arguments).event(event).end(); }) - .tween("zoom", function() { - var that = this, - args = arguments, - g = gesture(that, args).event(event), - e = extent.apply(that, args), - p = point == null ? centroid(e) : typeof point === "function" ? point.apply(that, args) : point, - w = Math.max(e[1][0] - e[0][0], e[1][1] - e[0][1]), - a = that.__zoom, - b = typeof transform === "function" ? transform.apply(that, args) : transform, - i = interpolate(a.invert(p).concat(w / a.k), b.invert(p).concat(w / b.k)); - return function(t) { - if (t === 1) t = b; // Avoid rounding error on end. - else { var l = i(t), k = w / l[2]; t = new Transform(k, p[0] - l[0] * k, p[1] - l[1] * k); } - g.zoom(null, t); - }; - }); + .on("start.zoom", function() { + gesture(this, arguments).event(event).start(); + }) + .on("interrupt.zoom end.zoom", function() { + gesture(this, arguments).event(event).end(); + }) + .tween("zoom", function() { + var that = this, + args = arguments, + g = gesture(that, args).event(event), + e = extent.apply(that, args), + p = point == null ? centroid(e) : typeof point === "function" ? point.apply(that, args) : point, + w = Math.max(e[1][0] - e[0][0], e[1][1] - e[0][1]), + a = that.__zoom, + b = typeof transform === "function" ? transform.apply(that, args) : transform, + i = interpolate(a.invert(p).concat(w / a.k), b.invert(p).concat(w / b.k)); + return function(t) { + if (t === 1) t = b; // Avoid rounding error on end. + else { + var l = i(t), k = w / l[2]; + t = new Transform(k, p[0] - l[0] * k, p[1] - l[1] * k); + } + g.zoom(null, t); + }; + }); } function gesture(that, args, clean) { @@ -286,9 +314,9 @@ export default function() { function wheeled(event, ...args) { if (!filter.apply(this, arguments)) return; var g = gesture(this, args).event(event), - t = this.__zoom, - k = Math.max(scaleExtent[0], Math.min(scaleExtent[1], t.k * Math.pow(2, wheelDelta.apply(this, arguments)))), - p = pointer(event); + t = this.__zoom, + k = Math.max(scaleExtent[0], Math.min(scaleExtent[1], t.k * Math.pow(2, wheelDelta.apply(this, arguments)))), + p = pointer(event); // If the mouse is in the same location as before, reuse it. // If there were recent wheel events, reset the wheel idle timeout. @@ -323,11 +351,11 @@ export default function() { if (touchending || !filter.apply(this, arguments)) return; var currentTarget = event.currentTarget, - g = gesture(this, args, true).event(event), - v = select(event.view).on("mousemove.zoom", mousemoved, true).on("mouseup.zoom", mouseupped, true), - p = pointer(event, currentTarget), - x0 = event.clientX, - y0 = event.clientY; + g = gesture(this, args, true).event(event), + v = select(event.view).on("mousemove.zoom", mousemoved, true).on("mouseup.zoom", mouseupped, true), + p = pointer(event, currentTarget), + x0 = event.clientX, + y0 = event.clientY; dragDisable(event.view); nopropagation(event); @@ -335,7 +363,7 @@ export default function() { interrupt(this); g.start(); - setStart(event); + onStart(event); cancelMomentumTracking(); function mousemoved(event) { @@ -347,7 +375,7 @@ export default function() { translation = translate(g.that.__zoom, g.mouse[0] = pointer(event, currentTarget), g.mouse[1]); g.event(event).zoom("mouse", constrain(translation, g.extent, translateExtent)); - momentum && setVelocity(event); + momentumEnabled && onMove(event); } function mouseupped(event) { @@ -355,18 +383,22 @@ export default function() { dragEnable(event.view, g.moved); noevent(event); g.event(event).end(); - isDown = false; - momentum && beginMomentumTracking(g); + + if (momentumEnabled) { + momentum.isMoving = false; + beginMomentumTracking(g); + onEnd(); + } } } function dblclicked(event, ...args) { if (!filter.apply(this, arguments)) return; var t0 = this.__zoom, - p0 = pointer(event.changedTouches ? event.changedTouches[0] : event, this), - p1 = t0.invert(p0), - k1 = t0.k * (event.shiftKey ? 0.5 : 2), - t1 = constrain(translate(scale(t0, k1), p0, p1), extent.apply(this, args), translateExtent); + p0 = pointer(event.changedTouches ? event.changedTouches[0] : event, this), + p1 = t0.invert(p0), + k1 = t0.k * (event.shiftKey ? 0.5 : 2), + t1 = constrain(translate(scale(t0, k1), p0, p1), extent.apply(this, args), translateExtent); noevent(event); if (duration > 0) select(this).transition().duration(duration).call(schedule, t1, p0, event); @@ -376,9 +408,9 @@ export default function() { function touchstarted(event, ...args) { if (!filter.apply(this, arguments)) return; var touches = event.touches, - n = touches.length, - g = gesture(this, args, event.changedTouches.length === n).event(event), - started, i, t, p; + n = touches.length, + g = gesture(this, args, event.changedTouches.length === n).event(event), + started, i, t, p; nopropagation(event); for (i = 0; i < n; ++i) { @@ -390,7 +422,7 @@ export default function() { if (touchstarting) touchstarting = clearTimeout(touchstarting); - setStart(t); + onStart(t); cancelMomentumTracking(); if (started) { @@ -405,8 +437,8 @@ export default function() { function touchmoved(event, ...args) { if (!this.__zooming) return; var g = gesture(this, args).event(event), - touches = event.changedTouches, - n = touches.length, i, t, p, l; + touches = event.changedTouches, + n = touches.length, i, t, p, l; noevent(event); for (i = 0; i < n; ++i) { @@ -418,9 +450,9 @@ export default function() { var z = g.that.__zoom; if (g.touch1) { var p0 = g.touch0[0], l0 = g.touch0[1], - p1 = g.touch1[0], l1 = g.touch1[1], - dp = (dp = p1[0] - p0[0]) * dp + (dp = p1[1] - p0[1]) * dp, - dl = (dl = l1[0] - l0[0]) * dl + (dl = l1[1] - l0[1]) * dl; + p1 = g.touch1[0], l1 = g.touch1[1], + dp = (dp = p1[0] - p0[0]) * dp + (dp = p1[1] - p0[1]) * dp, + dl = (dl = l1[0] - l0[0]) * dl + (dl = l1[1] - l0[1]) * dl; z = scale(z, Math.sqrt(dp / dl)); p = [(p0[0] + p1[0]) / 2, (p0[1] + p1[1]) / 2]; l = [(l0[0] + l1[0]) / 2, (l0[1] + l1[1]) / 2]; @@ -429,14 +461,14 @@ export default function() { translation = translate(z, p, l); g.zoom("touch", constrain(translation, g.extent, translateExtent)); - momentum && setVelocity(t); + momentumEnabled && onMove(t); } function touchended(event, ...args) { if (!this.__zooming) return; var g = gesture(this, args).event(event), - touches = event.changedTouches, - n = touches.length, i, t; + touches = event.changedTouches, + n = touches.length, i, t; nopropagation(event); if (touchending) clearTimeout(touchending); @@ -456,14 +488,17 @@ export default function() { if (g.taps === 2) { t = pointer(t, this); if (Math.hypot(touchfirst[0] - t[0], touchfirst[1] - t[1]) < tapDistance) { - var p = select(this).on('dblclick.zoom'); + var p = select(this).on("dblclick.zoom"); if (p) p.apply(this, arguments); } } } - isDown = false; - momentum && beginMomentumTracking(g); + if (momentumEnabled) { + momentum.isMoving = false; + beginMomentumTracking(g); + onEnd(); + } } zoom.wheelDelta = function(_) { @@ -479,7 +514,7 @@ export default function() { }; zoom.momentum = function(_) { - return arguments.length ? (momentum = _, zoom) : momentum; + return arguments.length ? (momentumEnabled = _, zoom) : momentumEnabled; }; zoom.extent = function(_) {