From 1a3c6498e82c7a32bbac8b88b471e8254c9a5e5a Mon Sep 17 00:00:00 2001 From: Scott Erickson Date: Fri, 10 Jan 2014 16:48:28 -0800 Subject: [PATCH] Built the thang editor colors tab. --- app/lib/sprites/SpriteBuilder.coffee | 67 ++++++- app/lib/utils.coffee | 18 +- app/styles/editor/thang/colors_tab.sass | 55 ++++++ app/templates/editor/thang/colors_tab.jade | 22 +++ app/templates/editor/thang/edit.jade | 4 + app/views/editor/thang/colors_tab_view.coffee | 164 ++++++++++++++++++ app/views/editor/thang/edit.coffee | 10 +- app/views/kinds/CocoView.coffee | 9 + server/handlers/thang_type.coffee | 1 + server/schemas/thang_type.coffee | 6 + 10 files changed, 347 insertions(+), 9 deletions(-) create mode 100644 app/styles/editor/thang/colors_tab.sass create mode 100644 app/templates/editor/thang/colors_tab.jade create mode 100644 app/views/editor/thang/colors_tab_view.coffee diff --git a/app/lib/sprites/SpriteBuilder.coffee b/app/lib/sprites/SpriteBuilder.coffee index 154e90cc0..95d3a67a8 100644 --- a/app/lib/sprites/SpriteBuilder.coffee +++ b/app/lib/sprites/SpriteBuilder.coffee @@ -1,9 +1,13 @@ +{hexToHSL, hslToHex} = require 'lib/utils' + module.exports = class SpriteBuilder constructor: (@thangType, @options) -> + @options ?= {} raw = _.cloneDeep(@thangType.get('raw')) @shapeStore = raw.shapes @containerStore = raw.containers @animationStore = raw.animations + @buildColorMaps() setOptions: (@options) -> @@ -93,7 +97,7 @@ module.exports = class SpriteBuilder if shapeData.lf? shape.graphics.lf shapeData.lf... else if shapeData.fc? - shape.graphics.f shapeData.fc + shape.graphics.f @colorMap[shapeKey] or shapeData.fc if shapeData.ls? shape.graphics.ls shapeData.ls... else if shapeData.sc? @@ -118,3 +122,64 @@ module.exports = class SpriteBuilder cont.addChild(child) cont.bounds = new createjs.Rectangle(contData.b...) cont + + buildColorMaps: -> + @colorMap = {} + colorGroups = @thangType.get('colorGroups') + return if _.isEmpty colorGroups + 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) + @adjustColorMap(colors, 1, config.lightness) + @adjustColorMap(colors, 2, config.saturation) + @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 + + adjustColorMap: (colorMap, index, adjustment) -> + return unless adjustment + for hex, hsl of colorMap + value = hsl[index] + if adjustment > 0 + hsl[index] = value + (1 - value) * adjustment + else + hsl[index] = value + value * adjustment + + 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) \ No newline at end of file diff --git a/app/lib/utils.coffee b/app/lib/utils.coffee index 499fdcc3b..8fe47dbee 100644 --- a/app/lib/utils.coffee +++ b/app/lib/utils.coffee @@ -28,4 +28,20 @@ module.exports.normalizeFunc = (func_thing, object) -> console.error("Could not find method", func_thing, 'in object', @) return => null # always return a func, or Mediator will go boom func_thing = func - return func_thing \ No newline at end of file + return func_thing + +module.exports.hexToHSL = (hex) -> + rgbToHsl(hexToR(hex), hexToG(hex), hexToB(hex)) + +hexToR = (h) -> parseInt (cutHex(h)).substring(0, 2), 16 +hexToG = (h) -> parseInt (cutHex(h)).substring(2, 4), 16 +hexToB = (h) -> parseInt (cutHex(h)).substring(4, 6), 16 +cutHex = (h) -> (if (h.charAt(0) is "#") then h.substring(1, 7) else h) + +module.exports.hslToHex = (hsl) -> + '#' + (toHex(n) for n in hslToRgb(hsl...)).join('') + +toHex = (n) -> + h = Math.floor(n).toString(16) + h = '0'+h if h.length is 1 + h \ No newline at end of file diff --git a/app/styles/editor/thang/colors_tab.sass b/app/styles/editor/thang/colors_tab.sass new file mode 100644 index 000000000..d91d81fd0 --- /dev/null +++ b/app/styles/editor/thang/colors_tab.sass @@ -0,0 +1,55 @@ +$height: 550px + +#editor-thang-colors-tab-view + #color-group-settings + width: 75% + box-sizing: border-box + float: right + + #color-groups-treema + float: left + width: 25% + height: $height + box-sizing: border-box + margin-bottom: 20px + + #shape-buttons + border: 1px solid saddlebrown + margin: 0 20px 0 10px + width: 30% + float: left + height: $height + overflow: scroll + background: sandybrown + padding: 5px + box-sizing: border-box + + button + width: 20px + height: 25px + border: 2px solid white + margin: 1px + + &.selected + border: 2px solid black + + #controls + //clear: both + float: left + width: 60% + text-align: center + background-color: aliceblue + border: 1px solid blue + + .slider-cell + margin: 20px 10px + border-color: black + .ui-slider + border: 1px solid black + .ui-slider-handle + border: 1px solid black + + canvas + width: 60% + background: steelblue + border: 1px solid darkblue \ No newline at end of file diff --git a/app/templates/editor/thang/colors_tab.jade b/app/templates/editor/thang/colors_tab.jade new file mode 100644 index 000000000..3c517093e --- /dev/null +++ b/app/templates/editor/thang/colors_tab.jade @@ -0,0 +1,22 @@ +div#color-groups-treema + +div#color-group-settings.hide + div#shape-buttons + + canvas#tinting-display(width=400, height=400) + + div#controls + div.slider-cell + | Hue: + span.hue-label + .selector#hue-slider + + div.slider-cell + | Saturation: + span.saturation-label + .selector#saturation-slider + + div.slider-cell + | Lightness: + span.lightness-label + .selector#lightness-slider diff --git a/app/templates/editor/thang/edit.jade b/app/templates/editor/thang/edit.jade index ae85337c4..9878c4e92 100644 --- a/app/templates/editor/thang/edit.jade +++ b/app/templates/editor/thang/edit.jade @@ -16,8 +16,12 @@ block content a(href="#editor-thang-components-tab-view", data-toggle="tab") Components li a(href="#editor-thang-canvases-view", data-toggle="tab") Canvases + li + a(href="#editor-thang-colors-tab-view", data-toggle="tab")#color-tab Colors div.tab-content + div.tab-pane#editor-thang-colors-tab-view + div.tab-pane.active#editor-thang-main-tab-view div.main-area.well diff --git a/app/views/editor/thang/colors_tab_view.coffee b/app/views/editor/thang/colors_tab_view.coffee new file mode 100644 index 000000000..1f00181b2 --- /dev/null +++ b/app/views/editor/thang/colors_tab_view.coffee @@ -0,0 +1,164 @@ +CocoView = require 'views/kinds/CocoView' +template = require 'templates/editor/thang/colors_tab' +SpriteBuilder = require 'lib/sprites/SpriteBuilder' +{hexToHSL} = require 'lib/utils' + +module.exports = class ColorsTabView extends CocoView + id: 'editor-thang-colors-tab-view' + template: template + className: 'tab-pane' + + offset: 0 + + constructor: (@thangType, options) -> + @thangType.once 'sync', @tryToBuild, @ + @thangType.schema().once 'sync', @tryToBuild, @ + @colorConfig = { hue: 0, saturation: 0, lightness: 0 } + @spriteBuilder = new SpriteBuilder(@thangType) + f = => + @offset++ + @updateMovieClip() + @interval = setInterval f, 1000 + super options + + afterRender: -> + super() + @createShapeButtons() + @initStage() + @initSliders() + @tryToBuild() + + # sliders + + initSliders: -> + @hueSlider = @initSlider $("#hue-slider", @$el), 0, @updateHue + @saturationSlider = @initSlider $("#saturation-slider", @$el), 50, @updateSaturation + @lightnessSlider = @initSlider $("#lightness-slider", @$el), 50, @updateLightness + + updateHue: => + @colorConfig.hue = @hueSlider.slider('value') / 100 + @updateMovieClip() + + updateSaturation: => + @colorConfig.saturation = (@saturationSlider.slider('value') / 50) - 1 + @updateMovieClip() + + updateLightness: => + @colorConfig.lightness = (@lightnessSlider.slider('value') / 50) - 1 + @updateMovieClip() + + # movie clip + + initStage: -> + canvas = @$el.find('#tinting-display') + @stage = new createjs.Stage(canvas[0]) + createjs.Ticker.setFPS 20 + createjs.Ticker.addEventListener("tick", @stage) + @updateMovieClip() + + updateMovieClip: -> + return unless @currentColorGroupTreema + actionDict = @thangType.getActions() + animations = (a.animation for key, a of actionDict when a.animation) + index = @offset % animations.length + animation = animations[index] + return unless animation + @stage.removeChild(@movieClip) if @movieClip + options = {colorConfig: {}} + options.colorConfig[@currentColorGroupTreema.keyForParent] = @colorConfig + console.log 'options are', options + @spriteBuilder.setOptions options + @spriteBuilder.buildColorMaps() + @movieClip = @spriteBuilder.buildMovieClip animation + larger = Math.min(400 / @movieClip.nominalBounds.width, 400 / @movieClip.nominalBounds.height) + @movieClip.scaleX = larger + @movieClip.scaleY = larger + @movieClip.regX = @movieClip.nominalBounds.x + @movieClip.regY = @movieClip.nominalBounds.y + console.log 'added movie clip', @movieClip + @stage.addChild @movieClip + + createShapeButtons: -> + buttons = $('
').prop('id', 'shape-buttons') + shapes = (shape for key, shape of @thangType.get('raw')?.shapes or {}) + colors = (s.fc for s in shapes when s.fc?) + colors = _.uniq(colors) + colors.sort (a, b) -> + aHSL = hexToHSL(a) + bHSL = hexToHSL(b) + if aHSL[0] > bHSL[0] then -1 else 1 + + for color in colors + button = $('').addClass('btn') + button.css('background', color) + button.val color + buttons.append(button) + buttons.click (e) => + $(e.target).toggleClass('selected') + @updateColorGroup() + @$el.find('#shape-buttons').replaceWith(buttons) + console.log 'making buttons' + @buttons = buttons + + tryToBuild: -> + return unless @thangType.loaded and @thangType.schema().loaded + data = @thangType.get('colorGroups') + data ?= {} + schema = @thangType.schema().attributes.properties?.colorGroups + treemaOptions = + data: data + schema: schema + callbacks: + change: @onColorGroupsChanged + select: @onColorGroupSelected + nodeClasses: + 'thang-color-group': ColorGroupNode + @colorGroups = @$el.find('#color-groups-treema').treema treemaOptions + @colorGroups.build() + @colorGroups.open() + keys = Object.keys @colorGroups.childrenTreemas + console.log 'selected?' + @colorGroups.childrenTreemas[keys[0]]?.$el.click() if keys[0] + + onColorGroupsChanged: => + @thangType.set('colorGroups', @colorGroups.data) + + onColorGroupSelected: (e, selected) => + console.log 'selected!' + @$el.find('#color-group-settings').toggleClass('hide', not selected.length) + treema = @colorGroups.getLastSelectedTreema() + return unless treema + @currentColorGroupTreema = treema + + shapes = {} + shapes[shape] = true for shape in treema.data + + colors = {} + for key, shape of @thangType.get('raw')?.shapes or {} + continue unless shape.fc? + colors[shape.fc] = true if shapes[key] + + console.log 'buttons?', @buttons + @buttons.find('button').removeClass('selected') + @buttons.find('button').each (i, button) -> + $(button).addClass('selected') if colors[$(button).val()] + + @updateMovieClip() + + updateColorGroup: -> + colors = {} + @buttons.find('button').each (i, button) -> + return unless $(button).hasClass('selected') + window.button = button + colors[$(button).val()] = true + + shapes = [] + for key, shape of @thangType.get('raw')?.shapes or {} + continue unless shape.fc? + shapes.push(key) if colors[shape.fc] + + @currentColorGroupTreema.set('/', shapes) + +class ColorGroupNode extends TreemaNode.nodeMap.array + collection: false + canAddChild: -> false diff --git a/app/views/editor/thang/edit.coffee b/app/views/editor/thang/edit.coffee index 06091470d..a218ca4af 100644 --- a/app/views/editor/thang/edit.coffee +++ b/app/views/editor/thang/edit.coffee @@ -8,6 +8,8 @@ Camera = require 'lib/surface/Camera' ThangComponentEditView = require 'views/editor/components/main' DocumentFiles = require 'collections/DocumentFiles' +ColorsTabView = require './colors_tab_view' + CENTER = {x:200, y:300} module.exports = class ThangTypeEditView extends View @@ -66,6 +68,7 @@ module.exports = class ThangTypeEditView extends View @buildTreema() @initSliders() @initComponents() + @insertSubView(new ColorsTabView(@thangType)) initComponents: => options = @@ -243,13 +246,6 @@ module.exports = class ThangTypeEditView extends View # sliders - initSlider: ($el, startValue, changeCallback) -> - slider = $el.slider({ animate: "fast" }) - slider.slider('value', startValue) - slider.on('slide',changeCallback) - slider.on('slidechange',changeCallback) - slider - initSliders: -> @rotationSlider = @initSlider $("#rotation-slider", @$el), 50, @updateRotation @scaleSlider = @initSlider $('#scale-slider', @$el), 29, @updateScale diff --git a/app/views/kinds/CocoView.coffee b/app/views/kinds/CocoView.coffee index debb13f56..8ee5cacd7 100644 --- a/app/views/kinds/CocoView.coffee +++ b/app/views/kinds/CocoView.coffee @@ -217,5 +217,14 @@ module.exports = class CocoView extends Backbone.View ua = navigator.userAgent or navigator.vendor or window.opera return ua.search("MSIE") != -1 + initSlider: ($el, startValue, changeCallback) -> + slider = $el.slider({ animate: "fast" }) + slider.slider('value', startValue) + slider.on('slide',changeCallback) + slider.on('slidechange',changeCallback) + slider + + + mobileRELong = /(android|bb\d+|meego).+mobile|avantgo|bada\/|blackberry|blazer|compal|elaine|fennec|hiptop|iemobile|ip(hone|od)|iris|kindle|lge |maemo|midp|mmp|mobile.+firefox|netfront|opera m(ob|in)i|palm( os)?|phone|p(ixi|re)\/|plucker|pocket|psp|series(4|6)0|symbian|treo|up\.(browser|link)|vodafone|wap|windows (ce|phone)|xda|xiino/i mobileREShort = /1207|6310|6590|3gso|4thp|50[1-6]i|770s|802s|a wa|abac|ac(er|oo|s\-)|ai(ko|rn)|al(av|ca|co)|amoi|an(ex|ny|yw)|aptu|ar(ch|go)|as(te|us)|attw|au(di|\-m|r |s )|avan|be(ck|ll|nq)|bi(lb|rd)|bl(ac|az)|br(e|v)w|bumb|bw\-(n|u)|c55\/|capi|ccwa|cdm\-|cell|chtm|cldc|cmd\-|co(mp|nd)|craw|da(it|ll|ng)|dbte|dc\-s|devi|dica|dmob|do(c|p)o|ds(12|\-d)|el(49|ai)|em(l2|ul)|er(ic|k0)|esl8|ez([4-7]0|os|wa|ze)|fetc|fly(\-|_)|g1 u|g560|gene|gf\-5|g\-mo|go(\.w|od)|gr(ad|un)|haie|hcit|hd\-(m|p|t)|hei\-|hi(pt|ta)|hp( i|ip)|hs\-c|ht(c(\-| |_|a|g|p|s|t)|tp)|hu(aw|tc)|i\-(20|go|ma)|i230|iac( |\-|\/)|ibro|idea|ig01|ikom|im1k|inno|ipaq|iris|ja(t|v)a|jbro|jemu|jigs|kddi|keji|kgt( |\/)|klon|kpt |kwc\-|kyo(c|k)|le(no|xi)|lg( g|\/(k|l|u)|50|54|\-[a-w])|libw|lynx|m1\-w|m3ga|m50\/|ma(te|ui|xo)|mc(01|21|ca)|m\-cr|me(rc|ri)|mi(o8|oa|ts)|mmef|mo(01|02|bi|de|do|t(\-| |o|v)|zz)|mt(50|p1|v )|mwbp|mywa|n10[0-2]|n20[2-3]|n30(0|2)|n50(0|2|5)|n7(0(0|1)|10)|ne((c|m)\-|on|tf|wf|wg|wt)|nok(6|i)|nzph|o2im|op(ti|wv)|oran|owg1|p800|pan(a|d|t)|pdxg|pg(13|\-([1-8]|c))|phil|pire|pl(ay|uc)|pn\-2|po(ck|rt|se)|prox|psio|pt\-g|qa\-a|qc(07|12|21|32|60|\-[2-7]|i\-)|qtek|r380|r600|raks|rim9|ro(ve|zo)|s55\/|sa(ge|ma|mm|ms|ny|va)|sc(01|h\-|oo|p\-)|sdk\/|se(c(\-|0|1)|47|mc|nd|ri)|sgh\-|shar|sie(\-|m)|sk\-0|sl(45|id)|sm(al|ar|b3|it|t5)|so(ft|ny)|sp(01|h\-|v\-|v )|sy(01|mb)|t2(18|50)|t6(00|10|18)|ta(gt|lk)|tcl\-|tdg\-|tel(i|m)|tim\-|t\-mo|to(pl|sh)|ts(70|m\-|m3|m5)|tx\-9|up(\.b|g1|si)|utst|v400|v750|veri|vi(rg|te)|vk(40|5[0-3]|\-v)|vm40|voda|vulc|vx(52|53|60|61|70|80|81|83|85|98)|w3c(\-| )|webc|whit|wi(g |nc|nw)|wmlb|wonu|x700|yas\-|your|zeto|zte\-/i diff --git a/server/handlers/thang_type.coffee b/server/handlers/thang_type.coffee index d80b70e48..d7a9b59dd 100644 --- a/server/handlers/thang_type.coffee +++ b/server/handlers/thang_type.coffee @@ -19,6 +19,7 @@ ThangTypeHandler = class ThangTypeHandler extends Handler 'positions', 'snap', 'components' + 'colorGroups' ] hasAccess: (req) -> diff --git a/server/schemas/thang_type.coffee b/server/schemas/thang_type.coffee index 11a6a1db0..8583955b2 100644 --- a/server/schemas/thang_type.coffee +++ b/server/schemas/thang_type.coffee @@ -122,6 +122,12 @@ _.extend ThangTypeSchema.properties, title: 'Scale' type: 'number' positions: PositionsSchema + colorGroups: c.object + title: 'Color Groups' + additionalProperties: + type:'array' + format: 'thang-color-group' + items: {type:'string'} snap: c.object { title: "Snap", description: "In the level editor, snap positioning to these intervals.", required: ['x', 'y'] }, x: title: "Snap X"