<!doctype html><html lang="zh-CN"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>猫的海岸线 · Coast Cat</title><style>*{box-sizing:border-box}html,body{margin:0;width:100%;height:100%;overflow:hidden;background:#bee4e4;color:#233f46;font-family:"Microsoft YaHei",system-ui,sans-serif}canvas{display:block;touch-action:none}.title{position:absolute;top:38px;left:42px;pointer-events:none}.eyebrow{font-size:11px;letter-spacing:4px;font-weight:700;color:#477b7e}.title h1{font-size:clamp(28px,4vw,52px);font-weight:600;letter-spacing:5px;margin:12px 0 8px}.title p{font-size:13px;letter-spacing:2px;opacity:.7}.badge{position:absolute;top:40px;right:40px;border:1px solid #fff9;border-radius:30px;padding:12px 18px;background:#ffffff55;backdrop-filter:blur(10px);font-size:12px;letter-spacing:2px}.dot{display:inline-block;width:7px;height:7px;border-radius:50%;background:#3e998b;margin-right:9px}.controls{position:absolute;bottom:30px;left:50%;transform:translateX(-50%);display:flex;align-items:center;gap:8px;padding:10px;border:1px solid #fffa;border-radius:22px;background:#f7fbf0dc;box-shadow:0 10px 45px #244e5220;backdrop-filter:blur(16px)}button{font:inherit;white-space:nowrap;border:0;border-radius:13px;padding:13px 19px;background:transparent;color:#36565b;cursor:pointer;font-size:13px}button:hover{background:#d8e8de}button.active{background:#285c60;color:white}button:focus-visible,input:focus-visible{outline:3px solid #e99a52;outline-offset:3px}.speed{display:flex;gap:9px;align-items:center;padding:0 12px;border-left:1px solid #bdd3cb;font-size:12px;white-space:nowrap}input{width:86px;accent-color:#347c76}.footer{position:absolute;bottom:38px;left:42px;line-height:1.8;font-size:10px;letter-spacing:2px;color:#507475}.hint{position:absolute;right:35px;bottom:40px;font-size:11px;color:#507475}.error{position:absolute;inset:40% 10%;text-align:center;display:none;background:#fff;padding:30px;border-radius:20px}@media(max-width:700px){.title{top:23px;left:23px}.title p{font-size:11px;letter-spacing:1px}.badge{right:20px;top:24px;font-size:10px;padding:9px}.controls{bottom:22px;gap:2px;padding:7px;max-width:96vw;flex-wrap:wrap;justify-content:center;width:max-content}button{padding:11px 12px;font-size:12px}.speed{padding:0 8px}input{width:62px}.footer,.hint{display:none}.title h1{letter-spacing:2px}}</style></head><body><div class="title"><div class="eyebrow">COAST CAT / 一路向海</div><h1>猫的海岸线</h1><p>海风刚好,今天慢一点也没关系。</p></div><div class="badge"><span class="dot"></span>沿海公路 · 01</div><div class="footer">A LITTLE RIDE, A BIG BLUE SEA.<br>橘子号 / 海滨漫游</div><div class="hint">拖动环绕 · 滚轮缩放</div><div class="controls"><button id="play" class="active" aria-label="暂停动画">Ⅱ 暂停</button><button id="view">◎ 海岸视角</button><button id="reset" aria-label="重置视角和行程">↺ 重置</button><label class="speed">速度 <input id="speed" aria-label="骑行速度" type="range" min="0.4" max="1.8" step="0.1" value="1"><span id="speedValue">1.0×</span></label></div><div id="error" class="error"></div><script>/* THREE_INLINE */</script><script>'use strict';try {const scene=new THREE.Scene();scene.background=new THREE.Color('#bce2e4');scene.fog=new THREE.Fog('#bce2e4',55,160);const renderer=new THREE.WebGLRenderer({antialias:true,alpha:false});renderer.setSize(innerWidth,innerHeight);renderer.setPixelRatio(Math.min(devicePixelRatio,1.7));renderer.shadowMap.enabled=true;renderer.shadowMap.type=THREE.PCFSoftShadowMap;renderer.outputColorSpace=THREE.SRGBColorSpace;renderer.toneMapping=THREE.ACESFilmicToneMapping;renderer.toneMappingExposure=1.2;document.body.prepend(renderer.domElement);const camera=new THREE.PerspectiveCamera(40,innerWidth/innerHeight,.1,220);scene.add(new THREE.HemisphereLight('#fff7df','#74a8a2',2.5));const sun=new THREE.DirectionalLight('#fff1cf',3.2);sun.position.set(-12,25,12);sun.castShadow=true;sun.shadow.mapSize.set(2048,2048);Object.assign(sun.shadow.camera,{left:-26,right:26,top:22,bottom:-22,near:1,far:70});sun.shadow.bias=-.0004;scene.add(sun);const mat=(c,extra={})=>new THREE.MeshStandardMaterial({color:c,roughness:.78,...extra});const M={sand:mat('#eedbb0'),road:mat('#65767b'),cream:mat('#fff2d1'),mint:mat('#68c5b0',{metalness:.18,roughness:.35}),dark:mat('#263f47'),rubber:mat('#293a3f'),steel:mat('#c5e0d8',{metalness:.7,roughness:.3}),orange:mat('#eea04e'),light:mat('#ffe0a0'),stripe:mat('#bb6e35'),pink:mat('#edb2a0'),red:mat('#e66e52'),green:mat('#509b79'),trunk:mat('#a88156'),white:mat('#fff8de')};function mesh(g,m,p,x=0,y=0,z=0){const o=new THREE.Mesh(g,m);o.position.set(x,y,z);o.castShadow=true;o.receiveShadow=true;p.add(o);return o}function box(p,m,x,y,z,a,b,c,r=0){const o=mesh(new THREE.BoxGeometry(a,b,c),m,p,x,y,z);o.rotation.z=r;return o}function ball(p,m,x,y,z,a,b=a,c=a){const o=mesh(new THREE.SphereGeometry(1,24,16),m,p,x,y,z);o.scale.set(a,b,c);return o}function rod(p,m,a,b,r=.05){const av=new THREE.Vector3(...a),bv=new THREE.Vector3(...b);const o=mesh(new THREE.CylinderGeometry(r,r,av.distanceTo(bv),12),m,p);o.position.copy(av.add(bv).multiplyScalar(.5));o.quaternion.setFromUnitVectors(new THREE.Vector3(0,1,0),new THREE.Vector3(...b).sub(new THREE.Vector3(...a)).normalize());return o}function curve(p,m,pts,r=.05){return mesh(new THREE.TubeGeometry(new THREE.CatmullRomCurve3(pts.map(v=>new THREE.Vector3(...v))),32,r,8,false),m,p)}const ground=box(scene,M.sand,0,-.28,5,240,.5,38);box(scene,M.road,0,.006,3.5,240,.12,7.4);box(scene,mat('#e7d9bb'),0,-.02,-1.2,240,.14,2);box(scene,mat('#d5cbb2'),0,.07,7.55,240,.25,.65);for(const z of [.04,6.94])box(scene,M.cream,0,.078,z,240,.018,.08);const waterGeo=new THREE.PlaneGeometry(260,125,100,56);waterGeo.rotateX(-Math.PI/2);const water=mesh(waterGeo,mat('#57b8bc',{roughness:.34,metalness:.16}),scene,0,-.18,-65);water.castShadow=false;const foam=[];for(let i=0;i<6;i++){const g=new THREE.Group();g.position.z=-3.4-i*2.2;scene.add(g);for(let j=0;j<28;j++){const s=box(g,mat(i<2?'#edf2d8':'#a0ddd2'),j*8-110,-.07,Math.sin(j*2)*.3,2+((j*13)%5),.025,.065);s.rotation.y=Math.sin(j)*.05;}foam.push(g)}// A soft sun and distant islands make a complete coastal horizon.const sunDisk=ball(scene,mat('#fff1c1',{emissive:'#ffdf9d',emissiveIntensity:.3}),-40,32,-95,7);sunDisk.castShadow=false;const islandMat=mat('#86b9af');for(let i=0;i<9;i++)ball(scene,islandMat,Math.sin(i*4)*75,-1,-90-i%3*7,13+i%4*5,3+i%3*2,8);const cloudMat=mat('#eff5e8');for(let i=0;i<10;i++){const g=new THREE.Group();g.position.set(i*22-100,18+(i%3)*4,-55-(i%2)*20);scene.add(g);for(let j=0;j<4;j++)ball(g,cloudMat,j*2.8,Math.sin(j)*.8,0,3,1.1+j%2*.5,1.2)}const rider=new THREE.Group();rider.position.set(0,0,3.45);scene.add(rider);const wheels=[];for(const x of [-1.05,1.16]){const g=new THREE.Group();g.position.set(x,.48,0);rider.add(g);wheels.push(g);mesh(new THREE.TorusGeometry(.36,.105,12,36),M.rubber,g);mesh(new THREE.CylinderGeometry(.27,.27,.16,32),M.steel,g).rotation.x=Math.PI/2;mesh(new THREE.CylinderGeometry(.095,.095,.2,20),M.mint,g).rotation.x=Math.PI/2;for(let i=0;i<6;i++){const a=i*Math.PI/3;rod(g,M.cream,[Math.cos(a)*.11,Math.sin(a)*.11,.092],[Math.cos(a)*.245,Math.sin(a)*.245,.092],.017)}const f=mesh(new THREE.TorusGeometry(.5,.085,10,30,Math.PI),M.mint,rider,x,.47,0);f.scale.z=2;}box(rider,M.dark,-.1,.65,0,1.92,.15,.63);box(rider,M.mint,-.15,.78,0,1.8,.15,.75);ball(rider,M.mint,-.95,1,0,.62,.43,.42);box(rider,M.dark,-.85,1.34,0,1.13,.16,.68);ball(rider,M.dark,-.85,1.35,0,.53,.12,.35);// Curved front apron, fork, lamp, and real transverse handlebars.rod(rider,M.steel,[1.16,.48,-.17],[.87,1.55,-.17],.055);rod(rider,M.steel,[1.16,.48,.17],[.87,1.55,.17],.055);const apron=ball(rider,M.mint,.74,1.13,0,.20,.64,.42);apron.rotation.z=.2;rod(rider,M.mint,[.65,1.18,0],[.53,1.95,0],.12);rod(rider,M.steel,[.54,1.93,-.59],[.54,1.93,.59],.046);for(const z of [-.52,.52])rod(rider,M.dark,[.54,1.93,z-.1],[.54,1.93,z+.1],.063);ball(rider,M.mint,.65,1.88,0,.27,.22,.25);ball(rider,mat('#fff7ce',{emissive:'#ffe5a3',emissiveIntensity:.7}),.88,1.88,0,.055,.155,.18);ball(rider,M.red,-1.49,1.02,0,.05,.12,.20);for(const z of [-.47,.47]){rod(rider,M.steel,[.54,1.94,z],[.62,2.34,z*1.25],.024);ball(rider,M.dark,.64,2.36,z*1.25,.12,.10,.035);ball(rider,M.steel,.64,2.36,z*1.25+.023,.09,.074,.012)}// Small rear luggage rack and a coral picnic parcel.box(rider,M.steel,-1.49,1.3,0,.42,.06,.68);box(rider,M.cream,-1.50,1.53,0,.48,.4,.54);box(rider,M.red,-1.5,1.53,0,.5,.42,.07);box(rider,M.red,-1.5,1.74,0,.07,.015,.55);const cat=new THREE.Group();rider.add(cat);ball(cat,M.orange,-.57,1.89,0,.43,.66,.36);ball(cat,M.light,-.28,1.91,.06,.12,.41,.30);// Bent legs rest on the scooter floor, both paws stay on the grips.for(const z of [-.31,.31]){rod(cat,M.orange,[-.68,1.57,z],[-.16,1.26,z*1.13],.16);rod(cat,M.orange,[-.16,1.26,z*1.13],[-.03,.94,z*1.15],.12);ball(cat,M.light,.08,.92,z*1.15,.23,.10,.14);rod(cat,M.orange,[-.35,2.19,z],[.03,1.99,z*1.48],.12);rod(cat,M.orange,[.03,1.99,z*1.48],[.53,1.94,z*1.6],.095);ball(cat,M.light,.54,1.94,z*1.6,.14,.105,.12)}const head=new THREE.Group();head.position.set(-.34,2.66,0);cat.add(head);ball(head,M.orange,0,0,0,.52,.46,.44);for(const z of [-.30,.30]){const ear=mesh(new THREE.ConeGeometry(.23,.48,3),M.orange,head,-.06,.4,z);ear.rotation.y=Math.PI/2;ear.rotation.x=z>0?.16:-.16;const inn=mesh(new THREE.ConeGeometry(.135,.3,3),M.pink,head,.04,.43,z);inn.rotation.y=Math.PI/2;}// Face looks down the road (+X); cheeks and whiskers visible from either side.for(const z of [-.205,.205]){ball(head,M.light,.405,-.13,z,.17,.15,.20);ball(head,M.dark,.434,.07,z,.057,.075,.048);ball(head,M.white,.475,.091,z+.012,.017);for(let k=0;k<3;k++)rod(head,M.cream,[.48,-.14-k*.022,z*1.7],[.46-k*.04,-.1-k*.085,z*2.75],.009)}ball(head,M.pink,.563,-.115,0,.06,.044,.067);curve(head,M.dark,[[.55,-.16,0],[.54,-.215,0],[.51,-.23,.07]],.009);curve(head,M.dark,[[.55,-.16,0],[.54,-.215,0],[.51,-.23,-.07]],.009);for(const z of [-.42,.42])for(let k=0;k<3;k++)ball(head,M.stripe,-.09-k*.12,.01-k*.03,z,.045,.14,.019);const helmet=mesh(new THREE.SphereGeometry(.525,32,16,0,Math.PI*2,0,Math.PI*.49),M.cream,head,-.04,.16,0);helmet.scale.set(1, .77, .96);ball(head,M.mint,.34,.19,0,.28,.046,.47);curve(head,M.dark,[[.03,.16,.43],[.12,-.34,.31],[.16,-.37,-.31],[.03,.16,-.43]],.026);box(head,M.red,-.04,.56,0,.16,.013,.34);const scarf=mesh(new THREE.TorusGeometry(.255,.09,10,28),M.red,cat,-.34,2.29,0);scarf.rotation.x=Math.PI/2;const scarfTail=new THREE.Group();scarfTail.position.set(-.63,2.3,-.06);cat.add(scarfTail);const scarfGeo=new THREE.PlaneGeometry(.9,.22,12,1);const scarfMesh=mesh(scarfGeo,new THREE.MeshStandardMaterial({color:'#e66e52',side:THREE.DoubleSide,roughness:.9}),scarfTail,-.45,0,0);const tail=new THREE.Group();tail.position.set(-.91,1.54,.07);cat.add(tail);curve(tail,M.orange,[[0,0,0],[-.5,.12,.1],[-.91,.46,.16],[-1,.79,.16],[-.78,.9,.13]],.10);ball(tail,M.light,-.78,.9,.13,.105);// Repeatable roadside segments; the vehicle advances relative to every world landmark.const segments=[];function palm(g,x,z,scale=1){const p=new THREE.Group();p.position.set(x,.05,z);p.scale.setScalar(scale);g.add(p);curve(p,M.trunk,[[0,0,0],[.12,1.6,0],[.4,3.3,0],[.62,4.3,0]],.14);for(let k=0;k<7;k++){const a=k*Math.PI*2/7;const pts=[[.62,4.3,0],[.62+Math.cos(a)*.9,4.55,Math.sin(a)*.9],[.62+Math.cos(a)*2,3.8,Math.sin(a)*2]];const path=new THREE.CatmullRomCurve3(pts.map(p=>new THREE.Vector3(...p)));const geo=new THREE.BufferGeometry();const vertices=[],idx=[];for(let n=0;n<=12;n++){const t=n/12,v=path.getPoint(t),w=Math.sin(t*Math.PI)*.29;vertices.push(v.x-Math.sin(a)*w,v.y,v.z+Math.cos(a)*w,v.x+Math.sin(a)*w,v.y,v.z-Math.cos(a)*w);if(n<12){const b=n*2;idx.push(b,b+1,b+2,b+1,b+3,b+2)}}geo.setAttribute('position',new THREE.Float32BufferAttribute(vertices,3));geo.setIndex(idx);geo.computeVertexNormals();mesh(geo,new THREE.MeshStandardMaterial({color:k%2?'#488e72':'#66ac7e',side:THREE.DoubleSide}),p)}for(let k=0;k<3;k++)ball(p,M.trunk,.65+Math.sin(k)*.17,4.13,Math.cos(k)*.17,.15)}for(let i=0;i<9;i++){const g=new THREE.Group();scene.add(g);segments.push(g);g.userData.base=(i-4)*18;for(let j=0;j<3;j++)box(g,M.cream,j*6,.08,3.5,2.7,.015,.10);for(let j=0;j<3;j++){box(g,M.cream,j*6,.44,-.47,.14,.85,.16);box(g,M.red,j*6,.68,-.47,.145,.12,.165)}rod(g,M.cream,[0,.69,-.47],[18,.69,-.47],.045);palm(g,4,-2.1,.85+(i%3)*.13);if(i%2===0)palm(g,12,9,1.15);for(let j=0;j<3;j++){ball(g,mat('#94aa79'),j*5+1,.15,9.1,.8,.3,.5);ball(g,M.cream,j*5+2,.10,-2.5,.45,.16,.3)}if(i%3===1){rod(g,M.steel,[9,0,8.2],[9,3.2,8.2],.06);box(g,M.mint,9,2.75,8.2,1.8,.65,.1);box(g,M.cream,9,2.75,8.27,1.2,.065,.025)}}// Lighthouse on a small rocky spit.const lighthouse=new THREE.Group();scene.add(lighthouse);lighthouse.position.set(-21,0,-24);ball(lighthouse,M.sand,0,-.4,0,7,.9,4);for(let i=0;i<7;i++)ball(lighthouse,mat('#a2aaa0'),Math.cos(i)*5,-.02,Math.sin(i)*2.6,1,.7,.8);mesh(new THREE.CylinderGeometry(.65,1.05,5,24),M.cream,lighthouse,0,2.4,0);for(const y of [1,2.9,4.1])mesh(new THREE.CylinderGeometry(.74+(4-y)*.073,.80+(4-y)*.073,.45,24),M.red,lighthouse,0,y,0);mesh(new THREE.CylinderGeometry(1.05,1.05,.15,24),M.dark,lighthouse,0,5,0);mesh(new THREE.CylinderGeometry(.62,.62,.8,12),mat('#d9ede0',{metalness:.3}),lighthouse,0,5.45,0);mesh(new THREE.ConeGeometry(.98,.65,24),M.red,lighthouse,0,6.1,0);const boat=new THREE.Group();boat.position.set(15,0,-28);scene.add(boat);ball(boat,M.cream,0,0,0,1.6,.38,.55);rod(boat,M.trunk,[0,0,0],[0,3.8,0],.045);const sailGeo=new THREE.BufferGeometry();sailGeo.setAttribute('position',new THREE.Float32BufferAttribute([.1,.5,0,.1,3.65,0,1.5,.5,0],3));sailGeo.computeVertexNormals();mesh(sailGeo,new THREE.MeshStandardMaterial({color:'#fff2d1',side:THREE.DoubleSide}),boat);const birds=[];for(let i=0;i<6;i++){const g=new THREE.Group();scene.add(g);const wings=[];for(const side of [-1,1]){const w=new THREE.Group();g.add(w);rod(w,M.white,[0,0,0],[side*.38,.12,0],.035);rod(w,M.white,[side*.38,.12,0],[side*.65,.04,-.08],.023);wings.push(w)}birds.push({g,wings})}let distance=0,time=0,paused=matchMedia('(prefers-reduced-motion: reduce)').matches,speed=1,view=0,azimuth=0,elevation=0,zoom=1,drag=null;const views=[{p:[8.3,5.1,13.8],look:[0,1.6,1.5],name:'海岸视角'},{p:[1.8,3.8,10],look:[0,1.7,3.45],name:'近景跟拍'},{p:[-9,5.9,11],look:[0,1.4,1],name:'后方追逐'}];const play=document.getElementById('play');function sync(){play.textContent=paused?'▶ 继续':'Ⅱ 暂停';play.setAttribute('aria-label',paused?'继续动画':'暂停动画');play.classList.toggle('active',!paused)}sync();play.onclick=()=>{paused=!paused;sync()};document.getElementById('view').onclick=()=>{view=(view+1)%views.length;azimuth=elevation=0;zoom=1;document.getElementById('view').textContent='◎ '+views[view].name};document.getElementById('speed').oninput=e=>{speed=+e.target.value;document.getElementById('speedValue').textContent=speed.toFixed(1)+'×'};document.getElementById('reset').onclick=()=>{distance=time=0;view=0;zoom=1;azimuth=elevation=0;document.getElementById('view').textContent='◎ 海岸视角'};const canvas=renderer.domElement;canvas.onpointerdown=e=>{drag=[e.clientX,e.clientY];canvas.setPointerCapture(e.pointerId)};canvas.onpointermove=e=>{if(drag){azimuth-=(e.clientX-drag[0])*.006;elevation=THREE.MathUtils.clamp(elevation+(e.clientY-drag[1])*.006,-.3,.75);drag=[e.clientX,e.clientY]}};canvas.onpointerup=canvas.onpointercancel=()=>drag=null;canvas.onwheel=e=>{e.preventDefault();zoom=THREE.MathUtils.clamp(zoom+e.deltaY*.001,.65,1.8)};document.addEventListener('keydown',e=>{if(e.code==='Space'&&!['INPUT','BUTTON'].includes(document.activeElement.tagName)){e.preventDefault();play.click()}});onresize=()=>{camera.aspect=innerWidth/innerHeight;camera.updateProjectionMatrix();renderer.setSize(innerWidth,innerHeight)};let last=performance.now();const baseWater=waterGeo.attributes.position.array.slice();function animate(now){requestAnimationFrame(animate);const dt=Math.min((now-last)/1000,.05);last=now;if(!paused){time+=dt*speed;distance+=dt*speed*3.8}for(const g of segments)g.position.x=THREE.MathUtils.euclideanModulo(g.userData.base-distance+81,162)-81;wheels.forEach(w=>w.rotation.z=-distance/.465);rider.position.y=Math.sin(time*8)*.012;tail.rotation.x=Math.sin(time*3)*.17;head.rotation.y=Math.sin(time*.7)*.035;scarfTail.rotation.y=Math.sin(time*5)*.15;const sa=scarfGeo.attributes.position;for(let i=0;i<sa.count;i++){const x=sa.getX(i);sa.setZ(i,Math.sin(time*9+x*8)*.07*(-x+.5))}sa.needsUpdate=true;scarfGeo.computeVertexNormals();const wp=waterGeo.attributes.position;for(let i=0;i<wp.count;i++){const x=baseWater[i*3],z=baseWater[i*3+2];wp.setY(i,Math.sin(x*.24+time*1.3+z*.5)*.07+Math.cos(z*.33+time)*.045)}wp.needsUpdate=true;for(let i=0;i<foam.length;i++){foam[i].position.x=Math.sin(time*.5+i)*1.6;foam[i].position.y=Math.sin(time+i)*.025}boat.rotation.z=Math.sin(time)*.055;boat.position.y=Math.sin(time*.8)*.09;birds.forEach((b,i)=>{b.g.position.set(Math.sin(time*.1+i)*17,9+Math.sin(time*.3+i),-17-i*3);b.wings.forEach((w,j)=>w.rotation.z=Math.sin(time*4+i)*.25*(j?1:-1))});const v=views[view],look=new THREE.Vector3(...v.look),offset=new THREE.Vector3(...v.p).sub(look);const sph=new THREE.Spherical().setFromVector3(offset);sph.theta+=azimuth;sph.phi=THREE.MathUtils.clamp(sph.phi-elevation,.25,1.48);sph.radius*=zoom*(innerWidth<700?1.25:1);camera.position.copy(look).add(new THREE.Vector3().setFromSpherical(sph));camera.lookAt(look);renderer.render(scene,camera);window.sceneState={distance,time,paused,speed,view,wheelAngle:wheels[0].rotation.z,drawCalls:renderer.info.render.calls,triangles:renderer.info.render.triangles}}requestAnimationFrame(animate);window.coastScene={renderer,scene,camera};} catch(e){const el=document.getElementById('error');el.style.display='block';el.textContent='场景暂时无法启动,请使用支持 WebGL 的 Chrome 或 Edge 浏览器。'+e.message;console.error(e)}</script></body></html>