littlejsengine 1.18.29 → 1.20.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/FAQ.md +841 -815
- package/README.md +211 -199
- package/dist/littlejs.d.ts +4383 -812
- package/dist/littlejs.esm.js +31848 -16680
- package/dist/littlejs.esm.min.js +14 -1
- package/dist/littlejs.js +31230 -16166
- package/dist/littlejs.min.js +13 -1
- package/dist/littlejs.release.js +27419 -15457
- package/package.json +10 -5
- package/plugins/audioEffects.js +375 -0
- package/plugins/box2d.js +2516 -2073
- package/plugins/drawUtilities.js +206 -31
- package/plugins/gltf.js +740 -0
- package/plugins/jsconfig.json +19 -0
- package/plugins/lightSystem.js +719 -330
- package/plugins/math3d.js +911 -0
- package/plugins/medalSystem.js +118 -72
- package/plugins/newgrounds.js +400 -199
- package/plugins/pathFinder.js +208 -175
- package/plugins/pluginExport.js +196 -115
- package/plugins/postProcess.js +284 -166
- package/plugins/render3d.js +4072 -0
- package/plugins/render3dDebug.js +109 -0
- package/plugins/render3dExtras.js +1754 -0
- package/plugins/render3dVoxels.js +502 -0
- package/plugins/textureSheet.js +86 -48
- package/plugins/threejs.js +183 -158
- package/plugins/tweakables.js +411 -0
- package/plugins/tweenSystem.js +646 -526
- package/plugins/uiSystem.js +2024 -1687
- package/plugins/zzfxm.js +8 -15
- package/src/engine.js +345 -194
- package/src/engineAudio.js +1022 -777
- package/src/engineBuild.mjs +289 -253
- package/src/engineDebug.js +912 -735
- package/src/engineDraw.js +1774 -1510
- package/src/engineEditor.js +2431 -0
- package/src/engineExport.js +419 -396
- package/src/engineInput.js +1462 -1293
- package/src/engineLogo.js +4 -3
- package/src/engineMath.js +1274 -1183
- package/src/engineObject.js +674 -551
- package/src/engineParticles.js +583 -532
- package/src/engineRelease.js +67 -45
- package/src/engineSettings.js +808 -726
- package/src/engineTileLayer.js +995 -741
- package/src/engineUtilities.js +32 -13
- package/src/engineWebGL.js +1162 -941
- package/src/jsconfig.json +17 -13
- package/plugins/box2d.wasm.js +0 -630
- package/plugins/box2d.wasm.wasm +0 -0
package/src/engineDraw.js
CHANGED
|
@@ -1,1511 +1,1775 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* LittleJS Drawing System
|
|
3
|
-
* - Hybrid rendering with both Canvas2D and WebGL support
|
|
4
|
-
* - Optimized tile sheet sprite rendering using WebGL batching
|
|
5
|
-
* - Primitive drawing for polygons, ellipses, and lines
|
|
6
|
-
* - Tile-based rendering with TileInfo and TextureInfo classes
|
|
7
|
-
* - Text rendering with custom fonts and ImageFont support
|
|
8
|
-
* - Color and additive color blending for effects
|
|
9
|
-
* - Rotation, mirroring, and scaling transformations
|
|
10
|
-
* - Camera system with position, scale, and rotation
|
|
11
|
-
* - Multiple canvas support (main, WebGL, work canvases)
|
|
12
|
-
* - Gradient fills and outlined shapes
|
|
13
|
-
* - Image manipulation and color tinting
|
|
14
|
-
*
|
|
15
|
-
* Rendering Architecture:
|
|
16
|
-
* - glCanvas: WebGL canvas for accelerated sprite batch rendering
|
|
17
|
-
* - mainCanvas: Canvas2D overlay for text, UI, and custom drawing
|
|
18
|
-
* - All draw functions default to WebGL when enabled, can force Canvas2D with useWebGL parameter
|
|
19
|
-
*
|
|
20
|
-
* @namespace Draw
|
|
21
|
-
*/
|
|
22
|
-
|
|
23
|
-
'use strict';
|
|
24
|
-
|
|
25
|
-
/** The primary 2D canvas visible to the user
|
|
26
|
-
* @type {HTMLCanvasElement}
|
|
27
|
-
* @memberof Draw */
|
|
28
|
-
let mainCanvas;
|
|
29
|
-
|
|
30
|
-
/** 2d context for mainCanvas
|
|
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
|
-
* @type {
|
|
73
|
-
* @memberof Draw */
|
|
74
|
-
let
|
|
75
|
-
|
|
76
|
-
/**
|
|
77
|
-
* @type {
|
|
78
|
-
* @memberof Draw */
|
|
79
|
-
let
|
|
80
|
-
|
|
81
|
-
/** Keeps track of how many
|
|
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
|
-
const
|
|
136
|
-
|
|
137
|
-
if (
|
|
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
|
-
* @param {
|
|
462
|
-
* @param {
|
|
463
|
-
* @param {
|
|
464
|
-
* @param {Color} [
|
|
465
|
-
* @param {
|
|
466
|
-
* @param {boolean} [
|
|
467
|
-
* @param {
|
|
468
|
-
* @
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
ASSERT(
|
|
476
|
-
ASSERT(
|
|
477
|
-
ASSERT(
|
|
478
|
-
ASSERT(
|
|
479
|
-
ASSERT(!(
|
|
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
|
-
* @param {
|
|
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
|
-
ASSERT(
|
|
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
|
-
* @param {
|
|
846
|
-
* @param {
|
|
847
|
-
* @
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
ASSERT(
|
|
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
|
-
* @param {
|
|
904
|
-
* @param {
|
|
905
|
-
* @param {
|
|
906
|
-
* @param {
|
|
907
|
-
* @
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
//
|
|
919
|
-
|
|
920
|
-
size
|
|
921
|
-
lineWidth
|
|
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
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
{
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
* @param {
|
|
1072
|
-
* @
|
|
1073
|
-
* @
|
|
1074
|
-
|
|
1075
|
-
{
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
/**
|
|
1091
|
-
*
|
|
1092
|
-
*
|
|
1093
|
-
*
|
|
1094
|
-
* @
|
|
1095
|
-
* @
|
|
1096
|
-
|
|
1097
|
-
{
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
}
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
|
|
1179
|
-
ASSERT(
|
|
1180
|
-
ASSERT(
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
|
|
1200
|
-
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
|
|
1206
|
-
|
|
1207
|
-
|
|
1208
|
-
|
|
1209
|
-
|
|
1210
|
-
|
|
1211
|
-
|
|
1212
|
-
{
|
|
1213
|
-
|
|
1214
|
-
|
|
1215
|
-
|
|
1216
|
-
|
|
1217
|
-
|
|
1218
|
-
|
|
1219
|
-
|
|
1220
|
-
|
|
1221
|
-
|
|
1222
|
-
|
|
1223
|
-
|
|
1224
|
-
|
|
1225
|
-
|
|
1226
|
-
|
|
1227
|
-
|
|
1228
|
-
{
|
|
1229
|
-
|
|
1230
|
-
|
|
1231
|
-
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
|
|
1235
|
-
|
|
1236
|
-
|
|
1237
|
-
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
}
|
|
1242
|
-
|
|
1243
|
-
|
|
1244
|
-
|
|
1245
|
-
|
|
1246
|
-
|
|
1247
|
-
|
|
1248
|
-
|
|
1249
|
-
|
|
1250
|
-
|
|
1251
|
-
|
|
1252
|
-
|
|
1253
|
-
|
|
1254
|
-
|
|
1255
|
-
|
|
1256
|
-
|
|
1257
|
-
|
|
1258
|
-
|
|
1259
|
-
|
|
1260
|
-
|
|
1261
|
-
|
|
1262
|
-
|
|
1263
|
-
|
|
1264
|
-
|
|
1265
|
-
|
|
1266
|
-
|
|
1267
|
-
|
|
1268
|
-
|
|
1269
|
-
|
|
1270
|
-
|
|
1271
|
-
|
|
1272
|
-
|
|
1273
|
-
|
|
1274
|
-
|
|
1275
|
-
|
|
1276
|
-
|
|
1277
|
-
|
|
1278
|
-
|
|
1279
|
-
|
|
1280
|
-
|
|
1281
|
-
|
|
1282
|
-
|
|
1283
|
-
|
|
1284
|
-
|
|
1285
|
-
|
|
1286
|
-
|
|
1287
|
-
|
|
1288
|
-
|
|
1289
|
-
|
|
1290
|
-
|
|
1291
|
-
|
|
1292
|
-
|
|
1293
|
-
|
|
1294
|
-
|
|
1295
|
-
|
|
1296
|
-
|
|
1297
|
-
|
|
1298
|
-
|
|
1299
|
-
|
|
1300
|
-
|
|
1301
|
-
|
|
1302
|
-
|
|
1303
|
-
|
|
1304
|
-
|
|
1305
|
-
|
|
1306
|
-
|
|
1307
|
-
|
|
1308
|
-
|
|
1309
|
-
|
|
1310
|
-
|
|
1311
|
-
|
|
1312
|
-
|
|
1313
|
-
|
|
1314
|
-
|
|
1315
|
-
|
|
1316
|
-
|
|
1317
|
-
|
|
1318
|
-
|
|
1319
|
-
|
|
1320
|
-
const
|
|
1321
|
-
|
|
1322
|
-
|
|
1323
|
-
|
|
1324
|
-
|
|
1325
|
-
|
|
1326
|
-
|
|
1327
|
-
|
|
1328
|
-
|
|
1329
|
-
|
|
1330
|
-
|
|
1331
|
-
|
|
1332
|
-
|
|
1333
|
-
|
|
1334
|
-
|
|
1335
|
-
|
|
1336
|
-
|
|
1337
|
-
|
|
1338
|
-
|
|
1339
|
-
|
|
1340
|
-
|
|
1341
|
-
|
|
1342
|
-
|
|
1343
|
-
|
|
1344
|
-
|
|
1345
|
-
|
|
1346
|
-
|
|
1347
|
-
|
|
1348
|
-
|
|
1349
|
-
|
|
1350
|
-
|
|
1351
|
-
|
|
1352
|
-
|
|
1353
|
-
|
|
1354
|
-
|
|
1355
|
-
* @
|
|
1356
|
-
|
|
1357
|
-
|
|
1358
|
-
|
|
1359
|
-
|
|
1360
|
-
|
|
1361
|
-
|
|
1362
|
-
|
|
1363
|
-
|
|
1364
|
-
|
|
1365
|
-
|
|
1366
|
-
}
|
|
1367
|
-
|
|
1368
|
-
|
|
1369
|
-
* @
|
|
1370
|
-
* @memberof Draw */
|
|
1371
|
-
function
|
|
1372
|
-
{
|
|
1373
|
-
|
|
1374
|
-
|
|
1375
|
-
|
|
1376
|
-
|
|
1377
|
-
|
|
1378
|
-
|
|
1379
|
-
|
|
1380
|
-
|
|
1381
|
-
|
|
1382
|
-
|
|
1383
|
-
|
|
1384
|
-
|
|
1385
|
-
|
|
1386
|
-
|
|
1387
|
-
|
|
1388
|
-
|
|
1389
|
-
|
|
1390
|
-
*
|
|
1391
|
-
|
|
1392
|
-
|
|
1393
|
-
|
|
1394
|
-
|
|
1395
|
-
*
|
|
1396
|
-
*
|
|
1397
|
-
|
|
1398
|
-
|
|
1399
|
-
|
|
1400
|
-
|
|
1401
|
-
|
|
1402
|
-
|
|
1403
|
-
|
|
1404
|
-
|
|
1405
|
-
|
|
1406
|
-
|
|
1407
|
-
|
|
1408
|
-
|
|
1409
|
-
|
|
1410
|
-
|
|
1411
|
-
|
|
1412
|
-
|
|
1413
|
-
|
|
1414
|
-
|
|
1415
|
-
|
|
1416
|
-
|
|
1417
|
-
|
|
1418
|
-
|
|
1419
|
-
|
|
1420
|
-
|
|
1421
|
-
|
|
1422
|
-
|
|
1423
|
-
|
|
1424
|
-
|
|
1425
|
-
|
|
1426
|
-
|
|
1427
|
-
|
|
1428
|
-
|
|
1429
|
-
|
|
1430
|
-
|
|
1431
|
-
|
|
1432
|
-
|
|
1433
|
-
|
|
1434
|
-
|
|
1435
|
-
|
|
1436
|
-
|
|
1437
|
-
|
|
1438
|
-
|
|
1439
|
-
|
|
1440
|
-
|
|
1441
|
-
|
|
1442
|
-
|
|
1443
|
-
|
|
1444
|
-
|
|
1445
|
-
|
|
1446
|
-
|
|
1447
|
-
|
|
1448
|
-
|
|
1449
|
-
|
|
1450
|
-
|
|
1451
|
-
|
|
1452
|
-
|
|
1453
|
-
|
|
1454
|
-
|
|
1455
|
-
|
|
1456
|
-
|
|
1457
|
-
|
|
1458
|
-
|
|
1459
|
-
|
|
1460
|
-
|
|
1461
|
-
|
|
1462
|
-
|
|
1463
|
-
|
|
1464
|
-
|
|
1465
|
-
|
|
1466
|
-
|
|
1467
|
-
|
|
1468
|
-
|
|
1469
|
-
|
|
1470
|
-
|
|
1471
|
-
|
|
1472
|
-
|
|
1473
|
-
|
|
1474
|
-
|
|
1475
|
-
|
|
1476
|
-
|
|
1477
|
-
|
|
1478
|
-
|
|
1479
|
-
|
|
1480
|
-
|
|
1481
|
-
|
|
1482
|
-
|
|
1483
|
-
|
|
1484
|
-
|
|
1485
|
-
|
|
1486
|
-
|
|
1487
|
-
|
|
1488
|
-
|
|
1489
|
-
|
|
1490
|
-
|
|
1491
|
-
|
|
1492
|
-
|
|
1493
|
-
|
|
1494
|
-
|
|
1495
|
-
|
|
1496
|
-
|
|
1497
|
-
|
|
1498
|
-
|
|
1499
|
-
|
|
1500
|
-
|
|
1501
|
-
|
|
1502
|
-
|
|
1503
|
-
|
|
1504
|
-
|
|
1505
|
-
|
|
1506
|
-
|
|
1507
|
-
|
|
1508
|
-
|
|
1509
|
-
|
|
1510
|
-
|
|
1
|
+
/**
|
|
2
|
+
* LittleJS Drawing System
|
|
3
|
+
* - Hybrid rendering with both Canvas2D and WebGL support
|
|
4
|
+
* - Optimized tile sheet sprite rendering using WebGL batching
|
|
5
|
+
* - Primitive drawing for polygons, ellipses, and lines
|
|
6
|
+
* - Tile-based rendering with TileInfo and TextureInfo classes
|
|
7
|
+
* - Text rendering with custom fonts and ImageFont support
|
|
8
|
+
* - Color and additive color blending for effects
|
|
9
|
+
* - Rotation, mirroring, and scaling transformations
|
|
10
|
+
* - Camera system with position, scale, and rotation
|
|
11
|
+
* - Multiple canvas support (main, WebGL, work canvases)
|
|
12
|
+
* - Gradient fills and outlined shapes
|
|
13
|
+
* - Image manipulation and color tinting
|
|
14
|
+
*
|
|
15
|
+
* Rendering Architecture:
|
|
16
|
+
* - glCanvas: WebGL canvas for accelerated sprite batch rendering
|
|
17
|
+
* - mainCanvas: Canvas2D overlay for text, UI, and custom drawing
|
|
18
|
+
* - All draw functions default to WebGL when enabled, can force Canvas2D with useWebGL parameter
|
|
19
|
+
*
|
|
20
|
+
* @namespace Draw
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
'use strict';
|
|
24
|
+
|
|
25
|
+
/** The primary 2D canvas visible to the user
|
|
26
|
+
* @type {HTMLCanvasElement}
|
|
27
|
+
* @memberof Draw */
|
|
28
|
+
let mainCanvas;
|
|
29
|
+
|
|
30
|
+
/** 2d context for mainCanvas
|
|
31
|
+
* - Scaled by canvasPixelRatio, so drawing to it is in css pixels
|
|
32
|
+
* - getImageData and putImageData ignore that scale and work in backing store
|
|
33
|
+
* pixels, so use workReadCanvas to read pixels back instead of this
|
|
34
|
+
* @type {CanvasRenderingContext2D}
|
|
35
|
+
* @memberof Draw */
|
|
36
|
+
let mainContext;
|
|
37
|
+
|
|
38
|
+
/** The default 2d context to use for drawing, usually mainContext
|
|
39
|
+
* @type {CanvasRenderingContext2D|OffscreenCanvasRenderingContext2D}
|
|
40
|
+
* @memberof Draw */
|
|
41
|
+
let drawContext;
|
|
42
|
+
|
|
43
|
+
/** Offscreen canvas that can be used for image processing
|
|
44
|
+
* @type {OffscreenCanvas}
|
|
45
|
+
* @memberof Draw */
|
|
46
|
+
let workCanvas;
|
|
47
|
+
|
|
48
|
+
/** Offscreen canvas that can be used for image processing
|
|
49
|
+
* @type {OffscreenCanvasRenderingContext2D}
|
|
50
|
+
* @memberof Draw */
|
|
51
|
+
let workContext;
|
|
52
|
+
|
|
53
|
+
/** Offscreen canvas with willReadFrequently that can be used for image processing
|
|
54
|
+
* @type {OffscreenCanvas}
|
|
55
|
+
* @memberof Draw */
|
|
56
|
+
let workReadCanvas;
|
|
57
|
+
|
|
58
|
+
/** Offscreen canvas with willReadFrequently that can be used for image processing
|
|
59
|
+
* @type {OffscreenCanvasRenderingContext2D}
|
|
60
|
+
* @memberof Draw */
|
|
61
|
+
let workReadContext;
|
|
62
|
+
|
|
63
|
+
/** Extra canvas to composite behind the engine canvases when combining canvases
|
|
64
|
+
* Set by plugins that render to their own canvas below the LittleJS canvases
|
|
65
|
+
* @type {HTMLCanvasElement}
|
|
66
|
+
* @memberof Draw */
|
|
67
|
+
let backgroundCanvas;
|
|
68
|
+
|
|
69
|
+
/** The size of the main canvas (and other secondary canvases) in css pixels
|
|
70
|
+
* - This is the screen space coordinate system, matching mousePos
|
|
71
|
+
* - With canvasPixelRatio set the backing store is larger than this
|
|
72
|
+
* @type {Vector2}
|
|
73
|
+
* @memberof Draw */
|
|
74
|
+
let mainCanvasSize = vec2();
|
|
75
|
+
|
|
76
|
+
/** Array containing texture info for batch rendering system
|
|
77
|
+
* @type {Array<TextureInfo>}
|
|
78
|
+
* @memberof Draw */
|
|
79
|
+
let textureInfos = [];
|
|
80
|
+
|
|
81
|
+
/** Keeps track of how many draw calls there were each frame for debugging
|
|
82
|
+
* @type {number}
|
|
83
|
+
* @memberof Draw */
|
|
84
|
+
let drawCount = 0;
|
|
85
|
+
|
|
86
|
+
/** Keeps track of how many primitives were drawn each frame for debugging
|
|
87
|
+
* A single draw call can render many primitives (e.g. a WebGL sprite batch).
|
|
88
|
+
* @type {number}
|
|
89
|
+
* @memberof Draw */
|
|
90
|
+
let primitiveCount = 0;
|
|
91
|
+
|
|
92
|
+
// internal predicates for tint short-circuiting in canvas2D draw paths
|
|
93
|
+
// isWhite ignores alpha because alpha is applied via globalAlpha, not multiply
|
|
94
|
+
// isBlack includes alpha so additive colors that only contribute alpha are not skipped
|
|
95
|
+
/** @ignore
|
|
96
|
+
* @param {Color} c */
|
|
97
|
+
function isWhite(c) { return c.r >= 1 && c.g >= 1 && c.b >= 1; }
|
|
98
|
+
/** @ignore
|
|
99
|
+
* @param {Color} c */
|
|
100
|
+
function isBlack(c) { return c.r <= 0 && c.g <= 0 && c.b <= 0 && c.a <= 0; }
|
|
101
|
+
|
|
102
|
+
///////////////////////////////////////////////////////////////////////////////
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Create a tile info object using a grid based system
|
|
106
|
+
* - This can take vecs or floats for easier use and conversion
|
|
107
|
+
* - If an index is passed in, the tile size and index will determine the position
|
|
108
|
+
* @param {Vector2|number} [index=0] - Index of the tile in 1d or 2d form
|
|
109
|
+
* @param {Vector2|number} [size] - Size of tile in pixels
|
|
110
|
+
* @param {TextureInfo|number} [texture] - Texture index or info to use
|
|
111
|
+
* @param {number} [padding] - How many pixels padding around tiles
|
|
112
|
+
* @param {number} [bleed] - How many pixels smaller to draw tiles
|
|
113
|
+
* @return {TileInfo}
|
|
114
|
+
* @example
|
|
115
|
+
* tile(2) // a tile at index 2 using the default tile size of 16
|
|
116
|
+
* tile(5, 8) // a tile at index 5 using a tile size of 8
|
|
117
|
+
* tile(1, 16, 3) // a tile at index 1 of size 16 on texture 3
|
|
118
|
+
* tile(vec2(4,8), vec2(30,10)) // a tile at index (4,8) with a size of (30,10)
|
|
119
|
+
* @memberof Draw */
|
|
120
|
+
function tile(index=0, size=tileDefaultSize, texture=0, padding=tileDefaultPadding, bleed=tileDefaultBleed)
|
|
121
|
+
{
|
|
122
|
+
ASSERT(isVector2(index) || typeof index === 'number', 'index must be a vec2 or number');
|
|
123
|
+
ASSERT(isVector2(size) || typeof size === 'number', 'size must be a vec2 or number');
|
|
124
|
+
ASSERT(isNumber(texture) || texture instanceof TextureInfo, 'texture must be a number or TextureInfo');
|
|
125
|
+
ASSERT(isNumber(padding), 'padding must be a number');
|
|
126
|
+
|
|
127
|
+
if (typeof size === 'number')
|
|
128
|
+
{
|
|
129
|
+
// if size is a number, make it a vector
|
|
130
|
+
ASSERT(size > 0);
|
|
131
|
+
size = new Vector2(size, size);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// create tile info object
|
|
135
|
+
const textureInfo = typeof texture === 'number' ?
|
|
136
|
+
textureInfos[texture] : texture;
|
|
137
|
+
if (headlessMode && !textureInfo?.size.x)
|
|
138
|
+
return new TileInfo(new Vector2, size.copy(), textureInfo, padding, bleed); // no image loaded, no place in it
|
|
139
|
+
ASSERT(textureInfo instanceof TextureInfo, 'tile texture is not loaded');
|
|
140
|
+
ASSERT(textureInfo.size.x > 0, 'tile texture is not loaded');
|
|
141
|
+
|
|
142
|
+
// get the position of the tile
|
|
143
|
+
const sizePaddedX = size.x + padding*2;
|
|
144
|
+
const sizePaddedY = size.y + padding*2;
|
|
145
|
+
let x, y;
|
|
146
|
+
if (typeof index === 'number')
|
|
147
|
+
{
|
|
148
|
+
const cols = textureInfo.size.x / sizePaddedX |0;
|
|
149
|
+
x = index % cols;
|
|
150
|
+
y = index / cols |0;
|
|
151
|
+
}
|
|
152
|
+
else
|
|
153
|
+
{
|
|
154
|
+
x = index.x;
|
|
155
|
+
y = index.y;
|
|
156
|
+
}
|
|
157
|
+
const pos = new Vector2(x*sizePaddedX + padding, y*sizePaddedY + padding);
|
|
158
|
+
return new TileInfo(pos, size, textureInfo, padding, bleed);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* Tile Info - Stores info about how to draw a tile
|
|
163
|
+
* @memberof Draw
|
|
164
|
+
*/
|
|
165
|
+
class TileInfo
|
|
166
|
+
{
|
|
167
|
+
/** Create a tile info object
|
|
168
|
+
* @param {Vector2} [pos=vec2()] - Top left corner of tile in pixels
|
|
169
|
+
* @param {Vector2} [size] - Size of tile in pixels
|
|
170
|
+
* @param {TextureInfo} [textureInfo] - Texture info to use
|
|
171
|
+
* @param {number} [padding] - How many pixels padding around all sides of each tile (increases grid size, does not affect tile size)
|
|
172
|
+
* @param {number} [bleed] - How many pixels smaller to shrink UVS of tiles (does not affect grid size, only UVs)
|
|
173
|
+
* @param {number} [columns] - How many frames per row for frame(), 0 to keep frames on a single row
|
|
174
|
+
*/
|
|
175
|
+
constructor(pos=vec2(), size=tileDefaultSize, textureInfo=textureInfos[0], padding=tileDefaultPadding, bleed=tileDefaultBleed, columns=0)
|
|
176
|
+
{
|
|
177
|
+
/** @property {Vector2} - Top left corner of tile in pixels */
|
|
178
|
+
this.pos = pos.copy();
|
|
179
|
+
/** @property {Vector2} - Size of tile in pixels */
|
|
180
|
+
this.size = size.copy();
|
|
181
|
+
/** @property {number} - How many pixels padding around tiles */
|
|
182
|
+
this.padding = padding;
|
|
183
|
+
/** @property {TextureInfo} - The texture info for this tile */
|
|
184
|
+
this.textureInfo = textureInfo;
|
|
185
|
+
/** @property {number} - Shrinks tile by this many pixels to prevent neighbors bleeding */
|
|
186
|
+
this.bleed = bleed;
|
|
187
|
+
/** @property {number} - How many frames per row for frame(), 0 to keep frames on a single row */
|
|
188
|
+
this.columns = columns;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
/** Returns a copy of this tile offset by a vector
|
|
192
|
+
* @param {Vector2} offset - Offset to apply in pixels
|
|
193
|
+
* @return {TileInfo}
|
|
194
|
+
*/
|
|
195
|
+
offset(offset)
|
|
196
|
+
{ return new TileInfo(this.pos.add(offset), this.size, this.textureInfo, this.padding, this.bleed, this.columns); }
|
|
197
|
+
|
|
198
|
+
/** Returns a copy of this tile offset by a number of animation frames
|
|
199
|
+
* Frames wrap down to the next row if columns is set
|
|
200
|
+
* @param {number} frame - Offset to apply in animation frames
|
|
201
|
+
* @return {TileInfo}
|
|
202
|
+
*/
|
|
203
|
+
frame(frame)
|
|
204
|
+
{
|
|
205
|
+
ASSERT(typeof frame === 'number');
|
|
206
|
+
const w = this.size.x + this.padding*2;
|
|
207
|
+
const h = this.size.y + this.padding*2;
|
|
208
|
+
const x = (this.columns ? frame % this.columns : frame) * w;
|
|
209
|
+
const y = (this.columns ? frame / this.columns | 0 : 0) * h;
|
|
210
|
+
ASSERT(!this.textureInfo || this.pos.x + x + this.size.x <= this.textureInfo.size.x, 'frame extends beyond texture width!');
|
|
211
|
+
ASSERT(!this.textureInfo || this.pos.y + y + this.size.y <= this.textureInfo.size.y, 'frame extends beyond texture height!');
|
|
212
|
+
return this.offset(new Vector2(x, y));
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
/** Set how many frames per row this tile uses, so frame() can wrap
|
|
216
|
+
* @param {number} [columns] - Frames per row, 0 to keep frames on a single row
|
|
217
|
+
* @return {TileInfo}
|
|
218
|
+
*/
|
|
219
|
+
setColumns(columns=0)
|
|
220
|
+
{
|
|
221
|
+
ASSERT(isNumber(columns) && columns >= 0, 'columns must be a number >= 0');
|
|
222
|
+
this.columns = columns;
|
|
223
|
+
return this;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
/**
|
|
227
|
+
* Returns a tile info for an index using this tile's size, texture and padding as reference
|
|
228
|
+
* - the index counts from the texture's origin like tile(), for a sprite packed in a texture sheet use frame()
|
|
229
|
+
* @param {Vector2|number} [index=0]
|
|
230
|
+
* @return {TileInfo}
|
|
231
|
+
*/
|
|
232
|
+
index(index)
|
|
233
|
+
{ return tile(index, this.size, this.textureInfo, this.padding, this.bleed).setColumns(this.columns); }
|
|
234
|
+
|
|
235
|
+
/**
|
|
236
|
+
* Set this tile to use a full image in a texture info
|
|
237
|
+
* @param {TextureInfo} [textureInfo]
|
|
238
|
+
* @return {TileInfo}
|
|
239
|
+
*/
|
|
240
|
+
setFullImage(textureInfo=this.textureInfo)
|
|
241
|
+
{
|
|
242
|
+
this.textureInfo = textureInfo;
|
|
243
|
+
this.pos = new Vector2;
|
|
244
|
+
this.size = textureInfo.size.copy();
|
|
245
|
+
this.bleed = this.padding = this.columns = 0;
|
|
246
|
+
return this;
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
/**
|
|
251
|
+
* Texture Info - Stores info about each texture
|
|
252
|
+
* @memberof Draw
|
|
253
|
+
*/
|
|
254
|
+
class TextureInfo
|
|
255
|
+
{
|
|
256
|
+
/**
|
|
257
|
+
* Create a TextureInfo, called automatically by the engine
|
|
258
|
+
* @param {HTMLImageElement|HTMLCanvasElement|OffscreenCanvas|ImageBitmap} image
|
|
259
|
+
* @param {boolean} [useWebGL] - Should use WebGL if available?
|
|
260
|
+
* @param {boolean|Array<number>} [wrap] - Should the texture wrap (REPEAT) or clamp (CLAMP_TO_EDGE)? Or the WebGL
|
|
261
|
+
* modes across and down, like [gl.CLAMP_TO_EDGE, gl.MIRRORED_REPEAT], as a glTF sampler gives them
|
|
262
|
+
*/
|
|
263
|
+
constructor(image, useWebGL=true, wrap=false)
|
|
264
|
+
{
|
|
265
|
+
/** @property {HTMLImageElement|HTMLCanvasElement|OffscreenCanvas|ImageBitmap} - image source */
|
|
266
|
+
this.image = image;
|
|
267
|
+
/** @property {Vector2} - size of the image */
|
|
268
|
+
this.size = image ? vec2(image.width, image.height) : vec2();
|
|
269
|
+
/** @property {Vector2} - inverse of the size, cached for rendering */
|
|
270
|
+
this.sizeInverse = image ? vec2(1/image.width, 1/image.height) : vec2();
|
|
271
|
+
/** @property {WebGLTexture|undefined} - WebGL texture
|
|
272
|
+
* @type {WebGLTexture|undefined} */
|
|
273
|
+
this.glTexture = undefined;
|
|
274
|
+
/** @property {boolean|Array<number>} - true for REPEAT wrap mode, false for CLAMP_TO_EDGE, or the WebGL modes
|
|
275
|
+
* across and down
|
|
276
|
+
* @type {boolean|Array<number>} */
|
|
277
|
+
this.wrap = wrap;
|
|
278
|
+
useWebGL && this.createWebGLTexture();
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
/** Creates the WebGL texture, updates if already created */
|
|
282
|
+
createWebGLTexture()
|
|
283
|
+
{
|
|
284
|
+
const image = this.image;
|
|
285
|
+
if (image?.width) // the image may have been resized since
|
|
286
|
+
{
|
|
287
|
+
this.size = vec2(image.width, image.height);
|
|
288
|
+
this.sizeInverse = vec2(1/image.width, 1/image.height);
|
|
289
|
+
}
|
|
290
|
+
glRegisterTextureInfo(this);
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
/** Destroys the WebGL texture */
|
|
294
|
+
destroyWebGLTexture() { glUnregisterTextureInfo(this); }
|
|
295
|
+
|
|
296
|
+
/** Check if the texture is webgl enabled
|
|
297
|
+
* @return {boolean} */
|
|
298
|
+
hasWebGL() { return !!this.glTexture; }
|
|
299
|
+
|
|
300
|
+
/** Set the wrap mode for this texture
|
|
301
|
+
* @param {boolean|Array<number>} [wrap] - true for REPEAT, false for CLAMP_TO_EDGE, or the WebGL modes across
|
|
302
|
+
* and down */
|
|
303
|
+
setWrap(wrap=true)
|
|
304
|
+
{
|
|
305
|
+
this.wrap = wrap;
|
|
306
|
+
glSetTextureWrap(this.glTexture, wrap);
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
///////////////////////////////////////////////////////////////////////////////
|
|
311
|
+
/**
|
|
312
|
+
* SpriteAnimation - Steps a tile through its frames over time: looping, once, or there and back
|
|
313
|
+
* - Driven by the engine time like a Timer, so it pauses with the game and needs no update call
|
|
314
|
+
* - Read tileInfo each frame for the frame to draw, from an object's update or before a drawTile
|
|
315
|
+
* - loop, play and pingPong each start over from the first frame; stop holds the current one
|
|
316
|
+
* - Frames follow each other along the row, as tileInfo.frame counts them
|
|
317
|
+
* @example
|
|
318
|
+
* const walk = new SpriteAnimation(tile(0, 16), 4, .1); // four frames, a tenth of a second each
|
|
319
|
+
* const attack = new SpriteAnimation(tile(4, 16), 3, .05).play(); // once, then holds the last frame
|
|
320
|
+
* // in update: this.tileInfo = (attack.isDone ? walk : attack).tileInfo;
|
|
321
|
+
* @memberof Draw
|
|
322
|
+
*/
|
|
323
|
+
class SpriteAnimation
|
|
324
|
+
{
|
|
325
|
+
/** Create an animation over a run of frames, looping from the start
|
|
326
|
+
* @param {TileInfo} tileInfo - The first frame
|
|
327
|
+
* @param {number} frameCount - How many frames, one or more
|
|
328
|
+
* @param {number} [frameTime] - Seconds each frame shows for */
|
|
329
|
+
constructor(tileInfo, frameCount, frameTime=.1)
|
|
330
|
+
{
|
|
331
|
+
ASSERT(tileInfo instanceof TileInfo, 'the first frame must be a TileInfo');
|
|
332
|
+
ASSERT(frameCount >= 1 && frameTime > 0, 'an animation needs at least one frame and a positive frame time');
|
|
333
|
+
/** @property {TileInfo} - The first frame, the others follow it along the row */
|
|
334
|
+
this.firstTile = tileInfo;
|
|
335
|
+
/** @property {number} - How many frames */
|
|
336
|
+
this.frameCount = frameCount;
|
|
337
|
+
/** @property {number} - Seconds each frame shows for */
|
|
338
|
+
this.frameTime = frameTime;
|
|
339
|
+
/** @property {number} - Rate multiplier, 2 plays twice as fast; set it before starting */
|
|
340
|
+
this.speed = 1;
|
|
341
|
+
/** @property {string} - How it runs: 'loop', 'once' or 'pingPong', set by loop, play and pingPong */
|
|
342
|
+
this.mode = 'loop';
|
|
343
|
+
/** @property {number} - Engine time it started at */
|
|
344
|
+
this.startTime = time;
|
|
345
|
+
/** @property {number|undefined} - The frame held by stop, undefined while running
|
|
346
|
+
* @type {number|undefined} */
|
|
347
|
+
this.heldFrame = undefined;
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
/** Start over from the first frame and repeat forever
|
|
351
|
+
* @return {SpriteAnimation} */
|
|
352
|
+
loop() { return this.restart('loop'); }
|
|
353
|
+
|
|
354
|
+
/** Start over from the first frame, run through once and hold the last frame, last to first at a negative speed
|
|
355
|
+
* @return {SpriteAnimation} */
|
|
356
|
+
play() { return this.restart('once'); }
|
|
357
|
+
|
|
358
|
+
/** Start over from the first frame and run there and back forever
|
|
359
|
+
* @return {SpriteAnimation} */
|
|
360
|
+
pingPong() { return this.restart('pingPong'); }
|
|
361
|
+
|
|
362
|
+
/** Hold the current frame
|
|
363
|
+
* @return {SpriteAnimation} */
|
|
364
|
+
stop() { this.heldFrame = this.frame; return this; }
|
|
365
|
+
|
|
366
|
+
/** Start over from the first frame in a mode
|
|
367
|
+
* @param {string} [mode] - 'loop', 'once' or 'pingPong', the current mode when left out
|
|
368
|
+
* @return {SpriteAnimation} */
|
|
369
|
+
restart(mode=this.mode)
|
|
370
|
+
{
|
|
371
|
+
this.mode = mode;
|
|
372
|
+
this.startTime = time;
|
|
373
|
+
this.heldFrame = undefined;
|
|
374
|
+
return this;
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
/** How many frames have gone by since the start, fractional
|
|
378
|
+
* @return {number} */
|
|
379
|
+
get elapsedFrames() { return (time - this.startTime) * this.speed / this.frameTime; }
|
|
380
|
+
|
|
381
|
+
/** The frame showing now, 0 to frameCount-1
|
|
382
|
+
* @return {number} */
|
|
383
|
+
get frame()
|
|
384
|
+
{
|
|
385
|
+
if (this.heldFrame !== undefined)
|
|
386
|
+
return this.heldFrame;
|
|
387
|
+
const n = this.frameCount, f = floor(this.elapsedFrames);
|
|
388
|
+
if (this.mode == 'once') // backward it counts down from the last frame, showing it from the start
|
|
389
|
+
return clamp(this.speed < 0 ? n - 1 + ceil(this.elapsedFrames) : f, 0, n - 1);
|
|
390
|
+
if (this.mode == 'loop')
|
|
391
|
+
return mod(f, n);
|
|
392
|
+
const period = max(2 * n - 2, 1), k = mod(f, period); // there and back, the ends once each
|
|
393
|
+
return k < n ? k : period - k;
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
/** The tile of the frame showing now
|
|
397
|
+
* @return {TileInfo} */
|
|
398
|
+
get tileInfo() { return this.firstTile.frame(this.frame); }
|
|
399
|
+
|
|
400
|
+
/** True once a play has shown its last frame for its time
|
|
401
|
+
* @return {boolean} */
|
|
402
|
+
get isDone() { return this.mode == 'once' && this.heldFrame === undefined && abs(this.elapsedFrames) >= this.frameCount; }
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
///////////////////////////////////////////////////////////////////////////////
|
|
406
|
+
/**
|
|
407
|
+
* Shader - A custom fragment shader for objects and draws, 2D or 3D
|
|
408
|
+
* - Write a mainImage function in the post processing style, the renderer wraps it with its own program
|
|
409
|
+
* - It gives the surface color, then the object's color and additive color apply in 2D, and the lighting,
|
|
410
|
+
* shadows and fog in 3D; set emissive to 1 on a 3D object for the snippet's color to be final
|
|
411
|
+
* - Set it as obj.shader, or use setShader for 2D draws and render3D.shader for 3D draws
|
|
412
|
+
* - Draws that share a Shader share a batch; with no Shader set nothing changes
|
|
413
|
+
* - In 2D it shades textured draws, untextured ones like drawRect draw as they are
|
|
414
|
+
* - A render target, like a tile layer drawn in WebGL, holds premultiplied color, and so does every image when
|
|
415
|
+
* tilesPixelated is false; there iChannel0 reads premultiplied texels and the snippet's color is taken as
|
|
416
|
+
* premultiplied too. premultipliedTexture is true there, so a snippet that changes the alpha, or makes a see
|
|
417
|
+
* through color of its own, scales the rgb with it: `if (premultipliedTexture) c.rgb *= k;`
|
|
418
|
+
* - Compiled once per renderer by the first draw that needs it; a bad snippet throws with the GLSL log in debug
|
|
419
|
+
* - Make each Shader once, at init, and share it; every one made lives for the session with its programs
|
|
420
|
+
* - Names in both renderers: iChannel0 the texture, iTime, iResolution, premultipliedTexture, and localUV, 0 to 1
|
|
421
|
+
* across the sprite or the mesh's own uv
|
|
422
|
+
* - Names in 3D only: worldPos, worldNormal, cameraPos, sunDirection, sunColor, ambientColor, ambientGroundColor,
|
|
423
|
+
* lightCount, lights[i], lightColors[i] and shadow()
|
|
424
|
+
* - In 3D the shadow map is drawn without the Shader, cut only by the texture's alpha, so a snippet that removes
|
|
425
|
+
* parts of a surface still shadows with the whole of it
|
|
426
|
+
* @example
|
|
427
|
+
* const fade = new Shader(`
|
|
428
|
+
* void mainImage(out vec4 c, vec2 uv)
|
|
429
|
+
* {
|
|
430
|
+
* c = texture(iChannel0, uv);
|
|
431
|
+
* float fade = .5 + .5*sin(iTime);
|
|
432
|
+
* c.a *= fade;
|
|
433
|
+
* if (premultipliedTexture) c.rgb *= fade; // a tile layer, or any image with tilesPixelated false
|
|
434
|
+
* }`);
|
|
435
|
+
* obj.shader = fade;
|
|
436
|
+
* @memberof Draw
|
|
437
|
+
*/
|
|
438
|
+
class Shader
|
|
439
|
+
{
|
|
440
|
+
/** Create a shader from a fragment snippet that defines void mainImage(out vec4 c, vec2 uv)
|
|
441
|
+
* @param {string} fragmentCode */
|
|
442
|
+
constructor(fragmentCode)
|
|
443
|
+
{
|
|
444
|
+
ASSERT(isStringLike(fragmentCode) && String(fragmentCode).includes('mainImage'), 'a Shader needs fragment code that defines mainImage');
|
|
445
|
+
/** @property {string} - The mainImage snippet */
|
|
446
|
+
this.fragmentCode = String(fragmentCode);
|
|
447
|
+
/** @property {WebGLProgram|undefined} - The 2D program, compiled by the first draw that needs it, read only
|
|
448
|
+
* @type {WebGLProgram|undefined} */
|
|
449
|
+
this.program = undefined;
|
|
450
|
+
/** @property {WebGLProgram|undefined} - The 3D program, compiled by the 3D plugin the same way, read only
|
|
451
|
+
* @type {WebGLProgram|undefined} */
|
|
452
|
+
this.program3D = undefined;
|
|
453
|
+
glShaderObjects.push(this); // a lost context drops the programs of every one
|
|
454
|
+
}
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
///////////////////////////////////////////////////////////////////////////////
|
|
458
|
+
// Drawing functions
|
|
459
|
+
|
|
460
|
+
/** Draw textured tile centered in world space
|
|
461
|
+
* @param {Vector2} pos - Center of the tile in world space
|
|
462
|
+
* @param {Vector2} [size=vec2(1)] - Size of the tile in world space
|
|
463
|
+
* @param {TileInfo} [tileInfo] - Tile info to use, untextured if undefined
|
|
464
|
+
* @param {Color} [color=WHITE] - Color to modulate with
|
|
465
|
+
* @param {number} [angle] - Angle to rotate by
|
|
466
|
+
* @param {boolean} [mirror] - Is image flipped along the Y axis?
|
|
467
|
+
* @param {Color} [additiveColor] - Additive color to be applied if any
|
|
468
|
+
* @param {boolean} [useWebGL=glEnable] - Use accelerated WebGL rendering?
|
|
469
|
+
* @param {boolean} [screenSpace=drawScreenSpace] - Are the pos and size are in screen space?
|
|
470
|
+
* @param {CanvasRenderingContext2D|OffscreenCanvasRenderingContext2D} [context] - Canvas 2D context to draw to
|
|
471
|
+
* @memberof Draw */
|
|
472
|
+
function drawTile(pos, size=vec2(1), tileInfo, color=WHITE,
|
|
473
|
+
angle=0, mirror, additiveColor, useWebGL=glEnable, screenSpace=drawScreenSpace, context)
|
|
474
|
+
{
|
|
475
|
+
ASSERT(isVector2(pos), 'pos must be a vec2');
|
|
476
|
+
ASSERT(isVector2(size), 'size must be a vec2');
|
|
477
|
+
ASSERT(isColor(color), 'color is invalid');
|
|
478
|
+
ASSERT(isNumber(angle), 'angle must be a number');
|
|
479
|
+
ASSERT(!additiveColor || isColor(additiveColor), 'additiveColor must be a color');
|
|
480
|
+
ASSERT(!context || !useWebGL, 'context only supported in canvas 2D mode');
|
|
481
|
+
|
|
482
|
+
if (headlessMode && !context) return; // headless has no canvas, only a context passed in is drawn to
|
|
483
|
+
|
|
484
|
+
const textureInfo = tileInfo?.textureInfo;
|
|
485
|
+
if (textureInfo && !(tileInfo.size.x && tileInfo.size.y))
|
|
486
|
+
return; // a tile with no area draws nothing, like a sprite still loading
|
|
487
|
+
const bleed = tileInfo?.bleed ?? 0;
|
|
488
|
+
if (useWebGL && glEnable)
|
|
489
|
+
{
|
|
490
|
+
ASSERT(!!glContext, 'WebGL is not enabled!');
|
|
491
|
+
if (screenSpace)
|
|
492
|
+
{
|
|
493
|
+
if (glSkipScreenSpace) return;
|
|
494
|
+
[pos, size, angle] = screenToWorldTransform(pos, size, angle);
|
|
495
|
+
}
|
|
496
|
+
if (textureInfo)
|
|
497
|
+
{
|
|
498
|
+
ASSERT(!!textureInfo.glTexture, 'texture has no WebGL texture, draw it with useWebGL false');
|
|
499
|
+
// calculate uvs and render
|
|
500
|
+
const sizeInverse = textureInfo.sizeInverse;
|
|
501
|
+
const x = tileInfo.pos.x * sizeInverse.x;
|
|
502
|
+
const y = tileInfo.pos.y * sizeInverse.y;
|
|
503
|
+
const w = tileInfo.size.x * sizeInverse.x;
|
|
504
|
+
const h = tileInfo.size.y * sizeInverse.y;
|
|
505
|
+
glSetTexture(textureInfo.glTexture);
|
|
506
|
+
if (bleed)
|
|
507
|
+
{
|
|
508
|
+
const bleedX = sizeInverse.x*bleed;
|
|
509
|
+
const bleedY = sizeInverse.y*bleed;
|
|
510
|
+
glDraw(pos.x, pos.y, mirror ? -size.x : size.x, size.y, angle,
|
|
511
|
+
x + bleedX, y + bleedY,
|
|
512
|
+
x - bleedX + w, y - bleedY + h,
|
|
513
|
+
color.rgbaInt(), additiveColor && additiveColor.rgbaInt());
|
|
514
|
+
}
|
|
515
|
+
else
|
|
516
|
+
{
|
|
517
|
+
glDraw(pos.x, pos.y, mirror ? -size.x : size.x, size.y, angle,
|
|
518
|
+
x, y, x + w, y + h,
|
|
519
|
+
color.rgbaInt(), additiveColor && additiveColor.rgbaInt());
|
|
520
|
+
}
|
|
521
|
+
}
|
|
522
|
+
else
|
|
523
|
+
{
|
|
524
|
+
// untextured: color plus additive in one color, as the Canvas2D path below does
|
|
525
|
+
const combined = additiveColor ? color.add(additiveColor) : color;
|
|
526
|
+
glDrawUntextured(pos.x, pos.y, size.x, size.y, angle, combined.rgbaInt());
|
|
527
|
+
}
|
|
528
|
+
}
|
|
529
|
+
else
|
|
530
|
+
{
|
|
531
|
+
// normal canvas 2D rendering method (slower)
|
|
532
|
+
++drawCount;
|
|
533
|
+
++primitiveCount;
|
|
534
|
+
drawCanvas2D(pos, size, angle, mirror, (context)=>
|
|
535
|
+
{
|
|
536
|
+
if (textureInfo)
|
|
537
|
+
{
|
|
538
|
+
// un-flip Y so the image renders right-side up under drawCanvas2D's Y flip
|
|
539
|
+
context.scale(1, -1);
|
|
540
|
+
// calculate uvs and render
|
|
541
|
+
const x = tileInfo.pos.x, y = tileInfo.pos.y;
|
|
542
|
+
const w = tileInfo.size.x, h = tileInfo.size.y;
|
|
543
|
+
drawImageColor(context, textureInfo.image, x, y, w, h, -.5, -.5, 1, 1, color, additiveColor, bleed);
|
|
544
|
+
}
|
|
545
|
+
else
|
|
546
|
+
{
|
|
547
|
+
// if no tile info, use untextured rect (Y-symmetric, no compensation needed)
|
|
548
|
+
const c = additiveColor ? color.add(additiveColor) : color;
|
|
549
|
+
context.fillStyle = c.toString();
|
|
550
|
+
context.fillRect(-.5, -.5, 1, 1);
|
|
551
|
+
}
|
|
552
|
+
}, screenSpace, context);
|
|
553
|
+
}
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
/** Draw colored rect centered on pos
|
|
557
|
+
* @param {Vector2} pos
|
|
558
|
+
* @param {Vector2} [size=vec2(1)]
|
|
559
|
+
* @param {Color} [color=WHITE]
|
|
560
|
+
* @param {number} [angle]
|
|
561
|
+
* @param {boolean} [useWebGL=glEnable]
|
|
562
|
+
* @param {boolean} [screenSpace=drawScreenSpace]
|
|
563
|
+
* @param {CanvasRenderingContext2D|OffscreenCanvasRenderingContext2D} [context]
|
|
564
|
+
* @memberof Draw */
|
|
565
|
+
function drawRect(pos, size, color, angle, useWebGL, screenSpace, context)
|
|
566
|
+
{
|
|
567
|
+
drawTile(pos, size, undefined, color, angle, false, undefined, useWebGL, screenSpace, context);
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
/** Draw a rect centered on pos with a gradient from top to bottom
|
|
571
|
+
* @param {Vector2} pos
|
|
572
|
+
* @param {Vector2} [size=vec2(1)]
|
|
573
|
+
* @param {Color} [colorTop=WHITE]
|
|
574
|
+
* @param {Color} [colorBottom=CLEAR_WHITE]
|
|
575
|
+
* @param {number} [angle]
|
|
576
|
+
* @param {boolean} [useWebGL=glEnable]
|
|
577
|
+
* @param {boolean} [screenSpace=drawScreenSpace]
|
|
578
|
+
* @param {CanvasRenderingContext2D|OffscreenCanvasRenderingContext2D} [context]
|
|
579
|
+
* @memberof Draw */
|
|
580
|
+
function drawRectGradient(pos, size, colorTop=WHITE, colorBottom=CLEAR_WHITE, angle=0, useWebGL=glEnable, screenSpace=drawScreenSpace, context)
|
|
581
|
+
{
|
|
582
|
+
ASSERT(isVector2(pos), 'pos must be a vec2');
|
|
583
|
+
ASSERT(isVector2(size), 'size must be a vec2');
|
|
584
|
+
ASSERT(isColor(colorTop) && isColor(colorBottom), 'color is invalid');
|
|
585
|
+
ASSERT(isNumber(angle), 'angle must be a number');
|
|
586
|
+
ASSERT(!context || !useWebGL, 'context only supported in canvas 2D mode');
|
|
587
|
+
|
|
588
|
+
if (headlessMode && !context) return; // headless has no canvas, only a context passed in is drawn to
|
|
589
|
+
|
|
590
|
+
if (useWebGL && glEnable)
|
|
591
|
+
{
|
|
592
|
+
ASSERT(!!glContext, 'WebGL is not enabled!');
|
|
593
|
+
if (screenSpace)
|
|
594
|
+
{
|
|
595
|
+
if (glSkipScreenSpace) return;
|
|
596
|
+
[pos, size, angle] = screenToWorldTransform(pos, size, angle);
|
|
597
|
+
}
|
|
598
|
+
// build 4 corner points for the rectangle
|
|
599
|
+
const points = [], colors = [];
|
|
600
|
+
const halfSizeX = size.x/2, halfSizeY = size.y/2;
|
|
601
|
+
const colorTopInt = colorTop.rgbaInt();
|
|
602
|
+
const colorBottomInt = colorBottom.rgbaInt();
|
|
603
|
+
const c = cos(-angle), s = sin(-angle);
|
|
604
|
+
for (let i=4; i--;)
|
|
605
|
+
{
|
|
606
|
+
const x = i & 1 ? halfSizeX : -halfSizeX;
|
|
607
|
+
const y = i & 2 ? halfSizeY : -halfSizeY;
|
|
608
|
+
const rx = x * c - y * s;
|
|
609
|
+
const ry = x * s + y * c;
|
|
610
|
+
const color = i & 2 ? colorTopInt : colorBottomInt;
|
|
611
|
+
points.push(vec2(pos.x + rx, pos.y + ry));
|
|
612
|
+
colors.push(color);
|
|
613
|
+
}
|
|
614
|
+
glDrawColoredPoints(points, colors);
|
|
615
|
+
}
|
|
616
|
+
else
|
|
617
|
+
{
|
|
618
|
+
// normal canvas 2D rendering method (slower)
|
|
619
|
+
++drawCount;
|
|
620
|
+
++primitiveCount;
|
|
621
|
+
drawCanvas2D(pos, size, angle, false, (context)=>
|
|
622
|
+
{
|
|
623
|
+
// gradient endpoints are flipped to match the Y flip inside drawCanvas2D
|
|
624
|
+
const gradient = context.createLinearGradient(0, .5, 0, -.5);
|
|
625
|
+
gradient.addColorStop(0, colorTop.toString());
|
|
626
|
+
gradient.addColorStop(1, colorBottom.toString());
|
|
627
|
+
context.fillStyle = gradient;
|
|
628
|
+
context.fillRect(-.5, -.5, 1, 1);
|
|
629
|
+
}, screenSpace, context);
|
|
630
|
+
}
|
|
631
|
+
}
|
|
632
|
+
|
|
633
|
+
/** Draw a texture tiled (wrapped) across a rectangle in world space.
|
|
634
|
+
* Useful for backgrounds, repeating patterns, and seamless fills.
|
|
635
|
+
* The whole texture is tiled — sub-region (TileInfo) wrapping is not supported.
|
|
636
|
+
* @param {Vector2} pos - Center of the rect in world space
|
|
637
|
+
* @param {Vector2} size - Size of the rect in world space
|
|
638
|
+
* @param {Vector2} wrapCount - How many times the texture repeats (x, y)
|
|
639
|
+
* @param {TextureInfo|number} [texture=0] - TextureInfo or texture index into textureInfos
|
|
640
|
+
* @param {Color} [color=WHITE] - Color to modulate with
|
|
641
|
+
* @param {number} [angle=0] - Angle to rotate by
|
|
642
|
+
* @param {Color} [additiveColor] - Additive color to be applied if any
|
|
643
|
+
* @param {boolean} [useWebGL=glEnable] - Use accelerated WebGL rendering?
|
|
644
|
+
* @param {boolean} [screenSpace=drawScreenSpace] - Are pos and size in screen space?
|
|
645
|
+
* @param {CanvasRenderingContext2D|OffscreenCanvasRenderingContext2D} [context] - Canvas 2D context to draw to
|
|
646
|
+
* @memberof Draw */
|
|
647
|
+
function drawTextureWrapped(pos, size, wrapCount, texture=0, color=WHITE,
|
|
648
|
+
angle=0, additiveColor, useWebGL=glEnable, screenSpace=drawScreenSpace, context)
|
|
649
|
+
{
|
|
650
|
+
ASSERT(isVector2(pos), 'pos must be a vec2');
|
|
651
|
+
ASSERT(isVector2(size), 'size must be a vec2');
|
|
652
|
+
ASSERT(isVector2(wrapCount), 'wrapCount must be a vec2');
|
|
653
|
+
ASSERT(isColor(color), 'color is invalid');
|
|
654
|
+
ASSERT(isNumber(angle), 'angle must be a number');
|
|
655
|
+
ASSERT(!additiveColor || isColor(additiveColor), 'additiveColor must be a color');
|
|
656
|
+
ASSERT(!context || !useWebGL, 'context only supported in canvas 2D mode');
|
|
657
|
+
ASSERT(!(texture instanceof TileInfo),
|
|
658
|
+
'pass a TextureInfo or texture index, not a TileInfo — use tileInfo.textureInfo');
|
|
659
|
+
|
|
660
|
+
// only a context passed in is drawn to in headless mode, before the texture lookup, textureInfos is empty there
|
|
661
|
+
if (headlessMode && !context) return;
|
|
662
|
+
|
|
663
|
+
// resolve texture argument: TextureInfo or index
|
|
664
|
+
const textureInfo = typeof texture === 'number' ? textureInfos[texture] : texture;
|
|
665
|
+
ASSERT(textureInfo instanceof TextureInfo, 'texture not loaded');
|
|
666
|
+
ASSERT(textureInfo.size.x > 0, 'texture not loaded');
|
|
667
|
+
ASSERT(textureInfo.wrap,
|
|
668
|
+
'drawTextureWrapped requires a wrap-enabled texture; call textureInfo.setWrap(true) first');
|
|
669
|
+
|
|
670
|
+
if (useWebGL && glEnable)
|
|
671
|
+
{
|
|
672
|
+
ASSERT(!!glContext, 'WebGL is not enabled!');
|
|
673
|
+
if (screenSpace)
|
|
674
|
+
{
|
|
675
|
+
if (glSkipScreenSpace) return;
|
|
676
|
+
[pos, size, angle] = screenToWorldTransform(pos, size, angle);
|
|
677
|
+
}
|
|
678
|
+
glSetTexture(textureInfo.glTexture);
|
|
679
|
+
glDraw(pos.x, pos.y, size.x, size.y, angle,
|
|
680
|
+
0, 0, wrapCount.x, wrapCount.y,
|
|
681
|
+
color.rgbaInt(), additiveColor && additiveColor.rgbaInt());
|
|
682
|
+
return;
|
|
683
|
+
}
|
|
684
|
+
|
|
685
|
+
// Canvas2D path — increment counts here (WebGL counts via glFlush)
|
|
686
|
+
++drawCount;
|
|
687
|
+
++primitiveCount;
|
|
688
|
+
|
|
689
|
+
if (!screenSpace)
|
|
690
|
+
{
|
|
691
|
+
pos = worldToScreen(pos);
|
|
692
|
+
size = size.scale(cameraScale);
|
|
693
|
+
angle -= cameraAngle;
|
|
694
|
+
}
|
|
695
|
+
|
|
696
|
+
// pick image source: raw, or tinted bake. Match drawImageColor's
|
|
697
|
+
// "no tint needed" predicate so behavior stays consistent.
|
|
698
|
+
const noTint = !canvasColorTiles ||
|
|
699
|
+
isWhite(color) && (!additiveColor || isBlack(additiveColor));
|
|
700
|
+
// alpha is baked into pixels by bakeTintedImage's additive branch;
|
|
701
|
+
// in that case globalAlpha must NOT also apply color.a
|
|
702
|
+
const alphaBaked = !noTint && additiveColor && !isBlack(additiveColor);
|
|
703
|
+
const source = noTint
|
|
704
|
+
? textureInfo.image
|
|
705
|
+
: bakeTintedImage(textureInfo.image, color, additiveColor);
|
|
706
|
+
|
|
707
|
+
context = context || drawContext;
|
|
708
|
+
context.save();
|
|
709
|
+
context.translate(pos.x + .5, pos.y + .5);
|
|
710
|
+
context.rotate(angle);
|
|
711
|
+
context.globalAlpha = alphaBaked ? 1 : color.a;
|
|
712
|
+
|
|
713
|
+
const pattern = context.createPattern(source, 'repeat');
|
|
714
|
+
// map pattern-source pixels into user space so the rect contains
|
|
715
|
+
// wrapCount.x × wrapCount.y repeats
|
|
716
|
+
const m = new DOMMatrix()
|
|
717
|
+
.translate(-size.x/2, -size.y/2)
|
|
718
|
+
.scale(size.x / (wrapCount.x * source.width),
|
|
719
|
+
size.y / (wrapCount.y * source.height));
|
|
720
|
+
pattern.setTransform(m);
|
|
721
|
+
context.fillStyle = pattern;
|
|
722
|
+
context.fillRect(-size.x/2, -size.y/2, size.x, size.y);
|
|
723
|
+
context.globalAlpha = 1;
|
|
724
|
+
context.restore();
|
|
725
|
+
}
|
|
726
|
+
|
|
727
|
+
/** Draw connected lines between a series of points
|
|
728
|
+
* @param {Array<Vector2>} points
|
|
729
|
+
* @param {number} [width]
|
|
730
|
+
* @param {Color} [color=WHITE]
|
|
731
|
+
* @param {boolean} [wrap] - Should the last point connect to the first?
|
|
732
|
+
* @param {Vector2} [pos=vec2()] - Offset to apply
|
|
733
|
+
* @param {number} [angle] - Angle to rotate by
|
|
734
|
+
* @param {boolean} [useWebGL=glEnable]
|
|
735
|
+
* @param {boolean} [screenSpace=drawScreenSpace]
|
|
736
|
+
* @param {CanvasRenderingContext2D|OffscreenCanvasRenderingContext2D} [context]
|
|
737
|
+
* @memberof Draw */
|
|
738
|
+
function drawLineList(points, width=.1, color=WHITE, wrap=false, pos=vec2(), angle=0, useWebGL=glEnable, screenSpace=drawScreenSpace, context)
|
|
739
|
+
{
|
|
740
|
+
ASSERT(isArray(points), 'points must be an array');
|
|
741
|
+
ASSERT(isNumber(width), 'width must be a number');
|
|
742
|
+
ASSERT(isColor(color), 'color is invalid');
|
|
743
|
+
ASSERT(isVector2(pos), 'pos must be a vec2');
|
|
744
|
+
ASSERT(isNumber(angle), 'angle must be a number');
|
|
745
|
+
ASSERT(!context || !useWebGL, 'context only supported in canvas 2D mode');
|
|
746
|
+
|
|
747
|
+
if (headlessMode && !context) return; // headless has no canvas, only a context passed in is drawn to
|
|
748
|
+
|
|
749
|
+
if (useWebGL && glEnable)
|
|
750
|
+
{
|
|
751
|
+
ASSERT(!!glContext, 'WebGL is not enabled!');
|
|
752
|
+
let size = vec2(1);
|
|
753
|
+
if (screenSpace)
|
|
754
|
+
{
|
|
755
|
+
if (glSkipScreenSpace) return;
|
|
756
|
+
[pos, size, angle] = screenToWorldTransform(pos, size, angle);
|
|
757
|
+
}
|
|
758
|
+
glDrawOutlineTransform(points, color.rgbaInt(), width, pos.x, pos.y, size.x, size.y, angle, wrap);
|
|
759
|
+
}
|
|
760
|
+
else
|
|
761
|
+
{
|
|
762
|
+
// normal canvas 2D rendering method (slower)
|
|
763
|
+
++drawCount;
|
|
764
|
+
++primitiveCount;
|
|
765
|
+
drawCanvas2D(pos, vec2(1), angle, false, (context)=>
|
|
766
|
+
{
|
|
767
|
+
// Canvas2D ignores a width of 0 or below and keeps the last one, WebGL draws none for 0
|
|
768
|
+
// and a negative width as its size, the outline turned inside out
|
|
769
|
+
if (!width) return;
|
|
770
|
+
context.strokeStyle = color.toString();
|
|
771
|
+
context.lineWidth = abs(width);
|
|
772
|
+
context.beginPath();
|
|
773
|
+
for (let i=0; i<points.length; ++i)
|
|
774
|
+
{
|
|
775
|
+
const point = points[i];
|
|
776
|
+
context.lineTo(point.x, point.y);
|
|
777
|
+
}
|
|
778
|
+
wrap && context.closePath();
|
|
779
|
+
context.stroke();
|
|
780
|
+
}, screenSpace, context);
|
|
781
|
+
}
|
|
782
|
+
}
|
|
783
|
+
|
|
784
|
+
/** Draw colored line between two points
|
|
785
|
+
* @param {Vector2} posA
|
|
786
|
+
* @param {Vector2} posB
|
|
787
|
+
* @param {number} [width]
|
|
788
|
+
* @param {Color} [color=WHITE]
|
|
789
|
+
* @param {Vector2} [pos=vec2()] - Offset to apply
|
|
790
|
+
* @param {number} [angle] - Angle to rotate by
|
|
791
|
+
* @param {boolean} [useWebGL=glEnable]
|
|
792
|
+
* @param {boolean} [screenSpace=drawScreenSpace]
|
|
793
|
+
* @param {CanvasRenderingContext2D|OffscreenCanvasRenderingContext2D} [context]
|
|
794
|
+
* @memberof Draw */
|
|
795
|
+
function drawLine(posA, posB, width=.1, color=WHITE, pos=vec2(), angle=0, useWebGL=glEnable, screenSpace=drawScreenSpace, context)
|
|
796
|
+
{
|
|
797
|
+
const halfDelta = vec2((posB.x - posA.x)/2, (posB.y - posA.y)/2);
|
|
798
|
+
const size = vec2(width, halfDelta.length()*2);
|
|
799
|
+
const middle = posA.add(halfDelta);
|
|
800
|
+
pos = pos.add(angle ? middle.rotate(screenSpace ? -angle : angle) : middle); // screen y is down, so it turns back
|
|
801
|
+
if (screenSpace)
|
|
802
|
+
halfDelta.y *= -1; // flip angle Y if screen space
|
|
803
|
+
angle += halfDelta.angle();
|
|
804
|
+
drawRect(pos, size, color, angle, useWebGL, screenSpace, context);
|
|
805
|
+
}
|
|
806
|
+
|
|
807
|
+
/** Draw colored regular polygon using passed in number of sides
|
|
808
|
+
* @param {Vector2} pos
|
|
809
|
+
* @param {Vector2} [size=vec2(1)]
|
|
810
|
+
* @param {number} [sides]
|
|
811
|
+
* @param {Color} [color=WHITE]
|
|
812
|
+
* @param {number} [lineWidth]
|
|
813
|
+
* @param {Color} [lineColor=BLACK]
|
|
814
|
+
* @param {number} [angle]
|
|
815
|
+
* @param {boolean} [useWebGL=glEnable]
|
|
816
|
+
* @param {boolean} [screenSpace=drawScreenSpace]
|
|
817
|
+
* @param {CanvasRenderingContext2D|OffscreenCanvasRenderingContext2D} [context]
|
|
818
|
+
* @memberof Draw */
|
|
819
|
+
function drawRegularPoly(pos, size=vec2(1), sides=3, color=WHITE, lineWidth=0, lineColor=BLACK, angle=0, useWebGL=glEnable, screenSpace=drawScreenSpace, context)
|
|
820
|
+
{
|
|
821
|
+
ASSERT(isVector2(size), 'size must be a vec2');
|
|
822
|
+
ASSERT(isNumber(sides), 'sides must be a number');
|
|
823
|
+
|
|
824
|
+
// build regular polygon points
|
|
825
|
+
const points = [];
|
|
826
|
+
const sizeX = size.x/2, sizeY = size.y/2;
|
|
827
|
+
for (let i=sides; i--;)
|
|
828
|
+
{
|
|
829
|
+
const a = (i/sides)*PI*2;
|
|
830
|
+
points.push(vec2(sin(a)*sizeX, cos(a)*sizeY));
|
|
831
|
+
}
|
|
832
|
+
drawPoly(points, color, lineWidth, lineColor, pos, angle, useWebGL, screenSpace, context);
|
|
833
|
+
}
|
|
834
|
+
|
|
835
|
+
/** Draw colored polygon using passed in points
|
|
836
|
+
* - WebGL fills a polygon whose edges do not cross, concave or not; a self crossing one, like a star through its
|
|
837
|
+
* outer points, fills wrong there, so draw it as its simple outline or in parts
|
|
838
|
+
* @param {Array<Vector2>} points - Array of Vector2 points
|
|
839
|
+
* @param {Color} [color=WHITE]
|
|
840
|
+
* @param {number} [lineWidth]
|
|
841
|
+
* @param {Color} [lineColor=BLACK]
|
|
842
|
+
* @param {Vector2} [pos=vec2()] - Offset to apply
|
|
843
|
+
* @param {number} [angle] - Angle to rotate by
|
|
844
|
+
* @param {boolean} [useWebGL=glEnable]
|
|
845
|
+
* @param {boolean} [screenSpace=drawScreenSpace]
|
|
846
|
+
* @param {CanvasRenderingContext2D|OffscreenCanvasRenderingContext2D} [context]
|
|
847
|
+
* @memberof Draw */
|
|
848
|
+
function drawPoly(points, color=WHITE, lineWidth=0, lineColor=BLACK, pos=vec2(), angle=0, useWebGL=glEnable, screenSpace=drawScreenSpace, context=undefined)
|
|
849
|
+
{
|
|
850
|
+
ASSERT(isVector2(pos), 'pos must be a vec2');
|
|
851
|
+
ASSERT(isArray(points), 'points must be an array');
|
|
852
|
+
ASSERT(isColor(color) && isColor(lineColor), 'color is invalid');
|
|
853
|
+
ASSERT(isNumber(lineWidth), 'lineWidth must be a number');
|
|
854
|
+
ASSERT(isNumber(angle), 'angle must be a number');
|
|
855
|
+
ASSERT(!context || !useWebGL, 'context only supported in canvas 2D mode');
|
|
856
|
+
|
|
857
|
+
if (headlessMode && !context) return; // headless has no canvas, only a context passed in is drawn to
|
|
858
|
+
|
|
859
|
+
if (useWebGL && glEnable)
|
|
860
|
+
{
|
|
861
|
+
ASSERT(!!glContext, 'WebGL is not enabled!');
|
|
862
|
+
let size = vec2(1);
|
|
863
|
+
if (screenSpace)
|
|
864
|
+
{
|
|
865
|
+
if (glSkipScreenSpace) return;
|
|
866
|
+
[pos, size, angle] = screenToWorldTransform(pos, size, angle);
|
|
867
|
+
}
|
|
868
|
+
glDrawPointsTransform(points, color.rgbaInt(), pos.x, pos.y, size.x, size.y, angle);
|
|
869
|
+
if (lineWidth > 0)
|
|
870
|
+
glDrawOutlineTransform(points, lineColor.rgbaInt(), lineWidth, pos.x, pos.y, size.x, size.y, angle);
|
|
871
|
+
}
|
|
872
|
+
else
|
|
873
|
+
{
|
|
874
|
+
++drawCount;
|
|
875
|
+
++primitiveCount;
|
|
876
|
+
drawCanvas2D(pos, vec2(1), angle, false, context=>
|
|
877
|
+
{
|
|
878
|
+
context.fillStyle = color.toString();
|
|
879
|
+
context.beginPath();
|
|
880
|
+
for (const point of points)
|
|
881
|
+
context.lineTo(point.x, point.y);
|
|
882
|
+
context.closePath();
|
|
883
|
+
context.fill();
|
|
884
|
+
if (lineWidth > 0) // as in WebGL, Canvas2D would stroke a width below 0 with the last one
|
|
885
|
+
{
|
|
886
|
+
context.strokeStyle = lineColor.toString();
|
|
887
|
+
context.lineWidth = lineWidth;
|
|
888
|
+
context.stroke();
|
|
889
|
+
}
|
|
890
|
+
}, screenSpace, context);
|
|
891
|
+
}
|
|
892
|
+
}
|
|
893
|
+
|
|
894
|
+
// the unit rings drawEllipse fills with in WebGL, one for each side count
|
|
895
|
+
const drawEllipseRings = new Map;
|
|
896
|
+
|
|
897
|
+
/** Draw colored ellipse using passed in point
|
|
898
|
+
* @param {Vector2} pos
|
|
899
|
+
* @param {Vector2} [size=vec2(1)] - Width and height diameter
|
|
900
|
+
* @param {Color} [color=WHITE]
|
|
901
|
+
* @param {number} [angle]
|
|
902
|
+
* @param {number} [lineWidth]
|
|
903
|
+
* @param {Color} [lineColor=BLACK]
|
|
904
|
+
* @param {boolean} [useWebGL=glEnable]
|
|
905
|
+
* @param {boolean} [screenSpace=drawScreenSpace]
|
|
906
|
+
* @param {CanvasRenderingContext2D|OffscreenCanvasRenderingContext2D} [context]
|
|
907
|
+
* @memberof Draw */
|
|
908
|
+
function drawEllipse(pos, size=vec2(1), color=WHITE, angle=0, lineWidth=0, lineColor=BLACK, useWebGL=glEnable, screenSpace=drawScreenSpace, context)
|
|
909
|
+
{
|
|
910
|
+
ASSERT(isVector2(pos), 'pos must be a vec2');
|
|
911
|
+
ASSERT(isVector2(size), 'size must be a vec2');
|
|
912
|
+
ASSERT(isColor(color) && isColor(lineColor), 'color is invalid');
|
|
913
|
+
ASSERT(isNumber(angle), 'angle must be a number');
|
|
914
|
+
ASSERT(isNumber(lineWidth), 'lineWidth must be a number');
|
|
915
|
+
ASSERT(lineWidth >= 0, 'lineWidth must be a positive value or 0');
|
|
916
|
+
ASSERT(!context || !useWebGL, 'context only supported in canvas 2D mode');
|
|
917
|
+
|
|
918
|
+
if (headlessMode && !context) return; // headless has no canvas, only a context passed in is drawn to
|
|
919
|
+
|
|
920
|
+
// clamp line width to prevent artifacts, a negative size draws mirrored
|
|
921
|
+
lineWidth = clamp(lineWidth, 0, min(abs(size.x), abs(size.y)));
|
|
922
|
+
|
|
923
|
+
if (useWebGL && glEnable)
|
|
924
|
+
{
|
|
925
|
+
const sides = glCircleSides;
|
|
926
|
+
if (lineWidth > 0)
|
|
927
|
+
{
|
|
928
|
+
// draw as a regular polygon, the outline is made from the points at their size
|
|
929
|
+
drawRegularPoly(pos, size, sides, color, lineWidth, lineColor, angle, useWebGL, screenSpace, context);
|
|
930
|
+
return;
|
|
931
|
+
}
|
|
932
|
+
|
|
933
|
+
// a fill is a unit ring scaled, made once for each side count and already in strip order
|
|
934
|
+
let ring = drawEllipseRings.get(sides);
|
|
935
|
+
if (!ring)
|
|
936
|
+
{
|
|
937
|
+
const points = [];
|
|
938
|
+
for (let i=sides; i--;)
|
|
939
|
+
{
|
|
940
|
+
const a = (i/sides)*PI*2;
|
|
941
|
+
points.push(vec2(sin(a), cos(a)));
|
|
942
|
+
}
|
|
943
|
+
drawEllipseRings.set(sides, ring = glPolyStrip(points));
|
|
944
|
+
}
|
|
945
|
+
if (screenSpace)
|
|
946
|
+
{
|
|
947
|
+
if (glSkipScreenSpace) return;
|
|
948
|
+
[pos, size, angle] = screenToWorldTransform(pos, size, angle);
|
|
949
|
+
}
|
|
950
|
+
glDrawPointsTransform(ring, color.rgbaInt(), pos.x, pos.y, size.x/2, size.y/2, angle, false);
|
|
951
|
+
}
|
|
952
|
+
else
|
|
953
|
+
{
|
|
954
|
+
++drawCount;
|
|
955
|
+
++primitiveCount;
|
|
956
|
+
drawCanvas2D(pos, vec2(1), angle, false, context=>
|
|
957
|
+
{
|
|
958
|
+
context.fillStyle = color.toString();
|
|
959
|
+
context.beginPath();
|
|
960
|
+
context.ellipse(0, 0, abs(size.x)/2, abs(size.y)/2, 0, 0, 9); // it throws on a negative radius
|
|
961
|
+
context.fill();
|
|
962
|
+
if (lineWidth > 0)
|
|
963
|
+
{
|
|
964
|
+
context.strokeStyle = lineColor.toString();
|
|
965
|
+
context.lineWidth = lineWidth;
|
|
966
|
+
context.stroke();
|
|
967
|
+
}
|
|
968
|
+
}, screenSpace, context);
|
|
969
|
+
}
|
|
970
|
+
}
|
|
971
|
+
|
|
972
|
+
/** Draw colored circle using passed in point
|
|
973
|
+
* @param {Vector2} pos
|
|
974
|
+
* @param {number} [size=1] - Diameter
|
|
975
|
+
* @param {Color} [color=WHITE]
|
|
976
|
+
* @param {number} [lineWidth=0]
|
|
977
|
+
* @param {Color} [lineColor=BLACK]
|
|
978
|
+
* @param {boolean} [useWebGL=glEnable]
|
|
979
|
+
* @param {boolean} [screenSpace=drawScreenSpace]
|
|
980
|
+
* @param {CanvasRenderingContext2D|OffscreenCanvasRenderingContext2D} [context]
|
|
981
|
+
* @memberof Draw */
|
|
982
|
+
function drawCircle(pos, size=1, color=WHITE, lineWidth=0, lineColor=BLACK, useWebGL=glEnable, screenSpace=drawScreenSpace, context)
|
|
983
|
+
{
|
|
984
|
+
ASSERT(isNumber(size), 'size must be a number');
|
|
985
|
+
drawEllipse(pos, vec2(size), color, 0, lineWidth, lineColor, useWebGL, screenSpace, context);
|
|
986
|
+
}
|
|
987
|
+
|
|
988
|
+
let drawEllipseGradientOffset = 0;
|
|
989
|
+
/** Draw an ellipse filled with a radial gradient from the center to the rim
|
|
990
|
+
* - Best when batched with other untextured polys
|
|
991
|
+
* - If drawing mostly textured sprites, bake the gradient into a texture and use drawTile instead
|
|
992
|
+
* - Stacking gradients at the exact same position may show a faint vertical artifact
|
|
993
|
+
* @param {Vector2} pos
|
|
994
|
+
* @param {Vector2} [size=vec2(1)] - Width and height diameter
|
|
995
|
+
* @param {Color} [colorInner=WHITE]
|
|
996
|
+
* @param {Color} [colorOuter=CLEAR_WHITE]
|
|
997
|
+
* @param {number} [angle]
|
|
998
|
+
* @param {boolean} [useWebGL=glEnable]
|
|
999
|
+
* @param {boolean} [screenSpace=drawScreenSpace]
|
|
1000
|
+
* @param {CanvasRenderingContext2D|OffscreenCanvasRenderingContext2D} [context]
|
|
1001
|
+
* @memberof Draw */
|
|
1002
|
+
function drawEllipseGradient(pos, size=vec2(1), colorInner=WHITE, colorOuter=CLEAR_WHITE, angle=0, useWebGL=glEnable, screenSpace=drawScreenSpace, context)
|
|
1003
|
+
{
|
|
1004
|
+
ASSERT(isVector2(pos), 'pos must be a vec2');
|
|
1005
|
+
ASSERT(isVector2(size), 'size must be a vec2');
|
|
1006
|
+
ASSERT(isColor(colorInner) && isColor(colorOuter), 'color is invalid');
|
|
1007
|
+
ASSERT(isNumber(angle), 'angle must be a number');
|
|
1008
|
+
ASSERT(!context || !useWebGL, 'context only supported in canvas 2D mode');
|
|
1009
|
+
|
|
1010
|
+
if (headlessMode && !context) return; // headless has no canvas, only a context passed in is drawn to
|
|
1011
|
+
|
|
1012
|
+
if (useWebGL && glEnable)
|
|
1013
|
+
{
|
|
1014
|
+
ASSERT(!!glContext, 'WebGL is not enabled!');
|
|
1015
|
+
if (screenSpace)
|
|
1016
|
+
{
|
|
1017
|
+
if (glSkipScreenSpace) return;
|
|
1018
|
+
[pos, size, angle] = screenToWorldTransform(pos, size, angle);
|
|
1019
|
+
}
|
|
1020
|
+
// fan as tristrip; rotate the boundary vertex by one slice per call
|
|
1021
|
+
// so back-to-back gradients at the same position have their hole
|
|
1022
|
+
// (from gpu edge-rule on the boundary line-degen) at different rim
|
|
1023
|
+
// verts and don't visibly stack
|
|
1024
|
+
const sides = glCircleSides;
|
|
1025
|
+
const radiusX = size.x/2, radiusY = size.y/2;
|
|
1026
|
+
const innerInt = colorInner.rgbaInt();
|
|
1027
|
+
const outerInt = colorOuter.rgbaInt();
|
|
1028
|
+
const offset = drawEllipseGradientOffset++;
|
|
1029
|
+
const c = cos(-angle), s = sin(-angle);
|
|
1030
|
+
const rim = (a) =>
|
|
1031
|
+
{
|
|
1032
|
+
const lx = sin(a)*radiusX, ly = cos(a)*radiusY;
|
|
1033
|
+
return vec2(pos.x + lx*c - ly*s, pos.y + lx*s + ly*c);
|
|
1034
|
+
};
|
|
1035
|
+
const startA = (offset%sides)/sides*PI*2;
|
|
1036
|
+
const points = [rim(startA)];
|
|
1037
|
+
const colors = [outerInt];
|
|
1038
|
+
for (let i=sides; i--;)
|
|
1039
|
+
{
|
|
1040
|
+
const a = ((i+offset)%sides)/sides*PI*2;
|
|
1041
|
+
points.push(pos);
|
|
1042
|
+
colors.push(innerInt);
|
|
1043
|
+
points.push(rim(a));
|
|
1044
|
+
colors.push(outerInt);
|
|
1045
|
+
}
|
|
1046
|
+
glDrawColoredPoints(points, colors);
|
|
1047
|
+
}
|
|
1048
|
+
else
|
|
1049
|
+
{
|
|
1050
|
+
// normal canvas 2D rendering method (slower)
|
|
1051
|
+
++drawCount;
|
|
1052
|
+
++primitiveCount;
|
|
1053
|
+
drawCanvas2D(pos, size, angle, false, (context)=>
|
|
1054
|
+
{
|
|
1055
|
+
const gradient = context.createRadialGradient(0, 0, 0, 0, 0, .5);
|
|
1056
|
+
gradient.addColorStop(0, colorInner.toString());
|
|
1057
|
+
gradient.addColorStop(1, colorOuter.toString());
|
|
1058
|
+
context.fillStyle = gradient;
|
|
1059
|
+
context.beginPath();
|
|
1060
|
+
context.ellipse(0, 0, .5, .5, 0, 0, 9);
|
|
1061
|
+
context.fill();
|
|
1062
|
+
}, screenSpace, context);
|
|
1063
|
+
}
|
|
1064
|
+
}
|
|
1065
|
+
|
|
1066
|
+
/** Draw a circle filled with a radial gradient from the center to the rim
|
|
1067
|
+
* - Best when batched with other untextured polys
|
|
1068
|
+
* - If drawing mostly textured sprites, bake the gradient into a texture and use drawTile instead
|
|
1069
|
+
* - Stacking gradients at the exact same position may show a faint vertical artifact
|
|
1070
|
+
* @param {Vector2} pos
|
|
1071
|
+
* @param {number} [size=1] - Diameter
|
|
1072
|
+
* @param {Color} [colorInner=WHITE]
|
|
1073
|
+
* @param {Color} [colorOuter=CLEAR_WHITE]
|
|
1074
|
+
* @param {boolean} [useWebGL=glEnable]
|
|
1075
|
+
* @param {boolean} [screenSpace=drawScreenSpace]
|
|
1076
|
+
* @param {CanvasRenderingContext2D|OffscreenCanvasRenderingContext2D} [context]
|
|
1077
|
+
* @memberof Draw */
|
|
1078
|
+
function drawCircleGradient(pos, size=1, colorInner=WHITE, colorOuter=CLEAR_WHITE, useWebGL=glEnable, screenSpace=drawScreenSpace, context)
|
|
1079
|
+
{
|
|
1080
|
+
ASSERT(isNumber(size), 'size must be a number');
|
|
1081
|
+
drawEllipseGradient(pos, vec2(size), colorInner, colorOuter, 0, useWebGL, screenSpace, context);
|
|
1082
|
+
}
|
|
1083
|
+
|
|
1084
|
+
/**
|
|
1085
|
+
* @callback Canvas2DDrawFunction - A function that draws to a 2D canvas context
|
|
1086
|
+
* @param {CanvasRenderingContext2D|OffscreenCanvasRenderingContext2D} context
|
|
1087
|
+
* @memberof Draw
|
|
1088
|
+
*/
|
|
1089
|
+
|
|
1090
|
+
/** Draw directly to a 2d canvas context in world space.
|
|
1091
|
+
* The Y axis is flipped so world-Y-up coordinates render right-side up
|
|
1092
|
+
* (matches the WebGL path). Callers whose drawing depends on Y direction
|
|
1093
|
+
* (e.g. linear gradients) should flip their own Y endpoints accordingly.
|
|
1094
|
+
* @param {Vector2} pos
|
|
1095
|
+
* @param {Vector2} size
|
|
1096
|
+
* @param {number} [angle]
|
|
1097
|
+
* @param {boolean} [mirror]
|
|
1098
|
+
* @param {Canvas2DDrawFunction} [drawFunction] - Needed, marked optional only because the ones before it are
|
|
1099
|
+
* @param {boolean} [screenSpace=drawScreenSpace]
|
|
1100
|
+
* @param {CanvasRenderingContext2D|OffscreenCanvasRenderingContext2D} [context=drawContext]
|
|
1101
|
+
* @memberof Draw */
|
|
1102
|
+
function drawCanvas2D(pos, size, angle=0, mirror=false, drawFunction, screenSpace=drawScreenSpace, context=drawContext)
|
|
1103
|
+
{
|
|
1104
|
+
ASSERT(isVector2(pos), 'pos must be a vec2');
|
|
1105
|
+
ASSERT(isVector2(size), 'size must be a vec2');
|
|
1106
|
+
ASSERT(isNumber(angle), 'angle must be a number');
|
|
1107
|
+
ASSERT(typeof drawFunction === 'function', 'drawFunction must be a function');
|
|
1108
|
+
|
|
1109
|
+
if (headlessMode && !context) return; // headless has no canvas, only a context passed in is drawn to
|
|
1110
|
+
|
|
1111
|
+
if (!screenSpace)
|
|
1112
|
+
{
|
|
1113
|
+
pos = worldToScreen(pos);
|
|
1114
|
+
size = size.scale(cameraScale);
|
|
1115
|
+
angle -= cameraAngle;
|
|
1116
|
+
}
|
|
1117
|
+
context.save();
|
|
1118
|
+
context.translate(pos.x+.5, pos.y+.5);
|
|
1119
|
+
context.rotate(angle);
|
|
1120
|
+
context.scale(mirror ? -size.x : size.x, -size.y);
|
|
1121
|
+
drawFunction(context);
|
|
1122
|
+
context.restore();
|
|
1123
|
+
}
|
|
1124
|
+
|
|
1125
|
+
///////////////////////////////////////////////////////////////////////////////
|
|
1126
|
+
// Text Drawing Functions
|
|
1127
|
+
|
|
1128
|
+
/** Draw text on main canvas in world space
|
|
1129
|
+
* Automatically splits new lines into rows
|
|
1130
|
+
* @param {string|number} text
|
|
1131
|
+
* @param {Vector2} pos
|
|
1132
|
+
* @param {number} [size]
|
|
1133
|
+
* @param {Color} [color=WHITE]
|
|
1134
|
+
* @param {number} [lineWidth]
|
|
1135
|
+
* @param {Color} [lineColor=BLACK]
|
|
1136
|
+
* @param {'left'|'center'|'right'} [textAlign='center']
|
|
1137
|
+
* @param {string} [font=fontDefault]
|
|
1138
|
+
* @param {string} [fontStyle]
|
|
1139
|
+
* @param {number} [maxWidth]
|
|
1140
|
+
* @param {number} [angle]
|
|
1141
|
+
* @param {CanvasRenderingContext2D|OffscreenCanvasRenderingContext2D} [context=drawContext]
|
|
1142
|
+
* @memberof Draw */
|
|
1143
|
+
function drawText(text, pos, size=1, color=WHITE, lineWidth=0, lineColor=BLACK, textAlign='center', font=fontDefault, fontStyle='', maxWidth, angle=0, context=drawContext)
|
|
1144
|
+
{
|
|
1145
|
+
// convert to screen space
|
|
1146
|
+
pos = worldToScreen(pos);
|
|
1147
|
+
size *= cameraScale;
|
|
1148
|
+
lineWidth *= cameraScale;
|
|
1149
|
+
if (maxWidth !== undefined)
|
|
1150
|
+
maxWidth *= cameraScale;
|
|
1151
|
+
angle -= cameraAngle;
|
|
1152
|
+
|
|
1153
|
+
drawTextScreen(text, pos, size, color, lineWidth, lineColor, textAlign, font, fontStyle, maxWidth, angle, context);
|
|
1154
|
+
}
|
|
1155
|
+
|
|
1156
|
+
/** Draw text in screen space
|
|
1157
|
+
* Automatically splits new lines into rows
|
|
1158
|
+
* @param {string|number} text
|
|
1159
|
+
* @param {Vector2} pos
|
|
1160
|
+
* @param {number} size
|
|
1161
|
+
* @param {Color} [color=WHITE]
|
|
1162
|
+
* @param {number} [lineWidth]
|
|
1163
|
+
* @param {Color} [lineColor=BLACK]
|
|
1164
|
+
* @param {'left'|'center'|'right'} [textAlign]
|
|
1165
|
+
* @param {string} [font=fontDefault]
|
|
1166
|
+
* @param {string} [fontStyle]
|
|
1167
|
+
* @param {number} [maxWidth]
|
|
1168
|
+
* @param {number} [angle] - Clockwise, like the other screen space draws
|
|
1169
|
+
* @param {CanvasRenderingContext2D|OffscreenCanvasRenderingContext2D} [context=drawContext]
|
|
1170
|
+
* @memberof Draw */
|
|
1171
|
+
function drawTextScreen(text, pos, size, color=WHITE, lineWidth=0, lineColor=BLACK, textAlign='center', font=fontDefault, fontStyle='', maxWidth, angle=0, context=drawContext)
|
|
1172
|
+
{
|
|
1173
|
+
ASSERT(isStringLike(text), 'text must be a string');
|
|
1174
|
+
ASSERT(isVector2(pos), 'pos must be a vec2');
|
|
1175
|
+
ASSERT(isNumber(size), 'size must be a number');
|
|
1176
|
+
ASSERT(isColor(color), 'color must be a color');
|
|
1177
|
+
ASSERT(isNumber(lineWidth), 'lineWidth must be a number');
|
|
1178
|
+
ASSERT(isColor(lineColor), 'lineColor must be a color');
|
|
1179
|
+
ASSERT(['left','center','right'].includes(textAlign), 'align must be left, center, or right');
|
|
1180
|
+
ASSERT(isStringLike(font), 'font must be a string');
|
|
1181
|
+
ASSERT(isStringLike(fontStyle), 'fontStyle must be a string');
|
|
1182
|
+
ASSERT(isNumber(angle), 'angle must be a number');
|
|
1183
|
+
|
|
1184
|
+
if (headlessMode && !context) return; // headless has no canvas, only a context passed in is drawn to
|
|
1185
|
+
|
|
1186
|
+
const lines = (text+'').split('\n');
|
|
1187
|
+
// save before style mutations so caller's context state is preserved
|
|
1188
|
+
context.save();
|
|
1189
|
+
context.fillStyle = color.toString();
|
|
1190
|
+
context.strokeStyle = lineColor.toString();
|
|
1191
|
+
context.lineWidth = lineWidth;
|
|
1192
|
+
context.textAlign = textAlign;
|
|
1193
|
+
context.font = fontStyle + ' ' + size + 'px '+ font;
|
|
1194
|
+
context.textBaseline = 'middle';
|
|
1195
|
+
context.translate(pos.x + .5, pos.y + .5); // a screen position is the center of a pixel, as for every other draw
|
|
1196
|
+
context.rotate(angle);
|
|
1197
|
+
let yOffset = -(lines.length-1) * size/2; // center vertically
|
|
1198
|
+
lines.forEach(line=>
|
|
1199
|
+
{
|
|
1200
|
+
lineWidth && context.strokeText(line, 0, yOffset, maxWidth);
|
|
1201
|
+
context.fillText(line, 0, yOffset, maxWidth);
|
|
1202
|
+
yOffset += size;
|
|
1203
|
+
});
|
|
1204
|
+
context.restore();
|
|
1205
|
+
}
|
|
1206
|
+
|
|
1207
|
+
///////////////////////////////////////////////////////////////////////////////
|
|
1208
|
+
// Drawing utilities
|
|
1209
|
+
|
|
1210
|
+
/** Load a texture at a specific index after engineInit, the images passed to engineInit load this way
|
|
1211
|
+
* @param {number} textureIndex - Index to store the texture at, an unused one
|
|
1212
|
+
* @param {string} [src] - Image source path
|
|
1213
|
+
* @return {Promise<TextureInfo>} Resolves to the texture info once the image loads, or fails to with a warning
|
|
1214
|
+
* @memberof Draw */
|
|
1215
|
+
async function loadTexture(textureIndex, src)
|
|
1216
|
+
{
|
|
1217
|
+
ASSERT(isNumber(textureIndex), 'textureIndex must be a number');
|
|
1218
|
+
// engineInit's empty placeholder in slot 0, when it was given no images, or a load that failed can be replaced
|
|
1219
|
+
const old = textureInfos[textureIndex];
|
|
1220
|
+
ASSERT(!old?.size.x, 'textureIndex is already loaded!');
|
|
1221
|
+
old?.destroyWebGLTexture();
|
|
1222
|
+
ASSERT(!src || isStringLike(src), 'image src must be a string');
|
|
1223
|
+
|
|
1224
|
+
const image = new Image;
|
|
1225
|
+
if (src)
|
|
1226
|
+
{
|
|
1227
|
+
await new Promise(resolve =>
|
|
1228
|
+
{
|
|
1229
|
+
image.onload = resolve;
|
|
1230
|
+
image.onerror = ()=>
|
|
1231
|
+
{
|
|
1232
|
+
console.warn('failed to load image: ' + src); // in release too, like the WebGL warning
|
|
1233
|
+
resolve();
|
|
1234
|
+
};
|
|
1235
|
+
image.crossOrigin = 'anonymous';
|
|
1236
|
+
image.src = src;
|
|
1237
|
+
});
|
|
1238
|
+
}
|
|
1239
|
+
|
|
1240
|
+
return textureInfos[textureIndex] = new TextureInfo(image);
|
|
1241
|
+
}
|
|
1242
|
+
|
|
1243
|
+
/** Convert from screen to world space coordinates
|
|
1244
|
+
* @param {Vector2} screenPos
|
|
1245
|
+
* @return {Vector2}
|
|
1246
|
+
* @memberof Draw */
|
|
1247
|
+
function screenToWorld(screenPos)
|
|
1248
|
+
{
|
|
1249
|
+
ASSERT(isVector2(screenPos), 'screenPos must be a vec2');
|
|
1250
|
+
|
|
1251
|
+
let x = (screenPos.x - mainCanvasSize.x/2 + .5) / cameraScale;
|
|
1252
|
+
let y = (screenPos.y - mainCanvasSize.y/2 + .5) / -cameraScale;
|
|
1253
|
+
if (cameraAngle)
|
|
1254
|
+
{
|
|
1255
|
+
// apply camera rotation
|
|
1256
|
+
const c = cos(-cameraAngle), s = sin(-cameraAngle);
|
|
1257
|
+
const xr = x * c - y * s, yr = x * s + y * c;
|
|
1258
|
+
x = xr; y = yr;
|
|
1259
|
+
}
|
|
1260
|
+
return new Vector2(x + cameraPos.x, y + cameraPos.y);
|
|
1261
|
+
}
|
|
1262
|
+
|
|
1263
|
+
/** Convert from world to screen space coordinates
|
|
1264
|
+
* @param {Vector2} worldPos
|
|
1265
|
+
* @return {Vector2}
|
|
1266
|
+
* @memberof Draw */
|
|
1267
|
+
function worldToScreen(worldPos)
|
|
1268
|
+
{
|
|
1269
|
+
ASSERT(isVector2(worldPos), 'worldPos must be a vec2');
|
|
1270
|
+
|
|
1271
|
+
let x = worldPos.x - cameraPos.x;
|
|
1272
|
+
let y = worldPos.y - cameraPos.y;
|
|
1273
|
+
if (cameraAngle)
|
|
1274
|
+
{
|
|
1275
|
+
// apply inverse camera rotation
|
|
1276
|
+
const c = cos(cameraAngle), s = sin(cameraAngle);
|
|
1277
|
+
const xr = x * c - y * s, yr = x * s + y * c;
|
|
1278
|
+
x = xr; y = yr;
|
|
1279
|
+
}
|
|
1280
|
+
return new Vector2
|
|
1281
|
+
(
|
|
1282
|
+
x * cameraScale + mainCanvasSize.x/2 - .5,
|
|
1283
|
+
y * -cameraScale + mainCanvasSize.y/2 - .5
|
|
1284
|
+
);
|
|
1285
|
+
}
|
|
1286
|
+
|
|
1287
|
+
/** Convert from screen to world space coordinates for a directional vector (no translation)
|
|
1288
|
+
* @param {Vector2} screenDelta
|
|
1289
|
+
* @return {Vector2}
|
|
1290
|
+
* @memberof Draw */
|
|
1291
|
+
function screenToWorldDelta(screenDelta)
|
|
1292
|
+
{
|
|
1293
|
+
ASSERT(isVector2(screenDelta), 'screenDelta must be a vec2');
|
|
1294
|
+
|
|
1295
|
+
let x = screenDelta.x / cameraScale;
|
|
1296
|
+
let y = screenDelta.y / -cameraScale;
|
|
1297
|
+
if (cameraAngle)
|
|
1298
|
+
{
|
|
1299
|
+
// apply camera rotation
|
|
1300
|
+
const c = cos(-cameraAngle), s = sin(-cameraAngle);
|
|
1301
|
+
const xr = x * c - y * s, yr = x * s + y * c;
|
|
1302
|
+
x = xr; y = yr;
|
|
1303
|
+
}
|
|
1304
|
+
return new Vector2(x, y);
|
|
1305
|
+
}
|
|
1306
|
+
|
|
1307
|
+
/** Convert from world to screen space coordinates for a directional vector (no translation)
|
|
1308
|
+
* @param {Vector2} worldDelta
|
|
1309
|
+
* @return {Vector2}
|
|
1310
|
+
* @memberof Draw */
|
|
1311
|
+
function worldToScreenDelta(worldDelta)
|
|
1312
|
+
{
|
|
1313
|
+
ASSERT(isVector2(worldDelta), 'worldDelta must be a vec2');
|
|
1314
|
+
|
|
1315
|
+
let x = worldDelta.x;
|
|
1316
|
+
let y = worldDelta.y;
|
|
1317
|
+
if (cameraAngle)
|
|
1318
|
+
{
|
|
1319
|
+
// apply inverse camera rotation
|
|
1320
|
+
const c = cos(cameraAngle), s = sin(cameraAngle);
|
|
1321
|
+
const xr = x * c - y * s, yr = x * s + y * c;
|
|
1322
|
+
x = xr; y = yr;
|
|
1323
|
+
}
|
|
1324
|
+
return new Vector2(x * cameraScale, y * -cameraScale);
|
|
1325
|
+
}
|
|
1326
|
+
|
|
1327
|
+
/** Convert screen space transform to world space
|
|
1328
|
+
* @param {Vector2} screenPos
|
|
1329
|
+
* @param {Vector2} screenSize
|
|
1330
|
+
* @param {number} [screenAngle]
|
|
1331
|
+
* @return {[Vector2, Vector2, number]} - [pos, size, angle]
|
|
1332
|
+
* @memberof Draw */
|
|
1333
|
+
function screenToWorldTransform(screenPos, screenSize, screenAngle=0)
|
|
1334
|
+
{
|
|
1335
|
+
ASSERT(isVector2(screenPos), 'screenPos must be a vec2');
|
|
1336
|
+
ASSERT(isVector2(screenSize), 'screenSize must be a vec2');
|
|
1337
|
+
ASSERT(isNumber(screenAngle), 'screenAngle must be a number');
|
|
1338
|
+
|
|
1339
|
+
return [
|
|
1340
|
+
screenToWorld(screenPos),
|
|
1341
|
+
screenSize.scale(1/cameraScale),
|
|
1342
|
+
screenAngle + cameraAngle
|
|
1343
|
+
];
|
|
1344
|
+
}
|
|
1345
|
+
|
|
1346
|
+
/** Get the size of the camera window in world space
|
|
1347
|
+
* @return {Vector2}
|
|
1348
|
+
* @memberof Draw */
|
|
1349
|
+
function getCameraSize() { return mainCanvasSize.scale(1/cameraScale); }
|
|
1350
|
+
|
|
1351
|
+
/** Padding for each side of a rectangle, the sides left out are 0
|
|
1352
|
+
* @typedef {Object} CameraFitSides
|
|
1353
|
+
* @property {number} [top]
|
|
1354
|
+
* @property {number} [right]
|
|
1355
|
+
* @property {number} [bottom]
|
|
1356
|
+
* @property {number} [left]
|
|
1357
|
+
* @memberof Draw */
|
|
1358
|
+
|
|
1359
|
+
/** Fit the camera to a rectangle in world space by setting cameraPos and cameraScale
|
|
1360
|
+
* - worldMargin pads the content rectangle in world units, so the gap scales with the content on resize
|
|
1361
|
+
* - screenInset reserves space in screen pixels on each viewport edge (for example a HUD band) and
|
|
1362
|
+
* re-centers the content away from that edge, so the reserved band stays a fixed pixel size on resize
|
|
1363
|
+
* - worldMargin and screenInset may each be a number for all sides, a Vector2 (x=left/right, y=top/bottom),
|
|
1364
|
+
* or an object with any of {top, right, bottom, left}
|
|
1365
|
+
* @param {Vector2} center - Center of the rectangle in world space
|
|
1366
|
+
* @param {Vector2} size - Size of the rectangle in world space
|
|
1367
|
+
* @param {number|Vector2|CameraFitSides} [worldMargin] - World space padding added around the content rectangle
|
|
1368
|
+
* @param {number|Vector2|CameraFitSides} [screenInset] - Screen space padding in pixels reserved on each viewport edge
|
|
1369
|
+
* @return {number} - The new camera scale
|
|
1370
|
+
* @memberof Draw */
|
|
1371
|
+
function cameraFit(center, size, worldMargin, screenInset)
|
|
1372
|
+
{
|
|
1373
|
+
ASSERT(isVector2(center), 'center must be a vec2');
|
|
1374
|
+
ASSERT(isVector2(size), 'size must be a vec2');
|
|
1375
|
+
|
|
1376
|
+
// pad the content
|
|
1377
|
+
const margin = padSides(worldMargin);
|
|
1378
|
+
const inset = padSides(screenInset);
|
|
1379
|
+
const worldW = size.x + margin.left + margin.right;
|
|
1380
|
+
const worldH = size.y + margin.top + margin.bottom;
|
|
1381
|
+
const viewW = mainCanvasSize.x - inset.left - inset.right;
|
|
1382
|
+
const viewH = mainCanvasSize.y - inset.top - inset.bottom;
|
|
1383
|
+
|
|
1384
|
+
// bail on a degenerate rect or viewport rather than NaN the camera
|
|
1385
|
+
if (!(worldW > 0 && worldH > 0 && viewW > 0 && viewH > 0))
|
|
1386
|
+
return cameraScale;
|
|
1387
|
+
|
|
1388
|
+
// scale to fit the padded content, measured along the camera's axes when it is turned
|
|
1389
|
+
const c = cos(cameraAngle), s = sin(cameraAngle);
|
|
1390
|
+
const fitW = abs(c) * worldW + abs(s) * worldH, fitH = abs(s) * worldW + abs(c) * worldH;
|
|
1391
|
+
cameraScale = min(viewW / fitW, viewH / fitH);
|
|
1392
|
+
|
|
1393
|
+
// calculate offset vectors, the inset is on the screen, so it turns back into the world with the camera
|
|
1394
|
+
const marginVector = vec2(margin.right - margin.left, margin.top - margin.bottom).scale(.5);
|
|
1395
|
+
const ix = (inset.right - inset.left) * .5 / cameraScale, iy = (inset.top - inset.bottom) * .5 / cameraScale;
|
|
1396
|
+
const insetVector = vec2(ix * c + iy * s, iy * c - ix * s);
|
|
1397
|
+
|
|
1398
|
+
// apply the offsets and return camera scale
|
|
1399
|
+
cameraPos = center.add(marginVector).add(insetVector);
|
|
1400
|
+
return cameraScale;
|
|
1401
|
+
|
|
1402
|
+
function padSides(p)
|
|
1403
|
+
{
|
|
1404
|
+
// normalize a padding option to {top, right, bottom, left}
|
|
1405
|
+
if (p === undefined || isNumber(p))
|
|
1406
|
+
p = vec2(p);
|
|
1407
|
+
if (isVector2(p))
|
|
1408
|
+
return { top: p.y, right: p.x, bottom: p.y, left: p.x };
|
|
1409
|
+
return {
|
|
1410
|
+
top: p.top || 0,
|
|
1411
|
+
right: p.right || 0,
|
|
1412
|
+
bottom: p.bottom || 0,
|
|
1413
|
+
left: p.left || 0,
|
|
1414
|
+
};
|
|
1415
|
+
}
|
|
1416
|
+
}
|
|
1417
|
+
|
|
1418
|
+
/** Check if a box, point, or circle is on screen with a circle test
|
|
1419
|
+
* If size is a Vector2, uses the length as diameter
|
|
1420
|
+
* This can be used to cull offscreen objects from render or update
|
|
1421
|
+
* @param {Vector2} pos - world space position
|
|
1422
|
+
* @param {Vector2|number} [size] - world space size or diameter
|
|
1423
|
+
* @return {boolean}
|
|
1424
|
+
* @memberof Draw */
|
|
1425
|
+
function isOnScreen(pos, size=0)
|
|
1426
|
+
{
|
|
1427
|
+
ASSERT(isVector2(pos), 'pos must be a vec2');
|
|
1428
|
+
ASSERT(isVector2(size) || isNumber(size), 'size must be a vec2 or number');
|
|
1429
|
+
|
|
1430
|
+
// cameraScale of 0 collapses world coords; nothing is visible
|
|
1431
|
+
if (!cameraScale) return false;
|
|
1432
|
+
|
|
1433
|
+
// circle on screen test, worldToScreen inlined in doubled screen units so it compares against the full canvas size
|
|
1434
|
+
let x = pos.x - cameraPos.x;
|
|
1435
|
+
let y = pos.y - cameraPos.y;
|
|
1436
|
+
if (cameraAngle)
|
|
1437
|
+
{
|
|
1438
|
+
// apply inverse camera rotation
|
|
1439
|
+
const c = cos(cameraAngle), s = sin(cameraAngle);
|
|
1440
|
+
const xr = x * c - y * s, yr = x * s + y * c;
|
|
1441
|
+
x = xr; y = yr;
|
|
1442
|
+
}
|
|
1443
|
+
x *= cameraScale*2; y *= -cameraScale*2;
|
|
1444
|
+
|
|
1445
|
+
if (size instanceof Vector2)
|
|
1446
|
+
size = size.length(); // use length of vector as diameter
|
|
1447
|
+
size *= cameraScale;
|
|
1448
|
+
|
|
1449
|
+
// check against screen bounds
|
|
1450
|
+
const w = mainCanvasSize.x, h = mainCanvasSize.y;
|
|
1451
|
+
return x + size > -w && x - size < w &&
|
|
1452
|
+
y + size > -h && y - size < h;
|
|
1453
|
+
}
|
|
1454
|
+
|
|
1455
|
+
/** Enable additive blending
|
|
1456
|
+
* @param {boolean} [additive]
|
|
1457
|
+
* @memberof Draw */
|
|
1458
|
+
function setAdditiveBlendMode(additive=true)
|
|
1459
|
+
{
|
|
1460
|
+
glAdditive = additive;
|
|
1461
|
+
drawContext.globalCompositeOperation = additive ? 'lighter' : 'source-over';
|
|
1462
|
+
}
|
|
1463
|
+
|
|
1464
|
+
/** Set the Shader that 2D draws use from now on, none for the engine's own
|
|
1465
|
+
* - The object render loop sets each object's own shader, so this is for draws in gameRender and gameRenderPost
|
|
1466
|
+
* @param {Shader} [shader]
|
|
1467
|
+
* @memberof Draw */
|
|
1468
|
+
function setShader(shader)
|
|
1469
|
+
{
|
|
1470
|
+
ASSERT(!shader || shader instanceof Shader, 'shader must be a Shader');
|
|
1471
|
+
glCustomShader = shader || undefined; // null is no shader too, so it batches with none
|
|
1472
|
+
}
|
|
1473
|
+
|
|
1474
|
+
/** Set an extra canvas to composite behind the engine canvases when combining
|
|
1475
|
+
* Plugins that insert their own canvas below the LittleJS canvases should set
|
|
1476
|
+
* this so it appears in screenshots and video capture
|
|
1477
|
+
* @param {HTMLCanvasElement} [canvas]
|
|
1478
|
+
* @memberof Draw */
|
|
1479
|
+
function setBackgroundCanvas(canvas) { backgroundCanvas = canvas; }
|
|
1480
|
+
|
|
1481
|
+
/** Combines LittleJS canvases onto the main canvas
|
|
1482
|
+
* This is necessary for things like screenshots and video
|
|
1483
|
+
* @memberof Draw */
|
|
1484
|
+
function combineCanvases()
|
|
1485
|
+
{
|
|
1486
|
+
// this composites raw canvases so it works in backing store pixels,
|
|
1487
|
+
// mainCanvasSize is css pixels and would throw away resolution
|
|
1488
|
+
const w = mainCanvas.width, h = mainCanvas.height;
|
|
1489
|
+
workCanvas.width = w;
|
|
1490
|
+
workCanvas.height = h;
|
|
1491
|
+
// remove background alpha — explicit fillStyle so a previous caller
|
|
1492
|
+
// leaving workContext.fillStyle transparent can't silently no-op this
|
|
1493
|
+
workContext.fillStyle = '#000';
|
|
1494
|
+
workContext.fillRect(0,0,w,h);
|
|
1495
|
+
if (backgroundCanvas)
|
|
1496
|
+
workContext.drawImage(backgroundCanvas, 0, 0, w, h);
|
|
1497
|
+
glCopyToContext(workContext);
|
|
1498
|
+
workContext.drawImage(mainCanvas, 0, 0);
|
|
1499
|
+
|
|
1500
|
+
// draw back 1:1, mainContext is scaled to css pixels
|
|
1501
|
+
mainContext.save();
|
|
1502
|
+
mainContext.setTransform(1, 0, 0, 1, 0, 0);
|
|
1503
|
+
mainContext.drawImage(workCanvas, 0, 0);
|
|
1504
|
+
mainContext.restore();
|
|
1505
|
+
}
|
|
1506
|
+
|
|
1507
|
+
// tint straight color pixels in place, rgb times color, plus additiveColor with alpha when it adds anything;
|
|
1508
|
+
// returns true when color.a went into the alpha, so it is not applied again as globalAlpha
|
|
1509
|
+
function tintImageData(data, color, additiveColor)
|
|
1510
|
+
{
|
|
1511
|
+
if (additiveColor && !isBlack(additiveColor))
|
|
1512
|
+
{
|
|
1513
|
+
// multiply + additive (slower), color.a is baked into the alpha channel here
|
|
1514
|
+
const colorMultiply = [color.r, color.g, color.b, color.a];
|
|
1515
|
+
const colorAdd = [additiveColor.r * 255, additiveColor.g * 255,
|
|
1516
|
+
additiveColor.b * 255, additiveColor.a * 255];
|
|
1517
|
+
for (let i = 0; i < data.length; ++i)
|
|
1518
|
+
data[i] = data[i] * colorMultiply[i&3] + colorAdd[i&3] |0;
|
|
1519
|
+
return true;
|
|
1520
|
+
}
|
|
1521
|
+
|
|
1522
|
+
// RGB only, faster — alpha left intact for the caller
|
|
1523
|
+
for (let i = 0; i < data.length; i+=4)
|
|
1524
|
+
{
|
|
1525
|
+
data[i ] *= color.r;
|
|
1526
|
+
data[i+1] *= color.g;
|
|
1527
|
+
data[i+2] *= color.b;
|
|
1528
|
+
}
|
|
1529
|
+
return false;
|
|
1530
|
+
}
|
|
1531
|
+
|
|
1532
|
+
// Internal: bake a color/additive-color tint into workReadCanvas at the
|
|
1533
|
+
// image's native resolution. Returns the work canvas, suitable for
|
|
1534
|
+
// passing to context.createPattern. Used by drawTextureWrapped's
|
|
1535
|
+
// Canvas2D path. Caller is responsible for short-circuiting when no
|
|
1536
|
+
// tint is needed (i.e. color is white and additiveColor is black/none).
|
|
1537
|
+
function bakeTintedImage(image, color, additiveColor)
|
|
1538
|
+
{
|
|
1539
|
+
const w = image.width|0, h = image.height|0;
|
|
1540
|
+
workReadCanvas.width = w;
|
|
1541
|
+
workReadCanvas.height = h;
|
|
1542
|
+
workReadContext.drawImage(image, 0, 0);
|
|
1543
|
+
|
|
1544
|
+
const imageData = workReadContext.getImageData(0, 0, w, h);
|
|
1545
|
+
tintImageData(imageData.data, color, additiveColor);
|
|
1546
|
+
workReadContext.putImageData(imageData, 0, 0);
|
|
1547
|
+
return workReadCanvas;
|
|
1548
|
+
}
|
|
1549
|
+
|
|
1550
|
+
/** Internal: draw an image with color and additive color applied in Canvas2D, drawTile calls it
|
|
1551
|
+
* This is slower than normal drawImage when color is applied
|
|
1552
|
+
* @ignore
|
|
1553
|
+
* @param {CanvasRenderingContext2D|OffscreenCanvasRenderingContext2D} context
|
|
1554
|
+
* @param {HTMLImageElement|HTMLCanvasElement|OffscreenCanvas|ImageBitmap} image
|
|
1555
|
+
* @param {number} sx
|
|
1556
|
+
* @param {number} sy
|
|
1557
|
+
* @param {number} sWidth
|
|
1558
|
+
* @param {number} sHeight
|
|
1559
|
+
* @param {number} dx
|
|
1560
|
+
* @param {number} dy
|
|
1561
|
+
* @param {number} dWidth
|
|
1562
|
+
* @param {number} dHeight
|
|
1563
|
+
* @param {Color} color
|
|
1564
|
+
* @param {Color} [additiveColor]
|
|
1565
|
+
* @param {number} [bleed] - How many pixels to shrink the source, used to fix bleeding
|
|
1566
|
+
* @memberof Draw */
|
|
1567
|
+
function drawImageColor(context, image, sx, sy, sWidth, sHeight, dx, dy, dWidth, dHeight, color, additiveColor, bleed=0)
|
|
1568
|
+
{
|
|
1569
|
+
const sx2 = bleed;
|
|
1570
|
+
const sy2 = bleed;
|
|
1571
|
+
sWidth = max(1,sWidth|0);
|
|
1572
|
+
sHeight = max(1,sHeight|0);
|
|
1573
|
+
const sWidth2 = sWidth - 2*bleed;
|
|
1574
|
+
const sHeight2 = sHeight - 2*bleed;
|
|
1575
|
+
if (!canvasColorTiles || isWhite(color) && (!additiveColor || isBlack(additiveColor)))
|
|
1576
|
+
{
|
|
1577
|
+
// white tint and nothing added, the texels are drawn as they are
|
|
1578
|
+
context.globalAlpha = color.a;
|
|
1579
|
+
context.drawImage(image, sx+sx2, sy+sy2, sWidth2, sHeight2, dx, dy, dWidth, dHeight);
|
|
1580
|
+
context.globalAlpha = 1;
|
|
1581
|
+
}
|
|
1582
|
+
else
|
|
1583
|
+
{
|
|
1584
|
+
// copy to offscreen canvas
|
|
1585
|
+
workReadCanvas.width = sWidth;
|
|
1586
|
+
workReadCanvas.height = sHeight;
|
|
1587
|
+
workReadContext.drawImage(image, sx|0, sy|0, sWidth, sHeight, 0, 0, sWidth, sHeight);
|
|
1588
|
+
|
|
1589
|
+
// tint image using offscreen work context
|
|
1590
|
+
const imageData = workReadContext.getImageData(0, 0, sWidth, sHeight);
|
|
1591
|
+
const alphaBaked = tintImageData(imageData.data, color, additiveColor);
|
|
1592
|
+
workReadContext.putImageData(imageData, 0, 0);
|
|
1593
|
+
if (!alphaBaked) context.globalAlpha = color.a;
|
|
1594
|
+
context.drawImage(workReadCanvas, sx2, sy2, sWidth2, sHeight2, dx, dy, dWidth, dHeight);
|
|
1595
|
+
if (!alphaBaked) context.globalAlpha = 1;
|
|
1596
|
+
}
|
|
1597
|
+
}
|
|
1598
|
+
|
|
1599
|
+
|
|
1600
|
+
/** Returns true if fullscreen mode is active
|
|
1601
|
+
* @return {boolean}
|
|
1602
|
+
* @memberof Draw */
|
|
1603
|
+
function isFullscreen() { return !!document.fullscreenElement; }
|
|
1604
|
+
|
|
1605
|
+
/** Toggle fullscreen mode
|
|
1606
|
+
* @memberof Draw */
|
|
1607
|
+
function toggleFullscreen()
|
|
1608
|
+
{
|
|
1609
|
+
const rootElement = mainCanvas.parentElement;
|
|
1610
|
+
if (isFullscreen())
|
|
1611
|
+
{
|
|
1612
|
+
if (document.exitFullscreen)
|
|
1613
|
+
document.exitFullscreen();
|
|
1614
|
+
}
|
|
1615
|
+
else if (rootElement.requestFullscreen)
|
|
1616
|
+
rootElement.requestFullscreen();
|
|
1617
|
+
}
|
|
1618
|
+
|
|
1619
|
+
/** Set the cursor style
|
|
1620
|
+
* @param {string} [cursorStyle] - CSS cursor style (auto, none, crosshair, etc)
|
|
1621
|
+
* @memberof Draw */
|
|
1622
|
+
function setCursor(cursorStyle = 'auto')
|
|
1623
|
+
{
|
|
1624
|
+
const rootElement = mainCanvas.parentElement;
|
|
1625
|
+
rootElement.style.cursor = cursorStyle;
|
|
1626
|
+
}
|
|
1627
|
+
|
|
1628
|
+
///////////////////////////////////////////////////////////////////////////////
|
|
1629
|
+
|
|
1630
|
+
/** Engine font image, 8x8 font provided by the engine
|
|
1631
|
+
* @type {ImageFont}
|
|
1632
|
+
* @memberof Draw */
|
|
1633
|
+
let engineImageFont;
|
|
1634
|
+
|
|
1635
|
+
/**
|
|
1636
|
+
* Image Font Object - Draw text by using tiles in an image
|
|
1637
|
+
* - 96 characters (from space to tilde) are stored in an image
|
|
1638
|
+
* - A 8x8 default engine font is supplied for general use
|
|
1639
|
+
* - This system is WebGL enabled for fast text rendering
|
|
1640
|
+
* - Fonts can also be colored and scaled along each axis
|
|
1641
|
+
*
|
|
1642
|
+
* @memberof Draw
|
|
1643
|
+
* @example
|
|
1644
|
+
* // use built in font
|
|
1645
|
+
* const font = engineImageFont;
|
|
1646
|
+
*
|
|
1647
|
+
* // draw text
|
|
1648
|
+
* font.drawTextScreen('LittleJS\nHello World!', vec2(200, 50), 16);
|
|
1649
|
+
*/
|
|
1650
|
+
class ImageFont
|
|
1651
|
+
{
|
|
1652
|
+
/** Create an image font
|
|
1653
|
+
* @param {TileInfo} tileInfo - Tile info of first character in font
|
|
1654
|
+
*/
|
|
1655
|
+
constructor(tileInfo)
|
|
1656
|
+
{
|
|
1657
|
+
ASSERT(!!tileInfo, 'tileInfo is required for ImageFont');
|
|
1658
|
+
|
|
1659
|
+
/** @property {TileInfo} - Tile info for the font */
|
|
1660
|
+
this.tileInfo = tileInfo; // kept, not copied, so a loadSprite tile filled in once it loads is seen
|
|
1661
|
+
}
|
|
1662
|
+
|
|
1663
|
+
/** Draw text in world space using the image font
|
|
1664
|
+
* - The text stays upright and ignores cameraAngle, each glyph is snapped to whole screen pixels to keep it crisp
|
|
1665
|
+
* @param {string|number} text
|
|
1666
|
+
* @param {Vector2} pos
|
|
1667
|
+
* @param {Vector2|number} [size]
|
|
1668
|
+
* @param {boolean} [center=true] - center each line on pos, and the lines of multi-line text around it
|
|
1669
|
+
* @param {Color} [color=WHITE]
|
|
1670
|
+
* @param {boolean} [useWebGL=glEnable]
|
|
1671
|
+
* @param {CanvasRenderingContext2D|OffscreenCanvasRenderingContext2D} [context]
|
|
1672
|
+
*/
|
|
1673
|
+
drawText(text, pos, size=1, center, color, useWebGL, context)
|
|
1674
|
+
{
|
|
1675
|
+
ASSERT(isVector2(size) || typeof size === 'number', 'size must be a vec2 or number');
|
|
1676
|
+
|
|
1677
|
+
if (typeof size === 'number')
|
|
1678
|
+
{
|
|
1679
|
+
// if size is a number, make it a vector
|
|
1680
|
+
ASSERT(size > 0);
|
|
1681
|
+
size *= cameraScale;
|
|
1682
|
+
size = new Vector2(size, size);
|
|
1683
|
+
}
|
|
1684
|
+
else
|
|
1685
|
+
size = size.scale(cameraScale);
|
|
1686
|
+
// world text, only drawn through screen space, so it still casts in a pass that skips screen space draws
|
|
1687
|
+
const skip = glSkipScreenSpace;
|
|
1688
|
+
glSkipScreenSpace = false;
|
|
1689
|
+
this.drawTextScreen(text, worldToScreen(pos), size, center, color, useWebGL, context);
|
|
1690
|
+
glSkipScreenSpace = skip;
|
|
1691
|
+
}
|
|
1692
|
+
|
|
1693
|
+
/** Draw text in screen space using the image font
|
|
1694
|
+
* @param {string|number} text
|
|
1695
|
+
* @param {Vector2} pos
|
|
1696
|
+
* @param {Vector2|number} size
|
|
1697
|
+
* @param {boolean} [center] - center each line on pos, and the lines of multi-line text around it
|
|
1698
|
+
* @param {Color} [color=WHITE]
|
|
1699
|
+
* @param {boolean} [useWebGL=glEnable]
|
|
1700
|
+
* @param {CanvasRenderingContext2D|OffscreenCanvasRenderingContext2D} [context]
|
|
1701
|
+
*/
|
|
1702
|
+
drawTextScreen(text, pos, size, center=true, color=WHITE, useWebGL=glEnable, context)
|
|
1703
|
+
{
|
|
1704
|
+
ASSERT(isStringLike(text), 'text must be a string');
|
|
1705
|
+
ASSERT(isVector2(pos), 'pos must be a vec2');
|
|
1706
|
+
ASSERT(isVector2(size) || typeof size === 'number', 'size must be a vec2 or number');
|
|
1707
|
+
ASSERT(typeof center === 'boolean', 'center must be a boolean, the color comes after it, unlike drawText');
|
|
1708
|
+
ASSERT(isColor(color), 'color must be a color');
|
|
1709
|
+
|
|
1710
|
+
// if size is a number, make it a vector
|
|
1711
|
+
const glyphSize = typeof size === 'number' ? new Vector2(size, size) : size;
|
|
1712
|
+
|
|
1713
|
+
// precache objects for drawing, a copy of the tile info each glyph moves, the font's own stays put
|
|
1714
|
+
const drawPos = new Vector2;
|
|
1715
|
+
const tileInfo = this.tileInfo.frame(0);
|
|
1716
|
+
|
|
1717
|
+
// draw each line of text, centered vertically like drawTextScreen when center is set
|
|
1718
|
+
const lines = (text+'').split('\n');
|
|
1719
|
+
const centerOffsetY = center ? (lines.length-1) * glyphSize.y / 2 : 0;
|
|
1720
|
+
lines.forEach((line, j)=>
|
|
1721
|
+
{
|
|
1722
|
+
const centerOffset = center ? (line.length-1) * glyphSize.x / 2 : 0;
|
|
1723
|
+
for (let i=line.length; i--;)
|
|
1724
|
+
{
|
|
1725
|
+
// get the glyph, out of range characters use the last one
|
|
1726
|
+
const charCode = line.charCodeAt(i);
|
|
1727
|
+
this.getGlyphPos(charCode < 32 || charCode > 127 ? 95 : charCode - 32, tileInfo.pos);
|
|
1728
|
+
|
|
1729
|
+
// snap the glyph edges to whole pixels
|
|
1730
|
+
// tiles are drawn from their center, so snapping the center
|
|
1731
|
+
// to a whole pixel puts the edges on half pixels when the
|
|
1732
|
+
// size is even, and a row or column of the glyph then has
|
|
1733
|
+
// no pixel center inside it and is not rasterized at all
|
|
1734
|
+
// ceil picks the nearest aligned position, breaking ties
|
|
1735
|
+
// downward to match how this used to truncate
|
|
1736
|
+
drawPos.x = ceil(pos.x + i * glyphSize.x - centerOffset - glyphSize.x/2) + glyphSize.x/2 - .5;
|
|
1737
|
+
drawPos.y = ceil(pos.y + j * glyphSize.y - centerOffsetY - glyphSize.y/2) + glyphSize.y/2 - .5;
|
|
1738
|
+
drawTile(drawPos, glyphSize, tileInfo, color, 0, false, undefined, useWebGL, true, context);
|
|
1739
|
+
}
|
|
1740
|
+
});
|
|
1741
|
+
}
|
|
1742
|
+
|
|
1743
|
+
/** Get where a glyph sits in the texture: counted in the font's own columns when its tile has them, like a font
|
|
1744
|
+
* packed by loadSprite, otherwise along the texture's grid from the font's first tile, the way tile() lays it out
|
|
1745
|
+
* @param {number} index - Glyph number, 0 is the space and the characters follow in ASCII order
|
|
1746
|
+
* @param {Vector2} [pos] - Written into and returned, for a loop that places many
|
|
1747
|
+
* @return {Vector2} */
|
|
1748
|
+
getGlyphPos(index, pos=new Vector2)
|
|
1749
|
+
{
|
|
1750
|
+
const t = this.tileInfo, padding = t.padding;
|
|
1751
|
+
const w = t.size.x + padding*2, h = t.size.y + padding*2;
|
|
1752
|
+
if (t.columns)
|
|
1753
|
+
return pos.set(t.pos.x + index % t.columns * w, t.pos.y + (index / t.columns |0) * h);
|
|
1754
|
+
const columns = t.textureInfo.size.x / w |0;
|
|
1755
|
+
const glyph = ((t.pos.y - padding) / h |0) * columns + ((t.pos.x - padding) / w |0) + index;
|
|
1756
|
+
return pos.set(glyph % columns * w + padding, (glyph / columns |0) * h + padding);
|
|
1757
|
+
}
|
|
1758
|
+
}
|
|
1759
|
+
|
|
1760
|
+
// load engine font, called automatically on startup
|
|
1761
|
+
async function imageFontInit()
|
|
1762
|
+
{
|
|
1763
|
+
const image = new Image;
|
|
1764
|
+
await new Promise(resolve =>
|
|
1765
|
+
{
|
|
1766
|
+
image.onerror = image.onload = resolve;
|
|
1767
|
+
image.crossOrigin = 'anonymous';
|
|
1768
|
+
image.src = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAUAAAAAeAQMAAABnrVXaAAAABlBMVEUAAAD///+l2Z/dAAAAAXRSTlMAQObYZgAAAjpJREFUOMu9kzFu2zAUhn+CAROgqrk+B2l0BWYxMjlXeYaAtFtbdA1sGgHqRQfI0CNkSG5AwYB0BQ8d5Bsomwah6CPVeGg6tEPzAxLwyI+P78cP4u9lNO9OoMKnLMOobG5020/yaj/MrRcCGh1gBbyiLTPJEYaIiom5KM9Jq7KgynMGtb6L4GL4MF2H4LQKCXTvDVw2I4MsgZT7QLExdiutH+D08VOP3INXRrWX1/mmpbkNgAPYRVANb4xpcegYvhiNbIXauQICEjBuYLfMakaakWQeXxiZ0VDtuJCKs3ztMV59QtsHJNcRxDzfdL21ty3PrfIcXTN+E+GFAv6T5nbT9jd50/WFxb5ksdAv49qS6ouymG66ji08UMT6moykYLAo+V0j23GN4m829ZySAD5K7QsBfQTvOG8eE+gTeGYRAmnNAubN3hf5Zv9tJWDHp/VTuaSm7SN4fyINQqaNO3RMVxvpSPXnOChnRNvFcGY0gnwiPswYwTKVPE0zVtX3mTEIOoFzaqLrGuJaV+Uqumb71fVk/VoOH3cdLNQP/FHi8hV0CQNoqBZsUPlLPMsdCJro9QAaQQ0woDy9BJm0eTxCFnO9srcYlhNVlfR2EyTrph1uUtbUtAJifwRgrKuYdXVHeb0YI3QpawohQHkloI3J5FuVwI5ORxC9k2Tuz9Ir1IjgeIPGMHYkAZe2RuYkmWFmt3gGbTPOmBUWVTmRmHtGrfpzG/yuQNOKa6gBB/WA9khitPgl6/GP+gl2Af6tCbvaygAAAABJRU5ErkJggg==';
|
|
1769
|
+
});
|
|
1770
|
+
|
|
1771
|
+
const tilePos=vec2(), tileSize=vec2(8), padding=1, bleed=0;
|
|
1772
|
+
const textureInfo = new TextureInfo(image);
|
|
1773
|
+
const tileInfo = new TileInfo(tilePos, tileSize, textureInfo, padding, bleed);
|
|
1774
|
+
engineImageFont = new ImageFont(tileInfo);
|
|
1511
1775
|
}
|