-
Notifications
You must be signed in to change notification settings - Fork 2
/
Copy pathmain.js
2106 lines (1884 loc) · 95 KB
/
main.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
// Notate: Jupyter Notebook Extension for writing/drawing inside an iPython notebook.
// Copyright (C) 2021-2022 Ian Arawjo
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Lesser General Public License v3 as published by
// the Free Software Foundation.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Lesser General Public License v3 for more details.
//
// You should have received a copy of the GNU Lesser General Public License v3
// along with this program. If not, see <https://www.gnu.org/licenses/lgpl-3.0.en.html>.
// Logging infrastructure
var Logger = (function() {
const ALSO_PRINT_TO_CONSOLE = true;
let _log = [];
return {
log: function(evtname, info) {
if (ALSO_PRINT_TO_CONSOLE) console.log(evtname, info);
_log.push([Date.now().toString(), evtname, info]);
},
logCodeCellChange : function(event) {
let data = "";
if (event.removed.length > 0 && (event.removed[0].length > 0 || event.removed.length > 1))
data = "-:"+Date.now().toString()+":"+event.removed.join('\n')+':'+event.from.line+','+event.from.ch+':'+event.to.line+","+event.to.ch;
if (event.text.length > 0 && (event.text[0].length > 0 || event.text.length > 1))
data = "+:"+Date.now().toString()+":"+event.text.join('\n')+':'+event.from.line+','+event.from.ch+':'+event.to.line+","+event.to.ch;
_log.push(data);
},
lastLogType: function() {
if (_log.length === 0) return "event";
return typeof _log[_log.length-1] === "string" ? "code_edit" : "event";
},
getData: function() {
return _log;
},
clear: function() {
_log = [];
return true;
},
clearCache: function() {
_log = [];
const cells = Jupyter.notebook.get_cells();
if (cells.length > 0) {
// Append general log
if ('log' in cells[0].metadata)
cells[0].metadata['log'] = [];
}
return true;
}
}
}());
define([
'require',
'jquery',
'base/js/namespace',
'base/js/events',
'notebook/js/codecell',
'nbextensions/notate-jupyter/libs/spectrum.min',
], function(requirejs, $, Jupyter, events, codecell) {
// The Python code silectly injected when cells are run:
const PYCODE_SETUP = `
import base64
import numpy as np
from io import BytesIO
from PIL import Image
class NotateArray(np.ndarray):
def __new__(cls, input_array, locals=None):
obj = np.asarray(input_array).view(cls)
obj.locals = locals
return obj
def __array_finalize__(self, obj):
if obj is None: return
self.locals = getattr(obj, 'locals', None)
`;
// For keeping track of canvases and copy-paste function:
var canvases = {};
var CodeCell = codecell.CodeCell;
// Web socket for sending canvas data on change
var socket;
// var socket = new NotateWebSocket();
// Adds a cell above current cell (will be top if no cells)
var add_cell = function() {
Jupyter.notebook.insert_cell_above('code').set_text("");
Jupyter.notebook.select_prev();
Jupyter.notebook.execute_cell_and_select_below();
};
// Button to add default cell
var defaultCellButton = function () {
Jupyter.toolbar.add_buttons_group([
Jupyter.keyboard_manager.actions.register ({
'help': 'Add default cell',
'icon' : 'fa-play-circle',
'handler': add_cell
}, 'add-default-cell', 'Default cell')
])
};
// Generates unique id for a new canvas
var uniqueCanvasId = function() {
// Get all existing ids as numbers, in ascending order
nums = [];
for (let id in canvases)
nums.push(parseInt(id.substring(4, id.length-2)));
nums.sort(function(a, b) {
return a - b;
});
// Find first unique index not already represented in the list
let k = 0;
for (const n of nums) {
if (k !== n) break;
k += 1;
}
return "__c_"+k+"__";
};
// Given a notebook cell, checks what canvases are actually in the cell,
// and removes any which aren't from the cell's metadata (used on save_notebook()).
var cleanupCellMetadata = function(cell) {
if (!('notate_canvi' in cell.metadata)) return;
let cm = cell.code_mirror;
let ids = Object.keys(cell.metadata['notate_canvi']);
for(let idx of ids) {
// Check if a canvas with this index is in the text of the cell:
let cursor = cm.getSearchCursor(idx);
let found = false;
while(cursor.findNext()) {
found = true;
break;
}
if (!found) { // Delete unused canvases
console.log('Deleting unused canvas', idx, 'in cell metadata.');
delete cell.metadata['notate_canvi'][idx];
}
}
};
// var initialize = function () {
// // == Add any relative css to the page ==
// // Add Font Awesome 4.7 Icons:
// $('<link/>')
// .attr({
// rel: 'stylesheet',
// type: 'text/css',
// href: requirejs.toUrl('./css/font-awesome.min.css')
// })
// .appendTo('head');
// };
function initialize() {
// events.on('execute.CodeCell', function(evt, data) {
// var cell = data.cell;
// var cm = cell.code_mirror;
// cm.replaceRange('x = y', {'line':0, 'ch':0});
// console.log('HELLO WORLD', cell, data);
// });
Logger.log("Initialize", Jupyter.notebook.notebook_path);
// Attach window focus change callbacks
window.onfocus = function () {
Logger.log("Window", "focused")
};
window.onblur = function () {
Logger.log('Window', "blurred");
};
// Incredibly sketchy wrapper over Jupyter saving.
// Attempts to cleanup excess metadata in cells before saving.
var origSaveNotebook = Jupyter.notebook.__proto__.save_notebook.bind(Jupyter.notebook);
Jupyter.notebook.__proto__.save_notebook = function() {
let cells = Jupyter.notebook.get_cells();
for (let cell of cells)
cleanupCellMetadata(cell);
// Append log data as metadata to first cell of the notebook
// (could be any cell really)
if (cells.length > 0) {
// Append general log
if ('log' in cells[0].metadata) // Append to existing
cells[0].metadata['log'] = cells[0].metadata['log'].concat(Logger.getData());
else // Create new entry to metadata to store logs
cells[0].metadata['log'] = Logger.getData();
Logger.clear(); // Clear the local log data, since we just appended it to the end of the saved notebook's metadata
}
origSaveNotebook();
};
// See https://github.com/jupyter/notebook/blob/42227e99f98c3f6b2b292cbef85fa643c8396dc9/notebook/static/services/kernels/kernel.js#L728
function run_code_silently(code, cb) {
var kernel = Jupyter.notebook.kernel;
var callbacks = {
shell : {
reply : function(msg) {
// console.log(msg);
if (msg.content.status === 'error') {
// handle error
console.error('Error running silent code', code);
} else {
// status is OK
cb();
}
} //execute_reply_callback,
}
};
var options = {
silent : true,
user_expressions : {},
allow_stdin : false
};
kernel.execute(code, callbacks, options);
// console.log('tried to run code', code);
}
// Intercept running code cells to convert canvases
let runCodeForCell = function(cell, cb) {
// Running a cell that includes a canvas
var cm = cell.code_mirror;
// Find all unique canvas id's in the selected cell
let activatedCanvasIds = [];
let cursor = cm.getSearchCursor(/__c_([0-9]+)__/g);
while(cursor.findNext())
activatedCanvasIds.push(cm.getRange(cursor.from(), cursor.to()));
// Convert canvases to PNGs and encode as base64 str
// to 'send' to corresponding Python variables:
let data_urls = {};
let code = PYCODE_SETUP;// 'import base64\nimport numpy as np\nfrom io import BytesIO\nfrom PIL import Image\n';
let injected_code = false;
for (let idx of activatedCanvasIds) {
if (!(idx in canvases)) {
console.warn('@ Run cell: Could not find a notate canvas with id', idx, 'Skipping...');
continue;
}
data_urls[idx] = canvases[idx].toOpaqueDataURL().split(',')[1];
code += idx + '=np.array(Image.open(BytesIO(base64.b64decode("' + data_urls[idx] + '"))).convert("RGB"), dtype="uint8")\n';
injected_code = true;
}
// Log the original val of get_text
// if (injected_code) Logger.log("Silent execute", code);
Logger.log("Execute:cell", cell.get_text());
// Alter 'get_text' to insert artificial code into cell where canvases are
cell.get_text = function() {
let raw_code = this.code_mirror.getValue();
let lines = raw_code.split("\n");
let corrected_code = "";
lines.forEach((line, i) => {
if (line.includes('__c_')) {
let idx = line.lastIndexOf('__)');
if (idx > -1) {
let beg_igx = line.lastIndexOf('__c_')
line = line.slice(0, beg_igx) + "NotateArray(" + line.slice(beg_igx, idx+2) + ", locals())" + line.slice(idx+2);
}
}
corrected_code += line + '\n';
});
return corrected_code;
}.bind(cell);
// cm.replaceRange('x = y', {'line':0, 'ch':0});
// console.log('HELLO WORLD', cell);
// First run some background code silently to setup the environment
// for this cell. The __*__ canvases will become numpy 2d arrays (images)
run_code_silently(code, function () {
// Call execute_cell(), which triggers our hijacked get_text() method
cb();
// Repair the old get_text so nothing funny happens:
cell.get_text = function() {
return this.code_mirror.getValue();
}.bind(cell);
// Wait for output and log it
let output_cb = function(event, cells) {
if (!('cell' in cells)) {
console.warn('@ finished_execute callback: Could not determine executed cell.');
return;
}
// Log output
const output = cells.cell.output_area.outputs;
output.forEach(function(out) {
const outtype = out.output_type;
if (outtype == 'error') { // The cell errored while executing, spitting out an execution error msg
Logger.log('Execution:'+outtype+":"+out.ename+':'+out.evalue, out.traceback.join('\n\n\n'));
} else {
let text = "(unknown)";
if ('data' in out && 'text/plain' in out.data)
text = out.data['text/plain'];
else if ('text' in out)
text = out.text;
Logger.log('Execution:'+outtype, text);
}
});
// Cleanup --Remove our custom cb from events
cells.cell.events.off('finished_execute.CodeCell', output_cb);
};
cell.events.on('finished_execute.CodeCell', output_cb);
});
}
// var origExecuteCell = Jupyter.notebook.__proto__.execute_cells.bind(Jupyter.notebook);
// Jupyter.notebook.__proto__.execute_cells = function() {
// console.log("heloooo");
// let cell = Jupyter.notebook.get_selected_cell();
// runCodeForCell(cell, origExecuteCell);
// };
// Hijack the core "execute" method for running code in cells.
Jupyter.CodeCell.prototype.__execute = Jupyter.CodeCell.prototype.execute;
Jupyter.CodeCell.prototype.execute = function(stop_on_error) {
runCodeForCell(this, this.__execute.bind(this));
};
// var shortcuts = {
// 'ctrl-enter': function(pager, evt) {
// let cell = Jupyter.notebook.get_selected_cell();
// runCodeForCell(cell, function() { Jupyter.notebook.execute_cell(); });
// },
// 'shift-enter': function(pager, evt) {
// let cell = Jupyter.notebook.get_selected_cell();
// runCodeForCell(cell, function() { Jupyter.notebook.execute_cell_and_select_below(); });
// }
// };
// Jupyter.notebook.keyboard_manager.edit_shortcuts.add_shortcuts(shortcuts);
// Jupyter.notebook.keyboard_manager.command_shortcuts.add_shortcuts(shortcuts);
// Canvas generation functions
function create_canvas(width, height) {
var canvas = document.createElement('canvas');
canvas.id = 'notate-canvas';
canvas.width = width;
canvas.height = height;
canvas.style.width = width/2 + "px";
canvas.style.height = height/2 + "px";
canvas.style.display = "inline-block";
canvas.style.verticalAlign = "middle";
canvas.style.zIndex = 3; // 2 is the code blocks
canvas.style.cursor = "default";
canvas.style.border = "thin solid #ccc";
canvas.style.touchAction = "none";
// Set bg color to translucent white. This seems small but it lets
// the "selection" cursor (blue) show through when the canvas is highlighted as/with text.
canvas.style.backgroundColor = "rgba(255, 255, 255, 0.4)";
return canvas;
}
// Get all code cells
var code_cells = Jupyter.notebook.get_cells().filter(
function(cell) {
return cell.cell_type == "code";
});
// Attach the canvas-tear event handler
let attachCanvasEventHandlers = function(cell) {
let cm = cell.code_mirror;
let insert_canvas_at_pos = function(from, to, cm, canvas) {
cm.markText(from, to, {replacedWith:canvas});
}
let insert_canvas_at_cursor = function(cm, canvas) {
// Put canvas at cursor position
cursorPos = cm.getCursor();
dummy_idx = uniqueCanvasId();
cm.replaceRange(dummy_idx, cursorPos); // Insert a dummy character at cursor position
insert_canvas_at_pos({"line":cursorPos.line, "ch":cursorPos.ch}, // replace it with canvas
{"line":cursorPos.line, "ch":cursorPos.ch+dummy_idx.length},
cm, canvas)
return {canvas:canvas, idx:dummy_idx};
};
// Load + inflate saved canvi from cell metadata
if ('notate_canvi' in cell.metadata) {
for (let idx in cell.metadata['notate_canvi']) {
// For each 'idx' (e.g. __c_0__), check if its found in the cell's text.
// If found, create + insert canvas at that index, +load it with saved image data.
let cursor = cm.getSearchCursor(idx);
while(cursor.findNext()) {
// We've found a match. Insert canvas at position + populate it:
// Create new HTML canvas element + setup
let canvas = create_canvas(600, 340);
// Create NotateCanvas and attach event handlers
let notate_canvas = NotateCanvasManager.setup(canvas);
// Load canvas with saved image data
notate_canvas.loadFromDataURL(cell.metadata['notate_canvi'][idx])
// Insert canvas at cursor position in current cell
insert_canvas_at_pos(cursor.from(), cursor.to(), cm, canvas);
// Index canvas for future reference
canvases[idx] = notate_canvas;
notate_canvas.idx = idx;
notate_canvas.cell = cell;
}
}
} else
cell.metadata['notate_canvi'] = {};
// Insert new canvas on Ctrl+Enter key press:
cm.addKeyMap({"Ctrl-\\":function(cm) {
// Create new HTML canvas element + setup
let canvas = create_canvas(600, 340);
// Create NotateCanvas and attach event handlers
let notate_canvas = NotateCanvasManager.setup(canvas);
// Insert canvas at cursor position in current cell
let c = insert_canvas_at_cursor(cm, canvas);
Logger.log("Created new canvas with Ctrl-\\", "id:"+c.idx);
// Index canvas for future reference
canvases[c.idx] = notate_canvas;
notate_canvas.idx = c.idx;
notate_canvas.cell = cell;
}});
// Paste a canvas somewhere else
let just_pasted = false;
cm.on('change', function(cm, event) { // 'After paste' event
// Log a snapshot of the text in the cell at the start of editing the code
if (Logger.lastLogType() !== "code_edit")
Logger.log("Editing:cell:begin", cell.get_text());
// Log change to code cell
Logger.logCodeCellChange(event);
if (just_pasted !== false) {
let txt = just_pasted;
// Search the text for matches of NotateCanvas id's.
// Replace all matches with corresponding canvas elements.
let ids = Object.keys(canvases);
for (let id of ids) {
if (txt.includes(id)) {
console.log('Searching for', id, '...');
// For each 'idx' (e.g. __c_0__), check where it's found in the cell's text.
// If there isn't a canvas already at that index, create + insert it, +load it with saved image data.
let cursor = cm.getSearchCursor(id);
while(cursor.findNext()) {
// Skip any matches where there's already a canvas...
let from = cursor.from();
let to = cursor.to();
if (cm.findMarks(from, to).length > 0)
continue;
console.log('Found match for', id, 'at selection', from, to);
Logger.log("Pasted", "canvas_duplicated:"+id);
// Replace this match with a unique ID.
let new_id = uniqueCanvasId();
cm.replaceRange(new_id, from, to);
// Fix the selection width in case the new_idx is longer than the old one:
to = {line: from.line, ch:from.ch + new_id.length};
// Insert new canvas at position + populate it:
// Create new HTML canvas element + setup
let canvas = create_canvas(600, 340);
let copied_notate_canvas = canvases[id].clone(canvas);
// Add cloned canvas to manager
NotateCanvasManager.add(copied_notate_canvas);
// Insert canvas at cursor position in current cell
insert_canvas_at_pos(from, to, cm, canvas);
// Index canvas for future reference
canvases[new_id] = copied_notate_canvas;
copied_notate_canvas.idx = new_id;
copied_notate_canvas.cell = cell;
// Save data to cell's metadata
cell.metadata['notate_canvi'][new_id] = canvases[id].canvas.toDataURL();
}
}
}
cleanupCellMetadata(cell);
just_pasted = false;
}
});
cm.on('copy', function(cm, event) {
let txt = window.getSelection().toString(); // kind of a hack, but the following 'correct' way doesn't work: event.clipboardData.getData("text");
Logger.log("Copied", "raw_text:"+txt);
});
cm.on('cut', function(cm, event) {
let txt = window.getSelection().toString(); // kind of a hack, but the following 'correct' way doesn't work: event.clipboardData.getData("text");
Logger.log("Cut", "raw_text:"+txt);
});
cm.on('paste', function(cm, event) {
let items = event.clipboardData.items;
// Support for pasting an image:
if (items.length > 1 && items[1]["kind"] === "file" && items[1]["type"].includes("image/")) {
let imageBlob = items[1].getAsFile();
let canvas = create_canvas(600, 340);
Logger.log("Pasted", "image_from_clipboard");
// Create NotateCanvas and attach event handlers
let notate_canvas = NotateCanvasManager.setup(canvas);
// Crossbrowser support for URL
let URLObj = window.URL || window.webkitURL;
// Creates a DOMString containing a URL representing the object given in the parameter
// namely the original Blob
notate_canvas.loadFromDataURL(URLObj.createObjectURL(imageBlob));
// Insert canvas at cursor position in current cell
let c = insert_canvas_at_cursor(cm, canvas);
// Index canvas for future reference
canvases[c.idx] = notate_canvas;
notate_canvas.idx = c.idx;
notate_canvas.cell = cell;
}
let txt = event.clipboardData.getData("text");
console.log("pasted!", txt);
Logger.log("Pasted", "raw_text:"+txt);
just_pasted = txt;
});
};
// Add event handlers to all preloaded cells
code_cells.forEach(attachCanvasEventHandlers);
// Intercept cell creation to add event handlers
$([IPython.events]).on("create.Cell", function(evt, data) {
attachCanvasEventHandlers(data["cell"]);
});
}
// Wait until something is loaded
// function defer(check, cb) {
// if (check()) {
// cb();
// } else {
// setTimeout(function() { defer(check, cb); }, 50);
// }
// }
// This function is called when a notebook is started.
function load_ipython_extension() {
// Load Spectrum JS color selector library CSS file.
$('<link>')
.attr({
rel: 'stylesheet',
type: 'text/css',
href: requirejs.toUrl('./libs/spectrum.min.css')
})
.appendTo('head');
initialize();
}
return {
load_ipython_extension: load_ipython_extension
};
});
var NotateCanvasManager = (function() {
const canvases = [];
return {
setup: function(canvas) {
canvases.push( new NotateCanvas(canvas) );
return canvases[canvases.length-1];
},
add: function(notateCanvas) {
canvases.push( notateCanvas );
},
remove: function(canvas) {
for (var i = 0; i < canvases.length; i++) {
if (canvases[i].canvas.id === canvas.id) {
canvases[i].destruct(); // remove event handlers
canvases.splice(i, 1); // remove NotateCanvas at index i
return;
}
}
}
}
}());
// A NotateCanvas wraps a canvas and takes care of setting up
// all the basic drawing events for different platforms.
class NotateCanvas {
clone(new_canvas_element) { // Clone this NotateCanvas, e.g. to duplicate the HTML canvas.
let c = new NotateCanvas(new_canvas_element);
c.canvas.width = this.canvas.width;
c.canvas.height = this.canvas.height;
c.canvas.style.width = this.canvas.style.width;
c.canvas.style.height = this.canvas.style.height;
c.canvas.style.backgroundColor = this.canvas.style.backgroundColor;
// c.strokes = JSON.parse(JSON.stringify(this.strokes)); // deep copy stroke data
c.resolution = this.resolution;
c.bg_color = this.bg_color;
c.bg_opacity = this.bg_opacity;
c.pen_weight = this.pen_weight;
c.pen_color = this.pen_color;
c.clear();
c.loadFromDataURL(this.toDataURL(), true, true);
return c;
}
constructor(canvas_element) {
this.strokes = [];
this.idx = null; // undefined
this.cell = null;
this.stateStack = [];
this.stateIdx = -1;
this.canvas = canvas_element;
this.ctx = this.canvas.getContext('2d', {
desynchronized: false
});
this.ctx.imageSmoothingEnabled = true; // anti-aliasing
this.CHANGESIZE_DIALOG_ENABLED = false;
const default_linewidth = 2;
const default_color = '#000';
const default_border = "thin solid #ccc";
const hover_border = "thin solid #888";
this.pen_color = default_color;
this.pen_weight = default_linewidth;
this.resolution = 2;
// this.pos = { x:this.canvas.offsetLeft, y:this.canvas.offsetTop };
this.pos = { x:0, y:0 };
this.bg_color = '#fff';
this.bg_opacity = 0.4;
this.default_linewidth = default_linewidth
// Resize parameters
this.default_resize_thresh = 20;
this.resize_thresh = 20;
this.default_resize_settings = Object.freeze({
'right': {
'cursor': 'col-resize',
'borderRight': '3px solid gray',
'borderBottom': hover_border
},
'bot': {
'cursor': 'row-resize',
'borderRight': hover_border,
'borderBottom': '3px solid gray'
},
'botright': {
'cursor': 'nwse-resize',
'borderRight': '3px solid gray',
'borderBottom': '3px solid gray'
},
'default': {
'border': hover_border,
'cursor': 'auto'
}
});
this.resize_settings = this.default_resize_settings;
this.disable_resize = false;
this.disable_expand = false;
this.resizing = false;
this.pointer_down = false;
this.pointer_moved = false;
this.disable_drawing = false;
this.saved_img = null;
this._is_dirty = false;
this._resize_canvas_copy = null;
this.new_strokes = {};
// Attach pointer event listeners
let pointerEnter = function pointerEnter(e) {
Logger.log(this.getName(), 'pointerenter:'+e.pointerType);
if (!this.disable_expand)
this.canvas.style.border = this.resize_settings.default.border;
if (this.saved_img === null)
this.saved_img = this.toDataURL();
}.bind(this);
let pointerDown = function pointerDown(e) {
Logger.log(this.getName(), 'pointerdown:'+e.pointerType);
this.pointer_down = true;
this.pointer_moved = false;
if (this.resizing && e.pointerType !== "touch") {
Logger.log(this.getName(), 'start_resizing:'+e.pointerType+';'+this.canvas.style.width+';'+this.canvas.style.height);
// create backing canvas
let backCanvas = document.createElement('canvas');
backCanvas.width = this.canvas.width;
backCanvas.height = this.canvas.height;
backCanvas.getContext('2d').drawImage(this.canvas, 0, 0);
this._resize_canvas_copy = backCanvas;
e.preventDefault();
return;
}
else if (e.pointerId in this.new_strokes) {
console.warn('A stroke is already being drawn with this id. Did you forget to cancel?');
return;
} else if (e.pointerType === "touch") {
this.canvas.style.border = "thin solid #222"
return;
}
// Skip if drawing is disabled
if (this.disable_drawing) return;
// Create new stroke and attach reference
let s = { pts: [this.getPointerValue(e)], weight:this.pen_weight, color:this.pen_color };
this.new_strokes[e.pointerId] = s;
Logger.log(this.getName(), 'start_drawing:'+e.pointerType);
this.drawStroke(s);
}.bind(this);
let pointerMove = function pointerMove(e) {
// Skip if drawing is disabled
if (this.disable_drawing) return;
else if (e.pointerType === "touch") return; // disable move events on touch
let pos = this.getPointerValue(e);
this.pointer_moved = true;
// Ensure pointer event is being tracked, if not, err:
if (e.pointerId in this.new_strokes) {
// Add point to end of stroke:
this.new_strokes[e.pointerId].pts.push( pos );
// Draw new line of stroke:
this.drawStroke( { pts:this.new_strokes[e.pointerId].pts.slice(-2),
width:this.new_strokes[e.pointerId].weight,
color:this.new_strokes[e.pointerId].color });
} else if (this.pointer_down && this.resizing) {
// Resize canvas from bottom-right corner:
const d = this.resize_thresh;
if (this.resizing.includes('hor')) {
this.canvas.style.width = Math.floor(e.offsetX + d) + "px";
this.canvas.width = Math.floor(e.offsetX + d) * 2;
}
if (this.resizing.includes('vert')) {
this.canvas.style.height = Math.floor(e.offsetY + d) + "px";
this.canvas.height = Math.floor(e.offsetY + d) * 2;
}
// After resize, we have to redraw the pre-resize contents of the canvas:
this.canvas.getContext('2d').drawImage(this._resize_canvas_copy, 0, 0);
} else if (!this.disable_resize) {
// Bottom-right resize
const d = this.resize_thresh;
if (pos.x >= this.canvas.width - d && pos.y >= this.canvas.height - d) {
this.canvas.style.cursor = this.resize_settings.botright.cursor;
this.canvas.style.borderRight = this.resize_settings.botright.borderRight;
this.canvas.style.borderBottom = this.resize_settings.botright.borderBottom;
this.resizing = "hor-vert";
}
else if (pos.x >= this.canvas.width - d) {
this.canvas.style.cursor = this.resize_settings.right.cursor;
this.canvas.style.borderRight = this.resize_settings.right.borderRight;
this.canvas.style.borderBottom = this.resize_settings.right.borderBottom;
this.resizing = "hor";
}
else if (pos.y >= this.canvas.height - d) {
this.canvas.style.cursor = this.resize_settings.bot.cursor;
this.canvas.style.borderRight = this.resize_settings.bot.borderRight;
this.canvas.style.borderBottom = this.resize_settings.bot.borderBottom;
this.resizing = "vert";
}
else {
this.canvas.style.cursor = this.resize_settings.default.cursor;
// this.canvas.style.border = "thin solid #aaa";
this.canvas.style.border = this.resize_settings.default.border;
this.resizing = false;
}
}
}.bind(this);
let pointerLeave = function pointerLeave(e) {
if (this.pointer_down) {
if (this.resizing) {
// Continue to resize canvas from bottom-right corner:
const d = this.resize_thresh;
if (this.resizing.includes('hor')) {
this.canvas.style.width = Math.floor(e.offsetX + d) + "px";
this.canvas.width = Math.floor(e.offsetX + d) * 2;
}
if (this.resizing.includes('vert')) {
this.canvas.style.height = Math.floor(e.offsetY + d) + "px";
this.canvas.height = Math.floor(e.offsetY + d) * 2;
}
// After resize, we have to redraw the pre-resize contents of the canvas:
this.canvas.getContext('2d').drawImage(this._resize_canvas_copy, 0, 0);
e.preventDefault();
}
}
else {
Logger.log(this.getName(), 'pointerleave:'+e.pointerType);
}
if (!this.disable_expand)
this.canvas.style.border = this.resize_settings.default.border;
}.bind(this);
let pointerUp = function pointerUp(e) {
Logger.log(this.getName(), 'pointerup:'+e.pointerType);
// Skip if drawing is disabled
if (this.disable_drawing) return;
if (this.pointer_down) {
if (this.resizing !== false) { // End of resizing canvas operation.
Logger.log(this.getName(), 'finish_resize:'+e.pointerType+';'+this.canvas.style.width+';'+this.canvas.style.height);
if (this.pointer_moved) {
// After resize, we have to redraw the pre-resize contents of the canvas:
this.canvas.getContext('2d').drawImage(this._resize_canvas_copy, 0, 0);
this.saved_img = this.toDataURL();
this.pushState(this.saved_img);
this.saveMetadataToCell(); // Save resized image to cell metadata
} else { // Open modal input asking for specific width/height pixel values
if (this.CHANGESIZE_DIALOG_ENABLED)
this.openChangeSizeDialog();
}
} else if (!this.pointer_moved && !this.disable_expand) { // Clicked the canvas: expand to fullscreen.
let _this = this;
Logger.log("Fullscreen mode", "entered:"+this.getName());
// A black, translucent background for the popover:
let site = document.getElementsByTagName("BODY")[0];
let site_bounds = site.getBoundingClientRect();
let bg = document.createElement('div');
bg.style.backgroundColor = '#000';
bg.style.width = "100%";
bg.style.height = "100%";
bg.style.position = "absolute";
bg.style.left = "0px";
bg.style.top = "0px";
bg.style.zIndex = "5";
bg.style.display = "block";
bg.style.opacity = 0.4;
site.appendChild(bg);
// Div wrapper over the DOM canvas:
const bounds = this.canvas.getBoundingClientRect();
const margin = 100;
let scaleX = Math.min((site_bounds.width - margin*2) / bounds.width, (site_bounds.height - 110 - margin*2) / bounds.height);
let canvas_wrapper = document.createElement('div');
canvas_wrapper.style.position = "absolute";
canvas_wrapper.style.display = "block";
canvas_wrapper.style.margin = "0";
canvas_wrapper.style.padding = "0";
canvas_wrapper.style.left = (site_bounds.width/2 - bounds.width/2*scaleX) + "px";
canvas_wrapper.style.top = (site_bounds.height/2 - bounds.height/2*scaleX) + "px";
canvas_wrapper.style.width = Math.floor(bounds.width*scaleX) + "px";
canvas_wrapper.style.height = Math.floor(bounds.height*scaleX) + "px";
canvas_wrapper.style.zIndex = "6";
site.append(canvas_wrapper);
// The cloned DOM canvas:
let clone = this.canvas.cloneNode(false);
clone.style.backgroundColor = "#fff";
clone.style.position = "inherit";
clone.style.display = "block";
clone.style.margin = "0";
// clone.style.zIndex = "6";
clone.style.left = Math.floor(bounds.width*scaleX - bounds.width)/2 + "px";
clone.style.top = Math.floor(bounds.height*scaleX - bounds.height)/2 + "px";
clone.style.width = Math.floor(bounds.width) + "px";
clone.style.height = Math.floor(bounds.height) + "px";
clone.style.transform = "scale(" + scaleX + "," + scaleX + ")";
clone.style.border = "none";
clone.style.cursor = "crosshair";
canvas_wrapper.appendChild(clone);
// clone.style.transition = "transform 1000ms cubic-bezier(0.165, 0.84, 0.44, 1)";
// Resize settings for fullscreen mode:
const fullscreen_resize_settings = Object.freeze({
'right': {
'cursor': 'col-resize',
'borderRight': '1px solid gray',
'borderBottom': '0px solid gray'
},
'bot': {
'cursor': 'row-resize',
'borderRight': '0px solid gray',
'borderBottom': '1px solid gray'
},
'botright': {
'cursor': 'nwse-resize',
'borderRight': '1px solid gray',
'borderBottom': '1px solid gray'
},
'default': {
'border': '0px',
'cursor': 'crosshair'
}
});
// Add pen-based draw capabilities to canvas w/ draw library code:
let notate_clone = NotateCanvasManager.setup(clone);
notate_clone.bg_opacity = 1.0;
// notate_clone.disable_resize = true;
// console.log(clone.style.width, scaleX);
notate_clone.resize_thresh = 10;
notate_clone.resize_settings = fullscreen_resize_settings;
notate_clone.disable_expand = true;
notate_clone.strokes = this.strokes;
notate_clone.setPenColor(this.pen_color);
notate_clone.idx = this.idx;
notate_clone.cell = this.cell;
notate_clone.loadFromDataURL(this.toDataURL(), true, true);
let cursorsvg = document.createElementNS("http://www.w3.org/2000/svg", 'svg'); //Create a path in SVG's namespace
cursorsvg.style.position = "absolute";
cursorsvg.style.display = "block";
cursorsvg.style.pointerEvents = "none";
cursorsvg.setAttribute("width", "32px");
cursorsvg.setAttribute("height", "32px");
// cursorsvg.setAttribute("opacity", "0.5");
cursorsvg.setAttribute("viewBox", "0 0 32 32");
cursorsvg.style.zIndex = "4";
cursorsvg.innerHTML += '<circle cx="15" cy="15" r="4" stroke="black" fill="black" stroke-width="0"></circle>';
cursorsvg.drag_resize = false; // special attribute when drawing elements like a rect and circle
cursorsvg.drag_start = null;
cursorsvg.offset = {x:-16, y:-16};
canvas_wrapper.appendChild(cursorsvg);
const div_to_canvas_coord = function(p) {
return { x: p.x/scaleX*2, y: p.y/scaleX*2 }; // notate_clone.resolution
};
// canvas_wrapper.innerHTML += '<svg height="30" width="30" style="position:absolute;z-index:7"></svg>';
// Toolbar icons
// == Toolbar button helper functions ==
function createIcon(fontAwesomeIconName, tooltip, innerAdj) {
let iconbg = document.createElement('div');
iconbg.style.position = "absolute";
iconbg.style.display = "block";
iconbg.style.width = "32px";
iconbg.style.height = "32px";
iconbg.style.zIndex = "7";
iconbg.style.backgroundColor = "#eee";
iconbg.title = tooltip;
// There isn't actually an icon in FontAwesome that represents
// a line, so we manually create it w/ an SVG element:
if (fontAwesomeIconName == 'line') {
let svg = document.createElementNS("http://www.w3.org/2000/svg", 'svg'); //Create a path in SVG's namespace
svg.style.position = "relative";
svg.style.display = "block";
svg.setAttribute("width", "32px");
svg.setAttribute("height", "32px");
svg.setAttribute("viewBox", "0 0 32 32");
svg.style.zIndex = "8";
let path = document.createElement('path');
path.setAttribute("d","M 5 27 L 27 5"); //Set path's data
path.setAttribute("stroke", "black"); //Set stroke colour
path.setAttribute("stroke-width", "2"); //Set stroke width
svg.appendChild(path);
iconbg.appendChild(svg);
// Force a draw of the SVG element. See: https://stackoverflow.com/a/56923928
svg.innerHTML += "";
} else {
let i = document.createElement('i');