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