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);
};