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