# 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 STYLE_NORMAL = 0 STYLE_SELECTED = 1 STYLE_HOVER = 2 STYLE_EDITING = 3 STYLE_DRAGGING = 4 STYLE_TO_CLASS = [ "normal" "selected" "hover" "editing" "dragging" ] 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 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 @style = args.style ? STYLE_NORMAL destruct: -> move: (xy) -> # just move @x = xy.x @y = xy.y drag: (xy) -> # react to mouse drag (obey constraints, etc.) @move xy proximity: (xy) -> # return the square of the distance to your visible bits return PROX_TOO_FAR set_style: (style) -> @style = style 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 ControlPoint extends Control constructor: (args) -> super args @el = json_to_svg circle: cx: @x + 1 cy: @y + 1 r: 6 class: 'control_point normal' @svg.appendChild @el destruct: -> super() if @el? @svg.removeChild @el move: (args) -> super args @el.setAttribute 'cx', @x @el.setAttribute 'cy', @y 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 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_style: (style) -> super style set_style_class el: @el, class: 'box', style: style 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) -> @width = wh.w @el.setAttribute 'width', @width @height = wh.h @el.setAttribute 'height', @height if @controls.length > 1 @controls[1].move x: @x + @width, y: @y + @height 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 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 ] 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,.box.hover,.box.selected{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 on_canvas = {} selected = {} controls = {} editing = {} # has controls dragging = false 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] return deselect_all = (args) -> except = args?.except ? null for id, s of selected deselect 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 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) } mousedown = (e) -> 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 else closest = find_closest controls, xy unless closest? closest = find_closest on_canvas, xy if closest? console.log closest if selected[closest.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 else if shift_key_down select_also closest else select_only closest for id, s of selected s.set_style STYLE_DRAGGING dragging = true drag_from = xy else deselect_all() return false mouseup = (e) -> mousemove e if dragging for id, w of selected if w.y < supply_height deselect w w.destruct() delete on_canvas[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] dragging = false return false prev_hover = null 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 selected w.drag x: w.x + rel_x, y: w.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 return false $svg.mousedown mousedown $svg.mouseup mouseup $svg.mousemove mousemove $(document).on 'keyup keydown', (e) -> shift_key_down = e.shiftKey return true #($ document).keydown (e) -> $ init