<!DOCTYPE html>
<html lang="ru">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width,initial-scale=1,maximum-scale=1,user-scalable=no">
<title>Racing Zone 3D</title>
<style>
*{box-sizing:border-box}
body{margin:0;overflow:hidden;background:#87c9ff;font-family:Arial;color:white;touch-action:none}
canvas{display:block;touch-action:none}
#hud{position:fixed;inset:0;pointer-events:none}
.panel{background:#101a2ddd;border:1px solid #ffffff30;border-radius:14px;padding:12px;backdrop-filter:blur(8px)}
#top{position:absolute;top:10px;left:10px;right:10px;display:flex;justify-content:space-between;gap:8px;align-items:start}
#logo{font-weight:900;font-size:20px;color:#65e0ff}
#stats{text-align:right;font-size:14px;line-height:1.6}
#message{position:absolute;top:26%;left:50%;transform:translateX(-50%);text-align:center;font-size:25px;font-weight:bold;text-shadow:0 3px 8px #000;white-space:pre-line}
#controls{position:absolute;bottom:18px;left:12px;right:12px;display:flex;justify-content:space-between;align-items:end}
.group{display:flex;gap:10px;pointer-events:auto}
.control{width:66px;height:66px;border-radius:18px;border:1px solid #ffffff70;background:#152943dd;color:white;font-size:25px;font-weight:bold;touch-action:none}
.control:active{background:#168ce0}
#gas{width:78px;height:78px;background:#00a85a;font-size:16px}
#brake{background:#d73745;font-size:14px}
#start{position:absolute;left:50%;top:55%;transform:translate(-50%,-50%);pointer-events:auto;border:0;border-radius:15px;background:#00b9ff;color:#071323;padding:16px 30px;font-weight:bold;font-size:20px;box-shadow:0 6px 0 #0877ad}
#restart{pointer-events:auto;border:0;border-radius:9px;background:#ffffff20;color:white;padding:7px 10px}
#help{position:absolute;left:10px;bottom:112px;font-size:12px;color:#fff;text-shadow:0 2px 5px #000}
@media(min-width:800px){.control{width:74px;height:74px}#logo{font-size:25px}}
</style>
</head>
<body>
<div id="hud">
<div id="top">
<div class="panel">
<div id="logo">🏁 RACING ZONE</div>
<div style="font-size:12px">3D ARCADE RACING</div>
</div>
<div class="panel" id="stats">
<div>🏆 Место: <b id="place">—</b></div>
<div>🔄 Круг: <b id="lap">0 / 3</b></div>
<div>⚡ Скорость: <b id="speed">0</b> км/ч</div>
<div>⏱️ Время: <b id="time">0.0</b> с</div>
<button id="restart">↻ Перезапуск</button>
</div>
</div>
<div id="message">RACING ZONE</div>
<button id="start">▶ НАЧАТЬ ГОНКУ</button>
<div id="help">👆 Вращай камеру пальцем · ◀ ▶ Поворот · 🟢 Газ</div>
<div id="controls">
<div class="group">
<button class="control" data-key="left">◀</button>
<button class="control" data-key="right">▶</button>
</div>
<div class="group">
<button class="control" id="brake" data-key="brake">ТОРМОЗ</button>
<button class="control" id="gas" data-key="gas">ГАЗ 🚀</button>
</div>
</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(0x91d0ff);
scene.fog=new THREE.Fog(0x91d0ff,100,270);
const camera=new THREE.PerspectiveCamera(65,innerWidth/innerHeight,.1,500);
const renderer=new THREE.WebGLRenderer({antialias:true});
renderer.setSize(innerWidth,innerHeight);
renderer.setPixelRatio(Math.min(devicePixelRatio,1.7));
renderer.shadowMap.enabled=true;
renderer.outputColorSpace=THREE.SRGBColorSpace;
renderer.domElement.style.position='fixed';
renderer.domElement.style.inset='0';
renderer.domElement.style.zIndex='0';
document.body.prepend(renderer.domElement);
scene.add(new THREE.HemisphereLight(0xe6f6ff,0x526344,2.2));
const sun=new THREE.DirectionalLight(0xffffff,2.5);
sun.position.set(-35,60,25);
sun.castShadow=true;
scene.add(sun);
const mat=(color,roughness=.8)=>new THREE.MeshStandardMaterial({color,roughness});
const grass=new THREE.Mesh(new THREE.PlaneGeometry(600,600),mat(0x4b9b4c));
grass.rotation.x=-Math.PI/2;
grass.position.y=-.16;
scene.add(grass);
const RX=58,RZ=37,ROAD=12,TAU=Math.PI*2;
function trackPoint(t,lane=0){
return new THREE.Vector3(
(RX+lane)*Math.cos(t),0,
(RZ+lane)*Math.sin(t)
);
}
function tube(points,radius,material){
const curve=new THREE.CatmullRomCurve3(points,true);
const mesh=new THREE.Mesh(
new THREE.TubeGeometry(curve,radius===.15?500:350,radius,8,true),
material
);
mesh.position.y=.02;
scene.add(mesh);
}
function makeRoad(){
const segments=360,verts=[],indices=[],uv=[];
for(let i=0;i<=segments;i++){
const t=i/segments*TAU;
for(const side of [-ROAD/2,ROAD/2]){
const p=trackPoint(t,side);
verts.push(p.x,0,p.z);
uv.push(i/segments,side/ROAD+.5);
}
}
for(let i=0;i<segments;i++){
const a=i*2,b=a+1,c=a+2,d=a+3;
indices.push(a,b,c,b,d,c);
}
const geo=new THREE.BufferGeometry();
geo.setAttribute('position',new THREE.Float32BufferAttribute(verts,3));
geo.setAttribute('uv',new THREE.Float32BufferAttribute(uv,2));
geo.setIndex(indices);
geo.computeVertexNormals();
const road=new THREE.Mesh(geo,mat(0x343a45));
road.position.y=-.03;
road.receiveShadow=true;
scene.add(road);
for(let i=0;i<120;i++){
const t=i/120*TAU,p=trackPoint(t);
const dash=new THREE.Mesh(
new THREE.BoxGeometry(.35,.025,2.1),mat(0xf7f7e8)
);
dash.position.set(p.x,.015,p.z);
dash.rotation.y=-t;
scene.add(dash);
}
for(const lane of [-ROAD/2-.7,ROAD/2+.7]){
const points=[];
for(let i=0;i<180;i++)
points.push(trackPoint(i/180*TAU,lane).setY(.1));
tube(points,.15,mat(0xe5e8ed));
}
for(let i=0;i<144;i++){
const t=i/144*TAU;
for(const side of [-1,1]){
const p=trackPoint(t,side*(ROAD/2+.3));
const curb=new THREE.Mesh(
new THREE.BoxGeometry(1.4,.13,.65),
mat(i%2?0xffffff:0xff4242)
);
curb.position.set(p.x,.03,p.z);
curb.rotation.y=-t;
scene.add(curb);
}
}
}
makeRoad();
function tree(x,z,s=1){
const g=new THREE.Group();
const trunk=new THREE.Mesh(
new THREE.CylinderGeometry(.25*s,.4*s,2*s,8),mat(0x79502d)
);
trunk.position.y=s;
g.add(trunk);
const crown=new THREE.Mesh(
new THREE.ConeGeometry(1.5*s,3*s,9),mat(0x217d3c)
);
crown.position.y=3*s;
g.add(crown);
g.position.set(x,0,z);
scene.add(g);
}
for(let i=0;i<100;i++){
const t=i*2.399,r=75+(i%5)*7;
tree(Math.cos(t)*r,Math.sin(t)*r,.7+(i%4)*.17);
}
function createCar(color){
const car=new THREE.Group();
const body=new THREE.Mesh(
new THREE.BoxGeometry(2.05,.62,3.6),mat(color,.35)
);
body.position.y=.75;
body.castShadow=true;
car.add(body);
const cabin=new THREE.Mesh(
new THREE.BoxGeometry(1.45,.65,1.65),mat(0x9ce7ff,.25)
);
cabin.position.set(0,1.28,-.2);
car.add(cabin);
const windshield=new THREE.Mesh(
new THREE.BoxGeometry(1.28,.42,.07),mat(0x253d50,.2)
);
windshield.position.set(0,1.35,-1.02);
car.add(windshield);
for(const x of [-1,1])for(const z of [-1.15,1.15]){
const wheel=new THREE.Mesh(
new THREE.CylinderGeometry(.43,.43,.28,16),mat(0x17191e)
);
wheel.rotation.z=Math.PI/2;
wheel.position.set(x*1.03,.43,z);
car.add(wheel);
}
const spoiler=new THREE.Mesh(
new THREE.BoxGeometry(1.65,.12,.28),mat(0x17191e)
);
spoiler.position.set(0,1.15,1.55);
car.add(spoiler);
scene.add(car);
return car;
}
const player=createCar(0x168dff);
const botColors=[0xff3b45,0xffcc22,0xb04dff,0x24d7a2,0xff8735];
const bots=botColors.map((c,i)=>({
car:createCar(c),
t:.12+i*.12,
speed:.40+Math.random()*.13,
lane:(i%3-1)*2.5,
lap:0
}));
let started=false,finished=false,elapsed=0,lap=0;
let playerT=0,playerSpeed=0,steering=0;
const keys={left:false,right:false,gas:false,brake:false};
const TOTAL_LAPS=3;
// Свободная камера
let cameraYaw=0;
let cameraPitch=.34;
let cameraDistance=12;
let dragging=false,lastX=0,lastY=0;
renderer.domElement.addEventListener('pointerdown',e=>{
if(e.pointerType==='mouse'&&e.button!==0)return;
dragging=true;
lastX=e.clientX;
lastY=e.clientY;
renderer.domElement.setPointerCapture(e.pointerId);
});
renderer.domElement.addEventListener('pointermove',e=>{
if(!dragging)return;
const dx=e.clientX-lastX;
const dy=e.clientY-lastY;
lastX=e.clientX;
lastY=e.clientY;
cameraYaw-=dx*.008;
cameraPitch=THREE.MathUtils.clamp(cameraPitch-dy*.006,-.12,1.05);
});
function stopDrag(){dragging=false}
renderer.domElement.addEventListener('pointerup',stopDrag);
renderer.domElement.addEventListener('pointercancel',stopDrag);
renderer.domElement.addEventListener('wheel',e=>{
cameraDistance=THREE.MathUtils.clamp(cameraDistance+e.deltaY*.015,6,22);
},{passive:true});
function placeCar(car,t,lane=0){
const p=trackPoint(t,lane);
car.position.set(p.x,.03,p.z);
// Машина направлена по движению трассы
car.rotation.y=Math.PI-t;
}
function updatePlayer(dt){
if(keys.gas)playerSpeed+=.2*dt;
else playerSpeed-=.09*dt;
if(keys.brake)playerSpeed-=.42*dt;
playerSpeed=THREE.MathUtils.clamp(playerSpeed,0,.47);
if(keys.left)steering=THREE.MathUtils.damp(steering,-1,.09,dt*60);
else if(keys.right)steering=THREE.MathUtils.damp(steering,1,.09,dt*60);
else steering=THREE.MathUtils.damp(steering,0,.1,dt*60);
playerT+=playerSpeed*dt*(1+steering*.10);
playerT=(playerT%TAU+TAU)%TAU;
placeCar(player,playerT,steering*2.4);
}
function updateBots(dt){
bots.forEach(b=>{
if(started&&!finished){
b.t+=b.speed*dt;
if(b.t>=TAU){
b.t-=TAU;
b.lap++;
}
}
placeCar(b.car,b.t,b.lane);
});
}
const cameraOffset=new THREE.Vector3();
function updateCamera(){
const angle=player.rotation.y+cameraYaw;
const horizontal=Math.cos(cameraPitch)*cameraDistance;
cameraOffset.set(
Math.sin(angle)*horizontal,
Math.sin(cameraPitch)*cameraDistance,
Math.cos(angle)*horizontal
);
const desired=player.position.clone().add(cameraOffset);
camera.position.lerp(desired,.16);
const target=player.position.clone().add(new THREE.Vector3(0,1.1,0));
camera.lookAt(target);
}
function updateHUD(){
const progress=playerT/TAU+lap;
const botProgress=bots.map(b=>b.t/TAU+b.lap);
const position=1+botProgress.filter(p=>p>progress).length;
document.getElementById('place').textContent=
finished?'ФИНИШ':position+' / 6';
document.getElementById('lap').textContent=
Math.min(lap+1,TOTAL_LAPS)+' / '+TOTAL_LAPS;
document.getElementById('speed').textContent=Math.round(playerSpeed*500);
document.getElementById('time').textContent=elapsed.toFixed(1);
}
function newRace(){
started=true;
finished=false;
elapsed=0;
lap=0;
playerT=0;
playerSpeed=0;
steering=0;
cameraYaw=0;
cameraPitch=.34;
cameraDistance=12;
bots.forEach((b,i)=>{
b.t=.08+i*.07;
b.lap=0;
b.speed=.40+Math.random()*.13;
});
document.getElementById('start').style.display='none';
document.getElementById('message').textContent='🏁 ПОЕХАЛИ!';
setTimeout(()=>{
if(started&&!finished)
document.getElementById('message').textContent='';
},1300);
}
function finishRace(){
finished=true;
started=false;
playerSpeed=0;
document.getElementById('message').textContent=
'🏆 ФИНИШ!\nВремя: '+elapsed.toFixed(1)+' сек.';
document.getElementById('start').textContent='↻ ЕЩЁ ГОНКА';
document.getElementById('start').style.display='block';
}
document.getElementById('start').onclick=newRace;
document.getElementById('restart').onclick=()=>{
started=false;
finished=false;
document.getElementById('start').textContent='▶ НАЧАТЬ ГОНКУ';
document.getElementById('message').textContent='RACING ZONE';
newRace();
};
document.querySelectorAll('[data-key]').forEach(btn=>{
const k=btn.dataset.key;
const on=e=>{e.preventDefault();keys[k]=true};
const off=e=>{e.preventDefault();keys[k]=false};
btn.addEventListener('pointerdown',on);
btn.addEventListener('pointerup',off);
btn.addEventListener('pointercancel',off);
btn.addEventListener('pointerleave',off);
});
addEventListener('keydown',e=>{
const k=e.key.toLowerCase();
if(k==='arrowleft'||k==='a')keys.left=true;
if(k==='arrowright'||k==='d')keys.right=true;
if(k==='arrowup'||k==='w')keys.gas=true;
if(k==='arrowdown'||k==='s')keys.brake=true;
});
addEventListener('keyup',e=>{
const k=e.key.toLowerCase();
if(k==='arrowleft'||k==='a')keys.left=false;
if(k==='arrowright'||k==='d')keys.right=false;
if(k==='arrowup'||k==='w')keys.gas=false;
if(k==='arrowdown'||k==='s')keys.brake=false;
});
let previous=performance.now();
function animate(now){
requestAnimationFrame(animate);
const dt=Math.min((now-previous)/1000,.04);
previous=now;
if(started&&!finished){
elapsed+=dt;
const before=playerT;
updatePlayer(dt);
if(playerT<before)lap++;
if(lap>=TOTAL_LAPS)finishRace();
}
updateBots(dt);
updateCamera();
updateHUD();
renderer.render(scene,camera);
}
placeCar(player,0);
bots.forEach(b=>placeCar(b.car,b.t,b.lane));
camera.position.set(0,7,15);
animate(performance.now());
addEventListener('resize',()=>{
camera.aspect=innerWidth/innerHeight;
camera.updateProjectionMatrix();
renderer.setSize(innerWidth,innerHeight);
});
</script>
</body>
</html>