mirror of
https://github.com/john-okeefe/foliate-js.git
synced 2026-09-09 11:29:14 -04:00
New <foliate-webtoon> custom element: every page stacked in one
scrolling column (max-width 900px centered on wide screens), the
standard long-strip manga reading mode.
- Pages lazy-load through an IntersectionObserver with a 150% root
margin so they're ready before scrolling into view; placeholders
carry an estimated min-height until the image loads, then the real
aspect-ratio — set on the placeholder element — so geometry stays
stable across unloads and the scrollbar never jumps.
- Pages more than 12 positions from the reading point are unloaded
(image removed, section blob URL revoked) to bound memory on long
webtoons; they simply reload if the user scrolls back.
- Relocate events ({reason:'scroll', index, fraction, size:1}) fire on
rAF-throttled scroll with the viewport-center page as index and the
visible position within it as fraction — compatible with view.js's
SectionProgress enrichment, so progress saving/restoring, the
slider, and the back-stack all work unchanged.
- Renderer contract mirrors fixed-layout: goTo(await {index}) scrolls
the page into view; next/prev page by ~one screen (view.init()'s
fresh-start probe is a no-op that reports location); goLeft/goRight
map to prev/next (vertical flow is direction-independent).
No zoom/magnifier APIs — the host already gates those on the flow.
- Page images honor --fx-filter from the host for the new
brightness/contrast/invert controls.
view.js picks the renderer when book.format === 'comic' (now tagged
by makeComicBook) and options.comic.flow === 'webtoon'.
47 lines
1.9 KiB
JavaScript
47 lines
1.9 KiB
JavaScript
export const makeComicBook = ({ entries, loadBlob, getSize }, file) => {
|
|
const cache = new Map()
|
|
const urls = new Map()
|
|
const load = async name => {
|
|
if (cache.has(name)) return cache.get(name)
|
|
const src = URL.createObjectURL(await loadBlob(name))
|
|
const page = URL.createObjectURL(
|
|
new Blob([`<!DOCTYPE html><html><head><meta charset="utf-8"></head><body style="margin: 0; touch-action: none"><img src="${src}" style="display: block; image-rendering: high-quality"></body></html>`], { type: 'text/html' }))
|
|
urls.set(name, [src, page])
|
|
cache.set(name, page)
|
|
return page
|
|
}
|
|
const unload = name => {
|
|
urls.get(name)?.forEach?.(url => URL.revokeObjectURL(url))
|
|
urls.delete(name)
|
|
cache.delete(name)
|
|
}
|
|
|
|
const exts = ['.jpg', '.jpeg', '.png', '.gif', '.bmp', '.webp', '.svg', '.jxl', '.avif']
|
|
const files = entries
|
|
.map(entry => entry.filename)
|
|
.filter(name => exts.some(ext => name.endsWith(ext)))
|
|
.sort(new Intl.Collator([], { numeric: true }).compare)
|
|
if (!files.length) throw new Error('No supported image files in archive')
|
|
|
|
const book = {}
|
|
book.getCover = () => loadBlob(files[0])
|
|
book.metadata = { title: file.name }
|
|
book.sections = files.map(name => ({
|
|
id: name,
|
|
load: () => load(name),
|
|
unload: () => unload(name),
|
|
size: getSize(name),
|
|
}))
|
|
book.toc = files.map(name => ({ label: name, href: name }))
|
|
book.rendition = { layout: 'pre-paginated' }
|
|
book.format = 'comic'
|
|
book.resolveHref = href => ({ index: book.sections.findIndex(s => s.id === href) })
|
|
book.splitTOCHref = href => [href, null]
|
|
book.getTOCFragment = doc => doc.documentElement
|
|
book.destroy = () => {
|
|
for (const arr of urls.values())
|
|
for (const url of arr) URL.revokeObjectURL(url)
|
|
}
|
|
return book
|
|
}
|