/** * Game class library, utility functions and globals. * * @author Kevin Roast * * 30/04/09 Initial version. * 12/05/09 Refactored to remove globals into GameHandler instance and added FPS controller game loop. * 17/01/11 Full screen resizable canvas * 26/01/11 World to screen transformation - no longer unit=pixel * 03/08/11 Modified version for CanvasMark usage */ var KEY = { SHIFT:16, CTRL:17, ESC:27, RIGHT:39, UP:38, LEFT:37, DOWN:40, SPACE:32, A:65, D:68, E:69, G:71, L:76, P:80, R:82, S:83, W:87, Z:90, OPENBRACKET:219, CLOSEBRACKET:221 }; var iOS = (navigator.userAgent.indexOf("iPhone;") != -1 || navigator.userAgent.indexOf("iPod;") != -1 || navigator.userAgent.indexOf("iPad;") != -1); /** * Game Handler. * * Singleton instance responsible for managing the main game loop and * maintaining a few global references such as the canvas and frame counters. */ var GameHandler = { /** * The single Game.Main derived instance */ game: null, /** * True if the game is in pause state, false if running */ paused: false, /** * The single canvas play field element reference */ canvas: null, /** * Width of the canvas play field */ width: 0, /** * Height of the canvas play field */ height: 0, offsetX: 0, offsetY: 0, /** * Frame counter */ frameCount: 0, sceneStartTime: 0, benchmarkScoreCount: 0, benchmarkScores: [], FPSMS: 60, FRAME_TIME_MAX: 1000/30, MAX_GLITCH_COUNT: 10, /** * Debugging output */ maxfps: 0, frametime: 0, frameInterval: 0, /** * Init function called once by your window.onload handler */ init: function() { this.canvas = document.getElementById('canvas'); this.width = this.canvas.height; this.height = this.canvas.width; var me = GameHandler; var el = me.canvas, x = 0, y = 0; do { y += el.offsetTop; x += el.offsetLeft; } while (el = el.offsetParent); // compute canvas offset including page view position me.offsetX = x - window.pageXOffset; me.offsetY = y - window.pageYOffset; }, /** * Game start method - begins the main game loop. * Pass in the object that represent the game to execute. * Also called each frame by the main game loop unless paused. * * @param {Game.Main} game main derived object handler */ start: function(game) { if (game) this.game = game; GameHandler.game.frame(); }, /** * Game pause toggle method. */ pause: function() { if (this.paused) { this.paused = false; GameHandler.game.frame(); } else { this.paused = true; } } }; /** * Game root namespace. * * @namespace Game */ if (typeof Game == "undefined" || !Game) { var Game = {}; } /** * Transform a vector from world coordinates to screen * * @method worldToScreen * @return Vector or null if non visible */ Game.worldToScreen = function worldToScreen(vector, world, radiusx, radiusy) { // transform a vector from the world to the screen radiusx = (radiusx ? radiusx : 0); radiusy = (radiusy ? radiusy : radiusx); var screenvec = null, viewx = vector.x - world.viewx, viewy = vector.y - world.viewy; if (viewx < world.viewsize + radiusx && viewy < world.viewsize + radiusy && viewx > -radiusx && viewy > -radiusy) { screenvec = new Vector(viewx, viewy).scale(world.scale); } return screenvec; }; /** * Game main loop class. * * @namespace Game * @class Game.Main */ (function() { Game.Main = function() { var me = this; document.onkeydown = function(event) { var keyCode = (event === null ? window.event.keyCode : event.keyCode); if (me.sceneIndex !== -1) { if (me.scenes[me.sceneIndex].onKeyDownHandler(keyCode)) { // if the key is handled, prevent any further events if (event) { event.preventDefault(); event.stopPropagation(); } } } }; document.onkeyup = function(event) { var keyCode = (event === null ? window.event.keyCode : event.keyCode); if (me.sceneIndex !== -1) { if (me.scenes[me.sceneIndex].onKeyUpHandler(keyCode)) { // if the key is handled, prevent any further events if (event) { event.preventDefault(); event.stopPropagation(); } } } }; }; Game.Main.prototype = { scenes: [], startScene: null, endScene: null, currentScene: null, sceneIndex: -1, lastFrameStart: 0, interval: null, /** * Game frame method - called by window timeout. */ frame: function frame() { var frameStart = Date.now(); GameHandler.frameInterval = frameStart - GameHandler.frameStart; if (GameHandler.frameInterval === 0) GameHandler.frameInterval = 1; // calculate scene transition and current scene var currentScene = this.currentScene; if (currentScene === null) { // set to scene zero (game init) this.sceneIndex = 0; currentScene = this.scenes[0]; currentScene._onInitScene(); currentScene.onInitScene(); } if ((currentScene.interval === null || currentScene.interval.complete) && currentScene.isComplete()) { if (this.sceneIndex === 0) { // reset total score recorded during the benchmark GameHandler.benchmarkScoreCount = 0; } this.sceneIndex++; if (this.sceneIndex < this.scenes.length) { currentScene = this.scenes[this.sceneIndex]; } else { this.sceneIndex = 0; currentScene = this.scenes[0]; } currentScene._onInitScene(); currentScene.onInitScene(); } // get canvas context for a render pass var ctx = GameHandler.canvas.getContext('2d'); // calculate viewport transform and offset against the world // we want to show a fixed number of world units in our viewport // so calculate the scaling factor to transform world to view currentScene.world.scale = GameHandler.width / currentScene.world.viewsize; // render the game and current scene if (currentScene.interval === null || currentScene.interval.complete) { currentScene.onBeforeRenderScene(currentScene._onBeforeRenderScene()); currentScene.onRenderScene(ctx); } else { // for the benchmark we just clear the canvas ctx.clearRect(0, 0, GameHandler.width, GameHandler.height); currentScene.interval.intervalRenderer.call(currentScene, currentScene.interval, ctx); } // update global frame counter and current scene reference this.currentScene = currentScene; GameHandler.frameCount++; // calculate frame time and frame multiplier required for smooth animation var now = Date.now(); GameHandler.frametime = now - frameStart; GameHandler.frameMultipler = GameHandler.frameInterval / GameHandler.FPSMS; GameHandler.frameStart = frameStart; // update last fps every few frames for debugging output if (GameHandler.frameCount % 16 === 0) GameHandler.lastfps = ~~(1000 / GameHandler.frameInterval); // IE9 does not support requestAnimationFrame so need to calc interval manually var ieinterval = 17 - (GameHandler.frametime); requestAnimFrame(GameHandler.start, ieinterval > 0 ? ieinterval : 1); }, isGameOver: function isGameOver() { return false; } }; })(); // requestAnimFrame shim window.requestAnimFrame = (function() { return window.requestAnimationFrame || window.webkitRequestAnimationFrame || window.oRequestAnimationFrame || window.mozRequestAnimationFrame || window.msRequestAnimationFrame || function(callback, frameOffset) { window.setTimeout(callback, frameOffset); }; })(); /** * Game scene base class. Adapted for Benchmark scoring. * * @namespace Game * @class Game.Scene */ (function() { Game.Scene = function(playable, interval) { this.playable = playable; this.interval = interval; }; Game.Scene.prototype = { playable: true, interval: null, sceneStartTime: null, sceneCompletedTime: null, sceneGlitchCount: 0, testState: 0, testScore: 0, world: { size: 1500, // total units vertically and horizontally viewx: 0, // current view left corner xpos viewy: 0, // current view left corner ypos viewsize: 1500, // size of the viewable area scale: 1 // scale for world->view transformation - calculated based on physical viewport size }, /** * Return true if this scene should update the actor list. */ isPlayable: function isPlayable() { return this.playable; }, _onInitScene: function _onInitScene() { this.sceneGlitchCount = this.testScore = this.testState = 0; this.sceneStartTime = Date.now(); this.sceneCompletedTime = null; }, onInitScene: function onInitScene() { if (this.interval !== null) { // reset interval flag this.interval.reset(); } }, _onBeforeRenderScene: function _onBeforeRenderScene() { // calculate if the scene shoud render in benchmark mode or not if (this.playable) { if (!this.sceneCompletedTime) { if (GameHandler.frameInterval > GameHandler.FRAME_TIME_MAX) { this.sceneGlitchCount++; } if (this.sceneGlitchCount < GameHandler.MAX_GLITCH_COUNT) { return true; } else { // too many FPS glitches! so benchmark scene completed (allow to run visually for a few seconds) this.sceneCompletedTime = Date.now(); var score = ~~(((this.sceneCompletedTime - this.sceneStartTime) * this.testScore) / 100); GameHandler.benchmarkScoreCount += score; GameHandler.benchmarkScores.push(score); if (typeof console !== "undefined") { console.log(score + " [" + this.interval.label + "]"); } } } } return false; }, getTransientTestScore: function getTransientTestScore() { var score = ((this.sceneCompletedTime ? this.sceneCompletedTime : Date.now()) - this.sceneStartTime) * this.testScore; return ~~(score/100); }, onBeforeRenderScene: function onBeforeRenderScene() { }, onRenderScene: function onRenderScene(ctx) { }, onRenderInterval: function onRenderInterval(ctx) { }, onKeyDownHandler: function onKeyDownHandler(keyCode) { }, onKeyUpHandler: function onKeyUpHandler(keyCode) { }, isComplete: function isComplete() { return this.sceneCompletedTime && (Date.now() - this.sceneCompletedTime > 2000); }, intervalRenderer: function intervalRenderer(interval, ctx) { if (interval.framecounter++ < 100) { Game.centerFillText(ctx, interval.label, "14pt Courier New", GameHandler.height/2 - 8, "white"); } else { interval.complete = true; } } }; })(); (function() { Game.Interval = function(label, intervalRenderer) { this.label = label; this.intervalRenderer = intervalRenderer; this.framecounter = 0; this.complete = false; }; Game.Interval.prototype = { label: null, intervalRenderer: null, framecounter: 0, complete: false, reset: function reset() { this.framecounter = 0; this.complete = false; } }; })(); /** * Actor base class. * * Game actors have a position in the game world and a current vector to indicate * direction and speed of travel per frame. They each support the onUpdate() and * onRender() event methods, finally an actor has an expired() method which should * return true when the actor object should be removed from play. * * An actor can be hit and destroyed by bullets or similar. The class supports a hit() * method which should return true when the actor should be removed from play. * * @namespace Game * @class Game.Actor */ (function() { Game.Actor = function(p, v) { this.position = p; this.vector = v; return this; }; Game.Actor.prototype = { /** * Actor position * * @property position * @type Vector */ position: null, /** * Actor vector * * @property vector * @type Vector */ vector: null, /** * Alive flag * * @property alive * @type boolean */ alive: true, /** * Radius - default is zero to imply that it is not affected by collision tests etc. * * @property radius * @type int */ radius: 0, /** * Actor expiration test * * @method expired * @return true if expired and to be removed from the actor list, false if still in play */ expired: function expired() { return !(this.alive); }, /** * Hit by bullet * * @param force of the impacting bullet (as the actor may support health) * @return true if destroyed, false otherwise */ hit: function hit(force) { this.alive = false; return true; }, /** * Transform current position vector from world coordinates to screen. * Applies the appropriate translation and scaling to the canvas context. * * @method worldToScreen * @return Vector or null if non visible */ worldToScreen: function worldToScreen(ctx, world, radius) { var viewposition = Game.worldToScreen(this.position, world, radius); if (viewposition) { // scale ALL graphics... - translate to position apply canvas scaling ctx.translate(viewposition.x, viewposition.y); ctx.scale(world.scale, world.scale); } return viewposition; }, /** * Actor game loop update event method. Called for each actor * at the start of each game loop cycle. * * @method onUpdate */ onUpdate: function onUpdate() { }, /** * Actor rendering event method. Called for each actor to * render for each frame. * * @method onRender * @param ctx {object} Canvas rendering context * @param world {object} World metadata */ onRender: function onRender(ctx, world) { } }; })(); /** * SpriteActor base class. * * An actor that can be rendered by a bitmap. The sprite handling code deals with the increment * of the current frame within the supplied bitmap sprite strip image, based on animation direction, * animation speed and the animation length before looping. Call renderSprite() each frame. * * NOTE: by default sprites source images are 64px wide 64px by N frames high and scaled to the * appropriate final size. Any other size input source should be set in the constructor. * * @namespace Game * @class Game.SpriteActor */ (function() { Game.SpriteActor = function(p, v, s) { Game.SpriteActor.superclass.constructor.call(this, p, v); if (s) this.frameSize = s; return this; }; extend(Game.SpriteActor, Game.Actor, { /** * Size in pixels of the width/height of an individual frame in the image */ frameSize: 64, /** * Animation image sprite reference. * Sprite image sources are all currently 64px wide 64px by N frames high. */ animImage: null, /** * Length in frames of the sprite animation */ animLength: 0, /** * Animation direction, true for forward, false for reverse. */ animForward: true, /** * Animation frame inc/dec speed. */ animSpeed: 1.0, /** * Current animation frame index */ animFrame: 0, /** * Render sprite graphic based on current anim image, frame and anim direction * Automatically updates the current anim frame. * * Optionally this method will automatically correct for objects moving on/off * a cyclic canvas play area - if so it will render the appropriate stencil * sections of the sprite top/bottom/left/right as needed to complete the image. * Note that this feature can only be used if the sprite is absolutely positioned * and not translated/rotated into position by canvas operations. */ renderSprite: function renderSprite(ctx, x, y, w, cyclic) { var offset = this.animFrame << 6, fs = this.frameSize; ctx.drawImage(this.animImage, 0, offset, fs, fs, x, y, w, w); if (cyclic) { if (x < 0 || y < 0) { ctx.drawImage(this.animImage, 0, offset, fs, fs, (x < 0 ? (GameHandler.width + x) : x), (y < 0 ? (GameHandler.height + y) : y), w, w); } if (x + w >= GameHandler.width || y + w >= GameHandler.height) { ctx.drawImage(this.animImage, 0, offset, fs, fs, (x + w >= GameHandler.width ? (x - GameHandler.width) : x), (y + w >= GameHandler.height ? (y - GameHandler.height) : y), w, w); } } // update animation frame index if (this.animForward) { this.animFrame += this.animSpeed; if (this.animFrame >= this.animLength) { this.animFrame = 0; } } else { this.animFrame -= this.animSpeed; if (this.animFrame < 0) { this.animFrame = this.animLength - 1; } } } }); })(); /** * EffectActor base class. * * An actor representing a transient effect in the game world. An effect is nothing more than * a special graphic that does not play any direct part in the game and does not interact with * any other objects. It automatically expires after a set lifespan, generally the rendering of * the effect is based on the remaining lifespan. * * @namespace Game * @class Game.EffectActor */ (function() { Game.EffectActor = function(p, v, lifespan) { Game.EffectActor.superclass.constructor.call(this, p, v); this.lifespan = lifespan; return this; }; extend(Game.EffectActor, Game.Actor, { /** * Effect lifespan remaining */ lifespan: 0, /** * Actor expiration test * * @return true if expired and to be removed from the actor list, false if still in play */ expired: function expired() { // deduct lifespan from the explosion return (--this.lifespan === 0); } }); })(); /** * Image Preloader class. Executes the supplied callback function once all * registered images are loaded by the browser. * * @namespace Game * @class Game.Preloader */ (function() { Game.Preloader = function() { this.images = []; return this; }; Game.Preloader.prototype = { /** * Image list * * @property images * @type Array */ images: null, /** * Callback function * * @property callback * @type Function */ callback: null, /** * Images loaded so far counter */ counter: 0, /** * Add an image to the list of images to wait for */ addImage: function addImage(img, url) { var me = this; img.url = url; // attach closure to the image onload handler img.onload = function() { me.counter++; if (me.counter === me.images.length) { // all images are loaded - execute callback function me.callback.call(me); } }; this.images.push(img); }, /** * Load the images and call the supplied function when ready */ onLoadCallback: function onLoadCallback(fn) { this.counter = 0; this.callback = fn; // load the images for (var i=0, j=this.images.length; i 20) s = 20; else if (s < 8) s = 8; return s; }; Game.fontFamily = function fontFamily(world, size, font) { return Game.fontSize(world, size) + "pt " + (font ? font : "Courier New"); };/** * Feature test scenes for CanvasMark Rendering Benchmark - March 2013 * * (C) 2013 Kevin Roast kevtoast@yahoo.com @kevinroast * * Please see: license.txt * You are welcome to use this code, but I would appreciate an email or tweet * if you do anything interesting with it! */ /** * Feature root namespace. * * @namespace Feature */ if (typeof Feature == "undefined" || !Feature) { var Feature = {}; } Feature.textureImage = new Image(); Feature.blurImage = new Image(); /** * Feature main Benchmark Test class. * * @namespace Feature * @class Feature.Test */ (function() { Feature.Test = function(benchmark, loader) { loader.addImage(Feature.textureImage, "./images/texture5.png"); loader.addImage(Feature.blurImage, "./images/fruit.jpg"); // add benchmark scenes var t = benchmark.scenes.length; for (var i=0; i<3; i++) { benchmark.addBenchmarkScene(new Feature.GameScene(this, t+i, i)); } }; Feature.Test.prototype = { }; })(); (function() { /** * Feature.K3DController constructor */ Feature.K3DController = function() { Feature.K3DController.superclass.constructor.call(this); }; extend(Feature.K3DController, K3D.BaseController, { /** * Render tick - should be called from appropriate scene renderer */ render: function(ctx) { // execute super class method to process render pipelines ctx.save(); ctx.translate(GameHandler.width/2, GameHandler.height/2); this.processFrame(ctx); ctx.restore(); } }); })(); /** * Feature Game scene class. * * @namespace Feature * @class Feature.GameScene */ (function() { Feature.GameScene = function(game, test, feature) { this.game = game; this.test = test; this.feature = feature; var msg = "Test " + test + " - "; switch (feature) { case 0: msg += "Plasma - Maths, canvas shapes"; break; case 1: msg += "3D Rendering - Maths, polygons, image transforms"; break; case 2: msg += "Pixel blur - Math, getImageData, putImageData"; break; } var interval = new Game.Interval(msg, this.intervalRenderer); Feature.GameScene.superclass.constructor.call(this, true, interval); }; extend(Feature.GameScene, Game.Scene, { feature: 0, index: 0, game: null, /** * Scene init event handler */ onInitScene: function onInitScene() { switch (this.feature) { case 0: { // generate plasma palette var palette = []; for (var i=0,r,g,b; i<256; i++) { r = ~~(128 + 128 * Math.sin(Math.PI * i / 32)); g = ~~(128 + 128 * Math.sin(Math.PI * i / 64)); b = ~~(128 + 128 * Math.sin(Math.PI * i / 128)); palette[i] = "rgb(" + ~~r + "," + ~~g + "," + ~~b + ")"; } this.paletteoffset = 0; this.palette = palette; // size of the plasma pixels ratio - bigger = more calculations and rendering this.plasmasize = 8; this.testScore = 10; break; } case 1: { // K3D controller this.k3d = new Feature.K3DController(); // generate 3D objects for (var i=0; i<10; i++) { this.add3DObject(i); } this.testScore = 10; break; } case 2: { this.testScore = 25; break; } } }, add3DObject: function add3DObject(offset) { var gap = 360/20; var obj = new K3D.K3DObject(); obj.ophi = (360 / gap) * offset; obj.otheta = (180 / gap / 2) * offset; obj.textures.push(Feature.textureImage); with (obj) { drawmode = "solid"; // one of "point", "wireframe", "solid" shademode = "lightsource"; // one of "plain", "depthcue", "lightsource" (solid drawing mode only) addgamma = 0.5; addtheta = -1.0; addphi = -0.75; aboutx = 150; abouty = -150; aboutz = -50; perslevel = 512; scale = 13; init( // describe the points of a simple unit cube [{x:-1,y:1,z:-1}, {x:1,y:1,z:-1}, {x:1,y:-1,z:-1}, {x:-1,y:-1,z:-1}, {x:-1,y:1,z:1}, {x:1,y:1,z:1}, {x:1,y:-1,z:1}, {x:-1,y:-1,z:1}], // describe the edges of the cube [{a:0,b:1}, {a:1,b:2}, {a:2,b:3}, {a:3,b:0}, {a:4,b:5}, {a:5,b:6}, {a:6,b:7}, {a:7,b:4}, {a:0,b:4}, {a:1,b:5}, {a:2,b:6}, {a:3,b:7}], // describe the polygon faces of the cube [{color:[255,0,0],vertices:[0,1,2,3],texture:0},{color:[0,255,0],vertices:[0,4,5,1]},{color:[0,0,255],vertices:[1,5,6,2]},{color:[255,255,0],vertices:[2,6,7,3]},{color:[0,255,255],vertices:[3,7,4,0]},{color:[255,0,255],vertices:[7,6,5,4],texture:0}] ); } // add another 3D object to the controller this.k3d.addK3DObject(obj); }, /** * Scene before rendering event handler */ onBeforeRenderScene: function onBeforeRenderScene(benchmark) { if (benchmark) { switch (this.feature) { case 0: { if (Date.now() - this.sceneStartTime > this.testState) { this.testState+=100; this.plasmasize++; } break; } case 1: { if (Date.now() - this.sceneStartTime > this.testState) { this.testState+=100; this.add3DObject(this.k3d.objects.length); } break; } case 2: { if (Date.now() - this.sceneStartTime > this.testState) { this.testState+=2; } break; } } } }, /** * Scene rendering event handler */ onRenderScene: function onRenderScene(ctx) { ctx.clearRect(0, 0, GameHandler.width, GameHandler.height); // render feature benchmark var width = GameHandler.width, height = GameHandler.height; switch (this.feature) { case 0: { var dist = function dist(a, b, c, d) { return Math.sqrt((a - c) * (a - c) + (b - d) * (b - d)); } // plasma source width and height - variable benchmark state var pwidth = this.plasmasize; var pheight = pwidth * (height/width); // scale the plasma source to the canvas width/height var vpx = width/pwidth, vpy = height/pheight; var time = Date.now() / 64; var colour = function colour(x, y) { // plasma function return (128 + (128 * Math.sin(x * 0.0625)) + 128 + (128 * Math.sin(y * 0.03125)) + 128 + (128 * Math.sin(dist(x + time, y - time, width, height) * 0.125)) + 128 + (128 * Math.sin(Math.sqrt(x * x + y * y) * 0.125)) ) * 0.25; } // render plasma effect for (var y=0,x; y> 4 + 1, 1 ); break; } } ctx.save(); ctx.shadowBlur = 0; // Benchmark - information output if (this.sceneCompletedTime) { Game.fillText(ctx, "TEST "+this.test+" COMPLETED: "+this.getTransientTestScore(), "20pt Courier New", 4, 40, "white"); } Game.fillText(ctx, "SCORE: " + this.getTransientTestScore(), "12pt Courier New", 0, GameHandler.height - 42, "lightblue"); Game.fillText(ctx, "TSF: " + Math.round(GameHandler.frametime) + "ms", "12pt Courier New", 0, GameHandler.height - 22, "lightblue"); Game.fillText(ctx, "FPS: " + GameHandler.lastfps, "12pt Courier New", 0, GameHandler.height - 2, "lightblue"); ctx.restore(); } }); })(); /* Superfast Blur - a fast Box Blur For Canvas Version: 0.5 Author: Mario Klingemann Contact: mario@quasimondo.com Website: http://www.quasimondo.com/BoxBlurForCanvas Twitter: @quasimondo In case you find this class useful - especially in commercial projects - I am not totally unhappy for a small donation to my PayPal account mario@quasimondo.de Or support me on flattr: https://flattr.com/thing/140066/Superfast-Blur-a-pretty-fast-Box-Blur-Effect-for-CanvasJavascript Copyright (c) 2011 Mario Klingemann Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ var mul_table = [ 1,57,41,21,203,34,97,73,227,91,149,62,105,45,39,137,241,107,3,173,39,71,65,238,219,101,187,87,81,151,141,133,249,117,221,209,197,187,177,169,5,153,73,139,133,127,243,233,223,107,103,99,191,23,177,171,165,159,77,149,9,139,135,131,253,245,119,231,224,109,211,103,25,195,189,23,45,175,171,83,81,79,155,151,147,9,141,137,67,131,129,251,123,30,235,115,113,221,217,53,13,51,50,49,193,189,185,91,179,175,43,169,83,163,5,79,155,19,75,147,145,143,35,69,17,67,33,65,255,251,247,243,239,59,29,229,113,111,219,27,213,105,207,51,201,199,49,193,191,47,93,183,181,179,11,87,43,85,167,165,163,161,159,157,155,77,19,75,37,73,145,143,141,35,138,137,135,67,33,131,129,255,63,250,247,61,121,239,237,117,29,229,227,225,111,55,109,216,213,211,209,207,205,203,201,199,197,195,193,48,190,47,93,185,183,181,179,178,176,175,173,171,85,21,167,165,41,163,161,5,79,157,78,154,153,19,75,149,74,147,73,144,143,71,141,140,139,137,17,135,134,133,66,131,65,129,1]; var shg_table = [0,9,10,10,14,12,14,14,16,15,16,15,16,15,15,17,18,17,12,18,16,17,17,19,19,18,19,18,18,19,19,19,20,19,20,20,20,20,20,20,15,20,19,20,20,20,21,21,21,20,20,20,21,18,21,21,21,21,20,21,17,21,21,21,22,22,21,22,22,21,22,21,19,22,22,19,20,22,22,21,21,21,22,22,22,18,22,22,21,22,22,23,22,20,23,22,22,23,23,21,19,21,21,21,23,23,23,22,23,23,21,23,22,23,18,22,23,20,22,23,23,23,21,22,20,22,21,22,24,24,24,24,24,22,21,24,23,23,24,21,24,23,24,22,24,24,22,24,24,22,23,24,24,24,20,23,22,23,24,24,24,24,24,24,24,23,21,23,22,23,24,24,24,22,24,24,24,23,22,24,24,25,23,25,25,23,24,25,25,24,22,25,25,25,24,23,24,25,25,25,25,25,25,25,25,25,25,25,25,23,25,23,24,25,25,25,25,25,25,25,25,25,24,22,25,25,23,25,25,20,24,25,24,25,25,22,24,25,24,25,24,25,25,24,25,25,25,25,22,25,25,25,24,25,24,25,18]; function boxBlurCanvasRGBA( context, top_x, top_y, width, height, radius, iterations ){ radius |= 0; var imageData = context.getImageData( top_x, top_y, width, height ); var pixels = imageData.data; var rsum,gsum,bsum,asum,x,y,i,p,p1,p2,yp,yi,yw,idx,pa; var wm = width - 1; var hm = height - 1; var wh = width * height; var rad1 = radius + 1; var mul_sum = mul_table[radius]; var shg_sum = shg_table[radius]; var r = []; var g = []; var b = []; var a = []; var vmin = []; var vmax = []; while ( iterations-- > 0 ){ yw = yi = 0; for ( y=0; y < height; y++ ){ rsum = pixels[yw] * rad1; gsum = pixels[yw+1] * rad1; bsum = pixels[yw+2] * rad1; asum = pixels[yw+3] * rad1; for( i = 1; i <= radius; i++ ){ p = yw + (((i > wm ? wm : i )) << 2 ); rsum += pixels[p++]; gsum += pixels[p++]; bsum += pixels[p++]; asum += pixels[p] } for ( x = 0; x < width; x++ ) { r[yi] = rsum; g[yi] = gsum; b[yi] = bsum; a[yi] = asum; if( y==0) { vmin[x] = ( ( p = x + rad1) < wm ? p : wm ) << 2; vmax[x] = ( ( p = x - radius) > 0 ? p << 2 : 0 ); } p1 = yw + vmin[x]; p2 = yw + vmax[x]; rsum += pixels[p1++] - pixels[p2++]; gsum += pixels[p1++] - pixels[p2++]; bsum += pixels[p1++] - pixels[p2++]; asum += pixels[p1] - pixels[p2]; yi++; } yw += ( width << 2 ); } for ( x = 0; x < width; x++ ) { yp = x; rsum = r[yp] * rad1; gsum = g[yp] * rad1; bsum = b[yp] * rad1; asum = a[yp] * rad1; for( i = 1; i <= radius; i++ ) { yp += ( i > hm ? 0 : width ); rsum += r[yp]; gsum += g[yp]; bsum += b[yp]; asum += a[yp]; } yi = x << 2; for ( y = 0; y < height; y++) { pixels[yi+3] = pa = (asum * mul_sum) >>> shg_sum; if ( pa > 0 ) { pa = 255 / pa; pixels[yi] = ((rsum * mul_sum) >>> shg_sum) * pa; pixels[yi+1] = ((gsum * mul_sum) >>> shg_sum) * pa; pixels[yi+2] = ((bsum * mul_sum) >>> shg_sum) * pa; } else { pixels[yi] = pixels[yi+1] = pixels[yi+2] = 0; } if( x == 0 ) { vmin[y] = ( ( p = y + rad1) < hm ? p : hm ) * width; vmax[y] = ( ( p = y - radius) > 0 ? p * width : 0 ); } p1 = x + vmin[y]; p2 = x + vmax[y]; rsum += r[p1] - r[p2]; gsum += g[p1] - g[p2]; bsum += b[p1] - b[p2]; asum += a[p1] - a[p2]; yi += width << 2; } } } context.putImageData( imageData, top_x, top_y ); }/** * Asteroids HTML5 Canvas Game * Scenes for CanvasMark Rendering Benchmark - March 2013 * * @email kevtoast at yahoo dot com * @twitter kevinroast * * (C) 2013 Kevin Roast * * Please see: license.txt * You are welcome to use this code, but I would appreciate an email or tweet * if you do anything interesting with it! */ // Globals var BITMAPS = true; var GLOWEFFECT = false; var g_asteroidImg1 = new Image(); var g_asteroidImg2 = new Image(); var g_asteroidImg3 = new Image(); var g_asteroidImg4 = new Image(); var g_shieldImg = new Image(); var g_backgroundImg = new Image(); var g_playerImg = new Image(); var g_enemyshipImg = new Image(); /** * Asteroids root namespace. * * @namespace Asteroids */ if (typeof Asteroids == "undefined" || !Asteroids) { var Asteroids = {}; } /** * Asteroids benchmark test class. * * @namespace Asteroids * @class Asteroids.Test */ (function() { Asteroids.Test = function(benchmark, loader) { // get the image graphics loading loader.addImage(g_backgroundImg, './images/bg3_1.jpg'); loader.addImage(g_playerImg, './images/player.png'); loader.addImage(g_asteroidImg1, './images/asteroid1.png'); loader.addImage(g_asteroidImg2, './images/asteroid2.png'); loader.addImage(g_asteroidImg3, './images/asteroid3.png'); loader.addImage(g_asteroidImg4, './images/asteroid4.png'); loader.addImage(g_enemyshipImg, './images/enemyship1.png'); // generate the single player actor - available across all scenes this.player = new Asteroids.Player(new Vector(GameHandler.width / 2, GameHandler.height / 2), new Vector(0.0, 0.0), 0.0); // add the Asteroid game benchmark scenes for (var level, i=0, t=benchmark.scenes.length; i<4; i++) { level = new Asteroids.BenchMarkScene(this, t+i, i+1);// NOTE: asteroids indexes feature from 1... benchmark.addBenchmarkScene(level); } }; Asteroids.Test.prototype = { /** * Reference to the single game player actor */ player: null, /** * Lives count (only used to render overlay graphics during benchmark mode) */ lives: 3 }; })(); /** * Asteroids Benchmark scene class. * * @namespace Asteroids * @class Asteroids.BenchMarkScene */ (function() { Asteroids.BenchMarkScene = function(game, test, feature) { this.game = game; this.test = test; this.feature = feature; this.player = game.player; var msg = "Test " + test + " - Asteroids - "; switch (feature) { case 1: msg += "Bitmaps"; break; case 2: msg += "Vectors"; break; case 3: msg += "Bitmaps, shapes, text"; break; case 4: msg += "Shapes, shadows, blending"; break; } var interval = new Game.Interval(msg, this.intervalRenderer); Asteroids.BenchMarkScene.superclass.constructor.call(this, true, interval); // generate background starfield for (var star, i=0; i GameHandler.height || s.prevy > GameHandler.width) { s.init(); } } }, /** * Scene init event handler */ onInitScene: function onInitScene() { // generate the actors and add the actor sub-lists to the main actor list this.actors = []; this.enemies = []; this.actors.push(this.enemies); this.actors.push(this.playerBullets = []); this.actors.push(this.enemyBullets = []); this.actors.push(this.effects = []); this.actors.push([this.player]); // reset the player position with (this.player) { position.x = GameHandler.width / 2; position.y = GameHandler.height / 2; vector.x = 0.0; vector.y = 0.0; heading = 0.0; } // tests 1-2 display asteroids in various modes switch (this.feature) { case 1: { // start with 10 asteroids - more will be added if framerate is acceptable for (var i=0; i<10; i++) { this.enemies.push(this.generateAsteroid(Math.random()+1.0, ~~(Math.random()*4) + 1)); } this.testScore = 10; break; } case 2: { // start with 10 asteroids - more will be added if framerate is acceptable for (var i=0; i<10; i++) { this.enemies.push(this.generateAsteroid(Math.random()+1.0, ~~(Math.random()*4) + 1)); } this.testScore = 20; break; } case 3: { // test 3 generates lots of enemy ships that fire for (var i=0; i<10; i++) { this.enemies.push(new Asteroids.EnemyShip(this, i%2)); } this.testScore = 10; break; } case 4: { this.testScore = 25; break; } } // tests 2 in wireframe, all others are bitmaps BITMAPS = !(this.feature === 2); // reset interval flag this.interval.reset(); }, /** * Scene before rendering event handler */ onBeforeRenderScene: function onBeforeRenderScene(benchmark) { // add items to the test if (benchmark) { switch (this.feature) { case 1: case 2: { var count; switch (this.feature) { case 1: count = 10; break; case 2: count = 5; break; } for (var i=0; i this.testState) { this.testState += 20; for (var i=0; i<2; i++) { this.enemies.push(new Asteroids.EnemyShip(this, i%2)); } this.enemies[0].hit(); this.destroyEnemy(this.enemies[0], new Vector(0, 1)); } break; } case 4: { if (Date.now() - this.sceneStartTime > this.testState) { this.testState += 25; // spray forward guns for (var i=0; i<=~~(this.testState/500); i++) { h = this.player.heading - 15; t = new Vector(0.0, -7.0).rotate(h * RAD).add(this.player.vector); this.playerBullets.push(new Asteroids.Bullet(this.player.position.clone(), t, h)); h = this.player.heading; t = new Vector(0.0, -7.0).rotate(h * RAD).add(this.player.vector); this.playerBullets.push(new Asteroids.Bullet(this.player.position.clone(), t, h)); h = this.player.heading + 15; t = new Vector(0.0, -7.0).rotate(h * RAD).add(this.player.vector); this.playerBullets.push(new Asteroids.Bullet(this.player.position.clone(), t, h)); } // side firing guns also h = this.player.heading - 90; t = new Vector(0.0, -8.0).rotate(h * RAD).add(this.player.vector); this.playerBullets.push(new Asteroids.Bullet(this.player.position.clone(), t, h, 25)); h = this.player.heading + 90; t = new Vector(0.0, -8.0).rotate(h * RAD).add(this.player.vector); this.playerBullets.push(new Asteroids.Bullet(this.player.position.clone(), t, h, 25)); // update player heading to rotate this.player.heading += 8; } break; } } } // update all actors using their current vector this.updateActors(); }, /** * Scene rendering event handler */ onRenderScene: function onRenderScene(ctx) { // setup canvas for a render pass and apply background if (BITMAPS) { // draw a scrolling background image ctx.drawImage(g_backgroundImg, this.backgroundX++, 0, GameHandler.width, GameHandler.height, 0, 0, GameHandler.width, GameHandler.height); if (this.backgroundX == (g_backgroundImg.width / 2)) { this.backgroundX = 0; } ctx.shadowBlur = 0; } else { // clear the background to black ctx.fillStyle = "black"; ctx.fillRect(0, 0, GameHandler.width, GameHandler.height); // glowing vector effect shadow ctx.shadowBlur = GLOWEFFECT ? 8 : 0; // update and render background starfield effect this.updateStarfield(ctx); } // render the game actors this.renderActors(ctx); // render info overlay graphics this.renderOverlay(ctx); }, /** * Randomly generate a new large asteroid. Ensures the asteroid is not generated * too close to the player position! * * @param speedFactor {number} Speed multiplier factor to apply to asteroid vector */ generateAsteroid: function generateAsteroid(speedFactor, size) { while (true) { // perform a test to check it is not too close to the player var apos = new Vector(Math.random()*GameHandler.width, Math.random()*GameHandler.height); if (this.player.position.distance(apos) > 125) { var vec = new Vector( ((Math.random()*2)-1)*speedFactor, ((Math.random()*2)-1)*speedFactor ); var asteroid = new Asteroids.Asteroid( apos, vec, size ? size : 4); return asteroid; } } }, /** * Update the actors position based on current vectors and expiration. */ updateActors: function updateActors() { for (var i = 0, j = this.actors.length; i < j; i++) { var actorList = this.actors[i]; for (var n = 0; n < actorList.length; n++) { var actor = actorList[n]; // call onUpdate() event for each actor actor.onUpdate(this); // expiration test first if (actor.expired()) { actorList.splice(n, 1); } else { // update actor using its current vector actor.position.add(actor.vector); // handle traversing out of the coordinate space and back again if (actor.position.x >= GameHandler.width) { actor.position.x = 0; } else if (actor.position.x < 0) { actor.position.x = GameHandler.width - 1; } if (actor.position.y >= GameHandler.height) { actor.position.y = 0; } else if (actor.position.y < 0) { actor.position.y = GameHandler.height - 1; } } } } }, /** * Blow up an enemy. * * An asteroid may generate new baby asteroids and leave an explosion * in the wake. * * Also applies the score for the destroyed item. * * @param enemy {Game.Actor} The enemy to destory and add score for * @param parentVector {Vector} The vector of the item that hit the enemy * @param player {boolean} If true, the player was the destroyed */ destroyEnemy: function destroyEnemy(enemy, parentVector) { if (enemy instanceof Asteroids.Asteroid) { // generate baby asteroids this.generateBabyAsteroids(enemy, parentVector); // add an explosion actor at the asteriod position and vector var boom = new Asteroids.Explosion(enemy.position.clone(), enemy.vector.clone(), enemy.size); this.effects.push(boom); // generate a score effect indicator at the destroyed enemy position var vec = new Vector(0, -(Math.random()*2 + 0.5)); var effect = new Asteroids.ScoreIndicator( new Vector(enemy.position.x, enemy.position.y), vec, Math.floor(100 + (Math.random()*100))); this.effects.push(effect); } else if (enemy instanceof Asteroids.EnemyShip) { // add an explosion actor at the asteriod position and vector var boom = new Asteroids.Explosion(enemy.position.clone(), enemy.vector.clone(), 4); this.effects.push(boom); // generate a score text effect indicator at the destroyed enemy position var vec = new Vector(0, -(Math.random()*2 + 0.5)); var effect = new Asteroids.ScoreIndicator( new Vector(enemy.position.x, enemy.position.y), vec, Math.floor(100 + (Math.random()*100))); this.effects.push(effect); } }, /** * Generate a number of baby asteroids from a detonated parent asteroid. The number * and size of the generated asteroids are based on the parent size. Some of the * momentum of the parent vector (e.g. impacting bullet) is applied to the new asteroids. * * @param asteroid {Asteroids.Asteroid} The parent asteroid that has been destroyed * @param parentVector {Vector} Vector of the impacting object e.g. a bullet */ generateBabyAsteroids: function generateBabyAsteroids(asteroid, parentVector) { // generate some baby asteroid(s) if bigger than the minimum size if (asteroid.size > 1) { for (var x=0, xc=Math.floor(asteroid.size/2); x= 0; n--) { actorList[n].onRender(ctx); } } }, /** * Render player information HUD overlay graphics. * * @param ctx {object} Canvas rendering context */ renderOverlay: function renderOverlay(ctx) { ctx.save(); // energy bar (100 pixels across, scaled down from player energy max) ctx.strokeStyle = "rgb(50,50,255)"; ctx.strokeRect(4, 4, 101, 6); ctx.fillStyle = "rgb(100,100,255)"; var energy = this.player.energy; if (energy > this.player.ENERGY_INIT) { // the shield is on for "free" briefly when he player respawns energy = this.player.ENERGY_INIT; } ctx.fillRect(5, 5, (energy / (this.player.ENERGY_INIT / 100)), 5); // lives indicator graphics for (var i=0; i 0 && this.energy > 0) { if (BITMAPS) { // render shield graphic bitmap ctx.save(); ctx.translate(this.position.x, this.position.y); ctx.rotate(headingRad); this.renderSprite(ctx, -this.SHIELD_RADIUS-1, -this.SHIELD_RADIUS-1, (this.SHIELD_RADIUS * 2) + 2); ctx.restore(); } else { // render shield as a simple circle around the ship ctx.save(); ctx.translate(this.position.x, this.position.y); ctx.rotate(headingRad); ctx.shadowColor = ctx.strokeStyle = "rgb(100,100,255)"; ctx.beginPath(); ctx.arc(0, 2, this.SHIELD_RADIUS, 0, TWOPI, true); ctx.closePath(); ctx.stroke(); ctx.restore(); } this.shieldCounter--; this.energy--; } }, /** * Execute player forward thrust request * Automatically a delay is used between each application - to ensure stable thrust on all machines. */ thrust: function thrust() { // now test we did not thrust too recently - to stop fast key repeat issues if (GameHandler.frameCount - this.thrustRecharge > this.THRUST_DELAY) { // update last frame count this.thrustRecharge = GameHandler.frameCount; // generate a small thrust vector var t = new Vector(0.0, -0.55); // rotate thrust vector by player current heading t.rotate(this.heading * RAD); // test player won't then exceed maximum velocity var pv = this.vector.clone(); if (pv.add(t).length() < this.MAX_PLAYER_VELOCITY) { // add to current vector (which then gets applied during each render loop) this.vector.add(t); } } // mark so that we know to render engine thrust graphics this.engineThrust = true; }, /** * Execute player active shield request * If energy remaining the shield will be briefly applied - or until key is release */ activateShield: function activateShield() { // ensure shield stays up for a brief pulse between key presses! if (this.energy > 0) { this.shieldCounter = this.SHIELD_MIN_PULSE; } }, isShieldActive: function isShieldActive() { return (this.shieldCounter > 0 && this.energy > 0); }, radius: function radius() { return (this.isShieldActive() ? this.SHIELD_RADIUS : this.PLAYER_RADIUS); }, expired: function expired() { return !(this.alive); }, kill: function kill() { this.alive = false; this.killedOnFrame = GameHandler.frameCount; }, /** * Fire primary weapon(s) * @param bulletList {Array} to add bullet(s) to on success */ firePrimary: function firePrimary(bulletList) { // attempt to fire the primary weapon(s) // first ensure player is alive and the shield is not up if (this.alive && (!this.isShieldActive() || this.fireWhenShield)) { for (var w in this.primaryWeapons) { var b = this.primaryWeapons[w].fire(); if (b) { if (isArray(b)) { for (var i=0; i this.BOMB_ENERGY) { // now test we did not fire too recently if (GameHandler.frameCount - this.bombRecharge > this.BOMB_RECHARGE) { // ok, update last fired frame and we can now generate a bomb this.bombRecharge = GameHandler.frameCount; // decrement energy supply this.energy -= this.BOMB_ENERGY; // generate a vector rotated to the player heading and then add the current player // vector to give the bomb the correct directional momentum var t = new Vector(0.0, -6.0); t.rotate(this.heading * RAD); t.add(this.vector); bulletList.push(new Asteroids.Bomb(this.position.clone(), t)); } } }, onUpdate: function onUpdate() { // slowly recharge the shield - if not active if (!this.isShieldActive() && this.energy < this.ENERGY_INIT) { this.energy += 0.1; } }, reset: function reset(persistPowerUps) { // reset energy, alive status, weapons and power up flags this.alive = true; if (!persistPowerUps) { this.primaryWeapons = []; this.primaryWeapons["main"] = new Asteroids.PrimaryWeapon(this); this.fireWhenShield = false; } this.energy = this.ENERGY_INIT + this.SHIELD_MIN_PULSE; // for shield as below // active shield briefly this.activateShield(); } }); })(); /** * Asteroid actor class. * * @namespace Asteroids * @class Asteroids.Asteroid */ (function() { Asteroids.Asteroid = function(p, v, s, t) { Asteroids.Asteroid.superclass.constructor.call(this, p, v); this.size = s; this.health = s; // randomly select an asteroid image bitmap if (t === undefined) { t = randomInt(1, 4); } eval("this.animImage=g_asteroidImg" + t); this.type = t; // randomly setup animation speed and direction this.animForward = (Math.random() < 0.5); this.animSpeed = 0.25 + Math.random(); this.animLength = this.ANIMATION_LENGTH; this.rotation = randomInt(0, 180); this.rotationSpeed = randomInt(-1, 1) / 25; return this; }; extend(Asteroids.Asteroid, Game.SpriteActor, { ANIMATION_LENGTH: 180, /** * Asteroid size - values from 1-4 are valid. */ size: 0, /** * Asteroid type i.e. which bitmap it is drawn from */ type: 1, /** * Asteroid health before it's destroyed */ health: 0, /** * Retro graphics mode rotation orientation and speed */ rotation: 0, rotationSpeed: 0, /** * Asteroid rendering method */ onRender: function onRender(ctx) { var rad = this.size * 8; ctx.save(); if (BITMAPS) { // render asteroid graphic bitmap // bitmap is rendered slightly large than the radius as the raytraced asteroid graphics do not // quite touch the edges of the 64x64 sprite - this improves perceived collision detection this.renderSprite(ctx, this.position.x - rad - 2, this.position.y - rad - 2, (rad * 2)+4, true); } else { // draw asteroid outline circle ctx.shadowColor = ctx.strokeStyle = "white"; ctx.translate(this.position.x, this.position.y); ctx.scale(this.size * 0.8, this.size * 0.8); ctx.rotate(this.rotation += this.rotationSpeed); ctx.lineWidth = (0.8 / this.size) * 2; ctx.beginPath(); // asteroid wires switch (this.type) { case 1: ctx.moveTo(0,10); ctx.lineTo(8,6); ctx.lineTo(10,-4); ctx.lineTo(4,-2); ctx.lineTo(6,-6); ctx.lineTo(0,-10); ctx.lineTo(-10,-3); ctx.lineTo(-10,5); break; case 2: ctx.moveTo(0,10); ctx.lineTo(8,6); ctx.lineTo(10,-4); ctx.lineTo(4,-2); ctx.lineTo(6,-6); ctx.lineTo(0,-10); ctx.lineTo(-8,-8); ctx.lineTo(-6,-3); ctx.lineTo(-8,-4); ctx.lineTo(-10,5); break; case 3: ctx.moveTo(-4,10); ctx.lineTo(1,8); ctx.lineTo(7,10); ctx.lineTo(10,-4); ctx.lineTo(4,-2); ctx.lineTo(6,-6); ctx.lineTo(0,-10); ctx.lineTo(-10,-3); ctx.lineTo(-10,5); break; case 4: ctx.moveTo(-8,10); ctx.lineTo(7,8); ctx.lineTo(10,-2); ctx.lineTo(6,-10); ctx.lineTo(-2,-8); ctx.lineTo(-6,-10); ctx.lineTo(-10,-6); ctx.lineTo(-7,0); break; } ctx.closePath(); ctx.stroke(); } ctx.restore(); }, radius: function radius() { return this.size * 8; }, /** * Asteroid hit by player bullet * * @param force of the impacting bullet, -1 for instant kill * @return true if destroyed, false otherwise */ hit: function hit(force) { if (force !== -1) { this.health -= force; } else { // instant kill this.health = 0; } return !(this.alive = (this.health > 0)); } }); })(); /** * Enemy Ship actor class. * * @namespace Asteroids * @class Asteroids.EnemyShip */ (function() { Asteroids.EnemyShip = function(scene, size) { this.size = size; // small ship, alter settings slightly if (this.size === 1) { this.BULLET_RECHARGE = 45; this.RADIUS = 8; } // randomly setup enemy initial position and vector // ensure the enemy starts in the opposite quadrant to the player var p, v; if (scene.player.position.x < GameHandler.width / 2) { // player on left of the screen if (scene.player.position.y < GameHandler.height / 2) { // player in top left of the screen p = new Vector(GameHandler.width-48, GameHandler.height-48); } else { // player in bottom left of the screen p = new Vector(GameHandler.width-48, 48); } v = new Vector(-(Math.random() + 1 + size), Math.random() + 0.5 + size); } else { // player on right of the screen if (scene.player.position.y < GameHandler.height / 2) { // player in top right of the screen p = new Vector(0, GameHandler.height-48); } else { // player in bottom right of the screen p = new Vector(0, 48); } v = new Vector(Math.random() + 1 + size, Math.random() + 0.5 + size); } // setup SpriteActor values this.animImage = g_enemyshipImg; this.animLength = this.SHIP_ANIM_LENGTH; Asteroids.EnemyShip.superclass.constructor.call(this, p, v); return this; }; extend(Asteroids.EnemyShip, Game.SpriteActor, { SHIP_ANIM_LENGTH: 90, RADIUS: 16, BULLET_RECHARGE: 60, /** * Enemy ship size - 0 = large (slow), 1 = small (fast) */ size: 0, /** * Bullet fire recharging counter */ bulletRecharge: 0, onUpdate: function onUpdate(scene) { // change enemy direction randomly if (this.size === 0) { if (Math.random() < 0.01) { this.vector.y = -(this.vector.y + (0.25 - (Math.random()/2))); } } else { if (Math.random() < 0.02) { this.vector.y = -(this.vector.y + (0.5 - Math.random())); } } // regular fire a bullet at the player if (GameHandler.frameCount - this.bulletRecharge > this.BULLET_RECHARGE && scene.player.alive) { // ok, update last fired frame and we can now generate a bullet this.bulletRecharge = GameHandler.frameCount; // generate a vector pointed at the player // by calculating a vector between the player and enemy positions var v = scene.player.position.clone().sub(this.position); // scale resulting vector down to bullet vector size var scale = (this.size === 0 ? 5.0 : 6.0) / v.length(); v.x *= scale; v.y *= scale; // slightly randomize the direction (big ship is less accurate also) v.x += (this.size === 0 ? (Math.random() * 2 - 1) : (Math.random() - 0.5)); v.y += (this.size === 0 ? (Math.random() * 2 - 1) : (Math.random() - 0.5)); // - could add the enemy motion vector for correct momentum // - but problem is this leads to slow bullets firing back from dir of travel // - so pretend that enemies are clever enough to account for this... //v.add(this.vector); var bullet = new Asteroids.EnemyBullet(this.position.clone(), v); scene.enemyBullets.push(bullet); } }, /** * Enemy rendering method */ onRender: function onRender(ctx) { if (BITMAPS) { // render enemy graphic bitmap var rad = this.RADIUS + 2; this.renderSprite(ctx, this.position.x - rad, this.position.y - rad, rad * 2, true); } else { ctx.save(); ctx.translate(this.position.x, this.position.y); if (this.size === 0) { ctx.scale(2, 2); } ctx.beginPath(); ctx.moveTo(0, -4); ctx.lineTo(8, 3); ctx.lineTo(0, 8); ctx.lineTo(-8, 3); ctx.lineTo(0, -4); ctx.closePath(); ctx.shadowColor = ctx.strokeStyle = "rgb(100,150,100)"; ctx.stroke(); ctx.beginPath(); ctx.moveTo(0, -8); ctx.lineTo(4, -4); ctx.lineTo(0, 0); ctx.lineTo(-4, -4); ctx.lineTo(0, -8); ctx.closePath(); ctx.shadowColor = ctx.strokeStyle = "rgb(150,200,150)"; ctx.stroke(); ctx.restore(); } }, radius: function radius() { return this.RADIUS; } }); })(); /** * Weapon system base class for the player actor. * * @namespace Asteroids * @class Asteroids.Weapon */ (function() { Asteroids.Weapon = function(player) { this.player = player; return this; }; Asteroids.Weapon.prototype = { WEAPON_RECHARGE: 3, weaponRecharge: 0, player: null, fire: function() { // now test we did not fire too recently if (GameHandler.frameCount - this.weaponRecharge > this.WEAPON_RECHARGE) { // ok, update last fired frame and we can now generate a bullet this.weaponRecharge = GameHandler.frameCount; return this.doFire(); } }, doFire: function() { } }; })(); /** * Basic primary weapon for the player actor. * * @namespace Asteroids * @class Asteroids.PrimaryWeapon */ (function() { Asteroids.PrimaryWeapon = function(player) { Asteroids.PrimaryWeapon.superclass.constructor.call(this, player); return this; }; extend(Asteroids.PrimaryWeapon, Asteroids.Weapon, { doFire: function() { // generate a vector rotated to the player heading and then add the current player // vector to give the bullet the correct directional momentum var t = new Vector(0.0, -8.0); t.rotate(this.player.heading * RAD); t.add(this.player.vector); return new Asteroids.Bullet(this.player.position.clone(), t, this.player.heading); } }); })(); /** * Twin Cannons primary weapon for the player actor. * * @namespace Asteroids * @class Asteroids.TwinCannonsWeapon */ (function() { Asteroids.TwinCannonsWeapon = function(player) { Asteroids.TwinCannonsWeapon.superclass.constructor.call(this, player); return this; }; extend(Asteroids.TwinCannonsWeapon, Asteroids.Weapon, { doFire: function() { var t = new Vector(0.0, -8.0); t.rotate(this.player.heading * RAD); t.add(this.player.vector); return new Asteroids.BulletX2(this.player.position.clone(), t, this.player.heading); } }); })(); /** * V Spray Cannons primary weapon for the player actor. * * @namespace Asteroids * @class Asteroids.VSprayCannonsWeapon */ (function() { Asteroids.VSprayCannonsWeapon = function(player) { this.WEAPON_RECHARGE = 5; Asteroids.VSprayCannonsWeapon.superclass.constructor.call(this, player); return this; }; extend(Asteroids.VSprayCannonsWeapon, Asteroids.Weapon, { doFire: function() { var t, h; var bullets = []; h = this.player.heading - 15; t = new Vector(0.0, -7.0).rotate(h * RAD).add(this.player.vector); bullets.push(new Asteroids.Bullet(this.player.position.clone(), t, h)); h = this.player.heading; t = new Vector(0.0, -7.0).rotate(h * RAD).add(this.player.vector); bullets.push(new Asteroids.Bullet(this.player.position.clone(), t, h)); h = this.player.heading + 15; t = new Vector(0.0, -7.0).rotate(h * RAD).add(this.player.vector); bullets.push(new Asteroids.Bullet(this.player.position.clone(), t, h)); return bullets; } }); })(); /** * Side Guns additional primary weapon for the player actor. * * @namespace Asteroids * @class Asteroids.SideGunWeapon */ (function() { Asteroids.SideGunWeapon = function(player) { this.WEAPON_RECHARGE = 5; Asteroids.SideGunWeapon.superclass.constructor.call(this, player); return this; }; extend(Asteroids.SideGunWeapon, Asteroids.Weapon, { doFire: function() { var t, h; var bullets = []; h = this.player.heading - 90; t = new Vector(0.0, -8.0).rotate(h * RAD).add(this.player.vector); bullets.push(new Asteroids.Bullet(this.player.position.clone(), t, h, 25)); h = this.player.heading + 90; t = new Vector(0.0, -8.0).rotate(h * RAD).add(this.player.vector); bullets.push(new Asteroids.Bullet(this.player.position.clone(), t, h, 25)); return bullets; } }); })(); /** * Rear Gun additional primary weapon for the player actor. * * @namespace Asteroids * @class Asteroids.RearGunWeapon */ (function() { Asteroids.RearGunWeapon = function(player) { this.WEAPON_RECHARGE = 5; Asteroids.RearGunWeapon.superclass.constructor.call(this, player); return this; }; extend(Asteroids.RearGunWeapon, Asteroids.Weapon, { doFire: function() { var t = new Vector(0.0, -8.0); var h = this.player.heading + 180; t.rotate(h * RAD); t.add(this.player.vector); return new Asteroids.Bullet(this.player.position.clone(), t, h, 25); } }); })(); /** * Player Bullet actor class. * * @namespace Asteroids * @class Asteroids.Bullet */ (function() { Asteroids.Bullet = function(p, v, h, lifespan) { Asteroids.Bullet.superclass.constructor.call(this, p, v); this.heading = h; if (lifespan) { this.lifespan = lifespan; } return this; }; extend(Asteroids.Bullet, Game.Actor, { BULLET_WIDTH: 2, BULLET_HEIGHT: 6, FADE_LENGTH: 5, /** * Bullet heading */ heading: 0.0, /** * Bullet lifespan remaining */ lifespan: 40, /** * Bullet power energy */ powerLevel: 1, /** * Bullet rendering method * * @param ctx {object} Canvas rendering context */ onRender: function onRender(ctx) { var width = this.BULLET_WIDTH; var height = this.BULLET_HEIGHT; ctx.save(); ctx.globalCompositeOperation = "lighter"; if (this.lifespan < this.FADE_LENGTH) { // fade out ctx.globalAlpha = (1.0 / this.FADE_LENGTH) * this.lifespan; } if (BITMAPS) { ctx.shadowBlur = 8; ctx.shadowColor = ctx.fillStyle = "rgb(50,255,50)"; } else { ctx.shadowColor = ctx.strokeStyle = "rgb(50,255,50)"; } // rotate the little bullet rectangle into the correct heading ctx.translate(this.position.x, this.position.y); ctx.rotate(this.heading * RAD); var x = -(width / 2); var y = -(height / 2); if (BITMAPS) { ctx.fillRect(x, y, width, height); ctx.fillRect(x, y+1, width, height-1); } else { ctx.strokeRect(x, y-1, width, height+1); ctx.strokeRect(x, y, width, height); } ctx.restore(); }, /** * Actor expiration test * * @return true if expired and to be removed from the actor list, false if still in play */ expired: function expired() { // deduct lifespan from the bullet return (--this.lifespan === 0); }, /** * Area effect weapon radius - zero for primary bullets */ effectRadius: function effectRadius() { return 0; }, radius: function radius() { // approximate based on average between width and height return (this.BULLET_HEIGHT + this.BULLET_WIDTH) / 2; }, power: function power() { return this.powerLevel; } }); })(); /** * Player BulletX2 actor class. Used by the Twin Cannons primary weapon. * * @namespace Asteroids * @class Asteroids.BulletX2 */ (function() { Asteroids.BulletX2 = function(p, v, h) { Asteroids.BulletX2.superclass.constructor.call(this, p, v, h); this.lifespan = 50; this.powerLevel = 2; return this; }; extend(Asteroids.BulletX2, Asteroids.Bullet, { /** * Bullet rendering method * * @param ctx {object} Canvas rendering context */ onRender: function onRender(ctx) { var width = this.BULLET_WIDTH; var height = this.BULLET_HEIGHT; ctx.save(); ctx.globalCompositeOperation = "lighter"; if (this.lifespan < this.FADE_LENGTH) { // fade out ctx.globalAlpha = (1.0 / this.FADE_LENGTH) * this.lifespan; } if (BITMAPS) { ctx.shadowBlur = 8; ctx.shadowColor = ctx.fillStyle = "rgb(50,255,128)"; } else { ctx.shadowColor = ctx.strokeStyle = "rgb(50,255,128)"; } // rotate the little bullet rectangle into the correct heading ctx.translate(this.position.x, this.position.y); ctx.rotate(this.heading * RAD); var x = -(width / 2); var y = -(height / 2); if (BITMAPS) { ctx.fillRect(x - 4, y, width, height); ctx.fillRect(x - 4, y+1, width, height-1); ctx.fillRect(x + 4, y, width, height); ctx.fillRect(x + 4, y+1, width, height-1); } else { ctx.strokeRect(x - 4, y-1, width, height+1); ctx.strokeRect(x - 4, y, width, height); ctx.strokeRect(x + 4, y-1, width, height+1); ctx.strokeRect(x + 4, y, width, height); } ctx.restore(); }, radius: function radius() { // double width bullets - so bigger hit area than basic ones return (this.BULLET_HEIGHT); } }); })(); /** * Bomb actor class. * * @namespace Asteroids * @class Asteroids.Bomb */ (function() { Asteroids.Bomb = function(p, v) { Asteroids.Bomb.superclass.constructor.call(this, p, v); return this; }; extend(Asteroids.Bomb, Asteroids.Bullet, { BOMB_RADIUS: 4, FADE_LENGTH: 5, EFFECT_RADIUS: 45, /** * Bomb lifespan remaining */ lifespan: 80, /** * Bomb rendering method * * @param ctx {object} Canvas rendering context */ onRender: function onRender(ctx) { var rad = this.BOMB_RADIUS; ctx.save(); ctx.globalCompositeOperation = "lighter"; var alpha = 0.8; if (this.lifespan < this.FADE_LENGTH) { // fade out alpha = (0.8 / this.FADE_LENGTH) * this.lifespan; rad = (this.BOMB_RADIUS / this.FADE_LENGTH) * this.lifespan; } ctx.globalAlpha = alpha; if (BITMAPS) { ctx.fillStyle = "rgb(155,255,155)"; } else { ctx.shadowColor = ctx.strokeStyle = "rgb(155,255,155)"; } ctx.translate(this.position.x, this.position.y); ctx.rotate(GameHandler.frameCount % 360); // account for the fact that stroke() draws around the shape if (!BITMAPS) ctx.scale(0.8, 0.8); ctx.beginPath() ctx.moveTo(rad * 2, 0); for (var i=0; i<15; i++) { ctx.rotate(Math.PI / 8); if (i % 2 == 0) { ctx.lineTo((rad * 2 / 0.525731) * 0.200811, 0); } else { ctx.lineTo(rad * 2, 0); } } ctx.closePath(); if (BITMAPS) ctx.fill(); else ctx.stroke(); ctx.restore(); }, /** * Area effect weapon radius */ effectRadius: function effectRadius() { return this.EFFECT_RADIUS; }, radius: function radius() { var rad = this.BOMB_RADIUS; if (this.lifespan <= this.FADE_LENGTH) { rad = (this.BOMB_RADIUS / this.FADE_LENGTH) * this.lifespan; } return rad; } }); })(); /** * Enemy Bullet actor class. * * @namespace Asteroids * @class Asteroids.EnemyBullet */ (function() { Asteroids.EnemyBullet = function(p, v) { Asteroids.EnemyBullet.superclass.constructor.call(this, p, v); return this; }; extend(Asteroids.EnemyBullet, Game.Actor, { BULLET_RADIUS: 4, FADE_LENGTH: 5, /** * Bullet lifespan remaining */ lifespan: 60, /** * Bullet rendering method * * @param ctx {object} Canvas rendering context */ onRender: function onRender(ctx) { var rad = this.BULLET_RADIUS; ctx.save(); ctx.globalCompositeOperation = "lighter"; var alpha = 0.7; if (this.lifespan < this.FADE_LENGTH) { // fade out and make smaller alpha = (0.7 / this.FADE_LENGTH) * this.lifespan; rad = (this.BULLET_RADIUS / this.FADE_LENGTH) * this.lifespan; } ctx.globalAlpha = alpha; if (BITMAPS) { ctx.fillStyle = "rgb(150,255,150)"; } else { ctx.shadowColor = ctx.strokeStyle = "rgb(150,255,150)"; } ctx.beginPath(); ctx.arc(this.position.x, this.position.y, (rad-1 > 0 ? rad-1 : 0.1), 0, TWOPI, true); ctx.closePath(); if (BITMAPS) ctx.fill(); else ctx.stroke(); ctx.translate(this.position.x, this.position.y); ctx.rotate((GameHandler.frameCount % 720) / 2); ctx.beginPath() ctx.moveTo(rad * 2, 0); for (var i=0; i<7; i++) { ctx.rotate(Math.PI/4); if (i%2 == 0) { ctx.lineTo((rad * 2/0.525731) * 0.200811, 0); } else { ctx.lineTo(rad * 2, 0); } } ctx.closePath(); if (BITMAPS) ctx.fill(); else ctx.stroke(); ctx.restore(); }, /** * Actor expiration test * * @return true if expired and to be removed from the actor list, false if still in play */ expired: function expired() { // deduct lifespan from the bullet return (--this.lifespan === 0); }, radius: function radius() { var rad = this.BULLET_RADIUS; if (this.lifespan <= this.FADE_LENGTH) { rad = (rad / this.FADE_LENGTH) * this.lifespan; } return rad; } }); })(); /** * Basic explosion effect actor class. * * @namespace Asteroids * @class Asteroids.Explosion */ (function() { Asteroids.Explosion = function(p, v, s) { Asteroids.Explosion.superclass.constructor.call(this, p, v, this.FADE_LENGTH); this.size = s; return this; }; extend(Asteroids.Explosion, Game.EffectActor, { FADE_LENGTH: 10, /** * Explosion size */ size: 0, /** * Explosion rendering method * * @param ctx {object} Canvas rendering context */ onRender: function onRender(ctx) { // fade out var brightness = Math.floor((255 / this.FADE_LENGTH) * this.lifespan); var rad = (this.size * 8 / this.FADE_LENGTH) * this.lifespan; var rgb = brightness.toString(); ctx.save(); ctx.globalAlpha = 0.75; ctx.fillStyle = "rgb(" + rgb + ",0,0)"; ctx.beginPath(); ctx.arc(this.position.x, this.position.y, rad, 0, TWOPI, true); ctx.closePath(); ctx.fill(); ctx.restore(); } }); })(); /** * Player Explosion effect actor class. * * @namespace Asteroids * @class Asteroids.PlayerExplosion */ (function() { Asteroids.PlayerExplosion = function(p, v) { Asteroids.PlayerExplosion.superclass.constructor.call(this, p, v, this.FADE_LENGTH); return this; }; extend(Asteroids.PlayerExplosion, Game.EffectActor, { FADE_LENGTH: 15, /** * Explosion rendering method * * @param ctx {object} Canvas rendering context */ onRender: function onRender(ctx) { ctx.save(); var alpha = (1.0 / this.FADE_LENGTH) * this.lifespan; ctx.globalCompositeOperation = "lighter"; ctx.globalAlpha = alpha; var rad; if (this.lifespan > 5 && this.lifespan <= 15) { var offset = this.lifespan - 5; rad = (48 / this.FADE_LENGTH) * offset; ctx.fillStyle = "rgb(255,170,30)"; ctx.beginPath(); ctx.arc(this.position.x-2, this.position.y-2, rad, 0, TWOPI, true); ctx.closePath(); ctx.fill(); } if (this.lifespan > 2 && this.lifespan <= 12) { var offset = this.lifespan - 2; rad = (32 / this.FADE_LENGTH) * offset; ctx.fillStyle = "rgb(255,255,50)"; ctx.beginPath(); ctx.arc(this.position.x+2, this.position.y+2, rad, 0, TWOPI, true); ctx.closePath(); ctx.fill(); } if (this.lifespan <= 10) { var offset = this.lifespan; rad = (24 / this.FADE_LENGTH) * offset; ctx.fillStyle = "rgb(255,70,100)"; ctx.beginPath(); ctx.arc(this.position.x+2, this.position.y-2, rad, 0, TWOPI, true); ctx.closePath(); ctx.fill(); } ctx.restore(); } }); })(); /** * Impact effect (from bullet hitting an object) actor class. * * @namespace Asteroids * @class Asteroids.Impact */ (function() { Asteroids.Impact = function(p, v) { Asteroids.Impact.superclass.constructor.call(this, p, v, this.FADE_LENGTH); return this; }; extend(Asteroids.Impact, Game.EffectActor, { FADE_LENGTH: 12, /** * Impact effect rendering method * * @param ctx {object} Canvas rendering context */ onRender: function onRender(ctx) { // fade out alpha var alpha = (1.0 / this.FADE_LENGTH) * this.lifespan; ctx.save(); ctx.globalAlpha = alpha * 0.75; if (BITMAPS) { ctx.fillStyle = "rgb(50,255,50)"; } else { ctx.shadowColor = ctx.strokeStyle = "rgb(50,255,50)"; } ctx.beginPath(); ctx.arc(this.position.x, this.position.y, 2, 0, TWOPI, true); ctx.closePath(); if (BITMAPS) ctx.fill(); else ctx.stroke(); ctx.globalAlpha = alpha; ctx.beginPath(); ctx.arc(this.position.x, this.position.y, 1, 0, TWOPI, true); ctx.closePath(); if (BITMAPS) ctx.fill(); else ctx.stroke(); ctx.restore(); } }); })(); /** * Text indicator effect actor class. * * @namespace Asteroids * @class Asteroids.TextIndicator */ (function() { Asteroids.TextIndicator = function(p, v, msg, textSize, colour, fadeLength) { this.fadeLength = (fadeLength ? fadeLength : this.DEFAULT_FADE_LENGTH); Asteroids.TextIndicator.superclass.constructor.call(this, p, v, this.fadeLength); this.msg = msg; if (textSize) { this.textSize = textSize; } if (colour) { this.colour = colour; } return this; }; extend(Asteroids.TextIndicator, Game.EffectActor, { DEFAULT_FADE_LENGTH: 16, fadeLength: 0, textSize: 12, msg: null, colour: "rgb(255,255,255)", /** * Text indicator effect rendering method * * @param ctx {object} Canvas rendering context */ onRender: function onRender(ctx) { // fade out alpha var alpha = (1.0 / this.fadeLength) * this.lifespan; ctx.save(); ctx.globalAlpha = alpha; Game.fillText(ctx, this.msg, this.textSize + "pt Courier New", this.position.x, this.position.y, this.colour); ctx.restore(); } }); })(); /** * Score indicator effect actor class. * * @namespace Asteroids * @class Asteroids.ScoreIndicator */ (function() { Asteroids.ScoreIndicator = function(p, v, score, textSize, prefix, colour, fadeLength) { var msg = score.toString(); if (prefix) { msg = prefix + ' ' + msg; } Asteroids.ScoreIndicator.superclass.constructor.call(this, p, v, msg, textSize, colour, fadeLength); return this; }; extend(Asteroids.ScoreIndicator, Asteroids.TextIndicator, { }); })(); /** * Power up collectable. * * @namespace Asteroids * @class Asteroids.PowerUp */ (function() { Asteroids.PowerUp = function(p, v) { Asteroids.PowerUp.superclass.constructor.call(this, p, v); return this; }; extend(Asteroids.PowerUp, Game.EffectActor, { RADIUS: 8, pulse: 128, pulseinc: 8, /** * Power up rendering method * * @param ctx {object} Canvas rendering context */ onRender: function onRender(ctx) { ctx.save(); ctx.globalAlpha = 0.75; var col = "rgb(255," + this.pulse.toString() + ",0)"; if (BITMAPS) { ctx.fillStyle = col; ctx.strokeStyle = "rgb(255,255,128)"; } else { ctx.lineWidth = 2.0; ctx.shadowColor = ctx.strokeStyle = col; } ctx.beginPath(); ctx.arc(this.position.x, this.position.y, this.RADIUS, 0, TWOPI, true); ctx.closePath(); if (BITMAPS) { ctx.fill(); } ctx.stroke(); ctx.restore(); this.pulse += this.pulseinc; if (this.pulse > 255) { this.pulse = 256 - this.pulseinc; this.pulseinc =- this.pulseinc; } else if (this.pulse < 0) { this.pulse = 0 - this.pulseinc; this.pulseinc =- this.pulseinc; } }, radius: function radius() { return this.RADIUS; }, collected: function collected(game, player, scene) { // randomly select a powerup to apply var message = null; switch (randomInt(0, 9)) { case 0: case 1: // boost energy message = "Energy Boost!"; player.energy += player.ENERGY_INIT/2; if (player.energy > player.ENERGY_INIT) { player.energy = player.ENERGY_INIT; } break; case 2: // fire when shieled message = "Fire When Shielded!"; player.fireWhenShield = true; break; case 3: // extra life message = "Extra Life!"; game.lives++; break; case 4: // slow down asteroids message = "Slow Down Asteroids!"; for (var n = 0, m = scene.enemies.length, enemy; n < m; n++) { enemy = scene.enemies[n]; if (enemy instanceof Asteroids.Asteroid) { enemy.vector.scale(0.75); } } break; case 5: // smart bomb message = "Smart Bomb!"; var effectRad = 96; // add a BIG explosion actor at the smart bomb weapon position and vector var boom = new Asteroids.Explosion( this.position.clone(), this.vector.clone().scale(0.5), effectRad / 8); scene.effects.push(boom); // test circle intersection with each enemy actor // we check the enemy list length each iteration to catch baby asteroids // this is a fully fledged smart bomb after all! for (var n = 0, enemy, pos = this.position; n < scene.enemies.length; n++) { enemy = scene.enemies[n]; // test the distance against the two radius combined if (pos.distance(enemy.position) <= effectRad + enemy.radius()) { // intersection detected! enemy.hit(-1); scene.generatePowerUp(enemy); scene.destroyEnemy(enemy, this.vector, true); } } break; case 6: // twin cannon primary weapon upgrade message = "Twin Cannons!"; player.primaryWeapons["main"] = new Asteroids.TwinCannonsWeapon(player); break; case 7: // v spray cannons message = "Spray Cannons!"; player.primaryWeapons["main"] = new Asteroids.VSprayCannonsWeapon(player); break; case 8: // rear guns message = "Rear Gun!"; player.primaryWeapons["rear"] = new Asteroids.RearGunWeapon(player); break; case 9: // side guns message = "Side Guns!"; player.primaryWeapons["side"] = new Asteroids.SideGunWeapon(player); break; } if (message) { // generate a effect indicator at the destroyed enemy position var vec = new Vector(0, -3.0); var effect = new Asteroids.TextIndicator( new Vector(this.position.x, this.position.y - this.RADIUS), vec, message, null, null, 32); scene.effects.push(effect); } } }); })(); /** * Arena5 HTML canvas game. * Scenes for CanvasMark Rendering Benchmark - March 2013 * * (C) 2013 Kevin Roast kevtoast@yahoo.com @kevinroast * * Please see: license.txt * You are welcome to use this code, but I would appreciate an email or tweet * if you do anything interesting with it! */ /** * Arena root namespace. * * @namespace Arena */ if (typeof Arena == "undefined" || !Arena) { var Arena = {}; } /** * Arena prerenderer class. * * @namespace Arena * @class Arena.Prerenderer */ (function() { Arena.Prerenderer = function() { this.images = []; this._renderers = []; return this; }; Arena.Prerenderer.prototype = { /** * Image list. Keyed by renderer ID - returning an array also. So to get * the first image output by prerenderer with id "default": images["default"][0] * * @public * @property images * @type Array */ images: null, _renderers: null, /** * Add a renderer function to the list of renderers to execute * * @param fn {function} Callback to execute to perform prerender * Passed canvas element argument - to execute against - the * callback is responsible for setting appropriate width/height * of the buffer and should not assume it is cleared. * Should return Array of images from prerender process * @param id {string} Id of the prerender - used to lookup images later */ addRenderer: function addRenderer(fn, id) { this._renderers[id] = fn; }, /** * Execute all prerender functions - call once all renderers have been added */ execute: function execute() { var buffer = document.createElement('canvas'); for (var id in this._renderers) { this.images[id] = this._renderers[id].call(this, buffer); } } }; })(); /** * Arena main Benchmark Test class. * * @namespace Arena * @class Arena.Test */ (function() { Arena.Test = function(benchmark) { // generate the single player actor - available across all scenes this.player = new Arena.Player(new Vector(GameHandler.width / 2, GameHandler.height / 2), new Vector(0, 0), 0); // add benchmark scene benchmark.addBenchmarkScene(new Arena.BenchmarkScene(this, benchmark.scenes.length, 0)); // perform prerender steps - create bitmap graphics to use later var pr = new Arena.Prerenderer(); // function to generate a set of point particle images var fnPointRenderer = function(buffer, colour) { var imgs = []; for (var size=4; size<=10; size+=2) { var width = size << 1; buffer.width = buffer.height = width; var ctx = buffer.getContext('2d'); var radgrad = ctx.createRadialGradient(size, size, size >> 1, size, size, size); radgrad.addColorStop(0, colour); radgrad.addColorStop(1, "#000"); ctx.fillStyle = radgrad; ctx.fillRect(0, 0, width, width); var img = new Image(); img.src = buffer.toDataURL("image/png"); imgs.push(img); } return imgs; }; // add the various point particle image prerenderers based on above function // default explosion colour pr.addRenderer(function(buffer) { return fnPointRenderer.call(this, buffer, "rgb(255,125,50)"); }, "points_rgb(255,125,50)"); // Tracker: enemy particles pr.addRenderer(function(buffer) { return fnPointRenderer.call(this, buffer, "rgb(255,96,0)"); }, "points_rgb(255,96,0)"); // Borg: enemy particles pr.addRenderer(function(buffer) { return fnPointRenderer.call(this, buffer, "rgb(0,255,64)"); }, "points_rgb(0,255,64)"); // Splitter: enemy particles pr.addRenderer(function(buffer) { return fnPointRenderer.call(this, buffer, "rgb(148,0,255)"); }, "points_rgb(148,0,255)"); // Bomber: enemy particles pr.addRenderer(function(buffer) { return fnPointRenderer.call(this, buffer, "rgb(255,0,255)"); }, "points_rgb(255,0,255)"); // add the smudge explosion particle image prerenderer pr.addRenderer(function(buffer) { var imgs = []; for (var size=8; size<=64; size+=8) { var width = size << 1; buffer.width = buffer.height = width; var ctx = buffer.getContext('2d'); var radgrad = ctx.createRadialGradient(size, size, size >> 3, size, size, size); radgrad.addColorStop(0, "rgb(255,125,50)"); radgrad.addColorStop(1, "#000"); ctx.fillStyle = radgrad; ctx.fillRect(0, 0, width, width); var img = new Image(); img.src = buffer.toDataURL("image/png"); imgs.push(img); } return imgs; }, "smudges"); pr.addRenderer(function(buffer) { var imgs = []; var size = 40; buffer.width = buffer.height = size; var ctx = buffer.getContext('2d'); // draw bullet primary weapon var rf = function(width, height) { ctx.beginPath(); ctx.moveTo(0, height); ctx.lineTo(width, 0); ctx.lineTo(width, -height); ctx.lineTo(0, -height*0.5); ctx.lineTo(-width, -height); ctx.lineTo(-width, 0); ctx.closePath(); ctx.fill(); }; ctx.shadowBlur = 8; ctx.globalCompositeOperation = "lighter"; ctx.translate(size/2, size/2); ctx.shadowColor = "rgb(255,255,255)"; ctx.fillStyle = "rgb(255,220,75)"; rf.call(this, 10, 15) ctx.shadowColor = "rgb(255,100,100)"; ctx.fillStyle = "rgb(255,50,50)"; rf.call(this, 10 * 0.75, 15 * 0.75); var img = new Image(); img.src = buffer.toDataURL("image/png"); imgs.push(img); return imgs; }, "playerweapon"); pr.execute(); GameHandler.prerenderer = pr; }; Arena.Test.prototype = { /** * Reference to the single game player actor */ player: null, /** * Lives count */ lives: 1, /** * Current game score */ score: 0, /** * High score */ highscore: 0, /** * Last score */ lastscore: 0, /** * Current multipler */ scoreMultiplier: 1 }; })(); /** * Arena Game scene class. * * @namespace Arena * @class Arena.BenchmarkScene */ (function() { Arena.BenchmarkScene = function(game, test, feature) { this.game = game; this.test = test; this.feature = feature; this.player = game.player; var msg = "Test " + test + " - Arena5 - Vectors, shadows, bitmaps, text"; var interval = new Game.Interval(msg, this.intervalRenderer); Arena.BenchmarkScene.superclass.constructor.call(this, true, interval); }; extend(Arena.BenchmarkScene, Game.Scene, { test: 0, game: null, /** * Local reference to the game player actor */ player: null, /** * Top-level list of game actors sub-lists */ actors: null, /** * List of player fired bullet actors */ playerBullets: null, /** * List of enemy actors (asteroids, ships etc.) */ enemies: null, /** * List of enemy fired bullet actors */ enemyBullets: null, /** * List of effect actors */ effects: null, /** * List of collectables actors */ collectables: null, /** * Displayed score (animates towards actual score) */ scoredisplay: 0, /** * Scene init event handler */ onInitScene: function onInitScene() { // generate the actors and add the actor sub-lists to the main actor list this.actors = []; this.actors.push(this.enemies = []); this.actors.push(this.playerBullets = []); this.actors.push(this.enemyBullets = []); this.actors.push(this.effects = []); this.actors.push(this.collectables = []); // start view centered in the game world this.world.viewx = this.world.viewy = (this.world.size / 2) - (this.world.viewsize / 2); this.testScore = 10; // reset player this.resetPlayerActor(); }, /** * Restore the player to the game - reseting position etc. */ resetPlayerActor: function resetPlayerActor(persistPowerUps) { this.actors.push([this.player]); // reset the player position - centre of world with (this.player) { position.x = position.y = this.world.size / 2; vector.x = vector.y = heading = 0; reset(persistPowerUps); } }, /** * Scene before rendering event handler */ onBeforeRenderScene: function onBeforeRenderScene(benchmark) { var p = this.player, w = this.world; // update view position based on new player position var viewedge = w.viewsize * 0.2; if (p.position.x > viewedge && p.position.x < w.size - viewedge) { w.viewx = p.position.x - w.viewsize * 0.5; } if (p.position.y > viewedge && p.position.y < w.size - viewedge) { w.viewy = p.position.y - w.viewsize * 0.5; } if (benchmark) { if (Date.now() - this.sceneStartTime > this.testState) { this.testState += 500; for (var i=0; i<2; i++) { this.enemies.push(new Arena.EnemyShip(this, (this.enemies.length % 6) + 1)); } this.enemies[0].damageBy(100); this.destroyEnemy(this.enemies[0], new Vector(0,1), this.player) } } // update all actors using their current vector in the game world this.updateActors(); }, /** * Scene rendering event handler */ onRenderScene: function onRenderScene(ctx) { ctx.clearRect(0, 0, GameHandler.width, GameHandler.height); // glowing vector effect shadow ctx.shadowBlur = (DEBUG && DEBUG.DISABLEGLOWEFFECT) ? 0 : 8; // render background effect - wire grid this.renderBackground(ctx); // render the game actors this.renderActors(ctx); // render info overlay graphics this.renderOverlay(ctx); // detect bullet collisions this.collisionDetectBullets(); }, /** * Render background effects for the scene */ renderBackground: function renderBackground(ctx) { // render background effect - wire grid // manually transform world to screen for this effect and therefore // assume there is a horizonal and vertical "wire" every N units ctx.save(); ctx.strokeStyle = "rgb(0,30,60)"; ctx.lineWidth = 1.0; ctx.shadowBlur = 0; ctx.beginPath(); var UNIT = 100; var w = this.world; xoff = UNIT - w.viewx % UNIT, yoff = UNIT - w.viewy % UNIT, // calc top left edge of world (prescaled) x1 = (w.viewx >= 0 ? 0 : -w.viewx) * w.scale, y1 = (w.viewy >= 0 ? 0 : -w.viewy) * w.scale, // calc bottom right edge of world (prescaled) x2 = (w.viewx < w.size - w.viewsize ? w.viewsize : w.size - w.viewx) * w.scale, y2 = (w.viewy < w.size - w.viewsize ? w.viewsize : w.size - w.viewy) * w.scale; // plot the grid wires that make up the background for (var i=0, j=w.viewsize/UNIT; i 0 && xoff + w.viewx < w.size) { ctx.moveTo(xoff * w.scale, y1); ctx.lineTo(xoff * w.scale, y2); } if (yoff + w.viewy > 0 && yoff + w.viewy < w.size) { ctx.moveTo(x1, yoff * w.scale); ctx.lineTo(x2, yoff * w.scale); } xoff += UNIT; yoff += UNIT; } ctx.closePath(); ctx.stroke(); // render world edges ctx.strokeStyle = "rgb(60,128,90)"; ctx.lineWidth = 1; ctx.beginPath(); if (w.viewx <= 0) { xoff = -w.viewx; ctx.moveTo(xoff * w.scale, y1); ctx.lineTo(xoff * w.scale, y2); } else if (w.viewx >= w.size - w.viewsize) { xoff = w.size - w.viewx; ctx.moveTo(xoff * w.scale, y1); ctx.lineTo(xoff * w.scale, y2); } if (w.viewy <= 0) { yoff = -w.viewy; ctx.moveTo(x1, yoff * w.scale); ctx.lineTo(x2, yoff * w.scale); } else if (w.viewy >= w.size - w.viewsize) { yoff = w.size - w.viewy; ctx.moveTo(x1, yoff * w.scale); ctx.lineTo(x2, yoff * w.scale); } ctx.closePath(); ctx.stroke(); ctx.restore(); }, /** * Update the scene actors based on current vectors and expiration. */ updateActors: function updateActors() { for (var i = 0, j = this.actors.length; i < j; i++) { var actorList = this.actors[i]; for (var n = 0; n < actorList.length; n++) { var actor = actorList[n]; // call onUpdate() event for each actor actor.onUpdate(this); // expiration test first if (actor.expired()) { actorList.splice(n, 1); } else { // update actor using its current vector actor.position.add(actor.vector); // TODO: different behavior for traversing out of the world space? // add behavior flag to Actor i.e. bounce, invert, disipate etc. // - could add method to actor itself - so would handle internally... if (actor === this.player) { if (actor.position.x >= this.world.size || actor.position.x < 0 || actor.position.y >= this.world.size || actor.position.y < 0) { actor.vector.invert(); actor.vector.scale(0.75); actor.position.add(actor.vector); } } else { var bounceX = false, bounceY = false; if (actor.position.x >= this.world.size) { actor.position.x = this.world.size; bounceX = true; } else if (actor.position.x < 0) { actor.position.x = 0; bounceX = true; } if (actor.position.y >= this.world.size) { actor.position.y = this.world.size; bounceY = true; } else if (actor.position.y < 0) { actor.position.y = 0; bounceY = true } // bullets don't bounce - create an effect at the arena boundry instead if ((bounceX || bounceY) && ((actor instanceof Arena.Bullet && !this.player.bounceWeapons) || actor instanceof Arena.EnemyBullet)) { // replace bullet with a particle effect at the same position and vector var vec = actor.vector.nscale(0.5); this.effects.push(new Arena.BulletImpactEffect(actor.position.clone(), vec)); // remove bullet actor from play actorList.splice(n, 1); } else { if (bounceX) { var h = actor.vector.thetaTo2(new Vector(0, 1)); actor.vector.rotate(h*2); actor.vector.scale(0.9); actor.position.add(actor.vector); // TODO: add "interface" for actor with heading? // or is hasProperty() more "javascript" if (actor.hasOwnProperty("heading")) actor.heading += (h*2)/RAD; } if (bounceY) { var h = actor.vector.thetaTo2(new Vector(1, 0)); actor.vector.rotate(h*2); actor.vector.scale(0.9); actor.position.add(actor.vector); if (actor.hasOwnProperty("heading")) actor.heading += (h*2)/RAD; } } } } } } }, /** * Detect bullet collisions with enemy actors. */ collisionDetectBullets: function collisionDetectBullets() { var bullet, bulletRadius, bulletPos; // collision detect player bullets with enemies // NOTE: test length each loop as list length can change for (var i = 0; i < this.playerBullets.length; i++) { bullet = this.playerBullets[i]; bulletRadius = bullet.radius; bulletPos = bullet.position; // test circle intersection with each enemy actor for (var n = 0, m = this.enemies.length, enemy, z; n < m; n++) { enemy = this.enemies[n]; // test the distance against the two radius combined if (bulletPos.distance(enemy.position) <= bulletRadius + enemy.radius) { // test for area effect bomb weapon var effectRad = bullet.effectRadius(); //if (effectRad === 0) { // impact the enemy with the bullet - may destroy it or just damage it if (enemy.damageBy(bullet.power())) { // destroy the enemy under the bullet this.destroyEnemy(enemy, bullet.vector, true); this.generateMultiplier(enemy); this.generatePowerUp(enemy); } else { // add bullet impact effect to show the bullet hit var effect = new Arena.EnemyImpact( bullet.position.clone(), bullet.vector.nscale(0.5 + Rnd() * 0.5), enemy); this.effects.push(effect); } } // remove this bullet from the actor list as it has been destroyed this.playerBullets.splice(i, 1); break; } } } }, /** * Destroy an enemy. Replace with appropriate effect. * Also applies the score for the destroyed item if the player caused it. * * @param enemy {Game.EnemyActor} The enemy to destory and add score for * @param parentVector {Vector} The vector of the item that hit the enemy * @param player {boolean} If true, the player was the destroyer */ destroyEnemy: function destroyEnemy(enemy, parentVector, player) { // add an explosion actor at the enemy position and vector var vec = enemy.vector.clone(); // add scaled parent vector - to give some momentum from the impact vec.add(parentVector.nscale(0.2)); this.effects.push(new Arena.EnemyExplosion(enemy.position.clone(), vec, enemy)); if (player) { // increment score var inc = (enemy.scoretype + 1) * 5 * this.game.scoreMultiplier; this.game.score += inc; // generate a score effect indicator at the destroyed enemy position var vec = new Vector(0, -5.0).add(enemy.vector.nscale(0.5)); this.effects.push(new Arena.ScoreIndicator( new Vector(enemy.position.x, enemy.position.y - 16), vec, inc)); // call event handler for enemy enemy.onDestroyed(this, player); } }, /** * Generate score multiplier(s) for player to collect after enemy is destroyed */ generateMultiplier: function generateMultiplier(enemy) { if (enemy.dropsMutliplier) { var count = randomInt(1, (enemy.type < 5 ? enemy.type : 4)); for (var i=0; i= 0; n--) { actorList[n].onRender(ctx, this.world); } } }, /** * Render player information HUD overlay graphics. * * @param ctx {object} Canvas rendering context */ renderOverlay: function renderOverlay(ctx) { var w = this.world, width = GameHandler.width, height = GameHandler.height; ctx.save(); ctx.shadowBlur = 0; // energy bar (scaled down from player energy max) var ewidth = ~~(100 * w.scale * 2), eheight = ~~(4 * w.scale * 2); ctx.strokeStyle = "rgb(128,128,50)"; ctx.strokeRect(4, 4, ewidth+1, 4 + eheight); ctx.fillStyle = "rgb(255,255,150)"; ctx.fillRect(5, 5, (this.player.energy / (this.player.ENERGY_INIT / ewidth)), 3 + eheight); // score display - update towards the score in increments to animate it var font12pt = Game.fontFamily(w, 12), font12size = Game.fontSize(w, 12); var score = this.game.score, inc = (score - this.scoredisplay) * 0.1; this.scoredisplay += inc; if (this.scoredisplay > score) { this.scoredisplay = score; } var sscore = Ceil(this.scoredisplay).toString(); // pad with zeros for (var i=0, j=8-sscore.length; i this.game.highscore) { this.game.highscore = score; } sscore = this.game.highscore.toString(); // pad with zeros for (var i=0, j=8-sscore.length; i 270 || h < 90) { if (h > 270) this.heading -= ((h - 270) * 0.2); else this.heading -= ((h + 90) * 0.2); } else this.heading += ((270 - h) * 0.2); } if (input.right) { if (h < 90 || h > 270) { if (h < 90) this.heading += ((90 - h) * 0.2); else this.heading += ((h - 90) * 0.2); } else this.heading -= ((h - 90) * 0.2); } if (input.up) { if (h < 180) { this.heading -= (h * 0.2); } else this.heading += ((360 - h) * 0.2); } if (input.down) { if (h < 180) { this.heading += ((180 - h) * 0.2); } else this.heading -= ((h - 180) * 0.2); } // second section applies the direct thrust angled vector // this ensures a snappy control method with the above heading effect var angle = null; if (input.left) { if (input.up) angle = 315; else if (input.down) angle = 225; else angle = 270; } else if (input.right) { if (input.up) angle = 45; else if (input.down) angle = 135; else angle = 90; } else if (input.up) { if (input.left) angle = 315; else if (input.right) angle = 45; else angle = 0; } else if (input.down) { if (input.left) angle = 225; else if (input.right) angle = 135; else angle = 180; } if (angle !== null) { this.thrust(angle); } else { // reduce thrust over time if player isn't actively moving this.vector.scale(0.9); } }, /** * Execute player forward thrust request * Automatically a delay is used between each application - to ensure stable thrust on all machines. */ thrust: function thrust(angle) { // now test we did not thrust too recently - to stop fast key repeat issues if (GameHandler.frameCount - this.thrustRecharge > this.THRUST_DELAY) { // update last frame count this.thrustRecharge = GameHandler.frameCount; // generate a small thrust vector var t = new Vector(0.0, -2.00); // rotate thrust vector by player current heading t.rotate(angle * RAD); // add player thrust vector to position this.vector.add(t); // player can't exceed maximum velocity - scale vector down if // this occurs - do this rather than not adding the thrust at all // otherwise the player cannot turn and thrust at max velocity if (this.vector.length() > this.MAX_PLAYER_VELOCITY) { this.vector.scale(this.MAX_PLAYER_VELOCITY / this.vector.length()); } } // mark so that we know to render engine thrust graphics this.engineThrust = true; }, damageBy: function damageBy(enemy) { this.energy -= enemy.playerDamage; if (this.energy <= 0) { this.energy = 0; this.kill(); } }, kill: function kill() { this.alive = false; this.killedOnFrame = GameHandler.frameCount; }, /** * Fire primary weapon(s) * @param bulletList {Array} to add bullet(s) to on success * @param heading {Number} bullet heading */ firePrimary: function firePrimary(bulletList, vector, heading) { // attempt to fire the primary weapon(s) // first ensure player is alive if (this.alive) { for (var w in this.primaryWeapons) { var b = this.primaryWeapons[w].fire(vector, heading); if (b) { for (var i=0; i player.ENERGY_INIT) { player.energy = player.ENERGY_INIT; } // display indicator var vec = new Vector(0, -5.0).add(this.vector); scene.effects.push(new Arena.TextIndicator( this.position.clone(), vec, "Energy Boost!", 32, "white", 32)); } }); })(); /** * Weapon system base class for the player actor. * * @namespace Arena * @class Arena.Weapon */ (function() { Arena.Weapon = function(player) { this.player = player; return this; }; Arena.Weapon.prototype = { rechargeTime: 3, weaponRecharged: 0, player: null, fire: function(v, h) { // now test we did not fire too recently if (GameHandler.frameCount - this.weaponRecharged > this.rechargeTime) { // ok, update last fired frame and we can now generate a bullet this.weaponRecharged = GameHandler.frameCount; return this.doFire(v, h); } }, doFire: function(v, h) { } }; })(); /** * Basic primary weapon for the player actor. * * @namespace Arena * @class Arena.PrimaryWeapon */ (function() { Arena.PrimaryWeapon = function(player) { Arena.PrimaryWeapon.superclass.constructor.call(this, player); this.rechargeTime = this.DEFAULT_RECHARGE; return this; }; extend(Arena.PrimaryWeapon, Arena.Weapon, { DEFAULT_RECHARGE: 5, bulletCount: 1, // increase this to output more intense bullet stream doFire: function(vector, heading) { var bullets = [], count = this.bulletCount, total = (count > 2 ? randomInt(count - 1, count) : count); for (var i=0; i 1 ? Rnd() * PIO16 * (count-1) : 0), h = heading + offset - (PIO32 * (count-1)), v = vector.nrotate(offset - (PIO32 * (count-1))).scale(1 + Rnd() * 0.1 - 0.05); v.add(this.player.vector); bullets.push(new Arena.Bullet(this.player.position.clone(), v, h)); } return bullets; } }); })(); /** * Player Bullet actor class. * * @namespace Arena * @class Arena.Bullet */ (function() { Arena.Bullet = function(p, v, h, lifespan) { Arena.Bullet.superclass.constructor.call(this, p, v); this.heading = h; this.lifespan = (lifespan ? lifespan : this.BULLET_LIFESPAN); this.radius = this.BULLET_RADIUS; return this; }; extend(Arena.Bullet, Game.Actor, { BULLET_RADIUS: 12, BULLET_LIFESPAN: 30, FADE_LENGTH: 5, /** * Bullet heading */ heading: 0, /** * Bullet lifespan remaining */ lifespan: 0, /** * Bullet power energy */ powerLevel: 1, /** * Bullet rendering method * * @param ctx {object} Canvas rendering context * @param world {object} World metadata */ onRender: function onRender(ctx, world) { ctx.save(); ctx.shadowBlur = 0; ctx.globalCompositeOperation = "lighter"; if (this.worldToScreen(ctx, world, this.BULLET_RADIUS) && this.lifespan < this.BULLET_LIFESPAN - 1) // hack - to stop draw over player ship { if (this.lifespan < this.FADE_LENGTH) { ctx.globalAlpha = (1.0 / this.FADE_LENGTH) * this.lifespan; } // rotate into the correct heading ctx.rotate(this.heading * RAD); // draw bullet primary weapon try { ctx.drawImage(GameHandler.prerenderer.images["playerweapon"][0], -20, -20); } catch (error) { if (console !== undefined) console.log(error.message); } } ctx.restore(); }, /** * Actor expiration test * * @return true if expired and to be removed from the actor list, false if still in play */ expired: function expired() { // deduct lifespan from the bullet return (--this.lifespan === 0); }, /** * Area effect weapon radius - zero for primary bullets */ effectRadius: function effectRadius() { return 0; }, power: function power() { return this.powerLevel; } }); })(); /** * Enemy Bullet actor class. * * @namespace Arena * @class Arena.EnemyBullet */ (function() { Arena.EnemyBullet = function(p, v, power) { Arena.EnemyBullet.superclass.constructor.call(this, p, v); this.powerLevel = this.playerDamage = power; this.lifespan = this.BULLET_LIFESPAN; this.radius = this.BULLET_RADIUS; return this; }; extend(Arena.EnemyBullet, Game.Actor, { BULLET_LIFESPAN: 75, BULLET_RADIUS: 10, FADE_LENGTH: 8, powerLevel: 0, playerDamage: 0, /** * Bullet lifespan remaining */ lifespan: 0, /** * Bullet rendering method * * @param ctx {object} Canvas rendering context * @param world {object} World metadata */ onRender: function onRender(ctx, world) { ctx.save(); ctx.globalCompositeOperation = "lighter"; if (this.worldToScreen(ctx, world, this.BULLET_RADIUS) && this.lifespan < this.BULLET_LIFESPAN - 1) // hack - to stop draw over enemy { if (this.lifespan < this.FADE_LENGTH) { ctx.globalAlpha = (1.0 / this.FADE_LENGTH) * this.lifespan; } ctx.shadowColor = ctx.fillStyle = "rgb(150,255,150)"; var rad = this.BULLET_RADIUS - 2; ctx.beginPath(); ctx.arc(0, 0, (rad-1 > 0 ? rad-1 : 0.1), 0, TWOPI, true); ctx.closePath(); ctx.fill(); ctx.rotate((GameHandler.frameCount % 1800) / 5); ctx.beginPath() ctx.moveTo(rad * 2, 0); for (var i=0; i<7; i++) { ctx.rotate(PIO4); if (i%2 === 0) { ctx.lineTo((rad * 2 / 0.5) * 0.2, 0); } else { ctx.lineTo(rad * 2, 0); } } ctx.closePath(); ctx.fill(); } ctx.restore(); }, /** * Actor expiration test * * @return true if expired and to be removed from the actor list, false if still in play */ expired: function expired() { // deduct lifespan from the bullet return (--this.lifespan === 0); }, power: function power() { return this.powerLevel; } }); })(); /** * Enemy Ship actor class. * * @namespace Arena * @class Arena.EnemyShip */ (function() { Arena.EnemyShip = function(scene, type) { // enemy score multiplier based on type buy default - but some enemies // will tweak this in the individual setup code later this.type = this.scoretype = type; // generate enemy at start position - not too close to the player var p, v = null; while (!v) { p = new Vector(Rnd() * scene.world.size, Rnd() * scene.world.size); if (scene.player.position.distance(p) > 220) { v = new Vector(0,0); } } Arena.EnemyShip.superclass.constructor.call(this, p, v); // 3D sprite object - must be created after constructor call var me = this; var obj = new K3D.K3DObject(); with (obj) { drawmode = "wireframe"; shademode = "depthcue"; depthscale = 32; linescale = 3; perslevel = 256; switch (type) { case 0: // Dumbo: blue stretched cubiod me.radius = 22; me.playerDamage = 10; me.colorRGB = "rgb(0,128,255)"; color = [0,128,255]; addphi = -1.0; addgamma = -0.75; init( [{x:-20,y:-20,z:12}, {x:-20,y:20,z:12}, {x:20,y:20,z:12}, {x:20,y:-20,z:12}, {x:-10,y:-10,z:-12}, {x:-10,y:10,z:-12}, {x:10,y:10,z:-12}, {x:10,y:-10,z:-12}], [{a:0,b:1}, {a:1,b:2}, {a:2,b:3}, {a:3,b:0}, {a:4,b:5}, {a:5,b:6}, {a:6,b:7}, {a:7,b:4}, {a:0,b:4}, {a:1,b:5}, {a:2,b:6}, {a:3,b:7}], []); break; case 1: // Zoner: yellow diamond me.radius = 22; me.playerDamage = 10; me.colorRGB = "rgb(255,255,0)"; color = [255,255,0]; addphi = 0.5; addgamma = -0.5; addtheta = -1.0; init( [{x:-20,y:-20,z:0}, {x:-20,y:20,z:0}, {x:20,y:20,z:0}, {x:20,y:-20,z:0}, {x:0,y:0,z:-20}, {x:0,y:0,z:20}], [{a:0,b:1}, {a:1,b:2}, {a:2,b:3}, {a:3,b:0}, {a:0,b:4}, {a:1,b:4}, {a:2,b:4}, {a:3,b:4}, {a:0,b:5}, {a:1,b:5}, {a:2,b:5}, {a:3,b:5}], []); break; case 2: // Tracker: red flattened square me.radius = 22; me.health = 2; me.playerDamage = 15; me.colorRGB = "rgb(255,96,0)"; color = [255,96,0]; addgamma = 1.0; init( [{x:-20,y:-20,z:5}, {x:-20,y:20,z:5}, {x:20,y:20,z:5}, {x:20,y:-20,z:5}, {x:-15,y:-15,z:-5}, {x:-15,y:15,z:-5}, {x:15,y:15,z:-5}, {x:15,y:-15,z:-5}], [{a:0,b:1}, {a:1,b:2}, {a:2,b:3}, {a:3,b:0}, {a:4,b:5}, {a:5,b:6}, {a:6,b:7}, {a:7,b:4}, {a:0,b:4}, {a:1,b:5}, {a:2,b:6}, {a:3,b:7}], []); break; case 3: // Borg: big green cube me.radius = 52; me.health = 5; me.playerDamage = 25; me.colorRGB = "rgb(0,255,64)"; color = [0,255,64]; depthscale = 96; // tweak for larger object addphi = -1.5; init( [{x:-40,y:-40,z:40}, {x:-40,y:40,z:40}, {x:40,y:40,z:40}, {x:40,y:-40,z:40}, {x:-40,y:-40,z:-40}, {x:-40,y:40,z:-40}, {x:40,y:40,z:-40}, {x:40,y:-40,z:-40}], [{a:0,b:1}, {a:1,b:2}, {a:2,b:3}, {a:3,b:0}, {a:4,b:5}, {a:5,b:6}, {a:6,b:7}, {a:7,b:4}, {a:0,b:4}, {a:1,b:5}, {a:2,b:6}, {a:3,b:7}], []); break; case 4: // Dodger: small cyan cube me.radius = 25; me.playerDamage = 10; me.colorRGB = "rgb(0,255,255)"; color = [0,255,255]; addphi = 0.5; addtheta = -3.0; init( [{x:-20,y:-20,z:20}, {x:-20,y:20,z:20}, {x:20,y:20,z:20}, {x:20,y:-20,z:20}, {x:-20,y:-20,z:-20}, {x:-20,y:20,z:-20}, {x:20,y:20,z:-20}, {x:20,y:-20,z:-20}], [{a:0,b:1}, {a:1,b:2}, {a:2,b:3}, {a:3,b:0}, {a:4,b:5}, {a:5,b:6}, {a:6,b:7}, {a:7,b:4}, {a:0,b:4}, {a:1,b:5}, {a:2,b:6}, {a:3,b:7}], []); break; case 5: // Splitter: medium purple pyrimid (converts to 2x smaller versions when hit) me.radius = 25; me.health = 3; me.playerDamage = 20; me.colorRGB = "rgb(148,0,255)"; color = [148,0,255]; depthscale = 56; // tweak for larger object addphi = 3.0; init( [{x:-30,y:-20,z:0}, {x:0,y:-20,z:30}, {x:30,y:-20,z:0}, {x:0,y:-20,z:-30}, {x:0,y:30,z:0}], [{a:0,b:1}, {a:1,b:2}, {a:2,b:3}, {a:3,b:0}, {a:0,b:4}, {a:1,b:4}, {a:2,b:4}, {a:3,b:4}], []); break; case 6: // Bomber: medium magenta star - dodge bullets, dodge player! me.radius = 28; me.health = 5; me.playerDamage = 20; me.colorRGB = "rgb(255,0,255)"; color = [255,0,255]; depthscale = 56; // tweak for larger object addgamma = -5.0; init( [{x:-30,y:-30,z:10}, {x:-30,y:30,z:10}, {x:30,y:30,z:10}, {x:30,y:-30,z:10}, {x:-15,y:-15,z:-15}, {x:-15,y:15,z:-15}, {x:15,y:15,z:-15}, {x:15,y:-15,z:-15}], [{a:0,b:1}, {a:1,b:2}, {a:2,b:3}, {a:3,b:0}, {a:4,b:5}, {a:5,b:6}, {a:6,b:7}, {a:7,b:4}, {a:0,b:4}, {a:1,b:5}, {a:2,b:6}, {a:3,b:7}], []); break; case 99: // Splitter-mini: see Splitter above me.scoretype = 4; // override default score type setting me.dropsMutliplier = false; me.radius = 12; me.health = 1; me.playerDamage = 5; me.colorRGB = "rgb(148,0,211)"; color = [148,0,211]; depthscale = 16; // tweak for smaller object addphi = 5.0; init( [{x:-15,y:-10,z:0}, {x:0,y:-10,z:15}, {x:15,y:-10,z:0}, {x:0,y:-10,z:-15}, {x:0,y:15,z:0}], [{a:0,b:1}, {a:1,b:2}, {a:2,b:3}, {a:3,b:0}, {a:0,b:4}, {a:1,b:4}, {a:2,b:4}, {a:3,b:4}], []); break; } } this.setK3DObject(obj); return this; }; extend(Arena.EnemyShip, Arena.K3DActor, { BULLET_RECHARGE: 50, SPAWN_LENGTH: 20, // TODO: replace this with anim state machine aliveTime: 0, // TODO: replace this with anim state machine type: 0, scoretype: 0, dropsMutliplier: true, health: 1, colorRGB: null, playerDamage: 0, bulletRecharge: 0, hit: false, // TODO: replace with state? - "extends" default render state...? onUpdate: function onUpdate(scene) { // TODO: replace this with anim state machine if (++this.aliveTime < this.SPAWN_LENGTH) { // TODO: needs enemy state implemented so can test for "alive" state // for collision detection? // other methods can then test state such as onRender() // SPAWNED->ALIVE->DEAD return; } else if (this.aliveTime === this.SPAWN_LENGTH) { // initial vector needed for some enemy types - others will set later this.vector = new Vector(4 * (Rnd < 0.5 ? 1 : -1), 4 * (Rnd < 0.5 ? 1 : -1)); } switch (this.type) { case 0: // dumb - change direction randomly if (Rnd() < 0.01) { this.vector.y = -(this.vector.y + (0.5 - Rnd())); } break; case 1: // randomly reorientate towards player ("perception level") // so player can avade by moving around them if (Rnd() < 0.04) { // head towards player - generate a vector pointed at the player // by calculating a vector between the player and enemy positions var v = scene.player.position.nsub(this.position); // scale resulting vector down to fixed vector size i.e. speed this.vector = v.scaleTo(4); } break; case 2: // very perceptive and faster - this one is mean if (Rnd() < 0.2) { var v = scene.player.position.nsub(this.position); this.vector = v.scaleTo(8); } break; case 3: // fast dash towards player, otherwise it slows down if (Rnd() < 0.03) { var v = scene.player.position.nsub(this.position); this.vector = v.scaleTo(12); } else { this.vector.scale(0.95); } break; case 4: // perceptive and fast - and tries to dodgy bullets! var dodged = false; // if we are close to the player then don't try and dodge, // otherwise enemy might dash away rather than go for the kill if (scene.player.position.nsub(this.position).length() > 150) { var p = this.position, r = this.radius + 50; // bullet "distance" perception // look at player bullets list - are any about to hit? for (var i=0, j=scene.playerBullets.length, bullet, n; i < j; i++) { bullet = scene.playerBullets[i]; // test the distance against the two radius combined if (bullet.position.distance(p) <= bullet.radius + r) { // if so attempt a fast sideways dodge! var v = bullet.position.nsub(p).scaleTo(12); // randomise dodge direction a bit v.rotate((n = Rnd()) < 0.5 ? n*PIO4 : -n*PIO4); v.invert(); this.vector = v; dodged = true; break; } } } if (!dodged && Rnd() < 0.04) { var v = scene.player.position.nsub(this.position); this.vector = v.scaleTo(8); } break; case 5: if (Rnd() < 0.04) { var v = scene.player.position.nsub(this.position); this.vector = v.scaleTo(5); } break; case 6: // if we are near the player move away // if we are far from the player move towards var v = scene.player.position.nsub(this.position); if (v.length() > 400) { // move closer if (Rnd() < 0.08) this.vector = v.scaleTo(8); } else if (v.length() < 350) { // move away if (Rnd() < 0.08) this.vector = v.invert().scaleTo(8); } else { // slow down into a firing position this.vector.scale(0.8); // reguarly fire at the player if (GameHandler.frameCount - this.bulletRecharge > this.BULLET_RECHARGE && scene.player.alive) { // update last fired frame and generate a bullet this.bulletRecharge = GameHandler.frameCount; // generate a vector pointed at the player // by calculating a vector between the player and enemy positions // then scale to a fixed size - i.e. bullet speed var v = scene.player.position.nsub(this.position).scaleTo(10); // slightly randomize the direction to apply some accuracy issues v.x += (Rnd() * 2 - 1); v.y += (Rnd() * 2 - 1); var bullet = new Arena.EnemyBullet(this.position.clone(), v, 10); scene.enemyBullets.push(bullet); } } break; case 99: if (Rnd() < 0.04) { var v = scene.player.position.nsub(this.position); this.vector = v.scaleTo(8); } break; } }, /** * Enemy rendering method * * @param ctx {object} Canvas rendering context * @param world {object} World metadata */ onRender: function onRender(ctx, world) { ctx.save(); if (this.worldToScreen(ctx, world, this.radius)) { // render 3D sprite if (!this.hit) { ctx.shadowColor = this.colorRGB; } else { // override colour with plain white for "hit" effect ctx.shadowColor = "white"; var oldColor = this.k3dObject.color; this.k3dObject.color = [255,255,255]; this.k3dObject.shademode = "plain"; } // TODO: replace this with anim state machine test... // TODO: adjust RADIUS for collision etc. during spawn! if (this.aliveTime < this.SPAWN_LENGTH) { // nifty scaling effect as an enemy spawns into position var scale = 1 - (this.SPAWN_LENGTH - this.aliveTime) / this.SPAWN_LENGTH; if (scale <= 0) scale = 0.01; else if (scale > 1) scale = 1; ctx.scale(scale, scale); } this.renderK3D(ctx); if (this.hit) { // restore colour and depthcue rendering mode this.k3dObject.color = oldColor; this.k3dObject.shademode = "depthcue"; this.hit = false; } } ctx.restore(); }, damageBy: function damageBy(force) { // record hit - will change enemy colour for a single frame this.hit = true; if (force === -1 || (this.health -= force) <= 0) { this.alive = false; } return !this.alive; }, onDestroyed: function onDestroyed(scene, player) { if (this.type === 5) { // Splitter enemy divides into two smaller ones var enemy = new Arena.EnemyShip(scene, 99); // update position and vector // TODO: move this as option in constructor enemy.vector = this.vector.nrotate(PIO2); enemy.position = this.position.nadd(enemy.vector); scene.enemies.push(enemy); enemy = new Arena.EnemyShip(scene, 99); enemy.vector = this.vector.nrotate(-PIO2); enemy.position = this.position.nadd(enemy.vector); scene.enemies.push(enemy); } } }); })(); /** * Particle emitter effect actor class. * * A simple particle emitter, that does not recycle particles, but sets itself as expired() once * all child particles have expired. * * Requires a function known as the emitter that is called per particle generated. * * @namespace Arena * @class Arena.Particles */ (function() { /** * Constructor * * @param p {Vector} Emitter position * @param v {Vector} Emitter velocity * @param count {Integer} Number of particles * @param fnEmitter {Function} Emitter function to call per particle generated */ Arena.Particles = function(p, v, count, fnEmitter) { Arena.Particles.superclass.constructor.call(this, p, v); // generate particles based on the supplied emitter function this.particles = new Array(count); for (var i=0; i 1 // lookup based on particle colour e.g. points_rgb(x,y,z) ctx.drawImage( GameHandler.prerenderer.images["points_" + this.colour][this.size], 0, 0); break; case 1: // line var s = this.size; ctx.rotate(this.rotate); this.rotate += this.rotationv; // specific line colour - for enemy explosion pieces ctx.strokeStyle = this.colour; ctx.lineWidth = 2.0; ctx.beginPath(); ctx.moveTo(-s, -s); ctx.lineTo(s, s); ctx.closePath(); ctx.stroke(); break; case 2: // smudge (prerendered image) ctx.drawImage(GameHandler.prerenderer.images["smudges"][this.size - 4], 0, 0); break; } } catch (error) { if (console !== undefined) console.log(error.message); } }; } /** * Enemy explosion - Particle effect actor class. * * @namespace Arena * @class Arena.EnemyExplosion */ (function() { /** * Constructor */ Arena.EnemyExplosion = function(p, v, enemy) { Arena.EnemyExplosion.superclass.constructor.call(this, p, v, 16, function() { // randomise start position slightly var pos = p.clone(); pos.x += randomInt(-5, 5); pos.y += randomInt(-5, 5); // randomise radial direction vector - speed and angle, then add parent vector switch (randomInt(0, 2)) { case 0: var t = new Vector(0, randomInt(20, 25)); t.rotate(Rnd() * TWOPI); t.add(v); return new ArenaParticle( pos, t, ~~(Rnd() * 4), 0, 20, 15); case 1: var t = new Vector(0, randomInt(5, 10)); t.rotate(Rnd() * TWOPI); t.add(v); // create line particle - size based on enemy type return new ArenaParticle( pos, t, (enemy.type !== 3 ? Rnd() * 5 + 5 : Rnd() * 10 + 10), 1, 20, 15, enemy.colorRGB); case 2: var t = new Vector(0, randomInt(2, 4)); t.rotate(Rnd() * TWOPI); t.add(v); return new ArenaParticle( pos, t, ~~(Rnd() * 4 + 4), 2, 20, 15); } }); return this; }; extend(Arena.EnemyExplosion, Arena.Particles); })(); /** * Enemy impact effect - Particle effect actor class. * Used when an enemy is hit by player bullet but not destroyed. * * @namespace Arena * @class Arena.EnemyImpact */ (function() { /** * Constructor */ Arena.EnemyImpact = function(p, v, enemy) { Arena.EnemyImpact.superclass.constructor.call(this, p, v, 5, function() { // slightly randomise vector angle - then add parent vector var t = new Vector(0, Rnd() < 0.5 ? randomInt(-5, -10) : randomInt(5, 10)); t.rotate(Rnd() * PIO2 - PIO4); t.add(v); return new ArenaParticle( p.clone(), t, ~~(Rnd() * 4), 0, 15, 10, enemy.colorRGB); }); return this; }; extend(Arena.EnemyImpact, Arena.Particles); })(); /** * Bullet impact effect - Particle effect actor class. * Used when an bullet hits an object and is destroyed. * * @namespace Arena * @class Arena.BulletImpactEffect */ (function() { /** * Constructor */ Arena.BulletImpactEffect = function(p, v, enemy) { Arena.BulletImpactEffect.superclass.constructor.call(this, p, v, 3, function() { return new ArenaParticle( p.clone(), v.nrotate(Rnd()*PIO8), ~~(Rnd() * 4), 0, 15, 10); }); return this; }; extend(Arena.BulletImpactEffect, Arena.Particles); })(); /** * Player explosion - Particle effect actor class. * * @namespace Arena * @class Arena.PlayerExplosion */ (function() { /** * Constructor */ Arena.PlayerExplosion = function(p, v) { Arena.PlayerExplosion.superclass.constructor.call(this, p, v, 20, function() { // randomise start position slightly var pos = p.clone(); pos.x += randomInt(-5, 5); pos.y += randomInt(-5, 5); // randomise radial direction vector - speed and angle, then add parent vector switch (randomInt(1,2)) { case 1: var t = new Vector(0, randomInt(5, 8)); t.rotate(Rnd() * TWOPI); t.add(v); return new ArenaParticle( pos, t, Rnd() * 5 + 5, 1, 25, 15, "white"); case 2: var t = new Vector(0, randomInt(5, 10)); t.rotate(Rnd() * TWOPI); t.add(v); return new ArenaParticle( pos, t, ~~(Rnd() * 4 + 4), 2, 25, 15); } }); return this; }; extend(Arena.PlayerExplosion, Arena.Particles); })(); /** * Text indicator effect actor class. * * @namespace Arena * @class Arena.TextIndicator */ (function() { Arena.TextIndicator = function(p, v, msg, textSize, colour, fadeLength) { this.fadeLength = (fadeLength ? fadeLength : this.DEFAULT_FADE_LENGTH); Arena.TextIndicator.superclass.constructor.call(this, p, v, this.fadeLength); this.msg = msg; if (textSize) { this.textSize = textSize; } if (colour) { this.colour = colour; } return this; }; extend(Arena.TextIndicator, Game.EffectActor, { DEFAULT_FADE_LENGTH: 16, fadeLength: 0, textSize: 22, msg: null, colour: "rgb(255,255,255)", /** * Text indicator effect rendering method * * @param ctx {object} Canvas rendering context */ onRender: function onRender(ctx, world) { ctx.save(); if (this.worldToScreen(ctx, world, 128)) { var alpha = (1.0 / this.fadeLength) * this.lifespan; ctx.globalAlpha = alpha; ctx.shadowBlur = 0; Game.fillText(ctx, this.msg, this.textSize + "pt Courier New", 0, 0, this.colour); } ctx.restore(); } }); })(); /** * Score indicator effect actor class. * * @namespace Arena * @class Arena.ScoreIndicator */ (function() { Arena.ScoreIndicator = function(p, v, score, textSize, prefix, colour, fadeLength) { var msg = score.toString(); if (prefix) { msg = prefix + ' ' + msg; } Arena.ScoreIndicator.superclass.constructor.call(this, p, v, msg, textSize, colour, fadeLength); return this; }; extend(Arena.ScoreIndicator, Arena.TextIndicator, { }); })(); /** * CanvasMark HTML5 Canvas Rendering Benchmark - March 2013 * * @email kevtoast at yahoo dot com * @twitter kevinroast * * (C) 2013 Kevin Roast * * Please see: license.txt * You are welcome to use this code, but I would appreciate an email or tweet * if you do anything interesting with it! */ window.addEventListener('load', onloadHandler, false); /** * Global window onload handler */ var g_splashImg = new Image(); function onloadHandler() { // once the slash screen is loaded, bootstrap the main benchmark class g_splashImg.src = 'images/canvasmark2013.jpg'; g_splashImg.onload = function() { // init our game with Game.Main derived instance GameHandler.init(); GameHandler.start(new Benchmark.Main()); }; } /** * Benchmark root namespace. * * @namespace Benchmark */ if (typeof Benchmark == "undefined" || !Benchmark) { var Benchmark = {}; } /** * Benchmark main class. * * @namespace Benchmark * @class Benchmark.Main */ (function() { Benchmark.Main = function() { Benchmark.Main.superclass.constructor.call(this); // create the scenes that are directly part of the Benchmark container var infoScene = new Benchmark.InfoScene(this); // add the info scene - must be added first this.scenes.push(infoScene); // create the Test instances that the benchmark should manage // each Test instance will add child scenes to the benchmark var loader = new Game.Preloader(); this.asteroidsTest = new Asteroids.Test(this, loader); this.arenaTest = new Arena.Test(this, loader); this.featureTest = new Feature.Test(this, loader); // add benchmark completed scene this.scenes.push(new Benchmark.CompletedScene(this)); // the benchmark info scene is displayed first and responsible for allowing the // benchmark to start once images required by the game engines have been loaded loader.onLoadCallback(function() { infoScene.ready(); }); }; extend(Benchmark.Main, Game.Main, { asteroidsTest: null, arenaTest: null, featureTest: null, addBenchmarkScene: function addBenchmarkScene(scene) { this.scenes.push(scene); } }); })(); /** * Benchmark Benchmark Info Scene scene class. * * @namespace Benchmark * @class Benchmark.InfoScene */ (function() { Benchmark.InfoScene = function(game) { this.game = game; // allow start via mouse click - also for starting benchmark on touch devices var me = this; var fMouseDown = function(e) { if (e.button == 0) { if (me.imagesLoaded) { me.start = true; return true; } } }; GameHandler.canvas.addEventListener("mousedown", fMouseDown, false); Benchmark.InfoScene.superclass.constructor.call(this, false, null); }; extend(Benchmark.InfoScene, Game.Scene, { game: null, start: false, imagesLoaded: false, sceneStarted: null, loadingMessage: false, /** * Scene completion polling method */ isComplete: function isComplete() { return this.start; }, onInitScene: function onInitScene() { this.playable = false; this.start = false; this.yoff = 1; }, onRenderScene: function onRenderScene(ctx) { ctx.save(); if (this.imagesLoaded) { // splash logo image dimensions var w = 640, h = 640; if (this.yoff < h - 1) { // liquid fill bg effect ctx.drawImage(g_splashImg, 0, 0, w, this.yoff, 0, 0, w, this.yoff); ctx.drawImage(g_splashImg, 0, this.yoff, w, 2, 0, this.yoff, w, h-this.yoff); this.yoff++; } else { var toff = (GameHandler.height/2 + 196), tsize = 40; ctx.drawImage(g_splashImg, 0, toff-tsize+12, w, tsize, 0, toff-tsize+12, w, tsize); ctx.shadowBlur = 6; ctx.shadowColor = "#000"; // alpha fade bounce in a single tertiary statement using a single counter // first 64 values of 128 perform a fade in, for second 64 values, fade out ctx.globalAlpha = (this.yoff % 128 < 64) ? ((this.yoff % 64) / 64) : (1 - ((this.yoff % 64) / 64)); Game.centerFillText(ctx, "Click or press SPACE to run CanvasMark", "18pt Helvetica", toff, "#fff"); } this.yoff++; } else if (!this.loadingMessage) { Game.centerFillText(ctx, "Please wait... Loading Images...", "18pt Helvetica", GameHandler.height/2, "#eee"); this.loadingMessage = true; } ctx.restore(); }, /** * Callback from image preloader when all images are ready */ ready: function ready() { this.imagesLoaded = true; if (location.search === "?auto=true") { this.start = true; } }, onKeyDownHandler: function onKeyDownHandler(keyCode) { switch (keyCode) { case KEY.SPACE: { if (this.imagesLoaded) { this.start = true; } return true; break; } } } }); })(); /** * Benchmark CompletedScene scene class. * * @namespace Benchmark * @class Benchmark.CompletedScene */ (function() { Benchmark.CompletedScene = function(game) { this.game = game; // construct the interval to represent the Game Over text effect var interval = new Game.Interval("CanvasMark Completed!", this.intervalRenderer); Benchmark.CompletedScene.superclass.constructor.call(this, false, interval); }; extend(Benchmark.CompletedScene, Game.Scene, { game: null, exit: false, /** * Scene init event handler */ onInitScene: function onInitScene() { this.game.fps = 1; this.interval.reset(); this.exit = false; }, /** * Scene completion polling method */ isComplete: function isComplete() { return true; }, intervalRenderer: function intervalRenderer(interval, ctx) { ctx.clearRect(0, 0, GameHandler.width, GameHandler.height); var score = GameHandler.benchmarkScoreCount; if (interval.framecounter === 0) { var browser = BrowserDetect.browser + " " + BrowserDetect.version; var OS = BrowserDetect.OS; if (location.search === "?auto=true") { alert(score); } else { // write results to browser $("#results").html("

CanvasMark Score: " + score + " (" + browser + " on " + OS + ")

"); // tweet this result link var tweet = "http://twitter.com/home/?status=" + browser + " (" + OS + ") scored " + score + " in the CanvasMark HTML5 benchmark! Test your browser: http://bit.ly/canvasmark %23javascript %23html5"; $("#tweetlink").attr('href', tweet.replace(/ /g, "%20")); $("#results-wrapper").fadeIn(); } } Game.centerFillText(ctx, interval.label, "18pt Helvetica", GameHandler.height/2 - 32, "white"); Game.centerFillText(ctx, "Benchmark Score: " + score, "14pt Helvetica", GameHandler.height/2, "white"); interval.complete = (this.exit || interval.framecounter++ > 400); }, onKeyDownHandler: function onKeyDownHandler(keyCode) { switch (keyCode) { case KEY.SPACE: { this.exit = true; return true; break; } } } }); })(); var BrowserDetect = { init: function () { this.browser = this.searchString(this.dataBrowser) || "Unknown Browser"; this.version = this.searchVersion(navigator.userAgent) || this.searchVersion(navigator.appVersion) || "an unknown version"; this.OS = this.searchString(this.dataOS) || "Unknown OS"; }, searchString: function (data) { for (var i=0;i