From a633f6f82f6b8bda95e6ef84fb6a608a19c31585 Mon Sep 17 00:00:00 2001 From: Scott Erickson Date: Mon, 15 Sep 2014 13:53:20 -0700 Subject: [PATCH] Built most of the WebGLSprite. --- app/lib/sprites/SpriteBuilder.coffee | 2 +- app/lib/surface/WebGLLayer.coffee | 1 - app/lib/surface/WebGLSprite.coffee | 141 +++++++++++++++++++ test/app/lib/surface/WebGLSprite.spec.coffee | 79 +++++++++++ 4 files changed, 221 insertions(+), 2 deletions(-) create mode 100644 test/app/lib/surface/WebGLSprite.spec.coffee diff --git a/app/lib/sprites/SpriteBuilder.coffee b/app/lib/sprites/SpriteBuilder.coffee index f88003ca7..e34e7d901 100644 --- a/app/lib/sprites/SpriteBuilder.coffee +++ b/app/lib/sprites/SpriteBuilder.coffee @@ -77,7 +77,7 @@ module.exports = class SpriteBuilder buildMovieClipAnimations: (localAnimations) -> map = {} for localAnimation in localAnimations - animation = @buildMovieClip(localAnimation.gn, localAnimation.a) + animation = @buildMovieClip(localAnimation.gn, localAnimation.a...) animation.setTransform(localAnimation.t...) map[localAnimation.bn] = animation map diff --git a/app/lib/surface/WebGLLayer.coffee b/app/lib/surface/WebGLLayer.coffee index 993435ffd..a4d354390 100644 --- a/app/lib/surface/WebGLLayer.coffee +++ b/app/lib/surface/WebGLLayer.coffee @@ -11,7 +11,6 @@ module.exports = class WebGLLayer extends createjs.SpriteContainer constructor: -> super(arguments...) - @spriteSheetBuilder = new createjs.SpriteSheetBuilder() @actionRenderState = {} @toRenderBundles = [] @initialize(arguments...) diff --git a/app/lib/surface/WebGLSprite.coffee b/app/lib/surface/WebGLSprite.coffee index e69de29bb..094fb6647 100644 --- a/app/lib/surface/WebGLSprite.coffee +++ b/app/lib/surface/WebGLSprite.coffee @@ -0,0 +1,141 @@ +SpriteBuilder = require 'lib/sprites/SpriteBuilder' + +module.exports = class WebGLSprite extends createjs.SpriteContainer + constructor: (@spriteSheet, @thangType, @spriteSheetPrefix) -> + @initialize(@spriteSheet) + + gotoAndPlay: (actionName) -> @goto(actionName, false) + gotoAndStop: (actionName) -> @goto(actionName, true) + + goto: (actionName, @paused=true) -> + @currentAnimation = actionName + action = @thangType.getActions()[actionName] + if action.animation + @baseMovieClip = @buildMovieClip(action.animation) + @mirrorMovieClip(@baseMovieClip, @) + @framerate = (action.framerate ? 20) * (action.speed ? 1) + @frames = action.frames + @currentFrame = 0 + if @frames + @frames = (parseInt(f) for f in @frames.split(',')) + + if @frames and @frames.length is 1 + @baseMovieClip.gotoAndStop(@frames[0]) + @paused = true + @loop = action.loops isnt false + @goesTo = action.goesTo + @animLength = if @frames then @frames.length else @baseMovieClip.frameBounds.length + + return + + mirrorMovieClip: (movieClip, spriteContainer) -> + movieClips = [] + spriteContainer.children = movieClip.children + for child, index in spriteContainer.children + if child instanceof createjs.MovieClip + movieClips.push(child) + childMovieClip = child + childSpriteContainer = new createjs.SpriteContainer(@spriteSheet) + spriteContainer.children[index] = childSpriteContainer + movieClips = movieClips.concat(@mirrorMovieClip(childMovieClip, childSpriteContainer)) + return movieClips + + buildMovieClip: (animationName, mode, startPosition, loops) -> + raw = @thangType.get('raw') + animData = raw.animations[animationName] + movieClip = new createjs.MovieClip() + + locals = {} + _.extend locals, @buildMovieClipContainers(animData.containers) + _.extend locals, @buildMovieClipAnimations(animData.animations) + + anim = new createjs.MovieClip() + anim.initialize(mode ? createjs.MovieClip.INDEPENDENT, startPosition ? 0, loops ? true) + + for tweenData in animData.tweens + stopped = false + tween = createjs.Tween + for func in tweenData + args = $.extend(true, [], (func.a)) + args = @dereferenceArgs(args, locals) + if args is false + console.log 'could not dereference args', args + stopped = true + break + tween = tween[func.n](args...) + continue if stopped + anim.timeline.addTween(tween) + + anim.nominalBounds = new createjs.Rectangle(animData.bounds...) + if animData.frameBounds + anim.frameBounds = (new createjs.Rectangle(bounds...) for bounds in animData.frameBounds) + return anim + + buildMovieClipContainers: (localContainers) -> + map = {} + for localContainer in localContainers + container = new createjs.Sprite(@spriteSheet) + container.gotoAndStop(@spriteSheetPrefix + localContainer.gn) + container.setTransform(localContainer.t...) + container._off = localContainer.o if localContainer.o? + container.alpha = localContainer.al if localContainer.al? + map[localContainer.bn] = container + return map + + buildMovieClipAnimations: (localAnimations) -> + map = {} + for localAnimation in localAnimations + animation = @buildMovieClip(localAnimation.gn, localAnimation.a...) + animation.setTransform(localAnimation.t...) + map[localAnimation.bn] = animation + return map + + dereferenceArgs: (args, locals) -> + for key, val of args + if locals[val] + args[key] = locals[val] + else if val is null + args[key] = {} + else if _.isString(val) and val.indexOf('createjs.') is 0 + args[key] = eval(val) # TODO: Security risk + else if _.isObject(val) or _.isArray(val) + res = @dereferenceArgs(val, locals) + return res if res is false + else if _.isString(val) + return false + return args + + tick: (delta) -> + return unless @framerate and not @paused + newFrame = @currentFrame + @framerate * delta / 1000 + + if newFrame > @animLength + if @goesTo + @gotoAndPlay(@goesTo) + return + else if not @loop + @paused = false + newFrame = @animLength - 1 + @dispatchEvent('animationend') + else + newFrame = newFrame % @animLength + + if @frames + prevFrame = Math.floor(newFrame) + nextFrame = Math.ceil(newFrame) + if prevFrame is nextFrame + @baseMovieClip.gotoAndStop(@frames[newFrame]) + else if nextFrame is @frames.length + @baseMovieClip.gotoAndStop(@frames[prevFrame]) + else + # interpolate between frames + pct = newFrame % 1 + newFrameIndex = @frames[prevFrame] + (pct * (@frames[nextFrame] - @frames[prevFrame])) + @baseMovieClip.gotoAndStop(newFrameIndex) + else + @baseMovieClip.gotoAndStop(newFrame) + + @currentFrame = newFrame + + getBounds: -> + @baseMovieClip.getBounds() diff --git a/test/app/lib/surface/WebGLSprite.spec.coffee b/test/app/lib/surface/WebGLSprite.spec.coffee new file mode 100644 index 000000000..d3c324cde --- /dev/null +++ b/test/app/lib/surface/WebGLSprite.spec.coffee @@ -0,0 +1,79 @@ +WebGLLayer = require 'lib/surface/WebGLLayer' +WebGLSprite = require 'lib/surface/WebGLSprite' +CocoSprite = require 'lib/surface/CocoSprite' +ThangType = require 'models/ThangType' +treeThangType = new ThangType(require 'test/app/fixtures/tree1.thang.type') +ogreMunchkinThangType = new ThangType(require 'test/app/fixtures/ogre-munchkin-m.thang.type') + +describe 'WebGLSprite', -> + webGLSprite = null + + showMe = -> + canvas = $('').css('position', 'absolute').css('index', 1000).css('background', 'white') + $('body').append(canvas) + stage = new createjs.SpriteStage(canvas[0]) + stage.addChild(webGLSprite) + + ticks = 0 + listener = { + handleEvent: -> +# return if ticks >= 100 + webGLSprite.tick(arguments[0].delta) + stage.update() + ticks += 1 + } + createjs.Ticker.addEventListener "tick", listener + + beforeEach -> + layer = new WebGLLayer() + ogreMunchkinThangType.markToRevert() + ogreMunchkinThangType.set('renderStrategy', 'container') + actions = ogreMunchkinThangType.getActions() + + # couple extra actions for doing some tests + actions.littledance = {animation:'enemy_small_move_side',framerate:1, frames:'0,6,2,6,2,8,0'} + actions.onestep = {animation:'enemy_small_move_side', loops: false} + + colorConfig = {team: {hue: 0, saturation: 1, lightness: 0.5}} + sprite = new CocoSprite(ogreMunchkinThangType, {colorConfig: colorConfig}) + layer.addCocoSprite(sprite) + sheet = layer.renderNewSpriteSheet() + prefix = layer.renderGroupingKey(ogreMunchkinThangType, null, colorConfig) + '.' + window.webGLSprite = webGLSprite = new WebGLSprite(sheet, ogreMunchkinThangType, prefix) + + afterEach -> + ogreMunchkinThangType.revert() + + it 'has gotoAndPlay, gotoAndStop, and paused like a MovieClip or Sprite', -> + webGLSprite.gotoAndPlay('move_fore') + expect(webGLSprite.baseMovieClip).toBeDefined() + expect(webGLSprite.paused).toBe(false) + webGLSprite.gotoAndStop('move_fore') + expect(webGLSprite.paused).toBe(true) + + it 'is paused when the action is a single frame', -> + webGLSprite.gotoAndPlay('idle') + expect(webGLSprite.paused).toBe(true) + + it 'has a tick function which moves the animation forward', -> + webGLSprite.gotoAndPlay('move_fore') + webGLSprite.tick(100) # one hundred milliseconds + expect(webGLSprite.baseMovieClip.currentFrame).toBe(webGLSprite.framerate*100/1000) + + it 'will interpolate between frames of a custom frame set', -> + webGLSprite.gotoAndPlay('littledance') + webGLSprite.tick(1000) + expect(webGLSprite.baseMovieClip.currentFrame).toBe(6) + webGLSprite.tick(1000) + expect(webGLSprite.baseMovieClip.currentFrame).toBe(2) + webGLSprite.tick(500) + expect(webGLSprite.baseMovieClip.currentFrame).toBe(4) + webGLSprite.tick(500) + expect(webGLSprite.baseMovieClip.currentFrame).toBe(6) + + it 'emits animationend for animations where loops is false and there is no goesTo', -> + fired = false + webGLSprite.gotoAndPlay('onestep') + webGLSprite.on('animationend', -> fired = true) + webGLSprite.tick(1000) + expect(fired).toBe(true) \ No newline at end of file