1 # Copyright 2015 Jason Woofenden
3 # This program is free software: you can redistribute it and/or modify it under
4 # the terms of the GNU General Public License as published by the Free Software
5 # Foundation, either version 3 of the License, or (at your option) any later
8 # This program is distributed in the hope that it will be useful, but WITHOUT
9 # ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
10 # FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
13 # You should have received a copy of the GNU General Public License along with
14 # this program. If not, see <http://www.gnu.org/licenses/>.
21 CLICK_FUZ = 10 # this far away from things is close enough to be "clicked on"
22 PROX_MAX = CLICK_FUZ * CLICK_FUZ
23 PROX_TOO_FAR = PROX_MAX + 1 # no need to be precice when it's too far
27 NORMAL: { txt: 'normal' }
28 SELECTED: { txt: 'selected' }
29 DRAGGING: { txt: 'dragging' }
30 EDITING: { txt: 'editing' }
35 # json (compiled to javascript and minified) is ~8% smaller than the resulting xml
36 json_to_svg = (json) ->
37 for tag, attrs of json
38 el = document.createElementNS 'http://www.w3.org/2000/svg', tag
42 el.appendChild json_to_svg child
43 else if k is 'contents'
44 el.appendChild document.createTextNode v
49 resizer_nw = (widget) ->
51 widget.resize w: widget.width - dxy.x, h: widget.height - dxy.y
52 widget.move x: widget.x + dxy.x, y: widget.y + dxy.y
53 resizer_n = (widget) ->
55 widget.resize w: widget.width, h: widget.height - dxy.y
56 widget.move x: widget.x, y: widget.y + dxy.y
57 resizer_ne = (widget) ->
59 widget.resize w: widget.width + dxy.x, h: widget.height - dxy.y
60 widget.move x: widget.x, y: widget.y + dxy.y
61 resizer_e = (widget) ->
63 widget.resize w: widget.width + dxy.x, h: widget.height
64 resizer_se = (widget) ->
66 widget.resize w: widget.width + dxy.x, h: widget.height + dxy.y
67 resizer_s = (widget) ->
69 widget.resize w: widget.width, h: widget.height + dxy.y
70 resizer_sw = (widget) ->
72 widget.resize w: widget.width - dxy.x, h: widget.height + dxy.y
73 widget.move x: widget.x + dxy.x, y: widget.y
74 resizer_w = (widget) ->
76 widget.resize w: widget.width - dxy.x, h: widget.height
77 widget.move x: widget.x + dxy.x, y: widget.y
89 (xy) -> return "M#{@x - 5} #{@y - 5}h6l-2 2 4 4 2 -2v6h-6l2-2-4-4-2 2z"
90 (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"
91 (xy) -> return "M#{@x + 5} #{@y - 5}v6l-2-2-4 4 2 2h-6v-6l2 2 4-4-2-2z"
92 (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"
96 # public vars: x, y, width, height, el
99 constructor: (args) ->
105 @width = args.width ? 50
106 @height = args.height ? 34
107 @state = args.state ? STATES.NORMAL
111 css_class = "#{@css_class} #{@state.txt}"
113 css_class += " hover"
114 @el.setAttribute 'class', css_class
119 move: (xy) -> # just move
122 drag: (dxy) -> # react to mouse drag (obey constraints, etc.)
123 @move x: @x + dxy.x, y: @y + dxy.y
124 proximity: (xy) -> # return the square of the distance to your visible bits
126 set_state: (state) ->
129 class Control extends Visible
130 constructor: (args) ->
134 @on_destruct = args.done ? null
139 drag: (args) -> # call this when control point is being manipulated directly
141 proximity: (xy) -> # return the square of the distance to your visible bits
144 return dx * dx + dy * dy
145 class ControlPath extends Control
146 constructor: (args) ->
148 @css_class = 'control_point'
149 @make_path = args.shape
150 @el = json_to_svg path:
152 class: 'control_point normal'
160 @el.setAttribute 'd', @make_path()
162 class Widget extends Visible
163 #sub-classes are expected to implement all of these:
164 constructor: (args) ->
172 make_controls: -> # create controls, return them
179 move: (xy) -> # just move
184 c.move x: c.x + dx, y: c.y + dy
185 set_state: (state) ->
186 return if @state is state
187 if @state is STATES.EDITING
191 class RectWidget extends Widget
192 constructor: (args) ->
195 @el = json_to_svg rect:
207 return new RectWidget @
208 set_state: (state) ->
213 @el.setAttribute 'x', @x + 1
214 @el.setAttribute 'y', @y + 1
215 @reposition_controls()
216 proximity: (xy) -> # return the square of the distance to your visible bits
222 if x > @x - CLICK_FUZ and x < @x + @width + CLICK_FUZ
224 if y < @y + @height / 2
227 new_prox = @y + @height - y
231 if y > @y - CLICK_FUZ and y < @y + @height + CLICK_FUZ
233 if x < @x + @width / 2
236 new_prox = @x + @width - x
240 if in_x and in_y and prox > PROX_MAX
247 @el.setAttribute 'width', @width - 2
249 @el.setAttribute 'height', @height - 2
250 @reposition_controls()
251 reposition_controls: ->
252 if @controls.length > 1
253 positions = @control_positions()
254 for i in [0...positions.length]
255 @controls[i].move x: positions[i].x, y: positions[i].y
256 control_positions: ->
259 w2p = Math.floor(@width / 2) + 0.5
260 h2p = Math.floor(@height / 2) + 0.5
262 { x: @x - gap, y: @y - gap }
263 { x: @x + w2p, y: @y - mgap }
264 { x: @x + @width + gap, y: @y - gap }
265 { x: @x + @width + mgap, y: @y + h2p }
266 { x: @x + @width + gap, y: @y + @height + gap }
267 { x: @x + w2p, y: @y + @height + mgap }
268 { x: @x - gap, y: @y + @height + gap }
269 { x: @x - mgap, y: @y + h2p }
271 make_controls: (args) -> # create controls, return them
272 if @controls.length > 0
274 console.log "warning: re-adding controls"
276 positions = @control_positions()
277 for i in [0...positions.length]
278 @controls.push new ControlPath {
284 shape: resizer_shapes[i % resizer_shapes.length]
288 # called automatically on domcontentloaded
291 $container = $ '.crayon_mockup'
292 svg = json_to_svg svg: width: width, height: height, viewBox: "0 0 #{width} #{height}"
294 $container.append $svg
295 svg.appendChild json_to_svg filter:
296 id: 'crayon', filterUnits: 'userSpaceOnUse'
297 x: '-5%', y: '-5%', height: '110%', width: '110%'
299 { feTurbulence: baseFrequency: '.3', numOctaves: '2', type: 'fractalNoise' }
300 { feDisplacementMap: scale: '6', xChannelSelector: 'R', in: 'SourceGraphic' }
302 svg.appendChild json_to_svg style:
304 contents: '.box.normal{filter: url(#crayon)}'
306 # create canvas border
307 svg.appendChild json_to_svg rect:
311 height: height - 2 - supply_height
312 class: 'canvas_border'
316 { width: 40, height: 40 }
317 { width: 12, height: 50 }
318 { width: 70, height: 12 }
320 widget = new RectWidget {
323 x: 30 + i * 90 + (70 - args.width) / 2
324 y: (supply_height - args.height) / 2
327 supply[widget.id] = widget
330 controls_layer = { all: {}, selected: {} }
331 widget_layer = { all: {}, selected: {}, editing: null }
332 layers = [controls_layer, widget_layer]
333 hovered = null # can be in any layer
334 dragging = false # mouse state
336 drag_from = x: 0, y: 0 # mouse was here at last frame of drag
337 shift_key_down = false
340 if widget_layer.editing
341 widget_layer.editing.kill_controls()
342 widget_layer.editing = null
343 deselect = (layer, s) ->
344 return unless layer.selected[s.id]?
345 s.set_state STATES.NORMAL
346 delete layer.selected[s.id]
347 if widget_layer.editing is s
348 widget_layer.editing = null
350 deselect_all = (layer, except = null) ->
351 for id, s of layer.selected
354 _select = (layer, s) -> # don't call this directly, use select_only() or select_also()
355 s.set_state STATES.SELECTED
356 layer.selected[s.id] = s
358 select_only = (layer, s) ->
359 deselect_all layer, s
360 return if layer.selected[s.id]?
363 select_also = (layer, s) ->
364 return if layer.selected[s.id]?
365 if layer is widget_layer
369 find_closest = (widgets, xy) ->
373 new_prox = w.proximity xy
380 svg_event_to_xy = (e) ->
382 svg_offset = $svg.offset()
384 x: Math.round(e.pageX - svg_offset.left)
385 y: Math.round(e.pageY - svg_offset.top)
387 closest_in_layers = (xy) ->
389 s = find_closest layer.selected, xy
390 return layer: layer, s: s if s?
391 s = find_closest layer.all, xy
392 return layer: layer, s: s if s?
399 if dragging # two mousedowns in a row?! it happens
401 xy = svg_event_to_xy e
402 if xy.y < supply_height
403 s = find_closest supply, xy
409 widget_layer.all[hit.s.id] = hit.s
411 hit = closest_in_layers xy
413 if hit.layer.selected[hit.s.id]
415 # TODO start detection of a click that doesn't drag (to shrink selection)
416 else if xy.y < supply_height
417 # dragging a new thing in
418 select_only hit.layer, hit.s
419 else if shift_key_down
420 select_also hit.layer, hit.s
422 select_only hit.layer, hit.s
423 for id, s of hit.layer.selected
424 s.set_state STATES.DRAGGING
426 drag_layer = hit.layer
429 deselect_all widget_layer
435 for id, s of drag_layer.selected
436 if s.y < supply_height and drag_layer is widget_layer
437 deselect drag_layer, s
439 delete drag_layer.all[id]
442 s.set_state STATES.SELECTED
443 if drag_layer is widget_layer and selected_count is 1
444 for id, s of drag_layer.selected
445 s.set_state STATES.EDITING
446 cs = s.make_controls done: (c) ->
447 deselect controls_layer, c
448 delete controls_layer.all[c.id]
450 controls_layer.all[c.id] = c
451 widget_layer.editing = s
455 xy = svg_event_to_xy e
457 return if drag_from.x is xy.x and drag_from.y is xy.y
458 rel_x = xy.x - drag_from.x
459 rel_y = xy.y - drag_from.y
461 for id, w of drag_layer.selected
462 w.drag x: rel_x, y: rel_y
464 hit = closest_in_layers xy
465 if hovered and hovered isnt hit?.s
466 hovered.set_hover false
469 hovered.set_hover true
471 $svg.mousedown (e) ->
477 $svg.mousemove (e) ->
480 $(document).on 'keyup keydown', (e) ->
481 shift_key_down = e.shiftKey
483 #($ document).keydown (e) ->