├── .gitignore
├── readme_assets
├── screenshot.png
├── screenshot_overhead.png
├── extrusion_1.svg
└── extrusion_4.svg
├── js
├── isTouchDevice.js
├── worker.js
├── parseFunctions.js
├── lineModelers.js
├── buildLevel.js
├── math.js
├── renderLevel.js
├── geometry.js
├── materials.js
├── roads.js
├── paths.js
├── Input.js
├── levelSchemas.js
├── Car.js
├── Screen.js
├── Dashboard.js
├── modelLevel.js
└── Buttons.js
├── drivey.sublime-project
├── legacy
├── js
│ ├── RoadLineStyle.js
│ ├── WarpGate.js
│ ├── DeepDarkNight.js
│ ├── TestLevel.js
│ ├── RoadPath.js
│ ├── Spectre.js
│ ├── Tunnel.js
│ ├── CliffsideBeach.js
│ ├── TrainTracks.js
│ ├── Overpass.js
│ ├── City.js
│ ├── Input.js
│ ├── Level.js
│ ├── Buttons.js
│ ├── Dashboard.js
│ ├── Screen.js
│ └── Car.js
├── reset.css
├── lib
│ ├── CopyShader.js
│ ├── ShaderPass.js
│ ├── RenderPass.js
│ ├── SobelOperatorShader.js
│ ├── theme.js
│ └── EffectComposer.js
└── index.html
├── levels
├── DeepDarkNight.html
├── TestLevel.html
├── WarpGate.html
├── Spectre.html
├── Tunnel.html
├── Reference.html
├── CliffsideBeach.html
├── City.html
├── TrainTracks.html
├── Overpass.html
├── CurveTestLevel.html
└── IndustrialZone.html
├── index.html
├── lib
├── three
│ ├── shaders
│ │ ├── CopyShader.js
│ │ └── SobelOperatorShader.js
│ └── postprocessing
│ │ ├── ShaderPass.js
│ │ ├── Pass.js
│ │ ├── RenderPass.js
│ │ └── MaskPass.js
└── theme.js
├── TODO.txt
├── css
└── drivey.css
└── README.md
/.gitignore:
--------------------------------------------------------------------------------
1 | haxe/drivey.js
2 | haxe/drivey.js.map
3 |
--------------------------------------------------------------------------------
/readme_assets/screenshot.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Rezmason/drivey/HEAD/readme_assets/screenshot.png
--------------------------------------------------------------------------------
/readme_assets/screenshot_overhead.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Rezmason/drivey/HEAD/readme_assets/screenshot_overhead.png
--------------------------------------------------------------------------------
/js/isTouchDevice.js:
--------------------------------------------------------------------------------
1 | export default (() => "ontouchstart" in window || navigator.maxTouchPoints > 0 || navigator.msMaxTouchPoints > 0)();
2 |
--------------------------------------------------------------------------------
/js/worker.js:
--------------------------------------------------------------------------------
1 | import modelLevel from "./modelLevel.js";
2 |
3 | addEventListener("message", ({ data }) => {
4 | const level = modelLevel(data);
5 | postMessage(level);
6 | });
7 |
--------------------------------------------------------------------------------
/drivey.sublime-project:
--------------------------------------------------------------------------------
1 | {
2 | "folders":
3 | [
4 | {
5 | "path": "."
6 | }
7 | ],
8 | "settings":
9 | {
10 | "tab_size": 2,
11 | "translate_tabs_to_spaces": true
12 | }
13 | }
14 |
--------------------------------------------------------------------------------
/legacy/js/RoadLineStyle.js:
--------------------------------------------------------------------------------
1 | "use strict";
2 |
3 | const RoadLineStyle = {
4 | type: {
5 | SOLID: "SOLID",
6 | DASH: "DASH",
7 | DOT: "DOT"
8 | },
9 |
10 | SOLID: pointSpacing => ({ type: "SOLID", pointSpacing }),
11 | DASH: (on, off, pointSpacing) => ({ type: "DASH", on, off, pointSpacing }),
12 | DOT: spacing => ({ type: "DOT", spacing })
13 | };
14 |
--------------------------------------------------------------------------------
/legacy/reset.css:
--------------------------------------------------------------------------------
1 | * {
2 | margin: 0;
3 | padding: 0;
4 | border: 0;
5 | outline: 0;
6 | text-decoration: none;
7 | font-weight: inherit;
8 | font-style: inherit;
9 | color: inherit;
10 | font-size: 100%;
11 | font-family: inherit;
12 | vertical-align: baseline;
13 | list-style: none;
14 | border-collapse: collapse;
15 | border-spacing: 0;
16 | -webkit-font-smoothing: antialiased;
17 | -moz-osx-font-smoothing: grayscale;
18 | }
19 |
--------------------------------------------------------------------------------
/levels/DeepDarkNight.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
--------------------------------------------------------------------------------
/levels/TestLevel.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
--------------------------------------------------------------------------------
/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | Drivey.js - GL on Wheels
5 |
6 |
7 |
8 |
9 |
10 |
17 |
18 |
19 |
--------------------------------------------------------------------------------
/levels/WarpGate.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
--------------------------------------------------------------------------------
/js/parseFunctions.js:
--------------------------------------------------------------------------------
1 | import { Color, Vector2 } from "./../lib/three/three.module.js";
2 |
3 | const verbatim = (_) => _;
4 | const safeParseFloat = (s) => {
5 | const f = parseFloat(s);
6 | return isNaN(f) ? 0 : f;
7 | };
8 | const safeParseInt = (s) => {
9 | const i = parseInt(s);
10 | return isNaN(i) ? 0 : i;
11 | };
12 | const parseNumberList = (s) => s.split(",").map((s) => safeParseFloat(s.trim()));
13 | const parseColor = (s) => new Color(...parseNumberList(s));
14 | const parseVec2 = (s) => new Vector2(...parseNumberList(s));
15 | const parseBool = (s) => s === "true";
16 |
17 | export { verbatim, safeParseFloat, safeParseInt, parseNumberList, parseColor, parseVec2, parseBool };
18 |
--------------------------------------------------------------------------------
/legacy/lib/CopyShader.js:
--------------------------------------------------------------------------------
1 | /**
2 | * @author alteredq / http://alteredqualia.com/
3 | *
4 | * Full-screen textured quad shader
5 | */
6 |
7 | THREE.CopyShader = {
8 |
9 | uniforms: {
10 |
11 | "tDiffuse": { value: null },
12 | "opacity": { value: 1.0 }
13 |
14 | },
15 |
16 | vertexShader: [
17 |
18 | "varying vec2 vUv;",
19 |
20 | "void main() {",
21 |
22 | " vUv = uv;",
23 | " gl_Position = projectionMatrix * modelViewMatrix * vec4( position, 1.0 );",
24 |
25 | "}"
26 |
27 | ].join( "\n" ),
28 |
29 | fragmentShader: [
30 |
31 | "uniform float opacity;",
32 |
33 | "uniform sampler2D tDiffuse;",
34 |
35 | "varying vec2 vUv;",
36 |
37 | "void main() {",
38 |
39 | " vec4 texel = texture2D( tDiffuse, vUv );",
40 | " gl_FragColor = opacity * texel;",
41 |
42 | "}"
43 |
44 | ].join( "\n" )
45 |
46 | };
47 |
--------------------------------------------------------------------------------
/lib/three/shaders/CopyShader.js:
--------------------------------------------------------------------------------
1 | /**
2 | * @author alteredq / http://alteredqualia.com/
3 | *
4 | * Full-screen textured quad shader
5 | */
6 |
7 |
8 |
9 | var CopyShader = {
10 |
11 | uniforms: {
12 |
13 | "tDiffuse": { value: null },
14 | "opacity": { value: 1.0 }
15 |
16 | },
17 |
18 | vertexShader: [
19 |
20 | "varying vec2 vUv;",
21 |
22 | "void main() {",
23 |
24 | " vUv = uv;",
25 | " gl_Position = projectionMatrix * modelViewMatrix * vec4( position, 1.0 );",
26 |
27 | "}"
28 |
29 | ].join( "\n" ),
30 |
31 | fragmentShader: [
32 |
33 | "uniform float opacity;",
34 |
35 | "uniform sampler2D tDiffuse;",
36 |
37 | "varying vec2 vUv;",
38 |
39 | "void main() {",
40 |
41 | " vec4 texel = texture2D( tDiffuse, vUv );",
42 | " gl_FragColor = opacity * texel;",
43 |
44 | "}"
45 |
46 | ].join( "\n" )
47 |
48 | };
49 |
50 | export { CopyShader };
51 |
--------------------------------------------------------------------------------
/levels/Spectre.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
--------------------------------------------------------------------------------
/legacy/js/WarpGate.js:
--------------------------------------------------------------------------------
1 | "use strict";
2 |
3 | class WarpGate extends Level {
4 | build(meshes, transparentMeshes, skyMeshes) {
5 | this.name = "The Cyber Tube";
6 | this.tint = new THREE.Color(1, 0.1, 0.3);
7 | this.laneWidth = 0;
8 |
9 | for (let i = 0; i < 2; i += 1 / 8) {
10 | const x = Math.cos(i * Math.PI);
11 | const y = Math.sin(i * Math.PI);
12 | const linePath1 = new THREE.ShapePath();
13 | this.drawRoadLine(this.roadPath, linePath1, x * 4, 0.15, RoadLineStyle.DASH(10, 10 + i * 10, 0), 0, 1);
14 | const mesh1 = makeMesh(linePath1, 0.1, 0, 0.5);
15 | mesh1.position.z = y * 4;
16 | meshes.push(mesh1);
17 |
18 | const linePath2 = new THREE.ShapePath();
19 | this.drawRoadLine(this.roadPath, linePath2, x * 20, 0.15, RoadLineStyle.DASH(5, 50 + i * 50, 0), 0, 1);
20 | const mesh2 = makeMesh(linePath2, 0.1, 0, 1);
21 | mesh2.position.z = y * 20;
22 | meshes.push(mesh2);
23 | }
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/TODO.txt:
--------------------------------------------------------------------------------
1 | STAR GUITAR
2 | Leverage loops
3 | Make it work before you make it look good
4 |
5 | Minified Drivey: browserify / webpack / rollup
6 |
7 | Portable Drivey
8 | Like A-Frame, drivey.js (and minified) would find a root tag in the DOM and render it
9 | No CSS, no UI, no built in levels, no interaction, no wireframe, no app logic, no drag-and-drop
10 | show-dashboard="true" npc-cars="0" driving-side="right"
11 |
12 | Driving in reverse stinks
13 | Change car auto-steering logic when the car is moving backwards
14 |
15 | Test on multiple devices
16 | Chances are, the effect composer messes things up in browsers that don't support floating point textures
17 |
18 | Activate level modeling in a worker
19 | Waiting on browser support
20 |
21 | Improve performance of Approximation::getNearest
22 |
23 | Cavern level
24 | Stalagmites and stalagtites, subtle color variation?
25 |
26 | Much, much later
27 | Spotify playlist embed?
28 | About box?
29 | Localization
30 | Collision
31 | "theWalls" in jj
32 |
--------------------------------------------------------------------------------
/legacy/js/DeepDarkNight.js:
--------------------------------------------------------------------------------
1 | "use strict";
2 |
3 | class DeepDarkNight extends Level {
4 | build(meshes, transparentMeshes, skyMeshes) {
5 | this.name = "The Deep Dark Night";
6 | this.tint = new THREE.Color(0.7, 0.7, 0.7);
7 | // this.tint = new THREE.Color(0, 0.6, 1);
8 | this.laneWidth = 3;
9 | const roadLineColor = 0.75;
10 | this.roadPath.scale(2, 2);
11 | const linePath = new THREE.ShapePath();
12 | this.drawRoadLine(this.roadPath, linePath, 0, 0.2, RoadLineStyle.DASH(4, 10, 0), 0, 1);
13 | this.drawRoadLine(this.roadPath, linePath, -3, 0.15, RoadLineStyle.DASH(30, 2, 5), 0, 1);
14 | this.drawRoadLine(this.roadPath, linePath, 3, 0.15, RoadLineStyle.DASH(30, 2, 5), 0, 1);
15 | meshes.push(makeMesh(linePath, 0, 1, roadLineColor, 1, 5));
16 | const postPath = new THREE.ShapePath();
17 | this.drawRoadLine(this.roadPath, postPath, -6, 0.2, RoadLineStyle.DASH(0.2, 50, 0), 0, 1);
18 | this.drawRoadLine(this.roadPath, postPath, 6, 0.2, RoadLineStyle.DASH(0.2, 50, 0), 0, 1);
19 | meshes.push(makeMesh(postPath, 0.6, 1, roadLineColor, 1, 5));
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/levels/Tunnel.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
--------------------------------------------------------------------------------
/levels/Reference.html:
--------------------------------------------------------------------------------
1 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
--------------------------------------------------------------------------------
/levels/CliffsideBeach.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
--------------------------------------------------------------------------------
/levels/City.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
--------------------------------------------------------------------------------
/levels/TrainTracks.html:
--------------------------------------------------------------------------------
1 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
--------------------------------------------------------------------------------
/levels/Overpass.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
--------------------------------------------------------------------------------
/legacy/js/TestLevel.js:
--------------------------------------------------------------------------------
1 | "use strict";
2 |
3 | class TestLevel extends Level {
4 | build(meshes, transparentMeshes, skyMeshes) {
5 | this.name = "Test";
6 | this.tint = new THREE.Color(0.2, 0.8, 1);
7 | this.skyLow = 0.0;
8 | this.skyHigh = 0.5;
9 | this.ground = 0.125;
10 |
11 | const roadStripesPath = new THREE.ShapePath();
12 | mergeShapePaths(roadStripesPath, this.drawRoadLine(this.roadPath, new THREE.ShapePath(), 3, 0.15, RoadLineStyle.SOLID(5), 0, 1));
13 | mergeShapePaths(roadStripesPath, this.drawRoadLine(this.roadPath, new THREE.ShapePath(), -3, 0.15, RoadLineStyle.SOLID(5), 0, 1));
14 | meshes.push(makeMesh(roadStripesPath, 0, 1000, 0.58));
15 |
16 | const dashedLinePath = new THREE.ShapePath();
17 | this.drawRoadLine(this.roadPath, dashedLinePath, 0.0625, 0.0625, RoadLineStyle.DASH(30, 30, 5), 0, 1);
18 | this.drawRoadLine(this.roadPath, dashedLinePath, -0.0625, 0.0625, RoadLineStyle.DASH(30, 30, 5), 0, 1);
19 | meshes.push(makeMesh(dashedLinePath, 0, 1000, 0.58));
20 |
21 | // croquet hoops
22 | const hoopTopsPath = new THREE.ShapePath();
23 | const hoopSidesPath = new THREE.ShapePath();
24 | this.drawRoadLine(this.roadPath, hoopTopsPath, 0, 10, RoadLineStyle.DASH(1, 20, 0), 0, 1);
25 | this.drawRoadLine(this.roadPath, hoopSidesPath, -5, 1, RoadLineStyle.DASH(1, 20, 0), 0, 1);
26 | this.drawRoadLine(this.roadPath, hoopSidesPath, 5, 1, RoadLineStyle.DASH(1, 20, 0), 0, 1);
27 | const hoopTopsMesh = makeMesh(hoopTopsPath, 1, 20, 0.5);
28 | hoopTopsMesh.position.z = 5;
29 | meshes.push(hoopTopsMesh);
30 | const hoopSidesMesh = makeMesh(hoopSidesPath, 6, 20, 0.5);
31 | meshes.push(hoopSidesMesh);
32 | }
33 | }
34 |
--------------------------------------------------------------------------------
/legacy/js/RoadPath.js:
--------------------------------------------------------------------------------
1 | "use strict";
2 |
3 | class RoadPath {
4 | constructor(points) {
5 | this.points = points;
6 | this.curve = makeSplinePath(points, true);
7 | }
8 |
9 | clone() {
10 | return new RoadPath(this.points);
11 | }
12 |
13 | scale(x, y) {
14 | this.points = this.points.map(point => new THREE.Vector2(point.x * x, point.y * y));
15 | this.curve = makeSplinePath(this.points, true);
16 | }
17 |
18 | getPoint(t) {
19 | if (arguments.length > 1) throw "!";
20 | const pos = this.curve.getPoint(mod(t, 1));
21 | return new THREE.Vector2(pos.x, pos.y);
22 | }
23 |
24 | getTangent(t) {
25 | if (arguments.length > 1) throw "!";
26 | const EPSILON = 0.00001;
27 | const point = this.getPoint(t + EPSILON).sub(this.getPoint(t - EPSILON));
28 | return point.normalize();
29 | }
30 |
31 | getNormal(t) {
32 | if (arguments.length > 1) throw "!";
33 | const normal = this.getTangent(t);
34 | return new THREE.Vector2(-normal.y, normal.x);
35 | }
36 |
37 | approximate(resolution = 1000) {
38 | return new Approximation(this, resolution);
39 | }
40 |
41 | get length() {
42 | return this.curve.getLength();
43 | }
44 | }
45 |
46 | class Approximation {
47 | constructor(roadPath, resolution) {
48 | this.roadPath = roadPath;
49 | this.resolution = resolution;
50 | this.points = [];
51 | for (let i = 0; i < resolution; i++) {
52 | this.points.push(roadPath.getPoint(i / resolution));
53 | }
54 | }
55 |
56 | getNearest(to) {
57 | return minDistSquaredIndex(this.points, to) / this.resolution;
58 | }
59 |
60 | getNearestPoint(to) {
61 | return this.roadPath.getPoint(this.getNearest(to));
62 | }
63 | }
64 |
--------------------------------------------------------------------------------
/legacy/lib/ShaderPass.js:
--------------------------------------------------------------------------------
1 | /**
2 | * @author alteredq / http://alteredqualia.com/
3 | */
4 |
5 | THREE.ShaderPass = function ( shader, textureID ) {
6 |
7 | THREE.Pass.call( this );
8 |
9 | this.textureID = ( textureID !== undefined ) ? textureID : "tDiffuse";
10 |
11 | if ( shader instanceof THREE.ShaderMaterial ) {
12 |
13 | this.uniforms = shader.uniforms;
14 |
15 | this.material = shader;
16 |
17 | } else if ( shader ) {
18 |
19 | this.uniforms = THREE.UniformsUtils.clone( shader.uniforms );
20 |
21 | this.material = new THREE.ShaderMaterial( {
22 |
23 | defines: Object.assign( {}, shader.defines ),
24 | uniforms: this.uniforms,
25 | vertexShader: shader.vertexShader,
26 | fragmentShader: shader.fragmentShader
27 |
28 | } );
29 |
30 | }
31 |
32 | this.fsQuad = new THREE.Pass.FullScreenQuad( this.material );
33 |
34 | };
35 |
36 | THREE.ShaderPass.prototype = Object.assign( Object.create( THREE.Pass.prototype ), {
37 |
38 | constructor: THREE.ShaderPass,
39 |
40 | render: function ( renderer, writeBuffer, readBuffer /*, deltaTime, maskActive */ ) {
41 |
42 | if ( this.uniforms[ this.textureID ] ) {
43 |
44 | this.uniforms[ this.textureID ].value = readBuffer.texture;
45 |
46 | }
47 |
48 | this.fsQuad.material = this.material;
49 |
50 | if ( this.renderToScreen ) {
51 |
52 | renderer.setRenderTarget( null );
53 | this.fsQuad.render( renderer );
54 |
55 | } else {
56 |
57 | renderer.setRenderTarget( writeBuffer );
58 | // TODO: Avoid using autoClear properties, see https://github.com/mrdoob/three.js/pull/15571#issuecomment-465669600
59 | if ( this.clear ) renderer.clear( renderer.autoClearColor, renderer.autoClearDepth, renderer.autoClearStencil );
60 | this.fsQuad.render( renderer );
61 |
62 | }
63 |
64 | }
65 |
66 | } );
67 |
--------------------------------------------------------------------------------
/levels/CurveTestLevel.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
--------------------------------------------------------------------------------
/legacy/lib/RenderPass.js:
--------------------------------------------------------------------------------
1 | /**
2 | * @author alteredq / http://alteredqualia.com/
3 | */
4 |
5 | THREE.RenderPass = function ( scene, camera, overrideMaterial, clearColor, clearAlpha ) {
6 |
7 | THREE.Pass.call( this );
8 |
9 | this.scene = scene;
10 | this.camera = camera;
11 |
12 | this.overrideMaterial = overrideMaterial;
13 |
14 | this.clearColor = clearColor;
15 | this.clearAlpha = ( clearAlpha !== undefined ) ? clearAlpha : 0;
16 |
17 | this.clear = true;
18 | this.clearDepth = false;
19 | this.needsSwap = false;
20 |
21 | };
22 |
23 | THREE.RenderPass.prototype = Object.assign( Object.create( THREE.Pass.prototype ), {
24 |
25 | constructor: THREE.RenderPass,
26 |
27 | render: function ( renderer, writeBuffer, readBuffer /*, deltaTime, maskActive */ ) {
28 |
29 | var oldAutoClear = renderer.autoClear;
30 | renderer.autoClear = false;
31 |
32 | this.scene.overrideMaterial = this.overrideMaterial;
33 |
34 | var oldClearColor, oldClearAlpha;
35 |
36 | if ( this.clearColor ) {
37 |
38 | oldClearColor = renderer.getClearColor().getHex();
39 | oldClearAlpha = renderer.getClearAlpha();
40 |
41 | renderer.setClearColor( this.clearColor, this.clearAlpha );
42 |
43 | }
44 |
45 | if ( this.clearDepth ) {
46 |
47 | renderer.clearDepth();
48 |
49 | }
50 |
51 | renderer.setRenderTarget( this.renderToScreen ? null : readBuffer );
52 |
53 | // TODO: Avoid using autoClear properties, see https://github.com/mrdoob/three.js/pull/15571#issuecomment-465669600
54 | if ( this.clear ) renderer.clear( renderer.autoClearColor, renderer.autoClearDepth, renderer.autoClearStencil );
55 | renderer.render( this.scene, this.camera );
56 |
57 | if ( this.clearColor ) {
58 |
59 | renderer.setClearColor( oldClearColor, oldClearAlpha );
60 |
61 | }
62 |
63 | this.scene.overrideMaterial = null;
64 | renderer.autoClear = oldAutoClear;
65 |
66 | }
67 |
68 | } );
69 |
--------------------------------------------------------------------------------
/lib/three/postprocessing/ShaderPass.js:
--------------------------------------------------------------------------------
1 | /**
2 | * @author alteredq / http://alteredqualia.com/
3 | */
4 |
5 | import {
6 | ShaderMaterial,
7 | UniformsUtils
8 | } from "../three.module.js";
9 | import { Pass } from "../postprocessing/Pass.js";
10 |
11 | var ShaderPass = function ( shader, textureID ) {
12 |
13 | Pass.call( this );
14 |
15 | this.textureID = ( textureID !== undefined ) ? textureID : "tDiffuse";
16 |
17 | if ( shader instanceof ShaderMaterial ) {
18 |
19 | this.uniforms = shader.uniforms;
20 |
21 | this.material = shader;
22 |
23 | } else if ( shader ) {
24 |
25 | this.uniforms = UniformsUtils.clone( shader.uniforms );
26 |
27 | this.material = new ShaderMaterial( {
28 |
29 | defines: Object.assign( {}, shader.defines ),
30 | uniforms: this.uniforms,
31 | vertexShader: shader.vertexShader,
32 | fragmentShader: shader.fragmentShader
33 |
34 | } );
35 |
36 | }
37 |
38 | this.fsQuad = new Pass.FullScreenQuad( this.material );
39 |
40 | };
41 |
42 | ShaderPass.prototype = Object.assign( Object.create( Pass.prototype ), {
43 |
44 | constructor: ShaderPass,
45 |
46 | render: function ( renderer, writeBuffer, readBuffer /*, deltaTime, maskActive */ ) {
47 |
48 | if ( this.uniforms[ this.textureID ] ) {
49 |
50 | this.uniforms[ this.textureID ].value = readBuffer.texture;
51 |
52 | }
53 |
54 | this.fsQuad.material = this.material;
55 |
56 | if ( this.renderToScreen ) {
57 |
58 | renderer.setRenderTarget( null );
59 | this.fsQuad.render( renderer );
60 |
61 | } else {
62 |
63 | renderer.setRenderTarget( writeBuffer );
64 | // TODO: Avoid using autoClear properties, see https://github.com/mrdoob/three.js/pull/15571#issuecomment-465669600
65 | if ( this.clear ) renderer.clear( renderer.autoClearColor, renderer.autoClearDepth, renderer.autoClearStencil );
66 | this.fsQuad.render( renderer );
67 |
68 | }
69 |
70 | }
71 |
72 | } );
73 |
74 | export { ShaderPass };
75 |
--------------------------------------------------------------------------------
/js/lineModelers.js:
--------------------------------------------------------------------------------
1 | import { getOffsetPoints, makeCirclePath, makePolygonPath } from "./paths.js";
2 | import { fract } from "./math.js";
3 |
4 | const modelSolidLine = ({ spacing, curve, x, width, start, end }) => {
5 | if (start === end) return [];
6 | width = Math.abs(width);
7 | const outsidePoints = getOffsetPoints(curve, x - width / 2, start, end, spacing);
8 | const insidePoints = getOffsetPoints(curve, x + width / 2, start, end, spacing);
9 | outsidePoints.reverse();
10 | if (Math.abs(end - start) < 1) {
11 | return [makePolygonPath(outsidePoints.concat(insidePoints))];
12 | } else {
13 | return [makePolygonPath(outsidePoints), makePolygonPath(insidePoints)];
14 | }
15 | };
16 |
17 | const modelDashedLine = ({ spacing, length, curve, x, width, start, end }) => {
18 | if (start === end) return [];
19 | start = fract(start);
20 | end = end === 1 ? 1 : fract(end);
21 | if (end < start) end++;
22 |
23 | const dashSpan = length + spacing;
24 | const numDashes = Math.floor((end - start) / dashSpan);
25 | return Array(numDashes)
26 | .fill()
27 | .map((_, index) => start + index * dashSpan)
28 | .map((start) =>
29 | modelSolidLine({
30 | curve,
31 | x,
32 | width,
33 | start,
34 | end: Math.min(end, start + length),
35 | })
36 | )
37 | .flat();
38 | };
39 |
40 | const modelDottedLine = ({ spacing, curve, x, width, start, end }) => {
41 | if (start === end) return [];
42 | const positions = getOffsetPoints(curve, x, start, end, spacing);
43 | return positions.map((pos) => makeCirclePath(pos.x, pos.y, width)).flat();
44 | };
45 |
46 | const lineModelersByType = {
47 | solid: modelSolidLine,
48 | dashed: modelDashedLine,
49 | dotted: modelDottedLine,
50 | };
51 |
52 | const partModelersByType = {
53 | disk: modelDottedLine,
54 | box: modelDashedLine,
55 | wire: modelSolidLine,
56 | };
57 |
58 | export { lineModelersByType, partModelersByType };
59 |
--------------------------------------------------------------------------------
/lib/three/postprocessing/Pass.js:
--------------------------------------------------------------------------------
1 | import {
2 | OrthographicCamera,
3 | PlaneBufferGeometry,
4 | Mesh
5 | } from "../three.module.js";
6 |
7 | function Pass() {
8 |
9 | // if set to true, the pass is processed by the composer
10 | this.enabled = true;
11 |
12 | // if set to true, the pass indicates to swap read and write buffer after rendering
13 | this.needsSwap = true;
14 |
15 | // if set to true, the pass clears its buffer before rendering
16 | this.clear = false;
17 |
18 | // if set to true, the result of the pass is rendered to screen. This is set automatically by EffectComposer.
19 | this.renderToScreen = false;
20 |
21 | }
22 |
23 | Object.assign( Pass.prototype, {
24 |
25 | setSize: function ( /* width, height */ ) {},
26 |
27 | render: function ( /* renderer, writeBuffer, readBuffer, deltaTime, maskActive */ ) {
28 |
29 | console.error( 'THREE.Pass: .render() must be implemented in derived pass.' );
30 |
31 | }
32 |
33 | } );
34 |
35 | // Helper for passes that need to fill the viewport with a single quad.
36 |
37 | Pass.FullScreenQuad = ( function () {
38 |
39 | var camera = new OrthographicCamera( - 1, 1, 1, - 1, 0, 1 );
40 | var geometry = new PlaneBufferGeometry( 2, 2 );
41 |
42 | var FullScreenQuad = function ( material ) {
43 |
44 | this._mesh = new Mesh( geometry, material );
45 |
46 | };
47 |
48 | Object.defineProperty( FullScreenQuad.prototype, 'material', {
49 |
50 | get: function () {
51 |
52 | return this._mesh.material;
53 |
54 | },
55 |
56 | set: function ( value ) {
57 |
58 | this._mesh.material = value;
59 |
60 | }
61 |
62 | } );
63 |
64 | Object.assign( FullScreenQuad.prototype, {
65 |
66 | dispose: function () {
67 |
68 | this._mesh.geometry.dispose();
69 |
70 | },
71 |
72 | render: function ( renderer ) {
73 |
74 | renderer.render( this._mesh, camera );
75 |
76 | }
77 |
78 | } );
79 |
80 | return FullScreenQuad;
81 |
82 | } )();
83 |
84 | export { Pass };
85 |
--------------------------------------------------------------------------------
/js/buildLevel.js:
--------------------------------------------------------------------------------
1 | import { Parser } from "./../lib/expr-eval.js";
2 | import { getDefaultValuesForType, getParseFuncForAttribute, getHoistForType } from "./levelSchemas.js";
3 |
4 | const parser = new Parser();
5 | const evaluateExpression = (expression, scope) => parser.parse(expression).evaluate(scope);
6 | const braced = /^{(.*)}$/;
7 | const evaluateRawValue = (parser, scope, rawValue) => {
8 | if (rawValue == null || rawValue === "") return undefined;
9 | if (typeof rawValue !== "string") return rawValue;
10 |
11 | const expression = rawValue.match(braced)?.[1];
12 | if (expression != null) {
13 | return evaluateExpression(expression, scope);
14 | }
15 |
16 | return parser(rawValue);
17 | };
18 |
19 | const dashed = /(.*?)-([a-zA-Z])/g;
20 | const dashedToCamelCase = (s) => s.replace(dashed, (_, a, b) => a + b.toUpperCase());
21 |
22 | const simplify = (element, parentScope = {}) => {
23 | const type = element.tagName.toLowerCase();
24 |
25 | const rawAttributes = {
26 | ...getDefaultValuesForType(type),
27 | ...Object.fromEntries(Array.from(element.attributes).map(({ name, value }) => [dashedToCamelCase(name), value])),
28 | };
29 |
30 | const attributes = Object.fromEntries(
31 | Object.entries(rawAttributes).map(([name, value]) => [name, evaluateRawValue(getParseFuncForAttribute(type, name), parentScope, value)])
32 | );
33 |
34 | const hoist = getHoistForType(type)?.(attributes);
35 | Object.assign(parentScope, hoist);
36 |
37 | const iteratorId = type === "repeat" ? Object.keys(hoist).pop() : undefined;
38 | const count = type === "repeat" ? hoist[iteratorId] : 1;
39 | const scopes = Array(count)
40 | .fill()
41 | .map((_, index) => ({ ...parentScope, [iteratorId]: index }));
42 | const children = scopes
43 | .map((scope) => Array.from(element.children).map((child) => simplify(child, scope)))
44 | .flat()
45 | .map((child) => (child.type === "repeat" ? child.children : [child]))
46 | .flat();
47 |
48 | return { type, id: attributes.id, attributes, children, ...hoist };
49 | };
50 |
51 | export default (dom) => simplify(dom.querySelector("drivey"));
52 |
--------------------------------------------------------------------------------
/lib/three/postprocessing/RenderPass.js:
--------------------------------------------------------------------------------
1 | /**
2 | * @author alteredq / http://alteredqualia.com/
3 | */
4 |
5 |
6 | import { Pass } from "../postprocessing/Pass.js";
7 |
8 | var RenderPass = function ( scene, camera, overrideMaterial, clearColor, clearAlpha ) {
9 |
10 | Pass.call( this );
11 |
12 | this.scene = scene;
13 | this.camera = camera;
14 |
15 | this.overrideMaterial = overrideMaterial;
16 |
17 | this.clearColor = clearColor;
18 | this.clearAlpha = ( clearAlpha !== undefined ) ? clearAlpha : 0;
19 |
20 | this.clear = true;
21 | this.clearDepth = false;
22 | this.needsSwap = false;
23 |
24 | };
25 |
26 | RenderPass.prototype = Object.assign( Object.create( Pass.prototype ), {
27 |
28 | constructor: RenderPass,
29 |
30 | render: function ( renderer, writeBuffer, readBuffer /*, deltaTime, maskActive */ ) {
31 |
32 | var oldAutoClear = renderer.autoClear;
33 | renderer.autoClear = false;
34 |
35 | var oldClearColor, oldClearAlpha, oldOverrideMaterial;
36 |
37 | if ( this.overrideMaterial !== undefined ) {
38 |
39 | oldOverrideMaterial = this.scene.overrideMaterial;
40 |
41 | this.scene.overrideMaterial = this.overrideMaterial;
42 |
43 | }
44 |
45 | if ( this.clearColor ) {
46 |
47 | oldClearColor = renderer.getClearColor().getHex();
48 | oldClearAlpha = renderer.getClearAlpha();
49 |
50 | renderer.setClearColor( this.clearColor, this.clearAlpha );
51 |
52 | }
53 |
54 | if ( this.clearDepth ) {
55 |
56 | renderer.clearDepth();
57 |
58 | }
59 |
60 | renderer.setRenderTarget( this.renderToScreen ? null : readBuffer );
61 |
62 | // TODO: Avoid using autoClear properties, see https://github.com/mrdoob/three.js/pull/15571#issuecomment-465669600
63 | if ( this.clear ) renderer.clear( renderer.autoClearColor, renderer.autoClearDepth, renderer.autoClearStencil );
64 | renderer.render( this.scene, this.camera );
65 |
66 | if ( this.clearColor ) {
67 |
68 | renderer.setClearColor( oldClearColor, oldClearAlpha );
69 |
70 | }
71 |
72 | if ( this.overrideMaterial !== undefined ) {
73 |
74 | this.scene.overrideMaterial = oldOverrideMaterial;
75 |
76 | }
77 |
78 | renderer.autoClear = oldAutoClear;
79 |
80 | }
81 |
82 | } );
83 |
84 | export { RenderPass };
85 |
--------------------------------------------------------------------------------
/legacy/js/Spectre.js:
--------------------------------------------------------------------------------
1 | "use strict";
2 |
3 | class Spectre extends Level {
4 | build(meshes, transparentMeshes, skyMeshes) {
5 | this.name = "The Deep Dark Night";
6 | this.tint = new THREE.Color(1, 0, 1);
7 | this.skyLow = 0.35;
8 | this.skyHigh = -1;
9 | this.laneWidth = 4;
10 | const postPath = new THREE.ShapePath();
11 | this.drawRoadLine(this.roadPath, postPath, -25, 0.1, RoadLineStyle.DASH(0.1, 27, 0), 0, 1);
12 | this.drawRoadLine(this.roadPath, postPath, -15, 0.1, RoadLineStyle.DASH(0.1, 23, 0), 0, 1);
13 | this.drawRoadLine(this.roadPath, postPath, -5, 0.1, RoadLineStyle.DASH(0.1, 20, 0), 0, 1);
14 | this.drawRoadLine(this.roadPath, postPath, 5, 0.1, RoadLineStyle.DASH(0.1, 20, 0), 0, 1);
15 | this.drawRoadLine(this.roadPath, postPath, 15, 0.1, RoadLineStyle.DASH(0.1, 23, 0), 0, 1);
16 | this.drawRoadLine(this.roadPath, postPath, 25, 0.1, RoadLineStyle.DASH(0.1, 27, 0), 0, 1);
17 | meshes.push(makeMesh(postPath, 0.1, 0, 1));
18 |
19 | const dotsPath = new THREE.ShapePath();
20 | const mag = 0.3;
21 | const width = 30 * mag;
22 | const radius = 1500 * mag;
23 | const approximation = this.roadPath.approximate();
24 | let x = -radius;
25 | while (x < radius) {
26 | let y = -radius;
27 | while (y < radius) {
28 | const pos = new THREE.Vector2(x, y);
29 | if (pos.length() < radius && distance(approximation.getNearestPoint(pos), pos) > 30 * mag) {
30 | addPath(dotsPath, makeRectanglePath(pos.x + -width / 2, pos.y + -width / 2, width, width));
31 | }
32 | y += 1000 * mag;
33 | }
34 | x += 200 * mag;
35 | }
36 |
37 | meshes.push(makeMesh(dotsPath, width * 1.25, 1, 0.5));
38 |
39 | const signpostsPath = new THREE.ShapePath();
40 | this.drawRoadLine(this.roadPath, signpostsPath, -12, 0.2, RoadLineStyle.DASH(0.2, 400, 0), 0, 1);
41 | this.drawRoadLine(this.roadPath, signpostsPath, 12, 0.2, RoadLineStyle.DASH(0.2, 300, 0), 0, 1);
42 | meshes.push(makeMesh(signpostsPath, 14, 0, 0.9));
43 |
44 | const signsPath = new THREE.ShapePath();
45 | this.drawRoadLine(this.roadPath, signsPath, -15, 6, RoadLineStyle.DASH(0.2, 400, 0), 0, 1);
46 | this.drawRoadLine(this.roadPath, signsPath, 15, 6, RoadLineStyle.DASH(0.2, 300, 0), 0, 1);
47 | const signsMesh = makeMesh(signsPath, 4, 0, 0.9);
48 | signsMesh.position.z = 10;
49 | meshes.push(signsMesh);
50 | }
51 | }
52 |
--------------------------------------------------------------------------------
/js/math.js:
--------------------------------------------------------------------------------
1 | import { Vector2, Vector3 } from "./../lib/three/three.module.js";
2 |
3 | const [PI, TWO_PI] = [Math.PI, Math.PI * 2];
4 |
5 | const sign = (input) => (input < 0 ? -1 : 1);
6 |
7 | const fract = (n) => ((n % 1) + 1) % 1;
8 |
9 | const getAngle = (v2) => Math.atan2(v2.y, v2.x);
10 |
11 | const lerp = (from, to, amount) => from * (1 - amount) + to * amount;
12 |
13 | const distanceSquared = (v1, v2) => (v1.x - v2.x) ** 2 + (v1.y - v2.y) ** 2;
14 |
15 | const distance = (v1, v2) => Math.sqrt(distanceSquared(v1, v2));
16 |
17 | const modAngle = (angle) => (angle % TWO_PI) * sign(angle);
18 |
19 | const origin = new Vector2();
20 |
21 | const unitVector = new Vector2(1, 0);
22 |
23 | const intMax = Math.pow(2, 32);
24 |
25 | const lcg = (seed) => (seed * 1664525 + 1013904223) % intMax;
26 |
27 | const rotate = (v2, angle) => {
28 | const cos = Math.cos(angle);
29 | const sin = Math.sin(angle);
30 | return new Vector2(v2.x * cos - v2.y * sin, v2.x * sin + v2.y * cos);
31 | };
32 |
33 | const rotateY = (v3, angle) => {
34 | const cos = Math.cos(angle);
35 | const sin = Math.sin(angle);
36 | return new Vector3(v3.x * cos - v3.z * sin, v3.y, v3.x * sin + v3.z * cos);
37 | };
38 |
39 | const closestPointIndex = (points, toPoint) => {
40 | // const distancesSquared = points.map(point => distanceSquared(point, toPoint));
41 | // const minimum = Math.min(...distancesSquared);
42 | // return distancesSquared.indexOf(minimum);
43 |
44 | let minimum = Infinity;
45 | let minimumPoint = -1;
46 | const numPoints = points.length;
47 | for (let i = 0; i < numPoints; i++) {
48 | const point = points[i];
49 | const distSquared = (toPoint.x - point.x) ** 2 + (toPoint.y - point.y) ** 2;
50 | if (minimum > distSquared) {
51 | minimum = distSquared;
52 | minimumPoint = i;
53 | }
54 | }
55 | return minimumPoint;
56 | };
57 |
58 | const modDiffAngle = (angle1, angle2) => {
59 | let diffAngle = modAngle(angle1) - modAngle(angle2);
60 | if (diffAngle > PI) diffAngle -= TWO_PI;
61 | if (diffAngle < -PI) diffAngle += TWO_PI;
62 | return diffAngle;
63 | };
64 |
65 | const sanitize = (value, defaultValue) => (isNaN(value) ? defaultValue : value);
66 |
67 | export {
68 | PI,
69 | TWO_PI,
70 | sign,
71 | fract,
72 | getAngle,
73 | lerp,
74 | distanceSquared,
75 | distance,
76 | modAngle,
77 | rotate,
78 | rotateY,
79 | closestPointIndex,
80 | modDiffAngle,
81 | origin,
82 | unitVector,
83 | lcg,
84 | intMax,
85 | sanitize,
86 | };
87 |
--------------------------------------------------------------------------------
/legacy/js/Tunnel.js:
--------------------------------------------------------------------------------
1 | "use strict";
2 |
3 | class Tunnel extends Level {
4 | build(meshes, transparentMeshes, skyMeshes) {
5 | this.name = "The Tunnel";
6 | this.tint = new THREE.Color(0.2, 0.7, 0.1);
7 | this.laneWidth = 3.5;
8 | const tarmac = 0.1; // 0.1
9 | const whiteLinesColor = 0.8;
10 | const lightColor = 1;
11 | const wallColor = 0;
12 |
13 | const tarmacPath = new THREE.ShapePath();
14 | this.drawRoadLine(this.roadPath, tarmacPath, 0, 7, RoadLineStyle.SOLID(5), 0, 1);
15 | const tarmacMesh = makeMesh(tarmacPath, 0, 1, tarmac);
16 | meshes.push(tarmacMesh);
17 |
18 | // do white lines
19 | const roadLinesPath = new THREE.ShapePath();
20 | this.drawRoadLine(this.roadPath, roadLinesPath, -3.5, 0.2, RoadLineStyle.DASH(30, 2, 5), 0, 1);
21 | this.drawRoadLine(this.roadPath, roadLinesPath, 3.5, 0.2, RoadLineStyle.DASH(30, 2, 5), 0, 1);
22 | this.drawRoadLine(this.roadPath, roadLinesPath, -0.15, 0.15, RoadLineStyle.DASH(4, 8, 0), 0, 1);
23 | const roadLinesMesh = makeMesh(roadLinesPath, 0, 1, whiteLinesColor, 1, 1);
24 | roadLinesMesh.position.z = 0.1;
25 | meshes.push(roadLinesMesh);
26 |
27 | // do crossings
28 | const crossingPath = new THREE.ShapePath();
29 | this.drawRoadLine(this.roadPath, crossingPath, 0, 1, RoadLineStyle.DASH(2, 200, 0), 0, 1);
30 | const crossingMesh = makeMesh(crossingPath, 0, 1, tarmac);
31 | crossingMesh.position.z = 0.001;
32 | meshes.push(crossingMesh);
33 | const crossingLinesPath = new THREE.ShapePath();
34 | for (let i = 0; i < 6; i++) {
35 | const width = (6.0 / 6) * 0.5;
36 | this.drawRoadLine(this.roadPath, crossingLinesPath, i * 2 * width - 3 + width, width, RoadLineStyle.DASH(2, 200, 0), 0, 1);
37 | }
38 | const crossingLinesMesh = makeMesh(crossingLinesPath, 0, 1, whiteLinesColor, 1, 1);
39 | crossingLinesMesh.position.z = 0.01;
40 | meshes.push(crossingLinesMesh);
41 |
42 | // do lights
43 | const lightsPath = new THREE.ShapePath();
44 | this.drawRoadLine(this.roadPath, lightsPath, -4, 0.1, RoadLineStyle.DASH(4, 6, 0), 0, 1);
45 | this.drawRoadLine(this.roadPath, lightsPath, 4, 0.1, RoadLineStyle.DASH(4, 6, 0), 0, 1);
46 | const lightsMesh = makeMesh(lightsPath, 0.1, 1, lightColor);
47 | meshes.push(lightsMesh);
48 | lightsMesh.position.z = 4;
49 |
50 | // do walls
51 | const wallPath = new THREE.ShapePath();
52 | this.drawRoadLine(this.roadPath, wallPath, -5, 0.4, RoadLineStyle.SOLID(5), 0, 1);
53 | this.drawRoadLine(this.roadPath, wallPath, 5, 0.4, RoadLineStyle.SOLID(5), 0, 1);
54 | const wallMesh = makeMesh(wallPath, 4, 1, wallColor, 1, 2);
55 | meshes.push(wallMesh);
56 | }
57 | }
58 |
--------------------------------------------------------------------------------
/readme_assets/extrusion_1.svg:
--------------------------------------------------------------------------------
1 |
13 |
--------------------------------------------------------------------------------
/lib/three/shaders/SobelOperatorShader.js:
--------------------------------------------------------------------------------
1 | /**
2 | * @author Mugen87 / https://github.com/Mugen87
3 | *
4 | * Sobel Edge Detection (see https://youtu.be/uihBwtPIBxM)
5 | *
6 | * As mentioned in the video the Sobel operator expects a grayscale image as input.
7 | *
8 | */
9 |
10 | import {
11 | Vector2
12 | } from "../three.module.js";
13 |
14 | var SobelOperatorShader = {
15 |
16 | uniforms: {
17 |
18 | "tDiffuse": { value: null },
19 | "resolution": { value: new Vector2() }
20 |
21 | },
22 |
23 | vertexShader: [
24 |
25 | "varying vec2 vUv;",
26 |
27 | "void main() {",
28 |
29 | " vUv = uv;",
30 |
31 | " gl_Position = projectionMatrix * modelViewMatrix * vec4( position, 1.0 );",
32 |
33 | "}"
34 |
35 | ].join( "\n" ),
36 |
37 | fragmentShader: [
38 |
39 | "uniform sampler2D tDiffuse;",
40 | "uniform vec2 resolution;",
41 | "varying vec2 vUv;",
42 |
43 | "void main() {",
44 |
45 | " vec2 texel = vec2( 1.0 / resolution.x, 1.0 / resolution.y );",
46 |
47 | // kernel definition (in glsl matrices are filled in column-major order)
48 |
49 | " const mat3 Gx = mat3( -1, -2, -1, 0, 0, 0, 1, 2, 1 );", // x direction kernel
50 | " const mat3 Gy = mat3( -1, 0, 1, -2, 0, 2, -1, 0, 1 );", // y direction kernel
51 |
52 | // fetch the 3x3 neighbourhood of a fragment
53 |
54 | // first column
55 |
56 | " float tx0y0 = texture2D( tDiffuse, vUv + texel * vec2( -1, -1 ) ).r;",
57 | " float tx0y1 = texture2D( tDiffuse, vUv + texel * vec2( -1, 0 ) ).r;",
58 | " float tx0y2 = texture2D( tDiffuse, vUv + texel * vec2( -1, 1 ) ).r;",
59 |
60 | // second column
61 |
62 | " float tx1y0 = texture2D( tDiffuse, vUv + texel * vec2( 0, -1 ) ).r;",
63 | " float tx1y1 = texture2D( tDiffuse, vUv + texel * vec2( 0, 0 ) ).r;",
64 | " float tx1y2 = texture2D( tDiffuse, vUv + texel * vec2( 0, 1 ) ).r;",
65 |
66 | // third column
67 |
68 | " float tx2y0 = texture2D( tDiffuse, vUv + texel * vec2( 1, -1 ) ).r;",
69 | " float tx2y1 = texture2D( tDiffuse, vUv + texel * vec2( 1, 0 ) ).r;",
70 | " float tx2y2 = texture2D( tDiffuse, vUv + texel * vec2( 1, 1 ) ).r;",
71 |
72 | // gradient value in x direction
73 |
74 | " float valueGx = Gx[0][0] * tx0y0 + Gx[1][0] * tx1y0 + Gx[2][0] * tx2y0 + ",
75 | " Gx[0][1] * tx0y1 + Gx[1][1] * tx1y1 + Gx[2][1] * tx2y1 + ",
76 | " Gx[0][2] * tx0y2 + Gx[1][2] * tx1y2 + Gx[2][2] * tx2y2; ",
77 |
78 | // gradient value in y direction
79 |
80 | " float valueGy = Gy[0][0] * tx0y0 + Gy[1][0] * tx1y0 + Gy[2][0] * tx2y0 + ",
81 | " Gy[0][1] * tx0y1 + Gy[1][1] * tx1y1 + Gy[2][1] * tx2y1 + ",
82 | " Gy[0][2] * tx0y2 + Gy[1][2] * tx1y2 + Gy[2][2] * tx2y2; ",
83 |
84 | // magnitute of the total gradient
85 |
86 | " float G = sqrt( ( valueGx * valueGx ) + ( valueGy * valueGy ) );",
87 |
88 | " gl_FragColor = vec4( vec3( G ), 1 );",
89 |
90 | "}"
91 |
92 | ].join( "\n" )
93 |
94 | };
95 |
96 | export { SobelOperatorShader };
97 |
--------------------------------------------------------------------------------
/js/renderLevel.js:
--------------------------------------------------------------------------------
1 | import { Group, BufferGeometry, BufferAttribute } from "./../lib/three/three.module.js";
2 | import { Road } from "./roads.js";
3 | import { getTriangleCount, makeMesh } from "./geometry.js";
4 | import modelLevel from "./modelLevel.js";
5 |
6 | const rehydrate = (attributeData) => {
7 | const geometry = new BufferGeometry();
8 | Object.keys(attributeData).forEach((name) => {
9 | const { array, itemSize } = attributeData[name];
10 | if (itemSize > 0) {
11 | geometry.setAttribute(name, new BufferAttribute(array, itemSize));
12 | }
13 | });
14 | return geometry;
15 | };
16 |
17 | // const worker = new Worker("./js/worker.js", {type: "module"});
18 | // worker.addEventListener("error", event => console.log(event));
19 |
20 | const modelLevelAsync = (levelData) =>
21 | new Promise((resolve) => {
22 | resolve(modelLevel(levelData));
23 | /*
24 | const uid = Math.floor(Math.random() * 0xFFFFFFFF);
25 | const handler = ({data}) => {
26 | if (data.uid === uid) {
27 | worker.removeEventListener("message", handler);
28 | resolve(data);
29 | }
30 | };
31 | worker.addEventListener("message", handler);
32 | worker.postMessage({ ...levelData, uid });
33 | */
34 | });
35 |
36 | export default async (levelData) => {
37 | console.dir(levelData.attributes.name, levelData);
38 | console.time("Modeling " + levelData.attributes.name);
39 | const models = await modelLevelAsync(levelData);
40 | console.timeEnd("Modeling " + levelData.attributes.name);
41 |
42 | const opaqueGeometry = rehydrate(models.opaqueGeometry);
43 | const transparentGeometry = rehydrate(models.transparentGeometry);
44 | const skyGeometry = rehydrate(models.skyGeometry);
45 | const roadsById = Object.fromEntries(Object.entries(models.roadsById).map(([id, points]) => [id, new Road(points)]));
46 |
47 | opaqueGeometry.computeBoundingSphere();
48 | const worldRadius = opaqueGeometry.boundingSphere.radius;
49 | const world = new Group();
50 |
51 | const opaqueCount = getTriangleCount(opaqueGeometry);
52 | if (opaqueCount > 0) {
53 | world.add(makeMesh(opaqueGeometry, false));
54 | console.log(opaqueCount, "opaque triangles");
55 | }
56 |
57 | const transparentCount = getTriangleCount(transparentGeometry);
58 | if (transparentCount > 0) {
59 | world.add(makeMesh(transparentGeometry, true));
60 | console.log(transparentCount, "transparent triangles");
61 | }
62 |
63 | const sky = new Group();
64 | const skyCount = getTriangleCount(skyGeometry);
65 | if (skyCount > 0) {
66 | sky.add(makeMesh(skyGeometry, false));
67 | console.log(skyCount, "sky triangles");
68 | }
69 |
70 | const dispose = () => {
71 | world.parent?.remove(world);
72 | world.children.forEach((child) => child.geometry.dispose());
73 | };
74 |
75 | return {
76 | ...levelData.attributes,
77 | ...roadsById,
78 | world,
79 | sky,
80 | worldRadius,
81 | dispose,
82 | };
83 | };
84 |
--------------------------------------------------------------------------------
/lib/three/postprocessing/MaskPass.js:
--------------------------------------------------------------------------------
1 | /**
2 | * @author alteredq / http://alteredqualia.com/
3 | */
4 |
5 |
6 | import { Pass } from "../postprocessing/Pass.js";
7 |
8 | var MaskPass = function ( scene, camera ) {
9 |
10 | Pass.call( this );
11 |
12 | this.scene = scene;
13 | this.camera = camera;
14 |
15 | this.clear = true;
16 | this.needsSwap = false;
17 |
18 | this.inverse = false;
19 |
20 | };
21 |
22 | MaskPass.prototype = Object.assign( Object.create( Pass.prototype ), {
23 |
24 | constructor: MaskPass,
25 |
26 | render: function ( renderer, writeBuffer, readBuffer /*, deltaTime, maskActive */ ) {
27 |
28 | var context = renderer.getContext();
29 | var state = renderer.state;
30 |
31 | // don't update color or depth
32 |
33 | state.buffers.color.setMask( false );
34 | state.buffers.depth.setMask( false );
35 |
36 | // lock buffers
37 |
38 | state.buffers.color.setLocked( true );
39 | state.buffers.depth.setLocked( true );
40 |
41 | // set up stencil
42 |
43 | var writeValue, clearValue;
44 |
45 | if ( this.inverse ) {
46 |
47 | writeValue = 0;
48 | clearValue = 1;
49 |
50 | } else {
51 |
52 | writeValue = 1;
53 | clearValue = 0;
54 |
55 | }
56 |
57 | state.buffers.stencil.setTest( true );
58 | state.buffers.stencil.setOp( context.REPLACE, context.REPLACE, context.REPLACE );
59 | state.buffers.stencil.setFunc( context.ALWAYS, writeValue, 0xffffffff );
60 | state.buffers.stencil.setClear( clearValue );
61 | state.buffers.stencil.setLocked( true );
62 |
63 | // draw into the stencil buffer
64 |
65 | renderer.setRenderTarget( readBuffer );
66 | if ( this.clear ) renderer.clear();
67 | renderer.render( this.scene, this.camera );
68 |
69 | renderer.setRenderTarget( writeBuffer );
70 | if ( this.clear ) renderer.clear();
71 | renderer.render( this.scene, this.camera );
72 |
73 | // unlock color and depth buffer for subsequent rendering
74 |
75 | state.buffers.color.setLocked( false );
76 | state.buffers.depth.setLocked( false );
77 |
78 | // only render where stencil is set to 1
79 |
80 | state.buffers.stencil.setLocked( false );
81 | state.buffers.stencil.setFunc( context.EQUAL, 1, 0xffffffff ); // draw if == 1
82 | state.buffers.stencil.setOp( context.KEEP, context.KEEP, context.KEEP );
83 | state.buffers.stencil.setLocked( true );
84 |
85 | }
86 |
87 | } );
88 |
89 |
90 | var ClearMaskPass = function () {
91 |
92 | Pass.call( this );
93 |
94 | this.needsSwap = false;
95 |
96 | };
97 |
98 | ClearMaskPass.prototype = Object.create( Pass.prototype );
99 |
100 | Object.assign( ClearMaskPass.prototype, {
101 |
102 | render: function ( renderer /*, writeBuffer, readBuffer, deltaTime, maskActive */ ) {
103 |
104 | renderer.state.buffers.stencil.setLocked( false );
105 | renderer.state.buffers.stencil.setTest( false );
106 |
107 | }
108 |
109 | } );
110 |
111 | export { MaskPass, ClearMaskPass };
112 |
--------------------------------------------------------------------------------
/legacy/js/CliffsideBeach.js:
--------------------------------------------------------------------------------
1 | "use strict";
2 |
3 | class CliffsideBeach extends Level {
4 | build(meshes, transparentMeshes, skyMeshes) {
5 | this.tint = new THREE.Color(0, 0.7, 0.7);
6 | this.skyLow = 0.8;
7 | this.skyHigh = 1.8;
8 | this.ground = 0.4;
9 | this.laneWidth = 3;
10 | this.numLanes = 2;
11 |
12 | const beach = 0.9;
13 | const tarmac = 0;
14 | const cliffs = 0.3;
15 | const waves = 1;
16 |
17 | const beachPath = new THREE.ShapePath();
18 | this.drawRoadLine(this.roadPath, beachPath, 0, 25, RoadLineStyle.SOLID(50), 0, 1);
19 | this.drawRoadLine(this.roadPath, beachPath, 0, 28, RoadLineStyle.DASH(10, 10, 0), 0, 1);
20 | this.drawRoadLine(this.roadPath, beachPath, -20, 20, RoadLineStyle.SOLID(50), 0, 1);
21 | const beachMesh = makeMesh(beachPath, 0.1, 1, beach);
22 | beachMesh.position.z = -0.3;
23 | meshes.push(beachMesh);
24 |
25 | const wavesPath = new THREE.ShapePath();
26 | this.drawRoadLine(this.roadPath, wavesPath, 25, 10, RoadLineStyle.DASH(10, 100, 0), 0, 1);
27 | this.drawRoadLine(this.roadPath, wavesPath, 35, 5, RoadLineStyle.DASH(10, 50, 0), 0, 1);
28 | this.drawRoadLine(this.roadPath, wavesPath, 45, 5, RoadLineStyle.DASH(20, 70, 0), 0, 1);
29 | const wavesMesh = makeMesh(wavesPath, 0, 1, waves);
30 | wavesMesh.position.z = 0.5;
31 | meshes.push(wavesMesh);
32 |
33 | const cliffsPath1 = new THREE.ShapePath();
34 | this.drawRoadLine(this.roadPath, cliffsPath1, -25, 20, RoadLineStyle.DASH(6, 6, 0), 0, 1);
35 | this.drawRoadLine(this.roadPath, cliffsPath1, -27, 20, RoadLineStyle.SOLID(50), 0, 1);
36 | const cliffsMesh1 = makeMesh(cliffsPath1, 10, 3, cliffs + 0.0);
37 | meshes.push(cliffsMesh1);
38 |
39 | const cliffsPath2 = new THREE.ShapePath();
40 | this.drawRoadLine(this.roadPath, cliffsPath2, -30, 20, RoadLineStyle.DASH(6, 12, 0), 0, 1);
41 | const cliffsMesh2 = makeMesh(cliffsPath2, 18, 3, cliffs + 0.05);
42 | meshes.push(cliffsMesh2);
43 |
44 | const tarmacPath = new THREE.ShapePath();
45 | this.drawRoadLine(this.roadPath, tarmacPath, 0, 14, RoadLineStyle.SOLID(4), 0, 1);
46 | const tarmacMesh = makeMesh(tarmacPath, 0, 100, tarmac);
47 | meshes.push(tarmacMesh);
48 |
49 | const roadLinesPath = new THREE.ShapePath();
50 | this.drawRoadLine(this.roadPath, roadLinesPath, 0, 0.1, RoadLineStyle.SOLID(5), 0, 1);
51 | this.drawRoadLine(this.roadPath, roadLinesPath, 0.2, 0.1, RoadLineStyle.SOLID(5), 0, 1);
52 | this.drawRoadLine(this.roadPath, roadLinesPath, -6, 0.15, RoadLineStyle.DASH(30, 1, 5), 0, 1);
53 | this.drawRoadLine(this.roadPath, roadLinesPath, 6, 0.15, RoadLineStyle.DASH(30, 1, 5), 0, 1);
54 | this.drawRoadLine(this.roadPath, roadLinesPath, -3, 0.15, RoadLineStyle.DASH(3, 12, 0), 0, 1);
55 | this.drawRoadLine(this.roadPath, roadLinesPath, 3, 0.15, RoadLineStyle.DASH(3, 12, 0), 0, 1);
56 | const roadLinesMesh = makeMesh(roadLinesPath, 0, 1, 1);
57 | roadLinesMesh.position.z = 0.1;
58 | meshes.push(roadLinesMesh);
59 | }
60 | }
61 |
--------------------------------------------------------------------------------
/js/geometry.js:
--------------------------------------------------------------------------------
1 | import { Float32BufferAttribute, Mesh, BufferGeometry, ExtrudeBufferGeometry } from "./../lib/three/three.module.js";
2 | import { BufferGeometryUtils } from "./../lib/three/utils/BufferGeometryUtils.js";
3 | import { silhouetteMaterial, transparentMaterial } from "./materials.js";
4 |
5 | const makeGeometry = (source, height, shade = 0, alpha = 1, fade = 0) => {
6 | const geom = new ExtrudeBufferGeometry(source.toShapes(false, false), {
7 | depth: Math.max(Math.abs(height), 0.0000001),
8 | curveSegments: 10,
9 | bevelEnabled: false,
10 | });
11 | geom.deleteAttribute("uv");
12 | geom.deleteAttribute("normal");
13 | shadeGeometry(geom, shade, alpha, fade);
14 | idGeometry(geom);
15 | bulgeGeometry(geom);
16 | return geom;
17 | };
18 |
19 | const getTriangleCount = (geometry) => (geometry.getAttribute("position")?.count ?? 0) / 3;
20 |
21 | const bulgeGeometry = (geometry) => {
22 | const positions = geometry.getAttribute("position");
23 | const numVertices = positions.count;
24 | const bulgeDirections = [];
25 | for (let i = 0; i < numVertices; i++) {
26 | const z = positions.array[i * 3 + 2];
27 | bulgeDirections.push(z <= 0 ? -1 : 1);
28 | }
29 |
30 | geometry.setAttribute("bulgeDirection", new Float32BufferAttribute(bulgeDirections, 1));
31 | return geometry;
32 | };
33 |
34 | const shadeGeometry = (geometry, shade, alpha = 1, fade = 0) => {
35 | const numVertices = geometry.getAttribute("position").count;
36 | const monochromeValues = [];
37 | for (let i = 0; i < numVertices; i++) {
38 | monochromeValues.push(shade);
39 | monochromeValues.push(alpha);
40 | monochromeValues.push(fade);
41 | }
42 |
43 | geometry.setAttribute("monochromeValue", new Float32BufferAttribute(monochromeValues, 3));
44 | return geometry;
45 | };
46 |
47 | let [idRed, idGreen, idBlue] = [0, 0, 0];
48 |
49 | const idGeometry = (geometry) => {
50 | const numVertices = geometry.getAttribute("position").count;
51 | idRed = (idRed + 0x23) % 0xff;
52 | idGreen = (idGreen + 0x67) % 0xff;
53 | idBlue = (idBlue + 0xac) % 0xff;
54 | const idValues = [];
55 | for (let i = 0; i < numVertices; i++) {
56 | idValues.push(idRed / 0xff);
57 | idValues.push(idGreen / 0xff);
58 | idValues.push(idBlue / 0xff);
59 | }
60 |
61 | geometry.setAttribute("idColor", new Float32BufferAttribute(idValues, 3));
62 | return geometry;
63 | };
64 |
65 | const mergeGeometries = (geometries, dispose = true) => {
66 | if (geometries.length === 0) return new BufferGeometry();
67 |
68 | const numIndexed = geometries.filter((geometry) => geometry.index != null).length;
69 | if (numIndexed > 0 && numIndexed < geometries.length) {
70 | throw new Error("You can't merge indexed and non-indexed buffer geometries.");
71 | }
72 |
73 | const merged = BufferGeometryUtils.mergeBufferGeometries(geometries);
74 | if (dispose) geometries.forEach((geom) => geom.dispose());
75 | return merged;
76 | };
77 |
78 | const makeMesh = (geom, isTransparent = false) => new Mesh(geom, isTransparent ? transparentMaterial : silhouetteMaterial);
79 |
80 | export { makeGeometry, getTriangleCount, shadeGeometry, bulgeGeometry, idGeometry, mergeGeometries, makeMesh };
81 |
--------------------------------------------------------------------------------
/legacy/lib/SobelOperatorShader.js:
--------------------------------------------------------------------------------
1 | (function(root, factory) {
2 | if (typeof define === 'function' && define.amd) {
3 | define('three.SobelOperatorShader', ['three'], factory);
4 | }
5 | else if ('undefined' !== typeof exports && 'undefined' !== typeof module) {
6 | module.exports = factory(require('three'));
7 | }
8 | else {
9 | factory(root.THREE);
10 | }
11 | }(this, function(THREE) {
12 |
13 | /**
14 | * @author Mugen87 / https://github.com/Mugen87
15 | *
16 | * Sobel Edge Detection (see https://youtu.be/uihBwtPIBxM)
17 | *
18 | * As mentioned in the video the Sobel operator expects a grayscale image as input.
19 | *
20 | */
21 |
22 | THREE.SobelOperatorShader = {
23 |
24 | uniforms: {
25 |
26 | "tDiffuse": { value: null },
27 | "resolution": { value: new THREE.Vector2() }
28 |
29 | },
30 |
31 | vertexShader: [
32 |
33 | "varying vec2 vUv;",
34 |
35 | "void main() {",
36 |
37 | "vUv = uv;",
38 |
39 | "gl_Position = projectionMatrix * modelViewMatrix * vec4( position, 1.0 );",
40 |
41 | "}"
42 |
43 | ].join( "\n" ),
44 |
45 | fragmentShader: [
46 |
47 | "uniform sampler2D tDiffuse;",
48 | "uniform vec2 resolution;",
49 | "varying vec2 vUv;",
50 |
51 | "void main() {",
52 |
53 | "vec2 texel = vec2( 1.0 / resolution.x, 1.0 / resolution.y );",
54 |
55 | // kernel definition (in glsl matrices are filled in column-major order)
56 |
57 | "const mat3 Gx = mat3( -1, -2, -1, 0, 0, 0, 1, 2, 1 );", // x direction kernel
58 | "const mat3 Gy = mat3( -1, 0, 1, -2, 0, 2, -1, 0, 1 );", // y direction kernel
59 |
60 | // fetch the 3x3 neighbourhood of a fragment
61 |
62 | // first column
63 |
64 | "float tx0y0 = texture2D( tDiffuse, vUv + texel * vec2( -1, -1 ) ).r;",
65 | "float tx0y1 = texture2D( tDiffuse, vUv + texel * vec2( -1, 0 ) ).r;",
66 | "float tx0y2 = texture2D( tDiffuse, vUv + texel * vec2( -1, 1 ) ).r;",
67 |
68 | // second column
69 |
70 | "float tx1y0 = texture2D( tDiffuse, vUv + texel * vec2( 0, -1 ) ).r;",
71 | "float tx1y1 = texture2D( tDiffuse, vUv + texel * vec2( 0, 0 ) ).r;",
72 | "float tx1y2 = texture2D( tDiffuse, vUv + texel * vec2( 0, 1 ) ).r;",
73 |
74 | // third column
75 |
76 | "float tx2y0 = texture2D( tDiffuse, vUv + texel * vec2( 1, -1 ) ).r;",
77 | "float tx2y1 = texture2D( tDiffuse, vUv + texel * vec2( 1, 0 ) ).r;",
78 | "float tx2y2 = texture2D( tDiffuse, vUv + texel * vec2( 1, 1 ) ).r;",
79 |
80 | // gradient value in x direction
81 |
82 | "float valueGx = Gx[0][0] * tx0y0 + Gx[1][0] * tx1y0 + Gx[2][0] * tx2y0 + ",
83 | "Gx[0][1] * tx0y1 + Gx[1][1] * tx1y1 + Gx[2][1] * tx2y1 + ",
84 | "Gx[0][2] * tx0y2 + Gx[1][2] * tx1y2 + Gx[2][2] * tx2y2; ",
85 |
86 | // gradient value in y direction
87 |
88 | "float valueGy = Gy[0][0] * tx0y0 + Gy[1][0] * tx1y0 + Gy[2][0] * tx2y0 + ",
89 | "Gy[0][1] * tx0y1 + Gy[1][1] * tx1y1 + Gy[2][1] * tx2y1 + ",
90 | "Gy[0][2] * tx0y2 + Gy[1][2] * tx1y2 + Gy[2][2] * tx2y2; ",
91 |
92 | // magnitute of the total gradient
93 |
94 | "float G = sqrt( ( valueGx * valueGx ) + ( valueGy * valueGy ) );",
95 |
96 | "gl_FragColor = vec4( vec3( G ), 1 );",
97 |
98 | "}"
99 |
100 | ].join( "\n" )
101 |
102 | };
103 | }));
104 |
--------------------------------------------------------------------------------
/legacy/js/TrainTracks.js:
--------------------------------------------------------------------------------
1 | "use strict";
2 |
3 | class TrainTracks extends Level {
4 | build(meshes, transparentMeshes, skyMeshes) {
5 | this.name = "The Indian Pacific";
6 | this.tint = new THREE.Color(0.8, 0.4, 0.2);
7 | this.skyHigh = 0.9;
8 | this.skyLow = 1.0; // 1.0
9 | this.skyGradient = 0.25;
10 | this.ground = 0.5;
11 | this.roadPath.scale(1/2, 2);
12 | this.cruiseSpeed = 10;
13 | this.laneWidth = 5;
14 |
15 | const railColor = 0.4;
16 | const sleeperColor = 0.45;
17 | const ballastColor = 0.6;
18 |
19 | // sky
20 | const cloudsPath = new THREE.ShapePath();
21 | for (let i = 0; i < 200; i++) {
22 | const pos = new THREE.Vector2(Math.random() - 0.5, Math.random() - 0.5);
23 | if (pos.length() > 0.9 || pos.length() < 0.3) { // 0.5, 0.1
24 | continue;
25 | }
26 | pos.multiplyScalar(1000);
27 |
28 | // TODO: more efficient distance test
29 | // if ((pos - cloudsPath.getNearestPoint(pos)).length() < 200) continue;
30 |
31 | addPath(cloudsPath, makeCirclePath(pos.x, pos.y, 50));
32 | }
33 | const cloudsMesh = makeMesh(cloudsPath, 1, 200, (this.skyLow + this.skyHigh) / 2);
34 | cloudsMesh.scale.multiplyScalar(2);
35 | cloudsMesh.position.z = 80;
36 | skyMeshes.push(cloudsMesh);
37 |
38 | const rails = new THREE.ShapePath();
39 | this.drawRoadLine(this.roadPath, rails, -2.5 - 0.6, 0.15, RoadLineStyle.SOLID(5), 0, 1);
40 | this.drawRoadLine(this.roadPath, rails, -2.5 + 0.6, 0.15, RoadLineStyle.SOLID(5), 0, 1);
41 | this.drawRoadLine(this.roadPath, rails, 2.5 - 0.6, 0.15, RoadLineStyle.SOLID(5), 0, 1);
42 | this.drawRoadLine(this.roadPath, rails, 2.5 + 0.6, 0.15, RoadLineStyle.SOLID(5), 0, 1);
43 | meshes.push(makeMesh(rails, 0.125, 1, railColor, 1));
44 |
45 | const sleepers = new THREE.ShapePath();
46 | this.drawRoadLine(this.roadPath, sleepers, -2.5, 1.75, RoadLineStyle.DASH(0.5, 1.5, 0), 0, 1);
47 | this.drawRoadLine(this.roadPath, sleepers, 2.5, 1.75, RoadLineStyle.DASH(0.5, 1.5, 0), 0, 1);
48 | const sleeperMesh = makeMesh(sleepers, 0.12, 1, sleeperColor, 1);
49 | sleeperMesh.position.z = -0.125;
50 | meshes.push(sleeperMesh);
51 |
52 | const ballast = new THREE.ShapePath();
53 | this.drawRoadLine(this.roadPath, ballast, -2.5, 2.5, RoadLineStyle.SOLID(5), 0, 1);
54 | this.drawRoadLine(this.roadPath, ballast, 2.5, 2.5, RoadLineStyle.SOLID(5), 0, 1);
55 | const ballastMesh = makeMesh(ballast, 0, 1, ballastColor, 1);
56 | ballastMesh.position.z = -0.125;
57 | meshes.push(ballastMesh);
58 |
59 | const lightFeatures = new THREE.ShapePath();
60 | this.drawRoadLine(this.roadPath, lightFeatures, -20, 8, RoadLineStyle.DOT(200/2), 0, 1);
61 | this.drawRoadLine(this.roadPath, lightFeatures, -12, 8, RoadLineStyle.DOT(1500/2), 0, 1);
62 | this.drawRoadLine(this.roadPath, lightFeatures, 20, 8, RoadLineStyle.DOT(140/2), 0, 1);
63 | this.drawRoadLine(this.roadPath, lightFeatures, 24, 8, RoadLineStyle.DOT(220/2), 0, 1);
64 | const lightFeaturesMesh = makeMesh(lightFeatures, 0, 12, this.ground + 0.1, 1);
65 | lightFeaturesMesh.position.z = -0.1;
66 | meshes.push(lightFeaturesMesh);
67 |
68 | const darkFeatures = new THREE.ShapePath();
69 | this.drawRoadLine(this.roadPath, darkFeatures, -19, 2, RoadLineStyle.DOT(160/2), 0, 1);
70 | this.drawRoadLine(this.roadPath, darkFeatures, -13, 2, RoadLineStyle.DOT(1200/2), 0, 1);
71 | this.drawRoadLine(this.roadPath, darkFeatures, 15, 2, RoadLineStyle.DOT(110/2), 0, 1);
72 | this.drawRoadLine(this.roadPath, darkFeatures, 19, 2, RoadLineStyle.DOT(180/2), 0, 1);
73 | meshes.push(makeMesh(darkFeatures, 0, 12, this.ground - 0.2, 1));
74 | }
75 | }
76 |
--------------------------------------------------------------------------------
/js/materials.js:
--------------------------------------------------------------------------------
1 | import { RawShaderMaterial, Vector2, Color } from "./../lib/three/three.module.js";
2 |
3 | const blendColors = ({ dark, full, light }, shade) => (shade < 0.5 ? dark.clone().lerp(full, shade * 2.0) : full.clone().lerp(light, shade * 2.0 - 1.0));
4 |
5 | const silhouetteMaterial = new RawShaderMaterial({
6 | uniforms: {
7 | resolution: { type: "v2", value: new Vector2(1, 1) },
8 | },
9 | vertexShader: `
10 | attribute vec3 idColor;
11 | attribute vec3 monochromeValue;
12 | attribute float bulgeDirection;
13 | attribute vec3 position;
14 | uniform bool isWireframe;
15 | uniform mat4 projectionMatrix;
16 | uniform mat4 modelViewMatrix;
17 | uniform vec2 resolution;
18 | varying float vShade;
19 | varying float vOpacity;
20 | varying vec4 vIdColor;
21 | void main() {
22 | float shade = monochromeValue.r;
23 | float fade = monochromeValue.b;
24 | vec4 worldPosition = modelViewMatrix * vec4(position, 1.0);
25 | float screenZ = (projectionMatrix * worldPosition).z;
26 |
27 | shade = clamp(shade - fade * screenZ / resolution.y, 0., 1.);
28 | vShade = shade;
29 | vOpacity = monochromeValue.g;
30 | vIdColor = vec4(idColor, 1.0);
31 |
32 | float bulgeAmount = isWireframe ? 1.5 : 0.9;
33 | worldPosition.y += bulgeDirection * screenZ * bulgeAmount / resolution.y;
34 | gl_Position = projectionMatrix * worldPosition;
35 | }
36 | `,
37 |
38 | fragmentShader: `
39 | precision mediump float;
40 | #define PI 3.14159265359
41 |
42 | uniform vec3 fullTint;
43 | uniform vec3 darkTint;
44 | uniform vec3 lightTint;
45 | uniform float scramble;
46 | uniform float ditherMagnitude;
47 | uniform bool isWireframe;
48 | varying float vShade;
49 | varying float vOpacity;
50 | varying vec4 vIdColor;
51 |
52 | highp float rand( const in vec2 uv, const in float t ) {
53 | const highp float a = 12.9898, b = 78.233, c = 43758.5453;
54 | highp float dt = dot( uv.xy, vec2( a,b ) ), sn = mod( dt, PI );
55 |
56 | return fract(sin(sn) * c + t);
57 | }
58 |
59 | void main() {
60 | if (isWireframe) {
61 | if (vOpacity < 1.0) {
62 | discard;
63 | }
64 |
65 | gl_FragColor = vIdColor;
66 | } else {
67 | float shade = clamp(vShade + (rand( gl_FragCoord.xy, scramble ) - 0.5) * ditherMagnitude, 0., 1.);
68 |
69 | vec3 color = shade < 0.5
70 | ? mix(darkTint, fullTint, shade * 2.0)
71 | : mix(fullTint, lightTint, shade * 2.0 - 1.0);
72 |
73 | gl_FragColor = vec4(color, vOpacity);
74 | }
75 | }
76 | `,
77 | });
78 | silhouetteMaterial.uniforms.ditherMagnitude = { value: 0.02 };
79 | silhouetteMaterial.uniforms.isWireframe = { value: false };
80 | silhouetteMaterial.uniforms.scramble = { value: 0 };
81 | silhouetteMaterial.uniforms.fullTint = { value: new Color() };
82 | silhouetteMaterial.uniforms.darkTint = { value: new Color(0, 0, 0) };
83 | silhouetteMaterial.uniforms.lightTint = { value: new Color(1, 1, 1) };
84 |
85 | const transparentMaterial = new RawShaderMaterial({
86 | vertexShader: silhouetteMaterial.vertexShader,
87 | fragmentShader: silhouetteMaterial.fragmentShader,
88 | transparent: true,
89 | });
90 | transparentMaterial.uniforms.ditherMagnitude = { value: 0.02 };
91 | transparentMaterial.uniforms.isWireframe = { value: false };
92 | transparentMaterial.uniforms.scramble = { value: 0 };
93 | transparentMaterial.uniforms.fullTint = { value: new Color() };
94 | transparentMaterial.uniforms.darkTint = { value: new Color(0, 0, 0) };
95 | transparentMaterial.uniforms.lightTint = { value: new Color(1, 1, 1) };
96 |
97 | export { silhouetteMaterial, transparentMaterial, blendColors };
98 |
--------------------------------------------------------------------------------
/js/roads.js:
--------------------------------------------------------------------------------
1 | import { Vector2 } from "./../lib/three/three.module.js";
2 | import { fract, lerp, closestPointIndex, lcg, intMax, TWO_PI } from "./math.js";
3 | import { makeSplinePath } from "./paths.js";
4 |
5 | class Road {
6 | constructor(points) {
7 | this.points = points;
8 | this.curve = makeSplinePath(points, true);
9 | }
10 |
11 | clone() {
12 | return new Road(this.points);
13 | }
14 |
15 | scale(x, y) {
16 | this.points = this.points.map((point) => new Vector2(point.x * x, point.y * y));
17 | this.curve = makeSplinePath(this.points, true);
18 | }
19 |
20 | getPoint(t) {
21 | const pos = this.curve.getPoint(fract(t));
22 | return new Vector2(pos.x, pos.y);
23 | }
24 |
25 | getTangent(t) {
26 | const EPSILON = 0.00001;
27 | const point = this.getPoint(t + EPSILON).sub(this.getPoint(t - EPSILON));
28 | return point.normalize();
29 | }
30 |
31 | getNormal(t) {
32 | const normal = this.getTangent(t);
33 | return new Vector2(-normal.y, normal.x);
34 | }
35 |
36 | approximate(resolution = 1000) {
37 | return new Approximation(this, resolution);
38 | }
39 |
40 | get length() {
41 | return this.curve.getLength();
42 | }
43 | }
44 |
45 | class Approximation {
46 | constructor(road, resolution) {
47 | this.road = road;
48 | this.resolution = resolution;
49 | this.points = [];
50 | for (let i = 0; i < resolution; i++) {
51 | this.points.push(road.getPoint(i / resolution));
52 | }
53 | }
54 |
55 | getNearest(to) {
56 | return closestPointIndex(this.points, to) / this.resolution;
57 | }
58 |
59 | getNearestPoint(to) {
60 | return this.road.getPoint(this.getNearest(to));
61 | }
62 | }
63 |
64 | const numControlPoints = 16;
65 |
66 | const roadWedges = Array(numControlPoints)
67 | .fill()
68 | .map((_, index) => (index / numControlPoints) * TWO_PI)
69 | .map((theta) => new Vector2(-Math.cos(theta), Math.sin(theta)));
70 |
71 | const makeRandomControlPoints = (seed, windiness, scale) => {
72 | const randomValues = [];
73 | let value = lcg(isNaN(seed) ? Date.now() : seed);
74 | for (let i = 0; i < numControlPoints; i++) {
75 | randomValues.push(value / intMax);
76 | value = lcg(value);
77 | }
78 | const controlPoints = roadWedges.map((point, i) => point.clone().multiplyScalar(lerp(1, randomValues[i], windiness)));
79 |
80 | const minX = Math.min(...controlPoints.map(({ x }) => x));
81 | const maxX = Math.max(...controlPoints.map(({ x }) => x));
82 | const minY = Math.min(...controlPoints.map(({ y }) => y));
83 | const maxY = Math.max(...controlPoints.map(({ y }) => y));
84 |
85 | const center = new Vector2(maxX + minX, maxY + minY).multiplyScalar(0.5);
86 | const aspect = new Vector2(1, (maxY - minY) / (maxX - minX));
87 |
88 | controlPoints.forEach((point) => {
89 | point.sub(center);
90 | point.multiply(aspect);
91 | point.multiply(scale);
92 | });
93 |
94 | return controlPoints;
95 | };
96 |
97 | const makeControlPoints = (splinePoints) =>
98 | splinePoints.length < 6
99 | ? null
100 | : Array(splinePoints.length / 2)
101 | .fill()
102 | .map((_, index) => new Vector2(splinePoints[index * 2], splinePoints[index * 2 + 1]));
103 |
104 | const makeRoad = ({ windiness, scale, seed, splinePoints, length }, basis) => {
105 | if (basis != null) {
106 | const road = basis.clone();
107 | road.scale(scale.x, scale.y);
108 | return road;
109 | }
110 |
111 | const controlPoints = makeControlPoints(splinePoints) ?? makeRandomControlPoints(seed, windiness, scale);
112 | const road = new Road(controlPoints);
113 | const scalarMultiply = length / road.length;
114 | road.scale(scalarMultiply, scalarMultiply);
115 | return road;
116 | };
117 |
118 | export { Road, makeRoad };
119 |
--------------------------------------------------------------------------------
/legacy/js/Overpass.js:
--------------------------------------------------------------------------------
1 | "use strict";
2 |
3 | class Overpass extends Level {
4 | build(meshes, transparentMeshes, skyMeshes) {
5 | this.tint = new THREE.Color(0.5, 0.8, 0.2);
6 | this.ground = 0.1;
7 | this.skyLow = 0.8;
8 | this.skyHigh = 0;
9 | this.roadPath.scale(0.5, 0.5);
10 | this.cruiseSpeed = 4;
11 | this.laneWidth = 4;
12 |
13 | const tarmacPath = new THREE.ShapePath();
14 | this.drawRoadLine(this.roadPath, tarmacPath, 0, 9, RoadLineStyle.SOLID(2), 0, 1);
15 | const tarmacMesh = makeMesh(tarmacPath, 0.5, 100, 0.05);
16 | tarmacMesh.position.z = -0.5;
17 | meshes.push(tarmacMesh);
18 |
19 | const shoulderPath = new THREE.ShapePath();
20 | this.drawRoadLine(this.roadPath, shoulderPath, -5.5, 2, RoadLineStyle.SOLID(2), 0, 1);
21 | this.drawRoadLine(this.roadPath, shoulderPath, 5.5, 2, RoadLineStyle.SOLID(2), 0, 1);
22 | const shoulderMesh = makeMesh(shoulderPath, 0.5, 100, 0.25);
23 | shoulderMesh.position.z = -0.55;
24 | meshes.push(shoulderMesh);
25 |
26 | const underneathPath = new THREE.ShapePath();
27 | this.drawRoadLine(this.roadPath, underneathPath, 0, 5.25, RoadLineStyle.SOLID(10), 0, 1);
28 | const underneathMesh = makeMesh(underneathPath, 2, 1, 0.07);
29 | underneathMesh.position.z = -2.5;
30 | meshes.push(underneathMesh);
31 |
32 | const roadLinesPath = new THREE.ShapePath();
33 | this.drawRoadLine(this.roadPath, roadLinesPath, -4, 0.1, RoadLineStyle.DASH(30, 1, 2), 0, 1);
34 | this.drawRoadLine(this.roadPath, roadLinesPath, 4, 0.1, RoadLineStyle.DASH(30, 1, 2), 0, 1);
35 | this.drawRoadLine(this.roadPath, roadLinesPath, 0, 0.15, RoadLineStyle.DASH(5, 5, 0), 0, 1);
36 | const roadLinesMesh = makeMesh(roadLinesPath, 0, 1, 0.7);
37 | roadLinesMesh.position.z = 0.1;
38 | meshes.push(roadLinesMesh);
39 |
40 | const railsPath = new THREE.ShapePath();
41 | this.drawRoadLine(this.roadPath, railsPath, -6.0, 0.175, RoadLineStyle.SOLID(3), 0, 1);
42 | this.drawRoadLine(this.roadPath, railsPath, 6.0, 0.175, RoadLineStyle.SOLID(3), 0, 1);
43 | const railsMesh = makeMesh(railsPath, 0.1, 1, 0.3);
44 | railsMesh.position.z = 0.25;
45 | meshes.push(railsMesh);
46 |
47 | const railsTopPath = new THREE.ShapePath();
48 | this.drawRoadLine(this.roadPath, railsTopPath, -6.0, 0.1, RoadLineStyle.SOLID(3), 0, 1);
49 | this.drawRoadLine(this.roadPath, railsTopPath, 6.0, 0.1, RoadLineStyle.SOLID(3), 0, 1);
50 | const railsTopMesh = makeMesh(railsTopPath, 0.125, 1, 0.35);
51 | railsTopMesh.position.z = 0.25;
52 | meshes.push(railsTopMesh);
53 |
54 | const railStakesPath = new THREE.ShapePath();
55 | this.drawRoadLine(this.roadPath, railStakesPath, -6.0, 0.15, RoadLineStyle.DASH(0.2, 2, 0), 0, 1);
56 | this.drawRoadLine(this.roadPath, railStakesPath, 6.0, 0.15, RoadLineStyle.DASH(0.2, 2, 0), 0, 1);
57 | const railStakesMesh = makeMesh(railStakesPath, 0.55, 1, 0.3);
58 | railStakesMesh.position.z = -0.25;
59 | meshes.push(railStakesMesh);
60 |
61 | const supportsPath = new THREE.ShapePath();
62 | this.drawRoadLine(this.roadPath, supportsPath, 0, 5, RoadLineStyle.DOT(50), 0, 1);
63 | const supportsMesh = makeMesh(supportsPath, 2, 100, 0.07);
64 | supportsMesh.position.z = -3.5;
65 | meshes.push(supportsMesh);
66 |
67 | const columnsPath = new THREE.ShapePath();
68 | this.drawRoadLine(this.roadPath, columnsPath, 0, 5, RoadLineStyle.DOT(50), 0, 1);
69 | const columnsMesh = makeMesh(columnsPath, 2, 100, 0.07);
70 | columnsMesh.position.z = -5.5;
71 | meshes.push(columnsMesh);
72 |
73 | const river = this.makeRoadPath(0.3);
74 | river.scale(6, 6);
75 |
76 | const riverPath = new THREE.ShapePath();
77 | this.drawRoadLine(river, riverPath, 0, 70, RoadLineStyle.SOLID(10), 0, 1);
78 | const riverMesh = makeMesh(riverPath, 0, 100, 0.5, 1, -1);
79 | riverMesh.position.z = -5.5;
80 | meshes.push(riverMesh);
81 | }
82 | }
83 |
--------------------------------------------------------------------------------
/js/paths.js:
--------------------------------------------------------------------------------
1 | import { Path, CatmullRomCurve3, Vector3, Vector2 } from "./../lib/three/three.module.js";
2 | import { fract, modDiffAngle, TWO_PI, origin, unitVector } from "./math.js";
3 |
4 | const makeSplinePath = (pts, closed) => {
5 | const path = new Path();
6 | path.curves.push(
7 | new CatmullRomCurve3(
8 | pts.map(({ x, y }) => new Vector3(x, y)),
9 | closed
10 | )
11 | );
12 |
13 | return path;
14 | };
15 |
16 | const circleCache = new Map();
17 |
18 | const makeCirclePath = (x, y, radius, aClockwise = true) => {
19 | if (!circleCache.has(radius)) {
20 | const numPoints = Math.max(10, Math.ceil(5 * radius ** 0.5));
21 | const wedges = Array(numPoints)
22 | .fill()
23 | .map((_, index) => {
24 | return unitVector
25 | .clone()
26 | .rotateAround(origin, (index / numPoints) * TWO_PI)
27 | .multiplyScalar(radius);
28 | });
29 | circleCache.set(radius, wedges);
30 | }
31 | const pos = new Vector2(x, y);
32 | const points = circleCache.get(radius).map((point) => point.clone().add(pos));
33 | if (aClockwise) {
34 | points.reverse();
35 | }
36 | return makePolygonPath(points);
37 | };
38 |
39 | const makeSquarePath = (x, y, width) =>
40 | makePolygonPath([
41 | new Vector2(x - width / 2, y - width / 2),
42 | new Vector2(x + width / 2, y - width / 2),
43 | new Vector2(x + width / 2, y + width / 2),
44 | new Vector2(x - width / 2, y + width / 2),
45 | ]);
46 |
47 | const makePolygonPath = (points) => new Path(points);
48 |
49 | const getOffsetSample = (source, t, offset) => {
50 | const fractT = fract(t);
51 | const tangent = source.getTangent(fractT);
52 | return {
53 | t,
54 | pos: new Vector2(-tangent.y, tangent.x).multiplyScalar(offset).add(source.getPoint(fractT)),
55 | angle: Math.atan2(tangent.y, tangent.x),
56 | };
57 | };
58 |
59 | const maxAllowedDistanceSquared = 50;
60 | const maxAllowedDifferenceAngle = TWO_PI / 360;
61 | const maxIterations = 10;
62 |
63 | const getOffsetPoints = (source, offset, start = 0, end = 1, spacing = 0) => {
64 | start = fract(start);
65 | end = fract(end);
66 | if (end <= start) {
67 | end++;
68 | }
69 |
70 | const startSample = getOffsetSample(source, start, offset);
71 | const endSample = getOffsetSample(source, end, offset);
72 |
73 | if (spacing > 0) {
74 | const numPoints = Math.ceil((end - start) / spacing);
75 | const points = Array(numPoints - 1)
76 | .fill()
77 | .map((_, index) => getOffsetSample(source, start + (index + 1) * spacing, offset).pos);
78 | return [startSample.pos, ...points, endSample.pos];
79 | }
80 |
81 | const middleSample = getOffsetSample(source, (start + end) / 2, offset);
82 | startSample.next = middleSample;
83 | middleSample.next = endSample;
84 | let numSamples = 3;
85 | for (let iteration = 0; iteration < maxIterations; iteration++) {
86 | let numAddedSamples = 0;
87 | let sample = startSample;
88 | for (let i = 1; i < numSamples; i++) {
89 | const nextSample = sample.next;
90 | if (fract(sample.t) !== fract(nextSample.t)) {
91 | const tooFarApart = sample.pos.distanceToSquared(nextSample.pos) > maxAllowedDistanceSquared;
92 | const tooPointy = modDiffAngle(sample.angle, nextSample.angle) > maxAllowedDifferenceAngle;
93 | if (tooFarApart || tooPointy) {
94 | const halfwaySample = getOffsetSample(source, (sample.t + nextSample.t) / 2, offset);
95 | halfwaySample.next = nextSample;
96 | sample.next = halfwaySample;
97 | numAddedSamples++;
98 | }
99 | }
100 | sample = nextSample;
101 | }
102 | numSamples += numAddedSamples;
103 | if (numAddedSamples === 0) {
104 | break;
105 | }
106 | }
107 |
108 | const points = [];
109 | let sample = startSample;
110 | for (let i = 0; i < numSamples; i++) {
111 | points.push(sample.pos);
112 | sample = sample.next;
113 | }
114 |
115 | return points;
116 | };
117 |
118 | export { makeCirclePath, makePolygonPath, makeSquarePath, makeSplinePath, getOffsetPoints };
119 |
--------------------------------------------------------------------------------
/legacy/js/City.js:
--------------------------------------------------------------------------------
1 | "use strict";
2 |
3 | class City extends Level {
4 | build(meshes, transparentMeshes, skyMeshes) {
5 | this.name = "The City";
6 | this.tint = new THREE.Color(0.33, 0.33, 1); // * 1.5
7 |
8 | this.roadPath.scale(2, 2);
9 | this.ground = 0.05;
10 | const roadLineColor = 0.6;
11 | this.skyLow = 0.4;
12 | this.skyHigh = 0;
13 |
14 | this.laneWidth = 3;
15 | this.numLanes = 2;
16 |
17 | // sky
18 | const cloudsPath = new THREE.ShapePath();
19 | for (let i = 0; i < 100; i++) {
20 | const pos = new THREE.Vector2(Math.random() - 0.5, Math.random() - 0.5);
21 | if (pos.length() > 0.9 || pos.length() < 0.3) { // 0.5, 0.1
22 | continue;
23 | }
24 | pos.multiplyScalar(1000);
25 |
26 | // TODO: more efficient distance test
27 | // if ((pos - cloudsPath.getNearestPoint(pos)).length() < 200) continue;
28 |
29 | addPath(cloudsPath, makeCirclePath(pos.x, pos.y, 50));
30 | }
31 | const cloudsMesh = makeMesh(cloudsPath, 1, 200, (this.skyLow + this.skyHigh) / 2);
32 | cloudsMesh.scale.multiplyScalar(2);
33 | cloudsMesh.position.z = 80;
34 | skyMeshes.push(cloudsMesh);
35 |
36 | // do bg
37 | const shorterBuildings = new THREE.ShapePath();
38 | const shortBuildings = new THREE.ShapePath();
39 | const tallBuildings = new THREE.ShapePath();
40 | const tallerBuildings = new THREE.ShapePath();
41 | const mag = 0.6;
42 | const width = 40 * mag;
43 | const radius = 1800 * mag;
44 | const approximation = this.roadPath.approximate();
45 | let x = -radius;
46 | while (x < radius) {
47 | let y = -radius;
48 | while (y < radius) {
49 | const pos1 = new THREE.Vector2(x, y);
50 | if (pos1.length() < radius && distance(approximation.getNearestPoint(pos1), pos1) > 60 * mag) {
51 | const building = makeRectanglePath(pos1.x + -width / 2, pos1.y + -width / 2, width, width);
52 | if (Math.random() > 0.8) {
53 | addPath(shorterBuildings, building);
54 | } else if (Math.random() > 0.5) {
55 | addPath(shortBuildings, building);
56 | } else if (Math.random() > 0.25) {
57 | addPath(tallBuildings, building);
58 | } else {
59 | addPath(tallerBuildings, building);
60 | }
61 | }
62 | y += 150 * mag;
63 | }
64 | x += 150 * mag;
65 | }
66 |
67 | // meshes.push(makeMesh(shorterBuildings, 15 * mag, 1, this.ground));
68 | meshes.push(makeMesh(shortBuildings, 30 * mag, 1, this.ground));
69 | meshes.push(makeMesh(tallBuildings, 50 * mag, 1, this.ground));
70 | meshes.push(makeMesh(tallerBuildings, 120 * mag, 1, this.ground));
71 |
72 | const signpostsPath = new THREE.ShapePath();
73 | this.drawRoadLine(this.roadPath, signpostsPath, -16, 0.2, RoadLineStyle.DASH(0.2, 400, 0), 0, 1);
74 | this.drawRoadLine(this.roadPath, signpostsPath, -12, 0.2, RoadLineStyle.DASH(0.2, 400, 0), 0, 1);
75 | this.drawRoadLine(this.roadPath, signpostsPath, 12, 0.2, RoadLineStyle.DASH(0.2, 300, 0), 0, 1);
76 | this.drawRoadLine(this.roadPath, signpostsPath, 16, 0.2, RoadLineStyle.DASH(0.2, 300, 0), 0, 1);
77 | meshes.push(makeMesh(signpostsPath, 10, 0, this.ground));
78 |
79 | const signsPath = new THREE.ShapePath();
80 | this.drawRoadLine(this.roadPath, signsPath, -14, 6, RoadLineStyle.DASH(0.2, 400, 0), 0, 1);
81 | this.drawRoadLine(this.roadPath, signsPath, 14, 6, RoadLineStyle.DASH(0.2, 300, 0), 0, 1);
82 | const signsMesh = makeMesh(signsPath, 4, 0, this.ground);
83 | signsMesh.position.z = 10;
84 | meshes.push(signsMesh);
85 |
86 | const roadLinesPath = new THREE.ShapePath();
87 | this.drawRoadLine(this.roadPath, roadLinesPath, 0, 0.1, RoadLineStyle.SOLID(10), 0, 1);
88 | this.drawRoadLine(this.roadPath, roadLinesPath, 0.2, 0.1, RoadLineStyle.SOLID(10), 0, 1);
89 | this.drawRoadLine(this.roadPath, roadLinesPath, -6, 0.15, RoadLineStyle.DASH(30, 1, 10), 0, 1);
90 | this.drawRoadLine(this.roadPath, roadLinesPath, 6, 0.15, RoadLineStyle.DASH(30, 1, 10), 0, 1);
91 | this.drawRoadLine(this.roadPath, roadLinesPath, -3, 0.15, RoadLineStyle.DASH(3, 12, 0), 0, 1);
92 | this.drawRoadLine(this.roadPath, roadLinesPath, 3, 0.15, RoadLineStyle.DASH(3, 12, 0), 0, 1);
93 | meshes.push(makeMesh(roadLinesPath, 0, 1, roadLineColor, 1, 1));
94 | }
95 | }
96 |
--------------------------------------------------------------------------------
/css/drivey.css:
--------------------------------------------------------------------------------
1 | * {
2 | margin: 0;
3 | padding: 0;
4 | border: 0;
5 | outline: 0;
6 | text-decoration: none;
7 | font-weight: inherit;
8 | font-style: inherit;
9 | color: inherit;
10 | font-size: 100%;
11 | font-family: inherit;
12 | vertical-align: baseline;
13 | list-style: none;
14 | border-collapse: collapse;
15 | border-spacing: 0;
16 | -webkit-font-smoothing: antialiased;
17 | -moz-osx-font-smoothing: grayscale;
18 | }
19 |
20 | body {
21 | background: black;
22 | color: white;
23 | overflow: hidden;
24 | margin: 0;
25 | }
26 |
27 | body, colors {
28 | --dashboard-background-color: black;
29 | --dashboard-border-color: #888;
30 | --dashboard-light-color: #fff;
31 | }
32 |
33 | canvas#renderer {
34 | display: block;
35 | image-rendering: optimizeSpeed;
36 | }
37 |
38 | div#buttonsContainer {
39 | position: absolute;
40 | bottom: 1em;
41 | width: 100%;
42 | display: flex;
43 | justify-content: center;
44 | opacity: 0;
45 | transition: opacity 4000ms;
46 | }
47 |
48 | div#buttonsContainer.awake {
49 | opacity: 1;
50 | transition: opacity 250ms;
51 | }
52 |
53 | div#buttons {
54 | display: inline-flex;
55 | align-items: center;
56 | color: white;
57 | font-weight: bolder;
58 | font-size: 1vw;
59 | font-family: sans-serif;
60 | text-align: center;
61 | border: 0.4em solid var(--dashboard-border-color);
62 | border-radius: 3em;
63 | overflow: hidden;
64 | background: var(--dashboard-background-color);
65 | }
66 |
67 | div#buttons button {
68 | text-transform: uppercase;
69 | background-color: var(--dashboard-background-color);
70 | padding:0 1.5em;
71 | height: 5em;
72 | border-left: 0.2em solid var(--dashboard-border-color);
73 | border-right: 0.2em solid var(--dashboard-border-color);
74 | }
75 |
76 | #buttons button:first-of-type {
77 | border-left: unset;
78 | }
79 |
80 | #buttons button:last-of-type {
81 | border-right: unset;
82 | }
83 |
84 | div#buttons button#button_music {
85 | display: none;
86 | }
87 |
88 | div#buttons #embedded-playlist {
89 | border-left: 0.2em solid var(--dashboard-border-color);
90 | border-right: 0.2em solid var(--dashboard-border-color);
91 |
92 | position: relative;
93 |
94 | --scale: 1.5;
95 | --width: 300;
96 | --height: 80;
97 |
98 | display: flex;
99 | justify-content: center;
100 | align-items: center;
101 |
102 | width: calc(var(--width) * 1px * var(--scale));
103 | height: 5vw;
104 | overflow: hidden;
105 | }
106 |
107 | @media (max-width: 2000px) { div#buttons #embedded-playlist { --scale: 1.25; } }
108 | @media (max-width: 1675px) { div#buttons #embedded-playlist { --scale: 1; } }
109 | @media (max-width: 1350px) {
110 | div#buttons button#button_music {
111 | display: block;
112 | }
113 |
114 | div#buttons #embedded-playlist {
115 | display: none;
116 | }
117 | }
118 |
119 | div#buttons #embedded-playlist #backdrop {
120 | position: absolute;
121 | width: calc(var(--width) * 1px * var(--scale));
122 | height: calc(var(--height) * 1px * var(--scale));
123 | }
124 |
125 | div#buttons #embedded-playlist iframe {
126 |
127 | overflow: hidden;
128 | width: 300px;
129 | height: 80px;
130 | scale: var(--scale);
131 | transform-origin: center;
132 |
133 | background: #282828;
134 | filter: contrast(150%) brightness(150%);
135 | mix-blend-mode: luminosity;
136 | }
137 |
138 | div#buttons:not(.wireframe) #embedded-playlist #backdrop {
139 | background: var(--dashboard-light-color);
140 | }
141 |
142 | div#buttons.wireframe #embedded-playlist iframe {
143 | filter: contrast(200%) brightness(200%) saturate(0%);
144 | mix-blend-mode: screen;
145 | }
146 |
147 | div#buttons button div.label {
148 | display:block;
149 | line-height: 2em;
150 | }
151 |
152 | div#buttons button div.option {
153 | display:block;
154 | line-height: 2em;
155 | }
156 |
157 | span.indicator {
158 | color: var(--dashboard-light-color);
159 | text-shadow: 0 0 0.5em var(--dashboard-light-color);
160 | }
161 |
162 | span.light {
163 | display: inline-block;
164 | width: 1.25em;
165 | height: 1em;
166 | background-color: var(--dashboard-light-color);
167 | box-shadow: 0 0 1em var(--dashboard-light-color);
168 | }
169 |
170 | span.light.off {
171 | background-color: var(--dashboard-border-color);
172 | box-shadow: unset;
173 | }
174 |
--------------------------------------------------------------------------------
/lib/theme.js:
--------------------------------------------------------------------------------
1 | 'use strict'
2 |
3 | /* global localStorage */
4 | /* global FileReader */
5 | /* global DOMParser */
6 |
7 | function Theme (client) {
8 | this.el = document.createElement('style')
9 | this.el.type = 'text/css'
10 |
11 | this.active = {}
12 | this.default = {
13 | background: '#eeeeee',
14 | f_high: '#0a0a0a',
15 | f_med: '#4a4a4a',
16 | f_low: '#6a6a6a',
17 | f_inv: '#111111',
18 | b_high: '#a1a1a1',
19 | b_med: '#c1c1c1',
20 | b_low: '#ffffff',
21 | b_inv: '#ffb545'
22 | }
23 |
24 | // Callbacks
25 | this.onLoad = () => {}
26 |
27 | this.install = (host = document.body) => {
28 | window.addEventListener('dragover', this.drag)
29 | window.addEventListener('drop', this.drop)
30 | host.appendChild(this.el)
31 | }
32 |
33 | this.start = () => {
34 | console.log('Theme', 'Starting..')
35 | if (isJson(localStorage.theme)) {
36 | const storage = JSON.parse(localStorage.theme)
37 | if (isValid(storage)) {
38 | console.log('Theme', 'Loading theme in localStorage..')
39 | this.load(storage)
40 | return
41 | }
42 | }
43 | this.load(this.default)
44 | }
45 |
46 | this.open = () => {
47 | console.log('Theme', 'Open theme..')
48 | const input = document.createElement('input')
49 | input.type = 'file'
50 | input.onchange = (e) => {
51 | this.read(e.target.files[0], this.load)
52 | }
53 | input.click()
54 | }
55 |
56 | this.load = (data) => {
57 | const theme = this.parse(data)
58 | if (!isValid(theme)) { console.warn('Theme', 'Invalid format'); return }
59 | console.log('Theme', 'Loaded theme!')
60 | this.el.innerHTML = `:root {
61 | --background: ${theme.background};
62 | --f_high: ${theme.f_high};
63 | --f_med: ${theme.f_med};
64 | --f_low: ${theme.f_low};
65 | --f_inv: ${theme.f_inv};
66 | --b_high: ${theme.b_high};
67 | --b_med: ${theme.b_med};
68 | --b_low: ${theme.b_low};
69 | --b_inv: ${theme.b_inv};
70 | }`
71 | localStorage.setItem('theme', JSON.stringify(theme))
72 | this.active = theme
73 | if (this.onLoad) {
74 | this.onLoad(data)
75 | }
76 | }
77 |
78 | this.reset = () => {
79 | this.load(this.default)
80 | }
81 |
82 | this.set = (key, val) => {
83 | if (!val) { return }
84 | const hex = (`${val}`.substr(0, 1) !== '#' ? '#' : '') + `${val}`
85 | if (!isColor(hex)) { console.warn('Theme', `${hex} is not a valid color.`); return }
86 | this.active[key] = hex
87 | }
88 |
89 | this.get = (key) => {
90 | return this.active[key]
91 | }
92 |
93 | this.parse = (any) => {
94 | if (isValid(any)) { return any }
95 | if (isJson(any)) { return JSON.parse(any) }
96 | if (isHtml(any)) { return extract(any) }
97 | }
98 |
99 | // Drag
100 |
101 | this.drag = (e) => {
102 | e.stopPropagation()
103 | e.preventDefault()
104 | e.dataTransfer.dropEffect = 'copy'
105 | }
106 |
107 | this.drop = (e) => {
108 | e.preventDefault()
109 | const file = e.dataTransfer.files[0]
110 | if (file.name.indexOf('.svg') > -1) {
111 | this.read(file, this.load)
112 | }
113 | e.stopPropagation()
114 | }
115 |
116 | this.read = (file, callback) => {
117 | const reader = new FileReader()
118 | reader.onload = (event) => {
119 | callback(event.target.result)
120 | }
121 | reader.readAsText(file, 'UTF-8')
122 | }
123 |
124 | // Helpers
125 |
126 | function extract (xml) {
127 | const svg = new DOMParser().parseFromString(xml, 'text/xml')
128 | try {
129 | return {
130 | background: svg.getElementById('background').getAttribute('fill'),
131 | f_high: svg.getElementById('f_high').getAttribute('fill'),
132 | f_med: svg.getElementById('f_med').getAttribute('fill'),
133 | f_low: svg.getElementById('f_low').getAttribute('fill'),
134 | f_inv: svg.getElementById('f_inv').getAttribute('fill'),
135 | b_high: svg.getElementById('b_high').getAttribute('fill'),
136 | b_med: svg.getElementById('b_med').getAttribute('fill'),
137 | b_low: svg.getElementById('b_low').getAttribute('fill'),
138 | b_inv: svg.getElementById('b_inv').getAttribute('fill')
139 | }
140 | } catch (err) {
141 | console.warn('Theme', 'Incomplete SVG Theme', err)
142 | }
143 | }
144 |
145 | function isValid (json) {
146 | if (!json) { return false }
147 | if (!json.background || !isColor(json.background)) { return false }
148 | if (!json.f_high || !isColor(json.f_high)) { return false }
149 | if (!json.f_med || !isColor(json.f_med)) { return false }
150 | if (!json.f_low || !isColor(json.f_low)) { return false }
151 | if (!json.f_inv || !isColor(json.f_inv)) { return false }
152 | if (!json.b_high || !isColor(json.b_high)) { return false }
153 | if (!json.b_med || !isColor(json.b_med)) { return false }
154 | if (!json.b_low || !isColor(json.b_low)) { return false }
155 | if (!json.b_inv || !isColor(json.b_inv)) { return false }
156 | return true
157 | }
158 |
159 | function isColor (hex) {
160 | return /^#([0-9A-F]{3}){1,2}$/i.test(hex)
161 | }
162 |
163 | function isJson (text) {
164 | try { JSON.parse(text); return true } catch (error) { return false }
165 | }
166 |
167 | function isHtml (text) {
168 | try { new DOMParser().parseFromString(text, 'text/xml'); return true } catch (error) { return false }
169 | }
170 | }
171 |
--------------------------------------------------------------------------------
/legacy/lib/theme.js:
--------------------------------------------------------------------------------
1 | 'use strict'
2 |
3 | /* global localStorage */
4 | /* global FileReader */
5 | /* global DOMParser */
6 |
7 | function Theme (client) {
8 | this.el = document.createElement('style')
9 | this.el.type = 'text/css'
10 |
11 | this.active = {}
12 | this.default = {
13 | background: '#eeeeee',
14 | f_high: '#0a0a0a',
15 | f_med: '#4a4a4a',
16 | f_low: '#6a6a6a',
17 | f_inv: '#111111',
18 | b_high: '#a1a1a1',
19 | b_med: '#c1c1c1',
20 | b_low: '#ffffff',
21 | b_inv: '#ffb545'
22 | }
23 |
24 | // Callbacks
25 | this.onLoad = () => {}
26 |
27 | this.install = (host = document.body) => {
28 | window.addEventListener('dragover', this.drag)
29 | window.addEventListener('drop', this.drop)
30 | host.appendChild(this.el)
31 | }
32 |
33 | this.start = () => {
34 | console.log('Theme', 'Starting..')
35 | if (isJson(localStorage.theme)) {
36 | const storage = JSON.parse(localStorage.theme)
37 | if (isValid(storage)) {
38 | console.log('Theme', 'Loading theme in localStorage..')
39 | this.load(storage)
40 | return
41 | }
42 | }
43 | this.load(this.default)
44 | }
45 |
46 | this.open = () => {
47 | console.log('Theme', 'Open theme..')
48 | const input = document.createElement('input')
49 | input.type = 'file'
50 | input.onchange = (e) => {
51 | this.read(e.target.files[0], this.load)
52 | }
53 | input.click()
54 | }
55 |
56 | this.load = (data) => {
57 | const theme = this.parse(data)
58 | if (!isValid(theme)) { console.warn('Theme', 'Invalid format'); return }
59 | console.log('Theme', 'Loaded theme!')
60 | this.el.innerHTML = `:root {
61 | --background: ${theme.background};
62 | --f_high: ${theme.f_high};
63 | --f_med: ${theme.f_med};
64 | --f_low: ${theme.f_low};
65 | --f_inv: ${theme.f_inv};
66 | --b_high: ${theme.b_high};
67 | --b_med: ${theme.b_med};
68 | --b_low: ${theme.b_low};
69 | --b_inv: ${theme.b_inv};
70 | }`
71 | localStorage.setItem('theme', JSON.stringify(theme))
72 | this.active = theme
73 | if (this.onLoad) {
74 | this.onLoad(data)
75 | }
76 | }
77 |
78 | this.reset = () => {
79 | this.load(this.default)
80 | }
81 |
82 | this.set = (key, val) => {
83 | if (!val) { return }
84 | const hex = (`${val}`.substr(0, 1) !== '#' ? '#' : '') + `${val}`
85 | if (!isColor(hex)) { console.warn('Theme', `${hex} is not a valid color.`); return }
86 | this.active[key] = hex
87 | }
88 |
89 | this.get = (key) => {
90 | return this.active[key]
91 | }
92 |
93 | this.parse = (any) => {
94 | if (isValid(any)) { return any }
95 | if (isJson(any)) { return JSON.parse(any) }
96 | if (isHtml(any)) { return extract(any) }
97 | }
98 |
99 | // Drag
100 |
101 | this.drag = (e) => {
102 | e.stopPropagation()
103 | e.preventDefault()
104 | e.dataTransfer.dropEffect = 'copy'
105 | }
106 |
107 | this.drop = (e) => {
108 | e.preventDefault()
109 | const file = e.dataTransfer.files[0]
110 | if (file.name.indexOf('.svg') > -1) {
111 | this.read(file, this.load)
112 | }
113 | e.stopPropagation()
114 | }
115 |
116 | this.read = (file, callback) => {
117 | const reader = new FileReader()
118 | reader.onload = (event) => {
119 | callback(event.target.result)
120 | }
121 | reader.readAsText(file, 'UTF-8')
122 | }
123 |
124 | // Helpers
125 |
126 | function extract (xml) {
127 | const svg = new DOMParser().parseFromString(xml, 'text/xml')
128 | try {
129 | return {
130 | background: svg.getElementById('background').getAttribute('fill'),
131 | f_high: svg.getElementById('f_high').getAttribute('fill'),
132 | f_med: svg.getElementById('f_med').getAttribute('fill'),
133 | f_low: svg.getElementById('f_low').getAttribute('fill'),
134 | f_inv: svg.getElementById('f_inv').getAttribute('fill'),
135 | b_high: svg.getElementById('b_high').getAttribute('fill'),
136 | b_med: svg.getElementById('b_med').getAttribute('fill'),
137 | b_low: svg.getElementById('b_low').getAttribute('fill'),
138 | b_inv: svg.getElementById('b_inv').getAttribute('fill')
139 | }
140 | } catch (err) {
141 | console.warn('Theme', 'Incomplete SVG Theme', err)
142 | }
143 | }
144 |
145 | function isValid (json) {
146 | if (!json) { return false }
147 | if (!json.background || !isColor(json.background)) { return false }
148 | if (!json.f_high || !isColor(json.f_high)) { return false }
149 | if (!json.f_med || !isColor(json.f_med)) { return false }
150 | if (!json.f_low || !isColor(json.f_low)) { return false }
151 | if (!json.f_inv || !isColor(json.f_inv)) { return false }
152 | if (!json.b_high || !isColor(json.b_high)) { return false }
153 | if (!json.b_med || !isColor(json.b_med)) { return false }
154 | if (!json.b_low || !isColor(json.b_low)) { return false }
155 | if (!json.b_inv || !isColor(json.b_inv)) { return false }
156 | return true
157 | }
158 |
159 | function isColor (hex) {
160 | return /^#([0-9A-F]{3}){1,2}$/i.test(hex)
161 | }
162 |
163 | function isJson (text) {
164 | try { JSON.parse(text); return true } catch (error) { return false }
165 | }
166 |
167 | function isHtml (text) {
168 | try { new DOMParser().parseFromString(text, 'text/xml'); return true } catch (error) { return false }
169 | }
170 | }
171 |
--------------------------------------------------------------------------------
/legacy/js/Input.js:
--------------------------------------------------------------------------------
1 | "use strict";
2 |
3 | class Input {
4 | constructor() {
5 | this.slow = false;
6 | this.fast = false;
7 | this.gasPedal = 0;
8 | this.brakePedal = 0;
9 | this.handbrake = 0;
10 | this.steer = 0;
11 | this.minCruiseSpeed = 0;
12 | this.manualSteerSensitivity = 1;
13 | this.autoSteerSensitivity = 1;
14 | this.cruiseSpeedMultiplier = 1;
15 | this.laneShift = 0;
16 | }
17 |
18 | update() {}
19 | }
20 |
21 | const touchesFrom = event => Array.from(event.changedTouches);
22 |
23 | class TouchInput extends Input {
24 | constructor() {
25 | super();
26 | this.manualSteerSensitivity = 0.025;
27 | this.deltas = new Map();
28 |
29 | document.addEventListener("touchstart", event => {
30 | touchesFrom(event).filter(touch => touch.target.type !== "button").forEach(touch => {
31 | this.deltas.set(touch.identifier, {x: touch.clientX, y: touch.clientY, dx: 0, dy: 0});
32 | });
33 | });
34 | document.addEventListener("touchend", event => {
35 | touchesFrom(event).filter(touch => this.deltas.has(touch.identifier)).forEach(touch => {
36 | this.deltas.delete(touch.identifier);
37 | });
38 | });
39 | document.addEventListener("touchmove", event => {
40 | touchesFrom(event).filter(touch => this.deltas.has(touch.identifier)).forEach(touch => {
41 | const delta = this.deltas.get(touch.identifier);
42 | [delta.dx, delta.dy] = [touch.clientX - delta.x, touch.clientY - delta.y];
43 | });
44 | });
45 | document.addEventListener("touchcancel", event => this.deltas.clear());
46 |
47 | document.addEventListener("mousedown", event => {
48 | if (event.target.type === "button") return;
49 | if (event.button !== 0) return;
50 | this.deltas.set(-1, {x: event.clientX, y: event.clientY, dx: 0, dy: 0 });
51 | });
52 | document.addEventListener("mouseup", event => this.deltas.delete(-1));
53 | document.addEventListener("mousemove", event => {
54 | const delta = this.deltas.get(-1);
55 | if (delta != null) [delta.dx, delta.dy] = [event.clientX - delta.x, event.clientY - delta.y];
56 | });
57 | document.addEventListener("mouseout", event => this.deltas.delete(-1));
58 | }
59 |
60 | update() {
61 | const total = {x: 0, y: 0};
62 | for (const delta of this.deltas.values()) {
63 | total.x += delta.dx;
64 | total.y += delta.dy;
65 | }
66 |
67 | const minDimension = Math.min(window.innerWidth, window.innerHeight);
68 |
69 | total.x = Math.min(1, Math.max(-1, total.x * 2 / minDimension));
70 | total.y = Math.min(1, Math.max(-1, total.y * 2 / minDimension));
71 |
72 | this.steer = -total.x;
73 | this.gasPedal = total.y < 0 ? -total.y : 0;
74 | this.brakePedal = total.y > 0 ? total.y : 0;
75 | }
76 | }
77 |
78 | class KeyboardInput extends Input {
79 | constructor() {
80 | super();
81 | this.manualSteerSensitivity = 0.025;
82 | this.keysDown = new Set();
83 | document.addEventListener("keydown", event => this.keysDown.add(event.code));
84 | document.addEventListener("keyup", event => this.keysDown.delete(event.code));
85 | }
86 |
87 | update() {
88 | this.slow = this.keysDown.has("ShiftLeft") || this.keysDown.has("ShiftRight");
89 | this.fast = this.keysDown.has("ControlLeft") || this.keysDown.has("ControlRight");
90 | this.gasPedal = this.keysDown.has("ArrowUp") ? 1 : 0;
91 | this.brakePedal = this.keysDown.has("ArrowDown") ? 1 : 0;
92 | this.handbrake = this.keysDown.has("Space") ? 1 : 0;
93 | this.steer = 0;
94 | if (this.keysDown.has("ArrowLeft")) this.steer += 1;
95 | if (this.keysDown.has("ArrowRight")) this.steer -= 1;
96 | }
97 | }
98 |
99 | class OneSwitchInput extends Input {
100 | constructor() {
101 | super();
102 | this.manualSteerSensitivity = 0.0125;
103 | this.autoSteerSensitivity = 0.5;
104 | this.minCruiseSpeed = 0.5;
105 | this.cruiseSpeedMultiplier = 4;
106 | this.shiftSpeed = -0.2;
107 | document.addEventListener("click", event => {
108 | if (event.target.type !== "button") this.shiftSpeed *= -1;
109 | });
110 | document.addEventListener("touchstart", event => {
111 | if (event.target.type !== "button") this.shiftSpeed *= -1;
112 | });
113 | }
114 |
115 | update() {
116 | this.laneShift += this.shiftSpeed;
117 | }
118 | }
119 |
120 | class EyeGazeInput extends Input {
121 | constructor() {
122 | super();
123 | this.manualSteerSensitivity = 0.025;
124 | this.autoSteerSensitivity = 1;
125 | this.minCruiseSpeed = 0.5;
126 | this.cruiseSpeedMultiplier = 4;
127 | this.xRatio = 0.5;
128 | this.yRatio = 0.5;
129 | document.addEventListener("mousemove", event => {
130 | this.xRatio = event.clientX / window.innerWidth;
131 | this.yRatio = event.clientY / window.innerWidth;
132 | });
133 | document.addEventListener("touchstart", event => {
134 | this.xRatio = touchesFrom(event).pop().clientX / window.innerWidth;
135 | this.yRatio = touchesFrom(event).pop().clientY / window.innerWidth;
136 | });
137 | }
138 |
139 | update() {
140 | const HORIZONTAL_DEAD_ZONE = 0.45;
141 | const VERTICAL_DEAD_ZONE = 0.35;
142 |
143 | this.steer = 0;
144 | if (this.xRatio < 0.5 - HORIZONTAL_DEAD_ZONE) this.steer = 1;
145 | if (this.xRatio > 0.5 + HORIZONTAL_DEAD_ZONE) this.steer = -1;
146 |
147 | this.gasPedal = (this.yRatio < 0.5 - VERTICAL_DEAD_ZONE) ? 1 : 0;
148 | this.brakePedal = (this.yRatio > 0.5 + VERTICAL_DEAD_ZONE) ? 1 : 0;
149 | }
150 | }
151 |
--------------------------------------------------------------------------------
/legacy/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | Drivey.js - GL on Wheels
5 |
6 |
7 |
103 |
104 |
105 |
106 |
107 |
108 |
109 |
110 |
111 |
112 |
113 |
114 |
115 |
116 |
117 |
118 |
119 |
120 |
121 |
122 |
123 |
124 |
125 |
126 |
127 |
128 |
129 |
130 |
131 |
132 |
133 |
134 |
135 |
136 |
137 |
138 |
139 |
140 |
141 |
142 |
143 |
147 |
148 |
149 |
--------------------------------------------------------------------------------
/readme_assets/extrusion_4.svg:
--------------------------------------------------------------------------------
1 |
36 |
--------------------------------------------------------------------------------
/legacy/js/Level.js:
--------------------------------------------------------------------------------
1 | "use strict";
2 |
3 | class Level {
4 | constructor() {
5 | this.tint = new THREE.Color(0.7, 0.7, 0.7);
6 | this.skyGradient = 0;
7 | this.skyHigh = 0;
8 | this.skyLow = 0;
9 | this.ground = 0;
10 | this.roadPath = this.makeRoadPath();
11 | this.cruiseSpeed = 50 / 3; // 50 kph
12 |
13 | this.laneWidth = 2;
14 | this.numLanes = 1;
15 |
16 | const meshes = [];
17 | const transparentMeshes = [];
18 | const skyMeshes = [];
19 | this.build(meshes, transparentMeshes, skyMeshes);
20 | this.finish(meshes, transparentMeshes, skyMeshes);
21 | }
22 |
23 | dispose() {
24 | if (this.world.parent != null) {
25 | this.world.parent.remove(this.world);
26 | }
27 | this.world.children.forEach(child => child.geometry.dispose());
28 | this.world = null;
29 | }
30 |
31 | build(meshes, transparentMeshes, skyMeshes) {}
32 |
33 | drawRoadLine(roadPath, shapePath, xPos, width, style, start, end) {
34 | if (start == end) {
35 | return shapePath;
36 | }
37 | switch (style.type) {
38 | case RoadLineStyle.type.SOLID:
39 | {
40 | const [pointSpacing] = [style.pointSpacing];
41 | width = Math.abs(width);
42 | const outsideOffset = xPos - width / 2;
43 | const insideOffset = xPos + width / 2;
44 | const outsidePoints = [];
45 | const insidePoints = [];
46 | outsidePoints.push(getExtrudedPointAt(roadPath.curve, start, outsideOffset));
47 | insidePoints.push(getExtrudedPointAt(roadPath.curve, start, insideOffset));
48 | if (pointSpacing > 0) {
49 | const psFraction = pointSpacing / roadPath.length;
50 | let i = Math.ceil(start / psFraction) * psFraction;
51 | if (i == start) i += psFraction;
52 | while (i < end) {
53 | outsidePoints.push(getExtrudedPointAt(roadPath.curve, i, outsideOffset));
54 | insidePoints.push(getExtrudedPointAt(roadPath.curve, i, insideOffset));
55 | i += psFraction;
56 | }
57 | }
58 | outsidePoints.push(getExtrudedPointAt(roadPath.curve, end, outsideOffset));
59 | insidePoints.push(getExtrudedPointAt(roadPath.curve, end, insideOffset));
60 | outsidePoints.reverse();
61 | if (start == 0 && end == 1) {
62 | addPath(shapePath, makePolygonPath(outsidePoints));
63 | addPath(shapePath, makePolygonPath(insidePoints));
64 | } else {
65 | addPath(shapePath, makePolygonPath(outsidePoints.concat(insidePoints)));
66 | }
67 | }
68 | break;
69 | case RoadLineStyle.type.DASH:
70 | {
71 | const [off, on, pointSpacing] = [style.off, style.on, style.pointSpacing];
72 | let dashStart = start;
73 | const dashSpan = (on + off) / roadPath.length;
74 | const dashLength = (dashSpan * on) / (on + off);
75 | while (dashStart < end) {
76 | this.drawRoadLine(roadPath, shapePath, xPos, width, RoadLineStyle.SOLID(pointSpacing), dashStart, Math.min(end, dashStart + dashLength));
77 | dashStart += dashSpan;
78 | }
79 | }
80 | break;
81 | case RoadLineStyle.type.DOT:
82 | {
83 | const [spacing] = [style.spacing];
84 | let dotStart = start;
85 | const dotSpan = spacing / roadPath.length;
86 | while (dotStart < end) {
87 | const pos = getExtrudedPointAt(roadPath.curve, dotStart, xPos);
88 | addPath(shapePath, makeCirclePath(pos.x, pos.y, width));
89 | dotStart += dotSpan;
90 | }
91 | }
92 | break;
93 | }
94 | return shapePath;
95 | }
96 |
97 | finish(meshes, transparentMeshes, skyMeshes) {
98 | this.world = new THREE.Group();
99 | meshes.forEach(flattenMesh);
100 | const combinedMesh = mergeMeshes(meshes);
101 | combinedMesh.geometry.computeBoundingSphere();
102 | this.worldRadius = combinedMesh.geometry.boundingSphere.radius;
103 | if (meshes.length > 0) this.world.add(combinedMesh);
104 | meshes.forEach(mesh => mesh.geometry.dispose());
105 | meshes.length = 0;
106 |
107 | transparentMeshes.forEach(flattenMesh);
108 | if (transparentMeshes.length > 0) this.world.add(mergeMeshes(transparentMeshes));
109 | transparentMeshes.forEach(mesh => mesh.geometry.dispose());
110 | transparentMeshes.length = 0;
111 |
112 | skyMeshes.forEach(flattenMesh);
113 | this.sky = new THREE.Group();
114 | if (skyMeshes.length > 0) this.sky.add(mergeMeshes(skyMeshes));
115 | skyMeshes.forEach(mesh => mesh.geometry.dispose());
116 | skyMeshes.length = 0;
117 | }
118 |
119 | makeRoadPath(windiness = 5) {
120 | const points = [];
121 | let minX = Infinity;
122 | let maxX = -Infinity;
123 | let minY = Infinity;
124 | let maxY = -Infinity;
125 |
126 | const n = 16;
127 | for (let i = 0; i < n; i++) {
128 | const theta = (i * Math.PI * 2) / n;
129 | const radius = Math.random() + windiness;
130 | const point = new THREE.Vector2(Math.cos(theta) * -radius, Math.sin(theta) * radius);
131 | points.push(point);
132 | minX = Math.min(minX, point.x);
133 | maxX = Math.max(maxX, point.x);
134 | minY = Math.min(minY, point.y);
135 | maxY = Math.max(maxY, point.y);
136 | }
137 | const centerX = (maxX + minX) * 0.5;
138 | const centerY = (maxY + minY) * 0.5;
139 | const width = maxX - minX;
140 | const height = maxY - minY;
141 | for (const point of points) {
142 | point.x -= centerX;
143 | point.y -= centerY;
144 | point.y *= width / height;
145 | point.x *= 80; // 400
146 | point.y *= 80; // 400
147 | }
148 | return new RoadPath(points);
149 | }
150 | }
151 |
--------------------------------------------------------------------------------
/legacy/js/Buttons.js:
--------------------------------------------------------------------------------
1 | "use strict";
2 |
3 | class Buttons {
4 | constructor() {
5 | this.listeners = [];
6 | this.buttonsContainer = document.createElement("div");
7 | this.buttonsContainer.id = "buttonsContainer";
8 | this.element = document.createElement("div");
9 | this.element.id = "buttons";
10 | document.body.appendChild(this.buttonsContainer);
11 | this.buttonsContainer.appendChild(this.element);
12 |
13 | this.addButton("cruise", 2, [0, 1, 2, 3], value => {
14 | const index = parseInt(value);
15 | return `autopilot
16 | ${
17 | Array(3).fill().map((_, id) => {
18 | return ``;
19 | }).join(" ")
20 | }
`;
21 | });
22 |
23 | this.addButton("npcCars", 0, [0, 1, 2, 3], value => {
24 | const index = parseInt(value);
25 | return `cars
26 | ${
27 | Array(3).fill().map((_, id) => {
28 | return ``;
29 | }).join(" ")
30 | }
`;
31 | });
32 |
33 | this.addButton("drivingSide", "right", ["left", "right"], value => `
34 | side
35 |
36 | ${value}
37 |
`);
38 |
39 | this.addButton("camera", "driver", ["driver", "hood", "rear", "chase", "aerial", "satellite"], value => `
40 | camera
41 |
42 | ${value}
43 |
`);
44 |
45 | this.addButton("effect", "ombré", ["ombré", "wireframe", "technicolor", "merveilles"], value => `
46 | effect
47 |
48 | ${value}
49 |
`);
50 |
51 | this.addButton("controls", isMobile ? "touch" : "arrows", ["touch", "arrows", "1 switch", "eye gaze"], value => `
52 | controls
53 |
54 | ${value.replace("_", "
")}
55 |
`);
56 |
57 | this.addButton("quality", "high", ["high", "medium", "low"], value => `
58 | quality
59 |
60 | ${value}
61 |
`);
62 |
63 | this.addButton("music", "", [""], value => `
64 | mixtape
65 |
66 | play
67 |
`);
68 |
69 | this.addButton("level", "industrial", ["industrial", "night", "city", "tunnel", "beach", "warp", "spectre", "nullarbor", "marshland"], value => `
70 | level select
71 |
72 | ${value}
73 |
`);
74 |
75 | const stylesheet = Array.from(document.styleSheets).find(sheet => sheet.title === "main");
76 | this.bodyRule = Array.from(stylesheet.cssRules).find(rule => rule.selectorText === "body, colors");
77 |
78 | document.addEventListener("mousemove", this.onMouse.bind(this), false);
79 | document.addEventListener("mousedown", this.onMouse.bind(this), false);
80 | document.addEventListener("mouseup", this.onMouse.bind(this), false);
81 | document.addEventListener("touchstart", this.onMouse.bind(this), false);
82 | document.addEventListener("touchmove", this.onMouse.bind(this), false);
83 | document.addEventListener("touchend", this.onMouse.bind(this), false);
84 | }
85 |
86 | onMouse() {
87 | this.wakeUp()
88 | }
89 |
90 | wakeUp() {
91 | if (this.buttonsContainer.className === "awake") return;
92 | clearTimeout(this.awakeTimer);
93 | this.buttonsContainer.className = "awake";
94 | this.awakeTimer = setTimeout(() => this.buttonsContainer.className = "", 3000);
95 | }
96 |
97 | addListener(func) {
98 | if (!this.listeners.includes(func)) {
99 | this.listeners.push(func);
100 | }
101 | }
102 |
103 | dispatch(id, value) {
104 | for (const listener of this.listeners) {
105 | listener(id, value);
106 | }
107 | }
108 |
109 | addButton(id, defaultValue, allValues, labelMaker) {
110 | const button = document.createElement("button");
111 | allValues = allValues.map(value => value.toString());
112 | button.allValues = allValues;
113 | button.value = defaultValue.toString();
114 | button.index = allValues.indexOf(button.value);
115 | button.id = `button_${id}`;
116 | button.name = id;
117 | button.type = "button";
118 | button.innerHTML = labelMaker(button.value);
119 | button.labelMaker = labelMaker;
120 | this.element.appendChild(button);
121 | button.addEventListener("click", () => {
122 | button.index = (button.index + 1) % allValues.length;
123 | button.value = allValues[button.index];
124 | button.innerHTML = labelMaker(button.value);
125 | this.dispatch(id, button.value);
126 | });
127 | }
128 |
129 | setButton(id, value) {
130 | const button = document.getElementById(`button_${id}`);
131 | button.index = button.allValues.indexOf(value);
132 | if (button.index === -1) {
133 | button.index = 0;
134 | }
135 | button.value = button.allValues[button.index];
136 | button.innerHTML = button.labelMaker(button.value);
137 | this.dispatch(id, button.value);
138 | }
139 |
140 | setColors(backgroundColor, borderColor, lightColor) {
141 | this.bodyRule.style.setProperty("--dashboard-background-color", `#${backgroundColor.getHex().toString(16).padStart(6, "0")}`);
142 | this.bodyRule.style.setProperty("--dashboard-border-color", `#${borderColor.getHex().toString(16).padStart(6, "0")}`);
143 | this.bodyRule.style.setProperty("--dashboard-light-color", `#${lightColor.getHex().toString(16).padStart(6, "0")}`);
144 | }
145 | }
146 |
--------------------------------------------------------------------------------
/js/Input.js:
--------------------------------------------------------------------------------
1 | class Input {
2 | constructor() {
3 | this.slow = false;
4 | this.fast = false;
5 | this.gasPedal = 0;
6 | this.brakePedal = 0;
7 | this.handbrake = 0;
8 | this.steer = 0;
9 | this.minCruiseSpeed = 0;
10 | this.manualSteerSensitivity = 1;
11 | this.autoSteerSensitivity = 1;
12 | this.cruiseSpeedMultiplier = 1;
13 | this.laneShift = 0;
14 | }
15 |
16 | update() {}
17 | }
18 |
19 | const touchesFrom = (event) => Array.from(event.changedTouches);
20 |
21 | class TouchInput extends Input {
22 | constructor() {
23 | super();
24 | this.manualSteerSensitivity = 0.025;
25 | this.deltas = new Map();
26 |
27 | document.addEventListener("touchstart", (event) => {
28 | touchesFrom(event)
29 | .filter((touch) => touch.target.type !== "button")
30 | .forEach((touch) => {
31 | this.deltas.set(touch.identifier, {
32 | x: touch.clientX,
33 | y: touch.clientY,
34 | dx: 0,
35 | dy: 0,
36 | });
37 | });
38 | });
39 | document.addEventListener("touchend", (event) => {
40 | touchesFrom(event)
41 | .filter((touch) => this.deltas.has(touch.identifier))
42 | .forEach((touch) => {
43 | this.deltas.delete(touch.identifier);
44 | });
45 | });
46 | document.addEventListener("touchmove", (event) => {
47 | touchesFrom(event)
48 | .filter((touch) => this.deltas.has(touch.identifier))
49 | .forEach((touch) => {
50 | const delta = this.deltas.get(touch.identifier);
51 | [delta.dx, delta.dy] = [touch.clientX - delta.x, touch.clientY - delta.y];
52 | });
53 | });
54 | document.addEventListener("touchcancel", (event) => this.deltas.clear());
55 | document.addEventListener("mousedown", (event) => {
56 | if (event.target.type === "button") return;
57 | if (event.button !== 0) return;
58 | this.deltas.set(-1, { x: event.clientX, y: event.clientY, dx: 0, dy: 0 });
59 | });
60 | document.addEventListener("mouseup", (event) => this.deltas.delete(-1));
61 | document.addEventListener("mousemove", (event) => {
62 | const delta = this.deltas.get(-1);
63 | if (delta != null) [delta.dx, delta.dy] = [event.clientX - delta.x, event.clientY - delta.y];
64 | });
65 | document.addEventListener("mouseout", (event) => this.deltas.delete(-1));
66 | }
67 |
68 | update() {
69 | const total = { x: 0, y: 0 };
70 |
71 | for (const delta of this.deltas.values()) {
72 | total.x += delta.dx;
73 | total.y += delta.dy;
74 | }
75 |
76 | const minDimension = Math.min(window.innerWidth, window.innerHeight);
77 |
78 | total.x = Math.min(1, Math.max(-1, (total.x * 2) / minDimension));
79 | total.y = Math.min(1, Math.max(-1, (total.y * 2) / minDimension));
80 |
81 | this.steer = -total.x;
82 | this.gasPedal = total.y < 0 ? -total.y : 0;
83 | this.brakePedal = total.y > 0 ? total.y : 0;
84 | }
85 | }
86 |
87 | class KeyboardInput extends Input {
88 | constructor() {
89 | super();
90 | this.manualSteerSensitivity = 0.025;
91 | this.keysDown = new Set();
92 | document.addEventListener("keydown", (event) => this.keysDown.add(event.code));
93 | document.addEventListener("keyup", (event) => this.keysDown.delete(event.code));
94 | }
95 |
96 | update() {
97 | this.slow = this.keysDown.has("ShiftLeft") || this.keysDown.has("ShiftRight");
98 | this.fast = this.keysDown.has("ControlLeft") || this.keysDown.has("ControlRight");
99 | this.gasPedal = this.keysDown.has("ArrowUp") ? 1 : 0;
100 | this.brakePedal = this.keysDown.has("ArrowDown") ? 1 : 0;
101 | this.handbrake = this.keysDown.has("Space") ? 1 : 0;
102 | this.steer = 0;
103 | if (this.keysDown.has("ArrowLeft")) this.steer += 1;
104 | if (this.keysDown.has("ArrowRight")) this.steer -= 1;
105 | }
106 | }
107 |
108 | class OneSwitchInput extends Input {
109 | constructor() {
110 | super();
111 | this.manualSteerSensitivity = 0.0125;
112 | this.autoSteerSensitivity = 0.5;
113 | this.minCruiseSpeed = 0.5;
114 | this.cruiseSpeedMultiplier = 4;
115 | this.shiftSpeed = -0.2;
116 |
117 | document.addEventListener("click", (event) => {
118 | if (event.target.type !== "button") this.shiftSpeed *= -1;
119 | });
120 | document.addEventListener("touchstart", (event) => {
121 | if (event.target.type !== "button") this.shiftSpeed *= -1;
122 | });
123 | }
124 |
125 | update() {
126 | this.laneShift += this.shiftSpeed;
127 | }
128 | }
129 |
130 | class EyeGazeInput extends Input {
131 | constructor() {
132 | super();
133 | this.manualSteerSensitivity = 0.025;
134 | this.autoSteerSensitivity = 1;
135 | this.minCruiseSpeed = 0.5;
136 | this.cruiseSpeedMultiplier = 4;
137 | this.xRatio = 0.5;
138 | this.yRatio = 0.5;
139 |
140 | document.addEventListener("mousemove", (event) => {
141 | this.xRatio = event.clientX / window.innerWidth;
142 | this.yRatio = event.clientY / window.innerWidth;
143 | });
144 | document.addEventListener("touchstart", (event) => {
145 | this.xRatio = touchesFrom(event).pop().clientX / window.innerWidth;
146 | this.yRatio = touchesFrom(event).pop().clientY / window.innerWidth;
147 | });
148 | }
149 |
150 | update() {
151 | const HORIZONTAL_DEAD_ZONE = 0.45;
152 | const VERTICAL_DEAD_ZONE = 0.35;
153 |
154 | this.steer = 0;
155 | if (this.xRatio < 0.5 - HORIZONTAL_DEAD_ZONE) this.steer = 1;
156 | if (this.xRatio > 0.5 + HORIZONTAL_DEAD_ZONE) this.steer = -1;
157 |
158 | this.gasPedal = this.yRatio < 0.5 - VERTICAL_DEAD_ZONE ? 1 : 0;
159 | this.brakePedal = this.yRatio > 0.5 + VERTICAL_DEAD_ZONE ? 1 : 0;
160 | }
161 | }
162 |
163 | const controlSchemesByName = new Map([
164 | ["touch", new TouchInput()],
165 | ["arrows", new KeyboardInput()],
166 | ["1 switch", new OneSwitchInput()],
167 | ["eye gaze", new EyeGazeInput()],
168 | ]);
169 |
170 | export { Input, controlSchemesByName };
171 |
--------------------------------------------------------------------------------
/js/levelSchemas.js:
--------------------------------------------------------------------------------
1 | import { Color, Vector2 } from "./../lib/three/three.module.js";
2 | import { verbatim, safeParseFloat, safeParseInt, parseNumberList, parseColor, parseVec2, parseBool } from "./parseFunctions.js";
3 |
4 | const idAttribute = { id: {} };
5 | const shadeAttribute = {
6 | shade: { parseFunc: safeParseFloat, defaultValue: 0.5 },
7 | };
8 | const alphaAttribute = {
9 | alpha: { parseFunc: safeParseFloat, defaultValue: 1 },
10 | };
11 | const roadAttribute = {
12 | road: { parseFunc: verbatim, defaultValue: "{mainRoad}" },
13 | };
14 | const fadeAttribute = { fade: { parseFunc: safeParseFloat, defaultValue: 0 } };
15 | const spacingAttribute = {
16 | spacing: { parseFunc: safeParseFloat, defaultValue: 1 },
17 | };
18 |
19 | const horizontalTagSchema = {
20 | mirror: { parseFunc: parseBool, defaultValue: false },
21 | x: { parseFunc: safeParseFloat, defaultValue: 0 },
22 | width: { parseFunc: safeParseFloat, defaultValue: 1 },
23 | };
24 |
25 | const verticalTagSchema = {
26 | y: { parseFunc: safeParseFloat, defaultValue: 0 },
27 | height: { parseFunc: safeParseFloat, defaultValue: 0 },
28 | };
29 |
30 | const numberTagSchema = {
31 | ...idAttribute,
32 | value: { parseFunc: safeParseFloat },
33 | };
34 |
35 | const repeatTagSchema = {
36 | ...idAttribute,
37 | value: { parseFunc: safeParseInt, defaultValue: 1 },
38 | };
39 |
40 | const shapeTagSchema = {
41 | ...shadeAttribute,
42 | ...alphaAttribute,
43 | ...fadeAttribute,
44 | ...verticalTagSchema,
45 | scale: { parseFunc: parseVec2, defaultValue: new Vector2(1, 1) },
46 | };
47 |
48 | const segmentTagSchema = {
49 | start: { parseFunc: safeParseFloat, defaultValue: NaN },
50 | end: { parseFunc: safeParseFloat, defaultValue: NaN },
51 | };
52 |
53 | const lineTagSchema = {
54 | ...roadAttribute,
55 | ...horizontalTagSchema,
56 | ...segmentTagSchema,
57 | };
58 |
59 | const dashedTagSchema = {
60 | ...lineTagSchema,
61 | ...spacingAttribute,
62 | length: { parseFunc: safeParseFloat, defaultValue: 1 },
63 | };
64 |
65 | const solidAttribute = {
66 | ...lineTagSchema,
67 | spacing: { parseFunc: safeParseFloat, defaultValue: 0 },
68 | };
69 |
70 | const dottedTagSchema = {
71 | ...lineTagSchema,
72 | ...spacingAttribute,
73 | };
74 |
75 | const cityscapeTagSchema = {
76 | ...roadAttribute,
77 | rowSpacing: { parseFunc: safeParseFloat, defaultValue: 200 },
78 | columnSpacing: { parseFunc: safeParseFloat, defaultValue: 200 },
79 | heights: { parseFunc: parseNumberList, defaultValue: [50] },
80 | width: { parseFunc: safeParseFloat, defaultValue: 100 },
81 | proximity: { parseFunc: safeParseFloat, defaultValue: 100 },
82 | radius: { parseFunc: safeParseFloat, defaultValue: 2000 },
83 | ...shadeAttribute,
84 | };
85 |
86 | const cloudsTagSchema = {
87 | count: { parseFunc: safeParseInt, defaultValue: 100 },
88 | ...shadeAttribute,
89 | scale: { parseFunc: parseVec2, defaultValue: new Vector2(1000, 1000) },
90 | altitude: { parseFunc: safeParseFloat, defaultValue: 0 },
91 | cloudRadius: { parseFunc: safeParseFloat, defaultValue: 50 },
92 | };
93 |
94 | const roadTagSchema = {
95 | ...idAttribute,
96 | basis: { parseFunc: verbatim, defaultValue: null },
97 | windiness: { parseFunc: safeParseFloat, defaultValue: 0 },
98 | scale: { parseFunc: parseVec2, defaultValue: new Vector2(1, 1) },
99 | seed: { parseFunc: safeParseInt, defaultValue: NaN },
100 | splinePoints: { parseFunc: parseNumberList, defaultValue: [] },
101 | length: { parseFunc: safeParseFloat, defaultValue: 1000 },
102 | };
103 |
104 | const driveyTagSchema = {
105 | ...roadTagSchema,
106 | id: { parseFunc: (_) => "mainRoad", defaultValue: "mainRoad" },
107 | name: { parseFunc: verbatim, defaultValue: "Untitled Level" },
108 | tint: { parseFunc: parseColor, defaultValue: new Color(0.7, 0.7, 0.7) },
109 | skyHigh: { parseFunc: safeParseFloat, defaultValue: 0 },
110 | skyLow: { parseFunc: safeParseFloat, defaultValue: 0 },
111 | ground: { parseFunc: safeParseFloat, defaultValue: 0 },
112 | cruiseSpeed: { parseFunc: safeParseFloat, defaultValue: 50 / 3 }, // 50 kph
113 | laneWidth: { parseFunc: safeParseFloat, defaultValue: 2 },
114 | numLanes: { parseFunc: safeParseFloat, defaultValue: 1 },
115 | };
116 |
117 | const featureTagSchema = {
118 | ...spacingAttribute,
119 | ...roadAttribute,
120 | ...segmentTagSchema,
121 | };
122 |
123 | const partTagSchema = {
124 | ...shadeAttribute,
125 | ...alphaAttribute,
126 | ...fadeAttribute,
127 | ...horizontalTagSchema,
128 | ...verticalTagSchema,
129 | z: { parseFunc: safeParseFloat, defaultValue: 0 },
130 | };
131 |
132 | const boxTagSchema = {
133 | ...partTagSchema,
134 | length: { parseFunc: safeParseFloat, defaultValue: 0 },
135 | };
136 |
137 | const diskTagSchema = {
138 | ...partTagSchema,
139 | };
140 |
141 | const wireTagSchema = {
142 | ...partTagSchema,
143 | };
144 |
145 | const schemasByType = {
146 | number: numberTagSchema,
147 | repeat: repeatTagSchema,
148 | shape: shapeTagSchema,
149 | dashed: dashedTagSchema,
150 | solid: solidAttribute,
151 | dotted: dottedTagSchema,
152 | cityscape: cityscapeTagSchema,
153 | clouds: cloudsTagSchema,
154 | road: roadTagSchema,
155 | drivey: driveyTagSchema,
156 | feature: featureTagSchema,
157 | box: boxTagSchema,
158 | disk: diskTagSchema,
159 | wire: wireTagSchema,
160 | };
161 |
162 | const hoistForId = (renderFunc) => (attributes) => ({
163 | [attributes.id]: renderFunc(attributes),
164 | });
165 |
166 | const hoistsByType = {
167 | number: hoistForId(({ value }) => value),
168 | repeat: hoistForId(({ value }) => value),
169 | road: hoistForId(verbatim),
170 | drivey: hoistForId(verbatim),
171 | };
172 |
173 | const getDefaultValuesForType = (type) => {
174 | const schema = schemasByType[type] ?? {};
175 | return Object.fromEntries(Object.entries(schema).map(([name, { defaultValue }]) => [name, defaultValue]));
176 | };
177 |
178 | const getParseFuncForAttribute = (type, name) => {
179 | const schema = schemasByType[type] ?? {};
180 | return schema[name]?.parseFunc ?? verbatim;
181 | };
182 |
183 | const getHoistForType = (type) => hoistsByType[type];
184 |
185 | export { getDefaultValuesForType, getParseFuncForAttribute, getHoistForType };
186 |
--------------------------------------------------------------------------------
/js/Car.js:
--------------------------------------------------------------------------------
1 | import { Vector2 } from "./../lib/three/three.module.js";
2 | import { getAngle, lerp, rotate, rotateY, sign } from "./math.js";
3 |
4 | export default class Car {
5 | constructor() {
6 | this.lastPos = new Vector2();
7 | this.pos = new Vector2();
8 | this.vel = new Vector2();
9 | this.lastVel = new Vector2();
10 | }
11 |
12 | place(road, approximation, along, laneWidth, numLanes, drivingSide, roadDir, initialSpeed) {
13 | this.laneOffset = laneWidth * (0.5 + Math.floor(Math.random() * numLanes));
14 | this.weaving = (Math.random() - 0.5) * 0.5 * laneWidth;
15 |
16 | const pos = road.getPoint(along).add(road.getNormal(along).multiplyScalar((this.laneOffset + this.weaving) * roadDir * drivingSide));
17 | const tangent = road.getTangent(along).multiplyScalar(roadDir);
18 | const angle = getAngle(tangent);
19 | const vel = tangent.multiplyScalar(initialSpeed * 2);
20 |
21 | this.road = road;
22 | this.approximation = approximation;
23 |
24 | this.pos.copy(pos);
25 | this.lastPos.copy(pos);
26 | this.vel.copy(vel);
27 | this.lastVel.copy(vel);
28 |
29 | this.accelerate = 0;
30 | this.handbrake = 0;
31 |
32 | this.angle = angle;
33 |
34 | this.tilt = 0;
35 | this.pitch = 0;
36 |
37 | this.tiltV = 0;
38 | this.pitchV = 0;
39 |
40 | this.roadPos = 0;
41 | this.roadDir = roadDir;
42 |
43 | this.steer = 0;
44 | this.steerPos = 0;
45 | this.steerTo = 0;
46 | this.steerV = 0;
47 |
48 | this.sliding = false;
49 | this.spin = 0;
50 | }
51 |
52 | remove() {
53 | this.road = null;
54 | this.approximation = null;
55 | }
56 |
57 | drive(step, cruiseSpeed, controlScheme, drivingSide) {
58 | if (cruiseSpeed > 0) {
59 | this.roadPos += 3 * step * controlScheme.steer * controlScheme.autoSteerSensitivity;
60 | if (this.roadPos > 0.1) this.roadPos -= step;
61 | else if (this.roadPos < -0.1) this.roadPos += step;
62 | if (controlScheme.handbrake < 0.9) {
63 | this.weaving += (Math.random() - 0.5) * 0.025;
64 | this.weaving *= 0.999;
65 | }
66 | this.autoSteer(step, (this.laneOffset + this.weaving + controlScheme.laneShift) * drivingSide);
67 | this.matchSpeed(cruiseSpeed * controlScheme.cruiseSpeedMultiplier);
68 | } else {
69 | const diff = -sign(this.steerTo) * 0.0002 * this.vel.length() * step;
70 | if (Math.abs(diff) >= Math.abs(this.steerTo)) this.steerTo = 0;
71 | else this.steerTo += diff;
72 | this.steerTo = this.steerTo + controlScheme.steer * controlScheme.manualSteerSensitivity * step;
73 | this.accelerate = 0;
74 | }
75 |
76 | this.handbrake = controlScheme.handbrake;
77 | this.accelerate += controlScheme.brakePedal * -2 + controlScheme.gasPedal;
78 | this.advance(step);
79 | }
80 |
81 | autoSteer(step, offset) {
82 | // get goal position, based on position on road 1 second in the future
83 | const dir = this.vel.length() > 0 ? this.vel.clone().normalize() : this.dir();
84 | const lookAhead = 20;
85 | dir.multiplyScalar(lookAhead);
86 | const futurePos = this.pos.clone().add(dir);
87 | const along = this.approximation.getNearest(futurePos);
88 | const targetDir = this.road
89 | .getPoint(along)
90 | .sub(this.pos)
91 | .add(this.road.getNormal(along).multiplyScalar(4 * this.roadPos + this.roadDir * offset));
92 |
93 | // mix it with the slope of the road at that point
94 | let tangent = this.road.getTangent(along);
95 | tangent.multiplyScalar(this.roadDir);
96 | if (targetDir.length() > 0) tangent.lerp(targetDir, 0.1);
97 |
98 | // measure the difference in angle to that point and car's current angle
99 | let newAngle = Math.atan2(tangent.y, tangent.x) - this.angle;
100 | // represent it as an angle between -π and π
101 | while (newAngle > Math.PI) newAngle -= Math.PI * 2;
102 | while (newAngle < -Math.PI) newAngle += Math.PI * 2;
103 | // "normalize" it, so it is no larger than 1 radian
104 | if (Math.abs(newAngle) > 1) newAngle /= Math.abs(newAngle);
105 | // Generate a steerTo value (these are pretty small)
106 | let steerTo = newAngle / (Math.min(targetDir.length() * 0.5, 50) + 1);
107 | if (Math.abs(steerTo) > 0.02) steerTo *= 0.02 / Math.abs(steerTo);
108 | this.steerTo = lerp(this.steerTo, steerTo, Math.min(1, step * 10));
109 | }
110 |
111 | matchSpeed(speed) {
112 | if (this.vel.length() < speed) this.accelerate = 1;
113 | else this.accelerate = speed / this.vel.length();
114 | }
115 |
116 | dir() {
117 | return rotate(new Vector2(1, 0), this.angle);
118 | }
119 |
120 | advance(t) {
121 | if (t <= 0) {
122 | return;
123 | }
124 |
125 | let dir = this.dir();
126 |
127 | const acc = dir.clone().multiplyScalar(this.accelerate).multiplyScalar(10).add(this.vel.clone().multiplyScalar(-0.1));
128 | const newVel = dir.clone().multiplyScalar(this.vel.clone().add(acc.clone().multiplyScalar(t)).dot(dir));
129 |
130 | if (this.handbrake >= 0.9) newVel.set(0, 0, 0);
131 |
132 | if (!this.sliding && newVel.clone().sub(this.vel).length() / t > 750) {
133 | // maximum acceleration allowable?
134 | this.sliding = true;
135 | } else if (this.sliding && newVel.clone().sub(this.vel).length() / t < 50) {
136 | this.sliding = false;
137 | }
138 |
139 | if (this.sliding) {
140 | const friction = newVel.clone().sub(this.vel).clone().normalize().clone().multiplyScalar(20);
141 | this.vel = this.vel.clone().add(friction.clone().multiplyScalar(t));
142 | }
143 |
144 | if (!this.sliding) {
145 | this.vel = newVel;
146 | }
147 |
148 | this.spin = this.vel.clone().dot(dir) * this.steerPos * (this.sliding ? 0.5 : 1.0);
149 | this.angle += this.spin * t;
150 | this.pos = this.pos.clone().add(this.vel.clone().multiplyScalar(t));
151 | const velDiff = this.vel.clone().sub(this.lastVel);
152 | this.tiltV = this.tiltV + (this.tiltV * -0.2 + (velDiff.clone().dot(rotateY(dir, Math.PI * -0.5)) * 0.001) / t - this.tilt) * t * 20;
153 | this.tilt += this.tiltV * t;
154 | this.pitchV = this.pitchV + (this.pitchV * -0.2 + (velDiff.clone().dot(dir) * 0.001) / t - this.pitch) * t * 20;
155 | this.pitch += this.pitchV * t;
156 |
157 | this.steerTo = Math.max((-Math.PI * 2) / 50, Math.min((Math.PI * 2) / 50, this.steerTo));
158 |
159 | this.steerPos = this.steerTo;
160 | this.lastVel = this.vel;
161 | this.lastPos = this.pos;
162 | }
163 | }
164 |
--------------------------------------------------------------------------------
/legacy/js/Dashboard.js:
--------------------------------------------------------------------------------
1 | "use strict";
2 |
3 | class Dashboard {
4 | constructor() {
5 | this.object = new THREE.Group();
6 | const edge1 = 2;
7 | this.backing = this.addDashboardElement(this.makeDashboardBacking(), edge1, true);
8 | this.backing.position.set(-50, -80, -110);
9 | this.speedometer1 = this.addDashboardElement(this.makeSpeedometer(), 0, true);
10 | this.speedometer1.position.set(-25, -35, -105);
11 | this.needle1 = this.addDashboardElement(this.makeNeedle(), 0, true);
12 | this.needle1.position.set(-25, -35, -105);
13 | this.needle1.rotation.z = Math.PI * 1.5;
14 | this.speedometer2 = this.addDashboardElement(this.makeSpeedometer(), 0, true);
15 | this.speedometer2.position.set(-70, -35, -105);
16 | this.needle2 = this.addDashboardElement(this.makeNeedle(), 0, true);
17 | this.needle2.position.set(-70, -35, -105);
18 | this.needle2.rotation.z = Math.PI * 1.5;
19 | this.wheel = this.addDashboardElement(this.makeSteeringWheel(), edge1, true);
20 | this.wheel.position.set(-50, -55, -100);
21 | this.wheel.rotation.z = Math.PI;
22 | this.drivingSide = -1;
23 | }
24 |
25 | addDashboardElement(path, edgeAmount, hasFill) {
26 | if (edgeAmount == null) {
27 | edgeAmount = 0;
28 | }
29 | const element = new THREE.Group();
30 | if (edgeAmount != 0) {
31 | const edge = makeMesh(expandShapePath(path, 1 + edgeAmount, 250), 0, 0, 0.2);
32 | edge.position.z = -0.1;
33 | element.add(edge);
34 | }
35 | if (hasFill && edgeAmount != 0) {
36 | const fill = makeMesh(expandShapePath(path, 1, 250), 0, 0, 0);
37 | fill.position.z = 0;
38 | element.add(fill);
39 | } else if (hasFill) {
40 | const fill1 = makeMesh(path, 0, 240, 0.2);
41 | fill1.position.z = 0;
42 | element.add(fill1);
43 | }
44 | this.object.add(element);
45 | return element;
46 | }
47 |
48 | makeDashboardBacking() {
49 | const pts = [new THREE.Vector2(-200, -40), new THREE.Vector2(-200, 40), new THREE.Vector2(200, 40), new THREE.Vector2(200, -40)];
50 | const path = makeSplinePath(pts, true);
51 | const shapePath = new THREE.ShapePath();
52 | shapePath.subPaths.push(path);
53 | return shapePath;
54 | }
55 |
56 | makeSpeedometer() {
57 | const shapePath = new THREE.ShapePath();
58 | const outerRadius = 20;
59 | const innerRadius = outerRadius - 1;
60 | const dashEnd = innerRadius - 2;
61 | const outerRim = makeCirclePath(0, 0, outerRadius);
62 | const innerRim = makeCirclePath(0, 0, innerRadius, false);
63 | shapePath.subPaths.push(outerRim);
64 | shapePath.subPaths.push(innerRim);
65 | const nudge = Math.PI * 0.0075;
66 | for (let i = 0; i < 10; i++) {
67 | const angle = (Math.PI * 2 * (i + 0.5)) / 10;
68 | shapePath.subPaths.push(
69 | makePolygonPath([
70 | new THREE.Vector2(Math.cos(angle - nudge) * outerRadius, Math.sin(angle - nudge) * outerRadius),
71 | new THREE.Vector2(Math.cos(angle - nudge) * dashEnd, Math.sin(angle - nudge) * dashEnd),
72 | new THREE.Vector2(Math.cos(angle + nudge) * dashEnd, Math.sin(angle + nudge) * dashEnd),
73 | new THREE.Vector2(Math.cos(angle + nudge) * outerRadius, Math.sin(angle + nudge) * outerRadius)
74 | ])
75 | );
76 | }
77 | return shapePath;
78 | }
79 |
80 | makeNeedle() {
81 | const shapePath = new THREE.ShapePath();
82 | const scale = 40;
83 | shapePath.subPaths.push(
84 | makePolygonPath([
85 | new THREE.Vector2(-0.02 * scale, 0.1 * scale),
86 | new THREE.Vector2(-0.005 * scale, -0.4 * scale),
87 | new THREE.Vector2(0.005 * scale, -0.4 * scale),
88 | new THREE.Vector2(0.02 * scale, 0.1 * scale)
89 | ])
90 | );
91 | return shapePath;
92 | }
93 |
94 | makeSteeringWheel() {
95 | const scale = 148;
96 | const shapePath = new THREE.ShapePath();
97 | const outerRim = makeCirclePath(0, 0, scale * 0.5);
98 | const innerRim1Points = [];
99 | const n = 60;
100 | for (let i = 0; i < 25; i++) {
101 | const theta = ((57 - i) * Math.PI * 2) / n;
102 | const mag = ((i & 1) != 0 ? 0.435 : 0.45) * scale;
103 | innerRim1Points.push(new THREE.Vector2(Math.cos(theta) * mag, Math.sin(theta) * mag));
104 | }
105 | innerRim1Points.reverse();
106 | const innerRim1 = makeSplinePath(innerRim1Points, true);
107 | const innerRim2Points = [];
108 | for (let i = 0; i < 29; i++) {
109 | const theta1 = ((29 - i) * 2 * Math.PI) / n;
110 | const mag1 = ((i & 1) != 0 ? 0.435 : 0.45) * scale;
111 | innerRim2Points.push(new THREE.Vector2(Math.cos(theta1) * mag1, Math.sin(theta1) * mag1));
112 | }
113 | innerRim2Points.push(new THREE.Vector2(scale * 0.25, scale * 0.075));
114 | innerRim2Points.push(new THREE.Vector2(scale * 0.125, scale * 0.2));
115 | innerRim2Points.push(new THREE.Vector2(scale * -0.125, scale * 0.2));
116 | innerRim2Points.push(new THREE.Vector2(scale * -0.25, scale * 0.075));
117 | innerRim2Points.reverse();
118 | const innerRim2 = makeSplinePath(innerRim2Points, true);
119 | shapePath.subPaths.push(outerRim);
120 | shapePath.subPaths.push(innerRim1);
121 | shapePath.subPaths.push(innerRim2);
122 | return shapePath;
123 | }
124 |
125 | get wheelRotation() {
126 | return this.wheel.rotation.z;
127 | }
128 |
129 | set wheelRotation(value) {
130 | this.wheel.rotation.z = value;
131 | return value;
132 | }
133 |
134 | get needle1Rotation() {
135 | return this.needle1.rotation.z;
136 | }
137 |
138 | set needle1Rotation(value) {
139 | this.needle1.rotation.z = value;
140 | return value;
141 | }
142 |
143 | get needle2Rotation() {
144 | return this.needle2.rotation.z;
145 | }
146 |
147 | set needle2Rotation(value) {
148 | this.needle2.rotation.z = value;
149 | return value;
150 | }
151 |
152 | update() {
153 | const lerpAmount = 0.05;
154 | this.backing.position.x = lerp(this.backing.position.x, 50 * this.drivingSide, lerpAmount);
155 | this.wheel.position.x = lerp(this.wheel.position.x, 50 * this.drivingSide, lerpAmount);
156 | const speedometerPositions = [25 * this.drivingSide, 70 * this.drivingSide];
157 | this.speedometer1.position.x = lerp(this.speedometer1.position.x, Math.max(...speedometerPositions), lerpAmount);
158 | this.speedometer2.position.x = lerp(this.speedometer2.position.x, Math.min(...speedometerPositions), lerpAmount);
159 |
160 | this.needle1.position.x = this.speedometer1.position.x;
161 | this.needle2.position.x = this.speedometer2.position.x;
162 | }
163 | }
164 |
--------------------------------------------------------------------------------
/legacy/js/Screen.js:
--------------------------------------------------------------------------------
1 | "use strict";
2 |
3 | /*
4 |
5 | Screen is responsible for initializing the three.js renderer, scene, cameras,
6 | as well as establishing an animation loop and handling window resizing events.
7 |
8 | */
9 |
10 | class Screen {
11 | constructor(animate = true) {
12 | this.time = 0;
13 | this.updateListeners = [];
14 | this.element = document.createElement("div");
15 | document.body.appendChild(this.element);
16 | this.resolution = 1;
17 | this.active = true;
18 | this.scene = new THREE.Scene();
19 | this.camera = new THREE.PerspectiveCamera(90, 1, 0.05, 100000);
20 | this.camera.rotation.order = "YZX";
21 | this.renderer = new THREE.WebGLRenderer();
22 | this.renderer.setPixelRatio(window.devicePixelRatio);
23 | this.element.appendChild(this.renderer.domElement);
24 | this.renderer.domElement.id = "renderer";
25 | this.composer = new THREE.EffectComposer(this.renderer);
26 | this.renderPass = new THREE.RenderPass(this.scene, this.camera);
27 | this.composer.addPass(this.renderPass);
28 | // this.renderPass.renderToScreen = true;
29 |
30 | this.sobelPass = new THREE.ShaderPass(THREE.SobelOperatorShader);
31 | this.composer.addPass(this.sobelPass);
32 | this.sobelPass.enabled = false;
33 |
34 | this.blueprintPass = new THREE.ShaderPass({
35 | uniforms: {
36 | tDiffuse: { type: "t", value: null }
37 | },
38 | vertexShader: `
39 | varying vec2 vUV;
40 | void main() {
41 | vUV = uv;
42 | gl_Position = projectionMatrix * modelViewMatrix * vec4( position, 1.0 );
43 | }
44 | `,
45 | fragmentShader: `
46 | precision mediump float;
47 | uniform sampler2D tDiffuse;
48 | varying vec2 vUV;
49 |
50 | void main() {
51 | vec3 color = vec3(0.1, 0.15, 0.7);
52 | float line = texture2D(tDiffuse, vUV).r;
53 | if (line > 0.02) {
54 | color = clamp(line * 10., 0., 1.) * vec3(0.9, 0.9, 1.);
55 | }
56 | gl_FragColor = vec4(color, 1.);
57 | }
58 | `
59 | });
60 | this.composer.addPass(this.blueprintPass);
61 | this.blueprintPass.enabled = false;
62 |
63 | this.colorCyclePass = new THREE.ShaderPass({
64 | uniforms: {
65 | tDiffuse: { type: "t", value: null },
66 | time: { type: "f", value: 0 }
67 | },
68 | vertexShader: `
69 | varying vec2 vUV;
70 | void main() {
71 | vUV = uv;
72 | gl_Position = projectionMatrix * modelViewMatrix * vec4( position, 1.0 );
73 | }
74 | `,
75 | fragmentShader: `
76 | precision mediump float;
77 | uniform sampler2D tDiffuse;
78 | uniform float time;
79 | varying vec2 vUV;
80 |
81 | vec3 hueShift( const in vec3 color, const in float amount ) {
82 | vec3 p = vec3(0.55735) * dot(vec3(0.55735), color);
83 | vec3 u = color - p;
84 | vec3 v = cross(vec3(0.55735), u);
85 | return u * cos(amount * 6.2832) + v * sin(amount * 6.2832) + p;
86 | }
87 |
88 | void main() {
89 | gl_FragColor = vec4(hueShift(texture2D(tDiffuse, vUV).rgb, time), 1.0);
90 | }
91 | `
92 | });
93 | this.composer.addPass(this.colorCyclePass);
94 | this.colorCyclePass.enabled = false;
95 |
96 | this.aaPass = new THREE.SMAAPass(1, 1);
97 | this.aaPass.renderToScreen = true;
98 | this.composer.addPass(this.aaPass);
99 | window.addEventListener("resize", this.onWindowResize.bind(this), false);
100 | this.onWindowResize();
101 | if (animate) {
102 | this.update();
103 | this.render();
104 | }
105 | // window.addEventListener("focus", this.onWindowFocus.bind(this), false);
106 | // window.addEventListener("blur", this.onWindowBlur.bind(this), false);
107 | this.frameRate = 1;
108 | this.startFrameTime = Date.now();
109 | this.lastFrameTime = this.startFrameTime;
110 | }
111 |
112 | onWindowFocus() {
113 | this.active = true;
114 | }
115 |
116 | onWindowBlur() {
117 | this.active = false;
118 | }
119 |
120 | onWindowResize() {
121 | const aspect = window.innerWidth / window.innerHeight;
122 | this.camera.aspect = aspect;
123 | this.camera.updateProjectionMatrix();
124 | this.setResolution(this.resolution);
125 | }
126 |
127 | setResolution(amount) {
128 | this.resolution = amount;
129 | const width = Math.ceil(window.innerWidth * this.resolution);
130 | const height = Math.ceil(window.innerHeight * this.resolution);
131 | this.renderer.setSize(width, height);
132 | this.renderer.domElement.style.width = "100%";
133 | this.renderer.domElement.style.height = "100%";
134 |
135 | this.sobelPass.uniforms.resolution.value.x = width;
136 | this.sobelPass.uniforms.resolution.value.y = height;
137 |
138 | this.composer.setSize(width, height);
139 | silhouette.uniforms.resolution.value.set(width, height);
140 | }
141 |
142 | setWireframe(enabled) {
143 | this.sobelPass.enabled = enabled;
144 | this.blueprintPass.enabled = enabled;
145 | }
146 |
147 | setCycleColors(enabled) {
148 | this.colorCyclePass.enabled = enabled;
149 | }
150 |
151 | update() {
152 | if (this.active) {
153 | for (const listener of this.updateListeners) {
154 | listener();
155 | }
156 | }
157 | setTimeout(this.update.bind(this), 1000 / 60);
158 | }
159 |
160 | render() {
161 | requestAnimationFrame(this.render.bind(this));
162 | if (!this.active) return;
163 |
164 | this.time += 0.05;
165 | silhouette.uniforms.scramble.value = this.time;
166 | transparent.uniforms.scramble.value = this.time;
167 | this.colorCyclePass.uniforms.time.value = this.time * 0.02;
168 |
169 | if (this.camera != null) {
170 | this.renderPass.camera = this.camera;
171 | this.composer.render();
172 | // this.renderer.render( this.scene, this.camera );
173 | }
174 |
175 | const frameTime = Date.now() - this.startFrameTime;
176 | const frameDuration = frameTime - this.lastFrameTime;
177 | this.frameRate = 1000 / frameDuration;
178 | this.lastFrameTime = frameTime;
179 | }
180 |
181 | addUpdateListener(func) {
182 | if (!this.updateListeners.includes(func)) {
183 | this.updateListeners.push(func);
184 | }
185 | }
186 |
187 | get width() {
188 | return window.innerWidth;
189 | }
190 |
191 | get height() {
192 | return window.innerHeight;
193 | }
194 |
195 | get backgroundColor() {
196 | return this.scene.background;
197 | }
198 |
199 | set backgroundColor(color) {
200 | this.scene.background = color;
201 | return color;
202 | }
203 | }
204 |
--------------------------------------------------------------------------------
/legacy/js/Car.js:
--------------------------------------------------------------------------------
1 | "use strict";
2 |
3 | class Car {
4 | constructor() {
5 | this.lastPos = new THREE.Vector2();
6 | this.pos = new THREE.Vector2();
7 | this.vel = new THREE.Vector2();
8 | this.lastVel = new THREE.Vector2();
9 | }
10 |
11 | place(roadPath, approximation, along, laneWidth, numLanes, drivingSide, roadDir, initialSpeed) {
12 |
13 | this.laneOffset = laneWidth * (0.5 + Math.floor(Math.random() * numLanes));
14 | this.laneOffset += (Math.random() - 0.5) * 0.5 * laneWidth;
15 |
16 | const pos = roadPath.getPoint(along).add(roadPath.getNormal(along).multiplyScalar(this.laneOffset * roadDir * drivingSide));
17 | const tangent = roadPath.getTangent(along).multiplyScalar(roadDir);
18 | const angle = getAngle(tangent);
19 | const vel = tangent.multiplyScalar(initialSpeed * 2);
20 |
21 | this.roadPath = roadPath;
22 | this.approximation = approximation;
23 |
24 | this.pos.copy(pos);
25 | this.lastPos.copy(pos);
26 | this.vel.copy(vel);
27 | this.lastVel.copy(vel);
28 |
29 | this.accelerate = 0;
30 | this.handbrake = 0;
31 |
32 | this.angle = angle;
33 |
34 | this.tilt = 0;
35 | this.pitch = 0;
36 |
37 | this.tiltV = 0;
38 | this.pitchV = 0;
39 |
40 | this.roadPos = 0;
41 | this.roadDir = roadDir;
42 |
43 | this.steer = 0;
44 | this.steerPos = 0;
45 | this.steerTo = 0;
46 | this.steerV = 0;
47 |
48 | this.sliding = false;
49 | this.spin = 0;
50 | }
51 |
52 | remove() {
53 | this.roadPath = null;
54 | this.approximation = null;
55 | }
56 |
57 | drive(step, cruiseSpeed, controlScheme, drivingSide) {
58 | if (cruiseSpeed > 0) {
59 | this.roadPos += 3 * step * controlScheme.steer * controlScheme.autoSteerSensitivity;
60 | if (this.roadPos > 0.1) this.roadPos -= step;
61 | else if (this.roadPos < -0.1) this.roadPos += step;
62 | this.autoSteer(
63 | step,
64 | (this.laneOffset + controlScheme.laneShift) * drivingSide
65 | );
66 | this.matchSpeed(
67 | cruiseSpeed * controlScheme.cruiseSpeedMultiplier
68 | );
69 | } else {
70 | const diff = -sign(this.steerTo) * 0.0002 * this.vel.length() * step;
71 | if (Math.abs(diff) >= Math.abs(this.steerTo)) this.steerTo = 0;
72 | else this.steerTo += diff;
73 | this.steerTo = this.steerTo + controlScheme.steer * controlScheme.manualSteerSensitivity * step;
74 | this.accelerate = 0;
75 | }
76 |
77 | this.handbrake = controlScheme.handbrake;
78 | this.accelerate += controlScheme.brakePedal * -2 + controlScheme.gasPedal;
79 | this.advance(step);
80 | }
81 |
82 | autoSteer(step, offset) {
83 | // get goal position, based on position on road 1 second in the future
84 | const dir = this.vel.length() > 0 ? this.vel.clone().normalize() : this.dir();
85 | const lookAhead = 20;
86 | dir.multiplyScalar(lookAhead);
87 | const futurePos = this.pos.clone().add(dir);
88 | const along = this.approximation.getNearest(futurePos);
89 | const targetDir = this.roadPath
90 | .getPoint(along)
91 | .sub(this.pos)
92 | .add(this.roadPath.getNormal(along).multiplyScalar(4 * this.roadPos + this.roadDir * offset));
93 |
94 | // mix it with the slope of the road at that point
95 | let tangent = this.roadPath.getTangent(along);
96 | tangent.multiplyScalar(this.roadDir);
97 | if (targetDir.length() > 0) tangent.lerp(targetDir, 0.1);
98 |
99 | // measure the difference in angle to that point and car's current angle
100 | let newAngle = Math.atan2(tangent.y, tangent.x) - this.angle;
101 | // represent it as an angle between -π and π
102 | while (newAngle > Math.PI) newAngle -= Math.PI * 2;
103 | while (newAngle < -Math.PI) newAngle += Math.PI * 2;
104 | // "normalize" it, so it is no larger than 1 radian
105 | if (Math.abs(newAngle) > 1) newAngle /= Math.abs(newAngle);
106 | // Generate a steerTo value (these are pretty small)
107 |
108 | let steerTo = newAngle / (Math.min(targetDir.length() * 0.5, 50) + 1);
109 | if (Math.abs(steerTo) > 0.02) steerTo *= 0.02 / Math.abs(steerTo);
110 | this.steerTo = lerp(this.steerTo, steerTo, Math.min(1, step * 10));
111 | }
112 |
113 | matchSpeed(speed) {
114 | if (this.vel.length() < speed) this.accelerate = 1;
115 | else this.accelerate = speed / this.vel.length();
116 | }
117 |
118 | dir() {
119 | return rotate(new THREE.Vector2(1, 0), this.angle);
120 | }
121 |
122 | advance(t) {
123 | if (t <= 0) {
124 | return;
125 | }
126 |
127 | let dir = this.dir();
128 |
129 | const acc = dir
130 | .clone()
131 | .multiplyScalar(this.accelerate)
132 | .multiplyScalar(10)
133 | .add(this.vel.clone().multiplyScalar(-0.1));
134 | const newVel = dir.clone().multiplyScalar(
135 | this.vel
136 | .clone()
137 | .add(acc.clone().multiplyScalar(t))
138 | .dot(dir)
139 | );
140 | if (this.handbrake >= 0.9) newVel.set(0, 0, 0);
141 |
142 | if (
143 | !this.sliding &&
144 | newVel
145 | .clone()
146 | .sub(this.vel)
147 | .length() /
148 | t >
149 | 750
150 | ) {
151 | // maximum acceleration allowable?
152 | this.sliding = true;
153 | } else if (
154 | this.sliding &&
155 | newVel
156 | .clone()
157 | .sub(this.vel)
158 | .length() /
159 | t <
160 | 50
161 | ) {
162 | this.sliding = false;
163 | }
164 |
165 | if (this.sliding) {
166 | const friction = newVel
167 | .clone()
168 | .sub(this.vel)
169 | .clone()
170 | .normalize()
171 | .clone()
172 | .multiplyScalar(20);
173 | this.vel = this.vel.clone().add(friction.clone().multiplyScalar(t));
174 | }
175 |
176 | if (!this.sliding) {
177 | this.vel = newVel;
178 | }
179 |
180 | this.spin = this.vel.clone().dot(dir) * this.steerPos * (this.sliding ? 0.5 : 1.0);
181 | this.angle += this.spin * t;
182 | this.pos = this.pos.clone().add(this.vel.clone().multiplyScalar(t));
183 | const velDiff = this.vel.clone().sub(this.lastVel);
184 | this.tiltV = this.tiltV + (this.tiltV * -0.2 + (velDiff.clone().dot(rotateY(dir, Math.PI * -0.5)) * 0.001) / t - this.tilt) * t * 20;
185 | this.tilt += this.tiltV * t;
186 | this.pitchV = this.pitchV + (this.pitchV * -0.2 + (velDiff.clone().dot(dir) * 0.001) / t - this.pitch) * t * 20;
187 | this.pitch += this.pitchV * t;
188 |
189 | this.steerTo = Math.max(-Math.PI * 2 / 50, Math.min(Math.PI * 2 / 50, this.steerTo));
190 |
191 | this.steerPos = this.steerTo;
192 | this.lastVel = this.vel;
193 | this.lastPos = this.pos;
194 | }
195 | }
196 |
--------------------------------------------------------------------------------
/levels/IndustrialZone.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
53 |
54 |
55 |
56 |
57 |
58 |
59 |
60 |
61 |
62 |
63 |
64 |
65 |
66 |
67 |
68 |
74 |
75 |
76 |
77 |
78 |
79 |
80 |
81 |
82 |
83 |
84 |
85 |
86 |
87 |
88 |
89 |
90 |
91 |
92 |
93 |
94 |
95 |
96 |
97 |
98 |
99 |
100 |
101 |
102 |
103 |
104 |
105 |
106 |
107 |
108 |
109 |
110 |
111 |
112 |
113 |
114 |
115 |
116 |
117 |
118 |
119 |
120 |
--------------------------------------------------------------------------------
/js/Screen.js:
--------------------------------------------------------------------------------
1 | /*
2 |
3 | Screen is responsible for initializing the three.js renderer, scene, cameras,
4 | as well as establishing an animation loop and handling window resizing events.
5 |
6 | */
7 |
8 | import { Scene, PerspectiveCamera, WebGLRenderer } from "./../lib/three/three.module.js";
9 | import { EffectComposer } from "./../lib/three/postprocessing/EffectComposer.js";
10 | import { RenderPass } from "./../lib/three/postprocessing/RenderPass.js";
11 | import { ShaderPass } from "./../lib/three/postprocessing/ShaderPass.js";
12 | import { SMAAPass } from "./../lib/three/postprocessing/SMAAPass.js";
13 | import { SobelOperatorShader } from "./../lib/three/shaders/SobelOperatorShader.js";
14 |
15 | import { silhouetteMaterial, transparentMaterial } from "./materials.js";
16 |
17 | export default class Screen {
18 | constructor(animate = true) {
19 | this.time = 0;
20 | this.updateListeners = [];
21 | this.element = document.createElement("div");
22 | document.body.appendChild(this.element);
23 | this.resolution = 1;
24 | this.active = true;
25 | this.scene = new Scene();
26 | this.camera = new PerspectiveCamera(90, 1, 0.05, 100000);
27 | this.camera.rotation.order = "YZX";
28 | this.renderer = new WebGLRenderer();
29 | this.renderer.setPixelRatio(window.devicePixelRatio);
30 | this.element.appendChild(this.renderer.domElement);
31 | this.renderer.domElement.id = "renderer";
32 | this.composer = new EffectComposer(this.renderer);
33 | this.renderPass = new RenderPass(this.scene, this.camera);
34 | this.composer.addPass(this.renderPass);
35 | // this.renderPass.renderToScreen = true;
36 |
37 | this.sobelPass = new ShaderPass(SobelOperatorShader);
38 | this.composer.addPass(this.sobelPass);
39 | this.sobelPass.enabled = false;
40 |
41 | this.blueprintPass = new ShaderPass({
42 | uniforms: {
43 | tDiffuse: { type: "t", value: null },
44 | },
45 | vertexShader: `
46 | varying vec2 vUV;
47 | void main() {
48 | vUV = uv;
49 | gl_Position = projectionMatrix * modelViewMatrix * vec4( position, 1.0 );
50 | }
51 | `,
52 | fragmentShader: `
53 | precision mediump float;
54 |
55 | #define prussianBlue vec3(0.1, 0.15, 0.7)
56 |
57 | uniform sampler2D tDiffuse;
58 | varying vec2 vUV;
59 |
60 | void main() {
61 | float edge = texture2D(tDiffuse, vUV).r * 20.;
62 | if (edge < 0.4) {
63 | edge = 0.;
64 | }
65 | vec3 color = mix(prussianBlue, vec3(0.9, 0.9, 1.0), edge);
66 | gl_FragColor = vec4(color, 1.);
67 | }
68 | `,
69 | });
70 | this.composer.addPass(this.blueprintPass);
71 | this.blueprintPass.enabled = false;
72 |
73 | this.colorCyclePass = new ShaderPass({
74 | uniforms: {
75 | tDiffuse: { type: "t", value: null },
76 | time: { type: "f", value: 0 },
77 | },
78 | vertexShader: `
79 | varying vec2 vUV;
80 | void main() {
81 | vUV = uv;
82 | gl_Position = projectionMatrix * modelViewMatrix * vec4( position, 1.0 );
83 | }
84 | `,
85 | fragmentShader: `
86 | precision mediump float;
87 | uniform sampler2D tDiffuse;
88 | uniform float time;
89 | varying vec2 vUV;
90 |
91 | vec3 hueShift( const in vec3 color, const in float amount ) {
92 | vec3 p = vec3(0.55735) * dot(vec3(0.55735), color);
93 | vec3 u = color - p;
94 | vec3 v = cross(vec3(0.55735), u);
95 | return u * cos(amount * 6.2832) + v * sin(amount * 6.2832) + p;
96 | }
97 |
98 | void main() {
99 | gl_FragColor = vec4(hueShift(texture2D(tDiffuse, vUV).rgb, time), 1.0);
100 | }
101 | `,
102 | });
103 | this.composer.addPass(this.colorCyclePass);
104 | this.colorCyclePass.enabled = false;
105 |
106 | this.aaPass = new SMAAPass(1, 1);
107 | this.aaPass.renderToScreen = true;
108 | this.composer.addPass(this.aaPass);
109 | window.addEventListener("resize", this.onWindowResize.bind(this), false);
110 | this.onWindowResize();
111 | if (animate) {
112 | this.update();
113 | this.render();
114 | }
115 |
116 | this.frameRate = 1;
117 | this.startFrameTime = Date.now();
118 | this.lastFrameTime = this.startFrameTime;
119 | }
120 |
121 | onWindowResize() {
122 | const aspect = window.innerWidth / window.innerHeight;
123 | this.camera.aspect = aspect;
124 | this.camera.updateProjectionMatrix();
125 | this.setResolution(this.resolution);
126 | }
127 |
128 | setResolution(amount) {
129 | this.resolution = amount;
130 | const width = Math.ceil(window.innerWidth * this.resolution);
131 | const height = Math.ceil(window.innerHeight * this.resolution);
132 | this.renderer.setSize(width, height);
133 | this.renderer.domElement.style.width = "100%";
134 | this.renderer.domElement.style.height = "100%";
135 |
136 | this.sobelPass.uniforms.resolution.value.x = width;
137 | this.sobelPass.uniforms.resolution.value.y = height;
138 |
139 | this.composer.setSize(width, height);
140 | silhouetteMaterial.uniforms.resolution.value.set(width, height);
141 | }
142 |
143 | setWireframe(enabled) {
144 | this.sobelPass.enabled = enabled;
145 | this.blueprintPass.enabled = enabled;
146 | }
147 |
148 | setCycleColors(enabled) {
149 | this.colorCyclePass.enabled = enabled;
150 | }
151 |
152 | update() {
153 | if (this.active) {
154 | for (const listener of this.updateListeners) {
155 | listener();
156 | }
157 | }
158 |
159 | setTimeout(this.update.bind(this), 1000 / 60);
160 | }
161 |
162 | render() {
163 | requestAnimationFrame(this.render.bind(this));
164 | if (!this.active) return;
165 |
166 | this.time += 0.05;
167 | silhouetteMaterial.uniforms.scramble.value = this.time;
168 | transparentMaterial.uniforms.scramble.value = this.time;
169 | this.colorCyclePass.uniforms.time.value = this.time * 0.02;
170 |
171 | if (this.camera != null) {
172 | this.renderPass.camera = this.camera;
173 | this.composer.render();
174 | // this.renderer.render( this.scene, this.camera );
175 | }
176 |
177 | const frameTime = Date.now() - this.startFrameTime;
178 | const frameDuration = frameTime - this.lastFrameTime;
179 | this.frameRate = 1000 / frameDuration;
180 | this.lastFrameTime = frameTime;
181 | }
182 |
183 | addUpdateListener(func) {
184 | if (!this.updateListeners.includes(func)) {
185 | this.updateListeners.push(func);
186 | }
187 | }
188 |
189 | get width() {
190 | return window.innerWidth;
191 | }
192 |
193 | get height() {
194 | return window.innerHeight;
195 | }
196 |
197 | get backgroundColor() {
198 | return this.scene.background;
199 | }
200 |
201 | set backgroundColor(color) {
202 | this.scene.background = color;
203 | return color;
204 | }
205 | }
206 |
--------------------------------------------------------------------------------
/js/Dashboard.js:
--------------------------------------------------------------------------------
1 | import { Group, Vector2, ShapePath, Path } from "./../lib/three/three.module.js";
2 |
3 | import { lerp, PI, TWO_PI, unitVector, origin } from "./math.js";
4 | import { makeSplinePath, getOffsetPoints, makeCirclePath, makePolygonPath } from "./paths.js";
5 | import { makeGeometry, makeMesh } from "./geometry.js";
6 |
7 | const wheelScale = 5;
8 | const gaugeScale = 5;
9 | const gaugeMinAngle = PI * (1 + 0.8);
10 | const gaugeMaxAngle = PI * (1 - 0.8);
11 |
12 | const expandShapePath = (source, offset) => {
13 | const expansion = new ShapePath();
14 | source.subPaths.forEach((subPath) => expansion.subPaths.push(new Path(getOffsetPoints(subPath, offset))));
15 | return expansion;
16 | };
17 |
18 | const makeDashboardElement = (path, strokeWidth, hasFill) => {
19 | const element = new Group();
20 | const hasStroke = strokeWidth != 0;
21 |
22 | if (hasStroke) {
23 | const stroke = makeMesh(makeGeometry(expandShapePath(path, 1 + strokeWidth), 0, 0.2));
24 | stroke.position.z = -0.1;
25 | element.add(stroke);
26 | }
27 |
28 | if (hasFill) {
29 | const fill = hasStroke ? makeMesh(makeGeometry(expandShapePath(path, 1), 0, 0)) : makeMesh(makeGeometry(path, 0, 0.2));
30 | fill.position.z = 0;
31 | element.add(fill);
32 | }
33 |
34 | return element;
35 | };
36 |
37 | const makeBacking = () => {
38 | const pts = [new Vector2(-200, -40), new Vector2(-200, 40), new Vector2(200, 40), new Vector2(200, -40)];
39 | const path = makeSplinePath(pts, true);
40 | const shapePath = new ShapePath();
41 | shapePath.subPaths.push(path);
42 | return shapePath;
43 | };
44 |
45 | const makeGauge = () => {
46 | const shapePath = new ShapePath();
47 | const outerRadius = 20 * gaugeScale;
48 | const innerRadius = outerRadius - gaugeScale;
49 | const dashEnd = innerRadius - 2 * gaugeScale;
50 |
51 | const nudge = PI * 0.0075;
52 | shapePath.subPaths = [
53 | makeCirclePath(0, 0, outerRadius),
54 | makeCirclePath(0, 0, innerRadius, false),
55 | ...Array(10)
56 | .fill()
57 | .map((_, index) => (TWO_PI * (index + 0.5)) / 10)
58 | .map((angle) =>
59 | makePolygonPath([
60 | new Vector2(Math.cos(angle - nudge) * outerRadius, Math.sin(angle - nudge) * outerRadius),
61 | new Vector2(Math.cos(angle - nudge) * dashEnd, Math.sin(angle - nudge) * dashEnd),
62 | new Vector2(Math.cos(angle + nudge) * dashEnd, Math.sin(angle + nudge) * dashEnd),
63 | new Vector2(Math.cos(angle + nudge) * outerRadius, Math.sin(angle + nudge) * outerRadius),
64 | ])
65 | ),
66 | ];
67 |
68 | return shapePath;
69 | };
70 |
71 | const makeNeedle = () => {
72 | const shapePath = new ShapePath();
73 | const scale = 40;
74 | shapePath.subPaths.push(
75 | makePolygonPath([
76 | new Vector2(-0.02 * scale, 0.1 * scale),
77 | new Vector2(-0.005 * scale, -0.4 * scale),
78 | new Vector2(0.005 * scale, -0.4 * scale),
79 | new Vector2(0.02 * scale, 0.1 * scale),
80 | ])
81 | );
82 | return shapePath;
83 | };
84 |
85 | const makeSteeringWheel = () => {
86 | const scale = 148 * wheelScale;
87 | const shapePath = new ShapePath();
88 |
89 | shapePath.subPaths.push(makeCirclePath(0, 0, scale * 0.5));
90 |
91 | const numBumps = 60;
92 | const topGap = 3;
93 | const bottomGap = 1;
94 | const numTopBumps = numBumps / 2 - 2 * topGap + 1;
95 | const numBottomBumps = numBumps / 2 - 2 * bottomGap + 1;
96 |
97 | const upperVertices = Array(numTopBumps)
98 | .fill()
99 | .map((_, index) =>
100 | unitVector
101 | .clone()
102 | .rotateAround(origin, (TWO_PI * (index + topGap)) / numBumps + PI)
103 | .multiplyScalar((index % 2 === 1 ? 0.42 : 0.435) * scale)
104 | );
105 | shapePath.subPaths.push(makeSplinePath(upperVertices, true));
106 |
107 | const lowerVertices = Array(numBottomBumps)
108 | .fill()
109 | .map((_, index) =>
110 | unitVector
111 | .clone()
112 | .rotateAround(origin, (TWO_PI * (index + bottomGap)) / numBumps)
113 | .multiplyScalar((index % 2 === 1 ? 0.42 : 0.435) * scale)
114 | )
115 | .concat([
116 | new Vector2(scale * -0.25, scale * 0.085),
117 | new Vector2(scale * -0.125, scale * 0.235),
118 | new Vector2(scale * 0.125, scale * 0.235),
119 | new Vector2(scale * 0.25, scale * 0.085),
120 | ]);
121 |
122 | shapePath.subPaths.push(makeSplinePath(lowerVertices, true));
123 |
124 | return shapePath;
125 | };
126 |
127 | export default class Dashboard {
128 | constructor() {
129 | this.speed = 1;
130 | this.tach = 1;
131 | this.wheelRotation = PI;
132 | this.drivingSide = -1;
133 | this.object = new Group();
134 |
135 | this.backing = makeDashboardElement(makeBacking(), 2, true);
136 | this.backing.position.set(-50, -80, -110);
137 |
138 | this.speedGauge = makeDashboardElement(makeGauge(), 0, true);
139 | this.speedGauge.position.set(-25, -35, -105);
140 | this.speedGauge.scale.set(1 / gaugeScale, 1 / gaugeScale, 1);
141 |
142 | this.speedNeedle = makeDashboardElement(makeNeedle(), 0, true);
143 | this.speedNeedle.position.set(-25, -35, -105);
144 | this.speedNeedle.rotation.z = PI * 1.5;
145 |
146 | this.tachGauge = makeDashboardElement(makeGauge(), 0, true);
147 | this.tachGauge.position.set(-70, -35, -105);
148 | this.tachGauge.scale.set(1 / gaugeScale, 1 / gaugeScale, 1);
149 |
150 | this.tachNeedle = makeDashboardElement(makeNeedle(), 0, true);
151 | this.tachNeedle.position.set(-70, -35, -105);
152 | this.tachNeedle.rotation.z = PI * 1.5;
153 |
154 | this.wheel = makeDashboardElement(makeSteeringWheel(), 2 * wheelScale, true);
155 | this.wheel.position.set(-50, -55, -100);
156 | this.wheel.scale.set(1 / wheelScale, 1 / wheelScale, 1);
157 |
158 | [this.backing, this.speedGauge, this.speedNeedle, this.tachGauge, this.tachNeedle, this.wheel].forEach((element) => this.object.add(element));
159 |
160 | this.update();
161 | }
162 |
163 | update() {
164 | const lerpAmount = 0.05;
165 |
166 | this.backing.position.x = lerp(this.backing.position.x, 50 * this.drivingSide, lerpAmount);
167 |
168 | this.wheel.position.x = lerp(this.wheel.position.x, 50 * this.drivingSide, lerpAmount);
169 | this.wheel.rotation.z = this.wheelRotation;
170 |
171 | const gaugePositions = [25 * this.drivingSide, 70 * this.drivingSide];
172 |
173 | this.speedGauge.position.x = lerp(this.speedGauge.position.x, Math.max(...gaugePositions), lerpAmount);
174 | this.speedNeedle.position.x = this.speedGauge.position.x;
175 | this.speedNeedle.rotation.z = lerp(this.speedNeedle.rotation.z, lerp(gaugeMinAngle, gaugeMaxAngle, Math.min(this.speed, 1)), 0.05);
176 |
177 | this.tachGauge.position.x = lerp(this.tachGauge.position.x, Math.min(...gaugePositions), lerpAmount);
178 | this.tachNeedle.position.x = this.tachGauge.position.x;
179 | this.tachNeedle.rotation.z = lerp(this.tachNeedle.rotation.z, lerp(gaugeMinAngle, gaugeMaxAngle, Math.min(this.tach, 1)), 0.05);
180 | }
181 | }
182 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | [](https://rezmason.github.io/drivey)
2 |
3 | # [Drivey.js](https://rezmason.github.io/drivey)
4 |
5 | This is a JavaScript port of the **2007 graphics demo** [Drivey](http://web.archive.org/web/20211022163339/https://drivey.com/).
6 |
7 | ### Play Drivey.js online [here](https://rezmason.github.io/drivey/).
8 | ### On older browsers, try the [legacy version](https://rezmason.github.io/drivey/legacy).
9 |
10 | ### Purpose
11 |
12 | Driving down the open road elicits nostalgia in some people. A couple of them made old arcade games about driving.
13 | Old driving games triggered nostalgia in [Mark Pursey](https://github.com/MarkPursey), so he made a graphics demo about driving games.
14 | I feel nostalgia for his driving demo, so here we are. I've written a whole lot more on the subject in [_MY OTHER CAR IS A BÉZIER_](./MY_OTHER_CAR_IS_A_BEZIER.md).
15 |
16 | ### Controls
17 | #### Touch
18 | You can use any combination of fingers (or the mouse):
19 | - `Up-Down`, adjust the driving speed.
20 | - `Left-right`, turn the steering wheel.
21 | #### Keyboard
22 | - `Up Arrow`, gas.
23 | - `Down Arrow`, brake.
24 | - `Space Bar`, handbrake.
25 | - `Left Arrow`, steer left.
26 | - `Right Arrow`, steer right.
27 | - `Shift and Control keys`, slow down and speed up the demo.
28 | #### 1 Switch
29 | Constantly drives with a slight turn. Click to switch between left turns and right turns.
30 | #### Eye Gaze
31 | Constantly drives straight. The car turns left and right if the mouse is on the far left or far right of the window.
32 | ### Original features
33 |
34 | - [x] Stylized 3D Rendering
35 | - [x] Four levels: Deep Dark Night, Tunnel, City and Industrial Zone
36 | - [x] Simulated self-driving car with optional manual control
37 | - [x] NPC cars
38 | - [x] Rear view
39 | - [x] Optional simulated dashboard
40 | - [x] Support for driving on the left and on the right
41 | - [x] Wireframe mode
42 | - [x] Color cycling mode
43 | - [ ] Collision detection
44 | - [ ] Engine revving audio support
45 | - [ ] ~Steering wheel peripheral support~
46 |
47 | ### New features
48 | - [x] Runs right in the browser, on any computer
49 | - [x] Three new levels: Cliffside Beach, Warp Gate and Spectre (inspired by [Craig Fryar](https://www.youtube.com/watch?v=b0X74Oe80tg))
50 | - [x] Two "refurbished" levels: Nullarbor and Marshland
51 | - [x] Drivey.js mixtape on the car stereo
52 | - [x] Procedural automobile generator
53 | - [x] Tablet-friendly UI
54 | - [x] Three camera angles
55 | - [x] One switch support
56 | - [x] Eye gaze support
57 | - [x] Touch screen steering control scheme
58 | - [x] [Merveilles Theme](https://github.com/hundredrabbits/Themes) drag-and-drop support
59 | - [x] HTML levels, with drag-and-drop support
60 | - [ ] Optional ambient audio
61 |
62 | ### History
63 |
64 | Back in 2000, JavaScript was a slow, underpowered, interpreted scripting language meant for adding simple behaviors to web pages. Still, it showed enormous potential, and lots of money went into various efforts to make more expressive variations of it. Mark Pursey, Drivey's author, invested his free time and creative energy writing his own variant, which he called [JujuScript](https://web.archive.org/web/20110807170635/http://jujusoft.com/software/jujuscript/index.html).
65 |
66 | JujuScript is very similar to JavaScript, but adds strong type support and operator overloading. Furthermore, Pursey embedded a graphics API in JujuScript's interpreter that specializes in composing and rendering text and font-like graphics. When he decided in 2004 (I believe) to make a driving simulator, naturally it had a high legibility and visual fidelity. And he had the foresight to [share the code](https://web.archive.org/web/20160313145032/http://www.jujusoft.com/download/jujuscript-1.0.zip) for free.
67 |
68 | Unfortunately JujuScript's launcher is a Windows-only executable (which [runs very well](https://appdb.winehq.org/objectManager.php?sClass=application&iId=8828) under wine), and isn't open source. Its "2.5D" graphics API is also undocumented. To expand its reach, break its dependency on 2000s-era Windows APIs and boost its [bus factor](https://en.wikipedia.org/wiki/Bus_factor), it made sense to convert the Drivey demo to a more broadly adopted platform (ie. *the web*).
69 |
70 | In the intervening decades, JavaScript has matured into a robust, expressive language, bolstered by a thriving ecosystem. It did take a while to get there, but its momentum has also increased at the same time. The advent of [WebGL](https://developer.mozilla.org/en-US/docs/Web/API/WebGL_API) and [three.js](https://threejs.org/) marked the introduction of a common hardware-accelerated graphics pipeline to the world's most widespread platform.
71 |
72 | As of September 2023, **Drivey.js** is just shy of being feature complete (see above); already, though, it ties the long-term fate of the demo to the long-term fate of the web. [The old Windows demo is still online](http://web.archive.org/web/20211022163339/https://drivey.com/), in the meantime, and is still notable for its unique approach to rendering realtime 3D graphics.
73 |
74 | ## Techniques
75 |
76 | ### Level generation
77 | You may wonder what process generates these silhouetted landscapes. Set the camera angle to "Satellite" and you'll see the road's shape determines the placement of almost everything:
78 |
79 | 
80 |
81 | In fact, most of the geometry in both Drivey and Drivey.js is just an extrusion of a single curve (a [closed spline](https://threejs.org/docs/#api/en/extras/curves/CatmullRomCurve3)), which runs down the middle of the road. In other words, the demo marches steadily along this curve, regularly dropping points along the side, sometimes suspending them in the air, and afterwards it connects them into shapes. Every solid or dashed line, every wire and pole, is generated in this way, and the level generates them anew each time you visit. There are very few exceptions, such as the clouds and buildings in the City level.
82 | ### Car generation
83 | A different process governs the shape of every car. A handful of numbers and decisions are randomly picked— the length of the cabin, for instance, or whether the car is a convertible- and these values are used to create a basic side view diagram of the car:
84 | ```
85 | T---U------V----------W
86 | P----Q------R-----------S
87 | / | | \
88 | / | | \
89 | K_______-------L-------M------N--------------O
90 | | | | | |
91 | F--------------G-------H------I--------------J
92 | \ __ | | | __ / ====33
93 | A-----/ \---B-------C------D-----/ \---E
94 | FA RA
95 | ```
96 | Once the points in this diagram are computed, the areas between them are filled with extruded boxes; some are thin, some are thick, some are opaque, some are transparent, some are light and some are dark. Combined together, they form the shape of a car.
97 |
--------------------------------------------------------------------------------
/js/modelLevel.js:
--------------------------------------------------------------------------------
1 | import { Vector2, Vector3, Matrix4, ShapePath } from "./../lib/three/three.module.js";
2 | import { getOffsetPoints, makeCirclePath, makeSquarePath } from "./paths.js";
3 | import { distance, origin, sanitize, TWO_PI } from "./math.js";
4 | import { makeGeometry, mergeGeometries } from "./geometry.js";
5 | import { lineModelersByType, partModelersByType } from "./lineModelers.js";
6 | import { makeRoad } from "./roads.js";
7 |
8 | const getChildrenOfTypes = (node, types) => node.children.filter((child) => types.includes(child.type));
9 |
10 | const getRoad = (attributes, roadsById) => {
11 | if (attributes == null) return null;
12 | const { id } = attributes;
13 | if (roadsById[id] == null) {
14 | const basis = getRoad(attributes.basis, roadsById);
15 | roadsById[id] = makeRoad(attributes, basis);
16 | }
17 | return roadsById[id];
18 | };
19 |
20 | const modelLine = ({ attributes, type }, roadsById) => {
21 | const modeler = lineModelersByType[type];
22 | const road = getRoad(attributes.road, roadsById);
23 | const roadLength = road.length;
24 | const curve = road.curve;
25 | const segmentAttributes = {
26 | start: sanitize(attributes.start / roadLength, 0),
27 | end: sanitize(attributes.end / roadLength, 1),
28 | spacing: attributes.spacing / roadLength,
29 | length: attributes.length / roadLength,
30 | };
31 | const paths = modeler({ ...attributes, curve, ...segmentAttributes });
32 | if (attributes.mirror) {
33 | return paths.concat(modeler({ ...attributes, curve, ...segmentAttributes, x: -attributes.x }));
34 | }
35 | return paths;
36 | };
37 |
38 | const pathsToModel = (paths, { y = 0, height = 0, shade = 0.5, alpha = 1, fade = 0, scaleX = 1, scaleY = 1, scaleZ = 1 }) => {
39 | const shape = new ShapePath();
40 | shape.subPaths = paths;
41 | const transparent = alpha < 1;
42 | return {
43 | transparent,
44 | geometry: makeGeometry(shape, height, shade, alpha, fade),
45 | position: new Vector3(0, 0, y),
46 | scale: new Vector3(scaleX, scaleY, scaleZ),
47 | };
48 | };
49 |
50 | const modelShape = (node, roadsById) => [
51 | pathsToModel(
52 | getChildrenOfTypes(node, ["solid", "dashed", "dotted"])
53 | .map((line) => modelLine(line, roadsById))
54 | .flat(),
55 | {
56 | ...node.attributes,
57 | scaleX: node.attributes.scale.x,
58 | scaleY: node.attributes.scale.y,
59 | }
60 | ),
61 | ];
62 |
63 | const modelPart = ({ attributes, type }, featureAttributes) => {
64 | const modeler = partModelersByType[type];
65 | const lineAttributes = {
66 | ...attributes,
67 | ...featureAttributes,
68 | curve: featureAttributes.road.curve,
69 | start: featureAttributes.start + attributes.z,
70 | end: featureAttributes.end + attributes.z,
71 | length: attributes.length / featureAttributes.road.length,
72 | };
73 | const model = pathsToModel(modeler(lineAttributes), lineAttributes);
74 | if (attributes.mirror) {
75 | return [model, pathsToModel(modeler({ ...lineAttributes, x: -attributes.x }), lineAttributes)];
76 | }
77 | return [model];
78 | };
79 |
80 | const modelFeature = (node, roadsById) => {
81 | const road = getRoad(node.attributes.road, roadsById);
82 | const roadLength = road.length;
83 | const attributes = {
84 | ...node.attributes,
85 | road,
86 | start: sanitize(node.attributes.start / roadLength, 0),
87 | end: sanitize(node.attributes.end / roadLength, 1),
88 | spacing: node.attributes.spacing / roadLength,
89 | };
90 | return getChildrenOfTypes(node, ["box", "disk", "wire"])
91 | .map((part) => modelPart(part, attributes))
92 | .flat();
93 | };
94 |
95 | const modelCityscape = ({ attributes }, roadsById) => {
96 | const { rowSpacing, columnSpacing, heights, proximity, width, radius } = attributes;
97 | const road = getRoad(attributes.road, roadsById);
98 | if (rowSpacing <= 0) rowSpacing = 100;
99 | if (columnSpacing <= 0) columnSpacing = 100;
100 | const paths = Array(heights.length)
101 | .fill()
102 | .map((_) => []);
103 |
104 | const approximation = road.approximate();
105 | for (let x = -radius; x < radius; x += rowSpacing) {
106 | for (let y = -radius; y < radius; y += columnSpacing) {
107 | const pos = new Vector2(x, y);
108 | if (pos.length() < radius && distance(approximation.getNearestPoint(pos), pos) > proximity) {
109 | paths[Math.floor(Math.random() * heights.length)].push(makeSquarePath(pos.x, pos.y, width));
110 | }
111 | }
112 | }
113 | return heights.filter((height) => height > 0).map((height, index) => pathsToModel(paths[index], { ...attributes, height }));
114 | };
115 |
116 | const modelClouds = ({ attributes }) => {
117 | const { count, shade, scale, altitude, cloudRadius } = attributes;
118 | const paths = Array(count)
119 | .fill()
120 | .map((_) => new Vector2(Math.random() * 0.6 + 0.3).rotateAround(origin, Math.random() * TWO_PI).multiply(scale))
121 | .map(({ x, y }) => makeCirclePath(x, y, cloudRadius));
122 | return pathsToModel(paths, {
123 | y: altitude,
124 | height: 1,
125 | shade,
126 | });
127 | };
128 |
129 | const matrixElements = Array(16).fill(0);
130 | const transformGeometry = ({ geometry, scale, position }) => {
131 | matrixElements[0] = scale.x;
132 | matrixElements[5] = scale.y;
133 | matrixElements[10] = scale.z;
134 | matrixElements[11] = position.z;
135 | matrixElements[12] = position.x;
136 | matrixElements[13] = position.y;
137 | matrixElements[15] = 1;
138 | geometry.applyMatrix4(new Matrix4().set(...matrixElements));
139 | return geometry;
140 | };
141 |
142 | const dehydrate = (geometry) =>
143 | Object.fromEntries(
144 | ["bulgeDirection", "idColor", "monochromeValue", "position"].map((name) => {
145 | const { array, itemSize } = geometry.attributes[name] ?? {
146 | array: null,
147 | itemSize: 0,
148 | };
149 | return [name, { array, itemSize }];
150 | })
151 | );
152 |
153 | export default (node) => {
154 | const { uid } = node;
155 | const roadsById = {};
156 | getRoad(node.attributes, roadsById);
157 | const allModels = [
158 | ...getChildrenOfTypes(node, ["feature"])
159 | .map((feature) => modelFeature(feature, roadsById))
160 | .flat(),
161 | ...getChildrenOfTypes(node, ["shape"]).map((shape) => modelShape(shape, roadsById)),
162 | ...getChildrenOfTypes(node, ["cityscape"]).map((cityscape) => modelCityscape(cityscape, roadsById)),
163 | ].flat();
164 | const opaqueGeometry = dehydrate(mergeGeometries(allModels.filter((model) => !model.transparent).map(transformGeometry)));
165 | const transparentGeometry = dehydrate(mergeGeometries(allModels.filter((model) => model.transparent).map(transformGeometry)));
166 | const skyGeometry = dehydrate(mergeGeometries(getChildrenOfTypes(node, ["clouds"]).map(modelClouds).map(transformGeometry)));
167 | return {
168 | uid,
169 | opaqueGeometry,
170 | transparentGeometry,
171 | skyGeometry,
172 | roadsById: Object.fromEntries(Object.entries(roadsById).map(([key, { points }]) => [key, points])),
173 | };
174 | };
175 |
--------------------------------------------------------------------------------
/legacy/lib/EffectComposer.js:
--------------------------------------------------------------------------------
1 | /**
2 | * @author alteredq / http://alteredqualia.com/
3 | */
4 |
5 | THREE.EffectComposer = function ( renderer, renderTarget ) {
6 |
7 | this.renderer = renderer;
8 |
9 | if ( renderTarget === undefined ) {
10 |
11 | var parameters = {
12 | minFilter: THREE.LinearFilter,
13 | magFilter: THREE.LinearFilter,
14 | format: THREE.RGBAFormat,
15 | stencilBuffer: false
16 | };
17 |
18 | var size = renderer.getSize( new THREE.Vector2() );
19 | this._pixelRatio = renderer.getPixelRatio();
20 | this._width = size.width;
21 | this._height = size.height;
22 |
23 | renderTarget = new THREE.WebGLRenderTarget( this._width * this._pixelRatio, this._height * this._pixelRatio, parameters );
24 | renderTarget.texture.name = 'EffectComposer.rt1';
25 |
26 | } else {
27 |
28 | this._pixelRatio = 1;
29 | this._width = renderTarget.width;
30 | this._height = renderTarget.height;
31 |
32 | }
33 |
34 | this.renderTarget1 = renderTarget;
35 | this.renderTarget2 = renderTarget.clone();
36 | this.renderTarget2.texture.name = 'EffectComposer.rt2';
37 |
38 | this.writeBuffer = this.renderTarget1;
39 | this.readBuffer = this.renderTarget2;
40 |
41 | this.renderToScreen = true;
42 |
43 | this.passes = [];
44 |
45 | // dependencies
46 |
47 | if ( THREE.CopyShader === undefined ) {
48 |
49 | console.error( 'THREE.EffectComposer relies on THREE.CopyShader' );
50 |
51 | }
52 |
53 | if ( THREE.ShaderPass === undefined ) {
54 |
55 | console.error( 'THREE.EffectComposer relies on THREE.ShaderPass' );
56 |
57 | }
58 |
59 | this.copyPass = new THREE.ShaderPass( THREE.CopyShader );
60 |
61 | this.clock = new THREE.Clock();
62 |
63 | };
64 |
65 | Object.assign( THREE.EffectComposer.prototype, {
66 |
67 | swapBuffers: function () {
68 |
69 | var tmp = this.readBuffer;
70 | this.readBuffer = this.writeBuffer;
71 | this.writeBuffer = tmp;
72 |
73 | },
74 |
75 | addPass: function ( pass ) {
76 |
77 | this.passes.push( pass );
78 | pass.setSize( this._width * this._pixelRatio, this._height * this._pixelRatio );
79 |
80 | },
81 |
82 | insertPass: function ( pass, index ) {
83 |
84 | this.passes.splice( index, 0, pass );
85 |
86 | },
87 |
88 | isLastEnabledPass: function ( passIndex ) {
89 |
90 | for ( var i = passIndex + 1; i < this.passes.length; i ++ ) {
91 |
92 | if ( this.passes[ i ].enabled ) {
93 |
94 | return false;
95 |
96 | }
97 |
98 | }
99 |
100 | return true;
101 |
102 | },
103 |
104 | render: function ( deltaTime ) {
105 |
106 | // deltaTime value is in seconds
107 |
108 | if ( deltaTime === undefined ) {
109 |
110 | deltaTime = this.clock.getDelta();
111 |
112 | }
113 |
114 | var currentRenderTarget = this.renderer.getRenderTarget();
115 |
116 | var maskActive = false;
117 |
118 | var pass, i, il = this.passes.length;
119 |
120 | for ( i = 0; i < il; i ++ ) {
121 |
122 | pass = this.passes[ i ];
123 |
124 | if ( pass.enabled === false ) continue;
125 |
126 | pass.renderToScreen = ( this.renderToScreen && this.isLastEnabledPass( i ) );
127 | pass.render( this.renderer, this.writeBuffer, this.readBuffer, deltaTime, maskActive );
128 |
129 | if ( pass.needsSwap ) {
130 |
131 | if ( maskActive ) {
132 |
133 | var context = this.renderer.getContext();
134 | var stencil = this.renderer.state.buffers.stencil;
135 |
136 | //context.stencilFunc( context.NOTEQUAL, 1, 0xffffffff );
137 | stencil.setFunc( context.NOTEQUAL, 1, 0xffffffff );
138 |
139 | this.copyPass.render( this.renderer, this.writeBuffer, this.readBuffer, deltaTime );
140 |
141 | //context.stencilFunc( context.EQUAL, 1, 0xffffffff );
142 | stencil.setFunc( context.EQUAL, 1, 0xffffffff );
143 |
144 | }
145 |
146 | this.swapBuffers();
147 |
148 | }
149 |
150 | if ( THREE.MaskPass !== undefined ) {
151 |
152 | if ( pass instanceof THREE.MaskPass ) {
153 |
154 | maskActive = true;
155 |
156 | } else if ( pass instanceof THREE.ClearMaskPass ) {
157 |
158 | maskActive = false;
159 |
160 | }
161 |
162 | }
163 |
164 | }
165 |
166 | this.renderer.setRenderTarget( currentRenderTarget );
167 |
168 | },
169 |
170 | reset: function ( renderTarget ) {
171 |
172 | if ( renderTarget === undefined ) {
173 |
174 | var size = this.renderer.getSize( new THREE.Vector2() );
175 | this._pixelRatio = this.renderer.getPixelRatio();
176 | this._width = size.width;
177 | this._height = size.height;
178 |
179 | renderTarget = this.renderTarget1.clone();
180 | renderTarget.setSize( this._width * this._pixelRatio, this._height * this._pixelRatio );
181 |
182 | }
183 |
184 | this.renderTarget1.dispose();
185 | this.renderTarget2.dispose();
186 | this.renderTarget1 = renderTarget;
187 | this.renderTarget2 = renderTarget.clone();
188 |
189 | this.writeBuffer = this.renderTarget1;
190 | this.readBuffer = this.renderTarget2;
191 |
192 | },
193 |
194 | setSize: function ( width, height ) {
195 |
196 | this._width = width;
197 | this._height = height;
198 |
199 | var effectiveWidth = this._width * this._pixelRatio;
200 | var effectiveHeight = this._height * this._pixelRatio;
201 |
202 | this.renderTarget1.setSize( effectiveWidth, effectiveHeight );
203 | this.renderTarget2.setSize( effectiveWidth, effectiveHeight );
204 |
205 | for ( var i = 0; i < this.passes.length; i ++ ) {
206 |
207 | this.passes[ i ].setSize( effectiveWidth, effectiveHeight );
208 |
209 | }
210 |
211 | },
212 |
213 | setPixelRatio: function ( pixelRatio ) {
214 |
215 | this._pixelRatio = pixelRatio;
216 |
217 | this.setSize( this._width, this._height );
218 |
219 | }
220 |
221 | } );
222 |
223 |
224 | THREE.Pass = function () {
225 |
226 | // if set to true, the pass is processed by the composer
227 | this.enabled = true;
228 |
229 | // if set to true, the pass indicates to swap read and write buffer after rendering
230 | this.needsSwap = true;
231 |
232 | // if set to true, the pass clears its buffer before rendering
233 | this.clear = false;
234 |
235 | // if set to true, the result of the pass is rendered to screen. This is set automatically by EffectComposer.
236 | this.renderToScreen = false;
237 |
238 | };
239 |
240 | Object.assign( THREE.Pass.prototype, {
241 |
242 | setSize: function ( /* width, height */ ) {},
243 |
244 | render: function ( /* renderer, writeBuffer, readBuffer, deltaTime, maskActive */ ) {
245 |
246 | console.error( 'THREE.Pass: .render() must be implemented in derived pass.' );
247 |
248 | }
249 |
250 | } );
251 |
252 | // Helper for passes that need to fill the viewport with a single quad.
253 | THREE.Pass.FullScreenQuad = ( function () {
254 |
255 | var camera = new THREE.OrthographicCamera( - 1, 1, 1, - 1, 0, 1 );
256 | var geometry = new THREE.PlaneBufferGeometry( 2, 2 );
257 |
258 | var FullScreenQuad = function ( material ) {
259 |
260 | this._mesh = new THREE.Mesh( geometry, material );
261 |
262 | };
263 |
264 | Object.defineProperty( FullScreenQuad.prototype, 'material', {
265 |
266 | get: function () {
267 |
268 | return this._mesh.material;
269 |
270 | },
271 |
272 | set: function ( value ) {
273 |
274 | this._mesh.material = value;
275 |
276 | }
277 |
278 | } );
279 |
280 | Object.assign( FullScreenQuad.prototype, {
281 |
282 | render: function ( renderer ) {
283 |
284 | renderer.render( this._mesh, camera );
285 |
286 | }
287 |
288 | } );
289 |
290 | return FullScreenQuad;
291 |
292 | } )();
293 |
--------------------------------------------------------------------------------
/js/Buttons.js:
--------------------------------------------------------------------------------
1 | import isTouchDevice from "./isTouchDevice.js";
2 |
3 | export default class Buttons {
4 | constructor() {
5 | this.listeners = [];
6 | this.buttonsContainer = document.createElement("div");
7 | this.buttonsContainer.id = "buttonsContainer";
8 | this.element = document.createElement("div");
9 | this.element.id = "buttons";
10 | this.isMouseOverEmbeddedPlaylist = false;
11 | this.isMouseOver = false;
12 | document.body.appendChild(this.buttonsContainer);
13 | this.buttonsContainer.appendChild(this.element);
14 |
15 | this.addButton("cruise", 2, [0, 1, 2, 3], (value) => {
16 | const index = parseInt(value);
17 | return `autopilot
18 | ${Array(3)
19 | .fill()
20 | .map((_, id) => {
21 | return ``;
22 | })
23 | .join(" ")}
`;
24 | });
25 |
26 | this.addButton("npcCars", 2, [0, 1, 2, 3], (value) => {
27 | const index = parseInt(value);
28 | const lights = Array(3)
29 | .fill()
30 | .map((_, id) => ``)
31 | .join(" ");
32 | return `cars
33 | ${lights}
`;
34 | });
35 |
36 | this.addButton(
37 | "drivingSide",
38 | "right",
39 | ["left", "right"],
40 | (value) => `
41 | side
42 |
43 | ${value}
44 |
`
45 | );
46 |
47 | this.addButton(
48 | "camera",
49 | "driver",
50 | ["driver", "hood", "rear", "backseat", "chase", "aerial", "satellite"],
51 | (value) => `
52 | camera
53 |
54 | ${value}
55 |
`
56 | );
57 |
58 | this.addButton(
59 | "effect",
60 | "ombré",
61 | ["ombré", "wireframe", "technicolor", "merveilles"],
62 | (value) => `
63 | effect
64 |
65 | ${value}
66 |
`
67 | );
68 |
69 | this.addButton(
70 | "controls",
71 | isTouchDevice ? "touch" : "arrows",
72 | ["touch", "arrows", "1 switch", "eye gaze"],
73 | (value) => `
74 | controls
75 |
76 | ${value.replace("_", "
")}
77 |
`
78 | );
79 |
80 | this.addButton(
81 | "quality",
82 | "high",
83 | ["high", "medium", "low"],
84 | (value) => `
85 | quality
86 |
87 | ${value}
88 |
`
89 | );
90 |
91 | this.addButton(
92 | "music",
93 | "",
94 | [""],
95 | (value) => `
96 | mixtape
97 |
98 | play
99 |
`
100 | );
101 |
102 | const embeddedPlaylist = document.createElement("div");
103 | embeddedPlaylist.id = "embedded-playlist";
104 | embeddedPlaylist.innerHTML = `
105 |
106 | `;
114 | this.element.appendChild(embeddedPlaylist);
115 | embeddedPlaylist.addEventListener("mouseover", () => (this.isMouseOverEmbeddedPlaylist = true));
116 | embeddedPlaylist.addEventListener("mouseout", () => (this.isMouseOverEmbeddedPlaylist = false));
117 |
118 | this.addButton(
119 | "level",
120 | "industrial",
121 | ["industrial", "night", "city", "tunnel", "beach", "warp", "spectre", "nullarbor", "marshland"],
122 | (value) => `
123 | level select
124 |
125 | ${value}
126 |
`
127 | );
128 |
129 | const stylesheet = Array.from(document.styleSheets).find((sheet) => sheet.title === "main");
130 | this.bodyRule = Array.from(stylesheet.cssRules).find((rule) => rule.selectorText === "body, colors");
131 |
132 | document.addEventListener("mousemove", this.onMouse.bind(this), false);
133 | document.addEventListener("mousedown", this.onMouse.bind(this), false);
134 | document.addEventListener("mouseup", this.onMouse.bind(this), false);
135 | document.addEventListener("touchstart", this.onMouse.bind(this), false);
136 | document.addEventListener("touchmove", this.onMouse.bind(this), false);
137 | document.addEventListener("touchend", this.onMouse.bind(this), false);
138 |
139 | this.element.addEventListener("mouseover", () => (this.isMouseOver = true));
140 | this.element.addEventListener("mouseout", () => (this.isMouseOver = false));
141 | }
142 |
143 | onMouse() {
144 | this.wakeUp();
145 | }
146 |
147 | wakeUp() {
148 | if (this.buttonsContainer.classList.contains("awake")) return;
149 | clearTimeout(this.awakeTimer);
150 | this.buttonsContainer.classList.toggle("awake", true);
151 | this.awakeTimer = setTimeout(() => {
152 | this.buttonsContainer.classList.toggle("awake", false);
153 | if (this.isMouseOver || this.isMouseOverEmbeddedPlaylist) {
154 | this.wakeUp();
155 | }
156 | }, 3000);
157 | }
158 |
159 | addListener(func) {
160 | if (!this.listeners.includes(func)) {
161 | this.listeners.push(func);
162 | }
163 | }
164 |
165 | dispatch(id, value) {
166 | for (const listener of this.listeners) {
167 | listener(id, value);
168 | }
169 | }
170 |
171 | addButton(id, defaultValue, allValues, labelMaker) {
172 | const button = document.createElement("button");
173 | allValues = allValues.map((value) => value.toString());
174 | button.allValues = allValues;
175 | button.value = defaultValue.toString();
176 | button.index = allValues.indexOf(button.value);
177 | button.id = `button_${id}`;
178 | button.name = id;
179 | button.type = "button";
180 | button.innerHTML = labelMaker(button.value);
181 | button.labelMaker = labelMaker;
182 | this.element.appendChild(button);
183 | button.addEventListener("click", () => {
184 | button.index = (button.index + 1) % allValues.length;
185 | button.value = allValues[button.index];
186 | button.innerHTML = labelMaker(button.value);
187 | this.dispatch(id, button.value);
188 | });
189 | }
190 |
191 | setButton(id, value) {
192 | const button = document.getElementById(`button_${id}`);
193 | button.index = button.allValues.indexOf(value);
194 | if (button.index === -1) {
195 | button.index = 0;
196 | }
197 |
198 | button.value = button.allValues[button.index];
199 | button.innerHTML = button.labelMaker(button.value);
200 | this.dispatch(id, button.value);
201 | }
202 |
203 | setColors(backgroundColor, borderColor, lightColor) {
204 | this.bodyRule.style.setProperty("--dashboard-background-color", `#${backgroundColor.getHex().toString(16).padStart(6, "0")}`);
205 | this.bodyRule.style.setProperty("--dashboard-border-color", `#${borderColor.getHex().toString(16).padStart(6, "0")}`);
206 | this.bodyRule.style.setProperty("--dashboard-light-color", `#${lightColor.getHex().toString(16).padStart(6, "0")}`);
207 | }
208 |
209 | setWireframe(enabled) {
210 | this.element.classList.toggle("wireframe", enabled);
211 | }
212 | }
213 |
--------------------------------------------------------------------------------