pdf-flipbook 1.0.0 → 1.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -21209,291 +21209,331 @@ if (typeof window !== "undefined") {
21209
21209
  __webpack_exports__GlobalWorkerOptions.workerSrc = `https://cdnjs.cloudflare.com/ajax/libs/pdf.js/${__webpack_exports__version}/pdf.worker.min.mjs`;
21210
21210
  }
21211
21211
  var DEFAULTS = {
21212
- /** Flip animation duration in ms */
21213
- flipDuration: 700,
21214
- /** Shadow opacity at peak of flip (0-1) */
21215
- shadowIntensity: 0.4,
21216
- /** Device-pixel-ratio cap for canvas rendering */
21212
+ flipDuration: 800,
21213
+ shadowIntensity: 0.35,
21217
21214
  maxDpr: 2,
21218
- /** Whether to show the toolbar */
21219
21215
  toolbar: true,
21220
- /** Toolbar theme: "light" | "dark" */
21221
21216
  theme: "dark",
21222
- /** Preload N pages ahead */
21223
- preloadAhead: 2,
21224
- /** Called when a page turn starts: (fromPage, toPage) */
21225
21217
  onFlip: null,
21226
- /** Called when all pages are loaded */
21227
21218
  onReady: null,
21228
- /** Called on load error */
21229
21219
  onError: null
21230
21220
  };
21231
21221
  var clamp = (v, lo, hi) => Math.max(lo, Math.min(hi, v));
