PageSourceSearch

https://soci-316-w4.netlify.app/index_files/libs/revealjs/plugin/reveal-chalkboard/plugin.js

js soci-316-w4.netlify.app collected 2026-10-03 11:25:40 UTC 57,340 bytes, 1,975 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.3.3
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
16"use strict";
17
18window.RevealChalkboard = window.RevealChalkboard || {
19	id: 'RevealChalkboard',
20	init: function ( deck ) {
21		initChalkboard.call(this, deck );
22	},
23	configure: function ( config ) {
24		configure( config );
25	},
26	toggleNotesCanvas: function () {
27		toggleNotesCanvas();
28	},
29	toggleChalkboard: function () {
30		toggleChalkboard();
31	},
32	colorIndex: function () {
33		colorIndex();
34	},
35	colorNext: function () {
36		colorNext();
37	},
38	colorPrev: function () {
39		colorPrev();
40	},
41	clear: function () {
42		clear();
43	},
44	reset: function () {
45		reset();
46	},
47	resetAll: function () {
48		resetAll();
49	},
50	updateStorage: function () {
51		updateStorage();
52	},
53	getData: function () {
54		return getData();
55	},
56	download: function () {
57		download();
58	},
59};
60
61function scriptPath() {
62	// obtain plugin path from the script element
63	var src;
64	if ( document.currentScript ) {
65		src = document.currentScript.src;
66	} else {
67		var sel = document.querySelector( 'script[src$="/chalkboard/plugin.js"]' )
68		if ( sel ) {
69			src = sel.src;
70		}
71	}
72	var path = ( src === undefined ) ? "" : src.slice( 0, src.lastIndexOf( "/" ) + 1 );
73//console.log("Path: " + path);
74	return path;
75}
76var path = scriptPath();
77
78const initChalkboard = function ( Reveal ) {
79//console.warn(path);
80	/* Feature detection for passive event handling*/
81	var passiveSupported = false;
82
83	try {
84		window.addEventListener( 'test', null, Object.defineProperty( {}, 'passive', {
85			get: function () {
86				passiveSupported = true;
87			}
88		} ) );
89	} catch ( err ) {}
90
91
92/*****************************************************************
93 ** Configuration
94 ******************************************************************/
95	var background, pens, draw, color;
96	var grid = false;
97	var boardmarkerWidth = 3;
98	var chalkWidth = 7;
99	var chalkEffect = 1.0;
100	var rememberColor = [ true, false ];
101	var eraser = {
102		src: path + 'img/sponge.png',
103		radius: 20
104	};
105	var boardmarkers = [ {
106			color: 'rgba(100,100,100,1)',
107			cursor: 'url(' + path + 'img/boardmarker-black.png), auto'
108		},
109		{
110			color: 'rgba(30,144,255, 1)',
111			cursor: 'url(' + path + 'img/boardmarker-blue.png), auto'
112		},
113		{
114			color: 'rgba(220,20,60,1)',
115			cursor: 'url(' + path + 'img/boardmarker-red.png), auto'
116		},
117		{
118			color: 'rgba(50,205,50,1)',
119			cursor: 'url(' + path + 'img/boardmarker-green.png), auto'
120		},
121		{
122			color: 'rgba(255,140,0,1)',
123			cursor: 'url(' + path + 'img/boardmarker-orange.png), auto'
124		},
125		{
126			color: 'rgba(150,0,20150,1)',
127			cursor: 'url(' + path + 'img/boardmarker-purple.png), auto'
128		},
129		{
130			color: 'rgba(255,220,0,1)',
131			cursor: 'url(' + path + 'img/boardmarker-yellow.png), auto'
132		}
133	];
134	var chalks = [ {
135			color: 'rgba(255,255,255,0.5)',
136			cursor: 'url(' + path + 'img/chalk-white.png), auto'
137		},
138		{
139			color: 'rgba(96, 154, 244, 0.5)',
140			cursor: 'url(' + path + 'img/chalk-blue.png), auto'
141		},
142		{
143			color: 'rgba(237, 20, 28, 0.5)',
144			cursor: 'url(' + path + 'img/chalk-red.png), auto'
145		},
146		{
147			color: 'rgba(20, 237, 28, 0.5)',
148			cursor: 'url(' + path + 'img/chalk-green.png), auto'
149		},
150		{
151			color: 'rgba(220, 133, 41, 0.5)',
152			cursor: 'url(' + path + 'img/chalk-orange.png), auto'
153		},
154		{
155			color: 'rgba(220,0,220,0.5)',
156			cursor: 'url(' + path + 'img/chalk-purple.png), auto'
157		},
158		{
159			color: 'rgba(255,220,0,0.5)',
160			cursor: 'url(' + path + 'img/chalk-yellow.png), auto'
161		}
162	];
163
164  var sponge = 		{
165		cursor: 'url(' + path + 'img/sponge.png), auto'
166	}
167
168
169	var keyBindings = {
170		toggleNotesCanvas: {
171			keyCode: 67,
172			key: 'C',
173			description: 'Toggle notes canvas'
174		},
175		toggleChalkboard: {
176			keyCode: 66,
177			key: 'B',
178			description: 'Toggle chalkboard'
179		},
180		clear: {
181			keyCode: 46,
182			key: 'DEL',
183			description: 'Clear drawings on slide'
184		},
185/*
186		reset: {
187			keyCode: 173,
188			key: '-',
189			description: 'Reset drawings on slide'
190		},
191*/
192		resetAll: {
193			keyCode: 8,
194			key: 'BACKSPACE',
195			description: 'Reset all drawings'
196		},
197		colorNext: {
198			keyCode: 88,
199			key: 'X',
200			description: 'Next color'
201		},
202		colorPrev: {
203			keyCode: 89,
204			key: 'Y',
205			description: 'Previous color'
206		},
207		download: {
208			keyCode: 68,
209			key: 'D',
210			description: 'Download drawings'
211		}
212	};
213
214
215	var theme = 'chalkboard';
216	var color = [ 0, 0 ];
217	var toggleChalkboardButton = false;
218	var toggleNotesButton = false;
219	var colorButtons = true;
220	var boardHandle = true;
221	var transition = 800;
222
223	var readOnly = false;
224	var messageType = 'broadcast';
225
226	var config = configure( Reveal.getConfig().chalkboard || {} );
227	if ( config.keyBindings ) {
228		for ( var key in config.keyBindings ) {
229			keyBindings[ key ] = config.keyBindings[ key ];
230		};
231	}
232
233	function configure( config ) {
234
235		if ( config.boardmarkerWidth || config.penWidth ) boardmarkerWidth = config.boardmarkerWidth || config.penWidth;
236		if ( config.chalkWidth ) chalkWidth = config.chalkWidth;
237		if ( config.chalkEffect ) chalkEffect = config.chalkEffect;
238		if ( config.rememberColor ) rememberColor = config.rememberColor;
239		if ( config.eraser ) eraser = config.eraser;
240		if ( config.boardmarkers ) boardmarkers = config.boardmarkers;
241		if ( config.chalks ) chalks = config.chalks;
242
243		if ( config.theme ) theme = config.theme;
244		switch ( theme ) {
245		case 'whiteboard':
246			background = [ 'rgba(127,127,127,.1)', path + 'img/whiteboard.png' ];
247			draw = [ drawWithBoardmarker, drawWithBoardmarker ];
248			pens = [ boardmarkers, boardmarkers ];
249			grid = {
250				color: 'rgb(127,127,255,0.1)',
251				distance: 40,
252				width: 2
253			};
254			break;
255		case 'chalkboard':
256		default:
257			background = [ 'rgba(127,127,127,.1)', path + 'img/blackboard.png' ];
258			draw = [ drawWithBoardmarker, drawWithChalk ];
259			pens = [ boardmarkers, chalks ];
260			grid = {
261				color: 'rgb(50,50,10,0.5)',
262				distance: 80,
263				width: 2
264			};
265		}
266
267		if ( config.background ) background = config.background;
268		if ( config.grid != undefined ) grid = config.grid;
269
270		if ( config.toggleChalkboardButton != undefined ) toggleChalkboardButton = config.toggleChalkboardButton;
271		if ( config.toggleNotesButton != undefined ) toggleNotesButton = config.toggleNotesButton;
272		if ( config.colorButtons != undefined ) colorButtons = config.colorButtons;
273		if ( config.boardHandle != undefined ) boardHandle = config.boardHandle;
274		if ( config.transition ) transition = config.transition;
275
276		if ( config.readOnly != undefined ) readOnly = config.readOnly;
277		if ( config.messageType ) messageType = config.messageType;
278
279		if ( drawingCanvas && ( config.theme || config.background || config.grid ) ) {
280			var canvas = document.getElementById( drawingCanvas[ 1 ].id );
281			canvas.style.background = 'url("' + background[ 1 ] + '") repeat';
282			clearCanvas( 1 );
283			drawGrid();
284		}
285
286		return config;
287	}
288/*****************************************************************
289 ** Setup
290 ******************************************************************/
291
292	function whenReady( callback ) {
293		// wait for markdown to be parsed and code to be highlighted
294		if ( !document.querySelector( 'section[data-markdown]:not([data-markdown-parsed])' ) 
295			   && !document.querySelector( '[data-load]:not([data-loaded])') 
296		     && !document.querySelector( 'code[data-line-numbers*="|"]') 	
297		) {
298			callback();
299		} else {
300			console.log( "Wait for external sources to be loaded and code to be highlighted" );
301			setTimeout( whenReady, 500, callback )
302		}
303	}
304
305	function whenLoaded( callback ) {
306		// wait for drawings to be loaded and markdown to be parsed
307		if ( loaded !== null ) {
308			callback();
309		} else {
310			console.log( "Wait for drawings to be loaded" );
311			setTimeout( whenLoaded, 500, callback )
312		}
313	}
314
315	var drawingCanvas = [ {
316		id: 'notescanvas'
317	}, {
318		id: 'chalkboard'
319	} ];
320	setupDrawingCanvas( 0 );
321	setupDrawingCanvas( 1 );
322
323	var mode = 0; // 0: notes canvas, 1: chalkboard
324	var board = 0; // board index (only for chalkboard)
325
326	var mouseX = 0;
327	var mouseY = 0;
328	var lastX = null;
329	var lastY = null;
330
331	var drawing = false;
332	var erasing = false;
333
334	var slideStart = Date.now();
335	var slideIndices = {
336		h: 0,
337		v: 0
338	};
339
340	var timeouts = [
341		[],
342		[]
343	];
344	var slidechangeTimeout = null;
345	var updateStorageTimeout = null;
346	var playback = false;
347
348  function changeCursor( element, tool ) {
349    element.style.cursor = tool.cursor;
350    var palette = document.querySelector('.palette[data-mode="' + mode + '"]');
351    if ( palette ) {
352      palette.style.cursor = tool.cursor;
353    }
354  }
355
356	function createPalette( colors, length ) {
357		if ( length === true || length > colors.length ) {
358			length = colors.length;
359		}
360		var palette = document.createElement( 'div' );
361		palette.classList.add( 'palette' );
362		var list = document.createElement( 'ul' );
363		// color pickers
364		for ( var i = 0; i < length; i++ ) {
365			var colorButton = document.createElement( 'li' );
366			colorButton.setAttribute( 'data-color', i );
367			colorButton.innerHTML = '<i class="fa fa-square"></i>';
368			colorButton.style.color = colors[ i ].color;
369			colorButton.addEventListener( 'click', function ( e ) {
370				var element = e.target;
371				while ( !element.hasAttribute( 'data-color' ) ) {
372					element = element.parentElement;
373				}
374				colorIndex( parseInt( element.getAttribute( 'data-color' ) ) );
375			} );
376			colorButton.addEventListener( 'touchstart', function ( e ) {
377				var element = e.target;
378				while ( !element.hasAttribute( 'data-color' ) ) {
379					element = element.parentElement;
380				}
381				colorIndex( parseInt( element.getAttribute( 'data-color' ) ) );
382			} );
383			list.appendChild( colorButton );
384		}
385    // eraser
386    var eraserButton = document.createElement( 'li' );
387		eraserButton.setAttribute( 'data-eraser', 'true' );
388		var spongeImg = document.createElement( 'img' );
389		spongeImg.src = eraser.src;
390    spongeImg.height = "24";
391    spongeImg.width = "24";
392    spongeImg.style.marginTop = '10px';
393    spongeImg.style.marginRight = '0';
394    spongeImg.style.marginBottom = '0';
395    spongeImg.style.marginLeft = '0';
396		eraserButton.appendChild(spongeImg);
397		eraserButton.addEventListener( 'click', function ( e ) {
398			colorIndex( -1 );
399		} );
400		eraserButton.addEventListener( 'touchstart', function ( e ) {
401			colorIndex( -1 );
402		} );
403		list.appendChild( eraserButton );
404
405		palette.appendChild( list );
406		return palette;
407	};
408
409	function switchBoard( boardIdx ) {
410		selectBoard( boardIdx, true );
411		// broadcast
412		var message = new CustomEvent( messageType );
413			message.content = {
414			sender: 'chalkboard-plugin',
415			type: 'selectboard',
416			timestamp: Date.now() - slideStart,
417			mode,
418			board
419		};
420		document.dispatchEvent( message );	
421	}
422
423	function setupDrawingCanvas( id ) {
424		var container = document.createElement( 'div' );
425		container.id = drawingCanvas[ id ].id;
426		container.classList.add( 'overlay' );
427		container.setAttribute( 'data-prevent-swipe', 'true' );
428		container.oncontextmenu = function () {
429			return false;
430		}
431
432    changeCursor( container, pens[ id ][ color[ id ] ] );
433
434		drawingCanvas[ id ].width = window.innerWidth;
435		drawingCanvas[ id ].height = window.innerHeight;
436		drawingCanvas[ id ].scale = 1;
437		drawingCanvas[ id ].xOffset = 0;
438		drawingCanvas[ id ].yOffset = 0;
439
440		if ( id == "0" ) {
441			container.style.background = 'rgba(0,0,0,0)';
442			container.style.zIndex = 24;
443			container.style.opacity = 1;
444			container.style.visibility = 'visible';
445			container.style.pointerEvents = 'none';
446			container.style['backdrop-filter'] = 'none';
447			container.style['-webkit-backdrop-filter'] = 'none';
448
449			var slides = document.querySelector( '.slides' );
450			var aspectRatio = Reveal.getConfig().width / Reveal.getConfig().height;
451			if ( drawingCanvas[ id ].width > drawingCanvas[ id ].height * aspectRatio ) {
452				drawingCanvas[ id ].xOffset = ( drawingCanvas[ id ].width - drawingCanvas[ id ].height * aspectRatio ) / 2;
453			} else if ( drawingCanvas[ id ].height > drawingCanvas[ id ].width / aspectRatio ) {
454				drawingCanvas[ id ].yOffset = ( drawingCanvas[ id ].height - drawingCanvas[ id ].width / aspectRatio ) / 2;
455			}
456
457			if ( colorButtons ) {
458				var palette = createPalette( boardmarkers, colorButtons );
459        palette.dataset.mode = id;
460				palette.style.visibility = 'hidden'; // only show palette in drawing mode
461				container.appendChild( palette );
462			}
463		} else {
464			container.style.background = 'url("' + background[ id ] + '") repeat';
465			container.style.zIndex = 26;
466			container.style.opacity = 0;
467			container.style.visibility = 'hidden';
468
469			if ( colorButtons ) {
470				var palette = createPalette( chalks, colorButtons );
471        palette.dataset.mode = id;
472				container.appendChild( palette );
473			}
474			if ( boardHandle ) {
475				var handle = document.createElement( 'div' );
476				handle.classList.add( 'boardhandle' );
477				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>';
478				handle.querySelector( '#previousboard' ).addEventListener( 'click', function ( e ) {
479					e.preventDefault();
480					switchBoard( board - 1 );
481				} );
482				handle.querySelector( '#nextboard' ).addEventListener( 'click', function ( e ) {
483					e.preventDefault();
484					switchBoard( board + 1 );
485				} );
486				handle.querySelector( '#previousboard' ).addEventListener( 'touchstart', function ( e ) {
487					e.preventDefault();
488					switchBoard( board - 1 );
489				} );
490				handle.querySelector( '#nextboard' ).addEventListener( 'touchstart', function ( e ) {
491					e.preventDefault();
492					switchBoard( board + 1 );
493				} );
494
495				container.appendChild( handle );
496			}
497		}
498
499		var canvas = document.createElement( 'canvas' );
500		canvas.width = drawingCanvas[ id ].width;
501		canvas.height = drawingCanvas[ id ].height;
502		canvas.setAttribute( 'data-chalkboard', id );
503    changeCursor( canvas, pens[ id ][ color[ id ] ] );
504		container.appendChild( canvas );
505		drawingCanvas[ id ].canvas = canvas;
506
507		drawingCanvas[ id ].context = canvas.getContext( '2d' );
508
509		setupCanvasEvents( container );
510
511		document.querySelector( '.reveal' ).appendChild( container );
512		drawingCanvas[ id ].container = container;
513	}
514
515
516/*****************************************************************
517 ** Storage
518 ******************************************************************/
519
520	var storage = [ {
521			width: Reveal.getConfig().width,
522			height: Reveal.getConfig().height,
523			data: []
524		},
525		{
526			width: Reveal.getConfig().width,
527			height: Reveal.getConfig().height,
528			data: []
529		}
530	];
531
532	var loaded = null;
533
534	if ( config.storage ) {
535		// Get chalkboard drawings from session storage
536		loaded = initStorage( sessionStorage.getItem( config.storage ) );
537	}
538
539	if ( !loaded && config.src != null ) {
540		// Get chalkboard drawings from the given file
541		loadData( config.src );
542	}
543
544	/**
545	 * Initialize storage.
546	 */
547	function initStorage( json ) {
548		var success = false;
549		try {
550			var data = JSON.parse( json );
551			for ( var id = 0; id < data.length; id++ ) {
552				if ( drawingCanvas[ id ].width != data[ id ].width || drawingCanvas[ id ].height != data[ id ].height ) {
553					drawingCanvas[ id ].scale = Math.min( drawingCanvas[ id ].width / data[ id ].width, drawingCanvas[ id ].height / data[ id ].height );
554					drawingCanvas[ id ].xOffset = ( drawingCanvas[ id ].width - data[ id ].width * drawingCanvas[ id ].scale ) / 2;
555					drawingCanvas[ id ].yOffset = ( drawingCanvas[ id ].height - data[ id ].height * drawingCanvas[ id ].scale ) / 2;
556				}
557				if ( config.readOnly ) {
558					drawingCanvas[ id ].container.style.cursor = 'default';
559					drawingCanvas[ id ].canvas.style.cursor = 'default';
560				}
561			}
562			success = true;
563			storage = data;
564		} catch ( err ) {
565			console.warn( "Cannot initialise storage!" );
566		}
567		return success;
568	}
569
570
571	/**
572	 * Load data.
573	 */
574	function loadData( filename ) {
575		var xhr = new XMLHttpRequest();
576		xhr.onload = function () {
577			if ( xhr.readyState === 4 && xhr.status != 404 ) {
578				loaded = initStorage( xhr.responseText );
579				updateStorage();
580				console.log( "Drawings loaded from file" );
581			} else {
582				config.readOnly = undefined;
583				readOnly = undefined;
584				console.warn( 'Failed to get file ' + filename + '. ReadyState: ' + xhr.readyState + ', Status: ' + xhr.status );
585				loaded = false;
586			}
587		};
588
589		xhr.open( 'GET', filename, true );
590		try {
591			xhr.send();
592		} catch ( error ) {
593			config.readOnly = undefined;
594			readOnly = undefined;
595			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 );
596			loaded = false;
597		}
598	}
599
600
601	function storageChanged( now ) {
602		if ( !now ) {
603			// create or update timer
604			if ( updateStorageTimeout ) {
605				clearTimeout( updateStorageTimeout );
606			}
607			updateStorageTimeout = setTimeout( storageChanged, 1000, true);
608		}
609		else {
610// console.log("Update storage", updateStorageTimeout,  Date.now());
611			updateStorage();
612			updateStorageTimeout = null;
613		}
614	}
615
616	function updateStorage() {
617		var json = JSON.stringify( storage )
618		if ( config.storage ) {
619			sessionStorage.setItem( config.storage, json )
620		}
621		return json;
622	}
623
624	function recordEvent( event ) {
625//console.log(event);
626		event.time = Date.now() - slideStart;
627		if ( mode == 1 ) event.board = board;
628		var slideData = getSlideData();
629		var i = slideData.events.length;
630		while ( i > 0 && event.time < slideData.events[ i - 1 ].time ) {
631			i--;
632		}
633		slideData.events.splice( i, 0, event );
634		slideData.duration = Math.max( slideData.duration, Date.now() - slideStart ) + 1;
635
636		storageChanged();
637	}
638
639	/**
640	 * Get data as json string.
641	 */
642	function getData() {
643		// cleanup slide data without events
644		for ( var id = 0; id < 2; id++ ) {
645			for ( var i = storage[ id ].data.length - 1; i >= 0; i-- ) {
646				if ( storage[ id ].data[ i ].events.length == 0 ) {
647					storage[ id ].data.splice( i, 1 );
648				}
649			}
650		}
651
652		return updateStorage();
653	}
654
655	/**
656	 * Download data.
657	 */
658	function downloadData() {
659		var a = document.createElement( 'a' );
660		document.body.appendChild( a );
661		try {
662			a.download = 'chalkboard.json';
663			var blob = new Blob( [ getData() ], {
664				type: 'application/json'
665			} );
666			a.href = window.URL.createObjectURL( blob );
667		} catch ( error ) {
668			// https://stackoverflow.com/a/6234804
669			// escape data for proper handling of quotes and line breaks
670			// in case malicious user gets a chance to craft the exception message
671			error = String(error)
672					.replace(/&/g, "&amp;")
673					.replace(/</g, "&lt;")
674					.replace(/>/g, "&gt;")
675					.replace(/"/g, "&quot;")
676					.replace(/'/g, "&#039;");
677			a.innerHTML += ' (' + error + ')';
678		}
679		a.click();
680		document.body.removeChild( a );
681	}
682
683	/**
684	 * Returns data object for the slide with the given indices.
685	 */
686	function getSlideData( indices, id ) {
687		if ( id == undefined ) id = mode;
688		if ( !indices ) indices = slideIndices;
689		var data;
690		for ( var i = 0; i < storage[ id ].data.length; i++ ) {
691			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 ) {
692				data = storage[ id ].data[ i ];
693				return data;
694			}
695		}
696		var page = Number( Reveal.getCurrentSlide().getAttribute('data-pdf-page-number') ); 
697//console.log( indices, Reveal.getCurrentSlide() );
698		storage[ id ].data.push( {
699			slide: indices,
700			page,
701			events: [],
702			duration: 0
703		} );
704		data = storage[ id ].data[ storage[ id ].data.length - 1 ];
705		return data;
706	}
707
708	/**
709	 * Returns maximum duration of slide playback for both modes
710	 */
711	function getSlideDuration( indices ) {
712		if ( !indices ) indices = slideIndices;
713		var duration = 0;
714		for ( var id = 0; id < 2; id++ ) {
715			for ( var i = 0; i < storage[ id ].data.length; i++ ) {
716				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 ) {
717					duration = Math.max( duration, storage[ id ].data[ i ].duration );
718					break;
719				}
720			}
721		}
722//console.log( duration );
723		return duration;
724	}
725
726/*****************************************************************
727 ** Print
728 ******************************************************************/
729	var printMode = ( /print-pdf/gi ).test( window.location.search );
730//console.log("createPrintout" + printMode)
731
732	function addPageNumbers() {
733		// determine page number for printouts with fragments serialised
734		var slides = Reveal.getSlides();
735		var page = 0;
736		for ( var i=0; i < slides.length; i++) {
737			slides[i].setAttribute('data-pdf-page-number',page.toString());
738			// add number of fragments without fragment indices
739			var count = slides[i].querySelectorAll('.fragment:not([data-fragment-index])').length;
740			var fragments = slides[i].querySelectorAll('.fragment[data-fragment-index]');
741			for ( var j=0; j < fragments.length; j++) {
742				// increasenumber of fragments by highest fragment index (which start at 0)
743				if ( Number(fragments[j].getAttribute('data-fragment-index')) + 1 > count ) {
744					count = Number(fragments[j].getAttribute('data-fragment-index')) + 1;
745				}
746			}
747			page += count + 1;
748		}
749	}
750
751	function createPrintout() {
752		//console.warn(Reveal.getTotalSlides(),Reveal.getSlidesElement());
753		if ( storage[ 1 ].data.length == 0 ) return;
754		console.log( 'Create printout(s) for ' + storage[ 1 ].data.length + " slides" );
755		drawingCanvas[ 0 ].container.style.opacity = 0; // do not print notes canvas
756		drawingCanvas[ 0 ].container.style.visibility = 'hidden';
757
758		var patImg = new Image();
759		patImg.onload = function () {
760			var slides = Reveal.getSlides();
761//console.log(slides);
762			for ( var i = storage[ 1 ].data.length - 1; i >= 0; i-- ) {
763				console.log( 'Create printout for slide ' + storage[ 1 ].data[ i ].slide.h + '.' + storage[ 1 ].data[ i ].slide.v );
764				var slideData = getSlideData( storage[ 1 ].data[ i ].slide, 1 );
765				var drawings = createDrawings( slideData, patImg );
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
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 + eraser.radius, y + eraser.radius, eraser.radius, 0, 2 * Math.PI, false );
943		context.clip();
944		context.clearRect( x - 1, y - 1, eraser.radius * 2 + 2, eraser.radiu
944s * 2 + 2 );
945		context.restore();
946		if ( mode == 1 && grid ) {
947			redrawGrid( x + eraser.radius, y + eraser.radius, eraser.radius );
948		}
949	}
950
951
952	/**
953	 * Show an overlay for the chalkboard.
954	 */
955	function showChalkboard() {
956//console.log("showChalkboard");
957		drawingCanvas[ 1 ].container.style.opacity = 1;
958		drawingCanvas[ 1 ].container.style.visibility = 'visible';
959		mode = 1;
960	}
961
962
963	/**
964	 * Closes open chalkboard.
965	 */
966	function closeChalkboard() {
967		drawingCanvas[ 1 ].container.style.opacity = 0;
968		drawingCanvas[ 1 ].container.style.visibility = 'hidden';
969		lastX = null;
970		lastY = null;
971		mode = 0;
972	}
973
974	/**
975	 * Clear current canvas.
976	 */
977	function clearCanvas( id ) {
978		if ( id == 0 ) clearTimeout( slidechangeTimeout );
979		drawingCanvas[ id ].context.clearRect( 0, 0, drawingCanvas[ id ].width, drawingCanvas[ id ].height );
980		if ( id == 1 && grid ) drawGrid();
981	}
982
983	/**
984	 * Draw grid on background
985	 */
986	function drawGrid() {
987		var context = drawingCanvas[ 1 ].context;
988
989		drawingCanvas[ 1 ].scale = Math.min( drawingCanvas[ 1 ].width / storage[ 1 ].width, drawingCanvas[ 1 ].height / storage[ 1 ].height );
990		drawingCanvas[ 1 ].xOffset = ( drawingCanvas[ 1 ].width - storage[ 1 ].width * drawingCanvas[ 1 ].scale ) / 2;
991		drawingCanvas[ 1 ].yOffset = ( drawingCanvas[ 1 ].height - storage[ 1 ].height * drawingCanvas[ 1 ].scale ) / 2;
992
993		var scale = drawingCanvas[ 1 ].scale;
994		var xOffset = drawingCanvas[ 1 ].xOffset;
995		var yOffset = drawingCanvas[ 1 ].yOffset;
996
997		var distance = grid.distance * scale;
998
999		var fromX = drawingCanvas[ 1 ].width / 2 - distance / 2 - Math.floor( ( drawingCanvas[ 1 ].width - distance ) / 2 / distance ) * distance;
1000		for ( var x = fromX; x < drawingCanvas[ 1 ].width; x += distance ) {
1001			context.beginPath();
1002			context.lineWidth = grid.width * scale;
1003			context.lineCap = 'round';
1004			context.fillStyle = grid.color;
1005			context.strokeStyle = grid.color;
1006			context.moveTo( x, 0 );
1007			context.lineTo( x, drawingCanvas[ 1 ].height );
1008			context.stroke();
1009		}
1010		var fromY = drawingCanvas[ 1 ].height / 2 - distance / 2 - Math.floor( ( drawingCanvas[ 1 ].height - distance ) / 2 / distance ) * distance;
1011
1012		for ( var y = fromY; y < drawingCanvas[ 1 ].height; y += distance ) {
1013			context.beginPath();
1014			context.lineWidth = grid.width * scale;
1015			context.lineCap = 'round';
1016			context.fillStyle = grid.color;
1017			context.strokeStyle = grid.color;
1018			context.moveTo( 0, y );
1019			context.lineTo( drawingCanvas[ 1 ].width, y );
1020			context.stroke();
1021		}
1022	}
1023
1024	function redrawGrid( centerX, centerY, diameter ) {
1025		var context = drawingCanvas[ 1 ].context;
1026
1027		drawingCanvas[ 1 ].scale = Math.min( drawingCanvas[ 1 ].width / storage[ 1 ].width, drawingCanvas[ 1 ].height / storage[ 1 ].height );
1028		drawingCanvas[ 1 ].xOffset = ( drawingCanvas[ 1 ].width - storage[ 1 ].width * drawingCanvas[ 1 ].scale ) / 2;
1029		drawingCanvas[ 1 ].yOffset = ( drawingCanvas[ 1 ].height - storage[ 1 ].height * drawingCanvas[ 1 ].scale ) / 2;
1030
1031		var scale = drawingCanvas[ 1 ].scale;
1032		var xOffset = drawingCanvas[ 1 ].xOffset;
1033		var yOffset = drawingCanvas[ 1 ].yOffset;
1034
1035		var distance = grid.distance * scale;
1036
1037		var fromX = drawingCanvas[ 1 ].width / 2 - distance / 2 - Math.floor( ( drawingCanvas[ 1 ].width - distance ) / 2 / distance ) * distance;
1038
1039		for ( var x = fromX + distance * Math.ceil( ( centerX - diameter - fromX ) / distance ); x <= fromX + distance * Math.floor( ( centerX + diameter - fromX ) / distance ); x += distance ) {
1040			context.beginPath();
1041			context.lineWidth = grid.width * scale;
1042			context.lineCap = 'round';
1043			context.fillStyle = grid.color;
1044			context.strokeStyle = grid.color;
1045			context.moveTo( x, centerY - Math.sqrt( diameter * diameter - ( centerX - x ) * ( centerX - x ) ) );
1046			context.lineTo( x, centerY + Math.sqrt( diameter * diameter - ( centerX - x ) * ( centerX - x ) ) );
1047			context.stroke();
1048		}
1049		var fromY = drawingCanvas[ 1 ].height / 2 - distance / 2 - Math.floor( ( drawingCanvas[ 1 ].height - distance ) / 2 / distance ) * distance;
1050		for ( var y = fromY + distance * Math.ceil( ( centerY - diameter - fromY ) / distance ); y <= fromY + distance * Math.floor( ( centerY + diameter - fromY ) / distance ); y += distance ) {
1051			context.beginPath();
1052			context.lineWidth = grid.width * scale;
1053			context.lineCap = 'round';
1054			context.fillStyle = grid.color;
1055			context.strokeStyle = grid.color;
1056			context.moveTo( centerX - Math.sqrt( diameter * diameter - ( centerY - y ) * ( centerY - y ) ), y );
1057			context.lineTo( centerX + Math.sqrt( diameter * diameter - ( centerY - y ) * ( centerY - y ) ), y );
1058			context.stroke();
1059		}
1060	}
1061
1062	/**
1063	 * Set the  color
1064	 */
1065	function setColor( index, record ) {    
1066 		// protect against out of bounds (this could happen when
1067  	// replaying events recorded with different color settings).
1068    if ( index >= pens[ mode ].length ) index = 0;
1069
1070	  color[ mode ] = index;
1071
1072    if ( color[ mode ] < 0 ) {
1073      // use eraser
1074      changeCursor( drawingCanvas[ mode ].canvas, sponge );
1075    }
1076    else {
1077      changeCursor( drawingCanvas[ mode ].canvas, pens[ mode ][ color[ mode ] ] );
1078    }
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		erasePoint( x, y );
1365	}
1366
1367	function erasePoint( x, y ) {
1368		var ctx = drawingCanvas[ mode ].context;
1369		var scale = drawingCanvas[ mode ].scale;
1370		var xOffset = drawingCanvas[ mode ].xOffset;
1371		var yOffset = drawingCanvas[ mode ].yOffset;
1372
1373		recordEvent( {
1374			type: 'erase',
1375			x,
1376			y
1377		} );
1378
1379		if (
1380			x * scale + xOffset > 0 &&
1381			y * scale + yOffset > 0 &&
1382			x * scale + xOffset < drawingCanvas[ mode ].width &&
1383			y * scale + yOffset < drawingCanvas[ mode ].height
1384		) {
1385			eraseWithSponge( ctx, x * scale + xOffset, y * scale + yOffset );
1386		}
1387	}
1388
1389	function stopErasing() {
1390		erasing = false;
1391	}
1392
1393	function startDrawing( x, y ) {
1394		drawing = true;
1395
1396		var ctx = drawingCanvas[ mode ].context;
1397		var scale = drawingCanvas[ mode ].scale;
1398		var xOffset = drawingCanvas[ mode ].xOffset;
1399		var yOffset = drawingCanvas[ mode ].yOffset;
1400		lastX = x * scale + xOffset;
1401		lastY = y * scale + yOffset;
1402	}
1403
1404	function drawSegment( fromX, fromY, toX, toY, colorIdx ) {
1405		var ctx = drawingCanvas[ mode ].context;
1406		var scale = drawingCanvas[ mode ].scale;
1407		var xOffset = drawingCanvas[ mode ].xOffset;
1408		var yOffset = drawingCanvas[ mode ].yOffset;
1409
1410		recordEvent( {
1411			type: 'draw',
1412			color: colorIdx,
1413			x1: fromX,
1414			y1: fromY,
1415			x2: toX,
1416			y2: toY
1417		} );
1418
1419		if (
1420			fromX * scale + xOffset > 0 &&
1421			fromY * scale + yOffset > 0 &&
1422			fromX * scale + xOffset < drawingCanvas[ mode ].width &&
1423			fromY * scale + yOffset < drawingCanvas[ mode ].height &&
1424			toX * scale + xOffset > 0 &&
1425			toY * scale + yOffset > 0 &&
1426			toX * scale + xOffset < drawingCanvas[ mode ].width &&
1427			toY * scale + yOffset < drawingCanvas[ mode ].height
1428		) {
1429			draw[ mode ]( ctx, fromX * scale + xOffset, fromY * scale + yOffset, toX * scale + xOffset, toY * scale + yOffset, colorIdx );
1430		}
1431	}
1432
1433	function stopDrawing() {
1434		drawing = false;
1435	}
1436
1437
1438/*****************************************************************
1439 ** User interface
1440 ******************************************************************/
1441
1442	function setupCanvasEvents( canvas ) {
1443// TODO: check all touchevents
1444		canvas.addEventListener( 'touchstart', function ( evt ) {
1445			evt.preventDefault();
1446//console.log("Touch start");
1447			if ( !readOnly && evt.target.getAttribute( 'data-chalkboard' ) == mode ) {
1448				var scale = drawingCanvas[ mode ].scale;
1449				var xOffset = drawingCanvas[ mode ].xOffset;
1450				var yOffset = drawingCanvas[ mode ].yOffset;
1451
1452				var touch = evt.touches[ 0 ];
1453				mouseX = touch.pageX;
1454				mouseY = touch.pageY;
1455        if ( color[ mode ]  < 0 ) {
1456          startErasing( ( mouseX - xOffset ) / scale, ( mouseY - yOffset ) / scale);
1457        }
1458        else {
1459  				startDrawing( ( mouseX - xOffset ) / scale, ( mouseY - yOffset ) / scale );
1460        }
1461			}
1462		}, passiveSupported ? {
1463			passive: false
1464		} : false );
1465
1466		canvas.addEventListener( 'touchmove', function ( evt ) {
1467			evt.preventDefault();
1468//console.log("Touch move");
1469			if ( drawing || erasing ) {
1470				var scale = drawingCanvas[ mode ].scale;
1471				var xOffset = drawingCanvas[ mode ].xOffset;
1472				var yOffset = drawingCanvas[ mode ].yOffset;
1473
1474				var touch = evt.touches[ 0 ];
1475				mouseX = touch.pageX;
1476				mouseY = touch.pageY;
1477
1478				if ( drawing ) {
1479					drawSegment( ( lastX - xOffset ) / scale, ( lastY - yOffset ) / scale, ( mouseX - xOffset ) / scale, ( mouseY - yOffset ) / scale, color[ mode ] );
1480					// broadcast
1481					var message = new CustomEvent( messageType );
1482					message.content = {
1483						sender: 'chalkboard-plugin',
1484						type: 'draw',
1485						timestamp: Date.now() - slideStart,
1486						mode,
1487						board,
1488						fromX: ( lastX - xOffset ) / scale,
1489						fromY: ( lastY - yOffset ) / scale,
1490						toX: ( mouseX - xOffset ) / scale,
1491						toY: ( mouseY - yOffset ) / scale,
1492						color: color[ mode ]
1493					};
1494					document.dispatchEvent( message );
1495
1496					lastX = mouseX;
1497					lastY = mouseY;
1498				} else {
1499					erasePoint( ( mouseX - xOffset ) / scale, ( mouseY - yOffset ) / scale );
1500					// broadcast
1501					var message = new CustomEvent( messageType );
1502					message.content = {
1503						sender: 'chalkboard-plugin',
1504						type: 'erase',
1505						timestamp: Date.now() - slideStart,
1506						mode,
1507						board,
1508						x: ( mouseX - xOffset ) / scale,
1509						y: ( mouseY - yOffset ) / scale
1510					};
1511					document.dispatchEvent( message );
1512				}
1513
1514			}
1515		}, false );
1516
1517
1518		canvas.addEventListener( 'touchend', function ( evt ) {
1519			evt.preventDefault();
1520			stopDrawing();
1521			stopErasing();
1522		}, false );
1523
1524		canvas.addEventListener( 'mousedown', function ( evt ) {
1525			evt.preventDefault();
1526			if ( !readOnly && evt.target.getAttribute( 'data-chalkboard' ) == mode ) {
1527//console.log( "mousedown: " + evt.button );
1528				var scale = drawingCanvas[ mode ].scale;
1529				var xOffset = drawingCanvas[ mode ].xOffset;
1530				var yOffset = drawingCanvas[ mode ].yOffset;
1531
1532				mouseX = evt.pageX;
1533				mouseY = evt.pageY;
1534
1535				if ( color[ mode ]  < 0 || evt.button == 2 || evt.button == 1 ) {
1536          if ( color[ mode ]  >= 0 ) {
1537            // show sponge
1538            changeCursor( drawingCanvas[ mode ].canvas, sponge );
1539          }
1540					startErasing( ( mouseX - xOffset ) / scale, ( mouseY - yOffset ) / scale );
1541					// broadcast
1542					var message = new CustomEvent( messageType );
1543					message.content = {
1544						sender: 'chalkboard-plugin',
1545						type: 'erase',
1546						timestamp: Date.now() - slideStart,
1547						mode,
1548						board,
1549						x: ( mouseX - xOffset ) / scale,
1550						y: ( mouseY - yOffset ) / scale
1551					};
1552					document.dispatchEvent( message );
1553				} else {
1554					startDrawing( ( mouseX - xOffset ) / scale, ( mouseY - yOffset ) / scale );
1555				}
1556			}
1557		} );
1558
1559		canvas.addEventListener( 'mousemove', function ( evt ) {
1560			evt.preventDefault();
1561//console.log("Mouse move");
1562
1563			var scale = drawingCanvas[ mode ].scale;
1564			var xOffset = drawingCanvas[ mode ].xOffset;
1565			var yOffset = drawingCanvas[ mode ].yOffset;
1566
1567			mouseX = evt.pageX;
1568			mouseY = evt.pageY;
1569
1570			if ( drawing || erasing ) {
1571				var scale = drawingCanvas[ mode ].scale;
1572				var xOffset = drawingCanvas[ mode ].xOffset;
1573				var yOffset = drawingCanvas[ mode ].yOffset;
1574
1575				mouseX = evt.pageX;
1576				mouseY = evt.pageY;
1577
1578				if ( drawing ) {
1579					drawSegment( ( lastX - xOffset ) / scale, ( lastY - yOffset ) / scale, ( mouseX - xOffset ) / scale, ( mouseY - yOffset ) / scale, color[ mode ] );
1580					// broadcast
1581					var message = new CustomEvent( messageType );
1582					message.content = {
1583						sender: 'chalkboard-plugin',
1584						type: 'draw',
1585						timestamp: Date.now() - slideStart,
1586						mode,
1587						board,
1588						fromX: ( lastX - xOffset ) / scale,
1589						fromY: ( lastY - yOffset ) / scale,
1590						toX: ( mouseX - xOffset ) / scale,
1591						toY: ( mouseY - yOffset ) / scale,
1592						color: color[ mode ]
1593					};
1594					document.dispatchEvent( message );
1595
1596					lastX = mouseX;
1597					lastY = mouseY;
1598				} else {
1599					erasePoint( ( mouseX - xOffset ) / scale, ( mouseY - yOffset ) / scale );
1600					// broadcast
1601					var message = new CustomEvent( messageType );
1602					message.content = {
1603						sender: 'chalkboard-plugin',
1604						type: 'erase',
1605						timestamp: Date.now() - slideStart,
1606						mode,
1607						board,
1608						x: ( mouseX - xOffset ) / scale,
1609						y: ( mouseY - yOffset ) / scale
1610					};
1611					document.dispatchEvent( message );
1612				}
1613
1614			}
1615		} );
1616
1617
1618		canvas.addEventListener( 'mouseup', function ( evt ) {
1619			evt.preventDefault();
1620      if ( color[ mode ] >= 0 ) {
1621        changeCursor( drawingCanvas[ mode ].canvas, pens[ mode ][ color[ mode ] ] );
1622      }
1623			if ( drawing || erasing ) {
1624				stopDrawing();
1625				stopErasing();
1626			}
1627		} );
1628	}
1629
1630	function resize() {
1631//console.log("resize");
1632		// Resize the canvas and draw everything again
1633		var timestamp = Date.now() - slideStart;
1634		if ( !playback ) {
1635			timestamp = getSlideDuration();
1636		}
1637
1638//console.log( drawingCanvas[0].scale + "/" + drawingCanvas[0].xOffset + "/" 
1638+drawingCanvas[0].yOffset );
1639		for ( var id = 0; id < 2; id++ ) {
1640			drawingCanvas[ id ].width = window.innerWidth;
1641			drawingCanvas[ id ].height = window.innerHeight;
1642			drawingCanvas[ id ].canvas.width = drawingCanvas[ id ].width;
1643			drawingCanvas[ id ].canvas.height = drawingCanvas[ id ].height;
1644			drawingCanvas[ id ].context.canvas.width = drawingCanvas[ id ].width;
1645			drawingCanvas[ id ].context.canvas.height = drawingCanvas[ id ].height;
1646
1647			drawingCanvas[ id ].scale = Math.min( drawingCanvas[ id ].width / storage[ id ].width, drawingCanvas[ id ].height / storage[ id ].height );
1648			drawingCanvas[ id ].xOffset = ( drawingCanvas[ id ].width - storage[ id ].width * drawingCanvas[ id ].scale ) / 2;
1649			drawingCanvas[ id ].yOffset = ( drawingCanvas[ id ].height - storage[ id ].height * drawingCanvas[ id ].scale ) / 2;
1650//console.log( drawingCanvas[id].scale + "/" + drawingCanvas[id].xOffset + "/" +drawingCanvas[id].yOffset );
1651		}
1652//console.log( window.innerWidth + "/" + window.innerHeight);
1653		startPlayback( timestamp, mode, true );
1654	}
1655
1656	Reveal.addEventListener( 'pdf-ready', function ( evt ) {
1657//		console.log( "Create printouts when ready" );
1658		whenLoaded( createPrintout );
1659	});
1660
1661	Reveal.addEventListener( 'ready', function ( evt ) {
1662//console.log('ready');
1663		if ( !printMode ) {
1664			window.addEventListener( 'resize', resize );
1665
1666			slideStart = Date.now() - getSlideDuration();
1667			slideIndices = Reveal.getIndices();
1668			if ( !playback ) {
1669				startPlayback( getSlideDuration(), 0 );
1670			}
1671			if ( Reveal.isAutoSliding() ) {
1672				var event = new CustomEvent( 'startplayback' );
1673				event.timestamp = 0;
1674				document.dispatchEvent( event );
1675			}
1676			updateStorage();
1677			whenReady( addPageNumbers );
1678		}
1679	} );
1680	Reveal.addEventListener( 'slidechanged', function ( evt ) {
1681//		clearTimeout( slidechangeTimeout );
1682//console.log('slidechanged');
1683		if ( !printMode ) {
1684			slideStart = Date.now() - getSlideDuration();
1685			slideIndices = Reveal.getIndices();
1686			closeChalkboard();
1687			board = 0;
1688			clearCanvas( 0 );
1689			clearCanvas( 1 );
1690			if ( !playback ) {
1691				slidechangeTimeout = setTimeout( startPlayback, transition, getSlideDuration(), 0 );
1692			}
1693			if ( Reveal.isAutoSliding() ) {
1694				var event = new CustomEvent( 'startplayback' );
1695				event.timestamp = 0;
1696				document.dispatchEvent( event );
1697			}
1698		}
1699	} );
1700	Reveal.addEventListener( 'fragmentshown', function ( evt ) {
1701//		clearTimeout( slidechangeTimeout );
1702//console.log('fragmentshown');
1703		if ( !printMode ) {
1704			slideStart = Date.now() - getSlideDuration();
1705			slideIndices = Reveal.getIndices();
1706			closeChalkboard();
1707			board = 0;
1708			clearCanvas( 0 );
1709			clearCanvas( 1 );
1710			if ( Reveal.isAutoSliding() ) {
1711				var event = new CustomEvent( 'startplayback' );
1712				event.timestamp = 0;
1713				document.dispatchEvent( event );
1714			} else if ( !playback ) {
1715				startPlayback( getSlideDuration(), 0 );
1716//				closeChalkboard();
1717			}
1718		}
1719	} );
1720	Reveal.addEventListener( 'fragmenthidden', function ( evt ) {
1721//		clearTimeout( slidechangeTimeout );
1722//console.log('fragmenthidden');
1723		if ( !printMode ) {
1724			slideStart = Date.now() - getSlideDuration();
1725			slideIndices = Reveal.getIndices();
1726			closeChalkboard();
1727			board = 0;
1728			clearCanvas( 0 );
1729			clearCanvas( 1 );
1730			if ( Reveal.isAutoSliding() ) {
1731				document.dispatchEvent( new CustomEvent( 'stopplayback' ) );
1732			} else if ( !playback ) {
1733				startPlayback( getSlideDuration() );
1734				closeChalkboard();
1735			}
1736		}
1737	} );
1738
1739	Reveal.addEventListener( 'autoslideresumed', function ( evt ) {
1740//console.log('autoslideresumed');
1741		var event = new CustomEvent( 'startplayback' );
1742		event.timestamp = 0;
1743		document.dispatchEvent( event );
1744	} );
1745	Reveal.addEventListener( 'autoslidepaused', function ( evt ) {
1746//console.log('autoslidepaused');
1747		document.dispatchEvent( new CustomEvent( 'stopplayback' ) );
1748
1749		// advance to end of slide
1750//		closeChalkboard();
1751		startPlayback( getSlideDuration(), 0 );
1752	} );
1753
1754	function toggleNotesCanvas() {
1755		if ( !readOnly ) {
1756			if ( mode == 1 ) {
1757				toggleChalkboard();
1758				notescanvas.style.background = background[ 0 ]; //'rgba(255,0,0,0.5)';
1759				notescanvas.style.pointerEvents = 'auto';
1760			}
1761			else {
1762				if ( notescanvas.style.pointerEvents != 'none' ) {
1763					// hide notes canvas
1764					if ( colorButtons ) {
1765						notescanvas.querySelector( '.palette' ).style.visibility = 'hidden';
1766					}
1767					notescanvas.style.background = 'rgba(0,0,0,0)';
1768					notescanvas.style.pointerEvents = 'none';
1769				}
1770				else {
1771					// show notes canvas
1772					if ( colorButtons ) {
1773						notescanvas.querySelector( '.palette' ).style.visibility = 'visible';
1774					}
1775					notescanvas.style.background = background[ 0 ]; //'rgba(255,0,0,0.5)';
1776					notescanvas.style.pointerEvents = 'auto';
1777
1778					var idx = 0;
1779					if ( color[ mode ] ) {
1780						idx = color[ mode ];
1781					}
1782
1783					setColor( idx, true );
1784				}
1785			}
1786		}
1787	};
1788
1789	function toggleChalkboard() {
1790//console.log("toggleChalkboard " + mode);
1791		if ( mode == 1 ) {
1792			if ( !readOnly ) {
1793				recordEvent( { type: 'close' } );
1794			}
1795			closeChalkboard();
1796
1797			// broadcast
1798			var message = new CustomEvent( messageType );
1799			message.content = {
1800				sender: 'chalkboard-plugin',
1801				type: 'closeChalkboard',
1802				timestamp: Date.now() - slideStart,
1803				mode: 0,
1804				board
1805			};
1806			document.dispatchEvent( message );
1807
1808
1809		} else {
1810			showChalkboard();
1811			if ( !readOnly ) {
1812				recordEvent( { type: 'open' } );
1813				// broadcast
1814				var message = new CustomEvent( messageType );
1815				message.content = {
1816					sender: 'chalkboard-plugin',
1817					type: 'showChalkboard',
1818					timestamp: Date.now() - slideStart,
1819					mode: 1,
1820					board
1821				};
1822				document.dispatchEvent( message );
1823
1824				var idx = 0;
1825
1826				if ( rememberColor[ mode ] ) {
1827					idx = color[ mode ];
1828				}
1829
1830				setColor( idx, true );
1831			}
1832		}
1833	};
1834
1835	function clearSlide() {
1836		recordEvent( { type: 'clear' } );
1837		clearCanvas( mode );
1838	}
1839
1840	function clear() {
1841		if ( !readOnly ) {
1842			clearSlide();
1843			// broadcast
1844			var message = new CustomEvent( messageType );
1845			message.content = {
1846				sender: 'chalkboard-plugin',
1847				type: 'clear',
1848				timestamp: Date.now() - slideStart,
1849				mode,
1850				board
1851			};
1852			document.dispatchEvent( message );
1853		}
1854	};
1855
1856	function colorIndex( idx ) {
1857		if ( !readOnly ) {
1858			setColor( idx, true );
1859		}
1860	}
1861
1862	function colorNext() {
1863		if ( !readOnly ) {
1864			let idx = cycleColorNext();
1865			setColor( idx, true );
1866		}
1867	}
1868
1869	function colorPrev() {
1870		if ( !readOnly ) {
1871			let idx = cycleColorPrev();
1872			setColor( idx, true );
1873		}
1874	}
1875
1876	function resetSlideDrawings() {
1877		slideStart = Date.now();
1878		closeChalkboard();
1879
1880		clearCanvas( 0 );
1881		clearCanvas( 1 );
1882
1883		mode = 1;
1884		var slideData = getSlideData();
1885		slideData.duration = 0;
1886		slideData.events = [];
1887		mode = 0;
1888		var slideData = getSlideData();
1889		slideData.duration = 0;
1890		slideData.events = [];
1891
1892		updateStorage();
1893	}
1894
1895	function resetSlide( force ) {
1896		var ok = force || confirm( "Please confirm to delete chalkboard drawings on this slide!" );
1897		if ( ok ) {
1898//console.log("resetSlide ");
1899			stopPlayback();
1900			resetSlideDrawings();
1901			// broadcast
1902			var message = new CustomEvent( messageType );
1903			message.content = {
1904				sender: 'chalkboard-plugin',
1905				type: 'resetSlide',
1906				timestamp: Date.now() - slideStart,
1907				mode,
1908				board
1909			};
1910			document.dispatchEvent( message );
1911		}
1912	};
1913
1914	function resetStorage( force ) {
1915		var ok = force || confirm( "Please confirm to delete all chalkboard drawings!" );
1916		if ( ok ) {
1917			stopPlayback();
1918			slideStart = Date.now();
1919			clearCanvas( 0 );
1920			clearCanvas( 1 );
1921			if ( mode == 1 ) {
1922				closeChalkboard();
1923			}
1924
1925			storage = [ {
1926					width: Reveal.getConfig().width,
1927					height: Reveal.getConfig().height,
1928					data: []
1929				},
1930				{
1931					width: Reveal.getConfig().width,
1932					height: Reveal.getConfig().height,
1933					data: []
1934				}
1935			];
1936
1937			if ( config.storage ) {
1938				sessionStorage.setItem( config.storage, null )
1939			}
1940			// broadcast
1941			var message = new CustomEvent( messageType );
1942			message.content = {
1943				sender: 'chalkboard-plugin',
1944				type: 'init',
1945				timestamp: Date.now() - slideStart,
1946				storage,
1947				mode,
1948				board
1949			};
1950			document.dispatchEvent( message );
1951		}
1952	};
1953
1954	this.toggleNotesCanvas = toggleNotesCanvas;
1955	this.toggleChalkboard = toggleChalkboard;
1956	this.colorIndex = colorIndex;
1957	this.colorNext = colorNext;
1958	this.colorPrev = colorPrev;
1959	this.clear = clear;
1960	this.reset = resetSlide;
1961	this.resetAll = resetStorage;
1962	this.download = downloadData;
1963	this.updateStorage = updateStorage;
1964	this.getData = getData;
1965	this.configure = configure;
1966
1967
1968	for ( var key in keyBindings ) {
1969		if ( keyBindings[ key ] ) {
1970			Reveal.addKeyBinding( keyBindings[ key ], RevealChalkboard[ key ] );
1971		}
1972	};
1973
1974	return this;
1975};

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.