JasonWoof Got questions, comments, patches, etc.? Contact Jason Woofenden
editing/selection/hover working
authorJason Woofenden <jason@jasonwoof.com>
Sat, 17 Oct 2015 18:30:38 +0000 (14:30 -0400)
committerJason Woofenden <jason@jasonwoof.com>
Sat, 17 Oct 2015 18:30:38 +0000 (14:30 -0400)
main.coffee
style.styl

index 906228d..3ed08db 100644 (file)
@@ -23,24 +23,15 @@ PROX_MAX = CLICK_FUZ * CLICK_FUZ
 PROX_TOO_FAR = PROX_MAX + 1 # no need to be precice when it's too far
 
 # constants
-STYLE_NORMAL = 0
-STYLE_SELECTED = 1
-STYLE_HOVER = 2
-STYLE_EDITING = 3
-STYLE_DRAGGING = 4
-STYLE_TO_CLASS = [
-       "normal"
-       "selected"
-       "hover"
-       "editing"
-       "dragging"
-]
+STATES = {
+       NORMAL:   { txt: 'normal' }
+       SELECTED: { txt: 'selected' }
+       DRAGGING: { txt: 'dragging' }
+       EDITING:  { txt: 'editing' }
+}
 TYPE_WIDGET = 1
 TYPE_CONTROL = 2
 
-set_style_class = (args) ->
-       args.el.setAttribute 'class', "#{args.class} #{STYLE_TO_CLASS[args.style]}"
-
 # json (compiled to javascript and minified) is ~8% smaller than the resulting xml
 json_to_svg = (json) ->
        for tag, attrs of json
@@ -67,17 +58,27 @@ class Visible
                @y = args.y ? 1
                @width = args.width ? 50
                @height = args.height ? 34
-               @style = args.style ? STYLE_NORMAL
+               @state = args.state ? STATES.NORMAL
+               @hover = false
        destruct: ->
+       update_class: ->
+               css_class = "#{@css_class} #{@state.txt}"
+               if @hover
+                       css_class += " hover"
+               @el.setAttribute 'class', css_class
+       set_hover: (tf) ->
+               if tf != @hover
+                       @hover = tf
+                       @update_class()
        move: (xy) -> # just move
                @x = xy.x
                @y = xy.y
-       drag: (xy) -> # react to mouse drag (obey constraints, etc.)
-               @move xy
+       drag: (dxy) -> # react to mouse drag (obey constraints, etc.)
+               @move x: @x + dxy.x, y: @y + dxy.y
        proximity: (xy) -> # return the square of the distance to your visible bits
                return PROX_TOO_FAR
-       set_style: (style) ->
-               @style = style
+       set_state: (state) ->
+               @state = state
 
 class Control extends Visible
        constructor: (args) ->
@@ -99,6 +100,7 @@ class Control extends Visible
 class ControlPoint extends Control
        constructor: (args) ->
                super args
+               @css_class = 'control_point'
                @el = json_to_svg circle:
                        cx: @x + 1
                        cy: @y + 1
@@ -138,6 +140,11 @@ class Widget extends Visible
                super xy
                for c in @controls
                        c.move x: c.x + dx, y: c.y + dy
+       set_state: (state) ->
+               return if @state is state
+               if @state is STATES.EDITING
+                       @kill_controls()
+               super state
 
 class RectWidget extends Widget
        constructor: (args) ->
@@ -156,9 +163,9 @@ class RectWidget extends Widget
                        @svg.removeChild @el
        clone: ->
                return new RectWidget @
-       set_style: (style) ->
-               super style
-               set_style_class el: @el, class: 'box', style: style
+       set_state: (state) ->
+               super state
+               @update_class()
        move: (args) ->
                super args
                @el.setAttribute 'x', @x + 1
@@ -204,15 +211,11 @@ class RectWidget extends Widget
                        @kill_controls()
                w = @
                @controls = [
-                       new ControlPoint svg: @svg, x: @x, y: @y, done: args.done, drag: (xy) ->
-                               dx = xy.x - @x
-                               dy = xy.y - @y
-                               w.resize w: w.width - dx, h: w.height - dy
-                               w.move x: w.x + dx, y: w.y + dy
-                       new ControlPoint svg: @svg, x: @x + @width, y: @y + @height, done: args.done, drag: (xy) ->
-                               dx = xy.x - @x
-                               dy = xy.y - @y
-                               w.resize w: w.width + dx, h: w.height + dy
+                       new ControlPoint svg: @svg, x: @x, y: @y, done: args.done, drag: (dxy) ->
+                               w.resize w: w.width - dxy.x, h: w.height - dxy.y
+                               w.move x: w.x + dxy.x, y: w.y + dxy.y
+                       new ControlPoint svg: @svg, x: @x + @width, y: @y + @height, done: args.done, drag: (dxy) ->
+                               w.resize w: w.width + dxy.x, h: w.height + dxy.y
                ]
                return @controls
 
