Add spriteSheets to ThangType, export modal to Thang Editor

Units can be exported as rastered sprite sheets. This is the first part of the project,
the second part will be having the game use them.
This commit is contained in:
Scott Erickson 2015-10-12 16:47:48 -07:00
parent b38a6c5060
commit 919e0605e9
9 changed files with 307 additions and 34 deletions

View file

@ -0,0 +1,98 @@
SpriteBuilder = require('./SpriteBuilder')
ThangType = require('models/ThangType')
CocoClass = require('core/CocoClass')
class SpriteExporter extends CocoClass
'''
To be used by the ThangTypeEditView to export ThangTypes to single sprite sheets which can be uploaded to
GridFS and used in gameplay, avoiding rendering vector images.
Code has been copied and reworked and simplified from LayerAdapter. Some shared code has been refactored into
ThangType, but more work could be done to rethink and reorganize Sprite rendering.
'''
constructor: (thangType, options) ->
@thangType = thangType
options ?= {}
@colorConfig = options.colorConfig or {}
@resolutionFactor = options.resolutionFactor or 1
@actionNames = options.actionNames or (action.name for action in @thangType.getDefaultActions())
super()
build: (renderType) ->
spriteSheetBuilder = new createjs.SpriteSheetBuilder()
if (renderType or @thangType.get('spriteType') or 'segmented') is 'segmented'
@renderSegmentedThangType(spriteSheetBuilder)
else
@renderSingularThangType(spriteSheetBuilder)
try
spriteSheetBuilder.buildAsync()
catch e
@resolutionFactor *= 0.9
return @build()
spriteSheetBuilder.on 'complete', @onBuildSpriteSheetComplete, @, true, spriteSheetBuilder
@asyncBuilder = spriteSheetBuilder
renderSegmentedThangType: (spriteSheetBuilder) ->
containersToRender = @thangType.getContainersForActions(@actionNames)
spriteBuilder = new SpriteBuilder(@thangType, {colorConfig: @colorConfig})
for containerGlobalName in containersToRender
container = spriteBuilder.buildContainerFromStore(containerGlobalName)
frame = spriteSheetBuilder.addFrame(container, null, @resolutionFactor * (@thangType.get('scale') or 1))
spriteSheetBuilder.addAnimation(containerGlobalName, [frame], false)
renderSingularThangType: (spriteSheetBuilder) ->
actionObjects = _.values(@thangType.getActions())
animationActions = []
for a in actionObjects
continue unless a.animation
continue unless a.name in @actionNames
animationActions.push(a)
spriteBuilder = new SpriteBuilder(@thangType, {colorConfig: @colorConfig})
animationGroups = _.groupBy animationActions, (action) -> action.animation
for animationName, actions of animationGroups
scale = actions[0].scale or @thangType.get('scale') or 1
mc = spriteBuilder.buildMovieClip(animationName, null, null, null, {'temp':0})
spriteSheetBuilder.addMovieClip(mc, null, scale * @resolutionFactor)
frames = spriteSheetBuilder._animations['temp'].frames
framesMap = _.zipObject _.range(frames.length), frames
for action in actions
if action.frames
frames = (framesMap[parseInt(frame)] for frame in action.frames.split(','))
else
frames = _.sortBy(_.values(framesMap))
next = @nextForAction(action)
spriteSheetBuilder.addAnimation(action.name, frames, next)
containerActions = []
for a in actionObjects
continue unless a.container
continue unless a.name in @actionNames
containerActions.push(a)
containerGroups = _.groupBy containerActions, (action) -> action.container
for containerName, actions of containerGroups
container = spriteBuilder.buildContainerFromStore(containerName)
scale = actions[0].scale or @thangType.get('scale') or 1
frame = spriteSheetBuilder.addFrame(container, null, scale * @resolutionFactor)
for action in actions
spriteSheetBuilder.addAnimation(action.name, [frame], false)
onBuildSpriteSheetComplete: (e, builder) ->
if builder.spriteSheet._images.length > 1
total = 0
# get a rough estimate of how much smaller the spritesheet needs to be
for image, index in builder.spriteSheet._images
total += image.height / builder.maxHeight
@resolutionFactor /= (Math.max(1.1, Math.sqrt(total)))
@_renderNewSpriteSheet(e.async)
return
@trigger 'build', { spriteSheet: builder.spriteSheet }
module.exports = SpriteExporter

View file

