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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: e2c16f1de4dfa9e1401b2d8882200287df206d61
4
- data.tar.gz: e4db72901b96e5faf1c35ad018e60d3c29a14865
3
+ metadata.gz: 0a62e1cad01e674aaa210d8ec7130949db0965fb
4
+ data.tar.gz: 88a4bb776f6c82aa0ddc05efc73dc1379567bafb
5
5
  SHA512:
6
- metadata.gz: 95c4ef9e3f7d94060af10f68bf7a7b0188c99b034eefc431e75e26cf4943d7697ccbf1d6f8c3cc884af6f97f03534af19b04c05e2cc00f615d6e1c12826667ed
7
- data.tar.gz: c9c46ab3f68540dd773d7140c05d3e3cb8c23646e0a945ed7b0b5c455418e9dd9a10837f4bf0e33ae0bbe6bac72e8056944eaf03e29f43dc0f94a857d37a6f22
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[:url], options[:verbose]) do
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']
@@ -1,3 +1,3 @@
1
1
  # No namespace here since ShowOff is a class and I'd have to inherit from
2
2
  # Sinatra::Application (which we don't want to load here)
3
- SHOWOFF_VERSION = '0.13.4'
3
+ SHOWOFF_VERSION = '0.14.0'
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, verbose=false)
208
+ def self.clone(url, branch=nil, path=nil)
209
209
  require 'tmpdir'
210
210
  Dir.mktmpdir do |dir|
211
- Dir.chdir dir do
212
- puts "Cloning presentation repository to #{dir}..." if verbose
213
- system('git', 'clone', '--depth', '1', url, '.')
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
@@ -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: 1;
223
- flex: 1;
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;
@@ -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 > ul {
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
+ });
@@ -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().substr(0, 20).trim();
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="preview">
78
- <img id="disconnected" src="<%= @asset_path %>/css/disconnected-large.png" />
79
- <div id="preso">loading presentation...</div>
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.13.4
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-10 00:00:00.000000000 Z
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