littlejsengine 1.23.1 → 1.24.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/CHANGELOG.md +261 -0
- package/COPYRIGHT.txt +0 -27
- package/FAQ.md +4 -9
- package/README.md +4 -3
- package/REFERENCE.md +34 -18
- package/dist/littlejs.d.ts +160 -105
- package/dist/littlejs.esm.js +490 -277
- package/dist/littlejs.esm.min.js +3 -3
- package/dist/littlejs.js +489 -273
- package/dist/littlejs.min.js +2 -2
- package/dist/littlejs.release.js +463 -264
- package/package.json +2 -1
- package/plugins/box2d.js +1 -1
- package/plugins/pluginExport.js +0 -4
- package/plugins/render3d.js +93 -17
- package/plugins/render3dEditor.js +9 -3
- package/plugins/render3dExtras.js +169 -70
- package/plugins/render3dLevel.js +24 -4
- package/plugins/render3dVoxels.js +15 -8
- package/src/engine.js +1 -1
- package/src/engineAudio.js +2 -2
- package/src/engineBuild.mjs +0 -1
- package/src/engineEditor.js +17 -6
- package/src/engineExport.js +1 -0
- package/src/engineSettings.js +5 -1
- package/src/engineTileLayer.js +154 -1
- package/plugins/zzfxm.js +0 -162
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "littlejsengine",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.24.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",
|
|
@@ -40,6 +40,7 @@
|
|
|
40
40
|
"!plugins/box2d.wasm.*",
|
|
41
41
|
"COPYRIGHT.txt",
|
|
42
42
|
"FAQ.md",
|
|
43
|
+
"CHANGELOG.md",
|
|
43
44
|
"REFERENCE.md",
|
|
44
45
|
"EDITOR.md"
|
|
45
46
|
],
|
package/plugins/box2d.js
CHANGED
|
@@ -414,7 +414,7 @@ class Box2dObject extends EngineObject
|
|
|
414
414
|
|
|
415
415
|
const points = [];
|
|
416
416
|
const radius = diameter/2;
|
|
417
|
-
for (let i=sides; i
|
|
417
|
+
for (let i=sides; i-- > 0;) // a count that is not a whole number still ends
|
|
418
418
|
points.push(vec2(radius,0).rotate((i+.5)/sides*PI*2));
|
|
419
419
|
return this.addPoly(points, density, friction, restitution, isSensor);
|
|
420
420
|
}
|
package/plugins/pluginExport.js
CHANGED
package/plugins/render3d.js
CHANGED
|
@@ -915,8 +915,8 @@ class Render3DPlugin
|
|
|
915
915
|
}
|
|
916
916
|
|
|
917
917
|
/** Find the nearest object under a screen position or along a ray, for clicking on things
|
|
918
|
-
* -
|
|
919
|
-
*
|
|
918
|
+
* - A mesh is hit on its triangles, the ones that face the ray as they are drawn, both sides of a doubleSided
|
|
919
|
+
* mesh; a sprite is hit as the quad it draws, and a height map or a voxel map on its surface
|
|
920
920
|
* - engineObjectsRaycast3D is the other half of this, every object along a ray instead of the nearest
|
|
921
921
|
* @param {Vector2|Ray3D} from - A screen position like mousePosScreen, or a ray to look along
|
|
922
922
|
* @param {Array<EngineObject>} [objects] - Defaults to every object; only those with a mesh or a sprite count
|
|
@@ -4283,8 +4283,8 @@ function engineObjectsCollect3D(pos, size, objects=engineObjects, testCenters=fa
|
|
|
4283
4283
|
return collected;
|
|
4284
4284
|
}
|
|
4285
4285
|
|
|
4286
|
-
// how far along a ray an object is hit, or undefined for a miss;
|
|
4287
|
-
//
|
|
4286
|
+
// 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,
|
|
4287
|
+
// and a sprite as the quad it draws
|
|
4288
4288
|
function render3DRaycastObject(ray, o)
|
|
4289
4289
|
{
|
|
4290
4290
|
if (o.destroyed || !(o instanceof EngineObject3D)) return;
|
|
@@ -4301,25 +4301,55 @@ function render3DRaycastObject(ray, o)
|
|
|
4301
4301
|
const distance = raycastSphere(ray, center, radius);
|
|
4302
4302
|
if (distance === undefined) return;
|
|
4303
4303
|
|
|
4304
|
-
// the sphere
|
|
4305
|
-
//
|
|
4304
|
+
// the sphere and then the box in the mesh's own space are quick rejects, and a ray that meets the box is tested
|
|
4305
|
+
// against the triangles; the direction is not made unit length, so the distance holds in the world;
|
|
4306
4306
|
// a mesh flattened to nothing on an axis has no inverse, it is hit as a disc like a sprite
|
|
4307
4307
|
if (!matrix.determinant()) return render3DRaycastDisc(ray, center, radius);
|
|
4308
4308
|
const inverse = matrix.copy().invert(), bounds = mesh.bounds || mesh.getBounds();
|
|
4309
4309
|
const local = new Ray3D(inverse.transformPoint(ray.origin), inverse.transformDirection(ray.direction));
|
|
4310
4310
|
const hit = raycastBox(local, bounds.min.add(bounds.max).scale(.5), bounds.max.subtract(bounds.min));
|
|
4311
|
-
if (hit
|
|
4311
|
+
if (hit === undefined || !ray.direction.lengthSquared())
|
|
4312
|
+
return hit; // a ray of no length is where it starts, inside the box
|
|
4313
|
+
return render3DRaycastMesh(local, mesh);
|
|
4314
|
+
}
|
|
4312
4315
|
|
|
4313
|
-
|
|
4314
|
-
|
|
4315
|
-
|
|
4316
|
-
|
|
4317
|
-
|
|
4318
|
-
|
|
4319
|
-
|
|
4320
|
-
|
|
4321
|
-
|
|
4322
|
-
|
|
4316
|
+
// how far along a ray, in the mesh's own space, its nearest triangle is hit, or undefined: the triangles that face
|
|
4317
|
+
// the ray as the pass draws them, clockwise from the front, and the back faces too of a doubleSided mesh; a strip's
|
|
4318
|
+
// 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
|
|
4319
|
+
// never hit; nothing is kept between calls, so a mesh that changed is hit as it is now
|
|
4320
|
+
function render3DRaycastMesh(ray, mesh)
|
|
4321
|
+
{
|
|
4322
|
+
const points = mesh.points, indices = mesh.indices, both = mesh.doubleSided;
|
|
4323
|
+
const ox = ray.origin.x, oy = ray.origin.y, oz = ray.origin.z;
|
|
4324
|
+
const dx = ray.direction.x, dy = ray.direction.y, dz = ray.direction.z;
|
|
4325
|
+
const count = indices ? indices.length / 3 | 0 : points.length - 2;
|
|
4326
|
+
let nearest;
|
|
4327
|
+
for (let i = 0; i < count; ++i)
|
|
4328
|
+
{
|
|
4329
|
+
// the corners clockwise from the front: an indexed list is written the other way round
|
|
4330
|
+
let a, b, c;
|
|
4331
|
+
if (indices)
|
|
4332
|
+
a = points[indices[i*3]], b = points[indices[i*3+2]], c = points[indices[i*3+1]];
|
|
4333
|
+
else
|
|
4334
|
+
a = points[i & 1 ? i + 1 : i], b = points[i & 1 ? i : i + 1], c = points[i + 2];
|
|
4335
|
+
|
|
4336
|
+
// Moller and Trumbore: the determinant is the ray along the face's normal, below zero from the front
|
|
4337
|
+
const e1x = b.x - a.x, e1y = b.y - a.y, e1z = b.z - a.z;
|
|
4338
|
+
const e2x = c.x - a.x, e2y = c.y - a.y, e2z = c.z - a.z;
|
|
4339
|
+
const hx = dy * e2z - dz * e2y, hy = dz * e2x - dx * e2z, hz = dx * e2y - dy * e2x;
|
|
4340
|
+
const det = e1x * hx + e1y * hy + e1z * hz;
|
|
4341
|
+
if (both ? !det : !(det < 0)) continue;
|
|
4342
|
+
const sx = ox - a.x, sy = oy - a.y, sz = oz - a.z;
|
|
4343
|
+
const u = (sx * hx + sy * hy + sz * hz) / det;
|
|
4344
|
+
if (!(u >= 0 && u <= 1)) continue;
|
|
4345
|
+
const qx = sy * e1z - sz * e1y, qy = sz * e1x - sx * e1z, qz = sx * e1y - sy * e1x;
|
|
4346
|
+
const v = (dx * qx + dy * qy + dz * qz) / det;
|
|
4347
|
+
if (!(v >= 0 && u + v <= 1)) continue;
|
|
4348
|
+
const t = (e2x * qx + e2y * qy + e2z * qz) / det;
|
|
4349
|
+
if (t >= 0 && !(t >= nearest))
|
|
4350
|
+
nearest = t;
|
|
4351
|
+
}
|
|
4352
|
+
return nearest;
|
|
4323
4353
|
}
|
|
4324
4354
|
|
|
4325
4355
|
// a sprite is the quad drawBillboard draws, its size3D grown by the x and y scale as the draw does, facing the camera
|
|
@@ -4697,10 +4727,12 @@ class InstancedMesh3D extends EngineObject3D
|
|
|
4697
4727
|
* - radius is a world distance, so scale3D does not change it
|
|
4698
4728
|
* - An alpha, an intensity or a radius of 0 switches it off, and a light that is off takes none of those slots
|
|
4699
4729
|
* - Draws nothing but its glow, when it has one; add a small emissive mesh if the lamp itself should be seen
|
|
4730
|
+
* - addFlare gives it a lens flare, see LensFlare3D
|
|
4700
4731
|
* @extends EngineObject3D
|
|
4701
4732
|
* @memberof Render3D
|
|
4702
4733
|
* @example
|
|
4703
4734
|
* const torch = new Light3D(vec3(0, 3, 0), 10, hsl(.1, 1, .65));
|
|
4735
|
+
* torch.addFlare(); // light in the lens when the torch is in view
|
|
4704
4736
|
*/
|
|
4705
4737
|
class Light3D extends EngineObject3D
|
|
4706
4738
|
{
|
|
@@ -4735,6 +4767,50 @@ class Light3D extends EngineObject3D
|
|
|
4735
4767
|
* bright core */
|
|
4736
4768
|
this.glowFalloff = 1;
|
|
4737
4769
|
this.additive = true; // the glow is added on, in the transparent stage; a light with none draws nothing
|
|
4770
|
+
/** @type {LensFlare3D|undefined} */
|
|
4771
|
+
this.flareObject = undefined;
|
|
4772
|
+
}
|
|
4773
|
+
|
|
4774
|
+
/** Give the light a lens flare, made with the arguments of LensFlare3D, in place of the one it had
|
|
4775
|
+
* @param {number} [size] - Scales every part of it
|
|
4776
|
+
* @param {number} [count] - How many ghosts there are, besides the glow at the light
|
|
4777
|
+
* @param {number} [intensity] - How bright it is
|
|
4778
|
+
* @param {number} [saturation] - How colorful the ghosts are
|
|
4779
|
+
* @param {Color} [color] - Tints the flare, with the light's own color
|
|
4780
|
+
* @return {LensFlare3D} - The flare, to change: light.addFlare().shapes = ['hex'] */
|
|
4781
|
+
addFlare(size, count, intensity, saturation, color)
|
|
4782
|
+
{
|
|
4783
|
+
const flare = new LensFlare3D(size, count, intensity, saturation, color);
|
|
4784
|
+
this.flare = flare;
|
|
4785
|
+
return flare;
|
|
4786
|
+
}
|
|
4787
|
+
|
|
4788
|
+
/** The light's lens flare, undefined for none: addFlare makes it, or set a LensFlare3D of your own, and
|
|
4789
|
+
* destroying the flare takes it away
|
|
4790
|
+
* - The flare is the light's: it is attached to the light as its child, so it stays through a scene change
|
|
4791
|
+
* when the light does, and is destroyed with the light or when another takes its place
|
|
4792
|
+
* @type {LensFlare3D|undefined} */
|
|
4793
|
+
get flare() { return this.flareObject && !this.flareObject.destroyed ? this.flareObject : undefined; }
|
|
4794
|
+
|
|
4795
|
+
set flare(flare)
|
|
4796
|
+
{
|
|
4797
|
+
const old = this.flare; // a flare that was destroyed on its own is no flare
|
|
4798
|
+
if (flare === old) return;
|
|
4799
|
+
old && old.destroy();
|
|
4800
|
+
if (flare)
|
|
4801
|
+
{
|
|
4802
|
+
flare.light = this;
|
|
4803
|
+
flare.parent || this.addChild(flare);
|
|
4804
|
+
}
|
|
4805
|
+
this.flareObject = flare || undefined;
|
|
4806
|
+
}
|
|
4807
|
+
|
|
4808
|
+
/** Destroy the light, and its flare with it
|
|
4809
|
+
* @param {boolean} [immediate] */
|
|
4810
|
+
destroy(immediate)
|
|
4811
|
+
{
|
|
4812
|
+
this.flare = undefined;
|
|
4813
|
+
super.destroy(immediate);
|
|
4738
4814
|
}
|
|
4739
4815
|
|
|
4740
4816
|
/** Draw the glow, a quad facing the camera with a soft round glow on it, pulled toward the camera by half its
|
|
@@ -354,7 +354,7 @@ function editor3DSceneState()
|
|
|
354
354
|
const r = render3D;
|
|
355
355
|
return {sky: r.sky, ambientColor: r.ambientColor.copy(), ambientGroundColor: r.ambientGroundColor?.copy(),
|
|
356
356
|
fogStart: r.fogStart, fogEnd: r.fogEnd, fogColor: r.fogColor?.copy(), sunDirection: r.sunDirection.copy(),
|
|
357
|
-
sunColor: r.sunColor.copy(), shadows: r.shadows};
|
|
357
|
+
sunColor: r.sunColor.copy(), shadows: r.shadows, sunFlare: !!level3DSunFlare && !level3DSunFlare.destroyed};
|
|
358
358
|
}
|
|
359
359
|
function editor3DSceneRestore(state)
|
|
360
360
|
{
|
|
@@ -369,6 +369,7 @@ function editor3DSceneRestore(state)
|
|
|
369
369
|
r.sunDirection = state.sunDirection.copy();
|
|
370
370
|
r.sunColor = state.sunColor.copy();
|
|
371
371
|
r.shadows = state.shadows;
|
|
372
|
+
level3DSceneFlare(state.sunFlare); // a flare an earlier level's scene gave the sun, never the game's own
|
|
372
373
|
}
|
|
373
374
|
|
|
374
375
|
// the scene on screen as a block, to start a level's scene from: a sky only when the dome's colors are known
|
|
@@ -390,6 +391,7 @@ function editor3DSceneFromView()
|
|
|
390
391
|
scene.fog = [editor3DRound(r.fogStart || 0), editor3DRound(r.fogEnd || 0)];
|
|
391
392
|
scene.fogColor = editor3DHex(r.fogColor || canvasClearColor);
|
|
392
393
|
scene.shadows = !!r.shadows;
|
|
394
|
+
scene.lensFlare = level3DSunHasFlare();
|
|
393
395
|
return scene;
|
|
394
396
|
}
|
|
395
397
|
|
|
@@ -547,9 +549,11 @@ function editor3DLevelLoaded(level)
|
|
|
547
549
|
level.objects = editor3DCopy(saved.objects);
|
|
548
550
|
for (const name of editor3DLevelPartNames)
|
|
549
551
|
parts[name] ? level[name] = editor3DCopy(parts[name]) : delete level[name];
|
|
552
|
+
saved.stale && console.warn(`LittleJS editor: the edits brought back to ${record.fileName} are older than ` +
|
|
553
|
+
'the last ones, which storage had no room for');
|
|
550
554
|
}
|
|
551
|
-
else if (saved)
|
|
552
|
-
record.pending = saved;
|
|
555
|
+
else if (saved && isArray(saved.objects))
|
|
556
|
+
record.pending = saved; // one that is broken is passed by, and the next autosave writes over it
|
|
553
557
|
}
|
|
554
558
|
|
|
555
559
|
// called by level3DLoad for each object, with what its type made
|
|
@@ -3378,6 +3382,8 @@ function editor3DSceneUpdate(box)
|
|
|
3378
3382
|
color('Fog color', 'The sky\'s horizon color when the level has a sky and no fog color',
|
|
3379
3383
|
scene.fogColor ?? (sky ? sky[1] : view.fogColor), (hex)=> change('fogColor', hex));
|
|
3380
3384
|
toggle('Shadows', 'The sun casts shadows', !!value('shadows'), (on)=> change('shadows', on));
|
|
3385
|
+
toggle('Lens flare', 'The sun flares in the lens when it is in view', !!value('lensFlare'), (on)=>
|
|
3386
|
+
change('lensFlare', on));
|
|
3381
3387
|
}
|
|
3382
3388
|
|
|
3383
3389
|
// the properties box: the position, rotation and scale of the one selected object and an input for each default
|
|
@@ -1476,31 +1476,74 @@ class Trail3D extends EngineObject3D
|
|
|
1476
1476
|
// lens flare
|
|
1477
1477
|
|
|
1478
1478
|
// the shapes a flare is made of, white on clear on one smooth texture made the first time it is asked for: a soft
|
|
1479
|
-
// glow, a flat disc with a brighter rim,
|
|
1479
|
+
// glow, a flat disc with a brighter rim, a ring, a hexagon like the blades of a lens, a streak across, and a star
|
|
1480
|
+
// of six rays; undefined headless or without a canvas
|
|
1481
|
+
const render3DFlareShapes = ['glow', 'disc', 'ring', 'hex', 'streak', 'star'];
|
|
1480
1482
|
let render3DFlareTiles;
|
|
1481
1483
|
function render3DFlareTile(shape)
|
|
1482
1484
|
{
|
|
1483
1485
|
if (!render3DFlareTiles)
|
|
1484
1486
|
{
|
|
1485
1487
|
if (headlessMode || !glContext || typeof OffscreenCanvas == 'undefined') return;
|
|
1486
|
-
const cell = 128, shapes =
|
|
1488
|
+
const cell = 128, shapes = render3DFlareShapes, context = createCanvasContext(cell * shapes.length, cell);
|
|
1487
1489
|
// each shape is how see-through it is from its middle out
|
|
1488
1490
|
const alpha =
|
|
1489
1491
|
{
|
|
1490
1492
|
glow: (t)=> engineGlowAlpha(t, 1),
|
|
1491
1493
|
disc: (t)=> t < .8 ? .4 + .1 * t : t < .92 ? .48 + (t - .8) * 3 : (1 - t) / .08 * .84,
|
|
1492
1494
|
ring: (t)=> max(0, 1 - abs(t - .86) / .12),
|
|
1495
|
+
hex: (t)=> t < .8 ? .4 + .1 * t : t < .92 ? .48 + (t - .8) * 3 : (1 - t) / .08 * .84,
|
|
1496
|
+
streak: (t)=> (1 - t) ** 1.5,
|
|
1493
1497
|
};
|
|
1494
|
-
|
|
1498
|
+
// a round gradient from the middle of the cell out, squashed for a ray and cut to a path for the hexagon
|
|
1499
|
+
const fill = (shape, r, path)=>
|
|
1495
1500
|
{
|
|
1496
|
-
const
|
|
1497
|
-
const gradient = context.createRadialGradient(x, y, 0, x, y, r);
|
|
1501
|
+
const steps = 32, gradient = context.createRadialGradient(0, 0, 0, 0, 0, r);
|
|
1498
1502
|
for (let k = 0; k <= steps; ++k)
|
|
1499
1503
|
gradient.addColorStop(k / steps, 'rgba(255,255,255,' + clamp(alpha[shape](k / steps)).toFixed(4) + ')');
|
|
1500
1504
|
context.fillStyle = gradient;
|
|
1501
1505
|
context.beginPath();
|
|
1502
|
-
context.arc(
|
|
1506
|
+
path ? path() : context.arc(0, 0, r, 0, 2 * PI);
|
|
1503
1507
|
context.fill();
|
|
1508
|
+
};
|
|
1509
|
+
const ray = (r, angle)=>
|
|
1510
|
+
{
|
|
1511
|
+
context.save();
|
|
1512
|
+
context.rotate(angle);
|
|
1513
|
+
context.scale(1, .07);
|
|
1514
|
+
fill('streak', r);
|
|
1515
|
+
context.restore();
|
|
1516
|
+
};
|
|
1517
|
+
shapes.forEach((shape, i)=>
|
|
1518
|
+
{
|
|
1519
|
+
const r = cell / 2 - 2;
|
|
1520
|
+
context.save();
|
|
1521
|
+
context.translate(i * cell + cell / 2, cell / 2);
|
|
1522
|
+
if (shape == 'hex')
|
|
1523
|
+
{
|
|
1524
|
+
// a gradient is round, so the hexagon is set pixel by pixel, by how far each is toward a side
|
|
1525
|
+
const image = context.createImageData(cell, cell), data = image.data, apothem = r * cos(PI / 6);
|
|
1526
|
+
for (let k = 0; k < cell * cell; ++k)
|
|
1527
|
+
{
|
|
1528
|
+
const x = abs(k % cell + .5 - cell / 2), y = abs((k / cell | 0) + .5 - cell / 2);
|
|
1529
|
+
const t = max(y, x * cos(PI / 6) + y / 2) / apothem;
|
|
1530
|
+
data[k * 4] = data[k * 4 + 1] = data[k * 4 + 2] = 255;
|
|
1531
|
+
data[k * 4 + 3] = t < 1 ? clamp(alpha.hex(t)) * 255 : 0;
|
|
1532
|
+
}
|
|
1533
|
+
context.putImageData(image, i * cell, 0);
|
|
1534
|
+
}
|
|
1535
|
+
else if (shape == 'streak')
|
|
1536
|
+
ray(r, 0);
|
|
1537
|
+
else if (shape == 'star')
|
|
1538
|
+
{
|
|
1539
|
+
for (let k = 3; k--;)
|
|
1540
|
+
ray(r, k * PI / 3 + PI / 2);
|
|
1541
|
+
context.scale(.3, .3);
|
|
1542
|
+
fill('glow', r);
|
|
1543
|
+
}
|
|
1544
|
+
else
|
|
1545
|
+
fill(shape, r);
|
|
1546
|
+
context.restore();
|
|
1504
1547
|
});
|
|
1505
1548
|
const texture = new TextureInfo(context.canvas, true, false, false); // smooth even in a pixel art game
|
|
1506
1549
|
render3DFlareTiles = new Map(shapes.map((shape, i)=>
|
|
@@ -1509,16 +1552,32 @@ function render3DFlareTile(shape)
|
|
|
1509
1552
|
return render3DFlareTiles.get(shape) || render3DFlareTiles.get('glow');
|
|
1510
1553
|
}
|
|
1511
1554
|
|
|
1555
|
+
/**
|
|
1556
|
+
* A part of a lens flare
|
|
1557
|
+
* @typedef {Object} LensFlareElement
|
|
1558
|
+
* @property {number} at - Where along the line: 0 the sun, 1 the middle of the screen, 2 as far past it
|
|
1559
|
+
* @property {number|Vector2} size - How big across, as a part of the screen's height, a vector for a part wider
|
|
1560
|
+
* than it is tall
|
|
1561
|
+
* @property {Color} color - Its color, the alpha how bright
|
|
1562
|
+
* @property {string} [shape] - glow, disc, ring, hex, streak or star, a glow when left out
|
|
1563
|
+
* @property {TileInfo} [tileInfo] - A tile of the game's own to draw in place of a shape, best white on clear
|
|
1564
|
+
* @property {number} [angle] - How far it is turned, in radians
|
|
1565
|
+
* @memberof Render3D
|
|
1566
|
+
*/
|
|
1567
|
+
|
|
1512
1568
|
/**
|
|
1513
1569
|
* LensFlare3D - The sun's lens flare, the old kind: a glow at the sun and a row of discs and rings of different
|
|
1514
1570
|
* sizes along the line from the sun through the middle of the screen
|
|
1515
1571
|
* - Make one and it shows, over the 3D scene and under what the game draws after, a HUD; destroy it to take it away
|
|
1516
1572
|
* - It follows render3D.sunDirection, and fades out as the sun leaves the screen or goes behind something
|
|
1573
|
+
* - A Light3D gets one of its own with light.addFlare()
|
|
1517
1574
|
* - flareSize, count, intensity and saturation set its look, seed picks another arrangement, and its color tints it,
|
|
1518
|
-
* with the sun's own color;
|
|
1519
|
-
*
|
|
1575
|
+
* with the sun's own color; shapes says what its ghosts are, glowSize and ghostSize how big its parts are; or
|
|
1576
|
+
* give it elements of your own, which may be tiles of the game's
|
|
1577
|
+
* - 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
|
|
1578
|
+
* sun is too far off the screen for the flare to show, a third of the screen past its edge, where nothing is tested
|
|
1520
1579
|
* - What hides the sun is found with a ray from the camera, against the level and every object that is not see
|
|
1521
|
-
* through, each
|
|
1580
|
+
* through, each on the triangles of its mesh, see render3D.pick; turn it off with occlusion
|
|
1522
1581
|
* - It needs WebGL, and it draws nothing in the shadow of renderAfter2D
|
|
1523
1582
|
* @extends EngineObject3D
|
|
1524
1583
|
* @memberof Render3D
|
|
@@ -1548,48 +1607,62 @@ class LensFlare3D extends EngineObject3D
|
|
|
1548
1607
|
this.saturation = saturation;
|
|
1549
1608
|
/** @property {number} - Picks the arrangement of the ghosts, another seed is another flare */
|
|
1550
1609
|
this.seed = 1;
|
|
1551
|
-
/** @property {Array<
|
|
1552
|
-
*
|
|
1553
|
-
*
|
|
1554
|
-
*
|
|
1555
|
-
|
|
1610
|
+
/** @property {Array<string>|undefined} - The shapes the ghosts are picked from, glow, disc, ring, hex,
|
|
1611
|
+
* streak or star: ['hex'] makes every ghost a hexagon, and a shape listed twice is picked twice as often;
|
|
1612
|
+
* discs, rings and glows when not set
|
|
1613
|
+
* @type {Array<string>|undefined} */
|
|
1614
|
+
this.shapes = undefined;
|
|
1615
|
+
/** @property {number} - Scales the glow at the sun, 0 for none */
|
|
1616
|
+
this.glowSize = 1;
|
|
1617
|
+
/** @property {number} - Scales the ghosts */
|
|
1618
|
+
this.ghostSize = 1;
|
|
1619
|
+
/** @property {Array<LensFlareElement>|undefined} - The parts of the flare, to set your own in place of the
|
|
1620
|
+
* ones made from count, seed, saturation, shapes, glowSize and ghostSize
|
|
1621
|
+
* @type {Array<LensFlareElement>|undefined} */
|
|
1556
1622
|
this.elements = undefined;
|
|
1557
1623
|
/** @property {Light3D|undefined} - A light the flare is of in place of the sun, a lamp or a spotlight: the
|
|
1558
1624
|
* flare is at the light and in its color, smaller from farther than the light reaches, hidden by what is
|
|
1559
|
-
* in front of the light,
|
|
1625
|
+
* in front of the light but its lamp, a mesh around it no wider than half its radius, and a spotlight's
|
|
1626
|
+
* shows from inside its beam only; a DirectionalLight3D's is far
|
|
1627
|
+
* away where it shines from, like the sun's; the flare is destroyed when its light is; light.addFlare
|
|
1628
|
+
* sets this
|
|
1560
1629
|
* @type {Light3D|undefined} */
|
|
1561
1630
|
this.light = undefined;
|
|
1562
1631
|
/** @property {boolean} - Fade out when something is between the camera and the sun */
|
|
1563
1632
|
this.occlusion = true;
|
|
1564
1633
|
/** @property {number} - Seconds the flare takes to fade out or in when the sun is hidden or shows again */
|
|
1565
1634
|
this.fadeTime = .15;
|
|
1566
|
-
/** @property {number} - How much of the sun shows, 0 hidden or behind the camera to 1 in
|
|
1635
|
+
/** @property {number} - How much of the sun shows, 0 hidden, off the screen or behind the camera to 1 in
|
|
1636
|
+
* plain view, eased */
|
|
1567
1637
|
this.visible = 1;
|
|
1568
1638
|
this.renderOrder = 1e9; // over the game's sprites, it is light in the lens
|
|
1569
1639
|
this.madeKey = '';
|
|
1570
|
-
/** @type {Array<
|
|
1640
|
+
/** @type {Array<LensFlareElement>} */
|
|
1571
1641
|
this.made = [];
|
|
1572
1642
|
}
|
|
1573
1643
|
|
|
1574
|
-
/** The parts of the flare: the elements set by hand, or the ones made from count, seed
|
|
1575
|
-
* and a core at the sun and the ghosts, made again when one of those changes
|
|
1576
|
-
* @return {Array<
|
|
1644
|
+
/** The parts of the flare: the elements set by hand, or the ones made from count, seed, saturation, shapes,
|
|
1645
|
+
* glowSize and ghostSize, a glow and a core at the sun and the ghosts, made again when one of those changes
|
|
1646
|
+
* @return {Array<LensFlareElement>} */
|
|
1577
1647
|
getElements()
|
|
1578
1648
|
{
|
|
1579
1649
|
if (this.elements) return this.elements;
|
|
1580
|
-
const
|
|
1650
|
+
const shapes = this.shapes?.length ? this.shapes : undefined, glow = this.glowSize, ghost = this.ghostSize;
|
|
1651
|
+
const key = [this.count, this.seed, this.saturation, glow, ghost, shapes?.length, shapes].join();
|
|
1581
1652
|
if (key !== this.madeKey)
|
|
1582
1653
|
{
|
|
1583
1654
|
const random = new RandomGenerator(this.seed * 7919 + 1), s = clamp(this.saturation);
|
|
1584
|
-
|
|
1585
|
-
|
|
1586
|
-
{at: 0, size: .
|
|
1655
|
+
/** @type {Array<LensFlareElement>} */
|
|
1656
|
+
const made = this.made = glow > 0 ? [
|
|
1657
|
+
{at: 0, size: .7 * glow, color: hsl(0, 0, 1, .5), shape: 'glow'},
|
|
1658
|
+
{at: 0, size: .25 * glow, color: hsl(0, 0, 1, .9), shape: 'glow'}] : [];
|
|
1587
1659
|
for (let i = 0; i < this.count; ++i)
|
|
1588
1660
|
{
|
|
1589
1661
|
// spread along the line, each a place of its own, the far ones bigger
|
|
1590
1662
|
const at = (i + random.float(.2, .8)) / max(this.count, 1) * 1.9 + .2;
|
|
1591
|
-
const pick = random.float(), shape = pick
|
|
1592
|
-
|
|
1663
|
+
const pick = random.float(), shape = shapes ? shapes[min(pick * shapes.length | 0, shapes.length - 1)] :
|
|
1664
|
+
pick < .5 ? 'disc' : pick < .8 ? 'ring' : 'glow';
|
|
1665
|
+
made.push({at, size: random.float(.04, .1) * (1 + at) * ghost, shape,
|
|
1593
1666
|
color: hsl(random.float(), s * .9, .6, random.float(.15, .4))});
|
|
1594
1667
|
}
|
|
1595
1668
|
this.madeKey = key;
|
|
@@ -1597,69 +1670,85 @@ class LensFlare3D extends EngineObject3D
|
|
|
1597
1670
|
return this.made;
|
|
1598
1671
|
}
|
|
1599
1672
|
|
|
1600
|
-
// what the flare is of, seen from the camera: the way to it, how far it is, Infinity for the sun
|
|
1601
|
-
//
|
|
1673
|
+
// what the flare is of, seen from the camera: the way to it, how far it is, Infinity for the sun and for a
|
|
1674
|
+
// directional light, which shines from its place toward the origin, and a point to find it on the screen by;
|
|
1675
|
+
// undefined with no direction, or a light that is gone or at the camera
|
|
1602
1676
|
flareSource()
|
|
1603
1677
|
{
|
|
1604
1678
|
const camera = render3D.camera.pos, light = this.light;
|
|
1605
|
-
if (
|
|
1679
|
+
if (light?.destroyed) return;
|
|
1680
|
+
if (!light || light.directional)
|
|
1606
1681
|
{
|
|
1607
|
-
const
|
|
1608
|
-
if (!
|
|
1609
|
-
const direction =
|
|
1682
|
+
const from = light ? light.getWorldPos3D() : render3D.sunDirection;
|
|
1683
|
+
if (!from.lengthSquared()) return;
|
|
1684
|
+
const direction = from.normalize();
|
|
1610
1685
|
return {direction, distance: Infinity, pos: camera.add(direction.scale(100))};
|
|
1611
1686
|
}
|
|
1612
|
-
if (light.destroyed) return;
|
|
1613
1687
|
const pos = light.getWorldPos3D(), offset = pos.subtract(camera), distance = offset.length();
|
|
1614
1688
|
return distance ? {direction: offset.scale(1 / distance), distance, pos} : undefined;
|
|
1615
1689
|
}
|
|
1616
1690
|
|
|
1617
|
-
|
|
1618
|
-
|
|
1619
|
-
|
|
1620
|
-
|
|
1691
|
+
// how the flare would show with nothing in the way: where its source is on the screen, how strong it is there,
|
|
1692
|
+
// fading as it leaves the screen, its tint and the height its sizes are parts of; undefined when it would not
|
|
1693
|
+
// show at all, behind the camera, off the screen, or of a light that is off or seen from outside its cone
|
|
1694
|
+
flareLook()
|
|
1621
1695
|
{
|
|
1622
|
-
const source = this.flareSource();
|
|
1623
|
-
|
|
1624
|
-
|
|
1625
|
-
|
|
1626
|
-
/** The parts of the flare as they are drawn now: each one's place on the screen, its size in pixels and its
|
|
1627
|
-
* color, dimmed by how much of the sun shows; empty when there is nothing to draw
|
|
1628
|
-
* @return {Array<{pos: Vector2, size: number, color: Color, shape: string}>} */
|
|
1629
|
-
getScreenElements()
|
|
1630
|
-
{
|
|
1631
|
-
const sun = this.getSunScreenPos(), center = mainCanvasSize.scale(.5);
|
|
1632
|
-
if (!sun || !center.x || !center.y) return [];
|
|
1696
|
+
const source = this.flareSource(), center = mainCanvasSize.scale(.5);
|
|
1697
|
+
const sun = source && render3D.worldToScreen(source.pos);
|
|
1698
|
+
if (!sun || !center.x || !center.y) return;
|
|
1633
1699
|
// it fades as the sun leaves the screen, gone when it is a third of the screen past the edge
|
|
1634
1700
|
const off = max(abs(sun.x - center.x) / center.x, abs(sun.y - center.y) / center.y);
|
|
1635
|
-
const strength =
|
|
1636
|
-
if (!(strength > 0)) return
|
|
1701
|
+
const strength = clamp((1.3 - off) / .5) * this.intensity;
|
|
1702
|
+
if (!(strength > 0)) return;
|
|
1637
1703
|
let tint = this.color.multiply(render3D.sunColor), height = mainCanvasSize.y * this.flareSize;
|
|
1638
|
-
const light = this.light
|
|
1704
|
+
const light = this.light;
|
|
1639
1705
|
if (light)
|
|
1640
1706
|
{
|
|
1641
|
-
if (!source) return [];
|
|
1642
1707
|
// a light's flare is the light's color, as bright as the light up to 1, and only from inside its cone
|
|
1643
1708
|
const cone = render3DLightCone(light), d = source.direction;
|
|
1644
1709
|
const inCone = clamp(-(cone[0] * d.x + cone[1] * d.y + cone[2] * d.z) - cone[3]);
|
|
1645
1710
|
const c = light.color, bright = c.a * clamp(light.intensity) * inCone;
|
|
1646
|
-
if (!(bright > 0)) return
|
|
1711
|
+
if (!(bright > 0)) return;
|
|
1647
1712
|
tint = this.color.multiply(rgb(c.r, c.g, c.b, bright));
|
|
1648
|
-
|
|
1713
|
+
if (!light.directional)
|
|
1714
|
+
height *= min(1, light.radius / source.distance); // smaller from farther than it reaches
|
|
1649
1715
|
}
|
|
1716
|
+
return height > 0 ? {sun, center, strength, tint, height} : undefined;
|
|
1717
|
+
}
|
|
1718
|
+
|
|
1719
|
+
/** Where the sun, or the flare's light, is on the screen, in pixels like mousePosScreen, undefined when it is
|
|
1720
|
+
* behind the camera
|
|
1721
|
+
* @return {Vector2|undefined} */
|
|
1722
|
+
getScreenPos()
|
|
1723
|
+
{
|
|
1724
|
+
const source = this.flareSource();
|
|
1725
|
+
return source && render3D.worldToScreen(source.pos);
|
|
1726
|
+
}
|
|
1727
|
+
|
|
1728
|
+
/** The parts of the flare as they are drawn now: each one's place on the screen, its size in pixels, a vector
|
|
1729
|
+
* when the element's is, and its color, dimmed by how much of the sun shows; empty when there is nothing to draw
|
|
1730
|
+
* @return {Array<{pos: Vector2, size: number|Vector2, color: Color, shape: string|undefined,
|
|
1731
|
+
* tileInfo: TileInfo|undefined, angle: number}>} */
|
|
1732
|
+
getScreenElements()
|
|
1733
|
+
{
|
|
1734
|
+
const look = this.visible > 0 && this.flareLook();
|
|
1735
|
+
if (!look) return [];
|
|
1736
|
+
const {sun, center, tint, height} = look, strength = look.strength * this.visible;
|
|
1650
1737
|
return this.getElements().map((e)=>
|
|
1651
1738
|
{
|
|
1652
1739
|
const c = e.color.multiply(tint);
|
|
1653
1740
|
// along the line and past the middle, a lerp would stop there
|
|
1654
|
-
|
|
1655
|
-
|
|
1741
|
+
const size = typeof e.size == 'number' ? e.size * height : e.size.scale(height);
|
|
1742
|
+
return {pos: sun.add(center.subtract(sun).scale(e.at)), size, shape: e.shape, tileInfo: e.tileInfo,
|
|
1743
|
+
angle: e.angle || 0, color: rgb(c.r, c.g, c.b, c.a * strength)};
|
|
1656
1744
|
});
|
|
1657
1745
|
}
|
|
1658
1746
|
|
|
1659
1747
|
/** Is something between the camera and the sun, or the flare's light: the level, or an object that is not see
|
|
1660
|
-
* through
|
|
1748
|
+
* through, hit on its triangles as render3D.pick hits it, so a mesh the camera is inside hides nothing unless
|
|
1749
|
+
* it is doubleSided
|
|
1661
1750
|
* @return {boolean} */
|
|
1662
|
-
|
|
1751
|
+
isHidden()
|
|
1663
1752
|
{
|
|
1664
1753
|
const source = this.flareSource();
|
|
1665
1754
|
if (!source) return true;
|
|
@@ -1673,22 +1762,31 @@ class LensFlare3D extends EngineObject3D
|
|
|
1673
1762
|
for (const map of maps)
|
|
1674
1763
|
if (map.raycast(ray, reach, (type)=> !map.blockType(type).seeThrough))
|
|
1675
1764
|
return true;
|
|
1676
|
-
//
|
|
1677
|
-
//
|
|
1678
|
-
//
|
|
1679
|
-
const
|
|
1680
|
-
|
|
1681
|
-
|
|
1682
|
-
|
|
1683
|
-
|
|
1765
|
+
// a light's lamp, the mesh the light is inside, does not hide it: one whose box, in its own space, the light
|
|
1766
|
+
// is in, and no wider than half the light's reach, since a room or a whole level has a box around its lights
|
|
1767
|
+
// too, and its walls do hide them from outside
|
|
1768
|
+
const lamp = (o)=>
|
|
1769
|
+
{
|
|
1770
|
+
if (!this.light || reach === Infinity || !o.mesh || o instanceof HeightMap) return false;
|
|
1771
|
+
const matrix = render3DObjectMatrix(o), b = o.mesh.bounds || o.mesh.getBounds();
|
|
1772
|
+
const side = max(b.max.x - b.min.x, b.max.y - b.min.y, b.max.z - b.min.z) * render3DMaxStretch(matrix.m);
|
|
1773
|
+
if (!matrix.determinant() || side > this.light.radius / 2) return false;
|
|
1774
|
+
const p = matrix.copy().invert().transformPoint(source.pos);
|
|
1775
|
+
return p.x >= b.min.x && p.x <= b.max.x && p.y >= b.min.y && p.y <= b.max.y &&
|
|
1776
|
+
p.z >= b.min.z && p.z <= b.max.z;
|
|
1777
|
+
};
|
|
1778
|
+
const hit = render3D.pick(ray, blockers.filter((o)=> !maps.some((map)=> map === o) && !lamp(o)));
|
|
1684
1779
|
return !!hit && hit.distance < reach;
|
|
1685
1780
|
}
|
|
1686
1781
|
|
|
1687
|
-
/** Ease visible toward whether the sun shows, called automatically each frame
|
|
1782
|
+
/** Ease visible toward whether the sun shows, called automatically each frame; a flare that would not show
|
|
1783
|
+
* wherever the sun is, off the screen or its light off, looks for nothing in the way */
|
|
1688
1784
|
update()
|
|
1689
1785
|
{
|
|
1690
1786
|
super.update();
|
|
1691
|
-
|
|
1787
|
+
if (this.light?.destroyed)
|
|
1788
|
+
return this.destroy(); // the flare of a light that is gone
|
|
1789
|
+
const shows = !!this.flareLook() && !(this.occlusion && this.isHidden());
|
|
1692
1790
|
const step = this.fadeTime > 0 ? timeDelta / this.fadeTime : 1;
|
|
1693
1791
|
this.visible = clamp(this.visible + (shows ? step : -step));
|
|
1694
1792
|
}
|
|
@@ -1702,8 +1800,9 @@ class LensFlare3D extends EngineObject3D
|
|
|
1702
1800
|
setAdditiveBlendMode(true);
|
|
1703
1801
|
for (const e of elements)
|
|
1704
1802
|
{
|
|
1705
|
-
const tileInfo = render3DFlareTile(e.shape);
|
|
1706
|
-
|
|
1803
|
+
const tileInfo = e.tileInfo || render3DFlareTile(e.shape);
|
|
1804
|
+
const size = typeof e.size == 'number' ? vec2(e.size) : e.size;
|
|
1805
|
+
tileInfo && drawTile(e.pos, size, tileInfo, e.color, e.angle, false, undefined, true, true);
|
|
1707
1806
|
}
|
|
1708
1807
|
setAdditiveBlendMode(false);
|
|
1709
1808
|
}
|