05 gpt-6.1-sol / max · 原始源码

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

回到评估报告 · 下载原源码
1import * as THREE from 'three';
2import { mergeGeometries } from 'three/addons/utils/BufferGeometryUtils.js';
3
4const $ = (id) => document.getElementById(id);
5const V = (x = 0, y = 0, z = 0) => new THREE.Vector3(x, y, z);
6const clamp = THREE.MathUtils.clamp;
7const loading = $('loading');
8let renderer;
9try {
10 renderer = new THREE.WebGLRenderer({ antialias: true, alpha: false, powerPreference: 'high-performance' });
11} catch (error) {
12 $('loading-detail').textContent = '当前浏览器没有启用 3D 图形。请在 Chrome 或 Edge 中启用硬件加速,再打开这个网页。';
13 document.documentElement.dataset.error = error.message;
14 throw error;
15}
16renderer.setPixelRatio(Math.min(devicePixelRatio, innerWidth < 650 ? 1.5 : 1.75));
17renderer.setSize(innerWidth, innerHeight);
18renderer.outputColorSpace = THREE.SRGBColorSpace;
19renderer.toneMapping = THREE.ACESFilmicToneMapping;
20renderer.toneMappingExposure = 1.2;
21renderer.shadowMap.enabled = true;
22renderer.shadowMap.type = THREE.PCFSoftShadowMap;
23$('world').appendChild(renderer.domElement);
24const scene = new THREE.Scene();
25scene.background = new THREE.Color('#d4edee');
26scene.fog = new THREE.Fog('#d9ede6', 58, 170);
27const camera = new THREE.PerspectiveCamera(42, innerWidth / innerHeight, .1, 450);
28scene.add(new THREE.HemisphereLight('#e5f9ff', '#d8b582', 2.4));
29const sunlight = new THREE.DirectionalLight('#fff1d6', 3.1);
30sunlight.castShadow = true;
31sunlight.shadow.mapSize.set(innerWidth < 650 ? 1024 : 2048, innerWidth < 650 ? 1024 : 2048);
32Object.assign(sunlight.shadow.camera, { left: -13, right: 13, top: 13, bottom: -13, near: 1, far: 95 });
33sunlight.shadow.bias = -.0003;
34sunlight.shadow.normalBias = .035;
35sunlight.shadow.radius = 3;
36scene.add(sunlight, sunlight.target);
37
38const mats = {};
39function material(name, color, extra = {}) {
40 mats[name] = new THREE.MeshStandardMaterial({ color, roughness: .82, ...extra });
41 return mats[name];
42}
43material('fur', '#e89b46'); material('furLight', '#f7b35c'); material('tabby', '#a85c31');
44material('cream', '#fff0ce'); material('pink', '#de8b7c'); material('ink', '#283d40');
45material('mint', '#60b8aa', { roughness: .35, metalness: .12 });
46material('mintLight', '#9ad5bc', { roughness: .48 }); material('shirt', '#437f81');
47material('scarf', '#e98163'); material('rubber', '#283b42', { roughness: .95 });
48material('metal', '#c3d0ca', { roughness: .32, metalness: .65 }); material('seat', '#634b3e');
49material('road', '#627879'); material('paint', '#fbf1cd'); material('concrete', '#efe7c9');
50material('sand', '#f0d7a4'); material('grass', '#a3ba87'); material('grassLight', '#c0ce96');
51material('leaf', '#488b72'); material('leafLight', '#76a782'); material('trunk', '#b49163');
52material('rock', '#9ba591'); material('roof', '#d27d5d'); material('house', '#f5dbb5');
53material('housePink', '#dfb6a0'); material('glass', '#376a71', { roughness: .24, metalness: .12 });
54material('blue', '#7bbabd'); material('yellow', '#f1bc65');
55material('lamp', '#fff6bc', { emissive: '#ffdf95', emissiveIntensity: .5 });
56const sphereGeo = new THREE.SphereGeometry(1, 24, 16);
57const boxGeo = new THREE.BoxGeometry(1, 1, 1);
58const yAxis = V(0, 1, 0);
59function mesh(parent, geometry, mat, x = 0, y = 0, z = 0) {
60 const m = new THREE.Mesh(geometry, mat); m.position.set(x, y, z);
61 m.castShadow = true; m.receiveShadow = true; parent.add(m); return m;
62}
63function box(parent, mat, pos, scale) { const m = mesh(parent, boxGeo, mat, ...pos); m.scale.set(...scale); return m; }
64function ball(parent, mat, pos, scale) { const m = mesh(parent, sphereGeo, mat, ...pos); m.scale.set(...scale); return m; }
65function cylinderBetween(parent, mat, a, b, radius, topRadius = radius, segments = 10) {
66 const start = V(...a), end = V(...b), delta = end.clone().sub(start);
67 const m = mesh(parent, new THREE.CylinderGeometry(topRadius, radius, delta.length(), segments), mat);
68 m.position.copy(start).add(end).multiplyScalar(.5); m.quaternion.setFromUnitVectors(yAxis, delta.normalize()); return m;
69}
70function curveTube(parent, mat, points, radius, segments = 24) {
71 return mesh(parent, new THREE.TubeGeometry(new THREE.CatmullRomCurve3(points.map(p => V(...p))), segments, radius, 6, false), mat);
72}
73function roundedBox(parent, mat, width, height, depth, radius, pos) {
74 const s = new THREE.Shape(), x = -width / 2, y = -height / 2;
75 s.moveTo(x + radius, y); s.lineTo(x + width - radius, y);
76 s.quadraticCurveTo(x + width, y, x + width, y + radius); s.lineTo(x + width, y + height - radius);
77 s.quadraticCurveTo(x + width, y + height, x + width - radius, y + height); s.lineTo(x + radius, y + height);
78 s.quadraticCurveTo(x, y + height, x, y + height - radius); s.lineTo(x, y + radius);
79 s.quadraticCurveTo(x, y, x + radius, y);
80 return mesh(parent, new THREE.ExtrudeGeometry(s, { depth, bevelEnabled: true, bevelSegments: 2, steps: 1, bevelSize: .025, bevelThickness: .025, curveSegments: 5 }), mat, ...pos);
81}
82function batchStatic(group) {
83 group.updateMatrixWorld(true);
84 const byMaterial = new Map();
85 group.traverse(o => {
86 if (!o.isMesh) return;
87 const g = o.geometry.clone(); g.applyMatrix4(o.matrixWorld);
88 if (g.index) { const flat = g.toNonIndexed(); g.dispose(); byMaterial.set(o.material, [...(byMaterial.get(o.material) || []), flat]); }
89 else byMaterial.set(o.material, [...(byMaterial.get(o.material) || []), g]);
90 });
91 group.clear();
92 for (const [mat, geometries] of byMaterial) {
93 const combined = mergeGeometries(geometries, false);
94 geometries.forEach(g => g.dispose());
95 mesh(group, combined, mat);
96 }
97}
98
99// Sky and water are generated locally: no remote images or model files.
100const sky = new THREE.Mesh(new THREE.SphereGeometry(290, 32, 20), new THREE.ShaderMaterial({
101 side: THREE.BackSide, depthWrite: false,
102 vertexShader: 'varying vec3 vDir;void main(){vDir=position;gl_Position=projectionMatrix*modelViewMatrix*vec4(position,1.0);}',
103 fragmentShader: `varying vec3 vDir;void main(){vec3 d=normalize(vDir);float t=smoothstep(-.12,.72,d.y);vec3 col=mix(vec3(.86,.94,.89),vec3(.55,.80,.86),t);float glow=pow(max(0.,dot(d,normalize(vec3(-.6,.25,.9)))),32.);col+=glow*vec3(.09,.04,-.015);gl_FragColor=vec4(col,1.);}`
104}));
105sky.renderOrder = -10; scene.add(sky);
106const sun = mesh(scene, new THREE.SphereGeometry(6.5, 24, 16), new THREE.MeshBasicMaterial({ color: '#fff3c5', fog: false }), -113, 53, 168);
107sun.castShadow = false;
108const seaUniforms = { uTime: { value: 0 }, uTravel: { value: 0 } };
109const ocean = new THREE.Mesh(new THREE.PlaneGeometry(330, 660, 80, 160), new THREE.ShaderMaterial({
110 uniforms: seaUniforms,
111 vertexShader: `uniform float uTime;uniform float uTravel;varying vec3 vWorld;void main(){vec3 p=position;float z=p.y+uTravel;float w=sin(p.x*.37+z*.21+uTime*1.2)*.047+sin(z*.51-p.x*.12-uTime*.8)*.028;p.z+=w;vec4 world=modelMatrix*vec4(p,1.);vWorld=world.xyz;gl_Position=projectionMatrix*viewMatrix*world;}`,
112 fragmentShader: `uniform float uTime;varying vec3 vWorld;void main(){float x=vWorld.x,z=vWorld.z;float nearShore=smoothstep(-90.,-7.,x);vec3 col=mix(vec3(.19,.57,.65),vec3(.36,.77,.72),nearShore);float ripple=sin(z*1.42+x*.66-uTime*1.55)+sin(x*1.7-z*.39+uTime);col+=ripple*.022;float line=abs(sin(z*.25+x*1.25+uTime*.65));float glint=pow(max(0.,sin(z*3.6+x*.59+uTime*.8)*sin(x*2.5-z*.36-uTime*.55)),16.);col+=glint*.17;float foam=(1.-smoothstep(.035,.2,abs(x+7.9+sin(z*.22+uTime)*.21)))*(.55+.3*sin(z*.65+uTime));col=mix(col,vec3(.88,.96,.84),foam);float mist=smoothstep(90.,200.,length(vWorld.xz-cameraPosition.xz));col=mix(col,vec3(.79,.9,.86),mist*.8);gl_FragColor=vec4(col,1.);}`
113}));
114ocean.rotation.x = -Math.PI / 2; ocean.position.set(-172, -.07, 0); scene.add(ocean);
115const land = box(scene, mats.grass, [38, -.17, 0], [70, .3, 680]); land.castShadow = false;
116
117// A reusable stretch of coastline. Static pieces are merged by material.
118function palm(parent, x, z, height = 4.4, spin = 0) {
119 const g = new THREE.Group(); g.position.set(x, 0, z); g.rotation.y = spin; parent.add(g);
120 const lean = .37;
121 cylinderBetween(g, mats.trunk, [0, .2, 0], [lean * .35, height * .58, .08], .14, .105);
122 cylinderBetween(g, mats.trunk, [lean * .35, height * .58, .08], [lean, height, .17], .106, .075);
123 for (let j = 0; j < 8; j++) {
124 const theta = j * Math.PI / 4 + .14;
125 const positions = [], normals = [], uvs = [];
126 const count = 10, span = 1.8 + (j % 3) * .15;
127 for (let i = 0; i < count; i++) {
128 const a = i / count, b = (i + 1) / count;
129 const p = t => { const width = Math.sin(t * Math.PI) * .22; const l = t * span; const y = height + .12 + Math.sin(t * Math.PI) * .44 - t * .64; return [V(lean + Math.sin(theta) * l + Math.cos(theta) * width, y, .17 + Math.cos(theta) * l - Math.sin(theta) * width), V(lean + Math.sin(theta) * l - Math.cos(theta) * width, y, .17 + Math.cos(theta) * l + Math.sin(theta) * width)]; };
130 const [al, ar] = p(a), [bl, br] = p(b);
131 for (const v of [al, ar, bl, ar, br, bl]) { positions.push(v.x, v.y, v.z); normals.push(0, 1, 0); uvs.push(v.x, v.z); }
132 }
133 const geo = new THREE.BufferGeometry(); geo.setAttribute('position', new THREE.Float32BufferAttribute(positions, 3)); geo.setAttribute('normal', new THREE.Float32BufferAttribute(normals, 3)); geo.setAttribute('uv', new THREE.Float32BufferAttribute(uvs, 2));
134 mesh(g, geo, j % 2 ? mats.leaf : mats.leafLight);
135 }
136 for (let j = 0; j < 3; j++) ball(g, mats.trunk, [lean + Math.sin(j * 2) * .12, height -.13, .17 + Math.cos(j * 2) * .12], [.14, .18, .14]);
137}
138mats.leaf.side = THREE.DoubleSide; mats.leafLight.side = THREE.DoubleSide;
139function umbrella(parent, x, z, color) {
140 cylinderBetween(parent, mats.cream, [x, .04, z], [x, 1.65, z], .025);
141 const canopy = mesh(parent, new THREE.ConeGeometry(.85, .34, 12, 1, true), color, x, 1.72, z);
142 canopy.material.side = THREE.DoubleSide;
143 mesh(parent, new THREE.ConeGeometry(.035, .12, 8), mats.cream, x, 1.93, z);
144 box(parent, mats.cream, [x + .15, .08, z + 1.1], [.65, .018, 1.18]);
145 for (let i = 0; i < 3; i++) box(parent, color, [x + .15, .095, z + .75 + i * .31], [.65, .02, .10]);
146}
147function house(parent, z, color, flip) {
148 const g = new THREE.Group(); parent.add(g); g.position.set(10.8, 0, z); g.rotation.y = -Math.PI / 2;
149 box(g, color, [0, 1.15, 0], [3.8, 2.3, 3]);
150 const roofShape = new THREE.Shape(); roofShape.moveTo(-2.10, 0); roofShape.lineTo(0, .94); roofShape.lineTo(2.10, 0); roofShape.closePath();
151 mesh(g, new THREE.ExtrudeGeometry(roofShape, { depth: 3.48, bevelEnabled: false }), flip ? mats.mint : mats.roof, 0, 2.28, -1.74);
152 box(g, mats.cream, [0, 1.45, 1.53], [3.85, .14, .10]);
153 box(g, mats.glass, [-.85, 1.18, 1.53], [.84, .83, .06]);
154 box(g, mats.cream, [-.85, 1.18, 1.58], [.055, .86, .04]); box(g, mats.cream, [-.85, 1.18, 1.58], [.89, .055, .04]);
155 box(g, mats.shirt, [.83, .75, 1.53], [.68, 1.48, .07]); ball(g, mats.yellow, [.61, .78, 1.59], [.035, .035, .035]);
156 const awning = box(g, flip ? mats.yellow : mats.blue, [0, 1.89, 1.95], [3.3, .08, 1.0]); awning.rotation.x = .13;
157 for (let j = -2; j <= 2; j++) { const stripe = box(g, mats.cream, [j * .58, 1.90, 1.95], [.26, .085, 1.02]); stripe.rotation.x = .13; }
158 cylinderBetween(g, mats.trunk, [-1.45, .05, 2.3], [-1.45, 1.8, 2.3], .04);
159 cylinderBetween(g, mats.trunk, [1.45, .05, 2.3], [1.45, 1.8, 2.3], .04);
160 box(g, mats.concrete, [0, .12, 2], [4.15, .24, 1.55]);
161 for (const x of [-2.15, 2.15]) { mesh(g, new THREE.CylinderGeometry(.23, .17, .35, 10), mats.roof, x, .3, 1.8); ball(g, mats.leafLight, [x, .69, 1.8], [.42, .38, .42]); }
162}
163function sign(parent, x, z) {
164 cylinderBetween(parent, mats.trunk, [x, .2, z], [x, 2.15, z], .045);
165 box(parent, mats.shirt, [x, 1.92, z], [1.15, .42, .08]);
166 const cv = document.createElement('canvas'); cv.width = 384; cv.height = 128;
167 const ctx = cv.getContext('2d'); ctx.fillStyle = '#fff1cf'; ctx.font = '600 48px "Microsoft YaHei",sans-serif'; ctx.textAlign = 'center'; ctx.textBaseline = 'middle'; ctx.fillText('海风湾 →', 192, 64);
168 const texture = new THREE.CanvasTexture(cv); texture.colorSpace = THREE.SRGBColorSpace;
169 const mat = new THREE.MeshBasicMaterial({ map: texture, transparent: true, side: THREE.DoubleSide });
170 mesh(parent, new THREE.PlaneGeometry(1.02, .34), mat, x, 1.92, z + .047);
171}
172function streetlamp(parent, x, z) {
173 cylinderBetween(parent, mats.shirt, [x, .22, z], [x, 3.6, z], .045);
174 curveTube(parent, mats.shirt, [[x, 3.4, z], [x -.16, 3.85, z], [x -.70, 3.86, z], [x -.87, 3.57, z]], .042, 12);
175 mesh(parent, new THREE.ConeGeometry(.22, .17, 12), mats.shirt, x -.87, 3.52, z);
176 ball(parent, mats.lamp, [x -.87, 3.46, z], [.14, .045, .14]);
177}
178const tileSize = 64, tileCount = 7, tiles = [];
179function coastTile() {
180 const tile = new THREE.Group();
181 box(tile, mats.road, [1, .08, 32], [7.8, .2, 64]);
182 box(tile, mats.concrete, [-3.65, .15, 32], [1.5, .30, 64]);
183 box(tile, mats.concrete, [5.65, .15, 32], [1.50, .30, 64]);
184 box(tile, mats.sand, [-5.92, -.025, 32], [3.04, .13, 64]);
185 for (const x of [-2.66, 4.67]) box(tile, mats.paint, [x, .186, 32], [.065, .008, 64]);
186 for (let z = 1; z < 64; z += 6.4) box(tile, mats.paint, [1, .187, z + 1.6], [.105, .012, 2.9]);
187 for (let z = 0; z < 64; z += 4) {
188 box(tile, mats.cream, [-4.32, .56, z], [.10, .82, .10]);
189 ball(tile, mats.cream, [-4.32, .99, z], [.07, .045, .07]);
190 }
191 box(tile, mats.cream, [-4.32, .82, 32], [.06, .06, 64]); box(tile, mats.cream, [-4.32, .50, 32], [.055, .055, 64]);
192 palm(tile, 7.3, 8, 4.9, .8); palm(tile, 7.5, 43, 4.3, 2.3); palm(tile, -5.3, 40, 3.8, 1.7);
193 house(tile, 19, mats.house, false); house(tile, 55, mats.housePink, true);
194 umbrella(tile, -6.3, 12, mats['scarf']);
195 umbrella(tile, -6.25, 52, mats.yellow);
196 streetlamp(tile, 5.94, 4); streetlamp(tile, 5.94, 36);
197 sign(tile, 6.8, 30);
198 // A seaside bench and small, varied roadside planting.
199 for (let i = 0; i < 3; i++) box(tile, mats.trunk, [-3.55 + i * .14, .63, 25], [.115, .065, 1.5]);
200 for (let i = 0; i < 3; i++) box(tile, mats.trunk, [-3.88, .80 + i * .13, 25], [.075, .095, 1.5]);
201 for (const z of [24.5, 25.5]) { box(tile, mats.shirt, [-3.7, .46, z], [.065, .37, .06]); box(tile, mats.shirt, [-3.39, .46, z], [.065, .37, .06]); }
202 for (let j = 0; j < 17; j++) {
203 const z = (j * 13.73 + 3) % 64, x = 7 + ((j * 2.31) % 7);
204 ball(tile, j % 3 ? mats.grassLight : mats.leafLight, [x, .32, z], [.6 + (j % 3) * .16, .4, .55]);
205 }
206 for (let j = 0; j < 12; j++) {
207 const z = (j * 17.1) % 64, x = -7.3 + (j % 3) * .14;
208 const stone = mesh(tile, new THREE.IcosahedronGeometry(1, 0), mats.rock, x, .03, z); stone.scale.set(.13 + (j % 2) * .1, .12, .23); stone.rotation.y = j;
209 }
210 return tile;
211}
212const prototypeTile = coastTile();
213batchStatic(prototypeTile);
214for (let i = 0; i < tileCount; i++) { const tile = prototypeTile.clone(); scene.add(tile); tiles.push(tile); }
215
216// Far out to sea: a lighthouse, quiet sails, and low islands.
217const offshore = new THREE.Group(); scene.add(offshore);
218function sailboat(x, z, size) {
219 const g = new THREE.Group(); g.position.set(x, -.03, z); g.scale.setScalar(size); offshore.add(g);
220 ball(g, mats.cream, [0, .13, 0], [.46, .22, 1.08]);
221 cylinderBetween(g, mats.trunk, [0, .18, 0], [0, 2.15, 0], .025);
222 const shape = new THREE.Shape(); shape.moveTo(0, 0); shape.lineTo(0, 1.72); shape.lineTo(1.05, .08); shape.closePath();
223 const sail = mesh(g, new THREE.ShapeGeometry(shape), mats.cream, .04, .34, 0); sail.rotation.y = Math.PI / 2;
224 const shape2 = new THREE.Shape(); shape2.moveTo(0, .05); shape2.lineTo(0, 1.38); shape2.lineTo(-.7, .05); shape2.closePath();
225 const sail2 = mesh(g, new THREE.ShapeGeometry(shape2), mats.yellow, -.035, .36, 0); sail2.rotation.y = Math.PI / 2;
226 g.rotation.y = .8;
227}
228mats.cream.side = THREE.DoubleSide; mats.yellow.side = THREE.DoubleSide;
229sailboat(-29, 41, .95); sailboat(-72, -10, 1.5); sailboat(-56, 117, 1.3);
230for (let i = 0; i < 5; i++) {
231 const island = mesh(offshore, new THREE.IcosahedronGeometry(1, 1), mats.leafLight, -87 - (i % 2) * 40, 1, -135 + i * 84);
232 island.scale.set(14 + i * 2, 3 + (i % 3) * 1.6, 9 + i * 1.3); island.rotation.y = i;
233}
234const lighthouse = new THREE.Group(); lighthouse.position.set(-42, 0, 87); offshore.add(lighthouse);
235const islandRock = mesh(lighthouse, new THREE.IcosahedronGeometry(1, 1), mats.rock, 0, .18, 0); islandRock.scale.set(4.8, .86, 3.5);
236mesh(lighthouse, new THREE.CylinderGeometry(.49, .78, 4.6, 20), mats.cream, 0, 2.82, 0);
237mesh(lighthouse, new THREE.CylinderGeometry(.59, .62, .53, 20), mats['scarf'], 0, 3.12, 0);
238mesh(lighthouse, new THREE.CylinderGeometry(.65, .65, .10, 20), mats.shirt, 0, 5.17, 0);
239mesh(lighthouse, new THREE.CylinderGeometry(.46, .46, .69, 12), mats.glass, 0, 5.56, 0);
240mesh(lighthouse, new THREE.ConeGeometry(.75, .41, 20), mats['scarf'], 0, 6.09, 0);
241ball(lighthouse, mats.lamp, [0, 5.54, 0], [.3, .25, .3]);
242batchStatic(offshore);
243
244// The electric scooter uses an enclosed motor hub and a footboard.
245const ride = new THREE.Group(); scene.add(ride); ride.position.set(-.9, .18, 0);
246const scooter = new THREE.Group(); ride.add(scooter);
247const wheelRadius = .44, wheels = [];
248function makeWheel(parent, z) {
249 const g = new THREE.Group(); g.position.set(0, .44, z); parent.add(g);
250 const tire = mesh(g, new THREE.TorusGeometry(.35, .09, 12, 32), mats.rubber); tire.rotation.y = Math.PI / 2;
251 const rim = mesh(g, new THREE.CylinderGeometry(.272, .272, .13, 24), mats.metal); rim.rotation.z = Math.PI / 2;
252 for (const side of [-1, 1]) {
253 const hub = mesh(g, new THREE.CylinderGeometry(.185, .185, .017, 24), mats.mintLight, side * .082, 0, 0); hub.rotation.z = Math.PI / 2;
254 for (let j = 0; j < 5; j++) {
255 const spoke = box(g, mats.cream, [side * .078, Math.cos(j * Math.PI * 2 / 5) * .207, Math.sin(j * Math.PI * 2 / 5) * .207], [.015, .09, .029]); spoke.rotation.x = j * Math.PI * 2 / 5;
256 }
257 const axle = mesh(g, new THREE.CylinderGeometry(.045, .045, .024, 12), mats.seat, side * .096, 0, 0); axle.rotation.z = Math.PI / 2;
258 }
259 // A small valve also makes the live rotation easy to see.
260 box(g, mats.metal, [-.055, .30, 0], [.02, .042, .02]); wheels.push(g); return g;
261}
262const rearWheel = makeWheel(scooter, -.89);
263const steering = new THREE.Group(); steering.position.z = .98; scooter.add(steering);
264const frontWheel = makeWheel(steering, 0);
265for (const z of [-.89, .98]) {
266 const fender = mesh(scooter, new THREE.TorusGeometry(.49, .075, 8, 24, Math.PI), mats.mintLight, 0, .44, z); fender.rotation.y = Math.PI / 2;
267}
268ball(scooter, mats.mint, [0, .85, -.65], [.405, .34, .59]);
269roundedBox(scooter, mats.mint, .60, .12, .94, .06, [0, .53, -.51]);
270for (const x of [-.245, .245]) box(scooter, mats.rubber, [x, .629, -.02], [.12, .018, .69]);
271ball(scooter, mats.seat, [0, 1.19, -.43], [.36, .12, .56]);
272ball(scooter, mats.cream, [0, 1.14, -.44], [.345, .052, .545]);
273curveTube(scooter, mats.metal, [[-.3, 1.16, -.96], [-.31, 1.23, -1.13], [0, 1.27, -1.16], [.31, 1.23, -1.13], [.3, 1.16, -.96]], .022);
274const shieldShape = new THREE.Shape();
275shieldShape.moveTo(-.33, .52); shieldShape.quadraticCurveTo(-.45, .76, -.28, 1.31); shieldShape.quadraticCurveTo(-.26, 1.49, 0, 1.49); shieldShape.quadraticCurveTo(.26, 1.49, .28, 1.31); shieldShape.quadraticCurveTo(.45, .76, .33, .52); shieldShape.closePath();
276const shield = mesh(scooter, new THREE.ExtrudeGeometry(shieldShape, { depth: .13, bevelEnabled: true, bevelSize: .045, bevelThickness: .045, bevelSegments: 3, curveSegments: 16 }), mats.mint, 0, 0, .58); shield.rotation.x = -.16;
277const inner = ball(scooter, mats.cream, [0, .99, .478], [.27, .38, .06]); inner.rotation.x = -.15;
278curveTube(scooter, mats.mintLight, [[0, .77, .67], [0, 1.08, .58], [0, 1.40, .43]], .018);
279for (const x of [-.13, .13]) cylinderBetween(scooter, mats.metal, [x, .43, .98], [x, 1.30, .62], .038);
280cylinderBetween(scooter, mats.mint, [0, 1.08, .61], [0, 1.59, .60], .075, .085, 16);
281const handlebars = new THREE.Group(); handlebars.position.set(0, 1.60, .59); scooter.add(handlebars);
282curveTube(handlebars, mats.metal, [[-.49, 0, -.045], [-.26, .04, .045], [0, .035, .055], [.26, .04, .045], [.49, 0, -.045]], .025);
283for (const x of [-.46, .46]) { const grip = mesh(handlebars, new THREE.CylinderGeometry(.038, .038, .17, 12), mats.seat, x, 0, -.045); grip.rotation.z = Math.PI / 2; }
284ball(handlebars, mats.mint, [0, .04, .088], [.19, .15, .17]);
285const headlight = mesh(handlebars, new THREE.CylinderGeometry(.112, .112, .042, 24), mats.lamp, 0, .036, .244); headlight.rotation.x = Math.PI / 2;
286const lightRing = mesh(handlebars, new THREE.TorusGeometry(.12, .015, 8, 24), mats.metal, 0, .036, .268);
287for (const x of [-.29, .29]) ball(handlebars, mats.yellow, [x, -.028, .105], [.042, .035, .055]);
288for (const side of [-1, 1]) {
289 cylinderBetween(scooter, mats.metal, [side * .39, 1.6, .55], [side * .56, 1.91, .58], .014);
290 ball(scooter, mats.mint, [side * .57, 1.95, .58], [.125, .088, .04]);
291 ball(scooter, mats.metal, [side * .57, 1.95, .539], [.103, .065, .007]);
292 ball(scooter, mats.cream, [side * .383, .84, -.68], [.035, .15, .35]);
293 const bolt = new THREE.Shape(); bolt.moveTo(.02, .09); bolt.lineTo(-.075, -.008); bolt.lineTo(-.015, -.008); bolt.lineTo(-.042, -.10); bolt.lineTo(.075, .028); bolt.lineTo(.02, .028); bolt.closePath();
294 const emblem = mesh(scooter, new THREE.ShapeGeometry(bolt), mats.shirt, side * .423, .85, -.66); emblem.rotation.y = side * Math.PI / 2;
295}
296ball(scooter, mats['scarf'], [0, .86, -1.22], [.12, .048, .022]);
297roundedBox(scooter, mats.cream, .22, .105, .013, .015, [0, .65, -1.19]);
298cylinderBetween(scooter, mats.metal, [-.2, .46, -.87], [-.2, .89, -.6], .024);
299
300// Orange tabby: ears, muzzle, whiskers, blinking eyes, and a windblown scarf.
301const cat = new THREE.Group(); ride.add(cat);
302ball(cat, mats.shirt, [0, 1.51, -.37], [.30, .36, .26]);
303ball(cat, mats.cream, [0, 1.46, -.126], [.185, .24, .028]);
304ball(cat, mats.fur, [0, 1.27, -.48], [.275, .18, .28]);
305for (const side of [-1, 1]) {
306 const hip = [side * .17, 1.27, -.48], knee = [side * .31, .97, -.15], ankle = [side * .27, .72, .17];
307 cylinderBetween(cat, mats.fur, hip, knee, .112, .13, 14); ball(cat, mats.furLight, knee, [.117, .12, .13]);
308 cylinderBetween(cat, mats.furLight, knee, ankle, .081, .099, 14);
309 ball(cat, mats.cream, [side * .27, .695, .24], [.10, .065, .17]);
310 for (let j = 0; j < 2; j++) ball(cat, mats.tabby, [side * .363, 1.01 + j * .065, -.175 - j * .06], [.008, .028, .078]);
311 const shoulder = [side * .245, 1.70, -.33], elbow = [side * .37, 1.49, .07], hand = [side * .46, 1.61, .546];
312 cylinderBetween(cat, mats.shirt, shoulder, [side * .30, 1.62, -.14], .097, .105, 14);
313 cylinderBetween(cat, mats.fur, [side * .30, 1.62, -.14], elbow, .068, .085, 14);
314 ball(cat, mats.furLight, elbow, [.079, .078, .08]); cylinderBetween(cat, mats.furLight, elbow, hand, .061, .075, 14);
315 ball(cat, mats.cream, hand, [.094, .064, .091]);
316}
317const tail = new THREE.Group(); tail.position.set(0, 1.25, -.75); cat.add(tail);
318const tailPath = [[0, .02, 0], [-.17, .01, -.20], [-.43, .16, -.33], [-.48, .42, -.31], [-.32, .59, -.31], [-.16, .57, -.33]];
319curveTube(tail, mats.furLight, tailPath, .064, 30); ball(tail, mats.cream, [-.16, .57, -.33], [.073, .068, .073]);
320for (const t of [.36, .58, .76]) {
321 const path = new THREE.CatmullRomCurve3(tailPath.map(p => V(...p)));
322 const a = path.getPoint(t), b = path.getPoint(t + .036);
323 cylinderBetween(tail, mats.tabby, a.toArray(), b.toArray(), .066, .066, 10);
324}
325const head = new THREE.Group(); head.position.set(0, 2.13, -.29); cat.add(head);
326ball(head, mats.furLight, [0, 0, 0], [.445, .43, .385]);
327for (const side of [-1, 1]) {
328 const ear = new THREE.Shape(); ear.moveTo(-.145, 0); ear.quadraticCurveTo(-.13, .18, -.08, .40); ear.quadraticCurveTo(-.02, .48, .015, .41); ear.lineTo(.15, 0); ear.closePath();
329 const e = mesh(head, new THREE.ExtrudeGeometry(ear, { depth: .095, bevelEnabled: true, bevelSegments: 2, bevelSize: .015, bevelThickness: .015 }), mats.fur, side * .29, .27, -.038); e.rotation.z = -side * .20;
330 const innerEar = new THREE.Shape(); innerEar.moveTo(-.09, .04); innerEar.lineTo(-.053, .31); innerEar.lineTo(.085, .04); innerEar.closePath();
331 const ie = mesh(head, new THREE.ShapeGeometry(innerEar), mats.pink, side * .29, .27, .076); ie.rotation.z = -side * .20;
332 ball(head, mats.cream, [side * .098, -.118, .35], [.12, .105, .099]);
333 ball(head, mats.pink, [side * .269, -.088, .294], [.066, .031, .014]);
334 for (let j = 0; j < 3; j++) {
335 curveTube(head, mats.seat, [[side * .26, -.10 - j * .039, .336], [side * .42, -.07 - j * .045, .36], [side * .57, -.04 - j * .063, .35]], .006, 8);
336 }
337 for (let j = 0; j < 2; j++) ball(head, mats.tabby, [side * (.33 + j * .035), .012 - j * .10, .20], [.019, .040, .07]);
338}
339const eyes = [];
340for (const side of [-1, 1]) {
341 const eye = new THREE.Group(); eye.position.set(side * .163, .055, .349); head.add(eye); eyes.push(eye);
342 ball(eye, mats.cream, [0, 0, 0], [.071, .087, .030]); ball(eye, mats.ink, [side * .008, -.003, .024], [.041, .060, .018]);
343 const shine = mesh(eye, sphereGeo, new THREE.MeshBasicMaterial({ color: '#fffdf0' }), -.012, .022, .040); shine.scale.set(.014, .016, .006);
344 curveTube(head, mats.tabby, [[side * .22, .165, .31], [side * .164, .186, .337], [side * .114, .17, .337]], .012, 8);
345}
346const nose = new THREE.Shape(); nose.moveTo(-.057, .016); nose.quadraticCurveTo(0, .033, .057, .016); nose.lineTo(0, -.041); nose.closePath();
347mesh(head, new THREE.ShapeGeometry(nose), mats.pink, 0, -.09, .454);
348curveTube(head, mats.seat, [[0, -.126, .449], [0, -.16, .434], [-.057, -.18, .419]], .007, 10);
349curveTube(head, mats.seat, [[0, -.16, .435], [.033, -.18, .429], [.06, -.17, .419]], .007, 10);
350for (const x of [-.115, 0, .115]) {
351 const stripe = ball(head, mats.tabby, [x, .265, .285], [.027, .073, .012]); stripe.rotation.z = -x * 2;
352}
353const helmet = mesh(head, new THREE.SphereGeometry(.472, 32, 20, 0, Math.PI * 2, 0, 1.65), mats.mint, 0, .185, -.025); helmet.scale.z = .96;
354const helmetRim = mesh(head, new THREE.TorusGeometry(.470, .023, 8, 40), mats.cream, 0, .148, -.025); helmetRim.rotation.x = Math.PI / 2; helmetRim.scale.y = .96;
355curveTube(head, mats.cream, Array.from({ length: 17 }, (_, i) => { const a = i / 16 * Math.PI; return [0, .185 + Math.sin(a) * .478, -.025 + Math.cos(a) * .459]; }), .022, 25);
356for (const side of [-1, 1]) curveTube(head, mats.seat, [[side * .386, .05, 0], [side * .31, -.27, .03], [side * .10, -.39, .14]], .013, 12);
357ball(cat, mats['scarf'], [0, 1.79, -.27], [.315, .075, .262]);
358ball(cat, mats['scarf'], [-.25, 1.78, -.45], [.095, .086, .084]);
359const scarfSegments = 16, scarfGeo = new THREE.BufferGeometry();
360const scarfPositions = new Float32Array((scarfSegments + 1) * 2 * 3), scarfUV = [], scarfIndices = [];
361for (let i = 0; i <= scarfSegments; i++) { scarfUV.push(0, i / scarfSegments, 1, i / scarfSegments); if (i < scarfSegments) { const n = i * 2; scarfIndices.push(n, n + 1, n + 2, n + 1, n + 3, n + 2); } }
362scarfGeo.setAttribute('position', new THREE.BufferAttribute(scarfPositions, 3)); scarfGeo.setAttribute('uv', new THREE.Float32BufferAttribute(scarfUV, 2)); scarfGeo.setIndex(scarfIndices);
363mats['scarf'].side = THREE.DoubleSide;
364const scarf = mesh(cat, scarfGeo, mats['scarf']); scarf.frustumCulled = false;
365function animateCat(t, speed) {
366 head.rotation.z = Math.sin(t * 1.7) * .023; head.rotation.y = Math.sin(t * .62) * .035;
367 tail.rotation.z = Math.sin(t * 1.7) * .08; tail.rotation.y = Math.sin(t * 1.15) * .10;
368 const blinkPhase = t % 4.7, blink = blinkPhase > 4.48 ? Math.max(.06, Math.abs((blinkPhase - 4.59) / .11)) : 1;
369 eyes.forEach(e => e.scale.y = Math.min(1, blink));
370 for (let i = 0; i <= scarfSegments; i++) {
371 const q = i / scarfSegments, centerX = -.27 - q * .11 + Math.sin(t * 4.8 - q * 7) * .055 * q;
372 const y = 1.78 + Math.sin(t * 5.0 - q * 9) * .065 * q + q * .07;
373 const z = -.46 - q * (.51 + speed / 60), width = .105 * (1 - q * .45);
374 for (let side = 0; side < 2; side++) { const at = (i * 2 + side) * 3; scarfPositions[at] = centerX + (side ? width : -width); scarfPositions[at + 1] = y + (side ? .024 : -.024); scarfPositions[at + 2] = z; }
375 }
376 scarfGeo.attributes.position.needsUpdate = true; scarfGeo.computeVertexNormals();
377}
378
379// A soft contact shadow complements the real sunlight shadows.
380const shadowCanvas = document.createElement('canvas'); shadowCanvas.width = shadowCanvas.height = 128;
381const sc = shadowCanvas.getContext('2d'), gradient = sc.createRadialGradient(64, 64, 6, 64, 64, 62);
382gradient.addColorStop(0, '#253e4280'); gradient.addColorStop(1, '#253e4200'); sc.fillStyle = gradient; sc.fillRect(0, 0, 128, 128);
383const shadow = mesh(ride, new THREE.PlaneGeometry(1.9, 3.25), new THREE.MeshBasicMaterial({ map: new THREE.CanvasTexture(shadowCanvas), transparent: true, depthWrite: false }), 0, .011, 0);
384shadow.rotation.x = -Math.PI / 2; shadow.castShadow = false;
385
386const birds = [];
387function wingGeometry(side) {
388 const g = new THREE.BufferGeometry(); g.setAttribute('position', new THREE.Float32BufferAttribute([0, 0, 0, side * .50, .02, -.19, side * .83, -.05, -.33, side * .47, -.025, .02], 3)); g.setIndex([0, 1, 3, 1, 2, 3]); g.computeVertexNormals(); return g;
389}
390for (let i = 0; i < 6; i++) {
391 const g = new THREE.Group(); scene.add(g);
392 const left = mesh(g, wingGeometry(-1), mats.cream), right = mesh(g, wingGeometry(1), mats.cream);
393 ball(g, mats.cream, [0, -.016, .045], [.065, .09, .20]); ball(g, mats.yellow, [0, .018, .258], [.026, .022, .08]);
394 g.scale.setScalar(.50 + (i % 3) * .17); birds.push({ group: g, left, right, i });
395}
396
397const reducedMotion = matchMedia('(prefers-reduced-motion: reduce)').matches;
398const state = { paused: reducedMotion, speed: 24, path: 0, distance: 0, time: 0, frames: 0, view: 'follow', ready: false, sound: false };
399const presets = { follow: { yaw: .79, elevation: .30, radius: 8.3 }, side: { yaw: 1.54, elevation: .32, radius: 8.2 }, wide: { yaw: .86, elevation: .55, radius: 19.5 } };
400let desired = { ...presets.follow }, current = { ...desired };
401function setView(view) {
402 state.view = view; desired = { ...presets[view] };
403 document.querySelectorAll('button[data-view]').forEach(b => b.setAttribute('aria-pressed', String(b.dataset.view === view)));
404}
405function syncPlayButton() {
406 $('play').setAttribute('aria-pressed', String(state.paused));
407 $('play').setAttribute('aria-label', state.paused ? '继续骑行' : '暂停骑行');
408 $('play-label').textContent = state.paused ? '继续骑行' : '暂停骑行';
409 $('play-icon').innerHTML = state.paused ? '<path d="m4 2 10 6-10 6z"/>' : '<path d="M3 2h3v12H3zm7 0h3v12h-3z"/>';
410}
411function togglePlay() { state.paused = !state.paused; syncPlayButton(); }
412$('play').addEventListener('click', togglePlay); syncPlayButton();
413document.querySelectorAll('button[data-view]').forEach(b => b.addEventListener('click', () => setView(b.dataset.view)));
414$('speed').addEventListener('input', e => { state.speed = Number(e.target.value); $('speed-value').innerHTML = `${state.speed} <span>km/h</span>`; });
415addEventListener('keydown', e => {
416 if (e.target.matches('input,button,select,textarea')) return;
417 if (e.code === 'Space') { e.preventDefault(); togglePlay(); }
418 if (e.code === 'KeyR') { setView('follow'); toast('镜头已回到小猫身边'); }
419 if (['Digit1', 'Digit2', 'Digit3'].includes(e.code)) setView(['follow', 'side', 'wide'][Number(e.key) - 1]);
420});
421let toastTimer;
422function toast(text) { $('toast').textContent = text; $('toast').classList.add('show'); clearTimeout(toastTimer); toastTimer = setTimeout(() => $('toast').classList.remove('show'), 2300); }
423
424// Pointer controls remain active while the riding animation is paused.
425const pointers = new Map(); let lastPinch = 0;
426const canvas = renderer.domElement;
427canvas.addEventListener('pointerdown', e => { canvas.setPointerCapture(e.pointerId); pointers.set(e.pointerId, { x: e.clientX, y: e.clientY }); lastPinch = 0; });
428canvas.addEventListener('pointermove', e => {
429 const previous = pointers.get(e.pointerId); if (!previous) return;
430 pointers.set(e.pointerId, { x: e.clientX, y: e.clientY });
431 if (pointers.size === 1) { desired.yaw -= (e.clientX - previous.x) * .006; desired.elevation = clamp(desired.elevation + (e.clientY - previous.y) * .004, .14, 1.18); }
432 else {
433 const [a, b] = [...pointers.values()]; const d = Math.hypot(a.x - b.x, a.y - b.y);
434 if (lastPinch > 0) desired.radius = clamp(desired.radius * lastPinch / d, 6.0, 31);
435 lastPinch = d;
436 }
437});
438function endPointer(e) { pointers.delete(e.pointerId); lastPinch = 0; }
439canvas.addEventListener('pointerup', endPointer); canvas.addEventListener('pointercancel', endPointer); canvas.addEventListener('lostpointercapture', endPointer);
440canvas.addEventListener('wheel', e => { e.preventDefault(); desired.radius = clamp(desired.radius * Math.exp(e.deltaY * .001), 6.0, 31); }, { passive: false });
441addEventListener('resize', () => { camera.aspect = innerWidth / innerHeight; camera.updateProjectionMatrix(); renderer.setPixelRatio(Math.min(devicePixelRatio, innerWidth < 650 ? 1.5 : 1.75)); renderer.setSize(innerWidth, innerHeight); });
442
443let audio;
444function createAudio() {
445 const context = new (window.AudioContext || window.webkitAudioContext)();
446 const buffer = context.createBuffer(1, context.sampleRate * 3, context.sampleRate), data = buffer.getChannelData(0);
447 let last = 0; for (let i = 0; i < data.length; i++) { last = (last + .02 * (Math.random() * 2 - 1)) / 1.02; data[i] = last * 3.5; }
448 const noise = context.createBufferSource(); noise.buffer = buffer; noise.loop = true;
449 const filter = context.createBiquadFilter(); filter.type = 'lowpass'; filter.frequency.value = 800;
450 const waveGain = context.createGain(); waveGain.gain.value = .14; noise.connect(filter).connect(waveGain).connect(context.destination); noise.start();
451 const motor = context.createOscillator(); motor.type = 'sine'; motor.frequency.value = 85;
452 const motorGain = context.createGain(); motorGain.gain.value = .004; motor.connect(motorGain).connect(context.destination); motor.start();
453 return { context, waveGain, motor, motorGain };
454}
455$('sound').addEventListener('click', async () => {
456 try {
457 if (!audio) audio = createAudio(); state.sound = !state.sound;
458 if (state.sound) await audio.context.resume(); else await audio.context.suspend();
459 $('sound').setAttribute('aria-pressed', String(state.sound)); $('sound').setAttribute('aria-label', state.sound ? '关闭海浪声音' : '开启海浪声音');
460 $('sound-waves').setAttribute('d', state.sound ? 'M16 8c3 2 3 6 0 8m3-11c5 4 5 10 0 14' : 'm16 9 5 6m0-6-5 6');
461 toast(state.sound ? '听,海风和浪花' : '海浪声音已关闭');
462 } catch { state.sound = false; toast('这个浏览器暂时无法播放声音'); }
463});
464let lastFrame = performance.now(), hidden = document.hidden;
465document.addEventListener('visibilitychange', () => { hidden = document.hidden; lastFrame = performance.now(); if (audio && state.sound) { if (hidden) audio.context.suspend(); else audio.context.resume(); } });
466canvas.addEventListener('webglcontextlost', e => { e.preventDefault(); document.documentElement.dataset.contextLost = 'true'; toast('正在恢复 3D 画面…'); });
467canvas.addEventListener('webglcontextrestored', () => { delete document.documentElement.dataset.contextLost; toast('画面已恢复'); });
468const offset = V(), aim = V(), previousPosition = V();
469function updateCamera(dt) {
470 const factor = 1 - Math.exp(-dt * 6);
471 for (const key of ['yaw', 'elevation', 'radius']) current[key] += (desired[key] - current[key]) * factor;
472 const radius = current.radius * (innerWidth < 650 && state.view === 'wide' ? 1.12 : 1);
473 offset.set(Math.sin(current.yaw) * Math.cos(current.elevation) * radius, Math.sin(current.elevation) * radius, Math.cos(current.yaw) * Math.cos(current.elevation) * radius);
474 const bias = innerWidth < 650 ? 0 : state.view === 'wide' ? .35 : .80;
475 aim.copy(ride.position).add(V(-Math.cos(current.yaw) * bias, innerWidth < 650 ? 1.40 : 1.25, Math.sin(current.yaw) * bias));
476 camera.position.copy(aim).add(offset); camera.lookAt(aim);
477}
478function render(now) {
479 requestAnimationFrame(render);
480 const dt = Math.min(.05, Math.max(0, (now - lastFrame) / 1000)); lastFrame = now;
481 if (hidden) return;
482 if (!state.paused) {
483 state.time += dt;
484 previousPosition.copy(ride.position);
485 const derivative = .012 * Math.cos(state.path * .12), v = state.speed / 3.6;
486 state.path += dt * v / Math.sqrt(1 + derivative * derivative);
487 ride.position.x = -.9 + .10 * Math.sin(state.path * .12); ride.position.z = state.path;
488 state.distance += previousPosition.distanceTo(ride.position);
489 }
490 const derivative = .012 * Math.cos(state.path * .12), curvature = -.00144 * Math.sin(state.path * .12) / Math.pow(1 + derivative * derivative, 1.5);
491 ride.rotation.y = Math.atan(derivative); steering.rotation.y = Math.atan(1.87 * curvature); handlebars.rotation.y = steering.rotation.y;
492 rearWheel.rotation.x = state.distance / wheelRadius; frontWheel.rotation.x = state.distance / wheelRadius;
493 animateCat(state.time, state.speed);
494 const base = Math.floor(state.path / tileSize);
495 tiles.forEach((tile, i) => tile.position.z = (base + i - 3) * tileSize);
496 ocean.position.z = state.path; land.position.z = state.path; sky.position.copy(ride.position);
497 sun.position.z = state.path + 168; seaUniforms.uTime.value = state.time; seaUniforms.uTravel.value = -state.path;
498 offshore.position.z = Math.floor((state.path + 100) / 420) * 420;
499 sunlight.position.copy(ride.position).add(V(-26, 47, 22)); sunlight.target.position.copy(ride.position);
500 birds.forEach(({ group, left, right, i }) => {
501 const a = state.time * .11 + i * 1.04;
502 group.position.set(-13 - i * 2.6 + Math.cos(a) * 5, 6.3 + Math.sin(a * 1.8) * .85 + i * .44, state.path + 4 + Math.sin(a) * 18 + i * 3);
503 group.rotation.y = .35 + Math.sin(a) * .35;
504 left.rotation.z = Math.sin(state.time * 2.8 + i * 1.5) * .28; right.rotation.z = -left.rotation.z;
505 });
506 if (audio && state.sound) {
507 const clock = audio.context.currentTime;
508 audio.waveGain.gain.setTargetAtTime(.13 + Math.sin(state.time * .6) * .035, clock, .2);
509 audio.motor.frequency.setTargetAtTime(50 + state.speed * 1.2, clock, .1);
510 audio.motorGain.gain.setTargetAtTime(state.paused ? 0 : .0045, clock, .1);
511 }
512 updateCamera(dt); renderer.render(scene, camera); state.frames++;
513 if (state.frames % 12 === 0) {
514 $('distance').textContent = (state.distance / 1000).toFixed(2);
515 const data = document.documentElement.dataset; data.distance = state.distance.toFixed(6); data.wheelAngle = rearWheel.rotation.x.toFixed(6); data.simTime = state.time.toFixed(6); data.paused = String(state.paused); data.speed = String(state.speed); data.view = state.view; data.frames = String(state.frames);
516 }
517 if (!state.ready) { state.ready = true; document.documentElement.dataset.ready = 'true'; loading.classList.add('hide'); setTimeout(() => loading.remove(), 600); }
518}
519// Read-only observations used to verify the delivered animation.
520window.__coastSnapshot = () => ({ ...state, wheelRadius, rearAngle: rearWheel.rotation.x, frontAngle: frontWheel.rotation.x, position: ride.position.toArray(), heading: ride.rotation.y, camera: camera.position.toArray(), drawCalls: renderer.info.render.calls, triangles: renderer.info.render.triangles, yaw: current.yaw, radius: current.radius, scarfTip: [scarfPositions[scarfPositions.length - 3], scarfPositions[scarfPositions.length - 2], scarfPositions[scarfPositions.length - 1]], eyeScale: eyes[0].scale.y, webgl: renderer.getContext().getParameter(renderer.getContext().VERSION) });
521animateCat(0, state.speed); updateCamera(1);
522requestAnimationFrame(render);