@@ -232,7 +235,7 @@ init = ->
                ]
        svg.appendChild json_to_svg style:
                type: 'text/css'
-               contents: '.box.normal,.box.hover,.box.selected{filter: url(#crayon)}'
+               contents: '.box.normal,.box.selected{filter: url(#crayon)}'
 
        # create canvas border
        svg.appendChild json_to_svg rect:
@@ -258,39 +261,44 @@ init = ->
                supply[widget.id] = widget
 
        # editor state
-       on_canvas = {}
-       selected = {}
-       controls = {}
-       editing = {} # has controls
-       dragging = false
+       controls_layer = { all: {}, selected: {} }
+       widget_layer = { all: {}, selected: {}, editing: null }
+       layers = [controls_layer, widget_layer]
+       hovered = null # can be in any layer
+       dragging = false # mouse state
+       drag_layer = null
        drag_from = x: 0, y: 0 # mouse was here at last frame of drag
        shift_key_down = false
 
-       selected_type = ->
-               for s of selected
-                       return s.type
-               return null
-       deselect = (s) ->
-               s.set_style STYLE_NORMAL
-               if s.type is TYPE_WIDGET
-                       s.kill_controls()
-               delete selected[s.id]
+       stop_editing = ->
+               if widget_layer.editing
+                       widget_layer.editing.kill_controls()
+                       widget_layer.editing = null
+       deselect = (layer, s) ->
+               return unless layer.selected[s.id]?
+               s.set_state STATES.NORMAL
+               delete layer.selected[s.id]
+               if widget_layer.editing is s
+                       widget_layer.editing = null
+               return
+       deselect_all = (layer, except = null) ->
+               for id, s of layer.selected
+                       deselect layer, s
                return
-       deselect_all = (args) ->
-               except = args?.except ? null
-               for id, s of selected
-                       deselect s
+       _select = (layer, s) -> # don't call this directly, use select_only() or select_also()
+               s.set_state STATES.SELECTED
+               layer.selected[s.id] = s
                return
-       select_only = (sel) ->
-               deselect_all except: sel
-               return if selected[sel.id]?
-               selected[sel.id] = sel
-       select_also = (sel) ->
-               return if selected[sel.id]?
-               sel_type = selected_type()
-               if sel_type isnt sel.type
-                       deselect_all()
-               selected[sel.id] = sel
+       select_only = (layer, s) ->
+               deselect_all layer, s
+               return if layer.selected[s.id]?
+               _select layer, s
+               return
+       select_also = (layer, s) ->
+               return if layer.selected[s.id]?
+               if layer is widget_layer
+                       stop_editing()
+               _select layer, s
                return
        find_closest = (widgets, xy) ->
                prox = PROX_TOO_FAR
@@ -310,60 +318,74 @@ init = ->
                        x: Math.round(e.pageX - svg_offset.left)
                        y: Math.round(e.pageY - svg_offset.top)
                }
+       closest_in_layers = (xy) ->
+               for layer in layers
+                       s = find_closest layer.selected, xy
+                       return layer: layer, s: s if s?
+                       s = find_closest layer.all, xy
+                       return layer: layer, s: s if s?
+               return null
        mousedown = (e) ->
+               hit = null
+               closest = null
+               layer = null
                mousemove e
                if dragging # two mousedowns in a row?! it happens
                        return mouseup e
                xy = svg_event_to_xy e
                if xy.y < supply_height
-                       closest = find_closest supply, xy
-                       if closest?
-                               closest = closest.clone()
-                               on_canvas[closest.id] = closest
+                       s = find_closest supply, xy
+                       if s?
+                               hit = {
+                                       s: s.clone()
+                                       layer: widget_layer
+                               }
+                               widget_layer.all[hit.s.id] = hit.s
                else
-                       closest = find_closest controls, xy
-                       unless closest?
-                               closest = find_closest on_canvas, xy
-               if closest?
-                       console.log closest
-                       if selected[closest.id]
+                       hit = closest_in_layers xy
+               if hit?
+                       if hit.layer.selected[hit.s.id]
                                # already selected
                                # TODO start detection of a click that doesn't drag (to shrink selection)
                        else if xy.y < supply_height
                                # dragging a new thing in
