littlejsengine 1.23.1 → 1.24.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/CHANGELOG.md +234 -0
- package/COPYRIGHT.txt +0 -27
- package/FAQ.md +4 -9
- package/README.md +3 -2
- package/REFERENCE.md +30 -17
- package/dist/littlejs.d.ts +141 -97
- package/dist/littlejs.esm.js +407 -258
- package/dist/littlejs.esm.min.js +3 -3
- package/dist/littlejs.js +406 -254
- package/dist/littlejs.min.js +2 -2
- package/dist/littlejs.release.js +401 -253
- package/package.json +2 -1
- package/plugins/pluginExport.js +0 -4
- package/plugins/render3d.js +92 -17
- package/plugins/render3dEditor.js +5 -1
- package/plugins/render3dExtras.js +166 -70
- package/plugins/render3dLevel.js +24 -4
- package/src/engine.js +1 -1
- package/src/engineAudio.js +2 -2
- package/src/engineBuild.mjs +0 -1
- package/src/engineExport.js +1 -0
- package/src/engineTileLayer.js +117 -0
- package/plugins/zzfxm.js +0 -162
package/dist/littlejs.esm.js
CHANGED
|
@@ -35,7 +35,7 @@ const engineName = 'LittleJS';
|
|
|
35
35
|
* @type {string}
|
|
36
36
|
* @default
|
|
37
37
|
* @memberof Engine */
|
|
38
|
-
const engineVersion = '1.
|
|
38
|
+
const engineVersion = '1.24.0';
|
|
39
39
|
|
|
40
40
|
/** Frames per second to update
|
|
41
41
|
* @type {number}
|
|
@@ -8430,7 +8430,7 @@ function touchGamepadPointerUp(e)
|
|
|
8430
8430
|
* - 2D spatial audio based on camera position with distance-based falloff
|
|
8431
8431
|
* - Sound instance management (pause, resume, stop)
|
|
8432
8432
|
* - Speech synthesis for text-to-speech
|
|
8433
|
-
* - Music playback
|
|
8433
|
+
* - Music playback from audio files, looping by default
|
|
8434
8434
|
* - Web Audio API integration with master gain control
|
|
8435
8435
|
* - Sounds and the master bus can route through effects, see the audio effects plugin
|
|
8436
8436
|
* @namespace Audio
|
|
@@ -9308,7 +9308,7 @@ function playAudioBuffer(buffer, volume=1, rate=1, pan=0, loop=false, gainNode,
|
|
|
9308
9308
|
}
|
|
9309
9309
|
|
|
9310
9310
|
///////////////////////////////////////////////////////////////////////////////
|
|
9311
|
-
//
|
|
9311
|
+
// ZzFX - Zuper Zmall Zound Zynth - v1.4.0 by Frank Force
|
|
9312
9312
|
|
|
9313
9313
|
/** Generate and play a ZzFX sound
|
|
9314
9314
|
*
|
|
@@ -9593,6 +9593,10 @@ const tileLayersTiledFlips = [[0,0], [3,1], [2,1], [3,0], [0,1], [1,0], [2,0], [
|
|
|
9593
9593
|
* Load tile layers from exported data
|
|
9594
9594
|
* - Tiled maps come in as they are, flipped and turned tiles included, from one tileset image (a second tileset's
|
|
9595
9595
|
* tiles continue its numbering), finite maps in the CSV or array layer format; layer offsets and parallax are not read
|
|
9596
|
+
* - A tileset kept in the map with a margin or a spacing, a sheet with gaps between its tiles, is read where its
|
|
9597
|
+
* tiles are, whatever padding the tile info has; one in a file of its own (a tsx) is not read, pass a tile info
|
|
9598
|
+
* with its padding
|
|
9599
|
+
* - An LDtk level loads through tileLayersFromLDtk, which makes it a map like these
|
|
9596
9600
|
* - Group layers are flattened in order, each replaced by the layers inside it, so the layer indices
|
|
9597
9601
|
* (collisionLayer and the returned array) count that flattened list; a group's tint, opacity and
|
|
9598
9602
|
* visibility carry to the layers inside it
|
|
@@ -9625,6 +9629,18 @@ function tileLayersLoad(tileMapData, tileInfo=tileLayerDefaultTile(), renderOrde
|
|
|
9625
9629
|
ASSERT(tileMapData.width && tileMapData.height);
|
|
9626
9630
|
ASSERT(tileMapData.layers && tileMapData.layers.length);
|
|
9627
9631
|
|
|
9632
|
+
// a sheet with a margin around its tiles or a spacing between them: the first tile is at the margin and each
|
|
9633
|
+
// cell is a tile and a spacing, which is a padding of half the spacing counted from there, by the tileset's
|
|
9634
|
+
// own count of columns, since the image's width does not say with a spacing that is not all around
|
|
9635
|
+
const tileset = tileMapData.tilesets?.[0];
|
|
9636
|
+
if (tileInfo && tileset && (tileset.margin > 0 || tileset.spacing > 0))
|
|
9637
|
+
{
|
|
9638
|
+
const margin = tileset.margin || 0, spacing = tileset.spacing || 0, size = tileInfo.size;
|
|
9639
|
+
const width = tileInfo.textureInfo?.size.x || 0;
|
|
9640
|
+
const columns = tileset.columns || max(1, floor((width - margin*2 + spacing) / (size.x + spacing)));
|
|
9641
|
+
tileInfo = new TileInfo(vec2(margin), size, tileInfo.textureInfo, spacing / 2, tileInfo.bleed, columns);
|
|
9642
|
+
}
|
|
9643
|
+
|
|
9628
9644
|
// flatten group layers in order, a group's color and visibility carry to the layers inside it
|
|
9629
9645
|
/** @type {Array<{dataLayer: Object, color?: Color, visible?: boolean}>} */
|
|
9630
9646
|
const layers = [];
|
|
@@ -9702,6 +9718,107 @@ function tileLayersLoad(tileMapData, tileInfo=tileLayerDefaultTile(), renderOrde
|
|
|
9702
9718
|
function tileLayersColor(hex)
|
|
9703
9719
|
{ return new Color().setHex(hex.length === 9 ? '#' + hex.slice(3) + hex.slice(1, 3) : hex); }
|
|
9704
9720
|
|
|
9721
|
+
// the Tiled property type of an LDtk field type, for the ones Tiled has a property for
|
|
9722
|
+
const tileLayersLDtkTypes = {Int: 'int', Float: 'float', Bool: 'bool', String: 'string', Multilines: 'string',
|
|
9723
|
+
Color: 'color', FilePath: 'file'};
|
|
9724
|
+
|
|
9725
|
+
/**
|
|
9726
|
+
* Make a Tiled map of a level of an LDtk project, to load with tileLayersLoad and objectLayersLoad
|
|
9727
|
+
* - Each Tiles, AutoLayer and IntGrid layer is a tile layer, the bottom one first as in Tiled, so the last layer
|
|
9728
|
+
* of the LDtk file is layer 0; where LDtk stacks tiles in a cell, an edge over a fill, each tile over another
|
|
9729
|
+
* goes in a layer of its own just above, named with (2), (3) and so on, so a layer of the first tiles has one
|
|
9730
|
+
* wherever the LDtk layer has any, which makes it the one for collision
|
|
9731
|
+
* - An IntGrid layer with no tiles is a hidden layer of its values, for collision: pass its index as collisionLayer
|
|
9732
|
+
* - An Entities layer is an object layer: an entity's name is its type for objectLayersAddType, it is placed at
|
|
9733
|
+
* its middle, and its Int, Float, Bool, String, Color and FilePath fields are its properties (an enum is a string)
|
|
9734
|
+
* - The tileset is the first tile layer's, with its padding and spacing; give tileLayersLoad a tile info of its image
|
|
9735
|
+
* - The level is in the project file (not saved as separate level files), its layers of one grid size; a layer of
|
|
9736
|
+
* another grid size or another tileset is left out, with a warning in debug builds
|
|
9737
|
+
* - The level editor edits the map this returns, and saves it as a Tiled map
|
|
9738
|
+
* @param {Object} ldtk - The LDtk project, its JSON
|
|
9739
|
+
* @param {number|string} [level] - Which level, by its index or its identifier
|
|
9740
|
+
* @return {Object} - A Tiled map: width, height, tilewidth, tileheight, tilesets and layers
|
|
9741
|
+
* @example
|
|
9742
|
+
* const map = tileLayersFromLDtk(await fetchJSON('world.ldtk'), 'Level_0');
|
|
9743
|
+
* const layers = tileLayersLoad(map, tile(0, 16), 0, 1); // layer 1 is solid
|
|
9744
|
+
* objectLayersLoad(map);
|
|
9745
|
+
* @memberof TileLayers */
|
|
9746
|
+
function tileLayersFromLDtk(ldtk, level=0)
|
|
9747
|
+
{
|
|
9748
|
+
const levels = ldtk?.levels || [];
|
|
9749
|
+
const data = typeof level === 'string' ? levels.find((l)=> l.identifier === level) : levels[level];
|
|
9750
|
+
ASSERT(data, 'LDtk level not found', level);
|
|
9751
|
+
ASSERT(isArray(data.layerInstances), 'LDtk level has no layers: levels saved as separate files are not read');
|
|
9752
|
+
const instances = data.layerInstances;
|
|
9753
|
+
const tiled = (l)=> l.__type !== 'Entities';
|
|
9754
|
+
const first = instances.find((l)=> tiled(l) && l.__tilesetDefUid != undefined) || instances.find(tiled) || instances[0];
|
|
9755
|
+
const grid = first?.__gridSize || 16;
|
|
9756
|
+
const width = first?.__cWid || ceil(data.pxWid / grid), height = first?.__cHei || ceil(data.pxHei / grid);
|
|
9757
|
+
const map = {width, height, tilewidth: grid, tileheight: grid, orientation: 'orthogonal', renderorder: 'right-down',
|
|
9758
|
+
infinite: false, layers: [], nextlayerid: 1, nextobjectid: 1};
|
|
9759
|
+
|
|
9760
|
+
// the tileset of the first layer that has one, where its tiles are in its image
|
|
9761
|
+
const tileset = (ldtk?.defs?.tilesets || []).find((t)=> t.uid === first?.__tilesetDefUid);
|
|
9762
|
+
if (tileset)
|
|
9763
|
+
map.tilesets = [{firstgid: 1, name: String(tileset.relPath || '').replace(/^.*[\\/]/, '').replace(/\.\w+$/, ''),
|
|
9764
|
+
image: tileset.relPath, imagewidth: tileset.pxWid, imageheight: tileset.pxHei,
|
|
9765
|
+
tilewidth: tileset.tileGridSize, tileheight: tileset.tileGridSize, margin: tileset.padding || 0,
|
|
9766
|
+
spacing: tileset.spacing || 0, columns: tileset.__cWid, tilecount: tileset.__cWid * tileset.__cHei}];
|
|
9767
|
+
|
|
9768
|
+
// LDtk lists the top layer first, Tiled the bottom one
|
|
9769
|
+
for (let i = instances.length; i--;)
|
|
9770
|
+
{
|
|
9771
|
+
const instance = instances[i], name = instance.__identifier, id = map.nextlayerid++;
|
|
9772
|
+
if (!tiled(instance))
|
|
9773
|
+
{
|
|
9774
|
+
const objects = (instance.entityInstances || []).map((entity)=>
|
|
9775
|
+
{
|
|
9776
|
+
// an entity is placed by its pivot, an object here by its middle
|
|
9777
|
+
const [pivotX=0, pivotY=0] = entity.__pivot || [], w = entity.width || 0, h = entity.height || 0;
|
|
9778
|
+
const properties = [];
|
|
9779
|
+
for (const field of entity.fieldInstances || [])
|
|
9780
|
+
{
|
|
9781
|
+
const type = tileLayersLDtkTypes[field.__type] || (/^(Local|Extern)Enum\./.test(field.__type) && 'string');
|
|
9782
|
+
type && field.__value != undefined && properties.push({name: field.__identifier, type, value: field.__value});
|
|
9783
|
+
}
|
|
9784
|
+
return {id: map.nextobjectid++, name: '', type: entity.__identifier, x: entity.px[0] + (.5 - pivotX) * w,
|
|
9785
|
+
y: entity.px[1] + (.5 - pivotY) * h, width: w, height: h, rotation: 0, visible: true, properties};
|
|
9786
|
+
});
|
|
9787
|
+
map.layers.push({id, name, type: 'objectgroup', objects, opacity: 1, visible: true, x: 0, y: 0});
|
|
9788
|
+
continue;
|
|
9789
|
+
}
|
|
9790
|
+
const tiles = [...(instance.autoLayerTiles || []), ...(instance.gridTiles || [])];
|
|
9791
|
+
const usable = instance.__gridSize === grid && instance.__cWid === width && instance.__cHei === height &&
|
|
9792
|
+
(!tiles.length || instance.__tilesetDefUid === tileset?.uid);
|
|
9793
|
+
if (!usable)
|
|
9794
|
+
{
|
|
9795
|
+
debug && console.warn(`tileLayersFromLDtk: layer ${name} has another grid or tileset, left out`);
|
|
9796
|
+
continue;
|
|
9797
|
+
}
|
|
9798
|
+
const empty = ()=> new Array(width * height).fill(0), stacks = [empty()];
|
|
9799
|
+
if (tiles.length)
|
|
9800
|
+
{
|
|
9801
|
+
// a tile's place in pixels, its tile in the sheet, and its flips: bit 0 across, bit 1 down, which are
|
|
9802
|
+
// Tiled's two top bits; LDtk lists them in the order it draws them, so the first in a cell is the
|
|
9803
|
+
// bottom one and each after it goes a layer higher
|
|
9804
|
+
const depth = new Uint16Array(width * height);
|
|
9805
|
+
for (const t of tiles)
|
|
9806
|
+
{
|
|
9807
|
+
const x = floor(t.px[0] / grid), y = floor(t.px[1] / grid), cell = x + y * width;
|
|
9808
|
+
if (x >= 0 && x < width && y >= 0 && y < height)
|
|
9809
|
+
(stacks[depth[cell]++] ||= empty())[cell] =
|
|
9810
|
+
(t.t + 1 | (t.f & 1 ? 0x80000000 : 0) | (t.f & 2 ? 0x40000000 : 0)) >>> 0;
|
|
9811
|
+
}
|
|
9812
|
+
}
|
|
9813
|
+
else
|
|
9814
|
+
(instance.intGridCsv || []).forEach((value, k)=> k < stacks[0].length && (stacks[0][k] = value));
|
|
9815
|
+
const visible = instance.__type === 'IntGrid' && !tiles.length ? false : instance.visible !== false;
|
|
9816
|
+
stacks.forEach((data, k)=> map.layers.push({id: k ? map.nextlayerid++ : id, name: k ? `${name} (${k + 1})` : name,
|
|
9817
|
+
type: 'tilelayer', width, height, data, opacity: instance.__opacity ?? 1, visible, x: 0, y: 0}));
|
|
9818
|
+
}
|
|
9819
|
+
return map;
|
|
9820
|
+
}
|
|
9821
|
+
|
|
9705
9822
|
///////////////////////////////////////////////////////////////////////////////
|
|
9706
9823
|
// Object layers
|
|
9707
9824
|
|
|
@@ -14443,166 +14560,6 @@ class Light extends EngineObject
|
|
|
14443
14560
|
}
|
|
14444
14561
|
}
|
|
14445
14562
|
|
|
14446
|
-
/**
|
|
14447
|
-
* LittleJS ZzFXM Plugin
|
|
14448
|
-
* - A port of ZzFXM, the Zuper Zmall Zound Zynth music player, by Keith Clark and Frank Force, MIT licensed,
|
|
14449
|
-
* https://github.com/keithclark/ZzFXM; its notice is in COPYRIGHT.txt
|
|
14450
|
-
* @namespace ZzFXM
|
|
14451
|
-
*/
|
|
14452
|
-
|
|
14453
|
-
/**
|
|
14454
|
-
* Music Object - Stores a zzfx music track for later use
|
|
14455
|
-
*
|
|
14456
|
-
* <a href=https://keithclark.github.io/ZzFXM/>Create music with the ZzFXM tracker.</a>
|
|
14457
|
-
* @extends Sound
|
|
14458
|
-
* @memberof ZzFXM
|
|
14459
|
-
* @example
|
|
14460
|
-
* // create some music
|
|
14461
|
-
* const music_example = new ZzFXMusic(
|
|
14462
|
-
* [
|
|
14463
|
-
* [ // instruments
|
|
14464
|
-
* [,0,400] // simple note
|
|
14465
|
-
* ],
|
|
14466
|
-
* [ // patterns
|
|
14467
|
-
* [ // pattern 1
|
|
14468
|
-
* [ // channel 0
|
|
14469
|
-
* 0, -1, // instrument 0, left speaker
|
|
14470
|
-
* 1, 0, 9, 1 // channel notes
|
|
14471
|
-
* ],
|
|
14472
|
-
* [ // channel 1
|
|
14473
|
-
* 0, 1, // instrument 0, right speaker
|
|
14474
|
-
* 0, 12, 17, -1 // channel notes
|
|
14475
|
-
* ]
|
|
14476
|
-
* ],
|
|
14477
|
-
* ],
|
|
14478
|
-
* [0, 0, 0, 0], // sequence, play pattern 0 four times
|
|
14479
|
-
* 90 // BPM
|
|
14480
|
-
* ]);
|
|
14481
|
-
*
|
|
14482
|
-
* // play the music on a loop
|
|
14483
|
-
* music_example.playMusic();
|
|
14484
|
-
*/
|
|
14485
|
-
class ZzFXMusic extends Sound
|
|
14486
|
-
{
|
|
14487
|
-
/** Create a music object and cache the zzfx music samples for later use
|
|
14488
|
-
* @param {[Array, Array, Array, number?, ...any[]]} zzfxMusic - Array of zzfx music parameters: instruments, patterns, sequence, and an optional BPM; anything after it, like the tracker's metadata, is ignored
|
|
14489
|
-
*/
|
|
14490
|
-
constructor(zzfxMusic)
|
|
14491
|
-
{
|
|
14492
|
-
super(undefined);
|
|
14493
|
-
|
|
14494
|
-
if (!soundEnable || headlessMode) return;
|
|
14495
|
-
const [instruments, patterns, sequence, BPM] = zzfxMusic;
|
|
14496
|
-
super.sampleChannels = zzfxM(instruments, patterns, sequence, BPM); // the setter, without declaring a field that hides it in the typings
|
|
14497
|
-
this.buildSampleBuffer(); // hand the samples to an audio buffer now, like a zzfx sound, so the arrays are released
|
|
14498
|
-
this.loadedPercent = 1; // generated in place, so it is loaded like a zzfx sound
|
|
14499
|
-
}
|
|
14500
|
-
}
|
|
14501
|
-
|
|
14502
|
-
///////////////////////////////////////////////////////////////////////////////
|
|
14503
|
-
// ZzFX Music Renderer v2.0.3 by Keith Clark and Frank Force
|
|
14504
|
-
|
|
14505
|
-
/** Generate samples for a ZzFM song with given parameters
|
|
14506
|
-
* @param {Array} instruments - Array of ZzFX sound parameters
|
|
14507
|
-
* @param {Array} patterns - Array of pattern data
|
|
14508
|
-
* @param {Array} sequence - Array of pattern indexes
|
|
14509
|
-
* @param {number} [BPM] - Playback speed of the song in BPM
|
|
14510
|
-
* @return {Array} - Left and right channel sample data
|
|
14511
|
-
* @memberof ZzFXM */
|
|
14512
|
-
function zzfxM(instruments, patterns, sequence, BPM = 125)
|
|
14513
|
-
{
|
|
14514
|
-
let i, j, k;
|
|
14515
|
-
let instrumentParameters;
|
|
14516
|
-
let note;
|
|
14517
|
-
let sample;
|
|
14518
|
-
let patternChannel;
|
|
14519
|
-
let notFirstBeat;
|
|
14520
|
-
let stop;
|
|
14521
|
-
let instrument;
|
|
14522
|
-
let attenuation;
|
|
14523
|
-
let outSampleOffset;
|
|
14524
|
-
let isSequenceEnd;
|
|
14525
|
-
let sampleOffset = 0;
|
|
14526
|
-
let nextSampleOffset;
|
|
14527
|
-
let sampleBuffer = [];
|
|
14528
|
-
let leftChannelBuffer = [];
|
|
14529
|
-
let rightChannelBuffer = [];
|
|
14530
|
-
let channelIndex = 0;
|
|
14531
|
-
let panning = 0;
|
|
14532
|
-
let hasMore = 1;
|
|
14533
|
-
let sampleCache = {};
|
|
14534
|
-
let beatLength = audioDefaultSampleRate / BPM * 60 >> 2;
|
|
14535
|
-
|
|
14536
|
-
// for each channel in order until there are no more
|
|
14537
|
-
for (; hasMore; channelIndex++) {
|
|
14538
|
-
|
|
14539
|
-
// reset current values
|
|
14540
|
-
sampleBuffer = [hasMore = notFirstBeat = outSampleOffset = 0];
|
|
14541
|
-
|
|
14542
|
-
// for each pattern in sequence
|
|
14543
|
-
sequence.forEach((patternIndex, sequenceIndex)=> {
|
|
14544
|
-
// get pattern for current channel, use empty 1 note pattern if none found
|
|
14545
|
-
patternChannel = patterns[patternIndex][channelIndex] || [0, 0, 0];
|
|
14546
|
-
|
|
14547
|
-
// check if there are more channels
|
|
14548
|
-
hasMore |= patterns[patternIndex][channelIndex]&&1;
|
|
14549
|
-
|
|
14550
|
-
// get next offset, use the length of first channel
|
|
14551
|
-
nextSampleOffset = outSampleOffset + (patterns[patternIndex][0].length - 2 - (notFirstBeat?0:1)) * beatLength;
|
|
14552
|
-
// for each beat in pattern, plus one extra if end of sequence
|
|
14553
|
-
isSequenceEnd = sequenceIndex === sequence.length - 1;
|
|
14554
|
-
for (i = 2, k = outSampleOffset; i < patternChannel.length + isSequenceEnd; notFirstBeat = ++i) {
|
|
14555
|
-
|
|
14556
|
-
// <channel-note>
|
|
14557
|
-
note = patternChannel[i];
|
|
14558
|
-
|
|
14559
|
-
// stop if end, different instrument or new note
|
|
14560
|
-
stop = i === patternChannel.length + isSequenceEnd - 1 && isSequenceEnd ||
|
|
14561
|
-
instrument !== (patternChannel[0] || 0) || note | 0;
|
|
14562
|
-
|
|
14563
|
-
// fill buffer with samples for previous beat, most cpu intensive part
|
|
14564
|
-
for (j = 0; j < beatLength && notFirstBeat;
|
|
14565
|
-
|
|
14566
|
-
// fade off attenuation at end of beat if stopping note, prevents clicking
|
|
14567
|
-
j++ > beatLength - 99 && stop && attenuation < 1? attenuation += 1 / 99 : 0
|
|
14568
|
-
) {
|
|
14569
|
-
// copy sample to stereo buffers with panning
|
|
14570
|
-
sample = (1 - attenuation) * sampleBuffer[sampleOffset++] / 2 || 0;
|
|
14571
|
-
leftChannelBuffer[k] = (leftChannelBuffer[k] || 0) - sample * panning + sample;
|
|
14572
|
-
rightChannelBuffer[k] = (rightChannelBuffer[k++] || 0) + sample * panning + sample;
|
|
14573
|
-
}
|
|
14574
|
-
|
|
14575
|
-
// set up for next note
|
|
14576
|
-
if (note) {
|
|
14577
|
-
// set attenuation
|
|
14578
|
-
attenuation = note % 1;
|
|
14579
|
-
panning = patternChannel[1] || 0;
|
|
14580
|
-
if (note |= 0) {
|
|
14581
|
-
// get cached sample
|
|
14582
|
-
sampleBuffer = sampleCache[
|
|
14583
|
-
[
|
|
14584
|
-
instrument = patternChannel[sampleOffset = 0] || 0,
|
|
14585
|
-
note
|
|
14586
|
-
]
|
|
14587
|
-
] = sampleCache[[instrument, note]] || (
|
|
14588
|
-
// add sample to cache
|
|
14589
|
-
instrumentParameters = [...instruments[instrument]],
|
|
14590
|
-
instrumentParameters[2] = (instrumentParameters[2] || 220) * 2**(note / 12 - 1),
|
|
14591
|
-
|
|
14592
|
-
// allow negative values to stop notes
|
|
14593
|
-
note > 0 ? zzfxG(...instrumentParameters) : []
|
|
14594
|
-
);
|
|
14595
|
-
}
|
|
14596
|
-
}
|
|
14597
|
-
}
|
|
14598
|
-
|
|
14599
|
-
// update the sample offset
|
|
14600
|
-
outSampleOffset = nextSampleOffset;
|
|
14601
|
-
});
|
|
14602
|
-
}
|
|
14603
|
-
|
|
14604
|
-
return [leftChannelBuffer, rightChannelBuffer];
|
|
14605
|
-
}
|
|
14606
14563
|
/**
|
|
14607
14564
|
* LittleJS Audio Effects Plugin
|
|
14608
14565
|
* - Web Audio effects with a wet/dry mix: filter, reverb, delay, distortion, compressor
|
|
@@ -24025,8 +23982,8 @@ class Render3DPlugin
|
|
|
24025
23982
|
}
|
|
24026
23983
|
|
|
24027
23984
|
/** Find the nearest object under a screen position or along a ray, for clicking on things
|
|
24028
|
-
* -
|
|
24029
|
-
*
|
|
23985
|
+
* - A mesh is hit on its triangles, the ones that face the ray as they are drawn, both sides of a doubleSided
|
|
23986
|
+
* mesh; a sprite is hit as the quad it draws, and a height map or a voxel map on its surface
|
|
24030
23987
|
* - engineObjectsRaycast3D is the other half of this, every object along a ray instead of the nearest
|
|
24031
23988
|
* @param {Vector2|Ray3D} from - A screen position like mousePosScreen, or a ray to look along
|
|
24032
23989
|
* @param {Array<EngineObject>} [objects] - Defaults to every object; only those with a mesh or a sprite count
|
|
@@ -27393,8 +27350,8 @@ function engineObjectsCollect3D(pos, size, objects=engineObjects, testCenters=fa
|
|
|
27393
27350
|
return collected;
|
|
27394
27351
|
}
|
|
27395
27352
|
|
|
27396
|
-
// how far along a ray an object is hit, or undefined for a miss;
|
|
27397
|
-
//
|
|
27353
|
+
// how far along a ray an object is hit, or undefined for a miss; a mesh is hit on its triangles, in its own space,
|
|
27354
|
+
// and a sprite as the quad it draws
|
|
27398
27355
|
function render3DRaycastObject(ray, o)
|
|
27399
27356
|
{
|
|
27400
27357
|
if (o.destroyed || !(o instanceof EngineObject3D)) return;
|
|
@@ -27411,25 +27368,55 @@ function render3DRaycastObject(ray, o)
|
|
|
27411
27368
|
const distance = raycastSphere(ray, center, radius);
|
|
27412
27369
|
if (distance === undefined) return;
|
|
27413
27370
|
|
|
27414
|
-
// the sphere
|
|
27415
|
-
//
|
|
27371
|
+
// the sphere and then the box in the mesh's own space are quick rejects, and a ray that meets the box is tested
|
|
27372
|
+
// against the triangles; the direction is not made unit length, so the distance holds in the world;
|
|
27416
27373
|
// a mesh flattened to nothing on an axis has no inverse, it is hit as a disc like a sprite
|
|
27417
27374
|
if (!matrix.determinant()) return render3DRaycastDisc(ray, center, radius);
|
|
27418
27375
|
const inverse = matrix.copy().invert(), bounds = mesh.bounds || mesh.getBounds();
|
|
27419
27376
|
const local = new Ray3D(inverse.transformPoint(ray.origin), inverse.transformDirection(ray.direction));
|
|
27420
27377
|
const hit = raycastBox(local, bounds.min.add(bounds.max).scale(.5), bounds.max.subtract(bounds.min));
|
|
27421
|
-
if (hit
|
|
27422
|
-
|
|
27423
|
-
|
|
27424
|
-
|
|
27425
|
-
|
|
27426
|
-
|
|
27378
|
+
if (hit === undefined || !ray.direction.lengthSquared())
|
|
27379
|
+
return hit; // a ray of no length is where it starts, inside the box
|
|
27380
|
+
return render3DRaycastMesh(local, mesh);
|
|
27381
|
+
}
|
|
27382
|
+
|
|
27383
|
+
// how far along a ray, in the mesh's own space, its nearest triangle is hit, or undefined: the triangles that face
|
|
27384
|
+
// the ray as the pass draws them, clockwise from the front, and the back faces too of a doubleSided mesh; a strip's
|
|
27385
|
+
// triangle i is (i-2, i-1, i), the odd ones read the other way, and a join between its pieces has no area and is
|
|
27386
|
+
// never hit; nothing is kept between calls, so a mesh that changed is hit as it is now
|
|
27387
|
+
function render3DRaycastMesh(ray, mesh)
|
|
27388
|
+
{
|
|
27389
|
+
const points = mesh.points, indices = mesh.indices, both = mesh.doubleSided;
|
|
27390
|
+
const ox = ray.origin.x, oy = ray.origin.y, oz = ray.origin.z;
|
|
27391
|
+
const dx = ray.direction.x, dy = ray.direction.y, dz = ray.direction.z;
|
|
27392
|
+
const count = indices ? indices.length / 3 | 0 : points.length - 2;
|
|
27393
|
+
let nearest;
|
|
27394
|
+
for (let i = 0; i < count; ++i)
|
|
27427
27395
|
{
|
|
27428
|
-
|
|
27429
|
-
|
|
27430
|
-
|
|
27396
|
+
// the corners clockwise from the front: an indexed list is written the other way round
|
|
27397
|
+
let a, b, c;
|
|
27398
|
+
if (indices)
|
|
27399
|
+
a = points[indices[i*3]], b = points[indices[i*3+2]], c = points[indices[i*3+1]];
|
|
27400
|
+
else
|
|
27401
|
+
a = points[i & 1 ? i + 1 : i], b = points[i & 1 ? i : i + 1], c = points[i + 2];
|
|
27402
|
+
|
|
27403
|
+
// Moller and Trumbore: the determinant is the ray along the face's normal, below zero from the front
|
|
27404
|
+
const e1x = b.x - a.x, e1y = b.y - a.y, e1z = b.z - a.z;
|
|
27405
|
+
const e2x = c.x - a.x, e2y = c.y - a.y, e2z = c.z - a.z;
|
|
27406
|
+
const hx = dy * e2z - dz * e2y, hy = dz * e2x - dx * e2z, hz = dx * e2y - dy * e2x;
|
|
27407
|
+
const det = e1x * hx + e1y * hy + e1z * hz;
|
|
27408
|
+
if (both ? !det : !(det < 0)) continue;
|
|
27409
|
+
const sx = ox - a.x, sy = oy - a.y, sz = oz - a.z;
|
|
27410
|
+
const u = (sx * hx + sy * hy + sz * hz) / det;
|
|
27411
|
+
if (!(u >= 0 && u <= 1)) continue;
|
|
27412
|
+
const qx = sy * e1z - sz * e1y, qy = sz * e1x - sx * e1z, qz = sx * e1y - sy * e1x;
|
|
27413
|
+
const v = (dx * qx + dy * qy + dz * qz) / det;
|
|
27414
|
+
if (!(v >= 0 && u + v <= 1)) continue;
|
|
27415
|
+
const t = (e2x * qx + e2y * qy + e2z * qz) / det;
|
|
27416
|
+
if (t >= 0 && !(t >= nearest))
|
|
27417
|
+
nearest = t;
|
|
27431
27418
|
}
|
|
27432
|
-
return
|
|
27419
|
+
return nearest;
|
|
27433
27420
|
}
|
|
27434
27421
|
|
|
27435
27422
|
// a sprite is the quad drawBillboard draws, its size3D grown by the x and y scale as the draw does, facing the camera
|
|
@@ -27807,10 +27794,12 @@ class InstancedMesh3D extends EngineObject3D
|
|
|
27807
27794
|
* - radius is a world distance, so scale3D does not change it
|
|
27808
27795
|
* - An alpha, an intensity or a radius of 0 switches it off, and a light that is off takes none of those slots
|
|
27809
27796
|
* - Draws nothing but its glow, when it has one; add a small emissive mesh if the lamp itself should be seen
|
|
27797
|
+
* - addFlare gives it a lens flare, see LensFlare3D
|
|
27810
27798
|
* @extends EngineObject3D
|
|
27811
27799
|
* @memberof Render3D
|
|
27812
27800
|
* @example
|
|
27813
27801
|
* const torch = new Light3D(vec3(0, 3, 0), 10, hsl(.1, 1, .65));
|
|
27802
|
+
* torch.addFlare(); // light in the lens when the torch is in view
|
|
27814
27803
|
*/
|
|
27815
27804
|
class Light3D extends EngineObject3D
|
|
27816
27805
|
{
|
|
@@ -27845,6 +27834,49 @@ class Light3D extends EngineObject3D
|
|
|
27845
27834
|
* bright core */
|
|
27846
27835
|
this.glowFalloff = 1;
|
|
27847
27836
|
this.additive = true; // the glow is added on, in the transparent stage; a light with none draws nothing
|
|
27837
|
+
/** @type {LensFlare3D|undefined} */
|
|
27838
|
+
this.flareObject = undefined;
|
|
27839
|
+
}
|
|
27840
|
+
|
|
27841
|
+
/** Give the light a lens flare, made with the arguments of LensFlare3D, in place of the one it had
|
|
27842
|
+
* @param {number} [size] - Scales every part of it
|
|
27843
|
+
* @param {number} [count] - How many ghosts there are, besides the glow at the light
|
|
27844
|
+
* @param {number} [intensity] - How bright it is
|
|
27845
|
+
* @param {number} [saturation] - How colorful the ghosts are
|
|
27846
|
+
* @return {LensFlare3D} - The flare, to change: light.addFlare().shapes = ['hex'] */
|
|
27847
|
+
addFlare(size, count, intensity, saturation)
|
|
27848
|
+
{
|
|
27849
|
+
const flare = new LensFlare3D(size, count, intensity, saturation);
|
|
27850
|
+
this.flare = flare;
|
|
27851
|
+
return flare;
|
|
27852
|
+
}
|
|
27853
|
+
|
|
27854
|
+
/** The light's lens flare, undefined for none: addFlare makes it, or set a LensFlare3D of your own, and
|
|
27855
|
+
* destroying the flare takes it away
|
|
27856
|
+
* - The flare is the light's: it is attached to the light as its child, so it stays through a scene change
|
|
27857
|
+
* when the light does, and is destroyed with the light or when another takes its place
|
|
27858
|
+
* @type {LensFlare3D|undefined} */
|
|
27859
|
+
get flare() { return this.flareObject && !this.flareObject.destroyed ? this.flareObject : undefined; }
|
|
27860
|
+
|
|
27861
|
+
set flare(flare)
|
|
27862
|
+
{
|
|
27863
|
+
const old = this.flare; // a flare that was destroyed on its own is no flare
|
|
27864
|
+
if (flare === old) return;
|
|
27865
|
+
old && old.destroy();
|
|
27866
|
+
if (flare)
|
|
27867
|
+
{
|
|
27868
|
+
flare.light = this;
|
|
27869
|
+
flare.parent || this.addChild(flare);
|
|
27870
|
+
}
|
|
27871
|
+
this.flareObject = flare || undefined;
|
|
27872
|
+
}
|
|
27873
|
+
|
|
27874
|
+
/** Destroy the light, and its flare with it
|
|
27875
|
+
* @param {boolean} [immediate] */
|
|
27876
|
+
destroy(immediate)
|
|
27877
|
+
{
|
|
27878
|
+
this.flare = undefined;
|
|
27879
|
+
super.destroy(immediate);
|
|
27848
27880
|
}
|
|
27849
27881
|
|
|
27850
27882
|
/** Draw the glow, a quad facing the camera with a soft round glow on it, pulled toward the camera by half its
|
|
@@ -29363,31 +29395,74 @@ class Trail3D extends EngineObject3D
|
|
|
29363
29395
|
// lens flare
|
|
29364
29396
|
|
|
29365
29397
|
// the shapes a flare is made of, white on clear on one smooth texture made the first time it is asked for: a soft
|
|
29366
|
-
// glow, a flat disc with a brighter rim,
|
|
29398
|
+
// glow, a flat disc with a brighter rim, a ring, a hexagon like the blades of a lens, a streak across, and a star
|
|
29399
|
+
// of six rays; undefined headless or without a canvas
|
|
29400
|
+
const render3DFlareShapes = ['glow', 'disc', 'ring', 'hex', 'streak', 'star'];
|
|
29367
29401
|
let render3DFlareTiles;
|
|
29368
29402
|
function render3DFlareTile(shape)
|
|
29369
29403
|
{
|
|
29370
29404
|
if (!render3DFlareTiles)
|
|
29371
29405
|
{
|
|
29372
29406
|
if (headlessMode || !glContext || typeof OffscreenCanvas == 'undefined') return;
|
|
29373
|
-
const cell = 128, shapes =
|
|
29407
|
+
const cell = 128, shapes = render3DFlareShapes, context = createCanvasContext(cell * shapes.length, cell);
|
|
29374
29408
|
// each shape is how see-through it is from its middle out
|
|
29375
29409
|
const alpha =
|
|
29376
29410
|
{
|
|
29377
29411
|
glow: (t)=> engineGlowAlpha(t, 1),
|
|
29378
29412
|
disc: (t)=> t < .8 ? .4 + .1 * t : t < .92 ? .48 + (t - .8) * 3 : (1 - t) / .08 * .84,
|
|
29379
29413
|
ring: (t)=> max(0, 1 - abs(t - .86) / .12),
|
|
29414
|
+
hex: (t)=> t < .8 ? .4 + .1 * t : t < .92 ? .48 + (t - .8) * 3 : (1 - t) / .08 * .84,
|
|
29415
|
+
streak: (t)=> (1 - t) ** 1.5,
|
|
29380
29416
|
};
|
|
29381
|
-
|
|
29417
|
+
// a round gradient from the middle of the cell out, squashed for a ray and cut to a path for the hexagon
|
|
29418
|
+
const fill = (shape, r, path)=>
|
|
29382
29419
|
{
|
|
29383
|
-
const
|
|
29384
|
-
const gradient = context.createRadialGradient(x, y, 0, x, y, r);
|
|
29420
|
+
const steps = 32, gradient = context.createRadialGradient(0, 0, 0, 0, 0, r);
|
|
29385
29421
|
for (let k = 0; k <= steps; ++k)
|
|
29386
29422
|
gradient.addColorStop(k / steps, 'rgba(255,255,255,' + clamp(alpha[shape](k / steps)).toFixed(4) + ')');
|
|
29387
29423
|
context.fillStyle = gradient;
|
|
29388
29424
|
context.beginPath();
|
|
29389
|
-
context.arc(
|
|
29425
|
+
path ? path() : context.arc(0, 0, r, 0, 2 * PI);
|
|
29390
29426
|
context.fill();
|
|
29427
|
+
};
|
|
29428
|
+
const ray = (r, angle)=>
|
|
29429
|
+
{
|
|
29430
|
+
context.save();
|
|
29431
|
+
context.rotate(angle);
|
|
29432
|
+
context.scale(1, .07);
|
|
29433
|
+
fill('streak', r);
|
|
29434
|
+
context.restore();
|
|
29435
|
+
};
|
|
29436
|
+
shapes.forEach((shape, i)=>
|
|
29437
|
+
{
|
|
29438
|
+
const r = cell / 2 - 2;
|
|
29439
|
+
context.save();
|
|
29440
|
+
context.translate(i * cell + cell / 2, cell / 2);
|
|
29441
|
+
if (shape == 'hex')
|
|
29442
|
+
{
|
|
29443
|
+
// a gradient is round, so the hexagon is set pixel by pixel, by how far each is toward a side
|
|
29444
|
+
const image = context.createImageData(cell, cell), data = image.data, apothem = r * cos(PI / 6);
|
|
29445
|
+
for (let k = 0; k < cell * cell; ++k)
|
|
29446
|
+
{
|
|
29447
|
+
const x = abs(k % cell + .5 - cell / 2), y = abs((k / cell | 0) + .5 - cell / 2);
|
|
29448
|
+
const t = max(y, x * cos(PI / 6) + y / 2) / apothem;
|
|
29449
|
+
data[k * 4] = data[k * 4 + 1] = data[k * 4 + 2] = 255;
|
|
29450
|
+
data[k * 4 + 3] = t < 1 ? clamp(alpha.hex(t)) * 255 : 0;
|
|
29451
|
+
}
|
|
29452
|
+
context.putImageData(image, i * cell, 0);
|
|
29453
|
+
}
|
|
29454
|
+
else if (shape == 'streak')
|
|
29455
|
+
ray(r, 0);
|
|
29456
|
+
else if (shape == 'star')
|
|
29457
|
+
{
|
|
29458
|
+
for (let k = 3; k--;)
|
|
29459
|
+
ray(r, k * PI / 3 + PI / 2);
|
|
29460
|
+
context.scale(.3, .3);
|
|
29461
|
+
fill('glow', r);
|
|
29462
|
+
}
|
|
29463
|
+
else
|
|
29464
|
+
fill(shape, r);
|
|
29465
|
+
context.restore();
|
|
29391
29466
|
});
|
|
29392
29467
|
const texture = new TextureInfo(context.canvas, true, false, false); // smooth even in a pixel art game
|
|
29393
29468
|
render3DFlareTiles = new Map(shapes.map((shape, i)=>
|
|
@@ -29396,16 +29471,32 @@ function render3DFlareTile(shape)
|
|
|
29396
29471
|
return render3DFlareTiles.get(shape) || render3DFlareTiles.get('glow');
|
|
29397
29472
|
}
|
|
29398
29473
|
|
|
29474
|
+
/**
|
|
29475
|
+
* A part of a lens flare
|
|
29476
|
+
* @typedef {Object} LensFlareElement
|
|
29477
|
+
* @property {number} at - Where along the line: 0 the sun, 1 the middle of the screen, 2 as far past it
|
|
29478
|
+
* @property {number|Vector2} size - How big across, as a part of the screen's height, a vector for a part wider
|
|
29479
|
+
* than it is tall
|
|
29480
|
+
* @property {Color} color - Its color, the alpha how bright
|
|
29481
|
+
* @property {string} [shape] - glow, disc, ring, hex, streak or star, a glow when left out
|
|
29482
|
+
* @property {TileInfo} [tileInfo] - A tile of the game's own to draw in place of a shape, best white on clear
|
|
29483
|
+
* @property {number} [angle] - How far it is turned, in radians
|
|
29484
|
+
* @memberof Render3D
|
|
29485
|
+
*/
|
|
29486
|
+
|
|
29399
29487
|
/**
|
|
29400
29488
|
* LensFlare3D - The sun's lens flare, the old kind: a glow at the sun and a row of discs and rings of different
|
|
29401
29489
|
* sizes along the line from the sun through the middle of the screen
|
|
29402
29490
|
* - Make one and it shows, over the 3D scene and under what the game draws after, a HUD; destroy it to take it away
|
|
29403
29491
|
* - It follows render3D.sunDirection, and fades out as the sun leaves the screen or goes behind something
|
|
29492
|
+
* - A Light3D gets one of its own with light.addFlare()
|
|
29404
29493
|
* - flareSize, count, intensity and saturation set its look, seed picks another arrangement, and its color tints it,
|
|
29405
|
-
* with the sun's own color;
|
|
29406
|
-
*
|
|
29494
|
+
* with the sun's own color; shapes says what its ghosts are, glowSize and ghostSize how big its parts are; or
|
|
29495
|
+
* give it elements of your own, which may be tiles of the game's
|
|
29496
|
+
* - visible is how much of the sun shows, 0 to 1, eased over fadeTime, there for a game to read; it is 0 while the
|
|
29497
|
+
* sun is off the screen, where nothing is tested
|
|
29407
29498
|
* - What hides the sun is found with a ray from the camera, against the level and every object that is not see
|
|
29408
|
-
* through, each
|
|
29499
|
+
* through, each on the triangles of its mesh, see render3D.pick; turn it off with occlusion
|
|
29409
29500
|
* - It needs WebGL, and it draws nothing in the shadow of renderAfter2D
|
|
29410
29501
|
* @extends EngineObject3D
|
|
29411
29502
|
* @memberof Render3D
|
|
@@ -29435,48 +29526,61 @@ class LensFlare3D extends EngineObject3D
|
|
|
29435
29526
|
this.saturation = saturation;
|
|
29436
29527
|
/** @property {number} - Picks the arrangement of the ghosts, another seed is another flare */
|
|
29437
29528
|
this.seed = 1;
|
|
29438
|
-
/** @property {Array<
|
|
29439
|
-
*
|
|
29440
|
-
*
|
|
29441
|
-
*
|
|
29442
|
-
|
|
29529
|
+
/** @property {Array<string>|undefined} - The shapes the ghosts are picked from, glow, disc, ring, hex,
|
|
29530
|
+
* streak or star: ['hex'] makes every ghost a hexagon, and a shape listed twice is picked twice as often;
|
|
29531
|
+
* discs, rings and glows when not set
|
|
29532
|
+
* @type {Array<string>|undefined} */
|
|
29533
|
+
this.shapes = undefined;
|
|
29534
|
+
/** @property {number} - Scales the glow at the sun, 0 for none */
|
|
29535
|
+
this.glowSize = 1;
|
|
29536
|
+
/** @property {number} - Scales the ghosts */
|
|
29537
|
+
this.ghostSize = 1;
|
|
29538
|
+
/** @property {Array<LensFlareElement>|undefined} - The parts of the flare, to set your own in place of the
|
|
29539
|
+
* ones made from count, seed, saturation, shapes, glowSize and ghostSize
|
|
29540
|
+
* @type {Array<LensFlareElement>|undefined} */
|
|
29443
29541
|
this.elements = undefined;
|
|
29444
29542
|
/** @property {Light3D|undefined} - A light the flare is of in place of the sun, a lamp or a spotlight: the
|
|
29445
29543
|
* flare is at the light and in its color, smaller from farther than the light reaches, hidden by what is
|
|
29446
|
-
* in front of the light, and a spotlight's shows from inside its beam only
|
|
29544
|
+
* in front of the light, and a spotlight's shows from inside its beam only; a DirectionalLight3D's is far
|
|
29545
|
+
* away where it shines from, like the sun's; the flare is destroyed when its light is; light.addFlare
|
|
29546
|
+
* sets this
|
|
29447
29547
|
* @type {Light3D|undefined} */
|
|
29448
29548
|
this.light = undefined;
|
|
29449
29549
|
/** @property {boolean} - Fade out when something is between the camera and the sun */
|
|
29450
29550
|
this.occlusion = true;
|
|
29451
29551
|
/** @property {number} - Seconds the flare takes to fade out or in when the sun is hidden or shows again */
|
|
29452
29552
|
this.fadeTime = .15;
|
|
29453
|
-
/** @property {number} - How much of the sun shows, 0 hidden or behind the camera to 1 in
|
|
29553
|
+
/** @property {number} - How much of the sun shows, 0 hidden, off the screen or behind the camera to 1 in
|
|
29554
|
+
* plain view, eased */
|
|
29454
29555
|
this.visible = 1;
|
|
29455
29556
|
this.renderOrder = 1e9; // over the game's sprites, it is light in the lens
|
|
29456
29557
|
this.madeKey = '';
|
|
29457
|
-
/** @type {Array<
|
|
29558
|
+
/** @type {Array<LensFlareElement>} */
|
|
29458
29559
|
this.made = [];
|
|
29459
29560
|
}
|
|
29460
29561
|
|
|
29461
|
-
/** The parts of the flare: the elements set by hand, or the ones made from count, seed
|
|
29462
|
-
* and a core at the sun and the ghosts, made again when one of those changes
|
|
29463
|
-
* @return {Array<
|
|
29562
|
+
/** The parts of the flare: the elements set by hand, or the ones made from count, seed, saturation, shapes,
|
|
29563
|
+
* glowSize and ghostSize, a glow and a core at the sun and the ghosts, made again when one of those changes
|
|
29564
|
+
* @return {Array<LensFlareElement>} */
|
|
29464
29565
|
getElements()
|
|
29465
29566
|
{
|
|
29466
29567
|
if (this.elements) return this.elements;
|
|
29467
|
-
const
|
|
29568
|
+
const shapes = this.shapes?.length ? this.shapes : undefined, glow = this.glowSize, ghost = this.ghostSize;
|
|
29569
|
+
const key = [this.count, this.seed, this.saturation, glow, ghost, shapes?.length, shapes].join();
|
|
29468
29570
|
if (key !== this.madeKey)
|
|
29469
29571
|
{
|
|
29470
29572
|
const random = new RandomGenerator(this.seed * 7919 + 1), s = clamp(this.saturation);
|
|
29471
|
-
|
|
29472
|
-
|
|
29473
|
-
{at: 0, size: .
|
|
29573
|
+
/** @type {Array<LensFlareElement>} */
|
|
29574
|
+
const made = this.made = glow > 0 ? [
|
|
29575
|
+
{at: 0, size: .7 * glow, color: hsl(0, 0, 1, .5), shape: 'glow'},
|
|
29576
|
+
{at: 0, size: .25 * glow, color: hsl(0, 0, 1, .9), shape: 'glow'}] : [];
|
|
29474
29577
|
for (let i = 0; i < this.count; ++i)
|
|
29475
29578
|
{
|
|
29476
29579
|
// spread along the line, each a place of its own, the far ones bigger
|
|
29477
29580
|
const at = (i + random.float(.2, .8)) / max(this.count, 1) * 1.9 + .2;
|
|
29478
|
-
const pick = random.float(), shape = pick
|
|
29479
|
-
|
|
29581
|
+
const pick = random.float(), shape = shapes ? shapes[min(pick * shapes.length | 0, shapes.length - 1)] :
|
|
29582
|
+
pick < .5 ? 'disc' : pick < .8 ? 'ring' : 'glow';
|
|
29583
|
+
made.push({at, size: random.float(.04, .1) * (1 + at) * ghost, shape,
|
|
29480
29584
|
color: hsl(random.float(), s * .9, .6, random.float(.15, .4))});
|
|
29481
29585
|
}
|
|
29482
29586
|
this.madeKey = key;
|
|
@@ -29484,69 +29588,85 @@ class LensFlare3D extends EngineObject3D
|
|
|
29484
29588
|
return this.made;
|
|
29485
29589
|
}
|
|
29486
29590
|
|
|
29487
|
-
// what the flare is of, seen from the camera: the way to it, how far it is, Infinity for the sun
|
|
29488
|
-
//
|
|
29591
|
+
// what the flare is of, seen from the camera: the way to it, how far it is, Infinity for the sun and for a
|
|
29592
|
+
// directional light, which shines from its place toward the origin, and a point to find it on the screen by;
|
|
29593
|
+
// undefined with no direction, or a light that is gone or at the camera
|
|
29489
29594
|
flareSource()
|
|
29490
29595
|
{
|
|
29491
29596
|
const camera = render3D.camera.pos, light = this.light;
|
|
29492
|
-
if (
|
|
29597
|
+
if (light?.destroyed) return;
|
|
29598
|
+
if (!light || light.directional)
|
|
29493
29599
|
{
|
|
29494
|
-
const
|
|
29495
|
-
if (!
|
|
29496
|
-
const direction =
|
|
29600
|
+
const from = light ? light.getWorldPos3D() : render3D.sunDirection;
|
|
29601
|
+
if (!from.lengthSquared()) return;
|
|
29602
|
+
const direction = from.normalize();
|
|
29497
29603
|
return {direction, distance: Infinity, pos: camera.add(direction.scale(100))};
|
|
29498
29604
|
}
|
|
29499
|
-
if (light.destroyed) return;
|
|
29500
29605
|
const pos = light.getWorldPos3D(), offset = pos.subtract(camera), distance = offset.length();
|
|
29501
29606
|
return distance ? {direction: offset.scale(1 / distance), distance, pos} : undefined;
|
|
29502
29607
|
}
|
|
29503
29608
|
|
|
29504
|
-
|
|
29505
|
-
|
|
29506
|
-
|
|
29507
|
-
|
|
29609
|
+
// how the flare would show with nothing in the way: where its source is on the screen, how strong it is there,
|
|
29610
|
+
// fading as it leaves the screen, its tint and the height its sizes are parts of; undefined when it would not
|
|
29611
|
+
// show at all, behind the camera, off the screen, or of a light that is off or seen from outside its cone
|
|
29612
|
+
flareLook()
|
|
29508
29613
|
{
|
|
29509
|
-
const source = this.flareSource();
|
|
29510
|
-
|
|
29511
|
-
|
|
29512
|
-
|
|
29513
|
-
/** The parts of the flare as they are drawn now: each one's place on the screen, its size in pixels and its
|
|
29514
|
-
* color, dimmed by how much of the sun shows; empty when there is nothing to draw
|
|
29515
|
-
* @return {Array<{pos: Vector2, size: number, color: Color, shape: string}>} */
|
|
29516
|
-
getScreenElements()
|
|
29517
|
-
{
|
|
29518
|
-
const sun = this.getSunScreenPos(), center = mainCanvasSize.scale(.5);
|
|
29519
|
-
if (!sun || !center.x || !center.y) return [];
|
|
29614
|
+
const source = this.flareSource(), center = mainCanvasSize.scale(.5);
|
|
29615
|
+
const sun = source && render3D.worldToScreen(source.pos);
|
|
29616
|
+
if (!sun || !center.x || !center.y) return;
|
|
29520
29617
|
// it fades as the sun leaves the screen, gone when it is a third of the screen past the edge
|
|
29521
29618
|
const off = max(abs(sun.x - center.x) / center.x, abs(sun.y - center.y) / center.y);
|
|
29522
|
-
const strength =
|
|
29523
|
-
if (!(strength > 0)) return
|
|
29619
|
+
const strength = clamp((1.3 - off) / .5) * this.intensity;
|
|
29620
|
+
if (!(strength > 0)) return;
|
|
29524
29621
|
let tint = this.color.multiply(render3D.sunColor), height = mainCanvasSize.y * this.flareSize;
|
|
29525
|
-
const light = this.light
|
|
29622
|
+
const light = this.light;
|
|
29526
29623
|
if (light)
|
|
29527
29624
|
{
|
|
29528
|
-
if (!source) return [];
|
|
29529
29625
|
// a light's flare is the light's color, as bright as the light up to 1, and only from inside its cone
|
|
29530
29626
|
const cone = render3DLightCone(light), d = source.direction;
|
|
29531
29627
|
const inCone = clamp(-(cone[0] * d.x + cone[1] * d.y + cone[2] * d.z) - cone[3]);
|
|
29532
29628
|
const c = light.color, bright = c.a * clamp(light.intensity) * inCone;
|
|
29533
|
-
if (!(bright > 0)) return
|
|
29629
|
+
if (!(bright > 0)) return;
|
|
29534
29630
|
tint = this.color.multiply(rgb(c.r, c.g, c.b, bright));
|
|
29535
|
-
|
|
29631
|
+
if (!light.directional)
|
|
29632
|
+
height *= min(1, light.radius / source.distance); // smaller from farther than it reaches
|
|
29536
29633
|
}
|
|
29634
|
+
return height > 0 ? {sun, center, strength, tint, height} : undefined;
|
|
29635
|
+
}
|
|
29636
|
+
|
|
29637
|
+
/** Where the sun, or the flare's light, is on the screen, in pixels like mousePosScreen, undefined when it is
|
|
29638
|
+
* behind the camera
|
|
29639
|
+
* @return {Vector2|undefined} */
|
|
29640
|
+
getScreenPos()
|
|
29641
|
+
{
|
|
29642
|
+
const source = this.flareSource();
|
|
29643
|
+
return source && render3D.worldToScreen(source.pos);
|
|
29644
|
+
}
|
|
29645
|
+
|
|
29646
|
+
/** The parts of the flare as they are drawn now: each one's place on the screen, its size in pixels, a vector
|
|
29647
|
+
* when the element's is, and its color, dimmed by how much of the sun shows; empty when there is nothing to draw
|
|
29648
|
+
* @return {Array<{pos: Vector2, size: number|Vector2, color: Color, shape: string|undefined,
|
|
29649
|
+
* tileInfo: TileInfo|undefined, angle: number}>} */
|
|
29650
|
+
getScreenElements()
|
|
29651
|
+
{
|
|
29652
|
+
const look = this.visible > 0 && this.flareLook();
|
|
29653
|
+
if (!look) return [];
|
|
29654
|
+
const {sun, center, tint, height} = look, strength = look.strength * this.visible;
|
|
29537
29655
|
return this.getElements().map((e)=>
|
|
29538
29656
|
{
|
|
29539
29657
|
const c = e.color.multiply(tint);
|
|
29540
29658
|
// along the line and past the middle, a lerp would stop there
|
|
29541
|
-
|
|
29542
|
-
|
|
29659
|
+
const size = typeof e.size == 'number' ? e.size * height : e.size.scale(height);
|
|
29660
|
+
return {pos: sun.add(center.subtract(sun).scale(e.at)), size, shape: e.shape, tileInfo: e.tileInfo,
|
|
29661
|
+
angle: e.angle || 0, color: rgb(c.r, c.g, c.b, c.a * strength)};
|
|
29543
29662
|
});
|
|
29544
29663
|
}
|
|
29545
29664
|
|
|
29546
29665
|
/** Is something between the camera and the sun, or the flare's light: the level, or an object that is not see
|
|
29547
|
-
* through
|
|
29666
|
+
* through, hit on its triangles as render3D.pick hits it, so a mesh the camera is inside hides nothing unless
|
|
29667
|
+
* it is doubleSided
|
|
29548
29668
|
* @return {boolean} */
|
|
29549
|
-
|
|
29669
|
+
isHidden()
|
|
29550
29670
|
{
|
|
29551
29671
|
const source = this.flareSource();
|
|
29552
29672
|
if (!source) return true;
|
|
@@ -29560,22 +29680,29 @@ class LensFlare3D extends EngineObject3D
|
|
|
29560
29680
|
for (const map of maps)
|
|
29561
29681
|
if (map.raycast(ray, reach, (type)=> !map.blockType(type).seeThrough))
|
|
29562
29682
|
return true;
|
|
29563
|
-
//
|
|
29564
|
-
//
|
|
29565
|
-
|
|
29566
|
-
|
|
29567
|
-
|
|
29568
|
-
|
|
29569
|
-
|
|
29570
|
-
|
|
29683
|
+
// a light's lamp, the mesh the light is inside, does not hide it: one whose box, in its own space, the light
|
|
29684
|
+
// is in
|
|
29685
|
+
const lamp = (o)=>
|
|
29686
|
+
{
|
|
29687
|
+
if (!this.light || reach === Infinity || !o.mesh || o instanceof HeightMap) return false;
|
|
29688
|
+
const matrix = render3DObjectMatrix(o);
|
|
29689
|
+
if (!matrix.determinant()) return false;
|
|
29690
|
+
const p = matrix.copy().invert().transformPoint(source.pos), b = o.mesh.bounds || o.mesh.getBounds();
|
|
29691
|
+
return p.x >= b.min.x && p.x <= b.max.x && p.y >= b.min.y && p.y <= b.max.y &&
|
|
29692
|
+
p.z >= b.min.z && p.z <= b.max.z;
|
|
29693
|
+
};
|
|
29694
|
+
const hit = render3D.pick(ray, blockers.filter((o)=> !maps.some((map)=> map === o) && !lamp(o)));
|
|
29571
29695
|
return !!hit && hit.distance < reach;
|
|
29572
29696
|
}
|
|
29573
29697
|
|
|
29574
|
-
/** Ease visible toward whether the sun shows, called automatically each frame
|
|
29698
|
+
/** Ease visible toward whether the sun shows, called automatically each frame; a flare that would not show
|
|
29699
|
+
* wherever the sun is, off the screen or its light off, looks for nothing in the way */
|
|
29575
29700
|
update()
|
|
29576
29701
|
{
|
|
29577
29702
|
super.update();
|
|
29578
|
-
|
|
29703
|
+
if (this.light?.destroyed)
|
|
29704
|
+
return this.destroy(); // the flare of a light that is gone
|
|
29705
|
+
const shows = !!this.flareLook() && !(this.occlusion && this.isHidden());
|
|
29579
29706
|
const step = this.fadeTime > 0 ? timeDelta / this.fadeTime : 1;
|
|
29580
29707
|
this.visible = clamp(this.visible + (shows ? step : -step));
|
|
29581
29708
|
}
|
|
@@ -29589,8 +29716,9 @@ class LensFlare3D extends EngineObject3D
|
|
|
29589
29716
|
setAdditiveBlendMode(true);
|
|
29590
29717
|
for (const e of elements)
|
|
29591
29718
|
{
|
|
29592
|
-
const tileInfo = render3DFlareTile(e.shape);
|
|
29593
|
-
|
|
29719
|
+
const tileInfo = e.tileInfo || render3DFlareTile(e.shape);
|
|
29720
|
+
const size = typeof e.size == 'number' ? vec2(e.size) : e.size;
|
|
29721
|
+
tileInfo && drawTile(e.pos, size, tileInfo, e.color, e.angle, false, undefined, true, true);
|
|
29594
29722
|
}
|
|
29595
29723
|
setAdditiveBlendMode(false);
|
|
29596
29724
|
}
|
|
@@ -30704,8 +30832,9 @@ function level3DAddMesh(name, mesh, tileInfo, color=WHITE)
|
|
|
30704
30832
|
* are added before its objects are made, one the game added itself keeps its place
|
|
30705
30833
|
* - A level may set the scene too, in a scene block beside its objects: sky, three colors for straight up, the
|
|
30706
30834
|
* horizon and straight down, ambient, how much of them lights the scene, .5 when not given, sunDirection and
|
|
30707
|
-
* sunColor, fog, its start and end, fogColor, the horizon color when not given, and
|
|
30708
|
-
* leaves out stays as the game set it, and a level with no block changes
|
|
30835
|
+
* sunColor, fog, its start and end, fogColor, the horizon color when not given, shadows, and lensFlare, the
|
|
30836
|
+
* sun's lens flare; what the block leaves out stays as the game set it, and a level with no block changes
|
|
30837
|
+
* nothing
|
|
30709
30838
|
* @param {Object} level - The level, the level editor edits this same object
|
|
30710
30839
|
* @return {Array<any>} - What each object's type made, a function that made nothing is left out
|
|
30711
30840
|
* @memberof Level3D */
|
|
@@ -30930,8 +31059,25 @@ function level3DSceneApply(scene)
|
|
|
30930
31059
|
r.fogColor = color(scene.fogColor) || r.fogColor;
|
|
30931
31060
|
if (typeof scene.shadows === 'boolean')
|
|
30932
31061
|
r.shadows = scene.shadows;
|
|
31062
|
+
if (typeof scene.lensFlare === 'boolean')
|
|
31063
|
+
level3DSceneFlare(scene.lensFlare);
|
|
30933
31064
|
}
|
|
30934
31065
|
|
|
31066
|
+
// the sun's lens flare as a scene sets it: on makes one unless the sun has a flare already, the game's own or an
|
|
31067
|
+
// earlier level's, and off takes away the one a scene made, never the game's
|
|
31068
|
+
let level3DSunFlare;
|
|
31069
|
+
function level3DSceneFlare(on)
|
|
31070
|
+
{
|
|
31071
|
+
if (!on)
|
|
31072
|
+
{
|
|
31073
|
+
level3DSunFlare && !level3DSunFlare.destroyed && level3DSunFlare.destroy();
|
|
31074
|
+
level3DSunFlare = undefined;
|
|
31075
|
+
}
|
|
31076
|
+
else if (!level3DSunHasFlare())
|
|
31077
|
+
level3DSunFlare = new LensFlare3D;
|
|
31078
|
+
}
|
|
31079
|
+
const level3DSunHasFlare = ()=> engineObjects.some((o)=> o instanceof LensFlare3D && !o.destroyed && !o.light);
|
|
31080
|
+
|
|
30935
31081
|
// a vec3 of an array of three numbers, as the file has them, or the fallback
|
|
30936
31082
|
function level3DVector(value, fallback)
|
|
30937
31083
|
{
|
|
@@ -31280,7 +31426,8 @@ level3DAddType('Cylinder', function(pos, properties)
|
|
|
31280
31426
|
{ return level3DMakeShape(pos, properties, level3DCylinderMesh ||= buildCylinder()); },
|
|
31281
31427
|
{color: WHITE, tile: -1, solid: true});
|
|
31282
31428
|
// a light, a spotlight with a cone: cone is the angle in degrees from its forward out to the edge of its beam, the
|
|
31283
|
-
// object's rotation aims it, softness is how much of the cone fades,
|
|
31429
|
+
// object's rotation aims it, softness is how much of the cone fades, shadows makes it the one that casts them, and
|
|
31430
|
+
// lensFlare gives it a lens flare; not flare, since a property is also set on the object, where flare is the flare
|
|
31284
31431
|
level3DAddType('Light', function(pos, properties)
|
|
31285
31432
|
{
|
|
31286
31433
|
const light = new Light3D(pos, properties.radius, properties.color, properties.intensity);
|
|
@@ -31288,8 +31435,9 @@ level3DAddType('Light', function(pos, properties)
|
|
|
31288
31435
|
light.coneSoftness = properties.softness;
|
|
31289
31436
|
if (properties.shadows && render3D)
|
|
31290
31437
|
render3D.shadowLight = light;
|
|
31438
|
+
properties.lensFlare && light.addFlare();
|
|
31291
31439
|
return light;
|
|
31292
|
-
}, {color: WHITE, radius: 5, intensity: 1, cone: 0, softness: .2, shadows: false});
|
|
31440
|
+
}, {color: WHITE, radius: 5, intensity: 1, cone: 0, softness: .2, shadows: false, lensFlare: false});
|
|
31293
31441
|
|
|
31294
31442
|
/**
|
|
31295
31443
|
* LittleJS Particle Effects Plugin
|
|
@@ -36835,7 +36983,7 @@ function editor3DSceneState()
|
|
|
36835
36983
|
const r = render3D;
|
|
36836
36984
|
return {sky: r.sky, ambientColor: r.ambientColor.copy(), ambientGroundColor: r.ambientGroundColor?.copy(),
|
|
36837
36985
|
fogStart: r.fogStart, fogEnd: r.fogEnd, fogColor: r.fogColor?.copy(), sunDirection: r.sunDirection.copy(),
|
|
36838
|
-
sunColor: r.sunColor.copy(), shadows: r.shadows};
|
|
36986
|
+
sunColor: r.sunColor.copy(), shadows: r.shadows, sunFlare: !!level3DSunFlare && !level3DSunFlare.destroyed};
|
|
36839
36987
|
}
|
|
36840
36988
|
function editor3DSceneRestore(state)
|
|
36841
36989
|
{
|
|
@@ -36850,6 +36998,7 @@ function editor3DSceneRestore(state)
|
|
|
36850
36998
|
r.sunDirection = state.sunDirection.copy();
|
|
36851
36999
|
r.sunColor = state.sunColor.copy();
|
|
36852
37000
|
r.shadows = state.shadows;
|
|
37001
|
+
level3DSceneFlare(state.sunFlare); // a flare an earlier level's scene gave the sun, never the game's own
|
|
36853
37002
|
}
|
|
36854
37003
|
|
|
36855
37004
|
// the scene on screen as a block, to start a level's scene from: a sky only when the dome's colors are known
|
|
@@ -36871,6 +37020,7 @@ function editor3DSceneFromView()
|
|
|
36871
37020
|
scene.fog = [editor3DRound(r.fogStart || 0), editor3DRound(r.fogEnd || 0)];
|
|
36872
37021
|
scene.fogColor = editor3DHex(r.fogColor || canvasClearColor);
|
|
36873
37022
|
scene.shadows = !!r.shadows;
|
|
37023
|
+
scene.lensFlare = level3DSunHasFlare();
|
|
36874
37024
|
return scene;
|
|
36875
37025
|
}
|
|
36876
37026
|
|
|
@@ -39859,6 +40009,8 @@ function editor3DSceneUpdate(box)
|
|
|
39859
40009
|
color('Fog color', 'The sky\'s horizon color when the level has a sky and no fog color',
|
|
39860
40010
|
scene.fogColor ?? (sky ? sky[1] : view.fogColor), (hex)=> change('fogColor', hex));
|
|
39861
40011
|
toggle('Shadows', 'The sun casts shadows', !!value('shadows'), (on)=> change('shadows', on));
|
|
40012
|
+
toggle('Lens flare', 'The sun flares in the lens when it is in view', !!value('lensFlare'), (on)=>
|
|
40013
|
+
change('lensFlare', on));
|
|
39862
40014
|
}
|
|
39863
40015
|
|
|
39864
40016
|
// the properties box: the position, rotation and scale of the one selected object and an input for each default
|
|
@@ -40418,6 +40570,7 @@ export
|
|
|
40418
40570
|
tileCollisionTest,
|
|
40419
40571
|
tileCollisionRaycast,
|
|
40420
40572
|
tileLayersLoad,
|
|
40573
|
+
tileLayersFromLDtk,
|
|
40421
40574
|
objectLayersAddType,
|
|
40422
40575
|
objectLayersLoad,
|
|
40423
40576
|
TileLayerData,
|
|
@@ -40476,10 +40629,6 @@ export
|
|
|
40476
40629
|
LightSystemPlugin,
|
|
40477
40630
|
Light,
|
|
40478
40631
|
|
|
40479
|
-
// ZzFXMusic
|
|
40480
|
-
ZzFXMusic,
|
|
40481
|
-
zzfxM,
|
|
40482
|
-
|
|
40483
40632
|
// Audio Effects
|
|
40484
40633
|
AudioEffect,
|
|
40485
40634
|
AudioFilter,
|