Estante 3D: entradas, orden A–Z, sin conexión, pósit y giroscopio calibrado
- Estante: entradas desde el menú, la librería y la ficha; orden reciente o A–Z; foto del estante, arte de lomos y portadas guardados en el dispositivo; sin conexión muestra solo lo guardado. - Notas de 40 letras sin cortar palabras, lomo curvo, sombra en las portadas de frente y lomos con logo sin el título. - Pósit cuadrado con el capítulo y la página por la que va el usuario; paso al lector sin pantalla de carga. - Giroscopio: integra rotationRate (sin los saltos de β/γ con el dispositivo vertical), tope suave, menos sensibilidad al caminar, lado en horizontal según la gravedad, suavizado por tiempo y más rango en el estante y el libro. - Laboratorio del giroscopio en Ajustes (temporal, solo desarrollo) para grabar los sensores. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
+157
-57
@@ -117,6 +117,7 @@ uniform float uHas, uPitch, uDepth, uDim, uAlpha;
|
||||
uniform vec4 uSpot; // mesa: centro del tomo y radio de la luz
|
||||
uniform vec2 uTab; // mesa propia: escala de la foto; sin ella, madera repetida
|
||||
uniform vec2 uSize;
|
||||
uniform vec2 uFace; // portada de frente: ancho y alto, para la sombra que le hace el hueco del estante (0: sin sombra)
|
||||
uniform vec4 uHide; // x0, x1, y0, y1 del hueco del tomo sacado: ni él ni sus costados se pintan en la fila
|
||||
in vec3 vPos; in vec3 vNor; in vec2 vUV;
|
||||
out vec4 color;
|
||||
@@ -131,7 +132,16 @@ void main() {
|
||||
vec3 c;
|
||||
if (uKind == 0) c = texture(uTex, vUV, -0.4).rgb * vec3(0.92, 0.84, 0.76); // madera del mueble, algo más oscura que la mesa
|
||||
else if (uKind == 1) c = texture(uTex, vUV).rgb; // lomos
|
||||
else if (uKind == 2) c = uHas > 0.5 ? texture(uTex, vUV, -0.5).rgb : uColor; // portada
|
||||
else if (uKind == 2) { // portada
|
||||
c = uHas > 0.5 ? texture(uTex, vUV, -0.5).rgb : uColor;
|
||||
if (uFace.x > 0.0) {
|
||||
// Metida en el estante: la luz llega de arriba a la izquierda, así que el lomo o el costado de la izquierda le hace
|
||||
// sombra, y la balda de arriba y el fondo la oscurecen hacia los bordes.
|
||||
float dl = vUV.x * uFace.x, dr = (1.0 - vUV.x) * uFace.x, dt = vUV.y * uFace.y, db = (1.0 - vUV.y) * uFace.y;
|
||||
c *= mix(0.28, 1.0, smoothstep(0.0, 0.13, dl)) * mix(0.6, 1.0, smoothstep(0.0, 0.06, dr))
|
||||
* mix(0.45, 1.0, smoothstep(0.0, 0.36, dt)) * mix(0.66, 1.0, smoothstep(0.0, 0.04, db)) * 0.88;
|
||||
}
|
||||
}
|
||||
else if (uKind == 3 || uKind == 5) { // las hojas, vistas por arriba: rayas irregulares a lo ancho del tomo, medidas en el mueble
|
||||
float a = uKind == 3 ? vUV.x : vUV.y, v = a / 0.0042, w = fwidth(v), k = floor(v);
|
||||
float n = fract(sin(k * 12.9898) * 43758.5453);
|
||||
@@ -156,6 +166,19 @@ void main() {
|
||||
color = vec4(c * lit * shade * room * uDim * vec3(1.03, 1.0, 0.95), uAlpha);
|
||||
}`
|
||||
|
||||
// Lomo curvo: a lo ancho, el centro sobresale y los bordes quedan SPINE_ROUND del ancho más atrás.
|
||||
// a0 y len: dónde empieza y cuánto mide a lo ancho; top y H: arriba y alto; front: lo más saliente.
|
||||
// place(a lo ancho, salida) da x y z del punto; turn pasa la normal (a lo ancho, 0, salida) a esos mismos ejes.
|
||||
const SPINE_ROUND = 0.16, SPINE_COLS = 8
|
||||
function roundSpine(sh, a0, len, top, H, front, R, place, [U0, U1], turn = n => n) {
|
||||
const w = Math.abs(len)
|
||||
sh.grid(SPINE_COLS, 1, (c, r) => {
|
||||
const t = c / SPINE_COLS, out = front - R * (1 - Math.sin(Math.PI * t)), slope = ((R * Math.PI) / w) * Math.cos(Math.PI * t)
|
||||
const k = Math.hypot(slope, 1), n = turn([-slope / k * Math.sign(len), 0, 1 / k])
|
||||
const [px, , pz] = place(a0 + len * t, out)
|
||||
return [px, top - H * r, pz, ...n, U0 + (U1 - U0) * t, r]
|
||||
})
|
||||
}
|
||||
// Un cuadrilátero: origen, dos lados, normal y coordenadas de textura en sus esquinas.
|
||||
function quad(sh, o, a, b, n, uv = [0, 0, 1, 1]) {
|
||||
sh.grid(1, 1, (c, r) => [o[0] + a[0] * c + b[0] * r, o[1] + a[1] * c + b[1] * r, o[2] + a[2] * c + b[2] * r, ...n, uv[0] + (uv[2] - uv[0]) * c, uv[1] + (uv[3] - uv[1]) * r])
|
||||
@@ -164,6 +187,15 @@ const hash = s => { let h = 7; for (const ch of String(s)) h = (h * 31 + ch.char
|
||||
const hsl = (h, s, l) => { const a = s * Math.min(l, 1 - l), f = n => { const k = (n + h / 30) % 12; return l - a * Math.max(-1, Math.min(k - 3, 9 - k, 1)) }; return [f(0), f(8), f(4)] }
|
||||
|
||||
const STORE = 'shelfPalette'
|
||||
// Lo que cuesta sacar se guarda en el dispositivo (Cache Storage, se conserva al actualizar la app): el arte recortado
|
||||
// de cada lomo, y las portadas de frente y los logos tal cual. La siguiente visita no espera a la red, y sin
|
||||
// conexión el mueble se ve igual.
|
||||
const KEEP = 'shelf-v1'
|
||||
let keepBox = null
|
||||
const keeper = () => (keepBox ||= typeof caches !== 'undefined' && globalThis.isSecureContext !== false ? caches.open(KEEP).catch(() => null) : Promise.resolve(null))
|
||||
const keptKey = (kind, url) => `/__shelf/${kind}?u=${encodeURIComponent(url)}`
|
||||
export const keptBlob = (kind, url) => keeper().then(c => c?.match(keptKey(kind, url))).then(r => (r ? r.blob() : null)).catch(() => null)
|
||||
export const keepBlob = (kind, url, blob) => keeper().then(c => c?.put(keptKey(kind, url), new Response(blob, { headers: { 'content-type': blob.type || 'image/jpeg' } }))).catch(() => {})
|
||||
const STRIP_PER_UNIT = 440, STRIP_MAX = 2048, STRIP_H = 512, IMG_AT_ONCE = 4 // ancho de la tira según el de la fila; más de 2048 no se nota y gasta memoria
|
||||
const blurBox = typeof document === 'undefined' ? null : document.createElement('canvas') // de paso, para desenfocar bajo las letras
|
||||
const ART_SKIP = 0.26, FACE_INPUT = 360 // sin rostro se salta lo alto de la portada; ancho con que se buscan los rostros
|
||||
@@ -194,10 +226,10 @@ export function loadNoteFonts() {
|
||||
const IDENTITY = new Float32Array([1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1])
|
||||
const FLY_MS = 900
|
||||
const PEEK_OPEN_MS = 720, PEEK_CLOSE_MS = 520, PEEK_SLIDE = 0.34 // el tomo sale recto de la fila y luego gira hacia la cámara
|
||||
const ZOOM_MAX = 3, ORBIT_TILT = 10, ORBIT_YAW = 18, ORBIT_BACK = 0.08, REST_TILT = -3, FRICTION_MS = 320
|
||||
const ZOOM_MAX = 3, ORBIT_TILT = 14, ORBIT_YAW = 26, ORBIT_BACK = 0.11, REST_TILT = -3, FRICTION_MS = 320
|
||||
|
||||
export function createShelf3D(container, lib, { works: firstWorks = [], rowsLandscape = 2.3, rowsPortrait = 3, still = false, pagesOf = null, onPeek = null, onOpen = null, table = null } = {}) {
|
||||
const { Shape, woodCanvas, cameraMatrix, project, FOV, coverPalette, inkOn, css, createTilt, MAX_DEG } = lib
|
||||
const { Shape, woodCanvas, cameraMatrix, project, FOV, coverPalette, inkOn, css, createTilt, followTilt } = lib
|
||||
const canvas = document.createElement('canvas')
|
||||
canvas.className = 'shelfcanvas'
|
||||
const gl = canvas.getContext('webgl2', { alpha: false, antialias: true, depth: true, powerPreference: 'high-performance' })
|
||||
@@ -209,6 +241,7 @@ export function createShelf3D(container, lib, { works: firstWorks = [], rowsLand
|
||||
let rows = [], rowWidth = 3, size = { w: 1, h: 1 }, visibleH = 2.8, top = 0, bottom = 0, y = 0, layoutTimer = 0, rowsKey = ''
|
||||
const built = new Map(), covers = new Map(), loadingImg = new Map(), waiting = []
|
||||
const arts = new Map(), panels = new Map(), askedPanel = new Set() // arte de los lomos, y página a color de los tomos sin portada
|
||||
let toTopNext = false
|
||||
let x = 0, zoom = 1, velocity = 0, lastMove = 0, focusId = null // focusId: obra que se mantiene a la vista hasta que se toque el mueble
|
||||
const tiltNow = { x: 0, y: 0 }
|
||||
const tilt = createTilt(() => draw())
|
||||
@@ -257,7 +290,7 @@ export function createShelf3D(container, lib, { works: firstWorks = [], rowsLand
|
||||
if (!gl.getProgramParameter(prog, gl.LINK_STATUS)) throw new Error(gl.getProgramInfoLog(prog) || 'shader')
|
||||
gl.useProgram(prog)
|
||||
u = {}
|
||||
for (const n of ['uM', 'uModel', 'uKind', 'uTex', 'uColor', 'uHas', 'uPitch', 'uDepth', 'uSize', 'uDim', 'uHide', 'uAlpha', 'uSpot', 'uTab']) u[n] = gl.getUniformLocation(prog, n)
|
||||
for (const n of ['uM', 'uModel', 'uKind', 'uTex', 'uColor', 'uHas', 'uPitch', 'uDepth', 'uSize', 'uDim', 'uHide', 'uAlpha', 'uSpot', 'uTab', 'uFace']) u[n] = gl.getUniformLocation(prog, n)
|
||||
gl.uniform1i(u.uTex, 0)
|
||||
gl.uniformMatrix4fv(u.uModel, false, IDENTITY)
|
||||
gl.uniform1f(u.uDim, 1)
|
||||
@@ -333,11 +366,37 @@ export function createShelf3D(container, lib, { works: firstWorks = [], rowsLand
|
||||
}
|
||||
const coverKey = item => urlOf(item) || `plain:${item.work.id}`
|
||||
const plainFor = key => item => { if (!covers.has(key)) { covers.set(key, texFrom(plainCover(item))); draw() } }
|
||||
function loadImage(url, done) {
|
||||
// keep: la imagen se guarda en el dispositivo (portadas de frente y logos) y de ahí sale la próxima vez.
|
||||
const keepUrls = new Set()
|
||||
function loadImage(url, done, keep = false) {
|
||||
if (loadingImg.has(url)) { loadingImg.get(url).push(done); return }
|
||||
loadingImg.set(url, [done])
|
||||
waiting.push(url)
|
||||
pumpImages()
|
||||
if (!keep || !url.startsWith('/')) { waiting.push(url); return pumpImages() }
|
||||
keepUrls.add(url)
|
||||
// Lo guardado en el dispositivo no espera a la red: no ocupa sitio en la cola.
|
||||
keptBlob('img', url).then(hit => {
|
||||
if (dead) return
|
||||
if (!hit) { waiting.push(url); return pumpImages() }
|
||||
const im = new Image(), src = URL.createObjectURL(hit)
|
||||
im.decoding = 'async'
|
||||
const finish = ok => {
|
||||
URL.revokeObjectURL(src)
|
||||
if (!ok) { waiting.push(url); return pumpImages() } // lo guardado no sirve: de la red
|
||||
const list = loadingImg.get(url) || []
|
||||
loadingImg.delete(url)
|
||||
for (const f of list) f(im)
|
||||
}
|
||||
im.onload = () => finish(true)
|
||||
im.onerror = () => finish(false)
|
||||
im.src = src
|
||||
})
|
||||
}
|
||||
async function keptSrc(url) { // ya se miró lo guardado: de la red, y se guarda
|
||||
const r = await fetch(url, { credentials: 'same-origin' }).catch(() => null)
|
||||
if (!r?.ok || !/^image\//.test(r.headers.get('content-type') || '')) return null
|
||||
const blob = await r.blob()
|
||||
keepBlob('img', url, blob)
|
||||
return URL.createObjectURL(blob)
|
||||
}
|
||||
function pumpImages() {
|
||||
while (!dead && imgBusy < IMG_AT_ONCE && waiting.length) {
|
||||
@@ -355,9 +414,20 @@ export function createShelf3D(container, lib, { works: firstWorks = [], rowsLand
|
||||
if (!dead) for (const f of list) f(ok ? im : null)
|
||||
free()
|
||||
}
|
||||
im.onload = () => finish(true)
|
||||
im.onerror = () => finish(false)
|
||||
im.src = url
|
||||
if (!keepUrls.has(url)) {
|
||||
im.onload = () => finish(true)
|
||||
im.onerror = () => finish(false)
|
||||
im.src = url
|
||||
continue
|
||||
}
|
||||
keptSrc(url).catch(() => null).then(src => {
|
||||
im.onload = () => { if (src) URL.revokeObjectURL(src); finish(true) }
|
||||
im.onerror = () => {
|
||||
if (src) { URL.revokeObjectURL(src); src = null; im.src = url; return } // lo bajado no se pudo leer: tal cual
|
||||
finish(false)
|
||||
}
|
||||
im.src = src || url
|
||||
})
|
||||
}
|
||||
}
|
||||
// Alto y fondo de cada obra, por la proporción de su portada. Lo aprendido rehace el mueble una vez; luego se recuerda.
|
||||
@@ -456,10 +526,37 @@ export function createShelf3D(container, lib, { works: firstWorks = [], rowsLand
|
||||
const at = bestColumn(g.getImageData(0, 0, small.width, small.height).data, small.width, small.height, Math.min(cw, small.width), from)
|
||||
out.getContext('2d').drawImage(im, (at / small.width) * w, h * ART_SKIP, h * ART_RATIO * keep, h * keep, 0, 0, out.width, out.height)
|
||||
}
|
||||
arts.set(url, out)
|
||||
if (arts.size > ART_KEEP) arts.delete(arts.keys().next().value) // se vuelve a sacar de la caché del navegador si hace falta
|
||||
putArt(url, out)
|
||||
out.toBlob?.(b => { if (b) keepBlob('art', url, b) }, 'image/jpeg', 0.88)
|
||||
} catch {}
|
||||
}
|
||||
function putArt(url, art) {
|
||||
arts.delete(url)
|
||||
arts.set(url, art)
|
||||
if (arts.size > ART_KEEP) { const old = arts.keys().next().value; arts.get(old)?.close?.(); arts.delete(old) } // se vuelve a sacar del dispositivo si hace falta
|
||||
}
|
||||
// El arte ya recortado en otra visita. Solo vale si también se recuerda su color (localStorage); si no, se rehace.
|
||||
const artAsked = new Set()
|
||||
function keptArt(url, done) {
|
||||
if (artAsked.has(url)) return
|
||||
artAsked.add(url)
|
||||
const miss = () => { artAsked.delete(url); done(false) }
|
||||
if (!palettes[url]) return miss()
|
||||
keptBlob('art', url).then(async b => {
|
||||
if (dead) return
|
||||
if (!b) return miss()
|
||||
let art = null
|
||||
try { art = typeof createImageBitmap === 'function' ? await createImageBitmap(b) : null } catch {}
|
||||
if (!art) {
|
||||
art = await new Promise(res => { const im = new Image(), src = URL.createObjectURL(b); im.onload = () => { URL.revokeObjectURL(src); res(im) }; im.onerror = () => { URL.revokeObjectURL(src); res(null) }; im.src = src })
|
||||
}
|
||||
artAsked.delete(url)
|
||||
if (dead) return
|
||||
if (!art) return done(false)
|
||||
putArt(url, art)
|
||||
done(true)
|
||||
})
|
||||
}
|
||||
// Tomo sin portada: la primera de sus páginas que sea a color. Lo encontrado se recuerda; lo no encontrado, también, para no volver a descargar páginas.
|
||||
function findPanel(item, done) {
|
||||
const key = panelKey(item)
|
||||
@@ -504,7 +601,7 @@ export function createShelf3D(container, lib, { works: firstWorks = [], rowsLand
|
||||
if (!im || dead || im.naturalWidth < im.naturalHeight * 1.2) return
|
||||
logos.set(id, { im, light: logoLight(im) })
|
||||
for (const b of built.values()) if ((rows[b.k] || []).some(i => i.work.id === id)) b.repaint?.()
|
||||
})
|
||||
}, true)
|
||||
}
|
||||
|
||||
// ----- Notas: un pósit por colección, pegado al canto de la balda de arriba -----
|
||||
@@ -657,24 +754,26 @@ export function createShelf3D(container, lib, { works: firstWorks = [], rowsLand
|
||||
g.drawImage(logo.im, -len / 2, -thick / 2, len, thick)
|
||||
g.restore()
|
||||
}
|
||||
let size = wText * (art ? 0.44 : 0.5)
|
||||
g.font = `700 ${Math.round(size)}px system-ui, sans-serif`
|
||||
let long = g.measureText(it.work.title).width
|
||||
if (long > room) { size = Math.max(wText * 0.32, (size * room) / long); g.font = `700 ${Math.round(size)}px system-ui, sans-serif`; long = g.measureText(it.work.title).width }
|
||||
const len = Math.min(long, room), mid = art ? to - len / STRIP_H / 2 - 0.012 : (from + to) / 2
|
||||
if (backing && !logo) {
|
||||
const bw = size * 1.45 * sx, pad = size * 0.55
|
||||
ink = backing((w - bw) / 2, STRIP_H * mid - len / 2 - pad, bw, len + pad * 2)
|
||||
// Con logo, el título ya está: solo queda el número del tomo.
|
||||
if (!logo) {
|
||||
let size = wText * (art ? 0.44 : 0.5)
|
||||
g.font = `700 ${Math.round(size)}px system-ui, sans-serif`
|
||||
let long = g.measureText(it.work.title).width
|
||||
if (long > room) { size = Math.max(wText * 0.32, (size * room) / long); g.font = `700 ${Math.round(size)}px system-ui, sans-serif`; long = g.measureText(it.work.title).width }
|
||||
const len = Math.min(long, room), mid = art ? to - len / STRIP_H / 2 - 0.012 : (from + to) / 2
|
||||
if (backing) {
|
||||
const bw = size * 1.45 * sx, pad = size * 0.55
|
||||
ink = backing((w - bw) / 2, STRIP_H * mid - len / 2 - pad, bw, len + pad * 2)
|
||||
}
|
||||
g.save()
|
||||
g.scale(sx, 1)
|
||||
g.fillStyle = css(ink)
|
||||
if (backing) { g.shadowColor = ink[0] > 0.5 ? 'rgba(0,0,0,.55)' : 'rgba(255,255,255,.5)'; g.shadowBlur = 3 }
|
||||
g.translate(wText / 2, STRIP_H * mid)
|
||||
g.rotate(Math.PI / 2)
|
||||
g.fillText(it.work.title, 0, 0, room)
|
||||
g.restore()
|
||||
}
|
||||
g.save()
|
||||
g.scale(sx, 1)
|
||||
if (logo) g.globalAlpha = 0 // ya está el logo
|
||||
g.fillStyle = css(ink)
|
||||
if (backing) { g.shadowColor = ink[0] > 0.5 ? 'rgba(0,0,0,.55)' : 'rgba(255,255,255,.5)'; g.shadowBlur = 3 }
|
||||
g.translate(wText / 2, STRIP_H * mid)
|
||||
g.rotate(Math.PI / 2)
|
||||
g.fillText(it.work.title, 0, 0, room)
|
||||
g.restore()
|
||||
if (it.vol.vol != null) {
|
||||
const nSize = wText * 0.5, label = String(it.vol.vol), cy = STRIP_H * 0.935
|
||||
g.font = `800 ${Math.round(nSize)}px system-ui, sans-serif`
|
||||
@@ -712,12 +811,13 @@ export function createShelf3D(container, lib, { works: firstWorks = [], rowsLand
|
||||
faces.push({ it, mesh: makeMesh(sh), edge: makeMesh(edge), pages: makeMesh(pages) })
|
||||
continue
|
||||
}
|
||||
quad(spines, [x0, base + H, F], [it.w, 0, 0], [0, -H, 0], [0, 0, 1], [U0, 0, U1, 1])
|
||||
const R = it.w * SPINE_ROUND, Fe = F - R // los bordes del lomo, algo más atrás que su centro
|
||||
roundSpine(spines, x0, it.w, base + H, H, F, R, (along, out) => [along, 0, out], [U0, U1])
|
||||
// Los costados son las tapas: un solo color del lomo, no una columna de la tira estirada a lo largo del fondo.
|
||||
const Um = (U0 + U1) / 2, side = [Um, 0.02, Um, 0.02]
|
||||
quad(spines, [x0, base, F - D], [0, 0, D], [0, H, 0], [-1, 0, 0], side)
|
||||
quad(spines, [x1, base, F], [0, 0, -D], [0, H, 0], [1, 0, 0], side)
|
||||
quad(paper, [x0, base + H, F - D], [it.w, 0, 0], [0, 0, D], [0, 1, 0], [0, it.w, it.w, it.w]) // u: a lo ancho, en el mueble; v: el grosor
|
||||
quad(spines, [x0, base, F - D], [0, 0, D - R], [0, H, 0], [-1, 0, 0], side)
|
||||
quad(spines, [x1, base, Fe], [0, 0, -(D - R)], [0, H, 0], [1, 0, 0], side)
|
||||
quad(paper, [x0, base + H, F - D], [it.w, 0, 0], [0, 0, D - R], [0, 1, 0], [0, it.w, it.w, it.w]) // u: a lo ancho, en el mueble; v: el grosor
|
||||
}
|
||||
paintStrip(row, strip)
|
||||
const b = { k, key: rowKey(row), strip, tex: texFrom(strip), spines: spines.i.length ? makeMesh(spines) : null, paper: paper.i.length ? makeMesh(paper) : null, faces, stale: 0 }
|
||||
@@ -744,7 +844,7 @@ export function createShelf3D(container, lib, { works: firstWorks = [], rowsLand
|
||||
b.repaint = repaint
|
||||
if (!it.face && !it.vol.art && !it.vol.thumb) findPanel(it, () => {
|
||||
const panel = artOf(it)
|
||||
if (panel && !arts.has(panel)) loadImage(panel, im => { if (im) { learnPalette(panel, im); learnArt(panel, im, true); repaint() } })
|
||||
if (panel && !arts.has(panel)) keptArt(panel, ok => ok ? repaint() : loadImage(panel, im => { if (im) { learnPalette(panel, im); learnArt(panel, im, true); repaint() } }))
|
||||
else repaint()
|
||||
})
|
||||
if (!url) continue
|
||||
@@ -756,15 +856,21 @@ export function createShelf3D(container, lib, { works: firstWorks = [], rowsLand
|
||||
learnArt(url, im)
|
||||
covers.set(url, texFrom(im))
|
||||
draw()
|
||||
})
|
||||
}, true)
|
||||
} else if (arts.has(url)) {
|
||||
if (palettes[`face:${url}`] === undefined) askFace(url) // su arte salió de la portada de frente, donde no se buscan rostros
|
||||
} else loadImage(url, im => {
|
||||
if (!im) return
|
||||
learnSize(it, url, im)
|
||||
learnPalette(url, im)
|
||||
learnArt(url, im, true)
|
||||
repaint()
|
||||
} else keptArt(url, ok => {
|
||||
if (ok) {
|
||||
if (palettes[`face:${url}`] === undefined) askFace(url)
|
||||
return repaint()
|
||||
}
|
||||
loadImage(url, im => {
|
||||
if (!im) return
|
||||
learnSize(it, url, im)
|
||||
learnPalette(url, im)
|
||||
learnArt(url, im, true)
|
||||
repaint()
|
||||
})
|
||||
})
|
||||
}
|
||||
return b
|
||||
@@ -821,7 +927,8 @@ export function createShelf3D(container, lib, { works: firstWorks = [], rowsLand
|
||||
} else for (const b of [...built.values()]) if (b.key !== rowKey(rows[b.k])) freeRow(b)
|
||||
const was = top
|
||||
limits()
|
||||
y = y === was || !y ? top : y
|
||||
y = y === was || !y || toTopNext ? top : y
|
||||
toTopNext = false
|
||||
if (focusId) { const k = rows.findIndex(r => r.some(i => i.work.id === focusId)); if (k >= 0) y = rowBase(k) + SHELF.PITCH * 0.42 }
|
||||
clampView()
|
||||
}
|
||||
@@ -841,18 +948,7 @@ export function createShelf3D(container, lib, { works: firstWorks = [], rowsLand
|
||||
if (Math.abs(velocity) < 0.00004) velocity = 0
|
||||
return !!velocity
|
||||
}
|
||||
function stepTilt(now) {
|
||||
tilt.fade(now)
|
||||
const on = !still && zoom === 1
|
||||
const want = on ? { x: tilt.target.x / MAX_DEG, y: tilt.target.y / MAX_DEG } : { x: 0, y: 0 }
|
||||
let moving = !!(tilt.target.x || tilt.target.y) && on
|
||||
for (const k of ['x', 'y']) {
|
||||
const d = want[k] - tiltNow[k]
|
||||
if (Math.abs(d) < 0.002) tiltNow[k] = want[k]
|
||||
else { tiltNow[k] += d * 0.25; moving = true }
|
||||
}
|
||||
return moving
|
||||
}
|
||||
const stepTilt = now => followTilt(tilt, tiltNow, now, !still && zoom === 1)
|
||||
function paint() {
|
||||
raf = 0
|
||||
if (dead) return
|
||||
@@ -903,8 +999,10 @@ export function createShelf3D(container, lib, { works: firstWorks = [], rowsLand
|
||||
gl.uniform1i(u.uKind, 2)
|
||||
gl.uniform1f(u.uHas, tex ? 1 : 0)
|
||||
gl.uniform3fv(u.uColor, base)
|
||||
gl.uniform2f(u.uFace, f.it.w, f.it.h)
|
||||
if (tex) gl.bindTexture(gl.TEXTURE_2D, tex)
|
||||
drawMesh(f.mesh)
|
||||
gl.uniform2f(u.uFace, 0, 0)
|
||||
gl.uniform1i(u.uKind, 4)
|
||||
drawMesh(f.edge)
|
||||
gl.uniform1i(u.uKind, 5)
|
||||
@@ -937,7 +1035,8 @@ export function createShelf3D(container, lib, { works: firstWorks = [], rowsLand
|
||||
function peekBox(W, H, T, U0, U1) {
|
||||
const cover = new Shape(), spine = new Shape(), back = new Shape(), paper = new Shape(), X = W / 2, Y = H / 2, Z = T / 2
|
||||
quad(cover, [-X, Y, Z], [W, 0, 0], [0, -H, 0], [0, 0, 1])
|
||||
quad(spine, [X, Y, Z], [0, 0, -T], [0, -H, 0], [1, 0, 0], [U0, 0, U1, 1]) // en la fila, con el giro, coincide con su trozo de la tira
|
||||
// En la fila, con el giro, coincide con su trozo de la tira, también en la curva.
|
||||
roundSpine(spine, Z, -T, Y, H, X, T * SPINE_ROUND, (along, out) => [out, 0, along], [U0, U1], n => [n[2], 0, n[0]])
|
||||
quad(back, [X, Y, -Z], [-W, 0, 0], [0, -H, 0], [0, 0, -1])
|
||||
quad(paper, [-X, Y, -Z], [W, 0, 0], [0, 0, T], [0, 1, 0], [0, 0, W, T])
|
||||
quad(paper, [-X, -Y, -Z], [W, 0, 0], [0, 0, T], [0, -1, 0], [0, 0, W, T])
|
||||
@@ -1227,6 +1326,7 @@ export function createShelf3D(container, lib, { works: firstWorks = [], rowsLand
|
||||
|
||||
const api = {
|
||||
setWorks(next) { works = next; api.relayout() },
|
||||
toTop() { focusId = null; velocity = 0; toTopNext = true; api.relayout() }, // tras cambiar el orden
|
||||
relayout() {
|
||||
clearTimeout(layoutTimer)
|
||||
layoutTimer = setTimeout(() => {
|
||||
@@ -1256,7 +1356,7 @@ export function createShelf3D(container, lib, { works: firstWorks = [], rowsLand
|
||||
setZoom,
|
||||
draw,
|
||||
// Para las pruebas.
|
||||
debug: () => ({ faces: Object.entries(palettes).filter(([k, v]) => k.startsWith('face:') && v).length, facesAsked: Object.keys(palettes).filter(k => k.startsWith('face:')).length, faceWaiting: faceWaiting.length, faceBusy, faceWorker: faceWorker === false ? 'off' : !!faceWorker, rows: rows.map(r => r.map(i => ({ id: i.work.id, vol: i.vol.vol, x: i.x, w: i.w, h: i.h, d: i.d, face: i.face }))), rowWidth, y, x, zoom, velocity, top, bottom, peek: peek && { id: peek.it.work.id, vol: peek.it.vol.vol, t: peek.t, dir: peek.dir, rank: peek.rank, rect: peekRect(), fly: peek.fly ? peek.fly.t : null }, focusId, notes: [...built.values()].map(b => ({ k: b.k, notes: (b.notes?.list || []).map(n => ({ id: n.work.id, x: n.x, w: n.w })) })), logos: logos.size, tilt: { ...tiltNow }, listening: tilt.listening, arts: arts.size, panels: panels.size, built: [...built.keys()], covers: covers.size, palettes: Object.keys(palettes).length, waiting: waiting.length, busy: imgBusy, ctx: !gl.isContextLost() }),
|
||||
debug: () => ({ faces: Object.entries(palettes).filter(([k, v]) => k.startsWith('face:') && v).length, facesAsked: Object.keys(palettes).filter(k => k.startsWith('face:')).length, faceWaiting: faceWaiting.length, faceBusy, faceWorker: faceWorker === false ? 'off' : !!faceWorker, rows: rows.map(r => r.map(i => ({ id: i.work.id, vol: i.vol.vol, x: i.x, w: i.w, h: i.h, d: i.d, face: i.face }))), rowWidth, y, x, zoom, velocity, top, bottom, peek: peek && { id: peek.it.work.id, vol: peek.it.vol.vol, t: peek.t, dir: peek.dir, rank: peek.rank, rect: peekRect(), fly: peek.fly ? peek.fly.t : null }, focusId, notes: [...built.values()].map(b => ({ k: b.k, notes: (b.notes?.list || []).map(n => ({ id: n.work.id, x: n.x, w: n.w })) })), logos: logos.size, tilt: { x: tiltNow.x, y: tiltNow.y }, listening: tilt.listening, arts: arts.size, panels: panels.size, built: [...built.keys()], covers: covers.size, palettes: Object.keys(palettes).length, waiting: waiting.length, busy: imgBusy, ctx: !gl.isContextLost() }),
|
||||
destroy() {
|
||||
if (faceWorker) faceWorker.terminate()
|
||||
dead = true
|
||||
|
||||
Reference in New Issue
Block a user