06 gpt-6.1-sol / xhigh · 原始源码

原源码的只读副本,未修改原交付。行号可用于核对报告中的结论。

回到评估报告 · 下载原源码
1import * as THREE from 'three';
2import { OrbitControls } from 'three/addons/controls/OrbitControls.js';
3import { RoundedBoxGeometry } from 'three/addons/geometries/RoundedBoxGeometry.js';
4import { mergeGeometries } from 'three/addons/utils/BufferGeometryUtils.js';
5
6const $ = id => document.getElementById(id);
7const reducedMotion = matchMedia('(prefers-reduced-motion: reduce)').matches;
8const state = { paused:reducedMotion, kmh:18, distance:0, time:0, wheelRear:0, wheelFront:0, view:0, sunset:false, lightBlend:0, hornTime:-10, frames:0 };
9let renderer;
10try {
11 renderer = new THREE.WebGLRenderer({ antialias:true, alpha:false, powerPreference:'high-performance' });
12} catch(error) {
13 $('loading').style.display='none'; $('error').style.display='grid';
14 console.error(error);
15 throw error;
16}
17renderer.setPixelRatio(Math.min(devicePixelRatio, 1.65));
18renderer.setSize(innerWidth,innerHeight);
19renderer.outputColorSpace = THREE.SRGBColorSpace;
20renderer.toneMapping = THREE.ACESFilmicToneMapping;
21renderer.toneMappingExposure = 1.22;
22renderer.shadowMap.enabled = true;
23renderer.shadowMap.type = THREE.PCFSoftShadowMap;
24$('scene').appendChild(renderer.domElement);
25renderer.domElement.setAttribute('aria-label','猫咪沿海骑电动车的三维画面,可拖动旋转视角');
26
27const scene = new THREE.Scene();
28scene.background = new THREE.Color('#bfdfe2');
29scene.fog = new THREE.Fog('#bfdfe2',65,190);
30const camera = new THREE.PerspectiveCamera(39,innerWidth/innerHeight,.1,750);
31const controls = new OrbitControls(camera,renderer.domElement);
32controls.enableDamping = true;
33controls.dampingFactor = .08;
34controls.enablePan = false;
35controls.minDistance = 4.2;
36controls.maxDistance = 28;
37controls.maxPolarAngle = Math.PI*.475;
38controls.minPolarAngle = .12;
39controls.target.set(0,1.65,1.5);
40const hemi = new THREE.HemisphereLight('#eff8e6','#bea27f',2.5);
41scene.add(hemi);
42const sunLight = new THREE.DirectionalLight('#fff4d4',3.6);
43sunLight.position.set(12,22,8);
44sunLight.castShadow = true;
45sunLight.shadow.mapSize.set(2048,2048);
46Object.assign(sunLight.shadow.camera,{left:-23,right:23,top:23,bottom:-23,near:.5,far:70});
47sunLight.shadow.bias=-.0004;
48sunLight.shadow.normalBias=.022;
49sunLight.shadow.radius=3;
50scene.add(sunLight);
51
52const mat = (color,roughness=.75,metalness=0) => new THREE.MeshStandardMaterial({color,roughness,metalness});
53const M = {
54 mint:mat('#6fbca6',.35), mintDark:mat('#307b6c',.45), cream:mat('#fff1cb'),
55 orange:mat('#e59746'), stripe:mat('#b76e32'), muzzle:mat('#fff0d4'), pink:mat('#ed978c'),
56 tire:mat('#28383a'), hub:mat('#ccd3c0',.3,.55), metal:mat('#789b97',.33,.65),
57 dark:mat('#203e40'), seat:mat('#a65e43'), scarf:mat('#d76a4f'), shirt:mat('#fff4d6'),
58 sand:mat('#e7ce99'), grass:mat('#9bb581'), grassDark:mat('#74946b'), wood:mat('#b88a57'),
59 woodLight:mat('#d6b283'), leaf:mat('#659675'), leafLight:mat('#87ae7e'),
60 white:mat('#fff6e1'), rock:mat('#a9b7a2'), peach:mat('#e6ac8d'), teal:mat('#71ada7'),
61 glass:mat('#d3e9df',.1,.12), red:new THREE.MeshStandardMaterial({color:'#d05a40',emissive:'#b23c21',emissiveIntensity:.35}),
62 lamp:new THREE.MeshStandardMaterial({color:'#fff0c1',emissive:'#ffc36f',emissiveIntensity:.25}),
63 eyes:mat('#192f32',.2), glint:new THREE.MeshBasicMaterial({color:'#fffdf6'})
64};
65
66function mesh(geometry,material,parent,pos=[0,0,0],scale=[1,1,1],rot=[0,0,0],shadow=true) {
67 const obj = new THREE.Mesh(geometry,material);
68 obj.position.set(...pos); obj.scale.set(...scale); obj.rotation.set(...rot);
69 obj.castShadow = shadow; obj.receiveShadow = shadow;
70 parent.add(obj); return obj;
71}
72function box(parent,material,pos,size,r=.04,rot=[0,0,0]) {
73 return mesh(new RoundedBoxGeometry(...size,2,r),material,parent,pos,[1,1,1],rot);
74}
75function ball(parent,material,pos,scale,segments=20) {
76 return mesh(new THREE.SphereGeometry(1,segments,14),material,parent,pos,scale);
77}
78function rod(parent,material,a,b,r=.035,r2=r,sides=10) {
79 const A=new THREE.Vector3(...a), B=new THREE.Vector3(...b), v=B.clone().sub(A);
80 const o=mesh(new THREE.CylinderGeometry(r2,r,v.length(),sides),material,parent,A.clone().add(B).multiplyScalar(.5).toArray());
81 o.quaternion.setFromUnitVectors(new THREE.Vector3(0,1,0),v.normalize()); return o;
82}
83function tube(parent,material,points,r=.03) {
84 const curve=new THREE.CatmullRomCurve3(points.map(p=>new THREE.Vector3(...p)));
85 return mesh(new THREE.TubeGeometry(curve,Math.max(10,points.length*7),r,7,false),material,parent);
86}
87function flatTriangle(parent,material,vertices,thickness=.035) {
88 const shape=new THREE.Shape(); shape.moveTo(vertices[0][0],vertices[0][1]);
89 shape.lineTo(vertices[1][0],vertices[1][1]); shape.lineTo(vertices[2][0],vertices[2][1]); shape.closePath();
90 return mesh(new THREE.ExtrudeGeometry(shape,{depth:thickness,bevelEnabled:true,bevelSegments:1,steps:1,bevelSize:.014,bevelThickness:.014}),material,parent);
91}
92function seeded(n) {
93 let s=(n+1)*987231;
94 return () => { s=(s*1664525+1013904223)>>>0; return s/4294967296; };
95}
96
97// Procedural textures keep the delivered HTML fully offline.
98const asphaltCanvas = document.createElement('canvas');
99asphaltCanvas.width=asphaltCanvas.height=128;
100const ac=asphaltCanvas.getContext('2d'), ai=ac.createImageData(128,128), ar=seeded(77);
101for(let i=0;i<ai.data.length;i+=4) {
102 const v=100+ar()*16; ai.data[i]=v*.83; ai.data[i+1]=v*.94; ai.data[i+2]=v; ai.data[i+3]=255;
103}
104ac.putImageData(ai,0,0);
105const asphaltTexture=new THREE.CanvasTexture(asphaltCanvas);
106asphaltTexture.colorSpace=THREE.SRGBColorSpace;
107asphaltTexture.wrapS=asphaltTexture.wrapT=THREE.RepeatWrapping;
108asphaltTexture.repeat.set(220,5);
109const roadMaterial=new THREE.MeshStandardMaterial({map:asphaltTexture,roughness:.97,color:'#b1b7ad'});
110mesh(new THREE.PlaneGeometry(520,8),roadMaterial,scene,[0,-.015,0],[1,1,1],[-Math.PI/2,0,0]);
111box(scene,M.sand,[0,-.28,-10.55],[520,.4,11.1],.01);
112box(scene,M.grass,[0,-.21,50],[520,.4,92],.01);
113box(scene,M.cream,[0,.015,-5.85],[520,.16,3.7],.02);
114box(scene,M.woodLight,[0,-.02,-8.2],[520,.12,1.1],.01);
115box(scene,M.sand,[0,-.07,4.35],[520,.12,.65],.01);
116
117const skyUniforms={top:{value:new THREE.Color('#7ebcc9')},bottom:{value:new THREE.Color('#fff0cb')}};
118const sky = mesh(new THREE.SphereGeometry(500,32,20),new THREE.ShaderMaterial({
119 side:THREE.BackSide, depthWrite:false, uniforms:skyUniforms,
120 vertexShader:'varying vec3 vPos; void main(){vPos=position; gl_Position=projectionMatrix*modelViewMatrix*vec4(position,1.);}',
121 fragmentShader:'uniform vec3 top; uniform vec3 bottom; varying vec3 vPos; void main(){float h=clamp(normalize(vPos).y*2.5+.1,0.,1.); gl_FragColor=vec4(mix(bottom,top,pow(h,.6)),1.); #include <tonemapping_fragment> \n #include <colorspace_fragment> }'
122}),scene,[0,0,0],[1,1,1],[0,0,0],false);
123// Shader chunks must begin on their own line.
124sky.material.fragmentShader=sky.material.fragmentShader.replace(' #include','\n #include');
125const sun=mesh(new THREE.SphereGeometry(12,32,20),new THREE.MeshBasicMaterial({color:'#fff4ca',fog:false}),scene,[-95,38,-245],[1,1,1],[0,0,0],false);
126const oceanUniforms={time:{value:0},lightBlend:{value:0}};
127const oceanMaterial=new THREE.ShaderMaterial({
128 uniforms:oceanUniforms,
129 vertexShader:`uniform float time; varying vec3 vPos; varying float vWave;
130 void main(){vec3 p=position; float w=sin(p.x*.12+time*.8)*.14+sin(p.z*.22-time*1.25+p.x*.06)*.14;
131 p.y+=w; vPos=p; vWave=w; gl_Position=projectionMatrix*modelViewMatrix*vec4(p,1.);}`,
132 fragmentShader:`uniform float time; uniform float lightBlend; varying vec3 vPos; varying float vWave;
133 void main(){float shore=1.-smoothstep(-55.,-16.,-vPos.z); float shallow=smoothstep(-70.,-12.,vPos.z);
134 vec3 blue=mix(vec3(.08,.49,.59),vec3(.29,.77,.72),shallow);
135 float ribbons=sin(vPos.z*.47+sin(vPos.x*.065+time*.3)*1.8-time*1.2);
136 blue+=pow(max(ribbons,0.),14.)*.045;
137 float sparkle=pow(max(0.,sin(vPos.x*3.1+time*.8)*sin(vPos.z*5.7-time*.8)),25.);
138 blue+=sparkle*.32*smoothstep(-220.,-30.,vPos.z);
139 blue=mix(blue,blue*vec3(1.12,.78,.78)+vec3(.12,.05,.035),lightBlend*.7);
140 float mist=smoothstep(95.,300.,length(vPos.xz)); blue=mix(blue,mix(vec3(.7,.83,.81),vec3(.88,.7,.58),lightBlend),mist);
141 gl_FragColor=vec4(blue,1.);
142 #include <tonemapping_fragment>
143 #include <colorspace_fragment>
144 }`
145});
146const oceanGeo=new THREE.PlaneGeometry(900,500,100,65); oceanGeo.rotateX(-Math.PI/2); oceanGeo.translate(0,-.24,-266);
147const ocean=mesh(oceanGeo,oceanMaterial,scene,[0,0,0],[1,1,1],[0,0,0],false);
148const foam=[];
149for(let k=0;k<4;k++) {
150 const n=130, ps=[], indices=[];
151 for(let i=0;i<=n;i++) { const x=(i/n-.5)*420; ps.push(x,-.09,-16-k*.75,x,-.09,-16.18-k*.75); if(i<n){const a=i*2;indices.push(a,a+1,a+2,a+1,a+3,a+2);} }
152 const g=new THREE.BufferGeometry();g.setAttribute('position',new THREE.Float32BufferAttribute(ps,3));g.setIndex(indices);g.computeVertexNormals();
153 const f=mesh(g,new THREE.MeshBasicMaterial({color:'#e4f2d7',transparent:true,opacity:.57-k*.09,side:THREE.DoubleSide,depthWrite:false}),scene,[0,0,0],[1,1,1],[0,0,0],false);
154 foam.push(f);
155}
156
157function palm(parent,x,z,height=5,phase=0) {
158 const g=new THREE.Group();g.position.set(x,0,z);parent.add(g);
159 tube(g,M.wood,[[0,0,0],[.07,height*.34,0],[.36,height*.67,.05],[.55,height,.03]],.12);
160 for(let i=1;i<9;i++) mesh(new THREE.TorusGeometry(.12,.013,5,10),M.woodLight,g,[.55*(i/9)**2,height*i/9,0],[1,1,1],[Math.PI/2,0,-.08]);
161 const crown=new THREE.Group();crown.position.set(.55,height,.03);crown.rotation.y=phase;g.add(crown);
162 for(let j=0;j<7;j++) {
163 const angle=j*Math.PI*2/7, p=[], ind=[];
164 for(let k=0;k<=9;k++) {
165 const t=k/9, l=t*2.5, y=Math.sin(t*Math.PI)*.52-t*t*.72, w=Math.sin(Math.PI*t)*.34;
166 p.push(Math.cos(angle)*l-Math.sin(angle)*w,y,Math.sin(angle)*l+Math.cos(angle)*w);
167 p.push(Math.cos(angle)*l,y+.075*Math.sin(Math.PI*t),Math.sin(angle)*l);
168 p.push(Math.cos(angle)*l+Math.sin(angle)*w,y,Math.sin(angle)*l-Math.cos(angle)*w);
169 if(k<9) { const a=k*3;ind.push(a,a+3,a+1,a+1,a+3,a+4,a+1,a+4,a+2,a+2,a+4,a+5); }
170 }
171 const geo=new THREE.BufferGeometry();geo.setAttribute('position',new THREE.Float32BufferAttribute(p,3));geo.setIndex(ind);geo.computeVertexNormals();
172 const material=(j%2?M.leaf:M.leafLight);material.side=THREE.DoubleSide;
173 mesh(geo,material,crown);
174 }
175 for(let j=0;j<3;j++) ball(crown,M.wood,[.2*Math.cos(j*2),-.12,.2*Math.sin(j*2)],[.17,.21,.16],10);
176}
177function bench(parent,x,z) {
178 for(let i=0;i<4;i++)box(parent,M.woodLight,[x,.65,z+i*.14],[1.8,.1,.11],.015);
179 for(let i=0;i<3;i++)box(parent,M.woodLight,[x,.94+i*.15,z-.12],[1.8,.12,.08],.01,[0,0,0]);
180 for(const dx of [-.63,.63]) {box(parent,M.dark,[x+dx,.33,z+.2],[.08,.61,.46],.015);rod(parent,M.dark,[x+dx,.5,z-.12],[x+dx,1.3,z-.12],.035);}
181}
182function streetLamp(parent,x,z) {
183 rod(parent,M.metal,[x,0,z],[x,4.25,z],.055,.04);
184 tube(parent,M.metal,[[x,3.9,z],[x,4.3,z],[x,4.4,z+.4],[x,4.2,z+.72]],.044);
185 mesh(new THREE.ConeGeometry(.28,.18,12),M.dark,parent,[x,4.15,z+.72]);
186 ball(parent,M.lamp,[x,4.06,z+.72],[.17,.055,.17],12);
187}
188function umbrella(parent,x,z,color) {
189 rod(parent,M.wood,[x,-.02,z],[x,2.17,z],.035);
190 const cover=mesh(new THREE.ConeGeometry(1.55,.55,10,1,true),color,parent,[x,2.04,z]); cover.material.side=THREE.DoubleSide;
191 for(let i=0;i<10;i++) {const a=i*Math.PI*2/10;rod(parent,M.cream,[x,2.31,z],[x+1.53*Math.cos(a),1.77,z+1.53*Math.sin(a)],.018);}
192 ball(parent,M.cream,[x,2.36,z],[.08,.1,.08],10);
193 for(const s of [-1,1]) {
194 const g=new THREE.Group();g.position.set(x+s*.6,0,z+1.1);g.rotation.y=-.12;parent.add(g);
195 box(g,M.woodLight,[0,.3,0],[.55,.1,1.1],.015);box(g,M.cream,[0,.43,-.53],[.52,.09,.8],.015,[-.55,0,0]);
196 rod(g,M.wood,[-.2,0,.3],[-.2,.32,-.3],.025);rod(g,M.wood,[.2,0,.3],[.2,.32,-.3],.025);
197 }
198}
199function roadSign(parent,x,z,type) {
200 rod(parent,M.metal,[x,0,z],[x,2.9,z],.042);
201 const cv=document.createElement('canvas');cv.width=384;cv.height=192;const ctx=cv.getContext('2d');
202 ctx.fillStyle='#397b70';ctx.fillRect(0,0,384,192);ctx.strokeStyle='#f7edce';ctx.lineWidth=5;ctx.strokeRect(10,10,364,172);
203 ctx.fillStyle='#f7edce';ctx.textAlign='center';ctx.font='600 43px Microsoft YaHei, sans-serif';ctx.fillText(type===0?'海岸线 →':'海风充电站',192,87);
204 ctx.font='20px Segoe UI, sans-serif';ctx.fillText(type===0?'COASTAL ROAD':'TAKE IT SLOW',192,137);
205 const texture=new THREE.CanvasTexture(cv);texture.colorSpace=THREE.SRGBColorSpace;
206 const board=mesh(new THREE.BoxGeometry(2.55,1.25,.09),[M.wood,M.wood,M.wood,M.wood,new THREE.MeshStandardMaterial({map:texture,roughness:.8}),M.wood],parent,[x,2.7,z]);
207 board.rotation.y=.25;
208}
209function mergeStatic(group) {
210 group.updateMatrixWorld(true);
211 const byMat=new Map(),old=[];
212 group.traverse(o=>{
213 if(o.isMesh && !Array.isArray(o.material)) {
214 const key=o.material.uuid;
215 if(!byMat.has(key))byMat.set(key,{material:o.material,geometries:[]});
216 const geo=o.geometry.index?o.geometry.toNonIndexed():o.geometry.clone();geo.applyMatrix4(o.matrixWorld);
217 // All merged geometry uses position, normal and uv attributes.
218 if(!geo.attributes.uv)geo.setAttribute('uv',new THREE.Float32BufferAttribute(new Array(geo.attributes.position.count*2).fill(0),2));
219 byMat.get(key).geometries.push(geo);old.push(o);
220 }
221 });
222 for(const o of old)o.parent.remove(o);
223 for(const b of byMat.values()) {const geo=mergeGeometries(b.geometries,false);if(geo){const m=mesh(geo,b.material,group);m.frustumCulled=true;} b.geometries.forEach(g=>g.dispose());}
224 for(const o of old)o.geometry.dispose();
225}
226const tiles=[],tileLength=48,tileCount=7;
227for(let t=0;t<tileCount;t++) {
228 const g=new THREE.Group(),rnd=seeded(t+121);scene.add(g);
229 for(let x=-24;x<24;x+=6)box(g,M.cream,[x,.006,0],[2.7,.02,.11],.006);
230 for(const z of [-3.6,3.6])box(g,M.cream,[0,.007,z],[48,.02,.10],.005);
231 for(let x=-24;x<24;x+=3) {
232 box(g,M.white,[x,.31,-4.55],[.065,.65,.09],.015);
233 if((x+24)%6===0)box(g,M.sand,[x,.055,-4.1],[.8,.15,.23],.01);
234 }
235 for(const h of [.32,.61])box(g,M.white,[0,h,-4.55],[48,.06,.075],.01);
236 for(let x=-22;x<24;x+=24) {palm(g,x,-7.35,4.8+rnd()*.7,t);streetLamp(g,x+10,-4.7);}
237 bench(g,-3,-6.15);
238 for(let j=0;j<6;j++) {
239 const x=-23+rnd()*46,z=7+rnd()*5;
240 ball(g,j%2?M.grassDark:M.grass,[x,.3,z],[.8+rnd()*.7,.35+rnd()*.4,.65+rnd()],10);
241 for(let k=0;k<3;k++)rod(g,M.grassDark,[x+(k-1)*.13,0,z],[x+(k-1)*.3,.7+rnd()*.5,z-.15],.028,.005,5);
242 }
243 palm(g,18,11,5.6,t+.5);
244 for(let j=0;j<4;j++)mesh(new THREE.DodecahedronGeometry(1,0),M.rock,g,[-20+rnd()*40,-.11,-12-rnd()*3],[.2+rnd()*.6,.2+rnd()*.35,.3+rnd()*.7],[0,rnd()*6,0]);
245 if(t%2===0)umbrella(g,-12,-11.5,t%3===0?M.peach:M.teal);
246 if(t===1||t===5)roadSign(g,7,-7.45,t===1?0:1);
247 if(t===3) {
248 box(g,M.peach,[3,1.65,20],[7,3.3,4.3],.05);
249 box(g,M.cream,[3,3.4,20],[7.5,.26,4.8],.025);
250 for(const dx of [-2.2,0,2.2]) {box(g,M.teal,[3+dx,1.9,22.17],[1.25,1.6,.08],.01);box(g,M.white,[3+dx,1.9,22.24],[.06,1.65,.04],.005);}
251 box(g,M.teal,[3,2.5,23.1],[7.3,.16,2.3],.025,[-.1,0,0]);
252 for(const dx of [-3.4,3.4])rod(g,M.white,[3+dx,0,23.5],[3+dx,2.5,23.5],.05);
253 }
254 mergeStatic(g); tiles.push(g);
255}
256
257const distant=new THREE.Group();scene.add(distant);
258for(const [x,z,size] of [[-90,-155,1.1],[68,-185,1.5],[125,-125,.7]]) {
259 const island=new THREE.Group();island.position.set(x,-.8,z);distant.add(island);
260 mesh(new THREE.ConeGeometry(23*size,14*size,7),M.grassDark,island,[0,2,0],[1,.6,.55],[0,.3,0],false);
261 mesh(new THREE.ConeGeometry(18*size,18*size,6),M.rock,island,[12*size,4,0],[1,.5,.55],[0,.6,0],false);
262}
263const lighthouse=new THREE.Group();lighthouse.position.set(39,-.2,-67);distant.add(lighthouse);
264mesh(new THREE.DodecahedronGeometry(6,0),M.rock,lighthouse,[0,-1,0],[1,.45,.65],[0,0,.15],false);
265mesh(new THREE.CylinderGeometry(1.05,1.6,7,12),M.white,lighthouse,[0,3.1,0],[1,1,1],[0,0,0],false);
266mesh(new THREE.CylinderGeometry(1.13,1.2,1.2,12),M.scarf,lighthouse,[0,4.8,0],[1,1,1],[0,0,0],false);
267mesh(new THREE.CylinderGeometry(1.1,1.1,1.3,12),M.glass,lighthouse,[0,7.1,0],[1,1,1],[0,0,0],false);
268mesh(new THREE.ConeGeometry(1.7,1.1,12),M.scarf,lighthouse,[0,8.25,0],[1,1,1],[0,0,0],false);
269box(lighthouse,M.dark,[0,.9,1.38],[.6,1.6,.1],.04);
270const boats=[];
271for(let i=0;i<3;i++) {
272 const g=new THREE.Group();g.position.set(-36+i*38,-.12,-45-i*24);g.rotation.y=.5+i*.8;distant.add(g);
273 ball(g,i%2?M.peach:M.teal,[0,0,0],[1.45,.35,.55],12);
274 rod(g,M.wood,[0,0,0],[0,4.1,0],.038);
275 const sail=flatTriangle(g,M.white,[[.05,.9],[.05,3.95],[2.1,.9]],.02);sail.position.z=.01;
276 const sail2=flatTriangle(g,M.cream,[[-.08,1.1],[-.08,3.8],[-1.05,1.1]],.02);
277 boats.push(g);
278}
279const clouds=[];
280for(let i=0;i<8;i++) {
281 const g=new THREE.Group();g.position.set(-190+i*56,24+(i%3)*7,-200-(i%2)*70);scene.add(g);
282 for(let j=0;j<5;j++)ball(g,M.white,[(j-2)*3.3,Math.sin(j*1.4)*1.5,0],[5,2.2+Math.sin(j)*.6,2.3],12);
283 g.traverse(o=>{if(o.isMesh){o.castShadow=false;o.receiveShadow=false;}}); clouds.push(g);
284}
285const gulls=[];
286for(let i=0;i<7;i++) {
287 const g=new THREE.Group();scene.add(g);
288 ball(g,M.white,[0,0,0],[.25,.12,.085],12);
289 const wings=[];
290 for(const s of [-1,1]) {const pivot=new THREE.Group();g.add(pivot);const wing=flatTriangle(pivot,M.white,[[0,0],[s*.8,-.12],[s*.16,-.23]],.018);wing.rotation.x=Math.PI/2;wings.push(pivot);}
291 gulls.push({g,wings,phase:i*1.7});
292}
293
294// Vehicle and rider are built below; wheel spin is driven by travelled distance.
295const vehicle=new THREE.Group();scene.add(vehicle);
296const chassis=new THREE.Group();vehicle.add(chassis);
297const wheelRadius=.36,wheelbase=1.75;
298function wheel(parent,x) {
299 const g=new THREE.Group();g.position.set(x,wheelRadius,0);parent.add(g);
300 mesh(new THREE.TorusGeometry(.285,.075,12,32),M.tire,g);
301 mesh(new THREE.CylinderGeometry(.235,.235,.12,24),M.metal,g,[0,0,0],[1,1,1],[Math.PI/2,0,0]);
302 for(const z of [-.067,.067]) {
303 mesh(new THREE.TorusGeometry(.218,.018,6,24),M.hub,g,[0,0,z]);
304 for(let k=0;k<6;k++) {
305 const a=k*Math.PI/3;
306 rod(g,M.hub,[Math.cos(a)*.035,Math.sin(a)*.035,z],[Math.cos(a+.16)*.2,Math.sin(a+.16)*.2,z],.021,.014,6);
307 }
308 mesh(new THREE.CylinderGeometry(.06,.06,.017,16),M.cream,g,[0,0,z*1.15],[1,1,1],[Math.PI/2,0,0]);
309 for(let k=0;k<12;k++) {const a=k*Math.PI/6;box(g,M.tire,[Math.cos(a)*.3,Math.sin(a)*.3,z*.73],[.05,.018,.018],.003,[0,0,a]);}
310 }
311 return g;
312}
313const rearWheel=wheel(vehicle,-.87);
314const steering=new THREE.Group();steering.position.set(.88,0,0);vehicle.add(steering);
315const frontWheel=wheel(steering,0);
316// Rounded battery enclosure, floorboard and rear bodywork.
317box(chassis,M.mint,[-.55,.69,0],[1.03,.59,.72],.20);
318box(chassis,M.mint,[.04,.49,0],[1.7,.16,.70],.07);
319box(chassis,M.dark,[.02,.59,0],[.95,.055,.53],.02);
320for(let i=0;i<7;i++)box(chassis,M.mintDark,[-.35+i*.1,.621,0],[.018,.009,.40],.002);
321box(chassis,M.seat,[-.52,1.025,0],[.89,.18,.71],.08);
322box(chassis,M.cream,[-.52,.953,0],[.91,.045,.68],.02);
323box(chassis,M.mintDark,[-.91,.78,0],[.43,.07,.77],.03);
324rod(chassis,M.metal,[-.83,.43,.20],[-.56,.88,.20],.06);
325rod(chassis,M.metal,[-.83,.43,-.20],[-.56,.88,-.20],.06);
326box(chassis,M.red,[-1.09,.85,0],[.065,.15,.28],.035);
327box(chassis,M.dark,[-1.12,.63,0],[.055,.21,.34],.015,[0,0,-.18]);
328box(chassis,M.cream,[-1.155,.64,0],[.012,.12,.26],.01,[0,0,-.18]);
329// Small electric badge: a geometric lightning bolt on each side.
330for(const s of [-1,1]) {
331 const emblem=flatTriangle(chassis,M.cream,[[-.10,.07],[.07,.07],[-.07,-.07]],.01);
332 emblem.position.set(-.58,.71,s*.367);if(s<0)emblem.rotation.y=Math.PI;
333 const emblem2=flatTriangle(chassis,M.cream,[[.06,.07],[.10,-.07],[-.07,-.07]],.01);
334 emblem2.position.copy(emblem.position);emblem2.rotation.copy(emblem.rotation);
335 // Rear luggage rail.
336 tube(chassis,M.hub,[[-.87,1.01,s*.40],[-1.10,1.12,s*.4],[-1.13,1.14,0]],.025);
337}
338const basket=new THREE.Group();basket.position.set(-1.10,1.27,0);chassis.add(basket);
339box(basket,M.woodLight,[0,0,0],[.40,.27,.52],.03);
340for(let i=0;i<4;i++)for(const s of [-1,1])box(basket,M.wood,[-.14+i*.092,0,s*.265],[.021,.25,.015],.003);
341box(basket,M.cream,[.02,.135,0],[.25,.11,.36],.04);
342// A little baguette and beach towel in the rear basket.
343ball(basket,M.sand,[-.04,.25,.06],[.07,.25,.07],12).rotation.z=-.4;
344for(let i=0;i<3;i++)box(basket,M.woodLight,[-.04+i*.045,.27+i*.055,.123],[.08,.018,.01],.005,[0,0,.45]);
345box(basket,M.teal,[.05,.21,-.13],[.29,.16,.13],.05);
346
347box(chassis,M.mint,[.57,1.0,0],[.26,.87,.66],.11,[0,0,.10]);
348box(chassis,M.cream,[.715,1.0,0],[.025,.52,.40],.03,[0,0,.10]);
349box(chassis,M.mintDark,[.72,1.13,0],[.03,.11,.25],.02,[0,0,.10]);
350for(const s of [-1,1])rod(steering,M.metal,[0,.36,s*.15],[-.14,1.37,s*.15],.042);
351// Front fender is a true curved arch over the rotating tire.
352mesh(new THREE.TorusGeometry(.40,.061,9,24,Math.PI),M.mint,steering,[0,.35,0],[1,1,2.0]);
353mesh(new THREE.TorusGeometry(.4,.055,8,20,Math.PI),M.mint,chassis,[-.87,.35,0],[1,1,2]);
354rod(steering,M.metal,[-.14,1.2,0],[-.18,1.59,0],.047);
355box(steering,M.mint,[-.11,1.48,0],[.38,.26,.50],.095);
356mesh(new THREE.CylinderGeometry(.16,.16,.08,24),M.hub,steering,[.095,1.49,0],[1,1,1],[0,0,-Math.PI/2]);
357mesh(new THREE.CylinderGeometry(.137,.137,.035,24),M.lamp,steering,[.145,1.49,0],[1,1,1],[0,0,-Math.PI/2]);
358ball(steering,M.glint,[.172,1.49,0],[.018,.104,.104],16);
359rod(steering,M.hub,[-.18,1.59,-.49],[-.18,1.59,.49],.038);
360const gripPoints=[];
361for(const s of [-1,1]) {
362 rod(steering,M.dark,[-.18,1.59,s*.35],[-.18,1.59,s*.51],.052);
363 rod(steering,M.hub,[-.14,1.55,s*.38],[-.035,1.55,s*.47],.016);
364 tube(steering,M.metal,[[-.16,1.59,s*.35],[-.14,1.8,s*.50],[-.11,1.91,s*.55]],.017);
365 ball(steering,M.mint,[-.1,1.95,s*.55],[.07,.105,.14],16);
366 ball(steering,M.glass,[-.073,1.95,s*.55],[.048,.083,.112],16);
367 const gp=new THREE.Object3D();gp.position.set(-.18,1.61,s*.44);steering.add(gp);gripPoints.push(gp);
368}
369box(steering,M.dark,[-.26,1.62,0],[.14,.05,.20],.03,[0,0,.2]);
370box(steering,M.teal,[-.26,1.655,0],[.095,.012,.14],.02,[0,0,.2]);
371
372const rider=new THREE.Group();chassis.add(rider);
373const torso=ball(rider,M.shirt,[-.40,1.52,0],[.34,.47,.30],24);torso.rotation.z=-.13;
374ball(rider,M.orange,[-.51,1.12,0],[.31,.23,.29],20);
375// Hoodie trim and central zipper.
376tube(rider,M.mintDark,[[-.35,1.17,-.27],[-.20,1.19,0],[-.35,1.17,.27]],.025);
377tube(rider,M.woodLight,[[-.08,1.32,0],[-.045,1.55,0],[-.10,1.76,0]],.008);
378for(const s of [-1,1]) {
379 ball(rider,M.orange,[-.26,1.06,s*.24],[.28,.17,.15],18).rotation.z=-.4;
380 rod(rider,M.orange,[-.05,.89,s*.27],[.24,.57,s*.28],.105,.12,14);
381 ball(rider,M.muzzle,[.28,.56,s*.29],[.19,.09,.14],18);
382 for(let j=0;j<2;j++)tube(rider,M.stripe,[[.33,.60,s*.27+(j-.5)*.08],[.41,.595,s*.27+(j-.5)*.08]],.007);
383}
384const head=new THREE.Group();head.position.set(-.17,2.08,0);rider.add(head);
385ball(head,M.orange,[0,0,0],[.43,.43,.39],28);
386ball(head,M.orange,[.05,-.19,0],[.38,.24,.37],24);
387// Forward-facing cream muzzle, glossy eyes, nose, mouth and whiskers.
388for(const s of [-1,1]) {
389 ball(head,M.muzzle,[.365,-.115,s*.115],[.13,.12,.15],20);
390 ball(head,M.eyes,[.347,.105,s*.206],[.065,.12,.092],24);
391 ball(head,M.glint,[.407,.152,s*.203],[.020,.030,.023],14);
392 ball(head,M.glint,[.409,.080,s*.23],[.012,.015,.012],10);
393 ball(head,M.pink,[.315,-.11,s*.288],[.02,.04,.06],14);
394 for(let j=0;j<3;j++) {
395 const y=-.14+(j-1)*.048;
396 tube(head,M.dark,[[.414,y,s*.20],[.425,y+(j-1)*.025,s*.37],[.391,y+(j-1)*.054,s*.52]],.006);
397 }
398 for(let j=0;j<2;j++)tube(head,M.stripe,[[.16,.01-j*.115,s*.355],[.04,-.015-j*.115,s*.385],[-.04,-.04-j*.10,s*.373]],.027);
399 const ear=new THREE.Group();ear.position.set(.01,.29,s*.39);head.add(ear);
400 const e=flatTriangle(ear,M.orange,[[-.19,0],[.19,0],[-s*.06,.34]],.075);e.rotation.y=Math.PI/2;e.position.x=.0;
401 const inner=flatTriangle(ear,M.pink,[[-.12,.045],[.12,.045],[-s*.044,.27]],.008);inner.rotation.y=Math.PI/2;inner.position.x=.083;
402 ball(head,M.orange,[.01,.29,s*.36],[.12,.12,.16],16);
403}
404ball(head,M.pink,[.494,-.05,0],[.041,.027,.047],16);
405tube(head,M.dark,[[.497,-.073,0],[.490,-.132,0]],.009);
406for(const s of [-1,1])tube(head,M.dark,[[.49,-.13,0],[.475,-.157,s*.05],[.45,-.141,s*.08]],.007);
407// Forehead tabby markings peek below the helmet rim.
408for(const s of [-1,1])tube(head,M.stripe,[[.31,.27,s*.08],[.36,.23,s*.115],[.35,.20,s*.135]],.023);
409const helmet=mesh(new THREE.SphereGeometry(.456,32,20,0,Math.PI*2,0,Math.PI*.47),M.cream,head,[0,.145,0],[1,1,.97]);
410mesh(new THREE.TorusGeometry(.451,.024,8,32),M.mintDark,head,[0,.189,0],[1,1,.97],[Math.PI/2,0,0]);
411tube(head,M.mintDark,[[-.43,.21,0],[-.34,.45,0],[0,.602,0],[.34,.45,0],[.43,.21,0]],.038);
412for(const s of [-1,1])tube(head,M.seat,[[-.04,.15,s*.4],[-.03,-.15,s*.34],[.05,-.30,s*.14],[.11,-.32,0]],.018);
413box(head,M.metal,[.10,-.315,.13],[.07,.05,.10],.014);
414
415const arms=[];
416for(const s of [-1,1]) {
417 const shoulder=new THREE.Vector3(-.27,1.74,s*.285);
418 const upper=mesh(new THREE.CylinderGeometry(.105,.12,1,14),M.shirt,rider);
419 const fore=mesh(new THREE.CylinderGeometry(.073,.09,1,14),M.orange,rider);
420 const elbow=ball(rider,M.orange,[0,0,0],[.09,.09,.09],16);
421 const hand=ball(rider,M.muzzle,[0,0,0],[.115,.10,.105],20);
422 const cuff=ball(rider,M.mintDark,[0,0,0],[.107,.07,.107],16);
423 arms.push({s,shoulder,upper,fore,elbow,hand,cuff,target:new THREE.Vector3()});
424}
425function placeLimb(obj,a,b) {
426 const dir=new THREE.Vector3().subVectors(b,a),length=dir.length();
427 obj.position.copy(a).add(b).multiplyScalar(.5);obj.scale.set(1,length,1);
428 obj.quaternion.setFromUnitVectors(new THREE.Vector3(0,1,0),dir.normalize());
429}
430const tailGeo=new THREE.BufferGeometry(),tailPos=[],tailIndex=[],tailN=25,tailR=8;
431for(let i=0;i<=tailN;i++)for(let j=0;j<tailR;j++)tailPos.push(0,0,0);
432for(let i=0;i<tailN;i++)for(let j=0;j<tailR;j++){const a=i*tailR+j,b=i*tailR+(j+1)%tailR;tailIndex.push(a,a+tailR,b,b,a+tailR,b+tailR);}
433tailGeo.setAttribute('position',new THREE.Float32BufferAttribute(tailPos,3));tailGeo.setIndex(tailIndex);
434const tail=mesh(tailGeo,M.orange,rider);
435const tailBands=[];
436for(let j=0;j<4;j++) {const ring=mesh(new THREE.TorusGeometry(.079,.017,6,12),M.stripe,rider);ring.rotation.y=Math.PI/2;tailBands.push(ring);}
437function tailCenter(u,t) {return new THREE.Vector3(-.62-u*.92,1.20+Math.sin(u*2.7)*.31,.29+Math.sin(t*2+u*3)*u*.17);}
438function animateTail(t) {
439 const p=tailGeo.attributes.position;
440 for(let i=0;i<=tailN;i++) {
441 const u=i/tailN,c=tailCenter(u,t),rad=.086*(1-u*.67);
442 for(let j=0;j<tailR;j++){const a=j/tailR*Math.PI*2;p.setXYZ(i*tailR+j,c.x,c.y+Math.cos(a)*rad,c.z+Math.sin(a)*rad);}
443 }
444 p.needsUpdate=true;tailGeo.computeVertexNormals();
445 for(let j=0;j<4;j++){const u=.35+j*.17,c=tailCenter(u,t);tailBands[j].position.copy(c);tailBands[j].scale.setScalar(1-u*.67);}
446}
447// Wind-blown scarf is a deformed ribbon, not a prerecorded texture.
448tube(rider,M.scarf,[[-.33,1.79,-.25],[-.07,1.82,0],[-.33,1.79,.25]],.073);
449ball(rider,M.scarf,[-.51,1.82,.27],[.11,.10,.10],14);
450const scarfGeo=new THREE.BufferGeometry(),scarfPos=[],scarfIx=[];
451for(let i=0;i<=18;i++){scarfPos.push(0,0,0,0,0,0);if(i<18){const a=i*2;scarfIx.push(a,a+1,a+2,a+1,a+3,a+2);}}
452scarfGeo.setAttribute('position',new THREE.Float32BufferAttribute(scarfPos,3));scarfGeo.setIndex(scarfIx);
453const scarfMat=M.scarf.clone();scarfMat.side=THREE.DoubleSide;
454const scarf=mesh(scarfGeo,scarfMat,rider);
455function animateScarf(t) {
456 const p=scarfGeo.attributes.position;
457 for(let i=0;i<=18;i++) {const u=i/18,x=-.49-u*.88,y=1.82+.07*Math.sin(t*5-u*5)*u+.10*u,z=.27+u*.1,w=.105*(1-u*.22);p.setXYZ(i*2,x,y-w,z);p.setXYZ(i*2+1,x,y+w,z+.025);}
458 p.needsUpdate=true;scarfGeo.computeVertexNormals();
459}
460const hornRings=[];
461for(let i=0;i<3;i++) {const m=mesh(new THREE.TorusGeometry(.20,.015,6,28),new THREE.MeshBasicMaterial({color:'#fff6c3',transparent:true,opacity:0,depthWrite:false}),steering,[.25,1.5,0],[1,1,1],[0,Math.PI/2,0],false);m.visible=false;hornRings.push(m);}
462
463const eyeObjects=[];head.traverse(o=>{if(o.isMesh&&(o.material===M.eyes||o.material===M.glint))eyeObjects.push({o,sy:o.scale.y});});
464let pausedByHidden=false, lastTime=performance.now(), toastTimer, dragging=false, audioContext;
465const viewNames=['随行视角','侧面视角','海岸全景'];
466const viewOffsets=[new THREE.Vector3(6.0,3.65,7.7),new THREE.Vector3(.1,3.05,9.2),new THREE.Vector3(9,10.5,17.5)];
467const desiredCamera=new THREE.Vector3(),desiredTarget=new THREE.Vector3();
468const mobile=()=>innerWidth<600;
469function cameraOffset() {const v=viewOffsets[Math.max(0,state.view)].clone();if(mobile()){v.multiplyScalar(1.27);v.y+=.5;}return v;}
470camera.position.copy(cameraOffset()).add(new THREE.Vector3(0,0,1.5));controls.update();
471controls.addEventListener('start',()=>{dragging=true;state.view=-1;$('viewLabel').textContent='自由视角';$('view').setAttribute('aria-label','切换镜头,当前自由视角');});
472controls.addEventListener('end',()=>{dragging=false;});
473function toast(msg) {clearTimeout(toastTimer);$('toast').textContent=msg;$('toast').classList.add('visible');toastTimer=setTimeout(()=>$('toast').classList.remove('visible'),2200);}
474function updatePlayUI() {
475 $('play').setAttribute('aria-pressed',String(state.paused));
476 $('play').setAttribute('aria-label',state.paused?'继续骑行':'暂停骑行');
477 $('playIcon').innerHTML=state.paused?'<path d="m9 5 10 7-10 7z" fill="currentColor" stroke="none"/>':'<path d="M8 6v12M16 6v12" stroke-width="3"/>';
478 $('weather').textContent=state.paused?'停一停,看看海':state.sunset?'落日兜风中':'海风正好';
479 $('speedReadout').textContent=state.paused?'0':state.kmh;
480}
481$('play').addEventListener('click',()=>{state.paused=!state.paused;if(!state.paused){clearTimeout(toastTimer);$('toast').classList.remove('visible');}updatePlayUI();});
482$('speedControl').addEventListener('input',e=>{state.kmh=Number(e.target.value);$('speedLabel').textContent=state.kmh+' km/h';e.target.setAttribute('aria-valuetext',state.kmh+' 千米每小时');updatePlayUI();});
483$('view').addEventListener('click',()=>{state.view=(state.view+1)%3;$('viewLabel').textContent=viewNames[state.view];$('view').setAttribute('aria-label','切换镜头,当前'+viewNames[state.view]);toast(viewNames[state.view]+' · 拖动可自由环视');});
484$('sunset').addEventListener('click',()=>{state.sunset=!state.sunset;$('sunset').setAttribute('aria-pressed',String(state.sunset));$('sunset').setAttribute('aria-label',state.sunset?'切换晴日':'切换黄昏');$('lightLabel').textContent=state.sunset?'黄昏':'晴日';updatePlayUI();});
485async function horn() {
486 state.hornTime=performance.now()/1000;toast('滴滴~海边借过!');
487 try {
488 audioContext??=new (window.AudioContext||window.webkitAudioContext)();await audioContext.resume();
489 const start=audioContext.currentTime;
490 for(let i=0;i<2;i++) {
491 const o=audioContext.createOscillator(),g=audioContext.createGain();o.type='sine';o.frequency.setValueAtTime(i?660:550,start+i*.14);
492 g.gain.setValueAtTime(0,start+i*.14);g.gain.linearRampToValueAtTime(.09,start+i*.14+.018);g.gain.exponentialRampToValueAtTime(.001,start+i*.14+.13);
493 o.connect(g);g.connect(audioContext.destination);o.start(start+i*.14);o.stop(start+i*.14+.14);
494 }
495 } catch { /* The visual horn remains available if the browser disables sound. */ }
496}
497$('horn').addEventListener('click',horn);
498window.addEventListener('keydown',e=>{
499 if(e.repeat||e.ctrlKey||e.metaKey||e.altKey||/INPUT|SELECT|TEXTAREA/.test(e.target.tagName))return;
500 if(e.code==='Space'){e.preventDefault();$('play').click();}
501 if(e.code==='KeyC')$('view').click();if(e.code==='KeyL')$('sunset').click();if(e.code==='KeyH')horn();
502});
503document.addEventListener('visibilitychange',()=>{pausedByHidden=document.hidden;lastTime=performance.now();});
504renderer.domElement.addEventListener('webglcontextlost',e=>{e.preventDefault();state.paused=true;updatePlayUI();$('errorMessage').textContent='浏览器的三维画面暂时中断。重新打开页面即可继续海岸骑行。';$('error').style.display='grid';});
505window.addEventListener('resize',()=>{camera.aspect=innerWidth/innerHeight;camera.updateProjectionMatrix();renderer.setSize(innerWidth,innerHeight);});
506updatePlayUI();
507
508const colorCache={
509 fogDay:new THREE.Color('#bfdfe2'),fogNight:new THREE.Color('#e5b49a'),
510 skyDay:new THREE.Color('#7ebcc9'),skyNight:new THREE.Color('#8da5be'),
511 horizonDay:new THREE.Color('#fff0cb'),horizonNight:new THREE.Color('#ffc591'),
512 lightDay:new THREE.Color('#fff4d4'),lightNight:new THREE.Color('#ffc29a'),
513 hemiDay:new THREE.Color('#eff8e6'),hemiNight:new THREE.Color('#e5d6c5')
514};
515const armWorld=new THREE.Vector3();
516let maxGripError=0;
517function updateRider(t) {
518 rider.position.y=.008*Math.sin(t*6.5);
519 head.rotation.y=.06*Math.sin(t*.8);head.rotation.z=.012*Math.sin(t*1.7);
520 const blinkPhase=t%5.1,blink=blinkPhase>4.85?Math.max(.09,Math.abs(blinkPhase-4.975)/.125):1;
521 for(const {o,sy} of eyeObjects)o.scale.y=sy*blink;
522 animateTail(t);animateScarf(t);
523 vehicle.updateMatrixWorld(true);
524 maxGripError=0;
525 arms.forEach((a,i)=>{
526 gripPoints[i].getWorldPosition(a.target);rider.worldToLocal(a.target);
527 const elbow=new THREE.Vector3(.08,1.43+a.s*.01,a.s*.38);
528 placeLimb(a.upper,a.shoulder,elbow);placeLimb(a.fore,elbow,a.target);
529 a.elbow.position.copy(elbow);a.hand.position.copy(a.target);
530 a.cuff.position.copy(a.shoulder).lerp(elbow,.66);
531 a.hand.updateWorldMatrix(true,false);a.hand.getWorldPosition(armWorld);
532 maxGripError=Math.max(maxGripError,armWorld.distanceTo(gripPoints[i].getWorldPosition(new THREE.Vector3())));
533 });
534}
535function updateWorld(t) {
536 const span=tileLength*tileCount;
537 tiles.forEach((g,i)=>{g.position.x=((i*tileLength-state.distance+span*.5)%span+span)%span-span*.5;});
538 asphaltTexture.offset.x=state.distance/520*220;
539 const wavePhase=state.distance*.009;
540 distant.position.x=-Math.sin(wavePhase)*9;
541 oceanUniforms.time.value=t;oceanUniforms.lightBlend.value=state.lightBlend;
542 foam.forEach((f,k)=>{
543 const a=f.geometry.attributes.position;
544 for(let i=0;i<a.count;i++){const x=a.getX(i);a.setY(i,-.10+Math.sin(x*.08+t*.85+k)*.035);a.setZ(i,-16-k*.86-(i%2)*(.14+k*.06)+Math.sin(x*.038+t*.38+k*.5)*.3+Math.sin(t*.75+k)*.38);}
545 a.needsUpdate=true;f.material.opacity=(.45+Math.sin(t*.6+k)*.13)*(1-k*.15);
546 });
547 clouds.forEach((g,i)=>{g.position.x=-190+i*56+Math.sin(t*.035+i)*10;});
548 boats.forEach((g,i)=>{g.position.y=-.12+Math.sin(t*.8+i)*.10;g.rotation.z=Math.sin(t*.9+i)*.025;});
549 gulls.forEach(({g,wings,phase},i)=>{
550 g.position.set(-20+Math.sin(t*.12+phase)*29,9+i%3*2+Math.sin(t*.4+phase),-24-Math.cos(t*.12+phase)*13-i*2);
551 g.rotation.y=-t*.12-phase;g.rotation.z=.10*Math.cos(t*.12+phase);
552 wings.forEach((w,j)=>{w.rotation.x=(j?1:-1)*(.2+Math.sin(t*4.5+phase)*.35);});
553 });
554 const elapsed=performance.now()/1000-state.hornTime;
555 hornRings.forEach((m,i)=>{const p=elapsed-i*.14;m.visible=p>=0&&p<.7;if(m.visible){m.position.x=.24+p*.9;m.scale.setScalar(1+p*2);m.material.opacity=(1-p/.7)*.7;}});
556}
557function updateLighting(dt) {
558 const target=state.sunset?1:0;
559 state.lightBlend+= (target-state.lightBlend)*(reducedMotion?1:1-Math.exp(-dt*2.3));
560 const k=state.lightBlend;
561 skyUniforms.top.value.lerpColors(colorCache.skyDay,colorCache.skyNight,k);
562 skyUniforms.bottom.value.lerpColors(colorCache.horizonDay,colorCache.horizonNight,k);
563 scene.fog.color.lerpColors(colorCache.fogDay,colorCache.fogNight,k);
564 scene.background.copy(scene.fog.color);
565 sunLight.color.lerpColors(colorCache.lightDay,colorCache.lightNight,k);
566 sunLight.intensity=3.6-k*1.1;sunLight.position.y=22-k*12;
567 hemi.color.lerpColors(colorCache.hemiDay,colorCache.hemiNight,k);hemi.intensity=2.5-k*.65;
568 sun.position.y=38-k*26;sun.material.color.set(k>.5?'#ffdb9c':'#fff4ca');
569 M.lamp.emissiveIntensity=.25+k*1.2;
570 renderer.toneMappingExposure=1.22-k*.06;
571}
572const probe={};
573function recordState() {
574 Object.assign(probe,{ready:true,paused:state.paused,kmh:state.kmh,distance:state.distance,time:state.time,
575 rearWheelAngle:rearWheel.rotation.z,frontWheelAngle:frontWheel.rotation.z,wheelRadius,steering:steering.rotation.y,
576 gripError:maxGripError,view:state.view,sunset:state.sunset,lightBlend:state.lightBlend,frames:state.frames,
577 renderCalls:renderer.info.render.calls,triangles:renderer.info.render.triangles,webgl2:renderer.capabilities.isWebGL2,
578 catPosition:vehicle.position.toArray(),camera:camera.position.toArray(),tailTip:tailCenter(1,state.time).toArray()});
579 document.documentElement.dataset.distance=state.distance.toFixed(6);
580 document.documentElement.dataset.wheelRotation=rearWheel.rotation.z.toFixed(6);
581 document.documentElement.dataset.paused=String(state.paused);
582 document.documentElement.dataset.time=state.time.toFixed(6);
583 document.documentElement.dataset.frontWheelRotation=frontWheel.rotation.z.toFixed(6);
584 document.documentElement.dataset.wheelRadius=String(wheelRadius);
585 document.documentElement.dataset.gripError=maxGripError.toExponential(3);
586 document.documentElement.dataset.view=String(state.view);
587 document.documentElement.dataset.sunset=String(state.sunset);
588 document.documentElement.dataset.lightBlend=state.lightBlend.toFixed(5);
589 document.documentElement.dataset.frames=String(state.frames);
590 document.documentElement.dataset.webgl=String(renderer.capabilities.isWebGL2);
591 document.documentElement.dataset.drawCalls=String(renderer.info.render.calls);
592}
593window.__coastalRide=probe;
594function animate(now) {
595 requestAnimationFrame(animate);
596 const dt=Math.max(0,Math.min((now-lastTime)/1000,.06));lastTime=now;
597 const running=!state.paused&&!pausedByHidden;
598 if(running) {
599 const ds=state.kmh/3.6*dt;state.distance+=ds;state.time+=dt;
600 const curvature=-.24/625*Math.sin(state.distance/25);
601 const steer=Math.atan(wheelbase*curvature);
602 state.wheelRear-=ds/wheelRadius;state.wheelFront-=ds/(wheelRadius*Math.cos(steer));
603 steering.rotation.y=-steer;
604 }
605 const t=state.time;
606 vehicle.position.z=1.5+.24*Math.sin(state.distance/25);
607 vehicle.rotation.y=-Math.atan(.24/25*Math.cos(state.distance/25));
608 vehicle.rotation.x=-.008*Math.sin(state.distance/25)*(state.kmh/18)**2;
609 chassis.position.y=.006*Math.sin(t*6.5);
610 rearWheel.rotation.z=state.wheelRear;frontWheel.rotation.z=state.wheelFront;
611 updateRider(t);updateWorld(t);updateLighting(dt);
612 if(state.view>=0&&!dragging) {
613 desiredTarget.set(.1,state.view===2?1.3:1.65,vehicle.position.z);
614 desiredCamera.copy(cameraOffset()).add(new THREE.Vector3(0,0,vehicle.position.z));
615 const f=1-Math.exp(-dt*4);
616 camera.position.lerp(desiredCamera,f);controls.target.lerp(desiredTarget,f);
617 }
618 controls.update();renderer.render(scene,camera);state.frames++;
619 if(state.frames%6===0)recordState();
620 if(state.frames===2){$('loading').classList.add('fade');setTimeout(()=>$('loading').remove(),650);if(reducedMotion&&state.paused)toast('已暂停 · 点播放开始海岸骑行');recordState();}
621}
622updateRider(0);updateWorld(0);recordState();requestAnimationFrame(animate);