├── .gitignore ├── README.md ├── jamscreenshot.png ├── assets ├── audio │ ├── laser_music.mp3 │ ├── laser_music.ogg │ ├── building_move.ogg │ ├── laser_fireup.mp3 │ ├── laser_fireup.ogg │ ├── laser_completed.mp3 │ └── laser_completed.ogg └── models │ ├── stop_stationary.json │ ├── sidewalk.json │ ├── roof_stationary.json │ └── hole_stationary.json ├── package.json ├── src ├── lib │ ├── mousetrap-global-bind.js │ ├── hsl.js │ ├── tween.min.js │ ├── FileSaver.js │ └── SkyShader.js ├── statemachine.js ├── renderableobject.js ├── gameparameters.js ├── monospacebitmapfont.js ├── utilthree.js ├── orbitcameracontrol.js ├── gamepad.js ├── editor.js ├── loadingbar.js ├── unlocker.js ├── threesceneobject.js ├── mainloop.js ├── audio.js ├── animatedsprite.js ├── game.js ├── hitbox.js ├── laser.js ├── canvasui.js ├── inputmapper.js └── sprite.js └── index.html /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # lasertown 2 | Ludum Dare 35 Game 3 | -------------------------------------------------------------------------------- /jamscreenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Oletus/lasertown/HEAD/jamscreenshot.png -------------------------------------------------------------------------------- /assets/audio/laser_music.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Oletus/lasertown/HEAD/assets/audio/laser_music.mp3 -------------------------------------------------------------------------------- /assets/audio/laser_music.ogg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Oletus/lasertown/HEAD/assets/audio/laser_music.ogg -------------------------------------------------------------------------------- /assets/audio/building_move.ogg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Oletus/lasertown/HEAD/assets/audio/building_move.ogg -------------------------------------------------------------------------------- /assets/audio/laser_fireup.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Oletus/lasertown/HEAD/assets/audio/laser_fireup.mp3 -------------------------------------------------------------------------------- /assets/audio/laser_fireup.ogg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Oletus/lasertown/HEAD/assets/audio/laser_fireup.ogg -------------------------------------------------------------------------------- /assets/audio/laser_completed.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Oletus/lasertown/HEAD/assets/audio/laser_completed.mp3 -------------------------------------------------------------------------------- /assets/audio/laser_completed.ogg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Oletus/lasertown/HEAD/assets/audio/laser_completed.ogg -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Laser Town", 3 | "version": "0.1.0", 4 | "description": "Laser Town game", 5 | "repository": "https://github.com/Oletus/lasertown.git", 6 | "dependencies": { 7 | "howler": "^1.1.28", 8 | "tween": "^0.9.0" 9 | }, 10 | "devDependencies": { 11 | "gulp": "^3.9.0", 12 | "gulp-fluent-ffmpeg": "^1.0.2" 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/lib/mousetrap-global-bind.js: -------------------------------------------------------------------------------- 1 | /** 2 | * adds a bindGlobal method to Mousetrap that allows you to 3 | * bind specific keyboard shortcuts that will still work 4 | * inside a text input field 5 | * 6 | * usage: 7 | * Mousetrap.bindGlobal('ctrl+s', _saveChanges); 8 | */ 9 | /* global Mousetrap:true */ 10 | window.Mousetrap = (function(Mousetrap) { 11 | var _globalCallbacks = {}, 12 | _originalStopCallback = Mousetrap.stopCallback; 13 | 14 | Mousetrap.stopCallback = function(e, element, combo, sequence) { 15 | if (_globalCallbacks[combo] || _globalCallbacks[sequence]) { 16 | return false; 17 | } 18 | 19 | return _originalStopCallback(e, element, combo); 20 | }; 21 | 22 | Mousetrap.bindGlobal = function(keys, callback, action) { 23 | Mousetrap.bind(keys, callback, action); 24 | 25 | if (keys instanceof Array) { 26 | for (var i = 0; i < keys.length; i++) { 27 | _globalCallbacks[keys[i]] = true; 28 | } 29 | return; 30 | } 31 | 32 | _globalCallbacks[keys] = true; 33 | }; 34 | 35 | return Mousetrap; 36 | }) (window.Mousetrap); 37 | -------------------------------------------------------------------------------- /src/statemachine.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | // Requires utiljs.js 4 | 5 | if (typeof GJS === "undefined") { 6 | var GJS = {}; 7 | } 8 | 9 | /** 10 | * A very simple state machine. Tracks state and the time that the machine has been in that state. 11 | * @constructor 12 | * @param {Object} options Options for the object. Contains keys: 13 | * stateSet: An object with number values, each key-value pair identifying a state. Example: 14 | * { IDLE: 0, RUNNING: 1 } 15 | * id: Number that identifies the initial state. 16 | */ 17 | GJS.StateMachine = function(options) { 18 | var defaults = { 19 | id: null, 20 | stateSet: {} 21 | }; 22 | objectUtil.initWithDefaults(this, defaults, options); 23 | if (this.id === null) { 24 | for (var key in this.stateSet) { 25 | if (this.stateSet.hasOwnProperty(key)) { 26 | this.id = this.stateSet[key]; 27 | break; 28 | } 29 | } 30 | } 31 | this.time = 0; 32 | }; 33 | 34 | /** 35 | * @param {number} newStateId Id of the new state. 36 | */ 37 | GJS.StateMachine.prototype.change = function(newStateId) { 38 | this.id = newStateId; 39 | this.time = 0; 40 | }; 41 | 42 | /** 43 | * Call this regularly to update the state machine. 44 | * @param {number} deltaTime Time change since last call to this function. 45 | */ 46 | GJS.StateMachine.prototype.update = function(deltaTime) { 47 | this.time += deltaTime; 48 | }; 49 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Lasertown 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | -------------------------------------------------------------------------------- /src/renderableobject.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | // Requires utiljs.js 4 | 5 | /** 6 | * A renderable object that game objects can inherit. May contain multiple renderers that have different render order, 7 | * like a foreground and background. 8 | * @constructor 9 | */ 10 | var RenderableObject = function() { 11 | }; 12 | 13 | /** 14 | * Initialize the object. 15 | */ 16 | RenderableObject.prototype.initRenderableObject = function() { 17 | this.renderOrder = 0; // Renderers with greater render order get rendered first 18 | }; 19 | 20 | /** 21 | * Override this function to generate multiple renderers from this object. 22 | * @return {Array.} Renderers aka. objects with render(ctx) functions and renderOrder. 23 | */ 24 | RenderableObject.prototype.renderObjects = function() { 25 | return [this]; 26 | }; 27 | 28 | /** 29 | * In case the object doesn't have multiple renderers, the render(ctx) function on the object itself is used to render 30 | * it. Override this function to implement rendering. 31 | * @param {CanvasRenderingContext2D} ctx Rendering context. 32 | */ 33 | RenderableObject.prototype.render = function(ctx) { 34 | return; 35 | }; 36 | 37 | /** 38 | * Push the renderers of this object to renderList. 39 | * @param {Array.} renderList 40 | */ 41 | RenderableObject.prototype.pushRenderers = function(renderList) { 42 | renderList.push.apply(renderList, this.renderObjects()); 43 | }; 44 | 45 | /** 46 | * Sort renderable objects for rendering. 47 | * @protected 48 | */ 49 | RenderableObject._renderSort = function(a, b) { 50 | if (a.renderOrder > b.renderOrder) { 51 | return -1; 52 | } 53 | if (b.renderOrder > a.renderOrder) { 54 | return 1; 55 | } 56 | return 0; 57 | }; 58 | 59 | /** 60 | * Render a list of renderable objects in the correct order. 61 | * @param {CanvasRenderingContext2D} ctx Rendering context. 62 | * @param {Array.} renderList List of renderers generated by RenderableObject.renderObjects() 63 | */ 64 | RenderableObject.renderList = function(ctx, renderList) { 65 | arrayUtil.stableSort(renderList, RenderableObject._renderSort); 66 | for (var i = 0; i < renderList.length; ++i) { 67 | renderList[i].render(ctx); 68 | } 69 | }; 70 | -------------------------------------------------------------------------------- /src/gameparameters.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /** 4 | * A class for runtime developer settings and tuning game parameters. 5 | * @constructor 6 | * @param {Object} params An object with parameters that can be adjusted. Example: 7 | * { 8 | * 'playerJumpHeight': {initial: 1, min: 0.1, max: 2}, 9 | * 'muteAudio': false 10 | * } 11 | */ 12 | var GameParameters = function(params) { 13 | this._params = params; 14 | this._values = {}; 15 | for (var key in params) { 16 | if (params.hasOwnProperty(key)) { 17 | if (params[key].options !== undefined && params[key].initial === undefined) { 18 | this._values[key] = params[key].options[0]; 19 | } else { 20 | this._values[key] = params[key].initial; 21 | } 22 | } 23 | } 24 | }; 25 | 26 | /** 27 | * Add dat.gui for changing the parameters. 28 | * @param {Object=} preset Preset data for dat.GUI. 29 | */ 30 | GameParameters.prototype.initGUI = function(preset) { 31 | if (preset !== undefined) { 32 | preset = {load: preset}; 33 | } 34 | var gui = new dat.GUI(preset); 35 | var params = this._params; 36 | gui.remember(this._values); 37 | for (var key in params) { 38 | if (params.hasOwnProperty(key)) { 39 | var param = params[key]; 40 | var added = null; 41 | if (param.color !== undefined) { 42 | added = gui.addColor(this._values, key); 43 | } else if (param.options !== undefined) { 44 | added = gui.add(this._values, key, param.options); 45 | } else if (param.min !== undefined) { 46 | added = gui.add(this._values, key, param.min, param.max); 47 | } else { 48 | added = gui.add(this._values, key); 49 | } 50 | if (param.step !== undefined) { 51 | added.step(param.step); 52 | } 53 | if (added) { 54 | added.listen(); 55 | } 56 | } 57 | } 58 | }; 59 | 60 | /** 61 | * @param {string} key Key for the parameter. 62 | * @return {Object} The current value of a parameter. 63 | */ 64 | GameParameters.prototype.get = function(key) { 65 | return this._values[key]; 66 | }; 67 | 68 | /** 69 | * @param {string} key Key for the parameter. 70 | * @param {Object} value The value of a parameter to set. 71 | */ 72 | GameParameters.prototype.set = function(key, value) { 73 | if (this._values.hasOwnProperty(key)) { 74 | this._values[key] = value; 75 | } 76 | }; 77 | 78 | /** 79 | * @param {string} key Key for the parameter. 80 | * @return {function} Function that returns the value of the parameter. 81 | */ 82 | GameParameters.prototype.wrap = function(key) { 83 | var values = this._values; 84 | return function() { 85 | return values[key]; 86 | }; 87 | }; 88 | -------------------------------------------------------------------------------- /src/monospacebitmapfont.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /** 4 | * Bitmap font that uses a simple ISO-8859-1 monospace grid sprite sheet. 5 | * @param {Object} options Constructor options. 6 | * @constructor 7 | */ 8 | var MonospaceBitmapFont = function(options) { 9 | var defaults = { 10 | spriteSrc: 'bitmapfont-tiny.png', 11 | characterHeight: 6, 12 | characterWidth: 4, 13 | charactersPerRow: undefined, 14 | color: undefined, 15 | closerKerningCharacters: [], // list of characters to kern closer when in pairs. for example: ['i', 'l'] 16 | kerningAmount: 1 17 | }; 18 | objectUtil.initWithDefaults(this, defaults, options); 19 | if (this.color !== undefined) { 20 | this.sprite = new Sprite(this.spriteSrc, Sprite.turnSolidColored(this.color)); 21 | } else { 22 | this.sprite = new Sprite(this.spriteSrc); 23 | } 24 | }; 25 | 26 | /** 27 | * Draw a single character. 28 | * @param {CanvasRenderingContext2D} ctx Context to draw to. 29 | * @param {string} A single-character string to draw. 30 | */ 31 | MonospaceBitmapFont.prototype.drawCharacter = function(ctx, character) { 32 | if (this.sprite.loaded) { 33 | if (this.charactersPerRow === undefined) { 34 | this.charactersPerRow = this.sprite.width / this.characterWidth; 35 | } 36 | var code = character.charCodeAt(0); 37 | var row = Math.floor(code / this.charactersPerRow); 38 | var col = code - (row * this.charactersPerRow); 39 | ctx.drawImage(this.sprite.img, 40 | col * this.characterWidth, row * this.characterHeight, 41 | this.characterWidth, this.characterHeight, 42 | 0, 0, 43 | this.characterWidth, this.characterHeight); 44 | } 45 | }; 46 | 47 | /** 48 | * Draw a string of text. The "textAlign" property of the canvas context affects its placement. 49 | * @param {CanvasRenderingContext2D} ctx Context to draw to. 50 | * @param {string} string String to draw. 51 | * @param {number} x Horizontal coordinate. 52 | * @param {number} y Vertical coordinate. 53 | */ 54 | MonospaceBitmapFont.prototype.drawText = function(ctx, string, x, y) { 55 | var drawnWidth = string.length * this.characterWidth; 56 | var kerningActive = this.closerKerningCharacters.length > 0 && this.kerningAmount != 0; 57 | var prevCharacterNarrow = false; 58 | if (kerningActive) { 59 | for (var i = 0; i < string.length; ++i) { 60 | if (this.closerKerningCharacters.indexOf(string[i]) >= 0) { 61 | if (prevCharacterNarrow) { 62 | drawnWidth -= this.kerningAmount; 63 | } 64 | prevCharacterNarrow = true; 65 | } else { 66 | prevCharacterNarrow = false; 67 | } 68 | } 69 | } 70 | 71 | var baselineTranslate = 0; // Default for top and hanging 72 | if (ctx.textBaseline == 'bottom' || ctx.textBaseline == 'alphabetic' || ctx.textBaseline == 'ideographic') { 73 | baselineTranslate = -this.characterHeight; 74 | } else if (ctx.textBaseline == 'middle') { 75 | baselineTranslate = Math.floor(-this.characterHeight * 0.5); 76 | } 77 | var alignTranslate = 0; 78 | if (ctx.textAlign == 'center') { 79 | alignTranslate = -Math.floor(drawnWidth * 0.5); 80 | } else if (ctx.textAlign == 'right') { 81 | alignTranslate = -Math.floor(drawnWidth); 82 | } 83 | ctx.save(); 84 | ctx.translate(x + alignTranslate, y + baselineTranslate); 85 | for (var i = 0; i < string.length; ++i) { 86 | this.drawCharacter(ctx, string[i]); 87 | if (kerningActive) { 88 | if (this.closerKerningCharacters.indexOf(string[i]) >= 0 && i + 1 < string.length && 89 | this.closerKerningCharacters.indexOf(string[i + 1]) >= 0) { 90 | ctx.translate(this.characterWidth - this.kerningAmount, 0); 91 | } else { 92 | ctx.translate(this.characterWidth, 0); 93 | } 94 | 95 | } else { 96 | ctx.translate(this.characterWidth, 0); 97 | } 98 | } 99 | ctx.restore(); 100 | }; 101 | -------------------------------------------------------------------------------- /src/lib/hsl.js: -------------------------------------------------------------------------------- 1 | // From: http://axonflux.com/handy-rgb-to-hsl-and-rgb-to-hsv-color-model-c 2 | 3 | /** 4 | * Converts an RGB color value to HSL. Conversion formula 5 | * adapted from http://en.wikipedia.org/wiki/HSL_color_space. 6 | * Assumes r, g, and b are contained in the set [0, 255] and 7 | * returns h, s, and l in the set [0, 1]. 8 | * 9 | * @param {Number} r The red color value 10 | * @param {Number} g The green color value 11 | * @param {Number} b The blue color value 12 | * @return {Array} The HSL representation 13 | */ 14 | function rgbToHsl(r, g, b){ 15 | r /= 255, g /= 255, b /= 255; 16 | var max = Math.max(r, g, b), min = Math.min(r, g, b); 17 | var h, s, l = (max + min) / 2; 18 | 19 | if(max == min){ 20 | h = s = 0; // achromatic 21 | }else{ 22 | var d = max - min; 23 | s = l > 0.5 ? d / (2 - max - min) : d / (max + min); 24 | switch(max){ 25 | case r: h = (g - b) / d + (g < b ? 6 : 0); break; 26 | case g: h = (b - r) / d + 2; break; 27 | case b: h = (r - g) / d + 4; break; 28 | } 29 | h /= 6; 30 | } 31 | 32 | return [h, s, l]; 33 | } 34 | 35 | /** 36 | * Converts an HSL color value to RGB. Conversion formula 37 | * adapted from http://en.wikipedia.org/wiki/HSL_color_space. 38 | * Assumes h, s, and l are contained in the set [0, 1] and 39 | * returns r, g, and b in the set [0, 255]. 40 | * 41 | * @param {Number} h The hue 42 | * @param {Number} s The saturation 43 | * @param {Number} l The lightness 44 | * @return {Array} The RGB representation 45 | */ 46 | function hslToRgb(h, s, l){ 47 | var r, g, b; 48 | 49 | if(s == 0){ 50 | r = g = b = l; // achromatic 51 | }else{ 52 | function hue2rgb(p, q, t){ 53 | if(t < 0) t += 1; 54 | if(t > 1) t -= 1; 55 | if(t < 1/6) return p + (q - p) * 6 * t; 56 | if(t < 1/2) return q; 57 | if(t < 2/3) return p + (q - p) * (2/3 - t) * 6; 58 | return p; 59 | } 60 | 61 | var q = l < 0.5 ? l * (1 + s) : l + s - l * s; 62 | var p = 2 * l - q; 63 | r = hue2rgb(p, q, h + 1/3); 64 | g = hue2rgb(p, q, h); 65 | b = hue2rgb(p, q, h - 1/3); 66 | } 67 | 68 | return [r * 255, g * 255, b * 255]; 69 | } 70 | 71 | /** 72 | * Converts an RGB color value to HSV. Conversion formula 73 | * adapted from http://en.wikipedia.org/wiki/HSV_color_space. 74 | * Assumes r, g, and b are contained in the set [0, 255] and 75 | * returns h, s, and v in the set [0, 1]. 76 | * 77 | * @param {Number} r The red color value 78 | * @param {Number} g The green color value 79 | * @param {Number} b The blue color value 80 | * @return {Array} The HSV representation 81 | */ 82 | function rgbToHsv(r, g, b){ 83 | r = r/255, g = g/255, b = b/255; 84 | var max = Math.max(r, g, b), min = Math.min(r, g, b); 85 | var h, s, v = max; 86 | 87 | var d = max - min; 88 | s = max == 0 ? 0 : d / max; 89 | 90 | if(max == min){ 91 | h = 0; // achromatic 92 | }else{ 93 | switch(max){ 94 | case r: h = (g - b) / d + (g < b ? 6 : 0); break; 95 | case g: h = (b - r) / d + 2; break; 96 | case b: h = (r - g) / d + 4; break; 97 | } 98 | h /= 6; 99 | } 100 | 101 | return [h, s, v]; 102 | } 103 | 104 | /** 105 | * Converts an HSV color value to RGB. Conversion formula 106 | * adapted from http://en.wikipedia.org/wiki/HSV_color_space. 107 | * Assumes h, s, and v are contained in the set [0, 1] and 108 | * returns r, g, and b in the set [0, 255]. 109 | * 110 | * @param {Number} h The hue 111 | * @param {Number} s The saturation 112 | * @param {Number} v The value 113 | * @return {Array} The RGB representation 114 | */ 115 | function hsvToRgb(h, s, v){ 116 | var r, g, b; 117 | 118 | var i = Math.floor(h * 6); 119 | var f = h * 6 - i; 120 | var p = v * (1 - s); 121 | var q = v * (1 - f * s); 122 | var t = v * (1 - (1 - f) * s); 123 | 124 | switch(i % 6){ 125 | case 0: r = v, g = t, b = p; break; 126 | case 1: r = q, g = v, b = p; break; 127 | case 2: r = p, g = v, b = t; break; 128 | case 3: r = p, g = q, b = v; break; 129 | case 4: r = t, g = p, b = v; break; 130 | case 5: r = v, g = p, b = q; break; 131 | } 132 | 133 | return [r * 255, g * 255, b * 255]; 134 | } 135 | -------------------------------------------------------------------------------- /src/utilthree.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | if (typeof GJS === "undefined") { 4 | var GJS = {}; 5 | } 6 | 7 | // Three.js utils. 8 | GJS.utilTHREE = {}; 9 | 10 | /** 11 | * Path to load models from. 12 | */ 13 | GJS.utilTHREE.modelsPath = 'assets/models/'; 14 | 15 | /** 16 | * Path to load fonts from. 17 | */ 18 | GJS.utilTHREE.fontsPath = 'assets/fonts/'; 19 | 20 | /** 21 | * How many models/fonts have been created. 22 | */ 23 | GJS.utilTHREE.createdCount = 0; 24 | /** 25 | * How many models/fonts have been fully loaded. 26 | */ 27 | GJS.utilTHREE.loadedCount = 0; 28 | 29 | /** 30 | * @return {number} Amount of three.js models/fonts that have been fully loaded per amount that has been created. 31 | * Name specified as string to support Closure compiler together with loadingbar.js. 32 | */ 33 | GJS.utilTHREE['loadedFraction'] = function() { 34 | if (GJS.utilTHREE.createdCount === 0) { 35 | return 1.0; 36 | } 37 | return GJS.utilTHREE.loadedCount / GJS.utilTHREE.createdCount; 38 | }; 39 | 40 | /** 41 | * @param {string} filename Name of the model file to load without the .json extension. 42 | * @param {function} objectCallback Function to call with the created mesh as a parameter. 43 | */ 44 | GJS.utilTHREE.loadJSONModel = function(filename, objectCallback) { 45 | var loader = new THREE.JSONLoader(); 46 | 47 | ++GJS.utilTHREE.createdCount; 48 | 49 | loader.load(GJS.utilTHREE.modelsPath + filename + '.json', function(geometry, materials) { 50 | var material = new THREE.MeshFaceMaterial(materials); 51 | var mesh = new THREE.Mesh(geometry, material); 52 | objectCallback(mesh); 53 | ++GJS.utilTHREE.loadedCount; 54 | }); 55 | }; 56 | 57 | /** 58 | * @param {string} fontName Name of the font to load. 59 | * @param {function} objectCallback Function to call with the created font as a parameter. 60 | */ 61 | GJS.utilTHREE.loadFont = function(fontName, objectCallback) { 62 | var loader = new THREE.FontLoader(); 63 | 64 | ++GJS.utilTHREE.createdCount; 65 | 66 | loader.load(GJS.utilTHREE.fontsPath + fontName + '.js', function ( response ) { 67 | objectCallback(response); 68 | ++GJS.utilTHREE.loadedCount; 69 | }); 70 | }; 71 | 72 | /** 73 | * @param {number} faceSize The width and height of the shape. 74 | * @param {number} holeSize The width and height of the hole in the middle. 75 | * @return {THREE.Shape} 76 | */ 77 | GJS.utilTHREE.createSquareWithHoleShape = function(faceSize, holeSize) { 78 | var fs = faceSize / 2; 79 | var hs = holeSize / 2; 80 | var shape = new THREE.Shape(); 81 | shape.moveTo(-fs, -fs); 82 | shape.lineTo( fs, -fs); 83 | shape.lineTo( fs, fs); 84 | shape.lineTo(-fs, fs); 85 | var hole = new THREE.Path(); 86 | hole.moveTo(-hs, -hs); 87 | hole.lineTo( hs, -hs); 88 | hole.lineTo( hs, hs); 89 | hole.lineTo(-hs, hs); 90 | shape.holes.push(hole); 91 | return shape; 92 | }; 93 | 94 | /** 95 | * @param {number} faceSize The width and height of the U shape. 96 | * @param {number} edgeSize The width of the edges of the U. 97 | * @param {number} bottomEdgeSize The width of the bottom edge of the U. Defaults to edgeSize. 98 | * @return {THREE.Shape} 99 | */ 100 | GJS.utilTHREE.createUShape = function(faceSize, edgeSize, bottomEdgeSize) { 101 | if (bottomEdgeSize === undefined) { 102 | bottomEdgeSize = edgeSize; 103 | } 104 | var fs = faceSize / 2; 105 | var es = edgeSize; 106 | var bs = bottomEdgeSize; 107 | 108 | var shape = new THREE.Shape(); 109 | shape.moveTo(-fs, -fs); 110 | shape.lineTo( fs, -fs); 111 | shape.lineTo( fs, fs); 112 | shape.lineTo( fs - es, fs); 113 | shape.lineTo( fs - es, -fs + bs); 114 | shape.lineTo(-fs + es, -fs + bs); 115 | shape.lineTo(-fs + es, fs); 116 | shape.lineTo(-fs, fs); 117 | return shape; 118 | }; 119 | 120 | /** 121 | * @param {number} triWidth The width of the triangle part of the arrow. 122 | * @param {number} triHeight The height of the triangle part of the arrow. 123 | * @param {number} stemWidth The width of the stem of the arrow. 124 | * @param {number} stemHeight The height of the stem of the arrow. 125 | * @return {THREE.Shape} 126 | */ 127 | GJS.utilTHREE.createArrowShape = function(triWidth, triHeight, stemWidth, stemHeight) { 128 | var tw = triWidth * 0.5; 129 | var sw = stemWidth * 0.5; 130 | 131 | var shape = new THREE.Shape(); 132 | shape.moveTo( tw, 0); 133 | shape.lineTo( 0, triHeight); 134 | shape.lineTo(-tw, 0); 135 | shape.lineTo(-sw, 0); 136 | shape.lineTo(-sw, -stemHeight); 137 | shape.lineTo( sw, -stemHeight); 138 | shape.lineTo( sw, 0); 139 | return shape; 140 | }; 141 | -------------------------------------------------------------------------------- /src/lib/tween.min.js: -------------------------------------------------------------------------------- 1 | // tween.js - http://github.com/sole/tween.js 2 | 'use strict';void 0===Date.now&&(Date.now=function(){return(new Date).valueOf()});var TWEEN=TWEEN||function(){var a=[];return{REVISION:"8",getAll:function(){return a},removeAll:function(){a=[]},add:function(c){a.push(c)},remove:function(c){c=a.indexOf(c);-1!==c&&a.splice(c,1)},update:function(c){if(0===a.length)return!1;for(var b=0,d=a.length,c=void 0!==c?c:Date.now();b(a*=2)?0.5*a*a:-0.5*(--a*(a-2)-1)}},Cubic:{In:function(a){return a*a*a},Out:function(a){return--a*a*a+1},InOut:function(a){return 1>(a*=2)?0.5*a*a*a:0.5*((a-=2)*a*a+2)}},Quartic:{In:function(a){return a*a*a*a},Out:function(a){return 1- --a*a*a*a},InOut:function(a){return 1>(a*=2)?0.5*a*a*a*a:-0.5*((a-=2)*a*a*a-2)}},Quintic:{In:function(a){return a*a*a* 7 | a*a},Out:function(a){return--a*a*a*a*a+1},InOut:function(a){return 1>(a*=2)?0.5*a*a*a*a*a:0.5*((a-=2)*a*a*a*a+2)}},Sinusoidal:{In:function(a){return 1-Math.cos(a*Math.PI/2)},Out:function(a){return Math.sin(a*Math.PI/2)},InOut:function(a){return 0.5*(1-Math.cos(Math.PI*a))}},Exponential:{In:function(a){return 0===a?0:Math.pow(1024,a-1)},Out:function(a){return 1===a?1:1-Math.pow(2,-10*a)},InOut:function(a){return 0===a?0:1===a?1:1>(a*=2)?0.5*Math.pow(1024,a-1):0.5*(-Math.pow(2,-10*(a-1))+2)}},Circular:{In:function(a){return 1- 8 | Math.sqrt(1-a*a)},Out:function(a){return Math.sqrt(1- --a*a)},InOut:function(a){return 1>(a*=2)?-0.5*(Math.sqrt(1-a*a)-1):0.5*(Math.sqrt(1-(a-=2)*a)+1)}},Elastic:{In:function(a){var c,b=0.1;if(0===a)return 0;if(1===a)return 1;!b||1>b?(b=1,c=0.1):c=0.4*Math.asin(1/b)/(2*Math.PI);return-(b*Math.pow(2,10*(a-=1))*Math.sin((a-c)*2*Math.PI/0.4))},Out:function(a){var c,b=0.1;if(0===a)return 0;if(1===a)return 1;!b||1>b?(b=1,c=0.1):c=0.4*Math.asin(1/b)/(2*Math.PI);return b*Math.pow(2,-10*a)*Math.sin((a-c)* 9 | 2*Math.PI/0.4)+1},InOut:function(a){var c,b=0.1;if(0===a)return 0;if(1===a)return 1;!b||1>b?(b=1,c=0.1):c=0.4*Math.asin(1/b)/(2*Math.PI);return 1>(a*=2)?-0.5*b*Math.pow(2,10*(a-=1))*Math.sin((a-c)*2*Math.PI/0.4):0.5*b*Math.pow(2,-10*(a-=1))*Math.sin((a-c)*2*Math.PI/0.4)+1}},Back:{In:function(a){return a*a*(2.70158*a-1.70158)},Out:function(a){return--a*a*(2.70158*a+1.70158)+1},InOut:function(a){return 1>(a*=2)?0.5*a*a*(3.5949095*a-2.5949095):0.5*((a-=2)*a*(3.5949095*a+2.5949095)+2)}},Bounce:{In:function(a){return 1- 10 | TWEEN.Easing.Bounce.Out(1-a)},Out:function(a){return a<1/2.75?7.5625*a*a:a<2/2.75?7.5625*(a-=1.5/2.75)*a+0.75:a<2.5/2.75?7.5625*(a-=2.25/2.75)*a+0.9375:7.5625*(a-=2.625/2.75)*a+0.984375},InOut:function(a){return 0.5>a?0.5*TWEEN.Easing.Bounce.In(2*a):0.5*TWEEN.Easing.Bounce.Out(2*a-1)+0.5}}}; 11 | TWEEN.Interpolation={Linear:function(a,c){var b=a.length-1,d=b*c,e=Math.floor(d),f=TWEEN.Interpolation.Utils.Linear;return 0>c?f(a[0],a[1],d):1b?b:e+1],d-e)},Bezier:function(a,c){var b=0,d=a.length-1,e=Math.pow,f=TWEEN.Interpolation.Utils.Bernstein,h;for(h=0;h<=d;h++)b+=e(1-c,d-h)*e(c,h)*a[h]*f(d,h);return b},CatmullRom:function(a,c){var b=a.length-1,d=b*c,e=Math.floor(d),f=TWEEN.Interpolation.Utils.CatmullRom;return a[0]===a[b]?(0>c&&(e=Math.floor(d=b*(1+c))),f(a[(e- 12 | 1+b)%b],a[e],a[(e+1)%b],a[(e+2)%b],d-e)):0>c?a[0]-(f(a[0],a[0],a[1],a[1],-d)-a[0]):1 1) { 76 | t = 1; 77 | this.state.change(GJS.OrbitCameraControl.State.CONTROLLABLE); 78 | } 79 | this.orbitAngle = mathUtil.mixSmooth(this.startOrbitAngle, this.targetOrbitAngle, t); 80 | this.y = mathUtil.mixSmooth(this.startY, this.targetY, t); 81 | this.updateCamera(); 82 | } 83 | }; 84 | 85 | /** 86 | * Start animated camera transition. 87 | * @param {Object} options Options for the animation to start. 88 | */ 89 | GJS.OrbitCameraControl.prototype.animate = function(options) { 90 | this.state.change(GJS.OrbitCameraControl.State.ANIMATING); 91 | this.startOrbitAngle = this.orbitAngle; 92 | this.startY = this.y; 93 | var defaults = { 94 | targetY: this.y, 95 | targetOrbitAngle: this.orbitAngle, 96 | animationDuration: 1 97 | }; 98 | objectUtil.initWithDefaults(this, defaults, options); 99 | }; 100 | 101 | /** 102 | * Zoom the camera (move it up and down on the y axis). TODO: Add more options for zooming. 103 | * @param {number} amount How much to change the zoom level. 104 | */ 105 | GJS.OrbitCameraControl.prototype.zoom = function(amount) { 106 | if (this.zoomMovesY) { 107 | this.y -= amount; 108 | this.clampY(); 109 | } 110 | this.updateCamera(); 111 | }; 112 | 113 | /** 114 | * Clamp the camera position in the y position to the configured max and min values. 115 | */ 116 | GJS.OrbitCameraControl.prototype.clampY = function() { 117 | this.y = mathUtil.clamp(this.minY, this.maxY, this.y); 118 | }; 119 | 120 | /** 121 | * Move the orbit angle of the camera. 122 | * @param {number} amount How much to move the orbit angle in radians. 123 | */ 124 | GJS.OrbitCameraControl.prototype.moveOrbitAngle = function(amount) { 125 | this.orbitAngle += amount; 126 | this.clampOrbitAngle(); 127 | this.updateCamera(); 128 | }; 129 | 130 | /** 131 | * Clamp the orbit angle to the configured max and min values. 132 | */ 133 | GJS.OrbitCameraControl.prototype.clampOrbitAngle = function() { 134 | this.orbitAngle = mathUtil.clamp(this.minOrbitAngle, this.maxOrbitAngle, this.orbitAngle); 135 | }; 136 | -------------------------------------------------------------------------------- /src/gamepad.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /** 4 | * @constructor 5 | */ 6 | var Gamepad = function(callbackObj) { 7 | this.downListeners = []; 8 | this.indexToPlayer = {}; 9 | this.players = []; 10 | this.callbackObj = callbackObj; 11 | }; 12 | 13 | Gamepad.debugLogEnabled = false; 14 | 15 | Gamepad.prototype.gamepadForPlayer = function(gamepads, playerNumber) { 16 | for (var i = 0; i < gamepads.length; ++i) { 17 | if (gamepads[i] !== undefined && gamepads[i] !== null && gamepads[i].index === this.players[playerNumber]) { 18 | return gamepads[i]; 19 | } 20 | } 21 | return null; 22 | }; 23 | 24 | /** 25 | * @protected 26 | */ 27 | Gamepad.prototype._markDownAndCallback = function(l, p, value) { 28 | if (value > 0.5) { 29 | if (!l.isDown[p]) { 30 | l.isDown[p] = true; 31 | if (l.callback !== undefined) { 32 | l.callback.call(this.callbackObj, p); 33 | } 34 | } 35 | } else if (value < 0.3) { 36 | if (l.isDown[p]) { 37 | l.isDown[p] = false; 38 | if (l.callbackUp !== undefined) { 39 | l.callbackUp.call(this.callbackObj, p); 40 | } 41 | } 42 | } 43 | }; 44 | 45 | Gamepad.prototype.update = function() { 46 | var gamepads; 47 | if (navigator.getGamepads) { 48 | gamepads = navigator.getGamepads(); 49 | } else if (navigator.webkitGetGamepads) { 50 | gamepads = navigator.webkitGetGamepads(); 51 | } 52 | if (gamepads === undefined) { 53 | return; 54 | } 55 | 56 | for (var i = 0; i < gamepads.length; ++i) { 57 | if (gamepads[i] !== undefined && gamepads[i] !== null) { 58 | var key = 'index' + gamepads[i].index; 59 | if (!this.indexToPlayer.hasOwnProperty(key)) { 60 | this.indexToPlayer[key] = this.players.length; 61 | this.players.push(gamepads[i].index); 62 | } 63 | } 64 | } 65 | for (var i = 0; i < this.downListeners.length; ++i) { 66 | for (var p = 0; p < this.players.length; ++p) { 67 | var l = this.downListeners[i]; 68 | var pad = this.gamepadForPlayer(gamepads, p); 69 | if (pad != null) { 70 | var value; 71 | var buttonNumber = l.buttonNumber; 72 | if (l.buttonNumber > 100) { 73 | buttonNumber -= 100; 74 | } 75 | try { 76 | if ('value' in pad.buttons[buttonNumber]) { 77 | value = pad.buttons[buttonNumber].value; 78 | } else { 79 | value = pad.buttons[buttonNumber]; 80 | } 81 | } catch(e) { 82 | // Accessing pad.buttons seems to randomly fail in Firefox after long uptime sometimes. 83 | if (Gamepad.debugLogEnabled) { 84 | console.log('Accessing pad.buttons failed, pad.buttons is: ', pad.buttons); 85 | } 86 | continue; 87 | } 88 | if (l.buttonNumber > 100) { 89 | var axis = (l.buttonNumber <= Gamepad.BUTTONS.DOWN_OR_ANALOG_DOWN) ? 1 : 0; 90 | var axisValue = pad.axes[axis]; 91 | // positive values are down/right, negative up/left 92 | if (l.buttonNumber % 2 === Gamepad.BUTTONS.UP_OR_ANALOG_UP % 2) { 93 | axisValue = -axisValue; 94 | } 95 | this._markDownAndCallback(l, p, Math.max(value, axisValue)); 96 | } else { 97 | this._markDownAndCallback(l, p, value); 98 | } 99 | } 100 | } 101 | } 102 | }; 103 | 104 | Gamepad.prototype.addButtonChangeListener = function(buttonNumber, callbackDown, callbackUp) { 105 | this.downListeners.push({buttonNumber: buttonNumber, callback: callbackDown, callbackUp: callbackUp, isDown: [false, false, false, false]}); 106 | }; 107 | 108 | /** 109 | * Face button names according to the common XBox 360 gamepad. 110 | */ 111 | Gamepad.BUTTONS = { 112 | A: 0, // Face (main) buttons 113 | B: 1, 114 | X: 2, 115 | Y: 3, 116 | L1: 4, // Top shoulder buttons 117 | R1: 5, 118 | L2: 6, // Bottom shoulder buttons 119 | R2: 7, 120 | SELECT: 8, 121 | START: 9, 122 | LEFT_STICK: 10, // Analogue sticks (if depressible) 123 | RIGHT_STICK: 11, 124 | UP: 12, // Directional (discrete) pad 125 | DOWN: 13, 126 | LEFT: 14, 127 | RIGHT: 15, 128 | UP_OR_ANALOG_UP: 112, 129 | DOWN_OR_ANALOG_DOWN: 113, 130 | LEFT_OR_ANALOG_LEFT: 114, 131 | RIGHT_OR_ANALOG_RIGHT: 115 132 | }; 133 | 134 | /** 135 | * Face button names according to the common XBox 360 gamepad. 136 | */ 137 | Gamepad.BUTTON_INSTRUCTION = [ 138 | 'A', 139 | 'B', 140 | 'X', 141 | 'Y', 142 | 'L1', 143 | 'R1', 144 | 'L2', 145 | 'R2', 146 | 'SELECT', 147 | 'START', 148 | 'LEFT STICK', 149 | 'RIGHT STICK', 150 | 'UP', 151 | 'DOWN', 152 | 'LEFT', 153 | 'RIGHT' 154 | ]; 155 | -------------------------------------------------------------------------------- /src/editor.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var MAX_BLOCKS = 7; // Includes base block 4 | 5 | var LevelEditor = function(level, sceneParent) { 6 | this.level = level; 7 | this.sceneParent = sceneParent; 8 | this.chosenY = 0; 9 | 10 | this.buildingCursor = new BuildingCursor({ 11 | level: this.level, 12 | sceneParent: this.sceneParent, 13 | color: 0xff0000, 14 | y: this.chosenY + 0.7, 15 | arrows: false 16 | }); 17 | 18 | this.buildingCursor.addToScene(); 19 | }; 20 | 21 | LevelEditor.prototype.zPress = function() { 22 | if (this.chosenY > 0) { 23 | --this.chosenY; 24 | } 25 | }; 26 | 27 | LevelEditor.prototype.aPress = function() { 28 | if (this.chosenY < MAX_BLOCKS - 2) { 29 | ++this.chosenY; 30 | } 31 | }; 32 | 33 | LevelEditor.prototype.getChosenBlock = function() { 34 | if (this.chosenBuilding) { 35 | var atLevel = this.chosenBuilding.getBlockAtLevel(this.chosenY + 0.5); 36 | if (atLevel === null && this.chosenBuilding && this.chosenBuilding.blocks.length < MAX_BLOCKS && this.chosenBuilding.topYTarget > this.chosenY - 0.5) { 37 | this.chosenBuilding.addBlockToTop({blockConstructor: StopBlock}); 38 | ++this.chosenBuilding.topYTarget; 39 | this.chosenBuilding.topY = this.chosenBuilding.topYTarget; 40 | atLevel = this.chosenBuilding.getBlockAtLevel(this.chosenY + 0.5); 41 | } 42 | return atLevel; 43 | } else { 44 | return null; 45 | } 46 | }; 47 | 48 | LevelEditor.prototype.oPress = function() { 49 | var block = this.getChosenBlock(); 50 | if (block !== null) { 51 | var dir = true; 52 | if (block instanceof HoleBlock) { 53 | dir = !block.holeDirection; 54 | } 55 | this.chosenBuilding.replaceBlockSpec(block, {blockConstructor: HoleBlock, holeDirection: dir}); 56 | } 57 | }; 58 | 59 | LevelEditor.prototype.mPress = function() { 60 | var block = this.getChosenBlock(); 61 | if (block !== null) { 62 | var dir = true; 63 | if (block instanceof MirrorBlock) { 64 | dir = !block.mirrorDirection; 65 | } 66 | this.chosenBuilding.replaceBlockSpec(block, {blockConstructor: MirrorBlock, mirrorDirection: dir}); 67 | } 68 | }; 69 | 70 | LevelEditor.prototype.kPress = function() { 71 | var block = this.getChosenBlock(); 72 | if (block !== null) { 73 | this.chosenBuilding.replaceBlockSpec(block, {blockConstructor: StopBlock}); 74 | } 75 | }; 76 | 77 | LevelEditor.prototype.pPress = function() { 78 | var block = this.getChosenBlock(); 79 | if (block !== null) { 80 | var dir = Laser.Direction.POSITIVE_X; 81 | if (block instanceof PeriscopeBlock) { 82 | dir = Laser.cycleHorizontalDirection(block.periscopeDirection); 83 | } 84 | var blockSpec = { 85 | blockConstructor: PeriscopeBlock, 86 | periscopeDirection: dir, 87 | isUpperBlock: block.topY === this.chosenBuilding.topY 88 | }; 89 | this.chosenBuilding.replaceBlockSpec(block, blockSpec); 90 | } 91 | }; 92 | 93 | LevelEditor.prototype.xPress = function() { 94 | if (this.chosenBuilding && this.chosenBuilding.blocks.length > 1) { 95 | this.chosenBuilding.removeBlock(this.chosenBuilding.blocks[this.chosenBuilding.blocks.length - 2]); 96 | this.chosenBuilding.clampY(); 97 | } 98 | }; 99 | 100 | LevelEditor.prototype.cPress = function() { 101 | if (this.chosenBuilding && this.chosenBuilding.blocks.length < MAX_BLOCKS) { 102 | this.chosenBuilding.addBlock({blockConstructor: StopBlock}); 103 | this.chosenBuilding.topYTarget++; 104 | } 105 | }; 106 | 107 | LevelEditor.prototype.qPress = function() { 108 | if (this.chosenBuilding && this.chosenBuilding.blocks.length > 1) { 109 | this.chosenBuilding.setStationary(!this.chosenBuilding.stationary); 110 | } 111 | }; 112 | 113 | LevelEditor.prototype.lPress = function() { 114 | var block = this.getChosenBlock(); 115 | if (block !== null) { 116 | var top = block.topY === this.chosenBuilding.topY; 117 | var goalConstructor = top ? GoalBlock : GoalPostBlock; 118 | var dir = true; 119 | if (block instanceof goalConstructor) { 120 | dir = !block.goalDirection; 121 | } 122 | this.chosenBuilding.replaceBlockSpec(block, {blockConstructor: goalConstructor, goalDirection: dir}); 123 | } 124 | }; 125 | 126 | LevelEditor.prototype.ctrlsPress = function() { 127 | var blob = new Blob([this.level.getSpec()], {type: 'text/plain'}); 128 | saveAs(blob, 'level.txt'); 129 | }; 130 | 131 | LevelEditor.prototype.updateBuildingCursor = function() { 132 | this.buildingCursor.y = this.chosenY + 0.4; 133 | this.chosenBuilding = this.level.chosenBuilding; 134 | if (this.chosenBuilding) { 135 | this.buildingCursor.gridX = this.chosenBuilding.gridX; 136 | this.buildingCursor.gridZ = this.chosenBuilding.gridZ; 137 | } 138 | }; 139 | 140 | LevelEditor.prototype.update = function(deltaTime) { 141 | this.updateBuildingCursor(); 142 | this.buildingCursor.update(deltaTime); 143 | }; 144 | -------------------------------------------------------------------------------- /src/loadingbar.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | if (typeof GJS === "undefined") { 4 | var GJS = {}; 5 | } 6 | 7 | /** 8 | * Loading bar. 9 | * @param {Array.=} objectsToPoll Objects that contain loadedFraction() 10 | * function that returns 1 when the object is fully loaded. 11 | * @constructor 12 | */ 13 | GJS.LoadingBar = function(objectsToPoll) { 14 | if (objectsToPoll === undefined) { 15 | objectsToPoll = []; 16 | if (typeof GJS.Sprite !== 'undefined') { 17 | objectsToPoll.push(GJS.Sprite); 18 | } 19 | if (typeof GJS.Audio !== 'undefined') { 20 | objectsToPoll.push(GJS.Audio); 21 | } 22 | if (typeof GJS.utilTHREE !== 'undefined') { 23 | objectsToPoll.push(GJS.utilTHREE); 24 | } 25 | } 26 | this.objectsToPoll = objectsToPoll; 27 | this.loadedFraction = 0; 28 | this.allLoaded = false; 29 | this.sinceLoaded = 0; 30 | this.sinceStarted = 0; 31 | }; 32 | 33 | /** 34 | * @param {number} deltaTime Time passed from the last frame. 35 | * @return {boolean} True when fully loaded. 36 | */ 37 | GJS.LoadingBar.prototype.update = function(deltaTime) { 38 | this.sinceStarted += deltaTime; 39 | if (this.allLoaded) { 40 | this.sinceLoaded += deltaTime; 41 | return this.allLoaded; 42 | } 43 | this.loadedFraction = 0; 44 | this.allLoaded = true; 45 | for (var i = 0; i < this.objectsToPoll.length; ++i) { 46 | // 'loadedFraction' function name specified as string to support Closure compiler. 47 | var loadedFraction = this.objectsToPoll[i]['loadedFraction'](); 48 | if (loadedFraction < 1) { 49 | this.allLoaded = false; 50 | } 51 | this.loadedFraction += Math.min(loadedFraction, 1.0) / this.objectsToPoll.length; 52 | } 53 | return this.allLoaded; 54 | }; 55 | 56 | /** 57 | * @return {boolean} True when fully loaded. 58 | */ 59 | GJS.LoadingBar.prototype.finished = function() { 60 | return this.allLoaded; 61 | }; 62 | 63 | /** 64 | * Draw the loading bar. 65 | * @param {CanvasRenderingContext2D} ctx Context to draw the loading bar to. 66 | */ 67 | GJS.LoadingBar.prototype.render = function(ctx) { 68 | if (ctx === undefined) { 69 | return; 70 | } 71 | if (this.sinceLoaded < 1.0) { 72 | // Fake some loading animation even if loading doesn't really take any time. 73 | var percentage = Math.min(this.loadedFraction, this.sinceStarted * 4); 74 | 75 | var gl; 76 | if (ctx instanceof WebGLRenderingContext) { 77 | gl = ctx; 78 | } 79 | if (typeof THREE !== 'undefined' && ctx instanceof THREE.WebGLRenderer) { 80 | gl = ctx.context; 81 | } 82 | if (gl !== undefined) { 83 | if (this.sinceLoaded > 0.0) { 84 | // WebGL loading bar doesn't support fading out yet. 85 | return; 86 | } 87 | var restoreClearColor = gl.getParameter(gl.COLOR_CLEAR_VALUE); 88 | var restoreScissor = gl.getParameter(gl.SCISSOR_BOX); 89 | var restoreScissorTest = gl.isEnabled(gl.SCISSOR_TEST); 90 | 91 | var barWidth = Math.min(gl.canvas.width - 40, 200); 92 | var width = gl.canvas.width; 93 | var height = gl.canvas.height; 94 | 95 | gl.disable(gl.SCISSOR_TEST); 96 | gl.clearColor(0, 0, 0, 1); 97 | gl.clear(gl.COLOR_BUFFER_BIT); 98 | 99 | gl.enable(gl.SCISSOR_TEST); 100 | 101 | gl.clearColor(1, 1, 1, 1); 102 | gl.scissor(Math.floor(width * 0.5 - barWidth * 0.5), Math.floor(height * 0.5 - 25), 103 | barWidth, 50); 104 | gl.clear(gl.COLOR_BUFFER_BIT); 105 | 106 | gl.clearColor(0, 0, 0, 1); 107 | gl.scissor(Math.floor(width * 0.5 - (barWidth - 10) * 0.5), Math.floor(height * 0.5 - 20), 108 | barWidth - 10, 40); 109 | gl.clear(gl.COLOR_BUFFER_BIT); 110 | 111 | gl.clearColor(1, 1, 1, 1); 112 | gl.scissor(Math.floor(width * 0.5 - (barWidth - 20) * 0.5), Math.floor(height * 0.5 - 15), 113 | Math.floor((barWidth - 20) * percentage), 30); 114 | gl.clear(gl.COLOR_BUFFER_BIT); 115 | 116 | gl.clearColor(restoreClearColor[0], restoreClearColor[1], restoreClearColor[2], restoreClearColor[3]); 117 | gl.scissor(restoreScissor[0], restoreScissor[1], restoreScissor[2], restoreScissor[3]); 118 | if (!restoreScissorTest) { 119 | gl.disable(gl.SCISSOR_TEST); 120 | } 121 | return; 122 | } 123 | if (ctx.fillRect === undefined) { 124 | return; 125 | } 126 | var barWidth = Math.min(ctx.canvas.width - 40, 200); 127 | ctx.save(); 128 | ctx.globalAlpha = Math.min(1.0, (1.0 - this.sinceLoaded) * 1.5); 129 | ctx.fillStyle = '#000'; 130 | ctx.fillRect(0, 0, ctx.canvas.width, ctx.canvas.height); 131 | ctx.translate(ctx.canvas.width * 0.5, ctx.canvas.height * 0.5); 132 | ctx.fillStyle = '#fff'; 133 | ctx.fillRect(-barWidth * 0.5, -25, barWidth, 50); 134 | ctx.fillStyle = '#000'; 135 | ctx.fillRect(-(barWidth - 10) * 0.5, -20, barWidth - 10, 40); 136 | ctx.fillStyle = '#fff'; 137 | ctx.fillRect(-(barWidth - 20) * 0.5, -15, (barWidth - 20) * percentage, 30); 138 | ctx.restore(); 139 | } 140 | }; 141 | -------------------------------------------------------------------------------- /src/unlocker.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | // Requires utiljs.js 4 | 5 | /** 6 | * Class to inherit to implement a condition for unlocking a single unlock. 7 | * @constructor 8 | */ 9 | var UnlockCondition = function(options) { 10 | }; 11 | 12 | /** 13 | * Initialize the condition. 14 | * @param {Object} options Object with the following keys: 15 | * unlockId: string Identifier for the unlock. 16 | */ 17 | UnlockCondition.prototype.initCondition = function(options) { 18 | var defaults = { 19 | unlockId: '' 20 | }; 21 | objectUtil.initWithDefaults(this, defaults, options); 22 | this.fulfilled = false; 23 | }; 24 | 25 | /** 26 | * Set the unlock id for the condition. Only call before the condition is added to the Unlocker. 27 | * @param {string} unlockId Identifier for the unlock. 28 | */ 29 | UnlockCondition.prototype.setId = function(unlockId) { 30 | this.unlockId = unlockId; 31 | }; 32 | 33 | /** 34 | * Evaluate unlocking condition and set the member "fulfilled" when the condition is fulfilled. 35 | * @param {Object} gameState Object that unlocking is based on. 36 | * @param {number} deltaTime Time that has passed since the last update in seconds. 37 | */ 38 | UnlockCondition.prototype.update = function(gameState, deltaTime) { 39 | return; 40 | }; 41 | 42 | /** 43 | * @return {string} A description of the unlock condition. 44 | */ 45 | UnlockCondition.prototype.getDescription = function() { 46 | return ""; 47 | }; 48 | 49 | 50 | /** 51 | * An unlock condition that always passes. 52 | * @constructor 53 | * @param {Object} options Object with the following keys: 54 | * unlockId: string Identifier for the unlock. 55 | */ 56 | var UnlockByDefault = function(options) { 57 | this.initCondition(options); 58 | this.fulfilled = true; 59 | }; 60 | 61 | UnlockByDefault.prototype = new UnlockCondition(); 62 | 63 | 64 | /** 65 | * An unlock condition that never passes. 66 | * @constructor 67 | * @param {Object} options Object with the following keys: 68 | * unlockId: string Identifier for the unlock. 69 | */ 70 | var NeverUnlock = function(options) { 71 | this.initCondition(options); 72 | }; 73 | 74 | NeverUnlock.prototype = new UnlockCondition(); 75 | 76 | 77 | /** 78 | * @constructor 79 | * Engine for managing game unlocks. Each unlock is identified by an id, has a condition that's an instance of 80 | * UnlockCondition based on game state and can be either unlocked (true) or locked (false). 81 | */ 82 | var Unlocker = function(options) { 83 | var defaults = { 84 | gameName: 'game', 85 | needCommitUnlocks: false, 86 | conditions: [] 87 | }; 88 | objectUtil.initWithDefaults(this, defaults, options); 89 | this._fulfilledConditions = []; 90 | this.unlocks = {}; 91 | this.unlocksInOrder = []; 92 | for (var i = 0; i < this.conditions.length; ++i) { 93 | var condition = this.conditions[i]; 94 | this.unlocks[condition.unlockId] = false; 95 | this._checkFulfilled(condition); 96 | } 97 | }; 98 | 99 | /** 100 | * @param {UnlockCondition} condition Check if a condition is fulfilled. 101 | * @protected 102 | */ 103 | Unlocker.prototype._checkFulfilled = function(condition) { 104 | if (condition.fulfilled) { 105 | this._fulfilledConditions.push(condition.unlockId); 106 | if (!this.needCommitUnlocks) { 107 | this.commitUnlock(condition.unlockId); 108 | } 109 | } 110 | }; 111 | 112 | /** 113 | * Evaluate unlocking conditions. 114 | * @param {Object} gameState Object that unlocking is based on. 115 | * @param {number} deltaTime Time that has passed since the last update in seconds. 116 | */ 117 | Unlocker.prototype.update = function(gameState, deltaTime) { 118 | for (var i = 0; i < this.conditions.length; ++i) { 119 | var condition = this.conditions[i]; 120 | if (!this.unlocks[condition.unlockId] && this._fulfilledConditions.indexOf(condition.unlockId) < 0) { 121 | condition.update(gameState, deltaTime); 122 | this._checkFulfilled(condition); 123 | } 124 | } 125 | }; 126 | 127 | /** 128 | * @param {string} unlockId Id of the condition to get the description for. 129 | * @return {string} A description of the unlock condition. 130 | */ 131 | Unlocker.prototype.getDescription = function(unlockId) { 132 | for (var i = 0; i < this.conditions.length; ++i) { 133 | var condition = this.conditions[i]; 134 | if (condition.unlockId === unlockId) { 135 | return condition.getDescription(); 136 | } 137 | } 138 | return ''; 139 | }; 140 | 141 | /** 142 | * @return {Array.} List of unlockIds of the conditions that have been fulfilled since the last time this 143 | * function was called. 144 | */ 145 | Unlocker.prototype.popFulfilledUnlockConditions = function() { 146 | var fulfilledConditions = this._fulfilledConditions; 147 | this._fulfilledConditions = []; 148 | return fulfilledConditions; 149 | }; 150 | 151 | /** 152 | * @param {string} unlockId Id to mark as unlocked. 153 | * @return {boolean} True if the unlock was actually stored. 154 | */ 155 | Unlocker.prototype.commitUnlock = function(unlockId) { 156 | if (this.unlocks.hasOwnProperty(unlockId)) { 157 | this.unlocks[unlockId] = true; 158 | this.unlocksInOrder.push(unlockId); 159 | return true; 160 | } 161 | return false; 162 | }; 163 | 164 | /** 165 | * Load unlocks from storage. 166 | * @param {Storage} storage Storage object to load from. 167 | */ 168 | Unlocker.prototype.loadFrom = function(storage) { 169 | var unlocksInOrder = null; 170 | try { 171 | unlocksInOrder = JSON.parse(storage.getItem(this.gameName + '-gameutilsjs-unlocks-in-order')); 172 | } catch(e) { 173 | return; 174 | } 175 | if (unlocksInOrder === null) { 176 | return; 177 | } 178 | this.unlocksInOrder = []; 179 | for (var i = 0; i < unlocksInOrder.length; ++i) { 180 | var key = unlocksInOrder[i]; 181 | this.commitUnlock(key); 182 | } 183 | }; 184 | 185 | /** 186 | * Save unlocks to storage. 187 | * @param {Storage} storage Storage object to save to. 188 | */ 189 | Unlocker.prototype.saveTo = function(storage) { 190 | storage.setItem(this.gameName + '-gameutilsjs-unlocks-version', '1'); 191 | storage.setItem(this.gameName + '-gameutilsjs-unlocks-in-order', JSON.stringify(this.unlocksInOrder)); 192 | }; 193 | -------------------------------------------------------------------------------- /assets/models/stop_stationary.json: -------------------------------------------------------------------------------- 1 | { 2 | "vertices":[0.422749,-0.5,-0.5,-0.422749,-0.5,-0.5,-0.413625,-0.489208,-0.479966,0.413625,-0.489208,-0.479966,0.422749,0.5,-0.5,0.422749,-0.5,-0.5,0.413625,-0.489208,-0.479966,0.413625,0.489208,-0.479966,-0.422749,-0.5,0.5,0.422749,-0.5,0.5,0.413625,-0.489208,0.479966,-0.413625,-0.489208,0.479966,-0.422749,-0.5,0.5,-0.422749,0.5,0.5,-0.5,0.5,0.422749,-0.5,-0.5,0.422749,-0.422749,0.5,-0.5,-0.422749,-0.5,-0.5,-0.5,-0.5,-0.422749,-0.5,0.5,-0.422749,0.5,0.5,-0.422749,0.5,-0.5,-0.422749,0.422749,-0.5,-0.5,0.422749,0.5,-0.5,0.422749,0.5,0.5,0.422749,-0.5,0.5,0.5,-0.5,0.422749,0.5,0.5,0.422749,-0.5,-0.5,0.422749,-0.5,0.5,0.422749,-0.479966,0.489208,0.413625,-0.479966,-0.489208,0.413625,0.479966,0.489208,-0.413625,0.479966,0.489208,0.413625,0.479966,-0.489208,0.413625,0.479966,-0.489208,-0.413625,0.413625,0.489208,0.479966,-0.413625,0.489208,0.479966,-0.413625,-0.489208,0.479966,0.413625,-0.489208,0.479966,-0.413625,0.489208,-0.479966,0.413625,0.489208,-0.479966,0.413625,-0.489208,-0.479966,-0.413625,-0.489208,-0.479966,-0.479966,0.489208,0.413625,-0.479966,0.489208,-0.413625,-0.479966,-0.489208,-0.413625,-0.479966,-0.489208,0.413625,-0.422749,-0.5,-0.5,-0.422749,0.5,-0.5,-0.413625,0.489208,-0.479966,-0.413625,-0.489208,-0.479966,0.5,-0.5,0.422749,0.5,-0.5,-0.422749,0.479966,-0.489208,-0.413625,0.479966,-0.489208,0.413625,0.422749,-0.5,0.5,0.422749,0.5,0.5,0.413625,0.489208,0.479966,0.413625,-0.489208,0.479966,0.422749,0.5,0.5,-0.422749,0.5,0.5,-0.413625,0.489208,0.479966,0.413625,0.489208,0.479966,-0.5,0.5,-0.422749,-0.5,-0.5,-0.422749,-0.479966,-0.489208,-0.413625,-0.479966,0.489208,-0.413625,0.5,0.5,-0.422749,0.5,0.5,0.422749,0.479966,0.489208,0.413625,0.479966,0.489208,-0.413625,0.5,0.5,0.422749,0.5,-0.5,0.422749,0.479966,-0.489208,0.413625,0.479966,0.489208,0.413625,-0.422749,0.5,-0.5,0.422749,0.5,-0.5,0.413625,0.489208,-0.479966,-0.413625,0.489208,-0.479966,-0.422749,0.5,0.5,-0.422749,-0.5,0.5,-0.413625,-0.489208,0.479966,-0.413625,0.489208,0.479966,-0.5,0.5,0.422749,-0.5,0.5,-0.422749,-0.479966,0.489208,-0.413625,-0.479966,0.489208,0.413625,0.5,-0.5,-0.422749,0.5,0.5,-0.422749,0.479966,0.489208,-0.413625,0.479966,-0.489208,-0.413625,-0.5,-0.5,-0.422749,-0.5,-0.5,0.422749,-0.479966,-0.489208,0.413625,-0.479966,-0.489208,-0.413625,-0.3,0.5,-0,-0.3,-0.5,0,-0.3,-0.5,-0.3,-0.3,0.5,-0.3,0,0.5,-0.3,0,-0.5,-0.3,0.3,-0.5,-0.3,0.3,0.5,-0.3,0.3,0.5,-0,0.3,-0.5,0,0.3,-0.5,0.3,0.3,0.5,0.3,0,0.5,0.3,0,-0.5,0.3,-0.3,-0.5,0.3,-0.3,0.5,0.3,0.3,0.5,-0.3,0.3,-0.5,-0.3,0.3,-0.5,0,0.3,0.5,-0,-0.3,0.5,-0.3,-0.3,-0.5,-0.3,0,-0.5,-0.3,0,0.5,-0.3,-0.3,0.5,0.3,-0.3,-0.5,0.3,-0.3,-0.5,0,-0.3,0.5,-0,0.3,0.5,0.3,0.3,-0.5,0.3,0,-0.5,0.3,0,0.5,0.3,-0.5,0.5,0.422749,-0.3,0.5,0.3,-0.3,0.5,-0,-0.5,0.5,-0.422749,-0.5,0.5,-0.422749,-0.3,0.5,-0,-0.3,0.5,-0.3,-0.422749,0.5,-0.5,-0.422749,0.5,-0.5,-0.3,0.5,-0.3,0,0.5,-0.3,0.422749,0.5,-0.5,0.422749,0.5,-0.5,0,0.5,-0.3,0.3,0.5,-0.3,0.5,0.5,-0.422749,0.5,0.5,-0.422749,0.3,0.5,-0.3,0.3,0.5,-0,0.5,0.5,0.422749,0.5,0.5,0.422749,0.3,0.5,-0,0.3,0.5,0.3,0.422749,0.5,0.5,0.422749,0.5,0.5,0.3,0.5,0.3,0,0.5,0.3,-0.422749,0.5,0.5,-0.422749,0.5,0.5,0,0.5,0.3,-0.3,0.5,0.3,-0.5,0.5,0.422749,-0.422749,-0.5,0.5,-0.3,-0.5,0.3,0,-0.5,0.3,0.422749,-0.5,0.5,0.422749,-0.5,0.5,0,-0.5,0.3,0.3,-0.5,0.3,0.5,-0.5,0.422749,0.5,-0.5,0.422749,0.3,-0.5,0.3,0.3,-0.5,0,0.5,-0.5,-0.422749,0.5,-0.5,-0.422749,0.3,-0.5,0,0.3,-0.5,-0.3,0.422749,-0.5,-0.5,0.422749,-0.5,-0.5,0.3,-0.5,-0.3,0,-0.5,-0.3,-0.422749,-0.5,-0.5,-0.422749,-0.5,-0.5,0,-0.5,-0.3,-0.3,-0.5,-0.3,-0.5,-0.5,-0.422749,-0.5,-0.5,-0.422749,-0.3,-0.5,-0.3,-0.3,-0.5,0,-0.5,-0.5,0.422749,-0.5,-0.5,0.422749,-0.3,-0.5,0,-0.3,-0.5,0.3,-0.422749,-0.5,0.5], 3 | "metadata":{ 4 | "vertices":192, 5 | "faces":48, 6 | "generator":"io_three", 7 | "version":3, 8 | "type":"Geometry", 9 | "uvs":0, 10 | "materials":2, 11 | "normals":26 12 | }, 13 | "faces":[35,0,1,2,3,0,0,0,0,0,35,4,5,6,7,0,1,1,1,1,35,8,9,10,11,0,2,2,2,2,35,12,13,14,15,0,3,3,3,3,35,16,17,18,19,0,4,4,4,4,35,20,21,22,23,0,5,5,5,5,35,24,25,26,27,0,6,6,6,6,35,28,29,30,31,0,7,7,7,7,35,32,33,34,35,1,8,8,8,8,35,36,37,38,39,1,9,9,9,9,35,40,41,42,43,1,10,10,10,10,35,44,45,46,47,1,11,11,11,11,35,48,49,50,51,0,12,12,12,12,35,52,53,54,55,0,13,13,13,13,35,56,57,58,59,0,14,14,14,14,35,60,61,62,63,0,15,15,15,15,35,64,65,66,67,0,16,16,16,16,35,68,69,70,71,0,17,17,17,17,35,72,73,74,75,0,18,18,18,18,35,76,77,78,79,0,19,19,19,19,35,80,81,82,83,0,20,20,20,20,35,84,85,86,87,0,21,21,21,21,35,88,89,90,91,0,22,22,22,22,35,92,93,94,95,0,23,23,23,23,35,96,97,98,99,0,8,8,8,8,35,100,101,102,103,0,9,9,9,9,35,104,105,106,107,0,11,11,11,11,35,108,109,110,111,0,10,10,10,10,35,112,113,114,115,0,11,11,11,11,35,116,117,118,119,0,9,9,9,9,35,120,121,122,123,0,8,8,8,8,35,124,125,126,127,0,10,10,10,10,35,128,129,130,131,0,24,24,24,24,35,132,133,134,135,0,24,24,24,24,35,136,137,138,139,0,24,24,24,24,35,140,141,142,143,0,24,24,24,24,35,144,145,146,147,0,24,24,24,24,35,148,149,150,151,0,24,24,24,24,35,152,153,154,155,0,24,24,24,24,35,156,157,158,159,0,24,24,24,24,35,160,161,162,163,0,25,25,25,25,35,164,165,166,167,0,25,25,25,25,35,168,169,170,171,0,25,25,25,25,35,172,173,174,175,0,25,25,25,25,35,176,177,178,179,0,25,25,25,25,35,180,181,182,183,0,25,25,25,25,35,184,185,186,187,0,25,25,25,25,35,188,189,190,191,0,25,25,25,25], 14 | "uvs":[], 15 | "materials":[{ 16 | "DbgIndex":0, 17 | "DbgName":"stationary_frame", 18 | "specularCoef":50, 19 | "wireframe":false, 20 | "colorDiffuse":[0.4,0.4,0.4], 21 | "shading":"phong", 22 | "depthWrite":true, 23 | "DbgColor":15658734, 24 | "transparent":false, 25 | "opacity":1, 26 | "colorSpecular":[0.05102,0.05102,0.05102], 27 | "visible":true, 28 | "colorEmissive":[0,0,0], 29 | "depthTest":true, 30 | "blending":"NormalBlending" 31 | },{ 32 | "DbgIndex":1, 33 | "DbgName":"stationary_window", 34 | "specularCoef":50, 35 | "wireframe":false, 36 | "colorDiffuse":[0.038537,0.064176,0.106295], 37 | "shading":"phong", 38 | "depthWrite":true, 39 | "DbgColor":15597568, 40 | "transparent":false, 41 | "opacity":1, 42 | "colorSpecular":[0.377551,0.377551,0.377551], 43 | "visible":true, 44 | "colorEmissive":[0,0,0], 45 | "depthTest":true, 46 | "blending":"NormalBlending" 47 | }], 48 | "normals":[0,0.880367,-0.474227,-0.910031,0,-0.414472,0,0.880367,0.474227,-0.707083,0,0.707083,-0.707083,0,-0.707083,0.707083,0,-0.707083,0.707083,0,0.707083,-0.414472,0,-0.910031,1,0,0,0,0,1,0,0,-1,-1,0,0,0.910031,0,-0.414472,0.474227,0.880367,0,-0.910031,0,0.414472,0,-0.880367,0.474227,-0.414472,0,0.910031,0.474227,-0.880367,0,0.414472,0,-0.910031,0,-0.880367,-0.474227,0.910031,0,0.414472,-0.474227,-0.880367,0,0.414472,0,0.910031,-0.474227,0.880367,0,0,1,0,0,-1,0], 49 | "name":"Cube.004Geometry" 50 | } -------------------------------------------------------------------------------- /src/threesceneobject.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | // Requires utiljs.js 4 | 5 | if (typeof GJS === "undefined") { 6 | var GJS = {}; 7 | } 8 | 9 | /** 10 | * An object that owns a THREE.Object3D. 11 | * @constructor 12 | */ 13 | GJS.ThreeSceneObject = function() { 14 | }; 15 | 16 | /** 17 | * Initialize. 18 | * @param {Object} options Options with the following keys: 19 | * sceneParent (Object3D): Parent of the object in the scene. 20 | * object (Object3D): Object that this object will own and add under sceneParent. 21 | */ 22 | GJS.ThreeSceneObject.prototype.initThreeSceneObject = function(options) { 23 | var defaults = { 24 | sceneParent: null, 25 | object: null 26 | }; 27 | objectUtil.initWithDefaults(this, defaults, options); 28 | this._inScene = false; 29 | }; 30 | 31 | /** 32 | * Add this object to the scene if it is not there. 33 | */ 34 | GJS.ThreeSceneObject.prototype.addToScene = function() { 35 | if (!this._inScene) { 36 | this.sceneParent.add(this.object); 37 | this._inScene = true; 38 | } 39 | }; 40 | 41 | /** 42 | * Remove this object from the scene if it is in there. 43 | */ 44 | GJS.ThreeSceneObject.prototype.removeFromScene = function() { 45 | if (this._inScene) { 46 | this.sceneParent.remove(this.object); 47 | this._inScene = false; 48 | } 49 | }; 50 | 51 | /** 52 | * @param {THREE.Object3D} object Object to query. 53 | * @return {boolean} True if object is in the owned part of the scene graph. 54 | */ 55 | GJS.ThreeSceneObject.prototype.ownsSceneObject = function(object) { 56 | var matches = false; 57 | this.getOwnQueryObject().traverse(function(obj) { 58 | if (obj === object) { 59 | matches = true; 60 | } 61 | }); 62 | return matches; 63 | }; 64 | 65 | /** 66 | * Override this to customize which parts of the scene this object is considered to own for the purposes of 67 | * ownsSceneObject. 68 | * @return {THREE.Object3D} object this object owns 69 | */ 70 | GJS.ThreeSceneObject.prototype.getOwnQueryObject = function() { 71 | return this.object; 72 | }; 73 | 74 | /** 75 | * Update the object. Override this to do time-based updates. 76 | */ 77 | GJS.ThreeSceneObject.prototype.update = function(deltaTime) { 78 | }; 79 | 80 | 81 | 82 | /** 83 | * Base type for objects that display a text string as a Three.js mesh. 84 | * Use classes that inherit this class, like GJS.ThreeExtrudedTextObject to display text in the 3D scene. 85 | * @constructor 86 | */ 87 | GJS.ThreeTextObject = function() { 88 | }; 89 | 90 | GJS.ThreeTextObject.prototype = new GJS.ThreeSceneObject(); 91 | 92 | /** 93 | * @param {Object} options 94 | */ 95 | GJS.ThreeTextObject.prototype.initThreeTextObject = function(options) { 96 | var defaults = { 97 | string: "", 98 | maxRowLength: -1, 99 | rowSpacing: 1.3, 100 | textAlign: 'center' 101 | }; 102 | objectUtil.initWithDefaults(this, defaults, options); 103 | 104 | this.initThreeSceneObject(options); 105 | this.object = new THREE.Object3D(); 106 | 107 | var string = this.string; 108 | this.string = ""; 109 | this.setString(string); 110 | }; 111 | 112 | /** 113 | * @param {string} string String to display. 114 | */ 115 | GJS.ThreeTextObject.prototype.setString = function(string) { 116 | this.string = string; 117 | this.stringSplitToRows = stringUtil.splitToRows(this.string, this.maxRowLength); 118 | }; 119 | 120 | /** 121 | * An object that displays a text string as an extruded Three.js mesh. 122 | * To use this, first set GJS.ThreeExtrudedTextObject.defaultFont. You can use GJS.utilTHREE.loadFont to load the font. 123 | * The scene object that acts as a parent to the text will be stored under the property "object" and can be accessed 124 | * directly to set its model transform or to add additional children. 125 | * @param {Object} options Options with the following keys, in addition to THREE.TextGeometry and ThreeSceneObject 126 | * options: 127 | * string (string) 128 | * maxRowLength (number): Maximum row length in characters. 129 | * rowSpacing (number): Relative spacing of rows. 130 | * textAlign (string): 'left', 'center' or 'right' 131 | * receiveShadow (boolean) 132 | * castShadow (boolean) 133 | * @constructor 134 | */ 135 | GJS.ThreeExtrudedTextObject = function(options) { 136 | var defaults = { 137 | material: GJS.ThreeExtrudedTextObject.defaultMaterial, 138 | font: GJS.ThreeExtrudedTextObject.defaultFont, 139 | extrusionHeight: 0.1, 140 | curveSegments: 1, 141 | bevelEnabled: false, 142 | castShadow: false, 143 | receiveShadow: false 144 | }; 145 | objectUtil.initWithDefaults(this, defaults, options); 146 | this.rowMeshes = []; 147 | this.initThreeTextObject(options); 148 | }; 149 | 150 | GJS.ThreeExtrudedTextObject.defaultMaterial = new THREE.MeshPhongMaterial( { color: 0x333333, specular: 0x000000 } ); 151 | GJS.ThreeExtrudedTextObject.defaultFont = null; 152 | 153 | GJS.ThreeExtrudedTextObject.prototype = new GJS.ThreeTextObject(); 154 | 155 | /** 156 | * @param {string} string String to display. 157 | */ 158 | GJS.ThreeExtrudedTextObject.prototype.setString = function(string) { 159 | if (string != this.string) { 160 | GJS.ThreeTextObject.prototype.setString.call(this, string); 161 | for (var i = 0; i < this.rowMeshes.length; ++i) { 162 | this.object.remove(this.rowMeshes[i]); 163 | } 164 | this.rowMeshes.splice(0); 165 | for (var i = 0; i < this.stringSplitToRows.length; ++i) { 166 | var rowMesh = this._createTextMesh(this.stringSplitToRows[i]); 167 | if (this.textAlign === 'left') { 168 | rowMesh.position.x = -rowMesh.geometry.boundingBox.min.x; 169 | } else if (this.textAlign === 'right') { 170 | rowMesh.position.x = -rowMesh.geometry.boundingBox.max.x; 171 | } 172 | rowMesh.position.y = (this.stringSplitToRows.length - i - 0.5) * this.rowSpacing; 173 | this.object.add(rowMesh); 174 | this.rowMeshes.push(rowMesh); 175 | } 176 | } 177 | }; 178 | 179 | /** 180 | * @param {string} string String to create a mesh for. 181 | * @return {THREE.Object3D} Text geometry object. 182 | */ 183 | GJS.ThreeExtrudedTextObject.prototype._createTextMesh = function(string) { 184 | var textGeo = new THREE.TextGeometry( string, { 185 | font: this.font, 186 | size: 1, 187 | height: this.extrusionHeight, 188 | curveSegments: this.curveSegments, 189 | bevelEnabled: this.bevelEnabled, 190 | }); 191 | textGeo.center(); 192 | textGeo.computeBoundingBox(); 193 | var textMesh = new THREE.Mesh( textGeo, this.material ); 194 | textMesh.castShadow = this.castShadow; 195 | textMesh.receiveShadow = this.receiveShadow; 196 | return textMesh; 197 | }; 198 | -------------------------------------------------------------------------------- /src/mainloop.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /** 4 | * Start a main loop on the provided game with the provided options. 5 | * @param {Array.} updateables Objects with two functions: update() and render(). 6 | * update(deltaTime) should update the game state. The deltaTime parameter 7 | * is time passed since the last update in seconds. 8 | * render() should draw the current game state and optionally return a 9 | * CanvasRenderingContext2D that the following updateables in the array will use. 10 | * Updateables that are processed after the first one receive this rendering context 11 | * as a parameter. 12 | * @param {Object} options Takes the following keys (all optional): 13 | * 14 | * updateFPS: number 15 | * The rate at which the game state receives update() calls. 16 | * Having a fixed update rate can help you to make the game deterministic 17 | * and to keep physics calculations stable. 18 | * Every update is not necessarily displayed on the screen. 19 | * 20 | * debugMode: boolean 21 | * If Mousetrap is imported, you may hold F to speed up the game 22 | * execution or G to slow it down while in debug mode. 23 | * 24 | * frameLog: boolean 25 | * When frame log is on, a timeline of frames is drawn on the canvas returned 26 | * from updateables[i].render(). 27 | * - Green in the log is an update which was rendered to the screen. 28 | * - Orange in the log is an update which was not rendered to the screen. 29 | * - White in the log is a frame on which the game state was not updated. 30 | * 31 | * onRefocus: function 32 | * Function that should be called when the window becomes visible after it 33 | * has been invisible for a while. 34 | */ 35 | var startMainLoop = function(updateables, options) { 36 | var defaults = { 37 | updateFPS: 60, 38 | debugMode: false, 39 | frameLog: false, 40 | onRefocus: null 41 | }; 42 | 43 | if (options === undefined) { 44 | options = {}; 45 | } 46 | for(var key in defaults) { 47 | if(!options.hasOwnProperty(key)) { 48 | options[key] = defaults[key]; 49 | } 50 | } 51 | if (!(updateables instanceof Array)) { 52 | updateables = [updateables]; 53 | } 54 | 55 | var now = function() { 56 | if (typeof performance !== 'undefined' && 'now' in performance) { 57 | return performance.now(); 58 | } else { 59 | return Date.now(); 60 | } 61 | }; 62 | 63 | var timePerUpdate = 1000 / options.updateFPS; 64 | 65 | var nextFrameTime = -1; 66 | 67 | var frameLog = []; 68 | 69 | var logTimeToX = function(time, lastTime, canvasWidth) { 70 | var fpsMult = Math.max(60, options.updateFPS) / 60; 71 | return (canvasWidth - 1) - (Math.ceil(lastTime / 2000) * 2000 - time) * 0.2 * fpsMult; 72 | } 73 | 74 | var drawFrameLog = function(ctx, callbackTime) { 75 | var w = ctx.canvas.width; 76 | ctx.save(); 77 | ctx.globalAlpha = 0.5; 78 | ctx.fillStyle = '#000'; 79 | ctx.fillRect(0, 0, w, 10); 80 | 81 | ctx.globalAlpha = 1.0; 82 | ctx.fillStyle = '#0f0'; 83 | var lastTime = callbackTime; 84 | var i = frameLog.length; 85 | var x = ctx.canvas.width; 86 | while (i > 0 && x > 0) { 87 | --i; 88 | var frameStats = frameLog[i]; 89 | if (frameStats.updates >= 1) { 90 | ctx.fillRect(logTimeToX(frameStats.time, lastTime, w), 0, 2, 10); 91 | if (frameStats.updates > 1) { 92 | ctx.fillStyle = '#f84'; 93 | for (var j = 1; j < frameStats.updates; ++j) { 94 | var updateX = logTimeToX(frameStats.time - (j - 0.5) * timePerUpdate, lastTime, w); 95 | ctx.fillRect(Math.round(updateX), 0, 2, 10); 96 | } 97 | ctx.fillStyle = '#0f0'; 98 | } 99 | } else { 100 | ctx.fillStyle = '#fff'; 101 | ctx.fillRect(logTimeToX(frameStats.time, lastTime, w), 0, 2, 10); 102 | ctx.fillStyle = '#0f0'; 103 | } 104 | } 105 | ctx.restore(); 106 | }; 107 | 108 | var visible = true; 109 | var visibilityChange = function() { 110 | visible = document.visibilityState == document.PAGE_VISIBLE || (document.hidden === false); 111 | nextFrameTime = -1; 112 | if (visible && options.onRefocus != null) { 113 | options.onRefocus(); 114 | } 115 | }; 116 | 117 | document.addEventListener('visibilitychange', visibilityChange); 118 | 119 | var fastForward = false; 120 | var slowedDown = false; 121 | if (options.debugMode && typeof window.Mousetrap !== 'undefined' && window.Mousetrap.bindGlobal !== undefined) { 122 | var speedUp = function() { 123 | fastForward = true; 124 | }; 125 | var noSpeedUp = function() { 126 | fastForward = false; 127 | }; 128 | var slowDown = function() { 129 | slowedDown = true; 130 | }; 131 | var noSlowDown = function() { 132 | slowedDown = false; 133 | }; 134 | window.Mousetrap.bindGlobal('f', speedUp, 'keydown'); 135 | window.Mousetrap.bindGlobal('f', noSpeedUp, 'keyup'); 136 | window.Mousetrap.bindGlobal('g', slowDown, 'keydown'); 137 | window.Mousetrap.bindGlobal('g', noSlowDown, 'keyup'); 138 | } 139 | 140 | var frame = function() { 141 | // Process a single requestAnimationFrame callback 142 | if (!visible) { 143 | requestAnimationFrame(frame); 144 | return; 145 | } 146 | var time = now(); 147 | var callbackTime = time; 148 | var updated = false; 149 | var updates = 0; 150 | if (nextFrameTime < 0) { 151 | nextFrameTime = time - timePerUpdate * 0.5; 152 | } 153 | // If there's been a long time since the last callback, it can be a sign that the game 154 | // is running very badly but it is possible that the game has gone out of focus entirely. 155 | // In either case, it is reasonable to do a maximum of half a second's worth of updates 156 | // at once. 157 | if (time - nextFrameTime > 500) { 158 | nextFrameTime = time - 500; 159 | } 160 | while (time > nextFrameTime) { 161 | if (fastForward) { 162 | nextFrameTime += timePerUpdate / 5; 163 | } else { 164 | if (slowedDown) { 165 | nextFrameTime += timePerUpdate * 5; 166 | } else { 167 | nextFrameTime += timePerUpdate; 168 | } 169 | } 170 | for (var i = 0; i < updateables.length; ++i) { 171 | updateables[i].update(timePerUpdate * 0.001); 172 | } 173 | updates++; 174 | } 175 | if (options.frameLog) { 176 | frameLog.push({time: callbackTime, updates: updates}); 177 | } 178 | if (updates > 0) { 179 | var ctx = updateables[0].render(); 180 | for (var i = 1; i < updateables.length; ++i) { 181 | var candidateCtx = updateables[i].render(ctx); 182 | if (candidateCtx !== undefined) { 183 | ctx = candidateCtx; 184 | } 185 | } 186 | if (options.frameLog && (ctx instanceof CanvasRenderingContext2D)) { 187 | drawFrameLog(ctx, callbackTime); 188 | if (frameLog.length >= 1024) { 189 | frameLog.splice(0, 512); 190 | } 191 | } 192 | } 193 | requestAnimationFrame(frame); 194 | }; 195 | frame(); 196 | }; 197 | -------------------------------------------------------------------------------- /src/audio.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | if (typeof GJS === "undefined") { 4 | var GJS = {}; 5 | } 6 | 7 | /** 8 | * An object representing one audio sample. Uses Howler.js under the hood if it is available. 9 | * @param {string} filename Name of the audio file without a file extension. Assumes that the audio file is located 10 | * in GJS.Audio.audioPath. 11 | * @param {Array.} fileExtensions Array of extensions. Defaults to ogg and mp3, which should be enough for 12 | * cross-browser compatibility. The default file extensions are configurable through GJS.Audio.defaultExtensions. 13 | * @constructor 14 | */ 15 | GJS.Audio = function(filename, fileExtensions) { 16 | if (fileExtensions === undefined) { 17 | fileExtensions = GJS.Audio.defaultExtensions; 18 | } 19 | var that = this; 20 | GJS.Audio.allAudio.push(this); 21 | this.loaded = false; // Used purely for purposes of marking the audio loaded. 22 | var markLoaded = function() { 23 | that._markLoaded(); 24 | } 25 | 26 | this.filenames = []; 27 | var canDetermineLoaded = false; 28 | for (var i = 0; i < fileExtensions.length; ++i) { 29 | this.filenames.push(GJS.Audio.audioPath + filename + '.' + fileExtensions[i]); 30 | } 31 | // Don't use howler when using the file protocol, since it requires CORS requests 32 | if (typeof Howl !== 'undefined' && window.location.origin.substring(0, 4) != 'file') { 33 | // Use howler.js to implement GJS.Audio 34 | this._howl = new Howl({ 35 | src: this.filenames, 36 | onload: markLoaded, 37 | onloaderror: markLoaded 38 | }); 39 | return; 40 | } else { 41 | this._howl = null; 42 | } 43 | 44 | this.audio = document.createElement('audio'); 45 | for (var i = 0; i < fileExtensions.length; ++i) { 46 | if (fileExtensions[i] === 'ogg' && !canDetermineLoaded) { 47 | canDetermineLoaded = this.audio.canPlayType('audio/ogg;codecs="vorbis"') == 'probably'; 48 | } 49 | if (fileExtensions[i] === 'mp3' && !canDetermineLoaded) { 50 | canDetermineLoaded = this.audio.canPlayType('audio/mpeg') == 'probably'; 51 | } 52 | } 53 | 54 | this.playWhenReady = null; // Event listener to start playing when audio is ready. 55 | if (canDetermineLoaded) { 56 | this.audio.addEventListener('canplay', markLoaded); 57 | // Can never be sure that the audio will load. Fake loaded after 10 seconds to unblock loading bar. 58 | setTimeout(markLoaded, 10000); 59 | } else { 60 | this._markLoaded(); 61 | } 62 | this.addSourcesTo(this.audio); 63 | this.clones = []; 64 | this.ensureOneClone(); 65 | }; 66 | 67 | /** 68 | * Path for audio files. Set this before creating any GJS.Audio objects. 69 | */ 70 | GJS.Audio.audioPath = 'assets/audio/'; 71 | 72 | /** 73 | * Default file extensions. Set this before creating any GJS.Audio objects. Ogg and mp3 are enough for cross-browser 74 | * compatibility. 75 | */ 76 | GJS.Audio.defaultExtensions = ['ogg', 'mp3']; 77 | 78 | /** 79 | * True when all audio is muted. Set this by calling muteAll. 80 | */ 81 | GJS.Audio.allMuted = false; 82 | 83 | /** 84 | * @param {boolean} mute Set to true to mute all audio. 85 | */ 86 | GJS.Audio.muteAll = function(mute) { 87 | if (GJS.Audio.allMuted !== mute) { 88 | GJS.Audio.allMuted = mute; 89 | if (typeof Howler !== 'undefined') { 90 | if (mute) { 91 | Howler.mute(true); 92 | } else { 93 | Howler.mute(false); 94 | } 95 | } else { 96 | for (var i = 0; i < GJS.Audio.allAudio.length; ++i) { 97 | var audio = GJS.Audio.allAudio[i]; 98 | audio.audio.muted = mute; 99 | for (var j = 0; j < audio.clones.length; ++j) { 100 | audio.clones[j].muted = mute; 101 | } 102 | } 103 | } 104 | } 105 | }; 106 | 107 | /** 108 | * All audio objects that have been created. 109 | */ 110 | GJS.Audio.allAudio = []; 111 | 112 | /** 113 | * How many GJS.Audio objects have been fully loaded. 114 | */ 115 | GJS.Audio.loadedCount = 0; 116 | 117 | /** 118 | * @return {number} Amount of GJS.Audio objects that have been fully loaded per amount that has been created. 119 | * Name specified as string to support Closure compiler together with loadingbar.js. 120 | */ 121 | GJS.Audio['loadedFraction'] = function() { 122 | if (GJS.Audio.allAudio.length === 0) { 123 | return 1.0; 124 | } 125 | return GJS.Audio.loadedCount / GJS.Audio.allAudio.length; 126 | }; 127 | 128 | /** 129 | * @param {HTMLAudioElement} audioElement Element to add audio sources to. 130 | * @protected 131 | */ 132 | GJS.Audio.prototype.addSourcesTo = function(audioElement) { 133 | for (var i = 0; i < this.filenames.length; ++i) { 134 | var source = document.createElement('source'); 135 | source.src = this.filenames[i]; 136 | audioElement.appendChild(source); 137 | } 138 | }; 139 | 140 | /** 141 | * Play a clone of this sample. Will not affect other clones. Playback will not loop and playback can not be stopped. 142 | */ 143 | GJS.Audio.prototype.play = function () { 144 | if (this._howl) { 145 | this._howl.play(); 146 | return; 147 | } 148 | // If readyState was compared against 4, Firefox wouldn't play audio at all sometimes. That's why using 2 here. 149 | if (this.audio.readyState < 2) { 150 | return; 151 | } 152 | var clone = this.ensureOneClone(); 153 | clone.play(); 154 | this.ensureOneClone(); // Make another clone ready ahead of time. 155 | }; 156 | 157 | /** 158 | * Play this sample when it is ready. Use only if only one copy of this sample is going to play simultaneously. 159 | * Playback can be stopped by calling stop(). 160 | * @param {boolean=} loop Whether the sample should loop when played. Defaults to false. 161 | */ 162 | GJS.Audio.prototype.playSingular = function (loop) { 163 | if (loop === undefined) { 164 | loop = false; 165 | } 166 | if (this._howl) { 167 | if (this._howl.playing(0)) { 168 | return; 169 | } 170 | this._howl.play(); 171 | this._howl.loop(loop); 172 | return; 173 | } 174 | this.audio.loop = loop; 175 | if (this.audio.readyState >= 2) { 176 | if (this.playWhenReady !== null) { 177 | this.audio.removeEventListener('canplay', this.playWhenReady); 178 | this.playWhenReady = null; 179 | } 180 | this.audio.play(); 181 | this._markLoaded(); 182 | } else if (this.playWhenReady === null) { 183 | var that = this; 184 | this.playWhenReady = function() { 185 | that.audio.play(); 186 | that._markLoaded(); 187 | } 188 | this.audio.addEventListener('canplay', this.playWhenReady); 189 | } 190 | }; 191 | 192 | /** 193 | * Stop playing this sample. 194 | */ 195 | GJS.Audio.prototype.stop = function () { 196 | if (this._howl) { 197 | this._howl.stop(); 198 | return; 199 | } 200 | if (this.playWhenReady !== null) { 201 | this.audio.removeEventListener('canplay', this.playWhenReady); 202 | this.playWhenReady = null; 203 | } 204 | this.audio.pause(); 205 | this.audio.currentTime = 0; 206 | }; 207 | 208 | /** 209 | * Ensure that there is one clone available for playback and return it. 210 | * @protected 211 | * @return {HTMLAudioElement} Clone that is ready for playback. 212 | */ 213 | GJS.Audio.prototype.ensureOneClone = function() { 214 | for (var i = 0; i < this.clones.length; ++i) { 215 | if (this.clones[i].ended || (this.clones[i].readyState == 4 && this.clones[i].paused)) { 216 | this.clones[i].currentTime = 0; 217 | return this.clones[i]; 218 | } 219 | } 220 | var clone = document.createElement('audio'); 221 | if (GJS.Audio.allMuted) { 222 | clone.muted = true; 223 | } 224 | this.addSourcesTo(clone); 225 | this.clones.push(clone); 226 | return clone; 227 | }; 228 | 229 | /** 230 | * Mark this audio sample loaded. 231 | * @protected 232 | */ 233 | GJS.Audio.prototype._markLoaded = function() { 234 | if (this.loaded) { 235 | return; 236 | } 237 | this.loaded = true; 238 | GJS.Audio.loadedCount++; 239 | }; 240 | -------------------------------------------------------------------------------- /src/lib/FileSaver.js: -------------------------------------------------------------------------------- 1 | /* FileSaver.js 2 | * A saveAs() FileSaver implementation. 3 | * 2013-10-21 4 | * 5 | * By Eli Grey, http://eligrey.com 6 | * License: X11/MIT 7 | * See LICENSE.md 8 | */ 9 | 10 | /*global self */ 11 | /*jslint bitwise: true, regexp: true, confusion: true, es5: true, vars: true, white: true, 12 | plusplus: true */ 13 | 14 | /*! @source http://purl.eligrey.com/github/FileSaver.js/blob/master/FileSaver.js */ 15 | 16 | var saveAs = saveAs 17 | || (typeof navigator !== 'undefined' && navigator['msSaveOrOpenBlob'] && navigator['msSaveOrOpenBlob'].bind(navigator)) 18 | || (function(view) { 19 | "use strict"; 20 | var 21 | doc = view.document 22 | // only get URL when necessary in case BlobBuilder.js hasn't overridden it yet 23 | , get_URL = function() { 24 | return view.URL || view.webkitURL || view; 25 | } 26 | , URL = view.URL || view.webkitURL || view 27 | , save_link = doc.createElementNS("http://www.w3.org/1999/xhtml", "a") 28 | , can_use_save_link = !view.externalHost && "download" in save_link 29 | , click = function(node) { 30 | var event = doc.createEvent("MouseEvents"); 31 | event.initMouseEvent( 32 | "click", true, false, view, 0, 0, 0, 0, 0 33 | , false, false, false, false, 0, null 34 | ); 35 | node.dispatchEvent(event); 36 | } 37 | , webkit_req_fs = view.webkitRequestFileSystem 38 | , req_fs = view.requestFileSystem || webkit_req_fs || view['mozRequestFileSystem'] 39 | , throw_outside = function (ex) { 40 | (view.setImmediate || view.setTimeout)(function() { 41 | throw ex; 42 | }, 0); 43 | } 44 | , force_saveable_type = "application/octet-stream" 45 | , fs_min_size = 0 46 | , deletion_queue = [] 47 | , process_deletion_queue = function() { 48 | var i = deletion_queue.length; 49 | while (i--) { 50 | var file = deletion_queue[i]; 51 | if (typeof file === "string") { // file is an object URL 52 | URL.revokeObjectURL(file); 53 | } else { // file is a File 54 | file.remove(); 55 | } 56 | } 57 | deletion_queue.length = 0; // clear queue 58 | } 59 | , dispatch = function(filesaver, event_types, event) { 60 | event_types = [].concat(event_types); 61 | var i = event_types.length; 62 | while (i--) { 63 | var listener = filesaver["on" + event_types[i]]; 64 | if (typeof listener === "function") { 65 | try { 66 | listener.call(filesaver, event || filesaver); 67 | } catch (ex) { 68 | throw_outside(ex); 69 | } 70 | } 71 | } 72 | } 73 | , FileSaver = function(blob, name) { 74 | // First try a.download, then web filesystem, then object URLs 75 | var 76 | filesaver = this 77 | , type = blob.type 78 | , blob_changed = false 79 | , object_url 80 | , target_view 81 | , get_object_url = function() { 82 | var object_url = get_URL().createObjectURL(blob); 83 | deletion_queue.push(object_url); 84 | return object_url; 85 | } 86 | , dispatch_all = function() { 87 | dispatch(filesaver, "writestart progress write writeend".split(" ")); 88 | } 89 | // on any filesys errors revert to saving with object URLs 90 | , fs_error = function() { 91 | // don't create more object URLs than needed 92 | if (blob_changed || !object_url) { 93 | object_url = get_object_url(blob); 94 | } 95 | if (target_view) { 96 | target_view.location.href = object_url; 97 | } else { 98 | window.open(object_url, "_blank"); 99 | } 100 | filesaver.readyState = filesaver.DONE; 101 | dispatch_all(); 102 | } 103 | , abortable = function(func) { 104 | return function() { 105 | if (filesaver.readyState !== filesaver.DONE) { 106 | return func.apply(this, arguments); 107 | } 108 | }; 109 | } 110 | , create_if_not_found = {create: true, 'exclusive': false} 111 | , slice 112 | ; 113 | filesaver.readyState = filesaver.INIT; 114 | if (!name) { 115 | name = "download"; 116 | } 117 | if (can_use_save_link) { 118 | object_url = get_object_url(blob); 119 | // FF for Android has a nasty garbage collection mechanism 120 | // that turns all objects that are not pure javascript into 'deadObject' 121 | // this means `doc` and `save_link` are unusable and need to be recreated 122 | // `view` is usable though: 123 | doc = view.document; 124 | save_link = doc.createElementNS("http://www.w3.org/1999/xhtml", "a"); 125 | save_link.href = object_url; 126 | save_link['download'] = name; 127 | var event = doc.createEvent("MouseEvents"); 128 | event.initMouseEvent( 129 | "click", true, false, view, 0, 0, 0, 0, 0 130 | , false, false, false, false, 0, null 131 | ); 132 | save_link.dispatchEvent(event); 133 | filesaver.readyState = filesaver.DONE; 134 | dispatch_all(); 135 | return; 136 | } 137 | // Object and web filesystem URLs have a problem saving in Google Chrome when 138 | // viewed in a tab, so I force save with application/octet-stream 139 | // http://code.google.com/p/chromium/issues/detail?id=91158 140 | if (view.chrome && type && type !== force_saveable_type) { 141 | slice = blob.slice || blob.webkitSlice; 142 | blob = slice.call(blob, 0, blob.size, force_saveable_type); 143 | blob_changed = true; 144 | } 145 | // Since I can't be sure that the guessed media type will trigger a download 146 | // in WebKit, I append .download to the filename. 147 | // https://bugs.webkit.org/show_bug.cgi?id=65440 148 | if (webkit_req_fs && name !== "download") { 149 | name += ".download"; 150 | } 151 | if (type === force_saveable_type || webkit_req_fs) { 152 | target_view = view; 153 | } 154 | if (!req_fs) { 155 | fs_error(); 156 | return; 157 | } 158 | fs_min_size += blob.size; 159 | req_fs(view.TEMPORARY, fs_min_size, abortable(function(fs) { 160 | fs.root.getDirectory("saved", create_if_not_found, abortable(function(dir) { 161 | var save = function() { 162 | dir.getFile(name, create_if_not_found, abortable(function(file) { 163 | file.createWriter(abortable(function(writer) { 164 | writer.onwriteend = function(event) { 165 | target_view.location.href = file.toURL(); 166 | deletion_queue.push(file); 167 | filesaver.readyState = filesaver.DONE; 168 | dispatch(filesaver, "writeend", event); 169 | }; 170 | writer.onerror = function() { 171 | var error = writer.error; 172 | if (error.code !== error.ABORT_ERR) { 173 | fs_error(); 174 | } 175 | }; 176 | "writestart progress write abort".split(" ").forEach(function(event) { 177 | writer["on" + event] = filesaver["on" + event]; 178 | }); 179 | writer.write(blob); 180 | filesaver.abort = function() { 181 | writer.abort(); 182 | filesaver.readyState = filesaver.DONE; 183 | }; 184 | filesaver.readyState = filesaver.WRITING; 185 | }), fs_error); 186 | }), fs_error); 187 | }; 188 | dir.getFile(name, {create: false}, abortable(function(file) { 189 | // delete file if it already exists 190 | file.remove(); 191 | save(); 192 | }), abortable(function(ex) { 193 | if (ex.code === ex.NOT_FOUND_ERR) { 194 | save(); 195 | } else { 196 | fs_error(); 197 | } 198 | })); 199 | }), fs_error); 200 | }), fs_error); 201 | } 202 | , FS_proto = FileSaver.prototype 203 | , saveAs = function(blob, name) { 204 | return new FileSaver(blob, name); 205 | } 206 | ; 207 | FS_proto.abort = function() { 208 | var filesaver = this; 209 | filesaver.readyState = filesaver.DONE; 210 | dispatch(filesaver, "abort"); 211 | }; 212 | FS_proto.readyState = FS_proto.INIT = 0; 213 | FS_proto.WRITING = 1; 214 | FS_proto.DONE = 2; 215 | 216 | FS_proto.error = 217 | FS_proto.onwritestart = 218 | FS_proto.onprogress = 219 | FS_proto.onwrite = 220 | FS_proto.onabort = 221 | FS_proto.onerror = 222 | FS_proto.onwriteend = 223 | null; 224 | 225 | view.addEventListener("unload", process_deletion_queue, false); 226 | return saveAs; 227 | }(this.self || this.window || this.content)); 228 | // `self` is undefined in Firefox for Android content script context 229 | // while `this` is nsIContentFrameMessageManager 230 | // with an attribute `content` that corresponds to the window 231 | 232 | window['saveAs'] = saveAs; 233 | if (typeof module !== 'undefined') module['exports'] = saveAs; 234 | -------------------------------------------------------------------------------- /src/animatedsprite.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /** 4 | * An object for storing animations. 5 | * @constructor 6 | * @param {Object} animationData Data for animation frames. Keys are animation ids. 7 | * Values are arrays containing objects specifying frames. 8 | * Each frame has two mandatory keys: 'src' for frame source and 'duration' for a duration in milliseconds. 9 | * A series of frames can also be defined by using a wildcard. The number of frames needs to be specified. 10 | * Frame numbering starts from 1 if a wildcard is used. 11 | * Duration can be set to 0 to have the frame run into infinity. 12 | * Example: 13 | * { 14 | * idle: [{src: 'idle.png', duration: 0}], 15 | * walk: [{src: 'walk1.png', duration: 50}, {src: 'walk2.png', duration: 50}] 16 | * run: [{src: 'run*.png', frames: 5}] 17 | * } 18 | * @param {Object} options Object with the following optional keys: 19 | * frameConstructor: function Constructor for single frames that takes the 20 | * frame source as a parameter. Defaults to AnimatedSprite.frameConstructor. 21 | * durationMultiplier: number Multiplier for specified frame durations. Useful if you 22 | * want to have frame times relative to fixed FPS, for example. Defaults to 1. 23 | * defaultDuration: number Default duration for a single frame. Defaults to 1. 24 | */ 25 | var AnimatedSprite = function(animationData, options) { 26 | var defaults = { 27 | frameConstructor: AnimatedSprite.frameConstructor, 28 | durationMultiplier: 1, 29 | defaultDuration: 1 30 | }; 31 | for(var key in defaults) { 32 | if (!options.hasOwnProperty(key)) { 33 | this[key] = defaults[key]; 34 | } else { 35 | this[key] = options[key]; 36 | } 37 | } 38 | // Construct animations by generating animation frames based on the sources. 39 | this.animations = {}; 40 | this.defaultAnimation = undefined; 41 | for (var key in animationData) { 42 | if (animationData.hasOwnProperty(key)) { 43 | var animation = []; 44 | var singleAnimationData = animationData[key]; 45 | var frameSrc = []; 46 | for (var i = 0; i < singleAnimationData.length; ++i) { 47 | var duration = this.defaultDuration; 48 | if (singleAnimationData[i].duration !== undefined) 49 | { 50 | duration = singleAnimationData[i].duration; 51 | } 52 | if (singleAnimationData[i].frames !== undefined) { 53 | var frameCount = singleAnimationData[i].frames; 54 | var srcTemplate = singleAnimationData[i].src; 55 | for (var j = 1; j <= frameCount; ++j) { 56 | frameSrc.push({src: srcTemplate.replace('*', j), duration: duration}); 57 | } 58 | } else { 59 | frameSrc.push({src: singleAnimationData[i].src, duration: duration}); 60 | } 61 | } 62 | for (var i = 0; i < frameSrc.length; ++i) { 63 | var frame = AnimatedSprite._getFrame(frameSrc[i].src, this.frameConstructor); 64 | animation.push({frame: frame, duration: frameSrc[i].duration * this.durationMultiplier}); 65 | } 66 | this.animations[key] = animation; 67 | if (this.defaultAnimation === undefined) { 68 | this.defaultAnimation = key; 69 | } 70 | } 71 | } 72 | }; 73 | 74 | /** 75 | * Default constructor for single frames. Set this before loading any animations. 76 | */ 77 | AnimatedSprite.frameConstructor = null; 78 | if (typeof Sprite !== 'undefined') { 79 | AnimatedSprite.frameConstructor = Sprite; 80 | } 81 | 82 | AnimatedSprite._getFrame = (function() { 83 | var frameCaches = []; 84 | 85 | return (function(src, frameConstructor) { 86 | var cachedFrames; 87 | for (var j = 0; j < frameCaches.length; ++j) { 88 | if (frameCaches[j].frameConstructor === frameConstructor) { 89 | cachedFrames = frameCaches[j].cachedFrames; 90 | break; 91 | } 92 | } 93 | if (cachedFrames === undefined) { 94 | cachedFrames = {}; 95 | frameCaches.push({frameConstructor: frameConstructor, cachedFrames: cachedFrames}); 96 | } 97 | if (!cachedFrames.hasOwnProperty('_' + src)) { 98 | var frame = new frameConstructor(src); 99 | cachedFrames['_' + src] = frame; 100 | } 101 | return cachedFrames['_' + src]; 102 | }); 103 | })(); 104 | 105 | /** 106 | * An object that stores the current state of an animated sprite. 107 | * @constructor 108 | * @param {AnimatedSprite} animatedSprite The animated sprite to use. 109 | * @param {function=} finishedFrameCallback A callback to execute when an animation has finished. Can be used to 110 | * switch to a different animation, for example. Takes the finished animation key as a parameter. 111 | */ 112 | var AnimatedSpriteInstance = function(animatedSprite, finishedAnimationCallback) { 113 | this.animatedSprite = animatedSprite; 114 | this.finishedAnimationCallback = finishedAnimationCallback; 115 | this.setAnimation(this.animatedSprite.defaultAnimation); 116 | var frame = this.animatedSprite.animations[this.animationKey][this.frame].frame; 117 | 118 | // Add draw functions from Sprite if they are defined 119 | // A bit slow way to do this but needed to make the animation classes more generic. 120 | if (frame.implementsGameutilsSprite) 121 | { 122 | var that = this; 123 | this.draw = function(ctx, leftX, topY) { 124 | var frame = that.getCurrentFrame(); 125 | frame.draw(ctx, leftX, topY); 126 | }; 127 | this.drawRotated = function(ctx, centerX, centerY, angleRadians, /* optional */ scale) { 128 | var frame = that.getCurrentFrame(); 129 | frame.drawRotated(ctx, centerX, centerY, angleRadians, /* optional */ scale); 130 | }; 131 | this.drawRotatedNonUniform = function(ctx, centerX, centerY, angleRadians, scaleX, scaleY) { 132 | var frame = that.getCurrentFrame(); 133 | frame.drawRotatedNonUniform(ctx, centerX, centerY, angleRadians, scaleX, scaleY); 134 | }; 135 | } 136 | }; 137 | 138 | /** 139 | * Start playing an animation. 140 | * @param {string} animationKey The animation id in the AnimatedSprite. 141 | */ 142 | AnimatedSpriteInstance.prototype.setAnimation = function(animationKey) { 143 | this.animationKey = animationKey; 144 | this.frame = 0; 145 | this.framePos = 0; 146 | }; 147 | 148 | /** 149 | * Update the current animation frame. 150 | * @param {number} deltaTime Time that has passed since the last update. 151 | */ 152 | AnimatedSpriteInstance.prototype.update = function(deltaTime) { 153 | this._scrubInternal(deltaTime, this.finishedAnimationCallback); 154 | }; 155 | 156 | /** 157 | * Scrub the animation backwards or forwards. 158 | * @param {number} deltaTime Amount to scrub by. 159 | */ 160 | AnimatedSpriteInstance.prototype.scrub = function(deltaTime) { 161 | this._scrubInternal(deltaTime); 162 | }; 163 | 164 | AnimatedSpriteInstance.prototype._scrubInternal = function(deltaTime, finishCallback) { 165 | var currentAnimation = this.animatedSprite.animations[this.animationKey]; 166 | if (currentAnimation[this.frame].duration > 0) { 167 | this.framePos += deltaTime * 1000; 168 | while (this.framePos > currentAnimation[this.frame].duration) { 169 | this.framePos -= currentAnimation[this.frame].duration; 170 | ++this.frame; 171 | if (this.frame >= currentAnimation.length) { 172 | this.frame = 0; 173 | if (finishCallback !== undefined) { 174 | finishCallback(this.animationKey); 175 | } 176 | } 177 | if (currentAnimation[this.frame].duration <= 0) { 178 | this.framePos = 0.0; 179 | return; 180 | } 181 | } 182 | while (this.framePos < 0) { 183 | --this.frame; 184 | if (this.frame < 0) { 185 | this.frame = currentAnimation.length - 1; 186 | } 187 | this.framePos += currentAnimation[this.frame].duration; 188 | if (currentAnimation[this.frame].duration <= 0) { 189 | this.framePos = 0.0; 190 | return; 191 | } 192 | } 193 | } 194 | }; 195 | 196 | /** 197 | * @return {string} The current animation key. 198 | */ 199 | AnimatedSpriteInstance.prototype.getCurrentAnimation = function() { 200 | return this.animationKey; 201 | }; 202 | 203 | /** 204 | * @return {Object} The current frame of the animation. 205 | */ 206 | AnimatedSpriteInstance.prototype.getCurrentFrame = function() { 207 | return this.animatedSprite.animations[this.animationKey][this.frame].frame; 208 | }; 209 | -------------------------------------------------------------------------------- /src/lib/SkyShader.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @author zz85 / https://github.com/zz85 3 | * 4 | * Based on "A Practical Analytic Model for Daylight" 5 | * aka The Preetham Model, the de facto standard analytic skydome model 6 | * http://www.cs.utah.edu/~shirley/papers/sunsky/sunsky.pdf 7 | * 8 | * First implemented by Simon Wallner 9 | * http://www.simonwallner.at/projects/atmospheric-scattering 10 | * 11 | * Improved by Martin Upitis 12 | * http://blenderartists.org/forum/showthread.php?245954-preethams-sky-impementation-HDR 13 | * 14 | * Three.js integration by zz85 http://twitter.com/blurspline 15 | */ 16 | 17 | THREE.ShaderLib[ 'sky' ] = { 18 | 19 | uniforms: { 20 | 21 | luminance: { type: "f", value: 1 }, 22 | turbidity: { type: "f", value: 2 }, 23 | reileigh: { type: "f", value: 1 }, 24 | mieCoefficient: { type: "f", value: 0.005 }, 25 | mieDirectionalG: { type: "f", value: 0.8 }, 26 | sunPosition: { type: "v3", value: new THREE.Vector3() } 27 | 28 | }, 29 | 30 | vertexShader: [ 31 | 32 | "varying vec3 vWorldPosition;", 33 | 34 | "void main() {", 35 | 36 | "vec4 worldPosition = modelMatrix * vec4( position, 1.0 );", 37 | "vWorldPosition = worldPosition.xyz;", 38 | 39 | "gl_Position = projectionMatrix * modelViewMatrix * vec4( position, 1.0 );", 40 | 41 | "}", 42 | 43 | ].join( "\n" ), 44 | 45 | fragmentShader: [ 46 | 47 | "uniform sampler2D skySampler;", 48 | "uniform vec3 sunPosition;", 49 | "varying vec3 vWorldPosition;", 50 | 51 | "vec3 cameraPos = vec3(0., 0., 0.);", 52 | "// uniform sampler2D sDiffuse;", 53 | "// const float turbidity = 10.0; //", 54 | "// const float reileigh = 2.; //", 55 | "// const float luminance = 1.0; //", 56 | "// const float mieCoefficient = 0.005;", 57 | "// const float mieDirectionalG = 0.8;", 58 | 59 | "uniform float luminance;", 60 | "uniform float turbidity;", 61 | "uniform float reileigh;", 62 | "uniform float mieCoefficient;", 63 | "uniform float mieDirectionalG;", 64 | 65 | "// constants for atmospheric scattering", 66 | "const float e = 2.71828182845904523536028747135266249775724709369995957;", 67 | "const float pi = 3.141592653589793238462643383279502884197169;", 68 | 69 | "const float n = 1.0003; // refractive index of air", 70 | "const float N = 2.545E25; // number of molecules per unit volume for air at", 71 | "// 288.15K and 1013mb (sea level -45 celsius)", 72 | "const float pn = 0.035; // depolatization factor for standard air", 73 | 74 | "// wavelength of used primaries, according to preetham", 75 | "const vec3 lambda = vec3(680E-9, 550E-9, 450E-9);", 76 | 77 | "// mie stuff", 78 | "// K coefficient for the primaries", 79 | "const vec3 K = vec3(0.686, 0.678, 0.666);", 80 | "const float v = 4.0;", 81 | 82 | "// optical length at zenith for molecules", 83 | "const float rayleighZenithLength = 8.4E3;", 84 | "const float mieZenithLength = 1.25E3;", 85 | "const vec3 up = vec3(0.0, 1.0, 0.0);", 86 | 87 | "const float EE = 1000.0;", 88 | "const float sunAngularDiameterCos = 0.999956676946448443553574619906976478926848692873900859324;", 89 | "// 66 arc seconds -> degrees, and the cosine of that", 90 | 91 | "// earth shadow hack", 92 | "const float cutoffAngle = pi/1.95;", 93 | "const float steepness = 1.5;", 94 | 95 | 96 | "vec3 totalRayleigh(vec3 lambda)", 97 | "{", 98 | "return (8.0 * pow(pi, 3.0) * pow(pow(n, 2.0) - 1.0, 2.0) * (6.0 + 3.0 * pn)) / (3.0 * N * pow(lambda, vec3(4.0)) * (6.0 - 7.0 * pn));", 99 | "}", 100 | 101 | // see http://blenderartists.org/forum/showthread.php?321110-Shaders-and-Skybox-madness 102 | "// A simplied version of the total Reayleigh scattering to works on browsers that use ANGLE", 103 | "vec3 simplifiedRayleigh()", 104 | "{", 105 | "return 0.0005 / vec3(94, 40, 18);", 106 | // return 0.00054532832366 / (3.0 * 2.545E25 * pow(vec3(680E-9, 550E-9, 450E-9), vec3(4.0)) * 6.245); 107 | "}", 108 | 109 | "float rayleighPhase(float cosTheta)", 110 | "{ ", 111 | "return (3.0 / (16.0*pi)) * (1.0 + pow(cosTheta, 2.0));", 112 | "// return (1.0 / (3.0*pi)) * (1.0 + pow(cosTheta, 2.0));", 113 | "// return (3.0 / 4.0) * (1.0 + pow(cosTheta, 2.0));", 114 | "}", 115 | 116 | "vec3 totalMie(vec3 lambda, vec3 K, float T)", 117 | "{", 118 | "float c = (0.2 * T ) * 10E-18;", 119 | "return 0.434 * c * pi * pow((2.0 * pi) / lambda, vec3(v - 2.0)) * K;", 120 | "}", 121 | 122 | "float hgPhase(float cosTheta, float g)", 123 | "{", 124 | "return (1.0 / (4.0*pi)) * ((1.0 - pow(g, 2.0)) / pow(1.0 - 2.0*g*cosTheta + pow(g, 2.0), 1.5));", 125 | "}", 126 | 127 | "float sunIntensity(float zenithAngleCos)", 128 | "{", 129 | "return EE * max(0.0, 1.0 - exp(-((cutoffAngle - acos(zenithAngleCos))/steepness)));", 130 | "}", 131 | 132 | "// float logLuminance(vec3 c)", 133 | "// {", 134 | "// return log(c.r * 0.2126 + c.g * 0.7152 + c.b * 0.0722);", 135 | "// }", 136 | 137 | "// Filmic ToneMapping http://filmicgames.com/archives/75", 138 | "float A = 0.15;", 139 | "float B = 0.50;", 140 | "float C = 0.10;", 141 | "float D = 0.20;", 142 | "float E = 0.02;", 143 | "float F = 0.30;", 144 | "float W = 1000.0;", 145 | 146 | "vec3 Uncharted2Tonemap(vec3 x)", 147 | "{", 148 | "return ((x*(A*x+C*B)+D*E)/(x*(A*x+B)+D*F))-E/F;", 149 | "}", 150 | 151 | 152 | "void main() ", 153 | "{", 154 | "float sunfade = 1.0-clamp(1.0-exp((sunPosition.y/450000.0)),0.0,1.0);", 155 | 156 | "// luminance = 1.0 ;// vWorldPosition.y / 450000. + 0.5; //sunPosition.y / 450000. * 1. + 0.5;", 157 | 158 | "// gl_FragColor = vec4(sunfade, sunfade, sunfade, 1.0);", 159 | 160 | "float reileighCoefficient = reileigh - (1.0* (1.0-sunfade));", 161 | 162 | "vec3 sunDirection = normalize(sunPosition);", 163 | 164 | "float sunE = sunIntensity(dot(sunDirection, up));", 165 | 166 | "// extinction (absorbtion + out scattering) ", 167 | "// rayleigh coefficients", 168 | 169 | // "vec3 betaR = totalRayleigh(lambda) * reileighCoefficient;", 170 | "vec3 betaR = simplifiedRayleigh() * reileighCoefficient;", 171 | 172 | "// mie coefficients", 173 | "vec3 betaM = totalMie(lambda, K, turbidity) * mieCoefficient;", 174 | 175 | "// optical length", 176 | "// cutoff angle at 90 to avoid singularity in next formula.", 177 | "float zenithAngle = acos(max(0.0, dot(up, normalize(vWorldPosition - cameraPos))));", 178 | "float sR = rayleighZenithLength / (cos(zenithAngle) + 0.15 * pow(93.885 - ((zenithAngle * 180.0) / pi), -1.253));", 179 | "float sM = mieZenithLength / (cos(zenithAngle) + 0.15 * pow(93.885 - ((zenithAngle * 180.0) / pi), -1.253));", 180 | 181 | 182 | 183 | "// combined extinction factor ", 184 | "vec3 Fex = exp(-(betaR * sR + betaM * sM));", 185 | 186 | "// in scattering", 187 | "float cosTheta = dot(normalize(vWorldPosition - cameraPos), sunDirection);", 188 | 189 | "float rPhase = rayleighPhase(cosTheta*0.5+0.5);", 190 | "vec3 betaRTheta = betaR * rPhase;", 191 | 192 | "float mPhase = hgPhase(cosTheta, mieDirectionalG);", 193 | "vec3 betaMTheta = betaM * mPhase;", 194 | 195 | 196 | "vec3 Lin = pow(sunE * ((betaRTheta + betaMTheta) / (betaR + betaM)) * (1.0 - Fex),vec3(1.5));", 197 | "Lin *= mix(vec3(1.0),pow(sunE * ((betaRTheta + betaMTheta) / (betaR + betaM)) * Fex,vec3(1.0/2.0)),clamp(pow(1.0-dot(up, sunDirection),5.0),0.0,1.0));", 198 | 199 | "//nightsky", 200 | "vec3 direction = normalize(vWorldPosition - cameraPos);", 201 | "float theta = acos(direction.y); // elevation --> y-axis, [-pi/2, pi/2]", 202 | "float phi = atan(direction.z, direction.x); // azimuth --> x-axis [-pi/2, pi/2]", 203 | "vec2 uv = vec2(phi, theta) / vec2(2.0*pi, pi) + vec2(0.5, 0.0);", 204 | "// vec3 L0 = texture2D(skySampler, uv).rgb+0.1 * Fex;", 205 | "vec3 L0 = vec3(0.1) * Fex;", 206 | 207 | "// composition + solar disc", 208 | "//if (cosTheta > sunAngularDiameterCos)", 209 | "float sundisk = smoothstep(sunAngularDiameterCos,sunAngularDiameterCos+0.00002,cosTheta);", 210 | "// if (normalize(vWorldPosition - cameraPos).y>0.0)", 211 | "L0 += (sunE * 19000.0 * Fex)*sundisk;", 212 | 213 | 214 | "vec3 whiteScale = 1.0/Uncharted2Tonemap(vec3(W));", 215 | 216 | "vec3 texColor = (Lin+L0); ", 217 | "texColor *= 0.04 ;", 218 | "texColor += vec3(0.0,0.001,0.0025)*0.3;", 219 | 220 | "float g_fMaxLuminance = 1.0;", 221 | "float fLumScaled = 0.1 / luminance; ", 222 | "float fLumCompressed = (fLumScaled * (1.0 + (fLumScaled / (g_fMaxLuminance * g_fMaxLuminance)))) / (1.0 + fLumScaled); ", 223 | 224 | "float ExposureBias = fLumCompressed;", 225 | 226 | "vec3 curr = Uncharted2Tonemap((log2(2.0/pow(luminance,4.0)))*texColor);", 227 | "vec3 color = curr*whiteScale;", 228 | 229 | "vec3 retColor = pow(color,vec3(1.0/(1.2+(1.2*sunfade))));", 230 | 231 | 232 | "gl_FragColor.rgb = retColor;", 233 | 234 | "gl_FragColor.a = 1.0;", 235 | "}", 236 | 237 | ].join( "\n" ) 238 | 239 | }; 240 | 241 | THREE.Sky = function () { 242 | 243 | var skyShader = THREE.ShaderLib[ "sky" ]; 244 | var skyUniforms = THREE.UniformsUtils.clone( skyShader.uniforms ); 245 | 246 | var skyMat = new THREE.ShaderMaterial( { 247 | fragmentShader: skyShader.fragmentShader, 248 | vertexShader: skyShader.vertexShader, 249 | uniforms: skyUniforms, 250 | side: THREE.BackSide 251 | } ); 252 | 253 | var skyGeo = new THREE.SphereBufferGeometry( 450000, 32, 15 ); 254 | var skyMesh = new THREE.Mesh( skyGeo, skyMat ); 255 | 256 | 257 | // Expose variables 258 | this.mesh = skyMesh; 259 | this.uniforms = skyUniforms; 260 | 261 | }; 262 | -------------------------------------------------------------------------------- /src/game.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var _parsedSpec; 4 | 5 | var parseSpec = function(spec) { 6 | // TODO: Get rid of this eval(). 7 | eval("_parsedSpec = " + spec + ";"); 8 | return _parsedSpec; 9 | }; 10 | 11 | var Game = function(resizer, renderer, loadingBar) { 12 | this.resizer = resizer; 13 | this.renderer = renderer; 14 | this.loadingBar = loadingBar; 15 | this.initializedAfterLoad = false; 16 | this.renderer.setClearColor( 0x888888, 1); 17 | 18 | this.time = 0; 19 | 20 | var numPlayers = 1; 21 | this.input = new InputMapper(this, numPlayers); 22 | //this.input.addListener(Gamepad.BUTTONS.UP_OR_ANALOG_UP, ['up', 'w'], this.upPress/*, this.upRelease*/); 23 | //this.input.addListener(Gamepad.BUTTONS.DOWN_OR_ANALOG_DOWN, ['down', 's'], this.downPress/*, this.downRelease*/); 24 | 25 | if (DEV_MODE) { 26 | this.input.addListener(undefined, ['a'], this.aPress); 27 | this.input.addListener(undefined, ['z'], this.zPress); 28 | this.input.addListener(undefined, ['x'], this.xPress); 29 | this.input.addListener(undefined, ['c'], this.cPress); 30 | this.input.addListener(undefined, ['o'], this.oPress); 31 | this.input.addListener(undefined, ['l'], this.lPress); 32 | this.input.addListener(undefined, ['k'], this.kPress); 33 | this.input.addListener(undefined, ['m'], this.mPress); 34 | this.input.addListener(undefined, ['p'], this.pPress); 35 | this.input.addListener(undefined, ['q'], this.qPress); 36 | this.input.addListener(undefined, ['ctrl+s'], this.ctrlsPress); 37 | this.input.addListener(undefined, ['ctrl+n'], this.nextLevelCycle); 38 | } 39 | 40 | this.levelNumber = 0; 41 | 42 | this.downIndex = -1; 43 | }; 44 | 45 | Game.prototype.loadedInit = function() { 46 | if (!this.initializedAfterLoad) { 47 | var levelId = levelData.levelSequence[this.levelNumber]; 48 | this.loadLevel(levelId); 49 | Game.music.playSingular(true); 50 | this.initializedAfterLoad = true; 51 | } 52 | }; 53 | 54 | Game.music = new GJS.Audio('laser_music'); 55 | 56 | Game.prototype.zPress = function() { 57 | this.level.editor.zPress(); 58 | }; 59 | Game.prototype.aPress = function() { 60 | this.level.editor.aPress(); 61 | }; 62 | Game.prototype.oPress = function() { 63 | this.level.editor.oPress(); 64 | }; 65 | Game.prototype.kPress = function() { 66 | this.level.editor.kPress(); 67 | }; 68 | Game.prototype.mPress = function() { 69 | this.level.editor.mPress(); 70 | }; 71 | Game.prototype.pPress = function() { 72 | this.level.editor.pPress(); 73 | }; 74 | Game.prototype.cPress = function() { 75 | this.level.editor.cPress(); 76 | }; 77 | Game.prototype.xPress = function() { 78 | this.level.editor.xPress(); 79 | }; 80 | Game.prototype.qPress = function() { 81 | this.level.editor.qPress(); 82 | }; 83 | Game.prototype.lPress = function() { 84 | this.level.editor.lPress(); 85 | }; 86 | Game.prototype.ctrlsPress = function() { 87 | this.level.editor.ctrlsPress(); 88 | }; 89 | 90 | Game.prototype.canvasMove = function(event) { 91 | if (this.level) { 92 | this.level.setCursorPosition(this.positionAsVec3(event)); 93 | } 94 | }; 95 | 96 | Game.prototype.canvasPress = function(event) { 97 | if (this.level && this.downIndex === -1) { 98 | this.level.setCursorPosition(this.positionAsVec3(event)); 99 | this.level.mouseDown(); 100 | this.downIndex = event.index; 101 | } 102 | }; 103 | 104 | Game.prototype.canvasRelease = function(event) { 105 | if (this.level && this.downIndex === event.index) { 106 | this.level.setCursorPosition(this.positionAsVec3(event)); 107 | this.level.mouseUp(); 108 | this.downIndex = -1; 109 | } 110 | }; 111 | 112 | Game.prototype.positionAsVec3 = function(event) { 113 | return new THREE.Vector3(event.currentPosition.x, event.currentPosition.y, 0); 114 | }; 115 | 116 | Game.prototype.setCameraAspect = function(aspect) { 117 | if (this.level) { 118 | this.level.camera.aspect = aspect; 119 | this.level.camera.updateProjectionMatrix(); 120 | } 121 | }; 122 | 123 | Game.prototype.render = function() { 124 | if (this.level) { 125 | var fadeOpacity = 0.0; // Opacity of black fader over the game (implemented by fading the canvas) 126 | if (this.level.state.id === Level.State.INTRO) { 127 | fadeOpacity = 1.0 - this.level.state.time; 128 | } else if (this.level.state.id === Level.State.SUCCESS && this.level.successState.id === Level.SuccessState.FADE_OUT) { 129 | fadeOpacity = this.level.successState.time; 130 | } 131 | this.resizer.canvas.style.opacity = mathUtil.clamp(0.0, 1.0, 1.0 - fadeOpacity); 132 | this.level.render(this.renderer); 133 | } 134 | if (Game.parameters.get('postLD')) { 135 | return this.renderer; 136 | } 137 | }; 138 | 139 | Game.prototype.update = function(deltaTime) { 140 | this.time += deltaTime; 141 | this.input.update(); 142 | 143 | if (this.level) { 144 | this.level.update(deltaTime); 145 | if (this.level.state.id === Level.State.SUCCESS && 146 | this.level.successState.id === Level.SuccessState.FADE_OUT && 147 | this.level.successState.time > 1.0 && 148 | !this.level.editor) 149 | { 150 | this.nextLevel(); 151 | } 152 | } 153 | 154 | GJS.Audio.muteAll(Game.parameters.get('muteAudio')); 155 | 156 | if (this.loadingBar.finished() && !this.initializedAfterLoad) { 157 | this.loadedInit(); 158 | this.initializedAfterLoad = true; 159 | } 160 | }; 161 | 162 | Game.prototype.loadLevel = function(id) { 163 | var options = { 164 | cameraAspect: this.resizer.canvas.width / this.resizer.canvas.height 165 | }; 166 | this.level = Level.fromSpec(options, levelData.data[id]); 167 | }; 168 | 169 | Game.prototype.nextLevel = function() { 170 | ++this.levelNumber; 171 | if (this.levelNumber < levelData.levelSequence.length) { 172 | this.loadLevel(levelData.levelSequence[this.levelNumber]); 173 | } 174 | }; 175 | 176 | // For dev mode 177 | Game.prototype.nextLevelCycle = function() { 178 | if (this.levelNumber >= levelData.levelSequence.length - 1) { 179 | this.levelNumber = -1; 180 | } 181 | this.nextLevel(); 182 | }; 183 | 184 | // Parameters added here can be tuned at run time when in developer mode 185 | Game.parameters = new GameParameters({ 186 | 'roundedMovement': {initial: true}, 187 | 'playBuildingMoveSound': {initial: false}, 188 | 'buildingSpringStrength': {initial: 0.1, min: 0.01, max: 0.2}, 189 | 'buildingSpringDamping': {initial: 0.77, min: 0.5, max: 0.95}, 190 | 'muteAudio': {initial: false}, 191 | 'postLD': {initial: false} // To activate post-LD improvements 192 | }); 193 | 194 | var DEV_MODE = querystringUtil.get('devMode') !== undefined; 195 | var POST_COMPO = querystringUtil.get('postCompo') !== undefined; 196 | 197 | window['start'] = function() { 198 | var DEBUG_MAIN_LOOP = DEV_MODE && true; // Set to true to allow fast-forwarding main loop with 'f' 199 | Game.parameters.set('muteAudio', (DEV_MODE && true)); // Set to true if sounds annoy developers 200 | Game.parameters.set('postLD', POST_COMPO); 201 | if (DEV_MODE) { 202 | Game.parameters.initGUI(); 203 | } 204 | 205 | var renderer = new THREE.WebGLRenderer({ antialias: true }); 206 | renderer.shadowMap.enabled = true; 207 | renderer.shadowMap.type = THREE.PCFShadowMap; 208 | 209 | var game; 210 | 211 | var canvasWrapper = document.createElement('div'); 212 | canvasWrapper.appendChild(renderer.domElement); 213 | 214 | var postLD = Game.parameters.get('postLD'); 215 | 216 | if (postLD) { 217 | GJS.commonUI.createUI({ 218 | parent: canvasWrapper, 219 | fullscreenElement: document.body, 220 | twitterAccount: 'Oletus', 221 | fillStyle: '#ffffff', 222 | opacity: 0.2, 223 | scale: 0.8 224 | }); 225 | } 226 | 227 | var resizer = new GJS.CanvasResizer({ 228 | mode: GJS.CanvasResizer.Mode.FIXED_ASPECT_RATIO, 229 | canvas: renderer.domElement, 230 | wrapperElement: canvasWrapper, 231 | width: 16, 232 | height: 9, 233 | setCanvasSizeCallback: function(width, height) { 234 | renderer.setSize(width, height); 235 | if (game !== undefined) { 236 | game.setCameraAspect(width / height); 237 | } 238 | } 239 | }); 240 | 241 | var loadingBar = new GJS.LoadingBar(); 242 | game = new Game(resizer, renderer, loadingBar); 243 | var eventListener = resizer.createPointerEventListener(game, false, 244 | GJS.CanvasResizer.EventCoordinateSystem.WEBGL_NORMALIZED_DEVICE_COORDINATES); 245 | resizer.canvas.addEventListener('mousemove', eventListener); 246 | resizer.canvas.addEventListener('mousedown', eventListener); 247 | resizer.canvas.addEventListener('mouseup', eventListener); 248 | resizer.canvas.addEventListener('mouseout', eventListener); 249 | if (postLD) { 250 | resizer.canvas.addEventListener('touchmove', eventListener); 251 | resizer.canvas.addEventListener('touchstart', eventListener); 252 | resizer.canvas.addEventListener('touchend', eventListener); 253 | resizer.canvas.addEventListener('touchcancel', eventListener); 254 | } 255 | 256 | startMainLoop([resizer, game, loadingBar, resizer.pixelator()], {debugMode: DEBUG_MAIN_LOOP}); 257 | }; 258 | -------------------------------------------------------------------------------- /assets/models/sidewalk.json: -------------------------------------------------------------------------------- 1 | { 2 | "faces":[35,0,1,2,3,0,0,0,0,0,35,4,5,6,7,0,1,1,1,1,35,8,9,10,11,1,2,2,2,2,35,12,13,14,15,2,2,2,2,2,35,16,17,18,19,1,0,0,0,0,35,20,21,22,23,1,3,3,3,3,35,24,25,26,27,0,1,1,1,1,35,28,29,30,31,1,1,1,1,1,35,32,33,34,35,2,0,0,0,0,35,36,37,38,39,0,1,1,1,1,35,40,41,42,43,2,1,1,1,1,35,44,45,46,47,2,1,1,1,1,35,48,49,50,51,2,4,5,4,4,35,52,53,54,55,2,4,5,4,4,35,56,57,58,59,2,4,4,4,4,35,60,61,62,63,1,6,6,6,6,35,64,65,66,67,1,7,7,7,7,35,68,69,70,71,1,8,8,8,8,35,72,73,74,75,1,9,9,9,9,35,76,77,78,79,2,1,1,1,1,35,80,81,82,83,1,10,10,10,10,35,84,85,86,87,1,11,11,11,11,35,88,89,90,91,2,3,3,3,3,35,92,93,94,95,1,4,4,5,4,35,96,97,98,99,1,12,12,12,12,35,100,101,102,103,1,13,13,13,13,35,104,105,106,107,2,5,4,4,4,35,108,109,110,111,2,4,4,4,4,35,112,113,114,115,1,4,4,5,5,35,116,117,118,119,1,4,4,4,4,35,120,121,122,123,1,5,4,4,4,35,124,125,126,127,1,4,4,5,4,35,128,129,130,131,1,4,4,4,4,35,132,133,134,135,1,4,4,4,4,35,136,137,138,139,1,4,4,4,5,35,140,141,142,143,1,4,4,4,5,35,144,145,146,147,1,4,4,5,4,35,148,149,150,151,1,4,4,4,5,35,152,153,154,155,1,4,4,5,4,35,156,157,158,159,2,4,4,4,4,35,160,161,162,163,2,4,5,5,4,35,164,165,166,167,2,4,5,4,4,35,168,169,170,171,2,3,3,3,3,35,172,173,174,175,2,3,3,3,3,35,176,177,178,179,0,3,3,3,3,35,180,181,182,183,0,3,3,3,3,35,184,185,186,187,2,4,4,4,4,35,188,189,190,191,2,4,4,4,4,35,192,193,194,195,2,2,2,2,2,35,196,197,198,199,2,2,2,2,2,35,200,201,202,203,0,2,2,2,2,35,204,205,206,207,0,2,2,2,2,35,208,209,210,211,2,4,4,5,4,35,212,213,214,215,2,4,4,4,4,35,216,217,218,219,2,0,0,0,0,35,220,221,222,223,2,0,0,0,0,35,224,225,226,227,0,0,0,0,0,35,228,229,230,231,0,0,0,0,0,35,232,233,234,235,0,2,2,2,2,35,236,237,238,239,0,3,3,3,3], 3 | "normals":[0,0,1,-1,0,0,1,0,0,0,0,-1,0,1,0,0,0.999969,0,0.499985,0,0.866024,-0.866024,0,0.499985,-0.499985,0,0.866024,0.866024,0,0.499985,-0.499985,0,-0.866024,-0.866024,0,-0.499985,0.866024,0,-0.499985,0.499985,0,-0.866024], 4 | "vertices":[0.237535,0.392912,-0.554249,0.237535,-0.392912,-0.554249,0.554249,-0.392912,-0.554249,0.554249,0.392912,-0.554249,0.554249,0.392912,0.237535,0.554249,-0.392912,0.237535,0.554249,-0.392912,0.554249,0.554249,0.392912,0.554249,0.988007,-0.195687,-0.739118,0.988007,0.452936,-0.739118,0.988006,0.452936,0.739119,0.988006,-0.195687,0.739119,-0.554249,0.392912,-0.237535,-0.554249,0.392912,-0.554249,-0.554249,0.452936,-0.554249,-0.554249,0.452936,-0.237535,0.739118,-0.195687,0.988007,0.739118,0.452936,0.988007,-0.739118,0.452936,0.988006,-0.739118,-0.195687,0.988006,-0.739118,-0.195687,-0.988007,-0.739118,0.452936,-0.988007,0.739119,0.452936,-0.988006,0.739119,-0.195687,-0.988006,0.554249,0.392912,-0.237535,0.554249,-0.392912,-0.237535,0.554249,-0.392912,0.237535,0.554249,0.392912,0.237535,-0.988006,-0.195687,-0.739118,-0.988007,-0.195687,0.739118,-0.988007,0.452936,0.739118,-0.988006,0.452936,-0.739118,0.237535,0.392912,-0.554249,0.554249,0.392912,-0.554249,0.554249,0.452936,-0.554249,0.237535,0.452936,-0.554249,0.554249,0.392912,-0.554249,0.554249,-0.392912,-0.554249,0.554249,-0.392912,-0.237535,0.554249,0.392912,-0.237535,0.554249,0.392912,0.237535,0.554249,0.452936,0.237535,0.554249,0.452936,-0.237535,0.554249,0.392912,-0.237535,0.554249,0.392912,-0.554249,0.554249,0.392912,-0.237535,0.554249,0.452936,-0.237535,0.554249,0.452936,-0.554249,-0.83715,0.452936,-0.83715,-0.906377,0.452936,-0.717246,-0.554249,0.452936,-0.237535,-0.554249,0.452936,-0.554249,-0.237535,0.452936,-0.554249,-0.717246,0.452936,-0.906377,-0.83715,0.452936,-0.83715,-0.554249,0.452936,-0.554249,-0.717246,0.452936,-0.906377,-0.237535,0.452936,-0.554249,0.237535,0.452936,-0.554249,0.717246,0.452936,-0.906377,0.896907,0.452936,0.896908,0.739118,0.452936,0.988007,0.739118,-0.195687,0.988007,0.896907,-0.195687,0.896908,-0.988007,-0.195687,0.739118,-0.896907,-0.195687,0.896907,-0.896907,0.452936,0.896907,-0.988007,0.452936,0.739118,-0.896907,-0.195687,0.896907,-0.739118,-0.195687,0.988006,-0.739118,0.452936,0.988006,-0.896907,0.452936,0.896907,0.988006,0.452936,0.739119,0.896907,0.452936,0.896908,0.896907,-0.195687,0.896908,0.988006,-0.195687,0.739119,0.554249,0.392912,0.237535,0.554249,0.392912,0.554249,0.554249,0.452936,0.554249,0.554249,0.452936,0.237535,-0.739118,0.452936,-0.988007,-0.739118,-0.195687,-0.988007,-0.896907,-0.195687,-0.896907,-0.896907,0.452936,-0.896907,-0.988006,0.452936,-0.739118,-0.896907,0.452936,-0.896907,-0.896907,-0.195687,-0.896907,-0.988006,-0.195687,-0.739118,-0.237535,0.392912,0.554249,-0.554249,0.392912,0.554249,-0.554249,0.452936,0.554249,-0.237535,0.452936,0.554249,-0.83715,0.452936,0.83715,-0.906377,0.452936,0.717245,-0.988007,0.452936,0.739118,-0.896907,0.452936,0.896907,0.988007,-0.195687,-0.739118,0.896907,-0.195687,-0.896907,0.896907,0.452936,-0.896907,0.988007,0.452936,-0.739118,0.739119,-0.195687,-0.988006,0.739119,0.452936,-0.988006,0.896907,0.452936,-0.896907,0.896907,-0.195687,-0.896907,0.554249,0.452936,-0.554249,0.837151,0.452936,-0.83715,0.717246,0.452936,-0.906377,0.237535,0.452936,-0.554249,0.554249,0.452936,-0.237535,0.906377,0.452936,-0.717245,0.837151,0.452936,-0.83715,0.554249,0.452936,-0.554249,0.717245,0.452936,0.906377,-0.717246,0.452936,0.906377,-0.739118,0.452936,0.988006,0.739118,0.452936,0.988007,0.83715,0.452936,0.837151,0.717245,0.452936,0.906377,0.739118,0.452936,0.988007,0.896907,0.452936,0.896908,0.906377,0.452936,0.717246,0.83715,0.452936,0.837151,0.896907,0.452936,0.896908,0.988006,0.452936,0.739119,-0.717246,0.452936,0.906377,-0.83715,0.452936,0.83715,-0.896907,0.452936,0.896907,-0.739118,0.452936,0.988006,0.717246,0.452936,-0.906377,0.837151,0.452936,-0.83715,0.896907,0.452936,-0.896907,0.739119,0.452936,-0.988006,-0.717246,0.452936,-0.906377,0.717246,0.452936,-0.906377,0.739119,0.452936,-0.988006,-0.739118,0.452936,-0.988007,-0.83715,0.452936,-0.83715,-0.717246,0.452936,-0.906377,-0.739118,0.452936,-0.988007,-0.896907,0.452936,-0.896907,-0.906377,0.452936,-0.717246,-0.83715,0.452936,-0.83715,-0.896907,0.452936,-0.896907,-0.988006,0.452936,-0.739118,-0.906377,0.452936,0.717245,-0.906377,0.452936,-0.717246,-0.988006,0.452936,-0.739118,-0.988007,0.452936,0.739118,0.906377,0.452936,-0.717245,0.906377,0.452936,0.717246,0.988006,0.452936,0.739119,0.988007,0.452936,-0.739118,0.837151,0.452936,-0.83715,0.906377,0.452936,-0.717245,0.988007,0.452936,-0.739118,0.896907,0.452936,-0.896907,0.906377,0.452936,0.717246,0.906377,0.452936,-0.717245,0.554249,0.452936,-0.237535,0.554249,0.452936,0.237535,0.554249,0.452936,0.554249,0.83715,0.452936,0.837151,0.906377,0.452936,0.717246,0.554249,0.452936,0.237535,0.237535,0.452936,0.554249,0.717245,0.452936,0.906377,0.83715,0.452936,0.837151,0.554249,0.452936,0.554249,0.554249,0.392912,0.554249,0.237535,0.392912,0.554249,0.237535,0.452936,0.554249,0.554249,0.452936,0.554249,-0.237535,0.392912,0.554249,-0.237535,0.452936,0.554249,0.237535,0.452936,0.554249,0.237535,0.392912,0.554249,0.554249,-0.392912,0.554249,0.237535,-0.392912,0.554249,0.237535,0.392912,0.554249,0.554249,0.392912,0.554249,0.237535,-0.392912,0.554249,-0.237535,-0.392912,0.554249,-0.237535,0.392912,0.554249,0.237535,0.392912,0.554249,-0.554249,0.452936,0.554249,-0.83715,0.452936,0.83715,-0.717246,0.452936,0.906377,-0.237535,0.452936,0.554249,-0.717246,0.452936,0.906377,0.717245,0.452936,0.906377,0.237535,0.452936,0.554249,-0.237535,0.452936,0.554249,-0.554249,0.392912,0.237535,-0.554249,0.452936,0.237535,-0.554249,0.452936,0.554249,-0.554249,0.392912,0.554249,-0.554249,0.392912,-0.237535,-0.554249,0.452936,-0.237535,-0.554249,0.452936,0.237535,-0.554249,0.392912,0.237535,-0.554249,-0.392912,0.554249,-0.554249,-0.392912,0.237535,-0.554249,0.392912,0.237535,-0.554249,0.392912,0.554249,-0.554249,-0.392912,0.237535,-0.554249,-0.392912,-0.237535,-0.554249,0.392912,-0.237535,-0.554249,0.392912,0.237535,-0.554249,0.452936,0.237535,-0.906377,0.452936,0.717245,-0.83715,0.452936,0.83715,-0.554249,0.452936,0.554249,-0.906377,0.452936,-0.717246,-0.906377,0.452936,0.717245,-0.554249,0.452936,0.237535,-0.554249,0.452936,-0.237535,-0.237535,0.392912,-0.554249,-0.237535,0.452936,-0.554249,-0.554249,0.452936,-0.554249,-0.554249,0.392912,-0.554249,-0.237535,0.392912,-0.554249,0.237535,0.392912,-0.554249,0.237535,0.452936,-0.554249,-0.237535,0.452936,-0.554249,-0.554249,-0.392912,-0.554249,-0.237535,-0.392912,-0.554249,-0.237535,0.392912,-0.554249,-0.554249,0.392912,-0.554249,0.237535,-0.392912,-0.554249,0.237535,0.392912,-0.554249,-0.237535,0.392912,-0.554249,-0.237535,-0.392912,-0.554249,-0.554249,0.392912,-0.237535,-0.554249,-0.392912,-0.237535,-0.554249,-0.392912,-0.554249,-0.554249,0.392912,-0.554249,-0.237535,0.392912,0.554249,-0.237535,-0.392912,0.554249,-0.554249,-0.392912,0.554249,-0.554249,0.392912,0.554249], 5 | "metadata":{ 6 | "version":3, 7 | "generator":"io_three", 8 | "materials":3, 9 | "faces":60, 10 | "normals":14, 11 | "type":"Geometry", 12 | "vertices":240, 13 | "uvs":0 14 | }, 15 | "name":"Cube.001Geometry", 16 | "materials":[{ 17 | "specularCoef":50, 18 | "shading":"phong", 19 | "colorEmissive":[0,0,0], 20 | "wireframe":false, 21 | "blending":"NormalBlending", 22 | "colorSpecular":[0.072464,0.072464,0.072464], 23 | "transparent":false, 24 | "depthWrite":true, 25 | "colorDiffuse":[0.090838,0.073655,0.050265], 26 | "DbgColor":15658734, 27 | "DbgIndex":0, 28 | "opacity":1, 29 | "depthTest":true, 30 | "visible":true, 31 | "DbgName":"Material" 32 | },{ 33 | "specularCoef":50, 34 | "shading":"phong", 35 | "colorEmissive":[0,0,0], 36 | "wireframe":false, 37 | "blending":"NormalBlending", 38 | "colorSpecular":[0.086957,0.086957,0.086957], 39 | "transparent":false, 40 | "depthWrite":true, 41 | "colorDiffuse":[0.5596,0.387152,0.349366], 42 | "DbgColor":60928, 43 | "DbgIndex":2, 44 | "opacity":1, 45 | "depthTest":true, 46 | "visible":true, 47 | "DbgName":"Material.002" 48 | },{ 49 | "specularCoef":50, 50 | "shading":"phong", 51 | "colorEmissive":[0,0,0], 52 | "wireframe":false, 53 | "blending":"NormalBlending", 54 | "colorSpecular":[0.072464,0.072464,0.072464], 55 | "transparent":false, 56 | "depthWrite":true, 57 | "colorDiffuse":[0.295692,0.295692,0.295692], 58 | "DbgColor":15597568, 59 | "DbgIndex":1, 60 | "opacity":1, 61 | "depthTest":true, 62 | "visible":true, 63 | "DbgName":"Material.001" 64 | }], 65 | "uvs":[] 66 | } -------------------------------------------------------------------------------- /src/hitbox.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright Olli Etuaho 2015. 3 | */ 4 | 5 | 'use strict'; 6 | 7 | /** 8 | * Requires Rect, Vec2 and mathUtil from util2d.js. 9 | */ 10 | 11 | /** 12 | * A class to store a hit box that can be hit tested against other hit boxes. 13 | * It's called a box but can also represent other geometry. 14 | * @constructor 15 | */ 16 | var HitBox = function() { 17 | }; 18 | 19 | /** 20 | * Possible hitbox shapes 21 | * @enum {number} 22 | */ 23 | HitBox.Shape = { 24 | VEC2: 0, 25 | RECT: 1, 26 | CIRCLE: 2, 27 | SEGMENT: 3, 28 | COMBO: 4 29 | }; 30 | 31 | /** 32 | * @param {Vec2} pos Position to set. 33 | */ 34 | HitBox.prototype.setVec2 = function(pos) { 35 | this._shape = HitBox.Shape.VEC2; 36 | this._pos = pos; 37 | }; 38 | 39 | /** 40 | * @param {Rect} rect Rect to set. 41 | */ 42 | HitBox.prototype.setRect = function(rect) { 43 | this._shape = HitBox.Shape.RECT; 44 | this.rect = rect; 45 | }; 46 | 47 | /** 48 | * @param {Vec2} center Center of the circle to set. 49 | * @param {number} radius Radius of the circle. 50 | */ 51 | HitBox.prototype.setCircle = function(center, radius) { 52 | this._shape = HitBox.Shape.CIRCLE; 53 | this._center = center; 54 | this._radius = radius; 55 | }; 56 | 57 | /** 58 | * Set the hitbox to a circle segment smaller than 180 degrees. 59 | * @param {Vec2} center Center of the circle. 60 | * @param {number} radius Radius of the circle. 61 | * @param {number} angle1 Angle of one endpoint of the segment in radians. 62 | * @param {number} angle2 Angle of another endpoint of the segment in radians. 63 | */ 64 | HitBox.prototype.setSegment = function(center, radius, angle1, angle2) { 65 | this._shape = HitBox.Shape.SEGMENT; 66 | this._center = center; 67 | this._radius = radius; 68 | this._angle1 = mathUtil.fmod(angle1, Math.PI * 2); 69 | this._angle2 = mathUtil.fmod(angle2, Math.PI * 2); 70 | if (this._angle1 > this._angle2) { 71 | var swapped = this._angle2; 72 | this._angle2 = this._angle1; 73 | this._angle1 = swapped; 74 | } 75 | if (this._angle2 - this._angle1 > Math.PI) { 76 | var swapped = this._angle2; 77 | this._angle2 = this._angle1 + Math.PI * 2; 78 | this._angle1 = swapped; 79 | } 80 | var endPoint1 = new Vec2(this._center.x + Math.cos(this._angle1) * this._radius, 81 | this._center.y + Math.sin(this._angle1) * this._radius); 82 | var endPoint2 = new Vec2(this._center.x + Math.cos(this._angle2) * this._radius, 83 | this._center.y + Math.sin(this._angle2) * this._radius); 84 | this._lineSegmentCenter = new Vec2((endPoint1.x + endPoint2.x) * 0.5, 85 | (endPoint1.y + endPoint2.y) * 0.5); 86 | 87 | // Solve bounding rectangle (not axis aligned) 88 | var towardsSegCenter = new Vec2(this._lineSegmentCenter.x - this._center.x, 89 | this._lineSegmentCenter.y - this._center.y); 90 | var dist = towardsSegCenter.length(); 91 | towardsSegCenter.normalize(); 92 | towardsSegCenter.scale(this._radius - dist); 93 | var enclosingPoint1 = new Vec2(endPoint1.x, endPoint1.y); 94 | enclosingPoint1.translate(towardsSegCenter); 95 | var enclosingPoint2 = new Vec2(endPoint2.x, endPoint2.y); 96 | enclosingPoint2.translate(towardsSegCenter); 97 | this._boundingPolygon = new Polygon([endPoint1, endPoint2, enclosingPoint2, enclosingPoint1]); 98 | }; 99 | 100 | /** 101 | * Set this hitBox to a combination (union) of multiple hitboxes. 102 | * @param {Array.} hitBoxes Collection to set. 103 | */ 104 | HitBox.prototype.setCombination = function(hitBoxes) { 105 | this._shape = HitBox.Shape.COMBO; 106 | this.hitBoxes = hitBoxes; 107 | }; 108 | 109 | /** 110 | * @param {Object} target Arbitrary object that this hitbox has hit. 111 | * @return {Object} Get payload carried by this hitbox. Could be replaced by inheriting object. 112 | */ 113 | HitBox.prototype.getPayload = function(target) { 114 | return 1; 115 | }; 116 | 117 | /** 118 | * @param {HitBox} other Hitbox to test. 119 | * @return {boolean} True if this hitbox intersects the other one. 120 | */ 121 | HitBox.prototype.intersects = function(other) { 122 | if (this._shape === HitBox.Shape.COMBO) { 123 | for (var i = 0; i < this.hitBoxes.length; ++i) { 124 | if (this.hitBoxes[i].intersects(other)) { 125 | return true; 126 | } 127 | } 128 | return false; 129 | } 130 | if (other._shape === HitBox.Shape.COMBO) { 131 | return other.intersects(this); 132 | } 133 | var that = this; 134 | if (other._shape < that._shape) { 135 | that = other; 136 | other = this; 137 | } 138 | if (that._shape === HitBox.Shape.VEC2) { 139 | if (other._shape === HitBox.Shape.VEC2) { 140 | return that._shape.x === other._shape.x && that._shape.y === other._shape.y; 141 | } else if (other._shape === HitBox.Shape.RECT) { 142 | return other.rect.containsVec2(that._pos); 143 | } else if (other._shape === HitBox.Shape.CIRCLE) { 144 | return other._center.distance(that._pos) < other._radius; 145 | } else if (other._shape === HitBox.Shape.SEGMENT) { 146 | // Test against circle 147 | var dist = other._center.distance(that._pos); 148 | if (dist > other._radius) { 149 | return false; 150 | } 151 | // Test if the point projected to the normal of the segment edge is closer to circle center than the 152 | // segment edge. 153 | var segmentCenterAngle = mathUtil.mixAngles(other._angle1, other._angle2, 0.5); 154 | var pointAngle = Math.atan2(that._pos.y - other._center.y, that._pos.x - other._center.x); 155 | pointAngle = mathUtil.angleDifference(pointAngle, segmentCenterAngle); 156 | var distAlongSegmentNormal = Math.cos(pointAngle) * dist; 157 | return distAlongSegmentNormal > other._lineSegmentCenter.distance(other._center); 158 | } /*else if (other._shape === HitBox.Shape.SECTOR) { // TODO 159 | var dist = other._center.distance(that._pos); 160 | if (dist > other._radius) { 161 | return false; 162 | } 163 | // Test against sector 164 | var pointAngle = Math.atan2(that._pos.y - other._center.y, that._pos.x - other._center.x); 165 | if (mathUtil.angleGreater(pointAngle, other._angle2) || mathUtil.angleGreater(other._angle1, pointAngle)) { 166 | return false; 167 | } 168 | }*/ 169 | } else if (that._shape === HitBox.Shape.RECT) { 170 | if (other._shape === HitBox.Shape.RECT) { 171 | return other.rect.intersectsRect(that.rect); 172 | } else if (other._shape === HitBox.Shape.CIRCLE) { 173 | return that.rect.intersectsCircle(other._center, other._radius); 174 | } else if (other._shape === HitBox.Shape.SEGMENT) { 175 | if (!that.rect.intersectsCircle(other._center, other._radius)) { 176 | return false; 177 | } 178 | return other._boundingPolygon.intersectsRect(that.rect); 179 | } 180 | } else if (that._shape === HitBox.Shape.CIRCLE) { 181 | if (other._shape === HitBox.Shape.CIRCLE) { 182 | return other._center.distance(that._center) < other._radius + that._radius; 183 | } else if (other._shape === HitBox.Shape.SEGMENT) { 184 | if (other._center.distance(that._center) >= other._radius + that._radius) { 185 | return false; 186 | } 187 | return other._boundingPolygon.intersectsCircle(that._center, that._radius); 188 | } 189 | } else if (that._shape === HitBox.Shape.SEGMENT) { 190 | if (other._shape === HitBox.Shape.SEGMENT) { 191 | if (other._center.distance(that._center) >= other._radius + that._radius) { 192 | return false; 193 | } 194 | if (!other._boundingPolygon.intersectsCircle(that._center, that._radius)) { 195 | return false; 196 | } 197 | return that._boundingPolygon.intersectsCircle(other._center, other._radius); 198 | } 199 | } 200 | }; 201 | 202 | /** 203 | * @return {Vec2} The center point of this hitBox 204 | */ 205 | HitBox.prototype.getCenter = function() { 206 | if (this._shape === HitBox.Shape.VEC2) { 207 | return this._pos; 208 | } else if (this._shape === HitBox.Shape.RECT) { 209 | return this.rect.getCenter(); 210 | } else if (this._shape === HitBox.Shape.CIRCLE) { 211 | return this._center; 212 | } else if (this._shape === HitBox.Shape.SEGMENT) { 213 | return this._lineSegmentCenter; 214 | } else if (this._shape === HitBox.Shape.COMBO) { 215 | var center = new Vec2(0, 0); 216 | for (var i = 0; i < this.hitBoxes.length; ++i) { 217 | var subCenter = this.hitBoxes[i].getCenter(); 218 | center.translate(subCenter); 219 | } 220 | center.scale(1 / this.hitBoxes.length); 221 | return center; 222 | } 223 | }; 224 | 225 | /** 226 | * Render this hitbox on a canvas for debugging. Uses current fillStyle. 227 | * @param {CanvasRenderingContext2D} ctx Canvas to draw to. 228 | * @param {number?} pointScale Scale to draw points at. Defaults to 1. 229 | */ 230 | HitBox.prototype.render = function(ctx, pointScale) { 231 | if (pointScale === undefined) { 232 | pointScale = 1; 233 | } 234 | if (this._shape === HitBox.Shape.VEC2) { 235 | var pos = this._pos; 236 | ctx.fillRect(pos.x - pointScale * 0.5, pos.y - pointScale * 0.5, pointScale, pointScale); 237 | } else if (this._shape === HitBox.Shape.RECT) { 238 | ctx.fillRect(this.rect.left, this.rect.top, this.rect.width(), this.rect.height()); 239 | } else if (this._shape === HitBox.Shape.CIRCLE) { 240 | ctx.beginPath(); 241 | ctx.arc(this._center.x, this._center.y, this._radius, 0, Math.PI * 2); 242 | ctx.fill(); 243 | } else if (this._shape === HitBox.Shape.SEGMENT) { 244 | ctx.beginPath(); 245 | ctx.arc(this._center.x, this._center.y, this._radius, this._angle1, this._angle2); 246 | ctx.fill(); 247 | } else if (this._shape === HitBox.Shape.COMBO) { 248 | for (var i = 0; i < this.hitBoxes.length; ++i) { 249 | this.hitBoxes[i].render(ctx, pointScale); 250 | } 251 | } 252 | }; 253 | -------------------------------------------------------------------------------- /src/laser.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /** 4 | * @constructor 5 | */ 6 | var LaserSegmentLocation = function(options) { 7 | var defaults = { 8 | x: -1, 9 | y: 1.5, 10 | z: 2, 11 | direction: Laser.Direction.POSITIVE_X, 12 | }; 13 | objectUtil.initWithDefaults(this, defaults, options); 14 | }; 15 | 16 | LaserSegmentLocation.prototype.copy = function() { 17 | return new LaserSegmentLocation({ 18 | x: this.x, 19 | y: this.y, 20 | z: this.z, 21 | direction: this.direction 22 | }); 23 | }; 24 | 25 | LaserSegmentLocation.prototype.equals = function(other) { 26 | return this.x === other.x && 27 | this.y === other.y && 28 | this.z === other.z && 29 | this.direction === other.direction; 30 | }; 31 | 32 | LaserSegmentLocation.prototype.getSceneLocation = function(level) { 33 | return new THREE.Vector3(level.gridXToWorld(this.x), this.y, level.gridZToWorld(this.z)); 34 | }; 35 | 36 | 37 | /** 38 | * @constructor 39 | */ 40 | var Laser = function(options) { 41 | var defaults = { 42 | level: null, 43 | sceneParent: null 44 | }; 45 | objectUtil.initWithDefaults(this, defaults, options); 46 | this.segments = []; 47 | this.laserCannon = new LaserCannon({laser: this, sceneParent: this.sceneParent, level: this.level}); 48 | this.state = new GJS.StateMachine({stateSet: Laser.State}); 49 | }; 50 | 51 | Laser.State = { 52 | OFF: 0, 53 | ON: 1 54 | }; 55 | 56 | Laser.Handling = { 57 | STOP: 0, 58 | CONTINUE: 1, 59 | INFINITY: 2, 60 | GOAL: 3 61 | }; 62 | 63 | Laser.Direction = { 64 | POSITIVE_X: 0, 65 | NEGATIVE_X: 1, 66 | POSITIVE_Z: 2, 67 | NEGATIVE_Z: 3, 68 | POSITIVE_Y: 4, 69 | NEGATIVE_Y: 5 70 | }; 71 | 72 | 73 | Laser.cycleHorizontalDirection = function(direction) { 74 | direction += 1; 75 | if (direction > Laser.Direction.NEGATIVE_Z) { 76 | direction = Laser.Direction.POSITIVE_X; 77 | } 78 | return direction; 79 | }; 80 | 81 | Laser.isVerticalDirection = function(direction) { 82 | return (direction === Laser.Direction.POSITIVE_Y || direction === Laser.Direction.NEGATIVE_Y); 83 | }; 84 | 85 | Laser.offsetFromDirection = function(direction) { 86 | switch (direction) { 87 | case Laser.Direction.POSITIVE_X: 88 | return new THREE.Vector3(1, 0, 0); 89 | case Laser.Direction.NEGATIVE_X: 90 | return new THREE.Vector3(-1, 0, 0); 91 | case Laser.Direction.POSITIVE_Y: 92 | return new THREE.Vector3(0, 1, 0); 93 | case Laser.Direction.NEGATIVE_Y: 94 | return new THREE.Vector3(0, -1, 0); 95 | case Laser.Direction.POSITIVE_Z: 96 | return new THREE.Vector3(0, 0, 1); 97 | case Laser.Direction.NEGATIVE_Z: 98 | return new THREE.Vector3(0, 0, -1); 99 | } 100 | }; 101 | 102 | Laser.oppositeDirection = function(direction) { 103 | switch (direction) { 104 | case Laser.Direction.POSITIVE_X: 105 | return Laser.Direction.NEGATIVE_X; 106 | case Laser.Direction.POSITIVE_Y: 107 | return Laser.Direction.NEGATIVE_Y; 108 | case Laser.Direction.POSITIVE_Z: 109 | return Laser.Direction.NEGATIVE_Z; 110 | case Laser.Direction.NEGATIVE_X: 111 | return Laser.Direction.POSITIVE_X; 112 | case Laser.Direction.NEGATIVE_Y: 113 | return Laser.Direction.POSITIVE_Y; 114 | case Laser.Direction.NEGATIVE_Z: 115 | return Laser.Direction.POSITIVE_Z; 116 | } 117 | }; 118 | 119 | Laser.inPath = function(path, segment) { 120 | for (var i = 0; i < path.length; ++i) { 121 | if (path[i].equals(segment)) { 122 | return true; 123 | } 124 | } 125 | return false; 126 | }; 127 | 128 | Laser.startSound = new GJS.Audio('laser_fireup'); 129 | 130 | Laser.prototype.update = function(deltaTime) { 131 | this.laserCannon.update(deltaTime); 132 | if (this.state.id === Laser.State.OFF) { 133 | this.pruneSegments(0); 134 | } else if (this.state.id === Laser.State.ON && 135 | (this.segments.length === 0 || !this.segments[0].loc.equals(this.laserCannon.loc))) 136 | { 137 | if (this.segments.length === 0) { 138 | Laser.startSound.play(); 139 | } 140 | this.ensureSegmentExists(0); 141 | this.segments[0].loc = this.laserCannon.loc.copy(); 142 | } 143 | 144 | if (this.segments.length === 0) { 145 | return; 146 | } 147 | var segmentIndex = 0; 148 | var loc = this.segments[segmentIndex].loc.copy(); 149 | 150 | this.segments[segmentIndex].length = 0; 151 | var laserContinues = true; 152 | var path = []; 153 | while (laserContinues) { 154 | var offset = Laser.offsetFromDirection(loc.direction); 155 | loc.x = Math.round(loc.x + offset.x); 156 | loc.y = Math.round(loc.y + offset.y - 0.5) + 0.5; 157 | loc.z = Math.round(loc.z + offset.z); 158 | if (offset.y !== 0) { 159 | this.segments[segmentIndex].length += 1; 160 | } else { 161 | this.segments[segmentIndex].length += GRID_SPACING; 162 | } 163 | var handling = this.level.handleLaser(loc); 164 | if (handling instanceof LaserSegmentLocation) { 165 | // Make sure that laser doesn't loop 166 | if (Laser.inPath(path, handling)) { 167 | laserContinues = false; 168 | } else { 169 | path.push(handling.copy()); 170 | ++segmentIndex; 171 | this.ensureSegmentExists(segmentIndex); 172 | this.segments[segmentIndex].loc = handling; 173 | loc = this.segments[segmentIndex].loc.copy(); 174 | this.segments[segmentIndex].length = 0; 175 | } 176 | } else if (handling === Laser.Handling.STOP) { 177 | laserContinues = false; 178 | this.segments[segmentIndex].length -= 0.5; // stop at building wall 179 | } else if (handling === Laser.Handling.INFINITY) { 180 | laserContinues = false; 181 | this.segments[segmentIndex].length += GRID_SPACING * 10; // go beyond the edge of the level 182 | } 183 | } 184 | this.pruneSegments(segmentIndex + 1); 185 | for (var i = 0; i < this.segments.length; ++i) { 186 | this.segments[i].update(deltaTime); 187 | } 188 | }; 189 | 190 | Laser.prototype.ensureSegmentExists = function(i) { 191 | if (i >= this.segments.length) { 192 | this.segments.push(new LaserSegment({ 193 | level: this.level, 194 | sceneParent: this.sceneParent, 195 | laser: this 196 | })); 197 | } 198 | }; 199 | 200 | Laser.prototype.pruneSegments = function(startFrom) { 201 | if (startFrom < this.segments.length) { 202 | for (var i = startFrom; i < this.segments.length; ++i) { 203 | this.segments[i].removeFromScene(); 204 | } 205 | this.segments.splice(startFrom); 206 | } 207 | }; 208 | 209 | /** 210 | * @constructor 211 | */ 212 | var LaserCannon = function(options) { 213 | var defaults = { 214 | laser: null, 215 | level: null 216 | }; 217 | objectUtil.initWithDefaults(this, defaults, options); 218 | 219 | this.mesh = LaserCannon.model.clone(); 220 | this.mesh.position.x = -0.75; 221 | this.mesh.position.y = -0.8; 222 | 223 | this.origin = new THREE.Object3D(); 224 | this.origin.add(this.mesh); 225 | 226 | var boxGeometry = new THREE.BoxGeometry(4, 1, 3); 227 | var material = Level.groundMaterial; 228 | var box = new THREE.Mesh(boxGeometry, material); 229 | box.position.y = -2; 230 | this.origin.add(box); 231 | 232 | this.loc = new LaserSegmentLocation({}); 233 | 234 | this.initThreeSceneObject({ 235 | object: this.origin, 236 | sceneParent: options.sceneParent 237 | }); 238 | 239 | this.addToScene(); 240 | }; 241 | 242 | LaserCannon.prototype = new GJS.ThreeSceneObject(); 243 | 244 | LaserCannon.model = null; 245 | 246 | LaserCannon.prototype.update = function(deltaTime) { 247 | var originPos = this.loc.getSceneLocation(this.level); 248 | this.origin.position.set(originPos.x, originPos.y, originPos.z); 249 | }; 250 | 251 | /** 252 | * @constructor 253 | */ 254 | var LaserSegment = function(options) { 255 | var defaults = { 256 | loc: new LaserSegmentLocation({}), 257 | length: 2 * GRID_SPACING, 258 | level: null, 259 | laser: null 260 | }; 261 | objectUtil.initWithDefaults(this, defaults, options); 262 | 263 | this.mesh = new THREE.Object3D(); 264 | 265 | var geometry = new THREE.BoxGeometry( 0.2, 0.2, 1 ); 266 | var material = LaserSegment.outerMaterial; 267 | var outerMesh = new THREE.Mesh(geometry, material); 268 | this.mesh.add(outerMesh); 269 | 270 | var geometry = new THREE.BoxGeometry( 0.07, 0.07, 1 ); 271 | var material = LaserSegment.innerMaterial; 272 | var innerMesh = new THREE.Mesh(geometry, material); 273 | this.mesh.add(innerMesh); 274 | 275 | this.origin = new THREE.Object3D(); 276 | this.origin.add(this.mesh); 277 | 278 | this.initThreeSceneObject({ 279 | object: this.origin, 280 | sceneParent: options.sceneParent 281 | }); 282 | 283 | this.addToScene(); 284 | }; 285 | 286 | LaserSegment.prototype = new GJS.ThreeSceneObject(); 287 | 288 | LaserSegment.outerMaterial = (function() { 289 | var material = new THREE.MeshPhongMaterial( { color: 0x0, emissive: 0xff5555 } ); 290 | material.blending = THREE.AdditiveBlending; 291 | material.transparent = true; 292 | material.opacity = 0.7; 293 | return material; 294 | })(); 295 | LaserSegment.innerMaterial = (function() { 296 | var material = new THREE.MeshPhongMaterial( { color: 0x0, emissive: 0xffffff } ); 297 | return material; 298 | })(); 299 | 300 | LaserSegment.prototype.update = function(deltaTime) { 301 | var originPos = this.loc.getSceneLocation(this.level); 302 | this.origin.position.set(originPos.x, originPos.y, originPos.z); 303 | this.origin.rotation.x = 0; 304 | this.origin.rotation.y = 0; 305 | switch (this.loc.direction) { 306 | case Laser.Direction.POSITIVE_X: 307 | this.origin.rotation.y = Math.PI * 0.5; 308 | break; 309 | case Laser.Direction.NEGATIVE_X: 310 | this.origin.rotation.y = -Math.PI * 0.5; 311 | break; 312 | case Laser.Direction.POSITIVE_Z: 313 | this.origin.rotation.y = 0; 314 | break; 315 | case Laser.Direction.NEGATIVE_Z: 316 | this.origin.rotation.y = Math.PI; 317 | break; 318 | case Laser.Direction.POSITIVE_Y: 319 | this.origin.rotation.x = -Math.PI * 0.5; 320 | break; 321 | case Laser.Direction.NEGATIVE_Y: 322 | this.origin.rotation.x = Math.PI * 0.5; 323 | break; 324 | } 325 | 326 | this.mesh.position.z = this.length * 0.5; 327 | this.mesh.scale.z = this.length; 328 | }; 329 | -------------------------------------------------------------------------------- /assets/models/roof_stationary.json: -------------------------------------------------------------------------------- 1 | { 2 | "faces":[35,0,1,2,3,0,0,0,0,0,35,4,5,6,7,0,1,1,0,0,35,8,9,10,11,0,0,0,0,0,35,12,13,14,15,0,0,0,0,0,35,16,17,18,19,1,0,0,0,0,35,20,21,22,23,1,0,0,0,0,35,24,25,26,27,1,0,0,0,0,35,28,29,30,31,1,0,0,0,0,35,32,33,34,35,1,0,0,0,0,35,36,37,38,39,1,0,0,0,0,35,40,41,42,43,1,0,0,0,0,35,44,45,46,47,1,0,0,0,0,35,48,49,50,51,1,2,2,2,2,35,52,53,54,55,1,3,2,2,2,35,56,57,58,59,1,2,2,2,2,35,60,61,62,63,1,3,2,2,2,35,64,65,66,67,1,2,2,2,2,35,68,69,70,71,1,3,2,2,2,35,72,73,74,75,1,2,2,2,2,35,76,77,78,79,1,3,2,2,2,35,80,81,82,83,1,2,2,2,2,35,84,85,86,87,1,2,2,2,2,35,88,89,90,91,1,2,2,2,2,35,92,93,94,95,1,2,2,2,2,35,96,97,98,99,1,4,4,4,4,35,100,101,102,103,1,5,5,5,5,35,104,105,106,107,1,6,6,6,6,35,108,109,110,111,1,7,7,7,7,35,112,113,114,115,1,8,8,8,8,35,116,117,118,119,1,9,9,9,9,35,120,121,122,123,1,10,10,10,10,35,124,125,126,127,1,11,11,11,11,35,128,129,130,131,0,5,5,5,5,35,132,133,134,135,0,7,7,7,7,35,136,137,138,139,0,9,9,9,9,35,140,141,142,143,0,11,11,11,11,35,144,145,146,147,0,4,4,4,4,35,148,149,150,151,0,6,6,6,6,35,152,153,154,155,0,8,8,8,8,35,156,157,158,159,0,10,10,10,10,35,160,161,162,163,0,0,0,0,0,35,164,165,166,167,0,0,0,0,0,35,168,169,170,171,0,1,1,0,0,35,172,173,174,175,0,0,0,0,0,35,176,177,178,179,0,9,9,9,9,35,180,181,182,183,0,11,11,11,11,35,184,185,186,187,0,11,11,11,11,35,188,189,190,191,0,9,9,9,9,35,192,193,194,195,2,0,0,0,0,35,196,197,198,199,2,0,0,0,0,35,200,201,202,203,2,0,0,0,0,35,204,205,206,207,2,0,0,0,0,35,208,209,210,211,0,10,10,10,10,35,212,213,214,215,0,8,8,8,8,35,216,217,218,219,0,10,10,10,10,35,220,221,222,223,0,8,8,8,8,35,224,225,226,227,1,2,2,2,2,35,228,229,230,231,0,0,0,0,0,35,232,233,234,235,0,11,11,11,11,35,236,237,238,239,0,10,10,10,10,35,240,241,242,243,0,8,8,8,8,35,244,245,246,247,0,9,9,9,9,35,248,249,250,251,1,2,2,2,2,35,252,253,254,255,0,0,0,0,0,35,256,257,258,259,0,11,11,11,11,35,260,261,262,263,0,10,10,10,10,35,264,265,266,267,0,8,8,8,8,35,268,269,270,271,0,9,9,9,9,35,272,273,274,275,1,2,2,2,2,35,276,277,278,279,0,0,0,0,0,35,280,281,282,283,0,11,11,11,11,35,284,285,286,287,0,10,10,10,10,35,288,289,290,291,0,8,8,8,8,35,292,293,294,295,0,9,9,9,9,35,296,297,298,299,1,2,2,2,2,35,300,301,302,303,1,0,0,0,0,35,304,305,306,307,1,11,11,11,11,35,308,309,310,311,1,10,10,10,10,35,312,313,314,315,1,8,8,8,8,35,316,317,318,319,1,9,9,9,9], 3 | "normals":[0,1,0,0,0.999969,0,0,-1,0,0,-0.999969,0,-0.707083,0,0.707083,-0.707083,0,-0.707083,0.707083,0,-0.707083,0.707083,0,0.707083,-1,0,0,0,0,1,1,0,0,0,0,-1], 4 | "vertices":[0.407143,0.120726,-0.481542,0,0.120726,-0.389698,0.389698,0.120726,-0.389698,0.481542,0.120726,-0.407143,-0.407143,0.120726,-0.481542,-0.389698,0.120726,-0.389698,0,0.120726,-0.389698,0.407143,0.120726,-0.481542,-0.481542,0.120726,-0.407143,-0.389698,0.120726,-0,-0.389698,0.120726,-0.389698,-0.407143,0.120726,-0.481542,-0.481542,0.120726,0.407143,-0.389698,0.120726,0.389698,-0.389698,0.120726,-0,-0.481542,0.120726,-0.407143,-0.430158,0.06917,-0.508763,-0.508763,0.06917,-0.430158,-0.481542,0.06917,-0.407143,-0.407143,0.06917,-0.481542,0.430158,0.06917,0.508763,0.508763,0.06917,0.430158,0.481542,0.06917,0.407143,0.407143,0.06917,0.481542,-0.430158,0.06917,0.508763,0.430158,0.06917,0.508763,0.407143,0.06917,0.481542,-0.407143,0.06917,0.481542,0.430158,0.06917,-0.508763,-0.430158,0.06917,-0.508763,-0.407143,0.06917,-0.481542,0.407143,0.06917,-0.481542,-0.508763,0.06917,0.430158,-0.430158,0.06917,0.508763,-0.407143,0.06917,0.481542,-0.481542,0.06917,0.407143,0.508763,0.06917,-0.430158,0.430158,0.06917,-0.508763,0.407143,0.06917,-0.481542,0.481542,0.06917,-0.407143,-0.508763,0.06917,-0.430158,-0.508763,0.06917,0.430158,-0.481542,0.06917,0.407143,-0.481542,0.06917,-0.407143,0.508763,0.06917,0.430158,0.508763,0.06917,-0.430158,0.481542,0.06917,-0.407143,0.481542,0.06917,0.407143,-0.508763,0.03117,-0.430158,-0.370466,0.03117,-0,-0.370466,0.03117,0.370466,-0.508763,0.03117,0.430158,-0.430158,0.03117,-0.508763,-0.370466,0.03117,-0.370466,-0.370466,0.03117,-0,-0.508763,0.03117,-0.430158,0.430158,0.03117,-0.508763,0,0.03117,-0.370466,-0.370466,0.03117,-0.370466,-0.430158,0.03117,-0.508763,0.508763,0.03117,-0.430158,0.370466,0.03117,-0.370466,0,0.03117,-0.370466,0.430158,0.03117,-0.508763,0.508763,0.03117,0.430158,0.370466,0.03117,-0,0.370466,0.03117,-0.370466,0.508763,0.03117,-0.430158,0.430158,0.03117,0.508763,0.370466,0.03117,0.370466,0.370466,0.03117,-0,0.508763,0.03117,0.430158,-0.430158,0.03117,0.508763,0,0.03117,0.370466,0.370466,0.03117,0.370466,0.430158,0.03117,0.508763,-0.508763,0.03117,0.430158,-0.370466,0.03117,0.370466,0,0.03117,0.370466,-0.430158,0.03117,0.508763,-0.370466,0.03117,-0.370466,0,0.03117,-0.370466,0,0.03117,-0,-0.370466,0.03117,-0,0,0.03117,-0.370466,0.370466,0.03117,-0.370466,0.370466,0.03117,-0,0,0.03117,-0,-0.370466,0.03117,-0,0,0.03117,-0,0,0.03117,0.370466,-0.370466,0.03117,0.370466,0,0.03117,-0,0.370466,0.03117,-0,0.370466,0.03117,0.370466,0,0.03117,0.370466,-0.430158,0.06917,0.508763,-0.508763,0.06917,0.430158,-0.508763,0.03117,0.430158,-0.430158,0.03117,0.508763,-0.508763,0.06917,-0.430158,-0.430158,0.06917,-0.508763,-0.430158,0.03117,-0.508763,-0.508763,0.03117,-0.430158,0.430158,0.06917,-0.508763,0.508763,0.06917,-0.430158,0.508763,0.03117,-0.430158,0.430158,0.03117,-0.508763,0.508763,0.06917,0.430158,0.430158,0.06917,0.508763,0.430158,0.03117,0.508763,0.508763,0.03117,0.430158,-0.508763,0.06917,0.430158,-0.508763,0.06917,-0.430158,-0.508763,0.03117,-0.430158,-0.508763,0.03117,0.430158,0.430158,0.06917,0.508763,-0.430158,0.06917,0.508763,-0.430158,0.03117,0.508763,0.430158,0.03117,0.508763,0.508763,0.06917,-0.430158,0.508763,0.06917,0.430158,0.508763,0.03117,0.430158,0.508763,0.03117,-0.430158,-0.430158,0.06917,-0.508763,0.430158,0.06917,-0.508763,0.430158,0.03117,-0.508763,-0.430158,0.03117,-0.508763,-0.407143,0.06917,-0.481542,-0.481542,0.06917,-0.407143,-0.481542,0.120726,-0.407143,-0.407143,0.120726,-0.481542,0.407143,0.06917,0.481542,0.481542,0.06917,0.407143,0.481542,0.120726,0.407143,0.407143,0.120726,0.481542,-0.407143,0.06917,0.481542,0.407143,0.06917,0.481542,0.407143,0.120726,0.481542,-0.407143,0.120726,0.481542,0.407143,0.06917,-0.481542,-0.407143,0.06917,-0.481542,-0.407143,0.120726,-0.481542,0.407143,0.120726,-0.481542,-0.481542,0.06917,0.407143,-0.407143,0.06917,0.481542,-0.407143,0.120726,0.481542,-0.481542,0.120726,0.407143,0.481542,0.06917,-0.407143,0.407143,0.06917,-0.481542,0.407143,0.120726,-0.481542,0.481542,0.120726,-0.407143,-0.481542,0.06917,-0.407143,-0.481542,0.06917,0.407143,-0.481542,0.120726,0.407143,-0.481542,0.120726,-0.407143,0.481542,0.06917,0.407143,0.481542,0.06917,-0.407143,0.481542,0.120726,-0.407143,0.481542,0.120726,0.407143,0.481542,0.120726,-0.407143,0.389698,0.120726,-0.389698,0.389698,0.120726,-0,0.481542,0.120726,0.407143,0.481542,0.120726,0.407143,0.389698,0.120726,-0,0.389698,0.120726,0.389698,0.407143,0.120726,0.481542,0.407143,0.120726,0.481542,0.389698,0.120726,0.389698,0,0.120726,0.389698,-0.407143,0.120726,0.481542,-0.407143,0.120726,0.481542,0,0.120726,0.389698,-0.389698,0.120726,0.389698,-0.481542,0.120726,0.407143,0,0.120726,-0.389698,-0.389698,0.120726,-0.389698,-0.389698,0.092522,-0.389698,0,0.092522,-0.389698,0,0.120726,0.389698,0.389698,0.120726,0.389698,0.389698,0.092522,0.389698,0,0.092522,0.389698,-0.389698,0.120726,0.389698,0,0.120726,0.389698,0,0.092522,0.389698,-0.389698,0.092522,0.389698,0.389698,0.120726,-0.389698,0,0.120726,-0.389698,0,0.092522,-0.389698,0.389698,0.092522,-0.389698,-0.389698,0.092522,-0,0,0.092522,-0,0,0.092522,-0.389698,-0.389698,0.092522,-0.389698,0,0.092522,-0,0.389698,0.092522,-0,0.389698,0.092522,-0.389698,0,0.092522,-0.389698,-0.389698,0.092522,0.389698,0,0.092522,0.389698,0,0.092522,-0,-0.389698,0.092522,-0,0,0.092522,0.389698,0.389698,0.092522,0.389698,0.389698,0.092522,-0,0,0.092522,-0,-0.389698,0.120726,-0.389698,-0.389698,0.120726,-0,-0.389698,0.092522,-0,-0.389698,0.092522,-0.389698,0.389698,0.120726,0.389698,0.389698,0.120726,-0,0.389698,0.092522,-0,0.389698,0.092522,0.389698,-0.389698,0.120726,-0,-0.389698,0.120726,0.389698,-0.389698,0.092522,0.389698,-0.389698,0.092522,-0,0.389698,0.120726,-0,0.389698,0.120726,-0.389698,0.389698,0.092522,-0.389698,0.389698,0.092522,-0,0.11167,0.085332,0.332747,0.11167,0.085331,0.056951,0.334757,0.085331,0.056951,0.334757,0.085332,0.332747,0.11167,0.157331,0.332747,0.334757,0.157331,0.332747,0.334757,0.157331,0.056951,0.11167,0.157331,0.056951,0.334757,0.085331,0.056951,0.11167,0.085331,0.056951,0.11167,0.157331,0.056951,0.334757,0.157331,0.056951,0.334757,0.085332,0.332747,0.334757,0.085331,0.056951,0.334757,0.157331,0.056951,0.334757,0.157331,0.332747,0.11167,0.085331,0.056951,0.11167,0.085332,0.332747,0.11167,0.157331,0.332747,0.11167,0.157331,0.056951,0.11167,0.085332,0.332747,0.334757,0.085332,0.332747,0.334757,0.157331,0.332747,0.11167,0.157331,0.332747,0.254638,0.08889,-0.240708,0.254638,0.08889,-0.344698,0.338754,0.08889,-0.344698,0.338754,0.08889,-0.240708,0.254638,0.116038,-0.240708,0.338754,0.116038,-0.240708,0.338754,0.116038,-0.344698,0.254638,0.116038,-0.344698,0.338754,0.08889,-0.344698,0.254638,0.08889,-0.344698,0.254638,0.116038,-0.344698,0.338754,0.116038,-0.344698,0.338754,0.08889,-0.240708,0.338754,0.08889,-0.344698,0.338754,0.116038,-0.344698,0.338754,0.116038,-0.240708,0.254638,0.08889,-0.344698,0.254638,0.08889,-0.240708,0.254638,0.116038,-0.240708,0.254638,0.116038,-0.344698,0.254638,0.08889,-0.240708,0.338754,0.08889,-0.240708,0.338754,0.116038,-0.240708,0.254638,0.116038,-0.240708,0.15462,0.08889,-0.240708,0.15462,0.08889,-0.344698,0.238736,0.08889,-0.344698,0.238736,0.08889,-0.240708,0.15462,0.116038,-0.240708,0.238736,0.116038,-0.240708,0.238736,0.116038,-0.344698,0.15462,0.116038,-0.344698,0.238736,0.08889,-0.344698,0.15462,0.08889,-0.344698,0.15462,0.116038,-0.344698,0.238736,0.116038,-0.344698,0.238736,0.08889,-0.240708,0.238736,0.08889,-0.344698,0.238736,0.116038,-0.344698,0.238736,0.116038,-0.240708,0.15462,0.08889,-0.344698,0.15462,0.08889,-0.240708,0.15462,0.116038,-0.240708,0.15462,0.116038,-0.344698,0.15462,0.08889,-0.240708,0.238736,0.08889,-0.240708,0.238736,0.116038,-0.240708,0.15462,0.116038,-0.240708,-0.353821,0.08889,0.064478,-0.353821,0.08889,0.015538,-0.314234,0.08889,0.015538,-0.314234,0.08889,0.064478,-0.353821,0.101322,0.064478,-0.314234,0.101322,0.064478,-0.314234,0.101322,0.015538,-0.353821,0.101322,0.015538,-0.314234,0.08889,0.015538,-0.353821,0.08889,0.015538,-0.353821,0.101322,0.015538,-0.314234,0.101322,0.015538,-0.314234,0.08889,0.064478,-0.314234,0.08889,0.015538,-0.314234,0.101322,0.015538,-0.314234,0.101322,0.064478,-0.353821,0.08889,0.015538,-0.353821,0.08889,0.064478,-0.353821,0.101322,0.064478,-0.353821,0.101322,0.015538,-0.353821,0.08889,0.064478,-0.314234,0.08889,0.064478,-0.314234,0.101322,0.064478,-0.353821,0.101322,0.064478], 5 | "metadata":{ 6 | "version":3, 7 | "generator":"io_three", 8 | "materials":3, 9 | "faces":80, 10 | "normals":12, 11 | "type":"Geometry", 12 | "vertices":320, 13 | "uvs":0 14 | }, 15 | "name":"Cube.007Geometry", 16 | "materials":[{ 17 | "specularCoef":50, 18 | "shading":"phong", 19 | "colorEmissive":[0,0,0], 20 | "wireframe":false, 21 | "blending":"NormalBlending", 22 | "colorSpecular":[0.010204,0.010204,0.010204], 23 | "transparent":false, 24 | "depthWrite":true, 25 | "colorDiffuse":[0.181267,0.181267,0.181267], 26 | "DbgColor":15597568, 27 | "DbgIndex":1, 28 | "opacity":1, 29 | "depthTest":true, 30 | "visible":true, 31 | "DbgName":"stationary_roof" 32 | },{ 33 | "specularCoef":50, 34 | "shading":"phong", 35 | "colorEmissive":[0,0,0], 36 | "wireframe":false, 37 | "blending":"NormalBlending", 38 | "colorSpecular":[0.05102,0.05102,0.05102], 39 | "transparent":false, 40 | "depthWrite":true, 41 | "colorDiffuse":[0.4,0.4,0.4], 42 | "DbgColor":15658734, 43 | "DbgIndex":0, 44 | "opacity":1, 45 | "depthTest":true, 46 | "visible":true, 47 | "DbgName":"stationary_frame" 48 | },{ 49 | "specularCoef":50, 50 | "shading":"phong", 51 | "colorEmissive":[0,0,0], 52 | "wireframe":false, 53 | "blending":"NormalBlending", 54 | "colorSpecular":[0.010204,0.010204,0.010204], 55 | "transparent":false, 56 | "depthWrite":true, 57 | "colorDiffuse":[0.039639,0.039639,0.039639], 58 | "DbgColor":60928, 59 | "DbgIndex":2, 60 | "opacity":1, 61 | "depthTest":true, 62 | "visible":true, 63 | "DbgName":"stationary_rooftop" 64 | }], 65 | "uvs":[] 66 | } -------------------------------------------------------------------------------- /src/canvasui.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | // Requires util2d.js 4 | 5 | /** 6 | * Class for rendering and interacting with UI elements on a canvas. 7 | * @constructor 8 | */ 9 | var CanvasUI = function(options) { 10 | var defaults = { 11 | element: null, 12 | getCanvasPositionFromEvent: null 13 | }; 14 | for(var key in defaults) { 15 | if (!options.hasOwnProperty(key)) { 16 | this[key] = defaults[key]; 17 | } else { 18 | this[key] = options[key]; 19 | } 20 | } 21 | this.clear(); 22 | 23 | if (this.element !== null && this.getCanvasPositionFromEvent !== null) { 24 | var that = this; 25 | this.element.addEventListener('mousemove', function(event) { 26 | that.setCursorPosition(that.getCanvasPositionFromEvent(event)); 27 | }); 28 | this.element.addEventListener('touchmove', function(event) { 29 | that.setCursorPosition(that.getCanvasPositionFromEvent(event)); 30 | event.preventDefault(); 31 | }); 32 | this.element.addEventListener('mousedown', function(event) { 33 | that.down(that.getCanvasPositionFromEvent(event)); 34 | }); 35 | this.element.addEventListener('touchstart', function(event) { 36 | that.down(that.getCanvasPositionFromEvent(event)); 37 | event.preventDefault(); 38 | }); 39 | this.element.addEventListener('mouseup', function(event) { 40 | that.release(that.getCanvasPositionFromEvent(event)); 41 | }); 42 | this.element.addEventListener('touchend', function(event) { 43 | that.release(undefined); 44 | event.preventDefault(); 45 | }); 46 | } 47 | }; 48 | 49 | /** 50 | * Update UI element state and animations. 51 | * @param {number} deltaTime Time passed since the last update in seconds. 52 | */ 53 | CanvasUI.prototype.update = function(deltaTime) { 54 | for (var i = 0; i < this.uiElements.length; ++i) { 55 | this.uiElements[i].update(deltaTime); 56 | } 57 | }; 58 | 59 | /** 60 | * Render the UI. 61 | * @param {CanvasRenderingContext2D} ctx The canvas rendering context to use. 62 | */ 63 | CanvasUI.prototype.render = function(ctx) { 64 | var draggedElements = []; 65 | var i; 66 | for (i = 0; i < this.uiElements.length; ++i) { 67 | if (!this.uiElements[i].dragged) { 68 | this.uiElements[i].render(ctx, this.cursorX, this.cursorY); 69 | } else { 70 | draggedElements.push(this.uiElements[i]); 71 | } 72 | } 73 | for (i = 0; i < draggedElements.length; ++i) { 74 | draggedElements[i].render(ctx, this.cursorX, this.cursorY); 75 | } 76 | }; 77 | 78 | /** 79 | * Clear the UI from all elements. 80 | */ 81 | CanvasUI.prototype.clear = function() { 82 | this.uiElements = []; 83 | this.cursorX = 0; 84 | this.cursorY = 0; 85 | this.downButton = null; 86 | }; 87 | 88 | /** 89 | * Set the cursor position. 90 | * @param {Object|Vec2} vec New position to set. Needs to have x and y coordinates. Relative to the canvas coordinate 91 | * space. 92 | */ 93 | CanvasUI.prototype.setCursorPosition = function(vec) { 94 | this.cursorX = vec.x; 95 | this.cursorY = vec.y; 96 | if (this.downButton !== null && this.downButton.draggable) { 97 | this.downButton.draggedX = this.downButton.centerX + (this.cursorX - this.dragStartX); 98 | this.downButton.draggedY = this.downButton.centerY + (this.cursorY - this.dragStartY); 99 | } 100 | }; 101 | 102 | /** 103 | * Handle a mouse / touch down event. 104 | * @param {Object|Vec2} vec New position to set. Needs to have x and y coordinates. Relative to the canvas coordinate 105 | * space. 106 | */ 107 | CanvasUI.prototype.down = function(vec) { 108 | this.setCursorPosition(vec); 109 | for (var i = 0; i < this.uiElements.length; ++i) { 110 | if (this.uiElements[i].active && this.uiElements[i].hitTest(this.cursorX, this.cursorY)) { 111 | this.downButton = this.uiElements[i]; 112 | this.downButton.down(); 113 | if (this.uiElements[i].draggable) { 114 | this.downButton.dragged = true; 115 | this.dragStartX = this.cursorX; 116 | this.dragStartY = this.cursorY; 117 | } 118 | } 119 | } 120 | this.setCursorPosition(vec); 121 | }; 122 | 123 | /** 124 | * Handle a mouse / touch up event. 125 | * @param {Object|Vec2=} vec New position to set. Needs to have x and y coordinates. Relative to the canvas coordinate 126 | * space. May be undefined, in which case the last known position will be used to evaluate the effects. 127 | */ 128 | CanvasUI.prototype.release = function(vec) { 129 | if (vec !== undefined) { 130 | this.setCursorPosition(vec); 131 | } 132 | if (this.downButton !== null) { 133 | var clicked = false; 134 | for (var i = 0; i < this.uiElements.length; ++i) { 135 | if (this.uiElements[i].active && this.uiElements[i].hitTest(this.cursorX, this.cursorY)) { 136 | if (this.downButton === this.uiElements[i]) { 137 | clicked = true; 138 | } else if (this.uiElements[i].dragTargetCallback !== null && this.downButton.dragged) { 139 | this.uiElements[i].dragTargetCallback(this.downButton.draggedObjectFunc()); 140 | } 141 | } 142 | } 143 | this.downButton.release(clicked); 144 | this.downButton.dragged = false; 145 | this.downButton = null; 146 | } 147 | console.log(this.cursorX, this.cursorY); 148 | }; 149 | 150 | CanvasUI.prototype.addElement = function(element) { 151 | this.uiElements.push(element); 152 | }; 153 | 154 | /** 155 | * The default font for UI elements. 156 | */ 157 | CanvasUI.defaultFont = 'sans-serif'; 158 | 159 | /** 160 | * Minimum interval between clicks on the same button in seconds. 161 | */ 162 | CanvasUI.minimumClickInterval = 0.5; 163 | 164 | /** 165 | * A single UI element to draw on a canvas, typically either a button or a label. 166 | * Will be rendered with text by default, but can also be drawn with a custom rendering function renderFunc. 167 | * @constructor 168 | */ 169 | var CanvasUIElement = function(options) { 170 | var defaults = { 171 | label: 'Button', 172 | labelFunc: null, // Function that returns the current text to draw on the element. Overrides label if set. 173 | renderFunc: null, 174 | centerX: 0, 175 | centerY: 0, 176 | width: 100, 177 | height: 50, 178 | clickCallback: null, 179 | dragTargetCallback: null, // Called when something is dragged onto this object, with the dragged object as parameter. 180 | draggedObjectFunc: null, 181 | active: true, // Active elements are visible and can be interacted with. Inactive elements can't be interacted with. 182 | draggable: false, 183 | fontSize: 20, // In pixels 184 | font: CanvasUI.defaultFont, 185 | appearance: undefined // One of CanvasUIElement.Appearance. By default the appearance is determined based on callbacks. 186 | }; 187 | for(var key in defaults) { 188 | if (!options.hasOwnProperty(key)) { 189 | this[key] = defaults[key]; 190 | } else { 191 | this[key] = options[key]; 192 | } 193 | } 194 | this.draggedX = this.centerX; 195 | this.draggedY = this.centerY; 196 | this.dragged = false; 197 | this.time = 0.5; 198 | this.isDown = false; 199 | this.lastClick = 0; 200 | if (this.appearance === undefined) { 201 | if (this.clickCallback !== null) { 202 | this.appearance = CanvasUIElement.Appearance.BUTTON; 203 | } else { 204 | this.appearance = CanvasUIElement.Appearance.LABEL; 205 | } 206 | } 207 | }; 208 | 209 | CanvasUIElement.Appearance = { 210 | BUTTON: 0, 211 | LABEL: 1 212 | }; 213 | 214 | /** 215 | * Update UI element state and animations. 216 | * @param {number} deltaTime Time passed since the last update in seconds. 217 | */ 218 | CanvasUIElement.prototype.update = function(deltaTime) { 219 | this.time += deltaTime; 220 | }; 221 | 222 | /** 223 | * Render the element. Will call renderFunc if it is defined. 224 | * @param {CanvasRenderingContext2D} ctx Context to render to. 225 | * @param {number} cursorX Cursor horizontal coordinate in the canvas coordinate system. 226 | * @param {number} cursorY Cursor vertical coordinate in the canvas coordinate system. 227 | */ 228 | CanvasUIElement.prototype.render = function(ctx, cursorX, cursorY) { 229 | if (!this.active) { 230 | return; 231 | } 232 | var pressedExtent = this.isDown ? (this.time - this.lastDownTime) * 8.0 : 1.0 - (this.time - this.lastUpTime) * 3.0; 233 | pressedExtent = mathUtil.clamp(0, 1, pressedExtent); 234 | var cursorOn = this.hitTest(cursorX, cursorY); 235 | 236 | if (this.renderFunc !== null) { 237 | this.renderFunc(ctx, this, cursorOn, pressedExtent); 238 | return; 239 | } 240 | 241 | if (this.appearance === CanvasUIElement.Appearance.BUTTON) { 242 | var rect = this.getRect(); 243 | ctx.fillStyle = '#000'; 244 | if (pressedExtent > 0) { 245 | ctx.globalAlpha = 1.0 - pressedExtent * 0.2; 246 | } else if (cursorOn) { 247 | ctx.globalAlpha = 1.0; 248 | } else { 249 | ctx.globalAlpha = 0.5; 250 | } 251 | ctx.fillRect(rect.left, rect.top, rect.width(), rect.height()); 252 | ctx.lineWidth = 3; 253 | ctx.strokeStyle = '#fff'; 254 | if (!this.canClick()) { 255 | ctx.globalAlpha *= 0.6; 256 | } 257 | ctx.strokeRect(rect.left, rect.top, rect.width(), rect.height()); 258 | } 259 | ctx.globalAlpha = 1.0; 260 | ctx.textAlign = 'center'; 261 | ctx.fillStyle = '#fff'; 262 | ctx.font = this.fontSize + 'px ' + this.font; 263 | var label = this.label; 264 | if (this.labelFunc) { 265 | label = this.labelFunc(); 266 | } 267 | ctx.fillText(label, this.centerX, this.centerY + 7); 268 | }; 269 | 270 | /** 271 | * @return {number} The horizontal position to draw the element at. May be different from the logical position if the 272 | * element is being dragged. 273 | */ 274 | CanvasUIElement.prototype.visualX = function() { 275 | if (this.dragged) { 276 | return this.draggedX; 277 | } else { 278 | return this.centerX; 279 | } 280 | }; 281 | 282 | /** 283 | * @return {number} The vertical position to draw the element at. May be different from the logical position if the 284 | * element is being dragged. 285 | */ 286 | CanvasUIElement.prototype.visualY = function() { 287 | if (this.dragged) { 288 | return this.draggedY; 289 | } else { 290 | return this.centerY; 291 | } 292 | }; 293 | 294 | /** 295 | * @return {boolean} True when the element is being dragged. 296 | */ 297 | CanvasUIElement.prototype.isDragged = function() { 298 | return this.dragged; 299 | }; 300 | 301 | /** 302 | * @param {number} x Horizontal coordinate to test. 303 | * @param {number} y Vertical coordinate to test. 304 | * @return {boolean} Whether the coordinate is within the area of the element. 305 | */ 306 | CanvasUIElement.prototype.hitTest = function(x, y) { 307 | if (this.clickCallback !== null) { 308 | return this.getRect().containsVec2(new Vec2(x, y)); 309 | } 310 | return false; 311 | }; 312 | 313 | /** 314 | * @return boolean True if the element can generate click events right now. False if the click cooldown hasn't 315 | * completed. 316 | */ 317 | CanvasUIElement.prototype.canClick = function() { 318 | var sinceClicked = this.time - this.lastClick; 319 | return sinceClicked >= CanvasUI.minimumClickInterval; 320 | }; 321 | 322 | CanvasUIElement.prototype.getRect = function() { 323 | return new Rect( 324 | this.centerX - this.width * 0.5, 325 | this.centerX + this.width * 0.5, 326 | this.centerY - this.height * 0.5, 327 | this.centerY + this.height * 0.5 328 | ); 329 | }; 330 | 331 | /** 332 | * Mark the element as down, for visual purposes only. 333 | */ 334 | CanvasUIElement.prototype.down = function() { 335 | this.isDown = true; 336 | this.lastDownTime = this.time; 337 | }; 338 | 339 | /** 340 | * Mark the element as up. Will generate a click event if clicked is true. 341 | * @param {boolean} clicked True when clicked, false when the cursor position has left the area of the element. 342 | */ 343 | CanvasUIElement.prototype.release = function(clicked) { 344 | this.isDown = false; 345 | this.lastUpTime = this.time; 346 | if (!clicked || !this.canClick()) { 347 | return; 348 | } 349 | this.lastClick = this.time; 350 | if (this.clickCallback !== null) { 351 | this.clickCallback(); 352 | } 353 | }; 354 | -------------------------------------------------------------------------------- /assets/models/hole_stationary.json: -------------------------------------------------------------------------------- 1 | { 2 | "vertices":[0,-0.5,-0.5,-0.422749,-0.5,-0.5,-0.413625,-0.489208,-0.479966,0,-0.489208,-0.479966,0.422749,-0,-0.5,0.422749,-0.5,-0.5,0.413625,-0.489208,-0.479966,0.413625,-0,-0.479966,0,-0.5,0.5,0.422749,-0.5,0.5,0.413625,-0.489208,0.479966,0,-0.489208,0.479966,-0.422749,0,0.5,-0.422749,0.5,0.5,-0.5,0.5,0.422749,-0.5,0,0.422749,-0.422749,-0,-0.5,-0.422749,-0.5,-0.5,-0.5,-0.5,-0.422749,-0.5,-0,-0.422749,0.5,-0,-0.422749,0.5,-0.5,-0.422749,0.422749,-0.5,-0.5,0.422749,-0,-0.5,0.422749,0,0.5,0.422749,-0.5,0.5,0.5,-0.5,0.422749,0.5,0,0.422749,-0.5,0,0.422749,-0.5,0.5,0.422749,-0.479966,0.489208,0.413625,-0.479966,0,0.413625,0.479966,-0,-0.413625,0.479966,0,0.413625,0.479966,-0.489208,0.413625,0.479966,-0.489208,-0.413625,-0.479966,0,0.413625,-0.479966,-0,-0.413625,-0.479966,-0.489208,-0.413625,-0.479966,-0.489208,0.413625,-0.422749,-0,-0.5,-0.422749,0.5,-0.5,-0.413625,0.489208,-0.479966,-0.413625,-0,-0.479966,0.5,-0.5,0.422749,0.5,-0.5,-0.422749,0.479966,-0.489208,-0.413625,0.479966,-0.489208,0.413625,0.422749,0,0.5,0.422749,0.5,0.5,0.413625,0.489208,0.479966,0.413625,0,0.479966,0,0.5,0.5,-0.422749,0.5,0.5,-0.413625,0.489208,0.479966,0,0.489208,0.479966,-0.5,-0,-0.422749,-0.5,-0.5,-0.422749,-0.479966,-0.489208,-0.413625,-0.479966,-0,-0.413625,0.5,0.5,-0.422749,0.5,0.5,0.422749,0.479966,0.489208,0.413625,0.479966,0.489208,-0.413625,0.5,0,0.422749,0.5,-0.5,0.422749,0.479966,-0.489208,0.413625,0.479966,0,0.413625,0,0.5,-0.5,0.422749,0.5,-0.5,0.413625,0.489208,-0.479966,0,0.489208,-0.479966,-0.422749,0,0.5,-0.422749,-0.5,0.5,-0.413625,-0.489208,0.479966,-0.413625,0,0.479966,-0.5,0.5,0.422749,-0.5,0.5,-0.422749,-0.479966,0.489208,-0.413625,-0.479966,0.489208,0.413625,0.5,-0,-0.422749,0.5,0.5,-0.422749,0.479966,0.489208,-0.413625,0.479966,-0,-0.413625,-0.5,-0.5,-0.422749,-0.5,-0.5,0.422749,-0.479966,-0.489208,0.413625,-0.479966,-0.489208,-0.413625,0.422749,-0.5,-0.5,0,-0.5,-0.5,0,-0.489208,-0.479966,0.413625,-0.489208,-0.479966,0.422749,-0.5,-0.5,0.3,-0.5,-0.3,0,-0.5,-0.3,0,-0.5,-0.5,-0.422749,-0.5,0.5,0,-0.5,0.5,0,-0.489208,0.479966,-0.413625,-0.489208,0.479966,-0.422749,-0.5,0.5,-0.3,-0.5,0.3,0,-0.5,0.3,0,-0.5,0.5,-0.422749,0.5,-0.5,0,0.5,-0.5,0,0.489208,-0.479966,-0.413625,0.489208,-0.479966,-0.422749,0.5,-0.5,-0.3,0.5,-0.3,0,0.5,-0.3,0,0.5,-0.5,0.422749,0.5,0.5,0,0.5,0.5,0,0.489208,0.479966,0.413625,0.489208,0.479966,0.422749,0.5,0.5,0.3,0.5,0.3,0,0.5,0.3,0,0.5,0.5,-0.5,0.5,0.422749,-0.3,0.5,0.3,-0.3,0.5,-0,-0.5,0.5,-0.422749,-0.5,0.5,-0.422749,-0.3,0.5,-0,-0.3,0.5,-0.3,-0.422749,0.5,-0.5,0,0.5,-0.5,0,0.5,-0.3,0.422749,0.5,-0.5,0.422749,0.5,-0.5,0,0.5,-0.3,0.3,0.5,-0.3,0.5,0.5,-0.422749,0.5,0.5,-0.422749,0.3,0.5,-0.3,0.3,0.5,-0,0.5,0.5,0.422749,0.5,0.5,0.422749,0.3,0.5,-0,0.3,0.5,0.3,0.422749,0.5,0.5,0,0.5,0.5,0,0.5,0.3,-0.422749,0.5,0.5,-0.422749,0.5,0.5,0,0.5,0.3,-0.3,0.5,0.3,-0.5,0.5,0.422749,0,-0.5,0.5,0,-0.5,0.3,0.422749,-0.5,0.5,0.422749,-0.5,0.5,0,-0.5,0.3,0.3,-0.5,0.3,0.5,-0.5,0.422749,0.5,-0.5,0.422749,0.3,-0.5,0.3,0.3,-0.5,0,0.5,-0.5,-0.422749,0.5,-0.5,-0.422749,0.3,-0.5,0,0.3,-0.5,-0.3,0.422749,-0.5,-0.5,0,-0.5,-0.5,0,-0.5,-0.3,-0.422749,-0.5,-0.5,-0.422749,-0.5,-0.5,0,-0.5,-0.3,-0.3,-0.5,-0.3,-0.5,-0.5,-0.422749,-0.5,-0.5,-0.422749,-0.3,-0.5,-0.3,-0.3,-0.5,0,-0.5,-0.5,0.422749,-0.5,-0.5,0.422749,-0.3,-0.5,0,-0.3,-0.5,0.3,-0.422749,-0.5,0.5,0.3,-0.3,-0.3,0.3,-0.3,-0.479966,0,-0.3,-0.479966,0,-0.3,-0.3,-0.3,-0.3,-0.3,-0.3,-0.3,-0.479966,-0.3,-0,-0.479966,-0.3,-0,-0.3,-0.3,0.3,-0.3,-0.3,0.3,-0.479966,0,0.3,-0.479966,0,0.3,-0.3,0.3,0.3,-0.3,0.3,0.3,-0.479966,0.3,-0,-0.479966,0.3,-0,-0.3,0,-0.3,-0.3,0,-0.3,-0.479966,-0.3,-0.3,-0.479966,-0.3,-0.3,-0.3,0.3,-0,-0.3,0.3,-0,-0.479966,0.3,-0.3,-0.479966,0.3,-0.3,-0.3,0,0.3,-0.3,0,0.3,-0.479966,0.3,0.3,-0.479966,0.3,0.3,-0.3,-0.3,-0,-0.3,-0.3,-0,-0.479966,-0.3,0.3,-0.479966,-0.3,0.3,-0.3,-0.422749,0.5,0.5,-0.422749,0,0.5,-0.413625,0,0.479966,-0.413625,0.489208,0.479966,-0.5,0.5,-0.422749,-0.5,-0,-0.422749,-0.479966,-0,-0.413625,-0.479966,0.489208,-0.413625,-0.422749,-0.5,-0.5,-0.422749,-0,-0.5,-0.413625,-0,-0.479966,-0.413625,-0.489208,-0.479966,-0.479966,0.489208,0.413625,-0.479966,0.489208,-0.413625,-0.479966,-0,-0.413625,-0.479966,0,0.413625,-0.5,-0.5,0.422749,-0.5,0,0.422749,-0.479966,0,0.413625,-0.479966,-0.489208,0.413625,-0.422749,0.5,-0.5,-0.422749,-0,-0.5,-0.5,-0,-0.422749,-0.5,0.5,-0.422749,-0.422749,-0.5,0.5,-0.422749,0,0.5,-0.5,0,0.422749,-0.5,-0.5,0.422749,0.5,-0.5,-0.422749,0.5,-0,-0.422749,0.479966,-0,-0.413625,0.479966,-0.489208,-0.413625,0.5,0.5,0.422749,0.5,0,0.422749,0.479966,0,0.413625,0.479966,0.489208,0.413625,0.422749,-0.5,0.5,0.422749,0,0.5,0.413625,0,0.479966,0.413625,-0.489208,0.479966,0.479966,0.489208,-0.413625,0.479966,0.489208,0.413625,0.479966,0,0.413625,0.479966,-0,-0.413625,0.422749,0.5,0.5,0.422749,0,0.5,0.5,0,0.422749,0.5,0.5,0.422749,0.5,0.5,-0.422749,0.5,-0,-0.422749,0.422749,-0,-0.5,0.422749,0.5,-0.5,0.422749,0.5,-0.5,0.422749,-0,-0.5,0.413625,-0,-0.479966,0.413625,0.489208,-0.479966,-0.413625,0,0.479966,-0.413625,-0.489208,0.479966,-0.3,-0.3,0.479966,-0.3,0,0.479966,-0.413625,-0.489208,0.479966,0,-0.489208,0.479966,0,-0.3,0.479966,-0.3,-0.3,0.479966,0,-0.489208,0.479966,0.413625,-0.489208,0.479966,0.3,-0.3,0.479966,0,-0.3,0.479966,0.413625,-0.489208,0.479966,0.413625,0,0.479966,0.3,0,0.479966,0.3,-0.3,0.479966,0.413625,0,0.479966,0.413625,0.489208,0.479966,0.3,0.3,0.479966,0.3,0,0.479966,0.413625,0.489208,0.479966,0,0.489208,0.479966,0,0.3,0.479966,0.3,0.3,0.479966,0,0.489208,0.479966,-0.413625,0.489208,0.479966,-0.3,0.3,0.479966,0,0.3,0.479966,-0.413625,0.489208,0.479966,-0.413625,0,0.479966,-0.3,0,0.479966,-0.3,0.3,0.479966,-0.413625,-0,-0.479966,-0.413625,0.489208,-0.479966,-0.3,0.3,-0.479966,-0.3,-0,-0.479966,-0.413625,0.489208,-0.479966,0,0.489208,-0.479966,0,0.3,-0.479966,-0.3,0.3,-0.479966,0,0.489208,-0.479966,0.413625,0.489208,-0.479966,0.3,0.3,-0.479966,0,0.3,-0.479966,0.413625,0.489208,-0.479966,0.413625,-0,-0.479966,0.3,-0,-0.479966,0.3,0.3,-0.479966,0.413625,-0,-0.479966,0.413625,-0.489208,-0.479966,0.3,-0.3,-0.479966,0.3,-0,-0.479966,0.413625,-0.489208,-0.479966,0,-0.489208,-0.479966,0,-0.3,-0.479966,0.3,-0.3,-0.479966,0,-0.489208,-0.479966,-0.413625,-0.489208,-0.479966,-0.3,-0.3,-0.479966,0,-0.3,-0.479966,-0.413625,-0.489208,-0.479966,-0.413625,-0,-0.479966,-0.3,-0,-0.479966,-0.3,-0.3,-0.479966,-0.3,0,0.479966,-0.3,0,0.3,-0.3,0.3,0.3,-0.3,0.3,0.479966,-0.3,0,0,-0.3,-0,-0.3,-0.3,0.3,-0.3,-0.3,0.3,-0,0,0.3,0.479966,0,0.3,0.3,0.3,0.3,0.3,0.3,0.3,0.479966,0.3,0,0.479966,0.3,0,0.3,0.3,-0.3,0.3,0.3,-0.3,0.479966,0.3,0,0,0.3,-0,-0.3,0.3,-0.3,-0.3,0.3,-0.3,0,0,-0.3,0.479966,0,-0.3,0.3,-0.3,-0.3,0.3,-0.3,-0.3,0.479966,0.3,0.3,0.479966,0.3,0.3,0.3,0.3,0,0.3,0.3,0,0.479966,0.3,0.3,-0,0.3,0.3,-0.3,0.3,-0,-0.3,0.3,0,0,-0.3,0.3,0.479966,-0.3,0.3,0.3,0,0.3,0.3,0,0.3,0.479966,-0.3,0,0.3,-0.3,0,0,-0.3,0.3,-0,-0.3,0.3,0.3,-0.3,-0.3,0.479966,-0.3,-0.3,0.3,-0.3,0,0.3,-0.3,0,0.479966,-0.3,-0.3,0,-0.3,-0.3,-0.3,-0.3,-0,-0.3,-0.3,0,0,0.3,-0.3,0.479966,0.3,-0.3,0.3,0,-0.3,0.3,0,-0.3,0.479966,-0.3,-0.3,0.3,-0.3,-0.3,0,-0.3,0,0,-0.3,0,0.3,0.3,0.3,0.3,0.3,0.3,-0,0.3,0,0,0.3,0,0.3,0.3,0,0.3,0.3,0,0,0.3,-0.3,0,0.3,-0.3,0.3,-0.3,0.5,-0,-0.3,0.5,0.3,-0.3,0.3,0.3,-0.3,0.3,-0,-0.3,0.5,0.3,0,0.5,0.3,0,0.3,0.3,-0.3,0.3,0.3,0,0.5,0.3,0.3,0.5,0.3,0.3,0.3,0.3,0,0.3,0.3,0.3,0.5,0.3,0.3,0.5,-0,0.3,0.3,-0,0.3,0.3,0.3,0.3,0.5,-0,0.3,0.5,-0.3,0.3,0.3,-0.3,0.3,0.3,-0,0.3,0.5,-0.3,0,0.5,-0.3,0,0.3,-0.3,0.3,0.3,-0.3,0,0.5,-0.3,-0.3,0.5,-0.3,-0.3,0.3,-0.3,0,0.3,-0.3,-0.3,0.5,-0.3,-0.3,0.5,-0,-0.3,0.3,-0,-0.3,0.3,-0.3,-0.3,-0.5,0,-0.3,-0.5,-0.3,-0.3,-0.3,-0.3,-0.3,-0.3,0,-0.3,-0.5,-0.3,0,-0.5,-0.3,0,-0.3,-0.3,-0.3,-0.3,-0.3,0,-0.5,-0.3,0.3,-0.5,-0.3,0.3,-0.3,-0.3,0,-0.3,-0.3,0.3,-0.5,-0.3,0.3,-0.5,0,0.3,-0.3,0,0.3,-0.3,-0.3,0.3,-0.5,0,0.3,-0.5,0.3,0.3,-0.3,0.3,0.3,-0.3,0,0.3,-0.5,0.3,0,-0.5,0.3,0,-0.3,0.3,0.3,-0.3,0.3,0,-0.5,0.3,-0.3,-0.5,0.3,-0.3,-0.3,0.3,0,-0.3,0.3,-0.3,-0.5,0.3,-0.3,-0.5,0,-0.3,-0.3,0,-0.3,-0.3,0.3], 3 | "metadata":{ 4 | "vertices":460, 5 | "faces":116, 6 | "generator":"io_three", 7 | "version":3, 8 | "type":"Geometry", 9 | "uvs":0, 10 | "materials":2, 11 | "normals":29 12 | }, 13 | "faces":[35,0,1,2,3,0,0,0,0,0,35,4,5,6,7,0,1,1,1,1,35,8,9,10,11,0,2,2,2,2,35,12,13,14,15,0,3,3,3,3,35,16,17,18,19,0,4,4,4,4,35,20,21,22,23,0,5,5,5,5,35,24,25,26,27,0,6,6,6,6,35,28,29,30,31,0,7,7,7,7,35,32,33,34,35,1,8,8,8,8,35,36,37,38,39,1,9,9,9,9,35,40,41,42,43,0,10,10,10,10,35,44,45,46,47,0,11,11,11,11,35,48,49,50,51,0,12,12,12,12,35,52,53,54,55,0,13,13,13,13,35,56,57,58,59,0,14,14,14,14,35,60,61,62,63,0,15,15,15,15,35,64,65,66,67,0,16,16,16,16,35,68,69,70,71,0,17,17,17,17,35,72,73,74,75,0,18,18,18,18,35,76,77,78,79,0,19,19,19,19,35,80,81,82,83,0,20,20,20,20,35,84,85,86,87,0,21,21,21,21,35,88,89,90,91,0,0,0,0,0,35,92,93,94,95,0,22,22,22,22,35,96,97,98,99,0,2,2,2,2,35,100,101,102,103,0,22,22,22,22,35,104,105,106,107,0,17,17,17,17,35,108,109,110,111,0,23,23,23,23,35,112,113,114,115,0,13,13,13,13,35,116,117,118,119,0,23,23,23,23,35,120,121,122,123,0,23,23,23,23,35,124,125,126,127,0,23,23,23,23,34,128,129,130,0,23,23,23,35,131,132,133,134,0,23,23,23,23,35,135,136,137,138,0,23,23,23,23,35,139,140,141,142,0,23,23,23,23,34,143,144,145,0,23,23,23,35,146,147,148,149,0,23,23,23,23,34,150,151,152,0,22,22,22,35,153,154,155,156,0,22,22,22,22,35,157,158,159,160,0,22,22,22,22,35,161,162,163,164,0,22,22,22,22,34,165,166,167,0,22,22,22,35,168,169,170,171,0,22,22,22,22,35,172,173,174,175,0,22,22,22,22,35,176,177,178,179,0,22,22,22,22,35,180,181,182,183,0,23,23,23,23,35,184,185,186,187,0,24,8,8,8,35,188,189,190,191,0,22,22,22,22,35,192,193,194,195,0,9,9,9,9,35,196,197,198,199,0,23,23,23,23,35,200,201,202,203,0,9,9,9,9,35,204,205,206,207,0,22,22,22,22,35,208,209,210,211,0,8,8,8,8,35,212,213,214,215,0,18,18,18,18,35,216,217,218,219,0,14,14,14,14,35,220,221,222,223,0,10,10,10,10,35,224,225,226,227,1,9,9,9,9,35,228,229,230,231,0,7,7,7,7,35,232,233,234,235,0,4,4,4,4,35,236,237,238,239,0,3,3,3,3,35,240,241,242,243,0,20,20,20,20,35,244,245,246,247,0,16,16,16,16,35,248,249,250,251,0,12,12,12,12,35,252,253,254,255,1,8,8,8,8,35,256,257,258,259,0,6,6,6,6,35,260,261,262,263,0,5,5,5,5,35,264,265,266,267,0,1,1,1,1,35,268,269,270,271,0,25,25,25,25,35,272,273,274,275,0,25,25,25,25,35,276,277,278,279,0,25,25,25,25,35,280,281,282,283,0,25,25,25,25,35,284,285,286,287,0,25,25,25,25,35,288,289,290,291,0,25,25,25,25,35,292,293,294,295,0,25,25,25,25,35,296,297,298,299,0,25,25,25,25,35,300,301,302,303,0,26,26,26,26,35,304,305,306,307,0,26,26,26,26,35,308,309,310,311,0,26,26,26,26,35,312,313,314,315,0,26,26,26,26,35,316,317,318,319,0,26,26,26,26,35,320,321,322,323,0,26,26,26,26,35,324,325,326,327,0,26,26,26,26,35,328,329,330,331,0,26,26,26,26,35,332,333,334,335,0,8,8,8,8,35,336,337,338,339,0,8,24,8,8,35,340,341,342,343,0,22,22,22,22,35,344,345,346,347,0,9,27,9,9,35,348,349,350,351,0,9,9,27,9,35,352,353,354,355,0,23,23,28,23,35,356,357,358,359,0,9,9,9,9,35,360,361,362,363,0,9,9,27,9,35,364,365,366,367,0,22,22,22,22,35,368,369,370,371,0,8,8,24,8,35,372,373,374,375,0,8,8,8,8,35,376,377,378,379,0,8,24,8,24,35,380,381,382,383,0,23,23,23,23,35,384,385,386,387,0,8,24,8,24,35,388,389,390,391,0,9,27,9,9,35,392,393,394,395,0,9,9,9,9,35,396,397,398,399,0,8,24,8,24,35,400,401,402,403,0,26,26,26,26,35,404,405,406,407,0,26,26,26,26,35,408,409,410,411,0,27,9,27,9,35,412,413,414,415,0,27,27,9,9,35,416,417,418,419,0,25,25,25,25,35,420,421,422,423,0,25,25,25,25,35,424,425,426,427,0,24,24,8,8,35,428,429,430,431,0,8,8,8,24,35,432,433,434,435,0,25,25,25,25,35,436,437,438,439,0,25,25,25,25,35,440,441,442,443,0,9,9,9,9,35,444,445,446,447,0,9,27,9,9,35,448,449,450,451,0,26,26,26,26,35,452,453,454,455,0,26,26,26,26,35,456,457,458,459,0,8,24,8,8], 14 | "uvs":[], 15 | "materials":[{ 16 | "DbgIndex":0, 17 | "DbgName":"stationary_frame", 18 | "specularCoef":50, 19 | "wireframe":false, 20 | "colorDiffuse":[0.5,0.5,0.5], 21 | "shading":"phong", 22 | "depthWrite":true, 23 | "DbgColor":15658734, 24 | "transparent":false, 25 | "opacity":1, 26 | "colorSpecular":[0.05102,0.05102,0.05102], 27 | "visible":true, 28 | "colorEmissive":[0,0,0], 29 | "depthTest":true, 30 | "blending":"NormalBlending" 31 | },{ 32 | "DbgIndex":1, 33 | "DbgName":"stationary_window", 34 | "specularCoef":50, 35 | "wireframe":false, 36 | "colorDiffuse":[0.038537,0.064176,0.106295], 37 | "shading":"phong", 38 | "depthWrite":true, 39 | "DbgColor":15597568, 40 | "transparent":false, 41 | "opacity":1, 42 | "colorSpecular":[0.377551,0.377551,0.377551], 43 | "visible":true, 44 | "colorEmissive":[0,0,0], 45 | "depthTest":true, 46 | "blending":"NormalBlending" 47 | }], 48 | "normals":[0,0.880367,-0.474227,-0.910031,0,-0.414472,0,0.880367,0.474227,-0.707083,0,0.707083,-0.707083,0,-0.707083,0.707083,0,-0.707083,0.707083,0,0.707083,-0.414472,0,-0.910031,1,0,0,-1,0,0,0.910031,0,-0.414472,0.474227,0.880367,0,-0.910031,0,0.414472,0,-0.880367,0.474227,-0.414472,0,0.910031,0.474227,-0.880367,0,0.414472,0,-0.910031,0,-0.880367,-0.474227,0.910031,0,0.414472,-0.474227,-0.880367,0,0.414472,0,0.910031,-0.474227,0.880367,0,0,-1,0,0,1,0,0.999969,0,0,0,0,1,0,0,-1,-0.999969,0,0,0,0.999969,0], 49 | "name":"Cube.000Geometry.1" 50 | } -------------------------------------------------------------------------------- /src/inputmapper.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | /** 4 | * Mapper that automatically maps keyboard / gamepad input to different player numbers. 5 | * This can be used to implement keyboard / gamepad controls for a single player or a local 6 | * multiplayer game that allows players on the keyboard to play against players on gamepads. 7 | * Requires gamepad.js, mousetrap.js and mousetrap-global-bind.js to be included. 8 | * @constructor 9 | * @param {Object} callbackObj Object on which the callback functions will be called. 10 | * @param {number} maxPlayers Maximum number of players. If there are more active controllers 11 | * than this, then two controllers may be mapped to the same player. 12 | */ 13 | var InputMapper = function(callbackObj, maxPlayers) { 14 | this.gamepads = new Gamepad(this); 15 | this.callbackObj = callbackObj; 16 | this.maxPlayers = maxPlayers; 17 | this.resetPlayerMap(); 18 | this.keysDown = []; // Keyboard keys that are currently down 19 | this.callbacks = []; // Callback information for mapping callbacks back to buttons 20 | this.upCallbacksForKey = {}; // Map from keys to lists of callbacks, so each key can have multiple callbacks 21 | this.downCallbacksForKey = {}; // Map from keys to lists of callbacks, so each key can have multiple callbacks 22 | this._defaultController = new InputMapper.Controller(InputMapper.GAMEPAD, 0); 23 | }; 24 | 25 | // Controller types 26 | InputMapper.GAMEPAD = 0; 27 | InputMapper.KEYBOARD = 1; 28 | 29 | /** 30 | * Helper class to store the controller config each player has. 31 | * @constructor 32 | * @param {number} controllerType Controller type: either InputMapper.GAMEPAD or InputMapper.KEYBOARD 33 | * @param {number} controllerIndex Controller index: in case of keyboard, index into the array of keyboard keys given 34 | * to addListener. In case of gamepad, index of the gamepad. 35 | */ 36 | InputMapper.Controller = function(controllerType, controllerIndex) { 37 | this.controllerType = controllerType; 38 | this.controllerIndex = controllerIndex; 39 | this.lastUsed = 0; // A timestamp for when this controller was last used 40 | }; 41 | 42 | /** 43 | * Reset the map between controllers and player numbers. 44 | * @param {number?} maxPlayers Maximum player count. Default is to keep existing value. 45 | */ 46 | InputMapper.prototype.resetPlayerMap = function(maxPlayers) { 47 | if (maxPlayers !== undefined) { 48 | this.maxPlayers = maxPlayers; 49 | } 50 | this.players = []; // An array of arrays of controllers. Each player can have multiple controllers. 51 | for (var i = 0; i < this.maxPlayers; ++i) { 52 | this.players.push([]); 53 | } 54 | }; 55 | 56 | /** 57 | * Update the controller state and call listeners based on that. 58 | */ 59 | InputMapper.prototype.update = function() { 60 | this.gamepads.update(); 61 | }; 62 | 63 | /** 64 | * Return a player index for a player using a given controller. 65 | * @param {number} controllerType Controller type: either InputMapper.GAMEPAD or InputMapper.KEYBOARD 66 | * @param {number} controllerIndex Controller index: in case of keyboard, index into the array of keyboard keys given 67 | * to addListener. In case of gamepad, index of the gamepad. 68 | */ 69 | InputMapper.prototype.getPlayerIndex = function(controllerType, controllerIndex) { 70 | for (var i = 0; i < this.players.length; ++i) { 71 | var player = this.players[i]; 72 | for (var j = 0; j < player.length; ++j) { 73 | if (player[j].controllerType == controllerType && player[j].controllerIndex == controllerIndex) { 74 | player[j].lastUsed = Date.now(); 75 | return i; 76 | } 77 | } 78 | } 79 | var controller = new InputMapper.Controller(controllerType, controllerIndex); 80 | controller.lastUsed = Date.now(); 81 | // Map the controller for a player without a controller if there is one 82 | for (var i = 0; i < this.players.length; ++i) { 83 | var player = this.players[i]; 84 | if (player.length === 0) { 85 | player.push(controller); 86 | return i; 87 | } 88 | } 89 | // Map the controller for the first player without this type of a controller 90 | for (var i = 0; i < this.players.length; ++i) { 91 | var player = this.players[i]; 92 | var hasSameTypeController = false; 93 | for (var j = 0; j < player.length; ++j) { 94 | if (player[j].controllerType == controllerType) { 95 | hasSameTypeController = true; 96 | } 97 | } 98 | if (!hasSameTypeController) { 99 | player.push(controller); 100 | return i; 101 | } 102 | } 103 | // Just map the controller for the first player 104 | this.players[0].push(controller); 105 | return 0; 106 | }; 107 | 108 | /** 109 | * @param {number} gamepadButton A button from Gamepad.BUTTONS 110 | * @param {Array} keyboardBindings List of bindings for different players, for example ['up', 'w'] 111 | * @param {function=} downCallback Callback when the button is pressed down, that takes a player number as a parameter. 112 | * @param {function=} upCallback Callback when the button is released, that takes a player number as a parameter. 113 | */ 114 | InputMapper.prototype.addListener = function(gamepadButton, keyboardButtons, downCallback, upCallback) { 115 | var gamepadDownCallback = function(gamepadNumber) { 116 | var player = this.getPlayerIndex(InputMapper.GAMEPAD, gamepadNumber); 117 | if (downCallback !== undefined) { 118 | downCallback.call(this.callbackObj, player); 119 | } 120 | }; 121 | var gamepadUpCallback = function(gamepadNumber) { 122 | var player = this.getPlayerIndex(InputMapper.GAMEPAD, gamepadNumber); 123 | if (upCallback !== undefined) { 124 | upCallback.call(this.callbackObj, player); 125 | } 126 | }; 127 | this.gamepads.addButtonChangeListener(gamepadButton, gamepadDownCallback, gamepadUpCallback); 128 | 129 | var gamepadInstruction; 130 | 131 | if (gamepadButton < 100) { 132 | gamepadInstruction = Gamepad.BUTTON_INSTRUCTION[gamepadButton]; 133 | } else { 134 | gamepadInstruction = Gamepad.BUTTON_INSTRUCTION[gamepadButton - 100]; 135 | } 136 | 137 | if (downCallback !== undefined) { 138 | this.callbacks.push({key: gamepadInstruction, callback: downCallback, controllerType: InputMapper.GAMEPAD}); 139 | } 140 | if (upCallback !== undefined) { 141 | this.callbacks.push({key: gamepadInstruction, callback: upCallback, controllerType: InputMapper.GAMEPAD}); 142 | } 143 | 144 | var that = this; 145 | for (var i = 0; i < keyboardButtons.length; ++i) { 146 | (function(kbIndex) { 147 | if (!that.downCallbacksForKey.hasOwnProperty(keyboardButtons[kbIndex])) { 148 | that.keysDown[keyboardButtons[kbIndex]] = false; 149 | that.downCallbacksForKey[keyboardButtons[kbIndex]] = []; 150 | that.upCallbacksForKey[keyboardButtons[kbIndex]] = []; 151 | var keyDownCallback = function(e) { 152 | var player = that.getPlayerIndex(InputMapper.KEYBOARD, kbIndex); 153 | // Down events get generated multiple times while a key is down. Work around this. 154 | if (!that.keysDown[keyboardButtons[kbIndex]]) { 155 | that.keysDown[keyboardButtons[kbIndex]] = true; 156 | var callbacksToCall = that.downCallbacksForKey[keyboardButtons[kbIndex]]; 157 | for (var i = 0; i < callbacksToCall.length; ++i) { 158 | callbacksToCall[i].call(that.callbackObj, player); 159 | } 160 | } 161 | e.preventDefault(); 162 | }; 163 | var keyUpCallback = function(e) { 164 | var player = that.getPlayerIndex(InputMapper.KEYBOARD, kbIndex); 165 | that.keysDown[keyboardButtons[kbIndex]] = false; 166 | var callbacksToCall = that.upCallbacksForKey[keyboardButtons[kbIndex]]; 167 | for (var i = 0; i < callbacksToCall.length; ++i) { 168 | callbacksToCall[i].call(that.callbackObj, player); 169 | } 170 | e.preventDefault(); 171 | }; 172 | window.Mousetrap.bindGlobal(keyboardButtons[kbIndex], keyDownCallback, 'keydown'); 173 | window.Mousetrap.bindGlobal(keyboardButtons[kbIndex], keyUpCallback, 'keyup'); 174 | } 175 | if (downCallback !== undefined) { 176 | that.downCallbacksForKey[keyboardButtons[kbIndex]].push(downCallback); 177 | } 178 | if (upCallback !== undefined) { 179 | that.upCallbacksForKey[keyboardButtons[kbIndex]].push(upCallback); 180 | } 181 | })(i); 182 | if (downCallback !== undefined) { 183 | this.callbacks.push({key: keyboardButtons[i], callback: downCallback, controllerType: InputMapper.KEYBOARD, kbIndex: i}); 184 | } 185 | if (upCallback !== undefined) { 186 | this.callbacks.push({key: keyboardButtons[i], callback: upCallback, controllerType: InputMapper.KEYBOARD, kbIndex: i}); 187 | } 188 | } 189 | }; 190 | 191 | /** 192 | * Check if a given callback uses a given type of controller. Doesn't care about gamepad indices. 193 | * @protected 194 | * @param {InputMapper.Controller} controller 195 | * @param {Object} cbInfo Information on the callback, with keys controllerType and kbIndex in case of a keyboard. 196 | * @return {boolean} True if the given callback uses the given type of a controller. 197 | */ 198 | InputMapper._usesController = function(controller, cbInfo) { 199 | if (cbInfo.controllerType === controller.controllerType) { 200 | if (cbInfo.controllerType === InputMapper.KEYBOARD && controller.controllerIndex !== cbInfo.kbIndex) { 201 | // Each keyboard "controller" has different key bindings. 202 | return false; 203 | } 204 | return true; 205 | } 206 | }; 207 | 208 | /** 209 | * From an array of controllers, determine the one that was most recently used. 210 | * @protected 211 | * @param {Array.} player Array of controllers to check. 212 | * @return {InputMapper.Controller} The most recently used controller. 213 | */ 214 | InputMapper.prototype._getLastUsedController = function(player) { 215 | var controller; 216 | var lastUsed = 0; 217 | for (var j = 0; j < player.length; ++j) { 218 | if (player[j].lastUsed > lastUsed) { 219 | controller = player[j]; 220 | lastUsed = player[j].lastUsed; 221 | } 222 | } 223 | return controller; 224 | }; 225 | 226 | /** 227 | * Cycle the controller that is used for showing instructions by default. 228 | */ 229 | InputMapper.prototype.cycleDefaultControllerForInstruction = function() { 230 | if (this._defaultController.controllerType === InputMapper.KEYBOARD) { 231 | this._defaultController = new InputMapper.Controller(InputMapper.GAMEPAD, 0); 232 | } else { 233 | this._defaultController = new InputMapper.Controller(InputMapper.KEYBOARD, 0); 234 | } 235 | }; 236 | 237 | /** 238 | * Get instruction for a key. Prioritizes gamepad over keyboard if keyboard hasn't been used. If you want to change the 239 | * controller which is prioritized, call cycleDefaultControllerForInstruction(). 240 | * @param {function} callback A callback that has been previously attached to a button. 241 | * @param {playerIndex} index of the player to return information for. Set to undefined if the listener doesn't care 242 | * about the player number. 243 | * @return {string} String identifying the button for the player. 244 | */ 245 | InputMapper.prototype.getKeyInstruction = function(callback, playerIndex) { 246 | var controller; 247 | if (playerIndex !== undefined && this.players.length > playerIndex) { 248 | if (this.players[playerIndex].length > 0) { 249 | controller = this._getLastUsedController(this.players[playerIndex]); 250 | } else { 251 | // Gamepad instructions by default 252 | controller = this._defaultController; 253 | } 254 | } 255 | var returnStr = []; 256 | for (var i = 0; i < this.callbacks.length; ++i) { 257 | var cbInfo = this.callbacks[i]; 258 | if (cbInfo.callback === callback) { 259 | if (controller === undefined) { 260 | // Listener doesn't care about the player number. 261 | // Determine all keys mapped to that callback from different controllers. 262 | for (var j = 0; j < this.players.length; ++j) { 263 | for (var k = 0; k < this.players[j].length; ++k) { 264 | if (InputMapper._usesController(this.players[j][k], cbInfo)) { 265 | var hasInstruction = false; 266 | var instruction = cbInfo.key.toUpperCase(); 267 | for (var l = 0; l < returnStr.length; ++l) { 268 | if (returnStr[l] == instruction) { 269 | hasInstruction = true; 270 | } 271 | } 272 | if (!hasInstruction) { 273 | returnStr.push(instruction); 274 | } 275 | } 276 | } 277 | } 278 | } else { 279 | if (InputMapper._usesController(controller, cbInfo)) { 280 | return cbInfo.key.toUpperCase(); 281 | } 282 | } 283 | } 284 | } 285 | if (controller === undefined) { 286 | return returnStr.join('/'); 287 | } 288 | return ''; 289 | }; 290 | -------------------------------------------------------------------------------- /src/sprite.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | if (typeof GJS === "undefined") { 4 | var GJS = {}; 5 | } 6 | 7 | /** 8 | * A sprite that can be drawn on a 2D canvas. 9 | * @constructor 10 | * @param {string|HTMLImageElement|HTMLCanvasElement|GJS.Sprite} filename File to load, a graphical element that's already 11 | * loaded, or another GJS.Sprite. 12 | * @param {string=} filter Filter function to convert the sprite, for example GJS.Sprite.turnSolidColored('black') 13 | * @param {string|HTMLImageElement|HTMLCanvasElement=} fallback Fallback file to load or a graphical element that's 14 | * already loaded. 15 | */ 16 | GJS.Sprite = function(filename, /* Optional */ filter, fallback) { 17 | this.filename = filename; 18 | this.missing = false; 19 | this.fallback = fallback; 20 | this.filter = filter; 21 | GJS.Sprite.createdCount++; 22 | this.loadedListeners = []; 23 | this.implementsGameutilsSprite = true; 24 | this._reload(); 25 | }; 26 | 27 | GJS.Sprite.prototype.addLoadedListener = function(callback) { 28 | if (this.loaded) { 29 | callback(); 30 | } 31 | else { 32 | this.loadedListeners.push(callback); 33 | } 34 | }; 35 | 36 | GJS.Sprite.prototype._callLoadedListeners = function() { 37 | for (var i = 0; i < this.loadedListeners.length; ++i) { 38 | this.loadedListeners[i](); 39 | } 40 | }; 41 | 42 | /** 43 | * Reload the GJS.Sprite. 44 | * @protected 45 | */ 46 | GJS.Sprite.prototype._reload = function() { 47 | if (typeof this.filename != typeof '') { 48 | this.img = this.filename; 49 | if (this.img instanceof GJS.Sprite) { 50 | var that = this; 51 | this.img.addLoadedListener(function() { 52 | that.filename = that.img.img; 53 | that._reload(); 54 | }); 55 | return; 56 | } 57 | this.loaded = true; 58 | GJS.Sprite.loadedCount++; 59 | this.width = this.img.width; 60 | this.height = this.img.height; 61 | if (this.filter !== undefined) { 62 | this.filter(this); 63 | } 64 | } else { 65 | this.img = document.createElement('img'); 66 | if (this.filename.substring(0, 5) === 'data:' || this.filename.substring(0, 5) === 'http:') { 67 | this.img.src = this.filename; 68 | } else { 69 | this.img.src = GJS.Sprite.gfxPath + this.filename; 70 | } 71 | var that = this; 72 | this.loaded = false; 73 | this.img.onload = function() { 74 | that.loaded = true; 75 | GJS.Sprite.loadedCount++; 76 | that.width = that.img.width; 77 | that.height = that.img.height; 78 | if (that.filter !== undefined) { 79 | that.filter(that); 80 | } 81 | that._callLoadedListeners(); 82 | }; 83 | this.img.onerror = function() { 84 | if (that.fallback) { 85 | that.filename = that.fallback; 86 | that.fallback = undefined; 87 | that._reload(); 88 | return; 89 | } 90 | that.loaded = true; 91 | that.missing = true; 92 | GJS.Sprite.loadedCount++; 93 | that.img = document.createElement('canvas'); 94 | that.img.width = 150; 95 | that.img.height = 20; 96 | that.width = that.img.width; 97 | that.height = that.img.height; 98 | var ctx =that.img.getContext('2d'); 99 | ctx.textBaseline = 'top'; 100 | ctx.fillStyle = '#fff'; 101 | ctx.fillRect(0, 0, ctx.canvas.width, ctx.canvas.height); 102 | ctx.fillStyle = '#000'; 103 | ctx.fillText('Missing: ' + that.filename, 0, 0); 104 | that._callLoadedListeners(); 105 | }; 106 | } 107 | }; 108 | 109 | /** 110 | * Path for graphics files. Set this before creating any GJS.Sprite objects. 111 | */ 112 | GJS.Sprite.gfxPath = 'assets/gfx/'; 113 | 114 | /** 115 | * Filter for turning the sprite solid colored. 116 | */ 117 | GJS.Sprite.turnSolidColored = function(solidColor) { 118 | return function(sprite) { 119 | var canvas = document.createElement('canvas'); 120 | canvas.width = sprite.width; 121 | canvas.height = sprite.height; 122 | var ctx = canvas.getContext('2d'); 123 | ctx.fillStyle = solidColor; 124 | ctx.fillRect(0, 0, canvas.width, canvas.height); 125 | ctx.globalCompositeOperation = 'destination-in'; 126 | sprite.draw(ctx, 0, 0); 127 | sprite.img = canvas; 128 | }; 129 | }; 130 | 131 | /** 132 | * Filter for generating a different hued variation of the sprite. 133 | */ 134 | GJS.Sprite.varyHue = function(options) { 135 | var defaults = { 136 | minHue: 0, 137 | maxHue: 1, 138 | hueChange: 0 139 | }; 140 | for(var key in defaults) { 141 | if(!options.hasOwnProperty(key)) { 142 | options[key] = defaults[key]; 143 | } 144 | } 145 | while (options.hueChange < 0) { 146 | options.hueChange += 1; 147 | } 148 | while (options.hueChange > 1) { 149 | options.hueChange -= 1; 150 | } 151 | return function(sprite) { 152 | var canvas = document.createElement('canvas'); 153 | canvas.width = sprite.width; 154 | canvas.height = sprite.height; 155 | var ctx = canvas.getContext('2d'); 156 | sprite.draw(ctx, 0, 0); 157 | try { 158 | var data = ctx.getImageData(0, 0, canvas.width, canvas.height); 159 | } catch (e) { 160 | if (e.name == 'SecurityError') { 161 | if (!GJS.Sprite.reportedSecurityError) { 162 | GJS.Sprite.reportedSecurityError = true; 163 | console.log(e.message); 164 | } 165 | return; 166 | } 167 | } 168 | for (var i = 0; i < data.data.length; i += 4) { 169 | var r = data.data[i]; 170 | var g = data.data[i + 1]; 171 | var b = data.data[i + 2]; 172 | var hsl = rgbToHsl(r, g, b); 173 | if (hsl[0] >= options.minHue && hsl[0] <= options.maxHue) { 174 | hsl[0] += options.hueChange; 175 | if (hsl[0] > 1.0) { 176 | hsl[0] -= 1.0; 177 | } 178 | var rgb = hslToRgb(hsl[0], hsl[1], hsl[2]); 179 | data.data[i] = rgb[0]; 180 | data.data[i + 1] = rgb[1]; 181 | data.data[i + 2] = rgb[2]; 182 | } 183 | } 184 | ctx.putImageData(data, 0, 0); 185 | sprite.img = canvas; 186 | }; 187 | }; 188 | 189 | /** 190 | * Filter for generating a variation of the GJS.Sprite with colors replaced with others. 191 | * @param {Object} paletteMap A mapping from RGB source color values to target values. Source colors should be strings 192 | * in "R, G, B" format. Target colors should be arrays [R, G, B]. In both cases the scale is 0-255. 193 | * @param {number=} tolerance Tolerance for detecting source colors. 194 | */ 195 | GJS.Sprite.repalette = function(paletteMap, tolerance) { 196 | if (tolerance === undefined) { 197 | tolerance = 10; 198 | } 199 | var palette = []; 200 | for (var key in paletteMap) { 201 | if (paletteMap.hasOwnProperty(key)) { 202 | var sourceRgb = key.split(', '); 203 | palette.push({ 204 | r: sourceRgb[0], 205 | g: sourceRgb[1], 206 | b: sourceRgb[2], 207 | target: paletteMap[key] 208 | }); 209 | } 210 | } 211 | return function(sprite) { 212 | var canvas = document.createElement('canvas'); 213 | canvas.width = sprite.width; 214 | canvas.height = sprite.height; 215 | var ctx = canvas.getContext('2d'); 216 | sprite.draw(ctx, 0, 0); 217 | var data = ctx.getImageData(0, 0, canvas.width, canvas.height); 218 | for (var i = 0; i < data.data.length; i += 4) { 219 | var r = data.data[i]; 220 | var g = data.data[i + 1]; 221 | var b = data.data[i + 2]; 222 | for (var j = 0; j < palette.length; ++j) { 223 | if (Math.abs(r - palette[j].r) < tolerance && 224 | Math.abs(g - palette[j].g) < tolerance && 225 | Math.abs(b - palette[j].b) < tolerance) 226 | { 227 | var rgb = palette[j].target; 228 | data.data[i] = rgb[0]; 229 | data.data[i + 1] = rgb[1]; 230 | data.data[i + 2] = rgb[2]; 231 | } 232 | } 233 | } 234 | ctx.putImageData(data, 0, 0); 235 | sprite.img = canvas; 236 | }; 237 | }; 238 | 239 | GJS.Sprite.reportedSecurityError = false; 240 | 241 | /** 242 | * How many GJS.Sprite objects have been created. 243 | */ 244 | GJS.Sprite.createdCount = 0; 245 | /** 246 | * How many GJS.Sprite objects have been fully loaded. 247 | */ 248 | GJS.Sprite.loadedCount = 0; 249 | 250 | /** 251 | * @return {number} Amount of GJS.Sprite objects that have been fully loaded per amount that has been created. 252 | * Name specified as string to support Closure compiler together with loadingbar.js. 253 | */ 254 | GJS.Sprite['loadedFraction'] = function() { 255 | if (GJS.Sprite.createdCount === 0) { 256 | return 1.0; 257 | } 258 | return GJS.Sprite.loadedCount / GJS.Sprite.createdCount; 259 | }; 260 | 261 | /** 262 | * Draw this to the given 2D canvas. 263 | * @param {CanvasRenderingContext2D} ctx 264 | * @param {number} leftX X coordinate of the left edge. 265 | * @param {number} topY Y coordinate of the top edge. 266 | */ 267 | GJS.Sprite.prototype.draw = function(ctx, leftX, topY) { 268 | if (this.loaded) { 269 | ctx.drawImage(this.img, leftX, topY); 270 | } 271 | }; 272 | 273 | /** 274 | * Draw the sprite to the given 2D canvas. 275 | * @param {CanvasRenderingContext2D} ctx 276 | * @param {number} centerX X coordinate of the center of the sprite on the canvas. 277 | * @param {number} centerY Y coordinate of the center of the sprite on the canvas. 278 | * @param {number} angleRadians Angle to rotate the sprite with (relative to its center). 279 | * @param {number} scale Scale to scale the sprite with (relative to its center). 280 | */ 281 | GJS.Sprite.prototype.drawRotated = function(ctx, centerX, centerY, angleRadians, /* optional */ scale) { 282 | if (!this.loaded) { 283 | return; 284 | } 285 | if (angleRadians === undefined) { 286 | angleRadians = 0.0; 287 | } 288 | if (scale === undefined) { 289 | scale = 1.0; 290 | } 291 | if (this.loaded) { 292 | ctx.save(); 293 | ctx.translate(centerX, centerY); 294 | ctx.rotate(angleRadians); 295 | ctx.scale(scale, scale); 296 | ctx.translate(-this.width * 0.5, -this.height * 0.5); 297 | ctx.drawImage(this.img, 0, 0); 298 | ctx.restore(); 299 | } 300 | }; 301 | 302 | /** 303 | * Draw the sprite to the given 2D canvas. 304 | * @param {CanvasRenderingContext2D} ctx 305 | * @param {number} centerX X coordinate of the center of the sprite on the canvas. 306 | * @param {number} centerY Y coordinate of the center of the sprite on the canvas. 307 | * @param {number} angleRadians Angle to rotate the sprite with (relative to its center). 308 | * @param {number} scaleX Scale to scale the sprite with along the x axis (relative to its center). 309 | * @param {number} scaleY Scale to scale the sprite with along the y axis (relative to its center). 310 | */ 311 | GJS.Sprite.prototype.drawRotatedNonUniform = function(ctx, centerX, centerY, angleRadians, scaleX, scaleY) { 312 | if (!this.loaded) { 313 | return; 314 | } 315 | if (angleRadians === undefined) { 316 | angleRadians = 0.0; 317 | } 318 | if (scaleX === undefined) { 319 | scaleX = 1.0; 320 | } 321 | if (scaleY === undefined) { 322 | scaleY = 1.0; 323 | } 324 | if (this.loaded) { 325 | ctx.save(); 326 | ctx.translate(centerX, centerY); 327 | ctx.rotate(angleRadians); 328 | ctx.scale(scaleX, scaleY); 329 | ctx.translate(-this.width * 0.5, -this.height * 0.5); 330 | ctx.drawImage(this.img, 0, 0); 331 | ctx.restore(); 332 | } 333 | }; 334 | 335 | /** 336 | * Fill the canvas with the sprite, preserving the sprite's aspect ratio, with the sprite centered on the canvas. 337 | * @param {CanvasRenderingContext2D} ctx 338 | */ 339 | GJS.Sprite.prototype.fillCanvas = function(ctx) { 340 | if (!this.loaded) { 341 | return; 342 | } 343 | var scale = Math.max(ctx.canvas.width / this.width, ctx.canvas.height / this.height); 344 | this.drawRotated(ctx, ctx.canvas.width * 0.5, ctx.canvas.height * 0.5, 0, scale); 345 | }; 346 | 347 | /** 348 | * Fill the canvas with the sprite, preserving the sprite's aspect ratio, with the sprite's bottom touching the bottom 349 | * of the canvas. 350 | * @param {CanvasRenderingContext2D} ctx 351 | */ 352 | GJS.Sprite.prototype.fillCanvasFitBottom = function(ctx) { 353 | if (!this.loaded) { 354 | return; 355 | } 356 | var scale = Math.max(ctx.canvas.width / this.width, ctx.canvas.height / this.height); 357 | this.drawRotated(ctx, ctx.canvas.width * 0.5, ctx.canvas.height - scale * this.height * 0.5, 0, scale); 358 | }; 359 | 360 | /** 361 | * Fill the canvas horizontally with the sprite, preserving the sprite's aspect ratio, with the sprite's bottom touching the bottom 362 | * of the canvas. 363 | * @param {CanvasRenderingContext2D} ctx 364 | */ 365 | GJS.Sprite.prototype.fillCanvasHorizontallyFitBottom = function(ctx) { 366 | if (!this.loaded) { 367 | return; 368 | } 369 | var scale = ctx.canvas.width / this.width; 370 | this.drawRotated(ctx, ctx.canvas.width * 0.5, ctx.canvas.height - scale * this.height * 0.5, 0, scale); 371 | }; 372 | 373 | /** 374 | * Just here to make GJS.Sprite and GJS.AnimatedSpriteInstance interchangeable. 375 | */ 376 | GJS.Sprite.prototype.update = function() { 377 | }; 378 | --------------------------------------------------------------------------------