diff --git a/package.json b/package.json index 112e2cf0..527accf5 100644 --- a/package.json +++ b/package.json @@ -35,6 +35,7 @@ "src/html.js", "src/storage.js", "src/extensions.js", + "src/viewport.js", "src/device.js", "src/sprite.js", "src/canvas.js", diff --git a/src/DOM.js b/src/DOM.js index a81572d1..f073ebf6 100644 --- a/src/DOM.js +++ b/src/DOM.js @@ -389,8 +389,8 @@ Crafty.extend({ */ translate: function (x, y) { return { - x: (x - Crafty.stage.x + document.body.scrollLeft + document.documentElement.scrollLeft - Crafty.viewport._x)/Crafty.viewport._zoom, - y: (y - Crafty.stage.y + document.body.scrollTop + document.documentElement.scrollTop - Crafty.viewport._y)/Crafty.viewport._zoom + x: (x - Crafty.stage.x + document.body.scrollLeft + document.documentElement.scrollLeft - Crafty.viewport._x)/Crafty.viewport._scale, + y: (y - Crafty.stage.y + document.body.scrollTop + document.documentElement.scrollTop - Crafty.viewport._y)/Crafty.viewport._scale } } } diff --git a/src/DebugLayer.js b/src/DebugLayer.js index 88437b2a..4cfd0db1 100644 --- a/src/DebugLayer.js +++ b/src/DebugLayer.js @@ -227,7 +227,7 @@ Crafty.c("DebugPolygon", { ctx = Crafty.DebugCanvas.context; ctx.beginPath(); for (var p in this.polygon.points) { - ctx.lineTo(Crafty.viewport.x + this.map.points[p][0], Crafty.viewport.y + this.map.points[p][1]); + ctx.lineTo(this.map.points[p][0],this.map.points[p][1]); } ctx.closePath(); @@ -323,10 +323,8 @@ Crafty.DebugCanvas = { Crafty.DebugCanvas.context = c.getContext('2d'); Crafty.DebugCanvas._canvas = c; - //Set any existing transformations - var zoom = Crafty.viewport._zoom - if (zoom != 1) - Crafty.DebugCanvas.context.scale(zoom, zoom); + + } //Bind rendering of canvas context (see drawing.js) Crafty.unbind("RenderScene", Crafty.DebugCanvas.renderScene) @@ -344,8 +342,12 @@ Crafty.DebugCanvas = { ctx = Crafty.DebugCanvas.context, current; + var view = Crafty.viewport; + ctx.setTransform(view._scale, 0, 0, view._scale, view._x, view._y) + ctx.clearRect(rect._x, rect._y, rect._w, rect._h); + //sort the objects by the global Z //q.sort(zsort); for (; i < l; i++) { diff --git a/src/HashMap.js b/src/HashMap.js index 51a3bf78..6cce7b3f 100644 --- a/src/HashMap.js +++ b/src/HashMap.js @@ -97,10 +97,10 @@ obj = results[i]; if (!obj) continue; //skip if deleted id = obj[0]; //unique ID - + obj = obj._mbr || obj //check if not added to hash and that actually intersects - if (!found[id] && obj.x < rect._x + rect._w && obj._x + obj._w > rect._x && - obj.y < rect._y + rect._h && obj._h + obj._y > rect._y) + if (!found[id] && obj._x < rect._x + rect._w && obj._x + obj._w > rect._x && + obj._y < rect._y + rect._h && obj._h + obj._y > rect._y) found[id] = results[i]; } diff --git a/src/canvas.js b/src/canvas.js index 5e82220c..43481153 100644 --- a/src/canvas.js +++ b/src/canvas.js @@ -191,7 +191,7 @@ Crafty.extend({ Crafty.canvas._canvas = c; //Set any existing transformations - var zoom = Crafty.viewport._zoom + var zoom = Crafty.viewport._scale if (zoom != 1) Crafty.canvas.context.scale(zoom, zoom); diff --git a/src/core.js b/src/core.js index 55c5f36c..d954e2de 100644 --- a/src/core.js +++ b/src/core.js @@ -1037,6 +1037,8 @@ if (loops) { drawTimeStart = currentTime; Crafty.trigger("RenderScene") + // Post-render cleanup opportunity + Crafty.trigger("PostRender"); currentTime = +new Date(); Crafty.trigger("MeasureRenderTime", currentTime - drawTimeStart); diff --git a/src/drawing.js b/src/drawing.js index 8e26f4bb..51e32e05 100644 --- a/src/drawing.js +++ b/src/drawing.js @@ -337,7 +337,10 @@ Crafty.DrawManager = (function () { /** array of dirty rects on screen */ var dirty_rects = [], changed_objs = [], /** array of DOMs needed updating */ - dom = [], + dom = [], + + dirtyViewport = false, + /** recManager: an object for managing dirty rectangles. */ rectManager = { @@ -413,6 +416,9 @@ Crafty.DrawManager = (function () { }; + Crafty.bind("InvalidateViewport", function(){dirtyViewport=true}); + Crafty.bind("PostRender", function(){dirtyViewport=false}); + return { /**@ * #Crafty.DrawManager.total2D @@ -577,15 +583,21 @@ Crafty.DrawManager = (function () { renderCanvas: function() { var l = changed_objs.length; - if (!l) { return; } + if (!l && !dirtyViewport) { return; } var i = 0, l = changed_objs.length, rect, q, j, len, obj, ent, ctx = Crafty.canvas.context, DM = Crafty.DrawManager; + + if (dirtyViewport){ + var view = Crafty.viewport; + ctx.setTransform(view._scale, 0, 0, view._scale, view.x, view.y) + + } //if the amount of changed objects is over 60% of the total objects //do the naive method redrawing // TODO: I'm not sure this condition really makes that much sense! - if (l / DM.total2D > 0.6 ) { + if (l / DM.total2D > 0.6 || dirtyViewport) { DM.drawAll(); rectManager.clean() return; @@ -672,6 +684,15 @@ Crafty.DrawManager = (function () { * @see DOM.draw */ renderDOM: function() { + // Adjust the viewport + if (dirtyViewport){ + var style = Crafty.stage.inner.style, view = Crafty.viewport; + + style.transform = style[Crafty.support.prefix + "Transform"] = "scale(" + view._scale + ", " + view._scale + ")" + style.left = view.x + "px"; + style.top = view.y + "px"; + style.zIndex = 10; + } //if no objects have been changed, stop if (!dom.length) return; diff --git a/src/extensions.js b/src/extensions.js index 91c75a9a..11f42f67 100644 --- a/src/extensions.js +++ b/src/extensions.js @@ -379,670 +379,7 @@ Crafty.extend({ Crafty.stage.elem.style.background = style; }, - /**@ - * #Crafty.viewport - * @category Stage - * - * Viewport is essentially a 2D camera looking at the stage. Can be moved which - * in turn will react just like a camera moving in that direction. - */ - viewport: { - /**@ - * #Crafty.viewport.clampToEntities - * @comp Crafty.viewport - * - * Decides if the viewport functions should clamp to game entities. - * When set to `true` functions such as Crafty.viewport.mouselook() will not allow you to move the - * viewport over areas of the game that has no entities. - * For development it can be useful to set this to false. - */ - clampToEntities: true, - width: 0, - height: 0, - /**@ - * #Crafty.viewport.x - * @comp Crafty.viewport - * - * Will move the stage and therefore every visible entity along the `x` - * axis in the opposite direction. - * - * When this value is set, it will shift the entire stage. This means that entity - * positions are not exactly where they are on screen. To get the exact position, - * simply add `Crafty.viewport.x` onto the entities `x` position. - */ - _x: 0, - /**@ - * #Crafty.viewport.y - * @comp Crafty.viewport - * - * Will move the stage and therefore every visible entity along the `y` - * axis in the opposite direction. - * - * When this value is set, it will shift the entire stage. This means that entity - * positions are not exactly where they are on screen. To get the exact position, - * simply add `Crafty.viewport.y` onto the entities `y` position. - */ - _y: 0, - - /**@ - * #Crafty.viewport.bounds - * @comp Crafty.viewport - * - * A rectangle which defines the bounds of the viewport. If this - * variable is null, Crafty uses the bounding box of all the items - * on the stage. - */ - bounds:null, - - /**@ - * #Crafty.viewport.scroll - * @comp Crafty.viewport - * @sign Crafty.viewport.scroll(String axis, Number v) - * @param axis - 'x' or 'y' - * @param v - The new absolute position on the axis - * - * Will move the viewport to the position given on the specified axis - * - * @example - * Will move the camera 500 pixels right of its initial position, in effect - * shifting everything in the viewport 500 pixels to the left. - * - * ~~~ - * Crafty.viewport.scroll('_x', 500); - * ~~~ - */ - scroll: function (axis, v) { - v = Math.floor(v); - var change = v - this[axis], //change in direction - context = Crafty.canvas.context, - style = Crafty.stage.inner.style, - canvas; - - //update viewport and DOM scroll - this[axis] = v; - if (context) { - if (axis == '_x') { - context.translate(change, 0); - } else { - context.translate(0, change); - } - Crafty.DrawManager.drawAll(); - } - style[axis == '_x' ? "left" : "top"] = v + "px"; - }, - - rect: function () { - return { _x: -this._x, _y: -this._y, _w: this.width, _h: this.height }; - }, - - /**@ - * #Crafty.viewport.pan - * @comp Crafty.viewport - * @sign public void Crafty.viewport.pan(String axis, Number v, Number time) - * @param String axis - 'x' or 'y'. The axis to move the camera on - * @param Number v - the distance to move the camera by - * @param Number time - The duration in frames for the entire camera movement - * - * Pans the camera a given number of pixels over a given number of frames - */ - pan: (function () { - var tweens = {}, i, bound = false; - - function enterFrame(e) { - var l = 0; - for (i in tweens) { - var prop = tweens[i]; - if (prop.remTime > 0) { - prop.current += prop.diff; - prop.remTime--; - Crafty.viewport[i] = Math.floor(prop.current); - l++; - } - else { - delete tweens[i]; - } - } - if (l) Crafty.viewport._clamp(); - } - - return function (axis, v, time) { - Crafty.viewport.follow(); - if (axis == 'reset') { - for (i in tweens) { - tweens[i].remTime = 0; - } - return; - } - if (time == 0) time = 1; - tweens[axis] = { - diff: -v / time, - current: Crafty.viewport[axis], - remTime: time - }; - if (!bound) { - Crafty.bind("EnterFrame", enterFrame); - bound = true; - } - } - })(), - - /**@ - * #Crafty.viewport.follow - * @comp Crafty.viewport - * @sign public void Crafty.viewport.follow(Object target, Number offsetx, Number offsety) - * @param Object target - An entity with the 2D component - * @param Number offsetx - Follow target should be offsetx pixels away from center - * @param Number offsety - Positive puts target to the right of center - * - * Follows a given entity with the 2D component. If following target will take a portion of - * the viewport out of bounds of the world, following will stop until the target moves away. - * - * @example - * ~~~ - * var ent = Crafty.e('2D, DOM').attr({w: 100, h: 100:}); - * Crafty.viewport.follow(ent, 0, 0); - * ~~~ - */ - follow: (function () { - var oldTarget, offx, offy; - - function change() { - Crafty.viewport.scroll('_x', -(this.x + (this.w / 2) - (Crafty.viewport.width / 2) - offx)); - Crafty.viewport.scroll('_y', -(this.y + (this.h / 2) - (Crafty.viewport.height / 2) - offy)); - Crafty.viewport._clamp(); - } - - return function (target, offsetx, offsety) { - if (oldTarget) - oldTarget.unbind('Change', change); - if (!target || !target.has('2D')) - return; - Crafty.viewport.pan('reset'); - - oldTarget = target; - offx = (typeof offsetx != 'undefined') ? offsetx : 0; - offy = (typeof offsety != 'undefined') ? offsety : 0; - - target.bind('Change', change); - change.call(target); - } - })(), - - /**@ - * #Crafty.viewport.centerOn - * @comp Crafty.viewport - * @sign public void Crafty.viewport.centerOn(Object target, Number time) - * @param Object target - An entity with the 2D component - * @param Number time - The number of frames to perform the centering over - * - * Centers the viewport on the given entity - */ - centerOn: function (targ, time) { - var x = targ.x + Crafty.viewport.x, - y = targ.y + Crafty.viewport.y, - mid_x = targ.w / 2, - mid_y = targ.h / 2, - cent_x = Crafty.viewport.width / 2, - cent_y = Crafty.viewport.height / 2, - new_x = x + mid_x - cent_x, - new_y = y + mid_y - cent_y; - - Crafty.viewport.pan('reset'); - Crafty.viewport.pan('x', new_x, time); - Crafty.viewport.pan('y', new_y, time); - }, - /**@ - * #Crafty.viewport._zoom - * @comp Crafty.viewport - * - * This value keeps an amount of viewport zoom, required for calculating mouse position at entity - */ - _zoom : 1, - - /**@ - * #Crafty.viewport.zoom - * @comp Crafty.viewport - * @sign public void Crafty.viewport.zoom(Number amt, Number cent_x, Number cent_y, Number time) - * @param Number amt - amount to zoom in on the target by (eg. 2, 4, 0.5) - * @param Number cent_x - the center to zoom on - * @param Number cent_y - the center to zoom on - * @param Number time - the duration in frames of the entire zoom operation - * - * Zooms the camera in on a given point. amt > 1 will bring the camera closer to the subject - * amt < 1 will bring it farther away. amt = 0 will do nothing. - * Zooming is multiplicative. To reset the zoom amount, pass 0. - */ - zoom: (function () { - var zoom = 1, - zoom_tick = 0, - dur = 0, - prop = Crafty.support.prefix + "Transform", - bound = false, - act = {}, - prct = {}; - // what's going on: - // 1. Get the original point as a percentage of the stage - // 2. Scale the stage - // 3. Get the new size of the stage - // 4. Get the absolute position of our point using previous percentage - // 4. Offset inner by that much - - function enterFrame() { - if (dur > 0) { - if (isFinite(Crafty.viewport._zoom)) zoom = Crafty.viewport._zoom; - var old = { - width: act.width * zoom, - height: act.height * zoom - }; - zoom += zoom_tick; - Crafty.viewport._zoom = zoom; - var new_s = { - width: act.width * zoom, - height: act.height * zoom - }, - diff = { - width: new_s.width - old.width, - height: new_s.height - old.height - }; - Crafty.stage.inner.style[prop] = 'scale(' + zoom + ',' + zoom + ')'; - if (Crafty.canvas._canvas) { - var czoom = zoom / (zoom - zoom_tick); - Crafty.canvas.context.scale(czoom, czoom); - Crafty.DrawManager.drawAll(); - } - Crafty.viewport.x -= diff.width * prct.width; - Crafty.viewport.y -= diff.height * prct.height; - dur--; - } - } - - return function (amt, cent_x, cent_y, time) { - var bounds = this.bounds || Crafty.map.boundaries(), - final_zoom = amt ? zoom * amt : 1; - if (!amt) { // we're resetting to defaults - zoom = 1; - this._zoom = 1; - } - - act.width = bounds.max.x - bounds.min.x; - act.height = bounds.max.y - bounds.min.y; - - prct.width = cent_x / act.width; - prct.height = cent_y / act.height; - - if (time == 0) time = 1; - zoom_tick = (final_zoom - zoom) / time; - dur = time; - - Crafty.viewport.pan('reset'); - if (!bound) { - Crafty.bind('EnterFrame', enterFrame); - bound = true; - } - } - })(), - /**@ - * #Crafty.viewport.scale - * @comp Crafty.viewport - * @sign public void Crafty.viewport.scale(Number amt) - * @param Number amt - amount to zoom/scale in on the element on the viewport by (eg. 2, 4, 0.5) - * - * Zooms/scale the camera. amt > 1 increase all entities on stage - * amt < 1 will reduce all entities on stage. amt = 0 will reset the zoom/scale. - * Zooming/scaling is multiplicative. To reset the zoom/scale amount, pass 0. - * - * @example - * ~~~ - * Crafty.viewport.scale(2); //to see effect add some entities on stage. - * ~~~ - */ - scale: (function () { - var prop = Crafty.support.prefix + "Transform", - act = {}; - return function (amt) { - var bounds = this.bounds || Crafty.map.boundaries(), - final_zoom = amt ? this._zoom * amt : 1, - czoom = final_zoom / this._zoom; - - this._zoom = final_zoom; - act.width = bounds.max.x - bounds.min.x; - act.height = bounds.max.y - bounds.min.y; - var new_s = { - width: act.width * final_zoom, - height: act.height * final_zoom - } - Crafty.viewport.pan('reset'); - Crafty.stage.inner.style['transform'] = - Crafty.stage.inner.style[prop] = 'scale(' + this._zoom + ',' + this._zoom + ')'; - - if (Crafty.canvas._canvas) { - Crafty.canvas.context.scale(czoom, czoom); - Crafty.DrawManager.drawAll(); - } - //Crafty.viewport.width = new_s.width; - //Crafty.viewport.height = new_s.height; - } - })(), - /**@ - * #Crafty.viewport.mouselook - * @comp Crafty.viewport - * @sign public void Crafty.viewport.mouselook(Boolean active) - * @param Boolean active - Activate or deactivate mouselook - * - * Toggle mouselook on the current viewport. - * Simply call this function and the user will be able to - * drag the viewport around. - */ - mouselook: (function () { - var active = false, - dragging = false, - lastMouse = {} - old = {}; - - - return function (op, arg) { - if (typeof op == 'boolean') { - active = op; - if (active) { - Crafty.mouseObjs++; - } - else { - Crafty.mouseObjs = Math.max(0, Crafty.mouseObjs - 1); - } - return; - } - if (!active) return; - switch (op) { - case 'move': - case 'drag': - if (!dragging) return; - diff = { - x: arg.clientX - lastMouse.x, - y: arg.clientY - lastMouse.y - }; - - Crafty.viewport.x += diff.x; - Crafty.viewport.y += diff.y; - Crafty.viewport._clamp(); - case 'start': - lastMouse.x = arg.clientX; - lastMouse.y = arg.clientY; - dragging = true; - break; - case 'stop': - dragging = false; - break; - } - }; - })(), - _clamp: function () { - // clamps the viewport to the viewable area - // under no circumstances should the viewport see something outside the boundary of the 'world' - if (!this.clampToEntities) return; - var bound = this.bounds || Crafty.map.boundaries(); - bound.max.x *= this._zoom; - bound.min.x *= this._zoom; - bound.max.y *= this._zoom; - bound.min.y *= this._zoom; - if (bound.max.x - bound.min.x > Crafty.viewport.width) { - bound.max.x -= Crafty.viewport.width; - - if (Crafty.viewport.x < -bound.max.x) { - Crafty.viewport.x = -bound.max.x; - } - else if (Crafty.viewport.x > -bound.min.x) { - Crafty.viewport.x = -bound.min.x; - } - } - else { - Crafty.viewport.x = -1 * (bound.min.x + (bound.max.x - bound.min.x) / 2 - Crafty.viewport.width / 2); - } - if (bound.max.y - bound.min.y > Crafty.viewport.height) { - bound.max.y -= Crafty.viewport.height; - - if (Crafty.viewport.y < -bound.max.y) { - Crafty.viewport.y = -bound.max.y; - } - else if (Crafty.viewport.y > -bound.min.y) { - Crafty.viewport.y = -bound.min.y; - } - } - else { - Crafty.viewport.y = -1 * (bound.min.y + (bound.max.y - bound.min.y) / 2 - Crafty.viewport.height / 2); - } - }, - - /**@ - * #Crafty.viewport.init - * @comp Crafty.viewport - * @sign public void Crafty.viewport.init([Number width, Number height, String stage_elem]) - * @sign public void Crafty.viewport.init([Number width, Number height, HTMLElement stage_elem]) - * @param Number width - Width of the viewport - * @param Number height - Height of the viewport - * @param String or HTMLElement stage_elem - the element to use as the stage (either its id or the actual element). - * - * Initialize the viewport. If the arguments 'width' or 'height' are missing, or Crafty.mobile is true, use Crafty.DOM.window.width and Crafty.DOM.window.height (full screen model). - * - * The argument 'stage_elem' is used to specify a stage element other than the default, and can be either a string or an HTMLElement. If a string is provided, it will look for an element with that id and, if none exists, create a div. If an HTMLElement is provided, that is used directly. Omitting this argument is the same as passing an id of 'cr-stage'. - * - * @see Crafty.device, Crafty.DOM, Crafty.stage - */ - init: function (w, h, stage_elem) { - Crafty.DOM.window.init(); - - //fullscreen if mobile or not specified - this.width = (!w || Crafty.mobile) ? Crafty.DOM.window.width : w; - this.height = (!h || Crafty.mobile) ? Crafty.DOM.window.height : h; - - //check if stage exists - if(typeof stage_elem === 'undefined') - stage_elem = "cr-stage"; - - var crstage; - if(typeof stage_elem === 'string') - crstage = document.getElementById(stage_elem); - else if(typeof HTMLElement !== "undefined" ? stage_elem instanceof HTMLElement : stage_elem instanceof Element) - crstage = stage_elem; - else - throw new TypeError("stage_elem must be a string or an HTMLElement"); - - /**@ - * #Crafty.stage - * @category Core - * The stage where all the DOM entities will be placed. - */ - - /**@ - * #Crafty.stage.elem - * @comp Crafty.stage - * The `#cr-stage` div element. - */ - - /**@ - * #Crafty.stage.inner - * @comp Crafty.stage - * `Crafty.stage.inner` is a div inside the `#cr-stage` div that holds all DOM entities. - * If you use canvas, a `canvas` element is created at the same level in the dom - * as the the `Crafty.stage.inner` div. So the hierarchy in the DOM is - * - * `Crafty.stage.elem` - * - * - * - `Crafty.stage.inner` (a div HTMLElement) - * - * - `Crafty.canvas._canvas` (a canvas HTMLElement) - */ - - //create stage div to contain everything - Crafty.stage = { - x: 0, - y: 0, - fullscreen: false, - elem: (crstage ? crstage : document.createElement("div")), - inner: document.createElement("div") - }; - - //fullscreen, stop scrollbars - if ((!w && !h) || Crafty.mobile) { - document.body.style.overflow = "hidden"; - Crafty.stage.fullscreen = true; - } - - Crafty.addEvent(this, window, "resize", Crafty.viewport.reload); - - Crafty.addEvent(this, window, "blur", function () { - if (Crafty.settings.get("autoPause")) { - if(!Crafty._paused) Crafty.pause(); - } - }); - Crafty.addEvent(this, window, "focus", function () { - if (Crafty._paused && Crafty.settings.get("autoPause")) { - Crafty.pause(); - } - }); - - //make the stage unselectable - Crafty.settings.register("stageSelectable", function (v) { - Crafty.stage.elem.onselectstart = v ? function () { return true; } : function () { return false; }; - }); - Crafty.settings.modify("stageSelectable", false); - - //make the stage have no context menu - Crafty.settings.register("stageContextMenu", function (v) { - Crafty.stage.elem.oncontextmenu = v ? function () { return true; } : function () { return false; }; - }); - Crafty.settings.modify("stageContextMenu", false); - - Crafty.settings.register("autoPause", function (){ }); - Crafty.settings.modify("autoPause", false); - - //add to the body and give it an ID if not exists - if (!crstage) { - document.body.appendChild(Crafty.stage.elem); - Crafty.stage.elem.id = stage_elem; - } - - var elem = Crafty.stage.elem.style, - offset; - - Crafty.stage.elem.appendChild(Crafty.stage.inner); - Crafty.stage.inner.style.position = "absolute"; - Crafty.stage.inner.style.zIndex = "1"; - - //css style - elem.width = this.width + "px"; - elem.height = this.height + "px"; - elem.overflow = "hidden"; - - if (Crafty.mobile) { - elem.position = "absolute"; - elem.left = "0px"; - elem.top = "0px"; - - // remove default gray highlighting after touch - if (typeof elem.webkitTapHighlightColor != undefined) { - elem.webkitTapHighlightColor = "rgba(0,0,0,0)"; - } - - var meta = document.createElement("meta"), - head = document.getElementsByTagName("HEAD")[0]; - - //stop mobile zooming and scrolling - meta.setAttribute("name", "viewport"); - meta.setAttribute("content", "width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no"); - head.appendChild(meta); - - //hide the address bar - meta = document.createElement("meta"); - meta.setAttribute("name", "apple-mobile-web-app-capable"); - meta.setAttribute("content", "yes"); - head.appendChild(meta); - setTimeout(function () { window.scrollTo(0, 1); }, 0); - - Crafty.addEvent(this, window, "touchmove", function (e) { - e.preventDefault(); - }); - - Crafty.stage.x = 0; - Crafty.stage.y = 0; - - } else { - elem.position = "relative"; - //find out the offset position of the stage - offset = Crafty.DOM.inner(Crafty.stage.elem); - Crafty.stage.x = offset.x; - Crafty.stage.y = offset.y; - } - - if (Crafty.support.setter) { - //define getters and setters to scroll the viewport - this.__defineSetter__('x', function (v) { this.scroll('_x', v); }); - this.__defineSetter__('y', function (v) { this.scroll('_y', v); }); - this.__defineGetter__('x', function () { return this._x; }); - this.__defineGetter__('y', function () { return this._y; }); - //IE9 - } else if (Crafty.support.defineProperty) { - Object.defineProperty(this, 'x', { set: function (v) { this.scroll('_x', v); }, get: function () { return this._x; } }); - Object.defineProperty(this, 'y', { set: function (v) { this.scroll('_y', v); }, get: function () { return this._y; } }); - } else { - //create empty entity waiting for enterframe - this.x = this._x; - this.y = this._y; - Crafty.e("viewport"); - } - }, - - /**@ - * #Crafty.viewport.reload - * @comp Crafty.stage - * - * @sign public Crafty.viewport.reload() - * - * Recalculate and reload stage width, height and position. - * Useful when browser return wrong results on init (like safari on Ipad2). - * - */ - reload : function () { - Crafty.DOM.window.init(); - var w = Crafty.DOM.window.width, - h = Crafty.DOM.window.height, - offset; - - - if (Crafty.stage.fullscreen) { - this.width = w; - this.height = h; - Crafty.stage.elem.style.width = w + "px"; - Crafty.stage.elem.style.height = h + "px"; - - if (Crafty.canvas._canvas) { - Crafty.canvas._canvas.width = w; - Crafty.canvas._canvas.height = h; - Crafty.DrawManager.drawAll(); - } - } - - offset = Crafty.DOM.inner(Crafty.stage.elem); - Crafty.stage.x = offset.x; - Crafty.stage.y = offset.y; - }, - - /**@ - * #Crafty.viewport.reset - * @comp Crafty.stage - * - * @sign public Crafty.viewport.reset() - * - * Resets the viewport to starting values - * Called when scene() is run. - */ - reset: function () { - Crafty.viewport.pan('reset'); - Crafty.viewport.follow(); - Crafty.viewport.mouselook('stop'); - Crafty.viewport.scale(); - } - }, + /**@ * #Crafty.keys @@ -1256,22 +593,3 @@ Crafty.extend({ RIGHT: 2 } }); - - - -/** -* Entity fixes the lack of setter support -*/ -Crafty.c("viewport", { - init: function () { - this.bind("EnterFrame", function () { - if (Crafty.viewport._x !== Crafty.viewport.x) { - Crafty.viewport.scroll('_x', Crafty.viewport.x); - } - - if (Crafty.viewport._y !== Crafty.viewport.y) { - Crafty.viewport.scroll('_y', Crafty.viewport.y); - } - }); - } -}); diff --git a/src/viewport.js b/src/viewport.js new file mode 100644 index 00000000..89840dfa --- /dev/null +++ b/src/viewport.js @@ -0,0 +1,672 @@ +Crafty.extend({ + /**@ + * #Crafty.viewport + * @category Stage + * @trigger ViewportScroll - when the viewport's x or y coordinates change + * @trigger ViewportScale - when the viewport's scale changes + * @trigger InvalidateViewport - when the viewport changes + * + * Viewport is essentially a 2D camera looking at the stage. Can be moved which + * in turn will react just like a camera moving in that direction. + */ + viewport: { + /**@ + * #Crafty.viewport.clampToEntities + * @comp Crafty.viewport + * + * Decides if the viewport functions should clamp to game entities. + * When set to `true` functions such as Crafty.viewport.mouselook() will not allow you to move the + * viewport over areas of the game that has no entities. + * For development it can be useful to set this to false. + */ + clampToEntities: true, + width: 0, + height: 0, + /**@ + * #Crafty.viewport.x + * @comp Crafty.viewport + * + * Will move the stage and therefore every visible entity along the `x` + * axis in the opposite direction. + * + * When this value is set, it will shift the entire stage. This means that entity + * positions are not exactly where they are on screen. To get the exact position, + * simply add `Crafty.viewport.x` onto the entities `x` position. + */ + _x: 0, + /**@ + * #Crafty.viewport.y + * @comp Crafty.viewport + * + * Will move the stage and therefore every visible entity along the `y` + * axis in the opposite direction. + * + * When this value is set, it will shift the entire stage. This means that entity + * positions are not exactly where they are on screen. To get the exact position, + * simply add `Crafty.viewport.y` onto the entities `y` position. + */ + _y: 0, + + /**@ + * #Crafty.viewport._scale + * @comp Crafty.viewport + * + * What scale to render the viewport at. This does not alter the size of the stage itself, but the magnification of what it shows. + */ + + _scale: 1, + + /**@ + * #Crafty.viewport.bounds + * @comp Crafty.viewport + * + * A rectangle which defines the bounds of the viewport. If this + * variable is null, Crafty uses the bounding box of all the items + * on the stage. + */ + bounds:null, + + /**@ + * #Crafty.viewport.scroll + * @comp Crafty.viewport + * @sign Crafty.viewport.scroll(String axis, Number v) + * @param axis - 'x' or 'y' + * @param v - The new absolute position on the axis + * + * Will move the viewport to the position given on the specified axis + * + * @example + * Will move the camera 500 pixels right of its initial position, in effect + * shifting everything in the viewport 500 pixels to the left. + * + * ~~~ + * Crafty.viewport.scroll('_x', 500); + * ~~~ + */ + scroll: function (axis, v) { + v = Math.floor(v); + this[axis] = v + Crafty.trigger("ViewportScroll") + Crafty.trigger("InvalidateViewport") + }, + + rect: function () { + return { _x: -this._x/this._scale, _y: -this._y/this._scale, _w: this.width/this._scale, _h: this.height/this._scale }; + }, + + /**@ + * #Crafty.viewport.pan + * @comp Crafty.viewport + * @sign public void Crafty.viewport.pan(String axis, Number v, Number time) + * @param String axis - 'x' or 'y'. The axis to move the camera on + * @param Number v - the distance to move the camera by + * @param Number time - The duration in frames for the entire camera movement + * + * Pans the camera a given number of pixels over a given number of frames + */ + pan: (function () { + var tweens = {}, i, bound = false; + + function enterFrame(e) { + var l = 0; + for (i in tweens) { + var prop = tweens[i]; + if (prop.remTime > 0) { + prop.current += prop.diff; + prop.remTime--; + Crafty.viewport[i] = Math.floor(prop.current); + l++; + } + else { + delete tweens[i]; + } + } + if (l) Crafty.viewport._clamp(); + } + + return function (axis, v, time) { + Crafty.viewport.follow(); + if (axis == 'reset') { + for (i in tweens) { + tweens[i].remTime = 0; + } + return; + } + if (time == 0) time = 1; + tweens[axis] = { + diff: -v / time, + current: Crafty.viewport[axis], + remTime: time + }; + if (!bound) { + Crafty.bind("EnterFrame", enterFrame); + bound = true; + } + } + })(), + + /**@ + * #Crafty.viewport.follow + * @comp Crafty.viewport + * @sign public void Crafty.viewport.follow(Object target, Number offsetx, Number offsety) + * @param Object target - An entity with the 2D component + * @param Number offsetx - Follow target should be offsetx pixels away from center + * @param Number offsety - Positive puts target to the right of center + * + * Follows a given entity with the 2D component. If following target will take a portion of + * the viewport out of bounds of the world, following will stop until the target moves away. + * + * @example + * ~~~ + * var ent = Crafty.e('2D, DOM').attr({w: 100, h: 100:}); + * Crafty.viewport.follow(ent, 0, 0); + * ~~~ + */ + follow: (function () { + var oldTarget, offx, offy; + + function change() { + Crafty.viewport.scroll('_x', -(this.x + (this.w / 2) - (Crafty.viewport.width / 2) - offx)); + Crafty.viewport.scroll('_y', -(this.y + (this.h / 2) - (Crafty.viewport.height / 2) - offy)); + Crafty.viewport._clamp(); + } + + return function (target, offsetx, offsety) { + if (oldTarget) + oldTarget.unbind('Change', change); + if (!target || !target.has('2D')) + return; + Crafty.viewport.pan('reset'); + + oldTarget = target; + offx = (typeof offsetx != 'undefined') ? offsetx : 0; + offy = (typeof offsety != 'undefined') ? offsety : 0; + + target.bind('Change', change); + change.call(target); + } + })(), + + /**@ + * #Crafty.viewport.centerOn + * @comp Crafty.viewport + * @sign public void Crafty.viewport.centerOn(Object target, Number time) + * @param Object target - An entity with the 2D component + * @param Number time - The number of frames to perform the centering over + * + * Centers the viewport on the given entity + */ + centerOn: function (targ, time) { + var x = targ.x + Crafty.viewport.x, + y = targ.y + Crafty.viewport.y, + mid_x = targ.w / 2, + mid_y = targ.h / 2, + cent_x = Crafty.viewport.width / 2, + cent_y = Crafty.viewport.height / 2, + new_x = x + mid_x - cent_x, + new_y = y + mid_y - cent_y; + + Crafty.viewport.pan('reset'); + Crafty.viewport.pan('x', new_x, time); + Crafty.viewport.pan('y', new_y, time); + }, + /**@ + * #Crafty.viewport._zoom + * @comp Crafty.viewport + * + * This value keeps an amount of viewport zoom, required for calculating mouse position at entity + */ + _zoom : 1, + + /**@ + * #Crafty.viewport.zoom + * @comp Crafty.viewport + * @sign public void Crafty.viewport.zoom(Number amt, Number cent_x, Number cent_y, Number time) + * @param Number amt - amount to zoom in on the target by (eg. 2, 4, 0.5) + * @param Number cent_x - the center to zoom on + * @param Number cent_y - the center to zoom on + * @param Number time - the duration in frames of the entire zoom operation + * + * Zooms the camera in on a given point. amt > 1 will bring the camera closer to the subject + * amt < 1 will bring it farther away. amt = 0 will do nothing. + * Zooming is multiplicative. To reset the zoom amount, pass 0. + */ + zoom: (function () { + var zoom = 1, + zoom_tick = 0, + dur = 0, + prop = Crafty.support.prefix + "Transform", + bound = false, + act = {}, + prct = {}; + // what's going on: + // 1. Get the original point as a percentage of the stage + // 2. Scale the stage + // 3. Get the new size of the stage + // 4. Get the absolute position of our point using previous percentage + // 4. Offset inner by that much + + function enterFrame() { + if (dur > 0) { + if (isFinite(Crafty.viewport._zoom)) zoom = Crafty.viewport._zoom; + var old = { + width: act.width * zoom, + height: act.height * zoom + }; + zoom += zoom_tick; + Crafty.viewport._zoom = zoom; + var new_s = { + width: act.width * zoom, + height: act.height * zoom + }, + diff = { + width: new_s.width - old.width, + height: new_s.height - old.height + }; + Crafty.stage.inner.style[prop] = 'scale(' + zoom + ',' + zoom + ')'; + if (Crafty.canvas._canvas) { + var czoom = zoom / (zoom - zoom_tick); + Crafty.canvas.context.scale(czoom, czoom); + Crafty.trigger("InvalidateViewport") + } + Crafty.viewport.x -= diff.width * prct.width; + Crafty.viewport.y -= diff.height * prct.height; + dur--; + } + } + + return function (amt, cent_x, cent_y, time) { + var bounds = this.bounds || Crafty.map.boundaries(), + final_zoom = amt ? zoom * amt : 1; + if (!amt) { // we're resetting to defaults + zoom = 1; + this._zoom = 1; + } + + act.width = bounds.max.x - bounds.min.x; + act.height = bounds.max.y - bounds.min.y; + + prct.width = cent_x / act.width; + prct.height = cent_y / act.height; + + if (time == 0) time = 1; + zoom_tick = (final_zoom - zoom) / time; + dur = time; + + Crafty.viewport.pan('reset'); + if (!bound) { + Crafty.bind('EnterFrame', enterFrame); + bound = true; + } + } + })(), + /**@ + * #Crafty.viewport.scale + * @comp Crafty.viewport + * @sign public void Crafty.viewport.scale(Number amt) + * @param Number amt - amount to zoom/scale in on the element on the viewport by (eg. 2, 4, 0.5) + * + * Zooms/scale the camera. amt > 1 increase all entities on stage + * amt < 1 will reduce all entities on stage. amt = 0 will reset the zoom/scale. + * Zooming/scaling is multiplicative. To reset the zoom/scale amount, pass 0. + * + * @example + * ~~~ + * Crafty.viewport.scale(2); //to see effect add some entities on stage. + * ~~~ + */ + scale: (function () { + return function (amt) { + var bounds = this.bounds || Crafty.map.boundaries(), + final_zoom = amt ? amt : 1; + + + this._zoom = final_zoom; + this._scale = final_zoom; + Crafty.trigger("InvalidateViewport"); + Crafty.trigger("ViewportScale"); + + } + })(), + /**@ + * #Crafty.viewport.mouselook + * @comp Crafty.viewport + * @sign public void Crafty.viewport.mouselook(Boolean active) + * @param Boolean active - Activate or deactivate mouselook + * + * Toggle mouselook on the current viewport. + * Simply call this function and the user will be able to + * drag the viewport around. + */ + mouselook: (function () { + var active = false, + dragging = false, + lastMouse = {} + old = {}; + + + return function (op, arg) { + if (typeof op == 'boolean') { + active = op; + if (active) { + Crafty.mouseObjs++; + } + else { + Crafty.mouseObjs = Math.max(0, Crafty.mouseObjs - 1); + } + return; + } + if (!active) return; + switch (op) { + case 'move': + case 'drag': + if (!dragging) return; + diff = { + x: arg.clientX - lastMouse.x, + y: arg.clientY - lastMouse.y + }; + + Crafty.viewport.x += diff.x; + Crafty.viewport.y += diff.y; + Crafty.viewport._clamp(); + case 'start': + lastMouse.x = arg.clientX; + lastMouse.y = arg.clientY; + dragging = true; + break; + case 'stop': + dragging = false; + break; + } + }; + })(), + _clamp: function () { + // clamps the viewport to the viewable area + // under no circumstances should the viewport see something outside the boundary of the 'world' + if (!this.clampToEntities) return; + var bound = this.bounds || Crafty.map.boundaries(); + bound.max.x *= this._zoom; + bound.min.x *= this._zoom; + bound.max.y *= this._zoom; + bound.min.y *= this._zoom; + if (bound.max.x - bound.min.x > Crafty.viewport.width) { + bound.max.x -= Crafty.viewport.width; + + if (Crafty.viewport.x < -bound.max.x) { + Crafty.viewport.x = -bound.max.x; + } + else if (Crafty.viewport.x > -bound.min.x) { + Crafty.viewport.x = -bound.min.x; + } + } + else { + Crafty.viewport.x = -1 * (bound.min.x + (bound.max.x - bound.min.x) / 2 - Crafty.viewport.width / 2); + } + if (bound.max.y - bound.min.y > Crafty.viewport.height) { + bound.max.y -= Crafty.viewport.height; + + if (Crafty.viewport.y < -bound.max.y) { + Crafty.viewport.y = -bound.max.y; + } + else if (Crafty.viewport.y > -bound.min.y) { + Crafty.viewport.y = -bound.min.y; + } + } + else { + Crafty.viewport.y = -1 * (bound.min.y + (bound.max.y - bound.min.y) / 2 - Crafty.viewport.height / 2); + } + }, + + /**@ + * #Crafty.viewport.init + * @comp Crafty.viewport + * @sign public void Crafty.viewport.init([Number width, Number height, String stage_elem]) + * @sign public void Crafty.viewport.init([Number width, Number height, HTMLElement stage_elem]) + * @param Number width - Width of the viewport + * @param Number height - Height of the viewport + * @param String or HTMLElement stage_elem - the element to use as the stage (either its id or the actual element). + * + * Initialize the viewport. If the arguments 'width' or 'height' are missing, or Crafty.mobile is true, use Crafty.DOM.window.width and Crafty.DOM.window.height (full screen model). + * + * The argument 'stage_elem' is used to specify a stage element other than the default, and can be either a string or an HTMLElement. If a string is provided, it will look for an element with that id and, if none exists, create a div. If an HTMLElement is provided, that is used directly. Omitting this argument is the same as passing an id of 'cr-stage'. + * + * @see Crafty.device, Crafty.DOM, Crafty.stage + */ + init: function (w, h, stage_elem) { + Crafty.DOM.window.init(); + + //fullscreen if mobile or not specified + this.width = (!w || Crafty.mobile) ? Crafty.DOM.window.width : w; + this.height = (!h || Crafty.mobile) ? Crafty.DOM.window.height : h; + + //check if stage exists + if(typeof stage_elem === 'undefined') + stage_elem = "cr-stage"; + + var crstage; + if(typeof stage_elem === 'string') + crstage = document.getElementById(stage_elem); + else if(typeof HTMLElement !== "undefined" ? stage_elem instanceof HTMLElement : stage_elem instanceof Element) + crstage = stage_elem; + else + throw new TypeError("stage_elem must be a string or an HTMLElement"); + + /**@ + * #Crafty.stage + * @category Core + * The stage where all the DOM entities will be placed. + */ + + /**@ + * #Crafty.stage.elem + * @comp Crafty.stage + * The `#cr-stage` div element. + */ + + /**@ + * #Crafty.stage.inner + * @comp Crafty.stage + * `Crafty.stage.inner` is a div inside the `#cr-stage` div that holds all DOM entities. + * If you use canvas, a `canvas` element is created at the same level in the dom + * as the the `Crafty.stage.inner` div. So the hierarchy in the DOM is + * + * `Crafty.stage.elem` + * + * + * - `Crafty.stage.inner` (a div HTMLElement) + * + * - `Crafty.canvas._canvas` (a canvas HTMLElement) + */ + + //create stage div to contain everything + Crafty.stage = { + x: 0, + y: 0, + fullscreen: false, + elem: (crstage ? crstage : document.createElement("div")), + inner: document.createElement("div") + }; + + //fullscreen, stop scrollbars + if ((!w && !h) || Crafty.mobile) { + document.body.style.overflow = "hidden"; + Crafty.stage.fullscreen = true; + } + + Crafty.addEvent(this, window, "resize", Crafty.viewport.reload); + + Crafty.addEvent(this, window, "blur", function () { + if (Crafty.settings.get("autoPause")) { + if(!Crafty._paused) Crafty.pause(); + } + }); + Crafty.addEvent(this, window, "focus", function () { + if (Crafty._paused && Crafty.settings.get("autoPause")) { + Crafty.pause(); + } + }); + + //make the stage unselectable + Crafty.settings.register("stageSelectable", function (v) { + Crafty.stage.elem.onselectstart = v ? function () { return true; } : function () { return false; }; + }); + Crafty.settings.modify("stageSelectable", false); + + //make the stage have no context menu + Crafty.settings.register("stageContextMenu", function (v) { + Crafty.stage.elem.oncontextmenu = v ? function () { return true; } : function () { return false; }; + }); + Crafty.settings.modify("stageContextMenu", false); + + Crafty.settings.register("autoPause", function (){ }); + Crafty.settings.modify("autoPause", false); + + //add to the body and give it an ID if not exists + if (!crstage) { + document.body.appendChild(Crafty.stage.elem); + Crafty.stage.elem.id = stage_elem; + } + + var elem = Crafty.stage.elem.style, + offset; + + Crafty.stage.elem.appendChild(Crafty.stage.inner); + Crafty.stage.inner.style.position = "absolute"; + Crafty.stage.inner.style.zIndex = "1"; + Crafty.stage.inner.style.transformStyle = "preserve-3d"; // Seems necessary for Firefox to preserve zIndexes? + + //css style + elem.width = this.width + "px"; + elem.height = this.height + "px"; + elem.overflow = "hidden"; + + if (Crafty.mobile) { + elem.position = "absolute"; + elem.left = "0px"; + elem.top = "0px"; + + // remove default gray highlighting after touch + if (typeof elem.webkitTapHighlightColor != undefined) { + elem.webkitTapHighlightColor = "rgba(0,0,0,0)"; + } + + var meta = document.createElement("meta"), + head = document.getElementsByTagName("HEAD")[0]; + + //stop mobile zooming and scrolling + meta.setAttribute("name", "viewport"); + meta.setAttribute("content", "width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no"); + head.appendChild(meta); + + //hide the address bar + meta = document.createElement("meta"); + meta.setAttribute("name", "apple-mobile-web-app-capable"); + meta.setAttribute("content", "yes"); + head.appendChild(meta); + setTimeout(function () { window.scrollTo(0, 1); }, 0); + + Crafty.addEvent(this, window, "touchmove", function (e) { + e.preventDefault(); + }); + + Crafty.stage.x = 0; + Crafty.stage.y = 0; + + } else { + elem.position = "relative"; + //find out the offset position of the stage + offset = Crafty.DOM.inner(Crafty.stage.elem); + Crafty.stage.x = offset.x; + Crafty.stage.y = offset.y; + } + + if (Crafty.support.setter) { + //define getters and setters to scroll the viewport + this.__defineSetter__('x', function (v) { this.scroll('_x', v); }); + this.__defineSetter__('y', function (v) { this.scroll('_y', v); }); + this.__defineGetter__('x', function () { return this._x; }); + this.__defineGetter__('y', function () { return this._y; }); + + //IE9 + } else if (Crafty.support.defineProperty) { + Object.defineProperty(this, 'x', { set: function (v) { this.scroll('_x', v); }, get: function () { return this._x; } }); + Object.defineProperty(this, 'y', { set: function (v) { this.scroll('_y', v); }, get: function () { return this._y; } }); + } else { + //create empty entity waiting for enterframe + this.x = this._x; + this.y = this._y; + Crafty.e("ViewportSetter"); + } + }, + + /**@ + * #Crafty.viewport.reload + * @comp Crafty.stage + * + * @sign public Crafty.viewport.reload() + * + * Recalculate and reload stage width, height and position. + * Useful when browser return wrong results on init (like safari on Ipad2). + * + */ + reload : function () { + Crafty.DOM.window.init(); + var w = Crafty.DOM.window.width, + h = Crafty.DOM.window.height, + offset; + + + if (Crafty.stage.fullscreen) { + this.width = w; + this.height = h; + Crafty.stage.elem.style.width = w + "px"; + Crafty.stage.elem.style.height = h + "px"; + + if (Crafty.canvas._canvas) { + Crafty.canvas._canvas.width = w; + Crafty.canvas._canvas.height = h; + Crafty.trigger("InvalidateViewport") + } + } + + offset = Crafty.DOM.inner(Crafty.stage.elem); + Crafty.stage.x = offset.x; + Crafty.stage.y = offset.y; + }, + + /**@ + * #Crafty.viewport.reset + * @comp Crafty.stage + * + * @sign public Crafty.viewport.reset() + * + * Resets the viewport to starting values + * Called when scene() is run. + */ + reset: function () { + Crafty.viewport.pan('reset'); + Crafty.viewport.follow(); + Crafty.viewport.mouselook('stop'); + Crafty.viewport.scale(); + } + } +}); + + +/** +* Entity fixes the lack of setter support +*/ +Crafty.c("ViewportSetter", { + init: function () { + this.bind("EnterFrame", function () { + if (Crafty.viewport._x !== Crafty.viewport.x) { + Crafty.viewport.scroll('_x', Crafty.viewport.x); + } + + if (Crafty.viewport._y !== Crafty.viewport.y) { + Crafty.viewport.scroll('_y', Crafty.viewport.y); + } + + }); + } +}); \ No newline at end of file