diff --git a/app/assets/images/common/particles/bullet.png b/app/assets/images/common/particles/bullet.png new file mode 100644 index 000000000..8310ac5b9 Binary files /dev/null and b/app/assets/images/common/particles/bullet.png differ diff --git a/app/assets/images/common/particles/bullet2.png b/app/assets/images/common/particles/bullet2.png new file mode 100644 index 000000000..651fd98c2 Binary files /dev/null and b/app/assets/images/common/particles/bullet2.png differ diff --git a/app/assets/images/common/particles/cloud.png b/app/assets/images/common/particles/cloud.png new file mode 100644 index 000000000..c79d87506 Binary files /dev/null and b/app/assets/images/common/particles/cloud.png differ diff --git a/app/assets/images/common/particles/cloud_small.png b/app/assets/images/common/particles/cloud_small.png new file mode 100644 index 000000000..974e8ea21 Binary files /dev/null and b/app/assets/images/common/particles/cloud_small.png differ diff --git a/app/assets/images/common/particles/smoke.png b/app/assets/images/common/particles/smoke.png new file mode 100644 index 000000000..7bad8c685 Binary files /dev/null and b/app/assets/images/common/particles/smoke.png differ diff --git a/app/assets/images/common/particles/star.png b/app/assets/images/common/particles/star.png new file mode 100644 index 000000000..5e6f2a12f Binary files /dev/null and b/app/assets/images/common/particles/star.png differ diff --git a/app/core/ParticleMan.coffee b/app/core/ParticleMan.coffee new file mode 100644 index 000000000..a9fe982a4 --- /dev/null +++ b/app/core/ParticleMan.coffee @@ -0,0 +1,256 @@ +CocoClass = require 'core/CocoClass' +utils = require 'core/utils' + +module.exports = ParticleMan = class ParticleMan extends CocoClass + + constructor: -> + return @unsupported = true unless Modernizr.webgl + @renderer = new THREE.WebGLRenderer alpha: true + $(@renderer.domElement).addClass 'particle-man' + @scene = new THREE.Scene() + @clock = new THREE.Clock() + @particleGroups = [] + + destroy: -> + @detach() + # TODO: figure out how to dispose everything + # scene.remove(mesh) + # mesh.dispose() + # geometry.dispose() + # material.dispose() + # texture.dispose() + super() + + attach: (@$el) -> + return if @unsupported + width = @$el.innerWidth() + height = @$el.innerHeight() + @aspectRatio = width / height + @renderer.setSize(width, height) + @$el.append @renderer.domElement + @camera = camera = new THREE.OrthographicCamera( + 100 * -0.5, # Left + 100 * 0.5, # Right + 100 * 0.5 * @aspectRatio, # Top + 100 * -0.5 * @aspectRatio, # Bottom + 0, # Near frustrum distance + 1000 # Far frustrum distance + ) + @camera.position.set(0, 0, 100) + @camera.up = new THREE.Vector3(0, 1, 0) # http://stackoverflow.com/questions/14271672/moving-the-camera-lookat-and-rotations-in-three-js + @camera.lookAt new THREE.Vector3(0, 0, 0) + unless @started + @started = true + @render() + + detach: -> + return if @unsupported + @renderer.domElement.remove() + @started = false + + render: => + return if @unsupported + return if @destroyed + return unless @started + @renderer.render @scene, @camera + dt = @clock.getDelta() + for group in @particleGroups + group.tick dt + requestAnimationFrame @render + #@countFPS() + + countFPS: -> + @framesRendered ?= 0 + ++@framesRendered + @lastFPS ?= new Date() + now = new Date() + if now - @lastFPS > 1000 + console.log @framesRendered, 'fps with', @particleGroups.length, 'particle groups.' + @framesRendered = 0 + @lastFPS = now + + addEmitter: (x, y, kind="level-dungeon-premium") -> + return if @unsupported + console.log 'adding kind', kind + options = $.extend true, {}, particleKinds[kind] + options.group.texture = THREE.ImageUtils.loadTexture "/images/common/particles/#{options.group.texture}.png" + scale = 100 + aspectRatio = @$el + group = new SPE.Group options.group + group.mesh.position.x = scale * (-0.5 + x) + group.mesh.position.y = scale * (-0.5 + y) * @aspectRatio + emitter = new SPE.Emitter options.emitter + group.addEmitter emitter + @particleGroups.push group + @scene.add group.mesh + group + + removeEmitter: (group) -> + return if @unsupported + @scene.remove group.mesh + @particleGroups = _.without @particleGroups, group + + removeEmitters: -> + return if @unsupported + @removeEmitter group for group in @particleGroups.slice() + + #addTestCube: -> + #geometry = new THREE.BoxGeometry 5, 5, 5 + #material = new THREE.MeshLambertMaterial color: 0xFF0000 + #mesh = new THREE.Mesh geometry, material + #@scene.add mesh + #light = new THREE.PointLight 0xFFFF00 + #light.position.set 10, 0, 20 + #@scene.add light + + +hsl = (hue, saturation, lightness) -> + new THREE.Color utils.hslToHex([hue, saturation, lightness]) +vec = (x, y, z) -> + new THREE.Vector3 x, y, z + +defaults = + group: + texture: 'star' + maxAge: 4 + hasPerspective: 1 + colorize: 1 + transparent: 1 + alphaTest: 0.5 + depthWrite: false + depthTest: true + blending: THREE.NormalBlending + emitter: + type: "cube" + particleCount: 60 + position: vec 0, 0, 0 + positionSpread: vec 1, 0, 1 + acceleration: vec 0, -1, 0 + accelerationSpread: vec 0, 0, 0 + velocity: vec 0, 4, 0 + velocitySpread: vec 2, 2, 2 + sizeStart: 6 + sizeStartSpread: 0 + sizeMiddle: 4 + sizeMiddleSpread: 0 + sizeEnd: 2 + sizeEndSpread: 0 + angleStart: 0 + angleStartSpread: 0 + angleMiddle: 0 + angleMiddleSpread: 0 + angleEnd: 0 + angleEndSpread: 0 + angleAlignVelocity: false + colorStart: hsl 0.55, 0.75, 0.75 + colorStartSpread: vec 0.3, 0.3, 0.3 + colorMiddle: hsl 0.55, 0.6, 0.5 + colorMiddleSpread: vec 0.2, 0.2, 0.2 + colorEnd: hsl 0.55, 0.5, 0.25 + colorEndSpread: vec 0.1, 0.1, 0.1 + opacityStart: 1 + opacityStartSpread: 0 + opacityMiddle: 0.75 + opacityMiddleSpread: 0 + opacityEnd: 0.25 + opacityEndSpread: 0 + duration: null + alive: 1 + isStatic: 0 + +ext = (d, options) -> + $.extend true, {}, d, options ? {} + +particleKinds = + 'level-dungeon-premium': ext defaults + 'level-forest-premium': ext defaults, + emitter: + colorStart: hsl 0.56, 0.97, 0.5 + colorMiddle: hsl 0.56, 0.57, 0.5 + colorEnd: hsl 0.56, 0.17, 0.5 + 'level-desert-premium': ext defaults, + emitter: + colorStart: hsl 0.56, 0.97, 0.5 + colorMiddle: hsl 0.56, 0.57, 0.5 + colorEnd: hsl 0.56, 0.17, 0.5 + 'level-mountain-premium': ext defaults, + emitter: + colorStart: hsl 0.56, 0.97, 0.5 + colorMiddle: hsl 0.56, 0.57, 0.5 + colorEnd: hsl 0.56, 0.17, 0.5 + +particleKinds['level-dungeon-gate'] = ext particleKinds['level-dungeon-premium'], + emitter: + particleCount: 120 + velocity: vec 0, 6, 0 + colorStart: hsl 0.5, 0.75, 0.9 + colorMiddle: hsl 0.5, 0.75, 0.7 + colorEnd: hsl 0.5, 0.75, 0.3 + colorStartSpread: vec 1, 1, 1 + colorMiddleSpread: vec 1.5, 1.5, 1.5 + colorEndSpread: vec 2.5, 2.5, 2.5 + +particleKinds['level-dungeon-hero-ladder'] = ext particleKinds['level-dungeon-premium'], + emitter: + particleCount: 90 + velocity: vec 0, 4, 0 + colorStart: hsl 0, 0.75, 0.7 + colorMiddle: hsl 0, 0.75, 0.5 + colorEnd: hsl 0, 0.75, 0.3 + +particleKinds['level-forest-gate'] = ext particleKinds['level-forest-premium'], + emitter: + particleCount: 120 + velocity: vec 0, 8, 0 + colorStart: hsl 0.56, 0.97, 0.3 + colorMiddle: hsl 0.56, 0.57, 0.3 + colorEnd: hsl 0.56, 0.17, 0.3 + colorStartSpread: vec 1, 1, 1 + colorMiddleSpread: vec 1.5, 1.5, 1.5 + colorEndSpread: vec 2.5, 2.5, 2.5 + +particleKinds['level-forest-hero-ladder'] = ext particleKinds['level-forest-premium'], + emitter: + particleCount: 90 + velocity: vec 0, 4, 0 + colorStart: hsl 0, 0.95, 0.3 + colorMiddle: hsl 0, 1, 0.5 + colorEnd: hsl 0, 0.75, 0.1 + +particleKinds['level-desert-gate'] = ext particleKinds['level-desert-premium'], + emitter: + particleCount: 120 + velocity: vec 0, 8, 0 + colorStart: hsl 0.56, 0.97, 0.3 + colorMiddle: hsl 0.56, 0.57, 0.3 + colorEnd: hsl 0.56, 0.17, 0.3 + colorStartSpread: vec 1, 1, 1 + colorMiddleSpread: vec 1.5, 1.5, 1.5 + colorEndSpread: vec 2.5, 2.5, 2.5 + +particleKinds['level-desert-hero-ladder'] = ext particleKinds['level-desert-premium'], + emitter: + particleCount: 90 + velocity: vec 0, 4, 0 + colorStart: hsl 0, 0.95, 0.3 + colorMiddle: hsl 0, 1, 0.5 + colorEnd: hsl 0, 0.75, 0.1 + +particleKinds['level-dungeon-gate'] = ext particleKinds['level-dungeon-premium'], + emitter: + particleCount: 120 + velocity: vec 0, 8, 0 + colorStart: hsl 0.56, 0.97, 0.3 + colorMiddle: hsl 0.56, 0.57, 0.3 + colorEnd: hsl 0.56, 0.17, 0.3 + colorStartSpread: vec 1, 1, 1 + colorMiddleSpread: vec 1.5, 1.5, 1.5 + colorEndSpread: vec 2.5, 2.5, 2.5 + +particleKinds['level-dungeon-hero-ladder'] = ext particleKinds['level-dungeon-premium'], + emitter: + particleCount: 90 + velocity: vec 0, 4, 0 + colorStart: hsl 0, 0.95, 0.3 + colorMiddle: hsl 0, 1, 0.5 + colorEnd: hsl 0, 0.75, 0.1 diff --git a/app/models/User.coffee b/app/models/User.coffee index dbc84e732..ee537e1ae 100644 --- a/app/models/User.coffee +++ b/app/models/User.coffee @@ -123,6 +123,16 @@ module.exports = class User extends CocoModel application.tracker.identify gemPromptGroup: @gemPromptGroup unless me.isAdmin() @gemPromptGroup + getForeshadowsLevels: -> + return @foreshadowsLevels if @foreshadowsLevels? + group = me.get('testGroupNumber') % 16 + @foreshadowsLevels = switch group + when 0, 1, 2, 3, 4, 5, 6, 7 then true + when 8, 9, 10, 11, 12, 13, 14, 15 then false + @foreshadowsLevels = true if me.isAdmin() + application.tracker.identify foreshadowsLevels: @foreshadowsLevels unless me.isAdmin() + @foreshadowsLevels + getVideoTutorialStylesIndex: (numVideos=0)-> # A/B Testing video tutorial styles # Not a constant number of videos available (e.g. could be 0, 1, 3, or 4 currently) diff --git a/app/styles/common/common.sass b/app/styles/common/common.sass index 6fc7281bf..98f9ffee6 100644 --- a/app/styles/common/common.sass +++ b/app/styles/common/common.sass @@ -383,3 +383,10 @@ body > iframe[src^="https://apis.google.com"] @include scale(1.25) width: auto margin: 8px 15px 8px 15px + +.particle-man + position: absolute + z-index: 100 + top: 0 + left: 0 + pointer-events: none diff --git a/app/styles/play/campaign-view.sass b/app/styles/play/campaign-view.sass index ad1158ea5..915da4bd8 100644 --- a/app/styles/play/campaign-view.sass +++ b/app/styles/play/campaign-view.sass @@ -66,7 +66,7 @@ $gameControlMargin: 30px .level, .level-shadow position: absolute - border-radius: 100% + border-radius: 50% -webkit-transform: scaleY(0.75) transform: scaleY(0.75) @@ -130,16 +130,24 @@ $gameControlMargin: 30px &.started .glyphicon-star left: 0.5px - + img.hero-portrait width: 120% - position: absolute + height: auto bottom: 75% left: 75% - border: 1px solid black - border-radius: 100% - background: white + margin-left: 0 + margin-bottom: 0 + img.hero-portrait + position: absolute + border: 1px solid black + border-radius: 50% + background: white + width: $levelDotWidth * 1.5 + height: $levelDotHeight * 1.5 + margin-left: -0.5 * $levelDotWidth * 1.5 + margin-bottom: -$levelDotHeight / 3 * 1.5 .level-shadow z-index: 1 @@ -425,6 +433,9 @@ $gameControlMargin: 30px vertical-align: bottom margin: -20px 0 -6px 5px + .particle-man + z-index: 2 + body:not(.ipad) #campaign-view .level-info-container diff --git a/app/templates/play/campaign-view.jade b/app/templates/play/campaign-view.jade index 36568e89c..dc522cc25 100644 --- a/app/templates/play/campaign-view.jade +++ b/app/templates/play/campaign-view.jade @@ -8,8 +8,8 @@ each level in levels if !level.hidden div(style="left: #{level.position.x}%; bottom: #{level.position.y}%; background-color: #{level.color}", class="level" + (level.next ? " next" : "") + (level.disabled ? " disabled" : "") + (level.locked ? " locked" : "") + " " + levelStatusMap[level.slug] || "", data-level-slug=level.slug, data-level-original=level.original, title=i18n(level, 'name') + (level.disabled ? ' (Coming Soon to Adventurers)' : '')) - if level.unlocksHero && !level.unlockedHero - img.hero-portrait(src=level.unlocksHero.img) + if level.unlocksHero && (!level.purchasedHero || editorMode) + img.hero-portrait(src="/file/db/thang.type/#{level.unlocksHero}/portrait.png") a(href=level.type == 'hero' ? '#' : level.disabled ? "/play" : "/play/#{level.levelPath || 'level'}/#{level.slug}", disabled=level.disabled, data-level-slug=level.slug, data-level-path=level.levelPath || 'level', data-level-name=level.name) if level.requiresSubscription img.star(src="/images/pages/play/star.png") @@ -40,6 +40,8 @@ if isIPadApp && !level.disabled && !level.locked button.btn.btn-success.btn-lg.start-level(data-i18n="common.play") Play + else if level.unlocksHero && !level.purchasedHero + img.hero-portrait(src="/file/db/thang.type/#{level.unlocksHero}/portrait.png", style="left: #{level.position.x}%; bottom: #{level.position.y}%;") for adjacentCampaign in adjacentCampaigns a(href=(editorMode ? "/editor/campaign/" : "/play/") + adjacentCampaign.slug) diff --git a/app/views/play/CampaignView.coffee b/app/views/play/CampaignView.coffee index 12137f5a9..5866e59b7 100644 --- a/app/views/play/CampaignView.coffee +++ b/app/views/play/CampaignView.coffee @@ -13,6 +13,8 @@ AuthModal = require 'views/core/AuthModal' SubscribeModal = require 'views/core/SubscribeModal' Level = require 'models/Level' utils = require 'core/utils' +require 'vendor/three' +ParticleMan = require 'core/ParticleMan' trackedHourOfCode = false @@ -144,8 +146,10 @@ module.exports = class CampaignView extends RootView level.color = 'rgb(255, 80, 60)' if level.requiresSubscription level.color = 'rgb(80, 130, 200)' + if unlocksHero = _.find(level.rewards, 'hero')?.hero + level.unlocksHero = unlocksHero if level.unlocksHero - level.unlockedHero = level.unlocksHero.originalID in (me.get('earned')?.heroes or []) + level.purchasedHero = level.unlocksHero in (me.get('purchased')?.heroes or []) level.hidden = level.locked unless level.disabled ++context.levelsTotal @@ -202,6 +206,7 @@ module.exports = class CampaignView extends RootView if nextLevel = _.find(@campaign.renderedLevels, original: nextLevelOriginal) @createLine level.position, nextLevel.position @applyCampaignStyles() + @testParticles() afterInsert: -> super() @@ -253,6 +258,19 @@ module.exports = class CampaignView extends RootView @$el.find(".#{pos}-gradient").css 'background-image', "linear-gradient(to #{pos}, #{backgroundColorTransparent} 0%, #{backgroundColor} 100%)" @playAmbientSound() + testParticles: -> + return unless @campaign.loaded and me.getForeshadowsLevels() + @particleMan ?= new ParticleMan() + @particleMan.removeEmitters() + @particleMan.attach @$el.find('.map') + for level in @campaign.renderedLevels ? {} when level.hidden + particleKey = ['level', @terrain] + particleKey.push level.type if level.type and level.type isnt 'hero' + particleKey.push 'premium' if level.requiresSubscription + particleKey.push 'gate' if level.slug in ['kithgard-gates', 'siege-of-stonehold', 'clash-of-clones'] + continue if particleKey.length is 2 # Don't show basic levels + @particleMan.addEmitter level.position.x / 100, level.position.y / 100, particleKey.join('-') + onSessionsLoaded: (e) -> return if @editorMode for session in @sessions.models @@ -363,6 +381,7 @@ module.exports = class CampaignView extends RootView resultingMarginX = (pageWidth - resultingWidth) / 2 resultingMarginY = (pageHeight - resultingHeight) / 2 @$el.find('.map').css(width: resultingWidth, height: resultingHeight, 'margin-left': resultingMarginX, 'margin-top': resultingMarginY) + @testParticles() if @particleMan playAmbientSound: -> return if @ambientSound diff --git a/app/views/play/menu/GuideView.coffee b/app/views/play/menu/GuideView.coffee index 335d06628..fd447045f 100644 --- a/app/views/play/menu/GuideView.coffee +++ b/app/views/play/menu/GuideView.coffee @@ -94,18 +94,13 @@ module.exports = class LevelGuideView extends CocoView window.tracker?.trackEvent 'Finish help video', level: @levelID, ls: @sessionID, style: @helpVideos[@helpVideosIndex].style @trackedHelpVideoFinish = true - # we wan't to always use the same scheme (HTTP/HTTPS) as the page was loaded with, but don't want to require Artisans to have to remember - # not to include a scheme in help video url - fixupUri = (uri) -> - n = uri.indexOf('/') - if n < 1 - return uri - return uri.slice(n) - setupVideoPlayer: () -> - return unless @helpVideos.length > 0 - helpVideoURL = fixupUri(@helpVideos[@helpVideosIndex].url) - @setupVimeoVideoPlayer helpVideoURL + return unless @helpVideos?.length > 0 + return unless url = @helpVideos[@helpVideosIndex]?.url + # Always use HTTPS + # TODO: Not specifying the protocol should work based on Vimeo docs, but breaks postMessage/eventing in practice. + url = "https:" + url.substr url.indexOf '/' + @setupVimeoVideoPlayer url setupVimeoVideoPlayer: (helpVideoURL) -> # Setup Vimeo player diff --git a/bower.json b/bower.json index f718fa065..7b51ba77b 100644 --- a/bower.json +++ b/bower.json @@ -46,7 +46,8 @@ "zatanna": "https://github.com/differentmatt/zatanna.git#master", "modernizr": "~2.8.3", "backfire": "~0.3.0", - "fastclick": "~1.0.3" + "fastclick": "~1.0.3", + "three.js": "*" }, "overrides": { "backbone": { diff --git a/config.coffee b/config.coffee index 3f520ee87..b173c3c46 100644 --- a/config.coffee +++ b/config.coffee @@ -81,7 +81,7 @@ exports.config = #- vendor.js, all the vendor libraries 'javascripts/vendor.js': [ regJoin('^vendor/scripts/(?!(Box2d|coffeescript|difflib|diffview|jasmine))') - regJoin('^bower_components/(?!(aether|d3|treema))') + regJoin('^bower_components/(?!(aether|d3|treema|three.js))') 'bower_components/treema/treema-utils.js' ] 'javascripts/whole-vendor.js': if TRAVIS then [ @@ -112,6 +112,7 @@ exports.config = 'javascripts/app/vendor/treema.js': 'bower_components/treema/treema.js' 'javascripts/app/vendor/jasmine-bundle.js': regJoin('^vendor/scripts/jasmine') 'javascripts/app/vendor/jasmine-mock-ajax.js': 'vendor/scripts/jasmine-mock-ajax.js' + 'javascripts/app/vendor/three.js': 'bower_components/three.js/three.min.js' #- test, demo libraries 'javascripts/app/tests.js': regJoin('^test/app/') diff --git a/vendor/scripts/ShaderParticles.js b/vendor/scripts/ShaderParticles.js new file mode 100644 index 000000000..0897535f3 --- /dev/null +++ b/vendor/scripts/ShaderParticles.js @@ -0,0 +1,1179 @@ +// ShaderParticleUtils 0.7.8 +// +// (c) 2014 Luke Moody (http://www.github.com/squarefeet) +// & Lee Stemkoski (http://www.adelphi.edu/~stemkoski/) +// +// Based on Lee Stemkoski's original work: +// (https://github.com/stemkoski/stemkoski.github.com/blob/master/Three.js/js/ParticleEngine.js). +// +// ShaderParticleGroup may be freely distributed under the MIT license (See LICENSE.txt) + +var SPE = SPE || {}; + +SPE.utils = { + + /** + * Given a base vector and a spread range vector, create + * a new THREE.Vector3 instance with randomised values. + * + * @private + * + * @param {THREE.Vector3} base + * @param {THREE.Vector3} spread + * @return {THREE.Vector3} + */ + randomVector3: function( base, spread ) { + var v = new THREE.Vector3(); + + v.copy( base ); + + v.x += Math.random() * spread.x - (spread.x/2); + v.y += Math.random() * spread.y - (spread.y/2); + v.z += Math.random() * spread.z - (spread.z/2); + + return v; + }, + + /** + * Create a new THREE.Color instance and given a base vector and + * spread range vector, assign random values. + * + * Note that THREE.Color RGB values are in the range of 0 - 1, not 0 - 255. + * + * @private + * + * @param {THREE.Vector3} base + * @param {THREE.Vector3} spread + * @return {THREE.Color} + */ + randomColor: function( base, spread ) { + var v = new THREE.Color(); + + v.copy( base ); + + v.r += (Math.random() * spread.x) - (spread.x/2); + v.g += (Math.random() * spread.y) - (spread.y/2); + v.b += (Math.random() * spread.z) - (spread.z/2); + + v.r = Math.max( 0, Math.min( v.r, 1 ) ); + v.g = Math.max( 0, Math.min( v.g, 1 ) ); + v.b = Math.max( 0, Math.min( v.b, 1 ) ); + + return v; + }, + + /** + * Create a random Number value based on an initial value and + * a spread range + * + * @private + * + * @param {Number} base + * @param {Number} spread + * @return {Number} + */ + randomFloat: function( base, spread ) { + return base + spread * (Math.random() - 0.5); + }, + + /** + * Create a new THREE.Vector3 instance and project it onto a random point + * on a sphere with randomized radius. + * + * @param {THREE.Vector3} base + * @param {Number} radius + * @param {THREE.Vector3} radiusSpread + * @param {THREE.Vector3} radiusScale + * + * @private + * + * @return {THREE.Vector3} + */ + randomVector3OnSphere: function( base, radius, radiusSpread, radiusScale, radiusSpreadClamp ) { + var z = 2 * Math.random() - 1; + var t = 6.2832 * Math.random(); + var r = Math.sqrt( 1 - z*z ); + var vec = new THREE.Vector3( r * Math.cos(t), r * Math.sin(t), z ); + + var rand = this._randomFloat( radius, radiusSpread ); + + if( radiusSpreadClamp ) { + rand = Math.round( rand / radiusSpreadClamp ) * radiusSpreadClamp; + } + + vec.multiplyScalar( rand ); + + if( radiusScale ) { + vec.multiply( radiusScale ); + } + + vec.add( base ); + + return vec; + }, + + /** + * Create a new THREE.Vector3 instance and project it onto a random point + * on a disk (in the XY-plane) centered at `base` and with randomized radius. + * + * @param {THREE.Vector3} base + * @param {Number} radius + * @param {THREE.Vector3} radiusSpread + * @param {THREE.Vector3} radiusScale + * + * @private + * + * @return {THREE.Vector3} + */ + randomVector3OnDisk: function( base, radius, radiusSpread, radiusScale, radiusSpreadClamp ) { + var t = 6.2832 * Math.random(); + var rand = this._randomFloat( radius, radiusSpread ); + + if( radiusSpreadClamp ) { + rand = Math.round( rand / radiusSpreadClamp ) * radiusSpreadClamp; + } + + var vec = new THREE.Vector3( Math.cos(t), Math.sin(t), 0 ).multiplyScalar( rand ); + + if ( radiusScale ) { + vec.multiply( radiusScale ); + } + + vec.add( base ); + + return vec ; + }, + + + /** + * Create a new THREE.Vector3 instance, and given a sphere with center `base` and + * point `position` on sphere, set direction away from sphere center with random magnitude. + * + * @param {THREE.Vector3} base + * @param {THREE.Vector3} position + * @param {Number} speed + * @param {Number} speedSpread + * @param {THREE.Vector3} scale + * + * @private + * + * @return {THREE.Vector3} + */ + randomVelocityVector3OnSphere: function( base, position, speed, speedSpread, scale ) { + var direction = new THREE.Vector3().subVectors( base, position ); + + direction.normalize().multiplyScalar( Math.abs( this._randomFloat( speed, speedSpread ) ) ); + + if( scale ) { + direction.multiply( scale ); + } + + return direction; + }, + + + + /** + * Given a base vector and a spread vector, randomise the given vector + * accordingly. + * + * @param {THREE.Vector3} vector + * @param {THREE.Vector3} base + * @param {THREE.Vector3} spread + * + * @private + * + * @return {[type]} + */ + randomizeExistingVector3: function( v, base, spread ) { + v.copy( base ); + + v.x += Math.random() * spread.x - (spread.x/2); + v.y += Math.random() * spread.y - (spread.y/2); + v.z += Math.random() * spread.z - (spread.z/2); + }, + + + /** + * Randomize a THREE.Color instance and given a base vector and + * spread range vector, assign random values. + * + * Note that THREE.Color RGB values are in the range of 0 - 1, not 0 - 255. + * + * @private + * + * @param {THREE.Vector3} base + * @param {THREE.Vector3} spread + * @return {THREE.Color} + */ + randomizeExistingColor: function( v, base, spread ) { + v.copy( base ); + + v.r += (Math.random() * spread.x) - (spread.x/2); + v.g += (Math.random() * spread.y) - (spread.y/2); + v.b += (Math.random() * spread.z) - (spread.z/2); + + v.r = Math.max( 0, Math.min( v.r, 1 ) ); + v.g = Math.max( 0, Math.min( v.g, 1 ) ); + v.b = Math.max( 0, Math.min( v.b, 1 ) ); + }, + + /** + * Given an existing particle vector, project it onto a random point on a + * sphere with radius `radius` and position `base`. + * + * @private + * + * @param {THREE.Vector3} v + * @param {THREE.Vector3} base + * @param {Number} radius + */ + randomizeExistingVector3OnSphere: function( v, base, radius, radiusSpread, radiusScale, radiusSpreadClamp ) { + var z = 2 * Math.random() - 1, + t = 6.2832 * Math.random(), + r = Math.sqrt( 1 - z*z ), + rand = this._randomFloat( radius, radiusSpread ); + + if( radiusSpreadClamp ) { + rand = Math.round( rand / radiusSpreadClamp ) * radiusSpreadClamp; + } + + v.set( + (r * Math.cos(t)) * rand, + (r * Math.sin(t)) * rand, + z * rand + ).multiply( radiusScale ); + + v.add( base ); + }, + + + /** + * Given an existing particle vector, project it onto a random point + * on a disk (in the XY-plane) centered at `base` and with radius `radius`. + * + * @private + * + * @param {THREE.Vector3} v + * @param {THREE.Vector3} base + * @param {Number} radius + */ + randomizeExistingVector3OnDisk: function( v, base, radius, radiusSpread, radiusScale, radiusSpreadClamp ) { + var t = 6.2832 * Math.random(), + rand = Math.abs( this._randomFloat( radius, radiusSpread ) ); + + if( radiusSpreadClamp ) { + rand = Math.round( rand / radiusSpreadClamp ) * radiusSpreadClamp; + } + + v.set( + Math.cos( t ), + Math.sin( t ), + 0 + ).multiplyScalar( rand ); + + if ( radiusScale ) { + v.multiply( radiusScale ); + } + + v.add( base ); + }, + + randomizeExistingVelocityVector3OnSphere: function( v, base, position, speed, speedSpread ) { + v.copy(position) + .sub(base) + .normalize() + .multiplyScalar( Math.abs( this._randomFloat( speed, speedSpread ) ) ); + }, + + generateID: function() { + var str = 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'; + + str = str.replace(/[xy]/g, function(c) { + var rand = Math.random(); + var r = rand*16|0%16, v = c === 'x' ? r : (r&0x3|0x8); + + return v.toString(16); + }); + + return str; + } +};; + +// ShaderParticleGroup 0.7.8 +// +// (c) 2014 Luke Moody (http://www.github.com/squarefeet) +// & Lee Stemkoski (http://www.adelphi.edu/~stemkoski/) +// +// Based on Lee Stemkoski's original work: +// (https://github.com/stemkoski/stemkoski.github.com/blob/master/Three.js/js/ParticleEngine.js). +// +// ShaderParticleGroup may be freely distributed under the MIT license (See LICENSE.txt) + +var SPE = SPE || {}; + +SPE.Group = function( options ) { + var that = this; + + that.fixedTimeStep = parseFloat( typeof options.fixedTimeStep === 'number' ? options.fixedTimeStep : 0.016 ); + + // Uniform properties ( applied to all particles ) + that.maxAge = parseFloat( options.maxAge || 3 ); + that.texture = options.texture || null; + that.hasPerspective = parseInt( typeof options.hasPerspective === 'number' ? options.hasPerspective : 1, 10 ); + that.colorize = parseInt( typeof options.colorize === 'number' ? options.colorize : 1, 10 ); + + // Material properties + that.blending = typeof options.blending === 'number' ? options.blending : THREE.AdditiveBlending; + that.transparent = typeof options.transparent === 'number' ? options.transparent : 1; + that.alphaTest = typeof options.alphaTest === 'number' ? options.alphaTest : 0.5; + that.depthWrite = options.depthWrite || false; + that.depthTest = options.depthTest || true; + + // Create uniforms + that.uniforms = { + duration: { type: 'f', value: that.maxAge }, + texture: { type: 't', value: that.texture }, + hasPerspective: { type: 'i', value: that.hasPerspective }, + colorize: { type: 'i', value: that.colorize } + }; + + // Create a map of attributes that will hold values for each particle in this group. + that.attributes = { + acceleration: { type: 'v3', value: [] }, + velocity: { type: 'v3', value: [] }, + + alive: { type: 'f', value: [] }, + age: { type: 'f', value: [] }, + + size: { type: 'v3', value: [] }, + angle: { type: 'v4', value: [] }, + + colorStart: { type: 'c', value: [] }, + colorMiddle: { type: 'c', value: [] }, + colorEnd: { type: 'c', value: [] }, + + opacity: { type: 'v3', value: [] } + }; + + // Emitters (that aren't static) will be added to this array for + // processing during the `tick()` function. + that.emitters = []; + + // Create properties for use by the emitter pooling functions. + that._pool = []; + that._poolCreationSettings = null; + that._createNewWhenPoolEmpty = 0; + that.maxAgeMilliseconds = that.maxAge * 1000; + + // Create an empty geometry to hold the particles. + // Each particle is a vertex pushed into this geometry's + // vertices array. + that.geometry = new THREE.Geometry(); + + // Create the shader material using the properties we set above. + that.material = new THREE.ShaderMaterial({ + uniforms: that.uniforms, + attributes: that.attributes, + vertexShader: SPE.shaders.vertex, + fragmentShader: SPE.shaders.fragment, + blending: that.blending, + transparent: that.transparent, + alphaTest: that.alphaTest, + depthWrite: that.depthWrite, + depthTest: that.depthTest + }); + + // And finally create the ParticleSystem. It's got its `dynamic` property + // set so that THREE.js knows to update it on each frame. + that.mesh = new THREE.PointCloud( that.geometry, that.material ); + that.mesh.dynamic = true; +}; + +SPE.Group.prototype = { + + /** + * Tells the age and alive attributes (and the geometry vertices) + * that they need updating by THREE.js's internal tick functions. + * + * @private + * + * @return {this} + */ + _flagUpdate: function() { + var that = this; + + // Set flags to update (causes less garbage than + // ```ParticleSystem.sortParticles = true``` in THREE.r58 at least) + that.attributes.age.needsUpdate = true; + that.attributes.alive.needsUpdate = true; + that.attributes.angle.needsUpdate = true; + // that.attributes.angleAlignVelocity.needsUpdate = true; + that.attributes.velocity.needsUpdate = true; + that.attributes.acceleration.needsUpdate = true; + that.geometry.verticesNeedUpdate = true; + + return that; + }, + + /** + * Add an emitter to this particle group. Once added, an emitter will be automatically + * updated when SPE.Group#tick() is called. + * + * @param {SPE.Emitter} emitter + * @return {this} + */ + addEmitter: function( emitter ) { + var that = this; + + if( emitter.duration ) { + emitter.particlesPerSecond = emitter.particleCount / (that.maxAge < emitter.duration ? that.maxAge : emitter.duration) | 0; + } + else { + emitter.particlesPerSecond = emitter.particleCount / that.maxAge | 0 + } + + var vertices = that.geometry.vertices, + start = vertices.length, + end = emitter.particleCount + start, + a = that.attributes, + acceleration = a.acceleration.value, + velocity = a.velocity.value, + alive = a.alive.value, + age = a.age.value, + size = a.size.value, + angle = a.angle.value, + colorStart = a.colorStart.value, + colorMiddle = a.colorMiddle.value, + colorEnd = a.colorEnd.value, + opacity = a.opacity.value; + + emitter.particleIndex = parseFloat( start ); + + // Create the values + for( var i = start; i < end; ++i ) { + + if( emitter.type === 'sphere' ) { + vertices[i] = that._randomVector3OnSphere( emitter.position, emitter.radius, emitter.radiusSpread, emitter.radiusScale, emitter.radiusSpreadClamp ); + velocity[i] = that._randomVelocityVector3OnSphere( vertices[i], emitter.position, emitter.speed, emitter.speedSpread ); + } + else if( emitter.type === 'disk' ) { + vertices[i] = that._randomVector3OnDisk( emitter.position, emitter.radius, emitter.radiusSpread, emitter.radiusScale, emitter.radiusSpreadClamp ); + velocity[i] = that._randomVelocityVector3OnSphere( vertices[i], emitter.position, emitter.speed, emitter.speedSpread ); + } + else { + vertices[i] = that._randomVector3( emitter.position, emitter.positionSpread ); + velocity[i] = that._randomVector3( emitter.velocity, emitter.velocitySpread ); + } + + acceleration[i] = that._randomVector3( emitter.acceleration, emitter.accelerationSpread ); + + size[i] = new THREE.Vector3( + Math.abs( that._randomFloat( emitter.sizeStart, emitter.sizeStartSpread ) ), + Math.abs( that._randomFloat( emitter.sizeMiddle, emitter.sizeMiddleSpread ) ), + Math.abs( that._randomFloat( emitter.sizeEnd, emitter.sizeEndSpread ) ) + ); + + angle[i] = new THREE.Vector4( + that._randomFloat( emitter.angleStart, emitter.angleStartSpread ), + that._randomFloat( emitter.angleMiddle, emitter.angleMiddleSpread ), + that._randomFloat( emitter.angleEnd, emitter.angleEndSpread ), + emitter.angleAlignVelocity ? 1.0 : 0.0 + ); + + age[i] = 0.0; + alive[i] = emitter.isStatic ? 1.0 : 0.0; + + colorStart[i] = that._randomColor( emitter.colorStart, emitter.colorStartSpread ); + colorMiddle[i] = that._randomColor( emitter.colorMiddle, emitter.colorMiddleSpread ); + colorEnd[i] = that._randomColor( emitter.colorEnd, emitter.colorEndSpread ); + + opacity[i] = new THREE.Vector3( + Math.abs( that._randomFloat( emitter.opacityStart, emitter.opacityStartSpread ) ), + Math.abs( that._randomFloat( emitter.opacityMiddle, emitter.opacityMiddleSpread ) ), + Math.abs( that._randomFloat( emitter.opacityEnd, emitter.opacityEndSpread ) ) + ); + } + + // Cache properties on the emitter so we can access + // them from its tick function. + emitter.verticesIndex = parseFloat( start ); + emitter.attributes = a; + emitter.vertices = that.geometry.vertices; + emitter.maxAge = that.maxAge; + + // Assign a unique ID to this emitter + emitter.__id = that._generateID(); + + // Save this emitter in an array for processing during this.tick() + if( !emitter.isStatic ) { + that.emitters.push( emitter ); + } + + return that; + }, + + + removeEmitter: function( emitter ) { + var id, + emitters = this.emitters; + + if( emitter instanceof SPE.Emitter ) { + id = emitter.__id; + } + else if( typeof emitter === 'string' ) { + id = emitter; + } + else { + console.warn('Invalid emitter or emitter ID passed to SPE.Group#removeEmitter.' ); + return; + } + + for( var i = 0, il = emitters.length; i < il; ++i ) { + if( emitters[i].__id === id ) { + emitters.splice(i, 1); + break; + } + } + }, + + + /** + * The main particle group update function. Call this once per frame. + * + * @param {Number} dt + * @return {this} + */ + tick: function( dt ) { + var that = this, + emitters = that.emitters, + numEmitters = emitters.length; + + dt = dt || that.fixedTimeStep; + + if( numEmitters === 0 ) { + return; + } + + for( var i = 0; i < numEmitters; ++i ) { + emitters[i].tick( dt ); + } + + that._flagUpdate(); + return that; + }, + + + /** + * Fetch a single emitter instance from the pool. + * If there are no objects in the pool, a new emitter will be + * created if specified. + * + * @return {ShaderParticleEmitter | null} + */ + getFromPool: function() { + var that = this, + pool = that._pool, + createNew = that._createNewWhenPoolEmpty; + + if( pool.length ) { + return pool.pop(); + } + else if( createNew ) { + return new SPE.Emitter( that._poolCreationSettings ); + } + + return null; + }, + + + /** + * Release an emitter into the pool. + * + * @param {ShaderParticleEmitter} emitter + * @return {this} + */ + releaseIntoPool: function( emitter ) { + if( !(emitter instanceof SPE.Emitter) ) { + console.error( 'Will not add non-emitter to particle group pool:', emitter ); + return; + } + + emitter.reset(); + this._pool.unshift( emitter ); + + return this; + }, + + + /** + * Get the pool array + * + * @return {Array} + */ + getPool: function() { + return this._pool; + }, + + + /** + * Add a pool of emitters to this particle group + * + * @param {Number} numEmitters The number of emitters to add to the pool. + * @param {Object} emitterSettings An object describing the settings to pass to each emitter. + * @param {Boolean} createNew Should a new emitter be created if the pool runs out? + * @return {this} + */ + addPool: function( numEmitters, emitterSettings, createNew ) { + var that = this, + emitter; + + // Save relevant settings and flags. + that._poolCreationSettings = emitterSettings; + that._createNewWhenPoolEmpty = !!createNew; + + // Create the emitters, add them to this group and the pool. + for( var i = 0; i < numEmitters; ++i ) { + emitter = new SPE.Emitter( emitterSettings ); + that.addEmitter( emitter ); + that.releaseIntoPool( emitter ); + } + + return that; + }, + + + /** + * Internal method. Sets a single emitter to be alive + * + * @private + * + * @param {THREE.Vector3} pos + * @return {this} + */ + _triggerSingleEmitter: function( pos ) { + var that = this, + emitter = that.getFromPool(); + + if( emitter === null ) { + console.log('SPE.Group pool ran out.'); + return; + } + + // TODO: Should an instanceof check happen here? Or maybe at least a typeof? + if( pos ) { + emitter.position.copy( pos ); + } + + emitter.enable(); + + setTimeout( function() { + emitter.disable(); + that.releaseIntoPool( emitter ); + }, that.maxAgeMilliseconds ); + + return that; + }, + + + /** + * Set a given number of emitters as alive, with an optional position + * vector3 to move them to. + * + * @param {Number} numEmitters + * @param {THREE.Vector3} position + * @return {this} + */ + triggerPoolEmitter: function( numEmitters, position ) { + var that = this; + + if( typeof numEmitters === 'number' && numEmitters > 1) { + for( var i = 0; i < numEmitters; ++i ) { + that._triggerSingleEmitter( position ); + } + } + else { + that._triggerSingleEmitter( position ); + } + + return that; + } +}; + + +// Extend ShaderParticleGroup's prototype with functions from utils object. +for( var i in SPE.utils ) { + SPE.Group.prototype[ '_' + i ] = SPE.utils[i]; +} + + +// The all-important shaders +SPE.shaders = { + vertex: [ + 'uniform float duration;', + 'uniform int hasPerspective;', + + 'attribute vec3 colorStart;', + 'attribute vec3 colorMiddle;', + 'attribute vec3 colorEnd;', + 'attribute vec3 opacity;', + + 'attribute vec3 acceleration;', + 'attribute vec3 velocity;', + 'attribute float alive;', + 'attribute float age;', + + 'attribute vec3 size;', + 'attribute vec4 angle;', + + // values to be passed to the fragment shader + 'varying vec4 vColor;', + 'varying float vAngle;', + + + // Integrate acceleration into velocity and apply it to the particle's position + 'vec4 GetPos() {', + 'vec3 newPos = vec3( position );', + + // Move acceleration & velocity vectors to the value they + // should be at the current age + 'vec3 a = acceleration * age;', + 'vec3 v = velocity * age;', + + // Move velocity vector to correct values at this age + 'v = v + (a * age);', + + // Add velocity vector to the newPos vector + 'newPos = newPos + v;', + + // Convert the newPos vector into world-space + 'vec4 mvPosition = modelViewMatrix * vec4( newPos, 1.0 );', + + 'return mvPosition;', + '}', + + + 'void main() {', + + 'float positionInTime = (age / duration);', + + 'float lerpAmount1 = (age / (0.5 * duration));', // percentage during first half + 'float lerpAmount2 = ((age - 0.5 * duration) / (0.5 * duration));', // percentage during second half + 'float halfDuration = duration / 2.0;', + 'float pointSize = 0.0;', + + 'vAngle = 0.0;', + + 'if( alive > 0.5 ) {', + + // lerp the color and opacity + 'if( positionInTime < 0.5 ) {', + 'vColor = vec4( mix(colorStart, colorMiddle, lerpAmount1), mix(opacity.x, opacity.y, lerpAmount1) );', + '}', + 'else {', + 'vColor = vec4( mix(colorMiddle, colorEnd, lerpAmount2), mix(opacity.y, opacity.z, lerpAmount2) );', + '}', + + + // Get the position of this particle so we can use it + // when we calculate any perspective that might be required. + 'vec4 pos = GetPos();', + + + // Determine the angle we should use for this particle. + 'if( angle[3] == 1.0 ) {', + 'vAngle = -atan(pos.y, pos.x);', + '}', + 'else if( positionInTime < 0.5 ) {', + 'vAngle = mix( angle.x, angle.y, lerpAmount1 );', + '}', + 'else {', + 'vAngle = mix( angle.y, angle.z, lerpAmount2 );', + '}', + + // Determine point size. + 'if( positionInTime < 0.5) {', + 'pointSize = mix( size.x, size.y, lerpAmount1 );', + '}', + 'else {', + 'pointSize = mix( size.y, size.z, lerpAmount2 );', + '}', + + + 'if( hasPerspective == 1 ) {', + 'pointSize = pointSize * ( 300.0 / length( pos.xyz ) );', + '}', + + // Set particle size and position + 'gl_PointSize = pointSize;', + 'gl_Position = projectionMatrix * pos;', + '}', + + 'else {', + // Hide particle and set its position to the (maybe) glsl + // equivalent of Number.POSITIVE_INFINITY + 'vColor = vec4( 0.0, 0.0, 0.0, 0.0 );', + 'gl_Position = vec4(1000000000.0, 1000000000.0, 1000000000.0, 0.0);', + '}', + '}', + ].join('\n'), + + fragment: [ + 'uniform sampler2D texture;', + 'uniform int colorize;', + + 'varying vec4 vColor;', + 'varying float vAngle;', + + 'void main() {', + 'float c = cos(vAngle);', + 'float s = sin(vAngle);', + + 'vec2 rotatedUV = vec2(c * (gl_PointCoord.x - 0.5) + s * (gl_PointCoord.y - 0.5) + 0.5,', + 'c * (gl_PointCoord.y - 0.5) - s * (gl_PointCoord.x - 0.5) + 0.5);', + + 'vec4 rotatedTexture = texture2D( texture, rotatedUV );', + + 'if( colorize == 1 ) {', + 'gl_FragColor = vColor * rotatedTexture;', + '}', + 'else {', + 'gl_FragColor = rotatedTexture;', + '}', + '}' + ].join('\n') +}; +; + +// ShaderParticleEmitter 0.7.8 +// +// (c) 2014 Luke Moody (http://www.github.com/squarefeet) +// & Lee Stemkoski (http://www.adelphi.edu/~stemkoski/) +// +// Based on Lee Stemkoski's original work: +// (https://github.com/stemkoski/stemkoski.github.com/blob/master/Three.js/js/ParticleEngine.js). +// +// ShaderParticleEmitter may be freely distributed under the MIT license (See LICENSE.txt) + +var SPE = SPE || {}; + +SPE.Emitter = function( options ) { + // If no options are provided, fallback to an empty object. + options = options || {}; + + // Helps with minification. Not as easy to read the following code, + // but should still be readable enough! + var that = this; + + + that.particleCount = typeof options.particleCount === 'number' ? options.particleCount : 100; + that.type = (options.type === 'cube' || options.type === 'sphere' || options.type === 'disk') ? options.type : 'cube'; + + that.position = options.position instanceof THREE.Vector3 ? options.position : new THREE.Vector3(); + that.positionSpread = options.positionSpread instanceof THREE.Vector3 ? options.positionSpread : new THREE.Vector3(); + + // These two properties are only used when this.type === 'sphere' or 'disk' + that.radius = typeof options.radius === 'number' ? options.radius : 10; + that.radiusSpread = typeof options.radiusSpread === 'number' ? options.radiusSpread : 0; + that.radiusScale = options.radiusScale instanceof THREE.Vector3 ? options.radiusScale : new THREE.Vector3(1, 1, 1); + that.radiusSpreadClamp = typeof options.radiusSpreadClamp === 'number' ? options.radiusSpreadClamp : 0; + + that.acceleration = options.acceleration instanceof THREE.Vector3 ? options.acceleration : new THREE.Vector3(); + that.accelerationSpread = options.accelerationSpread instanceof THREE.Vector3 ? options.accelerationSpread : new THREE.Vector3(); + + that.velocity = options.velocity instanceof THREE.Vector3 ? options.velocity : new THREE.Vector3(); + that.velocitySpread = options.velocitySpread instanceof THREE.Vector3 ? options.velocitySpread : new THREE.Vector3(); + + + // And again here; only used when this.type === 'sphere' or 'disk' + that.speed = parseFloat( typeof options.speed === 'number' ? options.speed : 0.0 ); + that.speedSpread = parseFloat( typeof options.speedSpread === 'number' ? options.speedSpread : 0.0 ); + + + // Sizes + that.sizeStart = parseFloat( typeof options.sizeStart === 'number' ? options.sizeStart : 1.0 ); + that.sizeStartSpread = parseFloat( typeof options.sizeStartSpread === 'number' ? options.sizeStartSpread : 0.0 ); + + that.sizeEnd = parseFloat( typeof options.sizeEnd === 'number' ? options.sizeEnd : that.sizeStart ); + that.sizeEndSpread = parseFloat( typeof options.sizeEndSpread === 'number' ? options.sizeEndSpread : 0.0 ); + + that.sizeMiddle = parseFloat( + typeof options.sizeMiddle !== 'undefined' ? + options.sizeMiddle : + Math.abs(that.sizeEnd + that.sizeStart) / 2 + ); + that.sizeMiddleSpread = parseFloat( typeof options.sizeMiddleSpread === 'number' ? options.sizeMiddleSpread : 0 ); + + + // Angles + that.angleStart = parseFloat( typeof options.angleStart === 'number' ? options.angleStart : 0 ); + that.angleStartSpread = parseFloat( typeof options.angleStartSpread === 'number' ? options.angleStartSpread : 0 ); + + that.angleEnd = parseFloat( typeof options.angleEnd === 'number' ? options.angleEnd : 0 ); + that.angleEndSpread = parseFloat( typeof options.angleEndSpread === 'number' ? options.angleEndSpread : 0 ); + + that.angleMiddle = parseFloat( + typeof options.angleMiddle !== 'undefined' ? + options.angleMiddle : + Math.abs(that.angleEnd + that.angleStart) / 2 + ); + that.angleMiddleSpread = parseFloat( typeof options.angleMiddleSpread === 'number' ? options.angleMiddleSpread : 0 ); + + that.angleAlignVelocity = options.angleAlignVelocity || false; + + + // Colors + that.colorStart = options.colorStart instanceof THREE.Color ? options.colorStart : new THREE.Color( 'white' ); + that.colorStartSpread = options.colorStartSpread instanceof THREE.Vector3 ? options.colorStartSpread : new THREE.Vector3(); + + that.colorEnd = options.colorEnd instanceof THREE.Color ? options.colorEnd : that.colorStart.clone(); + that.colorEndSpread = options.colorEndSpread instanceof THREE.Vector3 ? options.colorEndSpread : new THREE.Vector3(); + + that.colorMiddle = + options.colorMiddle instanceof THREE.Color ? + options.colorMiddle : + new THREE.Color().addColors( that.colorStart, that.colorEnd ).multiplyScalar( 0.5 ); + that.colorMiddleSpread = options.colorMiddleSpread instanceof THREE.Vector3 ? options.colorMiddleSpread : new THREE.Vector3(); + + + + // Opacities + that.opacityStart = parseFloat( typeof options.opacityStart !== 'undefined' ? options.opacityStart : 1 ); + that.opacityStartSpread = parseFloat( typeof options.opacityStartSpread !== 'undefined' ? options.opacityStartSpread : 0 ); + + that.opacityEnd = parseFloat( typeof options.opacityEnd === 'number' ? options.opacityEnd : 0 ); + that.opacityEndSpread = parseFloat( typeof options.opacityEndSpread !== 'undefined' ? options.opacityEndSpread : 0 ); + + that.opacityMiddle = parseFloat( + typeof options.opacityMiddle !== 'undefined' ? + options.opacityMiddle : + Math.abs(that.opacityEnd + that.opacityStart) / 2 + ); + that.opacityMiddleSpread = parseFloat( typeof options.opacityMiddleSpread === 'number' ? options.opacityMiddleSpread : 0 ); + + + // Generic + that.duration = typeof options.duration === 'number' ? options.duration : null; + that.alive = parseFloat( typeof options.alive === 'number' ? options.alive : 1.0 ); + that.isStatic = typeof options.isStatic === 'number' ? options.isStatic : 0; + + // The following properties are used internally, and mostly set when this emitter + // is added to a particle group. + that.particlesPerSecond = 0; + that.attributes = null; + that.vertices = null; + that.verticesIndex = 0; + that.age = 0.0; + that.maxAge = 0.0; + + that.particleIndex = 0.0; + + that.__id = null; + + that.userData = {}; +}; + +SPE.Emitter.prototype = { + + /** + * Reset a particle's position. Accounts for emitter type and spreads. + * + * @private + * + * @param {THREE.Vector3} p + */ + _resetParticle: function( i ) { + var that = this, + type = that.type, + spread = that.positionSpread, + particlePosition = that.vertices[i], + a = that.attributes, + particleVelocity = a.velocity.value[i], + + vSpread = that.velocitySpread, + aSpread = that.accelerationSpread; + + // Optimise for no position spread or radius + if( + ( type === 'cube' && spread.x === 0 && spread.y === 0 && spread.z === 0 ) || + ( type === 'sphere' && that.radius === 0 ) || + ( type === 'disk' && that.radius === 0 ) + ) { + particlePosition.copy( that.position ); + that._randomizeExistingVector3( particleVelocity, that.velocity, vSpread ); + + if( type === 'cube' ) { + that._randomizeExistingVector3( that.attributes.acceleration.value[i], that.acceleration, aSpread ); + } + } + + // If there is a position spread, then get a new position based on this spread. + else if( type === 'cube' ) { + that._randomizeExistingVector3( particlePosition, that.position, spread ); + that._randomizeExistingVector3( particleVelocity, that.velocity, vSpread ); + that._randomizeExistingVector3( that.attributes.acceleration.value[i], that.acceleration, aSpread ); + } + + else if( type === 'sphere') { + that._randomizeExistingVector3OnSphere( particlePosition, that.position, that.radius, that.radiusSpread, that.radiusScale, that.radiusSpreadClamp ); + that._randomizeExistingVelocityVector3OnSphere( particleVelocity, that.position, particlePosition, that.speed, that.speedSpread ); + } + + else if( type === 'disk') { + that._randomizeExistingVector3OnDisk( particlePosition, that.position, that.radius, that.radiusSpread, that.radiusScale, that.radiusSpreadClamp ); + that._randomizeExistingVelocityVector3OnSphere( particleVelocity, that.position, particlePosition, that.speed, that.speedSpread ); + } + }, + + /** + * Update this emitter's particle's positions. Called by the SPE.Group + * that this emitter belongs to. + * + * @param {Number} dt + */ + tick: function( dt ) { + + if( this.isStatic ) { + return; + } + + // Cache some values for quicker access in loops. + var that = this, + a = that.attributes, + alive = a.alive.value, + age = a.age.value, + start = that.verticesIndex, + particleCount = that.particleCount, + end = start + particleCount, + pps = that.particlesPerSecond * that.alive, + ppsdt = pps * dt, + m = that.maxAge, + emitterAge = that.age, + duration = that.duration, + pIndex = that.particleIndex; + + // Loop through all the particles in this emitter and + // determine whether they're still alive and need advancing + // or if they should be dead and therefore marked as such. + for( var i = start; i < end; ++i ) { + if( alive[ i ] === 1.0 ) { + age[ i ] += dt; + } + + if( age[ i ] >= m ) { + age[ i ] = 0.0; + alive[ i ] = 0.0; + } + } + + // If the emitter is dead, reset any particles that are in + // the recycled vertices array and reset the age of the + // emitter to zero ready to go again if required, then + // exit this function. + if( that.alive === 0.0 ) { + that.age = 0.0; + return; + } + + // If the emitter has a specified lifetime and we've exceeded it, + // mark the emitter as dead and exit this function. + if( typeof duration === 'number' && emitterAge > duration ) { + that.alive = 0.0; + that.age = 0.0; + return; + } + + + + var n = Math.max( Math.min( end, pIndex + ppsdt ), 0), + count = 0, + index = 0, + pIndexFloor = pIndex | 0, + dtInc; + + for( i = pIndexFloor; i < n; ++i ) { + if( alive[ i ] !== 1.0 ) { + ++count; + } + } + + if( count !== 0 ) { + dtInc = dt / count; + + for( i = pIndexFloor; i < n; ++i, ++index ) { + if( alive[ i ] !== 1.0 ) { + alive[ i ] = 1.0; + age[ i ] = dtInc * index; + that._resetParticle( i ); + } + } + } + + that.particleIndex += ppsdt; + + if( that.particleIndex < 0.0 ) { + that.particleIndex = 0.0; + } + + if( pIndex >= start + particleCount ) { + that.particleIndex = parseFloat( start ); + } + + // Add the delta time value to the age of the emitter. + that.age += dt; + + if( that.age < 0.0 ) { + that.age = 0.0; + } + }, + + /** + * Reset this emitter back to its starting position. + * If `force` is truthy, then reset all particles in this + * emitter as well, even if they're currently alive. + * + * @param {Boolean} force + * @return {this} + */ + reset: function( force ) { + var that = this; + + that.age = 0.0; + that.alive = 0; + + if( force ) { + var start = that.verticesIndex, + end = that.verticesIndex + that.particleCount, + a = that.attributes, + alive = a.alive.value, + age = a.age.value; + + for( var i = start; i < end; ++i ) { + alive[ i ] = 0.0; + age[ i ] = 0.0; + } + } + + return that; + }, + + + /** + * Enable this emitter. + */ + enable: function() { + this.alive = 1; + }, + + /** + * Disable this emitter. + */ + disable: function() { + this.alive = 0; + } +}; + +// Extend SPE.Emitter's prototype with functions from utils object. +for( var i in SPE.utils ) { + SPE.Emitter.prototype[ '_' + i ] = SPE.utils[i]; +}