diff --git a/app/public/app.js b/app/public/app.js index 3452bd3..2bfa765 100644 --- a/app/public/app.js +++ b/app/public/app.js @@ -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 => ``).join('') + stage.innerHTML = shown.map((i, n) => ``).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() }) diff --git a/app/public/style.css b/app/public/style.css index 32566ca..d25e6f1 100644 --- a/app/public/style.css +++ b/app/public/style.css @@ -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 */