littlejsengine 1.18.29 → 1.20.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/FAQ.md +841 -815
- package/README.md +211 -199
- package/dist/littlejs.d.ts +4383 -812
- package/dist/littlejs.esm.js +31848 -16680
- package/dist/littlejs.esm.min.js +14 -1
- package/dist/littlejs.js +31230 -16166
- package/dist/littlejs.min.js +13 -1
- package/dist/littlejs.release.js +27419 -15457
- package/package.json +10 -5
- package/plugins/audioEffects.js +375 -0
- package/plugins/box2d.js +2516 -2073
- package/plugins/drawUtilities.js +206 -31
- package/plugins/gltf.js +740 -0
- package/plugins/jsconfig.json +19 -0
- package/plugins/lightSystem.js +719 -330
- package/plugins/math3d.js +911 -0
- package/plugins/medalSystem.js +118 -72
- package/plugins/newgrounds.js +400 -199
- package/plugins/pathFinder.js +208 -175
- package/plugins/pluginExport.js +196 -115
- package/plugins/postProcess.js +284 -166
- package/plugins/render3d.js +4072 -0
- package/plugins/render3dDebug.js +109 -0
- package/plugins/render3dExtras.js +1754 -0
- package/plugins/render3dVoxels.js +502 -0
- package/plugins/textureSheet.js +86 -48
- package/plugins/threejs.js +183 -158
- package/plugins/tweakables.js +411 -0
- package/plugins/tweenSystem.js +646 -526
- package/plugins/uiSystem.js +2024 -1687
- package/plugins/zzfxm.js +8 -15
- package/src/engine.js +345 -194
- package/src/engineAudio.js +1022 -777
- package/src/engineBuild.mjs +289 -253
- package/src/engineDebug.js +912 -735
- package/src/engineDraw.js +1774 -1510
- package/src/engineEditor.js +2431 -0
- package/src/engineExport.js +419 -396
- package/src/engineInput.js +1462 -1293
- package/src/engineLogo.js +4 -3
- package/src/engineMath.js +1274 -1183
- package/src/engineObject.js +674 -551
- package/src/engineParticles.js +583 -532
- package/src/engineRelease.js +67 -45
- package/src/engineSettings.js +808 -726
- package/src/engineTileLayer.js +995 -741
- package/src/engineUtilities.js +32 -13
- package/src/engineWebGL.js +1162 -941
- package/src/jsconfig.json +17 -13
- package/plugins/box2d.wasm.js +0 -630
- package/plugins/box2d.wasm.wasm +0 -0
package/plugins/textureSheet.js
CHANGED
|
@@ -48,10 +48,10 @@ class TextureSheet
|
|
|
48
48
|
|
|
49
49
|
/** @property {number} - Width and height of the sheet in pixels */
|
|
50
50
|
this.size = size;
|
|
51
|
-
/** @property {OffscreenCanvas} - Canvas holding the packed images */
|
|
52
|
-
this.canvas = headlessMode ? undefined : new OffscreenCanvas(size, size);
|
|
53
51
|
/** @property {OffscreenCanvasRenderingContext2D} - 2d context for the canvas */
|
|
54
|
-
this.context =
|
|
52
|
+
this.context = headlessMode ? undefined : createCanvasContext(size);
|
|
53
|
+
/** @property {OffscreenCanvas} - Canvas holding the packed images */
|
|
54
|
+
this.canvas = this.context?.canvas;
|
|
55
55
|
/** @property {TextureInfo} - The texture info for this sheet */
|
|
56
56
|
this.textureInfo = new TextureInfo(this.canvas);
|
|
57
57
|
/** @property {Vector2} - Where the next image will be packed */
|
|
@@ -71,23 +71,25 @@ class TextureSheet
|
|
|
71
71
|
|
|
72
72
|
/** Find a spot for an image on this sheet without drawing it
|
|
73
73
|
* @param {Vector2} imageSize - Size of the source image in pixels
|
|
74
|
-
* @param {Vector2} [frameSize] - Size of each frame, or the whole image if not passed
|
|
74
|
+
* @param {Vector2} [frameSize] - Size of each frame, or the whole image less its source padding if not passed
|
|
75
75
|
* @param {number} [padding] - How many pixels padding around each frame
|
|
76
76
|
* @param {number|Vector2} [sourcePadding] - How many pixels padding around each frame in the source image
|
|
77
|
-
* @return {TileInfo} Tile for the packed image, or undefined if the sheet is full */
|
|
78
|
-
tryAdd(imageSize, frameSize
|
|
77
|
+
* @return {TileInfo|undefined} Tile for the packed image, or undefined if the sheet is full */
|
|
78
|
+
tryAdd(imageSize, frameSize, padding=textureSheetPadding, sourcePadding=0)
|
|
79
79
|
{
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
sourcePadding = vec2(sourcePadding);
|
|
85
|
-
ASSERT(isVector2(sourcePadding) && sourcePadding.x >= 0 && sourcePadding.y >= 0,
|
|
80
|
+
// a number pads both axes the same
|
|
81
|
+
const sourcePad = isNumber(sourcePadding) ?
|
|
82
|
+
vec2(/** @type {number} */ (sourcePadding)) : /** @type {Vector2} */ (sourcePadding);
|
|
83
|
+
ASSERT(isVector2(sourcePad) && sourcePad.x >= 0 && sourcePad.y >= 0,
|
|
86
84
|
'sourcePadding must be a number or vec2 >= 0');
|
|
85
|
+
ASSERT(isVector2(imageSize), 'sizes must be vec2');
|
|
86
|
+
frameSize ||= imageSize.subtract(sourcePad.scale(2)); // the whole image, less the padding baked into it
|
|
87
|
+
ASSERT(isVector2(frameSize), 'sizes must be vec2');
|
|
88
|
+
ASSERT(frameSize.x > 0 && frameSize.y > 0, 'frame size must be positive');
|
|
87
89
|
|
|
88
90
|
// the source may have its own padding baked in around each frame
|
|
89
|
-
const sourceCellWidth = frameSize.x +
|
|
90
|
-
const sourceCellHeight = frameSize.y +
|
|
91
|
+
const sourceCellWidth = frameSize.x + sourcePad.x*2;
|
|
92
|
+
const sourceCellHeight = frameSize.y + sourcePad.y*2;
|
|
91
93
|
ASSERT(imageSize.x % sourceCellWidth === 0 && imageSize.y % sourceCellHeight === 0,
|
|
92
94
|
'image size must be a multiple of the padded frame size');
|
|
93
95
|
|
|
@@ -98,8 +100,11 @@ class TextureSheet
|
|
|
98
100
|
|
|
99
101
|
// keep the layout of the source image, but narrow it if a row is too wide
|
|
100
102
|
// frames wrap down to the next row, which TileInfo.frame handles via columns
|
|
101
|
-
|
|
102
|
-
const
|
|
103
|
+
// whole frames only, a partial one left over at an edge is ignored
|
|
104
|
+
const sourceColumns = imageSize.x / sourceCellWidth | 0;
|
|
105
|
+
const frameCount = sourceColumns * (imageSize.y / sourceCellHeight | 0);
|
|
106
|
+
if (!frameCount)
|
|
107
|
+
return undefined; // smaller than a frame, there is nothing to pack
|
|
103
108
|
const columns = min(sourceColumns, maxColumns);
|
|
104
109
|
const blockWidth = columns * cellWidth;
|
|
105
110
|
const blockHeight = ceil(frameCount / columns) * cellHeight;
|
|
@@ -126,7 +131,7 @@ class TextureSheet
|
|
|
126
131
|
}
|
|
127
132
|
|
|
128
133
|
/** Draw an image into this sheet at a tile returned by tryAdd
|
|
129
|
-
* @param {HTMLImageElement} image - Source image to copy from
|
|
134
|
+
* @param {HTMLImageElement|HTMLCanvasElement|OffscreenCanvas|ImageBitmap} image - Source image to copy from
|
|
130
135
|
* @param {TileInfo} tileInfo - Where to put it, from tryAdd
|
|
131
136
|
* @param {boolean} [update] - Upload to webgl now, pass false when batching
|
|
132
137
|
* @param {number|Vector2} [sourcePadding] - How many pixels padding around each frame in the source image */
|
|
@@ -134,23 +139,25 @@ class TextureSheet
|
|
|
134
139
|
{
|
|
135
140
|
ASSERT(!!this.context, 'texture sheet has no canvas');
|
|
136
141
|
|
|
137
|
-
|
|
138
|
-
|
|
142
|
+
// a number pads both axes the same
|
|
143
|
+
const sourcePad = isNumber(sourcePadding) ?
|
|
144
|
+
vec2(/** @type {number} */ (sourcePadding)) : /** @type {Vector2} */ (sourcePadding);
|
|
139
145
|
|
|
140
146
|
// copy frames in order, reading the source left to right, top to bottom
|
|
141
147
|
// the destination wraps at tileInfo.columns which may be narrower than the source
|
|
142
148
|
const frameSize = tileInfo.size;
|
|
143
|
-
const sourceCellWidth = frameSize.x +
|
|
144
|
-
const sourceCellHeight = frameSize.y +
|
|
145
|
-
|
|
146
|
-
const
|
|
149
|
+
const sourceCellWidth = frameSize.x + sourcePad.x*2;
|
|
150
|
+
const sourceCellHeight = frameSize.y + sourcePad.y*2;
|
|
151
|
+
// whole frames only as tryAdd packed them, a fractional count would never end the loop
|
|
152
|
+
const sourceColumns = image.width / sourceCellWidth | 0;
|
|
153
|
+
const frameCount = sourceColumns * (image.height / sourceCellHeight | 0);
|
|
147
154
|
const columns = tileInfo.columns || frameCount;
|
|
148
155
|
const cellWidth = frameSize.x + tileInfo.padding*2;
|
|
149
156
|
const cellHeight = frameSize.y + tileInfo.padding*2;
|
|
150
157
|
for (let i = frameCount; i--;)
|
|
151
158
|
{
|
|
152
|
-
const sourceX = (i % sourceColumns) * sourceCellWidth +
|
|
153
|
-
const sourceY = (i / sourceColumns | 0) * sourceCellHeight +
|
|
159
|
+
const sourceX = (i % sourceColumns) * sourceCellWidth + sourcePad.x;
|
|
160
|
+
const sourceY = (i / sourceColumns | 0) * sourceCellHeight + sourcePad.y;
|
|
154
161
|
this.context.drawImage(image,
|
|
155
162
|
sourceX, sourceY, frameSize.x, frameSize.y,
|
|
156
163
|
tileInfo.pos.x + (i % columns) * cellWidth,
|
|
@@ -182,7 +189,8 @@ class TextureSheet
|
|
|
182
189
|
* - Grid images keep their layout and frames wrap down to the next row
|
|
183
190
|
* - Pass sourcePadding if the source image has padding baked in around frames
|
|
184
191
|
* @param {string} src - Image source path
|
|
185
|
-
* @param {Vector2|number} [frameSize] - Size of each animation frame in pixels
|
|
192
|
+
* @param {Vector2|number} [frameSize] - Size of each animation frame in pixels, or the whole image less its
|
|
193
|
+
* source padding if not passed
|
|
186
194
|
* @param {number} [padding] - How many pixels padding around each frame
|
|
187
195
|
* @param {number|Vector2} [sourcePadding] - How many pixels padding around each frame in the source image
|
|
188
196
|
* @return {TileInfo}
|
|
@@ -196,9 +204,10 @@ function loadSprite(src, frameSize, padding=textureSheetPadding, sourcePadding=0
|
|
|
196
204
|
ASSERT(!frameSize || isVector2(frameSize) || isNumber(frameSize), 'frameSize must be a vec2 or number');
|
|
197
205
|
ASSERT(isNumber(padding), 'padding must be a number');
|
|
198
206
|
ASSERT(isNumber(sourcePadding) || isVector2(sourcePadding), 'sourcePadding must be a number or vec2');
|
|
207
|
+
ASSERT(engineInitialized || headlessMode, 'call loadSprite after engineInit, e.g. in gameInit');
|
|
199
208
|
|
|
200
209
|
if (isNumber(frameSize))
|
|
201
|
-
frameSize = vec2(frameSize);
|
|
210
|
+
frameSize = vec2(/** @type {number} */ (frameSize));
|
|
202
211
|
|
|
203
212
|
// start with an empty tile that gets filled in when the image loads
|
|
204
213
|
const tileInfo = new TileInfo(vec2(), vec2(), undefined, padding, 0);
|
|
@@ -217,8 +226,7 @@ function loadSprite(src, frameSize, padding=textureSheetPadding, sourcePadding=0
|
|
|
217
226
|
});
|
|
218
227
|
|
|
219
228
|
// pack through a queue so sheets fill in call order, not decode order
|
|
220
|
-
|
|
221
|
-
textureSheetQueue = textureSheetQueue.then(async ()=>
|
|
229
|
+
textureSheetQueueJob('loadSprite ' + src, async ()=>
|
|
222
230
|
{
|
|
223
231
|
await imagePromise;
|
|
224
232
|
if (image.width)
|
|
@@ -226,19 +234,21 @@ function loadSprite(src, frameSize, padding=textureSheetPadding, sourcePadding=0
|
|
|
226
234
|
// pack onto a sheet, then fill in the tile that was already handed out,
|
|
227
235
|
// copying every field so nothing is missed if TileInfo gains more of them
|
|
228
236
|
const imageSize = vec2(image.width, image.height);
|
|
229
|
-
const
|
|
230
|
-
|
|
231
|
-
|
|
237
|
+
const added = textureSheetAdd(imageSize, frameSize, padding, sourcePadding);
|
|
238
|
+
if (!added)
|
|
239
|
+
{
|
|
240
|
+
// leave the tile empty, no sheet can hold it
|
|
241
|
+
LOG('loadSprite image is too large to fit on a texture sheet:', src);
|
|
242
|
+
return;
|
|
243
|
+
}
|
|
244
|
+
Object.assign(tileInfo, added.tile);
|
|
245
|
+
added.sheet.drawImage(image, tileInfo, false, sourcePadding); // upload once per batch below
|
|
232
246
|
}
|
|
233
247
|
else
|
|
234
248
|
{
|
|
235
249
|
// leave the tile empty if the image failed to load
|
|
236
250
|
LOG('loadSprite failed to load image:', src);
|
|
237
251
|
}
|
|
238
|
-
|
|
239
|
-
// upload to webgl once per batch, when the last pending load finishes
|
|
240
|
-
if (!--textureSheetPendingCount)
|
|
241
|
-
textureSheets.forEach(s=> s.updateTexture());
|
|
242
252
|
});
|
|
243
253
|
|
|
244
254
|
return tileInfo;
|
|
@@ -254,7 +264,7 @@ function loadSprite(src, frameSize, padding=textureSheetPadding, sourcePadding=0
|
|
|
254
264
|
* @param {string} imageSrc - Atlas image path
|
|
255
265
|
* @param {string|Object} jsonSrc - Atlas json path, or already parsed json data
|
|
256
266
|
* @param {number} [padding] - How many pixels padding around each frame
|
|
257
|
-
* @return {Object} Object mapping frame and animation names to TileInfos
|
|
267
|
+
* @return {Object<string, TileInfo>} Object mapping frame and animation names to TileInfos
|
|
258
268
|
* @example
|
|
259
269
|
* const atlas = loadAtlas('sprites.png', 'sprites.json');
|
|
260
270
|
* await spritesReady();
|
|
@@ -266,7 +276,9 @@ function loadAtlas(imageSrc, jsonSrc, padding=textureSheetPadding)
|
|
|
266
276
|
ASSERT(isStringLike(imageSrc), 'atlas image src must be a string');
|
|
267
277
|
ASSERT(isStringLike(jsonSrc) || typeof jsonSrc === 'object', 'atlas json must be a path or object');
|
|
268
278
|
ASSERT(isNumber(padding), 'padding must be a number');
|
|
279
|
+
ASSERT(engineInitialized || headlessMode, 'call loadAtlas after engineInit, e.g. in gameInit');
|
|
269
280
|
|
|
281
|
+
/** @type {Object<string, TileInfo>} */
|
|
270
282
|
const atlas = {};
|
|
271
283
|
if (headlessMode) return atlas;
|
|
272
284
|
|
|
@@ -282,8 +294,7 @@ function loadAtlas(imageSrc, jsonSrc, padding=textureSheetPadding)
|
|
|
282
294
|
});
|
|
283
295
|
|
|
284
296
|
// pack through a queue so sheets fill in call order, not decode order
|
|
285
|
-
|
|
286
|
-
textureSheetQueue = textureSheetQueue.then(async ()=>
|
|
297
|
+
textureSheetQueueJob('loadAtlas ' + imageSrc, async ()=>
|
|
287
298
|
{
|
|
288
299
|
const data = await jsonPromise;
|
|
289
300
|
await imagePromise;
|
|
@@ -294,7 +305,13 @@ function loadAtlas(imageSrc, jsonSrc, padding=textureSheetPadding)
|
|
|
294
305
|
// reserve a block of full size cells, one per frame
|
|
295
306
|
const sourceSize = group.frames[0].sourceSize;
|
|
296
307
|
const blockSize = vec2(sourceSize.x*group.frames.length, sourceSize.y);
|
|
297
|
-
const
|
|
308
|
+
const added = textureSheetAdd(blockSize, sourceSize, padding);
|
|
309
|
+
if (!added)
|
|
310
|
+
{
|
|
311
|
+
LOG('loadAtlas frames are too large to fit on a texture sheet:', group.name);
|
|
312
|
+
continue;
|
|
313
|
+
}
|
|
314
|
+
const {sheet, tile} = added;
|
|
298
315
|
|
|
299
316
|
// draw each frame untrimmed into its cell
|
|
300
317
|
const context = sheet.context;
|
|
@@ -327,15 +344,32 @@ function loadAtlas(imageSrc, jsonSrc, padding=textureSheetPadding)
|
|
|
327
344
|
// leave the atlas empty if either file failed to load
|
|
328
345
|
LOG('loadAtlas failed to load:', imageSrc, jsonSrc);
|
|
329
346
|
}
|
|
330
|
-
|
|
331
|
-
// upload to webgl once per batch, when the last pending load finishes
|
|
332
|
-
if (!--textureSheetPendingCount)
|
|
333
|
-
textureSheets.forEach(s=> s.updateTexture());
|
|
334
347
|
});
|
|
335
348
|
|
|
336
349
|
return atlas;
|
|
337
350
|
}
|
|
338
351
|
|
|
352
|
+
// run a load in the queue: a load that throws is reported and the loads after it carry on, the pending count
|
|
353
|
+
// always comes back down, and the sheets upload to webgl once per batch, when the last pending load finishes
|
|
354
|
+
function textureSheetQueueJob(name, job)
|
|
355
|
+
{
|
|
356
|
+
++textureSheetPendingCount;
|
|
357
|
+
textureSheetQueue = textureSheetQueue.then(async ()=>
|
|
358
|
+
{
|
|
359
|
+
try { await job(); }
|
|
360
|
+
catch (e) { console.error(name + ' failed:', e); }
|
|
361
|
+
finally
|
|
362
|
+
{
|
|
363
|
+
if (!--textureSheetPendingCount)
|
|
364
|
+
for (const sheet of textureSheets)
|
|
365
|
+
{
|
|
366
|
+
try { sheet.updateTexture(); }
|
|
367
|
+
catch (e) { console.error('texture sheet upload failed:', e); }
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
});
|
|
371
|
+
}
|
|
372
|
+
|
|
339
373
|
/** Parse atlas json into a list of named frame groups, used by loadAtlas
|
|
340
374
|
* - Accepts TexturePacker json (hash and array) and Aseprite json
|
|
341
375
|
* - Frames tagged in Aseprite or named like run_0, run_1 group into animations
|
|
@@ -375,11 +409,11 @@ function parseAtlas(data)
|
|
|
375
409
|
}
|
|
376
410
|
|
|
377
411
|
// group frames that share a name stem with contiguous trailing numbers
|
|
378
|
-
// run_0.png and run_1.png become a 2 frame animation named run
|
|
412
|
+
// run_0.png and run_1.png become a 2 frame animation named run, and so do the folder style run/0.png and run/1.png
|
|
379
413
|
const stems = new Map;
|
|
380
414
|
for (const f of frames)
|
|
381
415
|
{
|
|
382
|
-
let match = f.name.match(/^(.+?)([-_ ])?(\d+)$/);
|
|
416
|
+
let match = f.name.match(/^(.+?)([-_ /])?(\d+)$/);
|
|
383
417
|
if (match && !match[2] && /\d$/.test(match[1]))
|
|
384
418
|
match = undefined; // all digit tails like 10 are a name, not frame 0 of 1
|
|
385
419
|
const stem = match ? match[1] : f.name;
|
|
@@ -428,7 +462,8 @@ function textureSheetCreate()
|
|
|
428
462
|
return sheet;
|
|
429
463
|
}
|
|
430
464
|
|
|
431
|
-
// use the first sheet with enough space, or make a new one
|
|
465
|
+
// use the first sheet with enough space, or make a new one,
|
|
466
|
+
// returns undefined if it would not fit even on an empty sheet
|
|
432
467
|
function textureSheetAdd(imageSize, frameSize, padding, sourcePadding)
|
|
433
468
|
{
|
|
434
469
|
let sheet, tile;
|
|
@@ -437,9 +472,12 @@ function textureSheetAdd(imageSize, frameSize, padding, sourcePadding)
|
|
|
437
472
|
break;
|
|
438
473
|
if (!tile)
|
|
439
474
|
{
|
|
475
|
+
// probe an empty sheet first, a new one is a canvas and a texture kept for good
|
|
476
|
+
const emptySheet = {size: textureSheetSize, cursor: vec2(), rowHeight: 0};
|
|
477
|
+
if (!TextureSheet.prototype.tryAdd.call(emptySheet, imageSize, frameSize, padding, sourcePadding))
|
|
478
|
+
return;
|
|
440
479
|
sheet = textureSheetCreate();
|
|
441
480
|
tile = sheet.tryAdd(imageSize, frameSize, padding, sourcePadding);
|
|
442
|
-
ASSERT(!!tile, 'image is too large to fit on a texture sheet');
|
|
443
481
|
}
|
|
444
482
|
return {sheet, tile};
|
|
445
483
|
}
|
package/plugins/threejs.js
CHANGED
|
@@ -1,158 +1,183 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* LittleJS Three.js Plugin
|
|
3
|
-
* - Renders a three.js scene on a canvas behind the LittleJS canvases
|
|
4
|
-
* - The three.js module is passed in by the user, nothing is bundled
|
|
5
|
-
* - Keep canvasClearColor transparent so the 3D scene shows through
|
|
6
|
-
* - Aligned camera mode locks the 3D camera to the LittleJS 2D camera
|
|
7
|
-
* - ThreeJSObject lets LittleJS physics drive a three.js mesh
|
|
8
|
-
* - Call new ThreeJSPlugin(THREE) in gameInit to set up
|
|
9
|
-
* @namespace ThreeJS
|
|
10
|
-
*/
|
|
11
|
-
|
|
12
|
-
'use strict';
|
|
13
|
-
|
|
14
|
-
///////////////////////////////////////////////////////////////////////////////
|
|
15
|
-
|
|
16
|
-
/** Global ThreeJS plugin object
|
|
17
|
-
* @type {ThreeJSPlugin}
|
|
18
|
-
* @memberof ThreeJS */
|
|
19
|
-
let threeJS;
|
|
20
|
-
|
|
21
|
-
///////////////////////////////////////////////////////////////////////////////
|
|
22
|
-
/**
|
|
23
|
-
* ThreeJS Plugin - Renders a three.js scene behind the LittleJS canvas
|
|
24
|
-
* @example
|
|
25
|
-
* // in gameInit, with three.js loaded by the user
|
|
26
|
-
* new ThreeJSPlugin(THREE);
|
|
27
|
-
* threeJS.scene.add(new THREE.AmbientLight);
|
|
28
|
-
* @memberof ThreeJS
|
|
29
|
-
*/
|
|
30
|
-
class ThreeJSPlugin
|
|
31
|
-
{
|
|
32
|
-
/** Set up the three.js rendering layer, call in gameInit
|
|
33
|
-
* @param {Object} THREE - The three.js module, supplied by the user
|
|
34
|
-
* @param {number} [cameraFOV] - Vertical field of view in degrees */
|
|
35
|
-
constructor(THREE, cameraFOV=60)
|
|
36
|
-
{
|
|
37
|
-
ASSERT(!threeJS, 'ThreeJS plugin already initialized');
|
|
38
|
-
threeJS = this;
|
|
39
|
-
if (headlessMode) return;
|
|
40
|
-
ASSERT(mainCanvas, 'ThreeJS plugin must be created after engineInit, call in gameInit');
|
|
41
|
-
ASSERT(THREE && THREE.WebGLRenderer, 'three.js module must be passed in');
|
|
42
|
-
|
|
43
|
-
/** @property {Object} - The three.js module passed into the constructor */
|
|
44
|
-
this.THREE = THREE;
|
|
45
|
-
/** @property {Object} - The three.js renderer */
|
|
46
|
-
this.renderer = new THREE.WebGLRenderer({antialias: true});
|
|
47
|
-
/** @property {Object} - The three.js scene, add lights and meshes here */
|
|
48
|
-
this.scene = new THREE.Scene();
|
|
49
|
-
/** @property {Object} - The three.js perspective camera */
|
|
50
|
-
this.camera = new THREE.PerspectiveCamera(cameraFOV, 1, .1, 1e3);
|
|
51
|
-
/** @property {boolean} - Lock the camera to the LittleJS 2D camera so the z=0 plane matches world space */
|
|
52
|
-
this.cameraAlign2D = true;
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
//
|
|
84
|
-
const
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
this.camera.
|
|
89
|
-
this.camera.
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
*
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
{
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
{
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
1
|
+
/**
|
|
2
|
+
* LittleJS Three.js Plugin
|
|
3
|
+
* - Renders a three.js scene on a canvas behind the LittleJS canvases
|
|
4
|
+
* - The three.js module is passed in by the user, nothing is bundled
|
|
5
|
+
* - Keep canvasClearColor transparent so the 3D scene shows through
|
|
6
|
+
* - Aligned camera mode locks the 3D camera to the LittleJS 2D camera
|
|
7
|
+
* - ThreeJSObject lets LittleJS physics drive a three.js mesh
|
|
8
|
+
* - Call new ThreeJSPlugin(THREE) in gameInit to set up
|
|
9
|
+
* @namespace ThreeJS
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
'use strict';
|
|
13
|
+
|
|
14
|
+
///////////////////////////////////////////////////////////////////////////////
|
|
15
|
+
|
|
16
|
+
/** Global ThreeJS plugin object
|
|
17
|
+
* @type {ThreeJSPlugin}
|
|
18
|
+
* @memberof ThreeJS */
|
|
19
|
+
let threeJS;
|
|
20
|
+
|
|
21
|
+
///////////////////////////////////////////////////////////////////////////////
|
|
22
|
+
/**
|
|
23
|
+
* ThreeJS Plugin - Renders a three.js scene behind the LittleJS canvas
|
|
24
|
+
* @example
|
|
25
|
+
* // in gameInit, with three.js loaded by the user
|
|
26
|
+
* new ThreeJSPlugin(THREE);
|
|
27
|
+
* threeJS.scene.add(new THREE.AmbientLight);
|
|
28
|
+
* @memberof ThreeJS
|
|
29
|
+
*/
|
|
30
|
+
class ThreeJSPlugin
|
|
31
|
+
{
|
|
32
|
+
/** Set up the three.js rendering layer, call in gameInit
|
|
33
|
+
* @param {Object} THREE - The three.js module, supplied by the user
|
|
34
|
+
* @param {number} [cameraFOV] - Vertical field of view in degrees */
|
|
35
|
+
constructor(THREE, cameraFOV=60)
|
|
36
|
+
{
|
|
37
|
+
ASSERT(!threeJS, 'ThreeJS plugin already initialized');
|
|
38
|
+
threeJS = this;
|
|
39
|
+
if (headlessMode) return;
|
|
40
|
+
ASSERT(mainCanvas, 'ThreeJS plugin must be created after engineInit, call in gameInit');
|
|
41
|
+
ASSERT(THREE && THREE.WebGLRenderer, 'three.js module must be passed in');
|
|
42
|
+
|
|
43
|
+
/** @property {Object} - The three.js module passed into the constructor */
|
|
44
|
+
this.THREE = THREE;
|
|
45
|
+
/** @property {Object} - The three.js renderer */
|
|
46
|
+
this.renderer = new THREE.WebGLRenderer({antialias: true});
|
|
47
|
+
/** @property {Object} - The three.js scene, add lights and meshes here */
|
|
48
|
+
this.scene = new THREE.Scene();
|
|
49
|
+
/** @property {Object} - The three.js perspective camera */
|
|
50
|
+
this.camera = new THREE.PerspectiveCamera(cameraFOV, 1, .1, 1e3);
|
|
51
|
+
/** @property {boolean} - Lock the camera to the LittleJS 2D camera so the z=0 plane matches world space */
|
|
52
|
+
this.cameraAlign2D = true;
|
|
53
|
+
/** @property {number|undefined} - Near plane of the aligned camera before any zoom out, from camera.near on the
|
|
54
|
+
* first aligned frame; while aligned, change this rather than camera.near
|
|
55
|
+
* @type {number|undefined} */
|
|
56
|
+
this.cameraNear = undefined;
|
|
57
|
+
/** @property {number|undefined} - Far plane of the aligned camera before any zoom out, from camera.far on the
|
|
58
|
+
* first aligned frame; while aligned, change this rather than camera.far
|
|
59
|
+
* @type {number|undefined} */
|
|
60
|
+
this.cameraFar = undefined;
|
|
61
|
+
|
|
62
|
+
// insert the canvas below the engine canvases and match the layout
|
|
63
|
+
const threeCanvas = this.renderer.domElement;
|
|
64
|
+
const rootElement = mainCanvas.parentElement;
|
|
65
|
+
rootElement.insertBefore(threeCanvas, rootElement.firstChild);
|
|
66
|
+
threeCanvas.style.cssText = mainCanvas.style.cssText;
|
|
67
|
+
|
|
68
|
+
// composite the 3D canvas into screenshots and video capture
|
|
69
|
+
setBackgroundCanvas(threeCanvas);
|
|
70
|
+
|
|
71
|
+
// render automatically each frame after the engine renders
|
|
72
|
+
engineAddPlugin(undefined, ()=> this.render());
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/** Position the camera so the z=0 plane exactly matches LittleJS world space,
|
|
76
|
+
* called automatically when cameraAlign2D is set */
|
|
77
|
+
alignCamera2D()
|
|
78
|
+
{
|
|
79
|
+
const halfHeight = mainCanvasSize.y / 2 / cameraScale; // half visible height in world units
|
|
80
|
+
const distance = halfHeight / tan(this.camera.fov/2 * PI/180);
|
|
81
|
+
|
|
82
|
+
// zoomed out far enough that the z=0 plane would be past the far plane, push both planes out together
|
|
83
|
+
// so the depth precision stays what it was, and bring them back from where they began when zoomed in again
|
|
84
|
+
const baseNear = this.cameraNear ??= this.camera.near, baseFar = this.cameraFar ??= this.camera.far;
|
|
85
|
+
const scale = max(1, distance * 2 / baseFar), near = baseNear * scale, far = baseFar * scale;
|
|
86
|
+
if (near != this.camera.near || far != this.camera.far)
|
|
87
|
+
{
|
|
88
|
+
this.camera.near = near;
|
|
89
|
+
this.camera.far = far;
|
|
90
|
+
this.camera.updateProjectionMatrix();
|
|
91
|
+
}
|
|
92
|
+
this.camera.position.set(cameraPos.x, cameraPos.y, distance);
|
|
93
|
+
// reset all axes in case a free camera was used, littlejs angles are clockwise
|
|
94
|
+
this.camera.rotation.set(0, 0, -cameraAngle);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/** Sync the canvas layout and render the scene, called automatically each frame */
|
|
98
|
+
render()
|
|
99
|
+
{
|
|
100
|
+
if (!this.renderer) return; // headless mode
|
|
101
|
+
|
|
102
|
+
// keep renderer size and css in sync with the LittleJS canvas
|
|
103
|
+
// mainCanvasSize is css pixels, three scales it by the pixel ratio
|
|
104
|
+
const threeCanvas = this.renderer.domElement;
|
|
105
|
+
const dpr = getCanvasPixelRatio();
|
|
106
|
+
const bufferSizeX = mainCanvasSize.x * dpr | 0;
|
|
107
|
+
const bufferSizeY = mainCanvasSize.y * dpr | 0;
|
|
108
|
+
if (threeCanvas.width != bufferSizeX || threeCanvas.height != bufferSizeY)
|
|
109
|
+
{
|
|
110
|
+
this.renderer.setPixelRatio(dpr);
|
|
111
|
+
this.renderer.setSize(mainCanvasSize.x, mainCanvasSize.y, false);
|
|
112
|
+
this.camera.aspect = mainCanvasSize.x / mainCanvasSize.y;
|
|
113
|
+
this.camera.updateProjectionMatrix();
|
|
114
|
+
}
|
|
115
|
+
if (threeCanvas.style.cssText != mainCanvas.style.cssText)
|
|
116
|
+
threeCanvas.style.cssText = mainCanvas.style.cssText;
|
|
117
|
+
|
|
118
|
+
if (this.cameraAlign2D)
|
|
119
|
+
this.alignCamera2D();
|
|
120
|
+
this.renderer.render(this.scene, this.camera);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
///////////////////////////////////////////////////////////////////////////////
|
|
125
|
+
/**
|
|
126
|
+
* ThreeJS Object - EngineObject that drives a three.js mesh
|
|
127
|
+
* - LittleJS physics moves the object and the mesh follows automatically
|
|
128
|
+
* - Destroying the object removes the mesh from the scene
|
|
129
|
+
* @extends EngineObject
|
|
130
|
+
* @memberof ThreeJS
|
|
131
|
+
*/
|
|
132
|
+
class ThreeJSObject extends EngineObject
|
|
133
|
+
{
|
|
134
|
+
/** Create an engine object that drives a three.js mesh
|
|
135
|
+
* @param {Vector2} [pos] - World space position
|
|
136
|
+
* @param {Vector2} [size] - World space size
|
|
137
|
+
* @param {Object} [mesh] - The three.js object3d to drive
|
|
138
|
+
* @param {number} [z] - Mesh height above the 2D plane */
|
|
139
|
+
constructor(pos, size, mesh, z=0)
|
|
140
|
+
{
|
|
141
|
+
super(pos, size);
|
|
142
|
+
ASSERT(threeJS, 'ThreeJS plugin must be initialized first');
|
|
143
|
+
|
|
144
|
+
/** @property {Object} - The three.js object3d this object drives */
|
|
145
|
+
this.mesh = mesh;
|
|
146
|
+
/** @property {number} - Mesh height above the 2D plane */
|
|
147
|
+
this.z = z;
|
|
148
|
+
if (mesh)
|
|
149
|
+
{
|
|
150
|
+
threeJS.scene?.add(mesh); // no scene in headless mode
|
|
151
|
+
this.syncMesh();
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/** Update the transform and sync the mesh to it, after the parent has placed a child, and while paused too
|
|
156
|
+
* @param {boolean} [updateChildren] - Also update the children's transforms */
|
|
157
|
+
updateTransforms(updateChildren=true)
|
|
158
|
+
{
|
|
159
|
+
super.updateTransforms(updateChildren);
|
|
160
|
+
this.syncMesh();
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/** Copy this object's transform to the mesh */
|
|
164
|
+
syncMesh()
|
|
165
|
+
{
|
|
166
|
+
if (!this.mesh) return;
|
|
167
|
+
this.mesh.position.set(this.pos.x, this.pos.y, this.z);
|
|
168
|
+
this.mesh.rotation.z = -this.angle; // littlejs angles are clockwise
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/** The mesh is this object's visual, the default 2D rendering is skipped */
|
|
172
|
+
render() {}
|
|
173
|
+
|
|
174
|
+
/** Destroy this object and remove its mesh from the scene
|
|
175
|
+
* @param {boolean} [immediate] */
|
|
176
|
+
destroy(immediate)
|
|
177
|
+
{
|
|
178
|
+
if (this.destroyed) return;
|
|
179
|
+
// note: frequently destroyed objects should also dispose geometry and material
|
|
180
|
+
this.mesh && threeJS.scene?.remove(this.mesh);
|
|
181
|
+
super.destroy(immediate);
|
|
182
|
+
}
|
|
183
|
+
}
|