showoff 0.13.4 → 0.14.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/bin/showoff +8 -2
- data/lib/showoff.rb +4 -0
- data/lib/showoff/version.rb +1 -1
- data/lib/showoff_utils.rb +8 -4
- data/public/css/presenter.css +65 -4
- data/public/css/showoff.css +31 -1
- data/public/js/annotations.js +279 -0
- data/public/js/presenter.js +81 -4
- data/public/js/showoff.js +87 -5
- data/views/header.erb +1 -0
- data/views/index.erb +2 -0
- data/views/presenter.erb +27 -3
- metadata +3 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 0a62e1cad01e674aaa210d8ec7130949db0965fb
|
4
|
+
data.tar.gz: 88a4bb776f6c82aa0ddc05efc73dc1379567bafb
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 7926f32b1b0c317bbd74355901e5cbe0ad9c8f827b1fb9401c13554ce5e90c2f73193ac83c87b42dfcfdc956a4968561389cc85c08ba55a25a2c869c720f63a0
|
7
|
+
data.tar.gz: 4accf69a75e69ce437ab85c81f6a1ca07ef22731f8d49ae2a434e3855b11297743d72aea711a3e90c54cfa26e8b122164b35c617e42068cfab51faf8c3f8a302
|
data/bin/showoff
CHANGED
@@ -176,7 +176,13 @@ command :serve do |c|
|
|
176
176
|
c.flag [:f, :file, :pres_file]
|
177
177
|
|
178
178
|
c.desc 'Git URL to a repository containing the presentation'
|
179
|
-
c.flag [:u, :url]
|
179
|
+
c.flag [:u, :url, :git_url]
|
180
|
+
|
181
|
+
c.desc 'Branch of the git repository to use'
|
182
|
+
c.flag [:git_branch]
|
183
|
+
|
184
|
+
c.desc 'Path of the presentation within the git repository'
|
185
|
+
c.flag [:git_path]
|
180
186
|
|
181
187
|
c.action do |global_options,options,args|
|
182
188
|
|
@@ -220,7 +226,7 @@ To run it from presenter view, go to: [ #{url}/presenter ]
|
|
220
226
|
"
|
221
227
|
|
222
228
|
if options[:url]
|
223
|
-
ShowOffUtils.clone(options[:
|
229
|
+
ShowOffUtils.clone(options[:git_url], options[:git_branch], options[:git_path]) do
|
224
230
|
ShowOff.run!(options) do |server|
|
225
231
|
if options[:ssl]
|
226
232
|
server.ssl = true
|
data/lib/showoff.rb
CHANGED
@@ -413,6 +413,7 @@ class ShowOff < Sinatra::Application
|
|
413
413
|
|
414
414
|
content += sl
|
415
415
|
content += "</div>\n"
|
416
|
+
content += "<canvas class=\"annotations\"></canvas>\n"
|
416
417
|
content += "</div>\n"
|
417
418
|
|
418
419
|
final += update_commandline_code(content)
|
@@ -1461,6 +1462,9 @@ class ShowOff < Sinatra::Application
|
|
1461
1462
|
when 'complete'
|
1462
1463
|
EM.next_tick { settings.sockets.each{|s| s.send(control.to_json) } }
|
1463
1464
|
|
1465
|
+
when 'annotation', 'annotationConfig'
|
1466
|
+
EM.next_tick { (settings.sockets - settings.presenters).each{|s| s.send(control.to_json) } }
|
1467
|
+
|
1464
1468
|
when 'feedback'
|
1465
1469
|
filename = "#{settings.statsdir}/#{settings.feedback}"
|
1466
1470
|
slide = control['slide']
|
data/lib/showoff/version.rb
CHANGED
data/lib/showoff_utils.rb
CHANGED
@@ -205,13 +205,17 @@ class ShowOffUtils
|
|
205
205
|
end
|
206
206
|
|
207
207
|
# clone a repo url, then run a provided block
|
208
|
-
def self.clone(url,
|
208
|
+
def self.clone(url, branch=nil, path=nil)
|
209
209
|
require 'tmpdir'
|
210
210
|
Dir.mktmpdir do |dir|
|
211
|
-
|
212
|
-
|
213
|
-
|
211
|
+
if branch
|
212
|
+
system('git', 'clone', '-b', branch, '--single-branch', '--depth', '1', url, dir)
|
213
|
+
else
|
214
|
+
system('git', 'clone', '--depth', '1', url, dir)
|
215
|
+
end
|
214
216
|
|
217
|
+
dir = File.join(dir, path) if path
|
218
|
+
Dir.chdir dir do
|
215
219
|
yield if block_given?
|
216
220
|
end
|
217
221
|
end
|
data/public/css/presenter.css
CHANGED
@@ -7,6 +7,7 @@
|
|
7
7
|
-webkit-flex-flow: column;
|
8
8
|
flex-flow: column;
|
9
9
|
min-width: 630px;
|
10
|
+
min-height: 675px;
|
10
11
|
}
|
11
12
|
|
12
13
|
#timerSection,
|
@@ -217,15 +218,22 @@
|
|
217
218
|
order: 1;
|
218
219
|
min-width: 0px;
|
219
220
|
}
|
221
|
+
#frame {
|
222
|
+
display: -webkit-flex;
|
223
|
+
display: flex;
|
224
|
+
-webkit-flex-flow: row;
|
225
|
+
flex-flow: row;
|
226
|
+
-webkit-flex: 10;
|
227
|
+
flex: 10;
|
228
|
+
}
|
220
229
|
|
221
230
|
#preview {
|
222
|
-
-webkit-flex:
|
223
|
-
flex:
|
231
|
+
-webkit-flex: 10;
|
232
|
+
flex: 10;
|
224
233
|
overflow: auto;
|
225
234
|
background: #eee;
|
226
235
|
padding: 1em;
|
227
236
|
}
|
228
|
-
|
229
237
|
#preview input[type=button].display {
|
230
238
|
display: inline;
|
231
239
|
}
|
@@ -245,6 +253,10 @@
|
|
245
253
|
visibility: visible;
|
246
254
|
color: #ccc;
|
247
255
|
}
|
256
|
+
#preview .inverse .incremental.hidden {
|
257
|
+
color: #404040;
|
258
|
+
}
|
259
|
+
|
248
260
|
|
249
261
|
#preso {
|
250
262
|
-webkit-box-shadow:0 0 25px rgba(0,0,0,0.35);
|
@@ -253,6 +265,8 @@
|
|
253
265
|
}
|
254
266
|
|
255
267
|
#statusbar {
|
268
|
+
-webkit-flex: 0.5;
|
269
|
+
flex: 0.5;
|
256
270
|
height: 22px;
|
257
271
|
line-height: 22px;
|
258
272
|
text-transform: uppercase;
|
@@ -267,7 +281,8 @@
|
|
267
281
|
display: inline;
|
268
282
|
}
|
269
283
|
#enableFollower,
|
270
|
-
#enableRemote
|
284
|
+
#enableRemote,
|
285
|
+
#enableAnnotations {
|
271
286
|
float: right;
|
272
287
|
margin: 1px;
|
273
288
|
padding: 0.1em;
|
@@ -279,6 +294,52 @@
|
|
279
294
|
box-shadow: 0px 0px 15px 5px rgba(255, 255, 190, .75);
|
280
295
|
}
|
281
296
|
|
297
|
+
#annotationToolbar {
|
298
|
+
display: -webkit-flex;
|
299
|
+
display: flex;
|
300
|
+
-webkit-flex-flow: column;
|
301
|
+
flex-flow: column;
|
302
|
+
-webkit-flex: 0.5;
|
303
|
+
flex: 0.5;
|
304
|
+
-webkit-order: 2;
|
305
|
+
order: 2;
|
306
|
+
min-width: 48px;
|
307
|
+
width: 48px;
|
308
|
+
padding: 0;
|
309
|
+
margin: 0;
|
310
|
+
background-color: black;
|
311
|
+
color: white;
|
312
|
+
text-align: center;
|
313
|
+
}
|
314
|
+
#annotationToolbar label {
|
315
|
+
font-size: 0.8em;
|
316
|
+
padding-top: 1em;
|
317
|
+
margin-bottom: 1px;
|
318
|
+
border-bottom: 1px solid #ccc;
|
319
|
+
}
|
320
|
+
#annotationToolbar i {
|
321
|
+
padding: 5px;
|
322
|
+
margin: 0 6px;
|
323
|
+
border-radius: 3px;
|
324
|
+
}
|
325
|
+
#annotationToolbar i:hover {
|
326
|
+
background-color: #555;
|
327
|
+
}
|
328
|
+
#annotationToolbar i.active {
|
329
|
+
background-color: #003d96;
|
330
|
+
border: 1px solid white;
|
331
|
+
}
|
332
|
+
#annotationToolbar i.lines.color1 { color: red }
|
333
|
+
#annotationToolbar i.lines.color2 { color: cyan }
|
334
|
+
#annotationToolbar i.lines.color3 { color: orange }
|
335
|
+
#annotationToolbar i.lines.color4 { color: green }
|
336
|
+
#annotationToolbar i.shapes.color1 { color: red }
|
337
|
+
#annotationToolbar i.shapes.color2 { color: cyan }
|
338
|
+
#annotationToolbar i.shapes.color3 { color: orange }
|
339
|
+
#annotationToolbar i.shapes.color4 { color: green }
|
340
|
+
|
341
|
+
|
342
|
+
|
282
343
|
#bottom {
|
283
344
|
display: -webkit-flex;
|
284
345
|
display: flex;
|
data/public/css/showoff.css
CHANGED
@@ -20,6 +20,11 @@ pre {
|
|
20
20
|
padding: .25em;
|
21
21
|
}
|
22
22
|
|
23
|
+
/* when code blocks are embedded in a table, we don't need giant whitespace */
|
24
|
+
table pre {
|
25
|
+
margin: 0;
|
26
|
+
}
|
27
|
+
|
23
28
|
/* applies to inline code only */
|
24
29
|
code {
|
25
30
|
padding: 2px 4px;
|
@@ -110,6 +115,16 @@ pre code {
|
|
110
115
|
max-width: 100%;
|
111
116
|
max-height: 100%;
|
112
117
|
}
|
118
|
+
|
119
|
+
/* set up annotation overlays */
|
120
|
+
.slide canvas.annotations {
|
121
|
+
position: absolute;
|
122
|
+
top: 0;
|
123
|
+
left: 0;
|
124
|
+
width: 100%;
|
125
|
+
height: 100%;
|
126
|
+
pointer-events: none;
|
127
|
+
}
|
113
128
|
}
|
114
129
|
|
115
130
|
/* plain (non-bullet) text */
|
@@ -167,6 +182,18 @@ pre code {
|
|
167
182
|
font-size: 70%;
|
168
183
|
}
|
169
184
|
|
185
|
+
/* inverse slide style */
|
186
|
+
.slide.inverse {
|
187
|
+
color: #efefef;
|
188
|
+
background-color: #262626;
|
189
|
+
}
|
190
|
+
.slide.inverse code {
|
191
|
+
color: black;
|
192
|
+
}
|
193
|
+
.slide.inverse h1:first-child {
|
194
|
+
border-bottom: 3px solid #efefef;
|
195
|
+
}
|
196
|
+
|
170
197
|
/**********************************
|
171
198
|
*** List styling ***
|
172
199
|
**********************************/
|
@@ -183,7 +210,7 @@ pre code {
|
|
183
210
|
}
|
184
211
|
|
185
212
|
/* ironically, normal lists have bullets and 'bullets' lists don't */
|
186
|
-
.bullets
|
213
|
+
.bullets ul {
|
187
214
|
list-style: none;
|
188
215
|
padding-left: 0px;
|
189
216
|
}
|
@@ -480,6 +507,9 @@ img#disconnected {
|
|
480
507
|
}
|
481
508
|
|
482
509
|
.navItem {
|
510
|
+
white-space: nowrap;
|
511
|
+
overflow: hidden;
|
512
|
+
text-overflow: ellipsis;
|
483
513
|
padding: .5em 1.5em;
|
484
514
|
}
|
485
515
|
|
@@ -0,0 +1,279 @@
|
|
1
|
+
function Annotate(params) {
|
2
|
+
if (!(this instanceof Annotate)) {
|
3
|
+
// the constructor was called without "new".
|
4
|
+
return new Annotate(params);;
|
5
|
+
}
|
6
|
+
params = typeof params !== 'undefined' ? params : {};
|
7
|
+
|
8
|
+
this.tool = 'draw';
|
9
|
+
this.context = null;
|
10
|
+
this.callbacks = null;
|
11
|
+
this.concurrent = 0;
|
12
|
+
this.imgData = null;
|
13
|
+
|
14
|
+
// I'd like this to be styled, but this is enough for MVP
|
15
|
+
this.lineWidth = params.lineWidth || 5;
|
16
|
+
this.lineColor = params.lineColor || "#df4b26";
|
17
|
+
this.fillColor = params.fillColor || "#F49638";
|
18
|
+
this.highlightRadius = params.highlightRadius || 20;
|
19
|
+
this.highlightPeriod = params.highlightPeriod || 500;
|
20
|
+
this.highlightFillColor = params.highlightFillColor || 'rgba(245, 213, 213, 0.5)';
|
21
|
+
this.highlightLineColor = params.highlightLineColor || '#cc0000';
|
22
|
+
this.zoom = params.zoom || 1; // Can you just die already?
|
23
|
+
|
24
|
+
this.setActiveCanvas = function(canvas) {
|
25
|
+
// dereference so we can use raw DOM objects or jQuery collections
|
26
|
+
canvas = canvas[0] || canvas;
|
27
|
+
if (canvas.nodeName.toLowerCase() !== 'canvas' ) {
|
28
|
+
throw new TypeError('Expected a DOM canvas element');
|
29
|
+
}
|
30
|
+
this.context = canvas.getContext("2d");
|
31
|
+
|
32
|
+
this.context.strokeStyle = this.lineColor;
|
33
|
+
this.context.fillStyle = this.fillColor;
|
34
|
+
this.context.lineWidth = this.lineWidth;
|
35
|
+
this.context.lineJoin = "round";
|
36
|
+
}
|
37
|
+
|
38
|
+
this.erase = function() {
|
39
|
+
if ( this.callbacks && this.callbacks['erase'] ) {
|
40
|
+
try {
|
41
|
+
this.callbacks['erase']();
|
42
|
+
} catch (e) {
|
43
|
+
console.log('Erase callback failed. ' + e);
|
44
|
+
}
|
45
|
+
}
|
46
|
+
|
47
|
+
this.context.clearRect(0, 0, this.context.canvas.width, this.context.canvas.height);
|
48
|
+
}
|
49
|
+
|
50
|
+
this.draw = function(x, y) {
|
51
|
+
if (this.tool == 'draw') {
|
52
|
+
// undo the effects of the zoom
|
53
|
+
x = x * this.zoom;
|
54
|
+
y = y * this.zoom;
|
55
|
+
|
56
|
+
|
57
|
+
if ( this.callbacks && this.callbacks['draw'] ) {
|
58
|
+
try {
|
59
|
+
this.callbacks['draw'](x, y);
|
60
|
+
} catch (e) {
|
61
|
+
console.log('Draw callback failed. ' + e);
|
62
|
+
}
|
63
|
+
}
|
64
|
+
|
65
|
+
this.context.strokeStyle = this.lineColor;
|
66
|
+
this.context.lineTo(x, y);
|
67
|
+
this.context.stroke();
|
68
|
+
}
|
69
|
+
}
|
70
|
+
|
71
|
+
this.click = function(x, y) {
|
72
|
+
// undo the effects of the zoom
|
73
|
+
x = x * this.zoom;
|
74
|
+
y = y * this.zoom;
|
75
|
+
|
76
|
+
if ( this.callbacks && this.callbacks['click'] ) {
|
77
|
+
try {
|
78
|
+
this.callbacks['click'](x, y);
|
79
|
+
} catch (e) {
|
80
|
+
console.log('Click callback failed. ' + e);
|
81
|
+
}
|
82
|
+
}
|
83
|
+
|
84
|
+
this.context.fillStyle = this.fillColor;
|
85
|
+
this.context.beginPath();
|
86
|
+
this.context.moveTo(x, y);
|
87
|
+
|
88
|
+
switch(this.tool) {
|
89
|
+
case 'leftArrow':
|
90
|
+
// IE doesn't understand Path2D
|
91
|
+
// var left = new Path2D('m'+x+','+y+' 40,-40 0,20 50,0 0,40 -50,0 0,20 -40,-40 z');
|
92
|
+
this.context.beginPath();
|
93
|
+
this.context.moveTo(x, y); x += 40; y -= 40;
|
94
|
+
this.context.lineTo(x, y); y += 20;
|
95
|
+
this.context.lineTo(x, y); x += 50;
|
96
|
+
this.context.lineTo(x, y); y += 40;
|
97
|
+
this.context.lineTo(x, y); x -= 50;
|
98
|
+
this.context.lineTo(x, y); y += 20;
|
99
|
+
this.context.lineTo(x, y); x -= 40; y -= 40;
|
100
|
+
|
101
|
+
this.context.fill();
|
102
|
+
break;
|
103
|
+
|
104
|
+
case 'rightArrow':
|
105
|
+
// var right = new Path2D('m'+x+','+y+' -40,-40 0,20 -50,0 0,40 50,0 0,20 40,-40 z');
|
106
|
+
this.context.beginPath();
|
107
|
+
this.context.moveTo(x, y); x -= 40; y -= 40;
|
108
|
+
this.context.lineTo(x, y); y += 20;
|
109
|
+
this.context.lineTo(x, y); x -= 50;
|
110
|
+
this.context.lineTo(x, y); y += 40;
|
111
|
+
this.context.lineTo(x, y); x += 50;
|
112
|
+
this.context.lineTo(x, y); y += 20;
|
113
|
+
this.context.lineTo(x, y); x += 40; y -= 40;
|
114
|
+
|
115
|
+
this.context.fill();
|
116
|
+
break;
|
117
|
+
|
118
|
+
case 'highlight':
|
119
|
+
// save the current state of the canvas so we can restore it
|
120
|
+
var width = this.context.canvas.width;
|
121
|
+
var height = this.context.canvas.height;
|
122
|
+
|
123
|
+
// save the canvas so we can restore it, but only if the user hasn't clicked multiple times.
|
124
|
+
if ( this.concurrent == 0 ) {
|
125
|
+
this.imgData = this.context.getImageData(0, 0, width, height);
|
126
|
+
}
|
127
|
+
this.concurrent += 1;
|
128
|
+
|
129
|
+
var period = this.highlightPeriod;
|
130
|
+
var start = null;
|
131
|
+
|
132
|
+
// Save the settings object so the animate() callback can get to it
|
133
|
+
var settings = this;
|
134
|
+
|
135
|
+
// can only accept a single timestamp argument
|
136
|
+
function animate(timestamp) {
|
137
|
+
if (!start) start = timestamp;
|
138
|
+
var progress = timestamp - start;
|
139
|
+
|
140
|
+
var linear = timestamp % period / period; // ranges from 0 to 1
|
141
|
+
var easing = Math.sin(linear * Math.PI); // simple easing to create some bounce
|
142
|
+
var radius = settings.highlightRadius * easing;
|
143
|
+
|
144
|
+
settings.context.clearRect(0, 0, width, height);
|
145
|
+
settings.context.beginPath();
|
146
|
+
settings.context.arc(x, y, radius, 0, Math.PI*2);
|
147
|
+
|
148
|
+
settings.context.fillStyle = settings.highlightFillColor;
|
149
|
+
settings.context.strokeStyle = settings.highlightLineColor;
|
150
|
+
|
151
|
+
settings.context.fill();
|
152
|
+
settings.context.stroke();
|
153
|
+
|
154
|
+
if (progress < 1000) {
|
155
|
+
window.requestAnimationFrame(animate);
|
156
|
+
}
|
157
|
+
else {
|
158
|
+
settings.concurrent -= 1;
|
159
|
+
if (settings.concurrent == 0) {
|
160
|
+
// We're done animating, restore the canvas
|
161
|
+
settings.context.clearRect(0, 0, width, height);
|
162
|
+
settings.context.putImageData(settings.imgData, 0, 0);
|
163
|
+
settings.context.strokeStyle = settings.lineColor;
|
164
|
+
settings.context.fillStyle = settings.fillColor;
|
165
|
+
settings.imgData = null;
|
166
|
+
}
|
167
|
+
}
|
168
|
+
}
|
169
|
+
window.requestAnimationFrame(animate);
|
170
|
+
break;
|
171
|
+
}
|
172
|
+
}
|
173
|
+
|
174
|
+
}
|
175
|
+
|
176
|
+
|
177
|
+
// Allow us to attach the annotations to canvases via jquery
|
178
|
+
// var annotations = new Annotate({lineColor: 'blue'});
|
179
|
+
// $('#overlay').annotate(annotations);
|
180
|
+
jQuery.fn.extend({
|
181
|
+
annotate: function (annotations) {
|
182
|
+
return this.each(function() {
|
183
|
+
if ( ! $(this).is( "canvas" ) ) {
|
184
|
+
throw new TypeError('The annotation functions only work on canvas elements');
|
185
|
+
}
|
186
|
+
if ( typeof annotations == 'undefined') {
|
187
|
+
// instantiate with defaults
|
188
|
+
annotations = new Annotate();
|
189
|
+
}
|
190
|
+
console.log('starting annotations');
|
191
|
+
var painting = false;
|
192
|
+
|
193
|
+
// the canvas cannot be css sized because reasons.
|
194
|
+
// Nor is it smart enough to understand how it fits into the styled page.
|
195
|
+
var height = $(this).parent().height();
|
196
|
+
var width = $(this).parent().width();
|
197
|
+
|
198
|
+
// We only want to do this the first time. It clears the canvas.
|
199
|
+
// Note that if the browser is resized, then the annotations are cleared.
|
200
|
+
if (this.height != height) {
|
201
|
+
this.height = height
|
202
|
+
}
|
203
|
+
if (this.width != width) {
|
204
|
+
this.width = width
|
205
|
+
}
|
206
|
+
|
207
|
+
annotations.setActiveCanvas(this);
|
208
|
+
|
209
|
+
// let the annotation overlay own mouse events.
|
210
|
+
// This means that clicking links or copying text will not work.
|
211
|
+
$(this).css('pointer-events', 'initial');
|
212
|
+
|
213
|
+
$(this).unbind( "mousedown" );
|
214
|
+
$(this).mousedown(function(e){
|
215
|
+
painting = true;
|
216
|
+
annotations.click(e.offsetX, e.offsetY)
|
217
|
+
});
|
218
|
+
|
219
|
+
$(this).unbind( "mouseup" );
|
220
|
+
$(this).mouseup(function(e){
|
221
|
+
painting = false;
|
222
|
+
});
|
223
|
+
|
224
|
+
$(this).unbind( "mouseleave" );
|
225
|
+
$(this).mouseleave(function(e){
|
226
|
+
painting = false;
|
227
|
+
});
|
228
|
+
|
229
|
+
$(this).unbind( "mouseleave" );
|
230
|
+
$(this).mousemove(function(e){
|
231
|
+
if(painting){
|
232
|
+
annotations.draw(e.offsetX, e.offsetY);
|
233
|
+
}
|
234
|
+
});
|
235
|
+
});
|
236
|
+
},
|
237
|
+
stopAnnotation: function () {
|
238
|
+
return this.each(function() {
|
239
|
+
if ( ! $(this).is( "canvas" ) ) {
|
240
|
+
throw new TypeError('The annotation functions only work on canvas elements');
|
241
|
+
}
|
242
|
+
// Ignore pointer events again to make the overlay inactive.
|
243
|
+
$(this).css('pointer-events', 'none');
|
244
|
+
|
245
|
+
$(this).unbind( "mousedown" );
|
246
|
+
$(this).unbind( "mouseup" );
|
247
|
+
$(this).unbind( "mouseleave" );
|
248
|
+
$(this).unbind( "mouseleave" );
|
249
|
+
});
|
250
|
+
},
|
251
|
+
annotationListener: function (settings) {
|
252
|
+
return this.each(function() {
|
253
|
+
if ( ! $(this).is( "canvas" ) ) {
|
254
|
+
throw new TypeError('The annotation functions only work on canvas elements');
|
255
|
+
}
|
256
|
+
if ( typeof settings == 'undefined') {
|
257
|
+
// instantiate with defaults
|
258
|
+
annotations = Annotate();
|
259
|
+
}
|
260
|
+
console.log('starting annotation listener');
|
261
|
+
|
262
|
+
// the canvas cannot be css sized because reasons.
|
263
|
+
height = $(this).parent().height();
|
264
|
+
width = $(this).parent().width();
|
265
|
+
|
266
|
+
// We only want to do this the first time. It clears the canvas.
|
267
|
+
// Note that if the browser is resized, then the annotations are cleared.
|
268
|
+
if (this.height != height) {
|
269
|
+
this.height = height
|
270
|
+
}
|
271
|
+
if (this.width != width) {
|
272
|
+
this.width = width
|
273
|
+
}
|
274
|
+
|
275
|
+
annotations.setActiveCanvas(this);
|
276
|
+
});
|
277
|
+
}
|
278
|
+
|
279
|
+
});
|
data/public/js/presenter.js
CHANGED
@@ -9,7 +9,7 @@ section = 'notes'; // which section the presenter has chosen to view
|
|
9
9
|
|
10
10
|
$(document).ready(function(){
|
11
11
|
// set up the presenter modes
|
12
|
-
mode = { track: true, follow: true, update: true, slave: false, next: false, notes: false};
|
12
|
+
mode = { track: true, follow: true, update: true, slave: false, next: false, notes: false, annotations: false};
|
13
13
|
|
14
14
|
// attempt to open another window for the presentation if the mode defaults
|
15
15
|
// to enabling this. It does not by default, so this is likely a no-op.
|
@@ -23,8 +23,8 @@ $(document).ready(function(){
|
|
23
23
|
$("#stopTimer").click(function() { stopTimer() });
|
24
24
|
|
25
25
|
/* zoom slide to match preview size, then set up resize handler. */
|
26
|
-
zoom();
|
27
|
-
$(window).resize(function() { zoom(); });
|
26
|
+
zoom(true);
|
27
|
+
$(window).resize(function() { zoom(true); });
|
28
28
|
|
29
29
|
$('#statslink').click(function(e) {
|
30
30
|
presenterPopupToggle('/stats', e);
|
@@ -54,13 +54,71 @@ $(document).ready(function(){
|
|
54
54
|
});
|
55
55
|
}
|
56
56
|
|
57
|
+
// Hide with js so jquery knows what display property to assign when showing
|
58
|
+
toggleAnnotations();
|
59
|
+
|
60
|
+
$('#annotationToolbar i.tool').click(function(e) {
|
61
|
+
var action = $(this).attr('data-action');
|
62
|
+
|
63
|
+
switch (action) {
|
64
|
+
case 'erase':
|
65
|
+
annotations.erase();
|
66
|
+
break;
|
67
|
+
|
68
|
+
default:
|
69
|
+
$('#annotationToolbar i.tool').removeClass('active');
|
70
|
+
$(this).addClass('active');
|
71
|
+
annotations.tool = action;
|
72
|
+
if (slaveWindow) slaveWindow.annotations.tool = action;
|
73
|
+
sendAnnotationConfig('tool', action);
|
74
|
+
}
|
75
|
+
|
76
|
+
});
|
77
|
+
|
78
|
+
$('#annotationToolbar i.lines').click(function(e) {
|
79
|
+
$('#annotationToolbar i.lines').removeClass('active');
|
80
|
+
$(this).addClass('active');
|
81
|
+
var color = $(this).css('color');
|
82
|
+
|
83
|
+
annotations.lineColor = color;
|
84
|
+
if (slaveWindow) slaveWindow.annotations.lineColor = color;
|
85
|
+
sendAnnotationConfig('lineColor', color);
|
86
|
+
});
|
87
|
+
|
88
|
+
$('#annotationToolbar i.shapes').click(function(e) {
|
89
|
+
$('#annotationToolbar i.shapes').removeClass('active');
|
90
|
+
$(this).addClass('active');
|
91
|
+
var color = $(this).css('color');
|
92
|
+
|
93
|
+
annotations.fillColor = color;
|
94
|
+
if (slaveWindow) slaveWindow.annotations.fillColor = color;
|
95
|
+
sendAnnotationConfig('fillColor', color);
|
96
|
+
});
|
97
|
+
|
57
98
|
$('#remoteToggle').change( toggleFollower );
|
58
99
|
$('#followerToggle').change( toggleUpdater );
|
100
|
+
$('#annotationsToggle').change( toggleAnnotations );
|
59
101
|
|
60
102
|
setInterval(function() { updatePace() }, 1000);
|
61
103
|
|
62
104
|
// Tell the showoff server that we're a presenter
|
63
105
|
register();
|
106
|
+
|
107
|
+
annotations.callbacks = {
|
108
|
+
erase: function() {
|
109
|
+
if (slaveWindow) slaveWindow.annotations.erase();
|
110
|
+
sendAnnotation('erase');
|
111
|
+
},
|
112
|
+
draw: function(x, y) {
|
113
|
+
if (slaveWindow) slaveWindow.annotations.draw(x,y);
|
114
|
+
sendAnnotation('draw', x, y);
|
115
|
+
},
|
116
|
+
click: function(x,y) {
|
117
|
+
if (slaveWindow) slaveWindow.annotations.click(x,y);
|
118
|
+
sendAnnotation('click', x, y);
|
119
|
+
}
|
120
|
+
};
|
121
|
+
|
64
122
|
});
|
65
123
|
|
66
124
|
function presenterPopupToggle(page, event) {
|
@@ -443,7 +501,7 @@ function postSlide() {
|
|
443
501
|
$(notesWindow.document.body).html(notes);
|
444
502
|
}
|
445
503
|
|
446
|
-
var fileName = currentSlide.children().first().attr('ref');
|
504
|
+
var fileName = currentSlide.children('div').first().attr('ref');
|
447
505
|
$('#slideFile').text(fileName);
|
448
506
|
|
449
507
|
$("#notes div.form.wrapper").each(function(e) {
|
@@ -641,3 +699,22 @@ function toggleUpdater()
|
|
641
699
|
mode.update = $("#followerToggle").prop("checked");
|
642
700
|
update();
|
643
701
|
}
|
702
|
+
|
703
|
+
/********************
|
704
|
+
Annotations
|
705
|
+
********************/
|
706
|
+
function toggleAnnotations()
|
707
|
+
{
|
708
|
+
mode.annotations = $("#annotationsToggle").prop("checked");
|
709
|
+
|
710
|
+
if(mode.annotations) {
|
711
|
+
$('#annotationToolbar').show();
|
712
|
+
currentSlide.find('canvas.annotations').annotate(annotations);
|
713
|
+
$('canvas.annotations').show();
|
714
|
+
}
|
715
|
+
else {
|
716
|
+
$('#annotationToolbar').hide();
|
717
|
+
$('canvas.annotations').stopAnnotation();
|
718
|
+
$('canvas.annotations').hide();
|
719
|
+
}
|
720
|
+
}
|
data/public/js/showoff.js
CHANGED
@@ -71,6 +71,9 @@ function setupPreso(load_slides, prefix) {
|
|
71
71
|
zoom();
|
72
72
|
$(window).resize(function() {zoom();});
|
73
73
|
|
74
|
+
// yes, this is a global
|
75
|
+
annotations = new Annotate();
|
76
|
+
|
74
77
|
// Open up our control socket
|
75
78
|
if(mode.track) {
|
76
79
|
connectControlChannel();
|
@@ -168,7 +171,7 @@ function initializePresentation(prefix) {
|
|
168
171
|
$("#preso").trigger("showoff:loaded");
|
169
172
|
}
|
170
173
|
|
171
|
-
function zoom() {
|
174
|
+
function zoom(presenter=false) {
|
172
175
|
var preso = $("#preso");
|
173
176
|
var hSlide = parseFloat(preso.height());
|
174
177
|
var wSlide = parseFloat(preso.width());
|
@@ -186,6 +189,21 @@ function zoom() {
|
|
186
189
|
// Don't use standard transform to avoid modifying Chrome
|
187
190
|
preso.css("-moz-transform", "scale(" + newZoom + ")");
|
188
191
|
preso.css("-moz-transform-origin", "0 0 0");
|
192
|
+
|
193
|
+
// correct the zoom factor for the presenter
|
194
|
+
if (presenter) {
|
195
|
+
// We only want to zoom if the canvas is actually zoomed. Firefox and IE
|
196
|
+
// should *not* be zoomed, so we want to exclude them. We do that by reading
|
197
|
+
// back the zoom property. It will return a string percentage in IE, which
|
198
|
+
// won't parse as a number, and Firefox simply returns undefined.
|
199
|
+
// Because reasons.
|
200
|
+
|
201
|
+
// TODO: When we fix the presenter on IE so the viewport isn't all wack, we
|
202
|
+
// may have to revisit this.
|
203
|
+
|
204
|
+
var zoomLevel = Number( preso.css('zoom') ) || 1;
|
205
|
+
annotations.zoom = 1 / zoomLevel
|
206
|
+
}
|
189
207
|
}
|
190
208
|
|
191
209
|
function setupSideMenu() {
|
@@ -252,6 +270,10 @@ function setupSideMenu() {
|
|
252
270
|
closeMenu();
|
253
271
|
});
|
254
272
|
|
273
|
+
$('#clearAnnotations').click(function() {
|
274
|
+
annotations.erase();
|
275
|
+
});
|
276
|
+
|
255
277
|
$('#closeMenu, #sidebarExit').click(function() {
|
256
278
|
closeMenu();
|
257
279
|
});
|
@@ -337,15 +359,19 @@ function setupMenu() {
|
|
337
359
|
// look for first header to use as a title
|
338
360
|
if (headers.length > 0) {
|
339
361
|
slideTitle = headers.first().text();
|
362
|
+
|
340
363
|
} else {
|
341
|
-
// if no header, look at content
|
364
|
+
// if no header, look at the first non-empty line of content
|
342
365
|
content = $(slide).find(".content");
|
343
|
-
slideTitle = content.text().
|
366
|
+
slideTitle = content.text().split("\n").filter(Boolean)[0] || ''; // split() gives us an empty array when there's no content.
|
344
367
|
|
345
368
|
// if no content (like photo only) fall back to slide name
|
346
369
|
if (slideTitle == "") {
|
347
370
|
slideTitle = content.attr('ref').split('/').pop();
|
348
371
|
}
|
372
|
+
|
373
|
+
// just in case we've got any extra whitespace around.
|
374
|
+
slideTitle = slideTitle.trim();
|
349
375
|
}
|
350
376
|
|
351
377
|
var navLink = $("<a>")
|
@@ -443,6 +469,11 @@ function showSlide(back_step, updatepv) {
|
|
443
469
|
return
|
444
470
|
}
|
445
471
|
|
472
|
+
// stop annotations on old slide if we're a presenter
|
473
|
+
if(currentSlide && typeof slaveWindow !== 'undefined') {
|
474
|
+
currentSlide.find('canvas.annotations').first().stopAnnotation();
|
475
|
+
}
|
476
|
+
|
446
477
|
currentSlide = slides.eq(slidenum)
|
447
478
|
|
448
479
|
var transition = currentSlide.attr('data-transition')
|
@@ -481,13 +512,24 @@ function showSlide(back_step, updatepv) {
|
|
481
512
|
|
482
513
|
var ret = setCurrentNotes();
|
483
514
|
|
484
|
-
var fileName = currentSlide.children().first().attr('ref');
|
515
|
+
var fileName = currentSlide.children('div').first().attr('ref');
|
485
516
|
$('#slideFilename').text(fileName);
|
486
517
|
|
487
518
|
if (query.next) {
|
488
519
|
$(currentSlide).find('li').removeClass('hidden');
|
489
520
|
}
|
490
521
|
|
522
|
+
if(typeof slaveWindow == 'undefined') {
|
523
|
+
// hook up the annotations for viewing
|
524
|
+
currentSlide.find('canvas.annotations').annotationListener(annotations);
|
525
|
+
}
|
526
|
+
else {
|
527
|
+
if (mode.annotations) {
|
528
|
+
currentSlide.find('canvas.annotations').annotate(annotations);
|
529
|
+
}
|
530
|
+
}
|
531
|
+
|
532
|
+
|
491
533
|
// Update presenter view, if we spawned one
|
492
534
|
if (updatepv && 'presenterView' in window && ! mode.next) {
|
493
535
|
var pv = window.presenterView;
|
@@ -861,10 +903,22 @@ function parseMessage(data) {
|
|
861
903
|
case 'cancel':
|
862
904
|
removeQuestion(command["questionID"]);
|
863
905
|
break;
|
906
|
+
|
907
|
+
case 'annotation':
|
908
|
+
invokeAnnotation(command["type"], command["x"], command["y"]);
|
909
|
+
break;
|
910
|
+
|
911
|
+
case 'annotationConfig':
|
912
|
+
setting = command['setting'];
|
913
|
+
value = command['value'];
|
914
|
+
|
915
|
+
annotations[setting] = value;
|
916
|
+
break;
|
917
|
+
|
864
918
|
}
|
865
919
|
}
|
866
920
|
catch(e) {
|
867
|
-
console.log("Not a presenter!");
|
921
|
+
console.log("Not a presenter! " + e);
|
868
922
|
}
|
869
923
|
|
870
924
|
}
|
@@ -905,6 +959,34 @@ function sendFeedback(rating, feedback) {
|
|
905
959
|
}
|
906
960
|
}
|
907
961
|
|
962
|
+
function sendAnnotation(type, x, y) {
|
963
|
+
if (ws.readyState == WebSocket.OPEN) {
|
964
|
+
ws.send(JSON.stringify({ message: 'annotation', type: type, x: x, y: y }));
|
965
|
+
}
|
966
|
+
}
|
967
|
+
|
968
|
+
function sendAnnotationConfig(setting, value) {
|
969
|
+
if (ws.readyState == WebSocket.OPEN) {
|
970
|
+
ws.send(JSON.stringify({ message: 'annotationConfig', setting: setting, value: value }));
|
971
|
+
}
|
972
|
+
}
|
973
|
+
|
974
|
+
function invokeAnnotation(type, x, y) {
|
975
|
+
switch (type) {
|
976
|
+
case 'erase':
|
977
|
+
annotations.erase();
|
978
|
+
break;
|
979
|
+
|
980
|
+
case 'draw':
|
981
|
+
annotations.draw(x,y);
|
982
|
+
break;
|
983
|
+
|
984
|
+
case 'click':
|
985
|
+
annotations.click(x,y);
|
986
|
+
break;
|
987
|
+
}
|
988
|
+
}
|
989
|
+
|
908
990
|
function feedbackActivity() {
|
909
991
|
$('#hamburger').addClass('highlight');
|
910
992
|
setTimeout(function() { $("#hamburger").removeClass('highlight') }, 75);
|
data/views/header.erb
CHANGED
@@ -23,6 +23,7 @@
|
|
23
23
|
|
24
24
|
<script type="text/javascript" src="<%= @asset_path %>/js/coffee-script.js"></script>
|
25
25
|
|
26
|
+
<script type="text/javascript" src="<%= @asset_path %>/js/annotations.js"></script>
|
26
27
|
<script type="text/javascript" src="<%= @asset_path %>/js/showoff.js"></script>
|
27
28
|
|
28
29
|
<% css_files.each do |css_file| %>
|
data/views/index.erb
CHANGED
@@ -63,6 +63,8 @@
|
|
63
63
|
<div id="editSlide" class="buttonWrapper">Edit Current Slide</div>
|
64
64
|
<% end %>
|
65
65
|
<hr>
|
66
|
+
<div id="clearAnnotations" class="buttonWrapper"><i class="fa fa-eraser"></i> Clear Annotations</div>
|
67
|
+
<hr>
|
66
68
|
<% end %>
|
67
69
|
|
68
70
|
<div id="closeMenu" class="buttonWrapper"><i class="fa fa-close"></i> Close</div>
|
data/views/presenter.erb
CHANGED
@@ -74,9 +74,29 @@
|
|
74
74
|
<div id="navigation" class="submenu"></div>
|
75
75
|
</div>
|
76
76
|
<div id="presenter">
|
77
|
-
<div id="
|
78
|
-
<
|
79
|
-
|
77
|
+
<div id="frame">
|
78
|
+
<div id="preview">
|
79
|
+
<img id="disconnected" src="<%= @asset_path %>/css/disconnected-large.png" />
|
80
|
+
<div id="preso">loading presentation...</div>
|
81
|
+
</div>
|
82
|
+
<div id="annotationToolbar">
|
83
|
+
<label>Tools</label>
|
84
|
+
<i class="fa fa-pencil tool default active" data-action="draw" aria-hidden="true"></i>
|
85
|
+
<i class="fa fa-arrow-right tool" data-action="rightArrow" aria-hidden="true"></i>
|
86
|
+
<i class="fa fa-arrow-left tool" data-action="leftArrow" aria-hidden="true"></i>
|
87
|
+
<i class="fa fa-bullseye tool" data-action="highlight" aria-hidden="true"></i>
|
88
|
+
<i class="fa fa-eraser tool" data-action="erase" aria-hidden="true"></i>
|
89
|
+
<label>Lines</label>
|
90
|
+
<i class="fa fa-square-o lines color1 active" aria-hidden="true"></i>
|
91
|
+
<i class="fa fa-square-o lines color2" aria-hidden="true"></i>
|
92
|
+
<i class="fa fa-square-o lines color3" aria-hidden="true"></i>
|
93
|
+
<i class="fa fa-square-o lines color4" aria-hidden="true"></i>
|
94
|
+
<label>Shapes</label>
|
95
|
+
<i class="fa fa-square shapes color1" aria-hidden="true"></i>
|
96
|
+
<i class="fa fa-square shapes color2" aria-hidden="true"></i>
|
97
|
+
<i class="fa fa-square shapes color3 active" aria-hidden="true"></i>
|
98
|
+
<i class="fa fa-square shapes color4" aria-hidden="true"></i>
|
99
|
+
</div>
|
80
100
|
</div>
|
81
101
|
<div id="statusbar">
|
82
102
|
<span id="progress">
|
@@ -89,6 +109,10 @@
|
|
89
109
|
<span id="enableFollower" title="Send slide change notifications.">
|
90
110
|
<label for="followerToggle">Update Follower</label><input type="checkbox" id="followerToggle" checked />
|
91
111
|
</span>
|
112
|
+
<span id="enableAnnotations" title="Enable the annotation system.">
|
113
|
+
<label for="annotationsToggle">Annotations</label><input type="checkbox" id="annotationsToggle" />
|
114
|
+
</span>
|
115
|
+
|
92
116
|
</div>
|
93
117
|
</div>
|
94
118
|
</div>
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: showoff
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.14.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Scott Chacon
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2016-08-
|
11
|
+
date: 2016-08-25 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: sinatra
|
@@ -326,6 +326,7 @@ files:
|
|
326
326
|
- public/css/titlebar/right.png
|
327
327
|
- public/favicon.ico
|
328
328
|
- public/js/TimeCircles.js
|
329
|
+
- public/js/annotations.js
|
329
330
|
- public/js/coffee-script.js
|
330
331
|
- public/js/highlight.pack.js
|
331
332
|
- public/js/jTypeWriter.js
|