codecombat/app/lib/sprites/SpriteBuilder.coffee

191 lines
6.6 KiB
CoffeeScript

{hexToHSL, hslToHex} = require 'core/utils'
module.exports = class SpriteBuilder
constructor: (@thangType, @options) ->
@options ?= {}
raw = @thangType.get('raw')
@shapeStore = raw.shapes
@containerStore = raw.containers
@animationStore = raw.animations
@buildColorMaps()
setOptions: (@options) ->
buildMovieClip: (animationName, mode, startPosition, loops, labels) ->
animData = @animationStore[animationName]
unless animData
console.error 'couldn\'t find animData from', @animationStore, 'for', animationName
return null
locals = {}
_.extend locals, @buildMovieClipShapes(animData.shapes)
_.extend locals, @buildMovieClipContainers(animData.containers)
_.extend locals, @buildMovieClipAnimations(animData.animations)
_.extend locals, @buildMovieClipGraphics(animData.graphics)
anim = new createjs.MovieClip()
if not labels
labels = {}
labels[animationName] = 0
anim.initialize(mode ? createjs.MovieClip.INDEPENDENT, startPosition ? 0, loops ? true, labels)
for tweenData in animData.tweens
tween = createjs.Tween
stopped = false
for func in tweenData
args = _.cloneDeep(func.a)
@dereferenceArgs(args, locals)
if tween[func.n]
tween = tween[func.n](args...)
else
# If we, say, skipped a shadow get(), then the wait() may not be present
stopped = true
break
anim.timeline.addTween(tween) unless stopped
anim.nominalBounds = new createjs.Rectangle(animData.bounds...)
if animData.frameBounds
anim.frameBounds = (new createjs.Rectangle(bounds...) for bounds in animData.frameBounds)
anim
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)
@dereferenceArgs(val, locals)
args
buildMovieClipShapes: (localShapes) ->
map = {}
for localShape in localShapes
if localShape.im
shape = new createjs.Shape()
shape._off = true
else
shape = @buildShapeFromStore(localShape.gn)
if localShape.m
shape.mask = map[localShape.m]
map[localShape.bn] = shape
map
buildMovieClipContainers: (localContainers) ->
map = {}
for localContainer in localContainers
container = @buildContainerFromStore(localContainer.gn)
container.setTransform(localContainer.t...)
container._off = localContainer.o if localContainer.o?
container.alpha = localContainer.al if localContainer.al?
map[localContainer.bn] = container
map
buildMovieClipAnimations: (localAnimations) ->
map = {}
for localAnimation in localAnimations
animation = @buildMovieClip(localAnimation.gn, localAnimation.a...)
animation.setTransform(localAnimation.t...)
map[localAnimation.bn] = animation
map
buildMovieClipGraphics: (localGraphics) ->
map = {}
for localGraphic in localGraphics
graphic = new createjs.Graphics().p(localGraphic.p)
map[localGraphic.bn] = graphic
map
buildShapeFromStore: (shapeKey, debug=false) ->
shapeData = @shapeStore[shapeKey]
shape = new createjs.Shape()
if shapeData.lf?
shape.graphics.lf shapeData.lf...
else if shapeData.fc?
shape.graphics.f @colorMap[shapeKey] or shapeData.fc
else if shapeData.rf?
shape.graphics.rf shapeData.rf...
if shapeData.ls?
shape.graphics.ls shapeData.ls...
else if shapeData.sc?
shape.graphics.s shapeData.sc
shape.graphics.ss shapeData.ss... if shapeData.ss?
shape.graphics.de shapeData.de... if shapeData.de?
shape.graphics.p shapeData.p if shapeData.p?
shape.setTransform shapeData.t...
shape
buildContainerFromStore: (containerKey) ->
console.error 'Yo we don\'t have no containerKey' unless containerKey
contData = @containerStore[containerKey]
cont = new createjs.Container()
cont.initialize()
for childData in contData.c
if _.isString(childData)
child = @buildShapeFromStore(childData)
else
continue if not childData.gn
child = @buildContainerFromStore(childData.gn)
child.setTransform(childData.t...)
cont.addChild(child)
cont.bounds = new createjs.Rectangle(contData.b...)
cont
buildColorMaps: ->
@colorMap = {}
colorGroups = @thangType.get('colorGroups')
return if _.isEmpty colorGroups
return unless _.size @shapeStore # We don't have the shapes loaded because we are doing a prerendered spritesheet approach
colorConfig = @options.colorConfig
# colorConfig ?= {team: {hue:0.4, saturation: -0.5, lightness: -0.5}} # test config
return if not colorConfig
for group, config of colorConfig
continue unless colorGroups[group] # color group not found...
@buildColorMapForGroup(colorGroups[group], config)
buildColorMapForGroup: (shapes, config) ->
return unless shapes.length
colors = @initColorMap(shapes)
@adjustHuesForColorMap(colors, config.hue)
@adjustValueForColorMap(colors, 1, config.saturation)
@adjustValueForColorMap(colors, 2, config.lightness)
@applyColorMap(shapes, colors)
initColorMap: (shapes) ->
colors = {}
for shapeKey in shapes
shape = @shapeStore[shapeKey]
continue if (not shape.fc?) or colors[shape.fc]
hsl = hexToHSL(shape.fc)
colors[shape.fc] = hsl
colors
adjustHuesForColorMap: (colors, targetHue) ->
hues = (hsl[0] for hex, hsl of colors)
# 'rotate' the hue spectrum so averaging works
if Math.max(hues) - Math.min(hues) > 0.5
hues = (if h < 0.5 then h + 1.0 else h for h in hues)
averageHue = sum(hues) / hues.length
averageHue %= 1
# end result should be something like a hue array of [0.9, 0.3] gets an average of 0.1
targetHue ?= 0
diff = targetHue - averageHue
hsl[0] = (hsl[0] + diff + 1) % 1 for hex, hsl of colors
adjustValueForColorMap: (colors, index, targetValue) ->
values = (hsl[index] for hex, hsl of colors)
averageValue = sum(values) / values.length
targetValue ?= 0.5
diff = targetValue - averageValue
for hex, hsl of colors
hsl[index] = Math.max(0, Math.min(1, hsl[index] + diff))
applyColorMap: (shapes, colors) ->
for shapeKey in shapes
shape = @shapeStore[shapeKey]
continue if (not shape.fc?) or not(colors[shape.fc])
@colorMap[shapeKey] = hslToHex(colors[shape.fc])
sum = (nums) -> _.reduce(nums, (s, num) -> s + num)