// MC.js
// MaxCanvas 2D engine for the HTML5 <canvas> element
// Copyright Max Bryans (Contact @ maxsfw3@gmail.com)
/**
* (<b>MaxCanvas</b>)
* <br> Version 1.0 (June 2018 - April 2019)
* <br> First Release
* <br>All Engine Variables reside in this namespace.
* <br><b>Avoid extending as future versions may overwrite your work !<b>
* @name <b>MC</b>
* @namespace MC
*/
var MC = {
_canvas: null,
_maths: null,
_draw: null,
_utils: null,
_mouse: null,
_keys: null,
_Typeface: null,
_GUI: null,
_ConBox: null,
_Point: null,
_Color: null,
_Picture: null,
_SpriteBin: null
}; // End of var MC Object Declaration
////////////////////
// INITIALISATION //
////////////////////
/**
*<hr>
*Initialises the MaxCanvas (MC) Engine by passing it a reference to the canvas it is to use.
*<br>(1) If the parameter string "fullWindow" is entered, the canvas will automatically re-size to the browser window size. It will subsequently be resized with the window.
*@param {Object} canvas - HTML canvas to be used
*@param {String} [fullWindow] - (1)
*@example
*var c=document.getElementById("canvas");
*MC.init(c,"fullWindow");
*/
MC.init = function(canvas,fullWindow) {
// Establish Internal Namespaces
_canvas = this.canvas;
_game = this.game;
_maths = this.maths;
_draw = this.draw;
_utils = this.utils;
_mouse = this.mouse;
_keys = this.keys;
_ConBox = this.ConBox;
_GUI = this.GUI;
_Typeface = this.Typeface;
_Point = this.Point;
_Color = this.Color;
_Picture = this.Picture;
_Sprite = this.Sprite;
_SpriteBin = this.SpriteBin;
_canvas.canvas = canvas;
_draw.context = canvas.getContext("2d");
_canvas.width = canvas.width;
_canvas.height = canvas.height;
_canvas.midX = _canvas.width / 2;
_canvas.midY = _canvas.height / 2;
_canvas.setBounds();
_backCanvas = document.createElement('canvas');
_backCanvasContext = _backCanvas.getContext('2d');
if (fullWindow) {
if (fullWindow === "fullWindow") {
this.canvas.fullWindow = true;
_canvas.windowResize();
}
}
window.onresize = function(e) { _canvas.windowResize(); }; // establish listener for windows re-size. will fullScreen canvas to match if _canvas.fullWindow == true
_game.gravity = new _Point();
_game.mouse = new _Point();
_game.oldTime = new Date().getTime();
_game.text = new _Typeface("Arial", 20, "Black");
_game.conBoxDefaults = {colorBody: "Orange", colorBodyHover: "DarkGreen", colorEdge: "Black", colorEdgeHover: "White", edgeWidth: 5};
_mouse.init(); // Mouse Events Listeners added here
_keys.init(); // Keyboard Listeners added here
}; // End of INITIALISATION
////////////////////
// CANVAS //
////////////////////
/**
*<hr>
*The canvas object contains references to and records properties of the canvas associated with the MaxCanvas engine at Initialisation
*<br>Properties are automatically updated as required.
*@property {Object} canvas Reference to the canvas associated with this instance of the MaxCanvas engine.
*<br>Defined at initialisation
*@property {Number} width Width of the full canvas (in pixels)
*@property {Number} height Height of the full canvas (in pixels)
*@property {Number} midX X coordinate of the canvas middle (in pixels)
*@property {Number} midY Y coordinate of the canvas middle (in pixels)
*@property {Number} top Top of the canvas after "bounds" have been applied (in pixels)
*@property {Number} bottom Bottom of the canvas after "bounds" have been applied (in pixels)
*@property {Number} left Left of the canvas after "bounds" have been applied (in pixels)
*@property {Number} right Right of the canvas after "bounds" have been applied (in pixels)
*@property {Boolean} fullWindow <tt>true</tt> if the Canvas is set to size to the window (and resize with it)
*@interface
*/
MC.canvas = {
canvas: null,
width: 0,
height: 0,
midX: 0,
midY: 0,
top: 0,
bottom: 0,
left: 0,
right: 0,
fullWindow: false,
/**
*<hr>
*Sets the <tt>canvas.top, bottom, left</tt> and <tt>right</tt> properties to reflect <tt>game.bounds</tt> and canvas dimensions
*<br>CAUTION. This is intended for Engine internal use. Developers should use <tt>game.setBounds()</tt> to safely change a canvas's bounds
*/
setBounds: function() {
this.top = this.height * _game.bounds.top;
this.bottom = this.height * _game.bounds.bottom;
this.left = this.width * _game.bounds.left;
this.right = this.width * _game.bounds.right;
},
/**
*<hr>
*Sets the canvas element's to a new width and height
*<br>All <tt>canvas</tt> properties (including bounds) are automatically reset
*@param {Number} newWidth New canvas width (in pixels)
*@param {Number} newHeight New canvas height (in pixels)
*/
setSize: function(newWidth,newHeight) {
if (_utils.isNumeric(newWidth) && _utils.isNumeric(newHeight)) {
this.canvas.width = newWidth;
this.canvas.height = newHeight;
this.width = newWidth;
this.height = newHeight;
this.midX = newWidth/2;
this.midY = newHeight/2;
this.setBounds();
}
},
/**
*<hr>
*Console logs the canvas properties
*/
log: function() {
console.log("Canvas status >> W: "+this.width+", H: "+this.height+", top: "+this.top+", bottom: "+this.bottom+", left: "+this.left+", right: "+this.right+", fullWindow: "+this.fullWindow);
},
/**
*<hr>
*Handles window re-size
*<br>CAUTION. This is intended for Engine internal use and is called automatically if required.
*/
windowResize: function () {
if (this.fullWindow) {
this.canvas.width = window.innerWidth || document.documentElement.clientWidth || document.body.clientWidth;
this.canvas.height = window.innerHeight || document.documentElement.clientHeight || document.body.clientHeight;
// next 2 lines from --- https://stackoverflow.com/questions/242608/disable-browsers-vertical-and-horizontal-scrollbars (June 2018)
document.documentElement.style.overflow = 'hidden'; // firefox, chrome
document.body.scroll = "no"; // ie only
this.canvas.style.border = "none";
this.canvas.style.left = '0px';
this.canvas.style.top = '0px';
this.canvas.style.right = '0px';
this.canvas.style.bottom = '0px';
this.width = this.canvas.width;
this.height = this.canvas.height;
this.midX = this.Width/2;
this.midY = this.Height/2;
this.setBounds();
}
}
}; // End of CANVAS
////////////////////
// GAME //
////////////////////
/**
*<hr>
*The game object contains essential game related properties
*@property {MC.Point} gravity The Global gravity acceleration vector to be applied to all Sprites which have their <tt>gravity</tt> property set to <tt>true</tt>
*<br>Set at initialisation to <tt>(0,0)</tt>
*@property {Number} deltaTime The time in SECONDS since the last frame
*<br>Refreshed by calling the MC.game.update() method within any game loop <tt>requestAnimationFrame()</tt> calls
*<br> So useful some developers use <tt>var DT = MC.game.deltaTime;</tt> as a shortcut
*@property {Number} oldTime Time of the previous frame
*<br>WARNING: used internally by the MC engine. <b>DO NOT MODIFY</b>
*<br>Initialised to Game Start time.
*@property {Object} bounds Comprising <tt>top, bottom, left</tt> and <tt>right</tt>.
*<br>These are the ratio of the actual canvas to be to be considered the "game boundry" for the purposes of Sprite edge checks.
*<br>Set to the whole canvas at initialisation. i.e. <tt>(top: 0, bottom: 1, left: 0, right: 1)</tt>
*<br>e.g. a 800 x 600px canvas could have it's bounds set to <tt>(top: 0, bottom: 1, left: 0.25, right: 0.75)</tt> in which case the game boundries would be a 400px central column in the middle of the canvas.
* <NB>CAUTION use <tt>game.setBounds()</tt> to modify
*@property {Number} physicsUpdates=1 - The number of physics updates to be applied each during the update cycle
*@property {MC.Point} mouse The canvas coordinates of the user mouse pointer (and mirrors <tt>MC.mouse.pos</tt>)
*<br>This is usually considered important enough for Developers to move it into Global name space with <tt>var mouse = MC.game.mouse</tt>. Allowing simple <tt>mouse.x</tt> and <tt>mouse.y</tt> to be used
*@property {Boolean} paused=false When <tt>true</tt> then MC.game.deltaTime will not update
*@property {MC.Typeface} text Defining the default MC.Engine text font, size and color. Used by any text when nothing else is defined. Initialised by <tt>MC.init()</tt>
*@property {Object} conBoxDefaults Set during <tt>MC.init()</tt> This container object holds the default values for the MC.ConBox objects.
*<br>N.B. ConBox default font is the same as the above <tt>MC.game.text.font</tt> value. Edit these to save having to do it for every new box.
*<br><b>Values</b>
*<br>colorBody (default = "Orange")
*<br>colorBodyHover (default = "DarkGreen")
*<br>colorEdge (default = "Black")
*<br>colorEdgeHover (default = "White")
*<br>edgeWidth (default = 5)
*@interface
*/
MC.game = {
gravity: null, // initialised as a Point(0,0)
deltaTime: 0,
oldTime: null,
bounds: {top: 0, bottom: 1, left: 0, right: 1},
physicsUpdates: 1,
mouse: null,
paused: false,
text: null,
conBoxDefaults: null,
/**
*<hr>
*<br> Call this method in any <tt>requestAnimationFrame()</tt> to update MC.game.deltaTime
*<br> If paused (i.e. <tt>MC.game.paused == true</tt>) deltaTime is set to 0 (zero)
*/
update: function() {
var Now = new Date().getTime();
if (!this.paused) {
this.deltaTime = (Now - this.oldTime)/1000;
}
else {
this.deltaTime = 0;
}
this.oldTime = Now;
},
/**
*<hr>Sets the game boundaries on the canvas (as proportions of canvas height and width)
*<br> All parameters are clamped to 0-1
*@param {Number} top Top boundary
*@param {Number} bottom Bottom boundary
*@param {Number} left Left boundary
*@param {Number} right Right boundary
*/
setBounds: function (top,bottom,left,right) {
this.bounds.top = _maths.clamp(top,0,1);
this.bounds.bottom = _maths.clamp(bottom,0,1);
this.bounds.left = _maths.clamp(left,0,1);
this.bounds.right = _maths.clamp(right,0,1);
_canvas.setBounds();
},
/**
*<hr>
*Resets the game boundaries to mirror the full canvas height/width i.e. the equivalent of calling <tt>setBounds(0,1,0,1);</tt>
*/
resetBounds: function () {
this.bounds.top = 0;
this.bounds.bottom = 1;
this.bounds.left = 0;
this.bounds.right = 1;
_canvas.setBounds();
},
/**
*<hr>
*Displays the game FPS (0 is paused) in the top left of the canvas
*<br>Drawn using the <tt>MC.game.text</tt> default Typeface
*<br>FPS is logged in 10fps increments (to render it legible)
*/
fpsLog : function () {
var t = "FPS: "+ ~~(1/this.deltaTime/10)*10;
_draw.text(t,new MC.Point(2,2+this.text.size));
},
/**
*<hr>
*Displays the mouse co-ordinates in the bottom left of the canvas
*<br>Drawn using the <tt>MC.game.text</tt> default Typeface
*/
mouseLog : function () {
_draw.text("Mouse "+ this.mouse.x + "/" + this.mouse.y,new MC.Point(2,_canvas.bottom - 4));
}
}; // End of GAME
////////////////////
// MATHS //
////////////////////
/**
*The maths object provides useful methods for Developer and Engine internal use.
*@property {Number} TO_RADIANS Ratio for converting Degrees to Radians (i.e. Pi / 180)
*@property {Number} TO_DEGREES - Ratio for converting Radians to Degrees (i.e. 180 / Pi)
*@property {Number} TWOPI (Pi * 2)
*@interface
*
*/
MC.maths = {
TO_RADIANS: Math.PI / 180,
TO_DEGREES: 180 / Math.PI,
TWOPI: 2 * Math.PI,
/**
*<hr>
*Degree to Radian convertion
*@param {Number} angle - Angle in Degrees
*@returns {Number} Angle in Radians
*/
degToRad: function (angle) {
return angle * _maths.TO_RADIANS;
},
/**
*<hr>
*Radian to Degree convertion
*@param {Number} angle - Angle in Radians
*@returns {Number} Angle in Degrees
*/
radToDeg: function (angle) {
return angle * _maths.TO_DEGREES;
},
/**
*<hr>
*@param {Number} - returns a minimum 2 length HEX number.
*<br> Used internally to convert Color values from rgb to Hex notation
*@returns {string}
*/
toHex: function(number) {
// https://stackoverflow.com/questions/57803/how-to-convert-decimal-to-hex-in-javascript (June 2018)
var mod = "";
var Ret = "";
if (number < 0) {number = 0xFFFFFFFF + number + 1; mod = "-";}
Ret = number.toString(16).toUpperCase();
if (Ret.length < 2) {Ret = "0"+Ret;}
Ret = mod+Ret;
return Ret;
},
/**
*<hr>
*Clamps a number between a pair of values
*@param {Number} value - Number to be clamped
*@param {Number} min - Minimum clamp value
*@param {Number} max - Maximum clamp value
*@returns {Number} The clamped value
*/
clamp: function (value, min, max) {
if (value >= max) return max;
else if (value <= min) {return min;}
else return value;
},
/**
*<hr>
*Returns a Random Integer between the provided values (inclusive)
*@param {Number} min - Mimimum value for the random number
*@param {Number} max - Maximum value for the random number
*@returns {Number} A Random Integer within the required range
*/
randBetween: function(min, max) {
return Math.floor(Math.random() * (max - min + 1)) + min;
},
/**
*<hr>
*Linear Interpolation between two values (either points or numbers)
*@param {Number|MC.Point} start - Starting (datum) point or number
*@param {Number|MC.Point} finish - Target point or number
*@param {Number} ratio - Proportion of travel required (N.B. decimals and -ve numbers are accepted)
*@returns {Number|MC.Point}
*@throws Console warning when neither a pair of Points or numbers passed in. Start parameter is returned.
*/
lerp: function(start,finish,ratio) {
if (_utils.hasXY(start) && _utils.hasXY(finish)) {
return new _Point(start.x + ((finish.x - start.x) * ratio), start.y + ((finish.y - start.y) * ratio));
}
else if (_utils.isNumeric(start) && _utils.isNumeric(finish)) {
return start + ((finish - start) * ratio);
}
else {
console.log("WARNING: Neither a pair of points or numbers passed to lerp()");
return start;
}
},
/**
*<hr>
*Returns the lowest value in a list of arguments OR an array
*<br>Similar to <tt>Math.min()</tt> but works when passed an array of numbers
*@param {Number|Array} arg Comma seperated list of numbers OR an Array of numbers
*@returns {Number} The lowest number presented
*@throws Console warning when nothing passed in. 0 (zero) returned
*/
minOf: function(arg) {
if (arguments.length == 0) {
console.log("WARNING: nothing passed to minOf()");
return 0;
}
var L = 0;
var lowest = 0;
if (typeof arg === "array" || arg instanceof Array) {
lowest = arg[0];
L = arg.length;
for (var i = 1 ; i < L; i++)
if (arg[i] < lowest) lowest = arg[i];
return lowest;
}
else {
L = arguments.length;
lowest = arguments[0];
for (var i = 1 ; i < L; i++)
if (arguments[i] < lowest) lowest = arguments[i];
return lowest;
}
},
/**
*<hr>
*Returns the highest value in a list of arguments OR an array
*<br>Similar to <tt>Math.max()</tt> but works when passed an array of numbers
*@param {Number|Array} arg Comma seperated list of numbers OR an Array of numbers
*@returns {Number} The highest number presented
*@throws Console warning when nothing passed in. 0 (zero) returned
*/
maxOf: function(arg) {
if (arguments.length == 0) {
console.log("WARNING: nothing passed to maxOf()");
return 0;
}
var L = 0;
var highest = 0;
if (typeof arg === "array" || arg instanceof Array) {
highest = arg[0];
L = arg.length;
for (var i = 1 ; i < L; i++)
if (arg[i] > highest) highest = arg[i];
return highest;
}
else {
L = arguments.length;
highest = arguments[0];
for (var i = 1 ; i < L; i++)
if (arguments[i] > highest) highest = arguments[i];
return highest;
}
},
/**
*<hr>
*Returns the mean/average value in a list of arguments OR an array
*@param {Number|Array} arg Comma seperated list of numbers OR an Array of numbers
*@returns {Number} The average value
*@throws Console warning when nothing passed in. 0 (zero) returned
*/
meanOf: function (arg) {
if (arguments.length == 0) {
console.log("WARNING: nothing passed to meanOf()");
return 0;
}
var L = 0;
var count = 0;
if (typeof arg === "array" || arg instanceof Array) {
L = arg.length;
for (var i = 0 ; i < L; i++) count += arg[i];
return count / L;
}
else {
L = arguments.length;
for (var i = 0 ; i < L; i++) count += arguments[i];
return count / L;
}
},
/**
*<hr>
*Returns the scalar distance between two points
*<br>Points can be entered in either <tt>(Point1,Point2)</tt> OR <tt>(x1,y1,x2,y2)</tt> format
*@param {Number|MC.Point} x1_or_pnt1 - First Point OR first point's x value
*@param {Number|MC.Point} y1_or_pnt2 - Second Point OR first point's y value
*@param {Number} [x2] - Second point's x value
*@param {Number} [y2] - Second point's y value
*@throws Console warning if neither 2 nor 4 arguments presented. 0 (zero) returned
*@returns {Number} the range between the points
*/
range: function(x1_or_pnt1,y1_or_pnt2,x2,y2) {
if (arguments.length !== 2 && arguments.length !== 4) {
console.log("WARNING: incorrect number of parameters passed to range()");
return 0;
}
var X1 =0,Y1=0,X2=0,Y2=0;
if (arguments.length == 2 && _utils.hasXY(x1_or_pnt1) && _utils.hasXY(y1_or_pnt2) ) {
X1 = x1_or_pnt1.x;
Y1 = x1_or_pnt1.y;
X2 = y1_or_pnt2.x;
Y2 = y1_or_pnt2.y;
}
else {
X1 = x1_or_pnt1;
Y1 = y1_or_pnt2;
X2 = x2;
Y2 = y2;
}
return Math.sqrt(Math.pow(Math.abs(X1 - X2),2) + Math.pow(Math.abs(Y1 - Y2),2));
},
/**
*<hr>
*Returns the scalar distance SQUARED between two points
*<br>Much more efficient for Hit Calculation purposes
*<br>Points can be entered in either <tt>(Point1,Point2)</tt> OR <tt>(x1,y1,x2,y2)</tt> format
*@param {Number|MC.Point} x1_or_pnt1 - First Point OR first point's x value
*@param {Number|MC.Point} y1_or_pnt2 - Second Point OR first point's y value
*@param {Number} [x2] - Second point's x value
*@param {Number} [y2] - Second point's y value
*@throws Console warning if neither 2 nor 4 arguments presented. 0 (zero) returned
*@returns {Number} the range squared between the points
*/
rangeSqr: function(x1_or_pnt1,y1_or_pnt2,x2,y2) {
if (arguments.length !== 2 && arguments.length !== 4) {
console.log("WARNING: incorrect number of parameters passed to range()");
return 0;
}
var X1 =0,Y1=0,X2=0,Y2=0;
if (arguments.length == 2 && _utils.hasXY(x1_or_pnt1) && _utils.hasXY(y1_or_pnt2) ) {
X1 = x1_or_pnt1.x;
Y1 = x1_or_pnt1.y;
X2 = y1_or_pnt2.x;
Y2 = y1_or_pnt2.y;
}
else {
X1 = x1_or_pnt1;
Y1 = y1_or_pnt2;
X2 = x2;
Y2 = y2;
}
return Math.pow(Math.abs(X1 - X2),2) + Math.pow(Math.abs(Y1 - Y2),2);
},
/**
*<hr>
*Returns the manhatten distance between two points
*<br>Points can be entered in either <tt>(Point1,Point2)</tt> OR <tt>(x1,y1,x2,y2)</tt> format
*@param {Number|MC.Point} x1_or_pnt1 - First Point OR first point's x value
*@param {Number|MC.Point} y1_or_pnt2 - Second Point OR first point's y value
*@param {Number} [x2] - Second point's x value
*@param {Number} [y2] - Second point's y value
*@throws Console warning if neither 2 nor 4 arguments presented. 0 (zero) returned
*@returns {Number} the manhatten range between the points
*/
manhatten: function(x1_or_pnt1,y1_or_pnt2,x2,y2) {
if (arguments.length !== 2 && arguments.length !== 4) {
console.log("WARNING: incorrect number of parameters passed to manhatten()");
return 0;
}
var X1 =0,Y1=0,X2=0,Y2=0;
if (arguments.length == 2 && _utils.hasXY(x1_or_pnt1) && _utils.hasXY(y1_or_pnt2) ) {
X1 = x1_or_pnt1.x;
Y1 = x1_or_pnt1.y;
X2 = y1_or_pnt2.x;
Y2 = y1_or_pnt2.y;
}
else {
X1 = x1_or_pnt1;
Y1 = y1_or_pnt2;
X2 = x2;
Y2 = y2;
}
return (Math.abs(X1 - X2) + Math.abs(Y1 - Y2));
},
/**
*<hr>
*Returns the Dot Product (aka Scalar Product) of two vectors (points) i.e. <tt>[(x1*x2)+(y1*y2)]</tt>
*<br>Points can be entered in either <tt>(Point1,Point2)</tt> OR <tt>(x1,y1,x2,y2)</tt> format
*@param {Number|MC.Point} x1_or_pnt1 - First Point OR first point's x value
*@param {Number|MC.Point} y1_or_pnt2 - Second Point OR first point's y value
*@param {Number} [x2] - Second point's x value
*@param {Number} [y2] - Second point's y value
*@throws Console warning if neither 2 nor 4 arguments presented. 0 (zero) returned
*@returns {Number} the Dot Product of the two vectors
*/
dotProd: function(x1_or_pnt1,y1_or_pnt2,x2,y2) {
if (arguments.length !== 2 && arguments.length !== 4) {
console.log("WARNING: incorrect number of parameters passed to dotProd()");
return 0;
}
var X1 =0,Y1=0,X2=0,Y2=0;
if (arguments.length == 2 && _utils.hasXY(x1_or_pnt1) && _utils.hasXY(y1_or_pnt2) ) {
X1 = x1_or_pnt1.x;
Y1 = x1_or_pnt1.y;
X2 = y1_or_pnt2.x;
Y2 = y1_or_pnt2.y;
}
else {
X1 = x1_or_pnt1;
Y1 = y1_or_pnt2;
X2 = x2;
Y2 = y2;
}
return (X1 * X2) + (Y1 * Y2);
},
/**
*<hr>
*Returns the Cross Product of two vectors (points) i.e. <tt>[(x1*y2)-(y1*x2)]</tt>
*<br>Points can be entered in either <tt>(Point1,Point2)</tt> OR <tt>(x1,y1,x2,y2)</tt> format
*@param {Number|MC.Point} x1_or_pnt1 - First Point OR first point's x value
*@param {Number|MC.Point} y1_or_pnt2 - Second Point OR first point's y value
*@param {Number} [x2] - Second point's x value
*@param {Number} [y2] - Second point's y value
*@throws Console warning if neither 2 nor 4 arguments presented. 0 (zero) returned
*@returns {Number} the Cross Product of the two vectors
*/
crossProd: function(x1_or_pnt1,y1_or_pnt2,x2,y2) {
if (arguments.length !== 2 && arguments.length !== 4) {
console.log("WARNING: incorrect number of parameters passed to crossProd()");
return 0;
}
var X1 =0,Y1=0,X2=0,Y2=0;
if (arguments.length == 2 && _utils.hasXY(x1_or_pnt1) && _utils.hasXY(y1_or_pnt2) ) {
X1 = x1_or_pnt1.x;
Y1 = x1_or_pnt1.y;
X2 = y1_or_pnt2.x;
Y2 = y1_or_pnt2.y;
}
else {
X1 = x1_or_pnt1;
Y1 = y1_or_pnt2;
X2 = x2;
Y2 = y2;
}
return (X1 * Y2) - (Y1 * X2);
},
/**
*<hr>
* Returns a NEW normalised vector (point) of what was passed in
* <br> N.B to normalise the original vector itself use <tt>Point.normalise()</tt> instead
* @param {MC.Point} - the vector
* @returns {MC.Point} A <tt>new</tt> normalised vector
* @throws Console warning if parameter does not have x and y properties. Input parameter is returned.
*/
normalOf: function(vector) {
if (_utils.hasXY(vector)) {
var ret = new _Point(0,0);
var L = _maths.range(ret,vector);
ret.x = vector.x / L;
ret.y = vector.y / L;
return ret;
}
else {
console.log("WARNING: item without x and/or y passed to normalOf()");
return vector;
}
},
/**
*<hr>
*Returns the surface area of a triangle from the lengths of it's sides (using Heron's formula)
*<br>Accepts either 3 lengths, or 3 points representing the corner co-ordinates can be used
*<br>CAUTION: do NOT mix parameter types
*@param {Number|MC.Point} len_or_Pnt1 - First side length or vertex coordinates
*@param {Number|MC.Point} len_or_Pnt2 - Second side length or vertex coordinates
*@param {Number|MC.Point} len_or_Pnt3 - Third side length or vertex coordinates
*@returns {Number} Area of the Triangle
*/
areaOfTriangle: function (len_or_Pnt1,len_or_Pnt2,len_or_Pnt3) {
var l1,l2,l3,p = 0;
if (_utils.hasXY(len_or_Pnt1) && _utils.hasXY(len_or_Pnt2) && _utils.hasXY(len_or_Pnt3)) {
l1 = _maths.range(len_or_Pnt1,len_or_Pnt2);
l2 = _maths.range(len_or_Pnt2,len_or_Pnt3);
l3 = _maths.range(len_or_Pnt3,len_or_Pnt1);
}
else {
l1 = len_or_Pnt1;
l2 = len_or_Pnt2;
l3 = len_or_Pnt3;
}
var p = (l1 + l2 + l3)/2;
return Math.sqrt( p*(p-l1)*(p-l2)*(p-l3) );
}
}; // End of MATHS
////////////////////
// DRAW //
////////////////////
/**
*The draw object provides methods for drawing to the HTML5 canvas element.
*@property {object} context Reference to the canvas 2dcontext.
*<br>Populated during <tt>MC.init()</tt>
*@interface
*
*/
MC.draw = {
context: null,
/**
*<hr>
*Clears the canvas by filling it with a single color.
*<br> 1 - either an MC.Color object or an HTML5 color name can be used as a Color
*@param {MC.Color|string} color - Desired canvas color (1)
*/
clearCanvas: function (color) {
var c = this.context;
c.fillStyle = _utils.colorOrString(color);
c.fillRect(0,0,c.canvas.width,c.canvas.height);
return;
},
/**
*<hr>
*Fills the canvas area defined as "inBounds" with a single color.
*<br>See <tt>game.setBounds()</tt> for more details
*<br> 1 - either an MC.Color object or an HTML5 color name can be used as a Color
*@param {MC.Color|string} color - Desired canvas color (1)
*/
clearInBounds: function (color) {
var c = this.context;
c.fillStyle = _utils.colorOrString(color);
c.fillRect(_canvas.left,_canvas.top,(_canvas.right - _canvas.left),(_canvas.bottom - _canvas.top));
return;
},
/**
*<hr>
*Fills the canvas area NOT defined as "inBounds" with a single color.
*<br>See <tt>game.setBounds()</tt> for more details
*<br> 1 - either an MC.Color object or an HTML5 color name can be used as a Color
*@param {MC.Color|string} color - Desired canvas color (1)
*/
clearOutBounds: function (color) {
var c = this.context;
c.fillStyle = _utils.colorOrString(color);
if (_canvas.top > 0) c.fillRect(0,0,c.canvas.width,_canvas.top);
if (_canvas.bottom < c.canvas.height) c.fillRect(0,_canvas.bottom,c.canvas.width,c.canvas.height - _canvas.bottom);
if (_canvas.left > 0) c.fillRect(0,0,_canvas.left,c.canvas.height);
if (_canvas.right < c.canvas.width) c.fillRect(_canvas.right,0,c.canvas.width - _canvas.right,c.canvas.height);
return;
},
/**
*<hr>
*Draws a line from one Point to another
*<br> N.B. Alpha (transparency) is only supported via a MC.Color parameter
*<br> 1 - either an MC.Color object or an HTML5 color name can be used as a Color
*@param {MC.Point} from - Line start point
*@param {MC.Point} to - Line finish point
*@param {MC.Color|string} color - Line color (1)
*@param {Number} [width=1] - Desired Line width
*/
line: function (from,to,color,width) {
var alpha = false;
var c = this.context;
if (color.a !== 1) {
c.globalAlpha = color.a;
alpha = true;
}
c.beginPath();
c.moveTo(from.x,from.y);
c.lineWidth = width || 1;
c.strokeStyle = _utils.colorOrString(color);
c.lineTo(to.x, to.y);
c.closePath();
c.stroke();
if (alpha) c.globalAlpha = 1;
return;
},
/**
*<hr>
*Draws a line through a series of Points
*<br> N.B. Alpha (transparency) is only supported via a MC.Color parameter
*<br> 1 - either an MC.Color object or an HTML5 color name can be used as a Color
*@param {Array} pointsArray - Array of <tt>Point<tt>
*@param {MC.Color|string} color - Line color (1)
*@param {Number} [width=1] - Desired Line width
*/
polyLine : function (pointsArray,color,width) {
var len = pointsArray.length;
for (var i = 0; i < len - 1 ; i = i+1) {
_draw.line(pointsArray[i],pointsArray[i+1],color,width);
}
},
/**
*<hr>
*Draws a Circle
*<br> N.B. Alpha (transparency) is only supported via a MC.Color parameter
*<br> 1 - either an MC.Color object or an HTML5 color name can be used as a Color
*<br> 2 - Omission of borderColor and edgeWidth parameters results in a filled circle (with no border)
*@param {MC.Point} centre - Circle centre
*@param {Number} radius - Circle Radius
*@param {MC.Color|string|null} fillColor - Circle fill color (1). <tt>null</tt> results in an non-filled circle
*@param {MC.Color|string} [borderColor] - Circle border line color (1)(2).
*@param {Number} [edgeWidth=0] - Circle border line width (2)
*/
circle: function (centre,radius,fillColor,borderColor,edgeWidth) {
var alpha = false;
var c = this.context;
if (fillColor !== null && fillColor.a !== 1) {
c.globalAlpha = fillColor.a;
alpha = true;
}
c.beginPath();
c.arc(centre.x,centre.y,radius,0, _maths.TWOPI,false);
if (fillColor !== null) {
c.fillStyle = _utils.colorOrString(fillColor);
c.fill();
}
if (alpha) c.globalAlpha = 1;
if (borderColor) {
if (borderColor && borderColor.a !== 1) {
c.globalAlpha = borderColor.a;
alpha = true;
}
c. lineWidth = edgeWidth || 1;
c.strokeStyle = _utils.colorOrString(borderColor);
c.stroke();
}
if (alpha) c.globalAlpha = 1;
c.closePath();
return;
},
/**
*<hr>
*Draw an angled rectangle
*<br>N.B. See <tt>Draw.rectBasic()</tt> for a non-rotated rectangle
*<br> N.B. Alpha (transparency) is only supported via a MC.Color parameter
*<br> 1 - either an MC.Color object or an HTML5 color name can be used as a Color
*<br> 2 - Omission of <tt>borderColor</tt> and <tt>edgeWidth</tt> parameters results in a filled rectangle (with no border)
*@param {MC.Point} centre - Rectangle centre
*@param {Number} width - Rectangle width
*@param {Number} height - Rectangle height
*@param {Number} angle - Draw angle (degrees)
*@param {MC.Color|string|null} fillColor - Rectangle fill color (1). <tt>null</tt> results in an non-filled rectangle
*@param {MC.Color|string} [borderColor] - Rectangle border line color (1)(2).
*@param {Number} [edgeWidth=0] - Rectangle border line width (2)
*/
rect: function (centre,width,height,angle,fillColor,borderColor,edgeWidth) {
var alpha = false;
var c = this.context;
c.save();
c.beginPath();
if (fillColor !== null && fillColor.a !== 1) {
c.globalAlpha = fillColor.a;
alpha = true;
}
c.translate(centre.x,centre.y);
if (angle !== 0 && angle !== 360) {
c.rotate(angle * _maths.TO_RADIANS);
}
if (fillColor !== null) {
c.fillStyle = _utils.colorOrString(fillColor);
c.fillRect(-(width/2),-(height/2),width,height);
}
if (alpha) c.globalAlpha = 1;
if (borderColor && borderColor.a !== 1) {
c.globalAlpha = borderColor.a;
alpha = true;
}
if (borderColor && borderColor !== null) {
c. lineWidth = edgeWidth || 1;
c.strokeStyle = _utils.colorOrString(borderColor);
c.rect(-(width/2),-(height/2),width,height);
c.stroke();
}
c.closePath();
c.restore();
if (alpha) c.globalAlpha = 1;
return;
},
/**
*<hr>
*Draws an axis-aligned rectangle.
*<br>This method is slightly quicker than <tt>Draw.rect()</tt>
*<br> N.B. Alpha (transparency) is only supported via a MC.Color parameter
*<br> 1 - either an MC.Color object or an HTML5 color name can be used as a Color
*<br> 2 - Omission of <tt>borderColor</tt> and <tt>edgeWidth</tt> parameters results in a filled rectangle (with no border)
*@param {MC.Point} centre - Rectangle centre
*@param {Number} width - Rectangle width
*@param {Number} height - Rectangle height
*@param {MC.Color|string|null} fillColor - Rectangle fill color (1). <tt>null</tt> results in an non-filled rectangle
*@param {MC.Color|string} [borderColor] - Rectangle border line color (1)(2).
*@param {number} [edgeWidth=1] - Rectangle border line width (2)
*/
rectBasic: function (centre,width,height,fillColor,borderColor,edgeWidth) {
var alpha = false;
var c = this.context;
c.beginPath();
if (fillColor !== null && fillColor.a !== 1) {
c.globalAlpha = fillColor.a;
alpha = true;
}
if (fillColor !== null) {
c.fillStyle = _utils.colorOrString(fillColor);
c.fillRect(centre.x-(width/2),centre.y-(height/2),width,height);
}
if (alpha) c.globalAlpha = 1;
if (borderColor && borderColor.a !== 1) {
c.globalAlpha = borderColor.a;
alpha = true;
}
if (borderColor && borderColor !== null) {
c. lineWidth = edgeWidth || 1;
c.strokeStyle = _utils.colorOrString(borderColor);
c.rect(centre.x-(width/2),centre.y-(height/2),width,height);
c.stroke();
}
c.closePath();
if (alpha) c.globalAlpha = 1;
return;
},
/**
*<hr>
*Draws a closed shape using an array of points relative to its centre
*<br> N.B. Alpha (transparency) is only supported via a MC.Color parameter
*<br> 1 - either an MC.Color object or an HTML5 color name can be used as a Color
*<br> 2 - Omission of <tt>borderColor</tt> and <tt>edgeWidth</tt> parameters results in a filled shape (with no border)
*@param {MC.Point} centre - Draw centre of the shape
*@param {Array} pointsArray - Array of draw Points relative to the draw centre
*@param {MC.Color|string|null} fillColor - Shape fill color (1). <tt>null</tt> results in an non-filled shape
*@param {MC.Color|string} [borderColor] - Shape border line color (1)(2).
*@param {Number} [edgeWidth=1] - Shape border line width (2)
*
*/
array: function (centre,pointsArray,fillColor,borderColor,edgeWidth) {
var c = this.context;
var alpha = false;
var L = pointsArray.length;
c.save();
c.translate(centre.x,centre.y);
c.beginPath();
c.moveTo(pointsArray[0].x,pointsArray[0].y);
for (var i = 1; i < L ; i++) {
c.lineTo(pointsArray[i].x,pointsArray[i].y);
}
c.closePath();
if (fillColor !== null && fillColor.a !== 1) {
c.globalAlpha = fillColor.a;
alpha = true;
}
if (fillColor !== null) {
c.fillStyle = _utils.colorOrString(fillColor);
c.fill();
}
c.globalAlpha = 1;
if (borderColor && borderColor.a !== 1) {
c.globalAlpha = borderColor.a;
alpha = true;
}
if (borderColor && borderColor !== null) {
c. lineWidth = edgeWidth || 1;
c.strokeStyle = _utils.colorOrString(borderColor);
c.stroke();
}
c.restore();
if (alpha) c.globalAlpha = 1;
return;
},
/**
*<hr>
*Draws a regular polygon
*<br> N.B. Alpha (transparency) is only supported via a MC.Color parameter
*<br> 1 - either an MC.Color object or an HTML5 color name can be used as a Color
*<br> 2 - Omission of <tt>borderColor</tt> and <tt>edgeWidth</tt> parameters results in a filled polygon (with no border)
*@param {MC.Point} centre - Draw centre of the shape
*@param {Number} sides - Number of sides
*@param {Number} radius - Distance from centre to each vertex
*@param {Number} angle - In degrees (clockwise), 0 = first vertex drawn to the right of centre, 90 = below centre, etc
*@param {MC.Color|string|null} fillColor - Shape fill color (1). <tt>null</tt> results in an non-filled polygon
*@param {MC.Color|string} [borderColor] - Shape border line color (1)(2).
*@param {Number} [edgeWidth=1] - Shape border line width (2)
*/
poly: function (centre,sides,radius,angle,fillColor,borderColor,edgeWidth) {
_draw.array(centre,_utils.getPolyArray(sides,radius,angle),fillColor,borderColor,edgeWidth);
},
/**
*<hr>
*Draws a regular star
*<br> N.B. Alpha (transparency) is only supported via a MC.Color parameter
*<br> 1 - either an MC.Color object or an HTML5 color name can be used as a Color
*<br> 2 - Omission of <tt>borderColor</tt> and <tt>edgeWidth</tt> parameters results in a filled star (with no border)
*@param {MC.Point} centre - Draw centre of the star
*@param {Number} points - Number of points
*@param {Number} OuterRadius - Distance from centre to each point
*@param {Number} InnerRadius - Distance from centre to each inner point
*@param {Number} angle - In degrees (clockwise), 0 = first vertex drawn to the right of centre, 90 = below centre, etc
*@param {MC.Color|string|null} fillColor - Shape fill color (1). <tt>null</tt> results in an non-filled star
*@param {MC.Color|string} [borderColor] - Shape border line color (1)(2).
*@param {Number} [edgeWidth=1] - Shape border line width (2)
*/
star: function (centre,points,outerRadius,innerRadius,angle,fillColor,borderColor,edgeWidth) {
_draw.array(centre,_utils.getStarArray(points,outerRadius,innerRadius,angle),fillColor,borderColor,edgeWidth);
},
/**
*<hr>
*Draws an <tt>image</tt> on the canvas
*<br>This method mirrors (but does modify) a native <tt>canvas</tt> API call (specifically <tt>context.drawImage()</tt>) with the addition of an <tt>angle</tt> parameter
*<br>N.B. this method requires EXACTLY the first 3,5 or all 7 parameters.
*<br>(1) Only using <tt>(image,centre,angle)</tt> the image is drawn at 1:1 scale, centred and rotated about the <tt>centre</tt> point
*<br>(2) Adding the <tt>width,height</tt> pair scales the image to the appropriate size (in pixels)
*<br>(3) Adding the <tt>start,finish</tt> pair clips the image to a rectangle defined by <tt>start</tt> (top left corner) and <tt>finish</tt> (bottom right hand corner). Essential for using sprite sheets.
*<BR>(4) If any <tt>start</tt> or <tt>finish</tt> parameter is less or equal to 1, then that ratio of the native image width/height will be applied.
*<br>If a Gray Box with Red cross appears, then this method has not been called and the <tt>Picture</tt> object has drawn a Placeholder to represent an image that has not loaded.
*@param {Image} image - The JS <tt>Image()</tt> to be drawn (1)
*@param {MC.Point} centre - Canvas location of the centre of the image (1)
*@param {Number} angle - Angle (degrees) the image is to be rotated about the centre. 0 (zero) = no rotation (1)
*@param {Number} [width] - Width image is to be displayed with (before any rotation). If used then <tt>height</tt> must be specified (2)
*@param {Number} [height] - As per width, but image display height in pixels (2)
*@param {MC.Point} [start] - For displaying only a section of the image, start is a point representing the top left coordinates of the section ot be displayed. If <tt>start</tt> is specified then <tt>finish</tt> has to be too (3)(4)
*@param {MC.Point} [finish] - As per <tt>start</tt> above, except this is the bottom right of the selection to be displayed (3)(4)
*
*/
picture: function (image,centre,angle,width,height,start,finish) {
var c = this.context;
var DX = image.width;
var DY = image.height;
var offX = width / 2 || DX/2;
var offY = height/2 || DY/2;
c.save();
c.translate(centre.x,centre.y);
c.rotate(angle * _maths.TO_RADIANS);
c.translate(-offX,-offY);
if (arguments.length == 3) {
c.drawImage(image,0,0);
}
else if (arguments.length == 5) {
c.drawImage(image,0,0,width,height);
}
else if (arguments.length == 7) {
var x1 = start.x;
if (x1 <=1) x1 *= DX;
var y1 = start.y;
if (y1 <=1) y1 *= DY;
var x2 = finish.x;
if (x2 <=1) x2 *= DX;
var y2 = finish.y;
if (y2 <=1) y2 *= DY;
c.drawImage(image,x1,y1,x2 - x1,y2 - y1,0,0,width,height);
// context.drawImage(img,sx,sy,swidth,sheight,x,y,width,height);
}
c.restore();
},
/**
*<hr>
*Displays a warning marker (a 20x20 grey square with a red cross)
*<br>Used to indicate an <tt>Image()</tt> that is not loaded
*@param {MC.Point} position - Canvas location for the centre of the marker
*/
loadWarning: function (position) {
var pos = new _Point(position.x,position.y);
_draw.rectBasic(pos.clone(),20,20,"DarkGray");
_draw.line(pos.clone().add(-6,-6),pos.clone().add(6,6),"Red",4);
_draw.line(pos.clone().add(6,-6),pos.clone().add(-6,6),"Red",4);
},
/**
*<hr>
*Draws a 6 sided dice with the designated value
*<br> N.B. this is somewhat intensive method and is only recommended for when caught short without a dice image file !
*<br> N.B. Alpha (transparency) is only supported via a MC.Color parameter
*<br> 1 - either an MC.Color object or an HTML5 color name can be used as a Color
*@param {MC.Point} centre - Centre of the dice
*@param {Number} size - Height and width of the dice
*@param {Number} value - Dice value (clamped to 1-6)
*@param {MC.Color|string} dieColor - Dice color (1)
*@param {MC.Color|string} pipColor - Color of dice pips (1)
*/
d6: function (centre,size,value,dieColor,pipColor)
{
var c = this.context;
var D = new _Point(centre.x,centre.y); // allows non-point items with x: and y: properties to be used
var sml = size / 8;
var lrg = size * (3/8);
_draw.circle(D.clone().add(-lrg,-lrg),sml,"Black");
_draw.circle(D.clone().add(lrg,-lrg),sml,"Black");
_draw.circle(D.clone().add(-lrg,lrg),sml, "Black");
_draw.circle(D.clone().add(lrg,lrg),sml, "Black");
c.fillStyle = "Black";
c.fillRect(centre.x-lrg-1,centre.y-(size/2)-1,2*lrg+2, size+2);
c.fillRect(centre.x-(size/2)-1,centre.y-lrg-1, size+2, 2*lrg+2);
_draw.circle(D.clone().add(-lrg+1,-lrg+1),sml, dieColor);
_draw.circle(D.clone().add(lrg-1, -lrg+1),sml, dieColor);
_draw.circle(D.clone().add(-lrg+1, lrg-1),sml, dieColor);
_draw.circle(D.clone().add(lrg-1, lrg-1),sml, dieColor);
c.fillStyle = _utils.colorOrString(dieColor);
c.fillRect(centre.x-lrg,centre.y-(size/2)+1,2*lrg, size-2);
c.fillRect(centre.x-(size/2)+1,centre.y-lrg, size-2, 2*lrg);
value = ~~_maths.clamp(value,1,6);
switch (value)
{
case 1:
_draw.circle(D,sml-1.5, pipColor);
break;
case 2:
_draw.circle(D.clone().add(size/4, -size/4),sml-1.5, pipColor);
_draw.circle(D.clone().add(-size /4, size/4),sml-1.5, pipColor);
break;
case 3:
_draw.circle(D,sml-1.5, pipColor);
_draw.circle(D.clone().add(-size/4, -size/4),sml-1.5, pipColor);
_draw.circle(D.clone().add(size /4, size/4),sml-1.5, pipColor);
break;
case 4:
_draw.circle(D.clone().add(-size/4, -size/4),sml-1.5, pipColor);
_draw.circle(D.clone().add(size /4, size/4),sml-1.5, pipColor);
_draw.circle(D.clone().add(-size/4, size/4),sml-1.5, pipColor);
_draw.circle(D.clone().add(size /4, -size/4),sml-1.5, pipColor);
break;
case 5:
_draw.circle(D,sml-1.5, pipColor);
_draw.circle(D.clone().add(-size/4, -size/4),sml-1.5, pipColor);
_draw.circle(D.clone().add(size /4, size/4),sml-1.5, pipColor);
_draw.circle(D.clone().add(-size/4, size/4),sml-1.5, pipColor);
_draw.circle(D.clone().add(size /4, -size/4),sml-1.5, pipColor);
break;
case 6:
_draw.circle(D.clone().add(-size/4, -size/4),sml-1.5, pipColor);
_draw.circle(D.clone().add(size /4, size/4),sml-1.5, pipColor);
_draw.circle(D.clone().add(-size/4, size/4),sml-1.5, pipColor);
_draw.circle(D.clone().add(size /4, -size/4),sml-1.5, pipColor);
_draw.circle(D.clone().add(-size/4, 0),sml-1.5, pipColor);
_draw.circle(D.clone().add(size /4, 0),sml-1.5, pipColor);
break;
default:
}
return;
},
/**
*<hr>
*Renders text to the canvas
*<br>All such text is drawn with the bottom left corner of the text appearing at the screen coordinates defined by the position MC.Point
*<br><b>Web Safe Fonts</b> Use these to ensure multi-browser support
*<br>Arial, Helvetica, Time New Roman, Times, Courier New, Courier, Verdana, Georgia, Palatino, Garamond, Bookman, Comic Sans MS, Trebuchet MS,Arial Black, Impact
*@param {String} text The text string to be rendered
*@param {MC.Point} position Screen coordinates of the bottom left of the text
*@param {MC.Typeface|MC.Color|String} [typeface = MC.game.text] Desired Typeface (font, size and color) .. otherwise <tt>MC.game.text</tt> is used
*<br>OR if a Color object is used the <tt>MC.game.text</tt> default font and size will be used with the desired color
*<br>OR a HTML5 color string name can be used instead (with <tt>MC.game.text defined font and size)
*@example
*var myTypeface = new MC.Typeface("Comic Sans MS",25,MC.utils.randomColor());
*var pos = new MC.Point(100,120);
*
*MC.draw.text("Hello, World", pos, myTypeface);
*/
// Thanks to https://websitesetup.org/web-safe-fonts-html-css/ (Dec 2018)
text: function text (text,position,typeface) {
var c = this.context;
var alpha = false;
var t;
if (typeface === undefined) {
t = _game.text;
}
else if (typeface instanceof MC.Typeface) {
t = typeface;
}
else {
t = new _Typeface (_game.text.font,_game.text.size,typeface);
}
if (t.color instanceof MC.Color && t.color.a !== 1) {
alpha = true;
c.globalAlpha = t.color.a;
}
c.font = t.log();
c.fillStyle = _utils.colorOrString(t.color);
c.fillText(text,position.x,position.y);
if (alpha) {
c.globalAlpha = 1;
}
},
/**
*<hr>
*Uses the HTML5 canvas method of <tt>measureText()</tt> to establish the best, centralised, fit for a piece of text in a box
*<br>Used internally by the <tt>MC.ConBox</tt> object
*@param {String} text to be fitted
*@param {Number} width of the box
*@param {Number} height of the box
*@param {String} font font to be used in the text fit
*@returns {Object} An Object comprising of {offset: {MC.Point}, size : {Number}}
*/
textFit: function (text,width,height,font) {
var c = this.context;
var s = height +1;
var len = 0;
do {
s--;
c.font = s+"px "+font;
len = c.measureText(text).width;
}
while (len > width);
// MAGIC NUMBER ALERT dividing size by 2.6 gets better results
var ret = {offset: new _Point(-len/2,s/2.6), size: s};
return ret;
}
}; // End of DRAW
////////////////////
// UTILITIES //
////////////////////
/**
*The utils (utilities) object provides internal engine methods.
*<br>Developers may find them useful.
*@interface
*
*/
MC.utils= {
/**
*<hr>
*Checks if a "color" is a string (i.e. HTML5 color name) or a {Color} object
*<br>Returns either the HTML5 string or the {Color} as a "rgb(r,g,b)" string
*@param {string|MC.Color} color
*@returns {string}
*/
colorOrString: function (color) {
// https://stackoverflow.com/questions/4059147/check-if-a-variable-is-a-string-in-javascript (June 2018)
if (typeof color === "string" || color instanceof String) return color;
else {return color.rgb();}
},
/**
*<hr>
*Checks if the entered items has x: and y: properties
*<br>Used as confirmation and to assist Method Polymorphism
*@param {anything} item
*@returns {Boolean} True if both <tt>item.x</tt> and <tt>item.y</tt> exist; else False
*/
hasXY: function(item) {
return (item.hasOwnProperty("x") && item.hasOwnProperty("y"));
},
/**
*<hr>
*Checks if an item is a number
*@param {anything} n
*@returns {Boolean} True if <tt>n</tt> is a number; else False
*/
isNumeric: function(n) {
return !isNaN(parseFloat(n)) && isFinite(n);
// thanks to http://stackoverflow.com/questions/9716468/is-there-any-function-like-isnumeric-in-javascript-to-validate-numbers (May 2017)
},
/**
*<hr>
*Shuffles an array
*@param {Array} o - Array to be scrambled
*@returns {Array} The same array, just in a different order
*/
shuffle: function(o) {
for(var j, x, i = o.length; i; j = Math.floor(Math.random() * i), x = o[--i], o[i] = o[j], o[j] = x);
return o;
},
/**
*<hr>
* Mixes two MC Color objects and returns a NEW Color object
* <br> N.B. alpha (<tt>Color.a</tt>) is also mixed
* @param {MC.Color} color1 - First Color
* @param {MC.Color} color2 - Second Color
* @param {Number} ratio - Ratio of how much travel required from first color to second (as a decimal 0-1)
* @returns {MC.Color} a NEW color object
*/
mix: function (color1,color2, ratio) {
return new _Color(_maths.clamp(~~(color1.r + ((color2.r - color1.r)*ratio)),0,255),
_maths.clamp(~~(color1.g + ((color2.g - color1.g)*ratio)),0,255),
_maths.clamp(~~(color1.b + ((color2.b - color1.b)*ratio)),0,255),
_maths.clamp(color1.a + ((color2.a - color1.a)*ratio),0,1) );
},
/**
*Creates an array of points representing a regular polygon's vertices about a central point
*<br>Used internally by various MC.draw methods (esp. <tt>MC.draw.poly()</tt>)
*@param {Number} sides - # of sides to the polygon
*@param {Number} radius - Distance from Poly centre to each vertex
*@param {Number} angle - Angle (in degrees) of first vertex. 0 = right of centre, 90 = below centre etc.
*@returns {Array.<MC.Point>} Array of <tt>MC.Point</tt> objects
*
*/
getPolyArray: function (sides, radius, angle) {
var arr = [];
arr[0] = new _Point(radius,0).rotate(angle);
for (var i = 1; i < sides; i++) arr.push(arr[0].clone().rotate(i * (360/sides)));
return arr;
},
/**
*Creates an array of points representing a regular star's vertices about a central point
*<br>Used internally by various MC.draw methods (esp. <tt>MC.draw.star()</tt>)
*@param {Number} sides - # of points to the star
*@param {Number} outerRadius - Distance from star centre to each point
*@param {Number} innerRadius - Distance from centre to inner points
*@param {Number} angle - Angle (in degrees) of first point. 0 = right of centre, 90 = below centre etc.
*@returns {Array.<MC.Point>} Array of <tt>MC.Point</tt> objects
*
*/
getStarArray: function (points, outerRadius, innerRadius, angle) {
var arr = [];
var p1 = new _Point(outerRadius,0).rotate(angle);
var p2 = new _Point(innerRadius,0).rotate(angle + (180/points));
for (var i = 0; i < points; i++) {
arr.push(p1.clone().rotate(i * (360/points)));
arr.push(p2.clone().rotate(i * (360/points)));
}
return arr;
},
/**
*Creates an array of points representing a rectangles vertices around a central point
*<br>Used internally by various MC methods
*@param {Number} width - Rectangle width
*@param {Number} height - Rectangle height
*@param {Number} angle - Angle (in degrees) of the rectangle rotation about centre. 0 = axis aligned, positive = clockwise
*@returns {Array.<MC.Point>} Array of <tt>MC.Point</tt> objects
*
*/
getRectArray: function (width, height, angle) {
var op = new _Point(width/2,height/2);
var arr = [op.clone().rotate(angle),
op.clone().times(-1,1).rotate(angle),
op.clone().times(-1,-1).rotate(angle),
op.clone().times(1,-1).rotate(angle) ];
return arr;
},
// NOT CURRENTLY IMPLEMENTED
blankCanvas: function (width,height) {
var MyNewCanvas = document.createElement("canvas");
MyNewCanvas.width = width;
MyNewCanvas.height = height;
return MyNewCanvas;
},
/**
*<hr>
*Accepts an angle (in degrees) and returns it's value in the range of 0-360
*@param {Number} angle - Angle to be fixed
*@returns {Number} Same angle within range 0-360
*@throws Returns original parameter if it is non-numeric
*/
fixAngle: function(angle) {
var out = angle;
if (_utils.isNumeric(out))
{
out = out % 360;
if (out < 0) out += 360;
}
return out;
},
/**
*<hr>
*Creates a random opaque Color Object.
*@returns a <tt>new</tt> {MC.Color}
*/
randomColor: function() {
return new _Color(_maths.randBetween(0,255),_maths.randBetween(0,255),_maths.randBetween(0,255),1);
},
/**
*<hr>
*Checks if an object is valid candidate for a hitTest, Specifically:
*<br>1. is a MC.Sprite
*<br>2. has <tt>collider === true</tt>
*<br>3. is <tt>dead === false</tt>
*<br>Used Internally an a TARGET by the MC Engine, recommended that the Developer call it on any Object prior to attempting to make it do a <tt>hitTest()</tt> call.
*@param {Object} object1 (ideally MC.Sprite) to be tested
*@param {Object} [object2] Optional second Object to test
*@returns {Boolean} <tt>True</tt> if all parameter Objects are valid hitTest candidate(s)
*@throws Console warning if invalid
*/
validHT: function(object1,object2) {
// Internal Helper Function
var spriteTest = function(o) {
if (o instanceof MC.Sprite) {
if (o.collider === true && o.dead !== true) {
return true;
}
}
return false;
}
// Actual method
if (arguments.length == 1) {
return spriteTest(object1);
}
else if (arguments.length == 2) {
if (spriteTest(object1) && spriteTest(object2)) {
return true;
}
}
else {
return false;
}
},
/**
*<hr>
*Checks if 2 passed Sprite Bounding Boxes intersect (indicating a possible hit) OR whether a point is inside a Bounding Box
*<br>Used internally by the MC Engine
*@param {Object} bb1 First MC.Sprite.boundingBox
*@param {Object|MC.Point} bb2 Second MC.Sprite.boundingBox OR an MC.Point
*@returns {Boolean} <tt>True</tt> if the two boxes (or single box and point) intersect
*/
checkBB: function(bb1, bb2) {
// Point in Boundingbox
if (bb2 instanceof MC.Sprite) {
if (bb2.x > bb1.r || bb2.x < bb1.l || bb2.y > bb1.b || bb2.y < bb1.t) {
return false;
}
return true;
}
// Boundingbox on BoundingBox
if (bb1.l > bb2.r || bb1.r < bb2.l || bb1.t > bb2.b || bb1.b < bb2.t) {
return false;
}
return true;
},
/**
*<hr>
*Checks if a point coordinate is within the bounds defined by an Array of 3 further points
*<br>Used internally by the MC engine.
*@param {MC.Point} Point to be tested
*@param {Array.<MC.Point>} Array defining bounds of the triangle
*@returns {Boolean} <tt>true</tt> if the point lies within the triangle, else <tt>false</tt>
*/
pointInTri: function (Point, Array) {
// WARNING: Here be dragons !! ;p
var c1 = 0; var c2 = 0; var test1; var test2;
var D = Point.clone().minus(Array[0]);
var V = Array[1].clone().minus(Array[0]);
test1 = V.isLeft(D); test2 = V.isRight(D);
if (test1 && !test2) { c1++;}
else if (!test1 && test2) {c2++;}
else {c1++; c2++;}
D = Point.clone().minus(Array[1]);
V = Array[2].clone().minus(Array[1]);
test1 = V.isLeft(D); test2 = V.isRight(D);
if (test1 && !test2) { c1++;}
else if (!test1 && test2) {c2++;}
else {c1++; c2++;}
D = Point.clone().minus(Array[2]);
V = Array[0].clone().minus(Array[2]);
test1 = V.isLeft(D); test2 = V.isRight(D);
if (test1 && !test2) { c1++;}
else if (!test1 && test2) {c2++;}
else {c1++; c2++;}
if (c1 == 3 || c2 == 3) {return true;} else {return false;}
},
/**
*<hr>
*Checks if a point coordinate is within a circle defined by centre (point) and radius
*@param {MC.Point} Point to be tested
*@param {MC.Point} Centre of circle to be tested against
*@param {Number} Radius of circle
*@returns {Boolean} <tt>true</tt> if the point is within (or on the perimeter of) the circle, else <tt>false</tt>
*/
pointInCirc: function (Point,Centre,Radius) {
return _maths.rangeSqr(Point,Centre) <= Radius * Radius;
},
/**
*<hr>
*Checks if a point coordinate is within a rectangle defined by an array of points
*@param {MC.Point} Point to be tested
*@param {Array.<MC.Point>} Corners of the Rectangle
*<br> winding direction does not matter but points should be ordered as if going around the perimeter of the rectangle
*@returns {Boolean} <tt>true</tt> if the point is within (or on the perimeter of) the rectangle, else <tt>false</tt>
*@throws Console warning if Corners Array does not have 4 members.
*/
pointInRect: function (Point, Corners) {
if (Corners.length != 4) {
console.log("WARNING: wrong Array length passed to MC.utils.pointInRect()");
return false;
}
if (_utils.pointInTri(Point,new Array (Corners[0],Corners[1],Corners[3]))) {
return true;
}
return _utils.pointInTri(Point,new Array (Corners[2],Corners[1],Corners[3]));
},
/**
*<hr>
*Checks if a point coordinate lies within the shape defined by a second array of points
*<br>This only produces reliable results with CONVEX shapes.
*@param {MC.Point} Point to be tested
*@param {Array.<MC.Point>} Array of points defining a shape to be tested against
*<br> winding direction does not matter but points should be ordered as if going around the perimeter of the shape
*@returns {Boolean} <tt>true</tt> if the point is within (or on the perimeter of) the shape, else <tt>false</tt>
*/
pointInShape: function (Point,Array) {
var arr = [3]; var len = Array.length;
var x = 0; var y = 0;
arr[0] = new _Point(); // mid point
for (var i = 0 ; i < len ; i++) {
arr[0].add(Array[i]);
}
arr[0].div(len);
for (var i = 0; i < len ; i++) {
arr[1] = Array[i];
if (i == len -1) {arr[2] = Array[0]; } else {arr[2] = Array[i+1];}
if (_utils.pointInTri(Point,arr)) { return true;}
}
return false;
},
/**
*<hr>
*Checks if any of an Array of point coordinates lies within the shape defined by a second array of points (and vice versa)
*<br>This only produces reliable results with shapes when each line between the vertices and centre is internal
*@param {Array.<MC.Point>} First array of points defining a shape to be tested
*@param {Array.<MC.Point>} Second array of points defining another shape to be tested
*<br> winding direction does not matter but points should be ordered as if going around the perimeter of the shapes
*@returns {Boolean} <tt>true</tt> if any of the points is within (or on the perimeter of) the other shape, else <tt>false</tt>
*/
shapeInShape: function (First,Second) {
var len1 = First.length; var len2 = Second.length;
for (var i = 0; i < len1 ; i++) {
if (_utils.pointInShape(First[i],Second)) { return true;} }
for (var i = 0; i < len2 ; i++) {
if (_utils.pointInShape(Second[i],First)) { return true;} }
return false;
},
// TO DO: gonna need to leverage line intersection too
// http://thirdpartyninjas.com/blog/2008/10/07/line-segment-intersection/
// https://bl.ocks.org/1wheel/464141fe9b940153e636
/**
*<hr>
*Checks is any of the vertices of a shape (defined by an array of MC.Point's) are within or touching a circle
*@param {Array.<MC.Point>} Shape points array to be tested
*@param {MC.Point} Centre of the circle to be tested against
*@param {Number} Radius of the circle
*@returns {Boolean} <tt>True</tt> if any of the points intersect the circle, else <tt>false</tt>
*/
shapeInCircle: function (Shape,Centre,Radius) {
// pointInCirc: function (Point,Centre,Radius)
var len = Shape.length;
for (var i = 0; i < len ; i++)
{
if (_utils.pointInCirc(Shape[i],Centre,Radius)) return true;
}
return false;
},
/**
*<hr>
*Checks if 2 circles intersect
*@param {MC.Point} Centre1 centre of the first circle
*@param {Number} Radius1 radius of the first circle
*@param {MC.Point} Centre2 centre of the second circle
*@param {Number} Radius2 radius of the second circle
*@returns {Boolean} <tt>true</tt> if the circle intersect, else <tt>false</tt>
*/
circInCircle: function (Centre1,Radius1,Centre2,Radius2) {
return _maths.rangeSqr(Centre1,Centre2) <= ((Radius1 + Radius2) * (Radius1 + Radius2));
},
/**
*<hr>
*copies the object using the native JS <tt>Object.assign()</tt> method
*@param {Object} thing What is to be copied
*@returns {Object} a <tt>new</tt> copt of the object
*CAUTION: only sure to work with enumerable items. May(will) not work 100% for nested elements within the object
*/
// Thanks to https://medium.com/@Farzad_YZ/3-ways-to-clone-objects-in-javascript-f752d148054d (Jan 2019)
// https://scotch.io/bar-talk/copying-objects-in-javascript (Jan 2019)
// https://stackoverflow.com/questions/122102/what-is-the-most-efficient-way-to-deep-clone-an-object-in-javascript (Jan 2019)
cloneWithAssign: function (thing) {
return Object.assign({},thing);
}
}; // End of UTILITIES
////////////////////
// MOUSE //
////////////////////
/**
*<hr>
*The <tt>MC.mouse</tt> object contains all mouse related variables.
*<br>Upon initialisation it sets up document event listeners for <tt>mousemove, mouseup, mousedown & wheel</tt>
*<br>The following functions are defined, but empty, for Developers to customise (see Example)
*<br>(a) <tt>onClick() & onClickUp()</tt> fired when ANY mouse button is depressed or released
*<br>(b) <tt>onClickL() & onClickUpL()</tt> as above, Left Mouse Button only
*<br>(c) <tt>onClickM() & onClickUpM()</tt> as above, Middle Mouse Button only
*<br>(d) <tt>onClickR() & onClickUpR()</tt> as above, Right Mouse Button only. <br>CAUTION: most browsers have build in RH button features
*<br>(e) <tt>wheelUp() & wheelDown()</tt> fires when the mouse wheel is scrolled. <br>CAUTION: works for Chrome, support varies for other browsers
*@property {MC.Point} pos Current mouse screen coordinates
*@property {MC.Point} clickStart Screen coordinates of ANY mouse button down click
*@property {MC.Point} clickFinish Screen coordinates of ANY mouse button release
*@property {Boolean} mouseDown <tt>true</tt> if ANY mouse button is currently depressed, else <tt>false</tt>
*@property {MC.Point} clickStartL Screen coordinates of the LEFT mouse button down click
*@property {MC.Point} clickFinishL Screen coordinates of the LEFT mouse button release
*@property {Boolean} mouseDownL <tt>true</tt> if LEFT mouse button is currently depressed, else <tt>false</tt>
*@property {MC.Point} clickStartM Screen coordinates of the MIDDLE mouse button down click
*@property {MC.Point} clickFinishM Screen coordinates of the MIDDLE button release
*@property {Boolean} mouseDownL <tt>true</tt> if MIDDLE mouse button is currently depressed, else <tt>false</tt>
*@property {MC.Point} clickStartR Screen coordinates of the RIGHT mouse button down click
*@property {MC.Point} clickFinishR Screen coordinates of the RIGHT button release
*@property {Boolean} mouseDownL <tt>true</tt> if RIGHT mouse button is currently depressed, else <tt>false</tt>
*@interface
*@example
*MC.mouse.onClickL = function () {
* console.log("Left Mouse Clicked");
* mySprite.moveTo(MC.mouse.clickStartL);
*};
*
*MC.mouse.wheelUp = function () {
* if (mySprite.hit(MC.mouse.pos)) {
* mySprite.scale += 0.2;
* }
*};
*/
MC.mouse = {
/**
*<hr>
*Used to Initialise the MC.mouse object, sets up properties and event listeners
*<br>Called automatically by the <tt>MC.game.init()</tt> method
*/
init: function () {
this.lastPos = new MC.Point();
this.pos = new MC.Point();
this.clickStart = new MC.Point();
this.clickFinish = new MC.Point();
this.clickStartL = new MC.Point();
this.clickFinishL = new MC.Point();
this.clickStartM = new MC.Point();
this.clickFinishM = new MC.Point();
this.clickStartR = new MC.Point();
this.clickFinishR = new MC.Point();
window.addEventListener("mousemove", getMouse, false); // MOUSE MOVE
function getMouse(event) {
_mouse.lastPos.set(_mouse.pos);
_mouse.pos.x = _game.mouse.x = Math.round(event.x - canvas.offsetLeft + document.body.scrollLeft + document.documentElement.scrollLeft);
_mouse.pos.y = _game.mouse.y = Math.round(event.y - canvas.offsetTop + document.body.scrollTop + document.documentElement.scrollTop);
}
window.addEventListener("mousedown", mouseDown, false);
function mouseDown(event) {
_mouse.mouseDown = true;
_mouse.clickStart.set(_mouse.pos);
_mouse.onClick();
switch (event.button) {
case 0:
_mouse.mouseDownL = true;
_mouse.clickStartL.set(_mouse.pos);
_mouse.onClickL();
break;
case 1:
_mouse.mouseDownM = true;
_mouse.clickStartM.set(_mouse.pos);
_mouse.onClickM();
break;
case 2:
_mouse.mouseDownR = true;
_mouse.clickStartR.set(_mouse.pos);
_mouse.onClickR();
break;
default:
break;
}
}
window.addEventListener("mouseup", mouseUp, false);
function mouseUp (event) {
_mouse.mouseDown = false;
_mouse.clickFinish.set(_mouse.pos);
_mouse.onClickUp();
switch (event.button) {
case 0:
_mouse.mouseDownL = false;
_mouse.clickFinishL.set(_mouse.pos);
_mouse.onClickUpL();
break;
case 1:
_mouse.mouseDownM = false;
_mouse.clickFinishM.set(_mouse.pos);
_mouse.onClickUpM();
break;
case 2:
_mouse.mouseDownR = false;
_mouse.clickFinishR.set(_mouse.pos);
_mouse.onClickUpR();
break;
default:
break;
}
}
window.addEventListener("wheel",wheel,false);
// further research required https://stackoverflow.com/questions/14926366/mousewheel-event-in-modern-browsers (Dec 2018)
function wheel (event) {
if (event.deltaY < 0) {
_mouse.wheelUp();
}
else if (event.deltaY > 0) {
_mouse.wheelDown();
}
}
},
lastPos: null,
pos: null,
clickStart: null,
clickFinish: null,
mouseDown: false,
onClick: function () { },
onClickUp: function () { },
clickStartL: null,
clickFinishL: null,
mouseDownL: false,
onClickL: function () { },
onClickUpL: function () { },
clickStartM: null,
clickFinishM: null,
mouseDownM: false,
onClickM: function () { },
onClickUpM: function () { },
clickStartR: null,
clickFinishR: null,
mouseDownR: false,
onClickR: function () { },
onClickUpR: function () { },
wheelUp: function () { },
wheelDown: function () { }
}; // End of MOUSE
////////////////////
// KEYS //
////////////////////
/**
*<hr>
*The <tt>MC.keys</tt> object contains all key related variables.
*<br>Upon initialisation it sets up document event listeners for <tt>keyup & keydown</tt> events
*<br>Each supported key has a <tt>MC.keys.#</tt> member object where the # is the key name
*<br>Within this are: <hr>
*<br> (1) down {Boolean} <tt>true</tt> if the key is currently depressed, else <tt>false</tt>
*<br> (2) onDown {function} A placeholder function for Developer customisation, called when the key is depressed
*<br> (3) onUp {function} A placeholder function for Developer customisation, called when the key is released <hr>
*<br>The following keys are supported by the MaxCanvas engine:
*<br>N.B. these names are to be used exactly as typed here
*<br><b>Misc: </b><tt>back, tab, enter, shift, ctrl, caps, esc, space, pageUp, pageDown, end, home, leftArrow, upArrow, rightArrow, downArrow, del</tt> (N.B. if a choice between left and right, the left is supported. e.g. the left shift key is supported, the right is not)
*<br><b>Numbers: </b><tt>_0,_1,_2,_3,_4,_5,_6,_7,_8,_9</tt> (N.B. There are prefixed with an underscore to comply with JS naming rules)
*<br><b>Letters: </b><tt>a,b,c,d,e,f,g,h,i,j,k,l,m,n,o,p,q,r,s,t,u,v,w,x,y,z</tt>
*<br><b>NumberPad: </b><tt>num0,num1,num2,num3,num4,num5,num6,num7,num8,num9</tt>
*<br>Other keys are not supported as they may vary based upon international keyboard layouts or have possible browser pre-sets (e.g. F5 is refresh)
*@interface
*@example
*MC.keys.p.onDown = function () {
* myPrintFunction();
*};
*
*MC.keys.space.onUp = function () {
* if (gun.status == "ready") {
* gun.fire();
* }
*};
*
*MC.keys.m.onDown = function () {
* if (MC.keys.ctrl.down) {
* console.log("MaxCanvas is great!!");
* }
*};
*
*/
MC.keys = {
/**
*<hr>
*Init initialises the <tt>MC.key</tt> object, by setting up the keyboard event handlers and pointing them at the different key property objects
*<br>Called automatically during the <tt>MC.init()</tt> call. There should be no need for a Developer to use it.
*/
init: function() {
window.addEventListener("keyup", keyUp,false);
function keyUp(event) {
switch (event.keyCode) {
case 8: _keys.back.down = false; _keys.back.onUp(); break;
case 9: _keys.tab.down = false; _keys.tab.onUp(); break;
case 13: _keys.enter.down = false; _keys.enter.onUp(); break;
case 16: _keys.shift.down = false; _keys.shift.onUp(); break;
case 17: _keys.ctrl.down = false; _keys.ctrl.onUp(); break;
case 18: _keys.alt.down = false; _keys.alt.onUp(); break;
case 20: _keys.caps.down = false; _keys.caps.onUp(); break;
case 27: _keys.esc.down = false; _keys.esc.onUp(); break;
case 32: _keys.space.down = false; _keys.space.onUp(); break;
case 33: _keys.pageUp.down = false; _keys.pageUp.onUp(); break;
case 34: _keys.pageDown.down = false; _keys.pageDown.onUp(); break;
case 35: _keys.end.down = false; _keys.end.onUp(); break;
case 36: _keys.home.down = false; _keys.home.onUp(); break;
case 37: _keys.leftArrow.down = false; _keys.leftArrow.onUp(); break;
case 38: _keys.upArrow.down = false; _keys.upArrow.onUp(); break;
case 39: _keys.rightArrow.down = false; _keys.rightArrow.onUp(); break;
case 40: _keys.downArrow.down = false; _keys.downArrow.onUp(); break;
case 46: _keys.del.down = false; _keys.del.onUp(); break;
case 48: _keys._0.down = false; _keys._0.onUp(); break;
case 49: _keys._1.down = false; _keys._1.onUp(); break;
case 50: _keys._2.down = false; _keys._2.onUp(); break;
case 51: _keys._3.down = false; _keys._3.onUp(); break;
case 52: _keys._4.down = false; _keys._4.onUp(); break;
case 53: _keys._5.down = false; _keys._5.onUp(); break;
case 54: _keys._6.down = false; _keys._6.onUp(); break;
case 55: _keys._7.down = false; _keys._7.onUp(); break;
case 56: _keys._8.down = false; _keys._8.onUp(); break;
case 57: _keys._9.down = false; _keys._9.onUp(); break;
case 65: _keys.a.down = false; _keys.a.onUp(); break;
case 66: _keys.b.down = false; _keys.b.onUp(); break;
case 67: _keys.c.down = false; _keys.c.onUp(); break;
case 68: _keys.d.down = false; _keys.d.onUp(); break;
case 69: _keys.e.down = false; _keys.e.onUp(); break;
case 70: _keys.f.down = false; _keys.f.onUp(); break;
case 71: _keys.g.down = false; _keys.g.onUp(); break;
case 72: _keys.h.down = false; _keys.h.onUp(); break;
case 73: _keys.i.down = false; _keys.i.onUp(); break;
case 74: _keys.j.down = false; _keys.j.onUp(); break;
case 75: _keys.k.down = false; _keys.k.onUp(); break;
case 76: _keys.l.down = false; _keys.l.onUp(); break;
case 77: _keys.m.down = false; _keys.m.onUp(); break;
case 78: _keys.n.down = false; _keys.n.onUp(); break;
case 79: _keys.o.down = false; _keys.o.onUp(); break;
case 80: _keys.p.down = false; _keys.p.onUp(); break;
case 81: _keys.q.down = false; _keys.q.onUp(); break;
case 82: _keys.r.down = false; _keys.r.onUp(); break;
case 83: _keys.s.down = false; _keys.s.onUp(); break;
case 84: _keys.t.down = false; _keys.t.onUp(); break;
case 85: _keys.u.down = false; _keys.u.onUp(); break;
case 86: _keys.v.down = false; _keys.v.onUp(); break;
case 87: _keys.w.down = false; _keys.w.onUp(); break;
case 88: _keys.x.down = false; _keys.x.onUp(); break;
case 89: _keys.y.down = false; _keys.y.onUp(); break;
case 90: _keys.z.down = false; _keys.z.onUp(); break;
case 96: _keys.num0.down = false; _keys.num0.onUp(); break;
case 97: _keys.num1.down = false; _keys.num1.onUp(); break;
case 98: _keys.num2.down = false; _keys.num2.onUp(); break;
case 99: _keys.num3.down = false; _keys.num3.onUp(); break;
case 100: _keys.num4.down = false; _keys.num4.onUp(); break;
case 101: _keys.num5.down = false; _keys.num5.onUp(); break;
case 102: _keys.num6.down = false; _keys.num6.onUp(); break;
case 103: _keys.num7.down = false; _keys.num7.onUp(); break;
case 104: _keys.num8.down = false; _keys.num8.onUp(); break;
case 105: _keys.num9.down = false; _keys.num9.onUp(); break;
default: break;
}
}
window.addEventListener("keydown", keyDown,false);
function keyDown(event) {
switch (event.keyCode) {
case 8: _keys.back.down = true; _keys.back.onDown(); break;
case 9: _keys.tab.down = true; _keys.tab.onDown(); break;
case 13: _keys.enter.down = true; _keys.enter.onDown(); break;
case 16: _keys.shift.down = true; _keys.shift.onDown(); break;
case 17: _keys.ctrl.down = true; _keys.ctrl.onDown(); break;
case 18: _keys.alt.down = true; _keys.alt.onDown(); break;
case 20: _keys.caps.down = true; _keys.caps.onDown(); break;
case 27: _keys.esc.down = true; _keys.esc.onDown(); break;
case 32: _keys.space.down = true; _keys.space.onDown(); break;
case 33: _keys.pageUp.down = true; _keys.pageUp.onDown(); break;
case 34: _keys.pageDown.down = true; _keys.pageDown.onDown(); break;
case 35: _keys.end.down = true; _keys.end.onDown(); break;
case 36: _keys.home.down = true; _keys.home.onDown(); break;
case 37: _keys.leftArrow.down = true; _keys.leftArrow.onDown(); break;
case 38: _keys.upArrow.down = true; _keys.upArrow.onDown(); break;
case 39: _keys.rightArrow.down = true; _keys.rightArrow.onDown(); break;
case 40: _keys.downArrow.down = true; _keys.downArrow.onDown(); break;
case 46: _keys.del.down = true; _keys.del.onDown(); break;
case 48: _keys._0.down = true; _keys._0.onDown(); break;
case 49: _keys._1.down = true; _keys._1.onDown(); break;
case 50: _keys._2.down = true; _keys._2.onDown(); break;
case 51: _keys._3.down = true; _keys._3.onDown(); break;
case 52: _keys._4.down = true; _keys._4.onDown(); break;
case 53: _keys._5.down = true; _keys._5.onDown(); break;
case 54: _keys._6.down = true; _keys._6.onDown(); break;
case 55: _keys._7.down = true; _keys._7.onDown(); break;
case 56: _keys._8.down = true; _keys._8.onDown(); break;
case 57: _keys._9.down = true; _keys._9.onDown(); break;
case 65: _keys.a.down = true; _keys.a.onDown(); break;
case 66: _keys.b.down = true; _keys.b.onDown(); break;
case 67: _keys.c.down = true; _keys.c.onDown(); break;
case 68: _keys.d.down = true; _keys.d.onDown(); break;
case 69: _keys.e.down = true; _keys.e.onDown(); break;
case 70: _keys.f.down = true; _keys.f.onDown(); break;
case 71: _keys.g.down = true; _keys.g.onDown(); break;
case 72: _keys.h.down = true; _keys.h.onDown(); break;
case 73: _keys.i.down = true; _keys.i.onDown(); break;
case 74: _keys.j.down = true; _keys.j.onDown(); break;
case 75: _keys.k.down = true; _keys.k.onDown(); break;
case 76: _keys.l.down = true; _keys.l.onDown(); break;
case 77: _keys.m.down = true; _keys.m.onDown(); break;
case 78: _keys.n.down = true; _keys.n.onDown(); break;
case 79: _keys.o.down = true; _keys.o.onDown(); break;
case 80: _keys.p.down = true; _keys.p.onDown(); break;
case 81: _keys.q.down = true; _keys.q.onDown(); break;
case 82: _keys.r.down = true; _keys.r.onDown(); break;
case 83: _keys.s.down = true; _keys.s.onDown(); break;
case 84: _keys.t.down = true; _keys.t.onDown(); break;
case 85: _keys.u.down = true; _keys.u.onDown(); break;
case 86: _keys.v.down = true; _keys.v.onDown(); break;
case 87: _keys.w.down = true; _keys.w.onDown(); break;
case 88: _keys.x.down = true; _keys.x.onDown(); break;
case 89: _keys.y.down = true; _keys.y.onDown(); break;
case 90: _keys.z.down = true; _keys.z.onDown(); break;
case 96: _keys.num0.down = true; _keys.num0.onDown(); break;
case 97: _keys.num1.down = true; _keys.num1.onDown(); break;
case 98: _keys.num2.down = true; _keys.num2.onDown(); break;
case 99: _keys.num3.down = true; _keys.num3.onDown(); break;
case 100: _keys.num4.down = true; _keys.num4.onDown(); break;
case 101: _keys.num5.down = true; _keys.num5.onDown(); break;
case 102: _keys.num6.down = true; _keys.num6.onDown(); break;
case 103: _keys.num7.down = true; _keys.num7.onDown(); break;
case 104: _keys.num8.down = true; _keys.num8.onDown(); break;
case 105: _keys.num9.down = true; _keys.num9.onDown(); break;
default: break;
}
}
},
back : {down: false, onDown: function(){},onUp: function(){}},
tab : {down: false, onDown: function(){},onUp: function(){}},
enter : {down: false, onDown: function(){},onUp: function(){}},
shift : {down: false, onDown: function(){},onUp: function(){}},
ctrl : {down: false, onDown: function(){},onUp: function(){}},
alt : {down: false, onDown: function(){},onUp: function(){}},
caps : {down: false, onDown: function(){},onUp: function(){}},
esc : {down: false, onDown: function(){},onUp: function(){}},
space : {down: false, onDown: function(){},onUp: function(){}},
pageUp : {down: false, onDown: function(){},onUp: function(){}},
pageDown : {down: false, onDown: function(){},onUp: function(){}},
end : {down: false, onDown: function(){},onUp: function(){}},
home : {down: false, onDown: function(){},onUp: function(){}},
leftArrow : {down: false, onDown: function(){},onUp: function(){}},
upArrow : {down: false, onDown: function(){},onUp: function(){}},
rightArrow : {down: false, onDown: function(){},onUp: function(){}},
downArrow : {down: false, onDown: function(){},onUp: function(){}},
del : {down: false, onDown: function(){},onUp: function(){}},
_0 : {down: false, onDown: function(){},onUp: function(){}},
_1 : {down: false, onDown: function(){},onUp: function(){}},
_2 : {down: false, onDown: function(){},onUp: function(){}},
_3 : {down: false, onDown: function(){},onUp: function(){}},
_4 : {down: false, onDown: function(){},onUp: function(){}},
_5 : {down: false, onDown: function(){},onUp: function(){}},
_6 : {down: false, onDown: function(){},onUp: function(){}},
_7 : {down: false, onDown: function(){},onUp: function(){}},
_8 : {down: false, onDown: function(){},onUp: function(){}},
_9 : {down: false, onDown: function(){},onUp: function(){}},
a : {down: false, onDown: function(){},onUp: function(){}},
b : {down: false, onDown: function(){},onUp: function(){}},
c : {down: false, onDown: function(){},onUp: function(){}},
d : {down: false, onDown: function(){},onUp: function(){}},
e : {down: false, onDown: function(){},onUp: function(){}},
f : {down: false, onDown: function(){},onUp: function(){}},
g : {down: false, onDown: function(){},onUp: function(){}},
h : {down: false, onDown: function(){},onUp: function(){}},
i : {down: false, onDown: function(){},onUp: function(){}},
j : {down: false, onDown: function(){},onUp: function(){}},
k : {down: false, onDown: function(){},onUp: function(){}},
l : {down: false, onDown: function(){},onUp: function(){}},
m : {down: false, onDown: function(){},onUp: function(){}},
n : {down: false, onDown: function(){},onUp: function(){}},
o : {down: false, onDown: function(){},onUp: function(){}},
p : {down: false, onDown: function(){},onUp: function(){}},
q : {down: false, onDown: function(){},onUp: function(){}},
r : {down: false, onDown: function(){},onUp: function(){}},
s : {down: false, onDown: function(){},onUp: function(){}},
t : {down: false, onDown: function(){},onUp: function(){}},
u : {down: false, onDown: function(){},onUp: function(){}},
v : {down: false, onDown: function(){},onUp: function(){}},
w : {down: false, onDown: function(){},onUp: function(){}},
x : {down: false, onDown: function(){},onUp: function(){}},
y : {down: false, onDown: function(){},onUp: function(){}},
z : {down: false, onDown: function(){},onUp: function(){}},
num0 : {down: false, onDown: function(){},onUp: function(){}},
num1 : {down: false, onDown: function(){},onUp: function(){}},
num2 : {down: false, onDown: function(){},onUp: function(){}},
num3 : {down: false, onDown: function(){},onUp: function(){}},
num4 : {down: false, onDown: function(){},onUp: function(){}},
num5 : {down: false, onDown: function(){},onUp: function(){}},
num6 : {down: false, onDown: function(){},onUp: function(){}},
num7 : {down: false, onDown: function(){},onUp: function(){}},
num8 : {down: false, onDown: function(){},onUp: function(){}},
num9 : {down: false, onDown: function(){},onUp: function(){}}
}; // End of KEYS
////////////////////
// PRIMITIVES //
////////////////////
////////////////////
// GUI //
////////////////////
/**
*<hr>
*The <tt>MC.GUI</tt> object provides a simple way to manage multiple <tt>MC.ConBox</tt> objects
*@constructor
*@param {MC.ConBox} [ConBoxList] Comma seperated list of the ConBox's to be added
*<br>N.B. Non-Control Box items will be ignored
*@returns {MC.GUI} A <tt>new</tt> MC.GUI object
*@property {Array} bin The Array holding the Control Box items
*/
MC.GUI = function(ConBoxList) {
this.bin = new Array();
if (ConBoxList !== undefined) {
this.push(ConBoxList);
}
return this;
};
/**
*<hr>
*Adds a <tt>MC.ConBox</tt> object to the end of the array.
*@param {MC.ConBox} ConBoxList Comma seperated list of the ConBox's to be added
*<br>N.B. Non-Control Box items will be ignored
*@returns {this} enables method chaining
*/
MC.GUI.prototype.push = function (ConBoxList) {
var len = arguments.length;
for (var i = 0 ; i < len ; i++) {
if (arguments[i] instanceof MC.ConBox) {
this.bin.push(arguments[i]);
}
}
return this;
};
/**
*<hr>
*Removes the last item from the <tt>bin</tt> array.
*@returns {this} Enables method chaining
*/
MC.GUI.prototype.pop = function () {
this.bin.pop();
return this;
};
/**
*<hr>
*Renders all Control Boxes held in the GUI to the canvas.
*<br>This call accepts 3 sets of parameters
*<br>(1) Nothing, and all Control Boxes will be positioned as per their individual <tt>pos</tt> property.
*<br>(2) An MC.Point, the GUI will auto fit it's contents using this position as a datum for the top left corner.
*<br>(3) Two numbers, which mirror and MC.Point (and GUI rendered as above)
*<br>In the latter two cases, the Control Boxes will be rendered in a left aligned vertical list
*@param {MC.Point|Number} [position_or_x]
*@param {Number} [y]
*<br>N.B. Each Control Box's <tt>pos</tt> attribute is overwritten with their new rendered position
*@returns {this} Enables method chaining
*/
MC.GUI.prototype.render = function (position_or_x,y) {
var len = this.bin.length;
if (position_or_x === undefined) {
for (var i = 0; i < len ; i++) {
if (this.bin[i] instanceof MC.ConBox) this.bin[i].render();
}
return this;
}
var position = new MC.Point;
if (_utils.hasXY(position_or_x)) {
position.set(position_or_x);
}
else if (arguments.length == 2 && _utils.isNumeric(position_or_x) && _utils.isNumeric(y)) {
position.set(position_or_x,y);
}
this.clean(); // ensure we only have ConBoxes in the bin.
len = this.bin.length;
var o = new MC.Point();
o.set(position);
var P = 0;
for (var i = 0; i < len ; i++) {
P = _maths.maxOf(P, this.bin[i].edgeWidth);
}
P *= 2;
for (var i = 0; i < len; i++) {
if (i > 0) {
o.set(P + this.bin[i].width / 2 + position.x, o.y + (this.bin[i].height + this.bin[i-1].height)/2 + P);
}
else {
o.set (P + this.bin[i].width / 2 + position.x, o.y + (this.bin[i].height / 2) + P);
}
this.bin[i].render(o);
}
return this;
};
/**
*<hr>
*Removes all non <tt>MC.ConBox</tt> from the GUI.
*<br>Called automatically by various other methods
*@returns {this} Enables method chaining
*/
MC.GUI.prototype.clean = function () {
var len = this.bin.length;
var arr = new Array();
for (var i = 0; i < len ; i++) {
if (this.bin[i] instanceof MC.ConBox) {
arr.push(this.bin[i]);
}
}
this.bin = arr;
return this;
};
/**
*<hr>
*Removes all elements from the GUI, making it available for reuse
*@returns {this} Enables method chaining
*/
MC.GUI.prototype.empty = function () {
var len = this.bin.length;
for (var i = 0; i < len; i++) {
this.bin[i] = null;
}
this.clean();
}
/**
*<hr>
*Calls the <tt>update()</tt> method of all ConBox's in the GUI
*<br>This method should be called within any game loop, but only needs to be called once per frame.
*<br>Therefore it is advised to call it AFTER an <tt>MC.game.physicsUpdate</tt> loop
*/
MC.GUI.prototype.update = function () {
var len = this.bin.length;
for (var i = 0; i < len ; i++) {
if (this.bin[i] instanceof MC.ConBox) this.bin[i].update();
}
return this;
};
/**
*<hr>
*Calls the <tt>checkClick()</tt> method of all ConBox's
*@returns {this} Enables method chaining
*/
MC.GUI.prototype.checkClick = function (mouse) {
var len = this.bin.length;
var m = mouse || _game.mouse;
for (var i = 0; i < len ; i++) {
if (this.bin[i] instanceof MC.ConBox) {
this.bin[i].checkClick(mouse);
}
}
return this;
};
/**
*<hr>
*Cycles through all ConBox's and calls their <tt>onClick()</tt> method if their <tt>hover</tt> property is <tt>true</tt>
*@returns {this} Enables method chaining
*/
MC.GUI.prototype.click = function () {
var len = this.bin.length;
for (var i = 0; i < len ; i++) {
if (this.bin[i] instanceof MC.ConBox) {
if (this.bin[i].hover === true) {
this.bin[i].onClick();
}
}
}
return this;
};
////////////////////
// ConBox //
////////////////////
/**
*<hr>
*The <tt>MC.ConBox</tt> (short for "Control Box") object provides a basic clickable element, or button, on the canvas.
*<br>Although they can be manually managed, they are best implemented en-masse with the <tt>MS.GUI</tt> object
*<br>ConBox's constructor calls are made with a value object in key: value pairs (see the <tt>MC.ConBox.setValues()</tt> method below)
*<br>ConBox(es) have a variety of features, namely:
*<br>... Axis Aligned Rectangle or Circular form
*<br>... configurable to have a "hover over" color change
*<br>... text which is auto scaled to fit
*<br>... placeholder <tt>onClick()</tt> function for Developer customisation
*@constructor
*@this {ConBox}
*@returns {ConBox} a <tt>new</tt> ConBox object
*@property {String} [type="r"] ConBox type is either "r" for Rectangle, or "c" for Circular
*@property {MC.Point} [pos= mid Canvas] Screen Co-ordinates of the ConBox CENTRE
*@property {Number} [width=250] Width of the Control box
*<br>N.B. this is the diameter of any circular ConBox's
*@property {Number} [height = 50] Height of the ConBox (autoset to match Width for circular ConBox's)
*@property {String} [font = MC.game.text.font] Desired Font
*@property {Number} [fSize=50] Font size to be used displaying any text
*<br>Automatically set upon any setting or change of <tt>width, height or text</tt> properties
*@property {MC.Color|String} [colorBody=MC.game.conBoxDefaults.colorBody] The base (inactive) colour of the button
*@property {MC.Color|String} [colorBodyHover=MC.game.conBoxDefaults.colorBodyHover] The base colour of the button, when <tt>hover</tt> property is true. i.e. the mouse pointer is over it
*@property {MC.Color|String} [colorEdge=MC.game.conBoxDefaults.colorEdge] The base (inactive) colour of the button edge and text
*@property {MC.Color|String} [colorEdgeHover=MC.game.conBoxDefaults.colorEdgeHover] The base colour of the button edge and text, when <tt>hover</tt> property is true
*@property {Number} [edgeWidth=MC.game.conBoxDefaults.edgeWidth] Size of the border to be applied to the button
*@property {String} [text="To Be Arranged"] Text (if any) to be displayed on the button
*@property {Boolean} [hover=false] Configured in the <tt>update()</tt> method. <tt>True</tt> if the mouse cursor is over the button, else <tt>false</tt>
*@property {MC.Point} [offset] Internally used value to reflect the start position of the button text (from centre)
*@property {String} [oldText] Internally used flag variable to detect whether text re-size is required
*@property {Number} [oldWidth] Internally used flag variable to detect whether text re-size is required
*@property {Number} [oldHeight] Internally used flag variable to detect whether text re-size is required
*@property {MC.Typeface} [typeface] Internally used object for text rendering
*@property {Boolean} [visible=true] Visible ConBox's will be rendered, but not if this is set to <tt>false</tt>
*/
MC.ConBox = function (values) {
// set defaults
this.type = "r";
this.pos = new MC.Point(_canvas.midX,_canvas.midY);
this.width = 250;
this.height = 50;
this.font = _game.text.font;
this.fSize = 50;
this.colorBody = _game.conBoxDefaults.colorBody;
this.colorBodyHover = _game.conBoxDefaults.colorBodyHover;
this.colorEdge = _game.conBoxDefaults.colorEdge;
this.colorEdgeHover = _game.conBoxDefaults.colorEdgeHover;
this.edgeWidth = _game.conBoxDefaults.edgeWidth;
this.text = "To Be Arranged";
this.offset = new MC.Point();
this.hover = false;
this.oldText = "To Be Arranged";
this.oldWidth = 250;
this.oldHeight = 50;
this.typeFace = new _Typeface();
this.visible = true;
// configure (as required)
this.setValues(values);
};
/**
*<hr>
*Configures the ConBox via an object consisting of key: value pairs
*<br>Called automatically upon construction, also calls the <tt>MC.ConBox.fitText()</tt> method
*@param {Object} values The values to be applied
*@throws Console warning if key value does not exist in the ConBox
*@returns {this} Enables method chaining
*@example
*var myConBox = new MC.ConBox (
* {width: 150,
* height: 30,
* pos: new MC.Point(300,200),
* font: "Impact",
* text: "Print Hello World",
* onClick: function () { console.log("Hello World");
* myOtherFunction(); },
* } );
*/
MC.ConBox.prototype.setValues = function (values) {
for (var key in values) {
var newValue = values[key];
if (newValue === undefined) {
console.log("WARNING: Value of "+ key+ " not set in ConBox.setValues()");
continue;
}
var oldValue = this[key];
if (oldValue === undefined) {
console.log("WARNING: Property "+ newValue+ " does not exist in ConBox.setValues()");
continue;
}
this[key] = newValue;
}
this.textFit();
return this;
};
/**
*<hr>
* Clones the ConBox, and returns a new one. Useful for rapid prototyping
*@returns {ConBox} a <tt>new</tt> ConBox object (with the same parameters)
*<br> WARNING: any custom behaviour defined in the <tt>onClick()</tt> function are NOT copied
*/
MC.ConBox.prototype.clone = function () {
var ret = new _ConBox();
ret.setValues({
type: this.type,
pos: this.pos.clone(),
width: this.width,
height: this.height,
font: this.font,
fSize: this.fSize,
colorBody: this.colorBody,
colorBodyHover: this.colorBodyHover,
colorEdge: this.colorEdge,
colorEdgeHover: this.colorEdgeHover,
edgeWidth: this.edgeWidth,
text: this.text,
offset: this.offset,
hover: this.hover,
oldText: this.oldText,
oldWidth: this.oldWidth,
oldHeight: this.oldHeight,
typeFace: this.typeFace.clone(),
visible: this.visible,
});
return ret;
};
/**
*<hr>
*Developer customisable function determining what is to happen when the button is clicked
*/
MC.ConBox.prototype.onClick = function () {
console.log(this.text + " clicked");
};
/**
*<hr>
*Calls the <tt>update()</tt> method and if the mouse if over the button (i.e. <tt>hover == true</tt>) calls the <tt>onClick()</tt> method
*@param {MC.Point} [mouse=MC.game.mouse] Screen coordinates to be be checked against
*/
MC.ConBox.prototype.checkClick = function (mouse) {
this.update(mouse);
if (this.hover) this.onClick();
};
/**
*<hr>
*Checks if the mouse coordinates are over the button. Is so, sets the <tt>hover</tt> property to <tt>true</tt>
*<br>Both the <tt>clickCheck()</tt> and <tt>render()</tt> methods depend upon the <tt>hover</tt> property
*<br>N.B. this need only be called once per frame, so in any <tt>requestAnimationFrame()</tt> gameloop it is recommended that this call is made after any <tt>MC.game.physicsUpates</tt> loop but before any render calls
*@param {MC.Point} [mouse=MC.game.mouse] Mouse coordinates to be checked
*@returns {this} Enables method chaining
*/
MC.ConBox.prototype.update = function (mouse) {
var m = mouse || _game.mouse;
if (this.type == "r") {
if (m.x < this.pos.x-(this.width/2) || m.x > this.pos.x+(this.width/2) || m.y < this.pos.y - (this.height/2) || m.y > this.pos.y + (this.height/2) ) {
this.hover = false;
return this;
}
}
if (this.type == "c") {
if (_maths.rangeSqr(this.pos,m) > (this.width/2)*(this.width/2)) {
this.hover = false;
return this;
}
}
this.hover = true;
return this;
};
/**
*<hr>
*Renders the ConBox to the canvas, colours used depend upon the <tt>hover</tt> value
*@param {MC.Point} [position=this.pos] Screen coordinates of the centre point of the button.
*<br>N.B. The ConBox <tt>pos</tt> property will be overwritten by any newly presented position
*/
MC.ConBox.prototype.render = function (position) {
if (!this.visible) {
return this;
}
if (position !== undefined) {
if (_utils.hasXY(position)) {
this.pos.set(position);
}
}
var c1 = this.colorBody;
var c2 = this.colorEdge;
if (this.hover) {
c1 = this.colorBodyHover;
c2 = this.colorEdgeHover;
}
if (this.oldText != this.text || this.oldWidth != this.width || this.oldHeight != this.Height) {
this.setValues();
}
this.typeFace.color = c2;
if (this.type == "r") {
_draw.rectBasic(this.pos,this.width,this.height,c1,c2,this.edgeWidth);
_draw.text(this.text,this.pos.clone().add(this.offset),this.typeFace);
}
if (this.type == "c") {
_draw.circle(this.pos,this.width/2,c1,c2,this.edgeWidth);
_draw.text(this.text,this.pos.clone().add(this.offset),this.typeFace);
}
return this;
};
/**
*<hr>
*Autofits the text to fit within the button.
*<br>Called automatically upon construction, <tt>setValues</tt> or when an <tt>update()</tt> detects a change between current and "old" values of width, height or text
*/
MC.ConBox.prototype.textFit = function () {
var o;
if (this.type == "r") {
var o = _draw.textFit(this.text,this.width * 0.9,this.height * 0.75,this.font);
}
if (this.type == "c") {
var o = _draw.textFit(this.text,this.width * 0.65,this.height * 0.65,this.font);
}
this.offset = o.offset;
this.typeFace.size = this.fSize = o.size;
this.oldText = this.text;
this.oldWidth = this.width;
this.oldHeight = this.height;
this.typeFace.font = this.font;
this.typeFace.size = this.fSize;
this.typeFace.color = this.colorEdge;
if (this.type == "c") this.height = this.width;
return this;
};
////////////////////
// TYPEFACE //
////////////////////
/**
*<hr>
*The <tt>MC.Typeface</tt> Object provides a simple container for recording text formats
*@namespace MC.Typeface
*@constructor
*@param {String} Font The desired HTML5 font
*@param {Number} Size The font size
*@param {String|MC.Color} Color The text colour. Either an HTML5 color string or <tt>MC.Color</tt> can be used
*@this {Typeface}
*@returns {Typeface} a <tt>new</tt> Typeface object
*/
MC.Typeface = function (Font,Size,Color) {
this.font = Font;
this.size = Size;
this.color = Color;
return this;
};
/**
*<hr>
*Clones the calling Typeface and returns a new one, useful for rapid prototyping
*@returns {Typeface} a <tt>new</tt> Typeface object (with the same values)
*/
MC.Typeface.prototype.clone = function () {
return new MC.Typeface(this.font,this.size,this.color);
};
/**
*<hr>Returns a string concatentation of the <tt>size+"Px "+font</tt>
*<br>Used internally by the <tt>MC.draw.text()</tt> method
*/
MC.Typeface.prototype.log = function () {
return this.size+"Px "+this.font;
};
////////////////////
// PICTURE //
////////////////////
/**
<hr>
* The MC Picture Object provides extensions to the native JS <tt>Image()</tt> object.
*A new <tt>Picture</tt> object can be created by 1 of two methods:
*<br>(1) Passing in an existing JS <tt>Image()</tt> object or
*<br>(2) Passing in a string representing the required Image source. <tt>Picture</tt> objects initialise themselves once the Image is loaded, and will not crash if <tt>render()</tt> is called before loading is complete
*
* @class
* @namespace MC.Picture
* @constructor
* @this {Picture}
* @param {Image|String|MC.Point} image - a JS <tt>Image()</tt> object OR a string direction to one e.g. "../images/bullet_cat.jpg" OR an MC.Point which creates a blank image with width of image.x and height of image.y (the latter being ready for subsequent manipulation)
* @returns {Picture} A new Picture object
* @property img {HTML_Image}
* @property loaded=false {Boolean} Set to <tt>true</tt> once Image has been loaded
* @property width=0 {Number} Native Width of the Image, set upon loading
* @property height=0 {Number} Native Height of the Image, set upon loading
* @property imageData {Array.<Number>} Raw bitmap Image data (needs width and height to be unwrapped)
*/
MC.Picture = function(image) {
this.img = null;
this.loaded = false;
this.width = 0;
this.height = 0;
this.imageData = null; // NOT IMPLEMENTED - Awaiting "dirty Canvas" decisions
if (image instanceof HTMLImageElement) {
this.init(image);
}
else if (typeof image ==="string" || image instanceof String) {
var myself = this;
var pending = new Image();
//pending.crossOrigin = '';
pending.src = image;
pending.onload = function () {
myself.init(pending);
};
}
else if (image instanceof _Point) {
this.img = new Image(image.x,image.y);
var data = [image.x];
this.init(this.img);
}
return this;
};
/**
*<hr>
*Initialises the <tt>Picture</tt>
*<br>CAUTION: This is automatically run upon image file loading. Creating a <tt>new</tt> object is always more reliable than editing properties.
*/
MC.Picture.prototype.init = function (image) {
this.img = image;
this.width = this.img.width;
this.height = this.img.height;
this.loaded = true;
//console.log(this.img.crossOrigin);
this.img.crossOrigin = '';
//console.log(this.img.crossOrigin);
//console.log(this.img);
//this.populateImageData();
return this;
};
// NOT IMPLEMENTED - Awaiting "dirty Canvas" decisions
MC.Picture.prototype.populateImageData = function () {
_backCanvas.width = this.img.width;
_backCanvas.height = this.img.height;
_backCanvasContext.drawImage(this.img,0,0);
this.imageData = _backCanvasContext.getImageData(0,0,this.img.width, this.img.height);
};
/**
*<hr>
*Renders the picture to the canvas
*<br> See MC.draw.picture() for parameter details
*<br>N.B. this method requires EXACTLY the first 2,3 or all 6 parameters.
*<br>(1) Only using <tt>(centre,angle)</tt> means the image is drawn at 1:1 scale, centred and rotated about the <tt>centre</tt> point
*<br>(2) Adding the <tt>width,height</tt> pair scales the image to the appropriate size (in pixels)
*<br>(3) Adding the <tt>start,finish</tt> pair clips the image to a rectangle defined by <tt>start</tt> (top left corner) and <tt>finish</tt> (bottom right hand corner). Essential for using sprite sheets.
*<br>(4) If any <tt>start</tt> or <tt>finish</tt> parameter is less or equal to 1, then that ratio of the native image width/height will be applied.
*<br>A Gray Box with Red cross appears (<tt>draw.loadWarning()</tt>) is draw is this method is called and the image has not loaded
*@param {MC.Point} centre - Canvas location of the centre of the image (1)
*@param {Number} angle - Angle (degrees) the image is to be rotated about the centre. 0 (zero) = no rotation (1)
*@param {Number} [width] - Width image is to be displayed with (before any rotation). If used then <tt>height</tt> must be specified (2)
*@param {Number} [height] - As per width, but image display height in pixels (2)
*@param {MC.Point} [start] - For displaying only a section of the image, start is a point representing the top left coordinates of the section ot be displayed. If <tt>start</tt> is specified then <tt>finish</tt> has to be too (3)(4)
*@param {MC.Point} [finish] - As per <tt>start</tt> above, except this is the bottom right of the selection to be displayed (3)(4)
*/
MC.Picture.prototype.render = function (centre,angle,width,height,start,finish) {
if (!this.loaded) {
_draw.loadWarning(centre);
}
else {
var L = arguments.length;
if (L == 2) _draw.picture(this.img,centre,angle);
else if (L == 4) _draw.picture(this.img,centre,angle,width,height);
else if (L == 6) _draw.picture(this.img,centre,angle,width,height,start,finish);
}
return;
};
// NOT IMPLEMENTED - Awaiting "dirty Canvas" decisions
MC.Picture.prototype.getPixel = function (XRatio,YRatio) {
var out = new _Color();
var W = this.img.width;
var X = Math.floor(W * XRatio);
var Y = Math.floor(this.img.height * YRatio);
W *= 4;
var Base = (Y * W) + (X * 4);
// Thanks to https://developer.mozilla.org/en-US/docs/Web/API/Canvas_API/Tutorial/Pixel_manipulation_with_canvas (Sept 2018)
out.r = this.img.imageData.data[Base];
out.g = this.img.imageData.data[Base + 1];
out.b = this.img.imageData.data[Base + 2];
out.a = this.img.imageData.data[Base + 3];
return out;
};
// NOT IMPLEMENTED - Awaiting "dirty Canvas" decisions
MC.Picture.prototype.paintPixel = function (XRatio, YRatio, Color) {
var W = this.img.width;
var X = Math.floor(W * XRatio);
var Y = Math.floor(this.img.height * YRatio);
W *= 4;
var Base = (Y * W) + (X * 4);
// Thanks to https://developer.mozilla.org/en-US/docs/Web/API/Canvas_API/Tutorial/Pixel_manipulation_with_canvas (Sept 2018)
this.img.imageData.data[Base] = Color.r;
this.img.imageData.data[Base + 1] = Color.g;
this.img.imageData.data[Base + 2] = Color.b;
this.img.imageData.data[Base + 3] = Color.a;
};
////////////////////
// POINT //
////////////////////
/**
<hr>
* Creates a 2 Dimensional Point / Vector
*
* @class
* @namespace MC.Point
* @constructor
* @this {MC.Point}
* @param {Number|Object} [x=0] - The x value for the Point, if an Object is entered which has numeric <tt>>x & y</tt> properties, such as another MC.Point, then the new Point is created with these values
* @param {Number} [y=0] - The y value for the Point
* @property x {Number}
* @property y {Number}
* @returns {MC.Point}
*/
MC.Point = function(x,y) {
if (x !== undefined &&_utils.hasXY(x)) {
if (_utils.isNumeric(x.x) && _utils.isNumeric(x.y)) {
this.x = x.x;
this.y = x.y;
}
}
else {
this.x = x || 0;
this.y = y || 0;
}
return this;
};
/**
*<hr>
*Console logs the value of a Point (for debugging and testing purposes)
*@returns {this} Enables method chaining
*/
MC.Point.prototype.log = function () {
console.log(" Point X: "+this.x,",Y: "+this.y);
return this;
};
/**
*<hr>
*Adds another point/vector or <tt>(x,y)</tt> to the calling point
*<br>N.B Any object with numeric <tt>x</tt> and <tt>y</tt> properties can be added to a Point
*@param {MC.Point|Number} point_or_x - Point (or <tt>x</tt> value) to add
*@param {Number} [y] - <tt>y</tt> value to add
*@returns {this} Enables method chaining
*/
MC.Point.prototype.add = function (point_or_x,y) {
if (_utils.hasXY(point_or_x)) {
if (_utils.isNumeric(point_or_x.x) && _utils.isNumeric(point_or_x.y)) {
this.x += point_or_x.x;
this.y += point_or_x.y;
}
}
else if (_utils.isNumeric(point_or_x) && _utils.isNumeric(y)) {
this.x += point_or_x;
this.y += y;
}
return this;
};
/**
*<hr>
*Subtracts another point/vector or <tt>(x,y)</tt> from the calling point
*<br>N.B Any object with numeric <tt>x</tt> and <tt>y</tt> properties can be subtracted from a Point
*@param {MC.Point|Number} point_or_x - Point (or <tt>x</tt> value) to subtract
*@param {Number} [y] - <tt>y</tt> value to subtract
*@returns {this} Enables method chaining
*/
MC.Point.prototype.minus = function (point_or_x,y) {
if (_utils.hasXY(point_or_x)) {
if (_utils.isNumeric(point_or_x.x) && _utils.isNumeric(point_or_x.y)) {
this.x -= point_or_x.x;
this.y -= point_or_x.y;
}
}
else if (_utils.isNumeric(point_or_x) && _utils.isNumeric(y)) {
this.x -= point_or_x;
this.y -= y;
}
return this;
};
/**
*<hr>
*Multiplies the point/vector by a scalar value
*@param {Number} x - Number to multiply the Point by OR (if <tt>y</tt> is included) number to multiply the <tt>x</tt> property by
*@param {Number} [y] - Number to multiply the <tt>y</tt> value by
*@returns {this} Enables method chaining
*/
MC.Point.prototype.times = function (x,y) {
if (arguments.length == 1) {
if (_utils.isNumeric(x)) {
this.x *= x;
this.y *= x;
}
}
else if (arguments.length == 2) {
if (_utils.isNumeric(x) && _utils.isNumeric(y)) {
this.x *= x;
this.y *= y;
}
}
return this;
};
/**
*<hr>
*Divides the point/vector by a scalar value
*@param {Number} - Number to divide Point by
*@returns {this} Enables method chaining
*@throws Console warning if attempting to divide by 0 (zero). Point is returned unchanged.
*/
MC.Point.prototype.div = function (number) {
if (number === 0) {
console.log("WARNING attempt to divide a Point by zero");
return this;
}
else if (_utils.isNumeric(number)) {
this.x /= number;
this.y /= number;
}
return this;
};
/**
*<hr>
*Checks if the Point (as a vector) it to the left of another
*@param {MC.Point} Vector to be checked against.
*<br> N.B anything with <tt>.x</tt> and <tt>.y</tt> properties can be checked against
*@returns {Boolean} <tt>true</tt> if the vector is to the left, else <tt>false</tt>
*/
MC.Point.prototype.isLeft = function (Vector) {
if (_utils.hasXY(Vector)) {
if (_maths.crossProd(this,Vector) < 0) return true;
}
return false;
};
/**
*<hr>
*Checks if the Point (as a vector) it to the right of another
*@param {MC.Point} Vector to be checked against.
*<br> N.B anything with <tt>.x</tt> and <tt>.y</tt> properties can be checked against
*@returns {Boolean} <tt>true</tt> if the vector is to the right, else <tt>false</tt>
*/
MC.Point.prototype.isRight = function (Vector) {
if (_utils.hasXY(Vector)) {
if (_maths.crossProd(this,Vector) > 0) return true;
}
return false;
};
/**
*<hr>
*Sets the point to equal another point/vector OR a <tt>(x,y)</tt>
*<br>N.B Any object with numeric <tt>x</tt> and <tt>y</tt> properties can be used
*@param {MC.Point|Number} point_or_x - Point to equal OR the new <tt>x</tt> value
*@param {Number} [y] - y value
*@returns {this} Enables method chaining
*/
MC.Point.prototype.set = function (point_or_x,y) {
if (_utils.hasXY(point_or_x)) {
if (_utils.isNumeric(point_or_x.x) && _utils.isNumeric(point_or_x.y)) {
this.x = point_or_x.x;
this.y = point_or_x.y;
}
}
else if (arguments.length == 2) {
if (_utils.isNumeric(point_or_x) && _utils.isNumeric(y)) {
this.x = point_or_x;
this.y = y;
}
}
return this;
};
/**
*<hr>
*Reports the scalar length of the vector / point
*@returns {Number} Length of the vector
*/
MC.Point.prototype.getLength = function() {
return Math.sqrt((this.x * this.x) + (this.y * this.y));
};
/**
*<hr>
*Reports the scalar length SQUARED of the vector / point
*@returns {Number} Length of the vector
*/
MC.Point.prototype.getLengthSqr = function() {
return (this.x * this.x) + (this.y * this.y);
};
/**
*<hr>
*Normalises the Vector.
*<br>NB. See <tt>MS.maths.normalOf()</tt> if you need the normal but want to keep the original vector
*<br>Both <tt>Point.normalise()</tt> OR <tt>Point.normalize()</tt> will work.
*@returns {this} Enables method chaining
*/
MC.Point.prototype.normalise = function() {
var L = this.getLength();
this.x /= L;
this.y /= L;
return this;
};
// spelling alternative
MC.Point.prototype.normalize = function () {
this.normalise();
return this;
};
/**
*<hr>
*Sets the length of a vector
*@param {Number} - Desired vector length
*@returns {this} Enables method chaining
*/
MC.Point.prototype.setLength = function (length) {
if (_utils.isNumeric(length)) {
this.normalise().times(length);
}
return this;
};
/**
*<hr>
*Inverts / reverses the direction of the vector
*@returns {this} Enables method chaining
*/
MC.Point.prototype.invert = function () {
this.x *= -1;
this.y *= -1;
return this;
};
/**
*<hr>
*Returns the angle of the vector (as if it's a line with origin 0,0)
*@returns {Number} The angle of the vector (in degrees clockwise)
*@example
* ..........| 270
* (0,-1)
* 180 | 360
* ------------(1,0)---->
* | 0
* | (1,1)
* 90 |
*var pnt = new MC.Point(1,0);
*var angle = pnt.getAngle(); // 0
*pnt.set(1,1);
*angle = pnt.getAngle(); // 45
*pnt.set(0,-1);
*angle = pnt.getAngle(); // 270
*/
MC.Point.prototype.getAngle = function () {
return _utils.fixAngle(Math.atan2(this.y , this.x) * _maths.TO_DEGREES);
};
/**
*<hr>
*Rotates a vector (as if it's a line with origin 0,0 ... or if as a coordinate with optional MC.Point as an origin)
*@param {Number} - Angle to rotate by (+ve = clockwise)
*@param {MC.Point} [origin] optional origin for a coordinate rotation
*@returns {this} Enables method chaining
*
*/
MC.Point.prototype.rotate = function (degrees,origin) {
var o = new _Point(0,0);
if (origin !== undefined && _utils.hasXY(origin)) {
o.x = origin.x;
o.y = origin.y;
}
if (degrees !== 0 && _utils.isNumeric(degrees)) {
this.minus(o);
var s = Math.sin(degrees * _maths.TO_RADIANS);
var c = Math.cos(degrees * _maths.TO_RADIANS);
var nx = (this.x * c) - (this.y * s);
var ny = (this.x * s) + (this.y * c);
this.x = nx;
this.y = ny;
this.add(o);
}
return this;
};
/**
*<hr>
*Set a vector to an angle (as if it's a line with origin 0,0)
*@param {Number} - Desired angle
*@returns {this} Enables method chaining
*/
MC.Point.prototype.setAngle = function (angle) {
if (angle !== 0 && _utils.isNumeric(angle)) {
this.rotate(angle - this.getAngle());
}
return this;
};
/**
*<hr>
*Calculates the angle from one vector to another
*N.B. +ve = clockwise, so in some circumstances this can be used to determine direction (i.e. a velocity vector with a relational direction vector as target)
*<br> 0-90 -- target is bearing front right
*<br> 90-180 -- target is bearing rear right
*<br> 180-270 -- target is bearing rear left
*<br> 270-360 -- target is bearing front left
*
*@param {MC.Point} - Target vector/point
*@returns {Number} Angle from calling vector to target vector/point (in degrees)
*@throws Console warning if target does not have <tt>x</tt> and <tt>y</tt> properties (or they are non-numeric). Calling vector is unchanged and 0 (zero) returned
*/
MC.Point.prototype.angleTo = function (target) {
if (_utils.hasXY(target)) {
if (_utils.isNumeric(target.x) && _utils.isNumeric(target.y)) {
var tp = new _Point(target.x,target.y); // allows a non-Point object which has x: & y: to be used as a target
return _utils.fixAngle(tp.getAngle() - this.getAngle());
}
}
// not returned, ergo invalid target
console.log("WARNING: invalid properties passed to Point.angleTo()");
return 0;
};
/**
*<hr>
*Clones the Point/Vector and returns a copy
*@returns {MC.Point} A NEW Point Object
*/
MC.Point.prototype.clone = function () {
return new _Point(this.x,this.y);
};
/**
*<hr>
*Returns the Point with Absolute values, e.g. (5,-4) is returned as (5,4)
*@returns {this} Enables method chaining
*/
MC.Point.prototype.abs = function()
{
if (this.x < 0) {
this.x *= -1;
}
if (this.y < 0) {
this.y *= -1;
}
return this;
};
/**
*<hr>
*Scales the vector
*@param {Number} scale - Scale (aka multiplier) to be applied
*@returns {this} Enables method chaining
*@throws Console warning if a non-numeric or 0 (zero) scale applied. Original Point returned
*/
MC.Point.prototype.scale = function (scale) {
if (scale === 1) { // quick "get out" to save time for non-event
return this;
}
if (_utils.isNumeric(scale) && scale !== 0) {
this.x *= scale;
this.y *= scale;
return this;
}
else {
console.log("WARNING: improper scale passed to Point.scale()");
return this;
}
};
/**
*<hr>
*Clamps the length of the vector to a maximum value OR between a pair of values
*<br>N.B. Useful for limiting velocities
*@param {Number} [min=0] - Minimum length of clamp value
*@param {Number} max - Maximum length of clamp value
*@return {this} Enables method chaining
*/
MC.Point.prototype.clamp = function (min,max) {
var L = this.getLength();
var big = L;
var small = 0;
if (arguments.length == 1 && _utils.isNumeric(min)) {
big = Math.abs(min);
}
else if (arguments.length == 2 && _utils.isNumeric(min) && _utils.isNumeric(max)) {
big = Math.abs(max);
small = Math.abs(min);
}
if (L > big) {
this.setLength(big);
}
else if (L < small) {
this.setLength(small);
}
return this;
};
/**
*<hr>
*Checks if the Point's <tt>x</tt> and <tt>y</tt> properties match those of another item (which may, or may not be another Point)
*@param {Object} item - Object for comparison
*@returns {Boolean} <tt>true</tt> if both <tt>x</tt> and <tt>y</tt> properties match
*/
MC.Point.prototype.equals = function (item) {
if (_utils.hasXY(item)) {
if (_utils.isNumeric(item.x) && _utils.isNumeric(item.y)) {
return (this.x == item.x && this.y == item.y);
}
}
return false;
};
////////////////////
// COLOR //
////////////////////
/**
*<hr>
*Creates a Color Object.
*<br>MC uses rbg (0-255) notation with alpha (0-1)
*<br>N.B. HTML5 default names colours (e.g. "Aqua" and "Salmon") can be used instead
*
*@class
*@namespace MC.Color
*@constructor
*@this {MC.Color}
*@param {Number} [r=255] - Red Color component (clamped 0-255)
*@param {Number} [g=255] - Green Color component (clamped 0-255)
*@param {Number} [b=255] - Blue Color component (clamped 0-255)
*@param {Number} [a=1] - Color alpha (aka transparency). 1 = solid, 0 = fully transparent
*@returns {Color}
*/
MC.Color= function (r,g,b,a) {
this.r = (r === 0) ? 0 : ~~_maths.clamp(r,0,255) || 255;
this.g = (g === 0) ? 0 : ~~_maths.clamp(g,0,255) || 255;
this.b = (b === 0) ? 0 : ~~_maths.clamp(b,0,255) || 255;
this.a = (a === 0) ? 0 : a || 1;
return this;
};
/**
*<hr>
*Shifts the color towards a second Color, by the ratio provided
*@this {MC.Color}
*@returns {this} Enables method chaining
*@param {MC.Color} Target Color to be shifted towards
*@param {Number} Ratio ratio of the shift, Values are capped between 0-1
*@throws Console warning if a non-MC.Color is passed in. Color itself is unchanged
*<br>N.B Alpha (a) transparency value is also shifted
*/
MC.Color.prototype.lerp = function (Target, Ratio) {
Ratio = _maths.clamp(0,1);
if (!(Target instanceof MC.Color)) {
console.log("WARNING: non-MC.Color passed to MC.Color.lerp()");
return this;
}
this.addR((Target.r - this.r) * Ratio);
this.addG((Target.g - this.g) * Ratio);
this.addB((Target.b - this.b) * Ratio);
this.a += (Target.a - this.a) * Ratio;
return this;
};
/**
*<hr>
*Returns the RGB value of a {MC.Color} in hex format. e.g. "#AA05F5"
*@this {MC.Color}
*@returns {string}
**/
MC.Color.prototype.toHex = function () {
return "#"+_maths.toHex(this.r)+_maths.toHex(this.g)+_maths.toHex(this.b);
};
/**
*<hr>
*Returns the RGB value of a {MC.Color} in rbg(r,g,b) format. e.g. "rgb(120,0,255)"
*@this {MC.Color}
*@returns {string}
**/
MC.Color.prototype.rgb = function () {
return "rgb("+this.r+","+this.g+","+this.b+")";
};
/**
*<hr>
*Adds <tt>r</tt> to the Color.r
*<br>Result is clamped between 0 and 255
*@param {Number} - value to add the R Color component
*@this {MC.Color}
*@returns {this} Enables method chaining
*/
MC.Color.prototype.addR = function (r) {
this.r = ~~_maths.clamp(this.r + r ,0,255);
return this;
};
/**
*<hr>
*Adds <tt>r</tt> to the Color.g
*<br>Result is clamped between 0 and 255
*@param {Number} - value to add the G Color component
*@this {MC.Color}
*@returns {this} Enables method chaining
*/
MC.Color.prototype.addG = function (g) {
this.g = ~~_maths.clamp(this.g + g ,0,255);
return this;
};
/**
*<hr>
*Adds <tt>r</tt> to the Color.b
*<br>Result is clamped between 0 and 255
*@param {Number} - value to add the B Color component
*@this {MC.Color}
*@returns {this} Enables method chaining
*/
MC.Color.prototype.addB = function (b) {
this.b = ~~_maths.clamp(this.b + b ,0,255);
return this;
};
/**
*<hr>
*Subracts <tt>r</tt> from the Color.r
*<br>Result is clamped between 0 and 255
*@param {Number} - value to subtract from the R Color component
*@this {MC.Color}
*@returns {this} Enables methof chaining
*/
MC.Color.prototype.minusR = function (r) {
this.r = ~~_maths.clamp(this.r - r ,0,255);
return this;
};
/**
*<hr>
*Subtracts <tt>r</tt> from the Color.g
*<br>Result is clamped between 0 and 255
*@param {Number} - value to subtract from the G Color component
*@this {MC.Color}
*@returns {this} Enables method chaining
*/
MC.Color.prototype.minusG = function (g) {
this.g = ~~_maths.clamp(this.g - g ,0,255);
return this;
};
/**
*<hr>
*Subtracts <tt>r</tt> from the Color.b
*<br>Result is clamped between 0 and 255
*@param {Number} - value to subtract from the B Color component
*@this {MC.Color}
*@returns {this} Enables method chaining
*/
MC.Color.prototype.minusB = function (b) {
this.b = ~~_maths.clamp(this.b - b ,0,255);
return this;
};
/**
*<hr>
*Inverts the Color (i.e. Red value becomes (255 - Red), etc)
*@this {MC.Color}
*@returns {this} Enables method chaining
*/
MC.Color.prototype.invert = function () {
this.r = ~~_maths.clamp(255-this.r,0,255);
this.g = ~~_maths.clamp(255-this.g,0,255);
this.b = ~~_maths.clamp(255-this.b,0,255);
return this;
};
/**
*<hr>
*Converts the Color to Grayscale
*<br> Uses a Luminocity formula of (Red * 0.21 + Green * 0.72 + Blue * 0.07).
*<br> See <a href="https://www.johndcook.com/blog/2009/08/24/algorithms-convert-color-grayscale/" target ="_blank">this link</a> for more info (opens in a new window)
*<br> N.B both <tt>Color.toGray()</tt> and <tt>Color.toGrey()</tt> work
*@this {MC.Color}
*@returns {this} Enables method chaining
*/
MC.Color.prototype.toGrey = function () {
// formula from https://www.johndcook.com/blog/2009/08/24/algorithms-convert-color-grayscale/ (June 2018)
this.r = this.g = this.b = _maths.clamp(~~((this.r * 0.21) + (this.g * 0.72) + (this.b * 0.07)),0,255);
return this;
};
// Spelling alternative of toGrey() (above)
MC.Color.prototype.toGray = function () {
this.toGrey();
return this;
};
/**
*<hr>
*Converts Color to Sepia
*<br> See <a href="https://stackoverflow.com/questions/1061093/how-is-a-sepia-tone-created" target ="_blank">this link</a> for more info (opens in a new window)
*@this {MC.Color}
*@returns {this} Enables method chaining
*/
MC.Color.prototype.toSepia = function () {
// formula from https://stackoverflow.com/questions/1061093/how-is-a-sepia-tone-created (June 2018)
var R = this.r;
var G = this.g;
var B = this.b;
this.r = _maths.clamp(~~((R * 0.393)+(G * 0.769)+(B * 0.189)),0,255);
this.g = _maths.clamp(~~((R * 0.349)+(G * 0.686)+(B * 0.168)),0,255);
this.b = _maths.clamp(~~((R * 0.272)+(G * 0.534)+(B * 0.131)),0,255);
return this;
};
/**
*<hr>
*Returns a string of the Color attributes in rbga format e.g. "rgba(120,45,67,0.34)"
*@returns {string}
*/
MC.Color.prototype.rgba = function() {
return "rbga("+this.r+","+this.g+","+this.b+","+this.a+")";
};
/**
*<hr>
*Sets a Color
*<br>Either a Color can be entered, or <tt>(r,g,b,a)</tt> (with the <tt>a</tt> being optional)
*@param {Number|MC.Color} red_or_Color - Red component (clamped to 0-255) OR the Color to be matched
*@param {Number} [green] - Green component (clamped to 0-255)
*@param {Number} [blue] - Blue component (clamped to 0-255)
*@param {Number} [alpha] - Transparency (clamped to 0-1)
*@returns {this} Enables method chaining
*
*/
MC.Color.prototype.set = function (red_or_Color,green, blue, alpha) {
if (arguments.length == 1) {
this.r = ~~_maths.clamp(red_or_Color.r,0,255);
this.g = ~~_maths.clamp(red_or_Color.g,0,255);
this.b = ~~_maths.clamp(red_or_Color.b,0,255);
this.a = _maths.clamp(red_or_Color.a,0,1);
return this;
}
else {
this.r = ~~_maths.clamp(red_or_Color,0,255);
this.g = ~~_maths.clamp(green,0,255);
this.b = ~~_maths.clamp(blue,0,255);
if (arguments.length == 4) {
this.a = _maths.clamp(alpha,0,1);
}
return this;
}
};
/**
*<hr>
*Sets the Color's Red component
*<br> N.B although e.g. <tt>Color.r = 25;</tt> could be used, this method clamps the value between legal 0-255
*@param {Number} - The new Red color component
*@returns {this} Enables method chaining
*/
MC.Color.prototype.setR = function (red) {
if (_utils.isNumeric(red)) {
this.r = ~~_maths.clamp(red,0,255);
}
return this;
};
/**
*<hr>
*Sets the Color's Green component
*<br> N.B although e.g. <tt>Color.g = 25;</tt> could be used, this method clamps the value between valid 0-255
*@param {Number} - The new Green color component
*@returns {this} Enables method chaining
*/
MC.Color.prototype.setG = function (green) {
if (_utils.isNumeric(green)) {
this.g = ~~_maths.clamp(green,0,255);
}
return this;
};
/**
*<hr>
*Sets the Color's Blue component
*<br> N.B although e.g. <tt>Color.r = 25;</tt> could be used, this method clamps the value between valid 0-255
*@param {Number} - The new Blue color component
*@returns {this} Enables method chaining
*/
MC.Color.prototype.setB = function (blue) {
if (_utils.isNumeric(blue)) {
this.b = ~~_maths.clamp(blue,0,255);
}
return this;
};
/**
*<hr>
*Sets the Color's Alpha component
*<br> N.B although e.g. <tt>Color.a = 0.3;</tt> could be used, this method clamps the value between valid 0-1
*@param {Number} - The new alpha color component
*@returns {this} Enables method chaining
*/
MC.Color.prototype.setA = function (alpha) {
if (_utils.isNumeric(alpha)) {
this.a = _maths.clamp(alpha,0,1);
}
return this;
};
/**
*<hr>
*Clones the current Color and returns a NEW copy
*@returns {MC.Color} A NEW color object with the same parameters
*/
MC.Color.prototype.clone = function () {
return new _Color(this.r,this.g,this.b,this.a);
};
////////////////////
// SPRITEBIN //
////////////////////
/**
*<hr>
*Creates <tt>new SpriteBin</tt> object.
*<br>SpriteBin's provide a useful set of functions for the easy management of Sprite Collections.
*<br>SpriteBin's sizes are adjusted as necessary at run-time.
*@class
*@namespace MC.SpriteBin
*@constructor
*@property {Array} bin The internal array holding the Sprites
*@property {Boolean} clean Internal flag to trigger <tt>cleanBin()</tt>
*@this {MC.SpriteBin}
*@returns {MC.SpriteBin} A <tt>new</tt> SpriteBin object
*/
MC.SpriteBin = function () {
this.bin = new Array();
this.clean = true;
this.DT;
};
/**
*<hr>
*Calls the <tt>update()</tt> function on each Object in the SpriteBin (if it has it).
*Elements with <tt>null</tt> or Sprites with <tt>.dead == True</tt> are flagged to require clean up.
*@param {Number} [deltaTime=MC.game.deltaTime] DeltaTime (before any physicsUpdates calculation) in Seconds.
*@returns {this} Enables Method chaining
*/
MC.SpriteBin.prototype.update = function (deltaTime) {
this.DT = deltaTime || _game.deltaTime;
var len = this.bin.length;
if (len == 0) {
return this;
}
for (var i = 0; i < len; i++)
{
if (this.bin[i] === undefined || this.bin[i] === null || this.bin[i].dead === true) {
this.clean = false;
continue;
}
else if(typeof this.bin[i].update === 'function') {
this.bin[i].update(this.DT);
}
}
return this;
};
/**
*<hr>
*Calls the <tt>render()</tt> function on each Object in the SpriteBin (if it has it) so it is drawn on the game canvas
*@returns {this} Enables Method chaining
*/
MC.SpriteBin.prototype.render = function () {
var len = this.bin.length;
for (var i = 0; i < len; i++) {
if(this.bin[i] !== null && this.bin[i].dead !== true && typeof this.bin[i].render === 'function') {
this.bin[i].render();
}
}
if (!this.clean) {
this.cleanBin();
}
return this;
};
/**
*<hr>
*Cycles through all Sprites in the bin and removes any which are <tt>null</tt> or where <tt>Sprite.dead == true</tt>
*<br>Called automatically at the end of any <tt>render()</tt> call should <tt>update()</tt> have found any candidates for deletion
*/
MC.SpriteBin.prototype.cleanBin = function () {
var ret = new Array();
var len = this.bin.length;
for (var i = 0; i < len;i++) {
if (this.bin[i] !== null && this.bin[i].dead !== true) {
ret.push(this.bin[i]);
}
}
this.bin = ret;
this.clean = true;
};
/**
*<hr>
*Cycles through the Spritebin and calls the <tt>bounce()</tt> method of each Sprite against the target Parameter
*@param {MC.Sprite|MC.SpriteBin} [target=self] Target for the <tt>bounce()</tt> call
*<br>If omitted, each Sprite in the bin will be bounced off each other
*<br>If an MC.Sprite, target is a single sprite
*<br>If a SpriteBin, each Sprite in the calling bin will be bounced off all the Sprites in the target bin.
*<br>WARNING: this last simple call can have a huge performance hit if Spritebins (and/or number of physicsUpdates) are large
*@returns {this} Enables method chaining.
*/
MC.SpriteBin.prototype.bounce = function (target) {
if (target === undefined) { // self hit bounce call
var len = this.bin.length;
for (var i = 0 ; i < len ; i++) {
if (!_utils.validHT(this.bin[i])) continue;
for (var j = 0; j < len; j++) {
if (!_utils.validHT(this.bin[j] || i == j)) {
continue;
}
this.bin[i].bounce(this.bin[j]);
}
}
}
else if (target instanceof MC.Sprite) { // single Sprite
// Quick get out of jail check
if (!_utils.validHT(target)) {
return this;
}
// valid target, can proceed
var len = this.bin.length;
for (var i = 0; i < len; i++) {
if (_utils.validHT(this.bin[i])) {
this.bin[i].bounce(target);
}
}
}
else if (target instanceof MC.SpriteBin) { // other SpriteBin
var len = this.bin.length;
var l2 = target.bin.length;
for (var i = 0 ; i < len ; i++) {
if (!_utils.validHT(this.bin[i])) continue;
for (var j = 0; j < l2 ; j++) {
if (_utils.validHT(target.bin[j])) {
this.bin[i].bounce(target.bin[j]);
}
}
}
}
return this;
};
/**
*<hr>
*Empties the bin of everything. Useful for game or level resets
*<br>The SpriteBin itself is still available for use
*@returns {this} Enabled Method Chaining
*/
MC.SpriteBin.prototype.empty = function () {
var len = this.bin.length;
for (var i = 0; i < len; i++) this.pop();
return this;
};
/**
*<hr>
*Adds Sprite(s) to the END of the SpriteBin list
*@param {MC.Sprite} sprites Comma seperated list of sprite(s)
*<br>N.B. Non-sprites will be ignored
*@returns {this} Enabled Method Chaining
*/
MC.SpriteBin.prototype.push = function (sprites) {
var len = arguments.length;
for (var i = 0; i < len; i++) {
if (arguments[i] instanceof MC.Sprite) {
this.bin.push(arguments[i]);
}
}
return this;
};
/**
*<hr>
*Removes the last object from the SpriteBin list
*@returns {this} Enabled Method Chaining
*/
MC.SpriteBin.prototype.pop = function () {
this.bin.pop();
return this;
};
////////////////////
// SPRITE //
////////////////////
/**
*<hr>
*Creates a <tt>Sprite</tt> Object.
*<br>MC considers all drawn assets to be Sprites
*<br>Constructor can be passed an Object of key: value pairs at configuration (see example below), or subsequently with the <tt>setValues(parameters)</tt> method
*<br>Sprites use the <tt>MC.Sprite.update</tt> methos to automatically change position, rotation, scale, animation etc etc.
*<br>Developers should augment the <tt>MC.Sprite.dev</tt> Object to further customise the Sprite's behaviour. CAUTION: Developers are recommended to ONLY modify this namespace, otherwise further MC Engine updates may break your code.
*<br>To assist, the <MC.Sprite.dev.update(param)</tt> method is automatically called after <tt>MC.Sprite.update()</tt>
*@example
*new MC.Sprite({
* type :"circ",
* size1 : 20,
* edgeColor : MC.utils.randomColor(),
* edgeWidth : 2,
* wrapAround : true,
* pos : new MC.Point(MC.maths.randBetween(0,MC.canvas.right),MC.maths.randBetween(0,MC.canvas.bottom)),
* fillColor : MC.utils.randomColor(),
* vel : new MC.Point(MC.maths.randBetween(-100,100),MC.maths.randBetween(-100,100))
* });
*
*@class
*@namespace MC.Sprite
*@constructor
*@this {MC.Sprite}
*@param {Object} [type="circ"] MUST be in format of {type:"circ"} or other type i.e. "poly", "star", "rect", "anim", "pict" or "free"
*@property {string} type="circ" Must be either "circ", "poly", "star", "rect", "anim", "pict" or "free"
*@property {Number} size1=0 For "circ" = radius, "poly" & "star" = outer radius, all other types = width
*@property {Number} size2=0 For "star" = inner radius, "rect", "anim" & "pict" = height
*@property {Number} sides=0 Number of sides for a "poly" or points for a "star"
*@property {Number} scale=1 Scale modifier applied to the base <tt>size1</tt> & <tt>size2</tt> properties
*@property {Object} dev A Place holder Object for Developer customisation.
*<br>Initialised with placeholder functions for
*<br>(1) <tt>dev.update()</tt> (automatically called at the end of any <tt>MC.Sprite.update()</tt> call) and
*<br>(2) <tt>dev.onKill()</tt> (automatically called at the end of the <tt>MC.Sprite.kill()</tt> call)
*<br>CAUTION: It is highly recommended that any Developer ONLY use this namespace to customise Sprites (and their behaviour).
*@property {Array.<MC.Point>} pointsArray Used internally by the MC Engine
*@property {Object} boundingBox Used internally by the MC Engine
*@property {Number} innerRadius Radius of the largest circle which can be contained within the Sprite (centred on it's position)
*@property {Object} hitBox Used internally by the MC Engine
*@property {MC.Color|string} fillColor Will also accept a HTML5 colour name string, e.g. "Black" or "Salmon"
*@property {MC.Color|string} edgeColor As above
*@property {Number} edgeWidth=0 N.B. greater than 1 adds to the render size of the object, but not for purposes of hit calculations
*@property {MC.Point} pos=Mid-Screen In-game canvas position of the CENTRE of the Sprite
*@property {MC.Point} vel=0 In-game velocity of the Sprite (in pixels per second)
*@property {MC.Point} acc=0 In-game Acceleration of the Sprite (in pixels per second per second)
*@property {Number} frontAngle=0 Rotational offset to indicate the "forward" direction of the Sprite (particulary useful with Sprite Sheets)
*@property {Number} angle=0 Current rotational angle of the sprite
*@property {Number} angleVel=0 Angular Velocity of the Sprite (in degrees per second, +ve = clockwise)
*@property {Boolean} gravity=False if True, Sprite will be accelerated by the value set in <tt>MC.game.gravity</tt>
*@property {Boolean} followNose=False if True, Sprite will automatically rotate to match it's velocity vector
*@property {Boolean} edgeBounce=False if True, Sprite will bounce off the edges of the game bounds (as defined <tt>MC.game.setBounds()</tt>)
*<br>N.B. Sprites edgeBouncing have their position retarded by any edge interpeneration
*@property {Boolean} edgeKill=False if true, Sprites that leave the Canvas Bounds will call the kill() method.
*<br>N.B. the kill () method only clears references to Sprites held in a MC.SpriteBin.
*@property {Boolean} wrapAround=False if True, Sprite will leave the game bounds and return from the other side
*@property {Boolean} mobile=True if False, Sprite will not move (or rotate) during <tt>update()</tt> - useful as a pause since other movement properties are maintained
*@property {Boolean} collider=True if false, Sprite Bounding Box is not maintained and all hitTest()'s will be ignored. Useful for background objects
*<br>Set this to false when not required, as it also saves some computing overhead.
*<br>WARNING: must be <tt>True</tt> for <tt>wrapAround</tt> and <tt>edgeBounce</tt> to work.
*@property {MC.Point} lastNormal During <tt>bounce()</tt> lastNormal is updated with the normal of the last collision surface
*@property {MC.Point} lastIP As above; this vector reports the Interpenetration of a sprite after a Hit Detection. In the direction of the lastNormal.
*@property {false|Object_reference} lastHit A reference to the last Sprite hit by this one. Reset to <tt>false</tt> after a "failed" <tt>Hit()</tt> test
*@property {Number} restitution=1 Value applied by default in any <tt>reflect()</tt> calls. 1 = perfectly elastic, 0 = all velocity killed on impact
*@property {Boolean} dead=False "dead" sprites are flagged for destuction (and will not <tt>update()</tt>, or <tt>render()</tt>)
*<br>CAUTION Sprites in SpriteBin's are set to null if they have been detected during an <tt>update()</tt> call.
*<br>If manaually handling Sprites, Developer's need to find other ways to kill references to dead Sprites
*@property {Number} killAge=null Age at which the Sprite will die, i.e. toggle <tt>dead</tt> to <tt>true</tt>
*<br>Age is incremented during <tt>update()</tt>, and not when </tt>MC.game.paused == true</tt>
*@property {Number} age=0 In-game age of the Sprite (in Seconds) updated in <tt>update()</tt>
*@returns {MC.Sprite} a new Object
*/
MC.Sprite = function (parameters) {
// circ,poly,star,rect,anim,pict,free
this.type = "circ";
this.size1 = 0;
this.size2 = 0;
this.sides = 0;
this.scale = 1;
this.dev = {update: function () {},
onKill: function () {} };
this.pointsArray = null;
this.boundingBox = {t:0, b:0, r:0, l:0, pos: new _Point(0,0), angle: 0, scale: 1};
this.hitBox = {type: "circ", scale: 1};
this.innerRadius = 0;
this.fillColor = new _Color();
this.edgeColor = new _Color();
this.edgeWidth = 0;
this.pos = new _Point(_canvas.midX,_canvas.midY);
this.vel = new _Point();
//this.speedClamp = null;
this.acc = new _Point();
this.frontAngle = 0;
this.angle = 0;
this.angleVel = 0;
//this.angleVelClamp = null;
this.gravity = false;
this.followNose = false;
this.edgeBounce = false;
this.edgeKill = false;
this.wrapAround = false;
this.mobile = true;
this.collider = true;
this.lastNormal = new MC.Point();
this.lastIP = new MC.Point();
this.lastHit = false;
//this.physics = true;
//this.mass = 0;
//this.density = 0;
this.restitution = 1;
this.dead = false;
this.killAge = null;
//this.startTime = new Date().getTime();
this.age = 0;
//this.user1 = null;
//this.user2 = null;
//this.user3 = null;
//this.tags = [];
// N.B. special parameters for Animations and Pictures are added during setValues(below)
this.setValues(parameters);
};
/**
*<hr>
*Enables Sprite Sheet animation for a <tt>type:"anim"</tt> Sprite
*@param {Number} GridX The number of horizontal frames in the Sprite Sheet
*@param {Number} GridY The number of vertical frames in the Sprite Sheet
*@param {Number} StartX The X frame section for the first of the animation frames (first = 0, last = GridX -1)
*@param {Number} StartY The Y frame section for the first of the animation frames (first = 0, last = GridY -1)
*@param {Number} FinishX The X frame section for the last of the animation frames (first = 0, last = GridX -1)
*@param {Number} FinishY The Y frame section for the last of the animation frames (first = 0, last = GridY -1)
*@param {Number} SecPerFrame Time between frame transitions
*@param {Number} [BaseRotation=0] Base Rotation to be applied to the Sprite sheet (to counter any art issues)
*@returns {this} Enables method chaining
*/
MC.Sprite.prototype.setAnimation = function (GridX, GridY,StartX,StartY,FinishX,FinishY, SecPerFrame,BaseRotation) {
this.animGrid.x = GridX;
this.animGrid.y = GridY;
this.animStart.x = this.animCurrent.x = StartX;
this.animStart.y = this.animCurrent.y = StartY;
this.animFinish.x = FinishX;
this.animFinish.y = FinishY;
this.animSecPerFrame = SecPerFrame;
this.animBaseRotation = BaseRotation || 0;
return this;
};
/**
*<hr>
*Clones the Sprite and returns a <tt>new</tt> copy.
*N.B. the <tt>MC.Sprite.dev</tt> object may or may not be deep-copied, depending upon how it has been Developer customised
*@returns {MC.Sprite} a <tt>new</tt> Sprite with the same propeties (EXCEPTION: age is set to 0)
*/
MC.Sprite.prototype.clone = function () {
// DEVELOPER NOTE - Extremely important to remember to augment this based upon future MC.Sprite property changes
var ret = new _Sprite({type: this.type});
ret.type = this.type;
ret.size1 = this.size1;
ret.size2 = this.size2;
ret.sides = this.sides;
ret.scale = this.scale;
ret.dev = _utils.cloneWithAssign(this.dev);
if (this.pointsArray == null) ret.pointsArray = null;
else { var len = this.pointsArray.length;
ret.pointsArray = new Array();
for (var i = 0 ; i < len ; i++) ret.pointsArray[i] = this.pointsArray[i].clone();
};
//ret.boundingBox.t = this.boundingBox.t;
//ret.boundingBox.b = this.boundingBox.b;
//ret.boundingBox.r = this.boundingBox.r;
//ret.boundingBox.l = this.boundingBox.l;
//ret.boundingBox.pos.set(this.boundingBox.pos);
//ret.boundingBox.angle = this.boundingBox.angle;
//ret.boundingBox.scale = this.boundingBox.scale;
ret.hitBox.type = this.hitBox.type;
ret.hitBox.scale = this.hitBox.scale;
ret.innerRadius = this.innerRadius;
if (this.fillColor instanceof _Color) ret.fillColor = this.fillColor.clone();
else {ret.fillColor = this.fillColor};
if (this.edgeColor instanceof _Color) ret.edgeColor = this.edgeColor.clone();
else {ret.edgeColor = this.edgeColor};
ret.edgeWidth = this.edgeWidth;
ret.pos.set(this.pos);
ret.vel.set(this.vel);
ret.acc.set(this.acc);
ret.frontAngle = this.frontAngle;
ret.angle = this.angle;
ret.angleVel = this.angleVel;
ret.gravity = this.gravity;
ret.followNose = this.followNose;
ret.edgeBounce = this.edgeBounce;
ret.edgeKill = this.edgeKill;
ret.wrapAround = this.wrapAround;
ret.mobile = this.mobile;
ret.collider = this.collider;
ret.lastNormal.set(this.lastNormal);
ret.lastIP.set(this.lastIP);
ret.restitution = this.restitution;
ret.dead = this.dead;
ret.killAge = this.killAge;
ret.age = 0;
if (this.type == "anim" || this.type == "pict") {
ret.picture = new _Picture(this.picture.img);
ret.loaded = this.loaded;
}
if (this.type == "anim") {
ret.animGrid.set(this.animGrid);
ret.animStart.set(this.animStart);
ret.animFinish.set(this.animFinish);
ret.animCurrent.set(this.animCurrent);
ret.animSecPerFrame = this.animSecPerFrame;
ret.animBaseRotation = this.animBaseRotation;
ret.animTimer = this.animTimer;
}
ret.updateBounds();
return ret;
};
/**
*<hr>
*Configures a Sprite by passing an object made of key:value pairs e.g. <tt>{size1 = 34, size2 = 10,fillColor = new MC.Color(125,230,10,1)}</tt>
*<br> N.B. called automatically by the Constructor with any such dictionary used as a parameter
*@throws Console warning if key is not present in the Sprite
*@param {Object} values {Object of key:value pairs}
*
*/
MC.Sprite.prototype.setValues = function (values) {
if (values === undefined || values === null) {
return this;
}
for (var key in values) {
var newValue = values[key];
if (newValue === undefined) {
console.log("WARNING: Value of "+ key+ " not set in Sprite.setValues()");
continue;
}
var oldValue = this[key];
if (oldValue === undefined) {
console.log("WARNING: Property "+ newValue+ " does not exist in Sprite.setValues()");
continue;
}
this[key] = newValue;
// if it's an animation or picture ... best we add the necessary parameters
if (this[key] === "anim" || this[key] === "pict") {
this.picture = new _Picture();
this.loaded = false;
}
if (this[key] === "anim") {
// FOR ANIMATIONS ONLY
this.animGrid = new _Point();
this.animStart = new _Point();
this.animFinish = new _Point();
this.animCurrent = new _Point();
this.animSecPerFrame = 0;
this.animBaseRotation = 0;
this.animTimer = 0;
}
// establish hitBox type and populate defaults
// default
this.hitBox.type = "rect";
if (this[key] === "circ") this.hitBox.type = "circ";
if (this[key] === "poly" || this[key] === "star") this.hitBox.type = "poly";
}
this.updateBounds();
return this;
};
/**
*<hr>
*Kills the Sprite by setting it's <tt>dead</tt> property to <tt>true</tt>
*<br>Additionally, calls the <tt>dev.onKill()</tt> method which can be customised by the Developer
*<br>CAUTION: <tt>MC.SpriteBin</tt>'s handle setting of their contents to null if a <tt>dead</tt> Sprite is detected. Destruction of manually managed Sprites is the responsibility of the Developer.
*@param {Number} [delay=0] In seconds, if provided, sets the Sprite killAge, and once reached will recall this method (and therefore fire the <tt>dev.onKill()</tt> method)
*/
MC.Sprite.prototype.kill = function (delay) {
if (delay === undefined || delay == 0) {
this.dead = true;
this.dev.onKill();
}
else {
this.killAge = this.age + delay;
}
};
/**
*<hr>
*Automatically updates the Sprite's position due to velocity, acceleration etc.
*This method should be called within any game <tt>requestAnimationFrame()</tt> game loop, and within any required <MC.game.physicsUpdates</tt> loop
*Once complete, this method calls the Developer customisable <tt>MC.Sprite.dev.update()</tt> method
*@param {Number} [DeltaTime=MC.game.deltaTime] The Time increment (in Seconds) to be applied to the Sprite
*<br>N.B. usually <tt>MC.game.deltaTime</tt> and applied within the GameLoop
*/
MC.Sprite.prototype.update = function (DeltaTime) {
var DT = 0;
if (DeltaTime !== undefined) {
DT = DeltaTime / _game.physicsUpdates;
}
else {
DT = _game.deltaTime / _game.physicsUpdates;
}
var A;
this.age += DT;
if (this.killAge !== null && this.age >= this.killAge) {
this.kill(0);
}
// confirm required images are loaded (and process if not)
if (this.type === "pict" || this.type === "anim") {
if (this.picture.loaded && !this.loaded) {
this.size1 = this.picture.width;
this.size2 = this.picture.height;
this.pointsArray = null;
this.loaded = true;
}
}
// advance and process Animation
if (this.type === "anim" && this.loaded) {
this.animTimer += DT;
if (this.animTimer >= 1/this.animSecPerFrame) {
// need to proceed Animation step
if (this.animCurrent.equals(this.animFinish)) {
this.animCurrent.set(this.animStart);
}
else {
if (this.animCurrent.x < this.animGrid.x -1) {
this.animCurrent.x += 1;
}
else {
this.animCurrent.x = 0 ;
this.animCurrent.y += 1;
}
}
this.animTimer %= 1/this.animSecPerFrame;
}
}
if (this.mobile && !this.dead) {
A = this.acc.clone();
if (this.gravity) {
A.add(_game.gravity.clone().div(_game.physicsUpdates));
}
this.vel.add(A.times(DT));
this.pos.add(this.vel.x * DT, this.vel.y * DT);
this.angle += this.angleVel * DT;
if (this.followNose === true && (this.vel.x !== 0 || this.vel.y !== 0)) {
this.angle = this.vel.getAngle();
}
this.angle = _utils.fixAngle(this.angle);
if (this.edgeBounce === true) {
var bounced = false;
if (this.boundingBox.t < _canvas.top && this.vel.y < 0) {
this.vel.times(1,-1);
this.pos.y += this.boundingBox.t - _canvas.top
bounced = true;
}
if (this.boundingBox.b > _canvas.bottom && this.vel.y > 0) {
this.vel.times(1,-1);
this.pos.y += _canvas.bottom - this.boundingBox.b;
bounced = true;
}
if (this.boundingBox.l < _canvas.left && this.vel.x < 0) {
this.vel.times(-1,1);
this.pos.x += _canvas.left - this.boundingBox.l;
bounced = true;
}
if (this.boundingBox.r > _canvas.right && this.vel.x > 0) {
this.vel.times(-1,1);
this.pos.x -= this.boundingBox.r - _canvas.right;
bounced = true;
}
if (bounced === true && this.followNose === true) {
this.angle = this.vel.getAngle() + this.frontAngle;
}
}
if (this.wrapAround === true) {
var t = this.boundingBox.t;
var b = this.boundingBox.b;
var l = this.boundingBox.l;
var r = this.boundingBox.r;
if (t > _canvas.bottom && this.vel.y > 0) {
this.pos.y = _canvas.top - (b - t)/2;
}
if (b < _canvas.top && this.vel.y < 0) {
this.pos.y = _canvas.bottom + (b - t)/2;
}
if (r < _canvas.left && this.vel.x < 0) {
this.pos.x = _canvas.right + (r - l)/2;
}
if (l > _canvas.right && this.vel.x > 0) {
this.pos.x = _canvas.left - (r - l)/2;
}
}
if (this.edgeKill === true) {
if (this.boundingBox.t > _canvas.bottom ||
this.boundingBox.b < _canvas.top ||
this.boundingBox.r < _canvas.left ||
this.boundingBox.l > _canvas.right) {
this.kill();
}
}
this.updateBounds();
}
// IMPORTANT - Herein lies the Developers avenue for customisation
this.dev.update();
};
/**
*<hr>
*Renders the Sprite to the canvas (as defined by <tt>MC.game.canvas</tt>)
*<br>Usually called at the end of the game loop (after <tt>update()</tt>)
*@returns {this} Enables method chaining
*/
MC.Sprite.prototype.render = function () {
if (this.type === "circ") {
_draw.circle(this.pos,(this.size1 * this.scale),this.fillColor,this.edgeColor,this.edgeWidth);
}
if (this.type === "poly" || this.type === "star" || this.type === "rect" || this.type === "free") {
_draw.array(this.pos,this.pointsArray,this.fillColor,this.edgeColor,this.edgeWidth);
}
if (this.type === "pict") {
this.picture.render(this.pos,this.angle+this.frontAngle,this.size1 * this.scale,this.size2 * this.scale);
}
if (this.type === "anim") {
var dx = (this.picture.width / this.animGrid.x) * this.scale;
var dy = (this.picture.height / this.animGrid.y) * this.scale;
this.picture.render(this.pos,this.angle+this.frontAngle+this.animBaseRotation,
dx,dy,
new _Point(this.animCurrent.x/this.animGrid.x,this.animCurrent.y/this.animGrid.y),
new _Point((this.animCurrent.x + 1)/this.animGrid.x,(this.animCurrent.y + 1)/this.animGrid.y)
);
}
return this;
};
/**
*<hr>
*Refreshes the internally held bounding box coordinates
*<br>This is called automatically upon <tt>update()</tt> and Developers should not need to call it
*<br>However: Sprites that are manually moved (by direct manipulation of the <tt>pos</tt> or <tt>angle</tt> properties), or flagged as NOT <tt>mobile</tt> may need it
*/
MC.Sprite.prototype.updateBounds = function () {
// Non-colliders do not require Bounds
if (!this.collider) {
return;
}
var DAngle, DScale, len = 0; // For "free" points Array handling
function setFlags(o) { // Internal function, no need for users to know. Sets boundingBox flags re angle, pos and scale
o.boundingBox.angle = o.angle;
o.boundingBox.pos.set(o.pos);
o.boundingBox.scale = o.scale;
return;
}
function findEdges(o) { // Internal function, loops through pointsArray and populates boundingBox t,b,l & r
if (this.pointsArray !== null) {
len = o.pointsArray.length;
var t = o.pointsArray[0].y;
var b = o.pointsArray[0].y;
var l = o.pointsArray[0].x;
var r = o.pointsArray[0].x;
for (var i = 1; i < len ; i = i+1) {
t = _maths.minOf(o.pointsArray[i].y, t);
b = _maths.maxOf(o.pointsArray[i].y, b);
l = _maths.minOf(o.pointsArray[i].x, l);
r = _maths.maxOf(o.pointsArray[i].x, r);
}
o.boundingBox.t = t + o.pos.y;
o.boundingBox.b = b + o.pos.y;
o.boundingBox.l = l + o.pos.x;
o.boundingBox.r = r + o.pos.x;
}
return;
}
if (!this.dead) {
if (this.type === "circ") { // get circles out of the way
this.boundingBox.t = this.pos.y - (this.size1 * this.scale);
this.boundingBox.b = this.pos.y + (this.size1 * this.scale);
this.boundingBox.l = this.pos.x - (this.size1 * this.scale);
this.boundingBox.r = this.pos.x + (this.size1 * this.scale);
setFlags(this);
this.innerRadius = this.getInnerRadius();
return; // no more circles in this function. Only pointArrays
}
if (this.type === "poly") {
this.pointsArray = _utils.getPolyArray(this.sides,this.size1 * this.scale,this.angle + this.frontAngle);
}
else if (this.type === "star") {
this.pointsArray = _utils.getStarArray(this.sides,this.size1* this.scale,this.size2* this.scale,this.angle + this.frontAngle);
}
else if (this.type === "rect" || this.type === "pict") {
this.pointsArray = _utils.getRectArray(this.size1 * this.scale, this.size2 * this.scale,this.angle + this.frontAngle);
}
else if (this.type === "anim") {
this.pointsArray = _utils.getRectArray((this.size1 * this.scale)/this.animGrid.x, (this.size2 * this.scale)/this.animGrid.y,this.angle + this.frontAngle + this.animBaseRotation);
}
else if (this.type === "free") { // the expensive one !!
if (this.boundingBox.angle !== this.angle || this.scale !== this.boundingBox.scale) {
DAngle = this.boundingBox.angle - this.angle;
DScale = this.scale / this.boundingBox.scale;
len = this.pointsArray.length;
for (var i = 0; i < len; i++) {
this.pointsArray[i].rotate(DAngle).scale(DScale);
}
}
}
setFlags(this);
findEdges(this);
this.innerRadius = this.getInnerRadius();
return;
}
};
/**
*<hr>
*Draws a 1x pixel outline showing the sprite's Bounding Box
*<br> Useful for debugging purposes
*<br> 1 - either an MC.Color object or an HTML5 color name can be used as a Color (White is default)
*@param {Color|string} [color="White"] - Desired outline color (1)
*@returns {this} Enabled method chaining
*/
MC.Sprite.prototype.drawBB = function(color = "White") {
MC.draw.polyLine( [new _Point(this.boundingBox.l,this.boundingBox.t),
new _Point(this.boundingBox.r,this.boundingBox.t),
new _Point(this.boundingBox.r,this.boundingBox.b),
new _Point(this.boundingBox.l,this.boundingBox.b),
new _Point(this.boundingBox.l,this.boundingBox.t)], _utils.colorOrString(color),1);
return this;
};
/**
*<hr>
*Draws a 1x pixel circle representing the Sprite's inner Circle.
*<br>Useful for debugging purposes
*<br> 1 - either an MC.Color object or an HTML5 color name can be used as a Color (White is default)
*@param {Color|string} [color="White"] - Desired outline color (1)
*@returns {this} Enabled method chaining
*/
MC.Sprite.prototype.drawIC = function(color = "White") {
MC.draw.circle(this.pos,this.innerRadius,null,_utils.colorOrString(color),2);
return this;
};
/**
*<hr>
*Reflects the sprite from a surface normal, resulting in a new direction of motion (and possible reduction of speed)
*<br>N.B currently the MC Engine does not support transfer of momentum (rotational or otherwise) between objects, so speed and rotation angle are preserved
*Used during the Sprite<tt> bounce()</tt> method calls
*@param {MC.Point} normal Normal of the surface impacted.
*@param {Number} [restitution=this.restitution] Used if the collision requires a restitution other than the Sprite's own.
*@returns {this} Enables method chaining
*/
MC.Sprite.prototype.reflect = function (normal,restitution)
{
var V = this.vel.clone();
var N = new _Point();
if (normal !== undefined)
N.set(normal).normalise();
else {
N.set(this.lastNormal).normalise() }
N.times(2 * _maths.dotProd(V,N));
V.minus(N);
var R = this.restitution;
if (restitution !== undefined) {
R = restitution;
}
this.vel = V.times(R);
return this;
};
/**
*<hr>
*Checks is a Sprite is moving towards a particular postion
*@param {MC.Point|MC.Sprite} target Screen location or Sprite sought
*@returns {Boolean} True is the Sprite's velocity will take it closer to the target Location
*/
MC.Sprite.prototype.towards = function (target) {
var t = new MC.Point();
if (target instanceof MC.Sprite) {
t.set(target.pos).minus(this.pos);
}
else {
t.set(target).minus(this.pos);
}
var a = this.vel.angleTo(t);
if (a < 90 || a > 270) {
return true;
}
return false;
};
/**
*<hr>
*Moves the Sprite's position by the desired amount
*<br>This is the prefered method to translate a Sprite, as it's bounding box is automatically updated.
*@param {MC.Point} Vector Translation required
*@returns {this} Enables Method Chaining
*/
MC.Sprite.prototype.moveBy = function(Vector) {
if (_utils.hasXY(Vector)) {
this.pos.add(Vector);
this.updateBounds();
}
return this;
};
/**
*<hr>
*Moves the Sprite's position to the desired place
*<br>This is the prefered method to move a Sprite, as it's bounding box is automatically updated.
*@param {MC.Point} Position where the new Sprite centre will be
*@returns {this} Enables Method Chaining
*/
MC.Sprite.prototype.moveTo = function(Position) {
if (_utils.hasXY(Position)) {
this.pos.set(Position);
this.updateBounds();
}
return this;
};
/**
*<hr>
*Rotates the Sprite by the desired amount
*<br>This is the prefered method to rotate a Sprite, as it's bounding box is automatically updated.
*@param {angle} Angle Rotation required (in degrees, +ve = clockwise)
*@returns {this} Enables Method Chaining
*/
MC.Sprite.prototype.rotateBy = function(angle) {
if (_utils.isNumeric(angle)) {
this.angle += angle;
this.updateBounds();
}
return this;
};
/**
*<hr>
*Retards the Sprite's position by the last recorded <tt>lastIP</tt> value.
*<br>The <tt>lastIP</tt> is then reset to zero.
*@returns {this} Enables method chaining
*/
MC.Sprite.prototype.retard = function() {
this.pos.add(this.lastIP);
this.lastIP.set(0,0);
return this;
};
/**
*<hr>
*Resets the Sprite's <tt>lastNormal</tt> and <tt>lastIP</tt> properties
*<br>Called automatically after a failed <tt>hitTest()</tt>
*@returns {this} Enables method chaining
*/
MC.Sprite.prototype.resetHit = function () {
this.lastIP.set(0,0);
this.lastNormal.set(0,0);
this.lastHit = false;
return this;
};
/**
*<hr>
*detects whether a Sprite has hit another and, if so, reflects off it
*<br>N.B. This is an approximation using the velocity of the calling Sprite.
*<br>The Sprite's <tt>lastIP, lastNormal and lastHit</tt> properties are reset at the beginning of this call.
*<br>So Developers can use <tt>if (mySprite.lastHit) { // do something} </tt> to detect a bounce during the update cycle
*<br>Rotating targets may lead to irregular results.
*<br>Velocity will be reduced based upon the calling sprite's Restitution value. Otherwise speed is maintained.
*<br>For simplicity Sprite.type "star" and "poly" are treated as circles, and there is no transfer of momentum/speed between objects.
*<br>The following collision types are supported
*<br>... Circle on Circle
*<br>... Circle on rotated Rectangle (and vice versa)
*<br>... Axis Aligned Rectangles
*<br>Non-axis aligned rectangles will "bounce" but use their (and/or target's) outer bounding boxes
*@param {MC.Sprite|MC.SpriteBin} target The Sprite to be tested against, if a SpriteBin is specified, then the <tt>bounce()</tt> method is called against all the contents
*@returns {this} Enables Method Chaining
*@throws Console warning if target is undefined or otherwise not valid
*<br>.
*/
MC.Sprite.prototype.bounce = function (target) {
this.resetHit();
var hit = false;
if (target === undefined) {
console.log("WARNING. No target passed to hitTest(): self returned");
return this;
}
if (target instanceof MC.Sprite) {
b(this,target);
}
else if (target instanceof MC.SpriteBin) {
var len = target.bin.length;
for (var i = 0; i < len ; i++) b(this,target.bin[i]);
}
function b(me,t) {
if (!_utils.validHT(t)) {
console.log("WARNING. Invalid target passed to hitTest(): self returned");
return this;
}
// Quick Bounding Box Check
if (!_utils.checkBB(me.boundingBox,t.boundingBox)) {
return this;
}
// Circle on Circle (types)
if ((me.type == "circ" || me.type == "poly" || me.type == "star") && (t.type == "circ" || t.type == "poly" || t.type == "star") ) {
if(me.hitTestCircOnCirc(t)) hit = true;
}
// Circle on Rectangle(s)
else if ((me.type == "circ" || me.type == "poly" || me.type == "star") && (t.type == "anim" || t.type == "rect" || t.type == "pict")) {
if(me.hitTestCircOnRect(t)) hit = true;
}
// Rect on Circ
else if ((t.type == "circ" || t.type == "poly" || t.type == "star") && (me.type == "anim" || me.type == "rect" || me.type == "pict")) {
if(me.hitTestCircOnRect(t)) hit = true;
}
else if ((t.type == "anim" || t.type == "rect") && (me.type == "anim" || me.type == "rect" || me.type == "pict")) {
if(me.AARectHit(t)) hit = true;
}
if (hit) {
me.lastHit = t;
me.moveBy(me.lastIP);
me.reflect();
}
}
return this;
};
// Internal Function
MC.Sprite.prototype.hitTestCircOnCirc = function (sprite) {
var s1,s2;
if (this.type === "star") {
s1 = _maths.maxOf(this.size1,this.size2);
}
else { s1 = this.size1;}
if (sprite.type === "star") {
s2 = _maths.maxOf(sprite.size1,sprite.size2);
}
else { s2 = sprite.size1;}
var R = (s1 * this.scale) + (s2 * sprite.scale);
var RSqr = R*R;
// no hit
if (RSqr < _maths.rangeSqr(this.pos,sprite.pos)) {
this.resetHit();
return false;
}
// hit
else {
var norm = sprite.pos.clone().minus(this.pos).normalise()
this.lastNormal = norm;
var D = R - _maths.range(this.pos,sprite.pos);
this.lastIP = norm.invert().times(D);
this.lastHit = sprite;
return true;
}
};
// Internal Function
MC.Sprite.prototype.hitTestCircOnRect = function (sprite) {
// establish what is the circle, and what is the rectangle
var rect, circ;
var swap = false;
if (this.type == "circ") {
circ = this;
rect = sprite;
}
else {
circ = sprite;
rect = this;
swap = true;
}
// rectangle variables
var rc = rect.pos.clone(); // rectangle centre
var rw = rect.size1 * rect.scale / 2; // rect width (halved)
var rh = rect.size2 * rect.scale / 2; // rect height (halved)
var rr = rect.angle + rect.frontAngle; // rectangle rotation
// divide for Spritesheet elements
if (rect.type == "anim") {
rw /= rect.animGrid.x;
rh /= rect.animGrid.y;
rr += rect.animBaseRotation;
}
// circle variables
var cc = circ.pos.clone(); // circle centre
var cr = circ.size1 * circ.scale; // circle radius
var cv = circ.vel.clone(); // circle velocity
if (swap) {
cv = rect.vel.clone().invert(); // or (depending on case ... the Rectangle velocity)
}
// set circle centre, so Rectangle Origin is @ (0,0)
cc.minus(rc);
// Rotate Circle Centre and Velocity to make Rectangle Axis-aligned
cc.rotate(-rr);
cv.rotate(-rr);
// Now have to establish what quadrent the Circle centre is, and rotate by 0, -90, 90 or 180
// So brings the circle centre into the +x/+y quadrent N.B. rectangle height and width will be swapped from -90 and 90 degree cases
var MasterR = 0;
if (cc.x <= 0 && cc.y >= 0) {
MasterR -= 90;
var tmp = rw; rw = rh; rh = tmp;}
else if (cc.x >= 0 && cc.y <= 0) {
MasterR += 90;
var tmp = rw; rw = rh; rh = tmp;}
else if (cc.x <= 0 && cc.y <= 0) {
MasterR += 180;}
// rotate cc and cv around origin
cc.rotate(MasterR);
cv.rotate(MasterR);
/*
* (0,0)------------------------------------
* | |
* | |
* | | cl (cc1 LEFT)
* | |
* | |
*---------------------------(rw,rh)
* | ct (cc3 CORNER)
* | (cc2 TOP)
*/
// Sanity preserving variables
var cx = cc.x; var cy = cc.y; var ct = cy - cr; var cl = cx - cr;
// Quick Escape check
if (ct >= rh || cl >= rw) {
this.resetHit();
return false;
}
var Norm = new _Point();
var IP = new _Point();
// cc1 Left of Circle (not corner)
if (cl <= rw && cy <= rh && cv.x <= 0) {
this.resetHit();
Norm.set(1,0).rotate(rr - MasterR);
IP.set(Norm).times(rw-cl);
if (swap) {
Norm.invert();
IP.invert();
}
this.lastNormal = Norm;
this.lastIP = IP;
this.lastHit = sprite;
return true;
}
// cc2 Top of Circle (not corner)
if (ct <= rh && cx <= rw && cv.y <= 0) {
this.resetHit();
Norm.set(0,1).rotate(rr - MasterR);
IP.set(Norm).times(rh-ct);
if (swap) {
Norm.invert();
IP.invert();
}
this.lastNormal = Norm;
this.lastIP = IP;
this.lastHit = sprite;
return true;
}
// cc3 (corner)
if (cx >= rw && cy >= rh) {
// inside radius?
var R = _maths.range(cx,cy,rw,rh);
if (R >= cr || (cv.x > 0 && cv.y > 0)) {
this.resetHit();
return false;
}
else {
Norm = cc.clone().minus(rw,rh).rotate(rr - MasterR).normalise();
IP = this.lastNormal.clone().times(cr - R);
if (swap) {
Norm.invert();
IP.invert();
}
this.lastNormal = Norm;
this.lastIP = IP;
this.lastHit = sprite;
return true;
}
}
// Just in case
this.resetHit();
return false;
};
// internal function
MC.Sprite.prototype.AARectHit = function (sprite) {
var bb1 = this.boundingBox;
var vel = this.vel.clone();
var bb2 = sprite.boundingBox;
var penx = 0;
var peny = 0;
if (vel.x > 0 && bb1.r > bb2.l) { // going right
penx = bb2.l - bb1.r;
}
if (vel.x < 0 && bb1.l < bb2.r) { // going left
penx = bb2.r - bb1.l;
}
if (vel.y > 0 && bb1.b > bb2.t) { // going down
peny = bb2.t - bb1.b;
}
if (vel.y < 0 && bb1.t < bb2.b) { // going up
peny = bb2.b - bb1.t;
}
if (Math.abs(penx) < Math.abs(peny)) {
// using X
this.lastIP.set(penx,0);
this.lastNormal.set(this.lastIP).normalise();
this.lastHit = sprite;
return true;
}
else {
this.lastIP.set(0,peny);
this.lastNormal.set(this.lastIP).normalise();
this.lastHit = sprite;
return true;
}
this.resetHit();
return false;
};
// Internal Function BUGGED DO NOT USE
MC.Sprite.prototype.hitTestRectOnRect = function (sprite) {
var swap = false;
var R1 = this;
var R2 = sprite;
var norm = new MC.Point(0,0);
var IP = new MC.Point(0,0);
if(InBox(R2,R1,1)) {R1.lastNormal = norm; R1.lastIP = IP; R1.vel.set(0,0);};
function InBox(T1,T2,o) {
// Calling Rectangle
var r1c = T1.pos.clone(); // postion
var r1v = T1.vel.clone(); // velocity
if (0 == 2) {
r1v.set(TS.vel);
}
var r1w = T1.size1 * T1.scale/2; // width
var r1h = T1.size2 * T1.scale/2; // height
var r1a = T1.angle + T1.frontAngle; // angle
if (T1.type === "anim") { // if animated
r1w /= T1.animGrid.x;
r1h /= T1.animGrid.y;
r1a += T1.animBaseRotation;
}
// Target Rectangle
var r2c = T2.pos.clone();
var r2w = T2.size1 * T2.scale/2;
var r2h = T2.size2 * T2.scale/2;
var r2a = T2.angle + T2.frontAngle;
if (T2.type === "anim") {
r2w /= T2.animGrid.x;
r2h /= T2.animGrid.y;
r2a += T2.animBaseRotation;
}
// Place parent (r1) at Origin, rotate to 0
r2c.minus(r1c).rotate(-r1a);
// establish secondary rotation to bring sprite to x+,y+
var MasterR = 0;
if (r2c.x <= 0 && r2c.y >= 0) {
MasterR -= 90;
var tmp = r1w; r1w = r1h; r1h = tmp;}
else if (r2c.x >= 0 && r2c.y <= 0) {
MasterR += 90;
var tmp = r1w; r1w = r1h; r1h = tmp;}
else if (r2c.x <= 0 && r2c.y <= 0) {
MasterR += 180;}
// rotate centre again and set velocity
r2c.rotate(MasterR);
// make original sprite velocity the relative velocity of the Target
r1v.rotate(-r1a + MasterR);
if (o == 2) {
r1v.invert();
}
// quick get out if moving away
if (r1v.x > 0 && r1v.y > 0) {
return false;
}
// Array of target Sprite Corners
var C2 = new Array();
C2[0] = new MC.Point(r2w,r2h).rotate(r2a+MasterR).add(r2c);
C2[1] = new MC.Point(-r2w,r2h).rotate(r2a+MasterR).add(r2c);
C2[2] = new MC.Point(-r2w,-r2h).rotate(r2a+MasterR).add(r2c);
C2[3] = new MC.Point(r2w,-r2h).rotate(r2a+MasterR).add(r2c);
// pot luck method ... returns first corner gets there wins
for (var i = 0; i < 4 ; i++)
{
if (C2[i].x > r1w || C2[i].x < 0 || C2[i].y > r1h || C2[i].y < 0 ) continue; // if either x,y > r1w,r1h .. no hit
// X goes first
if (r1v.x < 0) {
norm.set(-1,0).rotate(r1a - MasterR);
IP = norm.clone().times(r1w - C2[i].x);
return true;
}
if (r1v.y < 0) {
norm.set(0,-1).rotate(r1a - r2a - MasterR);
IP = norm.clone().times(r1h - C2[i].y);
return true;
}
}
return false;
}
};
/**
*<hr>
*Returns an array of the screen coordinates of the vertices of a non-circular sprite
*@returns {Array.<MC.Point>} Screen coordinates of the Sprite's vertices
*/
MC.Sprite.prototype.getPointsArray = function () {
var ret = new Array();
var len = this.pointsArray.length;
for (var i = 0; i < len ; i++) {
ret[i] = this.pointsArray[i].clone().add(this.pos);
}
return ret;
};
/**
*<hr>
*Returns the inner Radius of the Sprite. Anything inside this radius is 100% certain to be be inside the Sprite
*<br>CAUTION: "free" type sprites can have interesting results.
*<br> Used internally by the MC engine to populate the sprite's <tt>innerRadius</tt> property
*@returns {Number} The Sprite's inner radius
*/
MC.Sprite.prototype.getInnerRadius = function () {
if (this.type == "circ") return this.size1 * this.scale;
else if (this.type == "rect" || this.type == "pict") return _maths.minOf(this.size1,this.size2) * (this.scale / 2);
else if (this.type == "star") return _maths.minOf(this.size1,this.size2) * this.scale;
else if (this.type == "poly") return (this.size1 * this.scale) * Math.cos(Math.PI / this.sides);
else if (this.type == "anim") {
if (this.loaded) {
return _maths.minOf(this.size1 / this.animGrid.x,this.size2/this.animGrid.y) * (this.scale / 2);
}
else {
return 1;
}
}
else {
var o = new _Point(0,0);
var d = _maths.rangeSqr(o,this.pointsArray[0]);
var len = this.pointsArray.length;
for (var i = 1; i < len; i++) {
d = _maths.minOf(d,_maths.rangeSqr(o,this.pointsArray[i]));
}
return Math.sqrt(d);
}
};
// Internal Function
MC.Sprite.prototype.hitSprite = function(target) {
// Auto reset
this.resetHit();
// Bounding Boxes check
if (!_utils.checkBB(this.boundingBox,target.boundingBox)) {
return false;
}
// Inner Circle check
if (_utils.circInCircle(this.pos,this.innerRadius,target.pos,target.innerRadius)) {
this.lastHit = target;
return true;
}
// eliminate any circles
if (this.type == "circ") {
if (target.type == "circ") {
return false; // has been determined by the Inner Circle check
}
else if (target.type == "rect" || target.type == "anim" || target.type == "pict") {
var res = this.hitTestCircOnRect(target);
if (res) {
this.lastHit = target;
return true;
}
this.resetHit();
return false;
}
else {
if (_utils.shapeInCircle(target.getPointsArray(),this.pos,this.innerRadius)) {
this.lastHit = target;
return true;
}
else {
return false;
}
}
}
if (target.type == "circ") {
if (this.type == "rect" || this.type == "rect" || this.type == "pict") {
var res = target.hitTestCircOnRect(this);
if (res) {
this.lastHit = target;
return true;
}
return false;
}
else {
if (_utils.shapeInCircle(this.getPointsArray(),target.pos,target.innerRadius)) {
this.lastHit = target;
return true;
}
else {
return false;
}
}
}
//straight shape on shape now
if (_utils.shapeInShape(this.getPointsArray(),target.getPointsArray())) {
this.lastHit = target;
return true;
}
else {
return true;
}
};
/**
*<hr>
*Checks if a Sprite has "hit" (i.e. intersects with) something
*@param {MC.Point|MC.Sprite|MC.SpriteBin} target The something to check intersection with
*<br>N.B. it is very common for the "Point" option to be the Mouse Coordinates, e.g. <tt>MySprite.hit(MC.game.mouse)</tt>
*<br>N.B. Sprites with <tt>collider</tt> property set to </tt>false</tt> will always retuns a <tt>false</t>>
*@returns {Boolean} <tt>true</tt> if the point or any sprite (singular or in the SpriteBin) intersects the calling Sprite, else <tt>false</tt>
*/
MC.Sprite.prototype.hit = function (target) {
if (this.collider == false) {
return false;
}
if (target instanceof MC.Point) {
// Bounding Box check
if (!_utils.checkBB(this.boundingBox,target)) {
return false;
}
if (this.type == "circ") {
return _utils.pointInCirc(target,this.pos,this.size1* this.scale);
}
// innerRadius Check
if (_utils.pointInCirc(target, this.pos,this.innerRadius)) {
return true;
}
else if (this.type == "rect" || this.type == "anim" || this.type == "pict") {
return _utils.pointInRect(target,this.getPointsArray());
}
else {
return _utils.pointInShape(target,this.getPointsArray());
}
}
if (target instanceof MC.Sprite) {
return this.hitSprite(target);
}
if (target instanceof MC.SpriteBin) {
var len = target.bin.length;
var res = false;
for (var i = 0; i < len; i++) {
res = false;
if (target.bin[i] == null || target.bin[i] == undefined) {
continue;
}
if (target.bin[i] instanceof MC.Sprite) {
if (!target.bin[i].dead && target.bin[i].collider) {
res = this.hitSprite(target.bin[i]);
}
}
if (res) {
return true;
}
}
return false;
}
};
// End of File .. Nothing further to see here. Thanks for coming 8-)