diff --git a/demo/js/Demo.js b/demo/js/Demo.js index e31db07..c623753 100644 --- a/demo/js/Demo.js +++ b/demo/js/Demo.js @@ -1,375 +1,381 @@ -/** -* The Matter.js demo page controller and example runner. -* -* NOTE: For the actual example code, refer to the source files in `/examples/`. -* -* @class Demo -*/ - -(function() { - - var _isBrowser = typeof window !== 'undefined' && window.location, - _useInspector = _isBrowser && window.location.hash.indexOf('-inspect') !== -1, - _isMobile = _isBrowser && /(ipad|iphone|ipod|android)/gi.test(navigator.userAgent), - _isAutomatedTest = !_isBrowser || window._phantom; - - var Matter = _isBrowser ? window.Matter : require('../../build/matter-dev.js'); - - var Demo = {}; - Matter.Demo = Demo; - - if (!_isBrowser) { - module.exports = Demo; - window = {}; - } - - // Matter aliases - var Body = Matter.Body, - Example = Matter.Example, - Engine = Matter.Engine, - World = Matter.World, - Common = Matter.Common, - Bodies = Matter.Bodies, - Events = Matter.Events, - Mouse = Matter.Mouse, - MouseConstraint = Matter.MouseConstraint, - Runner = Matter.Runner, - Render = Matter.Render; - - // MatterTools aliases - if (window.MatterTools) { - var Gui = MatterTools.Gui, - Inspector = MatterTools.Inspector; - } - - Demo.create = function(options) { - var defaults = { - isManual: false, - sceneName: 'mixed', - sceneEvents: [] - }; - - return Common.extend(defaults, options); - }; - - Demo.init = function() { - var demo = Demo.create(); - Matter.Demo._demo = demo; - - // get container element for the canvas - demo.container = document.getElementById('canvas-container'); - - // create an example engine (see /examples/engine.js) - demo.engine = Example.engine(demo); - - // run the engine - demo.runner = Engine.run(demo.engine); - - // create a debug renderer - demo.render = Render.create({ - element: demo.container, - engine: demo.engine - }); - - // run the renderer - Render.run(demo.render); - - // add a mouse controlled constraint - demo.mouseConstraint = MouseConstraint.create(demo.engine, { - element: demo.render.canvas - }); - - World.add(demo.engine.world, demo.mouseConstraint); - - // pass mouse to renderer to enable showMousePosition - demo.render.mouse = demo.mouseConstraint.mouse; - - // get the scene function name from hash - if (window.location.hash.length !== 0) - demo.sceneName = window.location.hash.replace('#', '').replace('-inspect', ''); - - // set up a scene with bodies - Demo.reset(demo); - Demo.setScene(demo, demo.sceneName); - - // set up demo interface (see end of this file) - Demo.initControls(demo); - - // pass through runner as timing for debug rendering - demo.engine.metrics.timing = demo.runner; - - return demo; - }; - - // call init when the page has loaded fully - if (!_isAutomatedTest) { - if (window.addEventListener) { - window.addEventListener('load', Demo.init); - } else if (window.attachEvent) { - window.attachEvent('load', Demo.init); - } - } - - Demo.setScene = function(demo, sceneName) { - Example[sceneName](demo); - }; - - // the functions for the demo interface and controls below - Demo.initControls = function(demo) { - var demoSelect = document.getElementById('demo-select'), - demoReset = document.getElementById('demo-reset'); - - // create a Matter.Gui - if (!_isMobile && Gui) { - demo.gui = Gui.create(demo.engine, demo.runner, demo.render); - - // need to add mouse constraint back in after gui clear or load is pressed - Events.on(demo.gui, 'clear load', function() { - demo.mouseConstraint = MouseConstraint.create(demo.engine, { - element: demo.render.canvas - }); - - World.add(demo.engine.world, demo.mouseConstraint); - }); - } - - // create a Matter.Inspector - if (!_isMobile && Inspector && _useInspector) { - demo.inspector = Inspector.create(demo.engine, demo.runner, demo.render); - - Events.on(demo.inspector, 'import', function() { - demo.mouseConstraint = MouseConstraint.create(demo.engine); - World.add(demo.engine.world, demo.mouseConstraint); - }); - - Events.on(demo.inspector, 'play', function() { - demo.mouseConstraint = MouseConstraint.create(demo.engine); - World.add(demo.engine.world, demo.mouseConstraint); - }); - - Events.on(demo.inspector, 'selectStart', function() { - demo.mouseConstraint.constraint.render.visible = false; - }); - - Events.on(demo.inspector, 'selectEnd', function() { - demo.mouseConstraint.constraint.render.visible = true; - }); - } - - // go fullscreen when using a mobile device - if (_isMobile) { - var body = document.body; - - body.className += ' is-mobile'; - demo.render.canvas.addEventListener('touchstart', Demo.fullscreen); - - var fullscreenChange = function() { - var fullscreenEnabled = document.fullscreenEnabled || document.mozFullScreenEnabled || document.webkitFullscreenEnabled; - - // delay fullscreen styles until fullscreen has finished changing - setTimeout(function() { - if (fullscreenEnabled) { - body.className += ' is-fullscreen'; - } else { - body.className = body.className.replace('is-fullscreen', ''); - } - }, 2000); - }; - - document.addEventListener('webkitfullscreenchange', fullscreenChange); - document.addEventListener('mozfullscreenchange', fullscreenChange); - document.addEventListener('fullscreenchange', fullscreenChange); - } - - // keyboard controls - document.onkeypress = function(keys) { - // shift + a = toggle manual - if (keys.shiftKey && keys.keyCode === 65) { - Demo.setManualControl(demo, !demo.isManual); - } - - // shift + q = step - if (keys.shiftKey && keys.keyCode === 81) { - if (!demo.isManual) { - Demo.setManualControl(demo, true); - } - - Runner.tick(demo.runner, demo.engine); - } - }; - - // initialise demo selector - demoSelect.value = demo.sceneName; - Demo.setUpdateSourceLink(demo.sceneName); - - demoSelect.addEventListener('change', function(e) { - Demo.reset(demo); - Demo.setScene(demo,demo.sceneName = e.target.value); - - if (demo.gui) { - Gui.update(demo.gui); - } - - var scrollY = window.scrollY; - window.location.hash = demo.sceneName; - window.scrollY = scrollY; - Demo.setUpdateSourceLink(demo.sceneName); - }); - - demoReset.addEventListener('click', function(e) { - Demo.reset(demo); - Demo.setScene(demo, demo.sceneName); - - if (demo.gui) { - Gui.update(demo.gui); - } - - Demo.setUpdateSourceLink(demo.sceneName); - }); - }; - - Demo.setUpdateSourceLink = function(sceneName) { - var demoViewSource = document.getElementById('demo-view-source'), - sourceUrl = 'https://github.com/liabru/matter-js/blob/master/examples'; - demoViewSource.setAttribute('href', sourceUrl + '/' + sceneName + '.js'); - }; - - Demo.setManualControl = function(demo, isManual) { - var engine = demo.engine, - world = engine.world, - runner = demo.runner; - - demo.isManual = isManual; - - if (demo.isManual) { - Runner.stop(runner); - - // continue rendering but not updating - (function render(time){ - runner.frameRequestId = window.requestAnimationFrame(render); - Events.trigger(engine, 'beforeUpdate'); - Events.trigger(engine, 'tick'); - engine.render.controller.world(engine); - Events.trigger(engine, 'afterUpdate'); - })(); - } else { - Runner.stop(runner); - Runner.start(runner, engine); - } - }; - - Demo.fullscreen = function(demo) { - var _fullscreenElement = demo.render.canvas; - - if (!document.fullscreenElement && !document.mozFullScreenElement && !document.webkitFullscreenElement) { - if (_fullscreenElement.requestFullscreen) { - _fullscreenElement.requestFullscreen(); - } else if (_fullscreenElement.mozRequestFullScreen) { - _fullscreenElement.mozRequestFullScreen(); - } else if (_fullscreenElement.webkitRequestFullscreen) { - _fullscreenElement.webkitRequestFullscreen(Element.ALLOW_KEYBOARD_INPUT); - } - } - }; - - Demo.reset = function(demo) { - var world = demo.engine.world, - i; - - World.clear(world); - Engine.clear(demo.engine); - - // clear scene graph (if defined in controller) - if (demo.render) { - var renderController = demo.render.controller; - if (renderController && renderController.clear) - renderController.clear(demo.render); - } - - // clear all scene events - if (demo.engine.events) { - for (i = 0; i < demo.sceneEvents.length; i++) - Events.off(demo.engine, demo.sceneEvents[i]); - } - - if (demo.mouseConstraint && demo.mouseConstraint.events) { - for (i = 0; i < demo.sceneEvents.length; i++) - Events.off(demo.mouseConstraint, demo.sceneEvents[i]); - } - - if (world.events) { - for (i = 0; i < demo.sceneEvents.length; i++) - Events.off(world, demo.sceneEvents[i]); - } - - if (demo.runner && demo.runner.events) { - for (i = 0; i < demo.sceneEvents.length; i++) - Events.off(demo.runner, demo.sceneEvents[i]); - } - - if (demo.render && demo.render.events) { - for (i = 0; i < demo.sceneEvents.length; i++) - Events.off(demo.render, demo.sceneEvents[i]); - } - - demo.sceneEvents = []; - - // reset id pool - Body._nextCollidingGroupId = 1; - Body._nextNonCollidingGroupId = -1; - Body._nextCategory = 0x0001; - Common._nextId = 0; - - // reset random seed - Common._seed = 0; - - // reset mouse offset and scale (only required for Demo.views) - if (demo.mouseConstraint) { - Mouse.setScale(demo.mouseConstraint.mouse, { x: 1, y: 1 }); - Mouse.setOffset(demo.mouseConstraint.mouse, { x: 0, y: 0 }); - } - - demo.engine.enableSleeping = false; - demo.engine.world.gravity.y = 1; - demo.engine.world.gravity.x = 0; - demo.engine.timing.timeScale = 1; - - var offset = 5; - World.add(world, [ - Bodies.rectangle(400, -offset, 800.5 + 2 * offset, 50.5, { isStatic: true }), - Bodies.rectangle(400, 600 + offset, 800.5 + 2 * offset, 50.5, { isStatic: true }), - Bodies.rectangle(800 + offset, 300, 50.5, 600.5 + 2 * offset, { isStatic: true }), - Bodies.rectangle(-offset, 300, 50.5, 600.5 + 2 * offset, { isStatic: true }) - ]); - - if (demo.mouseConstraint) { - World.add(world, demo.mouseConstraint); - } - - if (demo.render) { - var renderOptions = demo.render.options; - renderOptions.wireframes = true; - renderOptions.hasBounds = false; - renderOptions.showDebug = false; - renderOptions.showBroadphase = false; - renderOptions.showBounds = false; - renderOptions.showVelocity = false; - renderOptions.showCollisions = false; - renderOptions.showAxes = false; - renderOptions.showPositions = false; - renderOptions.showAngleIndicator = true; - renderOptions.showIds = false; - renderOptions.showShadows = false; - renderOptions.showVertexNumbers = false; - renderOptions.showConvexHulls = false; - renderOptions.showInternalEdges = false; - renderOptions.showSeparations = false; - renderOptions.background = '#fff'; - - if (_isMobile) { - renderOptions.showDebug = true; - } - } - }; - -})(); +/** +* The Matter.js demo page controller and example runner. +* +* NOTE: For the actual example code, refer to the source files in `/examples/`. +* +* @class Demo +*/ + +(function() { + + var _isBrowser = typeof window !== 'undefined' && window.location, + _useInspector = _isBrowser && window.location.hash.indexOf('-inspect') !== -1, + _isMobile = _isBrowser && /(ipad|iphone|ipod|android)/gi.test(navigator.userAgent), + _isAutomatedTest = !_isBrowser || window._phantom; + + var Matter = _isBrowser ? window.Matter : require('../../build/matter-dev.js'); + + var Demo = {}; + Matter.Demo = Demo; + + if (!_isBrowser) { + module.exports = Demo; + window = {}; + } + + // Matter aliases + var Body = Matter.Body, + Example = Matter.Example, + Engine = Matter.Engine, + World = Matter.World, + Common = Matter.Common, + Bodies = Matter.Bodies, + Events = Matter.Events, + Mouse = Matter.Mouse, + MouseConstraint = Matter.MouseConstraint, + Runner = Matter.Runner, + Render = Matter.Render; + + // MatterTools aliases + if (window.MatterTools) { + var Gui = MatterTools.Gui, + Inspector = MatterTools.Inspector; + } + + Demo.create = function(options) { + var defaults = { + isManual: false, + sceneName: 'mixed', + sceneEvents: [] + }; + + return Common.extend(defaults, options); + }; + + Demo.init = function() { + var demo = Demo.create(); + Matter.Demo._demo = demo; + + Matter.use( + 'matter-plugin-fake', + 'matter-plugin-2', + window.MatterPlugin + ); + + // get container element for the canvas + demo.container = document.getElementById('canvas-container'); + + // create an example engine (see /examples/engine.js) + demo.engine = Example.engine(demo); + + // run the engine + demo.runner = Engine.run(demo.engine); + + // create a debug renderer + demo.render = Render.create({ + element: demo.container, + engine: demo.engine + }); + + // run the renderer + Render.run(demo.render); + + // add a mouse controlled constraint + demo.mouseConstraint = MouseConstraint.create(demo.engine, { + element: demo.render.canvas + }); + + World.add(demo.engine.world, demo.mouseConstraint); + + // pass mouse to renderer to enable showMousePosition + demo.render.mouse = demo.mouseConstraint.mouse; + + // get the scene function name from hash + if (window.location.hash.length !== 0) + demo.sceneName = window.location.hash.replace('#', '').replace('-inspect', ''); + + // set up a scene with bodies + Demo.reset(demo); + Demo.setScene(demo, demo.sceneName); + + // set up demo interface (see end of this file) + Demo.initControls(demo); + + // pass through runner as timing for debug rendering + demo.engine.metrics.timing = demo.runner; + + return demo; + }; + + // call init when the page has loaded fully + if (!_isAutomatedTest) { + if (window.addEventListener) { + window.addEventListener('load', Demo.init); + } else if (window.attachEvent) { + window.attachEvent('load', Demo.init); + } + } + + Demo.setScene = function(demo, sceneName) { + Example[sceneName](demo); + }; + + // the functions for the demo interface and controls below + Demo.initControls = function(demo) { + var demoSelect = document.getElementById('demo-select'), + demoReset = document.getElementById('demo-reset'); + + // create a Matter.Gui + if (!_isMobile && Gui) { + demo.gui = Gui.create(demo.engine, demo.runner, demo.render); + + // need to add mouse constraint back in after gui clear or load is pressed + Events.on(demo.gui, 'clear load', function() { + demo.mouseConstraint = MouseConstraint.create(demo.engine, { + element: demo.render.canvas + }); + + World.add(demo.engine.world, demo.mouseConstraint); + }); + } + + // create a Matter.Inspector + if (!_isMobile && Inspector && _useInspector) { + demo.inspector = Inspector.create(demo.engine, demo.runner, demo.render); + + Events.on(demo.inspector, 'import', function() { + demo.mouseConstraint = MouseConstraint.create(demo.engine); + World.add(demo.engine.world, demo.mouseConstraint); + }); + + Events.on(demo.inspector, 'play', function() { + demo.mouseConstraint = MouseConstraint.create(demo.engine); + World.add(demo.engine.world, demo.mouseConstraint); + }); + + Events.on(demo.inspector, 'selectStart', function() { + demo.mouseConstraint.constraint.render.visible = false; + }); + + Events.on(demo.inspector, 'selectEnd', function() { + demo.mouseConstraint.constraint.render.visible = true; + }); + } + + // go fullscreen when using a mobile device + if (_isMobile) { + var body = document.body; + + body.className += ' is-mobile'; + demo.render.canvas.addEventListener('touchstart', Demo.fullscreen); + + var fullscreenChange = function() { + var fullscreenEnabled = document.fullscreenEnabled || document.mozFullScreenEnabled || document.webkitFullscreenEnabled; + + // delay fullscreen styles until fullscreen has finished changing + setTimeout(function() { + if (fullscreenEnabled) { + body.className += ' is-fullscreen'; + } else { + body.className = body.className.replace('is-fullscreen', ''); + } + }, 2000); + }; + + document.addEventListener('webkitfullscreenchange', fullscreenChange); + document.addEventListener('mozfullscreenchange', fullscreenChange); + document.addEventListener('fullscreenchange', fullscreenChange); + } + + // keyboard controls + document.onkeypress = function(keys) { + // shift + a = toggle manual + if (keys.shiftKey && keys.keyCode === 65) { + Demo.setManualControl(demo, !demo.isManual); + } + + // shift + q = step + if (keys.shiftKey && keys.keyCode === 81) { + if (!demo.isManual) { + Demo.setManualControl(demo, true); + } + + Runner.tick(demo.runner, demo.engine); + } + }; + + // initialise demo selector + demoSelect.value = demo.sceneName; + Demo.setUpdateSourceLink(demo.sceneName); + + demoSelect.addEventListener('change', function(e) { + Demo.reset(demo); + Demo.setScene(demo,demo.sceneName = e.target.value); + + if (demo.gui) { + Gui.update(demo.gui); + } + + var scrollY = window.scrollY; + window.location.hash = demo.sceneName; + window.scrollY = scrollY; + Demo.setUpdateSourceLink(demo.sceneName); + }); + + demoReset.addEventListener('click', function(e) { + Demo.reset(demo); + Demo.setScene(demo, demo.sceneName); + + if (demo.gui) { + Gui.update(demo.gui); + } + + Demo.setUpdateSourceLink(demo.sceneName); + }); + }; + + Demo.setUpdateSourceLink = function(sceneName) { + var demoViewSource = document.getElementById('demo-view-source'), + sourceUrl = 'https://github.com/liabru/matter-js/blob/master/examples'; + demoViewSource.setAttribute('href', sourceUrl + '/' + sceneName + '.js'); + }; + + Demo.setManualControl = function(demo, isManual) { + var engine = demo.engine, + world = engine.world, + runner = demo.runner; + + demo.isManual = isManual; + + if (demo.isManual) { + Runner.stop(runner); + + // continue rendering but not updating + (function render(time){ + runner.frameRequestId = window.requestAnimationFrame(render); + Events.trigger(engine, 'beforeUpdate'); + Events.trigger(engine, 'tick'); + engine.render.controller.world(engine); + Events.trigger(engine, 'afterUpdate'); + })(); + } else { + Runner.stop(runner); + Runner.start(runner, engine); + } + }; + + Demo.fullscreen = function(demo) { + var _fullscreenElement = demo.render.canvas; + + if (!document.fullscreenElement && !document.mozFullScreenElement && !document.webkitFullscreenElement) { + if (_fullscreenElement.requestFullscreen) { + _fullscreenElement.requestFullscreen(); + } else if (_fullscreenElement.mozRequestFullScreen) { + _fullscreenElement.mozRequestFullScreen(); + } else if (_fullscreenElement.webkitRequestFullscreen) { + _fullscreenElement.webkitRequestFullscreen(Element.ALLOW_KEYBOARD_INPUT); + } + } + }; + + Demo.reset = function(demo) { + var world = demo.engine.world, + i; + + World.clear(world); + Engine.clear(demo.engine); + + // clear scene graph (if defined in controller) + if (demo.render) { + var renderController = demo.render.controller; + if (renderController && renderController.clear) + renderController.clear(demo.render); + } + + // clear all scene events + if (demo.engine.events) { + for (i = 0; i < demo.sceneEvents.length; i++) + Events.off(demo.engine, demo.sceneEvents[i]); + } + + if (demo.mouseConstraint && demo.mouseConstraint.events) { + for (i = 0; i < demo.sceneEvents.length; i++) + Events.off(demo.mouseConstraint, demo.sceneEvents[i]); + } + + if (world.events) { + for (i = 0; i < demo.sceneEvents.length; i++) + Events.off(world, demo.sceneEvents[i]); + } + + if (demo.runner && demo.runner.events) { + for (i = 0; i < demo.sceneEvents.length; i++) + Events.off(demo.runner, demo.sceneEvents[i]); + } + + if (demo.render && demo.render.events) { + for (i = 0; i < demo.sceneEvents.length; i++) + Events.off(demo.render, demo.sceneEvents[i]); + } + + demo.sceneEvents = []; + + // reset id pool + Body._nextCollidingGroupId = 1; + Body._nextNonCollidingGroupId = -1; + Body._nextCategory = 0x0001; + Common._nextId = 0; + + // reset random seed + Common._seed = 0; + + // reset mouse offset and scale (only required for Demo.views) + if (demo.mouseConstraint) { + Mouse.setScale(demo.mouseConstraint.mouse, { x: 1, y: 1 }); + Mouse.setOffset(demo.mouseConstraint.mouse, { x: 0, y: 0 }); + } + + demo.engine.enableSleeping = false; + demo.engine.world.gravity.y = 1; + demo.engine.world.gravity.x = 0; + demo.engine.timing.timeScale = 1; + + var offset = 5; + World.add(world, [ + Bodies.rectangle(400, -offset, 800.5 + 2 * offset, 50.5, { isStatic: true }), + Bodies.rectangle(400, 600 + offset, 800.5 + 2 * offset, 50.5, { isStatic: true }), + Bodies.rectangle(800 + offset, 300, 50.5, 600.5 + 2 * offset, { isStatic: true }), + Bodies.rectangle(-offset, 300, 50.5, 600.5 + 2 * offset, { isStatic: true }) + ]); + + if (demo.mouseConstraint) { + World.add(world, demo.mouseConstraint); + } + + if (demo.render) { + var renderOptions = demo.render.options; + renderOptions.wireframes = true; + renderOptions.hasBounds = false; + renderOptions.showDebug = false; + renderOptions.showBroadphase = false; + renderOptions.showBounds = false; + renderOptions.showVelocity = false; + renderOptions.showCollisions = false; + renderOptions.showAxes = false; + renderOptions.showPositions = false; + renderOptions.showAngleIndicator = true; + renderOptions.showIds = false; + renderOptions.showShadows = false; + renderOptions.showVertexNumbers = false; + renderOptions.showConvexHulls = false; + renderOptions.showInternalEdges = false; + renderOptions.showSeparations = false; + renderOptions.background = '#fff'; + + if (_isMobile) { + renderOptions.showDebug = true; + } + } + }; + +})(); \ No newline at end of file diff --git a/examples/plugin.js b/examples/plugin.js new file mode 100644 index 0000000..183ab1a --- /dev/null +++ b/examples/plugin.js @@ -0,0 +1,54 @@ +(function() { + + var chain = Matter.Common.chain, + last = Matter.Common.last; + + var MatterPlugin = { + name: 'matter-plugin', + + version: '0.2.0', + + for: 'matter-js@^0.10.0', + + uses: [ + { + plugin: 'matter-plugin-2@^0.0.1', + options: { + message: 'hello' + } + }, + 'matter-plugin-3@^0.10.0' + ], + + options: { + thing: 1 + }, + + install: function(base) { + base.Engine.create = chain( + Matter.Engine.create, + MatterPlugin.engineCreate + ); + + base.Body.create = chain( + MatterPlugin.bodyCreate, + Matter.Body.create + ); + }, + + engineCreate: function(element, options, engine) { + engine = last(arguments); + + console.log('2nd patched engine create!', engine); + }, + + bodyCreate: function(options) { + console.log('patched body create!', arguments); + } + }; + + Matter.Plugin.exports(MatterPlugin); + + window.MatterPlugin = MatterPlugin; + +})(); diff --git a/examples/plugin2.js b/examples/plugin2.js new file mode 100644 index 0000000..1d9c588 --- /dev/null +++ b/examples/plugin2.js @@ -0,0 +1,34 @@ +(function() { + + var chain = Matter.Common.chain; + + var MatterPlugin2 = { + name: 'matter-plugin-2', + + version: '0.1.0', + + for: 'matter-js@^0.10.0', + + uses: ['matter-plugin'], + + options: { + thing: 1 + }, + + install: function(matter) { + matter.Engine.create = chain( + matter.Engine.create, + MatterPlugin2.engineCreate + ); + }, + + engineCreate: function(element, options, engine) { + console.log('patched engine create!', arguments); + } + }; + + Matter.Plugin.exports(MatterPlugin2); + + window.MatterPlugin2 = MatterPlugin2; + +})(); diff --git a/src/core/Common.js b/src/core/Common.js index 9c43d24..a551d11 100644 --- a/src/core/Common.js +++ b/src/core/Common.js @@ -182,6 +182,26 @@ module.exports = Common; Common.isArray = function(obj) { return Object.prototype.toString.call(obj) === '[object Array]'; }; + + /** + * Returns true if the object is a function. + * @method isFunction + * @param {object} obj + * @return {boolean} True if the object is a function, otherwise false + */ + Common.isFunction = function(obj) { + return typeof obj === "function"; + }; + + /** + * Returns true if the object is a plain object. + * @method isPlainObject + * @param {object} obj + * @return {boolean} True if the object is a plain object, otherwise false + */ + Common.isPlainObject = function(obj) { + return typeof obj === 'object' && obj.constructor === Object; + }; /** * Returns the given value clamped between a minimum and maximum value. @@ -231,7 +251,6 @@ module.exports = Common; return performance.now(); }; - /** * Returns a random value between a minimum and a maximum value inclusive. @@ -247,6 +266,12 @@ module.exports = Common; return min + _seededRandom() * (max - min); }; + var _seededRandom = function() { + // https://gist.github.com/ngryman/3830489 + Common._seed = (Common._seed * 9301 + 49297) % 233280; + return Common._seed / 233280; + }; + /** * Converts a CSS hex colour string into an integer. * @method colorToNumber @@ -301,6 +326,7 @@ module.exports = Common; * @method indexOf * @param {array} haystack * @param {object} needle + * @return {number} The position of needle in haystack, otherwise -1. */ Common.indexOf = function(haystack, needle) { if (haystack.indexOf) @@ -314,10 +340,109 @@ module.exports = Common; return -1; }; - var _seededRandom = function() { - // https://gist.github.com/ngryman/3830489 - Common._seed = (Common._seed * 9301 + 49297) % 233280; - return Common._seed / 233280; + /** + * A cross browser compatible array map implementation. + * @method map + * @param {array} list + * @param {function} func + * @return {array} Values from list transformed by func. + */ + Common.map = function(list, func) { + if (list.map) { + return list.map(func); + } + + var mapped = []; + + for (var i = 0; i < list.length; i += 1) { + mapped.push(func(list[i])); + } + + return mapped; + }; + + /** + * Returns the last value in an array. + * @method last + * @param {array} list + * @return {} The last value in list. + */ + Common.last = function(list) { + return list[list.length - 1]; + }; + + /** + * Takes a directed graph and returns the partially ordered set of vertices in topological order. + * Circular dependencies are allowed. + * @method topologicalSort + * @param {object} graph + * @return {array} Partially ordered set of vertices in topological order. + */ + Common.topologicalSort = function(graph) { + // https://mgechev.github.io/javascript-algorithms/graphs_others_topological-sort.js.html + var result = [], + visited = [], + temp = []; + + for (var node in graph) { + if (!visited[node] && !temp[node]) { + _topologicalSort(node, visited, temp, graph, result); + } + } + + return result; + }; + + var _topologicalSort = function(node, visited, temp, graph, result) { + var neighbors = graph[node] || []; + temp[node] = true; + + for (var i = 0; i < neighbors.length; i += 1) { + var neighbor = neighbors[i]; + + if (temp[neighbor]) { + // skip circular dependencies + continue; + } + + if (!visited[neighbor]) { + _topologicalSort(neighbor, visited, temp, graph, result); + } + } + + temp[node] = false; + visited[node] = true; + + result.push(node); + }; + + /** + * Takes _n_ functions as arguments and returns a new function that calls them in order. + * The arguments and `this` value applied when calling the new function will be applied to every function passed. + * An additional final argument is passed to provide access to the last value returned (that was not `undefined`). + * Therefore if a passed function does not return a value, the previously returned value is passed as the final argument. + * After all passed functions have been called the new function returns the final value (if any). + * @method chain + * @param ...funcs {function} The functions to chain. + * @return {function} A new function that calls the passed functions in order. + */ + Common.chain = function() { + var funcs = Array.prototype.slice.call(arguments); + + return function() { + var args = Array.prototype.slice.call(arguments), + lastResult; + + for (var i = 0; i < funcs.length; i += 1) { + var result = funcs[i].apply(this, lastResult ? args.concat(lastResult) : args); + + if (typeof result !== 'undefined') { + lastResult = result; + } + } + + return lastResult; + }; }; })(); diff --git a/src/core/Matter.js b/src/core/Matter.js new file mode 100644 index 0000000..9fe5781 --- /dev/null +++ b/src/core/Matter.js @@ -0,0 +1,60 @@ +/** +* The `Matter` module is the top level namespace and includes functions for extending other modules. +* +* @class Matter +*/ + +var Matter = {}; + +module.exports = Matter; + +var Plugin = require('./Plugin'); + +(function() { + + /** + * The library name. + * @property Matter.name + * @type {String} + */ + Matter.name = 'matter-js'; + + /** + * The library version. + * @property Matter.version + * @type {String} + */ + Matter.version = 'master'; + + /** + * The plugins that have been _installed_ through `Matter.Plugin.install`. Read only. + * @property Matter.used + * @readOnly + * @type {Array} + */ + Matter.used = []; + + /** + * A list of plugin dependencies to be installed. These are normally set and installed through `Matter.use`. + * Alternatively set them and install manually through `Plugin.installDependencies`. + * @property Matter.used + * @readOnly + * @type {Array} + */ + Matter.uses = []; + + /** + * Installs plugins on the `Matter` namespace. + * Populates `Matter.used` with an array of the plugins in the order they were applied after dependencies were resolved. + * See `Common.use` in `Matter.Common` for more information. + * TODO: add link to wiki + * @method use + * @param ...plugins {Function} The plugins to install on `base`. + */ + Matter.use = function() { + Matter.uses = Array.prototype.slice.call(arguments); + + Plugin.installDependencies(Matter); + }; + +})(); diff --git a/src/core/Plugin.js b/src/core/Plugin.js new file mode 100644 index 0000000..b98fc60 --- /dev/null +++ b/src/core/Plugin.js @@ -0,0 +1,280 @@ +/** +* The `Matter.Plugin` module contains utility functions that are Plugin to all modules. +* +* @class Plugin +*/ + +var Plugin = {}; + +module.exports = Plugin; + +var Common = require('./Common'); + +(function() { + + //Plugin._anonymousName = 0; + Plugin._registry = {}; + + Plugin.exports = function(options) { + var plugin = options; + + /*plugin.uses = plugin.uses || []; + plugin.for = plugin.for || 'matter-js@*'; + plugin.name = plugin.name || Plugin.anonymousName(); + plugin.version = plugin.version || '0.0.0';*/ + //plugin.id = plugin.name + '@' + plugin.version; + + if (!Plugin.isPlugin(plugin)) { + Common.log('Plugin.exports: ' + plugin.name + ' does not implement all required fields.', 'warn'); + } + + if (plugin.name in Plugin._registry) { + var registered = Plugin._registry[plugin.name]; + + if (Plugin.versionGte(plugin.version, registered.version)) { + Plugin._registry[plugin.name] = plugin; + } + } else { + Plugin._registry[plugin.name] = plugin; + } + + return plugin; + }; + + /** + * Returns a unique identifier for anonymous plugins. + * @method anonymousName + * @return {Number} Unique identifier name + */ + /*Plugin.anonymousName = function() { + return 'anonymous-' + Plugin._nextId++; + };*/ + + Plugin.isPlugin = function(obj) { + return obj && obj.name && Common.isFunction(obj.install); + }; + + Plugin.isUsed = function(base, name) { + return base.used.indexOf(name) > -1; + //return (',' + base.used.join(',')).indexOf(',' + name + '@') > -1; + }; + + Plugin.isFor = function(plugin, base) { + var parsed = Plugin.versionParse(plugin.for); + return base.name === parsed.name && Plugin.versionSatisfies(base.version, parsed.version); + }; + + /** + * Installs plugins on an object and ensures all dependencies are loaded. + * TODO: add link to wiki + * @method installDependencies + * @param base {} The object to install the `plugins` on. + * @return {Array} An array of the plugins in the order they were applied after dependencies were resolved. + */ + Plugin.installDependencies = function(base) { + if (!base.uses || base.uses.length === 0) { + Common.log('Plugin.installDependencies: ' + base.name + ' does not specify any dependencies to install.', 'warn'); + return; + } + + if (base.used && base.used.length > 0) { + Common.log('Plugin.installDependencies: ' + base.name + ' has already installed its dependencies.', 'warn'); + return; + } + + var dependencies = Plugin.dependencies(base), + sortedDependencies = Common.topologicalSort(dependencies), + warnings = 0; + + console.log(dependencies, sortedDependencies); + + for (var i = 0; i < sortedDependencies.length; i += 1) { + var plugin = Plugin.resolve(sortedDependencies[i]); + + if (sortedDependencies[i] === base.name) { + continue; + } + + if (!plugin) { + Common.log('Plugin.installDependencies: ' + sortedDependencies[i] + ' could not be resolved.', 'warn'); + warnings += 1; + continue; + } + + if (Plugin.isUsed(base, plugin.name)) { + continue; + } + + if (!Plugin.isFor(plugin, base)) { + Common.log('Plugin.installDependencies: ' + plugin.name + '@' + plugin.version + ' is for ' + plugin.for + ' but used on ' + base.name + '@' + base.version + '.', 'warn'); + warnings += 1; + } + + var options = Common.isPlainObject(sortedDependencies[i]) ? sortedDependencies[i].options : null; + + if (plugin.install) { + plugin.install(base, options); + } + + base.used.push(plugin.name); + } + + if (warnings > 0) { + Common.log('Plugin.installDependencies: Some dependencies may not function as expected, see above warnings.', 'warn'); + } + }; + + Plugin.dependencies = function(base, _dependencies) { + base = Plugin.resolve(base) || base; + _dependencies = _dependencies || {}; + + var name = Plugin.versionParse(Plugin.dependencyName(base)).name; + + if (name in _dependencies) { + return; + } + + _dependencies[name] = Common.map(base.uses || [], function(dependency) { + return Plugin.versionParse(Plugin.dependencyName(dependency)).name; + }); + + for (var i = 0; i < _dependencies[name].length; i += 1) { + Plugin.dependencies(_dependencies[name][i], _dependencies); + } + + return _dependencies; + }; + + Plugin.dependencyName = function(dependency) { + return (dependency.plugin && (dependency.plugin.name || dependency.plugin)) || dependency.name || dependency; + }; + + Plugin.resolve = function(dependency) { + if (Plugin.isPlugin(dependency)) { + return dependency; + } + + var plugin = dependency.plugin && Plugin.resolve(dependency.plugin); + + if (plugin) { + return plugin; + } + + var parsed = Plugin.versionParse(Plugin.dependencyName(dependency)); + + plugin = Plugin._registry[parsed.name]; + + if (!plugin) { + return null; + } + + if (Plugin.versionSatisfies(plugin.version, parsed.version)) { + return plugin; + } + }; + + Plugin.versionParse = function(name) { + return { + name: name.split('@')[0], + pattern: name.split('@')[1] || '*' + }; + }; + + Plugin.semverParse = function(pattern) { + var parsed = {}; + + parsed.version = pattern; + + if (+parsed.version[0] === NaN) { + parsed.operator = parsed.version[0]; + parsed.version = parsed.version.substr(1); + } + + parsed.parts = Common.map(parsed.version.split('.'), function(part) { + return +part; + }); + + return parsed; + }; + + Plugin.versionNumber = function(version) { + var parts = Plugin.semverParse(version).parts; + return parts[0] * 1e8 + parts[1] * 1e4 + parts[2]; + }; + + Plugin.versionLt = function(versionA, versionB) { + return Plugin.versionNumber(versionA) < Plugin.versionNumber(versionB); + }; + + Plugin.versionLte = function(versionA, versionB) { + return Plugin.versionNumber(versionA) <= Plugin.versionNumber(versionB); + }; + + Plugin.versionGt = function(versionA, versionB) { + return Plugin.versionNumber(versionA) > Plugin.versionNumber(versionB); + }; + + Plugin.versionGte = function(versionA, versionB) { + return Plugin.versionNumber(versionA) >= Plugin.versionNumber(versionB); + }; + + Plugin.versionSatisfies = function(version, pattern) { + // https://docs.npmjs.com/misc/semver#advanced-range-syntax + var operator; + pattern = pattern || '*'; + + if (isNaN(+pattern[0])) { + operator = pattern[0]; + pattern = pattern.substr(1); + } + + var parts = Common.map(version.split('.'), function(part) { + return +part; + }); + + var patternParts = Common.map(pattern.split('.'), function(part) { + return +part; + }); + + /*var parsed = Plugin.semverParse(pattern); + + if (parsed.operator === '*') { + return true; + } + + if (parsed.operator === '~') { + return Plugin.versionGte(version, parsed.pattern) + + Plugin.versionLte(version, parts[0] + '.' + (+parts[1] + 1) + '.0'); + } + + if (parsed.operator === '^') { + return Plugin.versionGte(version, pattern) + + Plugin.versionLte(version, parts[0] + '.' + (+parts[1] + 1) + '.0'); + }*/ + + if (operator === '*') { + return true; + } + + if (operator === '~') { + return parts[0] === patternParts[0] && parts[1] === patternParts[1] && parts[2] >= patternParts[2]; + } + + if (operator === '^') { + if (patternParts[0] > 0) { + return parts[0] === patternParts[0] && Plugin.versionGte(version, pattern); + } + + if (patternParts[1] > 0) { + return parts[2] >= patternParts[2]; + } + + //return parts[0] === patternParts[0] && (parts[1] >= patternParts[1] || parts[2] >= patternParts[2]); + + //return '^' + parts[0] === patternParts[0] && +parts[1] >= +patternParts[1] || +parts[2] >= +patternParts[2]; + } + + return version === pattern; + }; + +})(); diff --git a/src/module/main.js b/src/module/main.js index 32b2ede..cb7eb7b 100644 --- a/src/module/main.js +++ b/src/module/main.js @@ -1,51 +1,51 @@ -var Matter = module.exports = {}; -Matter.version = 'master'; - -Matter.Body = require('../body/Body'); -Matter.Composite = require('../body/Composite'); -Matter.World = require('../body/World'); - -Matter.Contact = require('../collision/Contact'); -Matter.Detector = require('../collision/Detector'); -Matter.Grid = require('../collision/Grid'); -Matter.Pairs = require('../collision/Pairs'); -Matter.Pair = require('../collision/Pair'); -Matter.Query = require('../collision/Query'); -Matter.Resolver = require('../collision/Resolver'); -Matter.SAT = require('../collision/SAT'); - -Matter.Constraint = require('../constraint/Constraint'); -Matter.MouseConstraint = require('../constraint/MouseConstraint'); - -Matter.Common = require('../core/Common'); -Matter.Engine = require('../core/Engine'); -Matter.Events = require('../core/Events'); -Matter.Mouse = require('../core/Mouse'); -Matter.Runner = require('../core/Runner'); -Matter.Sleeping = require('../core/Sleeping'); - -// @if DEBUG -Matter.Metrics = require('../core/Metrics'); -// @endif - -Matter.Bodies = require('../factory/Bodies'); -Matter.Composites = require('../factory/Composites'); - -Matter.Axes = require('../geometry/Axes'); -Matter.Bounds = require('../geometry/Bounds'); -Matter.Svg = require('../geometry/Svg'); -Matter.Vector = require('../geometry/Vector'); -Matter.Vertices = require('../geometry/Vertices'); - -Matter.Render = require('../render/Render'); -Matter.RenderPixi = require('../render/RenderPixi'); - -// aliases - -Matter.World.add = Matter.Composite.add; -Matter.World.remove = Matter.Composite.remove; -Matter.World.addComposite = Matter.Composite.addComposite; -Matter.World.addBody = Matter.Composite.addBody; -Matter.World.addConstraint = Matter.Composite.addConstraint; -Matter.World.clear = Matter.Composite.clear; -Matter.Engine.run = Matter.Runner.run; +var Matter = module.exports = require('../core/Matter'); + +Matter.Body = require('../body/Body'); +Matter.Composite = require('../body/Composite'); +Matter.World = require('../body/World'); + +Matter.Contact = require('../collision/Contact'); +Matter.Detector = require('../collision/Detector'); +Matter.Grid = require('../collision/Grid'); +Matter.Pairs = require('../collision/Pairs'); +Matter.Pair = require('../collision/Pair'); +Matter.Query = require('../collision/Query'); +Matter.Resolver = require('../collision/Resolver'); +Matter.SAT = require('../collision/SAT'); + +Matter.Constraint = require('../constraint/Constraint'); +Matter.MouseConstraint = require('../constraint/MouseConstraint'); + +Matter.Common = require('../core/Common'); +Matter.Engine = require('../core/Engine'); +Matter.Events = require('../core/Events'); +Matter.Mouse = require('../core/Mouse'); +Matter.Runner = require('../core/Runner'); +Matter.Sleeping = require('../core/Sleeping'); +Matter.Plugin = require('../core/Plugin'); + +// @if DEBUG +Matter.Metrics = require('../core/Metrics'); +// @endif + +Matter.Bodies = require('../factory/Bodies'); +Matter.Composites = require('../factory/Composites'); + +Matter.Axes = require('../geometry/Axes'); +Matter.Bounds = require('../geometry/Bounds'); +Matter.Svg = require('../geometry/Svg'); +Matter.Vector = require('../geometry/Vector'); +Matter.Vertices = require('../geometry/Vertices'); + +Matter.Render = require('../render/Render'); +Matter.RenderPixi = require('../render/RenderPixi'); + +// aliases + +Matter.World.add = Matter.Composite.add; +Matter.World.remove = Matter.Composite.remove; +Matter.World.addComposite = Matter.Composite.addComposite; +Matter.World.addBody = Matter.Composite.addBody; +Matter.World.addConstraint = Matter.Composite.addConstraint; +Matter.World.clear = Matter.Composite.clear; +Matter.Engine.run = Matter.Runner.run;