JasonWoof Got questions, comments, patches, etc.? Contact Jason Woofenden
multi-select, widget lists as hashes
[crayon_mockup.git] / auto.coffee
1 # settings
2 width = 800
3 height = 600
4 supply_height = 96
5 CLICK_FUZ = 10 # this far away from things is close enough to be "clicked on"
6 PROX_MAX = CLICK_FUZ * CLICK_FUZ
7
8 # constants
9 STYLE_NORMAL = 0
10 STYLE_SELECTED = 1
11 STYLE_HOVER = 2
12 STYLE_EDITING = 3
13 STYLE_DRAGGING = 4
14
15 # json (compiled to javascript and minified) is ~8% smaller than the resulting xml
16 json_to_svg = (json) ->
17         for tag, attrs of json
18                 el = document.createElementNS 'http://www.w3.org/2000/svg', tag
19                 for k, v of attrs
20                         if k is 'children'
21                                 for child in v
22                                         el.appendChild json_to_svg child
23                         else
24                                 el.setAttribute k, v
25         return el
26
27 next_widget_id = 0
28 # public vars: x, y, width, height, el
29 class Widget
30         # required args: svg
31         constructor: (args) ->
32                 @id = next_widget_id
33                 next_widget_id += 1
34                 @svg = args.svg
35                 @style = args.style ? STYLE_NORMAL
36                 @x = args.x ? 1
37                 @y = args.y ? 1
38                 @width = args.width ? 50
39                 @height = args.height ? 34
40         destruct: ->
41         clone: ->
42                 return new Widget @
43         move: (args) ->
44                 @x = args.x
45                 @y = args.y
46         proximity: (xy) -> # return the square of the distance to your visible bits
47                 return PROX_MAX + 1
48         set_style: (style) ->
49                 return if @style is style
50                 if style is STYLE_NORMAL
51                         @el.setAttribute 'style', 'filter: url(#crayon)'
52                 else
53                         if @style is STYLE_NORMAL
54                                 @el.setAttribute 'style', ''
55                 switch style
56                         when STYLE_NORMAL
57                                 @el.setAttribute 'class', "#{@css_class}"
58                         when STYLE_SELECTED
59                                 @el.setAttribute 'class', "#{@css_class} selected"
60                         when STYLE_HOVER
61                                 @el.setAttribute 'class', "#{@css_class} hover"
62                         when STYLE_DRAGGING
63                                 @el.setAttribute 'class', "#{@css_class} dragging"
64                         # FIXME when STYLE_EDITING
65                 @style = style
66         controls: -> # create controls, return them
67                 return []
68         hide_controls: ->
69
70 class RectWidget extends Widget
71         constructor: (args) ->
72                 super args
73                 @css_class = 'box'
74                 @el = json_to_svg rect:
75                         x: @x + 1
76                         y: @y + 1
77                         width: @width - 2
78                         height: @height - 2
79                         class: 'box'
80                         style: if @style is STYLE_NORMAL then 'filter: url(#crayon)' else ''
81                 @svg.appendChild @el
82         destruct: ->
83                 super()
84                 if @el?
85                         @svg.removeChild @el
86         clone: ->
87                 return new RectWidget @
88         move: (args) ->
89                 super args
90                 @el.setAttribute 'x', @x + 1
91                 @el.setAttribute 'y', @y + 1
92         proximity: (xy) -> # return the square of the distance to your visible bits
93                 x = xy.x
94                 y = xy.y
95                 prox = PROX_MAX + 1
96                 in_x = false
97                 in_y = false
98                 if x > @x - CLICK_FUZ and x < @x + @width + CLICK_FUZ
99                         in_x = true
100                         if y < @y + @height / 2
101                                 new_prox = @y - y
102                         else
103                                 new_prox = @y + @height - y
104                         new_prox *= new_prox
105                         if new_prox < prox
106                                 prox = new_prox
107                 if y > @y - CLICK_FUZ and y < @y + @height + CLICK_FUZ
108                         in_y = true
109                         if x < @x + @width / 2
110                                 new_prox = @x - x
111                         else
112                                 new_prox = @x + @width - x
113                         new_prox *= new_prox
114                         if new_prox < prox
115                                 prox = new_prox
116                 if in_x and in_y and prox > PROX_MAX
117                         prox = PROX_MAX - 1
118                 return prox
119         controls: -> # create controls, return them
120                 return []
121         hide_controls: ->
122
123 # called automatically on domcontentloaded
124 init = ->
125         svg_offset = null
126         $container = $ '.crayon_mockup'
127         svg = json_to_svg svg: width: width, height: height, viewBox: "0 0 #{width} #{height}"
128         $svg = $ svg
129         $container.append $svg
130         svg.appendChild json_to_svg filter:
131                 id: 'crayon', filterUnits: 'userSpaceOnUse'
132                 x: '-5%', y: '-5%', height: '110%', width: '110%'
133                 children: [
134                         { feTurbulence: baseFrequency: '.3', numOctaves: '2', type: 'fractalNoise' }
135                         { feDisplacementMap: scale: '6', xChannelSelector: 'R', in: 'SourceGraphic' }
136                 ]
137
138         # create canvas border
139         svg.appendChild json_to_svg rect:
140                 x: 1
141                 y: supply_height + 1
142                 width: width - 2
143                 height: height - 2 - supply_height
144                 class: 'canvas_border'
145
146         supply = {}
147         for args, i in [
148                 { width: 40, height: 40 }
149                 { width: 12, height: 50 }
150                 { width: 70, height: 12 }
151         ]
152                 widget = new RectWidget {
153                         width: args.width
154                         height: args.height
155                         x: 30 + i * 90 + (70 - args.width) / 2
156                         y: (supply_height - args.height) / 2
157                         svg: svg
158                 }
159                 supply[widget.id] = widget
160
161         # editor state
162         on_canvas = {}
163         selected = {}
164         editing = {} # has controls
165         dragging = false
166         drag_from = x: 0, y: 0 # mouse was here at last frame of drag
167         shift_key_down = false
168
169         deselect_all = (args) ->
170                 except = args?.except ? null
171                 for id, w of selected
172                         w.set_style STYLE_NORMAL
173                         delete selected[id]
174         closest_widget = (widgets, xy) ->
175                 prox = PROX_MAX + 1
176                 closest = null
177                 for id, w of widgets
178                         new_prox = w.proximity xy
179                         if new_prox < prox
180                                 prox = new_prox
181                                 closest = w
182                 if prox < PROX_MAX
183                         return closest
184                 return null
185         svg_event_to_xy = (e) ->
186                 unless svg_offset?
187                         svg_offset = $svg.offset()
188                 return {
189                         x: Math.round(e.pageX - svg_offset.left)
190                         y: Math.round(e.pageY - svg_offset.top)
191                 }
192         mousedown = (e) ->
193                 mousemove e
194                 if dragging # two mousedowns in a row?! it happens
195                         return mouseup e
196                 xy = svg_event_to_xy e
197                 if xy.y < supply_height
198                         closest = closest_widget supply, xy
199                         if closest?
200                                 closest = closest.clone()
201                                 on_canvas[closest.id] = closest
202                 else
203                         closest = closest_widget on_canvas, xy
204                 if closest?
205                         unless (shift_key_down or selected[closest.id]?)
206                                 deselect_all except: closest
207                         selected[closest.id] = closest
208                         closest.set_style STYLE_DRAGGING
209                         dragging = true
210                         drag_from = xy
211                 else
212                         deselect_all()
213                 return false
214         mouseup = (e) ->
215                 mousemove e
216                 if dragging
217                         for id, w of selected
218                                 if w.y < supply_height
219                                         w.destruct()
220                                         delete selected[id]
221                                 else
222                                         w.set_style STYLE_SELECTED
223                 dragging = false
224                 return false
225         prev_hover = null
226         mousemove = (e) ->
227                 xy = svg_event_to_xy e
228                 if dragging
229                         return if drag_from.x is xy.x and drag_from.y is xy.y
230                         rel_x = xy.x - drag_from.x
231                         rel_y = xy.y - drag_from.y
232                         drag_from = xy
233                         for id, w of selected
234                                 w.move x: w.x + rel_x, y: w.y + rel_y
235                 else
236                         hover = closest_widget on_canvas, xy
237                         unless hover?
238                                 hover = closest_widget supply, xy
239                         if hover != prev_hover
240                                 if prev_hover?
241                                         # FIXME
242                                         if selected[prev_hover.id]?
243                                                 prev_hover.set_style STYLE_SELECTED
244                                         else
245                                                 prev_hover.set_style STYLE_NORMAL
246                                 if hover?
247                                         hover.set_style STYLE_HOVER
248                                 prev_hover = hover
249                 return false
250         $svg.mousedown mousedown
251         $svg.mouseup mouseup
252         $svg.mousemove mousemove
253         $(document).on 'keyup keydown', (e) ->
254                 shift_key_down = e.shiftKey
255                 return true
256         #($ document).keydown (e) ->
257
258 $ init