6.1 KiB
HANDOVER: the record flip/riffle animation — port THRIFTGOD's dig back into RecordGod
For: the RecordGod agent. Goal: adopt the evolved crate-digging mechanic into the
3D store served at robotmonster.party/store/ (this repo: webstore/index.html +
webstore/dig.js). Written: 2026-07-02 by the THRIFTGOD session. Self-contained —
you don't need that conversation.
Lineage (read this first)
RecordGod's webstore/dig.js is the ORIGINAL crate-inspect scene. THRIFTGOD
(/Users/johnking/Documents/OPSHOPGAME/web/dig.js) forked it and evolved it through a
day of live play-testing. The THRIFTGOD file is a superset of yours with the same
architecture — same createDig(renderer, opts) factory, same scene/camera/update/open/
close contract, same procedural audio. The cleanest port is: diff the two files, take
everything, then re-wire the two integration points listed at the end.
diff webstore/dig.js /Users/johnking/Documents/OPSHOPGAME/web/dig.js
The mechanics, and the numbers that make them feel right
All tuned by hand against real play. Change them at your peril; record them if you do.
-
Riffle = a damped cursor over the stack.
cursor(float index) +vel; per frame:vel *= Math.pow(0.0008, dt),cursor += vel * dt, clamped to [0, n−1].- Wheel:
vel += e.deltaY * 0.012. Drag:cursor += dy * 0.02; vel = dy * 0.6. - When
Math.floor(cursor)changes → play the fwip + load nearby covers + update the title label.
-
The flip is a hinge at the sleeve's bottom edge.
- Each record = a Group at the bin floor; the sleeve mesh is offset
y = h/2inside it, so rotating the GROUP hinges at the bottom edge — this is the whole trick. - Rest angle
BACK = −0.12(sleeves lean back); flipped-forward maxMAXFLIP = 1.95. - Per frame, each record i targets:
target = BACK + smoothstep(cursor − i, −0.4, 1.2) * (MAXFLIP − BACK)then eases:angle += (target − angle) * min(1, dt*12). Records behind the cursor stand; records ahead lie flopped forward. The smoothstep window (−0.4…1.2) is what makes riffling feel like fingers walking sleeves.
- Each record = a Group at the bin floor; the sleeve mesh is offset
-
Real format sizes + packing by thickness (THRIFTGOD addition):
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] }— [w, h, thickness] metres.- Stack:
z -= thickness + 0.010per sleeve. VHS riffles chunkier than vinyl for free. - RecordGod is LP-only today, but keep the map — CDs are inevitable.
-
Secondhand lean (cheap, huge): per sleeve seeded jitter —
rotation.z = ((i*2654435761>>>0)%100−50)/1800,position.x = ((i*40503>>>0)%100−50)/12000. No more parade-ground vinyl. -
The crate is drawn around the stack (THRIFTGOD addition): floor/left/right/back walls + a LOWER front lip (
H*0.55) the sleeves flip over; sized from max sleeve dims- stack depth; wood texture (any wood jpg with RepeatWrapping ×2). See
buildCrate().
- stack depth; wood texture (any wood jpg with RepeatWrapping ×2). See
-
Covers trickle-load; near-cursor loads win.
- Priority window on cursor move: indices
c−10 … c+15load immediately. - Background:
fillQueue()loads ONE unloaded cover every 120ms until the crate is full (clear the timer inclose()). Never load all covers eagerly; never texture more than the window synchronously.tx.colorSpace = THREE.SRGBColorSpacealways.
- Priority window on cursor move: indices
-
Pull-to-inspect presents at arm's length ON THE VIEW AXIS (final tuning after two bad iterations — first it cropped off-screen, then it became IMAX):
- Dig camera:
position (0, 0.5, 0.92),lookAt (0, 0.12, −0.2), FOV 45. - Pull target FOR THAT CAMERA:
position → (−0.1, 0.23 − h/2, 0.12)(h = sleeve height; the −0.1 x keeps it clear of a right-side info panel), androtation.x → −0.33so the cover faces the lens. - If your camera differs, derive it:
target = cam.pos + viewDir * 0.85, theny −= h/2, and tilt to face the camera. 0.85m gives an LP ~21° of a 45° FOV. - Lerp position
min(1, dt*5), rotationmin(1, dt*6). unpull()restoresposition.set(0, 0, homeZ); the angle system reclaims rotation.- Click-vs-drag: pull only fires if total pointer movement
< 4px.
- Dig camera:
-
After a sale, re-pack the stack: remove the record, then walk the survivors re-assigning
homeZwith the same thickness accumulation. Don't leave a gap. -
Procedural audio, no asset files (already in your ancestor, keep it):
- fwip: 50ms white-noise burst → bandpass 2100Hz Q0.7 → gain 0.22.
- thunk (pull): sine 105Hz, 5ms attack to 0.5, exp decay over ~0.28s.
- Lazily create AudioContext on first use (autoplay policies).
-
Pointer-lock gotcha (if your store uses PointerLockControls): Chrome refuses re-lock within ~1.3s of Esc. Never call
controls.lock()programmatically after closing a panel — wrap it: request,.catch()→ show a click-to-resume overlay (a fresh user gesture always succeeds). THRIFTGOD calls thissafeLock()inweb/index.html.
Integration points (the only THRIFTGOD-specific bits to rewire)
- Data in: THRIFTGOD uses
createDig(renderer, { onClose, getItems, onBought })withopen(kind); your ancestor usedgetRecords+open(crateId)fetching/virtual/crate/{id}/records. Keep YOUR fetch, adopt their buildStack. Records need{ title, artist, thumb, price, condition, fmt? }(fmt defaults tolp). - Buy action: THRIFTGOD's pull-panel button POSTs
/api/take(its basket system). Replace with RecordGod's add-to-cart. Everything else inpull()/unpull()ports as-is. - Images: THRIFTGOD proxies external covers via
/img?u=. RecordGod serves its own webp at/store/assets/— use yourthumbURLs directly, no proxy needed.
Definition of done
Riffle with wheel AND drag feels weighted; sleeves lean individually; the crate is visible around the stack; covers fill in behind you; pulling any format presents fully in frame, facing the camera, beside (not under) the info panel; buying closes the gap in the stack; Esc steps back out cleanly twice (pull → crate → store) with no pointer-lock console errors.