diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 00000000..6ee021dd --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,12 @@ +# How to contribute + +Hi, thanks for contributing to Crafty! We've got guidlines on + +- [How to build Crafty](https://github.com/craftyjs/Crafty/wiki/Building) +- [What workflow to use](https://github.com/craftyjs/Crafty/wiki/Workflow) + +## Quick summary + +- You'll need node, npm, and grunt-cli (installed globally) to work with Crafty's dev tools +- With those already installed, `npm install` from Crafty's directory will setup everything else you need +- Once you've made any changes, then `grunt check` will build and test your new version of Crafty. \ No newline at end of file diff --git a/Gruntfile.js b/Gruntfile.js index d60cb2fb..3cd633bb 100644 --- a/Gruntfile.js +++ b/Gruntfile.js @@ -76,8 +76,9 @@ module.exports = function (grunt) { }, jshint: { - files: ['Gruntfile.js', 'src/**/*.js'], + files: ['Gruntfile.js', 'src/**/*.js', 'tests/*.js'], options: { + trailing: true, globals: { } } @@ -85,24 +86,23 @@ module.exports = function (grunt) { qunit: { all: [ - 'tests/core.html', - 'tests/animation/animation.html', - 'tests/stage.html', - 'tests/events.html', - 'tests/math.html', - 'tests/isometric.html', - 'tests/loader.html', - 'tests/text.html', - 'tests/dom.html', - 'tests/tween.html', - 'tests/sound.html' + 'tests/index.html', + 'tests/animation/animation.html' ] - }, + }, jsvalidate: { - files: "crafty.js" + files: ['crafty.js', 'tests/*.js'] }, + connect: { + server: { + options: { + keepalive: true + } + } + } + }); // Load the plugin that provides the "uglify" task. @@ -110,6 +110,7 @@ module.exports = function (grunt) { grunt.loadNpmTasks('grunt-contrib-jshint'); grunt.loadNpmTasks('grunt-contrib-qunit'); grunt.loadNpmTasks('grunt-contrib-watch'); + grunt.loadNpmTasks('grunt-contrib-connect'); grunt.loadNpmTasks('grunt-jsvalidate'); grunt.loadNpmTasks('grunt-browserify'); grunt.loadNpmTasks('grunt-banner'); @@ -117,10 +118,10 @@ module.exports = function (grunt) { grunt.registerTask('version', 'Takes the version into src/version.js', function() { fs.writeFileSync('src/version.js', 'module.exports = "' + version + '";'); }); - + // Build development grunt.registerTask('build:dev', ['browserify:debug', 'usebanner']); - + // Build release grunt.registerTask('build:release', ['browserify:dist', 'usebanner']); @@ -139,5 +140,4 @@ module.exports = function (grunt) { // Run only tests grunt.registerTask('validate', ['qunit']); - }; diff --git a/README.md b/README.md index b4e1d6ae..c76687da 100644 --- a/README.md +++ b/README.md @@ -72,7 +72,7 @@ Crafty.e("RightPoints, DOM, 2D, Text") ##Developing If you want to fix a bug, please submit a pull request against the development branch. Some guides to help you can be found [on the wiki](https://github.com/craftyjs/Crafty/wiki) - + If you would like to make larger contributions please catch us in the [forum](https://groups.google.com/forum/?fromgroups#!forum/craftyjs) and we will help you get started. Much appreciated :-) diff --git a/build/api-gen.coffee b/build/api-gen.coffee index 566a2a39..24286b15 100644 --- a/build/api-gen.coffee +++ b/build/api-gen.coffee @@ -38,7 +38,7 @@ processBlock = (block)-> if not Table.comps[block.comp] Table.comps[block.name] = {name:block.name, parts:[]} Table.comps[block.name].block = block - + # Having a component tag means it's part of a component page if block.comp if not Table.comps[block.comp] @@ -123,10 +123,10 @@ class DocBlock else return false - # Method for returning the documentation for this block + # Method for returning the documentation for this block getContent: ()-> if @isFunctionTag(@prevTag) then @code.push("") - return @code.join("\n") + triggerBlock(@triggers) + seeBlock(@see) + return @code.join("\n") + triggerBlock(@triggers) + seeBlock(@see) # parse js file parseJS = (path) -> @@ -140,7 +140,7 @@ parseJS = (path) -> block = new DocBlock() block.file = path block.line = ln - open = true + open = true # process if open block.processLine(line) @@ -153,18 +153,18 @@ parseJS = (path) -> if block.categories.length is 0 and block.comp is null console.log("No component or category for block at #{block.file}:#{block.line+1} (#{block.name})") data.push block - + cleanName = (name) -> name.replace(".", "-") triggerBlock = (triggers, noheader)-> - return '' if triggers?.length is 0 - if noheader then block = "
" else block = "\n

Events

\n
" + return '' if triggers?.length is 0 + if noheader then block = "
" else block = "\n

Events

\n
" for t in triggers if t.objProp? - block+= "
#{t.event} [#{t.objName}: #{t.objProp}]
#{t.description}
\n" + block+= "
#{t.event} [#{t.objName}: #{t.objProp}]
#{t.description}
\n" else - block+= "
#{t.event}
#{t.description}
\n" + block+= "
#{t.event}
#{t.description}
\n" block+="
" return block @@ -185,9 +185,9 @@ createPage = (page)-> eventContent = "##{page.name}\n" + triggerBlock(page.block.triggers, true) partContent = "" - if page.parts.length + if page.parts.length partList = "

