littlejsengine 1.19.3 → 1.21.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 +2857 -955
- package/dist/littlejs.esm.js +33428 -20995
- package/dist/littlejs.esm.min.js +4 -3
- package/dist/littlejs.js +33386 -20996
- package/dist/littlejs.min.js +2 -2
- package/dist/littlejs.release.js +27841 -20422
- 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 +786 -0
- package/plugins/jsconfig.json +19 -0
- package/plugins/lightSystem.js +719 -332
- package/plugins/math3d.js +148 -27
- package/plugins/medalSystem.js +326 -280
- package/plugins/newgrounds.js +400 -199
- package/plugins/pathFinder.js +208 -175
- package/plugins/pluginExport.js +25 -0
- package/plugins/postProcess.js +96 -24
- package/plugins/render3d.js +2067 -1632
- package/plugins/render3dDebug.js +110 -0
- package/plugins/render3dEditor.js +1812 -0
- package/plugins/render3dExtras.js +1948 -0
- package/plugins/render3dLevel.js +189 -0
- package/plugins/render3dVoxels.js +514 -0
- package/plugins/sceneSystem.js +58 -0
- package/plugins/textureSheet.js +496 -458
- package/plugins/threejs.js +183 -163
- package/plugins/tweakables.js +411 -0
- package/plugins/tweenSystem.js +265 -145
- package/plugins/uiSystem.js +2024 -1687
- package/plugins/zzfxm.js +159 -159
- package/src/engine.js +378 -97
- package/src/engineAudio.js +231 -127
- package/src/engineBuild.mjs +292 -256
- package/src/engineDebug.js +967 -743
- package/src/engineDraw.js +391 -198
- package/src/engineEditor.js +2452 -0
- package/src/engineExport.js +18 -0
- package/src/engineInput.js +1466 -1293
- package/src/engineLogo.js +19 -0
- package/src/engineMath.js +1274 -1252
- package/src/engineObject.js +197 -82
- package/src/engineParticles.js +583 -532
- package/src/engineRelease.js +79 -45
- package/src/engineSettings.js +115 -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;
|
|
@@ -290,12 +308,20 @@ class TextureInfo
|
|
|
290
308
|
}
|
|
291
309
|
|
|
292
310
|
///////////////////////////////////////////////////////////////////////////////
|
|
311
|
+
/**
|
|
312
|
+
* @callback SpriteAnimationEndCallback - Called once when a SpriteAnimation's play ends
|
|
313
|
+
* @return {void}
|
|
314
|
+
* @memberof Draw
|
|
315
|
+
*/
|
|
316
|
+
|
|
293
317
|
/**
|
|
294
318
|
* SpriteAnimation - Steps a tile through its frames over time: looping, once, or there and back
|
|
295
319
|
* - Driven by the engine time like a Timer, so it pauses with the game and needs no update call
|
|
296
320
|
* - Read tileInfo each frame for the frame to draw, from an object's update or before a drawTile
|
|
297
321
|
* - loop, play and pingPong each start over from the first frame; stop holds the current one
|
|
322
|
+
* - play(onEnd) calls onEnd once the play ends, on the first read after it, since there is no update to call it
|
|
298
323
|
* - Frames follow each other along the row, as tileInfo.frame counts them
|
|
324
|
+
* - SpriteAnimator switches between a character's animations by name
|
|
299
325
|
* @example
|
|
300
326
|
* const walk = new SpriteAnimation(tile(0, 16), 4, .1); // four frames, a tenth of a second each
|
|
301
327
|
* const attack = new SpriteAnimation(tile(4, 16), 3, .05).play(); // once, then holds the last frame
|
|
@@ -327,15 +353,24 @@ class SpriteAnimation
|
|
|
327
353
|
/** @property {number|undefined} - The frame held by stop, undefined while running
|
|
328
354
|
* @type {number|undefined} */
|
|
329
355
|
this.heldFrame = undefined;
|
|
356
|
+
/** @property {SpriteAnimationEndCallback|undefined} - Called once when a play ends, on the first read after it
|
|
357
|
+
* @type {SpriteAnimationEndCallback|undefined} */
|
|
358
|
+
this.onEnd = undefined;
|
|
330
359
|
}
|
|
331
360
|
|
|
332
361
|
/** Start over from the first frame and repeat forever
|
|
333
362
|
* @return {SpriteAnimation} */
|
|
334
363
|
loop() { return this.restart('loop'); }
|
|
335
364
|
|
|
336
|
-
/** Start over from the first frame, run through once and hold the last frame
|
|
365
|
+
/** Start over from the first frame, run through once and hold the last frame, last to first at a negative speed
|
|
366
|
+
* @param {function():void} [onEnd] - Called once when it ends, on the first read of it after that
|
|
337
367
|
* @return {SpriteAnimation} */
|
|
338
|
-
play()
|
|
368
|
+
play(onEnd)
|
|
369
|
+
{
|
|
370
|
+
this.restart('once');
|
|
371
|
+
this.onEnd = onEnd;
|
|
372
|
+
return this;
|
|
373
|
+
}
|
|
339
374
|
|
|
340
375
|
/** Start over from the first frame and run there and back forever
|
|
341
376
|
* @return {SpriteAnimation} */
|
|
@@ -345,7 +380,7 @@ class SpriteAnimation
|
|
|
345
380
|
* @return {SpriteAnimation} */
|
|
346
381
|
stop() { this.heldFrame = this.frame; return this; }
|
|
347
382
|
|
|
348
|
-
/** Start over from the first frame in a mode
|
|
383
|
+
/** Start over from the first frame in a mode, with no end callback
|
|
349
384
|
* @param {string} [mode] - 'loop', 'once' or 'pingPong', the current mode when left out
|
|
350
385
|
* @return {SpriteAnimation} */
|
|
351
386
|
restart(mode=this.mode)
|
|
@@ -353,6 +388,7 @@ class SpriteAnimation
|
|
|
353
388
|
this.mode = mode;
|
|
354
389
|
this.startTime = time;
|
|
355
390
|
this.heldFrame = undefined;
|
|
391
|
+
this.onEnd = undefined;
|
|
356
392
|
return this;
|
|
357
393
|
}
|
|
358
394
|
|
|
@@ -364,14 +400,15 @@ class SpriteAnimation
|
|
|
364
400
|
* @return {number} */
|
|
365
401
|
get frame()
|
|
366
402
|
{
|
|
403
|
+
this.isDone; // a read after a play ends calls onEnd, which may start it over
|
|
367
404
|
if (this.heldFrame !== undefined)
|
|
368
405
|
return this.heldFrame;
|
|
369
406
|
const n = this.frameCount, f = floor(this.elapsedFrames);
|
|
370
|
-
if (this.mode == 'once')
|
|
371
|
-
return
|
|
407
|
+
if (this.mode == 'once') // backward it counts down from the last frame, showing it from the start
|
|
408
|
+
return clamp(this.speed < 0 ? n - 1 + ceil(this.elapsedFrames) : f, 0, n - 1);
|
|
372
409
|
if (this.mode == 'loop')
|
|
373
|
-
return f
|
|
374
|
-
const period = max(2 * n - 2, 1), k = f
|
|
410
|
+
return mod(f, n);
|
|
411
|
+
const period = max(2 * n - 2, 1), k = mod(f, period); // there and back, the ends once each
|
|
375
412
|
return k < n ? k : period - k;
|
|
376
413
|
}
|
|
377
414
|
|
|
@@ -379,9 +416,81 @@ class SpriteAnimation
|
|
|
379
416
|
* @return {TileInfo} */
|
|
380
417
|
get tileInfo() { return this.firstTile.frame(this.frame); }
|
|
381
418
|
|
|
382
|
-
/** True once a play has shown its last frame for its time
|
|
419
|
+
/** True once a play has shown its last frame for its time, the first read after that calls onEnd
|
|
383
420
|
* @return {boolean} */
|
|
384
|
-
get isDone()
|
|
421
|
+
get isDone()
|
|
422
|
+
{
|
|
423
|
+
const done = this.mode == 'once' && this.heldFrame === undefined && abs(this.elapsedFrames) >= this.frameCount;
|
|
424
|
+
if (done && this.onEnd)
|
|
425
|
+
{
|
|
426
|
+
const onEnd = this.onEnd;
|
|
427
|
+
this.onEnd = undefined; // once, and it can start a new play with a callback of its own
|
|
428
|
+
onEnd();
|
|
429
|
+
}
|
|
430
|
+
return done;
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
///////////////////////////////////////////////////////////////////////////////
|
|
435
|
+
/**
|
|
436
|
+
* SpriteAnimator - A character's animations by name, like idle, walk and attack, and the one showing now
|
|
437
|
+
* - Each clip is a SpriteAnimation and keeps its own mode, loop, play or pingPong
|
|
438
|
+
* - set starts a clip over only when it changes or a play of it has ended, so it can be called every update
|
|
439
|
+
* - Read tileInfo each frame for the frame to draw, as with a SpriteAnimation
|
|
440
|
+
* @example
|
|
441
|
+
* const hero = new SpriteAnimator({
|
|
442
|
+
* idle: new SpriteAnimation(tile(0, 16), 2, .4),
|
|
443
|
+
* walk: new SpriteAnimation(tile(2, 16), 4, .1),
|
|
444
|
+
* attack: new SpriteAnimation(tile(6, 16), 3, .05).play(),
|
|
445
|
+
* });
|
|
446
|
+
* hero.set('attack', ()=> hero.set('idle')); // back to idle when the attack ends
|
|
447
|
+
* // in update: this.tileInfo = hero.tileInfo;
|
|
448
|
+
* @memberof Draw
|
|
449
|
+
*/
|
|
450
|
+
class SpriteAnimator
|
|
451
|
+
{
|
|
452
|
+
/** Create an animator from its clips, showing the first one
|
|
453
|
+
* @param {Object<string, SpriteAnimation>} clips - The clips by name */
|
|
454
|
+
constructor(clips)
|
|
455
|
+
{
|
|
456
|
+
const names = Object.keys(clips);
|
|
457
|
+
ASSERT(names.length > 0, 'an animator needs at least one clip');
|
|
458
|
+
/** @property {Object<string, SpriteAnimation>} - The clips by name */
|
|
459
|
+
this.clips = clips;
|
|
460
|
+
/** @property {string} - The name of the clip showing now */
|
|
461
|
+
this.name = names[0];
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
/** Show a clip, starting it over in its own mode unless it is showing and has not ended
|
|
465
|
+
* @param {string} name - The clip's name
|
|
466
|
+
* @param {function():void} [onEnd] - Called once when a play clip ends, as with SpriteAnimation.play
|
|
467
|
+
* @return {SpriteAnimator} */
|
|
468
|
+
set(name, onEnd)
|
|
469
|
+
{
|
|
470
|
+
ASSERT(this.clips[name], 'no clip named ' + name);
|
|
471
|
+
if (name == this.name && !this.clip.isDone)
|
|
472
|
+
return this;
|
|
473
|
+
this.name = name;
|
|
474
|
+
this.clip.restart();
|
|
475
|
+
this.clip.onEnd = onEnd;
|
|
476
|
+
return this;
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
/** The clip showing now
|
|
480
|
+
* @return {SpriteAnimation} */
|
|
481
|
+
get clip() { return this.clips[this.name]; }
|
|
482
|
+
|
|
483
|
+
/** The tile of the frame showing now
|
|
484
|
+
* @return {TileInfo} */
|
|
485
|
+
get tileInfo() { return this.clip.tileInfo; }
|
|
486
|
+
|
|
487
|
+
/** The frame of the clip showing now
|
|
488
|
+
* @return {number} */
|
|
489
|
+
get frame() { return this.clip.frame; }
|
|
490
|
+
|
|
491
|
+
/** True once the clip showing now is a play that has ended
|
|
492
|
+
* @return {boolean} */
|
|
493
|
+
get isDone() { return this.clip.isDone; }
|
|
385
494
|
}
|
|
386
495
|
|
|
387
496
|
///////////////////////////////////////////////////////////////////////////////
|
|
@@ -393,18 +502,26 @@ class SpriteAnimation
|
|
|
393
502
|
* - Set it as obj.shader, or use setShader for 2D draws and render3D.shader for 3D draws
|
|
394
503
|
* - Draws that share a Shader share a batch; with no Shader set nothing changes
|
|
395
504
|
* - In 2D it shades textured draws, untextured ones like drawRect draw as they are
|
|
505
|
+
* - A render target, like a tile layer drawn in WebGL, holds premultiplied color, and so does every image when
|
|
506
|
+
* tilesPixelated is false; there iChannel0 reads premultiplied texels and the snippet's color is taken as
|
|
507
|
+
* premultiplied too. premultipliedTexture is true there, so a snippet that changes the alpha, or makes a see
|
|
508
|
+
* through color of its own, scales the rgb with it: `if (premultipliedTexture) c.rgb *= k;`
|
|
396
509
|
* - Compiled once per renderer by the first draw that needs it; a bad snippet throws with the GLSL log in debug
|
|
397
510
|
* - 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()
|
|
511
|
+
* - Names in both renderers: iChannel0 the texture, iTime, iResolution, premultipliedTexture, and localUV, 0 to 1
|
|
512
|
+
* across the sprite or the mesh's own uv
|
|
513
|
+
* - Names in 3D only: worldPos, worldNormal, cameraPos, sunDirection, sunColor, ambientColor, ambientGroundColor,
|
|
514
|
+
* lightCount, lights[i], lightColors[i] and shadow()
|
|
515
|
+
* - In 3D the shadow map is drawn without the Shader, cut only by the texture's alpha, so a snippet that removes
|
|
516
|
+
* parts of a surface still shadows with the whole of it
|
|
402
517
|
* @example
|
|
403
518
|
* const fade = new Shader(`
|
|
404
519
|
* void mainImage(out vec4 c, vec2 uv)
|
|
405
520
|
* {
|
|
406
521
|
* c = texture(iChannel0, uv);
|
|
407
|
-
*
|
|
522
|
+
* float fade = .5 + .5*sin(iTime);
|
|
523
|
+
* c.a *= fade;
|
|
524
|
+
* if (premultipliedTexture) c.rgb *= fade; // a tile layer, or any image with tilesPixelated false
|
|
408
525
|
* }`);
|
|
409
526
|
* obj.shader = fade;
|
|
410
527
|
* @memberof Draw
|
|
@@ -440,11 +557,11 @@ class Shader
|
|
|
440
557
|
* @param {boolean} [mirror] - Is image flipped along the Y axis?
|
|
441
558
|
* @param {Color} [additiveColor] - Additive color to be applied if any
|
|
442
559
|
* @param {boolean} [useWebGL=glEnable] - Use accelerated WebGL rendering?
|
|
443
|
-
* @param {boolean} [screenSpace=
|
|
560
|
+
* @param {boolean} [screenSpace=drawScreenSpace] - Are the pos and size are in screen space?
|
|
444
561
|
* @param {CanvasRenderingContext2D|OffscreenCanvasRenderingContext2D} [context] - Canvas 2D context to draw to
|
|
445
562
|
* @memberof Draw */
|
|
446
563
|
function drawTile(pos, size=vec2(1), tileInfo, color=WHITE,
|
|
447
|
-
angle=0, mirror, additiveColor, useWebGL=glEnable, screenSpace=
|
|
564
|
+
angle=0, mirror, additiveColor, useWebGL=glEnable, screenSpace=drawScreenSpace, context)
|
|
448
565
|
{
|
|
449
566
|
ASSERT(isVector2(pos), 'pos must be a vec2');
|
|
450
567
|
ASSERT(isVector2(size), 'size must be a vec2');
|
|
@@ -453,15 +570,23 @@ function drawTile(pos, size=vec2(1), tileInfo, color=WHITE,
|
|
|
453
570
|
ASSERT(!additiveColor || isColor(additiveColor), 'additiveColor must be a color');
|
|
454
571
|
ASSERT(!context || !useWebGL, 'context only supported in canvas 2D mode');
|
|
455
572
|
|
|
573
|
+
if (headlessMode && !context) return; // headless has no canvas, only a context passed in is drawn to
|
|
574
|
+
|
|
456
575
|
const textureInfo = tileInfo?.textureInfo;
|
|
576
|
+
if (textureInfo && !(tileInfo.size.x && tileInfo.size.y))
|
|
577
|
+
return; // a tile with no area draws nothing, like a sprite still loading
|
|
457
578
|
const bleed = tileInfo?.bleed ?? 0;
|
|
458
579
|
if (useWebGL && glEnable)
|
|
459
580
|
{
|
|
460
581
|
ASSERT(!!glContext, 'WebGL is not enabled!');
|
|
461
582
|
if (screenSpace)
|
|
583
|
+
{
|
|
584
|
+
if (glSkipScreenSpace) return;
|
|
462
585
|
[pos, size, angle] = screenToWorldTransform(pos, size, angle);
|
|
586
|
+
}
|
|
463
587
|
if (textureInfo)
|
|
464
588
|
{
|
|
589
|
+
ASSERT(!!textureInfo.glTexture, 'texture has no WebGL texture, draw it with useWebGL false');
|
|
465
590
|
// calculate uvs and render
|
|
466
591
|
const sizeInverse = textureInfo.sizeInverse;
|
|
467
592
|
const x = tileInfo.pos.x * sizeInverse.x;
|
|
@@ -487,8 +612,7 @@ function drawTile(pos, size=vec2(1), tileInfo, color=WHITE,
|
|
|
487
612
|
}
|
|
488
613
|
else
|
|
489
614
|
{
|
|
490
|
-
// untextured:
|
|
491
|
-
// color.add(additiveColor) on line ~337.
|
|
615
|
+
// untextured: color plus additive in one color, as the Canvas2D path below does
|
|
492
616
|
const combined = additiveColor ? color.add(additiveColor) : color;
|
|
493
617
|
glDrawUntextured(pos.x, pos.y, size.x, size.y, angle, combined.rgbaInt());
|
|
494
618
|
}
|
|
@@ -526,7 +650,7 @@ function drawTile(pos, size=vec2(1), tileInfo, color=WHITE,
|
|
|
526
650
|
* @param {Color} [color=WHITE]
|
|
527
651
|
* @param {number} [angle]
|
|
528
652
|
* @param {boolean} [useWebGL=glEnable]
|
|
529
|
-
* @param {boolean} [screenSpace]
|
|
653
|
+
* @param {boolean} [screenSpace=drawScreenSpace]
|
|
530
654
|
* @param {CanvasRenderingContext2D|OffscreenCanvasRenderingContext2D} [context]
|
|
531
655
|
* @memberof Draw */
|
|
532
656
|
function drawRect(pos, size, color, angle, useWebGL, screenSpace, context)
|
|
@@ -541,10 +665,10 @@ function drawRect(pos, size, color, angle, useWebGL, screenSpace, context)
|
|
|
541
665
|
* @param {Color} [colorBottom=CLEAR_WHITE]
|
|
542
666
|
* @param {number} [angle]
|
|
543
667
|
* @param {boolean} [useWebGL=glEnable]
|
|
544
|
-
* @param {boolean} [screenSpace]
|
|
668
|
+
* @param {boolean} [screenSpace=drawScreenSpace]
|
|
545
669
|
* @param {CanvasRenderingContext2D|OffscreenCanvasRenderingContext2D} [context]
|
|
546
670
|
* @memberof Draw */
|
|
547
|
-
function drawRectGradient(pos, size, colorTop=WHITE, colorBottom=CLEAR_WHITE, angle=0, useWebGL=glEnable, screenSpace=
|
|
671
|
+
function drawRectGradient(pos, size, colorTop=WHITE, colorBottom=CLEAR_WHITE, angle=0, useWebGL=glEnable, screenSpace=drawScreenSpace, context)
|
|
548
672
|
{
|
|
549
673
|
ASSERT(isVector2(pos), 'pos must be a vec2');
|
|
550
674
|
ASSERT(isVector2(size), 'size must be a vec2');
|
|
@@ -552,15 +676,15 @@ function drawRectGradient(pos, size, colorTop=WHITE, colorBottom=CLEAR_WHITE, an
|
|
|
552
676
|
ASSERT(isNumber(angle), 'angle must be a number');
|
|
553
677
|
ASSERT(!context || !useWebGL, 'context only supported in canvas 2D mode');
|
|
554
678
|
|
|
679
|
+
if (headlessMode && !context) return; // headless has no canvas, only a context passed in is drawn to
|
|
680
|
+
|
|
555
681
|
if (useWebGL && glEnable)
|
|
556
682
|
{
|
|
557
683
|
ASSERT(!!glContext, 'WebGL is not enabled!');
|
|
558
684
|
if (screenSpace)
|
|
559
685
|
{
|
|
560
|
-
|
|
561
|
-
pos =
|
|
562
|
-
size = size.scale(1/cameraScale);
|
|
563
|
-
angle += cameraAngle;
|
|
686
|
+
if (glSkipScreenSpace) return;
|
|
687
|
+
[pos, size, angle] = screenToWorldTransform(pos, size, angle);
|
|
564
688
|
}
|
|
565
689
|
// build 4 corner points for the rectangle
|
|
566
690
|
const points = [], colors = [];
|
|
@@ -608,11 +732,11 @@ function drawRectGradient(pos, size, colorTop=WHITE, colorBottom=CLEAR_WHITE, an
|
|
|
608
732
|
* @param {number} [angle=0] - Angle to rotate by
|
|
609
733
|
* @param {Color} [additiveColor] - Additive color to be applied if any
|
|
610
734
|
* @param {boolean} [useWebGL=glEnable] - Use accelerated WebGL rendering?
|
|
611
|
-
* @param {boolean} [screenSpace=
|
|
735
|
+
* @param {boolean} [screenSpace=drawScreenSpace] - Are pos and size in screen space?
|
|
612
736
|
* @param {CanvasRenderingContext2D|OffscreenCanvasRenderingContext2D} [context] - Canvas 2D context to draw to
|
|
613
737
|
* @memberof Draw */
|
|
614
738
|
function drawTextureWrapped(pos, size, wrapCount, texture=0, color=WHITE,
|
|
615
|
-
angle=0, additiveColor, useWebGL=glEnable, screenSpace=
|
|
739
|
+
angle=0, additiveColor, useWebGL=glEnable, screenSpace=drawScreenSpace, context)
|
|
616
740
|
{
|
|
617
741
|
ASSERT(isVector2(pos), 'pos must be a vec2');
|
|
618
742
|
ASSERT(isVector2(size), 'size must be a vec2');
|
|
@@ -624,8 +748,8 @@ function drawTextureWrapped(pos, size, wrapCount, texture=0, color=WHITE,
|
|
|
624
748
|
ASSERT(!(texture instanceof TileInfo),
|
|
625
749
|
'pass a TextureInfo or texture index, not a TileInfo — use tileInfo.textureInfo');
|
|
626
750
|
|
|
627
|
-
//
|
|
628
|
-
if (headlessMode) return;
|
|
751
|
+
// only a context passed in is drawn to in headless mode, before the texture lookup, textureInfos is empty there
|
|
752
|
+
if (headlessMode && !context) return;
|
|
629
753
|
|
|
630
754
|
// resolve texture argument: TextureInfo or index
|
|
631
755
|
const textureInfo = typeof texture === 'number' ? textureInfos[texture] : texture;
|
|
@@ -638,7 +762,10 @@ function drawTextureWrapped(pos, size, wrapCount, texture=0, color=WHITE,
|
|
|
638
762
|
{
|
|
639
763
|
ASSERT(!!glContext, 'WebGL is not enabled!');
|
|
640
764
|
if (screenSpace)
|
|
765
|
+
{
|
|
766
|
+
if (glSkipScreenSpace) return;
|
|
641
767
|
[pos, size, angle] = screenToWorldTransform(pos, size, angle);
|
|
768
|
+
}
|
|
642
769
|
glSetTexture(textureInfo.glTexture);
|
|
643
770
|
glDraw(pos.x, pos.y, size.x, size.y, angle,
|
|
644
771
|
0, 0, wrapCount.x, wrapCount.y,
|
|
@@ -660,9 +787,7 @@ function drawTextureWrapped(pos, size, wrapCount, texture=0, color=WHITE,
|
|
|
660
787
|
// pick image source: raw, or tinted bake. Match drawImageColor's
|
|
661
788
|
// "no tint needed" predicate so behavior stays consistent.
|
|
662
789
|
const noTint = !canvasColorTiles ||
|
|
663
|
-
(additiveColor
|
|
664
|
-
? isWhite(color.add(additiveColor)) && additiveColor.a <= 0
|
|
665
|
-
: isWhite(color));
|
|
790
|
+
isWhite(color) && (!additiveColor || isBlack(additiveColor));
|
|
666
791
|
// alpha is baked into pixels by bakeTintedImage's additive branch;
|
|
667
792
|
// in that case globalAlpha must NOT also apply color.a
|
|
668
793
|
const alphaBaked = !noTint && additiveColor && !isBlack(additiveColor);
|
|
@@ -698,10 +823,10 @@ function drawTextureWrapped(pos, size, wrapCount, texture=0, color=WHITE,
|
|
|
698
823
|
* @param {Vector2} [pos=vec2()] - Offset to apply
|
|
699
824
|
* @param {number} [angle] - Angle to rotate by
|
|
700
825
|
* @param {boolean} [useWebGL=glEnable]
|
|
701
|
-
* @param {boolean} [screenSpace]
|
|
826
|
+
* @param {boolean} [screenSpace=drawScreenSpace]
|
|
702
827
|
* @param {CanvasRenderingContext2D|OffscreenCanvasRenderingContext2D} [context]
|
|
703
828
|
* @memberof Draw */
|
|
704
|
-
function drawLineList(points, width=.1, color=WHITE, wrap=false, pos=vec2(), angle=0, useWebGL=glEnable, screenSpace=
|
|
829
|
+
function drawLineList(points, width=.1, color=WHITE, wrap=false, pos=vec2(), angle=0, useWebGL=glEnable, screenSpace=drawScreenSpace, context)
|
|
705
830
|
{
|
|
706
831
|
ASSERT(isArray(points), 'points must be an array');
|
|
707
832
|
ASSERT(isNumber(width), 'width must be a number');
|
|
@@ -710,12 +835,17 @@ function drawLineList(points, width=.1, color=WHITE, wrap=false, pos=vec2(), ang
|
|
|
710
835
|
ASSERT(isNumber(angle), 'angle must be a number');
|
|
711
836
|
ASSERT(!context || !useWebGL, 'context only supported in canvas 2D mode');
|
|
712
837
|
|
|
838
|
+
if (headlessMode && !context) return; // headless has no canvas, only a context passed in is drawn to
|
|
839
|
+
|
|
713
840
|
if (useWebGL && glEnable)
|
|
714
841
|
{
|
|
715
842
|
ASSERT(!!glContext, 'WebGL is not enabled!');
|
|
716
843
|
let size = vec2(1);
|
|
717
844
|
if (screenSpace)
|
|
845
|
+
{
|
|
846
|
+
if (glSkipScreenSpace) return;
|
|
718
847
|
[pos, size, angle] = screenToWorldTransform(pos, size, angle);
|
|
848
|
+
}
|
|
719
849
|
glDrawOutlineTransform(points, color.rgbaInt(), width, pos.x, pos.y, size.x, size.y, angle, wrap);
|
|
720
850
|
}
|
|
721
851
|
else
|
|
@@ -725,8 +855,11 @@ function drawLineList(points, width=.1, color=WHITE, wrap=false, pos=vec2(), ang
|
|
|
725
855
|
++primitiveCount;
|
|
726
856
|
drawCanvas2D(pos, vec2(1), angle, false, (context)=>
|
|
727
857
|
{
|
|
858
|
+
// Canvas2D ignores a width of 0 or below and keeps the last one, WebGL draws none for 0
|
|
859
|
+
// and a negative width as its size, the outline turned inside out
|
|
860
|
+
if (!width) return;
|
|
728
861
|
context.strokeStyle = color.toString();
|
|
729
|
-
context.lineWidth = width;
|
|
862
|
+
context.lineWidth = abs(width);
|
|
730
863
|
context.beginPath();
|
|
731
864
|
for (let i=0; i<points.length; ++i)
|
|
732
865
|
{
|
|
@@ -747,14 +880,15 @@ function drawLineList(points, width=.1, color=WHITE, wrap=false, pos=vec2(), ang
|
|
|
747
880
|
* @param {Vector2} [pos=vec2()] - Offset to apply
|
|
748
881
|
* @param {number} [angle] - Angle to rotate by
|
|
749
882
|
* @param {boolean} [useWebGL=glEnable]
|
|
750
|
-
* @param {boolean} [screenSpace]
|
|
883
|
+
* @param {boolean} [screenSpace=drawScreenSpace]
|
|
751
884
|
* @param {CanvasRenderingContext2D|OffscreenCanvasRenderingContext2D} [context]
|
|
752
885
|
* @memberof Draw */
|
|
753
|
-
function drawLine(posA, posB, width=.1, color=WHITE, pos=vec2(), angle=0, useWebGL=glEnable, screenSpace=
|
|
886
|
+
function drawLine(posA, posB, width=.1, color=WHITE, pos=vec2(), angle=0, useWebGL=glEnable, screenSpace=drawScreenSpace, context)
|
|
754
887
|
{
|
|
755
888
|
const halfDelta = vec2((posB.x - posA.x)/2, (posB.y - posA.y)/2);
|
|
756
889
|
const size = vec2(width, halfDelta.length()*2);
|
|
757
|
-
|
|
890
|
+
const middle = posA.add(halfDelta);
|
|
891
|
+
pos = pos.add(angle ? middle.rotate(screenSpace ? -angle : angle) : middle); // screen y is down, so it turns back
|
|
758
892
|
if (screenSpace)
|
|
759
893
|
halfDelta.y *= -1; // flip angle Y if screen space
|
|
760
894
|
angle += halfDelta.angle();
|
|
@@ -770,10 +904,10 @@ function drawLine(posA, posB, width=.1, color=WHITE, pos=vec2(), angle=0, useWeb
|
|
|
770
904
|
* @param {Color} [lineColor=BLACK]
|
|
771
905
|
* @param {number} [angle]
|
|
772
906
|
* @param {boolean} [useWebGL=glEnable]
|
|
773
|
-
* @param {boolean} [screenSpace]
|
|
907
|
+
* @param {boolean} [screenSpace=drawScreenSpace]
|
|
774
908
|
* @param {CanvasRenderingContext2D|OffscreenCanvasRenderingContext2D} [context]
|
|
775
909
|
* @memberof Draw */
|
|
776
|
-
function drawRegularPoly(pos, size=vec2(1), sides=3, color=WHITE, lineWidth=0, lineColor=BLACK, angle=0, useWebGL=glEnable, screenSpace=
|
|
910
|
+
function drawRegularPoly(pos, size=vec2(1), sides=3, color=WHITE, lineWidth=0, lineColor=BLACK, angle=0, useWebGL=glEnable, screenSpace=drawScreenSpace, context)
|
|
777
911
|
{
|
|
778
912
|
ASSERT(isVector2(size), 'size must be a vec2');
|
|
779
913
|
ASSERT(isNumber(sides), 'sides must be a number');
|
|
@@ -790,6 +924,8 @@ function drawRegularPoly(pos, size=vec2(1), sides=3, color=WHITE, lineWidth=0, l
|
|
|
790
924
|
}
|
|
791
925
|
|
|
792
926
|
/** Draw colored polygon using passed in points
|
|
927
|
+
* - WebGL fills a polygon whose edges do not cross, concave or not; a self crossing one, like a star through its
|
|
928
|
+
* outer points, fills wrong there, so draw it as its simple outline or in parts
|
|
793
929
|
* @param {Array<Vector2>} points - Array of Vector2 points
|
|
794
930
|
* @param {Color} [color=WHITE]
|
|
795
931
|
* @param {number} [lineWidth]
|
|
@@ -797,10 +933,10 @@ function drawRegularPoly(pos, size=vec2(1), sides=3, color=WHITE, lineWidth=0, l
|
|
|
797
933
|
* @param {Vector2} [pos=vec2()] - Offset to apply
|
|
798
934
|
* @param {number} [angle] - Angle to rotate by
|
|
799
935
|
* @param {boolean} [useWebGL=glEnable]
|
|
800
|
-
* @param {boolean} [screenSpace]
|
|
936
|
+
* @param {boolean} [screenSpace=drawScreenSpace]
|
|
801
937
|
* @param {CanvasRenderingContext2D|OffscreenCanvasRenderingContext2D} [context]
|
|
802
938
|
* @memberof Draw */
|
|
803
|
-
function drawPoly(points, color=WHITE, lineWidth=0, lineColor=BLACK, pos=vec2(), angle=0, useWebGL=glEnable, screenSpace=
|
|
939
|
+
function drawPoly(points, color=WHITE, lineWidth=0, lineColor=BLACK, pos=vec2(), angle=0, useWebGL=glEnable, screenSpace=drawScreenSpace, context=undefined)
|
|
804
940
|
{
|
|
805
941
|
ASSERT(isVector2(pos), 'pos must be a vec2');
|
|
806
942
|
ASSERT(isArray(points), 'points must be an array');
|
|
@@ -809,18 +945,25 @@ function drawPoly(points, color=WHITE, lineWidth=0, lineColor=BLACK, pos=vec2(),
|
|
|
809
945
|
ASSERT(isNumber(angle), 'angle must be a number');
|
|
810
946
|
ASSERT(!context || !useWebGL, 'context only supported in canvas 2D mode');
|
|
811
947
|
|
|
948
|
+
if (headlessMode && !context) return; // headless has no canvas, only a context passed in is drawn to
|
|
949
|
+
|
|
812
950
|
if (useWebGL && glEnable)
|
|
813
951
|
{
|
|
814
952
|
ASSERT(!!glContext, 'WebGL is not enabled!');
|
|
815
953
|
let size = vec2(1);
|
|
816
954
|
if (screenSpace)
|
|
955
|
+
{
|
|
956
|
+
if (glSkipScreenSpace) return;
|
|
817
957
|
[pos, size, angle] = screenToWorldTransform(pos, size, angle);
|
|
958
|
+
}
|
|
818
959
|
glDrawPointsTransform(points, color.rgbaInt(), pos.x, pos.y, size.x, size.y, angle);
|
|
819
960
|
if (lineWidth > 0)
|
|
820
961
|
glDrawOutlineTransform(points, lineColor.rgbaInt(), lineWidth, pos.x, pos.y, size.x, size.y, angle);
|
|
821
962
|
}
|
|
822
963
|
else
|
|
823
964
|
{
|
|
965
|
+
++drawCount;
|
|
966
|
+
++primitiveCount;
|
|
824
967
|
drawCanvas2D(pos, vec2(1), angle, false, context=>
|
|
825
968
|
{
|
|
826
969
|
context.fillStyle = color.toString();
|
|
@@ -829,7 +972,7 @@ function drawPoly(points, color=WHITE, lineWidth=0, lineColor=BLACK, pos=vec2(),
|
|
|
829
972
|
context.lineTo(point.x, point.y);
|
|
830
973
|
context.closePath();
|
|
831
974
|
context.fill();
|
|
832
|
-
if (lineWidth)
|
|
975
|
+
if (lineWidth > 0) // as in WebGL, Canvas2D would stroke a width below 0 with the last one
|
|
833
976
|
{
|
|
834
977
|
context.strokeStyle = lineColor.toString();
|
|
835
978
|
context.lineWidth = lineWidth;
|
|
@@ -839,6 +982,9 @@ function drawPoly(points, color=WHITE, lineWidth=0, lineColor=BLACK, pos=vec2(),
|
|
|
839
982
|
}
|
|
840
983
|
}
|
|
841
984
|
|
|
985
|
+
// the unit rings drawEllipse fills with in WebGL, one for each side count
|
|
986
|
+
const drawEllipseRings = new Map;
|
|
987
|
+
|
|
842
988
|
/** Draw colored ellipse using passed in point
|
|
843
989
|
* @param {Vector2} pos
|
|
844
990
|
* @param {Vector2} [size=vec2(1)] - Width and height diameter
|
|
@@ -847,10 +993,10 @@ function drawPoly(points, color=WHITE, lineWidth=0, lineColor=BLACK, pos=vec2(),
|
|
|
847
993
|
* @param {number} [lineWidth]
|
|
848
994
|
* @param {Color} [lineColor=BLACK]
|
|
849
995
|
* @param {boolean} [useWebGL=glEnable]
|
|
850
|
-
* @param {boolean} [screenSpace]
|
|
996
|
+
* @param {boolean} [screenSpace=drawScreenSpace]
|
|
851
997
|
* @param {CanvasRenderingContext2D|OffscreenCanvasRenderingContext2D} [context]
|
|
852
998
|
* @memberof Draw */
|
|
853
|
-
function drawEllipse(pos, size=vec2(1), color=WHITE, angle=0, lineWidth=0, lineColor=BLACK, useWebGL=glEnable, screenSpace=
|
|
999
|
+
function drawEllipse(pos, size=vec2(1), color=WHITE, angle=0, lineWidth=0, lineColor=BLACK, useWebGL=glEnable, screenSpace=drawScreenSpace, context)
|
|
854
1000
|
{
|
|
855
1001
|
ASSERT(isVector2(pos), 'pos must be a vec2');
|
|
856
1002
|
ASSERT(isVector2(size), 'size must be a vec2');
|
|
@@ -860,24 +1006,51 @@ function drawEllipse(pos, size=vec2(1), color=WHITE, angle=0, lineWidth=0, lineC
|
|
|
860
1006
|
ASSERT(lineWidth >= 0, 'lineWidth must be a positive value or 0');
|
|
861
1007
|
ASSERT(!context || !useWebGL, 'context only supported in canvas 2D mode');
|
|
862
1008
|
|
|
863
|
-
//
|
|
864
|
-
|
|
1009
|
+
if (headlessMode && !context) return; // headless has no canvas, only a context passed in is drawn to
|
|
1010
|
+
|
|
1011
|
+
// clamp line width to prevent artifacts, a negative size draws mirrored
|
|
1012
|
+
lineWidth = clamp(lineWidth, 0, min(abs(size.x), abs(size.y)));
|
|
865
1013
|
|
|
866
1014
|
if (useWebGL && glEnable)
|
|
867
1015
|
{
|
|
868
|
-
// draw as a regular polygon
|
|
869
1016
|
const sides = glCircleSides;
|
|
870
|
-
|
|
1017
|
+
if (lineWidth > 0)
|
|
1018
|
+
{
|
|
1019
|
+
// draw as a regular polygon, the outline is made from the points at their size
|
|
1020
|
+
drawRegularPoly(pos, size, sides, color, lineWidth, lineColor, angle, useWebGL, screenSpace, context);
|
|
1021
|
+
return;
|
|
1022
|
+
}
|
|
1023
|
+
|
|
1024
|
+
// a fill is a unit ring scaled, made once for each side count and already in strip order
|
|
1025
|
+
let ring = drawEllipseRings.get(sides);
|
|
1026
|
+
if (!ring)
|
|
1027
|
+
{
|
|
1028
|
+
const points = [];
|
|
1029
|
+
for (let i=sides; i--;)
|
|
1030
|
+
{
|
|
1031
|
+
const a = (i/sides)*PI*2;
|
|
1032
|
+
points.push(vec2(sin(a), cos(a)));
|
|
1033
|
+
}
|
|
1034
|
+
drawEllipseRings.set(sides, ring = glPolyStrip(points));
|
|
1035
|
+
}
|
|
1036
|
+
if (screenSpace)
|
|
1037
|
+
{
|
|
1038
|
+
if (glSkipScreenSpace) return;
|
|
1039
|
+
[pos, size, angle] = screenToWorldTransform(pos, size, angle);
|
|
1040
|
+
}
|
|
1041
|
+
glDrawPointsTransform(ring, color.rgbaInt(), pos.x, pos.y, size.x/2, size.y/2, angle, false);
|
|
871
1042
|
}
|
|
872
1043
|
else
|
|
873
1044
|
{
|
|
1045
|
+
++drawCount;
|
|
1046
|
+
++primitiveCount;
|
|
874
1047
|
drawCanvas2D(pos, vec2(1), angle, false, context=>
|
|
875
1048
|
{
|
|
876
1049
|
context.fillStyle = color.toString();
|
|
877
1050
|
context.beginPath();
|
|
878
|
-
context.ellipse(0, 0, size.x/2, size.y/2, 0, 0, 9);
|
|
1051
|
+
context.ellipse(0, 0, abs(size.x)/2, abs(size.y)/2, 0, 0, 9); // it throws on a negative radius
|
|
879
1052
|
context.fill();
|
|
880
|
-
if (lineWidth)
|
|
1053
|
+
if (lineWidth > 0)
|
|
881
1054
|
{
|
|
882
1055
|
context.strokeStyle = lineColor.toString();
|
|
883
1056
|
context.lineWidth = lineWidth;
|
|
@@ -894,15 +1067,16 @@ function drawEllipse(pos, size=vec2(1), color=WHITE, angle=0, lineWidth=0, lineC
|
|
|
894
1067
|
* @param {number} [lineWidth=0]
|
|
895
1068
|
* @param {Color} [lineColor=BLACK]
|
|
896
1069
|
* @param {boolean} [useWebGL=glEnable]
|
|
897
|
-
* @param {boolean} [screenSpace]
|
|
1070
|
+
* @param {boolean} [screenSpace=drawScreenSpace]
|
|
898
1071
|
* @param {CanvasRenderingContext2D|OffscreenCanvasRenderingContext2D} [context]
|
|
899
1072
|
* @memberof Draw */
|
|
900
|
-
function drawCircle(pos, size=1, color=WHITE, lineWidth=0, lineColor=BLACK, useWebGL=glEnable, screenSpace=
|
|
1073
|
+
function drawCircle(pos, size=1, color=WHITE, lineWidth=0, lineColor=BLACK, useWebGL=glEnable, screenSpace=drawScreenSpace, context)
|
|
901
1074
|
{
|
|
902
1075
|
ASSERT(isNumber(size), 'size must be a number');
|
|
903
1076
|
drawEllipse(pos, vec2(size), color, 0, lineWidth, lineColor, useWebGL, screenSpace, context);
|
|
904
1077
|
}
|
|
905
1078
|
|
|
1079
|
+
let drawEllipseGradientOffset = 0;
|
|
906
1080
|
/** Draw an ellipse filled with a radial gradient from the center to the rim
|
|
907
1081
|
* - Best when batched with other untextured polys
|
|
908
1082
|
* - If drawing mostly textured sprites, bake the gradient into a texture and use drawTile instead
|
|
@@ -913,11 +1087,10 @@ function drawCircle(pos, size=1, color=WHITE, lineWidth=0, lineColor=BLACK, useW
|
|
|
913
1087
|
* @param {Color} [colorOuter=CLEAR_WHITE]
|
|
914
1088
|
* @param {number} [angle]
|
|
915
1089
|
* @param {boolean} [useWebGL=glEnable]
|
|
916
|
-
* @param {boolean} [screenSpace]
|
|
1090
|
+
* @param {boolean} [screenSpace=drawScreenSpace]
|
|
917
1091
|
* @param {CanvasRenderingContext2D|OffscreenCanvasRenderingContext2D} [context]
|
|
918
1092
|
* @memberof Draw */
|
|
919
|
-
|
|
920
|
-
function drawEllipseGradient(pos, size=vec2(1), colorInner=WHITE, colorOuter=CLEAR_WHITE, angle=0, useWebGL=glEnable, screenSpace=false, context)
|
|
1093
|
+
function drawEllipseGradient(pos, size=vec2(1), colorInner=WHITE, colorOuter=CLEAR_WHITE, angle=0, useWebGL=glEnable, screenSpace=drawScreenSpace, context)
|
|
921
1094
|
{
|
|
922
1095
|
ASSERT(isVector2(pos), 'pos must be a vec2');
|
|
923
1096
|
ASSERT(isVector2(size), 'size must be a vec2');
|
|
@@ -925,17 +1098,15 @@ function drawEllipseGradient(pos, size=vec2(1), colorInner=WHITE, colorOuter=CLE
|
|
|
925
1098
|
ASSERT(isNumber(angle), 'angle must be a number');
|
|
926
1099
|
ASSERT(!context || !useWebGL, 'context only supported in canvas 2D mode');
|
|
927
1100
|
|
|
928
|
-
if (headlessMode) return;
|
|
1101
|
+
if (headlessMode && !context) return; // headless has no canvas, only a context passed in is drawn to
|
|
929
1102
|
|
|
930
1103
|
if (useWebGL && glEnable)
|
|
931
1104
|
{
|
|
932
1105
|
ASSERT(!!glContext, 'WebGL is not enabled!');
|
|
933
1106
|
if (screenSpace)
|
|
934
1107
|
{
|
|
935
|
-
|
|
936
|
-
pos =
|
|
937
|
-
size = size.scale(1/cameraScale);
|
|
938
|
-
angle += cameraAngle;
|
|
1108
|
+
if (glSkipScreenSpace) return;
|
|
1109
|
+
[pos, size, angle] = screenToWorldTransform(pos, size, angle);
|
|
939
1110
|
}
|
|
940
1111
|
// fan as tristrip; rotate the boundary vertex by one slice per call
|
|
941
1112
|
// so back-to-back gradients at the same position have their hole
|
|
@@ -992,10 +1163,10 @@ function drawEllipseGradient(pos, size=vec2(1), colorInner=WHITE, colorOuter=CLE
|
|
|
992
1163
|
* @param {Color} [colorInner=WHITE]
|
|
993
1164
|
* @param {Color} [colorOuter=CLEAR_WHITE]
|
|
994
1165
|
* @param {boolean} [useWebGL=glEnable]
|
|
995
|
-
* @param {boolean} [screenSpace]
|
|
1166
|
+
* @param {boolean} [screenSpace=drawScreenSpace]
|
|
996
1167
|
* @param {CanvasRenderingContext2D|OffscreenCanvasRenderingContext2D} [context]
|
|
997
1168
|
* @memberof Draw */
|
|
998
|
-
function drawCircleGradient(pos, size=1, colorInner=WHITE, colorOuter=CLEAR_WHITE, useWebGL=glEnable, screenSpace=
|
|
1169
|
+
function drawCircleGradient(pos, size=1, colorInner=WHITE, colorOuter=CLEAR_WHITE, useWebGL=glEnable, screenSpace=drawScreenSpace, context)
|
|
999
1170
|
{
|
|
1000
1171
|
ASSERT(isNumber(size), 'size must be a number');
|
|
1001
1172
|
drawEllipseGradient(pos, vec2(size), colorInner, colorOuter, 0, useWebGL, screenSpace, context);
|
|
@@ -1013,19 +1184,21 @@ function drawCircleGradient(pos, size=1, colorInner=WHITE, colorOuter=CLEAR_WHIT
|
|
|
1013
1184
|
* (e.g. linear gradients) should flip their own Y endpoints accordingly.
|
|
1014
1185
|
* @param {Vector2} pos
|
|
1015
1186
|
* @param {Vector2} size
|
|
1016
|
-
* @param {number} angle
|
|
1187
|
+
* @param {number} [angle]
|
|
1017
1188
|
* @param {boolean} [mirror]
|
|
1018
|
-
* @param {Canvas2DDrawFunction} [drawFunction]
|
|
1019
|
-
* @param {boolean} [screenSpace=
|
|
1189
|
+
* @param {Canvas2DDrawFunction} [drawFunction] - Needed, marked optional only because the ones before it are
|
|
1190
|
+
* @param {boolean} [screenSpace=drawScreenSpace]
|
|
1020
1191
|
* @param {CanvasRenderingContext2D|OffscreenCanvasRenderingContext2D} [context=drawContext]
|
|
1021
1192
|
* @memberof Draw */
|
|
1022
|
-
function drawCanvas2D(pos, size, angle=0, mirror=false, drawFunction, screenSpace=
|
|
1193
|
+
function drawCanvas2D(pos, size, angle=0, mirror=false, drawFunction, screenSpace=drawScreenSpace, context=drawContext)
|
|
1023
1194
|
{
|
|
1024
1195
|
ASSERT(isVector2(pos), 'pos must be a vec2');
|
|
1025
1196
|
ASSERT(isVector2(size), 'size must be a vec2');
|
|
1026
1197
|
ASSERT(isNumber(angle), 'angle must be a number');
|
|
1027
1198
|
ASSERT(typeof drawFunction === 'function', 'drawFunction must be a function');
|
|
1028
1199
|
|
|
1200
|
+
if (headlessMode && !context) return; // headless has no canvas, only a context passed in is drawn to
|
|
1201
|
+
|
|
1029
1202
|
if (!screenSpace)
|
|
1030
1203
|
{
|
|
1031
1204
|
pos = worldToScreen(pos);
|
|
@@ -1051,7 +1224,7 @@ function drawCanvas2D(pos, size, angle=0, mirror=false, drawFunction, screenSpac
|
|
|
1051
1224
|
* @param {Color} [color=WHITE]
|
|
1052
1225
|
* @param {number} [lineWidth]
|
|
1053
1226
|
* @param {Color} [lineColor=BLACK]
|
|
1054
|
-
* @param {
|
|
1227
|
+
* @param {'left'|'center'|'right'} [textAlign='center']
|
|
1055
1228
|
* @param {string} [font=fontDefault]
|
|
1056
1229
|
* @param {string} [fontStyle]
|
|
1057
1230
|
* @param {number} [maxWidth]
|
|
@@ -1064,8 +1237,9 @@ function drawText(text, pos, size=1, color=WHITE, lineWidth=0, lineColor=BLACK,
|
|
|
1064
1237
|
pos = worldToScreen(pos);
|
|
1065
1238
|
size *= cameraScale;
|
|
1066
1239
|
lineWidth *= cameraScale;
|
|
1240
|
+
if (maxWidth !== undefined)
|
|
1241
|
+
maxWidth *= cameraScale;
|
|
1067
1242
|
angle -= cameraAngle;
|
|
1068
|
-
angle *= -1;
|
|
1069
1243
|
|
|
1070
1244
|
drawTextScreen(text, pos, size, color, lineWidth, lineColor, textAlign, font, fontStyle, maxWidth, angle, context);
|
|
1071
1245
|
}
|
|
@@ -1078,11 +1252,11 @@ function drawText(text, pos, size=1, color=WHITE, lineWidth=0, lineColor=BLACK,
|
|
|
1078
1252
|
* @param {Color} [color=WHITE]
|
|
1079
1253
|
* @param {number} [lineWidth]
|
|
1080
1254
|
* @param {Color} [lineColor=BLACK]
|
|
1081
|
-
* @param {
|
|
1255
|
+
* @param {'left'|'center'|'right'} [textAlign]
|
|
1082
1256
|
* @param {string} [font=fontDefault]
|
|
1083
1257
|
* @param {string} [fontStyle]
|
|
1084
1258
|
* @param {number} [maxWidth]
|
|
1085
|
-
* @param {number} [angle]
|
|
1259
|
+
* @param {number} [angle] - Clockwise, like the other screen space draws
|
|
1086
1260
|
* @param {CanvasRenderingContext2D|OffscreenCanvasRenderingContext2D} [context=drawContext]
|
|
1087
1261
|
* @memberof Draw */
|
|
1088
1262
|
function drawTextScreen(text, pos, size, color=WHITE, lineWidth=0, lineColor=BLACK, textAlign='center', font=fontDefault, fontStyle='', maxWidth, angle=0, context=drawContext)
|
|
@@ -1097,9 +1271,10 @@ function drawTextScreen(text, pos, size, color=WHITE, lineWidth=0, lineColor=BLA
|
|
|
1097
1271
|
ASSERT(isStringLike(font), 'font must be a string');
|
|
1098
1272
|
ASSERT(isStringLike(fontStyle), 'fontStyle must be a string');
|
|
1099
1273
|
ASSERT(isNumber(angle), 'angle must be a number');
|
|
1274
|
+
|
|
1275
|
+
if (headlessMode && !context) return; // headless has no canvas, only a context passed in is drawn to
|
|
1100
1276
|
|
|
1101
1277
|
const lines = (text+'').split('\n');
|
|
1102
|
-
const posY = pos.y - (lines.length-1) * size/2; // center vertically
|
|
1103
1278
|
// save before style mutations so caller's context state is preserved
|
|
1104
1279
|
context.save();
|
|
1105
1280
|
context.fillStyle = color.toString();
|
|
@@ -1108,9 +1283,9 @@ function drawTextScreen(text, pos, size, color=WHITE, lineWidth=0, lineColor=BLA
|
|
|
1108
1283
|
context.textAlign = textAlign;
|
|
1109
1284
|
context.font = fontStyle + ' ' + size + 'px '+ font;
|
|
1110
1285
|
context.textBaseline = 'middle';
|
|
1111
|
-
context.translate(pos.x,
|
|
1112
|
-
context.rotate(
|
|
1113
|
-
let yOffset =
|
|
1286
|
+
context.translate(pos.x + .5, pos.y + .5); // a screen position is the center of a pixel, as for every other draw
|
|
1287
|
+
context.rotate(angle);
|
|
1288
|
+
let yOffset = -(lines.length-1) * size/2; // center vertically
|
|
1114
1289
|
lines.forEach(line=>
|
|
1115
1290
|
{
|
|
1116
1291
|
lineWidth && context.strokeText(line, 0, yOffset, maxWidth);
|
|
@@ -1123,29 +1298,37 @@ function drawTextScreen(text, pos, size, color=WHITE, lineWidth=0, lineColor=BLA
|
|
|
1123
1298
|
///////////////////////////////////////////////////////////////////////////////
|
|
1124
1299
|
// Drawing utilities
|
|
1125
1300
|
|
|
1126
|
-
/** Load a texture at a specific index
|
|
1127
|
-
* @param {number} textureIndex - Index to store the texture at
|
|
1301
|
+
/** Load a texture at a specific index after engineInit, the images passed to engineInit load this way
|
|
1302
|
+
* @param {number} textureIndex - Index to store the texture at, an unused one
|
|
1128
1303
|
* @param {string} [src] - Image source path
|
|
1129
|
-
* @return {Promise}
|
|
1304
|
+
* @return {Promise<TextureInfo>} Resolves to the texture info once the image loads, or fails to with a warning
|
|
1130
1305
|
* @memberof Draw */
|
|
1131
1306
|
async function loadTexture(textureIndex, src)
|
|
1132
1307
|
{
|
|
1133
1308
|
ASSERT(isNumber(textureIndex), 'textureIndex must be a number');
|
|
1134
|
-
|
|
1309
|
+
// engineInit's empty placeholder in slot 0, when it was given no images, or a load that failed can be replaced
|
|
1310
|
+
const old = textureInfos[textureIndex];
|
|
1311
|
+
ASSERT(!old?.size.x, 'textureIndex is already loaded!');
|
|
1312
|
+
old?.destroyWebGLTexture();
|
|
1135
1313
|
ASSERT(!src || isStringLike(src), 'image src must be a string');
|
|
1136
1314
|
|
|
1137
1315
|
const image = new Image;
|
|
1138
1316
|
if (src)
|
|
1139
1317
|
{
|
|
1140
|
-
await new Promise(resolve =>
|
|
1318
|
+
await engineAddLoad(new Promise(resolve => // startup waits for it
|
|
1141
1319
|
{
|
|
1142
|
-
image.
|
|
1320
|
+
image.onload = resolve;
|
|
1321
|
+
image.onerror = ()=>
|
|
1322
|
+
{
|
|
1323
|
+
console.warn('failed to load image: ' + src); // in release too, like the WebGL warning
|
|
1324
|
+
resolve();
|
|
1325
|
+
};
|
|
1143
1326
|
image.crossOrigin = 'anonymous';
|
|
1144
1327
|
image.src = src;
|
|
1145
|
-
});
|
|
1328
|
+
}));
|
|
1146
1329
|
}
|
|
1147
1330
|
|
|
1148
|
-
textureInfos[textureIndex] = new TextureInfo(image);
|
|
1331
|
+
return textureInfos[textureIndex] = new TextureInfo(image);
|
|
1149
1332
|
}
|
|
1150
1333
|
|
|
1151
1334
|
/** Convert from screen to world space coordinates
|
|
@@ -1212,7 +1395,7 @@ function screenToWorldDelta(screenDelta)
|
|
|
1212
1395
|
return new Vector2(x, y);
|
|
1213
1396
|
}
|
|
1214
1397
|
|
|
1215
|
-
/** Convert from
|
|
1398
|
+
/** Convert from world to screen space coordinates for a directional vector (no translation)
|
|
1216
1399
|
* @param {Vector2} worldDelta
|
|
1217
1400
|
* @return {Vector2}
|
|
1218
1401
|
* @memberof Draw */
|
|
@@ -1256,6 +1439,14 @@ function screenToWorldTransform(screenPos, screenSize, screenAngle=0)
|
|
|
1256
1439
|
* @memberof Draw */
|
|
1257
1440
|
function getCameraSize() { return mainCanvasSize.scale(1/cameraScale); }
|
|
1258
1441
|
|
|
1442
|
+
/** Padding for each side of a rectangle, the sides left out are 0
|
|
1443
|
+
* @typedef {Object} CameraFitSides
|
|
1444
|
+
* @property {number} [top]
|
|
1445
|
+
* @property {number} [right]
|
|
1446
|
+
* @property {number} [bottom]
|
|
1447
|
+
* @property {number} [left]
|
|
1448
|
+
* @memberof Draw */
|
|
1449
|
+
|
|
1259
1450
|
/** Fit the camera to a rectangle in world space by setting cameraPos and cameraScale
|
|
1260
1451
|
* - worldMargin pads the content rectangle in world units, so the gap scales with the content on resize
|
|
1261
1452
|
* - screenInset reserves space in screen pixels on each viewport edge (for example a HUD band) and
|
|
@@ -1264,8 +1455,8 @@ function getCameraSize() { return mainCanvasSize.scale(1/cameraScale); }
|
|
|
1264
1455
|
* or an object with any of {top, right, bottom, left}
|
|
1265
1456
|
* @param {Vector2} center - Center of the rectangle in world space
|
|
1266
1457
|
* @param {Vector2} size - Size of the rectangle in world space
|
|
1267
|
-
* @param {number|Vector2|
|
|
1268
|
-
* @param {number|Vector2|
|
|
1458
|
+
* @param {number|Vector2|CameraFitSides} [worldMargin] - World space padding added around the content rectangle
|
|
1459
|
+
* @param {number|Vector2|CameraFitSides} [screenInset] - Screen space padding in pixels reserved on each viewport edge
|
|
1269
1460
|
* @return {number} - The new camera scale
|
|
1270
1461
|
* @memberof Draw */
|
|
1271
1462
|
function cameraFit(center, size, worldMargin, screenInset)
|
|
@@ -1285,12 +1476,15 @@ function cameraFit(center, size, worldMargin, screenInset)
|
|
|
1285
1476
|
if (!(worldW > 0 && worldH > 0 && viewW > 0 && viewH > 0))
|
|
1286
1477
|
return cameraScale;
|
|
1287
1478
|
|
|
1288
|
-
// scale to fit the padded content
|
|
1289
|
-
|
|
1479
|
+
// scale to fit the padded content, measured along the camera's axes when it is turned
|
|
1480
|
+
const c = cos(cameraAngle), s = sin(cameraAngle);
|
|
1481
|
+
const fitW = abs(c) * worldW + abs(s) * worldH, fitH = abs(s) * worldW + abs(c) * worldH;
|
|
1482
|
+
cameraScale = min(viewW / fitW, viewH / fitH);
|
|
1290
1483
|
|
|
1291
|
-
// calculate offset vectors
|
|
1484
|
+
// calculate offset vectors, the inset is on the screen, so it turns back into the world with the camera
|
|
1292
1485
|
const marginVector = vec2(margin.right - margin.left, margin.top - margin.bottom).scale(.5);
|
|
1293
|
-
const
|
|
1486
|
+
const ix = (inset.right - inset.left) * .5 / cameraScale, iy = (inset.top - inset.bottom) * .5 / cameraScale;
|
|
1487
|
+
const insetVector = vec2(ix * c + iy * s, iy * c - ix * s);
|
|
1294
1488
|
|
|
1295
1489
|
// apply the offsets and return camera scale
|
|
1296
1490
|
cameraPos = center.add(marginVector).add(insetVector);
|
|
@@ -1316,7 +1510,7 @@ function cameraFit(center, size, worldMargin, screenInset)
|
|
|
1316
1510
|
* If size is a Vector2, uses the length as diameter
|
|
1317
1511
|
* This can be used to cull offscreen objects from render or update
|
|
1318
1512
|
* @param {Vector2} pos - world space position
|
|
1319
|
-
* @param {Vector2|number} size - world space size or diameter
|
|
1513
|
+
* @param {Vector2|number} [size] - world space size or diameter
|
|
1320
1514
|
* @return {boolean}
|
|
1321
1515
|
* @memberof Draw */
|
|
1322
1516
|
function isOnScreen(pos, size=0)
|
|
@@ -1327,8 +1521,7 @@ function isOnScreen(pos, size=0)
|
|
|
1327
1521
|
// cameraScale of 0 collapses world coords; nothing is visible
|
|
1328
1522
|
if (!cameraScale) return false;
|
|
1329
1523
|
|
|
1330
|
-
//
|
|
1331
|
-
// pos = worldToScreen(pos);
|
|
1524
|
+
// circle on screen test, worldToScreen inlined in doubled screen units so it compares against the full canvas size
|
|
1332
1525
|
let x = pos.x - cameraPos.x;
|
|
1333
1526
|
let y = pos.y - cameraPos.y;
|
|
1334
1527
|
if (cameraAngle)
|
|
@@ -1402,6 +1595,31 @@ function combineCanvases()
|
|
|
1402
1595
|
mainContext.restore();
|
|
1403
1596
|
}
|
|
1404
1597
|
|
|
1598
|
+
// tint straight color pixels in place, rgb times color, plus additiveColor with alpha when it adds anything;
|
|
1599
|
+
// returns true when color.a went into the alpha, so it is not applied again as globalAlpha
|
|
1600
|
+
function tintImageData(data, color, additiveColor)
|
|
1601
|
+
{
|
|
1602
|
+
if (additiveColor && !isBlack(additiveColor))
|
|
1603
|
+
{
|
|
1604
|
+
// multiply + additive (slower), color.a is baked into the alpha channel here
|
|
1605
|
+
const colorMultiply = [color.r, color.g, color.b, color.a];
|
|
1606
|
+
const colorAdd = [additiveColor.r * 255, additiveColor.g * 255,
|
|
1607
|
+
additiveColor.b * 255, additiveColor.a * 255];
|
|
1608
|
+
for (let i = 0; i < data.length; ++i)
|
|
1609
|
+
data[i] = data[i] * colorMultiply[i&3] + colorAdd[i&3] |0;
|
|
1610
|
+
return true;
|
|
1611
|
+
}
|
|
1612
|
+
|
|
1613
|
+
// RGB only, faster — alpha left intact for the caller
|
|
1614
|
+
for (let i = 0; i < data.length; i+=4)
|
|
1615
|
+
{
|
|
1616
|
+
data[i ] *= color.r;
|
|
1617
|
+
data[i+1] *= color.g;
|
|
1618
|
+
data[i+2] *= color.b;
|
|
1619
|
+
}
|
|
1620
|
+
return false;
|
|
1621
|
+
}
|
|
1622
|
+
|
|
1405
1623
|
// Internal: bake a color/additive-color tint into workReadCanvas at the
|
|
1406
1624
|
// image's native resolution. Returns the work canvas, suitable for
|
|
1407
1625
|
// passing to context.createPattern. Used by drawTextureWrapped's
|
|
@@ -1415,45 +1633,27 @@ function bakeTintedImage(image, color, additiveColor)
|
|
|
1415
1633
|
workReadContext.drawImage(image, 0, 0);
|
|
1416
1634
|
|
|
1417
1635
|
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
|
-
}
|
|
1636
|
+
tintImageData(imageData.data, color, additiveColor);
|
|
1438
1637
|
workReadContext.putImageData(imageData, 0, 0);
|
|
1439
1638
|
return workReadCanvas;
|
|
1440
1639
|
}
|
|
1441
1640
|
|
|
1442
|
-
/**
|
|
1443
|
-
* This is slower
|
|
1444
|
-
|
|
1445
|
-
|
|
1446
|
-
|
|
1447
|
-
|
|
1448
|
-
|
|
1449
|
-
|
|
1450
|
-
|
|
1451
|
-
|
|
1452
|
-
|
|
1453
|
-
|
|
1454
|
-
|
|
1455
|
-
|
|
1456
|
-
|
|
1641
|
+
/** Internal: draw an image with color and additive color applied in Canvas2D, drawTile calls it
|
|
1642
|
+
* This is slower than normal drawImage when color is applied
|
|
1643
|
+
* @ignore
|
|
1644
|
+
* @param {CanvasRenderingContext2D|OffscreenCanvasRenderingContext2D} context
|
|
1645
|
+
* @param {HTMLImageElement|HTMLCanvasElement|OffscreenCanvas|ImageBitmap} image
|
|
1646
|
+
* @param {number} sx
|
|
1647
|
+
* @param {number} sy
|
|
1648
|
+
* @param {number} sWidth
|
|
1649
|
+
* @param {number} sHeight
|
|
1650
|
+
* @param {number} dx
|
|
1651
|
+
* @param {number} dy
|
|
1652
|
+
* @param {number} dWidth
|
|
1653
|
+
* @param {number} dHeight
|
|
1654
|
+
* @param {Color} color
|
|
1655
|
+
* @param {Color} [additiveColor]
|
|
1656
|
+
* @param {number} [bleed] - How many pixels to shrink the source, used to fix bleeding
|
|
1457
1657
|
* @memberof Draw */
|
|
1458
1658
|
function drawImageColor(context, image, sx, sy, sWidth, sHeight, dx, dy, dWidth, dHeight, color, additiveColor, bleed=0)
|
|
1459
1659
|
{
|
|
@@ -1463,9 +1663,9 @@ function drawImageColor(context, image, sx, sy, sWidth, sHeight, dx, dy, dWidth,
|
|
|
1463
1663
|
sHeight = max(1,sHeight|0);
|
|
1464
1664
|
const sWidth2 = sWidth - 2*bleed;
|
|
1465
1665
|
const sHeight2 = sHeight - 2*bleed;
|
|
1466
|
-
if (!canvasColorTiles ||
|
|
1666
|
+
if (!canvasColorTiles || isWhite(color) && (!additiveColor || isBlack(additiveColor)))
|
|
1467
1667
|
{
|
|
1468
|
-
// white
|
|
1668
|
+
// white tint and nothing added, the texels are drawn as they are
|
|
1469
1669
|
context.globalAlpha = color.a;
|
|
1470
1670
|
context.drawImage(image, sx+sx2, sy+sy2, sWidth2, sHeight2, dx, dy, dWidth, dHeight);
|
|
1471
1671
|
context.globalAlpha = 1;
|
|
@@ -1479,31 +1679,11 @@ function drawImageColor(context, image, sx, sy, sWidth, sHeight, dx, dy, dWidth,
|
|
|
1479
1679
|
|
|
1480
1680
|
// tint image using offscreen work context
|
|
1481
1681
|
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
|
-
}
|
|
1682
|
+
const alphaBaked = tintImageData(imageData.data, color, additiveColor);
|
|
1683
|
+
workReadContext.putImageData(imageData, 0, 0);
|
|
1684
|
+
if (!alphaBaked) context.globalAlpha = color.a;
|
|
1685
|
+
context.drawImage(workReadCanvas, sx2, sy2, sWidth2, sHeight2, dx, dy, dWidth, dHeight);
|
|
1686
|
+
if (!alphaBaked) context.globalAlpha = 1;
|
|
1507
1687
|
}
|
|
1508
1688
|
}
|
|
1509
1689
|
|
|
@@ -1556,7 +1736,7 @@ let engineImageFont;
|
|
|
1556
1736
|
* const font = engineImageFont;
|
|
1557
1737
|
*
|
|
1558
1738
|
* // draw text
|
|
1559
|
-
* font.drawTextScreen('LittleJS\nHello World!', vec2(200, 50));
|
|
1739
|
+
* font.drawTextScreen('LittleJS\nHello World!', vec2(200, 50), 16);
|
|
1560
1740
|
*/
|
|
1561
1741
|
class ImageFont
|
|
1562
1742
|
{
|
|
@@ -1568,14 +1748,15 @@ class ImageFont
|
|
|
1568
1748
|
ASSERT(!!tileInfo, 'tileInfo is required for ImageFont');
|
|
1569
1749
|
|
|
1570
1750
|
/** @property {TileInfo} - Tile info for the font */
|
|
1571
|
-
this.tileInfo = tileInfo
|
|
1751
|
+
this.tileInfo = tileInfo; // kept, not copied, so a loadSprite tile filled in once it loads is seen
|
|
1572
1752
|
}
|
|
1573
1753
|
|
|
1574
1754
|
/** Draw text in world space using the image font
|
|
1755
|
+
* - The text stays upright and ignores cameraAngle, each glyph is snapped to whole screen pixels to keep it crisp
|
|
1575
1756
|
* @param {string|number} text
|
|
1576
1757
|
* @param {Vector2} pos
|
|
1577
1758
|
* @param {Vector2|number} [size]
|
|
1578
|
-
* @param {boolean} [center=true]
|
|
1759
|
+
* @param {boolean} [center=true] - center each line on pos, and the lines of multi-line text around it
|
|
1579
1760
|
* @param {Color} [color=WHITE]
|
|
1580
1761
|
* @param {boolean} [useWebGL=glEnable]
|
|
1581
1762
|
* @param {CanvasRenderingContext2D|OffscreenCanvasRenderingContext2D} [context]
|
|
@@ -1593,14 +1774,18 @@ class ImageFont
|
|
|
1593
1774
|
}
|
|
1594
1775
|
else
|
|
1595
1776
|
size = size.scale(cameraScale);
|
|
1777
|
+
// world text, only drawn through screen space, so it still casts in a pass that skips screen space draws
|
|
1778
|
+
const skip = glSkipScreenSpace;
|
|
1779
|
+
glSkipScreenSpace = false;
|
|
1596
1780
|
this.drawTextScreen(text, worldToScreen(pos), size, center, color, useWebGL, context);
|
|
1781
|
+
glSkipScreenSpace = skip;
|
|
1597
1782
|
}
|
|
1598
1783
|
|
|
1599
1784
|
/** Draw text in screen space using the image font
|
|
1600
1785
|
* @param {string|number} text
|
|
1601
1786
|
* @param {Vector2} pos
|
|
1602
1787
|
* @param {Vector2|number} size
|
|
1603
|
-
* @param {boolean} [center]
|
|
1788
|
+
* @param {boolean} [center] - center each line on pos, and the lines of multi-line text around it
|
|
1604
1789
|
* @param {Color} [color=WHITE]
|
|
1605
1790
|
* @param {boolean} [useWebGL=glEnable]
|
|
1606
1791
|
* @param {CanvasRenderingContext2D|OffscreenCanvasRenderingContext2D} [context]
|
|
@@ -1610,35 +1795,27 @@ class ImageFont
|
|
|
1610
1795
|
ASSERT(isStringLike(text), 'text must be a string');
|
|
1611
1796
|
ASSERT(isVector2(pos), 'pos must be a vec2');
|
|
1612
1797
|
ASSERT(isVector2(size) || typeof size === 'number', 'size must be a vec2 or number');
|
|
1798
|
+
ASSERT(typeof center === 'boolean', 'center must be a boolean, the color comes after it, unlike drawText');
|
|
1613
1799
|
ASSERT(isColor(color), 'color must be a color');
|
|
1614
1800
|
|
|
1615
1801
|
// if size is a number, make it a vector
|
|
1616
|
-
|
|
1802
|
+
const glyphSize = typeof size === 'number' ? new Vector2(size, size) : size;
|
|
1617
1803
|
|
|
1618
|
-
// precache objects for drawing
|
|
1804
|
+
// precache objects for drawing, a copy of the tile info each glyph moves, the font's own stays put
|
|
1619
1805
|
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)=>
|
|
1806
|
+
const tileInfo = this.tileInfo.frame(0);
|
|
1807
|
+
|
|
1808
|
+
// draw each line of text, centered vertically like drawTextScreen when center is set
|
|
1809
|
+
const lines = (text+'').split('\n');
|
|
1810
|
+
const centerOffsetY = center ? (lines.length-1) * glyphSize.y / 2 : 0;
|
|
1811
|
+
lines.forEach((line, j)=>
|
|
1628
1812
|
{
|
|
1629
|
-
const centerOffset = center ? (line.length-1) *
|
|
1813
|
+
const centerOffset = center ? (line.length-1) * glyphSize.x / 2 : 0;
|
|
1630
1814
|
for (let i=line.length; i--;)
|
|
1631
1815
|
{
|
|
1632
|
-
// get the
|
|
1816
|
+
// get the glyph, out of range characters use the last one
|
|
1633
1817
|
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;
|
|
1818
|
+
this.getGlyphPos(charCode < 32 || charCode > 127 ? 95 : charCode - 32, tileInfo.pos);
|
|
1642
1819
|
|
|
1643
1820
|
// snap the glyph edges to whole pixels
|
|
1644
1821
|
// tiles are drawn from their center, so snapping the center
|
|
@@ -1647,12 +1824,28 @@ class ImageFont
|
|
|
1647
1824
|
// no pixel center inside it and is not rasterized at all
|
|
1648
1825
|
// ceil picks the nearest aligned position, breaking ties
|
|
1649
1826
|
// 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,
|
|
1827
|
+
drawPos.x = ceil(pos.x + i * glyphSize.x - centerOffset - glyphSize.x/2) + glyphSize.x/2 - .5;
|
|
1828
|
+
drawPos.y = ceil(pos.y + j * glyphSize.y - centerOffsetY - glyphSize.y/2) + glyphSize.y/2 - .5;
|
|
1829
|
+
drawTile(drawPos, glyphSize, tileInfo, color, 0, false, undefined, useWebGL, true, context);
|
|
1653
1830
|
}
|
|
1654
1831
|
});
|
|
1655
1832
|
}
|
|
1833
|
+
|
|
1834
|
+
/** Get where a glyph sits in the texture: counted in the font's own columns when its tile has them, like a font
|
|
1835
|
+
* packed by loadSprite, otherwise along the texture's grid from the font's first tile, the way tile() lays it out
|
|
1836
|
+
* @param {number} index - Glyph number, 0 is the space and the characters follow in ASCII order
|
|
1837
|
+
* @param {Vector2} [pos] - Written into and returned, for a loop that places many
|
|
1838
|
+
* @return {Vector2} */
|
|
1839
|
+
getGlyphPos(index, pos=new Vector2)
|
|
1840
|
+
{
|
|
1841
|
+
const t = this.tileInfo, padding = t.padding;
|
|
1842
|
+
const w = t.size.x + padding*2, h = t.size.y + padding*2;
|
|
1843
|
+
if (t.columns)
|
|
1844
|
+
return pos.set(t.pos.x + index % t.columns * w, t.pos.y + (index / t.columns |0) * h);
|
|
1845
|
+
const columns = t.textureInfo.size.x / w |0;
|
|
1846
|
+
const glyph = ((t.pos.y - padding) / h |0) * columns + ((t.pos.x - padding) / w |0) + index;
|
|
1847
|
+
return pos.set(glyph % columns * w + padding, (glyph / columns |0) * h + padding);
|
|
1848
|
+
}
|
|
1656
1849
|
}
|
|
1657
1850
|
|
|
1658
1851
|
// load engine font, called automatically on startup
|