Replace js by ts

This commit is contained in:
2026-04-01 01:27:51 +02:00
parent 4613b8e5dd
commit 6306c73f26
291 changed files with 501210 additions and 525 deletions

361
src-tauri/dist/page-manager-enhanced.js vendored Normal file
View File

@@ -0,0 +1,361 @@
/**
* page-manager-enhanced.ts — Enhanced PageManager with performance optimizations
*
* Extends the original PageManager with:
* - Better integration with render system
* - Adaptive quality support
* - Memory-efficient canvas management
* - Smooth transitions during quality changes
*/
const MAX_CANVAS_PIXELS = 16777216; // 4096 × 4096
/**
* Clamp render scale so the canvas pixel count stays within the budget.
* CSS dimensions stay correct — pages appear at the right size, just at
* reduced effective DPI when zoomed very high.
*/
function clampedRenderScale(vpWidth, vpHeight, desiredScale, dpr, quality = 1.0) {
const effectiveScale = desiredScale * quality;
const w = vpWidth * effectiveScale * dpr;
const h = vpHeight * effectiveScale * dpr;
const pixels = w * h;
if (pixels <= MAX_CANVAS_PIXELS)
return effectiveScale * dpr;
return effectiveScale * dpr * Math.sqrt(MAX_CANVAS_PIXELS / pixels);
}
const State = Object.freeze({
PLACEHOLDER: 0,
RENDERING: 1,
RENDERED: 2,
QUALITY_ADJUSTING: 3 // New state for quality transitions
});
export class EnhancedPageManager {
/**
* @param wrapper - #pages-wrapper container (already in DOM)
* @param viewports - scale=1 viewports (0-indexed)
* @param initialScale - initial display scale
* @param dpr - devicePixelRatio
* @param dispatchRender - (pageNum, scale, vpWidth, vpHeight, gen, quality) => void
*/
constructor(wrapper, viewports, initialScale, dpr, dispatchRender) {
this._wrapper = wrapper;
this._viewports = viewports;
this._scale = initialScale;
this._dpr = dpr;
this._dispatchRender = dispatchRender;
this._renderGen = 0;
this._inFlight = 0;
this._zooming = false;
this._states = new Array(viewports.length).fill(State.PLACEHOLDER);
this._canvases = new Array(viewports.length).fill(null);
this._wrappers = [];
this._currentQualities = new Array(viewports.length).fill(1.0); // Track quality per page
this._targetQualities = new Array(viewports.length).fill(1.0); // Target quality for transitions
this._needsQualityUpdate = false;
this._buildPlaceholders();
}
get pageWrappers() { return this._wrappers; }
get numPages() { return this._viewports.length; }
get renderGen() { return this._renderGen; }
/** Called by ZoomController to suppress canvas teardown during CSS zoom. */
setZooming(z) { this._zooming = z; }
setScale(scale) {
this._scale = scale;
}
setQualityForPage(pageNum, quality) {
const i = pageNum - 1;
if (i >= 0 && i < this._viewports.length) {
this._targetQualities[i] = quality;
// If page is rendered and quality changed significantly, trigger re-render
if (this._states[i] === State.RENDERED &&
Math.abs(quality - this._currentQualities[i]) > 0.1) {
this._startQualityAdjustment(i);
}
}
}
setGlobalQuality(quality) {
for (let i = 0; i < this._targetQualities.length; i++) {
this._targetQualities[i] = quality;
}
// Re-render visible pages with new quality
this._needsQualityUpdate = true;
}
_buildPlaceholders() {
for (let i = 0; i < this._viewports.length; i++) {
const vp = this._viewports[i];
const wrap = document.createElement("div");
wrap.className = "page-wrapper";
wrap.dataset.page = String(i + 1);
wrap.style.width = vp.width * this._scale + "px";
wrap.style.height = vp.height * this._scale + "px";
this._wrapper.appendChild(wrap);
this._wrappers.push(wrap);
}
}
/**
* Called on each IntersectionObserver tick.
* Renders pages entering the buffer; cleans up pages leaving it.
* Visible pages are prioritized in the render queue.
*
* @param bufferSet - 1-based page numbers in the buffer zone
* @param visibleSet - 1-based page numbers currently on screen
*/
reconcile(bufferSet, visibleSet) {
const gen = this._renderGen;
// Prioritize visible pages, then the rest of the buffer
const toRender = [
...[...visibleSet].sort((a, b) => a - b),
...[...bufferSet].filter(p => !visibleSet.has(p)).sort((a, b) => a - b),
];
for (const pageNum of toRender) {
const i = pageNum - 1;
if (i < 0 || i >= this._viewports.length)
continue;
if (this._states[i] === State.PLACEHOLDER) {
this._startRender(i, gen);
}
else if (this._states[i] === State.QUALITY_ADJUSTING) {
// Already handling quality transition
}
else if (this._needsQualityUpdate && this._states[i] === State.RENDERED) {
// Apply quality updates if needed
if (Math.abs(this._targetQualities[i] - this._currentQualities[i]) > 0.05) {
this._startQualityAdjustment(i);
}
}
}
// Clean up pages no longer in the buffer
for (let i = 0; i < this._viewports.length; i++) {
const pageNum = i + 1;
if (!this._zooming && !bufferSet.has(pageNum) && this._states[i] === State.RENDERED) {
this._cleanup(i);
}
// Cancel stale RENDERING pages outside the buffer
if (!bufferSet.has(pageNum) && (this._states[i] === State.RENDERING || this._states[i] === State.QUALITY_ADJUSTING)) {
if (this._states[i] === State.RENDERING)
this._inFlight--;
this._states[i] = State.PLACEHOLDER;
}
}
this._needsQualityUpdate = false;
}
get allRendered() { return this._inFlight === 0; }
getPageElement(pageNum) {
const i = pageNum - 1;
if (i >= 0 && i < this._wrappers.length) {
return this._wrappers[i];
}
return null;
}
_startRender(i, gen) {
const vp = this._viewports[i];
const quality = this._targetQualities[i];
const scale = clampedRenderScale(vp.width, vp.height, this._scale, this._dpr, quality);
this._states[i] = State.RENDERING;
this._inFlight++;
this._dispatchRender(i + 1, this._scale, vp.width, vp.height, gen, quality);
}
_startQualityAdjustment(i) {
// Transition to quality adjustment state
this._states[i] = State.QUALITY_ADJUSTING;
const pageNum = i + 1;
const gen = ++this._renderGen; // Increment gen to cancel any pending renders
const vp = this._viewports[i];
const quality = this._targetQualities[i];
const scale = clampedRenderScale(vp.width, vp.height, this._scale, this._dpr, quality);
this._inFlight++;
this._dispatchRender(pageNum, this._scale, vp.width, vp.height, gen, quality);
}
_cleanup(i) {
if (this._states[i] === State.RENDERING || this._states[i] === State.QUALITY_ADJUSTING) {
this._inFlight--;
}
this._states[i] = State.PLACEHOLDER;
const canvas = this._canvases[i];
if (canvas) {
canvas.remove();
this._canvases[i] = null;
}
// Also remove the text and annotation layers
const wrap = this._wrappers[i];
wrap.querySelector(".text-layer")?.remove();
wrap.querySelector(".annot-layer")?.remove();
}
/**
* Called when the worker returns a rendered bitmap.
*
* @param pageNum
* @param gen - render generation the bitmap was rendered for
* @param bitmap
* @param quality - quality at which this bitmap was rendered
*/
onRendered(pageNum, gen, bitmap, quality) {
if (gen !== this._renderGen) {
bitmap.close();
return;
}
const i = pageNum - 1;
if (i < 0 || i >= this._viewports.length ||
(this._states[i] !== State.RENDERING && this._states[i] !== State.QUALITY_ADJUSTING)) {
bitmap.close();
return;
}
this._inFlight--;
this._currentQualities[i] = quality;
const vp = this._viewports[i];
const wrap = this._wrappers[i];
const cssW = vp.width * this._scale;
const cssH = vp.height * this._scale;
// For quality adjustments, we want smooth transitions
const wasQualityAdjustment = this._states[i] === State.QUALITY_ADJUSTING;
this._states[i] = State.RENDERED;
// Remove the old canvas
const old = wrap.querySelector("canvas");
if (old) {
if (wasQualityAdjustment) {
// For quality adjustments, fade out the old canvas for smooth transition
old.style.transition = "opacity 0.15s ease-out";
old.style.opacity = "0";
setTimeout(() => old.remove(), 150);
}
else {
old.remove();
}
}
const canvas = document.createElement("canvas");
canvas.width = bitmap.width;
canvas.height = bitmap.height;
canvas.style.width = cssW + "px";
canvas.style.height = cssH + "px";
// Zero-copy display — fall back to drawImage if bitmaprenderer unavailable
const ctx = canvas.getContext("bitmaprenderer");
if (ctx) {
ctx.transferFromImageBitmap(bitmap);
}
else {
const ctx2d = canvas.getContext("2d");
if (ctx2d) {
ctx2d.drawImage(bitmap, 0, 0);
bitmap.close();
}
}
// For quality adjustments, fade in the new canvas
if (wasQualityAdjustment) {
canvas.style.opacity = "0";
canvas.style.transition = "opacity 0.15s ease-in";
wrap.appendChild(canvas);
requestAnimationFrame(() => {
canvas.style.opacity = "1";
});
}
else {
wrap.appendChild(canvas);
}
this._canvases[i] = canvas;
}
/**
* Get the canvas element for a specific page
*/
getPageCanvas(pageNum) {
const i = pageNum - 1;
if (i >= 0 && i < this._canvases.length) {
return this._canvases[i];
}
return null;
}
/**
* Called after the zoom debounce fires.
* Increments renderGen (cancels stale work), resizes all placeholders,
* then re-renders the buffer pages at the new resolution.
*
* @param newScale
* @param bufferSet
* @param visibleSet
*/
onScaleChange(newScale, bufferSet, visibleSet) {
this._scale = newScale;
this._renderGen++;
for (let i = 0; i < this._viewports.length; i++) {
const pageNum = i + 1;
const vp = this._viewports[i];
const wrap = this._wrappers[i];
const cssW = vp.width * newScale;
const cssH = vp.height * newScale;
wrap.style.width = cssW + "px";
wrap.style.height = cssH + "px";
wrap.style.zoom = "1";
if (bufferSet.has(pageNum)) {
// Keep the old canvas visible (stretched to the new CSS size) so there
// is no blank flash while the new render is in flight.
const canvas = this._canvases[i];
if (canvas) {
canvas.style.width = cssW + "px";
canvas.style.height = cssH + "px";
}
if (this._states[i] === State.RENDERING || this._states[i] === State.QUALITY_ADJUSTING) {
this._inFlight--;
}
this._states[i] = State.PLACEHOLDER;
}
else {
// Off-screen: clean up immediately — not visible, so no flash.
const canvas = this._canvases[i];
if (canvas) {
canvas.remove();
this._canvases[i] = null;
}
this._states[i] = State.PLACEHOLDER;
}
}
this.reconcile(bufferSet, visibleSet);
}
/**
* Returns the 1-based number of the topmost visible page.
*
* @param visibleSet
*/
getCurrentPage(visibleSet) {
// Defensive programming: handle null/undefined visibleSet
if (!visibleSet || visibleSet.size === 0) {
// Fall back to scroll position
const top = this._wrapper.parentElement?.getBoundingClientRect().top ?? 0;
for (const wrap of this._wrappers) {
if (wrap.getBoundingClientRect().bottom > top + 4) {
return parseInt(wrap.dataset.page || '1', 10);
}
}
return 1;
}
return Math.min(...visibleSet);
}
/**
* Get the current quality for a specific page
*/
getPageQuality(pageNum) {
const i = pageNum - 1;
if (i >= 0 && i < this._currentQualities.length) {
return this._currentQualities[i];
}
return 1.0;
}
/**
* Get the current render state for a page
*/
getPageState(pageNum) {
const i = pageNum - 1;
if (i >= 0 && i < this._states.length) {
return this._states[i];
}
return State.PLACEHOLDER;
}
/**
* Force cleanup of all resources
*/
cleanup() {
for (let i = 0; i < this._viewports.length; i++) {
this._cleanup(i);
}
this._inFlight = 0;
this._renderGen = 0;
}
}
//# sourceMappingURL=page-manager-enhanced.js.map