1
<!doctype html>2
<html lang="zh-CN">3
<head>4
<meta charset="utf-8">5
<meta name="viewport" content="width=device-width,initial-scale=1,viewport-fit=cover">6
<meta name="theme-color" content="#edf1df">7
<title>河岸邮差 · A Little Delivery</title>8
<style>9
:root{color-scheme:light;--ink:#304d43;--paper:#faf8ec;--line:#38504426;--accent:#ce6951}*{box-sizing:border-box}html,body{margin:0;width:100%;height:100%;overflow:hidden}body{background:#e6eddc;font-family:"Segoe UI","Microsoft YaHei",sans-serif;color:var(--ink)}button,input{font:inherit}button{color:inherit;cursor:pointer;-webkit-tap-highlight-color:transparent}button:focus-visible,input:focus-visible{outline:2px solid #ce6951;outline-offset:5px}#stage{position:fixed;inset:0;touch-action:none;cursor:grab}#stage.dragging{cursor:grabbing}canvas{display:block;width:100%;height:100%}.vignette{position:fixed;inset:0;pointer-events:none;background:radial-gradient(ellipse at 50% 45%,transparent 48%,#6b794c12 100%)}header{position:fixed;left:42px;top:34px;pointer-events:none;user-select:none}header::before{content:"";position:absolute;inset:-50px -95px -42px -50px;background:radial-gradient(ellipse at 25% 28%,#f5f5e399,transparent 72%);z-index:-1;pointer-events:none}.edition{display:flex;gap:11px;align-items:center;font-size:10px;font-weight:650;letter-spacing:3px;text-transform:uppercase}.stamp{width:27px;height:27px;border:1px solid #42604d66;border-radius:50%;display:grid;place-items:center;transform:rotate(-13deg)}.stamp svg{width:17px;height:17px}h1{font-family:"STSong","SimSun",serif;font-weight:500;font-size:42px;letter-spacing:9px;margin:17px 0 10px}header p{font-size:12px;letter-spacing:2px;margin:0;opacity:.72}.chapter{position:fixed;right:39px;top:39px;text-align:right;font-size:10px;letter-spacing:2px;line-height:1.9}.chapter b{font-weight:500;font-size:14px}.chapter i{display:inline-block;width:5px;height:5px;border-radius:50%;background:#729b75;margin-right:7px;vertical-align:2px}.top-tools{position:fixed;right:37px;top:106px;display:flex;gap:8px}.round{border:1px solid var(--line);background:#faf8ecb8;backdrop-filter:blur(12px);width:38px;height:38px;border-radius:50%;display:grid;place-items:center;transition:background .2s,transform .2s}.round:hover{background:var(--paper);transform:translateY(-2px)}svg{width:18px;height:18px;fill:none;stroke:currentColor;stroke-width:1.6;stroke-linecap:round;stroke-linejoin:round}.caption{position:fixed;bottom:132px;left:43px;pointer-events:none;font-family:"STSong","SimSun",serif;font-size:15px;letter-spacing:3px;line-height:1.9}.caption small{font-family:"Segoe UI",sans-serif;display:block;font-size:9px;letter-spacing:2px;opacity:.6;margin-top:6px}.controls{position:fixed;bottom:31px;left:50%;transform:translateX(-50%);display:flex;align-items:center;gap:19px;padding:13px 19px 13px 13px;border:1px solid #ffffff99;border-radius:25px;background:#faf8ecd9;backdrop-filter:blur(18px);box-shadow:0 8px 30px #4a594615;white-space:nowrap;max-width:calc(100% - 32px)}.play{border:0;border-radius:50%;background:var(--ink);color:var(--paper);width:44px;height:44px;display:grid;place-items:center;flex-shrink:0}.play:hover{background:#47715d}.play svg{width:17px;height:17px}.divider{height:28px;width:1px;background:var(--line)}.speed-control{display:grid;gap:7px;min-width:118px}.speed-label{font-size:10px;display:flex;justify-content:space-between;letter-spacing:1px}.speed-label output{font-size:10px;font-variant-numeric:tabular-nums;opacity:.64}input[type=range]{appearance:none;-webkit-appearance:none;width:120px;height:3px;background:#80968055;border-radius:4px;margin:2px 0;cursor:pointer}input[type=range]::-webkit-slider-thumb{appearance:none;width:11px;height:11px;background:var(--ink);border-radius:50%;border:2px solid var(--paper);box-shadow:0 0 0 1px var(--ink)}input[type=range]::-moz-range-thumb{width:9px;height:9px;background:var(--ink);border-radius:50%;border:2px solid var(--paper)}.text-button{display:flex;align-items:center;gap:7px;padding:6px 0;border:0;background:none;font-size:11px;letter-spacing:1px}.text-button:hover{color:var(--accent)}.text-button.active{color:var(--accent)}.view-button{min-width:75px}.hint{position:fixed;bottom:43px;left:42px;font-size:10px;letter-spacing:1px;opacity:.65;line-height:1.9;pointer-events:none}.distance{position:fixed;right:40px;bottom:41px;text-align:right;font-size:10px;letter-spacing:2px;pointer-events:none}.distance strong{display:block;font-size:21px;letter-spacing:1px;font-weight:350;font-variant-numeric:tabular-nums;margin-bottom:4px}.distance span{opacity:.65}#toast{position:fixed;left:50%;top:25%;transform:translate(-50%,-8px);background:#faf8ecdf;backdrop-filter:blur(10px);padding:11px 20px;border-radius:25px;font-size:12px;letter-spacing:2px;opacity:0;transition:opacity .4s,transform .4s;pointer-events:none}#toast.show{opacity:1;transform:translate(-50%,0)}#loader{position:fixed;inset:0;background:#edf1df;display:flex;align-items:center;justify-content:center;flex-direction:column;gap:20px;z-index:10;transition:opacity .7s;font-family:"STSong","SimSun",serif;letter-spacing:4px;font-size:20px}#loader span{font-family:"Segoe UI",sans-serif;font-size:10px;letter-spacing:3px;opacity:.55}#loader.hide{opacity:0;pointer-events:none}.spinner{width:34px;height:34px;border:1px solid #80968066;border-top-color:#42604d;border-radius:50%;animation:spin 1.6s linear infinite}@keyframes spin{to{transform:rotate(360deg)}}#help{position:fixed;right:37px;top:157px;width:247px;padding:23px;background:#faf8ecef;border:1px solid #fff9;border-radius:20px;box-shadow:0 10px 35px #4a59461a;backdrop-filter:blur(15px);font-size:12px;line-height:2;display:none}#help.open{display:block}#help strong{font-family:"STSong","SimSun",serif;font-size:17px;letter-spacing:2px;display:block;margin-bottom:10px;font-weight:500}#help p{margin:5px 0;font-size:11px}#help kbd{font-family:inherit;font-size:10px;background:#e4e9db;border-radius:4px;padding:2px 5px;margin-right:5px}.postmark{position:fixed;right:41px;bottom:115px;opacity:.36;width:68px;height:68px;border:1px solid;border-radius:50%;display:flex;flex-direction:column;align-items:center;justify-content:center;transform:rotate(12deg);pointer-events:none;font-size:8px;letter-spacing:1.5px}.postmark b{font-family:serif;font-size:20px;font-weight:400;margin:2px}.sound-label{min-width:45px}@media(max-width:900px){.hint{display:none}.caption{bottom:132px;left:25px}.distance{right:25px;bottom:132px}.postmark{display:none}header{left:25px;top:25px}.chapter{right:25px;top:26px}.top-tools{right:24px;top:89px}h1{font-size:35px}.controls{bottom:23px;gap:14px}#help{right:25px;top:138px}}@media(max-width:580px){header{left:21px;top:23px}h1{font-size:31px;letter-spacing:6px;margin:14px 0 8px}.edition{font-size:8px;letter-spacing:2px}header p{font-size:10px;letter-spacing:1px}.chapter{top:27px;right:19px;font-size:8px;letter-spacing:1px}.chapter b{font-size:11px}.top-tools{right:17px;top:79px;flex-direction:column}.round{width:33px;height:33px}.controls{gap:11px;border-radius:22px;padding:11px;bottom:18px}.play{width:38px;height:38px}.speed-control{min-width:78px}input[type=range]{width:78px}.text-button{font-size:10px;gap:4px}.text-button svg{width:16px}.controls .divider{height:24px}.sound-label{display:none}.view-button{min-width:61px}.caption{font-size:12px;letter-spacing:2px;bottom:113px;left:23px}.caption small{font-size:8px}.distance{bottom:115px;right:22px;font-size:8px}.distance strong{font-size:18px}#help{right:18px;top:200px;width:229px}}@media(prefers-reduced-motion:reduce){.spinner{animation:none}}10
@media(max-width:380px){.controls{gap:8px}.speed-control{min-width:64px}input[type=range]{width:64px}.view-button{min-width:55px}#bell span{display:none}.controls .divider{display:none}}11
</style>12
</head>13
<body>14
<div id="stage" role="img" aria-label="一只戴红围巾的水獭,骑着薄荷绿自行车运送包裹,沿开满野花的河岸前进。拖动可旋转视角。"></div><div class="vignette"></div>15
<header><div class="edition"><span class="stamp"><svg viewBox="0 0 24 24"><path d="M3 6h18v12H3zM3 6l9 7 9-7"/></svg></span> LITTLE WANDERINGS · 01</div><h1>河岸邮差</h1><p>把日子放慢,把心意送达。</p></header>16
<div class="chapter"><i></i>沿河慢行<br><b>A LITTLE DELIVERY</b></div>17
<div class="top-tools"><button class="round" id="reset" title="重置视角(R)" aria-label="重置视角"><svg viewBox="0 0 24 24"><path d="M4 10a8 8 0 1 1 2 8M4 4v6h6"/></svg></button><button class="round" id="capture" title="保存明信片" aria-label="保存明信片"><svg viewBox="0 0 24 24"><path d="M4 7h4l2-3h4l2 3h4v13H4z"/><circle cx="12" cy="13" r="4"/></svg></button><button class="round" id="help-button" title="操作说明" aria-label="操作说明" aria-expanded="false"><svg viewBox="0 0 24 24"><path d="M9 8a3 3 0 0 1 6 0c0 2-3 2-3 5M12 17h.01"/></svg></button></div>18
<div class="caption">风经过围巾,<br>故事经过河岸。<small>A SOFT BREEZE. A SMALL JOURNEY.</small></div>19
<div class="postmark">RIVERSIDE<b>邮</b>SPECIAL DELIVERY</div>20
<div class="controls" aria-label="动画控制"><button class="play" id="play" title="暂停 / 继续(空格)" aria-label="暂停骑行"><svg viewBox="0 0 24 24" id="play-icon"><path d="M8 5v14M16 5v14" stroke-width="3"/></svg></button><div class="speed-control"><label class="speed-label" for="speed">骑行速度 <output id="speed-value">1.0×</output></label><input type="range" id="speed" min="0.35" max="2" step="0.05" value="1" aria-label="骑行速度"></div><div class="divider"></div><button class="text-button view-button" id="view" title="切换镜头(V)"><svg viewBox="0 0 24 24"><path d="M3 8V4h4M17 4h4v4M21 16v4h-4M7 20H3v-4"/><circle cx="12" cy="12" r="3"/></svg><span>沿河跟拍</span></button><button class="text-button" id="bell" title="按响车铃(B)" aria-label="按响车铃"><svg viewBox="0 0 24 24"><path d="M4 17h16l-2-3V9a6 6 0 0 0-12 0v5zM10 21h4M12 3V1"/></svg><span>叮铃</span></button><div class="divider"></div><button class="text-button" id="sound" aria-pressed="false" title="自然环境声"><svg viewBox="0 0 24 24"><path d="M3 9h4l5-4v14l-5-4H3zM16 9l5 6M21 9l-5 6"/></svg><span class="sound-label">环境声</span></button></div>21
<div class="hint">拖动环顾 · 滚轮缩放<br>空格暂停 · 点击水獭打招呼</div><div class="distance"><strong id="distance">0000 m</strong><span>心意,在路上</span></div>22
<aside id="help"><strong>一起,慢慢骑。</strong><p>拖动场景环顾,滚轮或双指缩放。</p><p>点击水獭,它会向你点头问好。</p><p><kbd>空格</kbd> 暂停 / 继续 <kbd>B</kbd> 车铃</p><p><kbd>V</kbd> 切换镜头 <kbd>R</kbd> 重置视角</p><p>相机按钮可保存当前画面为明信片。</p><p>全部内容已内置,可以离线欣赏。</p></aside><div id="toast" role="status"></div>23
<div id="loader"><div class="spinner"></div>一份心意,正在出发<span>RIVERSIDE POST</span></div>24
<script>/* THREE.JS 0.160.1 | MIT License | https://threejs.org */25
__THREE__26
</script>27
<script>28
'use strict';29
(()=>{30
const $=id=>document.getElementById(id), TAU=Math.PI*2;31
let renderer;try{renderer=new THREE.WebGLRenderer({antialias:true,alpha:false,powerPreference:'high-performance',preserveDrawingBuffer:true});}catch(error){$('loader').innerHTML='画面暂时无法启动<span>请用启用图形加速的现代浏览器打开此文件</span>';return;}32
renderer.setPixelRatio(Math.min(devicePixelRatio,1.7));renderer.setSize(innerWidth,innerHeight);renderer.shadowMap.enabled=true;renderer.shadowMap.type=THREE.PCFSoftShadowMap;renderer.outputColorSpace=THREE.SRGBColorSpace;renderer.toneMapping=THREE.ACESFilmicToneMapping;renderer.toneMappingExposure=1.14;$('stage').appendChild(renderer.domElement);33
const scene=new THREE.Scene();scene.background=new THREE.Color('#d9e7dd');scene.fog=new THREE.Fog('#d9e7dd',24,65);34
const camera=new THREE.PerspectiveCamera(36,innerWidth/innerHeight,.1,150);35
const hemi=new THREE.HemisphereLight('#f6f5df','#879369',2.5);scene.add(hemi);36
const sun=new THREE.DirectionalLight('#fff0ce',3.3);sun.position.set(-3,12,7);sun.castShadow=true;sun.shadow.mapSize.set(2048,2048);Object.assign(sun.shadow.camera,{left:-14,right:14,top:14,bottom:-14,near:.5,far:38});sun.shadow.bias=-.00018;sun.shadow.normalBias=.025;sun.shadow.radius=3;scene.add(sun);sun.target.position.set(0,0,1);scene.add(sun.target);37
const fill=new THREE.DirectionalLight('#deecf1',.65);fill.position.set(7,4,-9);scene.add(fill);38
const mat=(color,roughness=.82,metalness=0)=>new THREE.MeshStandardMaterial({color,roughness,metalness});39
const M={fur:mat('#83523b'),furLight:mat('#986445'),cream:mat('#ddbf8d'),muzzle:mat('#f2d7a6'),dark:mat('#302e28',.38),nose:mat('#332921',.32),blush:mat('#c3886e'),mint:mat('#53938a',.44,.28),mintLight:mat('#8cb9a0',.48,.18),silver:mat('#b7beb0',.4,.62),tire:mat('#3b4941'),tireSide:mat('#e0d3ad'),saddle:mat('#75513a'),scarf:mat('#c85740',.86),scarfLight:mat('#efac78'),box:mat('#d4a56b'),box2:mat('#b67e4f'),tape:mat('#e8c492'),white:mat('#fff2cf'),wood:mat('#846341'),woodLight:mat('#ba9366'),grass:mat('#89a574'),grassLight:mat('#aabc83'),grassDark:mat('#608769'),leaves:mat('#6f976e'),leavesLight:mat('#91ad76'),leavesDark:mat('#548568'),trunk:mat('#847157'),stone:mat('#a6ac92'),yellow:mat('#e5bd60'),flower:mat('#e6a895'),petal:mat('#faf0cb')};40
const sphereGeo=new THREE.SphereGeometry(1,24,16), lowSphere=new THREE.SphereGeometry(1,12,8), boxGeo=new THREE.BoxGeometry(1,1,1), cylGeo=new THREE.CylinderGeometry(1,1,1,12), leafGeo=new THREE.IcosahedronGeometry(1,1);41
function mesh(g,m,parent,pos=[0,0,0],scale=[1,1,1]){const o=new THREE.Mesh(g,m);o.position.set(...pos);o.scale.set(...scale);o.castShadow=true;o.receiveShadow=true;parent.add(o);return o;}42
function ell(parent,m,p,s,low=false){return mesh(low?lowSphere:sphereGeo,m,parent,p,s);}43
function box(parent,m,p,s){return mesh(boxGeo,m,parent,p,s);}44
function rod(parent,m,a,b,r=.04){const o=mesh(cylGeo,m,parent);placeRod(o,a,b,r);return o;}45
const tempV=new THREE.Vector3(),up=new THREE.Vector3(0,1,0);46
function placeRod(o,a,b,r,rz=r){tempV.set(b[0]-a[0],b[1]-a[1],b[2]-a[2]);o.position.set((a[0]+b[0])/2,(a[1]+b[1])/2,(a[2]+b[2])/2);o.scale.set(r,tempV.length(),rz);o.quaternion.setFromUnitVectors(up,tempV.normalize());}47
function tube(parent,m,points,r=.03,segments=30){return mesh(new THREE.TubeGeometry(new THREE.CatmullRomCurve3(points.map(p=>new THREE.Vector3(...p))),segments,r,8,false),m,parent);}48
function torus(parent,m,p,r,t=.035,arc=TAU){const o=mesh(new THREE.TorusGeometry(r,t,8,56,arc),m,parent,p);return o;}49
function group(parent,p=[0,0,0]){const g=new THREE.Group();g.position.set(...p);parent.add(g);return g;}50
let seed=72;function rand(){seed=(1664525*seed+1013904223)>>>0;return seed/4294967296;}function range(a,b){return a+(b-a)*rand();}51
const L=48;52
function pathZ(x){return 2.2+.5*Math.sin(x*TAU/L);}53
function riverNear(x){return -1.65+.45*Math.sin(x*TAU/L+.7);}54
function riverFar(x){return -7.6+.65*Math.sin(x*TAU/L-.3);}55
function strip(parent,m,x0,x1,z1,z2,yFn,segments=96,widthSteps=1){const p=[],uv=[],ix=[];for(let i=0;i<=segments;i++){let x=x0+(x1-x0)*i/segments;for(let j=0;j<=widthSteps;j++){let v=j/widthSteps,z=z1(x)+(z2(x)-z1(x))*v;p.push(x,yFn(x,z,v),z);uv.push(i/segments,v);if(i<segments&&j<widthSteps){let a=i*(widthSteps+1)+j,b=a+widthSteps+1;ix.push(a,a+1,b,b,a+1,b+1);}}}const g=new THREE.BufferGeometry();g.setAttribute('position',new THREE.Float32BufferAttribute(p,3));g.setAttribute('uv',new THREE.Float32BufferAttribute(uv,2));g.setIndex(ix);g.computeVertexNormals();return mesh(g,m,parent);}56
// Merge scenery by material so individual leaves do not each need a draw call.57
function bake(root){root.updateMatrixWorld(true);const buckets=new Map(),remove=[];root.traverse(o=>{if(!o.isMesh)return;const key=o.material.uuid;let b=buckets.get(key);if(!b){b={mat:o.material,p:[],n:[],uv:[],idx:[],count:0};buckets.set(key,b);}const g=o.geometry.clone();g.applyMatrix4(o.matrixWorld);const pos=g.attributes.position,normal=g.attributes.normal,uv=g.attributes.uv;for(let i=0;i<pos.count;i++){b.p.push(pos.getX(i),pos.getY(i),pos.getZ(i));b.n.push(normal.getX(i),normal.getY(i),normal.getZ(i));b.uv.push(uv?uv.getX(i):0,uv?uv.getY(i):0);}if(g.index){for(let i=0;i<g.index.count;i++)b.idx.push(g.index.getX(i)+b.count);}else for(let i=0;i<pos.count;i++)b.idx.push(i+b.count);b.count+=pos.count;g.dispose();remove.push(o);});root.clear();for(const b of buckets.values()){const g=new THREE.BufferGeometry();g.setAttribute('position',new THREE.Float32BufferAttribute(b.p,3));g.setAttribute('normal',new THREE.Float32BufferAttribute(b.n,3));g.setAttribute('uv',new THREE.Float32BufferAttribute(b.uv,2));g.setIndex(b.idx);g.computeBoundingSphere();mesh(g,b.mat,root);}}58
function grassClump(parent,x,z,s=1){const p=[],ix=[];for(let k=0;k<5;k++){const a=k*2.4,bx=Math.cos(a)*.065*s,bz=Math.sin(a)*.065*s,h=(.2+rand()*.22)*s,n=p.length/3;p.push(x+bx-.025*s,.17,z+bz,x+bx+.025*s,.17,z+bz,x+bx+Math.cos(a)*.14*s,.17+h,z+bz+Math.sin(a)*.1*s);ix.push(n,n+2,n+1,n+1,n+2,n);}const g=new THREE.BufferGeometry();g.setAttribute('position',new THREE.Float32BufferAttribute(p,3));g.setIndex(ix);g.computeVertexNormals();mesh(g,rand()>.5?M.grassDark:M.grassLight,parent);}59
function flower(parent,x,z,s=1,col=M.petal){const h=range(.25,.55)*s;rod(parent,M.grassDark,[x,.16,z],[x+.05,h+.16,z],.014*s);ell(parent,M.yellow,[x+.05,h+.17,z],[.055*s,.044*s,.055*s],true);for(let k=0;k<5;k++){const a=k*TAU/5;ell(parent,col,[x+.05+Math.cos(a)*.074*s,h+.17,z+Math.sin(a)*.074*s],[.068*s,.026*s,.043*s],true).rotation.y=-a;}if(rand()>.55)ell(parent,M.grassDark,[x+.07,h*.5+.16,z],[.12*s,.025*s,.045*s],true).rotation.z=.4;}60
function tree(parent,x,z,s=1){const g=group(parent,[x,.14,z]);const h=3.8*s;rod(g,M.trunk,[0,0,0],[.06,h,0],.14*s);for(let i=0;i<5;i++){let a=i*2.4,yy=h*(.5+i*.09),ex=Math.cos(a)*.8*s,ez=Math.sin(a)*.75*s;rod(g,M.trunk,[0,yy-.6*s,0],[ex,yy+.38*s,ez],.06*s);ell(g,i%2?M.leaves:M.leavesLight,[ex,yy+.65*s,ez],[.97*s,.91*s,.9*s],true);}ell(g,M.leaves,[.05,h+.2*s,.04],[1.0*s,1.1*s,.95*s],true);}61
function shrub(parent,x,z,s=1){for(let i=0;i<4;i++)ell(parent,i%2?M.leaves:M.leavesLight,[x+(i-1.5)*.23*s,.2+(i%2)*.12*s,z+Math.sin(i)*.16*s],[.39*s,.34*s,.38*s],true);}62
function rock(parent,x,z,s){const o=mesh(leafGeo,M.stone,parent,[x,.12,z],[s,s*.55,s*.72]);o.rotation.set(.1,rand()*6,.15);}63
function bridge(parent,x){const g=group(parent,[x,0,0]);const za=-8.15,zb=-.75,steps=24;for(let i=0;i<=steps;i++){let z=za+(zb-za)*i/steps,u=i/steps,y=.22+.6*Math.sin(u*Math.PI);box(g,i%3?M.woodLight:M.wood,[0,y,z],[1.8,.12,(zb-za)/steps-.022]);}for(let side of [-1,1]){const points=[];for(let i=0;i<=6;i++){const u=i/6,z=za+(zb-za)*u,y=.27+.6*Math.sin(u*Math.PI);rod(g,M.wood,[side*.84,y,z],[side*.84,y+.74,z],.044);ell(g,M.woodLight,[side*.84,y+.76,z],[.068,.052,.068],true);points.push([side*.84,y+.65,z]);}tube(g,M.woodLight,points,.045,32);tube(g,M.wood,points.map(p=>[p[0],p[1]-.33,p[2]]),.035,32);}}64
const groundMat=mat('#a9ba85'),farGroundMat=mat('#91ab7e'),roadMat=mat('#d5c49d'),sandMat=mat('#c5ba8f');65
const chunks=[];66
for(let c=0;c<3;c++){67
const root=new THREE.Group();68
strip(root,groundMat,-L/2,L/2,riverNear,()=>24,(x,z,v)=>.12+Math.min(v*12,1)*.06+Math.max(z-7,0)*.015,96,7);69
strip(root,farGroundMat,-L/2,L/2,()=>-36,riverFar,(x,z,v)=>.12+Math.sin(v*Math.PI)*.45,96,8);70
strip(root,sandMat,-L/2,L/2,x=>riverNear(x)-.22,x=>riverNear(x)+.14,()=>.1);71
strip(root,sandMat,-L/2,L/2,x=>riverFar(x)-.1,x=>riverFar(x)+.2,()=>.095);72
strip(root,roadMat,-L/2,L/2,x=>pathZ(x)-1.46,x=>pathZ(x)+1.46,()=>.198,96,2);73
strip(root,mat('#dcccaa'),-L/2,L/2,x=>pathZ(x)-1.17,x=>pathZ(x)+1.17,()=>.201,96,1);74
for(let i=0;i<190;i++){let x=range(-24,24),z=range(-1.0,9);if(Math.abs(z-pathZ(x))<1.65)continue;grassClump(root,x,z,range(.7,1.4));if(i%4===0)flower(root,x+.14,z+.12,range(.7,1.1),i%3?M.petal:M.flower);}75
for(let i=0;i<85;i++){let x=range(-24,24),z=pathZ(x)+range(-1.2,1.2);const o=ell(root,rand()>.4?sandMat:roadMat,[x,.211,z],[range(.014,.045),.01,range(.02,.09)],true);}76
for(let i=0;i<13;i++){const x=-23+i*3.8+range(-.5,.5);tree(root,x,range(-13,-10),range(.65,1.1));}77
for(let i=0;i<11;i++){const x=range(-24,24);shrub(root,x,range(-9,-8),range(.65,1.2));}78
for(let i=0;i<22;i++){let x=range(-24,24),z=riverNear(x)+range(.2,.6);rock(root,x,z,range(.12,.3));grassClump(root,x+.17,z+.24,1.5);}79
for(let i=0;i<6;i++)shrub(root,range(-24,24),range(7,10),range(.65,1.1));80
// Rounded distant hills keep the little journey enclosed in a soft landscape.81
for(let i=0;i<7;i++){ell(root,i%2?M.grass:M.grassLight,[-26+i*8,.5,-22-range(0,7)],[range(6,10),range(2,4),range(5,8)],true);}82
bridge(root,-11+c*3);83
// A riverside waymarker and a tiny mailbox.84
const sign=group(root,[9,.2,4.7]);rod(sign,M.wood,[0,0,0],[0,1.25,0],.048);box(sign,M.woodLight,[.16,1.11,0],[.76,.29,.11]);box(sign,M.white,[.16,1.11,.062],[.46,.035,.012]);85
const post=group(root,[-17,.2,5]);rod(post,M.wood,[0,0,0],[0,.83,0],.065);box(post,M.mint,[0,.99,0],[.58,.4,.4]);ell(post,M.mint,[0,1.19,0],[.3,.19,.2],true);box(post,M.dark,[0,1.05,.21],[.3,.045,.012]);box(post,M.white,[0,.9,.21],[.12,.08,.01]);86
bake(root);root.position.x=(c-1)*L;scene.add(root);chunks.push({root,base:(c-1)*L});87
}88
// Water is a fully procedural, offline shader: flowing ripples, glints and shallow-bank color.89
const waterMat=new THREE.ShaderMaterial({uniforms:{uTime:{value:0},uTravel:{value:0}},vertexShader:`varying vec3 vWorld;void main(){vec4 p=modelMatrix*vec4(position,1.);vWorld=p.xyz;gl_Position=projectionMatrix*viewMatrix*p;}`,fragmentShader:`precision highp float;varying vec3 vWorld;uniform float uTime;uniform float uTravel;void main(){vec2 p=vec2(vWorld.x+uTravel,vWorld.z);float a=sin(p.x*3.1+p.y*7.3-uTime*1.1);float b=sin(p.x*7.0-p.y*2.4+uTime*.8);float w=a*.5+b*.3+sin(p.x*.7+p.y*2.0+uTime*.5)*.2;vec3 col=mix(vec3(.10,.33,.285),vec3(.26,.5,.39),w*.32+.5);float line=pow(max(0.,sin(p.y*12.+sin(p.x*1.4-uTime*.8)*1.1+uTime*.45)),22.);float mask=smoothstep(.45,.93,sin(p.x*2.8+p.y*.5+uTime*.2));col+=line*mask*vec3(.19,.22,.16);float spark=pow(max(0.,sin(p.x*20.-p.y*6.+uTime*2.)),36.)*pow(max(0.,sin(p.y*17.+uTime)),32.);col+=spark*.22;gl_FragColor=vec4(col,1.);#include <tonemapping_fragment>\n#include <colorspace_fragment>}`});90
// Preprocessor directives require their own line in GLSL.91
waterMat.fragmentShader=waterMat.fragmentShader.replace(';#include',';\n#include');92
const water=mesh(new THREE.PlaneGeometry(160,18),waterMat,scene,[0,.07,-5]);water.rotation.x=-Math.PI/2;water.receiveShadow=false;water.castShadow=false;93
const foamMat=new THREE.MeshBasicMaterial({color:'#bfd8b7',transparent:true,opacity:.55});94
const ripples=[];for(let i=0;i<24;i++){const r=torus(scene,foamMat,[range(-22,22),.081,range(-6.5,-2.5)],range(.13,.38),.01);r.rotation.x=-Math.PI/2;r.scale.y=.35;r.castShadow=false;r.receiveShadow=false;ripples.push({mesh:r,x:r.position.x,z:r.position.z,phase:rand()*TAU});}95
// Lily pads and little white ducks pass gently with the riverbank.96
const riverProps=[];for(let i=0;i<16;i++){const p=group(scene,[range(-25,25),.082,range(-6.9,-2.5)]);const pad=mesh(new THREE.CircleGeometry(range(.15,.28),20,.15,TAU-.3),M.leaves,p);pad.rotation.x=-Math.PI/2;pad.rotation.z=rand()*TAU;pad.castShadow=false;if(i%4===0){ell(p,M.flower,[0,.08,0],[.09,.08,.09],true);ell(p,M.petal,[.04,.12,0],[.05,.05,.05],true);}riverProps.push({g:p,x:p.position.x,z:p.position.z});}97
function duck(x,z,small=false){const g=group(scene,[x,.13,z]),s=small?.58:1;ell(g,M.white,[0,.08,0],[.34,.2,.22]);ell(g,M.white,[.23,.33,0],[.18,.2,.16]);ell(g,M.yellow,[.39,.32,0],[.13,.055,.09]);ell(g,M.dark,[.32,.40,.13],[.018,.021,.018]);ell(g,M.dark,[.32,.40,-.13],[.018,.021,.018]);ell(g,M.cream,[-.11,.13,.18],[.18,.1,.05]);g.scale.setScalar(s);riverProps.push({g,x,z,duck:true});}duck(4,-4.8);duck(3.3,-4.35,true);duck(2.7,-4.8,true);98
const bikeRig=group(scene,[0,.215,pathZ(0)]),bike=group(bikeRig),rider=group(bikeRig);99
// A classic step-through bicycle, with tire walls, spinning spokes, a chain and a rear rack.100
const wheels=[];for(let x of [-1.43,1.43]){const w=group(bike,[x,.79,0]);torus(w,M.tire,[0,0,0],.716,.074);torus(w,M.tireSide,[0,0,.049],.718,.035);torus(w,M.tireSide,[0,0,-.049],.718,.035);torus(w,M.silver,[0,0,0],.66,.025);for(let i=0;i<20;i++){const a=i*TAU/20;rod(w,M.silver,[0,0,(i%2?.055:-.055)],[Math.cos(a)*.65,Math.sin(a)*.65,0],.008);}rod(w,M.silver,[0,0,-.11],[0,0,.11],.067);wheels.push(w);const f=torus(bike,M.mintLight,[x,.79,0],.823,.043,Math.PI*.89);f.rotation.z=.18;for(let side of [-1,1])rod(bike,M.silver,[x,.79,side*.11],[x-.56,1.39,side*.09],.014);}101
const rear=[-1.43,.79,0],front=[1.43,.79,0],crank=[-.08,.84,0],seat=[-.53,1.95,0],headTop=[1.12,2.02,0],headLow=[1.27,1.57,0];102
for(let s of [-1,1]){rod(bike,M.mint,[-1.43,.79,.10*s],[-.08,.84,.10*s],.037);rod(bike,M.mint,[-1.43,.79,.10*s],[-.53,1.82,.08*s],.035);rod(bike,M.mint,[1.43,.79,.105*s],[1.23,1.7,.075*s],.05);}103
rod(bike,M.mint,crank,seat,.058);rod(bike,M.mint,seat,headTop,.055);tube(bike,M.mint,[crank,[.33,.99,0],[.78,1.33,0],headLow],.061);rod(bike,M.mint,headLow,headTop,.072);rod(bike,M.silver,[-.53,1.89,0],[-.59,2.13,0],.04);104
ell(bike,M.saddle,[-.63,2.14,0],[.37,.095,.225]);ell(bike,M.saddle,[-.4,2.135,0],[.2,.07,.14]);105
rod(bike,M.silver,[1.10,1.98,0],[1.03,2.36,0],.035);tube(bike,M.silver,[[1.03,2.36,-.44],[1.08,2.4,-.28],[1.03,2.38,0],[1.08,2.4,.28],[1.03,2.36,.44]],.032);for(let s of [-1,1])rod(bike,M.saddle,[1.03,2.36,s*.31],[1.02,2.36,s*.54],.048);106
const bellMesh=ell(bike,M.silver,[1.03,2.435,.24],[.10,.067,.10]);rod(bike,M.dark,[1.08,2.43,.30],[1.16,2.43,.33],.017);107
// Brake cables and a warm front lamp.108
tube(bike,M.dark,[[1.0,2.36,.3],[1.42,2.24,.23],[1.54,1.90,.15],[1.3,1.45,.07]],.011);tube(bike,M.dark,[[1.0,2.36,-.3],[.96,2.19,-.12],[.65,1.98,-.07],[-.75,1.63,-.07],[-1.4,1.36,-.04]],.009);109
ell(bike,M.silver,[1.36,1.96,0],[.12,.11,.115]);ell(bike,M.white,[1.458,1.97,0],[.032,.085,.085]);110
torus(bike,M.silver,[-.08,.84,.15],.23,.022);torus(bike,M.dark,[-.08,.84,.151],.193,.032);torus(bike,M.silver,[-1.43,.79,.16],.096,.015);tube(bike,M.dark,[[-.08,1.06,.17],[-1.43,.884,.17],[-1.52,.79,.17],[-1.43,.696,.17],[-.08,.62,.17],[.14,.84,.17],[-.08,1.06,.17]],.012,55);111
const crankArms=[],pedals=[];for(let s of [-1,1]){crankArms.push(rod(bike,M.silver,crank,crank,.034));pedals.push(box(bike,M.dark,[0,0,0],[.28,.055,.19]));}112
for(let s of [-1,1]){rod(bike,M.silver,[-1.43,.80,.12*s],[-1.8,1.78,.24*s],.024);rod(bike,M.silver,[-1.43,.80,.12*s],[-.87,1.78,.24*s],.024);rod(bike,M.silver,[-2.13,1.80,.25*s],[-.82,1.80,.25*s],.03);}for(let i=0;i<6;i++)rod(bike,M.silver,[-2.09+i*.24,1.80,-.25],[-2.09+i*.24,1.80,.25],.024);113
const parcels=group(bike,[-1.62,1.82,0]);114
function parcel(parent,p,s,m,tilt=0){const g=group(parent,p);g.rotation.y=tilt;box(g,m,[0,s[1]/2,0],s);box(g,M.tape,[0,s[1]+.004,0],[.10,.013,s[2]+.012]);box(g,M.tape,[0,s[1]/2,s[2]/2+.005],[.10,s[1],.012]);box(g,M.tape,[0,s[1]/2,-s[2]/2-.005],[.10,s[1],.012]);box(g,M.white,[s[0]*.21,s[1]*.55,s[2]/2+.013],[s[0]*.25,s[1]*.27,.013]);for(let i=0;i<3;i++)box(g,M.box2,[s[0]*.21,s[1]*(.6-i*.055),s[2]/2+.022],[s[0]*.16,.01,.009]);for(let z of [-.19,.19])tube(g,M.wood,[[s[0]/2+.008,0,z],[s[0]/2+.009,s[1]+.012,z],[-s[0]/2-.009,s[1]+.012,z],[-s[0]/2-.008,0,z]],.009,12);return g;}115
parcel(parcels,[0,.01,0],[.93,.45,.70],M.box);parcel(parcels,[-.08,.48,-.035],[.69,.32,.57],M.box2,-.08);box(parcels,M.white,[.11,.821,.04],[.46,.019,.29]).rotation.y=.16;for(let z of [-.23,.23])tube(bike,M.saddle,[[-2.10,1.8,z],[-2.09,2.35,z],[-1.98,2.69,z],[-1.29,2.68,z],[-1.13,2.3,z],[-1.15,1.8,z]],.014,28);116
// Otter anatomy: long pear-shaped torso, pale muzzle, little ears and a tapered swimming tail.117
const torso=ell(rider,M.fur,[-.40,2.60,0],[.49,.79,.40]);torso.rotation.z=-.24;118
const belly=ell(rider,M.cream,[-.105,2.55,.095],[.32,.62,.344]);belly.rotation.z=-.26;119
ell(rider,M.fur,[-.53,2.02,0],[.43,.32,.4]);120
const head=group(rider,[.02,3.44,0]);ell(head,M.fur,[0,0,0],[.56,.49,.475]);ell(head,M.furLight,[.25,-.12,0],[.42,.31,.405]);for(let s of [-1,1]){ell(head,M.fur,[-.31,.27,s*.37],[.125,.14,.08]);ell(head,M.blush,[-.29,.29,s*.428],[.068,.075,.022]);ell(head,M.muzzle,[.415,-.14,s*.165],[.245,.197,.223]);ell(head,M.blush,[.30,-.08,s*.372],[.087,.049,.02]);}121
ell(head,M.nose,[.626,-.037,0],[.126,.085,.123]);ell(head,M.white,[.676,.002,.023],[.026,.013,.027]);122
const eyes=[];for(let s of [-1,1]){const e=group(head,[.284,.121,s*.381]);ell(e,M.dark,[0,0,0],[.071,.090,.044]);ell(e,M.white,[.012,.031,s*.032],[.023,.025,.015]);ell(e,M.white,[-.02,-.025,s*.035],[.009,.01,.008]);eyes.push(e);tube(head,M.furLight,[[.20,.24,s*.394],[.26,.26,s*.402],[.33,.247,s*.37]],.025,10);for(let k=0;k<3;k++){const y=-.16+k*.065;tube(head,M.cream,[[.51,y,s*.28],[.50,y+.03*(k-1),s*.47],[.41,y+.08*(k-1),s*.67]],.006,12);}tube(head,M.dark,[[.626,-.13,0],[.617,-.20,s*.075],[.563,-.215,s*.14]],.012,14);}123
for(let i=0;i<3;i++){const tuft=ell(head,M.fur,[-.15-i*.08,.44-i*.014,-.025+i*.036],[.11,.14,.08]);tuft.rotation.z=.4+i*.15;}124
// The scarf is a deforming two-sided cloth ribbon with its own stitched edges.125
const collar=torus(rider,M.scarf,[.012,3.065,0],.325,.103);collar.rotation.x=Math.PI/2;collar.scale.set(1.06,1,.85);ell(rider,M.scarf,[-.27,3.09,.25],[.17,.14,.15]);126
const fabricCanvas=document.createElement('canvas');fabricCanvas.width=256;fabricCanvas.height=64;const fabricCtx=fabricCanvas.getContext('2d');fabricCtx.fillStyle='#ca5b45';fabricCtx.fillRect(0,0,256,64);for(let i=0;i<64;i+=2){fabricCtx.fillStyle=i%4?'#ce654d':'#c65742';fabricCtx.fillRect(0,i,256,1);}for(let i=0;i<256;i+=3){fabricCtx.fillStyle='#ffd6a312';fabricCtx.fillRect(i,0,1,64);}fabricCtx.fillStyle='#edb180';fabricCtx.fillRect(220,0,5,64);fabricCtx.fillRect(232,0,3,64);const fabric=new THREE.CanvasTexture(fabricCanvas);fabric.colorSpace=THREE.SRGBColorSpace;fabric.anisotropy=Math.min(8,renderer.capabilities.getMaxAnisotropy());127
function ribbon(w,length,off){const N=34,p=new Float32Array((N+1)*2*3),ix=[],uv=[];for(let i=0;i<=N;i++)uv.push(i/N,0,i/N,1);for(let i=0;i<N;i++){const a=i*2;ix.push(a,a+1,a+2,a+1,a+3,a+2);}const geo=new THREE.BufferGeometry();geo.setAttribute('position',new THREE.BufferAttribute(p,3).setUsage(THREE.DynamicDrawUsage));geo.setAttribute('uv',new THREE.Float32BufferAttribute(uv,2));geo.setIndex(ix);const m=mat('#ffffff');m.map=fabric;m.side=THREE.DoubleSide;const cloth=mesh(geo,m,rider);cloth.frustumCulled=false;const edgeGeo=new THREE.BufferGeometry();edgeGeo.setAttribute('position',new THREE.BufferAttribute(new Float32Array(N*4*3),3).setUsage(THREE.DynamicDrawUsage));const edge=new THREE.LineSegments(edgeGeo,new THREE.LineBasicMaterial({color:'#efba82',transparent:true,opacity:.45}));edge.frustumCulled=false;rider.add(edge);return {N,p,geo,cloth,edgeGeo,w,length,off};}128
const ribbons=[ribbon(.28,1.80,0),ribbon(.20,1.36,1.7)];129
function animateScarf(t,speed){for(const r of ribbons){const edge=r.edgeGeo.attributes.position.array;for(let i=0;i<=r.N;i++){const u=i/r.N,phase=u*6.2-scarfPhase+r.off;const x=-.27-u*r.length,y=3.08+.17*Math.sin(u*3.5)+Math.sin(phase)*.15*u+Math.sin(u*11-t*5)*.045*u,z=.28+.12*u+Math.sin(phase*.9)*.10*u;for(let j=0;j<2;j++){const v=j-.5,k=(i*2+j)*3;r.p[k]=x+.018*Math.sin(u*30)*(u>.94?1:0);r.p[k+1]=y+v*r.w*(1-.12*u)*Math.cos(phase*.3);r.p[k+2]=z+v*r.w*Math.sin(phase*.5)*.45;}}let k=0;for(let i=0;i<r.N;i++)for(let j=0;j<2;j++)for(let e=0;e<2;e++){const at=((i+e)*2+j)*3;edge[k++]=r.p[at];edge[k++]=r.p[at+1];edge[k++]=r.p[at+2];}r.geo.attributes.position.needsUpdate=true;r.geo.computeVertexNormals();r.edgeGeo.attributes.position.needsUpdate=true;}}130
const tailGeo=new THREE.BufferGeometry(),tailN=22,tailSides=12,tailP=new Float32Array((tailN+1)*tailSides*3),tailI=[];for(let i=0;i<tailN;i++)for(let j=0;j<tailSides;j++){let a=i*tailSides+j,b=i*tailSides+(j+1)%tailSides;tailI.push(a,b,a+tailSides,b,b+tailSides,a+tailSides);}tailGeo.setAttribute('position',new THREE.BufferAttribute(tailP,3).setUsage(THREE.DynamicDrawUsage));tailGeo.setIndex(tailI);const tail=mesh(tailGeo,M.fur,rider);tail.frustumCulled=false;131
function animateTail(t){for(let i=0;i<=tailN;i++){let u=i/tailN,x=-.61-u*1.7,y=1.99-.92*Math.sin(u*Math.PI*.57),z=.18+.52*Math.sin(u*1.8)+Math.sin(t*1.9-u*3)*.06*u,r=.23*Math.pow(1-u,1.05)+.008;for(let j=0;j<tailSides;j++){const a=j*TAU/tailSides,k=(i*tailSides+j)*3;tailP[k]=x;tailP[k+1]=y+Math.cos(a)*r;tailP[k+2]=z+Math.sin(a)*r*.87;}}tailGeo.attributes.position.needsUpdate=true;tailGeo.computeVertexNormals();}132
const legs=[];for(let s of [-1,1]){const thigh=ell(rider,M.fur,[0,0,0],[1,1,1]),shin=ell(rider,M.fur,[0,0,0],[1,1,1]),foot=ell(rider,M.furLight,[0,0,0],[.235,.105,.155]);const knee=ell(rider,M.fur,[0,0,0],[.163,.175,.158]);legs.push({s,thigh,shin,foot,knee});}133
function limb(o,a,b,r,rz=r){placeRod(o,a,b,r,rz);o.scale.y*=.60;}134
const arms=[];for(let s of [-1,1]){const upper=ell(rider,M.fur,[0,0,0],[1,1,1]),fore=ell(rider,M.fur,[0,0,0],[1,1,1]);const paw=ell(rider,M.furLight,[1.015,2.39,s*.45],[.16,.115,.127]);arms.push({s,upper,fore,paw});for(let i=0;i<3;i++)tube(bike,M.fur,[[1.11,2.40,s*.45+(i-1)*.045],[1.14,2.36,s*.45+(i-1)*.045],[1.10,2.33,s*.45+(i-1)*.045]],.02,7);}135
// Drifting pollen and two small butterflies make the banks feel alive.136
const pollenGeo=new THREE.BufferGeometry(),pollenP=new Float32Array(64*3),pollenBase=[];for(let i=0;i<64;i++){pollenBase.push([range(-16,16),range(.6,5.5),range(-9,9)]);}pollenGeo.setAttribute('position',new THREE.BufferAttribute(pollenP,3));const pollen=new THREE.Points(pollenGeo,new THREE.PointsMaterial({color:'#fff3c6',size:.035,transparent:true,opacity:.64,sizeAttenuation:true,depthWrite:false}));scene.add(pollen);137
const butterflies=[];for(let i=0;i<3;i++){const g=group(scene),a=ell(g,i%2?M.yellow:M.flower,[0,0,.045],[.1,.013,.115],true),b=ell(g,i%2?M.yellow:M.flower,[0,0,-.045],[.1,.013,.115],true);ell(g,M.wood,[0,0,0],[.047,.025,.025],true);butterflies.push({g,a,b,phase:i*2.2});}138
// A small flock is drawn as curved wings against the distant trees.139
const birds=[];for(let i=0;i<5;i++){const g=group(scene);const a=rod(g,M.leavesDark,[0,0,0],[.19,.08,.04],.015),b=rod(g,M.leavesDark,[0,0,0],[-.19,.08,.04],.015);birds.push({g,a,b,p:i*.7});}140
let running=!matchMedia('(prefers-reduced-motion: reduce)').matches,speed=1,velocity=0,time=0,scarfPhase=0,travel=0,wheelPhase=0,crankPhase=0,greet=0,bellBounce=0,viewMode=0;141
const views=[{theta:.40,phi:1.12,r:15.4,label:'沿河跟拍',targetY:1.65},{theta:.13,phi:1.28,r:10.0,label:'近看邮差',targetY:2.12},{theta:.68,phi:.85,r:23,label:'河岸全景',targetY:.75}];142
let desired={...views[0]},orbit={...views[0]},manual=false;const target=new THREE.Vector3();143
function resetView(){desired={...views[viewMode]};manual=false;toast('回到最佳观景位置');}144
let toastTimer;function toast(s){$('toast').textContent=s;$('toast').classList.add('show');clearTimeout(toastTimer);toastTimer=setTimeout(()=>$('toast').classList.remove('show'),2300);}145
function updatePlay(){ $('play-icon').innerHTML=running?'<path d="M8 5v14M16 5v14" stroke-width="3"/>':'<path d="M8 4l12 8-12 8z" fill="currentColor" stroke="none"/>';$('play').setAttribute('aria-label',running?'暂停骑行':'继续骑行');}146
$('play').onclick=()=>{running=!running;updatePlay();};updatePlay();$('speed').oninput=e=>{speed=Number(e.target.value);$('speed-value').textContent=speed.toFixed(2).replace(/0$/,'')+'×';};147
$('view').onclick=()=>{viewMode=(viewMode+1)%views.length;desired={...views[viewMode]};manual=false;$('view').querySelector('span').textContent=desired.label;};$('reset').onclick=resetView;148
$('help-button').onclick=()=>{const open=$('help').classList.toggle('open');$('help-button').setAttribute('aria-expanded',String(open));};149
// All audio is synthesized locally. No recordings, URLs or network requests are used.150
let audio=null,ambience=null,audioOn=false,birdTimer=null;151
function getAudio(){if(!audio){const AC=window.AudioContext||window.webkitAudioContext;if(!AC){toast('这个浏览器暂不支持声音');return null;}audio=new AC();}if(audio.state==='suspended')audio.resume();return audio;}152
function ring(){const ac=getAudio();if(ac){for(let i=0;i<2;i++)for(let j=0;j<3;j++){const o=ac.createOscillator(),g=ac.createGain(),at=ac.currentTime+i*.16; o.type='sine';o.frequency.value=[1318.5,2641,3972][j];g.gain.setValueAtTime(0,at);g.gain.linearRampToValueAtTime(.075/(j+1),at+.004);g.gain.exponentialRampToValueAtTime(.0001,at+.9);o.connect(g);g.connect(ac.destination);o.start(at);o.stop(at+1);}}bellBounce=1;greet=1;toast('叮铃——借过一阵小风。');}153
$('bell').onclick=ring;154
function chirp(){if(!audioOn||document.hidden||!audio)return;const now=audio.currentTime;for(let i=0;i<3;i++){const o=audio.createOscillator(),g=audio.createGain();o.frequency.setValueAtTime(1800+i*260,now+i*.15);o.frequency.exponentialRampToValueAtTime(3000+i*230,now+i*.15+.07);o.frequency.exponentialRampToValueAtTime(2200,now+i*.15+.12);g.gain.setValueAtTime(0,now+i*.15);g.gain.linearRampToValueAtTime(.012,now+i*.15+.02);g.gain.linearRampToValueAtTime(0,now+i*.15+.13);o.connect(g);g.connect(ambience);o.start(now+i*.15);o.stop(now+i*.15+.15);}}155
$('sound').onclick=()=>{const ac=getAudio();if(!ac)return;audioOn=!audioOn;if(!ambience){ambience=ac.createGain();ambience.gain.value=0;ambience.connect(ac.destination);const buffer=ac.createBuffer(1,ac.sampleRate*4,ac.sampleRate),d=buffer.getChannelData(0);let last=0;for(let i=0;i<d.length;i++){last=(last+(Math.random()*2-1)*.02)/1.02;d[i]=last*3.5;}const noise=ac.createBufferSource();noise.buffer=buffer;noise.loop=true;const filter=ac.createBiquadFilter();filter.type='lowpass';filter.frequency.value=1100;noise.connect(filter);filter.connect(ambience);noise.start();birdTimer=setInterval(chirp,6200);}ambience.gain.setTargetAtTime(audioOn?.24:0,ac.currentTime,.6);$('sound').classList.toggle('active',audioOn);$('sound').setAttribute('aria-pressed',String(audioOn));$('sound').querySelector('svg').innerHTML=audioOn?'<path d="M3 9h4l5-4v14l-5-4H3zM16 8a6 6 0 0 1 0 8M19 5a10 10 0 0 1 0 14"/>':'<path d="M3 9h4l5-4v14l-5-4H3zM16 9l5 6M21 9l-5 6"/>';if(audioOn){chirp();toast('听听流水与鸟鸣');}};156
$('capture').onclick=()=>{renderer.render(scene,camera);const c=document.createElement('canvas');c.width=renderer.domElement.width;c.height=renderer.domElement.height;const ctx=c.getContext('2d');ctx.drawImage(renderer.domElement,0,0);const p=c.width*.035;ctx.fillStyle='#304d43';ctx.font=`${Math.round(c.width*.030)}px "Microsoft YaHei", sans-serif`;ctx.fillText('河岸邮差',p,p*1.7);ctx.font=`${Math.round(c.width*.010)}px "Segoe UI", sans-serif`;ctx.fillText('A LITTLE DELIVERY · RIVERSIDE POST',p,p*2.35);ctx.fillStyle='#faf8ecd9';ctx.fillRect(0,c.height-p*1.7,c.width,p*1.7);ctx.fillStyle='#304d43';ctx.font=`${Math.round(c.width*.010)}px "Microsoft YaHei",sans-serif`;ctx.fillText('把日子放慢,把心意送达。',p,c.height-p*.65);c.toBlob(blob=>{if(!blob)return;const url=URL.createObjectURL(blob),a=document.createElement('a');a.href=url;a.download='河岸邮差-明信片.png';a.click();setTimeout(()=>URL.revokeObjectURL(url),2000);toast('一段小小的旅程,收进明信片。');},'image/png');};157
const pointers=new Map();let dragStart=null,dragMoved=0,pinchDist=0;158
const stage=$('stage');stage.addEventListener('pointerdown',e=>{stage.setPointerCapture(e.pointerId);pointers.set(e.pointerId,{x:e.clientX,y:e.clientY});dragStart={x:e.clientX,y:e.clientY};dragMoved=0;stage.classList.add('dragging');if(pointers.size===2){const a=[...pointers.values()];pinchDist=Math.hypot(a[0].x-a[1].x,a[0].y-a[1].y);}});159
stage.addEventListener('pointermove',e=>{if(!pointers.has(e.pointerId))return;const old=pointers.get(e.pointerId),dx=e.clientX-old.x,dy=e.clientY-old.y;pointers.set(e.pointerId,{x:e.clientX,y:e.clientY});dragMoved+=Math.abs(dx)+Math.abs(dy);manual=true;if(pointers.size===2){const a=[...pointers.values()],d=Math.hypot(a[0].x-a[1].x,a[0].y-a[1].y);desired.r=THREE.MathUtils.clamp(desired.r*pinchDist/Math.max(d,1),7.5,30);pinchDist=d;}else{desired.theta-=dx*.005;desired.phi=THREE.MathUtils.clamp(desired.phi-dy*.004,.45,1.48);}});160
const raycaster=new THREE.Raycaster();stage.addEventListener('pointerup',e=>{if(pointers.size===1&&dragMoved<7){const rect=stage.getBoundingClientRect();raycaster.setFromCamera(new THREE.Vector2((e.clientX-rect.left)/rect.width*2-1,-(e.clientY-rect.top)/rect.height*2+1),camera);if(raycaster.intersectObject(rider,true).length){greet=1;toast('你好呀!今天也有好事正在路上。');}}pointers.delete(e.pointerId);if(!pointers.size)stage.classList.remove('dragging');});stage.addEventListener('pointercancel',e=>{pointers.delete(e.pointerId);stage.classList.remove('dragging');});stage.addEventListener('wheel',e=>{e.preventDefault();desired.r=THREE.MathUtils.clamp(desired.r*Math.exp(e.deltaY*.0008),7.5,30);manual=true;},{passive:false});161
window.addEventListener('keydown',e=>{if(e.target.matches('input'))return;if(e.code==='Space'){if(e.target.matches('button'))return;e.preventDefault();$('play').click();}if(e.code==='KeyB')ring();if(e.code==='KeyR')resetView();if(e.code==='KeyV')$('view').click();});162
window.addEventListener('resize',()=>{camera.aspect=innerWidth/innerHeight;camera.updateProjectionMatrix();renderer.setSize(innerWidth,innerHeight);});163
document.addEventListener('visibilitychange',()=>{last=performance.now();if(audio){if(document.hidden)audio.suspend();else if(audioOn)audio.resume();}});164
function wrap(v,size){return ((v+size/2)%size+size)%size-size/2;}165
let last=performance.now(),frameCount=0;166
function animate(now){requestAnimationFrame(animate);if(document.hidden){last=now;return;}const dt=Math.max(0,Math.min((now-last)/1000,.045));last=now;const simDt=running?dt:0;time+=simDt;scarfPhase+=simDt*(3.5+speed);velocity=running?THREE.MathUtils.lerp(velocity,1.72*speed,1-Math.exp(-dt*3)):0;const ds=velocity*dt;travel+=ds;wheelPhase-=ds/.79;crankPhase-=ds/.79/1.85;const bob=Math.sin(crankPhase*2)*.018*(running?speed:1);bikeRig.position.y=.216+bob*.35;bikeRig.position.z=pathZ(travel);bikeRig.rotation.y=-Math.atan(.5*TAU/L*Math.cos(travel*TAU/L));bikeRig.rotation.x=Math.sin(crankPhase)*.012;for(let w of wheels)w.rotation.z=wheelPhase;167
const breath=Math.sin(time*2.0)*.008;rider.position.y=bob+breath;rider.rotation.x=Math.sin(crankPhase)*.018;168
for(let i=0;i<2;i++){const leg=legs[i],s=leg.s,a=crankPhase+(s===1?0:Math.PI),foot=[-.08+Math.cos(a)*.34,.84+Math.sin(a)*.34+.09,s*.34];placeRod(crankArms[i],[-.08,.84,s*.16],[foot[0],foot[1]-.09,s*.25],.028);pedals[i].position.set(foot[0],foot[1]-.09,s*.36);const hip=[-.44,2.02,s*.30],dx=foot[0]-hip[0],dy=foot[1]-hip[1],dist=Math.hypot(dx,dy),l1=.79,l2=.82,along=(l1*l1-l2*l2+dist*dist)/(2*dist),height=Math.sqrt(Math.max(.005,l1*l1-along*along)),knee=[hip[0]+dx/dist*along-dy/dist*height,hip[1]+dy/dist*along+dx/dist*height,s*.38];limb(leg.thigh,hip,knee,.19,.17);limb(leg.shin,knee,foot,.135,.13);leg.knee.position.set(...knee);leg.foot.position.set(foot[0]+.055,foot[1],foot[2]);leg.foot.rotation.z=.08*Math.cos(a);}169
for(const arm of arms){const s=arm.s,shoulder=[.03,2.89,s*.31],elbow=[.52,2.53+Math.sin(crankPhase*2)*.015,s*.40],hand=[1.015,2.39-bob,s*.45];limb(arm.upper,shoulder,elbow,.137,.132);limb(arm.fore,elbow,hand,.115,.108);arm.paw.position.set(...hand);}170
if(greet>0)greet=Math.max(0,greet-dt*.65);head.rotation.y=Math.sin(time*.65)*.075-(greet>0?Math.sin((1-greet)*Math.PI)*.32:0);head.rotation.z=Math.sin(time*1.3)*.023+(greet>0?Math.sin((1-greet)*Math.PI*4)*.09:0);const blinkPhase=time%4.9;const blink=(blinkPhase>4.65)?Math.max(.08,Math.abs((blinkPhase-4.775)/.125)):1;for(const eye of eyes)eye.scale.y=blink;171
animateScarf(time,speed);animateTail(time);parcels.rotation.z=Math.sin(crankPhase*2+.4)*.013;bellBounce=Math.max(0,bellBounce-dt*2);bellMesh.rotation.z=Math.sin(now*.035)*bellBounce*.25;172
for(const c of chunks)c.root.position.x=wrap(c.base-travel,L*3);waterMat.uniforms.uTime.value=time;waterMat.uniforms.uTravel.value=travel;173
for(const r of ripples){r.mesh.position.x=wrap(r.x-travel-time*.08,48);r.mesh.scale.setScalar(.8+Math.sin(time+r.phase)*.15);r.mesh.scale.y=.36;}174
for(const p of riverProps){p.g.position.x=wrap(p.x-travel+(p.duck?time*.10:0),50);p.g.position.y=(p.duck?.14:.082)+Math.sin(time*1.4+p.x)*.014;}175
for(let i=0;i<64;i++){const p=pollenBase[i];pollenP[i*3]=wrap(p[0]-travel*.8+Math.sin(time*.4+i)*.35,32);pollenP[i*3+1]=p[1]+Math.sin(time*.65+i)*.2;pollenP[i*3+2]=p[2]+Math.cos(time*.3+i)*.3;}pollenGeo.attributes.position.needsUpdate=true;176
for(const b of butterflies){b.g.position.set(Math.sin(time*.25+b.phase)*6,1.0+Math.sin(time*1.7+b.phase)*.25,-.4+Math.cos(time*.4+b.phase)*1.0);b.g.rotation.y=time*.4;b.a.rotation.x=Math.sin(time*18+b.phase)*.8;b.b.rotation.x=-b.a.rotation.x;}177
for(let i=0;i<birds.length;i++){const b=birds[i];b.g.position.set(wrap(13-time*.28+i*.7,40),6.5+Math.sin(time*.5+b.p)*.15,-16-i*.4);b.a.rotation.z=Math.sin(time*4+b.p)*.32;b.b.rotation.z=-b.a.rotation.z;}178
const ease=1-Math.exp(-dt*5);for(const key of ['theta','phi','r','targetY'])orbit[key]+=(desired[key]-orbit[key])*ease;const responsive=innerWidth<650?1.48:1;target.set(0,orbit.targetY,pathZ(travel)-.1);const radius=orbit.r*responsive;camera.position.set(target.x+Math.sin(orbit.theta)*Math.sin(orbit.phi)*radius,target.y+Math.cos(orbit.phi)*radius,target.z+Math.cos(orbit.theta)*Math.sin(orbit.phi)*radius);camera.lookAt(target);renderer.render(scene,camera);179
if(frameCount++%12===0)$('distance').textContent=String(Math.floor(travel)).padStart(4,'0')+' m';if(frameCount===3)$('loader').classList.add('hide');180
window.__riverside={ready:true,travel,time,running,speed,drawCalls:renderer.info.render.calls,triangles:renderer.info.render.triangles,view:viewMode};181
}182
requestAnimationFrame(animate);183
renderer.domElement.addEventListener('webglcontextlost',e=>{e.preventDefault();toast('画面暂时中断,请重新打开文件。');});184
})()185
</script>186
<script>window.addEventListener('error',()=>{const e=document.getElementById('loader');if(!e.classList.contains('hide')){e.innerHTML='画面暂时无法启动<span>请用支持 WebGL 的现代浏览器打开此文件</span>';}});</script>187
<!-- Three.js is bundled under the MIT license. Copyright © 2010–2023 three.js authors.188
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.189
Scene, animation, sound and interface created specifically for this standalone HTML. No external assets, fonts or runtime network access. -->190
</body></html>