Commit Graph
46 Commits
Author SHA1 Message Date
john-okeefe 1c812e8813 Add recenter(): reset pan while keeping the current zoom 2026-08-17 08:40:03 -04:00
john-okeefe 86e234d006 Fix rect hit-test denominator: use the rendered canvas for PDFs
documentElement's screen rect is devicePixelRatio× smaller than the
visible page because pdf.js scales <html> by 1/dpr — using it as the
hit-test denominator made click fractions dpr× too large, so clicks
landed in the wrong place relative to stored (correct) fractions on
any dpr != 1 display. The canvas's post-transform rect is exactly the
visible page and shares the textLayer's transform space.
2026-08-17 08:32:02 -04:00
john-okeefe 1c0ebf331f Fix rect annotations: host-side overlay instead of in-iframe SVG
The in-iframe SVG overlay was invisible on PDFs for two reasons:

- pdf.js applies transform: scale(1/devicePixelRatio) to the iframe's
  <html>; an overlay inside that document shrinks into the top-left
  corner on any dpr != 1 display. The transform is also applied only
  after the page render, so load-time-injected overlays were sized
  before the scale factor existed.
- Fraction rect geometry itself was correct (selection rects and the
  denominator are both post-transform), but nothing inside the iframe
  can escape its html transform.

Render annotations host-side instead: a div inside the frame's
wrapper element with percentage-positioned children. The wrapper box
always equals the visible page area for both formats — comics (iframe
CSS-scaled inside it) and PDFs (re-rendered at true scale) — so the
overlay is immune to the html transform, zoom re-renders, iframe
scaling, and host pan/zoom, still with zero re-anchoring. The
in-iframe click hit-test is unchanged (it compares fractions against
the same post-transform denominator).
2026-08-17 07:45:52 -04:00
john-okeefe aba68d8723 Rect annotations for fixed-layout: fraction-space overlay + hit-testing
Adds a parallel annotation pipeline for PDF/comic pages (the CFI
overlayer pipeline only exists for the reflowable paginator):

- addRectAnnotation({key, index, rects, color}) /
  removeRectAnnotation(key): rects are page-fraction [x,y,w,h]
  quads, stored per key and rendered into a full-bleed SVG injected
  into the page iframe. The SVG uses viewBox 0 0 100 100 with
  preserveAspectRatio none, so fraction rects stay aligned through
  iframe CSS-zoom (comics), the host transform-based pan/zoom, and
  PDF hi-res re-renders (pdf.js rebuilds canvas/textLayer but not
  body children) — no re-anchoring hooks needed anywhere.
- Frames carry their section index; annotations re-render into each
  freshly created iframe (#showSpread destroys and recreates frames
  on every spread change).
