返回报告

08 · GPT-6 Astra / 极高(xhigh) · Standard · 第3次

原场景源文件,未修复或改写。下载源文件 · 源码目录 · 运行原交付

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(()=>{
30const $=id=>document.getElementById(id), TAU=Math.PI*2;
31let renderer;try{renderer=new THREE.WebGLRenderer({antialias:true,alpha:false,powerPreference:'high-performance',preserveDrawingBuffer:true});}catch(error){$('loader').innerHTML='画面暂时无法启动<span>请用启用图形加速的现代浏览器打开此文件</span>';return;}
32renderer.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);
33const scene=new THREE.Scene();scene.background=new THREE.Color('#d9e7dd');scene.fog=new THREE.Fog('#d9e7dd',24,65);
34const camera=new THREE.PerspectiveCamera(36,innerWidth/innerHeight,.1,150);
35const hemi=new THREE.HemisphereLight('#f6f5df','#879369',2.5);scene.add(hemi);
36const 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);
37const fill=new THREE.DirectionalLight('#deecf1',.65);fill.position.set(7,4,-9);scene.add(fill);
38const mat=(color,roughness=.82,metalness=0)=>new THREE.MeshStandardMaterial({color,roughness,metalness});
39const 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')};
40const 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);
41function 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;}
42function ell(parent,m,p,s,low=false){return mesh(low?lowSphere:sphereGeo,m,parent,p,s);}
43function box(parent,m,p,s){return mesh(boxGeo,m,parent,p,s);}
44function rod(parent,m,a,b,r=.04){const o=mesh(cylGeo,m,parent);placeRod(o,a,b,r);return o;}
45const tempV=new THREE.Vector3(),up=new THREE.Vector3(0,1,0);
46function 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());}
47function 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);}
48function 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;}
49function group(parent,p=[0,0,0]){const g=new THREE.Group();g.position.set(...p);parent.add(g);return g;}
50let seed=72;function rand(){seed=(1664525*seed+1013904223)>>>0;return seed/4294967296;}function range(a,b){return a+(b-a)*rand();}
51const L=48;
52function pathZ(x){return 2.2+.5*Math.sin(x*TAU/L);}
53function riverNear(x){return -1.65+.45*Math.sin(x*TAU/L+.7);}
54function riverFar(x){return -7.6+.65*Math.sin(x*TAU/L-.3);}
55function 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.
57function 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);}}
58function 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);}
59function 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;}
60function 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);}
61function 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);}
62function 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);}
63function 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);}}
64const groundMat=mat('#a9ba85'),farGroundMat=mat('#91ab7e'),roadMat=mat('#d5c49d'),sandMat=mat('#c5ba8f');
65const chunks=[];
66for(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.
89const 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.
91waterMat.fragmentShader=waterMat.fragmentShader.replace(';#include',';\n#include');
92const water=mesh(new THREE.PlaneGeometry(160,18),waterMat,scene,[0,.07,-5]);water.rotation.x=-Math.PI/2;water.receiveShadow=false;water.castShadow=false;
93const foamMat=new THREE.MeshBasicMaterial({color:'#bfd8b7',transparent:true,opacity:.55});
94const 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.
96const 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});}
97function 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);
98const 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.
100const 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);}
101const 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];
102for(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);}
103rod(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);
104ell(bike,M.saddle,[-.63,2.14,0],[.37,.095,.225]);ell(bike,M.saddle,[-.4,2.135,0],[.2,.07,.14]);
105rod(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);
106const 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.
108tube(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);
109ell(bike,M.silver,[1.36,1.96,0],[.12,.11,.115]);ell(bike,M.white,[1.458,1.97,0],[.032,.085,.085]);
110torus(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);
111const 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]));}
112for(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);
113const parcels=group(bike,[-1.62,1.82,0]);
114function 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;}
115parcel(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.
117const torso=ell(rider,M.fur,[-.40,2.60,0],[.49,.79,.40]);torso.rotation.z=-.24;
118const belly=ell(rider,M.cream,[-.105,2.55,.095],[.32,.62,.344]);belly.rotation.z=-.26;
119ell(rider,M.fur,[-.53,2.02,0],[.43,.32,.4]);
120const 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]);}
121ell(head,M.nose,[.626,-.037,0],[.126,.085,.123]);ell(head,M.white,[.676,.002,.023],[.026,.013,.027]);
122const 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);}
123for(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.
125const 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]);
126const 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());
127function 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};}
128const ribbons=[ribbon(.28,1.80,0),ribbon(.20,1.36,1.7)];
129function 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;}}
130const 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;
131function 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();}
132const 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});}
133function limb(o,a,b,r,rz=r){placeRod(o,a,b,r,rz);o.scale.y*=.60;}
134const 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.
136const 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);
137const 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.
139const 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});}
140let 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;
141const 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}];
142let desired={...views[0]},orbit={...views[0]},manual=false;const target=new THREE.Vector3();
143function resetView(){desired={...views[viewMode]};manual=false;toast('回到最佳观景位置');}
144let toastTimer;function toast(s){$('toast').textContent=s;$('toast').classList.add('show');clearTimeout(toastTimer);toastTimer=setTimeout(()=>$('toast').classList.remove('show'),2300);}
145function 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.
150let audio=null,ambience=null,audioOn=false,birdTimer=null;
151function 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;}
152function 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;
154function 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');};
157const pointers=new Map();let dragStart=null,dragMoved=0,pinchDist=0;
158const 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);}});
159stage.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);}});
160const 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});
161window.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();});
162window.addEventListener('resize',()=>{camera.aspect=innerWidth/innerHeight;camera.updateProjectionMatrix();renderer.setSize(innerWidth,innerHeight);});
163document.addEventListener('visibilitychange',()=>{last=performance.now();if(audio){if(document.hidden)audio.suspend();else if(audioOn)audio.resume();}});
164function wrap(v,size){return ((v+size/2)%size+size)%size-size/2;}
165let last=performance.now(),frameCount=0;
166function 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}
182requestAnimationFrame(animate);
183renderer.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.
188Permission 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.
189Scene, animation, sound and interface created specifically for this standalone HTML. No external assets, fonts or runtime network access. -->
190</body></html>