├── .gitignore ├── Box2dWeb-2.1.a.3.js ├── Box2dWeb-2.1.a.3.min.js ├── LICENSE ├── README.md ├── boxbox.js ├── boxbox.min.js ├── demos ├── chain │ ├── chain.html │ ├── chain.js │ ├── iphone.gif │ ├── parakeet chain.jpg │ └── parakeet toy.gif └── platformer │ ├── demo.html │ ├── demo.js │ └── wheel.png ├── grunt.js ├── test ├── index.html ├── lib │ ├── jquery-1.7.2.js │ ├── qunit-1.5.0.css │ ├── qunit-1.5.0.js │ ├── sinon-1.3.4.js │ └── underscore-1.3.3.js └── tests │ └── entity.js └── updoc.html /.gitignore: -------------------------------------------------------------------------------- 1 | .project 2 | .DS_Store 3 | *.swp 4 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | This is the license for boxbox.js. It does not apply to box2dweb, box2d, or any other dependency of boxbox.js. 2 | 3 | Copyright (C) 2012 Greg Smith 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # boxbox 2 | 3 | A framework that makes it easier to use the Box2d / Box2dweb physics engine in JavaScript. 4 | 5 | ## Learn about boxbox 6 | 7 | http://incompl.github.com/boxbox 8 | 9 | ## box2dweb files are from 10 | 11 | http://code.google.com/p/box2dweb/ 12 | 13 | ## Demos, experiments, projects, etc. 14 | 15 | * [Don't look at me](http://dontlookatme.maryrosecook.com/) 16 | * [Platformer demo](http://incompl.github.io/boxbox/boxbox/demos/platformer/demo.html) 17 | * [Chain demo](http://incompl.github.io/boxbox/boxbox/demos/chain/chain.html) 18 | * [Box Fall](http://bama.ua.edu/~ardixon1/MAIN/Code/block_fall/play.html) 19 | * Add your project or demo here! 20 | 21 | ## Created at Bocoup 22 | 23 | http://bocoup.com 24 | -------------------------------------------------------------------------------- /boxbox.js: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (C) 2012 Greg Smith 3 | 4 | Released under the MIT license: 5 | https://github.com/incompl/boxbox/blob/master/LICENSE 6 | 7 | Created at Bocoup http://bocoup.com 8 | */ 9 | 10 | /** 11 | * @_page_title boxbox 12 | * @_page_css updoc-custom.css 13 | * @_page_description api documentation 14 | * @_page_home_path . 15 | * @_page_compact_index 16 | */ 17 | 18 | // Erik Moller's requestAnimationFrame shim 19 | // http://my.opera.com/emoller/blog/2011/12/20/requestanimationframe-for-smart-er-animating 20 | (function() { 21 | var lastTime = 0; 22 | var vendors = ['ms', 'moz', 'webkit', 'o']; 23 | for(var x = 0; x < vendors.length && !window.requestAnimationFrame; ++x) { 24 | window.requestAnimationFrame = window[vendors[x]+'RequestAnimationFrame']; 25 | window.cancelRequestAnimationFrame = window[vendors[x]+ 26 | 'CancelRequestAnimationFrame']; 27 | } 28 | 29 | if (!window.requestAnimationFrame) { 30 | window.requestAnimationFrame = function(callback, element) { 31 | var currTime = new Date().getTime(); 32 | var timeToCall = Math.max(0, 16 - (currTime - lastTime)); 33 | var id = window.setTimeout(function() { callback(currTime + timeToCall); }, 34 | timeToCall); 35 | lastTime = currTime + timeToCall; 36 | return id; 37 | }; 38 | } 39 | 40 | if (!window.cancelAnimationFrame) { 41 | window.cancelAnimationFrame = function(id) { 42 | clearTimeout(id); 43 | }; 44 | } 45 | }()); 46 | 47 | (function() { 48 | 49 | var DEGREES_PER_RADIAN = 57.2957795; // 180 / pi 50 | 51 | /** 52 | * @description global boxbox object 53 | */ 54 | window.boxbox = {}; 55 | 56 | // Make sure Box2D exists 57 | if (Box2D === undefined) { 58 | console.error('boxbox needs Box2d to work'); 59 | return; 60 | } 61 | 62 | // Object creation inspired by Crockford 63 | // http://javascript.crockford.com/prototypal.html 64 | function create(o) { 65 | function F() {} 66 | F.prototype = o; 67 | return new F(); 68 | } 69 | 70 | // A minimal extend for simple objects inspired by jQuery 71 | function extend(target, o) { 72 | if (target === undefined) { 73 | target = {}; 74 | } 75 | if (o !== undefined) { 76 | for (var key in o) { 77 | if (o.hasOwnProperty(key) && target[key] === undefined) { 78 | target[key] = o[key]; 79 | } 80 | } 81 | } 82 | return target; 83 | } 84 | 85 | // these look like imports but there is no cost here 86 | var b2Vec2 = Box2D.Common.Math.b2Vec2; 87 | var b2Math = Box2D.Common.Math.b2Math; 88 | var b2BodyDef = Box2D.Dynamics.b2BodyDef; 89 | var b2Body = Box2D.Dynamics.b2Body; 90 | var b2FixtureDef = Box2D.Dynamics.b2FixtureDef; 91 | var b2Fixture = Box2D.Dynamics.b2Fixture; 92 | var b2World = Box2D.Dynamics.b2World; 93 | var shapes = Box2D.Collision.Shapes; 94 | var b2DebugDraw = Box2D.Dynamics.b2DebugDraw; 95 | var b2AABB = Box2D.Collision.b2AABB; 96 | 97 | /** 98 | * @_module boxbox 99 | * @_params canvas, [options] 100 | * @canvas element to render on 101 | * @options 102 | * 111 | * @return a new World 112 | * @description 113 | without options 114 | var canvasElem = document.getElementById("myCanvas"); 115 | var world = boxbox.createWorld(canvasElem); 116 | with options 117 | var canvasElem = document.getElementById("myCanvas"); 118 | var world = boxbox.createWorld(canvasElem, { 119 |   gravity: {x: 0, y: 20}, 120 |   scale: 60 121 | }); 122 | */ 123 | window.boxbox.createWorld = function(canvas, options) { 124 | var world = create(World); 125 | world._init(canvas, options); 126 | return world; 127 | }; 128 | 129 | var WORLD_DEFAULT_OPTIONS = { 130 | gravity: {x:0, y:10}, 131 | allowSleep: true, 132 | scale: 30, 133 | tickFrequency: 50, 134 | collisionOutlines: false 135 | }; 136 | 137 | var JOINT_DEFAULT_OPTIONS = { 138 | type: "distance", 139 | allowCollisions: false 140 | }; 141 | 142 | /** 143 | * @header 144 | * @description contains a single self-contained physics simulation 145 | */ 146 | var World = { 147 | 148 | _ops: null, 149 | _world: null, 150 | _canvas: null, 151 | _keydownHandlers: {}, 152 | _keyupHandlers: {}, 153 | _startContactHandlers: {}, 154 | _finishContactHandlers: {}, 155 | _impactHandlers: {}, 156 | _destroyQueue: [], 157 | _impulseQueue: [], 158 | _constantVelocities: {}, 159 | _constantForces: {}, 160 | _entities: {}, 161 | _nextEntityId: 0, 162 | _cameraX: 0, 163 | _cameraY: 0, 164 | _onRender: [], 165 | _onTick: [], 166 | _creationQueue: [], 167 | _positionQueue: [], 168 | 169 | _init: function(canvasElem, options) { 170 | var self = this; 171 | var key; 172 | var i; 173 | var world; 174 | var listener; 175 | this._ops = extend(options, WORLD_DEFAULT_OPTIONS); 176 | this._world = new b2World(new b2Vec2(this._ops.gravity.x, 177 | this._ops.gravity.y), 178 | true); 179 | world = this._world; 180 | this._canvas = canvasElem; 181 | this._ctx = this._canvas.getContext("2d"); 182 | this._scale = this._ops.scale; 183 | 184 | // Set up rendering on the provided canvas 185 | if (this._canvas !== undefined) { 186 | 187 | // debug rendering 188 | if (this._ops.debugDraw) { 189 | var debugDraw = new b2DebugDraw(); 190 | debugDraw.SetSprite(this._canvas.getContext("2d")); 191 | debugDraw.SetDrawScale(this._scale); // TODO update this if changed? 192 | debugDraw.SetFillAlpha(0.3); 193 | debugDraw.SetLineThickness(1.0); 194 | debugDraw.SetFlags(b2DebugDraw.e_shapeBit | b2DebugDraw.e_jointBit); 195 | world.SetDebugDraw(debugDraw); 196 | } 197 | 198 | // game loop (onTick events) 199 | window.setInterval(function() { 200 | var i; 201 | var ctx; 202 | for (i = 0; i < self._onTick.length; i++) { 203 | ctx = self._onTick[i].ctx; 204 | if (!ctx._destroyed) { 205 | self._onTick[i].fun.call(ctx); 206 | } 207 | } 208 | }, this._ops.tickFrequency); 209 | 210 | // animation loop 211 | (function animationLoop(){ 212 | 213 | var key; 214 | var entity; 215 | var v; 216 | var impulse; 217 | var f; 218 | var toDestroy; 219 | var id; 220 | var o; 221 | 222 | // set velocities for this step 223 | for (key in self._constantVelocities) { 224 | v = self._constantVelocities[key]; 225 | v.body.SetLinearVelocity(new b2Vec2(v.x, v.y), 226 | v.body.GetWorldCenter()); 227 | } 228 | 229 | // apply impulses for this step 230 | for (i = 0; i < self._impulseQueue.length; i++) { 231 | impulse = self._impulseQueue.pop(); 232 | impulse.body.ApplyImpulse(new b2Vec2(impulse.x, impulse.y), 233 | impulse.body.GetWorldCenter()); 234 | } 235 | 236 | // set forces for this step 237 | for (key in self._constantForces) { 238 | f = self._constantForces[key]; 239 | f.body.ApplyForce(new b2Vec2(f.x, f.y), 240 | f.body.GetWorldCenter()); 241 | } 242 | 243 | for (key in self._entities) { 244 | entity = self._entities[key]; 245 | v = entity._body.GetLinearVelocity(); 246 | if (v.x > entity._ops.maxVelocityX) { 247 | v.x = entity._ops.maxVelocityX; 248 | } 249 | if (v.x < -entity._ops.maxVelocityX) { 250 | v.x = -entity._ops.maxVelocityX; 251 | } 252 | if (v.y > entity._ops.maxVelocityY) { 253 | v.y = entity._ops.maxVelocityY; 254 | } 255 | if (v.y < -entity._ops.maxVelocityY) { 256 | v.y = -entity._ops.maxVelocityY; 257 | } 258 | } 259 | 260 | // destroy 261 | for (i = 0; i < self._destroyQueue.length; i++) { 262 | toDestroy = self._destroyQueue.pop(); 263 | id = toDestroy._id; 264 | world.DestroyBody(toDestroy._body); 265 | toDestroy._destroyed = true; 266 | delete self._keydownHandlers[id]; 267 | delete self._startContactHandlers[id]; 268 | delete self._finishContactHandlers[id]; 269 | delete self._impactHandlers[id]; 270 | self._destroyQueue.splice(id, 1); 271 | self._impulseQueue.splice(id, 1); 272 | delete self._constantVelocities[id]; 273 | delete self._constantForces[id]; 274 | delete self._entities[id]; 275 | } 276 | 277 | // framerate, velocity iterations, position iterations 278 | world.Step(1 / 60, 10, 10); 279 | 280 | // create 281 | for (i = 0; i < self._creationQueue.length; i++) { 282 | self.createEntity(self._creationQueue.pop()); 283 | } 284 | 285 | // position 286 | for (i = 0; i < self._positionQueue.length; i++) { 287 | o = self._positionQueue.pop(); 288 | o.o.position.call(o.o, o.val); 289 | } 290 | 291 | // render stuff 292 | self._canvas.width = self._canvas.width; 293 | for (key in self._entities) { 294 | entity = self._entities[key]; 295 | entity._draw(self._ctx, 296 | entity.canvasPosition().x, 297 | entity.canvasPosition().y); 298 | } 299 | for (i = 0; i < self._onRender.length; i++) { 300 | self._onRender[i].fun.call(self._onRender[i].ctx, self._ctx); 301 | } 302 | 303 | world.ClearForces(); 304 | world.DrawDebugData(); 305 | 306 | window.requestAnimationFrame(animationLoop); 307 | }()); 308 | 309 | // keyboard events 310 | window.addEventListener('keydown', function(e) { 311 | for (var key in self._keydownHandlers) { 312 | if (!self._entities[key]._destroyed) { 313 | self._keydownHandlers[key].call(self._entities[key], e); 314 | } 315 | } 316 | }, false); 317 | window.addEventListener('keyup', function(e) { 318 | for (var key in self._keyupHandlers) { 319 | if (!self._entities[key]._destroyed) { 320 | self._keyupHandlers[key].call(self._entities[key], e); 321 | } 322 | } 323 | }, false); 324 | 325 | // contact events 326 | listener = new Box2D.Dynamics.b2ContactListener(); 327 | listener.BeginContact = function(contact) { 328 | var a = self._entities[contact.GetFixtureA().GetBody()._bbid]; 329 | var b = self._entities[contact.GetFixtureB().GetBody()._bbid]; 330 | for (var key in self._startContactHandlers) { 331 | if (a._id === Number(key) && !a._destroyed) { 332 | self._startContactHandlers[key].call(self._entities[key], b); 333 | } 334 | if (b._id === Number(key) && !b._destroyed) { 335 | self._startContactHandlers[key].call(self._entities[key], a); 336 | } 337 | } 338 | }; 339 | listener.EndContact = function(contact) { 340 | var a = self._entities[contact.GetFixtureA().GetBody()._bbid]; 341 | var b = self._entities[contact.GetFixtureB().GetBody()._bbid]; 342 | for (var key in self._finishContactHandlers) { 343 | if (a._id === Number(key) && !a._destroyed) { 344 | self._finishContactHandlers[key].call(self._entities[key], b); 345 | } 346 | if (b._id === Number(key) && !b._destroyed) { 347 | self._finishContactHandlers[key].call(self._entities[key], a); 348 | } 349 | } 350 | }; 351 | listener.PostSolve = function(contact, impulse) { 352 | var a = self._entities[contact.GetFixtureA().GetBody()._bbid]; 353 | var b = self._entities[contact.GetFixtureB().GetBody()._bbid]; 354 | 355 | for (var key in self._impactHandlers) { 356 | if (a._id === Number(key) && !a._destroyed) { 357 | self._impactHandlers[key].call(self._entities[key], 358 | b, 359 | impulse.normalImpulses[0], 360 | impulse.tangentImpulses[0]); 361 | } 362 | if (b._id === Number(key) && !b._destroyed) { 363 | self._impactHandlers[key].call(self._entities[key], 364 | a, 365 | impulse.normalImpulses[0], 366 | impulse.tangentImpulses[0]); 367 | } 368 | } 369 | }; 370 | world.SetContactListener(listener); 371 | } 372 | }, 373 | 374 | _addKeydownHandler: function(id, f) { 375 | this._keydownHandlers[id] = f; 376 | }, 377 | 378 | _addKeyupHandler: function(id, f) { 379 | this._keyupHandlers[id] = f; 380 | }, 381 | 382 | _addStartContactHandler: function(id, f) { 383 | this._startContactHandlers[id] = f; 384 | }, 385 | 386 | _addFinishContactHandler: function(id, f) { 387 | this._finishContactHandlers[id] = f; 388 | }, 389 | 390 | _addImpactHandler: function(id, f) { 391 | this._impactHandlers[id] = f; 392 | }, 393 | 394 | _destroy: function(obj) { 395 | this._destroyQueue.push(obj); 396 | }, 397 | 398 | _applyImpulse: function(id, body, x, y) { 399 | this._impulseQueue.push({ 400 | id:id, 401 | body:body, 402 | x:x, 403 | y:y 404 | }); 405 | }, 406 | 407 | _setConstantVelocity: function(name, id, body, x, y) { 408 | this._constantVelocities[name + id] = { 409 | id:id, 410 | body:body, 411 | x:x, 412 | y:y 413 | }; 414 | }, 415 | 416 | _clearConstantVelocity: function(name, id) { 417 | delete this._constantVelocities[name + id]; 418 | }, 419 | 420 | _setConstantForce: function(name, id, body, x, y) { 421 | this._constantForces[name + id] = { 422 | id:id, 423 | body:body, 424 | x:x, 425 | y:y 426 | }; 427 | }, 428 | 429 | _clearConstantForce: function(name, id) { 430 | delete this._constantForces[name + id]; 431 | }, 432 | 433 | /** 434 | * @_module world 435 | * @_params [value] 436 | * @value: {x,y} 437 | * @return: {x,y} 438 | * @description get or set the world's gravity 439 | */ 440 | gravity: function(value) { 441 | if (value !== undefined) { 442 | this._world.SetGravity(new b2Vec2(0, value)); 443 | } 444 | var v = this._world.GetGravity(); 445 | return {x: v.x, y: v.y}; 446 | }, 447 | 448 | /** 449 | * @_module world 450 | * @_params options 451 | * @options 452 | * 495 | * @return a new Entity 496 | * @description 497 |

Example

498 | var player = world.createEntity({ 499 |   name: "player", 500 |   shape: "circle", 501 |   radius: 2 502 | }); 503 |

Templates

504 | You can pass multiple options objects. This allows for "templates" 505 | with reusable defaults: 506 | var redCircleTemplate = {color: "red", shape: "circle", radius: 3}; 507 | world.createEntity(redCircleTemplate, {x: 5, y: 5}); 508 | world.createEntity(redCircleTemplate, {x: 10, y: 5}); 509 | The options objects on the right take precedence. 510 |

Dollar Properties

