├── test
├── index.css
├── tests.html
├── tests.js
├── qunit-1.11.0.css
└── qunit-1.11.0.js
├── examples
├── ex1.html
├── ex2.html
├── ex2.js
└── ex1.js
├── README.md
└── quadtree.js
/test/index.css:
--------------------------------------------------------------------------------
1 | canvas {
2 | border:1px solid #333;
3 | text-align:center;
4 | float:left;
5 | margin-right:10px;
6 | margin-bottom:20px;
7 | }
8 |
9 | h1 { border-bottom: 1px solid #333; }
10 |
11 | h2 { border-top: 1px solid #333; }
12 |
13 | #text { clear:both; }
14 |
15 |
16 |
17 |
18 |
--------------------------------------------------------------------------------
/test/tests.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
24 |
Performance
25 |
The performance gain using the quadtree data structure depends on the geographic
26 | location of the objects. Worst case complexity (All objects are
27 | in the same region) is O(n^2), excactly the same as brute force.
28 | Plus, the quadtree has to be build freshly for each set of collision checks. This also takes
29 | some time. As a consequence, brute force may actually be less expensive in some (very few)
30 | scenarios. However, most of the time, collision detection with quadtree will be
31 | significantly faster.
32 |
33 |
34 |
35 |
36 |
--------------------------------------------------------------------------------
/examples/ex2.js:
--------------------------------------------------------------------------------
1 | /* f***ing grusige code, aber isch jo nur e demo :) */
2 |
3 | function init_canvas(elementId) {
4 | var canvas, ctx;
5 | try {
6 | canvas = document.getElementById(elementId);
7 | ctx = canvas.getContext('2d');
8 | } catch(e) {
9 | return false;
10 | }
11 | ctx.height = canvas.height;
12 | ctx.width = canvas.width;
13 | ctx.clear = function() {
14 | this.clearRect(0,0,this.canvas.width,this.canvas.height)
15 | }
16 | return ctx;
17 | }
18 |
19 | //convenience
20 | function addEvent(el, evt, fn) {
21 | if (el.addEventListener) {
22 | el.addEventListener(evt, fn, false);
23 | } else if (el.attachEvent) {
24 | el.attachEvent('on' + evt, fn);
25 | }
26 | }
27 |
28 | // init canvas and quadtree
29 | var ctx = init_canvas('quadtree');
30 | var tree = QUAD.init({
31 | x : 0,
32 | y : 0,
33 | w : ctx.width,
34 | h : ctx.height,
35 | maxChildren : 1
36 | });
37 | tree.clear();
38 | var objects = [];
39 | ctx.font = "40pt Arial";
40 | ctx.fillText("Click me!", 135, ctx.height/2);
41 |
42 | var insert = true;
43 |
44 | // orb prototype
45 | var orb = {
46 | w:15,
47 | h:15,
48 | draw : function () {
49 |
50 | ctx.beginPath();
51 | ctx.rect(this.x, this.y, this.w, this.h);
52 |
53 | ctx.closePath();
54 | ctx.stroke();
55 |
56 | },
57 | fill : function () {
58 | ctx.fillStyle = "rgb(200,0,0)";
59 |
60 | ctx.fillRect(this.x, this.y, this.w, this.h);
61 |
62 | }
63 | }
64 | var in_sel = document.getElementById('in_sel');
65 | in_sel.onclick = function () {
66 | insert = !insert;
67 | if (insert) {
68 | in_sel.value = "Switch to select";
69 | } else {
70 | in_sel.value = "Switch to insert";
71 | }
72 |
73 | }
74 |
75 | document.getElementById('quadtree').onclick = function (event){
76 |
77 | //get x y coords of the cursor
78 | pos_x = event.offsetX?(event.offsetX):event.pageX-document.getElementById("quadtree").offsetLeft;
79 | pos_y = event.offsetY?(event.offsetY):event.pageY-document.getElementById("quadtree").offsetTop;
80 |
81 | if (event.button == 3) {
82 | alert(event.button);
83 | }
84 |
85 | // clear canvas
86 | ctx.beginPath();
87 | ctx.clear();
88 | ctx.closePath();
89 |
90 | for (var i = 0; i < objects.length; i++) {
91 | objects[i].draw(); // draw the objects
92 | }
93 |
94 | var grplen = 0;
95 | // fill out all objects of the current group
96 | if (insert) {
97 | //create an orb with cursor coordinates
98 | var o = Object.create(orb);
99 | o.x = pos_x;
100 | o.y = pos_y;
101 | o.draw();
102 | objects.push(o);
103 | tree.insert(o);
104 | tree.retrieve(o, function(item) {
105 | item.fill();
106 | ++grplen;
107 | });
108 | } else {
109 | tree.retrieve({
110 | x: pos_x,
111 | y: pos_y,
112 | h : 0,
113 | w : 0
114 | }, function(item) {
115 | item.fill();
116 | ++grplen;
117 | });
118 | }
119 |
120 | var quadcount = 0;
121 | var len = objects.length;
122 |
123 | for (i = 0; i < len; i++) {
124 | // count quadtree collision checks
125 | tree.retrieve(objects[i], function() {
126 | ++quadcount;
127 | });
128 | --quadcount;
129 | }
130 |
131 | drawRegions(tree.root);
132 |
133 | // display stats
134 | document.getElementById('objtotal').innerHTML = len;
135 | document.getElementById('objgroup').innerHTML = grplen;
136 | document.getElementById('brute').innerHTML = len * (len-1);
137 | document.getElementById('quad').innerHTML = quadcount;
138 | };
139 |
140 | document.getElementById('clear').onclick = function () {
141 | tree.clear();
142 | ctx.clear();
143 | objects.length = 0;
144 | ctx.clear(0,0,ctx.width, ctx.height);
145 | ctx.fillStyle='rgb(0,0,0)';
146 | ctx.fillText("Click me!", 135, ctx.height/2);
147 | drawRegions(tree.root);
148 |
149 | document.getElementById('objtotal').innerHTML = 0;
150 | document.getElementById('objgroup').innerHTML = 0;
151 | document.getElementById('brute').innerHTML = 0;
152 | document.getElementById('quad').innerHTML = 0;
153 | }
154 |
155 | // draws the region-frames
156 | var drawRegions = function (node) {
157 |
158 | var nodes = node.getNodes();
159 | if (nodes) {
160 | for (var i = 0; i < nodes.length; i++) {
161 | drawRegions(nodes[i]);
162 | }
163 | }
164 | ctx.beginPath();
165 | ctx.rect(node.x, node.y, node.w, node.h);
166 | ctx.stroke();
167 | ctx.closePath();
168 | }
--------------------------------------------------------------------------------
/test/qunit-1.11.0.css:
--------------------------------------------------------------------------------
1 | /**
2 | * QUnit v1.11.0 - A JavaScript Unit Testing Framework
3 | *
4 | * http://qunitjs.com
5 | *
6 | * Copyright 2012 jQuery Foundation and other contributors
7 | * Released under the MIT license.
8 | * http://jquery.org/license
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-header, #qunit-banner, #qunit-userAgent, #qunit-testresult, #qunit-modulefilter {
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: 5px 5px 0 0;
42 | -moz-border-radius: 5px 5px 0 0;
43 | -webkit-border-top-right-radius: 5px;
44 | -webkit-border-top-left-radius: 5px;
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-testrunner-toolbar label {
58 | display: inline-block;
59 | padding: 0 .5em 0 .1em;
60 | }
61 |
62 | #qunit-banner {
63 | height: 5px;
64 | }
65 |
66 | #qunit-testrunner-toolbar {
67 | padding: 0.5em 0 0.5em 2em;
68 | color: #5E740B;
69 | background-color: #eee;
70 | overflow: hidden;
71 | }
72 |
73 | #qunit-userAgent {
74 | padding: 0.5em 0 0.5em 2.5em;
75 | background-color: #2b81af;
76 | color: #fff;
77 | text-shadow: rgba(0, 0, 0, 0.5) 2px 2px 1px;
78 | }
79 |
80 | #qunit-modulefilter-container {
81 | float: right;
82 | }
83 |
84 | /** Tests: Pass/Fail */
85 |
86 | #qunit-tests {
87 | list-style-position: inside;
88 | }
89 |
90 | #qunit-tests li {
91 | padding: 0.4em 0.5em 0.4em 2.5em;
92 | border-bottom: 1px solid #fff;
93 | list-style-position: inside;
94 | }
95 |
96 | #qunit-tests.hidepass li.pass, #qunit-tests.hidepass li.running {
97 | display: none;
98 | }
99 |
100 | #qunit-tests li strong {
101 | cursor: pointer;
102 | }
103 |
104 | #qunit-tests li a {
105 | padding: 0.5em;
106 | color: #c2ccd1;
107 | text-decoration: none;
108 | }
109 | #qunit-tests li a:hover,
110 | #qunit-tests li a:focus {
111 | color: #000;
112 | }
113 |
114 | #qunit-tests li .runtime {
115 | float: right;
116 | font-size: smaller;
117 | }
118 |
119 | .qunit-assert-list {
120 | margin-top: 0.5em;
121 | padding: 0.5em;
122 |
123 | background-color: #fff;
124 |
125 | border-radius: 5px;
126 | -moz-border-radius: 5px;
127 | -webkit-border-radius: 5px;
128 | }
129 |
130 | .qunit-collapsed {
131 | display: none;
132 | }
133 |
134 | #qunit-tests table {
135 | border-collapse: collapse;
136 | margin-top: .2em;
137 | }
138 |
139 | #qunit-tests th {
140 | text-align: right;
141 | vertical-align: top;
142 | padding: 0 .5em 0 0;
143 | }
144 |
145 | #qunit-tests td {
146 | vertical-align: top;
147 | }
148 |
149 | #qunit-tests pre {
150 | margin: 0;
151 | white-space: pre-wrap;
152 | word-wrap: break-word;
153 | }
154 |
155 | #qunit-tests del {
156 | background-color: #e0f2be;
157 | color: #374e0c;
158 | text-decoration: none;
159 | }
160 |
161 | #qunit-tests ins {
162 | background-color: #ffcaca;
163 | color: #500;
164 | text-decoration: none;
165 | }
166 |
167 | /*** Test Counts */
168 |
169 | #qunit-tests b.counts { color: black; }
170 | #qunit-tests b.passed { color: #5E740B; }
171 | #qunit-tests b.failed { color: #710909; }
172 |
173 | #qunit-tests li li {
174 | padding: 5px;
175 | background-color: #fff;
176 | border-bottom: none;
177 | list-style-position: inside;
178 | }
179 |
180 | /*** Passing Styles */
181 |
182 | #qunit-tests li li.pass {
183 | color: #3c510c;
184 | background-color: #fff;
185 | border-left: 10px solid #C6E746;
186 | }
187 |
188 | #qunit-tests .pass { color: #528CE0; background-color: #D2E0E6; }
189 | #qunit-tests .pass .test-name { color: #366097; }
190 |
191 | #qunit-tests .pass .test-actual,
192 | #qunit-tests .pass .test-expected { color: #999999; }
193 |
194 | #qunit-banner.qunit-pass { background-color: #C6E746; }
195 |
196 | /*** Failing Styles */
197 |
198 | #qunit-tests li li.fail {
199 | color: #710909;
200 | background-color: #fff;
201 | border-left: 10px solid #EE5757;
202 | white-space: pre;
203 | }
204 |
205 | #qunit-tests > li:last-child {
206 | border-radius: 0 0 5px 5px;
207 | -moz-border-radius: 0 0 5px 5px;
208 | -webkit-border-bottom-right-radius: 5px;
209 | -webkit-border-bottom-left-radius: 5px;
210 | }
211 |
212 | #qunit-tests .fail { color: #000000; background-color: #EE5757; }
213 | #qunit-tests .fail .test-name,
214 | #qunit-tests .fail .module-name { color: #000000; }
215 |
216 | #qunit-tests .fail .test-actual { color: #EE5757; }
217 | #qunit-tests .fail .test-expected { color: green; }
218 |
219 | #qunit-banner.qunit-fail { background-color: #EE5757; }
220 |
221 |
222 | /** Result */
223 |
224 | #qunit-testresult {
225 | padding: 0.5em 0.5em 0.5em 2.5em;
226 |
227 | color: #2b81af;
228 | background-color: #D2E0E6;
229 |
230 | border-bottom: 1px solid white;
231 | }
232 | #qunit-testresult .module-name {
233 | font-weight: bold;
234 | }
235 |
236 | /** Fixture */
237 |
238 | #qunit-fixture {
239 | position: absolute;
240 | top: -10000px;
241 | left: -10000px;
242 | width: 1000px;
243 | height: 1000px;
244 | }
245 |
--------------------------------------------------------------------------------
/examples/ex1.js:
--------------------------------------------------------------------------------
1 | // global var for EX1
2 | var EX1 = {};
3 |
4 | EX1.orb = {
5 | draw : function () {
6 | EX1.ctx.strokeStyle = "rgb(0,0,0)";
7 | EX1.ctx.beginPath();
8 | EX1.ctx.rect(this.x, this.y, this.w, this.h);
9 | EX1.ctx.closePath();
10 | EX1.ctx.stroke();
11 | },
12 |
13 | fill : function () {
14 | EX1.ctx.fillStyle = "rgb(150,0,0)";
15 | EX1.ctx.fillRect(this.x, this.y, this.w, this.h);
16 | },
17 | move : function (dt) {
18 | this.x += (this.dx / 1000) * dt;
19 | this.y += (this.dy / 1000) * dt;
20 | if (this.x < 0 || this.x > EX1.width) {
21 | this.dx *= -1;
22 | }
23 | if (this.y < 0 || this.y > EX1.height) {
24 | this.dy *= -1;
25 | }
26 | }
27 | };
28 |
29 | EX1.simulation = (function () {
30 | var interval, dt, i, frames, finterval,
31 | drawRegions = true;
32 | return {
33 | start : function (t) {
34 | dt = t;
35 | if (!interval) {
36 | // start simulation interval
37 | interval = setInterval(this.simulate, dt);
38 | // start fps interval. this calculates the current frame rate and sets the counter
39 | // to zero
40 | finterval = setInterval(function () {
41 | document.getElementById("ex1-fps").innerHTML = frames * 4;
42 | frames = 0;
43 | }, 250);
44 | }
45 | },
46 |
47 | stop : function () { clearInterval(interval); clearInterval(finterval); },
48 |
49 | toggleRegions : function () { drawRegions = !drawRegions; },
50 |
51 | simulate : function () {
52 | var n = EX1.elements.length;
53 | // clear the canvas
54 | EX1.ctx.clearRect(0, 0, EX1.width, EX1.height);
55 | // draw the quadtree regions
56 | if (drawRegions) {
57 | QUAD.drawRegions(EX1.tree.root, EX1.ctx);
58 | }
59 | // collision detection
60 | EX1.CD.check(EX1.elements[i]);
61 | // draw and move the elements
62 | for (i = 0; i < n; i++) {
63 | EX1.elements[i].draw();
64 | EX1.elements[i].move(dt);
65 | }
66 | // display check count
67 | document.getElementById("ex1-checks").innerHTML = EX1.CD.getNChecks();
68 | // increase frame count
69 | frames++;
70 | }
71 | };
72 | }());
73 |
74 | EX1.init = function (numberOfElements) {
75 | var canvas, args, i, n = numberOfElements || 500;
76 | // init canvas or display error message
77 | if (!(canvas = document.getElementById("ex1-canv"))) {
78 | document.getElementsByTagName("body")[0].innerHTML = "Canvas Error";
79 | }
80 | // init canvas context and save canvas properties for further use
81 | EX1.ctx = canvas.getContext("2d");
82 | EX1.height = canvas.height;
83 | EX1.width = canvas.width;
84 | // init the quadtree
85 | args = {x : 0, y : 0, h : EX1.height, w : EX1.width, maxChildren : 5, maxDepth : 5};
86 | EX1.tree = QUAD.init(args);
87 | // init the array that holds the objects
88 | EX1.elements = [];
89 | // fill the array with fresh objects
90 | for (i = 0; i < n; i++) {
91 | EX1.elements.push(Object.create(EX1.orb, {
92 | // set a random position
93 | x : {value: Math.random() * EX1.width, writable: true, enumerable: true, configurable: false },
94 | y : {value: Math.random() * EX1.height, writable: true, enumerable: true, configurable: false },
95 | // set random speed
96 | dx : {value: Math.random() * 100, writable: true, enumerable: true, configurable: false },
97 | dy : {value: Math.random() * 100, writable: true, enumerable: true, configurable: false },
98 | // set random size
99 | h : {value: (Math.random() * 10) + 5, writable: true, enumerable: true, configurable: false},
100 | w : {value: (Math.random() * 10) + 5, writable: true, enumerable: true, configurable: false}
101 | }));
102 | }
103 | // init buttons
104 | EX1.init_controls();
105 | // start simulation
106 | EX1.simulation.start(16);
107 | };
108 |
109 | EX1.CD = (function () {
110 | var bruteForce = false,
111 | nChecks;
112 | return {
113 | check : function (orb) {
114 | // reset check counter
115 | nChecks = 0;
116 | // select check algrithm
117 | if (!bruteForce) {
118 | this.quadTree(orb);
119 | } else {
120 | this.bruteForce(orb);
121 | }
122 | },
123 |
124 | quadTree : function () {
125 | var n = EX1.elements.length, m, region, i, k, orb;
126 | // clear the quadtree
127 | EX1.tree.clear();
128 | // fill the quadtree
129 | EX1.tree.insert(EX1.elements);
130 | // iterate all elements
131 | for (i = 0; i < n; i++) {
132 | orb = EX1.elements[i];
133 | // get all elements in the same region as orb
134 | region = EX1.tree.retrieve(orb, function(item) {
135 | EX1.CD.detectCollision(orb, item);
136 | nChecks++;
137 | });
138 | }
139 | },
140 |
141 | bruteForce : function () {
142 | var n = EX1.elements.length, i, k, orb;
143 | // iterate all elements
144 | for (i = 0; i < n; i++) {
145 | orb = EX1.elements[i];
146 | // iterate all elements and check for collision with the current element
147 | for (k = 0; k < n; k++) {
148 | this.detectCollision(orb, EX1.elements[k]);
149 | // increase check counter
150 | nChecks++;
151 | }
152 | }
153 | },
154 |
155 | detectCollision : function (orb1, orb2) {
156 | if (orb1 === orb2) {
157 | return;
158 | }
159 | if (orb1.x + orb1.w < orb2.x) {
160 | return;
161 | }
162 | if (orb1.x > orb2.x + orb2.w) {
163 | return;
164 | }
165 | if (orb1.y + orb1.h < orb2.y) {
166 | return;
167 | }
168 | if (orb1.y > orb2.y + orb2.h) {
169 | return;
170 | }
171 | orb1.fill();
172 | },
173 |
174 | toggleDetection : function () { bruteForce = !bruteForce; },
175 |
176 | getNChecks : function () { return nChecks; }
177 | };
178 | }());
179 |
180 | // ugly code
181 | EX1.init_controls = function () {
182 | var cRegions = document.getElementById("ex1-regions"),
183 | r250 = document.getElementById("250"),
184 | r500 = document.getElementById("500"),
185 | r1000 = document.getElementById("1000"),
186 | cType = document.getElementById("ex1-type");
187 | cRegions.onclick = function () { EX1.simulation.toggleRegions(); };
188 | r250.onclick = function () { EX1.init(250); };
189 | r500.onclick = function () { EX1.init(500); };
190 | r1000.onclick = function () { EX1.init(1000); };
191 | cType.onclick = function () { EX1.CD.toggleDetection(); };
192 | };
193 |
194 | // allows us to draw the current quadtree regions to a specific canvas
195 | QUAD.drawRegions = function (anode, ctx) {
196 | var nodes = anode.getNodes(), i;
197 | if (nodes) {
198 | for (i = 0; i < nodes.length; i++) {
199 | QUAD.drawRegions(nodes[i], ctx);
200 | }
201 | }
202 | ctx.beginPath();
203 | ctx.rect(anode.x, anode.y, anode.w, anode.h);
204 | ctx.stroke();
205 | ctx.closePath();
206 | };
207 |
208 | // init the application
209 | EX1.init(250);
--------------------------------------------------------------------------------
/quadtree.js:
--------------------------------------------------------------------------------
1 | /*
2 | * QuadTree Implementation in JavaScript
3 | * @author: silflow