@ -22,6 +22,7 @@ SpriteBuilder = require 'lib/sprites/SpriteBuilder'
CocoClass = require 'core/CocoClass'
SegmentedSprite = require './SegmentedSprite'
SingularSprite = require './SingularSprite'
ThangType = require 'models/ThangType'
NEVER_RENDER_ANYTHING = false # set to true to test placeholders
@ -42,7 +43,6 @@ module.exports = LayerAdapter = class LayerAdapter extends CocoClass
buildAutomatically: true
buildAsync: true
resolutionFactor: SPRITE_RESOLUTION_FACTOR
defaultActions: ['idle', 'die', 'move', 'attack']
numThingsLoading: 0
lanks: null
spriteSheet: null
@ -196,7 +196,7 @@ module.exports = LayerAdapter = class LayerAdapter extends CocoClass
@upsertActionToRender(lank.thangType)
else
for action in _.values(lank.thangType.getActions())
continue unless _.any @defaultActions, (prefix) -> _.string.startsWith(action.name, prefix)
continue unless _.any ThangType.defaultActions, (prefix) -> _.string.startsWith(action.name, prefix)
@upsertActionToRender(lank.thangType, action.name, lank.options.colorConfig)
upsertActionToRender: (thangType, actionName, colorConfig) ->
@ -346,17 +346,9 @@ module.exports = LayerAdapter = class LayerAdapter extends CocoClass
#- Rendering containers for segmented thang types
renderSegmentedThangType: (thangType, colorConfig, actionNames, spriteSheetBuilder) ->
containersToRender = {}
for actionName in actionNames
action = _.find(thangType.getActions(), {name: actionName})
if action.container
containersToRender[action.container] = true
else if action.animation
animationContainers = @getContainersForAnimation(thangType, action.animation, action)
containersToRender[container.gn] = true for container in animationContainers
containersToRender = thangType.getContainersForActions(actionNames)
spriteBuilder = new SpriteBuilder(thangType, {colorConfig: colorConfig})
for containerGlobalName in _.keys(containersToRender)
for containerGlobalName in containersToRender
containerKey = @renderGroupingKey(thangType, containerGlobalName, colorConfig)
if @spriteSheet?.resolutionFactor is @resolutionFactor and containerKey in @spriteSheet.getAnimations()
container = new createjs.Sprite(@spriteSheet)
@ -367,15 +359,6 @@ module.exports = LayerAdapter = class LayerAdapter extends CocoClass
frame = spriteSheetBuilder.addFrame(container, null, @resolutionFactor * (thangType.get('scale') or 1))
spriteSheetBuilder.addAnimation(containerKey, [frame], false)
getContainersForAnimation: (thangType, animation, action) ->
rawAnimation = thangType.get('raw').animations[animation]
if not rawAnimation
console.error 'thang type', thangType.get('name'), 'is missing animation', animation, 'from action', action
containers = rawAnimation.containers
for animation in thangType.get('raw').animations[animation].animations
containers = containers.concat(@getContainersForAnimation(thangType, animation.gn, action))
return containers
#- Rendering sprite sheets for singular thang types
renderSingularThangType: (thangType, colorConfig, actionNames, spriteSheetBuilder) ->

View file

@ -35,6 +35,7 @@ module.exports = class ThangType extends CocoModel
urlRoot: '/db/thang.type'
building: {}
editableByArtisans: true
@defaultActions: ['idle', 'die', 'move', 'attack']
initialize: ->
super()
@ -78,6 +79,14 @@ module.exports = class ThangType extends CocoModel
return {} unless @isFullyLoaded()
return @actions or @buildActions()
getDefaultActions: ->
actions = []
for action in _.values(@getActions())
continue unless _.any ThangType.defaultActions, (prefix) ->
_.string.startsWith(action.name, prefix)
actions.push(action)
return actions
buildActions: ->
return null unless @isFullyLoaded()
@actions = $.extend(true, {}, @get('actions'))
@ -479,3 +488,24 @@ module.exports = class ThangType extends CocoModel
playerLevel = me.constructor.levelForTier playerTier
#console.log 'Level required for', @get('name'), 'is', playerLevel, 'player tier', playerTier, 'because it is itemTier', itemTier, 'which is normally level', me.constructor.levelForTier(itemTier)
playerLevel
getContainersForAnimation: (animation, action) ->
rawAnimation = @get('raw').animations[animation]
if not rawAnimation
console.error 'thang type', @get('name'), 'is missing animation', animation, 'from action', action
containers = rawAnimation.containers
for animation in @get('raw').animations[animation].animations
containers = containers.concat(@getContainersForAnimation(animation.gn, action))
return containers
getContainersForActions: (actionNames) ->
containersToRender = {}
actions = @getActions()
for actionName in actionNames
action = _.find(actions, {name: actionName})
if action.container
containersToRender[action.container] = true
else if action.animation
animationContainers = @getContainersForAnimation(action.animation, action)
containersToRender[container.gn] = true for container in animationContainers
return _.keys(containersToRender)

