mirror of
https://github.com/john-okeefe/foliate-js.git
synced 2026-09-09 11:29:14 -04:00
Smooth scroll zoom with rAF accumulation and debounced PDF re-rendering
Previously, each wheel event immediately called #updateFrameScales which re-renders PDF canvases via onZoom on every tick. This caused visible jank because canvas re-rendering is expensive and blocks the main thread. Two improvements: 1. rAF accumulation: wheel events now accumulate zoom deltas and apply them once per animation frame via requestAnimationFrame, coalescing rapid scroll events into a single transform update. Reduced zoomStep from 0.1 to 0.05 for finer granularity. 2. PDF zoom debouncing: during active scroll-zoom, PDF iframes are visually scaled via CSS transform without calling onZoom (no canvas re-render). After zoom settles (150ms timeout), onZoom is called to re-render the canvas at the final resolution for crisp text. The #pdfLastRenderedScale field tracks what resolution the canvas was last rendered at so CSS scaling is relative to the rendered state. Comics continue to call #updateFrameScales every frame since their zoom is just a cheap CSS transform with no canvas work. Timeouts are cleared on page turn, resize, and destroy to prevent stale re-renders.
This commit is contained in:
+58
-6
@@ -44,8 +44,11 @@ export class FixedLayout extends HTMLElement {
|
|||||||
#zoomState = {
|
#zoomState = {
|
||||||
minScale: 0.1,
|
minScale: 0.1,
|
||||||
maxScale: 10,
|
maxScale: 10,
|
||||||
zoomStep: 0.1,
|
zoomStep: 0.05,
|
||||||
};
|
};
|
||||||
|
#zoomAccum = { ratio: 1, cx: 0, cy: 0, raf: null };
|
||||||
|
#pdfLastRenderedScale = 1;
|
||||||
|
#pdfSettleTimeout = null;
|
||||||
#dragState = {
|
#dragState = {
|
||||||
isDragging: false,
|
isDragging: false,
|
||||||
startX: 0,
|
startX: 0,
|
||||||
@@ -164,6 +167,7 @@ export class FixedLayout extends HTMLElement {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#onResize() {
|
#onResize() {
|
||||||
|
clearTimeout(this.#pdfSettleTimeout);
|
||||||
if (typeof this.#zoom === "number" && !isNaN(this.#zoom)) {
|
if (typeof this.#zoom === "number" && !isNaN(this.#zoom)) {
|
||||||
// Numeric zoom: rescale frames but preserve user's pan position
|
// Numeric zoom: rescale frames but preserve user's pan position
|
||||||
this.#updateFrameScales(this.#transform.scale);
|
this.#updateFrameScales(this.#transform.scale);
|
||||||
@@ -326,6 +330,34 @@ export class FixedLayout extends HTMLElement {
|
|||||||
this.#wrapper.style.transform = `translate(${this.#transform.x}px, ${this.#transform.y}px)`;
|
this.#wrapper.style.transform = `translate(${this.#transform.x}px, ${this.#transform.y}px)`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#applyPDFZoomScale(scale) {
|
||||||
|
const cssScale = scale / this.#pdfLastRenderedScale;
|
||||||
|
const candidates = this.#center
|
||||||
|
? [this.#center]
|
||||||
|
: [this.#left, this.#right];
|
||||||
|
for (const frame of candidates) {
|
||||||
|
if (!frame?.onZoom || !frame.iframe) continue;
|
||||||
|
Object.assign(frame.iframe.style, {
|
||||||
|
width: `${frame.width * this.#pdfLastRenderedScale}px`,
|
||||||
|
height: `${frame.height * this.#pdfLastRenderedScale}px`,
|
||||||
|
transform: `scale(${cssScale})`,
|
||||||
|
transformOrigin: "top left",
|
||||||
|
});
|
||||||
|
Object.assign(frame.element.style, {
|
||||||
|
width: `${frame.width * scale}px`,
|
||||||
|
height: `${frame.height * scale}px`,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#schedulePDFRerender(scale) {
|
||||||
|
clearTimeout(this.#pdfSettleTimeout);
|
||||||
|
this.#pdfSettleTimeout = setTimeout(() => {
|
||||||
|
this.#updateFrameScales(scale);
|
||||||
|
this.#pdfSettleTimeout = null;
|
||||||
|
}, 150);
|
||||||
|
}
|
||||||
|
|
||||||
#updateFrameScales(scale) {
|
#updateFrameScales(scale) {
|
||||||
const left = this.#left ?? {};
|
const left = this.#left ?? {};
|
||||||
const right = this.#center ?? this.#right ?? {};
|
const right = this.#center ?? this.#right ?? {};
|
||||||
@@ -368,6 +400,7 @@ export class FixedLayout extends HTMLElement {
|
|||||||
transform(left);
|
transform(left);
|
||||||
transform(right);
|
transform(right);
|
||||||
}
|
}
|
||||||
|
if (this.isPDF) this.#pdfLastRenderedScale = scale;
|
||||||
}
|
}
|
||||||
|
|
||||||
#getContentSize() {
|
#getContentSize() {
|
||||||
@@ -416,7 +449,12 @@ export class FixedLayout extends HTMLElement {
|
|||||||
this.#transform.scale = newScale;
|
this.#transform.scale = newScale;
|
||||||
|
|
||||||
this.#zoom = newScale;
|
this.#zoom = newScale;
|
||||||
this.#updateFrameScales(newScale);
|
if (this.isPDF) {
|
||||||
|
this.#applyPDFZoomScale(newScale);
|
||||||
|
this.#schedulePDFRerender(newScale);
|
||||||
|
} else {
|
||||||
|
this.#updateFrameScales(newScale);
|
||||||
|
}
|
||||||
this.#applyTransform();
|
this.#applyTransform();
|
||||||
|
|
||||||
this.dispatchEvent(
|
this.dispatchEvent(
|
||||||
@@ -430,14 +468,26 @@ export class FixedLayout extends HTMLElement {
|
|||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
|
|
||||||
const rect = this.getBoundingClientRect();
|
const rect = this.getBoundingClientRect();
|
||||||
const cx = event.clientX - rect.left;
|
this.#zoomAccum.cx = event.clientX - rect.left;
|
||||||
const cy = event.clientY - rect.top;
|
this.#zoomAccum.cy = event.clientY - rect.top;
|
||||||
|
|
||||||
const ratio =
|
const tick =
|
||||||
event.deltaY > 0
|
event.deltaY > 0
|
||||||
? 1 - this.#zoomState.zoomStep
|
? 1 - this.#zoomState.zoomStep
|
||||||
: 1 + this.#zoomState.zoomStep;
|
: 1 + this.#zoomState.zoomStep;
|
||||||
this.#zoomByRatio(cx, cy, ratio);
|
this.#zoomAccum.ratio = tick;
|
||||||
|
|
||||||
|
if (!this.#zoomAccum.raf) {
|
||||||
|
this.#zoomAccum.raf = requestAnimationFrame(() => {
|
||||||
|
this.#zoomByRatio(
|
||||||
|
this.#zoomAccum.cx,
|
||||||
|
this.#zoomAccum.cy,
|
||||||
|
this.#zoomAccum.ratio,
|
||||||
|
);
|
||||||
|
this.#zoomAccum.ratio = 1;
|
||||||
|
this.#zoomAccum.raf = null;
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#handleMouseDown(event) {
|
#handleMouseDown(event) {
|
||||||
@@ -706,6 +756,7 @@ export class FixedLayout extends HTMLElement {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async #showSpread({ left, right, center, side }) {
|
async #showSpread({ left, right, center, side }) {
|
||||||
|
clearTimeout(this.#pdfSettleTimeout);
|
||||||
this.#wrapper.replaceChildren();
|
this.#wrapper.replaceChildren();
|
||||||
this.#left = null;
|
this.#left = null;
|
||||||
this.#right = null;
|
this.#right = null;
|
||||||
@@ -921,6 +972,7 @@ export class FixedLayout extends HTMLElement {
|
|||||||
|
|
||||||
destroy() {
|
destroy() {
|
||||||
this.#observer.unobserve(this);
|
this.#observer.unobserve(this);
|
||||||
|
clearTimeout(this.#pdfSettleTimeout);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user