-
Notifications
You must be signed in to change notification settings - Fork 0
/
a11y_page.html
1205 lines (966 loc) · 44.1 KB
/
a11y_page.html
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
<html><head>
<title>PICO-8 Cartridge</title>
<meta name="viewport" content="width=device-width, user-scalable=no">
<style>
.p8_start_button{
/* a11y_page - if you have a background image, you can add it here */
/* by default, this will be on line 985 */
background:none;
}
</style>
<script type="text/javascript">
/* a11y_page - make sure to point this to wherever your actual cart lives */
window.cart_source = "./sample/aria_test.js"
// Default shell for PICO-8 0.2.2 (includes @weeble's gamepad mod 1.0)
// This file is available under a CC0 license https://creativecommons.org/share-your-work/public-domain/cc0/
// (note: "this file" does not include any cartridge or cartridge artwork injected into a derivative html file when using the PICO-8 html exporter)
// options
// fullscreen, sound, close button at top when playing on touchscreen
var p8_allow_mobile_menu = true;
// p8_autoplay true to boot the cartridge automatically after page load when possible
// if the browser can not create an audio context outside of a user gesture (e.g. on iOS), p8_autoplay has no effect
var p8_autoplay = false;
// When pico8_state is defined, PICO-8 will set .is_paused, .sound_volume and .frame_number each frame
// (used for determining button icons)
var pico8_state = [];
// When pico8_buttons is defined, PICO-8 reads each int as a bitfield holding that player's button states
// 0x1 left, 0x2 right, 0x4 up, 0x8 right, 0x10 O, 0x20 X, 0x40 menu
// (used by p8_update_gamepads)
var pico8_buttons = [0, 0, 0, 0, 0, 0, 0, 0]; // max 8 players
// When pico8_mouse is defined, PICO-8 reads the 3 integers as X, Y and a bitfield for buttons: 0x1 LMB, 0x2 RMB
var pico8_mouse = [];
// used to display number of detected joysticks
var pico8_gamepads = {};
pico8_gamepads.count = 0;
// When pico8_gpio is defined, reading and writing to gpio pins will read and write to these values
var pico8_gpio = new Array(128);
// When pico8_audio_context context is defined, the html shell (this file) is responsible for creating and managing it.
// This makes satisfying browser requirements easier -- e.g. initialising audio from a short script in response to a user action.
// Otherwise PICO-8 will try to create and use its own context.
var pico8_audio_context;
// menu button and controller graphics
p8_gfx_dat={
"p8b_pause1": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAYAAADgdz34AAAAOUlEQVRIx2NgGPbg/8cX/0F46FtAM4vobgHVLRowC6hm0YBbQLFFoxaM4FQ0dHPy0C1Nh26NNugBAAnizNiMfvbGAAAAAElFTkSuQmCC",
"p8b_controls":"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAYAAADgdz34AAAAQ0lEQVRIx2NgGAXEgP8fX/ynBaap4XBLhqcF1IyfYWQBrZLz0LEAlzqqxQFVLcAmT3MLqJqTaW7B4CqLaF4fjIIBBwBL/B2vqtPVIwAAAABJRU5ErkJggg==",
"p8b_full":"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAYAAADgdz34AAAAN0lEQVRIx2NgGPLg/8cX/2mJ6WcBrUJm4CwgOSgGrQVEB8WoBaMWDGMLhm5OHnql6dCt0YY8AAA9oZm+9Z9xQAAAAABJRU5ErkJggg==",
"p8b_pause0":"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAYAAADgdz34AAAAKUlEQVRIx2NgGHbg/8cX/7FhctWNWjBqwagFoxaMWjBqwagF5Fkw5AAAPaGZvsIUtXUAAAAASUVORK5CYII=",
"p8b_sound0":"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAYAAADgdz34AAAANklEQVRIx2NgGDHg/8cX/5Hx0LEA3cChYwEugwavBcRG4qgFoxYMZwuGfk4efqXp8KnRBj0AAMz7cLDnG4FeAAAAAElFTkSuQmCC",
"p8b_sound1":"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAYAAADgdz34AAAAPUlEQVRIx2NgGDHg/8cX/5Hx0LEA3cChYwEugwhZQLQDqG4BsZFIKMhGLRi1YChbMPRz8vArTYdPjTboAQCSVgpXUWQAMAAAAABJRU5ErkJggg==",
"p8b_close":"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAYAAADgdz34AAAAU0lEQVRIx2NkoDFgpJsF/z+++I8iwS9BkuW49A+cBcRaREgf/Swg1SJi1dHfAkIG4EyOOIJy4Cwg1iJCiWDUAvItGLqpaOjm5KFfmg79Gm3ItioAl+mAGVYIZUUAAAAASUVORK5CYII=",
"controls_left_panel":"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAASwAAAEsCAYAAAB5fY51AAAEI0lEQVR42u3dMU7DQBCG0Tjam9DTcP8jpEmfswS5iHBhAsLxev/hvQY6pGXyZRTQ+nQCAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHqbHEEtl+vt7hS+fLy/mXHBQqxEi/6aI/AiFW9SnB2BWDkDBAtAsADBAhAsAMECBAtAsAAECxAsAMECECxAsAAEC0CwONJ8tYvrXRAsImK19j0IFsPGSrQQLCJiNV+et7xAT7QQLIaN1dr3ooVgMWysRAvBIipWooVgERUr0UKwiIqVaCFYRMVKtBAsomIlWggWUbESLQSLqFiJFoJFVKxEC8EiKlaihWARFSvRQrDYJSSVfhaCBSBYAIIFCBbAHpoj4Bl/scOGBWDD4lX8iwE2LADBAgQLQLAABAsQLADBAhAsQLAABAtAsADBAhAsAMECBAtAsAAECxAsAMECECxAsAAECxAsAMECECxAsMh1ud7uTsHZVDcZyFo8Yt5sVJ6NyUAaSNEyIymaXwZepIKd4mwoQbAFC0CwAMECECwAwQIEC0CwAAQLECwAwQIQLECwAAQLQLAAwQI4UHME2/10QZq7usyBObBhRQwpmBUb1nADuPbuaUD/p2ezMH+1admwhosVfBcxb2SCJVaIlmAhVoiWYIkVoiVagiVWiJZgiZVYIVqCJVaIlmgJllghWoIlViBagiVWiJZoCZZYIVqCJVYgWoIlViBaggUIlnc0sPELlmghVmIlWKKFWAmWaIFYCZZoIVYIlmghVoIlWiBWgiVaiJVgIVqIlWCJFoiVYIkWYiVYiBZiJViihViJ1XbNEWyL1mMQRYvfvIGJlQ1rmE0LzIoNyyBiDrBhAYIFIFiAYAEIFoBgAYIFIFgAggUIFoBgAQgWIFgAggUgWIBgDc+Nn1D/tdH8YupwgZy5qG4ykKIlVmZDsDjshSlazqQqH7p793Q2CBaAYAGCBSBYAIIFCBaAYAEIFiBYAIIFIFiAYAEIFoBgAYIFIFgAggUIFoBgAQgWIFgAggUgWIBgAQgWwENzBKxZPub9CJ7WjA0LsGFRV+9N5+jNDhsWgGABggUgWACCxW56fgjuA3cEiz9Z/nWwR0iWP8P/YCFYDBstsUKwiIiWWCFYRERLrBAsIqIlVggWEdESKwSLiGiJFYJFRLTECsEiIlpihWARES2xQrCIiJZYIVhEREusECwioiVWCBYx0RIrBIuoaIkVr+YhFHTZtMCGBQgWgGABCBYgWACCBSBYgGABCBaAYAGCBSBYAIIFCBbj2uOR8s6AEbhexgsWYri3SKhKczcXAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAMA2n+e0UMDzh3yTAAAAAElFTkSuQmCC",
"controls_right_panel":"data:image/png;base64,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",
};
// added 0.2.1: work-around for iOS/Safari running from an iFrame (e.g. from itch.io page):
// touch events only register after adding dummy listeners on document.
document.addEventListener('touchstart', {});
document.addEventListener('touchmove', {});
document.addEventListener('touchend', {});
// --------------------------------------------------------------------------------------------------------------------------------
// pico-8 0.2.2: allow dropping files
var p8_dropped_cart = null;
var p8_dropped_cart_name = "";
function p8_drop_file(e)
{
// console.log("@@ dropping file...");
e.stopPropagation();
e.preventDefault();
if (e.dataTransfer && e.dataTransfer.files && e.dataTransfer.files[0])
{
// read from file
reader = new FileReader();
reader.onload = function (event)
{
p8_dropped_cart_name = 'untitled.p8';
if (typeof e.dataTransfer.files[0].name !== 'undefined') p8_dropped_cart_name = e.dataTransfer.files[0].name;
if (typeof e.dataTransfer.files[0].fileName !== 'undefined') p8_dropped_cart_name = e.dataTransfer.files[0].fileName;
p8_dropped_cart = reader.result;
// data:image/png;base64
e.stopPropagation();
e.preventDefault();
codo_command = 9; // read directly from p8_dropped_cart with libb64 decoder
};
reader.readAsDataURL(e.dataTransfer.files[0]);
}
else
{
// read from url (or data url)
txt = e.dataTransfer.getData('Text');
if (txt){
p8_dropped_cart_name = "untitled.p8.png";
p8_dropped_cart = txt;
codo_command = 9;
}
}
}
function nop(evt) {
evt.stopPropagation();
evt.preventDefault();
}
function dragover(evt) {
evt.stopPropagation();
evt.preventDefault();
Module.pico8DragOver();
}
function dragstop(evt) {
evt.stopPropagation();
evt.preventDefault();
Module.pico8DragStop();
}
// download (pico-8 0.2.4d web exports can save a .wav file)
function download_browser_file(filename, contents)
{
var element = document.createElement('a');
if (filename.substr(filename.length - 7) == ".p8.png")
element.setAttribute('href', 'data:image/png;base64,' + encodeURIComponent(contents));
else if (filename.substr(filename.length - 4) == ".wav")
element.setAttribute('href', 'data:audio/x-wav;base64,' + encodeURIComponent(contents));
else
element.setAttribute('href', 'data:text/plain;charset=utf-8,' + encodeURIComponent(contents));
element.setAttribute('download', filename);
element.style.display = 'none';
document.body.appendChild(element);
element.click();
document.body.removeChild(element);
}
// --------------------------------------------------------------------------------------------------------------------------------
var p8_buttons_hash = -1;
function p8_update_button_icons()
{
// buttons only appear when running
if (!p8_is_running)
{
requestAnimationFrame(p8_update_button_icons);
return;
}
var is_fullscreen=(document.fullscreenElement || document.mozFullScreenElement || document.webkitIsFullScreen || document.msFullscreenElement);
// hash based on: pico8_state.sound_volume pico8_state.is_paused bottom_margin left is_fullscreen p8_touch_detected
var hash = 0;
hash = pico8_state.sound_volume;
if (pico8_state.is_paused) hash += 0x100;
if (p8_touch_detected) hash += 0x200;
if (is_fullscreen) hash += 0x400;
if (p8_buttons_hash == hash)
{
requestAnimationFrame(p8_update_button_icons);
return;
}
p8_buttons_hash = hash;
// console.log("@@ updating button icons");
els = document.getElementsByClassName('p8_menu_button');
for (i = 0; i < els.length; i++)
{
el = els[i];
index = el.id;
if (index == 'p8b_sound') index += (pico8_state.sound_volume == 0 ? "0" : "1"); // 1 if undefined
if (index == 'p8b_pause') index += (pico8_state.is_paused > 0 ? "1" : "0"); // 0 if undefined
new_str = '<img aria-hidden="true" width=24 height=24 style="pointer-events:none" src="'+p8_gfx_dat[index]+'">';
if (el.innerHTML != new_str)
el.innerHTML = new_str;
// hide all buttons for touch mode (can pause with menu buttons)
var is_visible = p8_is_running;
if ((!p8_touch_detected || !p8_allow_mobile_menu) && el.parentElement.id == "p8_menu_buttons_touch")
is_visible = false;
if (p8_touch_detected && el.parentElement.id == "p8_menu_buttons")
is_visible = false;
if (is_fullscreen)
is_visible = false;
if (is_visible)
el.style.display="";
else
el.style.display="none";
}
requestAnimationFrame(p8_update_button_icons);
}
function abs(x)
{
return x < 0 ? -x : x;
}
// step 0 down 1 drag 2 up (not used)
function pico8_buttons_event(e, step)
{
if (!p8_is_running) return;
pico8_buttons[0] = 0;
if (step == 2 && typeof(pico8_mouse) !== 'undefined')
{
pico8_mouse[2] = 0;
}
var num = 0;
if (e.touches) num = e.touches.length;
if (num == 0 && typeof(pico8_mouse) !== 'undefined')
{
// no active touches: release mouse button from anywhere on page. (maybe redundant? but just in case)
pico8_mouse[2] = 0;
}
for (var i = 0; i < num; i++)
{
var touch = e.touches[i];
var x = touch.clientX;
var y = touch.clientY;
var w = window.innerWidth;
var h = window.innerHeight;
var r = Math.min(w,h) / 12;
if (r > 40) r = 40;
// mouse (0.1.12d)
let canvas = document.getElementById("canvas");
if (p8_touch_detected)
if (typeof(pico8_mouse) !== 'undefined')
if (canvas)
{
var rect = canvas.getBoundingClientRect();
//console.log(rect.top, rect.right, rect.bottom, rect.left, x, y);
if (x >= rect.left && x < rect.right && y >= rect.top && y < rect.bottom)
{
pico8_mouse = [
Math.floor((x - rect.left) * 128 / (rect.right - rect.left)),
Math.floor((y - rect.top) * 128 / (rect.bottom - rect.top)),
step < 2 ? 1 : 0
];
// return; // commented -- blocks overlapping buttons
}else
{
pico8_mouse[2] = 0;
}
}
// buttons
b = 0;
if (y < h - r*8)
{
// no controller buttons up here; includes canvas and menu buttons at top in touch mode
}
else
{
e.preventDefault();
if ((y < h - r*6) && y > (h - r*8))
{
// menu button: half as high as X O button
// stretch across right-hand half above X O buttons
if (x > w - r*3)
b |= 0x40;
}
else if (x < w/2 && x < r*6)
{
// stick
mask = 0xf; // dpad
var cx = 0 + r*3;
var cy = h - r*3;
deadzone = r/3;
var dx = x - cx;
var dy = y - cy;
if (abs(dx) > abs(dy) * 0.6) // horizontal
{
if (dx < -deadzone) b |= 0x1;
if (dx > deadzone) b |= 0x2;
}
if (abs(dy) > abs(dx) * 0.6) // vertical
{
if (dy < -deadzone) b |= 0x4;
if (dy > deadzone) b |= 0x8;
}
}
else if (x > w - r*6)
{
// button; diagonal split from bottom right corner
mask = 0x30;
// one or both of [X], [O]
if ( (h-y) > (w-x) * 0.8) b |= 0x10;
if ( (w-x) > (h-y) * 0.8) b |= 0x20;
}
}
pico8_buttons[0] |= b;
}
}
// p8_update_layout_hash is used to decide when to update layout (expensive especially when part of a heavy page)
var p8_update_layout_hash = -1;
var last_windowed_container_height = 512;
var p8_layout_frames = 0;
function p8_update_layout()
{
var canvas = document.getElementById("canvas");
var p8_playarea = document.getElementById("p8_playarea");
var p8_container = document.getElementById("p8_container");
var p8_frame = document.getElementById("p8_frame");
var csize = 512;
var margin_top = 0;
var margin_left = 0;
// page didn't load yet? first call should be after p8_frame is created so that layout doesn't jump around.
if (!canvas || !p8_playarea || !p8_container || !p8_frame)
{
p8_update_layout_hash = -1;
requestAnimationFrame(p8_update_layout);
return;
}
p8_layout_frames ++;
// assumes frame doesn't have padding
var is_fullscreen=(document.fullscreenElement || document.mozFullScreenElement || document.webkitIsFullScreen || document.msFullscreenElement);
var frame_width = p8_frame.offsetWidth;
var frame_height = p8_frame.offsetHeight;
if (is_fullscreen)
{
// same as window
frame_width = window.innerWidth;
frame_height = window.innerHeight;
}
else{
// never larger than window // (happens when address bar is down in portraight mode on phone)
frame_width = Math.min(frame_width, window.innerWidth);
frame_height = Math.min(frame_height, window.innerHeight);
}
// as big as will fit in a frame..
csize = Math.min(frame_width,frame_height);
// .. but never more than 2/3 of longest side for touch (e.g. leave space for controls on iPad)
if (p8_touch_detected && p8_is_running)
{
var longest_side = Math.max(window.innerWidth,window.innerHeight);
csize = Math.min(csize, longest_side * 2/3);
}
// pixel perfect: quantize to closest multiple of 128
// only when large display (desktop)
if (frame_width >= 512 && frame_height >= 512)
{
csize = (csize+1) & ~0x7f;
}
// csize should never be higher than parent frame
// (otherwise stretched large when fullscreen and then return)
if (!is_fullscreen && p8_frame)
csize = Math.min(csize, last_windowed_container_height); // p8_frame_0 parent
if (is_fullscreen)
{
// always center horizontally
margin_left = (frame_width - csize)/2;
if (p8_touch_detected)
{
if (window.innerWidth < window.innerHeight)
{
// portrait: keep at y=40 (avoid rounded top corners / camera nub thing etc.)
margin_top = Math.min(40, frame_height - csize);
}
else
{
// landscape: put a little above vertical center
margin_top = (frame_height - csize)/4;
}
}
else{
// non-touch: center vertically
margin_top = (frame_height - csize)/2;
}
}
// skip if relevant state has not changed
var update_hash = csize + margin_top * 1000.3 + margin_left * 0.001 + frame_width * 333.33 + frame_height * 772.15134;
if (is_fullscreen) update_hash += 0.1237;
// unexpected things can happen in the first few seconds, so just keep re-calculating layout. wasm version breaks layout otherwise.
// also: bonus refresh at 5, 8 seconds just in case ._.
if (p8_layout_frames < 180 || p8_layout_frames == 60*5 || p8_layout_frames == 60*8 )
update_hash = p8_layout_frames;
if (!is_fullscreen) // fullscreen: update every frame for safety. should be cheap!
if (!p8_touch_detected) // mobile: update every frame because nothing can be trusted
if (p8_update_layout_hash == update_hash)
{
//console.log("p8_update_layout(): skipping");
requestAnimationFrame(p8_update_layout);
return;
}
p8_update_layout_hash = update_hash;
// record this for returning to original size after fullscreen pushes out container height (argh)
if (!is_fullscreen && p8_frame)
last_windowed_container_height = p8_frame.parentNode.parentNode.offsetHeight;
// mobile in portrait mode: put screen at top (w / a little extra space for fullscreen button if needed)
// (don't cart too about buttons overlapping screen)
if (p8_touch_detected && p8_is_running && document.body.clientWidth < document.body.clientHeight)
p8_playarea.style.marginTop = p8_allow_mobile_menu ? 32 : 8;
else if (p8_touch_detected && p8_is_running) // landscape: slightly above vertical center (only relevant for iPad / highres devices)
p8_playarea.style.marginTop = (document.body.clientHeight - csize) / 4;
else
p8_playarea.style.marginTop = "";
canvas.style.width = csize;
canvas.style.height = csize;
// to do: this should just happen from css layout
canvas.style.marginLeft = margin_left;
canvas.style.marginTop = margin_top;
p8_container.style.width = csize;
p8_container.style.height = csize;
// set menu buttons position to bottom right
el = document.getElementById("p8_menu_buttons");
el.style.marginTop = csize - el.offsetHeight;
if (p8_touch_detected && p8_is_running)
{
// turn off pointer events to prevent double-tap zoom etc (works on Android)
// don't want this for desktop because breaks mouse input & click-to-focus when using codo_textarea
canvas.style.pointerEvents = "none";
p8_container.style.marginTop = "0px";
// buttons
// same as touch event handling
var w = window.innerWidth;
var h = window.innerHeight;
var r = Math.min(w,h) / 12;
if (r > 40) r = 40;
el = document.getElementById("controls_right_panel");
el.style.left = w-r*6;
el.style.top = h-r*7;
el.style.width = r*6;
el.style.height = r*7;
if (el.getAttribute("src") != p8_gfx_dat["controls_right_panel"]) // optimisation: avoid reload? (browser should handle though)
el.setAttribute("src", p8_gfx_dat["controls_right_panel"]);
el = document.getElementById("controls_left_panel");
el.style.left = 0;
el.style.top = h-r*6;
el.style.width = r*6;
el.style.height = r*6;
if (el.getAttribute("src") != p8_gfx_dat["controls_left_panel"]) // optimisation: avoid reload? (browser should handle though)
el.setAttribute("src", p8_gfx_dat["controls_left_panel"]);
// scroll to cart (commented; was a failed attempt to prevent scroll-on-drag on some browsers)
// p8_frame.scrollIntoView(true);
document.getElementById("touch_controls_gfx").style.display="table";
document.getElementById("touch_controls_background").style.display="table";
}
else{
document.getElementById("touch_controls_gfx").style.display="none";
document.getElementById("touch_controls_background").style.display="none";
}
if (!p8_is_running)
{
p8_playarea.style.display="none";
p8_container.style.display="flex";
p8_container.style.marginTop="auto";
el = document.getElementById("p8_start_button");
if (el) el.style.display="flex";
}
requestAnimationFrame(p8_update_layout);
}
var p8_touch_detected = false;
addEventListener("touchstart", function(event)
{
p8_touch_detected = true;
// hide codo_textarea -- clipboard support on mobile is not feasible
el = document.getElementById("codo_textarea");
if (el && el.style.display != "none"){
el.style.display="none";
}
}, {passive: true});
function p8_create_audio_context()
{
if (pico8_audio_context)
{
try {
pico8_audio_context.resume();
}
catch(err) {
console.log("** pico8_audio_context.resume() failed");
}
return;
}
var webAudioAPI = window.AudioContext || window.webkitAudioContext || window.mozAudioContext || window.oAudioContext || window.msAudioContext;
if (webAudioAPI)
{
pico8_audio_context = new webAudioAPI;
// wake up iOS
if (pico8_audio_context)
{
try {
var dummy_source_sfx = pico8_audio_context.createBufferSource();
dummy_source_sfx.buffer = pico8_audio_context.createBuffer(1, 1, 22050); // dummy
dummy_source_sfx.connect(pico8_audio_context.destination);
dummy_source_sfx.start(1, 0.25); // gives InvalidStateError -- why? hasn't been played before
//dummy_source_sfx.noteOn(0); // deleteme
}
catch(err) {
console.log("** dummy_source_sfx.start(1, 0.25) failed");
}
}
}
}
function p8_close_cart()
{
// just reload page! used for touch buttons -- hard to roll back state
window.location.hash = ""; // triggers reload
}
var p8_is_running = false;
var p8_script = null;
var Module = null;
function p8_run_cart()
{
if (p8_is_running) return;
p8_is_running = true;
// touch: hide everything except p8_frame_0
if (p8_touch_detected)
{
el = document.getElementById("body_0");
el2 = document.getElementById("p8_frame_0");
if (el && el2)
{
el.style.display="none";
el.parentNode.appendChild(el2);
}
}
// create audio context and wake it up (for iOS -- needs happen inside touch event)
p8_create_audio_context();
// show touch elements
els = document.getElementsByClassName('p8_controller_area');
for (i = 0; i < els.length; i++)
els[i].style.display="";
// install touch events. These also serve to block scrolling / pinching / zooming on phones when p8_is_running
// moved event.preventDefault(); calls into pico8_buttons_event() (want to let top buttons pass through)
addEventListener("touchstart", function(event){ pico8_buttons_event(event, 0); }, {passive: false});
addEventListener("touchmove", function(event){ pico8_buttons_event(event, 1); }, {passive: false});
addEventListener("touchend", function(event){ pico8_buttons_event(event, 2); }, {passive: false});
// load and run script
e = document.createElement("script");
p8_script = e;
e.onload = function(){
// show canvas / menu buttons only after loading
el = document.getElementById("p8_playarea");
if (el) el.style.display="table";
if (typeof(p8_update_layout_hash) !== 'undefined')
p8_update_layout_hash = -77;
if (typeof(p8_buttons_hash) !== 'undefined')
p8_buttons_hash = -33;
}
e.type = "application/javascript";
e.src = window.cart_source;
e.id = "e_script";
document.body.appendChild(e); // load and run
// hide start button and show canvas / menu buttons. hide start button
el = document.getElementById("p8_start_button");
if (el) el.style.display="none";
// add #playing for touchscreen devices (allows back button to close)
// X button can also be used to trigger this
if (p8_touch_detected)
{
window.location.hash = "#playing";
window.onhashchange = function()
{
if (window.location.hash.search("playing") < 0)
window.location.reload();
}
}
// install drag&drop listeners
{
let canvas = document.getElementById("canvas");
if (canvas)
{
canvas.addEventListener('dragenter', dragover, false);
canvas.addEventListener('dragover', dragover, false);
canvas.addEventListener('dragleave', dragstop, false);
canvas.addEventListener('drop', nop, false);
canvas.addEventListener('drop', p8_drop_file, false);
}
}
}
// Gamepad code
var P8_BUTTON_O = {action:'button', code: 0x10};
var P8_BUTTON_X = {action:'button', code: 0x20};
var P8_DPAD_LEFT = {action:'button', code: 0x1};
var P8_DPAD_RIGHT = {action:'button', code: 0x2};
var P8_DPAD_UP = {action:'button', code: 0x4};
var P8_DPAD_DOWN = {action:'button', code: 0x8};
var P8_MENU = {action:'menu'};
var P8_NO_ACTION = {action:'none'};
var P8_BUTTON_MAPPING = [
// ref: https://w3c.github.io/gamepad/#remapping
P8_BUTTON_O, // Bottom face button
P8_BUTTON_X, // Right face button
P8_BUTTON_X, // Left face button
P8_BUTTON_O, // Top face button
P8_NO_ACTION, // Near left shoulder button (L1)
P8_NO_ACTION, // Near right shoulder button (R1)
P8_NO_ACTION, // Far left shoulder button (L2)
P8_NO_ACTION, // Far right shoulder button (R2)
P8_MENU, // Left auxiliary button (select)
P8_MENU, // Right auxiliary button (start)
P8_NO_ACTION, // Left stick button
P8_NO_ACTION, // Right stick button
P8_DPAD_UP, // Dpad up
P8_DPAD_DOWN, // Dpad down
P8_DPAD_LEFT, // Dpad left
P8_DPAD_RIGHT, // Dpad right
];
// Track which player is controller by each gamepad. Gamepad index i controls the
// player with index pico8_gamepads_mapping[i]. Gamepads with null player are
// currently unassigned - they get assigned to a player when a button is pressed.
var pico8_gamepads_mapping = [];
function p8_unassign_gamepad(gamepad_index) {
if (pico8_gamepads_mapping[gamepad_index] == null) {
return;
}
pico8_buttons[pico8_gamepads_mapping[gamepad_index]] = 0;
pico8_gamepads_mapping[gamepad_index] = null;
}
function p8_first_player_without_gamepad(max_players) {
var allocated_players = pico8_gamepads_mapping.filter(function(x) { return x != null; });
var sorted_players = Array.from(allocated_players).sort();
for (var desired = 0; desired < sorted_players.length && desired < max_players; ++desired) {
if (desired != sorted_players[desired]) {
return desired;
}
}
if (sorted_players.length < max_players) {
return sorted_players.length;
}
return null;
}
function p8_assign_gamepad_to_player(gamepad_index, player_index) {
p8_unassign_gamepad(gamepad_index);
pico8_gamepads_mapping[gamepad_index] = player_index;
}
function p8_convert_standard_gamepad_to_button_state(gamepad, axis_threshold, button_threshold) {
// Given a gamepad object, return:
// {
// button_state: the binary encoded Pico 8 button state
// menu_button: true if any menu-mapped button was pressed
// any_button: true if any button was pressed, including d-pad
// buttons and unmapped buttons
// }
if (!gamepad || !gamepad.axes || !gamepad.buttons) {
return {
button_state: 0,
menu_button: false,
any_button: false
};
}
function button_state_from_axis(axis, low_state, high_state, default_state) {
if (axis && axis < -axis_threshold) return low_state;
if (axis && axis > axis_threshold) return high_state;
return default_state;
}
var axes_actions = [
button_state_from_axis(gamepad.axes[0], P8_DPAD_LEFT, P8_DPAD_RIGHT, P8_NO_ACTION),
button_state_from_axis(gamepad.axes[1], P8_DPAD_UP, P8_DPAD_DOWN, P8_NO_ACTION),
];
var button_actions = gamepad.buttons.map(function (button, index) {
var pressed = button.value > button_threshold || button.pressed;
if (!pressed) return P8_NO_ACTION;
return P8_BUTTON_MAPPING[index] || P8_NO_ACTION;
});
var all_actions = axes_actions.concat(button_actions);
var menu_button = button_actions.some(function (action) { return action.action == 'menu'; });
var button_state = (all_actions
.filter(function (a) { return a.action == 'button'; })
.map(function (a) { return a.code; })
.reduce(function (result, code) { return result | code; }, 0)
);
var any_button = gamepad.buttons.some(function (button) {
return button.value > button_threshold || button.pressed;
});
any_button |= button_state; //jww: include axes 0,1 as might be first intended action
return {
button_state,
menu_button,
any_button
};
}
// jww: pico-8 0.2.1 version for unmapped gamepads, following p8_convert_standard_gamepad_to_button_state
// axes 0,1 & buttons 0,1,2,3 are reasonably safe. don't try to read dpad.
// menu buttons are unpredictable, but use 6..8 anyway (better to have a weird menu button than none)
function p8_convert_unmapped_gamepad_to_button_state(gamepad, axis_threshold, button_threshold) {
if (!gamepad || !gamepad.axes || !gamepad.buttons) {
return {
button_state: 0,
menu_button: false,
any_button: false
};
}
var button_state = 0;
if (gamepad.axes[0] && gamepad.axes[0] < -axis_threshold) button_state |= 0x1;
if (gamepad.axes[0] && gamepad.axes[0] > axis_threshold) button_state |= 0x2;
if (gamepad.axes[1] && gamepad.axes[1] < -axis_threshold) button_state |= 0x4;
if (gamepad.axes[1] && gamepad.axes[1] > axis_threshold) button_state |= 0x8;
// buttons: first 4 taken to be O/X, 6..8 taken to be menu button
for (j = 0; j < gamepad.buttons.length; j++)
if (gamepad.buttons[j].value > 0 || gamepad.buttons[j].pressed)
{
if (j < 4)
button_state |= (0x10 << (((j+1)/2)&1)); // 0 1 1 0 -- A,X -> O,X on xbox360
else if (j >= 6 && j <= 8)
button_state |= 0x40;
}
var menu_button = button_state & 0x40;
var any_button = gamepad.buttons.some(function (button) {
return button.value > button_threshold || button.pressed;
});
any_button |= button_state; //jww: include axes 0,1 as might be first intended action
return {
button_state,
menu_button,
any_button
};
}
// gamepad https://developer.mozilla.org/en-US/docs/Web/API/Gamepad_API/Using_the_Gamepad_API
// (sets bits in pico8_buttons[])
function p8_update_gamepads() {
var axis_threshold = 0.3;
var button_threshold = 0.5; // Should be unnecessary, we should be able to trust .pressed
var max_players = 8;
var gps = navigator.getGamepads() || navigator.webkitGetGamepads();
if (!gps) return;
// In Chrome, gps is iterable but it's not an array.
gps = Array.from(gps);
pico8_gamepads.count = gps.length;
while (gps.length > pico8_gamepads_mapping.length) {
pico8_gamepads_mapping.push(null);
}
var menu_button = false;
var gamepad_states = gps.map(function (gp) {
return (gp && gp.mapping == "standard") ?
p8_convert_standard_gamepad_to_button_state(gp, axis_threshold, button_threshold) :
p8_convert_unmapped_gamepad_to_button_state(gp, axis_threshold, button_threshold);
});
// Unassign disconnected gamepads.
// gps.forEach(function (gp, i) { if (gp && !gp.connected) { p8_unassign_gamepad(i); }});
gps.forEach(function (gp, i) { if (!gp || !gp.connected) { p8_unassign_gamepad(i); }}); // https://www.lexaloffle.com/bbs/?pid=87132#p
// Assign unassigned gamepads when any button is pressed.
gamepad_states.forEach(function (state, i) {
if (state.any_button && pico8_gamepads_mapping[i] == null) {
var first_free_player = p8_first_player_without_gamepad(max_players);
p8_assign_gamepad_to_player(i, first_free_player);
}
});
// Update pico8_buttons array.
gamepad_states.forEach(function (gamepad_state, i) {
if (pico8_gamepads_mapping[i] != null) {
pico8_buttons[pico8_gamepads_mapping[i]] = gamepad_state.button_state;
}
});
// Update menu button.
// Pico 8 only recognises the menu button on the first player, so we
// press it when any gamepad has pressed a button mapped to menu.
if (gamepad_states.some(function (state) { return state.menu_button; })) {
pico8_buttons[0] |= 0x40;
}
requestAnimationFrame(p8_update_gamepads);
}
requestAnimationFrame(p8_update_gamepads);
// End of gamepad code
// key blocker. prevent browser operations while playing cart so that PICO-8 can use those keys e.g. cursors to scroll, ctrl-r to reload
document.addEventListener('keydown',
function (event) {
event = event || window.event;
if (!p8_is_running) return;
if (pico8_state.has_focus == 1)
if ([32, 37, 38, 39, 40, 77, 82, 80, 9].indexOf(event.keyCode) > -1) // block only cursors, M R P, tab
if (event.preventDefault) event.preventDefault();
},{passive: false});
// when using codo_textarea to determine focus, need to explicitly hand focus back when clicking a p8_menu_button
function p8_give_focus()
{
el = (typeof codo_textarea === 'undefined') ? document.getElementById("codo_textarea") : codo_textarea;
if (el)
{
el.focus();
el.select();
}
}
function p8_request_fullscreen() {
var is_fullscreen=(document.fullscreenElement || document.mozFullScreenElement || document.webkitIsFullScreen || document.msFullscreenElement);
if (is_fullscreen)
{
if (document.exitFullscreen) {
document.exitFullscreen();
} else if (document.webkitExitFullscreen) {
document.webkitExitFullscreen();
} else if (document.mozCancelFullScreen) {
document.mozCancelFullScreen();
} else if (document.msExitFullscreen) {
document.msExitFullscreen();
}
return;
}
var el = document.getElementById("p8_playarea");
if ( el.requestFullscreen ) {
el.requestFullscreen();
} else if ( el.mozRequestFullScreen ) {
el.mozRequestFullScreen();
} else if ( el.webkitRequestFullScreen ) {
el.webkitRequestFullScreen( Element.ALLOW_KEYBOARD_INPUT );
}
}
</script>
<STYLE TYPE="text/css">
.p8_menu_button{
opacity:0.3;
padding:4px;
display:table;
width:24px;
height:24px;
float:right;
}
@media screen and (min-width:512px) {
.p8_menu_button{
width:24px; margin-left:12px; margin-bottom:8px;
}
}
.p8_menu_button:hover{
opacity:1.0;
cursor:pointer;
}
canvas{
image-rendering: optimizeSpeed;
image-rendering: -moz-crisp-edges;
image-rendering: -webkit-optimize-contrast;
image-rendering: optimize-contrast;
image-rendering: pixelated;
-ms-interpolation-mode: nearest-neighbor;
border: 0px;
cursor: none;
}
.p8_start_button{
cursor:pointer;
-repeat: center;
-webkit-background-size:cover; -moz-background-size:cover; -o-background-size:cover; background-size:cover;
}
.button_gfx{
stroke-width:2;
stroke: #ffffff;