├── broadcast ├── nosignal.gif ├── generatehash.html ├── README.md ├── broadcast.js └── bCrypt.js ├── chalkboard ├── img │ ├── sponge.png │ ├── chalk-red.png │ ├── blackboard.png │ ├── chalk-blue.png │ ├── chalk-green.png │ ├── chalk-orange.png │ ├── chalk-purple.png │ ├── chalk-white.png │ ├── chalk-yellow.png │ ├── whiteboard.png │ ├── boardmarker-black.png │ ├── boardmarker-blue.png │ ├── boardmarker-green.png │ ├── boardmarker-red.png │ ├── boardmarker-orange.png │ ├── boardmarker-purple.png │ └── boardmarker-yellow.png ├── README.md └── chalkboard.js ├── .gitmodules ├── bower.json ├── package.json ├── embed-tweet ├── README.md └── embed-tweet.js ├── mathsvg ├── README.md └── math.js ├── spreadsheet ├── spreadsheet.css ├── README.md └── spreadsheet.js ├── LICENSE ├── fullscreen └── fullscreen.js ├── customcontrols ├── customcontrols.js └── README.md ├── anything ├── anything.js └── README.md ├── README.md ├── chart ├── csv2chart.js └── README.md └── audio-slideshow ├── README.md └── audio-slideshow.js /broadcast/nosignal.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hakimel/reveal.js-plugins/HEAD/broadcast/nosignal.gif -------------------------------------------------------------------------------- /chalkboard/img/sponge.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hakimel/reveal.js-plugins/HEAD/chalkboard/img/sponge.png -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "reveal.js-menu"] 2 | path = menu 3 | url = https://github.com/denehyg/reveal.js-menu.git 4 | -------------------------------------------------------------------------------- /chalkboard/img/chalk-red.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hakimel/reveal.js-plugins/HEAD/chalkboard/img/chalk-red.png -------------------------------------------------------------------------------- /chalkboard/img/blackboard.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hakimel/reveal.js-plugins/HEAD/chalkboard/img/blackboard.png -------------------------------------------------------------------------------- /chalkboard/img/chalk-blue.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hakimel/reveal.js-plugins/HEAD/chalkboard/img/chalk-blue.png -------------------------------------------------------------------------------- /chalkboard/img/chalk-green.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hakimel/reveal.js-plugins/HEAD/chalkboard/img/chalk-green.png -------------------------------------------------------------------------------- /chalkboard/img/chalk-orange.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hakimel/reveal.js-plugins/HEAD/chalkboard/img/chalk-orange.png -------------------------------------------------------------------------------- /chalkboard/img/chalk-purple.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hakimel/reveal.js-plugins/HEAD/chalkboard/img/chalk-purple.png -------------------------------------------------------------------------------- /chalkboard/img/chalk-white.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hakimel/reveal.js-plugins/HEAD/chalkboard/img/chalk-white.png -------------------------------------------------------------------------------- /chalkboard/img/chalk-yellow.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hakimel/reveal.js-plugins/HEAD/chalkboard/img/chalk-yellow.png -------------------------------------------------------------------------------- /chalkboard/img/whiteboard.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hakimel/reveal.js-plugins/HEAD/chalkboard/img/whiteboard.png -------------------------------------------------------------------------------- /chalkboard/img/boardmarker-black.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hakimel/reveal.js-plugins/HEAD/chalkboard/img/boardmarker-black.png -------------------------------------------------------------------------------- /chalkboard/img/boardmarker-blue.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hakimel/reveal.js-plugins/HEAD/chalkboard/img/boardmarker-blue.png -------------------------------------------------------------------------------- /chalkboard/img/boardmarker-green.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hakimel/reveal.js-plugins/HEAD/chalkboard/img/boardmarker-green.png -------------------------------------------------------------------------------- /chalkboard/img/boardmarker-red.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hakimel/reveal.js-plugins/HEAD/chalkboard/img/boardmarker-red.png -------------------------------------------------------------------------------- /chalkboard/img/boardmarker-orange.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hakimel/reveal.js-plugins/HEAD/chalkboard/img/boardmarker-orange.png -------------------------------------------------------------------------------- /chalkboard/img/boardmarker-purple.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hakimel/reveal.js-plugins/HEAD/chalkboard/img/boardmarker-purple.png -------------------------------------------------------------------------------- /chalkboard/img/boardmarker-yellow.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hakimel/reveal.js-plugins/HEAD/chalkboard/img/boardmarker-yellow.png -------------------------------------------------------------------------------- /bower.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "reveal.js-plugins", 3 | "version": "0.1.0", 4 | "homepage": "https://github.com/rajgoel/reveal.js-plugins", 5 | "authors": [ 6 | "Asvin Goel" 7 | ], 8 | "description": "A collection of plugins for Reveal.js", 9 | "keywords": [ 10 | "reveal", 11 | "plugin", 12 | "audio", 13 | "chart" 14 | ], 15 | "license": "MIT, Copyright (C) 2016 Asvin Goel", 16 | "ignore": [ 17 | "**/.*", 18 | "menu", 19 | "node_modules", 20 | "bower_components", 21 | "test", 22 | "tests" 23 | ] 24 | } 25 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "reveal.js-plugins", 3 | "version": "1.0.0", 4 | "description": "A plugin collection for your reveal.js presentations", 5 | "repository": { 6 | "type": "git", 7 | "url": "https://github.com/rajgoel/reveal.js-plugins.git" 8 | }, 9 | "keywords": [ 10 | "reveal", 11 | "plugins" 12 | ], 13 | "author": "Asvin Goel", 14 | "license": "MIT", 15 | "bugs": { 16 | "url": "https://github.com/rajgoel/reveal.js-plugins/issues" 17 | }, 18 | "homepage": "https://github.com/rajgoel/reveal.js-plugins/blob/master/README.md", 19 | "dependencies": { 20 | "npm": "^6.13.4" 21 | } 22 | } 23 | 24 | -------------------------------------------------------------------------------- /embed-tweet/README.md: -------------------------------------------------------------------------------- 1 | # Embed tweet 2 | 3 | A plugin for [Reveal.js](https://github.com/hakimel/reveal.js) allowing to easily embed tweets in your slides. 4 | 5 | [Check out the live demo](https://rajgoel.github.io/reveal.js-demos/embed-tweet-demo.html) 6 | 7 | ## Installation 8 | 9 | Copy the files ```embed-tweet.js``` into the plugin folder of your reveal.js presentation, i.e. ```plugin/embed-tweet```. 10 | 11 | Add the plugins to the dependencies in your presentation, as below. 12 | 13 | ```javascript 14 | Reveal.initialize({ 15 | // ... 16 | dependencies: [ 17 | // ... 18 | { src: 'plugin/embed-tweet/embed-tweet.js' }, 19 | // ... 20 | ] 21 | }); 22 | ``` 23 | 24 | ## Usage 25 | 26 | To embed a tweet, simply determine its URL and include the following code in your slides: 27 | 28 | ```html 29 |
30 | ``` 31 | 32 | ## License 33 | 34 | MIT licensed 35 | 36 | Copyright (C) 2017 Asvin Goel 37 | -------------------------------------------------------------------------------- /mathsvg/README.md: -------------------------------------------------------------------------------- 1 | # MathSVG 2 | 3 | An extension of the [math.js](https://github.com/hakimel/reveal.js/#mathjax) plugin allowing to render [LaTeX](http://en.wikipedia.org/wiki/LaTeX) in SVG. 4 | 5 | ## Installation 6 | 7 | Copy the file ```math.js``` into the plugin folder of your reveal.js presentation, i.e. ```plugin/mathsvg```. 8 | 9 | Add the plugins to the dependencies in your presentation as shown below. 10 | 11 | ```javascript 12 | Reveal.initialize({ 13 | // ... 14 | dependencies: [ 15 | // ... 16 | { src: 'plugin/mathsvg/math.js', async: true }, 17 | // ... 18 | ] 19 | }); 20 | ``` 21 | ## Usage 22 | 23 | With the plugin you can add ```text``` elements containing LaTeX expressions as shown in the example below. 24 | 25 | ```html 26 | 27 | 28 | $ a^2 + b^2 = c^2 $ 29 | 30 | ``` 31 | 32 | ## License 33 | 34 | MIT licensed 35 | 36 | Copyright (C) 2016 Asvin Goel 37 | -------------------------------------------------------------------------------- /spreadsheet/spreadsheet.css: -------------------------------------------------------------------------------- 1 | .spreadsheet table, .spreadsheet tr, .spreadsheet td, .spreadsheet input { 2 | margin: 0 auto; 3 | padding: 0; 4 | border: 0; 5 | } 6 | 7 | .spreadsheet table { 8 | border-collapse: collapse; 9 | } 10 | 11 | .spreadsheet td, .spreadsheet input { 12 | background-color: white; 13 | color: black; 14 | } 15 | 16 | .spreadsheet td { 17 | border: solid black 1px; 18 | } 19 | 20 | .spreadsheet input { 21 | background-color: transparent; 22 | } 23 | 24 | .spreadsheet tr:first-child td, .spreadsheet td:first-child { 25 | background-color: darkgray; 26 | padding: 1px 3px; 27 | font-weight: bold; 28 | text-align: center; 29 | } 30 | 31 | .spreadsheet td:hover { 32 | background-color: whitesmoke; 33 | } 34 | 35 | 36 | .spreadsheet input:not(:focus) { 37 | text-align: right; 38 | } 39 | 40 | .spreadsheet td.formula:not(:hover) { 41 | background-color: lightgray; 42 | } 43 | 44 | .spreadsheet td.error:not(:hover) { 45 | background-color: red; 46 | } 47 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 Asvin Goel 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | 23 | -------------------------------------------------------------------------------- /embed-tweet/embed-tweet.js: -------------------------------------------------------------------------------- 1 | var RevealEmbedTweet = window.RevealEmbedTweet || (function(){ 2 | var ready = false; 3 | window.twttr = (function(d, s, id) { 4 | var js, fjs = d.getElementsByTagName(s)[0], 5 | t = window.twttr || {}; 6 | if (d.getElementById(id)) return t; 7 | js = d.createElement(s); 8 | js.id = id; 9 | js.src = "https://platform.twitter.com/widgets.js"; 10 | fjs.parentNode.insertBefore(js, fjs); 11 | 12 | t._e = []; 13 | t.ready = function(f) { 14 | t._e.push(f); 15 | }; 16 | }(document, "script", "twitter-wjs")); 17 | 18 | 19 | function load() { 20 | if ( twttr != undefined && !document.querySelector('section[data-markdown]:not([data-markdown-parsed])') ) { 21 | tweets = document.querySelectorAll(".tweet"); 22 | for (i = 0; i < tweets.length; ++i) { 23 | tweets[i].style.cssText = "margin: 0;position: absolute; left: 50%;transform: translate(-50%,0%);" + tweets[i].style.cssText; 24 | tweets[i].innerHTML = '
Tweet
'; 25 | } 26 | twttr.widgets.load() 27 | } 28 | else { 29 | // wait for markdown to be loaded and parsed 30 | setTimeout( load, 100 ); 31 | } 32 | } 33 | 34 | Reveal.addEventListener( 'ready', function( event ) { 35 | load(); 36 | } ); 37 | 38 | this.refresh = load; 39 | 40 | return this; 41 | })(); 42 | 43 | 44 | 45 | 46 | -------------------------------------------------------------------------------- /fullscreen/fullscreen.js: -------------------------------------------------------------------------------- 1 | /***************************************************************** 2 | ** Author: Asvin Goel, goel@telematique.eu 3 | ** 4 | ** A plugin for reveal.js allowing slides to use the full window 5 | ** size. 6 | ** 7 | ** Version: 0.1 8 | ** 9 | ** License: MIT license (see LICENSE.md) 10 | ** 11 | ******************************************************************/ 12 | 13 | var RevealFullscreen= window.RevealFullscreen || (function(){ 14 | 15 | var config = null; 16 | var ready = false; 17 | 18 | Reveal.addEventListener( 'ready', function( event ) { 19 | ready = true; 20 | config = { width: Reveal.getConfig().width, height: Reveal.getConfig().height, margin: Reveal.getConfig().margin }; 21 | if ( Reveal.getCurrentSlide().hasAttribute("data-fullscreen") ) { 22 | Reveal.configure( { width: window.innerWidth, height: window.innerHeight, margin: 0 } ); 23 | } 24 | } ); 25 | 26 | Reveal.addEventListener( 'slidechanged', function( event ) { 27 | if ( Reveal.getCurrentSlide().hasAttribute("data-fullscreen") ) { 28 | Reveal.configure( { width: window.innerWidth, height: window.innerHeight, margin: 0 } ); 29 | } 30 | else { 31 | Reveal.configure( config ); 32 | } 33 | } ); 34 | 35 | window.addEventListener( 'resize', function( event ) { 36 | if ( ready && Reveal.getCurrentSlide().hasAttribute("data-fullscreen") ) { 37 | Reveal.configure( { width: window.innerWidth, height: window.innerHeight, margin: 0 } ); 38 | } 39 | } ); 40 | 41 | })(); 42 | -------------------------------------------------------------------------------- /customcontrols/customcontrols.js: -------------------------------------------------------------------------------- 1 | /***************************************************************** 2 | ** Author: Asvin Goel, goel@telematique.eu 3 | ** 4 | ** A plugin replacing the default controls by custom controls. 5 | ** 6 | ** Version: 0.1 7 | ** 8 | ** License: MIT license (see LICENSE.md) 9 | ** 10 | ******************************************************************/ 11 | 12 | var RevealCustomControls = window.RevealCustomControls || (function(){ 13 | var config = Reveal.getConfig().customcontrols || 14 | { 15 | slideNumberCSS : 'position: fixed; display: block; right: 90px; top: auto; left: auto; width: 50px; bottom: 30px; z-index: 31; font-family: Helvetica, sans-serif; font-size: 12px; line-height: 1; padding: 5px; text-align: center; border-radius: 10px; background-color: rgba(128,128,128,.5)', 16 | controls: [ 17 | { 18 | icon: '', 19 | css: 'position: fixed; right: 60px; bottom: 30px; z-index: 30; font-size: 24px;', 20 | action: 'Reveal.prev(); return false;' 21 | }, 22 | { 23 | icon: '', 24 | css: 'position: fixed; right: 30px; bottom: 30px; z-index: 30; font-size: 24px;', 25 | action: 'Reveal.next(); return false;' 26 | } 27 | ] 28 | }; 29 | 30 | var reveal = document.querySelector(".reveal"); 31 | 32 | for (var i = 0; i < config.controls.length; i++ ) { 33 | var control = document.createElement( 'div' ); 34 | control.className = "customcontrols"; 35 | control.style.cssText = config.controls[i].css; 36 | control.innerHTML = '' + config.controls[i].icon + ''; 37 | document.querySelector(".reveal").appendChild( control ); 38 | } 39 | 40 | Reveal.addEventListener( 'ready', function( event ) { 41 | if ( Reveal.getConfig().slideNumber && config.slideNumberCSS ) { 42 | var slideNumber = document.querySelector(".reveal .slide-number"); 43 | slideNumber.style.cssText = config.slideNumberCSS; 44 | } 45 | } ); 46 | 47 | return this; 48 | 49 | })(); 50 | 51 | -------------------------------------------------------------------------------- /customcontrols/README.md: -------------------------------------------------------------------------------- 1 | # Custom controls 2 | 3 | One of the core features of ```reveal.js``` is that slides are organised in two dimensions and the standard controls perfectly allow to advance the presentation horizontally and vertically. 4 | Sometimes, however, there is a need to customize the controls, e.g., if the slideshow is intended for users who mainly want to linearly advance through it. 5 | With this plugin you can add custom controls to reveal.js which allow arbitrary positioning, layout, and behaviour of the controls. 6 | 7 | [Check out the live demo](https://rajgoel.github.io/reveal.js-demos/customcontrols-demo.html) 8 | 9 | 10 | ## Installation 11 | 12 | Copy the file ```customcontrols.js``` into the plugin folder of your reveal.js presentation, i.e. ```plugin/customcontrols```. 13 | 14 | Add the plugin to the dependencies in your presentation and turn off the default controls as shown below: 15 | 16 | ```javascript 17 | Reveal.initialize({ 18 | controls: false, 19 | // ... 20 | dependencies: [ 21 | // ... 22 | { src: 'plugin/customcontrols/customcontrols.js' }, 23 | // ... 24 | ], 25 | // ... 26 | 27 | }); 28 | ``` 29 | 30 | Note, without configuration you need to add 31 | 32 | ```javascript 33 | 34 | ``` 35 | 36 | between `````` and `````` of your HTML file because the defaults use [Font Awesome](http://fontawesome.io/). 37 | 38 | 39 | 40 | ## Configuration 41 | 42 | The plugin can be configured by adding custom controls and changing the layout of the slide number, e.g., by: 43 | 44 | 45 | ```javascript 46 | Reveal.initialize({ 47 | // ... 48 | customcontrols: { 49 | slideNumberCSS : 'position: fixed; display: block; right: 90px; top: auto; left: auto; width: 50px; bottom: 30px; z-index: 31; font-family: Helvetica, sans-serif; font-size: 12px; line-height: 1; padding: 5px; text-align: center; border-radius: 10px; background-color: rgba(128,128,128,.5)', 50 | controls: [ 51 | { icon: '', 52 | css: 'position: fixed; right: 60px; bottom: 30px; z-index: 30; font-size: 24px;', 53 | action: 'Reveal.prev(); return false;' 54 | }, 55 | { icon: '', 56 | css: 'position: fixed; right: 30px; bottom: 30px; z-index: 30; font-size: 24px;', 57 | action: 'Reveal.next(); return false;' 58 | } 59 | ] 60 | }, 61 | // ... 62 | 63 | }); 64 | ``` 65 | 66 | The configuration should be self explaining and any number of controls can be added. 67 | 68 | ## License 69 | 70 | MIT licensed 71 | 72 | Copyright (C) 2017 Asvin Goel 73 | -------------------------------------------------------------------------------- /broadcast/generatehash.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 55 | 60 | 61 | 62 |
63 | 64 |
65 | 66 |
67 | 68 | 78 |
79 | 80 |
81 |
82 | 83 |
84 |
85 | 86 | 87 | -------------------------------------------------------------------------------- /anything/anything.js: -------------------------------------------------------------------------------- 1 | var RevealAnything = window.RevealAnything || (function(){ 2 | function parseJSON(str) { 3 | str = str.replace(/(\r\n|\n|\r|\t)/gm,""); // remove lien breaks and tabs 4 | var json; 5 | try { 6 | json = JSON.parse(str, function (key, value) { 7 | if (value && (typeof value === 'string') && value.indexOf("function") === 0) { 8 | // we can only pass a function as string in JSON ==> doing a real function 9 | // eval("var jsFunc = " + value); 10 | var jsFunc = new Function('return ' + value)(); 11 | return jsFunc; 12 | } 13 | return value; 14 | }); 15 | } catch (e) { 16 | return null; 17 | } 18 | return json; 19 | } 20 | 21 | /* 22 | * Recursively merge properties of two objects without overwriting the first 23 | */ 24 | function mergeRecursive(obj1, obj2) { 25 | for (var p in obj2) { 26 | try { 27 | // Property in destination object set; update its value. 28 | if ( obj2[p].constructor==Object ) { 29 | obj1[p] = mergeRecursive(obj1[p], obj2[p]); 30 | 31 | } else { 32 | if ( !obj1[p] ) obj1[p] = obj2[p]; 33 | 34 | } 35 | 36 | } catch(e) { 37 | // Property in destination object not set; create it and set its value. 38 | if ( !obj1[p] ) obj1[p] = obj2[p]; 39 | 40 | } 41 | } 42 | 43 | return obj1; 44 | } 45 | 46 | 47 | var config = Reveal.getConfig().anything; 48 | 49 | Reveal.addEventListener( 'ready', function( event ) { 50 | for (var i = 0; i < config.length; i++ ){ 51 | // Get all elements of the class 52 | var elements = document.getElementsByClassName(config[i].className); 53 | var initialize = config[i].initialize; 54 | // deprecated parameters 55 | if ( !initialize && config[i].f ) { 56 | initialize = config[i].f; 57 | console.warn('Setting parameter "f" is deprecated! Use "initialize" instead. '); 58 | } 59 | 60 | for (var j = 0; j < elements.length; j++ ){ 61 | var options = config[i].defaults; 62 | var comments = elements[j].innerHTML.trim().match(//g); 63 | if ( comments !== null ) for (var k = 0; k < comments.length; k++ ){ 64 | comments[k] = comments[k].replace(//,''); 66 | mergeRecursive( options, config[i].defaults); 67 | options = parseJSON(comments[k]); 68 | if ( options ) { 69 | mergeRecursive( options, config[i].defaults); 70 | break; 71 | } 72 | } 73 | // console.log(config[i].className + " options: " + JSON.stringify(options)) 74 | initialize(elements[j], options); 75 | // console.log(elements[j].outerHTML) 76 | } 77 | } 78 | 79 | 80 | } ); 81 | 82 | 83 | })(); 84 | 85 | 86 | -------------------------------------------------------------------------------- /spreadsheet/README.md: -------------------------------------------------------------------------------- 1 | # Spreadsheet 2 | 3 | A plugin for [Reveal.js](https://github.com/hakimel/reveal.js) allowing to add spreadsheets using [RuleJS](https://github.com/handsontable/RuleJS). 4 | 5 | [Check out the live demo](https://rajgoel.github.io/reveal.js-demos/spreadsheet-demo.html) 6 | 7 | ## Installation 8 | 9 | Copy the files ```ruleJS.all.full.min.js```, ```spreadsheet.js```, and ```spreadsheet.css``` into the plugin folder of your reveal.js presentation, i.e. ```reveal.js-plugins/spreadsheet```. 10 | 11 | Add the plugins to the dependencies in your presentation, as below. 12 | 13 | ```html 14 | 15 | 16 | 17 | 18 | 19 | 20 | ``` 21 | 22 | ```javascript 23 | 24 | Reveal.initialize({ 25 | // ... 26 | dependencies: [ 27 | // ... 28 | { src: 'reveal.js-plugins//spreadsheet/spreadsheet.js' }, 29 | // ... 30 | ] 31 | }); 32 | ``` 33 | 34 | ## Configuration 35 | 36 | The plugin has several optional parameters that you can set for your presentation by providing a```spreadsheet``` option in the reveal.js initialization options. The default values are given below. 37 | 38 | 39 | ```javascript 40 | Reveal.initialize({ 41 | // ... 42 | spreadsheet: { 43 | fontsize: 24, 44 | width: 150, 45 | delimiter: ",", 46 | precision: 4 // the maximum number of digits after the comma 47 | }, 48 | // ... 49 | }); 50 | ``` 51 | ## Usage 52 | 53 | A spreadsheet can be included in a slide by adding a ```div``` element with ```class="spreadsheet"```. You can prefill the spreadsheet by providing CSV data inside the ```div``` element. 54 | 55 | ```html 56 |
57 | =$B$2 'Maserati' "Mazda" "Mercedes" "Mini" =A$1 58 | 2009 0 2941 4303 354 5814 59 | 2010 5 2905 2867 =SUM(A4,2,3) =$B1 60 | 2011 4 2517 4822 552 6127 61 | 2012 =SUM(A2:A5) =SUM(B5,E3) =A2/B2 12 4151 62 |
63 | ``` 64 | You can add several attributes to a ```div``` element: 65 | - ```data-rows```: Provides the number of rows (uses the number of rows of your data if unspecified) 66 | - ```data-cols```: Provides the number of columns (uses the number of columns of your data if unspecified) 67 | - ```data-csv```: Provides the filename of a CSV-file containing the data 68 | - ```data-delimiter```: Provides the character that is used to separate cells in the input (uses the default configuration value if unspecified) 69 | - ```data-width```: Provides the column width (uses the default configuration value if unspecified) 70 | - ```data-fontsize```: Provides the font size (uses the default configuration value if unspecified) 71 | 72 | ## License 73 | 74 | MIT licensed 75 | 76 | Copyright (C) 2016 Asvin Goel 77 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ```diff 2 | - Important notes: 3 | - Currently, this repository is based on Reveal.js 3.9.0. 4 | - Reveal.js 4.0.0 has been released with some breaking changes. 5 | - The plugins provided here haven't (yet) been tested for the latest Reveal.js version. 6 | - It is the goal to update this repository to the latest version as soon as possible (until Fall). 7 | - Maintaining this project is a one-man show and updating may require some time. 8 | - Feedback on (broken) compatibility with the latest Reveal.js version is highly appreciated. 9 | ``` 10 | 11 | # reveal.js-plugins 12 | 13 | This is a collection of plugins for [Reveal.js](https://github.com/hakimel/reveal.js) - a framework for easily creating beautiful presentations using HTML. Example presentations and demos for these plugins can be found and added [here](https://github.com/rajgoel/reveal.js-plugins/wiki/Example-presentations). 14 | 15 | This collections includes the following plugins: 16 | 17 | - [Anything](https://github.com/rajgoel/reveal.js-plugins/tree/master/anything) ([Demo](https://rajgoel.github.io/reveal.js-demos/anything-demo.html)): A plugin for adding plots, charts, animated SVGs,or anything else inside an HTML object using a JSON string and a javascript function. 18 | - [Audio slideshow](https://github.com/rajgoel/reveal.js-plugins/tree/master/audio-slideshow) ([Demo](https://rajgoel.github.io/reveal.js-demos/audio-slideshow-demo.html)): A plugin for audio playback and recording. 19 | - [Broadcast](https://github.com/rajgoel/reveal.js-plugins/tree/master/broadcast) ([Demo](https://rajgoel.github.io/reveal.js-demos/broadcast-demo.html)): An extension of the multiplex plugin allowing to broadcast audio and video. 20 | - [Chalkboard](https://github.com/rajgoel/reveal.js-plugins/tree/master/chalkboard) ([Demo](https://rajgoel.github.io/reveal.js-demos/chalkboard-demo.html)): 21 | A plugin adding a chalkboard and slide annotation. 22 | - [Chart](https://github.com/rajgoel/reveal.js-plugins/tree/master/chart) ([Demo](https://rajgoel.github.io/reveal.js-demos/chart-demo.html)): 23 | A plugin for using Chart.js. 24 | - [Custom controls](https://github.com/rajgoel/reveal.js-plugins/tree/master/customcontrols) ([Demo](https://rajgoel.github.io/reveal.js-demos/customcontrols-demo.html)): 25 | A plugin for customization of controls. 26 | - [Embed tweet](https://github.com/rajgoel/reveal.js-plugins/tree/master/embed-tweet) ([Demo](https://rajgoel.github.io/reveal.js-demos/embed-tweet-demo.html)): 27 | A plugin allowing to easily embed tweets in your slides. 28 | - [Fullscreen](https://github.com/rajgoel/reveal.js-plugins/tree/master/fullscreen) ([Demo](https://rajgoel.github.io/reveal.js-demos/fullscreen-demo.html)): 29 | A simple plugin allowing to use fullscreen slides. 30 | - [MathSVG](https://github.com/rajgoel/reveal.js-plugins/tree/master/mathsvg): 31 | An extension of the [math.js](https://github.com/hakimel/reveal.js/#mathjax) plugin allowing to render LaTeX expressions in SVG. 32 | - [Menu](https://github.com/denehyg/reveal.js-menu) ([Demo](https://denehyg.github.io/reveal.js-menu)): A plugin by Greg Denehy for adding a slideout menu. 33 | - [Spreadsheet](https://github.com/rajgoel/reveal.js-plugins/tree/master/spreadsheet) ([Demo](https://rajgoel.github.io/reveal.js-demos/spreadsheet-demo.html)): 34 | A plugin for adding Excel-like spreadsheets with formulas. 35 | 36 | ## Getting started 37 | 38 | The source code of the demos can be found here: https://github.com/rajgoel/reveal.js-demos. 39 | 40 | ## Download 41 | 42 | You can download the plugins into the ```bower_components``` folder using 43 | 44 | ```bower install reveal.js-plugins``` 45 | 46 | or manually copy this repository next to the folder of your reveal.js presentation. 47 | 48 | Please note that the [menu](https://github.com/denehyg/reveal.js-menu)-plugin is a submodule and has to be downloaded separately. 49 | 50 | ## License 51 | 52 | MIT licensed 53 | 54 | Copyright (C) 2017 Asvin Goel 55 | 56 | -------------------------------------------------------------------------------- /chart/csv2chart.js: -------------------------------------------------------------------------------- 1 | /***************************************************************** 2 | ** Author: Asvin Goel, goel@telematique.eu 3 | ** 4 | ** csv2chart.js is a plugin for reveal.js allowing to integrate 5 | ** Chart.js in reveal.js 6 | ** 7 | ** Version: 0.2 8 | ** 9 | ** License: MIT license (see LICENSE.md) 10 | ** 11 | ******************************************************************/ 12 | 13 | var RevealChart = window.RevealChart || (function(){ 14 | function parseJSON(str) { 15 | var json; 16 | try { 17 | json = JSON.parse(str); 18 | } catch (e) { 19 | return null; 20 | } 21 | return json; 22 | } 23 | 24 | /* 25 | * Recursively merge properties of two objects 26 | */ 27 | function mergeRecursive(obj1, obj2) { 28 | 29 | for (var p in obj2) { 30 | try { 31 | // Property in destination object set; update its value. 32 | if ( obj1[p].constructor==Object && obj2[p].constructor==Object ) { 33 | obj1[p] = mergeRecursive(obj1[p], obj2[p]); 34 | 35 | } else { 36 | obj1[p] = obj2[p]; 37 | 38 | } 39 | 40 | } catch(e) { 41 | // Property in destination object not set; create it and set its value. 42 | obj1[p] = obj2[p]; 43 | 44 | } 45 | } 46 | 47 | return obj1; 48 | } 49 | 50 | 51 | function createChart(canvas, CSV, comments) { 52 | canvas.chart = null; 53 | var ctx = canvas.getContext("2d"); 54 | var chartOptions = { responsive: true }; 55 | var chartData = { labels: null, datasets: []}; 56 | if ( comments !== null ) for (var j = 0; j < comments.length; j++ ){ 57 | comments[j] = comments[j].replace(//,''); 59 | var config = parseJSON(comments[j]); 60 | if ( config ) { 61 | if ( config.data ) { 62 | mergeRecursive( chartData, config.data); 63 | } 64 | if ( config.options ) { 65 | mergeRecursive( chartOptions, config.options); 66 | } 67 | } 68 | } 69 | 70 | var lines = CSV.split('\n').filter(function(v){return v!==''}); 71 | // if labels are not defined, get them from first line 72 | if ( chartData.labels === null && lines.length > 0 ) { 73 | chartData.labels = lines[0].split(','); 74 | chartData.labels.shift(); 75 | lines.shift(); 76 | } 77 | // get data values 78 | for (var j = 0; j < lines.length; j++ ){ 79 | if (chartData.datasets.length <= j) chartData.datasets[j] = {}; 80 | chartData.datasets[j].data = lines[j].split(','); //.filter(function(v){return v!==''}); 81 | chartData.datasets[j].label = chartData.datasets[j].data[0]; 82 | chartData.datasets[j].data.shift(); 83 | for (var k = 0; k < chartData.datasets[j].data.length; k++ ){ 84 | chartData.datasets[j].data[k] = Number(chartData.datasets[j].data[k]); 85 | } 86 | } 87 | 88 | // add chart options 89 | var config = chartConfig[canvas.getAttribute("data-chart")]; 90 | if ( config ) { 91 | for (var j = 0; j < chartData.datasets.length; j++ ){ 92 | for (var attrname in config) { 93 | if ( !chartData.datasets[j][attrname] ) { 94 | chartData.datasets[j][attrname] = config[attrname][j%config[attrname].length]; 95 | } 96 | } 97 | } 98 | } 99 | 100 | canvas.chart = new Chart(ctx, { type: canvas.getAttribute("data-chart"), data: chartData, options: chartOptions }); 101 | 102 | } 103 | 104 | var initializeCharts = function(){ 105 | // Get all canvases 106 | var canvases = document.querySelectorAll("canvas"); 107 | for (var i = 0; i < canvases.length; i++ ){ 108 | // check if canvas has data-chart attribute 109 | if ( canvases[i].hasAttribute("data-chart") ){ 110 | var CSV = canvases[i].innerHTML.trim(); 111 | var comments = CSV.match(//g); 112 | CSV = CSV.replace(//g,'').replace(/^\s*\n/gm, "") 113 | if ( ! canvases[i].hasAttribute("data-chart-src") ) { 114 | createChart(canvases[i], CSV, comments); 115 | } 116 | else { 117 | var canvas = canvases[i]; 118 | var xhr = new XMLHttpRequest(); 119 | xhr.onload = function() { 120 | if (xhr.readyState === 4) { 121 | createChart(canvas, xhr.responseText, comments); 122 | } 123 | else { 124 | console.warn( 'Failed to get file ' + canvas.getAttribute("data-chart-src") +". ReadyState: " + xhr.readyState + ", Status: " + xhr.status); 125 | } 126 | }; 127 | 128 | xhr.open( 'GET', canvas.getAttribute("data-chart-src"), false ); 129 | try { 130 | xhr.send(); 131 | } 132 | catch ( error ) { 133 | console.warn( 'Failed to get file ' + canvas.getAttribute("data-chart-src") + '. Make sure that the presentation and the file are served by a HTTP server and the file can be found there. ' + error ); 134 | } 135 | } 136 | 137 | } 138 | } 139 | } 140 | 141 | function recreateChart(canvas) { 142 | var config = canvas.chart.config; 143 | canvas.chart.destroy(); 144 | setTimeout( function() { canvas.chart = new Chart(canvas, config); }, 500); // wait for slide transition 145 | } 146 | 147 | // check if chart option is given or not 148 | var chartConfig = Reveal.getConfig().chart || {}; 149 | 150 | // set global chart options 151 | var config = chartConfig["defaults"]; 152 | if ( config ) { 153 | mergeRecursive(Chart.defaults, config); 154 | } 155 | 156 | Reveal.addEventListener('ready', function(){ 157 | initializeCharts(); 158 | Reveal.addEventListener('slidechanged', function(){ 159 | var canvases = Reveal.getCurrentSlide().querySelectorAll("canvas[data-chart]"); 160 | for (var i = 0; i < canvases.length; i++ ){ 161 | if ( canvases[i].chart && canvases[i].chart.config.options.animation ) { 162 | recreateChart( canvases[i] ); 163 | } 164 | } 165 | 166 | }); 167 | }); 168 | })(); 169 | -------------------------------------------------------------------------------- /mathsvg/math.js: -------------------------------------------------------------------------------- 1 | /***************************************************************** 2 | ** Author: Asvin Goel, goel@telematique.eu 3 | ** 4 | ** An extension of the math.js plugin for reveal.js enabling 5 | ** rendering of math equations inside SVG graphics. 6 | ** 7 | ** Credits: 8 | ** - Hakim El Hattab for math.js 9 | ** MIT licensed 10 | ** - Jason M. Sachs for svg_mathjax.js 11 | ** Source: https://bitbucket.org/jason_s/svg_mathjax 12 | ** License: http://www.apache.org/licenses/LICENSE-2.0 13 | ******************************************************************/ 14 | 15 | var RevealMathSVG = window.RevealMathSVG || (function(){ 16 | 17 | var options = Reveal.getConfig().math || {}; 18 | options.mathjax = options.mathjax || 'https://cdn.mathjax.org/mathjax/latest/MathJax.js'; 19 | options.config = options.config || 'TeX-AMS-MML_SVG'; 20 | 21 | loadScript( options.mathjax + '?config=' + options.config, function() { 22 | MathJax.Hub.Config({ 23 | messageStyle: 'none', 24 | tex2jax: { 25 | inlineMath: [['$','$'],['\\(','\\)']] , 26 | skipTags: ['script','noscript','style','textarea','pre'] 27 | }, 28 | skipStartupTypeset: true 29 | }); 30 | 31 | // Typeset all math in SVGs 32 | typesetMathInSVG(); 33 | 34 | // Typeset followed by an immediate reveal.js layout since 35 | // the typesetting process could affect slide height 36 | MathJax.Hub.Queue( [ 'Typeset', MathJax.Hub ] ); 37 | MathJax.Hub.Queue( Reveal.layout ); 38 | 39 | // Reprocess equations in slides when they turn visible 40 | Reveal.addEventListener( 'slidechanged', function( event ) { 41 | MathJax.Hub.Queue( [ 'Typeset', MathJax.Hub, event.currentSlide ] ); 42 | } ); 43 | 44 | } ); 45 | 46 | function loadScript( url, callback ) { 47 | 48 | var head = document.querySelector( 'head' ); 49 | var script = document.createElement( 'script' ); 50 | script.type = 'text/javascript'; 51 | script.src = url; 52 | 53 | // Wrapper for callback to make sure it only fires once 54 | var finish = function() { 55 | if( typeof callback === 'function' ) { 56 | callback.call(); 57 | callback = null; 58 | } 59 | } 60 | 61 | script.onload = finish; 62 | 63 | // IE 64 | script.onreadystatechange = function() { 65 | if ( this.readyState === 'loaded' ) { 66 | finish(); 67 | } 68 | } 69 | 70 | // Normal browsers 71 | head.appendChild( script ); 72 | 73 | } 74 | 75 | // apply a function to elements of an array x 76 | function forEach( x, f ) { 77 | var n = x.length; for ( var i = 0; i < n; ++i ) { f( x[i] ); } 78 | } 79 | 80 | function cleanup( mathbucket ) { 81 | // remove the temporary items 82 | mathbucket.parentNode.removeChild( mathbucket ); 83 | } 84 | 85 | function replaceText( svgdest, mathjaxdiv, textcontainer ) { 86 | var svgmath = mathjaxdiv.getElementsByClassName( 'MathJax_SVG' )[0].getElementsByTagName( 'svg' )[0]; 87 | var svgmathinfo = { 88 | width: svgmath.viewBox.baseVal.width, 89 | height: svgmath.viewBox.baseVal.height 90 | }; 91 | // get graphics nodes 92 | var gnodes = svgmath.getElementsByTagName( 'g' )[0].cloneNode( true ); 93 | var fontsize = svgdest.getAttribute( 'font-size' ); 94 | var scale = 0.0016 * fontsize; 95 | var x = +svgdest.getAttribute( 'x' ); 96 | if ( svgdest.hasAttribute( 'dx' ) ) x = x + svgdest.getAttribute( 'dx' ); 97 | var y = +svgdest.getAttribute( 'y' ); 98 | if ( svgdest.hasAttribute( 'dy' ) ) x = x + svgdest.getAttribute( 'dy' ); 99 | 100 | var x0 = x; 101 | var y0 = y; 102 | var x1 = -svgmathinfo.width * 0.5; 103 | var y1 = svgmathinfo.height * 0.25; 104 | gnodes.setAttribute( 'transform', 'translate('+x0+' '+y0+') scale('+scale+') translate('+x1+' '+y1+') matrix(1 0 0 -1 0 0)' ); 105 | if ( svgdest.hasAttribute( 'fill' ) ) gnodes.setAttribute( 'fill', svgdest.getAttribute( 'fill' ) ); 106 | if ( svgdest.hasAttribute( 'stroke' ) ) gnodes.setAttribute( 'stroke', svgdest.getAttribute( 'stroke' ) ); 107 | 108 | textcontainer.parentNode.appendChild( gnodes ); 109 | svgdest.parentNode.removeChild( svgdest ); 110 | } 111 | 112 | function typeset( mathbucket, text, container ) { 113 | var regexp = /^\s*([LlRrCc]?)(\\\(.*\\\)|\$.*\$)\s*$/; 114 | var math = text.textContent.match(regexp); 115 | if ( math ) { 116 | var div = document.createElement( 'div' ); 117 | mathbucket.appendChild(div); 118 | var mathmarkup = math[2].replace(/^\$(.*)\$$/,'\\($1\\)'); 119 | div.appendChild( document.createTextNode( mathmarkup ) ); 120 | MathJax.Hub.Queue( [ "Typeset",MathJax.Hub,div ] ); 121 | MathJax.Hub.Queue( [ replaceText, text, div, container ] ); 122 | } 123 | } 124 | 125 | function typesetMathInSVG() { 126 | var mathbucket = document.createElement( 'div' ); 127 | mathbucket.setAttribute( 'id', 'mathjax_svg_bucket' ); 128 | document.body.appendChild( mathbucket ); 129 | 130 | forEach( document.getElementsByTagName( 'svg' ), function( svg ) { 131 | forEach( svg.getElementsByTagName( 'text' ), function( text ) { 132 | forEach( text.getElementsByTagName( 'tspan' ), function( tspan ) { 133 | if ( !tspan.hasAttribute( 'font-size' ) ) tspan.setAttribute( 'font-size', tspan.parentElement.getAttribute( 'font-size' ) ); 134 | if ( !tspan.hasAttribute( 'x' ) ) tspan.setAttribute( 'x', tspan.parentElement.getAttribute( 'x' ) ); 135 | if ( !tspan.hasAttribute( 'y' ) ) tspan.setAttribute( 'y', tspan.parentElement.getAttribute( 'y' ) ); 136 | if ( !tspan.hasAttribute( 'fill' ) && tspan.parentElement.hasAttribute( 'fill' ) ) tspan.setAttribute( 'fill', tspan.parentElement.getAttribute( 'fill' ) ); 137 | if ( !tspan.hasAttribute( 'stroke' ) && tspan.parentElement.hasAttribute( 'stroke' ) ) tspan.setAttribute( 'stroke', tspan.parentElement.getAttribute( 'stroke' ) ); 138 | typeset( mathbucket, tspan, tspan.parentElement ); 139 | }); 140 | typeset( mathbucket, text, text ); 141 | }); 142 | }); 143 | 144 | MathJax.Hub.Queue( [cleanup, mathbucket] ); 145 | } 146 | 147 | })(); 148 | -------------------------------------------------------------------------------- /broadcast/README.md: -------------------------------------------------------------------------------- 1 | # Broadcast # 2 | 3 | A plugin for Reveal.js allowing to broadcast audio and video for slide shows. 4 | 5 | [Check out the live demo](https://rajgoel.github.io/reveal.js-demos/broadcast-demo.html) 6 | 7 | ## Installation 8 | 9 | Copy the files of the plugin next to your reveal.js presentation and add the dependencies as below. 10 | 11 | ```javascript 12 | Reveal.initialize({ 13 | // ... 14 | dependencies: [ 15 | // ... 16 | { src: '../reveal.js-plugins/broadcast/RTCMultiConnection/RTCMultiConnection.min.js'}, 17 | { src: '../reveal.js-plugins/broadcast/RTCMultiConnection/adapter.js'}, 18 | { src: '../reveal.js-plugins/broadcast/RTCMultiConnection/socket.io.js'}, 19 | { src: '../reveal.js-plugins/broadcast/bCrypt.js'}, 20 | { src: '../reveal.js-plugins/broadcast/broadcast.js'}, 21 | // ... 22 | ] 23 | }); 24 | ``` 25 | ## Configuration 26 | 27 | You can configure the ```broadcast.js``` plugin by providing a ```broadcast``` option in the reveal.js initialization options. 28 | 29 | 30 | ```javascript 31 | Reveal.initialize({ 32 | // ... 33 | broadcast: { 34 | // Set master password to "123456" 35 | secret: '$2a$05$hhgakVn1DWBfgfSwMihABeYToIBEiQGJ.ONa.HWEiNGNI6mxFCy8S', 36 | // Configure RTCMultiConnection 37 | connection: { 38 | socketURL: 'https://revealjs-broadcast.herokuapp.com:443/' 39 | }, 40 | }, 41 | // ... 42 | }); 43 | ``` 44 | The parameter ```secret``` is a hash for the password which has to be provided when starting a broadcast. You can generate this secret with [```generatehash.html```](https://rajgoel.github.io/reveal.js-plugins/broadcast/generatehash.html). The parameter ```connection``` provides the configuration for RTCMultiConnection as described in the [API Reference](https://github.com/muaz-khan/RTCMultiConnection/blob/master/docs/api.md). The only required option is the parameter ```socketURL```. For testing purposes you may use the server ```https://revealjs-broadcast.herokuapp.com/```, but availability and stability are not guaranteed. For anything mission critical I recommend you run your own server. For example, you can deploy https://github.com/muaz-khan/RTCMultiConnection on [Heroku](https://www.heroku.com/) using this [Installation Guide](https://github.com/muaz-khan/RTCMultiConnection/blob/master/docs/installation-guide.md). Heroku allows you to directly use the GitHub repository without any changes. 45 | 46 | ## Start broadcast 47 | 48 | To start a broadcast you can include a button in the slideshow which calls the function ```RevealBroadcast.start``` with the broadcast id and the master password. 49 | 50 | ```html 51 | 52 | 53 | Start broadcast 54 | ``` 55 | After clicking the ```Start broadcast``` button, a draggable overlay for the video is shown. Initially, the video shows a snowy image. After connecting to the server and after the user has allowed acces to camera and microphone, the video captured by the camera is shown. 56 | 57 | ## Join broadcast 58 | 59 | To join a broadcast you can include a button in the slideshow which calls the function ```RevealBroadcast.connect``` with the broadcast id. 60 | 61 | ```html 62 | 63 | Join broadcast 64 | ``` 65 | After clicking the ```Join broadcast``` button, a draggable overlay for the video is shown. Initially, the video shows a snowy image. After connecting to the server, the client receives audio and video of the master and the slides are updated whith every update by the master. 66 | 67 | ## Custom events 68 | 69 | It is possible to send custom events to the clients by adding the following code to your presentation or plugin. 70 | 71 | ```javascript 72 | var message = new CustomEvent('send'); 73 | message.content = { sender: 'someplugin', type: 'somecustomevent' }; 74 | document.dispatchEvent( message ); 75 | ``` 76 | The broadcast plugin will forward this event to all connected clients who can listen to custom events using the following code. 77 | 78 | ```javascript 79 | document.addEventListener( 'received', function ( message ) { 80 | // only listen to events of the same sender 81 | if ( message.content && message.content.sender == 'someplugin' ) { 82 | switch ( message.content.type ) { 83 | case 'somecustomevent': 84 | // do something 85 | break; 86 | default: 87 | break; 88 | } 89 | } 90 | }); 91 | ``` 92 | Whenever a new client joins the broadcast the broadcast plugins sends a ```newclient``` event. You can react to the event, e.g., by broadcasting initialisation information to all clients. 93 | 94 | ```javascript 95 | document.addEventListener( 'newclient', function() { 96 | // (re-)send initialisation info as new client has joined 97 | var message = new CustomEvent('send'); 98 | message.content = { 99 | sender: 'someplugin', 100 | type: 'init', 101 | \\ ... 102 | }; 103 | document.dispatchEvent( message ); 104 | }); 105 | ``` 106 | In the [demo](https://rajgoel.github.io/reveal.js-demos/broadcast-demo.html) all drawings created with the ```chalkboard.js``` plugin are broadcasted to the clients. Checkout the source code of ```chalkboard.js``` plugin for an example of the implementation. 107 | 108 | ## Limitations 109 | 110 | Browser support may vary and usage on mobile OS may fail. It is recommended to use Chrome, Chromium, or Firefox on desktop. 111 | 112 | ## Credits 113 | 114 | The plugin uses a [patched](https://github.com/muaz-khan/RTCMultiConnection/pull/816) version of RTCMultiConnection by Muaz Khan (https://github.com/muaz-khan/RTCMultiConnection). 115 | 116 | ## License 117 | 118 | MIT licensed 119 | 120 | Copyright (C) 2020 Asvin Goel 121 | -------------------------------------------------------------------------------- /chart/README.md: -------------------------------------------------------------------------------- 1 | # Chart 2 | 3 | A plugin for [Reveal.js](https://github.com/hakimel/reveal.js) allowing to easily add charts using [Chart.js](http://www.chartjs.org/). 4 | 5 | [Check out the live demo](https://rajgoel.github.io/reveal.js-demos/chart-demo.html) 6 | 7 | ## Installation 8 | 9 | Copy the files ```Chart.min.js``` and ```csv2chart.js``` into the plugin folder of your reveal.js presentation, i.e. ```plugin/chart```. 10 | 11 | Add the plugins to the dependencies in your presentation, as below. 12 | 13 | ```javascript 14 | Reveal.initialize({ 15 | // ... 16 | dependencies: [ 17 | // ... 18 | { src: 'plugin/chart/Chart.min.js' }, 19 | { src: 'plugin/chart/csv2chart.js' }, 20 | // ... 21 | ] 22 | }); 23 | ``` 24 | ## Configuration 25 | 26 | The plugin has several parameters that you can set for your presentation by providing an ```chart``` option in the reveal.js initialization options. 27 | Note that all configuration parameters are optional and the defaults of [Chart.js 2.7](http://nnnick.github.io/Chart.js/docs-v2/) will be used for parameters that are not specified. 28 | 29 | 30 | ```javascript 31 | Reveal.initialize({ 32 | // ... 33 | chart: { 34 | defaults: { 35 | global: { 36 | title: { fontColor: "#FFF" }, 37 | legend: { 38 | labels: { fontColor: "#FFF" }, 39 | }, 40 | }, 41 | scale: { 42 | scaleLabel: { fontColor: "#FFF" }, 43 | gridLines: { color: "#FFF", zeroLineColor: "#FFF" }, 44 | ticks: { fontColor: "#FFF" }, 45 | } 46 | }, 47 | line: { borderColor: [ "rgba(20,220,220,.8)" , "rgba(220,120,120,.8)", "rgba(20,120,220,.8)" ], "borderDash": [ [5,10], [0,0] ]}, 48 | bar: { backgroundColor: [ "rgba(20,220,220,.8)" , "rgba(220,120,120,.8)", "rgba(20,120,220,.8)" ]}, 49 | pie: { backgroundColor: [ ["rgba(0,0,0,.8)" , "rgba(220,20,20,.8)", "rgba(20,220,20,.8)", "rgba(220,220,20,.8)", "rgba(20,20,220,.8)"] ]}, 50 | radar: { borderColor: [ "rgba(20,220,220,.8)" , "rgba(220,120,120,.8)", "rgba(20,120,220,.8)" ]}, 51 | }, 52 | // ... 53 | }); 54 | ``` 55 | The ```defaults``` parameter will overwrite ```Chart.defaults```. Furthermore, for any chart type, e.g. line, bar, etc., the parameters for the individual datasets can be specified. Where Chart.js allows to specify a single parameter for a particular dataset, the plugin allows to specify an array of values for this parameter, which will automatically be assigned to the different datasets. Note that if there are more datasets than elements in the array, the plugin will start again with the first value in the array. 56 | 57 | 58 | 59 | ## Usage 60 | 61 | A chart can be included in a slide by adding a ```canvas``` element with the ```data-chart``` attribute set to the desired chart type. 62 | 63 | The chart can be configured within the canvas body by a JSON string embedded into an HTML comment. 64 | 65 | ```html 66 | 67 | 85 | 86 | ``` 87 | It is possible to provide the chart data by comma separated values and use the JSON string within the HTML comment to configure the chart layout. 88 | 89 | ```html 90 | 91 | My first dataset, 65, 59, 80, 81, 56, 55, 40 92 | 93 | My second dataset, 28, 48, 40, 19, 86, 27, 90 94 | 102 | 103 | ``` 104 | 105 | The layout configuration provided in ```chart``` parameter (see Configuration) will be used by default and only those parameters that are specified in a JSON string are used to overwrite the default values. If no JSON string is provided to configure the chart layout the default configuration is used. Note, that if no labels for the data points are provided by a JSON string, the plugin expects that the first row provides table headers. 106 | 107 | ```html 108 | 109 | Month, January, February, March, April, May, June, July 110 | My first dataset, 65, 59, 80, 81, 56, 55, 40 111 | My second dataset, 28, 48, 40, 19, 86, 27, 90 112 | 113 | ``` 114 | 115 | The chart data can also be provided in an external CSV file. To include external data, the filename must be specified using the ```data-chart-src``` attribute of the ```canvas``` element. The CSV file is expected to only contain data values, whereas options for drawing the chart can be given as described above. 116 | 117 | ```html 118 | 119 | 127 | 128 | ``` 129 | 130 | ## Animation 131 | 132 | The plugin recreates the charts on a slide change to show the Chart.js animations. These animation can be turned off globally in the configuration by 133 | 134 | ```javascript 135 | Reveal.initialize({ 136 | // ... 137 | chart: { 138 | defaults: { 139 | global: { 140 | animation: null, 141 | // ... 142 | }, 143 | // ... 144 | }, 145 | }, 146 | // ... 147 | }); 148 | ``` 149 | 150 | or within a chart, e.g. by 151 | 152 | ```html 153 | 154 | 162 | 163 | ``` 164 | 165 | 166 | ## License 167 | 168 | MIT licensed 169 | 170 | Copyright (C) 2017 Asvin Goel 171 | -------------------------------------------------------------------------------- /spreadsheet/spreadsheet.js: -------------------------------------------------------------------------------- 1 | /***************************************************************** 2 | ** Author: Asvin Goel, goel@telematique.eu 3 | ** 4 | ** spreadsheet.js is a plugin for reveal.js allowing to integrate 5 | ** Excel-like tables supporting formulas 6 | ** 7 | ** Version: 0.1.1 8 | ** 9 | ** License: MIT license (see LICENSE.md) 10 | ** 11 | ******************************************************************/ 12 | 13 | var RevealSpreadsheet = window.RevealSpreadsheet || (function(){ 14 | // check if spreadsheet option is given or not 15 | var config = Reveal.getConfig().spreadsheet || {}; 16 | 17 | // default values 18 | if (!config.precision) config.precision = 4; 19 | if (!config.width) config.width = 150; 20 | if (!config.delimiter) config.delimiter = ','; 21 | if (!config.fontsize) config.fontsize = '24'; 22 | 23 | var currentSpreadsheet = null; 24 | var rules = new ruleJS(); 25 | rules.init(); 26 | rules.custom = { 27 | cellValue: function(row, col) { 28 | var cell = currentSpreadsheet.querySelector('input[data-row="' + row + '"][data-col="' + col + '"]'); 29 | return cell.value; 30 | } 31 | }; 32 | 33 | function createSpreadsheet(spreadsheet, CSV, comments) { 34 | var delimiter = spreadsheet.getAttribute("data-delimiter") || config.delimiter; 35 | var width = spreadsheet.getAttribute("data-width") || config.width; 36 | var fontsize = spreadsheet.getAttribute("data-fontsize") || config.fontsize; 37 | var table = document.createElement("table"); 38 | spreadsheet.innerHTML = ""; 39 | spreadsheet.appendChild(table); 40 | var lines = CSV.split('\n').filter(function(v){return v!==''}); 41 | // create empty spreadsheet 42 | var rows = spreadsheet.getAttribute("data-rows") || lines.length; 43 | var cols = spreadsheet.getAttribute("data-cols") || lines[0].split(delimiter).length; 44 | for (var i=0; i<=rows; i++) { 45 | var row = table.insertRow(-1); 46 | for (var j=0; j<=cols; j++) { 47 | var letter = (i+j == 0) ? "" : String.fromCharCode("A".charCodeAt(0)+j-1); 48 | var cell = row.insertCell(-1); 49 | cell.style.fontSize = fontsize + "px"; 50 | if ( i &&j ) { 51 | var input = document.createElement("input"); 52 | input.type = "text"; 53 | input.id = letter + i; 54 | input.setAttribute("data-row",(i-1)); 55 | input.setAttribute("data-col",(j-1)); 56 | input.style.width = width + "px"; 57 | input.style.fontSize = fontsize + "px"; 58 | input.onkeypress = function(e) { 59 | var keyCode = e.keyCode || e.which; 60 | if (keyCode == '13') { 61 | var nextid = this.id.charAt(0) + String.fromCharCode(this.id.charCodeAt(1)+1); 62 | var spreadsheet = e.target.parentElement; 63 | while ( spreadsheet.className != "spreadsheet" ) spreadsheet = spreadsheet.parentElement; 64 | (spreadsheet.querySelector('input[id="'+nextid+'"]') || spreadsheet.querySelector('input[id="'+this.id.charAt(0)+'1"]')).focus(); 65 | } 66 | }; 67 | input.onfocus = function(e) { 68 | // show function 69 | var formula = e.target.getAttribute("data-formula"); 70 | if ( formula ) { 71 | e.target.value = "=" + formula; 72 | } 73 | }; 74 | input.onkeydown = function(e) { 75 | // stop propagation 76 | e.stopPropagation(); 77 | }; 78 | input.onkeypress = function(e) { 79 | // stop propagation 80 | e.stopPropagation(); 81 | }; 82 | input.onblur = function(e) { 83 | if (e.target.value.charAt(0) == "=") { 84 | e.target.setAttribute("data-formula", e.target.value.substring(1)); 85 | e.target.parentElement.classList.add('formula'); 86 | } 87 | else { 88 | e.target.removeAttribute("data-formula"); 89 | e.target.parentElement.classList.remove('formula'); 90 | } 91 | var spreadsheet = e.target.parentElement; 92 | while ( spreadsheet.className != "spreadsheet" ) spreadsheet = spreadsheet.parentElement; 93 | updateSpreadsheet( spreadsheet ); 94 | }; 95 | 96 | cell.appendChild(input); 97 | } 98 | else { 99 | cell.innerHTML = i || letter; 100 | } 101 | } 102 | } 103 | 104 | 105 | // get data values 106 | for (var i = 0; i < Math.min(rows,lines.length); i++ ){ 107 | values = lines[i].split(delimiter); 108 | for (var j = 0; j < Math.min(cols, values.length); j++ ){ 109 | var value = values[j].trim(); 110 | value = value.replace(/^['"](.+)['"]$/,'$1'); 111 | value = value.replace('','').replace('',''); 112 | var cell = spreadsheet.querySelector('input[data-row="' + i + '"][data-col="' + j + '"]'); 113 | if (value.charAt(0) == "=") { 114 | // console.log( value.substring(1) ); 115 | cell.setAttribute("data-formula", value.substring(1)); 116 | cell.parentElement.classList.add('formula'); 117 | updateValue(spreadsheet, cell, value.substring(1)); 118 | } 119 | else { 120 | cell.value = value; 121 | } 122 | updateSpreadsheet( spreadsheet ); 123 | } 124 | } 125 | 126 | } 127 | 128 | function updateSpreadsheet(spreadsheet) { 129 | var inputs = spreadsheet.querySelectorAll("input"); 130 | for (var i = 0; i < inputs.length; i++ ){ 131 | var formula = inputs[i].getAttribute("data-formula"); 132 | if ( formula ) { 133 | updateValue(spreadsheet, inputs[i], formula); 134 | } 135 | } 136 | }; 137 | 138 | 139 | function updateValue(spreadsheet, cell, formula) { 140 | currentSpreadsheet = spreadsheet; 141 | var parsed = rules.parse(formula, {row: cell.getAttribute("data-row"), col: cell.getAttribute("data-col"), id: cell.id}); 142 | if ( parsed.error ) { 143 | cell.value = parsed.error; 144 | cell.parentElement.classList.add('error'); 145 | } 146 | else { 147 | if ( true || isNaN(parsed.result) ) { 148 | cell.value = parsed.result; 149 | } 150 | else { 151 | cell.value = +parsed.result.toFixed(config.precision); 152 | } 153 | cell.parentElement.classList.remove('error'); 154 | } 155 | } 156 | 157 | 158 | 159 | function initialize(){ 160 | // Get all spreadsheets 161 | var spreadsheets = document.querySelectorAll("div.spreadsheet"); 162 | for (var i = 0; i < spreadsheets.length; i++ ){ 163 | var CSV = spreadsheets[i].innerHTML.trim(); 164 | var comments = CSV.match(//g); 165 | CSV = CSV.replace(//g,'').replace(/^\s*\n/gm, ""); 166 | if ( ! spreadsheets[i].hasAttribute("data-csv") ) { 167 | createSpreadsheet(spreadsheets[i], CSV, comments); 168 | } 169 | else { 170 | var spreadsheet = spreadsheets[i]; 171 | var xhr = new XMLHttpRequest(); 172 | xhr.onload = function() { 173 | if (xhr.readyState === 4) { 174 | createSpreadsheet(spreadsheet, xhr.responseText, comments); 175 | } 176 | else { 177 | console.warn( 'Failed to get file ' + spreadsheet.getAttribute("data-csv") +". ReadyState: " + xhr.readyState + ", Status: " + xhr.status); 178 | } 179 | }; 180 | xhr.open( 'GET', spreadsheet.getAttribute("data-csv"), false ); 181 | try { 182 | xhr.send(); 183 | } 184 | catch ( error ) { 185 | console.warn( 'Failed to get file ' + spreadsheet.getAttribute("data-csv") + '. Make sure that the presentation and the file are served by a HTTP server and the file can be found there. ' + error ); 186 | } 187 | } 188 | } 189 | } 190 | Reveal.addEventListener('ready', function(){ 191 | initialize(); 192 | }); 193 | 194 | 195 | 196 | })(); 197 | -------------------------------------------------------------------------------- /anything/README.md: -------------------------------------------------------------------------------- 1 | # Anything 2 | 3 | A plugin for [Reveal.js](https://github.com/hakimel/reveal.js) allowing to add anything inside an HTML object using a JSON string and a javascript function. 4 | The plugin allows you to define a class for which the content of all HTML object of this class will be modified by a given javascript function. 5 | Inside the HTML object you can provide a comment containing a JSON string that will be used by function to customise the content. 6 | 7 | [Check out the live demo](https://rajgoel.github.io/reveal.js-demos/anything-demo.html) 8 | 9 | ## Installation 10 | 11 | Copy the files ```anything.js``` into the plugin folder of your reveal.js presentation, i.e. ```plugin/anything```. 12 | 13 | Add the plugins to the dependencies in your presentation, as below. 14 | 15 | ```javascript 16 | Reveal.initialize({ 17 | // ... 18 | dependencies: [ 19 | // ... 20 | { src: 'plugin/anything/anything.js' }, 21 | // ... 22 | ] 23 | }); 24 | ``` 25 | ## Configuration & basic usage 26 | 27 | The plugin can be configured by providing an ```anything``` option containing an array of ```className```, ```defaults```, and ```f``` within the reveal.js initialization options. 28 | 29 | 30 | ```javascript 31 | Reveal.initialize({ 32 | // ... 33 | anything: [ 34 | { 35 | className: "random", 36 | defaults: {min: 0, max: 9}, 37 | initialize: (function(container, options){ 38 | container.innerHTML = Math.trunc( options.min + Math.random()*(options.max-options.min + 1) ); 39 | }) 40 | }, 41 | // ... 42 | ], 43 | ``` 44 | 45 | With the above configuration the plugin searches for all HTML object with class ```random```. 46 | For each of the HTML objects it checks whether there is a JSON string within a comment inside the HTML object. 47 | Then, it calls the function ```function(container, options)``` where ```container``` is the HTML object and ```options``` is the JSON string. 48 | It is possible to specify the ```defaults``` parameter to be used if no JSON string is provided or not all values required by the function are given in the JSON string. 49 | 50 | The code 51 | ```html 52 |

53 | Today's winning 3 digit number is : 54 | , 55 | , 56 | . 57 |

58 | ``` 59 | produces the output 60 | 61 | ```html 62 |

63 | Today's winning 3 digit number is : 64 | 3, 65 | 8, 66 | 0. 67 |

68 | ``` 69 | The code 70 | ```html 71 |

72 | Today's roll of a die is: 73 | . 74 |

75 | ``` 76 | produces the output 77 | 78 | ```html 79 |

80 | Today's roll of a die is: 81 | 4. 82 |

83 | ``` 84 | 85 | 86 | 87 | ## Advanced usage 88 | 89 | The plugin can be used to easily integrate external javascript libraries. 90 | 91 | ### Charts.js 92 | 93 | With the plugin charts created by [Chart.js v2.0](http://www.chartjs.org/) can easily be included in the slides. 94 | 95 | ```javascript 96 | Reveal.initialize({ 97 | // ... 98 | anything: [ 99 | { 100 | className: "chart", 101 | initialize: (function(container, options){ container.chart = new Chart(container.getContext("2d"), options); }) 102 | }, 103 | // ... 104 | ], 105 | dependencies: [ 106 | // ... 107 | { src: 'Chart.min.js' }, 108 | { src: 'plugin/anything/anything.js' }, 109 | // ... 110 | ] 111 | }); 112 | ``` 113 | A chart can be included in a slide by adding a ```canvas``` element and a JSON string specifying the chart options. 114 | 115 | ```html 116 | 117 | 136 | 137 | ``` 138 | Note, that the [Chart plugin](https://github.com/rajgoel/reveal.js-plugins/tree/master/chart) provides an easier way to use Chart.js. 139 | 140 | ### Function-plot.js 141 | 142 | With the plugin plots of functions created by [Function-plot.js](https://github.com/maurizzzio/function-plot) can easily be included in the slides. 143 | 144 | ```javascript 145 | Reveal.initialize({ 146 | // ... 147 | anything: [ 148 | { 149 | className: "plot", 150 | defaults: {width:500, height: 500, grid:true}, 151 | initialize: (function(container, options){ options.target = "#"+container.id; functionPlot(options) }) 152 | }, 153 | // ... 154 | ], 155 | dependencies: [ 156 | // ... 157 | { src: 'reveal.js-plugins/function-plot/site/js/vendor/jquery-1.11.2.min.js' }, 158 | { src: 'reveal.js-plugins/function-plot/site/js/vendor/d3.js' }, 159 | { src: 'reveal.js-plugins/function-plot/site/js/function-plot.js' }, 160 | { src: 'plugin/anything/anything.js' }, 161 | // ... 162 | ] 163 | }); 164 | ``` 165 | A plot can be included in a slide by adding a ```div``` element and a JSON string specifying the options. 166 | 167 | ```html 168 |
169 | 180 |
181 | ``` 182 | With the above ```defaults```, the input can be eased, e.g. 183 | ```html 184 |
185 | 192 |
193 | ``` 194 | ## More advanced usage 195 | 196 | The plugin allows to define functions within the JSON options. 197 | 198 | ### Example 199 | 200 | In the following example, the function ```options.initialize(container)``` is called for each element of the class ```anything```. The function is defined within the JSON string. 201 | 202 | ```javascript 203 | Reveal.initialize({ 204 | // ... 205 | anything: [ 206 | { 207 | className: "anything", 208 | initialize: (function(container, options){ if (options && options.initialize) { options.initialize(container)} }) 209 | }, 210 | // ... 211 | ], 212 | dependencies: [ 213 | // ... 214 | { src: 'reveal.js-plugins/anything/d3/d3.v3.min.js' }, 215 | { src: 'reveal.js-plugins/anything/d3/topojson.v1.min.js' }, 216 | { src: 'plugin/anything/anything.js' }, 217 | // ... 218 | ] 219 | }); 220 | ``` 221 | The [d3.js](d3js.org) library can now be used to draw a [globe](http://bl.ocks.org/mbostock/ba63c55dd2dbc3ab0127) within a canvas element. 222 | 223 | ```html 224 | 225 | 264 | 265 | ``` 266 | 267 | ## License 268 | 269 | MIT licensed 270 | 271 | Copyright (C) 2016 Asvin Goel 272 | -------------------------------------------------------------------------------- /chalkboard/README.md: -------------------------------------------------------------------------------- 1 | # Chalkboard 2 | 3 | With this plugin you can add a chalkboard to reveal.js. The plugin provides two possibilities to include handwritten notes to your presentation: 4 | 5 | - you can make notes directly on the slides, e.g. to comment on certain aspects, 6 | - you can open a chalkboard or whiteboard on which you can make notes. 7 | 8 | The main use case in mind when implementing the plugin is classroom usage in which you may want to explain some course content and quickly need to make some notes. 9 | 10 | The plugin records all drawings made so that they can be play backed using the ```autoSlide``` feature or the ```audio-slideshow``` plugin. 11 | 12 | [Check out the live demo](https://rajgoel.github.io/reveal.js-demos/chalkboard-demo.html) 13 | 14 | The chalkboard effect is based on [Chalkboard](https://github.com/mmoustafa/Chalkboard) by Mohamed Moustafa. 15 | 16 | Multi color support added by Kurt Rinnert [GitHub](https://github.com/rinnert). 17 | 18 | ## Installation 19 | 20 | Copy the file ```chalkboard.js``` and the ```img``` directory into the plugin folder of your reveal.js presentation, i.e. ```plugin/chalkboard```. 21 | 22 | Add the plugins to the dependencies in your presentation as shown below. 23 | 24 | ```javascript 25 | Reveal.initialize({ 26 | // ... 27 | chalkboard: { 28 | // optionally load pre-recorded chalkboard drawing from file 29 | src: "chalkboard.json", 30 | }, 31 | dependencies: [ 32 | // ... 33 | { src: 'plugin/chalkboard/chalkboard.js' }, 34 | // ... 35 | ], 36 | keyboard: { 37 | 67: function() { RevealChalkboard.toggleNotesCanvas() }, // toggle notes canvas when 'c' is pressed 38 | 66: function() { RevealChalkboard.toggleChalkboard() }, // toggle chalkboard when 'b' is pressed 39 | 46: function() { RevealChalkboard.clear() }, // clear chalkboard when 'DEL' is pressed 40 | 8: function() { RevealChalkboard.reset() }, // reset chalkboard data on current slide when 'BACKSPACE' is pressed 41 | 68: function() { RevealChalkboard.download() }, // downlad recorded chalkboard drawing when 'd' is pressed 42 | 88: function() { RevealChalkboard.colorNext() }, // cycle colors forward when 'x' is pressed 43 | 89: function() { RevealChalkboard.colorPrev() }, // cycle colors backward when 'y' is pressed 44 | }, 45 | // ... 46 | 47 | }); 48 | ``` 49 | In order to include buttons for opening and closing the notes canvas or the chalkboard you should make sure that ```font-awesome``` is available. The easiest way is to include 50 | ``` 51 | 52 | ``` 53 | to the ```head``` section of you HTML-file. 54 | 55 | ## Usage 56 | 57 | ### Enable & disable 58 | 59 | With above configuration the notes canvas is opened and closed when pressing 'c' and the chalkboard is opened and closed when pressing 'b'. 60 | 61 | ### Mouse 62 | - Click the left mouse button and drag to write on notes canvas or chalkboard 63 | - Click the right mouse button and drag to wipe away previous drawings 64 | 65 | ### Touch 66 | - Touch and move to write on notes canvas or chalkboard 67 | - Touch and hold for half a second, then move to wipe away previous drawings 68 | 69 | ### Keyboard 70 | - Press the 'DEL' key to clear the chalkboard 71 | - Press the 'd' key to download chalkboard drawings 72 | - Press the 'BACKSPACE' key to delete all chalkboard drawings on the current slide 73 | - Press the 'x' key to cycle colors forward 74 | - Press the 'y' key to cycle colors backward 75 | 76 | ## Playback 77 | 78 | If the ```autoSlide``` feature is set or if the ```audio-slideshow``` plugin is used, pre-recorded chalkboard drawings can be played. The slideshow plays back the user interaction with the chalkboard in the same way as it was conducted when recording the data. 79 | 80 | ## PDF-Export 81 | 82 | If the slideshow is opened in [print mode](https://github.com/hakimel/reveal.js/#pdf-export) the pre-recorded chalkboard drawings (which must be provided in a file, see ```src``` option) are included in the PDF-file. Each drawing on the chalkboard is added after the slide that was shown when opening the chalkboard. Drawings are also included if they had been cleared (using the 'DEL' key). Drawings on the notes canvas are not included in the PDF-file. 83 | 84 | 85 | ## Configuration 86 | 87 | The plugin has several configuration options: 88 | 89 | - ```boardmarkerWidth```: an integer, the drawing width of the boardmarker; larger values draw thicker lines. 90 | - ```chalkWidth```: an integer, the drawing width of the chalk; larger values draw thicker lines. 91 | - ```chalkEffect```: a float in the range ```[0.0, 1.0]```, the intesity of the chalk effect on the chalk board. Full effect (default) ```1.0```, no effect ```0.0```. 92 | - ```src```: Optional filename for pre-recorded drawings. 93 | - ```readOnly```: Configuation option allowing to prevent changes to existing drawings. If set to ```true``` no changes can be made, if set to false ```false``` changes can be made, if unset or set to ```undefined``` no changes to the drawings can be made after returning to a slide or fragment for which drawings had been recorded before. In any case the recorded drawings for a slide or fragment can be cleared by pressing the 'DEL' key (i.e. by using the ```RevealChalkboard.clear()``` function). 94 | - ```toggleNotesButton```: If set to ```true``` a button for opening and closing the notes canvas is shown. Alternatively, the css position attributes can be provided if the default position is not appropriate. 95 | - ```toggleChalkboardButton```: If set to ```true``` a button for opening and closing the chalkboard is shown. Alternatively, the css position attributes can be provided if the default position is not appropriate. 96 | - ```transition```: Gives the duration (in milliseconds) of the transition for a slide change, so that the notes canvas is drawn after the transition is completed. 97 | - ```theme```: Can be set to either ```"chalkboard"``` or ```"whiteboard"```. 98 | 99 | The following configuration options allow to change the appearance of the notes canvas and the chalkboard. All of these options require two values, the first gives the value for the notes canvas, the second for the chalkboard. 100 | 101 | - ```background```: The first value expects a (semi-)transparent color which is used to provide visual feedback that the notes canvas is enabled, the second value expects a filename to a background image for the chalkboard. 102 | - ```grid```: By default whiteboard and chalkboard themes include a grid pattern on the background. This pattern can be modified by setting the color, the distance between lines, and the line width, e.g. ```{ color: 'rgb(127,127,255,0.1)', distance: 40, width: 2}```. Alternatively, the grid can be removed by setting the value to ```false```. 103 | - ```eraser```: An image path and radius for the eraser. 104 | - ```boardmarkers```: A list of boardmarkers with given color and cursor. 105 | - ```chalks```: A list of chalks with given color and cursor. 106 | - ```rememberColor```: Whether to remember the last selected color for the slide canvas or the board. 107 | 108 | All of the configurations are optional and the default values shown below are used if the options are not provided. 109 | 110 | ```javascript 111 | Reveal.initialize({ 112 | // ... 113 | chalkboard: { 114 | boardmarkerWidth: 3, 115 | chalkWidth: 7, 116 | chalkEffect: 1.0, 117 | src: null, 118 | readOnly: undefined, 119 | toggleChalkboardButton: { left: "30px", bottom: "30px", top: "auto", right: "auto" }, 120 | toggleNotesButton: { left: "30px", bottom: "30px", top: "auto", right: "auto" }, 121 | transition: 800, 122 | theme: "chalkboard", 123 | background: [ 'rgba(127,127,127,.1)' , path + 'img/blackboard.png' ], 124 | grid: { color: 'rgb(50,50,10,0.5)', distance: 80, width: 2}, 125 | eraser: { src: path + 'img/sponge.png', radius: 20}, 126 | boardmarkers : [ 127 | { color: 'rgba(100,100,100,1)', cursor: 'url(' + path + 'img/boardmarker-black.png), auto'}, 128 | { color: 'rgba(30,144,255, 1)', cursor: 'url(' + path + 'img/boardmarker-blue.png), auto'}, 129 | { color: 'rgba(220,20,60,1)', cursor: 'url(' + path + 'img/boardmarker-red.png), auto'}, 130 | { color: 'rgba(50,205,50,1)', cursor: 'url(' + path + 'img/boardmarker-green.png), auto'}, 131 | { color: 'rgba(255,140,0,1)', cursor: 'url(' + path + 'img/boardmarker-orange.png), auto'}, 132 | { color: 'rgba(150,0,20150,1)', cursor: 'url(' + path + 'img/boardmarker-purple.png), auto'}, 133 | { color: 'rgba(255,220,0,1)', cursor: 'url(' + path + 'img/boardmarker-yellow.png), auto'} 134 | ], 135 | chalks: [ 136 | { color: 'rgba(255,255,255,0.5)', cursor: 'url(' + path + 'img/chalk-white.png), auto'}, 137 | { color: 'rgba(96, 154, 244, 0.5)', cursor: 'url(' + path + 'img/chalk-blue.png), auto'}, 138 | { color: 'rgba(237, 20, 28, 0.5)', cursor: 'url(' + path + 'img/chalk-red.png), auto'}, 139 | { color: 'rgba(20, 237, 28, 0.5)', cursor: 'url(' + path + 'img/chalk-green.png), auto'}, 140 | { color: 'rgba(220, 133, 41, 0.5)', cursor: 'url(' + path + 'img/chalk-orange.png), auto'}, 141 | { color: 'rgba(220,0,220,0.5)', cursor: 'url(' + path + 'img/chalk-purple.png), auto'}, 142 | { color: 'rgba(255,220,0,0.5)', cursor: 'url(' + path + 'img/chalk-yellow.png), auto'} 143 | ] 144 | }, 145 | // ... 146 | 147 | }); 148 | ``` 149 | 150 | **Note:** Customisation of pens and eraser has changed since version 0.8 of the plugin. This behaviour may break backwards compatibility for older pen configurations. 151 | 152 | **Note:** Customisation of pens has changed since version 0.5 of the plugin, it is now possible to use standard cursors, e.g. by setting ```pen: [ 'crosshair', 'pointer' ]```. Please update your parameters if migrating from an older version. 153 | 154 | ## License 155 | 156 | **Note:** 157 | 158 | MIT licensed 159 | 160 | Copyright (C) 2016 Asvin Goel 161 | -------------------------------------------------------------------------------- /audio-slideshow/README.md: -------------------------------------------------------------------------------- 1 | # Audio slideshow 2 | 3 | A plugin for [Reveal.js](https://github.com/hakimel/reveal.js) allowing to easily add audio playback to each slide and fragment of your presentation. 4 | The slideshow adds an audio player to the slideshow and plays an audio file provided for each slide and fragment. 5 | When an audio file has finished playing, the plugin and automatically advances the slideshow to the next slide or fragment. 6 | 7 | [Check out the live demo](https://rajgoel.github.io/reveal.js-demos/audio-slideshow-demo.html) 8 | 9 | 10 | ## Installation 11 | 12 | Copy the files ```audio-slideshow.js``` and ```slideshow-recorder.js``` into the plugin folder of your reveal.js presentation, i.e. ```plugin/audio-slideshow```. 13 | 14 | Add the plugins to the dependencies in your presentation, as below. 15 | 16 | ```javascript 17 | Reveal.initialize({ 18 | // ... 19 | dependencies: [ 20 | // ... 21 | { src: 'plugin/audio-slideshow/RecordRTC.js', condition: function( ) { return !!document.body.classList; } }, 22 | { src: 'plugin/audio-slideshow/slideshow-recorder.js', condition: function( ) { return !!document.body.classList; } }, 23 | { src: 'plugin/audio-slideshow/audio-slideshow.js', condition: function( ) { return !!document.body.classList; } }, 24 | // ... 25 | ] 26 | }); 27 | ``` 28 | 29 | The plugin ```slideshow-recorder.js``` is optional and not necessary for audio playback. 30 | 31 | 32 | ## Configuration 33 | 34 | The ```audio-slideshow.js``` plugin has several parameters that you can set for your presentation by providing an ```audio``` option in the reveal.js initialization options. 35 | Note that all configuration parameters are optional and will default as specified below. 36 | 37 | 38 | ```javascript 39 | Reveal.initialize({ 40 | // ... 41 | audio: { 42 | prefix: 'audio/', // audio files are stored in the "audio" folder 43 | suffix: '.ogg', // audio files have the ".ogg" ending 44 | textToSpeechURL: null, // the URL to the text to speech converter 45 | defaultNotes: false, // use slide notes as default for the text to speech converter 46 | defaultText: false, // use slide text as default for the text to speech converter 47 | advance: 0, // advance to next slide after given time in milliseconds after audio has played, use negative value to not advance 48 | autoplay: false, // automatically start slideshow 49 | defaultDuration: 5, // default duration in seconds if no audio is available 50 | defaultAudios: true, // try to play audios with names such as audio/1.2.ogg 51 | playerOpacity: 0.05, // opacity value of audio player if unfocused 52 | playerStyle: 'position: fixed; bottom: 4px; left: 25%; width: 50%; height:75px; z-index: 33;', // style used for container of audio controls 53 | startAtFragment: false, // when moving to a slide, start at the current fragment or at the start of the slide 54 | }, 55 | // ... 56 | }); 57 | ``` 58 | 59 | You can configure keyboard shortcuts for the ```slideshow-recorder.js``` plugin by configuring the ```keyboard``` option in the reveal.js initialization options. 60 | 61 | ```javascript 62 | Reveal.initialize({ 63 | // ... 64 | keyboard: { 65 | 82: function() { Recorder.toggleRecording(); }, // press 'r' to start/stop recording 66 | 90: function() { Recorder.downloadZip(); }, // press 'z' to download zip containing audio files 67 | 84: function() { Recorder.fetchTTS(); } // press 't' to fetch TTS audio files 68 | } 69 | // ... 70 | }; 71 | ``` 72 | 73 | ## Preparing an audio slideshow 74 | 75 | For each slide or fragment you can explicitly specify a file to be played when the slide or fragment is shown by setting the ```data-audio-src``` attribute for the slide or fragment. 76 | 77 | ```html 78 |
79 |

80 | With audio slideshows you can add recorded audio to whatever you want to deliver to your audience. 81 |

82 |

83 | Listen to the birds 84 |

85 |
86 | ``` 87 | 88 | If no audio file is explicitly specified, the plugin automatically determines the name of the audio file using the given ```prefix```, the slide (or fragment) indices, and the ```suffix```, e.g. in the above code the slideshow will play the file ```audio/1.2.ogg``` before the fragment is shown (assuming that ```prefix``` is ```"audio/"```, ```suffix``` is ```".ogg"``` , ```Reveal.getIndices().h``` is ```"1"``` and ```Reveal.getIndices().v``` is ```"2"```). 89 | 90 | If you just want to play audio when file names are explicitly set with ```data-audio-src```, configure ```defaultAudios``` to ```false```. 91 | 92 | ### Text-to-speech 93 | 94 | If no audio file is explicitly specified and the default audio file is not found, the plugin can play audio files obtained from a text-to-speech generator. 95 | In order to enable text-to-speech functionality, the parameter ```textToSpeechURL``` must be specified. 96 | For example, in order to use the free text-to-speech generator of [Voice RSS](http://www.voicerss.org/) you can set ```textToSpeechURL: "http://api.voicerss.org/?key=[YOUR_KEY]&hl=en-gb&c=ogg&src="```, 97 | where ```[YOUR_KEY]``` should be the key that you obtained after [registration at Voice RSS](http://www.voicerss.org/registration.aspx). 98 | 99 | The plugin automatically extracts the text to be sent to the text-to-speech generator from the slide content in the following order: 100 | - If the optional ```data-audio-text``` attribute is given for the slide or fragment, the value of this attribute is used as the text. 101 | - If the parameter ```defaultNotes``` is set to ```true```, the text given in the notes of the slide are used as the text (note that this option does not work with fragments). 102 | - If the parameter ```defaultText``` is set to ```true```, the slide or fragment content is used as text. 103 | 104 | 105 | ```html 106 |
107 |

This is the text shown on the slide

108 |
109 |
110 |

This is the text shown on the slide

111 |
112 | 113 | ``` 114 | 115 | The ```slideshow-recorder.js``` plugin allows you to fetch the automatically generated audio files using the ```Recorder.fetchTTS()``` method (see configuration options). The fetched audio files are downloaded as a zip-file and can be provided to the slideshow. 116 | Note that the text-to-speech converter is only used if no audio file is provided with the slideshow. 117 | 118 | ### Audio recording 119 | 120 | You can use the ```slideshow-recorder.js``` plugin to record audio files for each slide and fragment. 121 | The ```Recorder.toggleRecording()``` method (see configuration options) is used to start or stop recording. 122 | A red circle in the upper right corner of the slideshow shows that the recorder is on. 123 | When navigating to a slide for which an audio file is already recorded, recording is suspended so that the previously recorded file is not lost. 124 | A yellow circle shows that recording is automatically resumed when navigating to a slide without a recorded audio file. 125 | After stopping the recorder, you can use the audio controls to check your recording and use the ```Recorder.downloadZip()``` method (see configuration options) to download a zip-file containing the audio files recorded for each slide and fragment. 126 | 127 | 128 | ### Navigation behaviour 129 | 130 | #### Slides without audio 131 | 132 | If no audio file and no text is provided for a slide or fragment, the slide advances after the duration specified by the ```defaultDuration``` parameter. 133 | 134 | #### Options for automatically advancing to next slide 135 | 136 | The ```advance``` parameter can be used to specify a time (in milliseconds) to wait before advancing to the next slide or fragment. 137 | If the parameter value is set to zero, the slideshow advances with the next slide or fragment immediately after the previous audio is played. 138 | If the parameter value is set to a negative value, the slideshow does not advance after the audio is played. 139 | For each slide or fragment the ```data-audio-advance``` attribute can be set to overwrite the parameter. 140 | 141 | #### Automatically start slideshow 142 | 143 | By default the slideshow does not start automatically. The ```autoplay``` parameter can be used to automatically start the slideshow when navigating to it. 144 | 145 | #### Navigating to a slide with fragments 146 | 147 | By default the audio slideshow does not show any fragment when navigating to a slide (even if they were shown previously). The ```startAtFragment``` parameter can be used to use the default behaviour of reveal.js. 148 | 149 | 150 | #### Linking audio controls to embedded video 151 | 152 | By setting the ```data-audio-controls``` attribute for a video, the audio player controls can be linked to an embedded video. 153 | 154 | ```html 155 | 157 | ``` 158 | 159 | 160 | ## Compatibility and known issues 161 | 162 | Playback is supported on recent desktop versions of Firefox, Chrome, and Opera. 163 | However, audio support of different browsers and for different operating systems is differently implemented and may not always work flawlessly. 164 | For example, playback of audio when using Chrome for Android, must be triggered [manually](https://code.google.com/p/chromium/issues/detail?id=178297) for each slide and fragment due to design decisions of Chrome developers. 165 | For other browser and mobile devices the functionality may be limited or the plugin may not work at all. 166 | 167 | 168 | The ```slideshow-recorder.js``` plugin is based on [RecordRTC.js](https://github.com/muaz-khan/WebRTC-Experiment/tree/master/RecordRTC) and supports recording on recent desktop versions of Firefox, Chrome, and Opera. 169 | For other browser and mobile devices recording may not work at all. 170 | 171 | ### Recording and fetching audio files on Chrome 172 | 173 | Your slideshow should be loaded on HTTP or HTTPS. For slide decks stored on the local disk, you may have to launch the Chrome browser from the command line window with additional arguments for full functionality. 174 | 175 | ``` 176 | google-chrome --disable-web-security --allow-file-access-from-files slidedeck.html 177 | ``` 178 | 179 | ## License 180 | 181 | MIT licensed 182 | 183 | Copyright (C) 2016 Asvin Goel 184 | -------------------------------------------------------------------------------- /broadcast/broadcast.js: -------------------------------------------------------------------------------- 1 | /***************************************************************** 2 | ** Author: Asvin Goel, goel@telematique.eu 3 | ** 4 | ** A plugin for broadcasting reveal.js presentations 5 | ** 6 | ** Version: 0.2 7 | ** 8 | ** License: MIT license (see LICENSE.md) 9 | ** 10 | ** Credits: 11 | ** Reveal.js & multiplex plugin by Hakim El Hattab (https://github.com/hakimel/reveal.js) 12 | ** RTCMultiConnection by Muaz Khan (https://github.com/muaz-khan/RTCMultiConnection) 13 | ******************************************************************/ 14 | 15 | var RevealBroadcast = window.RevealBroadcast || (function(){ 16 | 17 | /* 18 | * Recursively merge properties of two objects 19 | */ 20 | function mergeRecursive(obj1, obj2) { 21 | for (var p in obj2) { 22 | try { 23 | // Property in destination object set; update its value. 24 | if ( obj1[p].constructor==Object && obj2[p].constructor==Object ) { 25 | obj1[p] = mergeRecursive(obj1[p], obj2[p]); 26 | } else { 27 | obj1[p] = obj2[p]; 28 | } 29 | } catch(e) { 30 | // Property in destination object not set; create it and set its value. 31 | obj1[p] = obj2[p]; 32 | } 33 | } 34 | return obj1; 35 | }; 36 | 37 | var path = scriptPath(); 38 | function scriptPath() { 39 | // obtain plugin path from the script element 40 | var src; 41 | if (document.currentScript) { 42 | src = document.currentScript.src; 43 | } else { 44 | var sel = document.querySelector('script[src$="/chalkboard.js"]') 45 | if (sel) { 46 | src = sel.src; 47 | } 48 | } 49 | 50 | var path = typeof src === undefined ? src 51 | : src.slice(0, src.lastIndexOf("/") + 1); 52 | //console.log("Path: " + path); 53 | return path; 54 | } 55 | 56 | /***************************************************************** 57 | ** Initialisation 58 | ******************************************************************/ 59 | var config = Reveal.getConfig().broadcast || {}; 60 | 61 | var master = false; 62 | var broadcastId = window.location.pathname; 63 | if ( config.broadcastId ) broadcastId = config.broadcastId; 64 | if ( config.master ) master = config.master; 65 | var width = config.width || 640; 66 | var height = config.height || 480; 67 | var mediaPlayer = null; 68 | var defaults = { 69 | iceServers: [{ 70 | 'urls': [ 71 | 'stun:stun.l.google.com:19302', 72 | 'stun:stun1.l.google.com:19302', 73 | 'stun:stun2.l.google.com:19302', 74 | 'stun:stun.l.google.com:19302?transport=udp', 75 | ] 76 | }], 77 | enableScalableBroadcast: true, 78 | maxRelayLimitPerUser: 2, 79 | useDefaultDevices: true, // do not force selection of specific devices 80 | autoCloseEntireSession: true, 81 | // by default, socket.io server is assumed to be deployed on your own URL 82 | // overwrite scoketURL, e.g. connection.socketURL = 'https://rtcmulticonnection.herokuapp.com:443/'; 83 | socketURL: '/', 84 | session: { 85 | audio: true, 86 | video: true, 87 | oneway: true 88 | }, 89 | socketMessageEvent: 'reveal.js-broadcast-demo', 90 | enableLogs: true 91 | }; 92 | 93 | var connection = new RTCMultiConnection( window.location.pathname ); // use URL as channel-id 94 | mergeRecursive(connection,defaults); 95 | if ( config.connection ) mergeRecursive(connection,config.connection); 96 | 97 | connection.socketCustomEvent = connection.channel; 98 | 99 | function prestart( params ) { 100 | if ( config.secret ) { 101 | if ( params ) { 102 | checkpw( params.password, config.secret, function( verified ) { 103 | if ( verified ) { 104 | start( params ); 105 | } 106 | else { 107 | alert("Wrong password!"); 108 | } 109 | }); 110 | } 111 | else { 112 | alert("You are not allowed to start broadcast without password!"); 113 | } 114 | } 115 | else { 116 | start( params ); 117 | } 118 | } 119 | 120 | function start( params ) { 121 | if ( !document.getElementById('broadcast-mediaplayer') ) createPreview(); 122 | if ( params && params.id ) broadcastId = params.id; 123 | 124 | // user need to connect server, so that others can reach him. 125 | connection.connectSocket(function(socket) { 126 | 127 | socket.on('logs', function(log) { 128 | console.log( log ); 129 | }); 130 | 131 | // this event is emitted when a broadcast is absent. 132 | socket.on('start-broadcasting', function(typeOfStreams) { 133 | console.log('start-broadcasting', typeOfStreams); 134 | 135 | // host i.e. sender should always use this! 136 | connection.sdpConstraints.mandatory = { 137 | OfferToReceiveVideo: false, 138 | OfferToReceiveAudio: false 139 | }; 140 | connection.session = typeOfStreams; 141 | 142 | // "open" method here will capture media-stream 143 | // we can skip this function always; it is totally optional here. 144 | // we can use "connection.getUserMediaHandler" instead 145 | connection.open(connection.userid, function() { 146 | console.log("Connection opened: " + connection.sessionid); 147 | }); 148 | }); 149 | 150 | /* 151 | // this event is emitted when a broadcast is already created. 152 | socket.on('join-broadcaster', function(hintsToJoinBroadcast) { 153 | console.log('join-broadcaster', hintsToJoinBroadcast); 154 | 155 | connection.session = hintsToJoinBroadcast.typeOfStreams; 156 | connection.sdpConstraints.mandatory = { 157 | OfferToReceiveVideo: !!connection.session.video, 158 | OfferToReceiveAudio: !!connection.session.audio 159 | }; 160 | connection.broadcastId = hintsToJoinBroadcast.broadcastId; 161 | connection.join(hintsToJoinBroadcast.userid); 162 | }); 163 | */ 164 | /* 165 | socket.on('rejoin-broadcast', function(broadcastId) { 166 | console.log('rejoin-broadcast', broadcastId); 167 | 168 | connection.attachStreams = []; 169 | socket.emit('check-broadcast-presence', broadcastId, function(isBroadcastExists) { 170 | if(!isBroadcastExists) { 171 | // the first person (i.e. real-broadcaster) MUST set his user-id 172 | connection.userid = broadcastId; 173 | } 174 | 175 | socket.emit('join-broadcast', { 176 | broadcastId: broadcastId, 177 | userid: connection.userid, 178 | typeOfStreams: connection.session 179 | }); 180 | }); 181 | }); 182 | */ 183 | /* 184 | socket.on('broadcast-stopped', function(broadcastId) { 185 | console.error('broadcast-stopped', broadcastId); 186 | // alert('This broadcast has been stopped.'); 187 | }); 188 | */ 189 | // establish connection 190 | socket.emit('check-broadcast-presence', broadcastId, function(isBroadcastExists) { 191 | if(!isBroadcastExists) { 192 | // the first person (i.e. real-broadcaster) MUST set his user-id 193 | console.log('Start broadcast', broadcastId, isBroadcastExists); 194 | connection.userid = broadcastId; 195 | socket.emit('join-broadcast', { 196 | broadcastId: broadcastId, 197 | userid: connection.userid, 198 | typeOfStreams: connection.session 199 | }); 200 | } 201 | else { 202 | alert('Broadcast already exists!'); 203 | } 204 | 205 | function post( evt ) { 206 | socket.emit(connection.socketCustomEvent, { 207 | sender: connection.userid, 208 | state: Reveal.getState(), 209 | content: evt.content || null 210 | }); 211 | }; 212 | 213 | // Monitor events that trigger a change in state 214 | Reveal.addEventListener( 'slidechanged', post ); 215 | Reveal.addEventListener( 'fragmentshown', post ); 216 | Reveal.addEventListener( 'fragmenthidden', post ); 217 | Reveal.addEventListener( 'overviewhidden', post ); 218 | Reveal.addEventListener( 'overviewshown', post ); 219 | Reveal.addEventListener( 'paused', post ); 220 | Reveal.addEventListener( 'resumed', post ); 221 | document.addEventListener( 'send', post ); // broadcast custom events sent by other plugins 222 | }); 223 | 224 | }); 225 | 226 | connection.onNumberOfBroadcastViewersUpdated = function(event) { 227 | if (!connection.isInitiator) return; 228 | console.log("Number of viewers: " + event.numberOfBroadcastViewers); 229 | }; 230 | 231 | connection.onNewParticipant = function(participantId, userPreferences) { 232 | if (!connection.isInitiator) return; 233 | console.log( participantId + " joined"); 234 | connection.acceptParticipationRequest(participantId, userPreferences); 235 | // Send current state so that new participant will move to the same slide 236 | connection.socket.emit(connection.socketCustomEvent, { 237 | sender: connection.userid, 238 | state: Reveal.getState() 239 | }); 240 | // inform other plugins 241 | var event = new CustomEvent('newclient'); 242 | event.content = { id: participantId, preferences: userPreferences }; 243 | document.dispatchEvent( event ); 244 | 245 | }; 246 | 247 | }; 248 | 249 | function connect( params ) { 250 | if ( !document.getElementById('broadcast-mediaplayer') ) createPreview(); 251 | if ( params && params.id ) broadcastId = params.id; 252 | // user need to connect server, so that others can reach him. 253 | connection.connectSocket(function(socket) { 254 | // Receive custom event 255 | socket.on(connection.socketCustomEvent, function(message) { 256 | console.log("Received: " + JSON.stringify( message ) ); 257 | if ( message.state ) { 258 | Reveal.setState(message.state); 259 | } 260 | if ( message.content ) { 261 | // forward custom events to other plugins 262 | var event = new CustomEvent('received'); 263 | event.content = message.content; 264 | document.dispatchEvent( event ); 265 | } 266 | }); 267 | 268 | socket.on('logs', function(log) { 269 | console.log( log ); 270 | }); 271 | 272 | /* 273 | // this event is emitted when a broadcast is absent. 274 | socket.on('start-broadcasting', function(typeOfStreams) { 275 | console.log('start-broadcasting', typeOfStreams); 276 | 277 | // host i.e. sender should always use this! 278 | connection.sdpConstraints.mandatory = { 279 | OfferToReceiveVideo: false, 280 | OfferToReceiveAudio: false 281 | }; 282 | connection.session = typeOfStreams; 283 | 284 | // "open" method here will capture media-stream 285 | // we can skip this function always; it is totally optional here. 286 | // we can use "connection.getUserMediaHandler" instead 287 | connection.open(connection.userid, function() { 288 | console.log("Connection opened: " + connection.sessionid); 289 | }); 290 | }); 291 | */ 292 | 293 | // this event is emitted when a broadcast is already created. 294 | socket.on('join-broadcaster', function(hintsToJoinBroadcast) { 295 | console.log('join-broadcaster', hintsToJoinBroadcast); 296 | 297 | connection.session = hintsToJoinBroadcast.typeOfStreams; 298 | connection.sdpConstraints.mandatory = { 299 | OfferToReceiveVideo: !!connection.session.video, 300 | OfferToReceiveAudio: !!connection.session.audio 301 | }; 302 | connection.broadcastId = hintsToJoinBroadcast.broadcastId; 303 | connection.join(hintsToJoinBroadcast.userid); 304 | }); 305 | 306 | socket.on('rejoin-broadcast', function(broadcastId) { 307 | console.log('rejoin-broadcast', broadcastId); 308 | 309 | connection.attachStreams = []; 310 | socket.emit('check-broadcast-presence', broadcastId, function(isBroadcastExists) { 311 | if( isBroadcastExists ) { 312 | socket.emit('join-broadcast', { 313 | broadcastId: broadcastId, 314 | userid: connection.userid, 315 | typeOfStreams: connection.session 316 | }); 317 | } 318 | }); 319 | }); 320 | 321 | socket.on('broadcast-stopped', function(broadcastId) { 322 | console.error('broadcast-stopped', broadcastId); 323 | alert('This broadcast has been stopped.'); 324 | }); 325 | 326 | // establish connection 327 | socket.emit('check-broadcast-presence', broadcastId, function(isBroadcastExists) { 328 | if (isBroadcastExists) { 329 | console.log('Join broadcast', broadcastId, isBroadcastExists); 330 | socket.emit('join-broadcast', { 331 | broadcastId: broadcastId, 332 | userid: connection.userid, 333 | typeOfStreams: connection.session 334 | }); 335 | } 336 | else { 337 | alert('Broadcast does not yet exist!'); 338 | } 339 | }); 340 | 341 | }); 342 | } 343 | /// End connect 344 | 345 | 346 | connection.onstream = function(event) { 347 | if(connection.isInitiator && event.type !== 'local') { 348 | return; 349 | } 350 | 351 | connection.isUpperUserLeft = false; 352 | mediaPlayer.srcObject = event.stream; 353 | mediaPlayer.play(); 354 | 355 | mediaPlayer.userid = event.userid; 356 | 357 | if(event.type === 'local') { 358 | mediaPlayer.muted = true; 359 | } 360 | 361 | if (connection.isInitiator == false && event.type === 'remote') { 362 | // he is merely relaying the media 363 | connection.dontCaptureUserMedia = true; 364 | connection.attachStreams = [event.stream]; 365 | connection.sdpConstraints.mandatory = { 366 | OfferToReceiveAudio: false, 367 | OfferToReceiveVideo: false 368 | }; 369 | 370 | var socket = connection.getSocket(); 371 | socket.emit('can-relay-broadcast'); 372 | 373 | if(connection.DetectRTC.browser.name === 'Chrome') { 374 | connection.getAllParticipants().forEach(function(p) { 375 | if(p + '' != event.userid + '') { 376 | var peer = connection.peers[p].peer; 377 | peer.getLocalStreams().forEach(function(localStream) { 378 | peer.removeStream(localStream); 379 | }); 380 | peer.addStream(event.stream); 381 | connection.dontAttachStream = true; 382 | connection.renegotiate(p); 383 | connection.dontAttachStream = false; 384 | } 385 | }); 386 | } 387 | 388 | if(connection.DetectRTC.browser.name === 'Firefox') { 389 | // Firefox is NOT supporting removeStream method 390 | // that's why using alternative hack. 391 | // NOTE: Firefox seems unable to replace-tracks of the remote-media-stream 392 | // need to ask all deeper nodes to rejoin 393 | connection.getAllParticipants().forEach(function(p) { 394 | if(p + '' != event.userid + '') { 395 | connection.replaceTrack(event.stream, p); 396 | } 397 | }); 398 | } 399 | } 400 | }; 401 | 402 | /* 403 | connection.onstreamended = function() { 404 | console.log("Stream ended!"); 405 | }; 406 | 407 | connection.onleave = function(event) { 408 | if(event.userid !== videoPreview.userid) return; 409 | 410 | var socket = connection.getSocket(); 411 | socket.emit('can-not-relay-broadcast'); 412 | 413 | connection.isUpperUserLeft = true; 414 | //console.log('can-not-relay-broadcast'); 415 | }; 416 | */ 417 | 418 | 419 | /***************************************************************** 420 | ** Create preview 421 | ******************************************************************/ 422 | function scriptPath() { 423 | // obtain plugin path from the script element 424 | var src; 425 | if (document.currentScript) { 426 | src = document.currentScript.src; 427 | } else { 428 | var sel = document.querySelector('script[src$="/chalkboard.js"]') 429 | if (sel) { 430 | src = sel.src; 431 | } 432 | } 433 | 434 | var path = typeof src === undefined ? src 435 | : src.slice(0, src.lastIndexOf("/") + 1); 436 | //console.log("Path: " + path); 437 | return path; 438 | } 439 | 440 | function createPreview() { 441 | var reference = document.querySelector('.reveal'), selected = null, // Object of the element to be moved 442 | x_pos = 0, y_pos = 0, // Stores x & y coordinates of the mouse pointer 443 | x_elem = 0, y_elem = 0, // Stores top, left values (edge) of the element 444 | v_elem, h_elem; // Stores vertical and horizontal orientation 445 | // Will be called when user starts dragging an element 446 | function _drag_init(elem) { 447 | // Store the object of the element which needs to be moved 448 | selected = elem; 449 | x_elem = x_pos - selected.offsetLeft; 450 | y_elem = y_pos - selected.offsetTop; 451 | } 452 | 453 | // Will be called when user dragging an element 454 | function _move_elem(e) { 455 | x_pos = document.all ? window.event.clientX : e.pageX; 456 | y_pos = document.all ? window.event.clientY : e.pageY; 457 | if (selected !== null) { 458 | selected.style.left = (x_pos - x_elem) + 'px'; 459 | selected.style.top = (y_pos - y_elem) + 'px'; 460 | } 461 | _update_elem(); 462 | } 463 | 464 | // Destroy the object when we are done 465 | function _destroy() { 466 | selected = null; 467 | } 468 | 469 | function _update_elem() { 470 | if ( selected && reference ) { 471 | var referencerect = reference.getBoundingClientRect(); 472 | var selectedrect = selected.getBoundingClientRect(); 473 | // orientation 474 | h_elem = (selectedrect.left + selectedrect.width/2 - referencerect.width/2) /referencerect.width; 475 | v_elem = (selectedrect.top + selectedrect.height/2 - referencerect.height/2) /referencerect.height; 476 | } 477 | } 478 | 479 | var div = document.createElement("div") 480 | div.className = 'broadcast-preview'; 481 | div.style.cssText = 'position:fixed;top:0;right:0;z-index:50;box-shadow:10px 10px 5px rgba(0,0,0,.3);'; 482 | div.style.width = 0.33 * Reveal.getConfig().width * Reveal.getScale() + "px"; 483 | div.style.height = height / width * 0.33 * Reveal.getConfig().width * Reveal.getScale() + "px"; 484 | div.innerHTML = '' 485 | + ''; 486 | var reveal = document.querySelector('.reveal'); 487 | reveal.parentNode.insertBefore( div, reveal ); 488 | mediaPlayer = document.getElementById('broadcast-mediaplayer'); 489 | var preview = document.querySelector('.broadcast-preview'); 490 | 491 | if ( preview ) { 492 | // Bind mouse events 493 | preview.onmousedown = function () { 494 | _drag_init(this); 495 | return false; 496 | }; 497 | document.onmousemove = _move_elem; 498 | document.onmouseup = _destroy; 499 | // Bind touch events 500 | preview.ontouchstart = function () { 501 | _drag_init(this); 502 | return false; 503 | }; 504 | document.ontouchmove = _move_elem; 505 | document.ontouchend = _destroy; 506 | } 507 | 508 | window.addEventListener( 'resize', function( evt ) { 509 | // resize preview 510 | var preview = document.querySelector('.broadcast-preview'); 511 | preview.style.width = 0.33 * Reveal.getConfig().width * Reveal.getScale() + "px"; 512 | preview.style.height = height / width * 0.33 * Reveal.getConfig().width * Reveal.getScale() + "px"; 513 | var rect = preview.getBoundingClientRect(); 514 | // maintain preview orientation 515 | var presentation = document.querySelector('.reveal').getBoundingClientRect(); 516 | preview.style.left = h_elem * presentation.width - preview.getBoundingClientRect().width/2 + presentation.width/2 + "px"; 517 | preview.style.top = v_elem * presentation.height - preview.getBoundingClientRect().height/2 + presentation.height/2 + 'px'; 518 | }); 519 | 520 | mediaPlayer.addEventListener( 'loadeddata' , function( evt ) { 521 | width = mediaPlayer.videoWidth; 522 | height = mediaPlayer.videoHeight; 523 | console.log("Video resolution: " + width + "x" + height); 524 | var preview = document.querySelector('.broadcast-preview'); 525 | preview.style.width = 0.33 * Reveal.getConfig().width * Reveal.getScale() + "px"; 526 | preview.style.height = height / width * 0.33 * Reveal.getConfig().width * Reveal.getScale() + "px"; 527 | }); 528 | 529 | } 530 | 531 | this.start = prestart; 532 | this.connect = connect; 533 | return this; 534 | })(); 535 | 536 | 537 | -------------------------------------------------------------------------------- /audio-slideshow/audio-slideshow.js: -------------------------------------------------------------------------------- 1 | /***************************************************************** 2 | ** Author: Asvin Goel, goel@telematique.eu 3 | ** 4 | ** Audio slideshow is a plugin for reveal.js allowing to 5 | ** automatically play audio files for a slide deck. After an audio 6 | ** file has completed playing the next slide or fragment is 7 | ** automatically shown and the respective audio file is played. 8 | ** If no audio file is available, a blank audio file with default 9 | ** duration is played instead. 10 | ** 11 | ** Version: 0.6.1 12 | ** 13 | ** License: MIT license (see LICENSE.md) 14 | ** 15 | ******************************************************************/ 16 | 17 | var RevealAudioSlideshow = window.RevealAudioSlideshow || (function(){ 18 | 19 | // default parameters 20 | var prefix = "audio/"; 21 | var suffix = ".ogg"; 22 | var textToSpeechURL = null; // no text to speech converter 23 | // var textToSpeechURL = "http://api.voicerss.org/?key=[YOUR_KEY]&hl=en-gb&c=ogg&src="; // the text to speech converter 24 | var defaultNotes = false; // use slide notes as default for the text to speech converter 25 | var defaultText = false; // use slide text as default for the text to speech converter 26 | var defaultDuration = 5; // value in seconds 27 | var defaultAudios = true; // try to obtain audio for slide and fragment numbers 28 | var advance = 0; // advance to next slide after given time in milliseconds after audio has played, use negative value to not advance 29 | var autoplay = false; // automatically start slideshow 30 | var playerOpacity = .05; // opacity when the mouse is far from to the audioplayer 31 | var startAtFragment = false; // when moving to a slide, start at the current fragment or at the start of the slide 32 | var playerStyle = "position: fixed; bottom: 4px; left: 25%; width: 50%; height:75px; z-index: 33;"; // style used for container of audio controls 33 | // ------------------ 34 | 35 | var silence; 36 | var currentAudio = null; 37 | var previousAudio = null; 38 | var timer = null; 39 | 40 | Reveal.addEventListener( 'fragmentshown', function( event ) { 41 | if ( timer ) { clearTimeout( timer ); timer = null; } 42 | //console.debug( "fragmentshown "); 43 | selectAudio(); 44 | } ); 45 | 46 | Reveal.addEventListener( 'fragmenthidden', function( event ) { 47 | if ( timer ) { clearTimeout( timer ); timer = null; } 48 | //console.debug( "fragmenthidden "); 49 | selectAudio(); 50 | } ); 51 | 52 | Reveal.addEventListener( 'ready', function( event ) { 53 | setup(); 54 | //console.debug( "ready "); 55 | selectAudio(); 56 | document.dispatchEvent( new CustomEvent('stopplayback') ); 57 | 58 | } ); 59 | 60 | Reveal.addEventListener( 'slidechanged', function( event ) { 61 | if ( timer ) { clearTimeout( timer ); timer = null; } 62 | //console.debug( "slidechanged "); 63 | var indices = Reveal.getIndices(); 64 | if ( !startAtFragment && typeof indices.f !== 'undefined' && indices.f >= 0) { 65 | // hide fragments when slide is shown 66 | Reveal.slide(indices.h, indices.v, -1); 67 | } 68 | 69 | selectAudio(); 70 | } ); 71 | 72 | Reveal.addEventListener( 'paused', function( event ) { 73 | if ( timer ) { clearTimeout( timer ); timer = null; } 74 | currentAudio.pause(); 75 | } ); 76 | 77 | Reveal.addEventListener( 'resumed', function( event ) { 78 | if ( timer ) { clearTimeout( timer ); timer = null; } 79 | } ); 80 | 81 | Reveal.addEventListener( 'overviewshown', function( event ) { 82 | if ( timer ) { clearTimeout( timer ); timer = null; } 83 | currentAudio.pause(); 84 | document.querySelector(".audio-controls").style.visibility = "hidden"; 85 | } ); 86 | 87 | Reveal.addEventListener( 'overviewhidden', function( event ) { 88 | if ( timer ) { clearTimeout( timer ); timer = null; } 89 | document.querySelector(".audio-controls").style.visibility = "visible"; 90 | } ); 91 | 92 | function selectAudio( previousAudio ) { 93 | if ( currentAudio ) { 94 | currentAudio.pause(); 95 | currentAudio.style.display = "none"; 96 | } 97 | var indices = Reveal.getIndices(); 98 | var id = "audioplayer-" + indices.h + '.' + indices.v; 99 | if ( indices.f != undefined && indices.f >= 0 ) id = id + '.' + indices.f; 100 | currentAudio = document.getElementById( id ); 101 | if ( currentAudio ) { 102 | currentAudio.style.display = "block"; 103 | if ( previousAudio ) { 104 | if ( currentAudio.id != previousAudio.id ) { 105 | currentAudio.volume = previousAudio.volume; 106 | currentAudio.muted = previousAudio.muted; 107 | //console.debug( "Play " + currentAudio.id); 108 | currentAudio.play(); 109 | } 110 | } 111 | else if ( autoplay ) { 112 | currentAudio.play(); 113 | } 114 | 115 | } 116 | } 117 | 118 | 119 | function setup() { 120 | // deprecated parameters 121 | if ( Reveal.getConfig().audioPrefix ) { 122 | prefix = Reveal.getConfig().audioPrefix; 123 | console.warn('Setting parameter "audioPrefix" is deprecated!'); 124 | } 125 | if ( Reveal.getConfig().audioSuffix ) { 126 | suffix = Reveal.getConfig().audioSuffix; 127 | console.warn('Setting parameter "audioSuffix" is deprecated!'); 128 | } 129 | if ( Reveal.getConfig().audioTextToSpeechURL ) { 130 | textToSpeechURL = Reveal.getConfig().audioTextToSpeechURL; 131 | console.warn('Setting parameter "audioTextToSpeechURL" is deprecated!'); 132 | } 133 | if ( Reveal.getConfig().audioDefaultDuration ) { 134 | defaultDuration = Reveal.getConfig().audioDefaultDuration; 135 | console.warn('Setting parameter "audioDefaultDuration" is deprecated!'); 136 | } 137 | if ( Reveal.getConfig().audioAutoplay ) { 138 | autoplay = Reveal.getConfig().audioAutoplay; 139 | console.warn('Setting parameter "audioAutoplay" is deprecated!'); 140 | } 141 | if ( Reveal.getConfig().audioPlayerOpacity ) { 142 | playerOpacity = Reveal.getConfig().audioPlayerOpacity; 143 | console.warn('Setting parameter "audioPlayerOpacity" is deprecated!'); 144 | } 145 | 146 | // set parameters 147 | var config = Reveal.getConfig().audio; 148 | if ( config ) { 149 | if ( config.prefix != null ) prefix = config.prefix; 150 | if ( config.suffix != null ) suffix = config.suffix; 151 | if ( config.textToSpeechURL != null ) textToSpeechURL = config.textToSpeechURL; 152 | if ( config.defaultNotes != null ) defaultNotes = config.defaultNotes; 153 | if ( config.defaultText != null ) defaultText = config.defaultText; 154 | if ( config.defaultDuration != null ) defaultDuration = config.defaultDuration; 155 | if ( config.defaultAudios != null ) defaultAudios = config.defaultAudios; 156 | if ( config.advance != null ) advance = config.advance; 157 | if ( config.autoplay != null ) autoplay = config.autoplay; 158 | if ( config.playerOpacity != null ) playerOpacity = config.playerOpacity; 159 | if ( config.playerStyle != null ) playerStyle = config.playerStyle; 160 | } 161 | 162 | if ( 'ontouchstart' in window || navigator.msMaxTouchPoints ) { 163 | opacity = 1; 164 | } 165 | if ( Reveal.getConfig().audioStartAtFragment ) startAtFragment = Reveal.getConfig().audioStartAtFragment; 166 | 167 | // set style so that audio controls are shown on hover 168 | var css='.audio-controls>audio { opacity:' + playerOpacity + ';} .audio-controls:hover>audio { opacity:1;}'; 169 | style=document.createElement( 'style' ); 170 | if ( style.styleSheet ) { 171 | style.styleSheet.cssText=css; 172 | } 173 | else { 174 | style.appendChild( document.createTextNode( css ) ); 175 | } 176 | document.getElementsByTagName( 'head' )[0].appendChild( style ); 177 | 178 | silence = new SilentAudio( defaultDuration ); // create the wave file 179 | 180 | var divElement = document.createElement( 'div' ); 181 | divElement.className = "audio-controls"; 182 | divElement.setAttribute( 'style', playerStyle ); 183 | document.querySelector( ".reveal" ).appendChild( divElement ); 184 | 185 | // create audio players for all slides 186 | var horizontalSlides = document.querySelectorAll( '.reveal .slides>section' ); 187 | for( var h = 0, len1 = horizontalSlides.length; h < len1; h++ ) { 188 | var verticalSlides = horizontalSlides[ h ].querySelectorAll( 'section' ); 189 | if ( !verticalSlides.length ) { 190 | setupAllAudioElements( divElement, h, 0, horizontalSlides[ h ] ); 191 | } 192 | else { 193 | for( var v = 0, len2 = verticalSlides.length; v < len2; v++ ) { 194 | setupAllAudioElements( divElement, h, v, verticalSlides[ v ] ); 195 | } 196 | } 197 | } 198 | } 199 | function getText( textContainer ) { 200 | var elements = textContainer.querySelectorAll( '[data-audio-text]' ) ; 201 | for( var i = 0, len = elements.length; i < len; i++ ) { 202 | // replace all elements with data-audio-text by specified text 203 | textContainer.innerHTML = textContainer.innerHTML.replace(elements[i].outerHTML,elements[i].getAttribute('data-audio-text')); 204 | } 205 | return textContainer.textContent.trim().replace(/\s+/g, ' '); 206 | } 207 | 208 | function setupAllAudioElements( container, h, v, slide ) { 209 | var textContainer = document.createElement( 'div' ); 210 | var text = null; 211 | if ( !slide.hasAttribute( 'data-audio-src' ) ) { 212 | // determine text for TTS 213 | if ( slide.hasAttribute( 'data-audio-text' ) ) { 214 | text = slide.getAttribute( 'data-audio-text' ); 215 | } 216 | else if ( defaultNotes && Reveal.getSlideNotes( slide ) ) { 217 | // defaultNotes 218 | var div = document.createElement("div"); 219 | div.innerHTML = Reveal.getSlideNotes( slide ); 220 | text = div.textContent || ''; 221 | } 222 | else if ( defaultText ) { 223 | textContainer.innerHTML = slide.innerHTML; 224 | // remove fragments 225 | var fragments = textContainer.querySelectorAll( '.fragment' ) ; 226 | for( var f = 0, len = fragments.length; f < len; f++ ) { 227 | textContainer.innerHTML = textContainer.innerHTML.replace(fragments[f].outerHTML,''); 228 | } 229 | text = getText( textContainer); 230 | } 231 | // alert( h + '.' + v + ": " + text ); 232 | // console.log( h + '.' + v + ": " + text ); 233 | } 234 | setupAudioElement( container, h + '.' + v, slide.getAttribute( 'data-audio-src' ), text, slide.querySelector( ':not(.fragment) > video[data-audio-controls]' ) ); 235 | var i = 0; 236 | var fragments; 237 | while ( (fragments = slide.querySelectorAll( '.fragment[data-fragment-index="' + i +'"]' )).length > 0 ) { 238 | var audio = null; 239 | var video = null; 240 | var text = ''; 241 | for( var f = 0, len = fragments.length; f < len; f++ ) { 242 | if ( !audio ) audio = fragments[ f ].getAttribute( 'data-audio-src' ); 243 | if ( !video ) video = fragments[ f ].querySelector( 'video[data-audio-controls]' ); 244 | // determine text for TTS 245 | if ( fragments[ f ].hasAttribute( 'data-audio-text' ) ) { 246 | text += fragments[ f ].getAttribute( 'data-audio-text' ) + ' '; 247 | } 248 | else if ( defaultText ) { 249 | textContainer.innerHTML = fragments[ f ].textContent; 250 | text += getText( textContainer ); 251 | } 252 | } 253 | //console.log( h + '.' + v + '.' + i + ": >" + text +"<") 254 | setupAudioElement( container, h + '.' + v + '.' + i, audio, text, video ); 255 | i++; 256 | } 257 | } 258 | 259 | function linkVideoToAudioControls( audioElement, videoElement ) { 260 | audioElement.addEventListener( 'playing', function( event ) { 261 | videoElement.currentTime = audioElement.currentTime; 262 | } ); 263 | audioElement.addEventListener( 'play', function( event ) { 264 | videoElement.currentTime = audioElement.currentTime; 265 | if ( videoElement.paused ) videoElement.play(); 266 | } ); 267 | audioElement.addEventListener( 'pause', function( event ) { 268 | videoElement.currentTime = audioElement.currentTime; 269 | if ( !videoElement.paused ) videoElement.pause(); 270 | } ); 271 | audioElement.addEventListener( 'volumechange', function( event ) { 272 | videoElement.volume = audioElement.volume; 273 | videoElement.muted = audioElement.muted; 274 | } ); 275 | audioElement.addEventListener( 'seeked', function( event ) { 276 | videoElement.currentTime = audioElement.currentTime; 277 | } ); 278 | 279 | // add silent audio to video to be used as fallback 280 | var audioSource = audioElement.querySelector('source[data-audio-silent]'); 281 | if ( audioSource ) audioElement.removeChild( audioSource ); 282 | audioSource = document.createElement( 'source' ); 283 | var videoSilence = new SilentAudio( Math.round(videoElement.duration + .5) ); // create the wave file 284 | audioSource.src= videoSilence.dataURI; 285 | audioSource.setAttribute("data-audio-silent", videoElement.duration); 286 | audioElement.appendChild(audioSource, audioElement.firstChild); 287 | } 288 | 289 | function setupFallbackAudio( audioElement, text, videoElement ) { 290 | // default file cannot be read 291 | if ( textToSpeechURL != null && text != null && text != "" ) { 292 | var audioSource = document.createElement( 'source' ); 293 | audioSource.src = textToSpeechURL + encodeURIComponent(text); 294 | audioSource.setAttribute('data-tts',audioElement.id.split( '-' ).pop()); 295 | audioElement.appendChild(audioSource, audioElement.firstChild); 296 | } 297 | else { 298 | if ( !audioElement.querySelector('source[data-audio-silent]') ) { 299 | // create silent source if not yet existent 300 | var audioSource = document.createElement( 'source' ); 301 | audioSource.src = silence.dataURI; 302 | audioSource.setAttribute("data-audio-silent", defaultDuration); 303 | audioElement.appendChild(audioSource, audioElement.firstChild); 304 | } 305 | } 306 | } 307 | 308 | function setupAudioElement( container, indices, audioFile, text, videoElement ) { 309 | var audioElement = document.createElement( 'audio' ); 310 | audioElement.setAttribute( 'style', "position: relative; top: 20px; left: 10%; width: 80%;" ); 311 | audioElement.id = "audioplayer-" + indices; 312 | audioElement.style.display = "none"; 313 | audioElement.setAttribute( 'controls', '' ); 314 | audioElement.setAttribute( 'preload', 'none' ); 315 | 316 | if ( videoElement ) { 317 | // connect play, pause, volumechange, mute, timeupdate events to video 318 | if ( videoElement.duration ) { 319 | linkVideoToAudioControls( audioElement, videoElement ); 320 | } 321 | else { 322 | videoElement.onloadedmetadata = function() { 323 | linkVideoToAudioControls( audioElement, videoElement ); 324 | }; 325 | } 326 | } 327 | audioElement.addEventListener( 'ended', function( event ) { 328 | if ( typeof Recorder == 'undefined' || !Recorder.isRecording ) { 329 | // determine whether and when slideshow advances with next slide 330 | var advanceNow = advance; 331 | var slide = Reveal.getCurrentSlide(); 332 | // check current fragment 333 | var indices = Reveal.getIndices(); 334 | if ( typeof indices.f !== 'undefined' && indices.f >= 0) { 335 | var fragment = slide.querySelector( '.fragment[data-fragment-index="' + indices.f + '"][data-audio-advance]' ) ; 336 | if ( fragment ) { 337 | advanceNow = fragment.getAttribute( 'data-audio-advance' ); 338 | } 339 | } 340 | else if ( slide.hasAttribute( 'data-audio-advance' ) ) { 341 | advanceNow = slide.getAttribute( 'data-audio-advance' ); 342 | } 343 | // advance immediately or set a timer - or do nothing 344 | if ( advance == "true" || advanceNow == 0 ) { 345 | var previousAudio = currentAudio; 346 | Reveal.next(); 347 | selectAudio( previousAudio ); 348 | } 349 | else if ( advanceNow > 0 ) { 350 | timer = setTimeout( function() { 351 | var previousAudio = currentAudio; 352 | Reveal.next(); 353 | selectAudio( previousAudio ); 354 | timer = null; 355 | }, advanceNow ); 356 | } 357 | } 358 | } ); 359 | audioElement.addEventListener( 'play', function( event ) { 360 | var evt = new CustomEvent('startplayback'); 361 | evt.timestamp = 1000 * audioElement.currentTime; 362 | document.dispatchEvent( evt ); 363 | 364 | if ( timer ) { clearTimeout( timer ); timer = null; } 365 | // preload next audio element so that it is available after slide change 366 | var indices = Reveal.getIndices(); 367 | var nextId = "audioplayer-" + indices.h + '.' + indices.v; 368 | if ( indices.f != undefined && indices.f >= 0 ) { 369 | nextId = nextId + '.' + (indices.f + 1); 370 | } 371 | else { 372 | nextId = nextId + '.0'; 373 | } 374 | var nextAudio = document.getElementById( nextId ); 375 | if ( !nextAudio ) { 376 | nextId = "audioplayer-" + indices.h + '.' + (indices.v+1); 377 | nextAudio = document.getElementById( nextId ); 378 | if ( !nextAudio ) { 379 | nextId = "audioplayer-" + (indices.h+1) + '.0'; 380 | nextAudio = document.getElementById( nextId ); 381 | } 382 | } 383 | if ( nextAudio ) { 384 | //console.debug( "Preload: " + nextAudio.id ); 385 | nextAudio.load(); 386 | } 387 | } ); 388 | audioElement.addEventListener( 'pause', function( event ) { 389 | if ( timer ) { clearTimeout( timer ); timer = null; } 390 | document.dispatchEvent( new CustomEvent('stopplayback') ); 391 | } ); 392 | audioElement.addEventListener( 'seeked', function( event ) { 393 | var evt = new CustomEvent('seekplayback'); 394 | evt.timestamp = 1000 * audioElement.currentTime; 395 | document.dispatchEvent( evt ); 396 | if ( timer ) { clearTimeout( timer ); timer = null; } 397 | } ); 398 | 399 | if ( audioFile != null ) { 400 | // Support comma separated lists of audio sources 401 | audioFile.split( ',' ).forEach( function( source ) { 402 | var audioSource = document.createElement( 'source' ); 403 | audioSource.src = source; 404 | audioElement.insertBefore(audioSource, audioElement.firstChild); 405 | } ); 406 | } 407 | else if ( defaultAudios ) { 408 | var audioExists = false; 409 | try { 410 | // check if audio file exists 411 | var xhr = new XMLHttpRequest(); 412 | xhr.open('HEAD', prefix + indices + suffix, true); 413 | xhr.onload = function() { 414 | if (xhr.readyState === 4 && xhr.status >= 200 && xhr.status < 300) { 415 | var audioSource = document.createElement( 'source' ); 416 | audioSource.src = prefix + indices + suffix; 417 | audioElement.insertBefore(audioSource, audioElement.firstChild); 418 | audioExists = true; 419 | } 420 | else { 421 | setupFallbackAudio( audioElement, text, videoElement ); 422 | } 423 | } 424 | xhr.send(null); 425 | } catch( error ) { 426 | //console.log("Error checking audio" + audioExists); 427 | // fallback if checking of audio file fails (e.g. when running the slideshow locally) 428 | var audioSource = document.createElement( 'source' ); 429 | audioSource.src = prefix + indices + suffix; 430 | audioElement.insertBefore(audioSource, audioElement.firstChild); 431 | setupFallbackAudio( audioElement, text, videoElement ); 432 | } 433 | } 434 | if ( audioFile != null || defaultDuration > 0 ) { 435 | container.appendChild( audioElement ); 436 | } 437 | } 438 | 439 | 440 | 441 | })(); 442 | 443 | /***************************************************************** 444 | ** Create SilentAudio 445 | ** based on: RIFFWAVE.js v0.03 446 | ** http://www.codebase.es/riffwave/riffwave.js 447 | ** 448 | ** Usage: 449 | ** silence = new SilentAudio( 10 ); // create 10 seconds wave file 450 | ** 451 | ******************************************************************/ 452 | 453 | var FastBase64={chars:"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=",encLookup:[],Init:function(){for(var e=0;4096>e;e++)this.encLookup[e]=this.chars[e>>6]+this.chars[63&e]},Encode:function(e){for(var h=e.length,a="",t=0;h>2;)n=e[t]<<16|e[t+1]<<8|e[t+2],a+=this.encLookup[n>>12]+this.encLookup[4095&n],h-=3,t+=3;if(h>0){var s=(252&e[t])>>2,i=(3&e[t])<<4;if(h>1&&(i|=(240&e[++t])>>4),a+=this.chars[s],a+=this.chars[i],2==h){var r=(15&e[t++])<<2;r|=(192&e[t])>>6,a+=this.chars[r]}1==h&&(a+="="),a+="="}return a}};FastBase64.Init();var SilentAudio=function(e){function h(e){return[255&e,e>>8&255,e>>16&255,e>>24&255]}function a(e){return[255&e,e>>8&255]}function t(e){for(var h=[],a=0,t=e.length,s=0;t>s;s++)h[a++]=255&e[s],h[a++]=e[s]>>8&255;return h}this.data=[],this.wav=[],this.dataURI="",this.header={chunkId:[82,73,70,70],chunkSize:0,format:[87,65,86,69],subChunk1Id:[102,109,116,32],subChunk1Size:16,audioFormat:1,numChannels:1,sampleRate:8e3,byteRate:0,blockAlign:0,bitsPerSample:8,subChunk2Id:[100,97,116,97],subChunk2Size:0},this.Make=function(e){for(var s=0;s>3,this.header.byteRate=this.header.blockAlign*this.sampleRate,this.header.subChunk2Size=this.data.length*(this.header.bitsPerSample>>3),this.header.chunkSize=36+this.header.subChunk2Size,this.wav=this.header.chunkId.concat(h(this.header.chunkSize),this.header.format,this.header.subChunk1Id,h(this.header.subChunk1Size),a(this.header.audioFormat),a(this.header.numChannels),h(this.header.sampleRate),h(this.header.byteRate),a(this.header.blockAlign),a(this.header.bitsPerSample),this.header.subChunk2Id,h(this.header.subChunk2Size),16==this.header.bitsPerSample?t(this.data):this.data),this.dataURI="data:audio/wav;base64,"+FastBase64.Encode(this.wav)},this.Make(e)}; 454 | -------------------------------------------------------------------------------- /broadcast/bCrypt.js: -------------------------------------------------------------------------------- 1 | /* MIT License 2 | 3 | Copyright (c) 2016 Nevins Bartolomeo 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. */ 22 | 23 | 24 | function keyState() { 25 | this.P = P_ORIG.slice(0); 26 | this.S = S_ORIG.slice(0); 27 | this.offp = 0; 28 | } 29 | 30 | var GENSALT_DEFAULT_LOG2_ROUNDS = 10; 31 | var BCRYPT_SALT_LEN = 16; 32 | var BLOWFISH_NUM_ROUNDS = 16; 33 | var MAX_EXECUTION_TIME = 100; 34 | var P_ORIG = [0x243f6a88, 0x85a308d3, 0x13198a2e, 0x03707344, 0xa4093822, 35 | 0x299f31d0, 0x082efa98, 0xec4e6c89, 0x452821e6, 0x38d01377, 36 | 0xbe5466cf, 0x34e90c6c, 0xc0ac29b7, 0xc97c50dd, 0x3f84d5b5, 37 | 0xb5470917, 0x9216d5d9, 0x8979fb1b 38 | ]; 39 | var S_ORIG = [0xd1310ba6, 0x98dfb5ac, 0x2ffd72db, 0xd01adfb7, 0xb8e1afed, 40 | 0x6a267e96, 0xba7c9045, 0xf12c7f99, 0x24a19947, 0xb3916cf7, 41 | 0x0801f2e2, 0x858efc16, 0x636920d8, 0x71574e69, 0xa458fea3, 42 | 0xf4933d7e, 0x0d95748f, 0x728eb658, 0x718bcd58, 0x82154aee, 43 | 0x7b54a41d, 0xc25a59b5, 0x9c30d539, 0x2af26013, 0xc5d1b023, 44 | 0x286085f0, 0xca417918, 0xb8db38ef, 0x8e79dcb0, 0x603a180e, 45 | 0x6c9e0e8b, 0xb01e8a3e, 0xd71577c1, 0xbd314b27, 0x78af2fda, 46 | 0x55605c60, 0xe65525f3, 0xaa55ab94, 0x57489862, 0x63e81440, 47 | 0x55ca396a, 0x2aab10b6, 0xb4cc5c34, 0x1141e8ce, 0xa15486af, 48 | 0x7c72e993, 0xb3ee1411, 0x636fbc2a, 0x2ba9c55d, 0x741831f6, 49 | 0xce5c3e16, 0x9b87931e, 0xafd6ba33, 0x6c24cf5c, 0x7a325381, 50 | 0x28958677, 0x3b8f4898, 0x6b4bb9af, 0xc4bfe81b, 0x66282193, 51 | 0x61d809cc, 0xfb21a991, 0x487cac60, 0x5dec8032, 0xef845d5d, 52 | 0xe98575b1, 0xdc262302, 0xeb651b88, 0x23893e81, 0xd396acc5, 53 | 0x0f6d6ff3, 0x83f44239, 0x2e0b4482, 0xa4842004, 0x69c8f04a, 54 | 0x9e1f9b5e, 0x21c66842, 0xf6e96c9a, 0x670c9c61, 0xabd388f0, 55 | 0x6a51a0d2, 0xd8542f68, 0x960fa728, 0xab5133a3, 0x6eef0b6c, 56 | 0x137a3be4, 0xba3bf050, 0x7efb2a98, 0xa1f1651d, 0x39af0176, 57 | 0x66ca593e, 0x82430e88, 0x8cee8619, 0x456f9fb4, 0x7d84a5c3, 58 | 0x3b8b5ebe, 0xe06f75d8, 0x85c12073, 0x401a449f, 0x56c16aa6, 59 | 0x4ed3aa62, 0x363f7706, 0x1bfedf72, 0x429b023d, 0x37d0d724, 60 | 0xd00a1248, 0xdb0fead3, 0x49f1c09b, 0x075372c9, 0x80991b7b, 61 | 0x25d479d8, 0xf6e8def7, 0xe3fe501a, 0xb6794c3b, 0x976ce0bd, 62 | 0x04c006ba, 0xc1a94fb6, 0x409f60c4, 0x5e5c9ec2, 0x196a2463, 63 | 0x68fb6faf, 0x3e6c53b5, 0x1339b2eb, 0x3b52ec6f, 0x6dfc511f, 64 | 0x9b30952c, 0xcc814544, 0xaf5ebd09, 0xbee3d004, 0xde334afd, 65 | 0x660f2807, 0x192e4bb3, 0xc0cba857, 0x45c8740f, 0xd20b5f39, 66 | 0xb9d3fbdb, 0x5579c0bd, 0x1a60320a, 0xd6a100c6, 0x402c7279, 67 | 0x679f25fe, 0xfb1fa3cc, 0x8ea5e9f8, 0xdb3222f8, 0x3c7516df, 68 | 0xfd616b15, 0x2f501ec8, 0xad0552ab, 0x323db5fa, 0xfd238760, 69 | 0x53317b48, 0x3e00df82, 0x9e5c57bb, 0xca6f8ca0, 0x1a87562e, 70 | 0xdf1769db, 0xd542a8f6, 0x287effc3, 0xac6732c6, 0x8c4f5573, 71 | 0x695b27b0, 0xbbca58c8, 0xe1ffa35d, 0xb8f011a0, 0x10fa3d98, 72 | 0xfd2183b8, 0x4afcb56c, 0x2dd1d35b, 0x9a53e479, 0xb6f84565, 73 | 0xd28e49bc, 0x4bfb9790, 0xe1ddf2da, 0xa4cb7e33, 0x62fb1341, 74 | 0xcee4c6e8, 0xef20cada, 0x36774c01, 0xd07e9efe, 0x2bf11fb4, 75 | 0x95dbda4d, 0xae909198, 0xeaad8e71, 0x6b93d5a0, 0xd08ed1d0, 76 | 0xafc725e0, 0x8e3c5b2f, 0x8e7594b7, 0x8ff6e2fb, 0xf2122b64, 77 | 0x8888b812, 0x900df01c, 0x4fad5ea0, 0x688fc31c, 0xd1cff191, 78 | 0xb3a8c1ad, 0x2f2f2218, 0xbe0e1777, 0xea752dfe, 0x8b021fa1, 79 | 0xe5a0cc0f, 0xb56f74e8, 0x18acf3d6, 0xce89e299, 0xb4a84fe0, 80 | 0xfd13e0b7, 0x7cc43b81, 0xd2ada8d9, 0x165fa266, 0x80957705, 81 | 0x93cc7314, 0x211a1477, 0xe6ad2065, 0x77b5fa86, 0xc75442f5, 82 | 0xfb9d35cf, 0xebcdaf0c, 0x7b3e89a0, 0xd6411bd3, 0xae1e7e49, 83 | 0x00250e2d, 0x2071b35e, 0x226800bb, 0x57b8e0af, 0x2464369b, 84 | 0xf009b91e, 0x5563911d, 0x59dfa6aa, 0x78c14389, 0xd95a537f, 85 | 0x207d5ba2, 0x02e5b9c5, 0x83260376, 0x6295cfa9, 0x11c81968, 86 | 0x4e734a41, 0xb3472dca, 0x7b14a94a, 0x1b510052, 0x9a532915, 87 | 0xd60f573f, 0xbc9bc6e4, 0x2b60a476, 0x81e67400, 0x08ba6fb5, 88 | 0x571be91f, 0xf296ec6b, 0x2a0dd915, 0xb6636521, 0xe7b9f9b6, 89 | 0xff34052e, 0xc5855664, 0x53b02d5d, 0xa99f8fa1, 0x08ba4799, 90 | 0x6e85076a, 0x4b7a70e9, 0xb5b32944, 0xdb75092e, 0xc4192623, 91 | 0xad6ea6b0, 0x49a7df7d, 0x9cee60b8, 0x8fedb266, 0xecaa8c71, 92 | 0x699a17ff, 0x5664526c, 0xc2b19ee1, 0x193602a5, 0x75094c29, 93 | 0xa0591340, 0xe4183a3e, 0x3f54989a, 0x5b429d65, 0x6b8fe4d6, 94 | 0x99f73fd6, 0xa1d29c07, 0xefe830f5, 0x4d2d38e6, 0xf0255dc1, 95 | 0x4cdd2086, 0x8470eb26, 0x6382e9c6, 0x021ecc5e, 0x09686b3f, 96 | 0x3ebaefc9, 0x3c971814, 0x6b6a70a1, 0x687f3584, 0x52a0e286, 97 | 0xb79c5305, 0xaa500737, 0x3e07841c, 0x7fdeae5c, 0x8e7d44ec, 98 | 0x5716f2b8, 0xb03ada37, 0xf0500c0d, 0xf01c1f04, 0x0200b3ff, 99 | 0xae0cf51a, 0x3cb574b2, 0x25837a58, 0xdc0921bd, 0xd19113f9, 100 | 0x7ca92ff6, 0x94324773, 0x22f54701, 0x3ae5e581, 0x37c2dadc, 101 | 0xc8b57634, 0x9af3dda7, 0xa9446146, 0x0fd0030e, 0xecc8c73e, 102 | 0xa4751e41, 0xe238cd99, 0x3bea0e2f, 0x3280bba1, 0x183eb331, 103 | 0x4e548b38, 0x4f6db908, 0x6f420d03, 0xf60a04bf, 0x2cb81290, 104 | 0x24977c79, 0x5679b072, 0xbcaf89af, 0xde9a771f, 0xd9930810, 105 | 0xb38bae12, 0xdccf3f2e, 0x5512721f, 0x2e6b7124, 0x501adde6, 106 | 0x9f84cd87, 0x7a584718, 0x7408da17, 0xbc9f9abc, 0xe94b7d8c, 107 | 0xec7aec3a, 0xdb851dfa, 0x63094366, 0xc464c3d2, 0xef1c1847, 108 | 0x3215d908, 0xdd433b37, 0x24c2ba16, 0x12a14d43, 0x2a65c451, 109 | 0x50940002, 0x133ae4dd, 0x71dff89e, 0x10314e55, 0x81ac77d6, 110 | 0x5f11199b, 0x043556f1, 0xd7a3c76b, 0x3c11183b, 0x5924a509, 111 | 0xf28fe6ed, 0x97f1fbfa, 0x9ebabf2c, 0x1e153c6e, 0x86e34570, 112 | 0xeae96fb1, 0x860e5e0a, 0x5a3e2ab3, 0x771fe71c, 0x4e3d06fa, 113 | 0x2965dcb9, 0x99e71d0f, 0x803e89d6, 0x5266c825, 0x2e4cc978, 114 | 0x9c10b36a, 0xc6150eba, 0x94e2ea78, 0xa5fc3c53, 0x1e0a2df4, 115 | 0xf2f74ea7, 0x361d2b3d, 0x1939260f, 0x19c27960, 0x5223a708, 116 | 0xf71312b6, 0xebadfe6e, 0xeac31f66, 0xe3bc4595, 0xa67bc883, 117 | 0xb17f37d1, 0x018cff28, 0xc332ddef, 0xbe6c5aa5, 0x65582185, 118 | 0x68ab9802, 0xeecea50f, 0xdb2f953b, 0x2aef7dad, 0x5b6e2f84, 119 | 0x1521b628, 0x29076170, 0xecdd4775, 0x619f1510, 0x13cca830, 120 | 0xeb61bd96, 0x0334fe1e, 0xaa0363cf, 0xb5735c90, 0x4c70a239, 121 | 0xd59e9e0b, 0xcbaade14, 0xeecc86bc, 0x60622ca7, 0x9cab5cab, 122 | 0xb2f3846e, 0x648b1eaf, 0x19bdf0ca, 0xa02369b9, 0x655abb50, 123 | 0x40685a32, 0x3c2ab4b3, 0x319ee9d5, 0xc021b8f7, 0x9b540b19, 124 | 0x875fa099, 0x95f7997e, 0x623d7da8, 0xf837889a, 0x97e32d77, 125 | 0x11ed935f, 0x16681281, 0x0e358829, 0xc7e61fd6, 0x96dedfa1, 126 | 0x7858ba99, 0x57f584a5, 0x1b227263, 0x9b83c3ff, 0x1ac24696, 127 | 0xcdb30aeb, 0x532e3054, 0x8fd948e4, 0x6dbc3128, 0x58ebf2ef, 128 | 0x34c6ffea, 0xfe28ed61, 0xee7c3c73, 0x5d4a14d9, 0xe864b7e3, 129 | 0x42105d14, 0x203e13e0, 0x45eee2b6, 0xa3aaabea, 0xdb6c4f15, 130 | 0xfacb4fd0, 0xc742f442, 0xef6abbb5, 0x654f3b1d, 0x41cd2105, 131 | 0xd81e799e, 0x86854dc7, 0xe44b476a, 0x3d816250, 0xcf62a1f2, 132 | 0x5b8d2646, 0xfc8883a0, 0xc1c7b6a3, 0x7f1524c3, 0x69cb7492, 133 | 0x47848a0b, 0x5692b285, 0x095bbf00, 0xad19489d, 0x1462b174, 134 | 0x23820e00, 0x58428d2a, 0x0c55f5ea, 0x1dadf43e, 0x233f7061, 135 | 0x3372f092, 0x8d937e41, 0xd65fecf1, 0x6c223bdb, 0x7cde3759, 136 | 0xcbee7460, 0x4085f2a7, 0xce77326e, 0xa6078084, 0x19f8509e, 137 | 0xe8efd855, 0x61d99735, 0xa969a7aa, 0xc50c06c2, 0x5a04abfc, 138 | 0x800bcadc, 0x9e447a2e, 0xc3453484, 0xfdd56705, 0x0e1e9ec9, 139 | 0xdb73dbd3, 0x105588cd, 0x675fda79, 0xe3674340, 0xc5c43465, 140 | 0x713e38d8, 0x3d28f89e, 0xf16dff20, 0x153e21e7, 0x8fb03d4a, 141 | 0xe6e39f2b, 0xdb83adf7, 0xe93d5a68, 0x948140f7, 0xf64c261c, 142 | 0x94692934, 0x411520f7, 0x7602d4f7, 0xbcf46b2e, 0xd4a20068, 143 | 0xd4082471, 0x3320f46a, 0x43b7d4b7, 0x500061af, 0x1e39f62e, 144 | 0x97244546, 0x14214f74, 0xbf8b8840, 0x4d95fc1d, 0x96b591af, 145 | 0x70f4ddd3, 0x66a02f45, 0xbfbc09ec, 0x03bd9785, 0x7fac6dd0, 146 | 0x31cb8504, 0x96eb27b3, 0x55fd3941, 0xda2547e6, 0xabca0a9a, 147 | 0x28507825, 0x530429f4, 0x0a2c86da, 0xe9b66dfb, 0x68dc1462, 148 | 0xd7486900, 0x680ec0a4, 0x27a18dee, 0x4f3ffea2, 0xe887ad8c, 149 | 0xb58ce006, 0x7af4d6b6, 0xaace1e7c, 0xd3375fec, 0xce78a399, 150 | 0x406b2a42, 0x20fe9e35, 0xd9f385b9, 0xee39d7ab, 0x3b124e8b, 151 | 0x1dc9faf7, 0x4b6d1856, 0x26a36631, 0xeae397b2, 0x3a6efa74, 152 | 0xdd5b4332, 0x6841e7f7, 0xca7820fb, 0xfb0af54e, 0xd8feb397, 153 | 0x454056ac, 0xba489527, 0x55533a3a, 0x20838d87, 0xfe6ba9b7, 154 | 0xd096954b, 0x55a867bc, 0xa1159a58, 0xcca92963, 0x99e1db33, 155 | 0xa62a4a56, 0x3f3125f9, 0x5ef47e1c, 0x9029317c, 0xfdf8e802, 156 | 0x04272f70, 0x80bb155c, 0x05282ce3, 0x95c11548, 0xe4c66d22, 157 | 0x48c1133f, 0xc70f86dc, 0x07f9c9ee, 0x41041f0f, 0x404779a4, 158 | 0x5d886e17, 0x325f51eb, 0xd59bc0d1, 0xf2bcc18f, 0x41113564, 159 | 0x257b7834, 0x602a9c60, 0xdff8e8a3, 0x1f636c1b, 0x0e12b4c2, 160 | 0x02e1329e, 0xaf664fd1, 0xcad18115, 0x6b2395e0, 0x333e92e1, 161 | 0x3b240b62, 0xeebeb922, 0x85b2a20e, 0xe6ba0d99, 0xde720c8c, 162 | 0x2da2f728, 0xd0127845, 0x95b794fd, 0x647d0862, 0xe7ccf5f0, 163 | 0x5449a36f, 0x877d48fa, 0xc39dfd27, 0xf33e8d1e, 0x0a476341, 164 | 0x992eff74, 0x3a6f6eab, 0xf4f8fd37, 0xa812dc60, 0xa1ebddf8, 165 | 0x991be14c, 0xdb6e6b0d, 0xc67b5510, 0x6d672c37, 0x2765d43b, 166 | 0xdcd0e804, 0xf1290dc7, 0xcc00ffa3, 0xb5390f92, 0x690fed0b, 167 | 0x667b9ffb, 0xcedb7d9c, 0xa091cf0b, 0xd9155ea3, 0xbb132f88, 168 | 0x515bad24, 0x7b9479bf, 0x763bd6eb, 0x37392eb3, 0xcc115979, 169 | 0x8026e297, 0xf42e312d, 0x6842ada7, 0xc66a2b3b, 0x12754ccc, 170 | 0x782ef11c, 0x6a124237, 0xb79251e7, 0x06a1bbe6, 0x4bfb6350, 171 | 0x1a6b1018, 0x11caedfa, 0x3d25bdd8, 0xe2e1c3c9, 0x44421659, 172 | 0x0a121386, 0xd90cec6e, 0xd5abea2a, 0x64af674e, 0xda86a85f, 173 | 0xbebfe988, 0x64e4c3fe, 0x9dbc8057, 0xf0f7c086, 0x60787bf8, 174 | 0x6003604d, 0xd1fd8346, 0xf6381fb0, 0x7745ae04, 0xd736fccc, 175 | 0x83426b33, 0xf01eab71, 0xb0804187, 0x3c005e5f, 0x77a057be, 176 | 0xbde8ae24, 0x55464299, 0xbf582e61, 0x4e58f48f, 0xf2ddfda2, 177 | 0xf474ef38, 0x8789bdc2, 0x5366f9c3, 0xc8b38e74, 0xb475f255, 178 | 0x46fcd9b9, 0x7aeb2661, 0x8b1ddf84, 0x846a0e79, 0x915f95e2, 179 | 0x466e598e, 0x20b45770, 0x8cd55591, 0xc902de4c, 0xb90bace1, 180 | 0xbb8205d0, 0x11a86248, 0x7574a99e, 0xb77f19b6, 0xe0a9dc09, 181 | 0x662d09a1, 0xc4324633, 0xe85a1f02, 0x09f0be8c, 0x4a99a025, 182 | 0x1d6efe10, 0x1ab93d1d, 0x0ba5a4df, 0xa186f20f, 0x2868f169, 183 | 0xdcb7da83, 0x573906fe, 0xa1e2ce9b, 0x4fcd7f52, 0x50115e01, 184 | 0xa70683fa, 0xa002b5c4, 0x0de6d027, 0x9af88c27, 0x773f8641, 185 | 0xc3604c06, 0x61a806b5, 0xf0177a28, 0xc0f586e0, 0x006058aa, 186 | 0x30dc7d62, 0x11e69ed7, 0x2338ea63, 0x53c2dd94, 0xc2c21634, 187 | 0xbbcbee56, 0x90bcb6de, 0xebfc7da1, 0xce591d76, 0x6f05e409, 188 | 0x4b7c0188, 0x39720a3d, 0x7c927c24, 0x86e3725f, 0x724d9db9, 189 | 0x1ac15bb4, 0xd39eb8fc, 0xed545578, 0x08fca5b5, 0xd83d7cd3, 190 | 0x4dad0fc4, 0x1e50ef5e, 0xb161e6f8, 0xa28514d9, 0x6c51133c, 191 | 0x6fd5c7e7, 0x56e14ec4, 0x362abfce, 0xddc6c837, 0xd79a3234, 192 | 0x92638212, 0x670efa8e, 0x406000e0, 0x3a39ce37, 0xd3faf5cf, 193 | 0xabc27737, 0x5ac52d1b, 0x5cb0679e, 0x4fa33742, 0xd3822740, 194 | 0x99bc9bbe, 0xd5118e9d, 0xbf0f7315, 0xd62d1c7e, 0xc700c47b, 195 | 0xb78c1b6b, 0x21a19045, 0xb26eb1be, 0x6a366eb4, 0x5748ab2f, 196 | 0xbc946e79, 0xc6a376d2, 0x6549c2c8, 0x530ff8ee, 0x468dde7d, 197 | 0xd5730a1d, 0x4cd04dc6, 0x2939bbdb, 0xa9ba4650, 0xac9526e8, 198 | 0xbe5ee304, 0xa1fad5f0, 0x6a2d519a, 0x63ef8ce2, 0x9a86ee22, 199 | 0xc089c2b8, 0x43242ef6, 0xa51e03aa, 0x9cf2d0a4, 0x83c061ba, 200 | 0x9be96a4d, 0x8fe51550, 0xba645bd6, 0x2826a2f9, 0xa73a3ae1, 201 | 0x4ba99586, 0xef5562e9, 0xc72fefd3, 0xf752f7da, 0x3f046f69, 202 | 0x77fa0a59, 0x80e4a915, 0x87b08601, 0x9b09e6ad, 0x3b3ee593, 203 | 0xe990fd5a, 0x9e34d797, 0x2cf0b7d9, 0x022b8b51, 0x96d5ac3a, 204 | 0x017da67d, 0xd1cf3ed6, 0x7c7d2d28, 0x1f9f25cf, 0xadf2b89b, 205 | 0x5ad6b472, 0x5a88f54c, 0xe029ac71, 0xe019a5e6, 0x47b0acfd, 206 | 0xed93fa9b, 0xe8d3c48d, 0x283b57cc, 0xf8d56629, 0x79132e28, 207 | 0x785f0191, 0xed756055, 0xf7960e44, 0xe3d35e8c, 0x15056dd4, 208 | 0x88f46dba, 0x03a16125, 0x0564f0bd, 0xc3eb9e15, 0x3c9057a2, 209 | 0x97271aec, 0xa93a072a, 0x1b3f6d9b, 0x1e6321f5, 0xf59c66fb, 210 | 0x26dcf319, 0x7533d928, 0xb155fdf5, 0x03563482, 0x8aba3cbb, 211 | 0x28517711, 0xc20ad9f8, 0xabcc5167, 0xccad925f, 0x4de81751, 212 | 0x3830dc8e, 0x379d5862, 0x9320f991, 0xea7a90c2, 0xfb3e7bce, 213 | 0x5121ce64, 0x774fbe32, 0xa8b6e37e, 0xc3293d46, 0x48de5369, 214 | 0x6413e680, 0xa2ae0810, 0xdd6db224, 0x69852dfd, 0x09072166, 215 | 0xb39a460a, 0x6445c0dd, 0x586cdecf, 0x1c20c8ae, 0x5bbef7dd, 216 | 0x1b588d40, 0xccd2017f, 0x6bb4e3bb, 0xdda26a7e, 0x3a59ff45, 217 | 0x3e350a44, 0xbcb4cdd5, 0x72eacea8, 0xfa6484bb, 0x8d6612ae, 218 | 0xbf3c6f47, 0xd29be463, 0x542f5d9e, 0xaec2771b, 0xf64e6370, 219 | 0x740e0d8d, 0xe75b1357, 0xf8721671, 0xaf537d5d, 0x4040cb08, 220 | 0x4eb4e2cc, 0x34d2466a, 0x0115af84, 0xe1b00428, 0x95983a1d, 221 | 0x06b89fb4, 0xce6ea048, 0x6f3f3b82, 0x3520ab82, 0x011a1d4b, 222 | 0x277227f8, 0x611560b1, 0xe7933fdc, 0xbb3a792b, 0x344525bd, 223 | 0xa08839e1, 0x51ce794b, 0x2f32c9b7, 0xa01fbac9, 0xe01cc87e, 224 | 0xbcc7d1f6, 0xcf0111c3, 0xa1e8aac7, 0x1a908749, 0xd44fbd9a, 225 | 0xd0dadecb, 0xd50ada38, 0x0339c32a, 0xc6913667, 0x8df9317c, 226 | 0xe0b12b4f, 0xf79e59b7, 0x43f5bb3a, 0xf2d519ff, 0x27d9459c, 227 | 0xbf97222c, 0x15e6fc2a, 0x0f91fc71, 0x9b941525, 0xfae59361, 228 | 0xceb69ceb, 0xc2a86459, 0x12baa8d1, 0xb6c1075e, 0xe3056a0c, 229 | 0x10d25065, 0xcb03a442, 0xe0ec6e0e, 0x1698db3b, 0x4c98a0be, 230 | 0x3278e964, 0x9f1f9532, 0xe0d392df, 0xd3a0342b, 0x8971f21e, 231 | 0x1b0a7441, 0x4ba3348c, 0xc5be7120, 0xc37632d8, 0xdf359f8d, 232 | 0x9b992f2e, 0xe60b6f47, 0x0fe3f11d, 0xe54cda54, 0x1edad891, 233 | 0xce6279cf, 0xcd3e7e6f, 0x1618b166, 0xfd2c1d05, 0x848fd2c5, 234 | 0xf6fb2299, 0xf523f357, 0xa6327623, 0x93a83531, 0x56cccd02, 235 | 0xacf08162, 0x5a75ebb5, 0x6e163697, 0x88d273cc, 0xde966292, 236 | 0x81b949d0, 0x4c50901b, 0x71c65614, 0xe6c6c7bd, 0x327a140a, 237 | 0x45e1d006, 0xc3f27b9a, 0xc9aa53fd, 0x62a80f00, 0xbb25bfe2, 238 | 0x35bdd2f6, 0x71126905, 0xb2040222, 0xb6cbcf7c, 0xcd769c2b, 239 | 0x53113ec0, 0x1640e3d3, 0x38abbd60, 0x2547adf0, 0xba38209c, 240 | 0xf746ce76, 0x77afa1c5, 0x20756060, 0x85cbfe4e, 0x8ae88dd8, 241 | 0x7aaaf9b0, 0x4cf9aa7e, 0x1948c25c, 0x02fb8a8c, 0x01c36ae4, 242 | 0xd6ebe1f9, 0x90d4f869, 0xa65cdea0, 0x3f09252d, 0xc208e69f, 243 | 0xb74e6132, 0xce77e25b, 0x578fdfe3, 0x3ac372e6 244 | ]; 245 | var BF_CRYPT_CIPHERTEXT = [0x4f727068, 0x65616e42, 0x65686f6c, 0x64657253, 246 | 0x63727944, 0x6f756274 247 | ]; 248 | var BASE64_CODE = ['.', '/', 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 249 | 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 250 | 'W', 'X', 'Y', 'Z', 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 251 | 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 252 | 'w', 'x', 'y', 'z', '0', '1', '2', '3', '4', '5', '6', '7', '8', 253 | '9' 254 | ]; 255 | var INDEX_64 = [-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, 0, 1, 256 | 54, 55, 56, 57, 58, 59, 60, 61, 62, 63, -1, -1, -1, -1, -1, -1, -1, 257 | 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 258 | 21, 22, 23, 24, 25, 26, 27, -1, -1, -1, -1, -1, -1, 28, 29, 30, 31, 259 | 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 260 | 49, 50, 51, 52, 53, -1, -1, -1, -1, -1 261 | ]; 262 | 263 | function getByte(c) { 264 | var ret = 0; 265 | try { 266 | var b = c.charCodeAt(0); 267 | } catch (err) { 268 | b = c; 269 | } 270 | if (b > 127) { 271 | return -128 + (b % 128); 272 | } else { 273 | return b; 274 | } 275 | }; 276 | 277 | function encode_base64(d, len) { 278 | var off = 0; 279 | var rs = []; 280 | var c1; 281 | var c2; 282 | if (len <= 0 || len > d.length) 283 | throw "Invalid len"; 284 | while (off < len) { 285 | c1 = d[off++] & 0xff; 286 | rs.push(BASE64_CODE[(c1 >> 2) & 0x3f]); 287 | c1 = (c1 & 0x03) << 4; 288 | if (off >= len) { 289 | rs.push(BASE64_CODE[c1 & 0x3f]); 290 | break; 291 | } 292 | c2 = d[off++] & 0xff; 293 | c1 |= (c2 >> 4) & 0x0f; 294 | rs.push(BASE64_CODE[c1 & 0x3f]); 295 | c1 = (c2 & 0x0f) << 2; 296 | if (off >= len) { 297 | rs.push(BASE64_CODE[c1 & 0x3f]); 298 | break; 299 | } 300 | c2 = d[off++] & 0xff; 301 | c1 |= (c2 >> 6) & 0x03; 302 | rs.push(BASE64_CODE[c1 & 0x3f]); 303 | rs.push(BASE64_CODE[c2 & 0x3f]); 304 | } 305 | return rs.join(''); 306 | }; 307 | 308 | function char64(x) { 309 | var code = x.charCodeAt(0); 310 | if (code < 0 || code > INDEX_64.length) { 311 | return -1; 312 | } 313 | return INDEX_64[code]; 314 | }; 315 | 316 | function decode_base64(s, maxolen) { 317 | var off = 0; 318 | var slen = s.length; 319 | var olen = 0; 320 | var rs = []; 321 | var c1, c2, c3, c4, o; 322 | if (maxolen <= 0) 323 | throw "Invalid maxolen"; 324 | while (off < slen - 1 && olen < maxolen) { 325 | c1 = char64(s.charAt(off++)); 326 | c2 = char64(s.charAt(off++)); 327 | if (c1 == -1 || c2 == -1) { 328 | break; 329 | } 330 | o = getByte(c1 << 2); 331 | o |= (c2 & 0x30) >> 4; 332 | rs.push(String.fromCharCode(o)); 333 | if (++olen >= maxolen || off >= slen) { 334 | break; 335 | } 336 | c3 = char64(s.charAt(off++)); 337 | if (c3 == -1) { 338 | break; 339 | } 340 | o = getByte((c2 & 0x0f) << 4); 341 | o |= (c3 & 0x3c) >> 2; 342 | rs.push(String.fromCharCode(o)); 343 | if (++olen >= maxolen || off >= slen) { 344 | break; 345 | } 346 | c4 = char64(s.charAt(off++)); 347 | o = getByte((c3 & 0x03) << 6); 348 | o |= c4; 349 | rs.push(String.fromCharCode(o)); 350 | ++olen; 351 | } 352 | var ret = []; 353 | for (off = 0; off < olen; off++) { 354 | ret.push(getByte(rs[off])); 355 | } 356 | return ret; 357 | }; 358 | 359 | function encipher(lr, off, state) { 360 | var i; 361 | var n; 362 | var l = lr[off]; 363 | var r = lr[off + 1]; 364 | 365 | l ^= state.P[0]; 366 | for (i = 0; i <= BLOWFISH_NUM_ROUNDS - 2;) { 367 | // Feistel substitution on left word 368 | n = state.S[(l >> 24) & 0xff]; 369 | n += state.S[0x100 | ((l >> 16) & 0xff)]; 370 | n ^= state.S[0x200 | ((l >> 8) & 0xff)]; 371 | n += state.S[0x300 | (l & 0xff)]; 372 | r ^= n ^ state.P[++i]; 373 | 374 | // Feistel substitution on right word 375 | n = state.S[(r >> 24) & 0xff]; 376 | n += state.S[0x100 | ((r >> 16) & 0xff)]; 377 | n ^= state.S[0x200 | ((r >> 8) & 0xff)]; 378 | n += state.S[0x300 | (r & 0xff)]; 379 | l ^= n ^ state.P[++i]; 380 | } 381 | lr[off] = r ^ state.P[BLOWFISH_NUM_ROUNDS + 1]; 382 | lr[off + 1] = l; 383 | return state 384 | }; 385 | 386 | function streamtoword(data, state) { 387 | var i; 388 | var word = 0; 389 | var off = state.offp; 390 | for (i = 0; i < 4; i++) { 391 | word = (word << 8) | (data[off] & 0xff); 392 | off = (off + 1) % data.length; 393 | } 394 | state.offp = off; 395 | return word; 396 | }; 397 | 398 | function key(key, state) { 399 | var i; 400 | state.offp = 0; 401 | var lr = new Array(0x00000000, 0x00000000); 402 | var plen = state.P.length; 403 | var slen = state.S.length; 404 | 405 | for (i = 0; i < plen; i++) { 406 | state.P[i] = state.P[i] ^ streamtoword(key, state); 407 | } 408 | for (i = 0; i < plen; i += 2) { 409 | encipher(lr, 0, state); 410 | state.P[i] = lr[0]; 411 | state.P[i + 1] = lr[1]; 412 | } 413 | 414 | for (i = 0; i < slen; i += 2) { 415 | encipher(lr, 0, state); 416 | state.S[i] = lr[0]; 417 | state.S[i + 1] = lr[1]; 418 | } 419 | return state 420 | }; 421 | 422 | function ekskey(data, key, state) { 423 | var i; 424 | state.offp = 0; 425 | var lr = new Array(0x00000000, 0x00000000); 426 | var plen = state.P.length; 427 | var slen = state.S.length; 428 | 429 | for (i = 0; i < plen; i++) 430 | state.P[i] = state.P[i] ^ streamtoword(key, state); 431 | state.offp = 0; 432 | for (i = 0; i < plen; i += 2) { 433 | lr[0] ^= streamtoword(data, state); 434 | lr[1] ^= streamtoword(data, state); 435 | encipher(lr, 0, state); 436 | state.P[i] = lr[0]; 437 | state.P[i + 1] = lr[1]; 438 | } 439 | for (i = 0; i < slen; i += 2) { 440 | lr[0] ^= streamtoword(data, state); 441 | lr[1] ^= streamtoword(data, state); 442 | encipher(lr, 0, state); 443 | state.S[i] = lr[0]; 444 | state.S[i + 1] = lr[1]; 445 | } 446 | return state 447 | }; 448 | 449 | function crypt_raw(password, salt, log_rounds, cdata, callback, progress) { 450 | var rounds; 451 | var j; 452 | var clen = cdata.length; 453 | var one_percent; 454 | 455 | if (log_rounds < 4) 456 | throw "Minium of 4 rounds required, changing to default"; 457 | if (log_rounds > 30) 458 | throw "Maximum of 30 rounds exceded"; 459 | if (salt.length != BCRYPT_SALT_LEN) 460 | throw "Bad salt length"; 461 | 462 | rounds = 1 << log_rounds; 463 | one_percent = Math.floor(rounds / 100) + 1; 464 | var state = new keyState(); 465 | ekskey(salt, password, state); 466 | var i = 0; 467 | setTimeout(function() { 468 | if (i < rounds) { 469 | var start = new Date(); 470 | for (; i != rounds;) { 471 | i = i + 1; 472 | key(password, state); 473 | key(salt, state); 474 | if (i % one_percent == 0) { 475 | progress(); 476 | } 477 | if ((new Date() - start) > MAX_EXECUTION_TIME) { 478 | break; 479 | } 480 | } 481 | setTimeout(arguments.callee, 0); 482 | } else { 483 | for (i = 0; i < 64; i++) { 484 | for (j = 0; j < (clen >> 1); j++) { 485 | encipher(cdata, j << 1, state); 486 | } 487 | } 488 | var ret = []; 489 | for (i = 0; i < clen; i++) { 490 | ret.push(getByte((cdata[i] >> 24) & 0xff)); 491 | ret.push(getByte((cdata[i] >> 16) & 0xff)); 492 | ret.push(getByte((cdata[i] >> 8) & 0xff)); 493 | ret.push(getByte(cdata[i] & 0xff)); 494 | } 495 | callback(ret); 496 | } 497 | }, 0); 498 | }; 499 | 500 | function password_to_bytes(password) { 501 | var passwordb = []; 502 | for (var n = 0; n < password.length; n++) { 503 | var c = password.charCodeAt(n); 504 | if (c < 128) { 505 | passwordb.push(c); 506 | } else if ((c > 127) && (c < 2048)) { 507 | passwordb.push((c >> 6) | 192); 508 | passwordb.push((c & 63) | 128); 509 | } else if ((c >= 55296) && (c <= 56319)) { 510 | n++; 511 | if (n > password.length) { 512 | throw "utf-16 Decoding error: lead surrogate found without trail surrogate"; 513 | } 514 | c = password.charCodeAt(n); 515 | if (c < 56320 || c > 57343) { 516 | throw "utf-16 Decoding error: trail surrogate not in the range of 0xdc00 through 0xdfff"; 517 | } 518 | c = ((password.charCodeAt(n - 1) - 55296) << 10) + (c - 56320) + 65536; 519 | passwordb.push((c >> 18) | 240); 520 | passwordb.push(((c >> 12) & 63) | 128); 521 | passwordb.push(((c >> 6) & 63) | 128); 522 | passwordb.push((c & 63) | 128); 523 | } else { 524 | passwordb.push((c >> 12) | 224); 525 | passwordb.push(((c >> 6) & 63) | 128); 526 | passwordb.push((c & 63) | 128); 527 | } 528 | } 529 | return passwordb 530 | }; 531 | 532 | /* 533 | * callback: a function that will be passed the hash when it is complete 534 | * progress: optional - this function will be called every time 1% of hashing 535 | * is complete. 536 | */ 537 | function hashpw(password, salt, callback, progress) { 538 | var real_salt; 539 | var passwordb = []; 540 | var saltb = []; 541 | var hashed = []; 542 | var minor = String.fromCharCode(0); 543 | var rounds = 0; 544 | var off = 0; 545 | 546 | if (!progress) { 547 | progress = function() {}; 548 | } 549 | 550 | if (salt.charAt(0) != '$' || salt.charAt(1) != '2') 551 | throw "Invalid salt version"; 552 | if (salt.charAt(2) == '$') 553 | off = 3; 554 | else { 555 | minor = salt.charAt(2); 556 | if ((minor != 'a' && minor != 'b') || salt.charAt(3) != '$') 557 | throw "Invalid salt revision"; 558 | off = 4; 559 | } 560 | 561 | // Extract number of rounds 562 | if (salt.charAt(off + 2) > '$') 563 | throw "Missing salt rounds"; 564 | var r1 = parseInt(salt.substring(off, off + 1)) * 10; 565 | var r2 = parseInt(salt.substring(off + 1, off + 2)); 566 | rounds = r1 + r2; 567 | real_salt = salt.substring(off + 3, off + 25); 568 | password = password + (minor >= 'a' ? "\000" : ""); 569 | passwordb = password_to_bytes(password) 570 | saltb = decode_base64(real_salt, BCRYPT_SALT_LEN); 571 | crypt_raw( 572 | passwordb, 573 | saltb, 574 | rounds, 575 | BF_CRYPT_CIPHERTEXT.slice(0), 576 | function(hashed) { 577 | var rs = []; 578 | rs.push("$2"); 579 | if (minor >= 'a') 580 | rs.push(minor); 581 | rs.push("$"); 582 | if (rounds < 10) 583 | rs.push("0"); 584 | rs.push(rounds.toString()); 585 | rs.push("$"); 586 | rs.push(encode_base64(saltb, saltb.length)); 587 | rs.push(encode_base64(hashed, BF_CRYPT_CIPHERTEXT.length * 4 - 1)); 588 | callback(rs.join('')); 589 | }, 590 | progress); 591 | }; 592 | 593 | function gensalt(rounds) { 594 | var iteration_count = rounds; 595 | if (iteration_count < 4 || iteration_count > 30) { 596 | throw "Rounds exceded maximum (30)!" 597 | } 598 | var output = []; 599 | output.push("$2a$"); 600 | if (iteration_count < 10) 601 | output.push("0"); 602 | output.push(iteration_count.toString()); 603 | output.push('$'); 604 | var s1 = new Int8Array(BCRYPT_SALT_LEN); 605 | window.crypto.getRandomValues(s1); 606 | output.push(encode_base64(s1, BCRYPT_SALT_LEN)); 607 | return output.join(''); 608 | }; 609 | 610 | function checkpw(plaintext, hashed, callback, progress) { 611 | var off = 0; 612 | if (hashed.charAt(0) != '$' || hashed.charAt(1) != '2') 613 | throw "Invalid salt version"; 614 | if (hashed.charAt(2) == '$') 615 | off = 3; 616 | else { 617 | minor = hashed.charAt(2); 618 | if ((minor != 'a' && minor != 'b') || hashed.charAt(3) != '$') { 619 | throw "Invalid salt revision"; 620 | } 621 | off = 4; 622 | } 623 | salt = hashed.substring(0, off + 25); 624 | hashpw(plaintext, salt, function(try_pass) { 625 | var ret = 0; 626 | for (var i = 0; i < hashed.length; i++) { 627 | ret |= getByte(hashed[i]) ^ getByte(try_pass[i]) 628 | } 629 | callback(ret == 0); 630 | }, progress); 631 | }; 632 | -------------------------------------------------------------------------------- /chalkboard/chalkboard.js: -------------------------------------------------------------------------------- 1 | /***************************************************************** 2 | ** Author: Asvin Goel, goel@telematique.eu 3 | ** 4 | ** A plugin for reveal.js adding a chalkboard. 5 | ** 6 | ** Version: 0.9 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 by Kurt Rinnert https://github.com/rinnert 13 | ******************************************************************/ 14 | 15 | var RevealChalkboard = window.RevealChalkboard || (function(){ 16 | var path = scriptPath(); 17 | function scriptPath() { 18 | // obtain plugin path from the script element 19 | var src; 20 | if (document.currentScript) { 21 | src = document.currentScript.src; 22 | } else { 23 | var sel = document.querySelector('script[src$="/chalkboard.js"]') 24 | if (sel) { 25 | src = sel.src; 26 | } 27 | } 28 | 29 | var path = typeof src === undefined ? src 30 | : src.slice(0, src.lastIndexOf("/") + 1); 31 | //console.log("Path: " + path); 32 | return path; 33 | } 34 | 35 | 36 | /* Feature detection for passive event handling*/ 37 | var passiveSupported = false; 38 | 39 | try { 40 | window.addEventListener("test", null, Object.defineProperty({}, "passive", { get: function() { passiveSupported = true; } })); 41 | } catch(err) {} 42 | 43 | 44 | /***************************************************************** 45 | ** Configuration 46 | ******************************************************************/ 47 | var background, pen, draw, color; 48 | var grid = false; 49 | var boardmarkerWidth = 3; 50 | var chalkWidth = 7; 51 | var chalkEffect = 1.0; 52 | var rememberColor = [true, false]; 53 | var eraser = { src: path + 'img/sponge.png', radius: 20}; 54 | var boardmarkers = [ 55 | { color: 'rgba(100,100,100,1)', cursor: 'url(' + path + 'img/boardmarker-black.png), auto'}, 56 | { color: 'rgba(30,144,255, 1)', cursor: 'url(' + path + 'img/boardmarker-blue.png), auto'}, 57 | { color: 'rgba(220,20,60,1)', cursor: 'url(' + path + 'img/boardmarker-red.png), auto'}, 58 | { color: 'rgba(50,205,50,1)', cursor: 'url(' + path + 'img/boardmarker-green.png), auto'}, 59 | { color: 'rgba(255,140,0,1)', cursor: 'url(' + path + 'img/boardmarker-orange.png), auto'}, 60 | { color: 'rgba(150,0,20150,1)', cursor: 'url(' + path + 'img/boardmarker-purple.png), auto'}, 61 | { color: 'rgba(255,220,0,1)', cursor: 'url(' + path + 'img/boardmarker-yellow.png), auto'} 62 | ]; 63 | var chalks = [ 64 | { color: 'rgba(255,255,255,0.5)', cursor: 'url(' + path + 'img/chalk-white.png), auto'}, 65 | { color: 'rgba(96, 154, 244, 0.5)', cursor: 'url(' + path + 'img/chalk-blue.png), auto'}, 66 | { color: 'rgba(237, 20, 28, 0.5)', cursor: 'url(' + path + 'img/chalk-red.png), auto'}, 67 | { color: 'rgba(20, 237, 28, 0.5)', cursor: 'url(' + path + 'img/chalk-green.png), auto'}, 68 | { color: 'rgba(220, 133, 41, 0.5)', cursor: 'url(' + path + 'img/chalk-orange.png), auto'}, 69 | { color: 'rgba(220,0,220,0.5)', cursor: 'url(' + path + 'img/chalk-purple.png), auto'}, 70 | { color: 'rgba(255,220,0,0.5)', cursor: 'url(' + path + 'img/chalk-yellow.png), auto'} 71 | ]; 72 | 73 | var theme = "chalkboard"; 74 | var color = [0, 0]; 75 | var toggleChalkboardButton = true; 76 | var toggleNotesButton = true; 77 | var transition = 800; 78 | 79 | var readOnly = undefined; 80 | 81 | var config = configure( Reveal.getConfig().chalkboard || {} ); 82 | 83 | function configure( config ) { 84 | if ( config.boardmarkerWidth || config.penWidth ) boardmarkerWidth = config.boardmarkerWidth || config.penWidth; 85 | if ( config.chalkWidth ) chalkWidth = config.chalkWidth; 86 | if ( "chalkEffect" in config ) chalkEffect = ("chalkEffect" in config); 87 | if ( config.rememberColor ) rememberColor = config.rememberColor; 88 | if ( config.eraser ) eraser = config.eraser; 89 | if ("boardmarkers" in config) boardmarkers = config.boardmarkers; 90 | if ("chalks" in config) chalks = config.chalks; 91 | 92 | if ( config.theme ) theme = config.theme; 93 | switch ( theme ) { 94 | case "whiteboard": 95 | background = [ 'rgba(127,127,127,.1)' , path + 'img/whiteboard.png' ]; 96 | draw = [ drawWithBoardmarker , drawWithBoardmarker ]; 97 | pens = [ boardmarkers, boardmarkers ]; 98 | grid = { color: 'rgb(127,127,255,0.1)', distance: 40, width: 2}; 99 | break; 100 | case "chalkboard": 101 | default: 102 | background = [ 'rgba(127,127,127,.1)' , path + 'img/blackboard.png' ]; 103 | draw = [ drawWithBoardmarker , drawWithChalk ]; 104 | pens = [ boardmarkers, chalks ]; 105 | grid = { color: 'rgb(50,50,10,0.5)', distance: 80, width: 2}; 106 | } 107 | 108 | if ( config.background ) background = config.background; 109 | if ( config.grid != undefined ) grid = config.grid; 110 | 111 | if (config.toggleChalkboardButton != undefined) toggleChalkboardButton = config.toggleChalkboardButton; 112 | if (config.toggleNotesButton != undefined) toggleNotesButton = config.toggleNotesButton; 113 | if (config.transition) transition = config.transition; 114 | 115 | if (config.readOnly) readOnly = config.readOnly; 116 | 117 | if ( drawingCanvas && ( config.theme || config.background || config.grid ) ) { 118 | var canvas = document.getElementById( drawingCanvas[1].id ); 119 | canvas.style.background = 'url("' + background[1] + '") repeat'; 120 | clearCanvas( 1 ); 121 | drawGrid(); 122 | } 123 | return config; 124 | } 125 | /***************************************************************** 126 | ** Setup 127 | ******************************************************************/ 128 | 129 | function whenReady( callback ) { 130 | // wait for drawings to be loaded and markdown to be parsed 131 | if ( loaded == null || document.querySelector('section[data-markdown]:not([data-markdown-parsed])') ) { 132 | setTimeout( whenReady, 500, callback ) 133 | } 134 | else { 135 | callback(); 136 | } 137 | } 138 | 139 | 140 | if ( toggleChalkboardButton ) { 141 | //console.log("toggleChalkboardButton") 142 | var button = document.createElement( 'div' ); 143 | button.className = "chalkboard-button"; 144 | button.id = "toggle-chalkboard"; 145 | button.style.visibility = "visible"; 146 | button.style.position = "absolute"; 147 | button.style.zIndex = 30; 148 | button.style.fontSize = "24px"; 149 | 150 | button.style.left = toggleChalkboardButton.left || "30px"; 151 | button.style.bottom = toggleChalkboardButton.bottom || "30px"; 152 | button.style.top = toggleChalkboardButton.top || "auto"; 153 | button.style.right = toggleChalkboardButton.right || "auto"; 154 | 155 | button.innerHTML = '' 156 | document.querySelector(".reveal").appendChild( button ); 157 | } 158 | if ( toggleNotesButton ) { 159 | //console.log("toggleNotesButton") 160 | var button = document.createElement( 'div' ); 161 | button.className = "chalkboard-button"; 162 | button.id = "toggle-notes"; 163 | button.style.position = "absolute"; 164 | button.style.zIndex = 30; 165 | button.style.fontSize = "24px"; 166 | 167 | button.style.left = toggleNotesButton.left || "70px"; 168 | button.style.bottom = toggleNotesButton.bottom || "30px"; 169 | button.style.top = toggleNotesButton.top || "auto"; 170 | button.style.right = toggleNotesButton.right || "auto"; 171 | 172 | button.innerHTML = '' 173 | document.querySelector(".reveal").appendChild( button ); 174 | } 175 | //alert("Buttons"); 176 | 177 | var drawingCanvas = [ {id: "notescanvas" }, {id: "chalkboard" } ]; 178 | setupDrawingCanvas(0); 179 | setupDrawingCanvas(1); 180 | 181 | var mode = 0; // 0: notes canvas, 1: chalkboard 182 | 183 | var mouseX = 0; 184 | var mouseY = 0; 185 | var xLast = null; 186 | var yLast = null; 187 | 188 | var slideStart = Date.now(); 189 | var slideIndices = { h:0, v:0 }; 190 | var event = null; 191 | var timeouts = [ [], [] ]; 192 | var touchTimeout = null; 193 | var slidechangeTimeout = null; 194 | var playback = false; 195 | 196 | function setupDrawingCanvas( id ) { 197 | var container = document.createElement( 'div' ); 198 | container.id = drawingCanvas[id].id; 199 | container.classList.add( 'overlay' ); 200 | container.setAttribute( 'data-prevent-swipe', '' ); 201 | container.oncontextmenu = function() { return false; } 202 | container.style.cursor = pens[ id ][ color[id] ].cursor; 203 | 204 | drawingCanvas[id].width = window.innerWidth; 205 | drawingCanvas[id].height = window.innerHeight; 206 | drawingCanvas[id].scale = 1; 207 | drawingCanvas[id].xOffset = 0; 208 | drawingCanvas[id].yOffset = 0; 209 | 210 | 211 | if ( id == "0" ) { 212 | container.style.background = 'rgba(0,0,0,0)'; 213 | container.style.zIndex = "24"; 214 | container.classList.add( 'visible' ) 215 | container.style.pointerEvents = "none"; 216 | 217 | var slides = document.querySelector(".slides"); 218 | var aspectRatio = Reveal.getConfig().width / Reveal.getConfig().height; 219 | if ( drawingCanvas[id].width > drawingCanvas[id].height*aspectRatio ) { 220 | drawingCanvas[id].xOffset = (drawingCanvas[id].width - drawingCanvas[id].height*aspectRatio) / 2; 221 | } 222 | else if ( drawingCanvas[id].height > drawingCanvas[id].width/aspectRatio ) { 223 | drawingCanvas[id].yOffset = ( drawingCanvas[id].height - drawingCanvas[id].width/aspectRatio ) / 2; 224 | } 225 | } 226 | else { 227 | container.style.background = 'url("' + background[id] + '") repeat'; 228 | container.style.zIndex = "26"; 229 | } 230 | 231 | var sponge = document.createElement( 'img' ); 232 | sponge.src = eraser.src; 233 | sponge.id = "sponge"; 234 | sponge.style.visibility = "hidden"; 235 | sponge.style.position = "absolute"; 236 | container.appendChild( sponge ); 237 | drawingCanvas[id].sponge = sponge; 238 | 239 | var canvas = document.createElement( 'canvas' ); 240 | canvas.width = drawingCanvas[id].width; 241 | canvas.height = drawingCanvas[id].height; 242 | canvas.setAttribute( 'data-chalkboard', id ); 243 | canvas.style.cursor = pens[ id ][ color[id] ].cursor; 244 | container.appendChild( canvas ); 245 | drawingCanvas[id].canvas = canvas; 246 | 247 | drawingCanvas[id].context = canvas.getContext("2d"); 248 | 249 | 250 | document.querySelector( '.reveal' ).appendChild( container ); 251 | drawingCanvas[id].container = container; 252 | } 253 | 254 | 255 | /***************************************************************** 256 | ** Storage 257 | ******************************************************************/ 258 | 259 | var storage = [ 260 | { width: Reveal.getConfig().width, height: Reveal.getConfig().height, data: []}, 261 | { width: Reveal.getConfig().width, height: Reveal.getConfig().height, data: []} 262 | ]; 263 | //console.log( JSON.stringify(storage)); 264 | 265 | var loaded = null; 266 | if ( config.src != null ) { 267 | loadData( config.src ); 268 | } 269 | 270 | 271 | /** 272 | * Load data. 273 | */ 274 | function loadData( filename ) { 275 | var xhr = new XMLHttpRequest(); 276 | xhr.onload = function() { 277 | if (xhr.readyState === 4 && xhr.status != 404 ) { 278 | storage = JSON.parse(xhr.responseText); 279 | for (var id = 0; id < storage.length; id++) { 280 | if ( drawingCanvas[id].width != storage[id].width || drawingCanvas[id].height != storage[id].height ) { 281 | drawingCanvas[id].scale = Math.min( drawingCanvas[id].width/storage[id].width, drawingCanvas[id].height/storage[id].height); 282 | drawingCanvas[id].xOffset = (drawingCanvas[id].width - storage[id].width * drawingCanvas[id].scale)/2; 283 | drawingCanvas[id].yOffset = (drawingCanvas[id].height - storage[id].height * drawingCanvas[id].scale)/2; 284 | } 285 | if ( config.readOnly ) { 286 | drawingCanvas[id].container.style.cursor = 'default'; 287 | drawingCanvas[id].canvas.style.cursor = 'default'; 288 | } 289 | } 290 | loaded = true; 291 | //console.log("Drawings loaded"); 292 | } 293 | else { 294 | config.readOnly = undefined; 295 | readOnly = undefined; 296 | console.warn( 'Failed to get file ' + filename +". ReadyState: " + xhr.readyState + ", Status: " + xhr.status); 297 | loaded = false; 298 | } 299 | }; 300 | 301 | xhr.open( 'GET', filename, true ); 302 | try { 303 | xhr.send(); 304 | } 305 | catch ( error ) { 306 | config.readOnly = undefined; 307 | readOnly = undefined; 308 | 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 ); 309 | loaded = false; 310 | } 311 | } 312 | 313 | /** 314 | * Download data. 315 | */ 316 | function downloadData() { 317 | var a = document.createElement('a'); 318 | document.body.appendChild(a); 319 | try { 320 | // cleanup slide data without events 321 | for (var id = 0; id < 2; id++) { 322 | for (var i = storage[id].data.length-1; i >= 0; i--) { 323 | if (storage[id].data[i].events.length == 0) { 324 | storage[id].data.splice(i, 1); 325 | } 326 | } 327 | } 328 | a.download = "chalkboard.json"; 329 | var blob = new Blob( [ JSON.stringify( storage ) ], { type: "application/json"} ); 330 | a.href = window.URL.createObjectURL( blob ); 331 | } catch( error ) { 332 | a.innerHTML += " (" + error + ")"; 333 | } 334 | a.click(); 335 | document.body.removeChild(a); 336 | } 337 | 338 | /** 339 | * Returns data object for the slide with the given indices. 340 | */ 341 | function getSlideData( indices, id ) { 342 | if ( id == undefined ) id = mode; 343 | if (!indices) indices = slideIndices; 344 | var data; 345 | for (var i = 0; i < storage[id].data.length; i++) { 346 | 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 ) { 347 | data = storage[id].data[i]; 348 | return data; 349 | } 350 | } 351 | storage[id].data.push( { slide: indices, events: [], duration: 0 } ); 352 | data = storage[id].data[storage[id].data.length-1]; 353 | return data; 354 | } 355 | 356 | /** 357 | * Returns maximum duration of slide playback for both modes 358 | */ 359 | function getSlideDuration( indices ) { 360 | if (!indices) indices = slideIndices; 361 | var duration = 0; 362 | for (var id = 0; id < 2; id++) { 363 | for (var i = 0; i < storage[id].data.length; i++) { 364 | 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 ) { 365 | duration = Math.max( duration, storage[id].data[i].duration ); 366 | break; 367 | } 368 | } 369 | } 370 | //console.log( duration ); 371 | return duration; 372 | } 373 | 374 | /***************************************************************** 375 | ** Print 376 | ******************************************************************/ 377 | var printMode = ( /print-pdf/gi ).test( window.location.search ); 378 | //console.log("createPrintout" + printMode) 379 | 380 | function createPrintout( ) { 381 | //console.log( 'Create printout for ' + storage[1].data.length + " slides"); 382 | drawingCanvas[0].container.classList.remove( 'visible' ); // do not print notes canvas 383 | var nextSlide = []; 384 | for (var i = 0; i < storage[1].data.length; i++) { 385 | var slide = Reveal.getSlide( storage[1].data[i].slide.h, storage[1].data[i].slide.v ); 386 | nextSlide.push( slide.nextSibling ); 387 | } 388 | 389 | var patImg = new Image(); 390 | patImg.onload = function () { 391 | var width = Reveal.getConfig().width; 392 | var height = Reveal.getConfig().height; 393 | var scale = 1; 394 | var xOffset = 0; 395 | var yOffset = 0; 396 | if ( width != storage[1].width || height != storage[1].height ) { 397 | scale = Math.min( width/storage[1].width, height/storage[1].height); 398 | xOffset = (width - storage[1].width * scale)/2; 399 | yOffset = (height - storage[1].height * scale)/2; 400 | } 401 | mode = 1; 402 | 403 | for (var i = 0; i < storage[1].data.length; i++) { 404 | console.log( 'Create printout for slide ' + storage[1].data[i].slide.h + "." + storage[1].data[i].slide.v ); 405 | var parent = Reveal.getSlide( storage[1].data[i].slide.h, storage[1].data[i].slide.v ).parentElement; 406 | var slideData = getSlideData( storage[1].data[i].slide, 1 ); 407 | 408 | var imgCanvas = document.createElement('canvas'); 409 | imgCanvas.width = width; 410 | imgCanvas.height = height; 411 | 412 | var imgCtx = imgCanvas.getContext("2d"); 413 | imgCtx.fillStyle = imgCtx.createPattern( patImg ,'repeat'); 414 | imgCtx.rect(0,0,imgCanvas.width,imgCanvas.height); 415 | imgCtx.fill(); 416 | 417 | for (var j = 0; j < slideData.events.length; j++) { 418 | switch ( slideData.events[j].type ) { 419 | case "draw": 420 | for (var k = 1; k < slideData.events[j].curve.length; k++) { 421 | draw[1]( imgCtx, 422 | xOffset + slideData.events[j].curve[k-1].x*scale, 423 | yOffset + slideData.events[j].curve[k-1].y*scale, 424 | xOffset + slideData.events[j].curve[k].x*scale, 425 | yOffset + slideData.events[j].curve[k].y*scale 426 | ); 427 | } 428 | break; 429 | case "erase": 430 | for (var k = 0; k < slideData.events[j].curve.length; k++) { 431 | eraseWithSponge( imgCtx, 432 | xOffset + slideData.events[j].curve[k].x*scale, 433 | yOffset + slideData.events[j].curve[k].y*scale 434 | ); 435 | } 436 | break; 437 | case "setcolor": 438 | setColor(slideData.events[j].index); 439 | break; 440 | case "clear": 441 | addPrintout( parent, nextSlide[i], imgCanvas, patImg ); 442 | imgCtx.clearRect(0,0,imgCanvas.width,imgCanvas.height); 443 | imgCtx.fill(); 444 | break; 445 | default: 446 | break; 447 | } 448 | } 449 | mode = 0; 450 | if ( slideData.events.length ) { 451 | addPrintout( parent, nextSlide[i], imgCanvas, patImg ); 452 | } 453 | } 454 | Reveal.sync(); 455 | }; 456 | patImg.src = background[1]; 457 | } 458 | 459 | function addPrintout( parent, nextSlide, imgCanvas, patImg ) { 460 | var slideCanvas = document.createElement('canvas'); 461 | slideCanvas.width = Reveal.getConfig().width; 462 | slideCanvas.height = Reveal.getConfig().height; 463 | var ctx = slideCanvas.getContext("2d"); 464 | ctx.fillStyle = ctx.createPattern( patImg ,'repeat'); 465 | ctx.rect(0,0,slideCanvas.width,slideCanvas.height); 466 | ctx.fill(); 467 | ctx.drawImage(imgCanvas, 0, 0); 468 | 469 | var newSlide = document.createElement( 'section' ); 470 | newSlide.classList.add( 'present' ); 471 | newSlide.innerHTML = '

Drawing

'; 472 | newSlide.setAttribute("data-background-size", '100% 100%' ); 473 | newSlide.setAttribute("data-background-repeat", 'norepeat' ); 474 | newSlide.setAttribute("data-background", 'url("' + slideCanvas.toDataURL("image/png") +'")' ); 475 | if ( nextSlide != null ) { 476 | parent.insertBefore( newSlide, nextSlide ); 477 | } 478 | else { 479 | parent.append( newSlide ); 480 | } 481 | } 482 | 483 | /***************************************************************** 484 | ** Drawings 485 | ******************************************************************/ 486 | 487 | function drawWithBoardmarker(context,fromX,fromY,toX,toY){ 488 | context.lineWidth = boardmarkerWidth; 489 | context.lineCap = 'round'; 490 | context.strokeStyle = boardmarkers[color[mode]].color; 491 | context.beginPath(); 492 | context.moveTo(fromX, fromY); 493 | context.lineTo(toX, toY); 494 | context.stroke(); 495 | } 496 | 497 | function drawWithChalk(context,fromX,fromY,toX,toY) { 498 | var brushDiameter = chalkWidth; 499 | context.lineWidth = brushDiameter; 500 | context.lineCap = 'round'; 501 | context.fillStyle = chalks[color[mode]].color; // 'rgba(255,255,255,0.5)'; 502 | context.strokeStyle = chalks[color[mode]].color; 503 | /*var opacity = Math.min(0.8, Math.max(0,color[1].replace(/^.*,(.+)\)/,'$1') - 0.1)) + Math.random()*0.2;*/ 504 | var opacity = 1.0; 505 | context.strokeStyle = context.strokeStyle.replace(/[\d\.]+\)$/g, opacity + ')'); 506 | context.beginPath(); 507 | context.moveTo(fromX, fromY); 508 | context.lineTo(toX, toY); 509 | context.stroke(); 510 | // Chalk Effect 511 | var length = Math.round(Math.sqrt(Math.pow(toX-fromX,2)+Math.pow(toY-fromY,2))/(5/brushDiameter)); 512 | var xUnit = (toX-fromX)/length; 513 | var yUnit = (toY-fromY)/length; 514 | for(var i=0; i (Math.random() * 0.9)) { 516 | var xCurrent = fromX+(i*xUnit); 517 | var yCurrent = fromY+(i*yUnit); 518 | var xRandom = xCurrent+(Math.random()-0.5)*brushDiameter*1.2; 519 | var yRandom = yCurrent+(Math.random()-0.5)*brushDiameter*1.2; 520 | context.clearRect( xRandom, yRandom, Math.random()*2+2, Math.random()+1); 521 | } 522 | } 523 | } 524 | 525 | function eraseWithSponge(context,x,y) { 526 | context.save(); 527 | context.beginPath(); 528 | context.arc(x, y, eraser.radius, 0, 2 * Math.PI, false); 529 | context.clip(); 530 | context.clearRect(x - eraser.radius - 1, y - eraser.radius - 1, eraser.radius * 2 + 2, eraser.radius * 2 + 2); 531 | context.restore(); 532 | if ( mode == 1 && grid) { 533 | redrawGrid(x,y,eraser.radius); 534 | } 535 | } 536 | 537 | 538 | 539 | /** 540 | * Oboardmarkers an overlay for the chalkboard. 541 | */ 542 | function showChalkboard() { 543 | //console.log("showChalkboard"); 544 | clearTimeout(touchTimeout); 545 | touchTimeout = null; 546 | drawingCanvas[0].sponge.style.visibility = "hidden"; // make sure that the sponge from touch events is hidden 547 | drawingCanvas[1].sponge.style.visibility = "hidden"; // make sure that the sponge from touch events is hidden 548 | drawingCanvas[1].container.classList.add( 'visible' ); 549 | mode = 1; 550 | // broadcast 551 | var message = new CustomEvent('send'); 552 | message.content = { sender: 'chalkboard-plugin', type: 'showChalkboard' }; 553 | document.dispatchEvent( message ); 554 | } 555 | 556 | 557 | /** 558 | * Closes open chalkboard. 559 | */ 560 | function closeChalkboard() { 561 | clearTimeout(touchTimeout); 562 | touchTimeout = null; 563 | drawingCanvas[0].sponge.style.visibility = "hidden"; // make sure that the sponge from touch events is hidden 564 | drawingCanvas[1].sponge.style.visibility = "hidden"; // make sure that the sponge from touch events is hidden 565 | drawingCanvas[1].container.classList.remove( 'visible' ); 566 | xLast = null; 567 | yLast = null; 568 | event = null; 569 | mode = 0; 570 | // broadcast 571 | var message = new CustomEvent('send'); 572 | message.content = { sender: 'chalkboard-plugin', type: 'closeChalkboard' }; 573 | document.dispatchEvent( message ); 574 | } 575 | 576 | /** 577 | * Clear current canvas. 578 | */ 579 | function clearCanvas( id ) { 580 | if ( id == 0 ) clearTimeout( slidechangeTimeout ); 581 | drawingCanvas[id].context.clearRect(0,0,drawingCanvas[id].width,drawingCanvas[id].height); 582 | if ( id == 1 && grid ) drawGrid(); 583 | } 584 | 585 | /** 586 | * Draw grid on background 587 | */ 588 | function drawGrid() { 589 | var context = drawingCanvas[1].context; 590 | 591 | drawingCanvas[1].scale = Math.min( drawingCanvas[1].width/storage[1].width, drawingCanvas[1].height/storage[1].height ); 592 | drawingCanvas[1].xOffset = (drawingCanvas[1].width - storage[1].width * drawingCanvas[1].scale)/2; 593 | drawingCanvas[1].yOffset = (drawingCanvas[1].height - storage[1].height * drawingCanvas[1].scale)/2; 594 | 595 | var scale = drawingCanvas[1].scale; 596 | var xOffset = drawingCanvas[1].xOffset; 597 | var yOffset = drawingCanvas[1].yOffset; 598 | 599 | var distance = grid.distance*scale; 600 | 601 | var fromX = drawingCanvas[1].width/2 - distance/2 - Math.floor( (drawingCanvas[1].width - distance)/2 / distance ) * distance; 602 | for( var x=fromX; x < drawingCanvas[1].width; x+=distance ) { 603 | context.beginPath(); 604 | context.lineWidth = grid.width*scale; 605 | context.lineCap = 'round'; 606 | context.fillStyle = grid.color; 607 | context.strokeStyle = grid.color; 608 | context.moveTo(x, 0); 609 | context.lineTo(x, drawingCanvas[1].height); 610 | context.stroke(); 611 | } 612 | var fromY = drawingCanvas[1].height/2 - distance/2 - Math.floor( (drawingCanvas[1].height - distance)/2 / distance ) * distance ; 613 | 614 | for( var y=fromY; y < drawingCanvas[1].height; y+=distance ) { 615 | context.beginPath(); 616 | context.lineWidth = grid.width*scale; 617 | context.lineCap = 'round'; 618 | context.fillStyle = grid.color; 619 | context.strokeStyle = grid.color; 620 | context.moveTo(0, y); 621 | context.lineTo(drawingCanvas[1].width, y); 622 | context.stroke(); 623 | } 624 | } 625 | 626 | function redrawGrid(centerX,centerY,diameter) { 627 | var context = drawingCanvas[1].context; 628 | 629 | drawingCanvas[1].scale = Math.min( drawingCanvas[1].width/storage[1].width, drawingCanvas[1].height/storage[1].height ); 630 | drawingCanvas[1].xOffset = (drawingCanvas[1].width - storage[1].width * drawingCanvas[1].scale)/2; 631 | drawingCanvas[1].yOffset = (drawingCanvas[1].height - storage[1].height * drawingCanvas[1].scale)/2; 632 | 633 | var scale = drawingCanvas[1].scale; 634 | var xOffset = drawingCanvas[1].xOffset; 635 | var yOffset = drawingCanvas[1].yOffset; 636 | 637 | var distance = grid.distance*scale; 638 | 639 | var fromX = drawingCanvas[1].width/2 - distance/2 - Math.floor( (drawingCanvas[1].width - distance)/2 / distance ) * distance; 640 | 641 | for( var x=fromX + distance* Math.ceil( (centerX-diameter-fromX) / distance); x <= fromX + distance* Math.floor( (centerX+diameter-fromX) / distance); x+=distance ) { 642 | context.beginPath(); 643 | context.lineWidth = grid.width*scale; 644 | context.lineCap = 'round'; 645 | context.fillStyle = grid.color; 646 | context.strokeStyle = grid.color; 647 | context.moveTo(x, centerY - Math.sqrt( diameter*diameter - (centerX-x)*(centerX-x) )); 648 | context.lineTo(x, centerY + Math.sqrt( diameter*diameter - (centerX-x)*(centerX-x) ) ); 649 | context.stroke(); 650 | } 651 | var fromY = drawingCanvas[1].height/2 - distance/2 - Math.floor( (drawingCanvas[1].height - distance)/2 / distance ) * distance ; 652 | for( var y=fromY + distance* Math.ceil( (centerY-diameter-fromY) / distance); y <= fromY + distance* Math.floor( (centerY+diameter-fromY) / distance); y+=distance ) { 653 | context.beginPath(); 654 | context.lineWidth = grid.width*scale; 655 | context.lineCap = 'round'; 656 | context.fillStyle = grid.color; 657 | context.strokeStyle = grid.color; 658 | context.moveTo(centerX - Math.sqrt( diameter*diameter - (centerY-y)*(centerY-y) ), y ); 659 | context.lineTo(centerX + Math.sqrt( diameter*diameter - (centerY-y)*(centerY-y) ), y ); 660 | context.stroke(); 661 | } 662 | } 663 | 664 | /** 665 | * Set the color 666 | */ 667 | function setColor( index ) { 668 | // protect against out of bounds (this could happen when 669 | // replaying events recorded with different color settings). 670 | if ( index >= boardmarkers[mode].length ) index = 0; 671 | color[mode] = index; 672 | drawingCanvas[mode].canvas.style.cursor = pens[mode][color[mode]].cursor; 673 | } 674 | 675 | /** 676 | * Forward cycle color 677 | */ 678 | function cycleColorNext() { 679 | color[mode] = (color[mode] + 1) % pens[mode].length; 680 | return color[mode]; 681 | } 682 | 683 | /** 684 | * Backward cycle color 685 | */ 686 | function cycleColorPrev() { 687 | color[mode] = (color[mode] + (pens[mode].length - 1)) % pens[mode].length; 688 | return color[mode]; 689 | } 690 | 691 | /***************************************************************** 692 | ** Broadcast 693 | ******************************************************************/ 694 | document.addEventListener( 'received', function ( message ) { 695 | //console.log(JSON.stringify(message)); 696 | if ( message.content && message.content.sender == 'chalkboard-plugin' ) { 697 | switch ( message.content.type ) { 698 | case 'showChalkboard': 699 | showChalkboard(); 700 | break; 701 | case 'closeChalkboard': 702 | closeChalkboard(); 703 | break; 704 | case 'startDrawing': 705 | startDrawing(message.content.x, message.content.y, message.content.erase); 706 | break; 707 | case 'startErasing': 708 | if ( message.content ) { 709 | message.content.type = "erase"; 710 | message.content.begin = Date.now() - slideStart; 711 | eraseWithSponge(drawingCanvas[mode].context, message.content.x, message.content.y); 712 | } 713 | break; 714 | case 'drawSegment': 715 | drawSegment(message.content.x, message.content.y, message.content.erase); 716 | break; 717 | case 'stopDrawing': 718 | stopDrawing(); 719 | break; 720 | case 'clear': 721 | clear(); 722 | break; 723 | case 'setcolor': 724 | setColor(message.content.index); 725 | break; 726 | case 'resetSlide': 727 | resetSlide(true); 728 | break; 729 | case 'init': 730 | storage = message.content.storage; 731 | for (var id = 0; id < 2; id++ ) { 732 | drawingCanvas[id].scale = Math.min( drawingCanvas[id].width/storage[id].width, drawingCanvas[id].height/storage[id].height ); 733 | drawingCanvas[id].xOffset = (drawingCanvas[id].width - storage[id].width * drawingCanvas[id].scale)/2; 734 | drawingCanvas[id].yOffset = (drawingCanvas[id].height - storage[id].height * drawingCanvas[id].scale)/2; 735 | } 736 | clearCanvas( 0 ); 737 | clearCanvas( 1 ); 738 | if ( !playback ) { 739 | slidechangeTimeout = setTimeout( startPlayback, transition, getSlideDuration(), 0 ); 740 | } 741 | if ( mode == 1 && message.content.mode == 0) { 742 | setTimeout( closeChalkboard, transition + 50 ); 743 | } 744 | if ( mode == 0 && message.content.mode == 1) { 745 | setTimeout( showChalkboard, transition + 50 ); 746 | } 747 | mode = message.content.mode; 748 | break; 749 | default: 750 | break; 751 | } 752 | } 753 | }); 754 | 755 | document.addEventListener( 'newclient', function() { 756 | // broadcast storage 757 | var message = new CustomEvent('send'); 758 | message.content = { sender: 'chalkboard-plugin', type: 'init', storage: storage, mode: mode }; 759 | document.dispatchEvent( message ); 760 | }); 761 | 762 | /***************************************************************** 763 | ** Playback 764 | ******************************************************************/ 765 | 766 | document.addEventListener('seekplayback', function( event ) { 767 | //console.log('event seekplayback ' + event.timestamp); 768 | stopPlayback(); 769 | if ( !playback || event.timestamp == 0) { 770 | // in other cases startplayback fires after seeked 771 | startPlayback( event.timestamp ); 772 | } 773 | //console.log('seeked'); 774 | }); 775 | 776 | 777 | document.addEventListener('startplayback', function( event ) { 778 | //console.log('event startplayback ' + event.timestamp); 779 | stopPlayback(); 780 | playback = true; 781 | startPlayback( event.timestamp ); 782 | }); 783 | 784 | document.addEventListener('stopplayback', function( event ) { 785 | //console.log('event stopplayback ' + (Date.now() - slideStart) ); 786 | playback = false; 787 | stopPlayback(); 788 | }); 789 | 790 | document.addEventListener('startrecording', function( event ) { 791 | //console.log('event startrecording ' + event.timestamp); 792 | startRecording(); 793 | }); 794 | 795 | function recordEvent( event ) { 796 | var slideData = getSlideData(); 797 | var i = slideData.events.length; 798 | while ( i > 0 && event.begin < slideData.events[i-1].begin ) { 799 | i--; 800 | } 801 | slideData.events.splice( i, 0, event); 802 | slideData.duration = Math.max( slideData.duration, Date.now() - slideStart ) + 1; 803 | } 804 | 805 | function startRecording() { 806 | resetSlide( true ); 807 | updateReadOnlyMode(); 808 | slideStart = Date.now(); 809 | } 810 | 811 | function startPlayback( timestamp, finalMode, resized ) { 812 | //console.log("playback " + timestamp ); 813 | if ( resized == undefined ) { 814 | updateReadOnlyMode(); 815 | } 816 | slideStart = Date.now() - timestamp; 817 | closeChalkboard(); 818 | mode = 0; 819 | for ( var id = 0; id < 2; id++ ) { 820 | clearCanvas( id ); 821 | var slideData = getSlideData( slideIndices, id ); 822 | //console.log( timestamp +" / " + JSON.stringify(slideData)); 823 | var index = 0; 824 | while ( index < slideData.events.length && slideData.events[index].begin < (Date.now() - slideStart) ) { 825 | playEvent( id, slideData.events[index], timestamp ); 826 | index++; 827 | } 828 | 829 | while ( playback && index < slideData.events.length ) { 830 | timeouts[id].push( setTimeout( playEvent, slideData.events[index].begin - (Date.now() - slideStart), id, slideData.events[index], timestamp ) ); 831 | index++; 832 | } 833 | } 834 | //console.log("Mode: " + finalMode + "/" + mode ); 835 | if ( finalMode != undefined ) { 836 | mode = finalMode; 837 | } 838 | if( mode == 1 ) showChalkboard(); 839 | //console.log("playback (ok)"); 840 | 841 | }; 842 | 843 | function stopPlayback() { 844 | //console.log("stopPlayback"); 845 | //console.log("Timeouts: " + timeouts[0].length + "/"+ timeouts[1].length); 846 | for ( var id = 0; id < 2; id++ ) { 847 | for (var i = 0; i < timeouts[id].length; i++) { 848 | clearTimeout(timeouts[id][i]); 849 | } 850 | timeouts[id] = []; 851 | } 852 | }; 853 | 854 | function playEvent( id, event, timestamp ) { 855 | //console.log( timestamp +" / " + JSON.stringify(event)); 856 | //console.log( id + ": " + timestamp +" / " + event.begin +" / " + event.type +" / " + mode ); 857 | switch ( event.type ) { 858 | case "open": 859 | if ( timestamp <= event.begin ) { 860 | showChalkboard(); 861 | } 862 | else { 863 | mode = 1; 864 | } 865 | 866 | break; 867 | case "close": 868 | if ( timestamp < event.begin ) { 869 | closeChalkboard(); 870 | } 871 | else { 872 | mode = 0; 873 | } 874 | break; 875 | case "clear": 876 | clearCanvas( id ); 877 | break; 878 | case "setcolor": 879 | setColor(event.index); 880 | break; 881 | case "draw": 882 | drawCurve( id, event, timestamp ); 883 | break; 884 | case "erase": 885 | eraseCurve( id, event, timestamp ); 886 | break; 887 | 888 | } 889 | }; 890 | 891 | function drawCurve( id, event, timestamp ) { 892 | if ( event.curve.length > 1 ) { 893 | var ctx = drawingCanvas[id].context; 894 | var scale = drawingCanvas[id].scale; 895 | var xOffset = drawingCanvas[id].xOffset; 896 | var yOffset = drawingCanvas[id].yOffset; 897 | 898 | var stepDuration = ( event.end - event.begin )/ ( event.curve.length - 1 ); 899 | //console.log("---"); 900 | for (var i = 1; i < event.curve.length; i++) { 901 | if (event.begin + i * stepDuration <= (Date.now() - slideStart)) { 902 | //console.log( "Draw " + timestamp +" / " + event.begin + " + " + i + " * " + stepDuration ); 903 | draw[id](ctx, xOffset + event.curve[i-1].x*scale, yOffset + event.curve[i-1].y*scale, xOffset + event.curve[i].x*scale, yOffset + event.curve[i].y*scale); 904 | } 905 | else if ( playback ) { 906 | //console.log( "Cue " + timestamp +" / " + (Date.now() - slideStart) +" / " + event.begin + " + " + i + " * " + stepDuration + " = " + Math.max(0,event.begin + i * stepDuration - timestamp) ); 907 | timeouts.push( setTimeout( 908 | draw[id], Math.max(0,event.begin + i * stepDuration - (Date.now() - slideStart)), ctx, 909 | xOffset + event.curve[i-1].x*scale, 910 | yOffset + event.curve[i-1].y*scale, 911 | xOffset + event.curve[i].x*scale, 912 | yOffset + event.curve[i].y*scale 913 | ) 914 | ); 915 | } 916 | } 917 | } 918 | 919 | }; 920 | 921 | function eraseCurve( id, event, timestamp ) { 922 | if ( event.curve.length > 1 ) { 923 | var ctx = drawingCanvas[id].context; 924 | var scale = drawingCanvas[id].scale; 925 | var xOffset = drawingCanvas[id].xOffset; 926 | var yOffset = drawingCanvas[id].yOffset; 927 | 928 | var stepDuration = ( event.end - event.begin )/ event.curve.length; 929 | for (var i = 0; i < event.curve.length; i++) { 930 | if (event.begin + i * stepDuration <= (Date.now() - slideStart)) { 931 | eraseWithSponge(ctx, xOffset + event.curve[i].x*scale, yOffset + event.curve[i].y*scale); 932 | } 933 | else if ( playback ) { 934 | timeouts.push( setTimeout( 935 | eraseWithSponge, Math.max(0,event.begin + i * stepDuration - (Date.now() - slideStart)), ctx, 936 | xOffset + event.curve[i].x * scale, 937 | yOffset + event.curve[i].y * scale 938 | ) 939 | ); 940 | } 941 | } 942 | } 943 | 944 | }; 945 | 946 | 947 | function startDrawing( x, y, erase ) { 948 | var ctx = drawingCanvas[mode].context; 949 | var scale = drawingCanvas[mode].scale; 950 | var xOffset = drawingCanvas[mode].xOffset; 951 | var yOffset = drawingCanvas[mode].yOffset; 952 | xLast = x * scale + xOffset; 953 | yLast = y * scale + yOffset; 954 | if ( erase == true) { 955 | event = { type: "erase", begin: Date.now() - slideStart, end: null, curve: [{x: x, y: y}]}; 956 | drawingCanvas[mode].canvas.style.cursor = 'url("' + eraser.src + '") ' + eraser.radius + ' ' + eraser.radius + ', auto'; 957 | eraseWithSponge(ctx, x * scale + xOffset, y * scale + yOffset); 958 | } 959 | else { 960 | event = { type: "draw", begin: Date.now() - slideStart, end: null, curve: [{x: x, y: y}] }; 961 | } 962 | } 963 | 964 | 965 | function showSponge(x,y) { 966 | if ( event ) { 967 | event.type = "erase"; 968 | event.begin = Date.now() - slideStart; 969 | // show sponge image 970 | drawingCanvas[mode].sponge.style.left = (x - eraser.radius) +"px" ; 971 | drawingCanvas[mode].sponge.style.top = (y - eraser.radius) +"px" ; 972 | drawingCanvas[mode].sponge.style.visibility = "visible"; 973 | eraseWithSponge(drawingCanvas[mode].context,x,y); 974 | // broadcast 975 | var message = new CustomEvent('send'); 976 | message.content = { sender: 'chalkboard-plugin', type: 'startErasing', x: (mouseX - xOffset)/scale, y: (mouseY-yOffset)/scale }; 977 | document.dispatchEvent( message ); 978 | } 979 | } 980 | 981 | function drawSegment( x, y, erase ) { 982 | var ctx = drawingCanvas[mode].context; 983 | var scale = drawingCanvas[mode].scale; 984 | var xOffset = drawingCanvas[mode].xOffset; 985 | var yOffset = drawingCanvas[mode].yOffset; 986 | if ( !event ) { 987 | // safeguard if broadcast hickup 988 | startDrawing( x, y, erase ); 989 | } 990 | event.curve.push({x: x, y: y}); 991 | if(y * scale + yOffset < drawingCanvas[mode].height && x * scale + xOffset < drawingCanvas[mode].width) { 992 | if ( erase ) { 993 | eraseWithSponge(ctx, x * scale + xOffset, y * scale + yOffset); 994 | } 995 | else { 996 | draw[mode](ctx, xLast, yLast, x * scale + xOffset, y * scale + yOffset); 997 | } 998 | xLast = x * scale + xOffset; 999 | yLast = y * scale + yOffset; 1000 | } 1001 | } 1002 | 1003 | function stopDrawing() { 1004 | if ( event ) { 1005 | event.end = Date.now() - slideStart; 1006 | if ( event.type == "erase" || event.curve.length > 1 ) { 1007 | // do not save a line with a single point only 1008 | recordEvent( event ); 1009 | } 1010 | event = null; 1011 | } 1012 | } 1013 | 1014 | 1015 | /***************************************************************** 1016 | ** User interface 1017 | ******************************************************************/ 1018 | 1019 | 1020 | // TODO: check all touchevents 1021 | document.addEventListener('touchstart', function(evt) { 1022 | //console.log("Touch start"); 1023 | if ( !readOnly && evt.target.getAttribute('data-chalkboard') == mode ) { 1024 | // var ctx = drawingCanvas[mode].context; 1025 | var scale = drawingCanvas[mode].scale; 1026 | var xOffset = drawingCanvas[mode].xOffset; 1027 | var yOffset = drawingCanvas[mode].yOffset; 1028 | 1029 | evt.preventDefault(); 1030 | var touch = evt.touches[0]; 1031 | mouseX = touch.pageX; 1032 | mouseY = touch.pageY; 1033 | startDrawing( (mouseX - xOffset)/scale, (mouseY-yOffset)/scale, false ); 1034 | // broadcast 1035 | var message = new CustomEvent('send'); 1036 | message.content = { sender: 'chalkboard-plugin', type: 'startDrawing', x: (mouseX - xOffset)/scale, y: (mouseY-yOffset)/scale, erase: false }; 1037 | document.dispatchEvent( message ); 1038 | /* 1039 | xLast = mouseX; 1040 | yLast = mouseY; 1041 | event = { type: "draw", begin: Date.now() - slideStart, end: null, curve: [{x: (mouseX - xOffset)/scale, y: (mouseY-yOffset)/scale}] }; 1042 | */ 1043 | touchTimeout = setTimeout( showSponge, 500, mouseX, mouseY ); 1044 | } 1045 | }, passiveSupported ? {passive: false} : false); 1046 | 1047 | document.addEventListener('touchmove', function(evt) { 1048 | //console.log("Touch move"); 1049 | clearTimeout( touchTimeout ); 1050 | touchTimeout = null; 1051 | if ( event ) { 1052 | // var ctx = drawingCanvas[mode].context; 1053 | var scale = drawingCanvas[mode].scale; 1054 | var xOffset = drawingCanvas[mode].xOffset; 1055 | var yOffset = drawingCanvas[mode].yOffset; 1056 | 1057 | var touch = evt.touches[0]; 1058 | mouseX = touch.pageX; 1059 | mouseY = touch.pageY; 1060 | if (mouseY < drawingCanvas[mode].height && mouseX < drawingCanvas[mode].width) { 1061 | evt.preventDefault(); 1062 | // move sponge 1063 | if ( event.type == "erase" ) { 1064 | drawingCanvas[mode].sponge.style.left = (mouseX - eraser.radius) +"px" ; 1065 | drawingCanvas[mode].sponge.style.top = (mouseY - eraser.radius) +"px" ; 1066 | } 1067 | } 1068 | 1069 | drawSegment( (mouseX - xOffset)/scale, (mouseY-yOffset)/scale, ( event.type == "erase" ) ); 1070 | // broadcast 1071 | var message = new CustomEvent('send'); 1072 | message.content = { sender: 'chalkboard-plugin', type: 'drawSegment', x: (mouseX - xOffset)/scale, y: (mouseY-yOffset)/scale, erase: ( event.type == "erase" ) }; 1073 | document.dispatchEvent( message ); 1074 | /* 1075 | if (mouseY < drawingCanvas[mode].height && mouseX < drawingCanvas[mode].width) { 1076 | evt.preventDefault(); 1077 | event.curve.push({x: (mouseX - xOffset)/scale, y: (mouseY-yOffset)/scale}); 1078 | if ( event.type == "erase" ) { 1079 | drawingCanvas[mode].sponge.style.left = (mouseX - eraser.radius) +"px" ; 1080 | drawingCanvas[mode].sponge.style.top = (mouseY - eraser.radius) +"px" ; 1081 | eraseWithSponge(ctx, mouseX, mouseY); 1082 | } 1083 | else { 1084 | draw[mode](ctx, xLast, yLast, mouseX, mouseY); 1085 | } 1086 | xLast = mouseX; 1087 | yLast = mouseY; 1088 | } 1089 | */ 1090 | } 1091 | }, false); 1092 | 1093 | 1094 | document.addEventListener('touchend', function(evt) { 1095 | clearTimeout( touchTimeout ); 1096 | touchTimeout = null; 1097 | // hide sponge image 1098 | drawingCanvas[mode].sponge.style.visibility = "hidden"; 1099 | stopDrawing(); 1100 | // broadcast 1101 | var message = new CustomEvent('send'); 1102 | message.content = { sender: 'chalkboard-plugin', type: 'stopDrawing' }; 1103 | document.dispatchEvent( message ); 1104 | /* 1105 | if ( event ) { 1106 | event.end = Date.now() - slideStart; 1107 | if ( event.type == "erase" || event.curve.length > 1 ) { 1108 | // do not save a line with a single point only 1109 | recordEvent( event ); 1110 | } 1111 | event = null; 1112 | } 1113 | */ 1114 | }, false); 1115 | 1116 | document.addEventListener( 'mousedown', function( evt ) { 1117 | //console.log("Mouse down"); 1118 | //console.log( "Read only: " + readOnly ); 1119 | if ( !readOnly && evt.target.getAttribute('data-chalkboard') == mode ) { 1120 | //console.log( "mousedown: " + evt.button ); 1121 | // var ctx = drawingCanvas[mode].context; 1122 | var scale = drawingCanvas[mode].scale; 1123 | var xOffset = drawingCanvas[mode].xOffset; 1124 | var yOffset = drawingCanvas[mode].yOffset; 1125 | 1126 | mouseX = evt.pageX; 1127 | mouseY = evt.pageY; 1128 | startDrawing( (mouseX - xOffset)/scale, (mouseY-yOffset)/scale, ( evt.button == 2 || evt.button == 1) ); 1129 | // broadcast 1130 | var message = new CustomEvent('send'); 1131 | message.content = { sender: 'chalkboard-plugin', type: 'startDrawing', x: (mouseX - xOffset)/scale, y: (mouseY-yOffset)/scale, erase: ( evt.button == 2 || evt.button == 1) }; 1132 | document.dispatchEvent( message ); 1133 | /* 1134 | xLast = mouseX; 1135 | yLast = mouseY; 1136 | if ( evt.button == 2) { 1137 | event = { type: "erase", begin: Date.now() - slideStart, end: null, curve: [{x: (mouseX - xOffset)/scale, y: (mouseY-yOffset)/scale}]}; 1138 | drawingCanvas[mode].canvas.style.cursor = 'url("' + path + 'img/sponge.png") ' + eraser.radius + ' ' + eraser.radius + ', auto'; 1139 | eraseWithSponge(ctx,mouseX,mouseY); 1140 | } 1141 | else { 1142 | event = { type: "draw", begin: Date.now() - slideStart, end: null, curve: [{x: (mouseX - xOffset)/scale, y: (mouseY-yOffset)/scale}] }; 1143 | } 1144 | */ 1145 | } 1146 | } ); 1147 | 1148 | document.addEventListener( 'mousemove', function( evt ) { 1149 | //console.log("Mouse move"); 1150 | if ( event ) { 1151 | // var ctx = drawingCanvas[mode].context; 1152 | var scale = drawingCanvas[mode].scale; 1153 | var xOffset = drawingCanvas[mode].xOffset; 1154 | var yOffset = drawingCanvas[mode].yOffset; 1155 | 1156 | mouseX = evt.pageX; 1157 | mouseY = evt.pageY; 1158 | drawSegment( (mouseX - xOffset)/scale, (mouseY-yOffset)/scale, ( event.type == "erase" ) ); 1159 | // broadcast 1160 | var message = new CustomEvent('send'); 1161 | message.content = { sender: 'chalkboard-plugin', type: 'drawSegment', x: (mouseX - xOffset)/scale, y: (mouseY-yOffset)/scale, erase: ( event.type == "erase" ) }; 1162 | document.dispatchEvent( message ); 1163 | /* 1164 | event.curve.push({x: (mouseX - xOffset)/scale, y: (mouseY-yOffset)/scale}); 1165 | if(mouseY < drawingCanvas[mode].height && mouseX < drawingCanvas[mode].width) { 1166 | if ( event.type == "erase" ) { 1167 | eraseWithSponge(ctx,mouseX,mouseY); 1168 | } 1169 | else { 1170 | draw[mode](ctx, xLast, yLast, mouseX,mouseY); 1171 | } 1172 | xLast = mouseX; 1173 | yLast = mouseY; 1174 | } 1175 | */ 1176 | } 1177 | } ); 1178 | 1179 | 1180 | document.addEventListener( 'mouseup', function( evt ) { 1181 | drawingCanvas[mode].canvas.style.cursor = pens[mode][color[mode]].cursor; 1182 | if ( event ) { 1183 | stopDrawing(); 1184 | // broadcast 1185 | var message = new CustomEvent('send'); 1186 | message.content = { sender: 'chalkboard-plugin', type: 'stopDrawing' }; 1187 | document.dispatchEvent( message ); 1188 | /* if(evt.button == 2){ 1189 | } 1190 | event.end = Date.now() - slideStart; 1191 | if ( event.type == "erase" || event.curve.length > 1 ) { 1192 | // do not save a line with a single point only 1193 | recordEvent( event ); 1194 | } 1195 | event = null; 1196 | */ 1197 | } 1198 | } ); 1199 | 1200 | 1201 | window.addEventListener( "resize", function() { 1202 | //console.log("resize"); 1203 | // Resize the canvas and draw everything again 1204 | var timestamp = Date.now() - slideStart; 1205 | if ( !playback ) { 1206 | timestamp = getSlideDuration(); 1207 | } 1208 | 1209 | //console.log( drawingCanvas[0].scale + "/" + drawingCanvas[0].xOffset + "/" +drawingCanvas[0].yOffset ); 1210 | for (var id = 0; id < 2; id++ ) { 1211 | drawingCanvas[id].width = window.innerWidth; 1212 | drawingCanvas[id].height = window.innerHeight; 1213 | drawingCanvas[id].canvas.width = drawingCanvas[id].width; 1214 | drawingCanvas[id].canvas.height = drawingCanvas[id].height; 1215 | drawingCanvas[id].context.canvas.width = drawingCanvas[id].width; 1216 | drawingCanvas[id].context.canvas.height = drawingCanvas[id].height; 1217 | 1218 | drawingCanvas[id].scale = Math.min( drawingCanvas[id].width/storage[id].width, drawingCanvas[id].height/storage[id].height ); 1219 | drawingCanvas[id].xOffset = (drawingCanvas[id].width - storage[id].width * drawingCanvas[id].scale)/2; 1220 | drawingCanvas[id].yOffset = (drawingCanvas[id].height - storage[id].height * drawingCanvas[id].scale)/2; 1221 | //console.log( drawingCanvas[id].scale + "/" + drawingCanvas[id].xOffset + "/" +drawingCanvas[id].yOffset ); 1222 | } 1223 | //console.log( window.innerWidth + "/" + window.innerHeight); 1224 | startPlayback( timestamp, mode, true ); 1225 | 1226 | } ); 1227 | 1228 | function updateReadOnlyMode() { 1229 | //console.log("updateReadOnlyMode"); 1230 | if ( config.readOnly == undefined ) { 1231 | readOnly = ( getSlideDuration() > 0 ); 1232 | if ( readOnly ) { 1233 | drawingCanvas[0].container.style.cursor = 'default'; 1234 | drawingCanvas[1].container.style.cursor = 'default'; 1235 | drawingCanvas[0].canvas.style.cursor = 'default'; 1236 | drawingCanvas[1].canvas.style.cursor = 'default'; 1237 | if ( notescanvas.style.pointerEvents != "none" ) { 1238 | event = null; 1239 | notescanvas.style.background = 'rgba(0,0,0,0)'; 1240 | notescanvas.style.pointerEvents = "none"; 1241 | } 1242 | 1243 | } 1244 | else { 1245 | drawingCanvas[0].container.style.cursor = pens[0][color[0]].cursor; 1246 | drawingCanvas[1].container.style.cursor = pens[1][color[1]].cursor; 1247 | drawingCanvas[0].canvas.style.cursor = pens[0][color[0]].cursor; 1248 | drawingCanvas[1].canvas.style.cursor = pens[1][color[1]].cursor; 1249 | } 1250 | } 1251 | } 1252 | 1253 | Reveal.addEventListener( 'ready', function( evt ) { 1254 | //console.log('ready'); 1255 | if ( !printMode ) { 1256 | slideStart = Date.now(); 1257 | slideIndices = Reveal.getIndices(); 1258 | if ( !playback ) { 1259 | startPlayback( getSlideDuration(), 0 ); 1260 | } 1261 | if ( Reveal.isAutoSliding() ) { 1262 | var event = new CustomEvent('startplayback'); 1263 | event.timestamp = 0; 1264 | document.dispatchEvent( event ); 1265 | } 1266 | updateReadOnlyMode(); 1267 | } 1268 | else { 1269 | whenReady( createPrintout ); 1270 | } 1271 | }); 1272 | Reveal.addEventListener( 'slidechanged', function( evt ) { 1273 | // clearTimeout( slidechangeTimeout ); 1274 | //console.log('slidechanged'); 1275 | if ( !printMode ) { 1276 | slideStart = Date.now(); 1277 | slideIndices = Reveal.getIndices(); 1278 | closeChalkboard(); 1279 | clearCanvas( 0 ); 1280 | clearCanvas( 1 ); 1281 | if ( !playback ) { 1282 | slidechangeTimeout = setTimeout( startPlayback, transition, getSlideDuration(), 0 ); 1283 | } 1284 | if ( Reveal.isAutoSliding() ) { 1285 | var event = new CustomEvent('startplayback'); 1286 | event.timestamp = 0; 1287 | document.dispatchEvent( event ); 1288 | } 1289 | 1290 | updateReadOnlyMode(); 1291 | } 1292 | }); 1293 | Reveal.addEventListener( 'fragmentshown', function( evt ) { 1294 | // clearTimeout( slidechangeTimeout ); 1295 | //console.log('fragmentshown'); 1296 | if ( !printMode ) { 1297 | slideStart = Date.now(); 1298 | slideIndices = Reveal.getIndices(); 1299 | closeChalkboard(); 1300 | clearCanvas( 0 ); 1301 | clearCanvas( 1 ); 1302 | if ( Reveal.isAutoSliding() ) { 1303 | var event = new CustomEvent('startplayback'); 1304 | event.timestamp = 0; 1305 | document.dispatchEvent( event ); 1306 | } 1307 | else if ( !playback ) { 1308 | // 1309 | startPlayback( getSlideDuration(), 0 ); 1310 | // closeChalkboard(); 1311 | } 1312 | updateReadOnlyMode(); 1313 | } 1314 | }); 1315 | Reveal.addEventListener( 'fragmenthidden', function( evt ) { 1316 | // clearTimeout( slidechangeTimeout ); 1317 | //console.log('fragmenthidden'); 1318 | if ( !printMode ) { 1319 | slideStart = Date.now(); 1320 | slideIndices = Reveal.getIndices(); 1321 | closeChalkboard(); 1322 | clearCanvas( 0 ); 1323 | clearCanvas( 1 ); 1324 | if ( Reveal.isAutoSliding() ) { 1325 | document.dispatchEvent( new CustomEvent('stopplayback') ); 1326 | } 1327 | else if ( !playback ) { 1328 | startPlayback( getSlideDuration() ); 1329 | closeChalkboard(); 1330 | } 1331 | updateReadOnlyMode(); 1332 | } 1333 | }); 1334 | 1335 | Reveal.addEventListener( 'autoslideresumed', function( evt ) { 1336 | //console.log('autoslideresumed'); 1337 | var event = new CustomEvent('startplayback'); 1338 | event.timestamp = 0; 1339 | document.dispatchEvent( event ); 1340 | }); 1341 | Reveal.addEventListener( 'autoslidepaused', function( evt ) { 1342 | //console.log('autoslidepaused'); 1343 | document.dispatchEvent( new CustomEvent('stopplayback') ); 1344 | 1345 | // advance to end of slide 1346 | // closeChalkboard(); 1347 | startPlayback( getSlideDuration(), 0 ); 1348 | }); 1349 | 1350 | function toggleNotesCanvas() { 1351 | if ( !readOnly ) { 1352 | if ( mode == 1 ) { 1353 | toggleChalkboard(); 1354 | notescanvas.style.background = background[0]; //'rgba(255,0,0,0.5)'; 1355 | notescanvas.style.pointerEvents = "auto"; 1356 | } 1357 | else { 1358 | if ( notescanvas.style.pointerEvents != "none" ) { 1359 | event = null; 1360 | notescanvas.style.background = 'rgba(0,0,0,0)'; 1361 | notescanvas.style.pointerEvents = "none"; 1362 | } 1363 | else { 1364 | setColor(0); 1365 | recordEvent( { type:"setcolor", index: 0, begin: Date.now() - slideStart } ); 1366 | if (color[mode]) { 1367 | let idx = color[mode]; 1368 | setColor(idx); 1369 | recordEvent( { type:"setcolor", index: idx, begin: Date.now() - slideStart } ); 1370 | } else { 1371 | color[mode] = 0; 1372 | } 1373 | notescanvas.style.background = background[0]; //'rgba(255,0,0,0.5)'; 1374 | notescanvas.style.pointerEvents = "auto"; 1375 | } 1376 | } 1377 | } 1378 | }; 1379 | 1380 | function toggleChalkboard() { 1381 | //console.log("toggleChalkboard " + mode); 1382 | if ( mode == 1 ) { 1383 | event = null; 1384 | if ( !readOnly ) { 1385 | recordEvent( { type:"close", begin: Date.now() - slideStart } ); 1386 | } 1387 | closeChalkboard(); 1388 | } 1389 | else { 1390 | showChalkboard(); 1391 | if ( !readOnly ) { 1392 | recordEvent( { type:"open", begin: Date.now() - slideStart } ); 1393 | setColor(0); 1394 | recordEvent( { type:"setcolor", index: 0, begin: Date.now() - slideStart } ); 1395 | if (rememberColor[mode]) { 1396 | let idx = color[mode]; 1397 | setColor(idx); 1398 | recordEvent( { type:"setcolor", index: idx, begin: Date.now() - slideStart } ); 1399 | } else { 1400 | color[mode] = 0; 1401 | } 1402 | } 1403 | } 1404 | }; 1405 | 1406 | function clear() { 1407 | if ( !readOnly ) { 1408 | recordEvent( { type:"clear", begin: Date.now() - slideStart } ); 1409 | clearCanvas( mode ); 1410 | // broadcast 1411 | var message = new CustomEvent('send'); 1412 | message.content = { sender: 'chalkboard-plugin', type: 'clear' }; 1413 | document.dispatchEvent( message ); 1414 | } 1415 | }; 1416 | 1417 | function colorNext() { 1418 | if ( !readOnly ) { 1419 | let idx = cycleColorNext(); 1420 | setColor(idx); 1421 | recordEvent( { type: "setcolor", index: idx, begin: Date.now() - slideStart } ); 1422 | // broadcast 1423 | var message = new CustomEvent('send'); 1424 | message.content = { sender: 'chalkboard-plugin', type: 'setcolor', index: idx }; 1425 | document.dispatchEvent( message ); 1426 | } 1427 | } 1428 | 1429 | function colorPrev() { 1430 | if ( !readOnly ) { 1431 | let idx = cycleColorPrev(); 1432 | setColor(idx); 1433 | recordEvent( { type: "setcolor", index: idx, begin: Date.now() - slideStart } ); 1434 | // broadcast 1435 | var message = new CustomEvent('send'); 1436 | message.content = { sender: 'chalkboard-plugin', type: 'setcolor', index: idx }; 1437 | document.dispatchEvent( message ); 1438 | } 1439 | } 1440 | 1441 | function resetSlide( force ) { 1442 | var ok = force || confirm("Please confirm to delete chalkboard drawings on this slide!"); 1443 | if ( ok ) { 1444 | //console.log("resetSlide "); 1445 | stopPlayback(); 1446 | slideStart = Date.now(); 1447 | event = null; 1448 | closeChalkboard(); 1449 | 1450 | clearCanvas( 0 ); 1451 | clearCanvas( 1 ); 1452 | 1453 | mode = 1; 1454 | var slideData = getSlideData(); 1455 | slideData.duration = 0; 1456 | slideData.events = []; 1457 | mode = 0; 1458 | var slideData = getSlideData(); 1459 | slideData.duration = 0; 1460 | slideData.events = []; 1461 | 1462 | updateReadOnlyMode(); 1463 | // broadcast 1464 | var message = new CustomEvent('send'); 1465 | message.content = { sender: 'chalkboard-plugin', type: 'resetSlide' }; 1466 | document.dispatchEvent( message ); 1467 | } 1468 | }; 1469 | 1470 | function resetStorage( force ) { 1471 | var ok = force || confirm("Please confirm to delete all chalkboard drawings!"); 1472 | if ( ok ) { 1473 | stopPlayback(); 1474 | slideStart = Date.now(); 1475 | clearCanvas( 0 ); 1476 | clearCanvas( 1 ); 1477 | if ( mode == 1 ) { 1478 | event = null; 1479 | closeChalkboard(); 1480 | } 1481 | storage = [ 1482 | { width: drawingCanvas[0].width - 2 * drawingCanvas[0].xOffset, height: drawingCanvas[0].height - 2 * drawingCanvas[0].yOffset, data: []}, 1483 | { width: drawingCanvas[1].width, height: drawingCanvas[1].height, data: []} 1484 | ]; 1485 | 1486 | updateReadOnlyMode(); 1487 | // broadcast 1488 | var message = new CustomEvent('send'); 1489 | message.content = { sender: 'chalkboard-plugin', type: 'init', storage: storage, mode: mode }; 1490 | document.dispatchEvent( message ); 1491 | } 1492 | }; 1493 | 1494 | this.drawWithBoardmarker = drawWithBoardmarker; 1495 | this.drawWithChalk = drawWithChalk; 1496 | this.toggleNotesCanvas = toggleNotesCanvas; 1497 | this.toggleChalkboard = toggleChalkboard; 1498 | this.startRecording = startRecording; 1499 | this.clear = clear; 1500 | this.colorNext = colorNext; 1501 | this.colorPrev = colorPrev; 1502 | this.reset = resetSlide; 1503 | this.resetAll = resetStorage; 1504 | this.download = downloadData; 1505 | this.configure = configure; 1506 | 1507 | return this; 1508 | })(); 1509 | --------------------------------------------------------------------------------