|
|
<!DOCTYPE html> |
|
|
<html lang="en"> |
|
|
<head> |
|
|
<meta charset="utf-8"/> |
|
|
<meta name="viewport" content="width=device-width,initial-scale=1"/> |
|
|
<title>ZEN SkyLines — Jetstream Rally</title> |
|
|
<style> |
|
|
:root{ |
|
|
--hud:#eaf6ff;--dim:#9fb6c9;--glass:rgba(255,255,255,.06);--glass2:rgba(255,255,255,.12); |
|
|
--ok:#22d39a;--warn:#ffb020;--danger:#ff5577;--acc:#00e5ff; |
|
|
} |
|
|
html,body{margin:0;height:100%;background:#05070b;color:var(--hud);font-family:Inter,system-ui,Segoe UI,Roboto,Arial,sans-serif;overflow:hidden} |
|
|
#hud{position:fixed;inset:0;pointer-events:none} |
|
|
.pill{position:absolute;background:var(--glass);backdrop-filter:blur(8px);border:1px solid rgba(255,255,255,.08);border-radius:999px;padding:8px 12px;font-size:12px;letter-spacing:.3px} |
|
|
#dist{top:12px;left:12px} |
|
|
#score{top:12px;right:12px} |
|
|
#seed{top:44px;left:12px;opacity:.9} |
|
|
#gauges{position:absolute;bottom:16px;left:50%;transform:translateX(-50%);width:min(560px,92vw);display:grid;gap:10px} |
|
|
.bar{height:14px;border-radius:999px;background:rgba(255,255,255,.08);overflow:hidden;border:1px solid rgba(255,255,255,.1)} |
|
|
.fill{height:100%;width:0%} |
|
|
.fill.power{background:linear-gradient(90deg,#ffe08a,#ff9d00)} |
|
|
.fill.stam{background:linear-gradient(90deg,#8bffe1,#00e5ff)} |
|
|
#hint{position:absolute;bottom:64px;right:12px;opacity:.88;font-size:12px;background:var(--glass);border:1px solid rgba(255,255,255,.08);padding:8px 10px;border-radius:10px} |
|
|
#center{position:absolute;inset:0;display:none;place-items:center;text-align:center;pointer-events:none;font-weight:600;letter-spacing:.3px;text-shadow:0 6px 28px rgba(0,0,0,.55)} |
|
|
#center .card{background:var(--glass2);border:1px solid rgba(255,255,255,.12);border-radius:16px;padding:18px 20px;display:inline-grid;gap:8px;min-width:280px;backdrop-filter:blur(10px)} |
|
|
#btnRestart,#btnScreenshot{pointer-events:auto;cursor:pointer;margin-top:8px;border-radius:10px;border:0;padding:10px 14px;background:#0ea5e9;color:#fff;font-weight:700;box-shadow:0 8px 18px rgba(14,165,233,.35)} |
|
|
#btnScreenshot{background:#22d39a;margin-left:8px} |
|
|
#touchL,#touchR{position:fixed;top:0;bottom:0;width:50vw;pointer-events:auto} |
|
|
#touchL{left:0}#touchR{right:0} |
|
|
@media (pointer:fine){#touchL,#touchR{display:none}} |
|
|
.badge{display:inline-block;border:1px solid rgba(255,255,255,.12);border-radius:999px;padding:4px 8px;font-size:11px;background:var(--glass);margin-top:4px} |
|
|
.kicker{font-size:11px;color:var(--dim)} |
|
|
</style> |
|
|
</head> |
|
|
<body> |
|
|
<div id="hud"> |
|
|
<div id="dist" class="pill">Distance: 0 m</div> |
|
|
<div id="score" class="pill">Score: 0 | Combo ×1 | Best 0</div> |
|
|
<div id="seed" class="pill">Seed: <span id="seedVal"></span> <span class="kicker">[R to reroll]</span></div> |
|
|
<div id="gauges"> |
|
|
<div class="bar"><div id="pBar" class="fill power"></div></div> |
|
|
<div class="bar"><div id="sBar" class="fill stam"></div></div> |
|
|
</div> |
|
|
<div id="hint"> |
|
|
Space: charge → release to throw • ←/→ bank • ↑/↓ pitch • <b>Shift</b> Burst • <b>T</b> time bubble view • <b>P</b> photo • <b>ESC</b> restart |
|
|
</div> |
|
|
<div id="center"> |
|
|
<div class="card"> |
|
|
<div id="headline">Run Ended</div> |
|
|
<div id="details" style="font-size:12px;color:var(--dim)"></div> |
|
|
<div> |
|
|
<button id="btnRestart">Restart</button> |
|
|
<button id="btnScreenshot">Photo</button> |
|
|
</div> |
|
|
<div id="badges" style="margin-top:6px"></div> |
|
|
</div> |
|
|
</div> |
|
|
<div id="touchL"></div><div id="touchR"></div> |
|
|
</div> |
|
|
|
|
|
<script src="https://cdn.jsdelivr.net/npm/three@0.152.2/build/three.min.js"></script> |
|
|
<script> |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
let seed = (new URLSearchParams(location.search).get('seed')) || Math.floor(Math.random()*1e9).toString(36); |
|
|
document.getElementById('seedVal').textContent = seed; |
|
|
function sfc32FromSeed(s){ |
|
|
|
|
|
function xmur3(str){for(var i=1779033703^str.length,j=0;j<str.length;j++)i=Math.imul(i^str.charCodeAt(j),3432918353),i=i<<13|i>>>19;return function(){i=Math.imul(i^i>>>16,2246822507);i=Math.imul(i^i>>>13,3266489909);return (i^i>>>16)>>>0}} |
|
|
const x = xmur3(s); let a=x(), b=x(), c=x(), d=x(); |
|
|
return function(){a|=0; b|=0; c|=0; d|=0; var t=(a+b|0)+d|0; d=d+1|0; a=b^b>>>9; b=c+(c<<3)|0; c=(c<<21|c>>>11); c=c+t|0; return (t>>>0)/4294967296;} |
|
|
} |
|
|
const rand = sfc32FromSeed(seed); |
|
|
const R = (min,max)=>min+(max-min)*rand(); |
|
|
|
|
|
|
|
|
const scene = new THREE.Scene(); |
|
|
|
|
|
const skyCanvas=document.createElement('canvas'); skyCanvas.width=1; skyCanvas.height=256; |
|
|
const skyCtx=skyCanvas.getContext('2d'); const grad=skyCtx.createLinearGradient(0,256,0,0); |
|
|
grad.addColorStop(0,'#0b1220'); grad.addColorStop(.5,'#0e1c38'); grad.addColorStop(1,'#091019'); |
|
|
skyCtx.fillStyle=grad; skyCtx.fillRect(0,0,1,256); |
|
|
const skyTex=new THREE.CanvasTexture(skyCanvas); skyTex.center.set(.5,.5); scene.background=skyTex; |
|
|
|
|
|
|
|
|
{ |
|
|
const N=1100, pos=new Float32Array(N*3); |
|
|
for(let i=0;i<N;i++){ const r=700, th=rand()*Math.PI*2, ph=rand()*Math.PI*.55; |
|
|
pos[3*i]=r*Math.sin(ph)*Math.cos(th); |
|
|
pos[3*i+1]=r*Math.cos(ph)+100; |
|
|
pos[3*i+2]=r*Math.sin(ph)*Math.sin(th); |
|
|
} |
|
|
const g=new THREE.BufferGeometry(); g.setAttribute('position',new THREE.BufferAttribute(pos,3)); |
|
|
scene.add(new THREE.Points(g,new THREE.PointsMaterial({size:1,sizeAttenuation:false, color:0xffffff,opacity:.85,transparent:true}))); |
|
|
} |
|
|
|
|
|
const camera = new THREE.PerspectiveCamera(75, innerWidth/innerHeight, 0.1, 3000); |
|
|
const renderer = new THREE.WebGLRenderer({antialias:true}); |
|
|
renderer.setSize(innerWidth, innerHeight); renderer.shadowMap.enabled=true; |
|
|
document.body.appendChild(renderer.domElement); |
|
|
|
|
|
const hemi=new THREE.HemisphereLight(0x6ba7ff,0x0a0a0a,.7); scene.add(hemi); |
|
|
const sun=new THREE.DirectionalLight(0xfff2cc,1.1); sun.position.set(-120,180,-60); sun.castShadow=true; sun.shadow.mapSize.set(1024,1024); scene.add(sun); |
|
|
|
|
|
|
|
|
const groundGeom=new THREE.PlaneGeometry(4000,4000,40,40); |
|
|
const groundMat=new THREE.MeshBasicMaterial({color:0x0d121e, wireframe:true, wireframeLinewidth:1, opacity:.25, transparent:true}); |
|
|
const ground=new THREE.Mesh(groundGeom,groundMat); ground.rotation.x=-Math.PI/2; ground.position.y=-6; scene.add(ground); |
|
|
|
|
|
|
|
|
function flowVector(x,y,z,time){ |
|
|
|
|
|
const s = 0.015, t = time*0.25; |
|
|
const nx = Math.sin(s*z + 1.3*Math.cos(s*y+t)) + 0.6*Math.sin(s*0.7*z+2.1); |
|
|
const ny = 0.5*Math.sin(s*x + 0.8*Math.cos(s*z+t*0.7)); |
|
|
const nz = Math.sin(s*x + 1.1*Math.cos(s*y+t*1.1)) + 0.5*Math.sin(s*0.6*x+1.7); |
|
|
return {x:nx, y:ny, z:1.8 + nz}; |
|
|
} |
|
|
|
|
|
const ribbons=[]; |
|
|
function makeRibbon(pathZ){ |
|
|
const geo=new THREE.CylinderGeometry(0.15,0.15,6,6,1,true); |
|
|
const mat=new THREE.MeshBasicMaterial({color:0x00e5ff, transparent:true, opacity:.3, side:THREE.DoubleSide}); |
|
|
const cyl=new THREE.Mesh(geo,mat); |
|
|
cyl.position.set(R(-8,8), R(-1,3), pathZ); |
|
|
scene.add(cyl); ribbons.push(cyl); |
|
|
} |
|
|
|
|
|
for(let z=20; z<=1600; z+=20){ if(rand()<0.6) makeRibbon(z + R(-4,4)); } |
|
|
|
|
|
|
|
|
const walls=[]; const bubbles=[]; const rings=[]; |
|
|
function makeWall(z){ |
|
|
const w= R(8,14), h= R(6,10); |
|
|
const box=new THREE.Mesh(new THREE.BoxGeometry(w,h,1.4), new THREE.MeshBasicMaterial({color:0xff5577, transparent:true, opacity:.25})); |
|
|
box.position.set(R(-10,10), R(0.5,4.5), z); |
|
|
box.userData = {vx: R(-0.6,0.6), vy: R(-0.2,0.2)}; |
|
|
scene.add(box); walls.push(box); |
|
|
} |
|
|
function makeBubble(z){ |
|
|
const r=R(1.6,2.4); |
|
|
const s=new THREE.Mesh(new THREE.SphereGeometry(r, 20, 16), new THREE.MeshBasicMaterial({color:0x22d39a, transparent:true, opacity:.18})); |
|
|
s.position.set(R(-9,9), R(1.0,5.0), z); |
|
|
s.userData = {r}; scene.add(s); bubbles.push(s); |
|
|
} |
|
|
function makeRing(z){ |
|
|
const t=new THREE.Mesh(new THREE.TorusGeometry(1.6,.12,8,24), |
|
|
new THREE.MeshBasicMaterial({color:0x00e5ff,transparent:true,opacity:.9})); |
|
|
t.position.set(R(-8,8), R(1.2,4.8), z); |
|
|
scene.add(t); rings.push(t); |
|
|
} |
|
|
for(let z=40; z<=1600; ){ |
|
|
if(rand()<0.6) makeRing(z); |
|
|
if(rand()<0.45) makeBubble(z+R(4,10)); |
|
|
if(rand()<0.55) makeWall(z+R(8,14)); |
|
|
z += 26 + Math.floor(rand()*14); |
|
|
} |
|
|
|
|
|
|
|
|
function makeGlider(){ |
|
|
const g=new THREE.Group(); |
|
|
const paper=new THREE.MeshLambertMaterial({color:0xf4f6f8, side:THREE.DoubleSide}); |
|
|
const bodyS=new THREE.Shape(); bodyS.moveTo(0,0); bodyS.lineTo(-.22,.55); bodyS.lineTo(-.42,1.05); bodyS.lineTo(.42,1.05); bodyS.lineTo(.22,.55); bodyS.lineTo(0,0); |
|
|
const body=new THREE.Mesh(new THREE.ExtrudeGeometry(bodyS,{depth:.03,bevelEnabled:false}),paper); |
|
|
const wing=(d)=>{const s=new THREE.Shape(); s.moveTo(0,.2); s.lineTo(d*.32,.85); s.lineTo(d*1.0,.52); s.lineTo(0,.2); return new THREE.Mesh(new THREE.ExtrudeGeometry(s,{depth:.02,bevelEnabled:false}),paper);}; |
|
|
const L=wing(-1), Rg=wing(1); L.position.y=.01; Rg.position.y=.015; L.rotation.x=.18; Rg.rotation.x=.18; |
|
|
const crease=new THREE.Line(new THREE.BufferGeometry().setFromPoints([new THREE.Vector3(0,.03,0),new THREE.Vector3(0,.03,1.05)]), |
|
|
new THREE.LineBasicMaterial({color:0xdedede})); |
|
|
g.add(body,L,Rg,crease); g.rotation.order="ZXY"; g.rotation.x=-Math.PI/2; return g; |
|
|
} |
|
|
const glider=makeGlider(); scene.add(glider); |
|
|
|
|
|
|
|
|
let state='aim'; |
|
|
let power=0, maxPower=16, charging=false; |
|
|
let stam=100, maxStam=100, combo=1, comboTimer=0, comboWindow=2.6; |
|
|
let dist=0, score=0, best= Number(localStorage.getItem('zen_sky_best')||0); |
|
|
const vel=new THREE.Vector3(0,0,0); const forward={cur:0}; |
|
|
const gravity=5.2, drag=.013; |
|
|
let pitch=0, bank=0; |
|
|
let slowMo=1.0; |
|
|
let showBubbles=false; |
|
|
|
|
|
|
|
|
const $=id=>document.getElementById(id); |
|
|
const elDist=$('dist'), elScore=$('score'), elPow=$('pBar'), elStam=$('sBar'), center=$('center'); |
|
|
const headline=$('headline'), details=$('details'), badges=$('badges'); |
|
|
$('btnRestart').onclick=()=>reset(); $('btnScreenshot').onclick=()=>photo(); |
|
|
|
|
|
function setBar(el,v){ el.style.width = `${THREE.MathUtils.clamp(v,0,100)}%`; } |
|
|
|
|
|
|
|
|
let key={L:false,R:false,U:false,D:false,B:false}, tL=false,tR=false; |
|
|
addEventListener('keydown',e=>{ |
|
|
if(e.code==='Space'){ if(state==='aim'){charging=true;e.preventDefault();} else if(state==='end'){reset();}} |
|
|
if(e.code==='ArrowLeft') key.L=true; |
|
|
if(e.code==='ArrowRight') key.R=true; |
|
|
if(e.code==='ArrowUp') key.U=true; |
|
|
if(e.code==='ArrowDown') key.D=true; |
|
|
if(e.code==='ShiftLeft'||e.code==='ShiftRight') key.B=true; |
|
|
if(e.code==='KeyT') showBubbles = !showBubbles; |
|
|
if(e.code==='KeyP') photo(); |
|
|
if(e.code==='Escape') reset(); |
|
|
if(e.code==='KeyR'){ |
|
|
const newSeed=Math.floor(Math.random()*1e9).toString(36); |
|
|
location.search='?seed='+newSeed; |
|
|
} |
|
|
}); |
|
|
addEventListener('keyup',e=>{ |
|
|
if(e.code==='Space' && state==='aim'){ launch(); e.preventDefault(); } |
|
|
if(e.code==='ArrowLeft') key.L=false; |
|
|
if(e.code==='ArrowRight') key.R=false; |
|
|
if(e.code==='ArrowUp') key.U=false; |
|
|
if(e.code==='ArrowDown') key.D=false; |
|
|
if(e.code==='ShiftLeft'||e.code==='ShiftRight') key.B=false; |
|
|
}); |
|
|
$('touchL').addEventListener('touchstart',()=>tL=true,{passive:true}); |
|
|
$('touchL').addEventListener('touchend',()=>tL=false,{passive:true}); |
|
|
$('touchR').addEventListener('touchstart',()=>tR=true,{passive:true}); |
|
|
$('touchR').addEventListener('touchend',()=>tR=false,{passive:true}); |
|
|
addEventListener('mousedown',()=>{ if(state==='aim')charging=true; }); |
|
|
addEventListener('mouseup',()=>{ if(state==='aim'&&charging)launch(); }); |
|
|
|
|
|
function reset(){ |
|
|
state='aim'; power=0; charging=false; stam=maxStam; combo=1; comboTimer=0; dist=0; score=0; forward.cur=0; vel.set(0,0,0); pitch=0; bank=0; slowMo=1.0; |
|
|
glider.position.set(0, 2.5, 0); glider.rotation.set(-Math.PI/2,0,0); |
|
|
camera.position.set(0, glider.position.y+4, glider.position.z-10); |
|
|
center.style.display='none'; badges.innerHTML=''; |
|
|
} |
|
|
function launch(){ |
|
|
forward.cur = THREE.MathUtils.mapLinear(power,0,maxPower,9,28); |
|
|
vel.y = Math.sin(Math.PI/4)*(power*.68); |
|
|
state='fly'; power=0; charging=false; |
|
|
} |
|
|
reset(); |
|
|
|
|
|
|
|
|
const clock=new THREE.Clock(); |
|
|
function photo(){ |
|
|
const was= center.style.display; |
|
|
center.style.display='none'; |
|
|
const d=renderer.domElement.toDataURL('image/png'); |
|
|
const a=document.createElement('a'); a.href=d; a.download=`zen-skylines-${Date.now()}.png`; a.click(); |
|
|
center.style.display=was; |
|
|
} |
|
|
function endRun(reason, earned=[]){ |
|
|
state='end'; |
|
|
if(score>best){ best=score; localStorage.setItem('zen_sky_best', String(Math.floor(best))); } |
|
|
headline.textContent = reason||'Run Ended'; |
|
|
details.textContent = `Distance: ${dist.toFixed(1)} m • Score: ${Math.floor(score)} • Combo ×${combo} • Best ${Math.floor(best)}`; |
|
|
badges.innerHTML = earned.map(t=>`<span class="badge">${t}</span>`).join(' '); |
|
|
center.style.display='grid'; |
|
|
} |
|
|
|
|
|
|
|
|
function animate(){ |
|
|
requestAnimationFrame(animate); |
|
|
const rawDt = Math.min(.033, clock.getDelta()); |
|
|
const dt = rawDt * slowMo; |
|
|
|
|
|
|
|
|
skyTex.rotation = Math.min(1, dist/1600) * Math.PI * .42; skyTex.needsUpdate=true; |
|
|
|
|
|
|
|
|
setBar(elPow, state==='aim' ? (power/maxPower)*100 : 0); |
|
|
setBar(elStam, (stam/maxStam)*100); |
|
|
elDist.textContent = `Distance: ${dist.toFixed(1)} m`; |
|
|
elScore.textContent = `Score: ${Math.floor(score)} | Combo ×${combo} | Best ${Math.floor(best)}`; |
|
|
|
|
|
|
|
|
if(state==='aim' && charging){ power=Math.min(maxPower, power + 26*rawDt); } |
|
|
|
|
|
if(state==='fly'){ |
|
|
|
|
|
const f = flowVector(glider.position.x, glider.position.y, glider.position.z, performance.now()/1000); |
|
|
|
|
|
const steerL = key.L||tL, steerR = key.R||tR; |
|
|
const bankT = (steerL?-1:0)+(steerR?1:0); bank = THREE.MathUtils.lerp(bank, bankT*Math.PI/5, .1); |
|
|
const pitchT = (key.U?.35:0) + (key.D?-.35:0); pitch = THREE.MathUtils.lerp(pitch, pitchT, .12); |
|
|
|
|
|
|
|
|
forward.cur *= (1 - drag); |
|
|
if((key.B||key.D) && stam>0){ forward.cur += 8*dt; vel.y += 5.5*dt; stam -= 18*dt; } else { stam += 4*dt; } |
|
|
stam = THREE.MathUtils.clamp(stam, 0, maxStam); |
|
|
|
|
|
|
|
|
const lift=(Math.sin(pitch)*(forward.cur+6))*0.95 + f.y*0.8; |
|
|
vel.y += (lift - gravity) * dt; |
|
|
|
|
|
|
|
|
const lateral = (steerR?-1:0)+(steerL?1:0); |
|
|
glider.position.x += (lateral*3.6 + f.x*0.9) * dt; |
|
|
|
|
|
|
|
|
glider.position.z += (forward.cur + 10 + f.z*1.3) * dt; |
|
|
|
|
|
|
|
|
glider.position.y += vel.y * dt; |
|
|
|
|
|
if(glider.position.y<0.5){ glider.position.y=0.5; vel.y=Math.max(0,vel.y); } |
|
|
if(glider.position.y>18){ glider.position.y=18; vel.y=Math.min(0,vel.y); } |
|
|
|
|
|
|
|
|
glider.rotation.x = -Math.PI/2 + (-pitch*.6); |
|
|
glider.rotation.z = bank*.8; |
|
|
|
|
|
|
|
|
for(const rb of ribbons){ |
|
|
rb.position.y += Math.sin((performance.now()+rb.position.z*40)/7000)*0.02; |
|
|
rb.position.x += Math.sin((performance.now()+rb.position.z*55)/6000)*0.02; |
|
|
} |
|
|
|
|
|
|
|
|
let earned=[]; |
|
|
for(const w of walls){ |
|
|
w.position.x += w.userData.vx*dt; |
|
|
w.position.y += w.userData.vy*dt; |
|
|
if(w.position.x<-12||w.position.x>12) w.userData.vx*=-1; |
|
|
if(w.position.y<0.5||w.position.y>8) w.userData.vy*=-1; |
|
|
|
|
|
|
|
|
const dx=Math.abs(glider.position.x - w.position.x); |
|
|
const dy=Math.abs(glider.position.y - w.position.y); |
|
|
const dz=Math.abs(glider.position.z - w.position.z); |
|
|
const hx=w.geometry.parameters.width/2, hy=w.geometry.parameters.height/2, hz=0.7; |
|
|
if(dx<hx && dy<hy && dz<hz){ |
|
|
return endRun('Storm wall impact', ['Risk-taker']); |
|
|
} |
|
|
|
|
|
if(dz<hz*1.25){ |
|
|
const gx = Math.abs(dx-hx), gy=Math.abs(dy-hy); |
|
|
if((gx<1.0||gy<1.0) && rand()<0.02){ score += 6*combo; earned.push('Graze +6'); } |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
for(let i=rings.length-1;i>=0;i--){ |
|
|
const t=rings[i]; const d=glider.position.distanceTo(t.position); |
|
|
if(d<1.55){ |
|
|
scene.remove(t); rings.splice(i,1); |
|
|
stam = Math.min(maxStam, stam+34); combo = Math.min(12, combo+1); comboTimer = comboWindow; score += 160*combo; |
|
|
earned.push('Ring +160×'+combo); |
|
|
} else { |
|
|
t.rotation.x += .6*dt; t.rotation.z += .4*dt; const s=1+.08*Math.sin(performance.now()/260+i); t.scale.set(s,s,s); |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
let inBubble=false; |
|
|
for(const b of bubbles){ |
|
|
const d=glider.position.distanceTo(b.position); |
|
|
if(d < b.userData.r){ inBubble=true; score += 4*combo*dt; } |
|
|
} |
|
|
slowMo = THREE.MathUtils.lerp(slowMo, inBubble ? 0.6 : 1.0, 0.15); |
|
|
|
|
|
|
|
|
comboTimer -= dt; if(comboTimer<=0 && combo>1){ combo=Math.max(1, combo-1); comboTimer=.35; } |
|
|
|
|
|
|
|
|
const dz=(forward.cur + 10 + f.z*1.3) * dt; |
|
|
dist += dz; score += dz*(.9*combo); |
|
|
|
|
|
|
|
|
if(Math.abs(glider.position.x)>14) return endRun('You left the jetstream corridor', []); |
|
|
if(glider.position.z>1650) return endRun('Finish line reached!', ['Course Clear']); |
|
|
} |
|
|
|
|
|
|
|
|
const camTarget=new THREE.Vector3(glider.position.x, glider.position.y+3.4, glider.position.z-10.5); |
|
|
camera.position.lerp(camTarget,.12); camera.lookAt(glider.position); |
|
|
|
|
|
|
|
|
for(const b of bubbles){ b.material.opacity = showBubbles? .28 : .18; } |
|
|
|
|
|
renderer.render(scene,camera); |
|
|
} |
|
|
animate(); |
|
|
|
|
|
|
|
|
addEventListener('resize',()=>{ camera.aspect=innerWidth/innerHeight; camera.updateProjectionMatrix(); renderer.setSize(innerWidth,innerHeight); }); |
|
|
</script> |
|
|
</body> |
|
|
</html> |
|
|
|