1 # Copyright 2015 Jason Woofenden
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
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
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/>.
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
27 NORMAL: { txt: 'normal' }
28 SELECTED: { txt: 'selected' }
29 DRAGGING: { txt: 'dragging' }
30 EDITING: { txt: 'editing' }
36 # json (compiled to javascript and minified) is ~8% smaller than the resulting xml
37 json_to_svg = (json) ->
38 for tag, attrs of json
39 el = document.createElementNS 'http://www.w3.org/2000/svg', tag
43 el.appendChild json_to_svg child
44 else if k is 'contents'
45 el.appendChild document.createTextNode v
50 resizer_nw = (widget) ->
52 widget.resize w: widget.width - dxy.x, h: widget.height - dxy.y
53 widget.move x: widget.x + dxy.x, y: widget.y + dxy.y
54 resizer_n = (widget) ->
56 widget.resize w: widget.width, h: widget.height - dxy.y
57 widget.move x: widget.x, y: widget.y + dxy.y
58 resizer_ne = (widget) ->
60 widget.resize w: widget.width + dxy.x, h: widget.height - dxy.y
61 widget.move x: widget.x, y: widget.y + dxy.y
62 resizer_e = (widget) ->
64 widget.resize w: widget.width + dxy.x, h: widget.height
65 resizer_se = (widget) ->
67 widget.resize w: widget.width + dxy.x, h: widget.height + dxy.y
68 resizer_s = (widget) ->
70 widget.resize w: widget.width, h: widget.height + dxy.y
71 resizer_sw = (widget) ->
73 widget.resize w: widget.width - dxy.x, h: widget.height + dxy.y
74 widget.move x: widget.x + dxy.x, y: widget.y
75 resizer_w = (widget) ->
77 widget.resize w: widget.width - dxy.x, h: widget.height
78 widget.move x: widget.x + dxy.x, y: widget.y
90 -> return "M#{@x - 5} #{@y - 5}h6l-2 2 4 4 2 -2v6h-6l2-2-4-4-2 2z"
91 -> 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"
92 -> return "M#{@x + 5} #{@y - 5}v6l-2-2-4 4 2 2h-6v-6l2 2 4-4-2-2z"
93 -> 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"
95 shape_node_move = -> "M#{@x} #{@y - 9}l-2.5 4.5h2v2.404a2.156 2.156 0 0 0-1.596 1.596h-2.404v-2l-4.5 2.5 4.5 2.5v-2h2.404a2.156 2.156 0 0 0 1.596 1.596v2.404h-2l2.5 4.5 2.5-4.5h-2v-2.404a2.156 2.156 0 0 0 1.596-1.596h2.404v2l4.5-2.5-4.5-2.5v2h-2.404a2.156 2.156 0 0 0-1.596-1.596v-2.404h2l-2.5-4.5z"
98 # public vars: x, y, width, height, el
101 constructor: (args) ->
108 @width = args.width ? 50
109 @height = args.height ? 34
110 @state = args.state ? STATES.NORMAL
119 css_class = "#{@css_class} #{@state.txt}"
121 css_class += " hover"
122 @el.setAttribute 'class', css_class
135 if @el? and not @shown
139 move: (xy) -> # just move
143 drag: (dxy) -> # react to mouse drag (obey constraints, etc.)
144 @move x: @x + dxy.x, y: @y + dxy.y
146 proximity: (xy) -> # return the square of the distance to your visible bits
148 set_state: (state) ->
153 class Control extends Visible
154 constructor: (args) ->
159 drag: (args) -> # call this when control point is being manipulated directly
162 proximity: (xy) -> # return the square of the distance to your visible bits
165 return dx * dx + dy * dy
166 class ControlPath extends Control
167 constructor: (args) ->
169 @css_class = 'control_point'
170 @make_path = args.shape
171 @el = json_to_svg path:
173 class: 'control_point normal'
177 @el.setAttribute 'd', @make_path()
180 # A widget is a visable thing that can be edited via controls
181 class Widget extends Visible
182 constructor: (args) ->
186 @controls_shown = false
197 return unless @controls? and @controls_shown
200 @controls_shown = false
203 return if @controls? and @controls_shown
206 return unless @controls?
209 @controls_shown = true
217 c.move x: c.x + dx, y: c.y + dy
219 set_state: (state) ->
220 return if @state is state
221 if @state is STATES.EDITING
226 class RectWidget extends Widget
227 constructor: (args) ->
230 @el = json_to_svg rect:
239 return new RectWidget @
241 return [@x, @y, @width, @height]
242 from_array: (args, a) ->
247 return new RectWidget args
250 @el.setAttribute 'x', @x + 1
251 @el.setAttribute 'y', @y + 1
252 @reposition_controls()
254 proximity: (xy) -> # return the square of the distance to your visible bits
260 if x > @x - CLICK_FUZ and x < @x + @width + CLICK_FUZ
262 if y < @y + @height / 2
265 new_prox = @y + @height - y
269 if y > @y - CLICK_FUZ and y < @y + @height + CLICK_FUZ
271 if x < @x + @width / 2
274 new_prox = @x + @width - x
278 # "hit" anything inside
279 #if in_x and in_y and prox > PROX_MAX
280 # prox = PROX_MAX - 1
286 @el.setAttribute 'width', @width - 2
288 @el.setAttribute 'height', @height - 2
289 @reposition_controls()
291 reposition_controls: ->
292 if @controls? and @controls.length > 1
293 positions = @control_positions()
294 for i in [0...positions.length]
295 @controls[i].move x: positions[i].x, y: positions[i].y
297 control_positions: ->
300 w2p = Math.floor(@width / 2) + 0.5
301 h2p = Math.floor(@height / 2) + 0.5
303 { x: @x - gap, y: @y - gap }
304 { x: @x + w2p, y: @y - mgap }
305 { x: @x + @width + gap, y: @y - gap }
306 { x: @x + @width + mgap, y: @y + h2p }
307 { x: @x + @width + gap, y: @y + @height + gap }
308 { x: @x + w2p, y: @y + @height + mgap }
309 { x: @x - gap, y: @y + @height + gap }
310 { x: @x - mgap, y: @y + h2p }
314 positions = @control_positions()
315 for i in [0...positions.length]
316 @controls.push new ControlPath {
321 shape: resizer_shapes[i % resizer_shapes.length]
325 class PolylineWidget extends Widget
326 constructor: (args) ->
328 @css_class = 'polyline'
331 @nodes.push x: n.x, y: n.y
332 @el = json_to_svg path:
334 class: 'polyline normal'
342 from_array: (args, a) ->
343 # the first node is also used as the @x,@y
349 xy.x = a.shift() - args.x
350 xy.y = a.shift() - args.y
352 return new PolylineWidget args
354 return new PolylineWidget @
368 @el.setAttribute 'd', @my_path_d()
369 @reposition_controls()
371 proximity: (xy) -> # return the square of the distance to your visible bits
376 p = dx * dx + dy * dy
380 l1x = @x + @nodes[i-1].x
381 l1y = @y + @nodes[i-1].y
382 l2x = @x + @nodes[i].x
383 l2y = @y + @nodes[i].y
386 if ldx is 0 # vertical line
387 if (xy.y < l1y) is (xy.y < l2y)
391 else if ldy is 0 # horizontal line
392 if (xy.x < l1x) is (xy.x < l2x)
397 # https://en.wikipedia.org/wiki/Distance_from_a_point_to_a_line
400 c = l2x * l1y - l2y * l1x
401 y_on_line = (a * (a * xy.y - b * xy.x) - b * c) / (a * a + b * b)
402 if (y_on_line < l1y) is (y_on_line < l2y)
404 p = (a * xy.x + b * xy.y + c) / Math.sqrt(a * a + b * b)
412 #@el.setAttribute 'd', @my_path_d()
413 #@reposition_controls()
418 for j in [1...@nodes.length]
421 @move x: @x + dxy.x, y: @y + dxy.y
425 @el.setAttribute 'd', @my_path_d()
426 @reposition_controls()
427 reposition_controls: ->
428 if @controls? and @controls.length > 1
429 positions = @control_positions()
430 for i in [0...positions.length]
431 @controls[i].move x: positions[i].x, y: positions[i].y
433 control_positions: ->
436 ret.push x: @x + n.x, y: @y + n.y
440 positions = @control_positions()
441 for i in [0...positions.length]
442 @controls.push new ControlPath {
446 drag: @node_dragger i
447 shape: shape_node_move
451 CSS_CLASS_TO_PICKLE_TYPE = {
455 PICKLE_TYPE_TO_WIDGET_CLASS = [
459 # called automatically on domcontentloaded
462 $container = $ '.crayon_mockup'
463 svg = json_to_svg svg: width: width, height: height, viewBox: "0 0 #{width} #{height}"
465 $container.append $svg
466 svg.appendChild json_to_svg filter:
467 id: 'crayon', filterUnits: 'userSpaceOnUse'
468 x: '-5%', y: '-5%', height: '110%', width: '110%'
470 { feTurbulence: baseFrequency: '.3', numOctaves: '2', type: 'fractalNoise' }
471 { feDisplacementMap: scale: '6', xChannelSelector: 'R', in: 'SourceGraphic' }
473 svg.appendChild json_to_svg style:
475 contents: '.box.normal,.polyline.normal{filter: url(#crayon)}'
477 # create canvas border
478 svg.appendChild json_to_svg rect:
482 height: height - 2 - supply_height
483 class: 'canvas_border'
487 supply_add = (type, args) ->
490 args.x += 30 + supply_count * 90
491 args.y += (supply_height - 50) / 2
496 supply_add RectWidget, width: 50, height: 50
497 supply_add PolylineWidget, y: 25, nodes: [{x: 0, y: 0}, {x: 50, y: 0}]
498 supply_add PolylineWidget, x: 25, nodes: [{x: 0, y: 0}, {x: 0, y: 50}]
499 supply_add PolylineWidget, x: 10, nodes: [{x: 0, y: 0}, {x: 15, y: 50}, {x: 30, y: 0}]
500 supply_add PolylineWidget, y: 50, nodes: [{x: 0, y: 0}, {x: 17, y: -50}, {x: 33, y: 0}, {x: 50, y: -50}]
508 dragging = false # mouse state
509 drag_from = x: 0, y: 0 # mouse was here at last frame of drag
510 shift_key_down = false
512 lc = "abcdefghijklmnopqrstuvwxyz"
513 uc = "ABCDEFGHIJKLMNOPQRSTUVWXYZ"
514 cc_lca = lc.charCodeAt 0
515 cc_lcz = lc.charCodeAt 25
516 cc_uca = uc.charCodeAt 0
517 cc_ucz = uc.charCodeAt 25
518 cc_0 = '0'.charCodeAt 0
519 cc_9 = '9'.charCodeAt 0
520 # takes an array of positive integers, and encodes it as a string
526 while i > 0 or r is ''
527 digit = i % cs.length
530 r = cs.charAt(digit) + r
535 ret = '0' # version of this encoding scheme
537 if CSS_CLASS_TO_PICKLE_TYPE[w.css_class]?
538 ret += CSS_CLASS_TO_PICKLE_TYPE[w.css_class]
539 ret += pickle w.as_array()
542 window.location.hash = pickle_widgets()
544 return if str.charAt(1) isnt '0'
548 load_1 = (next_type) ->
553 w = PICKLE_TYPE_TO_WIDGET_CLASS[wtype]::from_array svg: svg, args
558 for i in [2...str.length]
559 c = str.charCodeAt(i)
562 else if cc_lca <= c <= cc_lcz
567 else if cc_uca <= c <= cc_ucz
577 return unless selected[w.id]?
578 w.set_state STATES.NORMAL
579 delete selected[w.id]
583 deselect_all = (except = null) ->
584 for id, w of selected
588 _select = (w) -> # don't call this directly, use select_only() or select_also()
589 w.set_state STATES.SELECTED
594 return if selected[w.id]?
598 return if selected[w.id]?
602 find_closest = (widgets, xy) ->
606 new_prox = w.proximity xy
613 find_closest_control = (xy) ->
616 for id, s of selected
620 new_prox = c.proximity xy
627 find_closest_thing = (xy) ->
628 if xy.y < supply_height
629 return find_closest supply, xy
631 hit = find_closest_control xy
632 hit ?= find_closest widgets, xy
634 svg_event_to_xy = (e) ->
636 svg_offset = $svg.offset()
638 x: Math.round(e.pageX - svg_offset.left)
639 y: Math.round(e.pageY - svg_offset.top)
645 if dragging # two mousedowns in a row?! it happens
647 xy = svg_event_to_xy e
648 if xy.y < supply_height
649 hit = find_closest supply, xy
652 widgets[hit.id] = hit
654 hit = find_closest_control xy
656 hit = find_closest widgets, xy
658 if hit.type is TYPE_WIDGET
661 # TODO start detection of a click that doesn't drag (to shrink selection)
662 else if xy.y < supply_height
663 # dragging a new thing in
665 else if shift_key_down
669 for id, w of selected
670 w.set_state STATES.DRAGGING
671 drag_targets = selected
674 drag_targets[hit.id] = hit
684 widgets_remaining = 0
685 for id, s of drag_targets
686 if s.type is TYPE_WIDGET
687 if s.y < supply_height
692 widgets_remaining += 1
693 s.set_state STATES.SELECTED
694 if widgets_remaining is 1
695 for id, s of drag_targets
696 if s.type is TYPE_WIDGET
697 s.set_state STATES.EDITING
702 xy = svg_event_to_xy e
704 return if drag_from.x is xy.x and drag_from.y is xy.y
705 rel_x = xy.x - drag_from.x
706 rel_y = xy.y - drag_from.y
708 for id, w of drag_targets
709 w.drag x: rel_x, y: rel_y
711 hit = find_closest_thing xy
712 return if hit is hovered # both null, or both the same
713 return if hit?.type is TYPE_CONTROL # hovering a control doesn't change display
715 hovered.set_hover false
720 $svg.mousedown (e) ->
726 $svg.mousemove (e) ->
729 $(document).on 'keyup keydown', (e) ->
730 shift_key_down = e.shiftKey
732 #($ document).keydown (e) ->
734 if window.location.hash
735 load window.location.hash