JasonWoof Got questions, comments, patches, etc.? Contact Jason Woofenden
major code cleanup (layers/controls)
[crayon_mockup.git] / main.coffee
1 # Copyright 2015 Jason Woofenden
2 #
3 # This program is free software: you can redistribute it and/or modify it under
4 # the terms of the GNU General Public License as published by the Free Software
5 # Foundation, either version 3 of the License, or (at your option) any later
6 # version.
7 #
8 # This program is distributed in the hope that it will be useful, but WITHOUT
9 # ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
10 # FOR A PARTICULAR PURPOSE.  See the GNU General Public License for more
11 # details.
12 #
13 # You should have received a copy of the GNU General Public License along with
14 # this program.  If not, see <http://www.gnu.org/licenses/>.
15
16
17 # settings
18 width = 800
19 height = 600
20 supply_height = 96
21 CLICK_FUZ = 10 # this far away from things is close enough to be "clicked on"
22 PROX_MAX = CLICK_FUZ * CLICK_FUZ
23 PROX_TOO_FAR = PROX_MAX + 1 # no need to be precice when it's too far
24
25 # constants
26 STATES = {
27         NORMAL:   { txt: 'normal' }
28         SELECTED: { txt: 'selected' }
29         DRAGGING: { txt: 'dragging' }
30         EDITING:  { txt: 'editing' }
31 }
32 TYPE_UNKNOWN = 0
33 TYPE_WIDGET = 1
34 TYPE_CONTROL = 2
35
36 # json (compiled to javascript and minified) is ~8% smaller than the resulting xml
37 json_to_svg = (json) ->
38         for tag, attrs of json
39                 el = document.createElementNS 'http://www.w3.org/2000/svg', tag
40                 for k, v of attrs
41                         if k is 'children'
42                                 for child in v
43                                         el.appendChild json_to_svg child
44                         else if k is 'contents'
45                                 el.appendChild document.createTextNode v
46                         else
47                                 el.setAttribute k, v
48         return el
49
50 resizer_nw = (widget) ->
51         return (dxy) ->
52                 widget.resize w: widget.width - dxy.x, h: widget.height - dxy.y
53                 widget.move x: widget.x + dxy.x, y: widget.y + dxy.y
54 resizer_n = (widget) ->
55         return (dxy) ->
56                 widget.resize w: widget.width, h: widget.height - dxy.y
57                 widget.move x: widget.x, y: widget.y + dxy.y
58 resizer_ne = (widget) ->
59         return (dxy) ->
60                 widget.resize w: widget.width + dxy.x, h: widget.height - dxy.y
61                 widget.move x: widget.x, y: widget.y + dxy.y
62 resizer_e = (widget) ->
63         return (dxy) ->
64                 widget.resize w: widget.width + dxy.x, h: widget.height
65 resizer_se = (widget) ->
66         return (dxy) ->
67                 widget.resize w: widget.width + dxy.x, h: widget.height + dxy.y
68 resizer_s = (widget) ->
69         return (dxy) ->
70                 widget.resize w: widget.width, h: widget.height + dxy.y
71 resizer_sw = (widget) ->
72         return (dxy) ->
73                 widget.resize w: widget.width - dxy.x, h: widget.height + dxy.y
74                 widget.move x: widget.x + dxy.x, y: widget.y
75 resizer_w = (widget) ->
76         return (dxy) ->
77                 widget.resize w: widget.width - dxy.x, h: widget.height
78                 widget.move x: widget.x + dxy.x, y: widget.y
79 resizers = [
80         resizer_nw
81         resizer_n
82         resizer_ne
83         resizer_e
84         resizer_se
85         resizer_s
86         resizer_sw
87         resizer_w
88 ]
89 resizer_shapes = [
90         -> return "M#{@x - 5} #{@y - 5}h6l-2 2 4 4 2 -2v6h-6l2-2-4-4-2 2z"
91         -> return "M #{@x},#{@y - 7} l 4,4 -2.5,0 0,5 2.5,0 -4,4 -4,-4 2.5,0 0,-5 -2.5,0 z"
92         -> return "M#{@x + 5} #{@y - 5}v6l-2-2-4 4 2 2h-6v-6l2 2 4-4-2-2z"
93         -> return "M #{@x - 7},#{@y} l 4,-4 0,2.5 5,0 0,-2.5 4,4 -4,4 0,-2.5 -5,0 0,2.5 z"
94 ]
95 shape_node_move = -> "M#{@x} #{@y - 9}l-2.5 4.5h2v2.404a2.156 2.156 0 0 0-1.596 1.596h-2.404v-2l-4.5 2.5 4.5 2.5v-2h2.404a2.156 2.156 0 0 0 1.596 1.596v2.404h-2l2.5 4.5 2.5-4.5h-2v-2.404a2.156 2.156 0 0 0 1.596-1.596h2.404v2l4.5-2.5-4.5-2.5v2h-2.404a2.156 2.156 0 0 0-1.596-1.596v-2.404h2l-2.5-4.5z"
96
97 next_widget_id = 0
98 # public vars: x, y, width, height, el
99 class Visible
100         # required args: svg
101         constructor: (args) ->
102                 @type = TYPE_UNKNOWN
103                 @id = next_widget_id
104                 next_widget_id += 1
105                 @svg = args.svg
106                 @x = args.x ? 1
107                 @y = args.y ? 1
108                 @width = args.width ? 50
109                 @height = args.height ? 34
110                 @state = args.state ? STATES.NORMAL
111                 @hover = false
112                 @shown = false
113                 @el = null
114                 return
115         destruct: ->
116                 @hide()
117                 return
118         update_class: ->
119                 css_class = "#{@css_class} #{@state.txt}"
120                 if @hover
121                         css_class += " hover"
122                 @el.setAttribute 'class', css_class
123                 return
124         set_hover: (bool) ->
125                 if bool != @hover
126                         @hover = bool
127                         @update_class()
128                 return
129         hide: ->
130                 if @el and @shown
131                         @svg.removeChild @el
132                         @shown = false
133                 return
134         show: ->
135                 if @el? and not @shown
136                         @svg.appendChild @el
137                         @shown = true
138                 return
139         move: (xy) -> # just move
140                 @x = xy.x
141                 @y = xy.y
142                 return
143         drag: (dxy) -> # react to mouse drag (obey constraints, etc.)
144                 @move x: @x + dxy.x, y: @y + dxy.y
145                 return
146         proximity: (xy) -> # return the square of the distance to your visible bits
147                 return PROX_TOO_FAR
148         set_state: (state) ->
149                 @state = state
150                 @update_class()
151                 return
152
153 class Control extends Visible
154         constructor: (args) ->
155                 super args
156                 @type = TYPE_CONTROL
157                 @on_drag = args.drag
158                 return
159         drag: (args) -> # call this when control point is being manipulated directly
160                 @on_drag args
161                 return
162         proximity: (xy) -> # return the square of the distance to your visible bits
163                 dx = xy.x - @x
164                 dy = xy.y - @y
165                 return dx * dx + dy * dy
166 class ControlPath extends Control
167         constructor: (args) ->
168                 super args
169                 @css_class = 'control_point'
170                 @make_path = args.shape
171                 @el = json_to_svg path:
172                         d: @make_path()
173                         class: 'control_point normal'
174                 return
175         move: (args) ->
176                 super args
177                 @el.setAttribute 'd', @make_path()
178                 return
179
180 # A widget is a visable thing that can be edited via controls
181 class Widget extends Visible
182         constructor: (args) ->
183                 super args
184                 @type = TYPE_WIDGET
185                 @controls = null
186                 @controls_shown = false
187         destruct: ->
188                 super()
189                 @hide_controls()
190                 return
191         clone: ->
192                 return new Widget @
193         make_controls: ->
194                 @controls = []
195                 return
196         hide_controls: ->
197                 return unless @controls? and @controls_shown
198                 for c in @controls
199                         c.hide()
200                 @controls_shown = false
201                 return
202         show_controls: ->
203                 return if @controls? and @controls_shown
204                 unless @controls?
205                         @make_controls()
206                 return unless @controls?
207                 for c in @controls
208                         c.show()
209                 @controls_shown = true
210                 return
211         move: (xy) ->
212                 dx = xy.x - @x
213                 dy = xy.y - @y
214                 super xy
215                 if @controls?
216                         for c in @controls
217                                 c.move x: c.x + dx, y: c.y + dy
218                 return
219         set_state: (state) ->
220                 return if @state is state
221                 if @state is STATES.EDITING
222                         @hide_controls()
223                 super state
224                 return
225
226 class RectWidget extends Widget
227         constructor: (args) ->
228                 super args
229                 @css_class = 'box'
230                 @el = json_to_svg rect:
231                         x: @x + 1
232                         y: @y + 1
233                         width: @width - 2
234                         height: @height - 2
235                         class: 'box normal'
236                 @show()
237                 return
238         clone: ->
239                 return new RectWidget @
240         as_array: ->
241                 return [@x, @y, @width, @height]
242         from_array: (args, a) ->
243                 args.x = a[0]
244                 args.y = a[1]
245                 args.width = a[2]
246                 args.height = a[3]
247                 return new RectWidget args
248         move: (args) ->
249                 super args
250                 @el.setAttribute 'x', @x + 1
251                 @el.setAttribute 'y', @y + 1
252                 @reposition_controls()
253                 return
254         proximity: (xy) -> # return the square of the distance to your visible bits
255                 x = xy.x
256                 y = xy.y
257                 prox = PROX_TOO_FAR
258                 in_x = false
259                 in_y = false
260                 if x > @x - CLICK_FUZ and x < @x + @width + CLICK_FUZ
261                         in_x = true
262                         if y < @y + @height / 2
263                                 new_prox = @y - y
264                         else
265                                 new_prox = @y + @height - y
266                         new_prox *= new_prox
267                         if new_prox < prox
268                                 prox = new_prox
269                 if y > @y - CLICK_FUZ and y < @y + @height + CLICK_FUZ
270                         in_y = true
271                         if x < @x + @width / 2
272                                 new_prox = @x - x
273                         else
274                                 new_prox = @x + @width - x
275                         new_prox *= new_prox
276                         if new_prox < prox
277                                 prox = new_prox
278                 # "hit" anything inside
279                 #if in_x and in_y and prox > PROX_MAX
280                 #       prox = PROX_MAX - 1
281                 return prox
282         resize: (wh) ->
283                 dw = wh.w - @width
284                 dh = wh.h - @height
285                 @width = wh.w
286                 @el.setAttribute 'width', @width - 2
287                 @height = wh.h
288                 @el.setAttribute 'height', @height - 2
289                 @reposition_controls()
290                 return
291         reposition_controls: ->
292                 if @controls? and @controls.length > 1
293                         positions = @control_positions()
294                         for i in [0...positions.length]
295                                 @controls[i].move x: positions[i].x, y: positions[i].y
296                 return
297         control_positions: ->
298                 gap = 7
299                 mgap = 9
300                 w2p = Math.floor(@width / 2) + 0.5
301                 h2p = Math.floor(@height / 2) + 0.5
302                 return [
303                         { x: @x - gap, y: @y - gap }
304                         { x: @x + w2p, y: @y - mgap }
305                         { x: @x + @width + gap, y: @y - gap }
306                         { x: @x + @width + mgap, y: @y + h2p }
307                         { x: @x + @width + gap, y: @y + @height + gap }
308                         { x: @x + w2p, y: @y + @height + mgap }
309                         { x: @x - gap, y: @y + @height + gap }
310                         { x: @x - mgap, y: @y + h2p }
311                 ]
312         make_controls: ->
313                 @controls = []
314                 positions = @control_positions()
315                 for i in [0...positions.length]
316                         @controls.push new ControlPath {
317                                 svg: @svg
318                                 x: positions[i].x
319                                 y: positions[i].y
320                                 drag: resizers[i] @
321                                 shape: resizer_shapes[i % resizer_shapes.length]
322                         }
323                 return
324
325 class PolylineWidget extends Widget
326         constructor: (args) ->
327                 super args
328                 @css_class = 'polyline'
329                 @nodes = []
330                 for n in args.nodes
331                         @nodes.push x: n.x, y: n.y
332                 @el = json_to_svg path:
333                         d: @my_path_d()
334                         class: 'polyline normal'
335                 @show()
336         as_array: ->
337                 ret = []
338                 for n in @nodes
339                         ret.push @x + n.x
340                         ret.push @y + n.y
341                 return ret
342         from_array: (args, a) ->
343                 # the first node is also used as the @x,@y
344                 args.x = a[0]
345                 args.y = a[1]
346                 args.nodes = []
347                 while a.length
348                         xy = {}
349                         xy.x = a.shift() - args.x
350                         xy.y = a.shift() - args.y
351                         args.nodes.push xy
352                 return new PolylineWidget args
353         clone: ->
354                 return new PolylineWidget @
355         my_path_d: ->
356                 ret = ''
357                 for n in @nodes
358                         if ret is ''
359                                 ret += 'M'
360                         else
361                                 ret += 'L'
362                         ret += n.x + @x
363                         ret += ' '
364                         ret += n.y + @y
365                 return ret
366         move: (args) ->
367                 super args
368                 @el.setAttribute 'd', @my_path_d()
369                 @reposition_controls()
370                 return
371         proximity: (xy) -> # return the square of the distance to your visible bits
372                 prox = PROX_TOO_FAR
373                 for n, i in @nodes
374                         dx = @x + n.x - xy.x
375                         dy = @y + n.y - xy.y
376                         p = dx * dx + dy * dy
377                         if p < prox
378                                 prox = p
379                         if i > 0
380                                 l1x = @x + @nodes[i-1].x
381                                 l1y = @y + @nodes[i-1].y
382                                 l2x = @x + @nodes[i].x
383                                 l2y = @y + @nodes[i].y
384                                 ldx = l2x - l1x
385                                 ldy = l2y - l1y
386                                 if ldx is 0 # vertical line
387                                         if (xy.y < l1y) is (xy.y < l2y)
388                                                 continue
389                                         dx = l1x - xy.x
390                                         p = dx * dx
391                                 else if ldy is 0 # horizontal line
392                                         if (xy.x < l1x) is (xy.x < l2x)
393                                                 continue
394                                         dy = l1y - xy.y
395                                         p = dy * dy
396                                 else # slanty line
397                                         # https://en.wikipedia.org/wiki/Distance_from_a_point_to_a_line
398                                         a = ldy
399                                         b = -1 * ldx
400                                         c = l2x * l1y - l2y * l1x
401                                         y_on_line = (a * (a * xy.y - b * xy.x) - b * c) / (a * a + b * b)
402                                         if (y_on_line < l1y) is (y_on_line < l2y)
403                                                 continue
404                                         p = (a * xy.x + b * xy.y + c) / Math.sqrt(a * a + b * b)
405                                         p *= p
406
407                                 if p < prox
408                                         prox = p
409                 return prox
410         resize: (wh) ->
411                 # TODO implement
412                 #@el.setAttribute 'd', @my_path_d()
413                 #@reposition_controls()
414                 return
415         node_dragger: (i) ->
416                 if i is 0
417                         return (dxy) =>
418                                 for j in [1...@nodes.length]
419                                         @nodes[j].x -= dxy.x
420                                         @nodes[j].y -= dxy.y
421                                 @move x: @x + dxy.x, y: @y + dxy.y
422                 return (dxy) =>
423                         @nodes[i].x += dxy.x
424                         @nodes[i].y += dxy.y
425                         @el.setAttribute 'd', @my_path_d()
426                         @reposition_controls()
427         reposition_controls: ->
428                 if @controls? and @controls.length > 1
429                         positions = @control_positions()
430                         for i in [0...positions.length]
431                                 @controls[i].move x: positions[i].x, y: positions[i].y
432                 return
433         control_positions: ->
434                 ret = []
435                 for n in @nodes
436                         ret.push x: @x + n.x, y: @y + n.y
437                 return ret
438         make_controls: ->
439                 @controls = []
440                 positions = @control_positions()
441                 for i in [0...positions.length]
442                         @controls.push new ControlPath {
443                                 svg: @svg
444                                 x: positions[i].x
445                                 y: positions[i].y
446                                 drag: @node_dragger i
447                                 shape: shape_node_move
448                         }
449                 return
450
451 CSS_CLASS_TO_PICKLE_TYPE = {
452         box: '0'
453         polyline: '1'
454 }
455 PICKLE_TYPE_TO_WIDGET_CLASS = [
456         RectWidget
457         PolylineWidget
458 ]
459 # called automatically on domcontentloaded
460 init = ->
461         svg_offset = null
462         $container = $ '.crayon_mockup'
463         svg = json_to_svg svg: width: width, height: height, viewBox: "0 0 #{width} #{height}"
464         $svg = $ svg
465         $container.append $svg
466         svg.appendChild json_to_svg filter:
467                 id: 'crayon', filterUnits: 'userSpaceOnUse'
468                 x: '-5%', y: '-5%', height: '110%', width: '110%'
469                 children: [
470                         { feTurbulence: baseFrequency: '.3', numOctaves: '2', type: 'fractalNoise' }
471                         { feDisplacementMap: scale: '6', xChannelSelector: 'R', in: 'SourceGraphic' }
472                 ]
473         svg.appendChild json_to_svg style:
474                 type: 'text/css'
475                 contents: '.box.normal,.polyline.normal{filter: url(#crayon)}'
476
477         # create canvas border
478         svg.appendChild json_to_svg rect:
479                 x: 1
480                 y: supply_height + 1
481                 width: width - 2
482                 height: height - 2 - supply_height
483                 class: 'canvas_border'
484
485         supply = {}
486         supply_count = 0
487         supply_add = (type, args) ->
488                 args.x ?= 0
489                 args.y ?= 0
490                 args.x += 30 + supply_count * 90
491                 args.y += (supply_height - 50) / 2
492                 args.svg = svg
493                 w = new type args
494                 supply[w.id] = w
495                 supply_count += 1
496         supply_add RectWidget, width: 50, height: 50
497         supply_add PolylineWidget, y: 25, nodes: [{x: 0, y: 0}, {x: 50, y: 0}]
498         supply_add PolylineWidget, x: 25, nodes: [{x: 0, y: 0}, {x: 0, y: 50}]
499         supply_add PolylineWidget, x: 10, nodes: [{x: 0, y: 0}, {x: 15, y: 50}, {x: 30, y: 0}]
500         supply_add PolylineWidget, y: 50, nodes: [{x: 0, y: 0}, {x: 17, y: -50}, {x: 33, y: 0}, {x: 50, y: -50}]
501
502         # editor state
503         widgets = {}
504         selected = {}
505         selected_count = 0
506         hovered = null
507         drag_targets = null
508         dragging = false # mouse state
509         drag_from = x: 0, y: 0 # mouse was here at last frame of drag
510         shift_key_down = false
511
512         lc = "abcdefghijklmnopqrstuvwxyz"
513         uc = "ABCDEFGHIJKLMNOPQRSTUVWXYZ"
514         cc_lca = lc.charCodeAt 0
515         cc_lcz = lc.charCodeAt 25
516         cc_uca = uc.charCodeAt 0
517         cc_ucz = uc.charCodeAt 25
518         cc_0 = '0'.charCodeAt 0
519         cc_9 = '9'.charCodeAt 0
520         # takes an array of positive integers, and encodes it as a string
521         pickle = (a) ->
522                 ret = ''
523                 for i in a
524                         cs = lc
525                         r = ''
526                         while i > 0 or r is ''
527                                 digit = i % cs.length
528                                 i -= digit
529                                 i /= cs.length
530                                 r = cs.charAt(digit) + r
531                                 cs = uc
532                         ret += r
533                 return ret
534         pickle_widgets = ->
535                 ret = '0' # version of this encoding scheme
536                 for id, w of widgets
537                         if CSS_CLASS_TO_PICKLE_TYPE[w.css_class]?
538                                 ret += CSS_CLASS_TO_PICKLE_TYPE[w.css_class]
539                                 ret += pickle w.as_array()
540                 return ret
541         save = ->
542                 window.location.hash = pickle_widgets()
543         load = (str) ->
544                 return if str.charAt(1) isnt '0'
545                 wtype = null
546                 args = []
547                 ii = 0
548                 load_1 = (next_type) ->
549                         unless wtype?
550                                 if next_type?
551                                         wtype = next_type
552                                 return
553                         w = PICKLE_TYPE_TO_WIDGET_CLASS[wtype]::from_array svg: svg, args
554                         widgets[w.id] = w
555                         wtype = next_type
556                         args = []
557                         ii = 0
558                 for i in [2...str.length]
559                         c = str.charCodeAt(i)
560                         if cc_0 <= c <= cc_9
561                                 load_1 c - cc_0
562                         else if cc_lca <= c <= cc_lcz
563                                 ii *= lc.length
564                                 ii += c - cc_lca
565                                 args.push ii
566                                 ii = 0
567                         else if cc_uca <= c <= cc_ucz
568                                 ii *= lc.length
569                                 ii += c - cc_uca
570                 load_1 null
571                 return
572         stop_editing = ->
573                 for id, w of widgets
574                         w.hide_controls()
575                 return
576         deselect = (w) ->
577                 return unless selected[w.id]?
578                 w.set_state STATES.NORMAL
579                 delete selected[w.id]
580                 selected_count -= 1
581                 w.hide_controls()
582                 return
583         deselect_all = (except = null) ->
584                 for id, w of selected
585                         if w isnt except
586                                 deselect w
587                 return
588         _select = (w) -> # don't call this directly, use select_only() or select_also()
589                 w.set_state STATES.SELECTED
590                 selected[w.id] = w
591                 return
592         select_only = (w) ->
593                 deselect_all w
594                 return if selected[w.id]?
595                 _select w
596                 return
597         select_also = (w) ->
598                 return if selected[w.id]?
599                 stop_editing()
600                 _select w
601                 return
602         find_closest = (widgets, xy) ->
603                 prox = PROX_TOO_FAR
604                 closest = null
605                 for id, w of widgets
606                         new_prox = w.proximity xy
607                         if new_prox < prox
608                                 prox = new_prox
609                                 closest = w
610                 if prox > PROX_MAX
611                         return null
612                 return closest
613         find_closest_control = (xy) ->
614                 prox = PROX_TOO_FAR
615                 closest = null
616                 for id, s of selected
617                         if s.controls_shown
618                                 for c in s.controls
619                                         if c.shown
620                                                 new_prox = c.proximity xy
621                                                 if new_prox < prox
622                                                         prox = new_prox
623                                                         closest = c
624                 if prox > PROX_MAX
625                         return null
626                 return closest
627         find_closest_thing = (xy) ->
628                 if xy.y < supply_height
629                         return find_closest supply, xy
630                 else
631                         hit = find_closest_control xy
632                         hit ?= find_closest widgets, xy
633                         return hit
634         svg_event_to_xy = (e) ->
635                 unless svg_offset?
636                         svg_offset = $svg.offset()
637                 return {
638                         x: Math.round(e.pageX - svg_offset.left)
639                         y: Math.round(e.pageY - svg_offset.top)
640                 }
641         mousedown = (e) ->
642                 hit = null
643                 closest = null
644                 mousemove e
645                 if dragging # two mousedowns in a row?! it happens
646                         return mouseup e
647                 xy = svg_event_to_xy e
648                 if xy.y < supply_height
649                         hit = find_closest supply, xy
650                         if hit?
651                                 hit = hit.clone()
652                                 widgets[hit.id] = hit
653                 else
654                         hit = find_closest_control xy
655                         unless hit?
656                                 hit = find_closest widgets, xy
657                 if hit?
658                         if hit.type is TYPE_WIDGET
659                                 if selected[hit.id]
660                                         # already selected
661                                         # TODO start detection of a click that doesn't drag (to shrink selection)
662                                 else if xy.y < supply_height
663                                         # dragging a new thing in
664                                         select_only hit
665                                 else if shift_key_down
666                                         select_also hit
667                                 else
668                                         select_only hit
669                                 for id, w of selected
670                                         w.set_state STATES.DRAGGING
671                                 drag_targets = selected
672                         else
673                                 drag_targets = {}
674                                 drag_targets[hit.id] = hit
675                         dragging = true
676                         drag_from = xy
677                 else
678                         deselect_all()
679                 return
680         mouseup = (e) ->
681                 save()
682                 mousemove e
683                 if dragging
684                         widgets_remaining = 0
685                         for id, s of drag_targets
686                                 if s.type is TYPE_WIDGET
687                                         if s.y < supply_height
688                                                 deselect s
689                                                 s.destruct()
690                                                 delete widgets[id]
691                                         else
692                                                 widgets_remaining += 1
693                                                 s.set_state STATES.SELECTED
694                         if widgets_remaining is 1
695                                 for id, s of drag_targets
696                                         if s.type is TYPE_WIDGET
697                                                 s.set_state STATES.EDITING
698                                                 s.show_controls()
699                 dragging = false
700                 return
701         mousemove = (e) ->
702                 xy = svg_event_to_xy e
703                 if dragging
704                         return if drag_from.x is xy.x and drag_from.y is xy.y
705                         rel_x = xy.x - drag_from.x
706                         rel_y = xy.y - drag_from.y
707                         drag_from = xy
708                         for id, w of drag_targets
709                                 w.drag x: rel_x, y: rel_y
710                 else
711                         hit = find_closest_thing xy
712                         return if hit is hovered # both null, or both the same
713                         return if hit?.type is TYPE_CONTROL # hovering a control doesn't change display
714                         if hovered?
715                                 hovered.set_hover false
716                         if hit?
717                                 hovered = hit
718                                 hit.set_hover true
719                 return
720         $svg.mousedown (e) ->
721                 mousedown e
722                 return false
723         $svg.mouseup (e) ->
724                 mouseup e
725                 return false
726         $svg.mousemove (e) ->
727                 mousemove e
728                 return false
729         $(document).on 'keyup keydown', (e) ->
730                 shift_key_down = e.shiftKey
731                 return true
732         #($ document).keydown (e) ->
733
734         if window.location.hash
735                 load window.location.hash
736
737 $ init