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