Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[api-minor] Add the pinch-to-zoom feature (bug 1677933) #15886

Merged
merged 1 commit into from Jan 4, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
152 changes: 138 additions & 14 deletions web/app.js
Expand Up @@ -122,6 +122,10 @@ class DefaultExternalServices {
throw new Error("Not implemented: createScripting");
}

static get supportsPinchToZoom() {
return shadow(this, "supportsPinchToZoom", true);
}

static get supportsIntegratedFind() {
return shadow(this, "supportsIntegratedFind", false);
}
Expand Down Expand Up @@ -213,10 +217,12 @@ const PDFViewerApplication = {
_contentLength: null,
_saveInProgress: false,
_wheelUnusedTicks: 0,
_touchUnusedTicks: 0,
_PDFBug: null,
_hasAnnotationEditors: false,
_title: document.title,
_printAnnotationStoragePromise: null,
_touchInfo: null,

// Called once when the document is loaded.
async initialize(appConfig) {
Expand Down Expand Up @@ -651,21 +657,25 @@ const PDFViewerApplication = {
return this._initializedCapability.promise;
},

zoomIn(steps) {
zoomIn(steps, scaleFactor) {
if (this.pdfViewer.isInPresentationMode) {
return;
}
this.pdfViewer.increaseScale(steps, {
this.pdfViewer.increaseScale({
drawingDelay: AppOptions.get("defaultZoomDelay"),
steps,
scaleFactor,
});
},

zoomOut(steps) {
zoomOut(steps, scaleFactor) {
if (this.pdfViewer.isInPresentationMode) {
return;
}
this.pdfViewer.decreaseScale(steps, {
this.pdfViewer.decreaseScale({
drawingDelay: AppOptions.get("defaultZoomDelay"),
steps,
scaleFactor,
});
},

Expand Down Expand Up @@ -696,6 +706,10 @@ const PDFViewerApplication = {
return shadow(this, "supportsFullscreen", document.fullscreenEnabled);
},

get supportsPinchToZoom() {
return this.externalServices.supportsPinchToZoom;
},

get supportsIntegratedFind() {
return this.externalServices.supportsIntegratedFind;
},
Expand Down Expand Up @@ -1915,6 +1929,12 @@ const PDFViewerApplication = {
window.addEventListener("touchstart", webViewerTouchStart, {
passive: false,
});
window.addEventListener("touchmove", webViewerTouchMove, {
passive: false,
});
window.addEventListener("touchend", webViewerTouchEnd, {
passive: false,
});
window.addEventListener("click", webViewerClick);
window.addEventListener("keydown", webViewerKeyDown);
window.addEventListener("resize", _boundEvents.windowResize);
Expand Down Expand Up @@ -1997,6 +2017,12 @@ const PDFViewerApplication = {
window.removeEventListener("touchstart", webViewerTouchStart, {
passive: false,
});
window.removeEventListener("touchmove", webViewerTouchMove, {
passive: false,
});
window.removeEventListener("touchend", webViewerTouchEnd, {
passive: false,
});
window.removeEventListener("click", webViewerClick);
window.removeEventListener("keydown", webViewerKeyDown);
window.removeEventListener("resize", _boundEvents.windowResize);
Expand Down Expand Up @@ -2030,6 +2056,20 @@ const PDFViewerApplication = {
return wholeTicks;
},

accumulateTouchTicks(ticks) {
// If the scroll direction changed, reset the accumulated touch ticks.
if (
(this._touchUnusedTicks > 0 && ticks < 0) ||
(this._touchUnusedTicks < 0 && ticks > 0)
) {
this._touchUnusedTicks = 0;
}
this._touchUnusedTicks += ticks;
const wholeTicks = Math.trunc(this._touchUnusedTicks);
this._touchUnusedTicks -= wholeTicks;
return wholeTicks;
},

/**
* 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
Expand Down Expand Up @@ -2673,19 +2713,103 @@ function webViewerWheel(evt) {
}

function webViewerTouchStart(evt) {
if (evt.touches.length > 1) {
// Disable touch-based zooming, because the entire UI bits gets zoomed and
// that doesn't look great. If we do want to have a good touch-based
// zooming experience, we need to implement smooth zoom capability (probably
// using a CSS transform for faster visual response, followed by async
// re-rendering at the final zoom level) and do gesture detection on the
// touchmove events to drive it. Or if we want to settle for a less good
// experience we can make the touchmove events drive the existing step-zoom
// behaviour that the ctrl+mousewheel path takes.
evt.preventDefault();
if (
PDFViewerApplication.pdfViewer.isInPresentationMode ||
evt.touches.length < 2
) {
return;
}
evt.preventDefault();

if (evt.touches.length !== 2) {
PDFViewerApplication._touchInfo = null;
return;
}

const [touch0, touch1] = evt.touches;
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,
};
}

function webViewerTouchMove(evt) {
if (!PDFViewerApplication._touchInfo || evt.touches.length !== 2) {
return;
}

const { pdfViewer, _touchInfo, supportsPinchToZoom } = PDFViewerApplication;
const [touch0, touch1] = evt.touches;
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;

if (supportsPinchToZoom && Math.abs(scaleFactor - 1) <= 1e-2) {
// Scale increase/decrease isn't significant enough.
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;
}

evt.preventDefault();

const previousScale = pdfViewer.currentScale;
if (supportsPinchToZoom) {
if (scaleFactor < 1) {
PDFViewerApplication.zoomOut(null, scaleFactor);
} else {
PDFViewerApplication.zoomIn(null, scaleFactor);
}
} else {
const PIXELS_PER_LINE_SCALE = 30;
const delta = (distance - previousDistance) / PIXELS_PER_LINE_SCALE;
const ticks = PDFViewerApplication.accumulateTouchTicks(delta);
if (ticks < 0) {
PDFViewerApplication.zoomOut(-ticks);
} else if (ticks > 0) {
PDFViewerApplication.zoomIn(ticks);
}
}

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 [top, left] = pdfViewer.containerTopLeft;
const dx = newCenterX - left;
const dy = newCenterY - top;
pdfViewer.container.scrollLeft += dx * scaleCorrectionFactor;
pdfViewer.container.scrollTop += dy * scaleCorrectionFactor;
}
}

function webViewerTouchEnd(evt) {
if (!PDFViewerApplication._touchInfo) {
return;
}

evt.preventDefault();
PDFViewerApplication._touchInfo = null;
PDFViewerApplication.pdfViewer.refresh();
PDFViewerApplication._touchUnusedTicks = 0;
}

function webViewerClick(evt) {
if (!PDFViewerApplication.secondaryToolbar?.isOpen) {
return;
Expand Down
5 changes: 5 additions & 0 deletions web/firefoxcom.js
Expand Up @@ -410,6 +410,11 @@ class FirefoxExternalServices extends DefaultExternalServices {
return FirefoxScripting;
}

static get supportsPinchToZoom() {
const support = FirefoxCom.requestSync("supportsPinchToZoom");
return shadow(this, "supportsPinchToZoom", support);
}

static get supportsIntegratedFind() {
const support = FirefoxCom.requestSync("supportsIntegratedFind");
return shadow(this, "supportsIntegratedFind", support);
Expand Down
70 changes: 54 additions & 16 deletions web/pdf_viewer.js
Expand Up @@ -1992,42 +1992,80 @@ class PDFViewer {

/**
* Increase the current zoom level one, or more, times.
* @param {number} [steps] - Defaults to zooming once.
* @param {Object|null} [options]
*/
increaseScale(steps = 1, options = null) {
increaseScale(options = null) {
calixteman marked this conversation as resolved.
Show resolved Hide resolved
if (
(typeof PDFJSDev === "undefined" || PDFJSDev.test("GENERIC")) &&
typeof options === "number"
) {
console.error(
"The `increaseScale` method-signature was updated, please use an object instead."
);
options = { steps: options };
}

if (!this.pdfDocument) {
return;
}
let newScale = this._currentScale;
do {
newScale = (newScale * DEFAULT_SCALE_DELTA).toFixed(2);
newScale = Math.ceil(newScale * 10) / 10;
newScale = Math.min(MAX_SCALE, newScale);
} while (--steps > 0 && newScale < MAX_SCALE);

options ||= Object.create(null);

let newScale = this._currentScale;
if (options.scaleFactor > 1) {
newScale = Math.min(
MAX_SCALE,
Math.round(newScale * options.scaleFactor * 100) / 100
);
} else {
let steps = options.steps ?? 1;
do {
newScale = (newScale * DEFAULT_SCALE_DELTA).toFixed(2);
newScale = Math.ceil(newScale * 10) / 10;
newScale = Math.min(MAX_SCALE, newScale);
} while (--steps > 0 && newScale < MAX_SCALE);
}

options.noScroll = false;
this._setScale(newScale, options);
}

/**
* Decrease the current zoom level one, or more, times.
* @param {number} [steps] - Defaults to zooming once.
* @param {Object|null} [options]
*/
decreaseScale(steps = 1, options = null) {
decreaseScale(options = null) {
calixteman marked this conversation as resolved.
Show resolved Hide resolved
if (
(typeof PDFJSDev === "undefined" || PDFJSDev.test("GENERIC")) &&
typeof options === "number"
) {
console.error(
"The `decreaseScale` method-signature was updated, please use an object instead."
);
options = { steps: options };
}

if (!this.pdfDocument) {
return;
}
let newScale = this._currentScale;
do {
newScale = (newScale / DEFAULT_SCALE_DELTA).toFixed(2);
newScale = Math.floor(newScale * 10) / 10;
newScale = Math.max(MIN_SCALE, newScale);
} while (--steps > 0 && newScale > MIN_SCALE);

options ||= Object.create(null);

let newScale = this._currentScale;
if (options.scaleFactor > 0 && options.scaleFactor < 1) {
newScale = Math.max(
MIN_SCALE,
Math.round(newScale * options.scaleFactor * 100) / 100
);
} else {
let steps = options.steps ?? 1;
do {
newScale = (newScale / DEFAULT_SCALE_DELTA).toFixed(2);
newScale = Math.floor(newScale * 10) / 10;
newScale = Math.max(MIN_SCALE, newScale);
} while (--steps > 0 && newScale > MIN_SCALE);
}

options.noScroll = false;
this._setScale(newScale, options);
}
Expand Down