Modo manga: capítulo siguiente a la izquierda y la hoja se curva según por dónde se agarra
- En der→izq, la barra inferior del lector va como la lectura: Cap. sig. a la izquierda y Cap. ant. a la derecha. - En móvil, la página y la barra de progreso van arriba, y cada botón de capítulo en su extremo. - Al arrastrar con el dedo por arriba o por abajo, esa parte de la hoja va por delante y el lomo no se mueve; por el centro, como siempre. Sin capas nuevas. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
+35
-9
@@ -1407,7 +1407,7 @@ async function viewReader(mangaId, chapterId, startPage) {
|
|||||||
<span id="pnum" class="muted"></span>
|
<span id="pnum" class="muted"></span>
|
||||||
<input type="range" id="slider" min="1" max="${total}" value="${page + 1}">
|
<input type="range" id="slider" min="1" max="${total}" value="${page + 1}">
|
||||||
<button class="btn small" id="zoomreset" hidden title="Quitar zoom">${icon('zoomOut')}</button>
|
<button class="btn small" id="zoomreset" hidden title="Quitar zoom">${icon('zoomOut')}</button>
|
||||||
${next ? `<a class="btn small" href="${readUrl(mangaId, next.id)}">Cap. sig. ${icon('next')}</a>` : ''}
|
${next ? `<a class="btn small chnext" href="${readUrl(mangaId, next.id)}">Cap. sig. ${icon('next')}</a>` : ''}
|
||||||
</div>
|
</div>
|
||||||
</div>`
|
</div>`
|
||||||
const reader = $('.reader', app)
|
const reader = $('.reader', app)
|
||||||
@@ -1542,6 +1542,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.
|
// 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 turning = null // { anims, cleanup } (y move/release si sigue al dedo)
|
||||||
let wantDrag = false
|
let wantDrag = false
|
||||||
|
let grabY = null // altura de la pantalla por la que el dedo 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).
|
// 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)
|
const turnPan = dir => (settings.dir === 'rtl' ? -1 : 1) * dir * (shown.length === 2 ? 0.3 : 0.12)
|
||||||
function finishTurn() {
|
function finishTurn() {
|
||||||
@@ -1609,13 +1610,17 @@ async function viewReader(mangaId, chapterId, startPage) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
// Devuelve cuánto está levantada la hoja (0 a 1), para la sombra de debajo.
|
// Devuelve cuánto está levantada la hoja (0 a 1), para la sombra de debajo.
|
||||||
function set(g, reverse) {
|
// tiltZ/tiltX: curvatura total de la hoja agarrada por arriba o por abajo. Se reparte entre las tiras, de modo
|
||||||
|
// que junto al lomo apenas se nota y el borde libre se lleva todo: la hoja sigue pegada al libro. Cambia de
|
||||||
|
// signo con la cara visible para que la parte agarrada vaya siempre por delante.
|
||||||
|
function set(g, reverse, tiltZ = 0, tiltX = 0) {
|
||||||
let prev = 0, lift = 0
|
let prev = 0, lift = 0
|
||||||
angles(g, reverse).forEach((a, i) => {
|
angles(g, reverse).forEach((a, i) => {
|
||||||
const t = strips[i]
|
const t = strips[i]
|
||||||
t.s.style.transform = `${i ? `translateX(${step}px) ` : ''}rotateY(${(sgn * (a - prev)).toFixed(2)}deg)`
|
|
||||||
prev = a
|
|
||||||
const c = Math.cos((a * Math.PI) / 180)
|
const c = Math.cos((a * Math.PI) / 180)
|
||||||
|
const bend = tiltZ || tiltX ? ` rotateZ(${((tiltZ * c) / N).toFixed(3)}deg) rotateX(${((tiltX * c) / N).toFixed(3)}deg)` : ''
|
||||||
|
t.s.style.transform = `${i ? `translateX(${step}px) ` : ''}rotateY(${(sgn * (a - prev)).toFixed(2)}deg)${bend}`
|
||||||
|
prev = a
|
||||||
// Se oscurece al inclinarse. Con un filtro en la propia cara y no con una capa negra encima: en Safari
|
// Se oscurece al inclinarse. Con un filtro en la propia cara y no con una capa negra encima: en Safari
|
||||||
// cada capa se compone aparte y en las uniones entre tiras asomaban líneas.
|
// cada capa se compone aparte y en las uniones entre tiras asomaban líneas.
|
||||||
t.f.style.filter = `brightness(${(1 - (1 - c) * 0.3).toFixed(3)})`
|
t.f.style.filter = `brightness(${(1 - (1 - c) * 0.3).toFixed(3)})`
|
||||||
@@ -1645,8 +1650,11 @@ async function viewReader(mangaId, chapterId, startPage) {
|
|||||||
}
|
}
|
||||||
return (lo + hi) / 2
|
return (lo + hi) / 2
|
||||||
}
|
}
|
||||||
return { el, set, span, solve }
|
// 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 }
|
||||||
}
|
}
|
||||||
|
const GRAB_TILT_Z = 8, GRAB_TILT_X = 6 // curvatura total, en grados, de la hoja agarrada por arriba o por abajo
|
||||||
// drag: la página sigue al dedo; devuelve el control (move/release) en vez de animarse sola.
|
// drag: la página sigue al dedo; devuelve el control (move/release) en vez de animarse sola.
|
||||||
function turnPage(old, stage, dir, drag = false) {
|
function turnPage(old, stage, dir, drag = false) {
|
||||||
// "Delante" es hacia la izquierda en izq→der (la hoja derecha pasa a la izquierda) y al revés en der→izq.
|
// "Delante" es hacia la izquierda en izq→der (la hoja derecha pasa a la izquierda) y al revés en der→izq.
|
||||||
@@ -1728,7 +1736,17 @@ async function viewReader(mangaId, chapterId, startPage) {
|
|||||||
box.append(drop, curl.el)
|
box.append(drop, curl.el)
|
||||||
extra.push(drop, curl.el)
|
extra.push(drop, curl.el)
|
||||||
;[g0, g1] = double ? [0, 1] : curl.span(reverse)
|
;[g0, g1] = double ? [0, 1] : curl.span(reverse)
|
||||||
step = g => { drop.style.opacity = (curl.set(g, reverse) * 0.8).toFixed(3) }
|
// 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.
|
||||||
|
const grabAt = drag && grabY != null ? (grabY - B.top - rect.t) / rect.h : 0.5
|
||||||
|
const grab = grabAt < 1 / 3 ? -1 : grabAt > 2 / 3 ? 1 : 0
|
||||||
|
if (grab) curl.pivot(grab < 0 ? 'bottom' : 'top')
|
||||||
|
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)
|
||||||
|
}
|
||||||
dur = double ? 700 : 560
|
dur = double ? 700 : 560
|
||||||
auto = t => g0 + (g1 - g0) * t
|
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);
|
// El borde libre va donde lo lleva el dedo: de su sitio al lomo (una página) o al otro lado (doble);
|
||||||
@@ -1803,6 +1821,13 @@ async function viewReader(mangaId, chapterId, startPage) {
|
|||||||
if (page > 0) goTo(groupOf(page - 1)[0], false, false, -1)
|
if (page > 0) goTo(groupOf(page - 1)[0], false, false, -1)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// En der → izq, la barra de abajo va como la lectura: el capítulo siguiente a la izquierda.
|
||||||
|
function syncDirUi() {
|
||||||
|
const rtl = isPaged() && settings.dir === 'rtl'
|
||||||
|
slider.dir = rtl ? 'rtl' : 'ltr'
|
||||||
|
$('.rbar.bottom').classList.toggle('rtl', rtl)
|
||||||
|
}
|
||||||
|
|
||||||
function render() {
|
function render() {
|
||||||
observer?.disconnect()
|
observer?.disconnect()
|
||||||
const keep = lastShown()
|
const keep = lastShown()
|
||||||
@@ -1811,7 +1836,7 @@ async function viewReader(mangaId, chapterId, startPage) {
|
|||||||
renderModeQuick()
|
renderModeQuick()
|
||||||
box.dataset.dir = settings.dir
|
box.dataset.dir = settings.dir
|
||||||
// En der → izq la barra de progreso también avanza de derecha a izquierda (en vertical no aplica).
|
// En der → izq la barra de progreso también avanza de derecha a izquierda (en vertical no aplica).
|
||||||
slider.dir = isPaged() && settings.dir === 'rtl' ? 'rtl' : 'ltr'
|
syncDirUi()
|
||||||
$('#dir').hidden = !isPaged()
|
$('#dir').hidden = !isPaged()
|
||||||
if (isPaged()) {
|
if (isPaged()) {
|
||||||
box.innerHTML = ''
|
box.innerHTML = ''
|
||||||
@@ -1933,7 +1958,8 @@ async function viewReader(mangaId, chapterId, startPage) {
|
|||||||
const prev = [...shown]
|
const prev = [...shown]
|
||||||
finishTurn()
|
finishTurn()
|
||||||
wantDrag = true
|
wantDrag = true
|
||||||
try { fwd ? forward() : backward() } finally { wantDrag = false }
|
grabY = tap.y
|
||||||
|
try { fwd ? forward() : backward() } finally { wantDrag = false; grabY = null }
|
||||||
if (turning?.move) {
|
if (turning?.move) {
|
||||||
gesture = { type: 'turn', ctl: turning, prev, dir: fwd ? 1 : -1, x0: tap.x, k: dx < 0 ? -1 : 1, d: Math.abs(dx), v: 0, t: e.timeStamp }
|
gesture = { type: 'turn', ctl: turning, prev, dir: fwd ? 1 : -1, x0: tap.x, k: dx < 0 ? -1 : 1, d: Math.abs(dx), v: 0, t: e.timeStamp }
|
||||||
turning.move(gesture.d / turning.range)
|
turning.move(gesture.d / turning.range)
|
||||||
@@ -2020,7 +2046,7 @@ async function viewReader(mangaId, chapterId, startPage) {
|
|||||||
render()
|
render()
|
||||||
})
|
})
|
||||||
$('#fit').addEventListener('change', e => { settings.fit = e.target.value; savePrefs(); 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; slider.dir = settings.dir; savePrefs() })
|
$('#dir').addEventListener('change', e => { settings.dir = e.target.value; box.dataset.dir = settings.dir; syncDirUi(); savePrefs() })
|
||||||
$('#filter').addEventListener('change', e => { settings.filter = e.target.value; saveGlobal(); applyFilter() })
|
$('#filter').addEventListener('change', e => { settings.filter = e.target.value; saveGlobal(); applyFilter() })
|
||||||
$('#anim').value = settings.anim
|
$('#anim').value = settings.anim
|
||||||
$('#anim').addEventListener('change', e => { settings.anim = e.target.value; saveGlobal() })
|
$('#anim').addEventListener('change', e => { settings.anim = e.target.value; saveGlobal() })
|
||||||
|
|||||||
@@ -98,6 +98,16 @@ input:focus, select:focus { border-color: var(--accent); }
|
|||||||
background: rgba(10, 9, 14, .9); backdrop-filter: blur(6px); transition: transform .2s; flex-wrap: wrap; }
|
background: rgba(10, 9, 14, .9); backdrop-filter: blur(6px); transition: transform .2s; flex-wrap: wrap; }
|
||||||
.rbar.top { top: 0; padding-top: max(8px, env(safe-area-inset-top)); }
|
.rbar.top { top: 0; padding-top: max(8px, env(safe-area-inset-top)); }
|
||||||
.rbar.bottom { bottom: 0; justify-content: center; padding-bottom: max(8px, env(safe-area-inset-bottom)); }
|
.rbar.bottom { bottom: 0; justify-content: center; padding-bottom: max(8px, env(safe-area-inset-bottom)); }
|
||||||
|
.rbar.bottom.rtl, .rbar.bottom.rtl a.btn { flex-direction: row-reverse; }
|
||||||
|
/* Móvil: arriba la página y la barra de progreso; abajo cada botón en su extremo. */
|
||||||
|
@media (max-width: 640px) {
|
||||||
|
.rbar.bottom { justify-content: space-between; }
|
||||||
|
.rbar.bottom #pnum { order: -2; }
|
||||||
|
.rbar.bottom input[type=range] { order: -1; flex: 1 1 calc(100% - 80px); max-width: none; }
|
||||||
|
.rbar.bottom .chnext { margin-left: auto; }
|
||||||
|
.rbar.bottom.rtl .chnext { margin-left: 0; margin-right: auto; }
|
||||||
|
}
|
||||||
|
.rbar.bottom.rtl a.btn .ic, .paged[data-dir="rtl"] .chapter-end a.primary .ic { transform: scaleX(-1); }
|
||||||
.reader.hide-ui .rbar.top { transform: translateY(-110%); }
|
.reader.hide-ui .rbar.top { transform: translateY(-110%); }
|
||||||
.reader.hide-ui .rbar.bottom { transform: translateY(110%); }
|
.reader.hide-ui .rbar.bottom { transform: translateY(110%); }
|
||||||
.rbar .ttl { flex: 1; min-width: 0; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; font-size: .9rem; }
|
.rbar .ttl { flex: 1; min-width: 0; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; font-size: .9rem; }
|
||||||
|
|||||||
Reference in New Issue
Block a user