- Clicks are hit-tested against the fraction rects (in iframe
  coordinate space, denominator = img or documentElement bounding
  rect so PDF's devicePixelRatio transform cancels) and emit
  show-rect-annotation with host-space popover coordinates. Skipped
  while a text selection is active so drag-selecting text doesn't
  pop the editor.
- Comic page img is now display:block so the document box equals
  the image box (no inline-baseline gap inflating denominators).
2026-08-16 12:44:22 -04:00
john-okeefe e9e61d885b Touch gestures for fixed-layout: pinch-zoom, pan, swipe paging, double-tap
- Single finger: pan while zoomed; horizontal swipe pages at fit
  (direction mapped through next()/prev() so RTL manga reads
  correctly); drag on PDF text still defers to selection via the
  existing smart-detection (realTarget).
- Two fingers: pinch-zoom around the midpoint (through the shared
  zoom machinery, so PDF re-render scheduling works) plus
  midpoint-pan; lifts back to single-finger pan when one lifts.
- Double-tap: zoom 2.5x at the point / reset to fit.
- Touch events forwarded from page iframes with converted
  coordinates; preventDefault only when the engine actually consumes
  the gesture so native selection/taps stay intact.
- touch-action: none on :host and in comic/pdf page documents so the
  browser does not fight the gesture engine inside iframes.
2026-08-14 15:54:00 -04:00
john-okeefe 29bc958c26 Add 'text' interaction mode for PDF (selection-only, no smart detection)
'select' keeps smart behavior (drag text to select, drag blank to
pan); 'text' always defers to the text/annotation layer so drags
never pan — for users who want pure selection. Shift still force-pans
in both modes; 'pan' remains force-pan.
2026-08-14 14:24:41 -04:00
john-okeefe d4d87a94f8 Avoid redundant render when setting spread before first display 2026-08-13 12:46:11 -04:00
john-okeefe d9b1d5d18f Smart PDF pan/select + reactive spread toggle
- In select mode, only defer to the text/annotation layer when the
  pointer is actually on text or a link; otherwise start panning.
  Lets PDF pages be dragged on blank space while keeping text
  selection. Shift and explicit 'pan' mode still force-pan.
- Make the 'spread' attribute reactive: extract #computeSpreads(),
  add #setSpread() which re-groups while preserving the current page.
2026-08-13 12:37:13 -04:00
john-okeefe 48feeeacad 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.
2026-04-18 23:52:50 -04:00
john-okeefe fa790c2abb Fix zoom percentage to track relative to fit-page scale
The zoom percentage display had two bugs:
1. Scroll-zoom never updated the status bar percentage because the
   'zoom' event listener was registered inside the magnifier-toggle
   click handler (only added on click, and re-added on each click).
2. After reset (fit-page), the status bar showed '100%' but the
   zoom-in button treated the current scale as 1.0 (absolute), not
   the fit-page scale (e.g. 0.3). This caused a wild zoom jump
   from '100%' to '120%' that was actually a 4x visual change.

Fix by introducing a #baseScale field that captures the fit-page or
fit-width scale whenever the component renders in auto-fit mode. The
new currentScale getter exposes the actual transform scale, and
zoomPercent expresses it as a percentage of baseScale. All zoom-in/
zoom-out handlers (buttons and keyboard) now use currentScale instead
of getAttribute('zoom') || 1, and all percentage displays use
zoomPercent. The zoom event listener is registered once at setup
rather than inside the magnifier toggle handler.

Also removes all diagnostic console.log statements from fixed-layout.js
and reader.js.
2026-04-18 23:04:48 -04:00
john-okeefe 7ec14df593 Fix panning bugs in FixedLayout zoom/pan mode
Several interrelated fixes for the drag/pan behavior when zoomed in:

- Add dedicated #onResize() handler: numeric zoom rescales frames in
  place (preserving pan position) while fit-page/fit-width does a full
  re-render. Replaces the old bare #render() in the ResizeObserver.

- Add resetZoom() public method that clears the numeric zoom state and
  re-renders, replacing the old removeAttribute('zoom') pattern which
  left stale internal state.

- Guard spread-side sync in #handleMouseUp with !this.#portrait so
  dual-frame logic only runs in landscape spreads, preventing incorrect
  side-switching in portrait mode.

- Guard #updateTransform spread offset with !portrait for the same
  reason — portrait layouts don't have left/right frames.

- Fix #handleMouseDown to return false when declining drag (select mode
  in PDFs) and return true when starting a drag, so the iframe event
  proxy can correctly decide whether to preventDefault.

- Only preventDefault on iframe mousemove when actively dragging or
  magnifier is enabled, allowing normal text selection to work.

- Store zoom level in #zoom during #zoomByRatio and dispatch zoom event
  instead of writing back to the DOM attribute.

- Add debug console.log statements to zoomByRatio and handleWheel for
  tracing transform calculations.
2026-04-18 22:34:03 -04:00
john-okeefe d5b2e3deda Add magnifier, interaction mode toggle, and zoom percentage display
Magnifier (fixed-layout.js):
- Rewrite magnifier to use CSS background-image for comics (blob URLs
  from iframe img elements) and canvas drawImage for PDFs. Replaces
  broken iframe.cloneNode approach that created empty sandboxed clones.
