X-Git-Url: https://jasonwoof.com/gitweb/?p=crayon_mockup.git;a=blobdiff_plain;f=main.coffee;h=0a60f329096c943477f89a689b7f10887781e6a8;hp=906228dcaf85c5c34bd0168496fd1a053394f827;hb=ecf9cebe3e8d5274c7247b5e2baf3527bd2c026a;hpb=8b0b3a723c187dd292219a82c4a237018b187f0e diff --git a/main.coffee b/main.coffee index 906228d..0a60f32 100644 --- a/main.coffee +++ b/main.coffee @@ -23,24 +23,15 @@ PROX_MAX = CLICK_FUZ * CLICK_FUZ PROX_TOO_FAR = PROX_MAX + 1 # no need to be precice when it's too far # constants -STYLE_NORMAL = 0 -STYLE_SELECTED = 1 -STYLE_HOVER = 2 -STYLE_EDITING = 3 -STYLE_DRAGGING = 4 -STYLE_TO_CLASS = [ - "normal" - "selected" - "hover" - "editing" - "dragging" -] +STATES = { + NORMAL: { txt: 'normal' } + SELECTED: { txt: 'selected' } + DRAGGING: { txt: 'dragging' } + EDITING: { txt: 'editing' } +} TYPE_WIDGET = 1 TYPE_CONTROL = 2 -set_style_class = (args) -> - args.el.setAttribute 'class', "#{args.class} #{STYLE_TO_CLASS[args.style]}" - # json (compiled to javascript and minified) is ~8% smaller than the resulting xml json_to_svg = (json) -> for tag, attrs of json @@ -55,6 +46,53 @@ json_to_svg = (json) -> el.setAttribute k, v return el +resizer_nw = (widget) -> + return (dxy) -> + widget.resize w: widget.width - dxy.x, h: widget.height - dxy.y + widget.move x: widget.x + dxy.x, y: widget.y + dxy.y +resizer_n = (widget) -> + return (dxy) -> + widget.resize w: widget.width, h: widget.height - dxy.y + widget.move x: widget.x, y: widget.y + dxy.y +resizer_ne = (widget) -> + return (dxy) -> + widget.resize w: widget.width + dxy.x, h: widget.height - dxy.y + widget.move x: widget.x, y: widget.y + dxy.y +resizer_e = (widget) -> + return (dxy) -> + widget.resize w: widget.width + dxy.x, h: widget.height +resizer_se = (widget) -> + return (dxy) -> + widget.resize w: widget.width + dxy.x, h: widget.height + dxy.y +resizer_s = (widget) -> + return (dxy) -> + widget.resize w: widget.width, h: widget.height + dxy.y +resizer_sw = (widget) -> + return (dxy) -> + widget.resize w: widget.width - dxy.x, h: widget.height + dxy.y + widget.move x: widget.x + dxy.x, y: widget.y +resizer_w = (widget) -> + return (dxy) -> + widget.resize w: widget.width - dxy.x, h: widget.height + widget.move x: widget.x + dxy.x, y: widget.y +resizers = [ + resizer_nw + resizer_n + resizer_ne + resizer_e + resizer_se + resizer_s + resizer_sw + resizer_w +] +resizer_shapes = [ + -> 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 class Visible @@ -67,17 +105,27 @@ class Visible @y = args.y ? 1 @width = args.width ? 50 @height = args.height ? 34 - @style = args.style ? STYLE_NORMAL + @state = args.state ? STATES.NORMAL + @hover = false destruct: -> + update_class: -> + css_class = "#{@css_class} #{@state.txt}" + if @hover + css_class += " hover" + @el.setAttribute 'class', css_class + set_hover: (tf) -> + if tf != @hover + @hover = tf + @update_class() move: (xy) -> # just move @x = xy.x @y = xy.y - drag: (xy) -> # react to mouse drag (obey constraints, etc.) - @move xy + drag: (dxy) -> # react to mouse drag (obey constraints, etc.) + @move x: @x + dxy.x, y: @y + dxy.y proximity: (xy) -> # return the square of the distance to your visible bits return PROX_TOO_FAR - set_style: (style) -> - @style = style + set_state: (state) -> + @state = state class Control extends Visible constructor: (args) -> @@ -95,14 +143,13 @@ class Control extends Visible dx = xy.x - @x dy = xy.y - @y return dx * dx + dy * dy - -class ControlPoint extends Control +class ControlPath extends Control constructor: (args) -> super args - @el = json_to_svg circle: - cx: @x + 1 - cy: @y + 1 - r: 6 + @css_class = 'control_point' + @make_path = args.shape + @el = json_to_svg path: + d: @make_path() class: 'control_point normal' @svg.appendChild @el destruct: -> @@ -111,8 +158,7 @@ class ControlPoint extends Control @svg.removeChild @el move: (args) -> super args - @el.setAttribute 'cx', @x - @el.setAttribute 'cy', @y + @el.setAttribute 'd', @make_path() class Widget extends Visible #sub-classes are expected to implement all of these: @@ -127,7 +173,6 @@ class Widget extends Visible make_controls: -> # create controls, return them return [] kill_controls: -> - console.log 'kill_controls' for c in @controls c.destruct() @controls = [] @@ -138,6 +183,11 @@ class Widget extends Visible super xy for c in @controls c.move x: c.x + dx, y: c.y + dy + set_state: (state) -> + return if @state is state + if @state is STATES.EDITING + @kill_controls() + super state class RectWidget extends Widget constructor: (args) -> @@ -156,13 +206,14 @@ class RectWidget extends Widget @svg.removeChild @el clone: -> return new RectWidget @ - set_style: (style) -> - super style - set_style_class el: @el, class: 'box', style: style + set_state: (state) -> + super state + @update_class() move: (args) -> super args @el.setAttribute 'x', @x + 1 @el.setAttribute 'y', @y + 1 + @reposition_controls() proximity: (xy) -> # return the square of the distance to your visible bits x = xy.x y = xy.y @@ -191,29 +242,172 @@ class RectWidget extends Widget prox = PROX_MAX - 1 return prox resize: (wh) -> + dw = wh.w - @width + dh = wh.h - @height @width = wh.w - @el.setAttribute 'width', @width + @el.setAttribute 'width', @width - 2 @height = wh.h - @el.setAttribute 'height', @height + @el.setAttribute 'height', @height - 2 + @reposition_controls() + reposition_controls: -> if @controls.length > 1 - @controls[1].move x: @x + @width, y: @y + @height + positions = @control_positions() + for i in [0...positions.length] + @controls[i].move x: positions[i].x, y: positions[i].y + control_positions: -> + gap = 7 + mgap = 9 + w2p = Math.floor(@width / 2) + 0.5 + h2p = Math.floor(@height / 2) + 0.5 + return [ + { x: @x - gap, y: @y - gap } + { x: @x + w2p, y: @y - mgap } + { x: @x + @width + gap, y: @y - gap } + { x: @x + @width + mgap, y: @y + h2p } + { x: @x + @width + gap, y: @y + @height + gap } + { x: @x + w2p, y: @y + @height + mgap } + { x: @x - gap, y: @y + @height + gap } + { x: @x - mgap, y: @y + h2p } + ] make_controls: (args) -> # create controls, return them - console.log 'make_controls' if @controls.length > 0 - console.log "warning: re-adding controls" + if console?.log? + console.log "warning: re-adding controls" @kill_controls() - w = @ - @controls = [ - new ControlPoint svg: @svg, x: @x, y: @y, done: args.done, drag: (xy) -> - dx = xy.x - @x - dy = xy.y - @y - w.resize w: w.width - dx, h: w.height - dy - w.move x: w.x + dx, y: w.y + dy - new ControlPoint svg: @svg, x: @x + @width, y: @y + @height, done: args.done, drag: (xy) -> - dx = xy.x - @x - dy = xy.y - @y - w.resize w: w.width + dx, h: w.height + dy - ] + 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: resizers[i] @ + shape: resizer_shapes[i % resizer_shapes.length] + } + 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 + 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 + console.log 'make line controls' + if @controls.length > 0 + if console?.log? + console.log "warning: re-adding 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 @@ -232,7 +426,7 @@ init = -> ] svg.appendChild json_to_svg style: type: 'text/css' - contents: '.box.normal,.box.hover,.box.selected{filter: url(#crayon)}' + contents: '.box.normal,.polyline.normal{filter: url(#crayon)}' # create canvas border svg.appendChild json_to_svg rect: @@ -243,54 +437,61 @@ 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, x: 0, nodes: [{x: 0, y: 50}, {x: 17, y: 0}, {x: 33, y: 50}, {x: 50, y: 0}] # editor state - on_canvas = {} - selected = {} - controls = {} - editing = {} # has controls - dragging = false + controls_layer = { all: {}, selected: {} } + widget_layer = { all: {}, selected: {}, editing: null } + layers = [controls_layer, widget_layer] + hovered = null # can be in any layer + dragging = false # mouse state + drag_layer = null drag_from = x: 0, y: 0 # mouse was here at last frame of drag shift_key_down = false - selected_type = -> - for s of selected - return s.type - return null - deselect = (s) -> - s.set_style STYLE_NORMAL - if s.type is TYPE_WIDGET - s.kill_controls() - delete selected[s.id] + stop_editing = -> + if widget_layer.editing + widget_layer.editing.kill_controls() + widget_layer.editing = null + deselect = (layer, s) -> + return unless layer.selected[s.id]? + s.set_state STATES.NORMAL + delete layer.selected[s.id] + if widget_layer.editing is s + widget_layer.editing = null return - deselect_all = (args) -> - except = args?.except ? null - for id, s of selected - deselect s + deselect_all = (layer, except = null) -> + for id, s of layer.selected + deselect layer, s return - select_only = (sel) -> - deselect_all except: sel - return if selected[sel.id]? - selected[sel.id] = sel - select_also = (sel) -> - return if selected[sel.id]? - sel_type = selected_type() - if sel_type isnt sel.type - deselect_all() - selected[sel.id] = sel + _select = (layer, s) -> # don't call this directly, use select_only() or select_also() + s.set_state STATES.SELECTED + layer.selected[s.id] = s + return + select_only = (layer, s) -> + deselect_all layer, s + return if layer.selected[s.id]? + _select layer, s + return + select_also = (layer, s) -> + return if layer.selected[s.id]? + if layer is widget_layer + stop_editing() + _select layer, s return find_closest = (widgets, xy) -> prox = PROX_TOO_FAR @@ -310,60 +511,73 @@ init = -> x: Math.round(e.pageX - svg_offset.left) y: Math.round(e.pageY - svg_offset.top) } + closest_in_layers = (xy) -> + for layer in layers + s = find_closest layer.selected, xy + return layer: layer, s: s if s? + s = find_closest layer.all, xy + return layer: layer, s: s if s? + return null mousedown = (e) -> + hit = null + closest = null + layer = null mousemove e if dragging # two mousedowns in a row?! it happens return mouseup e xy = svg_event_to_xy e if xy.y < supply_height - closest = find_closest supply, xy - if closest? - closest = closest.clone() - on_canvas[closest.id] = closest + s = find_closest supply, xy + if s? + hit = { + s: s.clone() + layer: widget_layer + } + widget_layer.all[hit.s.id] = hit.s else - closest = find_closest controls, xy - unless closest? - closest = find_closest on_canvas, xy - if closest? - console.log closest - if selected[closest.id] + hit = closest_in_layers xy + if hit? + if hit.layer.selected[hit.s.id] # already selected # TODO start detection of a click that doesn't drag (to shrink selection) else if xy.y < supply_height # dragging a new thing in - select_only closest + select_only hit.layer, hit.s else if shift_key_down - select_also closest + select_also hit.layer, hit.s else - select_only closest - for id, s of selected - s.set_style STYLE_DRAGGING + select_only hit.layer, hit.s + for id, s of hit.layer.selected + s.set_state STATES.DRAGGING dragging = true + drag_layer = hit.layer drag_from = xy else - deselect_all() - return false + deselect_all widget_layer + return mouseup = (e) -> mousemove e if dragging - for id, w of selected - if w.y < supply_height - deselect w - w.destruct() - delete on_canvas[id] + selected_count = 0 + for id, s of drag_layer.selected + if s.y < supply_height and drag_layer is widget_layer + deselect drag_layer, s + s.destruct() + delete drag_layer.all[id] else - w.set_style STYLE_SELECTED - if w.type is TYPE_WIDGET - cs = w.make_controls done: (c) -> - if controls[c.id]? - delete controls[c.id] - for c in cs - controls[c.id] = c - editing[w.id] = w - delete selected[w.id] + selected_count += 1 + s.set_state STATES.SELECTED + if drag_layer is widget_layer and selected_count is 1 + for id, s of drag_layer.selected + s.set_state STATES.EDITING + cs = s.make_controls done: (c) -> + deselect controls_layer, c + delete controls_layer.all[c.id] + for c in cs + controls_layer.all[c.id] = c + widget_layer.editing = s dragging = false - return false - prev_hover = null + return mousemove = (e) -> xy = svg_event_to_xy e if dragging @@ -371,26 +585,25 @@ init = -> rel_x = xy.x - drag_from.x rel_y = xy.y - drag_from.y drag_from = xy - for id, w of selected - w.drag x: w.x + rel_x, y: w.y + rel_y + for id, w of drag_layer.selected + w.drag x: rel_x, y: rel_y else - hover = find_closest on_canvas, xy - unless hover? - hover = find_closest supply, xy - if hover != prev_hover - if prev_hover? - # FIXME - if selected[prev_hover.id]? - prev_hover.set_style STYLE_SELECTED - else - prev_hover.set_style STYLE_NORMAL - if hover? - hover.set_style STYLE_HOVER - prev_hover = hover + hit = closest_in_layers xy + if hovered and hovered isnt hit?.s + hovered.set_hover false + return unless hit? + hovered = hit.s + hovered.set_hover true + return + $svg.mousedown (e) -> + mousedown e + return false + $svg.mouseup (e) -> + mouseup e + return false + $svg.mousemove (e) -> + mousemove e return false - $svg.mousedown mousedown - $svg.mouseup mouseup - $svg.mousemove mousemove $(document).on 'keyup keydown', (e) -> shift_key_down = e.shiftKey return true