diff --git a/web/app.js b/web/app.js index 684f29bfb..4c0bc95bf 100644 --- a/web/app.js +++ b/web/app.js @@ -217,7 +217,9 @@ const PDFViewerApplication = { _contentLength: null, _saveInProgress: false, _wheelUnusedTicks: 0, + _wheelUnusedFactor: 1, _touchUnusedTicks: 0, + _touchUnusedFactor: 1, _PDFBug: null, _hasAnnotationEditors: false, _title: document.title, @@ -2058,6 +2060,23 @@ const PDFViewerApplication = { return wholeTicks; }, + _accumulateFactor(previousScale, factor, prop) { + if (factor === 1) { + return 1; + } + // If the direction changed, reset the accumulated factor. + if ((this[prop] > 1 && factor < 1) || (this[prop] < 1 && factor > 1)) { + this[prop] = 1; + } + + const newFactor = + Math.floor(previousScale * factor * this[prop] * 100) / + (100 * previousScale); + this[prop] = factor / newFactor; + + return newFactor; + }, + /** * Should be called *after* all pages have loaded, or if an error occurred, * to unblock the "load" event; see https://bugzilla.mozilla.org/show_bug.cgi?id=1618553 @@ -2628,8 +2647,11 @@ function setZoomDisabledTimeout() { } function webViewerWheel(evt) { - const { pdfViewer, supportedMouseWheelZoomModifierKeys } = - PDFViewerApplication; + const { + pdfViewer, + supportedMouseWheelZoomModifierKeys, + supportsPinchToZoom, + } = PDFViewerApplication; if (pdfViewer.isInPresentationMode) { return; @@ -2653,45 +2675,61 @@ function webViewerWheel(evt) { return; } - // It is important that we query deltaMode before delta{X,Y}, so that - // Firefox doesn't switch to DOM_DELTA_PIXEL mode for compat with other - // browsers, see https://bugzilla.mozilla.org/show_bug.cgi?id=1392460. - const deltaMode = evt.deltaMode; - const delta = normalizeWheelEventDirection(evt); const previousScale = pdfViewer.currentScale; + if (isPinchToZoom && supportsPinchToZoom) { + // The following formula is a bit strange but it comes from: + // https://searchfox.org/mozilla-central/rev/d62c4c4d5547064487006a1506287da394b64724/widget/InputData.cpp#618-626 + let scaleFactor = Math.exp(-evt.deltaY / 100); + scaleFactor = PDFViewerApplication._accumulateFactor( + previousScale, + scaleFactor, + "_wheelUnusedFactor" + ); + if (scaleFactor < 1) { + PDFViewerApplication.zoomOut(null, scaleFactor); + } else if (scaleFactor > 1) { + PDFViewerApplication.zoomIn(null, scaleFactor); + } + } else { + // It is important that we query deltaMode before delta{X,Y}, so that + // Firefox doesn't switch to DOM_DELTA_PIXEL mode for compat with other + // browsers, see https://bugzilla.mozilla.org/show_bug.cgi?id=1392460. + const deltaMode = evt.deltaMode; + const delta = normalizeWheelEventDirection(evt); - let ticks = 0; - if ( - deltaMode === WheelEvent.DOM_DELTA_LINE || - deltaMode === WheelEvent.DOM_DELTA_PAGE - ) { - // For line-based devices, use one tick per event, because different - // OSs have different defaults for the number lines. But we generally - // want one "clicky" roll of the wheel (which produces one event) to - // adjust the zoom by one step. - if (Math.abs(delta) >= 1) { - ticks = Math.sign(delta); + let ticks = 0; + if ( + deltaMode === WheelEvent.DOM_DELTA_LINE || + deltaMode === WheelEvent.DOM_DELTA_PAGE + ) { + // For line-based devices, use one tick per event, because different + // OSs have different defaults for the number lines. But we generally + // want one "clicky" roll of the wheel (which produces one event) to + // adjust the zoom by one step. + if (Math.abs(delta) >= 1) { + ticks = Math.sign(delta); + } else { + // If we're getting fractional lines (I can't think of a scenario + // this might actually happen), be safe and use the accumulator. + ticks = PDFViewerApplication._accumulateTicks( + delta, + "_wheelUnusedTicks" + ); + } } else { - // If we're getting fractional lines (I can't think of a scenario - // this might actually happen), be safe and use the accumulator. + // pixel-based devices + const PIXELS_PER_LINE_SCALE = 30; ticks = PDFViewerApplication._accumulateTicks( - delta, + delta / PIXELS_PER_LINE_SCALE, "_wheelUnusedTicks" ); } - } else { - // pixel-based devices - const PIXELS_PER_LINE_SCALE = 30; - ticks = PDFViewerApplication._accumulateTicks( - delta / PIXELS_PER_LINE_SCALE, - "_wheelUnusedTicks" - ); - } - if (ticks < 0) { - PDFViewerApplication.zoomOut(-ticks); - } else if (ticks > 0) { - PDFViewerApplication.zoomIn(ticks); + if (ticks < 0) { + PDFViewerApplication.zoomOut(-ticks); + } else if (ticks > 0) { + PDFViewerApplication.zoomIn(ticks); + } } const currentScale = pdfViewer.currentScale; @@ -2725,12 +2763,15 @@ function webViewerTouchStart(evt) { return; } - const [touch0, touch1] = evt.touches; + let [touch0, touch1] = evt.touches; + if (touch0.identifier > touch1.identifier) { + [touch0, touch1] = [touch1, touch0]; + } PDFViewerApplication._touchInfo = { - centerX: (touch0.pageX + touch1.pageX) / 2, - centerY: (touch0.pageY + touch1.pageY) / 2, - distance: - Math.hypot(touch0.pageX - touch1.pageX, touch0.pageY - touch1.pageY) || 1, + touch0X: touch0.pageX, + touch0Y: touch0.pageY, + touch1X: touch1.pageX, + touch1Y: touch1.pageY, }; } @@ -2740,42 +2781,88 @@ function webViewerTouchMove(evt) { } const { pdfViewer, _touchInfo, supportsPinchToZoom } = PDFViewerApplication; - const [touch0, touch1] = evt.touches; + let [touch0, touch1] = evt.touches; + if (touch0.identifier > touch1.identifier) { + [touch0, touch1] = [touch1, touch0]; + } const { pageX: page0X, pageY: page0Y } = touch0; const { pageX: page1X, pageY: page1Y } = touch1; - const distance = Math.hypot(page0X - page1X, page0Y - page1Y) || 1; - const { distance: previousDistance } = _touchInfo; - const scaleFactor = distance / previousDistance; + const { + touch0X: pTouch0X, + touch0Y: pTouch0Y, + touch1X: pTouch1X, + touch1Y: pTouch1Y, + } = _touchInfo; - if (supportsPinchToZoom && Math.abs(scaleFactor - 1) <= 1e-2) { - // Scale increase/decrease isn't significant enough. + if ( + Math.abs(pTouch0X - page0X) <= 1 && + Math.abs(pTouch0Y - page0Y) <= 1 && + Math.abs(pTouch1X - page1X) <= 1 && + Math.abs(pTouch1Y - page1Y) <= 1 + ) { + // Touches are really too close and it's hard do some basic + // geometry in order to guess something. return; } - const { centerX, centerY } = _touchInfo; - const diff0X = page0X - centerX; - const diff1X = page1X - centerX; - const diff0Y = page0Y - centerY; - const diff1Y = page1Y - centerY; - const dotProduct = diff0X * diff1X + diff0Y * diff1Y; - if (dotProduct >= 0) { - // The two touches go in almost the same direction. - return; + _touchInfo.touch0X = page0X; + _touchInfo.touch0Y = page0Y; + _touchInfo.touch1X = page1X; + _touchInfo.touch1Y = page1Y; + + if (pTouch0X === page0X && pTouch0Y === page0Y) { + // First touch is fixed, if the vectors are collinear then we've a pinch. + const v1X = pTouch1X - page0X; + const v1Y = pTouch1Y - page0Y; + const v2X = page1X - page0X; + const v2Y = page1Y - page0Y; + const det = v1X * v2Y - v1Y * v2X; + // 0.02 is approximatively sin(0.15deg). + if (Math.abs(det) > 0.02 * Math.hypot(v1X, v1Y) * Math.hypot(v2X, v2Y)) { + return; + } + } else if (pTouch1X === page1X && pTouch1Y === page1Y) { + // Second touch is fixed, if the vectors are collinear then we've a pinch. + const v1X = pTouch0X - page1X; + const v1Y = pTouch0Y - page1Y; + const v2X = page0X - page1X; + const v2Y = page0Y - page1Y; + const det = v1X * v2Y - v1Y * v2X; + if (Math.abs(det) > 0.02 * Math.hypot(v1X, v1Y) * Math.hypot(v2X, v2Y)) { + return; + } + } else { + const diff0X = page0X - pTouch0X; + const diff1X = page1X - pTouch1X; + const diff0Y = page0Y - pTouch0Y; + const diff1Y = page1Y - pTouch1Y; + const dotProduct = diff0X * diff1X + diff0Y * diff1Y; + if (dotProduct >= 0) { + // The two touches go in almost the same direction. + return; + } } evt.preventDefault(); + const distance = Math.hypot(page0X - page1X, page0Y - page1Y) || 1; + const pDistance = Math.hypot(pTouch0X - pTouch1X, pTouch0Y - pTouch1Y) || 1; const previousScale = pdfViewer.currentScale; if (supportsPinchToZoom) { - if (scaleFactor < 1) { - PDFViewerApplication.zoomOut(null, scaleFactor); - } else { - PDFViewerApplication.zoomIn(null, scaleFactor); + const newScaleFactor = PDFViewerApplication._accumulateFactor( + previousScale, + distance / pDistance, + "_touchUnusedFactor" + ); + if (newScaleFactor < 1) { + PDFViewerApplication.zoomOut(null, newScaleFactor); + } else if (newScaleFactor > 1) { + PDFViewerApplication.zoomIn(null, newScaleFactor); } } else { const PIXELS_PER_LINE_SCALE = 30; const ticks = PDFViewerApplication._accumulateTicks( - (distance - previousDistance) / PIXELS_PER_LINE_SCALE, + (distance - pDistance) / PIXELS_PER_LINE_SCALE, "_touchUnusedTicks" ); if (ticks < 0) { @@ -2788,9 +2875,8 @@ function webViewerTouchMove(evt) { const currentScale = pdfViewer.currentScale; if (previousScale !== currentScale) { const scaleCorrectionFactor = currentScale / previousScale - 1; - const newCenterX = (_touchInfo.centerX = (page0X + page1X) / 2); - const newCenterY = (_touchInfo.centerY = (page0Y + page1Y) / 2); - _touchInfo.distance = distance; + const newCenterX = (page0X + page1X) / 2; + const newCenterY = (page0Y + page1Y) / 2; const [top, left] = pdfViewer.containerTopLeft; const dx = newCenterX - left; @@ -2807,8 +2893,8 @@ function webViewerTouchEnd(evt) { evt.preventDefault(); PDFViewerApplication._touchInfo = null; - PDFViewerApplication.pdfViewer.refresh(); PDFViewerApplication._touchUnusedTicks = 0; + PDFViewerApplication._touchUnusedFactor = 1; } function webViewerClick(evt) {