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
|
// --- 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 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
|
let leafGL = null, leafGLOff = false, leafTimer = 0
|
||||||
const leafTextures = new Map() // URL de la página → textura
|
const leafTextures = new Map() // URL de la página → textura
|
||||||
const leafNoTexture = new Set() // las que no se pudieron subir (demasiado grandes, sin permiso…)
|
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.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, indexBuf)
|
||||||
gl.bufferData(gl.ELEMENT_ARRAY_BUFFER, index, gl.STATIC_DRAW)
|
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)
|
gl.pixelStorei(gl.UNPACK_PREMULTIPLY_ALPHA_WEBGL, true)
|
||||||
const aniso = gl.getExtension('EXT_texture_filter_anisotropic')
|
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.
|
// 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() } })
|
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) }
|
uView: gl.getUniformLocation(prog, 'uView'), uPaper: gl.getUniformLocation(prog, 'uPaper'), maxSize: gl.getParameter(gl.MAX_TEXTURE_SIZE) }
|
||||||
return leafGL
|
return leafGL
|
||||||
}
|
}
|
||||||
@@ -1745,7 +1796,7 @@ async function viewReader(mangaId, chapterId, startPage) {
|
|||||||
leafTextures.clear()
|
leafTextures.clear()
|
||||||
}
|
}
|
||||||
// Devuelve null si no se puede (sin WebGL2 o sin textura): entonces se usa paperCurl.
|
// 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
|
if (!initLeafGL() || leafNoTexture.has(frontIm.src) || (backIm && leafNoTexture.has(backIm.src))) return null
|
||||||
const L = leafGL
|
const L = leafGL
|
||||||
const { gl, canvas, positions, uvs } = L
|
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` })
|
Object.assign(canvas.style, { width: `${W}px`, height: `${H}px` })
|
||||||
gl.viewport(0, 0, pw, ph)
|
gl.viewport(0, 0, pw, ph)
|
||||||
gl.uniform3f(L.uView, W, H, parseFloat(getComputedStyle(box).perspective) || 2200)
|
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.uniform1i(L.uPaper, back ? 0 : 1)
|
||||||
gl.activeTexture(gl.TEXTURE0)
|
gl.activeTexture(gl.TEXTURE0)
|
||||||
gl.bindTexture(gl.TEXTURE_2D, front)
|
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[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],
|
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.
|
// 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) {
|
||||||
const a = curve.angles(g, reverse)
|
const a = curve.angles(g, reverse)
|
||||||
@@ -1818,6 +1914,7 @@ async function viewReader(mangaId, chapterId, startPage) {
|
|||||||
gl.bufferSubData(gl.ARRAY_BUFFER, 0, positions)
|
gl.bufferSubData(gl.ARRAY_BUFFER, 0, positions)
|
||||||
gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT)
|
gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT)
|
||||||
gl.drawElements(gl.TRIANGLES, L.indexCount, gl.UNSIGNED_SHORT, 0)
|
gl.drawElements(gl.TRIANGLES, L.indexCount, gl.UNSIGNED_SHORT, 0)
|
||||||
|
if (lift > 0) drawShadow(lift)
|
||||||
return lift
|
return lift
|
||||||
}
|
}
|
||||||
const pivot = y => { pivotY = y === 'bottom' ? r.h : y === 'top' ? 0 : r.h / 2 }
|
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 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` })
|
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) {
|
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
|
||||||
// la página nueva del otro lado del lomo, y la página vieja de ese lado sigue a la vista (copia
|
// 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 front = sign > 0 ? oldX.at(-1) : oldX[0]
|
||||||
const stay = sign > 0 ? oldX[0] : oldX.at(-1)
|
const stay = sign > 0 ? oldX[0] : oldX.at(-1)
|
||||||
rect = rel(front)
|
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
|
frontIm = front
|
||||||
backIm = sign > 0 ? newX[0] : newX.at(-1)
|
backIm = sign > 0 ? newX[0] : newX.at(-1)
|
||||||
spineLeft = sign > 0
|
spineLeft = sign > 0
|
||||||
@@ -1949,16 +2049,21 @@ 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'
|
||||||
}
|
}
|
||||||
// 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 grabAt = grabY != null ? (grabY - B.top - rect.t) / rect.h : 0.5
|
||||||
const grab = grabAt < 1 / 3 ? -1 : grabAt > 2 / 3 ? 1 : 0
|
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)
|
const curl = glCurl(rect, frontIm, backIm, spineLeft, paper) || paperCurl(rect, frontIm.src, backIm?.src || null, spineLeft, !!grab)
|
||||||
if (!curl.gl) place(curl.el, rect)
|
// Sin WebGL, la sombra de debajo es un degradado desde el lomo.
|
||||||
box.append(drop, curl.el)
|
let drop = null
|
||||||
extra.push(drop, curl.el)
|
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)
|
;[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
|
// 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.
|
// 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 => {
|
step = g => {
|
||||||
const k = grab ? grab * Math.sin(Math.PI * Math.min(1, Math.max(0, (g - g0) / (g1 - g0 || 1)))) : 0
|
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)
|
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
|
dur = double ? 750 : 610
|
||||||
auto = t => g0 + (g1 - g0) * t
|
auto = t => g0 + (g1 - g0) * t
|
||||||
|
|||||||
Reference in New Issue
Block a user