littlejsengine 1.22.0 → 1.23.1
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/COPYRIGHT.txt +65 -38
- package/EDITOR.md +359 -0
- package/README.md +12 -7
- package/REFERENCE.md +2314 -0
- package/dist/littlejs.d.ts +640 -100
- package/dist/littlejs.esm.js +3596 -1444
- package/dist/littlejs.esm.min.js +3 -3
- package/dist/littlejs.js +3583 -1444
- package/dist/littlejs.min.js +2 -2
- package/dist/littlejs.release.js +2106 -1355
- package/package.json +4 -3
- package/plugins/gltf.js +29 -13
- package/plugins/lightSystem.js +8 -6
- package/plugins/parallax.js +174 -0
- package/plugins/particleEffects.js +37 -22
- package/plugins/pluginExport.js +8 -0
- package/plugins/postProcess.js +5 -2
- package/plugins/render3d.js +59 -21
- package/plugins/render3dEditor.js +1058 -118
- package/plugins/render3dExtras.js +82 -21
- package/plugins/render3dLevel.js +328 -8
- package/plugins/textureSheet.js +1 -0
- package/plugins/tweenSystem.js +39 -34
- package/plugins/uiSystem.js +7 -3
- package/plugins/zzfxm.js +161 -159
- package/src/engine.js +1 -1
- package/src/engineAudio.js +17 -3
- package/src/engineBuild.mjs +18 -2
- package/src/engineDraw.js +26 -10
- package/src/engineEditor.js +515 -52
- package/src/engineExport.js +5 -0
- package/src/engineInput.js +48 -6
- package/src/engineObject.js +675 -675
- package/src/engineRelease.js +17 -2
- package/src/engineSettings.js +14 -0
- package/src/engineTileLayer.js +2 -1
- package/src/engineUtilities.js +291 -291
- package/src/engineWebGL.js +12 -3
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "littlejsengine",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.23.1",
|
|
4
4
|
"description": "LittleJS - Tiny and Fast HTML5 Game Engine",
|
|
5
5
|
"main": "dist/littlejs.esm.js",
|
|
6
6
|
"types": "dist/littlejs.d.ts",
|
|
@@ -39,7 +39,9 @@
|
|
|
39
39
|
"plugins/",
|
|
40
40
|
"!plugins/box2d.wasm.*",
|
|
41
41
|
"COPYRIGHT.txt",
|
|
42
|
-
"FAQ.md"
|
|
42
|
+
"FAQ.md",
|
|
43
|
+
"REFERENCE.md",
|
|
44
|
+
"EDITOR.md"
|
|
43
45
|
],
|
|
44
46
|
"scripts": {
|
|
45
47
|
"build": "node src/engineBuild.mjs",
|
|
@@ -53,7 +55,6 @@
|
|
|
53
55
|
},
|
|
54
56
|
"devDependencies": {
|
|
55
57
|
"bestzip": "^2.2.1",
|
|
56
|
-
"clean-jsdoc-theme": "~4.2.9",
|
|
57
58
|
"electron": "^38.2.0",
|
|
58
59
|
"electron-packager": "^17.1.2",
|
|
59
60
|
"google-closure-compiler": "~20230502.0.0",
|
package/plugins/gltf.js
CHANGED
|
@@ -405,6 +405,11 @@ async function parseGLTF(data, baseUrl='')
|
|
|
405
405
|
for (const name of json.extensionsRequired || [])
|
|
406
406
|
if (name === 'KHR_draco_mesh_compression' || name === 'EXT_meshopt_compression')
|
|
407
407
|
throw new Error(`glTF with ${name} is not read, export it uncompressed`);
|
|
408
|
+
else if (name === 'KHR_texture_basisu')
|
|
409
|
+
throw new Error('glTF with KHR_texture_basisu textures is not read, export them as png or jpeg');
|
|
410
|
+
// one that only may use them has other images to fall back on, or none: said, since it loads with them left out
|
|
411
|
+
if (json.extensionsUsed?.includes('KHR_texture_basisu') && !json.extensionsRequired?.includes('KHR_texture_basisu'))
|
|
412
|
+
console.warn('glTF: KHR_texture_basisu textures are not read and are left out, export them as png or jpeg');
|
|
408
413
|
|
|
409
414
|
// the buffers: the GLB's own, a data uri, or a file beside the model
|
|
410
415
|
const buffers = await Promise.all((json.buffers || []).map((buffer, i)=>
|
|
@@ -447,7 +452,8 @@ async function parseGLTF(data, baseUrl='')
|
|
|
447
452
|
const bitmap = normalTextures.has(index) ?
|
|
448
453
|
await createImageBitmap(blob, {colorSpaceConversion: 'none', premultiplyAlpha: 'none'}) :
|
|
449
454
|
opaque ? await createImageBitmap(blob, {premultiplyAlpha: 'none'}).then(gltfOpaqueImage) : await createImageBitmap(blob);
|
|
450
|
-
|
|
455
|
+
// REPEAT by default, and hard edged only when its sampler says NEAREST, not as the game's tiles are
|
|
456
|
+
return new TextureInfo(bitmap, true, [sampler.wrapS ?? 10497, sampler.wrapT ?? 10497], sampler.magFilter === 9728);
|
|
451
457
|
}
|
|
452
458
|
catch (e) { LOG('glTF image not loaded', e); }
|
|
453
459
|
}));
|
|
@@ -557,17 +563,25 @@ function gltfOpaqueImage(image)
|
|
|
557
563
|
{
|
|
558
564
|
const gl = glContext, {width, height} = image;
|
|
559
565
|
if (gl.isContextLost()) return image; // nothing to read back through, it keeps its alpha
|
|
560
|
-
|
|
561
|
-
gl.bindTexture(gl.TEXTURE_2D, texture);
|
|
562
|
-
gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, image);
|
|
563
|
-
gl.bindFramebuffer(gl.FRAMEBUFFER, framebuffer);
|
|
564
|
-
gl.framebufferTexture2D(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, gl.TEXTURE_2D, texture, 0);
|
|
566
|
+
// the room to read into first: for a huge image it can fail, and must not with the framebuffer bound
|
|
565
567
|
const data = new Uint8ClampedArray(width * height * 4);
|
|
566
|
-
gl.
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
568
|
+
const texture = gl.createTexture(), framebuffer = gl.createFramebuffer(), bound = gl.getParameter(gl.FRAMEBUFFER_BINDING);
|
|
569
|
+
try
|
|
570
|
+
{
|
|
571
|
+
gl.bindTexture(gl.TEXTURE_2D, texture);
|
|
572
|
+
gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, image);
|
|
573
|
+
gl.bindFramebuffer(gl.FRAMEBUFFER, framebuffer);
|
|
574
|
+
gl.framebufferTexture2D(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, gl.TEXTURE_2D, texture, 0);
|
|
575
|
+
gl.readPixels(0, 0, width, height, gl.RGBA, gl.UNSIGNED_BYTE, data); // row 0 is the image's first row, as uploaded
|
|
576
|
+
}
|
|
577
|
+
finally
|
|
578
|
+
{
|
|
579
|
+
// the 2D renderer's own bindings back, whatever happened
|
|
580
|
+
gl.bindFramebuffer(gl.FRAMEBUFFER, bound);
|
|
581
|
+
gl.bindTexture(gl.TEXTURE_2D, glActiveTexture);
|
|
582
|
+
gl.deleteFramebuffer(framebuffer);
|
|
583
|
+
gl.deleteTexture(texture);
|
|
584
|
+
}
|
|
571
585
|
for (let i = 3; i < data.length; i += 4)
|
|
572
586
|
data[i] = 255;
|
|
573
587
|
image.close();
|
|
@@ -704,7 +718,7 @@ function gltfAccessor(json, buffers, index)
|
|
|
704
718
|
}
|
|
705
719
|
if (scale !== 1)
|
|
706
720
|
for (let i = 0; i < out.length; ++i)
|
|
707
|
-
out[i]
|
|
721
|
+
out[i] = max(out[i] / scale, -1); // a signed type's lowest value is -1 too, as the format says
|
|
708
722
|
return {data: out, components, count: a.count};
|
|
709
723
|
}
|
|
710
724
|
|
|
@@ -768,8 +782,10 @@ function gltfPart(json, buffers, textures, primitive, matrix, name)
|
|
|
768
782
|
const [er, eg, eb] = material.emissiveFactor || [0, 0, 0];
|
|
769
783
|
if (er || eg || eb)
|
|
770
784
|
{
|
|
785
|
+
// KHR_materials_emissive_strength makes it that many times brighter, emission above 1 in Blender
|
|
786
|
+
const strength = material.extensions?.KHR_materials_emissive_strength?.emissiveStrength ?? 1;
|
|
771
787
|
part.emissiveMap = emissiveRef ? textures[emissiveRef.index] : gltfWhiteTexture();
|
|
772
|
-
part.emissiveMapColor = rgb(gltfSRGB(er), gltfSRGB(eg), gltfSRGB(eb));
|
|
788
|
+
part.emissiveMapColor = rgb(gltfSRGB(er) * strength, gltfSRGB(eg) * strength, gltfSRGB(eb) * strength);
|
|
773
789
|
}
|
|
774
790
|
return part;
|
|
775
791
|
}
|
package/plugins/lightSystem.js
CHANGED
|
@@ -425,13 +425,13 @@ class LightSystemPlugin
|
|
|
425
425
|
// an automatic size follows the canvas, so reallocate the lightmap when
|
|
426
426
|
// the canvas changed size, after the flush so the batch keeps its texture
|
|
427
427
|
const size = lightSystem.textureSize;
|
|
428
|
-
|
|
429
|
-
|
|
428
|
+
const wantX = glClampTextureSize(mainCanvasSize.x), wantY = glClampTextureSize(mainCanvasSize.y);
|
|
429
|
+
if (lightSystem.textureSizeAuto && (size.x !== wantX || size.y !== wantY))
|
|
430
430
|
{
|
|
431
|
-
lightSystem.textureSize =
|
|
431
|
+
lightSystem.textureSize = vec2(wantX, wantY);
|
|
432
432
|
glContext.bindTexture(glContext.TEXTURE_2D, lightSystem.texture);
|
|
433
433
|
glContext.texImage2D(glContext.TEXTURE_2D, 0, glContext.RGBA,
|
|
434
|
-
|
|
434
|
+
wantX, wantY, 0,
|
|
435
435
|
glContext.RGBA, glContext.UNSIGNED_BYTE, null);
|
|
436
436
|
// put back the texture the engine tracks, a draw in renderLight must not sample the lightmap
|
|
437
437
|
if (glActiveTexture)
|
|
@@ -571,8 +571,10 @@ class LightSystemPlugin
|
|
|
571
571
|
this.shadowMapSize = glClampTextureSize(this.shadowMapSize);
|
|
572
572
|
this.shadowTextureSize = glClampTextureSize(this.shadowTextureSize);
|
|
573
573
|
const size = this.textureSize;
|
|
574
|
-
if (size)
|
|
575
|
-
|
|
574
|
+
if (!size) return;
|
|
575
|
+
const x = glClampTextureSize(size.x), y = glClampTextureSize(size.y);
|
|
576
|
+
if (x !== size.x || y !== size.y)
|
|
577
|
+
this.textureSize = vec2(x, y); // only when it is too large, this runs every frame
|
|
576
578
|
}
|
|
577
579
|
|
|
578
580
|
/** Draw a single Light's falloff blob into the currently bound lightmap.
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* LittleJS Parallax Plugin
|
|
3
|
+
* - A background layer that follows the camera by a part of its movement, so it looks far away
|
|
4
|
+
* - Repeats across the view, so a level of any width has a background
|
|
5
|
+
* - Draws mountains by default, or anything a game draws into it
|
|
6
|
+
* @namespace Parallax
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
'use strict';
|
|
10
|
+
|
|
11
|
+
///////////////////////////////////////////////////////////////////////////////
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* A background layer with parallax: an image that follows the camera by a part of its movement and repeats across
|
|
15
|
+
* the view, drawn once into a canvas by a function, mountains when none is given
|
|
16
|
+
* - A far layer has a parallax near 1 and a low renderOrder, a near one a smaller parallax and a higher renderOrder
|
|
17
|
+
* - The image should meet itself at its left and right edges to repeat without a seam, as the mountains do
|
|
18
|
+
* @memberof Parallax
|
|
19
|
+
* @extends CanvasLayer
|
|
20
|
+
* @example
|
|
21
|
+
* // three ranges of mountains, the far ones lighter and slower
|
|
22
|
+
* for (let i = 3; i--;)
|
|
23
|
+
* new ParallaxLayer(vec2(0, i*2), vec2(40, 20), .9 - i*.2, -1e3 + i,
|
|
24
|
+
* parallaxMountains(hsl(.6, .3, .7 - i*.2), hsl(.6, .5, .2), i));
|
|
25
|
+
*
|
|
26
|
+
* // or draw your own image into the layer
|
|
27
|
+
* new ParallaxLayer(vec2(), vec2(40, 20), .5, -1e3, (context, size)=>
|
|
28
|
+
* {
|
|
29
|
+
* context.fillStyle = '#fff';
|
|
30
|
+
* context.fillRect(size.x/2 - 20, size.y/2 - 20, 40, 40);
|
|
31
|
+
* });
|
|
32
|
+
*/
|
|
33
|
+
class ParallaxLayer extends CanvasLayer
|
|
34
|
+
{
|
|
35
|
+
/** Create a parallax layer and draw its image
|
|
36
|
+
* @param {Vector2} [pos] - Where the middle of the image is in the world when the camera is there too
|
|
37
|
+
* @param {Vector2} [size] - World size of one copy of the image
|
|
38
|
+
* @param {number|Vector2} [parallax] - How much of the camera's movement it follows, 0 stays with the world
|
|
39
|
+
* and 1 with the screen, a Vector2 to follow x and y by different amounts
|
|
40
|
+
* @param {number} [renderOrder] - Low to draw behind the game, far layers lowest
|
|
41
|
+
* @param {function(OffscreenCanvasRenderingContext2D, Vector2, ParallaxLayer): void} [drawFunction] - Draws
|
|
42
|
+
* the image, given the canvas context, its size in pixels and the layer; mountains when not given
|
|
43
|
+
* @param {Vector2} [canvasSize] - Size of the image in pixels */
|
|
44
|
+
constructor(pos=vec2(), size=vec2(32, 16), parallax=.5, renderOrder=-1e3, drawFunction=parallaxMountains(),
|
|
45
|
+
canvasSize=vec2(512, 256))
|
|
46
|
+
{
|
|
47
|
+
ASSERT(isNumber(parallax) || isVector2(parallax), 'parallax must be a number or a Vector2');
|
|
48
|
+
ASSERT(typeof drawFunction == 'function', 'drawFunction must be a function');
|
|
49
|
+
super(pos, size, 0, renderOrder, canvasSize);
|
|
50
|
+
|
|
51
|
+
/** @property {Vector2} - How much of the camera's movement it follows on each axis, 0 stays with the world
|
|
52
|
+
* and 1 with the screen */
|
|
53
|
+
this.parallax = isVector2(parallax) ? /** @type {Vector2} */ (parallax).copy() :
|
|
54
|
+
vec2(/** @type {number} */ (parallax));
|
|
55
|
+
/** @property {boolean} - Repeat the image across the view, left and right */
|
|
56
|
+
this.wrapX = true;
|
|
57
|
+
/** @property {boolean} - Repeat the image up and down the view */
|
|
58
|
+
this.wrapY = false;
|
|
59
|
+
/** @property {number} - How much the camera's zoom changes its size on the screen: 1 like the world, 0 not
|
|
60
|
+
* at all, the same on the screen at any zoom as it is at zoomScale */
|
|
61
|
+
this.zoomFollow = 1;
|
|
62
|
+
/** @property {number} - The camera scale its size is given for, used when zoomFollow is under 1 */
|
|
63
|
+
this.zoomScale = cameraScale;
|
|
64
|
+
/** @property {function(OffscreenCanvasRenderingContext2D, Vector2, ParallaxLayer): void} - Draws the
|
|
65
|
+
* image, redraw() after changing it */
|
|
66
|
+
this.drawFunction = drawFunction;
|
|
67
|
+
this.redraw();
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/** Draw the image again with drawFunction, after changing it or what it draws from */
|
|
71
|
+
redraw()
|
|
72
|
+
{
|
|
73
|
+
const context = this.context;
|
|
74
|
+
if (!context) return; // headless
|
|
75
|
+
const size = vec2(this.canvas.width, this.canvas.height);
|
|
76
|
+
context.clearRect(0, 0, size.x, size.y);
|
|
77
|
+
this.drawFunction(context, size, this);
|
|
78
|
+
this.updateWebGL();
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// how much smaller it is drawn than its size, from the camera's zoom and how much of it the layer follows
|
|
82
|
+
zoomFactor()
|
|
83
|
+
{ return this.zoomFollow < 1 && cameraScale > 0 ? (this.zoomScale / cameraScale) ** (1 - this.zoomFollow) : 1; }
|
|
84
|
+
|
|
85
|
+
/** World size one copy of the image is drawn at now, its size unless zoomFollow is under 1
|
|
86
|
+
* @return {Vector2} */
|
|
87
|
+
getDrawSize() { return this.size.scale(this.zoomFactor()); }
|
|
88
|
+
|
|
89
|
+
/** Where each copy of the image is drawn now, in the world: one place, or with wrapping every place that
|
|
90
|
+
* shows in the view
|
|
91
|
+
* @return {Array<Vector2>} */
|
|
92
|
+
getDrawPositions()
|
|
93
|
+
{
|
|
94
|
+
// its place seen from the camera is cut down by the parallax, and by the zoom it does not follow
|
|
95
|
+
const zoom = this.zoomFactor(), size = this.size.scale(zoom), p = this.parallax;
|
|
96
|
+
const offset = this.pos.subtract(cameraPos);
|
|
97
|
+
const center = cameraPos.add(vec2(offset.x * (1 - p.x) * zoom, offset.y * (1 - p.y) * zoom));
|
|
98
|
+
|
|
99
|
+
// the copies that reach into the view; a turned camera sees as far as its corner each way
|
|
100
|
+
const view = mainCanvasSize.scale(.5 / (cameraScale || 1));
|
|
101
|
+
const reach = cameraAngle ? vec2(view.length()) : view;
|
|
102
|
+
const range = (wrap, axis)=>
|
|
103
|
+
{
|
|
104
|
+
if (!wrap || !(size[axis] > 0)) return [0, 0];
|
|
105
|
+
const from = cameraPos[axis] - reach[axis] - size[axis] / 2 - center[axis];
|
|
106
|
+
const to = cameraPos[axis] + reach[axis] + size[axis] / 2 - center[axis];
|
|
107
|
+
const first = ceil(from / size[axis]);
|
|
108
|
+
return [first, min(floor(to / size[axis]), first + 99)]; // a tiny image does not draw without end
|
|
109
|
+
};
|
|
110
|
+
const [x0, x1] = range(this.wrapX, 'x'), [y0, y1] = range(this.wrapY, 'y'), positions = [];
|
|
111
|
+
for (let y = y0; y <= y1; ++y)
|
|
112
|
+
for (let x = x0; x <= x1; ++x)
|
|
113
|
+
positions.push(vec2(center.x + x * size.x, center.y + y * size.y));
|
|
114
|
+
return positions;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// Render the layer, called automatically by the engine
|
|
118
|
+
render()
|
|
119
|
+
{
|
|
120
|
+
const size = this.getDrawSize();
|
|
121
|
+
for (const pos of this.getDrawPositions())
|
|
122
|
+
this.draw(pos, size, this.color, 0, this.mirror, this.additiveColor, false);
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
///////////////////////////////////////////////////////////////////////////////
|
|
127
|
+
|
|
128
|
+
// the ridge of a range of mountains: a height for each column of an image and one more, the last the same as the
|
|
129
|
+
// first so the image repeats with no step; it wanders about the middle, turning now and then
|
|
130
|
+
function parallaxRidge(width, height, random)
|
|
131
|
+
{
|
|
132
|
+
const pointiness = .2, levelness = .005, slopeRange = height / 256; // how often it turns, how hard it is
|
|
133
|
+
const ridge = new Float32Array(width + 1), middle = height / 2; // pulled level, how steep it gets
|
|
134
|
+
let y = middle, slope = random.float(-slopeRange, slopeRange);
|
|
135
|
+
for (let x = 0; x <= width; ++x)
|
|
136
|
+
{
|
|
137
|
+
ridge[x] = y;
|
|
138
|
+
y += slope -= (y - middle) * levelness;
|
|
139
|
+
if (random.float() < pointiness)
|
|
140
|
+
slope = random.float(-slopeRange, slopeRange);
|
|
141
|
+
}
|
|
142
|
+
// take out the difference between its ends, a little from each column, and keep it inside the image
|
|
143
|
+
const drift = ridge[width] - ridge[0];
|
|
144
|
+
for (let x = 0; x <= width; ++x)
|
|
145
|
+
ridge[x] = clamp(ridge[x] - drift * x / width, 0, height);
|
|
146
|
+
return ridge;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/** A draw function for a ParallaxLayer: a range of mountains across the image, shaded from the color of their
|
|
150
|
+
* peaks down to the color of their feet, that repeats with no seam
|
|
151
|
+
* @param {Color} [topColor] - Color at the peaks
|
|
152
|
+
* @param {Color} [bottomColor] - Color at the bottom of the image
|
|
153
|
+
* @param {number} [seed] - The same seed gives the same mountains, random when not given
|
|
154
|
+
* @return {function(OffscreenCanvasRenderingContext2D, Vector2): void}
|
|
155
|
+
* @memberof Parallax */
|
|
156
|
+
function parallaxMountains(topColor=hsl(.6, .2, .6), bottomColor=hsl(.6, .3, .25), seed)
|
|
157
|
+
{
|
|
158
|
+
return (context, size)=>
|
|
159
|
+
{
|
|
160
|
+
const w = size.x, h = size.y;
|
|
161
|
+
const random = new RandomGenerator(seed === undefined ? randInt(1e9) + 1 : seed * 7919 + 1);
|
|
162
|
+
const ridge = parallaxRidge(w, h, random);
|
|
163
|
+
|
|
164
|
+
// a strip one pixel wide with the shading, stretched down each column from its peak
|
|
165
|
+
const strip = createCanvasContext(1, h);
|
|
166
|
+
for (let i = h; i--;)
|
|
167
|
+
{
|
|
168
|
+
strip.fillStyle = topColor.lerp(bottomColor, i / h).toString();
|
|
169
|
+
strip.fillRect(0, i, 1, 1);
|
|
170
|
+
}
|
|
171
|
+
for (let x = w; x--;)
|
|
172
|
+
ridge[x] < h && context.drawImage(strip.canvas, 0, 0, 1, h, x, ridge[x], 1, h - ridge[x]);
|
|
173
|
+
};
|
|
174
|
+
}
|
|
@@ -178,7 +178,9 @@ function particleEffectSanitize(raw)
|
|
|
178
178
|
settings[setting.name] = value === '' || particleEffectShapes.includes(value) ? value : fallback;
|
|
179
179
|
else if (isNumber(value))
|
|
180
180
|
{
|
|
181
|
-
|
|
181
|
+
// an angle past half a turn comes around, a clamp would point it the other way
|
|
182
|
+
const wraps = setting.name === 'angle' && isFinite(value) && abs(value) > PI;
|
|
183
|
+
const clamped = clamp(wraps ? mod(value + PI, 2*PI) - PI : value, setting.hardMin, setting.hardMax);
|
|
182
184
|
settings[setting.name] = setting.step >= 1 ? round(clamped) : clamped;
|
|
183
185
|
}
|
|
184
186
|
else
|
|
@@ -257,9 +259,11 @@ function particleEffectsGet(name) { return particleEffectsByName.get(String(name
|
|
|
257
259
|
* @param {string} text
|
|
258
260
|
* @return {Array<Object>}
|
|
259
261
|
* @memberof ParticleEffects */
|
|
260
|
-
function particleEffectsParse(text)
|
|
262
|
+
function particleEffectsParse(text) { return particleEffectsFromData(JSON.parse(text)); }
|
|
263
|
+
|
|
264
|
+
// a library file's data, or one effect's, into sanitized effects
|
|
265
|
+
function particleEffectsFromData(data)
|
|
261
266
|
{
|
|
262
|
-
const data = JSON.parse(text);
|
|
263
267
|
const list = isArray(data?.effects) ? data.effects : data?.settings ? [data] : undefined;
|
|
264
268
|
if (!list || !list.length)
|
|
265
269
|
throw new Error('no effects in this file');
|
|
@@ -278,7 +282,7 @@ function particleEffectsText(effects) { return JSON.stringify({version:1, effect
|
|
|
278
282
|
* @memberof ParticleEffects */
|
|
279
283
|
async function particleEffectsLoad(url)
|
|
280
284
|
{
|
|
281
|
-
const effects =
|
|
285
|
+
const effects = particleEffectsFromData(await fetchJSON(url));
|
|
282
286
|
particleEffectsAdd(effects);
|
|
283
287
|
return effects;
|
|
284
288
|
}
|
|
@@ -456,7 +460,13 @@ function particleEffectTileInfo(s)
|
|
|
456
460
|
* @memberof ParticleEffects */
|
|
457
461
|
function particleEffectApply(emitter, effect)
|
|
458
462
|
{
|
|
459
|
-
|
|
463
|
+
// an effect written by hand or changed since it was added
|
|
464
|
+
particleEffectSet(emitter, particleEffectSanitize(effect));
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
// set a 2D emitter to an effect that is sanitized already, as a play's is
|
|
468
|
+
function particleEffectSet(emitter, effect)
|
|
469
|
+
{
|
|
460
470
|
const s = effect.settings, ended = emitter.emitTime < 0; // destroyed, its last particles going
|
|
461
471
|
for (const setting of particleEffectSettings)
|
|
462
472
|
if (!particleEffectIndirect.includes(setting.name))
|
|
@@ -492,15 +502,14 @@ function particleEffectResolve(nameOrEffect, options)
|
|
|
492
502
|
* - Attach it to an object with addChild to follow it
|
|
493
503
|
* @param {string|Object} nameOrEffect - A built-in or added effect's name, or an effect
|
|
494
504
|
* @param {Vector2} [pos]
|
|
495
|
-
* @param {Object} [options]
|
|
496
|
-
*
|
|
497
|
-
*
|
|
498
|
-
*
|
|
499
|
-
*
|
|
500
|
-
*
|
|
501
|
-
* tinted by its colors
|
|
502
|
-
*
|
|
503
|
-
* the effect's own for this play
|
|
505
|
+
* @param {Object} [options] - What to change for this play, each left out when not wanted:
|
|
506
|
+
* scale grows the whole effect, the built-ins fit a one unit object at 1;
|
|
507
|
+
* hue turns its colors around the color wheel, 1 is all the way;
|
|
508
|
+
* saturation multiplies its saturation, 0 is grey;
|
|
509
|
+
* angle is its direction, 0 is up, the effect's own angle when not given;
|
|
510
|
+
* tileInfo, a TileInfo or a TextureInfo, is the game's own art to draw with in place of the effect's shape,
|
|
511
|
+
* tinted by its colors, a whole texture drawn as one tile;
|
|
512
|
+
* and any effect setting by its name, emitTime, emitRate, speed and the rest, replaces the effect's own
|
|
504
513
|
* @return {ParticleEmitter|undefined} - undefined when there is no such effect
|
|
505
514
|
* @memberof ParticleEffects */
|
|
506
515
|
function particleEffect(nameOrEffect, pos=vec2(), options={})
|
|
@@ -508,7 +517,7 @@ function particleEffect(nameOrEffect, pos=vec2(), options={})
|
|
|
508
517
|
const effect = particleEffectResolve(nameOrEffect, options);
|
|
509
518
|
if (!effect) return;
|
|
510
519
|
const emitter = new ParticleEmitter(pos.copy());
|
|
511
|
-
|
|
520
|
+
particleEffectSet(emitter, effect);
|
|
512
521
|
emitter.scale = options.scale ?? 1;
|
|
513
522
|
if (options.angle !== undefined)
|
|
514
523
|
emitter.angle = options.angle;
|
|
@@ -572,7 +581,7 @@ function particleEffectFromEmitter(emitter, name='Effect')
|
|
|
572
581
|
p.velocity.y += (p.pos.x - c.x) * s * .002;
|
|
573
582
|
};
|
|
574
583
|
b('wind').update3D = (p, s)=> p.velocity.x += s * .004 * p.scale * min(p.age / p.lifeTime, 1);
|
|
575
|
-
// stick has no 3D push:
|
|
584
|
+
// stick has no 3D push: particleEffectApply3D makes it the emitter's stick, its grip on landing
|
|
576
585
|
}
|
|
577
586
|
|
|
578
587
|
/** Set a 3D emitter to an effect, live, so a running one keeps its particles; its place, scale and flatten stay
|
|
@@ -581,7 +590,13 @@ function particleEffectFromEmitter(emitter, name='Effect')
|
|
|
581
590
|
* @memberof ParticleEffects */
|
|
582
591
|
function particleEffectApply3D(emitter, effect)
|
|
583
592
|
{
|
|
584
|
-
|
|
593
|
+
// an effect written by hand or changed since it was added
|
|
594
|
+
particleEffectSet3D(emitter, particleEffectSanitize(effect));
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
// set a 3D emitter to an effect that is sanitized already, as a play's is
|
|
598
|
+
function particleEffectSet3D(emitter, effect)
|
|
599
|
+
{
|
|
585
600
|
const s = effect.settings, e = emitter;
|
|
586
601
|
// a circle is a sphere and a rectangle a box as deep as it is wide
|
|
587
602
|
e.emitSize = s.emitRect ? vec3(s.emitSize, s.emitHeight, s.emitSize) : s.emitSize;
|
|
@@ -594,8 +609,8 @@ function particleEffectApply3D(emitter, effect)
|
|
|
594
609
|
'randomness', 'additive', 'gravityScale', 'angleSpeed', 'angleDamping', 'collideLevel', 'restitution'])
|
|
595
610
|
e[name] = s[name];
|
|
596
611
|
e.trailTime = s.trailScale / 60; // a stretch of speed times trailScale is a streak of that many frames
|
|
597
|
-
|
|
598
|
-
e.
|
|
612
|
+
e.friction = s.friction;
|
|
613
|
+
e.stick = effect.behaviors.find(b=> b.name == 'stick')?.strength ?? 0; // in 3D the emitter grips on landing
|
|
599
614
|
// a 2D emitter at angle a shoots along (sin a, cos a), a z turn r takes up to (-sin r, cos r), so r is -a
|
|
600
615
|
e.rotation3D = vec3(0, 0, -s.angle);
|
|
601
616
|
e.particleUpdateCallback = particleEffectUpdateCallback(effect.behaviors, true);
|
|
@@ -605,8 +620,8 @@ function particleEffectApply3D(emitter, effect)
|
|
|
605
620
|
* - The same effect data as particleEffect, so the look carries across: a circle spawn area becomes a sphere and a
|
|
606
621
|
* rectangle a box as deep as it is wide, both flat across the way it emits with options.flatten, a disc or a
|
|
607
622
|
* sheet; a trail becomes a streak of the same length, and the settings the 3D emitter lacks (particleConeAngle,
|
|
608
|
-
* randomColorLinear, velocityInheritance, localSpace) are left out; the stick behavior becomes the
|
|
609
|
-
* particle lands
|
|
623
|
+
* randomColorLinear, velocityInheritance, localSpace) are left out; the stick behavior becomes the emitter's
|
|
624
|
+
* stick, its grip where a particle lands
|
|
610
625
|
* @param {string|Object} nameOrEffect - A built-in or added effect's name, or an effect
|
|
611
626
|
* @param {Vector3} [pos3D]
|
|
612
627
|
* @param {Object} [options] - scale, hue, saturation, angle, tileInfo and any setting as particleEffect; angle turns it
|
|
@@ -618,7 +633,7 @@ function particleEffect3D(nameOrEffect, pos3D=vec3(), options={})
|
|
|
618
633
|
const effect = particleEffectResolve(nameOrEffect, options);
|
|
619
634
|
if (!effect) return;
|
|
620
635
|
const e = new ParticleEmitter3D(pos3D.copy());
|
|
621
|
-
|
|
636
|
+
particleEffectSet3D(e, effect);
|
|
622
637
|
if (options.angle !== undefined)
|
|
623
638
|
e.rotation3D = vec3(0, 0, -options.angle);
|
|
624
639
|
e.scale3D = vec3(options.scale ?? 1);
|
package/plugins/pluginExport.js
CHANGED
|
@@ -116,6 +116,10 @@ export
|
|
|
116
116
|
setScene,
|
|
117
117
|
getScene,
|
|
118
118
|
|
|
119
|
+
// Parallax
|
|
120
|
+
ParallaxLayer,
|
|
121
|
+
parallaxMountains,
|
|
122
|
+
|
|
119
123
|
// Path Finding
|
|
120
124
|
PathFinder,
|
|
121
125
|
PathFinderNode,
|
|
@@ -196,6 +200,10 @@ export
|
|
|
196
200
|
level3DAddType,
|
|
197
201
|
level3DAddMesh,
|
|
198
202
|
level3DLoad,
|
|
203
|
+
level3DAddPrefab,
|
|
204
|
+
level3DLoadPrefab,
|
|
205
|
+
level3DSpawn,
|
|
206
|
+
Prefab3D,
|
|
199
207
|
level3DVoxelSetup,
|
|
200
208
|
|
|
201
209
|
// Three.js
|
package/plugins/postProcess.js
CHANGED
|
@@ -158,8 +158,11 @@ class PostProcessPlugin
|
|
|
158
158
|
{
|
|
159
159
|
// copy main canvas to work canvas at the backing store size,
|
|
160
160
|
// mainCanvasSize is css pixels so it would lose resolution
|
|
161
|
-
|
|
162
|
-
workCanvas.height
|
|
161
|
+
// sized again only when the canvas changed, setting a size remakes the canvas even to the same one
|
|
162
|
+
if (workCanvas.width !== mainCanvas.width || workCanvas.height !== mainCanvas.height)
|
|
163
|
+
workCanvas.width = mainCanvas.width, workCanvas.height = mainCanvas.height;
|
|
164
|
+
else
|
|
165
|
+
workContext.clearRect(0, 0, workCanvas.width, workCanvas.height);
|
|
163
166
|
glCopyToContext(workContext);
|
|
164
167
|
workContext.drawImage(mainCanvas, 0, 0);
|
|
165
168
|
// clear the main canvas with clearRect, resizing it would also
|