From 517e9801fc676c5f9bb337f2e92b2f1e4dd25b48 Mon Sep 17 00:00:00 2001 From: Ian McGregor Date: Fri, 27 Jan 2017 10:32:24 +0000 Subject: [PATCH] :sparkles: Es6 + Rollup + Npm scripts + minor perf improvements --- .eslintrc | 114 ++++++++ .gitignore | 4 +- .jshintrc | 89 ------ .travis.yml | 2 +- bower.json | 22 -- dist/boid.js | 642 +++++++++++++++++++++++-------------------- dist/boid.js.map | 1 + dist/boid.min.js | 2 +- examples/js/flock.js | 117 ++++---- gulpfile.js | 111 -------- karma.conf.js | 130 +++------ package.json | 45 +-- rollup.config.js | 33 +++ src/boid.js | 335 +++++++++++----------- src/vec2.js | 220 ++++++++------- test/boid.spec.js | 7 +- 16 files changed, 905 insertions(+), 969 deletions(-) create mode 100644 .eslintrc delete mode 100644 .jshintrc delete mode 100644 bower.json create mode 100644 dist/boid.js.map delete mode 100644 gulpfile.js create mode 100644 rollup.config.js diff --git a/.eslintrc b/.eslintrc new file mode 100644 index 0000000..6559088 --- /dev/null +++ b/.eslintrc @@ -0,0 +1,114 @@ +{ + "parser": "babel-eslint", + "env": { + "browser": true, + "es6": true, + "mocha": true, + "node": true + }, + "globals": { + "expect": true, + "it": true + }, + "plugins": [], + "parserOptions": { + "ecmaVersion": 6, + "jsx": true, + "sourceType": "module" + }, + "rules": { + "array-bracket-spacing": [2, "never"], + "block-scoped-var": 0, + "block-spacing": 2, + "brace-style": [2, "1tbs"], + "callback-return": [2, ["cb", "callback", "next"]], + "camelcase": [2, {"properties": "never"}], + "comma-dangle": [2, "never"], + "comma-spacing": 2, + "comma-style": [2, "last"], + "consistent-return": 2, + "curly": [2, "all"], + "default-case": 2, + "dot-location": [2, "property"], + "dot-notation": [2, {"allowKeywords": true}], + "eol-last": 2, + "eqeqeq": 2, + "func-style": [2, "declaration"], + "guard-for-in": 2, + "indent": [2, 4, {"SwitchCase": 1}], + "key-spacing": [2, {"beforeColon": false, "afterColon": true}], + "keyword-spacing": 2, + "max-len": [1, 120], + "new-cap": 0, + "new-parens": 2, + "no-alert": 2, + "no-array-constructor": 2, + "no-caller": 2, + "no-confusing-arrow": 2, + "no-console": 0, + "no-const-assign": 2, + "no-constant-condition": 2, + "no-delete-var": 2, + "no-empty": 2, + "no-eval": 2, + "no-extend-native": 2, + "no-extra-bind": 2, + "no-extra-semi": 2, + "no-fallthrough": 2, + "no-floating-decimal": 2, + "no-implied-eval": 2, + "no-invalid-this": 2, + "no-iterator": 2, + "no-label-var": 2, + "no-labels": [2, {"allowLoop": true, "allowSwitch": true}], + "no-lone-blocks": 2, + "no-loop-func": 2, + "no-mixed-spaces-and-tabs": [2, false], + "no-multi-spaces": 2, + "no-multi-str": 2, + "no-native-reassign": 2, + "no-nested-ternary": 2, + "no-new-func": 2, + "no-new-object": 2, + "no-new-wrappers": 2, + "no-new": 2, + "no-octal-escape": 2, + "no-octal": 2, + "no-process-exit": 2, + "no-proto": 2, + "no-redeclare": 2, + "no-return-assign": 2, + "no-script-url": 2, + "no-sequences": 2, + "no-shadow-restricted-names": 2, + "no-shadow": 2, + "no-spaced-func": 2, + "no-trailing-spaces": 2, + "no-undef-init": 2, + "no-undef": 2, + "no-undefined": 2, + "no-underscore-dangle": 2, + "no-unused-expressions": 0, + "no-unused-vars": [2, {"vars": "all", "args": "after-used"}], + "no-use-before-define": 2, + "no-useless-concat": 2, + "no-var": 2, + "no-with": 2, + "object-curly-spacing": [2, "never"], + "prefer-const": 2, + "quotes": [2, "single"], + "radix": 2, + "require-jsdoc": 0, + "semi-spacing": [2, {"before": false, "after": true}], + "semi": 2, + "space-before-blocks": 2, + "space-before-function-paren": [0, "never"], + "space-infix-ops": 2, + "space-unary-ops": [2, {"words": true, "nonwords": false}], + "spaced-comment": [0, "always", {"exceptions": ["-"]}], + "strict": [1, "global"], + "valid-jsdoc": [0, {"prefer": { "return": "returns"}}], + "wrap-iife": 2, + "yoda": [2, "never"] + } +} diff --git a/.gitignore b/.gitignore index c61af37..fbc22d3 100644 --- a/.gitignore +++ b/.gitignore @@ -3,14 +3,14 @@ !/.gitattributes !/.gitignore !/.gitinclude -!/.jshintrc +!/.eslintrc !/.travis.yml ehthumbs.db Thumbs.db Desktop.ini user.properties - +npm-debug.log bower_components/ node_modules/ temp/ diff --git a/.jshintrc b/.jshintrc deleted file mode 100644 index 4cb357f..0000000 --- a/.jshintrc +++ /dev/null @@ -1,89 +0,0 @@ -{ - // JSHint Default Configuration File (as on JSHint website) - // See http://jshint.com/docs/ for more details - - "maxerr" : 50, // {int} Maximum error before stopping - - // Enforcing - "bitwise" : true, // true: Prohibit bitwise operators (&, |, ^, etc.) - "camelcase" : false, // true: Identifiers must be in camelCase - "curly" : true, // true: Require {} for every new block or scope - "eqeqeq" : true, // true: Require triple equals (===) for comparison - "forin" : true, // true: Require filtering for..in loops with obj.hasOwnProperty() - "freeze" : true, // true: prohibits overwriting prototypes of native objects such as Array, Date etc. - "immed" : false, // true: Require immediate invocations to be wrapped in parens e.g. `(function () { } ());` - "indent" : 4, // {int} Number of spaces to use for indentation - "latedef" : false, // true: Require variables/functions to be defined before being used - "newcap" : false, // true: Require capitalization of all constructor functions e.g. `new F()` - "noarg" : true, // true: Prohibit use of `arguments.caller` and `arguments.callee` - "noempty" : true, // true: Prohibit use of empty blocks - "nonbsp" : true, // true: Prohibit "non-breaking whitespace" characters. - "nonew" : false, // true: Prohibit use of constructors for side-effects (without assignment) - "plusplus" : false, // true: Prohibit use of `++` & `--` - "quotmark" : false, // Quotation mark consistency: - // false : do nothing (default) - // true : ensure whatever is used is consistent - // "single" : require single quotes - // "double" : require double quotes - "undef" : true, // true: Require all non-global variables to be declared (prevents global leaks) - "unused" : true, // true: Require all defined variables be used - "strict" : true, // true: Requires all functions run in ES5 Strict Mode - "maxparams" : false, // {int} Max number of formal params allowed per function - "maxdepth" : false, // {int} Max depth of nested blocks (within functions) - "maxstatements" : false, // {int} Max number statements per function - "maxcomplexity" : false, // {int} Max cyclomatic complexity per function - "maxlen" : false, // {int} Max number of characters per line - - // Relaxing - "asi" : false, // true: Tolerate Automatic Semicolon Insertion (no semicolons) - "boss" : false, // true: Tolerate assignments where comparisons would be expected - "debug" : false, // true: Allow debugger statements e.g. browser breakpoints. - "eqnull" : false, // true: Tolerate use of `== null` - "es5" : false, // true: Allow ES5 syntax (ex: getters and setters) - "esnext" : true, // true: Allow ES.next (ES6) syntax (ex: `const`) - "moz" : false, // true: Allow Mozilla specific syntax (extends and overrides esnext features) - // (ex: `for each`, multiple try/catch, function expression…) - "evil" : false, // true: Tolerate use of `eval` and `new Function()` - "expr" : true, // true: Tolerate `ExpressionStatement` as Programs - "funcscope" : false, // true: Tolerate defining variables inside control statements - "globalstrict" : false, // true: Allow global "use strict" (also enables 'strict') - "iterator" : false, // true: Tolerate using the `__iterator__` property - "lastsemic" : false, // true: Tolerate omitting a semicolon for the last statement of a 1-line block - "laxbreak" : false, // true: Tolerate possibly unsafe line breakings - "laxcomma" : false, // true: Tolerate comma-first style coding - "loopfunc" : false, // true: Tolerate functions being defined in loops - "multistr" : false, // true: Tolerate multi-line strings - "noyield" : false, // true: Tolerate generator functions with no yield statement in them. - "notypeof" : false, // true: Tolerate invalid typeof operator values - "proto" : false, // true: Tolerate using the `__proto__` property - "scripturl" : false, // true: Tolerate script-targeted URLs - "shadow" : false, // true: Allows re-define variables later in code e.g. `var x=1; x=2;` - "sub" : false, // true: Tolerate using `[]` notation when it can still be expressed in dot notation - "supernew" : false, // true: Tolerate `new function () { ... };` and `new Object;` - "validthis" : false, // true: Tolerate using this in a non-constructor function - - // Environments - "browser" : true, // Web Browser (window, document, etc) - "browserify" : true, // Browserify (node.js code in the browser) - "couch" : false, // CouchDB - "devel" : true, // Development/debugging (alert, confirm, etc) - "dojo" : false, // Dojo Toolkit - "jasmine" : false, // Jasmine - "jquery" : false, // jQuery - "mocha" : true, // Mocha - "mootools" : false, // MooTools - "node" : false, // Node.js - "nonstandard" : false, // Widely adopted globals (escape, unescape, etc) - "prototypejs" : false, // Prototype and Scriptaculous - "qunit" : false, // QUnit - "rhino" : false, // Rhino - "shelljs" : false, // ShellJS - "worker" : false, // Web Workers - "wsh" : false, // Windows Scripting Host - "yui" : false, // Yahoo User Interface - - // Custom Globals (additional predefined global variables) - "globals" : { - "expect": false - } -} diff --git a/.travis.yml b/.travis.yml index ec9b841..49c9266 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,5 +1,5 @@ language: node_js node_js: - - 0.12 + - 6 before_install: - 'npm install -g karma-cli' diff --git a/bower.json b/bower.json deleted file mode 100644 index e1e91f4..0000000 --- a/bower.json +++ /dev/null @@ -1,22 +0,0 @@ -{ - "name": "boid", - "version": "0.2.2", - "homepage": "https://github.com/ianmcgregor/boid", - "authors": [ - "Ian McGregor " - ], - "description": "Bird-like behaviours", - "main": "dist/boid.min.js", - "moduleType": [ - "node", - "amd", - "globals" - ], - "license": "MIT", - "ignore": [ - "**/.*", - "node_modules", - "bower_components", - "test" - ] -} diff --git a/dist/boid.js b/dist/boid.js index 0bb49a6..56346ed 100644 --- a/dist/boid.js +++ b/dist/boid.js @@ -1,7 +1,232 @@ -(function(f){if(typeof exports==="object"&&typeof module!=="undefined"){module.exports=f()}else if(typeof define==="function"&&define.amd){define([],f)}else{var g;if(typeof window!=="undefined"){g=window}else if(typeof global!=="undefined"){g=global}else if(typeof self!=="undefined"){g=self}else{g=this}g.Boid = f()}})(function(){var define,module,exports;return (function e(t,n,r){function s(o,u){if(!n[o]){if(!t[o]){var a=typeof require=="function"&&require;if(!u&&a)return a(o,!0);if(i)return i(o,!0);var f=new Error("Cannot find module '"+o+"'");throw f.code="MODULE_NOT_FOUND",f}var l=n[o]={exports:{}};t[o][0].call(l.exports,function(e){var n=t[o][1][e];return s(n?n:e)},l,l.exports,e,t,n,r)}return n[o].exports}var i=typeof require=="function"&&require;for(var o=0;o 0 && arguments[0] !== undefined ? arguments[0] : 0; + var y = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : 0; + classCallCheck(this, Vec2); + + this.x = x; + this.y = y; + } + + Vec2.prototype.add = function add(vec) { + this.x = this.x + vec.x; + this.y = this.y + vec.y; + return this; + }; + + Vec2.prototype.subtract = function subtract(vec) { + this.x = this.x - vec.x; + this.y = this.y - vec.y; + return this; + }; + + Vec2.prototype.normalize = function normalize() { + var lsq = this.lengthSquared; + if (lsq === 0) { + this.x = 1; + return this; + } + if (lsq === 1) { + return this; + } + var l = sqrt(lsq); + this.x /= l; + this.y /= l; + return this; + }; + + Vec2.prototype.isNormalized = function isNormalized() { + return this.lengthSquared === 1; + }; + + Vec2.prototype.truncate = function truncate(max) { + // if (this.length > max) { + if (this.lengthSquared > max * max) { + this.length = max; + } + return this; + }; + + Vec2.prototype.scaleBy = function scaleBy(mul) { + this.x *= mul; + this.y *= mul; + return this; + }; + + Vec2.prototype.divideBy = function divideBy(div) { + this.x /= div; + this.y /= div; + return this; + }; + + Vec2.prototype.equals = function equals(vec) { + return this.x === vec.x && this.y === vec.y; + }; + + Vec2.prototype.negate = function negate() { + this.x = -this.x; + this.y = -this.y; + return this; + }; + + Vec2.prototype.dotProduct = function dotProduct(vec) { + /* + If A and B are perpendicular (at 90 degrees to each other), the result + of the dot product will be zero, because cos(Θ) will be zero. + If the angle between A and B are less than 90 degrees, the dot product + will be positive (greater than zero), as cos(Θ) will be positive, and + the vector lengths are always positive values. + If the angle between A and B are greater than 90 degrees, the dot + product will be negative (less than zero), as cos(Θ) will be negative, + and the vector lengths are always positive values + */ + return this.x * vec.x + this.y * vec.y; + }; + + Vec2.prototype.crossProduct = function crossProduct(vec) { + /* + The sign tells us if vec to the left (-) or the right (+) of this vec + */ + return this.x * vec.y - this.y * vec.x; + }; + + Vec2.prototype.distanceSq = function distanceSq(vec) { + var dx = vec.x - this.x; + var dy = vec.y - this.y; + return dx * dx + dy * dy; + }; + + Vec2.prototype.distance = function distance(vec) { + return sqrt(this.distanceSq(vec)); + }; + + Vec2.prototype.clone = function clone() { + return Vec2.get(this.x, this.y); + }; + + Vec2.prototype.reset = function reset() { + this.x = 0; + this.y = 0; + return this; + }; + + Vec2.prototype.perpendicular = function perpendicular() { + return Vec2.get(-this.y, this.x); + }; + + Vec2.prototype.sign = function sign(vec) { + // Determines if a given vector is to the right or left of this vector. + // If to the left, returns -1. If to the right, +1. + var p = this.perpendicular(); + var s = p.dotProduct(vec) < 0 ? -1 : 1; + p.dispose(); + return s; + }; + + Vec2.prototype.set = function set$$1(angle, length) { + this.x = cos(angle) * length; + this.y = sin(angle) * length; + return this; + }; + + Vec2.prototype.dispose = function dispose() { + this.x = 0; + this.y = 0; + pool.push(this); + }; + + Vec2.get = function get$$1(x, y) { + var v = pool.length > 0 ? pool.pop() : new Vec2(); + v.x = x || 0; + v.y = y || 0; + return v; + }; + + Vec2.fill = function fill(n) { + while (pool.length < n) { + pool.push(new Vec2()); + } + }; + + Vec2.angleBetween = function angleBetween(a, b) { + if (!a.isNormalized()) { + a = a.clone().normalize(); + } + if (!b.isNormalized()) { + b = b.clone().normalize(); + } + return acos(a.dotProduct(b)); + }; + + createClass(Vec2, [{ + key: "lengthSquared", + get: function get$$1() { + return this.x * this.x + this.y * this.y; + } + }, { + key: "length", + get: function get$$1() { + return sqrt(this.lengthSquared); + }, + set: function set$$1(value) { + var a = this.angle; + this.x = cos(a) * value; + this.y = sin(a) * value; + } + }, { + key: "angle", + get: function get$$1() { + return atan2(this.y, this.x); + }, + set: function set$$1(value) { + var l = this.length; + this.x = cos(value) * l; + this.y = sin(value) * l; + } + }]); + return Vec2; +}(); -var Vec2 = require('./vec2.js'); +var PI_D2 = Math.PI / 2; var defaults = { bounds: { @@ -26,9 +251,26 @@ var defaults = { minDistance: 60 }; +function setDefaults(opts, defs) { + Object.keys(defs).forEach(function (key) { + if (typeof opts[key] === 'undefined') { + opts[key] = defs[key]; + } + }); +} + +function configure(options) { + options = options || {}; + options.bounds = options.bounds || {}; + setDefaults(options, defaults); + setDefaults(options.bounds, defaults.bounds); + return options; +} + function Boid(options) { options = configure(options); + var boid = null; var position = Vec2.get(); var velocity = Vec2.get(); var steeringForce = Vec2.get(); @@ -60,32 +302,16 @@ function Boid(options) { var minDistance = options.minDistance; var minDistanceSq = minDistance * minDistance; - var setBounds = function(width, height, x, y) { + function setBounds(width, height, x, y) { bounds.width = width; bounds.height = height; bounds.x = x || 0; bounds.y = y || 0; return boid; - }; - - var update = function() { - steeringForce.truncate(maxForce); - steeringForce.divideBy(mass); - velocity.add(steeringForce); - steeringForce.reset(); - velocity.truncate(maxSpeed); - position.add(velocity); - - if (edgeBehavior === Boid.EDGE_BOUNCE) { - bounce(); - } else if (edgeBehavior === Boid.EDGE_WRAP) { - wrap(); - } - return boid; - }; + } - var bounce = function() { + function bounce() { var maxX = bounds.x + bounds.width; if (position.x > maxX) { position.x = maxX; @@ -103,9 +329,9 @@ function Boid(options) { position.y = bounds.y; velocity.y *= -1; } - }; + } - var wrap = function() { + function wrap() { var maxX = bounds.x + bounds.width; if (position.x > maxX) { position.x = bounds.x; @@ -119,9 +345,9 @@ function Boid(options) { } else if (position.y < bounds.y) { position.y = maxY; } - }; + } - var seek = function(targetVec) { + function seek(targetVec) { var desiredVelocity = targetVec.clone().subtract(position); desiredVelocity.normalize(); desiredVelocity.scaleBy(maxSpeed); @@ -131,9 +357,9 @@ function Boid(options) { force.dispose(); return boid; - }; + } - var flee = function(targetVec) { + function flee(targetVec) { var desiredVelocity = targetVec.clone().subtract(position); desiredVelocity.normalize(); desiredVelocity.scaleBy(maxSpeed); @@ -143,10 +369,10 @@ function Boid(options) { force.dispose(); return boid; - }; + } // seek until within arriveThreshold - var arrive = function(targetVec) { + function arrive(targetVec) { var desiredVelocity = targetVec.clone().subtract(position); desiredVelocity.normalize(); @@ -162,10 +388,10 @@ function Boid(options) { force.dispose(); return boid; - }; + } // look at velocity of boid and try to predict where it's going - var pursue = function(targetBoid) { + function pursue(targetBoid) { var lookAheadTime = position.distanceSq(targetBoid.position) / maxSpeedSq; var scaledVelocity = targetBoid.velocity.clone().scaleBy(lookAheadTime); @@ -177,10 +403,10 @@ function Boid(options) { predictedTarget.dispose(); return boid; - }; + } // look at velocity of boid and try to predict where it's going - var evade = function(targetBoid) { + function evade(targetBoid) { var lookAheadTime = position.distanceSq(targetBoid.position) / maxSpeedSq; var scaledVelocity = targetBoid.velocity.clone().scaleBy(lookAheadTime); @@ -192,15 +418,16 @@ function Boid(options) { predictedTarget.dispose(); return boid; - }; + } // wander around, changing angle by a limited amount each tick - var wander = function() { + function wander() { var center = velocity.clone().normalize().scaleBy(wanderDistance); var offset = Vec2.get(); - offset.length = wanderRadius; - offset.angle = wanderAngle; + offset.set(wanderAngle, wanderRadius); + // offset.length = wanderRadius; + // offset.angle = wanderAngle; wanderAngle += Math.random() * wanderRange - wanderRange * 0.5; var force = center.add(offset); @@ -210,11 +437,11 @@ function Boid(options) { force.dispose(); return boid; - }; + } // gets a bit rough used in combination with seeking as the boid attempts // to seek straight through an object while simultaneously trying to avoid it - var avoid = function(obstacles) { + function avoid(obstacles) { for (var i = 0; i < obstacles.length; i++) { var obstacle = obstacles[i]; var heading = velocity.clone().normalize(); @@ -237,13 +464,14 @@ function Boid(options) { if (distance < (obstacle.radius || 0) + avoidBuffer && projection.length < feeler.length) { // calc a force +/- 90 deg from vec to circ var force = heading.clone().scaleBy(maxSpeed); - force.angle += difference.sign(velocity) * Math.PI / 2; + force.angle += difference.sign(velocity) * PI_D2; // scale force by distance (further = smaller force) - force.scaleBy(1 - projection.length / feeler.length); + var dist = projection.length / feeler.length; + force.scaleBy(1 - dist); // add to steering force steeringForce.add(force); // braking force - slows boid down so it has time to turn (closer = harder) - velocity.scaleBy(projection.length / feeler.length); + velocity.scaleBy(dist); force.dispose(); } @@ -255,10 +483,10 @@ function Boid(options) { difference.dispose(); } return boid; - }; + } // follow a path made up of an array or vectors - var followPath = function(path, loop) { + function followPath(path, loop) { loop = !!loop; var wayPoint = path[pathIndex]; @@ -281,10 +509,25 @@ function Boid(options) { seek(wayPoint); } return boid; - }; + } + + // is boid close enough to be in sight and facing + function inSight(b) { + if (position.distanceSq(b.position) > maxDistanceSq) { + return false; + } + var heading = velocity.clone().normalize(); + var difference = b.position.clone().subtract(position); + var dotProd = difference.dotProduct(heading); + + heading.dispose(); + difference.dispose(); + + return dotProd >= 0; + } // flock - group of boids loosely move together - var flock = function(boids) { + function flock(boids) { var averageVelocity = velocity.clone(); var averagePosition = Vec2.get(); var inSightCount = 0; @@ -293,7 +536,8 @@ function Boid(options) { if (b !== boid && inSight(b)) { averageVelocity.add(b.velocity); averagePosition.add(b.position); - if (tooClose(b)) { + + if (position.distanceSq(b.position) < minDistanceSq) { flee(b.position); } inSightCount++; @@ -309,33 +553,33 @@ function Boid(options) { averagePosition.dispose(); return boid; - }; + } - // is boid close enough to be in sight and facing - var inSight = function(boid) { - if (position.distanceSq(boid.position) > maxDistanceSq) { - return false; + function update() { + steeringForce.truncate(maxForce); + if (mass !== 1) { + steeringForce.divideBy(mass); } - var heading = velocity.clone().normalize(); - var difference = boid.position.clone().subtract(position); - var dotProd = difference.dotProduct(heading); - - heading.dispose(); - difference.dispose(); + // velocity.add(steeringForce); + velocity.x += steeringForce.x; + velocity.y += steeringForce.y; + // steeringForce.reset(); + steeringForce.x = 0; + steeringForce.y = 0; + velocity.truncate(maxSpeed); + // position.add(velocity); + position.x += velocity.x; + position.y += velocity.y; - if (dotProd < 0) { - return false; + if (edgeBehavior === Boid.EDGE_BOUNCE) { + bounce(); + } else if (edgeBehavior === Boid.EDGE_WRAP) { + wrap(); } - return true; - }; - - // is boid too close? - var tooClose = function(boid) { - return position.distanceSq(boid.position) < minDistanceSq; - }; + return boid; + } - // methods - var boid = { + boid = { bounds: bounds, setBounds: setBounds, update: update, @@ -356,123 +600,123 @@ function Boid(options) { // getters / setters Object.defineProperties(boid, { edgeBehavior: { - get: function() { + get: function get() { return edgeBehavior; }, - set: function(value) { + set: function set(value) { edgeBehavior = value; } }, mass: { - get: function() { + get: function get() { return mass; }, - set: function(value) { + set: function set(value) { mass = value; } }, maxSpeed: { - get: function() { + get: function get() { return maxSpeed; }, - set: function(value) { + set: function set(value) { maxSpeed = value; maxSpeedSq = value * value; } }, maxForce: { - get: function() { + get: function get() { return maxForce; }, - set: function(value) { + set: function set(value) { maxForce = value; } }, // arrive arriveThreshold: { - get: function() { + get: function get() { return arriveThreshold; }, - set: function(value) { + set: function set(value) { arriveThreshold = value; arriveThresholdSq = value * value; } }, // wander wanderDistance: { - get: function() { + get: function get() { return wanderDistance; }, - set: function(value) { + set: function set(value) { wanderDistance = value; } }, wanderRadius: { - get: function() { + get: function get() { return wanderRadius; }, - set: function(value) { + set: function set(value) { wanderRadius = value; } }, wanderRange: { - get: function() { + get: function get() { return wanderRange; }, - set: function(value) { + set: function set(value) { wanderRange = value; } }, // avoid avoidDistance: { - get: function() { + get: function get() { return avoidDistance; }, - set: function(value) { + set: function set(value) { avoidDistance = value; } }, avoidBuffer: { - get: function() { + get: function get() { return avoidBuffer; }, - set: function(value) { + set: function set(value) { avoidBuffer = value; } }, // followPath pathIndex: { - get: function() { + get: function get() { return pathIndex; }, - set: function(value) { + set: function set(value) { pathIndex = value; } }, pathThreshold: { - get: function() { + get: function get() { return pathThreshold; }, - set: function(value) { + set: function set(value) { pathThreshold = value; pathThresholdSq = value * value; } }, // flock maxDistance: { - get: function() { + get: function get() { return maxDistance; }, - set: function(value) { + set: function set(value) { maxDistance = value; maxDistanceSq = value * value; } }, minDistance: { - get: function() { + get: function get() { return minDistance; }, - set: function(value) { + set: function set(value) { minDistance = value; minDistanceSq = value * value; } @@ -490,211 +734,19 @@ Boid.EDGE_WRAP = 'wrap'; // vec2 Boid.Vec2 = Vec2; -Boid.vec2 = function(x, y) { +Boid.vec2 = function (x, y) { return Vec2.get(x, y); }; // for defining obstacles or areas to avoid -Boid.obstacle = function(radius, x, y) { +Boid.obstacle = function (radius, x, y) { return { radius: radius, position: Vec2.get(x, y) }; }; -function setDefaults(opts, defs) { - Object.keys(defs).forEach(function(key) { - if (typeof opts[key] === 'undefined') { - opts[key] = defs[key]; - } - }); -} - -function configure(options) { - options = options || {}; - options.bounds = options.bounds || {}; - setDefaults(options, defaults); - setDefaults(options.bounds, defaults.bounds); - return options; -} - -if (typeof module === 'object' && module.exports) { - module.exports = Boid; -} - -},{"./vec2.js":2}],2:[function(require,module,exports){ -'use strict'; - -function Vec2(x, y) { - this.x = x || 0; - this.y = y || 0; -} - -Vec2.prototype = { - add: function(vec) { - this.x = this.x + vec.x; - this.y = this.y + vec.y; - return this; - }, - subtract: function(vec) { - this.x = this.x - vec.x; - this.y = this.y - vec.y; - return this; - }, - normalize: function() { - var l = this.length; - if (l === 0) { - this.x = 1; - return this; - } - if (l === 1) { - return this; - } - this.x /= l; - this.y /= l; - return this; - }, - isNormalized: function() { - return this.length === 1; - }, - truncate: function(max) { - if (this.length > max) { - this.length = max; - } - return this; - }, - scaleBy: function(mul) { - this.x *= mul; - this.y *= mul; - return this; - }, - divideBy: function(div) { - this.x /= div; - this.y /= div; - return this; - }, - equals: function(vec) { - return this.x === vec.x && this.y === vec.y; - }, - negate: function() { - this.x = -this.x; - this.y = -this.y; - return this; - }, - dotProduct: function(vec) { - /* - If A and B are perpendicular (at 90 degrees to each other), the result - of the dot product will be zero, because cos(Θ) will be zero. - If the angle between A and B are less than 90 degrees, the dot product - will be positive (greater than zero), as cos(Θ) will be positive, and - the vector lengths are always positive values. - If the angle between A and B are greater than 90 degrees, the dot - product will be negative (less than zero), as cos(Θ) will be negative, - and the vector lengths are always positive values - */ - return this.x * vec.x + this.y * vec.y; - }, - crossProduct: function(vec) { - /* - The sign tells us if vec to the left (-) or the right (+) of this vec - */ - return this.x * vec.y - this.y * vec.x; - }, - distanceSq: function(vec) { - var dx = vec.x - this.x; - var dy = vec.y - this.y; - return dx * dx + dy * dy; - }, - distance: function(vec) { - return Math.sqrt(this.distanceSq(vec)); - }, - clone: function() { - return Vec2.get(this.x, this.y); - }, - reset: function() { - this.x = 0; - this.y = 0; - return this; - }, - perpendicular: function() { - return Vec2.get(-this.y, this.x); - }, - sign: function(vec) { - // Determines if a given vector is to the right or left of this vector. - // If to the left, returns -1. If to the right, +1. - var p = this.perpendicular(); - var s = p.dotProduct(vec) < 0 ? -1 : 1; - p.dispose(); - return s; - }, - set: function(x, y) { - this.x = x || 0; - this.y = y || 0; - return this; - }, - dispose: function() { - Vec2.pool.push(this.reset()); - } -}; - -// getters / setters - -Object.defineProperties(Vec2.prototype, { - lengthSquared: { - get: function() { - return this.x * this.x + this.y * this.y; - } - }, - length: { - get: function() { - return Math.sqrt(this.lengthSquared); - }, - set: function(value) { - var a = this.angle; - this.x = Math.cos(a) * value; - this.y = Math.sin(a) * value; - } - }, - angle: { - get: function() { - return Math.atan2(this.y, this.x); - }, - set: function(value) { - var l = this.length; - this.x = Math.cos(value) * l; - this.y = Math.sin(value) * l; - } - } -}); - -// static - -Vec2.pool = []; -Vec2.get = function(x, y) { - var v = Vec2.pool.length > 0 ? Vec2.pool.pop() : new Vec2(); - v.set(x, y); - return v; -}; - -Vec2.fill = function(n) { - while (Vec2.pool.length < n) { - Vec2.pool.push(new Vec2()); - } -}; - -Vec2.angleBetween = function(a, b) { - if (!a.isNormalized()) { - a = a.clone().normalize(); - } - if (!b.isNormalized()) { - b = b.clone().normalize(); - } - return Math.acos(a.dotProduct(b)); -}; - -if (typeof module === 'object' && module.exports) { - module.exports = Vec2; -} +return Boid; -},{}]},{},[1])(1) -}); \ No newline at end of file +}))); +//# sourceMappingURL=boid.js.map diff --git a/dist/boid.js.map b/dist/boid.js.map new file mode 100644 index 0000000..e0d912e --- /dev/null +++ b/dist/boid.js.map @@ -0,0 +1 @@ +{"version":3,"file":"boid.js","sources":["../src/vec2.js","../src/boid.js"],"sourcesContent":["const {acos, atan2, cos, sin, sqrt} = Math;\n\nconst pool = [];\n\nexport default class Vec2 {\n constructor(x = 0, y = 0) {\n this.x = x;\n this.y = y;\n }\n\n add(vec) {\n this.x = this.x + vec.x;\n this.y = this.y + vec.y;\n return this;\n }\n\n subtract(vec) {\n this.x = this.x - vec.x;\n this.y = this.y - vec.y;\n return this;\n }\n\n normalize() {\n const lsq = this.lengthSquared;\n if (lsq === 0) {\n this.x = 1;\n return this;\n }\n if (lsq === 1) {\n return this;\n }\n const l = sqrt(lsq);\n this.x /= l;\n this.y /= l;\n return this;\n }\n\n isNormalized() {\n return this.lengthSquared === 1;\n }\n\n truncate(max) {\n // if (this.length > max) {\n if (this.lengthSquared > max * max) {\n this.length = max;\n }\n return this;\n }\n\n scaleBy(mul) {\n this.x *= mul;\n this.y *= mul;\n return this;\n }\n\n divideBy(div) {\n this.x /= div;\n this.y /= div;\n return this;\n }\n\n equals(vec) {\n return this.x === vec.x && this.y === vec.y;\n }\n\n negate() {\n this.x = -this.x;\n this.y = -this.y;\n return this;\n }\n\n dotProduct(vec) {\n /*\n If A and B are perpendicular (at 90 degrees to each other), the result\n of the dot product will be zero, because cos(Θ) will be zero.\n If the angle between A and B are less than 90 degrees, the dot product\n will be positive (greater than zero), as cos(Θ) will be positive, and\n the vector lengths are always positive values.\n If the angle between A and B are greater than 90 degrees, the dot\n product will be negative (less than zero), as cos(Θ) will be negative,\n and the vector lengths are always positive values\n */\n return this.x * vec.x + this.y * vec.y;\n }\n\n crossProduct(vec) {\n /*\n The sign tells us if vec to the left (-) or the right (+) of this vec\n */\n return this.x * vec.y - this.y * vec.x;\n }\n\n distanceSq(vec) {\n const dx = vec.x - this.x;\n const dy = vec.y - this.y;\n return dx * dx + dy * dy;\n }\n\n distance(vec) {\n return sqrt(this.distanceSq(vec));\n }\n\n clone() {\n return Vec2.get(this.x, this.y);\n }\n\n reset() {\n this.x = 0;\n this.y = 0;\n return this;\n }\n\n perpendicular() {\n return Vec2.get(-this.y, this.x);\n }\n\n sign(vec) {\n // Determines if a given vector is to the right or left of this vector.\n // If to the left, returns -1. If to the right, +1.\n const p = this.perpendicular();\n const s = p.dotProduct(vec) < 0 ? -1 : 1;\n p.dispose();\n return s;\n }\n\n set(angle, length) {\n this.x = cos(angle) * length;\n this.y = sin(angle) * length;\n return this;\n }\n\n dispose() {\n this.x = 0;\n this.y = 0;\n pool.push(this);\n }\n\n get lengthSquared() {\n return this.x * this.x + this.y * this.y;\n }\n\n get length() {\n return sqrt(this.lengthSquared);\n }\n\n set length(value) {\n const a = this.angle;\n this.x = cos(a) * value;\n this.y = sin(a) * value;\n }\n\n get angle() {\n return atan2(this.y, this.x);\n }\n\n set angle(value) {\n const l = this.length;\n this.x = cos(value) * l;\n this.y = sin(value) * l;\n }\n\n static get(x, y) {\n const v = pool.length > 0 ? pool.pop() : new Vec2();\n v.x = x || 0;\n v.y = y || 0;\n return v;\n }\n\n static fill(n) {\n while (pool.length < n) {\n pool.push(new Vec2());\n }\n }\n\n static angleBetween(a, b) {\n if (!a.isNormalized()) {\n a = a.clone().normalize();\n }\n if (!b.isNormalized()) {\n b = b.clone().normalize();\n }\n return acos(a.dotProduct(b));\n }\n}\n","import Vec2 from './vec2.js';\n\nconst PI_D2 = Math.PI / 2;\n\nconst defaults = {\n bounds: {\n x: 0,\n y: 0,\n width: 640,\n height: 480\n },\n edgeBehavior: 'bounce',\n mass: 1.0,\n maxSpeed: 10,\n maxForce: 1,\n arriveThreshold: 50,\n wanderDistance: 10,\n wanderRadius: 5,\n wanderAngle: 0,\n wanderRange: 1,\n avoidDistance: 300,\n avoidBuffer: 20,\n pathThreshold: 20,\n maxDistance: 300,\n minDistance: 60\n};\n\nfunction setDefaults(opts, defs) {\n Object.keys(defs).forEach((key) => {\n if (typeof opts[key] === 'undefined') {\n opts[key] = defs[key];\n }\n });\n}\n\nfunction configure(options) {\n options = options || {};\n options.bounds = options.bounds || {};\n setDefaults(options, defaults);\n setDefaults(options.bounds, defaults.bounds);\n return options;\n}\n\nexport default function Boid(options) {\n options = configure(options);\n\n let boid = null;\n const position = Vec2.get();\n const velocity = Vec2.get();\n const steeringForce = Vec2.get();\n\n const bounds = options.bounds;\n let edgeBehavior = options.edgeBehavior;\n let mass = options.mass;\n let maxSpeed = options.maxSpeed;\n let maxSpeedSq = maxSpeed * maxSpeed;\n let maxForce = options.maxForce;\n // arrive\n let arriveThreshold = options.arriveThreshold;\n let arriveThresholdSq = arriveThreshold * arriveThreshold;\n // wander\n let wanderDistance = options.wanderDistance;\n let wanderRadius = options.wanderRadius;\n let wanderAngle = options.wanderAngle;\n let wanderRange = options.wanderRange;\n // avoid\n let avoidDistance = options.avoidDistance;\n let avoidBuffer = options.avoidBuffer;\n // follow path\n let pathIndex = 0;\n let pathThreshold = options.pathThreshold;\n let pathThresholdSq = pathThreshold * pathThreshold;\n // flock\n let maxDistance = options.maxDistance;\n let maxDistanceSq = maxDistance * maxDistance;\n let minDistance = options.minDistance;\n let minDistanceSq = minDistance * minDistance;\n\n function setBounds(width, height, x, y) {\n bounds.width = width;\n bounds.height = height;\n bounds.x = x || 0;\n bounds.y = y || 0;\n\n return boid;\n }\n\n function bounce() {\n const maxX = bounds.x + bounds.width;\n if (position.x > maxX) {\n position.x = maxX;\n velocity.x *= -1;\n } else if (position.x < bounds.x) {\n position.x = bounds.x;\n velocity.x *= -1;\n }\n\n const maxY = bounds.y + bounds.height;\n if (position.y > maxY) {\n position.y = maxY;\n velocity.y *= -1;\n } else if (position.y < bounds.y) {\n position.y = bounds.y;\n velocity.y *= -1;\n }\n }\n\n function wrap() {\n const maxX = bounds.x + bounds.width;\n if (position.x > maxX) {\n position.x = bounds.x;\n } else if (position.x < bounds.x) {\n position.x = maxX;\n }\n\n const maxY = bounds.y + bounds.height;\n if (position.y > maxY) {\n position.y = bounds.y;\n } else if (position.y < bounds.y) {\n position.y = maxY;\n }\n }\n\n function seek(targetVec) {\n const desiredVelocity = targetVec.clone().subtract(position);\n desiredVelocity.normalize();\n desiredVelocity.scaleBy(maxSpeed);\n\n const force = desiredVelocity.subtract(velocity);\n steeringForce.add(force);\n force.dispose();\n\n return boid;\n }\n\n function flee(targetVec) {\n const desiredVelocity = targetVec.clone().subtract(position);\n desiredVelocity.normalize();\n desiredVelocity.scaleBy(maxSpeed);\n\n const force = desiredVelocity.subtract(velocity);\n steeringForce.subtract(force);\n force.dispose();\n\n return boid;\n }\n\n // seek until within arriveThreshold\n function arrive(targetVec) {\n const desiredVelocity = targetVec.clone().subtract(position);\n desiredVelocity.normalize();\n\n const distanceSq = position.distanceSq(targetVec);\n if (distanceSq > arriveThresholdSq) {\n desiredVelocity.scaleBy(maxSpeed);\n } else {\n const scalar = maxSpeed * distanceSq / arriveThresholdSq;\n desiredVelocity.scaleBy(scalar);\n }\n const force = desiredVelocity.subtract(velocity);\n steeringForce.add(force);\n force.dispose();\n\n return boid;\n }\n\n // look at velocity of boid and try to predict where it's going\n function pursue(targetBoid) {\n const lookAheadTime = position.distanceSq(targetBoid.position) / maxSpeedSq;\n\n const scaledVelocity = targetBoid.velocity.clone().scaleBy(lookAheadTime);\n const predictedTarget = targetBoid.position.clone().add(scaledVelocity);\n\n seek(predictedTarget);\n\n scaledVelocity.dispose();\n predictedTarget.dispose();\n\n return boid;\n }\n\n // look at velocity of boid and try to predict where it's going\n function evade(targetBoid) {\n const lookAheadTime = position.distanceSq(targetBoid.position) / maxSpeedSq;\n\n const scaledVelocity = targetBoid.velocity.clone().scaleBy(lookAheadTime);\n const predictedTarget = targetBoid.position.clone().add(scaledVelocity);\n\n flee(predictedTarget);\n\n scaledVelocity.dispose();\n predictedTarget.dispose();\n\n return boid;\n }\n\n // wander around, changing angle by a limited amount each tick\n function wander() {\n const center = velocity.clone().normalize().scaleBy(wanderDistance);\n\n const offset = Vec2.get();\n offset.set(wanderAngle, wanderRadius);\n // offset.length = wanderRadius;\n // offset.angle = wanderAngle;\n wanderAngle += Math.random() * wanderRange - wanderRange * 0.5;\n\n const force = center.add(offset);\n steeringForce.add(force);\n\n offset.dispose();\n force.dispose();\n\n return boid;\n }\n\n // gets a bit rough used in combination with seeking as the boid attempts\n // to seek straight through an object while simultaneously trying to avoid it\n function avoid(obstacles) {\n for (let i = 0; i < obstacles.length; i++) {\n const obstacle = obstacles[i];\n const heading = velocity.clone().normalize();\n\n // vec between obstacle and boid\n const difference = obstacle.position.clone().subtract(position);\n const dotProd = difference.dotProduct(heading);\n\n // if obstacle in front of boid\n if (dotProd > 0) {\n // vec to represent 'feeler' arm\n const feeler = heading.clone().scaleBy(avoidDistance);\n // project difference onto feeler\n const projection = heading.clone().scaleBy(dotProd);\n // distance from obstacle to feeler\n const vecDistance = projection.subtract(difference);\n const distance = vecDistance.length;\n // if feeler intersects obstacle (plus buffer), and projection\n // less than feeler length, will collide\n if (distance < (obstacle.radius || 0) + avoidBuffer && projection.length < feeler.length) {\n // calc a force +/- 90 deg from vec to circ\n const force = heading.clone().scaleBy(maxSpeed);\n force.angle += difference.sign(velocity) * PI_D2;\n // scale force by distance (further = smaller force)\n const dist = projection.length / feeler.length;\n force.scaleBy(1 - dist);\n // add to steering force\n steeringForce.add(force);\n // braking force - slows boid down so it has time to turn (closer = harder)\n velocity.scaleBy(dist);\n\n force.dispose();\n }\n feeler.dispose();\n projection.dispose();\n vecDistance.dispose();\n }\n heading.dispose();\n difference.dispose();\n }\n return boid;\n }\n\n // follow a path made up of an array or vectors\n function followPath(path, loop) {\n loop = !!loop;\n\n const wayPoint = path[pathIndex];\n if (!wayPoint) {\n pathIndex = 0;\n return boid;\n }\n if (position.distanceSq(wayPoint) < pathThresholdSq) {\n if (pathIndex >= path.length - 1) {\n if (loop) {\n pathIndex = 0;\n }\n } else {\n pathIndex++;\n }\n }\n if (pathIndex >= path.length - 1 && !loop) {\n arrive(wayPoint);\n } else {\n seek(wayPoint);\n }\n return boid;\n }\n\n // is boid close enough to be in sight and facing\n function inSight(b) {\n if (position.distanceSq(b.position) > maxDistanceSq) {\n return false;\n }\n const heading = velocity.clone().normalize();\n const difference = b.position.clone().subtract(position);\n const dotProd = difference.dotProduct(heading);\n\n heading.dispose();\n difference.dispose();\n\n return dotProd >= 0;\n }\n\n // flock - group of boids loosely move together\n function flock(boids) {\n const averageVelocity = velocity.clone();\n const averagePosition = Vec2.get();\n let inSightCount = 0;\n for (let i = 0; i < boids.length; i++) {\n const b = boids[i];\n if (b !== boid && inSight(b)) {\n averageVelocity.add(b.velocity);\n averagePosition.add(b.position);\n\n if (position.distanceSq(b.position) < minDistanceSq) {\n flee(b.position);\n }\n inSightCount++;\n }\n }\n if (inSightCount > 0) {\n averageVelocity.divideBy(inSightCount);\n averagePosition.divideBy(inSightCount);\n seek(averagePosition);\n steeringForce.add(averageVelocity.subtract(velocity));\n }\n averageVelocity.dispose();\n averagePosition.dispose();\n\n return boid;\n }\n\n function update() {\n steeringForce.truncate(maxForce);\n if (mass !== 1) {\n steeringForce.divideBy(mass);\n }\n // velocity.add(steeringForce);\n velocity.x += steeringForce.x;\n velocity.y += steeringForce.y;\n // steeringForce.reset();\n steeringForce.x = 0;\n steeringForce.y = 0;\n velocity.truncate(maxSpeed);\n // position.add(velocity);\n position.x += velocity.x;\n position.y += velocity.y;\n\n if (edgeBehavior === Boid.EDGE_BOUNCE) {\n bounce();\n } else if (edgeBehavior === Boid.EDGE_WRAP) {\n wrap();\n }\n return boid;\n }\n\n boid = {\n bounds,\n setBounds,\n update,\n pursue,\n evade,\n wander,\n avoid,\n followPath,\n flock,\n arrive,\n seek,\n flee,\n position,\n velocity,\n userData: {}\n };\n\n // getters / setters\n Object.defineProperties(boid, {\n edgeBehavior: {\n get: function() {\n return edgeBehavior;\n },\n set: function(value) {\n edgeBehavior = value;\n }\n },\n mass: {\n get: function() {\n return mass;\n },\n set: function(value) {\n mass = value;\n }\n },\n maxSpeed: {\n get: function() {\n return maxSpeed;\n },\n set: function(value) {\n maxSpeed = value;\n maxSpeedSq = value * value;\n }\n },\n maxForce: {\n get: function() {\n return maxForce;\n },\n set: function(value) {\n maxForce = value;\n }\n },\n // arrive\n arriveThreshold: {\n get: function() {\n return arriveThreshold;\n },\n set: function(value) {\n arriveThreshold = value;\n arriveThresholdSq = value * value;\n }\n },\n // wander\n wanderDistance: {\n get: function() {\n return wanderDistance;\n },\n set: function(value) {\n wanderDistance = value;\n }\n },\n wanderRadius: {\n get: function() {\n return wanderRadius;\n },\n set: function(value) {\n wanderRadius = value;\n }\n },\n wanderRange: {\n get: function() {\n return wanderRange;\n },\n set: function(value) {\n wanderRange = value;\n }\n },\n // avoid\n avoidDistance: {\n get: function() {\n return avoidDistance;\n },\n set: function(value) {\n avoidDistance = value;\n }\n },\n avoidBuffer: {\n get: function() {\n return avoidBuffer;\n },\n set: function(value) {\n avoidBuffer = value;\n }\n },\n // followPath\n pathIndex: {\n get: function() {\n return pathIndex;\n },\n set: function(value) {\n pathIndex = value;\n }\n },\n pathThreshold: {\n get: function() {\n return pathThreshold;\n },\n set: function(value) {\n pathThreshold = value;\n pathThresholdSq = value * value;\n }\n },\n // flock\n maxDistance: {\n get: function() {\n return maxDistance;\n },\n set: function(value) {\n maxDistance = value;\n maxDistanceSq = value * value;\n }\n },\n minDistance: {\n get: function() {\n return minDistance;\n },\n set: function(value) {\n minDistance = value;\n minDistanceSq = value * value;\n }\n }\n });\n\n return Object.freeze(boid);\n}\n\n// edge behaviors\nBoid.EDGE_NONE = 'none';\nBoid.EDGE_BOUNCE = 'bounce';\nBoid.EDGE_WRAP = 'wrap';\n\n// vec2\nBoid.Vec2 = Vec2;\n\nBoid.vec2 = function(x, y) {\n return Vec2.get(x, y);\n};\n\n// for defining obstacles or areas to avoid\nBoid.obstacle = function(radius, x, y) {\n return {\n radius: radius,\n position: Vec2.get(x, y)\n };\n};\n"],"names":["acos","Math","atan2","cos","sin","sqrt","pool","Vec2","x","y","add","vec","subtract","normalize","lsq","lengthSquared","l","isNormalized","truncate","max","length","scaleBy","mul","divideBy","div","equals","negate","dotProduct","crossProduct","distanceSq","dx","dy","distance","clone","get","reset","perpendicular","sign","p","s","dispose","set","angle","push","v","pop","fill","n","angleBetween","a","b","value","PI_D2","PI","defaults","setDefaults","opts","defs","keys","forEach","key","configure","options","bounds","Boid","boid","position","velocity","steeringForce","edgeBehavior","mass","maxSpeed","maxSpeedSq","maxForce","arriveThreshold","arriveThresholdSq","wanderDistance","wanderRadius","wanderAngle","wanderRange","avoidDistance","avoidBuffer","pathIndex","pathThreshold","pathThresholdSq","maxDistance","maxDistanceSq","minDistance","minDistanceSq","setBounds","width","height","bounce","maxX","maxY","wrap","seek","targetVec","desiredVelocity","force","flee","arrive","scalar","pursue","targetBoid","lookAheadTime","scaledVelocity","predictedTarget","evade","wander","center","offset","random","avoid","obstacles","i","obstacle","heading","difference","dotProd","feeler","projection","vecDistance","radius","dist","followPath","path","loop","wayPoint","inSight","flock","boids","averageVelocity","averagePosition","inSightCount","update","EDGE_BOUNCE","EDGE_WRAP","defineProperties","Object","freeze","EDGE_NONE","vec2"],"mappings":"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"} \ No newline at end of file diff --git a/dist/boid.min.js b/dist/boid.min.js index 1534ac4..c371f79 100644 --- a/dist/boid.min.js +++ b/dist/boid.min.js @@ -1 +1 @@ -!function(t){if("object"==typeof exports&&"undefined"!=typeof module)module.exports=t();else if("function"==typeof define&&define.amd)define([],t);else{var e;e="undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof self?self:this,e.Boid=t()}}(function(){return function t(e,n,i){function r(s,u){if(!n[s]){if(!e[s]){var a="function"==typeof require&&require;if(!u&&a)return a(s,!0);if(o)return o(s,!0);var c=new Error("Cannot find module '"+s+"'");throw c.code="MODULE_NOT_FOUND",c}var d=n[s]={exports:{}};e[s][0].call(d.exports,function(t){var n=e[s][1][t];return r(n?n:t)},d,d.exports,t,e,n,i)}return n[s].exports}for(var o="function"==typeof require&&require,s=0;st?(e.x=t,n.x*=-1):e.xi?(e.y=i,n.y*=-1):e.yt?e.x=u.x:e.xn?e.y=u.y:e.yp)i.scaleBy(d);else{var s=d*o/p;i.scaleBy(s)}var u=i.subtract(n);return r.add(u),u.dispose(),W},_=function(t){var n=e.distanceSq(t.position)/h,i=t.velocity.clone().scaleBy(n),r=t.position.clone().add(i);return N(r),i.dispose(),r.dispose(),W},G=function(t){var n=e.distanceSq(t.position)/h,i=t.velocity.clone().scaleBy(n),r=t.position.clone().add(i);return R(r),i.dispose(),r.dispose(),W},A=function(){var t=n.clone().normalize().scaleBy(y),e=s.get();e.length=g,e.angle=x,x+=Math.random()*v-.5*v;var i=t.add(e);return r.add(i),e.dispose(),i.dispose(),W},F=function(t){for(var i=0;i0){var c=s.clone().scaleBy(m),h=s.clone().scaleBy(a),f=h.subtract(u),l=f.length;if(l<(o.radius||0)+b&&h.length=t.length-1?n&&(B=0):B++),B>=t.length-1&&!n?T(i):N(i),W):(B=0,W)},k=function(t){for(var e=n.clone(),i=s.get(),o=0,u=0;u0&&(e.divideBy(o),i.divideBy(o),N(i),r.add(e.subtract(n))),e.dispose(),i.dispose(),W},C=function(t){if(e.distanceSq(t.position)>E)return!1;var i=n.clone().normalize(),r=t.position.clone().subtract(e),o=r.dotProduct(i);return i.dispose(),r.dispose(),0>o?!1:!0},I=function(t){return e.distanceSq(t.position)t&&(this.length=t),this},scaleBy:function(t){return this.x*=t,this.y*=t,this},divideBy:function(t){return this.x/=t,this.y/=t,this},equals:function(t){return this.x===t.x&&this.y===t.y},negate:function(){return this.x=-this.x,this.y=-this.y,this},dotProduct:function(t){return this.x*t.x+this.y*t.y},crossProduct:function(t){return this.x*t.y-this.y*t.x},distanceSq:function(t){var e=t.x-this.x,n=t.y-this.y;return e*e+n*n},distance:function(t){return Math.sqrt(this.distanceSq(t))},clone:function(){return i.get(this.x,this.y)},reset:function(){return this.x=0,this.y=0,this},perpendicular:function(){return i.get(-this.y,this.x)},sign:function(t){var e=this.perpendicular(),n=e.dotProduct(t)<0?-1:1;return e.dispose(),n},set:function(t,e){return this.x=t||0,this.y=e||0,this},dispose:function(){i.pool.push(this.reset())}},Object.defineProperties(i.prototype,{lengthSquared:{get:function(){return this.x*this.x+this.y*this.y}},length:{get:function(){return Math.sqrt(this.lengthSquared)},set:function(t){var e=this.angle;this.x=Math.cos(e)*t,this.y=Math.sin(e)*t}},angle:{get:function(){return Math.atan2(this.y,this.x)},set:function(t){var e=this.length;this.x=Math.cos(t)*e,this.y=Math.sin(t)*e}}}),i.pool=[],i.get=function(t,e){var n=i.pool.length>0?i.pool.pop():new i;return n.set(t,e),n},i.fill=function(t){for(;i.pool.lengtht?(b.x=t,B.x*=-1):b.xe?(b.y=e,B.y*=-1):b.yt?b.x=D.x:b.xe?b.y=D.y:b.yk)e.scaleBy(E);else{var i=E*n/k;e.scaleBy(i)}var r=e.subtract(B);return w.add(r),r.dispose(),m}function c(t){var e=b.distanceSq(t.position)/z,n=t.velocity.clone().scaleBy(e),i=t.position.clone().add(n);return s(i),n.dispose(),i.dispose(),m}function d(t){var e=b.distanceSq(t.position)/z,n=t.velocity.clone().scaleBy(e),i=t.position.clone().add(n);return u(i),n.dispose(),i.dispose(),m}function y(){var t=B.clone().normalize().scaleBy(M),e=h.get();e.set(O,N),O+=Math.random()*T-.5*T;var n=t.add(e);return w.add(n),e.dispose(),n.dispose(),m}function l(t){for(var e=0;e0){var s=i.clone().scaleBy(j),u=i.clone().scaleBy(o),a=u.subtract(r),c=a.length;if(c<(n.radius||0)+G&&u.length=t.length-1?e&&(_=0):_++),_>=t.length-1&&!e?a(n):s(n),m):(_=0,m)}function g(t){if(b.distanceSq(t.position)>I)return!1;var e=B.clone().normalize(),n=t.position.clone().subtract(b),i=n.dotProduct(e);return e.dispose(),n.dispose(),i>=0}function x(t){for(var e=B.clone(),n=h.get(),i=0,r=0;r0&&(e.divideBy(i),n.divideBy(i),s(n),w.add(e.subtract(B))),e.dispose(),n.dispose(),m}function v(){return w.truncate(P),1!==q&&w.divideBy(q),B.x+=w.x,B.y+=w.y,w.x=0,w.y=0,B.truncate(E),b.x+=B.x,b.y+=B.y,S===n.EDGE_BOUNCE?r():S===n.EDGE_WRAP&&o(),m}t=e(t);var m=null,b=h.get(),B=h.get(),w=h.get(),D=t.bounds,S=t.edgeBehavior,q=t.mass,E=t.maxSpeed,z=E*E,P=t.maxForce,R=t.arriveThreshold,k=R*R,M=t.wanderDistance,N=t.wanderRadius,O=t.wanderAngle,T=t.wanderRange,j=t.avoidDistance,G=t.avoidBuffer,_=0,A=t.pathThreshold,C=A*A,F=t.maxDistance,I=F*F,U=t.minDistance,W=U*U;return m={bounds:D,setBounds:i,update:v,pursue:c,evade:d,wander:y,avoid:l,followPath:p,flock:x,arrive:a,seek:s,flee:u,position:b,velocity:B,userData:{}},Object.defineProperties(m,{edgeBehavior:{get:function(){return S},set:function(t){S=t}},mass:{get:function(){return q},set:function(t){q=t}},maxSpeed:{get:function(){return E},set:function(t){E=t,z=t*t}},maxForce:{get:function(){return P},set:function(t){P=t}},arriveThreshold:{get:function(){return R},set:function(t){R=t,k=t*t}},wanderDistance:{get:function(){return M},set:function(t){M=t}},wanderRadius:{get:function(){return N},set:function(t){N=t}},wanderRange:{get:function(){return T},set:function(t){T=t}},avoidDistance:{get:function(){return j},set:function(t){j=t}},avoidBuffer:{get:function(){return G},set:function(t){G=t}},pathIndex:{get:function(){return _},set:function(t){_=t}},pathThreshold:{get:function(){return A},set:function(t){A=t,C=t*t}},maxDistance:{get:function(){return F},set:function(t){F=t,I=t*t}},minDistance:{get:function(){return U},set:function(t){U=t,W=t*t}}}),Object.freeze(m)}var i=function(t,e){if(!(t instanceof e))throw new TypeError("Cannot call a class as a function")},r=function(){function t(t,e){for(var n=0;n0&&void 0!==arguments[0]?arguments[0]:0,n=arguments.length>1&&void 0!==arguments[1]?arguments[1]:0;i(this,t),this.x=e,this.y=n}return t.prototype.add=function(t){return this.x=this.x+t.x,this.y=this.y+t.y,this},t.prototype.subtract=function(t){return this.x=this.x-t.x,this.y=this.y-t.y,this},t.prototype.normalize=function(){var t=this.lengthSquared;if(0===t)return this.x=1,this;if(1===t)return this;var e=c(t);return this.x/=e,this.y/=e,this},t.prototype.isNormalized=function(){return 1===this.lengthSquared},t.prototype.truncate=function(t){return this.lengthSquared>t*t&&(this.length=t),this},t.prototype.scaleBy=function(t){return this.x*=t,this.y*=t,this},t.prototype.divideBy=function(t){return this.x/=t,this.y/=t,this},t.prototype.equals=function(t){return this.x===t.x&&this.y===t.y},t.prototype.negate=function(){return this.x=-this.x,this.y=-this.y,this},t.prototype.dotProduct=function(t){return this.x*t.x+this.y*t.y},t.prototype.crossProduct=function(t){return this.x*t.y-this.y*t.x},t.prototype.distanceSq=function(t){var e=t.x-this.x,n=t.y-this.y;return e*e+n*n},t.prototype.distance=function(t){return c(this.distanceSq(t))},t.prototype.clone=function(){return t.get(this.x,this.y)},t.prototype.reset=function(){return this.x=0,this.y=0,this},t.prototype.perpendicular=function(){return t.get(-this.y,this.x)},t.prototype.sign=function(t){var e=this.perpendicular(),n=e.dotProduct(t)<0?-1:1;return e.dispose(),n},t.prototype.set=function(t,e){return this.x=u(t)*e,this.y=a(t)*e,this},t.prototype.dispose=function(){this.x=0,this.y=0,d.push(this)},t.get=function(e,n){var i=d.length>0?d.pop():new t;return i.x=e||0,i.y=n||0,i},t.fill=function(e){for(;d.length value) { - flockers.pop(); - } + while (flockers.length < value) { + flockers.push(createFlocker()); + } + while (flockers.length > value) { + flockers.pop(); + } }); gui.add(options, 'randomColors').name('random colors'); gui.add(options, 'clear').name('clear canvas'); diff --git a/gulpfile.js b/gulpfile.js deleted file mode 100644 index e1466ab..0000000 --- a/gulpfile.js +++ /dev/null @@ -1,111 +0,0 @@ -'use strict'; - -var browserify = require('browserify'), - browserSync = require('browser-sync'), - buffer = require('vinyl-buffer'), - chalk = require('chalk'), - gulp = require('gulp'), - jshint = require('gulp-jshint'), - rename = require('gulp-rename'), - source = require('vinyl-source-stream'), - strip = require('gulp-strip-debug'), - uglify = require('gulp-uglify'), - watchify = require('watchify'); - -var standaloneName = 'Boid', - entryFileName = 'boid.js', - bundleFileName = 'boid.js'; - -// log -function logError(msg) { - console.log(chalk.bold.red('[ERROR] ' + msg.toString())); -} - -// bundler -var bundler = watchify(browserify({ - entries: ['src/' + entryFileName], - standalone: standaloneName, - debug: true, - cache: {}, - packageCache: {} -})); - -function bundle() { - return bundler - .bundle() - .on('error', logError) - .pipe(source(bundleFileName)) - .pipe(buffer()) - .pipe(gulp.dest('./dist/')) - .pipe(rename({ - extname: '.min.js' - })) - .pipe(uglify()) - .pipe(strip()) - .pipe(gulp.dest('./dist/')); -} - -bundler.on('update', bundle); // on any dep update, runs the bundler -gulp.task('bundle', ['jshint'], bundle); - -function bundleRelease(minify) { - return browserify({ - entries: ['./src/' + entryFileName], - standalone: standaloneName, - debug: !minify - }) - .bundle() - .on('error', logError) - .pipe(source(bundleFileName)) - .pipe(buffer()) - .pipe(gulp.dest('./dist/')) - .pipe(rename({ - extname: '.min.js' - })) - .pipe(uglify()) - .pipe(strip()) - .pipe(gulp.dest('./dist/')); -} - -gulp.task('release', bundleRelease); - -// connect browsers -gulp.task('connect', function() { - browserSync.init({ - server: { - baseDir: ['./', 'examples'] - }, - files: [ - 'dist/*', - 'examples/**/*' - ], - reloadDebounce: 500 - }); -}); - -// reload browsers -gulp.task('reload', function() { - browserSync.reload(); -}); - -// js hint -gulp.task('jshint', function() { - return gulp.src([ - './gulpfile.js', - 'src/**/*.js', - 'test/**/*.js', - 'examples/**/*.js', - '!examples/js/highlight.pack.js' - ]) - .pipe(jshint()) - .pipe(jshint.reporter('jshint-stylish')); -}); - -// watch -gulp.task('watch', function() { - gulp.watch('test/**/*.js', ['jshint']); - gulp.watch('examples/**/*.js', ['jshint']); -}); - -// default -gulp.task('default', ['connect', 'watch', 'bundle']); diff --git a/karma.conf.js b/karma.conf.js index 4445cd0..dc2823e 100644 --- a/karma.conf.js +++ b/karma.conf.js @@ -1,92 +1,42 @@ -'use strict'; - module.exports = function(config) { - config.set({ - - // base path, that will be used to resolve files and exclude - basePath: '', - - plugins: [ - 'karma-mocha', - 'karma-chai', - 'karma-browserify', - 'karma-phantomjs-launcher' - ], - - // frameworks to use - frameworks: ['browserify', 'mocha', 'chai'], - - // list of files / patterns to load in the browser - files: [ - 'test/**/*.spec.js' - ], - - - // list of files to exclude - exclude: [ - - ], - - // Browserify config (all optional) - browserify: { - // extensions: ['.coffee'], - // ignore: [], - // transform: ['coffeeify'], - // debug: true, - // noParse: ['jquery'], - // watch: true - }, - - // Add browserify to preprocessors - preprocessors: {'test/**/*.js': ['browserify']}, - - - // test results reporter to use - // possible values: 'dots', 'progress', 'junit', 'growl', 'coverage' - reporters: ['progress'], - - - // web server port - port: 9876, - - - // enable / disable colors in the output (reporters and logs) - colors: true, - - - // level of logging - /* possible values: - config.LOG_DISABLE - config.LOG_ERROR - config.LOG_WARN - config.LOG_INFO - config.LOG_DEBUG*/ - logLevel: config.LOG_INFO, - - - // enable / disable watching file and executing tests whenever any file changes - autoWatch: true, - - - // Start these browsers, currently available: - // - Chrome - // - ChromeCanary - // - Firefox - // - Opera (has to be installed with `npm install karma-opera-launcher`) - // - Safari (only Mac; has to be installed with `npm install karma-safari-launcher`) - // - PhantomJS - // - IE (only Windows; has to be installed with `npm install karma-ie-launcher`) - browsers: [ - 'PhantomJS' - ], - - - // If browser does not capture in given timeout [ms], kill it - captureTimeout: 60000, - - - // Continuous Integration mode - // if true, it capture browsers, run tests and exit - singleRun: false - }); + config.set({ + basePath: '', + plugins: [ + 'karma-mocha', + 'karma-chai', + 'karma-phantomjs-launcher' + ], + frameworks: ['mocha', 'chai'], + files: [ + 'dist/boid.js', + 'test/**/*.spec.js' + ], + exclude: [ + + ], + // possible values: 'dots', 'progress', 'junit', 'growl', 'coverage' + reporters: ['progress'], + port: 9876, + colors: true, + /* possible values: + config.LOG_DISABLE + config.LOG_ERROR + config.LOG_WARN + config.LOG_INFO + config.LOG_DEBUG*/ + logLevel: config.LOG_INFO, + autoWatch: true, + // - Chrome + // - ChromeCanary + // - Firefox + // - Opera (has to be installed with `npm install karma-opera-launcher`) + // - Safari (only Mac; has to be installed with `npm install karma-safari-launcher`) + // - PhantomJS + // - IE (only Windows; has to be installed with `npm install karma-ie-launcher`) + browsers: [ + 'PhantomJS' + ], + captureTimeout: 60000, + singleRun: false + }); }; diff --git a/package.json b/package.json index 452525b..fb29c18 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "boid", - "version": "0.2.4", + "version": "0.3.0", "description": "Bird-like behaviours", "keywords": [ "boid", @@ -12,7 +12,11 @@ "license": "MIT", "main": "src/boid.js", "scripts": { - "test": "./node_modules/.bin/karma start --single-run" + "test": "./node_modules/.bin/karma start --single-run", + "build": "NODE_ENV=production rollup -c && rollup -c", + "start": "rollup -c -w | npm run sync", + "lint": "eslint 'src/**/*.js'; exit 0", + "sync": "browser-sync start --server examples --server './' --files 'dist/*'" }, "readmeFilename": "README.md", "repository": { @@ -20,26 +24,23 @@ "url": "https://github.com/ianmcgregor/boid" }, "devDependencies": { - "browser-sync": "^2.7.0", - "browserify": "^11.2.0", - "chai": "^3.4.0", - "chalk": "^1.0.0", - "events": "^1.0.2", - "gulp": "^3.8.11", - "gulp-if": "^2.0.0", - "gulp-jshint": "^1.10.0", - "gulp-rename": "^1.2.2", - "gulp-strip-debug": "^1.0.2", - "gulp-uglify": "^1.2.0", - "jshint-stylish": "^2.0.1", - "karma": "^0.13.11", - "karma-browserify": "^4.1.2", + "babel-core": "^6.22.1", + "babel-eslint": "^7.1.1", + "babel-plugin-external-helpers": "^6.22.0", + "babel-preset-es2015": "^6.22.0", + "browser-sync": "^2.18.6", + "chai": "^3.5.0", + "karma": "^1.4.0", "karma-chai": "^0.1.0", - "karma-mocha": "^0.2.0", - "karma-phantomjs-launcher": "^0.2.1", - "mocha": "^2.2.4", - "vinyl-buffer": "^1.0.0", - "vinyl-source-stream": "^1.1.0", - "watchify": "^3.2.1" + "karma-mocha": "^1.3.0", + "karma-phantomjs-launcher": "^1.0.2", + "mocha": "^3.2.0", + "rollup": "^0.41.4", + "rollup-plugin-babel": "^2.7.1", + "rollup-plugin-commonjs": "^7.0.0", + "rollup-plugin-node-resolve": "^2.0.0", + "rollup-plugin-strip": "^1.1.1", + "rollup-plugin-uglify": "^1.0.1", + "rollup-watch": "^3.2.2" } } diff --git a/rollup.config.js b/rollup.config.js new file mode 100644 index 0000000..b832b46 --- /dev/null +++ b/rollup.config.js @@ -0,0 +1,33 @@ +import babel from 'rollup-plugin-babel'; +import nodeResolve from 'rollup-plugin-node-resolve'; +import strip from 'rollup-plugin-strip'; +import uglify from 'rollup-plugin-uglify'; + +const prod = process.env.NODE_ENV === 'production'; + +export default { + entry: 'src/boid.js', + format: 'umd', + moduleName: 'Boid', + dest: (prod ? 'dist/boid.min.js' : 'dist/boid.js'), + sourceMap: !prod, + plugins: [ + nodeResolve({ + jsnext: true, + main: true, + preferBuiltins: false + }), + babel({ + babelrc: false, + exclude: 'node_modules/**', + presets: [ + ['es2015', {loose: true, modules: false}] + ], + plugins: [ + 'external-helpers' + ] + }), + (prod && strip({sourceMap: false})), + (prod && uglify()) + ] +}; diff --git a/src/boid.js b/src/boid.js index f10bb60..c6e9a29 100644 --- a/src/boid.js +++ b/src/boid.js @@ -1,8 +1,8 @@ -'use strict'; +import Vec2 from './vec2.js'; -var Vec2 = require('./vec2.js'); +const PI_D2 = Math.PI / 2; -var defaults = { +const defaults = { bounds: { x: 0, y: 0, @@ -25,67 +25,68 @@ var defaults = { minDistance: 60 }; -function Boid(options) { - options = configure(options); +function setDefaults(opts, defs) { + Object.keys(defs).forEach((key) => { + if (typeof opts[key] === 'undefined') { + opts[key] = defs[key]; + } + }); +} - var position = Vec2.get(); - var velocity = Vec2.get(); - var steeringForce = Vec2.get(); +function configure(options) { + options = options || {}; + options.bounds = options.bounds || {}; + setDefaults(options, defaults); + setDefaults(options.bounds, defaults.bounds); + return options; +} + +export default function Boid(options) { + options = configure(options); - var bounds = options.bounds; - var edgeBehavior = options.edgeBehavior; - var mass = options.mass; - var maxSpeed = options.maxSpeed; - var maxSpeedSq = maxSpeed * maxSpeed; - var maxForce = options.maxForce; + let boid = null; + const position = Vec2.get(); + const velocity = Vec2.get(); + const steeringForce = Vec2.get(); + + const bounds = options.bounds; + let edgeBehavior = options.edgeBehavior; + let mass = options.mass; + let maxSpeed = options.maxSpeed; + let maxSpeedSq = maxSpeed * maxSpeed; + let maxForce = options.maxForce; // arrive - var arriveThreshold = options.arriveThreshold; - var arriveThresholdSq = arriveThreshold * arriveThreshold; + let arriveThreshold = options.arriveThreshold; + let arriveThresholdSq = arriveThreshold * arriveThreshold; // wander - var wanderDistance = options.wanderDistance; - var wanderRadius = options.wanderRadius; - var wanderAngle = options.wanderAngle; - var wanderRange = options.wanderRange; + let wanderDistance = options.wanderDistance; + let wanderRadius = options.wanderRadius; + let wanderAngle = options.wanderAngle; + let wanderRange = options.wanderRange; // avoid - var avoidDistance = options.avoidDistance; - var avoidBuffer = options.avoidBuffer; + let avoidDistance = options.avoidDistance; + let avoidBuffer = options.avoidBuffer; // follow path - var pathIndex = 0; - var pathThreshold = options.pathThreshold; - var pathThresholdSq = pathThreshold * pathThreshold; + let pathIndex = 0; + let pathThreshold = options.pathThreshold; + let pathThresholdSq = pathThreshold * pathThreshold; // flock - var maxDistance = options.maxDistance; - var maxDistanceSq = maxDistance * maxDistance; - var minDistance = options.minDistance; - var minDistanceSq = minDistance * minDistance; + let maxDistance = options.maxDistance; + let maxDistanceSq = maxDistance * maxDistance; + let minDistance = options.minDistance; + let minDistanceSq = minDistance * minDistance; - var setBounds = function(width, height, x, y) { + function setBounds(width, height, x, y) { bounds.width = width; bounds.height = height; bounds.x = x || 0; bounds.y = y || 0; return boid; - }; + } - var update = function() { - steeringForce.truncate(maxForce); - steeringForce.divideBy(mass); - velocity.add(steeringForce); - steeringForce.reset(); - velocity.truncate(maxSpeed); - position.add(velocity); - - if (edgeBehavior === Boid.EDGE_BOUNCE) { - bounce(); - } else if (edgeBehavior === Boid.EDGE_WRAP) { - wrap(); - } - return boid; - }; - - var bounce = function() { - var maxX = bounds.x + bounds.width; + function bounce() { + const maxX = bounds.x + bounds.width; if (position.x > maxX) { position.x = maxX; velocity.x *= -1; @@ -94,7 +95,7 @@ function Boid(options) { velocity.x *= -1; } - var maxY = bounds.y + bounds.height; + const maxY = bounds.y + bounds.height; if (position.y > maxY) { position.y = maxY; velocity.y *= -1; @@ -102,73 +103,73 @@ function Boid(options) { position.y = bounds.y; velocity.y *= -1; } - }; + } - var wrap = function() { - var maxX = bounds.x + bounds.width; + function wrap() { + const maxX = bounds.x + bounds.width; if (position.x > maxX) { position.x = bounds.x; } else if (position.x < bounds.x) { position.x = maxX; } - var maxY = bounds.y + bounds.height; + const maxY = bounds.y + bounds.height; if (position.y > maxY) { position.y = bounds.y; } else if (position.y < bounds.y) { position.y = maxY; } - }; + } - var seek = function(targetVec) { - var desiredVelocity = targetVec.clone().subtract(position); + function seek(targetVec) { + const desiredVelocity = targetVec.clone().subtract(position); desiredVelocity.normalize(); desiredVelocity.scaleBy(maxSpeed); - var force = desiredVelocity.subtract(velocity); + const force = desiredVelocity.subtract(velocity); steeringForce.add(force); force.dispose(); return boid; - }; + } - var flee = function(targetVec) { - var desiredVelocity = targetVec.clone().subtract(position); + function flee(targetVec) { + const desiredVelocity = targetVec.clone().subtract(position); desiredVelocity.normalize(); desiredVelocity.scaleBy(maxSpeed); - var force = desiredVelocity.subtract(velocity); + const force = desiredVelocity.subtract(velocity); steeringForce.subtract(force); force.dispose(); return boid; - }; + } // seek until within arriveThreshold - var arrive = function(targetVec) { - var desiredVelocity = targetVec.clone().subtract(position); + function arrive(targetVec) { + const desiredVelocity = targetVec.clone().subtract(position); desiredVelocity.normalize(); - var distanceSq = position.distanceSq(targetVec); + const distanceSq = position.distanceSq(targetVec); if (distanceSq > arriveThresholdSq) { desiredVelocity.scaleBy(maxSpeed); } else { - var scalar = maxSpeed * distanceSq / arriveThresholdSq; + const scalar = maxSpeed * distanceSq / arriveThresholdSq; desiredVelocity.scaleBy(scalar); } - var force = desiredVelocity.subtract(velocity); + const force = desiredVelocity.subtract(velocity); steeringForce.add(force); force.dispose(); return boid; - }; + } // look at velocity of boid and try to predict where it's going - var pursue = function(targetBoid) { - var lookAheadTime = position.distanceSq(targetBoid.position) / maxSpeedSq; + function pursue(targetBoid) { + const lookAheadTime = position.distanceSq(targetBoid.position) / maxSpeedSq; - var scaledVelocity = targetBoid.velocity.clone().scaleBy(lookAheadTime); - var predictedTarget = targetBoid.position.clone().add(scaledVelocity); + const scaledVelocity = targetBoid.velocity.clone().scaleBy(lookAheadTime); + const predictedTarget = targetBoid.position.clone().add(scaledVelocity); seek(predictedTarget); @@ -176,14 +177,14 @@ function Boid(options) { predictedTarget.dispose(); return boid; - }; + } // look at velocity of boid and try to predict where it's going - var evade = function(targetBoid) { - var lookAheadTime = position.distanceSq(targetBoid.position) / maxSpeedSq; + function evade(targetBoid) { + const lookAheadTime = position.distanceSq(targetBoid.position) / maxSpeedSq; - var scaledVelocity = targetBoid.velocity.clone().scaleBy(lookAheadTime); - var predictedTarget = targetBoid.position.clone().add(scaledVelocity); + const scaledVelocity = targetBoid.velocity.clone().scaleBy(lookAheadTime); + const predictedTarget = targetBoid.position.clone().add(scaledVelocity); flee(predictedTarget); @@ -191,58 +192,60 @@ function Boid(options) { predictedTarget.dispose(); return boid; - }; + } // wander around, changing angle by a limited amount each tick - var wander = function() { - var center = velocity.clone().normalize().scaleBy(wanderDistance); + function wander() { + const center = velocity.clone().normalize().scaleBy(wanderDistance); - var offset = Vec2.get(); - offset.length = wanderRadius; - offset.angle = wanderAngle; + const offset = Vec2.get(); + offset.set(wanderAngle, wanderRadius); + // offset.length = wanderRadius; + // offset.angle = wanderAngle; wanderAngle += Math.random() * wanderRange - wanderRange * 0.5; - var force = center.add(offset); + const force = center.add(offset); steeringForce.add(force); offset.dispose(); force.dispose(); return boid; - }; + } // gets a bit rough used in combination with seeking as the boid attempts // to seek straight through an object while simultaneously trying to avoid it - var avoid = function(obstacles) { - for (var i = 0; i < obstacles.length; i++) { - var obstacle = obstacles[i]; - var heading = velocity.clone().normalize(); + function avoid(obstacles) { + for (let i = 0; i < obstacles.length; i++) { + const obstacle = obstacles[i]; + const heading = velocity.clone().normalize(); // vec between obstacle and boid - var difference = obstacle.position.clone().subtract(position); - var dotProd = difference.dotProduct(heading); + const difference = obstacle.position.clone().subtract(position); + const dotProd = difference.dotProduct(heading); // if obstacle in front of boid if (dotProd > 0) { // vec to represent 'feeler' arm - var feeler = heading.clone().scaleBy(avoidDistance); + const feeler = heading.clone().scaleBy(avoidDistance); // project difference onto feeler - var projection = heading.clone().scaleBy(dotProd); + const projection = heading.clone().scaleBy(dotProd); // distance from obstacle to feeler - var vecDistance = projection.subtract(difference); - var distance = vecDistance.length; + const vecDistance = projection.subtract(difference); + const distance = vecDistance.length; // if feeler intersects obstacle (plus buffer), and projection // less than feeler length, will collide if (distance < (obstacle.radius || 0) + avoidBuffer && projection.length < feeler.length) { // calc a force +/- 90 deg from vec to circ - var force = heading.clone().scaleBy(maxSpeed); - force.angle += difference.sign(velocity) * Math.PI / 2; + const force = heading.clone().scaleBy(maxSpeed); + force.angle += difference.sign(velocity) * PI_D2; // scale force by distance (further = smaller force) - force.scaleBy(1 - projection.length / feeler.length); + const dist = projection.length / feeler.length; + force.scaleBy(1 - dist); // add to steering force steeringForce.add(force); // braking force - slows boid down so it has time to turn (closer = harder) - velocity.scaleBy(projection.length / feeler.length); + velocity.scaleBy(dist); force.dispose(); } @@ -254,13 +257,13 @@ function Boid(options) { difference.dispose(); } return boid; - }; + } // follow a path made up of an array or vectors - var followPath = function(path, loop) { + function followPath(path, loop) { loop = !!loop; - var wayPoint = path[pathIndex]; + const wayPoint = path[pathIndex]; if (!wayPoint) { pathIndex = 0; return boid; @@ -280,19 +283,35 @@ function Boid(options) { seek(wayPoint); } return boid; - }; + } + + // is boid close enough to be in sight and facing + function inSight(b) { + if (position.distanceSq(b.position) > maxDistanceSq) { + return false; + } + const heading = velocity.clone().normalize(); + const difference = b.position.clone().subtract(position); + const dotProd = difference.dotProduct(heading); + + heading.dispose(); + difference.dispose(); + + return dotProd >= 0; + } // flock - group of boids loosely move together - var flock = function(boids) { - var averageVelocity = velocity.clone(); - var averagePosition = Vec2.get(); - var inSightCount = 0; - for (var i = 0; i < boids.length; i++) { - var b = boids[i]; + function flock(boids) { + const averageVelocity = velocity.clone(); + const averagePosition = Vec2.get(); + let inSightCount = 0; + for (let i = 0; i < boids.length; i++) { + const b = boids[i]; if (b !== boid && inSight(b)) { averageVelocity.add(b.velocity); averagePosition.add(b.position); - if (tooClose(b)) { + + if (position.distanceSq(b.position) < minDistanceSq) { flee(b.position); } inSightCount++; @@ -308,47 +327,47 @@ function Boid(options) { averagePosition.dispose(); return boid; - }; + } - // is boid close enough to be in sight and facing - var inSight = function(boid) { - if (position.distanceSq(boid.position) > maxDistanceSq) { - return false; + function update() { + steeringForce.truncate(maxForce); + if (mass !== 1) { + steeringForce.divideBy(mass); } - var heading = velocity.clone().normalize(); - var difference = boid.position.clone().subtract(position); - var dotProd = difference.dotProduct(heading); - - heading.dispose(); - difference.dispose(); + // velocity.add(steeringForce); + velocity.x += steeringForce.x; + velocity.y += steeringForce.y; + // steeringForce.reset(); + steeringForce.x = 0; + steeringForce.y = 0; + velocity.truncate(maxSpeed); + // position.add(velocity); + position.x += velocity.x; + position.y += velocity.y; - if (dotProd < 0) { - return false; + if (edgeBehavior === Boid.EDGE_BOUNCE) { + bounce(); + } else if (edgeBehavior === Boid.EDGE_WRAP) { + wrap(); } - return true; - }; - - // is boid too close? - var tooClose = function(boid) { - return position.distanceSq(boid.position) < minDistanceSq; - }; - - // methods - var boid = { - bounds: bounds, - setBounds: setBounds, - update: update, - pursue: pursue, - evade: evade, - wander: wander, - avoid: avoid, - followPath: followPath, - flock: flock, - arrive: arrive, - seek: seek, - flee: flee, - position: position, - velocity: velocity, + return boid; + } + + boid = { + bounds, + setBounds, + update, + pursue, + evade, + wander, + avoid, + followPath, + flock, + arrive, + seek, + flee, + position, + velocity, userData: {} }; @@ -500,23 +519,3 @@ Boid.obstacle = function(radius, x, y) { position: Vec2.get(x, y) }; }; - -function setDefaults(opts, defs) { - Object.keys(defs).forEach(function(key) { - if (typeof opts[key] === 'undefined') { - opts[key] = defs[key]; - } - }); -} - -function configure(options) { - options = options || {}; - options.bounds = options.bounds || {}; - setDefaults(options, defaults); - setDefaults(options.bounds, defaults.bounds); - return options; -} - -if (typeof module === 'object' && module.exports) { - module.exports = Boid; -} diff --git a/src/vec2.js b/src/vec2.js index e54640a..cba437e 100644 --- a/src/vec2.js +++ b/src/vec2.js @@ -1,62 +1,75 @@ -'use strict'; +const {acos, atan2, cos, sin, sqrt} = Math; -function Vec2(x, y) { - this.x = x || 0; - this.y = y || 0; -} +const pool = []; + +export default class Vec2 { + constructor(x = 0, y = 0) { + this.x = x; + this.y = y; + } -Vec2.prototype = { - add: function(vec) { + add(vec) { this.x = this.x + vec.x; this.y = this.y + vec.y; return this; - }, - subtract: function(vec) { + } + + subtract(vec) { this.x = this.x - vec.x; this.y = this.y - vec.y; return this; - }, - normalize: function() { - var l = this.length; - if (l === 0) { + } + + normalize() { + const lsq = this.lengthSquared; + if (lsq === 0) { this.x = 1; return this; } - if (l === 1) { + if (lsq === 1) { return this; } + const l = sqrt(lsq); this.x /= l; this.y /= l; return this; - }, - isNormalized: function() { - return this.length === 1; - }, - truncate: function(max) { - if (this.length > max) { + } + + isNormalized() { + return this.lengthSquared === 1; + } + + truncate(max) { + // if (this.length > max) { + if (this.lengthSquared > max * max) { this.length = max; } return this; - }, - scaleBy: function(mul) { + } + + scaleBy(mul) { this.x *= mul; this.y *= mul; return this; - }, - divideBy: function(div) { + } + + divideBy(div) { this.x /= div; this.y /= div; return this; - }, - equals: function(vec) { + } + + equals(vec) { return this.x === vec.x && this.y === vec.y; - }, - negate: function() { + } + + negate() { this.x = -this.x; this.y = -this.y; return this; - }, - dotProduct: function(vec) { + } + + dotProduct(vec) { /* If A and B are perpendicular (at 90 degrees to each other), the result of the dot product will be zero, because cos(Θ) will be zero. @@ -68,105 +81,104 @@ Vec2.prototype = { and the vector lengths are always positive values */ return this.x * vec.x + this.y * vec.y; - }, - crossProduct: function(vec) { + } + + crossProduct(vec) { /* The sign tells us if vec to the left (-) or the right (+) of this vec */ return this.x * vec.y - this.y * vec.x; - }, - distanceSq: function(vec) { - var dx = vec.x - this.x; - var dy = vec.y - this.y; + } + + distanceSq(vec) { + const dx = vec.x - this.x; + const dy = vec.y - this.y; return dx * dx + dy * dy; - }, - distance: function(vec) { - return Math.sqrt(this.distanceSq(vec)); - }, - clone: function() { + } + + distance(vec) { + return sqrt(this.distanceSq(vec)); + } + + clone() { return Vec2.get(this.x, this.y); - }, - reset: function() { + } + + reset() { this.x = 0; this.y = 0; return this; - }, - perpendicular: function() { + } + + perpendicular() { return Vec2.get(-this.y, this.x); - }, - sign: function(vec) { + } + + sign(vec) { // Determines if a given vector is to the right or left of this vector. // If to the left, returns -1. If to the right, +1. - var p = this.perpendicular(); - var s = p.dotProduct(vec) < 0 ? -1 : 1; + const p = this.perpendicular(); + const s = p.dotProduct(vec) < 0 ? -1 : 1; p.dispose(); return s; - }, - set: function(x, y) { - this.x = x || 0; - this.y = y || 0; + } + + set(angle, length) { + this.x = cos(angle) * length; + this.y = sin(angle) * length; return this; - }, - dispose: function() { - Vec2.pool.push(this.reset()); } -}; -// getters / setters + dispose() { + this.x = 0; + this.y = 0; + pool.push(this); + } + + get lengthSquared() { + return this.x * this.x + this.y * this.y; + } -Object.defineProperties(Vec2.prototype, { - lengthSquared: { - get: function() { - return this.x * this.x + this.y * this.y; - } - }, - length: { - get: function() { - return Math.sqrt(this.lengthSquared); - }, - set: function(value) { - var a = this.angle; - this.x = Math.cos(a) * value; - this.y = Math.sin(a) * value; - } - }, - angle: { - get: function() { - return Math.atan2(this.y, this.x); - }, - set: function(value) { - var l = this.length; - this.x = Math.cos(value) * l; - this.y = Math.sin(value) * l; - } + get length() { + return sqrt(this.lengthSquared); } -}); -// static + set length(value) { + const a = this.angle; + this.x = cos(a) * value; + this.y = sin(a) * value; + } -Vec2.pool = []; -Vec2.get = function(x, y) { - var v = Vec2.pool.length > 0 ? Vec2.pool.pop() : new Vec2(); - v.set(x, y); - return v; -}; + get angle() { + return atan2(this.y, this.x); + } -Vec2.fill = function(n) { - while (Vec2.pool.length < n) { - Vec2.pool.push(new Vec2()); + set angle(value) { + const l = this.length; + this.x = cos(value) * l; + this.y = sin(value) * l; } -}; -Vec2.angleBetween = function(a, b) { - if (!a.isNormalized()) { - a = a.clone().normalize(); + static get(x, y) { + const v = pool.length > 0 ? pool.pop() : new Vec2(); + v.x = x || 0; + v.y = y || 0; + return v; } - if (!b.isNormalized()) { - b = b.clone().normalize(); + + static fill(n) { + while (pool.length < n) { + pool.push(new Vec2()); + } } - return Math.acos(a.dotProduct(b)); -}; -if (typeof module === 'object' && module.exports) { - module.exports = Vec2; + static angleBetween(a, b) { + if (!a.isNormalized()) { + a = a.clone().normalize(); + } + if (!b.isNormalized()) { + b = b.clone().normalize(); + } + return acos(a.dotProduct(b)); + } } diff --git a/test/boid.spec.js b/test/boid.spec.js index 439b146..2855591 100644 --- a/test/boid.spec.js +++ b/test/boid.spec.js @@ -1,11 +1,8 @@ -'use strict'; - -var Boid = require('../src/boid.js'); -// var Vec2 = require('../src/vec2.js'); +const Boid = window.Boid; describe('boid', function() { - var boid = new Boid(); + const boid = new Boid(); it('should have created a boid instance', function() { expect(boid).to.be.an('object');