forked from irrelevantdotcom/edit-tf
-
Notifications
You must be signed in to change notification settings - Fork 2
/
Copy pathteletext-editor.js
4880 lines (4153 loc) · 154 KB
/
teletext-editor.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
// Copyright 2015-2017 Simon Rawles, Alan Davies, Tim Hutton, Steve
// Horsley, Alistair Cree, Peter Fagan and David Hall.
//
// The JavaScript code in this page is free software: you can
// redistribute it and/or modify it under the terms of the GNU
// General Public License (GNU GPL) as published by the Free Software
// Foundation, either version 3 of the License, or (at your option)
// any later version. The code is distributed WITHOUT ANY WARRANTY;
// without even the implied warranty of MERCHANTABILITY or FITNESS
// FOR A PARTICULAR PURPOSE. See the GNU GPL for more details.
//
// As additional permission under GNU GPL version 3 section 7, you
// may distribute non-source (e.g., minimized or compacted) forms of
// that code without the copy of the GNU GPL normally required by
// section 4, provided you include this license notice and a URL
// through which recipients can access the Corresponding Source.
// Modified 2017/04/12 By Rob O'Donnell as part of Retrochallenge 2017/04
// to make the Editor's displayfacilties useable by alternative applications,
// e.g. an updated version of Adam Dawes teletext viewer, without need for
// it being "mercilessly and messily hacked about" :-D. And for the
// viewdata browser I need to do myself.
// Several editors can be on a single page. We need to keep track of
// which editor receives keypresses and mouse events.
var active_editor = null;
// We also need to allow at most one editor to read to and write from
// the URL.
var url_editor = null;
// Editor is the main object, and encapsulates all the functionality
// of a single editor canvas.
function Editor() { // beginning of Editor, but we're not indenting.
var editor_this = this;
this.associated_viewer = null;
///////////////////////
///// GLOBAL DATA /////
///////////////////////
// These mostly define the state of the frame or the editor UI.
// Descriptions refer to (x,y), ie row y, column x.
var cc = []; // cc[y][x] = the character code (0..127) at (x,y).
var fg = []; // fg[y][x] = foreground colour (0..7) at (x,y).
var bg = []; // bg[y][x] = background colour (0..7) at (x,y).
var tg = []; // text or graphics at (x,y)?
// tg[y][x] = 0 if text, 1 if graphics.
var cs = []; // contiguous or separated graphics at (x,y)?
// cs[y][x] = 0 if contiguous, 1 if separated.
var nd = []; // normal or double height text at (x,y)?
// nd[y][x] = 0 if normal, 1 if double,
// 2 if normal but has been reset from double.
var hg = []; // has held graphics been enabled for (x,y)?
// hg[y][x] = 0 if held graphics disabled, 1 if enabled.
var sc = []; // is the character at (x,y) shown or concealed?
// sc[y][x] = 0 if shown, 1 if concealed.
var sf = []; // is the character at (x,y) steady or flashing?
// sf[y][x] = 0 if steady, 1 if flashing.
var fs = []; // is row y the first or second row of double height?
// fs[y] = 0 if unassigned, 1 if first, 2 if second.
var font = []; // font[c][y] = integer describing the bit pattern for
// character c, row y.
var curx = 0; // the column at which the cursor is currently.
var cury = 0; // the row at which the cursor is currently.
// When doing cut and paste we display a rectangle which represents the area
// which is going to be cut. Usually this is set to -1 in both coordinates to
// show no area is active. Actually, when *either* is set to -1 we assume that
// rectangle selection is not active.
// When the escape key is pressed, they are set.
var curx_opposite = -1;
var cury_opposite = -1;
var clipboard = []; // clipboard[y][x] is the clipped character at row y, col x
var clipboard_size_x = 0;
var clipboard_size_y = 0;
var escape = 0; // has escape been pressed? 0 if no, 1 if yes.
var dead_key = 0; // dead key pressed previously. 0 if not, otherwise char code
var statusmode = 0; // what is the statusbar showing?
// 0 means the usual information about the current cell.
// 1 means the additional teletext metadata
var statushidden = 1; // is the statusbar temporarily hidden with ESC-0?
// 0 if no, 1 if yes.
var helpscreenshown = 0; // is the help screen being shown?
// 0 if no, 1 if yes
var showcc = 0; // are we showing control characters? 0 if no, 1 if yes.
var cset = 0; // the current character set (1..8).
var reveal = 0; // is reveal on? 0 if no, 1 if yes.
var grid = 1; // is the grid shown? 0 if no, 1 if guides, 2 if yes.
var blackfg = 0; // do we permit the use of black foreground (0x0 and
// 0x10) control codes? 0 if not, 1 if so.
var trace = 0; // Are we in tracing mode? 0 if no, 1 if yes
var trace_url = ""; // The last image URL used for this.
// We hold the trace rectangle in global state so that we can handle
// changes in aspect ratio.
var trace_position_x = 0;
var trace_position_y = 0;
var trace_size_x = 0;
var trace_size_y = 0;
var trace_whole_area = 0; // Does the trace image fill the whole area?
var trace_opacity = 1;
var full_pix_scale = 2;
// draw at a higher resolution than we display at, to
// look better zoomed in.
var pix_scale = full_pix_scale;
// specifies how much to stretch the x direction.
var aspect_ratios = [1, 1.1, 1.2, 1.22, 1.3, 1.33, 1.36, 1.4, 1.5, 1.75, 2];
var current_ratio = 2; // index of aspect_ratios
var aspect_ratio = aspect_ratios[current_ratio];
var pix_size = 1;
// If all the pixels are 1:1
var active_export = 0;
// if non-zero, there are URLs for export on the screen
// which should be invalidated on a change.
this.canvasid = "canvas";
// The HTML id for the canvas.
// Page metadata:
var m_page = 0x100; // This page's number within the magazine.
// This is hexadecimal. range: 0x100..0x7ff
var m_subpage = 0; // This page's subpage number within the
// page. Not necessarily the subcode. Range
// is 0x00 to 0xff.
var m_subcode = 0x3f7f; // This page's subcode/subpage number.
// This is hexadecimal. range: 0x0..0x3f7f
// Note that the third nybble may only range from
// 0 to 7.
var m_control = []; // The control bits for this page.
var m_fastext_red = 0;
var m_fastext_green = 0;
var m_fastext_yellow = 0;
var m_fastext_cyan = 0;
var m_fastext_link = 0;
var m_fastext_index = 0;
// These are fastext links to other pages.
// Also hexadecimal, but may be 0 to indicate
// no link.
// Initialises the state of the screen.
this.init_state = function() {
editor_this.init_canvas();
// Set up the arrays...
for (var r = 0; r <= 24; r++) {
cc[r] = []; fg[r] = []; bg[r] = [];
tg[r] = []; cs[r] = []; nd[r] = [];
hg[r] = []; sc[r] = []; sf[r] = [];
fs[r] = 0; clipboard[r] = [];
for (var c = 0; c < 40; c++) {
clear_char(c,r);
}
}
// Initialise the font to the default character set.
init_font(cset);
// Set the control bits
for ( var i = 4; i <= 14; i++ ) { m_control[i] = 0; }
// Load the page data from the hash, if possible.
load_from_hash();
}
// init_canvas() is called also when the aspect ratio is adjusted.
this.init_canvas = function() {
// The dimensions depend on whether the status bar is shown
var width = 480; var height = 540;
if ( statushidden == 1 ) { height = 500; }
var c = document.getElementById(editor_this.canvasid);
// set the 'logical' width and height, the code is designed for 480x520,
// scaled up to look better when zoomed in
c.width = width*pix_scale;
c.height = height*pix_scale;
// set the width and hight to display on-screen, with the modified aspect ratio
c.style.width = (pix_size*width*aspect_ratio)+"px";
c.style.height = (pix_size*height)+"px";
// Clear the canvas with a background colour
var ctx = c.getContext("2d");
ctx.fillStyle = "#000";
ctx.fillRect(0, 0, width*pix_scale, height*pix_scale);
ctx.textAlign = "left";
// Initialise the background trace image
init_trace();
}
var init_trace = function() {
var cfdiv = document.querySelector("div#canvasframe");
var cf = document.querySelector("canvas#frame");
if ( trace == 0 ) {
cfdiv.style.background = "";
cfdiv.style.backgroundSize = "";
trace_opacity = 1;
cf.style.opacity = "";
}
if ( trace == 1 ) {
if ( trace_whole_area == 1 ) { // We are tracing the whole area
cfdiv.style.background = "url(\"" + trace_url + "\") no-repeat center top";
} else { // We are tracing a sub-rectangle
cfdiv.style.background = "url(\"" + trace_url + "\") no-repeat "
+ ( trace_position_x * aspect_ratio) + "px " + trace_position_y + "px";
}
cfdiv.style.backgroundSize = ( trace_size_x * aspect_ratio ) + "px " + trace_size_y + "px";
cfdiv.style.backgroundOrigin = "content-box";
set_trace_opacity(0.5);
}
}
var set_trace_opacity = function(new_trace_opacity) {
// If trace is disabled, this makes no sense.
if ( trace == 0 ) { return; }
var cf = document.querySelector("canvas#frame");
trace_opacity = new_trace_opacity;
cf.style.opacity = trace_opacity;
}
// Resets an individual character at position (x,y) to default
// attributes, like you would find at the start of a line.
var clear_char = function(x,y) {
cc[y][x] = 32; fg[y][x] = 7; bg[y][x] = 0;
tg[y][x] = 0; cs[y][x] = 0; nd[y][x] = 0;
hg[y][x] = 0; sc[y][x] = 0; sf[y][x] = 0;
}
// Sets the grid on or off and renders the whole
// frame again to show it.
var show_grid = function(newgrid) {
grid = newgrid;
editor_this.render(0, 0, 40, 25);
}
var toggle_grid = function() {
var newgrid = grid + 1;
if ( newgrid == 3 ) { newgrid = 0; }
show_grid(newgrid);
}
// Changes whether we allow black foreground.
var set_blackfg = function(newblackfg) {
blackfg = newblackfg;
// We have to do a full redraw because the *meaning* of
// some control codes have changed!
redraw();
}
// Enables or disables the display of control codes and
// refreshes the affected cells.
var toggle_codes = function() {
showcc = 1 - showcc;
show_codes(showcc);
}
var show_codes = function(newcode) {
showcc = newcode;
// Update all cells which contain a control character
for (var r = 0; r < 25; r++) {
for (var c = 0; c < 40; c++) {
if (
( cc[r][c] >= 0 && cc[r][c] <= 31 ) // a control character
|| ( sc[r][c] > 0 ) // a concealed character
) {
autorender(c, r, 1, 1, 2);
}
}
}
}
// Clears those pixels corresponding to the cells described.
// The cells' top-left corner is (x,y), and the area has width
// w and height h.
var cls = function(ctx,x,y,w,h) {
ctx.clearRect(x*12*pix_scale, y*20*pix_scale, w*12*pix_scale, h*20*pix_scale);
}
// Performs a fill character copy, including attributes.
// Copies from cell (x1, y1) to (x2,y2).
var copy_char = function(x1,y1,x2,y2) {
cc[y2][x2] = cc[y1][x1]; fg[y2][x2] = fg[y1][x1];
bg[y2][x2] = bg[y1][x1]; tg[y2][x2] = tg[y1][x1];
cs[y2][x2] = cs[y1][x1]; nd[y2][x2] = nd[y1][x1];
hg[y2][x2] = hg[y1][x1]; sc[y2][x2] = sc[y1][x1];
sf[y2][x2] = sf[y1][x1];
}
// Deletes the row that the cursor is on.
var delete_row = function(r) {
invalidate_export();
// For each row, copy the data from the row below.
for ( var y = r; y < 24; y++ ) {
for ( var x = 0; x < 40; x++ ) {
copy_char(x,y+1,x,y);
}
}
// Clear the bottom row.
for ( var x = 0; x < 40; x++ ) {
clear_char(x,24)
}
// We may have deleted a double height character, so
// we need may need to adjust for this.
adjustdh_fullscreen(0);
// Re-render the affected area.
editor_this.render(0, r, 40, 25-r);
}
// Inserts an empty row at row r.
var insert_row = function(r) {
invalidate_export();
// Working up from the bottom of the screen, copy the
// data from the row above.
for ( var y = 23; y >= r; y-- ) {
for ( var x = 0; x < 40; x++ ) {
copy_char(x,y,x,y+1);
}
}
// Clear the row.
for ( var x = 0; x < 40; x++ ) {
clear_char(x,r)
}
adjustdh_fullscreen(0);
editor_this.render(0, r, 40, 25-r);
}
// Duplicates row r to the one below it, shifting all
// the rows below it down.
var duplicate_row = function(r) {
invalidate_export();
// Working up from the bottom of the screen, copy the
// data from the row above.
for ( var y = 23; y >= r; y-- ) {
for ( var x = 0; x < 40; x++ ) {
copy_char(x,y,x,y+1);
}
}
adjustdh_fullscreen(0);
editor_this.render(0, r, 40, 25-r);
}
// Redraw the whole screen by deleting its contents and
// re-writing each character onto it.
var redraw = function() {
// Clear all attributes
for ( var y = 0; y < 25; y++ ) {
for ( var x = 0; x < 40; x++ ) {
fg[y][x] = 7; bg[y][x] = 0;
tg[y][x] = 0; cs[y][x] = 0; nd[y][x] = 0;
hg[y][x] = 0; sc[y][x] = 0; sf[y][x] = 0;
}
}
// Write each character back to the screen.
for ( var r = 0; r < 25; r++) {
for ( var c = 0; c < 40; c++) {
var code = cc[r][c];
if ( placeable(code) == 1 ) {
place_code(c, r, code, 0);
} else {
cc[r][c] = code;
}
}
}
// Re-render the whole screen.
editor_this.render(0,0,40,25);
}
// Clear each character and reset the double height
// row. If andrender is non-zero, also re-renders the
// frame.
this.wipe = function(andrender) {
invalidate_export();
for ( var r = 0; r < 25; r++ ) {
for ( var c = 0; c < 40; c++ ) {
clear_char(c, r);
}
fs[r] = 0;
}
if ( andrender != 0 ) {
editor_this.render(0,0,40,25,0);
}
}
////////////////////////////////
///// MOUSE EVENT HANDLING /////
////////////////////////////////
// The following three variables together identify the subpixel which was
// last flipped, so that we don't rapidly flicker a subpixel on and off
// when the button is pressed.
var mouse_last_x = -1;
var mouse_last_y = -1;
var mouse_last_bitflip = -1;
// A change to graphics characters might have a knock-on effect via held
// graphics to other cells. Computing this on each bit-flip is expensive,
// so we store the span (the character cells between locations (x1,y1)
// and (x2,y2)) so that its effect on other characters can be determined.
var mouse_span_x1 = -1;
var mouse_span_y1 = -1;
var mouse_span_x2 = -1;
var mouse_span_y2 = -1;
// The status of the mouse button, describing whether it's up (0) or
// down (1).
var mouse_button = 0;
// 'State' here means whether we're clearing or setting pixels for a
// particular period of holding the mouse button.
// -1 means we haven't yet got an on-off state for this,
// 0 means turn off for this drag
// 1 means turn on for this drag
var mouse_state = -1;
// Handle a mouse click. (canvasx, canvasy) are the coordinates of the
// click relative to the canvas, rather than the browser window, or
// something else. state enables the caller to pass in a current value
// of the state, and if unset (-1) sets it to the right value.
// 'Click' is a misnomer. If the mouse is dragged, that's considered a
// series of clicks.
var mouse_click = function(canvasx, canvasy, state) {
// Before processing the click, hide the help screen if it's being
// displayed.
hide_help_screen();
// First, locate the position in the character grid (x,y) of this click,
// and the position in the character cell (sx,sy) itself.
var x = Math.floor( canvasx / (12*aspect_ratio) );
var y = Math.floor( canvasy / 20 );
var sx = canvasx - ( 12 * x * aspect_ratio);
var sy = canvasy - ( 20 * y );
// Just check that we're not in rectangle selection mode. If we are, we
// just want to reposition the opposite end of the rectangle, re-render
// and return.
if ( curx_opposite != -1 && cury_opposite != -1 ) {
old_curx = curx
old_cury = cury
cury = y
curx = x
var x1 = Math.min(old_curx, curx, curx_opposite);
var y1 = Math.min(old_cury, cury, cury_opposite);
var x2 = Math.max(old_curx, curx, curx_opposite);
var y2 = Math.max(old_cury, cury, cury_opposite);
editor_this.render(x1, y1, x2 - x1 + 1, y2 - y1 + 1);
return state;
}
// Double height, of course, complicates things. If we are in double
// height mode, flipping a bit would need to be done on maybe the
// cell (x,y) and maybe the cell above. The actual character we're
// editing is called (ex,ey)
var ex = x; var ey = y;
// dh_part identifies one of four situations we could be in with respect
// to double height. We default to 0, which means the normal height,
// nothing special or unusual.
var dh_part = 0;
if ( y > 0 && nd[y][x] == 1 && fs[y] == 1 ) {
// The top row of a double height line
dh_part = 1;
}
if ( y > 0 && nd[y-1][x] == 1 && fs[y] == 2 ) {
// The bottom row of a double height line
ey = y - 1;
dh_part = 2;
}
if ( y > 0 && ( nd[y-1][x] == 0 || nd[y-1][x] == 2 ) && fs[y] == 2 ) {
// The bottom row of a double height line, but one in which there's
// no double height showing there (it's normal height or has been
// reset from double height).
ey = y - 1;
dh_part = 3;
// This can't be edited, so we just ignore it by returning the
// supplied state.
return state;
}
// Can we even edit the character here? If not just return the state
// unchanged.
// If this is a text character, let's reposition the cursor there (bug
// #20) and return.
if ( tg[ey][ex] == 0 ) {
old_curx = curx
old_cury = cury
cury = ey
curx = ex
editor_this.render(old_curx, old_cury, 1, 1);
editor_this.render(curx, cury, 1, 1);
return state;
}
if ( ! ( tg[ey][ex] == 1 &&
( ( cc[ey][ex] >= 32 && cc[ey][ex] < 64 )
|| ( cc[ey][ex] >= 96 && cc[ey][ex] < 128 ) ) ) ) { return state; }
// 'Bitflip' here means the value of the bit which we want to flip.
// It therefore uniquely identifies the subpixel.
var bitflip = 0;
// In the normal case, we just need to look up which subpixel we're
// in by considering the region each subpixel occupies.
if ( dh_part == 0 ) {
if ( sx < 6 && sy < 6 ) { bitflip = 1; }
if ( sx > 5 && sy < 6 ) { bitflip = 2; }
if ( sx < 6 && sy > 5 && sy < 14 ) { bitflip = 4; }
if ( sx > 5 && sy > 5 && sy < 14 ) { bitflip = 8; }
if ( sx < 6 && sy > 13 ) { bitflip = 16; }
if ( sx > 5 && sy > 13 ) { bitflip = 64; }
}
// If it's part of a line, we need to consider these regions
// stretched over two lines, and what the boundaries of this stretched
// region would be on each of those lines.
if ( dh_part == 1 ) { // top part
if ( sx < 6 && sy < 12 ) { bitflip = 1; }
if ( sx > 5 && sy < 12 ) { bitflip = 2; }
if ( sx < 6 && sy > 11 ) { bitflip = 4; }
if ( sx > 5 && sy > 11 ) { bitflip = 8; }
}
if ( dh_part == 2 ) { // bottom part
if ( sx < 6 && sy < 8 ) { bitflip = 4; }
if ( sx > 5 && sy < 8 ) { bitflip = 8; }
if ( sx < 6 && sy > 7 ) { bitflip = 16; }
if ( sx > 5 && sy > 7 ) { bitflip = 64; }
}
// We might just have done this, and don't want to blink the bit
// forever, so if this was the last one, just return here with
// the state unchanged.
if ( mouse_last_x == ex && mouse_last_y == ey
&& mouse_last_bitflip == bitflip ) { return state; }
// If we've moved into this subpixel (or clicked on it), and
// we've not yet decided whether we're going to set or clear
// pixels on this drag, then decide.
if ( state == -1 ) {
if ( ( cc[ey][ex] & bitflip ) > 0 ) { state = 0; } else { state = 1; }
}
// Perform the flip.
if ( state == 0 ) { cc[ey][ex] &= ~bitflip } // Switch off
if ( state == 1 ) { cc[ey][ex] |= bitflip; } // Switch on
// This will invalidate any export.
invalidate_export();
// Extend the span if we're outside of it, so we can update the
// effects of this flip on characters affected by held graphics.
if ( mouse_span_x1 == -1 || mouse_span_y1 == -1 || ey < mouse_span_y1
|| ( ey == mouse_span_y1 && ex < mouse_span_x1 )) {
mouse_span_x1 = ex;
mouse_span_y1 = ey;
}
if ( mouse_span_x2 == -1 || mouse_span_y2 == -1 || ey > mouse_span_y2
|| ( ey == mouse_span_y2 && ex > mouse_span_x2 )) {
mouse_span_x2 = ex;
mouse_span_y2 = ey;
}
// Render this character
autorender(ex,ey,1,1,0);
// Update the last subpixel visited.
mouse_last_x = ex;
mouse_last_y = ey;
mouse_last_bitflip = bitflip;
// Return the (possibly new) value of state to the caller.
return state;
}
// click_listener takes the mouse events along with the current state
// and extracts the position of the click relative to the canvas.
var click_listener = function(event, state) {
// Is it a right-click? If so, ignore it - the user is likely
// trying to save the canvas.
if ( ( event.which && event.which == 3 )
|| ( event.button && event.button == 2 ) ) {
// Just return the state which we're in already.
return state;
}
// Compute the position of the canvas.
var offsetx = 0;
var offsety = 0;
var frame_element = document.getElementById(editor_this.canvasid);
// Step up through the frame's parents and accumulate their
// contribution to the offset.
do {
offsetx += frame_element.offsetLeft - frame_element.scrollLeft;
offsety += frame_element.offsetTop - frame_element.scrollTop;
}
while( frame_element = frame_element.offsetParent )
// Taking the position of the click relative to the page, subtract
// the offset of the canvas to get the position relative to the
// canvas.
var x = event.pageX - offsetx;
var y = event.pageY - offsety;
// We clip the result to the canvas coordinates.
if ( x < 0 ) { x = 0; }
if ( x >= 12*40*aspect_ratio ) { x = 12*40*aspect_ratio - 1; }
if ( y < 0 ) { y = 0; }
if ( y >= 20*25 ) { y = 20*25 - 1; }
// mouse_click will assign a new state which we can store in the
// global variable mouse_state
return mouse_click(x, y, state);
}
// Sets up the listeners for the mouse.
this.init_mouse = function() {
var canvas = document.getElementById(editor_this.canvasid);
// What happens when the mouse button is clicked ...
canvas.addEventListener("mousedown", function (e) {
mouse_button = 1;
// what will the state be for this drag?
mouse_state = click_listener(e, -1)
// This click makes the editor associated with this
// canvas the active editor.
active_editor = editor_this;
}, false);
// ... and when it's released ...
canvas.addEventListener("mouseup", function (e) {
mouse_button = 0;
// reset all the 'last' values
mouse_last_x = -1; mouse_last_y = -1; mouse_last_bitflip = -1;
mouse_state = -1;
// Update characters if we've affected them through
// held graphics
if ( mouse_span_x1 != -1 && mouse_span_y1 != -1
&& mouse_span_x2 != -1 && mouse_span_y2 != -1 ) {
gfx_change(mouse_span_x1, mouse_span_y1,
mouse_span_x2, mouse_span_y2);
}
// Reset the span, now we have dealt with it.
mouse_span_x1 = -1;
mouse_span_y1 = -1;
mouse_span_x2 = -1;
mouse_span_y2 = -1;
// update the url now the mouse has been released
save_to_hash();
}, false);
// ... and when it's dragged.
canvas.addEventListener("mousemove", function (e) {
// If the button is down, record this as a click.
if ( mouse_button == 1 ) {
mouse_state = click_listener(e, mouse_state);
}
}, false);
}
////////////////////////////////////////////////
///// LOADING AND SAVING FROM THE URL HASH /////
////////////////////////////////////////////////
// The editor doesn't communicate with a 'cloud' or anything like
// that. Teletext frames are small enough to sit in the URL itself.
// You can then save by bookmarking the page, or pasting it into an
// email to your teletext friends, etc. The data is in the 'hash'
// part of the URL, ie the part after the # symbol. These two functions
// help us load from it and save to it. This should be very cheap
// so while we could compress and decompress, we don't need to.
// The URL contains a base-64-encoded sequence of bits. The encoding
// is standard 'base64url' with URL and Filename Safe Alphabet (RFC
// 4648 §5 'Table 2: The "URL and Filename safe" Base 64 Alphabet').
// After decoding, the seven-bit character code for column c and row
// r appears at bit positions (280r+7c) to (280r+7c+6), the most
// significant bit appearing first. This gives hash strings of 'only'
// 1122 characters.
// A direct way to load and render a hashstring.
this.load = function(hashstring) {
load_from_hashstring(hashstring);
editor_this.render(0, 0, 40, 25, 0);
}
// Loads data from the hash into the frame.
var load_from_hash = function() {
// Stop here if this isn't the editor reading from the hash.
if ( editor_this != url_editor ) { return; }
// We fetch the hash's value and remove the first character
// which is the hash symbol itself.
var hashstring = window.location.hash.substring(1);
load_from_hashstring(hashstring);
}
var load_from_hashstring = function(hashstring) {
// It's a good idea to have a bit of metadata here describing
// which character set we're using. If the colon is there, this
// metadata is assumed to be supplied.
if ( hashstring.indexOf(":") > -1 ) {
// The metadata is here, so split it out.
var parts = hashstring.split(":");
// metadata is one nybble. The most significant bit is
// whether we're enabling black foreground. The three
// least significant bits describe the character set we're
// using.
// Extract the base-10 integer, assuming 0 (English) if it
// turns out not to make sense.
var metadata = parseInt(parts[0], 16);
if ( isNaN(metadata) ) { metadata = 0; }
var cset_reqd = metadata % 8;
blackfg = 0;
if ( metadata >= 8 ) { blackfg = 1; }
// A change of character set requires a reload of the font.
if ( cset_reqd >= 0 && cset_reqd < 8 && cset != cset_reqd ) {
cset = cset_reqd;
init_font(cset);
}
// The data replaces the value in hashstring ready for
// decoding.
hashstring = parts[1];
}
// We may be dealing with old hexadecimal format, in which the
// 1920 hexadecimal digits after the colon are such that the
// byte for row r and column c (both zero-indexed) is described
// by the two hex digits starting at position 80r+2c. Base-64
// is the new format. If we get a URL in the hexadecimal format
// the editor will convert it.
if ( hashstring.length == 1920 ) {
// The alphabet of symbols!
var hexdigits = "0123456789abcdef";
// Iterate through each row and each column in that row.
for ( var r = 0; r < 24; r++) {
// It's a good test to do this backwards!
for ( var c = 39 ; c >= 0; c--) {
// Default to a space.
cc[r][c] = 32;
// The characte offset for this value is as follows:
var offset = 2 * ( ( r * 40 ) + c );
// If the data is here, turn it into an integer between 0 and
// 127, and set the cc-array with that code.
// If it's a control character, place it, so the attributes update.
if ( offset + 1 < hashstring.length ) {
var hv1 = hexdigits.indexOf(hashstring.substr(offset, 1));
var hv2 = hexdigits.indexOf(hashstring.substr(offset + 1, 1));
if ( hv1 > -1 && hv2 > -1 ) {
var newcode = ( ( hv1 * 16 ) + hv2 ) % 128;
if ( placeable(newcode) == 1 ) {
place_code(c, r, newcode, 0);
} else {
cc[r][c] = newcode;
}
}
}
}
}
}
// This block deals with the new base 64 format.
// We need to be able to handle two cases here, depending on the
// size of the frame. 24-line frames have 1120 characters, and
// 25-line frames, the new way we do things, have 1167 characters.
// 25-line frames have two bits at the end which are ignored and
// just exist for padding.
if ( hashstring.length == 1120 || hashstring.length == 1167 ) {
var numlines = 25;
if ( hashstring.length == 1120 ) { numlines = 24; }
// As we scan across the hashstring, we keep track of the
// code for the current character cell we're writing into.
var currentcode = 0;
// p is the position in the string.
for ( var p = 0; p < hashstring.length; p++ ) {
var pc = hashstring.charAt(p);
var pc_dec = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_"
.indexOf(hashstring.charAt(p));
// b is the bit in the 6-bit base-64 character.
for ( var b = 0; b < 6; b++ ) {
// The current bit posiiton in the character being
// written to.
var charbit = ( 6*p + b ) % 7;
// The bit value (set or unset) of the bit we're
// reading from.
var b64bit = pc_dec & ( 1 << ( 5 - b ) );
if ( b64bit > 0 ) { b64bit = 1; }
// Update the current code.
currentcode |= b64bit << ( 6 - charbit );
// If we've reached the end of this character cell
// and it's the last bit in the character we're
// writing to, set the character code or place the
// code.
if ( charbit == 6 ) {
// Work out the cell to write to and put it there.
var charnum = ( ( 6*p + b ) - charbit ) / 7;
var c = charnum % 40;
var r = (charnum - c) / 40;
if ( placeable(currentcode) == 1 ) {
place_code(c, r, currentcode, 0);
} else {
cc[r][c] = currentcode;
}
// Reset for next time.
currentcode = 0;
}
}
}
// If we only read in a 24-line file, we need to blank the final
// line.
if ( numlines == 24 ) {
for ( var x = 0; x < 40; x++ ) { clear_char(x,24) }
}
}
}
// Similarly, we want to save the page to the hash. This simply
// converts the character set and page data into a hex string and
// puts it there.
// Now that the editor is 25 lines, this format is the one we
// save to. There are two whole left-over bits at the end of
// the encoding. Yes, it's wasteful, but for now, we'll have to
// let that go.
var save_to_hash = function() {
// Stop here if this isn't the editor reading from the hash.
if ( editor_this != url_editor ) { return; }
var encoding = "";
if (mouse_button == 1) {
// optimisation: don't update hash while drawing
return;
}
// Construct the metadata as described above.
var metadata = cset;
if ( blackfg != 0 ) { metadata += 8; }
encoding += metadata.toString(16);
encoding += ":";
// Construct a base-64 array by iterating over each character
// in the frame.
var b64 = [];
for ( var r=0; r<25; r++ ) {
for ( var c=0; c<40; c++ ) {
for ( var b=0; b<7; b++ ) {
// How many bits into the frame information we
// are.
var framebit = 7 * (( r * 40 ) + c) + b;
// Work out the position of the character in the
// base-64 encoding and the bit in that position.
var b64bitoffset = framebit % 6;
var b64charoffset = ( framebit - b64bitoffset ) / 6;
// Read a bit and write a bit.
var bitval = cc[r][c] & ( 1 << ( 6 - b ));
if ( bitval > 0 ) { bitval = 1; }
b64[b64charoffset] |= bitval << ( 5 - b64bitoffset );
}
}
}
// Encode bit-for-bit.
for ( var i = 0; i < 1167; i++ ) {
encoding += "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_".charAt(b64[i]);
}
if (window.location.hash != encoding) { window.location.hash = encoding; }
}
// Many people asked for a way to export frames from the editor. This function
// provides one way to do that which is easier than converting the URL. It
// converts the frame to raw format, and then edits the document to show a link
// using the data URI scheme. The user can save this link to their own computer
// without the server needing to store it.
var export_frame = function() {
// People have requested graphic file exports, so we hide the status
// bar in case we need to do this here.
hide_status_bar();
// We can't substitute characters for the base64 in the address bar
// becase the output must constain newlines and the addressbar uses
// seven bits for each character. Therefore we must export in a
// different way.
var rawstring_0 = "";
var rawstring_1 = "";
// We also construct TTI files for wxTED.
// PN: page number
var ttistring = "PN,"
+ m_page.toString(16).toUpperCase()
+ padstring("0", 2, m_subpage.toString())
+ "\r\n";
// DE: description
ttistring = ttistring + "DE,edit-tf\r\n";
// We construct the TTI page status (PS) value by copying over
// the values of the control bits.
var tti_ps = 0x8000; // normal parallel transmission
if ( m_control[4] != 0 ) { tti_ps += 0x4000; }
if ( m_control[5] != 0 ) { tti_ps += 0x0001; }
if ( m_control[6] != 0 ) { tti_ps += 0x0002; }
if ( m_control[7] != 0 ) { tti_ps += 0x0004; }
if ( m_control[8] != 0 ) { tti_ps += 0x0008; }
if ( m_control[9] != 0 ) { tti_ps += 0x0010; }
if ( m_control[10] != 0 ) { tti_ps += 0x0020; }
if ( m_control[11] != 0 ) { tti_ps += 0x0040; }
if ( m_control[12] != 0 ) { tti_ps += 0x0200; }
if ( m_control[13] != 0 ) { tti_ps += 0x0100; }
if ( m_control[14] != 0 ) { tti_ps += 0x0080; }
ttistring = ttistring + "PS,"
+ padstring("0", 4, tti_ps.toString(16).toUpperCase())
+ "\r\n";
// SC: subcode
ttistring = ttistring + "SC,"
+ padstring("0", 4, m_subcode.toString(16).toUpperCase())
+ "\r\n";