diff --git a/webstore/dig.js b/webstore/dig.js index 4ce8e5f..ac3f90a 100644 --- a/webstore/dig.js +++ b/webstore/dig.js @@ -4,15 +4,22 @@ import * as THREE from 'three'; // bin; scroll/drag riffles a damped "cursor" through them; each sleeve hinges forward at its // bottom edge as you pass it; tap the front sleeve to pull it out and inspect (price/condition/ // add-to-cart). Procedural fwip/thunk audio — no asset files. +// Mechanics + tuned numbers ported back from THRIFTGOD's evolved fork (docs/DIG_FLIP_HANDOVER.md). -const REC_W = 0.31, REC_H = 0.31, GAP = 0.013, MAXFLIP = 1.95, BACK = -0.12; +const MAXFLIP = 1.95, BACK = -0.12; +// sleeve [width, height, thickness] per format — records, CDs, DVDs, tapes all riffle the same +const FMT = { lp: [0.31, 0.31, 0.0035], cd: [0.142, 0.125, 0.010], dvd: [0.136, 0.19, 0.014], + vhs: [0.105, 0.187, 0.025], cass: [0.11, 0.07, 0.017] }; export function createDig(renderer, { onClose } = {}) { const scene = new THREE.Scene(); scene.background = new THREE.Color(0x0a0a0c); - const camera = new THREE.PerspectiveCamera(45, innerWidth / innerHeight, 0.01, 50); + const camera = new THREE.PerspectiveCamera(45, (innerWidth / innerHeight) || 1, 0.01, 50); camera.position.set(0, 0.5, 0.92); camera.lookAt(0, 0.12, -0.2); + addEventListener('resize', () => { // index.html's resize handler only feeds the walk camera + camera.aspect = (innerWidth / innerHeight) || 1; camera.updateProjectionMatrix(); + }); scene.add(new THREE.AmbientLight(0xffffff, 0.45)); const spot = new THREE.SpotLight(0xfff2e6, 26, 5, 0.7, 0.5, 1.2); @@ -29,7 +36,7 @@ export function createDig(renderer, { onClose } = {}) { const css = el('style'); css.textContent = ` .dg-hint{position:fixed;left:50%;bottom:18px;transform:translateX(-50%);background:rgba(0,0,0,.6);color:#eee;padding:6px 14px;border-radius:20px;font:13px system-ui;pointer-events:none;display:none} .dg-cur{position:fixed;left:50%;top:22px;transform:translateX(-50%);color:#fff;font:500 16px system-ui;text-shadow:0 1px 4px #000;pointer-events:none;text-align:center;max-width:80%;display:none} - .dg-panel{position:fixed;right:6%;top:50%;transform:translateY(-50%);width:280px;background:rgba(16,16,20,.94);border:1px solid #333;border-radius:12px;padding:18px;color:#eee;font:14px system-ui;display:none} + .dg-panel{position:fixed;right:6%;top:50%;transform:translateY(-50%);width:280px;background:rgba(16,16,20,.94);border:1px solid #333;border-radius:12px;padding:18px;color:#eee;font:14px system-ui;display:none;z-index:7} .dg-panel h3{margin:.1em 0;font-weight:500;color:#ff5db1} .dg-panel .meta{color:#aaa;font-size:13px;margin:4px 0 10px} .dg-panel .price{font-size:24px;font-weight:500} @@ -63,32 +70,76 @@ export function createDig(renderer, { onClose } = {}) { } catch (e) {} } + // the crate drawn around the stack — floor/left/right/back + a LOWER front lip you flip over + let crate = null; + function buildCrate(maxW, maxH, depth) { + if (crate) scene.remove(crate); + crate = new THREE.Group(); + // ponytail: plain wood colour — no wood texture in webstore/assets; add one + RepeatWrapping ×2 if it looks flat + const wood = new THREE.MeshStandardMaterial({ color: 0x8a6a45, roughness: 0.85 }); + const W = maxW + 0.06, H = maxH * 0.72, D = depth + 0.10, th = 0.016; + const add = (w, h, d, x, y, z) => { const m = new THREE.Mesh(new THREE.BoxGeometry(w, h, d), wood); m.position.set(x, y, z); crate.add(m); }; + add(W, th, D, 0, -th / 2, -D / 2 + 0.05); // floor + add(th, H, D, -W / 2, H / 2, -D / 2 + 0.05); // left + add(th, H, D, W / 2, H / 2, -D / 2 + 0.05); // right + add(W, H, th, 0, H / 2, -D + 0.05); // back + add(W, H * 0.55, th, 0, H * 0.275, 0.05); // front lip (lower — you flip over it) + scene.add(crate); + } + function buildStack(records) { recs.forEach(r => scene.remove(r.group)); recs = []; + let z = -0.05, maxW = 0.31, maxH = 0.31; records.forEach((rec, i) => { - const grp = new THREE.Group(); grp.position.set(0, 0, -i * GAP - 0.05); + const [w, h, th] = FMT[rec.fmt] || FMT.lp; + maxW = Math.max(maxW, w); maxH = Math.max(maxH, h); + const grp = new THREE.Group(); grp.position.set(0, 0, z); const mat = new THREE.MeshStandardMaterial({ color: 0x2b2b33, roughness: 0.75 }); - const mesh = new THREE.Mesh(new THREE.BoxGeometry(REC_W, REC_H, 0.0035), mat); - mesh.position.y = REC_H / 2; // hinge at group origin = bin floor + const mesh = new THREE.Mesh(new THREE.BoxGeometry(w, h, th), mat); + mesh.position.y = h / 2; // hinge at group origin = bin floor grp.add(mesh); grp.rotation.x = BACK; + // secondhand records don't stand to attention — every sleeve leans its own way + grp.rotation.z = (((i * 2654435761) >>> 0) % 100 - 50) / 1800; + grp.position.x = (((i * 40503) >>> 0) % 100 - 50) / 12000; scene.add(grp); - recs.push({ group: grp, mat, rec, angle: BACK, loaded: false, homeZ: -i * GAP - 0.05 }); + recs.push({ group: grp, mat, rec, angle: BACK, loaded: false, homeZ: z }); + z -= th + 0.010; // packed by real thickness — VHS riffles chunkier than vinyl }); + buildCrate(maxW, maxH, -z); cursor = 0; vel = 0; lastFloor = -1; loadCoversNear(); updateCur(); + fillQueue(); // trickle-load EVERY cover so the crate fills in behind you + } + + let fillTimer = null; + function fillQueue() { + clearTimeout(fillTimer); + const next = recs.find(r => !r.loaded); + if (!next) return; + loadCover(next); + fillTimer = setTimeout(fillQueue, 120); + } + + function loadCover(r) { + if (r.loaded) return; + r.loaded = true; + const url = r.rec.thumb; if (!url) return; + loader.load(url, tx => { tx.colorSpace = THREE.SRGBColorSpace; r.mat.map = tx; r.mat.color.set(0xffffff); r.mat.needsUpdate = true; }, undefined, () => {}); } function loadCoversNear() { const c = Math.round(cursor); - for (let i = Math.max(0, c - 8); i < Math.min(recs.length, c + 9); i++) { - const r = recs[i]; if (r.loaded) continue; r.loaded = true; - const url = r.rec.thumb; if (!url) continue; - loader.load(url, tx => { tx.colorSpace = THREE.SRGBColorSpace; r.mat.map = tx; r.mat.color.set(0xffffff); r.mat.needsUpdate = true; }, undefined, () => {}); - } + for (let i = Math.max(0, c - 10); i < Math.min(recs.length, c + 15); i++) loadCover(recs[i]); } function updateCur() { const r = recs[Math.round(cursor)]; - curLbl.textContent = r ? `${r.rec.title || r.rec.sku}${r.rec.artist ? ' — ' + r.rec.artist : ''}` : ''; + curLbl.textContent = r ? `${r.rec.title || r.rec.sku}${r.rec.artist ? ' — ' + r.rec.artist : ''}` : 'crate\'s empty — keep browsing'; + } + + // re-pack the survivors after a sleeve leaves the crate — no gap left behind + function repack() { + let z = -0.05; + recs.forEach(x => { x.homeZ = z; x.group.position.z = z; z -= (FMT[x.rec.fmt] || FMT.lp)[2] + 0.010; }); } function pull() { @@ -98,11 +149,26 @@ export function createDig(renderer, { onClose } = {}) { panel.innerHTML = `