showoff 0.13.4 → 0.14.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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
|