├── Rakefile ├── matrix_test.html ├── jsdoc.conf ├── README.markdown ├── matrix-1.2.0.min.js ├── qunit.css ├── test └── matrix.js ├── matrix.js └── qunit.js /Rakefile: -------------------------------------------------------------------------------- 1 | task :default => [:min, :doc] 2 | 3 | task :min do 4 | source_file = "matrix.js" 5 | version = "0.0.0" 6 | 7 | File.foreach(source_file) do |line| 8 | if line =~ /Matrix\.js v(\d+\.\d+\.\d+)/ 9 | version = $1 10 | break 11 | end 12 | end 13 | 14 | puts `java -jar yuicompressor-2.4.2.jar #{source_file} > matrix-#{version}.min.js` 15 | end 16 | 17 | task :doc do 18 | puts `java -jar jsdoc-toolkit/jsrun.jar jsdoc-toolkit/app/run.js -c=jsdoc.conf` 19 | end 20 | -------------------------------------------------------------------------------- /matrix_test.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Matrix Demo 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 |

Matrix.js

14 |

15 |

16 |
    17 |
    18 | 19 | 20 | -------------------------------------------------------------------------------- /jsdoc.conf: -------------------------------------------------------------------------------- 1 | /* 2 | This is an example of one way you could set up a configuration file to more 3 | conveniently define some commandline options. You might like to do this if 4 | you frequently reuse the same options. Note that you don't need to define 5 | every option in this file, you can combine a configuration file with 6 | additional options on the commandline if your wish. 7 | 8 | You would include this configuration file by running JsDoc Toolkit like so: 9 | java -jar jsrun.jar app/run.js -c=conf/sample.conf 10 | 11 | */ 12 | 13 | { 14 | // source files to use 15 | _: ['matrix.js'], 16 | 17 | // document all functions, even uncommented ones 18 | a: true, 19 | 20 | r: true, 21 | 22 | // including those marked @private 23 | p: true, 24 | 25 | // some extra variables I want to include 26 | D: {generatedBy: "STRd6", copyright: "2010"}, 27 | 28 | // use this directory as the output directory 29 | d: "docs", 30 | 31 | // use this template 32 | t: "jsdoc-toolkit/templates/jsdoc" 33 | } 34 | -------------------------------------------------------------------------------- /README.markdown: -------------------------------------------------------------------------------- 1 | # Matrix.js 1.2.0 2 | 3 | [Matrix.js Documentation](http://strd6.com/matrix.js/docs) 4 | 5 | [Unit Tests](http://strd6.com/matrix.js/matrix_test.html) 6 | 7 | [Guided Demo](http://strd6.com/2010/06/matrix-js-demo/) 8 | 9 | ## Using with HTML5 Canvas 10 | 11 | You'll probably want this (transformation is a Matrix instance): 12 | 13 | function withTransformation(transformation, block) { 14 | context.save(); 15 | 16 | context.transform( 17 | transformation.a, 18 | transformation.b, 19 | transformation.c, 20 | transformation.d, 21 | transformation.tx, 22 | transformation.ty 23 | ); 24 | 25 | try { 26 | block(); 27 | } finally { 28 | context.restore(); 29 | } 30 | } 31 | 32 | ## Changelog 33 | 34 | ### v1.2.0 35 | 36 | Added a bunch of utility methods to Points. 37 | 38 | `subtract`, `scale`, `equal`, and `magnitude`. 39 | 40 | ### v1.1.0 41 | 42 | Instance method transforamtions (scale, rotate, and translate) are now consistent with concatenation order. 43 | 44 | var m1 = Matrix().translate(tx, ty).scale(s).rotate(theta); 45 | var m2 = Matrix().concat(Matrix.translation(tx, ty)).concat(Matrix.scale(s)).concat(Matrix.rotation(theta)); 46 | 47 | matrixEqual(m1, m2); 48 | 49 | This means that if you were using a version prior to 1.1.0 **the order of concatenation has changed**. 50 | 51 | Added an optional `aboutPoint` parameter to the scale instance and class methods. 52 | -------------------------------------------------------------------------------- /matrix-1.2.0.min.js: -------------------------------------------------------------------------------- 1 | (function(){function b(c,d){return{x:c||0,y:d||0,equal:function(e){return this.x===e.x&&this.y===e.y},add:function(e){return b(this.x+e.x,this.y+e.y)},subtract:function(e){return b(this.x-e.x,this.y-e.y)},scale:function(e){return b(this.x*e,this.y*e)},magnitude:function(){return b.distance(b(0,0),this)}}}b.distance=function(d,c){return Math.sqrt(Math.pow(c.x-d.x,2)+Math.pow(c.y-d.y,2))};b.direction=function(d,c){return Math.atan2(c.y-d.y,c.x-d.x)};function a(h,f,j,i,g,e){h=h!==undefined?h:1;i=i!==undefined?i:1;return{a:h,b:f||0,c:j||0,d:i,tx:g||0,ty:e||0,concat:function(c){return a(this.a*c.a+this.c*c.b,this.b*c.a+this.d*c.b,this.a*c.c+this.c*c.d,this.b*c.c+this.d*c.d,this.a*c.tx+this.c*c.ty+this.tx,this.b*c.tx+this.d*c.ty+this.ty)},deltaTransformPoint:function(c){return b(this.a*c.x+this.c*c.y,this.b*c.x+this.d*c.y)},inverse:function(){var c=this.a*this.d-this.b*this.c;return a(this.d/c,-this.b/c,-this.c/c,this.a/c,(this.c*this.ty-this.d*this.tx)/c,(this.b*this.tx-this.a*this.ty)/c)},rotate:function(c,d){return this.concat(a.rotation(c,d))},scale:function(k,d,c){return this.concat(a.scale(k,d,c))},transformPoint:function(c){return b(this.a*c.x+this.c*c.y+this.tx,this.b*c.x+this.d*c.y+this.ty)},translate:function(d,c){return this.concat(a.translation(d,c))}}}a.rotation=function(d,e){var c=a(Math.cos(d),Math.sin(d),-Math.sin(d),Math.cos(d));if(e){c=a.translation(e.x,e.y).concat(c).concat(a.translation(-e.x,-e.y))}return c};a.scale=function(f,e,d){e=e||f;var c=a(f,0,0,e);if(d){c=a.translation(d.x,d.y).concat(c).concat(a.translation(-d.x,-d.y))}return c};a.translation=function(d,c){return a(1,0,0,1,d,c)};a.IDENTITY=a();a.HORIZONTAL_FLIP=a(-1,0,0,1);a.VERTICAL_FLIP=a(1,0,0,-1);window.Point=b;window.Matrix=a}()); -------------------------------------------------------------------------------- /qunit.css: -------------------------------------------------------------------------------- 1 | ol#qunit-tests { 2 | font-family: "Helvetica Neue Light", "HelveticaNeue-Light", "Helvetica Neue", Calibri, Helvetica, Arial; 3 | margin: 0; 4 | padding: 0; 5 | list-style-position: inside; 6 | font-size: smaller; } 7 | ol#qunit-tests li { 8 | padding: 0.4em 0.5em 0.4em 2.5em; 9 | border-bottom: 1px solid white; 10 | font-size: small; 11 | list-style-position: inside; } 12 | ol#qunit-tests li ol { 13 | box-shadow: inset 0px 2px 13px #999999; 14 | -moz-box-shadow: inset 0px 2px 13px #999999; 15 | -webkit-box-shadow: inset 0px 2px 13px #999999; 16 | margin-top: 0.5em; 17 | margin-left: 0; 18 | padding: 0.5em; 19 | background-color: white; 20 | border-radius: 15px; 21 | -moz-border-radius: 15px; 22 | -webkit-border-radius: 15px; } 23 | ol#qunit-tests li li { 24 | border-bottom: none; 25 | margin: 0.5em; 26 | background-color: white; 27 | list-style-position: inside; 28 | padding: 0.4em 0.5em 0.4em 0.5em; } 29 | ol#qunit-tests li li.pass { 30 | border-left: 26px solid #c6e746; 31 | background-color: white; 32 | color: #5e740b; } 33 | ol#qunit-tests li li.fail { 34 | border-left: 26px solid #ee5757; 35 | background-color: white; 36 | color: #710909; } 37 | ol#qunit-tests li.pass { 38 | background-color: #d2e0e6; 39 | color: #528ce0; } 40 | ol#qunit-tests li.fail { 41 | background-color: #ee5757; 42 | color: black; } 43 | ol#qunit-tests li.pass span.test-name { 44 | color: #366097; } 45 | ol#qunit-tests li.fail span.test-name { 46 | color: black; } 47 | ol#qunit-tests li.fail span.module-name { 48 | color: black; } 49 | ol#qunit-tests li li.fail span.test-actual { 50 | color: #ee5757; } 51 | ol#qunit-tests li li.pass span.test-expected, ol#qunit-tests li li.pass span.test-actual { 52 | color: #999999; } 53 | ol#qunit-tests li li.fail span.test-expected { 54 | color: green; } 55 | ol#qunit-tests li strong { 56 | cursor: pointer; } 57 | 58 | h1#qunit-header { 59 | background-color: #0d3349; 60 | margin: 0; 61 | padding: 0.5em 0 0.5em 1em; 62 | color: white; 63 | font-family: "Helvetica Neue Light", "HelveticaNeue-Light", "Helvetica Neue", Calibri, Helvetica, Arial; 64 | border-top-right-radius: 15px; 65 | border-top-left-radius: 15px; 66 | -moz-border-radius-topright: 15px; 67 | -moz-border-radius-topleft: 15px; 68 | -webkit-border-top-right-radius: 15px; 69 | -webkit-border-top-left-radius: 15px; 70 | text-shadow: rgba(0, 0, 0, 0.5) 4px 4px 1px; } 71 | 72 | h2#qunit-banner { 73 | font-family: "Helvetica Neue Light", "HelveticaNeue-Light", "Helvetica Neue", Calibri, Helvetica, Arial; 74 | height: 5px; 75 | margin: 0; 76 | padding: 0; } 77 | h2#qunit-banner.qunit-pass { 78 | background-color: #c6e746; } 79 | h2#qunit-banner.qunit-fail { 80 | background-color: #ee5757; } 81 | 82 | #qunit-testrunner-toolbar { 83 | background-color: #ee5757; 84 | font-family: "Helvetica Neue Light", "HelveticaNeue-Light", "Helvetica Neue", Calibri, Helvetica, Arial; 85 | padding: 0; 86 | /*width:80%; */ 87 | padding: 0em 0 0.5em 2em; 88 | font-size: small; } 89 | 90 | h2#qunit-userAgent { 91 | font-family: "Helvetica Neue Light", "HelveticaNeue-Light", "Helvetica Neue", Calibri, Helvetica, Arial; 92 | background-color: #2b81af; 93 | margin: 0; 94 | padding: 0; 95 | color: white; 96 | font-size: small; 97 | padding: 0.5em 0 0.5em 2.5em; 98 | text-shadow: rgba(0, 0, 0, 0.5) 2px 2px 1px; } 99 | 100 | p#qunit-testresult { 101 | font-family: "Helvetica Neue Light", "HelveticaNeue-Light", "Helvetica Neue", Calibri, Helvetica, Arial; 102 | margin: 0; 103 | font-size: small; 104 | color: #2b81af; 105 | border-bottom-right-radius: 15px; 106 | border-bottom-left-radius: 15px; 107 | -moz-border-radius-bottomright: 15px; 108 | -moz-border-radius-bottomleft: 15px; 109 | -webkit-border-bottom-right-radius: 15px; 110 | -webkit-border-bottom-left-radius: 15px; 111 | background-color: #d2e0e6; 112 | padding: 0.5em 0.5em 0.5em 2.5em; } 113 | 114 | strong b.fail { 115 | color: #710909; } 116 | strong b.pass { 117 | color: #5e740b; } 118 | 119 | #qunit-fixture { 120 | position: absolute; 121 | top: -10000px; 122 | left: -10000px; } 123 | -------------------------------------------------------------------------------- /test/matrix.js: -------------------------------------------------------------------------------- 1 | $(function() { 2 | var TOLERANCE = 0.00001; 3 | 4 | function equalEnough(expected, actual, tolerance, message) { 5 | message = message || "" + expected + " within " + tolerance + " of " + actual; 6 | ok(expected + tolerance >= actual && expected - tolerance <= actual, message); 7 | } 8 | 9 | function matrixEqual(m1, m2) { 10 | equalEnough(m1.a, m2.a, TOLERANCE); 11 | equalEnough(m1.b, m2.b, TOLERANCE); 12 | equalEnough(m1.c, m2.c, TOLERANCE); 13 | equalEnough(m1.d, m2.d, TOLERANCE); 14 | equalEnough(m1.tx, m2.tx, TOLERANCE); 15 | equalEnough(m1.ty, m2.ty, TOLERANCE); 16 | } 17 | 18 | test("Matrix() (Identity)", function() { 19 | var matrix = Matrix(); 20 | 21 | equals(matrix.a, 1, "a"); 22 | equals(matrix.b, 0, "b"); 23 | equals(matrix.c, 0, "c"); 24 | equals(matrix.d, 1, "d"); 25 | equals(matrix.tx, 0, "tx"); 26 | equals(matrix.ty, 0, "ty"); 27 | 28 | matrixEqual(matrix, Matrix.IDENTITY); 29 | }); 30 | 31 | test("Empty Matrix", function() { 32 | var matrix = Matrix(0, 0, 0, 0, 0, 0); 33 | 34 | equals(matrix.a, 0, "a"); 35 | equals(matrix.b, 0, "b"); 36 | equals(matrix.c, 0, "c"); 37 | equals(matrix.d, 0, "d"); 38 | equals(matrix.tx, 0, "tx"); 39 | equals(matrix.ty, 0, "ty"); 40 | }); 41 | 42 | test("Matrix.scale", function() { 43 | var matrix = Matrix.scale(2, 2); 44 | 45 | equals(matrix.a, 2, "a"); 46 | equals(matrix.b, 0, "b"); 47 | equals(matrix.c, 0, "c"); 48 | equals(matrix.d, 2, "d"); 49 | 50 | matrix = Matrix.scale(3); 51 | 52 | equals(matrix.a, 3, "a"); 53 | equals(matrix.b, 0, "b"); 54 | equals(matrix.c, 0, "c"); 55 | equals(matrix.d, 3, "d"); 56 | }); 57 | 58 | test("Matrix.scale (about a point)", function() { 59 | var p = Point(5, 17); 60 | 61 | var transformedPoint = Matrix.scale(3, 7, p).transformPoint(p); 62 | 63 | equals(transformedPoint.x, p.x, "Point should remain the same"); 64 | equals(transformedPoint.y, p.y, "Point should remain the same"); 65 | }); 66 | 67 | test("Matrix#scale (about a point)", function() { 68 | var p = Point(3, 11); 69 | 70 | var transformedPoint = Matrix.IDENTITY.scale(3, 7, p).transformPoint(p); 71 | 72 | equals(transformedPoint.x, p.x, "Point should remain the same"); 73 | equals(transformedPoint.y, p.y, "Point should remain the same"); 74 | }); 75 | 76 | test("Matrix.rotation", function() { 77 | var matrix = Matrix.rotation(Math.PI / 2); 78 | 79 | equalEnough(matrix.a, 0, TOLERANCE); 80 | equalEnough(matrix.b, 1, TOLERANCE); 81 | equalEnough(matrix.c,-1, TOLERANCE); 82 | equalEnough(matrix.d, 0, TOLERANCE); 83 | }); 84 | 85 | test("Matrix.rotation (about a point)", function() { 86 | var p = Point(11, 7); 87 | 88 | var transformedPoint = Matrix.rotation(Math.PI / 2, p).transformPoint(p); 89 | 90 | equals(transformedPoint.x, p.x, "Point should remain the same"); 91 | equals(transformedPoint.y, p.y, "Point should remain the same"); 92 | }); 93 | 94 | test("Matrix#rotate (about a point)", function() { 95 | var p = Point(8, 5); 96 | 97 | var transformedPoint = Matrix.IDENTITY.rotate(Math.PI / 2, p).transformPoint(p); 98 | 99 | equals(transformedPoint.x, p.x, "Point should remain the same"); 100 | equals(transformedPoint.y, p.y, "Point should remain the same"); 101 | }); 102 | 103 | test("Matrix#inverse (Identity)", function() { 104 | var matrix = Matrix().inverse(); 105 | 106 | equals(matrix.a, 1, "a"); 107 | equals(matrix.b, 0, "b"); 108 | equals(matrix.c, 0, "c"); 109 | equals(matrix.d, 1, "d"); 110 | equals(matrix.tx, 0, "tx"); 111 | equals(matrix.ty, 0, "ty"); 112 | }); 113 | 114 | test("Matrix#concat", function() { 115 | var matrix = Matrix.rotation(Math.PI / 2).concat(Matrix.rotation(-Math.PI / 2)); 116 | 117 | matrixEqual(matrix, Matrix.IDENTITY); 118 | }); 119 | 120 | test("Maths", function() { 121 | var a = Matrix(12, 3, 3, 1, 7, 9); 122 | var b = Matrix(3, 8, 3, 2, 1, 5); 123 | 124 | var c = a.concat(b); 125 | 126 | equals(c.a, 60); 127 | equals(c.b, 17); 128 | equals(c.c, 42); 129 | equals(c.d, 11); 130 | equals(c.tx, 34); 131 | equals(c.ty, 17); 132 | }); 133 | 134 | test("Order of transformations should match manual concat", function() { 135 | var tx = 10; 136 | var ty = 5; 137 | var theta = Math.PI/3; 138 | var s = 2; 139 | 140 | var m1 = Matrix().translate(tx, ty).scale(s).rotate(theta); 141 | var m2 = Matrix().concat(Matrix.translation(tx, ty)).concat(Matrix.scale(s)).concat(Matrix.rotation(theta)); 142 | 143 | matrixEqual(m1, m2); 144 | }); 145 | 146 | test("Point#add", function() { 147 | var p1 = Point(5, 6); 148 | var p2 = Point(7, 5); 149 | 150 | var result = p1.add(p2); 151 | 152 | equals(result.x, p1.x + p2.x); 153 | equals(result.y, p1.y + p2.y); 154 | }); 155 | 156 | test("Point#subtract", function() { 157 | var p1 = Point(5, 6); 158 | var p2 = Point(7, 5); 159 | 160 | var result = p1.subtract(p2); 161 | 162 | equals(result.x, p1.x - p2.x); 163 | equals(result.y, p1.y - p2.y); 164 | }); 165 | 166 | test("Point#scale", function() { 167 | var p1 = Point(5, 6); 168 | var scalar = 2; 169 | 170 | var result = p1.scale(scalar); 171 | 172 | equals(result.x, p1.x * scalar); 173 | equals(result.y, p1.y * scalar); 174 | }); 175 | 176 | test("Point#equal", function() { 177 | ok(Point(7, 8).equal(Point(7, 8))); 178 | }); 179 | 180 | test("Point#magnitude", function() { 181 | equals(Point(3, 4).magnitude(), 5); 182 | }); 183 | }); 184 | -------------------------------------------------------------------------------- /matrix.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Matrix.js v1.2.0 3 | * 4 | * Copyright (c) 2010 STRd6 5 | * 6 | * Permission is hereby granted, free of charge, to any person obtaining a copy 7 | * of this software and associated documentation files (the "Software"), to deal 8 | * in the Software without restriction, including without limitation the rights 9 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | * copies of the Software, and to permit persons to whom the Software is 11 | * furnished to do so, subject to the following conditions: 12 | * 13 | * The above copyright notice and this permission notice shall be included in 14 | * all copies or substantial portions of the Software. 15 | * 16 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 22 | * THE SOFTWARE. 23 | * 24 | * Loosely based on flash: 25 | * http://www.adobe.com/livedocs/flash/9.0/ActionScriptLangRefV3/flash/geom/Matrix.html 26 | */ 27 | (function() { 28 | 29 | /** 30 | * Create a new point with given x and y coordinates. If no arguments are given 31 | * defaults to (0, 0). 32 | * @name Point 33 | * @param {Number} [x] 34 | * @param {Number} [y] 35 | * @constructor 36 | */ 37 | function Point(x, y) { 38 | this.x = x || 0; 39 | this.y = y || 0; 40 | }; 41 | 42 | Point.prototype = { 43 | 44 | clone: function() { 45 | return new Point(this.x,this.y); 46 | }, 47 | 48 | /** 49 | * Check whether two points are equal. The x and y values must be exactly 50 | * equal for this method to return true. 51 | * @name equal 52 | * @methodOf Point# 53 | * 54 | * @param {Point} other The point to check for equality. 55 | * @returns true if this point is equal to the other point, false 56 | * otherwise. 57 | * @type Boolean 58 | */ 59 | equal: function(other) { 60 | return this.x === other.x && this.y === other.y; 61 | }, 62 | /** 63 | * Adds a point to this one and returns the new point. 64 | * @name add 65 | * @methodOf Point# 66 | * 67 | * @param {Point} other The point to add this point to. 68 | * @returns A new point, the sum of both. 69 | * @type Point 70 | */ 71 | add: function(other) { 72 | return new Point(this.x + other.x, this.y + other.y); 73 | }, 74 | /** 75 | * Subtracts a point from this one and returns the new point. 76 | * @name subtract 77 | * @methodOf Point# 78 | * 79 | * @param {Point} other The point to subtract from this point. 80 | * @returns A new point, the difference of both. 81 | * @type Point 82 | */ 83 | subtract: function(other) { 84 | return new Point(this.x - other.x, this.y - other.y); 85 | }, 86 | /** 87 | * Multiplies this point by a scalar value and returns the new point. 88 | * @name scale 89 | * @methodOf Point# 90 | * 91 | * @param {Point} scalar The value to scale this point by. 92 | * @returns A new point with x and y multiplied by the scalar value. 93 | * @type Point 94 | */ 95 | scale: function(scalar) { 96 | return new Point(this.x * scalar, this.y * scalar); 97 | }, 98 | 99 | /** 100 | * Returns the distance of this point from the origin. If this point is 101 | * thought of as a vector this distance is its magnitude. 102 | * @name magnitude 103 | * @methodOf Point# 104 | * 105 | * @returns The distance of this point from the origin. 106 | * @type Number 107 | */ 108 | magnitude: function() { 109 | return Point.distance(new Point(0, 0), this); 110 | }, 111 | multiply: function(d) { 112 | return new Point(this.x * d, this.y * d); 113 | }, 114 | 115 | dot: function(other) { 116 | return this.x * other.x + this.y * other.y; 117 | }, 118 | 119 | rotated: function() { 120 | // Return a new vector that's a copy of this vector rotated by a radians 121 | return new Vector(this.x * Math.cos(a) - this.y*Math.sin(a), 122 | this.x * Math.sin(a) + this.y*Math.cos(a)); 123 | }, 124 | 125 | 126 | angleTo: function(other) { 127 | return Math.acos(this.dot(other) / (Math.abs(this.dist()) * Math.abs(other.dist()))); 128 | }, 129 | 130 | toUnit: function() { 131 | return this.multiply(1/this.magnitude()); 132 | } 133 | }; 134 | 135 | 136 | /** 137 | * @param {Point} p1 138 | * @param {Point} p2 139 | * @returns The Euclidean distance between two points. 140 | */ 141 | Point.distance = function(p1, p2) { 142 | return Math.sqrt(Math.pow(p2.x - p1.x, 2) + Math.pow(p2.y - p1.y, 2)); 143 | }; 144 | 145 | /** 146 | * If you have two dudes, one standing at point p1, and the other 147 | * standing at point p2, then this method will return the direction 148 | * that the dude standing at p1 will need to face to look at p2. 149 | * @param {Point} p1 The starting point. 150 | * @param {Point} p2 The ending point. 151 | * @returns The direction from p1 to p2 in radians. 152 | */ 153 | Point.direction = function(p1, p2) { 154 | return Math.atan2( 155 | p2.y - p1.y, 156 | p2.x - p1.x 157 | ); 158 | } 159 | 160 | /** 161 | *
    162 |    *  _        _
    163 |    * | a  c tx  |
    164 |    * | b  d ty  |
    165 |    * |_0  0  1 _|
    166 |    * 
    167 | * Creates a matrix for 2d affine transformations. 168 | * 169 | * concat, inverse, rotate, scale and translate return new matrices with the 170 | * transformations applied. The matrix is not modified in place. 171 | * 172 | * Returns the identity matrix when called with no arguments. 173 | * @name Matrix 174 | * @param {Number} [a] 175 | * @param {Number} [b] 176 | * @param {Number} [c] 177 | * @param {Number} [d] 178 | * @param {Number} [tx] 179 | * @param {Number} [ty] 180 | * @constructor 181 | */ 182 | function Matrix(a, b, c, d, tx, ty) { 183 | this.a = a !== undefined ? a : 1; 184 | this.b = b || 0; 185 | this.c = c || 0; 186 | this.d = d !== undefined ? d : 1; 187 | this.tx = tx || 0; 188 | this.ty = ty || 0; 189 | } 190 | 191 | Matrix.prototype = { 192 | 193 | clone: function() { 194 | return new Matrix( 195 | this.a, 196 | this.b, 197 | this.c, 198 | this.d, 199 | this.tx, 200 | this.ty 201 | ); 202 | }, 203 | 204 | /** 205 | * Returns the result of this matrix multiplied by another matrix 206 | * combining the geometric effects of the two. In mathematical terms, 207 | * concatenating two matrixes is the same as combining them using matrix multiplication. 208 | * If this matrix is A and the matrix passed in is B, the resulting matrix is A x B 209 | * http://mathworld.wolfram.com/MatrixMultiplication.html 210 | * @name concat 211 | * @methodOf Matrix# 212 | * 213 | * @param {Matrix} matrix The matrix to multiply this matrix by. 214 | * @returns The result of the matrix multiplication, a new matrix. 215 | * @type Matrix 216 | */ 217 | concat: function(matrix) { 218 | return new Matrix( 219 | this.a * matrix.a + this.c * matrix.b, 220 | this.b * matrix.a + this.d * matrix.b, 221 | this.a * matrix.c + this.c * matrix.d, 222 | this.b * matrix.c + this.d * matrix.d, 223 | this.a * matrix.tx + this.c * matrix.ty + this.tx, 224 | this.b * matrix.tx + this.d * matrix.ty + this.ty 225 | ); 226 | }, 227 | 228 | /** 229 | * Given a point in the pretransform coordinate space, returns the coordinates of 230 | * that point after the transformation occurs. Unlike the standard transformation 231 | * applied using the transformnew Point() method, the deltaTransformnew Point() method's 232 | * transformation does not consider the translation parameters tx and ty. 233 | * @name deltaTransformPoint 234 | * @methodOf Matrix# 235 | * @see #transformPoint 236 | * 237 | * @return A new point transformed by this matrix ignoring tx and ty. 238 | * @type Point 239 | */ 240 | deltaTransformPoint: function(point) { 241 | return new Point( 242 | this.a * point.x + this.c * point.y, 243 | this.b * point.x + this.d * point.y 244 | ); 245 | }, 246 | 247 | /** 248 | * Returns the inverse of the matrix. 249 | * http://mathworld.wolfram.com/MatrixInverse.html 250 | * @name inverse 251 | * @methodOf Matrix# 252 | * 253 | * @returns A new matrix that is the inverse of this matrix. 254 | * @type Matrix 255 | */ 256 | inverse: function() { 257 | var determinant = this.a * this.d - this.b * this.c; 258 | return new Matrix( 259 | this.d / determinant, 260 | -this.b / determinant, 261 | -this.c / determinant, 262 | this.a / determinant, 263 | (this.c * this.ty - this.d * this.tx) / determinant, 264 | (this.b * this.tx - this.a * this.ty) / determinant 265 | ); 266 | }, 267 | 268 | /** 269 | * Returns a new matrix that corresponds this matrix multiplied by a 270 | * a rotation matrix. 271 | * @name rotate 272 | * @methodOf Matrix# 273 | * @see Matrix.rotation 274 | * 275 | * @param {Number} theta Amount to rotate in radians. 276 | * @param {Point} [aboutPoint] The point about which this rotation occurs. Defaults to (0,0). 277 | * @returns A new matrix, rotated by the specified amount. 278 | * @type Matrix 279 | */ 280 | rotate: function(theta, aboutPoint) { 281 | return this.concat(Matrix.rotation(theta, aboutPoint)); 282 | }, 283 | 284 | /** 285 | * Returns a new matrix that corresponds this matrix multiplied by a 286 | * a scaling matrix. 287 | * @name scale 288 | * @methodOf Matrix# 289 | * @see Matrix.scale 290 | * 291 | * @param {Number} sx 292 | * @param {Number} [sy] 293 | * @param {Point} [aboutPoint] The point that remains fixed during the scaling 294 | * @type Matrix 295 | */ 296 | scale: function(sx, sy, aboutPoint) { 297 | return this.concat(Matrix.scale(sx, sy, aboutPoint)); 298 | }, 299 | 300 | /** 301 | * Returns the result of applying the geometric transformation represented by the 302 | * Matrix object to the specified point. 303 | * @name transformPoint 304 | * @methodOf Matrix# 305 | * @see #deltaTransformPoint 306 | * 307 | * @returns A new point with the transformation applied. 308 | * @type Point 309 | */ 310 | transformPoint: function(point) { 311 | return new Point( 312 | this.a * point.x + this.c * point.y + this.tx, 313 | this.b * point.x + this.d * point.y + this.ty 314 | ); 315 | }, 316 | 317 | /** 318 | * Translates the matrix along the x and y axes, as specified by the tx and ty parameters. 319 | * @name translate 320 | * @methodOf Matrix# 321 | * @see Matrix.translation 322 | * 323 | * @param {Number} tx The translation along the x axis. 324 | * @param {Number} ty The translation along the y axis. 325 | * @returns A new matrix with the translation applied. 326 | * @type Matrix 327 | */ 328 | translate: function(tx, ty) { 329 | return this.concat(Matrix.translation(tx, ty)); 330 | } 331 | }; 332 | 333 | /** 334 | * Creates a matrix transformation that corresponds to the given rotation, 335 | * around (0,0) or the specified point. 336 | * @see Matrix#rotate 337 | * 338 | * @param {Number} theta Rotation in radians. 339 | * @param {Point} [aboutPoint] The point about which this rotation occurs. Defaults to (0,0). 340 | * @returns 341 | * @type Matrix 342 | */ 343 | Matrix.rotation = function(theta, aboutPoint) { 344 | var rotationMatrix = new Matrix( 345 | Math.cos(theta), 346 | Math.sin(theta), 347 | -Math.sin(theta), 348 | Math.cos(theta) 349 | ); 350 | 351 | if(aboutPoint) { 352 | rotationMatrix = 353 | Matrix.translation(aboutPoint.x, aboutPoint.y).concat( 354 | rotationMatrix 355 | ).concat( 356 | Matrix.translation(-aboutPoint.x, -aboutPoint.y) 357 | ); 358 | } 359 | 360 | return rotationMatrix; 361 | }; 362 | 363 | /** 364 | * Returns a matrix that corresponds to scaling by factors of sx, sy along 365 | * the x and y axis respectively. 366 | * If only one parameter is given the matrix is scaled uniformly along both axis. 367 | * If the optional aboutPoint parameter is given the scaling takes place 368 | * about the given point. 369 | * @see Matrix#scale 370 | * 371 | * @param {Number} sx The amount to scale by along the x axis or uniformly if no sy is given. 372 | * @param {Number} [sy] The amount to scale by along the y axis. 373 | * @param {Point} [aboutPoint] The point about which the scaling occurs. Defaults to (0,0). 374 | * @returns A matrix transformation representing scaling by sx and sy. 375 | * @type Matrix 376 | */ 377 | Matrix.scale = function(sx, sy, aboutPoint) { 378 | sy = sy || sx; 379 | 380 | var scaleMatrix = new Matrix(sx, 0, 0, sy); 381 | 382 | if(aboutPoint) { 383 | scaleMatrix = 384 | Matrix.translation(aboutPoint.x, aboutPoint.y).concat( 385 | scaleMatrix 386 | ).concat( 387 | Matrix.translation(-aboutPoint.x, -aboutPoint.y) 388 | ); 389 | } 390 | 391 | return scaleMatrix; 392 | }; 393 | 394 | /** 395 | * Returns a matrix that corresponds to a translation of tx, ty. 396 | * @see Matrix#translate 397 | * 398 | * @param {Number} tx The amount to translate in the x direction. 399 | * @param {Number} ty The amount to translate in the y direction. 400 | * @return A matrix transformation representing a translation by tx and ty. 401 | * @type Matrix 402 | */ 403 | Matrix.translation = function(tx, ty) { 404 | return new Matrix(1, 0, 0, 1, tx, ty); 405 | }; 406 | 407 | /** 408 | * A constant representing the identity matrix. 409 | * @name IDENTITY 410 | * @fieldOf Matrix 411 | */ 412 | Matrix.IDENTITY = new Matrix(); 413 | /** 414 | * A constant representing the horizontal flip transformation matrix. 415 | * @name HORIZONTAL_FLIP 416 | * @fieldOf Matrix 417 | */ 418 | Matrix.HORIZONTAL_FLIP = new Matrix(-1, 0, 0, 1); 419 | /** 420 | * A constant representing the vertical flip transformation matrix. 421 | * @name VERTICAL_FLIP 422 | * @fieldOf Matrix 423 | */ 424 | Matrix.VERTICAL_FLIP = new Matrix(1, 0, 0, -1); 425 | 426 | // Export to window 427 | window["Point"] = Point; 428 | window["Matrix"] = Matrix; 429 | }()); 430 | -------------------------------------------------------------------------------- /qunit.js: -------------------------------------------------------------------------------- 1 | /* 2 | * QUnit - A JavaScript Unit Testing Framework 3 | * 4 | * http://docs.jquery.com/QUnit 5 | * 6 | * Copyright (c) 2009 John Resig, Jörn Zaefferer 7 | * Dual licensed under the MIT (MIT-LICENSE.txt) 8 | * and GPL (GPL-LICENSE.txt) licenses. 9 | */ 10 | 11 | (function(window) { 12 | 13 | var QUnit = { 14 | 15 | // call on start of module test to prepend name to all tests 16 | module: function(name, testEnvironment) { 17 | config.currentModule = name; 18 | 19 | synchronize(function() { 20 | if ( config.currentModule ) { 21 | QUnit.moduleDone( config.currentModule, config.moduleStats.bad, config.moduleStats.all ); 22 | } 23 | 24 | config.currentModule = name; 25 | config.moduleTestEnvironment = testEnvironment; 26 | config.moduleStats = { all: 0, bad: 0 }; 27 | 28 | QUnit.moduleStart( name, testEnvironment ); 29 | }); 30 | }, 31 | 32 | asyncTest: function(testName, expected, callback) { 33 | if ( arguments.length === 2 ) { 34 | callback = expected; 35 | expected = 0; 36 | } 37 | 38 | QUnit.test(testName, expected, callback, true); 39 | }, 40 | 41 | test: function(testName, expected, callback, async) { 42 | var name = '' + testName + '', testEnvironment, testEnvironmentArg; 43 | 44 | if ( arguments.length === 2 ) { 45 | callback = expected; 46 | expected = null; 47 | } 48 | // is 2nd argument a testEnvironment? 49 | if ( expected && typeof expected === 'object') { 50 | testEnvironmentArg = expected; 51 | expected = null; 52 | } 53 | 54 | if ( config.currentModule ) { 55 | name = '' + config.currentModule + ": " + name; 56 | } 57 | 58 | if ( !validTest(config.currentModule + ": " + testName) ) { 59 | return; 60 | } 61 | 62 | synchronize(function() { 63 | 64 | testEnvironment = extend({ 65 | setup: function() {}, 66 | teardown: function() {} 67 | }, config.moduleTestEnvironment); 68 | if (testEnvironmentArg) { 69 | extend(testEnvironment,testEnvironmentArg); 70 | } 71 | 72 | QUnit.testStart( testName, testEnvironment ); 73 | 74 | // allow utility functions to access the current test environment 75 | QUnit.current_testEnvironment = testEnvironment; 76 | 77 | config.assertions = []; 78 | config.expected = expected; 79 | 80 | var tests = id("qunit-tests"); 81 | if (tests) { 82 | var b = document.createElement("strong"); 83 | b.innerHTML = "Running " + name; 84 | var li = document.createElement("li"); 85 | li.appendChild( b ); 86 | li.id = "current-test-output"; 87 | tests.appendChild( li ) 88 | } 89 | 90 | try { 91 | if ( !config.pollution ) { 92 | saveGlobal(); 93 | } 94 | 95 | testEnvironment.setup.call(testEnvironment); 96 | } catch(e) { 97 | QUnit.ok( false, "Setup failed on " + name + ": " + e.message ); 98 | } 99 | }); 100 | 101 | synchronize(function() { 102 | if ( async ) { 103 | QUnit.stop(); 104 | } 105 | 106 | try { 107 | callback.call(testEnvironment); 108 | } catch(e) { 109 | fail("Test " + name + " died, exception and test follows", e, callback); 110 | QUnit.ok( false, "Died on test #" + (config.assertions.length + 1) + ": " + e.message ); 111 | // else next test will carry the responsibility 112 | saveGlobal(); 113 | 114 | // Restart the tests if they're blocking 115 | if ( config.blocking ) { 116 | start(); 117 | } 118 | } 119 | }); 120 | 121 | synchronize(function() { 122 | try { 123 | checkPollution(); 124 | testEnvironment.teardown.call(testEnvironment); 125 | } catch(e) { 126 | QUnit.ok( false, "Teardown failed on " + name + ": " + e.message ); 127 | } 128 | }); 129 | 130 | synchronize(function() { 131 | try { 132 | QUnit.reset(); 133 | } catch(e) { 134 | fail("reset() failed, following Test " + name + ", exception and reset fn follows", e, reset); 135 | } 136 | 137 | if ( config.expected && config.expected != config.assertions.length ) { 138 | QUnit.ok( false, "Expected " + config.expected + " assertions, but " + config.assertions.length + " were run" ); 139 | } 140 | 141 | var good = 0, bad = 0, 142 | tests = id("qunit-tests"); 143 | 144 | config.stats.all += config.assertions.length; 145 | config.moduleStats.all += config.assertions.length; 146 | 147 | if ( tests ) { 148 | var ol = document.createElement("ol"); 149 | 150 | for ( var i = 0; i < config.assertions.length; i++ ) { 151 | var assertion = config.assertions[i]; 152 | 153 | var li = document.createElement("li"); 154 | li.className = assertion.result ? "pass" : "fail"; 155 | li.innerHTML = assertion.message || "(no message)"; 156 | ol.appendChild( li ); 157 | 158 | if ( assertion.result ) { 159 | good++; 160 | } else { 161 | bad++; 162 | config.stats.bad++; 163 | config.moduleStats.bad++; 164 | } 165 | } 166 | if (bad == 0) { 167 | ol.style.display = "none"; 168 | } 169 | 170 | var b = document.createElement("strong"); 171 | b.innerHTML = name + " (" + bad + ", " + good + ", " + config.assertions.length + ")"; 172 | 173 | addEvent(b, "click", function() { 174 | var next = b.nextSibling, display = next.style.display; 175 | next.style.display = display === "none" ? "block" : "none"; 176 | }); 177 | 178 | addEvent(b, "dblclick", function(e) { 179 | var target = e && e.target ? e.target : window.event.srcElement; 180 | if ( target.nodeName.toLowerCase() == "span" || target.nodeName.toLowerCase() == "b" ) { 181 | target = target.parentNode; 182 | } 183 | if ( window.location && target.nodeName.toLowerCase() === "strong" ) { 184 | window.location.search = "?" + encodeURIComponent(getText([target]).replace(/\(.+\)$/, "").replace(/(^\s*|\s*$)/g, "")); 185 | } 186 | }); 187 | 188 | var li = id("current-test-output"); 189 | li.id = ""; 190 | li.className = bad ? "fail" : "pass"; 191 | li.removeChild( li.firstChild ); 192 | li.appendChild( b ); 193 | li.appendChild( ol ); 194 | 195 | if ( bad ) { 196 | var toolbar = id("qunit-testrunner-toolbar"); 197 | if ( toolbar ) { 198 | toolbar.style.display = "block"; 199 | id("qunit-filter-pass").disabled = null; 200 | id("qunit-filter-missing").disabled = null; 201 | } 202 | } 203 | 204 | } else { 205 | for ( var i = 0; i < config.assertions.length; i++ ) { 206 | if ( !config.assertions[i].result ) { 207 | bad++; 208 | config.stats.bad++; 209 | config.moduleStats.bad++; 210 | } 211 | } 212 | } 213 | 214 | QUnit.testDone( testName, bad, config.assertions.length ); 215 | 216 | if ( !window.setTimeout && !config.queue.length ) { 217 | done(); 218 | } 219 | }); 220 | 221 | if ( window.setTimeout && !config.doneTimer ) { 222 | config.doneTimer = window.setTimeout(function(){ 223 | if ( !config.queue.length ) { 224 | done(); 225 | } else { 226 | synchronize( done ); 227 | } 228 | }, 13); 229 | } 230 | }, 231 | 232 | /** 233 | * Specify the number of expected assertions to gurantee that failed test (no assertions are run at all) don't slip through. 234 | */ 235 | expect: function(asserts) { 236 | config.expected = asserts; 237 | }, 238 | 239 | /** 240 | * Asserts true. 241 | * @example ok( "asdfasdf".length > 5, "There must be at least 5 chars" ); 242 | */ 243 | ok: function(a, msg) { 244 | msg = escapeHtml(msg); 245 | QUnit.log(a, msg); 246 | 247 | config.assertions.push({ 248 | result: !!a, 249 | message: msg 250 | }); 251 | }, 252 | 253 | /** 254 | * Checks that the first two arguments are equal, with an optional message. 255 | * Prints out both actual and expected values. 256 | * 257 | * Prefered to ok( actual == expected, message ) 258 | * 259 | * @example equal( format("Received {0} bytes.", 2), "Received 2 bytes." ); 260 | * 261 | * @param Object actual 262 | * @param Object expected 263 | * @param String message (optional) 264 | */ 265 | equal: function(actual, expected, message) { 266 | push(expected == actual, actual, expected, message); 267 | }, 268 | 269 | notEqual: function(actual, expected, message) { 270 | push(expected != actual, actual, expected, message); 271 | }, 272 | 273 | deepEqual: function(actual, expected, message) { 274 | push(QUnit.equiv(actual, expected), actual, expected, message); 275 | }, 276 | 277 | notDeepEqual: function(actual, expected, message) { 278 | push(!QUnit.equiv(actual, expected), actual, expected, message); 279 | }, 280 | 281 | strictEqual: function(actual, expected, message) { 282 | push(expected === actual, actual, expected, message); 283 | }, 284 | 285 | notStrictEqual: function(actual, expected, message) { 286 | push(expected !== actual, actual, expected, message); 287 | }, 288 | 289 | raises: function(fn, message) { 290 | try { 291 | fn(); 292 | ok( false, message ); 293 | } 294 | catch (e) { 295 | ok( true, message ); 296 | } 297 | }, 298 | 299 | start: function() { 300 | // A slight delay, to avoid any current callbacks 301 | if ( window.setTimeout ) { 302 | window.setTimeout(function() { 303 | if ( config.timeout ) { 304 | clearTimeout(config.timeout); 305 | } 306 | 307 | config.blocking = false; 308 | process(); 309 | }, 13); 310 | } else { 311 | config.blocking = false; 312 | process(); 313 | } 314 | }, 315 | 316 | stop: function(timeout) { 317 | config.blocking = true; 318 | 319 | if ( timeout && window.setTimeout ) { 320 | config.timeout = window.setTimeout(function() { 321 | QUnit.ok( false, "Test timed out" ); 322 | QUnit.start(); 323 | }, timeout); 324 | } 325 | } 326 | 327 | }; 328 | 329 | // Backwards compatibility, deprecated 330 | QUnit.equals = QUnit.equal; 331 | QUnit.same = QUnit.deepEqual; 332 | 333 | // Maintain internal state 334 | var config = { 335 | // The queue of tests to run 336 | queue: [], 337 | 338 | // block until document ready 339 | blocking: true 340 | }; 341 | 342 | // Load paramaters 343 | (function() { 344 | var location = window.location || { search: "", protocol: "file:" }, 345 | GETParams = location.search.slice(1).split('&'); 346 | 347 | for ( var i = 0; i < GETParams.length; i++ ) { 348 | GETParams[i] = decodeURIComponent( GETParams[i] ); 349 | if ( GETParams[i] === "noglobals" ) { 350 | GETParams.splice( i, 1 ); 351 | i--; 352 | config.noglobals = true; 353 | } else if ( GETParams[i].search('=') > -1 ) { 354 | GETParams.splice( i, 1 ); 355 | i--; 356 | } 357 | } 358 | 359 | // restrict modules/tests by get parameters 360 | config.filters = GETParams; 361 | 362 | // Figure out if we're running the tests from a server or not 363 | QUnit.isLocal = !!(location.protocol === 'file:'); 364 | })(); 365 | 366 | // Expose the API as global variables, unless an 'exports' 367 | // object exists, in that case we assume we're in CommonJS 368 | if ( typeof exports === "undefined" || typeof require === "undefined" ) { 369 | extend(window, QUnit); 370 | window.QUnit = QUnit; 371 | } else { 372 | extend(exports, QUnit); 373 | exports.QUnit = QUnit; 374 | } 375 | 376 | // define these after exposing globals to keep them in these QUnit namespace only 377 | extend(QUnit, { 378 | config: config, 379 | 380 | // Initialize the configuration options 381 | init: function() { 382 | extend(config, { 383 | stats: { all: 0, bad: 0 }, 384 | moduleStats: { all: 0, bad: 0 }, 385 | started: +new Date, 386 | updateRate: 1000, 387 | blocking: false, 388 | autostart: true, 389 | autorun: false, 390 | assertions: [], 391 | filters: [], 392 | queue: [] 393 | }); 394 | 395 | var tests = id("qunit-tests"), 396 | banner = id("qunit-banner"), 397 | result = id("qunit-testresult"); 398 | 399 | if ( tests ) { 400 | tests.innerHTML = ""; 401 | } 402 | 403 | if ( banner ) { 404 | banner.className = ""; 405 | } 406 | 407 | if ( result ) { 408 | result.parentNode.removeChild( result ); 409 | } 410 | }, 411 | 412 | /** 413 | * Resets the test setup. Useful for tests that modify the DOM. 414 | */ 415 | reset: function() { 416 | if ( window.jQuery ) { 417 | jQuery("#main, #qunit-fixture").html( config.fixture ); 418 | } 419 | }, 420 | 421 | /** 422 | * Trigger an event on an element. 423 | * 424 | * @example triggerEvent( document.body, "click" ); 425 | * 426 | * @param DOMElement elem 427 | * @param String type 428 | */ 429 | triggerEvent: function( elem, type, event ) { 430 | if ( document.createEvent ) { 431 | event = document.createEvent("MouseEvents"); 432 | event.initMouseEvent(type, true, true, elem.ownerDocument.defaultView, 433 | 0, 0, 0, 0, 0, false, false, false, false, 0, null); 434 | elem.dispatchEvent( event ); 435 | 436 | } else if ( elem.fireEvent ) { 437 | elem.fireEvent("on"+type); 438 | } 439 | }, 440 | 441 | // Safe object type checking 442 | is: function( type, obj ) { 443 | return QUnit.objectType( obj ) == type; 444 | }, 445 | 446 | objectType: function( obj ) { 447 | if (typeof obj === "undefined") { 448 | return "undefined"; 449 | 450 | // consider: typeof null === object 451 | } 452 | if (obj === null) { 453 | return "null"; 454 | } 455 | 456 | var type = Object.prototype.toString.call( obj ) 457 | .match(/^\[object\s(.*)\]$/)[1] || ''; 458 | 459 | switch (type) { 460 | case 'Number': 461 | if (isNaN(obj)) { 462 | return "nan"; 463 | } else { 464 | return "number"; 465 | } 466 | case 'String': 467 | case 'Boolean': 468 | case 'Array': 469 | case 'Date': 470 | case 'RegExp': 471 | case 'Function': 472 | return type.toLowerCase(); 473 | } 474 | if (typeof obj === "object") { 475 | return "object"; 476 | } 477 | return undefined; 478 | }, 479 | 480 | // Logging callbacks 481 | begin: function() {}, 482 | done: function(failures, total) {}, 483 | log: function(result, message) {}, 484 | testStart: function(name, testEnvironment) {}, 485 | testDone: function(name, failures, total) {}, 486 | moduleStart: function(name, testEnvironment) {}, 487 | moduleDone: function(name, failures, total) {} 488 | }); 489 | 490 | if ( typeof document === "undefined" || document.readyState === "complete" ) { 491 | config.autorun = true; 492 | } 493 | 494 | addEvent(window, "load", function() { 495 | QUnit.begin(); 496 | 497 | // Initialize the config, saving the execution queue 498 | var oldconfig = extend({}, config); 499 | QUnit.init(); 500 | extend(config, oldconfig); 501 | 502 | config.blocking = false; 503 | 504 | var userAgent = id("qunit-userAgent"); 505 | if ( userAgent ) { 506 | userAgent.innerHTML = navigator.userAgent; 507 | } 508 | 509 | var toolbar = id("qunit-testrunner-toolbar"); 510 | if ( toolbar ) { 511 | toolbar.style.display = "none"; 512 | 513 | var filter = document.createElement("input"); 514 | filter.type = "checkbox"; 515 | filter.id = "qunit-filter-pass"; 516 | filter.disabled = true; 517 | addEvent( filter, "click", function() { 518 | var li = document.getElementsByTagName("li"); 519 | for ( var i = 0; i < li.length; i++ ) { 520 | if ( li[i].className.indexOf("pass") > -1 ) { 521 | li[i].style.display = filter.checked ? "none" : ""; 522 | } 523 | } 524 | }); 525 | toolbar.appendChild( filter ); 526 | 527 | var label = document.createElement("label"); 528 | label.setAttribute("for", "qunit-filter-pass"); 529 | label.innerHTML = "Hide passed tests"; 530 | toolbar.appendChild( label ); 531 | 532 | var missing = document.createElement("input"); 533 | missing.type = "checkbox"; 534 | missing.id = "qunit-filter-missing"; 535 | missing.disabled = true; 536 | addEvent( missing, "click", function() { 537 | var li = document.getElementsByTagName("li"); 538 | for ( var i = 0; i < li.length; i++ ) { 539 | if ( li[i].className.indexOf("fail") > -1 && li[i].innerHTML.indexOf('missing test - untested code is broken code') > - 1 ) { 540 | li[i].parentNode.parentNode.style.display = missing.checked ? "none" : "block"; 541 | } 542 | } 543 | }); 544 | toolbar.appendChild( missing ); 545 | 546 | label = document.createElement("label"); 547 | label.setAttribute("for", "qunit-filter-missing"); 548 | label.innerHTML = "Hide missing tests (untested code is broken code)"; 549 | toolbar.appendChild( label ); 550 | } 551 | 552 | var main = id('main') || id('qunit-fixture'); 553 | if ( main ) { 554 | config.fixture = main.innerHTML; 555 | } 556 | 557 | if (config.autostart) { 558 | QUnit.start(); 559 | } 560 | }); 561 | 562 | function done() { 563 | if ( config.doneTimer && window.clearTimeout ) { 564 | window.clearTimeout( config.doneTimer ); 565 | config.doneTimer = null; 566 | } 567 | 568 | if ( config.queue.length ) { 569 | config.doneTimer = window.setTimeout(function(){ 570 | if ( !config.queue.length ) { 571 | done(); 572 | } else { 573 | synchronize( done ); 574 | } 575 | }, 13); 576 | 577 | return; 578 | } 579 | 580 | config.autorun = true; 581 | 582 | // Log the last module results 583 | if ( config.currentModule ) { 584 | QUnit.moduleDone( config.currentModule, config.moduleStats.bad, config.moduleStats.all ); 585 | } 586 | 587 | var banner = id("qunit-banner"), 588 | tests = id("qunit-tests"), 589 | html = ['Tests completed in ', 590 | +new Date - config.started, ' milliseconds.
    ', 591 | '', config.stats.all - config.stats.bad, ' tests of ', config.stats.all, ' passed, ', config.stats.bad,' failed.'].join(''); 592 | 593 | if ( banner ) { 594 | banner.className = (config.stats.bad ? "qunit-fail" : "qunit-pass"); 595 | } 596 | 597 | if ( tests ) { 598 | var result = id("qunit-testresult"); 599 | 600 | if ( !result ) { 601 | result = document.createElement("p"); 602 | result.id = "qunit-testresult"; 603 | result.className = "result"; 604 | tests.parentNode.insertBefore( result, tests.nextSibling ); 605 | } 606 | 607 | result.innerHTML = html; 608 | } 609 | 610 | QUnit.done( config.stats.bad, config.stats.all ); 611 | } 612 | 613 | function validTest( name ) { 614 | var i = config.filters.length, 615 | run = false; 616 | 617 | if ( !i ) { 618 | return true; 619 | } 620 | 621 | while ( i-- ) { 622 | var filter = config.filters[i], 623 | not = filter.charAt(0) == '!'; 624 | 625 | if ( not ) { 626 | filter = filter.slice(1); 627 | } 628 | 629 | if ( name.indexOf(filter) !== -1 ) { 630 | return !not; 631 | } 632 | 633 | if ( not ) { 634 | run = true; 635 | } 636 | } 637 | 638 | return run; 639 | } 640 | 641 | function escapeHtml(s) { 642 | s = s === null ? "" : s + ""; 643 | return s.replace(/[\&"<>\\]/g, function(s) { 644 | switch(s) { 645 | case "&": return "&"; 646 | case "\\": return "\\\\";; 647 | case '"': return '\"';; 648 | case "<": return "<"; 649 | case ">": return ">"; 650 | default: return s; 651 | } 652 | }); 653 | } 654 | 655 | function push(result, actual, expected, message) { 656 | message = escapeHtml(message) || (result ? "okay" : "failed"); 657 | message = '' + message + ""; 658 | expected = escapeHtml(QUnit.jsDump.parse(expected)); 659 | actual = escapeHtml(QUnit.jsDump.parse(actual)); 660 | var output = message + ', expected: ' + expected + ''; 661 | if (actual != expected) { 662 | output += ' result: ' + actual + ', diff: ' + QUnit.diff(expected, actual); 663 | } 664 | 665 | // can't use ok, as that would double-escape messages 666 | QUnit.log(result, output); 667 | config.assertions.push({ 668 | result: !!result, 669 | message: output 670 | }); 671 | } 672 | 673 | function synchronize( callback ) { 674 | config.queue.push( callback ); 675 | 676 | if ( config.autorun && !config.blocking ) { 677 | process(); 678 | } 679 | } 680 | 681 | function process() { 682 | var start = (new Date()).getTime(); 683 | 684 | while ( config.queue.length && !config.blocking ) { 685 | if ( config.updateRate <= 0 || (((new Date()).getTime() - start) < config.updateRate) ) { 686 | config.queue.shift()(); 687 | 688 | } else { 689 | setTimeout( process, 13 ); 690 | break; 691 | } 692 | } 693 | } 694 | 695 | function saveGlobal() { 696 | config.pollution = []; 697 | 698 | if ( config.noglobals ) { 699 | for ( var key in window ) { 700 | config.pollution.push( key ); 701 | } 702 | } 703 | } 704 | 705 | function checkPollution( name ) { 706 | var old = config.pollution; 707 | saveGlobal(); 708 | 709 | var newGlobals = diff( old, config.pollution ); 710 | if ( newGlobals.length > 0 ) { 711 | ok( false, "Introduced global variable(s): " + newGlobals.join(", ") ); 712 | config.expected++; 713 | } 714 | 715 | var deletedGlobals = diff( config.pollution, old ); 716 | if ( deletedGlobals.length > 0 ) { 717 | ok( false, "Deleted global variable(s): " + deletedGlobals.join(", ") ); 718 | config.expected++; 719 | } 720 | } 721 | 722 | // returns a new Array with the elements that are in a but not in b 723 | function diff( a, b ) { 724 | var result = a.slice(); 725 | for ( var i = 0; i < result.length; i++ ) { 726 | for ( var j = 0; j < b.length; j++ ) { 727 | if ( result[i] === b[j] ) { 728 | result.splice(i, 1); 729 | i--; 730 | break; 731 | } 732 | } 733 | } 734 | return result; 735 | } 736 | 737 | function fail(message, exception, callback) { 738 | if ( typeof console !== "undefined" && console.error && console.warn ) { 739 | console.error(message); 740 | console.error(exception); 741 | console.warn(callback.toString()); 742 | 743 | } else if ( window.opera && opera.postError ) { 744 | opera.postError(message, exception, callback.toString); 745 | } 746 | } 747 | 748 | function extend(a, b) { 749 | for ( var prop in b ) { 750 | a[prop] = b[prop]; 751 | } 752 | 753 | return a; 754 | } 755 | 756 | function addEvent(elem, type, fn) { 757 | if ( elem.addEventListener ) { 758 | elem.addEventListener( type, fn, false ); 759 | } else if ( elem.attachEvent ) { 760 | elem.attachEvent( "on" + type, fn ); 761 | } else { 762 | fn(); 763 | } 764 | } 765 | 766 | function id(name) { 767 | return !!(typeof document !== "undefined" && document && document.getElementById) && 768 | document.getElementById( name ); 769 | } 770 | 771 | // Test for equality any JavaScript type. 772 | // Discussions and reference: http://philrathe.com/articles/equiv 773 | // Test suites: http://philrathe.com/tests/equiv 774 | // Author: Philippe Rathé 775 | QUnit.equiv = function () { 776 | 777 | var innerEquiv; // the real equiv function 778 | var callers = []; // stack to decide between skip/abort functions 779 | var parents = []; // stack to avoiding loops from circular referencing 780 | 781 | // Call the o related callback with the given arguments. 782 | function bindCallbacks(o, callbacks, args) { 783 | var prop = QUnit.objectType(o); 784 | if (prop) { 785 | if (QUnit.objectType(callbacks[prop]) === "function") { 786 | return callbacks[prop].apply(callbacks, args); 787 | } else { 788 | return callbacks[prop]; // or undefined 789 | } 790 | } 791 | } 792 | 793 | var callbacks = function () { 794 | 795 | // for string, boolean, number and null 796 | function useStrictEquality(b, a) { 797 | if (b instanceof a.constructor || a instanceof b.constructor) { 798 | // to catch short annotaion VS 'new' annotation of a declaration 799 | // e.g. var i = 1; 800 | // var j = new Number(1); 801 | return a == b; 802 | } else { 803 | return a === b; 804 | } 805 | } 806 | 807 | return { 808 | "string": useStrictEquality, 809 | "boolean": useStrictEquality, 810 | "number": useStrictEquality, 811 | "null": useStrictEquality, 812 | "undefined": useStrictEquality, 813 | 814 | "nan": function (b) { 815 | return isNaN(b); 816 | }, 817 | 818 | "date": function (b, a) { 819 | return QUnit.objectType(b) === "date" && a.valueOf() === b.valueOf(); 820 | }, 821 | 822 | "regexp": function (b, a) { 823 | return QUnit.objectType(b) === "regexp" && 824 | a.source === b.source && // the regex itself 825 | a.global === b.global && // and its modifers (gmi) ... 826 | a.ignoreCase === b.ignoreCase && 827 | a.multiline === b.multiline; 828 | }, 829 | 830 | // - skip when the property is a method of an instance (OOP) 831 | // - abort otherwise, 832 | // initial === would have catch identical references anyway 833 | "function": function () { 834 | var caller = callers[callers.length - 1]; 835 | return caller !== Object && 836 | typeof caller !== "undefined"; 837 | }, 838 | 839 | "array": function (b, a) { 840 | var i, j, loop; 841 | var len; 842 | 843 | // b could be an object literal here 844 | if ( ! (QUnit.objectType(b) === "array")) { 845 | return false; 846 | } 847 | 848 | len = a.length; 849 | if (len !== b.length) { // safe and faster 850 | return false; 851 | } 852 | 853 | //track reference to avoid circular references 854 | parents.push(a); 855 | for (i = 0; i < len; i++) { 856 | loop = false; 857 | for(j=0;j= 0) { 1002 | type = "array"; 1003 | } else { 1004 | type = typeof obj; 1005 | } 1006 | return type; 1007 | }, 1008 | separator:function() { 1009 | return this.multiline ? this.HTML ? '
    ' : '\n' : this.HTML ? ' ' : ' '; 1010 | }, 1011 | indent:function( extra ) {// extra can be a number, shortcut for increasing-calling-decreasing 1012 | if ( !this.multiline ) 1013 | return ''; 1014 | var chr = this.indentChar; 1015 | if ( this.HTML ) 1016 | chr = chr.replace(/\t/g,' ').replace(/ /g,' '); 1017 | return Array( this._depth_ + (extra||0) ).join(chr); 1018 | }, 1019 | up:function( a ) { 1020 | this._depth_ += a || 1; 1021 | }, 1022 | down:function( a ) { 1023 | this._depth_ -= a || 1; 1024 | }, 1025 | setParser:function( name, parser ) { 1026 | this.parsers[name] = parser; 1027 | }, 1028 | // The next 3 are exposed so you can use them 1029 | quote:quote, 1030 | literal:literal, 1031 | join:join, 1032 | // 1033 | _depth_: 1, 1034 | // This is the list of parsers, to modify them, use jsDump.setParser 1035 | parsers:{ 1036 | window: '[Window]', 1037 | document: '[Document]', 1038 | error:'[ERROR]', //when no parser is found, shouldn't happen 1039 | unknown: '[Unknown]', 1040 | 'null':'null', 1041 | undefined:'undefined', 1042 | 'function':function( fn ) { 1043 | var ret = 'function', 1044 | name = 'name' in fn ? fn.name : (reName.exec(fn)||[])[1];//functions never have name in IE 1045 | if ( name ) 1046 | ret += ' ' + name; 1047 | ret += '('; 1048 | 1049 | ret = [ ret, this.parse( fn, 'functionArgs' ), '){'].join(''); 1050 | return join( ret, this.parse(fn,'functionCode'), '}' ); 1051 | }, 1052 | array: array, 1053 | nodelist: array, 1054 | arguments: array, 1055 | object:function( map ) { 1056 | var ret = [ ]; 1057 | this.up(); 1058 | for ( var key in map ) 1059 | ret.push( this.parse(key,'key') + ': ' + this.parse(map[key]) ); 1060 | this.down(); 1061 | return join( '{', ret, '}' ); 1062 | }, 1063 | node:function( node ) { 1064 | var open = this.HTML ? '<' : '<', 1065 | close = this.HTML ? '>' : '>'; 1066 | 1067 | var tag = node.nodeName.toLowerCase(), 1068 | ret = open + tag; 1069 | 1070 | for ( var a in this.DOMAttrs ) { 1071 | var val = node[this.DOMAttrs[a]]; 1072 | if ( val ) 1073 | ret += ' ' + a + '=' + this.parse( val, 'attribute' ); 1074 | } 1075 | return ret + close + open + '/' + tag + close; 1076 | }, 1077 | functionArgs:function( fn ) {//function calls it internally, it's the arguments part of the function 1078 | var l = fn.length; 1079 | if ( !l ) return ''; 1080 | 1081 | var args = Array(l); 1082 | while ( l-- ) 1083 | args[l] = String.fromCharCode(97+l);//97 is 'a' 1084 | return ' ' + args.join(', ') + ' '; 1085 | }, 1086 | key:quote, //object calls it internally, the key part of an item in a map 1087 | functionCode:'[code]', //function calls it internally, it's the content of the function 1088 | attribute:quote, //node calls it internally, it's an html attribute value 1089 | string:quote, 1090 | date:quote, 1091 | regexp:literal, //regex 1092 | number:literal, 1093 | 'boolean':literal 1094 | }, 1095 | DOMAttrs:{//attributes to dump from nodes, name=>realName 1096 | id:'id', 1097 | name:'name', 1098 | 'class':'className' 1099 | }, 1100 | HTML:false,//if true, entities are escaped ( <, >, \t, space and \n ) 1101 | indentChar:' ',//indentation unit 1102 | multiline:false //if true, items in a collection, are separated by a \n, else just a space. 1103 | }; 1104 | 1105 | return jsDump; 1106 | })(); 1107 | 1108 | // from Sizzle.js 1109 | function getText( elems ) { 1110 | var ret = "", elem; 1111 | 1112 | for ( var i = 0; elems[i]; i++ ) { 1113 | elem = elems[i]; 1114 | 1115 | // Get the text from text nodes and CDATA nodes 1116 | if ( elem.nodeType === 3 || elem.nodeType === 4 ) { 1117 | ret += elem.nodeValue; 1118 | 1119 | // Traverse everything else, except comment nodes 1120 | } else if ( elem.nodeType !== 8 ) { 1121 | ret += getText( elem.childNodes ); 1122 | } 1123 | } 1124 | 1125 | return ret; 1126 | }; 1127 | 1128 | /* 1129 | * Javascript Diff Algorithm 1130 | * By John Resig (http://ejohn.org/) 1131 | * Modified by Chu Alan "sprite" 1132 | * 1133 | * Released under the MIT license. 1134 | * 1135 | * More Info: 1136 | * http://ejohn.org/projects/javascript-diff-algorithm/ 1137 | * 1138 | * Usage: QUnit.diff(expected, actual) 1139 | * 1140 | * QUnit.diff("the quick brown fox jumped over", "the quick fox jumps over") == "the quick brown fox jumped jumps over" 1141 | */ 1142 | QUnit.diff = (function() { 1143 | function escape(s){ 1144 | var n = s; 1145 | n = n.replace(/&/g, "&"); 1146 | n = n.replace(//g, ">"); 1148 | n = n.replace(/"/g, """); 1149 | 1150 | return n; 1151 | } 1152 | 1153 | function diff(o, n){ 1154 | var ns = new Object(); 1155 | var os = new Object(); 1156 | 1157 | for (var i = 0; i < n.length; i++) { 1158 | if (ns[n[i]] == null) 1159 | ns[n[i]] = { 1160 | rows: new Array(), 1161 | o: null 1162 | }; 1163 | ns[n[i]].rows.push(i); 1164 | } 1165 | 1166 | for (var i = 0; i < o.length; i++) { 1167 | if (os[o[i]] == null) 1168 | os[o[i]] = { 1169 | rows: new Array(), 1170 | n: null 1171 | }; 1172 | os[o[i]].rows.push(i); 1173 | } 1174 | 1175 | for (var i in ns) { 1176 | if (ns[i].rows.length == 1 && typeof(os[i]) != "undefined" && os[i].rows.length == 1) { 1177 | n[ns[i].rows[0]] = { 1178 | text: n[ns[i].rows[0]], 1179 | row: os[i].rows[0] 1180 | }; 1181 | o[os[i].rows[0]] = { 1182 | text: o[os[i].rows[0]], 1183 | row: ns[i].rows[0] 1184 | }; 1185 | } 1186 | } 1187 | 1188 | for (var i = 0; i < n.length - 1; i++) { 1189 | if (n[i].text != null && n[i + 1].text == null && n[i].row + 1 < o.length && o[n[i].row + 1].text == null && 1190 | n[i + 1] == o[n[i].row + 1]) { 1191 | n[i + 1] = { 1192 | text: n[i + 1], 1193 | row: n[i].row + 1 1194 | }; 1195 | o[n[i].row + 1] = { 1196 | text: o[n[i].row + 1], 1197 | row: i + 1 1198 | }; 1199 | } 1200 | } 1201 | 1202 | for (var i = n.length - 1; i > 0; i--) { 1203 | if (n[i].text != null && n[i - 1].text == null && n[i].row > 0 && o[n[i].row - 1].text == null && 1204 | n[i - 1] == o[n[i].row - 1]) { 1205 | n[i - 1] = { 1206 | text: n[i - 1], 1207 | row: n[i].row - 1 1208 | }; 1209 | o[n[i].row - 1] = { 1210 | text: o[n[i].row - 1], 1211 | row: i - 1 1212 | }; 1213 | } 1214 | } 1215 | 1216 | return { 1217 | o: o, 1218 | n: n 1219 | }; 1220 | } 1221 | 1222 | return function(o, n){ 1223 | o = o.replace(/\s+$/, ''); 1224 | n = n.replace(/\s+$/, ''); 1225 | var out = diff(o == "" ? [] : o.split(/\s+/), n == "" ? [] : n.split(/\s+/)); 1226 | 1227 | var str = ""; 1228 | 1229 | var oSpace = o.match(/\s+/g); 1230 | if (oSpace == null) { 1231 | oSpace = ["\n"]; 1232 | } 1233 | else { 1234 | oSpace.push("\n"); 1235 | } 1236 | var nSpace = n.match(/\s+/g); 1237 | if (nSpace == null) { 1238 | nSpace = ["\n"]; 1239 | } 1240 | else { 1241 | nSpace.push("\n"); 1242 | } 1243 | 1244 | if (out.n.length == 0) { 1245 | for (var i = 0; i < out.o.length; i++) { 1246 | str += '' + escape(out.o[i]) + oSpace[i] + ""; 1247 | } 1248 | } 1249 | else { 1250 | if (out.n[0].text == null) { 1251 | for (n = 0; n < out.o.length && out.o[n].text == null; n++) { 1252 | str += '' + escape(out.o[n]) + oSpace[n] + ""; 1253 | } 1254 | } 1255 | 1256 | for (var i = 0; i < out.n.length; i++) { 1257 | if (out.n[i].text == null) { 1258 | str += '' + escape(out.n[i]) + nSpace[i] + ""; 1259 | } 1260 | else { 1261 | var pre = ""; 1262 | 1263 | for (n = out.n[i].row + 1; n < out.o.length && out.o[n].text == null; n++) { 1264 | pre += '' + escape(out.o[n]) + oSpace[n] + ""; 1265 | } 1266 | str += " " + out.n[i].text + nSpace[i] + pre; 1267 | } 1268 | } 1269 | } 1270 | 1271 | return str; 1272 | } 1273 | })(); 1274 | 1275 | })(this); 1276 | --------------------------------------------------------------------------------