- Detect which iframe is under the cursor by checking bounding rects,
  so magnifier works on both left and right panes.
- Forward mousemove events from iframe documents to handleMagnifierMove
  so the magnifier follows the cursor inside iframes.
- Add background-repeat: no-repeat to lens styles.
- Add zoomMagnifierEnabled getter for external state checks.

Interaction mode toggle (PDF only):
- Add 'interaction-mode' observed attribute with 'select'/'pan' values.
- In select mode (default for PDFs): mouse events pass through for text
  selection; Shift+drag enables panning as a fallback.
- In pan mode: drag-to-pan works as normal.
- Comic books always use pan mode (no text to select).
- Auto-detect PDF via #isPDF flag set in #showSpread from onZoom presence.
- Add isPDF getter so reader.js can show/hide the toggle button.
- Add toggle button in reader.html, hidden by default, shown for PDFs
  via the view 'load' event.
- Escape key dismisses the magnifier.

Zoom percentage display (reader.js):
- Dispatch 'zoom' CustomEvent from #zoomByRatio with the new scale.
- Listener in reader.js updates the percentage display on scroll zoom.

Also includes minor formatting (line wrapping) in #goLeft/#goRight.
2026-04-18 00:39:15 -04:00
john-okeefe 3173cf038d Fix zoom reset (press 0) not disabling frame-to-frame navigation
When removeAttribute('zoom') was called, parseFloat(null) produced NaN,
and typeof NaN === 'number' is true in JS, so #goLeft/#goRight still
entered the zoomed-in dual-pane navigation branch.

Root cause fix: handle null value in attributeChangedCallback by setting
#zoom to undefined before the parseFloat path.

Defense in depth: add isNaN() guards to the zoom checks in #goLeft and
#goRight, matching the pattern already used in #render().
2026-04-17 22:46:04 -04:00
john-okeefe 9c1a007298 Center on focused frame instead of full spread when zoomed in
When a numeric zoom is active and both panes have content, #render()
now centers on the frame indicated by #side rather than centering the
entire spread. This means on page turns the view starts on the correct
frame (left for LTR, right for RTL) instead of the middle of the spread.

For fit-page/fit-width and single-pane views, centering behavior is
unchanged.
2026-04-17 22:33:18 -04:00
john-okeefe a369c54b14 Refactor FixedLayout zoom/pan from scroll-based to transform-based model
Replace the overflow:auto + scrollTo zoom approach with a CSS transform
model to fix the scroll-to-zoom cursor targeting bug in dual-pane layouts.

Root cause: flex centering (justify-content:center) and overflow:auto
conflict -- when content fits, flex positions it; when zoomed (overflow),
scrollLeft=0 jumps to the left edge. The zoom-to-cursor formula used
scrollLeft as reference without accounting for the centering offset,
causing systematic drift toward the right pane.

Changes:
- Host CSS: overflow:hidden, display:block, position:relative
- New #wrapper div with transform:translate(x,y) for pan positioning
- New #transform = { x, y, scale } as single source of truth
- #zoomByRatio(cx,cy,ratio) uses panzoom formula: x = cx - ratio*(cx - x)
- #render() centers via transform.x/y = (hostSize - contentSize) / 2
- #updateFrameScales() handles iframe size/scale separately from centering
- Simplified drag: tracks startTX/startTY, updates transform directly
- Simplified attributeChangedCallback: detects external vs internal zoom
  changes and zooms from viewport center for external ones
- Remove all scrollLeft/scrollTop/scrollTo usage
- Remove marginBlock:auto, long-press drag detection, debug console.logs
- Preserve external API: zoom attribute, dragOffset, toggleMagnifier(),
  load/relocate events
2026-04-17 21:33:41 -04:00
john-okeefe bd98d7e185 Add scroll position and source context to zoom/pan event data in FixedLayout
This commit enhances the FixedLayout component by including additional
contextual information when zoom or pan operations occur:

- scrollLeft and scrollTop: Current scroll position of the viewport
- source: Origin of the zoom/pan action (e.g., mouse wheel, pinch zoom, button)
- leftWidth: Width of the left page/panel in dual-page layouts

