supply_height = 96
CLICK_FUZ = 10 # this far away from things is close enough to be "clicked on"
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"
-]
-
-set_style_class = (args) ->
- args.el.setAttribute 'class', "#{args.class} #{STYLE_TO_CLASS[args.style]}"
+STATES = {
+ NORMAL: { txt: 'normal' }
+ SELECTED: { txt: 'selected' }
+ DRAGGING: { txt: 'dragging' }
+ EDITING: { txt: 'editing' }
+}
+TYPE_WIDGET = 1
+TYPE_CONTROL = 2
# json (compiled to javascript and minified) is ~8% smaller than the resulting xml
json_to_svg = (json) ->
@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: ->
- move: (args) ->
- @x = args.x
- @y = args.y
+ 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: (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_MAX + 1
- set_style: (style) ->
- @style = style
+ return PROX_TOO_FAR
+ set_state: (state) ->
+ @state = state
class Control extends Visible
+ constructor: (args) ->
+ super args
+ @type = TYPE_CONTROL
+ @on_drag = args.drag
+ @on_destruct = args.done ? null
+ destruct: ->
+ super()
+ if @on_destruct?
+ @on_destruct @
+ drag: (args) -> # call this when control point is being manipulated directly
+ @on_drag args
+ proximity: (xy) -> # return the square of the distance to your visible bits
+ dx = xy.x - @x
+ dy = xy.y - @y
+ return dx * dx + dy * dy
+
+class ControlPoint extends Control
+ constructor: (args) ->
+ super args
+ @css_class = 'control_point'
+ @el = json_to_svg circle:
+ cx: @x + 1
+ cy: @y + 1
+ r: 6
+ class: 'control_point normal'
+ @svg.appendChild @el
+ destruct: ->
+ super()
+ if @el?
+ @svg.removeChild @el
+ move: (args) ->
+ super args
+ @el.setAttribute 'cx', @x
+ @el.setAttribute 'cy', @y
class Widget extends Visible
#sub-classes are expected to implement all of these:
+ constructor: (args) ->
+ super args
+ @controls = []
+ @type = TYPE_WIDGET
+ destruct: ->
+ @kill_controls()
clone: ->
return new Widget @
- controls: -> # create controls, return them
+ make_controls: -> # create controls, return them
return []
- hide_controls: ->
+ kill_controls: ->
+ console.log 'kill_controls'
+ for c in @controls
+ c.destruct()
+ @controls = []
+ return
+ move: (xy) -> # just move
+ dx = xy.x - @x
+ dy = xy.y - @y
+ 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) ->
@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
proximity: (xy) -> # return the square of the distance to your visible bits
x = xy.x
y = xy.y
- prox = PROX_MAX + 1
+ prox = PROX_TOO_FAR
in_x = false
in_y = false
if x > @x - CLICK_FUZ and x < @x + @width + CLICK_FUZ
if in_x and in_y and prox > PROX_MAX
prox = PROX_MAX - 1
return prox
- controls: -> # create controls, return them
- return []
- hide_controls: ->
+ resize: (wh) ->
+ @width = wh.w
+ @el.setAttribute 'width', @width
+ @height = wh.h
+ @el.setAttribute 'height', @height
+ if @controls.length > 1
+ @controls[1].move x: @x + @width, y: @y + @height
+ make_controls: (args) -> # create controls, return them
+ console.log 'make_controls'
+ if @controls.length > 0
+ console.log "warning: re-adding controls"
+ @kill_controls()
+ w = @
+ @controls = [
+ 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
# called automatically on domcontentloaded
init = ->
]
svg.appendChild json_to_svg style:
type: 'text/css'
- contents: '.box.normal,.box.hover,.box.selected{filter: url(#crayon)}'
+ contents: '.box.normal{filter: url(#crayon)}'
# create canvas border
svg.appendChild json_to_svg rect:
supply[widget.id] = widget
# editor state
- on_canvas = {}
- selected = {}
- 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
- deselect_all = (args) ->
- except = args?.except ? null
- for id, w of selected
- w.set_style STYLE_NORMAL
- delete selected[id]
- closest_widget = (widgets, xy) ->
- prox = PROX_MAX + 1
+ 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
+ _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 = (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
closest = null
for id, w of widgets
new_prox = w.proximity xy
if new_prox < prox
prox = new_prox
closest = w
- if prox < PROX_MAX
- return closest
- return null
+ if prox > PROX_MAX
+ return null
+ return closest
svg_event_to_xy = (e) ->
unless svg_offset?
svg_offset = $svg.offset()
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 = closest_widget 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 = closest_widget on_canvas, xy
- if closest?
- unless (shift_key_down or selected[closest.id]?)
- deselect_all except: closest
- selected[closest.id] = closest
- closest.set_style STYLE_DRAGGING
+ 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 hit.layer, hit.s
+ else if shift_key_down
+ select_also hit.layer, hit.s
+ else
+ 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
- w.destruct()
- delete selected[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
+ 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
rel_x = xy.x - drag_from.x
rel_y = xy.y - drag_from.y
drag_from = xy
- for id, w of selected
- w.move 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 = closest_widget on_canvas, xy
- unless hover?
- hover = closest_widget 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
+ if hovered and hovered isnt hit?.s
+ hovered.set_hover false
+ return unless hit?
+ 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