JasonWoof Got questions, comments, patches, etc.? Contact Jason Woofenden
fix hover
[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 ControlPoint extends Control
101         constructor: (args) ->
102                 super args
103                 @css_class = 'control_point'
104                 @el = json_to_svg circle:
105                         cx: @x + 1
106                         cy: @y + 1
107                         r: 6
108                         class: 'control_point normal'
109                 @svg.appendChild @el
110         destruct: ->
111                 super()
112                 if @el?
113                         @svg.removeChild @el
114         move: (args) ->
115                 super args
116                 @el.setAttribute 'cx', @x
117                 @el.setAttribute 'cy', @y
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                 @width = wh.w
202                 @el.setAttribute 'width', @width
203                 @height = wh.h
204                 @el.setAttribute 'height', @height
205                 if @controls.length > 1
206                         @controls[1].move x: @x + @width, y: @y + @height
207         make_controls: (args) -> # create controls, return them
208                 console.log 'make_controls'
209                 if @controls.length > 0
210                         console.log "warning: re-adding controls"
211                         @kill_controls()
212                 w = @
213                 @controls = [
214                         new ControlPoint svg: @svg, x: @x, y: @y, done: args.done, drag: (dxy) ->
215                                 w.resize w: w.width - dxy.x, h: w.height - dxy.y
216                                 w.move x: w.x + dxy.x, y: w.y + dxy.y
217                         new ControlPoint svg: @svg, x: @x + @width, y: @y + @height, done: args.done, drag: (dxy) ->
218                                 w.resize w: w.width + dxy.x, h: w.height + dxy.y
219                 ]
220                 return @controls
221
222 # called automatically on domcontentloaded
223 init = ->
224         svg_offset = null
225         $container = $ '.crayon_mockup'
226         svg = json_to_svg svg: width: width, height: height, viewBox: "0 0 #{width} #{height}"
227         $svg = $ svg
228         $container.append $svg
229         svg.appendChild json_to_svg filter:
230                 id: 'crayon', filterUnits: 'userSpaceOnUse'
231                 x: '-5%', y: '-5%', height: '110%', width: '110%'
232                 children: [
233                         { feTurbulence: baseFrequency: '.3', numOctaves: '2', type: 'fractalNoise' }
234                         { feDisplacementMap: scale: '6', xChannelSelector: 'R', in: 'SourceGraphic' }
235                 ]
236         svg.appendChild json_to_svg style:
237                 type: 'text/css'
238                 contents: '.box.normal{filter: url(#crayon)}'
239
240         # create canvas border
241         svg.appendChild json_to_svg rect:
242                 x: 1
243                 y: supply_height + 1
244                 width: width - 2
245                 height: height - 2 - supply_height
246                 class: 'canvas_border'
247
248         supply = {}
249         for args, i in [
250                 { width: 40, height: 40 }
251                 { width: 12, height: 50 }
252                 { width: 70, height: 12 }
253         ]
254                 widget = new RectWidget {
255                         width: args.width
256                         height: args.height
257                         x: 30 + i * 90 + (70 - args.width) / 2
258                         y: (supply_height - args.height) / 2
259                         svg: svg
260                 }
261                 supply[widget.id] = widget
262
263         # editor state
264         controls_layer = { all: {}, selected: {} }
265         widget_layer = { all: {}, selected: {}, editing: null }
266         layers = [controls_layer, widget_layer]
267         hovered = null # can be in any layer
268         dragging = false # mouse state
269         drag_layer = null
270         drag_from = x: 0, y: 0 # mouse was here at last frame of drag
271         shift_key_down = false
272
273         stop_editing = ->
274                 if widget_layer.editing
275                         widget_layer.editing.kill_controls()
276                         widget_layer.editing = null
277         deselect = (layer, s) ->
278                 return unless layer.selected[s.id]?
279                 s.set_state STATES.NORMAL
280                 delete layer.selected[s.id]
281                 if widget_layer.editing is s
282                         widget_layer.editing = null
283                 return
284         deselect_all = (layer, except = null) ->
285                 for id, s of layer.selected
286                         deselect layer, s
287                 return
288         _select = (layer, s) -> # don't call this directly, use select_only() or select_also()
289                 s.set_state STATES.SELECTED
290                 layer.selected[s.id] = s
291                 return
292         select_only = (layer, s) ->
293                 deselect_all layer, s
294                 return if layer.selected[s.id]?
295                 _select layer, s
296                 return
297         select_also = (layer, s) ->
298                 return if layer.selected[s.id]?
299                 if layer is widget_layer
300                         stop_editing()
301                 _select layer, s
302                 return
303         find_closest = (widgets, xy) ->
304                 prox = PROX_TOO_FAR
305                 closest = null
306                 for id, w of widgets
307                         new_prox = w.proximity xy
308                         if new_prox < prox
309                                 prox = new_prox
310                                 closest = w
311                 if prox > PROX_MAX
312                         return null
313                 return closest
314         svg_event_to_xy = (e) ->
315                 unless svg_offset?
316                         svg_offset = $svg.offset()
317                 return {
318                         x: Math.round(e.pageX - svg_offset.left)
319                         y: Math.round(e.pageY - svg_offset.top)
320                 }
321         closest_in_layers = (xy) ->
322                 for layer in layers
323                         s = find_closest layer.selected, xy
324                         return layer: layer, s: s if s?
325                         s = find_closest layer.all, xy
326                         return layer: layer, s: s if s?
327                 return null
328         mousedown = (e) ->
329                 hit = null
330                 closest = null
331                 layer = null
332                 mousemove e
333                 if dragging # two mousedowns in a row?! it happens
334                         return mouseup e
335                 xy = svg_event_to_xy e
336                 if xy.y < supply_height
337                         s = find_closest supply, xy
338                         if s?
339                                 hit = {
340                                         s: s.clone()
341                                         layer: widget_layer
342                                 }
343                                 widget_layer.all[hit.s.id] = hit.s
344                 else
345                         hit = closest_in_layers xy
346                 if hit?
347                         if hit.layer.selected[hit.s.id]
348                                 # already selected
349                                 # TODO start detection of a click that doesn't drag (to shrink selection)
350                         else if xy.y < supply_height
351                                 # dragging a new thing in
352                                 select_only hit.layer, hit.s
353                         else if shift_key_down
354                                 select_also hit.layer, hit.s
355                         else
356                                 select_only hit.layer, hit.s
357                         for id, s of hit.layer.selected
358                                 s.set_state STATES.DRAGGING
359                         dragging = true
360                         drag_layer = hit.layer
361                         drag_from = xy
362                         console.log hit
363                 else
364                         deselect_all widget_layer
365                 return
366         mouseup = (e) ->
367                 mousemove e
368                 if dragging
369                         selected_count = 0
370                         for id, s of drag_layer.selected
371                                 if s.y < supply_height and drag_layer is widget_layer
372                                         deselect drag_layer, s
373                                         s.destruct()
374                                         delete drag_layer.all[id]
375                                 else
376                                         selected_count += 1
377                                         s.set_state STATES.SELECTED
378                         if drag_layer is widget_layer and selected_count is 1
379                                 for id, s of drag_layer.selected
380                                         s.set_state STATES.EDITING
381                                         cs = s.make_controls done: (c) ->
382                                                 deselect controls_layer, c
383                                                 delete controls_layer.all[c.id]
384                                         for c in cs
385                                                 controls_layer.all[c.id] = c
386                                         widget_layer.editing = s
387                 dragging = false
388                 return
389         mousemove = (e) ->
390                 xy = svg_event_to_xy e
391                 if dragging
392                         return if drag_from.x is xy.x and drag_from.y is xy.y
393                         rel_x = xy.x - drag_from.x
394                         rel_y = xy.y - drag_from.y
395                         drag_from = xy
396                         for id, w of drag_layer.selected
397                                 w.drag x: rel_x, y: rel_y
398                 else
399                         hit = closest_in_layers xy
400                         if hovered and hovered isnt hit?.s
401                                 hovered.set_hover false
402                         return unless hit?
403                         hovered = hit.s
404                         hovered.set_hover true
405                 return
406         $svg.mousedown (e) ->
407                 mousedown e
408                 return false
409         $svg.mouseup (e) ->
410                 mouseup e
411                 return false
412         $svg.mousemove (e) ->
413                 mousemove e
414                 return false
415         $(document).on 'keyup keydown', (e) ->
416                 shift_key_down = e.shiftKey
417                 return true
418         #($ document).keydown (e) ->
419
420 $ init