-                               select_only closest
+                               select_only hit.layer, hit.s
                        else if shift_key_down
-                               select_also closest
+                               select_also hit.layer, hit.s
                        else
-                               select_only closest
-                       for id, s of selected
-                               s.set_style STYLE_DRAGGING
+                               select_only hit.layer, hit.s
+                       for id, s of hit.layer.selected
+                               s.set_state STATES.DRAGGING
                        dragging = true
+                       drag_layer = hit.layer
                        drag_from = xy
+                       console.log hit
                else
-                       deselect_all()
-               return false
+                       deselect_all widget_layer
+               return
        mouseup = (e) ->
                mousemove e
                if dragging
-                       for id, w of selected
-                               if w.y < supply_height
-                                       deselect w
-                                       w.destruct()
-                                       delete on_canvas[id]
+                       selected_count = 0
+                       for id, s of drag_layer.selected
+                               if s.y < supply_height and drag_layer is widget_layer
+                                       deselect drag_layer, s
+                                       s.destruct()
+                                       delete drag_layer.all[id]
                                else
-                                       w.set_style STYLE_SELECTED
-                                       if w.type is TYPE_WIDGET
-                                               cs = w.make_controls done: (c) ->
-                                                       if controls[c.id]?
-                                                               delete controls[c.id]
-                                               for c in cs
-                                                       controls[c.id] = c
-                                               editing[w.id] = w
-                                               delete selected[w.id]
+                                       selected_count += 1
+                                       s.set_state STATES.SELECTED
+                       if drag_layer is widget_layer and selected_count is 1
+                               for id, s of drag_layer.selected
+                                       s.set_state STATES.EDITING
+                                       cs = s.make_controls done: (c) ->
+                                               deselect controls_layer, c
+                                               delete controls_layer.all[c.id]
+                                       for c in cs
+                                               controls_layer.all[c.id] = c
+                                       widget_layer.editing = s
                dragging = false
-               return false
-       prev_hover = null
+               return
        mousemove = (e) ->
                xy = svg_event_to_xy e
                if dragging
@@ -371,26 +393,26 @@ init = ->
                        rel_x = xy.x - drag_from.x
                        rel_y = xy.y - drag_from.y
                        drag_from = xy
-                       for id, w of selected
-                               w.drag x: w.x + rel_x, y: w.y + rel_y
+                       for id, w of drag_layer.selected
+                               w.drag x: rel_x, y: rel_y
                else
-                       hover = find_closest on_canvas, xy
-                       unless hover?
-                               hover = find_closest supply, xy
-                       if hover != prev_hover
-                               if prev_hover?
-                                       # FIXME
-                                       if selected[prev_hover.id]?
-                                               prev_hover.set_style STYLE_SELECTED
-                                       else
-                                               prev_hover.set_style STYLE_NORMAL
-                               if hover?
-                                       hover.set_style STYLE_HOVER
-                               prev_hover = hover
+                       hit = closest_in_layers xy
+                       return unless hit?
+                       return if hit.s is hovered
+                       if hovered
+                               hovered.set_hover false
+                       hovered = hit.s
+                       hovered.set_hover true
+               return
+       $svg.mousedown (e) ->
+               mousedown e
+               return false
+       $svg.mouseup (e) ->
+               mouseup e
+               return false
+       $svg.mousemove (e) ->
+               mousemove e
                return false
-       $svg.mousedown mousedown
-       $svg.mouseup mouseup
-       $svg.mousemove mousemove
        $(document).on 'keyup keydown', (e) ->
                shift_key_down = e.shiftKey
                return true
index ae2bdc4..5cd95d3 100644 (file)
@@ -15,6 +15,8 @@ color_selected = #544c4c
 color_hover = #777
 color_dragging = #9e9373
 color_controls = #000
+color_controls_hover = #222
+color_controls_selected = #444
 .crayon_mockup
        svg
                path.polyline
@@ -51,3 +53,10 @@ color_controls = #000
                        stroke: color_dragging
                rect.box.selected, path.polyline.selected
                        stroke: color_selected
+               circle.control_point
+                       stroke: none
+                       fill: color_controls
+               circle.control_point.hover
+                       fill: color_controls_hover
+               circle.control_point.selected
+                       fill: color_controls_selected