1
/* River Post — original procedural scene. No external assets or runtime requests. */2
(()=>{'use strict';3
const T=THREE,PI=Math.PI,TAU=PI*2,container=document.getElementById('scene');4
let renderer;5
try{renderer=new T.WebGLRenderer({antialias:true,alpha:false,powerPreference:'high-performance'});}catch(e){document.querySelector('#loading span').className='error';document.querySelector('#loading span').textContent='这个浏览器暂时无法开启 WebGL。请用支持硬件加速的 Chrome、Edge 或 Firefox 打开此文件。';return;}6
renderer.setPixelRatio(Math.min(devicePixelRatio,1.75));renderer.setSize(innerWidth,innerHeight);renderer.outputColorSpace=T.SRGBColorSpace;renderer.toneMapping=T.ACESFilmicToneMapping;renderer.toneMappingExposure=1.16;renderer.shadowMap.enabled=true;renderer.shadowMap.type=T.PCFSoftShadowMap;container.appendChild(renderer.domElement);7
const scene=new T.Scene();scene.background=new T.Color('#d9e9df');scene.fog=new T.FogExp2('#d9e9df',.017);8
const camera=new T.PerspectiveCamera(38,innerWidth/innerHeight,.12,155);9
const hemi=new T.HemisphereLight('#eaf6ff','#6b8250',2.25);scene.add(hemi);10
const sun=new T.DirectionalLight('#fff1c9',3.1);sun.position.set(-12,21,11);sun.castShadow=true;sun.shadow.mapSize.set(2048,2048);sun.shadow.camera.left=-18;sun.shadow.camera.right=18;sun.shadow.camera.top=18;sun.shadow.camera.bottom=-18;sun.shadow.camera.near=1;sun.shadow.camera.far=65;sun.shadow.normalBias=.035;sun.shadow.bias=-.00018;sun.shadow.radius=3;scene.add(sun);scene.add(sun.target);11
const fill=new T.DirectionalLight('#cfeced',.8);fill.position.set(8,5,-10);scene.add(fill);12
const mat=(color,roughness=.75,metalness=0)=>new T.MeshStandardMaterial({color,roughness,metalness});13
const M={fur:mat('#805138'),furLight:mat('#986342'),cream:mat('#e6c59a'),ear:mat('#c88e71'),eye:mat('#211e1b',.18),glint:mat('#fff8e3',.15),nose:mat('#332723',.2),scarf:mat('#d64e3e',.87),scarfLight:mat('#e87453'),bike:mat('#75b7a5',.38,.28),chrome:mat('#e9e4cf',.3,.6),brass:mat('#b89149',.32,.68),tire:mat('#393f38',.95),whitewall:mat('#eadac0'),seat:mat('#674e35'),wood:mat('#967b4e'),woodLight:mat('#bea572'),parcel:mat('#cda169'),parcel2:mat('#e0ba82'),twine:mat('#f3deb1'),grass:mat('#739158'),leaf:mat('#6f975a'),leafLight:mat('#93ad6e'),leafDark:mat('#4f7d55'),bark:mat('#88714b'),stone:mat('#bdb49b'),roof:mat('#ba7555'),house:mat('#e5d4ae'),door:mat('#597d68'),flower:mat('#fff3c4'),yellow:mat('#e3af48'),pink:mat('#e8a18a')};14
const G={sphere:new T.SphereGeometry(1,24,16),smallSphere:new T.SphereGeometry(1,12,8),leaf:new T.SphereGeometry(1,12,9),box:new T.BoxGeometry(1,1,1),cyl:new T.CylinderGeometry(1,1,1,10),cone:new T.ConeGeometry(1,1,9)};15
const v=(x=0,y=0,z=0)=>new T.Vector3(x,y,z);16
function mesh(parent,geo,material,pos,scale,rot){const o=new T.Mesh(geo,material);if(pos)o.position.set(...pos);if(scale)o.scale.set(...scale);if(rot)o.rotation.set(...rot);o.castShadow=true;o.receiveShadow=true;parent.add(o);return o;}17
function ell(parent,material,pos,scale,rot){return mesh(parent,G.sphere,material,pos,scale,rot);}18
function box(parent,material,pos,scale,rot){return mesh(parent,G.box,material,pos,scale,rot);}19
function rod(parent,a,b,r,material,r2){const av=Array.isArray(a)?v(...a):a,bv=Array.isArray(b)?v(...b):b;const d=bv.clone().sub(av);let o;if(r2!==undefined)o=new T.Mesh(new T.CylinderGeometry(r2,r,d.length(),12),material);else{o=new T.Mesh(G.cyl,material);o.scale.set(r,d.length(),r);}o.position.copy(av).add(bv).multiplyScalar(.5);o.quaternion.setFromUnitVectors(v(0,1,0),d.normalize());o.castShadow=true;o.receiveShadow=true;parent.add(o);return o;}20
function tube(parent,pts,r,material){const curve=new T.CatmullRomCurve3(pts.map(p=>v(...p)));return mesh(parent,new T.TubeGeometry(curve,Math.max(16,pts.length*9),r,8,false),material);}21
function torus(parent,r,t,material,pos,rot){return mesh(parent,new T.TorusGeometry(r,t,10,64),material,pos,null,rot);}22
// Bake stationary details into one draw per material, while preserving every animated joint.23
function mergeStatic(root,excluded=[]){root.updateWorldMatrix(true,true);const inverse=root.matrixWorld.clone().invert(),buckets=new Map(),remove=[];function visit(obj){if(excluded.includes(obj))return;if(obj.isMesh&&obj.geometry&&!Array.isArray(obj.material)){let g=obj.geometry.clone();if(g.index){const old=g;g=g.toNonIndexed();old.dispose();}g.applyMatrix4(inverse.clone().multiply(obj.matrixWorld));const key=obj.material.uuid;if(!buckets.has(key))buckets.set(key,{material:obj.material,gs:[],count:0});const b=buckets.get(key);b.gs.push(g);b.count+=g.attributes.position.count;remove.push(obj);}for(const child of obj.children)visit(child);}for(const child of root.children)visit(child);for(const b of buckets.values()){const p=new Float32Array(b.count*3),n=new Float32Array(b.count*3),uv=new Float32Array(b.count*2);let offset=0;for(const g of b.gs){p.set(g.attributes.position.array,offset*3);if(g.attributes.normal)n.set(g.attributes.normal.array,offset*3);if(g.attributes.uv)uv.set(g.attributes.uv.array,offset*2);offset+=g.attributes.position.count;g.dispose();}const g=new T.BufferGeometry();g.setAttribute('position',new T.BufferAttribute(p,3));g.setAttribute('normal',new T.BufferAttribute(n,3));g.setAttribute('uv',new T.BufferAttribute(uv,2));g.computeBoundingSphere();mesh(root,g,b.material);}remove.forEach(o=>o.removeFromParent());}24
function canvasTexture(w,h,draw){const c=document.createElement('canvas');c.width=w;c.height=h;draw(c.getContext('2d'),w,h);const tex=new T.CanvasTexture(c);tex.colorSpace=T.SRGBColorSpace;return tex;}25
const labelTex=canvasTexture(256,192,(c,w,h)=>{c.fillStyle='#f7e6bb';c.fillRect(0,0,w,h);c.strokeStyle='#916c46';c.lineWidth=2;c.strokeRect(12,12,w-24,h-24);c.fillStyle='#695239';c.font='bold 20px Georgia';c.fillText('RIVER POST',26,45);c.font='12px sans-serif';c.fillText('DELIVER WITH A LITTLE JOY',26,67);c.lineWidth=2;c.beginPath();c.moveTo(26,105);c.lineTo(164,105);c.moveTo(26,125);c.lineTo(138,125);c.moveTo(26,145);c.lineTo(156,145);c.stroke();c.fillStyle='#c66547';c.fillRect(181,92,48,56);c.fillStyle='#f5dca8';c.font='30px Georgia';c.fillText('♥',190,128);});26
const labelMat=new T.MeshStandardMaterial({map:labelTex,roughness:1});27
let seed=72819;function rnd(){seed=(Math.imul(seed,1664525)+1013904223)>>>0;return seed/4294967296;}const rand=(a,b)=>a+(b-a)*rnd();28
const PERIOD=160,K=TAU/PERIOD;function center(x){return 1.15*Math.sin(x*K)+.38*Math.sin(x*K*3);}function slope(x){return 1.15*K*Math.cos(x*K)+1.14*K*Math.cos(x*K*3);}function river(x){return center(x)-6.05;}29
// A continuous riverbank tile. Instancing keeps the meadows inexpensive to render.30
class Batch{constructor(parent){this.parent=parent;this.buckets=new Map();this.dummy=new T.Object3D();}add(geo,m,p,s,r){const key=geo.uuid+m.uuid;if(!this.buckets.has(key))this.buckets.set(key,{geo,m,items:[]});this.dummy.position.set(...p);this.dummy.scale.set(...s);this.dummy.rotation.set(...(r||[0,0,0]));this.dummy.updateMatrix();this.buckets.get(key).items.push(this.dummy.matrix.clone());}rod(a,b,r,m){const d=v(...b).sub(v(...a));this.dummy.position.copy(v(...a)).add(v(...b)).multiplyScalar(.5);this.dummy.scale.set(r,d.length(),r);this.dummy.quaternion.setFromUnitVectors(v(0,1,0),d.normalize());this.dummy.updateMatrix();const key=G.cyl.uuid+m.uuid;if(!this.buckets.has(key))this.buckets.set(key,{geo:G.cyl,m,items:[]});this.buckets.get(key).items.push(this.dummy.matrix.clone());}finish(){for(const b of this.buckets.values()){const o=new T.InstancedMesh(b.geo,b.m,b.items.length);b.items.forEach((m,i)=>o.setMatrixAt(i,m));o.castShadow=true;o.receiveShadow=true;o.computeBoundingSphere();this.parent.add(o);}}}31
const tile=new T.Group(),B=new Batch(tile);32
function strip(z0,z1,material,height,color1,color2){const N=240,W=10,pos=[],colors=[],idx=[];const a=new T.Color(color1||'#829c63'),b=new T.Color(color2||'#91a975');for(let i=0;i<=N;i++){const x=-80+160*i/N;for(let j=0;j<=W;j++){const t=j/W,offset=z0+(z1-z0)*t,z=center(x)+offset;const y=typeof height==='function'?height(x,offset,t):height;pos.push(x,y,z);const c=a.clone().lerp(b,.3+.22*Math.sin(x*.23+z*.65)+.15*Math.cos(x*.51-z*.22));colors.push(c.r,c.g,c.b);}}for(let i=0;i<N;i++)for(let j=0;j<W;j++){const q=i*(W+1)+j;idx.push(q,q+1,q+W+1,q+1,q+W+2,q+W+1);}const g=new T.BufferGeometry();g.setAttribute('position',new T.Float32BufferAttribute(pos,3));g.setAttribute('color',new T.Float32BufferAttribute(colors,3));g.setIndex(idx);g.computeVertexNormals();const o=mesh(tile,g,material);o.castShadow=false;return o;}33
const groundMat=new T.MeshStandardMaterial({vertexColors:true,roughness:1});34
strip(1.7,62,groundMat,(x,z,t)=>.02+Math.pow(t,1.3)*(1.5+.55*Math.sin(x*K*4)), '#7d975e','#a3b77c');35
strip(-72,-10.05,groundMat,(x,z,t)=>.05+Math.pow(1-t,1.7)*(4+1.1*Math.cos(x*K*3)), '#73996d','#a2b68a');36
strip(-2.2,1.7,new T.MeshStandardMaterial({vertexColors:true,roughness:1}),.055,'#d3c6a4','#e2d6b5');37
strip(-2.7,-2.2,groundMat,.018,'#a4b57b','#95a56e');38
strip(-10.3,-9.8,groundMat,(x,z,t)=>.05-t*.29,'#b2c08b','#859f6c');39
strip(-2.7,-2.1,groundMat,(x,z,t)=>-.27+t*.29,'#96aa72','#c1c38c');40
// River shader: geometric ripples, moving highlights and a softly lit translucent edge.41
const waterMat=new T.ShaderMaterial({uniforms:{uTime:{value:0},uEvening:{value:0}},vertexShader:`varying vec3 vP;varying vec3 vWorld;uniform float uTime;void main(){vec3 p=position;p.y+=.016*sin(p.x*1.9+p.z*3.1+uTime*1.5)+.009*sin(p.x*4.1-p.z*2.9-uTime*.8);vP=p;vWorld=(modelMatrix*vec4(p,1.)).xyz;gl_Position=projectionMatrix*modelViewMatrix*vec4(p,1.);}`,fragmentShader:`uniform float uTime;uniform float uEvening;varying vec3 vP;varying vec3 vWorld;void main(){vec2 q=vec2(vP.x*.8-uTime*.24,vP.z*3.+uTime*.18);float wave=sin(q.x*2.+.8*sin(q.y))*sin(q.y*3.+.4*cos(q.x*3.));float shine=pow(max(0.,sin(q.x*3.4+q.y*.6)*cos(q.y*3.2+sin(q.x))),18.);float fresnel=pow(1.-clamp(normalize(cameraPosition-vWorld).y,0.,1.),3.);vec3 base=mix(vec3(.16,.43,.39),vec3(.34,.47,.37),uEvening);vec3 col=base+vec3(.025,.043,.038)*wave+shine*vec3(.23,.25,.18);col=mix(col,mix(vec3(.64,.77,.67),vec3(.85,.66,.44),uEvening),fresnel*.65);float c=1.15*sin(vP.x*.039269908)+.38*sin(vP.x*.117809724)-6.305;float edge=abs(vP.z-c)/3.675;col+=vec3(.16,.19,.12)*smoothstep(.965,1.,edge+.009*sin(vP.x*8.+uTime))* (.5+.5*sin(vP.x*6.-uTime));float distanceFade=exp(-length(cameraPosition-vWorld)*.017);col=mix(mix(vec3(.72,.84,.76),vec3(.86,.72,.57),uEvening),col,distanceFade);gl_FragColor=vec4(col,1.);\n#include <tonemapping_fragment>\n#include <colorspace_fragment>}`});42
strip(-9.98,-2.63,waterMat,-.21);43
const rippleMat=new T.MeshBasicMaterial({color:'#d6e5bb',transparent:true,opacity:.48,depthWrite:false});44
// Road grain and scattered leaves.45
for(let i=0;i<550;i++){const x=rand(-80,80),z=center(x)+rand(-1.95,1.43);B.add(G.smallSphere,i%3===0?M.stone:M.parcel2,[x,.064,z],[rand(.015,.05),.012,rand(.012,.04)]);}46
const grassGeom=new T.ConeGeometry(1,1,3);for(let i=0;i<900;i++){const x=rand(-80,80),side=rnd()>.32;const z=center(x)+(side?rand(2,14):rand(-16,-10.5));const y=side?.04+(z-center(x)-1.7)/60*.5:.12;const h=rand(.12,.45);B.add(grassGeom,i%3?M.grass:M.leafLight,[x,y+h/2,z],[.05,h,.07],[rand(-.15,.15),rand(0,TAU),rand(-.2,.2)]);}47
function flower(x,z,scale=1){const y=.075,s=scale;B.rod([x,y,z],[x+.04*s,y+.32*s,z],.014*s,M.leafDark);B.add(G.smallSphere,M.yellow,[x+.04*s,y+.34*s,z],[.065*s,.04*s,.065*s]);for(let i=0;i<5;i++){const a=i*TAU/5;B.add(G.smallSphere,M.flower,[x+.04*s+Math.cos(a)*.075*s,y+.34*s,z+Math.sin(a)*.075*s],[.057*s,.027*s,.047*s],[0,-a,0]);}}48
for(let i=0;i<155;i++){const x=rand(-80,80);flower(x,center(x)+rand(1.9,7),rand(.7,1.25));}49
function tree(x,z,size=1,type=0){const y=.10;B.rod([x,y,z],[x-.10*size,y+2.7*size,z],.17*size,M.bark);for(let a=0;a<3;a++){const angle=a*2.2;B.rod([x,y+1.4*size,z],[x+Math.cos(angle)*.7*size,y+2.6*size,z+Math.sin(angle)*.7*size],.07*size,M.bark);}for(let j=0;j<6;j++){const a=j*2.4,r=j===0?0:.8*size;B.add(G.leaf,[M.leaf,M.leafLight,M.leafDark][(j+type)%3],[x+Math.cos(a)*r,y+(2.4+j%2*.48)*size,z+Math.sin(a)*r],[size*(j===0?1.12:.87),size*(type===1?1.65:1.10),size*(j===0?1.12:.88)],[0,a,0]);}}50
for(let i=0;i<32;i++){const x=-77+i*4.9+rand(-1.1,1.1);tree(x,center(x)+rand(-22,-13),rand(.8,1.5),i%2);}51
for(let i=0;i<15;i++){const x=-74+i*10+rand(-2,2);tree(x,center(x)+rand(17,25),rand(.8,1.35),i%2);}52
for(let i=0;i<27;i++){const x=rand(-80,80),z=center(x)+(rnd()>.5?rand(-12.1,-10.5):rand(2,6));B.add(G.leaf,M.stone,[x,.14,z],[rand(.24,.68),rand(.16,.36),rand(.24,.5)],[0,rand(0,PI),.1]);}53
// Reeds and cattails, rooted on both river edges.54
for(let i=0;i<135;i++){const x=rand(-80,80),z=center(x)+(i%2?rand(-2.95,-2.67):rand(-10,-9.82));if(x>21.6&&x<24.4&&i%2)continue;const h=rand(.45,.9);B.rod([x,-.17,z],[x+.1,h-.17,z+.04],.015,M.leafDark);if(i%3===0)B.add(G.cyl,M.wood,[x+.1,h-.08,z+.04],[.039,.20,.039]);B.rod([x,-.1,z],[x-.17,h*.55,z-.13],.016,M.leaf);}55
// Fence: hand-built timber runs beside the water with open sections for the dock.56
for(let x=-78;x<80;x+=3.6){if(x>17&&x<28||x>-54&&x<-44)continue;const z=center(x)-2.22;B.add(G.box,M.woodLight,[x,.41,z],[.13,.79,.13],[0,0,.03*Math.sin(x)]);B.add(G.box,M.wood,[x,.85,z],[.20,.10,.20]);const nx=Math.min(x+3.6,80);for(const y of [.34,.66])B.rod([x,y,z],[nx,y,center(nx)-2.22],.041,M.woodLight);}57
// Lily pads, little stepping-stones, river reflections.58
const padGeo=new T.CircleGeometry(1,22,.12,TAU-.36);const padMat=mat('#6e9965');59
for(let i=0;i<38;i++){const x=rand(-80,80),z=river(x)+rand(-3.1,3.1);B.add(padGeo,padMat,[x,-.162,z],[rand(.15,.35),rand(.15,.35),1],[-PI/2,0,rand(0,TAU)]);}60
function dock(){const x=23,z=center(x);for(let i=0;i<15;i++)B.add(G.box,M.woodLight,[x,-.015,z-2.2-i*.23],[2.5,.12,.20]);for(const dx of [-1.1,1.1])for(const dz of [-2.2,-5.25]){B.add(G.box,M.wood,[x+dx,-.27,z+dz],[.13,.78,.13]);B.add(G.box,M.woodLight,[x+dx,.38,z+dz],[.13,.60,.13]);}for(const dx of [-1.1,1.1])B.rod([x+dx,.62,z-2.2],[x+dx,.62,z-5.25],.04,M.twine);const benchZ=z-2.8;B.add(G.box,M.wood,[x+.35,.42,benchZ],[1.35,.12,.42]);B.add(G.box,M.woodLight,[x+.35,.85,benchZ-.2],[1.35,.45,.08]);for(const dx of [-.15,.85])B.add(G.box,M.wood,[x+dx,.17,benchZ],[.09,.49,.32]);}61
dock();62
// Stone arch footbridge on the far bend.63
function bridge(){const x=-23,z=river(x),half=4.8;const shape=new T.Shape();shape.moveTo(-half,.0);shape.lineTo(-half,.75);for(let i=0;i<=40;i++){const q=-half+2*half*i/40;shape.lineTo(q,.75+.78*Math.sin(i/40*PI));}shape.lineTo(half,0);shape.lineTo(3.5,0);for(let i=0;i<=32;i++){const q=3.5-7*i/32;shape.lineTo(q,.0+.95*Math.sin(i/32*PI));}shape.lineTo(-half,0);const g=new T.ExtrudeGeometry(shape,{depth:1.35,bevelEnabled:true,bevelSize:.07,bevelThickness:.06,bevelSegments:2,steps:1,curveSegments:24});mesh(tile,g,M.stone,[x-.675,-.20,z],null,[0,PI/2,0]);for(let i=0;i<21;i++){const q=-half+(i+.5)*2*half/21,yy=.58+.78*Math.sin((i+.5)/21*PI);const angle=.16*Math.cos((i+.5)/21*PI);for(const side of [-.83,.83])B.add(G.box,M.house,[x+side,yy+.32,z-q],[.23,.5,.44],[angle,0,0]);B.add(G.box,M.woodLight,[x,yy+.09,z-q],[1.45,.10,.46],[angle,0,0]);}}64
bridge();65
// A postcard cottage across the river.66
function cottage(){const x=14,z=center(x)-14.4;B.add(G.box,M.house,[x,1.12,z],[3.9,2.2,2.8]);const roofShape=new T.Shape();roofShape.moveTo(-2.35,0);roofShape.lineTo(0,1.70);roofShape.lineTo(2.35,0);roofShape.closePath();const roofGeo=new T.ExtrudeGeometry(roofShape,{depth:3.4,bevelEnabled:false});mesh(tile,roofGeo,M.roof,[x,2.14,z-1.7]);B.add(G.box,M.house,[x+1,3.4,z-.5],[.48,1.5,.54]);B.add(G.box,M.stone,[x+1,4.17,z-.5],[.66,.16,.70]);B.add(G.box,M.door,[x-.2,.69,z+1.43],[.65,1.4,.09]);B.add(G.smallSphere,M.brass,[x+.02,.70,z+1.50],[.04,.04,.04]);for(const dx of [-1.28,1.22]){B.add(G.box,M.wood,[x+dx,1.3,z+1.44],[.86,.88,.09]);B.add(G.box,M.flower,[x+dx,1.3,z+1.51],[.63,.66,.04]);B.add(G.box,M.woodLight,[x+dx,1.3,z+1.55],[.06,.68,.03]);B.add(G.box,M.woodLight,[x+dx,1.3,z+1.55],[.68,.06,.03]);B.add(G.box,M.door,[x+dx, .82,z+1.61],[.94,.18,.29]);for(let j=0;j<5;j++)B.add(G.smallSphere,j%2?M.pink:M.flower,[x+dx-.35+j*.17,.96,z+1.63],[.09,.1,.09]);}for(let i=0;i<6;i++)B.add(G.box,M.stone,[x-.2,.065,z+2+i*.52],[.72,.13,.4],[0,rand(-.1,.1),0]);for(let i=0;i<12;i++){B.add(G.box,M.woodLight,[x-2.9+i*.52,.50,z+3.1],[.10,1.0,.10]);}B.add(G.box,M.woodLight,[x-.04,.62,z+3.1],[5.95,.08,.08]);tree(x-4,z+1,.85,0);tree(x+4,z,1.10,1);}67
cottage();68
// Route signs and a little roadside mailbox.69
const signTex=canvasTexture(512,160,(c,w,h)=>{c.fillStyle='#efdeb1';c.fillRect(0,0,w,h);c.strokeStyle='#71826a';c.lineWidth=5;c.strokeRect(9,9,w-18,h-18);c.fillStyle='#47634f';c.textAlign='center';c.font='bold 46px Georgia';c.fillText('RIVERSIDE →',w/2,72);c.font='19px sans-serif';c.fillText('TAKE THE SCENIC WAY',w/2,118);});70
const signM=new T.MeshStandardMaterial({map:signTex,roughness:1});B.add(G.box,M.wood,[-9,1.02,center(-9)+2.05],[.12,2.04,.12]);box(tile,M.woodLight,[-9,1.76,center(-9)+2.05],[1.9,.66,.14]);mesh(tile,new T.PlaneGeometry(1.78,.56),signM,[-9,1.76,center(-9)+2.127]);71
B.add(G.box,M.wood,[63,.71,center(63)+2.4],[.13,1.42,.13]);B.add(G.box,M.scarf,[63,1.44,center(63)+2.4],[.62,.43,.44]);B.add(G.box,M.chrome,[63,1.5,center(63)+2.65],[.3,.032,.016]);72
// Distant rounded hills have a soft, paper-like palette.73
const hillMats=['#97b397','#a9be9f','#b5c6ad'].map(c=>mat(c));for(let i=0;i<16;i++){B.add(G.leaf,hillMats[i%3],[-78+i*10+rand(-3,3),0,rand(-69,-42)],[rand(12,23),rand(4,8),rand(9,16)]);}74
B.finish();const landscapes=[];for(let i=-1;i<=1;i++){const o=i===0?tile:tile.clone();o.position.x=i*PERIOD;scene.add(o);landscapes.push({o,index:i});}75
// The bicycle and its rider share one gently leaning assembly.76
const vehicle=new T.Group();vehicle.position.y=.048;scene.add(vehicle);const bike=new T.Group(),otter=new T.Group();vehicle.add(bike,otter);77
const wheels=[];78
for(const x of [-1.14,1.14]){const w=new T.Group();w.position.set(x,.71,0);bike.add(w);wheels.push(w);torus(w,.645,.058,M.tire);torus(w,.616,.021,M.whitewall);torus(w,.562,.026,M.chrome);rod(w,[0,0,-.17],[0,0,.17],.063,M.brass);for(let n=0;n<24;n++){const a=n*TAU/24;rod(w,[0,0,n%2?.035:-.035],[Math.cos(a)*.559,Math.sin(a)*.559,0],.0075,M.chrome);}for(const z of [-.064,.064]){torus(w,.616,.025,M.whitewall,[0,0,z]);torus(w,.566,.013,M.brass,[0,0,z]);}}79
const rear=[-1.14,.71,0],bb=[-.12,.79,0],seat=[-.57,1.61,0],head=[.84,1.57,0];80
rod(bike,seat,bb,.057,M.bike);rod(bike,seat,head,.048,M.bike);rod(bike,bb,head,.059,M.bike);for(const z of [-.13,.13]){rod(bike,[-1.14,.71,z],[-.57,1.61,z*.3],.032,M.bike);rod(bike,[-1.14,.71,z],[-.12,.79,z*.3],.032,M.bike);rod(bike,[1.14,.71,z],[.84,1.57,z*.3],.036,M.bike);}rod(bike,[.87,1.5,0],[.70,1.99,0],.035,M.chrome);rod(bike,[-.57,1.6,0],[-.65,1.83,0],.031,M.chrome);ell(bike,M.seat,[-.64,1.84,0],[.35,.095,.22]);box(bike,M.brass,[-.62,1.76,0],[.25,.028,.17]);81
tube(bike,[[.69,1.95,-.37],[.84,2.0,-.28],[.84,2.06,0],[.84,2.0,.28],[.69,1.95,.37]],.028,M.chrome);for(const z of [-.37,.37])rod(bike,[.61,1.95,z],[.78,1.95,z],.044,M.seat);82
const bellModel=ell(bike,M.brass,[.82,2.11,.20],[.095,.06,.095]);rod(bike,[.92,1.61,0],[1.10,1.56,0],.032,M.chrome);ell(bike,M.brass,[1.12,1.55,0],[.082,.104,.102]);ell(bike,M.glint,[1.17,1.55,0],[.032,.079,.079]);83
// Both mudguards cover only the upper tyre arcs.84
for(const x of [-1.14,1.14]){const curve=[];for(let i=0;i<=32;i++){const a=.12+2.90*i/32;curve.push([x+Math.cos(a)*.708,.71+Math.sin(a)*.708,0]);}tube(bike,curve,.048,M.bike);rod(bike,[x,.71,.12],[x-.42,1.25,.12],.012,M.chrome);}85
// Chain and spinning crank; the angular speed follows rolling distance exactly.86
const crank=new T.Group();crank.position.set(...bb);bike.add(crank);torus(crank,.20,.02,M.chrome,[0,0,.14]);for(let n=0;n<5;n++){const a=n*TAU/5;rod(crank,[0,0,.15],[.19*Math.cos(a),.19*Math.sin(a),.15],.013,M.chrome);}rod(bike,[-1.14,.71,.16],[-1.14,.71,.22],.08,M.chrome);87
const chainPts=[[-.12,.995,.185],[-1.14,.80,.185],[-1.23,.71,.185],[-1.14,.625,.185],[-.12,.585,.185],[.08,.79,.185],[-.12,.995,.185]];tube(bike,chainPts,.015,M.tire);88
const pedalGroups=[];for(const side of [-1,1]){rod(crank,[0,0,side*.16],[side*.25,0,side*.22],.022,M.brass);const p=new T.Group();p.position.set(side*.25,0,side*.26);crank.add(p);box(p,M.seat,[0,0,0],[.20,.06,.13]);pedalGroups.push(p);}89
// Rear luggage carrier, straps, labels and a tied bow.90
for(const z of [-.21,.21]){rod(bike,[-1.14,.71,z],[-1.40,1.46,z],.016,M.chrome);rod(bike,[-1.14,.71,z],[-.64,1.46,z],.016,M.chrome);rod(bike,[-1.54,1.46,z],[-.57,1.46,z],.023,M.chrome);}for(let n=0;n<6;n++)rod(bike,[-1.49+n*.15,1.46,-.24],[-1.49+n*.15,1.46,.24],.014,M.chrome);91
const parcels=new T.Group();parcels.position.set(-1.38,1.49,0);bike.add(parcels);box(parcels,M.parcel,[0,.29,0],[.92,.58,.65],[0,0,.025]);box(parcels,M.parcel2,[-.08,.735,-.02],[.68,.31,.52],[0,.10,-.035]);for(const y of [.29,.735]){const dims=y===.29?[.94,.60,.67]:[.70,.33,.54];box(parcels,M.twine,[y===.29?0:-.08,y,0],[.029,dims[1]+.012,dims[2]+.012]);box(parcels,M.twine,[y===.29?0:-.08,y,0],[dims[0]+.01,.03,dims[2]+.01]);}mesh(parcels,new T.PlaneGeometry(.48,.28),labelMat,[-.14,.39,.333],null,[0,0,.04]);tube(parcels,[[-.10,.906,0],[-.33,.97,-.08],[-.32,1.03,.07],[-.10,.906,0],[.16,1,.09],[.16,1.015,-.1],[-.10,.906,0]],.016,M.twine);92
// A small front basket woven from individual ribs, with three daisies.93
const basket=new T.Group();basket.position.set(1.21,1.7,0);bike.add(basket);box(basket,M.woodLight,[0,-.18,0],[.46,.035,.50]);for(let j=0;j<5;j++){const yy=-.16+j*.082;tube(basket,[[-.23,yy,-.26],[.23,yy,-.26],[.27,yy,.26],[-.27,yy,.26],[-.23,yy,-.26]],.017,M.woodLight);}for(let i=0;i<7;i++){const z=-.24+i*.08;rod(basket,[-.25,-.19,z],[-.28,.19,z],.011,M.wood);rod(basket,[.25,-.19,z],[.28,.19,z],.011,M.wood);}for(let i=0;i<3;i++){const x=-.12+i*.13,z=(i-1)*.14;rod(basket,[x,-.14,z],[x+.07,.45+i*.06,z],.015,M.leafDark);ell(basket,M.yellow,[x+.07,.46+i*.06,z],[.045,.05,.045]);for(let j=0;j<5;j++){const a=j*TAU/5;ell(basket,M.flower,[x+.07+Math.cos(a)*.072,.46+i*.06+Math.sin(a)*.072,z+.005],[.053,.035,.023],[0,0,a]);}}94
// The otter: a warm, rounded silhouette, a cream muzzle and fine whiskers.95
ell(otter,M.fur,[-.49,2.40,0],[.59,.78,.45],[0,0,-.21]);ell(otter,M.cream,[-.07,2.46,0],[.23,.59,.345],[0,0,-.20]);ell(otter,M.furLight,[-.68,2.09,0],[.43,.34,.40]);96
const headGroup=new T.Group();headGroup.position.set(.03,3.22,0);otter.add(headGroup);ell(headGroup,M.fur,[0,0,0],[.60,.48,.50],[0,0,.02]);ell(headGroup,M.furLight,[.14,-.14,0],[.48,.26,.435]);for(const side of [-1,1]){ell(headGroup,M.fur,[-.21,.26,side*.424],[.12,.13,.082],[side*.22,0,-.16]);ell(headGroup,M.ear,[-.15,.285,side*.479],[.064,.069,.027],[0,0,-.2]);ell(headGroup,M.cream,[.43,-.15,side*.145],[.24,.14,.235]);}97
ell(headGroup,M.nose,[.608,-.064,0],[.079,.058,.10]);ell(headGroup,M.glint,[.638,-.035,.018],[.025,.011,.025]);98
const eyes=[];for(const side of [-1,1]){const eye=ell(headGroup,M.eye,[.265,.11,side*.404],[.082,.098,.046],[0,side*.35,0]);eyes.push(eye);ell(eye,M.glint,[.20,.27,side*.78],[.25,.23,.19]);ell(headGroup,M.furLight,[.24,.252,side*.405],[.11,.033,.042],[0,side*.2,-.10]);for(let j=0;j<3;j++){const a=j-1;tube(headGroup,[[.46,-.17,side*.23],[.43,-.18+a*.04,side*.42],[.36-.05*j,-.15+a*.11,side*.65]],.0065,M.twine);}for(let i=0;i<3;i++)ell(headGroup,M.fur,[.57,-.17-i*.023,side*(.18+i*.021)],[.009,.009,.009]);}99
tube(headGroup,[[.615,-.106,0],[.619,-.185,0],[.583,-.22,.075]],.009,M.nose);tube(headGroup,[[.619,-.185,0],[.583,-.22,-.075]],.009,M.nose);100
// Thick tapering tail, held to the side of the luggage.101
const tail=new T.Group();otter.add(tail);const tailCurve=new T.CatmullRomCurve3([v(-.82,2.03,.13),v(-1.02,1.86,.52),v(-1.46,1.57,.70),v(-1.83,1.30,.57),v(-2.02,1.16,.36)]);const tailGeo=new T.TubeGeometry(tailCurve,32,1,12,false);const tp=tailGeo.attributes.position;for(let i=0;i<=32;i++){const centerP=tailCurve.getPointAt(i/32),rr=.21*Math.pow(1-i/32,.7)+.018;for(let j=0;j<=12;j++){const idx=i*13+j,p=v().fromBufferAttribute(tp,idx);p.sub(centerP).multiplyScalar(rr).add(centerP);tp.setXYZ(idx,p.x,p.y,p.z);}}tailGeo.computeVertexNormals();mesh(tail,tailGeo,M.fur);102
// Two-segment limbs are solved against the real pedals and handlebar grips.103
const limbGeo=new T.CapsuleGeometry(1,2,6,12);104
const legs=[],arms=[];function limb(side,isArm){const group=new T.Group();otter.add(group);const upper=mesh(group,limbGeo,M.fur),lower=mesh(group,limbGeo,M.furLight);const joint=ell(group,M.furLight,[0,0,0],isArm?[.105,.115,.105]:[.14,.145,.13]);const paw=ell(group,M.furLight,[0,0,0],isArm?[.15,.105,.115]:[.23,.13,.14]);const toes=[];for(let i=0;i<3;i++)toes.push(ell(group,M.fur,[0,0,0],[.038,.021,.035]));return{side,group,upper,lower,joint,paw,toes};}for(const s of [-1,1]){legs.push(limb(s,false));arms.push(limb(s,true));}105
function setRod(o,a,b,r){const d=b.clone().sub(a);o.position.copy(a).add(b).multiplyScalar(.5);o.scale.set(r,d.length(),r);o.quaternion.setFromUnitVectors(v(0,1,0),d.normalize());}106
function setBone(o,a,b,r){setRod(o,a,b,r);o.scale.y=(a.distanceTo(b)+.12)/4;}107
function solveJoint(a,b,l1,l2,bend){const dir=b.clone().sub(a),len=Math.max(.001,dir.length());dir.divideScalar(len);const along=(len*len+l1*l1-l2*l2)/(2*len),h=Math.sqrt(Math.max(.005,l1*l1-along*along));const perpendicular=bend.clone().addScaledVector(dir,-bend.dot(dir)).normalize();return a.clone().addScaledVector(dir,along).addScaledVector(perpendicular,h);}108
// A double-sided scarf ribbon with a stitched stripe and fringed ends.109
torus(otter,.32,.11,M.scarf,[.04,2.88,0],[PI/2,0,0]);ell(otter,M.scarfLight,[-.23,2.84,.29],[.14,.14,.12]);const scarfSegments=32,scarfGeo=new T.BufferGeometry(),scarfPos=new Float32Array((scarfSegments+1)*3*3),scarfUV=new Float32Array((scarfSegments+1)*3*2),scarfIdx=[];for(let i=0;i<=scarfSegments;i++)for(let j=0;j<3;j++){const id=i*3+j;scarfUV[id*2]=i/scarfSegments;scarfUV[id*2+1]=j/2;if(i<scarfSegments&&j<2)scarfIdx.push(id,id+3,id+1,id+1,id+3,id+4);}scarfGeo.setAttribute('position',new T.BufferAttribute(scarfPos,3));scarfGeo.setAttribute('uv',new T.BufferAttribute(scarfUV,2));scarfGeo.setIndex(scarfIdx);const scarfTex=canvasTexture(256,64,(c,w,h)=>{c.fillStyle='#d9513d';c.fillRect(0,0,w,h);c.fillStyle='#efa579';c.fillRect(0,7,w,3);c.fillRect(0,h-10,w,3);c.fillStyle='#b94035';for(let x=0;x<w;x+=4){c.fillRect(x,0,1,h);}});const scarfMat=new T.MeshStandardMaterial({map:scarfTex,roughness:.95,side:T.DoubleSide});const scarfMesh=mesh(otter,scarfGeo,scarfMat);scarfMesh.frustumCulled=false;const fringes=[];for(let j=0;j<8;j++)fringes.push(rod(otter,[0,0,0],[.01,0,0],.008,j%2?M.scarfLight:M.scarf));110
// Soft contact shadow on the road supplements the sunlight.111
const shadowTex=canvasTexture(128,128,(c,w,h)=>{const g=c.createRadialGradient(64,64,0,64,64,62);g.addColorStop(0,'rgba(31,46,30,.34)');g.addColorStop(.5,'rgba(31,46,30,.15)');g.addColorStop(1,'rgba(31,46,30,0)');c.fillStyle=g;c.fillRect(0,0,w,h);});const contact=new T.Mesh(new T.PlaneGeometry(4.1,2.1),new T.MeshBasicMaterial({map:shadowTex,transparent:true,depthWrite:false}));contact.rotation.x=-PI/2;contact.position.y=.072;scene.add(contact);112
// Clouds, swallows, bobbing ducks and drifting seeds complete the river.113
const clouds=new T.Group();scene.add(clouds);const cloudMat=mat('#f9f7e6');for(let i=0;i<14;i++){const c=new T.Group();c.position.set(rand(-90,90),rand(17,30),rand(-64,-20));for(let j=0;j<5;j++)ell(c,cloudMat,[(j-2)*2,Math.sin(j)*.8,0],[rand(2,3.4),rand(1.0,1.5),rand(1.4,2.4)]);c.traverse(o=>{if(o.isMesh){o.castShadow=false;o.receiveShadow=false;}});clouds.add(c);}114
const skyGeo=new T.SphereGeometry(160,32,16);const skyMat=new T.ShaderMaterial({side:T.BackSide,depthWrite:false,uniforms:{uEvening:{value:0}},vertexShader:'varying vec3 vDir;void main(){vDir=position;gl_Position=projectionMatrix*modelViewMatrix*vec4(position,1.);}',fragmentShader:'varying vec3 vDir;uniform float uEvening;void main(){vec3 d=normalize(vDir);float h=smoothstep(-.06,.7,d.y);vec3 top=mix(vec3(.52,.75,.77),vec3(.61,.59,.65),uEvening);vec3 bottom=mix(vec3(.87,.91,.78),vec3(1.,.82,.60),uEvening);vec3 col=mix(bottom,top,h);float sun=pow(max(0.,dot(d,normalize(vec3(-.48,.36,-.7)))),90.);col+=sun*mix(vec3(.16,.12,.04),vec3(.30,.17,.04),uEvening);gl_FragColor=vec4(col,1.);\n#include <tonemapping_fragment>\n#include <colorspace_fragment>}'});const sky=new T.Mesh(skyGeo,skyMat);scene.add(sky);115
const birds=[];const birdMat=mat('#465c54');for(let i=0;i<7;i++){const b=new T.Group(),left=new T.Group(),right=new T.Group();ell(b,birdMat,[0,0,0],[.16,.09,.075]);b.add(left,right);const wingGeo=new T.ConeGeometry(.10,.48,3);mesh(left,wingGeo,birdMat,[0,0,.22],null,[PI/2,0,0]);mesh(right,wingGeo,birdMat,[0,0,-.22],null,[-PI/2,0,0]);scene.add(b);birds.push({b,left,right,phase:i*1.7});}116
const ducks=[];for(let i=0;i<3;i++){const d=new T.Group();ell(d,M.flower,[0,0,0],[.31,.21,.20]);ell(d,M.leafDark,[.22,.22,0],[.16,.18,.14]);ell(d,M.yellow,[.39,.21,0],[.11,.04,.069]);ell(d,M.eye,[.29,.255,.112],[.024,.024,.013]);ell(d,M.woodLight,[-.10,.09,.15],[.18,.075,.06]);const r=new T.Mesh(new T.RingGeometry(.40,.414,48),rippleMat);r.rotation.x=-PI/2;r.position.y=-.11;d.add(r);scene.add(d);ducks.push({d,x:5+i*1.2,z:river(5)-.5-i*.4});}117
const seedGeo=new T.SphereGeometry(.023,6,4),seedMat=new T.MeshBasicMaterial({color:'#fff2c8',transparent:true,opacity:.75});const pollen=new T.InstancedMesh(seedGeo,seedMat,65);pollen.instanceMatrix.setUsage(T.DynamicDrawUsage);scene.add(pollen);const dustData=Array.from({length:65},()=>({x:rand(-12,12),y:rand(.7,5),z:rand(-7,6),p:rand(0,TAU)}));const dustDummy=new T.Object3D();118
const butterflies=[];for(let i=0;i<4;i++){const b=new T.Group(),ws=[];for(const s of [-1,1]){const w=ell(b,i%2?M.yellow:M.pink,[0,0,s*.055],[.06,.025,.07]);ws.push(w);}scene.add(b);butterflies.push({b,ws,p:i*2.1});}119
const ringGeo=new T.TorusGeometry(.1,.012,6,48),ringMat=new T.MeshBasicMaterial({color:'#f0d58c',transparent:true,opacity:0});const bellRings=[0,1,2].map(()=>{const m=new T.Mesh(ringGeo,ringMat.clone());vehicle.add(m);return m;});120
// Camera orbit with inertial drag, pinch zoom and smooth view changes.121
let az=.33,elev=.25,radius=12.0,targetAz=.33,targetElev=.25,targetRadius=12.0,camTarget=v(0,1.72,0),targetY=1.72,view='follow';122
let pointerMap=new Map(),lastPinch=0,manual=false;const clamp=T.MathUtils.clamp;123
container.addEventListener('pointerdown',e=>{container.setPointerCapture(e.pointerId);pointerMap.set(e.pointerId,{x:e.clientX,y:e.clientY});lastPinch=0;});124
container.addEventListener('pointermove',e=>{if(!pointerMap.has(e.pointerId))return;const old=pointerMap.get(e.pointerId);if(pointerMap.size===1){targetAz-=(e.clientX-old.x)*.006;targetElev=clamp(targetElev+(e.clientY-old.y)*.004,.075,1.25);manual=true;}pointerMap.set(e.pointerId,{x:e.clientX,y:e.clientY});if(pointerMap.size===2){const a=[...pointerMap.values()],d=Math.hypot(a[0].x-a[1].x,a[0].y-a[1].y);if(lastPinch)targetRadius=clamp(targetRadius*lastPinch/d,6,26);lastPinch=d;}});125
for(const evt of ['pointerup','pointercancel','lostpointercapture'])container.addEventListener(evt,e=>{pointerMap.delete(e.pointerId);lastPinch=0;});container.addEventListener('wheel',e=>{e.preventDefault();targetRadius=clamp(targetRadius*Math.exp(e.deltaY*.0008),6,26);manual=true;},{passive:false});container.addEventListener('dblclick',()=>setView(view));126
function setView(name){view=name;manual=false;const portrait=innerWidth<650;const settings={follow:[.33,.25,portrait?15.5:11.8,1.72],close:[.63,.20,portrait?10.8:7.8,2.04],wide:[-.18,.52,portrait?24:24,1.0]};const s=settings[name];[targetAz,targetElev,targetRadius,targetY]=s;document.querySelectorAll('[data-view]').forEach(b=>{b.classList.toggle('active',b.dataset.view===name);b.setAttribute('aria-pressed',String(b.dataset.view===name));});}127
document.querySelectorAll('[data-view]').forEach(b=>b.onclick=()=>setView(b.dataset.view));setView('follow');az=targetAz;elev=targetElev;radius=targetRadius;128
let paused=matchMedia('(prefers-reduced-motion: reduce)').matches,speed=1,evening=false,eveningBlend=0,dist=0,time=0,crankAngle=0,bellAt=-100,ambient=false;129
const play=document.getElementById('play');function updatePlay(){play.innerHTML=paused?'<svg viewBox="0 0 16 16"><path d="M4 2l10 6-10 6z"/></svg>':'<svg viewBox="0 0 16 16"><path d="M4 3h3v10H4zM9 3h3v10H9z"/></svg>';play.setAttribute('aria-label',paused?'播放动画':'暂停动画');play.setAttribute('aria-pressed',String(paused));}function togglePlay(){paused=!paused;updatePlay();}play.onclick=togglePlay;updatePlay();130
document.getElementById('speed').oninput=e=>{speed=+e.target.value;document.getElementById('speedValue').textContent=speed.toFixed(1)+'×';};131
document.getElementById('light').onclick=()=>{evening=!evening;document.querySelector('#light span').textContent=evening?'黄昏':'午后';document.getElementById('light').setAttribute('aria-label',evening?'切换为午后':'切换为黄昏');toast(evening?'晚风拂过,包裹还在路上。':'阳光正好,慢慢骑。');};132
let toastTimer;function toast(s){const o=document.getElementById('toast');o.textContent=s;o.classList.add('show');clearTimeout(toastTimer);toastTimer=setTimeout(()=>o.classList.remove('show'),2600);}133
// Sound is synthesized in the file. Audio begins only after a user's gesture.134
let audioCtx,ambientGain;function audioStart(){if(!audioCtx){audioCtx=new(window.AudioContext||window.webkitAudioContext)();ambientGain=audioCtx.createGain();ambientGain.gain.value=0;ambientGain.connect(audioCtx.destination);const samples=audioCtx.sampleRate*4,b=audioCtx.createBuffer(1,samples,audioCtx.sampleRate),a=b.getChannelData(0);let prev=0;for(let i=0;i<samples;i++){prev=(prev+.025*(Math.random()*2-1))/1.025;a[i]=prev*.65;}const source=audioCtx.createBufferSource();source.buffer=b;source.loop=true;const f=audioCtx.createBiquadFilter();f.type='lowpass';f.frequency.value=750;source.connect(f).connect(ambientGain);source.start();}audioCtx.resume();}135
function ding(){try{audioStart();for(const delay of [0,.14])for(const [hz,amp]of[[1175,.15],[2360,.052],[3520,.025]]){const o=audioCtx.createOscillator(),g=audioCtx.createGain();o.type='sine';o.frequency.value=hz;g.gain.setValueAtTime(amp,audioCtx.currentTime+delay);g.gain.exponentialRampToValueAtTime(.001,audioCtx.currentTime+delay+1.6);o.connect(g).connect(audioCtx.destination);o.start(audioCtx.currentTime+delay);o.stop(audioCtx.currentTime+delay+1.7);}}catch(e){}bellAt=elapsed;toast('叮铃!借过一下,小邮差来了。');}136
document.getElementById('bell').onclick=ding;document.getElementById('sound').onclick=()=>{try{audioStart();ambient=!ambient;ambientGain.gain.setTargetAtTime(ambient?.7:0,audioCtx.currentTime,.45);const o=document.getElementById('sound');o.setAttribute('aria-pressed',String(ambient));o.setAttribute('aria-label',ambient?'关闭环境音':'开启环境音');o.style.background=ambient?'#dce6d8':'';o.innerHTML=ambient?'<svg viewBox="0 0 20 20"><path d="M3 8h4l5-4v12l-5-4H3zM15 7q3 3 0 6m2-9q5 6 0 12"/></svg>':'<svg viewBox="0 0 20 20"><path d="M3 8h4l5-4v12l-5-4H3zM15 7l3 6m0-6-3 6"/></svg>';toast(ambient?'听听河水与林间的声音。':'环境音已关闭');}catch(e){toast('当前浏览器无法开启声音。');}};137
document.getElementById('full').onclick=()=>{if(!document.fullscreenElement)document.documentElement.requestFullscreen?.().catch(()=>toast('可使用浏览器的全屏功能。'));else document.exitFullscreen?.();};138
window.addEventListener('keydown',e=>{if(e.target instanceof HTMLInputElement&&e.target.type!=='range')return;if(e.code==='Space'){e.preventDefault();togglePlay();}if(e.code==='KeyB')ding();if(e.code==='KeyR'){setView('follow');toast('回到随行视角');}});139
function chirp(){if(!ambient||!audioCtx)return;const t=audioCtx.currentTime;for(let i=0;i<3;i++){const o=audioCtx.createOscillator(),g=audioCtx.createGain();o.frequency.setValueAtTime(2600+i*180,t+i*.12);o.frequency.exponentialRampToValueAtTime(3700-i*220,t+i*.12+.075);g.gain.setValueAtTime(0,t+i*.12);g.gain.linearRampToValueAtTime(.018,t+i*.12+.013);g.gain.exponentialRampToValueAtTime(.0001,t+i*.12+.09);o.connect(g).connect(audioCtx.destination);o.start(t+i*.12);o.stop(t+i*.12+.10);}}140
let nextChirp=6;141
function updateScarf(t){const wind=.85+speed*.3;for(let i=0;i<=scarfSegments;i++){const u=i/scarfSegments,fall=.12*u+.35*u*u;for(let j=0;j<3;j++){const edge=j/2-.5,id=(i*3+j)*3;const phase=t*6.0-u*9;scarfPos[id]=-.27-u*1.63;scarfPos[id+1]=2.86-fall+.16*Math.sin(phase)*u+.05*Math.sin(phase*1.5)*u+edge*.23*Math.cos(u*3+t*2)*u;scarfPos[id+2]=.26+u*.21+.16*wind*Math.sin(phase*.78)*u+edge*(.27-.055*u);}}scarfGeo.attributes.position.needsUpdate=true;scarfGeo.computeVertexNormals();for(let j=0;j<8;j++){const edge=j/7-.5,u=1,phase=t*6-9,a=v(-1.90,2.39+.16*Math.sin(phase)+.05*Math.sin(phase*1.5)+edge*.23*Math.cos(3+t*2),.47+.16*wind*Math.sin(phase*.78)+edge*.215),b=a.clone().add(v(-.11-.02*Math.sin(t*7+j),-.01,.035*Math.sin(t*7+j)));setRod(fringes[j],a,b,.007);}}142
function updateOtter(t,phase){const bob=.017*Math.sin(phase*2);otter.position.y=bob;headGroup.rotation.z=.018*Math.sin(t*1.2)+.012*Math.sin(phase*2);headGroup.rotation.y=.04*Math.sin(t*.55);tail.rotation.x=.045*Math.sin(t*2.0);tail.rotation.y=.03*Math.sin(t*1.8);for(const leg of legs){const s=leg.side,a=phase+(s===-1?PI:0),foot=v(-.12+.25*Math.cos(a),.91+.25*Math.sin(a),s*.30);const hip=v(-.60,1.96,s*.27),knee=solveJoint(hip,foot,.72,.79,v(1,.1,s*.14));setBone(leg.upper,hip,knee,.145);setBone(leg.lower,knee,foot,.105);leg.joint.position.copy(knee);leg.paw.position.copy(foot).add(v(.05,.015,0));leg.paw.rotation.z=.10*Math.sin(a);for(let i=0;i<3;i++)leg.toes[i].position.copy(leg.paw.position).add(v(.16,.0,(i-1)*.074));}for(const arm of arms){const s=arm.side,shoulder=v(-.11,2.63,s*.34),hand=v(.68,1.985,s*.37),elbow=solveJoint(shoulder,hand,.53,.53,v(.1,.2,s*.7));setBone(arm.upper,shoulder,elbow,.12);setBone(arm.lower,elbow,hand,.092);arm.joint.position.copy(elbow);arm.paw.position.copy(hand).add(v(.013,.035,0));for(let i=0;i<3;i++)arm.toes[i].position.copy(arm.paw.position).add(v(.092,.012,(i-1)*.052));}const blinkTime=t%5.2,blink=blinkTime>4.8&&blinkTime<4.98?Math.max(.08,Math.abs(blinkTime-4.89)/.09):1;eyes.forEach(e=>e.scale.y=.098*blink);updateScarf(t);}143
let last=performance.now(),elapsed=0;144
function resize(){renderer.setSize(innerWidth,innerHeight);camera.aspect=innerWidth/innerHeight;camera.updateProjectionMatrix();if(!manual)setView(view);}window.addEventListener('resize',resize);145
function animate(now){requestAnimationFrame(animate);const realDt=Math.min((now-last)/1000,.04);last=now;elapsed+=realDt;const dt=paused?0:realDt;time+=dt;dist+=dt*2.0*speed;crankAngle-=dt*2.0*speed/.645*.47;const phase=crankAngle;146
const local=dist%PERIOD,z=center(local),yaw=-Math.atan(slope(local));vehicle.position.z=z;vehicle.rotation.y=yaw;vehicle.rotation.x=.015*Math.sin(phase*2);vehicle.rotation.z=.005*Math.sin(phase*2);contact.position.z=z;contact.rotation.z=yaw;147
for(const item of landscapes)item.o.position.x=item.index*PERIOD-local;for(const wheel of wheels)wheel.rotation.z=-dist/.645;crank.rotation.z=phase;for(const p of pedalGroups)p.rotation.z=-phase;parcels.rotation.x=.012*Math.sin(phase*2);parcels.rotation.z=.007*Math.sin(phase*2);updateOtter(time,phase);148
waterMat.uniforms.uTime.value=time;clouds.position.x=-Math.sin(time*.005)*15;sky.position.copy(camera.position);149
for(const [i,d]of ducks.entries()){let x=(d.x-local+80)%PERIOD;if(x<0)x+=PERIOD;x-=80;d.d.position.set(x,-.07+.021*Math.sin(time*2+i),d.z);d.d.rotation.y=.3+.10*Math.sin(time*.8+i);d.d.rotation.z=.018*Math.sin(time*2+i);}150
birds.forEach((o,i)=>{const p=time*.13+o.phase;o.b.position.set(14*Math.sin(p),8.5+i*.40+Math.sin(p*2)*.5,-17+6*Math.cos(p));o.b.rotation.y=-p;o.left.rotation.x=.65*Math.sin(time*8+o.phase);o.right.rotation.x=-.65*Math.sin(time*8+o.phase);});151
butterflies.forEach((o,i)=>{const p=time*.7+o.p;o.b.position.set(3*Math.sin(p)+i*2-2,1.0+.25*Math.sin(p*2),z+2.7+Math.cos(p*.8));o.b.rotation.y=-p;o.ws[0].rotation.x=Math.sin(time*19+i)*.8;o.ws[1].rotation.x=-Math.sin(time*19+i)*.8;});152
dustData.forEach((o,i)=>{dustDummy.position.set(((o.x-time*.22+12)%24+24)%24-12,o.y+.17*Math.sin(time+o.p),o.z+.2*Math.sin(time*.6+o.p));dustDummy.scale.setScalar(.7+.3*Math.sin(time+o.p));dustDummy.updateMatrix();pollen.setMatrixAt(i,dustDummy.matrix);});pollen.instanceMatrix.needsUpdate=true;153
for(let i=0;i<3;i++){const t=elapsed-bellAt-i*.10,m=bellRings[i];m.visible=t>=0&&t<1.3;if(m.visible){m.position.set(.86,2.18,.21);m.scale.setScalar(1+t*10);m.quaternion.copy(camera.quaternion);m.material.opacity=(1-t/1.3)*.6;}}154
const blend=1-Math.exp(-realDt*2);eveningBlend+=((evening?1:0)-eveningBlend)*blend;waterMat.uniforms.uEvening.value=eveningBlend;skyMat.uniforms.uEvening.value=eveningBlend;scene.fog.color.set('#d9e9df').lerp(new T.Color('#e9c59f'),eveningBlend);hemi.color.set('#eaf6ff').lerp(new T.Color('#f4d9bb'),eveningBlend);hemi.intensity=2.25-.40*eveningBlend;sun.color.set('#fff1c9').lerp(new T.Color('#ffc287'),eveningBlend);sun.intensity=3.1-.45*eveningBlend;sun.position.set(-12,21-9*eveningBlend,z+11);sun.target.position.set(0,1,z);155
const smooth=1-Math.exp(-realDt*4.6);az+=(targetAz-az)*smooth;elev+=(targetElev-elev)*smooth;radius+=(targetRadius-radius)*smooth;camTarget.lerp(v(.0,targetY,z),smooth);const adapt=innerWidth<650?1.03:1;camera.position.set(camTarget.x+Math.sin(az)*Math.cos(elev)*radius*adapt,camTarget.y+Math.sin(elev)*radius,camTarget.z+Math.cos(az)*Math.cos(elev)*radius*adapt);camera.lookAt(camTarget);sky.position.copy(camera.position);156
if(time>nextChirp){chirp();nextChirp=time+7+Math.random()*9;}document.getElementById('distance').textContent=(dist/1000).toFixed(2);renderer.render(scene,camera);157
}158
for(const wheel of wheels)mergeStatic(wheel);mergeStatic(crank,pedalGroups);mergeStatic(parcels);mergeStatic(bike,[...wheels,crank,parcels]);mergeStatic(headGroup,eyes);mergeStatic(otter,[headGroup,tail,scarfMesh,...fringes,...legs.map(x=>x.group),...arms.map(x=>x.group)]);159
updateOtter(0,0);requestAnimationFrame(animate);setTimeout(()=>document.getElementById('loading').classList.add('hide'),350);160
document.addEventListener('visibilitychange',()=>{last=performance.now();});161
// A small read-only diagnostics surface aids offline validation.162
window.__riverPost={get state(){return{paused,speed,evening,dist,time,view,camera:[camera.position.x,camera.position.y,camera.position.z],three:T.REVISION,calls:renderer.info.render.calls,triangles:renderer.info.render.triangles};}};163
})();