# settings
-width = 500
-height = 500
+width = 800
+height = 600
+supply_height = 96
+CLICK_FUZ = 10 # this far away from things is close enough to be "clicked on"
+PROX_MAX = CLICK_FUZ * CLICK_FUZ
-# globals
-$svg = null # jquery object for svg element
-svg = null # dom object for svg element
-selection = null
-svg_ns = 'http://www.w3.org/2000/svg'
-mouse = [0,0]
+# constants
+STYLE_NORMAL = 0
+STYLE_SELECTED = 1
+STYLE_HOVER = 2
+STYLE_EDITING = 3
+STYLE_DRAGGING = 4
-update_path = (path, data, flags) ->
- d = ''
- for loc, i in data
- if i is 0
- d += 'M '
+# json (compiled to javascript and minified) is ~8% smaller than the resulting xml
+json_to_svg = (json) ->
+ for tag, attrs of json
+ el = document.createElementNS 'http://www.w3.org/2000/svg', tag
+ for k, v of attrs
+ if k is 'children'
+ for child in v
+ el.appendChild json_to_svg child
+ else
+ el.setAttribute k, v
+ return el
+
+next_widget_id = 0
+# public vars: x, y, width, height, el
+class Widget
+ # required args: svg
+ constructor: (args) ->
+ @id = next_widget_id
+ next_widget_id += 1
+ @svg = args.svg
+ @style = args.style ? STYLE_NORMAL
+ @x = args.x ? 1
+ @y = args.y ? 1
+ @width = args.width ? 50
+ @height = args.height ? 34
+ destruct: ->
+ clone: ->
+ return new Widget @
+ move: (args) ->
+ @x = args.x
+ @y = args.y
+ proximity: (xy) -> # return the square of the distance to your visible bits
+ return PROX_MAX + 1
+ set_style: (style) ->
+ return if @style is style
+ if style is STYLE_NORMAL
+ @el.setAttribute 'style', 'filter: url(#crayon)'
else
- d += ' L '
- d += "#{loc[0]} #{loc[1]}"
- if flags?.to_mouse?
- d += "L #{mouse[0]} #{mouse[1]}"
- if flags?.close?
- d += " z"
- path.setAttribute "d", d
+ if @style is STYLE_NORMAL
+ @el.setAttribute 'style', ''
+ switch style
+ when STYLE_NORMAL
+ @el.setAttribute 'class', "#{@css_class}"
+ when STYLE_SELECTED
+ @el.setAttribute 'class', "#{@css_class} selected"
+ when STYLE_HOVER
+ @el.setAttribute 'class', "#{@css_class} hover"
+ when STYLE_DRAGGING
+ @el.setAttribute 'class', "#{@css_class} dragging"
+ # FIXME when STYLE_EDITING
+ @style = style
+ controls: -> # create controls, return them
+ return []
+ hide_controls: ->
-stop_drawing = ->
- if selection?
- update_path selection.element, selection.data
- selection = null
- return false
-stop_close_drawing = ->
- if selection?
- update_path selection.element, selection.data, close: true
- selection = null
- return false
-click = (x, y) ->
- unless selection?
- path = document.createElementNS svg_ns, "path"
- selection = data: [], element: path
- svg.appendChild path
- selection.data.push [x, y]
- update_path selection.element, selection.data
-mousemove = (x, y) ->
- mouse[0] = x
- mouse[1] = y
- if selection?
- update_path selection.element, selection.data, to_mouse: true
+class RectWidget extends Widget
+ constructor: (args) ->
+ super args
+ @css_class = 'box'
+ @el = json_to_svg rect:
+ x: @x + 1
+ y: @y + 1
+ width: @width - 2
+ height: @height - 2
+ class: 'box'
+ style: if @style is STYLE_NORMAL then 'filter: url(#crayon)' else ''
+ @svg.appendChild @el
+ destruct: ->
+ if @el?
+ @svg.removeChild @el
+ clone: ->
+ return new RectWidget @
+ move: (args) ->
+ super args
+ @el.setAttribute 'x', @x + 1
+ @el.setAttribute 'y', @y + 1
+ proximity: (xy) -> # return the square of the distance to your visible bits
+ x = xy.x
+ y = xy.y
+ prox = PROX_MAX + 1
+ in_x = false
+ in_y = false
+ if x > @x - CLICK_FUZ and x < @x + @width + CLICK_FUZ
+ in_x = true
+ if y < @y + @height / 2
+ new_prox = @y - y
+ else
+ new_prox = @y + @height - y
+ new_prox *= new_prox
+ if new_prox < prox
+ prox = new_prox
+ if y > @y - CLICK_FUZ and y < @y + @height + CLICK_FUZ
+ in_y = true
+ if x < @x + @width / 2
+ new_prox = @x - x
+ else
+ new_prox = @x + @width - x
+ new_prox *= new_prox
+ if new_prox < prox
+ prox = new_prox
+ if in_x and in_y and prox > PROX_MAX
+ prox = PROX_MAX - 1
+ return prox
+ controls: -> # create controls, return them
+ return []
+ hide_controls: ->
# called automatically on domcontentloaded
init = ->
+ svg_offset = null
$container = $ '.crayon_mockup'
- $stop_button = $ '<span class="button">stop drawing</span>'
- $stop_close_button = $ '<span class="button">stop drawing, close loop</span>'
- $tools = $ '<div class="toolbar"></div>'
- $tools.append $stop_button
- $tools.append $stop_close_button
- $stop_button.click stop_drawing
- $stop_close_button.click stop_close_drawing
- $container.append $tools
- svg = document.createElementNS svg_ns, 'svg'
- svg.setAttribute 'width', width
- svg.setAttribute 'height', height
- svg.setAttribute 'viewBox', "0 0 #{width} #{height}"
+ svg = json_to_svg svg: width: width, height: height, viewBox: "0 0 #{width} #{height}"
$svg = $ svg
$container.append $svg
- $svg.mousedown (e) ->
- offset = $svg.offset()
- click e.pageX - offset.left, e.pageY - offset.top
+ svg.appendChild json_to_svg filter:
+ id: 'crayon', filterUnits: 'userSpaceOnUse'
+ x: '-5%', y: '-5%', height: '110%', width: '110%'
+ children: [
+ { feTurbulence: baseFrequency: '.3', numOctaves: '2', type: 'fractalNoise' }
+ { feDisplacementMap: scale: '6', xChannelSelector: 'R', in: 'SourceGraphic' }
+ ]
+
+ # create canvas border
+ svg.appendChild json_to_svg rect:
+ x: 1
+ y: supply_height + 1
+ width: width - 2
+ height: height - 2 - supply_height
+ class: 'canvas_border'
+
+ supply = [
+ new RectWidget svg: svg
+ new RectWidget svg: svg, width: 12, height: 50
+ new RectWidget svg: svg, width: 70, height: 12
+ ]
+ for widget, i in supply
+ widget.move {
+ x: 30 + i * 90 + (70 - widget.width) / 2
+ y: (supply_height - widget.height) / 2
+ }
+
+ # editor state
+ on_canvas = []
+ selected = []
+ editing = [] # has controls
+ dragging = false
+ dragging_offset = x: 0, y: 0 # from mouse x,y -> widget x,y
+
+ deselect_all = (args) ->
+ except = args?.except ? null
+ found = false
+ for w in selected
+ if w is except
+ found = true
+ else
+ w.set_style STYLE_NORMAL
+ if found
+ selected = [except]
+ else
+ selected = []
+ closest_widget = (widgets, xy) ->
+ prox = PROX_MAX + 1
+ closest = null
+ for w in widgets
+ new_prox = w.proximity xy
+ if new_prox < prox
+ prox = new_prox
+ closest = w
+ if prox < PROX_MAX
+ return closest
+ return null
+ svg_event_to_xy = (e) ->
+ unless svg_offset?
+ svg_offset = $svg.offset()
+ return {
+ x: Math.round(e.pageX - svg_offset.left)
+ y: Math.round(e.pageY - svg_offset.top)
+ }
+ mousedown = (e) ->
+ 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.push closest
+ else
+ closest = closest_widget on_canvas, xy
+ if closest?
+ deselect_all except: closest
+ selected = [closest]
+ closest.set_style STYLE_DRAGGING
+ dragging = true
+ dragging_offset.x = closest.x - xy.x
+ dragging_offset.y = closest.y - xy.y
+ else
+ deselect_all()
+ return false
+ mouseup = (e) ->
+ mousemove e
+ if dragging
+ for w in selected
+ if w.y < supply_height
+ w.destruct()
+ else
+ w.set_style STYLE_SELECTED
+ dragging = false
+ return false
+ prev_hover = null
+ mousemove = (e) ->
+ xy = svg_event_to_xy e
+ if dragging
+ xy.x += dragging_offset.x
+ xy.y += dragging_offset.y
+ selected[0].move xy
+ else
+ hover = closest_widget on_canvas, xy
+ unless hover?
+ hover = closest_widget supply, xy
+ if hover != prev_hover
+ prev_hover = hover
+ for w in selected
+ if w.style is STYLE_HOVER and w isnt hover
+ w.set_style STYLE_SELECTED
+ for w in supply
+ if w.style is STYLE_HOVER and w isnt hover
+ w.set_style STYLE_NORMAL
+ for w in on_canvas
+ if w.style is STYLE_HOVER and w isnt hover
+ w.set_style STYLE_NORMAL
+ if hover
+ hover.set_style STYLE_HOVER
return false
- $svg.mousemove (e) ->
- offset = $svg.offset()
- mousemove e.pageX - offset.left, e.pageY - offset.top
+ $svg.mousedown mousedown
+ $svg.mouseup mouseup
+ $svg.mousemove mousemove
+ #($ document).keydown (e) ->
$ init