From 9e9510a95fb3e7e0b553b45c8c1712601fa79b35 Mon Sep 17 00:00:00 2001 From: Ronen Date: Wed, 29 Jul 2015 03:21:50 +0300 Subject: [PATCH] update to version 1.1 ### 1.1 - Added repel() to shapes. - Improved some performance, especially with moving objects aabb calculations. - Fixed debug coloring bug with composite shapes. - Added another example with resolving penetration. - Extended Vectors API. - Added render bounding-box functionality to all shapes. - Fixed world render function to accept if should render grid and bounding boxes. --- README.md | 36 + dist/{ => older}/sscd.1.0.js | 0 dist/{ => older}/sscd.1.0.min.js | 0 dist/sscd.1.1.js | 2506 ++++++++++++++++++++++++++++++ dist/sscd.1.1.min.js | 1 + examples/README.md | 3 +- examples/example1.html | 11 +- examples/example2.html | 16 +- examples/example3.html | 11 +- examples/example4.html | 213 +++ src/shapes/circle.js | 16 +- src/shapes/composite_shape.js | 55 +- src/shapes/line.js | 2 +- src/shapes/lines_strip.js | 21 +- src/shapes/rectangle.js | 2 +- src/shapes/shape.js | 97 +- src/utils/vector.js | 43 + src/world.js | 39 +- 18 files changed, 3018 insertions(+), 54 deletions(-) rename dist/{ => older}/sscd.1.0.js (100%) rename dist/{ => older}/sscd.1.0.min.js (100%) create mode 100644 dist/sscd.1.1.js create mode 100644 dist/sscd.1.1.min.js create mode 100644 examples/example4.html diff --git a/README.md b/README.md index 314881a..960f898 100644 --- a/README.md +++ b/README.md @@ -254,6 +254,30 @@ That's it. You can use any string as tag and they don't need any special initial All new shapes are tagged as everything by default, and all collision tests will accept any tag unless stated otherwise. Note however that you are limited to only *32 different tags. (*can be more on some OS and browsers, but 32 is a safe number that should work for all machines). +### Resolve penetration + +While resolving penetration is a little bit outside SSCD scope, the framework does provide a helper function to get you started. +Every shape has a repel() function that will repel/push an object outside of the center of the shape. + +Here's a quick example of how you can use it to resolve penetration, assuming player_shape is the shape representing player collision: + +```javascript +// pick object player collide with +var collide_with = world.pick_object(player_shape); + +// if object found, use repel to prevent penetration +if (collide_with) +{ + collide_with.repel(player_shape, 1, 1); + + // here we will set the position of the player sprite to match its collision body.. +} +``` + +As you can see repel() is very simple. First param is which shape or vector to push outside, second param is pushing force, and last param is how many iterations of repel to run, while every iteration we check and if no longer collide we stop pushing. + +Note that while this method is good enough to get you started, for a serious game you'd probably want to implement a better penetration resolving algorithm. + ### Memory usage SSCD is very memory-efficient, and should not pose any problems. @@ -265,6 +289,18 @@ If this insignificant memory waste really bother you, you can tell the collision ```javascript world.cleanup(); ``` + +## Changelog + +### 1.1 + +- Added repel() to shapes. +- Improved some performance, especially with moving objects aabb calculations. +- Fixed debug coloring bug with composite shapes. +- Added another example with resolving penetration. +- Extended Vectors API. +- Added render bounding-box functionality to all shapes. +- Fixed world render function to accept if should render grid and bounding boxes. ## License SSCD is provided under the zlib-license, and is absolutely free for use for educational & commercial purposes. diff --git a/dist/sscd.1.0.js b/dist/older/sscd.1.0.js similarity index 100% rename from dist/sscd.1.0.js rename to dist/older/sscd.1.0.js diff --git a/dist/sscd.1.0.min.js b/dist/older/sscd.1.0.min.js similarity index 100% rename from dist/sscd.1.0.min.js rename to dist/older/sscd.1.0.min.js diff --git a/dist/sscd.1.1.js b/dist/sscd.1.1.js new file mode 100644 index 0000000..402f50a --- /dev/null +++ b/dist/sscd.1.1.js @@ -0,0 +1,2506 @@ +// FILE: license.js + +// SSCD (Super Simple Collision Detection) is distributed with the zlib-license: + +/* + This software is provided 'as-is', without any express or implied + warranty. In no event will the authors be held liable for any damages + arising from the use of this software. + + Permission is granted to anyone to use this software for any purpose, + including commercial applications, and to alter it and redistribute it + freely, subject to the following restrictions: + + 1. The origin of this software must not be misrepresented; you must not + claim that you wrote the original software. If you use this software + in a product, an acknowledgment in the product documentation would be + appreciated but is not required. + 2. Altered source versions must be plainly marked as such, and must not be + misrepresented as being the original software. + 3. This notice may not be removed or altered from any source distribution. + + Ronen Ness + ronenness@gmail.com + +*/ + +// FILE: world.js + +/* +* Physical world contains a grid of shapes you can efficiently check collision with +* Author: Ronen Ness, 2015 +*/ + + +// set namespace +var SSCD = SSCD || {}; + +// version identifier +SSCD.VERSION = 1.1; + +// a collision world. you create an instance of this class and add bodies to it to check collision. +// +// params is an optional dictionary with the following optional settings: +// grid_size: for better performance, the world is divided into a grid of world-chunks and when collision is checked we will +// only match objects from the same chunk(s) on grid. this param defines the grid size. default to 512. +SSCD.World = function (params) { + // set defaults + params = params || {}; + params.grid_size = params.grid_size || 512; + + // create grid and set params + this.__bodies = {}; + this.__params = params; + + // create the empty collision flags dictionary + this.__collision_tags = {}; + this.__next_coll_tag = 0; +}; + +// collision world prototype +SSCD.World.prototype = { + + // define a new collision tag + __create_collision_tag: function (name) + { + // if already exist throw exception + if (this.__collision_tags[name]) + { + throw new SSCD.IllegalActionError("Collision tag named '" + name + "' already exist!"); + } + + // set collision tag + this.__collision_tags[name] = 1 << this.__next_coll_tag++; + }, + + // all-tags flags + _ALL_TAGS_VAL: Number.MAX_SAFE_INTEGER || 4294967295, + + // clean-up world memory + cleanup: function() + { + // iterate over grid rows + var rows = Object.keys(this.__bodies); + for (var _i = 0; _i < rows.length; ++_i) + { + var i = rows[_i]; + + // iterate over grid columns in current row: + var columns = Object.keys(this.__bodies[i]); + for (var _j = 0; _j < columns.length; ++_j) + { + var j = columns[_j]; + + // if empty grid chunk delete it + if (this.__bodies[i][j].length === 0) + { + delete this.__bodies[i][j]; + } + } + + // if no more columns are left in current row delete the row itself + if (Object.keys(this.__bodies[i]).length === 0) + { + delete this.__bodies[i]; + } + } + }, + + // get the hash value of a list of collision tags or individual tag + // tags can either be a single string or a list of strings + __get_tags_value: function(tags) + { + // special case: undefined return all possible tags + if (tags === undefined) + { + return this._ALL_TAGS_VAL; + } + + // single tag: + if (typeof tags === "string") + { + return this.__collision_tag(tags); + } + + // else, assume a list + var ret = 0; + for (var i = 0; i < tags.length; ++i) + { + ret |= this.__collision_tag(tags[i]); + } + return ret; + }, + + // return the value of a single collision tag, define it if not exist + __collision_tag: function (name) + { + // if tag doesn't exist create it + if (this.__collision_tags[name] === undefined) + { + this.__create_collision_tag(name); + } + + // return collision tag + return this.__collision_tags[name]; + }, + + // get the grid range that this object touches + __get_grid_range: function(obj) + { + // get bounding box + var aabb = obj.get_aabb(); + + // calc all grid chunks this shape touches + var min_i = Math.floor((aabb.position.x) / this.__params.grid_size); + var min_j = Math.floor((aabb.position.y) / this.__params.grid_size); + var max_i = Math.floor((aabb.position.x + aabb.size.x) / this.__params.grid_size); + var max_j = Math.floor((aabb.position.y + aabb.size.y) / this.__params.grid_size); + + // return grid range + return {min_x: min_i, min_y: min_j, max_x: max_i, max_y: max_j}; + }, + + // add collision object to world + add: function (obj) + { + // if object already in world throw exception + if (obj.__world) + { + throw new SSCD.IllegalActionError("Object to add is already in a collision world!"); + } + + // get grid range + var grids = this.__get_grid_range(obj); + + // add shape to all grid parts + for (var i = grids.min_x; i <= grids.max_x; ++i) + { + for (var j = grids.min_y; j <= grids.max_y; ++j) + { + // make sure lists exist + this.__bodies[i] = this.__bodies[i] || {}; + this.__bodies[i][j] = this.__bodies[i][j] || []; + + // get current grid chunk + var curr_grid_chunk = this.__bodies[i][j]; + + // add object to grid chunk + curr_grid_chunk.push(obj); + + // add chunk to shape chunks list + obj.__grid_chunks.push(curr_grid_chunk); + } + } + + // set world and grid chunks boundaries + obj.__world = this; + obj.__grid_bounderies = grids; + + // return the newly added object + return obj; + }, + + // remove object from world + remove: function (obj) + { + // if object is not in this world throw exception + if (obj.__world !== this) + { + throw new SSCD.IllegalActionError("Object to remove is not in this collision world!"); + } + + // remove from all the grid chunks + for (var i = 0; i < obj.__grid_chunks.length; ++i) + { + // get current grid chunk + var grid_chunk = obj.__grid_chunks[i]; + + // remove object from grid + for (var j = 0; j < grid_chunk.length; ++j) + { + if (grid_chunk[j] === obj) + { + grid_chunk.splice(j, 1); + break; + } + } + } + + // clear shape world chunks and world pointer + obj.__grid_chunks = []; + obj.__world = null; + obj.__grid_bounderies = null; + }, + + // update object grid when it moves or resize etc. + // this function is used internally by the collision shapes. + __update_shape_grid: function(obj) + { + this.remove(obj); + this.add(obj); + }, + + // check collision and return first object found. + // obj: object to check collision with (vector or collision shape) + // collision_tags: optional single or multiple tags to check collision with + // return: first object collided with, or null if don't collide with anything + pick_object: function(obj, collision_tags) + { + var outlist = []; + if (this.test_collision(obj, collision_tags, outlist, 1)) + { + return outlist[0]; + } + return null; + }, + + // test collision with vector or object + // obj: object to check collision with, can be either Vector (for point collision) or any collision shape. + // collision_tags: optional string or list of strings of tags to match collision with. if undefined will accept all tags + // out_list: optional output list. if provided, will be filled with all objects collided with. note: collision is more efficient if not provided. + // ret_objs_count: if provided, will limit returned objects to given count. + // return true if collided with anything, false otherwise. + test_collision: function (obj, collision_tags, out_list, ret_objs_count) + { + // default collision flags + collision_tags = this.__get_tags_value(collision_tags); + + // handle vector + if (obj instanceof SSCD.Vector) + { + return this.__test_collision_point(obj, collision_tags, out_list); + } + // handle collision with shape + if (obj.is_shape) + { + return this.__test_collision_shape(obj, collision_tags, out_list); + } + }, + + // test collision for given point + // see test_collision comment for more info + __test_collision_point: function (vector, collision_tags_val, out_list, ret_objs_count) + { + // get current grid size + var grid_size = this.__params.grid_size; + + // get the grid chunk to test collision with + var i = Math.floor((vector.x) / grid_size); + var j = Math.floor((vector.y) / grid_size); + + // if grid chunk is not in use return empty list + if (this.__bodies[i] === undefined || this.__bodies[i][j] === undefined) + { + return false; + } + + // get current grid chunk + var grid_chunk = this.__bodies[i][j]; + + // iterate over all objects in current grid chunk and add them to render list + var found = 0; + for (var i = 0; i < grid_chunk.length; ++i) + { + // get current object to test + var curr_obj = grid_chunk[i]; + + // if collision tags don't match skip this object + if (!curr_obj.collision_tags_match(collision_tags_val)) + { + continue; + } + + // if collide with object: + if (curr_obj.test_collide_with(vector)) + { + // if got collision list to fill, add object and set return value to true + if (out_list) + { + found++; + out_list.push(curr_obj); + if (ret_objs_count && found >= ret_objs_count) + { + return true; + } + } + // if don't have collision list to fill simply return true + else + { + return true; + } + } + } + + // return if collided + // note: get here only if got list to fill or if no collision found + return found > 0; + }, + + // test collision with other shape + // see test_collision comment for more info + __test_collision_shape: function (obj, collision_tags_val, out_list, ret_objs_count) + { + // if shape is in this world, use its grid range from cache + if (obj.__world === this) + { + var grid = obj.__grid_bounderies; + } + // if not in world, generate grid range + else + { + var grid = this.__get_grid_range(obj); + } + + // for return value + var found = 0; + + // so we won't test same objects multiple times + var already_tests = {}; + + // iterate over grid this shape touches + for (var i = grid.min_x; i <= grid.max_x; ++i) + { + // skip empty rows + if (this.__bodies[i] === undefined) + { + continue; + } + + // iterate on current grid row + for (var j = grid.min_y; j <= grid.max_y; ++j) + { + var curr_grid_chunk = this.__bodies[i][j]; + + // skip empty grid chunks + if (curr_grid_chunk === undefined) + { + continue; + } + + // iterate over objects in grid chunk and check collision + for (var x = 0; x < curr_grid_chunk.length; ++x) + { + // get current object + var curr_obj = curr_grid_chunk[x]; + + // make sure object is not self + if (curr_obj === obj) + { + continue; + } + + // check if this object was already tested + if (already_tests[curr_obj.get_id()]) + { + continue; + } + already_tests[curr_obj.get_id()] = true; + + // if collision tags don't match skip this object + if (!curr_obj.collision_tags_match(collision_tags_val)) + { + continue; + } + + // if collide with object: + if (curr_obj.test_collide_with(obj)) + { + // if got collision list to fill, add object and set return value to true + if (out_list) + { + found++; + out_list.push(curr_obj); + if (ret_objs_count && found >= ret_objs_count) + { + return true; + } + } + // if don't have collision list to fill simply return true + else + { + return true; + } + } + } + + } + } + + // return if collided + // note: get here only if got list to fill or if no collision found + return found > 0; + }, + + // debug-render all the objects in world + // canvas: a 2d canvas object to render on. + // camera_pos: optional, vector that represent the current camera position is 2d space. + // show_grid: default to true, if set will render background grid that shows which grid chunks are currently active + // show_aabb: default to true, if set will render objects axis-aligned bounding boxes + // NOTE: this function will NOT clear canvas before rendering, if you render within a main loop its your responsibility. + render: function (canvas, camera_pos, show_grid, show_aabb) + { + // set default camera pos if doesn't exist + camera_pos = camera_pos || SSCD.Vector.ZERO; + + // set default show_grid and show_aabb + if (show_grid === undefined) {show_grid = true;} + if (show_aabb === undefined) {show_aabb = true;} + + // get ctx and reset previous transformations + var ctx = canvas.getContext('2d'); + ctx.setTransform(1, 0, 0, 1, 0, 0); + + // get current grid size + var grid_size = this.__params.grid_size; + + // get grid parts that are visible based on canvas size and camera position + var min_i = Math.floor((camera_pos.x) / grid_size); + var min_j = Math.floor((camera_pos.y) / grid_size); + var max_i = min_i + Math.ceil(canvas.width / grid_size); + var max_j = min_j + Math.ceil(canvas.height / grid_size); + + // a list of objects to render + var render_list = []; + + // iterate over grid + for (var i = min_i; i <= max_i; ++i) + { + + // go over grid row + for (var j = min_j; j <= max_j; ++j) + { + // get current grid chunk + var curr_grid_chunk = undefined; + if (this.__bodies[i]) + { + var curr_grid_chunk = this.__bodies[i][j]; + } + + // render current grid chunk + if (show_grid) + { + var position = new SSCD.Vector(i * grid_size, j * grid_size).sub_self(camera_pos); + ctx.beginPath(); + ctx.rect(position.x, position.y, grid_size-1, grid_size-1); + ctx.lineWidth = "1"; + if ((curr_grid_chunk === undefined) || (curr_grid_chunk.length === 0)) + { + ctx.strokeStyle = 'rgba(100, 100, 100, 0.255)'; + } + else + { + ctx.strokeStyle = 'rgba(255, 0, 0, 0.3)'; + } + ctx.stroke(); + } + + // if current grid chunk has no objects skip + if (curr_grid_chunk === undefined) + { + continue; + } + + // iterate over all objects in current grid chunk and add them to render list + for (var x = 0; x < curr_grid_chunk.length; ++x) + { + var curr_obj = curr_grid_chunk[x]; + if (render_list.indexOf (curr_obj) === -1) + { + render_list.push(curr_grid_chunk[x]); + } + } + } + } + + // now render all objects in render list + for (var i = 0; i < render_list.length; ++i) + { + render_list[i].render(ctx, camera_pos); + if (show_aabb) + { + render_list[i].render_aabb(ctx, camera_pos); + } + } + }, +}; + + +// for illegal action exception +SSCD.IllegalActionError = function (message) { + this.name = "Illegal Action"; + this.message = (message || ""); +} +SSCD.IllegalActionError.prototype = Error.prototype; + + + +// FILE: utils/math.js + +/* +* Add some useful Math functions +* Author: Ronen Ness, 2015 +*/ + +// set namespace +var SSCD = SSCD || {}; +SSCD.Math = {}; + +// Converts from degrees to radians. +SSCD.Math.to_radians = function (degrees) { + return degrees * Math.PI / 180; +}; + +// Converts from radians to degrees. +SSCD.Math.to_degrees = function (radians) { + return radians * 180 / Math.PI; +}; + +// get distance between vectors +SSCD.Math.distance = function (p1, p2) { + var dx = p2.x - p1.x, + dy = p2.y - p1.y; + return Math.sqrt(dx * dx + dy * dy); +}; + +// get distance without sqrt +SSCD.Math.dist2 = function (p1, p2) { + var dx = p2.x - p1.x, + dy = p2.y - p1.y; + return (dx * dx + dy * dy); +}; + +// angle between two vectors +SSCD.Math.angle = function (P1, P2) { + var deltaY = P2.y - P1.y, + deltaX = P2.x - P1.x; + + return Math.atan2(deltaY, deltaX) * 180 / Math.PI; +}; + +// distance from point to line +// p is point to check +// v and w are the two edges of the line segment +SSCD.Math.distance_to_line = function (p, v, w) { + + var l2 = SSCD.Math.dist2(v, w); + var t = ((p.x - v.x) * (w.x - v.x) + (p.y - v.y) * (w.y - v.y)) / l2; + if (t < 0) { + return SSCD.Math.distance(p, v); + } + if (t > 1) { + return SSCD.Math.distance(p, w); + } + return SSCD.Math.distance(p, { x: v.x + t * (w.x - v.x), + y: v.y + t * (w.y - v.y) }); +}; + +// Adapted from: http://stackoverflow.com/questions/563198/how-do-you-detect-where-two-line-segments-intersect/1968345#1968345 +// check if two lines intersect +SSCD.Math.line_intersects = function (p0, p1, p2, p3) { + + var s1_x, s1_y, s2_x, s2_y; + s1_x = p1.x - p0.x; + s1_y = p1.y - p0.y; + s2_x = p3.x - p2.x; + s2_y = p3.y - p2.y; + + var s, t; + s = (-s1_y * (p0.x - p2.x) + s1_x * (p0.y - p2.y)) / (-s2_x * s1_y + s1_x * s2_y); + t = (s2_x * (p0.y - p2.y) - s2_y * (p0.x - p2.x)) / (-s2_x * s1_y + s1_x * s2_y); + + if (s >= 0 && s <= 1 && t >= 0 && t <= 1) + { + // Collision detected + return 1; + } + + return 0; // No collision +}; + +// return if point is on given line +SSCD.Math.is_on_line = function (v, l1, l2) { + return SSCD.Math.distance_to_line(v, l1, l2) <= 5; +}; + +// FILE: utils/vector.js + +/* +* Define vector class +* Author: Ronen Ness, 2015 +*/ + +// set namespace +var SSCD = SSCD || {}; + +// a 2d vector +SSCD.Vector = function (x, y) { + this.x = x; + this.y = y; +}; + + +// set vector functions +SSCD.Vector.prototype = { + + // for debug and prints + get_name: function() + { + return "vector"; + }, + + // clone vector + clone: function () + { + return new SSCD.Vector(this.x, this.y); + }, + + // set value from another vector + set: function(vector) + { + this.x = vector.x; + this.y = vector.y; + }, + + // make negative (return without changing self) + negative: function() + { + return this.multiply_scalar(-1); + }, + + // make negative self (multiply by -1) + negative_self: function() + { + this.multiply_scalar_self(-1); + return this; + }, + + // get distance from another vector + distance_from: function (other) + { + return SSCD.Math.distance(this, other); + }, + + // get angle from another vector + angle_from: function (other) + { + return SSCD.Math.angle(this, other); + }, + + // move the position of this vector (same as add_self) + move: function(vector) + { + this.x += vector.x; + this.y += vector.y; + return this; + }, + + // normalize this vector + normalize_self: function() + { + var by = Math.sqrt(this.x * this.x + this.y * this.y); + this.x /= by; + this.y /= by; + return this; + }, + + // return normalized copy (don't change self) + normalize: function() + { + return this.clone().normalize_self(); + }, + + // add vector to self + add_self: function (other) + { + this.x += other.x; + this.y += other.y; + return this; + }, + + // sub vector from self + sub_self: function (other) + { + this.x -= other.x; + this.y -= other.y; + return this; + }, + + // divide vector from self + divide_self: function (other) + { + this.x /= other.x; + this.y /= other.y; + return this; + }, + + // multiple this vector with another + multiply_self: function (other) + { + this.x *= other.x; + this.y *= other.y; + return this; + }, + + // add scalar to self + add_scalar_self: function (val) + { + this.x += val; + this.y += val; + return this; + }, + + // substract scalar from self + sub_scalar_self: function (val) + { + this.x -= val; + this.y -= val; + return this; + }, + + // divide scalar from self + divide_scalar_self: function (val) + { + this.x /= val; + this.y /= val; + return this; + }, + + // multiply scalar from self + multiply_scalar_self: function (val) + { + this.x *= val; + this.y *= val; + return this; + }, + + // add to vector without changing self + add: function (other) + { + return this.clone().add_self(other); + }, + + // sub from vector without changing self + sub: function (other) + { + return this.clone().sub_self(other); + }, + + // multiply vector without changing self + multiply: function (other) + { + return this.clone().multiply_self(other); + }, + + // divide vector without changing self + divide: function (other) + { + return this.clone().divide_self(other); + }, + + // add scalar without changing self + add_scalar: function (val) + { + return this.clone().add_scalar_self(val); + }, + + // substract scalar without changing self + sub_scalar: function (val) + { + return this.clone().sub_scalar_self(val); + }, + + // multiply scalar without changing self + multiply_scalar: function (val) + { + return this.clone().multiply_scalar_self(val); + }, + + // divide scalar without changing self + divide_scalar: function (val) + { + return this.clone().divide_scalar_self(val); + }, + + // clamp vector values + clamp: function (min, max) + { + if (this.x < min) this.x = min; + if (this.y < min) this.y = min; + if (this.x > max) this.x = max; + if (this.y > max) this.y = max; + return this; + }, + + // get angle from vector + from_angle: function (angle) + { + this.x = Math.cos(angle); + this.y = Math.sin(angle); + return this; + }, + + // apply a function on x and y components on self + apply_self: function (func) + { + this.x = func(this.x); + this.y = func(this.y); + return this; + }, + + // apply a function on x and y components + apply: function (func) + { + return this.clone().apply_self(func); + }, + + // print debug + debug: function () + { + console.debug(this.x + ", " + this.y); + } +}; + +SSCD.Vector.ZERO = new SSCD.Vector(0, 0); +SSCD.Vector.ONE = new SSCD.Vector(1, 1); +SSCD.Vector.UP = new SSCD.Vector(0, -1); +SSCD.Vector.DOWN = new SSCD.Vector(0, 1); +SSCD.Vector.LEFT = new SSCD.Vector(-1, 0); +SSCD.Vector.RIGHT = new SSCD.Vector(1, 0); +SSCD.Vector.UP_LEFT = new SSCD.Vector(-1, -1); +SSCD.Vector.DOWN_LEFT = new SSCD.Vector(-1, 1); +SSCD.Vector.UP_RIGHT = new SSCD.Vector(1, -1); +SSCD.Vector.DOWN_RIGHT = new SSCD.Vector(1, 1) + +// FILE: utils/extend.js + +/* +* Provide simple inheritance (extend prototype) +* Author: Ronen Ness, 2015 +*/ + +// set namespace +var SSCD = SSCD || {}; + +// inherit base into child +// base / child must be object's prototype (eg SSCD.something.prototype) +// NOTE: don't use javascript built-ins so you won't mess up their prototypes. +SSCD.extend = function (base, child) +{ + + // copy all properties + for (var prop in base) + { + if (child[prop]) + continue; + + child[prop] = base[prop]; + } + + // create inits list (constructors) + // this creates a function namd .init() that will call all the __init__() functions in the inheritance chain by the order it was extended. + child.__inits = child.__inits || []; + + // add parent init function + if (base.__init__) + { + child.__inits.push(base.__init__); + } + + // set init function + child.init = function () + { + for (var i = 0; i < this.__inits.length; ++i) + { + this.__curr_init_func = this.__inits[i]; + this.__curr_init_func(); + } + delete this.__curr_init_func; + } +} + +// for not-implemented exceptions +SSCD.NotImplementedError = function (message) { + this.name = "NotImplementedError"; + this.message = (message || ""); +} +SSCD.NotImplementedError.prototype = Error.prototype; + +// FILE: utils/aabb.js + +/* +* Define axis-aligned-bounding-box class. +* Author: Ronen Ness, 2015 +*/ + +// set namespace +var SSCD = SSCD || {}; + +// Axis-aligned-bounding-box class +// position: top-left corner (vector) +// size: width and height (vector) +SSCD.AABB = function (position, size) { + this.position = position.clone(); + this.size = size.clone(); +}; + +// some aabb methods +SSCD.AABB.prototype = { + + // expand this bounding-box by other bounding box + expand: function (other) + { + // get new bounds + var min_x = Math.min(this.position.x, other.position.x); + var min_y = Math.min(this.position.y, other.position.y); + var max_x = Math.max(this.position.x + this.size.x, other.position.x + other.size.x); + var max_y = Math.max(this.position.y + this.size.y, other.position.y + other.size.y); + + // set them + this.position.x = min_x; + this.position.y = min_y; + this.size.x = max_x - min_x; + this.size.y = max_y - min_y; + }, + + // expand this bounding-box with vector + add_vector: function(vector) + { + // update position x + var push_pos_x = this.position.x - vector.x; + if (push_pos_x > 0) + { + this.position.x -= push_pos_x; + this.size.x += push_pos_x; + } + + // update position y + var push_pos_y = this.position.y - vector.y; + if (push_pos_y > 0) + { + this.position.y -= push_pos_y; + this.size.y += push_pos_y; + } + + // update size x + var push_size_x = vector.x - (this.position.x + this.size.x); + if (push_size_x > 0) + { + this.size.x += push_size_x; + } + + // update size y + var push_size_y = vector.y - (this.position.y + this.size.y); + if (push_size_y > 0) + { + this.size.y += push_size_y; + } + } + +}; + +// FILE: shapes/shape.js + +/* +* define the API of a collision shape. +* every type of shape should inherit from this class. +* Author: Ronen Ness, 2015 +*/ + + +// set namespace +var SSCD = SSCD || {}; + +SSCD.Shape = function () +{ +}; + +// shape api +SSCD.Shape.prototype = { + + // shape type (need to be overrided by children) + __type: "shape", + + // to detect if this object is a collision shape + is_shape: true, + + // optional data or object you can attach to shapes + __data: null, + + // to give unique id to every shape for internal usage + __next_id: 0, + + // default type flags: everything + __collision_tags: [], + __collision_tags_val: SSCD.World.prototype._ALL_TAGS_VAL, + + // init the general shape + __init__: function () + { + // create position and set default type + this.__position = new SSCD.Vector(); + + // for collision-world internal usage + this.__grid_chunks = []; // list with world chunks this shape is in + this.__world = null; // the parent collision world + this.__grid_bounderies = null; // grid bounderies + + // set unique ids + this.__id = SSCD.Shape.prototype.__next_id++; + }, + + // get shape unique id + get_id: function () + { + return this.__id; + }, + + // set the collision tags of this shape. + // for example, if you want this shape to be tagged as "wall", use: + // shape.set_collision_tags("walls"); + // + // you can also set multiple tags, like this: + // shape.set_collision_tags(["walls", "glass"]); + // + set_collision_tags: function (tags) + { + // can't set tags without world instance + if (this.__world === null) + { + throw new SSCD.IllegalActionError("Can't set tags for a shape that is not inside a collision world!"); + } + + // set the collision tag hash value + this.__collision_tags_val = this.__world.__get_tags_value(tags); + + // convert tags to array and store them + if (!tags instanceof Array) + { + tags = [tags]; + } + this.__collision_tags = tags; + + // if there's a hook to call when setting tags, call it + if (this.__update_tags_hook) + { + this.__update_tags_hook(); + } + + // return self + return this; + }, + + // optional hook to call after updating collision tags + __update_tags_hook: null, + + // return collision tag(s) (always return a list of strings) + get_collision_tags: function (tags) + { + return this.__collision_tags; + }, + + // check collision tags match + // tags can either be the tags numeric value, a single string, or a list of strings. + // note: if provided string or list of strings this shape must be inside a collision world. + collision_tags_match: function (tags) + { + // if need to convert tags to their numeric value + if (isNaN(tags)) + { + // if don't have collision world raise error + if (this.__world === null) + { + throw new SSCD.IllegalActionError("If you provide tags as string(s) the shape must be inside a collision world to convert them!"); + } + tags = this.__world.__get_tags_value(tags); + } + + // check if tags match + return (this.__collision_tags_val & tags) !== 0; + }, + + // check collision with other object (vector, other shape, etc..) + test_collide_with: function (obj) + { + return SSCD.CollisionManager.test_collision(this, obj); + }, + + // repeal an object from this object. + // this means, in simple words, we push the other object outside to prevent penetration. + // this works in a very simply way - it iterates and push the penetrating object outside from center until its no longer collided. + // obj: object or vector to repeal (must have move() function). + // force: force factor, the bigger this is the stronger / faster the repealing will be. default to 1. + // iterations: max iterations of repeal-and-test-again routines. default to 1. + // NOTE: this function assume there's collision on start, meaning first iteration of repeal will ALWAYS happen. + // return: total movement due to repeling (vector) + repel: function(obj, force, iterations) + { + // set defaults + force = force || 1; + iterations = iterations || 1; + + // get direction vector + var push_vector = this.get_repel_direction(obj).multiply_scalar_self(force); + + // for return value + var ret = SSCD.Vector.ZERO.clone(); + + // now do the repeling + var collide = true; + while (collide && iterations > 0) + { + // decreate iterations count + iterations--; + + // do pushing + obj.move(push_vector); + ret.add_self(push_vector); + + // check if still colliding + collide = this.test_collide_with(obj); + } + + // return total pushed + return ret; + }, + + // get repel direction between this shape and another shape / vector + get_repel_direction: function(obj) + { + // get the center of this object + var center = this.get_abs_center(); + + // get center of other object / vector + var other_center; + if (obj instanceof SSCD.Vector) + { + var other_center = obj; + } + else + { + other_center = obj.get_abs_center(); + } + + // return repel direction vector + return other_center.sub(center).normalize_self(); + }, + + // return shape fill color for debug rendering + __get_render_fill_color: function (opacity) + { + // if have override fill color use it: + if (this.__override_fill_color) + { + return this.__override_fill_color; + } + + // else, return color based on tag + return this.__collision_tags_to_color(this.__collision_tags_val, opacity); + }, + + // return shape stroke color for debug rendering + __get_render_stroke_color: function (opacity) + { + // if have override stroke color use it: + if (this.__override_stroke_color) + { + return this.__override_stroke_color; + } + + // else, return color based on tag + return this.__collision_tags_to_color(this.__collision_tags_val, opacity); + }, + + // set colors to override the debug rendering colors + // accept any html5 color value (eg "rgba(r,g,b,a)" or "white") + // set nulls to use default colors (based on shape tags) + set_debug_render_colors: function(fill_color, stroke_color) + { + this.__override_fill_color = fill_color; + this.__override_stroke_color = stroke_color; + }, + + // default override colors is null - don't override debug colors + __override_fill_color: null, + __override_stroke_color: null, + + // return color based on collision tags + __collision_tags_to_color: function (tags, opacity) + { + var r = Math.round(Math.abs(Math.sin(tags)) * 255); + var g = Math.round(Math.abs(Math.cos(tags)) * 255); + var b = Math.round(r ^ g); + return "rgba(" + r + "," + g + "," + b + "," + opacity + ")"; + }, + + // attach data/object to this shape + set_data: function (obj) + { + this.__data = obj; + return this; + }, + + // get attached data / object of this shape + get_data: function () + { + return this.__data; + }, + + // return shape type + get_name: function () + { + return this.__type; + }, + + // render shape axis-aligned-bounding-box + render_aabb: function(ctx, camera_pos) + { + var box = this.get_aabb(); + + // draw the rect + ctx.beginPath(); + ctx.rect(box.position.x - camera_pos.x, box.position.y - camera_pos.y, box.size.x, box.size.y); + + // draw stroke + ctx.lineWidth = "1"; + ctx.strokeStyle = 'rgba(50, 175, 45, 0.5)'; + ctx.stroke(); + }, + + // set position + set_position: function (vector) + { + this.__position.x = vector.x; + this.__position.y = vector.y; + this.__update_position(); + return this; + }, + + // get position (return vector) + get_position: function () + { + return this.__position.clone(); + }, + + // move the shape + move: function (vector) + { + this.set_position(this.__position.add(vector)); + return this; + }, + + // should be called whenever position changes + __update_position: function () + { + // call position-change hook + if (this.__update_position_hook) + { + this.__update_position_hook(); + } + + // remove bounding box cache + if (this.__aabb) + { + this.__update_aabb_pos(); + } + + // update in world + this.__update_parent_world(); + }, + + // called to update axis-aligned-bounding-box position + // this function called AFTER the position update, meaning new position applied + // this function only called if have aabb in cache. + __update_aabb_pos: function() + { + this.__aabb.position = this.__position; + }, + + // return the absolute center of the shape + get_abs_center: function() + { + var aabb = this.get_aabb(); + return aabb.position.add(aabb.size.multiply_scalar(0.5)); + }, + + // reset bounding box + reset_aabb: function() + { + this.__aabb = undefined; + }, + + // update this shape in parent world (call this when shape change position or change and need to notify world) + __update_parent_world: function () + { + if (this.__world) + { + this.__world.__update_shape_grid(this); + } + }, + + // optional hook you can override that will be called whenever shape position changes. + __update_position_hook: null, + + // render (for debug purposes) + // camera_pos is optional 2d camera position + render: function (ctx, camera_pos) + { + throw new SSCD.NotImplementedError(); + }, + + // build the shape's axis-aligned bounding box + build_aabb: function () + { + throw new SSCD.NotImplementedError(); + }, + + // return axis-aligned-bounding-box + get_aabb: function () + { + this.__aabb = this.__aabb || this.build_aabb(); + return this.__aabb; + }, + +}; + +// FILE: shapes/circle.js + +/* +* A circle collision shape +* Author: Ronen Ness, 2015 +*/ + + +// set namespace +var SSCD = SSCD || {}; + +// define the circle shape +// position - starting position (vector) +// radius - circle radius (integer) +SSCD.Circle = function (position, radius) +{ + // call init chain + this.init(); + + // set radius and size + this.__radius = radius; + this.__size = new SSCD.Vector(radius, radius).multiply_scalar_self(2); + + // set starting position + this.set_position(position); +}; + +// set circle methods +SSCD.Circle.prototype = { + + __type: "circle", + + // render (for debug purposes) + render: function (ctx, camera_pos) + { + // apply camera on position + var position = this.__position.sub(camera_pos); + + // draw the circle + ctx.beginPath(); + ctx.arc(position.x, position.y, this.__radius, 0, 2 * Math.PI, false); + + // draw stroke + ctx.lineWidth = "7"; + ctx.strokeStyle = this.__get_render_stroke_color(0.75); + ctx.stroke(); + + // draw fill + ctx.fillStyle = this.__get_render_fill_color(0.35); + ctx.fill(); + }, + + // return circle radius + get_radius: function () + { + return this.__radius; + }, + + // called to update axis-aligned-bounding-box position + __update_aabb_pos: function() + { + this.__aabb.position = this.__position.sub_scalar(this.__radius) + }, + + // return axis-aligned-bounding-box + build_aabb: function () + { + return new SSCD.AABB(this.__position.sub_scalar(this.__radius), this.__size); + }, + + // return the absolute center of the shape + get_abs_center: function() + { + return this.__position.clone(); + }, + +}; + +// inherit from basic shape class. +// this will fill the missing functions from parent, but will not replace functions existing in child. +SSCD.extend(SSCD.Shape.prototype, SSCD.Circle.prototype); + +// FILE: shapes/rectangle.js + +/* +* rectangle collision shape +* Author: Ronen Ness, 2015 +*/ + + +// set namespace +var SSCD = SSCD || {}; + +// define the rectangle shape +// position - starting position (vector) +// size - rectangle size (vector) +SSCD.Rectangle = function (position, size) +{ + // call init chain + this.init(); + + // set radius and size + this.__size = size; + + // set starting position + this.set_position(position); +}; + +// set Rectangle methods +SSCD.Rectangle.prototype = { + + __type: "rectangle", + + // render (for debug purposes) + render: function (ctx, camera_pos) + { + // apply camera on position + var position = this.__position.sub(camera_pos); + + // draw the rect + ctx.beginPath(); + ctx.rect(position.x, position.y, this.__size.x, this.__size.y); + + // draw stroke + ctx.lineWidth = "7"; + ctx.strokeStyle = this.__get_render_stroke_color(0.75); + ctx.stroke(); + + // draw fill + ctx.fillStyle = this.__get_render_fill_color(0.35); + ctx.fill(); + }, + + // return rectangle size + get_size: function () + { + return this.__size.clone(); + }, + + // return axis-aligned-bounding-box + build_aabb: function () + { + return new SSCD.AABB(this.__position, this.__size); + }, + + // return absolute top-left corner + get_top_left: function() + { + this.__top_left_c = this.__top_left_c || this.__position.clone(); + return this.__top_left_c; + }, + + // return absolute bottom-left corner + get_bottom_left: function() + { + this.__bottom_left_c = this.__bottom_left_c || this.__position.add(new SSCD.Vector(0, this.__size.y)); + return this.__bottom_left_c; + }, + + // return absolute top-right corner + get_top_right: function() + { + this.__top_right_c = this.__top_right_c || this.__position.add(new SSCD.Vector(this.__size.x, 0)); + return this.__top_right_c; + }, + + // return absolute bottom-right corner + get_bottom_right: function() + { + this.__bottom_right_c = this.__bottom_right_c || this.__position.add(new SSCD.Vector(this.__size.x, this.__size.y)); + return this.__bottom_right_c; + }, + + // return absolute center + get_abs_center: function() + { + this.__abs_center_c = this.__abs_center_c || this.__position.add(this.__size.divide_scalar(2)); + return this.__abs_center_c; + }, + + // on position change + __update_position_hook: function() + { + // clear corner cache + this.__top_left_c = undefined; + this.__top_right_c = undefined; + this.__bottom_left_c = undefined; + this.__bottom_right_c = undefined; + this.__abs_center_c = undefined; + }, + +}; + +// inherit from basic shape class. +// this will fill the missing functions from parent, but will not replace functions existing in child. +SSCD.extend(SSCD.Shape.prototype, SSCD.Rectangle.prototype); + +// FILE: shapes/line.js + +/* +* A line collision shape +* Author: Ronen Ness, 2015 +*/ + + +// set namespace +var SSCD = SSCD || {}; + +// define the line shape +// source - starting position (vector) +// dest - destination point (vector) +// output line will be from source to dest, and when you move it you will actually move the source position. +SSCD.Line = function (source, dest) +{ + // call init chain + this.init(); + + // set dest position + this.__dest = dest; + + // set starting position + this.set_position(source); +}; + +// set line methods +SSCD.Line.prototype = { + + __type: "line", + + // render (for debug purposes) + render: function (ctx, camera_pos) + { + // apply camera on position + var position = this.__position.sub(camera_pos); + + // draw the line + ctx.beginPath(); + ctx.moveTo(this.__position.x, this.__position.y); + var dest = this.__position.add(this.__dest); + ctx.lineTo(dest.x, dest.y); + + // draw stroke + ctx.lineWidth = "7"; + ctx.strokeStyle = this.__get_render_stroke_color(0.75); + ctx.stroke(); + + }, + + // return axis-aligned-bounding-box + build_aabb: function () + { + var pos = new SSCD.Vector(0, 0); + pos.x = this.__dest.x > 0 ? this.__position.x : this.__position.x - this.__dest.x; + pos.y = this.__dest.y > 0 ? this.__position.y : this.__position.y - this.__dest.y; + var size = this.__dest.apply(Math.abs); + return new SSCD.AABB(pos, size); + }, + + // return absolute first point + get_p1: function() + { + this.__p1_c = this.__p1_c || this.__position.clone(); + return this.__p1_c; + }, + + // return absolute second point + get_p2: function() + { + this.__p2_c = this.__p2_c || this.__position.add(this.__dest); + return this.__p2_c; + }, + + // on position change + __update_position_hook: function() + { + // clear points cache + this.__p1_c = undefined; + this.__p2_c = undefined; + }, + +}; + +// inherit from basic shape class. +// this will fill the missing functions from parent, but will not replace functions existing in child. +SSCD.extend(SSCD.Shape.prototype, SSCD.Line.prototype); + +// FILE: shapes/lines_strip.js + +/* +* A strip-of-lines collision shape +* Author: Ronen Ness, 2015 +*/ + + +// set namespace +var SSCD = SSCD || {}; + +// define the line shape +// position - starting position (vector) +// points - list of vectors that will make the lines. +// closed - default to false. if true, will close the shape. +SSCD.LineStrip = function (position, points, closed) +{ + // call init chain + this.init(); + + // set points + this.__points = points; + + // if not enough points assert + if (points.length <= 1) + { + throw new SSCD.IllegalActionError("Not enough vectors for LineStrip (got to have at least two vectors)"); + } + + // close shape + if (closed) + { + this.__points.push(this.__points[0]); + } + + // set starting position + this.set_position(position); +}; + +// set line methods +SSCD.LineStrip.prototype = { + + __type: "line-strip", + + // render (for debug purposes) + render: function (ctx, camera_pos) + { + // apply camera on position + var position = this.__position.sub(camera_pos); + + // draw the lines + ctx.beginPath(); + for (var i = 0; i < this.__points.length-1; ++i) + { + var from = this.__position.add(this.__points[i]); + var to = this.__position.add(this.__points[i+1]); + ctx.moveTo(from.x, from.y); + ctx.lineTo(to.x, to.y); + } + + // add last point + ctx.moveTo(to.x, to.y); + var to = this.__position.add(this.__points[this.__points.length-1]); + ctx.lineTo(to.x, to.y); + + // draw stroke + ctx.lineWidth = "7"; + ctx.strokeStyle = this.__get_render_stroke_color(0.75); + ctx.stroke(); + + }, + + // return line list with absolute positions + get_abs_lines: function() + { + // if got lines in cache return it + if (this.__abs_lines_c) + { + return this.__abs_lines_c; + } + + // create list of lines + var points = this.get_abs_points(); + var ret = []; + for (var i = 0; i < points.length-1; i++) + { + ret.push([points[i], points[i+1]]); + } + + // add to cache and return + this.__abs_lines_c = ret; + return ret; + }, + + // return points with absolute position + get_abs_points: function() + { + // if got points in cache return it + if (this.__abs_points_c) + { + return this.__abs_points_c; + } + + // convert points + var ret = []; + for (var i = 0; i < this.__points.length; i++) + { + ret.push(this.__points[i].add(this.__position)); + } + + // add to cache and return + this.__abs_points_c = ret; + return ret; + }, + + // on position change + __update_position_hook: function() + { + // clear points and lines cache + this.__abs_points_c = undefined; + this.__abs_lines_c = undefined; + }, + + // called to update axis-aligned-bounding-box position + __update_aabb_pos: function() + { + this.__aabb.position.set(this.__aabb_offset_c.add(this.__position)); + }, + + // return axis-aligned-bounding-box + build_aabb: function () + { + var ret = new SSCD.AABB( SSCD.Vector.ZERO, SSCD.Vector.ZERO); + for (var i = 0; i < this.__points.length; ++i) + { + ret.add_vector(this.__points[i]); + } + this.__aabb_offset_c = ret.position.clone(); + ret.position.add_self(this.__position); + return ret; + }, + +}; + +// inherit from basic shape class. +// this will fill the missing functions from parent, but will not replace functions existing in child. +SSCD.extend(SSCD.Shape.prototype, SSCD.LineStrip.prototype); + +// FILE: shapes/composite_shape.js + +/* +* a special shape made from multiple shapes combined together +* Author: Ronen Ness, 2015 +*/ + + +// set namespace +var SSCD = SSCD || {}; + +// create a composite shape +// position - optional starting position (vector) +// objects - optional list of collision objects to start with +SSCD.CompositeShape = function (position, objects) +{ + // call init chain + this.init(); + + // create empty list of shapes + this.__shapes = []; + + // default position + position = position || SSCD.Vector.ZERO; + this.set_position(position); + + // add objects if provided + if (objects) + { + for (var i = 0; i < objects.length; ++i) + { + this.add(objects[i]); + } + } +}; + +// set Rectangle methods +SSCD.CompositeShape.prototype = { + + __type: "composite-shape", + + // render (for debug purposes) + render: function (ctx, camera_pos) + { + // first render all shapes + for (var i = 0; i < this.__shapes.length; ++i) + { + this.__shapes[i].shape.render(ctx, camera_pos); + } + }, + + // repeal an object from this object. + // here we iterate over sub-object and repeal only from the ones we collide with + repel: function(obj, force, iterations) + { + var ret = SSCD.Vector.ZERO.clone(); + for (var i = 0; i < this.__shapes.length; ++i) + { + var shape = this.__shapes[i].shape; + if (shape.test_collide_with(obj)) + { + ret.add_self(shape.repel(obj, force, iterations)); + } + } + return ret; + }, + + // set colors to override the debug rendering colors + set_debug_render_colors: function(fill_color, stroke_color) + { + this.__override_fill_color = fill_color; + this.__override_stroke_color = stroke_color; + for (var i = 0; i < this.__shapes.length; ++i) + { + this.__shapes[i].shape.set_debug_render_colors(fill_color, stroke_color); + } + }, + + // get shapes list + get_shapes: function() + { + // if already got shapes list in cache return it + if (this.__shapes_list_c) + { + return this.__shapes_list_c; + } + + // create shapes list + var ret = []; + for (var i = 0; i < this.__shapes.length; ++i) + { + ret.push(this.__shapes[i].shape); + } + + // add to cache and return + this.__shapes_list_c = ret; + return ret; + }, + + // return axis-aligned-bounding-box + build_aabb: function () + { + // if no shapes return zero aabb + if (this.__shapes.length === 0) + { + this.__aabb_pos_offset_c = SSCD.Vector.ZERO; + return new SSCD.AABB(SSCD.Vector.ZERO, SSCD.Vector.ZERO); + } + + // return combined aabb + var ret = null; + for (var i = 0; i < this.__shapes.length; ++i) + { + var curr_aabb = this.__shapes[i].shape.get_aabb(); + if (ret) + { + ret.expand(curr_aabb); + } + else + { + ret = curr_aabb; + } + } + + // store diff between position and bounding-box position, for faster aabb movement + this.__aabb_pos_offset_c = ret.position.sub(this.__position); + + // return bounding-box + return ret; + }, + + // called to update axis-aligned-bounding-box position + __update_aabb_pos: function() + { + this.__aabb.position = this.__position.add(this.__aabb_pos_offset_c); + }, + + // add shape to the composite shape + // shape is shape to add + add: function (shape) + { + // make sure shape don't have a collision world + if (shape.__world) + { + throw new SSCD.IllegalActionError("Can't add shape with collision world to a composite shape!"); + } + + // store shape offset + var offset = shape.__position; + + // reset shapes list cache + this.__shapes_list_c = undefined; + + // add shape to list of shapes and fix position + this.__shapes.push({shape: shape, offset: offset.clone()}); + shape.set_position(this.__position.add(offset)); + + // reset bounding-box and notify collision world about the change + this.reset_aabb(); + this.__update_parent_world(); + + // set shape tags to be the composite shape tags + shape.__collision_tags_val = this.__collision_tags_val; + shape.__collision_tags = this.__collision_tags; + + // set shape debug colors + shape.__override_fill_color = this.__override_fill_color; + shape.__override_stroke_color = this.__override_stroke_color; + + // return the newly added shape + return shape; + }, + + // hook to call when update tags - update all child objects with new tags + __update_tags_hook: function() + { + // update all shapes about the new tags + for (var i = 0; i < this.__shapes; ++i) + { + var shape = this.__shapes[i].shape; + shape.__collision_tags_val = this.__collision_tags_val; + shape.__collision_tags = this.__collision_tags; + } + }, + + // remove a shape + remove: function (shape) + { + this.__shapes_list_c = undefined; + for (var i = 0; i < this.__shapes.length; ++i) + { + if (this.__shapes[i].shape === shape) + { + this.__shapes.splice(i, 1); + this.__update_parent_world(); + return; + } + } + + throw new SSCD.IllegalActionError("Shape to remove is not in composite shape!"); + }, + + // on position change - update all shapes + __update_position_hook: function () + { + for (var i = 0; i < this.__shapes.length; ++i) + { + this.__shapes[i].shape.set_position(this.__position.add(this.__shapes[i].offset)); + } + } +}; + +// inherit from basic shape class. +// this will fill the missing functions from parent, but will not replace functions existing in child. +SSCD.extend(SSCD.Shape.prototype, SSCD.CompositeShape.prototype); + +// FILE: shapes/shapes_collider.js + +/* +* here we define all the collision-detection functions for all possible shape combinations +* Author: Ronen Ness, 2015 +*/ + + +// set namespace +var SSCD = SSCD || {}; + +SSCD.CollisionManager = { + + // test collision between two objects, a and b, where they can be vectors or any valid collision shape. + test_collision: function (a, b) + { + // vector-vector collision + if (a instanceof SSCD.Vector && b instanceof SSCD.Vector) + { + return this._test_collision_vector_vector(a, b); + } + + // composite shape collision + if (a instanceof SSCD.CompositeShape) + { + return this._test_collision_composite_shape(a, b); + } + if (b instanceof SSCD.CompositeShape) + { + return this._test_collision_composite_shape(b, a); + } + + // circle-vector collision + if (a instanceof SSCD.Vector && b instanceof SSCD.Circle) + { + return this._test_collision_circle_vector(b, a); + } + if (a instanceof SSCD.Circle && b instanceof SSCD.Vector) + { + return this._test_collision_circle_vector(a, b); + } + + // circle-circle collision + if (a instanceof SSCD.Circle && b instanceof SSCD.Circle) + { + return this._test_collision_circle_circle(b, a); + } + + // circle-rectangle collision + if (a instanceof SSCD.Circle && b instanceof SSCD.Rectangle) + { + return this._test_collision_circle_rect(a, b); + } + if (a instanceof SSCD.Rectangle && b instanceof SSCD.Circle) + { + return this._test_collision_circle_rect(b, a); + } + + // circle-line collision + if (a instanceof SSCD.Circle && b instanceof SSCD.Line) + { + return this._test_collision_circle_line(a, b); + } + if (a instanceof SSCD.Line && b instanceof SSCD.Circle) + { + return this._test_collision_circle_line(b, a); + } + + // linestrip-line collision + if (a instanceof SSCD.LineStrip && b instanceof SSCD.Line) + { + return this._test_collision_linestrip_line(a, b); + } + if (a instanceof SSCD.Line && b instanceof SSCD.LineStrip) + { + return this._test_collision_linestrip_line(b, a); + } + + // circle-linestrip collision + if (a instanceof SSCD.Circle && b instanceof SSCD.LineStrip) + { + return this._test_collision_circle_linestrip(a, b); + } + if (a instanceof SSCD.LineStrip && b instanceof SSCD.Circle) + { + return this._test_collision_circle_linestrip(b, a); + } + + // rect-vector collision + if (a instanceof SSCD.Vector && b instanceof SSCD.Rectangle) + { + return this._test_collision_rect_vector(b, a); + } + if (a instanceof SSCD.Rectangle && b instanceof SSCD.Vector) + { + return this._test_collision_rect_vector(a, b); + } + + // rect-rect collision + if (a instanceof SSCD.Rectangle && b instanceof SSCD.Rectangle) + { + return this._test_collision_rect_rect(b, a); + } + + // line-strip with line-strip collision + if (a instanceof SSCD.LineStrip && b instanceof SSCD.LineStrip) + { + return this._test_collision_linestrip_linestrip(a, b) + } + + // rect-line collision + if (a instanceof SSCD.Line && b instanceof SSCD.Rectangle) + { + return this._test_collision_rect_line(b, a); + } + if (a instanceof SSCD.Rectangle && b instanceof SSCD.Line) + { + return this._test_collision_rect_line(a, b); + } + + // rect-linestrip collision + if (a instanceof SSCD.LineStrip && b instanceof SSCD.Rectangle) + { + return this._test_collision_rect_linestrip(b, a); + } + if (a instanceof SSCD.Rectangle && b instanceof SSCD.LineStrip) + { + return this._test_collision_rect_linestrip(a, b); + } + + // line-line collision + if (a instanceof SSCD.Line && b instanceof SSCD.Line) + { + return this._test_collision_line_line(a, b); + } + + // vector-line collision + if (a instanceof SSCD.Line && b instanceof SSCD.Vector) + { + return this._test_collision_vector_line(b, a); + } + if (a instanceof SSCD.Vector && b instanceof SSCD.Line) + { + return this._test_collision_vector_line(a, b); + } + + // vector-linestrip collision + if (a instanceof SSCD.LineStrip && b instanceof SSCD.Vector) + { + return this._test_collision_vector_linestrip(b, a); + } + if (a instanceof SSCD.Vector && b instanceof SSCD.LineStrip) + { + return this._test_collision_vector_linestrip(a, b); + } + + // unsupported shapes! + throw new SSCD.UnsupportedShapes(a, b); + }, + + // test collision between two vectors + _test_collision_vector_vector: function(a, b) + { + return (a.x === b.x) && (a.y === b.y); + }, + + // test collision between circle and vector + _test_collision_circle_vector: function (circle, vector) + { + return SSCD.Math.distance(circle.__position, vector) <= circle.__radius; + }, + + // test collision between circle and another circle + _test_collision_circle_circle: function (a, b) + { + return SSCD.Math.distance(a.__position, b.__position) <= a.__radius + b.__radius; + }, + + // test collision between rectangle and vector + _test_collision_rect_vector: function (rect, vector) + { + return (vector.x >= rect.__position.x) && (vector.y >= rect.__position.y) && + (vector.x <= rect.__position.x + rect.__size.x) && + (vector.y <= rect.__position.y + rect.__size.y); + }, + + // test collision vector with line + _test_collision_vector_line: function (v, line) + { + return SSCD.Math.is_on_line(v, line.get_p1(), line.get_p2()); + }, + + // test collision vector with linestrip + _test_collision_vector_linestrip: function(v, linestrip) + { + var lines = linestrip.get_abs_lines(); + for (var i = 0; i < lines.length; ++i) + { + if (SSCD.Math.is_on_line(v, lines[i][0], lines[i][1])) + { + return true; + } + } + return false; + }, + + // test collision between circle and line + _test_collision_circle_line: function (circle, line) + { + return SSCD.Math.distance_to_line(circle.__position, line.get_p1(), line.get_p2()) <= circle.__radius; + }, + + // test collision between circle and line-strip + _test_collision_circle_linestrip: function (circle, linestrip) + { + var lines = linestrip.get_abs_lines(); + for (var i = 0; i < lines.length; ++i) + { + if (SSCD.Math.distance_to_line(circle.__position, lines[i][0], lines[i][1]) <= circle.__radius) + { + return true; + } + } + return false; + }, + + // test collision between linestrip and a single line + _test_collision_linestrip_line: function(linestrip, line) + { + var lines = linestrip.get_abs_lines(); + var p1 = line.get_p1(), p2 = line.get_p2(); + for (var i = 0; i < lines.length; ++i) + { + if (SSCD.Math.line_intersects(p1, p2, lines[i][0], lines[i][1])) + { + return true; + } + } + return false; + }, + + // check collision line with line + _test_collision_line_line: function (a, b) + { + return SSCD.Math.line_intersects(a.get_p1(), a.get_p2(), + b.get_p1(), b.get_p2()); + }, + + // check collision between rectangle and line + _test_collision_rect_line: function (rect, line) + { + // get the line's two points + var p1 = line.get_p1(); + var p2 = line.get_p2(); + + // first check if one of the line points is contained inside the rectangle + if (SSCD.CollisionManager._test_collision_rect_vector(rect, p1) || + SSCD.CollisionManager._test_collision_rect_vector(rect, p2)) + { + return true; + } + + // now check collision between line and rect lines + + // left side + var r1 = rect.get_top_left(); + var r2 = rect.get_bottom_left(); + if (SSCD.Math.line_intersects(p1, p2, r1, r2)) + { + return true; + } + + // right side + var r3 = rect.get_top_right(); + var r4 = rect.get_bottom_right(); + if (SSCD.Math.line_intersects(p1, p2, r3, r4)) + { + return true; + } + + // top side + if (SSCD.Math.line_intersects(p1, p2, r1, r3)) + { + return true; + } + + // bottom side + if (SSCD.Math.line_intersects(p1, p2, r2, r4)) + { + return true; + } + + // no collision + return false; + }, + + // test collision between rectagnle and linesstrip + _test_collision_rect_linestrip: function(rect, linesstrip) + { + // first check all points + var points = linesstrip.get_abs_points(); + for (var i = 0; i < points.length; ++i) + { + if (this._test_collision_rect_vector(rect, points[i])) + { + return true; + } + } + + // now check intersection with rectangle sides + + var r1 = rect.get_top_left(); + var r2 = rect.get_bottom_left(); + var r3 = rect.get_top_right(); + var r4 = rect.get_bottom_right(); + + var lines = linesstrip.get_abs_lines(); + for (var i = 0; i < lines.length; ++i) + { + var p1 = lines[i][0]; + var p2 = lines[i][1]; + + // left side + if (SSCD.Math.line_intersects(p1, p2, r1, r2)) + { + return true; + } + + // right side + if (SSCD.Math.line_intersects(p1, p2, r3, r4)) + { + return true; + } + + // top side + if (SSCD.Math.line_intersects(p1, p2, r1, r3)) + { + return true; + } + + // bottom side + if (SSCD.Math.line_intersects(p1, p2, r2, r4)) + { + return true; + } + } + + // no collision + return false; + }, + + // test collision between two linestrips + _test_collision_linestrip_linestrip: function (strip1, strip2) + { + var lines1 = strip1.get_abs_lines(); + var lines2 = strip2.get_abs_lines(); + for (var i = 0; i < lines1.length; ++i) + { + for (var j = 0; j < lines2.length; ++j) + { + if (SSCD.Math.line_intersects( lines1[i][0], lines1[i][1], + lines2[j][0], lines2[j][1])) + { + return true; + } + } + } + return false; + }, + + // test composite shape with any other shape + _test_collision_composite_shape: function(composite, other) + { + // get all shapes in composite shape + var comp_shapes = composite.get_shapes(); + + // special case: other shape is a composite shape as well + if (other instanceof SSCD.CompositeShape) + { + var other_shapes = other.get_shapes(); + for (var i = 0; i < comp_shapes.length; ++i) + { + for (var j = 0; j < other_shapes.length; ++j) + { + if (SSCD.CollisionManager.test_collision(comp_shapes[i], other_shapes[j])) + { + return true; + } + } + } + } + // normal case - other shape is a normal shape + else + { + for (var i = 0; i < comp_shapes.length; ++i) + { + if (SSCD.CollisionManager.test_collision(comp_shapes[i], other)) + { + return true; + } + } + } + + // no collision found + return false; + + }, + + // test collision between circle and rectangle + _test_collision_circle_rect: function (circle, rect) + { + // get circle center + var circle_pos = circle.__position; + + // first check if circle center is inside the rectangle - easy case + var collide = SSCD.CollisionManager._test_collision_rect_vector(rect, circle_pos); + if (collide) + { + return true; + } + + // get rectangle center + var rect_center = rect.get_abs_center(); + + // now check other simple case - collision between rect center and circle + var collide = SSCD.CollisionManager._test_collision_circle_vector(circle, rect_center); + if (collide) + { + return true; + } + + var r1 = rect.get_top_left(); + var r2 = rect.get_bottom_left(); + var r3 = rect.get_top_right(); + var r4 = rect.get_bottom_right(); + + // create a list of lines to check (in the rectangle) based on circle position to rect center + var lines = []; + if (rect_center.x > circle_pos.x) + { + lines.push([rect.get_top_left(), rect.get_bottom_left()]); + } + else + { + lines.push([rect.get_top_right(), rect.get_bottom_right()]); + } + if (rect_center.y > circle_pos.y) + { + lines.push([rect.get_top_left(), rect.get_top_right()]); + } + else + { + lines.push([rect.get_bottom_left(), rect.get_bottom_right()]); + } + + // now check intersection between circle and each of the rectangle lines + for (var i = 0; i < lines.length; ++i) + { + var dist_to_line = SSCD.Math.distance_to_line(circle_pos, lines[i][0], lines[i][1]); + if (dist_to_line <= circle.__radius) + { + return true; + } + } + + // no collision.. + return false; + }, + + // test collision between circle and rectangle + _test_collision_rect_rect: function (a, b) + { + var r1 = { left: a.__position.x, right: a.__position.x + a.__size.x, + top: a.__position.y, bottom: a.__position.y + a.__size.y}; + var r2 = { left: b.__position.x, right: b.__position.x + b.__size.x, + top: b.__position.y, bottom: b.__position.y + b.__size.y}; + return !(r2.left > r1.right || + r2.right < r1.left || + r2.top > r1.bottom || + r2.bottom < r1.top); + }, +}; + +// exception when trying to check collision on shapes not supported +SSCD.UnsupportedShapes = function (a, b) { + this.name = "Unsupported Shapes"; + this.message = "Unsupported shapes collision test! '" + a.get_name() + "' <-> '" + b.get_name() + "'."; +} +SSCD.UnsupportedShapes.prototype = Error.prototype; + + + diff --git a/dist/sscd.1.1.min.js b/dist/sscd.1.1.min.js new file mode 100644 index 0000000..7ca21b0 --- /dev/null +++ b/dist/sscd.1.1.min.js @@ -0,0 +1 @@ +var SSCD=SSCD||{};SSCD.VERSION=1.1,SSCD.World=function(t){t=t||{},t.grid_size=t.grid_size||512,this.__bodies={},this.__params=t,this.__collision_tags={},this.__next_coll_tag=0},SSCD.World.prototype={__create_collision_tag:function(t){if(this.__collision_tags[t])throw new SSCD.IllegalActionError("Collision tag named '"+t+"' already exist!");this.__collision_tags[t]=1<=e)return!0}}return a>0},__test_collision_shape:function(t,i,_,e){if(t.__world===this)var s=t.__grid_bounderies;else var s=this.__get_grid_range(t);for(var o=0,n={},r=s.min_x;r<=s.max_x;++r)if(void 0!==this.__bodies[r])for(var a=s.min_y;a<=s.max_y;++a){var l=this.__bodies[r][a];if(void 0!==l)for(var h=0;h=e)return!0}}}return o>0},render:function(t,i,_,e){i=i||SSCD.Vector.ZERO,void 0===_&&(_=!0),void 0===e&&(e=!0);var s=t.getContext("2d");s.setTransform(1,0,0,1,0,0);for(var o=this.__params.grid_size,n=Math.floor(i.x/o),r=Math.floor(i.y/o),a=n+Math.ceil(t.width/o),l=r+Math.ceil(t.height/o),h=[],c=n;a>=c;++c)for(var S=r;l>=S;++S){var p=void 0;if(this.__bodies[c])var p=this.__bodies[c][S];if(_){var u=new SSCD.Vector(c*o,S*o).sub_self(i);s.beginPath(),s.rect(u.x,u.y,o-1,o-1),s.lineWidth="1",s.strokeStyle=void 0===p||0===p.length?"rgba(100, 100, 100, 0.255)":"rgba(255, 0, 0, 0.3)",s.stroke()}if(void 0!==p)for(var f=0;fs?SSCD.Math.distance(t,i):s>1?SSCD.Math.distance(t,_):SSCD.Math.distance(t,{x:i.x+s*(_.x-i.x),y:i.y+s*(_.y-i.y)})},SSCD.Math.line_intersects=function(t,i,_,e){var s,o,n,r;s=i.x-t.x,o=i.y-t.y,n=e.x-_.x,r=e.y-_.y;var a,l;return a=(-o*(t.x-_.x)+s*(t.y-_.y))/(-n*o+s*r),l=(n*(t.y-_.y)-r*(t.x-_.x))/(-n*o+s*r),a>=0&&1>=a&&l>=0&&1>=l?1:0},SSCD.Math.is_on_line=function(t,i,_){return SSCD.Math.distance_to_line(t,i,_)<=5};var SSCD=SSCD||{};SSCD.Vector=function(t,i){this.x=t,this.y=i},SSCD.Vector.prototype={get_name:function(){return"vector"},clone:function(){return new SSCD.Vector(this.x,this.y)},set:function(t){this.x=t.x,this.y=t.y},negative:function(){return this.multiply_scalar(-1)},negative_self:function(){return this.multiply_scalar_self(-1),this},distance_from:function(t){return SSCD.Math.distance(this,t)},angle_from:function(t){return SSCD.Math.angle(this,t)},move:function(t){return this.x+=t.x,this.y+=t.y,this},normalize_self:function(){var t=Math.sqrt(this.x*this.x+this.y*this.y);return this.x/=t,this.y/=t,this},normalize:function(){return this.clone().normalize_self()},add_self:function(t){return this.x+=t.x,this.y+=t.y,this},sub_self:function(t){return this.x-=t.x,this.y-=t.y,this},divide_self:function(t){return this.x/=t.x,this.y/=t.y,this},multiply_self:function(t){return this.x*=t.x,this.y*=t.y,this},add_scalar_self:function(t){return this.x+=t,this.y+=t,this},sub_scalar_self:function(t){return this.x-=t,this.y-=t,this},divide_scalar_self:function(t){return this.x/=t,this.y/=t,this},multiply_scalar_self:function(t){return this.x*=t,this.y*=t,this},add:function(t){return this.clone().add_self(t)},sub:function(t){return this.clone().sub_self(t)},multiply:function(t){return this.clone().multiply_self(t)},divide:function(t){return this.clone().divide_self(t)},add_scalar:function(t){return this.clone().add_scalar_self(t)},sub_scalar:function(t){return this.clone().sub_scalar_self(t)},multiply_scalar:function(t){return this.clone().multiply_scalar_self(t)},divide_scalar:function(t){return this.clone().divide_scalar_self(t)},clamp:function(t,i){return this.xi&&(this.x=i),this.y>i&&(this.y=i),this},from_angle:function(t){return this.x=Math.cos(t),this.y=Math.sin(t),this},apply_self:function(t){return this.x=t(this.x),this.y=t(this.y),this},apply:function(t){return this.clone().apply_self(t)},debug:function(){console.debug(this.x+", "+this.y)}},SSCD.Vector.ZERO=new SSCD.Vector(0,0),SSCD.Vector.ONE=new SSCD.Vector(1,1),SSCD.Vector.UP=new SSCD.Vector(0,-1),SSCD.Vector.DOWN=new SSCD.Vector(0,1),SSCD.Vector.LEFT=new SSCD.Vector(-1,0),SSCD.Vector.RIGHT=new SSCD.Vector(1,0),SSCD.Vector.UP_LEFT=new SSCD.Vector(-1,-1),SSCD.Vector.DOWN_LEFT=new SSCD.Vector(-1,1),SSCD.Vector.UP_RIGHT=new SSCD.Vector(1,-1),SSCD.Vector.DOWN_RIGHT=new SSCD.Vector(1,1);var SSCD=SSCD||{};SSCD.extend=function(t,i){for(var _ in t)i[_]||(i[_]=t[_]);i.__inits=i.__inits||[],t.__init__&&i.__inits.push(t.__init__),i.init=function(){for(var t=0;t0&&(this.position.x-=i,this.size.x+=i);var _=this.position.y-t.y;_>0&&(this.position.y-=_,this.size.y+=_);var e=t.x-(this.position.x+this.size.x);e>0&&(this.size.x+=e);var s=t.y-(this.position.y+this.size.y);s>0&&(this.size.y+=s)}};var SSCD=SSCD||{};SSCD.Shape=function(){},SSCD.Shape.prototype={__type:"shape",is_shape:!0,__data:null,__next_id:0,__collision_tags:[],__collision_tags_val:SSCD.World.prototype._ALL_TAGS_VAL,__init__:function(){this.__position=new SSCD.Vector,this.__grid_chunks=[],this.__world=null,this.__grid_bounderies=null,this.__id=SSCD.Shape.prototype.__next_id++},get_id:function(){return this.__id},set_collision_tags:function(t){if(null===this.__world)throw new SSCD.IllegalActionError("Can't set tags for a shape that is not inside a collision world!");return this.__collision_tags_val=this.__world.__get_tags_value(t),!t instanceof Array&&(t=[t]),this.__collision_tags=t,this.__update_tags_hook&&this.__update_tags_hook(),this},__update_tags_hook:null,get_collision_tags:function(){return this.__collision_tags},collision_tags_match:function(t){if(isNaN(t)){if(null===this.__world)throw new SSCD.IllegalActionError("If you provide tags as string(s) the shape must be inside a collision world to convert them!");t=this.__world.__get_tags_value(t)}return 0!==(this.__collision_tags_val&t)},test_collide_with:function(t){return SSCD.CollisionManager.test_collision(this,t)},repel:function(t,i,_){i=i||1,_=_||1;for(var e=this.get_repel_direction(t).multiply_scalar_self(i),s=SSCD.Vector.ZERO.clone(),o=!0;o&&_>0;)_--,t.move(e),s.add_self(e),o=this.test_collide_with(t);return s},get_repel_direction:function(t){var i,_=this.get_abs_center();if(t instanceof SSCD.Vector)var i=t;else i=t.get_abs_center();return i.sub(_).normalize_self()},__get_render_fill_color:function(t){return this.__override_fill_color?this.__override_fill_color:this.__collision_tags_to_color(this.__collision_tags_val,t)},__get_render_stroke_color:function(t){return this.__override_stroke_color?this.__override_stroke_color:this.__collision_tags_to_color(this.__collision_tags_val,t)},set_debug_render_colors:function(t,i){this.__override_fill_color=t,this.__override_stroke_color=i},__override_fill_color:null,__override_stroke_color:null,__collision_tags_to_color:function(t,i){var _=Math.round(255*Math.abs(Math.sin(t))),e=Math.round(255*Math.abs(Math.cos(t))),s=Math.round(_^e);return"rgba("+_+","+e+","+s+","+i+")"},set_data:function(t){return this.__data=t,this},get_data:function(){return this.__data},get_name:function(){return this.__type},render_aabb:function(t,i){var _=this.get_aabb();t.beginPath(),t.rect(_.position.x-i.x,_.position.y-i.y,_.size.x,_.size.y),t.lineWidth="1",t.strokeStyle="rgba(50, 175, 45, 0.5)",t.stroke()},set_position:function(t){return this.__position.x=t.x,this.__position.y=t.y,this.__update_position(),this},get_position:function(){return this.__position.clone()},move:function(t){return this.set_position(this.__position.add(t)),this},__update_position:function(){this.__update_position_hook&&this.__update_position_hook(),this.__aabb&&this.__update_aabb_pos(),this.__update_parent_world()},__update_aabb_pos:function(){this.__aabb.position=this.__position},get_abs_center:function(){var t=this.get_aabb();return t.position.add(t.size.multiply_scalar(.5))},reset_aabb:function(){this.__aabb=void 0},__update_parent_world:function(){this.__world&&this.__world.__update_shape_grid(this)},__update_position_hook:null,render:function(){throw new SSCD.NotImplementedError},build_aabb:function(){throw new SSCD.NotImplementedError},get_aabb:function(){return this.__aabb=this.__aabb||this.build_aabb(),this.__aabb}};var SSCD=SSCD||{};SSCD.Circle=function(t,i){this.init(),this.__radius=i,this.__size=new SSCD.Vector(i,i).multiply_scalar_self(2),this.set_position(t)},SSCD.Circle.prototype={__type:"circle",render:function(t,i){var _=this.__position.sub(i);t.beginPath(),t.arc(_.x,_.y,this.__radius,0,2*Math.PI,!1),t.lineWidth="7",t.strokeStyle=this.__get_render_stroke_color(.75),t.stroke(),t.fillStyle=this.__get_render_fill_color(.35),t.fill()},get_radius:function(){return this.__radius},__update_aabb_pos:function(){this.__aabb.position=this.__position.sub_scalar(this.__radius)},build_aabb:function(){return new SSCD.AABB(this.__position.sub_scalar(this.__radius),this.__size)},get_abs_center:function(){return this.__position.clone()}},SSCD.extend(SSCD.Shape.prototype,SSCD.Circle.prototype);var SSCD=SSCD||{};SSCD.Rectangle=function(t,i){this.init(),this.__size=i,this.set_position(t)},SSCD.Rectangle.prototype={__type:"rectangle",render:function(t,i){var _=this.__position.sub(i);t.beginPath(),t.rect(_.x,_.y,this.__size.x,this.__size.y),t.lineWidth="7",t.strokeStyle=this.__get_render_stroke_color(.75),t.stroke(),t.fillStyle=this.__get_render_fill_color(.35),t.fill()},get_size:function(){return this.__size.clone()},build_aabb:function(){return new SSCD.AABB(this.__position,this.__size)},get_top_left:function(){return this.__top_left_c=this.__top_left_c||this.__position.clone(),this.__top_left_c},get_bottom_left:function(){return this.__bottom_left_c=this.__bottom_left_c||this.__position.add(new SSCD.Vector(0,this.__size.y)),this.__bottom_left_c},get_top_right:function(){return this.__top_right_c=this.__top_right_c||this.__position.add(new SSCD.Vector(this.__size.x,0)),this.__top_right_c},get_bottom_right:function(){return this.__bottom_right_c=this.__bottom_right_c||this.__position.add(new SSCD.Vector(this.__size.x,this.__size.y)),this.__bottom_right_c},get_abs_center:function(){return this.__abs_center_c=this.__abs_center_c||this.__position.add(this.__size.divide_scalar(2)),this.__abs_center_c},__update_position_hook:function(){this.__top_left_c=void 0,this.__top_right_c=void 0,this.__bottom_left_c=void 0,this.__bottom_right_c=void 0,this.__abs_center_c=void 0}},SSCD.extend(SSCD.Shape.prototype,SSCD.Rectangle.prototype);var SSCD=SSCD||{};SSCD.Line=function(t,i){this.init(),this.__dest=i,this.set_position(t)},SSCD.Line.prototype={__type:"line",render:function(t,i){this.__position.sub(i);t.beginPath(),t.moveTo(this.__position.x,this.__position.y);var _=this.__position.add(this.__dest);t.lineTo(_.x,_.y),t.lineWidth="7",t.strokeStyle=this.__get_render_stroke_color(.75),t.stroke()},build_aabb:function(){var t=new SSCD.Vector(0,0);t.x=this.__dest.x>0?this.__position.x:this.__position.x-this.__dest.x,t.y=this.__dest.y>0?this.__position.y:this.__position.y-this.__dest.y;var i=this.__dest.apply(Math.abs);return new SSCD.AABB(t,i)},get_p1:function(){return this.__p1_c=this.__p1_c||this.__position.clone(),this.__p1_c},get_p2:function(){return this.__p2_c=this.__p2_c||this.__position.add(this.__dest),this.__p2_c},__update_position_hook:function(){this.__p1_c=void 0,this.__p2_c=void 0}},SSCD.extend(SSCD.Shape.prototype,SSCD.Line.prototype);var SSCD=SSCD||{};SSCD.LineStrip=function(t,i,_){if(this.init(),this.__points=i,i.length<=1)throw new SSCD.IllegalActionError("Not enough vectors for LineStrip (got to have at least two vectors)");_&&this.__points.push(this.__points[0]),this.set_position(t)},SSCD.LineStrip.prototype={__type:"line-strip",render:function(t,i){this.__position.sub(i);t.beginPath();for(var _=0;_=t.__position.x&&i.y>=t.__position.y&&i.x<=t.__position.x+t.__size.x&&i.y<=t.__position.y+t.__size.y},_test_collision_vector_line:function(t,i){return SSCD.Math.is_on_line(t,i.get_p1(),i.get_p2())},_test_collision_vector_linestrip:function(t,i){for(var _=i.get_abs_lines(),e=0;e<_.length;++e)if(SSCD.Math.is_on_line(t,_[e][0],_[e][1]))return!0;return!1},_test_collision_circle_line:function(t,i){return SSCD.Math.distance_to_line(t.__position,i.get_p1(),i.get_p2())<=t.__radius},_test_collision_circle_linestrip:function(t,i){for(var _=i.get_abs_lines(),e=0;e<_.length;++e)if(SSCD.Math.distance_to_line(t.__position,_[e][0],_[e][1])<=t.__radius)return!0;return!1},_test_collision_linestrip_line:function(t,i){for(var _=t.get_abs_lines(),e=i.get_p1(),s=i.get_p2(),o=0;o<_.length;++o)if(SSCD.Math.line_intersects(e,s,_[o][0],_[o][1]))return!0;return!1},_test_collision_line_line:function(t,i){return SSCD.Math.line_intersects(t.get_p1(),t.get_p2(),i.get_p1(),i.get_p2())},_test_collision_rect_line:function(t,i){var _=i.get_p1(),e=i.get_p2();if(SSCD.CollisionManager._test_collision_rect_vector(t,_)||SSCD.CollisionManager._test_collision_rect_vector(t,e))return!0;var s=t.get_top_left(),o=t.get_bottom_left();if(SSCD.Math.line_intersects(_,e,s,o))return!0;var n=t.get_top_right(),r=t.get_bottom_right();return SSCD.Math.line_intersects(_,e,n,r)?!0:SSCD.Math.line_intersects(_,e,s,n)?!0:SSCD.Math.line_intersects(_,e,o,r)?!0:!1},_test_collision_rect_linestrip:function(t,i){for(var _=i.get_abs_points(),e=0;e<_.length;++e)if(this._test_collision_rect_vector(t,_[e]))return!0;for(var s=t.get_top_left(),o=t.get_bottom_left(),n=t.get_top_right(),r=t.get_bottom_right(),a=i.get_abs_lines(),e=0;e_.x?[i.get_top_left(),i.get_bottom_left()]:[i.get_top_right(),i.get_bottom_right()]),o.push(s.y>_.y?[i.get_top_left(),i.get_top_right()]:[i.get_bottom_left(),i.get_bottom_right()]);for(var n=0;n_.right||e.right<_.left||e.top>_.bottom||e.bottom<_.top)}},SSCD.UnsupportedShapes=function(t,i){this.name="Unsupported Shapes",this.message="Unsupported shapes collision test! '"+t.get_name()+"' <-> '"+i.get_name()+"'."},SSCD.UnsupportedShapes.prototype=Error.prototype; \ No newline at end of file diff --git a/examples/README.md b/examples/README.md index bab64fa..b3fd0ee 100644 --- a/examples/README.md +++ b/examples/README.md @@ -6,4 +6,5 @@ Note: the examples rely on the /dist folder located one dir above this dir. example1: shows basic shapes and collision. example2: mini-game like flappy-bird, all rendered with the collision world debug renderer. -example3: using collision tags. \ No newline at end of file +example3: using collision tags. +example4: using repel() to resolve penetration. \ No newline at end of file diff --git a/examples/example1.html b/examples/example1.html index 1e3aac8..75b97a3 100644 --- a/examples/example1.html +++ b/examples/example1.html @@ -6,14 +6,16 @@ SSCD - + - + + +

