Repository: jes/nightdrive Branch: master Commit: 4a8899372bf6 Files: 6 Total size: 15.1 KB Directory structure: gitextract_olwi0mbm/ ├── index.html └── js/ ├── car.js ├── nightdrive.js ├── plane.js ├── scene.js └── vector.js ================================================ FILE CONTENTS ================================================ ================================================ FILE: index.html ================================================ Nightdrive Click to toggle music ================================================ FILE: js/car.js ================================================ function Car() { // physics this.pos = new V2d(0,1); this.vel = new V2d(0,0.005); // lights this.braking = false; this.mainbeam = false; this.leftindicator = false; this.rightindicator = false; this.indicatorperiod = 0.9 + Math.random()*0.2; // seconds this.indication = 0; this.lanes = [0]; // x coordinates of lanes this.lane = 0; // car configuration const lightheight = Math.random()*0.2+0.4; const lightwidth = Math.random()*0.3+1.3; const lightradius = Math.random()*0.05+0.1; const rearlightcolour = colour(180,255, 0,50, 0,50); this.rearlights = [ {xy: new V2d(-lightwidth/2, 0.0), z: lightheight, r: lightradius, col: rearlightcolour}, {xy: new V2d( lightwidth/2, 0.0), z: lightheight, r: lightradius, col: rearlightcolour}, ]; const headlightcolour = colour(180,255, 180,255, 180,255); this.headlights = [ {xy: new V2d(-lightwidth/2, 0.0), z: lightheight, r: lightradius, col: headlightcolour}, {xy: new V2d( lightwidth/2, 0.0), z: lightheight, r: lightradius, col: headlightcolour}, ]; const indicatorcolour = colour(180,255, 100,200, 0,100); const indicatorwidth = lightwidth + 0.2 + Math.random()*0.1; const indicatorheight = lightheight - 0.1 + Math.random()*0.2; const indicatorradius = lightradius - (0.04 + Math.random()*0.03); this.leftindicatorlights = [ {xy: new V2d(-indicatorwidth/2, 0.0), z: indicatorheight, r: indicatorradius, col: indicatorcolour}, ]; this.rightindicatorlights = [ {xy: new V2d( indicatorwidth/2, 0.0), z: indicatorheight, r: indicatorradius, col: indicatorcolour}, ]; }; Car.prototype.render = function(scene) { if (this.vel.y > 0) { // moving in same direction as viewer: draw rearlights this.drawLights(scene, this.rearlights); } else { // moving in opposite direction to viewer: draw headlights this.drawLights(scene, this.headlights); } if (this.leftindicator && this.indication > (this.indicatorperiod/2)) this.drawLights(scene, this.leftindicatorlights); if (this.rightindicator && this.indication > (this.indicatorperiod/2)) this.drawLights(scene, this.rightindicatorlights); }; Car.prototype.drawLights = function(scene, lights) { for (l of lights) { scene.drawCircle(this.pos.add(l.xy), l.z, l.r, l.col); } }; Car.prototype.step = function(dt) { this.pos = this.pos.add(this.vel.mul(dt)); this.indication += dt; while (this.indication > this.indicatorperiod) this.indication -= this.indicatorperiod; const wrapy = 2500; while (this.pos.y > observer.pos.y+wrapy && this.vel.y > observer.vel.y) this.pos.y -= wrapy; while (this.pos.y < observer.pos.y && this.vel.y < observer.vel.y) this.pos.y += wrapy; // which lane are we in? let mylane = 0; for (let i = 0; i < this.lanes.length; i++) { if (Math.abs(this.pos.x-this.lanes[i]) < (Math.abs(this.pos.x-this.lanes[mylane]))) mylane = i; } this.lane = mylane; const k = Math.sign(this.vel.y); if (this.changelane) { const m = this.targetlane - this.sourcelane; if (this.lane == this.targetlane) { // decelerate in x this.vel = new V2d(this.vel.x-k*m*0.79*dt, this.vel.y); } else { // accelerate in x this.vel = new V2d(this.vel.x+k*m*0.8*dt, this.vel.y); } // once we reach the centre of the target lane, snap to centre and stop changing lane if (Math.sign(this.pos.x-this.lanes[this.targetlane]) != Math.sign(this.pos.x+this.vel.x*4*dt - this.lanes[this.targetlane])) { this.lane = this.targetlane; this.pos = new V2d(this.lanes[this.lane], this.pos.y); this.vel = new V2d(0, this.vel.y); this.changelane = false; } } const collision_secs = 7.0; const min_clearance = 2.0; // metres let leftlanesafe = this.lane > 0; loop: //for (car of cars) { for (let j = 0; j < cars.length; j++) { const car = cars[j]; if (car == this) continue; if (Math.sign(car.vel.y*this.vel.y) == -1) continue; for (let i = -1; i <= 1; i++) { const yoff = i*wrapy; // if we're not in the fast lane, and we're behind this car, and it's in our lane, and we're going faster, and we'll hit it within N seconds, change lanes if (Math.sign(car.vel.y)==k && this.lane < this.lanes.length-1 && this.lane<=car.lane && k*(this.pos.y+yoff) < k*car.pos.y && k*this.vel.y > k*car.vel.y && k*(this.pos.y+yoff+this.vel.y*collision_secs+min_clearance) > k*(car.pos.y+car.vel.y*collision_secs)) { this.changelane = true; this.sourcelane = this.lane; this.targetlane = this.lane+1; break loop; } // the left lane is not safe if there is a car in it that we'd hit within 3N seconds if (Math.sign(car.vel.y)==k && car.lane==this.lane-1 && k*(this.pos.y+yoff) < k*car.pos.y && k*this.vel.y > k*car.vel.y && k*(this.pos.y+yoff+this.vel.y*collision_secs*3) > k*(car.pos.y+car.vel.y*collision_secs*3+min_clearance)) { leftlanesafe = false; } } } // move left if we're not changing lane and the left lane is safe if (!this.changelane && leftlanesafe) { this.changelane = true; this.sourcelane = this.lane; this.targetlane = this.lane-1; } if (this.changelane) { this.rightindicator = this.targetlane > this.sourcelane; this.leftindicator = this.targetlane < this.sourcelane; } else { this.indication = 0; this.leftindicator = false; this.rightindicator = false; } }; function colour(r1,r2, g1,g2, b1,b2) { const r = r1 + Math.random() * (r2-r1); const g = g1 + Math.random() * (g2-g1); const b = b1 + Math.random() * (b2-b1); return `rgb(${r},${g},${b})`; } ================================================ FILE: js/nightdrive.js ================================================ let laststep = null; let observer; let cars = []; let planes = []; let lastwidth; let lastheight; const lanes = [-10,-7,-4]; const speed = [60,66,80]; // mph const catseyedist = 20; // metres const streetlightdist = 107; // metres const started = Date.now(); const musiclabeltime = 5000; // ms function init() { observer = new Car(); observer.pos.x = lanes[1]; observer.vel.y = speed[1] * 1600/3600; observer.lanes = lanes; const ourlanes = lanes; const theirlanes = ourlanes.map((x) => -x); for (let i = 0; i < 100; i++) { const car = new Car(); const lane = Math.floor(Math.random()*lanes.length); car.pos = new V2d(lanes[lane],i*25); const mph = speed[lane]; car.vel = new V2d(0, mph * 1600 / 3600); car.lanes = ourlanes; if (Math.random() < 0.5) { car.vel.y = -car.vel.y; car.pos.x = -car.pos.x; car.lanes = theirlanes; } car.vel.y += Math.random() * 20 - 10; cars.push(car); } cars.push(observer); const canvas = document.getElementById('canvas'); resize(canvas); render(); } function render() { step(); const canvas = document.getElementById('canvas'); if (canvas.clientWidth != lastwidth || canvas.clientHeight != lastheight) { resize(canvas); } const ctx = canvas.getContext('2d'); ctx.fillStyle = 'rgba(1,1,1,0.3)'; ctx.fillRect(0, 0, canvas.width, canvas.height); const scene = new Scene(ctx); scene.viewpoint = observer.pos; scene.viewz = 1.0; // 1 metre off ground catseyes(scene, lanes[0]-1.5, '#811'); catseyes(scene, lanes[0]+1.5, '#666'); catseyes(scene, lanes[1]+1.5, '#666'); catseyes(scene, lanes[2]+1.5, '#861'); streetlights(scene, lanes[0]-3.5); streetlights(scene, -0.5); streetlights(scene, -(lanes[0]-3.5)); streetlights(scene, 0.5); for (car of cars) { car.render(scene); } for (plane of planes) { plane.render(scene); } const label = document.getElementById('clickformusic'); label.style.top = `${canvas.height/2 - 100}px`; label.style.left = `${canvas.width/2 - label.clientWidth/2}px`; const time = Date.now() - started; if (time < musiclabeltime) { const col = (musiclabeltime-time) * (200/musiclabeltime); label.style.color = `rgb(${col}, ${col}, ${col})`; } else { label.style.display = 'none'; } scene.render(); window.requestAnimationFrame(render); } function catseyes(scene, x, col) { const numlines = Math.floor(observer.pos.y / catseyedist); const starty = (numlines-1)*catseyedist; for (let y = starty; y < observer.pos.y+100; y += catseyedist) { scene.drawCircle(new V2d(x-0.02, y), 0.01, 0.015, col, {no_occlude: true}); scene.drawCircle(new V2d(x+0.02, y), 0.01, 0.015, col, {no_occlude: true}); } } function streetlights(scene, x) { const numlines = Math.floor(observer.pos.y / streetlightdist); const starty = (numlines-1)*streetlightdist; for (let y = starty; y < observer.pos.y+3000; y += streetlightdist) { const col = `rgb(255,255,${150+((99*x)+(31*y))%101})`; scene.drawCircle(new V2d(x, y), 5, 0.2, col); } } function step() { const now = Date.now(); if (!laststep) { laststep = now; return; } const dt = (now - laststep) / 1000; laststep = now; for (car of cars) { car.step(dt); } for (plane of planes) { plane.step(dt); } // delete planes that are behind the viewer planes = planes.filter((p) => p.pos.y > observer.pos.y); // add new planes occasionally if (Math.random() < 0.00005 && planes.length < 4) { let plane = new Plane(); plane.pos = new V2d(observer.pos.x-10000-Math.random()*2000, observer.pos.y+20000+Math.random()*5000); plane.vel = new V2d(30+Math.random()*20, Math.random()*10-5); if (Math.random() < 0.5) { plane.pos = new V2d(-plane.pos.x, plane.pos.y); plane.vel.x = -plane.vel.x; } plane.z = 3000+Math.random()*2000; planes.push(plane); } } function resize(canvas) { lastwidth = canvas.width = canvas.clientWidth; lastheight = canvas.height = canvas.clientHeight; const ctx = canvas.getContext('2d'); ctx.fillStyle = 'rgb(0,0,0)'; ctx.beginPath(); ctx.rect(0, 0, canvas.width, canvas.height); ctx.fill(); } init(); let playing = false; document.getElementById('canvas').onclick = function() { if (playing) document.getElementById('audio').pause(); else document.getElementById('audio').play(); playing = !playing; } ================================================ FILE: js/plane.js ================================================ function Plane() { // physics this.pos = new V2d(0,0); this.vel = new V2d(25,15); this.z = 3000; this.t = 0; this.period = 1.2+Math.random()*0.2; }; Plane.prototype.render = function(scene) { // red when moving left, green when moving right const col = this.vel.x > 0 ? '#585' : '#855'; if (this.t > this.period/2) scene.drawCircle(this.pos, this.z, 12, col); }; Plane.prototype.step = function(dt) { this.pos = this.pos.add(this.vel.mul(dt)); this.t += dt; while (this.t > this.period) this.t -= this.period; }; ================================================ FILE: js/scene.js ================================================ const fullcircle = 180*Math.PI; function Scene(ctx) { this.ctx = ctx; this.viewpoint = new V2d(0,0); this.viewz = 1.0; this.viewscale = 1200; this.distscale = 2; this.circles = []; } Scene.prototype.drawCircle = function(pos, z, r, col, opts) { let circle = this.project(pos, z, r); if (!circle) return; circle.col = col; circle.roady = pos.y; let ground = this.project(pos, 0, 0); circle.yground = ground.y; if (opts && opts.no_occlude) circle.no_occlude = true; this.circles.push(circle); }; Scene.prototype.render = function() { this.ctx.globalCompositeOperation = 'lighter'; this.ctx.globalAlpha = 0.2; // get the nearest circles first this.circles.sort((a,b) => { return a.roady - b.roady; }); // work out which circles are occluded by the road let highestroad = this.ctx.canvas.height; for (circle of this.circles) { if (circle.yground < highestroad) highestroad = circle.yground; if (circle.y > highestroad && !circle.no_occlude) circle.occluded = true; } for (circle of this.circles) { if (circle.occluded) continue; this.ctx.fillStyle = circle.col; for (let k = 1.0; k > 0; k -= 0.15) { this.ctx.beginPath(); this.ctx.arc(circle.x, circle.y, circle.r*1.5*k*k, 0, fullcircle); this.ctx.fill(); } } this.ctx.globalCompositeOperation = 'source-over'; this.ctx.globalAlpha = 1.0; }; Scene.prototype.project = function(pos, z, r) { const dy = 0.1; const dx = bend(this.viewpoint.y+dy) - bend(this.viewpoint.y); const theta = Math.atan2(dx,dy); const posrel1 = pos.add(new V2d(bend(pos.y),0)).sub(this.viewpoint.add(new V2d(bend(this.viewpoint.y),0))); const posrel = posrel1.rotate(theta); z = z + terrain(pos.y); const zrel = z - (this.viewz + terrain(this.viewpoint.y)); // things behind the viewer are not visible if (posrel.y <= 0) return null; const dist = this.distscale * Math.sqrt(posrel.y*posrel.y + zrel*zrel); // things too close are not visible if (dist < 0.5) return null; const scaleratio = this.viewscale * this.ctx.canvas.width / 640; const screenx = (this.ctx.canvas.width/2) + scaleratio * (posrel.x / dist); const screeny = (this.ctx.canvas.height/2) - scaleratio * (zrel / dist); const screenr = scaleratio * (r / dist); // px return { x: screenx, y: screeny, r: screenr, }; }; function terrain(y) { return 10*Math.sin(y/1000) + 5*Math.cos(y/527) + 2*Math.sin(y/219); } function bend(y) { return 200*Math.sin(y/909) + 51*Math.cos(y/517) + 23*Math.sin(y/201); } ================================================ FILE: js/vector.js ================================================ function V2d(x,y) { this.x = x; this.y = y; } V2d.prototype.add = function(v) { return new V2d(this.x + v.x, this.y + v.y); }; V2d.prototype.sub = function(v) { return new V2d(this.x - v.x, this.y - v.y); }; V2d.prototype.mul = function(k) { return new V2d(this.x * k, this.y * k); }; V2d.prototype.length = function() { return Math.sqrt(this.x*this.x + this.y*this.y); }; V2d.prototype.angle = function() { return Math.atan2(this.x, this.y); }; V2d.prototype.rotate = function(theta) { return new V2d(Math.cos(theta)*this.x - Math.sin(theta)*this.y, Math.sin(theta)*this.x + Math.cos(theta)*this.y); };