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 ControlPoint extends Control
101 constructor: (args) ->
103 @css_class = 'control_point'
104 @el = json_to_svg circle:
108 class: 'control_point normal'
116 @el.setAttribute 'cx', @x
117 @el.setAttribute 'cy', @y
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
202 @el.setAttribute 'width', @width
204 @el.setAttribute 'height', @height
205 if @controls.length > 1
206 @controls[1].move x: @x + @width, y: @y + @height
207 make_controls: (args) -> # create controls, return them
208 console.log 'make_controls'
209 if @controls.length > 0
210 console.log "warning: re-adding controls"
214 new ControlPoint svg: @svg, x: @x, y: @y, done: args.done, drag: (dxy) ->
215 w.resize w: w.width - dxy.x, h: w.height - dxy.y
216 w.move x: w.x + dxy.x, y: w.y + dxy.y
217 new ControlPoint svg: @svg, x: @x + @width, y: @y + @height, done: args.done, drag: (dxy) ->
218 w.resize w: w.width + dxy.x, h: w.height + dxy.y
222 # called automatically on domcontentloaded
225 $container = $ '.crayon_mockup'
226 svg = json_to_svg svg: width: width, height: height, viewBox: "0 0 #{width} #{height}"
228 $container.append $svg
229 svg.appendChild json_to_svg filter:
230 id: 'crayon', filterUnits: 'userSpaceOnUse'
231 x: '-5%', y: '-5%', height: '110%', width: '110%'
233 { feTurbulence: baseFrequency: '.3', numOctaves: '2', type: 'fractalNoise' }
234 { feDisplacementMap: scale: '6', xChannelSelector: 'R', in: 'SourceGraphic' }
236 svg.appendChild json_to_svg style:
238 contents: '.box.normal,.box.selected{filter: url(#crayon)}'
240 # create canvas border
241 svg.appendChild json_to_svg rect:
245 height: height - 2 - supply_height
246 class: 'canvas_border'
250 { width: 40, height: 40 }
251 { width: 12, height: 50 }
252 { width: 70, height: 12 }
254 widget = new RectWidget {
257 x: 30 + i * 90 + (70 - args.width) / 2
258 y: (supply_height - args.height) / 2
261 supply[widget.id] = widget
264 controls_layer = { all: {}, selected: {} }
265 widget_layer = { all: {}, selected: {}, editing: null }
266 layers = [controls_layer, widget_layer]
267 hovered = null # can be in any layer
268 dragging = false # mouse state
270 drag_from = x: 0, y: 0 # mouse was here at last frame of drag
271 shift_key_down = false
274 if widget_layer.editing
275 widget_layer.editing.kill_controls()
276 widget_layer.editing = null
277 deselect = (layer, s) ->
278 return unless layer.selected[s.id]?
279 s.set_state STATES.NORMAL
280 delete layer.selected[s.id]
281 if widget_layer.editing is s
282 widget_layer.editing = null
284 deselect_all = (layer, except = null) ->
285 for id, s of layer.selected
288 _select = (layer, s) -> # don't call this directly, use select_only() or select_also()
289 s.set_state STATES.SELECTED
290 layer.selected[s.id] = s
292 select_only = (layer, s) ->
293 deselect_all layer, s
294 return if layer.selected[s.id]?
297 select_also = (layer, s) ->
298 return if layer.selected[s.id]?
299 if layer is widget_layer
303 find_closest = (widgets, xy) ->
307 new_prox = w.proximity xy
314 svg_event_to_xy = (e) ->
316 svg_offset = $svg.offset()
318 x: Math.round(e.pageX - svg_offset.left)
319 y: Math.round(e.pageY - svg_offset.top)
321 closest_in_layers = (xy) ->
323 s = find_closest layer.selected, xy
324 return layer: layer, s: s if s?
325 s = find_closest layer.all, xy
326 return layer: layer, s: s if s?
333 if dragging # two mousedowns in a row?! it happens
335 xy = svg_event_to_xy e
336 if xy.y < supply_height
337 s = find_closest supply, xy
343 widget_layer.all[hit.s.id] = hit.s
345 hit = closest_in_layers xy
347 if hit.layer.selected[hit.s.id]
349 # TODO start detection of a click that doesn't drag (to shrink selection)
350 else if xy.y < supply_height
351 # dragging a new thing in
352 select_only hit.layer, hit.s
353 else if shift_key_down
354 select_also hit.layer, hit.s
356 select_only hit.layer, hit.s
357 for id, s of hit.layer.selected
358 s.set_state STATES.DRAGGING
360 drag_layer = hit.layer
364 deselect_all widget_layer
370 for id, s of drag_layer.selected
371 if s.y < supply_height and drag_layer is widget_layer
372 deselect drag_layer, s
374 delete drag_layer.all[id]
377 s.set_state STATES.SELECTED
378 if drag_layer is widget_layer and selected_count is 1
379 for id, s of drag_layer.selected
380 s.set_state STATES.EDITING
381 cs = s.make_controls done: (c) ->
382 deselect controls_layer, c
383 delete controls_layer.all[c.id]
385 controls_layer.all[c.id] = c
386 widget_layer.editing = s
390 xy = svg_event_to_xy e
392 return if drag_from.x is xy.x and drag_from.y is xy.y
393 rel_x = xy.x - drag_from.x
394 rel_y = xy.y - drag_from.y
396 for id, w of drag_layer.selected
397 w.drag x: rel_x, y: rel_y
399 hit = closest_in_layers xy
401 return if hit.s is hovered
403 hovered.set_hover false
405 hovered.set_hover true
407 $svg.mousedown (e) ->
413 $svg.mousemove (e) ->
416 $(document).on 'keyup keydown', (e) ->
417 shift_key_down = e.shiftKey
419 #($ document).keydown (e) ->