View file

@ -169,6 +169,42 @@ _.extend ThangTypeSchema.properties,
extendedName: {type: 'string', title: 'Extended Hero Name', description: 'The long form of the hero\'s name. Ex.: "Captain Anya Weston".'}
unlockLevelName: {type: 'string', title: 'Unlock Level Name', description: 'The name of the level in which the hero is unlocked.'}
tasks: c.array {title: 'Tasks', description: 'Tasks to be completed for this ThangType.'}, c.task
spriteSheets: c.array {title: 'SpriteSheets'},
c.object {title: 'SpriteSheet'},
actionNames: { type: 'array' }
animations:
type: 'object'
description: 'Third EaselJS SpriteSheet animations format'
additionalProperties: {
description: 'EaselJS animation'
type: 'object'
properties: {
frames: { type: 'array' }
next: { type: ['string', 'null'] }
speed: { type: 'number' }
}
}
colorConfig: c.colorConfig()
colorLabel: { enum: ['red', 'green', 'blue'] }
frames:
type: 'array'
description: 'Second EaselJS SpriteSheet frames format'
items:
type: 'array'
items: [
{ type: 'number', title: 'x' }
{ type: 'number', title: 'y' }
{ type: 'number', title: 'width' }
{ type: 'number', title: 'height' }
{ type: 'number', title: 'imageIndex' }
{ type: 'number', title: 'regX' }
{ type: 'number', title: 'regY' }
]
image: { type: 'string', format: 'image-file' }
resolutionFactor: {
type: 'number'
}
spriteType: { enum: ['singular', 'segmented'], title: 'Sprite Type' }
ThangTypeSchema.required = []

View file

@ -0,0 +1,34 @@
extends /templates/core/modal-base
block modal-header-content
h4.modal-title Export #{view.thangType.get('name')} SpriteSheet
block modal-body-content
.form-horizontal
.form-group
label.col-sm-3.control-label Team Color
.col-sm-9
select#color-config-select.form-control
option(value='') None
option(value="red") Red
option(value="blue") Blue
option(value="green") Green
.form-group
label.col-sm-3.control-label Resolution Factor
.col-sm-9
input#resolution-input.form-control(value=3)
.form-group
label.col-sm-3.control-label Actions
.col-sm-9
- var defaultActionNames = _.pluck(view.thangType.getDefaultActions(), 'name')
- var actions = view.thangType.getActions()
for action in actions
.checkbox
label
input(type="checkbox" name="action" value=action.name checked=_.contains(defaultActionNames, action.name))
| #{action.name}
block modal-footer-content
button.btn.btn-default(data-dismiss="modal") Cancel
button#save-btn.btn.btn-primary Save

View file

@ -79,19 +79,25 @@ block outer_content
div.tab-pane#editor-thang-colors-tab-view
div.tab-pane#editor-thang-main-tab-view.active
div#settings-col.well
img#portrait.img-thumbnail
div.file-controls
button(disabled=authorized === true ? undefined : "true").btn.btn-sm.btn-info#upload-button
span.glyphicon.glyphicon-upload
span.spl Upload Animation
button(disabled=authorized === true ? undefined : "true").btn.btn-sm.btn-danger#clear-button
span.glyphicon.glyphicon-remove
span.spl Clear Data
button#set-vector-icon(disabled=authorized === true ? undefined : "true").btn.btn-sm
span.glyphicon.glyphicon-gbp
span.spl Vector Icon Setup
input#real-upload-button(type="file")
#thang-type-file-size= fileSizeString
.row
.col-sm-3
img#portrait.img-thumbnail
.col-sm-9
div.file-controls
button(disabled=authorized === true ? undefined : "true").btn.btn-sm.btn-info#upload-button
span.glyphicon.glyphicon-upload
span.spl Upload Animation
button(disabled=authorized === true ? undefined : "true").btn.btn-sm.btn-danger#clear-button
span.glyphicon.glyphicon-remove
span.spl Clear Data
button#set-vector-icon(disabled=authorized === true ? undefined : "true").btn.btn-sm
span.glyphicon.glyphicon-gbp
span.spl Vector Icon Setup
button#export-sprite-sheet-btn.btn.btn-sm(disabled=authorized === true ? undefined : "true")
span.glyphicon.glyphicon-export
span.spl Export SpriteSheet
input#real-upload-button(type="file")
#thang-type-file-size= fileSizeString
div#thang-type-treema
.clearfix
div#display-col.well