511 | You can provide options that start with a $ like this: 512 | var ball = world.createEntity({color: "red", $customValue: 15}); 513 | These are passed onto the resulting entity as they are: 514 | ball.$customValue === 15 515 | This allows you to provide your own custom methods and properties. 516 | */ 517 | createEntity: function() { 518 | var o = {}; 519 | var args = Array.prototype.slice.call(arguments); 520 | args.reverse(); 521 | for (var key in args) { 522 | extend(o, args[key]); 523 | } 524 | if (this._world.IsLocked()) { 525 | this._creationQueue.push(o); 526 | return; 527 | } 528 | var entity = create(Entity); 529 | var id = this._nextEntityId++; 530 | entity._init(this, o, id); 531 | this._entities[id] = entity; 532 | return entity; 533 | }, 534 | 535 | /** 536 | * @_module world 537 | * @_params entity1, entity2, [options] 538 | * @entity1 Entity on one side of the joint 539 | * @entity2 Entity on the other side of the joint 540 | * @options 541 | * 556 | * @description Experimental joint support. 557 | * See box2d documentation for more 558 | * info. 559 | */ 560 | createJoint: function(entity1, entity2, options) { 561 | options = options || {}; 562 | options = extend(options, JOINT_DEFAULT_OPTIONS); 563 | var type = options.type; 564 | 565 | var joint; 566 | 567 | if (type === "distance") { 568 | joint = new Box2D.Dynamics.Joints.b2DistanceJointDef(); 569 | } 570 | else if (type === "revolute") { 571 | joint = new Box2D.Dynamics.Joints.b2RevoluteJointDef(); 572 | } 573 | else if (type === "gear") { 574 | joint = new Box2D.Dynamics.Joints.b2GearJointDef(); 575 | } 576 | else if (type === "friction") { 577 | joint = new Box2D.Dynamics.Joints.b2FrictionJointDef(); 578 | } 579 | else if (type === "prismatic") { 580 | joint = new Box2D.Dynamics.Joints.b2PrismaticJointDef(); 581 | } 582 | else if (type === "weld") { 583 | joint = new Box2D.Dynamics.Joints.b2WeldJointDef(); 584 | } 585 | else if (type === "pulley") { 586 | joint = new Box2D.Dynamics.Joints.b2PulleyJointDef(); 587 | } 588 | else if (type === "mouse") { 589 | joint = new Box2D.Dynamics.Joints.b2MouseJointDef(); 590 | } 591 | else if (type === "line") { 592 | joint = new Box2D.Dynamics.Joints.b2LineJointDef(); 593 | } 594 | 595 | if (options.enableMotor) { 596 | joint.enableMotor = true; 597 | } 598 | 599 | var jointPositionOnEntity1 = entity1._body.GetWorldCenter(); 600 | if (options.jointPositionOnEntity1) { 601 | jointPositionOnEntity1.x += options.jointPositionOnEntity1.x; 602 | jointPositionOnEntity1.y += options.jointPositionOnEntity1.y; 603 | } 604 | 605 | var jointPositionOnEntity2 = entity2._body.GetWorldCenter(); 606 | if (options.jointPositionOnEntity2) { 607 | jointPositionOnEntity2.x += options.jointPositionOnEntity2.x; 608 | jointPositionOnEntity2.y += options.jointPositionOnEntity2.y; 609 | } 610 | 611 | if (type === "mouse") { 612 | joint.bodyA = entity1._body; 613 | joint.bodyB = entity2._body; 614 | } 615 | else if (joint.Initialize) { 616 | joint.Initialize(entity1._body, 617 | entity2._body, 618 | jointPositionOnEntity1, 619 | jointPositionOnEntity2); 620 | } 621 | if (options.allowCollisions) { 622 | joint.collideConnected = true; 623 | } 624 | this._world.CreateJoint(joint); 625 | }, 626 | 627 | /** 628 | * @_module world 629 | * @x1 upper left of query box 630 | * @y1 upper left of query box 631 | * @x2 lower right of query box 632 | * @y2 lower right of query box 633 | * @return array of Entities. may be empty 634 | * @description find Entities in a given query box 635 | */ 636 | find: function(x1, y1, x2, y2) { 637 | if (x2 === undefined) { 638 | x2 = x1; 639 | } 640 | if (y2 === undefined) { 641 | y2 = y1; 642 | } 643 | var self = this; 644 | var result = []; 645 | var aabb = new b2AABB(); 646 | aabb.lowerBound.Set(x1, y1); 647 | aabb.upperBound.Set(x2, y2); 648 | this._world.QueryAABB(function(fixt) { 649 | result.push(self._entities[fixt.GetBody()._bbid]); 650 | return true; 651 | }, aabb); 652 | return result; 653 | }, 654 | 655 | /** 656 | * @_module world 657 | * @_params [value] 658 | * @value {x,y} 659 | * @return {x,y} 660 | * @description get or set position of camera 661 | */ 662 | camera: function(v) { 663 | v = v || {}; 664 | if (v.x === undefined && v.y === undefined) { 665 | return {x:this._cameraX, y: this._cameraY}; 666 | } 667 | if (v.x !== undefined) { 668 | this._cameraX = v.x; 669 | } 670 | if (v.y !== undefined) { 671 | this._cameraY = v.y; 672 | } 673 | }, 674 | 675 | /** 676 | * @_module world 677 | * @callback function( context ) 678 | * 682 | * @description Add an onRender callback to the World 683 | * This is useful for custom rendering. For example, to draw text 684 | * on every frame: 685 | * world.onRender(function(ctx) { 686 | *   ctx.fillText("Score: " + score, 10, 10); 687 | * }); 688 | * This callback occurs after all entities have been rendered on the 689 | * frame. 690 | *
691 | * Multiple onRender callbacks can be added, and they can be removed 692 | * with unbindOnRender. 693 | */ 694 | onRender: function(callback) { 695 | this._onRender.push({ 696 | fun: callback, 697 | ctx: this 698 | }); 699 | }, 700 | 701 | /** 702 | * @_module world 703 | * @callback callback 704 | * @description 705 | * If the provided function is currently an onRender callback for this 706 | * World, it is removed. 707 | */ 708 | unbindOnRender: function(callback) { 709 | var newArray = []; 710 | var i; 711 | for (i = 0; i < this._onRender.length; i++) { 712 | if (this._onRender[i].fun !== callback) { 713 | newArray.push(this._onRender[i]); 714 | } 715 | } 716 | this._onRender = newArray; 717 | }, 718 | 719 | /** 720 | * @_module world 721 | * @callback function() 722 | * 725 | * @description Add an onTick callback to the World 726 | *
727 | * Ticks are periodic events that happen independant of rendering. 728 | * You can use ticks as your "game loop". The default tick frequency 729 | * is 50 milliseconds, and it can be set as an option when creating 730 | * the world. 731 | *
732 | * Multiple onTick callbacks can be added, and they can be removed 733 | * with unbindOnTick. 734 | */ 735 | onTick: function(callback) { 736 | this._onTick.push({ 737 | fun: callback, 738 | ctx: this 739 | }); 740 | }, 741 | 742 | /** 743 | * @_module world 744 | * @callback callback 745 | * @description 746 | * If the provided function is currently an onTick callback for this 747 | * World, it is removed. 748 | */ 749 | unbindOnTick: function(callback) { 750 | var newArray = []; 751 | var i; 752 | for (i = 0; i < this._onTick.length; i++) { 753 | if (this._onTick[i].fun !== callback) { 754 | newArray.push(this._onTick[i]); 755 | } 756 | } 757 | this._onTick = newArray; 758 | }, 759 | 760 | /** 761 | * @_module world 762 | * @_params [value] 763 | * @value number 764 | * @return number 765 | * @description get or set the scale for rendering in pixels / meter 766 | */ 767 | scale: function(s) { 768 | if (s !== undefined) { 769 | this._scale = s; 770 | // TODO update debug draw? 771 | } 772 | return this._scale; 773 | }, 774 | 775 | /** 776 | * @_module world 777 | * @return {x,y} 778 | * @description Get a canvas position for a corresponding world position. Useful 779 | * for custom rendering in onRender. Respects world scale and camera position. 780 | */ 781 | canvasPositionAt: function(x, y) { 782 | var c = this.camera(); 783 | var s = this.scale(); 784 | 785 | return { 786 | x: Math.round((x + -c.x) * s), 787 | y: Math.round((y + -c.y) * s) 788 | }; 789 | } 790 | 791 | }; 792 | 793 | var ENTITY_DEFAULT_OPTIONS = { 794 | name: 'unnamed object', 795 | x: 10, 796 | y: 5, 797 | type: 'dynamic', // or static 798 | shape: 'square', // or circle or polygon 799 | height: 1, // for box 800 | width: 1, // for box 801 | radius: 1, // for circle 802 | points: [{x:0, y:0}, // for polygon 803 | {x:2, y:0}, 804 | {x:0, y:2}], 805 | density: 2, 806 | friction: 1, 807 | restitution: 0.2, // bounciness 808 | active: true, // participates in collision and dynamics 809 | rotation: null, 810 | fixedRotation: false, 811 | bullet: false, // perform expensive continuous collision detection 812 | maxVelocityX: 1000, 813 | maxVelocityY: 1000, 814 | image: null, 815 | imageOffsetX: 0, 816 | imageOffsetY: 0, 817 | imageStretchToFit: null, 818 | color: 'gray', 819 | borderColor: 'black', 820 | borderWidth: 1, 821 | spriteSheet: false, 822 | spriteWidth: 16, 823 | spriteHeight: 16, 824 | spriteX: 0, 825 | spriteY: 0, 826 | init: null, 827 | draw: function(ctx, x, y) { 828 | var cameraOffsetX = -this._world._cameraX; 829 | var cameraOffsetY = -this._world._cameraY; 830 | ctx.fillStyle = this._ops.color; 831 | ctx.strokeStyle = this._ops.borderColor; 832 | ctx.lineWidth = this._ops.borderWidth; 833 | var i; 834 | var scale = this._world._scale; 835 | var collisionOutlines = this._world._ops.collisionOutlines; 836 | var ox = this._ops.imageOffsetX || 0; 837 | var oy = this._ops.imageOffsetY || 0; 838 | ox *= scale; 839 | oy *= scale; 840 | if (this._sprite !== undefined) { 841 | var width; 842 | var height; 843 | if (this._ops.shape === "circle" && this._ops.imageStretchToFit) { 844 | width = height = this._ops.radius * 2; 845 | x -= this._ops.radius / 2 * scale; 846 | y -= this._ops.radius / 2 * scale; 847 | } 848 | else if (this._ops.imageStretchToFit) { 849 | width = this._ops.width; 850 | height = this._ops.height; 851 | } 852 | else if (this._ops.spriteSheet) { 853 | width = this._ops.spriteWidth / 30; 854 | height = this._ops.spriteHeight / 30; 855 | } 856 | else { 857 | width = this._sprite.width / 30; 858 | height = this._sprite.height / 30; 859 | } 860 | 861 | var tx = ox + (x + width / 4 * scale); 862 | var ty = oy + (y + height / 4 * scale); 863 | 864 | ctx.translate(tx, ty); 865 | 866 | ctx.rotate(this._body.GetAngle()); 867 | 868 | if (this._ops.spriteSheet) { 869 | ctx.drawImage(this._sprite, 870 | this._ops.spriteX * this._ops.spriteWidth, 871 | this._ops.spriteY * this._ops.spriteHeight, 872 | this._ops.spriteWidth, 873 | this._ops.spriteHeight, 874 | -(width / 2 * scale), 875 | -(height / 2 * scale), 876 | width * scale, 877 | height * scale); 878 | } 879 | else { 880 | ctx.drawImage(this._sprite, 881 | -(width / 2 * scale), 882 | -(height / 2 * scale), 883 | width * scale, 884 | height * scale); 885 | } 886 | 887 | ctx.rotate(0 - this._body.GetAngle()); 888 | 889 | ctx.translate(-tx, -ty); 890 | 891 | } 892 | 893 | if (this._sprite && !collisionOutlines) { 894 | return; 895 | } 896 | 897 | if (collisionOutlines) { 898 | if (this._sprite !== undefined) { 899 | ctx.fillStyle = "transparent"; 900 | } 901 | ctx.strokeStyle = "rgb(255, 0, 255)"; 902 | ctx.lineWidth = 2; 903 | } 904 | 905 | if (this._ops.shape === 'polygon' || this._ops.shape === 'square') { 906 | var poly = this._body.GetFixtureList().GetShape(); 907 | var vertexCount = parseInt(poly.GetVertexCount(), 10); 908 | var localVertices = poly.GetVertices(); 909 | var vertices = new Vector(vertexCount); 910 | var xf = this._body.m_xf; 911 | for (i = 0; i < vertexCount; ++i) { 912 | vertices[i] = b2Math.MulX(xf, localVertices[i]); 913 | } 914 | ctx.beginPath(); 915 | ctx.moveTo((cameraOffsetX + vertices[0].x) * scale, (cameraOffsetY + vertices[0].y) * scale); 916 | for (i = 1; i < vertices.length; i++) { 917 | ctx.lineTo((cameraOffsetX + vertices[i].x) * scale, (cameraOffsetY + vertices[i].y) * scale); 918 | } 919 | ctx.closePath(); 920 | if (this._ops.borderWidth !== 0 || collisionOutlines) { 921 | ctx.stroke(); 922 | } 923 | ctx.fill(); 924 | } 925 | else if (this._ops.shape === 'circle') { 926 | var p = this.position(); 927 | ctx.beginPath(); 928 | ctx.arc((cameraOffsetX + p.x) * scale, 929 | (cameraOffsetY + p.y) * scale, 930 | this._ops.radius * scale, 931 | 0, 932 | Math.PI * 2, true); 933 | ctx.closePath(); 934 | if (this._ops.borderWidth !== 0 || collisionOutlines) { 935 | ctx.stroke(); 936 | } 937 | ctx.fill(); 938 | } 939 | } 940 | }; 941 | 942 | /** 943 | * @header 944 | * @description a single physical object in the physics simulation 945 | */ 946 | var Entity = { 947 | 948 | _id: null, 949 | _ops: null, 950 | _body: null, 951 | _world: null, 952 | 953 | _init: function(world, options, id) { 954 | var ops; 955 | var op; 956 | 957 | if (options && options.components !== undefined) { 958 | options.components.reverse(); 959 | options.components.forEach(function(component) { 960 | extend(options, component); 961 | }); 962 | } 963 | 964 | this._ops = extend(options, ENTITY_DEFAULT_OPTIONS); 965 | ops = this._ops; 966 | 967 | this._body = new b2BodyDef(); 968 | var body = this._body; 969 | 970 | this._world = world; 971 | this._id = id; 972 | 973 | // $ props 974 | for (op in this._ops) { 975 | if (op.match(/^\$/)) { 976 | this[op] = this._ops[op]; 977 | } 978 | } 979 | 980 | var fixture = new b2FixtureDef(); 981 | fixture.density = ops.density; 982 | fixture.friction = ops.friction; 983 | fixture.restitution = ops.restitution; 984 | 985 | body.position.x = ops.x; 986 | body.position.y = ops.y; 987 | 988 | this._name = ops.name; 989 | 990 | // type 991 | if (ops.type === 'static') { 992 | body.type = b2Body.b2_staticBody; 993 | } 994 | else if (ops.type === 'dynamic') { 995 | body.type = b2Body.b2_dynamicBody; 996 | } 997 | 998 | // shape 999 | if (ops.shape === 'square') { 1000 | fixture.shape = new shapes.b2PolygonShape(); 1001 | // box2d asks for "half the width", we ask for the actual width 1002 | fixture.shape.SetAsBox(ops.width / 2, ops.height / 2); 1003 | } 1004 | else if (ops.shape === 'circle') { 1005 | fixture.shape = new shapes.b2CircleShape(ops.radius); 1006 | } 1007 | else if (ops.shape === 'polygon') { 1008 | fixture.shape = new shapes.b2PolygonShape(); 1009 | fixture.shape.SetAsArray(ops.points, ops.points.length); 1010 | } 1011 | 1012 | // rotation 1013 | if (ops.rotation) { 1014 | body.angle = ops.rotation / DEGREES_PER_RADIAN; 1015 | } 1016 | 1017 | // rendering stuff 1018 | if (ops.draw) { 1019 | this._draw = ops.draw; 1020 | } 1021 | if (ops.image) { 1022 | this._sprite = new Image(); 1023 | this._sprite.src = ops.image; 1024 | } 1025 | 1026 | body.active = ops.active; 1027 | body.fixedRotation = ops.fixedRotation; 1028 | body.bullet = ops.bullet; 1029 | 1030 | this._body = world._world.CreateBody(body); 1031 | this._body.CreateFixture(fixture); 1032 | this._body._bbid = id; 1033 | 1034 | // events 1035 | if (ops.onStartContact) { 1036 | this._world._addStartContactHandler(id, ops.onStartContact); 1037 | } 1038 | if (ops.onFinishContact) { 1039 | this._world._addFinishContactHandler(id, ops.onFinishContact); 1040 | } 1041 | if (ops.onImpact) { 1042 | this._world._addImpactHandler(id, ops.onImpact); 1043 | } 1044 | if (ops.onKeyDown) { 1045 | this._world._addKeydownHandler(id, ops.onKeyDown); 1046 | } 1047 | if (ops.onKeyUp) { 1048 | this._world._addKeyupHandler(id, ops.onKeyUp); 1049 | } 1050 | if (ops.onRender) { 1051 | this.onRender(ops.onRender); 1052 | } 1053 | if (ops.onTick) { 1054 | this.onTick(ops.onTick); 1055 | } 1056 | 1057 | // custom init function 1058 | if (ops.init) { 1059 | ops.init.call(this); 1060 | } 1061 | }, 1062 | 1063 | // returns a vector. params can be either of the following: 1064 | // power, x, y 1065 | // power, degrees 1066 | _toVector: function(power, a, b) { 1067 | var x; 1068 | var y; 1069 | a = a || 0; 1070 | if (b === undefined) { 1071 | a -= 90; 1072 | x = Math.cos(a * (Math.PI / 180)) * power; 1073 | y = Math.sin(a * (Math.PI / 180)) * power; 1074 | } 1075 | else { 1076 | x = a * power; 1077 | y = b * power; 1078 | } 1079 | return {x:x,y:y}; 1080 | }, 1081 | 1082 | /** 1083 | * @_module entity 1084 | * @_params [value] 1085 | * @return entity name 1086 | * @description get or set entity name 1087 | */ 1088 | name: function(value) { 1089 | if (value !== undefined) { 1090 | this._name = value; 1091 | } 1092 | return this._name; 1093 | }, 1094 | 1095 | /** 1096 | * @_module entity 1097 | * @_params [value] 1098 | * @value {x,y} 1099 | * @return {x,y} 1100 | * @description get or set entity position 1101 | */ 1102 | position: function(value) { 1103 | if (value !== undefined) { 1104 | if (this._world._world.IsLocked()) { 1105 | this._world._positionQueue.push({ 1106 | o: this, 1107 | val: value 1108 | }); 1109 | } 1110 | else { 1111 | this._body.SetPosition(new b2Vec2(value.x, value.y)); 1112 | } 1113 | } 1114 | var p = this._body.GetPosition(); 1115 | return {x: p.x, y: p.y}; 1116 | }, 1117 | 1118 | /** 1119 | * @_module entity 1120 | * @_params 1121 | * @return {x,y} 1122 | * @description Get the Entity position in pixels. Useful for custom 1123 | * rendering. Unlike position the result 1124 | * is relative to the World's scale and camera position. 1125 | */ 1126 | canvasPosition: function(value) { 1127 | if (value !== undefined) { 1128 | // TODO set 1129 | } 1130 | 1131 | var p = this.position(); 1132 | 1133 | return this._world.canvasPositionAt(p.x, p.y); 1134 | }, 1135 | 1136 | /** 1137 | * @_module entity 1138 | * @_params [value] 1139 | * @value degrees 1140 | * @return degrees 1141 | * @description get or set entity rotation 1142 | */ 1143 | rotation: function(value) { 1144 | if (value !== undefined) { 1145 | this._body.SetAngle(value / DEGREES_PER_RADIAN); 1146 | } 1147 | return this._body.GetAngle() * DEGREES_PER_RADIAN; 1148 | }, 1149 | 1150 | /** 1151 | * @_module entity 1152 | * @_params [value] 1153 | * @value number 1154 | * @return number 1155 | * @description get or set entity friction 1156 | */ 1157 | friction: function(value) { 1158 | if (value !== undefined) { 1159 | this._body.GetFixtureList().SetFriction(value); 1160 | } 1161 | return this._body.GetFixtureList().GetFriction(); 1162 | }, 1163 | 1164 | /** 1165 | * @_module entity 1166 | * @_params [value] 1167 | * @value number 1168 | * @return number 1169 | * @description get or set entity restitution (bounciness) 1170 | */ 1171 | restitution: function(value) { 1172 | if (value !== undefined) { 1173 | this._body.GetFixtureList().SetRestitution(value); 1174 | } 1175 | return this._body.GetFixtureList().GetRestitution(); 1176 | }, 1177 | 1178 | /** 1179 | * @_module entity 1180 | * @_params [value] 1181 | * @value number 1182 | * @return number 1183 | * @description get or set entity max velocity left or right 1184 | */ 1185 | maxVelocityX: function(value) { 1186 | if (value !== undefined) { 1187 | this._ops.maxVelocityX = value; 1188 | } 1189 | return this._ops.maxVelocityX; 1190 | }, 1191 | 1192 | /** 1193 | * @_module entity 1194 | * @_params [value] 1195 | * @value number 1196 | * @return number 1197 | * @description get or set entity max velocity up or down 1198 | */ 1199 | maxVelocityY: function(value) { 1200 | if (value !== undefined) { 1201 | this._ops.maxVelocityY = value; 1202 | } 1203 | return this._ops.maxVelocityY; 1204 | }, 1205 | 1206 | /** 1207 | * @_module entity 1208 | * @_params [value] 1209 | * @value string 1210 | * @return string 1211 | * @description get or set entity image 1212 | */ 1213 | image: function(value) { 1214 | if (value !== undefined) { 1215 | this._sprite = new Image(); 1216 | this._sprite.src = value; 1217 | } 1218 | return this._sprite.src; 1219 | }, 1220 | 1221 | /** 1222 | * @_module entity 1223 | * @_params [value] 1224 | * @value number 1225 | * @return number 1226 | * @description get or set entity image offset in the x direction 1227 | */ 1228 | imageOffsetX: function(value) { 1229 | if (value !== undefined) { 1230 | this._ops.imageOffsetX = value; 1231 | } 1232 | return this._ops.imageOffsetX; 1233 | }, 1234 | 1235 | /** 1236 | * @_module entity 1237 | * @_params [value] 1238 | * @value number 1239 | * @return number 1240 | * @description get or set entity image offset in the y direction 1241 | */ 1242 | imageOffsetY: function(value) { 1243 | if (value !== undefined) { 1244 | this._ops.imageOffsetY = value; 1245 | } 1246 | return this._ops.imageOffsetY; 1247 | }, 1248 | 1249 | /** 1250 | * @_module entity 1251 | * @_params [value] 1252 | * @value boolean 1253 | * @return boolean 1254 | * @description set to true to stretch image to entity size 1255 | */ 1256 | imageStretchToFit: function(value) { 1257 | if (value !== undefined) { 1258 | this._ops.imageStretchToFit = value; 1259 | } 1260 | return this._ops.imageStretchToFit; 1261 | }, 1262 | 1263 | /** 1264 | * @_module entity 1265 | * @_params [value] 1266 | * @value css color string 1267 | * @return css color string 1268 | * @description get or set entity's color 1269 | */ 1270 | color: function(value) { 1271 | if (value !== undefined) { 1272 | this._ops.color = value; 1273 | } 1274 | return this._ops.color; 1275 | }, 1276 | 1277 | /** 1278 | * @_module entity 1279 | * @_params [value] 1280 | * @value css color string 1281 | * @return css color string 1282 | * @description get or set entity's border color 1283 | */ 1284 | borderColor: function(value) { 1285 | if (value !== undefined) { 1286 | this._ops.borderColor = value; 1287 | } 1288 | return this._ops.borderColor; 1289 | }, 1290 | 1291 | /** 1292 | * @_module entity 1293 | * @_params [value] 1294 | * @value number 1295 | * @return number 1296 | * @description get or set entity's border width. 1297 | * Set to 0 to not show a border. 1298 | */ 1299 | borderWidth: function(value) { 1300 | if (value !== undefined) { 1301 | this._ops.borderWidth = value; 1302 | } 1303 | return this._ops.borderWidth; 1304 | }, 1305 | 1306 | /** 1307 | * @_module entity 1308 | * @_params [value] 1309 | * @value boolean 1310 | * @return boolean 1311 | * @description get or set if this entity's image is a sprite sheet 1312 | */ 1313 | spriteSheet: function(value) { 1314 | if (value !== undefined) { 1315 | this._ops.spriteSheet = value; 1316 | } 1317 | return this._ops.spriteSheet; 1318 | }, 1319 | 1320 | /** 1321 | * @_module entity 1322 | * @_params [value] 1323 | * @value number 1324 | * @return number 1325 | * @description get or set width of a sprite on the sprite sheet 1326 | */ 1327 | spriteWidth: function(value) { 1328 | if (value !== undefined) { 1329 | this._ops.spriteWidth = value; 1330 | } 1331 | return this._ops.spriteWidth; 1332 | }, 1333 | 1334 | /** 1335 | * @_module entity 1336 | * @_params [value] 1337 | * @value number 1338 | * @return number 1339 | * @description get or set height of a sprite on the sprite sheet 1340 | */ 1341 | spriteHeight: function(value) { 1342 | if (value !== undefined) { 1343 | this._ops.spriteHeight = value; 1344 | } 1345 | return this._ops.spriteHeight; 1346 | }, 1347 | 1348 | /** 1349 | * @_module entity 1350 | * @_params [value] 1351 | * @value function 1352 | * @return function 1353 | * @description get or set the draw function for this entity 1354 | */ 1355 | draw: function(value) { 1356 | if (value !== undefined) { 1357 | this._draw = value; 1358 | } 1359 | return this._draw; 1360 | }, 1361 | 1362 | /** 1363 | * @_module entity 1364 | * @description destroy this entity and remove it from the world 1365 | */ 1366 | destroy: function() { 1367 | this._destroyed = true; 1368 | this._world._destroy(this); 1369 | }, 1370 | 1371 | /** 1372 | * @_module entity 1373 | * @power of impulse 1374 | * @degrees direction of force. 0 is up, 90 is right, etc. 1375 | * @_params power, degrees 1376 | * @description Apply an instantanious force on this Entity. 1377 | *
1378 | * With this and all functions that take degrees, you can also provide 1379 | * a vector. 1380 | * entity.applyImpulse(10, 45); // 45 degree angle 1381 | * entity.applyImpulse(10, 1, -1); // the vector x=1 y=-1} 1382 | */ 1383 | applyImpulse: function(power, a, b) { 1384 | var v = this._toVector(power, a, b); 1385 | this._world._applyImpulse(this._id, this._body, v.x, v.y); 1386 | }, 1387 | 1388 | /** 1389 | * @_module entity 1390 | * @name of force 1391 | * @power of force 1392 | * @degrees direction of force 1393 | * @_params name, power, degrees 1394 | * @description Apply a constant force on this Entity. Can be removed later 1395 | * using clearForce. 1396 | */ 1397 | setForce: function(name, power, a, b) { 1398 | var v = this._toVector(power, a, b); 1399 | this._world._setConstantForce(name, this._id, this._body, v.x, v.y); 1400 | }, 1401 | 1402 | /** 1403 | * @_module entity 1404 | * @name of velocity 1405 | * @power of velocity 1406 | * @degrees direction of velocity 1407 | * @_params name, power, degrees 1408 | * @description Continuously override velocity of this Entity. Can be removed later 1409 | * using clearVelocity. Usually you probably want setForce or applyImpulse. 1410 | */ 1411 | setVelocity: function(name, power, a, b) { 1412 | var v = this._toVector(power, a, b); 1413 | this._world._setConstantVelocity(name, this._id, this._body, v.x, v.y); 1414 | }, 1415 | 1416 | /** 1417 | * @_module entity 1418 | * @description Stop the force with the given name. 1419 | */ 1420 | clearForce: function(name) { 1421 | this._world._clearConstantForce(name, this._id); 1422 | }, 1423 | 1424 | /** 1425 | * @_module entity 1426 | * @description Stop the constant velocity with the given name. 1427 | */ 1428 | clearVelocity: function(name) { 1429 | this._world._clearConstantVelocity(name, this._id); 1430 | }, 1431 | 1432 | /** 1433 | * @_module entity 1434 | * @callback function( e ) 1435 | * 1439 | * @description Handle keydown event for this entity. 1440 | */ 1441 | onKeydown: function(callback) { 1442 | this._world._addKeydownHandler(this._id, callback); 1443 | }, 1444 | 1445 | /** 1446 | * @_module entity 1447 | * @callback function( e ) 1448 | * 1452 | * @description Handle keyup event for this entity. 1453 | */ 1454 | onKeyup: function(callback) { 1455 | this._world._addKeyupHandler(this._id, callback); 1456 | }, 1457 | 1458 | /** 1459 | * @_module entity 1460 | * @callback function( entity ) 1461 | * 1465 | * @description Handle start of contact with another entity. 1466 | */ 1467 | onStartContact: function(callback) { 1468 | this._world._addStartContactHandler(this._id, callback); 1469 | }, 1470 | 1471 | /** 1472 | * @_module entity 1473 | * @callback function( entity ) 1474 | * 1478 | * @description Handle end of contact with another entity. 1479 | */ 1480 | onFinishContact: function(callback) { 1481 | this._world._addFinishContactHandler(this._id, callback); 1482 | }, 1483 | 1484 | /** 1485 | * @_module entity 1486 | * @callback function( entity, normalForce, tangentialForce ) 1487 | * 1493 | * @description Handle impact with another entity. 1494 | */ 1495 | onImpact: function(callback) { 1496 | this._world._addImpactHandler(this._id, callback); 1497 | }, 1498 | 1499 | /** 1500 | * @_module entity 1501 | * @callback function( context ) 1502 | * 1506 | * @description Add an onRender callback to this Entity 1507 | *
1508 | * Multiple onRender callbacks can be added, and they can be removed 1509 | * with world.unbindOnRender. 1510 | */ 1511 | onRender: function(callback) { 1512 | this._world._onRender.push({ 1513 | fun: callback, 1514 | ctx: this 1515 | }); 1516 | }, 1517 | 1518 | /** 1519 | * @_module entity 1520 | * @callback function() 1521 | * 1524 | * @description Add an onTick callback to this Entity 1525 | *
1526 | * Ticks are periodic events that happen independant of rendering. 1527 | * You can use ticks as your "game loop". The default tick frequency 1528 | * is 50 milliseconds, and it can be set as an option when creating 1529 | * the world. 1530 | *
1531 | * Multiple onTick callbacks can be added, and they can be removed 1532 | * with world.unbindOnTick. 1533 | */ 1534 | onTick: function(callback) { 1535 | this._world._onTick.push({ 1536 | fun: callback, 1537 | ctx: this 1538 | }); 1539 | }, 1540 | 1541 | /** 1542 | * @_module entity 1543 | * @description Set the entity's image to the sprite at x, y on the sprite sheet. 1544 | * Used only on entities with spriteSheet:true 1545 | */ 1546 | sprite: function(x, y) { 1547 | this._ops.spriteX = x; 1548 | this._ops.spriteY = y; 1549 | } 1550 | 1551 | }; 1552 | 1553 | }()); 1554 | -------------------------------------------------------------------------------- /boxbox.min.js: -------------------------------------------------------------------------------- 1 | /* Copyright 2012 Greg Smith. Licensed under the MIT License. http://incompl.github.com/boxbox/ */ 2 | (function(){var a=0,b=["ms","moz","webkit","o"];for(var c=0;cb._ops.maxVelocityX&&(c.x=b._ops.maxVelocityX),c.x<-b._ops.maxVelocityX&&(c.x=-b._ops.maxVelocityX),c.y>b._ops.maxVelocityY&&(c.y=b._ops.maxVelocityY),c.y<-b._ops.maxVelocityY&&(c.y=-b._ops.maxVelocityY);for(g=0;g 2 | 3 | 4 | 5 | boxbox demo 6 | 30 | 31 | 32 |
33 |
34 | An iPhone Game For Parakeets 35 |
36 | 37 | 38 | your browser doesn't support canvas 39 | 40 | 41 | 42 | 43 | 44 | 45 | -------------------------------------------------------------------------------- /demos/chain/chain.js: -------------------------------------------------------------------------------- 1 | (function() { 2 | 3 | var canvas = document.getElementById('bbdemo'); 4 | 5 | var world = boxbox.createWorld(canvas, {gravity: {x: 0, y: 20}}); 6 | 7 | var thisEntity; 8 | var previousEntity; 9 | 10 | var linkTemplate = { 11 | width: .4, 12 | height: 1, 13 | image: "parakeet chain.jpg", 14 | imageStretchToFit: true 15 | }; 16 | 17 | var x = 4.2; 18 | 19 | thisEntity = world.createEntity(linkTemplate, {x: x, y: -1, type: "static"}); 20 | 21 | var y; 22 | for (y = 0; y < 8; y++) { 23 | 24 | previousEntity = thisEntity; 25 | thisEntity = world.createEntity(linkTemplate, {x: x, y: y}); 26 | 27 | world.createJoint(previousEntity, thisEntity, { 28 | type: "revolute", 29 | jointPositionOnEntity1: {x:0, y:.25}, 30 | jointPositionOnEntity2: {x:0, y:.25} 31 | }); 32 | 33 | } 34 | 35 | previousEntity = thisEntity; 36 | thisEntity = world.createEntity({ 37 | x: x, 38 | y: y, 39 | image: "parakeet toy.gif", 40 | imageOffsetX: -1.5, 41 | imageOffsetY: -.3, 42 | density: 5, 43 | width: 2, 44 | height: 2 45 | }); 46 | 47 | world.createJoint(previousEntity, thisEntity, { 48 | type: "revolute" 49 | }); 50 | 51 | window.peck = function() { 52 | thisEntity.applyImpulse(150, Number(Math.random() * 360)); 53 | } 54 | 55 | })(); 56 | 57 | -------------------------------------------------------------------------------- /demos/chain/iphone.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/incompl/boxbox/51a79223ba039a023741160c1c4f9ff9cfe3b515/demos/chain/iphone.gif -------------------------------------------------------------------------------- /demos/chain/parakeet chain.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/incompl/boxbox/51a79223ba039a023741160c1c4f9ff9cfe3b515/demos/chain/parakeet chain.jpg -------------------------------------------------------------------------------- /demos/chain/parakeet toy.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/incompl/boxbox/51a79223ba039a023741160c1c4f9ff9cfe3b515/demos/chain/parakeet toy.gif -------------------------------------------------------------------------------- /demos/platformer/demo.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | boxbox demo 6 | 23 | 24 | 25 | 26 | your browser doesn't support canvas 27 | 28 |
29 | arrow keys to move 30 |
31 | space to jump 32 |
33 | health: 100 34 |
35 | score: 0 36 |
37 | 38 | 39 | 40 | 41 | -------------------------------------------------------------------------------- /demos/platformer/demo.js: -------------------------------------------------------------------------------- 1 | (function() { 2 | 3 | var canvas = document.getElementById('bbdemo'); 4 | 5 | var world = boxbox.createWorld(canvas, {debugDraw:false}); 6 | 7 | player = world.createEntity({ 8 | name: 'player', 9 | x: .5, 10 | y: 12, 11 | height: .4, 12 | width: .4, 13 | fixedRotation: true, 14 | friction: .3, 15 | restitution: 0, 16 | color: 'blue', 17 | maxVelocityX: 4 18 | }); 19 | 20 | var health = 100; 21 | 22 | var score = 0; 23 | 24 | function damage(x) { 25 | if (player._destroyed) { 26 | return; 27 | } 28 | health -= Math.round(x); 29 | if (health < 1) { 30 | health = 0; 31 | player.destroy(); 32 | alert('Game over.'); 33 | } 34 | document.getElementById('health').innerHTML = health; 35 | } 36 | 37 | function addScore(x) { 38 | score += Math.round(x); 39 | document.getElementById('score').innerHTML = '' + score; 40 | } 41 | 42 | player.onKeydown(function(e) { 43 | 44 | if (this._destroyed) { 45 | return; 46 | } 47 | 48 | var i; 49 | var obj; 50 | var player = this; 51 | 52 | // determine what you're standing on 53 | var standingOn; 54 | var pos = this.position(); 55 | var allUnderMe = world.find(pos.x - .08, pos.y + .1, pos.x + .09, pos.y + .105); 56 | for (i = 0; i < allUnderMe.length; i++) { 57 | obj = allUnderMe[i]; 58 | if (obj !== player) { 59 | standingOn = obj; 60 | break; 61 | } 62 | } 63 | 64 | // jump 65 | if (e.keyCode === 32 && standingOn) { 66 | this.applyImpulse(2); 67 | return false; 68 | } 69 | 70 | // when airborn movement is restricted 71 | var force = 8; 72 | if (!standingOn) { 73 | force = force / 2; 74 | } 75 | 76 | // move left 77 | if (e.keyCode === 37) { 78 | this.setForce('movement', force, 270); 79 | this.friction(.1); 80 | return false; 81 | } 82 | 83 | // move right 84 | if (e.keyCode === 39) { 85 | this.setForce('movement', force, 90); 86 | this.friction(.1); 87 | return false; 88 | } 89 | 90 | }); 91 | 92 | player.onKeyup(function(e) { 93 | 94 | if (this._destroyed) { 95 | return; 96 | } 97 | 98 | // clear movement force on arrow keyup 99 | if (e.keyCode === 37 || e.keyCode === 39) { 100 | this.clearForce('movement'); 101 | this.friction(3); 102 | return false; 103 | } 104 | 105 | }); 106 | 107 | player.onImpact(function(other, power, tangentPower) { 108 | if (power > 3) { 109 | damage(power - 3); 110 | } 111 | }); 112 | 113 | world.onRender(function(ctx) { 114 | 115 | // update camera position every draw 116 | var p = player.position(); 117 | var c = this.camera(); 118 | 119 | if (p.y < 14) { 120 | if (p.x - 8 < c.x) { 121 | this.camera({x: player.position().x - 8}); 122 | } 123 | else if (p.x - 12 > c.x) { 124 | this.camera({x: player.position().x - 12}); 125 | } 126 | } 127 | 128 | // If you fall off the world, zoom out 129 | else { 130 | var scale = 30; 131 | scale -= (p.y - 14); 132 | scale = scale < 1 ? 1 : scale; 133 | this.scale(scale); 134 | 135 | var newCameraX = c.x; 136 | if (newCameraX > -9 || newCameraX < -11) { 137 | if (newCameraX > -10) { 138 | newCameraX = newCameraX - .3; 139 | } 140 | if (newCameraX < -10) { 141 | newCameraX = newCameraX + .3; 142 | } 143 | this.camera({x: newCameraX}); 144 | } 145 | } 146 | 147 | // Rendering for the joint between the two wheels 148 | var p1 = wheel1.canvasPosition(); 149 | var p2 = wheel2.canvasPosition(); 150 | ctx.strokeStyle = "black"; 151 | ctx.lineWidth = 3; 152 | ctx.beginPath(); 153 | ctx.moveTo(p1.x, p1.y); 154 | ctx.lineTo(p2.x, p2.y); 155 | ctx.stroke(); 156 | }); 157 | 158 | var groundTemplate = { 159 | name: 'ground', 160 | type: 'static', 161 | height: .2, 162 | color: 'green', 163 | borderColor: 'rgba(0, 100, 0, .5)', 164 | borderWidth: 3 165 | }; 166 | 167 | world.createEntity(groundTemplate, {width: 20, x: 10, y: 13.22}); 168 | 169 | world.createEntity(groundTemplate, {width: 6, x: 3, y: 5}); 170 | 171 | world.createEntity(groundTemplate, {width: 8, x: 16, y: 5}); 172 | 173 | world.createEntity({ 174 | name: 'square', 175 | x: 13, 176 | y: 8, 177 | height: 1.6, 178 | width: .4, 179 | imageOffsetY: -.2 180 | }); 181 | 182 | world.createEntity({ 183 | name: 'circle', 184 | shape: 'circle', 185 | radius: 2, 186 | x: 14, 187 | y: 3, 188 | density: .5, 189 | image: 'wheel.png', 190 | imageStretchToFit: true 191 | }); 192 | 193 | world.createEntity({ 194 | name: 'poly', 195 | shape: 'polygon', 196 | x: 5, 197 | y: 8 198 | }); 199 | 200 | // Car thing 201 | var wheelTemplate = { 202 | name: 'wheel', 203 | shape: 'circle', 204 | radius: 1, 205 | image: 'wheel.png', 206 | imageStretchToFit: true 207 | }; 208 | var wheel1 = world.createEntity(wheelTemplate, {x: 1, y:1}); 209 | var wheel2 = world.createEntity(wheelTemplate, {x: 4, y:1}); 210 | world.createJoint(wheel1, wheel2); 211 | 212 | var platform = world.createEntity({ 213 | name: 'platform', 214 | fixedRotation: true, 215 | height: .2, 216 | width: 2 217 | }); 218 | 219 | var platformMovingUp = true; 220 | 221 | window.setInterval(function() { 222 | platformMovingUp = !platformMovingUp; 223 | if (platformMovingUp) { 224 | platform.setVelocity('moving platform', 5, 0); 225 | } 226 | else { 227 | platform.setVelocity('moving platform', 5, 180); 228 | } 229 | }, 1500); 230 | 231 | var coinTemplate = { 232 | name: 'coin', 233 | shape: 'circle', 234 | radius: .1, 235 | color: 'yellow', 236 | onStartContact: function(other) { 237 | if (other.name() === 'player') { 238 | addScore(100); 239 | this.destroy(); 240 | } 241 | } 242 | }; 243 | 244 | world.createEntity(coinTemplate, {x: 2, y: 4}); 245 | 246 | world.createEntity(coinTemplate, {x: 2, y: 12}); 247 | 248 | world.createEntity(coinTemplate, { 249 | x: 16, 250 | y: 5, 251 | shape: 'square', 252 | height: .2, 253 | width: .2 254 | }); 255 | 256 | })(); -------------------------------------------------------------------------------- /demos/platformer/wheel.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/incompl/boxbox/51a79223ba039a023741160c1c4f9ff9cfe3b515/demos/platformer/wheel.png -------------------------------------------------------------------------------- /grunt.js: -------------------------------------------------------------------------------- 1 | module.exports = function(grunt) { 2 | 3 | grunt.initConfig({ 4 | meta: { 5 | banner: '/* Copyright 2012 Greg Smith. Licensed under the MIT License. http://incompl.github.com/boxbox/ */' 6 | }, 7 | min: { 8 | 'boxbox.min.js': ['', 'boxbox.js'] 9 | }, 10 | lint: { 11 | files: ['grunt.js', 'boxbox.js'] 12 | }, 13 | watch: { 14 | files: ['boxbox.js'], 15 | tasks: 'min' 16 | }, 17 | qunit: { 18 | all: ['test/index.html'] 19 | }, 20 | jshint: { 21 | options: { 22 | curly: true, 23 | eqeqeq: true, 24 | immed: true, 25 | latedef: false, 26 | newcap: false, // for box2dweb code 27 | noarg: true, 28 | sub: true, 29 | undef: true, 30 | eqnull: true, 31 | browser: true 32 | }, 33 | globals: { 34 | Box2D: true, 35 | Vector: true, 36 | module: true, 37 | console: true 38 | } 39 | }, 40 | uglify: {} 41 | }); 42 | 43 | grunt.registerTask('default', 'lint min'); 44 | 45 | }; -------------------------------------------------------------------------------- /test/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | BoxBox Test Suite 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 |
20 | 21 | 22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /test/lib/qunit-1.5.0.css: -------------------------------------------------------------------------------- 1 | /** 2 | * QUnit v1.5.0 - A JavaScript Unit Testing Framework 3 | * 4 | * http://docs.jquery.com/QUnit 5 | * 6 | * Copyright (c) 2012 John Resig, Jörn Zaefferer 7 | * Dual licensed under the MIT (MIT-LICENSE.txt) 8 | * or GPL (GPL-LICENSE.txt) licenses. 9 | */ 10 | 11 | /** Font Family and Sizes */ 12 | 13 | #qunit-tests, #qunit-header, #qunit-banner, #qunit-testrunner-toolbar, #qunit-userAgent, #qunit-testresult { 14 | font-family: "Helvetica Neue Light", "HelveticaNeue-Light", "Helvetica Neue", Calibri, Helvetica, Arial, sans-serif; 15 | } 16 | 17 | #qunit-testrunner-toolbar, #qunit-userAgent, #qunit-testresult, #qunit-tests li { font-size: small; } 18 | #qunit-tests { font-size: smaller; } 19 | 20 | 21 | /** Resets */ 22 | 23 | #qunit-tests, #qunit-tests ol, #qunit-header, #qunit-banner, #qunit-userAgent, #qunit-testresult { 24 | margin: 0; 25 | padding: 0; 26 | } 27 | 28 | 29 | /** Header */ 30 | 31 | #qunit-header { 32 | padding: 0.5em 0 0.5em 1em; 33 | 34 | color: #8699a4; 35 | background-color: #0d3349; 36 | 37 | font-size: 1.5em; 38 | line-height: 1em; 39 | font-weight: normal; 40 | 41 | border-radius: 15px 15px 0 0; 42 | -moz-border-radius: 15px 15px 0 0; 43 | -webkit-border-top-right-radius: 15px; 44 | -webkit-border-top-left-radius: 15px; 45 | } 46 | 47 | #qunit-header a { 48 | text-decoration: none; 49 | color: #c2ccd1; 50 | } 51 | 52 | #qunit-header a:hover, 53 | #qunit-header a:focus { 54 | color: #fff; 55 | } 56 | 57 | #qunit-header label { 58 | display: inline-block; 59 | } 60 | 61 | #qunit-banner { 62 | height: 5px; 63 | } 64 | 65 | #qunit-testrunner-toolbar { 66 | padding: 0.5em 0 0.5em 2em; 67 | color: #5E740B; 68 | background-color: #eee; 69 | } 70 | 71 | #qunit-userAgent { 72 | padding: 0.5em 0 0.5em 2.5em; 73 | background-color: #2b81af; 74 | color: #fff; 75 | text-shadow: rgba(0, 0, 0, 0.5) 2px 2px 1px; 76 | } 77 | 78 | 79 | /** Tests: Pass/Fail */ 80 | 81 | #qunit-tests { 82 | list-style-position: inside; 83 | } 84 | 85 | #qunit-tests li { 86 | padding: 0.4em 0.5em 0.4em 2.5em; 87 | border-bottom: 1px solid #fff; 88 | list-style-position: inside; 89 | } 90 | 91 | #qunit-tests.hidepass li.pass, #qunit-tests.hidepass li.running { 92 | display: none; 93 | } 94 | 95 | #qunit-tests li strong { 96 | cursor: pointer; 97 | } 98 | 99 | #qunit-tests li a { 100 | padding: 0.5em; 101 | color: #c2ccd1; 102 | text-decoration: none; 103 | } 104 | #qunit-tests li a:hover, 105 | #qunit-tests li a:focus { 106 | color: #000; 107 | } 108 | 109 | #qunit-tests ol { 110 | margin-top: 0.5em; 111 | padding: 0.5em; 112 | 113 | background-color: #fff; 114 | 115 | border-radius: 15px; 116 | -moz-border-radius: 15px; 117 | -webkit-border-radius: 15px; 118 | 119 | box-shadow: inset 0px 2px 13px #999; 120 | -moz-box-shadow: inset 0px 2px 13px #999; 121 | -webkit-box-shadow: inset 0px 2px 13px #999; 122 | } 123 | 124 | #qunit-tests table { 125 | border-collapse: collapse; 126 | margin-top: .2em; 127 | } 128 | 129 | #qunit-tests th { 130 | text-align: right; 131 | vertical-align: top; 132 | padding: 0 .5em 0 0; 133 | } 134 | 135 | #qunit-tests td { 136 | vertical-align: top; 137 | } 138 | 139 | #qunit-tests pre { 140 | margin: 0; 141 | white-space: pre-wrap; 142 | word-wrap: break-word; 143 | } 144 | 145 | #qunit-tests del { 146 | background-color: #e0f2be; 147 | color: #374e0c; 148 | text-decoration: none; 149 | } 150 | 151 | #qunit-tests ins { 152 | background-color: #ffcaca; 153 | color: #500; 154 | text-decoration: none; 155 | } 156 | 157 | /*** Test Counts */ 158 | 159 | #qunit-tests b.counts { color: black; } 160 | #qunit-tests b.passed { color: #5E740B; } 161 | #qunit-tests b.failed { color: #710909; } 162 | 163 | #qunit-tests li li { 164 | margin: 0.5em; 165 | padding: 0.4em 0.5em 0.4em 0.5em; 166 | background-color: #fff; 167 | border-bottom: none; 168 | list-style-position: inside; 169 | } 170 | 171 | /*** Passing Styles */ 172 | 173 | #qunit-tests li li.pass { 174 | color: #5E740B; 175 | background-color: #fff; 176 | border-left: 26px solid #C6E746; 177 | } 178 | 179 | #qunit-tests .pass { color: #528CE0; background-color: #D2E0E6; } 180 | #qunit-tests .pass .test-name { color: #366097; } 181 | 182 | #qunit-tests .pass .test-actual, 183 | #qunit-tests .pass .test-expected { color: #999999; } 184 | 185 | #qunit-banner.qunit-pass { background-color: #C6E746; } 186 | 187 | /*** Failing Styles */ 188 | 189 | #qunit-tests li li.fail { 190 | color: #710909; 191 | background-color: #fff; 192 | border-left: 26px solid #EE5757; 193 | white-space: pre; 194 | } 195 | 196 | #qunit-tests > li:last-child { 197 | border-radius: 0 0 15px 15px; 198 | -moz-border-radius: 0 0 15px 15px; 199 | -webkit-border-bottom-right-radius: 15px; 200 | -webkit-border-bottom-left-radius: 15px; 201 | } 202 | 203 | #qunit-tests .fail { color: #000000; background-color: #EE5757; } 204 | #qunit-tests .fail .test-name, 205 | #qunit-tests .fail .module-name { color: #000000; } 206 | 207 | #qunit-tests .fail .test-actual { color: #EE5757; } 208 | #qunit-tests .fail .test-expected { color: green; } 209 | 210 | #qunit-banner.qunit-fail { background-color: #EE5757; } 211 | 212 | 213 | /** Result */ 214 | 215 | #qunit-testresult { 216 | padding: 0.5em 0.5em 0.5em 2.5em; 217 | 218 | color: #2b81af; 219 | background-color: #D2E0E6; 220 | 221 | border-bottom: 1px solid white; 222 | } 223 | #qunit-testresult .module-name { 224 | font-weight: bold; 225 | } 226 | 227 | /** Fixture */ 228 | 229 | #qunit-fixture { 230 | position: absolute; 231 | top: -10000px; 232 | left: -10000px; 233 | width: 1000px; 234 | height: 1000px; 235 | } 236 | -------------------------------------------------------------------------------- /test/lib/qunit-1.5.0.js: -------------------------------------------------------------------------------- 1 | /** 2 | * QUnit v1.5.0 - A JavaScript Unit Testing Framework 3 | * 4 | * http://docs.jquery.com/QUnit 5 | * 6 | * Copyright (c) 2012 John Resig, Jörn Zaefferer 7 | * Dual licensed under the MIT (MIT-LICENSE.txt) 8 | * or GPL (GPL-LICENSE.txt) licenses. 9 | */ 10 | 11 | (function(window) { 12 | 13 | var defined = { 14 | setTimeout: typeof window.setTimeout !== "undefined", 15 | sessionStorage: (function() { 16 | var x = "qunit-test-string"; 17 | try { 18 | sessionStorage.setItem(x, x); 19 | sessionStorage.removeItem(x); 20 | return true; 21 | } catch(e) { 22 | return false; 23 | } 24 | }()) 25 | }; 26 | 27 | var testId = 0, 28 | toString = Object.prototype.toString, 29 | hasOwn = Object.prototype.hasOwnProperty; 30 | 31 | var Test = function(name, testName, expected, async, callback) { 32 | this.name = name; 33 | this.testName = testName; 34 | this.expected = expected; 35 | this.async = async; 36 | this.callback = callback; 37 | this.assertions = []; 38 | }; 39 | Test.prototype = { 40 | init: function() { 41 | var tests = id("qunit-tests"); 42 | if (tests) { 43 | var b = document.createElement("strong"); 44 | b.innerHTML = "Running " + this.name; 45 | var li = document.createElement("li"); 46 | li.appendChild( b ); 47 | li.className = "running"; 48 | li.id = this.id = "test-output" + testId++; 49 | tests.appendChild( li ); 50 | } 51 | }, 52 | setup: function() { 53 | if (this.module != config.previousModule) { 54 | if ( config.previousModule ) { 55 | runLoggingCallbacks('moduleDone', QUnit, { 56 | name: config.previousModule, 57 | failed: config.moduleStats.bad, 58 | passed: config.moduleStats.all - config.moduleStats.bad, 59 | total: config.moduleStats.all 60 | } ); 61 | } 62 | config.previousModule = this.module; 63 | config.moduleStats = { all: 0, bad: 0 }; 64 | runLoggingCallbacks( 'moduleStart', QUnit, { 65 | name: this.module 66 | } ); 67 | } else if (config.autorun) { 68 | runLoggingCallbacks( 'moduleStart', QUnit, { 69 | name: this.module 70 | } ); 71 | } 72 | 73 | config.current = this; 74 | this.testEnvironment = extend({ 75 | setup: function() {}, 76 | teardown: function() {} 77 | }, this.moduleTestEnvironment); 78 | 79 | runLoggingCallbacks( 'testStart', QUnit, { 80 | name: this.testName, 81 | module: this.module 82 | }); 83 | 84 | // allow utility functions to access the current test environment 85 | // TODO why?? 86 | QUnit.current_testEnvironment = this.testEnvironment; 87 | 88 | if ( !config.pollution ) { 89 | saveGlobal(); 90 | } 91 | if ( config.notrycatch ) { 92 | this.testEnvironment.setup.call(this.testEnvironment); 93 | return; 94 | } 95 | try { 96 | this.testEnvironment.setup.call(this.testEnvironment); 97 | } catch(e) { 98 | QUnit.pushFailure( "Setup failed on " + this.testName + ": " + e.message, extractStacktrace( e, 1 ) ); 99 | } 100 | }, 101 | run: function() { 102 | config.current = this; 103 | 104 | var running = id("qunit-testresult"); 105 | 106 | if ( running ) { 107 | running.innerHTML = "Running:
" + this.name; 108 | } 109 | 110 | if ( this.async ) { 111 | QUnit.stop(); 112 | } 113 | 114 | if ( config.notrycatch ) { 115 | this.callback.call(this.testEnvironment); 116 | return; 117 | } 118 | try { 119 | this.callback.call(this.testEnvironment); 120 | } catch(e) { 121 | QUnit.pushFailure( "Died on test #" + (this.assertions.length + 1) + ": " + e.message, extractStacktrace( e, 1 ) ); 122 | // else next test will carry the responsibility 123 | saveGlobal(); 124 | 125 | // Restart the tests if they're blocking 126 | if ( config.blocking ) { 127 | QUnit.start(); 128 | } 129 | } 130 | }, 131 | teardown: function() { 132 | config.current = this; 133 | if ( config.notrycatch ) { 134 | this.testEnvironment.teardown.call(this.testEnvironment); 135 | return; 136 | } else { 137 | try { 138 | this.testEnvironment.teardown.call(this.testEnvironment); 139 | } catch(e) { 140 | QUnit.pushFailure( "Teardown failed on " + this.testName + ": " + e.message, extractStacktrace( e, 1 ) ); 141 | } 142 | } 143 | checkPollution(); 144 | }, 145 | finish: function() { 146 | config.current = this; 147 | if ( this.expected != null && this.expected != this.assertions.length ) { 148 | QUnit.pushFailure( "Expected " + this.expected + " assertions, but " + this.assertions.length + " were run" ); 149 | } else if ( this.expected == null && !this.assertions.length ) { 150 | QUnit.pushFailure( "Expected at least one assertion, but none were run - call expect(0) to accept zero assertions." ); 151 | } 152 | 153 | var good = 0, bad = 0, 154 | li, i, 155 | tests = id("qunit-tests"); 156 | 157 | config.stats.all += this.assertions.length; 158 | config.moduleStats.all += this.assertions.length; 159 | 160 | if ( tests ) { 161 | var ol = document.createElement("ol"); 162 | 163 | for ( i = 0; i < this.assertions.length; i++ ) { 164 | var assertion = this.assertions[i]; 165 | 166 | li = document.createElement("li"); 167 | li.className = assertion.result ? "pass" : "fail"; 168 | li.innerHTML = assertion.message || (assertion.result ? "okay" : "failed"); 169 | ol.appendChild( li ); 170 | 171 | if ( assertion.result ) { 172 | good++; 173 | } else { 174 | bad++; 175 | config.stats.bad++; 176 | config.moduleStats.bad++; 177 | } 178 | } 179 | 180 | // store result when possible 181 | if ( QUnit.config.reorder && defined.sessionStorage ) { 182 | if (bad) { 183 | sessionStorage.setItem("qunit-test-" + this.module + "-" + this.testName, bad); 184 | } else { 185 | sessionStorage.removeItem("qunit-test-" + this.module + "-" + this.testName); 186 | } 187 | } 188 | 189 | if (bad === 0) { 190 | ol.style.display = "none"; 191 | } 192 | 193 | var b = document.createElement("strong"); 194 | b.innerHTML = this.name + " (" + bad + ", " + good + ", " + this.assertions.length + ")"; 195 | 196 | var a = document.createElement("a"); 197 | a.innerHTML = "Rerun"; 198 | a.href = QUnit.url({ filter: getText([b]).replace(/\([^)]+\)$/, "").replace(/(^\s*|\s*$)/g, "") }); 199 | 200 | addEvent(b, "click", function() { 201 | var next = b.nextSibling.nextSibling, 202 | display = next.style.display; 203 | next.style.display = display === "none" ? "block" : "none"; 204 | }); 205 | 206 | addEvent(b, "dblclick", function(e) { 207 | var target = e && e.target ? e.target : window.event.srcElement; 208 | if ( target.nodeName.toLowerCase() == "span" || target.nodeName.toLowerCase() == "b" ) { 209 | target = target.parentNode; 210 | } 211 | if ( window.location && target.nodeName.toLowerCase() === "strong" ) { 212 | window.location = QUnit.url({ filter: getText([target]).replace(/\([^)]+\)$/, "").replace(/(^\s*|\s*$)/g, "") }); 213 | } 214 | }); 215 | 216 | li = id(this.id); 217 | li.className = bad ? "fail" : "pass"; 218 | li.removeChild( li.firstChild ); 219 | li.appendChild( b ); 220 | li.appendChild( a ); 221 | li.appendChild( ol ); 222 | 223 | } else { 224 | for ( i = 0; i < this.assertions.length; i++ ) { 225 | if ( !this.assertions[i].result ) { 226 | bad++; 227 | config.stats.bad++; 228 | config.moduleStats.bad++; 229 | } 230 | } 231 | } 232 | 233 | QUnit.reset(); 234 | 235 | runLoggingCallbacks( 'testDone', QUnit, { 236 | name: this.testName, 237 | module: this.module, 238 | failed: bad, 239 | passed: this.assertions.length - bad, 240 | total: this.assertions.length 241 | } ); 242 | }, 243 | 244 | queue: function() { 245 | var test = this; 246 | synchronize(function() { 247 | test.init(); 248 | }); 249 | function run() { 250 | // each of these can by async 251 | synchronize(function() { 252 | test.setup(); 253 | }); 254 | synchronize(function() { 255 | test.run(); 256 | }); 257 | synchronize(function() { 258 | test.teardown(); 259 | }); 260 | synchronize(function() { 261 | test.finish(); 262 | }); 263 | } 264 | // defer when previous test run passed, if storage is available 265 | var bad = QUnit.config.reorder && defined.sessionStorage && +sessionStorage.getItem("qunit-test-" + this.module + "-" + this.testName); 266 | if (bad) { 267 | run(); 268 | } else { 269 | synchronize(run, true); 270 | } 271 | } 272 | 273 | }; 274 | 275 | var QUnit = { 276 | 277 | // call on start of module test to prepend name to all tests 278 | module: function(name, testEnvironment) { 279 | config.currentModule = name; 280 | config.currentModuleTestEnviroment = testEnvironment; 281 | }, 282 | 283 | asyncTest: function(testName, expected, callback) { 284 | if ( arguments.length === 2 ) { 285 | callback = expected; 286 | expected = null; 287 | } 288 | 289 | QUnit.test(testName, expected, callback, true); 290 | }, 291 | 292 | test: function(testName, expected, callback, async) { 293 | var name = '' + escapeInnerText(testName) + ''; 294 | 295 | if ( arguments.length === 2 ) { 296 | callback = expected; 297 | expected = null; 298 | } 299 | 300 | if ( config.currentModule ) { 301 | name = '' + config.currentModule + ": " + name; 302 | } 303 | 304 | if ( !validTest(config.currentModule + ": " + testName) ) { 305 | return; 306 | } 307 | 308 | var test = new Test(name, testName, expected, async, callback); 309 | test.module = config.currentModule; 310 | test.moduleTestEnvironment = config.currentModuleTestEnviroment; 311 | test.queue(); 312 | }, 313 | 314 | // Specify the number of expected assertions to gurantee that failed test (no assertions are run at all) don't slip through. 315 | expect: function(asserts) { 316 | config.current.expected = asserts; 317 | }, 318 | 319 | // Asserts true. 320 | // @example ok( "asdfasdf".length > 5, "There must be at least 5 chars" ); 321 | ok: function(result, msg) { 322 | if (!config.current) { 323 | throw new Error("ok() assertion outside test context, was " + sourceFromStacktrace(2)); 324 | } 325 | result = !!result; 326 | var details = { 327 | result: result, 328 | message: msg 329 | }; 330 | msg = escapeInnerText(msg || (result ? "okay" : "failed")); 331 | if ( !result ) { 332 | var source = sourceFromStacktrace(2); 333 | if (source) { 334 | details.source = source; 335 | msg += '
Source:
' + escapeInnerText(source) + '
'; 336 | } 337 | } 338 | runLoggingCallbacks( 'log', QUnit, details ); 339 | config.current.assertions.push({ 340 | result: result, 341 | message: msg 342 | }); 343 | }, 344 | 345 | // Checks that the first two arguments are equal, with an optional message. Prints out both actual and expected values. 346 | // @example equal( format("Received {0} bytes.", 2), "Received 2 bytes." ); 347 | equal: function(actual, expected, message) { 348 | QUnit.push(expected == actual, actual, expected, message); 349 | }, 350 | 351 | notEqual: function(actual, expected, message) { 352 | QUnit.push(expected != actual, actual, expected, message); 353 | }, 354 | 355 | deepEqual: function(actual, expected, message) { 356 | QUnit.push(QUnit.equiv(actual, expected), actual, expected, message); 357 | }, 358 | 359 | notDeepEqual: function(actual, expected, message) { 360 | QUnit.push(!QUnit.equiv(actual, expected), actual, expected, message); 361 | }, 362 | 363 | strictEqual: function(actual, expected, message) { 364 | QUnit.push(expected === actual, actual, expected, message); 365 | }, 366 | 367 | notStrictEqual: function(actual, expected, message) { 368 | QUnit.push(expected !== actual, actual, expected, message); 369 | }, 370 | 371 | raises: function(block, expected, message) { 372 | var actual, ok = false; 373 | 374 | if (typeof expected === 'string') { 375 | message = expected; 376 | expected = null; 377 | } 378 | 379 | try { 380 | block.call(config.current.testEnvironment); 381 | } catch (e) { 382 | actual = e; 383 | } 384 | 385 | if (actual) { 386 | // we don't want to validate thrown error 387 | if (!expected) { 388 | ok = true; 389 | // expected is a regexp 390 | } else if (QUnit.objectType(expected) === "regexp") { 391 | ok = expected.test(actual); 392 | // expected is a constructor 393 | } else if (actual instanceof expected) { 394 | ok = true; 395 | // expected is a validation function which returns true is validation passed 396 | } else if (expected.call({}, actual) === true) { 397 | ok = true; 398 | } 399 | } 400 | 401 | QUnit.ok(ok, message); 402 | }, 403 | 404 | start: function(count) { 405 | config.semaphore -= count || 1; 406 | if (config.semaphore > 0) { 407 | // don't start until equal number of stop-calls 408 | return; 409 | } 410 | if (config.semaphore < 0) { 411 | // ignore if start is called more often then stop 412 | config.semaphore = 0; 413 | } 414 | // A slight delay, to avoid any current callbacks 415 | if ( defined.setTimeout ) { 416 | window.setTimeout(function() { 417 | if (config.semaphore > 0) { 418 | return; 419 | } 420 | if ( config.timeout ) { 421 | clearTimeout(config.timeout); 422 | } 423 | 424 | config.blocking = false; 425 | process(true); 426 | }, 13); 427 | } else { 428 | config.blocking = false; 429 | process(true); 430 | } 431 | }, 432 | 433 | stop: function(count) { 434 | config.semaphore += count || 1; 435 | config.blocking = true; 436 | 437 | if ( config.testTimeout && defined.setTimeout ) { 438 | clearTimeout(config.timeout); 439 | config.timeout = window.setTimeout(function() { 440 | QUnit.ok( false, "Test timed out" ); 441 | config.semaphore = 1; 442 | QUnit.start(); 443 | }, config.testTimeout); 444 | } 445 | } 446 | }; 447 | 448 | //We want access to the constructor's prototype 449 | (function() { 450 | function F(){} 451 | F.prototype = QUnit; 452 | QUnit = new F(); 453 | //Make F QUnit's constructor so that we can add to the prototype later 454 | QUnit.constructor = F; 455 | }()); 456 | 457 | // deprecated; still export them to window to provide clear error messages 458 | // next step: remove entirely 459 | QUnit.equals = function() { 460 | QUnit.push(false, false, false, "QUnit.equals has been deprecated since 2009 (e88049a0), use QUnit.equal instead"); 461 | }; 462 | QUnit.same = function() { 463 | QUnit.push(false, false, false, "QUnit.same has been deprecated since 2009 (e88049a0), use QUnit.deepEqual instead"); 464 | }; 465 | 466 | // Maintain internal state 467 | var config = { 468 | // The queue of tests to run 469 | queue: [], 470 | 471 | // block until document ready 472 | blocking: true, 473 | 474 | // when enabled, show only failing tests 475 | // gets persisted through sessionStorage and can be changed in UI via checkbox 476 | hidepassed: false, 477 | 478 | // by default, run previously failed tests first 479 | // very useful in combination with "Hide passed tests" checked 480 | reorder: true, 481 | 482 | // by default, modify document.title when suite is done 483 | altertitle: true, 484 | 485 | urlConfig: ['noglobals', 'notrycatch'], 486 | 487 | //logging callback queues 488 | begin: [], 489 | done: [], 490 | log: [], 491 | testStart: [], 492 | testDone: [], 493 | moduleStart: [], 494 | moduleDone: [] 495 | }; 496 | 497 | // Load paramaters 498 | (function() { 499 | var location = window.location || { search: "", protocol: "file:" }, 500 | params = location.search.slice( 1 ).split( "&" ), 501 | length = params.length, 502 | urlParams = {}, 503 | current; 504 | 505 | if ( params[ 0 ] ) { 506 | for ( var i = 0; i < length; i++ ) { 507 | current = params[ i ].split( "=" ); 508 | current[ 0 ] = decodeURIComponent( current[ 0 ] ); 509 | // allow just a key to turn on a flag, e.g., test.html?noglobals 510 | current[ 1 ] = current[ 1 ] ? decodeURIComponent( current[ 1 ] ) : true; 511 | urlParams[ current[ 0 ] ] = current[ 1 ]; 512 | } 513 | } 514 | 515 | QUnit.urlParams = urlParams; 516 | config.filter = urlParams.filter; 517 | 518 | // Figure out if we're running the tests from a server or not 519 | QUnit.isLocal = location.protocol === 'file:'; 520 | }()); 521 | 522 | // Expose the API as global variables, unless an 'exports' 523 | // object exists, in that case we assume we're in CommonJS - export everything at the end 524 | if ( typeof exports === "undefined" || typeof require === "undefined" ) { 525 | extend(window, QUnit); 526 | window.QUnit = QUnit; 527 | } 528 | 529 | // define these after exposing globals to keep them in these QUnit namespace only 530 | extend(QUnit, { 531 | config: config, 532 | 533 | // Initialize the configuration options 534 | init: function() { 535 | extend(config, { 536 | stats: { all: 0, bad: 0 }, 537 | moduleStats: { all: 0, bad: 0 }, 538 | started: +new Date(), 539 | updateRate: 1000, 540 | blocking: false, 541 | autostart: true, 542 | autorun: false, 543 | filter: "", 544 | queue: [], 545 | semaphore: 0 546 | }); 547 | 548 | var qunit = id( "qunit" ); 549 | if ( qunit ) { 550 | qunit.innerHTML = 551 | '

' + escapeInnerText( document.title ) + '

' + 552 | '

' + 553 | '
' + 554 | '

' + 555 | '
    '; 556 | } 557 | 558 | var tests = id( "qunit-tests" ), 559 | banner = id( "qunit-banner" ), 560 | result = id( "qunit-testresult" ); 561 | 562 | if ( tests ) { 563 | tests.innerHTML = ""; 564 | } 565 | 566 | if ( banner ) { 567 | banner.className = ""; 568 | } 569 | 570 | if ( result ) { 571 | result.parentNode.removeChild( result ); 572 | } 573 | 574 | if ( tests ) { 575 | result = document.createElement( "p" ); 576 | result.id = "qunit-testresult"; 577 | result.className = "result"; 578 | tests.parentNode.insertBefore( result, tests ); 579 | result.innerHTML = 'Running...
     '; 580 | } 581 | }, 582 | 583 | // Resets the test setup. Useful for tests that modify the DOM. 584 | // If jQuery is available, uses jQuery's html(), otherwise just innerHTML. 585 | reset: function() { 586 | if ( window.jQuery ) { 587 | jQuery( "#qunit-fixture" ).html( config.fixture ); 588 | } else { 589 | var main = id( 'qunit-fixture' ); 590 | if ( main ) { 591 | main.innerHTML = config.fixture; 592 | } 593 | } 594 | }, 595 | 596 | // Trigger an event on an element. 597 | // @example triggerEvent( document.body, "click" ); 598 | triggerEvent: function( elem, type, event ) { 599 | if ( document.createEvent ) { 600 | event = document.createEvent("MouseEvents"); 601 | event.initMouseEvent(type, true, true, elem.ownerDocument.defaultView, 602 | 0, 0, 0, 0, 0, false, false, false, false, 0, null); 603 | elem.dispatchEvent( event ); 604 | 605 | } else if ( elem.fireEvent ) { 606 | elem.fireEvent("on"+type); 607 | } 608 | }, 609 | 610 | // Safe object type checking 611 | is: function( type, obj ) { 612 | return QUnit.objectType( obj ) == type; 613 | }, 614 | 615 | objectType: function( obj ) { 616 | if (typeof obj === "undefined") { 617 | return "undefined"; 618 | 619 | // consider: typeof null === object 620 | } 621 | if (obj === null) { 622 | return "null"; 623 | } 624 | 625 | var type = toString.call( obj ).match(/^\[object\s(.*)\]$/)[1] || ''; 626 | 627 | switch (type) { 628 | case 'Number': 629 | if (isNaN(obj)) { 630 | return "nan"; 631 | } 632 | return "number"; 633 | case 'String': 634 | case 'Boolean': 635 | case 'Array': 636 | case 'Date': 637 | case 'RegExp': 638 | case 'Function': 639 | return type.toLowerCase(); 640 | } 641 | if (typeof obj === "object") { 642 | return "object"; 643 | } 644 | return undefined; 645 | }, 646 | 647 | push: function(result, actual, expected, message) { 648 | if (!config.current) { 649 | throw new Error("assertion outside test context, was " + sourceFromStacktrace()); 650 | } 651 | var details = { 652 | result: result, 653 | message: message, 654 | actual: actual, 655 | expected: expected 656 | }; 657 | 658 | message = escapeInnerText(message) || (result ? "okay" : "failed"); 659 | message = '' + message + ""; 660 | var output = message; 661 | if (!result) { 662 | expected = escapeInnerText(QUnit.jsDump.parse(expected)); 663 | actual = escapeInnerText(QUnit.jsDump.parse(actual)); 664 | output += ''; 665 | if (actual != expected) { 666 | output += ''; 667 | output += ''; 668 | } 669 | var source = sourceFromStacktrace(); 670 | if (source) { 671 | details.source = source; 672 | output += ''; 673 | } 674 | output += "
    Expected:
    ' + expected + '
    Result:
    ' + actual + '
    Diff:
    ' + QUnit.diff(expected, actual) +'
    Source:
    ' + escapeInnerText(source) + '
    "; 675 | } 676 | 677 | runLoggingCallbacks( 'log', QUnit, details ); 678 | 679 | config.current.assertions.push({ 680 | result: !!result, 681 | message: output 682 | }); 683 | }, 684 | 685 | pushFailure: function(message, source) { 686 | var details = { 687 | result: false, 688 | message: message 689 | }; 690 | var output = escapeInnerText(message); 691 | if (source) { 692 | details.source = source; 693 | output += '
    Source:
    ' + escapeInnerText(source) + '
    '; 694 | } 695 | runLoggingCallbacks( 'log', QUnit, details ); 696 | config.current.assertions.push({ 697 | result: false, 698 | message: output 699 | }); 700 | }, 701 | 702 | url: function( params ) { 703 | params = extend( extend( {}, QUnit.urlParams ), params ); 704 | var querystring = "?", 705 | key; 706 | for ( key in params ) { 707 | if ( !hasOwn.call( params, key ) ) { 708 | continue; 709 | } 710 | querystring += encodeURIComponent( key ) + "=" + 711 | encodeURIComponent( params[ key ] ) + "&"; 712 | } 713 | return window.location.pathname + querystring.slice( 0, -1 ); 714 | }, 715 | 716 | extend: extend, 717 | id: id, 718 | addEvent: addEvent 719 | }); 720 | 721 | //QUnit.constructor is set to the empty F() above so that we can add to it's prototype later 722 | //Doing this allows us to tell if the following methods have been overwritten on the actual 723 | //QUnit object, which is a deprecated way of using the callbacks. 724 | extend(QUnit.constructor.prototype, { 725 | // Logging callbacks; all receive a single argument with the listed properties 726 | // run test/logs.html for any related changes 727 | begin: registerLoggingCallback('begin'), 728 | // done: { failed, passed, total, runtime } 729 | done: registerLoggingCallback('done'), 730 | // log: { result, actual, expected, message } 731 | log: registerLoggingCallback('log'), 732 | // testStart: { name } 733 | testStart: registerLoggingCallback('testStart'), 734 | // testDone: { name, failed, passed, total } 735 | testDone: registerLoggingCallback('testDone'), 736 | // moduleStart: { name } 737 | moduleStart: registerLoggingCallback('moduleStart'), 738 | // moduleDone: { name, failed, passed, total } 739 | moduleDone: registerLoggingCallback('moduleDone') 740 | }); 741 | 742 | if ( typeof document === "undefined" || document.readyState === "complete" ) { 743 | config.autorun = true; 744 | } 745 | 746 | QUnit.load = function() { 747 | runLoggingCallbacks( 'begin', QUnit, {} ); 748 | 749 | // Initialize the config, saving the execution queue 750 | var oldconfig = extend({}, config); 751 | QUnit.init(); 752 | extend(config, oldconfig); 753 | 754 | config.blocking = false; 755 | 756 | var urlConfigHtml = '', len = config.urlConfig.length; 757 | for ( var i = 0, val; i < len; i++ ) { 758 | val = config.urlConfig[i]; 759 | config[val] = QUnit.urlParams[val]; 760 | urlConfigHtml += ''; 761 | } 762 | 763 | var userAgent = id("qunit-userAgent"); 764 | if ( userAgent ) { 765 | userAgent.innerHTML = navigator.userAgent; 766 | } 767 | var banner = id("qunit-header"); 768 | if ( banner ) { 769 | banner.innerHTML = ' ' + banner.innerHTML + ' ' + urlConfigHtml; 770 | addEvent( banner, "change", function( event ) { 771 | var params = {}; 772 | params[ event.target.name ] = event.target.checked ? true : undefined; 773 | window.location = QUnit.url( params ); 774 | }); 775 | } 776 | 777 | var toolbar = id("qunit-testrunner-toolbar"); 778 | if ( toolbar ) { 779 | var filter = document.createElement("input"); 780 | filter.type = "checkbox"; 781 | filter.id = "qunit-filter-pass"; 782 | addEvent( filter, "click", function() { 783 | var ol = document.getElementById("qunit-tests"); 784 | if ( filter.checked ) { 785 | ol.className = ol.className + " hidepass"; 786 | } else { 787 | var tmp = " " + ol.className.replace( /[\n\t\r]/g, " " ) + " "; 788 | ol.className = tmp.replace(/ hidepass /, " "); 789 | } 790 | if ( defined.sessionStorage ) { 791 | if (filter.checked) { 792 | sessionStorage.setItem("qunit-filter-passed-tests", "true"); 793 | } else { 794 | sessionStorage.removeItem("qunit-filter-passed-tests"); 795 | } 796 | } 797 | }); 798 | if ( config.hidepassed || defined.sessionStorage && sessionStorage.getItem("qunit-filter-passed-tests") ) { 799 | filter.checked = true; 800 | var ol = document.getElementById("qunit-tests"); 801 | ol.className = ol.className + " hidepass"; 802 | } 803 | toolbar.appendChild( filter ); 804 | 805 | var label = document.createElement("label"); 806 | label.setAttribute("for", "qunit-filter-pass"); 807 | label.innerHTML = "Hide passed tests"; 808 | toolbar.appendChild( label ); 809 | } 810 | 811 | var main = id('qunit-fixture'); 812 | if ( main ) { 813 | config.fixture = main.innerHTML; 814 | } 815 | 816 | if (config.autostart) { 817 | QUnit.start(); 818 | } 819 | }; 820 | 821 | addEvent(window, "load", QUnit.load); 822 | 823 | // addEvent(window, "error") gives us a useless event object 824 | window.onerror = function( message, file, line ) { 825 | if ( QUnit.config.current ) { 826 | QUnit.pushFailure( message, file + ":" + line ); 827 | } else { 828 | QUnit.test( "global failure", function() { 829 | QUnit.pushFailure( message, file + ":" + line ); 830 | }); 831 | } 832 | }; 833 | 834 | function done() { 835 | config.autorun = true; 836 | 837 | // Log the last module results 838 | if ( config.currentModule ) { 839 | runLoggingCallbacks( 'moduleDone', QUnit, { 840 | name: config.currentModule, 841 | failed: config.moduleStats.bad, 842 | passed: config.moduleStats.all - config.moduleStats.bad, 843 | total: config.moduleStats.all 844 | } ); 845 | } 846 | 847 | var banner = id("qunit-banner"), 848 | tests = id("qunit-tests"), 849 | runtime = +new Date() - config.started, 850 | passed = config.stats.all - config.stats.bad, 851 | html = [ 852 | 'Tests completed in ', 853 | runtime, 854 | ' milliseconds.
    ', 855 | '', 856 | passed, 857 | ' tests of ', 858 | config.stats.all, 859 | ' passed, ', 860 | config.stats.bad, 861 | ' failed.' 862 | ].join(''); 863 | 864 | if ( banner ) { 865 | banner.className = (config.stats.bad ? "qunit-fail" : "qunit-pass"); 866 | } 867 | 868 | if ( tests ) { 869 | id( "qunit-testresult" ).innerHTML = html; 870 | } 871 | 872 | if ( config.altertitle && typeof document !== "undefined" && document.title ) { 873 | // show ✖ for good, ✔ for bad suite result in title 874 | // use escape sequences in case file gets loaded with non-utf-8-charset 875 | document.title = [ 876 | (config.stats.bad ? "\u2716" : "\u2714"), 877 | document.title.replace(/^[\u2714\u2716] /i, "") 878 | ].join(" "); 879 | } 880 | 881 | // clear own sessionStorage items if all tests passed 882 | if ( config.reorder && defined.sessionStorage && config.stats.bad === 0 ) { 883 | var key; 884 | for ( var i = 0; i < sessionStorage.length; i++ ) { 885 | key = sessionStorage.key( i++ ); 886 | if ( key.indexOf("qunit-test-") === 0 ) { 887 | sessionStorage.removeItem( key ); 888 | } 889 | } 890 | } 891 | 892 | runLoggingCallbacks( 'done', QUnit, { 893 | failed: config.stats.bad, 894 | passed: passed, 895 | total: config.stats.all, 896 | runtime: runtime 897 | } ); 898 | } 899 | 900 | function validTest( name ) { 901 | var filter = config.filter, 902 | run = false; 903 | 904 | if ( !filter ) { 905 | return true; 906 | } 907 | 908 | var not = filter.charAt( 0 ) === "!"; 909 | if ( not ) { 910 | filter = filter.slice( 1 ); 911 | } 912 | 913 | if ( name.indexOf( filter ) !== -1 ) { 914 | return !not; 915 | } 916 | 917 | if ( not ) { 918 | run = true; 919 | } 920 | 921 | return run; 922 | } 923 | 924 | // so far supports only Firefox, Chrome and Opera (buggy), Safari (for real exceptions) 925 | // Later Safari and IE10 are supposed to support error.stack as well 926 | // See also https://developer.mozilla.org/en/JavaScript/Reference/Global_Objects/Error/Stack 927 | function extractStacktrace( e, offset ) { 928 | offset = offset || 3; 929 | if (e.stacktrace) { 930 | // Opera 931 | return e.stacktrace.split("\n")[offset + 3]; 932 | } else if (e.stack) { 933 | // Firefox, Chrome 934 | var stack = e.stack.split("\n"); 935 | if (/^error$/i.test(stack[0])) { 936 | stack.shift(); 937 | } 938 | return stack[offset]; 939 | } else if (e.sourceURL) { 940 | // Safari, PhantomJS 941 | // hopefully one day Safari provides actual stacktraces 942 | // exclude useless self-reference for generated Error objects 943 | if ( /qunit.js$/.test( e.sourceURL ) ) { 944 | return; 945 | } 946 | // for actual exceptions, this is useful 947 | return e.sourceURL + ":" + e.line; 948 | } 949 | } 950 | function sourceFromStacktrace(offset) { 951 | try { 952 | throw new Error(); 953 | } catch ( e ) { 954 | return extractStacktrace( e, offset ); 955 | } 956 | } 957 | 958 | function escapeInnerText(s) { 959 | if (!s) { 960 | return ""; 961 | } 962 | s = s + ""; 963 | return s.replace(/[\&<>]/g, function(s) { 964 | switch(s) { 965 | case "&": return "&"; 966 | case "<": return "<"; 967 | case ">": return ">"; 968 | default: return s; 969 | } 970 | }); 971 | } 972 | 973 | function synchronize( callback, last ) { 974 | config.queue.push( callback ); 975 | 976 | if ( config.autorun && !config.blocking ) { 977 | process(last); 978 | } 979 | } 980 | 981 | function process( last ) { 982 | function next() { 983 | process( last ); 984 | } 985 | var start = new Date().getTime(); 986 | config.depth = config.depth ? config.depth + 1 : 1; 987 | 988 | while ( config.queue.length && !config.blocking ) { 989 | if ( !defined.setTimeout || config.updateRate <= 0 || ( ( new Date().getTime() - start ) < config.updateRate ) ) { 990 | config.queue.shift()(); 991 | } else { 992 | window.setTimeout( next, 13 ); 993 | break; 994 | } 995 | } 996 | config.depth--; 997 | if ( last && !config.blocking && !config.queue.length && config.depth === 0 ) { 998 | done(); 999 | } 1000 | } 1001 | 1002 | function saveGlobal() { 1003 | config.pollution = []; 1004 | 1005 | if ( config.noglobals ) { 1006 | for ( var key in window ) { 1007 | if ( !hasOwn.call( window, key ) ) { 1008 | continue; 1009 | } 1010 | config.pollution.push( key ); 1011 | } 1012 | } 1013 | } 1014 | 1015 | function checkPollution( name ) { 1016 | var old = config.pollution; 1017 | saveGlobal(); 1018 | 1019 | var newGlobals = diff( config.pollution, old ); 1020 | if ( newGlobals.length > 0 ) { 1021 | QUnit.pushFailure( "Introduced global variable(s): " + newGlobals.join(", ") ); 1022 | } 1023 | 1024 | var deletedGlobals = diff( old, config.pollution ); 1025 | if ( deletedGlobals.length > 0 ) { 1026 | QUnit.pushFailure( "Deleted global variable(s): " + deletedGlobals.join(", ") ); 1027 | } 1028 | } 1029 | 1030 | // returns a new Array with the elements that are in a but not in b 1031 | function diff( a, b ) { 1032 | var result = a.slice(); 1033 | for ( var i = 0; i < result.length; i++ ) { 1034 | for ( var j = 0; j < b.length; j++ ) { 1035 | if ( result[i] === b[j] ) { 1036 | result.splice(i, 1); 1037 | i--; 1038 | break; 1039 | } 1040 | } 1041 | } 1042 | return result; 1043 | } 1044 | 1045 | function extend(a, b) { 1046 | for ( var prop in b ) { 1047 | if ( b[prop] === undefined ) { 1048 | delete a[prop]; 1049 | 1050 | // Avoid "Member not found" error in IE8 caused by setting window.constructor 1051 | } else if ( prop !== "constructor" || a !== window ) { 1052 | a[prop] = b[prop]; 1053 | } 1054 | } 1055 | 1056 | return a; 1057 | } 1058 | 1059 | function addEvent(elem, type, fn) { 1060 | if ( elem.addEventListener ) { 1061 | elem.addEventListener( type, fn, false ); 1062 | } else if ( elem.attachEvent ) { 1063 | elem.attachEvent( "on" + type, fn ); 1064 | } else { 1065 | fn(); 1066 | } 1067 | } 1068 | 1069 | function id(name) { 1070 | return !!(typeof document !== "undefined" && document && document.getElementById) && 1071 | document.getElementById( name ); 1072 | } 1073 | 1074 | function registerLoggingCallback(key){ 1075 | return function(callback){ 1076 | config[key].push( callback ); 1077 | }; 1078 | } 1079 | 1080 | // Supports deprecated method of completely overwriting logging callbacks 1081 | function runLoggingCallbacks(key, scope, args) { 1082 | //debugger; 1083 | var callbacks; 1084 | if ( QUnit.hasOwnProperty(key) ) { 1085 | QUnit[key].call(scope, args); 1086 | } else { 1087 | callbacks = config[key]; 1088 | for( var i = 0; i < callbacks.length; i++ ) { 1089 | callbacks[i].call( scope, args ); 1090 | } 1091 | } 1092 | } 1093 | 1094 | // Test for equality any JavaScript type. 1095 | // Author: Philippe Rathé 1096 | QUnit.equiv = (function() { 1097 | 1098 | var innerEquiv; // the real equiv function 1099 | var callers = []; // stack to decide between skip/abort functions 1100 | var parents = []; // stack to avoiding loops from circular referencing 1101 | 1102 | // Call the o related callback with the given arguments. 1103 | function bindCallbacks(o, callbacks, args) { 1104 | var prop = QUnit.objectType(o); 1105 | if (prop) { 1106 | if (QUnit.objectType(callbacks[prop]) === "function") { 1107 | return callbacks[prop].apply(callbacks, args); 1108 | } else { 1109 | return callbacks[prop]; // or undefined 1110 | } 1111 | } 1112 | } 1113 | 1114 | var getProto = Object.getPrototypeOf || function (obj) { 1115 | return obj.__proto__; 1116 | }; 1117 | 1118 | var callbacks = (function () { 1119 | 1120 | // for string, boolean, number and null 1121 | function useStrictEquality(b, a) { 1122 | if (b instanceof a.constructor || a instanceof b.constructor) { 1123 | // to catch short annotaion VS 'new' annotation of a 1124 | // declaration 1125 | // e.g. var i = 1; 1126 | // var j = new Number(1); 1127 | return a == b; 1128 | } else { 1129 | return a === b; 1130 | } 1131 | } 1132 | 1133 | return { 1134 | "string" : useStrictEquality, 1135 | "boolean" : useStrictEquality, 1136 | "number" : useStrictEquality, 1137 | "null" : useStrictEquality, 1138 | "undefined" : useStrictEquality, 1139 | 1140 | "nan" : function(b) { 1141 | return isNaN(b); 1142 | }, 1143 | 1144 | "date" : function(b, a) { 1145 | return QUnit.objectType(b) === "date" && a.valueOf() === b.valueOf(); 1146 | }, 1147 | 1148 | "regexp" : function(b, a) { 1149 | return QUnit.objectType(b) === "regexp" && 1150 | // the regex itself 1151 | a.source === b.source && 1152 | // and its modifers 1153 | a.global === b.global && 1154 | // (gmi) ... 1155 | a.ignoreCase === b.ignoreCase && 1156 | a.multiline === b.multiline; 1157 | }, 1158 | 1159 | // - skip when the property is a method of an instance (OOP) 1160 | // - abort otherwise, 1161 | // initial === would have catch identical references anyway 1162 | "function" : function() { 1163 | var caller = callers[callers.length - 1]; 1164 | return caller !== Object && typeof caller !== "undefined"; 1165 | }, 1166 | 1167 | "array" : function(b, a) { 1168 | var i, j, loop; 1169 | var len; 1170 | 1171 | // b could be an object literal here 1172 | if (QUnit.objectType(b) !== "array") { 1173 | return false; 1174 | } 1175 | 1176 | len = a.length; 1177 | if (len !== b.length) { // safe and faster 1178 | return false; 1179 | } 1180 | 1181 | // track reference to avoid circular references 1182 | parents.push(a); 1183 | for (i = 0; i < len; i++) { 1184 | loop = false; 1185 | for (j = 0; j < parents.length; j++) { 1186 | if (parents[j] === a[i]) { 1187 | loop = true;// dont rewalk array 1188 | } 1189 | } 1190 | if (!loop && !innerEquiv(a[i], b[i])) { 1191 | parents.pop(); 1192 | return false; 1193 | } 1194 | } 1195 | parents.pop(); 1196 | return true; 1197 | }, 1198 | 1199 | "object" : function(b, a) { 1200 | var i, j, loop; 1201 | var eq = true; // unless we can proove it 1202 | var aProperties = [], bProperties = []; // collection of 1203 | // strings 1204 | 1205 | // comparing constructors is more strict than using 1206 | // instanceof 1207 | if (a.constructor !== b.constructor) { 1208 | // Allow objects with no prototype to be equivalent to 1209 | // objects with Object as their constructor. 1210 | if (!((getProto(a) === null && getProto(b) === Object.prototype) || 1211 | (getProto(b) === null && getProto(a) === Object.prototype))) 1212 | { 1213 | return false; 1214 | } 1215 | } 1216 | 1217 | // stack constructor before traversing properties 1218 | callers.push(a.constructor); 1219 | // track reference to avoid circular references 1220 | parents.push(a); 1221 | 1222 | for (i in a) { // be strict: don't ensures hasOwnProperty 1223 | // and go deep 1224 | loop = false; 1225 | for (j = 0; j < parents.length; j++) { 1226 | if (parents[j] === a[i]) { 1227 | // don't go down the same path twice 1228 | loop = true; 1229 | } 1230 | } 1231 | aProperties.push(i); // collect a's properties 1232 | 1233 | if (!loop && !innerEquiv(a[i], b[i])) { 1234 | eq = false; 1235 | break; 1236 | } 1237 | } 1238 | 1239 | callers.pop(); // unstack, we are done 1240 | parents.pop(); 1241 | 1242 | for (i in b) { 1243 | bProperties.push(i); // collect b's properties 1244 | } 1245 | 1246 | // Ensures identical properties name 1247 | return eq && innerEquiv(aProperties.sort(), bProperties.sort()); 1248 | } 1249 | }; 1250 | }()); 1251 | 1252 | innerEquiv = function() { // can take multiple arguments 1253 | var args = Array.prototype.slice.apply(arguments); 1254 | if (args.length < 2) { 1255 | return true; // end transition 1256 | } 1257 | 1258 | return (function(a, b) { 1259 | if (a === b) { 1260 | return true; // catch the most you can 1261 | } else if (a === null || b === null || typeof a === "undefined" || 1262 | typeof b === "undefined" || 1263 | QUnit.objectType(a) !== QUnit.objectType(b)) { 1264 | return false; // don't lose time with error prone cases 1265 | } else { 1266 | return bindCallbacks(a, callbacks, [ b, a ]); 1267 | } 1268 | 1269 | // apply transition with (1..n) arguments 1270 | }(args[0], args[1]) && arguments.callee.apply(this, args.splice(1, args.length - 1))); 1271 | }; 1272 | 1273 | return innerEquiv; 1274 | 1275 | }()); 1276 | 1277 | /** 1278 | * jsDump Copyright (c) 2008 Ariel Flesler - aflesler(at)gmail(dot)com | 1279 | * http://flesler.blogspot.com Licensed under BSD 1280 | * (http://www.opensource.org/licenses/bsd-license.php) Date: 5/15/2008 1281 | * 1282 | * @projectDescription Advanced and extensible data dumping for Javascript. 1283 | * @version 1.0.0 1284 | * @author Ariel Flesler 1285 | * @link {http://flesler.blogspot.com/2008/05/jsdump-pretty-dump-of-any-javascript.html} 1286 | */ 1287 | QUnit.jsDump = (function() { 1288 | function quote( str ) { 1289 | return '"' + str.toString().replace(/"/g, '\\"') + '"'; 1290 | } 1291 | function literal( o ) { 1292 | return o + ''; 1293 | } 1294 | function join( pre, arr, post ) { 1295 | var s = jsDump.separator(), 1296 | base = jsDump.indent(), 1297 | inner = jsDump.indent(1); 1298 | if ( arr.join ) { 1299 | arr = arr.join( ',' + s + inner ); 1300 | } 1301 | if ( !arr ) { 1302 | return pre + post; 1303 | } 1304 | return [ pre, inner + arr, base + post ].join(s); 1305 | } 1306 | function array( arr, stack ) { 1307 | var i = arr.length, ret = new Array(i); 1308 | this.up(); 1309 | while ( i-- ) { 1310 | ret[i] = this.parse( arr[i] , undefined , stack); 1311 | } 1312 | this.down(); 1313 | return join( '[', ret, ']' ); 1314 | } 1315 | 1316 | var reName = /^function (\w+)/; 1317 | 1318 | var jsDump = { 1319 | parse: function( obj, type, stack ) { //type is used mostly internally, you can fix a (custom)type in advance 1320 | stack = stack || [ ]; 1321 | var parser = this.parsers[ type || this.typeOf(obj) ]; 1322 | type = typeof parser; 1323 | var inStack = inArray(obj, stack); 1324 | if (inStack != -1) { 1325 | return 'recursion('+(inStack - stack.length)+')'; 1326 | } 1327 | //else 1328 | if (type == 'function') { 1329 | stack.push(obj); 1330 | var res = parser.call( this, obj, stack ); 1331 | stack.pop(); 1332 | return res; 1333 | } 1334 | // else 1335 | return (type == 'string') ? parser : this.parsers.error; 1336 | }, 1337 | typeOf: function( obj ) { 1338 | var type; 1339 | if ( obj === null ) { 1340 | type = "null"; 1341 | } else if (typeof obj === "undefined") { 1342 | type = "undefined"; 1343 | } else if (QUnit.is("RegExp", obj)) { 1344 | type = "regexp"; 1345 | } else if (QUnit.is("Date", obj)) { 1346 | type = "date"; 1347 | } else if (QUnit.is("Function", obj)) { 1348 | type = "function"; 1349 | } else if (typeof obj.setInterval !== undefined && typeof obj.document !== "undefined" && typeof obj.nodeType === "undefined") { 1350 | type = "window"; 1351 | } else if (obj.nodeType === 9) { 1352 | type = "document"; 1353 | } else if (obj.nodeType) { 1354 | type = "node"; 1355 | } else if ( 1356 | // native arrays 1357 | toString.call( obj ) === "[object Array]" || 1358 | // NodeList objects 1359 | ( typeof obj.length === "number" && typeof obj.item !== "undefined" && ( obj.length ? obj.item(0) === obj[0] : ( obj.item( 0 ) === null && typeof obj[0] === "undefined" ) ) ) 1360 | ) { 1361 | type = "array"; 1362 | } else { 1363 | type = typeof obj; 1364 | } 1365 | return type; 1366 | }, 1367 | separator: function() { 1368 | return this.multiline ? this.HTML ? '
    ' : '\n' : this.HTML ? ' ' : ' '; 1369 | }, 1370 | indent: function( extra ) {// extra can be a number, shortcut for increasing-calling-decreasing 1371 | if ( !this.multiline ) { 1372 | return ''; 1373 | } 1374 | var chr = this.indentChar; 1375 | if ( this.HTML ) { 1376 | chr = chr.replace(/\t/g,' ').replace(/ /g,' '); 1377 | } 1378 | return new Array( this._depth_ + (extra||0) ).join(chr); 1379 | }, 1380 | up: function( a ) { 1381 | this._depth_ += a || 1; 1382 | }, 1383 | down: function( a ) { 1384 | this._depth_ -= a || 1; 1385 | }, 1386 | setParser: function( name, parser ) { 1387 | this.parsers[name] = parser; 1388 | }, 1389 | // The next 3 are exposed so you can use them 1390 | quote: quote, 1391 | literal: literal, 1392 | join: join, 1393 | // 1394 | _depth_: 1, 1395 | // This is the list of parsers, to modify them, use jsDump.setParser 1396 | parsers: { 1397 | window: '[Window]', 1398 | document: '[Document]', 1399 | error: '[ERROR]', //when no parser is found, shouldn't happen 1400 | unknown: '[Unknown]', 1401 | 'null': 'null', 1402 | 'undefined': 'undefined', 1403 | 'function': function( fn ) { 1404 | var ret = 'function', 1405 | name = 'name' in fn ? fn.name : (reName.exec(fn)||[])[1];//functions never have name in IE 1406 | if ( name ) { 1407 | ret += ' ' + name; 1408 | } 1409 | ret += '('; 1410 | 1411 | ret = [ ret, QUnit.jsDump.parse( fn, 'functionArgs' ), '){'].join(''); 1412 | return join( ret, QUnit.jsDump.parse(fn,'functionCode'), '}' ); 1413 | }, 1414 | array: array, 1415 | nodelist: array, 1416 | 'arguments': array, 1417 | object: function( map, stack ) { 1418 | var ret = [ ], keys, key, val, i; 1419 | QUnit.jsDump.up(); 1420 | if (Object.keys) { 1421 | keys = Object.keys( map ); 1422 | } else { 1423 | keys = []; 1424 | for (key in map) { keys.push( key ); } 1425 | } 1426 | keys.sort(); 1427 | for (i = 0; i < keys.length; i++) { 1428 | key = keys[ i ]; 1429 | val = map[ key ]; 1430 | ret.push( QUnit.jsDump.parse( key, 'key' ) + ': ' + QUnit.jsDump.parse( val, undefined, stack ) ); 1431 | } 1432 | QUnit.jsDump.down(); 1433 | return join( '{', ret, '}' ); 1434 | }, 1435 | node: function( node ) { 1436 | var open = QUnit.jsDump.HTML ? '<' : '<', 1437 | close = QUnit.jsDump.HTML ? '>' : '>'; 1438 | 1439 | var tag = node.nodeName.toLowerCase(), 1440 | ret = open + tag; 1441 | 1442 | for ( var a in QUnit.jsDump.DOMAttrs ) { 1443 | var val = node[QUnit.jsDump.DOMAttrs[a]]; 1444 | if ( val ) { 1445 | ret += ' ' + a + '=' + QUnit.jsDump.parse( val, 'attribute' ); 1446 | } 1447 | } 1448 | return ret + close + open + '/' + tag + close; 1449 | }, 1450 | functionArgs: function( fn ) {//function calls it internally, it's the arguments part of the function 1451 | var l = fn.length; 1452 | if ( !l ) { 1453 | return ''; 1454 | } 1455 | 1456 | var args = new Array(l); 1457 | while ( l-- ) { 1458 | args[l] = String.fromCharCode(97+l);//97 is 'a' 1459 | } 1460 | return ' ' + args.join(', ') + ' '; 1461 | }, 1462 | key: quote, //object calls it internally, the key part of an item in a map 1463 | functionCode: '[code]', //function calls it internally, it's the content of the function 1464 | attribute: quote, //node calls it internally, it's an html attribute value 1465 | string: quote, 1466 | date: quote, 1467 | regexp: literal, //regex 1468 | number: literal, 1469 | 'boolean': literal 1470 | }, 1471 | DOMAttrs:{//attributes to dump from nodes, name=>realName 1472 | id:'id', 1473 | name:'name', 1474 | 'class':'className' 1475 | }, 1476 | HTML:false,//if true, entities are escaped ( <, >, \t, space and \n ) 1477 | indentChar:' ',//indentation unit 1478 | multiline:true //if true, items in a collection, are separated by a \n, else just a space. 1479 | }; 1480 | 1481 | return jsDump; 1482 | }()); 1483 | 1484 | // from Sizzle.js 1485 | function getText( elems ) { 1486 | var ret = "", elem; 1487 | 1488 | for ( var i = 0; elems[i]; i++ ) { 1489 | elem = elems[i]; 1490 | 1491 | // Get the text from text nodes and CDATA nodes 1492 | if ( elem.nodeType === 3 || elem.nodeType === 4 ) { 1493 | ret += elem.nodeValue; 1494 | 1495 | // Traverse everything else, except comment nodes 1496 | } else if ( elem.nodeType !== 8 ) { 1497 | ret += getText( elem.childNodes ); 1498 | } 1499 | } 1500 | 1501 | return ret; 1502 | } 1503 | 1504 | //from jquery.js 1505 | function inArray( elem, array ) { 1506 | if ( array.indexOf ) { 1507 | return array.indexOf( elem ); 1508 | } 1509 | 1510 | for ( var i = 0, length = array.length; i < length; i++ ) { 1511 | if ( array[ i ] === elem ) { 1512 | return i; 1513 | } 1514 | } 1515 | 1516 | return -1; 1517 | } 1518 | 1519 | /* 1520 | * Javascript Diff Algorithm 1521 | * By John Resig (http://ejohn.org/) 1522 | * Modified by Chu Alan "sprite" 1523 | * 1524 | * Released under the MIT license. 1525 | * 1526 | * More Info: 1527 | * http://ejohn.org/projects/javascript-diff-algorithm/ 1528 | * 1529 | * Usage: QUnit.diff(expected, actual) 1530 | * 1531 | * QUnit.diff("the quick brown fox jumped over", "the quick fox jumps over") == "the quick brown fox jumped jumps over" 1532 | */ 1533 | QUnit.diff = (function() { 1534 | function diff(o, n) { 1535 | var ns = {}; 1536 | var os = {}; 1537 | var i; 1538 | 1539 | for (i = 0; i < n.length; i++) { 1540 | if (ns[n[i]] == null) { 1541 | ns[n[i]] = { 1542 | rows: [], 1543 | o: null 1544 | }; 1545 | } 1546 | ns[n[i]].rows.push(i); 1547 | } 1548 | 1549 | for (i = 0; i < o.length; i++) { 1550 | if (os[o[i]] == null) { 1551 | os[o[i]] = { 1552 | rows: [], 1553 | n: null 1554 | }; 1555 | } 1556 | os[o[i]].rows.push(i); 1557 | } 1558 | 1559 | for (i in ns) { 1560 | if ( !hasOwn.call( ns, i ) ) { 1561 | continue; 1562 | } 1563 | if (ns[i].rows.length == 1 && typeof(os[i]) != "undefined" && os[i].rows.length == 1) { 1564 | n[ns[i].rows[0]] = { 1565 | text: n[ns[i].rows[0]], 1566 | row: os[i].rows[0] 1567 | }; 1568 | o[os[i].rows[0]] = { 1569 | text: o[os[i].rows[0]], 1570 | row: ns[i].rows[0] 1571 | }; 1572 | } 1573 | } 1574 | 1575 | for (i = 0; i < n.length - 1; i++) { 1576 | if (n[i].text != null && n[i + 1].text == null && n[i].row + 1 < o.length && o[n[i].row + 1].text == null && 1577 | n[i + 1] == o[n[i].row + 1]) { 1578 | n[i + 1] = { 1579 | text: n[i + 1], 1580 | row: n[i].row + 1 1581 | }; 1582 | o[n[i].row + 1] = { 1583 | text: o[n[i].row + 1], 1584 | row: i + 1 1585 | }; 1586 | } 1587 | } 1588 | 1589 | for (i = n.length - 1; i > 0; i--) { 1590 | if (n[i].text != null && n[i - 1].text == null && n[i].row > 0 && o[n[i].row - 1].text == null && 1591 | n[i - 1] == o[n[i].row - 1]) { 1592 | n[i - 1] = { 1593 | text: n[i - 1], 1594 | row: n[i].row - 1 1595 | }; 1596 | o[n[i].row - 1] = { 1597 | text: o[n[i].row - 1], 1598 | row: i - 1 1599 | }; 1600 | } 1601 | } 1602 | 1603 | return { 1604 | o: o, 1605 | n: n 1606 | }; 1607 | } 1608 | 1609 | return function(o, n) { 1610 | o = o.replace(/\s+$/, ''); 1611 | n = n.replace(/\s+$/, ''); 1612 | var out = diff(o === "" ? [] : o.split(/\s+/), n === "" ? [] : n.split(/\s+/)); 1613 | 1614 | var str = ""; 1615 | var i; 1616 | 1617 | var oSpace = o.match(/\s+/g); 1618 | if (oSpace == null) { 1619 | oSpace = [" "]; 1620 | } 1621 | else { 1622 | oSpace.push(" "); 1623 | } 1624 | var nSpace = n.match(/\s+/g); 1625 | if (nSpace == null) { 1626 | nSpace = [" "]; 1627 | } 1628 | else { 1629 | nSpace.push(" "); 1630 | } 1631 | 1632 | if (out.n.length === 0) { 1633 | for (i = 0; i < out.o.length; i++) { 1634 | str += '' + out.o[i] + oSpace[i] + ""; 1635 | } 1636 | } 1637 | else { 1638 | if (out.n[0].text == null) { 1639 | for (n = 0; n < out.o.length && out.o[n].text == null; n++) { 1640 | str += '' + out.o[n] + oSpace[n] + ""; 1641 | } 1642 | } 1643 | 1644 | for (i = 0; i < out.n.length; i++) { 1645 | if (out.n[i].text == null) { 1646 | str += '' + out.n[i] + nSpace[i] + ""; 1647 | } 1648 | else { 1649 | var pre = ""; 1650 | 1651 | for (n = out.n[i].row + 1; n < out.o.length && out.o[n].text == null; n++) { 1652 | pre += '' + out.o[n] + oSpace[n] + ""; 1653 | } 1654 | str += " " + out.n[i].text + nSpace[i] + pre; 1655 | } 1656 | } 1657 | } 1658 | 1659 | return str; 1660 | }; 1661 | }()); 1662 | 1663 | // for CommonJS enviroments, export everything 1664 | if ( typeof exports !== "undefined" || typeof require !== "undefined" ) { 1665 | extend(exports, QUnit); 1666 | } 1667 | 1668 | // get at whatever the global object is, like window in browsers 1669 | }( (function() {return this;}.call()) )); 1670 | -------------------------------------------------------------------------------- /test/lib/underscore-1.3.3.js: -------------------------------------------------------------------------------- 1 | // Underscore.js 1.3.3 2 | // (c) 2009-2012 Jeremy Ashkenas, DocumentCloud Inc. 3 | // Underscore is freely distributable under the MIT license. 4 | // Portions of Underscore are inspired or borrowed from Prototype, 5 | // Oliver Steele's Functional, and John Resig's Micro-Templating. 6 | // For all details and documentation: 7 | // http://documentcloud.github.com/underscore 8 | 9 | (function() { 10 | 11 | // Baseline setup 12 | // -------------- 13 | 14 | // Establish the root object, `window` in the browser, or `global` on the server. 15 | var root = this; 16 | 17 | // Save the previous value of the `_` variable. 18 | var previousUnderscore = root._; 19 | 20 | // Establish the object that gets returned to break out of a loop iteration. 21 | var breaker = {}; 22 | 23 | // Save bytes in the minified (but not gzipped) version: 24 | var ArrayProto = Array.prototype, ObjProto = Object.prototype, FuncProto = Function.prototype; 25 | 26 | // Create quick reference variables for speed access to core prototypes. 27 | var slice = ArrayProto.slice, 28 | unshift = ArrayProto.unshift, 29 | toString = ObjProto.toString, 30 | hasOwnProperty = ObjProto.hasOwnProperty; 31 | 32 | // All **ECMAScript 5** native function implementations that we hope to use 33 | // are declared here. 34 | var 35 | nativeForEach = ArrayProto.forEach, 36 | nativeMap = ArrayProto.map, 37 | nativeReduce = ArrayProto.reduce, 38 | nativeReduceRight = ArrayProto.reduceRight, 39 | nativeFilter = ArrayProto.filter, 40 | nativeEvery = ArrayProto.every, 41 | nativeSome = ArrayProto.some, 42 | nativeIndexOf = ArrayProto.indexOf, 43 | nativeLastIndexOf = ArrayProto.lastIndexOf, 44 | nativeIsArray = Array.isArray, 45 | nativeKeys = Object.keys, 46 | nativeBind = FuncProto.bind; 47 | 48 | // Create a safe reference to the Underscore object for use below. 49 | var _ = function(obj) { return new wrapper(obj); }; 50 | 51 | // Export the Underscore object for **Node.js**, with 52 | // backwards-compatibility for the old `require()` API. If we're in 53 | // the browser, add `_` as a global object via a string identifier, 54 | // for Closure Compiler "advanced" mode. 55 | if (typeof exports !== 'undefined') { 56 | if (typeof module !== 'undefined' && module.exports) { 57 | exports = module.exports = _; 58 | } 59 | exports._ = _; 60 | } else { 61 | root['_'] = _; 62 | } 63 | 64 | // Current version. 65 | _.VERSION = '1.3.3'; 66 | 67 | // Collection Functions 68 | // -------------------- 69 | 70 | // The cornerstone, an `each` implementation, aka `forEach`. 71 | // Handles objects with the built-in `forEach`, arrays, and raw objects. 72 | // Delegates to **ECMAScript 5**'s native `forEach` if available. 73 | var each = _.each = _.forEach = function(obj, iterator, context) { 74 | if (obj == null) return; 75 | if (nativeForEach && obj.forEach === nativeForEach) { 76 | obj.forEach(iterator, context); 77 | } else if (obj.length === +obj.length) { 78 | for (var i = 0, l = obj.length; i < l; i++) { 79 | if (i in obj && iterator.call(context, obj[i], i, obj) === breaker) return; 80 | } 81 | } else { 82 | for (var key in obj) { 83 | if (_.has(obj, key)) { 84 | if (iterator.call(context, obj[key], key, obj) === breaker) return; 85 | } 86 | } 87 | } 88 | }; 89 | 90 | // Return the results of applying the iterator to each element. 91 | // Delegates to **ECMAScript 5**'s native `map` if available. 92 | _.map = _.collect = function(obj, iterator, context) { 93 | var results = []; 94 | if (obj == null) return results; 95 | if (nativeMap && obj.map === nativeMap) return obj.map(iterator, context); 96 | each(obj, function(value, index, list) { 97 | results[results.length] = iterator.call(context, value, index, list); 98 | }); 99 | if (obj.length === +obj.length) results.length = obj.length; 100 | return results; 101 | }; 102 | 103 | // **Reduce** builds up a single result from a list of values, aka `inject`, 104 | // or `foldl`. Delegates to **ECMAScript 5**'s native `reduce` if available. 105 | _.reduce = _.foldl = _.inject = function(obj, iterator, memo, context) { 106 | var initial = arguments.length > 2; 107 | if (obj == null) obj = []; 108 | if (nativeReduce && obj.reduce === nativeReduce) { 109 | if (context) iterator = _.bind(iterator, context); 110 | return initial ? obj.reduce(iterator, memo) : obj.reduce(iterator); 111 | } 112 | each(obj, function(value, index, list) { 113 | if (!initial) { 114 | memo = value; 115 | initial = true; 116 | } else { 117 | memo = iterator.call(context, memo, value, index, list); 118 | } 119 | }); 120 | if (!initial) throw new TypeError('Reduce of empty array with no initial value'); 121 | return memo; 122 | }; 123 | 124 | // The right-associative version of reduce, also known as `foldr`. 125 | // Delegates to **ECMAScript 5**'s native `reduceRight` if available. 126 | _.reduceRight = _.foldr = function(obj, iterator, memo, context) { 127 | var initial = arguments.length > 2; 128 | if (obj == null) obj = []; 129 | if (nativeReduceRight && obj.reduceRight === nativeReduceRight) { 130 | if (context) iterator = _.bind(iterator, context); 131 | return initial ? obj.reduceRight(iterator, memo) : obj.reduceRight(iterator); 132 | } 133 | var reversed = _.toArray(obj).reverse(); 134 | if (context && !initial) iterator = _.bind(iterator, context); 135 | return initial ? _.reduce(reversed, iterator, memo, context) : _.reduce(reversed, iterator); 136 | }; 137 | 138 | // Return the first value which passes a truth test. Aliased as `detect`. 139 | _.find = _.detect = function(obj, iterator, context) { 140 | var result; 141 | any(obj, function(value, index, list) { 142 | if (iterator.call(context, value, index, list)) { 143 | result = value; 144 | return true; 145 | } 146 | }); 147 | return result; 148 | }; 149 | 150 | // Return all the elements that pass a truth test. 151 | // Delegates to **ECMAScript 5**'s native `filter` if available. 152 | // Aliased as `select`. 153 | _.filter = _.select = function(obj, iterator, context) { 154 | var results = []; 155 | if (obj == null) return results; 156 | if (nativeFilter && obj.filter === nativeFilter) return obj.filter(iterator, context); 157 | each(obj, function(value, index, list) { 158 | if (iterator.call(context, value, index, list)) results[results.length] = value; 159 | }); 160 | return results; 161 | }; 162 | 163 | // Return all the elements for which a truth test fails. 164 | _.reject = function(obj, iterator, context) { 165 | var results = []; 166 | if (obj == null) return results; 167 | each(obj, function(value, index, list) { 168 | if (!iterator.call(context, value, index, list)) results[results.length] = value; 169 | }); 170 | return results; 171 | }; 172 | 173 | // Determine whether all of the elements match a truth test. 174 | // Delegates to **ECMAScript 5**'s native `every` if available. 175 | // Aliased as `all`. 176 | _.every = _.all = function(obj, iterator, context) { 177 | var result = true; 178 | if (obj == null) return result; 179 | if (nativeEvery && obj.every === nativeEvery) return obj.every(iterator, context); 180 | each(obj, function(value, index, list) { 181 | if (!(result = result && iterator.call(context, value, index, list))) return breaker; 182 | }); 183 | return !!result; 184 | }; 185 | 186 | // Determine if at least one element in the object matches a truth test. 187 | // Delegates to **ECMAScript 5**'s native `some` if available. 188 | // Aliased as `any`. 189 | var any = _.some = _.any = function(obj, iterator, context) { 190 | iterator || (iterator = _.identity); 191 | var result = false; 192 | if (obj == null) return result; 193 | if (nativeSome && obj.some === nativeSome) return obj.some(iterator, context); 194 | each(obj, function(value, index, list) { 195 | if (result || (result = iterator.call(context, value, index, list))) return breaker; 196 | }); 197 | return !!result; 198 | }; 199 | 200 | // Determine if a given value is included in the array or object using `===`. 201 | // Aliased as `contains`. 202 | _.include = _.contains = function(obj, target) { 203 | var found = false; 204 | if (obj == null) return found; 205 | if (nativeIndexOf && obj.indexOf === nativeIndexOf) return obj.indexOf(target) != -1; 206 | found = any(obj, function(value) { 207 | return value === target; 208 | }); 209 | return found; 210 | }; 211 | 212 | // Invoke a method (with arguments) on every item in a collection. 213 | _.invoke = function(obj, method) { 214 | var args = slice.call(arguments, 2); 215 | return _.map(obj, function(value) { 216 | return (_.isFunction(method) ? method || value : value[method]).apply(value, args); 217 | }); 218 | }; 219 | 220 | // Convenience version of a common use case of `map`: fetching a property. 221 | _.pluck = function(obj, key) { 222 | return _.map(obj, function(value){ return value[key]; }); 223 | }; 224 | 225 | // Return the maximum element or (element-based computation). 226 | _.max = function(obj, iterator, context) { 227 | if (!iterator && _.isArray(obj) && obj[0] === +obj[0]) return Math.max.apply(Math, obj); 228 | if (!iterator && _.isEmpty(obj)) return -Infinity; 229 | var result = {computed : -Infinity}; 230 | each(obj, function(value, index, list) { 231 | var computed = iterator ? iterator.call(context, value, index, list) : value; 232 | computed >= result.computed && (result = {value : value, computed : computed}); 233 | }); 234 | return result.value; 235 | }; 236 | 237 | // Return the minimum element (or element-based computation). 238 | _.min = function(obj, iterator, context) { 239 | if (!iterator && _.isArray(obj) && obj[0] === +obj[0]) return Math.min.apply(Math, obj); 240 | if (!iterator && _.isEmpty(obj)) return Infinity; 241 | var result = {computed : Infinity}; 242 | each(obj, function(value, index, list) { 243 | var computed = iterator ? iterator.call(context, value, index, list) : value; 244 | computed < result.computed && (result = {value : value, computed : computed}); 245 | }); 246 | return result.value; 247 | }; 248 | 249 | // Shuffle an array. 250 | _.shuffle = function(obj) { 251 | var shuffled = [], rand; 252 | each(obj, function(value, index, list) { 253 | rand = Math.floor(Math.random() * (index + 1)); 254 | shuffled[index] = shuffled[rand]; 255 | shuffled[rand] = value; 256 | }); 257 | return shuffled; 258 | }; 259 | 260 | // Sort the object's values by a criterion produced by an iterator. 261 | _.sortBy = function(obj, val, context) { 262 | var iterator = _.isFunction(val) ? val : function(obj) { return obj[val]; }; 263 | return _.pluck(_.map(obj, function(value, index, list) { 264 | return { 265 | value : value, 266 | criteria : iterator.call(context, value, index, list) 267 | }; 268 | }).sort(function(left, right) { 269 | var a = left.criteria, b = right.criteria; 270 | if (a === void 0) return 1; 271 | if (b === void 0) return -1; 272 | return a < b ? -1 : a > b ? 1 : 0; 273 | }), 'value'); 274 | }; 275 | 276 | // Groups the object's values by a criterion. Pass either a string attribute 277 | // to group by, or a function that returns the criterion. 278 | _.groupBy = function(obj, val) { 279 | var result = {}; 280 | var iterator = _.isFunction(val) ? val : function(obj) { return obj[val]; }; 281 | each(obj, function(value, index) { 282 | var key = iterator(value, index); 283 | (result[key] || (result[key] = [])).push(value); 284 | }); 285 | return result; 286 | }; 287 | 288 | // Use a comparator function to figure out at what index an object should 289 | // be inserted so as to maintain order. Uses binary search. 290 | _.sortedIndex = function(array, obj, iterator) { 291 | iterator || (iterator = _.identity); 292 | var low = 0, high = array.length; 293 | while (low < high) { 294 | var mid = (low + high) >> 1; 295 | iterator(array[mid]) < iterator(obj) ? low = mid + 1 : high = mid; 296 | } 297 | return low; 298 | }; 299 | 300 | // Safely convert anything iterable into a real, live array. 301 | _.toArray = function(obj) { 302 | if (!obj) return []; 303 | if (_.isArray(obj)) return slice.call(obj); 304 | if (_.isArguments(obj)) return slice.call(obj); 305 | if (obj.toArray && _.isFunction(obj.toArray)) return obj.toArray(); 306 | return _.values(obj); 307 | }; 308 | 309 | // Return the number of elements in an object. 310 | _.size = function(obj) { 311 | return _.isArray(obj) ? obj.length : _.keys(obj).length; 312 | }; 313 | 314 | // Array Functions 315 | // --------------- 316 | 317 | // Get the first element of an array. Passing **n** will return the first N 318 | // values in the array. Aliased as `head` and `take`. The **guard** check 319 | // allows it to work with `_.map`. 320 | _.first = _.head = _.take = function(array, n, guard) { 321 | return (n != null) && !guard ? slice.call(array, 0, n) : array[0]; 322 | }; 323 | 324 | // Returns everything but the last entry of the array. Especcialy useful on 325 | // the arguments object. Passing **n** will return all the values in 326 | // the array, excluding the last N. The **guard** check allows it to work with 327 | // `_.map`. 328 | _.initial = function(array, n, guard) { 329 | return slice.call(array, 0, array.length - ((n == null) || guard ? 1 : n)); 330 | }; 331 | 332 | // Get the last element of an array. Passing **n** will return the last N 333 | // values in the array. The **guard** check allows it to work with `_.map`. 334 | _.last = function(array, n, guard) { 335 | if ((n != null) && !guard) { 336 | return slice.call(array, Math.max(array.length - n, 0)); 337 | } else { 338 | return array[array.length - 1]; 339 | } 340 | }; 341 | 342 | // Returns everything but the first entry of the array. Aliased as `tail`. 343 | // Especially useful on the arguments object. Passing an **index** will return 344 | // the rest of the values in the array from that index onward. The **guard** 345 | // check allows it to work with `_.map`. 346 | _.rest = _.tail = function(array, index, guard) { 347 | return slice.call(array, (index == null) || guard ? 1 : index); 348 | }; 349 | 350 | // Trim out all falsy values from an array. 351 | _.compact = function(array) { 352 | return _.filter(array, function(value){ return !!value; }); 353 | }; 354 | 355 | // Return a completely flattened version of an array. 356 | _.flatten = function(array, shallow) { 357 | return _.reduce(array, function(memo, value) { 358 | if (_.isArray(value)) return memo.concat(shallow ? value : _.flatten(value)); 359 | memo[memo.length] = value; 360 | return memo; 361 | }, []); 362 | }; 363 | 364 | // Return a version of the array that does not contain the specified value(s). 365 | _.without = function(array) { 366 | return _.difference(array, slice.call(arguments, 1)); 367 | }; 368 | 369 | // Produce a duplicate-free version of the array. If the array has already 370 | // been sorted, you have the option of using a faster algorithm. 371 | // Aliased as `unique`. 372 | _.uniq = _.unique = function(array, isSorted, iterator) { 373 | var initial = iterator ? _.map(array, iterator) : array; 374 | var results = []; 375 | // The `isSorted` flag is irrelevant if the array only contains two elements. 376 | if (array.length < 3) isSorted = true; 377 | _.reduce(initial, function (memo, value, index) { 378 | if (isSorted ? _.last(memo) !== value || !memo.length : !_.include(memo, value)) { 379 | memo.push(value); 380 | results.push(array[index]); 381 | } 382 | return memo; 383 | }, []); 384 | return results; 385 | }; 386 | 387 | // Produce an array that contains the union: each distinct element from all of 388 | // the passed-in arrays. 389 | _.union = function() { 390 | return _.uniq(_.flatten(arguments, true)); 391 | }; 392 | 393 | // Produce an array that contains every item shared between all the 394 | // passed-in arrays. (Aliased as "intersect" for back-compat.) 395 | _.intersection = _.intersect = function(array) { 396 | var rest = slice.call(arguments, 1); 397 | return _.filter(_.uniq(array), function(item) { 398 | return _.every(rest, function(other) { 399 | return _.indexOf(other, item) >= 0; 400 | }); 401 | }); 402 | }; 403 | 404 | // Take the difference between one array and a number of other arrays. 405 | // Only the elements present in just the first array will remain. 406 | _.difference = function(array) { 407 | var rest = _.flatten(slice.call(arguments, 1), true); 408 | return _.filter(array, function(value){ return !_.include(rest, value); }); 409 | }; 410 | 411 | // Zip together multiple lists into a single array -- elements that share 412 | // an index go together. 413 | _.zip = function() { 414 | var args = slice.call(arguments); 415 | var length = _.max(_.pluck(args, 'length')); 416 | var results = new Array(length); 417 | for (var i = 0; i < length; i++) results[i] = _.pluck(args, "" + i); 418 | return results; 419 | }; 420 | 421 | // If the browser doesn't supply us with indexOf (I'm looking at you, **MSIE**), 422 | // we need this function. Return the position of the first occurrence of an 423 | // item in an array, or -1 if the item is not included in the array. 424 | // Delegates to **ECMAScript 5**'s native `indexOf` if available. 425 | // If the array is large and already in sort order, pass `true` 426 | // for **isSorted** to use binary search. 427 | _.indexOf = function(array, item, isSorted) { 428 | if (array == null) return -1; 429 | var i, l; 430 | if (isSorted) { 431 | i = _.sortedIndex(array, item); 432 | return array[i] === item ? i : -1; 433 | } 434 | if (nativeIndexOf && array.indexOf === nativeIndexOf) return array.indexOf(item); 435 | for (i = 0, l = array.length; i < l; i++) if (i in array && array[i] === item) return i; 436 | return -1; 437 | }; 438 | 439 | // Delegates to **ECMAScript 5**'s native `lastIndexOf` if available. 440 | _.lastIndexOf = function(array, item) { 441 | if (array == null) return -1; 442 | if (nativeLastIndexOf && array.lastIndexOf === nativeLastIndexOf) return array.lastIndexOf(item); 443 | var i = array.length; 444 | while (i--) if (i in array && array[i] === item) return i; 445 | return -1; 446 | }; 447 | 448 | // Generate an integer Array containing an arithmetic progression. A port of 449 | // the native Python `range()` function. See 450 | // [the Python documentation](http://docs.python.org/library/functions.html#range). 451 | _.range = function(start, stop, step) { 452 | if (arguments.length <= 1) { 453 | stop = start || 0; 454 | start = 0; 455 | } 456 | step = arguments[2] || 1; 457 | 458 | var len = Math.max(Math.ceil((stop - start) / step), 0); 459 | var idx = 0; 460 | var range = new Array(len); 461 | 462 | while(idx < len) { 463 | range[idx++] = start; 464 | start += step; 465 | } 466 | 467 | return range; 468 | }; 469 | 470 | // Function (ahem) Functions 471 | // ------------------ 472 | 473 | // Reusable constructor function for prototype setting. 474 | var ctor = function(){}; 475 | 476 | // Create a function bound to a given object (assigning `this`, and arguments, 477 | // optionally). Binding with arguments is also known as `curry`. 478 | // Delegates to **ECMAScript 5**'s native `Function.bind` if available. 479 | // We check for `func.bind` first, to fail fast when `func` is undefined. 480 | _.bind = function bind(func, context) { 481 | var bound, args; 482 | if (func.bind === nativeBind && nativeBind) return nativeBind.apply(func, slice.call(arguments, 1)); 483 | if (!_.isFunction(func)) throw new TypeError; 484 | args = slice.call(arguments, 2); 485 | return bound = function() { 486 | if (!(this instanceof bound)) return func.apply(context, args.concat(slice.call(arguments))); 487 | ctor.prototype = func.prototype; 488 | var self = new ctor; 489 | var result = func.apply(self, args.concat(slice.call(arguments))); 490 | if (Object(result) === result) return result; 491 | return self; 492 | }; 493 | }; 494 | 495 | // Bind all of an object's methods to that object. Useful for ensuring that 496 | // all callbacks defined on an object belong to it. 497 | _.bindAll = function(obj) { 498 | var funcs = slice.call(arguments, 1); 499 | if (funcs.length == 0) funcs = _.functions(obj); 500 | each(funcs, function(f) { obj[f] = _.bind(obj[f], obj); }); 501 | return obj; 502 | }; 503 | 504 | // Memoize an expensive function by storing its results. 505 | _.memoize = function(func, hasher) { 506 | var memo = {}; 507 | hasher || (hasher = _.identity); 508 | return function() { 509 | var key = hasher.apply(this, arguments); 510 | return _.has(memo, key) ? memo[key] : (memo[key] = func.apply(this, arguments)); 511 | }; 512 | }; 513 | 514 | // Delays a function for the given number of milliseconds, and then calls 515 | // it with the arguments supplied. 516 | _.delay = function(func, wait) { 517 | var args = slice.call(arguments, 2); 518 | return setTimeout(function(){ return func.apply(null, args); }, wait); 519 | }; 520 | 521 | // Defers a function, scheduling it to run after the current call stack has 522 | // cleared. 523 | _.defer = function(func) { 524 | return _.delay.apply(_, [func, 1].concat(slice.call(arguments, 1))); 525 | }; 526 | 527 | // Returns a function, that, when invoked, will only be triggered at most once 528 | // during a given window of time. 529 | _.throttle = function(func, wait) { 530 | var context, args, timeout, throttling, more, result; 531 | var whenDone = _.debounce(function(){ more = throttling = false; }, wait); 532 | return function() { 533 | context = this; args = arguments; 534 | var later = function() { 535 | timeout = null; 536 | if (more) func.apply(context, args); 537 | whenDone(); 538 | }; 539 | if (!timeout) timeout = setTimeout(later, wait); 540 | if (throttling) { 541 | more = true; 542 | } else { 543 | result = func.apply(context, args); 544 | } 545 | whenDone(); 546 | throttling = true; 547 | return result; 548 | }; 549 | }; 550 | 551 | // Returns a function, that, as long as it continues to be invoked, will not 552 | // be triggered. The function will be called after it stops being called for 553 | // N milliseconds. If `immediate` is passed, trigger the function on the 554 | // leading edge, instead of the trailing. 555 | _.debounce = function(func, wait, immediate) { 556 | var timeout; 557 | return function() { 558 | var context = this, args = arguments; 559 | var later = function() { 560 | timeout = null; 561 | if (!immediate) func.apply(context, args); 562 | }; 563 | if (immediate && !timeout) func.apply(context, args); 564 | clearTimeout(timeout); 565 | timeout = setTimeout(later, wait); 566 | }; 567 | }; 568 | 569 | // Returns a function that will be executed at most one time, no matter how 570 | // often you call it. Useful for lazy initialization. 571 | _.once = function(func) { 572 | var ran = false, memo; 573 | return function() { 574 | if (ran) return memo; 575 | ran = true; 576 | return memo = func.apply(this, arguments); 577 | }; 578 | }; 579 | 580 | // Returns the first function passed as an argument to the second, 581 | // allowing you to adjust arguments, run code before and after, and 582 | // conditionally execute the original function. 583 | _.wrap = function(func, wrapper) { 584 | return function() { 585 | var args = [func].concat(slice.call(arguments, 0)); 586 | return wrapper.apply(this, args); 587 | }; 588 | }; 589 | 590 | // Returns a function that is the composition of a list of functions, each 591 | // consuming the return value of the function that follows. 592 | _.compose = function() { 593 | var funcs = arguments; 594 | return function() { 595 | var args = arguments; 596 | for (var i = funcs.length - 1; i >= 0; i--) { 597 | args = [funcs[i].apply(this, args)]; 598 | } 599 | return args[0]; 600 | }; 601 | }; 602 | 603 | // Returns a function that will only be executed after being called N times. 604 | _.after = function(times, func) { 605 | if (times <= 0) return func(); 606 | return function() { 607 | if (--times < 1) { return func.apply(this, arguments); } 608 | }; 609 | }; 610 | 611 | // Object Functions 612 | // ---------------- 613 | 614 | // Retrieve the names of an object's properties. 615 | // Delegates to **ECMAScript 5**'s native `Object.keys` 616 | _.keys = nativeKeys || function(obj) { 617 | if (obj !== Object(obj)) throw new TypeError('Invalid object'); 618 | var keys = []; 619 | for (var key in obj) if (_.has(obj, key)) keys[keys.length] = key; 620 | return keys; 621 | }; 622 | 623 | // Retrieve the values of an object's properties. 624 | _.values = function(obj) { 625 | return _.map(obj, _.identity); 626 | }; 627 | 628 | // Return a sorted list of the function names available on the object. 629 | // Aliased as `methods` 630 | _.functions = _.methods = function(obj) { 631 | var names = []; 632 | for (var key in obj) { 633 | if (_.isFunction(obj[key])) names.push(key); 634 | } 635 | return names.sort(); 636 | }; 637 | 638 | // Extend a given object with all the properties in passed-in object(s). 639 | _.extend = function(obj) { 640 | each(slice.call(arguments, 1), function(source) { 641 | for (var prop in source) { 642 | obj[prop] = source[prop]; 643 | } 644 | }); 645 | return obj; 646 | }; 647 | 648 | // Return a copy of the object only containing the whitelisted properties. 649 | _.pick = function(obj) { 650 | var result = {}; 651 | each(_.flatten(slice.call(arguments, 1)), function(key) { 652 | if (key in obj) result[key] = obj[key]; 653 | }); 654 | return result; 655 | }; 656 | 657 | // Fill in a given object with default properties. 658 | _.defaults = function(obj) { 659 | each(slice.call(arguments, 1), function(source) { 660 | for (var prop in source) { 661 | if (obj[prop] == null) obj[prop] = source[prop]; 662 | } 663 | }); 664 | return obj; 665 | }; 666 | 667 | // Create a (shallow-cloned) duplicate of an object. 668 | _.clone = function(obj) { 669 | if (!_.isObject(obj)) return obj; 670 | return _.isArray(obj) ? obj.slice() : _.extend({}, obj); 671 | }; 672 | 673 | // Invokes interceptor with the obj, and then returns obj. 674 | // The primary purpose of this method is to "tap into" a method chain, in 675 | // order to perform operations on intermediate results within the chain. 676 | _.tap = function(obj, interceptor) { 677 | interceptor(obj); 678 | return obj; 679 | }; 680 | 681 | // Internal recursive comparison function. 682 | function eq(a, b, stack) { 683 | // Identical objects are equal. `0 === -0`, but they aren't identical. 684 | // See the Harmony `egal` proposal: http://wiki.ecmascript.org/doku.php?id=harmony:egal. 685 | if (a === b) return a !== 0 || 1 / a == 1 / b; 686 | // A strict comparison is necessary because `null == undefined`. 687 | if (a == null || b == null) return a === b; 688 | // Unwrap any wrapped objects. 689 | if (a._chain) a = a._wrapped; 690 | if (b._chain) b = b._wrapped; 691 | // Invoke a custom `isEqual` method if one is provided. 692 | if (a.isEqual && _.isFunction(a.isEqual)) return a.isEqual(b); 693 | if (b.isEqual && _.isFunction(b.isEqual)) return b.isEqual(a); 694 | // Compare `[[Class]]` names. 695 | var className = toString.call(a); 696 | if (className != toString.call(b)) return false; 697 | switch (className) { 698 | // Strings, numbers, dates, and booleans are compared by value. 699 | case '[object String]': 700 | // Primitives and their corresponding object wrappers are equivalent; thus, `"5"` is 701 | // equivalent to `new String("5")`. 702 | return a == String(b); 703 | case '[object Number]': 704 | // `NaN`s are equivalent, but non-reflexive. An `egal` comparison is performed for 705 | // other numeric values. 706 | return a != +a ? b != +b : (a == 0 ? 1 / a == 1 / b : a == +b); 707 | case '[object Date]': 708 | case '[object Boolean]': 709 | // Coerce dates and booleans to numeric primitive values. Dates are compared by their 710 | // millisecond representations. Note that invalid dates with millisecond representations 711 | // of `NaN` are not equivalent. 712 | return +a == +b; 713 | // RegExps are compared by their source patterns and flags. 714 | case '[object RegExp]': 715 | return a.source == b.source && 716 | a.global == b.global && 717 | a.multiline == b.multiline && 718 | a.ignoreCase == b.ignoreCase; 719 | } 720 | if (typeof a != 'object' || typeof b != 'object') return false; 721 | // Assume equality for cyclic structures. The algorithm for detecting cyclic 722 | // structures is adapted from ES 5.1 section 15.12.3, abstract operation `JO`. 723 | var length = stack.length; 724 | while (length--) { 725 | // Linear search. Performance is inversely proportional to the number of 726 | // unique nested structures. 727 | if (stack[length] == a) return true; 728 | } 729 | // Add the first object to the stack of traversed objects. 730 | stack.push(a); 731 | var size = 0, result = true; 732 | // Recursively compare objects and arrays. 733 | if (className == '[object Array]') { 734 | // Compare array lengths to determine if a deep comparison is necessary. 735 | size = a.length; 736 | result = size == b.length; 737 | if (result) { 738 | // Deep compare the contents, ignoring non-numeric properties. 739 | while (size--) { 740 | // Ensure commutative equality for sparse arrays. 741 | if (!(result = size in a == size in b && eq(a[size], b[size], stack))) break; 742 | } 743 | } 744 | } else { 745 | // Objects with different constructors are not equivalent. 746 | if ('constructor' in a != 'constructor' in b || a.constructor != b.constructor) return false; 747 | // Deep compare objects. 748 | for (var key in a) { 749 | if (_.has(a, key)) { 750 | // Count the expected number of properties. 751 | size++; 752 | // Deep compare each member. 753 | if (!(result = _.has(b, key) && eq(a[key], b[key], stack))) break; 754 | } 755 | } 756 | // Ensure that both objects contain the same number of properties. 757 | if (result) { 758 | for (key in b) { 759 | if (_.has(b, key) && !(size--)) break; 760 | } 761 | result = !size; 762 | } 763 | } 764 | // Remove the first object from the stack of traversed objects. 765 | stack.pop(); 766 | return result; 767 | } 768 | 769 | // Perform a deep comparison to check if two objects are equal. 770 | _.isEqual = function(a, b) { 771 | return eq(a, b, []); 772 | }; 773 | 774 | // Is a given array, string, or object empty? 775 | // An "empty" object has no enumerable own-properties. 776 | _.isEmpty = function(obj) { 777 | if (obj == null) return true; 778 | if (_.isArray(obj) || _.isString(obj)) return obj.length === 0; 779 | for (var key in obj) if (_.has(obj, key)) return false; 780 | return true; 781 | }; 782 | 783 | // Is a given value a DOM element? 784 | _.isElement = function(obj) { 785 | return !!(obj && obj.nodeType == 1); 786 | }; 787 | 788 | // Is a given value an array? 789 | // Delegates to ECMA5's native Array.isArray 790 | _.isArray = nativeIsArray || function(obj) { 791 | return toString.call(obj) == '[object Array]'; 792 | }; 793 | 794 | // Is a given variable an object? 795 | _.isObject = function(obj) { 796 | return obj === Object(obj); 797 | }; 798 | 799 | // Is a given variable an arguments object? 800 | _.isArguments = function(obj) { 801 | return toString.call(obj) == '[object Arguments]'; 802 | }; 803 | if (!_.isArguments(arguments)) { 804 | _.isArguments = function(obj) { 805 | return !!(obj && _.has(obj, 'callee')); 806 | }; 807 | } 808 | 809 | // Is a given value a function? 810 | _.isFunction = function(obj) { 811 | return toString.call(obj) == '[object Function]'; 812 | }; 813 | 814 | // Is a given value a string? 815 | _.isString = function(obj) { 816 | return toString.call(obj) == '[object String]'; 817 | }; 818 | 819 | // Is a given value a number? 820 | _.isNumber = function(obj) { 821 | return toString.call(obj) == '[object Number]'; 822 | }; 823 | 824 | // Is a given object a finite number? 825 | _.isFinite = function(obj) { 826 | return _.isNumber(obj) && isFinite(obj); 827 | }; 828 | 829 | // Is the given value `NaN`? 830 | _.isNaN = function(obj) { 831 | // `NaN` is the only value for which `===` is not reflexive. 832 | return obj !== obj; 833 | }; 834 | 835 | // Is a given value a boolean? 836 | _.isBoolean = function(obj) { 837 | return obj === true || obj === false || toString.call(obj) == '[object Boolean]'; 838 | }; 839 | 840 | // Is a given value a date? 841 | _.isDate = function(obj) { 842 | return toString.call(obj) == '[object Date]'; 843 | }; 844 | 845 | // Is the given value a regular expression? 846 | _.isRegExp = function(obj) { 847 | return toString.call(obj) == '[object RegExp]'; 848 | }; 849 | 850 | // Is a given value equal to null? 851 | _.isNull = function(obj) { 852 | return obj === null; 853 | }; 854 | 855 | // Is a given variable undefined? 856 | _.isUndefined = function(obj) { 857 | return obj === void 0; 858 | }; 859 | 860 | // Has own property? 861 | _.has = function(obj, key) { 862 | return hasOwnProperty.call(obj, key); 863 | }; 864 | 865 | // Utility Functions 866 | // ----------------- 867 | 868 | // Run Underscore.js in *noConflict* mode, returning the `_` variable to its 869 | // previous owner. Returns a reference to the Underscore object. 870 | _.noConflict = function() { 871 | root._ = previousUnderscore; 872 | return this; 873 | }; 874 | 875 | // Keep the identity function around for default iterators. 876 | _.identity = function(value) { 877 | return value; 878 | }; 879 | 880 | // Run a function **n** times. 881 | _.times = function (n, iterator, context) { 882 | for (var i = 0; i < n; i++) iterator.call(context, i); 883 | }; 884 | 885 | // Escape a string for HTML interpolation. 886 | _.escape = function(string) { 887 | return (''+string).replace(/&/g, '&').replace(//g, '>').replace(/"/g, '"').replace(/'/g, ''').replace(/\//g,'/'); 888 | }; 889 | 890 | // If the value of the named property is a function then invoke it; 891 | // otherwise, return it. 892 | _.result = function(object, property) { 893 | if (object == null) return null; 894 | var value = object[property]; 895 | return _.isFunction(value) ? value.call(object) : value; 896 | }; 897 | 898 | // Add your own custom functions to the Underscore object, ensuring that 899 | // they're correctly added to the OOP wrapper as well. 900 | _.mixin = function(obj) { 901 | each(_.functions(obj), function(name){ 902 | addToWrapper(name, _[name] = obj[name]); 903 | }); 904 | }; 905 | 906 | // Generate a unique integer id (unique within the entire client session). 907 | // Useful for temporary DOM ids. 908 | var idCounter = 0; 909 | _.uniqueId = function(prefix) { 910 | var id = idCounter++; 911 | return prefix ? prefix + id : id; 912 | }; 913 | 914 | // By default, Underscore uses ERB-style template delimiters, change the 915 | // following template settings to use alternative delimiters. 916 | _.templateSettings = { 917 | evaluate : /<%([\s\S]+?)%>/g, 918 | interpolate : /<%=([\s\S]+?)%>/g, 919 | escape : /<%-([\s\S]+?)%>/g 920 | }; 921 | 922 | // When customizing `templateSettings`, if you don't want to define an 923 | // interpolation, evaluation or escaping regex, we need one that is 924 | // guaranteed not to match. 925 | var noMatch = /.^/; 926 | 927 | // Certain characters need to be escaped so that they can be put into a 928 | // string literal. 929 | var escapes = { 930 | '\\': '\\', 931 | "'": "'", 932 | 'r': '\r', 933 | 'n': '\n', 934 | 't': '\t', 935 | 'u2028': '\u2028', 936 | 'u2029': '\u2029' 937 | }; 938 | 939 | for (var p in escapes) escapes[escapes[p]] = p; 940 | var escaper = /\\|'|\r|\n|\t|\u2028|\u2029/g; 941 | var unescaper = /\\(\\|'|r|n|t|u2028|u2029)/g; 942 | 943 | // Within an interpolation, evaluation, or escaping, remove HTML escaping 944 | // that had been previously added. 945 | var unescape = function(code) { 946 | return code.replace(unescaper, function(match, escape) { 947 | return escapes[escape]; 948 | }); 949 | }; 950 | 951 | // JavaScript micro-templating, similar to John Resig's implementation. 952 | // Underscore templating handles arbitrary delimiters, preserves whitespace, 953 | // and correctly escapes quotes within interpolated code. 954 | _.template = function(text, data, settings) { 955 | settings = _.defaults(settings || {}, _.templateSettings); 956 | 957 | // Compile the template source, taking care to escape characters that 958 | // cannot be included in a string literal and then unescape them in code 959 | // blocks. 960 | var source = "__p+='" + text 961 | .replace(escaper, function(match) { 962 | return '\\' + escapes[match]; 963 | }) 964 | .replace(settings.escape || noMatch, function(match, code) { 965 | return "'+\n_.escape(" + unescape(code) + ")+\n'"; 966 | }) 967 | .replace(settings.interpolate || noMatch, function(match, code) { 968 | return "'+\n(" + unescape(code) + ")+\n'"; 969 | }) 970 | .replace(settings.evaluate || noMatch, function(match, code) { 971 | return "';\n" + unescape(code) + "\n;__p+='"; 972 | }) + "';\n"; 973 | 974 | // If a variable is not specified, place data values in local scope. 975 | if (!settings.variable) source = 'with(obj||{}){\n' + source + '}\n'; 976 | 977 | source = "var __p='';" + 978 | "var print=function(){__p+=Array.prototype.join.call(arguments, '')};\n" + 979 | source + "return __p;\n"; 980 | 981 | var render = new Function(settings.variable || 'obj', '_', source); 982 | if (data) return render(data, _); 983 | var template = function(data) { 984 | return render.call(this, data, _); 985 | }; 986 | 987 | // Provide the compiled function source as a convenience for build time 988 | // precompilation. 989 | template.source = 'function(' + (settings.variable || 'obj') + '){\n' + 990 | source + '}'; 991 | 992 | return template; 993 | }; 994 | 995 | // Add a "chain" function, which will delegate to the wrapper. 996 | _.chain = function(obj) { 997 | return _(obj).chain(); 998 | }; 999 | 1000 | // The OOP Wrapper 1001 | // --------------- 1002 | 1003 | // If Underscore is called as a function, it returns a wrapped object that 1004 | // can be used OO-style. This wrapper holds altered versions of all the 1005 | // underscore functions. Wrapped objects may be chained. 1006 | var wrapper = function(obj) { this._wrapped = obj; }; 1007 | 1008 | // Expose `wrapper.prototype` as `_.prototype` 1009 | _.prototype = wrapper.prototype; 1010 | 1011 | // Helper function to continue chaining intermediate results. 1012 | var result = function(obj, chain) { 1013 | return chain ? _(obj).chain() : obj; 1014 | }; 1015 | 1016 | // A method to easily add functions to the OOP wrapper. 1017 | var addToWrapper = function(name, func) { 1018 | wrapper.prototype[name] = function() { 1019 | var args = slice.call(arguments); 1020 | unshift.call(args, this._wrapped); 1021 | return result(func.apply(_, args), this._chain); 1022 | }; 1023 | }; 1024 | 1025 | // Add all of the Underscore functions to the wrapper object. 1026 | _.mixin(_); 1027 | 1028 | // Add all mutator Array functions to the wrapper. 1029 | each(['pop', 'push', 'reverse', 'shift', 'sort', 'splice', 'unshift'], function(name) { 1030 | var method = ArrayProto[name]; 1031 | wrapper.prototype[name] = function() { 1032 | var wrapped = this._wrapped; 1033 | method.apply(wrapped, arguments); 1034 | var length = wrapped.length; 1035 | if ((name == 'shift' || name == 'splice') && length === 0) delete wrapped[0]; 1036 | return result(wrapped, this._chain); 1037 | }; 1038 | }); 1039 | 1040 | // Add all accessor Array functions to the wrapper. 1041 | each(['concat', 'join', 'slice'], function(name) { 1042 | var method = ArrayProto[name]; 1043 | wrapper.prototype[name] = function() { 1044 | return result(method.apply(this._wrapped, arguments), this._chain); 1045 | }; 1046 | }); 1047 | 1048 | // Start chaining a wrapped Underscore object. 1049 | wrapper.prototype.chain = function() { 1050 | this._chain = true; 1051 | return this; 1052 | }; 1053 | 1054 | // Extracts the result from a wrapped and chained object. 1055 | wrapper.prototype.value = function() { 1056 | return this._wrapped; 1057 | }; 1058 | 1059 | }).call(this); 1060 | -------------------------------------------------------------------------------- /test/tests/entity.js: -------------------------------------------------------------------------------- 1 | module("Entity", { 2 | setup: function() { 3 | this.canvasElem = document.createElement("canvas"); 4 | document.body.appendChild(this.canvasElem); 5 | this.world = boxbox.createWorld(this.canvasElem); 6 | }, 7 | teardown: function() { 8 | document.body.removeChild(this.canvasElem); 9 | } 10 | }); 11 | 12 | test("Square creation", function() { 13 | 14 | var ent, returnVal, shapeStub, setAsBoxSpy; 15 | returnVal = new Box2D.Collision.Shapes.b2PolygonShape(); 16 | shapeStub = sinon.stub(Box2D.Collision.Shapes, "b2PolygonShape"); 17 | setAsBoxSpy = sinon.spy(returnVal, "SetAsBox"); 18 | shapeStub.returns(returnVal); 19 | 20 | ent = this.world.createEntity({ 21 | shape: "square", 22 | width: 23, 23 | height: 45 24 | }); 25 | 26 | ok(setAsBoxSpy.calledOnce, "Invokes the 'SetAsBox' method"); 27 | // box2d uses half the width / height 28 | deepEqual(setAsBoxSpy.firstCall.args, [11.5, 22.5], 29 | "Calls with the specified dimensions"); 30 | 31 | shapeStub.restore(); 32 | }); 33 | 34 | test("Circle creation", function() { 35 | 36 | var ent, returnVal, shapeSpy; 37 | returnVal = new Box2D.Collision.Shapes.b2PolygonShape(); 38 | shapeSpy = sinon.spy(Box2D.Collision.Shapes, "b2CircleShape"); 39 | 40 | ent = this.world.createEntity({ 41 | shape: "circle", 42 | radius: 91 43 | }); 44 | 45 | ok(shapeSpy.calledOnce, "Invokes the 'b2CircleShape' method"); 46 | deepEqual(shapeSpy.firstCall.args, [91], 47 | "Calls with the specified radius"); 48 | 49 | shapeSpy.restore(); 50 | }); 51 | 52 | test("Polygon creation", function() { 53 | 54 | var ent, returnVal, shapeStub, setAsArraySpy, points; 55 | returnVal = new Box2D.Collision.Shapes.b2PolygonShape(); 56 | shapeStub = sinon.stub(Box2D.Collision.Shapes, "b2PolygonShape"); 57 | setAsArraySpy = sinon.spy(returnVal, "SetAsArray"); 58 | shapeStub.returns(returnVal); 59 | 60 | 61 | points = [{ x: 0, y: 1 }, { x: 1, y: 0 }, { x: -1, y: -1}]; 62 | ent = this.world.createEntity({ 63 | shape: "polygon", 64 | points: points 65 | }); 66 | 67 | ok(setAsArraySpy.calledOnce, "Invokes the 'SetAsArray' method"); 68 | deepEqual(setAsArraySpy.firstCall.args, [points, points.length], 69 | "Calls with the specified points"); 70 | 71 | shapeStub.restore(); 72 | }); 73 | --------------------------------------------------------------------------------