These parameters provide downstream handlers with complete state information
needed to implement sophisticated zoom behaviors, such as:
- Preserving scroll position during zoom operations
- Differentiating between user-initiated and programmatic zoom changes
- Handling asymmetric layouts with different panel widths

This change is part of the ongoing zoom control implementation for
fixed-layout e-books.
2026-04-17 16:59:26 -04:00
john-okeefe 60332911e6 Fix right iframe zoom and improve pan/drag behavior in fixed-layout e-books
This commit addresses several issues with zoom and pan functionality,
particularly when zooming from the right iframe in a two-page spread:

- Add source iframe tracking to wheel and mouse events
  * Pass frameId parameter through frame creation pipeline
  * Tag events with sourceIframe and sourceFrame properties
  * Enable proper cursor position calculation regardless of event source

- Fix zoom cursor calculation for two-page spreads
  * Calculate cursor position relative to spread viewport, not individual pages
  * Simplify scroll position calculation to work uniformly for both left and right pages
  * Add debug logging for cursor calculation verification

- Improve drag/pan behavior across zoom modes
  * Add draggable state property to control drag availability
  * Handle fit-page mode differently with dragOffset tracking
  * Direct scroll manipulation for zoom levels above 1
  * Add console logging for ignored drag scenarios

- Prevent native image drag interference
  * Set draggable="false" on all images in iframe documents
  * Disable user select and webkit drag on images
  * Prevent default behavior on iframe mouse events

These changes ensure that zoom operations work correctly from both left and
right iframes, maintaining proper cursor focus and scroll position.
2026-04-17 16:56:18 -04:00
john-okeefe 5b822b682b Fix drag offset calculation and scrolling behavior in FixedLayout component
This commit fixes several issues with the drag functionality in the FixedLayout
component to ensure smooth and accurate dragging behavior:

Changes:
- Initialize dragOffset to (0, 0) instead of scroll position when drag starts
- Fix drag offset calculation direction (changed from -= to +=)
- Add proper scroll position updates during drag operations
- Ensure dragOffset is reset to (0, 0) when drag completes

The previous implementation was incorrectly initializing dragOffset with the
current scroll position, which caused the drag offset to compound with scroll
position and led to incorrect rendering. The new approach starts drag offset
at zero and directly updates the scroll position during drag operations.

This ensures that:
1. Drag movements are accurately tracked relative to the drag start point
2. Scroll position is properly synchronized with drag movements
3. Drag state is cleanly reset when drag operations complete
2026-04-17 11:02:26 -04:00
john-okeefe 3f9ab7528b Implement zoom control functionality for fixed-layout e-books
This commit adds comprehensive zoom control capabilities for fixed-layout
e-book content, allowing users to zoom in, out, and reset zoom levels for
better readability and content inspection.

Key features added:
- Zoom control UI with +/- buttons and percentage display
- Keyboard shortcuts for zoom operations (Ctrl/Cmd +, -, 0)
- Mouse wheel zoom with Ctrl/Cmd modifier key
- Drag-to-pan functionality when zoomed in
- Smooth zoom transitions and state management
- Zoom constraints (min/max limits) to prevent over-zooming

Changes include:
- FixedLayout: Enhanced with zoom state management, drag handlers,
  and keyboard/mouse event listeners for intuitive zoom control
- Reader UI: Added zoom control toolbar with visual feedback
- FixedLayout renderer integration with reader zoom controls