View file

@ -0,0 +1,78 @@
ModalView = require 'views/core/ModalView'
template = require 'templates/editor/thang/export-thang-type-modal'
SpriteExporter = require 'lib/sprites/SpriteExporter'
module.exports = class ExportThangTypeModal extends ModalView
id: "export-thang-type-modal"
template: template
plain: true
events:
'click #save-btn': 'onClickSaveButton'
initialize: (options, @thangType) ->
@builder = null
@getFilename = _.once(@getFilename)
colorMap: {
red: { hue: 0, saturation: 0.75, lightness: 0.5 }
blue: { hue: 0.66, saturation: 0.75, lightness: 0.5 }
green: { hue: 0.33, saturation: 0.75, lightness: 0.5 }
}
getColorLabel: -> @$('#color-config-select').val()
getColorConfig: -> @colorMap[@getColorLabel()]
getActionNames: -> _.map @$('input[name="action"]:checked'), (el) -> $(el).val()
getResolutionFactor: -> parseInt(@$('#resolution-input').val()) or SPRITE_RESOLUTION_FACTOR
getFilename: -> 'spritesheet-'+_.string.slugify(moment().format())+'.png'
onClickSaveButton: ->
options = {
resolutionFactor: @getResolutionFactor()
actionNames: @getActionNames()
colorConfig: @getColorConfig()
}
console.log 'options?', options
@exporter = new SpriteExporter(@thangType, options)
@exporter.build()
@listenToOnce @exporter, 'build', @onExporterBuild
onExporterBuild: (e) ->
@spriteSheet = e.spriteSheet
$('body').empty().append(@spriteSheet._images[0])
return
src = @spriteSheet._images[0].toDataURL()
src = src.replace('data:image/png;base64,', '').replace(/\ /g, '+')
body =
filename: @getFilename()
mimetype: 'image/png'
path: "db/thang.type/#{@thangType.get('original')}"
b64png: src
$.ajax('/file', {type: 'POST', data: body, success: @onSpriteSheetUploaded})
onSpriteSheetUploaded: =>
spriteSheetData = {
actionNames: @getActionNames()
animations: @spriteSheet._data
frames: ([
f.rect.x
f.rect.y
f.rect.width
f.rect.height
0
f.regX
f.regY
] for f in @spriteSheet._frames)
image: "db/thang.type/#{@thangType.get('original')}/"+@getFilename()
resolutionFactor: @getResolutionFactor()
}
if config = @getColorConfig()
spriteSheetData.colorConfig = config
if label = @getColorLabel()
spriteSheetData.colorLabel = label
spriteSheets = _.clone(@thangType.get('spriteSheets') or [])
spriteSheets.push(spriteSheetData)
@thangType.set('spriteSheets', spriteSheets)
@thangType.save()
@listenToOnce @thangType, 'sync', @hide
window.SomeModal = module.exports

View file

@ -20,6 +20,7 @@ VectorIconSetupModal = require 'views/editor/thang/VectorIconSetupModal'
SaveVersionModal = require 'views/editor/modal/SaveVersionModal'
template = require 'templates/editor/thang/thang-type-edit-view'
storage = require 'core/storage'
ExportThangTypeModal = require './ExportThangTypeModal'
CENTER = {x: 200, y: 400}
@ -157,6 +158,7 @@ module.exports = class ThangTypeEditView extends RootView
'mousedown #canvas': 'onCanvasMouseDown'
'mouseup #canvas': 'onCanvasMouseUp'
'mousemove #canvas': 'onCanvasMouseMove'
'click #export-sprite-sheet-btn': 'onClickExportSpriteSheetButton'
onClickSetVectorIcon: ->
modal = new VectorIconSetupModal({}, @thangType)
@ -208,6 +210,7 @@ module.exports = class ThangTypeEditView extends RootView
@patchesView = @insertSubView(new PatchesView(@thangType), @$el.find('.patches-view'))
@showReadOnly() if me.get('anonymous')
@updatePortrait()
@onClickExportSpriteSheetButton()
initComponents: =>
options =
@ -651,6 +654,10 @@ module.exports = class ThangTypeEditView extends RootView
@canvasDragOffset = null
node.set '/', offset
onClickExportSpriteSheetButton: ->
modal = new ExportThangTypeModal({}, @thangType)
@openModalView(modal)
destroy: ->
@camera?.destroy()
super()

View file

@ -35,6 +35,7 @@ ThangTypeHandler = class ThangTypeHandler extends Handler
'unlockLevelName'
'tasks'
'terrains'
'spriteSheets'
]
hasAccess: (req) ->