├── .gitattributes
├── .gitignore
├── README.md
├── flow-field.jpg
├── img
├── beetle.png
├── grid_bg.png
├── target.png
└── tilesprite.png
├── index.html
└── scripts
├── Main.js
├── components
├── BoundingCircle.js
├── VectorFieldState.js
├── behaviors
│ └── Flock.js
└── input
│ ├── KeyboardController.js
│ └── MouseController.js
├── engine
├── CollisionGrid.js
├── ComponentType.js
├── DebugDraw.js
├── FlowGrid.js
├── FlowGridNode.js
├── Kai.js
└── TileMap.js
├── entities
├── Block.js
└── Thing.js
├── lib
├── Signal.js
├── dat.gui.min.js
├── linkedlist.js
├── pixi.js
└── require.js
└── math
├── Point3.js
└── Vec2.js
/.gitattributes:
--------------------------------------------------------------------------------
1 | # Auto detect text files and perform LF normalization
2 | * text=auto
3 |
4 | # Custom for Visual Studio
5 | *.cs diff=csharp
6 | *.sln merge=union
7 | *.csproj merge=union
8 | *.vbproj merge=union
9 | *.fsproj merge=union
10 | *.dbproj merge=union
11 |
12 | # Standard to msysgit
13 | *.doc diff=astextplain
14 | *.DOC diff=astextplain
15 | *.docx diff=astextplain
16 | *.DOCX diff=astextplain
17 | *.dot diff=astextplain
18 | *.DOT diff=astextplain
19 | *.pdf diff=astextplain
20 | *.PDF diff=astextplain
21 | *.rtf diff=astextplain
22 | *.RTF diff=astextplain
23 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Build and Release Folders
2 | bin/
3 | bin-debug/
4 | bin-release/
5 |
6 | # Other files and folders
7 | .settings/
8 |
9 | flowfield.*
10 |
11 | *.psd
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | 
2 |
3 | [Demo.](http://vonwolfehaus.github.io/flow-field/)
4 |
5 | Based on [this TutsPlus tutorial](http://gamedev.tutsplus.com/tutorials/implementation/goal-based-vector-field-pathfinding/).
6 |
7 | ## Overview
8 |
9 | Sometimes called flow fields, vector fields, wavefront expansion, brushfire, and so on. The idea is to use Dijkstra's algorithm to fill out a grid, starting from a single cell, with the distance from current cell to that original cell. On each cell we calculate a vector that points in the direction of the goal. As entities roll over a cell, we simply apply that vector to the entity's velocity.
10 |
11 | This makes pathfinding very large numbers of objects very efficient. However, there are some problems such as local optima. I hoped to solve local optima with flocking--if one entity gets stuck, it will come out of it by following its neighbors out (who did not get stuck).
12 |
13 | It didn't work as well as I hoped. I didn't spend enough time placing proper weights on the flocking rules, but just using collision resolution resulted in smoother pathing. Anyway, that's what experiments are for.
14 |
15 | # Details
16 |
17 | After playing [Planetary Annihilation](http://www.uberent.com/pa/) and witnessing how well [their flow fields](http://youtu.be/5Qyl7h7D1Q8?t=24m30s) worked for pathfinding tons of units at once while maintaining excellent performance, I was interested in trying it myself–unit movement in RTS games is a huge problem field with a lot of solutions. So I found [this article](http://gamedev.tutsplus.com/tutorials/implementation/goal-based-vector-field-pathfinding/) on the subject and proceeded to hack it out in JavaScript.
18 |
19 | ## Anatomy of a flow field
20 |
21 | The idea is to use Dijkstra’s algorithm to fill out a grid, starting from a single cell (the goal), with the distance from current cell to that goal cell. On each cell we calculate a vector (direction) derived from the distance value. As entities roll over a cell, we simply apply that vector to the entity’s velocity. That’s all there is to it!
22 |
23 | How it actually plays out is fascinating. By simply subtracting the weight of neighboring cells with each other (`left - right`, `top - bottom`), you get an angle that will point around obstacles if they exist nearby, as well as point directly at the goal itself if it’s in plain sight (see this post’s header image for a visual). So calculation is extremely fast, and it works really well!
24 |
25 | This makes pathfinding very large numbers of objects very efficient. Another huge bonus is that it makes pathfinding around dynamic objects efficient and easy. With traditional pathfinding like A*, you’d have to recalculate the path for dynamic terrain, but you’d also have to do special modifications to the steering behaviors to avoid dynamic objects. With a flow field, you simply mark the cells occupied by these dynamic obstacles with negative values–a much cheaper operation, certainly.
26 |
27 | However, there are some problems too. I hoped to solve the local optima problem in particular with flocking–if one entity gets stuck, it will come out of it by following its neighbors out (who did not get stuck since they were not occupying the same cell due to collision detection). That was the theory anyway…
28 |
29 | #### Performance concerns
30 |
31 | The trade-off is that the vector grid takes up a ton of memory. This is ok for desktop and laptop computers, but not for anything smaller. In addition, the sheer number of calculations makes this technique somewhat troublesome. So, severe optimizations have to be made and while they’re not hard to do, it does take a bit of grunt work to pull off.
32 |
33 | For example, to help with memory you can re-use grid cells and only create them when absolutely necessary at run-time. If a path is no longer used, toss those cells into the “free” list to reuse for a different query. You can also use one data structure per cell, but keeping an array of vectors inside of it so that multiple fields can exist in the same space simultaneously without creating multiple instances of the grid cell class.
34 |
35 | For keeping the number of calculations low, you can further partition the grid so that you only calculate the cells that the entities will actually run into (as opposed to the whole map). But figuring out which cells to use like this requires a different pathfinding algorithm to be used, and ran on a different, lower-fidelity grid. Additionally, you can compact large fields that carry the same value into a single cell reference–as you can see in the image at top, there are a lot of duplicate vectors everywhere, screaming to be optimized.
36 |
37 | ## Flocking
38 |
39 | As you can see for yourself in the demo, flocking doesn’t make it any better. This is sad news, since flocking is a very powerful tool for keeping unit movement realistic and even emergent. I think it can be made to work much better if the algorithms are tweaked a bit (and weighted), but I’m sad that what I _did_ have wasn’t good enough to improve the solution–it actually made it a little worse.
40 |
41 | However, I believe the problem is not in the steering behavior itself as much as it is with my particular implementation. The flocking algorithm is fragile, yes, but I think the fact that I use radial collision for obstacles kind of cheats the system a bit–when one hits, it forces the entity into a certain direction, thus “solving” the indirection problem that local optima creates. This means that collision detection is all that is necessary, so the flocking only threw it off the flow. That, and I have no angular velocity limit which is a key part of steering behaviors, further screwing things up. This is called “lazy science”.
42 |
43 | ## Entity Component System
44 |
45 | This was a good time to try the component-based approach to game engine architecture. Popular engines like [Unity3D](http://unity3d.com/) make excellent use of it to great effect–Unity3D is arguably the most flexible engine on the market today, and an absolute pleasure to work with. _(My implementation is not complete, and quite shallow since I’m merely playing with the idea–I still loop through all entities directly, instead of organizing and looping through component lists–a “system”–like a proper component engine would do.)_ Components are a particularly good choice for HTML5 games, since it works to JavaScript’s strengths, such as its object-based foundation and its completely dynamic nature.
46 |
47 | #### Overview
48 |
49 | In entity component systems, composition is used instead of inheritance. This is all kinds of excellent. There is no monolithic base object (or any monolithic classes at all!), nor awkward hierarchies that don’t satisfy every design requirement. I highly recommend researching this architecture if you’re not already familiar with it.
50 |
51 | There is some weirdness when it comes to the details of how each aspect works with the others, and everyone does it differently apparently. Eventually I decided on a system where each component has a reference to its parent object, and it’s instantiated by passing in that parent as well as (optionally) any settings unique to that component.
52 |
53 | Communication between components is done directly by accessing another component through its parent. In order to avoid errors, the component will attach any prerequisite components to its parent if it doesn’t already exist (on a per-component basis, in its init function). This results in fast execution since there’s no unnecessary layers of abstraction that the CPU would have to work through otherwise.
54 |
55 | #### Further details
56 |
57 | Often times in games you have entities that require special behaviors for special situations. To accommodate this, the entity can listen to a component’s [Signals](https://github.com/millermedeiros/js-signals), if any exist. For example, a Health component will dispatch a signal when its amount goes below 0, or another when it reaches its max amount again. The same goes for collider components: a signal is dispatched when a collision occurs (by the collision broadphase system), passing in the object it collided with and the manifold of the collision as parameters of the callback. The entity can listen for these signals, or its other components can as well.
58 |
59 | After I decided on this, the whole concept fell into place and made everything much more manageable, flexible, and even fun to build with. I will definitely going balls to the wall with component systems in future projects!
60 |
61 | ## Conclusion
62 |
63 | **Ultimately**, I think going with steering behaviors and doing a single A* calculation for a leader of the pack gets you better movement behavior for a lot less memory and CPU time. **Conversely**, this method can be augmented with more interesting features such as _potential fields_ which would make the flow field much more useful… but again, only with what seems like an unnecessary amount of work, and still at a loss of performance if not refactored to hell and back. There are alternative techniques that I think are more efficient but likely just as effective (if not more so), such as _nav mesh_.
64 |
65 | Keep in mind that I did not dig into this experiment too deep, as I was merely curious about the broader implications and other implementation details (_eg_ the component system). Honestly, I think my code is a poor indicator of how a production-quality system would perform, both in terms of CPU as well as the AI’s aesthetic aspect.
66 |
67 | With that said, the code is simple enough, so please have a look and play around with it yourself. If you found a superior solution, feel free to submit a pull request–I’d really appreciate it! I want this solution to work and interested in learning more about it, but I need to move on to other projects. Thanks!
68 |
--------------------------------------------------------------------------------
/flow-field.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/vonWolfehaus/flow-field/e3d367b45c5ffef3a3d78b600f5275bf7e7acff5/flow-field.jpg
--------------------------------------------------------------------------------
/img/beetle.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/vonWolfehaus/flow-field/e3d367b45c5ffef3a3d78b600f5275bf7e7acff5/img/beetle.png
--------------------------------------------------------------------------------
/img/grid_bg.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/vonWolfehaus/flow-field/e3d367b45c5ffef3a3d78b600f5275bf7e7acff5/img/grid_bg.png
--------------------------------------------------------------------------------
/img/target.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/vonWolfehaus/flow-field/e3d367b45c5ffef3a3d78b600f5275bf7e7acff5/img/target.png
--------------------------------------------------------------------------------
/img/tilesprite.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/vonWolfehaus/flow-field/e3d367b45c5ffef3a3d78b600f5275bf7e7acff5/img/tilesprite.png
--------------------------------------------------------------------------------
/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 | Shift+click to draw/erase obstacles
48 | Source code on GitHub
49 |
50 |
51 |
52 |
53 |
63 |
64 |
--------------------------------------------------------------------------------
/scripts/Main.js:
--------------------------------------------------------------------------------
1 | define(['engine/Kai', 'engine/ComponentType', 'engine/CollisionGrid', 'engine/FlowGrid', 'entities/Thing', 'engine/TileMap'], function(Kai, ComponentType, CollisionGrid, FlowGrid, Thing, TileMap) {
2 |
3 | return function Main() {
4 |
5 | var gui = new dat.GUI();
6 | var grid = new CollisionGrid(200);
7 | var map = new TileMap(50, document.getElementById('tilesprite'));
8 | var flow = new FlowGrid(50, window.innerWidth, window.innerHeight);
9 | var allTheThings = [];
10 | var target = null;
11 | var paint = 0;
12 | // var timer = 120; // DEBUG
13 |
14 | var dt, last, now;
15 |
16 | function update() {
17 | var i, tile, pos = Kai.mouse.position;
18 |
19 | now = window.performance.now();
20 | dt = now - last;
21 | last = now;
22 | Kai.elapsed = dt * 0.0001;
23 | Kai.debugCtx.clearRect(0, 0, window.innerWidth, window.innerHeight);
24 |
25 | if (Kai.keys.shift && Kai.mouse.down) {
26 | tile = map.getTile(pos.x, pos.y);
27 |
28 | if (paint !== tile) {
29 | newTile = map.setTile(pos.x, pos.y, paint);
30 | flow.setBlockAt(pos.x, pos.y);
31 | }
32 | }
33 |
34 | for (i = 0; i < allTheThings.length; i++) {
35 | allTheThings[i].update();
36 | }
37 |
38 | if (Kai.guiOptions.collision) grid.update();
39 |
40 | Kai.renderer.render(Kai.stage);
41 |
42 | if (Kai.guiOptions.drawVectors) flow.draw(Kai.debugCtx);
43 | // grid.draw(Kai.debugCtx);
44 |
45 | // if (timer--) {
46 | requestAnimFrame(update);
47 | // }
48 | }
49 |
50 | function onKeyDown(key) {
51 | if (key === 32) {
52 | // flow.build();
53 | // flow.log();
54 | // grid.log();
55 | // allTheThings[0].flocker.log();
56 | }
57 | }
58 |
59 | function onMouseDown(pos) {
60 | var i, on,
61 | x = ~~(pos.x/flow.cellPixelSize) * flow.cellPixelSize,
62 | y = ~~(pos.y/flow.cellPixelSize) * flow.cellPixelSize;
63 |
64 | if (Kai.mouse.shift) {
65 | paint = !!map.getTile(pos.x, pos.y) ? 0 : 1;
66 |
67 | } else {
68 | on = flow.setGoal(pos.x, pos.y);
69 | if (on) {
70 | flow.build();
71 |
72 | target.position.x = x;
73 | target.position.y = y;
74 | target.visible = true;
75 |
76 | for (i = 0; i < allTheThings.length; i++) {
77 | allTheThings[i].vecFieldState.reachedGoal = false;
78 | }
79 | }
80 | }
81 | }
82 |
83 |
84 | init();
85 | function init() {
86 | var debugCanvas = document.getElementById('debug');
87 | debugCanvas.width = window.innerWidth;
88 | debugCanvas.height = window.innerHeight;
89 | Kai.debugCtx = debugCanvas.getContext('2d');
90 |
91 | Kai.renderer = PIXI.autoDetectRenderer(window.innerWidth, window.innerHeight, null, true);
92 | document.body.appendChild(Kai.renderer.view);
93 | // document.body.insertBefore(Kai.renderer.view, debugCanvas);
94 |
95 | Kai.stage = new PIXI.Stage();
96 |
97 | var texture = PIXI.Texture.fromImage('img/target.png');
98 | target = new PIXI.Sprite(texture);
99 | target.visible = false;
100 | Kai.stage.addChild(target);
101 |
102 | var i, x = 0, y = 0,
103 | amount = 20, size = 50,
104 | g = ~~(amount / 4);
105 |
106 | for (i = 0; i < amount; i++) {
107 | allTheThings.push(new Thing(x*size+100, y*size+50));
108 | if (++x === g) {
109 | x = 0;
110 | y++;
111 | }
112 | }
113 |
114 | var datgui = document.getElementsByClassName('dg')[0];
115 | datgui.addEventListener('mousedown', function(evt) {
116 | evt.stopPropagation(); // don't let clicks on the gui trigger anything else, it's annoying
117 | }, false);
118 |
119 | gui.width = 200;
120 | gui.add(Kai.guiOptions, 'drawVectors');
121 | gui.add(Kai.guiOptions, 'flocking');
122 | gui.add(Kai.guiOptions, 'collision');
123 | gui.add(Kai.guiOptions, 'clearObstacles');
124 |
125 | Kai.mouse.onDown.add(onMouseDown);
126 | Kai.keys.onDown.add(onKeyDown);
127 |
128 | console.log('[Main] Running');
129 |
130 | last = window.performance.now();
131 | update();
132 | }
133 |
134 | } // class
135 | });
--------------------------------------------------------------------------------
/scripts/components/BoundingCircle.js:
--------------------------------------------------------------------------------
1 | define(['engine/Kai'], function(Kai) {
2 |
3 | return function BoundingCircle(entity, settings) {
4 | // public shared components
5 | this.position = null;
6 | this.velocity = null;
7 |
8 | // public members unique to this component
9 | this.uniqueId = Date.now() + '' + Math.floor(Math.random()*1000);
10 |
11 | // components with the same collisionId will not be checked against each other
12 | // this is for composite entities and flocks
13 | this.collisionId = this.uniqueId;
14 | this.center = new Vec2();
15 | this.radius = -1;
16 | this.solid = true;
17 | this.restitution = 0.4;
18 |
19 | this.mass = 1;
20 | this.invmass = 0;
21 | this.bounce = 0;
22 |
23 | this.collisionSignal = new Signal();
24 |
25 | // internal
26 | var _self = this,
27 | _tau = Math.PI * 2;
28 |
29 | this.setMass = function(newMass) {
30 | this.mass = newMass;
31 | if (newMass <= 0) {
32 | this.invmass = 0;
33 | } else {
34 | this.invmass = 1/newMass;
35 | }
36 | };
37 |
38 | // nothing uses center so there's no need for this
39 | /*this.update = function() {
40 | this.center.x = this.position.x + this.radius;
41 | this.center.y = this.position.y + this.radius;
42 | };*/
43 |
44 | this.draw = function(ctx) {
45 | ctx.lineWidth = 1;
46 | ctx.strokeStyle = 'rgb(200, 10, 10)';
47 | ctx.beginPath();
48 | ctx.arc(this.center.x, this.center.y, this.radius, 0, _tau, true);
49 | ctx.stroke();
50 | };
51 |
52 | init();
53 | function init() {
54 | var p, defaults = {
55 | mass: 50,
56 | radius: 25
57 | };
58 |
59 | for (p in defaults) {
60 | if (!!settings && settings.hasOwnProperty(p)) {
61 | _self[p] = settings[p];
62 | } else {
63 | _self[p] = defaults[p];
64 | }
65 | }
66 |
67 | _self.position = entity.position;
68 | _self.velocity = entity.velocity;
69 | _self.setMass(_self.mass);
70 | }
71 |
72 | } // class
73 |
74 | });
75 |
--------------------------------------------------------------------------------
/scripts/components/VectorFieldState.js:
--------------------------------------------------------------------------------
1 | define(['engine/Kai', 'components/behaviors/Flock'], function(Kai, Flock) {
2 |
3 | return function VectorFieldState(entity) {
4 |
5 | // public members unique to this component
6 | this.uniqueId = Date.now() + '' + Math.floor(Math.random()*1000);
7 |
8 | this.fieldID = -1; // flow field index (as it sits in the array)
9 | this.reachedGoal = true;
10 |
11 | // shared references
12 | var _position = null,
13 | _vecField = Kai.flow;
14 |
15 | // internal settings
16 | var _self = this,
17 | // always best to use a timer to spread out the processing for expensive ops, especially when the user won't notice it
18 | _pollTime = 12, _timer = Math.ceil(Math.random() * _pollTime);
19 |
20 |
21 | this.update = function() {
22 | var node;
23 | _timer--;
24 | if (_timer < 0 && !this.reachedGoal) {
25 | _timer = _pollTime;
26 |
27 | if (_position.distanceTo(Kai.flow.goalPixels) < 100) {
28 | // console.log('reached goal');
29 | this.reachedGoal = true;
30 | } else {
31 | node = entity.flock.nearby.first;
32 | while (node) {
33 | if (!node.obj.vecFieldState) {
34 | node = node.next;
35 | continue;
36 | }
37 |
38 | if (node.obj.vecFieldState.reachedGoal) {
39 | // console.log('neighbor reached goal');
40 | this.reachedGoal = true;
41 | break;
42 | }
43 | node = node.next;
44 | }
45 | }
46 | }
47 |
48 | return _vecField.getVectorAt(_position, this.fieldID);
49 | };
50 |
51 | this.destroy = function() {
52 | _vecField = null;
53 | _position = null;
54 | entity = null;
55 | };
56 |
57 | init();
58 | function init() {
59 | Kai.addComponent(entity, ComponentType.FLOCK, Flock);
60 |
61 | _position = entity.position;
62 | }
63 | } // class
64 |
65 | });
66 |
--------------------------------------------------------------------------------
/scripts/components/behaviors/Flock.js:
--------------------------------------------------------------------------------
1 | define(['engine/Kai'], function(Kai) {
2 |
3 | // http://files.arcticpaint.com/flock/
4 | // fuck, can't do that rotation shit, that's not how I store it
5 | // try this instead: http://processingjs.org/learning/topic/flocking/
6 | // http://lucasdup.in/js/bg.js
7 | return function Flock(entity, settings) {
8 |
9 | // public members unique to this component
10 | this.uniqueId = Date.now() + '' + Math.floor(Math.random()*1000);
11 | this.maxForce = 0;
12 | this.maxSpeed = 0;
13 | this.flockRadius = 0;
14 | this.flockId = 0;
15 | this.nearby = new LinkedList();
16 |
17 | // shared references
18 | var _position = entity.position,
19 | _velocity = entity.velocity,
20 | _collisionGrid = Kai.grid;
21 |
22 | // internal settings
23 | var _self = this, _cachedLifetime = 4, // number of ticks until we refresh nearby
24 | _stepToCache = Math.ceil(Math.random() * _cachedLifetime),
25 | _avoidRadius2 = 0;
26 |
27 | // scratch objects
28 | var _separation = new Vec2(),
29 | _alignment = new Vec2(),
30 | _cohesion = new Vec2(),
31 | _accel = new Vec2(),
32 | _steer = new Vec2(), _scratch = new Vec2();
33 |
34 | this.update = function() {
35 | var dx, dy, dist, node, obj, l, steer;
36 |
37 | _accel.x = _accel.y = 0;
38 | _separation.x = _separation.y = 0;
39 | _alignment.x = _alignment.y = 0;
40 | _cohesion.x = _cohesion.y = 0;
41 |
42 | if (--_stepToCache === 0) {
43 | _stepToCache = _cachedLifetime;
44 | _collisionGrid.getNeighbors(entity, this.flockRadius, this.nearby);
45 | }
46 |
47 | l = this.nearby.length;
48 | if (l > 0) {
49 | node = this.nearby.first;
50 |
51 | while (node) {
52 | obj = node.obj;
53 | if (!obj.flock || obj.flock.flockId !== this.flockId) {
54 | node = node.next;
55 | continue;
56 | }
57 |
58 | Vec2.draw(Kai.debugCtx, _position, obj.position, 'rgba(255, 255, 255, 0.6)'); // DEBUG
59 |
60 | dx = _position.x - obj.position.x; // this should be the center position
61 | dy = _position.y - obj.position.y;
62 | dist = Math.sqrt((dx * dx) + (dy * dy));
63 |
64 | if (dist < this.separationRadius) {
65 | // rule 1
66 | _scratch.x = dx;
67 | _scratch.y = dy;
68 | _scratch.divideScalar(dist);
69 | _separation.add(_scratch);
70 | } else {
71 | // rule 2
72 | _alignment.add(obj.velocity);
73 |
74 | // rule 3
75 | _cohesion.add(obj.position);
76 | }
77 |
78 | node = node.next;
79 | }
80 |
81 | // i made a mistake somewhere so that this is needed,
82 | // otherwise they seek the top left corner. halp.
83 | l--;
84 |
85 | _separation.divideScalar(l);
86 | _separation.normalize();
87 |
88 | _alignment.divideScalar(l);
89 | _alignment.normalize();
90 |
91 | _cohesion.divideScalar(l);
92 | this.seek(_cohesion);
93 | }
94 |
95 | _accel.add(_separation);
96 | _accel.add(_alignment);
97 |
98 | return _accel;
99 | };
100 |
101 | this.seek = function(target, slowdown) {
102 | var d;
103 | slowdown = !!slowdown;
104 |
105 | // DebugDraw.circle(target.x, target.y, 5); // DEBUG
106 |
107 | _scratch.x = target.x - _position.x;
108 | _scratch.y = target.y - _position.y;
109 | d = _scratch.getLength();
110 |
111 | if (d > 0) {
112 | _scratch.normalize();
113 |
114 | if (slowdown && d < this.slowingRadius) {
115 | _scratch.multiplyScalar(this.maxSpeed * (d / 100)); // arbitrary dampening
116 | } else {
117 | _scratch.multiplyScalar(this.maxSpeed);
118 | }
119 |
120 | _steer.x = _scratch.x - _velocity.x;
121 | _steer.y = _scratch.y - _velocity.y;
122 | _steer.truncate(this.maxForce);
123 |
124 | } else {
125 | return _accel;
126 | }
127 |
128 | return _accel.add(_steer);
129 | };
130 |
131 | this.flee = function(target, slowdown) {
132 | var d;
133 | slowdown = !!slowdown;
134 |
135 | // DebugDraw.circle(target.x, target.y, 5); // DEBUG
136 |
137 | _scratch.x = _position.x - target.x;
138 | _scratch.y = _position.y - target.y;
139 |
140 | _steer.x = _scratch.x - _velocity.x;
141 | _steer.y = _scratch.y - _velocity.y;
142 | _steer.truncate(this.maxForce);
143 |
144 | return _accel.add(_steer);
145 | };
146 |
147 | this.log = function() {
148 | console.log(_cohesion);
149 | };
150 |
151 |
152 | init();
153 | function init() {
154 | var p, defaults = {
155 | flockRadius: 100,
156 | flockId: 1,
157 | separationRadius: 50,
158 | maxForce: 1,
159 | maxSpeed: 10,
160 | slowingRadius: 100
161 | };
162 |
163 | for (p in defaults) {
164 | if (!!settings && settings.hasOwnProperty(p)) {
165 | _self[p] = settings[p];
166 | } else {
167 | _self[p] = defaults[p];
168 | }
169 | }
170 |
171 | _avoidRadius2 = _self.flockRadius * _self.flockRadius;
172 |
173 | }
174 |
175 | } // class
176 |
177 | });
178 |
--------------------------------------------------------------------------------
/scripts/components/input/KeyboardController.js:
--------------------------------------------------------------------------------
1 | define(function() {
2 |
3 | return function KeyboardController() {
4 |
5 | this.key = -1;
6 |
7 | this.onDown = new Signal();
8 | this.onUp = new Signal();
9 |
10 | this.down = false;
11 | this.shift = false;
12 | this.ctrl = false;
13 |
14 | var _self = this,
15 | _downPrev = false;
16 |
17 |
18 | function onDown(evt) {
19 | _self.down = true;
20 |
21 | _self.shift = !!evt.shiftKey;
22 | _self.ctrl = !!evt.ctrlKey;
23 | _self.key = evt.keyCode;
24 |
25 | _self.onDown.dispatch(_self.key);
26 | }
27 |
28 | function onUp(evt) {
29 | _self.down = false;
30 | _self.key = -1;
31 | _self.shift = false;
32 | _self.ctrl = false;
33 |
34 | _self.onUp.dispatch(_self.key);
35 | }
36 |
37 | init();
38 | function init() {
39 | document.addEventListener('keydown', onDown, false);
40 | document.addEventListener('keyup', onUp, false);
41 | }
42 |
43 | } // class
44 |
45 | });
--------------------------------------------------------------------------------
/scripts/components/input/MouseController.js:
--------------------------------------------------------------------------------
1 | define(function() {
2 |
3 | return function MouseController() {
4 |
5 | this.position = new Vec2();
6 |
7 | this.onDown = new Signal();
8 | this.onUp = new Signal();
9 |
10 | this.down = false;
11 | this.shift = false;
12 | this.ctrl = false;
13 |
14 | var _self = this,
15 | _downPrev = false;
16 |
17 | function onDown(evt) {
18 | _self.position.x = evt.pageX;
19 | _self.position.y = evt.pageY;
20 | _self.down = true;
21 |
22 | _self.shift = !!evt.shiftKey;
23 | _self.ctrl = !!evt.ctrlKey;
24 |
25 | _self.onDown.dispatch(_self.position);
26 | }
27 |
28 | function onUp(evt) {
29 | _self.position.x = evt.pageX;
30 | _self.position.y = evt.pageY;
31 | _self.down = false;
32 | _self.onUp.dispatch(_self.position);
33 | }
34 |
35 | function onMove(evt) {
36 | _self.position.x = evt.pageX;
37 | _self.position.y = evt.pageY;
38 | }
39 |
40 | function onOut(evt) {
41 | _self.down = false;
42 | }
43 |
44 | init();
45 | function init() {
46 | document.addEventListener('mousedown', onDown, false);
47 | document.addEventListener('mouseup', onUp, false);
48 | document.addEventListener('mouseout', onOut, false);
49 | document.addEventListener('mousemove', onMove, false);
50 | }
51 |
52 | } // class
53 |
54 | });
--------------------------------------------------------------------------------
/scripts/engine/CollisionGrid.js:
--------------------------------------------------------------------------------
1 | define(['engine/Kai', 'components/BoundingCircle'], function(Kai, BoundingCircle) {
2 |
3 | /**
4 | *
5 | */
6 | return function CollisionGrid(cellSize) {
7 |
8 | this.cellPixelSize = cellSize;
9 |
10 | this.widthInCells = Math.floor(Kai.width / cellSize) + 1;
11 | this.heightInCells = Math.floor(Kai.height / cellSize) + 1;
12 |
13 | this.numCells = this.widthInCells * this.heightInCells;
14 |
15 | var _self = this, _nearbyList = new LinkedList(),
16 | _cells = [], _lengths = [],
17 | _itemList = new LinkedList(), // ALL THE THINGS
18 | _sizeMulti = 1 / this.cellPixelSize;
19 |
20 | // scratch objects
21 | var _normal = new Vec2(),
22 | _rv = new Vec2(),
23 | _impulse = new Vec2(),
24 | _mtd = new Vec2(),
25 | _difference = new Vec2();
26 |
27 | // this is as naive a broadphase as you can get, so plenty of room to optimize!
28 | this.update = function() {
29 | var i, cell, cellPos, cellNode, m, node, item, other;
30 | var x, y, minX, minY, maxX, maxY, gridRadius;
31 |
32 | for (i = 0; i < this.numCells; i++) {
33 | _cells[i].clear();
34 | }
35 |
36 | node = _itemList.first;
37 | while (node) {
38 | item = node.obj;
39 | if (!item.collider.solid) {
40 | node = node.next;
41 | continue;
42 | }
43 |
44 | gridRadius = Math.ceil(item.collider.radius * _sizeMulti);
45 | itemX = ~~(item.position.x * _sizeMulti);
46 | itemY = ~~(item.position.y * _sizeMulti);
47 |
48 | // in our case it will grab a 3x3 section of the grid, which is unnecessary (should only get 2x2 based on quadrant) but it works
49 | minX = itemX - gridRadius;
50 | if (minX < 0) minX = 0;
51 | minY = itemY - gridRadius;
52 | if (minY < 0) minY = 0;
53 | maxX = itemX + gridRadius;
54 | if (maxX > this.widthInCells) maxX = this.widthInCells;
55 | maxY = itemY + gridRadius;
56 | if (maxY > this.heightInCells) maxY = this.heightInCells;
57 |
58 | for (x = minX; x <= maxX; x++) {
59 | for (y = minY; y <= maxY; y++) {
60 | cellPos = (x * this.heightInCells) + y;
61 | cell = _cells[cellPos];
62 | if (!cell) continue;
63 |
64 | cellNode = cell.first;
65 | while (cellNode) {
66 | other = cellNode.obj;
67 | if (!other.collider.solid || other.collider.collisionId === item.collider.collisionId) {
68 | cellNode = cellNode.next;
69 | continue;
70 | }
71 |
72 | m = this.collideBalls(item.collider, other.collider); // separates
73 | if (m) {
74 | this.resolveCollision(item.collider, other.collider, m); // reacts
75 | // item.collider.collisionSignal.dispatch(other, m);
76 | // other.collider.collisionSignal.dispatch(item, m);
77 | }
78 |
79 | cellNode = cellNode.next;
80 | }
81 |
82 | _cells[cellPos].add(item);
83 | }
84 | }
85 |
86 | node = node.next;
87 | }
88 | };
89 |
90 | this.draw = function(ctx) {
91 | var i, j;
92 |
93 | ctx.lineWidth = 1;
94 | ctx.strokeStyle = 'rgba(0, 120, 120, 0.5)';
95 |
96 | for (i = 0; i < this.widthInCells; i++) {
97 | for (j = 0; j < this.heightInCells; j++) {
98 | ctx.strokeRect(i*this.cellPixelSize, j*this.cellPixelSize, this.cellPixelSize, this.cellPixelSize);
99 | }
100 | }
101 | };
102 |
103 | this.add = function(obj) {
104 | if (!obj.collider) {
105 | throw new Error('Any object added to the collision grid must have a collider component');
106 | }
107 | if (_itemList.has(obj)) return;
108 | _itemList.add(obj);
109 | };
110 |
111 | this.remove = function(obj) {
112 | _itemList.remove(obj);
113 | };
114 |
115 | /**
116 | * Tests if there's any overlap between two given circles, and returns
117 | * the resulting Minimum Translation Distance if so.
118 | *
119 | * @source https://github.com/vonWolfehaus/von-physics
120 | */
121 | this.collideBalls = function(a, b) {
122 | var dx = a.position.x - b.position.x;
123 | var dy = a.position.y - b.position.y;
124 | var dist = (dx * dx) + (dy * dy);
125 | var radii = a.radius + b.radius;
126 |
127 | if (dist < radii * radii) {
128 | dist = Math.sqrt(dist);
129 |
130 | DebugDraw.circle(a.position.x, a.position.y, 25, 'rgba(0, 0, 0, 0.2)'); // DEBUG
131 |
132 | _difference.reset(dx, dy);
133 | if (dist == 0) {
134 | dist = a.radius + b.radius - 1;
135 | _difference.reset(radii, radii);
136 | }
137 | var j = (radii - dist) / dist;
138 | _mtd.reset(_difference.x * j, _difference.y * j);
139 |
140 | // separate them!
141 | var cim = a.invmass + b.invmass;
142 | a.position.x += _mtd.x * (a.invmass / cim);
143 | a.position.y += _mtd.y * (a.invmass / cim);
144 |
145 | b.position.x -= _mtd.x * (b.invmass / cim);
146 | b.position.y -= _mtd.y * (b.invmass / cim);
147 |
148 | return _mtd;
149 | }
150 | return null;
151 | };
152 |
153 | /**
154 | * Using the Minimum Translation Distance provided, will calculate the impulse to apply to
155 | * the circles to make them react "properly".
156 | *
157 | * @source https://github.com/vonWolfehaus/von-physics
158 | */
159 | this.resolveCollision = function(a, b, mtd) {
160 | // impact speed
161 | _rv.reset(a.velocity.x - b.velocity.x, a.velocity.y - b.velocity.y);
162 |
163 | _normal.copy(mtd).normalize();
164 |
165 | var velAlongNormal = _rv.dotProduct(_normal);
166 | if (velAlongNormal > 0) {
167 | // the 2 balls are intersecting, but they're moving away from each other already
168 | return;
169 | }
170 |
171 | var e = Math.min(a.restitution, b.restitution);
172 |
173 | // calculate impulse scalar
174 | var i = -(1 + e) * velAlongNormal;
175 | i /= a.invmass + b.invmass;
176 |
177 | _impulse.reset(_normal.x * i, _normal.y * i);
178 |
179 | a.velocity.x += (a.invmass * _impulse.x);
180 | a.velocity.y += (a.invmass * _impulse.y);
181 |
182 | b.velocity.x -= (b.invmass * _impulse.x);
183 | b.velocity.y -= (b.invmass * _impulse.y);
184 | };
185 |
186 | this.getNeighbors = function(entity, pixelRadius, list) {
187 | var x, y, dx, dy, cell, node, other, cellPos, minX, minY, maxX, maxY,
188 | influence = pixelRadius * pixelRadius,
189 | gridRadius = Math.ceil(pixelRadius * _sizeMulti),
190 | pos = entity.position,
191 | itemX = ~~(pos.x * _sizeMulti),
192 | itemY = ~~(pos.y * _sizeMulti);
193 |
194 | // return _itemList;
195 |
196 | if (!list) {
197 | list = _nearbyList;
198 | }
199 | list.clear();
200 |
201 | // enforce grid boundaries:
202 | minX = itemX - gridRadius;
203 | if (minX < 0) minX = 0;
204 | minY = itemY - gridRadius;
205 | if (minY < 0) minY = 0;
206 | maxX = itemX + gridRadius;
207 | if (maxX > this.widthInCells) maxX = this.widthInCells;
208 | maxY = itemY + gridRadius;
209 | if (maxY > this.heightInCells) maxY = this.heightInCells;
210 |
211 | // console.log('gridRadius: '+gridRadius+': '+minX+'-'+maxX+', '+minY+'-'+maxY);
212 |
213 | for (x = minX; x <= maxX; x++) {
214 | for (y = minY; y <= maxY; y++) {
215 | cellPos = (x * this.heightInCells) + y;
216 | cell = _cells[cellPos];
217 | if (!cell) continue;
218 |
219 | node = cell.first;
220 | while (node) {
221 | other = node.obj;
222 | if (other.uniqueId === entity.uniqueId) {
223 | node = node.next;
224 | continue;
225 | }
226 |
227 | dx = pos.x - other.position.x;
228 | dy = pos.y - other.position.y;
229 |
230 | if ((dx * dx) + (dy * dy) <= influence) {
231 | list.add(other);
232 | }
233 |
234 | node = node.next;
235 | }
236 | }
237 | }
238 |
239 | return list;
240 | };
241 |
242 | // does NOT clear the list for you; this is so we can build up a single list for multiple areas
243 | this.getNearby = function(pos, pixelRadius, list) {
244 | var x, y, dx, dy, cell, node, other, cellPos, minX, minY, maxX, maxY,
245 | influence = pixelRadius * pixelRadius,
246 | gridRadius = Math.ceil(pixelRadius * _sizeMulti),
247 | itemX = ~~(pos.x * _sizeMulti),
248 | itemY = ~~(pos.y * _sizeMulti);
249 |
250 | if (!list) {
251 | _nearbyList.clear();
252 | list = _nearbyList;
253 | }
254 |
255 | // enforce grid boundaries:
256 | minX = itemX - gridRadius;
257 | if (minX < 0) minX = 0;
258 | minY = itemY - gridRadius;
259 | if (minY < 0) minY = 0;
260 | maxX = itemX + gridRadius;
261 | if (maxX > this.widthInCells) maxX = this.widthInCells;
262 | maxY = itemY + gridRadius;
263 | if (maxY > this.heightInCells) maxY = this.heightInCells;
264 |
265 | // console.log('gridRadius: '+gridRadius+': '+minX+'-'+maxX+', '+minY+'-'+maxY);
266 |
267 | for (x = minX; x <= maxX; x++) {
268 | for (y = minY; y <= maxY; y++) {
269 | cellPos = (x * this.heightInCells) + y;
270 | cell = _cells[cellPos];
271 | if (!cell) continue;
272 |
273 | node = cell.first;
274 | while (node) {
275 | other = node.obj;
276 | dx = pos.x - other.position.x;
277 | dy = pos.y - other.position.y;
278 |
279 | if ((dx * dx) + (dy * dy) <= influence) {
280 | list.add(other);
281 | }
282 |
283 | node = node.next;
284 | }
285 | }
286 | }
287 |
288 | return list;
289 | };
290 |
291 | this.log = function() {
292 | console.log('Cells: '+_cells.length);
293 | };
294 |
295 |
296 | init();
297 | function init() {
298 | var i, j;
299 | // console.log(_cells);
300 | for (i = 0; i < _self.numCells; i++) {
301 | _cells[i] = new LinkedList();
302 | }
303 | // console.log(_cells);
304 |
305 | Kai.grid = _self;
306 | console.log('[CollisionGrid] '+_self.widthInCells+'x'+_self.heightInCells+': '+_self.numCells+' cells');
307 | }
308 |
309 | } // class
310 | });
--------------------------------------------------------------------------------
/scripts/engine/ComponentType.js:
--------------------------------------------------------------------------------
1 | // instead of typing all this shit out by hand, have a script that build the list of all js files in
2 | // the components folder and just load that in (they'll have to be sorted by dependencies)
3 | define(['components/behaviors/Flock', 'components/VectorFieldState', 'components/BoundingCircle'],
4 | function(Flock, VectorFieldState, BoundingCircle) {
5 |
6 | var c = {
7 | VECTOR_FIELD: { accessor: 'vecFieldState', index: 0, proto: VectorFieldState },
8 | FLOCK: { accessor: 'flock', index: 1, proto: Flock },
9 | RADIAL_COLLIDER: { accessor: 'collider', index: 1, proto: BoundingCircle }
10 | };
11 |
12 | window.ComponentType = c;
13 |
14 | return c;
15 |
16 | });
17 |
--------------------------------------------------------------------------------
/scripts/engine/DebugDraw.js:
--------------------------------------------------------------------------------
1 | var DebugDraw = {};
2 | (function() {
3 |
4 | var ctx = document.getElementById('debug').getContext('2d');
5 |
6 | DebugDraw.circle = function(x, y, radius, color) {
7 | color = color || 'rgba(10, 200, 30)';
8 | ctx.beginPath();
9 | ctx.arc(x, y, radius, 0, Math.PI*2);
10 | ctx.lineWidth = 1;
11 | ctx.strokeStyle = color;
12 | ctx.stroke();
13 | };
14 |
15 | DebugDraw.rectangle = function(x, y, sizeX, sizeY, color) {
16 | color = color || 'rgba(10, 200, 30)';
17 | ctx.beginPath();
18 | ctx.lineWidth = 1;
19 | ctx.strokeStyle = color;
20 | ctx.strokeRect(x - (sizeX*0.5), y - (sizeY*0.5), sizeX, sizeY);
21 | };
22 | console.log('[DebugDraw] ');
23 | }());
--------------------------------------------------------------------------------
/scripts/engine/FlowGrid.js:
--------------------------------------------------------------------------------
1 | define(['engine/Kai', 'engine/FlowGridNode'], function(Kai, FlowGridNode) {
2 |
3 | /**
4 | * This is a flow grid (or vector grid) which is a combination of a grid that's generated using the
5 | * wavefront algorithm, which is then used to build a grid of vectors that literally point to a goal.
6 | * This provides directions for any entity to the goal point quickly. It is best used in situations where
7 | * a LOT of entities share a goal, and even better when those entities use steering behaviors, making for a
8 | * very fluid, natural motion path.
9 | *
10 | * Flexibility can be added by temporarily "disrupting" the grid with other fields emitted by dynamics obstacles.
11 | * They would change the vector grid under them (not the grid) and have it return to normal as they move away.
12 | *
13 | * Optimization is needed. There should be sectors of the grid (or just one larger grid holding multiple
14 | * FlowGrid instances) that only get rebuilt when needed. This might require another pathfinder like A* in
15 | * order to determine which sectors need updating, to prevent the wave from propagating outside the needed
16 | * bounds.
17 | *
18 | * A good place to improve on this is potential fields: http://aigamedev.com/open/tutorials/potential-fields/
19 | *
20 | * @author Corey Birnbaum
21 | * @source http://gamedev.tutsplus.com/tutorials/implementation/goal-based-vector-field-pathfinding/
22 | */
23 | return function FlowGrid(cellSize, width, height) {
24 |
25 | this.cellPixelSize = cellSize;
26 |
27 | this.widthInCells = Math.floor(width / cellSize) + 1;
28 | this.heightInCells = Math.floor(height / cellSize) + 1;
29 |
30 | this.numCells = this.widthInCells * this.heightInCells;
31 |
32 | this.grid = [];
33 |
34 | this.goal = new Vec2();
35 | this.goalPixels = new Vec2();
36 |
37 | var _self = this, _openList = new LinkedList(), _tau = Math.PI*2,
38 | _sizeMulti = 1 / this.cellPixelSize;
39 |
40 |
41 | /**
42 | * Coordinates are in world space (pixels).
43 | */
44 | this.setGoal = function(endPixelX, endPixelY) {
45 | var endX = ~~(endPixelX * _sizeMulti);
46 | var endY = ~~(endPixelY * _sizeMulti);
47 |
48 | if (endX < 0 || endY < 0 || endX >= this.widthInCells || endY >= this.heightInCells) {
49 | throw new Error('[FlowGrid.build] Out of bounds');
50 | }
51 |
52 | if (this.goal.x === endX && this.goal.y === endY) return false;
53 |
54 | this.goal.x = endX;
55 | this.goal.y = endY;
56 | this.goalPixels.reset(endPixelX, endPixelY);
57 |
58 | return true;
59 | };
60 |
61 | /**
62 | * Runs a breadth-first search on the heatmap, stores how many steps it took to get to each tile
63 | * along the way. Then calculates the movement vectors.
64 | */
65 | this.build = function() {
66 | var i, j, current, node, neighbor,
67 | v, a, b;
68 |
69 | for (i = 0; i < this.widthInCells; i++) {
70 | for (j = 0; j < this.heightInCells; j++) {
71 | this.grid[i][j].open = true;
72 | }
73 | }
74 |
75 | _openList.clear();
76 |
77 | node = this.grid[this.goal.x][this.goal.y];
78 | node.cost = 0;
79 |
80 | _openList.add(node);
81 |
82 | // front the wave. set fire to the brush. etc.
83 | while (_openList.length) {
84 | node = _openList.shift();
85 | node.open = false;
86 |
87 | current = this.grid[node.gridX][node.gridY];
88 |
89 | // left
90 | neighbor = node.gridX-1 >= 0 ? this.grid[node.gridX-1][node.gridY] : null;
91 | if (neighbor && neighbor.open && neighbor.passable) {
92 | neighbor.cost = current.cost + 1;
93 | neighbor.open = false; // we must set false now, in case a different neighbor gets this as neighbor
94 | _openList.add(neighbor);
95 | }
96 | // right
97 | neighbor = this.grid[node.gridX+1] ? this.grid[node.gridX+1][node.gridY] : null;
98 | if (neighbor && neighbor.open && neighbor.passable) {
99 | neighbor.cost = current.cost + 1;
100 | neighbor.open = false;
101 | _openList.add(neighbor);
102 | }
103 | // up
104 | neighbor = this.grid[node.gridX][node.gridY-1] || null;
105 | if (neighbor && neighbor.open && neighbor.passable) {
106 | neighbor.cost = current.cost + 1;
107 | neighbor.open = false;
108 | _openList.add(neighbor);
109 | }
110 | // down
111 | neighbor = this.grid[node.gridX][node.gridY+1] || null;
112 | if (neighbor && neighbor.open && neighbor.passable) {
113 | neighbor.cost = current.cost + 1;
114 | neighbor.open = false;
115 | _openList.add(neighbor);
116 | }
117 | // i++; // DEBUG
118 | }
119 |
120 | // recalculate the vector field
121 | for (i = 0; i < this.widthInCells; i++) {
122 | for (j = 0; j < this.heightInCells; j++) {
123 | v = this.grid[i][j];
124 |
125 | a = i-1 >= 0 && this.grid[i-1][j].passable ? this.grid[i-1][j].cost : v.cost;
126 | b = i+1 < this.widthInCells && this.grid[i+1][j].passable ? this.grid[i+1][j].cost : v.cost;
127 | v.x = a - b;
128 |
129 | a = j-1 >= 0 && this.grid[i][j-1].passable ? this.grid[i][j-1].cost : v.cost;
130 | b = j+1 < this.heightInCells && this.grid[i][j+1].passable ? this.grid[i][j+1].cost : v.cost;
131 | v.y = a - b;
132 | }
133 | }
134 | // TODO: normalize values
135 |
136 | // console.log('[FlowGrid.regenHeatmap] Completed in '+i+' iterations:');
137 | // console.log(this.toString());
138 | };
139 |
140 | this.draw = function(ctx) {
141 | var i, j, v, vx, vy;
142 | ctx.lineWidth = 1;
143 | for (i = 0; i < this.widthInCells; i++) {
144 | for (j = 0; j < this.heightInCells; j++) {
145 | ctx.strokeStyle = 'rgba(0, 0, 0, 0.4)';
146 | ctx.fillStyle = 'rgba(0, 0, 0, 0.3)';
147 | v = this.grid[i][j];
148 | if (!v.passable) continue;
149 |
150 | vx = (i*this.cellPixelSize)+(this.cellPixelSize*0.5);
151 | vy = (j*this.cellPixelSize)+(this.cellPixelSize*0.5);
152 |
153 | ctx.beginPath();
154 | ctx.arc(vx, vy, 3, 0, _tau, false);
155 | ctx.fill();
156 | ctx.moveTo(vx, vy);
157 | ctx.lineTo(vx+(v.x*11), vy+(v.y*11));
158 | ctx.stroke();
159 |
160 | // ctx.strokeStyle = 'rgba(120, 0, 0, 0.5)';
161 | // ctx.strokeRect(i*this.cellPixelSize, j*this.cellPixelSize, this.cellPixelSize, this.cellPixelSize);
162 | }
163 | }
164 | };
165 |
166 | /**
167 | * Given the pixel coordinates, return the Vec2 associated with that position.
168 | */
169 | this.getVectorAt = function(pos) {
170 | var x = ~~(pos.x * _sizeMulti);
171 | var y = ~~(pos.y * _sizeMulti);
172 | if (this.grid[x] && this.grid[x][y]) {
173 | return this.grid[x][y];
174 | }
175 | return null;
176 | };
177 |
178 | /**
179 | * Flips the flow switch at the provided pixel coordinates, so it will either become passable, or not.
180 | */
181 | this.setBlockAt = function(x, y) {
182 | x = ~~(x * _sizeMulti);
183 | y = ~~(y * _sizeMulti);
184 | this.grid[x][y].passable = !this.grid[x][y].passable;
185 | this.grid[x][y].cost = -1;
186 |
187 | this.build();
188 |
189 | return !this.grid[x][y].passable;
190 | };
191 |
192 | this.clear = function() {
193 | var i, j, v;
194 | for (i = 0; i < this.widthInCells; i++) {
195 | for (j = 0; j < this.heightInCells; j++) {
196 | v = this.grid[i][j];
197 | v.passable = true;
198 | v.cost = -1;
199 | }
200 | }
201 | this.build();
202 | };
203 |
204 | this.toString = function() {
205 | var str = '', x = 0, y = 0,
206 | i, v;
207 |
208 | for (i = 0; i < this.numCells; i++) {
209 | v = this.grid[x][y].cost;
210 |
211 | if (v > 99) str += v + ',';
212 | else if (v > 9 && v < 100) str += ' ' + v + ',';
213 | else str += ' ' + v + ',';
214 |
215 | if (++x === this.widthInCells) {
216 | x = 0;
217 | y++;
218 | str += '\n';
219 | }
220 | }
221 | str = str.substring(0, str.length-2); // get rid of the trailing comma because i'm ocd or something
222 | return str;
223 | };
224 |
225 | this.log = function() {
226 | console.log(this.toString());
227 | };
228 |
229 | init();
230 | function init() {
231 | var i, j;
232 |
233 | for (i = 0; i < _self.widthInCells; i++) {
234 | _self.grid[i] = [];
235 | for (j = 0; j < _self.heightInCells; j++) {
236 | _self.grid[i][j] = new FlowGridNode(i, j);
237 | }
238 | }
239 |
240 | Kai.flow = _self;
241 |
242 | console.log('[FlowGrid] '+_self.widthInCells+'x'+_self.heightInCells);
243 | }
244 |
245 | } // class
246 | });
--------------------------------------------------------------------------------
/scripts/engine/FlowGridNode.js:
--------------------------------------------------------------------------------
1 | define(function() {
2 |
3 | return function FlowGridNode(gx, gy) {
4 | // velocity
5 | this.x = 0;
6 | this.y = 0;
7 |
8 | this.gridX = gx;
9 | this.gridY = gy;
10 |
11 | // heat value
12 | this.cost = 0;
13 | this.open = true;
14 | this.passable = true;
15 |
16 | this.uniqueId = Date.now() + '' + Math.floor(Math.random()*1000);
17 |
18 | } // class
19 | });
--------------------------------------------------------------------------------
/scripts/engine/Kai.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Global state resources. No idea why I called it 'Kai'.
3 | */
4 | define(['components/input/MouseController', 'components/input/KeyboardController'], function(MouseController, KeyboardController) {
5 | var g = {
6 | stage: null,
7 | renderer: null,
8 | grid: null,
9 | flow: null,
10 | map: null,
11 |
12 | debugCtx: null,
13 | components: null,
14 | guiOptions: {
15 | drawVectors: true,
16 | flocking: true,
17 | collision: true,
18 | clearObstacles: function() {
19 | window.Kai.map.clear();
20 | window.Kai.flow.clear();
21 | }
22 | },
23 |
24 | elapsed: 0,
25 |
26 | // sim world dimensions
27 | width: window.innerWidth,
28 | height: window.innerHeight,
29 |
30 | mouse: new MouseController(),
31 | keys: new KeyboardController(),
32 |
33 | addComponent: function(entity, componentData, options) {
34 | var prop, idx, ComponentObject;
35 | options = options || null;
36 |
37 | prop = componentData.accessor;
38 | idx = componentData.index;
39 | ComponentObject = componentData.proto;
40 |
41 | if (!entity[prop]) {
42 | // console.log('[Kai] Adding '+prop);
43 | entity[prop] = new ComponentObject(entity, options);
44 | // engine.systemList[idx].add(entity[prop]);
45 | } /*else console.log('[Kai] '+prop+' already exists on entity');*/
46 |
47 | return entity[prop];
48 | },
49 |
50 | removeComponent: function(entity, componentData) {
51 |
52 | }
53 | };
54 | window.Kai = g;
55 | return g;
56 | });
--------------------------------------------------------------------------------
/scripts/engine/TileMap.js:
--------------------------------------------------------------------------------
1 | define(['engine/Kai', 'entities/Block'], function(Kai, Block) {
2 |
3 | // Simple array map. Collision is done by adding/removing collider components to the system at grid positions that have
4 | // less than 3 neighbors.
5 |
6 | return function TileMap(tileSize, tilesprite) {
7 |
8 | this.widthInTiles = Math.floor(Kai.width / tileSize) + 1;
9 | this.heightInTiles = Math.floor(Kai.height / tileSize) + 1;
10 |
11 | this.numTiles = this.widthInTiles * this.heightInTiles;
12 | this.grid = [];
13 |
14 | // internal
15 | var _self = this,
16 | _blockCache = new LinkedList(),
17 | _blockLookup = {},
18 | _ctx = null,
19 | _sizeMulti = 1 / tileSize;
20 |
21 |
22 |
23 | /**
24 | * Add or remove a tile at the given pixel coordinates.
25 | * @returns [boolean] If a tile was changed.
26 | */
27 | this.setTile = function(x, y, forceValue) {
28 | var idx, tile, block, px, py;
29 |
30 | forceValue = forceValue || null;
31 | x = ~~(x * _sizeMulti);
32 | y = ~~(y * _sizeMulti);
33 | idx = (x * this.heightInTiles) + y;
34 | if (idx < 0 || idx >= this.numTiles) return false;
35 |
36 | px = x * tileSize;
37 | py = y * tileSize;
38 | tile = this.grid[idx];
39 | if (forceValue && tile === forceValue) return false;
40 | // console.log('[TileMap.setTile] '+x+', '+y+'; '+tile);
41 |
42 | if (tile > 0) {
43 | this.grid[idx] = 0;
44 | _ctx.clearRect(px, py, tileSize, tileSize);
45 |
46 | // kill the block
47 | block = _blockLookup[px+'-'+py];
48 | _blockCache.add(block);
49 | block.disable();
50 | delete _blockLookup[px+'-'+py];
51 |
52 | } else {
53 | this.grid[idx] = 1;
54 | _ctx.drawImage(tilesprite, px, py);
55 |
56 | // add a block to the grid
57 | if (!!_blockCache.length) {
58 | block = _blockCache.pop();
59 | block.position.x = px+25;
60 | block.position.y = py+25;
61 | } else {
62 | block = new Block(px+25, py+25);
63 | block.collider.setMass(0);
64 | }
65 |
66 | block.enable();
67 | _blockLookup[px+'-'+py] = block;
68 | }
69 |
70 | // console.log(this.toString());
71 | return true;
72 | };
73 |
74 | this.getTile = function(x, y) {
75 | var idx, tile;
76 |
77 | x = ~~(x * _sizeMulti);
78 | y = ~~(y * _sizeMulti);
79 | idx = (x * this.heightInTiles) + y;
80 | if (idx < 0 || idx >= this.numTiles) return null;
81 |
82 | return this.grid[idx];
83 | };
84 |
85 | this.clear = function() {
86 | for (var id in _blockLookup) {
87 | var str = id.split('-'),
88 | x = ~~(parseInt(str[0], 10) * _sizeMulti);
89 | y = ~~(parseInt(str[1], 10) * _sizeMulti);
90 | idx = (x * this.heightInTiles) + y;
91 |
92 | this.grid[idx] = 0;
93 | block = _blockLookup[id];
94 | _ctx.clearRect(block.position.x-25, block.position.y-25, tileSize, tileSize);
95 |
96 | _blockCache.add(block);
97 | block.disable();
98 | delete _blockLookup[id];
99 | }
100 | };
101 |
102 | this.toString = function() {
103 | var str = '', x = 0, y = 0,
104 | i, v;
105 |
106 | for (i = 0; i < this.numTiles; i++) {
107 | v = this.grid[~~((x * this.heightInTiles) + y)];
108 |
109 | if (v > 9 && v < 100) str += v + ',';
110 | else str += ' ' + v + ',';
111 |
112 | if (++x === this.widthInTiles) {
113 | x = 0;
114 | y++;
115 | str += '\n';
116 | }
117 | }
118 | str = str.substring(0, str.length-2); // get rid of the trailing comma because i'm ocd or something
119 | return str;
120 | };
121 |
122 | init();
123 | function init() {
124 | var canvas = document.getElementById('tilemap');
125 | canvas.width = window.innerWidth;
126 | canvas.height = window.innerHeight;
127 | _ctx = canvas.getContext('2d');
128 |
129 | for (var i = 0; i < _self.numTiles; i++) {
130 | _self.grid[i] = 0;
131 | }
132 |
133 | Kai.map = _self;
134 |
135 | console.log('[TileMap] '+_self.widthInTiles+'x'+_self.heightInTiles);
136 | // console.log(_self.toString());
137 | }
138 |
139 | } // class
140 |
141 | });
--------------------------------------------------------------------------------
/scripts/entities/Block.js:
--------------------------------------------------------------------------------
1 | define(['engine/Kai', 'components/BoundingCircle'],
2 | function(Kai, BoundingCircle) {
3 |
4 | return function Block(posx, posy) {
5 | if (typeof posx === 'undefined') posx = 0;
6 | if (typeof posy === 'undefined') posy = 0;
7 |
8 | // @private
9 | var _self = this;
10 |
11 | this.uniqueId = Date.now() + '' + Math.floor(Math.random()*1000);
12 |
13 | // base components
14 | this.position = new Vec2(posx, posy);
15 | this.velocity = new Vec2();
16 | this.collider = new BoundingCircle(this, {radius: 25});
17 |
18 | this.enable = function() {
19 | // Kai.grid.add(this);
20 | this.collider.solid = true;
21 | };
22 |
23 | this.disable = function() {
24 | // Kai.grid.remove(this);
25 | this.collider.solid = false;
26 | };
27 |
28 | init();
29 | function init() {
30 | Kai.grid.add(_self);
31 | }
32 |
33 | } // class
34 |
35 | });
--------------------------------------------------------------------------------
/scripts/entities/Thing.js:
--------------------------------------------------------------------------------
1 | define(['engine/Kai', 'components/BoundingCircle', 'components/VectorFieldState', 'components/behaviors/Flock'],
2 | function(Kai, BoundingCircle, FlowState, Flock) {
3 |
4 | return function Thing(posx, posy) {
5 | if (typeof posx === 'undefined') posx = 0;
6 | if (typeof posy === 'undefined') posy = 0;
7 |
8 | // @private
9 | var _self = this, _nearby = null, /*_tau = Math.PI * 2,*/
10 | _speed = 3,
11 | _angularSpeed = 2,
12 | _accel = new Vec2();
13 |
14 | this.uniqueId = Date.now() + '' + Math.floor(Math.random()*1000);
15 |
16 | // base components
17 | this.position = new Vec2(posx, posy);
18 | this.velocity = new Vec2(Math.random()*_speed-_speed*0.5, Math.random()*_speed-_speed*0.5);
19 | // this.rotation = new Vec2(Math.random()*_tau, Math.random()*_tau);
20 | this.sprite = null;
21 |
22 | // special components
23 | this.flock = new Flock(this, {maxSpeed: _speed});
24 | this.vecFieldState = new FlowState(this);
25 | this.collider = new BoundingCircle(this, {radius: 25});
26 |
27 | // internal
28 |
29 | // we should have a component system that updates each one for us, and then use signals for
30 | // actual gameplay logic. but until then...
31 | this.update = function() {
32 | var fieldForce = this.vecFieldState.update();
33 |
34 | if (this.vecFieldState.reachedGoal) {
35 | this.velocity.multiplyScalar(0.9);
36 | _accel.x = _accel.y = 0;
37 | } else {
38 | // VECTOR FIELD
39 | if (fieldForce) _accel.copy(fieldForce).normalize();
40 |
41 | // FLOCK
42 | if (Kai.guiOptions.flocking) {
43 | var flockForce = this.flock.update()/*.multiplyScalar(1.6)*/;
44 | _accel.add(flockForce);
45 | }
46 | }
47 |
48 | // _accel.truncate(_speed);
49 | this.velocity.add(_accel);
50 | // this.velocity.normalize().multiplyScalar(_speed);
51 | this.velocity.truncate(_speed);
52 |
53 | this.position.add(this.velocity);
54 | this.sprite.rotation = Math.atan2(this.velocity.y, this.velocity.x);
55 |
56 | // GRID TEST
57 | /*_nearby = Kai.grid.getNearby(this.position, 100);
58 | var node = _nearby.first;
59 | while (node) {
60 | var obj = node.obj;
61 | Vec2.draw(Kai.debugCtx, this.position, obj.position);
62 | node = node.next;
63 | }*/
64 |
65 | // screen wrap
66 | // if (this.position.x > window.innerWidth) this.position.x -= window.innerWidth;
67 | // if (this.position.x < 0) this.position.x += window.innerWidth;
68 | // if (this.position.y > window.innerHeight) this.position.y -= window.innerHeight;
69 | // if (this.position.y < 0) this.position.y += window.innerHeight;
70 | // bounds
71 | if (this.position.x > window.innerWidth) this.position.x = window.innerWidth;
72 | if (this.position.x < 0) this.position.x = 0;
73 | if (this.position.y > window.innerHeight) this.position.y = window.innerHeight;
74 | if (this.position.y < 0) this.position.y = 0;
75 | };
76 |
77 |
78 | init();
79 | function init() {
80 | var texture = PIXI.Texture.fromImage('img/beetle.png');
81 | _self.sprite = new PIXI.Sprite(texture);
82 | // center the _self sprite's anchor point (as opposed to pivot which is relative position to parent)
83 | _self.sprite.anchor.x = 0.5;
84 | _self.sprite.anchor.y = 0.5;
85 |
86 | Kai.stage.addChild(_self.sprite);
87 |
88 | Kai.grid.add(_self);
89 |
90 | // DEBUG
91 | _self.sprite.mousedown = function(data) {
92 | // TODO: check if this guy is in the grid and if not, what the fuck
93 | // console.log(_self.collider);
94 | // DebugDraw.circle(_self.position.x, _self.position.y, 25);
95 | };
96 |
97 | // link components
98 | _self.sprite.position = _self.position;
99 | }
100 |
101 | } // class
102 |
103 | });
--------------------------------------------------------------------------------
/scripts/lib/Signal.js:
--------------------------------------------------------------------------------
1 | var Signal = (function () {
2 | function Signal() {
3 | this._bindings = [];
4 | this._prevParams = null;
5 | this.memorize = false;
6 | this._shouldPropagate = true;
7 | this.active = true;
8 | }
9 | Signal.prototype.validateListener = function (listener, fnName) {
10 | if (typeof listener !== 'function') {
11 | throw new Error('listener is a required param of {fn}() and should be a Function.'.replace('{fn}', fnName));
12 | }
13 | };
14 |
15 | Signal.prototype._registerListener = function (listener, isOnce, listenerContext, priority) {
16 | var prevIndex = this._indexOfListener(listener, listenerContext);
17 | var binding;
18 |
19 | if (prevIndex !== -1) {
20 | binding = this._bindings[prevIndex];
21 |
22 | if (binding.isOnce() !== isOnce) {
23 | throw new Error('You cannot add' + (isOnce ? '' : 'Once') + '() then add' + (!isOnce ? '' : 'Once') + '() the same listener without removing the relationship first.');
24 | }
25 | } else {
26 | binding = new SignalBinding(this, listener, isOnce, listenerContext, priority);
27 |
28 | this._addBinding(binding);
29 | }
30 |
31 | if (this.memorize && this._prevParams) {
32 | binding.execute(this._prevParams);
33 | }
34 |
35 | return binding;
36 | };
37 |
38 | Signal.prototype._addBinding = function (binding) {
39 | var n = this._bindings.length;
40 |
41 | do {
42 | --n;
43 | } while(this._bindings[n] && binding.priority <= this._bindings[n].priority);
44 |
45 | this._bindings.splice(n + 1, 0, binding);
46 | };
47 |
48 | Signal.prototype._indexOfListener = function (listener, context) {
49 | var n = this._bindings.length;
50 | var cur;
51 |
52 | while (n--) {
53 | cur = this._bindings[n];
54 |
55 | if (cur.getListener() === listener && cur.context === context) {
56 | return n;
57 | }
58 | }
59 |
60 | return -1;
61 | };
62 |
63 | Signal.prototype.has = function (listener, context) {
64 | if (typeof context === "undefined") { context = null; }
65 | return this._indexOfListener(listener, context) !== -1;
66 | };
67 |
68 | Signal.prototype.add = function (listener, listenerContext, priority) {
69 | if (typeof listenerContext === "undefined") { listenerContext = null; }
70 | if (typeof priority === "undefined") { priority = 0; }
71 | this.validateListener(listener, 'add');
72 |
73 | return this._registerListener(listener, false, listenerContext, priority);
74 | };
75 |
76 | Signal.prototype.addOnce = function (listener, listenerContext, priority) {
77 | if (typeof listenerContext === "undefined") { listenerContext = null; }
78 | if (typeof priority === "undefined") { priority = 0; }
79 | this.validateListener(listener, 'addOnce');
80 |
81 | return this._registerListener(listener, true, listenerContext, priority);
82 | };
83 |
84 | Signal.prototype.remove = function (listener, context) {
85 | if (typeof context === "undefined") { context = null; }
86 | this.validateListener(listener, 'remove');
87 |
88 | var i = this._indexOfListener(listener, context);
89 |
90 | if (i !== -1) {
91 | this._bindings[i]._destroy();
92 | this._bindings.splice(i, 1);
93 | }
94 |
95 | return listener;
96 | };
97 |
98 | Signal.prototype.removeAll = function () {
99 | var n = this._bindings.length;
100 |
101 | while (n--) {
102 | this._bindings[n]._destroy();
103 | }
104 |
105 | this._bindings.length = 0;
106 | };
107 |
108 | Signal.prototype.getNumListeners = function () {
109 | return this._bindings.length;
110 | };
111 |
112 | Signal.prototype.halt = function () {
113 | this._shouldPropagate = false;
114 | };
115 |
116 | Signal.prototype.dispatch = function () {
117 | var paramsArr = [];
118 | for (var _i = 0; _i < (arguments.length - 0); _i++) {
119 | paramsArr[_i] = arguments[_i + 0];
120 | }
121 | if (!this.active) {
122 | return;
123 | }
124 |
125 | var n = this._bindings.length;
126 | var bindings;
127 |
128 | if (this.memorize) {
129 | this._prevParams = paramsArr;
130 | }
131 |
132 | if (!n) {
133 | return;
134 | }
135 |
136 | bindings = this._bindings.slice(0);
137 |
138 | this._shouldPropagate = true;
139 |
140 | do {
141 | n--;
142 | } while(bindings[n] && this._shouldPropagate && bindings[n].execute(paramsArr) !== false);
143 | };
144 |
145 | Signal.prototype.forget = function () {
146 | this._prevParams = null;
147 | };
148 |
149 | Signal.prototype.dispose = function () {
150 | this.removeAll();
151 |
152 | delete this._bindings;
153 | delete this._prevParams;
154 | };
155 |
156 | Signal.prototype.toString = function () {
157 | return '[Signal active:' + this.active + ' numListeners:' + this.getNumListeners() + ']';
158 | };
159 | Signal.VERSION = '1.0.0';
160 | return Signal;
161 | })();
162 | var SignalBinding = (function () {
163 | function SignalBinding(signal, listener, isOnce, listenerContext, priority) {
164 | if (typeof priority === "undefined") { priority = 0; }
165 | this.active = true;
166 | this.params = null;
167 | this._listener = listener;
168 | this._isOnce = isOnce;
169 | this.context = listenerContext;
170 | this._signal = signal;
171 | this.priority = priority || 0;
172 | }
173 | SignalBinding.prototype.execute = function (paramsArr) {
174 | var handlerReturn;
175 | var params;
176 |
177 | if (this.active && !!this._listener) {
178 | params = this.params ? this.params.concat(paramsArr) : paramsArr;
179 |
180 | handlerReturn = this._listener.apply(this.context, params);
181 |
182 | if (this._isOnce) {
183 | this.detach();
184 | }
185 | }
186 |
187 | return handlerReturn;
188 | };
189 |
190 | SignalBinding.prototype.detach = function () {
191 | return this.isBound() ? this._signal.remove(this._listener, this.context) : null;
192 | };
193 |
194 | SignalBinding.prototype.isBound = function () {
195 | return (!!this._signal && !!this._listener);
196 | };
197 |
198 | SignalBinding.prototype.isOnce = function () {
199 | return this._isOnce;
200 | };
201 |
202 | SignalBinding.prototype.getListener = function () {
203 | return this._listener;
204 | };
205 |
206 | SignalBinding.prototype.getSignal = function () {
207 | return this._signal;
208 | };
209 |
210 | SignalBinding.prototype._destroy = function () {
211 | delete this._signal;
212 | delete this._listener;
213 | delete this.context;
214 | };
215 |
216 | SignalBinding.prototype.toString = function () {
217 | return '[SignalBinding isOnce:' + this._isOnce + ', isBound:' + this.isBound() + ', active:' + this.active + ']';
218 | };
219 | return SignalBinding;
220 | })();
221 |
--------------------------------------------------------------------------------
/scripts/lib/dat.gui.min.js:
--------------------------------------------------------------------------------
1 | /**
2 | * dat-gui JavaScript Controller Library
3 | * http://code.google.com/p/dat-gui
4 | *
5 | * Copyright 2011 Data Arts Team, Google Creative Lab
6 | *
7 | * Licensed under the Apache License, Version 2.0 (the "License");
8 | * you may not use this file except in compliance with the License.
9 | * You may obtain a copy of the License at
10 | *
11 | * http://www.apache.org/licenses/LICENSE-2.0
12 | */
13 | var dat=dat||{};dat.gui=dat.gui||{};dat.utils=dat.utils||{};dat.controllers=dat.controllers||{};dat.dom=dat.dom||{};dat.color=dat.color||{};dat.utils.css=function(){return{load:function(e,a){var a=a||document,c=a.createElement("link");c.type="text/css";c.rel="stylesheet";c.href=e;a.getElementsByTagName("head")[0].appendChild(c)},inject:function(e,a){var a=a||document,c=document.createElement("style");c.type="text/css";c.innerHTML=e;a.getElementsByTagName("head")[0].appendChild(c)}}}();
14 | dat.utils.common=function(){var e=Array.prototype.forEach,a=Array.prototype.slice;return{BREAK:{},extend:function(c){this.each(a.call(arguments,1),function(a){for(var f in a)this.isUndefined(a[f])||(c[f]=a[f])},this);return c},defaults:function(c){this.each(a.call(arguments,1),function(a){for(var f in a)this.isUndefined(c[f])&&(c[f]=a[f])},this);return c},compose:function(){var c=a.call(arguments);return function(){for(var d=a.call(arguments),f=c.length-1;f>=0;f--)d=[c[f].apply(this,d)];return d[0]}},
15 | each:function(a,d,f){if(e&&a.forEach===e)a.forEach(d,f);else if(a.length===a.length+0)for(var b=0,n=a.length;b-1?d.length-d.indexOf(".")-1:0};c.superclass=e;a.extend(c.prototype,e.prototype,{setValue:function(a){if(this.__min!==void 0&&athis.__max)a=this.__max;this.__step!==void 0&&a%this.__step!=0&&(a=Math.round(a/this.__step)*this.__step);return c.superclass.prototype.setValue.call(this,a)},min:function(a){this.__min=a;return this},max:function(a){this.__max=a;return this},step:function(a){this.__step=a;return this}});return c}(dat.controllers.Controller,dat.utils.common);
29 | dat.controllers.NumberControllerBox=function(e,a,c){var d=function(f,b,e){function h(){var a=parseFloat(l.__input.value);c.isNaN(a)||l.setValue(a)}function j(a){var b=o-a.clientY;l.setValue(l.getValue()+b*l.__impliedStep);o=a.clientY}function m(){a.unbind(window,"mousemove",j);a.unbind(window,"mouseup",m)}this.__truncationSuspended=false;d.superclass.call(this,f,b,e);var l=this,o;this.__input=document.createElement("input");this.__input.setAttribute("type","text");a.bind(this.__input,"change",h);
30 | a.bind(this.__input,"blur",function(){h();l.__onFinishChange&&l.__onFinishChange.call(l,l.getValue())});a.bind(this.__input,"mousedown",function(b){a.bind(window,"mousemove",j);a.bind(window,"mouseup",m);o=b.clientY});a.bind(this.__input,"keydown",function(a){if(a.keyCode===13)l.__truncationSuspended=true,this.blur(),l.__truncationSuspended=false});this.updateDisplay();this.domElement.appendChild(this.__input)};d.superclass=e;c.extend(d.prototype,e.prototype,{updateDisplay:function(){var a=this.__input,
31 | b;if(this.__truncationSuspended)b=this.getValue();else{b=this.getValue();var c=Math.pow(10,this.__precision);b=Math.round(b*c)/c}a.value=b;return d.superclass.prototype.updateDisplay.call(this)}});return d}(dat.controllers.NumberController,dat.dom.dom,dat.utils.common);
32 | dat.controllers.NumberControllerSlider=function(e,a,c,d,f){var b=function(d,c,f,e,l){function o(b){b.preventDefault();var d=a.getOffset(g.__background),c=a.getWidth(g.__background);g.setValue(g.__min+(g.__max-g.__min)*((b.clientX-d.left)/(d.left+c-d.left)));return false}function y(){a.unbind(window,"mousemove",o);a.unbind(window,"mouseup",y);g.__onFinishChange&&g.__onFinishChange.call(g,g.getValue())}b.superclass.call(this,d,c,{min:f,max:e,step:l});var g=this;this.__background=document.createElement("div");
33 | this.__foreground=document.createElement("div");a.bind(this.__background,"mousedown",function(b){a.bind(window,"mousemove",o);a.bind(window,"mouseup",y);o(b)});a.addClass(this.__background,"slider");a.addClass(this.__foreground,"slider-fg");this.updateDisplay();this.__background.appendChild(this.__foreground);this.domElement.appendChild(this.__background)};b.superclass=e;b.useDefaultStyles=function(){c.inject(f)};d.extend(b.prototype,e.prototype,{updateDisplay:function(){this.__foreground.style.width=
34 | (this.getValue()-this.__min)/(this.__max-this.__min)*100+"%";return b.superclass.prototype.updateDisplay.call(this)}});return b}(dat.controllers.NumberController,dat.dom.dom,dat.utils.css,dat.utils.common,".slider {\n box-shadow: inset 0 2px 4px rgba(0,0,0,0.15);\n height: 1em;\n border-radius: 1em;\n background-color: #eee;\n padding: 0 0.5em;\n overflow: hidden;\n}\n\n.slider-fg {\n padding: 1px 0 2px 0;\n background-color: #aaa;\n height: 1em;\n margin-left: -0.5em;\n padding-right: 0.5em;\n border-radius: 1em 0 0 1em;\n}\n\n.slider-fg:after {\n display: inline-block;\n border-radius: 1em;\n background-color: #fff;\n border: 1px solid #aaa;\n content: '';\n float: right;\n margin-right: -1em;\n margin-top: -1px;\n height: 0.9em;\n width: 0.9em;\n}");
35 | dat.controllers.FunctionController=function(e,a,c){var d=function(c,b,e){d.superclass.call(this,c,b);var h=this;this.__button=document.createElement("div");this.__button.innerHTML=e===void 0?"Fire":e;a.bind(this.__button,"click",function(a){a.preventDefault();h.fire();return false});a.addClass(this.__button,"button");this.domElement.appendChild(this.__button)};d.superclass=e;c.extend(d.prototype,e.prototype,{fire:function(){this.__onChange&&this.__onChange.call(this);this.__onFinishChange&&this.__onFinishChange.call(this,
36 | this.getValue());this.getValue().call(this.object)}});return d}(dat.controllers.Controller,dat.dom.dom,dat.utils.common);
37 | dat.controllers.BooleanController=function(e,a,c){var d=function(c,b){d.superclass.call(this,c,b);var e=this;this.__prev=this.getValue();this.__checkbox=document.createElement("input");this.__checkbox.setAttribute("type","checkbox");a.bind(this.__checkbox,"change",function(){e.setValue(!e.__prev)},false);this.domElement.appendChild(this.__checkbox);this.updateDisplay()};d.superclass=e;c.extend(d.prototype,e.prototype,{setValue:function(a){a=d.superclass.prototype.setValue.call(this,a);this.__onFinishChange&&
38 | this.__onFinishChange.call(this,this.getValue());this.__prev=this.getValue();return a},updateDisplay:function(){this.getValue()===true?(this.__checkbox.setAttribute("checked","checked"),this.__checkbox.checked=true):this.__checkbox.checked=false;return d.superclass.prototype.updateDisplay.call(this)}});return d}(dat.controllers.Controller,dat.dom.dom,dat.utils.common);
39 | dat.color.toString=function(e){return function(a){if(a.a==1||e.isUndefined(a.a)){for(a=a.hex.toString(16);a.length<6;)a="0"+a;return"#"+a}else return"rgba("+Math.round(a.r)+","+Math.round(a.g)+","+Math.round(a.b)+","+a.a+")"}}(dat.utils.common);
40 | dat.color.interpret=function(e,a){var c,d,f=[{litmus:a.isString,conversions:{THREE_CHAR_HEX:{read:function(a){a=a.match(/^#([A-F0-9])([A-F0-9])([A-F0-9])$/i);return a===null?false:{space:"HEX",hex:parseInt("0x"+a[1].toString()+a[1].toString()+a[2].toString()+a[2].toString()+a[3].toString()+a[3].toString())}},write:e},SIX_CHAR_HEX:{read:function(a){a=a.match(/^#([A-F0-9]{6})$/i);return a===null?false:{space:"HEX",hex:parseInt("0x"+a[1].toString())}},write:e},CSS_RGB:{read:function(a){a=a.match(/^rgb\(\s*(.+)\s*,\s*(.+)\s*,\s*(.+)\s*\)/);
41 | return a===null?false:{space:"RGB",r:parseFloat(a[1]),g:parseFloat(a[2]),b:parseFloat(a[3])}},write:e},CSS_RGBA:{read:function(a){a=a.match(/^rgba\(\s*(.+)\s*,\s*(.+)\s*,\s*(.+)\s*\,\s*(.+)\s*\)/);return a===null?false:{space:"RGB",r:parseFloat(a[1]),g:parseFloat(a[2]),b:parseFloat(a[3]),a:parseFloat(a[4])}},write:e}}},{litmus:a.isNumber,conversions:{HEX:{read:function(a){return{space:"HEX",hex:a,conversionName:"HEX"}},write:function(a){return a.hex}}}},{litmus:a.isArray,conversions:{RGB_ARRAY:{read:function(a){return a.length!=
42 | 3?false:{space:"RGB",r:a[0],g:a[1],b:a[2]}},write:function(a){return[a.r,a.g,a.b]}},RGBA_ARRAY:{read:function(a){return a.length!=4?false:{space:"RGB",r:a[0],g:a[1],b:a[2],a:a[3]}},write:function(a){return[a.r,a.g,a.b,a.a]}}}},{litmus:a.isObject,conversions:{RGBA_OBJ:{read:function(b){return a.isNumber(b.r)&&a.isNumber(b.g)&&a.isNumber(b.b)&&a.isNumber(b.a)?{space:"RGB",r:b.r,g:b.g,b:b.b,a:b.a}:false},write:function(a){return{r:a.r,g:a.g,b:a.b,a:a.a}}},RGB_OBJ:{read:function(b){return a.isNumber(b.r)&&
43 | a.isNumber(b.g)&&a.isNumber(b.b)?{space:"RGB",r:b.r,g:b.g,b:b.b}:false},write:function(a){return{r:a.r,g:a.g,b:a.b}}},HSVA_OBJ:{read:function(b){return a.isNumber(b.h)&&a.isNumber(b.s)&&a.isNumber(b.v)&&a.isNumber(b.a)?{space:"HSV",h:b.h,s:b.s,v:b.v,a:b.a}:false},write:function(a){return{h:a.h,s:a.s,v:a.v,a:a.a}}},HSV_OBJ:{read:function(b){return a.isNumber(b.h)&&a.isNumber(b.s)&&a.isNumber(b.v)?{space:"HSV",h:b.h,s:b.s,v:b.v}:false},write:function(a){return{h:a.h,s:a.s,v:a.v}}}}}];return function(){d=
44 | false;var b=arguments.length>1?a.toArray(arguments):arguments[0];a.each(f,function(e){if(e.litmus(b))return a.each(e.conversions,function(e,f){c=e.read(b);if(d===false&&c!==false)return d=c,c.conversionName=f,c.conversion=e,a.BREAK}),a.BREAK});return d}}(dat.color.toString,dat.utils.common);
45 | dat.GUI=dat.gui.GUI=function(e,a,c,d,f,b,n,h,j,m,l,o,y,g,i){function q(a,b,r,c){if(b[r]===void 0)throw Error("Object "+b+' has no property "'+r+'"');c.color?b=new l(b,r):(b=[b,r].concat(c.factoryArgs),b=d.apply(a,b));if(c.before instanceof f)c.before=c.before.__li;t(a,b);g.addClass(b.domElement,"c");r=document.createElement("span");g.addClass(r,"property-name");r.innerHTML=b.property;var e=document.createElement("div");e.appendChild(r);e.appendChild(b.domElement);c=s(a,e,c.before);g.addClass(c,k.CLASS_CONTROLLER_ROW);
46 | g.addClass(c,typeof b.getValue());p(a,c,b);a.__controllers.push(b);return b}function s(a,b,d){var c=document.createElement("li");b&&c.appendChild(b);d?a.__ul.insertBefore(c,params.before):a.__ul.appendChild(c);a.onResize();return c}function p(a,d,c){c.__li=d;c.__gui=a;i.extend(c,{options:function(b){if(arguments.length>1)return c.remove(),q(a,c.object,c.property,{before:c.__li.nextElementSibling,factoryArgs:[i.toArray(arguments)]});if(i.isArray(b)||i.isObject(b))return c.remove(),q(a,c.object,c.property,
47 | {before:c.__li.nextElementSibling,factoryArgs:[b]})},name:function(a){c.__li.firstElementChild.firstElementChild.innerHTML=a;return c},listen:function(){c.__gui.listen(c);return c},remove:function(){c.__gui.remove(c);return c}});if(c instanceof j){var e=new h(c.object,c.property,{min:c.__min,max:c.__max,step:c.__step});i.each(["updateDisplay","onChange","onFinishChange"],function(a){var b=c[a],H=e[a];c[a]=e[a]=function(){var a=Array.prototype.slice.call(arguments);b.apply(c,a);return H.apply(e,a)}});
48 | g.addClass(d,"has-slider");c.domElement.insertBefore(e.domElement,c.domElement.firstElementChild)}else if(c instanceof h){var f=function(b){return i.isNumber(c.__min)&&i.isNumber(c.__max)?(c.remove(),q(a,c.object,c.property,{before:c.__li.nextElementSibling,factoryArgs:[c.__min,c.__max,c.__step]})):b};c.min=i.compose(f,c.min);c.max=i.compose(f,c.max)}else if(c instanceof b)g.bind(d,"click",function(){g.fakeEvent(c.__checkbox,"click")}),g.bind(c.__checkbox,"click",function(a){a.stopPropagation()});
49 | else if(c instanceof n)g.bind(d,"click",function(){g.fakeEvent(c.__button,"click")}),g.bind(d,"mouseover",function(){g.addClass(c.__button,"hover")}),g.bind(d,"mouseout",function(){g.removeClass(c.__button,"hover")});else if(c instanceof l)g.addClass(d,"color"),c.updateDisplay=i.compose(function(a){d.style.borderLeftColor=c.__color.toString();return a},c.updateDisplay),c.updateDisplay();c.setValue=i.compose(function(b){a.getRoot().__preset_select&&c.isModified()&&B(a.getRoot(),true);return b},c.setValue)}
50 | function t(a,b){var c=a.getRoot(),d=c.__rememberedObjects.indexOf(b.object);if(d!=-1){var e=c.__rememberedObjectIndecesToControllers[d];e===void 0&&(e={},c.__rememberedObjectIndecesToControllers[d]=e);e[b.property]=b;if(c.load&&c.load.remembered){c=c.load.remembered;if(c[a.preset])c=c[a.preset];else if(c[w])c=c[w];else return;if(c[d]&&c[d][b.property]!==void 0)d=c[d][b.property],b.initialValue=d,b.setValue(d)}}}function I(a){var b=a.__save_row=document.createElement("li");g.addClass(a.domElement,
51 | "has-save");a.__ul.insertBefore(b,a.__ul.firstChild);g.addClass(b,"save-row");var c=document.createElement("span");c.innerHTML=" ";g.addClass(c,"button gears");var d=document.createElement("span");d.innerHTML="Save";g.addClass(d,"button");g.addClass(d,"save");var e=document.createElement("span");e.innerHTML="New";g.addClass(e,"button");g.addClass(e,"save-as");var f=document.createElement("span");f.innerHTML="Revert";g.addClass(f,"button");g.addClass(f,"revert");var m=a.__preset_select=document.createElement("select");
52 | a.load&&a.load.remembered?i.each(a.load.remembered,function(b,c){C(a,c,c==a.preset)}):C(a,w,false);g.bind(m,"change",function(){for(var b=0;b0){a.preset=this.preset;if(!a.remembered)a.remembered={};a.remembered[this.preset]=z(this)}a.folders={};i.each(this.__folders,function(b,
69 | c){a.folders[c]=b.getSaveObject()});return a},save:function(){if(!this.load.remembered)this.load.remembered={};this.load.remembered[this.preset]=z(this);B(this,false)},saveAs:function(a){if(!this.load.remembered)this.load.remembered={},this.load.remembered[w]=z(this,true);this.load.remembered[a]=z(this);this.preset=a;C(this,a,true)},revert:function(a){i.each(this.__controllers,function(b){this.getRoot().load.remembered?t(a||this.getRoot(),b):b.setValue(b.initialValue)},this);i.each(this.__folders,
70 | function(a){a.revert(a)});a||B(this.getRoot(),false)},listen:function(a){var b=this.__listening.length==0;this.__listening.push(a);b&&E(this.__listening)}});return k}(dat.utils.css,'