<!doctype html>
<html lang="de">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1,maximum-scale=1,user-scalable=no,viewport-fit=cover">
<meta name="theme-color" content="#101d31">
<title>Web Swing Mobile</title>
<style>
*{box-sizing:border-box;-webkit-tap-highlight-color:transparent}
html,body{margin:0;width:100%;height:100%;overflow:hidden;background:#9bc4e8;font-family:system-ui,Arial;touch-action:none;user-select:none}
canvas{display:block;width:100%;height:100%;touch-action:none}
#hud{position:fixed;left:10px;top:calc(env(safe-area-inset-top) + 10px);z-index:3;padding:10px 12px;border-radius:13px;background:#071321c9;border:1px solid #ffffff30;color:#fff;pointer-events:none}
#hud b{font-size:15px;letter-spacing:.5px}#status{font-size:12px;color:#ffd166;margin-top:4px}
#help{position:fixed;top:calc(env(safe-area-inset-top) + 10px);right:10px;background:#071321b8;color:#e6eef7;font-size:11px;padding:8px 10px;border-radius:10px;max-width:150px;pointer-events:none;text-align:right}
#controls{position:fixed;inset:0;z-index:4;pointer-events:none}
#stickArea{position:absolute;left:15px;bottom:calc(env(safe-area-inset-bottom) + 20px);width:140px;height:140px;border-radius:50%;background:#09182744;border:2px solid #ffffff5c;pointer-events:auto;touch-action:none}
#stick{position:absolute;left:42px;top:42px;width:52px;height:52px;border-radius:50%;background:#ffffffa8;border:2px solid #fff;box-shadow:0 3px 14px #0004;pointer-events:none}
.action{position:absolute;right:18px;border-radius:50%;display:grid;place-items:center;color:#fff;font-weight:900;text-shadow:0 1px 3px #0007;border:2px solid #ffffffb0;box-shadow:0 5px 16px #0004;pointer-events:auto;touch-action:none}
#web{width:78px;height:78px;bottom:calc(env(safe-area-inset-bottom) + 30px);background:#087ebdce;font-size:14px}
#jump{width:62px;height:62px;bottom:calc(env(safe-area-inset-bottom) + 120px);right:34px;background:#d4474cdd;font-size:12px}
#release{width:54px;height:54px;bottom:calc(env(safe-area-inset-bottom) + 122px);right:108px;background:#1c2738d9;font-size:11px}
#lookHint{position:absolute;right:12px;top:45%;color:#fff9;background:#07132150;border-radius:8px;padding:6px 8px;font-size:10px;pointer-events:none}
#overlay{position:fixed;inset:0;z-index:10;display:grid;place-items:center;background:linear-gradient(#06101c99,#06101ccc);padding:22px}
#panel{width:min(500px,100%);text-align:center;background:#0b1b2bef;border:1px solid #ffffff30;border-radius:22px;padding:25px 22px;color:white;box-shadow:0 18px 70px #0008}
#panel h1{font-size:clamp(28px,8vw,42px);line-height:1.05;margin:0 0 10px}
#panel p{font-size:14px;line-height:1.55;color:#d4e0ed}
button{border:0;border-radius:999px;background:#ffcc4d;color:#152235;font-weight:900;padding:14px 24px;font-size:16px;cursor:pointer;touch-action:manipulation}
#pause{position:fixed;right:10px;top:calc(env(safe-area-inset-top) + 62px);z-index:5;border:1px solid #ffffff5c;background:#071321b8;color:#fff;border-radius:12px;padding:8px 10px;font-size:12px}
@media(min-width:800px){#stickArea{width:160px;height:160px}#stick{left:52px;top:52px}#help{max-width:240px;font-size:12px}}
</style>
</head>
<body>
<div id="hud"><b>🕸 WEB SWING MOBILE</b><div id="status">Bereit zum Schwingen</div></div>
<div id="help">Linker Daumen: bewegen<br>Rechte Bildschirmseite: Kamera<br>NETZ halten: schwingen</div>
<button id="pause">⏸ Pause</button>
<div id="controls">
<div id="stickArea"><div id="stick"></div></div>
<div id="lookHint">KAMERA</div>
<div class="action" id="web">🕸 NETZ</div>
<div class="action" id="jump">SPRUNG</div>
<div class="action" id="release">LÖSEN</div>
</div>
<div id="overlay"><div id="panel"><h1>WEB SWING<br>MOBILE</h1><p>Schwinge durch eine größere 3D-Stadt mit vielen Hochhäusern und fahrenden Autos. Für Android-Handys optimiert – mit Touch-Steuerung, virtuellem Joystick und Kamera per Fingerwisch.</p><p><b>Steuerung</b><br>Linken Kreis bewegen · rechte Seite wischen zum Umsehen · NETZ gedrückt halten zum Schwingen · SPRUNG zum Abspringen · LÖSEN zum Freigeben</p><button id="start">SPIEL STARTEN</button><p style="font-size:11px">Zum Laden der 3D-Engine wird Internet benötigt. Dies ist ein eigenständiger Fan-Prototyp.</p></div></div>
<script type="module">
import * as THREE from 'https://cdn.jsdelivr.net/npm/three@0.160.0/build/three.module.js';
const scene=new THREE.Scene();scene.background=new THREE.Color(0x9bc4e8);scene.fog=new THREE.Fog(0x9bc4e8,100,520);
const camera=new THREE.PerspectiveCamera(72,innerWidth/innerHeight,.1,1000);
const renderer=new THREE.WebGLRenderer({antialias:true,powerPreference:'high-performance'});
renderer.setSize(innerWidth,innerHeight);renderer.setPixelRatio(Math.min(devicePixelRatio,1.6));renderer.shadowMap.enabled=false;renderer.toneMapping=THREE.ACESFilmicToneMapping;renderer.toneMappingExposure=1.05;document.body.prepend(renderer.domElement);
scene.add(new THREE.HemisphereLight(0xdceeff,0x43515e,2.1));
const sun=new THREE.DirectionalLight(0xffe3bb,2.4);sun.position.set(-80,140,70);scene.add(sun);
const mat=(c,r=.8,m=.05)=>new THREE.MeshStandardMaterial({color:c,roughness:r,metalness:m});
const ground=new THREE.Mesh(new THREE.PlaneGeometry(1200,1200),mat(0x56616b));ground.rotation.x=-Math.PI/2;ground.position.y=-.5;scene.add(ground);
const roadMat=mat(0x252d35),windowMat=new THREE.MeshStandardMaterial({color:0x8cc7e8,roughness:.3,metalness:.15,emissive:0x172c3c});
const buildings=[];const palette=[0x788795,0x9a9d9d,0x687987,0xb1a99c,0x596a7b,0x8a969e,0x777f8b];
function building(x,z,w,d,h,idx){
const g=new THREE.Group();g.position.set(x,0,z);scene.add(g);
const body=new THREE.Mesh(new THREE.BoxGeometry(w,h,d),mat(palette[idx%palette.length],.7,.1));body.position.y=h/2-.45;g.add(body);
const roof=new THREE.Mesh(new THREE.BoxGeometry(w*.88,1.2,d*.88),mat(0x444e58));roof.position.y=h+.15;g.add(roof);
const cols=Math.max(2,Math.floor(w/3.8)),rows=Math.max(2,Math.floor(h/5));
const wg=new THREE.BoxGeometry(Math.min(1.35,w/cols*.5),1.55,.08);
for(let r=0;r<rows;r++)for(let c=0;c<cols;c++){let win=new THREE.Mesh(wg,windowMat);win.position.set(-w/2+(c+.5)*w/cols,2.4+r*(h-4)/rows,d/2+.045);g.add(win);}
const sc=Math.max(2,Math.floor(d/4)),sg=new THREE.BoxGeometry(.08,1.55,Math.min(1.35,d/sc*.5));
for(let r=0;r<rows;r++)for(let c=0;c<sc;c++){let win=new THREE.Mesh(sg,windowMat);win.position.set(w/2+.045,2.4+r*(h-4)/rows,-d/2+(c+.5)*d/sc);g.add(win);}
buildings.push({x,z,w,d,h,group:g});
}
for(let gx=-6;gx<=6;gx++)for(let gz=-6;gz<=6;gz++){const x=gx*43,z=gz*43;if(Math.abs(x)<28&&Math.abs(z)<28)continue;building(x,z,19+Math.abs(gx*7+gz*3)%13,19+Math.abs(gx*5-gz*9)%13,24+Math.abs(gx*31+gz*47)%118,Math.abs(gx*13+gz*19));}
for(let i=-6;i<=6;i++){let a=new THREE.Mesh(new THREE.PlaneGeometry(9,700),roadMat);a.rotation.x=-Math.PI/2;a.position.set(i*43,-.42,0);scene.add(a);let b=new THREE.Mesh(new THREE.PlaneGeometry(700,9),roadMat);b.rotation.x=-Math.PI/2;b.position.set(0,-.41,i*43);scene.add(b);}
// Traffic: simple colorful cars driving along the city streets in loops.
const trafficCars=[];
const carColors=[0xe34a45,0x2e7bcf,0xf0c94b,0xe7e9ed,0x303a49,0x43a879,0xd77a36];
const carBodyMats=carColors.map(c=>mat(c,.48,.12));
const tireMat=mat(0x171b20,.95), glassMat=new THREE.MeshStandardMaterial({color:0x8bc4dc,roughness:.2,metalness:.2});
function makeCar(i){
const g=new THREE.Group();
const body=new THREE.Mesh(new THREE.BoxGeometry(3.4,1.05,1.65),carBodyMats[i%carBodyMats.length]);body.position.y=.78;g.add(body);
const cabin=new THREE.Mesh(new THREE.BoxGeometry(1.65,.72,1.35),glassMat);cabin.position.set(-.15,1.55,0);g.add(cabin);
const tireGeo=new THREE.CylinderGeometry(.34,.34,.22,10);
for(const x of [-1.05,1.05])for(const z of [-.9,.9]){const t=new THREE.Mesh(tireGeo,tireMat);t.rotation.z=Math.PI/2;t.position.set(x,.37,z);g.add(t);}
const lightsMat=new THREE.MeshStandardMaterial({color:0xfff0c2,emissive:0x8b5a22});
for(const z of [-.48,.48]){const l=new THREE.Mesh(new THREE.BoxGeometry(.08,.18,.25),lightsMat);l.position.set(1.72,.83,z);g.add(l);}
scene.add(g);
const laneIndex=(i%9)-4, alongZ=(i%2===0), dir=(i%4<2?1:-1);
const lane=laneIndex*43+(dir>0?2.2:-2.2);
trafficCars.push({g,alongZ,dir,lane,pos:-300-(i%7)*60,speed:9+(i%5)*2.8});
}
for(let i=0;i<28;i++)makeCar(i);
const hero=new THREE.Group();scene.add(hero);
const blue=mat(0x123d85,.42,.15),red=mat(0xd62f3e,.4,.1),white=mat(0xf4f8ff,.25,.1);
function capsule(rad,len,m,x,y,z){let o=new THREE.Mesh(new THREE.CapsuleGeometry(rad,len,4,8),m);o.position.set(x,y,z);hero.add(o);return o;}
capsule(.42,.9,blue,0,1.45,0);
let head=new THREE.Mesh(new THREE.SphereGeometry(.36,16,12),red);head.position.set(0,2.35,0);head.scale.set(.88,1.05,.9);hero.add(head);
for(let x of [-.14,.14]){let eye=new THREE.Mesh(new THREE.SphereGeometry(.09,10,8),white);eye.position.set(x,2.38,.3);eye.scale.set(.8,1.35,.4);hero.add(eye);}
capsule(.16,.58,red,-.52,1.48,0);capsule(.16,.58,red,.52,1.48,0);capsule(.19,.68,blue,-.23,.58,0);capsule(.19,.68,blue,.23,.58,0);
let belt=new THREE.Mesh(new THREE.BoxGeometry(.72,.12,.48),red);belt.position.y=1;hero.add(belt);hero.position.set(0,2,0);
let yaw=0,pitch=-.08;const velocity=new THREE.Vector3(),forward=new THREE.Vector3(),right=new THREE.Vector3();
const raycaster=new THREE.Raycaster(),webMat=new THREE.LineBasicMaterial({color:0xffffff});
let webLine=null,anchor=null,swinging=false,ropeLength=0,paused=false,started=false;
const status=document.querySelector('#status');function say(s){status.textContent=s;}
function findAnchor(){
camera.updateMatrixWorld();raycaster.setFromCamera(new THREE.Vector2(0,0),camera);
const hits=raycaster.intersectObjects(buildings.map(b=>b.group),true);
for(const h of hits)if(h.distance<120&&h.point.y>5)return h.point.clone();
const origin=hero.position.clone(),dir=new THREE.Vector3(Math.sin(yaw),.55,-Math.cos(yaw)).normalize();let best=null,score=1e9;
for(const b of buildings){let p=new THREE.Vector3(b.x,b.h+2,b.z),delta=p.clone().sub(origin),dist=delta.length();if(dist>105)continue;let dot=delta.normalize().dot(dir);if(dot>.68&&dist-dot*30<score){best=p;score=dist-dot*30;}}
return best;
}
function attach(){let a=findAnchor();if(!a){say('Ziele auf ein Hochhaus');return;}anchor=a;swinging=true;ropeLength=Math.max(9,hero.position.distanceTo(a)*.88);if(webLine)scene.remove(webLine);webLine=new THREE.Line(new THREE.BufferGeometry().setFromPoints([hero.position.clone().add(new THREE.Vector3(0,1.8,0)),anchor]),webMat);scene.add(webLine);if(velocity.y<5)velocity.y+=3.5;say('Netz eingehakt! Schwinge!');}
function release(){swinging=false;anchor=null;if(webLine){scene.remove(webLine);webLine=null;}say('Netz gelöst!');}
const stickArea=document.querySelector('#stickArea'),stick=document.querySelector('#stick');
let stickId=null,stickX=0,stickY=0,moveX=0,moveY=0,lookId=null,lastLookX=0,lastLookY=0,webHeld=false;
function updateStick(e){let r=stickArea.getBoundingClientRect(),dx=e.clientX-(r.left+r.width/2),dy=e.clientY-(r.top+r.height/2),max=45,len=Math.hypot(dx,dy);if(len>max){dx=dx/len*max;dy=dy/len*max;}stick.style.transform=`translate(${dx}px,${dy}px)`;moveX=dx/max;moveY=dy/max;}
stickArea.addEventListener('pointerdown',e=>{e.preventDefault();stickId=e.pointerId;stickArea.setPointerCapture(stickId);updateStick(e);});
stickArea.addEventListener('pointermove',e=>{if(e.pointerId===stickId)updateStick(e);});
function resetStick(e){if(e.pointerId===stickId){stickId=null;moveX=moveY=0;stick.style.transform='translate(0,0)';}}
stickArea.addEventListener('pointerup',resetStick);stickArea.addEventListener('pointercancel',resetStick);
renderer.domElement.addEventListener('pointerdown',e=>{if(!started||paused)return;if(e.clientX>innerWidth*.38&&e.clientY>innerHeight*.18){lookId=e.pointerId;lastLookX=e.clientX;lastLookY=e.clientY;renderer.domElement.setPointerCapture(e.pointerId);}});
renderer.domElement.addEventListener('pointermove',e=>{if(e.pointerId!==lookId)return;let dx=e.clientX-lastLookX,dy=e.clientY-lastLookY;lastLookX=e.clientX;lastLookY=e.clientY;yaw-=dx*.006;pitch-=dy*.004;pitch=Math.max(-.8,Math.min(.5,pitch));});
function stopLook(e){if(e.pointerId===lookId)lookId=null;}renderer.domElement.addEventListener('pointerup',stopLook);renderer.domElement.addEventListener('pointercancel',stopLook);
const webBtn=document.querySelector('#web');
webBtn.addEventListener('pointerdown',e=>{e.preventDefault();webBtn.setPointerCapture(e.pointerId);webHeld=true;if(!swinging)attach();});
function webUp(){webHeld=false;if(swinging)release();}webBtn.addEventListener('pointerup',webUp);webBtn.addEventListener('pointercancel',webUp);webBtn.addEventListener('lostpointercapture',webUp);
document.querySelector('#release').addEventListener('pointerdown',e=>{e.preventDefault();release();});
document.querySelector('#jump').addEventListener('pointerdown',e=>{e.preventDefault();velocity.y=Math.max(velocity.y,10);if(hero.position.y<3)hero.position.y=3;say('Sprung!');});
document.querySelector('#pause').addEventListener('click',()=>{paused=!paused;document.querySelector('#pause').textContent=paused?'▶ Weiter':'⏸ Pause';say(paused?'Pausiert':'Weiter geht’s!');});
document.querySelector('#start').addEventListener('click',()=>{started=true;document.querySelector('#overlay').style.display='none';say('Los geht’s – halte NETZ gedrückt!');});
window.addEventListener('resize',()=>{camera.aspect=innerWidth/innerHeight;camera.updateProjectionMatrix();renderer.setSize(innerWidth,innerHeight);renderer.setPixelRatio(Math.min(devicePixelRatio,1.6));});
let last=performance.now();
function animate(now){requestAnimationFrame(animate);let dt=Math.min((now-last)/1000,.032);last=now;if(!paused){
// Kamera-relative Steuerung: Joystick nach oben = gerade in Blickrichtung laufen.
forward.set(Math.sin(yaw),0,-Math.cos(yaw)).normalize();
right.set(Math.cos(yaw),0,Math.sin(yaw)).normalize();
const rawForward=-moveY, rawSide=moveX;
const inputLength=Math.hypot(rawSide,rawForward);
const wish=new THREE.Vector3();
if(inputLength>.06){
const scale=inputLength>1?1:inputLength;
wish.addScaledVector(right,rawSide/inputLength*scale);
wish.addScaledVector(forward,rawForward/inputLength*scale);
}
const speed=rawForward>.1?21:15;
velocity.x+=wish.x*speed*dt;
velocity.z+=wish.z*speed*dt;
// Weniger Rutschen am Boden, Schwung bleibt beim Schwingen erhalten.
const drag=swinging?.995:(inputLength>.06?.94:.78);
velocity.x*=Math.pow(drag,dt*60);
velocity.z*=Math.pow(drag,dt*60);
velocity.y-=24*dt;
if(swinging&&anchor){let off=hero.position.clone().sub(anchor),dist=off.length();if(dist>ropeLength){off.normalize();hero.position.copy(anchor.clone().add(off.multiplyScalar(ropeLength)));velocity.sub(off.clone().multiplyScalar(velocity.dot(off)));}velocity.x+=wish.x*10*dt;velocity.z+=wish.z*10*dt;}
hero.position.addScaledVector(velocity,dt);if(hero.position.y<1.7){hero.position.y=1.7;velocity.y=Math.max(velocity.y,0);velocity.x*=.86;velocity.z*=.86;if(swinging)release();}
hero.position.x=Math.max(-315,Math.min(315,hero.position.x));hero.position.z=Math.max(-315,Math.min(315,hero.position.z));
if(Math.hypot(velocity.x,velocity.z)>2)hero.rotation.y=Math.atan2(velocity.x,velocity.z);
const dir=new THREE.Vector3(Math.sin(yaw)*Math.cos(pitch),Math.sin(pitch),-Math.cos(yaw)*Math.cos(pitch));
const desired=hero.position.clone().add(new THREE.Vector3(0,2.8,0)).addScaledVector(dir,-9.5);camera.position.lerp(desired,1-Math.pow(.001,dt));camera.lookAt(hero.position.clone().add(new THREE.Vector3(0,1.5,0)).addScaledVector(dir,10));
if(webLine&&anchor)webLine.geometry.setFromPoints([hero.position.clone().add(new THREE.Vector3(0,1.8,0)),anchor]);
if(swinging)say('Schwingen · Höhe '+Math.round(hero.position.y)+' m');
}
for(const c of trafficCars){
c.pos+=c.dir*c.speed*dt;
if(c.pos>330)c.pos=-330;if(c.pos< -330)c.pos=330;
if(c.alongZ){c.g.position.set(c.lane,.0,c.pos);c.g.rotation.y=c.dir>0?0:Math.PI;}
else{c.g.position.set(c.pos,.0,c.lane);c.g.rotation.y=c.dir>0?Math.PI/2:-Math.PI/2;}
}
renderer.render(scene,camera);}
requestAnimationFrame(animate);
</script>
</body></html>