blob: 76b736e28e70c7e1fac7d41d5a13b9d3031f8581 [file] [log] [blame]
Christophe Dervieuxe1893ae2021-10-07 17:09:02 +02001/*****************************************************************
2 ** Author: Asvin Goel, goel@telematique.eu
3 **
4 ** A plugin for reveal.js adding a chalkboard.
5 **
6 ** Version: 2.1.0
7 **
8 ** License: MIT license (see LICENSE.md)
9 **
10 ** Credits:
11 ** Chalkboard effect by Mohamed Moustafa https://github.com/mmoustafa/Chalkboard
12 ** Multi color support initially added by Kurt Rinnert https://github.com/rinnert
13 ** Compatibility with reveal.js v4 by Hakim El Hattab https://github.com/hakimel
14 ******************************************************************/
15
16window.RevealChalkboard = window.RevealChalkboard || {
17 id: 'RevealChalkboard',
18 init: function ( deck ) {
19 initChalkboard( deck );
20 },
21 configure: function ( config ) {
22 configure( config );
23 },
24 toggleNotesCanvas: function () {
25 toggleNotesCanvas();
26 },
27 toggleChalkboard: function () {
28 toggleChalkboard();
29 },
30 colorIndex: function () {
31 colorIndex();
32 },
33 colorNext: function () {
34 colorNext();
35 },
36 colorPrev: function () {
37 colorPrev();
38 },
39 clear: function () {
40 clear();
41 },
42 reset: function () {
43 reset();
44 },
45 resetAll: function () {
46 resetAll();
47 },
48 updateStorage: function () {
49 updateStorage();
50 },
51 getData: function () {
52 return getData();
53 },
54 download: function () {
55 download();
56 },
57};
58
59function scriptPath() {
60 // obtain plugin path from the script element
61 var src;
62 if ( document.currentScript ) {
63 src = document.currentScript.src;
64 } else {
65 var sel = document.querySelector( 'script[src$="/chalkboard/plugin.js"]' )
66 if ( sel ) {
67 src = sel.src;
68 }
69 }
70 var path = ( src === undefined ) ? "" : src.slice( 0, src.lastIndexOf( "/" ) + 1 );
71//console.log("Path: " + path);
72 return path;
73}
74var path = scriptPath();
75
76const initChalkboard = function ( Reveal ) {
77//console.warn(path);
78 /* Feature detection for passive event handling*/
79 var passiveSupported = false;
80
81 try {
82 window.addEventListener( 'test', null, Object.defineProperty( {}, 'passive', {
83 get: function () {
84 passiveSupported = true;
85 }
86 } ) );
87 } catch ( err ) {}
88
89
90/*****************************************************************
91 ** Configuration
92 ******************************************************************/
93 var background, pen, draw, color;
94 var grid = false;
95 var boardmarkerWidth = 3;
96 var chalkWidth = 7;
97 var chalkEffect = 1.0;
98 var rememberColor = [ true, false ];
99 var eraser = {
100 src: path + 'img/sponge.png',
101 radius: 20
102 };
103 var boardmarkers = [ {
104 color: 'rgba(100,100,100,1)',
105 cursor: 'url(' + path + 'img/boardmarker-black.png), auto'
106 },
107 {
108 color: 'rgba(30,144,255, 1)',
109 cursor: 'url(' + path + 'img/boardmarker-blue.png), auto'
110 },
111 {
112 color: 'rgba(220,20,60,1)',
113 cursor: 'url(' + path + 'img/boardmarker-red.png), auto'
114 },
115 {
116 color: 'rgba(50,205,50,1)',
117 cursor: 'url(' + path + 'img/boardmarker-green.png), auto'
118 },
119 {
120 color: 'rgba(255,140,0,1)',
121 cursor: 'url(' + path + 'img/boardmarker-orange.png), auto'
122 },
123 {
124 color: 'rgba(150,0,20150,1)',
125 cursor: 'url(' + path + 'img/boardmarker-purple.png), auto'
126 },
127 {
128 color: 'rgba(255,220,0,1)',
129 cursor: 'url(' + path + 'img/boardmarker-yellow.png), auto'
130 }
131 ];
132 var chalks = [ {
133 color: 'rgba(255,255,255,0.5)',
134 cursor: 'url(' + path + 'img/chalk-white.png), auto'
135 },
136 {
137 color: 'rgba(96, 154, 244, 0.5)',
138 cursor: 'url(' + path + 'img/chalk-blue.png), auto'
139 },
140 {
141 color: 'rgba(237, 20, 28, 0.5)',
142 cursor: 'url(' + path + 'img/chalk-red.png), auto'
143 },
144 {
145 color: 'rgba(20, 237, 28, 0.5)',
146 cursor: 'url(' + path + 'img/chalk-green.png), auto'
147 },
148 {
149 color: 'rgba(220, 133, 41, 0.5)',
150 cursor: 'url(' + path + 'img/chalk-orange.png), auto'
151 },
152 {
153 color: 'rgba(220,0,220,0.5)',
154 cursor: 'url(' + path + 'img/chalk-purple.png), auto'
155 },
156 {
157 color: 'rgba(255,220,0,0.5)',
158 cursor: 'url(' + path + 'img/chalk-yellow.png), auto'
159 }
160 ];
161 var keyBindings = {
162 toggleNotesCanvas: {
163 keyCode: 67,
164 key: 'C',
165 description: 'Toggle notes canvas'
166 },
167 toggleChalkboard: {
168 keyCode: 66,
169 key: 'B',
170 description: 'Toggle chalkboard'
171 },
172 clear: {
173 keyCode: 46,
174 key: 'DEL',
175 description: 'Clear drawings on slide'
176 },
177/*
178 reset: {
179 keyCode: 173,
180 key: '-',
181 description: 'Reset drawings on slide'
182 },
183*/
184 resetAll: {
185 keyCode: 8,
186 key: 'BACKSPACE',
187 description: 'Reset all drawings'
188 },
189 colorNext: {
190 keyCode: 88,
191 key: 'X',
192 description: 'Next color'
193 },
194 colorPrev: {
195 keyCode: 89,
196 key: 'Y',
197 description: 'Previous color'
198 },
199 download: {
200 keyCode: 68,
201 key: 'D',
202 description: 'Download drawings'
203 }
204 };
205
206
207 var theme = 'chalkboard';
208 var color = [ 0, 0 ];
209 var toggleChalkboardButton = false;
210 var toggleNotesButton = false;
211 var colorButtons = true;
212 var boardHandle = true;
213 var transition = 800;
214
215 var readOnly = false;
216 var messageType = 'broadcast';
217
218 var config = configure( Reveal.getConfig().chalkboard || {} );
219 if ( config.keyBindings ) {
220 for ( var key in config.keyBindings ) {
221 keyBindings[ key ] = config.keyBindings[ key ];
222 };
223 }
224
225 function configure( config ) {
226
227 if ( config.boardmarkerWidth || config.penWidth ) boardmarkerWidth = config.boardmarkerWidth || config.penWidth;
228 if ( config.chalkWidth ) chalkWidth = config.chalkWidth;
229 if ( config.chalkEffect ) chalkEffect = config.chalkEffect;
230 if ( config.rememberColor ) rememberColor = config.rememberColor;
231 if ( config.eraser ) eraser = config.eraser;
232 if ( config.boardmarkers ) boardmarkers = config.boardmarkers;
233 if ( config.chalks ) chalks = config.chalks;
234
235 if ( config.theme ) theme = config.theme;
236 switch ( theme ) {
237 case 'whiteboard':
238 background = [ 'rgba(127,127,127,.1)', path + 'img/whiteboard.png' ];
239 draw = [ drawWithBoardmarker, drawWithBoardmarker ];
240 pens = [ boardmarkers, boardmarkers ];
241 grid = {
242 color: 'rgb(127,127,255,0.1)',
243 distance: 40,
244 width: 2
245 };
246 break;
247 case 'chalkboard':
248 default:
249 background = [ 'rgba(127,127,127,.1)', path + 'img/blackboard.png' ];
250 draw = [ drawWithBoardmarker, drawWithChalk ];
251 pens = [ boardmarkers, chalks ];
252 grid = {
253 color: 'rgb(50,50,10,0.5)',
254 distance: 80,
255 width: 2
256 };
257 }
258
259 if ( config.background ) background = config.background;
260 if ( config.grid != undefined ) grid = config.grid;
261
262 if ( config.toggleChalkboardButton != undefined ) toggleChalkboardButton = config.toggleChalkboardButton;
263 if ( config.toggleNotesButton != undefined ) toggleNotesButton = config.toggleNotesButton;
264 if ( config.colorButtons != undefined ) colorButtons = config.colorButtons;
265 if ( config.boardHandle != undefined ) boardHandle = config.boardHandle;
266 if ( config.transition ) transition = config.transition;
267
268 if ( config.readOnly != undefined ) readOnly = config.readOnly;
269 if ( config.messageType ) messageType = config.messageType;
270
271 if ( drawingCanvas && ( config.theme || config.background || config.grid ) ) {
272 var canvas = document.getElementById( drawingCanvas[ 1 ].id );
273 canvas.style.background = 'url("' + background[ 1 ] + '") repeat';
274 clearCanvas( 1 );
275 drawGrid();
276 }
277
278 return config;
279 }
280/*****************************************************************
281 ** Setup
282 ******************************************************************/
283
284 function whenReady( callback ) {
285 // wait for markdown to be parsed and code to be highlighted
286 if ( !document.querySelector( 'section[data-markdown]:not([data-markdown-parsed])' )
287 && !document.querySelector( 'code[data-line-numbers*="|"]')
288 ) {
289 callback();
290 } else {
291 console.log( "Wait for markdown to be parsed and code to be highlighted" );
292 setTimeout( whenReady, 500, callback )
293 }
294 }
295
296 function whenLoaded( callback ) {
297 // wait for drawings to be loaded and markdown to be parsed
298 if ( loaded !== null ) {
299 callback();
300 } else {
301 console.log( "Wait for drawings to be loaded" );
302 setTimeout( whenLoaded, 500, callback )
303 }
304 }
305
306 if ( toggleChalkboardButton ) {
307console.warn( "toggleChalkboardButton is deprecated, use customcontrols plugin instead!" );
308//console.log("toggleChalkboardButton")
309 var button = document.createElement( 'div' );
310 button.className = "chalkboard-button";
311 button.id = "toggle-chalkboard";
312 button.style.visibility = "visible";
313 button.style.position = "absolute";
314 button.style.zIndex = 30;
315 button.style.fontSize = "24px";
316
317 button.style.left = toggleChalkboardButton.left || "30px";
318 button.style.bottom = toggleChalkboardButton.bottom || "30px";
319 button.style.top = toggleChalkboardButton.top || "auto";
320 button.style.right = toggleChalkboardButton.right || "auto";
321
322 button.innerHTML = '<a href="#" title="Toggle chalkboard (' + keyBindings.toggleChalkboard.key + ')" onclick="RevealChalkboard.toggleChalkboard(); return false;"><i class="fa fa-pen-square"></i></a>'
323 document.querySelector( ".reveal" ).appendChild( button );
324 }
325 if ( toggleNotesButton ) {
326console.warn( "toggleNotesButton is deprecated, use customcontrols plugin instead!" );
327//console.log("toggleNotesButton")
328 var button = document.createElement( 'div' );
329 button.className = "chalkboard-button";
330 button.id = "toggle-notes";
331 button.style.position = "absolute";
332 button.style.zIndex = 30;
333 button.style.fontSize = "24px";
334
335 button.style.left = toggleNotesButton.left || "70px";
336 button.style.bottom = toggleNotesButton.bottom || "30px";
337 button.style.top = toggleNotesButton.top || "auto";
338 button.style.right = toggleNotesButton.right || "auto";
339
340 button.innerHTML = '<a href="#" title="Toggle slide annotation (' + keyBindings.toggleNotesCanvas.key + ')" onclick="RevealChalkboard.toggleNotesCanvas(); return false;"><i class="fa fa-pen"></i></a>'
341 document.querySelector( ".reveal" ).appendChild( button );
342 }
343
344 var drawingCanvas = [ {
345 id: 'notescanvas'
346 }, {
347 id: 'chalkboard'
348 } ];
349 setupDrawingCanvas( 0 );
350 setupDrawingCanvas( 1 );
351
352 var mode = 0; // 0: notes canvas, 1: chalkboard
353 var board = 0; // board index (only for chalkboard)
354
355 var mouseX = 0;
356 var mouseY = 0;
357 var lastX = null;
358 var lastY = null;
359
360 var drawing = false;
361 var erasing = false;
362
363 var slideStart = Date.now();
364 var slideIndices = {
365 h: 0,
366 v: 0
367 };
368
369 var timeouts = [
370 [],
371 []
372 ];
373 var touchTimeout = null;
374 var slidechangeTimeout = null;
375 var updateStorageTimeout = null;
376 var playback = false;
377
378 function createPalette( colors, length ) {
379 if ( length === true || length > colors.length ) {
380 length = colors.length;
381 }
382 var palette = document.createElement( 'div' );
383 palette.classList.add( 'palette' );
384 var list = document.createElement( 'ul' );
385 // color pickers
386 for ( var i = 0; i < length; i++ ) {
387 var colorButton = document.createElement( 'li' );
388 colorButton.setAttribute( 'data-color', i );
389 colorButton.innerHTML = '<i class="fa fa-square"></i>';
390 colorButton.style.color = colors[ i ].color;
391 colorButton.addEventListener( 'click', function ( e ) {
392 var element = e.target;
393 while ( !element.hasAttribute( 'data-color' ) ) {
394 element = element.parentElement;
395 }
396 colorIndex( parseInt( element.getAttribute( 'data-color' ) ) );
397 } );
398 colorButton.addEventListener( 'touchstart', function ( e ) {
399 var element = e.target;
400 while ( !element.hasAttribute( 'data-color' ) ) {
401 element = element.parentElement;
402 }
403 colorIndex( parseInt( element.getAttribute( 'data-color' ) ) );
404 } );
405 list.appendChild( colorButton );
406 }
407 palette.appendChild( list );
408 return palette;
409 };
410
411 function switchBoard( boardIdx ) {
412 selectBoard( boardIdx, true );
413 // broadcast
414 var message = new CustomEvent( messageType );
415 message.content = {
416 sender: 'chalkboard-plugin',
417 type: 'selectboard',
418 timestamp: Date.now() - slideStart,
419 mode,
420 board
421 };
422 document.dispatchEvent( message );
423 }
424
425 function setupDrawingCanvas( id ) {
426 var container = document.createElement( 'div' );
427 container.id = drawingCanvas[ id ].id;
428 container.classList.add( 'overlay' );
429 container.setAttribute( 'data-prevent-swipe', 'true' );
430 container.oncontextmenu = function () {
431 return false;
432 }
433 container.style.cursor = pens[ id ][ color[ id ] ].cursor;
434
435 drawingCanvas[ id ].width = window.innerWidth;
436 drawingCanvas[ id ].height = window.innerHeight;
437 drawingCanvas[ id ].scale = 1;
438 drawingCanvas[ id ].xOffset = 0;
439 drawingCanvas[ id ].yOffset = 0;
440
441 if ( id == "0" ) {
442 container.style.background = 'rgba(0,0,0,0)';
443 container.style.zIndex = 24;
444 container.style.opacity = 1;
445 container.style.visibility = 'visible';
446 container.style.pointerEvents = 'none';
447
448 var slides = document.querySelector( '.slides' );
449 var aspectRatio = Reveal.getConfig().width / Reveal.getConfig().height;
450 if ( drawingCanvas[ id ].width > drawingCanvas[ id ].height * aspectRatio ) {
451 drawingCanvas[ id ].xOffset = ( drawingCanvas[ id ].width - drawingCanvas[ id ].height * aspectRatio ) / 2;
452 } else if ( drawingCanvas[ id ].height > drawingCanvas[ id ].width / aspectRatio ) {
453 drawingCanvas[ id ].yOffset = ( drawingCanvas[ id ].height - drawingCanvas[ id ].width / aspectRatio ) / 2;
454 }
455
456 if ( colorButtons ) {
457 var palette = createPalette( boardmarkers, colorButtons );
458 palette.style.visibility = 'hidden'; // only show palette in drawing mode
459 container.appendChild( palette );
460 }
461 } else {
462 container.style.background = 'url("' + background[ id ] + '") repeat';
463 container.style.zIndex = 26;
464 container.style.opacity = 0;
465 container.style.visibility = 'hidden';
466
467 if ( colorButtons ) {
468 var palette = createPalette( chalks, colorButtons );
469 container.appendChild( palette );
470 }
471 if ( boardHandle ) {
472 var handle = document.createElement( 'div' );
473 handle.classList.add( 'boardhandle' );
474 handle.innerHTML = '<ul><li><a id="previousboard" href="#" title="Previous board"><i class="fas fa-chevron-up"></i></a></li><li><a id="nextboard" href="#" title="Next board"><i class="fas fa-chevron-down"></i></a></li></ul>';
475 handle.querySelector( '#previousboard' ).addEventListener( 'click', function ( e ) {
476 e.preventDefault();
477 switchBoard( board - 1 );
478 } );
479 handle.querySelector( '#nextboard' ).addEventListener( 'click', function ( e ) {
480 e.preventDefault();
481 switchBoard( board + 1 );
482 } );
483 handle.querySelector( '#previousboard' ).addEventListener( 'touchstart', function ( e ) {
484 e.preventDefault();
485 switchBoard( board - 1 );
486 } );
487 handle.querySelector( '#nextboard' ).addEventListener( 'touchstart', function ( e ) {
488 e.preventDefault();
489 switchBoard( board + 1 );
490 } );
491
492 container.appendChild( handle );
493 }
494 }
495
496
497 var sponge = document.createElement( 'img' );
498 sponge.src = eraser.src;
499 sponge.id = 'sponge';
500 sponge.style.visibility = 'hidden';
501 sponge.style.position = 'absolute';
502 container.appendChild( sponge );
503 drawingCanvas[ id ].sponge = sponge;
504
505 var canvas = document.createElement( 'canvas' );
506 canvas.width = drawingCanvas[ id ].width;
507 canvas.height = drawingCanvas[ id ].height;
508 canvas.setAttribute( 'data-chalkboard', id );
509 canvas.style.cursor = pens[ id ][ color[ id ] ].cursor;
510 container.appendChild( canvas );
511 drawingCanvas[ id ].canvas = canvas;
512
513 drawingCanvas[ id ].context = canvas.getContext( '2d' );
514
515 setupCanvasEvents( container );
516
517 document.querySelector( '.reveal' ).appendChild( container );
518 drawingCanvas[ id ].container = container;
519 }
520
521
522/*****************************************************************
523 ** Storage
524 ******************************************************************/
525
526 var storage = [ {
527 width: Reveal.getConfig().width,
528 height: Reveal.getConfig().height,
529 data: []
530 },
531 {
532 width: Reveal.getConfig().width,
533 height: Reveal.getConfig().height,
534 data: []
535 }
536 ];
537
538 var loaded = null;
539
540 if ( config.storage ) {
541 // Get chalkboard drawings from session storage
542 loaded = initStorage( sessionStorage.getItem( config.storage ) );
543 }
544
545 if ( !loaded && config.src != null ) {
546 // Get chalkboard drawings from the given file
547 loadData( config.src );
548 }
549
550 /**
551 * Initialize storage.
552 */
553 function initStorage( json ) {
554 var success = false;
555 try {
556 var data = JSON.parse( json );
557 for ( var id = 0; id < data.length; id++ ) {
558 if ( drawingCanvas[ id ].width != data[ id ].width || drawingCanvas[ id ].height != data[ id ].height ) {
559 drawingCanvas[ id ].scale = Math.min( drawingCanvas[ id ].width / data[ id ].width, drawingCanvas[ id ].height / data[ id ].height );
560 drawingCanvas[ id ].xOffset = ( drawingCanvas[ id ].width - data[ id ].width * drawingCanvas[ id ].scale ) / 2;
561 drawingCanvas[ id ].yOffset = ( drawingCanvas[ id ].height - data[ id ].height * drawingCanvas[ id ].scale ) / 2;
562 }
563 if ( config.readOnly ) {
564 drawingCanvas[ id ].container.style.cursor = 'default';
565 drawingCanvas[ id ].canvas.style.cursor = 'default';
566 }
567 }
568 success = true;
569 storage = data;
570 } catch ( err ) {
571 console.warn( "Cannot initialise storage!" );
572 }
573 return success;
574 }
575
576
577 /**
578 * Load data.
579 */
580 function loadData( filename ) {
581 var xhr = new XMLHttpRequest();
582 xhr.onload = function () {
583 if ( xhr.readyState === 4 && xhr.status != 404 ) {
584 loaded = initStorage( xhr.responseText );
585 updateStorage();
586 console.log( "Drawings loaded from file" );
587 } else {
588 config.readOnly = undefined;
589 readOnly = undefined;
590 console.warn( 'Failed to get file ' + filename + '. ReadyState: ' + xhr.readyState + ', Status: ' + xhr.status );
591 loaded = false;
592 }
593 };
594
595 xhr.open( 'GET', filename, true );
596 try {
597 xhr.send();
598 } catch ( error ) {
599 config.readOnly = undefined;
600 readOnly = undefined;
601 console.warn( 'Failed to get file ' + filename + '. Make sure that the presentation and the file are served by a HTTP server and the file can be found there. ' + error );
602 loaded = false;
603 }
604 }
605
606
607 function storageChanged( now ) {
608 if ( !now ) {
609 // create or update timer
610 if ( updateStorageTimeout ) {
611 clearTimeout( updateStorageTimeout );
612 }
613 updateStorageTimeout = setTimeout( storageChanged, 1000, true);
614 }
615 else {
616// console.log("Update storage", updateStorageTimeout, Date.now());
617 updateStorage();
618 updateStorageTimeout = null;
619 }
620 }
621
622 function updateStorage() {
623 var json = JSON.stringify( storage )
624 if ( config.storage ) {
625 sessionStorage.setItem( config.storage, json )
626 }
627 return json;
628 }
629
630 function recordEvent( event ) {
631//console.log(event);
632 event.time = Date.now() - slideStart;
633 if ( mode == 1 ) event.board = board;
634 var slideData = getSlideData();
635 var i = slideData.events.length;
636 while ( i > 0 && event.time < slideData.events[ i - 1 ].time ) {
637 i--;
638 }
639 slideData.events.splice( i, 0, event );
640 slideData.duration = Math.max( slideData.duration, Date.now() - slideStart ) + 1;
641
642 storageChanged();
643 }
644
645 /**
646 * Get data as json string.
647 */
648 function getData() {
649 // cleanup slide data without events
650 for ( var id = 0; id < 2; id++ ) {
651 for ( var i = storage[ id ].data.length - 1; i >= 0; i-- ) {
652 if ( storage[ id ].data[ i ].events.length == 0 ) {
653 storage[ id ].data.splice( i, 1 );
654 }
655 }
656 }
657
658 return updateStorage();
659 }
660
661 /**
662 * Download data.
663 */
664 function downloadData() {
665 var a = document.createElement( 'a' );
666 document.body.appendChild( a );
667 try {
668 a.download = 'chalkboard.json';
669 var blob = new Blob( [ getData() ], {
670 type: 'application/json'
671 } );
672 a.href = window.URL.createObjectURL( blob );
673 } catch ( error ) {
674 a.innerHTML += ' (' + error + ')';
675 }
676 a.click();
677 document.body.removeChild( a );
678 }
679
680 /**
681 * Returns data object for the slide with the given indices.
682 */
683 function getSlideData( indices, id ) {
684 if ( id == undefined ) id = mode;
685 if ( !indices ) indices = slideIndices;
686 var data;
687 for ( var i = 0; i < storage[ id ].data.length; i++ ) {
688 if ( storage[ id ].data[ i ].slide.h === indices.h && storage[ id ].data[ i ].slide.v === indices.v && storage[ id ].data[ i ].slide.f === indices.f ) {
689 data = storage[ id ].data[ i ];
690 return data;
691 }
692 }
693 var page = Number( Reveal.getCurrentSlide().getAttribute('data-pdf-page-number') );
694//console.log( indices, Reveal.getCurrentSlide() );
695 storage[ id ].data.push( {
696 slide: indices,
697 page,
698 events: [],
699 duration: 0
700 } );
701 data = storage[ id ].data[ storage[ id ].data.length - 1 ];
702 return data;
703 }
704
705 /**
706 * Returns maximum duration of slide playback for both modes
707 */
708 function getSlideDuration( indices ) {
709 if ( !indices ) indices = slideIndices;
710 var duration = 0;
711 for ( var id = 0; id < 2; id++ ) {
712 for ( var i = 0; i < storage[ id ].data.length; i++ ) {
713 if ( storage[ id ].data[ i ].slide.h === indices.h && storage[ id ].data[ i ].slide.v === indices.v && storage[ id ].data[ i ].slide.f === indices.f ) {
714 duration = Math.max( duration, storage[ id ].data[ i ].duration );
715 break;
716 }
717 }
718 }
719//console.log( duration );
720 return duration;
721 }
722
723/*****************************************************************
724 ** Print
725 ******************************************************************/
726 var printMode = ( /print-pdf/gi ).test( window.location.search );
727//console.log("createPrintout" + printMode)
728
729 function addPageNumbers() {
730 // determine page number for printouts with fragments serialised
731 var slides = Reveal.getSlides();
732 var page = 0;
733 for ( var i=0; i < slides.length; i++) {
734 slides[i].setAttribute('data-pdf-page-number',page.toString());
735 // add number of fragments without fragment indices
736 var count = slides[i].querySelectorAll('.fragment:not([data-fragment-index])').length;
737 var fragments = slides[i].querySelectorAll('.fragment[data-fragment-index]');
738 for ( var j=0; j < fragments.length; j++) {
739 // increasenumber of fragments by highest fragment index (which start at 0)
740 if ( Number(fragments[j].getAttribute('data-fragment-index')) + 1 > count ) {
741 count = Number(fragments[j].getAttribute('data-fragment-index')) + 1;
742 }
743 }
744//console.log(count,fragments.length,( slides[i].querySelector('h1,h2,h3,h4')||{}).innerHTML, page);
745 page += count + 1;
746 }
747 }
748
749 function createPrintout() {
750 //console.warn(Reveal.getTotalSlides(),Reveal.getSlidesElement());
751 if ( storage[ 1 ].data.length == 0 ) return;
752 console.log( 'Create printout(s) for ' + storage[ 1 ].data.length + " slides" );
753 drawingCanvas[ 0 ].container.style.opacity = 0; // do not print notes canvas
754 drawingCanvas[ 0 ].container.style.visibility = 'hidden';
755
756 var patImg = new Image();
757 patImg.onload = function () {
758 var slides = Reveal.getSlides();
759//console.log(slides);
760 for ( var i = storage[ 1 ].data.length - 1; i >= 0; i-- ) {
761 console.log( 'Create printout for slide ' + storage[ 1 ].data[ i ].slide.h + '.' + storage[ 1 ].data[ i ].slide.v );
762 var slideData = getSlideData( storage[ 1 ].data[ i ].slide, 1 );
763 var drawings = createDrawings( slideData, patImg );
764//console.log("Page:", storage[ 1 ].data[ i ].page );
765//console.log("Slide:", slides[storage[ 1 ].data[ i ].page] );
766 addDrawings( slides[storage[ 1 ].data[ i ].page], drawings );
767
768 }
769// Reveal.sync();
770 };
771 patImg.src = background[ 1 ];
772 }
773
774
775 function cloneCanvas( oldCanvas ) {
776 //create a new canvas
777 var newCanvas = document.createElement( 'canvas' );
778 var context = newCanvas.getContext( '2d' );
779 //set dimensions
780 newCanvas.width = oldCanvas.width;
781 newCanvas.height = oldCanvas.height;
782 //apply the old canvas to the new one
783 context.drawImage( oldCanvas, 0, 0 );
784 //return the new canvas
785 return newCanvas;
786 }
787
788 function getCanvas( template, container, board ) {
789 var idx = container.findIndex( element => element.board === board );
790 if ( idx === -1 ) {
791 var canvas = cloneCanvas( template );
792 if ( !container.length ) {
793 idx = 0;
794 container.push( {
795 board,
796 canvas
797 } );
798 } else if ( board < container[ 0 ].board ) {
799 idx = 0;
800 container.unshift( {
801 board,
802 canvas
803 } );
804 } else if ( board > container[ container.length - 1 ].board ) {
805 idx = container.length;
806 container.push( {
807 board,
808 canvas
809 } );
810 }
811 }
812
813 return container[ idx ].canvas;
814 }
815
816 function createDrawings( slideData, patImg ) {
817 var width = Reveal.getConfig().width;
818 var height = Reveal.getConfig().height;
819 var scale = 1;
820 var xOffset = 0;
821 var yOffset = 0;
822 if ( width != storage[ 1 ].width || height != storage[ 1 ].height ) {
823 scale = Math.min( width / storage[ 1 ].width, height / storage[ 1 ].height );
824 xOffset = ( width - storage[ 1 ].width * scale ) / 2;
825 yOffset = ( height - storage[ 1 ].height * scale ) / 2;
826 }
827 mode = 1;
828 board = 0;
829// console.log( 'Create printout(s) for slide ', slideData );
830
831 var drawings = [];
832 var template = document.createElement( 'canvas' );
833 template.width = width;
834 template.height = height;
835
836 var imgCtx = template.getContext( '2d' );
837 imgCtx.fillStyle = imgCtx.createPattern( patImg, 'repeat' );
838 imgCtx.rect( 0, 0, width, height );
839 imgCtx.fill();
840
841 for ( var j = 0; j < slideData.events.length; j++ ) {
842 switch ( slideData.events[ j ].type ) {
843 case 'draw':
844 draw[ 1 ]( getCanvas( template, drawings, board ).getContext( '2d' ),
845 xOffset + slideData.events[ j ].x1 * scale,
846 yOffset + slideData.events[ j ].y1 * scale,
847 xOffset + slideData.events[ j ].x2 * scale,
848 yOffset + slideData.events[ j ].y2 * scale,
849 yOffset + slideData.events[ j ].color
850 );
851 break;
852 case 'erase':
853 eraseWithSponge( getCanvas( template, drawings, board ).getContext( '2d' ),
854 xOffset + slideData.events[ j ].x * scale,
855 yOffset + slideData.events[ j ].y * scale
856 );
857 break;
858 case 'selectboard':
859 selectBoard( slideData.events[ j ].board );
860 break;
861 case 'clear':
862 getCanvas( template, drawings, board ).getContext( '2d' ).clearRect( 0, 0, width, height );
863 getCanvas( template, drawings, board ).getContext( '2d' ).fill();
864 break;
865 default:
866 break;
867 }
868 }
869
870 drawings = drawings.sort( ( a, b ) => a.board > b.board && 1 || -1 );
871
872 mode = 0;
873
874 return drawings;
875 }
876
877 function addDrawings( slide, drawings ) {
878 var parent = slide.parentElement.parentElement;
879 var nextSlide = slide.parentElement.nextElementSibling;
880
881 for ( var i = 0; i < drawings.length; i++ ) {
882 var newPDFPage = document.createElement( 'div' );
883 newPDFPage.classList.add( 'pdf-page' );
884 newPDFPage.style.height = Reveal.getConfig().height;
885 newPDFPage.append( drawings[ i ].canvas );
886//console.log("Add drawing", newPDFPage);
887 if ( nextSlide != null ) {
888 parent.insertBefore( newPDFPage, nextSlide );
889 } else {
890 parent.append( newPDFPage );
891 }
892 }
893 }
894
895 /*****************************************************************
896 ** Drawings
897 ******************************************************************/
898
899 function drawWithBoardmarker( context, fromX, fromY, toX, toY, colorIdx ) {
900 if ( colorIdx == undefined ) colorIdx = color[ mode ];
901 context.lineWidth = boardmarkerWidth;
902 context.lineCap = 'round';
903 context.strokeStyle = boardmarkers[ colorIdx ].color;
904 context.beginPath();
905 context.moveTo( fromX, fromY );
906 context.lineTo( toX, toY );
907 context.stroke();
908 }
909
910 function drawWithChalk( context, fromX, fromY, toX, toY, colorIdx ) {
911 if ( colorIdx == undefined ) colorIdx = color[ mode ];
912 var brushDiameter = chalkWidth;
913 context.lineWidth = brushDiameter;
914 context.lineCap = 'round';
915 context.fillStyle = chalks[ colorIdx ].color; // 'rgba(255,255,255,0.5)';
916 context.strokeStyle = chalks[ colorIdx ].color;
917 /*var opacity = Math.min(0.8, Math.max(0,color[1].replace(/^.*,(.+)\)/,'$1') - 0.1)) + Math.random()*0.2;*/
918 var opacity = 1.0;
919 context.strokeStyle = context.strokeStyle.replace( /[\d\.]+\)$/g, opacity + ')' );
920 context.beginPath();
921 context.moveTo( fromX, fromY );
922 context.lineTo( toX, toY );
923 context.stroke();
924 // Chalk Effect
925 var length = Math.round( Math.sqrt( Math.pow( toX - fromX, 2 ) + Math.pow( toY - fromY, 2 ) ) / ( 5 / brushDiameter ) );
926 var xUnit = ( toX - fromX ) / length;
927 var yUnit = ( toY - fromY ) / length;
928 for ( var i = 0; i < length; i++ ) {
929 if ( chalkEffect > ( Math.random() * 0.9 ) ) {
930 var xCurrent = fromX + ( i * xUnit );
931 var yCurrent = fromY + ( i * yUnit );
932 var xRandom = xCurrent + ( Math.random() - 0.5 ) * brushDiameter * 1.2;
933 var yRandom = yCurrent + ( Math.random() - 0.5 ) * brushDiameter * 1.2;
934 context.clearRect( xRandom, yRandom, Math.random() * 2 + 2, Math.random() + 1 );
935 }
936 }
937 }
938
939 function eraseWithSponge( context, x, y ) {
940 context.save();
941 context.beginPath();
942 context.arc( x, y, eraser.radius, 0, 2 * Math.PI, false );
943 context.clip();
944 context.clearRect( x - eraser.radius - 1, y - eraser.radius - 1, eraser.radius * 2 + 2, eraser.radius * 2 + 2 );
945 context.restore();
946 if ( mode == 1 && grid ) {
947 redrawGrid( x, y, eraser.radius );
948 }
949 }
950
951
952 /**
953 * Show an overlay for the chalkboard.
954 */
955 function showChalkboard() {
956//console.log("showChalkboard");
957 clearTimeout( touchTimeout );
958 touchTimeout = null;
959 drawingCanvas[ 0 ].sponge.style.visibility = 'hidden'; // make sure that the sponge from touch events is hidden
960 drawingCanvas[ 1 ].sponge.style.visibility = 'hidden'; // make sure that the sponge from touch events is hidden
961 drawingCanvas[ 1 ].container.style.opacity = 1;
962 drawingCanvas[ 1 ].container.style.visibility = 'visible';
963 mode = 1;
964 }
965
966
967 /**
968 * Closes open chalkboard.
969 */
970 function closeChalkboard() {
971 clearTimeout( touchTimeout );
972 touchTimeout = null;
973 drawingCanvas[ 0 ].sponge.style.visibility = 'hidden'; // make sure that the sponge from touch events is hidden
974 drawingCanvas[ 1 ].sponge.style.visibility = 'hidden'; // make sure that the sponge from touch events is hidden
975 drawingCanvas[ 1 ].container.style.opacity = 0;
976 drawingCanvas[ 1 ].container.style.visibility = 'hidden';
977 lastX = null;
978 lastY = null;
979 mode = 0;
980 }
981
982 /**
983 * Clear current canvas.
984 */
985 function clearCanvas( id ) {
986 if ( id == 0 ) clearTimeout( slidechangeTimeout );
987 drawingCanvas[ id ].context.clearRect( 0, 0, drawingCanvas[ id ].width, drawingCanvas[ id ].height );
988 if ( id == 1 && grid ) drawGrid();
989 }
990
991 /**
992 * Draw grid on background
993 */
994 function drawGrid() {
995 var context = drawingCanvas[ 1 ].context;
996
997 drawingCanvas[ 1 ].scale = Math.min( drawingCanvas[ 1 ].width / storage[ 1 ].width, drawingCanvas[ 1 ].height / storage[ 1 ].height );
998 drawingCanvas[ 1 ].xOffset = ( drawingCanvas[ 1 ].width - storage[ 1 ].width * drawingCanvas[ 1 ].scale ) / 2;
999 drawingCanvas[ 1 ].yOffset = ( drawingCanvas[ 1 ].height - storage[ 1 ].height * drawingCanvas[ 1 ].scale ) / 2;
1000
1001 var scale = drawingCanvas[ 1 ].scale;
1002 var xOffset = drawingCanvas[ 1 ].xOffset;
1003 var yOffset = drawingCanvas[ 1 ].yOffset;
1004
1005 var distance = grid.distance * scale;
1006
1007 var fromX = drawingCanvas[ 1 ].width / 2 - distance / 2 - Math.floor( ( drawingCanvas[ 1 ].width - distance ) / 2 / distance ) * distance;
1008 for ( var x = fromX; x < drawingCanvas[ 1 ].width; x += distance ) {
1009 context.beginPath();
1010 context.lineWidth = grid.width * scale;
1011 context.lineCap = 'round';
1012 context.fillStyle = grid.color;
1013 context.strokeStyle = grid.color;
1014 context.moveTo( x, 0 );
1015 context.lineTo( x, drawingCanvas[ 1 ].height );
1016 context.stroke();
1017 }
1018 var fromY = drawingCanvas[ 1 ].height / 2 - distance / 2 - Math.floor( ( drawingCanvas[ 1 ].height - distance ) / 2 / distance ) * distance;
1019
1020 for ( var y = fromY; y < drawingCanvas[ 1 ].height; y += distance ) {
1021 context.beginPath();
1022 context.lineWidth = grid.width * scale;
1023 context.lineCap = 'round';
1024 context.fillStyle = grid.color;
1025 context.strokeStyle = grid.color;
1026 context.moveTo( 0, y );
1027 context.lineTo( drawingCanvas[ 1 ].width, y );
1028 context.stroke();
1029 }
1030 }
1031
1032 function redrawGrid( centerX, centerY, diameter ) {
1033 var context = drawingCanvas[ 1 ].context;
1034
1035 drawingCanvas[ 1 ].scale = Math.min( drawingCanvas[ 1 ].width / storage[ 1 ].width, drawingCanvas[ 1 ].height / storage[ 1 ].height );
1036 drawingCanvas[ 1 ].xOffset = ( drawingCanvas[ 1 ].width - storage[ 1 ].width * drawingCanvas[ 1 ].scale ) / 2;
1037 drawingCanvas[ 1 ].yOffset = ( drawingCanvas[ 1 ].height - storage[ 1 ].height * drawingCanvas[ 1 ].scale ) / 2;
1038
1039 var scale = drawingCanvas[ 1 ].scale;
1040 var xOffset = drawingCanvas[ 1 ].xOffset;
1041 var yOffset = drawingCanvas[ 1 ].yOffset;
1042
1043 var distance = grid.distance * scale;
1044
1045 var fromX = drawingCanvas[ 1 ].width / 2 - distance / 2 - Math.floor( ( drawingCanvas[ 1 ].width - distance ) / 2 / distance ) * distance;
1046
1047 for ( var x = fromX + distance * Math.ceil( ( centerX - diameter - fromX ) / distance ); x <= fromX + distance * Math.floor( ( centerX + diameter - fromX ) / distance ); x += distance ) {
1048 context.beginPath();
1049 context.lineWidth = grid.width * scale;
1050 context.lineCap = 'round';
1051 context.fillStyle = grid.color;
1052 context.strokeStyle = grid.color;
1053 context.moveTo( x, centerY - Math.sqrt( diameter * diameter - ( centerX - x ) * ( centerX - x ) ) );
1054 context.lineTo( x, centerY + Math.sqrt( diameter * diameter - ( centerX - x ) * ( centerX - x ) ) );
1055 context.stroke();
1056 }
1057 var fromY = drawingCanvas[ 1 ].height / 2 - distance / 2 - Math.floor( ( drawingCanvas[ 1 ].height - distance ) / 2 / distance ) * distance;
1058 for ( var y = fromY + distance * Math.ceil( ( centerY - diameter - fromY ) / distance ); y <= fromY + distance * Math.floor( ( centerY + diameter - fromY ) / distance ); y += distance ) {
1059 context.beginPath();
1060 context.lineWidth = grid.width * scale;
1061 context.lineCap = 'round';
1062 context.fillStyle = grid.color;
1063 context.strokeStyle = grid.color;
1064 context.moveTo( centerX - Math.sqrt( diameter * diameter - ( centerY - y ) * ( centerY - y ) ), y );
1065 context.lineTo( centerX + Math.sqrt( diameter * diameter - ( centerY - y ) * ( centerY - y ) ), y );
1066 context.stroke();
1067 }
1068 }
1069
1070 /**
1071 * Set the color
1072 */
1073 function setColor( index, record ) {
1074 // protect against out of bounds (this could happen when
1075 // replaying events recorded with different color settings).
1076 if ( index >= pens[ mode ].length ) index = 0;
1077 color[ mode ] = index;
1078 drawingCanvas[ mode ].canvas.style.cursor = pens[ mode ][ color[ mode ] ].cursor;
1079 }
1080
1081 /**
1082 * Set the board
1083 */
1084 function selectBoard( boardIdx, record ) {
1085//console.log("Set board",boardIdx);
1086 if ( board == boardIdx ) return;
1087
1088 board = boardIdx;
1089 redrawChalkboard( boardIdx );
1090 if ( record ) {
1091 recordEvent( { type: 'selectboard' } );
1092 }
1093 }
1094
1095 function redrawChalkboard( boardIdx ) {
1096 clearCanvas( 1 );
1097 var slideData = getSlideData( slideIndices, 1 );
1098 var index = 0;
1099 var play = ( boardIdx == 0 );
1100 while ( index < slideData.events.length && slideData.events[ index ].time < Date.now() - slideStart ) {
1101 if ( boardIdx == slideData.events[ index ].board ) {
1102 playEvent( 1, slideData.events[ index ], Date.now() - slideStart );
1103 }
1104
1105 index++;
1106 }
1107 }
1108
1109
1110 /**
1111 * Forward cycle color
1112 */
1113 function cycleColorNext() {
1114 color[ mode ] = ( color[ mode ] + 1 ) % pens[ mode ].length;
1115 return color[ mode ];
1116 }
1117
1118 /**
1119 * Backward cycle color
1120 */
1121 function cycleColorPrev() {
1122 color[ mode ] = ( color[ mode ] + ( pens[ mode ].length - 1 ) ) % pens[ mode ].length;
1123 return color[ mode ];
1124 }
1125
1126/*****************************************************************
1127 ** Broadcast
1128 ******************************************************************/
1129
1130 var eventQueue = [];
1131
1132 document.addEventListener( 'received', function ( message ) {
1133 if ( message.content && message.content.sender == 'chalkboard-plugin' ) {
1134 // add message to queue
1135 eventQueue.push( message );
1136 console.log( JSON.stringify( message ) );
1137 }
1138 if ( eventQueue.length == 1 ) processQueue();
1139 } );
1140
1141 function processQueue() {
1142 // take first message from queue
1143 var message = eventQueue.shift();
1144
1145 // synchronize time with seminar host
1146 slideStart = Date.now() - message.content.timestamp;
1147 // set status
1148 if ( mode < message.content.mode ) {
1149 // open chalkboard
1150 showChalkboard();
1151 } else if ( mode > message.content.mode ) {
1152 // close chalkboard
1153 closeChalkboard();
1154 }
1155 if ( board != message.content.board ) {
1156 board = message.content.board;
1157 redrawChalkboard( board );
1158 };
1159
1160 switch ( message.content.type ) {
1161 case 'showChalkboard':
1162 showChalkboard();
1163 break;
1164 case 'closeChalkboard':
1165 closeChalkboard();
1166 break;
1167 case 'erase':
1168 erasePoint( message.content.x, message.content.y );
1169 break;
1170 case 'draw':
1171 drawSegment( message.content.fromX, message.content.fromY, message.content.toX, message.content.toY, message.content.color );
1172 break;
1173 case 'clear':
1174 clearSlide();
1175 break;
1176 case 'selectboard':
1177 selectBoard( message.content.board, true );
1178 break;
1179 case 'resetSlide':
1180 resetSlideDrawings();
1181 break;
1182 case 'init':
1183 storage = message.content.storage;
1184 for ( var id = 0; id < 2; id++ ) {
1185 drawingCanvas[ id ].scale = Math.min( drawingCanvas[ id ].width / storage[ id ].width, drawingCanvas[ id ].height / storage[ id ].height );
1186 drawingCanvas[ id ].xOffset = ( drawingCanvas[ id ].width - storage[ id ].width * drawingCanvas[ id ].scale ) / 2;
1187 drawingCanvas[ id ].yOffset = ( drawingCanvas[ id ].height - storage[ id ].height * drawingCanvas[ id ].scale ) / 2;
1188 }
1189 clearCanvas( 0 );
1190 clearCanvas( 1 );
1191 if ( !playback ) {
1192 slidechangeTimeout = setTimeout( startPlayback, transition, getSlideDuration(), 0 );
1193 }
1194 if ( mode == 1 && message.content.mode == 0 ) {
1195 setTimeout( closeChalkboard, transition + 50 );
1196 }
1197 if ( mode == 0 && message.content.mode == 1 ) {
1198 setTimeout( showChalkboard, transition + 50 );
1199 }
1200 mode = message.content.mode;
1201 board = message.content.board;
1202 break;
1203 default:
1204 break;
1205 }
1206
1207 // continue with next message if queued
1208 if ( eventQueue.length > 0 ) {
1209 processQueue();
1210 } else {
1211 storageChanged();
1212 }
1213 }
1214
1215 document.addEventListener( 'welcome', function ( user ) {
1216 // broadcast storage
1217 var message = new CustomEvent( messageType );
1218 message.content = {
1219 sender: 'chalkboard-plugin',
1220 recipient: user.id,
1221 type: 'init',
1222 timestamp: Date.now() - slideStart,
1223 storage: storage,
1224 mode,
1225 board
1226 };
1227 document.dispatchEvent( message );
1228 } );
1229
1230 /*****************************************************************
1231 ** Playback
1232 ******************************************************************/
1233
1234 document.addEventListener( 'seekplayback', function ( event ) {
1235//console.log('event seekplayback ' + event.timestamp);
1236 stopPlayback();
1237 if ( !playback || event.timestamp == 0 ) {
1238 // in other cases startplayback fires after seeked
1239 startPlayback( event.timestamp );
1240 }
1241 //console.log('seeked');
1242 } );
1243
1244
1245 document.addEventListener( 'startplayback', function ( event ) {
1246//console.log('event startplayback ' + event.timestamp);
1247 stopPlayback();
1248 playback = true;
1249 startPlayback( event.timestamp );
1250 } );
1251
1252 document.addEventListener( 'stopplayback', function ( event ) {
1253//console.log('event stopplayback ' + (Date.now() - slideStart) );
1254 playback = false;
1255 stopPlayback();
1256 } );
1257
1258 document.addEventListener( 'startrecording', function ( event ) {
1259//console.log('event startrecording ' + event.timestamp);
1260 startRecording();
1261 } );
1262
1263
1264 function startRecording() {
1265 resetSlide( true );
1266 slideStart = Date.now();
1267 }
1268
1269 function startPlayback( timestamp, finalMode ) {
1270//console.log("playback " + timestamp );
1271 slideStart = Date.now() - timestamp;
1272 closeChalkboard();
1273 mode = 0;
1274 board = 0;
1275 for ( var id = 0; id < 2; id++ ) {
1276 clearCanvas( id );
1277 var slideData = getSlideData( slideIndices, id );
1278//console.log( timestamp +" / " + JSON.stringify(slideData));
1279 var index = 0;
1280 while ( index < slideData.events.length && slideData.events[ index ].time < ( Date.now() - slideStart ) ) {
1281 playEvent( id, slideData.events[ index ], timestamp );
1282 index++;
1283 }
1284
1285 while ( playback && index < slideData.events.length ) {
1286 timeouts[ id ].push( setTimeout( playEvent, slideData.events[ index ].time - ( Date.now() - slideStart ), id, slideData.events[ index ], timestamp ) );
1287 index++;
1288 }
1289 }
1290//console.log("Mode: " + finalMode + "/" + mode );
1291 if ( finalMode != undefined ) {
1292 mode = finalMode;
1293 }
1294 if ( mode == 1 ) showChalkboard();
1295//console.log("playback (ok)");
1296
1297 };
1298
1299 function stopPlayback() {
1300//console.log("stopPlayback");
1301//console.log("Timeouts: " + timeouts[0].length + "/"+ timeouts[1].length);
1302 for ( var id = 0; id < 2; id++ ) {
1303 for ( var i = 0; i < timeouts[ id ].length; i++ ) {
1304 clearTimeout( timeouts[ id ][ i ] );
1305 }
1306 timeouts[ id ] = [];
1307 }
1308 };
1309
1310 function playEvent( id, event, timestamp ) {
1311//console.log( timestamp +" / " + JSON.stringify(event));
1312//console.log( id + ": " + timestamp +" / " + event.time +" / " + event.type +" / " + mode );
1313 switch ( event.type ) {
1314 case 'open':
1315 if ( timestamp <= event.time ) {
1316 showChalkboard();
1317 } else {
1318 mode = 1;
1319 }
1320
1321 break;
1322 case 'close':
1323 if ( timestamp < event.time ) {
1324 closeChalkboard();
1325 } else {
1326 mode = 0;
1327 }
1328 break;
1329 case 'clear':
1330 clearCanvas( id );
1331 break;
1332 case 'selectboard':
1333 selectBoard( event.board );
1334 break;
1335 case 'draw':
1336 drawLine( id, event, timestamp );
1337 break;
1338 case 'erase':
1339 eraseCircle( id, event, timestamp );
1340 break;
1341 }
1342 };
1343
1344 function drawLine( id, event, timestamp ) {
1345 var ctx = drawingCanvas[ id ].context;
1346 var scale = drawingCanvas[ id ].scale;
1347 var xOffset = drawingCanvas[ id ].xOffset;
1348 var yOffset = drawingCanvas[ id ].yOffset;
1349 draw[ id ]( ctx, xOffset + event.x1 * scale, yOffset + event.y1 * scale, xOffset + event.x2 * scale, yOffset + event.y2 * scale, event.color );
1350 };
1351
1352 function eraseCircle( id, event, timestamp ) {
1353 var ctx = drawingCanvas[ id ].context;
1354 var scale = drawingCanvas[ id ].scale;
1355 var xOffset = drawingCanvas[ id ].xOffset;
1356 var yOffset = drawingCanvas[ id ].yOffset;
1357
1358 eraseWithSponge( ctx, xOffset + event.x * scale, yOffset + event.y * scale );
1359 };
1360
1361 function startErasing( x, y ) {
1362 drawing = false;
1363 erasing = true;
1364 drawingCanvas[ mode ].sponge.style.visibility = 'visible';
1365 erasePoint( x, y );
1366 }
1367
1368 function erasePoint( x, y ) {
1369 var ctx = drawingCanvas[ mode ].context;
1370 var scale = drawingCanvas[ mode ].scale;
1371 var xOffset = drawingCanvas[ mode ].xOffset;
1372 var yOffset = drawingCanvas[ mode ].yOffset;
1373
1374 // move sponge image
1375 drawingCanvas[ mode ].sponge.style.left = ( x * scale + xOffset - eraser.radius ) + 'px';
1376 drawingCanvas[ mode ].sponge.style.top = ( y * scale + yOffset - 2 * eraser.radius ) + 'px';
1377
1378 recordEvent( {
1379 type: 'erase',
1380 x,
1381 y
1382 } );
1383
1384 if (
1385 x * scale + xOffset > 0 &&
1386 y * scale + yOffset > 0 &&
1387 x * scale + xOffset < drawingCanvas[ mode ].width &&
1388 y * scale + yOffset < drawingCanvas[ mode ].height
1389 ) {
1390 eraseWithSponge( ctx, x * scale + xOffset, y * scale + yOffset );
1391 }
1392 }
1393
1394 function stopErasing() {
1395 erasing = false;
1396 // hide sponge
1397 drawingCanvas[ mode ].sponge.style.visibility = 'hidden';
1398 }
1399
1400 function startDrawing( x, y ) {
1401 drawing = true;
1402
1403 var ctx = drawingCanvas[ mode ].context;
1404 var scale = drawingCanvas[ mode ].scale;
1405 var xOffset = drawingCanvas[ mode ].xOffset;
1406 var yOffset = drawingCanvas[ mode ].yOffset;
1407 lastX = x * scale + xOffset;
1408 lastY = y * scale + yOffset;
1409 }
1410
1411 function drawSegment( fromX, fromY, toX, toY, colorIdx ) {
1412 var ctx = drawingCanvas[ mode ].context;
1413 var scale = drawingCanvas[ mode ].scale;
1414 var xOffset = drawingCanvas[ mode ].xOffset;
1415 var yOffset = drawingCanvas[ mode ].yOffset;
1416
1417 recordEvent( {
1418 type: 'draw',
1419 color: colorIdx,
1420 x1: fromX,
1421 y1: fromY,
1422 x2: toX,
1423 y2: toY
1424 } );
1425
1426 if (
1427 fromX * scale + xOffset > 0 &&
1428 fromY * scale + yOffset > 0 &&
1429 fromX * scale + xOffset < drawingCanvas[ mode ].width &&
1430 fromY * scale + yOffset < drawingCanvas[ mode ].height &&
1431 toX * scale + xOffset > 0 &&
1432 toY * scale + yOffset > 0 &&
1433 toX * scale + xOffset < drawingCanvas[ mode ].width &&
1434 toY * scale + yOffset < drawingCanvas[ mode ].height
1435 ) {
1436 draw[ mode ]( ctx, fromX * scale + xOffset, fromY * scale + yOffset, toX * scale + xOffset, toY * scale + yOffset, colorIdx );
1437 }
1438 }
1439
1440 function stopDrawing() {
1441 drawing = false;
1442 }
1443
1444
1445/*****************************************************************
1446 ** User interface
1447 ******************************************************************/
1448
1449 function setupCanvasEvents( canvas ) {
1450// TODO: check all touchevents
1451 canvas.addEventListener( 'touchstart', function ( evt ) {
1452 evt.preventDefault();
1453//console.log("Touch start");
1454 if ( !readOnly && evt.target.getAttribute( 'data-chalkboard' ) == mode ) {
1455 var scale = drawingCanvas[ mode ].scale;
1456 var xOffset = drawingCanvas[ mode ].xOffset;
1457 var yOffset = drawingCanvas[ mode ].yOffset;
1458
1459 var touch = evt.touches[ 0 ];
1460 mouseX = touch.pageX;
1461 mouseY = touch.pageY;
1462 startDrawing( ( mouseX - xOffset ) / scale, ( mouseY - yOffset ) / scale );
1463 touchTimeout = setTimeout( startErasing, 500, ( mouseX - xOffset ) / scale, ( mouseY - yOffset ) / scale );
1464 }
1465 }, passiveSupported ? {
1466 passive: false
1467 } : false );
1468
1469 canvas.addEventListener( 'touchmove', function ( evt ) {
1470 evt.preventDefault();
1471//console.log("Touch move");
1472 clearTimeout( touchTimeout );
1473 touchTimeout = null;
1474 if ( drawing || erasing ) {
1475 var scale = drawingCanvas[ mode ].scale;
1476 var xOffset = drawingCanvas[ mode ].xOffset;
1477 var yOffset = drawingCanvas[ mode ].yOffset;
1478
1479 var touch = evt.touches[ 0 ];
1480 mouseX = touch.pageX;
1481 mouseY = touch.pageY;
1482 if ( mouseY < drawingCanvas[ mode ].height && mouseX < drawingCanvas[ mode ].width ) {
1483 // move sponge
1484 if ( event.type == 'erase' ) {
1485 drawingCanvas[ mode ].sponge.style.left = ( mouseX - eraser.radius ) + 'px';
1486 drawingCanvas[ mode ].sponge.style.top = ( mouseY - eraser.radius ) + 'px';
1487 }
1488 }
1489
1490 if ( drawing ) {
1491 drawSegment( ( lastX - xOffset ) / scale, ( lastY - yOffset ) / scale, ( mouseX - xOffset ) / scale, ( mouseY - yOffset ) / scale, color[ mode ] );
1492 // broadcast
1493 var message = new CustomEvent( messageType );
1494 message.content = {
1495 sender: 'chalkboard-plugin',
1496 type: 'draw',
1497 timestamp: Date.now() - slideStart,
1498 mode,
1499 board,
1500 fromX: ( lastX - xOffset ) / scale,
1501 fromY: ( lastY - yOffset ) / scale,
1502 toX: ( mouseX - xOffset ) / scale,
1503 toY: ( mouseY - yOffset ) / scale,
1504 color: color[ mode ]
1505 };
1506 document.dispatchEvent( message );
1507
1508 lastX = mouseX;
1509 lastY = mouseY;
1510 } else {
1511 erasePoint( ( mouseX - xOffset ) / scale, ( mouseY - yOffset ) / scale );
1512 // broadcast
1513 var message = new CustomEvent( messageType );
1514 message.content = {
1515 sender: 'chalkboard-plugin',
1516 type: 'erase',
1517 timestamp: Date.now() - slideStart,
1518 mode,
1519 board,
1520 x: ( mouseX - xOffset ) / scale,
1521 y: ( mouseY - yOffset ) / scale
1522 };
1523 document.dispatchEvent( message );
1524 }
1525
1526 }
1527 }, false );
1528
1529
1530 canvas.addEventListener( 'touchend', function ( evt ) {
1531 evt.preventDefault();
1532 clearTimeout( touchTimeout );
1533 touchTimeout = null;
1534 // hide sponge image
1535 drawingCanvas[ mode ].sponge.style.visibility = 'hidden';
1536 stopDrawing();
1537 }, false );
1538
1539 canvas.addEventListener( 'mousedown', function ( evt ) {
1540 evt.preventDefault();
1541 if ( !readOnly && evt.target.getAttribute( 'data-chalkboard' ) == mode ) {
1542//console.log( "mousedown: " + evt.button );
1543 var scale = drawingCanvas[ mode ].scale;
1544 var xOffset = drawingCanvas[ mode ].xOffset;
1545 var yOffset = drawingCanvas[ mode ].yOffset;
1546
1547 mouseX = evt.pageX;
1548 mouseY = evt.pageY;
1549
1550 if ( evt.button == 2 || evt.button == 1 ) {
1551 startErasing( ( mouseX - xOffset ) / scale, ( mouseY - yOffset ) / scale );
1552 // broadcast
1553 var message = new CustomEvent( messageType );
1554 message.content = {
1555 sender: 'chalkboard-plugin',
1556 type: 'erase',
1557 timestamp: Date.now() - slideStart,
1558 mode,
1559 board,
1560 x: ( mouseX - xOffset ) / scale,
1561 y: ( mouseY - yOffset ) / scale
1562 };
1563 document.dispatchEvent( message );
1564 } else {
1565 startDrawing( ( mouseX - xOffset ) / scale, ( mouseY - yOffset ) / scale );
1566 }
1567 }
1568 } );
1569
1570 canvas.addEventListener( 'mousemove', function ( evt ) {
1571 evt.preventDefault();
1572//console.log("Mouse move");
1573 if ( drawing || erasing ) {
1574 var scale = drawingCanvas[ mode ].scale;
1575 var xOffset = drawingCanvas[ mode ].xOffset;
1576 var yOffset = drawingCanvas[ mode ].yOffset;
1577
1578 mouseX = evt.pageX;
1579 mouseY = evt.pageY;
1580
1581 if ( drawing ) {
1582 drawSegment( ( lastX - xOffset ) / scale, ( lastY - yOffset ) / scale, ( mouseX - xOffset ) / scale, ( mouseY - yOffset ) / scale, color[ mode ] );
1583 // broadcast
1584 var message = new CustomEvent( messageType );
1585 message.content = {
1586 sender: 'chalkboard-plugin',
1587 type: 'draw',
1588 timestamp: Date.now() - slideStart,
1589 mode,
1590 board,
1591 fromX: ( lastX - xOffset ) / scale,
1592 fromY: ( lastY - yOffset ) / scale,
1593 toX: ( mouseX - xOffset ) / scale,
1594 toY: ( mouseY - yOffset ) / scale,
1595 color: color[ mode ]
1596 };
1597 document.dispatchEvent( message );
1598
1599 lastX = mouseX;
1600 lastY = mouseY;
1601 } else {
1602 erasePoint( ( mouseX - xOffset ) / scale, ( mouseY - yOffset ) / scale );
1603 // broadcast
1604 var message = new CustomEvent( messageType );
1605 message.content = {
1606 sender: 'chalkboard-plugin',
1607 type: 'erase',
1608 timestamp: Date.now() - slideStart,
1609 mode,
1610 board,
1611 x: ( mouseX - xOffset ) / scale,
1612 y: ( mouseY - yOffset ) / scale
1613 };
1614 document.dispatchEvent( message );
1615 }
1616
1617 }
1618 } );
1619
1620
1621 canvas.addEventListener( 'mouseup', function ( evt ) {
1622 evt.preventDefault();
1623 drawingCanvas[ mode ].canvas.style.cursor = pens[ mode ][ color[ mode ] ].cursor;
1624 if ( drawing || erasing ) {
1625 stopDrawing();
1626 stopErasing();
1627 }
1628 } );
1629 }
1630
1631 function resize() {
1632//console.log("resize");
1633 // Resize the canvas and draw everything again
1634 var timestamp = Date.now() - slideStart;
1635 if ( !playback ) {
1636 timestamp = getSlideDuration();
1637 }
1638
1639//console.log( drawingCanvas[0].scale + "/" + drawingCanvas[0].xOffset + "/" +drawingCanvas[0].yOffset );
1640 for ( var id = 0; id < 2; id++ ) {
1641 drawingCanvas[ id ].width = window.innerWidth;
1642 drawingCanvas[ id ].height = window.innerHeight;
1643 drawingCanvas[ id ].canvas.width = drawingCanvas[ id ].width;
1644 drawingCanvas[ id ].canvas.height = drawingCanvas[ id ].height;
1645 drawingCanvas[ id ].context.canvas.width = drawingCanvas[ id ].width;
1646 drawingCanvas[ id ].context.canvas.height = drawingCanvas[ id ].height;
1647
1648 drawingCanvas[ id ].scale = Math.min( drawingCanvas[ id ].width / storage[ id ].width, drawingCanvas[ id ].height / storage[ id ].height );
1649 drawingCanvas[ id ].xOffset = ( drawingCanvas[ id ].width - storage[ id ].width * drawingCanvas[ id ].scale ) / 2;
1650 drawingCanvas[ id ].yOffset = ( drawingCanvas[ id ].height - storage[ id ].height * drawingCanvas[ id ].scale ) / 2;
1651//console.log( drawingCanvas[id].scale + "/" + drawingCanvas[id].xOffset + "/" +drawingCanvas[id].yOffset );
1652 }
1653//console.log( window.innerWidth + "/" + window.innerHeight);
1654 startPlayback( timestamp, mode, true );
1655 }
1656
1657 Reveal.addEventListener( 'pdf-ready', function ( evt ) {
1658// console.log( "Create printouts when ready" );
1659 whenLoaded( createPrintout );
1660 });
1661
1662 Reveal.addEventListener( 'ready', function ( evt ) {
1663//console.log('ready');
1664 if ( !printMode ) {
1665 window.addEventListener( 'resize', resize );
1666
1667 slideStart = Date.now() - getSlideDuration();
1668 slideIndices = Reveal.getIndices();
1669 if ( !playback ) {
1670 startPlayback( getSlideDuration(), 0 );
1671 }
1672 if ( Reveal.isAutoSliding() ) {
1673 var event = new CustomEvent( 'startplayback' );
1674 event.timestamp = 0;
1675 document.dispatchEvent( event );
1676 }
1677 updateStorage();
1678 whenReady( addPageNumbers );
1679 }
1680 } );
1681 Reveal.addEventListener( 'slidechanged', function ( evt ) {
1682// clearTimeout( slidechangeTimeout );
1683//console.log('slidechanged');
1684 if ( !printMode ) {
1685 slideStart = Date.now() - getSlideDuration();
1686 slideIndices = Reveal.getIndices();
1687 closeChalkboard();
1688 board = 0;
1689 clearCanvas( 0 );
1690 clearCanvas( 1 );
1691 if ( !playback ) {
1692 slidechangeTimeout = setTimeout( startPlayback, transition, getSlideDuration(), 0 );
1693 }
1694 if ( Reveal.isAutoSliding() ) {
1695 var event = new CustomEvent( 'startplayback' );
1696 event.timestamp = 0;
1697 document.dispatchEvent( event );
1698 }
1699 }
1700 } );
1701 Reveal.addEventListener( 'fragmentshown', function ( evt ) {
1702// clearTimeout( slidechangeTimeout );
1703//console.log('fragmentshown');
1704 if ( !printMode ) {
1705 slideStart = Date.now() - getSlideDuration();
1706 slideIndices = Reveal.getIndices();
1707 closeChalkboard();
1708 board = 0;
1709 clearCanvas( 0 );
1710 clearCanvas( 1 );
1711 if ( Reveal.isAutoSliding() ) {
1712 var event = new CustomEvent( 'startplayback' );
1713 event.timestamp = 0;
1714 document.dispatchEvent( event );
1715 } else if ( !playback ) {
1716 startPlayback( getSlideDuration(), 0 );
1717// closeChalkboard();
1718 }
1719 }
1720 } );
1721 Reveal.addEventListener( 'fragmenthidden', function ( evt ) {
1722// clearTimeout( slidechangeTimeout );
1723//console.log('fragmenthidden');
1724 if ( !printMode ) {
1725 slideStart = Date.now() - getSlideDuration();
1726 slideIndices = Reveal.getIndices();
1727 closeChalkboard();
1728 board = 0;
1729 clearCanvas( 0 );
1730 clearCanvas( 1 );
1731 if ( Reveal.isAutoSliding() ) {
1732 document.dispatchEvent( new CustomEvent( 'stopplayback' ) );
1733 } else if ( !playback ) {
1734 startPlayback( getSlideDuration() );
1735 closeChalkboard();
1736 }
1737 }
1738 } );
1739
1740 Reveal.addEventListener( 'autoslideresumed', function ( evt ) {
1741//console.log('autoslideresumed');
1742 var event = new CustomEvent( 'startplayback' );
1743 event.timestamp = 0;
1744 document.dispatchEvent( event );
1745 } );
1746 Reveal.addEventListener( 'autoslidepaused', function ( evt ) {
1747//console.log('autoslidepaused');
1748 document.dispatchEvent( new CustomEvent( 'stopplayback' ) );
1749
1750 // advance to end of slide
1751// closeChalkboard();
1752 startPlayback( getSlideDuration(), 0 );
1753 } );
1754
1755 function toggleNotesCanvas() {
1756 if ( !readOnly ) {
1757 if ( mode == 1 ) {
1758 toggleChalkboard();
1759 notescanvas.style.background = background[ 0 ]; //'rgba(255,0,0,0.5)';
1760 notescanvas.style.pointerEvents = 'auto';
1761 }
1762 else {
1763 if ( notescanvas.style.pointerEvents != 'none' ) {
1764 // hide notes canvas
1765 if ( colorButtons ) {
1766 notescanvas.querySelector( '.palette' ).style.visibility = 'hidden';
1767 }
1768 notescanvas.style.background = 'rgba(0,0,0,0)';
1769 notescanvas.style.pointerEvents = 'none';
1770 }
1771 else {
1772 // show notes canvas
1773 if ( colorButtons ) {
1774 notescanvas.querySelector( '.palette' ).style.visibility = 'visible';
1775 }
1776 notescanvas.style.background = background[ 0 ]; //'rgba(255,0,0,0.5)';
1777 notescanvas.style.pointerEvents = 'auto';
1778
1779 var idx = 0;
1780 if ( color[ mode ] ) {
1781 idx = color[ mode ];
1782 }
1783
1784 setColor( idx, true );
1785 }
1786 }
1787 }
1788 };
1789
1790 function toggleChalkboard() {
1791//console.log("toggleChalkboard " + mode);
1792 if ( mode == 1 ) {
1793 if ( !readOnly ) {
1794 recordEvent( { type: 'close' } );
1795 }
1796 closeChalkboard();
1797
1798 // broadcast
1799 var message = new CustomEvent( messageType );
1800 message.content = {
1801 sender: 'chalkboard-plugin',
1802 type: 'closeChalkboard',
1803 timestamp: Date.now() - slideStart,
1804 mode: 0,
1805 board
1806 };
1807 document.dispatchEvent( message );
1808
1809
1810 } else {
1811 showChalkboard();
1812 if ( !readOnly ) {
1813 recordEvent( { type: 'open' } );
1814 // broadcast
1815 var message = new CustomEvent( messageType );
1816 message.content = {
1817 sender: 'chalkboard-plugin',
1818 type: 'showChalkboard',
1819 timestamp: Date.now() - slideStart,
1820 mode: 1,
1821 board
1822 };
1823 document.dispatchEvent( message );
1824
1825 var idx = 0;
1826
1827 if ( rememberColor[ mode ] ) {
1828 idx = color[ mode ];
1829 }
1830
1831 setColor( idx, true );
1832 }
1833 }
1834 };
1835
1836 function clearSlide() {
1837 recordEvent( { type: 'clear' } );
1838 clearCanvas( mode );
1839 }
1840
1841 function clear() {
1842 if ( !readOnly ) {
1843 clearSlide();
1844 // broadcast
1845 var message = new CustomEvent( messageType );
1846 message.content = {
1847 sender: 'chalkboard-plugin',
1848 type: 'clear',
1849 timestamp: Date.now() - slideStart,
1850 mode,
1851 board
1852 };
1853 document.dispatchEvent( message );
1854 }
1855 };
1856
1857 function colorIndex( idx ) {
1858 if ( !readOnly ) {
1859 setColor( idx, true );
1860 }
1861 }
1862
1863 function colorNext() {
1864 if ( !readOnly ) {
1865 let idx = cycleColorNext();
1866 setColor( idx, true );
1867 }
1868 }
1869
1870 function colorPrev() {
1871 if ( !readOnly ) {
1872 let idx = cycleColorPrev();
1873 setColor( idx, true );
1874 }
1875 }
1876
1877 function resetSlideDrawings() {
1878 slideStart = Date.now();
1879 closeChalkboard();
1880
1881 clearCanvas( 0 );
1882 clearCanvas( 1 );
1883
1884 mode = 1;
1885 var slideData = getSlideData();
1886 slideData.duration = 0;
1887 slideData.events = [];
1888 mode = 0;
1889 var slideData = getSlideData();
1890 slideData.duration = 0;
1891 slideData.events = [];
1892
1893 updateStorage();
1894 }
1895
1896 function resetSlide( force ) {
1897 var ok = force || confirm( "Please confirm to delete chalkboard drawings on this slide!" );
1898 if ( ok ) {
1899//console.log("resetSlide ");
1900 stopPlayback();
1901 resetSlideDrawings();
1902 // broadcast
1903 var message = new CustomEvent( messageType );
1904 message.content = {
1905 sender: 'chalkboard-plugin',
1906 type: 'resetSlide',
1907 timestamp: Date.now() - slideStart,
1908 mode,
1909 board
1910 };
1911 document.dispatchEvent( message );
1912 }
1913 };
1914
1915 function resetStorage( force ) {
1916 var ok = force || confirm( "Please confirm to delete all chalkboard drawings!" );
1917 if ( ok ) {
1918 stopPlayback();
1919 slideStart = Date.now();
1920 clearCanvas( 0 );
1921 clearCanvas( 1 );
1922 if ( mode == 1 ) {
1923 closeChalkboard();
1924 }
1925
1926 storage = [ {
1927 width: Reveal.getConfig().width,
1928 height: Reveal.getConfig().height,
1929 data: []
1930 },
1931 {
1932 width: Reveal.getConfig().width,
1933 height: Reveal.getConfig().height,
1934 data: []
1935 }
1936 ];
1937
1938 if ( config.storage ) {
1939 sessionStorage.setItem( config.storage, null )
1940 }
1941 // broadcast
1942 var message = new CustomEvent( messageType );
1943 message.content = {
1944 sender: 'chalkboard-plugin',
1945 type: 'init',
1946 timestamp: Date.now() - slideStart,
1947 storage,
1948 mode,
1949 board
1950 };
1951 document.dispatchEvent( message );
1952 }
1953 };
1954
1955 this.toggleNotesCanvas = toggleNotesCanvas;
1956 this.toggleChalkboard = toggleChalkboard;
1957 this.colorIndex = colorIndex;
1958 this.colorNext = colorNext;
1959 this.colorPrev = colorPrev;
1960 this.clear = clear;
1961 this.reset = resetSlide;
1962 this.resetAll = resetStorage;
1963 this.download = downloadData;
1964 this.updateStorage = updateStorage;
1965 this.getData = getData;
1966 this.configure = configure;
1967
1968
1969 for ( var key in keyBindings ) {
1970 if ( keyBindings[ key ] ) {
1971 Reveal.addKeyBinding( keyBindings[ key ], RevealChalkboard[ key ] );
1972 }
1973 };
1974
1975 return this;
1976};