La hoja proyecta su sombra sobre el papel, con luz de frente
La sombra sigue la forma de la hoja y su borde se ensancha con la altura. Sin WebGL se mantiene el degradado desde el lomo. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
+118
-13
@@ -1615,6 +1615,8 @@ 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
|
||||
// (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_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
|
||||
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…)
|
||||
@@ -1687,11 +1689,60 @@ async function viewReader(mangaId, chapterId, startPage) {
|
||||
}
|
||||
gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, indexBuf)
|
||||
gl.bufferData(gl.ELEMENT_ARRAY_BUFFER, index, gl.STATIC_DRAW)
|
||||
// Sombra sobre el papel: la proyección de la hoja con la luz de frente. Se pinta detrás de la hoja, sin imagen.
|
||||
const shadowProg = gl.createProgram()
|
||||
gl.attachShader(shadowProg, shader(gl.VERTEX_SHADER, `#version 300 es
|
||||
uniform vec2 uSize;
|
||||
layout(location = 0) in vec2 aPos;
|
||||
layout(location = 1) in vec2 aRamp;
|
||||
layout(location = 2) in float aAlpha;
|
||||
out vec2 vRamp;
|
||||
out float vAlpha;
|
||||
void main() {
|
||||
gl_Position = vec4(aPos.x / uSize.x * 2.0 - 1.0, 1.0 - aPos.y / uSize.y * 2.0, 0.95, 1.0);
|
||||
vRamp = aRamp;
|
||||
vAlpha = aAlpha;
|
||||
}`))
|
||||
gl.attachShader(shadowProg, shader(gl.FRAGMENT_SHADER, `#version 300 es
|
||||
precision mediump float;
|
||||
in vec2 vRamp;
|
||||
in float vAlpha;
|
||||
out vec4 color;
|
||||
void main() {
|
||||
vec2 r = vRamp * vRamp * (3.0 - 2.0 * vRamp);
|
||||
color = vec4(0.0, 0.0, 0.0, vAlpha * r.x * r.y);
|
||||
}`))
|
||||
gl.linkProgram(shadowProg)
|
||||
if (!gl.getProgramParameter(shadowProg, gl.LINK_STATUS)) { leafGLOff = true; return null }
|
||||
// Cada tramo son 3×3 cuadros: el centro es la tira proyectada y el resto, su borde difuso.
|
||||
const shadowSegs = LEAF_COLS / LEAF_SHADOW_STEP
|
||||
const shadowVao = gl.createVertexArray(), shadowBuf = gl.createBuffer(), shadowIndex = gl.createBuffer()
|
||||
gl.bindVertexArray(shadowVao)
|
||||
gl.bindBuffer(gl.ARRAY_BUFFER, shadowBuf)
|
||||
gl.bufferData(gl.ARRAY_BUFFER, shadowSegs * 16 * 20, gl.DYNAMIC_DRAW)
|
||||
gl.enableVertexAttribArray(0)
|
||||
gl.vertexAttribPointer(0, 2, gl.FLOAT, false, 20, 0)
|
||||
gl.enableVertexAttribArray(1)
|
||||
gl.vertexAttribPointer(1, 2, gl.FLOAT, false, 20, 8)
|
||||
gl.enableVertexAttribArray(2)
|
||||
gl.vertexAttribPointer(2, 1, gl.FLOAT, false, 20, 16)
|
||||
const shadowIdx = new Uint16Array(shadowSegs * 54)
|
||||
let m = 0
|
||||
for (let q = 0; q < shadowSegs; q++) for (let j = 0; j < 3; j++) for (let k = 0; k < 3; k++) {
|
||||
const v = (a, b) => q * 16 + a * 4 + b
|
||||
shadowIdx.set([v(j, k), v(j, k + 1), v(j + 1, k), v(j + 1, k), v(j, k + 1), v(j + 1, k + 1)], m)
|
||||
m += 6
|
||||
}
|
||||
gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, shadowIndex)
|
||||
gl.bufferData(gl.ELEMENT_ARRAY_BUFFER, shadowIdx, gl.STATIC_DRAW)
|
||||
gl.bindVertexArray(null)
|
||||
gl.bindBuffer(gl.ARRAY_BUFFER, posBuf)
|
||||
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,
|
||||
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,
|
||||
uView: gl.getUniformLocation(prog, 'uView'), uPaper: gl.getUniformLocation(prog, 'uPaper'), maxSize: gl.getParameter(gl.MAX_TEXTURE_SIZE) }
|
||||
return leafGL
|
||||
}
|
||||
@@ -1745,7 +1796,7 @@ async function viewReader(mangaId, chapterId, startPage) {
|
||||
leafTextures.clear()
|
||||
}
|
||||
// Devuelve null si no se puede (sin WebGL2 o sin textura): entonces se usa paperCurl.
|
||||
function glCurl(r, frontIm, backIm, spineLeft) {
|
||||
function glCurl(r, frontIm, backIm, spineLeft, paper = r) {
|
||||
if (!initLeafGL() || leafNoTexture.has(frontIm.src) || (backIm && leafNoTexture.has(backIm.src))) return null
|
||||
const L = leafGL
|
||||
const { gl, canvas, positions, uvs } = L
|
||||
@@ -1760,6 +1811,11 @@ async function viewReader(mangaId, chapterId, startPage) {
|
||||
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.useProgram(L.shadowProg)
|
||||
gl.uniform2f(L.uSize, W, H)
|
||||
gl.useProgram(L.prog)
|
||||
const scale = pw / W
|
||||
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.activeTexture(gl.TEXTURE0)
|
||||
gl.bindTexture(gl.TEXTURE_2D, front)
|
||||
@@ -1785,7 +1841,47 @@ async function viewReader(mangaId, chapterId, startPage) {
|
||||
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
|
||||
// Con la luz de frente, cada tira cae en vertical sobre el papel (x e y sin cambiar). La altura decide el borde:
|
||||
// cerca del papel, nítido y oscuro; lejos, ancho y claro. Con MAX, los tramos que se solapan no oscurecen de más,
|
||||
// y con la prueba de profundidad no se pinta lo que tapa la hoja.
|
||||
function drawShadow(lift) {
|
||||
const data = L.shadowData, col = (R + 1) * 4, strength = SHADOW_DARK * Math.min(1, lift * 4), fall = r.w * SHADOW_FALL
|
||||
let n = 0
|
||||
for (let c = 0; c < N; c += LEAF_SHADOW_STEP) {
|
||||
const i = c * col, e = (c + LEAF_SHADOW_STEP) * col, down = R * 4
|
||||
const ax = positions[i], ay = positions[i + 1], bx = positions[i + down], by = positions[i + down + 1], cx = positions[e], cy = positions[e + 1]
|
||||
const len = Math.hypot(bx - ax, by - ay) || 1, vx = (bx - ax) / len, vy = (by - ay) / len
|
||||
let ux = vy, uy = -vx
|
||||
if ((cx - ax) * ux + (cy - ay) * uy < 0) { ux = -ux; uy = -uy }
|
||||
for (let j = 0; j < 4; j++) for (let k = 0; k < 4; k++) {
|
||||
const at = (j < 2 ? i : e) + (k < 2 ? 0 : down)
|
||||
const height = Math.abs(positions[at + 2]), soft = 2 + height * SHADOW_SOFT
|
||||
const oj = j === 0 ? -1 : j === 3 ? 1 : 0, ok = k === 0 ? -1 : k === 3 ? 1 : 0
|
||||
data[n++] = positions[at] + (ux * oj + vx * ok) * soft
|
||||
data[n++] = positions[at + 1] + (uy * oj + vy * ok) * soft
|
||||
data[n++] = oj ? 0 : 1
|
||||
data[n++] = ok ? 0 : 1
|
||||
data[n++] = strength / (1 + height / fall)
|
||||
}
|
||||
}
|
||||
gl.useProgram(L.shadowProg)
|
||||
gl.bindVertexArray(L.shadowVao)
|
||||
gl.bindBuffer(gl.ARRAY_BUFFER, L.shadowBuf)
|
||||
gl.bufferSubData(gl.ARRAY_BUFFER, 0, data)
|
||||
gl.enable(gl.SCISSOR_TEST)
|
||||
gl.enable(gl.BLEND)
|
||||
gl.blendEquation(gl.MAX)
|
||||
gl.blendFunc(gl.ONE, gl.ONE)
|
||||
gl.depthMask(false)
|
||||
gl.drawElements(gl.TRIANGLES, L.shadowCount, gl.UNSIGNED_SHORT, 0)
|
||||
gl.depthMask(true)
|
||||
gl.disable(gl.BLEND)
|
||||
gl.disable(gl.SCISSOR_TEST)
|
||||
gl.bindVertexArray(null)
|
||||
gl.useProgram(L.prog)
|
||||
gl.bindBuffer(gl.ARRAY_BUFFER, L.posBuf)
|
||||
}
|
||||
// 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.
|
||||
function set(g, reverse, tiltZ = 0, tiltX = 0) {
|
||||
const a = curve.angles(g, reverse)
|
||||
@@ -1818,6 +1914,7 @@ async function viewReader(mangaId, chapterId, startPage) {
|
||||
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)
|
||||
if (lift > 0) drawShadow(lift)
|
||||
return lift
|
||||
}
|
||||
const pivot = y => { pivotY = y === 'bottom' ? r.h : y === 'top' ? 0 : r.h / 2 }
|
||||
@@ -1916,7 +2013,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, frontIm, backIm = null, spineLeft, reverse = false
|
||||
let rect, paper, 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
|
||||
@@ -1928,6 +2025,9 @@ 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)
|
||||
const other = rel(stay)
|
||||
const left = Math.min(rect.l, other.l), top = Math.min(rect.t, other.t)
|
||||
paper = { l: left, t: top, w: Math.max(rect.l + rect.w, other.l + other.w) - left, h: Math.max(rect.t + rect.h, other.t + other.h) - top }
|
||||
frontIm = front
|
||||
backIm = sign > 0 ? newX[0] : newX.at(-1)
|
||||
spineLeft = sign > 0
|
||||
@@ -1949,16 +2049,21 @@ async function viewReader(mangaId, chapterId, startPage) {
|
||||
reverse = dir < 0
|
||||
if (dir > 0) old.style.visibility = 'hidden'
|
||||
}
|
||||
// Sombra que la hoja levantada proyecta sobre lo que hay debajo.
|
||||
const drop = document.createElement('i')
|
||||
drop.className = `curldrop ${spineLeft ? 'l' : 'r'}`
|
||||
place(drop, rect)
|
||||
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 = 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)
|
||||
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.
|
||||
let drop = null
|
||||
if (!curl.gl) {
|
||||
place(curl.el, rect)
|
||||
drop = document.createElement('i')
|
||||
drop.className = `curldrop ${spineLeft ? 'l' : 'r'}`
|
||||
place(drop, rect)
|
||||
box.append(drop)
|
||||
extra.push(drop)
|
||||
}
|
||||
box.append(curl.el)
|
||||
extra.push(curl.el)
|
||||
;[g0, g1] = double ? [0, 1] : curl.span(reverse)
|
||||
// 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.
|
||||
@@ -1967,7 +2072,7 @@ async function viewReader(mangaId, chapterId, startPage) {
|
||||
step = g => {
|
||||
const k = grab ? grab * Math.sin(Math.PI * Math.min(1, Math.max(0, (g - g0) / (g1 - g0 || 1)))) : 0
|
||||
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)
|
||||
if (drop) drop.style.opacity = (lift * 0.8).toFixed(3)
|
||||
}
|
||||
dur = double ? 750 : 610
|
||||
auto = t => g0 + (g1 - g0) * t
|
||||
|
||||
Reference in New Issue
Block a user