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
50 # public vars: x, y, width, height, el
53 constructor: (args) ->
59 @width = args.width ? 50
60 @height = args.height ? 34
61 @state = args.state ? STATES.NORMAL
65 css_class = "#{@css_class} #{@state.txt}"
68 @el.setAttribute 'class', css_class
73 move: (xy) -> # just move
76 drag: (dxy) -> # react to mouse drag (obey constraints, etc.)
77 @move x: @x + dxy.x, y: @y + dxy.y
78 proximity: (xy) -> # return the square of the distance to your visible bits
83 class Control extends Visible
84 constructor: (args) ->
88 @on_destruct = args.done ? null
93 drag: (args) -> # call this when control point is being manipulated directly
95 proximity: (xy) -> # return the square of the distance to your visible bits
98 return dx * dx + dy * dy
100 class ControlNWSE extends Control
101 constructor: (args) ->
103 @css_class = 'control_point'
104 @el = json_to_svg path:
106 class: 'control_point normal'
109 # / return "M#{@x + 5} #{@y - 5}v6l-2-2-4 4 2 2h-6v-6l2 2 4-4-2-2z"
110 return "M#{@x - 5} #{@y - 5}h6l-2 2 4 4 2 -2v6h-6l2-2-4-4-2 2z"
117 @el.setAttribute 'd', @make_path()
119 class Widget extends Visible
120 #sub-classes are expected to implement all of these:
121 constructor: (args) ->
129 make_controls: -> # create controls, return them
132 console.log 'kill_controls'
137 move: (xy) -> # just move
142 c.move x: c.x + dx, y: c.y + dy
143 set_state: (state) ->
144 return if @state is state
145 if @state is STATES.EDITING
149 class RectWidget extends Widget
150 constructor: (args) ->
153 @el = json_to_svg rect:
165 return new RectWidget @
166 set_state: (state) ->
171 @el.setAttribute 'x', @x + 1
172 @el.setAttribute 'y', @y + 1
173 proximity: (xy) -> # return the square of the distance to your visible bits
179 if x > @x - CLICK_FUZ and x < @x + @width + CLICK_FUZ
181 if y < @y + @height / 2
184 new_prox = @y + @height - y
188 if y > @y - CLICK_FUZ and y < @y + @height + CLICK_FUZ
190 if x < @x + @width / 2
193 new_prox = @x + @width - x
197 if in_x and in_y and prox > PROX_MAX
204 @el.setAttribute 'width', @width - 2
206 @el.setAttribute 'height', @height - 2
207 if @controls.length > 1
208 @controls[1].move x: @controls[1].x + dw, y: @controls[1].y + dh
209 make_controls: (args) -> # create controls, return them
210 console.log 'make_controls'
211 if @controls.length > 0
212 console.log "warning: re-adding controls"
216 new ControlNWSE svg: @svg, x: @x - 7, y: @y - 7, done: args.done, drag: (dxy) ->
217 w.resize w: w.width - dxy.x, h: w.height - dxy.y
218 w.move x: w.x + dxy.x, y: w.y + dxy.y
219 new ControlNWSE svg: @svg, x: @x + @width + 7, y: @y + @height + 7, done: args.done, drag: (dxy) ->
220 w.resize w: w.width + dxy.x, h: w.height + dxy.y
224 # called automatically on domcontentloaded
227 $container = $ '.crayon_mockup'
228 svg = json_to_svg svg: width: width, height: height, viewBox: "0 0 #{width} #{height}"
230 $container.append $svg
231 svg.appendChild json_to_svg filter:
232 id: 'crayon', filterUnits: 'userSpaceOnUse'
233 x: '-5%', y: '-5%', height: '110%', width: '110%'
235 { feTurbulence: baseFrequency: '.3', numOctaves: '2', type: 'fractalNoise' }
236 { feDisplacementMap: scale: '6', xChannelSelector: 'R', in: 'SourceGraphic' }
238 svg.appendChild json_to_svg style:
240 contents: '.box.normal{filter: url(#crayon)}'
242 # create canvas border
243 svg.appendChild json_to_svg rect:
247 height: height - 2 - supply_height
248 class: 'canvas_border'
252 { width: 40, height: 40 }
253 { width: 12, height: 50 }
254 { width: 70, height: 12 }
256 widget = new RectWidget {
259 x: 30 + i * 90 + (70 - args.width) / 2
260 y: (supply_height - args.height) / 2
263 supply[widget.id] = widget
266 controls_layer = { all: {}, selected: {} }
267 widget_layer = { all: {}, selected: {}, editing: null }
268 layers = [controls_layer, widget_layer]
269 hovered = null # can be in any layer
270 dragging = false # mouse state
272 drag_from = x: 0, y: 0 # mouse was here at last frame of drag
273 shift_key_down = false
276 if widget_layer.editing
277 widget_layer.editing.kill_controls()
278 widget_layer.editing = null
279 deselect = (layer, s) ->
280 return unless layer.selected[s.id]?
281 s.set_state STATES.NORMAL
282 delete layer.selected[s.id]
283 if widget_layer.editing is s
284 widget_layer.editing = null
286 deselect_all = (layer, except = null) ->
287 for id, s of layer.selected
290 _select = (layer, s) -> # don't call this directly, use select_only() or select_also()
291 s.set_state STATES.SELECTED
292 layer.selected[s.id] = s
294 select_only = (layer, s) ->
295 deselect_all layer, s
296 return if layer.selected[s.id]?
299 select_also = (layer, s) ->
300 return if layer.selected[s.id]?
301 if layer is widget_layer
305 find_closest = (widgets, xy) ->
309 new_prox = w.proximity xy
316 svg_event_to_xy = (e) ->
318 svg_offset = $svg.offset()
320 x: Math.round(e.pageX - svg_offset.left)
321 y: Math.round(e.pageY - svg_offset.top)
323 closest_in_layers = (xy) ->
325 s = find_closest layer.selected, xy
326 return layer: layer, s: s if s?
327 s = find_closest layer.all, xy
328 return layer: layer, s: s if s?
335 if dragging # two mousedowns in a row?! it happens
337 xy = svg_event_to_xy e
338 if xy.y < supply_height
339 s = find_closest supply, xy
345 widget_layer.all[hit.s.id] = hit.s
347 hit = closest_in_layers xy
349 if hit.layer.selected[hit.s.id]
351 # TODO start detection of a click that doesn't drag (to shrink selection)
352 else if xy.y < supply_height
353 # dragging a new thing in
354 select_only hit.layer, hit.s
355 else if shift_key_down
356 select_also hit.layer, hit.s
358 select_only hit.layer, hit.s
359 for id, s of hit.layer.selected
360 s.set_state STATES.DRAGGING
362 drag_layer = hit.layer
366 deselect_all widget_layer
372 for id, s of drag_layer.selected
373 if s.y < supply_height and drag_layer is widget_layer
374 deselect drag_layer, s
376 delete drag_layer.all[id]
379 s.set_state STATES.SELECTED
380 if drag_layer is widget_layer and selected_count is 1
381 for id, s of drag_layer.selected
382 s.set_state STATES.EDITING
383 cs = s.make_controls done: (c) ->
384 deselect controls_layer, c
385 delete controls_layer.all[c.id]
387 controls_layer.all[c.id] = c
388 widget_layer.editing = s
392 xy = svg_event_to_xy e
394 return if drag_from.x is xy.x and drag_from.y is xy.y
395 rel_x = xy.x - drag_from.x
396 rel_y = xy.y - drag_from.y
398 for id, w of drag_layer.selected
399 w.drag x: rel_x, y: rel_y
401 hit = closest_in_layers xy
402 if hovered and hovered isnt hit?.s
403 hovered.set_hover false
406 hovered.set_hover true
408 $svg.mousedown (e) ->
414 $svg.mousemove (e) ->
417 $(document).on 'keyup keydown', (e) ->
418 shift_key_down = e.shiftKey
420 #($ document).keydown (e) ->