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' }
34 CSS_CLASS_TO_PICKLE_TYPE = {
38 # json (compiled to javascript and minified) is ~8% smaller than the resulting xml
39 json_to_svg = (json) ->
40 for tag, attrs of json
41 el = document.createElementNS 'http://www.w3.org/2000/svg', tag
45 el.appendChild json_to_svg child
46 else if k is 'contents'
47 el.appendChild document.createTextNode v
52 resizer_nw = (widget) ->
54 widget.resize w: widget.width - dxy.x, h: widget.height - dxy.y
55 widget.move x: widget.x + dxy.x, y: widget.y + dxy.y
56 resizer_n = (widget) ->
58 widget.resize w: widget.width, h: widget.height - dxy.y
59 widget.move x: widget.x, y: widget.y + dxy.y
60 resizer_ne = (widget) ->
62 widget.resize w: widget.width + dxy.x, h: widget.height - dxy.y
63 widget.move x: widget.x, y: widget.y + dxy.y
64 resizer_e = (widget) ->
66 widget.resize w: widget.width + dxy.x, h: widget.height
67 resizer_se = (widget) ->
69 widget.resize w: widget.width + dxy.x, h: widget.height + dxy.y
70 resizer_s = (widget) ->
72 widget.resize w: widget.width, h: widget.height + dxy.y
73 resizer_sw = (widget) ->
75 widget.resize w: widget.width - dxy.x, h: widget.height + dxy.y
76 widget.move x: widget.x + dxy.x, y: widget.y
77 resizer_w = (widget) ->
79 widget.resize w: widget.width - dxy.x, h: widget.height
80 widget.move x: widget.x + dxy.x, y: widget.y
92 -> return "M#{@x - 5} #{@y - 5}h6l-2 2 4 4 2 -2v6h-6l2-2-4-4-2 2z"
93 -> 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"
94 -> return "M#{@x + 5} #{@y - 5}v6l-2-2-4 4 2 2h-6v-6l2 2 4-4-2-2z"
95 -> 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"
97 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"
100 # public vars: x, y, width, height, el
103 constructor: (args) ->
109 @width = args.width ? 50
110 @height = args.height ? 34
111 @state = args.state ? STATES.NORMAL
115 css_class = "#{@css_class} #{@state.txt}"
117 css_class += " hover"
118 @el.setAttribute 'class', css_class
123 move: (xy) -> # just move
126 drag: (dxy) -> # react to mouse drag (obey constraints, etc.)
127 @move x: @x + dxy.x, y: @y + dxy.y
128 proximity: (xy) -> # return the square of the distance to your visible bits
130 set_state: (state) ->
133 class Control extends Visible
134 constructor: (args) ->
138 @on_destruct = args.done ? null
143 drag: (args) -> # call this when control point is being manipulated directly
145 proximity: (xy) -> # return the square of the distance to your visible bits
148 return dx * dx + dy * dy
149 class ControlPath extends Control
150 constructor: (args) ->
152 @css_class = 'control_point'
153 @make_path = args.shape
154 @el = json_to_svg path:
156 class: 'control_point normal'
164 @el.setAttribute 'd', @make_path()
166 class Widget extends Visible
167 #sub-classes are expected to implement all of these:
168 constructor: (args) ->
176 make_controls: -> # create controls, return them
183 move: (xy) -> # just move
188 c.move x: c.x + dx, y: c.y + dy
189 set_state: (state) ->
190 return if @state is state
191 if @state is STATES.EDITING
195 class RectWidget extends Widget
196 constructor: (args) ->
199 @el = json_to_svg rect:
211 return new RectWidget @
213 return [@x, @y, @width, @height]
215 return new RectWidget x: a[0], y: a[1], width: a[2], height: a[3]
216 set_state: (state) ->
221 @el.setAttribute 'x', @x + 1
222 @el.setAttribute 'y', @y + 1
223 @reposition_controls()
224 proximity: (xy) -> # return the square of the distance to your visible bits
230 if x > @x - CLICK_FUZ and x < @x + @width + CLICK_FUZ
232 if y < @y + @height / 2
235 new_prox = @y + @height - y
239 if y > @y - CLICK_FUZ and y < @y + @height + CLICK_FUZ
241 if x < @x + @width / 2
244 new_prox = @x + @width - x
248 # "hit" anything inside
249 #if in_x and in_y and prox > PROX_MAX
250 # prox = PROX_MAX - 1
256 @el.setAttribute 'width', @width - 2
258 @el.setAttribute 'height', @height - 2
259 @reposition_controls()
260 reposition_controls: ->
261 if @controls.length > 1
262 positions = @control_positions()
263 for i in [0...positions.length]
264 @controls[i].move x: positions[i].x, y: positions[i].y
265 control_positions: ->
268 w2p = Math.floor(@width / 2) + 0.5
269 h2p = Math.floor(@height / 2) + 0.5
271 { x: @x - gap, y: @y - gap }
272 { x: @x + w2p, y: @y - mgap }
273 { x: @x + @width + gap, y: @y - gap }
274 { x: @x + @width + mgap, y: @y + h2p }
275 { x: @x + @width + gap, y: @y + @height + gap }
276 { x: @x + w2p, y: @y + @height + mgap }
277 { x: @x - gap, y: @y + @height + gap }
278 { x: @x - mgap, y: @y + h2p }
280 make_controls: (args) -> # create controls, return them
281 if @controls.length > 0
283 console.log "warning: re-adding controls"
285 positions = @control_positions()
286 for i in [0...positions.length]
287 @controls.push new ControlPath {
293 shape: resizer_shapes[i % resizer_shapes.length]
297 class PolylineWidget extends Widget
298 constructor: (args) ->
300 @css_class = 'polyline'
303 @nodes.push x: n.x, y: n.y
304 @el = json_to_svg path:
306 class: 'polyline normal'
325 args.nodes.push x: a.shift() - args.x, y: a.shift() - args.x
326 return new PolylineWidget args
328 return new PolylineWidget @
340 set_state: (state) ->
345 @el.setAttribute 'd', @my_path_d()
346 @reposition_controls()
347 proximity: (xy) -> # return the square of the distance to your visible bits
352 p = dx * dx + dy * dy
356 l1x = @x + @nodes[i-1].x
357 l1y = @y + @nodes[i-1].y
358 l2x = @x + @nodes[i].x
359 l2y = @y + @nodes[i].y
362 if ldx is 0 # vertical line
363 if (xy.y < l1y) is (xy.y < l2y)
367 else if ldy is 0 # horizontal line
368 if (xy.x < l1x) is (xy.x < l2x)
373 # https://en.wikipedia.org/wiki/Distance_from_a_point_to_a_line
376 c = l2x * l1y - l2y * l1x
377 y_on_line = (a * (a * xy.y - b * xy.x) - b * c) / (a * a + b * b)
378 if (y_on_line < l1y) is (y_on_line < l2y)
380 p = (a * xy.x + b * xy.y + c) / Math.sqrt(a * a + b * b)
387 # FIXME (apply to more than just 2nd node)
392 @el.setAttribute 'd', @my_path_d()
393 @reposition_controls()
398 for i in [1...@nodes.length]
401 @move x: @x + dxy.x, y: @y + dxy.y
405 @el.setAttribute 'd', @my_path_d()
406 @reposition_controls()
407 reposition_controls: ->
408 if @controls.length > 1
409 positions = @control_positions()
410 for i in [0...positions.length]
411 @controls[i].move x: positions[i].x, y: positions[i].y
413 control_positions: ->
416 ret.push x: @x + n.x, y: @y + n.y
418 make_controls: (args) -> # create controls, return them
419 if @controls.length > 0
421 console.log "warning: re-adding line controls"
423 positions = @control_positions()
424 for i in [0...positions.length]
425 @controls.push new ControlPath {
430 drag: @node_dragger i
431 shape: shape_node_move
435 # called automatically on domcontentloaded
438 $container = $ '.crayon_mockup'
439 svg = json_to_svg svg: width: width, height: height, viewBox: "0 0 #{width} #{height}"
441 $container.append $svg
442 svg.appendChild json_to_svg filter:
443 id: 'crayon', filterUnits: 'userSpaceOnUse'
444 x: '-5%', y: '-5%', height: '110%', width: '110%'
446 { feTurbulence: baseFrequency: '.3', numOctaves: '2', type: 'fractalNoise' }
447 { feDisplacementMap: scale: '6', xChannelSelector: 'R', in: 'SourceGraphic' }
449 svg.appendChild json_to_svg style:
451 contents: '.box.normal,.polyline.normal{filter: url(#crayon)}'
453 # create canvas border
454 svg.appendChild json_to_svg rect:
458 height: height - 2 - supply_height
459 class: 'canvas_border'
463 supply_add = (type, args) ->
466 args.x += 30 + supply_count * 90
467 args.y += (supply_height - 50) / 2
472 supply_add RectWidget, width: 50, height: 50
473 supply_add PolylineWidget, y: 25, nodes: [{x: 0, y: 0}, {x: 50, y: 0}]
474 supply_add PolylineWidget, x: 25, nodes: [{x: 0, y: 0}, {x: 0, y: 50}]
475 supply_add PolylineWidget, x: 10, nodes: [{x: 0, y: 0}, {x: 15, y: 50}, {x: 30, y: 0}]
476 supply_add PolylineWidget, nodes: [{x: 0, y: 50}, {x: 17, y: 0}, {x: 33, y: 50}, {x: 50, y: 0}]
479 controls_layer = { all: {}, selected: {} }
480 widget_layer = { all: {}, selected: {}, editing: null }
481 layers = [controls_layer, widget_layer]
482 hovered = null # can be in any layer
483 dragging = false # mouse state
485 drag_from = x: 0, y: 0 # mouse was here at last frame of drag
486 shift_key_down = false
488 lc = "abcdefghijklmnopqrstuvwxyz"
489 uc = "ABCDEFGHIJKLMNOPQRSTUVWXYZ"
490 # takes an array of positive integers, and encodes it as a string
496 while i > 0 or r is ''
497 digit = i % cs.length
500 r = cs.charAt(digit) + r
505 ret = '0' # version of this encoding scheme
506 for id, w of widget_layer.all
507 if CSS_CLASS_TO_PICKLE_TYPE[w.css_class]?
508 ret += CSS_CLASS_TO_PICKLE_TYPE[w.css_class]
509 ret += pickle w.as_array()
512 window.location.hash = pickle_widgets()
514 if widget_layer.editing
515 widget_layer.editing.kill_controls()
516 widget_layer.editing = null
517 deselect = (layer, s) ->
518 return unless layer.selected[s.id]?
519 s.set_state STATES.NORMAL
520 delete layer.selected[s.id]
521 if widget_layer.editing is s
522 widget_layer.editing = null
524 deselect_all = (layer, except = null) ->
525 for id, s of layer.selected
528 _select = (layer, s) -> # don't call this directly, use select_only() or select_also()
529 s.set_state STATES.SELECTED
530 layer.selected[s.id] = s
532 select_only = (layer, s) ->
533 deselect_all layer, s
534 return if layer.selected[s.id]?
537 select_also = (layer, s) ->
538 return if layer.selected[s.id]?
539 if layer is widget_layer
543 find_closest = (widgets, xy) ->
547 new_prox = w.proximity xy
554 svg_event_to_xy = (e) ->
556 svg_offset = $svg.offset()
558 x: Math.round(e.pageX - svg_offset.left)
559 y: Math.round(e.pageY - svg_offset.top)
561 closest_in_layers = (xy) ->
563 s = find_closest layer.selected, xy
564 return layer: layer, s: s if s?
565 s = find_closest layer.all, xy
566 return layer: layer, s: s if s?
573 if dragging # two mousedowns in a row?! it happens
575 xy = svg_event_to_xy e
576 if xy.y < supply_height
577 s = find_closest supply, xy
583 widget_layer.all[hit.s.id] = hit.s
585 hit = closest_in_layers xy
587 if hit.layer.selected[hit.s.id]
589 # TODO start detection of a click that doesn't drag (to shrink selection)
590 else if xy.y < supply_height
591 # dragging a new thing in
592 select_only hit.layer, hit.s
593 else if shift_key_down
594 select_also hit.layer, hit.s
596 select_only hit.layer, hit.s
597 for id, s of hit.layer.selected
598 s.set_state STATES.DRAGGING
600 drag_layer = hit.layer
603 deselect_all widget_layer
610 for id, s of drag_layer.selected
611 if s.y < supply_height and drag_layer is widget_layer
612 deselect drag_layer, s
614 delete drag_layer.all[id]
617 s.set_state STATES.SELECTED
618 if drag_layer is widget_layer and selected_count is 1
619 for id, s of drag_layer.selected
620 s.set_state STATES.EDITING
621 cs = s.make_controls done: (c) ->
622 deselect controls_layer, c
623 delete controls_layer.all[c.id]
625 controls_layer.all[c.id] = c
626 widget_layer.editing = s
630 xy = svg_event_to_xy e
632 return if drag_from.x is xy.x and drag_from.y is xy.y
633 rel_x = xy.x - drag_from.x
634 rel_y = xy.y - drag_from.y
636 for id, w of drag_layer.selected
637 w.drag x: rel_x, y: rel_y
639 hit = closest_in_layers xy
640 if hovered and hovered isnt hit?.s
641 hovered.set_hover false
644 hovered.set_hover true
646 $svg.mousedown (e) ->
652 $svg.mousemove (e) ->
655 $(document).on 'keyup keydown', (e) ->
656 shift_key_down = e.shiftKey
658 #($ document).keydown (e) ->