La vista de una página funciona como un libro abierto

Entre las dos caras de una pareja la vista se recorre, con las páginas unidas
por el lomo. Entre parejas gira la hoja, con la página nueva en el reverso, la
siguiente debajo y la vista acompañando al giro. La hendidura alterna de lado
según la cara, y las páginas anchas ya no la llevan.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-11-02 17:05:18 -06:00
co-authored by Claude Opus 5.5
parent eabf82ad90
commit 5776389ebb
2 changed files with 131 additions and 28 deletions
+129 -24
View File
@@ -1453,8 +1453,10 @@ async function viewReader(mangaId, chapterId, startPage) {
// --- Doble página: la 1.ª va sola (portada) y las apaisadas (ya dobles) también. // --- Doble página: la 1.ª va sola (portada) y las apaisadas (ya dobles) también.
const wide = new Set() const wide = new Set()
const isWide = img => img.naturalWidth > img.naturalHeight * 1.1 const isWide = img => img.naturalWidth > img.naturalHeight * 1.1
function groupOf(i) { const groupOf = i => (isDouble() ? spreadOf(i) : [i])
if (!isDouble()) return [i] // Las páginas que quedan frente a frente en el libro abierto: la portada sola, luego de dos en dos; una página
// ancha ocupa las dos caras.
function spreadOf(i) {
for (let s = 0; s < total;) { for (let s = 0; s < total;) {
const g = s === 0 || wide.has(s) || wide.has(s + 1) || s + 1 >= total ? [s] : [s, s + 1] const g = s === 0 || wide.has(s) || wide.has(s + 1) || s + 1 >= total ? [s] : [s, s + 1]
if (g.includes(i)) return g if (g.includes(i)) return g
@@ -1472,7 +1474,11 @@ async function viewReader(mangaId, chapterId, startPage) {
for (const i of want) { for (const i of want) {
if (preloaded.has(i)) continue if (preloaded.has(i)) continue
const im = new Image() const im = new Image()
im.onload = () => isWide(im) && wide.add(i) im.onload = () => {
if (!isWide(im) || wide.has(i)) return
wide.add(i)
refreshGutters() // una página ancha cambia las parejas de las páginas de delante
}
im.src = pages[i] im.src = pages[i]
im.decode?.().catch(() => {}) im.decode?.().catch(() => {})
preloaded.set(i, im) preloaded.set(i, im)
@@ -1481,11 +1487,22 @@ async function viewReader(mangaId, chapterId, startPage) {
scheduleLeafTextures() scheduleLeafTextures()
} }
const imgReady = im => im.complete && im.naturalWidth > 0 const imgReady = im => im.complete && im.naturalWidth > 0
// Lado de la hendidura del lomo: l o r (borde de la página) o m (página doble, en el centro). // Lado de la hendidura del lomo: l o r (borde de la página); una página ancha no lleva (none).
// En la vista de una página, la cara de delante de cada pareja lo tiene al lado contrario que la hoja que gira.
const gutterOf = (i, n) => { const gutterOf = (i, n) => {
if (wide.has(i)) return 'm' if (wide.has(i)) return 'none'
const rtl = settings.dir === 'rtl' const rtl = settings.dir === 'rtl'
return shown.length === 2 ? (rtl === (n === 0) ? 'l' : 'r') : rtl ? 'r' : 'l' if (shown.length === 2) return rtl === (n === 0) ? 'l' : 'r'
const spread = spreadOf(i)
return !isDouble() && spread.length === 2 && spread[0] === i ? (rtl ? 'l' : 'r') : rtl ? 'r' : 'l'
}
const refreshGutters = () => box.querySelectorAll('.stage:not(.leaving) img').forEach((im, n) => { im.dataset.gutter = gutterOf(Number(im.dataset.i), n) })
// Vista de una página con «pasar hoja»: entre las dos caras de una pareja no se gira la hoja, se recorre con la vista.
function isPan(old) {
const imgs = old.querySelectorAll('img')
if (settings.anim !== 'flip' || isDouble() || box.classList.contains('fit-width') || imgs.length !== 1 || shown.length !== 1) return false
const from = Number(imgs[0].dataset.i)
return from !== shown[0] && spreadOf(from).includes(shown[0])
} }
// Hacia una página que aún no ha llegado, la actual se queda (con un indicador tras un momento) hasta que // Hacia una página que aún no ha llegado, la actual se queda (con un indicador tras un momento) hasta que
// llegue o pasen 4 s, en vez de pasar la hoja y dejar negro debajo. Si se pide otra página mientras, // llegue o pasen 4 s, en vez de pasar la hoja y dejar negro debajo. Si se pide otra página mientras,
@@ -1535,12 +1552,12 @@ async function viewReader(mangaId, chapterId, startPage) {
}) })
stage.classList.toggle('loading', pending.length > 0) stage.classList.toggle('loading', pending.length > 0)
// Al arrastrar, el sonido sale al soltar (si la página llega a pasar). // Al arrastrar, el sonido sale al soltar (si la página llega a pasar).
if (old && dir && !wantDrag) turnSound(settings.sfx, turnPan(dir)) if (old && dir && !wantDrag && !isPan(old)) turnSound(settings.sfx, turnPan(dir))
if (old && dir && settings.anim !== 'none' && zoom.s === 1) turnPage(old, stage, dir, wantDrag) if (old && dir && settings.anim !== 'none' && zoom.s === 1) turnPage(old, stage, dir, wantDrag)
else { box.replaceChildren(stage) } else { box.replaceChildren(stage) }
stage.querySelectorAll('img').forEach(img => img.addEventListener('load', () => { stage.querySelectorAll('img').forEach(img => img.addEventListener('load', () => {
const i = Number(img.dataset.i) const i = Number(img.dataset.i)
if (isWide(img)) img.dataset.gutter = 'm' if (isWide(img)) img.dataset.gutter = 'none'
if (isWide(img) && !wide.has(i)) { if (isWide(img) && !wide.has(i)) {
wide.add(i) wide.add(i)
if (shown.length === 2 && shown.includes(i)) goTo(shown[0], false, true) // reagrupar if (shown.length === 2 && shown.includes(i)) goTo(shown[0], false, true) // reagrupar
@@ -1550,6 +1567,15 @@ async function viewReader(mangaId, chapterId, startPage) {
window.scrollTo(0, 0) window.scrollTo(0, 0)
preload() preload()
} }
// Giro entre dos parejas, en la vista de una página: la hoja lleva la página vieja por una cara y la nueva por la otra.
function isBookTurn(old, dir) {
const imgs = old.querySelectorAll('img')
if (settings.anim !== 'flip' || isDouble() || box.classList.contains('fit-width') || imgs.length !== 1 || shown.length !== 1) return false
const from = Number(imgs[0].dataset.i), to = shown[0]
if (Math.abs(from - to) !== 1) return false
const [opening, turned] = dir > 0 ? [to, from] : [from, to]
return spreadOf(opening)[0] === opening && spreadOf(opening).length === 2 && !wide.has(turned)
}
// --- Animación al pasar página. La página nueva se pone debajo y la vieja (o una hoja) se anima encima; // --- Animación al pasar página. La página nueva se pone debajo y la vieja (o una hoja) se anima encima;
// al terminar solo queda la nueva. Si se pasa otra página antes de acabar, la animación se completa ya. // al terminar solo queda la nueva. Si se pasa otra página antes de acabar, la animación se completa ya.
let turning = null // { anims, cleanup } (y move/release si sigue al dedo) let turning = null // { anims, cleanup } (y move/release si sigue al dedo)
@@ -1622,6 +1648,7 @@ async function viewReader(mangaId, chapterId, startPage) {
// --- Hoja con WebGL: una malla con la página como textura, en un solo canvas. Hace lo mismo que paperCurl // --- Hoja con WebGL: una malla con la página como textura, en un solo canvas. Hace lo mismo que paperCurl
// (que queda para cuando no hay WebGL2) sin una capa por cara, que es lo que le pesa a la GPU. // (que queda para cuando no hay WebGL2) sin una capa por cara, que es lo que le pesa a la GPU.
const LEAF_COLS = 32, LEAF_ROWS = 8 const LEAF_COLS = 32, LEAF_ROWS = 8
const BOOK_FOLLOW_FROM = 0.25 // parte del giro antes de que la vista empiece a acompañar a la hoja
const LEAF_SHADOW_STEP = 2 // columnas de la hoja por cada tramo de sombra const LEAF_SHADOW_STEP = 2 // columnas de la hoja por cada tramo de sombra
const SHADOW_DARK = 0.95, SHADOW_SOFT = 0.4, SHADOW_FALL = 3 // oscuridad junto al papel, ancho del borde y caída con la altura const SHADOW_DARK = 0.95, SHADOW_SOFT = 0.4, SHADOW_FALL = 3 // oscuridad junto al papel, ancho del borde y caída con la altura
let leafGL = null, leafGLOff = false, leafTimer = 0 let leafGL = null, leafGLOff = false, leafTimer = 0
@@ -1652,7 +1679,7 @@ async function viewReader(mangaId, chapterId, startPage) {
gl.attachShader(prog, shader(gl.FRAGMENT_SHADER, `#version 300 es gl.attachShader(prog, shader(gl.FRAGMENT_SHADER, `#version 300 es
precision highp float; precision highp float;
uniform sampler2D uFront, uBack; uniform sampler2D uFront, uBack;
uniform bool uPaper, uSpineLeft, uSpread; uniform bool uPaper, uSpineLeft, uWide;
in vec2 vUV; in vec2 vUV;
in float vC; in float vC;
out vec4 color; out vec4 color;
@@ -1670,7 +1697,7 @@ async function viewReader(mangaId, chapterId, startPage) {
float fromSpine = uSpineLeft ? vUV.x : 1.0 - vUV.x; float fromSpine = uSpineLeft ? vUV.x : 1.0 - vUV.x;
if (gl_FrontFacing) { if (gl_FrontFacing) {
t = texture(uFront, vUV, -0.5); t = texture(uFront, vUV, -0.5);
b = (1.0 - (1.0 - vC) * 0.3) * gutter(uSpread ? abs(vUV.x - 0.5) * 2.0 : fromSpine); b = (1.0 - (1.0 - vC) * 0.3) * (uWide ? 1.0 : gutter(fromSpine));
} else { } else {
t = uPaper ? vec4(0.914, 0.898, 0.863, 1.0) : texture(uBack, vec2(1.0 - vUV.x, vUV.y), -0.5); t = uPaper ? vec4(0.914, 0.898, 0.863, 1.0) : texture(uBack, vec2(1.0 - vUV.x, vUV.y), -0.5);
b = (1.0 - (1.0 + vC) * 0.3) * gutter(fromSpine); b = (1.0 - (1.0 + vC) * 0.3) * gutter(fromSpine);
@@ -1759,7 +1786,7 @@ async function viewReader(mangaId, chapterId, startPage) {
canvas.addEventListener('webglcontextlost', () => { if (leafGL?.gl === gl) { leafGL = null; leafTextures.clear() } }) canvas.addEventListener('webglcontextlost', () => { if (leafGL?.gl === gl) { leafGL = null; leafTextures.clear() } })
leafGL = { canvas, gl, prog, posBuf, uvBuf, aniso, shadowProg, shadowVao, shadowBuf, shadowCount: m, shadowData: new Float32Array(shadowSegs * 16 * 5), leafGL = { canvas, gl, prog, posBuf, uvBuf, aniso, shadowProg, shadowVao, shadowBuf, shadowCount: m, shadowData: new Float32Array(shadowSegs * 16 * 5),
uSize: gl.getUniformLocation(shadowProg, 'uSize'), positions: new Float32Array(count * 4), uvs: new Float32Array(count * 2), indexCount: n, uSize: gl.getUniformLocation(shadowProg, 'uSize'), positions: new Float32Array(count * 4), uvs: new Float32Array(count * 2), indexCount: n,
uView: gl.getUniformLocation(prog, 'uView'), uPaper: gl.getUniformLocation(prog, 'uPaper'), uSpineLeft: gl.getUniformLocation(prog, 'uSpineLeft'), uSpread: gl.getUniformLocation(prog, 'uSpread'), maxSize: gl.getParameter(gl.MAX_TEXTURE_SIZE) } uView: gl.getUniformLocation(prog, 'uView'), uPaper: gl.getUniformLocation(prog, 'uPaper'), uSpineLeft: gl.getUniformLocation(prog, 'uSpineLeft'), uWide: gl.getUniformLocation(prog, 'uWide'), maxSize: gl.getParameter(gl.MAX_TEXTURE_SIZE) }
return leafGL return leafGL
} }
function leafTexture(im) { function leafTexture(im) {
@@ -1834,7 +1861,7 @@ async function viewReader(mangaId, chapterId, startPage) {
gl.scissor(Math.round(paper.l * scale), Math.round((H - paper.t - paper.h) * scale), Math.round(paper.w * scale), Math.round(paper.h * scale)) gl.scissor(Math.round(paper.l * scale), Math.round((H - paper.t - paper.h) * scale), Math.round(paper.w * scale), Math.round(paper.h * scale))
gl.uniform1i(L.uPaper, back ? 0 : 1) gl.uniform1i(L.uPaper, back ? 0 : 1)
gl.uniform1i(L.uSpineLeft, spineLeft ? 1 : 0) gl.uniform1i(L.uSpineLeft, spineLeft ? 1 : 0)
gl.uniform1i(L.uSpread, frontIm.dataset.gutter === 'm' ? 1 : 0) gl.uniform1i(L.uWide, frontIm.dataset.gutter === 'none' ? 1 : 0)
gl.activeTexture(gl.TEXTURE0) gl.activeTexture(gl.TEXTURE0)
gl.bindTexture(gl.TEXTURE_2D, front) gl.bindTexture(gl.TEXTURE_2D, front)
gl.activeTexture(gl.TEXTURE1) gl.activeTexture(gl.TEXTURE1)
@@ -1901,13 +1928,13 @@ async function viewReader(mangaId, chapterId, startPage) {
} }
// Devuelve cuánto está levantada la hoja (0 a 1). Encadena los giros de tira en // Devuelve cuánto está levantada la hoja (0 a 1). Encadena los giros de tira en
// tira como lo hacen las capas anidadas de paperCurl. // tira como lo hacen las capas anidadas de paperCurl.
function set(g, reverse, tiltZ = 0, tiltX = 0) { function set(g, reverse, tiltZ = 0, tiltX = 0, cam = 0) {
const a = curve.angles(g, reverse) const a = curve.angles(g, reverse)
let m = [1, 0, 0, 0, 1, 0, 0, 0, 1], x = 0, y = 0, z = 0, prev = 0, prevC = 1, lift = 0, n = 0 let m = [1, 0, 0, 0, 1, 0, 0, 0, 1], x = 0, y = 0, z = 0, prev = 0, prevC = 1, lift = 0, n = 0
const column = c => { const column = c => {
for (let f = 0; f <= R; f++) { for (let f = 0; f <= R; f++) {
const v = (r.h * f) / R - pivotY const v = (r.h * f) / R - pivotY
positions[n++] = spineX + x + m[1] * v positions[n++] = spineX + cam + x + m[1] * v
positions[n++] = r.t + pivotY + y + m[4] * v positions[n++] = r.t + pivotY + y + m[4] * v
positions[n++] = z + m[7] * v positions[n++] = z + m[7] * v
positions[n++] = c positions[n++] = c
@@ -1997,7 +2024,8 @@ async function viewReader(mangaId, chapterId, startPage) {
// "Delante" es hacia la izquierda en izq→der (la hoja derecha pasa a la izquierda) y al revés en der→izq. // "Delante" es hacia la izquierda en izq→der (la hoja derecha pasa a la izquierda) y al revés en der→izq.
const sign = (settings.dir === 'rtl' ? -1 : 1) * dir const sign = (settings.dir === 'rtl' ? -1 : 1) * dir
const fitWidth = box.classList.contains('fit-width') const fitWidth = box.classList.contains('fit-width')
let kind = settings.anim const pan = isPan(old)
let kind = pan ? 'pan' : settings.anim
const ready = im => im.complete && im.naturalWidth > 0 const ready = im => im.complete && im.naturalWidth > 0
const oldImgs = [...old.querySelectorAll('img')] const oldImgs = [...old.querySelectorAll('img')]
// La hoja necesita las páginas ya cargadas y la misma disposición (1↔1 o 2↔2); si no, deslizar. // La hoja necesita las páginas ya cargadas y la misma disposición (1↔1 o 2↔2); si no, deslizar.
@@ -2014,9 +2042,33 @@ async function viewReader(mangaId, chapterId, startPage) {
const extra = [] const extra = []
const cleanup = () => { old.remove(); extra.forEach(e => e.remove()); stage.style.transform = '' } const cleanup = () => { old.remove(); extra.forEach(e => e.remove()); stage.style.transform = '' }
// Al soltar el dedo sin pasar la página: queda la vieja como estaba. // Al soltar el dedo sin pasar la página: queda la vieja como estaba.
const revert = () => { stage.remove(); extra.forEach(e => e.remove()); old.classList.remove('leaving'); Object.assign(old.style, { visibility: '', opacity: '', transform: '' }) } const revert = () => { stage.remove(); extra.forEach(e => e.remove()); old.classList.remove('leaving'); Object.assign(old.style, { visibility: '', opacity: '', transform: '', background: '' }) }
const easeOut = t => 1 - (1 - t) ** 3 const easeOut = t => 1 - (1 - t) ** 3
// Tapa lo que hay a los lados de la página, para que las vecinas no aparezcan de golpe en una pantalla más ancha.
const addSideMasks = width => {
const side = (box.clientWidth - width) / 2
if (side <= 0.5) return
for (const edge of ['left', 'right']) {
const mask = document.createElement('i')
mask.className = 'panmask'
Object.assign(mask.style, { [edge]: '0', width: `${side}px` })
box.append(mask)
extra.push(mask)
}
}
// step(g) dibuja el avance g; toG(q) convierte lo recorrido por el dedo (0-1 de range px) en avance. // step(g) dibuja el avance g; toG(q) convierte lo recorrido por el dedo (0-1 de range px) en avance.
const B = box.getBoundingClientRect()
const rel = im => { const r = im.getBoundingClientRect(); return { l: r.left - B.left, t: r.top - B.top, w: r.width, h: r.height } }
const place = (el, r) => Object.assign(el.style, { left: `${r.l}px`, top: `${r.t}px`, width: `${r.w}px`, height: `${r.h}px` })
const grabOf = r => {
const at = grabY != null ? (grabY - B.top - r.t) / r.h : 0.5
return at < 1 / 3 ? -1 : at > 2 / 3 ? 1 : 0
}
const newIm = stage.querySelector('img')
let bookCurl = null
if (kind === 'flip' && ready(newIm) && isBookTurn(old, dir)) {
bookCurl = glCurl(rel(oldImgs[0]), oldImgs[0], newIm, sign > 0, { l: 0, t: 0, w: box.clientWidth, h: box.clientHeight })
}
let step, dur, range = box.clientWidth, toG = q => q, auto = easeOut, g0 = 0, g1 = 1 let step, dur, range = box.clientWidth, toG = q => q, auto = easeOut, g0 = 0, g1 = 1
if (kind === 'fade') { if (kind === 'fade') {
step = g => { old.style.opacity = String(1 - g) } step = g => { old.style.opacity = String(1 - g) }
@@ -2024,13 +2076,67 @@ async function viewReader(mangaId, chapterId, startPage) {
} else if (kind === 'slide') { } else if (kind === 'slide') {
step = g => { old.style.transform = `translateX(${-sign * 100 * g}%)`; stage.style.transform = `translateX(${sign * 100 * (1 - g)}%)` } step = g => { old.style.transform = `translateX(${-sign * 100 * g}%)`; stage.style.transform = `translateX(${sign * 100 * (1 - g)}%)` }
dur = 260 dur = 260
} else if (kind === 'pan') {
// Las dos páginas van juntas, lomo con lomo: la nueva empuja a la vieja. Lo que se vea a los lados de la página
// queda tapado, para que la nueva no aparezca de golpe si la pantalla es más ancha que ella.
const oldW = oldImgs[0].getBoundingClientRect().width
const newW = ready(newIm) ? newIm.getBoundingClientRect().width : oldW
const shift = (oldW + newW) / 2
addSideMasks(Math.max(oldW, newW))
old.style.background = 'transparent'
step = g => { old.style.transform = `translateX(${-sign * shift * g}px)`; stage.style.transform = `translateX(${sign * shift * (1 - g)}px)` }
step(0)
dur = 460
range = shift
auto = t => (t < 0.5 ? 2 * t * t : 1 - 2 * (1 - t) ** 2)
} else if (bookCurl) {
// Como un libro: la hoja gira sobre el lomo y descubre la página que tiene debajo, la vista la acompaña y la
// nueva, en el reverso de la hoja, cae en su sitio. Alrededor están las páginas vecinas, lomo con lomo.
const rect = rel(oldImgs[0]), spineLeft = sign > 0
const hinge = spineLeft ? rect.l : rect.l + rect.w
const newRect = rel(newIm)
const travel = (spineLeft ? newRect.l + newRect.w : newRect.l) - hinge
const from = Number(oldImgs[0].dataset.i), to = shown[0]
const under = []
const neighbour = (i, onHingeSide) => {
if (i < 0 || i >= total || wide.has(i)) return
const im = preloaded.get(i) || Object.assign(new Image(), { src: pages[i] })
const fit = imgReady(im) ? Math.min(box.clientWidth / im.naturalWidth, box.clientHeight / im.naturalHeight) : 0
const w = fit ? im.naturalWidth * fit : rect.w, h = fit ? im.naturalHeight * fit : rect.h
im.className = 'leafkeep'
im.alt = ''
im.dataset.gutter = spineLeft === onHingeSide ? 'l' : 'r'
place(im, { l: spineLeft === onHingeSide ? hinge : hinge - w, t: (box.clientHeight - h) / 2, w, h })
im.style.willChange = 'transform'
under.push(im)
}
const back = document.createElement('i')
back.className = 'leafkeepback'
place(back, { l: 0, t: 0, w: box.clientWidth, h: box.clientHeight })
neighbour(to + dir, true)
neighbour(from - dir, false)
box.append(back, ...under, bookCurl.el)
extra.push(back, ...under, bookCurl.el)
addSideMasks(Math.max(rect.w, newRect.w))
old.style.visibility = 'hidden'
const grab = grabOf(rect)
if (grab) bookCurl.pivot(grab < 0 ? 'bottom' : 'top')
step = g => {
const follow = Math.min(1, Math.max(0, (g - BOOK_FOLLOW_FROM) / (1 - BOOK_FOLLOW_FROM)))
const cam = travel * follow * follow * (3 - 2 * follow)
for (const im of under) im.style.transform = `translateX(${cam}px)`
const k = grab ? grab * Math.sin(Math.PI * g) : 0
bookCurl.set(g, false, (spineLeft ? 1 : -1) * GRAB_TILT_Z * k, GRAB_TILT_X * k, cam)
}
step(0)
dur = 850
auto = t => t
range = rect.w
toG = q => bookCurl.solve(1 - q * 2, false)
} else { } else {
// Pasar hoja: la hoja se dobla como papel (se levanta primero el borde libre y el lomo la sigue). // Pasar hoja: la hoja se dobla como papel (se levanta primero el borde libre y el lomo la sigue).
// Medidas relativas al contenedor. // Medidas relativas al contenedor.
const B = box.getBoundingClientRect()
const rel = im => { const r = im.getBoundingClientRect(); return { l: r.left - B.left, t: r.top - B.top, w: r.width, h: r.height } }
const byX = imgs => [...imgs].sort((a, b) => a.getBoundingClientRect().left - b.getBoundingClientRect().left) const byX = imgs => [...imgs].sort((a, b) => a.getBoundingClientRect().left - b.getBoundingClientRect().left)
const place = (el, r) => Object.assign(el.style, { left: `${r.l}px`, top: `${r.t}px`, width: `${r.w}px`, height: `${r.h}px` })
let rect, paper, frontIm, backIm = null, spineLeft, reverse = false let rect, paper, frontIm, backIm = null, spineLeft, reverse = false
if (double) { if (double) {
// Doble página: gira la hoja del lado hacia el que se avanza (derecha si sign > 0); su reverso es // Doble página: gira la hoja del lado hacia el que se avanza (derecha si sign > 0); su reverso es
@@ -2072,8 +2178,7 @@ async function viewReader(mangaId, chapterId, startPage) {
reverse = dir < 0 reverse = dir < 0
if (dir > 0) old.style.visibility = 'hidden' if (dir > 0) old.style.visibility = 'hidden'
} }
const grabAt = grabY != null ? (grabY - B.top - rect.t) / rect.h : 0.5 const grab = grabOf(rect)
const grab = grabAt < 1 / 3 ? -1 : grabAt > 2 / 3 ? 1 : 0
const curl = glCurl(rect, frontIm, backIm, spineLeft, paper) || paperCurl(rect, frontIm.src, backIm?.src || null, spineLeft, !!grab) const curl = glCurl(rect, frontIm, backIm, spineLeft, paper) || paperCurl(rect, frontIm.src, backIm?.src || null, spineLeft, !!grab)
// Sin WebGL, la sombra de debajo es un degradado desde el lomo. // Sin WebGL, la sombra de debajo es un degradado desde el lomo.
let drop = null let drop = null
@@ -2108,7 +2213,7 @@ async function viewReader(mangaId, chapterId, startPage) {
let g = toG(0) let g = toG(0)
step(g) step(g)
const ctl = { const ctl = {
anims: [], cleanup, range, anims: [], cleanup, range, pan,
move(q) { g = toG(Math.min(1, Math.max(0, q))); step(g) }, move(q) { g = toG(Math.min(1, Math.max(0, q))); step(g) },
// Termina de pasar la página (commit) o la devuelve a su sitio; done(commit) al acabar. // Termina de pasar la página (commit) o la devuelve a su sitio; done(commit) al acabar.
release(commit, done) { release(commit, done) {
@@ -2347,7 +2452,7 @@ async function viewReader(mangaId, chapterId, startPage) {
if (turning !== g.ctl) return if (turning !== g.ctl) return
const q = g.d / g.ctl.range const q = g.d / g.ctl.range
const commit = e.type === 'pointerup' && (g.v > 0.35 || (q > 0.3 && g.v > -0.15)) const commit = e.type === 'pointerup' && (g.v > 0.35 || (q > 0.3 && g.v > -0.15))
if (commit) turnSound(settings.sfx, turnPan(g.dir)) if (commit && !g.ctl.pan) turnSound(settings.sfx, turnPan(g.dir))
g.ctl.release(commit, ok => { g.ctl.release(commit, ok => {
if (ok) return if (ok) return
shown = g.prev shown = g.prev
@@ -2401,7 +2506,7 @@ async function viewReader(mangaId, chapterId, startPage) {
$('#dir').addEventListener('change', e => { $('#dir').addEventListener('change', e => {
settings.dir = e.target.value settings.dir = e.target.value
box.dataset.dir = settings.dir box.dataset.dir = settings.dir
box.querySelectorAll('.stage img').forEach((im, n) => { im.dataset.gutter = gutterOf(Number(im.dataset.i), n) }) refreshGutters()
syncDirUi() syncDirUi()
savePrefs() savePrefs()
}) })
+2 -4
View File
@@ -398,7 +398,7 @@ body.reading #netbanner { display: none; }
.paged .curldrop.l { background: linear-gradient(90deg, rgba(0,0,0,.55), rgba(0,0,0,.2) 35%, rgba(0,0,0,0) 75%); } .paged .curldrop.l { background: linear-gradient(90deg, rgba(0,0,0,.55), rgba(0,0,0,.2) 35%, rgba(0,0,0,0) 75%); }
.paged .curldrop.r { background: linear-gradient(270deg, rgba(0,0,0,.55), rgba(0,0,0,.2) 35%, rgba(0,0,0,0) 75%); } .paged .curldrop.r { background: linear-gradient(270deg, rgba(0,0,0,.55), rgba(0,0,0,.2) 35%, rgba(0,0,0,0) 75%); }
/* Hendidura del lomo: la página se funde con el negro de detrás. Los puntos coinciden con gutter() en app.js. */ /* Hendidura del lomo: la página se funde con el negro de detrás. Los puntos coinciden con gutter() en app.js. */
.paged img[data-gutter] { .paged img[data-gutter="l"], .paged img[data-gutter="r"] {
--gutter-stops: rgba(0,0,0,.4) 0, rgba(0,0,0,.6) 1.2%, rgba(0,0,0,.8) 3%, rgba(0,0,0,.93) 6%, #000 10%; --gutter-stops: rgba(0,0,0,.4) 0, rgba(0,0,0,.6) 1.2%, rgba(0,0,0,.8) 3%, rgba(0,0,0,.93) 6%, #000 10%;
--gutter-mask: linear-gradient(var(--gutter-side), var(--gutter-stops)); --gutter-mask: linear-gradient(var(--gutter-side), var(--gutter-stops));
-webkit-mask-image: var(--gutter-mask); -webkit-mask-image: var(--gutter-mask);
@@ -406,9 +406,7 @@ body.reading #netbanner { display: none; }
} }
.paged img[data-gutter="l"] { --gutter-side: to right; } .paged img[data-gutter="l"] { --gutter-side: to right; }
.paged img[data-gutter="r"] { --gutter-side: to left; } .paged img[data-gutter="r"] { --gutter-side: to left; }
.paged img[data-gutter="m"] { .paged .panmask { position: absolute; top: 0; bottom: 0; z-index: 4; pointer-events: none; background: #000; }
--gutter-mask: linear-gradient(to right, #000 45%, rgba(0,0,0,.93) 47%, rgba(0,0,0,.8) 48.5%, rgba(0,0,0,.6) 49.4%, rgba(0,0,0,.4) 50%, rgba(0,0,0,.6) 50.6%, rgba(0,0,0,.8) 51.5%, rgba(0,0,0,.93) 53%, #000 55%);
}
.paged .leafkeepback { position: absolute; z-index: 2; pointer-events: none; background: #000; } .paged .leafkeepback { position: absolute; z-index: 2; pointer-events: none; background: #000; }
.paged .leafkeep { position: absolute; z-index: 2; pointer-events: none; max-width: none; max-height: none; object-fit: fill; } .paged .leafkeep { position: absolute; z-index: 2; pointer-events: none; max-width: none; max-height: none; object-fit: fill; }