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