codecombat/app/lib/sprites/SpriteParser.coffee

499 lines
21 KiB
CoffeeScript

module.exports = class SpriteParser
constructor: (@thangTypeModel) ->
# Create a new ThangType, or work with one we've been building
@thangType = _.cloneDeep(@thangTypeModel.attributes.raw)
@thangType ?= {shapes: {}, containers: {}, animations: {}}
# Internal parser state
@shapeLongKeys = {}
@containerLongKeys = {}
@containerRenamings = {}
@animationLongKeys = {}
@animationRenamings = {}
@populateLongKeys()
populateLongKeys: ->
for shortKey, shape of @thangType.shapes
longKey = JSON.stringify(_.values(shape))
@shapeLongKeys[longKey] = shortKey
for shortKey, container of @thangType.containers
longKey = JSON.stringify(_.values(container))
@containerLongKeys[longKey] = shortKey
for shortKey, animation of @thangType.animations
longKey = JSON.stringify(_.values(animation))
@animationLongKeys[longKey] = shortKey
parse: (source) ->
# Grab the library properties' width/height so we can subtract half of each from frame bounds
properties = source.match(/.*lib\.properties = \{\n.*?width: (\d+),\n.*?height: (\d+)/im)
@width = parseInt(properties[1] ? "0", 10)
@height = parseInt(properties[2] ? "0", 10)
options = {loc: false, range: true}
ast = esprima.parse source, options
blocks = @findBlocks ast, source
containers = _.filter blocks, {kind: 'Container'}
movieClips = _.filter blocks, {kind: 'MovieClip'}
if movieClips.length
# First movie clip is root, so do it last
movieClips = movieClips[1 ... movieClips.length].concat([movieClips[0]])
else if containers.length
# First container is root, so do it last
containers = containers[1 ... containers.length].concat([containers[0]])
mainClip = _.last(movieClips) ? _.last(containers)
@animationName = mainClip.name
for container in containers
[shapeKeys, localShapes] = @getShapesFromBlock container, source
localContainers = @getContainersFromMovieClip container, source
addChildArgs = @getAddChildCallArguments container, source
instructions = []
for bn in addChildArgs
gotIt = false
for shape in localShapes
if shape.bn is bn
instructions.push shape.gn
gotIt = true
break
continue if gotIt
for c in localContainers
if c.bn is bn
instructions.push { t: c.t, gn: c.gn }
break
@addContainer {c: instructions, b: container.bounds}, container.name
for movieClip in movieClips
localGraphics = @getGraphicsFromBlock(movieClip, source)
[shapeKeys, localShapes] = @getShapesFromBlock movieClip, source
localContainers = @getContainersFromMovieClip movieClip, source, true
localAnimations = @getAnimationsFromMovieClip movieClip, source, true
localTweens = @getTweensFromMovieClip movieClip, source, localShapes, localContainers, localAnimations
@addAnimation {
shapes: localShapes
containers: localContainers
animations: localAnimations
tweens: localTweens
graphics: localGraphics
bounds: movieClip.bounds
frameBounds: movieClip.frameBounds
}, movieClip.name
@saveToModel()
return movieClips[0]?.name
saveToModel: ->
@thangTypeModel.set('raw', @thangType)
addShape: (shape) ->
longKey = JSON.stringify(_.values(shape))
shortKey = @shapeLongKeys[longKey]
unless shortKey?
shortKey = '' + _.size @thangType.shapes
shortKey += '+' while @thangType.shapes[shortKey]
@thangType.shapes[shortKey] = shape
@shapeLongKeys[longKey] = shortKey
return shortKey
addContainer: (container, name) ->
longKey = JSON.stringify(_.values(container))
shortKey = @containerLongKeys[longKey]
if not shortKey?
shortKey = name
if @thangType.containers[shortKey]?
shortKey = @animationName + ":" + name
@thangType.containers[shortKey] = container
@containerLongKeys[longKey] = shortKey
@containerRenamings[name] = shortKey
return shortKey
addAnimation: (animation, name) ->
longKey = JSON.stringify(_.values(animation))
shortKey = @animationLongKeys[longKey]
if shortKey?
@animationRenamings[shortKey] = name
else
shortKey = name
@thangType.animations[shortKey] = animation
@animationLongKeys[longKey] = shortKey
@animationRenamings[name] = name
return shortKey
walk: (node, parent, fn) ->
node.parent = parent
for key, child of node
continue if key is 'parent'
if _.isArray child
for grandchild in child
@walk grandchild, node, fn if _.isString grandchild?.type
else if _.isString child?.type
node.parent = parent
@walk child, node, fn
fn node
orphanify: (node) ->
delete node.parent if node.parent
for key, child of node
continue if key is 'parent'
if _.isArray child
for grandchild in child
@orphanify grandchild if _.isString grandchild?.type
else if _.isString child?.type
delete node.parent if node.parent
@orphanify child
subSourceFromRange: (range, source) ->
source[range[0] ... range[1]]
grabFunctionArguments: (source, literal=false) ->
# Replace first and last parens with brackets to turn args into array
args = source.replace(/.*?\(/, '[').replace(/\)[^)]*?$/, ']')
if literal then eval(args) else args
findBlocks: (ast, source) ->
functionExpressions = []
rectangles = []
gatherFunctionExpressions = (node) =>
if node.type is 'FunctionExpression'
name = node.parent?.left?.property?.name
if name
expression = node.parent.parent
kind = expression.parent.right.right.callee.property.name
statement = node.parent.parent.parent.parent
statementIndex = _.indexOf statement.parent.body, statement
nominalBoundsStatement = statement.parent.body[statementIndex + 1]
nominalBoundsRange = nominalBoundsStatement.expression.right.range
nominalBoundsSource = @subSourceFromRange nominalBoundsRange, source
nominalBounds = @grabFunctionArguments nominalBoundsSource, true
frameBoundsStatement = statement.parent.body[statementIndex + 2]
if frameBoundsStatement
frameBoundsRange = frameBoundsStatement.expression.right.range
frameBoundsSource = @subSourceFromRange frameBoundsRange, source
if frameBoundsSource.search(/\[rect/) is -1 # some other statement; we don't have multiframe bounds
console.log "Didn't have multiframe bounds for this movie clip."
frameBounds = [nominalBounds]
else
lastRect = nominalBounds
frameBounds = []
for arg, i in frameBoundsStatement.expression.right.elements
bounds = null
argSource = @subSourceFromRange arg.range, source
if arg.type is 'Identifier'
bounds = lastRect
else if arg.type is 'NewExpression'
bounds = @grabFunctionArguments argSource, true
else if arg.type is 'AssignmentExpression'
bounds = @grabFunctionArguments argSource.replace('rect=', ''), true
lastRect = bounds
else if arg.type is 'Literal' and arg.value is null
bounds = [0, 0, 1, 1] # Let's try this.
frameBounds.push bounds
else
console.log "Didn't have multiframe bounds for this movie clip!"
frameBounds = [nominalBounds]
# Subtract half of width/height parsed from lib.properties
for bounds in frameBounds
bounds[0] -= @width / 2
bounds[1] -= @height / 2
functionExpressions.push {name: name, bounds: nominalBounds, frameBounds: frameBounds, expression: node.parent.parent, kind: kind}
@walk ast, null, gatherFunctionExpressions
functionExpressions
###
this.shape_1.graphics.f("#605E4A").s().p("AAOD/IgOgaIAEhkIgmgdIgMgBIgPgFIgVgJQA1h9g8jXQAQAHAOASQAQAUAKAeQARAuAJBJQAHA/gBA5IAAADIACAfIAFARIACAGIAEAHIAHAHQAVAXAQAUQAUAaANAUIABACIgsgdIgggXIAAAnIABAwIgBgBg");
this.shape_1.sett(23.2,30.1);
this.shape.graphics.f().s("#000000").ss(0.1,1,1).p("AAAAAQAAAAAAAA");
this.shape.sett(3.8,22.4);
###
getGraphicsFromBlock: (block, source) ->
block = block.expression.object.right.body
localGraphics = []
gatherShapeDefinitions = (node) =>
return unless node.type is 'NewExpression' and node.callee.property.name is 'Graphics'
blockName = node.parent.parent.parent.id.name
graphicsString = node.parent.parent.arguments[0].value
localGraphics.push({p:graphicsString, bn:blockName})
@walk block, null, gatherShapeDefinitions
return localGraphics
getShapesFromBlock: (block, source) ->
block = block.expression.object.right.body
shapeKeys = []
localShapes = []
gatherShapeDefinitions = (node) =>
return unless node.type is 'MemberExpression'
name = node.object?.object?.property?.name
if not name
name = node.parent?.parent?.id?.name
return unless name and name.indexOf('mask') is 0 and node.property?.name is 'Shape'
shape = { bn: name, im: true }
localShapes.push shape
return
return unless name.search('shape') is 0 and node.object.property?.name is 'graphics'
fillCall = node.parent
if fillCall.callee.property.name is 'lf'
linearGradientFillSource = @subSourceFromRange fillCall.parent.range, source
linearGradientFill = @grabFunctionArguments linearGradientFillSource.replace(/.*?lf\(/, 'lf('), true
else
fillColor = fillCall.arguments[0]?.value ? null
console.error "What is this?! Not a fill!" unless fillCall.callee.property.name is 'f'
strokeCall = node.parent.parent.parent.parent
if strokeCall.object.callee.property.name is 'ls'
linearGradientStrokeSource = @subSourceFromRange strokeCall.parent.range, source
linearGradientStroke = @grabFunctionArguments linearGradientStrokeSource.replace(/.*?ls\(/, 'ls(').replace(/\).ss\(.*/, ')'), true
else
strokeColor = strokeCall.object.arguments?[0]?.value ? null
console.error "What is this?! Not a stroke!" unless strokeCall.object.callee.property.name is 's'
strokeStyle = null
graphicsStatement = strokeCall.parent
if strokeColor or linearGradientStroke
# There might now be an extra node, ss, for stroke style
strokeStyleSource = @subSourceFromRange strokeCall.parent.range, source
if strokeStyleSource.search(/ss\(/) isnt -1
strokeStyle = @grabFunctionArguments strokeStyleSource.replace(/.*?ss\(/, 'ss('), true
graphicsStatement = strokeCall.parent.parent.parent
if graphicsStatement.callee.property.name is 'de'
drawEllipseSource = @subSourceFromRange graphicsStatement.parent.range, source
drawEllipse = @grabFunctionArguments drawEllipseSource.replace(/.*?de\(/, 'de('), true
else
path = graphicsStatement.arguments?[0]?.value ? null
console.error "What is this?! Not a path!" unless graphicsStatement.callee.property.name is 'p'
body = graphicsStatement.parent.parent.body
graphicsStatementIndex = _.indexOf body, graphicsStatement.parent
t = body[graphicsStatementIndex + 1].expression
tSource = @subSourceFromRange t.range, source
if tSource.search('setTransform') is -1
t = [0, 0]
else
t = @grabFunctionArguments tSource, true
for statement in body.slice(graphicsStatementIndex + 2)
# Handle things like
# this.shape.mask = this.shape_1.mask = this.shape_2.mask = this.shape_3.mask = mask;
continue unless statement.expression?.left?.property?.name is 'mask'
exp = statement.expression
matchedName = false
while exp
matchedName = matchedName or exp.left?.object?.property?.name is name
mask = exp.name
exp = exp.right
continue unless matchedName
break
shape = {t: t}
shape.p = path if path
shape.de = drawEllipse if drawEllipse
shape.sc = strokeColor if strokeColor
shape.ss = strokeStyle if strokeStyle
shape.fc = fillColor if fillColor
shape.lf = linearGradientFill if linearGradientFill
shape.ls = linearGradientStroke if linearGradientStroke
if name.search('shape') isnt -1 and shape.fc is "rgba(0,0,0,0.451)" and not shape.ss and not shape.sc
console.log "Skipping a shadow", name, shape, "because we're doing shadows separately now."
return
shapeKeys.push shapeKey = @addShape shape
localShape = {bn: name, gn: shapeKey}
localShape.m = mask if mask
localShapes.push localShape
@walk block, null, gatherShapeDefinitions
return [shapeKeys, localShapes]
getContainersFromMovieClip: (movieClip, source, possibleAnimations=false) ->
block = movieClip.expression.object.right.body
localContainers = []
gatherContainerDefinitions = (node) =>
return unless node.type is 'Identifier' and node.name is 'lib'
args = node.parent.parent.arguments
libName = node.parent.property.name
return if args.length and not possibleAnimations # might be animation, not container
gn = @containerRenamings[libName]
return if possibleAnimations and not gn # not a container we know about
bn = node.parent.parent.parent.left.property.name
expressionStatement = node.parent.parent.parent.parent
body = expressionStatement.parent.body
expressionStatementIndex = _.indexOf body, expressionStatement
t = body[expressionStatementIndex + 1].expression
tSource = @subSourceFromRange t.range, source
t = @grabFunctionArguments tSource, true
o = body[expressionStatementIndex + 2].expression
localContainer = {bn: bn, t: t, gn: gn}
if o and o.left?.object?.property?.name is bn and o.left.property?.name is '_off'
localContainer.o = o.right.value
else if o and o.left?.property?.name is 'alpha'
localContainer.al = o.right.value
localContainers.push localContainer
@walk block, null, gatherContainerDefinitions
return localContainers
getAnimationsFromMovieClip: (movieClip, source, possibleContainers=false) ->
block = movieClip.expression.object.right.body
localAnimations = []
gatherAnimationDefinitions = (node) =>
return unless node.type is 'Identifier' and node.name is 'lib'
args = node.parent.parent.arguments
libName = node.parent.property.name
return unless args.length or possibleContainers # might be container, not animation
return if @containerRenamings[libName] and not @animationRenamings[libName] # we have it as a container
args = @grabFunctionArguments @subSourceFromRange(node.parent.parent.range, source), true
bn = node.parent.parent.parent.left.property.name
expressionStatement = node.parent.parent.parent.parent
body = expressionStatement.parent.body
expressionStatementIndex = _.indexOf body, expressionStatement
t = body[expressionStatementIndex + 1].expression
tSource = @subSourceFromRange t.range, source
t = @grabFunctionArguments tSource, true
gn = @animationRenamings[libName] ? libName
localAnimation = {bn: bn, t: t, gn: gn, a: args}
localAnimations.push localAnimation
@walk block, null, gatherAnimationDefinitions
return localAnimations
getTweensFromMovieClip: (movieClip, source, localShapes, localContainers, localAnimations) ->
block = movieClip.expression.object.right.body
localTweens = []
gatherTweens = (node) =>
return unless node.property?.name is 'addTween'
callExpressions = []
tweenNode = node
gatherCallExpressions = (node) =>
return unless node.type is 'CallExpression'
name = node.callee.property?.name
return unless name in ['get', 'to', 'wait']
return if name is 'get' and callExpressions.length # avoid Ease calls in the tweens
flattenedRanges = _.flatten [a.range for a in node.arguments]
range = [_.min(flattenedRanges), _.max(flattenedRanges)]
# Replace "this.<local>" references with just the "name"
argsSource = @subSourceFromRange(range, source)
argsSource = argsSource.replace(/mask/g, 'this.mask') # so the mask thing will be handled correctly as a blockName in the next line
argsSource = argsSource.replace(/this\.([a-z_0-9]+)/ig, '"$1"') # turns this.shape literal to "shape" string
argsSource = argsSource.replace(/cjs(.+)\)/, '"createjs$1)"') # turns cjs.Ease.get(0.5)
args = eval "[#{argsSource}]"
if args[0]?.state?[0]?.t?.search?("shape") is 0 and not _.find(localShapes, bn: args[0].state[0].t)
console.log "Skipping tween", name, argsSource, args, "from localShapes", localShapes, "presumably because it's a shadow we skipped."
return
callExpressions.push {n: name, a: args}
@walk node.parent.parent, null, gatherCallExpressions
localTweens.push callExpressions
@walk block, null, gatherTweens
return localTweens
getAddChildCallArguments: (block, source) ->
block = block.expression.object.right.body
localArgs = []
gatherAddChildCalls = (node) =>
return unless node.type is "Identifier" and node.name is "addChild"
args = node.parent.parent.arguments
args = (arg.property.name for arg in args)
localArgs.push arg for arg in args
return
@walk block, null, gatherAddChildCalls
return localArgs
###
this.timeline.addTween(cjs.Tween.get(this.instance).to({scaleX:0.82,scaleY:0.79,rotation:-10.8,x:98.4,y:-86.5},4).to({scaleY:0.7,rotation:9.3,x:95.6,y:-48.8},1).to({scaleX:0.82,scaleY:0.61,rotation:29.4,x:92.8,y:-11},1).to({regX:7.3,scaleX:0.82,scaleY:0.53,rotation:49.7,x:90.1,y:26.6},1).to({regX:7.2,regY:29.8,scaleY:0.66,rotation:19.3,x:101.2,y:-27.8},2).to({regY:29.9,scaleY:0.79,rotation:-10.8,x:98.4,y:-86.5},2).to({scaleX:0.84,scaleY:0.83,rotation:-30.7,x:68.4,y:-110},2).to({regX:7.3,scaleX:0.84,scaleY:0.84,rotation:-33.9,x:63.5,y:-114},1).wait(1));
###
###
simpleSample = """
(function (lib, img, cjs) {
var p; // shortcut to reference prototypes
// stage content:
(lib.enemy_flying_move_side = function(mode,startPosition,loop) {
this.initialize(mode,startPosition,loop,{});
// D_Head
this.instance = new lib.Dragon_Head();
this.instance.setTransform(227,200.5,1,1,0,0,0,51.9,42.5);
this.timeline.addTween(cjs.Tween.get(this.instance).to({y:182.9},7).to({y:200.5},7).wait(1));
// Layer 7
this.shape = new cjs.Shape();
this.shape.graphics.f("#4F6877").s().p("AgsAxQgSgVgB");
this.shape.setTransform(283.1,146.1);
// Layer 7 2
this.shape_1 = new cjs.Shape();
this.shape_1.graphics.f("rgba(255,255,255,0.4)").s().p("ArTs0QSMB7EbVGQhsBhiGBHQjg1IvVkhg");
this.shape_1.setTransform(400.2,185.5);
this.timeline.addTween(cjs.Tween.get({}).to({state:[]}).to({state:[{t:this.shape}]},7).to({state:[]},2).wait(6));
// Wing
this.instance_9 = new lib.Wing_Animation("synched",0);
this.instance_9.setTransform(313.9,145.6,1,1,0,0,0,49,-83.5);
this.timeline.addTween(cjs.Tween.get(this.instance_9).to({y:128,startPosition:7},7).wait(1));
// Example hard one with two shapes
this.timeline.addTween(cjs.Tween.get({}).to({state:[]}).to({state:[{t:this.shape}]},7).to({state:[{t:this.shape_1}]},1).to({state:[]},1).wait(7));
}).prototype = p = new cjs.MovieClip();
p.nominalBounds = new cjs.Rectangle(7.1,48.9,528.7,431.1);
(lib.Dragon_Head = function() {
this.initialize();
// Isolation Mode
this.shape = new cjs.Shape();
this.shape.graphics.f("#1D2226").s().p("AgVAwQgUgdgN");
this.shape.setTransform(75,25.8);
this.shape_1 = new cjs.Shape();
this.shape_1.graphics.f("#1D2226").s().p("AgnBXQACABAF");
this.shape_1.setTransform(80.8,22);
this.addChild(this.shape_1,this.shape);
}).prototype = p = new cjs.Container();
p.nominalBounds = new cjs.Rectangle(5.8,0,87.9,85);
(lib.WingPart_01 = function() {
this.initialize();
// Layer 1
this.shape = new cjs.Shape();
this.shape.graphics.f("#DBDDBC").s().p("Ag3BeQgCgRA");
this.shape.setTransform(10.6,19.7,1.081,1.081);
this.shape_1 = new cjs.Shape();
this.shape_1.graphics.f("#1D2226").s().p("AB4CDQgGg");
this.shape_1.setTransform(19.9,17.6,1.081,1.081);
this.shape_2 = new cjs.Shape();
this.shape_2.graphics.f("#605E4A").s().p("AiECbQgRg");
this.shape_2.setTransform(19.5,18.4,1.081,1.081);
this.addChild(this.shape_2,this.shape_1,this.shape);
}).prototype = p = new cjs.Container();
p.nominalBounds = new cjs.Rectangle(0,-3.1,40,41.6);
(lib.Wing_Animation = function(mode,startPosition,loop) {
this.initialize(mode,startPosition,loop,{});
// WP_02
this.instance = new lib.WingPart_01();
this.instance.setTransform(53.6,-121.9,0.854,0.854,-40.9,0,0,7.2,29.9);
this.timeline.addTween(cjs.Tween.get(this.instance).to({scaleY:0.7,rotation:9.3,x:95.6,y:-48.8},1).wait(1));
}).prototype = p = new cjs.MovieClip();
p.nominalBounds = new cjs.Rectangle(-27.7,-161.6,153.4,156.2);
})(lib = lib||{}, images = images||{}, createjs = createjs||{});
var lib, images, createjs;
"""
###