X-Git-Url: https://jasonwoof.com/gitweb/?p=crayon_mockup.git;a=blobdiff_plain;f=main.coffee;h=632567f37cc00d9b6d349b95fb0dfd5dcc3d6267;hp=8f203135bb858cd7e8814d80ba5c9cb5b6b2997c;hb=HEAD;hpb=f65a88a4b471687241a8cb8ed43f3573b77a583e diff --git a/main.coffee b/main.coffee index 8f20313..632567f 100644 --- a/main.coffee +++ b/main.coffee @@ -29,6 +29,7 @@ STATES = { DRAGGING: { txt: 'dragging' } EDITING: { txt: 'editing' } } +TYPE_UNKNOWN = 0 TYPE_WIDGET = 1 TYPE_CONTROL = 2 @@ -98,6 +99,7 @@ next_widget_id = 0 class Visible # required args: svg constructor: (args) -> + @type = TYPE_UNKNOWN @id = next_widget_id next_widget_id += 1 @svg = args.svg @@ -107,38 +109,56 @@ class Visible @height = args.height ? 34 @state = args.state ? STATES.NORMAL @hover = false + @shown = false + @el = null + return destruct: -> + @hide() + return 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 + return + set_hover: (bool) -> + if bool != @hover + @hover = bool @update_class() + return + hide: -> + if @el and @shown + @svg.removeChild @el + @shown = false + return + show: -> + if @el? and not @shown + @svg.appendChild @el + @shown = true + return move: (xy) -> # just move @x = xy.x @y = xy.y + return drag: (dxy) -> # react to mouse drag (obey constraints, etc.) @move x: @x + dxy.x, y: @y + dxy.y + return proximity: (xy) -> # return the square of the distance to your visible bits return PROX_TOO_FAR set_state: (state) -> @state = state + @update_class() + return class Control extends Visible constructor: (args) -> super args @type = TYPE_CONTROL @on_drag = args.drag - @on_destruct = args.done ? null - destruct: -> - super() - if @on_destruct? - @on_destruct @ + return drag: (args) -> # call this when control point is being manipulated directly @on_drag args + return proximity: (xy) -> # return the square of the distance to your visible bits dx = xy.x - @x dy = xy.y - @y @@ -151,43 +171,57 @@ class ControlPath extends Control @el = json_to_svg path: d: @make_path() class: 'control_point normal' - @svg.appendChild @el - destruct: -> - super() - if @el? - @svg.removeChild @el + return move: (args) -> super args @el.setAttribute 'd', @make_path() + return +# A widget is a visable thing that can be edited via controls class Widget extends Visible - #sub-classes are expected to implement all of these: constructor: (args) -> super args - @controls = [] @type = TYPE_WIDGET + @controls = null + @controls_shown = false destruct: -> - @kill_controls() + super() + @hide_controls() + return clone: -> return new Widget @ - make_controls: -> # create controls, return them - return [] - kill_controls: -> - for c in @controls - c.destruct() + make_controls: -> @controls = [] return - move: (xy) -> # just move + hide_controls: -> + return unless @controls? and @controls_shown + for c in @controls + c.hide() + @controls_shown = false + return + show_controls: -> + return if @controls? and @controls_shown + unless @controls? + @make_controls() + return unless @controls? + for c in @controls + c.show() + @controls_shown = true + return + move: (xy) -> dx = xy.x - @x dy = xy.y - @y super xy - for c in @controls - c.move x: c.x + dx, y: c.y + dy + if @controls? + for c in @controls + c.move x: c.x + dx, y: c.y + dy + return set_state: (state) -> return if @state is state if @state is STATES.EDITING - @kill_controls() + @hide_controls() super state + return class RectWidget extends Widget constructor: (args) -> @@ -199,21 +233,24 @@ class RectWidget extends Widget width: @width - 2 height: @height - 2 class: 'box normal' - @svg.appendChild @el - destruct: -> - super() - if @el? - @svg.removeChild @el + @show() + return clone: -> return new RectWidget @ - set_state: (state) -> - super state - @update_class() + as_array: -> + return [@x, @y, @width, @height] + from_array: (args, a) -> + args.x = a[0] + args.y = a[1] + args.width = a[2] + args.height = a[3] + return new RectWidget args move: (args) -> super args @el.setAttribute 'x', @x + 1 @el.setAttribute 'y', @y + 1 @reposition_controls() + return proximity: (xy) -> # return the square of the distance to your visible bits x = xy.x y = xy.y @@ -250,11 +287,13 @@ class RectWidget extends Widget @height = wh.h @el.setAttribute 'height', @height - 2 @reposition_controls() + return reposition_controls: -> - if @controls.length > 1 + if @controls? and @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: -> gap = 7 mgap = 9 @@ -270,22 +309,18 @@ class RectWidget extends Widget { x: @x - gap, y: @y + @height + gap } { x: @x - mgap, y: @y + h2p } ] - make_controls: (args) -> # create controls, return them - if @controls.length > 0 - if console?.log? - console.log "warning: re-adding controls" - @kill_controls() + make_controls: -> + @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: resizers[i] @ shape: resizer_shapes[i % resizer_shapes.length] } - return @controls + return class PolylineWidget extends Widget constructor: (args) -> @@ -297,11 +332,24 @@ class PolylineWidget extends Widget @el = json_to_svg path: d: @my_path_d() class: 'polyline normal' - @svg.appendChild @el - destruct: -> - super() - if @el? - @svg.removeChild @el + @show() + as_array: -> + ret = [] + for n in @nodes + ret.push @x + n.x + ret.push @y + n.y + return ret + from_array: (args, a) -> + # the first node is also used as the @x,@y + args.x = a[0] + args.y = a[1] + args.nodes = [] + while a.length + xy = {} + xy.x = a.shift() - args.x + xy.y = a.shift() - args.y + args.nodes.push xy + return new PolylineWidget args clone: -> return new PolylineWidget @ my_path_d: -> @@ -315,13 +363,11 @@ class PolylineWidget extends Widget 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() + return proximity: (xy) -> # return the square of the distance to your visible bits prox = PROX_TOO_FAR for n, i in @nodes @@ -362,20 +408,16 @@ class PolylineWidget extends Widget 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() + # TODO implement + #@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 + for j in [1...@nodes.length] + @nodes[j].x -= dxy.x + @nodes[j].y -= dxy.y @move x: @x + dxy.x, y: @y + dxy.y return (dxy) => @nodes[i].x += dxy.x @@ -383,7 +425,7 @@ class PolylineWidget extends Widget @el.setAttribute 'd', @my_path_d() @reposition_controls() reposition_controls: -> - if @controls.length > 1 + if @controls? and @controls.length > 1 positions = @control_positions() for i in [0...positions.length] @controls[i].move x: positions[i].x, y: positions[i].y @@ -393,24 +435,27 @@ class PolylineWidget extends Widget 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() + make_controls: -> + @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 + return +CSS_CLASS_TO_PICKLE_TYPE = { + box: '0' + polyline: '1' +} +PICKLE_TYPE_TO_WIDGET_CLASS = [ + RectWidget + PolylineWidget +] # called automatically on domcontentloaded init = -> svg_offset = null @@ -452,47 +497,107 @@ init = -> 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}] + supply_add PolylineWidget, y: 50, nodes: [{x: 0, y: 0}, {x: 17, y: -50}, {x: 33, y: 0}, {x: 50, y: -50}] # editor state - controls_layer = { all: {}, selected: {} } - widget_layer = { all: {}, selected: {}, editing: null } - layers = [controls_layer, widget_layer] - hovered = null # can be in any layer + widgets = {} + selected = {} + selected_count = 0 + hovered = null + drag_targets = null 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 + lc = "abcdefghijklmnopqrstuvwxyz" + uc = "ABCDEFGHIJKLMNOPQRSTUVWXYZ" + cc_lca = lc.charCodeAt 0 + cc_lcz = lc.charCodeAt 25 + cc_uca = uc.charCodeAt 0 + cc_ucz = uc.charCodeAt 25 + cc_0 = '0'.charCodeAt 0 + cc_9 = '9'.charCodeAt 0 + # 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 widgets + 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() + load = (str) -> + return if str.charAt(1) isnt '0' + wtype = null + args = [] + ii = 0 + load_1 = (next_type) -> + unless wtype? + if next_type? + wtype = next_type + return + w = PICKLE_TYPE_TO_WIDGET_CLASS[wtype]::from_array svg: svg, args + widgets[w.id] = w + wtype = next_type + args = [] + ii = 0 + for i in [2...str.length] + c = str.charCodeAt(i) + if cc_0 <= c <= cc_9 + load_1 c - cc_0 + else if cc_lca <= c <= cc_lcz + ii *= lc.length + ii += c - cc_lca + args.push ii + ii = 0 + else if cc_uca <= c <= cc_ucz + ii *= lc.length + ii += c - cc_uca + load_1 null + return 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 = (layer, except = null) -> - for id, s of layer.selected - deselect layer, s - return - _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 + for id, w of widgets + w.hide_controls() + return + deselect = (w) -> + return unless selected[w.id]? + w.set_state STATES.NORMAL + delete selected[w.id] + selected_count -= 1 + w.hide_controls() + return + deselect_all = (except = null) -> + for id, w of selected + if w isnt except + deselect w + return + _select = (w) -> # don't call this directly, use select_only() or select_also() + w.set_state STATES.SELECTED + selected[w.id] = w + return + select_only = (w) -> + deselect_all w + return if selected[w.id]? + _select w + return + select_also = (w) -> + return if selected[w.id]? + stop_editing() + _select w return find_closest = (widgets, xy) -> prox = PROX_TOO_FAR @@ -505,6 +610,27 @@ init = -> if prox > PROX_MAX return null return closest + find_closest_control = (xy) -> + prox = PROX_TOO_FAR + closest = null + for id, s of selected + if s.controls_shown + for c in s.controls + if c.shown + new_prox = c.proximity xy + if new_prox < prox + prox = new_prox + closest = c + if prox > PROX_MAX + return null + return closest + find_closest_thing = (xy) -> + if xy.y < supply_height + return find_closest supply, xy + else + hit = find_closest_control xy + hit ?= find_closest widgets, xy + return hit svg_event_to_xy = (e) -> unless svg_offset? svg_offset = $svg.offset() @@ -512,71 +638,64 @@ 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 - s = find_closest supply, xy - if s? - hit = { - s: s.clone() - layer: widget_layer - } - widget_layer.all[hit.s.id] = hit.s + hit = find_closest supply, xy + if hit? + hit = hit.clone() + widgets[hit.id] = hit else - hit = closest_in_layers xy + hit = find_closest_control xy + unless hit? + hit = find_closest widgets, 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 hit.layer, hit.s - else if shift_key_down - select_also hit.layer, hit.s + if hit.type is TYPE_WIDGET + if selected[hit.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 hit + else if shift_key_down + select_also hit + else + select_only hit + for id, w of selected + w.set_state STATES.DRAGGING + drag_targets = selected else - select_only hit.layer, hit.s - for id, s of hit.layer.selected - s.set_state STATES.DRAGGING + drag_targets = {} + drag_targets[hit.id] = hit dragging = true - drag_layer = hit.layer drag_from = xy else - deselect_all widget_layer + deselect_all() return mouseup = (e) -> + save() mousemove e if dragging - 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 - 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 + widgets_remaining = 0 + for id, s of drag_targets + if s.type is TYPE_WIDGET + if s.y < supply_height + deselect s + s.destruct() + delete widgets[id] + else + widgets_remaining += 1 + s.set_state STATES.SELECTED + if widgets_remaining is 1 + for id, s of drag_targets + if s.type is TYPE_WIDGET + s.set_state STATES.EDITING + s.show_controls() dragging = false return mousemove = (e) -> @@ -586,15 +705,17 @@ init = -> rel_x = xy.x - drag_from.x rel_y = xy.y - drag_from.y drag_from = xy - for id, w of drag_layer.selected + for id, w of drag_targets w.drag x: rel_x, y: rel_y else - hit = closest_in_layers xy - if hovered and hovered isnt hit?.s + hit = find_closest_thing xy + return if hit is hovered # both null, or both the same + return if hit?.type is TYPE_CONTROL # hovering a control doesn't change display + if hovered? hovered.set_hover false - return unless hit? - hovered = hit.s - hovered.set_hover true + if hit? + hovered = hit + hit.set_hover true return $svg.mousedown (e) -> mousedown e @@ -610,4 +731,7 @@ init = -> return true #($ document).keydown (e) -> + if window.location.hash + load window.location.hash + $ init