PageSourceSearch

https://soci231-w9.netlify.app/index_files/libs/revealjs/plugin/reveal-chalkboard/plugin.js

js soci231-w9.netlify.app collected 2026-10-03 11:25:48 UTC 59,294 bytes, 1,985 lines download raw bytes

1/*****************************************************************
2 ** Author: Asvin Goel, [email protected]
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			// https://stackoverflow.com/a/6234804
675			// escape data for proper handling of quotes and line breaks
676			// in case malicious gets a chance to craft the exception message
677			error = String(error).replace(/&/g, "&amp;")
678					.replace(/</g, "&lt;")
679					.replace(/>/g, "&gt;")
680					.replace(/"/g, "&quot;")
681					.replace(/'/g, "&#039;");
682
683			a.innerHTML += ' (' + error + ')';
684		}
685		a.click();
686		document.body.removeChild( a );
687	}
688
689	/**
690	 * Returns data object for the slide with the given indices.
691	 */
692	function getSlideData( indices, id ) {
693		if ( id == undefined ) id = mode;
694		if ( !indices ) indices = slideIndices;
695		var data;
696		for ( var i = 0; i < storage[ id ].data.length; i++ ) {
697			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 ) {
698				data = storage[ id ].data[ i ];
699				return data;
700			}
701		}
702		var page = Number( Reveal.getCurrentSlide().getAttribute('data-pdf-page-number') ); 
703//console.log( indices, Reveal.getCurrentSlide() );
704		storage[ id ].data.push( {
705			slide: indices,
706			page,
707			events: [],
708			duration: 0
709		} );
710		data = storage[ id ].data[ storage[ id ].data.length - 1 ];
711		return data;
712	}
713
714	/**
715	 * Returns maximum duration of slide playback for both modes
716	 */
717	function getSlideDuration( indices ) {
718		if ( !indices ) indices = slideIndices;
719		var duration = 0;
720		for ( var id = 0; id < 2; id++ ) {
721			for ( var i = 0; i < storage[ id ].data.length; i++ ) {
722				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 ) {
723					duration = Math.max( duration, storage[ id ].data[ i ].duration );
724					break;
725				}
726			}
727		}
728//console.log( duration );
729		return duration;
730	}
731
732/*****************************************************************
733 ** Print
734 ******************************************************************/
735	var printMode = ( /print-pdf/gi ).test( window.location.search );
736//console.log("createPrintout" + printMode)
737
738	function addPageNumbers() {
739		// determine page number for printouts with fragments serialised
740		var slides = Reveal.getSlides();
741		var page = 0;
742		for ( var i=0; i < slides.length; i++) {
743			slides[i].setAttribute('data-pdf-page-number',page.toString());
744			// add number of fragments without fragment indices
745			var count = slides[i].querySelectorAll('.fragment:not([data-fragment-index])').length;
746			var fragments = slides[i].querySelectorAll('.fragment[data-fragment-index]');
747			for ( var j=0; j < fragments.length; j++) {
748				// increasenumber of fragments by highest fragment index (which start at 0)
749				if ( Number(fragments[j].getAttribute('data-fragment-index')) + 1 > count ) {
750					count = Number(fragments[j].getAttribute('data-fragment-index')) + 1;
751				}
752			}
753//console.log(count,fragments.length,( slides[i].querySelector('h1,h2,h3,h4')||{}).innerHTML, page); 
754			page += count + 1;
755		}
756	}
757
758	function createPrintout() {
759		//console.warn(Reveal.getTotalSlides(),Reveal.getSlidesElement());
760		if ( storage[ 1 ].data.length == 0 ) return;
761		console.log( 'Create printout(s) for ' + storage[ 1 ].data.length + " slides" );
762		drawingCanvas[ 0 ].container.style.opacity = 0; // do not print notes canvas
763		drawingCanvas[ 0 ].container.style.visibility = 'hidden';
764
765		var patImg = new Image();
766		patImg.onload = function () {
767			var slides = Reveal.getSlides();
768//console.log(slides);
769			for ( var i = storage[ 1 ].data.length - 1; i >= 0; i-- ) {
770				console.log( 'Create printout for slide ' + storage[ 1 ].data[ i ].slide.h + '.' + storage[ 1 ].data[ i ].slide.v );
771				var slideData = getSlideData( storage[ 1 ].data[ i ].slide, 1 );
772				var drawings = createDrawings( slideData, patImg );
773//console.log("Page:", storage[ 1 ].data[ i ].page );
774//console.log("Slide:", slides[storage[ 1 ].data[ i ].page] );
775				addDrawings( slides[storage[ 1 ].data[ i ].page], drawings );
776
777			}
778//			Reveal.sync();
779		};
780		patImg.src = background[ 1 ];
781	}
782
783
784	function cloneCanvas( oldCanvas ) {
785		//create a new canvas
786		var newCanvas = document.createElement( 'canvas' );
787		var context = newCanvas.getContext( '2d' );
788		//set dimensions
789		newCanvas.width = oldCanvas.width;
790		newCanvas.height = oldCanvas.height;
791		//apply the old canvas to the new one
792		context.drawImage( oldCanvas, 0, 0 );
793		//return the new canvas
794		return newCanvas;
795	}
796
797	function getCanvas( template, container, board ) {
798		var idx = container.findIndex( element => element.board === board );
799		if ( idx === -1 ) {
800			var canvas = cloneCanvas( template );
801			if ( !container.length ) {
802				idx = 0;
803				container.push( {
804					board,
805					canvas
806				} );
807			} else if ( board < container[ 0 ].board ) {
808				idx = 0;
809				container.unshift( {
810					board,
811					canvas
812				} );
813			} else if ( board > container[ container.length - 1 ].board ) {
814				idx = container.length;
815				container.push( {
816					board,
817					canvas
818				} );
819			}
820		}
821
822		return container[ idx ].canvas;
823	}
824
825	function createDrawings( slideData, patImg ) {
826		var width = Reveal.getConfig().width;
827		var height = Reveal.getConfig().height;
828		var scale = 1;
829		var xOffset = 0;
830		var yOffset = 0;
831		if ( width != storage[ 1 ].width || height != storage[ 1 ].height ) {
832			scale = Math.min( width / storage[ 1 ].width, height / storage[ 1 ].height );
833			xOffset = ( width - storage[ 1 ].width * scale ) / 2;
834			yOffset = ( height - storage[ 1 ].height * scale ) / 2;
835		}
836		mode = 1;
837		board = 0;
838//		console.log( 'Create printout(s) for slide ', slideData );
839
840		var drawings = [];
841		var template = document.createElement( 'canvas' );
842		template.width = width;
843		template.height = height;
844
845		var imgCtx = template.getContext( '2d' );
846		imgCtx.fillStyle = imgCtx.createPattern( patImg, 'repeat' );
847		imgCtx.rect( 0, 0, width, height );
848		imgCtx.fill();
849
850		for ( var j = 0; j < slideData.events.length; j++ ) {
851			switch ( slideData.events[ j ].type ) {
852			case 'draw':
853				draw[ 1 ]( getCanvas( template, drawings, board ).getContext( '2d' ),
854					xOffset + slideData.events[ j ].x1 * scale,
855					yOffset + slideData.events[ j ].y1 * scale,
856					xOffset + slideData.events[ j ].x2 * scale,
857					yOffset + slideData.events[ j ].y2 * scale,
858					yOffset + slideData.events[ j ].color
859				);
860				break;
861			case 'erase':
862				eraseWithSponge( getCanvas( template, drawings, board ).getContext( '2d' ),
863					xOffset + slideData.events[ j ].x * scale,
864					yOffset + slideData.events[ j ].y * scale
865				);
866				break;
867			case 'selectboard':
868				selectBoard( slideData.events[ j ].board );
869				break;
870			case 'clear':
871				getCanvas( template, drawings, board ).getContext( '2d' ).clearRect( 0, 0, width, height );
872				getCanvas( template, drawings, board ).getContext( '2d' ).fill();
873				break;
874			default:
875				break;
876			}
877		}
878
879		drawings = drawings.sort( ( a, b ) => a.board > b.board && 1 || -1 );
880
881		mode = 0;
882
883		return drawings;
884	}
885
886	function addDrawings( slide, drawings ) {
887		var parent = slide.parentElement.parentElement;
888		var nextSlide = slide.parentElement.nextElementSibling;
889
890		for ( var i = 0; i < drawings.length; i++ ) {
891			var newPDFPage = document.createElement( 'div' );
892			newPDFPage.classList.add( 'pdf-page' );
893			newPDFPage.style.height = Reveal.getConfig().height;
894			newPDFPage.append( drawings[ i ].canvas );
895//console.log("Add drawing", newPDFPage);
896			if ( nextSlide != null ) {
897				parent.insertBefore( newPDFPage, nextSlide );
898			} else {
899				parent.append( newPDFPage );
900			}
901		}
902	}
903
904	/*****************************************************************
905	 ** Drawings
906	 ******************************************************************/
907
908	function drawWithBoardmarker( context, fromX, fromY, toX, toY, colorIdx ) {
909		if ( colorIdx == undefined ) colorIdx = color[ mode ];
910		context.lineWidth = boardmarkerWidth;
911		context.lineCap = 'round';
912		context.strokeStyle = boardmarkers[ colorIdx ].color;
913		context.beginPath();
914		context.moveTo( fromX, fromY );
915		context.lineTo( toX, toY );
916		context.stroke();
917	}
918
919	function drawWithChalk( context, fromX, fromY, toX, toY, colorIdx ) {
920		if ( colorIdx == undefined ) colorIdx = color[ mode ];
921		var brushDiameter = chalkWidth;
922		context.lineWidth = brushDiameter;
923		context.lineCap = 'round';
924		context.fillStyle = chalks[ colorIdx ].color; // 'rgba(255,255,255,0.5)';
925		context.strokeStyle = chalks[ colorIdx ].color;
926		/*var opacity = Math.min(0.8, Math.max(0,color[1].replace(/^.*,(.+)\)/,'$1') - 0.1)) + Math.random()*0.2;*/
927		var opacity = 1.0;
928		context.strokeStyle = context.strokeStyle.replace( /[\d\.]+\)$/g, opacity + ')' );
929		context.beginPath();
930		context.moveTo( fromX, fromY );
931		context.lineTo( toX, toY );
932		context.stroke();
933		// Chalk Effect
934		var length = Math.round( Math.sqrt( Math.pow( toX - fromX, 2 ) + Math.pow( toY - fromY, 2 ) ) / ( 5 / brushDiameter ) );
935		var xUnit = ( toX - fromX ) / length;
936		var yUnit = ( toY - fromY ) / length;
937		for ( var i = 0; i < length; i++ ) {
938			if ( chalkEffect > ( Math.random() * 0.9 ) ) {
939				var xCurrent = fromX + ( i * xUnit );
940				var yCurrent = fromY + ( i * yUnit );
941				var xRandom = xCurrent + ( Math.random() - 0.5 ) * brushDiameter * 1.2;
942				var yRandom = yCurrent + ( Math.random() - 0.5 ) * brushDiameter * 1.2;
943				context.clearRect( xRandom, yRandom, Math.random() * 2 + 2, Math.random() + 1 );
944			}
945		}
946	}
947 
948	function eraseWithSponge( context, x, y ) {
949		context.save();
950		context.beginPath();
951		context.arc( x, y, eraser.radius, 0, 2 * Math.PI, false );
952		context.clip();
953		context.clearRect( x - eraser.radius - 1, y - eraser.radius - 1, eraser.radius * 2 + 2, eraser.radius * 2 + 2 );
954		context.restore();
955		if ( mode == 1 && grid ) {
956			redrawGrid( x, y, eraser.radius );
957		}
958	}
959
960
961	/**
962	 * Show an overlay for the chalkboard.
963	 */
964	function showChalkboard() {
965//console.log("showChalkboard");
966		clearTimeout( touchTimeout );
967		touchTimeout = null;
968		drawingCanvas[ 0 ].sponge.style.visibility = 'hidden'; // make sure that the sponge from touch events is hidden
969		drawingCanvas[ 1 ].sponge.style.visibility = 'hidden'; // make sure that the sponge from touch events is hidden
970		drawingCanvas[ 1 ].container.style.opacity = 1;
971		drawingCanvas[ 1 ].container.style.visibility = 'visible';
972		mode = 1;
973	}
974
975
976	/**
977	 * Closes open chalkboard.
978	 */
979	function closeChalkboard() {
980		clearTimeout( touchTimeout );
981		touchTimeout = null;
982		drawingCanvas[ 0 ].sponge.style.visibility = 'hidden'; // make sure that the sponge from touch events is hidden
983		drawingCanvas[ 1 ].sponge.style.visibility = 'hidden'; // make sure that the sponge from touch events is hidden
984		drawingCanvas[ 1 ].container.style.opacity = 0;
985		drawingCanvas[ 1 ].container.style.visibility = 'hidden';
986		lastX = null;
987		lastY = null;
988		mode = 0;
989	}
990
991	/**
992	 * Clear current canvas.
993	 */
994	function clearCanvas( id ) {
995		if ( id == 0 ) clearTimeout( slidechangeTimeout );
996		drawingCanvas[ id ].context.clearRect( 0, 0, drawingCanvas[ id ].width, drawingCanvas[ id ].height );
997		if ( id == 1 && grid ) drawGrid();
998	}
999
1000	/**
1001	 * Draw grid on background
1002	 */
1003	function drawGrid() {
1004		var context = drawingCanvas[ 1 ].context;
1005
1006		drawingCanvas[ 1 ].scale = Math.min( drawingCanvas[ 1 ].width / storage[ 1 ].width, drawingCanvas[ 1 ].height / storage[ 1 ].height );
1007		drawingCanvas[ 1 ].xOffset = ( drawingCanvas[ 1 ].width - storage[ 1 ].width * drawingCanvas[ 1 ].scale ) / 2;
1008		drawingCanvas[ 1 ].yOffset = ( drawingCanvas[ 1 ].height - storage[ 1 ].height * drawingCanvas[ 1 ].scale ) / 2;
1009
1010		var scale = drawingCanvas[ 1 ].scale;
1011		var xOffset = drawingCanvas[ 1 ].xOffset;
1012		var yOffset = drawingCanvas[ 1 ].yOffset;
1013
1014		var distance = grid.distance * scale;
1015
1016		var fromX = drawingCanvas[ 1 ].width / 2 - distance / 2 - Math.floor( ( drawingCanvas[ 1 ].width - distance ) / 2 / distance ) * distance;
1017		for ( var x = fromX; x < drawingCanvas[ 1 ].width; x += distance ) {
1018			context.beginPath();
1019			context.lineWidth = grid.width * scale;
1020			context.lineCap = 'round';
1021			context.fillStyle = grid.color;
1022			context.strokeStyle = grid.color;
1023			context.moveTo( x, 0 );
1024			context.lineTo( x, drawingCanvas[ 1 ].height );
1025			context.stroke();
1026		}
1027		var fromY = drawingCanvas[ 1 ].height / 2 - distance / 2 - Math.floor( ( drawingCanvas[ 1 ].height - distance ) / 2 / distance ) * distance;
1028
1029		for ( var y = fromY; y < drawingCanvas[ 1 ].height; y += distance ) {
1030			context.beginPath();
1031			context.lineWidth = grid.width * scale;
1032			context.lineCap = 'round';
1033			context.fillStyle = grid.color;
1034			context.strokeStyle = grid.color;
1035			context.moveTo( 0, y );
1036			context.lineTo( drawingCanvas[ 1 ].width, y );
1037			context.stroke();
1038		}
1039	}
1040
1041	function redrawGrid( centerX, centerY, diameter ) {
1042		var context = drawingCanvas[ 1 ].context;
1043
1044		drawingCanvas[ 1 ].scale = Math.min( drawingCanvas[ 1 ].width / storage[ 1 ].width, drawingCanvas[ 1 ].height / storage[ 1 ].height );
1045		drawingCanvas[ 1 ].xOffset = ( drawingCanvas[ 1 ].width - storage[ 1 ].width * drawingCanvas[ 1 ].scale ) / 2;
1046		drawingCanvas[ 1 ].yOffset = ( drawingCanvas[ 1 ].height - storage[ 1 ].height * drawingCanvas[ 1 ].scale ) / 2;
1047
1048		var scale = drawingCanvas[ 1 ].scale;
1049		var xOffset = drawingCanvas[ 1 ].xOffset;
1050		var yOffset = drawingCanvas[ 1 ].yOffset;
1051
1052		var distance = grid.distance * scale;
1053
1054		var fromX = drawingCanvas[ 1 ].width / 2 - distance / 2 - Math.floor( ( drawingCanvas[ 1 ].width - distance ) / 2 / distance ) * distance;
1055
1056		for ( var x = fromX + distance * Math.ceil( ( centerX - diameter - fromX ) / distance ); x <= fromX + distance * Math.floor( ( centerX + diameter - fromX ) / distance ); x += distance ) {
1057			context.beginPath();
1058			context.lineWidth = grid.width * scale;
1059			context.lineCap = 'round';
1060			context.fillStyle = grid.color;
1061			context.strokeStyle = grid.color;
1062			context.moveTo( x, centerY - Math.sqrt( diameter * diameter - ( centerX - x ) * ( centerX - x ) ) );
1063			context.lineTo( x, centerY + Math.sqrt( diameter * diameter - ( centerX - x ) * ( centerX - x ) ) );
1064			context.stroke();
1065		}
1066		var fromY = drawingCanvas[ 1 ].height / 2 - distance / 2 - Math.floor( ( drawingCanvas[ 1 ].height - distance ) / 2 / distance ) * distance;
1067		for ( var y = fromY + distance * Math.ceil( ( centerY - diameter - fromY ) / distance ); y <= fromY + distance * Math.floor( ( centerY + diameter - fromY ) / distance ); y += distance ) {
1068			context.beginPath();
1069			context.lineWidth = grid.width * scale;
1070			context.lineCap = 'round';
1071			context.fillStyle = grid.color;
1072			context.strokeStyle = grid.color;
1073			context.moveTo( centerX - Math.sqrt( diameter * diameter - ( centerY - y ) * ( centerY - y ) ), y );
1074			context.lineTo( centerX + Math.sqrt( diameter * diameter - ( centerY - y ) * ( centerY - y ) ), y );
1075			context.stroke();
1076		}
1077	}
1078
1079	/**
1080	 * Set the  color
1081	 */
1082	function setColor( index, record ) {
1083		// protect against out of bounds (this could happen when
1084		// replaying events recorded with different color settings).
1085		if ( index >= pens[ mode ].length ) index = 0;
1086		color[ mode ] = index;
1087		drawingCanvas[ mode ].canvas.style.cursor = pens[ mode ][ color[ mode ] ].cursor;
1088	}
1089
1090	/**
1091	 * Set the  board
1092	 */
1093	function selectBoard( boardIdx, record ) {
1094//console.log("Set board",boardIdx);
1095		if ( board == boardIdx ) return;
1096
1097		board = boardIdx;
1098		redrawChalkboard( boardIdx );
1099		if ( record ) {
1100			recordEvent( { type: 'selectboard' } );
1101		}
1102	}
1103
1104	function redrawChalkboard( boardIdx ) {
1105		clearCanvas( 1 );
1106		var slideData = getSlideData( slideIndices, 1 );
1107		var index = 0;
1108		var play = ( boardIdx == 0 );
1109		while ( index < slideData.events.length && slideData.events[ index ].time < Date.now() - slideStart ) {
1110			if ( boardIdx == slideData.events[ index ].board ) {
1111				playEvent( 1, slideData.events[ index ], Date.now() - slideStart );
1112			}
1113
1114			index++;
1115		}
1116	}
1117
1118
1119	/**
1120	 * Forward cycle color
1121	 */
1122	function cycleColorNext() {
1123		color[ mode ] = ( color[ mode ] + 1 ) % pens[ mode ].length;
1124		return color[ mode ];
1125	}
1126
1127	/**
1128	 * Backward cycle color
1129	 */
1130	function cycleColorPrev() {
1131		color[ mode ] = ( color[ mode ] + ( pens[ mode ].length - 1 ) ) % pens[ mode ].length;
1132		return color[ mode ];
1133	}
1134
1135/*****************************************************************
1136 ** Broadcast
1137 ******************************************************************/
1138
1139	var eventQueue = [];
1140
1141	document.addEventListener( 'received', function ( message ) {
1142		if ( message.content && message.content.sender == 'chalkboard-plugin' ) {
1143			// add message to queue
1144			eventQueue.push( message );
1145			console.log( JSON.stringify( message ) );
1146		}
1147		if ( eventQueue.length == 1 ) processQueue();
1148	} );
1149
1150	function processQueue() {
1151		// take first message from queue
1152		var message = eventQueue.shift();
1153
1154		// synchronize time with seminar host
1155		slideStart = Date.now() - message.content.timestamp;
1156		// set status
1157		if ( mode < message.content.mode ) {
1158			// open chalkboard
1159			showChalkboard();
1160		} else if ( mode > message.content.mode ) {
1161			// close chalkboard
1162			closeChalkboard();
1163		}
1164		if ( board != message.content.board ) {
1165			board = message.content.board;
1166			redrawChalkboard( board );
1167		};
1168
1169		switch ( message.content.type ) {
1170		case 'showChalkboard':
1171			showChalkboard();
1172			break;
1173		case 'closeChalkboard':
1174			closeChalkboard();
1175			break;
1176		case 'erase':
1177			erasePoint( message.content.x, message.content.y );
1178			break;
1179		case 'draw':
1180			drawSegment( message.content.fromX, message.content.fromY, message.content.toX, message.content.toY, message.content.color );
1181			break;
1182		case 'clear':
1183			clearSlide();
1184			break;
1185		case 'selectboard':
1186			selectBoard( message.content.board, true );
1187			break;
1188		case 'resetSlide':
1189			resetSlideDrawings();
1190			break;
1191		case 'init':
1192			storage = message.content.storage;
1193			for ( var id = 0; id < 2; id++ ) {
1194				drawingCanvas[ id ].scale = Math.min( drawingCanvas[ id ].width / storage[ id ].width, drawingCanvas[ id ].height / storage[ id ].height );
1195				drawingCanvas[ id ].xOffset = ( drawingCanvas[ id ].width - storage[ id ].width * drawingCanvas[ id ].scale ) / 2;
1196				drawingCanvas[ id ].yOffset = ( drawingCanvas[ id ].height - storage[ id ].height * drawingCanvas[ id ].scale ) / 2;
1197			}
1198			clearCanvas( 0 );
1199			clearCanvas( 1 );
1200			if ( !playback ) {
1201				slidechangeTimeout = setTimeout( startPlayback, transition, getSlideDuration(), 0 );
1202			}
1203			if ( mode == 1 && message.content.mode == 0 ) {
1204				setTimeout( closeChalkboard, transition + 50 );
1205			}
1206			if ( mode == 0 && message.content.mode == 1 ) {
1207				setTimeout( showChalkboard, transition + 50 );
1208			}
1209			mode = message.content.mode;
1210			board = message.content.board;
1211			break;
1212		default:
1213			break;
1214		}
1215
1216		// continue with next message if queued
1217		if ( eventQueue.length > 0 ) {
1218			processQueue();
1219		} else {
1220			storageChanged();
1221		}
1222	}
1223
1224	document.addEventListener( 'welcome', function ( user ) {
1225		// broadcast storage
1226		var message = new CustomEvent( messageType );
1227		message.content = {
1228			sender: 'chalkboard-plugin',
1229			recipient: user.id,
1230			type: 'init',
1231			timestamp: Date.now() - slideStart,
1232			storage: storage,
1233			mode,
1234			board
1235		};
1236		document.dispatchEvent( message );
1237	} );
1238
1239	/*****************************************************************
1240	 ** Playback
1241	 ******************************************************************/
1242
1243	document.addEventListener( 'seekplayback', function ( event ) {
1244//console.log('event seekplayback ' + event.timestamp);
1245		stopPlayback();
1246		if ( !playback || event.timestamp == 0 ) {
1247			// in other cases startplayback fires after seeked
1248			startPlayback( event.timestamp );
1249		}
1250		//console.log('seeked');
1251	} );
1252
1253
1254	document.addEventListener( 'startplayback', function ( event ) {
1255//console.log('event startplayback ' + event.timestamp);
1256		stopPlayback();
1257		playback = true;
1258		startPlayback( event.timestamp );
1259	} );
1260
1261	document.addEventListener( 'stopplayback', function ( event ) {
1262//console.log('event stopplayback ' + (Date.now() - slideStart) );
1263		playback = false;
1264		stopPlayback();
1265	} );
1266
1267	document.addEventListener( 'startrecording', function ( event ) {
1268//console.log('event startrecording ' + event.timestamp);
1269		startRecording();
1270	} );
1271
1272
1273	function startRecording() {
1274		resetSlide( true );
1275		slideStart = Date.now();
1276	}
1277
1278	function startPlayback( timestamp, finalMode ) {
1279//console.log("playback " + timestamp );
1280		slideStart = Date.now() - timestamp;
1281		closeChalkboard();
1282		mode = 0;
1283		board = 0;
1284		for ( var id = 0; id < 2; id++ ) {
1285			clearCanvas( id );
1286			var slideData = getSlideData( slideIndices, id );
1287//console.log( timestamp +" / " + JSON.stringify(slideData));
1288			var index = 0;
1289			while ( index < slideData.events.length && slideData.events[ index ].time < ( Date.now() - slideStart ) ) {
1290				playEvent( id, slideData.events[ index ], timestamp );
1291				index++;
1292			}
1293
1294			while ( playback && index < slideData.events.length ) {
1295				timeouts[ id ].push( setTimeout( playEvent, slideData.events[ index ].time - ( Date.now() - slideStart ), id, slideData.events[ index ], timestamp ) );
1296				index++;
1297			}
1298		}
1299//console.log("Mode: " + finalMode + "/" + mode );
1300		if ( finalMode != undefined ) {
1301			mode = finalMode;
1302		}
1303		if ( mode == 1 ) showChalkboard();
1304//console.log("playback (ok)");
1305
1306	};
1307
1308	function stopPlayback() {
1309//console.log("stopPlayback");
1310//console.log("Timeouts: " + timeouts[0].length + "/"+ timeouts[1].length);
1311		for ( var id = 0; id < 2; id++ ) {
1312			for ( var i = 0; i < timeouts[ id ].length; i++ ) {
1313				clearTimeout( timeouts[ id ][ i ] );
1314			}
1315			timeouts[ id ] = [];
1316		}
1317	};
1318
1319	function playEvent( id, event, timestamp ) {
1320//console.log( timestamp +" / " + JSON.stringify(event));
1321//console.log( id + ": " + timestamp +" / " +  event.time +" / " + event.type +" / " + mode );
1322		switch ( event.type ) {
1323		case 'open':
1324			if ( timestamp <= event.time ) {
1325				showChalkboard();
1326			} else {
1327				mode = 1;
1328			}
1329
1330			break;
1331		case 'close':
1332			if ( timestamp < event.time ) {
1333				closeChalkboard();
1334			} else {
1335				mode = 0;
1336			}
1337			break;
1338		case 'clear':
1339			clearCanvas( id );
1340			break;
1341		case 'selectboard':
1342			selectBoard( event.board );
1343			break;
1344		case 'draw':
1345			drawLine( id, event, timestamp );
1346			break;
1347		case 'erase':
1348			eraseCircle( id, event, timestamp );
1349			break;
1350		}
1351	};
1352
1353	function drawLine( id, event, timestamp ) {
1354		var ctx = drawingCanvas[ id ].context;
1355		var scale = drawingCanvas[ id ].scale;
1356		var xOffset = drawingCanvas[ id ].xOffset;
1357		var yOffset = drawingCanvas[ id ].yOffset;
1358		draw[ id ]( ctx, xOffset + event.x1 * scale, yOffset + event.y1 * scale, xOffset + event.x2 * scale, yOffset + event.y2 * scale, event.color );
1359	};
1360
1361	function eraseCircle( id, event, timestamp ) {
1362		var ctx = drawingCanvas[ id ].context;
1363		var scale = drawingCanvas[ id ].scale;
1364		var xOffset = drawingCanvas[ id ].xOffset;
1365		var yOffset = drawingCanvas[ id ].yOffset;
1366
1367		eraseWithSponge( ctx, xOffset + event.x * scale, yOffset + event.y * scale );
1368	};
1369
1370	function startErasing( x, y ) {
1371		drawing = false;
1372		erasing = true;
1373		drawingCanvas[ mode ].sponge.style.visibility = 'visible';
1374		erasePoint( x, y );
1375	}
1376
1377	function erasePoint( x, y ) {
1378		var ctx = drawingCanvas[ mode ].context;
1379		var scale = drawingCanvas[ mode ].scale;
1380		var xOffset = drawingCanvas[ mode ].xOffset;
1381		var yOffset = drawingCanvas[ mode ].yOffset;
1382
1383		// move sponge image
1384		drawingCanvas[ mode ].sponge.style.left = ( x * scale + xOffset - eraser.radius ) + 'px';
1385		drawingCanvas[ mode ].sponge.style.top = ( y * scale + yOffset - 2 * eraser.radius ) + 'px';
1386
1387		recordEvent( {
1388			type: 'erase',
1389			x,
1390			y
1391		} );
1392
1393		if (
1394			x * scale + xOffset > 0 &&
1395			y * scale + yOffset > 0 &&
1396			x * scale + xOffset < drawingCanvas[ mode ].width &&
1397			y * scale + yOffset < drawingCanvas[ mode ].height
1398		) {
1399			eraseWithSponge( ctx, x * scale + xOffset, y * scale + yOffset );
1400		}
1401	}
1402
1403	function stopErasing() {
1404		erasing = false;
1405		// hide sponge
1406		drawingCanvas[ mode ].sponge.style.visibility = 'hidden';
1407	}
1408
1409	function startDrawing( x, y ) {
1410		drawing = true;
1411
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		lastX = x * scale + xOffset;
1417		lastY = y * scale + yOffset;
1418	}
1419
1420	function drawSegment( fromX, fromY, toX, toY, colorIdx ) {
1421		var ctx = drawingCanvas[ mode ].context;
1422		var scale = drawingCanvas[ mode ].scale;
1423		var xOffset = drawingCanvas[ mode ].xOffset;
1424		var yOffset = drawingCanvas[ mode ].yOffset;
1425
1426		recordEvent( {
1427			type: 'draw',
1428			color: colorIdx,
1429			x1: fromX,
1430			y1: fromY,
1431			x2: toX,
1432			y2: toY
1433		} );
1434
1435		if (
1436			fromX * scale + xOffset > 0 &&
1437			fromY * scale + yOffset > 0 &&
1438			fromX * scale + xOffset < drawingCanvas[ mode ].width &&
1439			fromY * scale + yOffset < drawingCanvas[ mode ].height &&
1440			toX * scale + xOffset > 0 &&
1441			toY * scale + yOffset > 0 &&
1442			toX * scale + xOffset < drawingCanvas[ mode ].width &&
1443			toY * scale + yOffset < drawingCanvas[ mode ].height
1444		) {
1445			draw[ mode ]( ctx, fromX * scale + xOffset, fromY * scale + yOffset, toX * scale + xOffset, toY * scale + yOffset, colorIdx );
1446		}
1447	}
1448
1449	function stopDrawing() {
1450		drawing = false;
1451	}
1452
1453
1454/*****************************************************************
1455 ** User interface
1456 ******************************************************************/
1457
1458	function setupCanvasEvents( canvas ) {
1459// TODO: check all touchevents
1460		canvas.addEventListener( 'touchstart', function ( evt ) {
1461			evt.preventDefault();
1462//console.log("Touch start");
1463			if ( !readOnly && evt.target.getAttribute( 'data-chalkboard' ) == mode ) {
1464				var scale = drawingCanvas[ mode ].scale;
1465				var xOffset = drawingCanvas[ mode ].xOffset;
1466				var yOffset = drawingCanvas[ mode ].yOffset;
1467
1468				var touch = evt.touches[ 0 ];
1469				mouseX = touch.pageX;
1470				mouseY = touch.pageY;
1471				startDrawing( ( mouseX - xOffset ) / scale, ( mouseY - yOffset ) / scale );
1472				touchTimeout = setTimeout( startErasing, 500,  ( mouseX - xOffset ) / scale, ( mouseY - yOffset ) / scale );
1473			}
1474		}, passiveSupported ? {
1475			passive: false
1476		} : false );
1477
1478		canvas.addEventListener( 'touchmove', function ( evt ) {
1479			evt.preventDefault();
1480//console.log("Touch move");
1481			clearTimeout( touchTimeout );
1482			touchTimeout = null;
1483			if ( drawing || erasing ) {
1484				var scale = drawingCanvas[ mode ].scale;
1485				var xOffset = drawingCanvas[ mode ].xOffset;
1486				var yOffset = drawingCanvas[ mode ].yOffset;
1487
1488				var touch = evt.touches[ 0 ];
1489				mouseX = touch.pageX;
1490				mouseY = touch.pageY;
1491				if ( mouseY < drawingCanvas[ mode ].height && mouseX < drawingCanvas[ mode ].width ) {
1492					// move sponge
1493					if ( event.type == 'erase' ) {
1494						drawingCanvas[ mode ].sponge.style.left = ( mouseX - eraser.radius ) + 'px';
1495						drawingCanvas[ mode ].sponge.style.top = ( mouseY - eraser.radius ) + 'px';
1496					}
1497				}
1498
1499				if ( drawing ) {
1500					drawSegment( ( lastX - xOffset ) / scale, ( lastY - yOffset ) / scale, ( mouseX - xOffset ) / scale, ( mouseY - yOffset ) / scale, color[ mode ] );
1501					// broadcast
1502					var message = new CustomEvent( messageType );
1503					message.content = {
1504						sender: 'chalkboard-plugin',
1505						type: 'draw',
1506						timestamp: Date.now() - slideStart,
1507						mode,
1508						board,
1509						fromX: ( lastX - xOffset ) / scale,
1510						fromY: ( lastY - yOffset ) / scale,
1511						toX: ( mouseX - xOffset ) / scale,
1512						toY: ( mouseY - yOffset ) / scale,
1513						color: color[ mode ]
1514					};
1515					document.dispatchEvent( message );
1516
1517					lastX = mouseX;
1518					lastY = mouseY;
1519				} else {
1520					erasePoint( ( mouseX - xOffset ) / scale, ( mouseY - yOffset ) / scale );
1521					// broadcast
1522					var message = new CustomEvent( messageType );
1523					message.content = {
1524						sender: 'chalkboard-plugin',
1525						type: 'erase',
1526						timestamp: Date.now() - slideStart,
1527						mode,
1528						board,
1529						x: ( mouseX - xOffset ) / scale,
1530						y: ( mouseY - yOffset ) / scale
1531					};
1532					document.dispatchEvent( message );
1533				}
1534
1535			}
1536		}, false );
1537
1538
1539		canvas.addEventListener( 'touchend', function ( evt ) {
1540			evt.preventDefault();
1541			clearTimeout( touchTimeout );
1542			touchTimeout = null;
1543			// hide sponge image
1544			drawingCanvas[ mode ].sponge.style.visibility = 'hidden';
1545			stopDrawing();
1546		}, false );
1547
1548		canvas.addEventListener( 'mousedown', function ( evt ) {
1549			evt.preventDefault();
1550			if ( !readOnly && evt.target.getAttribute( 'data-chalkboard' ) == mode ) {
1551//console.log( "mousedown: " + evt.button );
1552				var scale = drawingCanvas[ mode ].scale;
1553				var xOffset = drawingCanvas[ mode ].xOffset;
1554				var yOffset = drawingCanvas[ mode ].yOffset;
1555
1556				mouseX = evt.pageX;
1557				mouseY = evt.pageY;
1558
1559				if ( evt.button == 2 || evt.button == 1 ) {
1560					startErasing( ( mouseX - xOffset ) / scale, ( mouseY - yOffset ) / scale );
1561					// broadcast
1562					var message = new CustomEvent( messageType );
1563					message.content = {
1564						sender: 'chalkboard-plugin',
1565						type: 'erase',
1566						timestamp: Date.now() - slideStart,
1567						mode,
1568						board,
1569						x: ( mouseX - xOffset ) / scale,
1570						y: ( mouseY - yOffset ) / scale
1571					};
1572					document.dispatchEvent( message );
1573				} else {
1574					startDrawing( ( mouseX - xOffset ) / scale, ( mouseY - yOffset ) / scale );
1575				}
1576			}
1577		} );
1578
1579		canvas.addEventListener( 'mousemove', function ( evt ) {
1580			evt.preventDefault();
1581//console.log("Mouse move");
1582			if ( drawing || erasing ) {
1583				var scale = drawingCanvas[ mode ].scale;
1584				var xOffset = drawingCanvas[ mode ].xOffset;
1585				var yOffset = drawingCanvas[ mode ].yOffset;
1586
1587				mouseX = evt.pageX;
1588				mouseY = evt.pageY;
1589
1590				if ( drawing ) {
1591					drawSegment( ( lastX - xOffset ) / scale, ( lastY - yOffset ) / scale, ( mouseX - xOffset ) / scale, ( mouseY - yOffset ) / scale, color[ mode ] );
1592					// broadcast
1593					var message = new CustomEvent( messageType );
1594					message.content = {
1595						sender: 'chalkboard-plugin',
1596						type: 'draw',
1597						timestamp: Date.now() - slideStart,
1598						mode,
1599						board,
1600						fromX: ( lastX - xOffset ) / scale,
1601						fromY: ( lastY - yOffset ) / scale,
1602						toX: ( mouseX - xOffset ) / scale,
1603						toY: ( mouseY - yOffset ) / scale,
1604						color: color[ mode ]
1605					};
1606					document.dispatchEvent( message );
1607
1608					lastX = mouseX;
1609					lastY = mouseY;
1610				} else {
1611					erasePoint( ( mouseX - xOffset ) / scale, ( mouseY - yOffset ) / scale );
1612					// broadcast
1613					var message = new CustomEvent( messageType );
1614					message.content = {
1615						sender: 'chalkboard-plugin',
1616						type: 'erase',
1617						timestamp: Date.now() - slideStart,
1618						mode,
1619						board,
1620						x: ( mouseX - xOffset ) / scale,
1621						y: ( mouseY - yOffset ) / scale
1622					};
1623					document.dispatchEvent( message );
1624				}
1625
1626			}
1627		} );
1628
1629
1630		canvas.addEventListener( 'mouseup', function ( evt ) {
1631			evt.preventDefault();
1632			drawingCanvas[ mode ].canvas.style.cursor = pens[ mode ][ color[ mode ] ].cursor;
1633			if ( drawing || erasing ) {
1634				stopDrawing();
1635				stopErasing();
1636			}
1637		} );
1638	}
1639
1640	function resize() {
1641//console.log("resize");
1642		// Resize the canvas and draw everything again
1643		var timestamp = Date.now() - slideStart;
1644		if ( !playback ) {
1645			timestamp = getSlideDuration();
1646		}
1647
1648//console.log( drawingCanvas[0].scale + "/" + drawingCanvas[0].xOffset + "/" 
1648+drawingCanvas[0].yOffset );
1649		for ( var id = 0; id < 2; id++ ) {
1650			drawingCanvas[ id ].width = window.innerWidth;
1651			drawingCanvas[ id ].height = window.innerHeight;
1652			drawingCanvas[ id ].canvas.width = drawingCanvas[ id ].width;
1653			drawingCanvas[ id ].canvas.height = drawingCanvas[ id ].height;
1654			drawingCanvas[ id ].context.canvas.width = drawingCanvas[ id ].width;
1655			drawingCanvas[ id ].context.canvas.height = drawingCanvas[ id ].height;
1656
1657			drawingCanvas[ id ].scale = Math.min( drawingCanvas[ id ].width / storage[ id ].width, drawingCanvas[ id ].height / storage[ id ].height );
1658			drawingCanvas[ id ].xOffset = ( drawingCanvas[ id ].width - storage[ id ].width * drawingCanvas[ id ].scale ) / 2;
1659			drawingCanvas[ id ].yOffset = ( drawingCanvas[ id ].height - storage[ id ].height * drawingCanvas[ id ].scale ) / 2;
1660//console.log( drawingCanvas[id].scale + "/" + drawingCanvas[id].xOffset + "/" +drawingCanvas[id].yOffset );
1661		}
1662//console.log( window.innerWidth + "/" + window.innerHeight);
1663		startPlayback( timestamp, mode, true );
1664	}
1665
1666	Reveal.addEventListener( 'pdf-ready', function ( evt ) {
1667//		console.log( "Create printouts when ready" );
1668		whenLoaded( createPrintout );
1669	});
1670
1671	Reveal.addEventListener( 'ready', function ( evt ) {
1672//console.log('ready');
1673		if ( !printMode ) {
1674			window.addEventListener( 'resize', resize );
1675
1676			slideStart = Date.now() - getSlideDuration();
1677			slideIndices = Reveal.getIndices();
1678			if ( !playback ) {
1679				startPlayback( getSlideDuration(), 0 );
1680			}
1681			if ( Reveal.isAutoSliding() ) {
1682				var event = new CustomEvent( 'startplayback' );
1683				event.timestamp = 0;
1684				document.dispatchEvent( event );
1685			}
1686			updateStorage();
1687			whenReady( addPageNumbers );
1688		}
1689	} );
1690	Reveal.addEventListener( 'slidechanged', function ( evt ) {
1691//		clearTimeout( slidechangeTimeout );
1692//console.log('slidechanged');
1693		if ( !printMode ) {
1694			slideStart = Date.now() - getSlideDuration();
1695			slideIndices = Reveal.getIndices();
1696			closeChalkboard();
1697			board = 0;
1698			clearCanvas( 0 );
1699			clearCanvas( 1 );
1700			if ( !playback ) {
1701				slidechangeTimeout = setTimeout( startPlayback, transition, getSlideDuration(), 0 );
1702			}
1703			if ( Reveal.isAutoSliding() ) {
1704				var event = new CustomEvent( 'startplayback' );
1705				event.timestamp = 0;
1706				document.dispatchEvent( event );
1707			}
1708		}
1709	} );
1710	Reveal.addEventListener( 'fragmentshown', function ( evt ) {
1711//		clearTimeout( slidechangeTimeout );
1712//console.log('fragmentshown');
1713		if ( !printMode ) {
1714			slideStart = Date.now() - getSlideDuration();
1715			slideIndices = Reveal.getIndices();
1716			closeChalkboard();
1717			board = 0;
1718			clearCanvas( 0 );
1719			clearCanvas( 1 );
1720			if ( Reveal.isAutoSliding() ) {
1721				var event = new CustomEvent( 'startplayback' );
1722				event.timestamp = 0;
1723				document.dispatchEvent( event );
1724			} else if ( !playback ) {
1725				startPlayback( getSlideDuration(), 0 );
1726//				closeChalkboard();
1727			}
1728		}
1729	} );
1730	Reveal.addEventListener( 'fragmenthidden', function ( evt ) {
1731//		clearTimeout( slidechangeTimeout );
1732//console.log('fragmenthidden');
1733		if ( !printMode ) {
1734			slideStart = Date.now() - getSlideDuration();
1735			slideIndices = Reveal.getIndices();
1736			closeChalkboard();
1737			board = 0;
1738			clearCanvas( 0 );
1739			clearCanvas( 1 );
1740			if ( Reveal.isAutoSliding() ) {
1741				document.dispatchEvent( new CustomEvent( 'stopplayback' ) );
1742			} else if ( !playback ) {
1743				startPlayback( getSlideDuration() );
1744				closeChalkboard();
1745			}
1746		}
1747	} );
1748
1749	Reveal.addEventListener( 'autoslideresumed', function ( evt ) {
1750//console.log('autoslideresumed');
1751		var event = new CustomEvent( 'startplayback' );
1752		event.timestamp = 0;
1753		document.dispatchEvent( event );
1754	} );
1755	Reveal.addEventListener( 'autoslidepaused', function ( evt ) {
1756//console.log('autoslidepaused');
1757		document.dispatchEvent( new CustomEvent( 'stopplayback' ) );
1758
1759		// advance to end of slide
1760//		closeChalkboard();
1761		startPlayback( getSlideDuration(), 0 );
1762	} );
1763
1764	function toggleNotesCanvas() {
1765		if ( !readOnly ) {
1766			if ( mode == 1 ) {
1767				toggleChalkboard();
1768				notescanvas.style.background = background[ 0 ]; //'rgba(255,0,0,0.5)';
1769				notescanvas.style.pointerEvents = 'auto';
1770			}
1771			else {
1772				if ( notescanvas.style.pointerEvents != 'none' ) {
1773					// hide notes canvas
1774					if ( colorButtons ) {
1775						notescanvas.querySelector( '.palette' ).style.visibility = 'hidden';
1776					}
1777					notescanvas.style.background = 'rgba(0,0,0,0)';
1778					notescanvas.style.pointerEvents = 'none';
1779				}
1780				else {
1781					// show notes canvas
1782					if ( colorButtons ) {
1783						notescanvas.querySelector( '.palette' ).style.visibility = 'visible';
1784					}
1785					notescanvas.style.background = background[ 0 ]; //'rgba(255,0,0,0.5)';
1786					notescanvas.style.pointerEvents = 'auto';
1787
1788					var idx = 0;
1789					if ( color[ mode ] ) {
1790						idx = color[ mode ];
1791					}
1792
1793					setColor( idx, true );
1794				}
1795			}
1796		}
1797	};
1798
1799	function toggleChalkboard() {
1800//console.log("toggleChalkboard " + mode);
1801		if ( mode == 1 ) {
1802			if ( !readOnly ) {
1803				recordEvent( { type: 'close' } );
1804			}
1805			closeChalkboard();
1806
1807			// broadcast
1808			var message = new CustomEvent( messageType );
1809			message.content = {
1810				sender: 'chalkboard-plugin',
1811				type: 'closeChalkboard',
1812				timestamp: Date.now() - slideStart,
1813				mode: 0,
1814				board
1815			};
1816			document.dispatchEvent( message );
1817
1818
1819		} else {
1820			showChalkboard();
1821			if ( !readOnly ) {
1822				recordEvent( { type: 'open' } );
1823				// broadcast
1824				var message = new CustomEvent( messageType );
1825				message.content = {
1826					sender: 'chalkboard-plugin',
1827					type: 'showChalkboard',
1828					timestamp: Date.now() - slideStart,
1829					mode: 1,
1830					board
1831				};
1832				document.dispatchEvent( message );
1833
1834				var idx = 0;
1835
1836				if ( rememberColor[ mode ] ) {
1837					idx = color[ mode ];
1838				}
1839
1840				setColor( idx, true );
1841			}
1842		}
1843	};
1844
1845	function clearSlide() {
1846		recordEvent( { type: 'clear' } );
1847		clearCanvas( mode );
1848	}
1849
1850	function clear() {
1851		if ( !readOnly ) {
1852			clearSlide();
1853			// broadcast
1854			var message = new CustomEvent( messageType );
1855			message.content = {
1856				sender: 'chalkboard-plugin',
1857				type: 'clear',
1858				timestamp: Date.now() - slideStart,
1859				mode,
1860				board
1861			};
1862			document.dispatchEvent( message );
1863		}
1864	};
1865
1866	function colorIndex( idx ) {
1867		if ( !readOnly ) {
1868			setColor( idx, true );
1869		}
1870	}
1871
1872	function colorNext() {
1873		if ( !readOnly ) {
1874			let idx = cycleColorNext();
1875			setColor( idx, true );
1876		}
1877	}
1878
1879	function colorPrev() {
1880		if ( !readOnly ) {
1881			let idx = cycleColorPrev();
1882			setColor( idx, true );
1883		}
1884	}
1885
1886	function resetSlideDrawings() {
1887		slideStart = Date.now();
1888		closeChalkboard();
1889
1890		clearCanvas( 0 );
1891		clearCanvas( 1 );
1892
1893		mode = 1;
1894		var slideData = getSlideData();
1895		slideData.duration = 0;
1896		slideData.events = [];
1897		mode = 0;
1898		var slideData = getSlideData();
1899		slideData.duration = 0;
1900		slideData.events = [];
1901
1902		updateStorage();
1903	}
1904
1905	function resetSlide( force ) {
1906		var ok = force || confirm( "Please confirm to delete chalkboard drawings on this slide!" );
1907		if ( ok ) {
1908//console.log("resetSlide ");
1909			stopPlayback();
1910			resetSlideDrawings();
1911			// broadcast
1912			var message = new CustomEvent( messageType );
1913			message.content = {
1914				sender: 'chalkboard-plugin',
1915				type: 'resetSlide',
1916				timestamp: Date.now() - slideStart,
1917				mode,
1918				board
1919			};
1920			document.dispatchEvent( message );
1921		}
1922	};
1923
1924	function resetStorage( force ) {
1925		var ok = force || confirm( "Please confirm to delete all chalkboard drawings!" );
1926		if ( ok ) {
1927			stopPlayback();
1928			slideStart = Date.now();
1929			clearCanvas( 0 );
1930			clearCanvas( 1 );
1931			if ( mode == 1 ) {
1932				closeChalkboard();
1933			}
1934
1935			storage = [ {
1936					width: Reveal.getConfig().width,
1937					height: Reveal.getConfig().height,
1938					data: []
1939				},
1940				{
1941					width: Reveal.getConfig().width,
1942					height: Reveal.getConfig().height,
1943					data: []
1944				}
1945			];
1946
1947			if ( config.storage ) {
1948				sessionStorage.setItem( config.storage, null )
1949			}
1950			// broadcast
1951			var message = new CustomEvent( messageType );
1952			message.content = {
1953				sender: 'chalkboard-plugin',
1954				type: 'init',
1955				timestamp: Date.now() - slideStart,
1956				storage,
1957				mode,
1958				board
1959			};
1960			document.dispatchEvent( message );
1961		}
1962	};
1963
1964	this.toggleNotesCanvas = toggleNotesCanvas;
1965	this.toggleChalkboard = toggleChalkboard;
1966	this.colorIndex = colorIndex;
1967	this.colorNext = colorNext;
1968	this.colorPrev = colorPrev;
1969	this.clear = clear;
1970	this.reset = resetSlide;
1971	this.resetAll = resetStorage;
1972	this.download = downloadData;
1973	this.updateStorage = updateStorage;
1974	this.getData = getData;
1975	this.configure = configure;
1976
1977
1978	for ( var key in keyBindings ) {
1979		if ( keyBindings[ key ] ) {
1980			Reveal.addKeyBinding( keyBindings[ key ], RevealChalkboard[ key ] );
1981		}
1982	};
1983
1984	return this;
1985};

Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.