codecombat/app/models/ThangType.coffee

245 lines
8.6 KiB
CoffeeScript
Raw Normal View History

2014-01-03 13:32:13 -05:00
CocoModel = require('./CocoModel')
SpriteBuilder = require 'lib/sprites/SpriteBuilder'
buildQueue = []
2014-01-03 13:32:13 -05:00
module.exports = class ThangType extends CocoModel
@className: "ThangType"
@schema: require 'schemas/models/thang_type'
2014-01-03 13:32:13 -05:00
urlRoot: "/db/thang.type"
building: {}
2014-01-03 13:32:13 -05:00
initialize: ->
super()
@building = {}
2014-01-03 13:32:13 -05:00
@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
2014-01-03 13:32:13 -05:00
getActions: ->
return {} unless @isFullyLoaded()
2014-01-03 13:32:13 -05:00
return @actions or @buildActions()
2014-01-03 13:32:13 -05:00
buildActions: ->
return null unless @isFullyLoaded()
@actions = $.extend(true, {}, @get('actions'))
2014-01-03 13:32:13 -05:00
for name, action of @actions
action.name = name
for relatedName, relatedAction of action.relatedActions ? {}
relatedAction.name = action.name + "_" + relatedName
@actions[relatedAction.name] = relatedAction
@actions
2014-01-03 13:32:13 -05:00
getSpriteSheet: (options) ->
options = @fillOptions options
key = @spriteSheetKey(options)
return @spriteSheets[key] or @buildSpriteSheet(options)
2014-01-03 13:32:13 -05:00
fillOptions: (options) ->
options ?= {}
options = _.clone options
options.resolutionFactor ?= 4
options.async ?= false
options
buildSpriteSheet: (options) ->
return false unless @isFullyLoaded()
@options = @fillOptions options
key = @spriteSheetKey(@options)
if ss = @spriteSheets[key] then return ss
return if @building[key]
@t0 = new Date().getTime()
@initBuild(options)
@addGeneralFrames() unless @options.portraitOnly
@addPortrait()
@building[key] = true
result = @finishBuild()
return result
2014-01-03 13:32:13 -05:00
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.
2014-01-06 15:37:35 -05:00
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
2014-01-09 01:30:00 -05:00
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
2014-01-03 13:32:13 -05:00
addGeneralFrames: ->
2014-01-03 13:32:13 -05:00
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
2014-01-03 13:32:13 -05:00
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?
2014-01-03 13:32:13 -05:00
next = true
next = action.goesTo if action.goesTo
next = false if action.loops is false
@builder.addAnimation name, frames, next
2014-01-03 13:32:13 -05:00
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
2014-01-09 01:30:00 -05:00
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(','))
2014-01-03 13:32:13 -05:00
finishBuild: ->
return if _.isEmpty(@builder._animations)
key = @spriteSheetKey(@options)
2014-01-03 13:32:13 -05:00
spriteSheet = null
if @options.async
buildQueue.push @builder
2014-03-19 20:11:45 -04:00
@builder.t0 = new Date().getTime()
@builder.buildAsync() unless buildQueue.length > 1
@builder.on 'complete', @onBuildSpriteSheetComplete, @, true, key
2014-01-03 13:32:13 -05:00
return true
spriteSheet = @builder.build()
console.debug "Built #{@get('name')} in #{new Date().getTime() - @t0}ms."
@spriteSheets[key] = spriteSheet
delete @building[key]
2014-01-03 13:32:13 -05:00
spriteSheet
2014-01-03 13:32:13 -05:00
onBuildSpriteSheetComplete: (e, key) ->
2014-03-19 20:11:45 -04:00
console.log "Built #{@get('name')} async in #{new Date().getTime() - @builder.t0}ms." if @builder
buildQueue = buildQueue.slice(1)
2014-03-19 20:11:45 -04:00
buildQueue[0].t0 = new Date().getTime() if buildQueue[0]
buildQueue[0]?.buildAsync()
2014-01-03 13:32:13 -05:00
@spriteSheets[key] = e.target.spriteSheet
delete @building[key]
2014-01-03 13:32:13 -05:00
@trigger 'build-complete'
@builder = null
@vectorParser = null
2014-01-03 13:32:13 -05:00
spriteSheetKey: (options) ->
colorConfigs = []
for groupName, config of options.colorConfig or {}
colorConfigs.push "#{groupName}:#{config.hue}|#{config.saturation}|#{config.lightness}"
colorConfigs = colorConfigs.join ','
2014-03-05 15:53:48 -05:00
portraitOnly = !!options.portraitOnly
"#{@get('name')} - #{options.resolutionFactor} - #{colorConfigs} - #{portraitOnly}"
2014-01-03 13:32:13 -05:00
2014-01-06 15:37:35 -05:00
getPortraitImage: (spriteOptionsOrKey, size=100) ->
src = @getPortraitSource(spriteOptionsOrKey, size)
2014-01-09 01:30:00 -05:00
return null unless src
2014-01-06 18:36:35 -05:00
$('<img />').attr('src', src)
2014-01-06 15:37:35 -05:00
getPortraitSource: (spriteOptionsOrKey, size=100) ->
2014-01-09 01:30:00 -05:00
stage = @getPortraitStage(spriteOptionsOrKey, size)
stage?.toDataURL()
getPortraitStage: (spriteOptionsOrKey, size=100) ->
return unless @isFullyLoaded()
2014-01-03 13:32:13 -05:00
key = spriteOptionsOrKey
2014-01-09 14:04:22 -05:00
key = if _.isString(key) then key else @spriteSheetKey(@fillOptions(key))
2014-01-03 13:32:13 -05:00
spriteSheet = @spriteSheets[key]
if not spriteSheet
options = if _.isPlainObject spriteOptionsOrKey then spriteOptionsOrKey else {}
options.portraitOnly = true
spriteSheet = @buildSpriteSheet(options)
2014-01-03 13:32:13 -05:00
return unless spriteSheet
canvas = $("<canvas width='#{size}' height='#{size}'></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
2014-01-03 13:32:13 -05:00
sprite.gotoAndStop 'portrait'
stage.addChild(sprite)
stage.update()
2014-01-09 01:30:00 -05:00
stage.startTalking = ->
sprite.gotoAndPlay 'portrait'
2014-01-09 14:04:22 -05:00
return if @tick
@tick = (e) => @update(e)
2014-01-09 01:30:00 -05:00
createjs.Ticker.addEventListener 'tick', @tick
stage.stopTalking = ->
2014-01-09 14:04:22 -05:00
sprite.gotoAndStop 'portrait'
@update()
2014-01-09 01:30:00 -05:00
createjs.Ticker.removeEventListener 'tick', @tick
2014-01-09 14:04:22 -05:00
@tick = null
2014-01-09 01:30:00 -05:00
stage
uploadGenericPortrait: (callback, src) ->
src ?= @getPortraitSource()
2014-01-09 14:04:22 -05:00
return callback?() unless src
2014-01-06 15:37:35 -05:00
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'
2014-01-06 18:36:35 -05:00
$.ajax('/file', { type: 'POST', data: body, success: callback or @onFileUploaded })
2014-01-06 15:37:35 -05:00
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