JasonWoof Got questions, comments, patches, etc.? Contact Jason Woofenden
experimental pickling to url hash
[crayon_mockup.git] / main.coffee
index de2e86b..d8f75b5 100644 (file)
@@ -31,7 +31,10 @@ STATES = {
 }
 TYPE_WIDGET = 1
 TYPE_CONTROL = 2
-
+CSS_CLASS_TO_PICKLE_TYPE = {
+       box: '0'
+       polyline: '1'
+}
 # json (compiled to javascript and minified) is ~8% smaller than the resulting xml
 json_to_svg = (json) ->
        for tag, attrs of json
@@ -86,11 +89,12 @@ resizers = [
        resizer_w
 ]
 resizer_shapes = [
-       (xy) -> return "M#{@x - 5} #{@y - 5}h6l-2 2 4 4 2 -2v6h-6l2-2-4-4-2 2z"
-       (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"
-       (xy) -> return "M#{@x + 5} #{@y - 5}v6l-2-2-4 4 2 2h-6v-6l2 2 4-4-2-2z"
-       (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"
+       -> return "M#{@x - 5} #{@y - 5}h6l-2 2 4 4 2 -2v6h-6l2-2-4-4-2 2z"
+       -> 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"
+       -> return "M#{@x + 5} #{@y - 5}v6l-2-2-4 4 2 2h-6v-6l2 2 4-4-2-2z"
+       -> 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"
 ]
+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"
 
 next_widget_id = 0
 # public vars: x, y, width, height, el
@@ -205,6 +209,10 @@ class RectWidget extends Widget
                        @svg.removeChild @el
        clone: ->
                return new RectWidget @
+       as_array: ->
+               return [@x, @y, @width, @height]
+       from_array: (a) ->
+               return new RectWidget x: a[0], y: a[1], width: a[2], height: a[3]
        set_state: (state) ->
                super state
                @update_class()
@@ -237,8 +245,9 @@ class RectWidget extends Widget
                        new_prox *= new_prox
                        if new_prox < prox
                                prox = new_prox
-               if in_x and in_y and prox > PROX_MAX
-                       prox = PROX_MAX - 1
+               # "hit" anything inside
+               #if in_x and in_y and prox > PROX_MAX
+               #       prox = PROX_MAX - 1
                return prox
        resize: (wh) ->
                dw = wh.w - @width
@@ -285,6 +294,144 @@ class RectWidget extends Widget
                        }
                return @controls
 
+class PolylineWidget extends Widget
+       constructor: (args) ->
+               super args
+               @css_class = 'polyline'
+               @nodes = []
+               for n in args.nodes
+                       @nodes.push x: n.x, y: n.y
+               @el = json_to_svg path:
+                       d: @my_path_d()
+                       class: 'polyline normal'
+               @svg.appendChild @el
+       destruct: ->
+               super()
+               if @el?
+                       @svg.removeChild @el
+       as_array: ->
+               ret = []
+               for n in @nodes
+                       ret.push @x + n.x
+                       ret.push @y + n.y
+               return ret
+       from_array: (a) ->
+               args = {
+                       x: a[0]
+                       y: a[1]
+                       nodes: []
+               }
+               while a.length
+                       args.nodes.push x: a.shift() - args.x, y: a.shift() - args.x
+               return new PolylineWidget args
+       clone: ->
+               return new PolylineWidget @
+       my_path_d: ->
+               ret = ''
+               for n in @nodes
+                       if ret is ''
+                               ret += 'M'
+                       else
+                               ret += 'L'
+                       ret += n.x + @x
+                       ret += ' '
+                       ret += n.y + @y
+               return ret
+       set_state: (state) ->
+               super state
+               @update_class()
+       move: (args) ->
+               super args
+               @el.setAttribute 'd', @my_path_d()
+               @reposition_controls()
+       proximity: (xy) -> # return the square of the distance to your visible bits
+               prox = PROX_TOO_FAR
+               for n, i in @nodes
+                       dx = @x + n.x - xy.x
+                       dy = @y + n.y - xy.y
+                       p = dx * dx + dy * dy
+                       if p < prox
+                               prox = p
+                       if i > 0
+                               l1x = @x + @nodes[i-1].x
+                               l1y = @y + @nodes[i-1].y
+                               l2x = @x + @nodes[i].x
+                               l2y = @y + @nodes[i].y
+                               ldx = l2x - l1x
+                               ldy = l2y - l1y
+                               if ldx is 0 # vertical line
+                                       if (xy.y < l1y) is (xy.y < l2y)
+                                               continue
+                                       dx = l1x - xy.x
+                                       p = dx * dx
+                               else if ldy is 0 # horizontal line
+                                       if (xy.x < l1x) is (xy.x < l2x)
+                                               continue
+                                       dy = l1y - xy.y
+                                       p = dy * dy
+                               else # slanty line
+                                       # https://en.wikipedia.org/wiki/Distance_from_a_point_to_a_line
+                                       a = ldy
+                                       b = -1 * ldx
+                                       c = l2x * l1y - l2y * l1x
+                                       y_on_line = (a * (a * xy.y - b * xy.x) - b * c) / (a * a + b * b)
+                                       if (y_on_line < l1y) is (y_on_line < l2y)
+                                               continue
+                                       p = (a * xy.x + b * xy.y + c) / Math.sqrt(a * a + b * b)
+                                       p *= p
+
+                               if p < prox
+                                       prox = p
+               return prox
+       resize: (wh) ->
+               # FIXME (apply to more than just 2nd node)
+               @nodes[1].x = wh.w
+               @nodes[1].y = wh.h
+               @width = wh.w
+               @height = wh.h
+               @el.setAttribute 'd', @my_path_d()
+               @reposition_controls()
+               return
+       node_dragger: (i) ->
+               if i is 0
+                       return (dxy) =>
+                               for i in [1...@nodes.length]
+                                       @nodes[i].x -= dxy.x
+                                       @nodes[i].y -= dxy.y
+                               @move x: @x + dxy.x, y: @y + dxy.y
+               return (dxy) =>
+                       @nodes[i].x += dxy.x
+                       @nodes[i].y += dxy.y
+                       @el.setAttribute 'd', @my_path_d()
+                       @reposition_controls()
+       reposition_controls: ->
+               if @controls.length > 1
+                       positions = @control_positions()
+                       for i in [0...positions.length]
+                               @controls[i].move x: positions[i].x, y: positions[i].y
+               return
+       control_positions: ->
+               ret = []
+               for n in @nodes
+                       ret.push x: @x + n.x, y: @y + n.y
+               return ret
+       make_controls: (args) -> # create controls, return them
+               if @controls.length > 0
+                       if console?.log?
+                               console.log "warning: re-adding line controls"
+                       @kill_controls()
+               positions = @control_positions()
+               for i in [0...positions.length]
+                       @controls.push new ControlPath {
+                               svg: @svg
+                               x: positions[i].x
+                               y: positions[i].y
+                               done: args.done
+                               drag: @node_dragger i
+                               shape: shape_node_move
+                       }
+               return @controls
+
 # called automatically on domcontentloaded
 init = ->
        svg_offset = null
@@ -301,7 +448,7 @@ init = ->
                ]
        svg.appendChild json_to_svg style:
                type: 'text/css'