The zoom controls provide a seamless experience for users who need to
examine fixed-layout content more closely or adjust content size for
their preferred reading experience.
2026-04-16 21:37:54 -04:00
John Factotum b26cde2527 FXL: don't use book viewport if it's empty
Fixes https://github.com/johnfactotum/foliate-js/issues/52#issuecomment-2956113877
2025-06-10 20:51:27 +08:00
John Factotum 12dd11d1aa Run eslint --fix 2025-05-31 12:24:30 +08:00
Protected 1fe4803888 Mitigate fixed-layout exception on render when there's no available source for blankWidth and blankHeight (#70) 2025-05-31 04:18:01 +00:00
Protected 42dea3be3d Fix for fixed-layout transform anchor point in RTL context. (#68)
If the viewer is running in an RTL context (this is not the same as the
reading direction of the Book itself), and a fixed-layout rendered view
is scaled (which is often the case), anchoring the transform to the
left breaks page positioning.
2025-05-31 03:52:04 +00:00
Protected ed6400ddcb Re-render single pages when paginating fixed layout (#61) 2025-05-30 08:46:42 +00:00
Protected 9fc0b62803 Fix for FixedLayout.index returning incorrect values depending on position in spread. (#59) 2025-05-13 13:18:03 +00:00
John Factotum b82ec0b294 FXL: add zoom setting
The value of the `zoom` attribute can be either `"fit-page"` (default),
`"fit-width"`, or a number.
2024-10-11 06:41:32 +08:00
John Factotum 1e952102bd PDF: rerender when scale changes
The fixed-layout renderer now accepts an object for the return value of
`load()` (on the `book.sections` items). If it's an object, the `src`
is the URL of the section, and `onZoom`, if present, will be called
when the viewport is zoomed. With this you can scale the page yourself
rather than relying on the renderer's own scaling with CSS transform.

Also upgraded to PDF.js 4.6.82. And now internal links are broken...
Don't know how to fix that yet.
2024-09-28 12:12:59 +08:00
John Factotum b5d0453004 FXL: ignore linear & default to odd pages right
This matches the behavior of other reading systems. See
https://github.com/johnfactotum/foliate/issues/1318#issuecomment-2349375230
2024-09-14 01:21:54 +08:00
John Factotum 819106c71f FXL: fix getting viewport from SVGs 2023-10-08 20:40:16 +08:00
John Factotum 83e709f575 FXL: fix page turning around blank pages 2023-09-09 15:55:01 +08:00
John Factotum 5e0f319382 FXL: fix scaling for page-spread-center 2023-09-09 15:49:07 +08:00
John Factotum 0cebcb93c4 FXL: fix spreads when first section is center 2023-09-09 14:34:43 +08:00
John Factotum 99e246ecb8 FXL: hide empty iframes 2023-09-09 14:34:27 +08:00
John Factotum df95960f82 Expose relocation reason
And fix history not updated when scrolling in scroll mode
2023-08-17 15:18:20 +08:00
John Factotum 57ab79d598 Replace deselect with more general content access method 2023-05-16 19:12:26 +08:00
John Factotum 0a3f4b8f27 Use Shadow DOM 2023-05-15 20:00:35 +08:00
John Factotum 4acfc050ea Turn View into a custom element, and use DOM events
Also change "relocated" -> "relocate"
2023-05-15 15:27:18 +08:00
John Factotum 5da3be5200 Add destroy method
Which revokes all blob URLs and removes the element from document.
2023-05-09 14:02:07 +08:00
John Factotum 41c00942d2 Further reduce header and footer styles
Instead, provide classes.

Also: prefix all classes with `foliate-`.
2023-05-03 17:31:22 +08:00
John Factotum bc1f799c44 FXL: sandbox the iframes 2023-03-30 16:21:28 +08:00
John Factotum 6d2caaad5b FXL: fix container, which should not span whole viewport 2023-03-26 03:58:04 +08:00
John Factotum b6acb4ff5c FXL: fix onLoad callback
Also add deselect method.
2023-03-08 11:40:18 +00:00
John Factotum a11fd9637d FXL: support spread both and portrait
Also fix `page-spread-center` in portrait
2022-10-29 16:45:17 +00:00
John Factotum 3aad499696 FXL: page-spread-center and spread none 2022-10-29 16:10:56 +00:00
John Factotum e74ad4311f FXL: fix overflowing
Which for some reason only happens in Firefox
2022-10-21 10:39:55 +00:00
John Factotum 5fc72d121c Add files via upload 2022-10-18 18:55:54 +00:00