# 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
-$tool_options = null # jquery object for tool options line
-selection = []
-svg_ns = 'http://www.w3.org/2000/svg'
-cur_tool = null
-cur_tool_name = null
-mouse = x: 0, y: 0, buttons: [0,0,0]
+# constants
+STYLE_NORMAL = 0
+STYLE_SELECTED = 1
+STYLE_HOVER = 2
+STYLE_EDITING = 3
+STYLE_DRAGGING = 4
-class EditableThing
- constructor: ->
- @el = null
- click_proximity: (x, y) ->
- return null
- clicked: ->
- update: ->
- destructor: ->
- if @el?
- svg.removeChild @el
- @el = null
+STYLE_TO_CLASS = [
+ "normal"
+ "selected"
+ "hover"
+ "editing"
+ "dragging"
+]
-class PolyLine extends EditableThing
- constructor: (args) ->
- super args
- @drawing = args?.drawing ? false
- @el = document.createElementNS svg_ns, "path"
- svg.appendChild @el
- @points = args?.points ? []
- @update()
- update: (args) ->
- if args?.drawing?
- @drawing = args.drawing
- d = ''
- l = ''
- sep = 'M'
- for loc, i in @points
- d += l = "#{sep} #{loc[0]} #{loc[1]}"
- sep = ' L'
- if args?.to_mouse?
- d += l = "#{sep} #{args.mouse_x} #{args.mouse_y}"
- if args?.close # FIXME ?remove
- d += l = ' z'
- if @points.length > 0
- if l is " L #{@points[0][0]} #{@points[0][1]}"
- d = d.substr 0, d.length - l.length
- d += ' z'
- @el.setAttribute "d", d
- add_point: (x, y) ->
- @points.push [x, y]
- close_loop: ->
- if @points.length > 2
- @add_point @points[0][0], @points[0][1]
+set_style_class = (args) ->
+ args.el.setAttribute 'class', "#{args.class} #{STYLE_TO_CLASS[args.style]}"
-class Tool
- constructor: (args) ->
- @button = args.button
- if @button?
- @button.addClass 'disabled'
- @tool_options = args.tool_options
- click: (x, y) ->
- mousemove: (x, y) ->
- keydown: (keycode) ->
- disable: ->
- if @button?
- @button.removeClass 'disabled'
- @tool_options.empty()
+# 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 if k is 'contents'
+ el.appendChild document.createTextNode v
+ else
+ el.setAttribute k, v
+ return el
-class TutorialTool extends Tool
+next_widget_id = 0
+# public vars: x, y, width, height, el
+class Visible
+ # required args: svg
constructor: (args) ->
- super args
- @paths = []
- choose = [
- [[219,34],[53,141],[96,143],[92,255],[365,257],[362,145],[407,144,'z']],
- [[161,118],[144,106],[130,115],[128,152],[140,160],[156,150]],
- [[173,107],[169,159],[180,137],[189,133],[193,160]],
- [[218,135],[205,144],[205,158],[215,164],[225,156],[225,144,'z']],
- [[242,135],[233,145],[233,157],[244,168],[256,158],[254,144,'z']],
- [[278,141],[269,135],[261,142],[264,151],[278,153],[281,162],[271,170],[264,163]],
- [[291,151],[305,151],[312,143],[299,135],[288,140],[293,160],[302,167],[313,158]],
- [[136,208],[121,206],[116,226],[128,233],[136,229],[137,209],[140,233]],
- [[160,207],[191,205]],
- [[176,184],[174,228],[180,238]],
- [[198,216],[187,223],[189,236],[200,242],[210,235],[209,224,'z']],
- [[227,216],[216,222],[216,236],[224,244],[237,240],[237,226,'z']],
- [[247,187],[249,241],[254,243]]
- ]
- for c in choose
- path = data: c, element: document.createElementNS svg_ns, "path"
- update_path path, close: c[c.length - 1][2] is 'z'
- svg.appendChild path.element
- @paths.push path
- @tip = $ "<span> </span>"
- @tool_options.append @tip
- disable: ->
- super()
- for p in @paths
- svg.removeChild p.element
+ @id = next_widget_id
+ next_widget_id += 1
+ @svg = args.svg
+ @x = args.x ? 1
+ @y = args.y ? 1
+ @width = args.width ? 50
+ @height = args.height ? 34
+ @style = args.style ? STYLE_NORMAL
+ destruct: ->
+ 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) ->
+ @style = style
-class DrawTool extends Tool
- constructor: (args) ->
- super args
- @tool_options.append $ "<span>Draw tool helpers:</span>"
- @stop_button = $ '<span class="button" title="keyboard shortcut: space">finish line</span>'
- @stop_close_button = $ '<span class="button" title="keyboard shortcut: O">close (loop)</span>'
- @cancel_button = $ '<span class="button" title="keyboard shortcut: Esc">cancel line</span>'
- @tool_options.append @stop_button
- @tool_options.append @stop_close_button
- @tool_options.append @cancel_button
- @stop_button.click @stop_drawing.bind @
- @stop_close_button.click @stop_close_drawing.bind @
- @cancel_button.click @cancel_drawing.bind @
- @update_helper_buttons()
- click: (x, y) ->
- if selection.length and not selection[0].drawing?
- selection = []
- unless selection.length
- selection.push new PolyLine drawing: true
- selection[0].add_point x, y
- selection[0].update()
- @update_helper_buttons()
- update_helper_buttons: ->
- if selection[0]?.drawing? and selection[0]?.points.length > 0
- @cancel_button.removeClass 'disabled'
- else
- @cancel_button.addClass 'disabled'
- if selection[0]?.drawing? and selection[0]?.points.length > 1
- @stop_button.removeClass 'disabled'
- else
- @stop_button.addClass 'disabled'
- if selection[0]?.drawing? and selection[0]?.points.length > 2
- @stop_close_button.removeClass 'disabled'
- else
- @stop_close_button.addClass 'disabled'
- cancel_drawing: ->
- if selection[0]?.drawing?
- selection[0].destructor()
- selection = []
- @update_helper_buttons()
- return false
- stop_drawing: ->
- if selection[0]?.drawing?
- if selection[0]?.points.length < 2
- return @cancel_drawing()
- if selection[0]?
- selection[0].update()
- selection = []
- @update_helper_buttons()
- return false
- stop_close_drawing: ->
- if selection[0]?.drawing?
- if selection[0]?.points.length < 3
- return @stop_drawing()
- selection[0].close_loop()
- selection[0].update drawing: false
- selection = []
- @update_helper_buttons()
- return false
- mousemove: (x, y) ->
- mouse.x = x
- mouse.y = y
- if selection[0]?.drawing?
- selection[0].update to_mouse: true, mouse_x: x, mouse_y: y
- keydown: (keycode) ->
- switch keycode
- when ('O'.charCodeAt 0), ('0'.charCodeAt 0)
- return @stop_close_drawing()
- when (' '.charCodeAt 0), 13, 10
- return @stop_drawing()
- when 27
- return @cancel_drawing()
- disable: ->
- super()
- @stop_drawing()
+class Control extends Visible
-class EditTool extends Tool
- constructor: (args) ->
- super args
- args.tool_options.append $ "<span>Oops, the edit tool isn't implemented yet</span>"
+class Widget extends Visible
+ #sub-classes are expected to implement all of these:
+ clone: ->
+ return new Widget @
+ controls: -> # create controls, return them
+ return []
+ hide_controls: ->
-class DeleteTool extends Tool
+class RectWidget extends Widget
constructor: (args) ->
super args
- args.tool_options.append $ "<span>To delete: click on a line below</span>"
- click: (x, y) ->
- $svg.find('path:hover').remove()
- return false
-
-update_path = (path, flags) ->
- d = ''
- sep = 'M'
- for loc, i in path.data
- d += "#{sep} #{loc[0]} #{loc[1]}"
- sep = ' L'
- if flags?.to_mouse?
- d += "#{sep} #{mouse.x} #{mouse.y}"
- if flags?.close
- d += " z"
- path.element.setAttribute "d", d
-
-switch_to_tool = (tool_class) ->
+ @css_class = 'box'
+ @el = json_to_svg rect:
+ x: @x + 1
+ y: @y + 1
+ width: @width - 2
+ height: @height - 2
+ class: 'box normal'
+ @svg.appendChild @el
+ destruct: ->
+ super()
+ if @el?
+ @svg.removeChild @el
+ clone: ->
+ return new RectWidget @
+ set_style: (style) ->
+ super style
+ set_style_class el: @el, class: 'box', style: style
+ 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'
- $toolbar = $ '<div class="toolbar"><span>Tools:</span></div>'
- $tool_options = $ '<div class="tool_options"></div>'
- $container.append $toolbar
- $container.append $tool_options
- 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.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' }
+ ]
+ svg.appendChild json_to_svg style:
+ type: 'text/css'
+ contents: '.box.normal,.box.hover,.box.selected{filter: url(#crayon)}'
+
+ # 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'
- tool_buttons =
- tutorial: default: true, factory: TutorialTool
- draw: button_text: 'draw', factory: DrawTool
- edit: button_text: 'edit', factory: EditTool
- delete: button_text: 'delete', factory: DeleteTool
- for k, t of tool_buttons
- if t.button_text?
- t.button = $ "<span class=\"button\"></span>"
- t.button.text t.button_text
- $toolbar.append t.button
- do (k, t) ->
- activate = ->
- if cur_tool?
- cur_tool.disable()
- $container.removeClass "#{cur_tool_name}_tool"
- ($toolbar.find '.button').removeClass 'disabled'
- cur_tool_name = k
- $container.addClass "#{cur_tool_name}_tool"
- $tool_options.empty()
- cur_tool = new t.factory button: t.button, tool_options: $tool_options
- if t.button?
- t.button.click activate
- if t.default
- activate()
+ supply = {}
+ for args, i in [
+ { width: 40, height: 40 }
+ { width: 12, height: 50 }
+ { width: 70, height: 12 }
+ ]
+ widget = new RectWidget {
+ width: args.width
+ height: args.height
+ x: 30 + i * 90 + (70 - args.width) / 2
+ y: (supply_height - args.height) / 2
+ svg: svg
+ }
+ supply[widget.id] = widget
- $svg.mousedown (e) ->
- offset = $svg.offset()
- if cur_tool?
- mouse_x = Math.round(e.pageX - offset.left)
- mouse_y = Math.round(e.pageY - offset.top)
- return cur_tool.click mouse_x, mouse_y
- $svg.mousemove (e) ->
- offset = $svg.offset()
- if cur_tool?
- mouse_x = Math.round(e.pageX - offset.left)
- mouse_y = Math.round(e.pageY - offset.top)
- return cur_tool.mousemove mouse_x, mouse_y
- ($ document).keydown (e) ->
- if cur_tool?
- return cur_tool.keydown e.keyCode
+ # editor state
+ on_canvas = {}
+ selected = {}
+ editing = {} # has controls
+ dragging = false
+ 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
+ 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
+ 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[closest.id] = closest
+ 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
+ dragging = true
+ drag_from = xy
+ else
+ deselect_all()
+ return false
+ mouseup = (e) ->
+ mousemove e
+ if dragging
+ for id, w of selected
+ if w.y < supply_height
+ w.destruct()
+ delete selected[id]
+ else
+ w.set_style STYLE_SELECTED
+ dragging = false
+ return false
+ prev_hover = null
+ mousemove = (e) ->
+ xy = svg_event_to_xy e
+ if dragging
+ return if drag_from.x is xy.x and drag_from.y is xy.y
+ 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
+ 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
+ return false
+ $svg.mousedown mousedown
+ $svg.mouseup mouseup
+ $svg.mousemove mousemove
+ $(document).on 'keyup keydown', (e) ->
+ shift_key_down = e.shiftKey
+ return true
+ #($ document).keydown (e) ->
$ init