-               contents: '.box.normal{filter: url(#crayon)}'
+               contents: '.box.normal,.polyline.normal{filter: url(#crayon)}'
 
        # create canvas border
        svg.appendChild json_to_svg rect:
@@ -312,19 +459,21 @@ init = ->
                class: 'canvas_border'
 
        supply = {}
-       for args, i in [
-               { width: 40, height: 40 }
-               { width: 12, height: 50 }
-               { width: 70, height: 12 }
-       ]
-               widget = new RectWidget {
-                       width: args.width
-                       height: args.height
-                       x: 30 + i * 90 + (70 - args.width) / 2
-                       y: (supply_height - args.height) / 2
-                       svg: svg
-               }
-               supply[widget.id] = widget
+       supply_count = 0
+       supply_add = (type, args) ->
+               args.x ?= 0
+               args.y ?= 0
+               args.x += 30 + supply_count * 90
+               args.y += (supply_height - 50) / 2
+               args.svg = svg
+               w = new type args
+               supply[w.id] = w
+               supply_count += 1
+       supply_add RectWidget, width: 50, height: 50
+       supply_add PolylineWidget, y: 25, nodes: [{x: 0, y: 0}, {x: 50, y: 0}]
+       supply_add PolylineWidget, x: 25, nodes: [{x: 0, y: 0}, {x: 0, y: 50}]
+       supply_add PolylineWidget, x: 10, nodes: [{x: 0, y: 0}, {x: 15, y: 50}, {x: 30, y: 0}]
+       supply_add PolylineWidget, nodes: [{x: 0, y: 50}, {x: 17, y: 0}, {x: 33, y: 50}, {x: 50, y: 0}]
 
        # editor state
        controls_layer = { all: {}, selected: {} }
@@ -336,6 +485,31 @@ init = ->
        drag_from = x: 0, y: 0 # mouse was here at last frame of drag
        shift_key_down = false
 
+       lc = "abcdefghijklmnopqrstuvwxyz"
+       uc = "ABCDEFGHIJKLMNOPQRSTUVWXYZ"
+       # takes an array of positive integers, and encodes it as a string
+       pickle = (a) ->
+               ret = ''
+               for i in a
+                       cs = lc
+                       r = ''
+                       while i > 0 or r is ''
+                               digit = i % cs.length
+                               i -= digit
+                               i /= cs.length
+                               r = cs.charAt(digit) + r
+                               cs = uc
+                       ret += r
+               return ret
+       pickle_widgets = ->
+               ret = '0' # version of this encoding scheme
+               for id, w of widget_layer.all
+                       if CSS_CLASS_TO_PICKLE_TYPE[w.css_class]?
+                               ret += CSS_CLASS_TO_PICKLE_TYPE[w.css_class]
+                               ret += pickle w.as_array()
+               return ret
+       save = ->
+               window.location.hash = pickle_widgets()
        stop_editing = ->
                if widget_layer.editing
                        widget_layer.editing.kill_controls()
@@ -429,6 +603,7 @@ init = ->
                        deselect_all widget_layer
                return
        mouseup = (e) ->
+               save()
                mousemove e
                if dragging
                        selected_count = 0