CocoModel = require('./CocoModel')
SpriteBuilder = require 'lib/sprites/SpriteBuilder'
buildQueue = []
module.exports = class ThangType extends CocoModel
@className: "ThangType"
@schema: require 'schemas/models/thang_type'
urlRoot: "/db/thang.type"
building: {}
initialize: ->
super()
@building = {}
@setDefaults()
@on 'sync', @setDefaults
@spriteSheets = {}
setDefaults: ->
@resetRawData() unless @get('raw')
resetRawData: ->
@set('raw', {shapes:{}, containers:{}, animations:{}})
resetSpriteSheetCache: ->
@buildActions()
@spriteSheets = {}
@building = {}
isFullyLoaded: ->
# TODO: Come up with a better way to identify when the model doesn't have everything needed to build the sprite. ie when it's a projection without all the required data.
return @get('actions') or @get('raster') # needs one of these two things
getActions: ->
return {} unless @isFullyLoaded()
return @actions or @buildActions()
buildActions: ->
return null unless @isFullyLoaded()
@actions = $.extend(true, {}, @get('actions'))
for name, action of @actions
action.name = name
for relatedName, relatedAction of action.relatedActions ? {}
relatedAction.name = action.name + "_" + relatedName
@actions[relatedAction.name] = relatedAction
@actions
getSpriteSheet: (options) ->
options = @fillOptions options
key = @spriteSheetKey(options)
return @spriteSheets[key] or @buildSpriteSheet(options)
fillOptions: (options) ->
options ?= {}
options = _.clone options
options.resolutionFactor ?= SPRITE_RESOLUTION_FACTOR
options.async ?= false
options
buildSpriteSheet: (options) ->
return false unless @isFullyLoaded()
@options = @fillOptions options
key = @spriteSheetKey(@options)
if ss = @spriteSheets[key] then return ss
if @building[key]
@options = null
return key
@t0 = new Date().getTime()
@initBuild(options)
@addGeneralFrames() unless @options.portraitOnly
@addPortrait()
@building[key] = true
result = @finishBuild()
return result
initBuild: (options) ->
@buildActions() if not @actions
@vectorParser = new SpriteBuilder(@, options)
@builder = new createjs.SpriteSheetBuilder()
@builder.padding = 2
@frames = {}
addPortrait: ->
# The portrait is built very differently than the other animations, so it gets a separate function.
return unless @actions
portrait = @actions.portrait
return unless portrait
scale = portrait.scale or 1
pt = portrait.positions?.registration
rect = new createjs.Rectangle(pt?.x/scale or 0, pt?.y/scale or 0, 100/scale, 100/scale)
if portrait.animation
mc = @vectorParser.buildMovieClip portrait.animation
mc.nominalBounds = mc.frameBounds = null # override what the movie clip says on bounding
@builder.addMovieClip(mc, rect, scale)
frames = @builder._animations[portrait.animation].frames
frames = @mapFrames(portrait.frames, frames[0]) if portrait.frames?
@builder.addAnimation 'portrait', frames, true
else if portrait.container
s = @vectorParser.buildContainerFromStore(portrait.container)
frame = @builder.addFrame(s, rect, scale)
@builder.addAnimation 'portrait', [frame], false
addGeneralFrames: ->
framesMap = {}
for animation in @requiredRawAnimations()
name = animation.animation
mc = @vectorParser.buildMovieClip name
continue unless mc
@builder.addMovieClip mc, null, animation.scale * @options.resolutionFactor
framesMap[animation.scale + "_" + name] = @builder._animations[name].frames
for name, action of @actions when action.animation
continue if name is 'portrait'
scale = action.scale ? @get('scale') ? 1
frames = framesMap[scale + "_" + action.animation]
continue unless frames
frames = @mapFrames(action.frames, frames[0]) if action.frames?
next = true
next = action.goesTo if action.goesTo
next = false if action.loops is false
@builder.addAnimation name, frames, next
for name, action of @actions when action.container and not action.animation
continue if name is 'portrait'
scale = @options.resolutionFactor * (action.scale or @get('scale') or 1)
s = @vectorParser.buildContainerFromStore(action.container)
continue unless s
frame = @builder.addFrame(s, s.bounds, scale)
@builder.addAnimation name, [frame], false
requiredRawAnimations: ->
required = []
for name, action of @get('actions')
continue if name is 'portrait'
allActions = [action].concat(_.values (action.relatedActions ? {}))
for a in allActions when a.animation
scale = if name is 'portrait' then a.scale or 1 else a.scale or @get('scale') or 1
animation = {animation: a.animation, scale: scale}
animation.portrait = name is 'portrait'
unless _.find(required, (r) -> _.isEqual r, animation)
required.push animation
required
mapFrames: (frames, frameOffset) ->
return frames unless _.isString(frames) # don't accidentally do this again
(parseInt(f, 10) + frameOffset for f in frames.split(','))
finishBuild: ->
return if _.isEmpty(@builder._animations)
key = @spriteSheetKey(@options)
spriteSheet = null
if @options.async
buildQueue.push @builder
@builder.t0 = new Date().getTime()
@builder.buildAsync() unless buildQueue.length > 1
@builder.on 'complete', @onBuildSpriteSheetComplete, @, true, [@builder, key, @options]
@builder = null
return key
spriteSheet = @builder.build()
@logBuild @t0, false, @options.portraitOnly
@spriteSheets[key] = spriteSheet
delete @building[key]
@builder = null
@options = null
spriteSheet
onBuildSpriteSheetComplete: (e, data) ->
[builder, key, options] = data
@logBuild builder.t0, true, options.portraitOnly
buildQueue = buildQueue.slice(1)
buildQueue[0].t0 = new Date().getTime() if buildQueue[0]
buildQueue[0]?.buildAsync()
@spriteSheets[key] = e.target.spriteSheet
delete @building[key]
@trigger 'build-complete', {key:key, thangType:@}
@vectorParser = null
logBuild: (startTime, async, portrait) ->
kind = if async then 'Async' else 'Sync '
portrait = if portrait then '(Portrait)' else ''
name = _.string.rpad @get('name'), 20
time = _.string.lpad '' + new Date().getTime() - startTime, 6
console.debug "Built sheet: #{name} #{time}ms #{kind} #{portrait}"
spriteSheetKey: (options) ->
colorConfigs = []
for groupName, config of options.colorConfig or {}
colorConfigs.push "#{groupName}:#{config.hue}|#{config.saturation}|#{config.lightness}"
colorConfigs = colorConfigs.join ','
portraitOnly = !!options.portraitOnly
"#{@get('name')} - #{options.resolutionFactor} - #{colorConfigs} - #{portraitOnly}"
getPortraitImage: (spriteOptionsOrKey, size=100) ->
src = @getPortraitSource(spriteOptionsOrKey, size)
return null unless src
$('').attr('src', src)
getPortraitSource: (spriteOptionsOrKey, size=100) ->
stage = @getPortraitStage(spriteOptionsOrKey, size)
stage?.toDataURL()
getPortraitStage: (spriteOptionsOrKey, size=100) ->
return unless @isFullyLoaded()
key = spriteOptionsOrKey
key = if _.isString(key) then key else @spriteSheetKey(@fillOptions(key))
spriteSheet = @spriteSheets[key]
if not spriteSheet
options = if _.isPlainObject spriteOptionsOrKey then spriteOptionsOrKey else {}
options.portraitOnly = true
spriteSheet = @buildSpriteSheet(options)
return if _.isString spriteSheet
return unless spriteSheet
canvas = $("")
stage = new createjs.Stage(canvas[0])
sprite = new createjs.Sprite(spriteSheet)
pt = @actions.portrait?.positions?.registration
sprite.regX = pt?.x or 0
sprite.regY = pt?.y or 0
sprite.framerate = @actions.portrait?.framerate ? 20
sprite.gotoAndStop 'portrait'
stage.addChild(sprite)
stage.update()
stage.startTalking = ->
sprite.gotoAndPlay 'portrait'
return if @tick
@tick = (e) => @update(e)
createjs.Ticker.addEventListener 'tick', @tick
stage.stopTalking = ->
sprite.gotoAndStop 'portrait'
@update()
createjs.Ticker.removeEventListener 'tick', @tick
@tick = null
stage
uploadGenericPortrait: (callback, src) ->
src ?= @getPortraitSource()
return callback?() unless src
src = src.replace('data:image/png;base64,', '').replace(/\ /g, '+')
body =
filename: 'portrait.png'
mimetype: 'image/png'
path: "db/thang.type/#{@get('original')}"
b64png: src
force: 'true'
$.ajax('/file', { type: 'POST', data: body, success: callback or @onFileUploaded })
onFileUploaded: =>
console.log 'Image uploaded'
@loadUniversalWizard: ->
return @wizardType if @wizardType
wizOriginal = "52a00d55cf1818f2be00000b"
url = "/db/thang.type/#{wizOriginal}/version"
@wizardType = new module.exports()
@wizardType.url = -> url
@wizardType.fetch()
@wizardType