# Copyright 2015 Jason Woofenden # # This program is free software: you can redistribute it and/or modify it under # the terms of the GNU General Public License as published by the Free Software # Foundation, either version 3 of the License, or (at your option) any later # version. # # This program is distributed in the hope that it will be useful, but WITHOUT # ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS # FOR A PARTICULAR PURPOSE. See the GNU General Public License for more # details. # # You should have received a copy of the GNU General Public License along with # this program. If not, see . # settings width = 800 height = 600 supply_height = 96 CLICK_FUZ = 10 # this far away from things is close enough to be "clicked on" PROX_MAX = CLICK_FUZ * CLICK_FUZ PROX_TOO_FAR = PROX_MAX + 1 # no need to be precice when it's too far # constants STATES = { NORMAL: { txt: 'normal' } SELECTED: { txt: 'selected' } DRAGGING: { txt: 'dragging' } EDITING: { txt: 'editing' } } TYPE_WIDGET = 1 TYPE_CONTROL = 2 # json (compiled to javascript and minified) is ~8% smaller than the resulting xml json_to_svg = (json) -> for tag, attrs of json el = document.createElementNS 'http://www.w3.org/2000/svg', tag for k, v of attrs if k is 'children' for child in v el.appendChild json_to_svg child else if k is 'contents' el.appendChild document.createTextNode v else el.setAttribute k, v return el next_widget_id = 0 # public vars: x, y, width, height, el class Visible # required args: svg constructor: (args) -> @id = next_widget_id next_widget_id += 1 @svg = args.svg @x = args.x ? 1 @y = args.y ? 1 @width = args.width ? 50 @height = args.height ? 34 @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: (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_state: (state) -> @state = state 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 @ drag: (args) -> # call this when control point is being manipulated directly @on_drag args proximity: (xy) -> # return the square of the distance to your visible bits dx = xy.x - @x dy = xy.y - @y return dx * dx + dy * dy class ControlNWSE extends Control constructor: (args) -> super args @css_class = 'control_point' @el = json_to_svg path: d: @make_path() class: 'control_point normal' @svg.appendChild @el make_path: -> # / return "M#{@x + 5} #{@y - 5}v6l-2-2-4 4 2 2h-6v-6l2 2 4-4-2-2z" return "M#{@x - 5} #{@y - 5}h6l-2 2 4 4 2 -2v6h-6l2-2-4-4-2 2z" destruct: -> super() if @el? @svg.removeChild @el move: (args) -> super args @el.setAttribute 'd', @make_path() class Widget extends Visible #sub-classes are expected to implement all of these: constructor: (args) -> super args @controls = [] @type = TYPE_WIDGET destruct: -> @kill_controls() clone: -> return new Widget @ make_controls: -> # create controls, return them return [] kill_controls: -> console.log 'kill_controls' for c in @controls c.destruct() @controls = [] return move: (xy) -> # just move dx = xy.x - @x dy = xy.y - @y 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) -> super args @css_class = 'box' @el = json_to_svg rect: x: @x + 1 y: @y + 1 width: @width - 2 height: @height - 2 class: 'box normal' @svg.appendChild @el destruct: -> super() if @el? @svg.removeChild @el clone: -> return new RectWidget @ set_state: (state) -> super state @update_class() move: (args) -> super args @el.setAttribute 'x', @x + 1 @el.setAttribute 'y', @y + 1 proximity: (xy) -> # return the square of the distance to your visible bits x = xy.x y = xy.y prox = PROX_TOO_FAR in_x = false in_y = false if x > @x - CLICK_FUZ and x < @x + @width + CLICK_FUZ in_x = true if y < @y + @height / 2 new_prox = @y - y else new_prox = @y + @height - y new_prox *= new_prox if new_prox < prox prox = new_prox if y > @y - CLICK_FUZ and y < @y + @height + CLICK_FUZ in_y = true if x < @x + @width / 2 new_prox = @x - x else new_prox = @x + @width - x 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 return prox resize: (wh) -> dw = wh.w - @width dh = wh.h - @height @width = wh.w @el.setAttribute 'width', @width - 2 @height = wh.h @el.setAttribute 'height', @height - 2 if @controls.length > 1 @controls[1].move x: @controls[1].x + dw, y: @controls[1].y + dh make_controls: (args) -> # create controls, return them console.log 'make_controls' if @controls.length > 0 console.log "warning: re-adding controls" @kill_controls() w = @ @controls = [ new ControlNWSE svg: @svg, x: @x - 7, y: @y - 7, done: args.done, drag: (dxy) -> w.resize w: w.width - dxy.x, h: w.height - dxy.y w.move x: w.x + dxy.x, y: w.y + dxy.y new ControlNWSE svg: @svg, x: @x + @width + 7, y: @y + @height + 7, done: args.done, drag: (dxy) -> w.resize w: w.width + dxy.x, h: w.height + dxy.y ] return @controls # called automatically on domcontentloaded init = -> svg_offset = null $container = $ '.crayon_mockup' svg = json_to_svg svg: width: width, height: height, viewBox: "0 0 #{width} #{height}" $svg = $ svg $container.append $svg svg.appendChild json_to_svg filter: id: 'crayon', filterUnits: 'userSpaceOnUse' x: '-5%', y: '-5%', height: '110%', width: '110%' children: [ { feTurbulence: baseFrequency: '.3', numOctaves: '2', type: 'fractalNoise' } { feDisplacementMap: scale: '6', xChannelSelector: 'R', in: 'SourceGraphic' } ] svg.appendChild json_to_svg style: type: 'text/css' contents: '.box.normal{filter: url(#crayon)}' # create canvas border svg.appendChild json_to_svg rect: x: 1 y: supply_height + 1 width: width - 2 height: height - 2 - supply_height 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 # editor state 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 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 return find_closest = (widgets, xy) -> prox = PROX_TOO_FAR closest = null for id, w of widgets new_prox = w.proximity xy if new_prox < prox prox = new_prox closest = w if prox > PROX_MAX return null return closest svg_event_to_xy = (e) -> unless svg_offset? svg_offset = $svg.offset() return { 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 else 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 hit.layer, hit.s else if shift_key_down select_also hit.layer, hit.s else 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 console.log hit else deselect_all widget_layer return mouseup = (e) -> 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 dragging = false return mousemove = (e) -> xy = svg_event_to_xy e if dragging return if drag_from.x is xy.x and drag_from.y is xy.y rel_x = xy.x - drag_from.x rel_y = xy.y - drag_from.y drag_from = xy for id, w of drag_layer.selected w.drag x: rel_x, y: rel_y else 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 $(document).on 'keyup keydown', (e) -> shift_key_down = e.shiftKey return true #($ document).keydown (e) -> $ init