La hoja se dibuja con WebGL y el toque elige la animación según su altura
Una sola malla en un canvas en lugar de una capa con filtro por cara. Las páginas se suben a la GPU en reposo; sin WebGL2 se usa la hoja de capas. Tocar arriba, al centro o abajo levanta la hoja por esa parte, como al arrastrarla, y el giro dura 20 ms más. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
+268
-44
@@ -1478,6 +1478,7 @@ async function viewReader(mangaId, chapterId, startPage) {
|
||||
preloaded.set(i, im)
|
||||
}
|
||||
for (const i of preloaded.keys()) if (i < shown[0] - 4 || i > shown.at(-1) + 10) preloaded.delete(i)
|
||||
scheduleLeafTextures()
|
||||
}
|
||||
const imgReady = im => im.complete && im.naturalWidth > 0
|
||||
// Hacia una página que aún no ha llegado, la actual se queda (con un indicador tras un momento) hasta que
|
||||
@@ -1497,12 +1498,16 @@ async function viewReader(mangaId, chapterId, startPage) {
|
||||
const imgs = [...stage.querySelectorAll('img')]
|
||||
if ($('.stage', box) && dir && !wantDrag && !imgs.every(imgReady)) {
|
||||
waitTimer = setTimeout(() => box.classList.add('waiting'), 200)
|
||||
const touchedAt = grabY // el toque ya habrá acabado cuando llegue la página
|
||||
const arrived = Promise.all(imgs.map(im => (im.decode ? im.decode() : new Promise(r => { im.onload = im.onerror = r })).catch(() => {})))
|
||||
Promise.race([arrived, new Promise(r => setTimeout(r, 4000))]).then(() => {
|
||||
if (seq !== showSeq) return
|
||||
clearTimeout(waitTimer)
|
||||
box.classList.remove('waiting')
|
||||
if (!atEnd && isPaged() && box.isConnected) presentPaged(stage, dir)
|
||||
if (!atEnd && isPaged() && box.isConnected) {
|
||||
grabY = touchedAt
|
||||
try { presentPaged(stage, dir) } finally { grabY = null }
|
||||
}
|
||||
})
|
||||
return
|
||||
}
|
||||
@@ -1542,7 +1547,7 @@ async function viewReader(mangaId, chapterId, startPage) {
|
||||
// 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 wantDrag = false
|
||||
let grabY = null // altura de la pantalla por la que el dedo agarró la página
|
||||
let grabY = null // altura de la pantalla por la que el dedo tocó o agarró la página
|
||||
// Lado por el que se mueve la hoja: el de la página que pasa (más marcado en doble página).
|
||||
const turnPan = dir => (settings.dir === 'rtl' ? -1 : 1) * dir * (shown.length === 2 ? 0.3 : 0.12)
|
||||
function finishTurn() {
|
||||
@@ -1571,6 +1576,253 @@ async function viewReader(mangaId, chapterId, startPage) {
|
||||
// Hoja de papel que se dobla: tiras verticales anidadas, cada una girada un poco respecto a la anterior,
|
||||
// así que la hoja forma una curva. Cada tira enseña su trozo de la página por delante y del reverso
|
||||
// (la página siguiente en doble página, o papel en blanco) por detrás.
|
||||
// Ángulos de la hoja dividida en N tiras, común a la versión WebGL y a la de capas.
|
||||
function curlCurve(N) {
|
||||
// Ángulo (0 = en su sitio, 180 = pasada) de cada tira en el avance g (0 a 1). El borde libre va por
|
||||
// delante del lomo y la curva es máxima a mitad de camino. reverse: la hoja vuelve a su sitio (el
|
||||
// mismo gesto al revés, con el borde libre llegando primero).
|
||||
function angles(g, reverse) {
|
||||
const e = g < 0.5 ? 4 * g ** 3 : 1 - (-2 * g + 2) ** 3 / 2
|
||||
const bend = Math.sin(Math.PI * g) * 125
|
||||
return Array.from({ length: N }, (_, i) => {
|
||||
const a = Math.min(180, Math.max(0, 180 * e + bend * (((i + 0.5) / N) ** 1.4 - 0.45)))
|
||||
return reverse ? 180 - a : a
|
||||
})
|
||||
}
|
||||
// Tramo de g en el que se ve algo de la hoja sobre la página (alguna tira por debajo de 90°): con una
|
||||
// sola página, lo que pasa del lomo queda fuera de la pantalla y no vale la pena esperarlo.
|
||||
function span(reverse) {
|
||||
const seen = g => angles(g, reverse).some(a => a < 88)
|
||||
let g0 = 0, g1 = 1
|
||||
while (g0 < 1 && !seen(g0)) g0 += 0.01
|
||||
while (g1 > g0 && !seen(g1)) g1 -= 0.01
|
||||
return [Math.max(0, g0 - 0.02), Math.min(1, g1 + 0.04)]
|
||||
}
|
||||
// Posición del borde libre (fracción del ancho desde el lomo: 1 = en su sitio, -1 = pasada del todo)
|
||||
// y el avance g que la pone donde está el dedo.
|
||||
const edge = (g, reverse) => angles(g, reverse).reduce((sum, a) => sum + Math.cos((a * Math.PI) / 180), 0) / N
|
||||
function solve(target, reverse) {
|
||||
let lo = 0, hi = 1
|
||||
for (let k = 0; k < 18; k++) {
|
||||
const m = (lo + hi) / 2
|
||||
if ((edge(m, reverse) > target) !== reverse) lo = m
|
||||
else hi = m
|
||||
}
|
||||
return (lo + hi) / 2
|
||||
}
|
||||
return { angles, span, solve }
|
||||
}
|
||||
// --- 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.
|
||||
const LEAF_COLS = 32, LEAF_ROWS = 8
|
||||
let leafGL = null, leafGLOff = false, leafTimer = 0
|
||||
const leafTextures = new Map() // URL de la página → textura
|
||||
const leafNoTexture = new Set() // las que no se pudieron subir (demasiado grandes, sin permiso…)
|
||||
function initLeafGL() {
|
||||
if (leafGL || leafGLOff) return leafGL
|
||||
const canvas = document.createElement('canvas')
|
||||
canvas.className = 'curlgl'
|
||||
const gl = canvas.getContext('webgl2', { alpha: true, premultipliedAlpha: true, antialias: true, depth: true, powerPreference: 'high-performance' })
|
||||
const shader = (type, src) => { const sh = gl.createShader(type); gl.shaderSource(sh, src); gl.compileShader(sh); return sh }
|
||||
const prog = gl?.createProgram()
|
||||
if (gl) {
|
||||
// Misma proyección que perspective en CSS: el origen en el centro y la escala 1 / (1 - z / perspectiva).
|
||||
gl.attachShader(prog, shader(gl.VERTEX_SHADER, `#version 300 es
|
||||
uniform vec3 uView;
|
||||
layout(location = 0) in vec4 aPos;
|
||||
layout(location = 1) in vec2 aUV;
|
||||
out vec2 vUV;
|
||||
out float vC;
|
||||
void main() {
|
||||
float w = 1.0 - aPos.z / uView.z;
|
||||
gl_Position = vec4(aPos.x / uView.x * 2.0 - 1.0, 1.0 - aPos.y / uView.y * 2.0, 1.05128 * w - 0.10256, w);
|
||||
vUV = aUV;
|
||||
vC = aPos.w;
|
||||
}`))
|
||||
// La cara se oscurece al inclinarse. El sesgo negativo evita que la página se vea blanda con los mipmaps.
|
||||
gl.attachShader(prog, shader(gl.FRAGMENT_SHADER, `#version 300 es
|
||||
precision highp float;
|
||||
uniform sampler2D uFront, uBack;
|
||||
uniform bool uPaper;
|
||||
in vec2 vUV;
|
||||
in float vC;
|
||||
out vec4 color;
|
||||
void main() {
|
||||
vec4 t;
|
||||
float b;
|
||||
if (gl_FrontFacing) {
|
||||
t = texture(uFront, vUV, -0.5);
|
||||
b = 1.0 - (1.0 - vC) * 0.3;
|
||||
} else {
|
||||
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;
|
||||
}
|
||||
color = vec4(t.rgb * b, t.a);
|
||||
}`))
|
||||
gl.linkProgram(prog)
|
||||
}
|
||||
if (!gl || !gl.getProgramParameter(prog, gl.LINK_STATUS)) { leafGLOff = true; return null }
|
||||
gl.useProgram(prog)
|
||||
gl.uniform1i(gl.getUniformLocation(prog, 'uFront'), 0)
|
||||
gl.uniform1i(gl.getUniformLocation(prog, 'uBack'), 1)
|
||||
gl.enable(gl.DEPTH_TEST)
|
||||
gl.depthFunc(gl.LEQUAL)
|
||||
gl.clearColor(0, 0, 0, 0)
|
||||
const count = (LEAF_COLS + 1) * (LEAF_ROWS + 1)
|
||||
const posBuf = gl.createBuffer(), uvBuf = gl.createBuffer(), indexBuf = gl.createBuffer()
|
||||
gl.bindBuffer(gl.ARRAY_BUFFER, posBuf)
|
||||
gl.bufferData(gl.ARRAY_BUFFER, count * 16, gl.DYNAMIC_DRAW)
|
||||
gl.enableVertexAttribArray(0)
|
||||
gl.vertexAttribPointer(0, 4, gl.FLOAT, false, 0, 0)
|
||||
gl.bindBuffer(gl.ARRAY_BUFFER, uvBuf)
|
||||
gl.bufferData(gl.ARRAY_BUFFER, count * 8, gl.DYNAMIC_DRAW)
|
||||
gl.enableVertexAttribArray(1)
|
||||
gl.vertexAttribPointer(1, 2, gl.FLOAT, false, 0, 0)
|
||||
const at = (c, f) => c * (LEAF_ROWS + 1) + f
|
||||
const index = new Uint16Array(LEAF_COLS * LEAF_ROWS * 6)
|
||||
let n = 0
|
||||
for (let c = 0; c < LEAF_COLS; c++) for (let f = 0; f < LEAF_ROWS; f++) {
|
||||
index.set([at(c, f), at(c, f + 1), at(c + 1, f), at(c + 1, f), at(c, f + 1), at(c + 1, f + 1)], n)
|
||||
n += 6
|
||||
}
|
||||
gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, indexBuf)
|
||||
gl.bufferData(gl.ELEMENT_ARRAY_BUFFER, index, gl.STATIC_DRAW)
|
||||
gl.pixelStorei(gl.UNPACK_PREMULTIPLY_ALPHA_WEBGL, true)
|
||||
const aniso = gl.getExtension('EXT_texture_filter_anisotropic')
|
||||
// Sin el contexto (iOS puede quitarlo con la app en segundo plano), el siguiente giro crea otro.
|
||||
canvas.addEventListener('webglcontextlost', () => { if (leafGL?.gl === gl) { leafGL = null; leafTextures.clear() } })
|
||||
leafGL = { canvas, gl, posBuf, uvBuf, aniso, positions: new Float32Array(count * 4), uvs: new Float32Array(count * 2), indexCount: n,
|
||||
uView: gl.getUniformLocation(prog, 'uView'), uPaper: gl.getUniformLocation(prog, 'uPaper'), maxSize: gl.getParameter(gl.MAX_TEXTURE_SIZE) }
|
||||
return leafGL
|
||||
}
|
||||
function leafTexture(im) {
|
||||
const known = leafTextures.get(im.src)
|
||||
if (known) return known
|
||||
const { gl, aniso, maxSize } = leafGL
|
||||
if (Math.max(im.naturalWidth, im.naturalHeight) > maxSize) throw new Error('página demasiado grande')
|
||||
const tex = gl.createTexture()
|
||||
gl.activeTexture(gl.TEXTURE0)
|
||||
gl.bindTexture(gl.TEXTURE_2D, tex)
|
||||
try {
|
||||
gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, im)
|
||||
gl.generateMipmap(gl.TEXTURE_2D)
|
||||
if (gl.getError() !== gl.NO_ERROR) throw new Error('sin memoria para la textura')
|
||||
} catch (e) { gl.deleteTexture(tex); throw e }
|
||||
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR_MIPMAP_LINEAR)
|
||||
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR)
|
||||
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE)
|
||||
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE)
|
||||
if (aniso) gl.texParameterf(gl.TEXTURE_2D, aniso.TEXTURE_MAX_ANISOTROPY_EXT, Math.min(4, gl.getParameter(aniso.MAX_TEXTURE_MAX_ANISOTROPY_EXT)))
|
||||
leafTextures.set(im.src, tex)
|
||||
return tex
|
||||
}
|
||||
// Sube con antelación, en reposo y de una en una, las páginas de los giros posibles: subirla al empezar el
|
||||
// giro daría un tirón. Una página que no llegó a subirse se sube al girar.
|
||||
function scheduleLeafTextures(delay = 300) {
|
||||
clearTimeout(leafTimer)
|
||||
if (!leafGLOff && settings.anim === 'flip' && isPaged() && !box.classList.contains('fit-width')) leafTimer = setTimeout(nextLeafTexture, delay)
|
||||
}
|
||||
function nextLeafTexture() {
|
||||
if (turning) return scheduleLeafTextures(400)
|
||||
const n = shown.length
|
||||
const near = []
|
||||
for (let k = 1; k <= n; k++) near.push(shown.at(-1) + k, shown[0] - k)
|
||||
const wanted = [...box.querySelectorAll('.stage img'), ...near.map(i => preloaded.get(i)).filter(Boolean)]
|
||||
const keep = new Set(wanted.map(im => im.src))
|
||||
for (const [src, tex] of leafTextures) if (!keep.has(src)) { leafGL?.gl.deleteTexture(tex); leafTextures.delete(src) }
|
||||
const missing = wanted.filter(im => !leafTextures.has(im.src) && !leafNoTexture.has(im.src))
|
||||
const im = missing.find(imgReady)
|
||||
if (!im) { if (missing.length) scheduleLeafTextures(800); return }
|
||||
if (!initLeafGL()) return
|
||||
try { leafTexture(im) } catch { leafNoTexture.add(im.src) }
|
||||
scheduleLeafTextures(120)
|
||||
}
|
||||
function freeLeafGL() {
|
||||
leafGLOff = true
|
||||
clearTimeout(leafTimer)
|
||||
leafGL?.gl.getExtension('WEBGL_lose_context')?.loseContext()
|
||||
leafGL = null
|
||||
leafTextures.clear()
|
||||
}
|
||||
// Devuelve null si no se puede (sin WebGL2 o sin textura): entonces se usa paperCurl.
|
||||
function glCurl(r, frontIm, backIm, spineLeft) {
|
||||
if (!initLeafGL() || leafNoTexture.has(frontIm.src) || (backIm && leafNoTexture.has(backIm.src))) return null
|
||||
const L = leafGL
|
||||
const { gl, canvas, positions, uvs } = L
|
||||
let front, back
|
||||
try {
|
||||
front = leafTexture(frontIm)
|
||||
back = backIm ? leafTexture(backIm) : null
|
||||
} catch { leafNoTexture.add(frontIm.src); if (backIm) leafNoTexture.add(backIm.src); return null }
|
||||
const W = box.clientWidth, H = box.clientHeight, N = LEAF_COLS, R = LEAF_ROWS
|
||||
const pw = Math.round(W * devicePixelRatio), ph = Math.round(H * devicePixelRatio)
|
||||
if (canvas.width !== pw || canvas.height !== ph) { canvas.width = pw; canvas.height = ph }
|
||||
Object.assign(canvas.style, { width: `${W}px`, height: `${H}px` })
|
||||
gl.viewport(0, 0, pw, ph)
|
||||
gl.uniform3f(L.uView, W, H, parseFloat(getComputedStyle(box).perspective) || 2200)
|
||||
gl.uniform1i(L.uPaper, back ? 0 : 1)
|
||||
gl.activeTexture(gl.TEXTURE0)
|
||||
gl.bindTexture(gl.TEXTURE_2D, front)
|
||||
gl.activeTexture(gl.TEXTURE1)
|
||||
gl.bindTexture(gl.TEXTURE_2D, back)
|
||||
// Las columnas van del lomo al borde libre: con el lomo a la derecha, la malla queda al revés.
|
||||
gl.frontFace(spineLeft ? gl.CCW : gl.CW)
|
||||
for (let c = 0, n = 0; c <= N; c++) for (let f = 0; f <= R; f++) {
|
||||
uvs[n++] = spineLeft ? c / N : 1 - c / N
|
||||
uvs[n++] = f / R
|
||||
}
|
||||
gl.bindBuffer(gl.ARRAY_BUFFER, L.uvBuf)
|
||||
gl.bufferSubData(gl.ARRAY_BUFFER, 0, uvs)
|
||||
gl.bindBuffer(gl.ARRAY_BUFFER, L.posBuf)
|
||||
const curve = curlCurve(N)
|
||||
const step = (spineLeft ? r.w : -r.w) / N
|
||||
const sgn = spineLeft ? -1 : 1
|
||||
const spineX = r.l + (spineLeft ? 0 : r.w)
|
||||
let pivotY = r.h / 2
|
||||
const rad = Math.PI / 180
|
||||
const mul = (a, b) => [
|
||||
a[0] * b[0] + a[1] * b[3] + a[2] * b[6], a[0] * b[1] + a[1] * b[4] + a[2] * b[7], a[0] * b[2] + a[1] * b[5] + a[2] * b[8],
|
||||
a[3] * b[0] + a[4] * b[3] + a[5] * b[6], a[3] * b[1] + a[4] * b[4] + a[5] * b[7], a[3] * b[2] + a[4] * b[5] + a[5] * b[8],
|
||||
a[6] * b[0] + a[7] * b[3] + a[8] * b[6], a[6] * b[1] + a[7] * b[4] + a[8] * b[7], a[6] * b[2] + a[7] * b[5] + a[8] * b[8],
|
||||
]
|
||||
// Devuelve cuánto está levantada la hoja (0 a 1), para la sombra de debajo. Encadena los giros de tira en
|
||||
// tira como lo hacen las capas anidadas de paperCurl.
|
||||
function set(g, reverse, tiltZ = 0, tiltX = 0) {
|
||||
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
|
||||
const column = c => {
|
||||
for (let f = 0; f <= R; f++) {
|
||||
const v = (r.h * f) / R - pivotY
|
||||
positions[n++] = spineX + x + m[1] * v
|
||||
positions[n++] = r.t + pivotY + y + m[4] * v
|
||||
positions[n++] = z + m[7] * v
|
||||
positions[n++] = c
|
||||
}
|
||||
}
|
||||
for (let i = 0; i < N; i++) {
|
||||
const c = Math.cos(a[i] * rad)
|
||||
if (i) { x += m[0] * step; y += m[3] * step; z += m[6] * step }
|
||||
const t = sgn * (a[i] - prev) * rad
|
||||
m = mul(m, [Math.cos(t), 0, Math.sin(t), 0, 1, 0, -Math.sin(t), 0, Math.cos(t)])
|
||||
if (i && (tiltZ || tiltX)) {
|
||||
const tz = ((tiltZ * c) / (N - 1)) * rad, tx = ((tiltX * c) / (N - 1)) * rad
|
||||
m = mul(mul(m, [Math.cos(tz), -Math.sin(tz), 0, Math.sin(tz), Math.cos(tz), 0, 0, 0, 1]), [1, 0, 0, 0, Math.cos(tx), -Math.sin(tx), 0, Math.sin(tx), Math.cos(tx)])
|
||||
}
|
||||
column(i ? (c + prevC) / 2 : c)
|
||||
prev = a[i]
|
||||
prevC = c
|
||||
lift = Math.max(lift, Math.sin(a[i] * rad))
|
||||
}
|
||||
x += m[0] * step; y += m[3] * step; z += m[6] * step
|
||||
column(prevC)
|
||||
gl.bufferSubData(gl.ARRAY_BUFFER, 0, positions)
|
||||
gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT)
|
||||
gl.drawElements(gl.TRIANGLES, L.indexCount, gl.UNSIGNED_SHORT, 0)
|
||||
return lift
|
||||
}
|
||||
const pivot = y => { pivotY = y === 'bottom' ? r.h : y === 'top' ? 0 : r.h / 2 }
|
||||
return { el: canvas, gl: true, set, span: curve.span, solve: curve.solve, pivot }
|
||||
}
|
||||
function paperCurl(r, frontSrc, backSrc, spineLeft, bent = false) {
|
||||
const N = Math.max(10, Math.min(22, Math.round(r.w / 24)))
|
||||
const w = r.w / N
|
||||
@@ -1599,17 +1851,7 @@ async function viewReader(mangaId, chapterId, startPage) {
|
||||
parent = s
|
||||
}
|
||||
const step = spineLeft ? w : -w
|
||||
// Ángulo (0 = en su sitio, 180 = pasada) de cada tira en el avance g (0 a 1). El borde libre va por
|
||||
// delante del lomo y la curva es máxima a mitad de camino. reverse: la hoja vuelve a su sitio (el
|
||||
// mismo gesto al revés, con el borde libre llegando primero).
|
||||
function angles(g, reverse) {
|
||||
const e = g < 0.5 ? 4 * g ** 3 : 1 - (-2 * g + 2) ** 3 / 2
|
||||
const bend = Math.sin(Math.PI * g) * 125
|
||||
return strips.map((t, i) => {
|
||||
const a = Math.min(180, Math.max(0, 180 * e + bend * (((i + 0.5) / N) ** 1.4 - 0.45)))
|
||||
return reverse ? 180 - a : a
|
||||
})
|
||||
}
|
||||
const { angles, span, solve } = curlCurve(N)
|
||||
// Devuelve cuánto está levantada la hoja (0 a 1), para la sombra de debajo.
|
||||
// tiltZ/tiltX: curvatura total de la hoja agarrada por arriba o por abajo. Se reparte entre las tiras salvo la
|
||||
// del lomo, que no se curva, y el borde libre se lleva todo: la hoja sigue pegada al libro. Cambia de
|
||||
@@ -1630,27 +1872,6 @@ async function viewReader(mangaId, chapterId, startPage) {
|
||||
})
|
||||
return lift
|
||||
}
|
||||
// Tramo de g en el que se ve algo de la hoja sobre la página (alguna tira por debajo de 90°): con una
|
||||
// sola página, lo que pasa del lomo queda fuera de la pantalla y no vale la pena esperarlo.
|
||||
function span(reverse) {
|
||||
const seen = g => angles(g, reverse).some(a => a < 88)
|
||||
let g0 = 0, g1 = 1
|
||||
while (g0 < 1 && !seen(g0)) g0 += 0.01
|
||||
while (g1 > g0 && !seen(g1)) g1 -= 0.01
|
||||
return [Math.max(0, g0 - 0.02), Math.min(1, g1 + 0.04)]
|
||||
}
|
||||
// Posición del borde libre (fracción del ancho desde el lomo: 1 = en su sitio, -1 = pasada del todo)
|
||||
// y el avance g que la pone donde está el dedo.
|
||||
const edge = (g, reverse) => angles(g, reverse).reduce((sum, a) => sum + Math.cos((a * Math.PI) / 180), 0) / N
|
||||
function solve(target, reverse) {
|
||||
let lo = 0, hi = 1
|
||||
for (let k = 0; k < 18; k++) {
|
||||
const m = (lo + hi) / 2
|
||||
if ((edge(m, reverse) > target) !== reverse) lo = m
|
||||
else hi = m
|
||||
}
|
||||
return (lo + hi) / 2
|
||||
}
|
||||
// Punto fijo de la curvatura: el borde opuesto al agarrado (no afecta al giro sobre el eje vertical).
|
||||
const pivot = y => strips.forEach(t => { t.s.style.transformOrigin = `${spineLeft ? 'left' : 'right'} ${y}` })
|
||||
return { el, set, span, solve, pivot }
|
||||
@@ -1695,7 +1916,7 @@ async function viewReader(mangaId, chapterId, startPage) {
|
||||
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 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, frontSrc, backSrc = null, spineLeft, reverse = false
|
||||
let rect, frontIm, backIm = null, spineLeft, reverse = false
|
||||
if (double) {
|
||||
// Doble página: gira la hoja del lado hacia el que se avanza (derecha si sign > 0); su reverso es
|
||||
// la página nueva del otro lado del lomo, y la página vieja de ese lado sigue a la vista (copia
|
||||
@@ -1707,8 +1928,8 @@ async function viewReader(mangaId, chapterId, startPage) {
|
||||
const front = sign > 0 ? oldX.at(-1) : oldX[0]
|
||||
const stay = sign > 0 ? oldX[0] : oldX.at(-1)
|
||||
rect = rel(front)
|
||||
frontSrc = front.src
|
||||
backSrc = (sign > 0 ? newX[0] : newX.at(-1)).src
|
||||
frontIm = front
|
||||
backIm = sign > 0 ? newX[0] : newX.at(-1)
|
||||
spineLeft = sign > 0
|
||||
const keep = document.createElement('img')
|
||||
keep.className = 'leafkeep'
|
||||
@@ -1723,7 +1944,7 @@ async function viewReader(mangaId, chapterId, startPage) {
|
||||
// gesto al revés, con el borde libre por delante), con la actual debajo hasta que la tapa.
|
||||
const img = dir > 0 ? oldImgs[0] : stage.querySelector('img')
|
||||
rect = rel(img)
|
||||
frontSrc = img.src
|
||||
frontIm = img
|
||||
spineLeft = settings.dir !== 'rtl'
|
||||
reverse = dir < 0
|
||||
if (dir > 0) old.style.visibility = 'hidden'
|
||||
@@ -1732,14 +1953,14 @@ async function viewReader(mangaId, chapterId, startPage) {
|
||||
const drop = document.createElement('i')
|
||||
drop.className = `curldrop ${spineLeft ? 'l' : 'r'}`
|
||||
place(drop, rect)
|
||||
const grabAt = drag && grabY != null ? (grabY - B.top - rect.t) / rect.h : 0.5
|
||||
const grabAt = grabY != null ? (grabY - B.top - rect.t) / rect.h : 0.5
|
||||
const grab = grabAt < 1 / 3 ? -1 : grabAt > 2 / 3 ? 1 : 0
|
||||
const curl = paperCurl(rect, frontSrc, backSrc, spineLeft, !!grab)
|
||||
place(curl.el, rect)
|
||||
const curl = glCurl(rect, frontIm, backIm, spineLeft) || paperCurl(rect, frontIm.src, backIm?.src || null, spineLeft, !!grab)
|
||||
if (!curl.gl) place(curl.el, rect)
|
||||
box.append(drop, curl.el)
|
||||
extra.push(drop, curl.el)
|
||||
;[g0, g1] = double ? [0, 1] : curl.span(reverse)
|
||||
// Agarrada por arriba o por abajo (tercio superior o inferior), esa parte va por delante: la hoja se curva
|
||||
// Tocada o agarrada por arriba o por abajo (tercio superior o inferior), esa parte va por delante: la hoja se curva
|
||||
// sin separarse del lomo. Sin capas nuevas; es 0 al principio y al final del giro, así que cae exacta.
|
||||
// Por el centro, como siempre.
|
||||
if (grab) curl.pivot(grab < 0 ? 'bottom' : 'top')
|
||||
@@ -1748,7 +1969,7 @@ async function viewReader(mangaId, chapterId, startPage) {
|
||||
const lift = curl.set(g, reverse, (spineLeft ? 1 : -1) * (reverse ? -1 : 1) * GRAB_TILT_Z * k, GRAB_TILT_X * k)
|
||||
drop.style.opacity = (lift * 0.8).toFixed(3)
|
||||
}
|
||||
dur = double ? 700 : 560
|
||||
dur = double ? 720 : 580
|
||||
auto = t => g0 + (g1 - g0) * t
|
||||
// El borde libre va donde lo lleva el dedo: de su sitio al lomo (una página) o al otro lado (doble);
|
||||
// hacia atrás en una página, del lomo a su sitio.
|
||||
@@ -1919,7 +2140,9 @@ async function viewReader(mangaId, chapterId, startPage) {
|
||||
}
|
||||
if (zoom.s > 1) return // con zoom, un toque no pasa página
|
||||
if (center) { clearTimeout(uiTimer); uiTimer = setTimeout(toggleUi, 280); return }
|
||||
;(xr >= 0.7) === (settings.dir === 'ltr') ? forward() : backward()
|
||||
// La hoja se levanta por la altura del toque, igual que al arrastrarla.
|
||||
grabY = cy
|
||||
try { (xr >= 0.7) === (settings.dir === 'ltr') ? forward() : backward() } finally { grabY = null }
|
||||
}
|
||||
box.addEventListener('pointerdown', e => {
|
||||
if (!isPaged() || atEnd || e.target.closest('a,button,select,input')) return
|
||||
@@ -2216,6 +2439,7 @@ async function viewReader(mangaId, chapterId, startPage) {
|
||||
|
||||
render()
|
||||
cleanup = () => {
|
||||
freeLeafGL()
|
||||
endCrop()
|
||||
if (saveTimer) saveNow()
|
||||
observer?.disconnect()
|
||||
|
||||
@@ -377,6 +377,7 @@ body.reading #netbanner { display: none; }
|
||||
.paged { position: relative; perspective: 2200px; }
|
||||
.paged .stage.leaving { position: absolute; inset: 0; z-index: 1; pointer-events: none; background: #000; }
|
||||
.paged.fit-width .stage.leaving { bottom: auto; }
|
||||
.paged .curlgl { position: absolute; left: 0; top: 0; z-index: 3; pointer-events: none; filter: var(--page-filter, none); }
|
||||
.paged .curl { position: absolute; z-index: 3; transform-style: preserve-3d; pointer-events: none; }
|
||||
.paged .curl .strip { position: absolute; top: 0; transform-style: preserve-3d; will-change: transform; }
|
||||
.paged .curl .sf { position: absolute; top: 0; bottom: 0; overflow: hidden; backface-visibility: hidden; -webkit-backface-visibility: hidden; }
|
||||
|
||||
Reference in New Issue
Block a user