Hendidura del lomo en las páginas y en la hoja que gira

En doble página va en el centro, en una página en el lado del lomo según el
sentido de lectura y en una página ancha en su centro.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-11-02 16:35:13 -06:00
co-authored by Claude Opus 5.5
parent a6f01b0e54
commit a80391f382
2 changed files with 50 additions and 8 deletions
+37 -8
View File
@@ -1481,6 +1481,12 @@ async function viewReader(mangaId, chapterId, startPage) {
scheduleLeafTextures()
}
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).
const gutterOf = (i, n) => {
if (wide.has(i)) return 'm'
const rtl = settings.dir === 'rtl'
return shown.length === 2 ? (rtl === (n === 0) ? 'l' : 'r') : rtl ? 'r' : 'l'
}
// 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,
// manda la última.
@@ -1494,7 +1500,7 @@ async function viewReader(mangaId, chapterId, startPage) {
const stage = document.createElement('div')
stage.className = 'stage'
stage.classList.toggle('two', shown.length === 2)
stage.innerHTML = shown.map(i => `<img data-i="${i}" src="${pages[i]}" alt="" draggable="false">`).join('')
stage.innerHTML = shown.map((i, n) => `<img data-i="${i}" data-gutter="${gutterOf(i, n)}" src="${pages[i]}" alt="" draggable="false">`).join('')
const imgs = [...stage.querySelectorAll('img')]
if ($('.stage', box) && dir && !wantDrag && !imgs.every(imgReady)) {
waitTimer = setTimeout(() => box.classList.add('waiting'), 200)
@@ -1534,6 +1540,7 @@ async function viewReader(mangaId, chapterId, startPage) {
else { box.replaceChildren(stage) }
stage.querySelectorAll('img').forEach(img => img.addEventListener('load', () => {
const i = Number(img.dataset.i)
if (isWide(img)) img.dataset.gutter = 'm'
if (isWide(img) && !wide.has(i)) {
wide.add(i)
if (shown.length === 2 && shown.includes(i)) goTo(shown[0], false, true) // reagrupar
@@ -1645,19 +1652,28 @@ async function viewReader(mangaId, chapterId, startPage) {
gl.attachShader(prog, shader(gl.FRAGMENT_SHADER, `#version 300 es
precision highp float;
uniform sampler2D uFront, uBack;
uniform bool uPaper;
uniform bool uPaper, uSpineLeft, uSpread;
in vec2 vUV;
in float vC;
out vec4 color;
// La hendidura del lomo: los mismos puntos que data-gutter en style.css, para que no salte al empezar el giro.
float gutter(float d) {
if (d < 0.012) return mix(0.4, 0.6, d / 0.012);
if (d < 0.03) return mix(0.6, 0.8, (d - 0.012) / 0.018);
if (d < 0.06) return mix(0.8, 0.93, (d - 0.03) / 0.03);
if (d < 0.1) return mix(0.93, 1.0, (d - 0.06) / 0.04);
return 1.0;
}
void main() {
vec4 t;
float b;
float fromSpine = uSpineLeft ? vUV.x : 1.0 - vUV.x;
if (gl_FrontFacing) {
t = texture(uFront, vUV, -0.5);
b = 1.0 - (1.0 - vC) * 0.3;
b = (1.0 - (1.0 - vC) * 0.3) * gutter(uSpread ? abs(vUV.x - 0.5) * 2.0 : fromSpine);
} 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;
b = (1.0 - (1.0 + vC) * 0.3) * gutter(fromSpine);
}
color = vec4(t.rgb * b, t.a);
}`))
@@ -1743,7 +1759,7 @@ async function viewReader(mangaId, chapterId, startPage) {
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),
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'), uSpineLeft: gl.getUniformLocation(prog, 'uSpineLeft'), uSpread: gl.getUniformLocation(prog, 'uSpread'), maxSize: gl.getParameter(gl.MAX_TEXTURE_SIZE) }
return leafGL
}
function leafTexture(im) {
@@ -1817,6 +1833,8 @@ async function viewReader(mangaId, chapterId, startPage) {
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.uSpineLeft, spineLeft ? 1 : 0)
gl.uniform1i(L.uSpread, frontIm.dataset.gutter === 'm' ? 1 : 0)
gl.activeTexture(gl.TEXTURE0)
gl.bindTexture(gl.TEXTURE_2D, front)
gl.activeTexture(gl.TEXTURE1)
@@ -2033,10 +2051,15 @@ async function viewReader(mangaId, chapterId, startPage) {
spineLeft = sign > 0
const keep = document.createElement('img')
keep.className = 'leafkeep'
keep.dataset.gutter = stay.dataset.gutter
keep.src = stay.src
place(keep, rel(stay))
box.append(keep)
extra.push(keep)
// Su hendidura es transparente: sin negro detrás, se vería la página nueva y la hendidura se aclararía.
const keepBack = document.createElement('i')
keepBack.className = 'leafkeepback'
place(keepBack, rel(stay))
box.append(keepBack, keep)
extra.push(keepBack, keep)
old.style.visibility = 'hidden' // la hoja y la copia ocupan su lugar
} else {
// Una página: el lomo está a la izquierda en izq→der y a la derecha en der→izq. Hacia delante, la
@@ -2375,7 +2398,13 @@ async function viewReader(mangaId, chapterId, startPage) {
render()
})
$('#fit').addEventListener('change', e => { settings.fit = e.target.value; savePrefs(); render() })
$('#dir').addEventListener('change', e => { settings.dir = e.target.value; box.dataset.dir = settings.dir; syncDirUi(); savePrefs() })
$('#dir').addEventListener('change', e => {
settings.dir = e.target.value
box.dataset.dir = settings.dir
box.querySelectorAll('.stage img').forEach((im, n) => { im.dataset.gutter = gutterOf(Number(im.dataset.i), n) })
syncDirUi()
savePrefs()
})
$('#filter').addEventListener('change', e => { settings.filter = e.target.value; saveGlobal(); applyFilter() })
$('#anim').value = settings.anim
$('#anim').addEventListener('change', e => { settings.anim = e.target.value; saveGlobal() })
+13
View File
@@ -397,6 +397,19 @@ body.reading #netbanner { display: none; }
.paged .curldrop { position: absolute; z-index: 2; pointer-events: none; opacity: 0; }
.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%); }
/* Hendidura del lomo: la página se funde con el negro de detrás. Los puntos coinciden con gutter() en app.js. */
.paged:not(.fit-width) img[data-gutter] {
--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));
-webkit-mask-image: var(--gutter-mask);
mask-image: var(--gutter-mask);
}
.paged:not(.fit-width) img[data-gutter="l"] { --gutter-side: to right; }
.paged:not(.fit-width) img[data-gutter="r"] { --gutter-side: to left; }
.paged:not(.fit-width) img[data-gutter="m"] {
--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 .leafkeep { position: absolute; z-index: 2; pointer-events: none; max-width: none; max-height: none; object-fit: fill; }
/* Info de la obra */