-
Notifications
You must be signed in to change notification settings - Fork 1
/
Copy pathgesture.coffee
288 lines (250 loc) · 6.4 KB
/
gesture.coffee
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
##################################################
# algorithms
##################################################
# trivial: only support up/down/left/right
gestrue_direction = ([x1, y1], [x2, y2]) ->
x = x2 - x1
y = y2 - y1
if x > 2 * Math.abs y
return 'right'
if x < -2 * Math.abs y
return 'left'
if y > 2 * Math.abs x
return 'down'
if y < -2 * Math.abs x
return 'up'
trivial_gesture = (vector) ->
minimum_samples = 5
if vector.length > minimum_samples
start = vector[0]
middle = vector[Math.floor vector.length/2]
end = vector[vector.length - 1]
d1 = gestrue_direction start, end
d2 = gestrue_direction start, middle
d3 = gestrue_direction middle, end
if d1? and d2? and d3? and d1 == d2 == d3
return d1
# simple: only support up/down/left/right
# http://doc.qt.digia.com/qq/qq18-mousegestures.html
filter_and_limit = (vector) ->
minimum_movement = 5
minimum_movement2 = minimum_movement * minimum_movement
# filter and limit
directions = []
[x0, y0] = vector[0]
for i in [1...vector.length]
[x1, y1] = vector[i]
dx = x1 - x0
dy = y1 - y0
if dx*dx + dy*dy >= minimum_movement2
if dy > 0
if dx > dy or -dx > dy
dy = 0
else
dx = 0
else
if dx > -dy or -dx > -dy
dy = 0
else
dx = 0
directions.push [dx, dy]
x0 = x1
y0 = y1
return directions
simplify = (vector) ->
if vector.length == 0
return vector
directions = []
[x0, y0] = vector[0]
for i in [1...vector.length]
[x1, y1] = vector[i]
if x0 * x1 + y0 * y1 > 0
x0 += x1
y0 += y1
else
directions.push [x0, y0]
x0 = x1
y0 = y1
directions.push [x0, y0]
return directions
calculate_length = (directions) ->
total = 0
for [x, y] in directions
total += Math.abs(x) + Math.abs(y)
return total
remove_shortest = (directions) ->
if directions.length <= 1
return []
shortest = 0
index = 0
for i in [0...directions.length]
[x, y] = directions[i]
n = Math.abs(x) + Math.abs(y)
if i == 0
shortest = n
else
if n < shortest
shortest = n
index = i
directions.splice index, 1
return directions
match_gesture = (directions, gesture) ->
if directions.length == gesture.length
for i in [0...directions.length]
[x0, y0] = directions[i]
[x1, y1] = gesture[i]
if x0*x1 + y0*y1 <= 0
return false
return true
find_matched_gesture = (directions, gestures) ->
for gesture in gestures
if match_gesture directions, gesture
return gesture
match_and_reduce = (directions, gestures) ->
directions = simplify directions
minimum_match = 0.9
minimum_length = calculate_length(directions) * minimum_match
while directions.length and calculate_length(directions) > minimum_length
gesture = find_matched_gesture(directions, gestures)
if gesture
return gesture
directions = simplify remove_shortest directions
parse_gesture_directions = (gesture) ->
directions = []
for direction in gesture.split ' '
if direction == 'left'
directions.push [-1, 0]
else if direction == 'right'
directions.push [1, 0]
else if direction == 'up'
directions.push [0, -1]
else if direction == 'down'
directions.push [0, 1]
else
throw Error("Invalid gesture direction: #{direction}")
return directions
simple_gesture = (vector) ->
if vector.length <= 1
return
directions = filter_and_limit vector
if not directions?.length
return
directions = simplify directions
direction_text = ([x, y]) ->
if x == 0
if y > 0
'down'
else
'up'
else
if x > 0
'right'
else
'left'
(direction_text direction for direction in directions).join ' '
simple_gesture_recognize = (vector, gestures) ->
if vector.length <= 1
return
directions = filter_and_limit vector
if not directions?.length
return
convert_gesture = (g) ->
gg = parse_gesture_directions(g)
gg.text = g
gg
match_and_reduce(directions, (convert_gesture g for g in gestures))?.text
##################################################
# front end
##################################################
class GestureCanvas
constructor: (@master) ->
@id = "gesture_canvas_" + new Date().getTime()
$('body').append "<canvas id='#{@id}'/>"
@element = $('#'+@id)
@element.css
'background-color': 'rgba(0, 0, 0, 0)'
'position': 'absolute'
'z-index': -10
@canvas = @element[0]
@context = @canvas.getContext("2d")
set_canvas_size = => @resize()
# set_canvas_size() # XXX: if I call set_canvas_size immidately, it gets size before the initial window resize. why?
setTimeout set_canvas_size, 100
$(window).resize set_canvas_size
resize: ->
@element.offset(@master.offset()).prop width: @master.width(), height: @master.height()
show: ->
@element.css 'z-index': 'auto'
hide: ->
@element.css 'z-index': -10
draw_line: ([x1, y1], [x2, y2])->
@context.strokeStyle = 'orangered'
@context.lineWidth = 2
@context.beginPath()
@context.moveTo x1, y1
@context.lineTo x2, y2
@context.stroke()
clear: ->
@context.clearRect(0, 0, @canvas.width, @canvas.height)
class GestureEvent
constructor: (@vector) ->
# @direction = simple_gesture @vector
recognize_gesture: (gestures) ->
simple_gesture_recognize @vector, gestures
class GestureManager
constructor: (@at, @handler) ->
@vector = []
@gesture_at = new Date()
@canvas = new GestureCanvas @at
@minimum_samples = 1
trace: ->
@vector = []
start: ->
@canvas.show()
cancel: ->
@canvas.hide()
@vector = []
@canvas.clear()
update: (x, y) ->
if @vector.length > 0
if @vector.length == @minimum_samples
@start()
@canvas.draw_line @vector[@vector.length-1], [x, y]
@vector.push [x, y]
update_event: (e) ->
offset = $(e.currentTarget).offset()
@update e.pageX - offset.left, e.pageY - offset.top
end: ->
@canvas.hide()
if @vector.length > @minimum_samples
@process()
@gesture_at = new Date()
@vector = []
@canvas.clear()
process: ->
@handler new GestureEvent @vector
$.fn.gesture = (handler) ->
gesture = new GestureManager(@, handler)
@mousedown (e) ->
if e.button != 2
return
gesture.trace()
@mouseup (e) ->
if e.button != 2
return
gesture.end()
@mousemove (e) ->
if e.button != 2
return
gesture.update_event e
@contextmenu (e) ->
if new Date() - gesture.gesture_at < 500
e.preventDefault()
e.stopImmediatePropagation()
# TODO: mouseout?
gesture.canvas.element.mousedown -> gesture.cancel()
gesture.canvas.element.mouseup -> gesture.end()
gesture.canvas.element.mousemove (e) ->
if e.button != 2
return
gesture.update_event e