21232
- var even = (n) => n % 2 === 0 ? n : n - 1;
21233
- var isTouchDevice = () => "ontouchstart" in window || navigator.maxTouchPoints > 0;
21222
+ var lerp = (a, b, t) => a + (b - a) * t;
21223
+ var easeInOut = (t) => t < 0.5 ? 2 * t * t : -1 + (4 - 2 * t) * t;
21234
21224
  var PdfFlipbook = class {
21235
- /**
21236
- * @param {string|HTMLElement} container CSS selector or DOM element
21237
- * @param {string} pdfUrl Remote or local PDF URL
21238
- * @param {object} [options] See DEFAULTS
21239
- */
21240
21225
  constructor(container, pdfUrl, options = {}) {
21241
21226
  if (!pdfUrl) throw new Error("[PdfFlipbook] pdfUrl is required");
21242
21227
  this._el = typeof container === "string" ? document.querySelector(container) : container;
21243
- if (!this._el) throw new Error(`[PdfFlipbook] container not found: ${container}`);
21228
+ if (!this._el) throw new Error("[PdfFlipbook] container not found");
21244
21229
  this._url = pdfUrl;
21245
21230
  this._opts = { ...DEFAULTS, ...options };
21246
21231
  this._pages = [];
21247
- this._pdf = null;
21248
- this._total = 0;
21249
21232
  this._spread = 0;
21233
+ this._total = 0;
21250
21234
  this._flipping = false;
21235
+ this._animId = null;
21236
+ this._pageAspect = 1.414;
21251
21237
  this._init();
21252
21238
  }
21253
- // ── Static workerSrc override ──────────────────────────────────────────────
21254
21239
  static set workerSrc(src) {
21255
21240
  __webpack_exports__GlobalWorkerOptions.workerSrc = src;
21256
21241
  }
21257
- // ═══ PUBLIC API ═══════════════════════════════════════════════════════════
21258
- /** Go to the next spread */
21259
21242
  next() {
21260
- this._flip(1);
21243
+ this._startFlip(1);
21261
21244
  }
21262
- /** Go to the previous spread */
21263
21245
  prev() {
21264
- this._flip(-1);
21246
+ this._startFlip(-1);
21265
21247
  }
21266
- /** Jump directly to a page (1-indexed) */
21267
- goTo(pageNum) {
21268
- const target = clamp(pageNum - 1, 0, this._total - 1);
21269
- const targetSpread = even(target);
21270
- if (targetSpread === this._spread) return;
21271
- const dir = targetSpread > this._spread ? 1 : -1;
21272
- this._animateFlip(dir, targetSpread);
21248
+ goTo(n) {
21249
+ const idx = clamp(n - 1, 0, this._total - 1);
21250
+ const target = idx % 2 === 0 ? idx : idx - 1;
21251
+ if (target !== this._spread) this._startFlip(target > this._spread ? 1 : -1, target);
21273
21252
  }
21274
- /** Destroy the flipbook and clean up */
21275
21253
  destroy() {
21276
- this._el.innerHTML = "";
21277
- this._el.classList.remove("pfb", `pfb--${this._opts.theme}`);
21278
- this._pages = [];
21254
+ cancelAnimationFrame(this._animId);
21279
21255
  window.removeEventListener("resize", this._onResize);
21280
21256
  window.removeEventListener("keydown", this._onKey);
21257
+ this._el.innerHTML = "";
21281
21258
  }
21282
- // ═══ PRIVATE ══════════════════════════════════════════════════════════════
21283
21259
  async _init() {
21284
21260
  this._buildShell();
21285
21261
  try {
21286
21262
  await this._loadPdf();
21287
21263
  this._bindEvents();
21288
21264
  this._opts.onReady?.();
21289
- } catch (err) {
21290
- console.error("[PdfFlipbook]", err);
21291
- this._showError(err.message);
21292
- this._opts.onError?.(err);
21265
+ } catch (e) {
21266
+ console.error("[PdfFlipbook]", e);
21267
+ this._showError(e.message);
21268
+ this._opts.onError?.(e);
21293
21269
  }
21294
21270
  }
21295
- // ── DOM Shell ──────────────────────────────────────────────────────────────
21296
21271
  _buildShell() {
21297
21272
  this._el.classList.add("pfb", `pfb--${this._opts.theme}`);
21298
21273
  this._el.innerHTML = `
21299
21274
  <div class="pfb__stage">
21300
- <div class="pfb__book">
21301
- <div class="pfb__page pfb__page--left">
21302
- <div class="pfb__page-front"></div>
21303
- <div class="pfb__page-back"></div>
21304
- <div class="pfb__shadow pfb__shadow--left"></div>
21305
- </div>
21306
- <div class="pfb__spine"></div>
21307
- <div class="pfb__page pfb__page--right">
21308
- <div class="pfb__page-front"></div>
21309
- <div class="pfb__page-back"></div>
21310
- <div class="pfb__shadow pfb__shadow--right"></div>
21311
- </div>
21312
- </div>
21313
- <button class="pfb__btn pfb__btn--prev" aria-label="Previous page">&#8592;</button>
21314
- <button class="pfb__btn pfb__btn--next" aria-label="Next page">&#8594;</button>
21315
- <div class="pfb__loader">
21316
- <div class="pfb__spinner"></div>
21317
- <span class="pfb__loader-text">Loading PDF\u2026</span>
21318
- </div>
21275
+ <canvas class="pfb__canvas"></canvas>
21276
+ <button class="pfb__btn pfb__btn--prev" aria-label="Previous">&#8592;</button>
21277
+ <button class="pfb__btn pfb__btn--next" aria-label="Next">&#8594;</button>
21278
+ <div class="pfb__loader"><div class="pfb__spinner"></div><span class="pfb__loader-text">Loading\u2026</span></div>
21319
21279
  <div class="pfb__error" hidden></div>
21320
21280
  </div>
21321
- ${this._opts.toolbar ? `
21322
- <div class="pfb__toolbar">
21323
- <button class="pfb__tb-btn pfb__tb-prev" aria-label="Previous">&#8592;</button>
21281
+ ${this._opts.toolbar ? `<div class="pfb__toolbar">
21282
+ <button class="pfb__tb-btn pfb__tb-prev">&#8592;</button>
21324
21283
  <span class="pfb__page-info"></span>
21325
- <button class="pfb__tb-btn pfb__tb-next" aria-label="Next">&#8594;</button>
21284
+ <button class="pfb__tb-btn pfb__tb-next">&#8594;</button>
21326
21285
  </div>` : ""}
21327
21286
  `;
21328
21287
  this._stage = this._el.querySelector(".pfb__stage");
21329
- this._book = this._el.querySelector(".pfb__book");
21288
+ this._canvas = this._el.querySelector(".pfb__canvas");
21289
+ this._ctx = this._canvas.getContext("2d");
21330
21290
  this._loader = this._el.querySelector(".pfb__loader");
21291
+ this._loaderTx = this._el.querySelector(".pfb__loader-text");
21331
21292
  this._errorEl = this._el.querySelector(".pfb__error");
21332
21293
  this._pageInfo = this._el.querySelector(".pfb__page-info");
21333
- this._leftFront = this._el.querySelector(".pfb__page--left .pfb__page-front");
21334
- this._leftBack = this._el.querySelector(".pfb__page--left .pfb__page-back");
21335
- this._leftShadow = this._el.querySelector(".pfb__shadow--left");
21336
- this._rightFront = this._el.querySelector(".pfb__page--right .pfb__page-front");
21337
- this._rightBack = this._el.querySelector(".pfb__page--right .pfb__page-back");
21338
- this._rightShadow = this._el.querySelector(".pfb__shadow--right");
21339
- }
21340
- // ── PDF Loading ────────────────────────────────────────────────────────────
21341
- async _loadPdf() {
21342
- const loadingTask = __webpack_exports__getDocument({ url: this._url, withCredentials: false });
21343
- this._pdf = await loadingTask.promise;
21344
- this._total = this._pdf.numPages;
21345
- await this._prerenderAll();
21346
- this._loader.hidden = true;
21347
- this._renderSpread(0, false);
21294
+ this._resizeCanvas();
21348
21295
  }
21349
- async _prerenderAll() {
21350
- this._loaderText = this._el.querySelector(".pfb__loader-text");
21351
- for (let i = 1; i <= this._total; i++) {
21352
- const canvas = await this._renderPage(i);
21353
- this._pages.push(canvas);
21354
- if (this._loaderText) {
21355
- this._loaderText.textContent = `Loading ${i} / ${this._total}\u2026`;
21356
- }
21357
- }
21358
- }
21359
- async _renderPage(pageNum) {
21360
- const page = await this._pdf.getPage(pageNum);
21361
- const dpr = clamp(window.devicePixelRatio || 1, 1, this._opts.maxDpr);
21362
- const viewport = page.getViewport({ scale: 1 });
21363
- const baseW = 900;
21364
- const scale = baseW / viewport.width;
21365
- const vp = page.getViewport({ scale: scale * dpr });
21366
- const canvas = document.createElement("canvas");
21367
- canvas.width = vp.width;
21368
- canvas.height = vp.height;
21369
- canvas.style.width = `${vp.width / dpr}px`;
21370
- canvas.style.height = `${vp.height / dpr}px`;
21371
- canvas.dataset.pageNum = pageNum;
21372
- const ctx = canvas.getContext("2d");
21373
- await page.render({ canvasContext: ctx, viewport: vp }).promise;
21374
- return canvas;
21296
+ _resizeCanvas() {
21297
+ const W = this._stage.clientWidth || 900;
21298
+ const H = this._stage.clientHeight || 560;
21299
+ this._canvas.width = W;
21300
+ this._canvas.height = H;
21301
+ this._canvas.style.width = W + "px";
21302
+ this._canvas.style.height = H + "px";
21303
+ if (this._total) this._drawSpread();
21375
21304
  }
21376
- // ── Spread Rendering ───────────────────────────────────────────────────────
21377
- _renderSpread(spreadIndex, animate) {
21378
- this._spread = clamp(spreadIndex, 0, even(this._total - 1) || 0);
21379
- const leftIdx = this._spread;
21380
- const rightIdx = this._spread + 1;
21381
- this._setFace(this._leftFront, leftIdx);
21382
- this._setFace(this._rightFront, rightIdx);
21383
- this._setFace(this._leftBack, null);
21384
- this._setFace(this._rightBack, null);
21305
+ async _loadPdf() {
21306
+ const pdf = await __webpack_exports__getDocument({ url: this._url, withCredentials: false }).promise;
21307
+ this._total = pdf.numPages;
21308
+ const fp = await pdf.getPage(1);
21309
+ const vp0 = fp.getViewport({ scale: 1 });
21310
+ this._pageAspect = vp0.height / vp0.width;
21311
+ for (let i = 1; i <= this._total; i++) {
21312
+ if (this._loaderTx) this._loaderTx.textContent = `Loading ${i} / ${this._total}\u2026`;
21313
+ const page = await pdf.getPage(i);
21314
+ const dpr = clamp(window.devicePixelRatio || 1, 1, this._opts.maxDpr);
21315
+ const rW = 1200 * dpr;
21316
+ const vp = page.getViewport({ scale: rW / vp0.width });
21317
+ const cvs = document.createElement("canvas");
21318
+ cvs.width = vp.width;
21319
+ cvs.height = vp.height;
21320
+ await page.render({ canvasContext: cvs.getContext("2d"), viewport: vp }).promise;
21321
+ this._pages.push(cvs);
21322
+ }
21323
+ this._loader.hidden = true;
21324
+ this._drawSpread();
21385
21325
  this._updateUI();
21386
21326
  }
21387
- _setFace(face, pageIndex) {
21388
- face.innerHTML = "";
21389
- if (pageIndex === null || pageIndex >= this._total) {
21390
- face.classList.add("pfb__face--blank");
21391
- return;
21392
- }
21393
- face.classList.remove("pfb__face--blank");
21394
- const canvas = this._pages[pageIndex];
21395
- if (canvas) {
21396
- const clone = canvas.cloneNode();
21397
- clone.getContext("2d").drawImage(canvas, 0, 0);
21398
- face.appendChild(clone);
21327
+ _bookRect() {
21328
+ const cW = this._canvas.width, cH = this._canvas.height;
21329
+ const mobile = cW < 580;
21330
+ let bW, bH;
21331
+ if (mobile) {
21332
+ bW = Math.min(cW - 24, 440);
21333
+ bH = bW * this._pageAspect;
21334
+ } else {
21335
+ bH = Math.min(cH - 60, 680);
21336
+ bW = bH / this._pageAspect * 2;
21337
+ if (bW > cW - 80) {
21338
+ bW = cW - 80;
21339
+ bH = bW / 2 * this._pageAspect;
21340
+ }
21341
+ }
21342
+ const x = (cW - bW) / 2, y = (cH - bH) / 2;
21343
+ return { x, y, w: bW, h: bH, mobile, pageW: mobile ? bW : bW / 2, pageH: bH };
21344
+ }
21345
+ _drawPage(ctx, canvas, px, py, pw, ph) {
21346
+ ctx.fillStyle = "#fdfaf5";
21347
+ ctx.fillRect(px, py, pw, ph);
21348
+ if (canvas) ctx.drawImage(canvas, px, py, pw, ph);
21349
+ ctx.strokeStyle = "rgba(0,0,0,0.07)";
21350
+ ctx.lineWidth = 1;
21351
+ ctx.strokeRect(px + 0.5, py + 0.5, pw - 1, ph - 1);
21352
+ }
21353
+ _drawSpine(ctx, sx, sy, sh) {
21354
+ const g = ctx.createLinearGradient(sx - 10, 0, sx + 10, 0);
21355
+ g.addColorStop(0, "rgba(0,0,0,0.2)");
21356
+ g.addColorStop(0.45, "rgba(0,0,0,0.5)");
21357
+ g.addColorStop(0.55, "rgba(0,0,0,0.5)");
21358
+ g.addColorStop(1, "rgba(0,0,0,0.08)");
21359
+ ctx.fillStyle = g;
21360
+ ctx.fillRect(sx - 10, sy, 20, sh);
21361
+ }
21362
+ _drawBookShadow(bx, by, bw, bh) {
21363
+ const ctx = this._ctx;
21364
+ const g = ctx.createLinearGradient(0, by + bh, 0, by + bh + 40);
21365
+ g.addColorStop(0, "rgba(0,0,0,0.35)");
21366
+ g.addColorStop(1, "rgba(0,0,0,0)");
21367
+ ctx.fillStyle = g;
21368
+ ctx.fillRect(bx, by + bh - 4, bw, 44);
21369
+ }
21370
+ _drawSpread(flip) {
21371
+ const ctx = this._ctx;
21372
+ const { x, y, w, h, mobile, pageW, pageH } = this._bookRect();
21373
+ ctx.clearRect(0, 0, this._canvas.width, this._canvas.height);
21374
+ this._drawBookShadow(x, y, w, h);
21375
+ if (mobile) {
21376
+ this._drawPage(ctx, this._pages[this._spread] || null, x, y, pageW, pageH);
21377
+ } else {
21378
+ this._drawPage(ctx, this._pages[this._spread] || null, x, y, pageW, pageH);
21379
+ this._drawPage(ctx, this._pages[this._spread + 1] || null, x + pageW, y, pageW, pageH);
21380
+ this._drawSpine(ctx, x + pageW, y, pageH);
21399
21381
  }
21382
+ if (flip) this._renderFlip(ctx, flip, x, y, pageW, pageH, mobile);
21400
21383
  }
21401
- // ── Flip Animation ────────────────────────────────────────────────────────
21402
- _flip(dir) {
21403
- if (this._flipping) return;
21404
- const next = this._spread + dir * 2;
21405
- if (next < 0 || next > even(this._total - 1)) return;
21406
- this._animateFlip(dir, next);
21407
- }
21408
- _animateFlip(dir, targetSpread) {
21409
- if (this._flipping) return;
21410
- this._flipping = true;
21411
- const { flipDuration, shadowIntensity, onFlip } = this._opts;
21412
- const currentLeft = this._spread;
21413
- const currentRight = this._spread + 1;
21414
- onFlip?.(currentLeft + 1, targetSpread + 1);
21384
+ _renderFlip(ctx, { t, dir }, bx, by, pageW, pageH) {
21385
+ const si = this._opts.shadowIntensity;
21386
+ const sp = this._spread;
21415
21387
  if (dir === 1) {
21416
- this._setFace(this._rightBack, targetSpread + 1);
21417
- this._setFace(this._leftBack, targetSpread);
21388
+ const foldX = lerp(bx + pageW * 2, bx + pageW, t);
21389
+ const turnW = bx + pageW * 2 - foldX;
21390
+ this._drawPage(ctx, this._pages[sp + 2] || null, bx, by, pageW, pageH);
21391
+ this._drawPage(ctx, this._pages[sp + 3] || null, bx + pageW, by, pageW, pageH);
21392
+ if (foldX > bx + pageW) {
21393
+ ctx.save();
21394
+ ctx.beginPath();
21395
+ ctx.rect(bx + pageW, by, foldX - (bx + pageW), pageH);
21396
+ ctx.clip();
21397
+ this._drawPage(ctx, this._pages[sp + 1] || null, bx + pageW, by, pageW, pageH);
21398
+ ctx.restore();
21399
+ }
21400
+ if (turnW > 1) {
21401
+ ctx.save();
21402
+ ctx.beginPath();
21403
+ ctx.rect(foldX, by, turnW, pageH);
21404
+ ctx.clip();
21405
+ const sc = Math.max(0.82, 1 - t * 0.18);
21406
+ ctx.translate(foldX + turnW, by);
21407
+ ctx.scale(-sc, 1);
21408
+ ctx.drawImage(this._pages[sp + 2] || this._pages[sp + 1], 0, 0, turnW / sc, pageH);
21409
+ const lg = ctx.createLinearGradient(0, 0, turnW / sc, 0);
21410
+ lg.addColorStop(0, `rgba(255,255,255,${0.08 * (1 - t)})`);
21411
+ lg.addColorStop(1, `rgba(0,0,0,${0.12 * t})`);
21412
+ ctx.fillStyle = lg;
21413
+ ctx.fillRect(0, 0, turnW / sc, pageH);
21414
+ ctx.restore();
21415
+ }
21416
+ const fg = ctx.createLinearGradient(foldX - 28, 0, foldX + 8, 0);
21417
+ fg.addColorStop(0, "rgba(0,0,0,0)");
21418
+ fg.addColorStop(0.55, `rgba(0,0,0,${si * 0.55})`);
21419
+ fg.addColorStop(0.85, `rgba(0,0,0,${si})`);
21420
+ fg.addColorStop(1, "rgba(255,255,255,0.15)");
21421
+ ctx.fillStyle = fg;
21422
+ ctx.fillRect(foldX - 28, by, 36, pageH);
21423
+ const cg = ctx.createLinearGradient(bx + pageW, 0, bx + pageW - 50, 0);
21424
+ cg.addColorStop(0, `rgba(0,0,0,${si * 0.4 * (1 - t)})`);
21425
+ cg.addColorStop(1, "rgba(0,0,0,0)");
21426
+ ctx.fillStyle = cg;
21427
+ ctx.fillRect(bx + pageW - 50, by, 50, pageH);
21418
21428
  } else {
21419
- this._setFace(this._leftBack, targetSpread);
21420
- this._setFace(this._rightBack, targetSpread + 1);
21421
- }
21422
- const flippingPage = dir === 1 ? this._el.querySelector(".pfb__page--right") : this._el.querySelector(".pfb__page--left");
21423
- const shadow2 = dir === 1 ? this._rightShadow : this._leftShadow;
21424
- const startAngle = 0;
21425
- const endAngle = dir === 1 ? -180 : 180;
21426
- const easeInOut = (t) => t < 0.5 ? 2 * t * t : -1 + (4 - 2 * t) * t;
21427
- let startTime = null;
21429
+ const foldX = lerp(bx, bx + pageW, t);
21430
+ const turnW = foldX - bx;
21431
+ this._drawPage(ctx, this._pages[sp - 2] || null, bx, by, pageW, pageH);
21432
+ this._drawPage(ctx, this._pages[sp - 1] || null, bx + pageW, by, pageW, pageH);
21433
+ if (foldX < bx + pageW) {
21434
+ ctx.save();
21435
+ ctx.beginPath();
21436
+ ctx.rect(foldX, by, bx + pageW - foldX, pageH);
21437
+ ctx.clip();
21438
+ this._drawPage(ctx, this._pages[sp] || null, bx, by, pageW, pageH);
21439
+ ctx.restore();
21440
+ }
21441
+ if (turnW > 1) {
21442
+ ctx.save();
21443
+ ctx.beginPath();
21444
+ ctx.rect(bx, by, turnW, pageH);
21445
+ ctx.clip();
21446
+ const sc = Math.max(0.82, 1 - t * 0.18);
21447
+ ctx.translate(bx + turnW, by);
21448
+ ctx.scale(-sc, 1);
21449
+ ctx.drawImage(this._pages[sp - 2] || this._pages[sp], 0, 0, turnW / sc, pageH);
21450
+ const lg = ctx.createLinearGradient(turnW / sc, 0, 0, 0);
21451
+ lg.addColorStop(0, `rgba(255,255,255,${0.08 * (1 - t)})`);
21452
+ lg.addColorStop(1, `rgba(0,0,0,${0.12 * t})`);
21453
+ ctx.fillStyle = lg;
21454
+ ctx.fillRect(0, 0, turnW / sc, pageH);
21455
+ ctx.restore();
21456
+ }
21457
+ const fg = ctx.createLinearGradient(foldX - 8, 0, foldX + 28, 0);
21458
+ fg.addColorStop(0, "rgba(255,255,255,0.15)");
21459
+ fg.addColorStop(0.15, `rgba(0,0,0,${si})`);
21460
+ fg.addColorStop(0.45, `rgba(0,0,0,${si * 0.55})`);
21461
+ fg.addColorStop(1, "rgba(0,0,0,0)");
21462
+ ctx.fillStyle = fg;
21463
+ ctx.fillRect(foldX - 8, by, 36, pageH);
21464
+ const cg = ctx.createLinearGradient(bx + pageW, 0, bx + pageW + 50, 0);
21465
+ cg.addColorStop(0, `rgba(0,0,0,${si * 0.4 * (1 - t)})`);
21466
+ cg.addColorStop(1, "rgba(0,0,0,0)");
21467
+ ctx.fillStyle = cg;
21468
+ ctx.fillRect(bx + pageW, by, 50, pageH);
21469
+ }
21470
+ this._drawSpine(ctx, bx + pageW, by, pageH);
21471
+ }
21472
+ _startFlip(dir, targetSpread) {
21473
+ if (this._flipping || !this._total) return;
21474
+ const next = targetSpread !== void 0 ? targetSpread : this._spread + dir * 2;
21475
+ if (next < 0 || next + 1 > this._total) return;
21476
+ this._flipping = true;
21477
+ this._opts.onFlip?.(this._spread + 1, next + 1);
21478
+ const duration = this._opts.flipDuration;
21479
+ let start = null;
21428
21480
  const step = (ts) => {
21429
- if (!startTime) startTime = ts;
21430
- const elapsed = ts - startTime;
21431
- const progress = clamp(elapsed / flipDuration, 0, 1);
21432
- const eased = easeInOut(progress);
21433
- const angle = startAngle + (endAngle - startAngle) * eased;
21434
- flippingPage.style.transform = `rotateY(${angle}deg)`;
21435
- const shadowPeak = Math.sin(progress * Math.PI);
21436
- shadow2.style.opacity = shadowPeak * shadowIntensity;
21437
- if (progress < 1) {
21438
- requestAnimationFrame(step);
21481
+ if (!start) start = ts;
21482
+ const raw = clamp((ts - start) / duration, 0, 1);
21483
+ const t = easeInOut(raw);
21484
+ this._drawSpread({ t, dir });
21485
+ if (raw < 1) {
21486
+ this._animId = requestAnimationFrame(step);
21439
21487
  } else {
21440
- flippingPage.style.transform = "";
21441
- shadow2.style.opacity = 0;
21442
- this._spread = targetSpread;
21488
+ this._spread = next;
21443
21489
  this._flipping = false;
21444
- this._renderSpread(targetSpread, false);
21490
+ this._drawSpread();
21491
+ this._updateUI();
21445
21492
  }
21446
21493
  };
21447
- requestAnimationFrame(step);
21494
+ this._animId = requestAnimationFrame(step);
21448
21495
  }
21449
- // ── Events ────────────────────────────────────────────────────────────────
21450
21496
  _bindEvents() {
21451
- const prevBtns = this._el.querySelectorAll(".pfb__btn--prev, .pfb__tb-prev");
21452
- const nextBtns = this._el.querySelectorAll(".pfb__btn--next, .pfb__tb-next");
21453
- prevBtns.forEach((b) => b.addEventListener("click", () => this.prev()));
21454
- nextBtns.forEach((b) => b.addEventListener("click", () => this.next()));
21497
+ this._el.querySelectorAll(".pfb__btn--prev, .pfb__tb-prev").forEach((b) => b.addEventListener("click", () => this.prev()));
21498
+ this._el.querySelectorAll(".pfb__btn--next, .pfb__tb-next").forEach((b) => b.addEventListener("click", () => this.next()));
21455
21499
  this._onKey = (e) => {
21456
- if (e.key === "ArrowRight" || e.key === "ArrowDown") this.next();
21457
- if (e.key === "ArrowLeft" || e.key === "ArrowUp") this.prev();
21500
+ if (["ArrowRight", "ArrowDown"].includes(e.key)) this.next();
21501
+ if (["ArrowLeft", "ArrowUp"].includes(e.key)) this.prev();
21458
21502
  };
21459
21503
  window.addEventListener("keydown", this._onKey);
21460
- if (isTouchDevice()) this._bindSwipe();
21461
- this._onResize = () => this._updateUI();
21462
- window.addEventListener("resize", this._onResize);
21463
- }
21464
- _bindSwipe() {
21465
- let startX = 0;
21466
- this._stage.addEventListener("touchstart", (e) => {
21467
- startX = e.touches[0].clientX;
21504
+ this._canvas.addEventListener("click", (e) => {
21505
+ const rect = this._canvas.getBoundingClientRect();
21506
+ const cx = e.clientX - rect.left;
21507
+ if (cx < this._canvas.width / 2) this.prev();
21508
+ else this.next();
21509
+ });
21510
+ let tx0 = 0;
21511
+ this._canvas.addEventListener("touchstart", (e) => {
21512
+ tx0 = e.touches[0].clientX;
21468
21513
  }, { passive: true });
21469
- this._stage.addEventListener("touchend", (e) => {
21470
- const dx = e.changedTouches[0].clientX - startX;
21514
+ this._canvas.addEventListener("touchend", (e) => {
21515
+ const dx = e.changedTouches[0].clientX - tx0;
21471
21516
  if (Math.abs(dx) > 40) dx < 0 ? this.next() : this.prev();
21472
21517
  }, { passive: true });
21518
+ this._onResize = () => this._resizeCanvas();
21519
+ window.addEventListener("resize", this._onResize);
21473
21520
  }
21474
- // ── UI Helpers ────────────────────────────────────────────────────────────
21475
21521
  _updateUI() {
21476
- if (!this._pageInfo) return;
21477
- const leftPage = this._spread + 1;
21478
- const rightPage = this._spread + 2;
21479
- if (rightPage <= this._total) {
21480
- this._pageInfo.textContent = `${leftPage} \u2013 ${rightPage} / ${this._total}`;
21481
- } else {
21482
- this._pageInfo.textContent = `${leftPage} / ${this._total}`;
21483
- }
21484
- const prevBtns = this._el.querySelectorAll(".pfb__btn--prev, .pfb__tb-prev");
21485
- const nextBtns = this._el.querySelectorAll(".pfb__btn--next, .pfb__tb-next");
21486
- prevBtns.forEach((b) => {
21487
- b.disabled = this._spread === 0;
21522
+ this._el.querySelectorAll(".pfb__btn--prev, .pfb__tb-prev").forEach((b) => {
21523
+ b.disabled = this._spread <= 0;
21488
21524
  });
21489
- nextBtns.forEach((b) => {
21525
+ this._el.querySelectorAll(".pfb__btn--next, .pfb__tb-next").forEach((b) => {
21490
21526
  b.disabled = this._spread + 2 >= this._total;
21491
21527
  });
21528
+ if (this._pageInfo) {
21529
+ const r = this._spread + 2;
21530
+ this._pageInfo.textContent = r <= this._total ? `${this._spread + 1} \u2013 ${r} / ${this._total}` : `${this._spread + 1} / ${this._total}`;
21531
+ }
21492
21532
  }
21493
21533
  _showError(msg) {
21494
21534
  this._loader.hidden = true;
21495
21535
  this._errorEl.hidden = false;
21496
- this._errorEl.textContent = `\u26A0\uFE0F Could not load PDF: ${msg}`;
21536
+ this._errorEl.textContent = `\u26A0\uFE0F ${msg}`;
21497
21537
  }
21498
21538
  };
21499
21539
  function createFlipbook(container, pdfUrl, options) {