Properties and Methods

    " - for part in page.parts + for part in page.parts clName = cleanName(part.name) partList += """
  • #{part.name}
  • """ partContent += "\n\n
    \n\nBack to top

    #{part.name}

    \n" + marked(part.getContent()) + "\n\n
    \n\n" @@ -219,7 +219,7 @@ saveMd = (data) -> nav_html+="
  • #{page.name}
  • " # Pages can be listed multiple times in the nav, but we should only generate them once - continue if page.flagged is true + continue if page.flagged is true page.flagged = true # Make page @@ -246,7 +246,7 @@ saveMd = (data) -> for page in pages html = template.replace("CONTENT_DIV", page.content_html).replace("NAV_DIV", nav_html) writeOut(page.name, html) - + return @@ -267,7 +267,7 @@ document = (files, output, template, version, callback)-> dirOut = output versionString = version for file in files - parseJS file + parseJS file # Save the data to files saveMd data diff --git a/build/template.html b/build/template.html index e86344c2..c973e7fc 100644 --- a/build/template.html +++ b/build/template.html @@ -1,4 +1,4 @@ ---- +--- layout: default --- diff --git a/changelog.txt b/changelog.txt index 54b3d5a1..bd4751fa 100644 --- a/changelog.txt +++ b/changelog.txt @@ -140,7 +140,7 @@ v0.3.2 * Fixed a collision bug -v0.3.1 +v0.3.1 * Window resize even on fullscreen * Use scrollTop and scrollLeft diff --git a/package.json b/package.json index 01b60eb6..79e38c92 100644 --- a/package.json +++ b/package.json @@ -37,6 +37,7 @@ "src/core.js", "src/HashMap.js", "src/2D.js", + "src/scenes.js", "src/collision.js", "src/DOM.js", "src/html.js", @@ -57,7 +58,8 @@ "src/loader.js", "src/math.js", "src/time.js", - "src/DebugLayer.js" + "src/DebugLayer.js", + "src/keycodes.js" ], "dependencies": { "grunt": "~0.4.1", @@ -71,5 +73,8 @@ "grunt-browserify": "~1.2.9", "grunt-banner": "~0.2.0", "grunt-contrib-watch": "~0.5.3" + }, + "devDependencies": { + "grunt-contrib-connect": "~0.6.0" } } diff --git a/src/2D.js b/src/2D.js index a4c4bf0f..46824cd2 100644 --- a/src/2D.js +++ b/src/2D.js @@ -1,14 +1,14 @@ var Crafty = require('./core.js'), document = window.document, HashMap = require('./HashMap.js'); -// Crafty._rectPool +// Crafty._rectPool // // This is a private object used internally by 2D methods // Cascade and _attr need to keep track of an entity's old position, // but we want to avoid creating temp objects every time an attribute is set. // The solution is to have a pool of objects that can be reused. // -// The current implementation makes a BIG ASSUMPTION: that if multiple rectangles are requested, +// The current implementation makes a BIG ASSUMPTION: that if multiple rectangles are requested, // the later one is recycled before any preceding ones. This matches how they are used in the code. // Each rect is created by a triggered event, and will be recycled by the time the event is complete. Crafty._rectPool = (function () { @@ -57,14 +57,22 @@ var M = Math, PI = M.PI, DEG_TO_RAD = PI / 180; +Crafty.extend({ + zeroFill: function (number, width) { + width -= number.toString().length; + if (width > 0) + return new Array(width + (/\./.test(number) ? 2 : 1)).join('0') + number; + return number.toString(); + } +}); /**@ * #2D * @category 2D * Component for any entity that has a position on the stage. * @trigger Move - when the entity has moved - { _x:Number, _y:Number, _w:Number, _h:Number } - Old position - * @trigger Change - when the entity has moved - { _x:Number, _y:Number, _w:Number, _h:Number } - Old position - * @trigger Rotate - when the entity is rotated - { cos:Number, sin:Number, deg:Number, rad:Number, o: {x:Number, y:Number}, matrix: {M11, M12, M21, M22} } + * @trigger Invalidate - when the entity needs to be redrawn + * @trigger Rotate - when the entity is rotated - { cos:Number, sin:Number, deg:Number, rad:Number, o: {x:Number, y:Number}} */ Crafty.c("2D", { /**@ @@ -318,87 +326,19 @@ Crafty.c("2D", { }); }, - _defineGetterSetter_fallback: function () { - //set the public properties to the current private properties - this.x = this._x; - this.y = this._y; - this.w = this._w; - this.h = this._h; - this.z = this._z; - this.rotation = this._rotation; - this.alpha = this._alpha; - this.visible = this._visible; - - //on every frame check for a difference in any property - this.bind("EnterFrame", function () { - //if there are differences between the public and private properties - if (this.x !== this._x || this.y !== this._y || - this.w !== this._w || this.h !== this._h || - this.z !== this._z || this.rotation !== this._rotation || - this.alpha !== this._alpha || this.visible !== this._visible) { - - //save the old positions - var old = Crafty._rectPool.copy(this); - - //if rotation has changed, use the private rotate method - if (this.rotation !== this._rotation) { - this._rotate(this.rotation); - } else { - //update the MBR - var mbr = this._mbr, - moved = false; - // If the browser doesn't have getters or setters, - // {x, y, w, h, z} and {_x, _y, _w, _h, _z} may be out of sync, - // in which case t checks if they are different on tick and executes the Change event. - if (mbr) { //check each value to see which has changed - if (this.x !== this._x) { - mbr._x -= this.x - this._x; - moved = true; - } else if (this.y !== this._y) { - mbr._y -= this.y - this._y; - moved = true; - } else if (this.w !== this._w) { - mbr._w -= this.w - this._w; - moved = true; - } else if (this.h !== this._h) { - mbr._h -= this.h - this._h; - moved = true; - } else if (this.z !== this._z) { - mbr._z -= this.z - this._z; - moved = true; - } - } - - //if the moved flag is true, trigger a move - if (moved) this.trigger("Move", old); - } - - //set the public properties to the private properties - this._x = this.x; - this._y = this.y; - this._w = this.w; - this._h = this.h; - this._z = this.z; - this._rotation = this.rotation; - this._alpha = this.alpha; - this._visible = this.visible; - - //trigger the changes - this.trigger("Change", old); - //without this entities weren't added correctly to Crafty.map.map in IE8. - //not entirely sure this is the best way to fix it though - this.trigger("Move", old); - Crafty._rectPool.recycle(old); - } - }); - }, - init: function () { this._globalZ = this[0]; this._origin = { x: 0, y: 0 }; + + // offsets for the basic bounding box + this._bx1 = 0; + this._bx2 = 0; + this._by1 = 0; + this._by2 = 0; + this._children = []; if (Crafty.support.setter) { @@ -406,13 +346,6 @@ Crafty.c("2D", { } else if (Crafty.support.defineProperty) { //IE9 supports Object.defineProperty this._defineGetterSetter_defineProperty(); - } else { - /* - If no setters and getters are supported (e.g. IE8) supports, - check on every frame for a difference between this._(x|y|w|h|z...) - and this.(x|y|w|h|z) and update accordingly. - */ - this._defineGetterSetter_fallback(); } //insert self into the HashMap @@ -420,7 +353,8 @@ Crafty.c("2D", { //when object changes, update HashMap this.bind("Move", function (e) { - var area = this._mbr || this; + // Choose the largest bounding region that exists + var area = this._cbr || this._mbr || this; this._entry.update(area); // Move children (if any) by the same amount if (this._children.length > 0) { @@ -429,7 +363,8 @@ Crafty.c("2D", { }); this.bind("Rotate", function (e) { - var old = this._mbr || this; + // Choose the largest bounding region that exists + var old = this._cbr || this._mbr || this; this._entry.update(old); // Rotate children (if any) by the same amount if (this._children.length > 0) { @@ -465,34 +400,69 @@ Crafty.c("2D", { }, + /**@ + * #.offsetBoundary + * @comp 2D + * Extends the MBR of the entity by a specified amount. + * + * @trigger BoundaryOffset - when the MBR offset changes + * @sign public this .offsetBoundary(Number dx1, Number dy1, Number dx2, Number dy2) + * @param dx1 - Extends the MBR to the left by this amount + * @param dy1 - Extends the MBR upward by this amount + * @param dx2 - Extends the MBR to the right by this amount + * @param dy2 - Extends the MBR downward by this amount + * + * @sign public this .offsetBoundary(Number offset) + * @param offset - Extend the MBR in all directions by this amount + * + * You would most likely use this function to ensure that custom canvas rendering beyond the extent of the entity's normal bounds is not clipped. + */ + offsetBoundary: function(x1, y1, x2, y2){ + if (arguments.length === 1) + y1 = x2 = y2 = x1; + this._bx1 = x1; + this._bx2 = x2; + this._by1 = y1; + this._by2 = y2; + this.trigger("BoundaryOffset"); + this._calculateMBR(); + return this; + }, + /** * Calculates the MBR when rotated some number of radians about an origin point o. - * Necessary on a rotation, or a resize (when already rotated) + * Necessary on a rotation, or a resize */ - _calculateMBR: function (ox, oy, rad) { - if (rad === 0) { - this._mbr = null; - return; - } + _calculateMBR: function () { + var ox = this._origin.x + this._x, + oy = this._origin.y + this._y, + rad = -this._rotation * DEG_TO_RAD; + // axis-aligned (unrotated) coordinates, relative to the origin point + var dx1 = this._x - this._bx1 - ox, + dx2 = this._x + this._w + this._bx2 - ox, + dy1 = this._y - this._by1 - oy, + dy2 = this._y + this._h + this._by2 - oy; var ct = Math.cos(rad), st = Math.sin(rad); // Special case 90 degree rotations to prevent rounding problems ct = (ct < 1e-10 && ct > -1e-10) ? 0 : ct; st = (st < 1e-10 && st > -1e-10) ? 0 : st; - var x0 = ox + (this._x - ox) * ct + (this._y - oy) * st, - y0 = oy - (this._x - ox) * st + (this._y - oy) * ct, - x1 = ox + (this._x + this._w - ox) * ct + (this._y - oy) * st, - y1 = oy - (this._x + this._w - ox) * st + (this._y - oy) * ct, - x2 = ox + (this._x + this._w - ox) * ct + (this._y + this._h - oy) * st, - y2 = oy - (this._x + this._w - ox) * st + (this._y + this._h - oy) * ct, - x3 = ox + (this._x - ox) * ct + (this._y + this._h - oy) * st, - y3 = oy - (this._x - ox) * st + (this._y + this._h - oy) * ct, - minx = Math.floor(Math.min(x0, x1, x2, x3)), - miny = Math.floor(Math.min(y0, y1, y2, y3)), - maxx = Math.ceil(Math.max(x0, x1, x2, x3)), - maxy = Math.ceil(Math.max(y0, y1, y2, y3)); + + // Calculate the new points relative to the origin, then find the new (absolute) bounding coordinates! + var x0 = dx1 * ct + dy1 * st, + y0 = - dx1 * st + dy1 * ct, + x1 = dx2 * ct + dy1 * st, + y1 = - dx2 * st + dy1 * ct, + x2 = dx2 * ct + dy2 * st, + y2 = - dx2 * st + dy2 * ct, + x3 = dx1 * ct + dy2 * st, + y3 = - dx1 * st + dy2 * ct, + minx = Math.floor(Math.min(x0, x1, x2, x3) + ox), + miny = Math.floor(Math.min(y0, y1, y2, y3) + oy), + maxx = Math.ceil(Math.max(x0, x1, x2, x3) + ox), + maxy = Math.ceil(Math.max(y0, y1, y2, y3) + oy); if (!this._mbr) { this._mbr = { _x: minx, @@ -507,6 +477,23 @@ Crafty.c("2D", { this._mbr._h = maxy - miny; } + // If a collision hitbox exists AND sits outside the entity, find a bounding box for both. + // `_cbr` contains information about a bounding circle of the hitbox. + // The bounds of `_cbr` will be the union of the `_mbr` and the bounding box of that circle. + // This will not be a minimal region, but since it's only used for the broad phase pass it's good enough. + // + // cbr is calculated by the `_checkBounds` method of the "Collision" component + if (this._cbr) { + var cbr = this._cbr; + var cx = cbr.cx, cy = cbr.cy, r = cbr.r; + var cx2 = ox + (cx + this._x - ox) * ct + (cy + this._y - oy) * st; + var cy2 = oy - (cx + this._x - ox) * st + (cy + this._y - oy) * ct; + cbr._x = Math.min(cx2 - r, minx); + cbr._y = Math.min(cy2 - r, miny); + cbr._w = Math.max(cx2 + r, maxx) - cbr._x; + cbr._h = Math.max(cy2 + r, maxy) - cbr._y; + } + }, /** @@ -518,6 +505,8 @@ Crafty.c("2D", { // skip if there's no rotation! if (difference === 0) return; + else + this._rotation = v; //Calculate the new MBR var rad = theta * DEG_TO_RAD, @@ -526,7 +515,7 @@ Crafty.c("2D", { y: this._origin.y + this._y }; - this._calculateMBR(o.x, o.y, rad); + this._calculateMBR(); //trigger "Rotate" event @@ -539,13 +528,7 @@ Crafty.c("2D", { sin: Math.sin(drad), deg: difference, rad: drad, - o: o, - matrix: { - M11: ct, - M12: st, - M21: -st, - M22: ct - } + o: o }); }, @@ -888,7 +871,7 @@ Crafty.c("2D", { /**@ * #.flip * @comp 2D - * @trigger Change - when the entity has flipped + * @trigger Invalidate - when the entity has flipped * @sign public this .flip(String dir) * @param dir - Flip direction * @@ -903,7 +886,7 @@ Crafty.c("2D", { dir = dir || "X"; if (!this["_flip" + dir]) { this["_flip" + dir] = true; - this.trigger("Change"); + this.trigger("Invalidate"); } return this; }, @@ -911,7 +894,7 @@ Crafty.c("2D", { /**@ * #.unflip * @comp 2D - * @trigger Change - when the entity has unflipped + * @trigger Invalidate - when the entity has unflipped * @sign public this .unflip(String dir) * @param dir - Unflip direction * @@ -926,7 +909,7 @@ Crafty.c("2D", { dir = dir || "X"; if (this["_flip" + dir]) { this["_flip" + dir] = false; - this.trigger("Change"); + this.trigger("Invalidate"); } return this; }, @@ -940,7 +923,7 @@ Crafty.c("2D", { y2 = (this._y + this._origin.y - e.o.y) * e.cos - (this._x + this._origin.x - e.o.x) * e.sin + (e.o.y - this._origin.y); this._attr('_rotation', this._rotation - e.deg); this._attr('_x', x2 ); - this._attr('_y', y2 ); + this._attr('_y', y2 ); }, /**@ @@ -967,10 +950,15 @@ Crafty.c("2D", { this.trigger("reorder"); //if the rect bounds change, update the MBR and trigger move } else if (name === '_x' || name === '_y') { + // mbr is the minimal bounding rectangle of the entity mbr = this._mbr; - if (mbr) { mbr[name] -= this[name] - value; + // cbr is a non-minmal bounding rectangle that contains both hitbox and mbr + // It will exist only when the collision hitbox sits outside the entity + if (this._cbr){ + this._cbr[name] -= this[name] - value; + } } this[name] = value; @@ -982,7 +970,7 @@ Crafty.c("2D", { var oldValue = this[name]; this[name] = value; if (mbr) { - this._calculateMBR(this._origin.x + this._x, this._origin.y + this._y, -this._rotation * DEG_TO_RAD); + this._calculateMBR(); } if (name === '_w') { this.trigger("Resize", { @@ -1002,8 +990,8 @@ Crafty.c("2D", { //everything will assume the value this[name] = value; - //trigger a change - this.trigger("Change", old); + // flag for redraw + this.trigger("Invalidate"); Crafty._rectPool.recycle(old); } @@ -1012,6 +1000,8 @@ Crafty.c("2D", { /**@ * #Gravity * @category 2D + * @trigger Moved - When entity has moved on y-axis a Moved event is triggered with an object specifying the old position {x: old_x, y: old_y} + * * Adds gravitational pull to the entity. */ Crafty.c("Gravity", { @@ -1079,6 +1069,7 @@ Crafty.c("Gravity", { //if falling, move the players Y this._gy += this._gravityConst; this.y += this._gy; + this.trigger('Moved', { x: this._x, y: this._y - this._gy }); } else { this._gy = 0; //reset change in y } @@ -1112,7 +1103,7 @@ Crafty.c("Gravity", { if (hit) { //stop falling if found and player is moving down if (this._falling && ((this._gy > this._jumpSpeed) || !this._up)){ this.stopFalling(hit); - } + } } else { this._falling = true; //keep falling otherwise } diff --git a/src/DOM.js b/src/DOM.js index c1a7fd05..961f3f51 100644 --- a/src/DOM.js +++ b/src/DOM.js @@ -40,7 +40,7 @@ Crafty.c("DOM", { this._element.style.position = "absolute"; this._element.id = "ent" + this[0]; - this.bind("Change", function () { + this.bind("Invalidate", function () { if (!this._changed) { this._changed = true; Crafty.DrawManager.addDom(this); @@ -58,23 +58,21 @@ Crafty.c("DOM", { this._element.className = str; } - this.bind("NewComponent", updateClass).bind("RemoveComponent", updateClass); - - if (Crafty.support.prefix === "ms" && Crafty.support.version < 9) { - this._filters = {}; - - this.bind("Rotate", function (e) { - var m = e.matrix, - elem = this._element.style, - M11 = m.M11.toFixed(8), - M12 = m.M12.toFixed(8), - M21 = m.M21.toFixed(8), - M22 = m.M22.toFixed(8); - - this._filters.rotation = "progid:DXImageTransform.Microsoft.Matrix(M11=" + M11 + ", M12=" + M12 + ", M21=" + M21 + ", M22=" + M22 + ",sizingMethod='auto expand')"; - }); + function removeClass(removedComponent) { + var i = 0, + c = this.__c, + str = ""; + for (i in c) { + if(i != removedComponent) { + str += ' ' + i; + } + } + str = str.substr(1); + this._element.className = str; } + this.bind("NewComponent", updateClass).bind("RemoveComponent", removeClass); + this.bind("Remove", this.undraw); this.bind("RemoveComponent", function (compName) { if (compName === "DOM") @@ -172,17 +170,6 @@ Crafty.c("DOM", { style[prefix + "Opacity"] = this._alpha; } - //if not version 9 of IE - if (prefix === "ms" && Crafty.support.version < 9) { - //for IE version 8, use ImageTransform filter - if (Crafty.support.version === 8) { - this._filters.alpha = "progid:DXImageTransform.Microsoft.Alpha(Opacity=" + (this._alpha * 100) + ")"; // first! - //all other versions use filter - } else { - this._filters.alpha = "alpha(opacity=" + (this._alpha * 100) + ")"; - } - } - if (this._mbr) { var origin = this._origin.x + "px " + this._origin.y + "px"; style.transformOrigin = origin; @@ -193,21 +180,10 @@ Crafty.c("DOM", { if (this._flipX) { trans.push("scaleX(-1)"); - if (prefix === "ms" && Crafty.support.version < 9) { - this._filters.flipX = "fliph"; - } } if (this._flipY) { trans.push("scaleY(-1)"); - if (prefix === "ms" && Crafty.support.version < 9) { - this._filters.flipY = "flipv"; - } - } - - //apply the filters if IE - if (prefix === "ms" && Crafty.support.version < 9) { - this.applyFilters(); } if (this._cssStyles.transform != trans.join(" ")) { @@ -225,18 +201,6 @@ Crafty.c("DOM", { return this; }, - applyFilters: function () { - this._element.style.filter = ""; - var str = ""; - - for (var filter in this._filters) { - if (!this._filters.hasOwnProperty(filter)) continue; - str += this._filters[filter] + " "; - } - - this._element.style.filter = str; - }, - /**@ * #.undraw * @comp DOM @@ -254,10 +218,11 @@ Crafty.c("DOM", { /**@ * #.css * @comp DOM - * @sign public * css(String property, String value) + * @sign public css(String property, String value) * @param property - CSS property to modify * @param value - Value to give the CSS property - * @sign public * css(Object map) + * + * @sign public css(Object map) * @param map - Object where the key is the CSS property and the value is CSS value * * Apply CSS styles to the element. @@ -305,18 +270,12 @@ Crafty.c("DOM", { } } - this.trigger("Change"); + this.trigger("Invalidate"); return this; } }); -/** - * Fix IE6 background flickering - */ -try { - document.execCommand("BackgroundImageCache", false, true); -} catch (e) {} Crafty.extend({ /**@ @@ -339,8 +298,15 @@ Crafty.extend({ this.height = window.innerHeight || (window.document.documentElement.clientHeight || window.document.body.clientHeight); // Bind scene rendering (see drawing.js) - Crafty.unbind("RenderScene", Crafty.DrawManager.renderDOM); - Crafty.bind("RenderScene", Crafty.DrawManager.renderDOM); + Crafty.uniqueBind("RenderScene", Crafty.DrawManager.renderDOM); + // Resize the viewport + Crafty.uniqueBind("ViewportResize", this._resize); + + }, + + _resize: function(){ + Crafty.stage.elem.style.width = Crafty.viewport.width + "px"; + Crafty.stage.elem.style.height = Crafty.viewport.height + "px"; }, width: 0, diff --git a/src/DebugLayer.js b/src/DebugLayer.js index 228ac18c..a777611e 100644 --- a/src/DebugLayer.js +++ b/src/DebugLayer.js @@ -56,9 +56,10 @@ Crafty.c("DebugCanvas", { * @comp DebugCanvas * @sign public .debugFill([String fillStyle]) * @param fillStyle - The color the component will be filled with. Defaults to "red". Pass the boolean false to turn off filling. + * @example * ~~~ * var myEntity = Crafty.e("2D, Collision, SolidHitBox ").debugFill("purple") - *~~~ + * ~~~ */ debugFill: function (fillStyle) { if (typeof fillStyle === 'undefined') @@ -72,9 +73,10 @@ Crafty.c("DebugCanvas", { * @comp DebugCanvas * @sign public .debugStroke([String strokeStyle]) * @param strokeStyle - The color the component will be outlined with. Defaults to "red". Pass the boolean false to turn this off. + * @example * ~~~ * var myEntity = Crafty.e("2D, Collision, WiredHitBox ").debugStroke("white") - *~~~ + * ~~~ */ debugStroke: function (strokeStyle) { if (typeof strokeStyle === 'undefined') diff --git a/src/HashMap.js b/src/HashMap.js index 223933cd..d9d973e1 100644 --- a/src/HashMap.js +++ b/src/HashMap.js @@ -307,10 +307,8 @@ var Crafty = require('./core.js'), * @category 2D * Broad-phase collision detection engine. See background information at * - * ~~~ * - [N Tutorial B - Broad-Phase Collision](http://www.metanetsoftware.com/technique/tutorialB.html) * - [Broad-Phase Collision Detection with CUDA](http.developer.nvidia.com/GPUGems3/gpugems3_ch32.html) - * ~~~ * @see Crafty.map */ diff --git a/src/animation.js b/src/animation.js index 96a19a59..e115723f 100644 --- a/src/animation.js +++ b/src/animation.js @@ -14,13 +14,13 @@ Crafty.easing.prototype = { steps: null, complete: false, paused: false, - - // init values - reset: function(){ + + // init values + reset: function(){ this.loops = 1; this.clock = 0; this.complete = false; - this.paused = false; + this.paused = false; }, repeat: function(loopCount){ @@ -31,7 +31,7 @@ Crafty.easing.prototype = { this.clock = this.duration * progress; if (typeof loopCount !== "undefined") this.loops = loopCount; - + }, pause: function(){ @@ -61,7 +61,7 @@ Crafty.easing.prototype = { // same as value for now; with other time value functions would be more useful time: function(){ return ( Math.min(this.clock/this.duration, 1) ); - + }, // Value is where along the tweening curve we are @@ -89,14 +89,14 @@ Crafty.easing.prototype = { * Must be applied to an entity that has a sprite-map component. * * To define an animation, see the `reel` method. To play an animation, see the `animate` method. -* +* * A reel is an object that contains the animation frames and current state for an animation. The reel object has the following properties: * @param id: (String) - the name of the reel * @param frames: (Array) - A list of frames in the format [xpos, ypos] * @param currentFrame: (Number) - The index of the current frame -* @param easing: (Crafty.easing object) - The object that handles the internal progress of the animation. +* @param easing: (Crafty.easing object) - The object that handles the internal progress of the animation. * @param duration: (Number) - The duration in milliseconds. -* +* * Many animation related events pass a reel object as data. As typical with events, this should be treated as read only data that might be later altered by the entity. If you wish to preserve the data, make a copy of it. * * @see crafty.sprite @@ -119,7 +119,7 @@ Crafty.c("SpriteAnimation", { /* * The currently active reel. - * This value is `null` if no reel is active. + * This value is `null` if no reel is active. */ _currentReel: null, @@ -128,6 +128,14 @@ Crafty.c("SpriteAnimation", { */ _isPlaying: false, + /**@ + * #.animationSpeed + * @comp SpriteAnimation + * + * The playback rate of the animation. This property defaults to 1. + */ + animationSpeed: 1, + init: function () { this._reels = {}; @@ -174,10 +182,10 @@ Crafty.c("SpriteAnimation", { * PlayerSprite: [0,0] * }); * - * // Define an animation on the second row of the sprite map (y = 1) + * // Define an animation on the second row of the sprite map (fromY = 1) * // from the left most sprite (fromX = 0) to the fourth sprite - * // on that row (toX = 3), with a duration of 1 second - * Crafty.e("2D, DOM, SpriteAnimation, PlayerSprite").reel('PlayerRunning', 1000, 0, 1, 3); + * // on that row (frameCount = 4), with a duration of 1 second + * Crafty.e("2D, DOM, SpriteAnimation, PlayerSprite").reel('PlayerRunning', 1000, 0, 1, 4); * * // This is the same animation definition, but using the alternative method * Crafty.e("2D, DOM, SpriteAnimation, PlayerSprite").reel('PlayerRunning', 1000, [[0, 1], [1, 1], [2, 1], [3, 1]]); @@ -205,47 +213,36 @@ Crafty.c("SpriteAnimation", { } - var reel, i, tile, tileh, pos; - - // Get the dimensions of a single frame, as defind in Sprite component. - tile = this.__tile + parseInt(this.__padding[0] || 0, 10); - tileh = this.__tileh + parseInt(this.__padding[1] || 0, 10); + var reel, i; reel = { id: reelId, frames: [], currentFrame: 0, - easing: new Crafty.easing(duration), + easing: new Crafty.easing(duration), defaultLoops: 1 }; reel.duration = reel.easing.duration; - // @sign public this .reel(String reelId, Number duration, Number fromX, Number y, Number toX) + // @sign public this .reel(String reelId, Number duration, Number fromX, Number fromY, Number frameDuration) if (typeof fromX === "number") { i = fromX; y = fromY; if (frameCount >= 0) { for (; i < fromX + frameCount ; i++) { - reel.frames.push([i * tile, y * tileh]); + reel.frames.push([i, y]); } } else { for (; i > fromX + frameCount; i--) { - reel.frames.push([i * tile, y * tileh]); + reel.frames.push([i, y]); } } } // @sign public this .reel(String reelId, Number duration, Array frames) else if (arguments.length === 3 && typeof fromX === "object") { - - i = 0; - toX = fromX.length - 1; - - for (; i <= toX; i++) { - pos = fromX[i]; - reel.frames.push([pos[0] * tile, pos[1] * tileh]); - } + reel.frames = fromX; } else { throw "Urecognized arguments. Please see the documentation for 'reel(...)'."; @@ -265,7 +262,7 @@ Crafty.c("SpriteAnimation", { * * Play one of the reels previously defined through `.reel(...)`. Simply pass the name of the reel. If you wish the * animation to play multiple times in succession, pass in the amount of times as an additional parameter. - * To have the animation repeat indefinitely, pass in `-1`. + * To have the animation repeat indefinitely, pass in `-1`. * * If another animation is currently playing, it will be paused. * @@ -304,7 +301,7 @@ Crafty.c("SpriteAnimation", { this.pauseAnimation(); // This will pause the current animation, if one is playing // Handle repeats; if loopCount is undefined and reelID is a number, calling with that signature - if (typeof loopCount === "undefined") + if (typeof loopCount === "undefined") if (typeof reelId === "number") loopCount = reelId; else @@ -312,9 +309,9 @@ Crafty.c("SpriteAnimation", { // set the animation to the beginning currentReel.easing.reset(); - - // user provided loop count. + + // user provided loop count. this.loops(loopCount); // trigger the necessary events and switch to the first frame @@ -333,7 +330,7 @@ Crafty.c("SpriteAnimation", { * @comp SpriteAnimation * @sign public this .resumeAnimation() * - * This will resume animation of the current reel from its current state. + * This will resume animation of the current reel from its current state. * If a reel is already playing, or there is no current reel, there will be no effect. */ resumeAnimation: function() { @@ -342,8 +339,8 @@ Crafty.c("SpriteAnimation", { this._isPlaying = true; this._currentReel.easing.resume(); this.trigger("StartAnimation", this._currentReel); - } - return this; + } + return this; }, /**@ @@ -368,7 +365,7 @@ Crafty.c("SpriteAnimation", { * @sign public this .resetAnimation() * * Resets the current animation to its initial state. Resets the number of loops to the last specified value, which defaults to 1. - * + * * Neither pauses nor resumes the current animation. */ resetAnimation: function(){ @@ -387,7 +384,7 @@ Crafty.c("SpriteAnimation", { * @sign public this .loops(Number loopCount) * @param loopCount - The number of times to play the animation * - * Sets the number of times the animation will loop for. + * Sets the number of times the animation will loop for. * If called while an animation is in progress, the current state will be considered the first loop. * * @sign public Number .loops() @@ -425,10 +422,10 @@ Crafty.c("SpriteAnimation", { * * @sign public this .reelPosition(String position) * Jumps to the specified position. The only currently accepted value is "end", which will jump to the end of the reel. - * + * * @sign public Number .reelPosition() * @returns The current frame number - * + * */ reelPosition: function(position) { if (this._currentReel === null) @@ -447,7 +444,7 @@ Crafty.c("SpriteAnimation", { position = Math.floor(l * progress); } else { if (position !== Math.floor(position)) - throw("Position " + position + " is invalid."); + throw("Position " + position + " is invalid."); if (position < 0) position = l - 1 + position; progress = position / l; @@ -459,14 +456,15 @@ Crafty.c("SpriteAnimation", { this._setFrame(position); return this; - + }, - + // Bound to "EnterFrame". Progresses the animation by dt, changing the frame if necessary. + // dt is multiplied by the animationSpeed property _animationTick: function(frameData) { var currentReel = this._reels[this._currentReelId]; - currentReel.easing.tick(frameData.dt); + currentReel.easing.tick(frameData.dt * this.animationSpeed); var progress = currentReel.easing.value(); var frameNumber = Math.min( Math.floor(currentReel.frames.length * progress), currentReel.frames.length - 1); @@ -480,7 +478,7 @@ Crafty.c("SpriteAnimation", { - + // Set the current frame and update the displayed sprite // The actual progress for the animation must be set seperately. @@ -490,16 +488,14 @@ Crafty.c("SpriteAnimation", { return; currentReel.currentFrame = frameNumber; this._updateSprite(); - this.trigger("FrameChange", currentReel); + this.trigger("FrameChange", currentReel); }, // Update the displayed sprite. _updateSprite: function() { var currentReel = this._currentReel; var pos = currentReel.frames[currentReel.currentFrame]; - this.__coord[0] = pos[0]; - this.__coord[1] = pos[1]; - this.trigger("Change"); // needed to trigger a redraw + this.sprite(pos[0], pos[1]); // .sprite will trigger redraw }, @@ -539,9 +535,9 @@ Crafty.c("SpriteAnimation", { * @comp SpriteAnimation * @sign public Reel .getReel() * @returns The current reel, or null if there is no active reel - * + * * @sign public Reel .getReel(reelId) - * @param reelId - The id of the reel to fetch. + * @param reelId - The id of the reel to fetch. * @returns The specified reel, or `undefined` if no such reel exists. * */ @@ -624,7 +620,7 @@ Crafty.c("Tween", { * */ tween: function (props, duration) { - + var tween = { props: props, easing: new Crafty.easing(duration) @@ -641,7 +637,7 @@ Crafty.c("Tween", { this.tweenGroup[propname] = props; } this.tweens.push(tween); - + return this; }, @@ -651,11 +647,11 @@ Crafty.c("Tween", { * @comp Tween * @sign public this .cancelTween(String target) * @param target - The property to cancel - * + * * @sign public this .cancelTween(Object target) * @param target - An object containing the properties to cancel. * - * Stops tweening the specified property or properties. + * Stops tweening the specified property or properties. * Passing the object used to start the tween might be a typical use of the second signature. */ cancelTween: function(target){ @@ -668,7 +664,7 @@ Crafty.c("Tween", { } return this; - + }, /* diff --git a/src/canvas.js b/src/canvas.js index 1fabe253..8409f503 100644 --- a/src/canvas.js +++ b/src/canvas.js @@ -13,8 +13,9 @@ var Crafty = require('./core.js'), * * Create a canvas entity like this * ~~~ - * var myEntity = Crafty.e("2D, Canvas, Color").color("green") - * .attr({x: 13, y: 37, w: 42, h: 42}); + * var myEntity = Crafty.e("2D, Canvas, Color") + * .color("green") + * .attr({x: 13, y: 37, w: 42, h: 42}); *~~~ */ Crafty.c("Canvas", { @@ -31,7 +32,7 @@ Crafty.c("Canvas", { this._changed = true; Crafty.DrawManager.addCanvas(this); - this.bind("Change", function (e) { + this.bind("Invalidate", function (e) { //flag if changed if (this._changed === false) { this._changed = true; @@ -102,7 +103,7 @@ Crafty.c("Canvas", { co.w = w || coord[2]; co.h = h || coord[3]; - if (this._mbr) { + if (this._rotation !== 0) { context.save(); context.translate(this._origin.x + this._x, this._origin.y + this._y); @@ -134,7 +135,7 @@ Crafty.c("Canvas", { this.drawVars.ctx = context; this.trigger("Draw", this.drawVars); - if (this._mbr || (this._flipX || this._flipY)) { + if (this._rotation !== 0 || (this._flipX || this._flipY)) { context.restore(); } if (globalpha) { @@ -206,8 +207,17 @@ Crafty.extend({ Crafty.canvas.context.scale(zoom, zoom); //Bind rendering of canvas context (see drawing.js) - Crafty.unbind("RenderScene", Crafty.DrawManager.renderCanvas); - Crafty.bind("RenderScene", Crafty.DrawManager.renderCanvas); + Crafty.uniqueBind("RenderScene", Crafty.DrawManager.renderCanvas); + + Crafty.uniqueBind("ViewportResize", this._resize); + }, + + // Resize the canvas element to the current viewport + _resize: function() { + var c = Crafty.canvas._canvas; + c.width = Crafty.viewport.width; + c.height = Crafty.viewport.height; + } } diff --git a/src/collision.js b/src/collision.js index bf5aeb17..96ab306c 100644 --- a/src/collision.js +++ b/src/collision.js @@ -17,11 +17,17 @@ Crafty.c("Collision", { */ init: function () { this.requires("2D"); - var area = this._mbr || this; - this.collision(); }, + + // Run by Crafty when the component is removed + remove: function() { + this._cbr = null; + this.unbind("Resize", this._resizeMap); + this.unbind("Resize", this._checkBounds); + }, + /**@ * #.collision * @comp Collision @@ -43,6 +49,8 @@ Crafty.c("Collision", { * * If no parameter is passed, the x, y, w, h properties of the entity will be used, and the hitbox will be resized when the entity is. * + * If a hitbox is set that is outside of the bounds of the entity itself, there will be a small performance penalty as it is tracked separately. + * * @example * ~~~ * Crafty.e("2D, Collision").collision( @@ -55,18 +63,32 @@ Crafty.c("Collision", { * @see Crafty.polygon */ collision: function (poly) { + // Unbind anything bound to "Resize" this.unbind("Resize", this._resizeMap); + this.unbind("Resize", this._checkBounds); + + + if (!poly) { + // If no polygon is specified, then a polygon is created that matches the bounds of the entity + // It will be adjusted on a "Resize" event poly = new Crafty.polygon([0, 0], [this._w, 0], [this._w, this._h], [0, this._h]); this.bind("Resize", this._resizeMap); + this._cbr = null; + } else { + // Otherwise, we set the specified hitbox, converting from a list of arguments to a polygon if necessary + if (arguments.length > 1) { + //convert args to array to create polygon + var args = Array.prototype.slice.call(arguments, 0); + poly = new Crafty.polygon(args); + } + // Check to see if the polygon sits outside the entity, and set _cbr appropriately + // On resize, the new bounds will be checked if necessary + this._findBounds(poly.points); } - if (arguments.length > 1) { - //convert args to array to create polygon - var args = Array.prototype.slice.call(arguments, 0); - poly = new Crafty.polygon(args); - } + // If the entity is currently rotated, the points in the hitbox must also be rotated if (this.rotation) { poly.rotate({ cos: Math.cos(-this.rotation * DEG_TO_RAD), @@ -78,6 +100,7 @@ Crafty.c("Collision", { }); } + // Finally, assign the hitbox, and attach it to the "Collision" entity this.map = poly; this.attach(this.map); this.map.shift(this._x, this._y); @@ -86,7 +109,66 @@ Crafty.c("Collision", { }, - // Change the hitbox when a "Resize" event triggers. + // If the hitbox is set by hand, it might extend beyond the entity. + // In such a case, we need to track this separately. + // This function finds a (non-minimal) bounding circle around the hitbox. + // + // It uses a pretty naive algorithm to do so, for more complicated options see [wikipedia](http://en.wikipedia.org/wiki/Bounding_sphere). + _findBounds: function(points) { + var minX = Infinity, maxX = -Infinity, minY=Infinity, maxY=-Infinity; + var p; + + // Calculate the MBR of the points by finding the min/max x and y + for (var i=0; i maxX) + maxX = p[0]; + if (p[1] < minY) + minY = p[1]; + if (p[1] > maxY) + maxY = p[1]; + } + + // This describes a circle centered on the MBR of the points, with a diameter equal to its diagonal + // It will be used to find a rough bounding box round the points, even if they've been rotated + var cbr = { + cx: (minX + maxX) / 2, + cy: (minY + maxY) / 2, + r: Math.sqrt( (maxX - minX)*(maxX - minX) + (maxY - minY)*(maxY - minY))/2, + }; + + // We need to worry about resizing, but only if resizing could possibly change whether the hitbox is in or out of bounds + // Thus if the upper-left corner is out of bounds, then there's no need to recheck on resize + if (minX >= 0 && minY >= 0) { + this._checkBounds = function() { + if (this._cbr === null && this._w < maxX || this._h < maxY ){ + this._cbr = cbr; + this._calculateMBR(); + } else if (this._cbr) { + this._cbr = null; + this._calculateMBR(); + } + }; + this.bind("Resize", this._checkBounds); + } + + // If the hitbox is within the entity, _cbr is null + // Otherwise, set it, and immediately calculate the bounding box. + if (minX >= 0 && minY >= 0 && maxX <= this._w && maxY <= this._h){ + this._cbr = null; + return false; + } else { + this._cbr = cbr; + this._calculateMBR(); + return true; + } + + }, + + // The default behavior is to match the hitbox to the entity. + // This function will change the hitbox when a "Resize" event triggers. _resizeMap: function (e) { var dx, dy, rot = this.rotation * DEG_TO_RAD, @@ -142,7 +224,7 @@ Crafty.c("Collision", { * ~~~ * [{ * obj: [entity], - * type "MBR" or "SAT", + * type: "MBR" or "SAT", * overlap: [number] * }] * ~~~ @@ -152,7 +234,7 @@ Crafty.c("Collision", { * @see .onHit, 2D */ hit: function (comp) { - var area = this._mbr || this, + var area = this._cbr || this._mbr || this, results = Crafty.map.search(area, false), i = 0, l = results.length, @@ -167,7 +249,7 @@ Crafty.c("Collision", { for (; i < l; ++i) { obj = results[i]; - oarea = obj._mbr || obj; //use the mbr + oarea = obj._cbr || obj._mbr || obj; //use the mbr if (!obj) continue; id = obj[0]; diff --git a/src/controls.js b/src/controls.js index 4a76de01..67b0631c 100644 --- a/src/controls.js +++ b/src/controls.js @@ -55,9 +55,12 @@ Crafty.extend({ * * Notable properties of a MouseEvent e: * ~~~ - * e.clientX, e.clientY //(x,y) coordinates of mouse event in web browser screen space - * e.realX, e.realY //(x,y) coordinates of mouse event in world/viewport space - * e.mouseButton // Normalized mouse button according to Crafty.mouseButtons + * //(x,y) coordinates of mouse event in web browser screen space + * e.clientX, e.clientY + * //(x,y) coordinates of mouse event in world/viewport space + * e.realX, e.realY + * // Normalized mouse button according to Crafty.mouseButtons + * e.mouseButton * ~~~ * @see Crafty.touchDispatch */ @@ -181,7 +184,7 @@ Crafty.extend({ * * TouchEvents have a different structure then MouseEvents. * The relevant data lives in e.changedTouches[0]. - * To normalize TouchEvents we catch em and dispatch a mock MouseEvent instead. + * To normalize TouchEvents we catch them and dispatch a mock MouseEvent instead. * * @see Crafty.mouseDispatch */ @@ -228,8 +231,14 @@ Crafty.extend({ first.target.dispatchEvent(simulatedEvent); } - if (e.preventDefault) e.preventDefault(); - else e.returnValue = false; + //Don't prevent default actions if target node is input or textarea. + if (e.target && e.target.nodeName !== 'INPUT' && e.target.nodeName !== 'TEXTAREA') { + if (e.preventDefault) { + e.preventDefault(); + } else { + e.returnValue = false; + } + } }, @@ -246,14 +255,14 @@ Crafty.extend({ * .attr({x: 100, y: 100, w: 50, h: 50}) * .color("red") * .bind('KeyDown', function(e) { - * if(e.key == Crafty.keys['LEFT_ARROW']) { - * this.x=this.x-1; - * } else if (e.key == Crafty.keys['RIGHT_ARROW']) { - * this.x=this.x+1; - * } else if (e.key == Crafty.keys['UP_ARROW']) { - * this.y=this.y-1; - * } else if (e.key == Crafty.keys['DOWN_ARROW']) { - * this.y=this.y+1; + * if(e.key == Crafty.keys.LEFT_ARROW) { + * this.x = this.x-1; + * } else if (e.key == Crafty.keys.RIGHT_ARROW) { + * this.x = this.x+1; + * } else if (e.key == Crafty.keys.UP_ARROW) { + * this.y = this.y-1; + * } else if (e.key == Crafty.keys.DOWN_ARROW) { + * this.y = this.y+1; * } * }); * ~~~ @@ -372,11 +381,9 @@ Crafty.bind("CraftyStop", function () { * @trigger MouseMove - when the mouse is over the entity and moves - MouseEvent * Crafty adds the mouseButton property to MouseEvents that match one of * - * ~~~ * - Crafty.mouseButtons.LEFT * - Crafty.mouseButtons.RIGHT * - Crafty.mouseButtons.MIDDLE - * ~~~ * * @example * ~~~ @@ -865,17 +872,17 @@ Crafty.c("Fourway", { /**@ * #Twoway * @category Input + * @trigger NewDirection - When direction changes a NewDirection event is triggered with an object detailing the new direction: {x: x_movement, y: y_movement}. This is consistent with Fourway and Multiway components. + * @trigger Moved - When entity has moved on x-axis a Moved event is triggered with an object specifying the old position {x: old_x, y: old_y} + * * Move an entity left or right using the arrow keys or `D` and `A` and jump using up arrow or `W`. - * - * When direction changes a NewDirection event is triggered with an object detailing the new direction: {x: x_movement, y: y_movement}. This is consistent with Fourway and Multiway components. - * When entity has moved on x-axis a Moved event is triggered with an object specifying the old position {x: old_x, y: old_y} */ Crafty.c("Twoway", { _speed: 3, _up: false, init: function () { - this.requires("Fourway, Keyboard"); + this.requires("Fourway, Keyboard, Gravity"); }, /**@ @@ -887,10 +894,8 @@ Crafty.c("Twoway", { * * Constructor to initialize the speed and power of jump. Component will * listen for key events and move the entity appropriately. This includes - * ~~~ - * `Up Arrow`, `Right Arrow`, `Left Arrow` as well as W, A, D. Used with the + * `Up Arrow`, `Right Arrow`, `Left Arrow` as well as `W`, `A`, `D`. Used with the * `gravity` component to simulate jumping. - * ~~~ * * The key presses will move the entity in that direction by the speed passed in * the argument. Pressing the `Up Arrow` or `W` will cause the entity to jump. @@ -919,9 +924,10 @@ Crafty.c("Twoway", { if (this._up) { this.y -= this._jumpSpeed; this._falling = true; + this.trigger('Moved', { x: this._x, y: this._y + this._jumpSpeed }); } }).bind("KeyDown", function (e) { - if (e.key === Crafty.keys.UP_ARROW || e.key === Crafty.keys.W || e.key === Crafty.keys.Z) + if (!this._falling && (e.key === Crafty.keys.UP_ARROW || e.key === Crafty.keys.W || e.key === Crafty.keys.Z)) this._up = true; }); diff --git a/src/core.js b/src/core.js index f89b358c..bd6fe16d 100644 --- a/src/core.js +++ b/src/core.js @@ -27,18 +27,27 @@ var version = require('./version'); * Crafty(1) * ~~~ * Passing an integer will select the entity with that `ID`. + * + * To work directly with an array of entities, use the `get()` method on a selection. + * To call a function in the context of each entity, use the `.each()` method. + * + * The event related methods such as `bind` and `trigger` will work on selections of entities. + * + * @see .get + * @see .each */ - + var Crafty = function (selector) { return new Crafty.fn.init(selector); }, // Internal variables GUID, frame, components, entities, handlers, onloads, - noSetter, slice, rlist, rspace, milliSecPerFrame; + slice, rlist, rspace, milliSecPerFrame; initState = function () { - GUID = 1; //GUID for entity IDs + GUID = 1, //GUID for entity IDs + frame = 0; components = {}; //map of components and their functions entities = {}; //map of entities and their data @@ -452,7 +461,8 @@ Crafty.fn = Crafty.prototype = { * @comp Crafty Core * @sign public this .toArray(void) * - * This method will simply return the found entities as an array. + * This method will simply return the found entities as an array of ids. To get an array of the actual entities, use `get()`. + * @see .get */ toArray: function () { return slice.call(this, 0); @@ -464,7 +474,7 @@ Crafty.fn = Crafty.prototype = { * @sign public this .timeout(Function callback, Number delay) * @param callback - Method to execute after given amount of milliseconds * @param delay - Amount of milliseconds to execute the method - * + * * The delay method will execute a function after a given amount of time in milliseconds. * * Essentially a wrapper for `setTimeout`. @@ -711,6 +721,53 @@ Crafty.fn = Crafty.prototype = { return this; }, + /**@ + * #.get + * @comp Crafty Core + * @sign public Array .get() + * @returns An array of entities corresponding to the active selector + * + * @sign public Entity .get(Number index) + * @returns an entity belonging to the current selection + * @param index - The index of the entity to return. If negative, counts back from the end of the array. + * + * + * @example + * Get an array containing every "2D" entity + * ~~~ + * var arr = Crafty("2D").get() + * ~~~ + * Get the first entity matching the selector + * ~~~ + * // equivalent to Crafty("2D").get()[0], but doesn't create a new array + * var e = Crafty("2D").get(0) + * ~~~ + * Get the last "2D" entity matching the selector + * ~~~ + * var e = Crafty("2D").get(-1) + * ~~~ + * + */ + get: function(index) { + var l = this.length; + if (typeof index !== "undefined") { + if (index >= l || index+l < 0) + return undefined; + if (index>=0) + return entities[this[index]]; + else + return entities[this[index+l]]; + } else { + var i=0, result = []; + for (; i < l; i++) { + //skip if not exists + if (!entities[this[i]]) continue; + result.push( entities[this[i]] ); + } + return result; + } + }, + /**@ * #.clone * @comp Crafty Core @@ -747,8 +804,6 @@ Crafty.fn = Crafty.prototype = { * Will watch a property waiting for modification and will then invoke the * given callback when attempting to modify. * - * *Note: Support in IE<9 is slightly different. The method will be executed - * after the property has been set* */ setter: function (prop, callback) { if (Crafty.support.setter) { @@ -758,12 +813,6 @@ Crafty.fn = Crafty.prototype = { set: callback, configurable: true }); - } else { - noSetter.push({ - prop: prop, - obj: this, - fn: callback - }); } return this; }, @@ -795,9 +844,12 @@ Crafty.fn = Crafty.prototype = { //give the init instances the Crafty prototype Crafty.fn.init.prototype = Crafty.fn; -/** - * Extension method to extend the namespace and - * selector instances + +/**@ + * #Crafty.extend + * @category Core + * Used to extend the Crafty namespace. + * */ Crafty.extend = Crafty.fn.extend = function (obj) { var target = this, @@ -814,11 +866,7 @@ Crafty.extend = Crafty.fn.extend = function (obj) { return target; }; -/**@ - * #Crafty.extend - * @category Core - * Used to extend the Crafty namespace. - */ + Crafty.extend({ /**@ * #Crafty.init @@ -972,8 +1020,7 @@ Crafty.extend({ // variables used by the game loop to track state var endTime = 0, timeSlip = 0, - gameTime, - frame = 0; + gameTime; // Controls the target rate of fixed mode loop. Set these with the Crafty.timer.FPS function var FPS = 50, @@ -1029,8 +1076,8 @@ Crafty.extend({ /**@ * #Crafty.timer.steptype * @comp Crafty.timer - * Can be called to set the type of timestep the game loop uses * @sign public void Crafty.timer.steptype(mode [, maxTimeStep]) + * Can be called to set the type of timestep the game loop uses * @param mode - the type of time loop. Allowed values are "fixed", "semifixed", and "variable". Crafty defaults to "fixed". * @param mode - For "fixed", sets the max number of frames per step. For "variable" and "semifixed", sets the maximum time step allowed. * @@ -1155,8 +1202,8 @@ Crafty.extend({ /**@ * #Crafty.timer.simulateFrames * @comp Crafty.timer - * Advances the game state by a number of frames and draws the resulting stage at the end. Useful for tests and debugging. * @sign public this Crafty.timer.simulateFrames(Number frames[, Number timestep]) + * Advances the game state by a number of frames and draws the resulting stage at the end. Useful for tests and debugging. * @param frames - number of frames to simulate * @param timestep - the duration to pass each frame. Defaults to milliSecPerFrame (20 ms) if not specified. */ @@ -1222,7 +1269,7 @@ Crafty.extend({ * @category Core * @sign public void Crafty.c(String name, Object component) * @param name - Name of the component - * @param component - Object with the components properties and methods + * @param component - Object with the component's properties and methods * Creates a component where the first argument is the ID and the second * is the object that will be inherited by entities. * @@ -1319,7 +1366,7 @@ Crafty.extend({ * @sign public Number bind(String eventName, Function callback) * @param eventName - Name of the event to bind to * @param callback - Method to execute upon event triggered - * @returns ID of the current callback used to unbind + * @returns callback function which can be used for unbind * * Binds to a global event. Method will be executed when `Crafty.trigger` is used * with the event name. @@ -1339,7 +1386,7 @@ Crafty.extend({ // entity6.trigger('Move')), it causes the execution of fnB() and fnC(). When // the Move event is triggered globally (i.e. Crafty.trigger('Move')), it // will execute fnA, fnB, fnC, fnD. - // + // // In this example, "this" is bound to entity #6 whenever fnB() is executed, and // "this" is bound to Crafty whenever fnD() is executed. // @@ -1351,7 +1398,8 @@ Crafty.extend({ var hdl = handlers[event]; if (!hdl.global) hdl.global = []; - return hdl.global.push(callback) - 1; + hdl.global.push(callback); + return callback; }, @@ -1361,7 +1409,7 @@ Crafty.extend({ * @sign public Number uniqueBind(String eventName, Function callback) * @param eventName - Name of the event to bind to * @param callback - Method to execute upon event triggered - * @returns ID of the current callback used to unbind + * @returns callback function which can be used for unbind * * Works like Crafty.bind, but prevents a callback from being bound multiple times. * @@ -1369,8 +1417,7 @@ Crafty.extend({ */ uniqueBind: function (event, callback) { this.unbind(event, callback); - this.bind(event, callback); - + return this.bind(event, callback); }, /**@ @@ -1379,7 +1426,7 @@ Crafty.extend({ * @sign public Number one(String eventName, Function callback) * @param eventName - Name of the event to bind to * @param callback - Method to execute upon event triggered - * @returns ID of the current callback used to unbind + * @returns callback function which can be used for unbind * * Works like Crafty.bind, but will be unbound once the event triggers. * @@ -1392,7 +1439,6 @@ Crafty.extend({ self.unbind(event, oneHandler); }; return self.bind(event, oneHandler); - }, /**@ @@ -1573,24 +1619,6 @@ function clone(obj) { return temp; } -Crafty.bind("Load", function () { - if (!Crafty.support.setter && Crafty.support.defineProperty) { - noSetter = []; - Crafty.bind("EnterFrame", function () { - var i = 0, - l = noSetter.length, - current; - for (; i < l; ++i) { - current = noSetter[i]; - if (current.obj[current.prop] !== current.obj['_' + current.prop]) { - current.fn.call(current.obj, current.obj[current.prop]); - } - } - }); - } -}); - - // export Crafty if (typeof define === 'function') { // AMD define('crafty', [], function () { diff --git a/src/diamondiso.js b/src/diamondiso.js index 246c358f..29579f4d 100644 --- a/src/diamondiso.js +++ b/src/diamondiso.js @@ -107,7 +107,7 @@ Crafty.extend({ vp._w += (this._tile.width / 2 + ow); vp._h += (this._tile.height / 2 + oh); /* Crafty.viewport.x = -vp._x; - Crafty.viewport.y = -vp._y; + Crafty.viewport.y = -vp._y; Crafty.viewport.width = vp._w; Crafty.viewport.height = vp._h; */ diff --git a/src/drawing.js b/src/drawing.js index 9b045621..cc7b2276 100644 --- a/src/drawing.js +++ b/src/drawing.js @@ -25,25 +25,25 @@ Crafty.c("Color", { /**@ * #.color * @comp Color - * @trigger Change - when the color changes + * @trigger Invalidate - when the color changes * @sign public this .color(String color) * @sign public String .color() * @param color - Color of the rectangle * Will create a rectangle of solid color for the entity, or return the color if no argument is given. * * The argument must be a color readable depending on which browser you - * choose to support. IE 8 and below doesn't support the rgb() syntax. + * choose to support. * * @example - * ~~~ + * ``` * Crafty.e("2D, DOM, Color") * .color("#969696"); - * ~~~ + * ``` */ color: function (color) { if (!color) return this._color; this._color = color; - this.trigger("Change"); + this.trigger("Invalidate"); return this; } }); @@ -75,7 +75,7 @@ Crafty.c("Tint", { /**@ * #.tint * @comp Tint - * @trigger Change - when the tint is applied + * @trigger Invalidate - when the tint is applied * @sign public this .tint(String color, Number strength) * @param color - The color in hexadecimal * @param strength - Level of opacity @@ -92,7 +92,7 @@ Crafty.c("Tint", { this._strength = strength; this._color = Crafty.toRGB(color, this._strength); - this.trigger("Change"); + this.trigger("Invalidate"); return this; } }); @@ -121,8 +121,10 @@ Crafty.c("Image", { context.fillRect(0, 0, this._w, this._h); context.restore(); } else if (e.type === "DOM") { - if (this.__image) - e.style.background = "url(" + this.__image + ") " + this._repeat; + if (this.__image) { + e.style.backgroundImage = "url(" + this.__image + ")"; + e.style.backgroundRepeat = this._repeat; + } } }; @@ -134,7 +136,7 @@ Crafty.c("Image", { /**@ * #.image * @comp Image - * @trigger Change - when the image is loaded + * @trigger Invalidate - when the image is loaded * @sign public this .image(String url[, String repeat]) * @param url - URL of the image * @param repeat - If the image should be repeated to fill the entity. @@ -181,7 +183,7 @@ Crafty.c("Image", { self.h = self.img.height; } - self.trigger("Change"); + self.trigger("Invalidate"); }; return this; @@ -195,110 +197,13 @@ Crafty.c("Image", { } - this.trigger("Change"); + this.trigger("Invalidate"); return this; } }); Crafty.extend({ - _scenes: {}, - _current: null, - - /**@ - * #Crafty.scene - * @category Scenes, Stage - * @trigger SceneChange - just before a new scene is initialized - { oldScene:String, newScene:String } - * @trigger SceneDestroy - just before the current scene is destroyed - { newScene:String } - * @sign public void Crafty.scene(String sceneName, Function init[, Function uninit]) - * @param sceneName - Name of the scene to add - * @param init - Function to execute when scene is played - * @param uninit - Function to execute before next scene is played, after entities with `2D` are destroyed - * @sign public void Crafty.scene(String sceneName) - * @param sceneName - Name of scene to play - * - * Method to create scenes on the stage. Pass an ID and function to register a scene. - * - * To play a scene, just pass the ID. When a scene is played, all - * previously-created entities with the `2D` component are destroyed. The - * viewport is also reset. - * - * If you want some entities to persist over scenes (as in, not be destroyed) - * simply add the component `Persist`. - * - * @example - * ~~~ - * Crafty.scene("loading", function() { - * Crafty.background("#000"); - * Crafty.e("2D, DOM, Text") - * .attr({ w: 100, h: 20, x: 150, y: 120 }) - * .text("Loading") - * .css({ "text-align": "center"}) - * .textColor("#FFFFFF"); - * }); - * - * Crafty.scene("UFO_dance", - * function() {Crafty.background("#444"); Crafty.e("UFO");}, - * function() {...send message to server...}); - * ~~~ - * This defines (but does not play) two scenes as discussed below. - * ~~~ - * Crafty.scene("loading"); - * ~~~ - * This command will clear the stage by destroying all `2D` entities (except - * those with the `Persist` component). Then it will set the background to - * black and display the text "Loading". - * ~~~ - * Crafty.scene("UFO_dance"); - * ~~~ - * This command will clear the stage by destroying all `2D` entities (except - * those with the `Persist` component). Then it will set the background to - * gray and create a UFO entity. Finally, the next time the game encounters - * another command of the form `Crafty.scene(scene_name)` (if ever), then the - * game will send a message to the server. - */ - scene: function (name, intro, outro) { - // ---FYI--- - // this._current is the name (ID) of the scene in progress. - // this._scenes is an object like the following: - // {'Opening scene': {'initialize': fnA, 'uninitialize': fnB}, - // 'Another scene': {'initialize': fnC, 'uninitialize': fnD}} - - // If there's one argument, play the scene - if (arguments.length === 1) { - Crafty.trigger("SceneDestroy", { - newScene: name - }); - Crafty.viewport.reset(); - - Crafty("2D").each(function () { - if (!this.has("Persist")) this.destroy(); - }); - // uninitialize previous scene - if (this._current !== null && 'uninitialize' in this._scenes[this._current]) { - this._scenes[this._current].uninitialize.call(this); - } - // initialize next scene - var oldScene = this._current; - this._current = name; - Crafty.trigger("SceneChange", { - oldScene: oldScene, - newScene: name - }); - this._scenes[name].initialize.call(this); - - return; - } - - // If there is more than one argument, add the scene information to _scenes - this._scenes[name] = {}; - this._scenes[name].initialize = intro; - if (typeof outro !== 'undefined') { - this._scenes[name].uninitialize = outro; - } - return; - }, - /**@ * #Crafty.toRGB * @category Graphics @@ -368,7 +273,7 @@ Crafty.DrawManager = (function () { target._y = Math.min(a._y, b._y); target._w -= target._x; target._h -= target._y; - + return target; }, @@ -521,10 +426,9 @@ Crafty.DrawManager = (function () { * @comp Crafty.DrawManager * @sign public Crafty.DrawManager.drawAll([Object rect]) * @param rect - a rectangular region {_x: x_val, _y: y_val, _w: w_val, _h: h_val} - * ~~~ + * * - If rect is omitted, redraw within the viewport * - If rect is provided, redraw within the rect - * ~~~ */ drawAll: function (rect) { rect = rect || Crafty.viewport.rect(); @@ -552,10 +456,9 @@ Crafty.DrawManager = (function () { * @comp Crafty.DrawManager * @sign public Crafty.DrawManager.boundingRect(set) * @param set - Undocumented - * ~~~ + * * - Calculate the common bounding rect of multiple canvas entities. * - Returns coords - * ~~~ */ boundingRect: function (set) { if (!set || !set.length) return; @@ -591,12 +494,11 @@ Crafty.DrawManager = (function () { * #Crafty.DrawManager.renderCanvas * @comp Crafty.DrawManager * @sign public Crafty.DrawManager.renderCanvas() - * ~~~ + * * - Triggered by the "RenderScene" event * - If the number of rects is over 60% of the total number of objects * do the naive method redrawing `Crafty.DrawManager.drawAll` * - Otherwise, clear the dirty regions, and redraw entities overlapping the dirty regions. - * ~~~ * * @see Canvas.draw */ @@ -615,7 +517,7 @@ Crafty.DrawManager = (function () { if (dirtyViewport) { var view = Crafty.viewport; - ctx.setTransform(view._scale, 0, 0, view._scale, view.x, view.y); + ctx.setTransform(view._scale, 0, 0, view._scale, view._x*view._scale, view._y*view._scale); } //if the amount of changed objects is over 60% of the total objects @@ -711,9 +613,8 @@ Crafty.DrawManager = (function () { * #Crafty.DrawManager.renderDOM * @comp Crafty.DrawManager * @sign public Crafty.DrawManager.renderDOM() - * ~~~ + * * When "RenderScene" is triggered, draws all DOM entities that have been flagged - * ~~~ * * @see DOM.draw */ @@ -724,8 +625,8 @@ Crafty.DrawManager = (function () { 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.left = view.x * view._scale + "px"; + style.top = view.y * view._scale + "px"; style.zIndex = 10; } @@ -747,3 +648,63 @@ Crafty.DrawManager = (function () { }; })(); + +Crafty.extend({ + /**@ + * #Crafty.pixelart + * @category Graphics + * @sign public void Crafty.pixelart(Boolean enabled) + * + * Sets the image smoothing for drawing images (for both DOM and Canvas). + * Setting this to true disables smoothing for images, which is the preferred + * way for drawing pixel art. Defaults to false. + * + * This feature is experimental and you should be careful with cross-browser compatibility. + * The best way to disable image smoothing is to use the Canvas render method and the Sprite component for drawing your entities. + * + * This method will have no effect for Canvas image smoothing if the canvas is not initialized yet. + * + * Note that Firefox_26 currently has a [bug](https://bugzilla.mozilla.org/show_bug.cgi?id=696630) + * which prevents disabling image smoothing for Canvas entities that use the Image component. Use the Sprite + * component instead. + * Note that Webkit (Chrome & Safari) currently has a bug [link1](http://code.google.com/p/chromium/issues/detail?id=134040) + * [link2](http://code.google.com/p/chromium/issues/detail?id=106662) that prevents disabling image smoothing + * for DOM entities. + * + * @example + * This is the preferred way to draw pixel art with the best cross-browser compatibility. + * ~~~ + * Crafty.canvas.init(); + * Crafty.pixelart(true); + * + * Crafty.sprite(imgWidth, imgHeight, "spriteMap.png", {sprite1:[0,0]}); + * Crafty.e("2D, Canvas, sprite1"); + * ~~~ + */ + pixelart: function(enabled) { + var context = Crafty.canvas.context; + if (context) { + context.imageSmoothingEnabled = !enabled; + context.mozImageSmoothingEnabled = !enabled; + context.webkitImageSmoothingEnabled = !enabled; + context.oImageSmoothingEnabled = !enabled; + context.msImageSmoothingEnabled = !enabled; + } + + var style = Crafty.stage.inner.style; + if (enabled) { + style[Crafty.DOM.camelize("image-rendering")] = "optimizeSpeed"; /* legacy */ + style[Crafty.DOM.camelize("image-rendering")] = "-moz-crisp-edges"; /* Firefox */ + style[Crafty.DOM.camelize("image-rendering")] = "-o-crisp-edges"; /* Opera */ + style[Crafty.DOM.camelize("image-rendering")] = "-webkit-optimize-contrast"; /* Webkit (Chrome & Safari) */ + style[Crafty.DOM.camelize("-ms-interpolation-mode")] = "nearest-neighbor"; /* IE */ + style[Crafty.DOM.camelize("image-rendering")] = "optimize-contrast"; /* CSS3 proposed */ + style[Crafty.DOM.camelize("image-rendering")] = "pixelated"; /* CSS4 proposed */ + style[Crafty.DOM.camelize("image-rendering")] = "crisp-edges"; /* CSS4 proposed */ + } else { + style[Crafty.DOM.camelize("image-rendering")] = "optimizeQuality"; /* legacy */ + style[Crafty.DOM.camelize("-ms-interpolation-mode")] = "bicubic"; /* IE */ + style[Crafty.DOM.camelize("image-rendering")] = "auto"; /* CSS3 */ + } + } +}); diff --git a/src/extensions.js b/src/extensions.js index 015fde2f..aabeece1 100644 --- a/src/extensions.js +++ b/src/extensions.js @@ -134,152 +134,8 @@ var Crafty = require('./core.js'), support.devicemotion = (typeof window.DeviceMotionEvent !== "undefined"); })(); -Crafty.extend({ - - zeroFill: function (number, width) { - width -= number.toString().length; - if (width > 0) - return new Array(width + (/\./.test(number) ? 2 : 1)).join('0') + number; - return number.toString(); - }, - - /**@ - * #Crafty.sprite - * @category Graphics - * @sign public this Crafty.sprite([Number tile, [Number tileh]], String url, Object map[, Number paddingX[, Number paddingY]]) - * @param tile - Tile size of the sprite map, defaults to 1 - * @param tileh - Height of the tile; if provided, tile is interpreted as the width - * @param url - URL of the sprite image - * @param map - Object where the key is what becomes a new component and the value points to a position on the sprite map - * @param paddingX - Horizontal space in between tiles. Defaults to 0. - * @param paddingY - Vertical space in between tiles. Defaults to paddingX. - * Generates components based on positions in a sprite image to be applied to entities. - * - * Accepts a tile size, URL and map for the name of the sprite and its position. - * - * The position must be an array containing the position of the sprite where index `0` - * is the `x` position, `1` is the `y` position and optionally `2` is the width and `3` - * is the height. If the sprite map has padding, pass the values for the `x` padding - * or `y` padding. If they are the same, just add one value. - * - * If the sprite image has no consistent tile size, `1` or no argument need be - * passed for tile size. - * - * Entities that add the generated components are also given the `2D` component, and - * a component called `Sprite`. - * - * @example - * ~~~ - * Crafty.sprite("imgs/spritemap6.png", {flower:[0,0,20,30]}); - * var flower_entity = Crafty.e("2D, DOM, flower"); - * ~~~ - * The first line creates a component called `flower` associated with the sub-image of - * spritemap6.png with top-left corner (0,0), width 20 pixels, and height 30 pixels. - * The second line creates an entity with that image. (Note: The `2D` is not really - * necessary here, because adding the `flower` component automatically also adds the - * `2D` component.) - * ~~~ - * Crafty.sprite(50, "imgs/spritemap6.png", {flower:[0,0], grass:[0,1,3,1]}); - * ~~~ - * In this case, the `flower` component is pixels 0 <= x < 50, 0 <= y < 50, and the - * `grass` component is pixels 0 <= x < 150, 50 <= y < 100. (The `3` means grass has a - * width of 3 tiles, i.e. 150 pixels.) - * ~~~ - * Crafty.sprite(50, 100, "imgs/spritemap6.png", {flower:[0,0], grass:[0,1]}, 10); - * ~~~ - * In this case, each tile is 50x100, and there is a spacing of 10 pixels between - * consecutive tiles. So `flower` is pixels 0 <= x < 50, 0 <= y < 100, and `grass` is - * pixels 0 <= x < 50, 110 <= y < 210. - * - * @see Sprite - */ - sprite: function (tile, tileh, url, map, paddingX, paddingY) { - var spriteName, temp, x, y, w, h, img; - - //if no tile value, default to 1. - //(if the first passed argument is a string, it must be the url.) - if (typeof tile === "string") { - paddingY = paddingX; - paddingX = map; - map = tileh; - url = tile; - tile = 1; - tileh = 1; - } - - if (typeof tileh == "string") { - paddingY = paddingX; - paddingX = map; - map = url; - url = tileh; - tileh = tile; - } - - //if no paddingY, use paddingX - if (!paddingY && paddingX) paddingY = paddingX; - paddingX = parseInt(paddingX || 0, 10); //just incase - paddingY = parseInt(paddingY || 0, 10); - - var markSpritesReady = function() { - this.ready = true; - this.trigger("Change"); - }; - - img = Crafty.asset(url); - if (!img) { - img = new Image(); - img.src = url; - Crafty.asset(url, img); - img.onload = function () { - //all components with this img are now ready - for (var spriteName in map) { - Crafty(spriteName).each(markSpritesReady); - } - }; - } - - var sharedSpriteInit = function() { - this.requires("2D, Sprite"); - this.__trim = [0, 0, 0, 0]; - this.__image = url; - this.__coord = [this.__coord[0], this.__coord[1], this.__coord[2], this.__coord[3]]; - this.__tile = tile; - this.__tileh = tileh; - this.__padding = [paddingX, paddingY]; - this.img = img; - - //draw now - if (this.img.complete && this.img.width > 0) { - this.ready = true; - this.trigger("Change"); - } - - //set the width and height to the sprite size - this.w = this.__coord[2]; - this.h = this.__coord[3]; - }; - - for (spriteName in map) { - if (!map.hasOwnProperty(spriteName)) continue; - - temp = map[spriteName]; - x = temp[0] * (tile + paddingX); - y = temp[1] * (tileh + paddingY); - w = temp[2] * tile || tile; - h = temp[3] * tileh || tileh; - - //generates sprite components for each tile in the map - Crafty.c(spriteName, { - ready: false, - __coord: [x, y, w, h], - - init: sharedSpriteInit - }); - } - - return this; - }, +Crafty.extend({ _events: {}, /**@ @@ -384,219 +240,5 @@ Crafty.extend({ */ background: function (style) { Crafty.stage.elem.style.background = style; - }, - - - - /**@ - * #Crafty.keys - * @category Input - * Object of key names and the corresponding key code. - * - * ~~~ - * BACKSPACE: 8, - * TAB: 9, - * ENTER: 13, - * PAUSE: 19, - * CAPS: 20, - * ESC: 27, - * SPACE: 32, - * PAGE_UP: 33, - * PAGE_DOWN: 34, - * END: 35, - * HOME: 36, - * LEFT_ARROW: 37, - * UP_ARROW: 38, - * RIGHT_ARROW: 39, - * DOWN_ARROW: 40, - * INSERT: 45, - * DELETE: 46, - * 0: 48, - * 1: 49, - * 2: 50, - * 3: 51, - * 4: 52, - * 5: 53, - * 6: 54, - * 7: 55, - * 8: 56, - * 9: 57, - * A: 65, - * B: 66, - * C: 67, - * D: 68, - * E: 69, - * F: 70, - * G: 71, - * H: 72, - * I: 73, - * J: 74, - * K: 75, - * L: 76, - * M: 77, - * N: 78, - * O: 79, - * P: 80, - * Q: 81, - * R: 82, - * S: 83, - * T: 84, - * U: 85, - * V: 86, - * W: 87, - * X: 88, - * Y: 89, - * Z: 90, - * NUMPAD_0: 96, - * NUMPAD_1: 97, - * NUMPAD_2: 98, - * NUMPAD_3: 99, - * NUMPAD_4: 100, - * NUMPAD_5: 101, - * NUMPAD_6: 102, - * NUMPAD_7: 103, - * NUMPAD_8: 104, - * NUMPAD_9: 105, - * MULTIPLY: 106, - * ADD: 107, - * SUBSTRACT: 109, - * DECIMAL: 110, - * DIVIDE: 111, - * F1: 112, - * F2: 113, - * F3: 114, - * F4: 115, - * F5: 116, - * F6: 117, - * F7: 118, - * F8: 119, - * F9: 120, - * F10: 121, - * F11: 122, - * F12: 123, - * SHIFT: 16, - * CTRL: 17, - * ALT: 18, - * PLUS: 187, - * COMMA: 188, - * MINUS: 189, - * PERIOD: 190, - * PULT_UP: 29460, - * PULT_DOWN: 29461, - * PULT_LEFT: 4, - * PULT_RIGHT': 5 - * ~~~ - */ - keys: { - 'BACKSPACE': 8, - 'TAB': 9, - 'ENTER': 13, - 'PAUSE': 19, - 'CAPS': 20, - 'ESC': 27, - 'SPACE': 32, - 'PAGE_UP': 33, - 'PAGE_DOWN': 34, - 'END': 35, - 'HOME': 36, - 'LEFT_ARROW': 37, - 'UP_ARROW': 38, - 'RIGHT_ARROW': 39, - 'DOWN_ARROW': 40, - 'INSERT': 45, - 'DELETE': 46, - '0': 48, - '1': 49, - '2': 50, - '3': 51, - '4': 52, - '5': 53, - '6': 54, - '7': 55, - '8': 56, - '9': 57, - 'A': 65, - 'B': 66, - 'C': 67, - 'D': 68, - 'E': 69, - 'F': 70, - 'G': 71, - 'H': 72, - 'I': 73, - 'J': 74, - 'K': 75, - 'L': 76, - 'M': 77, - 'N': 78, - 'O': 79, - 'P': 80, - 'Q': 81, - 'R': 82, - 'S': 83, - 'T': 84, - 'U': 85, - 'V': 86, - 'W': 87, - 'X': 88, - 'Y': 89, - 'Z': 90, - 'NUMPAD_0': 96, - 'NUMPAD_1': 97, - 'NUMPAD_2': 98, - 'NUMPAD_3': 99, - 'NUMPAD_4': 100, - 'NUMPAD_5': 101, - 'NUMPAD_6': 102, - 'NUMPAD_7': 103, - 'NUMPAD_8': 104, - 'NUMPAD_9': 105, - 'MULTIPLY': 106, - 'ADD': 107, - 'SUBSTRACT': 109, - 'DECIMAL': 110, - 'DIVIDE': 111, - 'F1': 112, - 'F2': 113, - 'F3': 114, - 'F4': 115, - 'F5': 116, - 'F6': 117, - 'F7': 118, - 'F8': 119, - 'F9': 120, - 'F10': 121, - 'F11': 122, - 'F12': 123, - 'SHIFT': 16, - 'CTRL': 17, - 'ALT': 18, - 'PLUS': 187, - 'COMMA': 188, - 'MINUS': 189, - 'PERIOD': 190, - 'PULT_UP': 29460, - 'PULT_DOWN': 29461, - 'PULT_LEFT': 4, - 'PULT_RIGHT': 5 - - }, - - /**@ - * #Crafty.mouseButtons - * @category Input - * Object of mouseButton names and the corresponding button ID. - * In all mouseEvents we add the e.mouseButton property with a value normalized to match e.button of modern webkit - * - * ~~~ - * LEFT: 0, - * MIDDLE: 1, - * RIGHT: 2 - * ~~~ - */ - mouseButtons: { - LEFT: 0, - MIDDLE: 1, - RIGHT: 2 } }); \ No newline at end of file diff --git a/src/html.js b/src/html.js index 842b898e..daadc349 100644 --- a/src/html.js +++ b/src/html.js @@ -26,7 +26,7 @@ Crafty.c("HTML", { * ~~~ * Crafty.e("HTML") * .attr({x:20, y:20, w:100, h:100}) - * .replace("Crafty.js"); + * .replace("Index"); * ~~~ */ replace: function (new_html) { @@ -48,7 +48,7 @@ Crafty.c("HTML", { * ~~~ * Crafty.e("HTML") * .attr({x:20, y:20, w:100, h:100}) - * .append("Crafty.js"); + * .append("Index"); * ~~~ */ append: function (new_html) { @@ -70,7 +70,7 @@ Crafty.c("HTML", { * ~~~ * Crafty.e("HTML") * .attr({x:20, y:20, w:100, h:100}) - * .prepend("Crafty.js"); + * .prepend("Index"); * ~~~ */ prepend: function (new_html) { diff --git a/src/isometric.js b/src/isometric.js index 13268014..b3c59199 100644 --- a/src/isometric.js +++ b/src/isometric.js @@ -111,7 +111,7 @@ Crafty.extend({ return { x: -Math.ceil(-left / this._tile.width - (top & 1) * 0.5), y: top / this._tile.height * 2 - }; + }; }, /**@ * #Crafty.isometric.centerAt diff --git a/src/keycodes.js b/src/keycodes.js new file mode 100644 index 00000000..7a731642 --- /dev/null +++ b/src/keycodes.js @@ -0,0 +1,216 @@ +var Crafty = require('./core.js'), + document = window.document; + +Crafty.extend({ + /**@ + * #Crafty.keys + * @category Input + * Object of key names and the corresponding key code. + * + * ~~~ + * BACKSPACE: 8, + * TAB: 9, + * ENTER: 13, + * PAUSE: 19, + * CAPS: 20, + * ESC: 27, + * SPACE: 32, + * PAGE_UP: 33, + * PAGE_DOWN: 34, + * END: 35, + * HOME: 36, + * LEFT_ARROW: 37, + * UP_ARROW: 38, + * RIGHT_ARROW: 39, + * DOWN_ARROW: 40, + * INSERT: 45, + * DELETE: 46, + * 0: 48, + * 1: 49, + * 2: 50, + * 3: 51, + * 4: 52, + * 5: 53, + * 6: 54, + * 7: 55, + * 8: 56, + * 9: 57, + * A: 65, + * B: 66, + * C: 67, + * D: 68, + * E: 69, + * F: 70, + * G: 71, + * H: 72, + * I: 73, + * J: 74, + * K: 75, + * L: 76, + * M: 77, + * N: 78, + * O: 79, + * P: 80, + * Q: 81, + * R: 82, + * S: 83, + * T: 84, + * U: 85, + * V: 86, + * W: 87, + * X: 88, + * Y: 89, + * Z: 90, + * NUMPAD_0: 96, + * NUMPAD_1: 97, + * NUMPAD_2: 98, + * NUMPAD_3: 99, + * NUMPAD_4: 100, + * NUMPAD_5: 101, + * NUMPAD_6: 102, + * NUMPAD_7: 103, + * NUMPAD_8: 104, + * NUMPAD_9: 105, + * MULTIPLY: 106, + * ADD: 107, + * SUBSTRACT: 109, + * DECIMAL: 110, + * DIVIDE: 111, + * F1: 112, + * F2: 113, + * F3: 114, + * F4: 115, + * F5: 116, + * F6: 117, + * F7: 118, + * F8: 119, + * F9: 120, + * F10: 121, + * F11: 122, + * F12: 123, + * SHIFT: 16, + * CTRL: 17, + * ALT: 18, + * PLUS: 187, + * COMMA: 188, + * MINUS: 189, + * PERIOD: 190, + * PULT_UP: 29460, + * PULT_DOWN: 29461, + * PULT_LEFT: 4, + * PULT_RIGHT': 5 + * ~~~ + */ + keys: { + 'BACKSPACE': 8, + 'TAB': 9, + 'ENTER': 13, + 'PAUSE': 19, + 'CAPS': 20, + 'ESC': 27, + 'SPACE': 32, + 'PAGE_UP': 33, + 'PAGE_DOWN': 34, + 'END': 35, + 'HOME': 36, + 'LEFT_ARROW': 37, + 'UP_ARROW': 38, + 'RIGHT_ARROW': 39, + 'DOWN_ARROW': 40, + 'INSERT': 45, + 'DELETE': 46, + '0': 48, + '1': 49, + '2': 50, + '3': 51, + '4': 52, + '5': 53, + '6': 54, + '7': 55, + '8': 56, + '9': 57, + 'A': 65, + 'B': 66, + 'C': 67, + 'D': 68, + 'E': 69, + 'F': 70, + 'G': 71, + 'H': 72, + 'I': 73, + 'J': 74, + 'K': 75, + 'L': 76, + 'M': 77, + 'N': 78, + 'O': 79, + 'P': 80, + 'Q': 81, + 'R': 82, + 'S': 83, + 'T': 84, + 'U': 85, + 'V': 86, + 'W': 87, + 'X': 88, + 'Y': 89, + 'Z': 90, + 'NUMPAD_0': 96, + 'NUMPAD_1': 97, + 'NUMPAD_2': 98, + 'NUMPAD_3': 99, + 'NUMPAD_4': 100, + 'NUMPAD_5': 101, + 'NUMPAD_6': 102, + 'NUMPAD_7': 103, + 'NUMPAD_8': 104, + 'NUMPAD_9': 105, + 'MULTIPLY': 106, + 'ADD': 107, + 'SUBSTRACT': 109, + 'DECIMAL': 110, + 'DIVIDE': 111, + 'F1': 112, + 'F2': 113, + 'F3': 114, + 'F4': 115, + 'F5': 116, + 'F6': 117, + 'F7': 118, + 'F8': 119, + 'F9': 120, + 'F10': 121, + 'F11': 122, + 'F12': 123, + 'SHIFT': 16, + 'CTRL': 17, + 'ALT': 18, + 'PLUS': 187, + 'COMMA': 188, + 'MINUS': 189, + 'PERIOD': 190, + 'PULT_UP': 29460, + 'PULT_DOWN': 29461, + 'PULT_LEFT': 4, + 'PULT_RIGHT': 5 + + }, + + /**@ + * #Crafty.mouseButtons + * @category Input + * An object mapping mouseButton names to the corresponding button ID. + * In all mouseEvents, we add the `e.mouseButton` property with a value normalized to match e.button of modern webkit browsers: + * + * ~~~ + * LEFT: 0, + * MIDDLE: 1, + * RIGHT: 2 + * ~~~ + */ + mouseButtons: { + LEFT: 0, + MIDDLE: 1, + RIGHT: 2 + } +}); \ No newline at end of file diff --git a/src/scenes.js b/src/scenes.js new file mode 100644 index 00000000..661d4c9e --- /dev/null +++ b/src/scenes.js @@ -0,0 +1,162 @@ +var Crafty = require('./core.js'), + document = window.document; + +Crafty.extend({ + _scenes: {}, + _current: null, + + /**@ + * #Crafty.scene + * @category Scenes, Stage + * @trigger SceneChange - just before a new scene is initialized - { oldScene:String, newScene:String } + * @trigger SceneDestroy - just before the current scene is destroyed - { newScene:String } + * + * @sign public void Crafty.scene(String sceneName, Function init[, Function uninit]) + * @param sceneName - Name of the scene to add + * @param init - Function to execute when scene is played + * @param uninit - Function to execute before next scene is played, after entities with `2D` are destroyed + * This is equivalent to calling `Crafty.defineScene`. + * + * @sign public void Crafty.scene(String sceneName[, Data]) + * @param sceneName - Name of scene to play + * @param Data - The init function of the scene will be called with this data as its parameter. Can be of any type other than a function. + * This is equivalent to calling `Crafty.enterScene`. + * + * Method to create scenes on the stage. Pass an ID and function to register a scene. + * + * To play a scene, just pass the ID. When a scene is played, all + * previously-created entities with the `2D` component are destroyed. The + * viewport is also reset. + * + * You can optionally specify an arugment that will be passed to the scene's init function. + * + * If you want some entities to persist over scenes (as in, not be destroyed) + * simply add the component `Persist`. + * + * @example + * ~~~ + * Crafty.defineScene("loading", function() { + * Crafty.background("#000"); + * Crafty.e("2D, DOM, Text") + * .attr({ w: 100, h: 20, x: 150, y: 120 }) + * .text("Loading") + * .css({ "text-align": "center"}) + * .textColor("#FFFFFF"); + * }); + * + * Crafty.defineScene("UFO_dance", + * function() {Crafty.background("#444"); Crafty.e("UFO");}, + * function() {...send message to server...}); + * + * // An example of an init function which accepts arguments, in this case an object. + * Crafty.defineScene("square", function(attributes) { + * Crafty.background("#000"); + * Crafty.e("2D, DOM, Color") + * .attr(attributes) + * .color("red"); + * + * }); + * + * ~~~ + * This defines (but does not play) two scenes as discussed below. + * ~~~ + * Crafty.enterScene("loading"); + * ~~~ + * This command will clear the stage by destroying all `2D` entities (except + * those with the `Persist` component). Then it will set the background to + * black and display the text "Loading". + * ~~~ + * Crafty.enterScene("UFO_dance"); + * ~~~ + * This command will clear the stage by destroying all `2D` entities (except + * those with the `Persist` component). Then it will set the background to + * gray and create a UFO entity. Finally, the next time the game encounters + * another command of the form `Crafty.scene(scene_name)` (if ever), then the + * game will send a message to the server. + * ~~~ + * Crafty.enterScene("square", {x:10, y:10, w:20, h:20}); + * ~~~ + * This will clear the stage, set the background black, and create a red square with the specified position and dimensions. + * ~~~ + */ + scene: function (name, intro, outro) { + // If there's one argument, or the second argument isn't a function, play the scene + if (arguments.length === 1 || typeof(arguments[1]) !== "function") { + Crafty.enterScene(name, arguments[1]); + return; + } + // Otherwise, this is a call to create a scene + Crafty.defineScene(name, intro, outro); + }, + + /* + * #Crafty.defineScene + * @category Scenes, Stage + * + * @sign public void Crafty.enterScene(String name[, Data]) + * @param name - Name of the scene to run. + * @param Data - The init function of the scene will be called with this data as its parameter. Can be of any type other than a function. + * + * @see Crafty.enterScene + * @see Crafty.scene + */ + defineScene: function(name, init, uninit){ + if (typeof init !== "function") + throw("Init function is the wrong type."); + this._scenes[name] = {}; + this._scenes[name].initialize = init; + if (typeof uninit !== 'undefined') { + this._scenes[name].uninitialize = uninit; + } + return; + + }, + + /* + * #Crafty.enterScene + * @category Scenes, Stage + * @trigger SceneChange - just before a new scene is initialized - { oldScene:String, newScene:String } + * @trigger SceneDestroy - just before the current scene is destroyed - { newScene:String } + * + * @sign public void Crafty.enterScene(String name[, Data]) + * @param name - Name of the scene to run. + * @param Data - The init function of the scene will be called with this data as its parameter. Can be of any type other than a function. + * + * @see Crafty.defineScene + * @see Crafty.scene + */ + enterScene: function(name, data){ + if (typeof data === "function") + throw("Scene data cannot be a function"); + + // ---FYI--- + // this._current is the name (ID) of the scene in progress. + // this._scenes is an object like the following: + // {'Opening scene': {'initialize': fnA, 'uninitialize': fnB}, + // 'Another scene': {'initialize': fnC, 'uninitialize': fnD}} + + Crafty.trigger("SceneDestroy", { + newScene: name + }); + Crafty.viewport.reset(); + + Crafty("2D").each(function () { + if (!this.has("Persist")) this.destroy(); + }); + // uninitialize previous scene + if (this._current !== null && 'uninitialize' in this._scenes[this._current]) { + this._scenes[this._current].uninitialize.call(this); + } + // initialize next scene + var oldScene = this._current; + this._current = name; + Crafty.trigger("SceneChange", { + oldScene: oldScene, + newScene: name + }); + this._scenes[name].initialize.call(this, data); + + return; + + } +}); \ No newline at end of file diff --git a/src/sound.js b/src/sound.js index 7e84a9a3..e01ec945 100644 --- a/src/sound.js +++ b/src/sound.js @@ -436,6 +436,7 @@ Crafty.extend({ * #Crafty.audio.pause * @comp Crafty.audio * @sign public this Crafty.audio.pause(string ID) + * @param {string} id - The id of the audio object to pause * * Pause the Audio instance specified by id param. * @@ -444,7 +445,6 @@ Crafty.extend({ * Crafty.audio.pause('music'); * ~~~ * - * @param {string} id The id of the audio object to pause */ pause: function (id) { if (!Crafty.support.audio || !id || !this.sounds[id]) @@ -462,6 +462,7 @@ Crafty.extend({ * #Crafty.audio.unpause * @comp Crafty.audio * @sign public this Crafty.audio.unpause(string ID) + * @param {string} id - The id of the audio object to unpause * * Resume playing the Audio instance specified by id param. * @@ -470,7 +471,6 @@ Crafty.extend({ * Crafty.audio.unpause('music'); * ~~~ * - * @param {string} id The id of the audio object to unpause */ unpause: function (id) { if (!Crafty.support.audio || !id || !this.sounds[id]) @@ -487,6 +487,7 @@ Crafty.extend({ * #Crafty.audio.togglePause * @comp Crafty.audio * @sign public this Crafty.audio.togglePause(string ID) + * @param {string} id - The id of the audio object to pause/ * * Toggle the pause status of the Audio instance specified by id param. * @@ -495,7 +496,6 @@ Crafty.extend({ * Crafty.audio.togglePause('music'); * ~~~ * - * @param {string} id The id of the audio object to pause/unpause */ togglePause: function (id) { if (!Crafty.support.audio || !id || !this.sounds[id]) diff --git a/src/sprite.js b/src/sprite.js index 1e8576bb..dba19be8 100644 --- a/src/sprite.js +++ b/src/sprite.js @@ -1,10 +1,149 @@ var Crafty = require('./core.js'), document = window.document; +Crafty.extend({ + + /**@ + * #Crafty.sprite + * @category Graphics + * @sign public this Crafty.sprite([Number tile, [Number tileh]], String url, Object map[, Number paddingX[, Number paddingY[, Boolean paddingAroundBorder]]]) + * @param tile - Tile size of the sprite map, defaults to 1 + * @param tileh - Height of the tile; if provided, tile is interpreted as the width + * @param url - URL of the sprite image + * @param map - Object where the key is what becomes a new component and the value points to a position on the sprite map + * @param paddingX - Horizontal space in between tiles. Defaults to 0. + * @param paddingY - Vertical space in between tiles. Defaults to paddingX. + * @param paddingAroundBorder - If padding should be applied around the border of the sprite sheet. If enabled the first tile starts at (paddingX,paddingY) instead of (0,0). Defaults to false. + * Generates components based on positions in a sprite image to be applied to entities. + * + * Accepts a tile size, URL and map for the name of the sprite and its position. + * + * The position must be an array containing the position of the sprite where index `0` + * is the `x` position, `1` is the `y` position and optionally `2` is the width and `3` + * is the height. If the sprite map has padding, pass the values for the `x` padding + * or `y` padding. If they are the same, just add one value. + * + * If the sprite image has no consistent tile size, `1` or no argument need be + * passed for tile size. + * + * Entities that add the generated components are also given the `2D` component, and + * a component called `Sprite`. + * + * @example + * ~~~ + * Crafty.sprite("imgs/spritemap6.png", {flower:[0,0,20,30]}); + * var flower_entity = Crafty.e("2D, DOM, flower"); + * ~~~ + * The first line creates a component called `flower` associated with the sub-image of + * spritemap6.png with top-left corner (0,0), width 20 pixels, and height 30 pixels. + * The second line creates an entity with that image. (Note: The `2D` is not really + * necessary here, because adding the `flower` component automatically also adds the + * `2D` component.) + * ~~~ + * Crafty.sprite(50, "imgs/spritemap6.png", {flower:[0,0], grass:[0,1,3,1]}); + * ~~~ + * In this case, the `flower` component is pixels 0 <= x < 50, 0 <= y < 50, and the + * `grass` component is pixels 0 <= x < 150, 50 <= y < 100. (The `3` means grass has a + * width of 3 tiles, i.e. 150 pixels.) + * ~~~ + * Crafty.sprite(50, 100, "imgs/spritemap6.png", {flower:[0,0], grass:[0,1]}, 10); + * ~~~ + * In this case, each tile is 50x100, and there is a spacing of 10 pixels between + * consecutive tiles. So `flower` is pixels 0 <= x < 50, 0 <= y < 100, and `grass` is + * pixels 0 <= x < 50, 110 <= y < 210. + * + * @see Sprite + */ + sprite: function (tile, tileh, url, map, paddingX, paddingY, paddingAroundBorder) { + var spriteName, temp, x, y, w, h, img; + + //if no tile value, default to 1. + //(if the first passed argument is a string, it must be the url.) + if (typeof tile === "string") { + paddingY = paddingX; + paddingX = map; + map = tileh; + url = tile; + tile = 1; + tileh = 1; + } + + if (typeof tileh == "string") { + paddingY = paddingX; + paddingX = map; + map = url; + url = tileh; + tileh = tile; + } + + //if no paddingY, use paddingX + if (!paddingY && paddingX) paddingY = paddingX; + paddingX = parseInt(paddingX || 0, 10); //just incase + paddingY = parseInt(paddingY || 0, 10); + + var markSpritesReady = function() { + this.ready = true; + this.trigger("Invalidate"); + }; + + img = Crafty.asset(url); + if (!img) { + img = new Image(); + img.src = url; + Crafty.asset(url, img); + img.onload = function () { + //all components with this img are now ready + for (var spriteName in map) { + Crafty(spriteName).each(markSpritesReady); + } + }; + } + + var sharedSpriteInit = function() { + this.requires("2D, Sprite"); + this.__trim = [0, 0, 0, 0]; + this.__image = url; + this.__coord = [this.__coord[0], this.__coord[1], this.__coord[2], this.__coord[3]]; + this.__tile = tile; + this.__tileh = tileh; + this.__padding = [paddingX, paddingY]; + this.__padBorder = paddingAroundBorder; + this.sprite(this.__coord[0], this.__coord[1], this.__coord[2], this.__coord[3]); + + this.img = img; + //draw now + if (this.img.complete && this.img.width > 0) { + this.ready = true; + this.trigger("Invalidate"); + } + + //set the width and height to the sprite size + this.w = this.__coord[2]; + this.h = this.__coord[3]; + }; + + for (spriteName in map) { + if (!map.hasOwnProperty(spriteName)) continue; + + temp = map[spriteName]; + + //generates sprite components for each tile in the map + Crafty.c(spriteName, { + ready: false, + __coord: [temp[0], temp[1], temp[2] || 1, temp[3] || 1], + + init: sharedSpriteInit + }); + } + + return this; + } +}); + /**@ * #Sprite * @category Graphics - * @trigger Change - when the sprites change + * @trigger Invalidate - when the sprites change * Component for using tiles in a sprite map. */ Crafty.c("Sprite", { @@ -56,7 +195,8 @@ Crafty.c("Sprite", { hscale = this._w / co.w, style = this._element.style; - style.background = style.backgroundColor + " url('" + this.__image + "') no-repeat -" + co.x * hscale + "px -" + co.y * vscale + "px"; + style.background = style.backgroundColor + " url('" + this.__image + "') no-repeat"; + style.backgroundPosition = "-" + co.x * hscale + "px -" + co.y * vscale + "px"; // style.backgroundSize must be set AFTER style.background! if (vscale != 1 || hscale != 1) { style.backgroundSize = (this.img.width * hscale) + "px" + " " + (this.img.height * vscale) + "px"; @@ -72,13 +212,13 @@ Crafty.c("Sprite", { /**@ * #.sprite * @comp Sprite - * @sign public this .sprite(Number x, Number y, Number w, Number h) + * @sign public this .sprite(Number x, Number y[, Number w, Number h]) * @param x - X cell position * @param y - Y cell position - * @param w - Width in cells - * @param h - Height in cells + * @param w - Width in cells. Optional. + * @param h - Height in cells. Optional. * - * Uses a new location on the sprite map as its sprite. + * Uses a new location on the sprite map as its sprite. If w or h are ommitted, the width and height are not changed. * * Values should be in tiles or cells (not pixels). * @@ -96,13 +236,16 @@ Crafty.c("Sprite", { * The coordinate of the slide within the sprite in the format of [x, y, w, h]. */ sprite: function (x, y, w, h) { - this.__coord = [x * (this.__tile + this.__padding[0]) + this.__trim[0], - y * (this.__tileh + this.__padding[1]) + this.__trim[1], - this.__trim[2] || w * this.__tile || this.__tile, - this.__trim[3] || h * this.__tileh || this.__tileh - ]; + this.__coord = this.__coord || [0, 0, 0, 0]; + + this.__coord[0] = x * (this.__tile + this.__padding[0]) + (this.__padBorder ? this.__padding[0] : 0) + this.__trim[0]; + this.__coord[1] = y * (this.__tileh + this.__padding[1]) + (this.__padBorder ? this.__padding[1] : 0) + this.__trim[1]; + if (typeof(w)!=='undefined' && typeof(h)!=='undefined') { + this.__coord[2] = this.__trim[2] || w * this.__tile || this.__tile; + this.__coord[3] = this.__trim[3] || h * this.__tileh || this.__tileh; + } - this.trigger("Change"); + this.trigger("Invalidate"); return this; }, @@ -140,7 +283,7 @@ Crafty.c("Sprite", { this._w = w; this._h = h; - this.trigger("Change", old); + this.trigger("Invalidate", old); return this; } }); \ No newline at end of file diff --git a/src/storage.js b/src/storage.js index 563d4eb8..66962158 100644 --- a/src/storage.js +++ b/src/storage.js @@ -1,615 +1,94 @@ var Crafty = require('./core.js'), document = window.document; -// Directive for jshint to ignore evals -// eval is used to support IE8, so this can be removed if we decide to drop that -/* jshint evil:true */ - /**@ * #Storage * @category Utilities - * Utility to allow data to be saved to a permanent storage solution: IndexedDB, WebSql, localstorage or cookies + * Very simple way to get and set values, which will persist when the browser is closed also. */ /**@ - * #.open + * #.storage * @comp Storage - * @sign .open(String gameName) - * @param gameName - a machine readable string to uniquely identify your game - * - * Opens a connection to the database. If the best they have is localstorage or lower, it does nothing + * @sign .storage(String key) + * @param key - a key you would like to get from the storage. It will return null if the key does not exists. + * @sign .storage(String key, String value) + * @param key - the key you would like to save the data under. + * @param value - the value you would like to save. + * @sign .storage(String key, [Object value, Array value, Boolean value]) + * @param key - the key you would like to save the data under. + * @param value - the value you would like to save, can be an Object or an Array. * - * @example - * Open a database - * ~~~ - * Crafty.storage.open('MyGame'); - * ~~~ - */ -/**@ - * #.save - * @comp Storage - * @sign .save(String key, String type, Mixed data) - * @param key - A unique key for identifying this piece of data - * @param type - 'save' or 'cache' - * @param data - Some kind of data. + * Storage function is very simple and can be used to either get or set values. + * You can store both booleans, strings, objects and arrays. * - * Saves a piece of data to the database. Can be anything, although entities are preferred. - * For all storage methods but IndexedDB, the data will be serialized as a string - * During serialization, an entity's SaveData event will be triggered. - * Components should implement a SaveData handler and attach the necessary information to the passed object + * Please note: You should not store data, while the game is playing, as it can cause the game to slow down. You should load data when you start the game, or when the user for an example click a "Save gameprocess" button. * * @example - * Saves an entity to the database + * Get an already stored value * ~~~ - * var ent = Crafty.e("2D, DOM") - * .attr({x: 20, y: 20, w: 100, h:100}); - * Crafty.storage.open('MyGame'); - * Crafty.storage.save('MyEntity', 'save', ent); + * var playername = Crafty.storage('playername'); * ~~~ - */ -/**@ - * #.load - * @comp Storage - * @sign .load(String key, String type) - * @param key - A unique key to search for - * @param type - 'save' or 'cache' - * @param callback - Do things with the data you get back - * - * Loads a piece of data from the database. - * Entities will be reconstructed from the serialized string - - * @example - * Loads an entity from the database - * ~~~ - * Crafty.storage.open('MyGame'); - * Crafty.storage.load('MyEntity', 'save', function (data) { // do things }); - * ~~~ - */ -/**@ - * #.getAllKeys - * @comp Storage - * @sign .getAllKeys(String type) - * @param type - 'save' or 'cache' - * Gets all the keys for a given type - - * @example - * Gets all the save games saved - * ~~~ - * Crafty.storage.open('MyGame'); - * var saves = Crafty.storage.getAllKeys('save'); - * ~~~ - */ -/**@ - * #.external - * @comp Storage - * @sign .external(String url) - * @param url - URL to an external to save games too - * - * Enables and sets the url for saving games to an external server * * @example - * Save an entity to an external server + * Save a value * ~~~ - * Crafty.storage.external('http://somewhere.com/server.php'); - * Crafty.storage.open('MyGame'); - * var ent = Crafty.e('2D, DOM') - * .attr({x: 20, y: 20, w: 100, h:100}); - * Crafty.storage.save('save01', 'save', ent); + * Crafty.storage('playername', 'Hero'); * ~~~ - */ -/**@ - * #SaveData event - * @comp Storage - * @param data - An object containing all of the data to be serialized - * @param prepare - The function to prepare an entity for serialization - * - * Any data a component wants to save when it's serialized should be added to this object. - * Straight attribute should be set in data.attr. - * Anything that requires a special handler should be set in a unique property. * * @example - * Saves the innerHTML of an entity + * Test to see if a value is already there. * ~~~ - * Crafty.e("2D DOM").bind("SaveData", function (data, prepare) { - * data.attr.x = this.x; - * data.attr.y = this.y; - * data.dom = this.element.innerHTML; - * }); + * var heroname = Crafty.storage('name'); + * if(!heroname){ + * // Maybe ask the player what their name is here + * heroname = 'Guest'; + * } + * // Do something with heroname * ~~~ */ -/**@ - * #LoadData event - * @comp Storage - * @param data - An object containing all the data that been saved - * @param process - The function to turn a string into an entity - * - * Handlers for processing any data that needs more than straight assignment - * - * Note that data stored in the .attr object is automatically added to the entity. - * It does not need to be handled here - * - * @example - * ~~~ - * Sets the innerHTML from a saved entity - * Crafty.e("2D DOM").bind("LoadData", function (data, process) { - * this.element.innerHTML = data.dom; - * }); - * ~~~ - */ -Crafty.storage = (function () { - var db = null, - url, gameName, timestamps = {}, - transactionType = { - READ: "readonly", - READ_WRITE: "readwrite" - }; - - /* - * Processes a retrieved object. - * Creates an entity if it is one - */ - - function process(obj) { - if (obj.c) { - var d = Crafty.e(obj.c) - .attr(obj.attr) - .trigger('LoadData', obj, process); - return d; - } else if (typeof obj == 'object') { - for (var prop in obj) { - obj[prop] = process(obj[prop]); - } - } - return obj; - } - - function unserialize(str) { - if (typeof str != 'string') return null; - var data = (JSON ? JSON.parse(str) : eval('(' + str + ')')); - return process(data); - } - /* recursive function - * searches for entities in an object and processes them for serialization - */ +Crafty.storage = function(key, value){ + var storage = window.localStorage, + _value = value; - function prep(obj) { - if (obj.__c) { - // object is entity - var data = { - c: [], - attr: {} - }; - obj.trigger("SaveData", data, prep); - for (var i in obj.__c) { - data.c.push(i); - } - data.c = data.c.join(', '); - obj = data; - } else if (typeof obj == 'object') { - // recurse and look for entities - for (var prop in obj) { - obj[prop] = prep(obj[prop]); - } - } - return obj; - } - - function serialize(e) { - if (JSON) { - var data = prep(e); - return JSON.stringify(data); - } else { - alert("Crafty does not support saving on your browser. Please upgrade to a newer browser."); - return false; - } - } - - // for saving a game to a central server - - function external(setUrl) { - url = setUrl; - } - - function openExternal() { - if (1 && typeof url == "undefined") return; - // get the timestamps for external saves and compare them to local - // if the external is newer, load it - - var xml = new XMLHttpRequest(); - xhr.open("POST", url); - xhr.onreadystatechange = function (evt) { - if (xhr.readyState == 4) { - if (xhr.status == 200) { - var data = eval("(" + xhr.responseText + ")"); - for (var i in data) { - if (Crafty.storage.check(data[i].key, data[i].timestamp)) { - loadExternal(data[i].key); - } - } - } - } - }; - xhr.send("mode=timestamps&game=" + gameName); - } - - function saveExternal(key, data, ts) { - if (1 && typeof url == "undefined") return; - var xhr = new XMLHttpRequest(); - xhr.open("POST", url); - xhr.send("mode=save&key=" + key + "&data=" + encodeURIComponent(data) + "&ts=" + ts + "&game=" + gameName); - } + if(!storage){ + return false; + } - function loadExternal(key) { - if (1 && typeof url == "undefined") return; - var xhr = new XMLHttpRequest(); - xhr.open("POST", url); - xhr.onreadystatechange = function (evt) { - if (xhr.readyState == 4) { - if (xhr.status == 200) { - var data = eval("(" + xhr.responseText + ")"); - Crafty.storage.save(key, 'save', data); - } - } - }; - xhr.send("mode=load&key=" + key + "&game=" + gameName); + if(arguments.length === 1) { + try { + return JSON.parse(storage.getItem(key)); } - - /** - * get timestamp - */ - - function ts() { - var d = new Date(); - return d.getTime(); + catch (e) { + return storage.getItem(key); } - - // everyone names their object different. Fix that nonsense. - if (typeof indexedDB != 'object') { - window.indexedDB = window.indexedDB || window.mozIndexedDB || window.webkitIndexedDB || window.msIndexedDB; - window.IDBTransaction = window.IDBTransaction || window.webkitIDBTransaction; - window.IDBKeyRange = window.IDBKeyRange || window.webkitIDBKeyRange || window.msIDBKeyRange; - - /* Numeric constants for transaction type are deprecated - * Ensure that the script will work consistenly for recent and legacy browser versions - */ - if (typeof IDBTransaction == 'object') { - transactionType.READ = IDBTransaction.READ || IDBTransaction.readonly || transactionType.READ || 'read'; - transactionType.READ_WRITE = IDBTransaction.READ_WRITE || IDBTransaction.readwrite || transactionType.READ_WRITE || 'readwrite'; - } + } else { + if(typeof value === "object") { + _value = JSON.stringify(value); } - if (typeof indexedDB == 'object') { - - return { - open: function (gameName_n) { - gameName = gameName_n; - var stores = []; - - if (arguments.length == 1) { - stores.push('save'); - stores.push('cache'); - } else { - stores = arguments; - stores.shift(); - stores.push('save'); - stores.push('cache'); - } - if (db === null) { - var request = indexedDB.open(gameName); - request.onsuccess = function (e) { - db = e.target.result; - getTimestamps(); - openExternal(); - }; - request.onupgradeneeded = function (e) { - createStores(); - }; - } else { - createStores(); - getTimestamps(); - openExternal(); - } - - // get all the timestamps for existing keys - - function getTimestamps() { - try { - var trans = db.transaction(['save'], "read"), - store = trans.objectStore('save'), - request = store.getAll(); - request.onsuccess = function (e) { - var i = 0, - a = event.target.result, - l = a.length; - for (; i < l; i++) { - timestamps[a[i].key] = a[i].timestamp; - } - }; - } catch (e) {} - } - - function createStores() { - var request = db.setVersion("1.0"); - request.onsuccess = function (e) { - for (var i = 0; i < stores.length; i++) { - var st = stores[i]; - if (db.objectStoreNames.contains(st)) continue; - var store = db.createObjectStore(st, { - keyPath: "key" - }); - } - }; - } - }, - - save: function (key, type, data, callback) { - if (db === null) { - setTimeout(function () { - Crafty.storage.save(key, type, data); - }, 1); - return; - } - - var str = serialize(data), - t = ts(); - if (type == 'save') saveExternal(key, str, t); - try { - var request = db.transaction([type], transactionType.READ_WRITE).objectStore(type).add({ - "data": str, - "timestamp": t, - "key": key - }); - if (typeof callback == 'function') { - request.onsuccess = callback; - } - } catch (e) { - console.error(e); - } - }, - - load: function (key, type, callback) { - if (db === null) { - setTimeout(function () { - Crafty.storage.load(key, type, callback); - }, 1); - return; - } - try { - var request = db.transaction([type], transactionType.READ).objectStore(type).get(key); - request.onsuccess = function (e) { - callback(unserialize(e.target.result.data)); - }; - } catch (e) { - console.error(e); - } - }, - - getAllKeys: function (type, callback) { - if (db === null) { - setTimeout(function () { - Crafty.storage.getAllkeys(type, callback); - }, 1); - } - try { - var request = db.transaction([type], transactionType.READ).objectStore(type).openCursor(), - res = []; - request.onsuccess = function (e) { - var cursor = e.target.result; - if (cursor) { - res.push(cursor.key); - // 'continue' is a reserved word, so .continue() causes IE8 to completely bark with "SCRIPT1010: Expected identifier". - cursor['continue'](); - } else { - callback(res); - } - }; - } catch (e) { - console.error(e); - } - }, - - check: function (key, timestamp) { - return (timestamps[key] > timestamp); - }, - - external: external - }; - } else if (typeof openDatabase == 'function') { - return { - open: function (gameName_n) { - gameName = gameName_n; - if (arguments.length == 1) { - db = { - save: openDatabase(gameName_n + '_save', '1.0', 'Saves games for ' + gameName_n, 5 * 1024 * 1024), - cache: openDatabase(gameName_n + '_cache', '1.0', 'Cache for ' + gameName_n, 5 * 1024 * 1024) - }; - } else { - // allows for any other types that can be thought of - var args = arguments, - i = 0; - args.shift(); - for (; i < args.length; i++) { - if (typeof db[args[i]] == 'undefined') - db[args[i]] = openDatabase(gameName + '_' + args[i], '1.0', type, 5 * 1024 * 1024); - } - } - - db.save.transaction(function (tx) { - tx.executeSql('SELECT key, timestamp FROM data', [], function (tx, res) { - var i = 0, - a = res.rows, - l = a.length; - for (; i < l; i++) { - timestamps[a.item(i).key] = a.item(i).timestamp; - } - }); - }); - }, - - save: function (key, type, data) { - if (typeof db[type] == 'undefined' && gameName !== '') { - this.open(gameName, type); - } - - var str = serialize(data), - t = ts(); - if (type == 'save') saveExternal(key, str, t); - db[type].transaction(function (tx) { - tx.executeSql('CREATE TABLE IF NOT EXISTS data (key unique, text, timestamp)'); - tx.executeSql('SELECT * FROM data WHERE key = ?', [key], function (tx, results) { - if (results.rows.length) { - tx.executeSql('UPDATE data SET text = ?, timestamp = ? WHERE key = ?', [str, t, key]); - } else { - tx.executeSql('INSERT INTO data VALUES (?, ?, ?)', [key, str, t]); - } - }); - }); - }, - - load: function (key, type, callback) { - if (typeof db[type] === 'undefined') { - setTimeout(function () { - Crafty.storage.load(key, type, callback); - }, 1); - return; - } - db[type].transaction(function (tx) { - tx.executeSql('SELECT text FROM data WHERE key = ?', [key], function (tx, results) { - if (results.rows.length) { - res = unserialize(results.rows.item(0).text); - callback(res); - } - }); - }); - }, - - getAllKeys: function (type, callback) { - if (typeof db[type] === 'undefined') { - setTimeout(function () { - Crafty.storage.getAllKeys(type, callback); - }, 1); - return; - } - db[type].transaction(function (tx) { - tx.executeSql('SELECT key FROM data', [], function (tx, results) { - callback(results.rows); - }); - }); - }, - - check: function (key, timestamp) { - return (timestamps[key] > timestamp); - }, - - external: external - }; - } else if (typeof window.localStorage == 'object') { - return { - open: function (gameName_n) { - gameName = gameName_n; - }, - - save: function (key, type, data) { - var k = gameName + '.' + type + '.' + key, - str = serialize(data), - t = ts(); - if (type == 'save') saveExternal(key, str, t); - window.localStorage[k] = str; - if (type == 'save') - window.localStorage[k + '.ts'] = t; - }, + storage.setItem(key, _value); + + } - load: function (key, type, callback) { - var k = gameName + '.' + type + '.' + key, - str = window.localStorage[k]; - - callback(unserialize(str)); - }, - - getAllKeys: function (type, callback) { - var res = {}, output = [], - header = gameName + '.' + type; - for (var i in window.localStorage) { - if (i.indexOf(header) != -1) { - var key = i.replace(header, '').replace('.ts', ''); - res[key] = true; - } - } - for (i in res) { - output.push(i); - } - callback(output); - }, - - check: function (key, timestamp) { - var ts = window.localStorage[gameName + '.save.' + key + '.ts']; - - return (parseInt(timestamp, 10) > parseInt(ts, 10)); - }, - - external: external - }; - } else { - // default fallback to cookies - return { - open: function (gameName_n) { - gameName = gameName_n; - }, - - save: function (key, type, data) { - // cookies are very limited in space. we can only keep saves there - if (type != 'save') return; - var str = serialize(data), - t = ts(); - if (type == 'save') saveExternal(key, str, t); - document.cookie = gameName + '_' + key + '=' + str + '; ' + gameName + '_' + key + '_ts=' + t + '; expires=Thur, 31 Dec 2099 23:59:59 UTC; path=/'; - }, - - load: function (key, type, callback) { - if (type != 'save') return; - var reg = new RegExp(gameName + '_' + key + '=[^;]*'), - result = reg.exec(document.cookie), - data = unserialize(result[0].replace(gameName + '_' + key + '=', '')); - - callback(data); - }, - - getAllKeys: function (type, callback) { - if (type != 'save') return; - var reg = new RegExp(gameName + '_[^_=]', 'g'), - matches = reg.exec(document.cookie), - i = 0, - l = matches.length, - res = {}, output = []; - for (; i < l; i++) { - var key = matches[i].replace(gameName + '_', ''); - res[key] = true; - } - for (i in res) { - output.push(i); - } - callback(output); - }, - - check: function (key, timestamp) { - var header = gameName + '_' + key + '_ts', - reg = new RegExp(header + '=[^;]'), - result = reg.exec(document.cookie), - ts = result[0].replace(header + '=', ''); - - return (parseInt(timestamp, 10) > parseInt(ts, 10)); - }, - - external: external - }; - } - /* template - return { - open: function (gameName) { - }, - save: function (key, type, data) { - }, - load: function (key, type, callback) { - }, - }*/ -})(); \ No newline at end of file +}; +/**@ + * #.storage.remove + * @comp Storage + * @sign .storage.remove(String key) + * @param key - a key where you will like to delete the value of. + * + * Generally you do not need to remove values from localStorage, but if you do + * store large amount of text, or want to unset something you can do that with + * this function. + * + * @example + * Get an already stored value + * ~~~ + * Crafty.storage.remove('playername'); + * ~~~ + * + */ +Crafty.storage.remove = function(key){ + window.localStorage.removeItem(key); +}; \ No newline at end of file diff --git a/src/text.js b/src/text.js index 89c5540c..a9959658 100644 --- a/src/text.js +++ b/src/text.js @@ -4,7 +4,7 @@ var Crafty = require('./core.js'), /**@ * #Text * @category Graphics - * @trigger Change - when the text is changed + * @trigger Invalidate - when the text is changed * @requires Canvas or DOM * Component to make a text entity. * @@ -28,6 +28,8 @@ Crafty.c("Text", { _text: "", defaultSize: "10px", defaultFamily: "sans-serif", + defaultVariant: "normal", + defaultLineHeight: "normal", ready: true, init: function () { @@ -36,7 +38,9 @@ Crafty.c("Text", { "type": "", "weight": "", "size": this.defaultSize, - "family": this.defaultFamily + "lineHeight":this.defaultLineHeight, + "family": this.defaultFamily, + "variant": this.defaultVariant }; this.bind("Draw", function (e) { @@ -77,11 +81,11 @@ Crafty.c("Text", { "cm": 96/2.54, "mm": 96/25.4, "in": 96, - "em": undefined, + "em": undefined, "ex": undefined }; return function (font){ - var number = parseFloat(font); + var number = parseFloat(font); var match = re.exec(font); var unit = match ? match[1] : "px"; if (multipliers[unit] !== undefined) @@ -125,7 +129,7 @@ Crafty.c("Text", { if (this.has("Canvas") ) this._resizeForCanvas(); - this.trigger("Change"); + this.trigger("Invalidate"); return this; }, @@ -143,9 +147,8 @@ Crafty.c("Text", { // Returns the font string to use _fontString: function(){ - return this._textFont.type + ' ' + this._textFont.weight + ' ' + this._textFont.size + ' ' + this._textFont.family; + return this._textFont.type + ' ' + this._textFont.variant + ' ' + this._textFont.weight + ' ' + this._textFont.size + ' / ' + this._textFont.lineHeight + ' ' + this._textFont.family; }, - /**@ * #.textColor * @comp Text @@ -168,14 +171,14 @@ Crafty.c("Text", { textColor: function (color, strength) { this._strength = strength; this._textColor = Crafty.toRGB(color, this._strength); - this.trigger("Change"); + this.trigger("Invalidate"); return this; }, /**@ * #.textFont * @comp Text - * @triggers Change + * @triggers Invalidate * @sign public this .textFont(String key, * value) * @param key - Property of the entity to modify * @param value - Value to set the property to @@ -183,7 +186,9 @@ Crafty.c("Text", { * @sign public this .textFont(Object map) * @param map - Object where the key is the property to modify and the value as the property value * - * Use this method to set font property of the text entity. + * Use this method to set font property of the text entity. Possible values are: type, weight, size, family, lineHeight, and variant. + * + * When rendered by the canvas, lineHeight and variant will be ignored. * * @example * ~~~ @@ -206,7 +211,7 @@ Crafty.c("Text", { if(propertyKey == 'family'){ this._textFont[propertyKey] = "'" + key[propertyKey] + "'"; } else { - this._textFont[propertyKey] = key[propertyKey]; + this._textFont[propertyKey] = key[propertyKey]; } } } @@ -217,13 +222,13 @@ Crafty.c("Text", { if (this.has("Canvas") ) this._resizeForCanvas(); - this.trigger("Change"); + this.trigger("Invalidate"); return this; }, /**@ * #.unselectable * @comp Text - * @triggers Change + * @triggers Invalidate * @sign public this .unselectable() * * This method sets the text so that it cannot be selected (highlighted) by dragging. @@ -246,7 +251,7 @@ Crafty.c("Text", { '-ms-user-select': 'none', 'user-select': 'none' }); - this.trigger("Change"); + this.trigger("Invalidate"); } return this; } diff --git a/src/viewport.js b/src/viewport.js index f9446ee1..edea3641 100644 --- a/src/viewport.js +++ b/src/viewport.js @@ -7,7 +7,10 @@ Crafty.extend({ * @category Stage * @trigger ViewportScroll - when the viewport's x or y coordinates change * @trigger ViewportScale - when the viewport's scale changes + * @trigger ViewportResize - when the viewport's dimension's change * @trigger InvalidateViewport - when the viewport changes + * @trigger StopCamera - when any camera animations should stop, such as at the start of a new animation. + * @trigger CameraAnimationDone - when a camera animation comes reaches completion * * 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. @@ -23,8 +26,8 @@ Crafty.extend({ * For development it can be useful to set this to false. */ clampToEntities: true, - width: 0, - height: 0, + _width: 0, + _height: 0, /**@ * #Crafty.viewport.x * @comp Crafty.viewport @@ -64,11 +67,15 @@ Crafty.extend({ * @comp Crafty.viewport * * A rectangle which defines the bounds of the viewport. - * It should be an object with two properties, `max` and `min`, + * It should be an object with two properties, `max` and `min`, * which are each an object with `x` and `y` properties. * * If this property is null, Crafty uses the bounding box of all the items - * on the stage. This is the initial value. + * on the stage. This is the initial value. (To prevent this behavior, set `Crafty.viewport.clampToEntities` to `false`) + * + * If you wish to bound the viewport along one axis but not the other, you can use `-Infinity` and `+Infinity` as bounds. + * + * @see Crafty.viewport.clampToEntities * * @example * Set the bounds to a 500 by 500 square: @@ -82,9 +89,9 @@ Crafty.extend({ /**@ * #Crafty.viewport.scroll * @comp Crafty.viewport - * @sign Crafty.viewport.scroll(String axis, Number v) + * @sign Crafty.viewport.scroll(String axis, Number val) * @param axis - 'x' or 'y' - * @param v - The new absolute position on the axis + * @param val - The new absolute position on the axis * * Will move the viewport to the position given on the specified axis * @@ -96,69 +103,74 @@ Crafty.extend({ * Crafty.viewport.scroll('_x', 500); * ~~~ */ - scroll: function (axis, v) { - v = Math.floor(v); - this[axis] = v; + scroll: function (axis, val) { + this[axis] = val; Crafty.trigger("ViewportScroll"); Crafty.trigger("InvalidateViewport"); }, rect: function () { return { - _x: -this._x / this._scale, - _y: -this._y / this._scale, + _x: -this._x, + _y: -this._y, _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 + * @param Number time - The duration in ms for the entire camera movement * - * Pans the camera a given number of pixels over a given number of frames + * Pans the camera a given number of pixels over the specified time */ pan: (function () { var tweens = {}, i, bound = false; + var targetX, targetY, startingX, startingY, easing; function enterFrame(e) { - var l = 0; - for (var 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]; - } + easing.tick(e.dt); + var v = easing.value(); + Crafty.viewport.x = (1-v) * startingX + v * targetX; + Crafty.viewport.y = (1-v) * startingY + v * targetY; + Crafty.viewport._clamp(); + + if (easing.complete){ + stopPan(); + Crafty.trigger("CameraAnimationDone"); } - if (l) Crafty.viewport._clamp(); } - return function (axis, v, time) { - Crafty.viewport.follow(); - if (axis == 'reset') { - for (var 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; + function stopPan(){ + Crafty.unbind("EnterFrame", enterFrame); + } + + Crafty.bind("StopCamera", stopPan); + + return function (dx, dy, time) { + // Cancel any current camera control + Crafty.trigger("StopCamera"); + + // Handle request to reset + if (dx == 'reset') { + return; } + + startingX = Crafty.viewport._x; + startingY = Crafty.viewport._y; + targetX = startingX - dx; + targetY = startingY - dy; + + easing = new Crafty.easing(time); + + // bind to event, using uniqueBind prevents multiple copies from being bound + Crafty.uniqueBind("EnterFrame", enterFrame); + }; })(), @@ -188,18 +200,23 @@ Crafty.extend({ Crafty.viewport._clamp(); } - return function (target, offsetx, offsety) { + function stopFollow(){ if (oldTarget) - oldTarget.unbind('Change', change); + oldTarget.unbind('Move', change); + } + + Crafty.bind("StopCamera", stopFollow); + + return function (target, offsetx, offsety) { if (!target || !target.has('2D')) return; - Crafty.viewport.pan('reset'); + Crafty.trigger("StopCamera"); oldTarget = target; offx = (typeof offsetx != 'undefined') ? offsetx : 0; offy = (typeof offsety != 'undefined') ? offsety : 0; - target.bind('Change', change); + target.bind('Move', change); change.call(target); }; })(), @@ -209,9 +226,9 @@ Crafty.extend({ * @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 + * @param Number time - The duration in ms of the camera motion * - * Centers the viewport on the given entity + * Centers the viewport on the given entity. */ centerOn: function (targ, time) { var x = targ.x + Crafty.viewport.x, @@ -223,9 +240,7 @@ Crafty.extend({ 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.pan(new_x, new_y, time); }, /**@ * #Crafty.viewport._zoom @@ -242,80 +257,87 @@ Crafty.extend({ * @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 + * @param Number time - the duration in ms 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. + * amt < 1 will bring it farther away. amt = 0 will reset to the default zoom level * 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--; + + + function stopZoom(){ + Crafty.unbind("EnterFrame", enterFrame); + } + Crafty.bind("StopCamera", stopZoom); + + var startingZoom, finalZoom, finalAmount, startingX, finalX, startingY, finalY, easing; + + function enterFrame(e){ + var amount, v; + + easing.tick(e.dt); + + // The scaling should happen smoothly -- start at 1, end at finalAmount, and at half way scaling should be by finalAmount^(1/2) + // Since value goes smoothly from 0 to 1, this fufills those requirements + amount = Math.pow(finalAmount, easing.value() ); + + // The viewport should move in such a way that no point reverses + // If a and b are the top left/bottom right of the viewport, then the below can be derived from + // (a_0-b_0)/(a-b) = amount, + // and the assumption that both a and b have the same form + // a = a_0 * (1-v) + a_f * v, + // b = b_0 * (1-v) + b_f * v. + // This is just an arbitrary parameterization of the only sensible path for the viewport corners to take. + // And by symmetry they should be parameterized in the same way! So not much choice here. + if (finalAmount === 1) + v = easing.value(); // prevent NaN! If zoom is used this way, it'll just become a pan. + else + v = (1/amount - 1 ) / (1/finalAmount - 1); + + // Set new scale and viewport position + Crafty.viewport.scale( amount * startingZoom ); + Crafty.viewport.scroll("_x", startingX * (1-v) + finalX * v ); + Crafty.viewport.scroll("_y", startingY * (1-v) + finalY * v ); + Crafty.viewport._clamp(); + + if (easing.complete){ + stopZoom(); + Crafty.trigger("CameraAnimationDone"); } + + } - return function (amt, cent_x, cent_y, time) { - var bounds = this.bounds || Crafty.map.boundaries(), - final_zoom = amt ? zoom * amt : 1; + return function (amt, cent_x, cent_y, time){ if (!amt) { // we're resetting to defaults - zoom = 1; - this._zoom = 1; + Crafty.viewport.scale(1); + return; } - act.width = bounds.max.x - bounds.min.x; - act.height = bounds.max.y - bounds.min.y; + if (arguments.length <= 2) { + time = cent_x; + cent_x = Crafty.viewport.x - Crafty.viewport.width; + cent_y = Crafty.viewport.y - Crafty.viewport.height; + } - prct.width = cent_x / act.width; - prct.height = cent_y / act.height; + Crafty.trigger("StopCamera"); + startingZoom = Crafty.viewport._zoom; + finalAmount = amt; + finalZoom = startingZoom * finalAmount; + - if (time === 0) time = 1; - zoom_tick = (final_zoom - zoom) / time; - dur = time; + startingX = Crafty.viewport.x; + startingY = Crafty.viewport.y; + finalX = - (cent_x - Crafty.viewport.width / (2 * finalZoom) ); + finalY = - (cent_y - Crafty.viewport.height / (2 * finalZoom) ); - Crafty.viewport.pan('reset'); - if (!bound) { - Crafty.bind('EnterFrame', enterFrame); - bound = true; - } + easing = new Crafty.easing(time); + + Crafty.uniqueBind("EnterFrame", enterFrame); }; + + })(), /**@ * #Crafty.viewport.scale @@ -323,9 +345,12 @@ Crafty.extend({ * @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 + * Adjusts the. 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. + * To reset the scale amount, pass 0. + * + * This method sets the absolute scale, while `Crafty.viewport.zoom` sets the scale relative to the existing value. + * @see Crafty.viewport.zoom * * @example * ~~~ @@ -334,9 +359,7 @@ Crafty.extend({ */ scale: (function () { return function (amt) { - var bounds = this.bounds || Crafty.map.boundaries(), - final_zoom = amt ? amt : 1; - + var final_zoom = amt ? amt : 1; this._zoom = final_zoom; this._scale = final_zoom; @@ -354,12 +377,17 @@ Crafty.extend({ * Toggle mouselook on the current viewport. * Simply call this function and the user will be able to * drag the viewport around. + * + * If the user starts a drag, "StopCamera" will be triggered, which will cancel any existing camera animations. */ mouselook: (function () { var active = false, dragging = false, lastMouse = {}; old = {}; + function stopLook(){ + dragging = false; + } return function (op, arg) { @@ -390,6 +418,7 @@ Crafty.extend({ Crafty.viewport._clamp(); break; case 'start': + Crafty.trigger("StopCamera"); lastMouse.x = arg.clientX; lastMouse.y = arg.clientY; dragging = true; @@ -410,10 +439,8 @@ Crafty.extend({ 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; + if (Crafty.viewport.x < -bound.max.x + Crafty.viewport.width) { + Crafty.viewport.x = -bound.max.x + Crafty.viewport.width; } else if (Crafty.viewport.x > -bound.min.x) { Crafty.viewport.x = -bound.min.x; } @@ -421,10 +448,8 @@ Crafty.extend({ 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; + if (Crafty.viewport.y < -bound.max.y + Crafty.viewport.height) { + Crafty.viewport.y = -bound.max.y + Crafty.viewport.height; } else if (Crafty.viewport.y > -bound.min.y) { Crafty.viewport.y = -bound.min.y; } @@ -451,9 +476,12 @@ Crafty.extend({ init: function (w, h, stage_elem) { Crafty.DOM.window.init(); + // setters+getters for the viewport + this._defineViewportProperties(); + //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; + 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') @@ -485,13 +513,12 @@ Crafty.extend({ * `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) + * + * ~~~ + * Crafty.stage.elem + * - Crafty.stage.inner (a div HTMLElement) + * - Crafty.canvas._canvas (a canvas HTMLElement) + * ~~~ */ //create stage div to contain everything @@ -564,6 +591,10 @@ Crafty.extend({ elem.height = this.height + "px"; elem.overflow = "hidden"; + + // resize events + Crafty.bind("ViewportResize", function(){Crafty.trigger("InvalidateViewport");}); + if (Crafty.mobile) { elem.position = "absolute"; elem.left = "0px"; @@ -606,6 +637,11 @@ Crafty.extend({ Crafty.stage.y = offset.y; } + + }, + + // Create setters/getters for x, y, width, height + _defineViewportProperties: function(){ if (Crafty.support.setter) { //define getters and setters to scroll the viewport this.__defineSetter__('x', function (v) { @@ -614,12 +650,28 @@ Crafty.extend({ this.__defineSetter__('y', function (v) { this.scroll('_y', v); }); + this.__defineSetter__('width', function (v) { + this._width = v; + Crafty.trigger("ViewportResize"); + }); + this.__defineSetter__('height', function (v) { + this._height = v; + Crafty.trigger("ViewportResize"); + }); this.__defineGetter__('x', function () { return this._x; }); this.__defineGetter__('y', function () { return this._y; }); + this.__defineGetter__('width', function () { + return this._width; + }); + this.__defineGetter__('height', function () { + return this._height; + }); + + //IE9 } else if (Crafty.support.defineProperty) { @@ -641,18 +693,25 @@ Crafty.extend({ }, configurable : true }); - } else { - // IE8 has no getter/setters -- Check for an update each frame. - this.x = this._x; - this.y = this._y; - Crafty.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); - } + Object.defineProperty(this, 'width', { + set: function (v) { + this._width = v; + Crafty.trigger("ViewportResize"); + }, + get: function () { + return this._width; + }, + configurable : true + }); + Object.defineProperty(this, 'height', { + set: function (v) { + this._height = v; + Crafty.trigger("ViewportResize"); + }, + get: function () { + return this._height; + }, + configurable : true }); } }, @@ -675,16 +734,9 @@ Crafty.extend({ 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"); - } + this._width = w; + this._height = h; + Crafty.trigger("ViewportResize"); } offset = Crafty.DOM.inner(Crafty.stage.elem); @@ -695,17 +747,17 @@ Crafty.extend({ /**@ * #Crafty.viewport.reset * @comp Crafty.stage + * @trigger StopCamera - called to cancel camera animations * * @sign public Crafty.viewport.reset() * - * Resets the viewport to starting values + * Resets the viewport to starting values, and cancels any existing camera animations. * Called when scene() is run. */ reset: function () { - Crafty.viewport.pan('reset'); - Crafty.viewport.follow(); - Crafty.viewport.mouselook('stop'); - Crafty.viewport.scale(); + Crafty.viewport.mouselook("stop"); + Crafty.trigger("StopCamera"); + Crafty.viewport.scale(1); } } }); diff --git a/tests/2d.js b/tests/2d.js new file mode 100644 index 00000000..065bd1bb --- /dev/null +++ b/tests/2d.js @@ -0,0 +1,468 @@ +module("2D", { + setup: function() { + // prepare something for all following tests + }, + teardown: function() { + // clean up after each test + Crafty("*").destroy(); + } +}); + +test("position", function() { + var player = Crafty.e("2D, DOM, Color").attr({ + w: 50, + h: 50 + }).color("red"); + player.x += 50; + strictEqual(player._x, 50, "X moved"); + + player.y += 50; + strictEqual(player._y, 50, "Y moved"); + + player.w += 50; + strictEqual(player._w, 100, "Width increase"); + + player.h += 50; + strictEqual(player._h, 100, "Height increase"); + + strictEqual(player._globalZ, player[0], "Global Z, Before"); + + player.z = 1; + strictEqual(player._z, 1, "Z index"); + + var global_z_guess; + if (player[0] < 10) { + global_z_guess = parseInt('10000' + player[0], 10); + } else { + global_z_guess = parseInt('1000' + player[0], 10); + } + strictEqual(player._globalZ, global_z_guess, "Global Z, After"); + +}); + +test("intersect", function() { + var player = Crafty.e("2D, DOM, Color").attr({ + w: 50, + h: 50 + }).color("red"); + player.x = 0; + player.y = 0; + player.w = 50; + player.h = 50; + + strictEqual(player.intersect(0, 0, 100, 50), true, "Intersected"); + + strictEqual(player.intersect({ + x: 0, + y: 0, + w: 100, + h: 50 + }), true, "Intersected Again"); + + strictEqual(player.intersect(100, 100, 100, 50), false, "Didn't intersect"); + +}); + +test("within", function() { + var player = Crafty.e("2D, DOM, Color").attr({ + w: 50, + h: 50 + }); + player.x = 0; + player.y = 0; + player.w = 50; + player.h = 50; + + strictEqual(player.within(0, 0, 50, 50), true, "Within"); + + strictEqual(player.within(-1, -1, 51, 51), true, "Within"); + + + strictEqual(player.within({ + _x: 0, + _y: 0, + _w: 50, + _h: 50 + }), true, "Within Again"); + + strictEqual(player.within(0, 0, 40, 50), false, "Wasn't within"); + + player.rotation = 90; // Once rotated, the entity should no longer be within the rectangle + + strictEqual(player.within(0, 0, 50, 50), false, "Rotated, Not within"); + strictEqual(player.within(-50, 0, 50, 50), true, "Rotated, within rotated area"); + +}); + +test("contains", function() { + var player = Crafty.e("2D, DOM, Color").attr({ + w: 50, + h: 50 + }); + player.x = 0; + player.y = 0; + player.w = 50; + player.h = 50; + + + strictEqual(player.contains(0, 0, 50, 50), true, "Contains"); + + strictEqual(player.contains(1, 1, 49, 49), true, "Contains"); + + strictEqual(player.contains({ + _x: 0, + _y: 0, + _w: 50, + _h: 50 + }), true, "Contains"); + + strictEqual(player.contains(1, 1, 51, 51), false, "Doesn't contain"); + + player.rotation = 90; + + strictEqual(player.contains(0, 0, 50, 50), false, "Rotated, no longer contains"); + strictEqual(player.within(-50, 0, 50, 50), true, "Rotated, contains rotated area"); + +}); + + + +test("circle", function() { + var player = Crafty.e("2D, DOM, Color").attr({ + w: 50, + h: 50 + }).color("red"); + var circle = new Crafty.circle(0, 0, 10); + + strictEqual(circle.containsPoint(1, 2), true, "Contained the point"); + strictEqual(circle.containsPoint(8, 9), false, "Didn't contain the point"); + + circle.shift(1, 0); + + strictEqual(circle.x, 1, "Shifted of one pixel on the x axis"); + strictEqual(circle.y, 0, "circle.y didn't change"); + strictEqual(circle.radius, 10, "circle.radius didn't change"); + +}); + +test("child", function() { + var parent0 = Crafty.e("2D, DOM, Color").attr({ + x: 0, + y: 0, + w: 50, + h: 50 + }).color("red"); + var child0 = Crafty.e("2D, DOM, Color").attr({ + x: 1, + y: 1, + w: 50, + h: 50 + }).color("red"); + var child1 = Crafty.e("2D, DOM, Color").attr({ + x: 2, + y: 2, + w: 50, + h: 50 + }).color("red"); + var child2 = Crafty.e("2D, DOM, Color").attr({ + x: 3, + y: 3, + w: 50, + h: 50 + }).color("red"); + var child3 = Crafty.e("2D, DOM, Color").attr({ + x: 4, + y: 4, + w: 50, + h: 50 + }).color("red"); + var child0_ID = child0[0]; + var child1_ID = child1[0]; + var child2_ID = child2[0]; + var child3_ID = child3[0]; + parent0.attach(child0); + parent0.attach(child1); + parent0.attach(child2); + parent0.attach(child3); + parent0.x += 50; + strictEqual(child0._x, 51, 'child0 shifted when parent did'); + strictEqual(child1._x, 52, 'child1 shifted when parent did'); + child0.x += 1; + child1.x += 1; + strictEqual(parent0._x, 50, 'child shifts do not move the parent'); + child1.destroy(); + deepEqual(parent0._children, [child0, child2, child3], 'child1 cleared itself from parent0._children when destroyed'); + parent0.destroy(); + strictEqual(Crafty(child0_ID).length, 0, 'destruction of parent killed child0'); + strictEqual(Crafty(child2_ID).length, 0, 'destruction of parent killed child2'); + strictEqual(Crafty(child3_ID).length, 0, 'destruction of parent killed child3'); + +}); + +test("child_rotate", function() { + var parent = Crafty.e("2D, DOM, Color") + .attr({ + x: 0, + y: 0, + w: 50, + h: 50, + rotation: 10 + }) + .color("red"); + var child = Crafty.e("2D, DOM, Color") + .attr({ + x: 10, + y: 10, + w: 50, + h: 50, + rotation: 15 + }) + .color("red"); + parent.attach(child); + + parent.rotation += 20; + strictEqual(parent.rotation, 30, 'parent rotates normally'); + strictEqual(child.rotation, 35, 'child follows parent rotation'); + + child.rotation += 22; + strictEqual(parent.rotation, 30, 'parent ignores child rotation'); + strictEqual(child.rotation, 57, 'child rotates normally'); + + parent.rotation = 100; // Rotation by 90 degrees from initial position + strictEqual(Round(child.x), -10, "Child moved around parent upon rotation (x)."); + strictEqual(Round(child.y), 10, "Child moved around parent upon rotation (y)."); + +}); + + + +// This test assumes that the "circles" are really octagons, as per Crafty.circle. +test("SAT", function() { + var e = Crafty.e("2D, Collision"); + var c1 = new Crafty.circle(100, 100, 10); + var c2 = new Crafty.circle(100, 105, 10); + strictEqual((e._SAT(c1, c2).overlap < -13.8 && e._SAT(c1, c2).overlap > -13.9), true, "Expected overlap to be about -13.86 ( or 15 cos[pi/8])"); + +}); + + +test("adjustable boundary", function() { + e = Crafty.e("2D").attr({ + x: 10, + y: 10, + w: 10, + h: 10 + }); + + // Four argument version + e.offsetBoundary(10, 1, 3, 0); + equal(e._bx1, 10, "X1 boundary set"); + equal(e._bx2, 3, "X2 boundary set"); + equal(e._by1, 1, "Y1 boundary set"); + equal(e._by2, 0, "Y2 boundary set"); + + e._calculateMBR(10, 10, 0); + + var mbr = e._mbr; + + equal(mbr._h, 11, "MBR height uses boundaries (11)"); + equal(mbr._w, 23, "MBR width uses boundaries (23)"); + + // One argument version + e.offsetBoundary(5); + equal(e._bx1, 5, "X1 boundary set"); + equal(e._bx2, 5, "X2 boundary set"); + equal(e._by1, 5, "Y1 boundary set"); + equal(e._by2, 5, "Y2 boundary set"); + +}); + + +test("disableControl and enableControl", function() { + var e = Crafty.e("2D, Twoway") + .attr({ + x: 0 + }) + .twoway(1); + + equal(e._movement.x, 0); + equal(e._x, 0); + Crafty.trigger('KeyDown', { + key: Crafty.keys.D + }); + Crafty.trigger('EnterFrame'); + equal(e._movement.x, 1); + equal(e._x, 1); + + e.disableControl(); + Crafty.trigger('EnterFrame'); + equal(e._movement.x, 1); + equal(e._x, 1); + + Crafty.trigger('KeyUp', { + key: Crafty.keys.D + }); + Crafty.trigger('EnterFrame'); + equal(e._movement.x, 0); + equal(e._x, 1); + + e.enableControl(); + Crafty.trigger('EnterFrame'); + equal(e._movement.x, 0); + equal(e._x, 1); + + Crafty.trigger('KeyDown', { + key: Crafty.keys.D + }); + Crafty.trigger('EnterFrame'); + equal(e._movement.x, 1); + equal(e._x, 2); + + Crafty.trigger('KeyUp', { + key: Crafty.keys.D + }); + Crafty.trigger('EnterFrame'); + equal(e._movement.x, 0); + equal(e._x, 2); + + e.destroy(); +}); + + +test("Resizing 2D objects & hitboxes", function() { + var e = Crafty.e("2D, Collision"); + e.attr({ + x: 0, + y: 0, + w: 40, + h: 50 + }); + + equal(e.map.points[0][0], 0, "Before rotation: x_0 is 0"); + equal(e.map.points[0][1], 0, "y_0 is 0"); + equal(e.map.points[2][0], 40, "x_2 is 40"); + equal(e.map.points[2][1], 50, "y_2 is 50"); + + e.rotation = 90; + + equal(Math.round(e.map.points[0][0]), 0, "After rotation by 90 deg: x_0 is 0"); + equal(Math.round(e.map.points[0][1]), 0, "y_0 is 0"); + equal(Math.round(e.map.points[2][0]), -50, "x_2 is -50"); + equal(Math.round(e.map.points[2][1]), 40, "y_2 is 40"); + + // After rotation the MBR will have changed + equal(Math.round(e._mbr._w), 50, "_mbr._w is 50"); + equal(Math.round(e._mbr._h), 40, "_mbr._h is 40"); + equal(Math.round(e._mbr._x), -50, "_mbr._x is -50"); + equal(Math.round(e._mbr._y), 0, "_mbr._y is 0"); + + e.collision(); // Check that regenerating the hitbox while rotated works correctly + + equal(Math.round(e.map.points[0][0]), 0, "After rotation and hitbox regeneration: x_0 is 0"); + equal(Math.round(e.map.points[0][1]), 0, "y_0 is 0"); + equal(Math.round(e.map.points[2][0]), -50, "x_2 is -50"); + equal(Math.round(e.map.points[2][1]), 40, "y_2 is 40"); + + + // Check that changing the width when rotated resizes correctly for both hitbox and MBR + // Rotated by 90 degrees, changing the width of the entity should change the height of the hitbox/mbr + e.w = 100; + + equal(Math.round(e.map.points[0][0]), 0, "After rotation and increase in width: x_0 is 0"); + equal(Math.round(e.map.points[0][1]), 0, "y_0 is 0"); + equal(Math.round(e.map.points[2][0]), -50, "x_2 is -50"); + equal(Math.round(e.map.points[2][1]), 100, "y_2 is 100"); + + // After rotation the MBR will have changed + equal(Math.round(e._mbr._w), 50, "_mbr._w is 50"); + equal(Math.round(e._mbr._h), 100, "_mbr._h is 100"); + equal(Math.round(e._mbr._x), -50, "_mbr._x is -50"); + equal(Math.round(e._mbr._y), 0, "_mbr._y is 0"); + + e.destroy(); +}); + +test("Hitboxes outside of entities (CBR)", function() { + var poly = new Crafty.polygon([ + [-8, 6], + [0, -8], + [8, -14], + [16, -8], + [24, 6] + ]); + + var e = Crafty.e("2D, Collision").attr({ + x: 50, + y: 50, + w: 16, + h: 16 + }).collision(poly); + + ok(e._cbr !== null, "_cbr exists"); + var cbr = e._cbr; + // Test whether cbr actually bounds hitbox+object + ok(cbr._x <= 42, "cbr x position correct"); + ok(cbr._y <= 36, "cbr y position correct"); + ok(cbr._x + cbr._w >= 74, "cbr width correct"); + ok(cbr._y + cbr._h >= 66, "cbr height correct"); + + var x0 = cbr._x, + y0 = cbr._y; + + e.x += 10; + e.y += 15; + + equal(cbr._x, x0 + 10, "cbr x position moves correctly"); + equal(cbr._y, y0 + 15, "cbr y position moves correctly"); + +}); + +test("CBRs on resize", function() { + var poly = new Crafty.polygon([ + [0, 0], + [0, 12], + [12, 12], + [12, 0] + ]); + + var e = Crafty.e("2D, Collision").attr({ + x: 50, + y: 50, + w: 15, + h: 15 + }).collision(poly); + + ok(e._cbr === null, "_cbr should not exist"); + + e.w = 10; + + ok(e._cbr !== null, "_cbr should now exist after entity shrinks"); + + e.w = 20; + + ok(e._cbr === null, "_cbr should not exist after entity grows again"); + +}); + +test("CBRs should be removed on removal of component", function() { + var poly = new Crafty.polygon([ + [0, 0], + [0, 12], + [12, 12], + [12, 0] + ]); + + var e = Crafty.e("2D, Collision").attr({ + x: 50, + y: 50, + w: 10, + h: 10 + }).collision(poly); + + ok(e._cbr !== null, "_cbr should exist to begin with"); + + e.removeComponent("Collision"); + + ok(e._cbr === null, "_cbr should now be removed along with Collision"); + +}); \ No newline at end of file diff --git a/tests/animation/animation.html b/tests/animation/animation.html index f22e5c78..17c022d0 100644 --- a/tests/animation/animation.html +++ b/tests/animation/animation.html @@ -25,10 +25,10 @@

    Sprite animation playground:

    - + - +
@@ -36,11 +36,11 @@

Sprite animation playground:

- + - + - +
diff --git a/tests/animation/sprite-animation.js b/tests/animation/sprite-animation.js index c0c412cc..95731aab 100644 --- a/tests/animation/sprite-animation.js +++ b/tests/animation/sprite-animation.js @@ -42,7 +42,7 @@ test("Test .getReel() with no active reel", function(){ test("Test .reel() with no active reel", function() { var ret; var newAnimation = Crafty.e("SpriteAnimation"); - + ret = newAnimation.reel(); strictEqual(ret, null, "reel() returns null"); @@ -52,7 +52,7 @@ test("Test .reel() with no active reel", function() { test("Test .animate() with no active reel", function() { var newAnimation = Crafty.e("SpriteAnimation"); - + throws(function(){newAnimation.animate()}, /No reel is specified, and there is no currently active reel./, "Throws when calling .animate().") throws(function(){newAnimation.animate(3)}, /No reel is specified, and there is no currently active reel./, "Throws when calling .animate() with loop count.") @@ -121,7 +121,7 @@ test("Test .resetAnimation() with no active reel", function() { newAnimation.destroy(); }); - + test("Test .isPlaying() with no active reel", function() { @@ -162,19 +162,19 @@ test("Test reel switching functionality", function(){ equal(spriteAnimation._currentReelId, "short", "Correct _currentReelId after switching"); equal(spriteAnimation._currentReel.id, "short", "Correct _currentReel.id after switching"); var e =""; - + throws( function(){spriteAnimation.reel("wrong");}, /The specified reel wrong is undefined/, "Function should throw on bad reel"); - + equal(spriteAnimation._currentReelId, "short", "Correct _currentReelId after attempting to switch to bad reel"); - - + + }); test("Test using reel() with no arguments", function(){ spriteAnimation.reel("count"); var ret = spriteAnimation.reel(); equal(ret, "count", ".reel() returns the current id"); - + // Test setting reel id manually, since that's what reel() should return // Don't ever do this in actual code! spriteAnimation._currentReelId = null; @@ -183,7 +183,7 @@ test("Test using reel() with no arguments", function(){ // Reset currentReelId, since we messed it up! spriteAnimation.reel("count"); - + }); test("Test using .getReel() with no arguments", function(){ @@ -210,7 +210,7 @@ test("Test using .reel to set an animation using start and end values", function spriteAnimation.reel('short-test'); var reel = spriteAnimation.getReel('short-test'); equal(reel.id, "short-test", "Id of reel is set correctly."); - + equal(reel.duration, 3, "Reel has correct duration.") equal(reel.currentFrame, 0, "Reel starts with correct currentFrame of 0."); equal(reel.defaultLoops, 1, "Reel starts with correct default number of loops."); @@ -218,8 +218,8 @@ test("Test using .reel to set an animation using start and end values", function var frames = reel.frames; equal(frames.length, 3, "Reel has correct number of frames."); deepEqual(frames[0], [0, 0], "First frame is correct."); - deepEqual(frames[1], [64, 0], "Second frame is correct."); - deepEqual(frames[2], [128, 0], "Third frame is correct."); + deepEqual(frames[1], [1, 0], "Second frame is correct."); + deepEqual(frames[2], [2, 0], "Third frame is correct."); }) @@ -230,7 +230,7 @@ test("Test using .reel to set an animation using an array of frames", function() spriteAnimation.reel('short-test-2'); var reel = spriteAnimation.getReel('short-test-2'); equal(reel.id, "short-test-2", "Id of reel is set correctly."); - + equal(reel.duration, 3, "Reel has correct duration.") equal(reel.currentFrame, 0, "Reel starts with correct currentFrame of 0."); equal(reel.defaultLoops, 1, "Reel starts with correct default number of loops."); @@ -239,9 +239,9 @@ test("Test using .reel to set an animation using an array of frames", function() equal(frames.length, 3, "Reel has correct number of frames."); // This relies on the sprite being defined with a size of 64 deepEqual(frames[0], [0, 0], "First frame is correct."); - deepEqual(frames[1], [64, 0], "Second frame is correct."); - deepEqual(frames[2], [128, 0], "Third frame is correct."); - + deepEqual(frames[1], [1, 0], "Second frame is correct."); + deepEqual(frames[2], [2, 0], "Third frame is correct."); + }) module("Reel state"); @@ -357,6 +357,24 @@ test("Play an animation where sprites are displayed for more than one frame", fu } }); +test("Play an animation at twice the rate", function(){ + spriteAnimation.animationSpeed = 2; + spriteAnimation.animate('count'); + Crafty.timer.simulateFrames(3); + var activeReel = spriteAnimation.getReel(); + equal(activeReel.currentFrame, 6, "Frame 6 should be displayed after 3 ticks at double speed"); + spriteAnimation.animationSpeed = 1; +}) + +test("Play an animation at half the rate", function(){ + spriteAnimation.animationSpeed = 0.5; + spriteAnimation.animate('count'); + Crafty.timer.simulateFrames(6); + var activeReel = spriteAnimation.getReel(); + equal(activeReel.currentFrame, 3, "Frame 3 should be displayed after 6 ticks at half speed"); + spriteAnimation.animationSpeed = 1; +}) + test("Show the last frame after an animation ends", function() { spriteAnimation.animate('count'); Crafty.timer.simulateFrames(20); @@ -414,7 +432,7 @@ test("Play an animation with an infinite repeat count", function() { expected.push(0); expected.push(1); expected.push(2); - } + } deepEqual(eventFrames, expected, "Expected events matching the amount of frames that pass"); deepEqual(finishedAnimations, [], "Expected no animation to end"); diff --git a/tests/audio.js b/tests/audio.js new file mode 100644 index 00000000..29f09f76 --- /dev/null +++ b/tests/audio.js @@ -0,0 +1,107 @@ +module("Audio", { + setup: function() { + // prepare something for all following tests + }, + teardown: function() { + // clean up after each test + Crafty("*").destroy(); + } +}); + +//Set up some test fixtures +function MockAudio() { + var self = this; + this.endedListeners = []; + this.canPlayType = function() { + return true; + }; + this.addEventListener = function(event, listener) { + switch (event) { + case "ended": + this.endedListeners.push(listener); + break; + default: + throw new Exception("Not implemented"); + } + }; + this.removeEventListener = function(event, listener) { + switch (event) { + case "ended": + var ind = this.endedListeners.indexOf(listener); + if (ind) this.endedListeners.splice(ind, 1); + break; + default: + throw new Exception("Not implemented"); + } + }; + + function fireEnded() { + setTimeout(function() { + self.ended = true; + self.endedListeners.forEach(function(f) { + f.call(self); + }); + }, 0); + } + this.play = function() { + if (this.src) { + fireEnded(); + } + }; + this.pause = function() {}; + this.ended = false; +} + +function ChromeBuggedAudio() { + var self = this; + this.canPlayType = function() { + return true; + }; + this.addEventListener = function(event, listener) {}; + this.removeEventListener = function(event, listener) {}; + this.play = function() { + if (this.src) { + self.ended = true; + self.src = null; + ok(true, "Audio played"); + } + }; + this.pause = function() {}; + this.ended = false; +} + + +asyncTest("setChannels", function() { + // Test that setChannels doesn't break sound + expect(2); + window.Audio = MockAudio; + Crafty.support.audio = true; + Crafty.audio.setChannels(5); + Crafty.audio.add("mockSound", ["sound.ogg"]); + var a = Crafty.audio.play("mockSound", 1); + ok(typeof a === "object", "Type of a is object: " + a); + a.addEventListener("ended", function() { + ok(true, "Sound played"); + delete window.Audio; //reset Audio to platform default + Crafty.audio.channels = []; + start(); + }); +}); + +test("chromeBug", function() { + // Test that we don't exhaust our audio channels if Chrome bug 280417 + // eats our "ended" events + expect(10); + window.Audio = ChromeBuggedAudio; + Crafty.support.audio = true; + Crafty.audio.setChannels(1); + Crafty.support.audio = true; + Crafty.audio.add("mockSound", ["sound.ogg"]); + + var a; + for (var i = 0; i < 10; i++) { + a = Crafty.audio.play("mockSound", 1); // This will trigger an assertion + } + delete window.Audio; //reset Audio to platform default + Crafty.audio.channels = []; +}); \ No newline at end of file diff --git a/tests/camera.html b/tests/camera.html index 951472fc..96cb3baf 100644 --- a/tests/camera.html +++ b/tests/camera.html @@ -13,15 +13,15 @@
  • (DD) Click is not working, Zoom is working, Hotspot visible
  • - +
  • (CC) Click is working, Zoom is not working, Hotspot visible
  • - +
  • (CD) Click is not working, Zoom is not working, Hotspot is visible and zooming.
  • - +
  • (DC) Click is not working (wrong hotspot), Zoom is working, Hotspot invisible
  • @@ -38,9 +38,9 @@ } , imageEntities = '2D, DOM, Image' , hotspotEntities = '2D, DOM, Color, Mouse'; - - - + + + config.imagePath = 'http://placekitten.com/' + config.playgroundWidth + '/' + config.playgroundHeight Crafty.scene('main', function() { diff --git a/tests/core.html b/tests/core.html deleted file mode 100755 index 82428a83..00000000 --- a/tests/core.html +++ /dev/null @@ -1,666 +0,0 @@ - - - - - - - - - - - - - - - -

    Crafty: Core

    -

    -
    -

    -
      -
      test markup, will be hidden
      - - diff --git a/tests/core.js b/tests/core.js new file mode 100644 index 00000000..410e0736 --- /dev/null +++ b/tests/core.js @@ -0,0 +1,491 @@ +module("Core", { + setup: function() { + // prepare something for all following tests + }, + teardown: function() { + // clean up after each test + Crafty("*").destroy(); + } +}); + +test("getVersion", function() { + + ok(Crafty.getVersion(), "The actual library version"); +}); + +test("selectors", function() { + var first = Crafty.e("test"); + Crafty.e("test"); + Crafty.e("test"); + Crafty.e("test"); + Crafty.e("test, test2"); + Crafty.e("test, test2"); + Crafty.e("test2"); + strictEqual(Crafty("test").length, 6, "Single component"); + strictEqual(Crafty("test test2").length, 2, "Two components ANDed"); + strictEqual(Crafty("test, test2").length, 7, "Two components ORed"); + + strictEqual(Crafty("*").length, 7, "All components - universal selector"); + + strictEqual(Crafty(first[0]), first, "Get by ID"); + +}); + +test("addComponent and removeComponent", function() { + var first = Crafty.e("test"); + Crafty.c("comp", { + added: true + }); + first.addComponent("test3"); + strictEqual(first.has("test3"), true, "component added"); + + first.addComponent("comp"); + strictEqual(first.added, true, "component with property exists"); + + first.addComponent("multi1, multi2"); + strictEqual(first.has("multi1") && first.has("multi2"), true, "multiple components added"); + + first.removeComponent("test3"); + strictEqual(first.has("test3"), false, "component removed"); + + first.removeComponent("comp"); + strictEqual(first.added && !first.has("comp"), true, "soft-removed component (properties remain)"); + first.removeComponent("comp", false); + strictEqual(!first.added && !first.has("comp"), true, "hard-removed component (properties are gone)"); + +}); + +test("remove", function() { + var removeRan = false, + destroyFlag = false; + + Crafty.c("comp", { + remove: function(destroyed) { + removeRan = true; + destroyFlag = destroyed; + } + }); + var e = Crafty.e("comp, blank"); + e.removeComponent("blank"); + strictEqual(removeRan, false, "Remove doesn't run on other component removal"); + + removeRan = false; + e.removeComponent("comp"); + strictEqual(removeRan, true, "Remove runs on correct component removal"); + strictEqual(destroyFlag, false, "Destroy flag false on regular removal"); + + removeRan = false; + e.addComponent("comp"); + e.destroy(); + strictEqual(removeRan, true, "Remove runs on component destrution"); + strictEqual(destroyFlag, true, "Destroy flag true on destruction"); +}); + +test("attr", function() { + var first = Crafty.e("test"); + first.attr("single", true); + strictEqual(first.single, true, "single attribute assigned"); + + first.attr({ + prop: "test", + another: 56 + }); + strictEqual(first.prop, "test", "properties from object assigned"); + strictEqual(first.another, 56, "properties from object assigned"); + +}); + +test("setter", function() { + if (!(Crafty.support.setter || Crafty.support.defineProperty)) { + // IE8 has a setter() function but it behaves differently. No test is currently written for IE8. + expect(0); + return; + } + var first = Crafty.e("test"); + first.setter('p1', function(v) { + this._p1 = v * 2; + }); + first.p1 = 2; + strictEqual(first._p1, 4, "single property setter"); + + first.setter('p2', function(v) { + this._p2 = v * 2; + }).setter('p3', function(v) { + this._p3 = v * 2; + }); + first.p2 = 2; + first.p3 = 3; + strictEqual(first._p2 + first._p3, 10, "two property setters"); + +}); + +test("bind", function() { + var first = Crafty.e("test"), + triggered = false; + first.bind("myevent", function() { + triggered = true; + }); + first.trigger("myevent"); + strictEqual(triggered, true, "custom event triggered"); + +}); + +test("unbind", function() { + var first = Crafty.e("test"); + first.bind("myevent", function() { + ok(false, "This should not be triggered (unbinding all)"); + }); + first.unbind("myevent"); + first.trigger("myevent"); + + function callback() { + ok(false, "This should also not be triggered (unbind by FN)"); + } + + function callback2() { + ok(true, "This should be triggered"); + } + + first.bind("myevent", callback); + first.bind("myevent", callback2); + first.unbind("myevent", callback); + first.trigger("myevent"); + +}); + +test("globalBindAndUnbind", function() { + var flag = 0; + var add_1 = function() { + flag += 1; + }; + var add_10 = function() { + flag += 10; + }; + var add_100 = function() { + flag += 100; + }; + Crafty.bind("theglobalevent", add_1); + Crafty.bind("theglobalevent", add_10); + Crafty.bind("theglobalevent", add_100); + Crafty.trigger("theglobalevent"); + strictEqual(flag, 111, "global event binding worked"); + Crafty.unbind("theglobalevent", add_1); + Crafty.trigger("theglobalevent"); + strictEqual(flag, 221, "global event single-function unbinding worked"); + Crafty.unbind("theglobalevent"); + Crafty.trigger("theglobalevent"); + strictEqual(flag, 221, "global event full unbinding worked"); +}); + +test("each", function() { + var count = 0; + Crafty.e("test"); + Crafty.e("test"); + Crafty.e("test"); + Crafty.e("test"); + Crafty.e("test, test2"); + Crafty.e("test, test2"); + Crafty.e("test2"); + + Crafty("test").each(function() { + count++; + }); + strictEqual(count, 6, "Iterated all elements with certain component"); + + count = 0; + Crafty("*").each(function() { + count++; + }); + strictEqual(count, 7, "Iterated all elements"); + +}); + +test("Crafty.get() to find an array", function() { + Crafty.e("test"); + Crafty.e("test"); + Crafty.e("test"); + + var collection = Crafty("test"); + var result = collection.get(); + equal(result.length, 3, "resultant array should be length 3"); + equal(result[0].has("test"), true, "Result elements should have correct component"); + equal(collection[0], result[0].getId(), "First id of result should match first id of Crafty array"); + +}); + +test("Crafty.get(index) to find the indicated entity", function() { + Crafty.e("test"); + Crafty.e("test"); + Crafty.e("test"); + var collection, result; + + collection = Crafty("test"); + result = collection.get(0); + equal(result.has("test"), true, "Result should have correct component"); + equal(result.getId(), collection[0], "result should be first element of collection"); + + result = collection.get(-1); + equal(result.has("test"), true, "Result should have correct component"); + equal(result.getId(), collection[2], "result should be last element of collection"); + +}); + +test("Crafty.get(index) error checking", function() { + Crafty.e("test"); + Crafty.e("test"); + Crafty.e("test"); + var collection, result; + + collection = Crafty("test"); + + result = collection.get(3); + equal(typeof result, "undefined", "result of get(3) should be undefined"); + + result = collection.get(-4); + equal(typeof result, "undefined", "result of get(-4) should be undefined"); + +}); + +test("Crafty.get with only one object", function() { + var e = Crafty.e("test"); + var collection = Crafty("test"); + result = collection.get(0); + equal(result.getId(), e.getId(), "result of get(0) is correct entity"); + result = collection.get(); + equal(result.length, 1, "result of get() is array of length 1"); + +}); + +test("requires", function() { + var first = Crafty.e("test"); + Crafty.c("already", { + already: true + }); + Crafty.c("notyet", { + notyet: true + }); + + first.addComponent("already"); + first.already = "already"; + + first.requires("already, notyet"); + + strictEqual(first.already, "already", "Didn't overwrite property"); + strictEqual(first.notyet, true, "Assigned if didn't have"); + ok(first.has("already") && first.has("notyet"), "Both added"); + +}); + +test("destroy", function() { + var first = Crafty.e("test"), + id = first[0]; //id + first.destroy(); + strictEqual(Crafty(id).length, 0, "Not listed"); + +}); + +test(".frame() function", function(){ + var frameNumber; + var frameFunction = function() { + frameNumber = Crafty.frame(); + }; + Crafty.bind('EnterFrame', frameFunction); + Crafty.timer.simulateFrames(1); + + ok(frameNumber, '.frame function should return a value.'); + + Crafty.unbind(frameFunction); +}); + +module("Scenes", { + setup: function() { + // prepare something for all following tests + }, + teardown: function() { + // clean up after each test + Crafty("*").destroy(); + } +}); + +test("Scene calling", function() { + var x = 0; + var sceneInit = function() { + x = 13; + }; + Crafty.scene("test-call", sceneInit); + Crafty.scene("test-call"); + equal(x, 13, "Scene called succesfully."); + +}); + +test("Scene parameters", function() { + var x = 0; + var paramTaker = function(y) { + x = y; + }; + Crafty.scene("test-param", paramTaker); + Crafty.scene("test-param", 11); + equal(x, 11, "Scene called succesfully with parameter."); +}); + +test("Calling a scene destroys 2D entities", function() { + var e = Crafty.e("2D"); + var sceneInit = function() {}; + Crafty.scene("test-destroy", sceneInit); + Crafty.scene("test-destroy"); + var l = Crafty("2D").length; + equal(l, 0, "2D entity destroyed on scene change."); + +}); + +test("Calling a scene doesn't destroy 2D entities with Persist", function() { + var e = Crafty.e("2D, Persist"); + var sceneInit = function() {}; + Crafty.scene("test-persist", sceneInit); + Crafty.scene("test-persist"); + var l = Crafty("2D").length; + equal(l, 1, "Persist entity remains on scene change."); + +}); + + +test("Scene uninit function called", function() { + var x = 0; + var y = 0; + var sceneInit = function() { + x = 13; + }; + var sceneUninit = function() { + x = 20; + }; + var sceneGame = function() { + y = 5; + }; + Crafty.defineScene("test-uninit", sceneInit, sceneUninit); + Crafty.defineScene("game", sceneGame); + Crafty.enterScene("test-uninit"); + Crafty.enterScene("game"); + equal(x, 20, "Uninit scene called successfully when chanced to another scene"); + +}); + + +module("DebugLayer", { + setup: function() { + // prepare something for all following tests + }, + teardown: function() { + // clean up after each test + Crafty("*").destroy(); + } +}); + +test("DebugCanvas", function() { + if (!(Crafty.support.canvas)) { + expect(0); + return; + } + var e = Crafty.e("2D, DebugCanvas"); + var ctx = Crafty.DebugCanvas.context; + + e.debugFill("purple"); + equal(e._debug.fillStyle, "purple", "fill style set correctly on entity"); + + e.debugStroke("green"); + equal(e._debug.strokeStyle, "green", "stroke style set correctly on entity"); + + e.debugDraw(ctx); + equal(ctx.fillStyle, "#800080", "context.fillStyle set correctly on draw"); // fillStyle will report the hex code + equal(ctx.strokeStyle, "#008000", "context.strokeStyle set correctly on draw"); + + e.debugFill(); + equal(e._debug.fillStyle, "red", "default fill style set correctly"); + + e.debugStroke(); + equal(e._debug.strokeStyle, "red", "default stroke style set correctly"); + + + e.destroy(); + +}); + +test("VisibleMBR and DebugRect", function() { + var e = Crafty.e("2D, VisibleMBR").attr({ + x: 10, + y: 10, + w: 10, + h: 20 + }); + e._assignRect(); + equal(e.debugRect._x, 10, "debugRect has correct x coord"); + equal(e.debugRect._h, 20, "debugRect has correct height"); + + e.rotation = 90; + e._assignRect(); + equal(e.debugRect._h, 10, "debugRect has correct height of MBR after rotation"); + + e.destroy(); + +}); + +test("Hitbox debugging", function() { + var e = Crafty.e("2D, Collision, WiredHitBox").attr({ + x: 10, + y: 10, + w: 10, + h: 20 + }).collision(); + e.matchHitBox(); // only necessary until collision works properly! + equal(e.polygon.points[0][0], 10, "WiredHitBox -- correct x coord for upper right corner"); + equal(e.polygon.points[2][1], 30, "correct y coord for lower right corner"); + notEqual(typeof e._debug.strokeStyle, "undefined", "stroke style is assigned"); + equal(typeof e._debug.fillStyle, "undefined", "fill style is undefined"); + + e.destroy(); + + var e2 = Crafty.e("2D, Collision, SolidHitBox").attr({ + x: 10, + y: 10, + w: 10, + h: 20 + }).collision(); + e2.matchHitBox(); // only necessary until collision works properly! + equal(e2.polygon.points[0][0], 10, "SolidHitBox -- correct x coord for upper right corner"); + equal(e2.polygon.points[2][1], 30, "correct y coord for lower right corner"); + equal(typeof e2._debug.strokeStyle, "undefined", "stroke style is undefined"); + notEqual(typeof e2._debug.fillStyle, "undefined", "fill style is assigned"); + + e2.collision(new Crafty.polygon([0, 0], [15, 0], [0, 15])); + e2.matchHitBox(); + equal(e2.polygon.points[2][1], 25, "After change -- correct y coord for third point"); + + e2.destroy(); + +}); + +module("Easing", { + setup: function() { + // prepare something for all following tests + }, + teardown: function() { + // clean up after each test + Crafty("*").destroy(); + } +}); + +test("Crafty.easing duration", function() { + var e = new Crafty.easing(80); // 4 frames == 80ms by default + equal(e.duration, 80, "Default duration in ms"); +}); + +test("Crafty.easing", function() { + var e = new Crafty.easing(80); // 4 frames == 80ms by default + e.tick(20); + e.tick(20); + equal(e.value(), 0.5, ".5 after two steps"); + e.tick(20); + e.tick(20); + equal(e.value(), 1, "1 after completed"); + e.tick(20); + equal(e.value(), 1, "Remains 1 after completion"); +}); \ No newline at end of file diff --git a/tests/dom.html b/tests/dom.html deleted file mode 100644 index 461efbcc..00000000 --- a/tests/dom.html +++ /dev/null @@ -1,65 +0,0 @@ - - - - - - - - - - - - - - - -

      Crafty: Core

      -

      -
      -

      -
        -
        test markup, will be hidden
        - - diff --git a/tests/dom.js b/tests/dom.js new file mode 100644 index 00000000..4e597b60 --- /dev/null +++ b/tests/dom.js @@ -0,0 +1,58 @@ +module("DOM", { + setup: function() { + // prepare something for all following tests + }, + teardown: function() { + // clean up after each test + Crafty("*").destroy(); + } +}); + +test("avoidCss3dTransforms", function() { + var useCss3dTransforms = Crafty.e("2D, DOM") + .attr({ + x: 10, + y: 10 + }) + .draw(); + + strictEqual(useCss3dTransforms.avoidCss3dTransforms, false); + if (Crafty.support.css3dtransform) { + strictEqual(useCss3dTransforms._cssStyles.transform, "translate3d(10px,10px,0)"); + strictEqual(useCss3dTransforms._cssStyles.top, ""); + strictEqual(useCss3dTransforms._cssStyles.left, ""); + } else { + strictEqual(avoidCss3dTransforms._cssStyles.transform, ""); + strictEqual(avoidCss3dTransforms._cssStyles.top, 10); + strictEqual(avoidCss3dTransforms._cssStyles.left, 10); + } + + var avoidCss3dTransforms = Crafty.e("2D, DOM") + .attr({ + x: 10, + y: 10, + avoidCss3dTransforms: true + }) + .draw(); + + strictEqual(avoidCss3dTransforms.avoidCss3dTransforms, true); + strictEqual(avoidCss3dTransforms._cssStyles.transform, ""); + strictEqual(avoidCss3dTransforms._cssStyles.top, 10); + strictEqual(avoidCss3dTransforms._cssStyles.left, 10); + // Clean up + Crafty("*").destroy(); +}); + +test("removeComponent removes element class", function() { + var element = Crafty.e("DOM"); + hasClassName = function(el, name) { + return el._element.className.indexOf(name) >= 0; + }; + element.addComponent("removeMe"); + strictEqual(element.has("removeMe"), true, "component added"); + strictEqual(hasClassName(element, "removeMe"), true, "classname added"); + + element.removeComponent("removeMe"); + strictEqual(element.has("removeMe"), false, "component removed"); + strictEqual(hasClassName(element, "removeMe"), false, "classname removed"); +}); \ No newline at end of file diff --git a/tests/events.html b/tests/events.html index 73168b37..37e24307 100644 --- a/tests/events.html +++ b/tests/events.html @@ -1,4 +1,4 @@ - @@ -12,7 +12,7 @@ security settings when firefox opens an HTML file saved on your computer; it does not affect your security when you are browsing the internet.] * To run the tests in chrome: You need to pass a flag when opening chrome. - For example, in Linux, you would run this command in a Linux terminal: + For example, in Linux, you would run this command in a Linux terminal: chromium-browser /path/to/core.html --allow-file-access-from-files --> @@ -26,9 +26,9 @@ $(document).ready(function() { Crafty.init(500,500); - + module("EVENTS"); - + test("Global binding events", function() { var x = 0; function add(){x++} @@ -36,13 +36,19 @@ Crafty.bind("Increment", add) Crafty.trigger("Increment") strictEqual(x, 1, "Crafty.bind fired once"); - + x = 0; Crafty.unbind("Increment", add) Crafty.trigger("Increment") strictEqual(x, 0, "Crafty.bind does not fire once unbound"); + x = 0; + var ref = Crafty.bind("Increment", add) + Crafty.unbind("Increment", ref) + Crafty.trigger("Increment") + strictEqual(x, 0, "Crafty.bind does not fire once unbound via reference"); + x = 0; Crafty.one("Increment", add) Crafty.trigger("Increment") @@ -54,7 +60,7 @@ Crafty.uniqueBind("Increment", add); Crafty.trigger("Increment"); strictEqual(x, 1, "Event bound twice by Crafty.uniqueBound fires only once"); - + x = 0; Crafty.unbind("Increment", add); Crafty.trigger("Increment") @@ -63,7 +69,7 @@ }); test("Entity binding events", function() { - + var x = 0; function add(){x++} var e = Crafty.e("Triggerable") @@ -71,7 +77,7 @@ e.bind("Increment", add) e.trigger("Increment") strictEqual(x, 1, ".bind fired once"); - + x = 0; e.unbind("Increment", add) @@ -89,7 +95,7 @@ e.uniqueBind("Increment", add); e.trigger("Increment"); strictEqual(x, 1, "Event bound twice by .uniqueBound fires only once"); - + x = 0; e.unbind("Increment", add); e.trigger("Increment") @@ -145,13 +151,13 @@ strictEqual(temp.def, 1, "second one() should trigger once"); Crafty.unbind("Event A") - + }); test("Data passing", function(){ var x = 0, e; function add(data){x+=data.amount} - + x = 0; e = Crafty.e("Triggerable") e.bind("Increment", add) @@ -182,10 +188,10 @@ }); - + }); - +

        Crafty: Events

        diff --git a/tests/events.js b/tests/events.js new file mode 100644 index 00000000..b0d6dacd --- /dev/null +++ b/tests/events.js @@ -0,0 +1,204 @@ +module("Events", { + setup: function() { + // prepare something for all following tests + }, + teardown: function() { + // clean up after each test + Crafty("*").destroy(); + } +}); + +test("Global binding events", function() { + var x = 0; + + function add() { + x++; + } + + Crafty.bind("Increment", add); + Crafty.trigger("Increment"); + strictEqual(x, 1, "Crafty.bind fired once"); + + + x = 0; + Crafty.unbind("Increment", add); + Crafty.trigger("Increment"); + strictEqual(x, 0, "Crafty.bind does not fire once unbound"); + + x = 0; + var ref = Crafty.bind("Increment", add); + Crafty.unbind("Increment", ref); + Crafty.trigger("Increment"); + strictEqual(x, 0, "Crafty.bind does not fire once unbound via reference"); + + x = 0; + Crafty.one("Increment", add); + Crafty.trigger("Increment"); + Crafty.trigger("Increment"); + strictEqual(x, 1, "Event bound by Crafty.one fires exactly once"); + + x = 0; + Crafty.uniqueBind("Increment", add); + Crafty.uniqueBind("Increment", add); + Crafty.trigger("Increment"); + strictEqual(x, 1, "Event bound twice by Crafty.uniqueBound fires only once"); + + x = 0; + Crafty.unbind("Increment", add); + Crafty.trigger("Increment"); + strictEqual(x, 0, "uniqueBound does not fire once unbound"); + +}); + +test("Entity binding events", function() { + + var x = 0; + + function add() { + x++; + } + var e = Crafty.e("Triggerable"); + + e.bind("Increment", add); + e.trigger("Increment"); + strictEqual(x, 1, ".bind fired once"); + + + x = 0; + e.unbind("Increment", add); + e.trigger("Increment"); + strictEqual(x, 0, ".bind does not fire once unbound"); + + x = 0; + e.one("Increment", add); + e.trigger("Increment"); + e.trigger("Increment"); + strictEqual(x, 1, "Event bound by .one fires exactly once"); + + x = 0; + e.uniqueBind("Increment", add); + e.uniqueBind("Increment", add); + e.trigger("Increment"); + strictEqual(x, 1, "Event bound twice by .uniqueBound fires only once"); + + x = 0; + e.unbind("Increment", add); + e.trigger("Increment"); + strictEqual(x, 0, "uniqueBound does not fire once unbound"); + + e.destroy(); +}); + +test("Multiple bound events", function() { + + //Test with entity trigger + var temp = Crafty.e('Triggerable'); + temp.xyz = 0; + temp.abc = 0; + temp.def = 0; + temp.one('Event A', function() { + this.xyz++; + }); + temp.bind('Event A', function() { + this.abc++; + }); + temp.one('Event A', function() { + this.def++; + }); + temp.trigger('Event A'); + temp.trigger('Event A'); + strictEqual(temp.xyz, 1, "ENTITY -- first one() should trigger once"); + strictEqual(temp.abc, 2, "regular event should trigger twice"); + strictEqual(temp.def, 1, "second one() should trigger once"); + temp.destroy(); + + //Test with global trigger on entity + temp = Crafty.e('Triggerable'); + temp.xyz = 0; + temp.abc = 0; + temp.def = 0; + temp.one('Event A', function() { + this.xyz++; + }); + temp.bind('Event A', function() { + this.abc++; + }); + temp.one('Event A', function() { + this.def++; + }); + Crafty.trigger('Event A'); + Crafty.trigger('Event A'); + strictEqual(temp.xyz, 1, "GLOBAL TRIGGER -- first one() should trigger once"); + strictEqual(temp.abc, 2, "regular event should trigger twice"); + strictEqual(temp.def, 1, "second one() should trigger once"); + temp.destroy(); + + //Test with global trigger, events bound on global + temp = Crafty; + temp.xyz = 0; + temp.abc = 0; + temp.def = 0; + temp.one('Event A', function() { + this.xyz++; + }); + temp.bind('Event A', function() { + this.abc++; + }); + temp.one('Event A', function() { + this.def++; + }); + Crafty.trigger('Event A'); + Crafty.trigger('Event A'); + strictEqual(temp.xyz, 1, "GLOBAL BIND -- first one() should trigger once"); + strictEqual(temp.abc, 2, "regular event should trigger twice"); + strictEqual(temp.def, 1, "second one() should trigger once"); + + Crafty.unbind("Event A"); + +}); + +test("Data passing", function() { + var x = 0, + e; + + function add(data) { + x += data.amount; + } + + x = 0; + e = Crafty.e("Triggerable"); + e.bind("Increment", add); + e.trigger("Increment", { + amount: 2 + }); + strictEqual(x, 2, "data passed correctly with .bind"); + e.destroy(); + + x = 0; + e = Crafty.e("Triggerable"); + e.one("Increment", add); + e.trigger("Increment", { + amount: 2 + }); + strictEqual(x, 2, "data passed correctly with .one"); + e.destroy(); + + x = 0; + Crafty.bind("Increment", add); + Crafty.trigger("Increment", { + amount: 2 + }); + strictEqual(x, 2, "data passed correctly with Crafty.bind"); + Crafty.unbind("Increment"); + + x = 0; + Crafty.one("Increment", add); + Crafty.trigger("Increment", { + amount: 3 + }); + strictEqual(x, 3, "data passed correctly with Crafty.one"); + Crafty.unbind("Increment"); + + + +}); \ No newline at end of file diff --git a/tests/index.html b/tests/index.html new file mode 100755 index 00000000..4a1dfdf7 --- /dev/null +++ b/tests/index.html @@ -0,0 +1,60 @@ + + + + + + + + + + +   + + + + + + + + +

        Crafty.js Test Suite

        +

        +
        +

        +
          +
          test markup, will be hidden
          + + + + + + + + + + + + + + + + + + + + + diff --git a/tests/isometric.html b/tests/isometric.html deleted file mode 100644 index 255ba4c4..00000000 --- a/tests/isometric.html +++ /dev/null @@ -1,115 +0,0 @@ - - - - - - - - - - - - - - - -

          Crafty: Core

          -

          -
          -

          -
            -
            test markup, will be hidden
            - - diff --git a/tests/isometric.js b/tests/isometric.js new file mode 100644 index 00000000..2aadff12 --- /dev/null +++ b/tests/isometric.js @@ -0,0 +1,92 @@ +module("Isometric", { + setup: function() { + // prepare something for all following tests + }, + teardown: function() { + // clean up after each test + Crafty("*").destroy(); + } +}); + +test("place tile", function() { + var iso = Crafty.isometric.size(64, 16); + + var tile1 = Crafty.e("2D, DOM, Color").attr({ + x: 0, + y: 0, + w: 64, + h: 16 + }).color("red"); + var tile2 = Crafty.e("2D, DOM, Color").attr({ + x: 100, + y: 100, + z: 3, + w: 64, + h: 16 + }).color("blue"); + + iso.place(0, 0, 0, tile1); + iso.place(1, 2, 5, tile2); + + equal(tile1.attr('x'), 0, "First tile should default to origin"); + equal(tile1.attr('y'), 0, "First tile should default to origin"); + equal(tile1.attr('z'), 0, "z-index should be transferred unchanged"); + + equal(tile2.attr('x'), 64 + Crafty.viewport._x, "Each tile should be offset by the sum of the width of those before it"); + equal(tile2.attr('y'), -24 + Crafty.viewport._y, "The row should be offset by one and a half times the height"); + equal(tile2.attr('z'), 8, "z-index should be added to existing value"); + + // Clean up + Crafty("*").destroy(); +}); + +test("pos2px", function() { + var iso = Crafty.isometric.size(64, 16); + + var origin = iso.pos2px(0, 0); + equal(origin.left, 0, "First tile should default to origin"); + equal(origin.top, 0, "First tile should default to origin"); + + var oddNumberedRow = iso.pos2px(0, 1); + equal(oddNumberedRow.left, 32, "Odd numbered rows should be be inset by half the width"); + equal(oddNumberedRow.top, 8, "Each row should move down by half the height"); + + var evenNumberedRow = iso.pos2px(0, 2); + equal(evenNumberedRow.left, 0, "Even numbered rows should not be be inset"); + equal(evenNumberedRow.top, 16, "Each row should move down by half the height"); + + var numberedColumn = iso.pos2px(3, 0); + equal(numberedColumn.left, 64 * 3, "Should be inset by the width times the x position"); +}); + +test("px2pos", function() { + var iso = Crafty.isometric.size(64, 16); + + var origin = iso.px2pos(0, 0); + equal(origin.x, 0, "Origin should be the corner of the lowest numbered tile"); + equal(origin.y, 0, "Origin should be the corner of the lowest numbered tile"); + + var oddNumberedRow = iso.px2pos(32, 8); + equal(oddNumberedRow.x, 0, "Odd numbered rows should be be inset by half the width"); + equal(oddNumberedRow.y, 1, "Each row should move down by half the height"); + + var evenNumberedRow = iso.px2pos(0, 16); + equal(evenNumberedRow.x, 0, "Even numbered rows should not be be inset"); + equal(evenNumberedRow.y, 2, "Each row should move down by half the height"); + + var numberedColumn = iso.px2pos(128, 0); + equal(numberedColumn.x, 2, "Should be inset by the width times the x position"); +}); + +test("round trip conversions", function() { + var iso = Crafty.isometric.size(64, 16); + + var startX = 14; + var startY = 21; + + var startingPoint = iso.pos2px(startX, startY); + var end = iso.px2pos(startingPoint.left, startingPoint.top); + + equal(end.x, startX, "x position should match"); + equal(end.y, startY, "y position should match"); +}); \ No newline at end of file diff --git a/tests/lib/helperFunctions.js b/tests/lib/helperFunctions.js new file mode 100644 index 00000000..b2057ded --- /dev/null +++ b/tests/lib/helperFunctions.js @@ -0,0 +1,3 @@ +Round = function(x){ + return Math.round(x*100)/100; +}; \ No newline at end of file diff --git a/tests/lib/modernizr-2.7.1.min.js b/tests/lib/modernizr-2.7.1.min.js new file mode 100644 index 00000000..58c6829b --- /dev/null +++ b/tests/lib/modernizr-2.7.1.min.js @@ -0,0 +1,4 @@ +/* Modernizr 2.7.1 (Custom Build) | MIT & BSD + * Build: http://modernizr.com/download/#-fontface-backgroundsize-borderimage-borderradius-boxshadow-flexbox-hsla-multiplebgs-opacity-rgba-textshadow-cssanimations-csscolumns-generatedcontent-cssgradients-cssreflections-csstransforms-csstransforms3d-csstransitions-applicationcache-canvas-canvastext-draganddrop-hashchange-history-audio-video-indexeddb-input-inputtypes-localstorage-postmessage-sessionstorage-websockets-websqldatabase-webworkers-geolocation-inlinesvg-smil-svg-svgclippaths-touch-webgl-shiv-mq-cssclasses-addtest-prefixed-teststyles-testprop-testallprops-hasevent-prefixes-domprefixes-load + */ +;window.Modernizr=function(a,b,c){function D(a){j.cssText=a}function E(a,b){return D(n.join(a+";")+(b||""))}function F(a,b){return typeof a===b}function G(a,b){return!!~(""+a).indexOf(b)}function H(a,b){for(var d in a){var e=a[d];if(!G(e,"-")&&j[e]!==c)return b=="pfx"?e:!0}return!1}function I(a,b,d){for(var e in a){var f=b[a[e]];if(f!==c)return d===!1?a[e]:F(f,"function")?f.bind(d||b):f}return!1}function J(a,b,c){var d=a.charAt(0).toUpperCase()+a.slice(1),e=(a+" "+p.join(d+" ")+d).split(" ");return F(b,"string")||F(b,"undefined")?H(e,b):(e=(a+" "+q.join(d+" ")+d).split(" "),I(e,b,c))}function K(){e.input=function(c){for(var d=0,e=c.length;d',a,""].join(""),l.id=h,(m?l:n).innerHTML+=f,n.appendChild(l),m||(n.style.background="",n.style.overflow="hidden",k=g.style.overflow,g.style.overflow="hidden",g.appendChild(n)),i=c(l,a),m?l.parentNode.removeChild(l):(n.parentNode.removeChild(n),g.style.overflow=k),!!i},z=function(b){var c=a.matchMedia||a.msMatchMedia;if(c)return c(b).matches;var d;return y("@media "+b+" { #"+h+" { position: absolute; } }",function(b){d=(a.getComputedStyle?getComputedStyle(b,null):b.currentStyle)["position"]=="absolute"}),d},A=function(){function d(d,e){e=e||b.createElement(a[d]||"div"),d="on"+d;var f=d in e;return f||(e.setAttribute||(e=b.createElement("div")),e.setAttribute&&e.removeAttribute&&(e.setAttribute(d,""),f=F(e[d],"function"),F(e[d],"undefined")||(e[d]=c),e.removeAttribute(d))),e=null,f}var a={select:"input",change:"input",submit:"form",reset:"form",error:"img",load:"img",abort:"img"};return d}(),B={}.hasOwnProperty,C;!F(B,"undefined")&&!F(B.call,"undefined")?C=function(a,b){return B.call(a,b)}:C=function(a,b){return b in a&&F(a.constructor.prototype[b],"undefined")},Function.prototype.bind||(Function.prototype.bind=function(b){var c=this;if(typeof c!="function")throw new TypeError;var d=w.call(arguments,1),e=function(){if(this instanceof e){var a=function(){};a.prototype=c.prototype;var f=new a,g=c.apply(f,d.concat(w.call(arguments)));return Object(g)===g?g:f}return c.apply(b,d.concat(w.call(arguments)))};return e}),s.flexbox=function(){return J("flexWrap")},s.canvas=function(){var a=b.createElement("canvas");return!!a.getContext&&!!a.getContext("2d")},s.canvastext=function(){return!!e.canvas&&!!F(b.createElement("canvas").getContext("2d").fillText,"function")},s.webgl=function(){return!!a.WebGLRenderingContext},s.touch=function(){var c;return"ontouchstart"in a||a.DocumentTouch&&b instanceof DocumentTouch?c=!0:y(["@media (",n.join("touch-enabled),("),h,")","{#modernizr{top:9px;position:absolute}}"].join(""),function(a){c=a.offsetTop===9}),c},s.geolocation=function(){return"geolocation"in navigator},s.postmessage=function(){return!!a.postMessage},s.websqldatabase=function(){return!!a.openDatabase},s.indexedDB=function(){return!!J("indexedDB",a)},s.hashchange=function(){return A("hashchange",a)&&(b.documentMode===c||b.documentMode>7)},s.history=function(){return!!a.history&&!!history.pushState},s.draganddrop=function(){var a=b.createElement("div");return"draggable"in a||"ondragstart"in a&&"ondrop"in a},s.websockets=function(){return"WebSocket"in a||"MozWebSocket"in a},s.rgba=function(){return D("background-color:rgba(150,255,150,.5)"),G(j.backgroundColor,"rgba")},s.hsla=function(){return D("background-color:hsla(120,40%,100%,.5)"),G(j.backgroundColor,"rgba")||G(j.backgroundColor,"hsla")},s.multiplebgs=function(){return D("background:url(https://),url(https://),red url(https://)"),/(url\s*\(.*?){3}/.test(j.background)},s.backgroundsize=function(){return J("backgroundSize")},s.borderimage=function(){return J("borderImage")},s.borderradius=function(){return J("borderRadius")},s.boxshadow=function(){return J("boxShadow")},s.textshadow=function(){return b.createElement("div").style.textShadow===""},s.opacity=function(){return E("opacity:.55"),/^0.55$/.test(j.opacity)},s.cssanimations=function(){return J("animationName")},s.csscolumns=function(){return J("columnCount")},s.cssgradients=function(){var a="background-image:",b="gradient(linear,left top,right bottom,from(#9f9),to(white));",c="linear-gradient(left top,#9f9, white);";return D((a+"-webkit- ".split(" ").join(b+a)+n.join(c+a)).slice(0,-a.length)),G(j.backgroundImage,"gradient")},s.cssreflections=function(){return J("boxReflect")},s.csstransforms=function(){return!!J("transform")},s.csstransforms3d=function(){var a=!!J("perspective");return a&&"webkitPerspective"in g.style&&y("@media (transform-3d),(-webkit-transform-3d){#modernizr{left:9px;position:absolute;height:3px;}}",function(b,c){a=b.offsetLeft===9&&b.offsetHeight===3}),a},s.csstransitions=function(){return J("transition")},s.fontface=function(){var a;return y('@font-face {font-family:"font";src:url("https://")}',function(c,d){var e=b.getElementById("smodernizr"),f=e.sheet||e.styleSheet,g=f?f.cssRules&&f.cssRules[0]?f.cssRules[0].cssText:f.cssText||"":"";a=/src/i.test(g)&&g.indexOf(d.split(" ")[0])===0}),a},s.generatedcontent=function(){var a;return y(["#",h,"{font:0/0 a}#",h,':after{content:"',l,'";visibility:hidden;font:3px/1 a}'].join(""),function(b){a=b.offsetHeight>=3}),a},s.video=function(){var a=b.createElement("video"),c=!1;try{if(c=!!a.canPlayType)c=new Boolean(c),c.ogg=a.canPlayType('video/ogg; codecs="theora"').replace(/^no$/,""),c.h264=a.canPlayType('video/mp4; codecs="avc1.42E01E"').replace(/^no$/,""),c.webm=a.canPlayType('video/webm; codecs="vp8, vorbis"').replace(/^no$/,"")}catch(d){}return c},s.audio=function(){var a=b.createElement("audio"),c=!1;try{if(c=!!a.canPlayType)c=new Boolean(c),c.ogg=a.canPlayType('audio/ogg; codecs="vorbis"').replace(/^no$/,""),c.mp3=a.canPlayType("audio/mpeg;").replace(/^no$/,""),c.wav=a.canPlayType('audio/wav; codecs="1"').replace(/^no$/,""),c.m4a=(a.canPlayType("audio/x-m4a;")||a.canPlayType("audio/aac;")).replace(/^no$/,"")}catch(d){}return c},s.localstorage=function(){try{return localStorage.setItem(h,h),localStorage.removeItem(h),!0}catch(a){return!1}},s.sessionstorage=function(){try{return sessionStorage.setItem(h,h),sessionStorage.removeItem(h),!0}catch(a){return!1}},s.webworkers=function(){return!!a.Worker},s.applicationcache=function(){return!!a.applicationCache},s.svg=function(){return!!b.createElementNS&&!!b.createElementNS(r.svg,"svg").createSVGRect},s.inlinesvg=function(){var a=b.createElement("div");return a.innerHTML="",(a.firstChild&&a.firstChild.namespaceURI)==r.svg},s.smil=function(){return!!b.createElementNS&&/SVGAnimate/.test(m.call(b.createElementNS(r.svg,"animate")))},s.svgclippaths=function(){return!!b.createElementNS&&/SVGClipPath/.test(m.call(b.createElementNS(r.svg,"clipPath")))};for(var L in s)C(s,L)&&(x=L.toLowerCase(),e[x]=s[L](),v.push((e[x]?"":"no-")+x));return e.input||K(),e.addTest=function(a,b){if(typeof a=="object")for(var d in a)C(a,d)&&e.addTest(d,a[d]);else{a=a.toLowerCase();if(e[a]!==c)return e;b=typeof b=="function"?b():b,typeof f!="undefined"&&f&&(g.className+=" "+(b?"":"no-")+a),e[a]=b}return e},D(""),i=k=null,function(a,b){function l(a,b){var c=a.createElement("p"),d=a.getElementsByTagName("head")[0]||a.documentElement;return c.innerHTML="x",d.insertBefore(c.lastChild,d.firstChild)}function m(){var a=s.elements;return typeof a=="string"?a.split(" "):a}function n(a){var b=j[a[h]];return b||(b={},i++,a[h]=i,j[i]=b),b}function o(a,c,d){c||(c=b);if(k)return c.createElement(a);d||(d=n(c));var g;return d.cache[a]?g=d.cache[a].cloneNode():f.test(a)?g=(d.cache[a]=d.createElem(a)).cloneNode():g=d.createElem(a),g.canHaveChildren&&!e.test(a)&&!g.tagUrn?d.frag.appendChild(g):g}function p(a,c){a||(a=b);if(k)return a.createDocumentFragment();c=c||n(a);var d=c.frag.cloneNode(),e=0,f=m(),g=f.length;for(;e",g="hidden"in a,k=a.childNodes.length==1||function(){b.createElement("a");var a=b.createDocumentFragment();return typeof a.cloneNode=="undefined"||typeof a.createDocumentFragment=="undefined"||typeof a.createElement=="undefined"}()}catch(c){g=!0,k=!0}})();var s={elements:d.elements||"abbr article aside audio bdi canvas data datalist details dialog figcaption figure footer header hgroup main mark meter nav output progress section summary template time video",version:c,shivCSS:d.shivCSS!==!1,supportsUnknownElements:k,shivMethods:d.shivMethods!==!1,type:"default",shivDocument:r,createElement:o,createDocumentFragment:p};a.html5=s,r(b)}(this,b),e._version=d,e._prefixes=n,e._domPrefixes=q,e._cssomPrefixes=p,e.mq=z,e.hasEvent=A,e.testProp=function(a){return H([a])},e.testAllProps=J,e.testStyles=y,e.prefixed=function(a,b,c){return b?J(a,b,c):J(a,"pfx")},g.className=g.className.replace(/(^|\s)no-js(\s|$)/,"$1$2")+(f?" js "+v.join(" "):""),e}(this,this.document),function(a,b,c){function d(a){return"[object Function]"==o.call(a)}function e(a){return"string"==typeof a}function f(){}function g(a){return!a||"loaded"==a||"complete"==a||"uninitialized"==a}function h(){var a=p.shift();q=1,a?a.t?m(function(){("c"==a.t?B.injectCss:B.injectJs)(a.s,0,a.a,a.x,a.e,1)},0):(a(),h()):q=0}function i(a,c,d,e,f,i,j){function k(b){if(!o&&g(l.readyState)&&(u.r=o=1,!q&&h(),l.onload=l.onreadystatechange=null,b)){"img"!=a&&m(function(){t.removeChild(l)},50);for(var d in y[c])y[c].hasOwnProperty(d)&&y[c][d].onload()}}var j=j||B.errorTimeout,l=b.createElement(a),o=0,r=0,u={t:d,s:c,e:f,a:i,x:j};1===y[c]&&(r=1,y[c]=[]),"object"==a?l.data=c:(l.src=c,l.type=a),l.width=l.height="0",l.onerror=l.onload=l.onreadystatechange=function(){k.call(this,r)},p.splice(e,0,u),"img"!=a&&(r||2===y[c]?(t.insertBefore(l,s?null:n),m(k,j)):y[c].push(l))}function j(a,b,c,d,f){return q=0,b=b||"j",e(a)?i("c"==b?v:u,a,b,this.i++,c,d,f):(p.splice(this.i++,0,a),1==p.length&&h()),this}function k(){var a=B;return a.loader={load:j,i:0},a}var l=b.documentElement,m=a.setTimeout,n=b.getElementsByTagName("script")[0],o={}.toString,p=[],q=0,r="MozAppearance"in l.style,s=r&&!!b.createRange().compareNode,t=s?l:n.parentNode,l=a.opera&&"[object Opera]"==o.call(a.opera),l=!!b.attachEvent&&!l,u=r?"object":l?"script":"img",v=l?"script":u,w=Array.isArray||function(a){return"[object Array]"==o.call(a)},x=[],y={},z={timeout:function(a,b){return b.length&&(a.timeout=b[0]),a}},A,B;B=function(a){function b(a){var a=a.split("!"),b=x.length,c=a.pop(),d=a.length,c={url:c,origUrl:c,prefixes:a},e,f,g;for(f=0;f - - - - - - - - - - - - - - -

            Crafty: Loader

            -

            -
            -

            -
              -
              test markup, will be hidden
              - - diff --git a/tests/loader.js b/tests/loader.js new file mode 100644 index 00000000..45e5b971 --- /dev/null +++ b/tests/loader.js @@ -0,0 +1,22 @@ +module('Loader', { + setup: function() { + // prepare something for all following tests + }, + teardown: function() { + // clean up after each test + Crafty("*").destroy(); + } +}); + +asyncTest('assets loading', function() { + expect(1); + + var onload; + + Crafty.load(['assets/100x100.png'], function() { + ok(typeof onload !== 'undefined' && onload.src.match('assets/100x100.png'), '100x100.png has been loaded'); + start(); + }, function(data) { + onload = data; + }); +}); \ No newline at end of file diff --git a/tests/math.html b/tests/math.html deleted file mode 100644 index c040f275..00000000 --- a/tests/math.html +++ /dev/null @@ -1 +0,0 @@ -

              Crafty: Stage

                test markup, will be hidden
                \ No newline at end of file diff --git a/tests/math.js b/tests/math.js new file mode 100644 index 00000000..ecd9cb4a --- /dev/null +++ b/tests/math.js @@ -0,0 +1,319 @@ +var Matrix2D = Crafty.math.Matrix2D; +var Vector2D = Crafty.math.Vector2D; + +// tests for general functions should go here (.abs(), .amountOf(), etc) + +module("Math - Vector2D", { + setup: function() { + // prepare something for all following tests + }, + teardown: function() { + // clean up after each test + Crafty("*").destroy(); + } +}); + +test("constructor", function() { + var v0 = new Vector2D(); + var v00 = new Vector2D(0, 0); + var v12 = new Vector2D(1, 2); + var v12_2 = new Vector2D(v12); + + equal(v0.equals(v00), true, "new Vector2D() equals new Vector2D(0, 0)"); + equal(v12.equals(v12_2), true, "new Vector2D(1, 2) equals new Vector2D(new Vector2D(1,2))"); +}); + +test("add()", function() { + var v12 = new Vector2D(1, 2); + var v34 = new Vector2D(3, 4); + var v = new Vector2D(4, 6); + + equal(v12.add(v34).equals(v), true, "<1,2> + <3,4> = <4,6>"); +}); + +test("angleBetween()", function() { + var v10 = new Vector2D(1, 0); + var v_11 = new Vector2D(-1, 1); + var v1_1 = new Vector2D(1, -1); + + equal(v10.angleBetween(v_11), 3 * Math.PI / 4, "<1,0>.angleBetween(<0,1>) = 3*PI/4"); + equal(v10.angleBetween(v1_1), -Math.PI / 4, "<1,0>.angleBetween(<1,-1>) = -PI/4"); +}); + +test("angleTo()", function() { + var v0 = new Vector2D(); + var v11 = new Vector2D(1, 1); + var v10 = new Vector2D(1, 0); + var v0_1 = new Vector2D(0, -1); + + equal(v0.angleTo(v11), Math.PI / 4, "<0,0>.angleTo(<1,1>) = PI/4"); + equal(v10.angleTo(v0_1), -3 * Math.PI / 4, "<1,0>.angleTo(<0,-1>) = -3*PI/4"); +}); + +test("clone()", function() { + var v0 = new Vector2D(); + var v3_7 = new Vector2D(3, -7); + + equal(v0.equals(v0.clone()), true, "<0,0> = <0,0>.clone()"); + equal(v3_7.clone().equals(v3_7), true, "<3,-7>.clone() = <3,-7>"); +}); + +test("distance()", function() { + var v0 = new Vector2D(); + var v10 = new Vector2D(1, 0); + var v11 = new Vector2D(1, 1); + + equal(v10.distance(v11), 1, "<1,0>.distance(<1,1>) = 1"); + equal(v0.distance(v11), Math.sqrt(2), "<0,0>.distance(<1,1>) = sqrt(2)"); +}); + +test("distanceSq()", function() { + var v0 = new Vector2D(); + var v10 = new Vector2D(1, 0); + var v11 = new Vector2D(1, 1); + + equal(v10.distanceSq(v11), 1, "<1,0>.distanceSq(<1,1>) = 1"); + equal(v0.distanceSq(v11), 2, "<0,0>.distanceSq(<1,1>) = 2"); +}); + +test("divide()", function() { + var v12 = new Vector2D(1, 2); + var v34 = new Vector2D(3, 4); + var v = new Vector2D(1 / 3, 2 / 4); + + equal(v12.divide(v34).equals(v), true, "<1,2> / <3,4> = <1/3,1/2>"); +}); + +test("dotProduct()", function() { + var v12 = new Vector2D(1, 2); + var v34 = new Vector2D(3, 4); + var v46 = new Vector2D(4, 6); + + equal(v12.dotProduct(v34), 11, "<1,2>.dotProduct(<3,4>) = 11"); + equal(v34.dotProduct(v46), 36, "<3,4>.dotProduct(<4,6>) = 36"); + equal(v46.dotProduct(v12), 16, "<4,6>.dotProduct(<1,2>) = 16"); +}); + +test("equals()", function() { + var v12 = new Vector2D(1, 2); + var v34 = new Vector2D(3, 4); + var v46 = new Vector2D(4, 6); + + equal(v12.equals(new Vector2D(1, 2)), true, "<1,2>.equals(<1,2>) = true"); + equal(v34.equals(new Vector2D(3, 4)), true, "<3,4>.equals(<3,4>) = true"); + equal(v46.equals(new Vector2D(4, 6)), true, "<4,6>.equals(<4,6>) = true"); +}); + +test("getNormal()", function() { + var v10 = new Vector2D(1, 0); + var v32 = new Vector2D(3, 2); + + equal(v10.getNormal().equals(new Vector2D(0, 1)), true, "<1,0>.getNormal() = <0,1>"); + equal(v10.getNormal(v32).equals((new Vector2D(1, -1)).normalize()), true, "<1,0>.getNormal(<3,2>) = "); +}); + +test("isZero()", function() { + var v0 = new Vector2D(); + var v10 = new Vector2D(1, 0); + + equal(v0.isZero(), true, "<0,0>.isZero() = true"); + equal(v10.isZero(), false, "<1,0>.isZero() = false"); +}); + +test("magnitude()", function() { + var v0 = new Vector2D(); + var v10 = new Vector2D(1, 0); + var v_79 = new Vector2D(-7, 9); + + equal(v0.magnitude(), 0, "<0,0>.magnitude() = 0"); + equal(v10.magnitude(), 1, "<1,0>.magnitude() = 1"); + equal(v_79.magnitude(), 11.40175425099138, "<-7,9>.magnitude() = 11.40175425099138"); +}); + +test("magnitudeSq()", function() { + var v0 = new Vector2D(); + var v10 = new Vector2D(1, 0); + var v_79 = new Vector2D(-7, 9); + + equal(v0.magnitudeSq(), 0, "<0,0>.magnitudeSq() = 0"); + equal(v10.magnitudeSq(), 1, "<1,0>.magnitudeSq() = 1"); + equal(v_79.magnitudeSq(), 130, "<-7,9>.magnitudeSq() = 130"); +}); + +test("multiply()", function() { + var v12 = new Vector2D(1, 2); + var v34 = new Vector2D(3, 4); + var v = new Vector2D(3, 8); + + equal(v12.multiply(v34).equals(v), true, "<1,2> * <3,4> = <3,8>"); +}); + +test("negate()", function() { + var v_79 = new Vector2D(-7, 9); + var v7_9 = new Vector2D(7, -9); + + equal(v_79.negate().equals(v7_9), true, "<-7,9>.negate() = <7,-9>"); +}); + +test("normalize()", function() { + var v0 = new Vector2D(); + var v01 = new Vector2D(0, 1); + var v_79 = new Vector2D(-7, 9); + + equal(v0.normalize().equals(new Vector2D(1, 0)), true, "<0,0>.normalize() = <1,0>"); + equal(v01.normalize().equals(new Vector2D(0, 1)), true, "<1,0>.normalize() = <0,1>"); + equal(v_79.normalize().equals(new Vector2D(-0.6139406135149205, 0.7893522173763263)), true, "<-7,9>.normalize() = <-0.6139406135149205,0.7893522173763263>"); +}); + +test("scale()", function() { + var v11 = new Vector2D(1, 1); + + equal(v11.scale(2).equals(new Vector2D(2, 2)), true, "<1,1>.scale(2) = <2,2>"); + equal(v11.scale(2, -3).equals(new Vector2D(4, -6)), true, "<2,2>.scale(2, -3) = <4,-6>"); +}); + +test("magnitudeSq()", function() { + var v34 = new Vector2D(3, 4); + + equal(v34.normalize().scaleToMagnitude(5).equals(new Vector2D(3, 4)), true, "<3,4>.normalize().scaleToMagnitude(5) = <3,4>"); +}); + +test("setValues", function() { + var v0 = new Vector2D(); + var v12 = new Vector2D(1, 2); + var v44 = new Vector2D(4, 4); + + equal(v0.setValues(1, 2).equals(v12), true, "<0,0>.setValues(<1,2>) = <1,2>"); + equal(v0.setValues(v44).equals(v44), true, "<1,2>.setValues(<4,4>) = <4,4>"); +}); + +test("subtract()", function() { + var v12 = new Vector2D(1, 2); + var v34 = new Vector2D(3, 4); + var v = new Vector2D(-2, -2); + + equal(v12.subtract(v34).equals(v), true, "<1,2> - <3,4> = <-2,-2>"); +}); + +test("toString()", function() { + var v12 = new Vector2D(1, 2); + + equal(v12.toString(), "Vector2D(1, 2)", "<1,2> = \"Vector2D(1, 2)\""); +}); + +test("translate()", function() { + var v11 = new Vector2D(1, 1); + + equal(v11.translate(2).equals(new Vector2D(3, 3)), true, "<1,1>.translate(2) = <3,3>"); + equal(v11.translate(2, -3).equals(new Vector2D(5, 0)), true, "<2,2>.translate(2, -3) = <5,0>"); +}); + +test("tripleProduct()", function() { + var va = new Vector2D(1, 2); + var vb = new Vector2D(3, 4); + var vc = new Vector2D(5, 6); + var vtp = new Vector2D(12, -10); + + equal(Vector2D.tripleProduct(va, vb, vc).equals(vtp), true, "tripleProduct(<1,2>, <3,4>, <5,6>) = <10,-12>"); +}); + +module("Matrix2D"); + +test("apply()", function() { + equal((new Matrix2D()).rotate(Math.PI / 2).apply(new Vector2D(1, 2)).equals(new Vector2D(-2, 1.0000000000000002)), + true, "(new Matrix2D()).rotate(Math.PI/2).apply(new Vector2D(1, 2)).equals(new Vector2D(-2, 1.0000000000000002))"); +}); + +test("clone()", function() { + equal((new Matrix2D(1, 2, 3, 4, 5, 6)).clone().equals(new Matrix2D(1, 2, 3, 4, 5, 6)), + true, "(new Matrix2D(1, 2, 3, 4, 5, 6)).clone().equals(new Matrix2D(1, 2, 3, 4, 5, 6))"); +}); + +test("combine()", function() { + equal((new Matrix2D()).scale(2).combine((new Matrix2D()).rotate(0.75)).equals((new Matrix2D()).scale(2).rotate(0.75)), + true, "(new Matrix2D()).scale(2).combine((new Matrix2D()).rotate(0.75)).equals((new Matrix2D()).scale(2).rotate(0.75))"); +}); + +test("equals()", function() { + equal((new Matrix2D()).equals(new Matrix2D()), + true, "(new Matrix2D()).equals(new Matrix2D())"); + equal((new Matrix2D()).scale(2).equals(new Matrix2D()), + false, "(new Matrix2D()).scale(2).equals(new Matrix2D())"); +}); + +test("determinant()", function() { + equal((new Matrix2D()).scale(2, 3).rotate(Math.PI / 2).determinant(), + 6, "(new Matrix2D()).scale(2, 3).rotate(Math.PI / 2).determinant()"); +}); + +test("invert()", function() { + var m = new Matrix2D(4, 3, 3, 2, 0, 0); + var m2 = new Matrix2D(-2, 3, 3, -4, 0, 0); + ok( m.invert().equals(m2), "Matrix (4,3,3,2) inverts to (-2,3,3,-4)"); +}); + +test("isIdentity()", function() { + equal((new Matrix2D()).isIdentity(), + true, "(new Matrix2D()).isIdentity()"); + equal((new Matrix2D()).scale(2).isIdentity(), + false, "(new Matrix2D()).scale(2).isIdentity()"); +}); + +test("isInvertible()", function() { + equal((new Matrix2D()).scale(2, 3).rotate(Math.PI / 2).isInvertible(), + true, "(new Matrix2D()).scale(2, 3).rotate(Math.PI / 2).isInvertible()"); + equal((new Matrix2D()).scale(0, 3).rotate(Math.PI / 2).isInvertible(), + false, "(new Matrix2D()).scale(0, 3).rotate(Math.PI / 2).isInvertible()"); +}); + +test("preRotate()", function() { + equal((new Matrix2D()).preRotate(0).equals(new Matrix2D()), + true, "(new Matrix2D()).preRotate(0).equals(new Matrix2D())"); + equal((new Matrix2D()).preRotate(Math.PI / 2).equals((new Matrix2D()).rotate(Math.PI / 2)), + true, "(new Matrix2D()).preRotate(Math.PI / 2).equals((new Matrix2D()).rotate(Math.PI / 2))"); +}); + +test("preScale()", function() { + equal((new Matrix2D()).preScale(2).equals(new Matrix2D(2, 0, 0, 2, 0, 0)), + true, "(new Matrix2D()).preScale(2).equals(new Matrix2D(2, 0, 0, 2, 0, 0))"); + equal((new Matrix2D()).preScale(2.5).equals((new Matrix2D()).scale(2.5)), + true, "(new Matrix2D()).preScale(2.5).equals((new Matrix2D()).scale(2.5))"); +}); + +test("preTranslate()", function() { + equal((new Matrix2D()).preTranslate(1, 2).equals(new Matrix2D(1, 0, 0, 1, 1, 2)), + true, "(new Matrix2D()).preTranslate(1, 2).equals(new Matrix2D(1, 0, 0, 1, 1, 2)"); + equal((new Matrix2D()).preTranslate(1, 2).equals((new Matrix2D()).translate(new Vector2D(1, 2))), + true, "(new Matrix2D()).preTranslate(1, 2).equals((new Matrix2D()).translate(new Vector2D(1, 2)))"); + equal((new Matrix2D()).preTranslate(new Vector2D(1, 2)).equals(new Matrix2D(1, 0, 0, 1, 1, 2)), + true, "(new Matrix2D()).preTranslate(new Vector2D(1, 2)).equals(new Matrix2D(1, 0, 0, 1, 1, 2))"); + equal((new Matrix2D()).preTranslate(new Vector2D(1, 2)).equals((new Matrix2D()).translate(new Vector2D(1, 2))), + true, "(new Matrix2D()).preTranslate(new Vector2D(1, 2)).equals((new Matrix2D()).translate(new Vector2D(1, 2)))"); +}); + +test("rotate()", function() { + equal((new Matrix2D()).rotate(0).equals(new Matrix2D()), + true, "(new Matrix2D()).rotate(0).equals(new Matrix2D())"); +}); + +test("scale()", function() { + equal((new Matrix2D()).scale(2, 3).equals(new Matrix2D(2, 0, 0, 3, 0, 0)), + true, "(new Matrix2D()).scale(2, 3).equals(new Matrix2D(2, 0, 0, 3, 0, 0))"); +}); + +test("setValues()", function() { + equal((new Matrix2D()).setValues(1, 2, 3, 4, 5, 6).equals(new Matrix2D(1, 2, 3, 4, 5, 6)), + true, "(new Matrix2D()).setValues(1, 2, 3, 4, 5, 6).equals(new Matrix2D(1, 2, 3, 4, 5, 6))"); +}); + +test("toString()", function() { + equal((new Matrix2D()).toString(), + "Matrix2D([1, 0, 0] [0, 1, 0] [0, 0, 1])", "(new Matrix2D()).toString()"); +}); + +test("translate()", function() { + equal((new Matrix2D()).translate(1, 2).equals(new Matrix2D(1, 0, 0, 1, 1, 2)), + true, "(new Matrix2D()).translate(1, 2).equals(new Matrix2D(1, 0, 0, 1, 1, 2))"); + equal((new Matrix2D()).translate(new Vector2D(1, 2)).equals(new Matrix2D(1, 0, 0, 1, 1, 2)), + true, "(new Matrix2D()).translate(new Vector2D(1, 2)).equals(new Matrix2D(1, 0, 0, 1, 1, 2))"); +}); \ No newline at end of file diff --git a/tests/playground.html b/tests/playground.html deleted file mode 100755 index 35bbadc4..00000000 --- a/tests/playground.html +++ /dev/null @@ -1,7 +0,0 @@ - - - - - - - \ No newline at end of file diff --git a/tests/sound.html b/tests/sound.html deleted file mode 100644 index 3c1ff098..00000000 --- a/tests/sound.html +++ /dev/null @@ -1,122 +0,0 @@ - - - - - - - - - - - - -

                Crafty: Core

                -

                -
                -

                -
                  -
                  test markup, will be hidden
                  - - diff --git a/tests/stage.html b/tests/stage.html deleted file mode 100644 index aa393f60..00000000 --- a/tests/stage.html +++ /dev/null @@ -1,145 +0,0 @@ - - - - - - - - - - - - -

                  Crafty: Stage

                  -

                  -
                  -

                  -
                    -
                    test markup, will be hidden
                    - - diff --git a/tests/stage.js b/tests/stage.js new file mode 100644 index 00000000..e1957aed --- /dev/null +++ b/tests/stage.js @@ -0,0 +1,248 @@ +var reset = function() { + Crafty.viewport.reset(); + Crafty.viewport.scroll('_x', 0); + Crafty.viewport.scroll('_y', 0); + Crafty.viewport.clampToEntities = true; + +}; + +test("simulateFrames", function() { + var framesPlayed = 0; + Crafty.bind("EnterFrame", function() { + framesPlayed++; + }); + Crafty.timer.simulateFrames(1); + equal(framesPlayed, 1, "A frame should have been simulated"); + + Crafty.timer.simulateFrames(100); + equal(framesPlayed, 101, "101 frames should have been simulated"); +}); + +module("Viewport", { + setup: function() { + reset(); + }, + teardown: function() { + // clean up after each test + Crafty("*").destroy(); + } +}); + +test("scroll using _x, _y", function() { + var e = Crafty.e("2D, DOM").attr({ + x: 50, + y: 50 + }); + var before = Crafty.DOM.translate(e.x, e.y); + + Crafty.viewport.scroll('_x', 100); + equal(before.x - Crafty.DOM.translate(e.x, e.y).x, 100, "Scroll in x direction"); + + Crafty.viewport.scroll('_y', 70); + equal(before.y - Crafty.DOM.translate(e.x, e.y).y, 70, "Scroll in y direction"); + + Crafty.viewport.scroll('_x', 0); + Crafty.viewport.scroll('_y', 0); + equal(before.x - Crafty.DOM.translate(e.x, e.y).x, 0, "Scroll to 0"); + equal(before.y - Crafty.DOM.translate(e.x, e.y).y, 0, "Scroll to 0"); +}); + +test("scroll using x, y", function() { + var e = Crafty.e("2D, DOM").attr({ + x: 50, + y: 50 + }); + var before = Crafty.DOM.translate(e.x, e.y); + + Crafty.viewport.scroll('x', 100); + equal(before.x - Crafty.DOM.translate(e.x, e.y).x, 100, "Scroll in x direction"); + + Crafty.viewport.scroll('y', 70); + equal(before.y - Crafty.DOM.translate(e.x, e.y).y, 70, "Scroll in y direction"); + + Crafty.viewport.scroll('x', 0); + Crafty.viewport.scroll('y', 0); + equal(before.x - Crafty.DOM.translate(e.x, e.y).x, 0, "Scroll to 0"); + equal(before.y - Crafty.DOM.translate(e.x, e.y).y, 0, "Scroll to 0"); +}); + +test("Viewport resizing", function(){ + var flag = 0; + var e = Crafty("2D, Canvas"); + Crafty.canvas.init(); + + var w = Crafty.viewport.width; + + equal( Crafty.canvas._canvas.width, Crafty.viewport.width, "Initial canvas size matches viewport"); + equal(Crafty.stage.elem.style.width, Crafty.viewport.width + "px", "Initial stage size matches viewport"); + Crafty.bind("ViewportResize", function(){flag++;}); + + Crafty.viewport.width += 10; + + equal(flag, 1, "ViewportResize triggered"); + equal(Crafty.viewport.width, w+10, "Viewport increased in width"); + equal( Crafty.canvas._canvas.width, Crafty.viewport.width , "Canvas size matches viewport after change"); + equal(Crafty.stage.elem.style.width, Crafty.viewport.width +"px", "Stage size matches viewport after change"); + + var h = Crafty.viewport.height; + + Crafty.viewport.height += 10; + + equal(flag, 2, "ViewportResize triggered"); + equal(Crafty.viewport.height, h+10, "Viewport increased in width"); + equal( Crafty.canvas._canvas.height, Crafty.viewport.height , "Canvas size matches viewport after change"); + equal(Crafty.stage.elem.style.height, Crafty.viewport.height +"px", "Stage size matches viewport after change"); + +}); + +test("follow", function() { + Crafty.viewport.clampToEntities = false; + var e = Crafty.e("2D, DOM").attr({ + x: Crafty.viewport.width + 100, + y: Crafty.viewport.height + 70 + }); + Crafty.viewport.follow(e, 0, 0); + equal(Crafty.viewport._x, (-(Crafty.viewport.width / 2 + 100)), "Center viewport on entity.x"); + equal(Crafty.viewport._y, (-(Crafty.viewport.height / 2 + 70)), "Center viewport on entity.y"); + +}); + +test("pan", function() { + Crafty.viewport.clampToEntities = false; + Crafty.e("2D, DOM").attr({ + x: 0, + y: 0, + w: Crafty.viewport.width * 2, + h: Crafty.viewport.height * 2 + }); + + var done = 0; + var panDone = function() { + done++; + }; + Crafty.one("CameraAnimationDone", panDone); + + Crafty.viewport.pan(100, 0, 10 * 20); + Crafty.timer.simulateFrames(5); + equal(Crafty.viewport._x, -50, "Pan half the way on half the time"); + equal(done, 0, "CameraAnimationDone hasn't fired yet"); + Crafty.timer.simulateFrames(5); + equal(Crafty.viewport._x, -100, "Pan all the way when all the time is spent"); + equal(done, 1, "CameraAnimationDone has fired once"); + + done = 0; + Crafty.one("CameraAnimationDone", panDone); + Crafty.viewport.pan(0, 100, 10); + Crafty.timer.simulateFrames(20); + equal(Crafty.viewport._y, -100, "Pan all the way and stay there"); + equal(done, 1, "CameraAnimationDone has fired once"); + + Crafty.viewport.scroll('x', 0); + Crafty.viewport.scroll('y', 0); +}); + +test("zoom", function() { + + Crafty.viewport.clampToEntities = false; + + var done = 0; + var panDone = function() { + done++; + }; + Crafty.one("CameraAnimationDone", panDone); + + Crafty.e("2D, DOM").attr({ + x: 0, + y: 0, + w: Crafty.viewport.width * 2, + h: Crafty.viewport.height * 2 + }); + Crafty.viewport.scroll('x', 0); + Crafty.viewport.scroll('y', 0); + Crafty.viewport.scale(1); + + Crafty.viewport.zoom(2, 0, 0, 10 * 20); + Crafty.timer.simulateFrames(5); + + equal(Crafty.viewport._scale, Math.sqrt(2), "Zooms sqrt(2) in half the time"); + equal(done, 0, "CameraAnimationDone hasn't fired yet"); + + Crafty.timer.simulateFrames(5); + equal(Crafty.viewport._x, Crafty.viewport.width / 4, "move all the way when all the time is spent"); + equal(Crafty.viewport._y, Crafty.viewport.height / 4, "move all the way when all the time is spent"); + equal(Crafty.viewport._scale, 2, "Zooms all the way in full time."); + equal(done, 1, "CameraAnimationDone has fired once"); + +}); + +test("centerOn", function() { + var e = Crafty.e("2D, DOM").attr({ + x: 0, + y: 0, + w: Crafty.viewport.width * 2, + h: Crafty.viewport.height * 2 + }); + Crafty.viewport.clampToEntities = false; + + var done = 0; + var panDone = function() { + done++; + }; + Crafty.one("CameraAnimationDone", panDone); + + + Crafty.viewport.centerOn(e, 10); + Crafty.timer.simulateFrames(10); + + equal(Crafty.viewport._x, -e.w / 2 + Crafty.viewport.width / 2, "Entity centered after exact duration"); + equal(done, 1, "CameraAnimationDone has fired once"); + done = 0; + Crafty.one("CameraAnimationDone", panDone); + Crafty.timer.simulateFrames(10); + equal(Crafty.viewport._x, -e.w / 2 + Crafty.viewport.width / 2, "Entity still centered 10 frames later"); + equal(done, 0, "CameraAnimationDone doesn't fire after completion"); + + var e2 = Crafty.e("2D, DOM").attr({ + x: 450, + y: 450, + w: 20, + h: 20 + }); + Crafty.viewport.scroll('x', 1500); + Crafty.viewport.scroll('y', 300); + Crafty.viewport.centerOn(e2, 1); + Crafty.timer.simulateFrames(1); + equal(Crafty.viewport._x, (-(e2.x + e2.w / 2 - Crafty.viewport.width / 2)), "Entity centered from non-zero origin"); + equal(Crafty.viewport._y, (-(e2.y + e2.h / 2 - Crafty.viewport.height / 2)), "Entity centered from non-zero origin"); + + Crafty.viewport.clampToEntities = true; + Crafty.viewport.scroll('x', 0); + Crafty.viewport.scroll('y', 0); +}); + +module("Crafty.timer", { + setup: function() { + // prepare something for all following tests + }, + teardown: function() { + // clean up after each test + Crafty("*").destroy(); + } +}); +test("curTime", 1, function() { + var startTime, lastKnownTime; + Crafty.e("").bind("EnterFrame", function(params) { + if (!startTime) { + startTime = params.gameTime; + } else { + lastKnownTime = params.gameTime; + } + }); + + setTimeout(function() { + var endTime = lastKnownTime; + ok(endTime > startTime, "EndTime " + endTime + " must be larger than StartTime " + startTime); + start(); + }, 100); + stop(); // pause the QUnit so the timeout has time to complete. +}); \ No newline at end of file diff --git a/tests/storage.html b/tests/storage.html deleted file mode 100644 index 88b982a6..00000000 --- a/tests/storage.html +++ /dev/null @@ -1,85 +0,0 @@ - - - - - - - - - - - - -

                    Crafty: Core

                    -

                    -
                    -

                    -
                      -
                      test markup, will be hidden
                      - - diff --git a/tests/storage.js b/tests/storage.js new file mode 100644 index 00000000..da43b83e --- /dev/null +++ b/tests/storage.js @@ -0,0 +1,33 @@ +module("Storage", { + setup: function() { + reset(); + }, + teardown: function() { + // clean up after each test + Crafty("*").destroy(); + } +}); + +test('get a value', function(){ + Crafty.storage('name', 'test'); + var name = Crafty.storage('name'); + + equal(name, 'test', 'the values should be equal'); + + Crafty.storage.remove('name'); +}); + +test('get null when a value does not exist', function(){ + var name = Crafty.storage('notexisting'); + equal(name, null, 'should be null'); +}); + +test('remove an value', function(){ + var person = Crafty.storage('person', 'test'); + equal(Crafty.storage('person'), 'test', 'person should be defined'); + + Crafty.storage.remove('person'); + + var savedperson = Crafty.storage('person'); + equal(savedperson, null, 'should be null because we just removed the value'); +}); \ No newline at end of file diff --git a/tests/text.html b/tests/text.html deleted file mode 100644 index dae3c237..00000000 --- a/tests/text.html +++ /dev/null @@ -1,66 +0,0 @@ - - - - - - - - - - - - -

                      Crafty: Core

                      -

                      -
                      -

                      -
                        -
                        test markup, will be hidden
                        - - diff --git a/tests/text.js b/tests/text.js new file mode 100644 index 00000000..aefbd9f8 --- /dev/null +++ b/tests/text.js @@ -0,0 +1,48 @@ +module("Text", { + setup: function() { + // prepare something for all following tests + }, + teardown: function() { + // clean up after each test + Crafty("*").destroy(); + } +}); + +test("fontFamily", function() { + var text = Crafty.e('DOM, Text').textFont({ + family: 'Times New Roman 400', + size: '30px' + }).text('Test'); + equal(text.attr('_textFont').family, "'Times New Roman 400'", 'Expect to have singlequotes arount the family property.'); + +}); + +test("_getFontHeight", function() { + var e = Crafty.e("Text"); + var h = e._getFontHeight("10px"); + equal(h, 10, "Font height is 10 pixels"); + h = e._getFontHeight("10in"); + equal(h, 960, "Font height is 960 pixels"); +}); + +test("Width of canvas element", function() { + var e = Crafty.e("2D, Canvas, Text"); + e.text("a"); + var w1 = e.w; + e.text("abc"); + var w2 = e.w; + ok(w2 > w1, "Entity increases in width when text is changed."); +}); + +test("Height of canvas element", function() { + var e = Crafty.e("2D, Canvas, Text"); + e.text("a"); + e.textFont("size", "10"); + var h1 = e.h; + ok(h1 > 10, "Font height set correctly."); + e.textFont("size", "20"); + var h2 = e.h; + ok(h2 > 20, "Font height set correctly."); + ok(h2 > h1, "Entity increases in height when font size is increased."); + +}); \ No newline at end of file diff --git a/tests/touchevents.html b/tests/touchevents.html index fd8f3089..ee85c449 100644 --- a/tests/touchevents.html +++ b/tests/touchevents.html @@ -1,28 +1,28 @@ - - + + \ No newline at end of file diff --git a/tests/tween.js b/tests/tween.js new file mode 100644 index 00000000..0c49a97b --- /dev/null +++ b/tests/tween.js @@ -0,0 +1,67 @@ +module("Tween", { + setup: function() { + // prepare something for all following tests + }, + teardown: function() { + // clean up after each test + Crafty("*").destroy(); + } +}); + +test("Tween", function() { + var e = Crafty.e("2D, Tween"); + e.x = 0; + e.y = 10; + var ret = e.tween({ + x: 10, + y: 16 + }, 200); // 10 frames == 200 ms by efault + equal(ret, e, ".tween() returned self correctly"); + Crafty.timer.simulateFrames(5); + equal(Round(e.x), 5, "Halfway tweened x"); + equal(Round(e.y), 13, "Halfway tweened y"); + Crafty.timer.simulateFrames(10); + equal(e.x, 10, "Fully tweened x"); + equal(e.y, 16, "Fully tweened y"); +}); +asyncTest('correct tweening', function() { + expect(1); + + var e1 = Crafty.e('2D, Tween') + .attr({ + x: 0, + y: 0 + }) + .tween({ + x: 100 + }, 50); + e1.bind('TweenEnd', function() { + ok(this.x === 100); + start(); + }); +}); + +asyncTest('correct tweening with multiple entities', function() { + expect(1); + + var e1 = Crafty.e('2D, Tween') + .attr({ + x: 0, + y: 0 + }) + .tween({ + x: 100 + }, 50); + var e2 = Crafty.e('2D, Tween') + .attr({ + x: 0, + y: 0 + }) + .tween({ + x: 100 + }, 50); + e1.bind('TweenEnd', function() { + ok(this.x === 100); + start(); + }); +}); \ No newline at end of file