├── requirements.txt ├── imgs └── in_action.gif ├── postBuild ├── pizarra-nb ├── pizarra.yaml ├── sketchpad.js └── pizarra.js ├── LICENSE ├── .gitignore └── README.md /requirements.txt: -------------------------------------------------------------------------------- 1 | notebook 2 | -------------------------------------------------------------------------------- /imgs/in_action.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kikocorreoso/pizarra-nb/HEAD/imgs/in_action.gif -------------------------------------------------------------------------------- /postBuild: -------------------------------------------------------------------------------- 1 | jupyter nbextension install pizarra-nb --user 2 | jupyter nbextension enable pizarra-nb/pizarra 3 | -------------------------------------------------------------------------------- /pizarra-nb/pizarra.yaml: -------------------------------------------------------------------------------- 1 | Type: Jupyter Notebook Extension 2 | Name: Pizarra 3 | Description: Adds a button to add draw tools on the selected markdown or code cell 4 | Link: README.md 5 | Main: pizarra.js 6 | Compatibility: 5.x -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 kikocorreoso 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 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | *.egg-info/ 24 | .installed.cfg 25 | *.egg 26 | MANIFEST 27 | 28 | # PyInstaller 29 | # Usually these files are written by a python script from a template 30 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 31 | *.manifest 32 | *.spec 33 | 34 | # Installer logs 35 | pip-log.txt 36 | pip-delete-this-directory.txt 37 | 38 | # Unit test / coverage reports 39 | htmlcov/ 40 | .tox/ 41 | .coverage 42 | .coverage.* 43 | .cache 44 | nosetests.xml 45 | coverage.xml 46 | *.cover 47 | .hypothesis/ 48 | .pytest_cache/ 49 | 50 | # Translations 51 | *.mo 52 | *.pot 53 | 54 | # Django stuff: 55 | *.log 56 | local_settings.py 57 | db.sqlite3 58 | 59 | # Flask stuff: 60 | instance/ 61 | .webassets-cache 62 | 63 | # Scrapy stuff: 64 | .scrapy 65 | 66 | # Sphinx documentation 67 | docs/_build/ 68 | 69 | # PyBuilder 70 | target/ 71 | 72 | # Jupyter Notebook 73 | .ipynb_checkpoints 74 | 75 | # pyenv 76 | .python-version 77 | 78 | # celery beat schedule file 79 | celerybeat-schedule 80 | 81 | # SageMath parsed files 82 | *.sage.py 83 | 84 | # Environments 85 | .env 86 | .venv 87 | env/ 88 | venv/ 89 | ENV/ 90 | env.bak/ 91 | venv.bak/ 92 | 93 | # Spyder project settings 94 | .spyderproject 95 | .spyproject 96 | 97 | # Rope project settings 98 | .ropeproject 99 | 100 | # mkdocs documentation 101 | /site 102 | 103 | # mypy 104 | .mypy_cache/ 105 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Pizarra-nb 2 | ========== 3 | 4 | [![Binder](https://mybinder.org/badge_logo.svg)](https://mybinder.org/v2/gh/kikocorreoso/pizarra-nb/master) 5 | 6 | This is a Jupyter notebook extension that creates a canvas of a cell and allows 7 | to paint onto the contents so it could be used to explain some concepts in an 8 | interactive way without the need to use a blackboard. 9 | 10 | See an example below: 11 | 12 | ![example](imgs/in_action.gif) 13 | 14 | THIS IS TESTED ONLY IN FIREFOX WITH THE LATEST VERSION OF NOTEBOOK. USE IT 15 | AT YOUR OWN RISK. 16 | 17 | What is 'pizarra'? 18 | ------------------ 19 | 20 | Pizarra is the spanish word for blackboard, whiteboard, greenboard. 21 | 22 | Motivation 23 | ---------- 24 | 25 | I have to give workshops, lessons, classes,..., in some of them there isn't 26 | a blackboard available to explain some concepts so, why not something available 27 | from the notebook itself? 28 | 29 | Features 30 | -------- 31 | 32 | You can use: 33 | 34 | * a pen to draw over the cell, you can select the size, color and transparency. 35 | * you can undo an action. 36 | * you can redo an action. 37 | * you can save the result of the cell and your drawings as a png file in your disk drive. 38 | * you can save the result of the cell and your drawings as an image included in a new cell below the used cell. 39 | * you can reset the canvas so you can start again. 40 | 41 | How it works? 42 | ------------- 43 | 44 | After clicking the toolbar button (see video above) the selected cell is 45 | transformed to a HTML5 canvas using html2canvas (see next section). This 46 | canvas is included in a HTML div element and some functionality is added 47 | with the help of a modified version of sketchpad (see *Third party libs used* section). 48 | The final result is shown in a dialog where you can interact with the converted 49 | cell. 50 | 51 | I've tested images, latex formulas, code cells, markdown cells,... If it is 52 | in the notebook it seems is working. If you use, for instance, an image 53 | from other server it will not work. 54 | 55 | Potential improvements 56 | ---------------------- 57 | 58 | Open an issue indicating a functionality that could be useful for your usecase. 59 | Some ideas: 60 | 61 | * The possibility to write text. 62 | * The possibility to use arrows (single arrow and double arrow). 63 | * The possibility to draw squares/rectangles. 64 | * The possibility to draw circles/ellipses. 65 | * ... 66 | 67 | Third party libs used 68 | --------------------- 69 | 70 | * [html2canvas](http://html2canvas.hertzen.com/) 71 | * A modified version of [Sketchpad](http://yiom.github.io/sketchpad/) 72 | 73 | References used 74 | --------------- 75 | 76 | * [https://www.stefaanlippens.net/jupyter-notebook-dialog.html](https://www.stefaanlippens.net/jupyter-notebook-dialog.html) 77 | 78 | * [https://reference.codeproject.com/book/dom/canvas_api/drawing_dom_objects_into_a_canvas](https://reference.codeproject.com/book/dom/canvas_api/drawing_dom_objects_into_a_canvas) 79 | 80 | * [https://jsfiddle.net/codepo8/V6ufG/2/](https://jsfiddle.net/codepo8/V6ufG/2/) 81 | -------------------------------------------------------------------------------- /pizarra-nb/sketchpad.js: -------------------------------------------------------------------------------- 1 | define(function() { 2 | return class Sketchpad { 3 | constructor(options) { 4 | // Support both old api (element) and new (canvas) 5 | options.canvas = options.canvas || options.element; 6 | if (!options.canvas) { 7 | console.error('[SKETCHPAD]: Please provide an element/canvas:'); 8 | return; 9 | } 10 | 11 | if (typeof options.canvas === 'string') { 12 | options.canvas = document.querySelector(options.canvas); 13 | } 14 | 15 | this.canvas = options.canvas; 16 | 17 | // Try to extract 'width', 'height', 'color', 'penSize' and 'alpha' 18 | // from the options or the DOM element. 19 | ['width', 'height', 'color', 'penSize', 'alpha'].forEach(function(attr) { 20 | this[attr] = options[attr] || this.canvas.getAttribute('data-' + attr); 21 | }, this); 22 | 23 | // Setting default values 24 | this.width = this.width || 0; 25 | this.height = this.height || 0; 26 | 27 | this.color = this.color || '#aaa'; 28 | this.penSize = this.penSize || 5; 29 | this.alpha = this.alpha || 1; 30 | 31 | // Sketchpad History settings 32 | this.strokes = options.strokes || []; 33 | 34 | this.undoHistory = options.undoHistory || []; 35 | 36 | // Enforce context for Moving Callbacks 37 | this.onMouseMove = this.onMouseMove.bind(this); 38 | 39 | // Setup Internal Events 40 | this.events = {}; 41 | this.events['mousemove'] = []; 42 | this.internalEvents = ['MouseDown', 'MouseUp', 'MouseOut']; 43 | this.internalEvents.forEach(function(name) { 44 | let lower = name.toLowerCase(); 45 | this.events[lower] = []; 46 | 47 | // Enforce context for Internal Event Functions 48 | this['on' + name] = this['on' + name].bind(this); 49 | 50 | // Add DOM Event Listeners 51 | this.canvas.addEventListener(lower, (...args) => this.trigger(lower, args)); 52 | }, this); 53 | this._bg = this.canvas.getContext('2d').getImageData(0, 0, this.width, this.height); 54 | this.reset(); 55 | } 56 | 57 | /* 58 | * Private API 59 | */ 60 | 61 | _position(event) { 62 | var bounds = this.canvas.getBoundingClientRect(); 63 | var pos = { 64 | x: event.pageX - bounds.x, 65 | y: event.pageY - bounds.y, 66 | }; 67 | return pos; 68 | } 69 | 70 | _stroke(stroke) { 71 | if (stroke.type === 'clear') { 72 | return this.context.clearRect(0, 0, this.canvas.width, this.canvas.height); 73 | } 74 | 75 | stroke.lines.forEach(function(line) { 76 | this._line(line.start, line.end, stroke.color, stroke.size); 77 | }, this); 78 | } 79 | 80 | _draw(start, end, color, size) { 81 | this._line(start, end, color, size, 'source-over'); 82 | } 83 | 84 | _erase(start, end, color, size) { 85 | this._line(start, end, color, size, 'destination-out'); 86 | } 87 | 88 | _line(start, end, color, size, compositeOperation) { 89 | this.context.save(); 90 | this.context.lineJoin = 'round'; 91 | this.context.lineCap = 'round'; 92 | this.context.strokeStyle = color; 93 | this.context.lineWidth = size; 94 | this.context.globalCompositeOperation = compositeOperation; 95 | this.context.beginPath(); 96 | this.context.moveTo(start.x, start.y); 97 | this.context.lineTo(end.x, end.y); 98 | this.context.closePath(); 99 | this.context.stroke(); 100 | this.context.restore(); 101 | } 102 | 103 | // adapted from https://stackoverflow.com/a/21648508 104 | _torgba(hex, alpha) { 105 | var c; 106 | if(/^#([A-Fa-f0-9]{3}){1,2}$/.test(hex)) { 107 | c = hex.substring(1).split(''); 108 | if(c.length== 3) { 109 | c = [c[0], c[0], c[1], c[1], c[2], c[2]]; 110 | } 111 | c = '0x'+c.join(''); 112 | return 'rgba('+[(c>>16)&255, (c>>8)&255, c&255, alpha].join(',')+')'; 113 | } 114 | throw new Error('Bad Hex'); 115 | } 116 | 117 | /* 118 | * Events/Callback 119 | */ 120 | 121 | onMouseDown(event) { 122 | this._sketching = true; 123 | this._lastPosition = this._position(event); 124 | this._currentStroke = { 125 | color: this._torgba(this.color, this.alpha), 126 | size: this.penSize, 127 | lines: [], 128 | }; 129 | 130 | this.canvas.addEventListener('mousemove', this.onMouseMove); 131 | } 132 | 133 | onMouseUp(event) { 134 | if (this._sketching) { 135 | this.strokes.push(this._currentStroke); 136 | this._sketching = false; 137 | } 138 | 139 | this.canvas.removeEventListener('mousemove', this.onMouseMove); 140 | } 141 | 142 | onMouseOut(event) { 143 | this.onMouseUp(event); 144 | } 145 | 146 | onMouseMove(event) { 147 | let currentPosition = this._position(event); 148 | this._draw( 149 | this._lastPosition, 150 | currentPosition, 151 | this._torgba(this.color, this.alpha), 152 | this.penSize 153 | ); 154 | this._currentStroke.lines.push({ 155 | start: this._lastPosition, 156 | end: currentPosition, 157 | }); 158 | this._lastPosition = currentPosition; 159 | 160 | this.trigger('mousemove', [event]); 161 | } 162 | 163 | /* 164 | * Public API 165 | */ 166 | 167 | toObject() { 168 | return { 169 | width: this.canvas.width, 170 | height: this.canvas.height, 171 | strokes: this.strokes, 172 | undoHistory: this.undoHistory, 173 | }; 174 | } 175 | 176 | toJSON() { 177 | return JSON.stringify(this.toObject()); 178 | } 179 | 180 | toPNG() { 181 | return this.canvas.toDataURL(); 182 | } 183 | 184 | redo() { 185 | var stroke = this.undoHistory.pop(); 186 | if (stroke) { 187 | this.strokes.push(stroke); 188 | this._stroke(stroke); 189 | } 190 | } 191 | 192 | undo() { 193 | this.context.clearRect(0, 0, this.canvas.width, this.canvas.height); 194 | this.context.putImageData(this._bg, 0, 0); 195 | var stroke = this.strokes.pop(); 196 | this.redraw(); 197 | 198 | if (stroke) { 199 | this.undoHistory.push(stroke); 200 | } 201 | } 202 | 203 | redraw() { 204 | this.strokes.forEach(function(stroke) { 205 | this._stroke(stroke); 206 | }, this); 207 | } 208 | 209 | reset() { 210 | // Setup canvas 211 | this.canvas.width = this.width; 212 | this.canvas.height = this.height; 213 | this.context = this.canvas.getContext('2d'); 214 | 215 | // Redraw image 216 | this.context.clearRect(0, 0, this.canvas.width, this.canvas.height); 217 | this.context.putImageData(this._bg, 0, 0); 218 | this.strokes = []; 219 | this.undoHistory = []; 220 | 221 | // Attach all event listeners 222 | this.internalEvents.forEach(name => this.on(name.toLowerCase(), this['on' + name])); 223 | 224 | } 225 | 226 | cancelAnimation() { 227 | this.animateIds = this.animateIds || []; 228 | this.animateIds.forEach(function(id) { 229 | clearTimeout(id); 230 | }); 231 | this.animateIds = []; 232 | } 233 | 234 | animate(interval=10, loop=false, loopInterval=0) { 235 | let delay = interval; 236 | 237 | this.cancelAnimation(); 238 | this.context.clearRect(0, 0, this.canvas.width, this.canvas.height); 239 | 240 | this.strokes.forEach(stroke => { 241 | if (stroke.type === 'clear') { 242 | delay += interval; 243 | return this.animateIds.push(setTimeout(() => { 244 | this.context.clearRect(0, 0, this.canvas.width, 245 | this.canvas.height); 246 | }, delay)); 247 | } 248 | 249 | stroke.lines.forEach(line => { 250 | delay += interval; 251 | this.animateIds.push(setTimeout(() => { 252 | this._draw(line.start, line.end, stroke.color, stroke.size); 253 | }, delay)); 254 | }); 255 | }); 256 | 257 | if (loop) { 258 | this.animateIds.push(setTimeout(() => { 259 | this.animate(interval=10, loop, loopInterval); 260 | }, delay + interval + loopInterval)); 261 | } 262 | 263 | this.animateIds(setTimeout(() => { 264 | this.trigger('animation-end', [interval, loop, loopInterval]); 265 | }, delay + interval)); 266 | } 267 | 268 | /* 269 | * Event System 270 | */ 271 | 272 | /* Attach an event callback 273 | * 274 | * @param {String} action Which action will have a callback attached 275 | * @param {Function} callback What will be executed when this event happen 276 | */ 277 | on(action, callback) { 278 | // Tell the user if the action he has input was invalid 279 | if (this.events[action] === undefined) { 280 | return console.error(`Sketchpad: No such action '${action}'`); 281 | } 282 | 283 | this.events[action].push(callback); 284 | } 285 | 286 | /* Detach an event callback 287 | * 288 | * @param {String} action Which action will have event(s) detached 289 | * @param {Function} callback Which function will be detached. If none is 290 | * provided, all callbacks are detached 291 | */ 292 | off(action, callback) { 293 | if (callback) { 294 | // If a callback has been specified delete it specifically 295 | var index = this.events[action].indexOf(callback); 296 | (index !== -1) && this.events[action].splice(index, 1); 297 | return index !== -1; 298 | } 299 | 300 | // Else just erase all callbacks 301 | this.events[action] = []; 302 | } 303 | 304 | /* Trigger an event 305 | * 306 | * @param {String} action Which event will be triggered 307 | * @param {Array} args Which arguments will be provided to the callbacks 308 | */ 309 | trigger(action, args=[]) { 310 | // Fire all events with the given callback 311 | this.events[action].forEach(function(callback) { 312 | callback(...args); 313 | }); 314 | } 315 | } 316 | }); 317 | 318 | -------------------------------------------------------------------------------- /pizarra-nb/pizarra.js: -------------------------------------------------------------------------------- 1 | /* 2 | * This is a Jupyter Notebook Extension that adds drawing capabilities to 3 | * notebooks. 4 | * This is, more or less, similar to the live drawing capabilities available 5 | * on Powerpoint during live presentations. 6 | * https://support.office.com/en-us/article/Draw-on-slides-during-a-presentation-80a78a11-cb5d-4dfc-a1ad-a26e877da770 7 | */ 8 | 9 | define([ 10 | 'jquery', 11 | 'base/js/namespace', 12 | 'base/js/dialog', 13 | './html2canvas', 14 | './sketchpad' 15 | ], function ( 16 | $, 17 | Jupyter, 18 | Dialog, 19 | h2c, 20 | sp 21 | ) { 22 | "use strict"; 23 | var modal_width; 24 | 25 | var initialize = function() { 26 | Jupyter.toolbar.add_buttons_group([ 27 | Jupyter.keyboard_manager.actions.register( 28 | { 29 | help : 'Open Pizarra and draw tools', 30 | icon : 'fa-paint-brush', 31 | handler: handler 32 | }, 33 | 'draw-on-notebook', 34 | 'pizarra-nb' 35 | ) 36 | ]) 37 | }; 38 | 39 | var get_html = function() { 40 | var cell = Jupyter.notebook.get_selected_cell(); 41 | var w = cell.element.width(); 42 | var h = cell.element.height(); 43 | var element = document.getElementsByClassName("selected")[0]; 44 | var header = "Pizarra-nb"; 45 | 46 | return {header: header, 47 | element: element, 48 | width: w, 49 | height: h}; 50 | }; 51 | 52 | // adapted from https://stackoverflow.com/a/21648508 53 | var hexToRgbA = function(hex, alpha){ 54 | var c; 55 | if(/^#([A-Fa-f0-9]{3}){1,2}$/.test(hex)){ 56 | c= hex.substring(1).split(''); 57 | if(c.length== 3){ 58 | c= [c[0], c[0], c[1], c[1], c[2], c[2]]; 59 | } 60 | c= '0x'+c.join(''); 61 | return 'rgba('+[(c>>16)&255, (c>>8)&255, c&255, alpha].join(',')+')'; 62 | } 63 | throw new Error('Bad Hex'); 64 | }; 65 | 66 | var create_div = function(width, height, canvas, pizarra) { 67 | // Main div to be included in the modal. 68 | // The div contains an area for the canvas and an area for the controls 69 | var div_main = document.createElement("div"); 70 | div_main.width = width + 2; 71 | div_main.height = height + 52; 72 | // div containing the html transformed to canvas using html2canvas 73 | var div_canvas = document.createElement("div"); 74 | div_canvas.width = width + 1; 75 | div_canvas.height = height + 1; 76 | div_canvas.style = "text-align: center"; 77 | div_canvas.appendChild(canvas); 78 | div_main.appendChild(div_canvas); 79 | // div containing the controls to draw on the canvas 80 | var div_tools = document.createElement("div"); 81 | div_tools.classList.add("form-inline"); 82 | div_main.appendChild(div_tools); 83 | // Button to undo an action (included in div_tools) 84 | var btn = document.createElement("button"); 85 | btn.innerHTML = "undo"; 86 | btn.onclick = undo; 87 | btn.classList.add("btn"); 88 | btn.classList.add("btn-default"); 89 | div_tools.appendChild(btn); 90 | // Button to redo an action (included in div_tools) 91 | var btn = document.createElement("button"); 92 | btn.innerHTML = "redo"; 93 | btn.onclick = redo; 94 | btn.classList.add("btn"); 95 | btn.classList.add("btn-default"); 96 | div_tools.appendChild(btn); 97 | // select element to choose the tool to be used to paint (included in div_tools) 98 | var slt = document.createElement("select"); 99 | slt.classList.add("span2"); 100 | slt.name = "category"; 101 | slt.style.fontFamily = "font-family: sans-serif, 'FontAwesome'"; 102 | var slt_options = { 103 | "brush": " Brush", 104 | "rectangle": " Rectangle", 105 | "circle": " Circle", 106 | "arrow": " Arrow", 107 | "arrows": " Arrows", 108 | }; 109 | for (var opt in slt_options){ 110 | var option = document.createElement('option'); 111 | option.value = opt; 112 | option.innerHTML = slt_options[opt]; 113 | slt.appendChild(option); 114 | }; 115 | slt.disabled = "disabled"; ///// At this moment this control is disabled as only the brush/pen is available 116 | div_tools.appendChild(slt); 117 | // label and input range for pen/text width/size (included in div_tools) 118 | var lab = document.createElement("label"); 119 | lab.for = "width-range"; 120 | lab.innerHTML = "Width:"; 121 | div_tools.appendChild(lab); 122 | var div = document.createElement("div"); 123 | div.classList.add("form-group"); 124 | div.style.border = "1px solid #888"; 125 | var input = document.createElement("input"); 126 | input.type = "range"; 127 | input.classList.add("form-control"); 128 | input.id = "width-range"; 129 | input.style.width = "100px"; 130 | input.value = "5"; 131 | input.min = "1"; 132 | input.max = "50"; 133 | input.addEventListener("change", size); 134 | div.appendChild(input); 135 | div_tools.appendChild(div); 136 | // label and input range for pen transparency (included in div_tools) 137 | var lab = document.createElement("label"); 138 | lab.for = "alpha-range"; 139 | lab.innerHTML = "Alpha:"; 140 | div_tools.appendChild(lab); 141 | var div = document.createElement("div"); 142 | div.classList.add("form-group"); 143 | div.style.border = "1px solid #888"; 144 | var input = document.createElement("input"); 145 | input.type = "range"; 146 | input.classList.add("form-control"); 147 | input.id = "alpha-range"; 148 | input.style.width = "100px"; 149 | input.value = "1"; 150 | input.min = "0"; 151 | input.max = "1"; 152 | input.step = "0.05"; 153 | input.addEventListener("change", transparency); 154 | div.appendChild(input); 155 | div_tools.appendChild(div); 156 | // input color to get a the color to draw (included in div_tools) 157 | var input = document.createElement("input"); 158 | input.id = "color_picker"; 159 | input.type = "color"; 160 | input.value = "#aaaaaa"; 161 | input.addEventListener("change", color); 162 | div_tools.appendChild(input); 163 | // link button to save to png (included in div_tools) 164 | var link = document.createElement('a'); 165 | link.innerHTML = 'save as png'; 166 | link.classList.add("btn"); 167 | link.classList.add("btn-default"); 168 | link.role = "button"; 169 | link.onclick = save_png; 170 | link.download = "cell_result.png"; 171 | div_tools.appendChild(link); 172 | // button to save to cell below (included in div_tools) 173 | var btn = document.createElement("button"); 174 | btn.innerHTML = "save to cell below"; 175 | btn.onclick = save_cell; 176 | btn.classList.add("btn"); 177 | btn.classList.add("btn-default"); 178 | div_tools.appendChild(btn); 179 | // button to clear the included modifications (included in div_tools) 180 | var btn = document.createElement("button"); 181 | btn.innerHTML = "reset"; 182 | btn.onclick = reset; 183 | btn.classList.add("btn"); 184 | btn.classList.add("btn-default"); 185 | div_tools.appendChild(btn); 186 | 187 | function undo() { 188 | pizarra.undo(); 189 | }; 190 | function redo() { 191 | pizarra.redo(); 192 | }; 193 | function color(event) { 194 | pizarra.color = $(event.target).val(); 195 | }; 196 | function size(event) { 197 | pizarra.penSize = $(event.target).val(); 198 | }; 199 | function transparency(event) { 200 | pizarra.alpha = $(event.target).val(); 201 | }; 202 | function save_png(event) { 203 | $(event.target).attr("href", pizarra.toPNG()); 204 | }; 205 | function save_cell() { 206 | var result = pizarra.toPNG(); 207 | Jupyter.notebook.insert_cell_below("markdown"); 208 | var next_cell = Jupyter.notebook.get_next_cell(); 209 | next_cell.set_text(''); 210 | next_cell.execute(); 211 | }; 212 | function reset() { 213 | pizarra.reset(); 214 | }; 215 | /* 216 | function animateSketchpad() { 217 | pizarra.animate(10); 218 | }; 219 | */ 220 | return div_main; 221 | 222 | }; 223 | 224 | var start_the_magic = function() { 225 | // get current cell data (HTML stuff) 226 | var result = get_html(); 227 | var options = {width: result.width, height: result.height}; 228 | // Here HTML is converted to canvas 229 | h2c(result.element, options).then(canvas => { 230 | // In the promise 231 | 232 | // Create a sketchpad with the html converted 233 | var pizarra = new sp({ 234 | canvas: canvas, 235 | width: result.width, 236 | height: result.height 237 | }); 238 | 239 | // Create div with canvas and tools 240 | var main_div = create_div( 241 | options.width, 242 | options.height, 243 | canvas, 244 | pizarra 245 | ); 246 | 247 | var modal = Dialog.modal({ 248 | title: "Pizarra-nb", 249 | body: main_div, 250 | buttons: { 251 | 'Close': {} 252 | }, 253 | //sanitize: false 254 | }); 255 | modal.children().width(result.width + 40) 256 | }); 257 | }; 258 | 259 | var handler = function() { 260 | start_the_magic(); 261 | }; 262 | 263 | function load_jupyter_extension () { 264 | return Jupyter.notebook.config.loaded.then(initialize); 265 | } 266 | 267 | return { 268 | load_jupyter_extension: load_jupyter_extension, 269 | load_ipython_extension: load_jupyter_extension 270 | }; 271 | }); --------------------------------------------------------------------------------