1 var jaws = (function(jaws) { 2 3 /** 4 * @class A basic but powerfull sprite for all your onscreen-game objects. "Field Summary" contains options for the Sprite()-constructor. 5 * @constructor 6 * 7 * @property {int} x Horizontal position (0 = furthest left) 8 * @property {int} y Vertical position (0 = top) 9 * @property {image} image Image/canvas or string pointing to an asset ("player.png") 10 * @property {int} alpha Transparency 0=fully transparent, 1=no transperency 11 * @property {int} angle Angle in degrees (0-360) 12 * @property {bool} flipped Flip sprite horizontally, usefull for sidescrollers 13 * @property {string} anchor String stating how to anchor the sprite to canvas, @see Sprite#anchor ("top_left", "center" etc) 14 * @property {int} scale_image Scale the sprite by this factor 15 * @property {string,gradient} color If set, draws a rectangle of dimensions rect() with specified color or gradient (linear or radial) 16 * 17 * @example 18 * // create new sprite at top left of the screen, will use jaws.assets.get("foo.png") 19 * new Sprite({image: "foo.png", x: 0, y: 0}) 20 * 21 * // sets anchor to "center" on creation 22 * new Sprite({image: "topdownspaceship.png", anchor: "center"}) 23 * 24 */ 25 jaws.Sprite = function Sprite(options) { 26 if( !(this instanceof arguments.callee) ) return new arguments.callee( options ); 27 this.set(options) 28 this.context = options.context ? options.context : jaws.context; // Prefer given canvas-context, fallback to jaws.context 29 } 30 31 jaws.Sprite.prototype.default_options = { 32 x: 0, 33 y: 0, 34 alpha: 1, 35 angle: 0, 36 flipped: false, 37 anchor_x: 0, 38 anchor_y: 0, 39 image: null, 40 image_path: null, 41 anchor: null, 42 scale_image: null, 43 damping: 1, 44 scale_x: 1, 45 scale_y: 1, 46 scale: 1, 47 color: "#ddd", 48 width: 16, 49 height: 16, 50 _constructor: null, 51 context: null, 52 data: null 53 } 54 55 /** 56 * @private 57 * Call setters from JSON object. Used to parse options. 58 */ 59 jaws.Sprite.prototype.set = function(options) { 60 if(jaws.isString(this.image)) this.image_path = this.image; 61 jaws.parseOptions(this, options, this.default_options); 62 63 if(this.scale) this.scale_x = this.scale_y = this.scale; 64 if(this.image) this.setImage(this.image); 65 if(this.scale_image) this.scaleImage(this.scale_image); 66 if(this.anchor) this.setAnchor(this.anchor); 67 68 if(!this.image && this.color && this.width && this.height) { 69 var canvas = document.createElement('canvas'); 70 var context = canvas.getContext('2d'); 71 canvas.width = this.width; 72 canvas.height = this.height; 73 context.fillStyle = this.color; 74 context.fillRect(0, 0, this.width, this.height); 75 this.image = canvas; 76 } 77 78 this.cacheOffsets() 79 80 return this 81 } 82 83 /** 84 * @private 85 * 86 * Creates a new sprite from current sprites attributes() 87 * Checks JawsJS magic property '_constructor' when deciding with which constructor to create it 88 * 89 */ 90 jaws.Sprite.prototype.clone = function(object) { 91 var constructor = this._constructor ? eval(this._constructor) : this.constructor 92 var new_sprite = new constructor( this.attributes() ); 93 new_sprite._constructor = this._constructor || this.constructor.name 94 return new_sprite 95 } 96 97 98 /** 99 * Sets image from image/canvas or asset-string ("foo.png") 100 * If asset isn't previously loaded setImage() will try to load it. 101 */ 102 jaws.Sprite.prototype.setImage = function(value) { 103 var that = this 104 105 // An image, great, set this.image and return 106 if(jaws.isDrawable(value)) { 107 this.image = value 108 return this.cacheOffsets() 109 } 110 // Not an image, therefore an asset string, i.e. "ship.bmp" 111 else { 112 // Assets already loaded? Set this.image 113 if(jaws.assets.isLoaded(value)) { this.image = jaws.assets.get(value); this.cacheOffsets(); } 114 115 // Not loaded? Load it with callback to set image. 116 else { 117 jaws.log.warn("Image '" + value + "' not preloaded with jaws.assets.add(). Image and a working sprite.rect() will be delayed.") 118 jaws.assets.load(value, {onload: function() { that.image = jaws.assets.get(value); that.cacheOffsets();} } ) 119 } 120 } 121 return this 122 } 123 124 /** 125 * Steps 1 pixel towards the given X/Y. Horizontal and vertical steps are done separately between each callback. 126 * Exits when the continueStep-callback returns true for both vertical and horizontal steps or if target X/Y has been reached. 127 * 128 * @returns {object} Object with 2 x/y-properties indicating what plane we moved in when stepToWhile was stopped. 129 */ 130 jaws.Sprite.prototype.stepToWhile = function(target_x, target_y, continueStep) { 131 var step = 1; 132 var step_x = (target_x < this.x) ? -step : step; 133 var step_y = (target_y < this.y) ? -step : step; 134 135 target_x = parseInt(target_x) 136 target_y = parseInt(target_y) 137 138 var collision_x = false; 139 var collision_y = false; 140 141 while( true ) { 142 if(collision_x === false) { 143 if(this.x != target_x) { this.x += step_x } 144 if( !continueStep(this) ) { this.x -= step_x; collision_x = true } 145 } 146 147 if(collision_y === false) { 148 if(this.y != target_y) { this.y += step_y } 149 if( !continueStep(this) ) { this.y -= step_y; collision_y = true } 150 } 151 152 if( (collision_x || this.x == target_x) && (collision_y || this.y == target_y) ) 153 return {x: collision_x, y: collision_y}; 154 } 155 } 156 /** 157 * Moves with given vx/vy velocoties by stepping 1 pixel at the time. Horizontal and vertical steps are done separately between each callback. 158 * Exits when the continueStep-callback returns true for both vertical and horizontal steps or if target X/Y has been reached. 159 * 160 * @returns {object} Object with 2 x/y-properties indicating what plane we moved in when stepWhile was stopped. 161 */ 162 jaws.Sprite.prototype.stepWhile = function(vx, vy, continueStep) { 163 return this.stepToWhile(this.x + vx, this.y + vy, continueStep) 164 } 165 166 /** Flips image vertically, usefull for sidescrollers when player is walking left/right */ 167 jaws.Sprite.prototype.flip = function() { this.flipped = this.flipped ? false : true; return this } 168 jaws.Sprite.prototype.flipTo = function(value) { this.flipped = value; return this } 169 /** Rotate sprite by value degrees */ 170 jaws.Sprite.prototype.rotate = function(value) { this.angle += value; return this } 171 /** Force an rotation-angle on sprite */ 172 jaws.Sprite.prototype.rotateTo = function(value) { this.angle = value; return this } 173 174 /** Set x/y */ 175 jaws.Sprite.prototype.moveTo = function(x, y) { 176 if(jaws.isArray(x) && y === undefined) { 177 y = x[1] 178 x = x[0] 179 } 180 this.x = x; 181 this.y = y; 182 return this; 183 } 184 /** Modify x/y */ 185 jaws.Sprite.prototype.move = function(x, y) { 186 if(jaws.isArray(x) && y === undefined) { 187 y = x[1] 188 x = x[0] 189 } 190 191 if(x) this.x += x; 192 if(y) this.y += y; 193 return this 194 } 195 /** 196 * scale sprite by given factor. 1=don't scale. <1 = scale down. 1>: scale up. 197 * Modifies width/height. 198 **/ 199 jaws.Sprite.prototype.scaleAll = function(value) { this.scale_x *= value; this.scale_y *= value; return this.cacheOffsets() } 200 /** set scale factor. ie. 2 means a doubling if sprite in both directions. */ 201 jaws.Sprite.prototype.scaleTo = function(value) { this.scale_x = this.scale_y = value; return this.cacheOffsets() } 202 /** scale sprite horizontally by scale_factor. Modifies width. */ 203 jaws.Sprite.prototype.scaleWidth = function(value) { this.scale_x *= value; return this.cacheOffsets() } 204 /** scale sprite vertically by scale_factor. Modifies height. */ 205 jaws.Sprite.prototype.scaleHeight = function(value) { this.scale_y *= value; return this.cacheOffsets() } 206 207 /** Sets x */ 208 jaws.Sprite.prototype.setX = function(value) { this.x = value; return this } 209 /** Sets y */ 210 jaws.Sprite.prototype.setY = function(value) { this.y = value; return this } 211 212 /** Position sprites top on the y-axis */ 213 jaws.Sprite.prototype.setTop = function(value) { this.y = value + this.top_offset; return this } 214 /** Position sprites bottom on the y-axis */ 215 jaws.Sprite.prototype.setBottom = function(value) { this.y = value - this.bottom_offset; return this } 216 /** Position sprites left side on the x-axis */ 217 jaws.Sprite.prototype.setLeft = function(value) { this.x = value + this.left_offset; return this } 218 /** Position sprites right side on the x-axis */ 219 jaws.Sprite.prototype.setRight = function(value) { this.x = value - this.right_offset; return this } 220 221 /** Set new width. Scales sprite. */ 222 jaws.Sprite.prototype.setWidth = function(value) { this.scale_x = value/this.image.width; return this.cacheOffsets() } 223 /** Set new height. Scales sprite. */ 224 jaws.Sprite.prototype.setHeight = function(value) { this.scale_y = value/this.image.height; return this.cacheOffsets() } 225 /** Resize sprite by adding width */ 226 jaws.Sprite.prototype.resize = function(width, height) { 227 if(jaws.isArray(width) && height === undefined) { 228 height = width[1] 229 width = width[0] 230 } 231 232 this.scale_x = (this.width + width) / this.image.width 233 this.scale_y = (this.height + height) / this.image.height 234 return this.cacheOffsets() 235 } 236 /** 237 * Resize sprite to exact width/height 238 */ 239 jaws.Sprite.prototype.resizeTo = function(width, height) { 240 if(jaws.isArray(width) && height === undefined) { 241 height = width[1] 242 width = width[0] 243 } 244 245 this.scale_x = width / this.image.width 246 this.scale_y = height / this.image.height 247 return this.cacheOffsets() 248 } 249 250 /** 251 * The sprites anchor could be describe as "the part of the sprite will be placed at x/y" 252 * or "when rotating, what point of the of the sprite will it rotate round" 253 * 254 * @example 255 * For example, a topdown shooter could use setAnchor("center") --> Place middle of the ship on x/y 256 * .. and a sidescroller would probably use setAnchor("center_bottom") --> Place "feet" at x/y 257 */ 258 jaws.Sprite.prototype.setAnchor = function(value) { 259 var anchors = { 260 top_left: [0,0], 261 left_top: [0,0], 262 center_left: [0,0.5], 263 left_center: [0,0.5], 264 bottom_left: [0,1], 265 left_bottom: [0,1], 266 top_center: [0.5,0], 267 center_top: [0.5,0], 268 center_center: [0.5,0.5], 269 center: [0.5,0.5], 270 bottom_center: [0.5,1], 271 center_bottom: [0.5,1], 272 top_right: [1,0], 273 right_top: [1,0], 274 center_right: [1,0.5], 275 right_center: [1,0.5], 276 bottom_right: [1,1], 277 right_bottom: [1,1] 278 } 279 280 if(a = anchors[value]) { 281 this.anchor_x = a[0] 282 this.anchor_y = a[1] 283 if(this.image) this.cacheOffsets(); 284 } 285 return this 286 } 287 288 /** @private */ 289 jaws.Sprite.prototype.cacheOffsets = function() { 290 if(!this.image) { return } 291 292 this.width = this.image.width * this.scale_x 293 this.height = this.image.height * this.scale_y 294 this.left_offset = this.width * this.anchor_x 295 this.top_offset = this.height * this.anchor_y 296 this.right_offset = this.width * (1.0 - this.anchor_x) 297 this.bottom_offset = this.height * (1.0 - this.anchor_y) 298 299 if(this.cached_rect) this.cached_rect.resizeTo(this.width, this.height); 300 return this 301 } 302 303 /** Returns a jaws.Rect() perfectly surrouning sprite. Also cache rect in this.cached_rect. */ 304 jaws.Sprite.prototype.rect = function() { 305 if(!this.cached_rect && this.width) this.cached_rect = new jaws.Rect(this.x, this.y, this.width, this.height); 306 if(this.cached_rect) this.cached_rect.moveTo(this.x - this.left_offset, this.y - this.top_offset); 307 return this.cached_rect 308 } 309 310 /** Draw sprite on active canvas */ 311 jaws.Sprite.prototype.draw = function() { 312 if(!this.image) { return this } 313 314 this.context.save() 315 this.context.translate(this.x, this.y) 316 if(this.angle!=0) { jaws.context.rotate(this.angle * Math.PI / 180) } 317 this.flipped && this.context.scale(-1, 1) 318 this.context.globalAlpha = this.alpha 319 this.context.translate(-this.left_offset, -this.top_offset) // Needs to be separate from above translate call cause of flipped 320 this.context.drawImage(this.image, 0, 0, this.width, this.height) 321 this.context.restore() 322 return this 323 } 324 325 /** 326 * Scales image using hard block borders. Useful for that cute, blocky retro-feeling. 327 * Depends on gfx.js beeing loaded. 328 */ 329 jaws.Sprite.prototype.scaleImage = function(factor) { 330 if(!this.image) return; 331 this.setImage( jaws.retroScaleImage(this.image, factor) ) 332 return this 333 } 334 335 /** 336 * Returns sprite as a canvas context. 337 * For certain browsers, a canvas context is faster to work with then a pure image. 338 */ 339 jaws.Sprite.prototype.asCanvasContext = function() { 340 var canvas = document.createElement("canvas") 341 canvas.width = this.width 342 canvas.height = this.height 343 344 var context = canvas.getContext("2d") 345 if(jaws.context) context.mozImageSmoothingEnabled = jaws.context.mozImageSmoothingEnabled; 346 347 context.drawImage(this.image, 0, 0, this.width, this.height) 348 return context 349 } 350 351 /** 352 * Returns sprite as a canvas 353 */ 354 jaws.Sprite.prototype.asCanvas = function() { 355 var canvas = document.createElement("canvas") 356 canvas.width = this.width 357 canvas.height = this.height 358 359 var context = canvas.getContext("2d") 360 if(jaws.context) context.mozImageSmoothingEnabled = jaws.context.mozImageSmoothingEnabled; 361 362 context.drawImage(this.image, 0, 0, this.width, this.height) 363 return canvas 364 } 365 366 jaws.Sprite.prototype.toString = function() { return "[Sprite " + this.x.toFixed(2) + ", " + this.y.toFixed(2) + ", " + this.width + ", " + this.height + "]" } 367 368 /** returns Sprites state/properties as a pure object */ 369 jaws.Sprite.prototype.attributes = function() { 370 var object = {} // Starting with this.options could create circular references through "context" 371 object["_constructor"] = this._constructor || "jaws.Sprite" 372 object["x"] = parseFloat(this.x.toFixed(2)) 373 object["y"] = parseFloat(this.y.toFixed(2)) 374 object["image"] = this.image_path 375 object["alpha"] = this.alpha 376 object["flipped"] = this.flipped 377 object["angle"] = parseFloat(this.angle.toFixed(2)) 378 object["scale_x"] = this.scale_x; 379 object["scale_y"] = this.scale_y; 380 object["anchor_x"] = this.anchor_x 381 object["anchor_y"] = this.anchor_y 382 383 if(this.data !== null) object["data"] = jaws.clone(this.data); // For external data (for example added by the editor) that you want serialized 384 385 return object 386 } 387 /** 388 * Load/creates sprites from given data 389 * 390 * Argument could either be 391 * - an array of Sprite objects 392 * - an array of JSON objects 393 * - a JSON.stringified string representing an array of JSON objects 394 * 395 * @return Array of created sprite 396 * 397 */ 398 jaws.Sprite.parse = function(objects) { 399 var sprites = [] 400 401 if(jaws.isArray(objects)) { 402 // If this is an array of JSON representations, parse it 403 if(objects.every(function(item) { return item._constructor })) { 404 parseArray(objects) 405 } else { 406 // This is already an array of Sprites, load it directly 407 sprites = objects 408 } 409 } 410 else if(jaws.isString(objects)) { parseArray( JSON.parse(objects) ); jaws.log.info(objects) } 411 412 function parseArray(array) { 413 array.forEach( function(data) { 414 var constructor = data._constructor ? eval(data._constructor) : data.constructor 415 if(jaws.isFunction(constructor)) { 416 jaws.log.info("Creating " + data._constructor + "(" + data.toString() + ")", true) 417 var object = new constructor(data) 418 object._constructor = data._constructor || data.constructor.name 419 sprites.push(object); 420 } 421 }); 422 } 423 424 return sprites; 425 } 426 427 /** 428 * returns a JSON-string representing the state of the Sprite. 429 * 430 * Use this to serialize your sprites / game objects, maybe to save in local storage or on a server 431 * 432 * jaws.game_states.Edit uses this to export all edited objects. 433 * 434 */ 435 jaws.Sprite.prototype.toJSON = function() { 436 return JSON.stringify(this.attributes()) 437 } 438 439 return jaws; 440 })(jaws || {}); 441 442 // Support CommonJS require() 443 if(typeof module !== "undefined" && ('exports' in module)) { module.exports = jaws.Sprite } 444 445 /* 446 // Chainable setters under consideration: 447 jaws.Sprite.prototype.setFlipped = function(value) { this.flipped = value; return this } 448 jaws.Sprite.prototype.setAlpha = function(value) { this.alpha = value; return this } 449 jaws.Sprite.prototype.setAnchorX = function(value) { this.anchor_x = value; this.cacheOffsets(); return this } 450 jaws.Sprite.prototype.setAnchorY = function(value) { this.anchor_y = value; this.cacheOffsets(); return this } 451 jaws.Sprite.prototype.setAngle = function(value) { this.angle = value; return this } 452 jaws.Sprite.prototype.setScale = function(value) { this.scale_x = this.scale_y = value; this.cacheOffsets(); return this } 453 jaws.Sprite.prototype.setScaleX = function(value) { this.scale_x = value; this.cacheOffsets(); return this } 454 jaws.Sprite.prototype.setScaleY = function(value) { this.scale_y = value; this.cacheOffsets(); return this } 455 jaws.Sprite.prototype.moveX = function(x) { this.x += x; return this } 456 jaws.Sprite.prototype.moveXTo = function(x) { this.x = x; return this } 457 jaws.Sprite.prototype.moveY = function(y) { this.y += y; return this } 458 jaws.Sprite.prototype.moveYTo = function(y) { this.y = y; return this } 459 jaws.Sprite.prototype.scaleWidthTo = function(value) { this.scale_x = value; return this.cacheOffsets() } 460 jaws.Sprite.prototype.scaleHeightTo = function(value) { this.scale_y = value; return this.cachOfffsets() } 461 */ 462 463