SSCD Example I

@@ -21,8 +23,11 @@

SSCD Example I

Click on a shape to pick it up, and click again to drop it.

At the panel below you will see which shapes currently collide with the shape you are currently dragging.

+ +
+ + - + + +

SSCD Example II

@@ -25,8 +27,16 @@

SSCD Example II

- + + +
+ +
+ + + + + - + + +

SSCD Example III

@@ -22,12 +24,17 @@

SSCD Example III Shapes will only collide with other shapes that have the same tags! shapes with the same tags are marked with the same color.

+ +

AIR

WATER

FIRE

ALL ELEMENTS

+ +

+ + + + + + + + + + +
+

SSCD Example IV

+

+ This example is a mini-game similar to the mobile game "flappy-bird".

+ This is like example II but with a twist - this time when you touch obstacles instead of dying you will just be repelled by them.
+ Note! this example moves a lot slower than the original example, on purpose. So you can appreciate the repelling. +

+
+ + +
+ +
+ + + + + + + + + + diff --git a/src/shapes/circle.js b/src/shapes/circle.js index f70f838..0bceeba 100644 --- a/src/shapes/circle.js +++ b/src/shapes/circle.js @@ -32,7 +32,7 @@ SSCD.Circle.prototype = { render: function (ctx, camera_pos) { // apply camera on position - var position = this.get_position().sub(camera_pos); + var position = this.__position.sub(camera_pos); // draw the circle ctx.beginPath(); @@ -54,10 +54,22 @@ SSCD.Circle.prototype = { return this.__radius; }, + // called to update axis-aligned-bounding-box position + __update_aabb_pos: function() + { + this.__aabb.position = this.__position.sub_scalar(this.__radius) + }, + // return axis-aligned-bounding-box build_aabb: function () { - return new SSCD.AABB(this.get_position().sub_scalar(this.__radius), this.__size); + return new SSCD.AABB(this.__position.sub_scalar(this.__radius), this.__size); + }, + + // return the absolute center of the shape + get_abs_center: function() + { + return this.__position.clone(); }, }; diff --git a/src/shapes/composite_shape.js b/src/shapes/composite_shape.js index 2d7e74d..b457a53 100644 --- a/src/shapes/composite_shape.js +++ b/src/shapes/composite_shape.js @@ -45,18 +45,33 @@ SSCD.CompositeShape.prototype = { { this.__shapes[i].shape.render(ctx, camera_pos); } - - // now render bounding-box to mark their group boundaries - var box = this.get_aabb(); - - // draw the rect - ctx.beginPath(); - ctx.rect(box.position.x - camera_pos.x, box.position.y - camera_pos.y, box.size.x, box.size.y); - - // draw stroke - ctx.lineWidth = "1"; - ctx.strokeStyle = 'rgba(150, 75, 45, 0.5)'; - ctx.stroke(); + }, + + // repeal an object from this object. + // here we iterate over sub-object and repeal only from the ones we collide with + repel: function(obj, force, iterations) + { + var ret = SSCD.Vector.ZERO.clone(); + for (var i = 0; i < this.__shapes.length; ++i) + { + var shape = this.__shapes[i].shape; + if (shape.test_collide_with(obj)) + { + ret.add_self(shape.repel(obj, force, iterations)); + } + } + return ret; + }, + + // set colors to override the debug rendering colors + set_debug_render_colors: function(fill_color, stroke_color) + { + this.__override_fill_color = fill_color; + this.__override_stroke_color = stroke_color; + for (var i = 0; i < this.__shapes.length; ++i) + { + this.__shapes[i].shape.set_debug_render_colors(fill_color, stroke_color); + } }, // get shapes list @@ -86,6 +101,7 @@ SSCD.CompositeShape.prototype = { // if no shapes return zero aabb if (this.__shapes.length === 0) { + this.__aabb_pos_offset_c = SSCD.Vector.ZERO; return new SSCD.AABB(SSCD.Vector.ZERO, SSCD.Vector.ZERO); } @@ -103,9 +119,20 @@ SSCD.CompositeShape.prototype = { ret = curr_aabb; } } + + // store diff between position and bounding-box position, for faster aabb movement + this.__aabb_pos_offset_c = ret.position.sub(this.__position); + + // return bounding-box return ret; }, + // called to update axis-aligned-bounding-box position + __update_aabb_pos: function() + { + this.__aabb.position = this.__position.add(this.__aabb_pos_offset_c); + }, + // add shape to the composite shape // shape is shape to add add: function (shape) @@ -134,6 +161,10 @@ SSCD.CompositeShape.prototype = { shape.__collision_tags_val = this.__collision_tags_val; shape.__collision_tags = this.__collision_tags; + // set shape debug colors + shape.__override_fill_color = this.__override_fill_color; + shape.__override_stroke_color = this.__override_stroke_color; + // return the newly added shape return shape; }, diff --git a/src/shapes/line.js b/src/shapes/line.js index 37b569f..80b4bdd 100644 --- a/src/shapes/line.js +++ b/src/shapes/line.js @@ -32,7 +32,7 @@ SSCD.Line.prototype = { render: function (ctx, camera_pos) { // apply camera on position - var position = this.get_position().sub(camera_pos); + var position = this.__position.sub(camera_pos); // draw the line ctx.beginPath(); diff --git a/src/shapes/lines_strip.js b/src/shapes/lines_strip.js index 024e39f..0693184 100644 --- a/src/shapes/lines_strip.js +++ b/src/shapes/lines_strip.js @@ -44,7 +44,7 @@ SSCD.LineStrip.prototype = { render: function (ctx, camera_pos) { // apply camera on position - var position = this.get_position().sub(camera_pos); + var position = this.__position.sub(camera_pos); // draw the lines ctx.beginPath(); @@ -66,18 +66,6 @@ SSCD.LineStrip.prototype = { ctx.strokeStyle = this.__get_render_stroke_color(0.75); ctx.stroke(); - // now render bounding-box - var box = this.get_aabb(); - - // draw the rect - ctx.beginPath(); - ctx.rect(box.position.x - camera_pos.x, box.position.y - camera_pos.y, box.size.x, box.size.y); - - // draw stroke - ctx.lineWidth = "1"; - ctx.strokeStyle = 'rgba(50, 175, 45, 0.5)'; - ctx.stroke(); - }, // return line list with absolute positions @@ -131,6 +119,12 @@ SSCD.LineStrip.prototype = { this.__abs_lines_c = undefined; }, + // called to update axis-aligned-bounding-box position + __update_aabb_pos: function() + { + this.__aabb.position.set(this.__aabb_offset_c.add(this.__position)); + }, + // return axis-aligned-bounding-box build_aabb: function () { @@ -139,6 +133,7 @@ SSCD.LineStrip.prototype = { { ret.add_vector(this.__points[i]); } + this.__aabb_offset_c = ret.position.clone(); ret.position.add_self(this.__position); return ret; }, diff --git a/src/shapes/rectangle.js b/src/shapes/rectangle.js index a5dc596..e6f66cd 100644 --- a/src/shapes/rectangle.js +++ b/src/shapes/rectangle.js @@ -31,7 +31,7 @@ SSCD.Rectangle.prototype = { render: function (ctx, camera_pos) { // apply camera on position - var position = this.get_position().sub(camera_pos); + var position = this.__position.sub(camera_pos); // draw the rect ctx.beginPath(); diff --git a/src/shapes/shape.js b/src/shapes/shape.js index 53df62c..3905ce1 100644 --- a/src/shapes/shape.js +++ b/src/shapes/shape.js @@ -122,6 +122,66 @@ SSCD.Shape.prototype = { return SSCD.CollisionManager.test_collision(this, obj); }, + // repeal an object from this object. + // this means, in simple words, we push the other object outside to prevent penetration. + // this works in a very simply way - it iterates and push the penetrating object outside from center until its no longer collided. + // obj: object or vector to repeal (must have move() function). + // force: force factor, the bigger this is the stronger / faster the repealing will be. default to 1. + // iterations: max iterations of repeal-and-test-again routines. default to 1. + // NOTE: this function assume there's collision on start, meaning first iteration of repeal will ALWAYS happen. + // return: total movement due to repeling (vector) + repel: function(obj, force, iterations) + { + // set defaults + force = force || 1; + iterations = iterations || 1; + + // get direction vector + var push_vector = this.get_repel_direction(obj).multiply_scalar_self(force); + + // for return value + var ret = SSCD.Vector.ZERO.clone(); + + // now do the repeling + var collide = true; + while (collide && iterations > 0) + { + // decreate iterations count + iterations--; + + // do pushing + obj.move(push_vector); + ret.add_self(push_vector); + + // check if still colliding + collide = this.test_collide_with(obj); + } + + // return total pushed + return ret; + }, + + // get repel direction between this shape and another shape / vector + get_repel_direction: function(obj) + { + // get the center of this object + var center = this.get_abs_center(); + + // get center of other object / vector + var other_center; + if (obj instanceof SSCD.Vector) + { + var other_center = obj; + } + else + { + other_center = obj.get_abs_center(); + } + + // return repel direction vector + return other_center.sub(center).normalize_self(); + }, + // return shape fill color for debug rendering __get_render_fill_color: function (opacity) { @@ -189,6 +249,21 @@ SSCD.Shape.prototype = { return this.__type; }, + // render shape axis-aligned-bounding-box + render_aabb: function(ctx, camera_pos) + { + var box = this.get_aabb(); + + // draw the rect + ctx.beginPath(); + ctx.rect(box.position.x - camera_pos.x, box.position.y - camera_pos.y, box.size.x, box.size.y); + + // draw stroke + ctx.lineWidth = "1"; + ctx.strokeStyle = 'rgba(50, 175, 45, 0.5)'; + ctx.stroke(); + }, + // set position set_position: function (vector) { @@ -207,7 +282,7 @@ SSCD.Shape.prototype = { // move the shape move: function (vector) { - this.set_position(this.get_position().add_self(vector)); + this.set_position(this.__position.add(vector)); return this; }, @@ -221,12 +296,30 @@ SSCD.Shape.prototype = { } // remove bounding box cache - this.reset_aabb(); + if (this.__aabb) + { + this.__update_aabb_pos(); + } // update in world this.__update_parent_world(); }, + // called to update axis-aligned-bounding-box position + // this function called AFTER the position update, meaning new position applied + // this function only called if have aabb in cache. + __update_aabb_pos: function() + { + this.__aabb.position = this.__position; + }, + + // return the absolute center of the shape + get_abs_center: function() + { + var aabb = this.get_aabb(); + return aabb.position.add(aabb.size.multiply_scalar(0.5)); + }, + // reset bounding box reset_aabb: function() { diff --git a/src/utils/vector.js b/src/utils/vector.js index 3284132..d1fc147 100644 --- a/src/utils/vector.js +++ b/src/utils/vector.js @@ -28,6 +28,26 @@ SSCD.Vector.prototype = { return new SSCD.Vector(this.x, this.y); }, + // set value from another vector + set: function(vector) + { + this.x = vector.x; + this.y = vector.y; + }, + + // make negative (return without changing self) + negative: function() + { + return this.multiply_scalar(-1); + }, + + // make negative self (multiply by -1) + negative_self: function() + { + this.multiply_scalar_self(-1); + return this; + }, + // get distance from another vector distance_from: function (other) { @@ -40,6 +60,29 @@ SSCD.Vector.prototype = { return SSCD.Math.angle(this, other); }, + // move the position of this vector (same as add_self) + move: function(vector) + { + this.x += vector.x; + this.y += vector.y; + return this; + }, + + // normalize this vector + normalize_self: function() + { + var by = Math.sqrt(this.x * this.x + this.y * this.y); + this.x /= by; + this.y /= by; + return this; + }, + + // return normalized copy (don't change self) + normalize: function() + { + return this.clone().normalize_self(); + }, + // add vector to self add_self: function (other) { diff --git a/src/world.js b/src/world.js index 657a44b..e7733d0 100644 --- a/src/world.js +++ b/src/world.js @@ -8,7 +8,7 @@ var SSCD = SSCD || {}; // version identifier -SSCD.VERSION = 1.0; +SSCD.VERSION = 1.1; // a collision world. you create an instance of this class and add bodies to it to check collision. // @@ -407,12 +407,17 @@ SSCD.World.prototype = { // canvas: a 2d canvas object to render on. // camera_pos: optional, vector that represent the current camera position is 2d space. // show_grid: default to true, if set will render background grid that shows which grid chunks are currently active + // show_aabb: default to true, if set will render objects axis-aligned bounding boxes // NOTE: this function will NOT clear canvas before rendering, if you render within a main loop its your responsibility. - render: function (canvas, camera_pos, show_grid) + render: function (canvas, camera_pos, show_grid, show_aabb) { // set default camera pos if doesn't exist camera_pos = camera_pos || SSCD.Vector.ZERO; + // set default show_grid and show_aabb + if (show_grid === undefined) {show_grid = true;} + if (show_aabb === undefined) {show_aabb = true;} + // get ctx and reset previous transformations var ctx = canvas.getContext('2d'); ctx.setTransform(1, 0, 0, 1, 0, 0); @@ -444,20 +449,22 @@ SSCD.World.prototype = { } // render current grid chunk - var position = new SSCD.Vector(i * grid_size, j * grid_size).sub_self(camera_pos); - ctx.beginPath(); - ctx.rect(position.x, position.y, grid_size-1, grid_size-1); - ctx.lineWidth = "1"; - if ((curr_grid_chunk === undefined) || (curr_grid_chunk.length === 0)) - { - ctx.strokeStyle = 'rgba(100, 100, 100, 0.255)'; - } - else + if (show_grid) { - ctx.strokeStyle = 'rgba(255, 0, 0, 0.3)'; + var position = new SSCD.Vector(i * grid_size, j * grid_size).sub_self(camera_pos); + ctx.beginPath(); + ctx.rect(position.x, position.y, grid_size-1, grid_size-1); + ctx.lineWidth = "1"; + if ((curr_grid_chunk === undefined) || (curr_grid_chunk.length === 0)) + { + ctx.strokeStyle = 'rgba(100, 100, 100, 0.255)'; + } + else + { + ctx.strokeStyle = 'rgba(255, 0, 0, 0.3)'; + } + ctx.stroke(); } - ctx.stroke(); - // if current grid chunk has no objects skip if (curr_grid_chunk === undefined) @@ -481,6 +488,10 @@ SSCD.World.prototype = { for (var i = 0; i < render_list.length; ++i) { render_list[i].render(ctx, camera_pos); + if (show_aabb) + { + render_list[i].render_aabb(ctx, camera_pos); + } } }, };