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