littlejsengine 1.19.3 → 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 -820
- package/README.md +29 -25
- package/dist/littlejs.d.ts +2455 -935
- package/dist/littlejs.esm.js +30344 -21110
- package/dist/littlejs.esm.min.js +4 -3
- package/dist/littlejs.js +30259 -21052
- package/dist/littlejs.min.js +2 -2
- package/dist/littlejs.release.js +26567 -20455
- package/package.json +10 -5
- package/plugins/audioEffects.js +28 -24
- 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 -332
- package/plugins/math3d.js +58 -17
- package/plugins/medalSystem.js +326 -280
- package/plugins/newgrounds.js +400 -199
- package/plugins/pathFinder.js +208 -175
- package/plugins/pluginExport.js +15 -0
- package/plugins/postProcess.js +56 -13
- package/plugins/render3d.js +1643 -1577
- package/plugins/render3dDebug.js +109 -0
- package/plugins/render3dExtras.js +1754 -0
- package/plugins/render3dVoxels.js +502 -0
- package/plugins/textureSheet.js +496 -458
- package/plugins/threejs.js +183 -163
- package/plugins/tweakables.js +411 -0
- package/plugins/tweenSystem.js +263 -145
- package/plugins/uiSystem.js +2024 -1687
- package/plugins/zzfxm.js +159 -159
- package/src/engine.js +191 -75
- package/src/engineAudio.js +229 -125
- package/src/engineBuild.mjs +289 -256
- package/src/engineDebug.js +912 -742
- package/src/engineDraw.js +295 -193
- package/src/engineEditor.js +2431 -0
- package/src/engineExport.js +12 -0
- package/src/engineInput.js +1462 -1293
- package/src/engineMath.js +1274 -1252
- package/src/engineObject.js +194 -80
- package/src/engineParticles.js +583 -532
- package/src/engineRelease.js +67 -45
- package/src/engineSettings.js +77 -29
- package/src/engineTileLayer.js +995 -741
- package/src/engineUtilities.js +19 -13
- package/src/engineWebGL.js +239 -82
- 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
|
@@ -81,19 +81,23 @@ let textureInfos = [];
|
|
|
81
81
|
/** Keeps track of how many draw calls there were each frame for debugging
|
|
82
82
|
* @type {number}
|
|
83
83
|
* @memberof Draw */
|
|
84
|
-
let drawCount;
|
|
84
|
+
let drawCount = 0;
|
|
85
85
|
|
|
86
86
|
/** Keeps track of how many primitives were drawn each frame for debugging
|
|
87
87
|
* A single draw call can render many primitives (e.g. a WebGL sprite batch).
|
|
88
88
|
* @type {number}
|
|
89
89
|
* @memberof Draw */
|
|
90
|
-
let primitiveCount;
|
|
90
|
+
let primitiveCount = 0;
|
|
91
91
|
|
|
92
92
|
// internal predicates for tint short-circuiting in canvas2D draw paths
|
|
93
93
|
// isWhite ignores alpha because alpha is applied via globalAlpha, not multiply
|
|
94
94
|
// isBlack includes alpha so additive colors that only contribute alpha are not skipped
|
|
95
|
-
/** @
|
|
96
|
-
|
|
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; }
|
|
97
101
|
|
|
98
102
|
///////////////////////////////////////////////////////////////////////////////
|
|
99
103
|
|
|
@@ -120,8 +124,6 @@ function tile(index=0, size=tileDefaultSize, texture=0, padding=tileDefaultPaddi
|
|
|
120
124
|
ASSERT(isNumber(texture) || texture instanceof TextureInfo, 'texture must be a number or TextureInfo');
|
|
121
125
|
ASSERT(isNumber(padding), 'padding must be a number');
|
|
122
126
|
|
|
123
|
-
if (headlessMode) return new TileInfo;
|
|
124
|
-
|
|
125
127
|
if (typeof size === 'number')
|
|
126
128
|
{
|
|
127
129
|
// if size is a number, make it a vector
|
|
@@ -132,6 +134,8 @@ function tile(index=0, size=tileDefaultSize, texture=0, padding=tileDefaultPaddi
|
|
|
132
134
|
// create tile info object
|
|
133
135
|
const textureInfo = typeof texture === 'number' ?
|
|
134
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
|
|
135
139
|
ASSERT(textureInfo instanceof TextureInfo, 'tile texture is not loaded');
|
|
136
140
|
ASSERT(textureInfo.size.x > 0, 'tile texture is not loaded');
|
|
137
141
|
|
|
@@ -203,8 +207,8 @@ class TileInfo
|
|
|
203
207
|
const h = this.size.y + this.padding*2;
|
|
204
208
|
const x = (this.columns ? frame % this.columns : frame) * w;
|
|
205
209
|
const y = (this.columns ? frame / this.columns | 0 : 0) * h;
|
|
206
|
-
ASSERT(this.pos.x + x + this.size.x <= this.textureInfo.size.x, 'frame extends beyond texture width!');
|
|
207
|
-
ASSERT(this.pos.y + y + this.size.y <= this.textureInfo.size.y, 'frame extends beyond texture height!');
|
|
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!');
|
|
208
212
|
return this.offset(new Vector2(x, y));
|
|
209
213
|
}
|
|
210
214
|
|
|
@@ -220,7 +224,8 @@ class TileInfo
|
|
|
220
224
|
}
|
|
221
225
|
|
|
222
226
|
/**
|
|
223
|
-
* Returns a tile info for an index using this tile as reference
|
|
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()
|
|
224
229
|
* @param {Vector2|number} [index=0]
|
|
225
230
|
* @return {TileInfo}
|
|
226
231
|
*/
|
|
@@ -243,20 +248,21 @@ class TileInfo
|
|
|
243
248
|
}
|
|
244
249
|
|
|
245
250
|
/**
|
|
246
|
-
*
|
|
251
|
+
* Texture Info - Stores info about each texture
|
|
247
252
|
* @memberof Draw
|
|
248
253
|
*/
|
|
249
254
|
class TextureInfo
|
|
250
255
|
{
|
|
251
256
|
/**
|
|
252
257
|
* Create a TextureInfo, called automatically by the engine
|
|
253
|
-
* @param {HTMLImageElement|OffscreenCanvas} image
|
|
258
|
+
* @param {HTMLImageElement|HTMLCanvasElement|OffscreenCanvas|ImageBitmap} image
|
|
254
259
|
* @param {boolean} [useWebGL] - Should use WebGL if available?
|
|
255
|
-
* @param {boolean} [wrap] - Should the texture wrap (REPEAT) or clamp (CLAMP_TO_EDGE)?
|
|
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
|
|
256
262
|
*/
|
|
257
263
|
constructor(image, useWebGL=true, wrap=false)
|
|
258
264
|
{
|
|
259
|
-
/** @property {HTMLImageElement|OffscreenCanvas} - image source */
|
|
265
|
+
/** @property {HTMLImageElement|HTMLCanvasElement|OffscreenCanvas|ImageBitmap} - image source */
|
|
260
266
|
this.image = image;
|
|
261
267
|
/** @property {Vector2} - size of the image */
|
|
262
268
|
this.size = image ? vec2(image.width, image.height) : vec2();
|
|
@@ -265,13 +271,24 @@ class TextureInfo
|
|
|
265
271
|
/** @property {WebGLTexture|undefined} - WebGL texture
|
|
266
272
|
* @type {WebGLTexture|undefined} */
|
|
267
273
|
this.glTexture = undefined;
|
|
268
|
-
/** @property {boolean} - true for REPEAT wrap mode, false for CLAMP_TO_EDGE
|
|
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>} */
|
|
269
277
|
this.wrap = wrap;
|
|
270
278
|
useWebGL && this.createWebGLTexture();
|
|
271
279
|
}
|
|
272
280
|
|
|
273
281
|
/** Creates the WebGL texture, updates if already created */
|
|
274
|
-
createWebGLTexture()
|
|
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
|
+
}
|
|
275
292
|
|
|
276
293
|
/** Destroys the WebGL texture */
|
|
277
294
|
destroyWebGLTexture() { glUnregisterTextureInfo(this); }
|
|
@@ -281,7 +298,8 @@ class TextureInfo
|
|
|
281
298
|
hasWebGL() { return !!this.glTexture; }
|
|
282
299
|
|
|
283
300
|
/** Set the wrap mode for this texture
|
|
284
|
-
* @param {boolean} [wrap] - true for REPEAT, false for CLAMP_TO_EDGE
|
|
301
|
+
* @param {boolean|Array<number>} [wrap] - true for REPEAT, false for CLAMP_TO_EDGE, or the WebGL modes across
|
|
302
|
+
* and down */
|
|
285
303
|
setWrap(wrap=true)
|
|
286
304
|
{
|
|
287
305
|
this.wrap = wrap;
|
|
@@ -333,7 +351,7 @@ class SpriteAnimation
|
|
|
333
351
|
* @return {SpriteAnimation} */
|
|
334
352
|
loop() { return this.restart('loop'); }
|
|
335
353
|
|
|
336
|
-
/** Start over from the first frame, run through once and hold the last frame
|
|
354
|
+
/** Start over from the first frame, run through once and hold the last frame, last to first at a negative speed
|
|
337
355
|
* @return {SpriteAnimation} */
|
|
338
356
|
play() { return this.restart('once'); }
|
|
339
357
|
|
|
@@ -367,11 +385,11 @@ class SpriteAnimation
|
|
|
367
385
|
if (this.heldFrame !== undefined)
|
|
368
386
|
return this.heldFrame;
|
|
369
387
|
const n = this.frameCount, f = floor(this.elapsedFrames);
|
|
370
|
-
if (this.mode == 'once')
|
|
371
|
-
return
|
|
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);
|
|
372
390
|
if (this.mode == 'loop')
|
|
373
|
-
return f
|
|
374
|
-
const period = max(2 * n - 2, 1), k = f
|
|
391
|
+
return mod(f, n);
|
|
392
|
+
const period = max(2 * n - 2, 1), k = mod(f, period); // there and back, the ends once each
|
|
375
393
|
return k < n ? k : period - k;
|
|
376
394
|
}
|
|
377
395
|
|
|
@@ -381,7 +399,7 @@ class SpriteAnimation
|
|
|
381
399
|
|
|
382
400
|
/** True once a play has shown its last frame for its time
|
|
383
401
|
* @return {boolean} */
|
|
384
|
-
get isDone() { return this.mode == 'once' && this.heldFrame === undefined && this.elapsedFrames >= this.frameCount; }
|
|
402
|
+
get isDone() { return this.mode == 'once' && this.heldFrame === undefined && abs(this.elapsedFrames) >= this.frameCount; }
|
|
385
403
|
}
|
|
386
404
|
|
|
387
405
|
///////////////////////////////////////////////////////////////////////////////
|
|
@@ -393,18 +411,26 @@ class SpriteAnimation
|
|
|
393
411
|
* - Set it as obj.shader, or use setShader for 2D draws and render3D.shader for 3D draws
|
|
394
412
|
* - Draws that share a Shader share a batch; with no Shader set nothing changes
|
|
395
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;`
|
|
396
418
|
* - Compiled once per renderer by the first draw that needs it; a bad snippet throws with the GLSL log in debug
|
|
397
419
|
* - Make each Shader once, at init, and share it; every one made lives for the session with its programs
|
|
398
|
-
* - Names in both renderers: iChannel0 the texture, iTime, iResolution, and localUV, 0 to 1
|
|
399
|
-
* or the mesh's own uv
|
|
400
|
-
* - Names in 3D only: worldPos, worldNormal, cameraPos, sunDirection, sunColor, ambientColor,
|
|
401
|
-
* lights[i], lightColors[i] and shadow()
|
|
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
|
|
402
426
|
* @example
|
|
403
427
|
* const fade = new Shader(`
|
|
404
428
|
* void mainImage(out vec4 c, vec2 uv)
|
|
405
429
|
* {
|
|
406
430
|
* c = texture(iChannel0, uv);
|
|
407
|
-
*
|
|
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
|
|
408
434
|
* }`);
|
|
409
435
|
* obj.shader = fade;
|
|
410
436
|
* @memberof Draw
|
|
@@ -440,11 +466,11 @@ class Shader
|
|
|
440
466
|
* @param {boolean} [mirror] - Is image flipped along the Y axis?
|
|
441
467
|
* @param {Color} [additiveColor] - Additive color to be applied if any
|
|
442
468
|
* @param {boolean} [useWebGL=glEnable] - Use accelerated WebGL rendering?
|
|
443
|
-
* @param {boolean} [screenSpace=
|
|
469
|
+
* @param {boolean} [screenSpace=drawScreenSpace] - Are the pos and size are in screen space?
|
|
444
470
|
* @param {CanvasRenderingContext2D|OffscreenCanvasRenderingContext2D} [context] - Canvas 2D context to draw to
|
|
445
471
|
* @memberof Draw */
|
|
446
472
|
function drawTile(pos, size=vec2(1), tileInfo, color=WHITE,
|
|
447
|
-
angle=0, mirror, additiveColor, useWebGL=glEnable, screenSpace=
|
|
473
|
+
angle=0, mirror, additiveColor, useWebGL=glEnable, screenSpace=drawScreenSpace, context)
|
|
448
474
|
{
|
|
449
475
|
ASSERT(isVector2(pos), 'pos must be a vec2');
|
|
450
476
|
ASSERT(isVector2(size), 'size must be a vec2');
|
|
@@ -453,15 +479,23 @@ function drawTile(pos, size=vec2(1), tileInfo, color=WHITE,
|
|
|
453
479
|
ASSERT(!additiveColor || isColor(additiveColor), 'additiveColor must be a color');
|
|
454
480
|
ASSERT(!context || !useWebGL, 'context only supported in canvas 2D mode');
|
|
455
481
|
|
|
482
|
+
if (headlessMode && !context) return; // headless has no canvas, only a context passed in is drawn to
|
|
483
|
+
|
|
456
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
|
|
457
487
|
const bleed = tileInfo?.bleed ?? 0;
|
|
458
488
|
if (useWebGL && glEnable)
|
|
459
489
|
{
|
|
460
490
|
ASSERT(!!glContext, 'WebGL is not enabled!');
|
|
461
491
|
if (screenSpace)
|
|
492
|
+
{
|
|
493
|
+
if (glSkipScreenSpace) return;
|
|
462
494
|
[pos, size, angle] = screenToWorldTransform(pos, size, angle);
|
|
495
|
+
}
|
|
463
496
|
if (textureInfo)
|
|
464
497
|
{
|
|
498
|
+
ASSERT(!!textureInfo.glTexture, 'texture has no WebGL texture, draw it with useWebGL false');
|
|
465
499
|
// calculate uvs and render
|
|
466
500
|
const sizeInverse = textureInfo.sizeInverse;
|
|
467
501
|
const x = tileInfo.pos.x * sizeInverse.x;
|
|
@@ -487,8 +521,7 @@ function drawTile(pos, size=vec2(1), tileInfo, color=WHITE,
|
|
|
487
521
|
}
|
|
488
522
|
else
|
|
489
523
|
{
|
|
490
|
-
// untextured:
|
|
491
|
-
// color.add(additiveColor) on line ~337.
|
|
524
|
+
// untextured: color plus additive in one color, as the Canvas2D path below does
|
|
492
525
|
const combined = additiveColor ? color.add(additiveColor) : color;
|
|
493
526
|
glDrawUntextured(pos.x, pos.y, size.x, size.y, angle, combined.rgbaInt());
|
|
494
527
|
}
|
|
@@ -526,7 +559,7 @@ function drawTile(pos, size=vec2(1), tileInfo, color=WHITE,
|
|
|
526
559
|
* @param {Color} [color=WHITE]
|
|
527
560
|
* @param {number} [angle]
|
|
528
561
|
* @param {boolean} [useWebGL=glEnable]
|
|
529
|
-
* @param {boolean} [screenSpace]
|
|
562
|
+
* @param {boolean} [screenSpace=drawScreenSpace]
|
|
530
563
|
* @param {CanvasRenderingContext2D|OffscreenCanvasRenderingContext2D} [context]
|
|
531
564
|
* @memberof Draw */
|
|
532
565
|
function drawRect(pos, size, color, angle, useWebGL, screenSpace, context)
|
|
@@ -541,10 +574,10 @@ function drawRect(pos, size, color, angle, useWebGL, screenSpace, context)
|
|
|
541
574
|
* @param {Color} [colorBottom=CLEAR_WHITE]
|
|
542
575
|
* @param {number} [angle]
|
|
543
576
|
* @param {boolean} [useWebGL=glEnable]
|
|
544
|
-
* @param {boolean} [screenSpace]
|
|
577
|
+
* @param {boolean} [screenSpace=drawScreenSpace]
|
|
545
578
|
* @param {CanvasRenderingContext2D|OffscreenCanvasRenderingContext2D} [context]
|
|
546
579
|
* @memberof Draw */
|
|
547
|
-
function drawRectGradient(pos, size, colorTop=WHITE, colorBottom=CLEAR_WHITE, angle=0, useWebGL=glEnable, screenSpace=
|
|
580
|
+
function drawRectGradient(pos, size, colorTop=WHITE, colorBottom=CLEAR_WHITE, angle=0, useWebGL=glEnable, screenSpace=drawScreenSpace, context)
|
|
548
581
|
{
|
|
549
582
|
ASSERT(isVector2(pos), 'pos must be a vec2');
|
|
550
583
|
ASSERT(isVector2(size), 'size must be a vec2');
|
|
@@ -552,15 +585,15 @@ function drawRectGradient(pos, size, colorTop=WHITE, colorBottom=CLEAR_WHITE, an
|
|
|
552
585
|
ASSERT(isNumber(angle), 'angle must be a number');
|
|
553
586
|
ASSERT(!context || !useWebGL, 'context only supported in canvas 2D mode');
|
|
554
587
|
|
|
588
|
+
if (headlessMode && !context) return; // headless has no canvas, only a context passed in is drawn to
|
|
589
|
+
|
|
555
590
|
if (useWebGL && glEnable)
|
|
556
591
|
{
|
|
557
592
|
ASSERT(!!glContext, 'WebGL is not enabled!');
|
|
558
593
|
if (screenSpace)
|
|
559
594
|
{
|
|
560
|
-
|
|
561
|
-
pos =
|
|
562
|
-
size = size.scale(1/cameraScale);
|
|
563
|
-
angle += cameraAngle;
|
|
595
|
+
if (glSkipScreenSpace) return;
|
|
596
|
+
[pos, size, angle] = screenToWorldTransform(pos, size, angle);
|
|
564
597
|
}
|
|
565
598
|
// build 4 corner points for the rectangle
|
|
566
599
|
const points = [], colors = [];
|
|
@@ -608,11 +641,11 @@ function drawRectGradient(pos, size, colorTop=WHITE, colorBottom=CLEAR_WHITE, an
|
|
|
608
641
|
* @param {number} [angle=0] - Angle to rotate by
|
|
609
642
|
* @param {Color} [additiveColor] - Additive color to be applied if any
|
|
610
643
|
* @param {boolean} [useWebGL=glEnable] - Use accelerated WebGL rendering?
|
|
611
|
-
* @param {boolean} [screenSpace=
|
|
644
|
+
* @param {boolean} [screenSpace=drawScreenSpace] - Are pos and size in screen space?
|
|
612
645
|
* @param {CanvasRenderingContext2D|OffscreenCanvasRenderingContext2D} [context] - Canvas 2D context to draw to
|
|
613
646
|
* @memberof Draw */
|
|
614
647
|
function drawTextureWrapped(pos, size, wrapCount, texture=0, color=WHITE,
|
|
615
|
-
angle=0, additiveColor, useWebGL=glEnable, screenSpace=
|
|
648
|
+
angle=0, additiveColor, useWebGL=glEnable, screenSpace=drawScreenSpace, context)
|
|
616
649
|
{
|
|
617
650
|
ASSERT(isVector2(pos), 'pos must be a vec2');
|
|
618
651
|
ASSERT(isVector2(size), 'size must be a vec2');
|
|
@@ -624,8 +657,8 @@ function drawTextureWrapped(pos, size, wrapCount, texture=0, color=WHITE,
|
|
|
624
657
|
ASSERT(!(texture instanceof TileInfo),
|
|
625
658
|
'pass a TextureInfo or texture index, not a TileInfo — use tileInfo.textureInfo');
|
|
626
659
|
|
|
627
|
-
//
|
|
628
|
-
if (headlessMode) return;
|
|
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;
|
|
629
662
|
|
|
630
663
|
// resolve texture argument: TextureInfo or index
|
|
631
664
|
const textureInfo = typeof texture === 'number' ? textureInfos[texture] : texture;
|
|
@@ -638,7 +671,10 @@ function drawTextureWrapped(pos, size, wrapCount, texture=0, color=WHITE,
|
|
|
638
671
|
{
|
|
639
672
|
ASSERT(!!glContext, 'WebGL is not enabled!');
|
|
640
673
|
if (screenSpace)
|
|
674
|
+
{
|
|
675
|
+
if (glSkipScreenSpace) return;
|
|
641
676
|
[pos, size, angle] = screenToWorldTransform(pos, size, angle);
|
|
677
|
+
}
|
|
642
678
|
glSetTexture(textureInfo.glTexture);
|
|
643
679
|
glDraw(pos.x, pos.y, size.x, size.y, angle,
|
|
644
680
|
0, 0, wrapCount.x, wrapCount.y,
|
|
@@ -660,9 +696,7 @@ function drawTextureWrapped(pos, size, wrapCount, texture=0, color=WHITE,
|
|
|
660
696
|
// pick image source: raw, or tinted bake. Match drawImageColor's
|
|
661
697
|
// "no tint needed" predicate so behavior stays consistent.
|
|
662
698
|
const noTint = !canvasColorTiles ||
|
|
663
|
-
(additiveColor
|
|
664
|
-
? isWhite(color.add(additiveColor)) && additiveColor.a <= 0
|
|
665
|
-
: isWhite(color));
|
|
699
|
+
isWhite(color) && (!additiveColor || isBlack(additiveColor));
|
|
666
700
|
// alpha is baked into pixels by bakeTintedImage's additive branch;
|
|
667
701
|
// in that case globalAlpha must NOT also apply color.a
|
|
668
702
|
const alphaBaked = !noTint && additiveColor && !isBlack(additiveColor);
|
|
@@ -698,10 +732,10 @@ function drawTextureWrapped(pos, size, wrapCount, texture=0, color=WHITE,
|
|
|
698
732
|
* @param {Vector2} [pos=vec2()] - Offset to apply
|
|
699
733
|
* @param {number} [angle] - Angle to rotate by
|
|
700
734
|
* @param {boolean} [useWebGL=glEnable]
|
|
701
|
-
* @param {boolean} [screenSpace]
|
|
735
|
+
* @param {boolean} [screenSpace=drawScreenSpace]
|
|
702
736
|
* @param {CanvasRenderingContext2D|OffscreenCanvasRenderingContext2D} [context]
|
|
703
737
|
* @memberof Draw */
|
|
704
|
-
function drawLineList(points, width=.1, color=WHITE, wrap=false, pos=vec2(), angle=0, useWebGL=glEnable, screenSpace=
|
|
738
|
+
function drawLineList(points, width=.1, color=WHITE, wrap=false, pos=vec2(), angle=0, useWebGL=glEnable, screenSpace=drawScreenSpace, context)
|
|
705
739
|
{
|
|
706
740
|
ASSERT(isArray(points), 'points must be an array');
|
|
707
741
|
ASSERT(isNumber(width), 'width must be a number');
|
|
@@ -710,12 +744,17 @@ function drawLineList(points, width=.1, color=WHITE, wrap=false, pos=vec2(), ang
|
|
|
710
744
|
ASSERT(isNumber(angle), 'angle must be a number');
|
|
711
745
|
ASSERT(!context || !useWebGL, 'context only supported in canvas 2D mode');
|
|
712
746
|
|
|
747
|
+
if (headlessMode && !context) return; // headless has no canvas, only a context passed in is drawn to
|
|
748
|
+
|
|
713
749
|
if (useWebGL && glEnable)
|
|
714
750
|
{
|
|
715
751
|
ASSERT(!!glContext, 'WebGL is not enabled!');
|
|
716
752
|
let size = vec2(1);
|
|
717
753
|
if (screenSpace)
|
|
754
|
+
{
|
|
755
|
+
if (glSkipScreenSpace) return;
|
|
718
756
|
[pos, size, angle] = screenToWorldTransform(pos, size, angle);
|
|
757
|
+
}
|
|
719
758
|
glDrawOutlineTransform(points, color.rgbaInt(), width, pos.x, pos.y, size.x, size.y, angle, wrap);
|
|
720
759
|
}
|
|
721
760
|
else
|
|
@@ -725,8 +764,11 @@ function drawLineList(points, width=.1, color=WHITE, wrap=false, pos=vec2(), ang
|
|
|
725
764
|
++primitiveCount;
|
|
726
765
|
drawCanvas2D(pos, vec2(1), angle, false, (context)=>
|
|
727
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;
|
|
728
770
|
context.strokeStyle = color.toString();
|
|
729
|
-
context.lineWidth = width;
|
|
771
|
+
context.lineWidth = abs(width);
|
|
730
772
|
context.beginPath();
|
|
731
773
|
for (let i=0; i<points.length; ++i)
|
|
732
774
|
{
|
|
@@ -747,14 +789,15 @@ function drawLineList(points, width=.1, color=WHITE, wrap=false, pos=vec2(), ang
|
|
|
747
789
|
* @param {Vector2} [pos=vec2()] - Offset to apply
|
|
748
790
|
* @param {number} [angle] - Angle to rotate by
|
|
749
791
|
* @param {boolean} [useWebGL=glEnable]
|
|
750
|
-
* @param {boolean} [screenSpace]
|
|
792
|
+
* @param {boolean} [screenSpace=drawScreenSpace]
|
|
751
793
|
* @param {CanvasRenderingContext2D|OffscreenCanvasRenderingContext2D} [context]
|
|
752
794
|
* @memberof Draw */
|
|
753
|
-
function drawLine(posA, posB, width=.1, color=WHITE, pos=vec2(), angle=0, useWebGL=glEnable, screenSpace=
|
|
795
|
+
function drawLine(posA, posB, width=.1, color=WHITE, pos=vec2(), angle=0, useWebGL=glEnable, screenSpace=drawScreenSpace, context)
|
|
754
796
|
{
|
|
755
797
|
const halfDelta = vec2((posB.x - posA.x)/2, (posB.y - posA.y)/2);
|
|
756
798
|
const size = vec2(width, halfDelta.length()*2);
|
|
757
|
-
|
|
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
|
|
758
801
|
if (screenSpace)
|
|
759
802
|
halfDelta.y *= -1; // flip angle Y if screen space
|
|
760
803
|
angle += halfDelta.angle();
|
|
@@ -770,10 +813,10 @@ function drawLine(posA, posB, width=.1, color=WHITE, pos=vec2(), angle=0, useWeb
|
|
|
770
813
|
* @param {Color} [lineColor=BLACK]
|
|
771
814
|
* @param {number} [angle]
|
|
772
815
|
* @param {boolean} [useWebGL=glEnable]
|
|
773
|
-
* @param {boolean} [screenSpace]
|
|
816
|
+
* @param {boolean} [screenSpace=drawScreenSpace]
|
|
774
817
|
* @param {CanvasRenderingContext2D|OffscreenCanvasRenderingContext2D} [context]
|
|
775
818
|
* @memberof Draw */
|
|
776
|
-
function drawRegularPoly(pos, size=vec2(1), sides=3, color=WHITE, lineWidth=0, lineColor=BLACK, angle=0, useWebGL=glEnable, screenSpace=
|
|
819
|
+
function drawRegularPoly(pos, size=vec2(1), sides=3, color=WHITE, lineWidth=0, lineColor=BLACK, angle=0, useWebGL=glEnable, screenSpace=drawScreenSpace, context)
|
|
777
820
|
{
|
|
778
821
|
ASSERT(isVector2(size), 'size must be a vec2');
|
|
779
822
|
ASSERT(isNumber(sides), 'sides must be a number');
|
|
@@ -790,6 +833,8 @@ function drawRegularPoly(pos, size=vec2(1), sides=3, color=WHITE, lineWidth=0, l
|
|
|
790
833
|
}
|
|
791
834
|
|
|
792
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
|
|
793
838
|
* @param {Array<Vector2>} points - Array of Vector2 points
|
|
794
839
|
* @param {Color} [color=WHITE]
|
|
795
840
|
* @param {number} [lineWidth]
|
|
@@ -797,10 +842,10 @@ function drawRegularPoly(pos, size=vec2(1), sides=3, color=WHITE, lineWidth=0, l
|
|
|
797
842
|
* @param {Vector2} [pos=vec2()] - Offset to apply
|
|
798
843
|
* @param {number} [angle] - Angle to rotate by
|
|
799
844
|
* @param {boolean} [useWebGL=glEnable]
|
|
800
|
-
* @param {boolean} [screenSpace]
|
|
845
|
+
* @param {boolean} [screenSpace=drawScreenSpace]
|
|
801
846
|
* @param {CanvasRenderingContext2D|OffscreenCanvasRenderingContext2D} [context]
|
|
802
847
|
* @memberof Draw */
|
|
803
|
-
function drawPoly(points, color=WHITE, lineWidth=0, lineColor=BLACK, pos=vec2(), angle=0, useWebGL=glEnable, screenSpace=
|
|
848
|
+
function drawPoly(points, color=WHITE, lineWidth=0, lineColor=BLACK, pos=vec2(), angle=0, useWebGL=glEnable, screenSpace=drawScreenSpace, context=undefined)
|
|
804
849
|
{
|
|
805
850
|
ASSERT(isVector2(pos), 'pos must be a vec2');
|
|
806
851
|
ASSERT(isArray(points), 'points must be an array');
|
|
@@ -809,18 +854,25 @@ function drawPoly(points, color=WHITE, lineWidth=0, lineColor=BLACK, pos=vec2(),
|
|
|
809
854
|
ASSERT(isNumber(angle), 'angle must be a number');
|
|
810
855
|
ASSERT(!context || !useWebGL, 'context only supported in canvas 2D mode');
|
|
811
856
|
|
|
857
|
+
if (headlessMode && !context) return; // headless has no canvas, only a context passed in is drawn to
|
|
858
|
+
|
|
812
859
|
if (useWebGL && glEnable)
|
|
813
860
|
{
|
|
814
861
|
ASSERT(!!glContext, 'WebGL is not enabled!');
|
|
815
862
|
let size = vec2(1);
|
|
816
863
|
if (screenSpace)
|
|
864
|
+
{
|
|
865
|
+
if (glSkipScreenSpace) return;
|
|
817
866
|
[pos, size, angle] = screenToWorldTransform(pos, size, angle);
|
|
867
|
+
}
|
|
818
868
|
glDrawPointsTransform(points, color.rgbaInt(), pos.x, pos.y, size.x, size.y, angle);
|
|
819
869
|
if (lineWidth > 0)
|
|
820
870
|
glDrawOutlineTransform(points, lineColor.rgbaInt(), lineWidth, pos.x, pos.y, size.x, size.y, angle);
|
|
821
871
|
}
|
|
822
872
|
else
|
|
823
873
|
{
|
|
874
|
+
++drawCount;
|
|
875
|
+
++primitiveCount;
|
|
824
876
|
drawCanvas2D(pos, vec2(1), angle, false, context=>
|
|
825
877
|
{
|
|
826
878
|
context.fillStyle = color.toString();
|
|
@@ -829,7 +881,7 @@ function drawPoly(points, color=WHITE, lineWidth=0, lineColor=BLACK, pos=vec2(),
|
|
|
829
881
|
context.lineTo(point.x, point.y);
|
|
830
882
|
context.closePath();
|
|
831
883
|
context.fill();
|
|
832
|
-
if (lineWidth)
|
|
884
|
+
if (lineWidth > 0) // as in WebGL, Canvas2D would stroke a width below 0 with the last one
|
|
833
885
|
{
|
|
834
886
|
context.strokeStyle = lineColor.toString();
|
|
835
887
|
context.lineWidth = lineWidth;
|
|
@@ -839,6 +891,9 @@ function drawPoly(points, color=WHITE, lineWidth=0, lineColor=BLACK, pos=vec2(),
|
|
|
839
891
|
}
|
|
840
892
|
}
|
|
841
893
|
|
|
894
|
+
// the unit rings drawEllipse fills with in WebGL, one for each side count
|
|
895
|
+
const drawEllipseRings = new Map;
|
|
896
|
+
|
|
842
897
|
/** Draw colored ellipse using passed in point
|
|
843
898
|
* @param {Vector2} pos
|
|
844
899
|
* @param {Vector2} [size=vec2(1)] - Width and height diameter
|
|
@@ -847,10 +902,10 @@ function drawPoly(points, color=WHITE, lineWidth=0, lineColor=BLACK, pos=vec2(),
|
|
|
847
902
|
* @param {number} [lineWidth]
|
|
848
903
|
* @param {Color} [lineColor=BLACK]
|
|
849
904
|
* @param {boolean} [useWebGL=glEnable]
|
|
850
|
-
* @param {boolean} [screenSpace]
|
|
905
|
+
* @param {boolean} [screenSpace=drawScreenSpace]
|
|
851
906
|
* @param {CanvasRenderingContext2D|OffscreenCanvasRenderingContext2D} [context]
|
|
852
907
|
* @memberof Draw */
|
|
853
|
-
function drawEllipse(pos, size=vec2(1), color=WHITE, angle=0, lineWidth=0, lineColor=BLACK, useWebGL=glEnable, screenSpace=
|
|
908
|
+
function drawEllipse(pos, size=vec2(1), color=WHITE, angle=0, lineWidth=0, lineColor=BLACK, useWebGL=glEnable, screenSpace=drawScreenSpace, context)
|
|
854
909
|
{
|
|
855
910
|
ASSERT(isVector2(pos), 'pos must be a vec2');
|
|
856
911
|
ASSERT(isVector2(size), 'size must be a vec2');
|
|
@@ -860,24 +915,51 @@ function drawEllipse(pos, size=vec2(1), color=WHITE, angle=0, lineWidth=0, lineC
|
|
|
860
915
|
ASSERT(lineWidth >= 0, 'lineWidth must be a positive value or 0');
|
|
861
916
|
ASSERT(!context || !useWebGL, 'context only supported in canvas 2D mode');
|
|
862
917
|
|
|
863
|
-
//
|
|
864
|
-
|
|
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)));
|
|
865
922
|
|
|
866
923
|
if (useWebGL && glEnable)
|
|
867
924
|
{
|
|
868
|
-
// draw as a regular polygon
|
|
869
925
|
const sides = glCircleSides;
|
|
870
|
-
|
|
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);
|
|
871
951
|
}
|
|
872
952
|
else
|
|
873
953
|
{
|
|
954
|
+
++drawCount;
|
|
955
|
+
++primitiveCount;
|
|
874
956
|
drawCanvas2D(pos, vec2(1), angle, false, context=>
|
|
875
957
|
{
|
|
876
958
|
context.fillStyle = color.toString();
|
|
877
959
|
context.beginPath();
|
|
878
|
-
context.ellipse(0, 0, size.x/2, size.y/2, 0, 0, 9);
|
|
960
|
+
context.ellipse(0, 0, abs(size.x)/2, abs(size.y)/2, 0, 0, 9); // it throws on a negative radius
|
|
879
961
|
context.fill();
|
|
880
|
-
if (lineWidth)
|
|
962
|
+
if (lineWidth > 0)
|
|
881
963
|
{
|
|
882
964
|
context.strokeStyle = lineColor.toString();
|
|
883
965
|
context.lineWidth = lineWidth;
|
|
@@ -894,15 +976,16 @@ function drawEllipse(pos, size=vec2(1), color=WHITE, angle=0, lineWidth=0, lineC
|
|
|
894
976
|
* @param {number} [lineWidth=0]
|
|
895
977
|
* @param {Color} [lineColor=BLACK]
|
|
896
978
|
* @param {boolean} [useWebGL=glEnable]
|
|
897
|
-
* @param {boolean} [screenSpace]
|
|
979
|
+
* @param {boolean} [screenSpace=drawScreenSpace]
|
|
898
980
|
* @param {CanvasRenderingContext2D|OffscreenCanvasRenderingContext2D} [context]
|
|
899
981
|
* @memberof Draw */
|
|
900
|
-
function drawCircle(pos, size=1, color=WHITE, lineWidth=0, lineColor=BLACK, useWebGL=glEnable, screenSpace=
|
|
982
|
+
function drawCircle(pos, size=1, color=WHITE, lineWidth=0, lineColor=BLACK, useWebGL=glEnable, screenSpace=drawScreenSpace, context)
|
|
901
983
|
{
|
|
902
984
|
ASSERT(isNumber(size), 'size must be a number');
|
|
903
985
|
drawEllipse(pos, vec2(size), color, 0, lineWidth, lineColor, useWebGL, screenSpace, context);
|
|
904
986
|
}
|
|
905
987
|
|
|
988
|
+
let drawEllipseGradientOffset = 0;
|
|
906
989
|
/** Draw an ellipse filled with a radial gradient from the center to the rim
|
|
907
990
|
* - Best when batched with other untextured polys
|
|
908
991
|
* - If drawing mostly textured sprites, bake the gradient into a texture and use drawTile instead
|
|
@@ -913,11 +996,10 @@ function drawCircle(pos, size=1, color=WHITE, lineWidth=0, lineColor=BLACK, useW
|
|
|
913
996
|
* @param {Color} [colorOuter=CLEAR_WHITE]
|
|
914
997
|
* @param {number} [angle]
|
|
915
998
|
* @param {boolean} [useWebGL=glEnable]
|
|
916
|
-
* @param {boolean} [screenSpace]
|
|
999
|
+
* @param {boolean} [screenSpace=drawScreenSpace]
|
|
917
1000
|
* @param {CanvasRenderingContext2D|OffscreenCanvasRenderingContext2D} [context]
|
|
918
1001
|
* @memberof Draw */
|
|
919
|
-
|
|
920
|
-
function drawEllipseGradient(pos, size=vec2(1), colorInner=WHITE, colorOuter=CLEAR_WHITE, angle=0, useWebGL=glEnable, screenSpace=false, context)
|
|
1002
|
+
function drawEllipseGradient(pos, size=vec2(1), colorInner=WHITE, colorOuter=CLEAR_WHITE, angle=0, useWebGL=glEnable, screenSpace=drawScreenSpace, context)
|
|
921
1003
|
{
|
|
922
1004
|
ASSERT(isVector2(pos), 'pos must be a vec2');
|
|
923
1005
|
ASSERT(isVector2(size), 'size must be a vec2');
|
|
@@ -925,17 +1007,15 @@ function drawEllipseGradient(pos, size=vec2(1), colorInner=WHITE, colorOuter=CLE
|
|
|
925
1007
|
ASSERT(isNumber(angle), 'angle must be a number');
|
|
926
1008
|
ASSERT(!context || !useWebGL, 'context only supported in canvas 2D mode');
|
|
927
1009
|
|
|
928
|
-
if (headlessMode) return;
|
|
1010
|
+
if (headlessMode && !context) return; // headless has no canvas, only a context passed in is drawn to
|
|
929
1011
|
|
|
930
1012
|
if (useWebGL && glEnable)
|
|
931
1013
|
{
|
|
932
1014
|
ASSERT(!!glContext, 'WebGL is not enabled!');
|
|
933
1015
|
if (screenSpace)
|
|
934
1016
|
{
|
|
935
|
-
|
|
936
|
-
pos =
|
|
937
|
-
size = size.scale(1/cameraScale);
|
|
938
|
-
angle += cameraAngle;
|
|
1017
|
+
if (glSkipScreenSpace) return;
|
|
1018
|
+
[pos, size, angle] = screenToWorldTransform(pos, size, angle);
|
|
939
1019
|
}
|
|
940
1020
|
// fan as tristrip; rotate the boundary vertex by one slice per call
|
|
941
1021
|
// so back-to-back gradients at the same position have their hole
|
|
@@ -992,10 +1072,10 @@ function drawEllipseGradient(pos, size=vec2(1), colorInner=WHITE, colorOuter=CLE
|
|
|
992
1072
|
* @param {Color} [colorInner=WHITE]
|
|
993
1073
|
* @param {Color} [colorOuter=CLEAR_WHITE]
|
|
994
1074
|
* @param {boolean} [useWebGL=glEnable]
|
|
995
|
-
* @param {boolean} [screenSpace]
|
|
1075
|
+
* @param {boolean} [screenSpace=drawScreenSpace]
|
|
996
1076
|
* @param {CanvasRenderingContext2D|OffscreenCanvasRenderingContext2D} [context]
|
|
997
1077
|
* @memberof Draw */
|
|
998
|
-
function drawCircleGradient(pos, size=1, colorInner=WHITE, colorOuter=CLEAR_WHITE, useWebGL=glEnable, screenSpace=
|
|
1078
|
+
function drawCircleGradient(pos, size=1, colorInner=WHITE, colorOuter=CLEAR_WHITE, useWebGL=glEnable, screenSpace=drawScreenSpace, context)
|
|
999
1079
|
{
|
|
1000
1080
|
ASSERT(isNumber(size), 'size must be a number');
|
|
1001
1081
|
drawEllipseGradient(pos, vec2(size), colorInner, colorOuter, 0, useWebGL, screenSpace, context);
|
|
@@ -1013,19 +1093,21 @@ function drawCircleGradient(pos, size=1, colorInner=WHITE, colorOuter=CLEAR_WHIT
|
|
|
1013
1093
|
* (e.g. linear gradients) should flip their own Y endpoints accordingly.
|
|
1014
1094
|
* @param {Vector2} pos
|
|
1015
1095
|
* @param {Vector2} size
|
|
1016
|
-
* @param {number} angle
|
|
1096
|
+
* @param {number} [angle]
|
|
1017
1097
|
* @param {boolean} [mirror]
|
|
1018
|
-
* @param {Canvas2DDrawFunction} [drawFunction]
|
|
1019
|
-
* @param {boolean} [screenSpace=
|
|
1098
|
+
* @param {Canvas2DDrawFunction} [drawFunction] - Needed, marked optional only because the ones before it are
|
|
1099
|
+
* @param {boolean} [screenSpace=drawScreenSpace]
|
|
1020
1100
|
* @param {CanvasRenderingContext2D|OffscreenCanvasRenderingContext2D} [context=drawContext]
|
|
1021
1101
|
* @memberof Draw */
|
|
1022
|
-
function drawCanvas2D(pos, size, angle=0, mirror=false, drawFunction, screenSpace=
|
|
1102
|
+
function drawCanvas2D(pos, size, angle=0, mirror=false, drawFunction, screenSpace=drawScreenSpace, context=drawContext)
|
|
1023
1103
|
{
|
|
1024
1104
|
ASSERT(isVector2(pos), 'pos must be a vec2');
|
|
1025
1105
|
ASSERT(isVector2(size), 'size must be a vec2');
|
|
1026
1106
|
ASSERT(isNumber(angle), 'angle must be a number');
|
|
1027
1107
|
ASSERT(typeof drawFunction === 'function', 'drawFunction must be a function');
|
|
1028
1108
|
|
|
1109
|
+
if (headlessMode && !context) return; // headless has no canvas, only a context passed in is drawn to
|
|
1110
|
+
|
|
1029
1111
|
if (!screenSpace)
|
|
1030
1112
|
{
|
|
1031
1113
|
pos = worldToScreen(pos);
|
|
@@ -1051,7 +1133,7 @@ function drawCanvas2D(pos, size, angle=0, mirror=false, drawFunction, screenSpac
|
|
|
1051
1133
|
* @param {Color} [color=WHITE]
|
|
1052
1134
|
* @param {number} [lineWidth]
|
|
1053
1135
|
* @param {Color} [lineColor=BLACK]
|
|
1054
|
-
* @param {
|
|
1136
|
+
* @param {'left'|'center'|'right'} [textAlign='center']
|
|
1055
1137
|
* @param {string} [font=fontDefault]
|
|
1056
1138
|
* @param {string} [fontStyle]
|
|
1057
1139
|
* @param {number} [maxWidth]
|
|
@@ -1064,8 +1146,9 @@ function drawText(text, pos, size=1, color=WHITE, lineWidth=0, lineColor=BLACK,
|
|
|
1064
1146
|
pos = worldToScreen(pos);
|
|
1065
1147
|
size *= cameraScale;
|
|
1066
1148
|
lineWidth *= cameraScale;
|
|
1149
|
+
if (maxWidth !== undefined)
|
|
1150
|
+
maxWidth *= cameraScale;
|
|
1067
1151
|
angle -= cameraAngle;
|
|
1068
|
-
angle *= -1;
|
|
1069
1152
|
|
|
1070
1153
|
drawTextScreen(text, pos, size, color, lineWidth, lineColor, textAlign, font, fontStyle, maxWidth, angle, context);
|
|
1071
1154
|
}
|
|
@@ -1078,11 +1161,11 @@ function drawText(text, pos, size=1, color=WHITE, lineWidth=0, lineColor=BLACK,
|
|
|
1078
1161
|
* @param {Color} [color=WHITE]
|
|
1079
1162
|
* @param {number} [lineWidth]
|
|
1080
1163
|
* @param {Color} [lineColor=BLACK]
|
|
1081
|
-
* @param {
|
|
1164
|
+
* @param {'left'|'center'|'right'} [textAlign]
|
|
1082
1165
|
* @param {string} [font=fontDefault]
|
|
1083
1166
|
* @param {string} [fontStyle]
|
|
1084
1167
|
* @param {number} [maxWidth]
|
|
1085
|
-
* @param {number} [angle]
|
|
1168
|
+
* @param {number} [angle] - Clockwise, like the other screen space draws
|
|
1086
1169
|
* @param {CanvasRenderingContext2D|OffscreenCanvasRenderingContext2D} [context=drawContext]
|
|
1087
1170
|
* @memberof Draw */
|
|
1088
1171
|
function drawTextScreen(text, pos, size, color=WHITE, lineWidth=0, lineColor=BLACK, textAlign='center', font=fontDefault, fontStyle='', maxWidth, angle=0, context=drawContext)
|
|
@@ -1097,9 +1180,10 @@ function drawTextScreen(text, pos, size, color=WHITE, lineWidth=0, lineColor=BLA
|
|
|
1097
1180
|
ASSERT(isStringLike(font), 'font must be a string');
|
|
1098
1181
|
ASSERT(isStringLike(fontStyle), 'fontStyle must be a string');
|
|
1099
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
|
|
1100
1185
|
|
|
1101
1186
|
const lines = (text+'').split('\n');
|
|
1102
|
-
const posY = pos.y - (lines.length-1) * size/2; // center vertically
|
|
1103
1187
|
// save before style mutations so caller's context state is preserved
|
|
1104
1188
|
context.save();
|
|
1105
1189
|
context.fillStyle = color.toString();
|
|
@@ -1108,9 +1192,9 @@ function drawTextScreen(text, pos, size, color=WHITE, lineWidth=0, lineColor=BLA
|
|
|
1108
1192
|
context.textAlign = textAlign;
|
|
1109
1193
|
context.font = fontStyle + ' ' + size + 'px '+ font;
|
|
1110
1194
|
context.textBaseline = 'middle';
|
|
1111
|
-
context.translate(pos.x,
|
|
1112
|
-
context.rotate(
|
|
1113
|
-
let yOffset =
|
|
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
|
|
1114
1198
|
lines.forEach(line=>
|
|
1115
1199
|
{
|
|
1116
1200
|
lineWidth && context.strokeText(line, 0, yOffset, maxWidth);
|
|
@@ -1123,15 +1207,18 @@ function drawTextScreen(text, pos, size, color=WHITE, lineWidth=0, lineColor=BLA
|
|
|
1123
1207
|
///////////////////////////////////////////////////////////////////////////////
|
|
1124
1208
|
// Drawing utilities
|
|
1125
1209
|
|
|
1126
|
-
/** Load a texture at a specific index
|
|
1127
|
-
* @param {number} textureIndex - Index to store the texture at
|
|
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
|
|
1128
1212
|
* @param {string} [src] - Image source path
|
|
1129
|
-
* @return {Promise}
|
|
1213
|
+
* @return {Promise<TextureInfo>} Resolves to the texture info once the image loads, or fails to with a warning
|
|
1130
1214
|
* @memberof Draw */
|
|
1131
1215
|
async function loadTexture(textureIndex, src)
|
|
1132
1216
|
{
|
|
1133
1217
|
ASSERT(isNumber(textureIndex), 'textureIndex must be a number');
|
|
1134
|
-
|
|
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();
|
|
1135
1222
|
ASSERT(!src || isStringLike(src), 'image src must be a string');
|
|
1136
1223
|
|
|
1137
1224
|
const image = new Image;
|
|
@@ -1139,13 +1226,18 @@ async function loadTexture(textureIndex, src)
|
|
|
1139
1226
|
{
|
|
1140
1227
|
await new Promise(resolve =>
|
|
1141
1228
|
{
|
|
1142
|
-
image.
|
|
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
|
+
};
|
|
1143
1235
|
image.crossOrigin = 'anonymous';
|
|
1144
1236
|
image.src = src;
|
|
1145
1237
|
});
|
|
1146
1238
|
}
|
|
1147
1239
|
|
|
1148
|
-
textureInfos[textureIndex] = new TextureInfo(image);
|
|
1240
|
+
return textureInfos[textureIndex] = new TextureInfo(image);
|
|
1149
1241
|
}
|
|
1150
1242
|
|
|
1151
1243
|
/** Convert from screen to world space coordinates
|
|
@@ -1212,7 +1304,7 @@ function screenToWorldDelta(screenDelta)
|
|
|
1212
1304
|
return new Vector2(x, y);
|
|
1213
1305
|
}
|
|
1214
1306
|
|
|
1215
|
-
/** Convert from
|
|
1307
|
+
/** Convert from world to screen space coordinates for a directional vector (no translation)
|
|
1216
1308
|
* @param {Vector2} worldDelta
|
|
1217
1309
|
* @return {Vector2}
|
|
1218
1310
|
* @memberof Draw */
|
|
@@ -1256,6 +1348,14 @@ function screenToWorldTransform(screenPos, screenSize, screenAngle=0)
|
|
|
1256
1348
|
* @memberof Draw */
|
|
1257
1349
|
function getCameraSize() { return mainCanvasSize.scale(1/cameraScale); }
|
|
1258
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
|
+
|
|
1259
1359
|
/** Fit the camera to a rectangle in world space by setting cameraPos and cameraScale
|
|
1260
1360
|
* - worldMargin pads the content rectangle in world units, so the gap scales with the content on resize
|
|
1261
1361
|
* - screenInset reserves space in screen pixels on each viewport edge (for example a HUD band) and
|
|
@@ -1264,8 +1364,8 @@ function getCameraSize() { return mainCanvasSize.scale(1/cameraScale); }
|
|
|
1264
1364
|
* or an object with any of {top, right, bottom, left}
|
|
1265
1365
|
* @param {Vector2} center - Center of the rectangle in world space
|
|
1266
1366
|
* @param {Vector2} size - Size of the rectangle in world space
|
|
1267
|
-
* @param {number|Vector2|
|
|
1268
|
-
* @param {number|Vector2|
|
|
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
|
|
1269
1369
|
* @return {number} - The new camera scale
|
|
1270
1370
|
* @memberof Draw */
|
|
1271
1371
|
function cameraFit(center, size, worldMargin, screenInset)
|
|
@@ -1285,12 +1385,15 @@ function cameraFit(center, size, worldMargin, screenInset)
|
|
|
1285
1385
|
if (!(worldW > 0 && worldH > 0 && viewW > 0 && viewH > 0))
|
|
1286
1386
|
return cameraScale;
|
|
1287
1387
|
|
|
1288
|
-
// scale to fit the padded content
|
|
1289
|
-
|
|
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);
|
|
1290
1392
|
|
|
1291
|
-
// calculate offset vectors
|
|
1393
|
+
// calculate offset vectors, the inset is on the screen, so it turns back into the world with the camera
|
|
1292
1394
|
const marginVector = vec2(margin.right - margin.left, margin.top - margin.bottom).scale(.5);
|
|
1293
|
-
const
|
|
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);
|
|
1294
1397
|
|
|
1295
1398
|
// apply the offsets and return camera scale
|
|
1296
1399
|
cameraPos = center.add(marginVector).add(insetVector);
|
|
@@ -1316,7 +1419,7 @@ function cameraFit(center, size, worldMargin, screenInset)
|
|
|
1316
1419
|
* If size is a Vector2, uses the length as diameter
|
|
1317
1420
|
* This can be used to cull offscreen objects from render or update
|
|
1318
1421
|
* @param {Vector2} pos - world space position
|
|
1319
|
-
* @param {Vector2|number} size - world space size or diameter
|
|
1422
|
+
* @param {Vector2|number} [size] - world space size or diameter
|
|
1320
1423
|
* @return {boolean}
|
|
1321
1424
|
* @memberof Draw */
|
|
1322
1425
|
function isOnScreen(pos, size=0)
|
|
@@ -1327,8 +1430,7 @@ function isOnScreen(pos, size=0)
|
|
|
1327
1430
|
// cameraScale of 0 collapses world coords; nothing is visible
|
|
1328
1431
|
if (!cameraScale) return false;
|
|
1329
1432
|
|
|
1330
|
-
//
|
|
1331
|
-
// pos = worldToScreen(pos);
|
|
1433
|
+
// circle on screen test, worldToScreen inlined in doubled screen units so it compares against the full canvas size
|
|
1332
1434
|
let x = pos.x - cameraPos.x;
|
|
1333
1435
|
let y = pos.y - cameraPos.y;
|
|
1334
1436
|
if (cameraAngle)
|
|
@@ -1402,6 +1504,31 @@ function combineCanvases()
|
|
|
1402
1504
|
mainContext.restore();
|
|
1403
1505
|
}
|
|
1404
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
|
+
|
|
1405
1532
|
// Internal: bake a color/additive-color tint into workReadCanvas at the
|
|
1406
1533
|
// image's native resolution. Returns the work canvas, suitable for
|
|
1407
1534
|
// passing to context.createPattern. Used by drawTextureWrapped's
|
|
@@ -1415,45 +1542,27 @@ function bakeTintedImage(image, color, additiveColor)
|
|
|
1415
1542
|
workReadContext.drawImage(image, 0, 0);
|
|
1416
1543
|
|
|
1417
1544
|
const imageData = workReadContext.getImageData(0, 0, w, h);
|
|
1418
|
-
|
|
1419
|
-
if (additiveColor && !isBlack(additiveColor))
|
|
1420
|
-
{
|
|
1421
|
-
// multiply + additive (slower)
|
|
1422
|
-
const colorMultiply = [color.r, color.g, color.b, color.a];
|
|
1423
|
-
const colorAdd = [additiveColor.r * 255, additiveColor.g * 255,
|
|
1424
|
-
additiveColor.b * 255, additiveColor.a * 255];
|
|
1425
|
-
for (let i = 0; i < data.length; ++i)
|
|
1426
|
-
data[i] = data[i] * colorMultiply[i&3] + colorAdd[i&3] |0;
|
|
1427
|
-
}
|
|
1428
|
-
else
|
|
1429
|
-
{
|
|
1430
|
-
// RGB only, faster — alpha left intact for the caller
|
|
1431
|
-
for (let i = 0; i < data.length; i+=4)
|
|
1432
|
-
{
|
|
1433
|
-
data[i ] *= color.r;
|
|
1434
|
-
data[i+1] *= color.g;
|
|
1435
|
-
data[i+2] *= color.b;
|
|
1436
|
-
}
|
|
1437
|
-
}
|
|
1545
|
+
tintImageData(imageData.data, color, additiveColor);
|
|
1438
1546
|
workReadContext.putImageData(imageData, 0, 0);
|
|
1439
1547
|
return workReadCanvas;
|
|
1440
1548
|
}
|
|
1441
1549
|
|
|
1442
|
-
/**
|
|
1443
|
-
* This is slower
|
|
1444
|
-
|
|
1445
|
-
|
|
1446
|
-
|
|
1447
|
-
|
|
1448
|
-
|
|
1449
|
-
|
|
1450
|
-
|
|
1451
|
-
|
|
1452
|
-
|
|
1453
|
-
|
|
1454
|
-
|
|
1455
|
-
|
|
1456
|
-
|
|
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
|
|
1457
1566
|
* @memberof Draw */
|
|
1458
1567
|
function drawImageColor(context, image, sx, sy, sWidth, sHeight, dx, dy, dWidth, dHeight, color, additiveColor, bleed=0)
|
|
1459
1568
|
{
|
|
@@ -1463,9 +1572,9 @@ function drawImageColor(context, image, sx, sy, sWidth, sHeight, dx, dy, dWidth,
|
|
|
1463
1572
|
sHeight = max(1,sHeight|0);
|
|
1464
1573
|
const sWidth2 = sWidth - 2*bleed;
|
|
1465
1574
|
const sHeight2 = sHeight - 2*bleed;
|
|
1466
|
-
if (!canvasColorTiles ||
|
|
1575
|
+
if (!canvasColorTiles || isWhite(color) && (!additiveColor || isBlack(additiveColor)))
|
|
1467
1576
|
{
|
|
1468
|
-
// white
|
|
1577
|
+
// white tint and nothing added, the texels are drawn as they are
|
|
1469
1578
|
context.globalAlpha = color.a;
|
|
1470
1579
|
context.drawImage(image, sx+sx2, sy+sy2, sWidth2, sHeight2, dx, dy, dWidth, dHeight);
|
|
1471
1580
|
context.globalAlpha = 1;
|
|
@@ -1479,31 +1588,11 @@ function drawImageColor(context, image, sx, sy, sWidth, sHeight, dx, dy, dWidth,
|
|
|
1479
1588
|
|
|
1480
1589
|
// tint image using offscreen work context
|
|
1481
1590
|
const imageData = workReadContext.getImageData(0, 0, sWidth, sHeight);
|
|
1482
|
-
const
|
|
1483
|
-
|
|
1484
|
-
|
|
1485
|
-
|
|
1486
|
-
|
|
1487
|
-
const colorAdd = [additiveColor.r * 255, additiveColor.g * 255, additiveColor.b * 255, additiveColor.a * 255];
|
|
1488
|
-
for (let i = 0; i < data.length; ++i)
|
|
1489
|
-
data[i] = data[i] * colorMultiply[i&3] + colorAdd[i&3] |0;
|
|
1490
|
-
workReadContext.putImageData(imageData, 0, 0);
|
|
1491
|
-
context.drawImage(workReadCanvas, sx2, sy2, sWidth2, sHeight2, dx, dy, dWidth, dHeight);
|
|
1492
|
-
}
|
|
1493
|
-
else
|
|
1494
|
-
{
|
|
1495
|
-
// faster path with no additive color
|
|
1496
|
-
for (let i = 0; i < data.length; i+=4)
|
|
1497
|
-
{
|
|
1498
|
-
data[i ] *= color.r;
|
|
1499
|
-
data[i+1] *= color.g;
|
|
1500
|
-
data[i+2] *= color.b;
|
|
1501
|
-
}
|
|
1502
|
-
workReadContext.putImageData(imageData, 0, 0);
|
|
1503
|
-
context.globalAlpha = color.a;
|
|
1504
|
-
context.drawImage(workReadCanvas, sx2, sy2, sWidth2, sHeight2, dx, dy, dWidth, dHeight);
|
|
1505
|
-
context.globalAlpha = 1;
|
|
1506
|
-
}
|
|
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;
|
|
1507
1596
|
}
|
|
1508
1597
|
}
|
|
1509
1598
|
|
|
@@ -1556,7 +1645,7 @@ let engineImageFont;
|
|
|
1556
1645
|
* const font = engineImageFont;
|
|
1557
1646
|
*
|
|
1558
1647
|
* // draw text
|
|
1559
|
-
* font.drawTextScreen('LittleJS\nHello World!', vec2(200, 50));
|
|
1648
|
+
* font.drawTextScreen('LittleJS\nHello World!', vec2(200, 50), 16);
|
|
1560
1649
|
*/
|
|
1561
1650
|
class ImageFont
|
|
1562
1651
|
{
|
|
@@ -1568,14 +1657,15 @@ class ImageFont
|
|
|
1568
1657
|
ASSERT(!!tileInfo, 'tileInfo is required for ImageFont');
|
|
1569
1658
|
|
|
1570
1659
|
/** @property {TileInfo} - Tile info for the font */
|
|
1571
|
-
this.tileInfo = tileInfo
|
|
1660
|
+
this.tileInfo = tileInfo; // kept, not copied, so a loadSprite tile filled in once it loads is seen
|
|
1572
1661
|
}
|
|
1573
1662
|
|
|
1574
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
|
|
1575
1665
|
* @param {string|number} text
|
|
1576
1666
|
* @param {Vector2} pos
|
|
1577
1667
|
* @param {Vector2|number} [size]
|
|
1578
|
-
* @param {boolean} [center=true]
|
|
1668
|
+
* @param {boolean} [center=true] - center each line on pos, and the lines of multi-line text around it
|
|
1579
1669
|
* @param {Color} [color=WHITE]
|
|
1580
1670
|
* @param {boolean} [useWebGL=glEnable]
|
|
1581
1671
|
* @param {CanvasRenderingContext2D|OffscreenCanvasRenderingContext2D} [context]
|
|
@@ -1593,14 +1683,18 @@ class ImageFont
|
|
|
1593
1683
|
}
|
|
1594
1684
|
else
|
|
1595
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;
|
|
1596
1689
|
this.drawTextScreen(text, worldToScreen(pos), size, center, color, useWebGL, context);
|
|
1690
|
+
glSkipScreenSpace = skip;
|
|
1597
1691
|
}
|
|
1598
1692
|
|
|
1599
1693
|
/** Draw text in screen space using the image font
|
|
1600
1694
|
* @param {string|number} text
|
|
1601
1695
|
* @param {Vector2} pos
|
|
1602
1696
|
* @param {Vector2|number} size
|
|
1603
|
-
* @param {boolean} [center]
|
|
1697
|
+
* @param {boolean} [center] - center each line on pos, and the lines of multi-line text around it
|
|
1604
1698
|
* @param {Color} [color=WHITE]
|
|
1605
1699
|
* @param {boolean} [useWebGL=glEnable]
|
|
1606
1700
|
* @param {CanvasRenderingContext2D|OffscreenCanvasRenderingContext2D} [context]
|
|
@@ -1610,35 +1704,27 @@ class ImageFont
|
|
|
1610
1704
|
ASSERT(isStringLike(text), 'text must be a string');
|
|
1611
1705
|
ASSERT(isVector2(pos), 'pos must be a vec2');
|
|
1612
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');
|
|
1613
1708
|
ASSERT(isColor(color), 'color must be a color');
|
|
1614
1709
|
|
|
1615
1710
|
// if size is a number, make it a vector
|
|
1616
|
-
|
|
1711
|
+
const glyphSize = typeof size === 'number' ? new Vector2(size, size) : size;
|
|
1617
1712
|
|
|
1618
|
-
// precache objects for drawing
|
|
1713
|
+
// precache objects for drawing, a copy of the tile info each glyph moves, the font's own stays put
|
|
1619
1714
|
const drawPos = new Vector2;
|
|
1620
|
-
const tileInfo = this.tileInfo;
|
|
1621
|
-
|
|
1622
|
-
|
|
1623
|
-
const
|
|
1624
|
-
const
|
|
1625
|
-
|
|
1626
|
-
// draw each line of text
|
|
1627
|
-
(text+'').split('\n').forEach((line, j)=>
|
|
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)=>
|
|
1628
1721
|
{
|
|
1629
|
-
const centerOffset = center ? (line.length-1) *
|
|
1722
|
+
const centerOffset = center ? (line.length-1) * glyphSize.x / 2 : 0;
|
|
1630
1723
|
for (let i=line.length; i--;)
|
|
1631
1724
|
{
|
|
1632
|
-
// get the
|
|
1725
|
+
// get the glyph, out of range characters use the last one
|
|
1633
1726
|
const charCode = line.charCodeAt(i);
|
|
1634
|
-
|
|
1635
|
-
95 : charCode - 32; // handle out of range characters
|
|
1636
|
-
|
|
1637
|
-
// get the position of the tile
|
|
1638
|
-
const x = index % cols;
|
|
1639
|
-
const y = index / cols |0;
|
|
1640
|
-
tileInfo.pos.x = x*sizePaddedX + padding;
|
|
1641
|
-
tileInfo.pos.y = y*sizePaddedY + padding;
|
|
1727
|
+
this.getGlyphPos(charCode < 32 || charCode > 127 ? 95 : charCode - 32, tileInfo.pos);
|
|
1642
1728
|
|
|
1643
1729
|
// snap the glyph edges to whole pixels
|
|
1644
1730
|
// tiles are drawn from their center, so snapping the center
|
|
@@ -1647,12 +1733,28 @@ class ImageFont
|
|
|
1647
1733
|
// no pixel center inside it and is not rasterized at all
|
|
1648
1734
|
// ceil picks the nearest aligned position, breaking ties
|
|
1649
1735
|
// downward to match how this used to truncate
|
|
1650
|
-
drawPos.x = ceil(pos.x + i *
|
|
1651
|
-
drawPos.y = ceil(pos.y + j *
|
|
1652
|
-
drawTile(drawPos,
|
|
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);
|
|
1653
1739
|
}
|
|
1654
1740
|
});
|
|
1655
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
|
+
}
|
|
1656
1758
|
}
|
|
1657
1759
|
|
|
1658
1760
|
// load engine font, called automatically on startup
|