10 · GPT-5.6 Sol / max · 原场景逻辑源码

返回测评 · 运行原作品 · 下载原始源码

本页只阅读源码,不运行代码;行号对应原文件,完整成品保持原交付字节。

1import * as THREE from 'three';23const sceneHost = document.querySelector('#scene');4const loading = document.querySelector('#loading');5const errorPanel = document.querySelector('#errorPanel');6const pauseButton = document.querySelector('#pauseButton');7const pauseLabel = document.querySelector('#pauseLabel');8const speedRange = document.querySelector('#speedRange');9const speedValue = document.querySelector('#speedValue');10const viewButton = document.querySelector('#viewButton');11const viewLabel = document.querySelector('#viewLabel');12const hint = document.querySelector('#hint');1314const reducedMotion = window.matchMedia('(prefers-reduced-motion: reduce)').matches;15const state = {16  playing: !reducedMotion,17  speedKmh: 24,18  distance: 10,19  cameraMode: 0,20  dragYaw: 0,21  dragPitch: 0,22  zoom: 1,23};2425const palette = {26  skyTop: 0x68cadc,27  skyLow: 0xdff5df,28  seaDeep: 0x147f9d,29  seaLight: 0x42ced0,30  ink: 0x113d4c,31  road: 0x495961,32  roadSide: 0xe9e1c4,33  grass: 0x78a85a,34  grassLight: 0xa3c973,35  sand: 0xeecf8e,36  cliff: 0xba7555,37  coral: 0xf06d52,38  mint: 0x6fd3b6,39  mintDark: 0x2d8f82,40  orange: 0xe9913d,41  orangeDark: 0xb85b2a,42  cream: 0xffe2ad,43};4445let renderer;46let camera;47let oceanMaterial;48let roadCurve;49let pathLength;50let rideRig;51let scooterVisual;52let rearWheel;53let frontWheel;54let frontAssembly;55let catHead;56let catBody;57let tailSegments = [];58let scarfTails = [];59let eyes = [];60let gulls = [];61let clouds = [];62let cameraTarget = new THREE.Vector3();63let firstFrame = true;6465const mat = (color, roughness = .78, metalness = .02) => new THREE.MeshStandardMaterial({66  color,67  roughness,68  metalness,69  flatShading: true,70});7172const materials = {73  road: mat(palette.road, .95),74  white: mat(0xf8f5e9, .82),75  yellow: mat(0xffd66b, .72),76  dark: mat(palette.ink, .72),77  tire: mat(0x17272d, .92),78  metal: mat(0xc5d4d3, .34, .7),79  mint: mat(palette.mint, .55, .08),80  mintDark: mat(palette.mintDark, .65),81  coral: mat(palette.coral, .68),82  orange: mat(palette.orange, .8),83  orangeDark: mat(palette.orangeDark, .82),84  cream: mat(palette.cream, .82),85  pink: mat(0xef8f8b, .75),86  green: mat(palette.grass, .92),87  greenLight: mat(palette.grassLight, .92),88  sand: mat(palette.sand, .96),89  cliff: mat(palette.cliff, .96),90  trunk: mat(0x9b613c, .93),91  leaf: mat(0x2f7f64, .9),92  red: mat(0xe95548, .66),93};9495function shadow(mesh, cast = true, receive = true) {96  mesh.castShadow = cast;97  mesh.receiveShadow = receive;98  return mesh;99}100101function mesh(geometry, material, cast = true, receive = true) {102  return shadow(new THREE.Mesh(geometry, material), cast, receive);103}104105function cylinderBetween(a, b, radius, material, radial = 8) {106  const start = a.clone();107  const end = b.clone();108  const delta = end.clone().sub(start);109  const item = mesh(new THREE.CylinderGeometry(radius, radius * .92, delta.length(), radial), material);110  item.position.copy(start).add(end).multiplyScalar(.5);111  item.quaternion.setFromUnitVectors(new THREE.Vector3(0, 1, 0), delta.normalize());112  return item;113}114115function createSky() {116  const geometry = new THREE.SphereGeometry(260, 32, 16);117  const material = new THREE.ShaderMaterial({118    side: THREE.BackSide,119    uniforms: {120      topColor: { value: new THREE.Color(palette.skyTop) },121      bottomColor: { value: new THREE.Color(palette.skyLow) },122    },123    vertexShader: `124      varying vec3 vWorldPosition;125      void main() {126        vec4 worldPosition = modelMatrix * vec4(position, 1.0);127        vWorldPosition = worldPosition.xyz;128        gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);129      }130    `,131    fragmentShader: `132      uniform vec3 topColor;133      uniform vec3 bottomColor;134      varying vec3 vWorldPosition;135      void main() {136        float h = normalize(vWorldPosition).y * 0.5 + 0.5;137        vec3 c = mix(bottomColor, topColor, smoothstep(0.1, 0.86, h));138        gl_FragColor = vec4(c, 1.0);139      }140    `,141  });142  scene.add(new THREE.Mesh(geometry, material));143144  const sun = mesh(new THREE.SphereGeometry(7.5, 24, 16), new THREE.MeshBasicMaterial({ color: 0xffe6a0 }), false, false);145  sun.position.set(-72, 58, -110);146  scene.add(sun);147148  const halo = mesh(new THREE.SphereGeometry(11, 20, 12), new THREE.MeshBasicMaterial({149    color: 0xffdd8a,150    transparent: true,151    opacity: .16,152    depthWrite: false,153  }), false, false);154  halo.position.copy(sun.position);155  scene.add(halo);156}157158function createOcean() {159  const geometry = new THREE.PlaneGeometry(520, 520, 90, 90);160  oceanMaterial = new THREE.ShaderMaterial({161    uniforms: {162      uTime: { value: 0 },163      deep: { value: new THREE.Color(palette.seaDeep) },164      light: { value: new THREE.Color(palette.seaLight) },165      sunColor: { value: new THREE.Color(0xffe5a4) },166    },167    vertexShader: `168      uniform float uTime;169      varying float vWave;170      varying vec2 vUv;171      void main() {172        vUv = uv;173        vec3 p = position;174        float w1 = sin(p.x * .09 + uTime * .85) * .22;175        float w2 = sin(p.y * .135 - uTime * 1.12) * .16;176        float w3 = sin((p.x + p.y) * .055 + uTime * .55) * .12;177        p.z += w1 + w2 + w3;178        vWave = p.z;179        gl_Position = projectionMatrix * modelViewMatrix * vec4(p, 1.0);180      }181    `,182    fragmentShader: `183      uniform vec3 deep;184      uniform vec3 light;185      uniform vec3 sunColor;186      uniform float uTime;187      varying float vWave;188      varying vec2 vUv;189      void main() {190        float band = .5 + .5 * sin(vUv.y * 210.0 + vUv.x * 35.0 + uTime * .6);191        float glint = smoothstep(.94, 1.0, band) * smoothstep(.1, .72, vUv.y);192        vec3 color = mix(deep, light, clamp(vWave * 1.25 + .5, 0.0, 1.0));193        color = mix(color, sunColor, glint * .2);194        gl_FragColor = vec4(color, 1.0);195      }196    `,197  });198  const ocean = new THREE.Mesh(geometry, oceanMaterial);199  ocean.rotation.x = -Math.PI / 2;200  ocean.position.y = -2.25;201  ocean.receiveShadow = true;202  scene.add(ocean);203}204205function createIsland() {206  const cliff = mesh(new THREE.CylinderGeometry(72, 77, 3.2, 64), materials.cliff, false, true);207  cliff.scale.set(1.12, 1, .76);208  cliff.position.y = -1.55;209  scene.add(cliff);210211  const sand = mesh(new THREE.CylinderGeometry(70.8, 73, .8, 64), materials.sand, false, true);212  sand.scale.set(1.12, 1, .76);213  sand.position.y = -.23;214  scene.add(sand);215216  const grass = mesh(new THREE.CylinderGeometry(62.5, 64, .75, 64), materials.green, false, true);217  grass.scale.set(1.12, 1, .74);218  grass.position.y = .12;219  scene.add(grass);220221  const foam = mesh(new THREE.TorusGeometry(1, .022, 4, 160), new THREE.MeshBasicMaterial({222    color: 0xf4ffff,223    transparent: true,224    opacity: .75,225  }), false, false);226  foam.scale.set(78, 55, 1);227  foam.rotation.x = Math.PI / 2;228  foam.position.y = -1.8;229  scene.add(foam);230}231232function createRoadRibbon(curve, width, material, y, offset = 0) {233  const segments = 420;234  const positions = [];235  const indices = [];236  for (let i = 0; i <= segments; i++) {237    const u = (i % segments) / segments;238    const p = curve.getPointAt(u);239    const t = curve.getTangentAt(u).normalize();240    const n = new THREE.Vector3(t.z, 0, -t.x).normalize();241    const center = p.clone().addScaledVector(n, offset);242    const left = center.clone().addScaledVector(n, width / 2);243    const right = center.clone().addScaledVector(n, -width / 2);244    positions.push(left.x, y, left.z, right.x, y, right.z);245    if (i < segments) {246      const a = i * 2;247      indices.push(a, a + 1, a + 2, a + 1, a + 3, a + 2);248    }249  }250  const geometry = new THREE.BufferGeometry();251  geometry.setAttribute('position', new THREE.Float32BufferAttribute(positions, 3));252  geometry.setIndex(indices);253  geometry.computeVertexNormals();254  const ribbon = mesh(geometry, material, false, true);255  scene.add(ribbon);256  return ribbon;257}258259function createRoad() {260  const points = [261    new THREE.Vector3(-33, 0, 35),262    new THREE.Vector3(8, 0, 39),263    new THREE.Vector3(47, 0, 24),264    new THREE.Vector3(58, 0, -4),265    new THREE.Vector3(39, 0, -31),266    new THREE.Vector3(2, 0, -40),267    new THREE.Vector3(-40, 0, -31),268    new THREE.Vector3(-57, 0, -4),269    new THREE.Vector3(-51, 0, 24),270  ];271  roadCurve = new THREE.CatmullRomCurve3(points, true, 'centripetal', .45);272  pathLength = roadCurve.getLength();273274  createRoadRibbon(roadCurve, 9.4, materials.roadSide, .56);275  createRoadRibbon(roadCurve, 8.2, materials.road, .59);276  createRoadRibbon(roadCurve, .13, materials.white, .625, 3.63);277  createRoadRibbon(roadCurve, .13, materials.white, .625, -3.63);278279  for (let i = 0; i < 72; i += 2) {280    const u = i / 72;281    const p = roadCurve.getPointAt(u);282    const t = roadCurve.getTangentAt(u).normalize();283    const dash = mesh(new THREE.BoxGeometry(.16, .045, 2.35), materials.yellow, false, true);284    dash.position.set(p.x, .655, p.z);285    dash.rotation.y = Math.atan2(t.x, t.z);286    scene.add(dash);287  }288}289290function createPalm(scale = 1) {291  const group = new THREE.Group();292  const trunkPoints = [293    new THREE.Vector3(0, 0, 0),294    new THREE.Vector3(.12, 1.6 * scale, 0),295    new THREE.Vector3(-.08, 3.2 * scale, .08),296    new THREE.Vector3(.18, 4.7 * scale, 0),297  ];298  for (let i = 0; i < trunkPoints.length - 1; i++) {299    group.add(cylinderBetween(trunkPoints[i], trunkPoints[i + 1], (.19 - i * .025) * scale, materials.trunk, 7));300  }301  const crown = new THREE.Vector3(.18, 4.75 * scale, 0);302  for (let i = 0; i < 7; i++) {303    const angle = i / 7 * Math.PI * 2;304    const leaf = mesh(new THREE.ConeGeometry(.34 * scale, 3.1 * scale, 5), i % 2 ? materials.greenLight : materials.leaf);305    leaf.position.copy(crown).add(new THREE.Vector3(Math.cos(angle) * 1.25 * scale, -.2 * scale, Math.sin(angle) * 1.25 * scale));306    leaf.rotation.z = Math.PI / 2 + .2;307    leaf.rotation.y = -angle;308    group.add(leaf);309  }310  const coconuts = mesh(new THREE.IcosahedronGeometry(.28 * scale, 1), materials.trunk);311  coconuts.position.copy(crown).add(new THREE.Vector3(.15, -.3, .08));312  group.add(coconuts);313  return group;314}315316function createBush(scale = 1) {317  const group = new THREE.Group();318  for (let i = 0; i < 4; i++) {319    const b = mesh(new THREE.IcosahedronGeometry((.7 + i * .08) * scale, 1), i % 2 ? materials.greenLight : materials.green);320    b.position.set((i - 1.5) * .45 * scale, (i % 2) * .22 * scale, (i % 3 - 1) * .32 * scale);321    group.add(b);322  }323  return group;324}325326function scatterIslandLife() {327  const palmUs = [.03, .12, .23, .35, .48, .61, .73, .84, .93];328  palmUs.forEach((u, i) => {329    const p = roadCurve.getPointAt(u);330    const t = roadCurve.getTangentAt(u).normalize();331    const n = new THREE.Vector3(t.z, 0, -t.x).normalize();332    const palm = createPalm(.72 + (i % 3) * .12);333    palm.position.copy(p).addScaledVector(n, -7.2 - (i % 2) * 3.1);334    palm.position.y = .48;335    palm.rotation.y = i * 1.7;336    scene.add(palm);337  });338339  for (let i = 0; i < 24; i++) {340    const u = (i * .041 + .018) % 1;341    const p = roadCurve.getPointAt(u);342    const t = roadCurve.getTangentAt(u).normalize();343    const n = new THREE.Vector3(t.z, 0, -t.x).normalize();344    const bush = createBush(.45 + (i % 4) * .08);345    bush.position.copy(p).addScaledVector(n, -6.6 - (i % 3) * 1.8);346    bush.position.y = .58;347    scene.add(bush);348  }349350  for (let i = 0; i < 16; i++) {351    const angle = i / 16 * Math.PI * 2;352    const rock = mesh(new THREE.DodecahedronGeometry(1.1 + (i % 3) * .35, 0), i % 2 ? materials.sand : materials.cliff);353    rock.scale.y = .55;354    rock.position.set(Math.cos(angle) * (70 + (i % 2) * 3), -1.2, Math.sin(angle) * (48 + (i % 3)));355    rock.rotation.set(i * .4, i * .77, i * .21);356    scene.add(rock);357  }358}359360function createLighthouse() {361  const group = new THREE.Group();362  const base = mesh(new THREE.CylinderGeometry(2.3, 2.8, 1.2, 10), materials.cliff);363  base.position.y = -1.0;364  group.add(base);365  const tower = mesh(new THREE.CylinderGeometry(.75, 1.15, 7.2, 12), materials.white);366  tower.position.y = 3.15;367  group.add(tower);368  const stripe = mesh(new THREE.CylinderGeometry(.91, .97, 1.25, 12), materials.coral);369  stripe.position.y = 3.0;370  group.add(stripe);371  const cap = mesh(new THREE.ConeGeometry(1.18, 1.15, 12), materials.coral);372  cap.position.y = 7.25;373  group.add(cap);374  const lamp = mesh(new THREE.CylinderGeometry(.7, .7, 1.1, 12), new THREE.MeshStandardMaterial({375    color: 0xffe7a0,376    emissive: 0xffcf62,377    emissiveIntensity: 1.4,378    transparent: true,379    opacity: .92,380  }));381  lamp.position.y = 6.45;382  group.add(lamp);383  group.position.set(-78, -.8, 8);384  scene.add(group);385}386387function createCloud(x, y, z, scale, speed) {388  const group = new THREE.Group();389  const cloudMat = new THREE.MeshBasicMaterial({ color: 0xffffff, transparent: true, opacity: .76, depthWrite: false });390  [[0,0,0,2.2],[2.1,.2,0,1.65],[-2,.05,0,1.5],[.5,.75,0,1.55]].forEach(([px,py,pz,s]) => {391    const puff = mesh(new THREE.IcosahedronGeometry(s, 1), cloudMat, false, false);392    puff.position.set(px, py, pz);393    group.add(puff);394  });395  group.position.set(x, y, z);396  group.scale.setScalar(scale);397  group.userData.speed = speed;398  scene.add(group);399  clouds.push(group);400}401402function createGull(x, y, z, phase) {403  const group = new THREE.Group();404  const body = mesh(new THREE.CapsuleGeometry(.12, .45, 2, 6), materials.white, false, false);405  body.rotation.x = Math.PI / 2;406  group.add(body);407  const wingGeo = new THREE.ConeGeometry(.09, 1.2, 4);408  const left = mesh(wingGeo, materials.white, false, false);409  left.position.x = -.55;410  left.rotation.z = -Math.PI / 2;411  const right = mesh(wingGeo, materials.white, false, false);412  right.position.x = .55;413  right.rotation.z = Math.PI / 2;414  group.add(left, right);415  group.position.set(x, y, z);416  group.scale.setScalar(.8);417  group.userData = { phase, left, right };418  scene.add(group);419  gulls.push(group);420}421422function createWheel() {423  const group = new THREE.Group();424  const tire = mesh(new THREE.TorusGeometry(.64, .115, 10, 28), materials.tire);425  tire.rotation.y = Math.PI / 2;426  group.add(tire);427428  const rim = mesh(new THREE.TorusGeometry(.48, .028, 6, 28), materials.metal);429  rim.rotation.y = Math.PI / 2;430  group.add(rim);431432  for (let i = 0; i < 8; i++) {433    const spoke = mesh(new THREE.BoxGeometry(.024, .92, .026), materials.metal, false, false);434    spoke.rotation.x = i / 8 * Math.PI;435    group.add(spoke);436  }437  const hub = mesh(new THREE.CylinderGeometry(.09, .09, .34, 10), materials.metal);438  hub.rotation.z = Math.PI / 2;439  group.add(hub);440  return group;441}442443function createScooter() {444  rideRig = new THREE.Group();445  scooterVisual = new THREE.Group();446  rideRig.add(scooterVisual);447  scene.add(rideRig);448449  const blob = mesh(new THREE.CircleGeometry(1.9, 24), new THREE.MeshBasicMaterial({450    color: 0x17343d,451    transparent: true,452    opacity: .18,453    depthWrite: false,454  }), false, false);455  blob.rotation.x = -Math.PI / 2;456  blob.scale.set(1, 1.7, 1);457  blob.position.y = .02;458  rideRig.add(blob);459460  rearWheel = createWheel();461  rearWheel.position.set(0, .64, -1.48);462  scooterVisual.add(rearWheel);463464  frontAssembly = new THREE.Group();465  frontAssembly.position.z = 1.48;466  frontWheel = createWheel();467  frontWheel.position.y = .64;468  frontAssembly.add(frontWheel);469  scooterVisual.add(frontAssembly);470471  const deck = mesh(new THREE.BoxGeometry(.72, .18, 2.35), materials.mintDark);472  deck.position.set(0, .83, -.02);473  deck.rotation.x = -.02;474  scooterVisual.add(deck);475476  const floor = mesh(new THREE.BoxGeometry(.58, .08, 1.68), materials.dark);477  floor.position.set(0, .95, .13);478  scooterVisual.add(floor);479480  const body = mesh(new THREE.CapsuleGeometry(.48, .58, 5, 10), materials.mint);481  body.scale.set(1.08, 1, .78);482  body.rotation.x = -.28;483  body.position.set(0, 1.43, -1.03);484  scooterVisual.add(body);485486  const panel = mesh(new THREE.CapsuleGeometry(.29, .32, 4, 8), materials.mintDark);487  panel.rotation.x = Math.PI / 2;488  panel.position.set(0, 1.25, -1.38);489  scooterVisual.add(panel);490491  const seatStem = mesh(new THREE.CylinderGeometry(.1, .13, .68, 8), materials.metal);492  seatStem.position.set(0, 1.66, -.65);493  scooterVisual.add(seatStem);494  const seat = mesh(new THREE.CapsuleGeometry(.26, .52, 4, 8), materials.dark);495  seat.rotation.x = Math.PI / 2;496  seat.scale.x = 1.35;497  seat.position.set(0, 2.03, -.63);498  scooterVisual.add(seat);499500  const stemA = new THREE.Vector3(0, .88, 1.45);501  const stemB = new THREE.Vector3(0, 2.58, 1.65);502  frontAssembly.add(cylinderBetween(stemA.clone().setZ(0), stemB.clone().setZ(.17), .075, materials.metal, 10));503  const head = mesh(new THREE.CapsuleGeometry(.32, .42, 4, 8), materials.mint);504  head.rotation.x = -.22;505  head.position.set(0, 1.88, .08);506  frontAssembly.add(head);507508  const handle = mesh(new THREE.CylinderGeometry(.055, .055, 1.48, 10), materials.dark);509  handle.rotation.z = Math.PI / 2;510  handle.position.set(0, 2.55, .18);511  frontAssembly.add(handle);512513  const lamp = mesh(new THREE.SphereGeometry(.22, 12, 8), new THREE.MeshStandardMaterial({514    color: 0xfff1ba,515    emissive: 0xffda72,516    emissiveIntensity: 1.2,517    roughness: .35,518  }));519  lamp.scale.set(1, .75, .45);520  lamp.position.set(0, 2.03, .44);521  frontAssembly.add(lamp);522523  [-1, 1].forEach((side) => {524    const mirrorStem = cylinderBetween(new THREE.Vector3(side * .48, 2.57, .18), new THREE.Vector3(side * .65, 2.93, .2), .025, materials.metal, 7);525    frontAssembly.add(mirrorStem);526    const mirror = mesh(new THREE.SphereGeometry(.14, 10, 7), materials.dark);527    mirror.scale.set(1.25, .9, .35);528    mirror.position.set(side * .67, 2.97, .21);529    frontAssembly.add(mirror);530  });531532  createCat();533}534535function createCat() {536  const cat = new THREE.Group();537  scooterVisual.add(cat);538539  catBody = mesh(new THREE.CapsuleGeometry(.5, 1.0, 6, 12), materials.orange);540  catBody.position.set(0, 2.94, -.45);541  catBody.rotation.x = -.11;542  cat.add(catBody);543544  const belly = mesh(new THREE.CapsuleGeometry(.28, .7, 5, 10), materials.cream);545  belly.position.set(0, 2.98, -.03);546  belly.rotation.x = -.18;547  belly.scale.z = .45;548  cat.add(belly);549550  catHead = new THREE.Group();551  catHead.position.set(0, 4.13, -.03);552  cat.add(catHead);553554  const head = mesh(new THREE.SphereGeometry(.58, 18, 13), materials.orange);555  head.scale.set(.94, .9, .9);556  catHead.add(head);557558  [-1, 1].forEach((side) => {559    const ear = mesh(new THREE.ConeGeometry(.23, .58, 4), materials.orange);560    ear.position.set(side * .34, .48, -.03);561    ear.rotation.z = side * -.09;562    ear.rotation.y = Math.PI / 4;563    catHead.add(ear);564    const inner = mesh(new THREE.ConeGeometry(.105, .31, 4), materials.pink);565    inner.position.set(side * .34, .5, .18);566    inner.rotation.z = side * -.09;567    inner.rotation.y = Math.PI / 4;568    catHead.add(inner);569  });570571  const muzzleLeft = mesh(new THREE.SphereGeometry(.18, 12, 8), materials.cream);572  muzzleLeft.position.set(-.13, -.1, .48);573  muzzleLeft.scale.set(1, .72, .55);574  const muzzleRight = muzzleLeft.clone();575  muzzleRight.position.x = .13;576  catHead.add(muzzleLeft, muzzleRight);577578  const nose = mesh(new THREE.ConeGeometry(.07, .1, 3), materials.pink);579  nose.rotation.x = Math.PI / 2;580  nose.position.set(0, -.05, .59);581  catHead.add(nose);582583  [-1, 1].forEach((side) => {584    const eye = mesh(new THREE.SphereGeometry(.075, 10, 7), materials.dark);585    eye.position.set(side * .205, .16, .49);586    eye.scale.set(.72, 1.3, .42);587    catHead.add(eye);588    eyes.push(eye);589590    for (let i = -1; i <= 1; i++) {591      const whisker = cylinderBetween(592        new THREE.Vector3(side * .24, -.08 + i * .06, .48),593        new THREE.Vector3(side * .73, -.04 + i * .13, .58),594        .008,595        materials.cream,596        5,597      );598      catHead.add(whisker);599    }600  });601602  const helmet = mesh(new THREE.SphereGeometry(.63, 18, 9, 0, Math.PI * 2, 0, Math.PI / 2), materials.coral);603  helmet.position.y = .08;604  helmet.scale.set(1, .82, 1);605  catHead.add(helmet);606  const strap = mesh(new THREE.TorusGeometry(.48, .028, 6, 20, Math.PI), materials.dark);607  strap.rotation.set(Math.PI / 2, 0, 0);608  strap.position.set(0, -.18, .04);609  catHead.add(strap);610611  [-1, 1].forEach((side) => {612    const shoulder = new THREE.Vector3(side * .36, 3.45, -.08);613    const elbow = new THREE.Vector3(side * .5, 3.02, .7);614    const paw = new THREE.Vector3(side * .55, 2.62, 1.64);615    cat.add(cylinderBetween(shoulder, elbow, .115, materials.orange, 9));616    cat.add(cylinderBetween(elbow, paw, .1, materials.cream, 9));617    const pawMesh = mesh(new THREE.SphereGeometry(.13, 10, 8), materials.cream);618    pawMesh.position.copy(paw);619    cat.add(pawMesh);620621    const hip = new THREE.Vector3(side * .3, 2.45, -.5);622    const knee = new THREE.Vector3(side * .42, 1.75, -.05);623    const foot = new THREE.Vector3(side * .32, 1.1, .35);624    cat.add(cylinderBetween(hip, knee, .15, materials.orange, 9));625    cat.add(cylinderBetween(knee, foot, .13, materials.orangeDark, 9));626    const footMesh = mesh(new THREE.CapsuleGeometry(.11, .27, 4, 8), materials.cream);627    footMesh.rotation.x = Math.PI / 2;628    footMesh.position.copy(foot).add(new THREE.Vector3(0, 0, .1));629    cat.add(footMesh);630  });631632  const scarfKnot = mesh(new THREE.SphereGeometry(.14, 10, 7), materials.red);633  scarfKnot.position.set(.34, 3.7, -.36);634  cat.add(scarfKnot);635  [0, 1].forEach((i) => {636    const tail = mesh(new THREE.CapsuleGeometry(.065, .58 - i * .08, 4, 7), materials.red);637    tail.rotation.x = Math.PI / 2 + .18;638    tail.rotation.z = -.15 + i * .28;639    tail.position.set(.25 + i * .14, 3.63 - i * .08, -.73 - i * .16);640    cat.add(tail);641    scarfTails.push(tail);642  });643644  const tailRoot = new THREE.Group();645  tailRoot.position.set(-.28, 2.77, -.74);646  cat.add(tailRoot);647  let parent = tailRoot;648  for (let i = 0; i < 6; i++) {649    const segmentRoot = new THREE.Group();650    const segment = mesh(new THREE.CapsuleGeometry(.14 - i * .012, .38, 4, 7), i % 3 === 1 ? materials.orangeDark : materials.orange);651    segment.rotation.x = Math.PI / 2;652    segment.position.z = -.28;653    segmentRoot.add(segment);654    parent.add(segmentRoot);655    segmentRoot.position.z = i === 0 ? 0 : -.5;656    segmentRoot.rotation.y = -.08;657    tailSegments.push(segmentRoot);658    parent = segmentRoot;659  }660}661662function addLights() {663  const hemi = new THREE.HemisphereLight(0xdaf6ff, 0x815c42, 2.2);664  scene.add(hemi);665  const sun = new THREE.DirectionalLight(0xfff0ca, 3.4);666  sun.position.set(-45, 62, -30);667  sun.castShadow = true;668  sun.shadow.mapSize.set(1536, 1536);669  sun.shadow.camera.left = -35;670  sun.shadow.camera.right = 35;671  sun.shadow.camera.top = 35;672  sun.shadow.camera.bottom = -35;673  sun.shadow.camera.near = 1;674  sun.shadow.camera.far = 150;675  sun.shadow.bias = -.0004;676  scene.add(sun);677}678679function init() {680  try {681    const canvas = document.createElement('canvas');682    const gl = canvas.getContext('webgl2') || canvas.getContext('webgl');683    if (!gl) throw new Error('WebGL unavailable');684685    renderer = new THREE.WebGLRenderer({ antialias: true, powerPreference: 'high-performance', alpha: false });686    renderer.setPixelRatio(Math.min(window.devicePixelRatio, window.innerWidth < 700 ? 1.45 : 1.8));687    renderer.setSize(window.innerWidth, window.innerHeight);688    renderer.shadowMap.enabled = true;689    renderer.shadowMap.type = THREE.PCFSoftShadowMap;690    renderer.outputColorSpace = THREE.SRGBColorSpace;691    renderer.toneMapping = THREE.ACESFilmicToneMapping;692    renderer.toneMappingExposure = 1.05;693    sceneHost.appendChild(renderer.domElement);694695    scene.background = new THREE.Color(palette.skyTop);696    scene.fog = new THREE.Fog(0x9adfcf, 95, 230);697    camera = new THREE.PerspectiveCamera(48, window.innerWidth / window.innerHeight, .1, 600);698    camera.position.set(0, 8, -14);699700    createSky();701    createOcean();702    createIsland();703    createRoad();704    scatterIslandLife();705    createLighthouse();706    createCloud(-75, 45, -105, 1.9, .16);707    createCloud(38, 52, -130, 1.3, .1);708    createCloud(96, 39, -72, 1.05, .13);709    createGull(-18, 17, -36, 0);710    createGull(-23, 19, -41, 1.3);711    createGull(29, 16, -48, 2.1);712    createScooter();713    addLights();714    bindControls();715    updatePauseUI();716    resize();717    animate();718  } catch (error) {719    console.error(error);720    loading.classList.add('is-hidden');721    errorPanel.hidden = false;722    document.documentElement.dataset.sceneStatus = 'error';723  }724}725726function ridePose() {727  const u = ((state.distance % pathLength) + pathLength) % pathLength / pathLength;728  const point = roadCurve.getPointAt(u);729  const tangent = roadCurve.getTangentAt(u).normalize();730  const normal = new THREE.Vector3(tangent.z, 0, -tangent.x).normalize();731  const lanePoint = point.clone().addScaledVector(normal, 2.02);732  lanePoint.y = .66;733  rideRig.position.copy(lanePoint);734  rideRig.rotation.y = Math.atan2(tangent.x, tangent.z);735736  const ahead = roadCurve.getTangentAt((u + .008) % 1).normalize();737  const heading = Math.atan2(tangent.x, tangent.z);738  const headingAhead = Math.atan2(ahead.x, ahead.z);739  let steering = headingAhead - heading;740  steering = Math.atan2(Math.sin(steering), Math.cos(steering));741  frontAssembly.rotation.y = THREE.MathUtils.clamp(steering * 2.6, -.27, .27);742  return { point: lanePoint, tangent, heading };743}744745function updateRide(delta, elapsed) {746  if (state.playing) state.distance += (state.speedKmh / 3.6) * delta;747  const pose = ridePose();748  const travelPhase = state.distance * 1.85;749  const wheelSpin = -state.distance / .64;750  rearWheel.rotation.x = wheelSpin;751  frontWheel.rotation.x = wheelSpin;752  scooterVisual.position.y = Math.sin(travelPhase) * .018 + Math.sin(travelPhase * .47) * .012;753  scooterVisual.rotation.z = Math.sin(travelPhase * .18) * .012;754755  catBody.rotation.z = Math.sin(travelPhase * .62) * .025;756  catHead.rotation.y = Math.sin(elapsed * .43) * .13;757  catHead.rotation.z = Math.sin(elapsed * .72) * .018;758  const blinkCycle = elapsed % 4.6;759  const blink = blinkCycle > 4.18 && blinkCycle < 4.36 ? .08 : 1;760  eyes.forEach((eye) => { eye.scale.y = 1.3 * blink; });761762  tailSegments.forEach((segment, i) => {763    segment.rotation.y = Math.sin(elapsed * 2.0 - i * .5) * (.19 + i * .015) - .08;764    segment.rotation.x = Math.cos(elapsed * 1.45 - i * .35) * .055;765  });766  scarfTails.forEach((tail, i) => {767    tail.rotation.y = Math.sin(elapsed * 4.2 + i) * .11;768    tail.rotation.x = Math.PI / 2 + .18 + Math.sin(elapsed * 3.2 + i * .8) * .1;769  });770771  document.documentElement.dataset.sceneStatus = 'ready';772  document.documentElement.dataset.distance = state.distance.toFixed(4);773  document.documentElement.dataset.wheelRotation = wheelSpin.toFixed(4);774  document.documentElement.dataset.speedKmh = String(state.speedKmh);775  document.documentElement.dataset.playing = String(state.playing);776  document.documentElement.dataset.cameraMode = String(state.cameraMode);777  return pose;778}779780const cameraModes = [781  { label: '追随镜头', offset: new THREE.Vector3(0, 5.1, -10.5), target: new THREE.Vector3(0, 2.7, 1.5), fov: 48 },782  { label: '侧拍镜头', offset: new THREE.Vector3(10.5, 4.0, -1.5), target: new THREE.Vector3(0, 2.55, .25), fov: 50 },783  { label: '海岸全景', offset: new THREE.Vector3(17, 15, -18), target: new THREE.Vector3(0, 1.2, 2), fov: 54 },784];785786function updateCamera(pose, delta) {787  const mode = cameraModes[state.cameraMode];788  const yaw = pose.heading + state.dragYaw;789  const raw = mode.offset.clone();790  raw.y += state.dragPitch * 8;791  raw.multiplyScalar(state.zoom);792  const c = Math.cos(yaw);793  const s = Math.sin(yaw);794  const rotated = new THREE.Vector3(raw.x * c + raw.z * s, raw.y, -raw.x * s + raw.z * c);795  const desired = pose.point.clone().add(rotated);796797  const targetLocal = mode.target.clone();798  const targetRotated = new THREE.Vector3(799    targetLocal.x * Math.cos(pose.heading) + targetLocal.z * Math.sin(pose.heading),800    targetLocal.y,801    -targetLocal.x * Math.sin(pose.heading) + targetLocal.z * Math.cos(pose.heading),802  );803  const desiredTarget = pose.point.clone().add(targetRotated);804  const ease = 1 - Math.exp(-delta * 4.2);805  camera.position.lerp(desired, ease);806  cameraTarget.lerp(desiredTarget, ease);807  camera.fov = THREE.MathUtils.lerp(camera.fov, mode.fov, ease);808  camera.updateProjectionMatrix();809  camera.lookAt(cameraTarget);810}811812function updateWorld(elapsed) {813  oceanMaterial.uniforms.uTime.value = elapsed;814  gulls.forEach((gull, i) => {815    const wing = Math.sin(elapsed * 3.4 + gull.userData.phase) * .42;816    gull.userData.left.rotation.z = -Math.PI / 2 + wing;817    gull.userData.right.rotation.z = Math.PI / 2 - wing;818    gull.position.y += Math.sin(elapsed * .8 + i) * .0015;819    gull.rotation.y = Math.sin(elapsed * .18 + i) * .28;820  });821  clouds.forEach((cloud) => {822    cloud.position.x += cloud.userData.speed * .01;823    if (cloud.position.x > 130) cloud.position.x = -130;824  });825}826827const clock = new THREE.Clock();828function animate() {829  const delta = Math.min(clock.getDelta(), .05);830  const elapsed = clock.elapsedTime;831  const pose = updateRide(delta, elapsed);832  updateWorld(elapsed);833  updateCamera(pose, delta);834  renderer.render(scene, camera);835  if (firstFrame) {836    firstFrame = false;837    requestAnimationFrame(() => loading.classList.add('is-hidden'));838    window.__sceneReady = true;839  }840  requestAnimationFrame(animate);841}842843function updatePauseUI() {844  pauseButton.setAttribute('aria-pressed', String(!state.playing));845  pauseLabel.textContent = state.playing ? '暂停' : '继续';846}847848function togglePause() {849  state.playing = !state.playing;850  updatePauseUI();851}852853function cycleCamera() {854  state.cameraMode = (state.cameraMode + 1) % cameraModes.length;855  state.dragYaw = 0;856  state.dragPitch = 0;857  state.zoom = 1;858  viewLabel.textContent = cameraModes[state.cameraMode].label;859}860861function bindControls() {862  pauseButton.addEventListener('click', togglePause);863  viewButton.addEventListener('click', cycleCamera);864  speedRange.addEventListener('input', () => {865    state.speedKmh = Number(speedRange.value);866    speedValue.textContent = speedRange.value;867  });868869  window.addEventListener('keydown', (event) => {870    if (event.target instanceof HTMLInputElement) return;871    if (event.code === 'Space' || event.key.toLowerCase() === 'p') {872      event.preventDefault();873      togglePause();874    } else if (event.key.toLowerCase() === 'v') {875      cycleCamera();876    } else if (event.key === 'ArrowUp' || event.key === 'ArrowRight') {877      state.speedKmh = Math.min(36, state.speedKmh + 1);878      speedRange.value = String(state.speedKmh);879      speedValue.textContent = String(state.speedKmh);880    } else if (event.key === 'ArrowDown' || event.key === 'ArrowLeft') {881      state.speedKmh = Math.max(10, state.speedKmh - 1);882      speedRange.value = String(state.speedKmh);883      speedValue.textContent = String(state.speedKmh);884    }885  });886887  const canvas = renderer.domElement;888  let dragging = false;889  let pointerX = 0;890  let pointerY = 0;891  canvas.addEventListener('pointerdown', (event) => {892    dragging = true;893    pointerX = event.clientX;894    pointerY = event.clientY;895    canvas.setPointerCapture(event.pointerId);896    hint.classList.add('is-hidden');897  });898  canvas.addEventListener('pointermove', (event) => {899    if (!dragging) return;900    const dx = event.clientX - pointerX;901    const dy = event.clientY - pointerY;902    pointerX = event.clientX;903    pointerY = event.clientY;904    state.dragYaw -= dx * .005;905    state.dragPitch = THREE.MathUtils.clamp(state.dragPitch + dy * .003, -.32, .48);906  });907  const stopDrag = (event) => {908    dragging = false;909    if (event.pointerId !== undefined && canvas.hasPointerCapture(event.pointerId)) canvas.releasePointerCapture(event.pointerId);910  };911  canvas.addEventListener('pointerup', stopDrag);912  canvas.addEventListener('pointercancel', stopDrag);913  canvas.addEventListener('wheel', (event) => {914    event.preventDefault();915    state.zoom = THREE.MathUtils.clamp(state.zoom + event.deltaY * .0007, .68, 1.55);916    hint.classList.add('is-hidden');917  }, { passive: false });918  window.setTimeout(() => hint.classList.add('is-hidden'), 7000);919}920921function resize() {922  if (!renderer || !camera) return;923  camera.aspect = window.innerWidth / window.innerHeight;924  camera.updateProjectionMatrix();925  renderer.setPixelRatio(Math.min(window.devicePixelRatio, window.innerWidth < 700 ? 1.45 : 1.8));926  renderer.setSize(window.innerWidth, window.innerHeight);927}928929window.addEventListener('resize', resize);930const scene = new THREE.Scene();931init();932