littlejsengine 1.24.1 → 1.24.3
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/CHANGELOG.md +78 -1
- package/EDITOR.md +2 -0
- package/FAQ.md +13 -2
- package/README.md +1 -1
- package/REFERENCE.md +22 -7
- package/dist/littlejs.d.ts +204 -118
- package/dist/littlejs.esm.js +2479 -1983
- package/dist/littlejs.esm.min.js +3 -3
- package/dist/littlejs.js +2476 -1983
- package/dist/littlejs.min.js +2 -2
- package/dist/littlejs.release.js +2434 -1990
- package/package.json +1 -1
- package/plugins/box2d.js +9 -5
- package/plugins/gltf.js +67 -9
- package/plugins/lightSystem.js +1 -1
- package/plugins/math3d.js +1014 -1013
- package/plugins/medalSystem.js +7 -11
- package/plugins/particleEffects.js +2 -2
- package/plugins/pathFinder.js +2 -2
- package/plugins/pluginExport.js +1 -0
- package/plugins/render3d.js +131 -55
- package/plugins/render3dEditor.js +24 -10
- package/plugins/render3dExtras.js +41 -25
- package/plugins/render3dLevel.js +21 -12
- package/plugins/render3dVoxels.js +2 -2
- package/plugins/textureSheet.js +109 -7
- package/plugins/tweakables.js +1 -1
- package/plugins/tweenSystem.js +2 -2
- package/plugins/uiSystem.js +6 -2
- package/src/engine.js +60 -23
- package/src/engineAudio.js +16 -6
- package/src/engineBuild.mjs +49 -2
- package/src/engineDebug.js +4 -1
- package/src/engineDraw.js +40 -25
- package/src/engineEditor.js +82 -50
- package/src/engineExport.js +2 -0
- package/src/engineInput.js +21 -4
- package/src/engineMath.js +6 -2
- package/src/engineObject.js +675 -675
- package/src/engineParticles.js +1 -1
- package/src/engineSettings.js +16 -0
- package/src/engineTileLayer.js +85 -22
- package/src/engineUtilities.js +20 -8
- package/src/engineWebGL.js +15 -11
package/src/engine.js
CHANGED
|
@@ -32,7 +32,7 @@ const engineName = 'LittleJS';
|
|
|
32
32
|
* @type {string}
|
|
33
33
|
* @default
|
|
34
34
|
* @memberof Engine */
|
|
35
|
-
const engineVersion = '1.24.
|
|
35
|
+
const engineVersion = '1.24.3';
|
|
36
36
|
|
|
37
37
|
/** Frames per second to update
|
|
38
38
|
* @type {number}
|
|
@@ -165,6 +165,7 @@ let timeFixedStart = 0, frameFixedStart = 0;
|
|
|
165
165
|
let windowWidthLast = 0, windowHeightLast = 0, windowPixelRatioLast = 0;
|
|
166
166
|
let engineUpdateInternal; // assigned by engineInit so engineStep can drive it
|
|
167
167
|
let engineFrameScheduled = false; // a frame of the loop is asked for and has not run yet
|
|
168
|
+
let engineFrameErrorLast; // the last error a release build's loop went on past, as text
|
|
168
169
|
|
|
169
170
|
// the pairs of objects asked about a collision this update, so the other's own physics does not ask again: each
|
|
170
171
|
// asker's others, with true for a pair both said to resolve, and false for one left overlapping, ignored or only
|
|
@@ -418,20 +419,30 @@ async function engineInit(gameInit, gameUpdate, gameUpdatePost, gameRender, game
|
|
|
418
419
|
frameTimeBufferMS = 0;
|
|
419
420
|
}
|
|
420
421
|
|
|
421
|
-
// update multiple frames if necessary in case of slow framerate
|
|
422
|
-
|
|
422
|
+
// update multiple frames if necessary in case of slow framerate; a tick that throws still spends its time
|
|
423
|
+
// and the smoothing still comes back, so a release build going on past an error each tick keeps its rate
|
|
424
|
+
try
|
|
423
425
|
{
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
426
|
+
while (frameTimeBufferMS >= 0)
|
|
427
|
+
{
|
|
428
|
+
try
|
|
429
|
+
{
|
|
430
|
+
// read again each tick, so a pause set by the game stops the rest of this frame's catch-up ticks
|
|
431
|
+
const frozenTick = paused || !(timeScale * debugScale);
|
|
432
|
+
|
|
433
|
+
// increment frame and update time, frozen does not advance time
|
|
434
|
+
if (!frozenTick)
|
|
435
|
+
time = timeFixedStart + (frame++ - frameFixedStart) / frameRate;
|
|
436
|
+
engineTick(frozenTick);
|
|
437
|
+
}
|
|
438
|
+
finally { frameTimeBufferMS -= 1e3 / frameRate; }
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
finally
|
|
442
|
+
{
|
|
443
|
+
// add the time smoothing back in
|
|
444
|
+
frameTimeBufferMS += deltaSmooth;
|
|
431
445
|
}
|
|
432
|
-
|
|
433
|
-
// add the time smoothing back in
|
|
434
|
-
frameTimeBufferMS += deltaSmooth;
|
|
435
446
|
}
|
|
436
447
|
|
|
437
448
|
// one tick of the loop: update game and objects, when frozen update everything except them
|
|
@@ -450,7 +461,7 @@ async function engineInit(gameInit, gameUpdate, gameUpdatePost, gameRender, game
|
|
|
450
461
|
o.parent || o.updateTransforms();
|
|
451
462
|
|
|
452
463
|
// objects made and destroyed while paused, like a menu's effects, still leave the list
|
|
453
|
-
engineObjects = engineObjects.filter(o=>!o.destroyed);
|
|
464
|
+
engineObjects.some(o=>o.destroyed) && (engineObjects = engineObjects.filter(o=>!o.destroyed));
|
|
454
465
|
}
|
|
455
466
|
else
|
|
456
467
|
engineObjectsUpdate();
|
|
@@ -497,7 +508,7 @@ async function engineInit(gameInit, gameUpdate, gameUpdatePost, gameRender, game
|
|
|
497
508
|
// render the game and objects
|
|
498
509
|
enginePreRender();
|
|
499
510
|
gameRender();
|
|
500
|
-
|
|
511
|
+
engineObjectsSort();
|
|
501
512
|
for (const o of engineObjects)
|
|
502
513
|
{
|
|
503
514
|
if (o.destroyed) continue;
|
|
@@ -600,7 +611,7 @@ async function engineInit(gameInit, gameUpdate, gameUpdatePost, gameRender, game
|
|
|
600
611
|
}
|
|
601
612
|
|
|
602
613
|
// the splash first, the images load under it, then the loading screen for the rest
|
|
603
|
-
showSplashScreen && await promises.
|
|
614
|
+
showSplashScreen && await promises[promises.length - 1];
|
|
604
615
|
return startEngine(promises);
|
|
605
616
|
|
|
606
617
|
// gameInit runs once the images are in, and the game loop starts once it and everything loaded while it ran are
|
|
@@ -723,7 +734,20 @@ function engineScheduleFrame()
|
|
|
723
734
|
const next = (frameTimeMS)=>
|
|
724
735
|
{
|
|
725
736
|
engineFrameScheduled = false;
|
|
726
|
-
|
|
737
|
+
if (engineManualStep) return;
|
|
738
|
+
if (debug) return engineUpdateInternal(frameTimeMS); // a debug build stops at an error, where it shows it
|
|
739
|
+
try { engineUpdateInternal(frameTimeMS); }
|
|
740
|
+
catch (error)
|
|
741
|
+
{
|
|
742
|
+
// a release build goes on past an error in a frame, a frozen game is the worst a player can get; an error
|
|
743
|
+
// is logged when it is not the last one again, one every frame would flood the console
|
|
744
|
+
const text = String(error);
|
|
745
|
+
text === engineFrameErrorLast || console.error(error);
|
|
746
|
+
engineFrameErrorLast = text;
|
|
747
|
+
// the frame's input is cleared as its tick would have, or a key press that threw would be pressed again
|
|
748
|
+
inputUpdatePost();
|
|
749
|
+
engineScheduleFrame();
|
|
750
|
+
}
|
|
727
751
|
};
|
|
728
752
|
if (typeof requestAnimationFrame === 'function')
|
|
729
753
|
requestAnimationFrame(next);
|
|
@@ -769,14 +793,18 @@ function engineObjectsUpdate()
|
|
|
769
793
|
++engineObjectsUpdateCount;
|
|
770
794
|
engineObjectsCollidePairs.clear();
|
|
771
795
|
// objects update in render order, which rendering keeps them in, so a headless run or a frame that rendered
|
|
772
|
-
// nothing updates them the same way
|
|
773
|
-
|
|
796
|
+
// nothing updates them the same way
|
|
797
|
+
engineObjectsSort();
|
|
774
798
|
// get list of solid objects for physics optimization, in update order, which 3D collision pairs by;
|
|
775
799
|
// 2D checks the static ones last, so a contact with a moving object can not leave something back inside a static
|
|
776
800
|
// solid it was already pushed out of
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
801
|
+
const fixed = [];
|
|
802
|
+
engineObjectsCollide = [];
|
|
803
|
+
engineObjectsCollideStaticLast = [];
|
|
804
|
+
for (const o of engineObjects)
|
|
805
|
+
o.collideSolidObjects && (engineObjectsCollide.push(o), (o.mass ? engineObjectsCollideStaticLast : fixed).push(o));
|
|
806
|
+
for (const o of fixed)
|
|
807
|
+
engineObjectsCollideStaticLast.push(o);
|
|
780
808
|
|
|
781
809
|
// update physics before object update
|
|
782
810
|
for (const o of engineObjects)
|
|
@@ -826,7 +854,16 @@ function engineObjectsUpdate()
|
|
|
826
854
|
updateTopObject(o);
|
|
827
855
|
|
|
828
856
|
// remove destroyed objects
|
|
829
|
-
engineObjects = engineObjects.filter(o=>!o.destroyed);
|
|
857
|
+
engineObjects.some(o=>o.destroyed) && (engineObjects = engineObjects.filter(o=>!o.destroyed));
|
|
858
|
+
}
|
|
859
|
+
|
|
860
|
+
// sort the objects by render order, keeping the order of equals, only when one is out of order, as they are kept
|
|
861
|
+
// sorted and most frames change none
|
|
862
|
+
function engineObjectsSort()
|
|
863
|
+
{
|
|
864
|
+
for (let i = engineObjects.length; --i > 0;)
|
|
865
|
+
if (engineObjects[i].renderOrder < engineObjects[i-1].renderOrder)
|
|
866
|
+
return void engineObjects.sort((a,b)=> a.renderOrder - b.renderOrder);
|
|
830
867
|
}
|
|
831
868
|
|
|
832
869
|
/** Destroy and remove all objects
|
package/src/engineAudio.js
CHANGED
|
@@ -58,9 +58,19 @@ function audioInit()
|
|
|
58
58
|
{
|
|
59
59
|
if (!soundEnable || headlessMode) return;
|
|
60
60
|
|
|
61
|
+
audioSetSession();
|
|
61
62
|
document.addEventListener('visibilitychange', audioVisibilityChange);
|
|
62
63
|
}
|
|
63
64
|
|
|
65
|
+
// on an iPhone the silent switch mutes the page's sound, unless soundIgnoreSilentSwitch has it play as media,
|
|
66
|
+
// Safari 16.4 and up; elsewhere there is no audioSession and nothing changes
|
|
67
|
+
function audioSetSession()
|
|
68
|
+
{
|
|
69
|
+
const session = typeof navigator != 'undefined' && /** @type {any} */ (navigator).audioSession;
|
|
70
|
+
if (session)
|
|
71
|
+
session.type = soundIgnoreSilentSwitch ? 'playback' : 'auto';
|
|
72
|
+
}
|
|
73
|
+
|
|
64
74
|
// a hidden page stops the game, so its sound stops too, and the audio clock with it so every sound picks up
|
|
65
75
|
// exactly where it was; only a suspend made here is undone, not one the browser holds until the first input
|
|
66
76
|
let audioSuspendedWhenHidden = false;
|
|
@@ -203,13 +213,13 @@ function audioParamRamp(param, value, fadeTime=0)
|
|
|
203
213
|
* @memberof Audio
|
|
204
214
|
* @example
|
|
205
215
|
* // load an audio asset file
|
|
206
|
-
* const
|
|
216
|
+
* const music = new Sound('sound.mp3');
|
|
207
217
|
*
|
|
208
218
|
* // create a zzfx sound
|
|
209
|
-
* const
|
|
219
|
+
* const blip = new Sound([.5,.5]);
|
|
210
220
|
*
|
|
211
221
|
* // play a sound
|
|
212
|
-
*
|
|
222
|
+
* blip.play();
|
|
213
223
|
*/
|
|
214
224
|
class Sound
|
|
215
225
|
{
|
|
@@ -222,7 +232,7 @@ class Sound
|
|
|
222
232
|
*/
|
|
223
233
|
constructor(asset, randomness, range, taper=soundDefaultTaper, onloadCallback)
|
|
224
234
|
{
|
|
225
|
-
if (!soundEnable || headlessMode)
|
|
235
|
+
if (!soundEnable || headlessMode || !audioContext) // a browser with no audio makes none either
|
|
226
236
|
{
|
|
227
237
|
// no sound is made: it counts as loaded, so a game that waits for its sounds goes on
|
|
228
238
|
this.loadedPercent = 1;
|
|
@@ -292,8 +302,8 @@ class Sound
|
|
|
292
302
|
// load the audio file, a URL object as bundlers give works like its string;
|
|
293
303
|
// report failures rather than leaving an unhandled rejection, the sound just stays unloaded and silent
|
|
294
304
|
const filename = asset + '';
|
|
295
|
-
engineAddLoad(this.loadSound(filename).catch(e=>
|
|
296
|
-
|
|
305
|
+
engineAddLoad(this.loadSound(filename).catch(e=> // startup waits for it, and a release build says why too
|
|
306
|
+
console.warn('Sound load failed for', filename, '-', e?.message ?? e)));
|
|
297
307
|
}
|
|
298
308
|
}
|
|
299
309
|
|
package/src/engineBuild.mjs
CHANGED
|
@@ -289,17 +289,64 @@ function typeScriptBuildStep(filename)
|
|
|
289
289
|
try
|
|
290
290
|
{
|
|
291
291
|
const tsFilename = join(BUILD_FOLDER, `${ENGINE_NAME}.d.ts`);
|
|
292
|
-
|
|
292
|
+
// strictNullChecks keeps undefined in the types, where tsc would drop it from accessors and object types
|
|
293
|
+
// whatever the JSDoc says, so a game in strict mode can set render3D.shader = undefined and is told a
|
|
294
|
+
// flare may be missing; a game not in strict mode reads the same types as before
|
|
295
|
+
execSync(`npx -p typescript tsc "${filename}" --declaration --allowJs --emitDeclarationOnly --strictNullChecks --outFile "${tsFilename}"`);
|
|
293
296
|
|
|
294
297
|
// Make declare module part use the package name littlejsengine
|
|
295
298
|
let fileContent = fs.readFileSync(tsFilename, 'utf8');
|
|
296
299
|
fileContent = fileContent.replace(`${ENGINE_NAME}\.esm`, 'littlejsengine')
|
|
297
|
-
fs.writeFileSync(tsFilename, fileContent);
|
|
300
|
+
fs.writeFileSync(tsFilename, typeScriptOptionalTidy(fileContent));
|
|
298
301
|
|
|
299
302
|
}
|
|
300
303
|
catch (e) { handleError(e, 'Failed to run TypeScript build step!'); }
|
|
301
304
|
};
|
|
302
305
|
|
|
306
|
+
// strictNullChecks writes an optional parameter or field as x?: T | undefined, which is the same as x?: T, so the
|
|
307
|
+
// redundant | undefined is taken off: each ?: is followed to the end of its type, the first , ; ) or line end outside
|
|
308
|
+
// brackets, and only that type is changed; a required x: T | undefined keeps it
|
|
309
|
+
function typeScriptOptionalTidy(text)
|
|
310
|
+
{
|
|
311
|
+
let out = '', i = 0;
|
|
312
|
+
for (let at; (at = text.indexOf('?: ', i)) >= 0;)
|
|
313
|
+
{
|
|
314
|
+
const start = at + 3;
|
|
315
|
+
let end = start, depth = 0;
|
|
316
|
+
for (; end < text.length; ++end)
|
|
317
|
+
{
|
|
318
|
+
const c = text[end];
|
|
319
|
+
if (c === '=' && text[end + 1] === '>') { ++end; continue; } // an arrow is not a closing bracket
|
|
320
|
+
if ('([{<'.includes(c)) ++depth;
|
|
321
|
+
else if (')]}>'.includes(c) && depth) --depth;
|
|
322
|
+
else if (!depth && (',;)\n'.includes(c))) break;
|
|
323
|
+
}
|
|
324
|
+
let type = typeScriptOptionalTidy(text.slice(start, end)); // and the optional fields of a record type in it
|
|
325
|
+
if (type.endsWith(' | undefined'))
|
|
326
|
+
{
|
|
327
|
+
type = type.slice(0, -12);
|
|
328
|
+
// a function type was wrapped for the union, (() => void) | undefined, and needs it no more
|
|
329
|
+
if (type[0] === '(' && typeScriptCloseOf(type, 0) === type.length - 1)
|
|
330
|
+
type = type.slice(1, -1);
|
|
331
|
+
}
|
|
332
|
+
out += text.slice(i, start) + type;
|
|
333
|
+
i = end;
|
|
334
|
+
}
|
|
335
|
+
return out + text.slice(i);
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
// the index of the bracket that closes the one at start
|
|
339
|
+
function typeScriptCloseOf(text, start)
|
|
340
|
+
{
|
|
341
|
+
let depth = 0;
|
|
342
|
+
for (let k = start; k < text.length; ++k)
|
|
343
|
+
{
|
|
344
|
+
if (text[k] === '(') ++depth;
|
|
345
|
+
else if (text[k] === ')' && !--depth) return k;
|
|
346
|
+
}
|
|
347
|
+
return -1;
|
|
348
|
+
}
|
|
349
|
+
|
|
303
350
|
// display the error and exit
|
|
304
351
|
function handleError(e,message)
|
|
305
352
|
{
|
package/src/engineDebug.js
CHANGED
|
@@ -269,7 +269,10 @@ function debugShowErrors()
|
|
|
269
269
|
{
|
|
270
270
|
// replace entire page with error message
|
|
271
271
|
document.body.style.cssText = 'background-color:#111;margin:8px';
|
|
272
|
-
|
|
272
|
+
const pre = document.createElement('pre'); // the message as text, a file name in it never markup
|
|
273
|
+
pre.style.cssText = 'color:#f00;font-size:28px;white-space:pre-wrap';
|
|
274
|
+
pre.textContent = String(message);
|
|
275
|
+
document.body.replaceChildren(pre);
|
|
273
276
|
}
|
|
274
277
|
|
|
275
278
|
const originalAssert = console.assert;
|
package/src/engineDraw.js
CHANGED
|
@@ -134,10 +134,13 @@ function tile(index=0, size=tileDefaultSize, texture=0, padding=tileDefaultPaddi
|
|
|
134
134
|
// create tile info object
|
|
135
135
|
const textureInfo = typeof texture === 'number' ?
|
|
136
136
|
textureInfos[texture] : texture;
|
|
137
|
-
if (
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
137
|
+
if (!textureInfo?.size.x)
|
|
138
|
+
{
|
|
139
|
+
// no image: headless loads none and keeps the size; an image that failed to load, whose warning named it, gives
|
|
140
|
+
// a tile of no size, which draws nothing; a slot never given an image is a mistake a debug build points out
|
|
141
|
+
ASSERT(headlessMode || textureInfo instanceof TextureInfo, 'tile texture is not loaded', texture);
|
|
142
|
+
return new TileInfo(new Vector2, headlessMode ? size.copy() : new Vector2, textureInfo, padding, bleed);
|
|
143
|
+
}
|
|
141
144
|
|
|
142
145
|
// get the position of the tile
|
|
143
146
|
const sizePaddedX = size.x + padding*2;
|
|
@@ -182,6 +185,10 @@ class TileInfo
|
|
|
182
185
|
this.padding = padding;
|
|
183
186
|
/** @property {TextureInfo} - The texture info for this tile */
|
|
184
187
|
this.textureInfo = textureInfo;
|
|
188
|
+
/** @property {Array<TileInfo>|undefined} - A tile set's tiles, each wherever it was packed, as loadTiles makes
|
|
189
|
+
* them: a tile layer given this tile info draws its tile n from tiles[n] and not from a grid on one sheet
|
|
190
|
+
* @type {Array<TileInfo>|undefined} */
|
|
191
|
+
this.tiles = undefined;
|
|
185
192
|
/** @property {number} - Shrinks tile by this many pixels to prevent neighbors bleeding */
|
|
186
193
|
this.bleed = bleed;
|
|
187
194
|
/** @property {number} - How many frames per row for frame(), 0 to keep frames on a single row */
|
|
@@ -601,8 +608,8 @@ function drawTile(pos, size=vec2(1), tileInfo, color=WHITE,
|
|
|
601
608
|
if (headlessMode && !context) return; // headless has no canvas, only a context passed in is drawn to
|
|
602
609
|
|
|
603
610
|
const textureInfo = tileInfo?.textureInfo;
|
|
604
|
-
if (textureInfo && !(tileInfo.size.x && tileInfo.size.y))
|
|
605
|
-
return; // a tile with no area draws nothing, like a sprite still loading
|
|
611
|
+
if (textureInfo && !(tileInfo.size.x && tileInfo.size.y && textureInfo.size.x))
|
|
612
|
+
return; // a tile with no area draws nothing, like a sprite still loading, and nor does an image that failed
|
|
606
613
|
const bleed = tileInfo?.bleed ?? 0;
|
|
607
614
|
if (useWebGL && glEnable)
|
|
608
615
|
{
|
|
@@ -696,7 +703,7 @@ function drawRect(pos, size, color, angle, useWebGL, screenSpace, context)
|
|
|
696
703
|
* @param {boolean} [screenSpace=drawScreenSpace]
|
|
697
704
|
* @param {CanvasRenderingContext2D|OffscreenCanvasRenderingContext2D} [context]
|
|
698
705
|
* @memberof Draw */
|
|
699
|
-
function drawRectGradient(pos, size, colorTop=WHITE, colorBottom=CLEAR_WHITE, angle=0, useWebGL=glEnable, screenSpace=drawScreenSpace, context)
|
|
706
|
+
function drawRectGradient(pos, size=vec2(1), colorTop=WHITE, colorBottom=CLEAR_WHITE, angle=0, useWebGL=glEnable, screenSpace=drawScreenSpace, context)
|
|
700
707
|
{
|
|
701
708
|
ASSERT(isVector2(pos), 'pos must be a vec2');
|
|
702
709
|
ASSERT(isVector2(size), 'size must be a vec2');
|
|
@@ -868,13 +875,15 @@ function drawLineList(points, width=.1, color=WHITE, wrap=false, pos=vec2(), ang
|
|
|
868
875
|
if (useWebGL && glEnable)
|
|
869
876
|
{
|
|
870
877
|
ASSERT(!!glContext, 'WebGL is not enabled!');
|
|
871
|
-
let
|
|
878
|
+
let sx = 1, sy = 1;
|
|
872
879
|
if (screenSpace)
|
|
873
880
|
{
|
|
874
881
|
if (glSkipScreenSpace) return;
|
|
875
|
-
|
|
882
|
+
let size;
|
|
883
|
+
[pos, size, angle] = screenToWorldTransform(pos, vec2(1), angle);
|
|
884
|
+
sx = size.x, sy = size.y;
|
|
876
885
|
}
|
|
877
|
-
glDrawOutlineTransform(points, color.rgbaInt(), width, pos.x, pos.y,
|
|
886
|
+
glDrawOutlineTransform(points, color.rgbaInt(), width, pos.x, pos.y, sx, sy, angle, wrap);
|
|
878
887
|
}
|
|
879
888
|
else
|
|
880
889
|
{
|
|
@@ -940,17 +949,22 @@ function drawRegularPoly(pos, size=vec2(1), sides=3, color=WHITE, lineWidth=0, l
|
|
|
940
949
|
ASSERT(isVector2(size), 'size must be a vec2');
|
|
941
950
|
ASSERT(isNumber(sides), 'sides must be a number');
|
|
942
951
|
|
|
943
|
-
// build regular polygon points
|
|
944
|
-
const points =
|
|
952
|
+
// build regular polygon points, into vectors kept for the next, as drawPoly is done with them when it returns
|
|
953
|
+
const points = drawRegularPolyPoints, pool = drawRegularPolyPool;
|
|
954
|
+
points.length = 0;
|
|
945
955
|
const sizeX = size.x/2, sizeY = size.y/2;
|
|
946
956
|
for (let i=sides; i-- > 0;) // a count that is not whole, or below zero, still ends
|
|
947
957
|
{
|
|
948
|
-
const a = (i/sides)*PI*2;
|
|
949
|
-
|
|
958
|
+
const a = (i/sides)*PI*2, point = pool[points.length] ||= vec2();
|
|
959
|
+
point.x = sin(a)*sizeX, point.y = cos(a)*sizeY;
|
|
960
|
+
points.push(point);
|
|
950
961
|
}
|
|
951
962
|
drawPoly(points, color, lineWidth, lineColor, pos, angle, useWebGL, screenSpace, context);
|
|
952
963
|
}
|
|
953
964
|
|
|
965
|
+
// the points drawRegularPoly draws and the vectors it writes them into
|
|
966
|
+
const drawRegularPolyPoints = [], drawRegularPolyPool = [];
|
|
967
|
+
|
|
954
968
|
/** Draw colored polygon using passed in points
|
|
955
969
|
* - WebGL fills a polygon whose edges do not cross, concave or not; a self crossing one, like a star through its
|
|
956
970
|
* outer points, fills wrong there, so draw it as its simple outline or in parts
|
|
@@ -978,15 +992,17 @@ function drawPoly(points, color=WHITE, lineWidth=0, lineColor=BLACK, pos=vec2(),
|
|
|
978
992
|
if (useWebGL && glEnable)
|
|
979
993
|
{
|
|
980
994
|
ASSERT(!!glContext, 'WebGL is not enabled!');
|
|
981
|
-
let
|
|
995
|
+
let sx = 1, sy = 1;
|
|
982
996
|
if (screenSpace)
|
|
983
997
|
{
|
|
984
998
|
if (glSkipScreenSpace) return;
|
|
985
|
-
|
|
999
|
+
let size;
|
|
1000
|
+
[pos, size, angle] = screenToWorldTransform(pos, vec2(1), angle);
|
|
1001
|
+
sx = size.x, sy = size.y;
|
|
986
1002
|
}
|
|
987
|
-
glDrawPointsTransform(points, color.rgbaInt(), pos.x, pos.y,
|
|
1003
|
+
glDrawPointsTransform(points, color.rgbaInt(), pos.x, pos.y, sx, sy, angle);
|
|
988
1004
|
if (lineWidth > 0)
|
|
989
|
-
glDrawOutlineTransform(points, lineColor.rgbaInt(), lineWidth, pos.x, pos.y,
|
|
1005
|
+
glDrawOutlineTransform(points, lineColor.rgbaInt(), lineWidth, pos.x, pos.y, sx, sy, angle);
|
|
990
1006
|
}
|
|
991
1007
|
else
|
|
992
1008
|
{
|
|
@@ -1631,7 +1647,7 @@ function tintImageData(data, color, additiveColor)
|
|
|
1631
1647
|
if (additiveColor && !isBlack(additiveColor))
|
|
1632
1648
|
{
|
|
1633
1649
|
// multiply + additive (slower), color.a is baked into the alpha channel here
|
|
1634
|
-
const colorMultiply = [color.r, color.g, color.b, color.a];
|
|
1650
|
+
const colorMultiply = [clamp(color.r), clamp(color.g), clamp(color.b), clamp(color.a)];
|
|
1635
1651
|
const colorAdd = [additiveColor.r * 255, additiveColor.g * 255,
|
|
1636
1652
|
additiveColor.b * 255, additiveColor.a * 255];
|
|
1637
1653
|
for (let i = 0; i < data.length; ++i)
|
|
@@ -1642,9 +1658,9 @@ function tintImageData(data, color, additiveColor)
|
|
|
1642
1658
|
// RGB only, faster — alpha left intact for the caller
|
|
1643
1659
|
for (let i = 0; i < data.length; i+=4)
|
|
1644
1660
|
{
|
|
1645
|
-
data[i ] *= color.r;
|
|
1646
|
-
data[i+1] *= color.g;
|
|
1647
|
-
data[i+2] *= color.b;
|
|
1661
|
+
data[i ] *= clamp(color.r);
|
|
1662
|
+
data[i+1] *= clamp(color.g);
|
|
1663
|
+
data[i+2] *= clamp(color.b);
|
|
1648
1664
|
}
|
|
1649
1665
|
return false;
|
|
1650
1666
|
}
|
|
@@ -1682,8 +1698,7 @@ function bakeTintedImage(image, color, additiveColor)
|
|
|
1682
1698
|
* @param {number} dHeight
|
|
1683
1699
|
* @param {Color} color
|
|
1684
1700
|
* @param {Color} [additiveColor]
|
|
1685
|
-
* @param {number} [bleed] - How many pixels to shrink the source, used to fix bleeding
|
|
1686
|
-
* @memberof Draw */
|
|
1701
|
+
* @param {number} [bleed] - How many pixels to shrink the source, used to fix bleeding */
|
|
1687
1702
|
function drawImageColor(context, image, sx, sy, sWidth, sHeight, dx, dy, dWidth, dHeight, color, additiveColor, bleed=0)
|
|
1688
1703
|
{
|
|
1689
1704
|
const sx2 = bleed;
|
|
@@ -1896,7 +1911,7 @@ function engineGlowTexture(falloff=1)
|
|
|
1896
1911
|
ASSERT(isNumber(falloff) && falloff > 0, 'glowFalloff must be a number above 0');
|
|
1897
1912
|
const key = max(round(falloff * 10), 1) / 10;
|
|
1898
1913
|
let texture = engineGlowTextures.get(key);
|
|
1899
|
-
if (texture || !glContext ||
|
|
1914
|
+
if (texture || !glContext || !canvasAvailable()) return texture;
|
|
1900
1915
|
const size = 64, context = createCanvasContext(size), steps = 16;
|
|
1901
1916
|
const gradient = context.createRadialGradient(size / 2, size / 2, 0, size / 2, size / 2, size / 2);
|
|
1902
1917
|
for (let i = 0; i <= steps; ++i)
|