JasonWoof Got questions, comments, patches, etc.? Contact Jason Woofenden
906228dcaf85c5c34bd0168496fd1a053394f827
[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 STYLE_NORMAL = 0
27 STYLE_SELECTED = 1
28 STYLE_HOVER = 2
29 STYLE_EDITING = 3
30 STYLE_DRAGGING = 4
31 STYLE_TO_CLASS = [
32         "normal"
33         "selected"
34         "hover"
35         "editing"
36         "dragging"
37 ]
38 TYPE_WIDGET = 1
39 TYPE_CONTROL = 2
40
41 set_style_class = (args) ->
42         args.el.setAttribute 'class', "#{args.class} #{STYLE_TO_CLASS[args.style]}"
43
44 # json (compiled to javascript and minified) is ~8% smaller than the resulting xml
45 json_to_svg = (json) ->
46         for tag, attrs of json
47                 el = document.createElementNS 'http://www.w3.org/2000/svg', tag
48                 for k, v of attrs
49                         if k is 'children'
50                                 for child in v
51                                         el.appendChild json_to_svg child
52                         else if k is 'contents'
53                                 el.appendChild document.createTextNode v
54                         else
55                                 el.setAttribute k, v
56         return el
57
58 next_widget_id = 0
59 # public vars: x, y, width, height, el
60 class Visible
61         # required args: svg
62         constructor: (args) ->
63                 @id = next_widget_id
64                 next_widget_id += 1
65                 @svg = args.svg
66                 @x = args.x ? 1
67                 @y = args.y ? 1
68                 @width = args.width ? 50
69                 @height = args.height ? 34
70                 @style = args.style ? STYLE_NORMAL
71         destruct: ->
72         move: (xy) -> # just move
73                 @x = xy.x
74                 @y = xy.y
75         drag: (xy) -> # react to mouse drag (obey constraints, etc.)
76                 @move xy
77         proximity: (xy) -> # return the square of the distance to your visible bits
78                 return PROX_TOO_FAR
79         set_style: (style) ->
80                 @style = style
81
82 class Control extends Visible
83         constructor: (args) ->
84                 super args
85                 @type = TYPE_CONTROL
86                 @on_drag = args.drag
87                 @on_destruct = args.done ? null
88         destruct: ->
89                 super()
90                 if @on_destruct?
91                         @on_destruct @
92         drag: (args) -> # call this when control point is being manipulated directly
93                 @on_drag args
94         proximity: (xy) -> # return the square of the distance to your visible bits
95                 dx = xy.x - @x
96                 dy = xy.y - @y
97                 return dx * dx + dy * dy
98
99 class ControlPoint extends Control
100         constructor: (args) ->
101                 super args
102                 @el = json_to_svg circle:
103                         cx: @x + 1
104                         cy: @y + 1
105                         r: 6
106                         class: 'control_point normal'
107                 @svg.appendChild @el
108         destruct: ->
109                 super()
110                 if @el?
111                         @svg.removeChild @el
112         move: (args) ->
113                 super args
114                 @el.setAttribute 'cx', @x
115                 @el.setAttribute 'cy', @y
116
117 class Widget extends Visible
118         #sub-classes are expected to implement all of these:
119         constructor: (args) ->
120                 super args
121                 @controls = []
122                 @type = TYPE_WIDGET
123         destruct: ->
124                 @kill_controls()
125         clone: ->
126                 return new Widget @
127         make_controls: -> # create controls, return them
128                 return []
129         kill_controls: ->
130                 console.log 'kill_controls'
131                 for c in @controls
132                         c.destruct()
133                 @controls = []
134                 return
135         move: (xy) -> # just move
136                 dx = xy.x - @x
137                 dy = xy.y - @y
138                 super xy
139                 for c in @controls
140                         c.move x: c.x + dx, y: c.y + dy
141
142 class RectWidget extends Widget
143         constructor: (args) ->
144                 super args
145                 @css_class = 'box'
146                 @el = json_to_svg rect:
147                         x: @x + 1
148                         y: @y + 1
149                         width: @width - 2
150                         height: @height - 2
151                         class: 'box normal'
152                 @svg.appendChild @el
153         destruct: ->
154                 super()
155                 if @el?
156                         @svg.removeChild @el
157         clone: ->
158                 return new RectWidget @
159         set_style: (style) ->
160                 super style
161                 set_style_class el: @el, class: 'box', style: style
162         move: (args) ->
163                 super args
164                 @el.setAttribute 'x', @x + 1
165                 @el.setAttribute 'y', @y + 1
166         proximity: (xy) -> # return the square of the distance to your visible bits
167                 x = xy.x
168                 y = xy.y
169                 prox = PROX_TOO_FAR
170                 in_x = false
171                 in_y = false
172                 if x > @x - CLICK_FUZ and x < @x + @width + CLICK_FUZ
173                         in_x = true
174                         if y < @y + @height / 2
175                                 new_prox = @y - y
176                         else
177                                 new_prox = @y + @height - y
178                         new_prox *= new_prox
179                         if new_prox < prox
180                                 prox = new_prox
181                 if y > @y - CLICK_FUZ and y < @y + @height + CLICK_FUZ
182                         in_y = true
183                         if x < @x + @width / 2
184                                 new_prox = @x - x
185                         else
186                                 new_prox = @x + @width - x
187                         new_prox *= new_prox
188                         if new_prox < prox
189                                 prox = new_prox
190                 if in_x and in_y and prox > PROX_MAX
191                         prox = PROX_MAX - 1
192                 return prox
193         resize: (wh) ->
194                 @width = wh.w
195                 @el.setAttribute 'width', @width
196                 @height = wh.h
197                 @el.setAttribute 'height', @height
198                 if @controls.length > 1
199                         @controls[1].move x: @x + @width, y: @y + @height
200         make_controls: (args) -> # create controls, return them
201                 console.log 'make_controls'
202                 if @controls.length > 0
203                         console.log "warning: re-adding controls"
204                         @kill_controls()
205                 w = @
206                 @controls = [
207                         new ControlPoint svg: @svg, x: @x, y: @y, done: args.done, drag: (xy) ->
208                                 dx = xy.x - @x
209                                 dy = xy.y - @y
210                                 w.resize w: w.width - dx, h: w.height - dy
211                                 w.move x: w.x + dx, y: w.y + dy
212                         new ControlPoint svg: @svg, x: @x + @width, y: @y + @height, done: args.done, drag: (xy) ->
213                                 dx = xy.x - @x
214                                 dy = xy.y - @y
215                                 w.resize w: w.width + dx, h: w.height + dy
216                 ]
217                 return @controls
218
219 # called automatically on domcontentloaded
220 init = ->
221         svg_offset = null
222         $container = $ '.crayon_mockup'
223         svg = json_to_svg svg: width: width, height: height, viewBox: "0 0 #{width} #{height}"
224         $svg = $ svg
225         $container.append $svg
226         svg.appendChild json_to_svg filter:
227                 id: 'crayon', filterUnits: 'userSpaceOnUse'
228                 x: '-5%', y: '-5%', height: '110%', width: '110%'
229                 children: [
230                         { feTurbulence: baseFrequency: '.3', numOctaves: '2', type: 'fractalNoise' }
231                         { feDisplacementMap: scale: '6', xChannelSelector: 'R', in: 'SourceGraphic' }
232                 ]
233         svg.appendChild json_to_svg style:
234                 type: 'text/css'
235                 contents: '.box.normal,.box.hover,.box.selected{filter: url(#crayon)}'
236
237         # create canvas border
238         svg.appendChild json_to_svg rect:
239                 x: 1
240                 y: supply_height + 1
241                 width: width - 2
242                 height: height - 2 - supply_height
243                 class: 'canvas_border'
244
245         supply = {}
246         for args, i in [
247                 { width: 40, height: 40 }
248                 { width: 12, height: 50 }
249                 { width: 70, height: 12 }
250         ]
251                 widget = new RectWidget {
252                         width: args.width
253                         height: args.height
254                         x: 30 + i * 90 + (70 - args.width) / 2
255                         y: (supply_height - args.height) / 2
256                         svg: svg
257                 }
258                 supply[widget.id] = widget
259
260         # editor state
261         on_canvas = {}
262         selected = {}
263         controls = {}
264         editing = {} # has controls
265         dragging = false
266         drag_from = x: 0, y: 0 # mouse was here at last frame of drag
267         shift_key_down = false
268
269         selected_type = ->
270                 for s of selected
271                         return s.type
272                 return null
273         deselect = (s) ->
274                 s.set_style STYLE_NORMAL
275                 if s.type is TYPE_WIDGET
276                         s.kill_controls()
277                 delete selected[s.id]
278                 return
279         deselect_all = (args) ->
280                 except = args?.except ? null
281                 for id, s of selected
282                         deselect s
283                 return
284         select_only = (sel) ->
285                 deselect_all except: sel
286                 return if selected[sel.id]?
287                 selected[sel.id] = sel
288         select_also = (sel) ->
289                 return if selected[sel.id]?
290                 sel_type = selected_type()
291                 if sel_type isnt sel.type
292                         deselect_all()
293                 selected[sel.id] = sel
294                 return
295         find_closest = (widgets, xy) ->
296                 prox = PROX_TOO_FAR
297                 closest = null
298                 for id, w of widgets
299                         new_prox = w.proximity xy
300                         if new_prox < prox
301                                 prox = new_prox
302                                 closest = w
303                 if prox > PROX_MAX
304                         return null
305                 return closest
306         svg_event_to_xy = (e) ->
307                 unless svg_offset?
308                         svg_offset = $svg.offset()
309                 return {
310                         x: Math.round(e.pageX - svg_offset.left)
311                         y: Math.round(e.pageY - svg_offset.top)
312                 }
313         mousedown = (e) ->
314                 mousemove e
315                 if dragging # two mousedowns in a row?! it happens
316                         return mouseup e
317                 xy = svg_event_to_xy e
318                 if xy.y < supply_height
319                         closest = find_closest supply, xy
320                         if closest?
321                                 closest = closest.clone()
322                                 on_canvas[closest.id] = closest
323                 else
324                         closest = find_closest controls, xy
325                         unless closest?
326                                 closest = find_closest on_canvas, xy
327                 if closest?
328                         console.log closest
329                         if selected[closest.id]
330                                 # already selected
331                                 # TODO start detection of a click that doesn't drag (to shrink selection)
332                         else if xy.y < supply_height
333                                 # dragging a new thing in
334                                 select_only closest
335                         else if shift_key_down
336                                 select_also closest
337                         else
338                                 select_only closest
339                         for id, s of selected
340                                 s.set_style STYLE_DRAGGING
341                         dragging = true
342                         drag_from = xy
343                 else
344                         deselect_all()
345                 return false
346         mouseup = (e) ->
347                 mousemove e
348                 if dragging
349                         for id, w of selected
350                                 if w.y < supply_height
351                                         deselect w
352                                         w.destruct()
353                                         delete on_canvas[id]
354                                 else
355                                         w.set_style STYLE_SELECTED
356                                         if w.type is TYPE_WIDGET
357                                                 cs = w.make_controls done: (c) ->
358                                                         if controls[c.id]?
359                                                                 delete controls[c.id]
360                                                 for c in cs
361                                                         controls[c.id] = c
362                                                 editing[w.id] = w
363                                                 delete selected[w.id]
364                 dragging = false
365                 return false
366         prev_hover = null
367         mousemove = (e) ->
368                 xy = svg_event_to_xy e
369                 if dragging
370                         return if drag_from.x is xy.x and drag_from.y is xy.y
371                         rel_x = xy.x - drag_from.x
372                         rel_y = xy.y - drag_from.y
373                         drag_from = xy
374                         for id, w of selected
375                                 w.drag x: w.x + rel_x, y: w.y + rel_y
376                 else
377                         hover = find_closest on_canvas, xy
378                         unless hover?
379                                 hover = find_closest supply, xy
380                         if hover != prev_hover
381                                 if prev_hover?
382                                         # FIXME
383                                         if selected[prev_hover.id]?
384                                                 prev_hover.set_style STYLE_SELECTED
385                                         else
386                                                 prev_hover.set_style STYLE_NORMAL
387                                 if hover?
388                                         hover.set_style STYLE_HOVER
389                                 prev_hover = hover
390                 return false
391         $svg.mousedown mousedown
392         $svg.mouseup mouseup
393         $svg.mousemove mousemove
394         $(document).on 'keyup keydown', (e) ->
395                 shift_key_down = e.shiftKey
396                 return true
397         #($ document).keydown (e) ->
398
399 $ init