PageSourceSearch

https://georgluif.at/assets/shelf-kit.js?v=339428fa

js georgluif.at collected 2026-10-09 21:26:25 UTC 16,534 bytes, 13 lines download raw bytes

1/*! © 2011–2026 Georg Luif · georgluif.at · All rights reserved. */
2/*!
3shelf-kit.js — shared helpers + the composable bookshelf rig.
4
5Part 1 (helpers): side-effect-free pieces used by diorama.js (the self-contained physics
6rig behind the station tables) and by the hero scene: seeded RNG, painted canvas textures,
7the wood-impact sound kit, and the Proxy-node → collision-box reader.
8
9Part 2 (createShelfKit): the interactive bookshelf as a plug-in unit for an externally
10owned scene/camera/canvas (the hero). Ported from diorama.js's shelf slices — kept as a
11deliberate duplicate so the four station tables never depend on hero refactors.
12*/
13import*as s from"./three.module.min.js";import{loadModel as Lt,siteAssetUrl as Wt}from"./model-loader.js";import*as b from"./cannon-es.js";export function hashSeed(r){let c=2166136261;for(let i=0;i<r.length;i++)c^=r.charCodeAt(i),c=Math.imul(c,16777619);return c>>>0}export function mulberry32(r){let c=r>>>0;return function(){c=c+1831565813|0;let i=Math.imul(c^c>>>15,1|c);return i=i+Math.imul(i^i>>>7,61|i)^i,((i^i>>>14)>>>0)/4294967296}}export function hexToHsl(r){const c=/^#?([0-9a-f]{6})$/i.exec(r||"");if(!c)return{h:30,s:30,l:60};const i=parseInt(c[1],16),u=(i>>16&255)/255,d=(i>>8&255)/255,v=(i&255)/255,w=Math.max(u,d,v),W=Math.min(u,d,v),C=w-W;let x=0;C>0&&(w===u?x=((d-v)/C+(d<v?6:0))*60:w===d?x=((v-u)/C+2)*60:x=((u-d)/C+4)*60);const M=(w+W)/2,T=C===0?0:C/(1-Math.abs(2*M-1));return{h:x,s:T*100,l:M*100}}export function makePaintedTexture(r,c,i){i=i||256;const u=document.createElement("canvas");u.width=u.height=i;const d=u.getContext("2d");d.fillStyle="hsl("+c.h+","+c.s+"%,"+c.l+"%)",d.fillRect(0,0,i,i);for(let w=0;w<150;w++){const W=c.h+(r()-.5)*18,C=Math.max(8,Math.min(80,c.s*(.7+r()*.6))),x=Math.max(14,Math.min(90,c.l+(r()-.5)*30));d.fillStyle="hsla("+W+","+C+"%,"+x+"%,"+(.08+r()*.12)+")",d.save(),d.translate(r()*i,r()*i),d.rotate(r()*Math.PI),d.beginPath(),d.ellipse(0,0,16+r()*48,3+r()*9,0,0,Math.PI*2),d.fill(),d.restore()}const v=new s.CanvasTexture(u);return v.colorSpace=s.SRGBColorSpace,v}var Zt=["./assets/sounds/wood-impact-1.wav","./assets/sounds/wood-impact-2.wav","./assets/sounds/wood-impact-3.wav","./assets/sounds/wood-impact-4.wav","./assets/sounds/wood-impact-5.wav"],Qt=.45;export function createSoundKit(){let r=null,c=[],i=!1,u=0,d=null;function v(){i||c.length||(i=!0,Zt.forEach(function(x){fetch(Wt(x)).then(function(M){return M.arrayBuffer()}).then(function(M){return r.decodeAudioData(M)}).then(function(M){c.push(M)}).catch(function(){})}))}function w(){if(!r)try{r=new(window.AudioContext||window.webkitAudioContext)}catch{return}r.state==="suspended"&&r.resume(),v()}function W(x){if(!(!r||r.state!=="running"||!c.length)){var M=r.currentTime;if(!(M-u<.05)){u=M;var T=Math.max(.05,Math.min(1,x??.5))*Qt,g=r.createBufferSource(),E=c[Math.random()*c.length|0];c.length>1&&E===d&&(E=c[(c.indexOf(E)+1)%c.length]),d=E,g.buffer=E,g.playbackRate.value=.9+Math.random()*.25;var n=r.createGain();n.gain.value=T,g.connect(n).connect(r.destination),g.start(M)}}}function C(){r&&(r.close(),r=null,c=[])}return{unlock:w,playImpact:W,close:C,ready:function(){return!!c.length}}}export function collectProxies(r){const c=[];return r.traverse(function(i){if(/^proxy/i.test(i.name||"")){i.visible=!1;for(let u=i.parent;u;u=u.parent)if(c.indexOf(u)!==-1)return;c.push(i)}}),c}export function proxyBoxes(r){const c=[],i=new s.Vector3;return r.traverse(function(u){if(!u.isMesh||!u.geometry)return;const d=u.geometry.getAttribute("position");if(!d)return;const v=d.count,w=new Int32Array(v),W=new Map;let C=0;for(let n=0;n<v;n++){i.fromBufferAttribute(d,n);const h=Math.round(i.x*1e4)+":"+Math.round(i.y*1e4)+":"+Math.round(i.z*1e4);let m=W.get(h);m===void 0&&(m=C++,W.set(h,m)),w[n]=m}const x=new Int32Array(C);for(let n=0;n<C;n++)x[n]=n;function M(n){for(;x[n]!==n;)x[n]=x[x[n]],n=x[n];return n}function T(n,h){n=M(n),h=M(h),n!==h&&(x[n]=h)}const g=u.geometry.index?u.geometry.index.array:null;if(g)for(let n=0;n+2<g.length;n+=3)T(w[g[n]],w[g[n+1]]),T(w[g[n+1]],w[g[n+2]]);else for(let n=0;n+2<v;n+=3)T(w[n],w[n+1]),T(w[n+1],w[n+2]);const E=new Map;for(let n=0;n<v;n++){const h=M(w[n]);let m=E.get(h);m||(m=new s.Box3,E.set(h,m)),i.fromBufferAttribute(d,n).applyMatrix4(u.matrixWorld),m.expandByPoint(i)}E.forEach(function(n){const h=new s.Vector3;n.getCenter(h);const m=new s.Vector3;n.getSize(m),c.push({cx:h.x,cy:h.y,cz:h.z,hx:m.x/2,hy:m.y/2,hz:m.z/2})})}),c}const Kt=[.16,.368,.592,.799],$t=.1,Jt=.06,D=.06,Tt=.22,Vt=.34,te=.42,ee=1/60;export function createShelfKit(r){const c=r.scene,i=r.camera,u=r.domElement,d=r.overlayHost||u.parentNode,v=r.projects||[],w=hexToHsl(r.tint||"#c98a4b"),W=!!r.reducedMotion,C=window.matchMedia("(hover: hover)").matches,x=r.targetHeight||3.2,M=r.loadScope?{scope:r.loadScope}:void 0,T=!!r.shadows,g={soundEnabled:r.soundEnabled!==!1,wireframe:!1},E=typeof r.onWake=="function"?r.onWake:function(){},n=new s.Group;r.position&&n.position.copy(r.position),n.rotation.y=r.rotationY||0,c.add(n),n.updateMatrixWorld(!0);const h=new b.World({gravity:new b.Vec3(0,-9.82,0)});h.allowSleep=!0,h.defaultContactMaterial.friction=.4,h.defaultContactMaterial.restitution=.15;const m=[],K=[],pt=[],rt=[];function O(t){return rt.push(t),t}const Ft=O(new s.MeshBasicMaterial({color:3386111,wireframe:!0,transparent:!0,opacity:.6})),It=O(new s.MeshBasicMaterial({color:4521881,wireframe:!0,transparent:!0,opacity:.4})),$=createSoundKit();let ht=!1;const B=document.createElement("div");B.className="table-card cutout",B.hidden=!0,B.addEventListener("contextmenu",function(t){t.preventDefault()}),d.appendChild(B);let q=null;function Nt(t){B.textContent="";const e=document.createElement("span");e.className="table-card-body";const o=document.createElement("strong");if(o.textContent=t.name||"",e.appendChild(o),t.year){const a=document.createElement("span");a.className="table-year",a.textContent=t.year,e.appendChild(a)}if(t.blurb){const a=document.createElement("span");a.className="table-blurb",a.textContent=t.blurb,e.appendChild(a)}if(B.appendChild(e),t.image){const a=document.createElement("span");a.className="table-card-media";const l=document.createElement("img");l.src=Wt(t.image),l.alt="",l.loading="lazy",l.draggable=!1,a.appendChild(l),B.appendChild(a),B.classList.add("has-media")}else B.classList.remove("has-media")}function J(t){q!==t&&(q=t,t?(Nt(t.project),B.hidden=!1):B.hidden=!0,mt(t),E())}const X=new s.Vector3;
13function At(){if(!q||B.hidden)return;q.mesh.getWorldPosition(X),X.y+=q.half+.12,X.project(i);const t=d.clientWidth||1,e=d.clientHeight||1;B.style.left=Math.min(t-70,Math.max(70,(X.x*.5+.5)*t))+"px",B.style.top=Math.min(e-8,Math.max(56,(-X.y*.5+.5)*e))+"px"}const I=document.createElement("span");I.className="shelf-tag",I.hidden=!0,d.appendChild(I);let H=null;function mt(t){const e=t&&t.project&&t.project.label;H=e?t:null,H?(I.textContent=e,I.hidden=!1):I.hidden=!0}const Z=new s.Vector3;function Dt(){if(!H||I.hidden)return;H.mesh.getWorldPosition(Z),Z.y+=H.half+(q===H&&!B.hidden?.58:.18),Z.project(i);const t=d.clientWidth||1,e=d.clientHeight||1;I.style.left=Math.min(t-40,Math.max(40,(Z.x*.5+.5)*t))+"px",I.style.top=Math.max(14,(-Z.y*.5+.5)*e)+"px"}function U(t,e,o,a,l,f,k){const p=new b.Body({mass:0});p.addShape(new b.Box(new b.Vec3(a,l,f))),p.position.set(t,e,o),h.addBody(p),pt.push({cx:t,cy:e,cz:o,hx:a,hy:l,hz:f});const y=new s.Mesh(new s.BoxGeometry(a*2,l*2,f*2),k||Ft);return y.position.set(t,e,o),y.visible=g.wireframe,n.add(y),K.push(y),p}let it=null,st=!1,Y=null;const yt=new s.Matrix4,wt=new s.Vector3,xt=new s.Quaternion,gt=new s.Quaternion;function bt(t,e){return n.updateMatrixWorld(!0),yt.copy(n.matrixWorld).invert(),wt.set(t[0],t[1],t[2]).applyMatrix4(yt),n.getWorldQuaternion(gt).invert(),xt.set(e[0],e[1],e[2],e[3]).premultiply(gt),{pos:wt.clone(),quat:xt.clone()}}function Mt(){Y=new b.Body({mass:0,shape:new b.Plane}),Y.quaternion.setFromEuler(-Math.PI/2,0,0),Y.position.set(0,-.05,0),h.addBody(Y)}function vt(t){const e=bt(t.position,t.quaternion),o=new b.Body({mass:0});for(const a of t.boxes||[])o.addShape(new b.Box(new b.Vec3(a.halfExtents[0],a.halfExtents[1],a.halfExtents[2])),new b.Vec3(a.offset[0],a.offset[1],a.offset[2]));o.position.set(e.pos.x,e.pos.y,e.pos.z),o.quaternion.set(e.quat.x,e.quat.y,e.quat.z,e.quat.w),h.addBody(o);for(const a of t.boxes||[]){const l=new s.Mesh(new s.BoxGeometry(a.halfExtents[0]*2,a.halfExtents[1]*2,a.halfExtents[2]*2),It);l.position.set(a.offset[0],a.offset[1],a.offset[2]).applyQuaternion(e.quat).add(e.pos),l.quaternion.copy(e.quat),l.visible=g.wireframe,n.add(l),K.push(l)}E()}function qt(t){if(t=t||{},it=typeof t.groundY=="function"?t.groundY:null,Y&&(h.removeBody(Y),Y=null),t.heightfield){const e=t.heightfield,o=bt(e.position,e.quaternion),a=new b.Body({mass:0});a.addShape(new b.Heightfield(e.data,{elementSize:e.elementSize})),a.position.set(o.pos.x,o.pos.y,o.pos.z),a.quaternion.set(o.quat.x,o.quat.y,o.quat.z,o.quat.w),h.addBody(a)}else Mt();(t.boxes||[]).forEach(vt),st=!0,E()}function Et(t,e,o,a,l,f,k){n.add(o);const p=a.x*2,y=a.z*2,P=(f()-.5)*Math.max(0,e.xHalf*2-p)*e.xFactor,V=(f()-.5)*Math.max(0,e.zHalf*2-y)*e.zFactor,A=e.floorY+a.y+.01,S=W?0:Math.max(0,Math.min(.35,e.ceilY-e.floorY-a.y*2-.02))*(.3+f()*.5),z=new b.Body({mass:1,shape:new b.Box(a)});z.allowSleep=!0,z.sleepSpeedLimit=.15,z.sleepTimeLimit=.4,z.position.set(P,A+S,V),W||(k?z.quaternion.setFromEuler((f()-.5)*.5,f()*Math.PI,(f()-.5)*.5):z.quaternion.setFromEuler(0,f()*Math.PI*2,0));const F={mesh:o,body:z,project:t,half:a.y,flash:0,mat:l||null};return F.spawnPos=z.position.clone(),F.spawnQuat=z.quaternion.clone(),z.addEventListener("collide",function(ft){const R=Math.abs(ft.contact.getImpactVelocityAlongNormal());R>1.2&&(F.flash=Math.min(1,R/6),E()),R>1.6&&g.soundEnabled&&$.playImpact(Math.min(1,R/7))}),h.addBody(z),o.userData.cube=F,m.push(F),E(),F}const Yt=.62;function Bt(t,e,o){const a=mulberry32(hashSeed(t.slug||t.name||String(e))),l=Vt+a()*(te-Vt),f=l/2;function k(){const p=O(makePaintedTexture(a,{h:w.h+(a()-.5)*40,s:46,l:58},128)),y=new s.Mesh(O(new s.BoxGeometry(l,l,l)),O(new s.MeshBasicMaterial({map:p})));y.castShadow=T,Et(t,o,y,new b.Vec3(f,f,f),y.material,a,!0)}if(t.model){Lt(t.model,M).then(function(p){const y=p.scene.clone(!0),P=new s.Box3().setFromObject(y),V=P.getSize(new s.Vector3),A=P.getCenter(new s.Vector3),S=Math.max(V.x,V.y,V.z)||1,z=Yt/S;y.position.sub(A);const F=new s.Group;F.add(y),F.scale.setScalar(z),F.traverse(function(R){R.isMesh&&(R.castShadow=T)});const ft=new b.Vec3(Math.max(.08,V.x*z/2),Math.max(.08,V.y*z/2),Math.max(.08,V.z*z/2));Et(t,o,F,ft,null,a,!1)},function(p){console.warn("shelf token model failed, painted cube fallback:",t.model,p),k()});return}k()}function _t(t,e,o,a){t=t.slice().sort(function(l,f){return l.cy-f.cy}),t.forEach(function(l){U(l.cx,l.cy,l.cz,l.hx,l.hy,l.hz)}),v.forEach(function(l,f){const k=t[f%t.length],p=t[f%t.length+1],y=k.cy+k.hy;Bt(l,f,{floorY:y,ceilY:p?p.cy-p.hy:y+1,xHalf:k.hx,zHalf:k.hz,xFactor:.8,zFactor:.5}
13)})}function Gt(t,e,o){const a=t*(.5-$t),l=o*(.5-Jt),f=Kt.map(function(p){return p*e});f.forEach(function(p){U(0,p-D,0,a,D,l)}),U(-a-D,e/2,0,D,e/2,l),U(a+D,e/2,0,D,e/2,l),U(0,e/2,-l-D,a,e/2,D),f.forEach(function(p){U(0,p+Tt/2,l+D,a,Tt/2,D)});const k=[f[1],f[2],f[3],e];v.forEach(function(p,y){const P=y%4;Bt(p,y,{floorY:f[P],ceilY:k[P],xHalf:a,zHalf:l,xFactor:.8,zFactor:.5})})}function Ot(t){!st&&!Y&&Mt();const e=collectProxies(t),o=O(makePaintedTexture(mulberry32(hashSeed("buchregal")),{h:28,s:34,l:46},512)),a=O(new s.MeshBasicMaterial({map:o}));t.traverse(function(S){S.isMesh&&(S.material=a,S.castShadow=T,S.geometry.computeBoundingBox&&S.geometry.computeBoundingBox())});const l=new s.Box3().setFromObject(t),f=new s.Vector3;l.getSize(f);const k=x/(f.y||1),p=new s.Group;p.add(t),p.scale.setScalar(k);const y=new s.Box3().setFromObject(p),P=new s.Vector3;y.getSize(P);const V=new s.Vector3;y.getCenter(V),p.position.set(-V.x,-y.min.y,-V.z),p.updateMatrixWorld(!0);let A=null;if(e.length){A=[];for(const S of e)A.push.apply(A,proxyBoxes(S));for(const S of e)S.parent&&S.parent.remove(S),S.geometry&&S.geometry.dispose()}n.add(p),n.updateMatrixWorld(!0),A&&A.length?_t(A,P.x,P.y,P.z):Gt(P.x,P.y,P.z),ht=!0,E()}const jt=new Promise(function(t){Lt(r.modelUrl||"./assets/3d/buchregal.glb",M).then(function(e){try{Ot(e.scene.clone(!0))}catch(o){console.warn("shelf kit build failed:",o)}t()},function(e){console.warn("shelf kit model failed to load:",e),t()})}),tt=new s.Raycaster,ct=new s.Vector2;let _=null,j=!1,L=null,G=null;const et=new s.Plane,lt=new s.Vector3,ut=new s.Vector3,nt=new s.Vector3,N=new s.Vector3;function ot(t){const e=u.getBoundingClientRect();ct.set((t.clientX-e.left)/e.width*2-1,-((t.clientY-e.top)/e.height)*2+1)}function dt(){tt.setFromCamera(ct,i);const t=tt.intersectObjects(m.map(function(o){return o.mesh}),!0)[0];if(!t)return null;let e=t.object;for(;e&&!e.userData.cube;)e=e.parent;return e?{cube:e.userData.cube,point:t.point}:null}function Rt(){const t=G.cube,e=t.body;t.dragType=e.type,t.dragMask=e.collisionFilterMask,e.type=b.Body.KINEMATIC,e.collisionFilterMask=0,e.velocity.setZero(),e.angularVelocity.setZero(),e.wakeUp(),ut.copy(G.point),N.copy(G.point),n.worldToLocal(N),nt.set(e.position.x-N.x,e.position.y-N.y,e.position.z-N.z),L=t,j=!0}function St(t){g.soundEnabled&&$.unlock(),_={x:t.clientX,y:t.clientY},G=null,ot(t);const e=dt();e&&(G=e)}function Ct(t){if(_&&(G||j)){const e=Math.hypot(t.clientX-_.x,t.clientY-_.y);if(!j&&G&&e>6&&Rt(),j&&(ot(t),tt.setFromCamera(ct,i),i.getWorldDirection(et.normal),et.setFromNormalAndCoplanarPoint(et.normal,ut),tt.ray.intersectPlane(et,lt))){ut.copy(lt),N.copy(lt),n.worldToLocal(N);const o=L.body;o.position.set(N.x+nt.x,N.y+nt.y,N.z+nt.z),o.velocity.setZero(),E()}return}if(C){ot(t);const e=dt();u.style.cursor=e?"pointer":"",mt(e?e.cube:q)}}const at=new s.Vector3;function Ht(t,e){return it?(at.set(t,0,e),n.localToWorld(at),it(at.x,at.z)-n.position.y):0}function Ut(){if(L){const t=L.body,e=Ht(t.position.x,t.position.z)+L.half;t.position.y<e&&(t.position.y=e+.01),t.type=L.dragType!=null?L.dragType:b.Body.DYNAMIC,t.collisionFilterMask=L.dragMask!=null?L.dragMask:-1,t.velocity.setZero(),t.angularVelocity.setZero(),t.wakeUp(),L.dragType=L.dragMask=void 0}L=null,G=null}function zt(t){const e=j;if(j=!1,Ut(),!_)return;const o=Math.hypot(t.clientX-_.x,t.clientY-_.y);if(_=null,e||o>6){E();return}ot(t);const a=dt();if(!a){J(null);return}if(a.cube===q){J(null);return}J(a.cube)}function kt(t){B.hidden||B.contains(t.target)||t.target!==u&&J(null)}u.addEventListener("pointerdown",St),window.addEventListener("pointermove",Ct),window.addEventListener("pointerup",zt),document.addEventListener("pointerdown",kt);function Pt(){for(let t=0;t<m.length;t++)if(m[t].body.sleepState!==b.Body.SLEEPING)return!0;return!1}function Xt(t){if(!ht)return!1;h.step(ee,t,3);for(let e=0;e<m.length;e++){const o=m[e];if(o!==L&&o.body.position.y<-25&&(o.body.position.copy(o.spawnPos),o.body.quaternion.copy(o.spawnQuat),o.body.velocity.setZero(),o.body.angularVelocity.setZero(),o.body.wakeUp()),o.mesh.position.copy(o.body.position),o.mesh.quaternion.copy(o.body.quaternion),o.mat)if(o.flash>.001){const a=1+o.flash*.7;o.mat.color.setRGB(a,a,a),o.flash*=Math.pow(.01,t)}else o.mat.color.r!==1&&o.mat.color.setRGB(1,1,1)}return At(),Dt(),Pt()||j}const Q=new s.Vector3;return{ready:jt,group:n,step:Xt,anyAwake:Pt,setEnvironment:qt,addEnvironmentBody:vt,staticBoxes:function(){return pt.slice()},cubePushers:function(){const t=[];for(let e=0;e<m.length;e++)Q.copy(m[e].body.position),n.localToWorld(Q),t.push({x:Q.x,y:Q.y,z:Q.z,r:m[e].half,vy:m[e].body.velocity.y,dragging:m[e]===L});return t}
13,setWireframe:function(t){g.wireframe=!!t,K.forEach(function(e){e.visible=g.wireframe})},setSoundEnabled:function(t){g.soundEnabled=!!t,t&&$.unlock()},isSoundEnabled:function(){return g.soundEnabled},probe:function(){const t=u.getBoundingClientRect(),e=new s.Vector3;return{env:st,staticBodies:h.bodies.length,cubes:m.map(function(o){return o.mesh.getWorldPosition(e),e.project(i),{x:o.body.position.x,y:o.body.position.y,z:o.body.position.z,screenX:t.left+(e.x*.5+.5)*t.width,screenY:t.top+(-e.y*.5+.5)*t.height}})}},dispose:function(){u.removeEventListener("pointerdown",St),window.removeEventListener("pointermove",Ct),window.removeEventListener("pointerup",zt),document.removeEventListener("pointerdown",kt),B.remove(),I.remove(),$.close(),K.forEach(function(t){t.geometry.dispose()});for(let t=0;t<rt.length;t++){const e=rt[t];e&&e.dispose&&e.dispose()}c.remove(n)}}}

Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.