VICE HORIZON — open world toybox in three.js
HTML
<link rel="preconnect" href="https://fonts.googleapis.com">
<link href="https://fonts.googleapis.com/css2?family=Pricedown&family=Barlow+Condensed:wght@400;600;700&display=swap" rel="stylesheet">
<canvas id="c"></canvas>
<div id="hud">
<div id="logo">
<div class="l1"><b>VICE</b> <i>HORIZON</i></div>
<div id="area">DOWNTOWN</div>
</div>
<div id="tr">
<div id="stars"><span class="off">★</span><span class="off">★</span><span class="off">★</span><span class="off">★</span><span class="off">★</span></div>
<div id="cash">$250</div>
</div>
<div id="mapwrap"><canvas id="map" width="176" height="176"></canvas></div>
<div id="bars">
<div class="bar" id="hp"><i></i></div>
<div class="bar" id="nitro"><i></i></div>
</div>
<div id="br">
<div id="speed">0 <small>KM/H</small></div>
<div id="radio"><div class="in">
<div id="stn">SUNSET FM 96.5</div>
<div id="songwrap"><span id="song">NOW PLAYING — NEON TIDE</span></div>
</div></div>
</div>
<div id="mission"></div>
<div id="hint"><b>WASD</b> drive · <b>SPACE</b> handbrake · <b>SHIFT</b> nitro · <b>E</b> in/out · <b>F</b> shoot · <b>R</b> reload · <b>M</b> radio</div>
<div id="toasts"></div>
<div id="cross"></div>
<div id="vign"></div>
<div id="state"><div class="big"></div><div class="sub"></div></div>
</div>
<div id="title">
<div class="bg"></div><div class="sun"></div><div class="city"></div><div class="scan"></div>
<div class="inner">
<h1>VICE<br><span>HORIZON</span></h1>
<div class="tag">AN OPEN-WORLD TOYBOX · BUILT IN THREE.JS</div>
<div class="ctrls">
<div><b>W A S D</b>DRIVE / MOVE</div><div><b>SPACE</b>HANDBRAKE · DRIFT</div>
<div><b>SHIFT</b>NITRO</div><div><b>E</b>GET IN / OUT OF CARS</div>
<div><b>F</b>SHOOT · <b>R</b>RELOAD</div><div><b>M</b>RADIO · <b>0</b>MUTE</div>
</div>
<div class="start">▶ CLICK TO START</div>
</div>
<div class="credit">A...
CSS
:root{
--pink:#ff2e7e; --cyan:#35e0d2; --orange:#ff9e2c; --cash:#8dff70;
--panel:rgba(7,22,29,.82); --line:rgba(255,46,126,.4);
}
*{margin:0;padding:0;box-sizing:border-box}
html,body{width:100%;height:100%;overflow:hidden;background:#06121a;font-family:"Barlow Condensed",sans-serif;color:#fff;
-webkit-user-select:none;user-select:none}
#c{position:fixed;inset:0;display:block}
/* ---------- HUD ---------- */
#hud{position:fixed;inset:0;pointer-events:none;z-index:10;display:none}
.shadowed{text-shadow:0 2px 0 #000,0 0 10px rgba(0,0,0,.6)}
#logo{position:absolute;top:14px;left:16px}
#logo .l1{font-family:Pricedown;font-size:24px;letter-spacing:1px;transform:skewX(-6deg);display:inline-block}
#logo .l1 b{color:var(--pink);font-weight:400}
#logo .l1 i{color:#fff;font-style:normal}
#area{margin-top:2px;font-weight:600;font-size:14px;letter-spacing:4px;color:var(--cyan);text-transform:uppercase;
text-shadow:0 1px 0 #000;transition:opacity .5s}
#area.fade{opacity:0}
#tr{position:absolute;top:14px;right:18px;text-align:right}
#stars{font-size:22px;letter-spacing:4px;margin-bottom:2px}
#stars .on{color:#fff;text-shadow:0 0 10px #fff,0 2px 0 #000}
#stars .off{color:rgba(255,255,255,.18);text-shadow:0 1px 0 #000}
#cash{font-family:Pricedown;font-size:28px;color:var(--cash);text-shadow:0 2px 0 #000,0 0 14px rgba(141,255,112,.35)}
#cash.pop{animation:pop .35s}
@keyframes pop{0%{transform:scale(1)}40%{transform:scale(1.25)}100%{transform:scale(1)}}
#mapwrap{position:absolute;left:16px;bottom:16px;width:176px;height:176px;border-radius:12px;overflow:hidden;
border:2px solid var(--line);box-shadow:0 0 0 2px rgba(0,0,0,.5),0 6px 22px rgba(0,0,0,.5);background:#0d2228}
#map{width:100%;height:100%;display:block}
#bars{position:absolute;left:16px;bottom:202px;width:176px;display:flex;flex-direction:column;gap:3px}
.bar{height:7px;background:rgba(0,0,0,.6);border:1px solid...
JavaScript
{"imports":{"three":"https://cdn.jsdelivr.net/npm/three@0.160.0/build/three.module.js"}}
import * as THREE from 'three';
/* ============================== UTILS ============================== */
const $=id=>document.getElementById(id);
const R=(a=1,b)=>b===undefined?Math.random()*a:a+Math.random()*(b-a);
const RI=(a,b)=>Math.floor(R(a,b+1));
const P=a=>a[RI(0,a.length-1)];
const C=(min,max,v)=>Math.max(min,Math.min(max,v));
const L=(a,b,t)=>a+(b-a)*t;
const angD=(a,b)=>{let d=(b-a)%(Math.PI*2);if(d>Math.PI)d-=Math.PI*2;if(d<-Math.PI)d+=Math.PI*2;return d;};
const V3=(x,y,z)=>new THREE.Vector3(x,y,z);
/* ============================== CITY DATA ============================== */
const CELL=60, ROAD=14, BLOCK=46, KMIN=-4, KMAX=4; // road centres at k*CELL
const SAND0=244, WATER=330, MAPEDGE=268;
/* ============================== RENDERER / SCENE ============================== */
let renderer;
try{
renderer=new THREE.WebGLRenderer({canvas:$('c'),antialias:true});
}catch(e){
document.body.innerHTML='<div style="padding:40px;font-family:sans-serif;color:#fff">WebGL unavailable — try a modern desktop browser.</div>';
throw e;
}
renderer.setPixelRatio(Math.min(devicePixelRatio,2));
renderer.setSize(innerWidth,innerHeight);
renderer.outputColorSpace=THREE.SRGBColorSpace;
renderer.toneMapping=THREE.ACESFilmicToneMapping;
renderer.toneMappingExposure=1.05;
const scene=new THREE.Scene();
scene.fog=new THREE.Fog(0xff9e6e,180,760);
const camera=new THREE.PerspectiveCamera(62,innerWidth/innerHeight,0.1,2500);
camera.position.set(0,60,180);
const sunDir=V3(-0.62,0.17,-0.28).normalize();
scene.add(new THREE.HemisphereLight(0xffd0a8,0x33565e,0.85));
const sun=new THREE.DirectionalLight(0xffb36b,2.4); sun.position.copy(sunDir).multiplyScalar(300); scene.add(sun);
const fill=new THREE.DirectionalLight(0x7fb4d6,0.5); fill.position.set(200,120,150); scene.add(fill);
/* sky dome */
const skyMat=new...