littlejsengine 1.23.0 → 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 +38 -38
- package/EDITOR.md +359 -0
- package/FAQ.md +4 -9
- package/README.md +14 -8
- package/REFERENCE.md +2327 -0
- package/dist/littlejs.d.ts +149 -104
- package/dist/littlejs.esm.js +1652 -1332
- package/dist/littlejs.esm.min.js +3 -3
- package/dist/littlejs.js +1651 -1328
- package/dist/littlejs.min.js +2 -2
- package/dist/littlejs.release.js +1553 -1294
- package/package.json +5 -3
- package/plugins/gltf.js +29 -13
- package/plugins/pluginExport.js +0 -4
- package/plugins/postProcess.js +5 -2
- package/plugins/render3d.js +136 -31
- package/plugins/render3dEditor.js +51 -17
- package/plugins/render3dExtras.js +166 -70
- package/plugins/render3dLevel.js +52 -6
- package/plugins/textureSheet.js +1 -0
- package/plugins/tweenSystem.js +39 -34
- package/plugins/uiSystem.js +7 -3
- package/src/engine.js +1 -1
- package/src/engineAudio.js +9 -3
- package/src/engineBuild.mjs +17 -3
- package/src/engineDraw.js +11 -7
- package/src/engineEditor.js +52 -18
- package/src/engineExport.js +1 -0
- package/src/engineObject.js +675 -675
- package/src/engineRelease.js +5 -1
- package/src/engineTileLayer.js +119 -1
- package/src/engineUtilities.js +291 -291
- package/src/engineWebGL.js +12 -3
- package/plugins/zzfxm.js +0 -160
|
@@ -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 off the screen, 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,61 @@ 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, and a spotlight's shows from inside its beam only
|
|
1625
|
+
* in front of the light, and a spotlight's shows from inside its beam only; a DirectionalLight3D's is far
|
|
1626
|
+
* away where it shines from, like the sun's; the flare is destroyed when its light is; light.addFlare
|
|
1627
|
+
* sets this
|
|
1560
1628
|
* @type {Light3D|undefined} */
|
|
1561
1629
|
this.light = undefined;
|
|
1562
1630
|
/** @property {boolean} - Fade out when something is between the camera and the sun */
|
|
1563
1631
|
this.occlusion = true;
|
|
1564
1632
|
/** @property {number} - Seconds the flare takes to fade out or in when the sun is hidden or shows again */
|
|
1565
1633
|
this.fadeTime = .15;
|
|
1566
|
-
/** @property {number} - How much of the sun shows, 0 hidden or behind the camera to 1 in
|
|
1634
|
+
/** @property {number} - How much of the sun shows, 0 hidden, off the screen or behind the camera to 1 in
|
|
1635
|
+
* plain view, eased */
|
|
1567
1636
|
this.visible = 1;
|
|
1568
1637
|
this.renderOrder = 1e9; // over the game's sprites, it is light in the lens
|
|
1569
1638
|
this.madeKey = '';
|
|
1570
|
-
/** @type {Array<
|
|
1639
|
+
/** @type {Array<LensFlareElement>} */
|
|
1571
1640
|
this.made = [];
|
|
1572
1641
|
}
|
|
1573
1642
|
|
|
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<
|
|
1643
|
+
/** The parts of the flare: the elements set by hand, or the ones made from count, seed, saturation, shapes,
|
|
1644
|
+
* glowSize and ghostSize, a glow and a core at the sun and the ghosts, made again when one of those changes
|
|
1645
|
+
* @return {Array<LensFlareElement>} */
|
|
1577
1646
|
getElements()
|
|
1578
1647
|
{
|
|
1579
1648
|
if (this.elements) return this.elements;
|
|
1580
|
-
const
|
|
1649
|
+
const shapes = this.shapes?.length ? this.shapes : undefined, glow = this.glowSize, ghost = this.ghostSize;
|
|
1650
|
+
const key = [this.count, this.seed, this.saturation, glow, ghost, shapes?.length, shapes].join();
|
|
1581
1651
|
if (key !== this.madeKey)
|
|
1582
1652
|
{
|
|
1583
1653
|
const random = new RandomGenerator(this.seed * 7919 + 1), s = clamp(this.saturation);
|
|
1584
|
-
|
|
1585
|
-
|
|
1586
|
-
{at: 0, size: .
|
|
1654
|
+
/** @type {Array<LensFlareElement>} */
|
|
1655
|
+
const made = this.made = glow > 0 ? [
|
|
1656
|
+
{at: 0, size: .7 * glow, color: hsl(0, 0, 1, .5), shape: 'glow'},
|
|
1657
|
+
{at: 0, size: .25 * glow, color: hsl(0, 0, 1, .9), shape: 'glow'}] : [];
|
|
1587
1658
|
for (let i = 0; i < this.count; ++i)
|
|
1588
1659
|
{
|
|
1589
1660
|
// spread along the line, each a place of its own, the far ones bigger
|
|
1590
1661
|
const at = (i + random.float(.2, .8)) / max(this.count, 1) * 1.9 + .2;
|
|
1591
|
-
const pick = random.float(), shape = pick
|
|
1592
|
-
|
|
1662
|
+
const pick = random.float(), shape = shapes ? shapes[min(pick * shapes.length | 0, shapes.length - 1)] :
|
|
1663
|
+
pick < .5 ? 'disc' : pick < .8 ? 'ring' : 'glow';
|
|
1664
|
+
made.push({at, size: random.float(.04, .1) * (1 + at) * ghost, shape,
|
|
1593
1665
|
color: hsl(random.float(), s * .9, .6, random.float(.15, .4))});
|
|
1594
1666
|
}
|
|
1595
1667
|
this.madeKey = key;
|
|
@@ -1597,69 +1669,85 @@ class LensFlare3D extends EngineObject3D
|
|
|
1597
1669
|
return this.made;
|
|
1598
1670
|
}
|
|
1599
1671
|
|
|
1600
|
-
// what the flare is of, seen from the camera: the way to it, how far it is, Infinity for the sun
|
|
1601
|
-
//
|
|
1672
|
+
// what the flare is of, seen from the camera: the way to it, how far it is, Infinity for the sun and for a
|
|
1673
|
+
// directional light, which shines from its place toward the origin, and a point to find it on the screen by;
|
|
1674
|
+
// undefined with no direction, or a light that is gone or at the camera
|
|
1602
1675
|
flareSource()
|
|
1603
1676
|
{
|
|
1604
1677
|
const camera = render3D.camera.pos, light = this.light;
|
|
1605
|
-
if (
|
|
1678
|
+
if (light?.destroyed) return;
|
|
1679
|
+
if (!light || light.directional)
|
|
1606
1680
|
{
|
|
1607
|
-
const
|
|
1608
|
-
if (!
|
|
1609
|
-
const direction =
|
|
1681
|
+
const from = light ? light.getWorldPos3D() : render3D.sunDirection;
|
|
1682
|
+
if (!from.lengthSquared()) return;
|
|
1683
|
+
const direction = from.normalize();
|
|
1610
1684
|
return {direction, distance: Infinity, pos: camera.add(direction.scale(100))};
|
|
1611
1685
|
}
|
|
1612
|
-
if (light.destroyed) return;
|
|
1613
1686
|
const pos = light.getWorldPos3D(), offset = pos.subtract(camera), distance = offset.length();
|
|
1614
1687
|
return distance ? {direction: offset.scale(1 / distance), distance, pos} : undefined;
|
|
1615
1688
|
}
|
|
1616
1689
|
|
|
1617
|
-
|
|
1618
|
-
|
|
1619
|
-
|
|
1620
|
-
|
|
1690
|
+
// how the flare would show with nothing in the way: where its source is on the screen, how strong it is there,
|
|
1691
|
+
// fading as it leaves the screen, its tint and the height its sizes are parts of; undefined when it would not
|
|
1692
|
+
// show at all, behind the camera, off the screen, or of a light that is off or seen from outside its cone
|
|
1693
|
+
flareLook()
|
|
1621
1694
|
{
|
|
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 [];
|
|
1695
|
+
const source = this.flareSource(), center = mainCanvasSize.scale(.5);
|
|
1696
|
+
const sun = source && render3D.worldToScreen(source.pos);
|
|
1697
|
+
if (!sun || !center.x || !center.y) return;
|
|
1633
1698
|
// it fades as the sun leaves the screen, gone when it is a third of the screen past the edge
|
|
1634
1699
|
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
|
|
1700
|
+
const strength = clamp((1.3 - off) / .5) * this.intensity;
|
|
1701
|
+
if (!(strength > 0)) return;
|
|
1637
1702
|
let tint = this.color.multiply(render3D.sunColor), height = mainCanvasSize.y * this.flareSize;
|
|
1638
|
-
const light = this.light
|
|
1703
|
+
const light = this.light;
|
|
1639
1704
|
if (light)
|
|
1640
1705
|
{
|
|
1641
|
-
if (!source) return [];
|
|
1642
1706
|
// a light's flare is the light's color, as bright as the light up to 1, and only from inside its cone
|
|
1643
1707
|
const cone = render3DLightCone(light), d = source.direction;
|
|
1644
1708
|
const inCone = clamp(-(cone[0] * d.x + cone[1] * d.y + cone[2] * d.z) - cone[3]);
|
|
1645
1709
|
const c = light.color, bright = c.a * clamp(light.intensity) * inCone;
|
|
1646
|
-
if (!(bright > 0)) return
|
|
1710
|
+
if (!(bright > 0)) return;
|
|
1647
1711
|
tint = this.color.multiply(rgb(c.r, c.g, c.b, bright));
|
|
1648
|
-
|
|
1712
|
+
if (!light.directional)
|
|
1713
|
+
height *= min(1, light.radius / source.distance); // smaller from farther than it reaches
|
|
1649
1714
|
}
|
|
1715
|
+
return height > 0 ? {sun, center, strength, tint, height} : undefined;
|
|
1716
|
+
}
|
|
1717
|
+
|
|
1718
|
+
/** Where the sun, or the flare's light, is on the screen, in pixels like mousePosScreen, undefined when it is
|
|
1719
|
+
* behind the camera
|
|
1720
|
+
* @return {Vector2|undefined} */
|
|
1721
|
+
getScreenPos()
|
|
1722
|
+
{
|
|
1723
|
+
const source = this.flareSource();
|
|
1724
|
+
return source && render3D.worldToScreen(source.pos);
|
|
1725
|
+
}
|
|
1726
|
+
|
|
1727
|
+
/** The parts of the flare as they are drawn now: each one's place on the screen, its size in pixels, a vector
|
|
1728
|
+
* when the element's is, and its color, dimmed by how much of the sun shows; empty when there is nothing to draw
|
|
1729
|
+
* @return {Array<{pos: Vector2, size: number|Vector2, color: Color, shape: string|undefined,
|
|
1730
|
+
* tileInfo: TileInfo|undefined, angle: number}>} */
|
|
1731
|
+
getScreenElements()
|
|
1732
|
+
{
|
|
1733
|
+
const look = this.visible > 0 && this.flareLook();
|
|
1734
|
+
if (!look) return [];
|
|
1735
|
+
const {sun, center, tint, height} = look, strength = look.strength * this.visible;
|
|
1650
1736
|
return this.getElements().map((e)=>
|
|
1651
1737
|
{
|
|
1652
1738
|
const c = e.color.multiply(tint);
|
|
1653
1739
|
// along the line and past the middle, a lerp would stop there
|
|
1654
|
-
|
|
1655
|
-
|
|
1740
|
+
const size = typeof e.size == 'number' ? e.size * height : e.size.scale(height);
|
|
1741
|
+
return {pos: sun.add(center.subtract(sun).scale(e.at)), size, shape: e.shape, tileInfo: e.tileInfo,
|
|
1742
|
+
angle: e.angle || 0, color: rgb(c.r, c.g, c.b, c.a * strength)};
|
|
1656
1743
|
});
|
|
1657
1744
|
}
|
|
1658
1745
|
|
|
1659
1746
|
/** Is something between the camera and the sun, or the flare's light: the level, or an object that is not see
|
|
1660
|
-
* through
|
|
1747
|
+
* through, hit on its triangles as render3D.pick hits it, so a mesh the camera is inside hides nothing unless
|
|
1748
|
+
* it is doubleSided
|
|
1661
1749
|
* @return {boolean} */
|
|
1662
|
-
|
|
1750
|
+
isHidden()
|
|
1663
1751
|
{
|
|
1664
1752
|
const source = this.flareSource();
|
|
1665
1753
|
if (!source) return true;
|
|
@@ -1673,22 +1761,29 @@ class LensFlare3D extends EngineObject3D
|
|
|
1673
1761
|
for (const map of maps)
|
|
1674
1762
|
if (map.raycast(ray, reach, (type)=> !map.blockType(type).seeThrough))
|
|
1675
1763
|
return true;
|
|
1676
|
-
//
|
|
1677
|
-
//
|
|
1678
|
-
|
|
1679
|
-
|
|
1680
|
-
|
|
1681
|
-
|
|
1682
|
-
|
|
1683
|
-
|
|
1764
|
+
// a light's lamp, the mesh the light is inside, does not hide it: one whose box, in its own space, the light
|
|
1765
|
+
// is in
|
|
1766
|
+
const lamp = (o)=>
|
|
1767
|
+
{
|
|
1768
|
+
if (!this.light || reach === Infinity || !o.mesh || o instanceof HeightMap) return false;
|
|
1769
|
+
const matrix = render3DObjectMatrix(o);
|
|
1770
|
+
if (!matrix.determinant()) return false;
|
|
1771
|
+
const p = matrix.copy().invert().transformPoint(source.pos), b = o.mesh.bounds || o.mesh.getBounds();
|
|
1772
|
+
return p.x >= b.min.x && p.x <= b.max.x && p.y >= b.min.y && p.y <= b.max.y &&
|
|
1773
|
+
p.z >= b.min.z && p.z <= b.max.z;
|
|
1774
|
+
};
|
|
1775
|
+
const hit = render3D.pick(ray, blockers.filter((o)=> !maps.some((map)=> map === o) && !lamp(o)));
|
|
1684
1776
|
return !!hit && hit.distance < reach;
|
|
1685
1777
|
}
|
|
1686
1778
|
|
|
1687
|
-
/** Ease visible toward whether the sun shows, called automatically each frame
|
|
1779
|
+
/** Ease visible toward whether the sun shows, called automatically each frame; a flare that would not show
|
|
1780
|
+
* wherever the sun is, off the screen or its light off, looks for nothing in the way */
|
|
1688
1781
|
update()
|
|
1689
1782
|
{
|
|
1690
1783
|
super.update();
|
|
1691
|
-
|
|
1784
|
+
if (this.light?.destroyed)
|
|
1785
|
+
return this.destroy(); // the flare of a light that is gone
|
|
1786
|
+
const shows = !!this.flareLook() && !(this.occlusion && this.isHidden());
|
|
1692
1787
|
const step = this.fadeTime > 0 ? timeDelta / this.fadeTime : 1;
|
|
1693
1788
|
this.visible = clamp(this.visible + (shows ? step : -step));
|
|
1694
1789
|
}
|
|
@@ -1702,8 +1797,9 @@ class LensFlare3D extends EngineObject3D
|
|
|
1702
1797
|
setAdditiveBlendMode(true);
|
|
1703
1798
|
for (const e of elements)
|
|
1704
1799
|
{
|
|
1705
|
-
const tileInfo = render3DFlareTile(e.shape);
|
|
1706
|
-
|
|
1800
|
+
const tileInfo = e.tileInfo || render3DFlareTile(e.shape);
|
|
1801
|
+
const size = typeof e.size == 'number' ? vec2(e.size) : e.size;
|
|
1802
|
+
tileInfo && drawTile(e.pos, size, tileInfo, e.color, e.angle, false, undefined, true, true);
|
|
1707
1803
|
}
|
|
1708
1804
|
setAdditiveBlendMode(false);
|
|
1709
1805
|
}
|
package/plugins/render3dLevel.js
CHANGED
|
@@ -84,8 +84,9 @@ function level3DAddMesh(name, mesh, tileInfo, color=WHITE)
|
|
|
84
84
|
* are added before its objects are made, one the game added itself keeps its place
|
|
85
85
|
* - A level may set the scene too, in a scene block beside its objects: sky, three colors for straight up, the
|
|
86
86
|
* horizon and straight down, ambient, how much of them lights the scene, .5 when not given, sunDirection and
|
|
87
|
-
* sunColor, fog, its start and end, fogColor, the horizon color when not given, and
|
|
88
|
-
* leaves out stays as the game set it, and a level with no block changes
|
|
87
|
+
* sunColor, fog, its start and end, fogColor, the horizon color when not given, shadows, and lensFlare, the
|
|
88
|
+
* sun's lens flare; what the block leaves out stays as the game set it, and a level with no block changes
|
|
89
|
+
* nothing
|
|
89
90
|
* @param {Object} level - The level, the level editor edits this same object
|
|
90
91
|
* @return {Array<any>} - What each object's type made, a function that made nothing is left out
|
|
91
92
|
* @memberof Level3D */
|
|
@@ -116,7 +117,8 @@ let level3DVoxelTiles, level3DVoxelSetupMap, level3DVoxelMap;
|
|
|
116
117
|
|
|
117
118
|
/** How a level's block map is made: the sheet its blocks show tiles of, and a function to set it up
|
|
118
119
|
* - A level's voxels block makes a VoxelMap when the level loads, with texture 0 and the default tile size unless
|
|
119
|
-
* a sheet is given here; a block's type shows that tile of the sheet on every face
|
|
120
|
+
* a sheet is given here; a block's type shows that tile of the sheet on every face; a game that has loaded no
|
|
121
|
+
* image gets plain blocks, a color for each type
|
|
120
122
|
* - setup is called with each map a level makes, to give block types their own faces or make them see-through
|
|
121
123
|
* - Call it before level3DLoad; with no arguments the defaults are back
|
|
122
124
|
* @param {TileInfo} [tileInfo] - The sheet's first tile, as for a VoxelMap
|
|
@@ -178,7 +180,7 @@ function level3DVoxelsMake(voxels)
|
|
|
178
180
|
{
|
|
179
181
|
const shape = level3DVoxelsShape(voxels);
|
|
180
182
|
if (!shape || typeof VoxelMap == 'undefined') return;
|
|
181
|
-
const map = new VoxelMap(shape.pos, shape.size, level3DVoxelTiles ||
|
|
183
|
+
const map = new VoxelMap(shape.pos, shape.size, level3DVoxelTiles || level3DVoxelPlainTiles());
|
|
182
184
|
level3DVoxelsDecode(voxels.blocks, map.data);
|
|
183
185
|
level3DVoxelSetupMap?.(map);
|
|
184
186
|
map.rebuild();
|
|
@@ -240,6 +242,31 @@ function level3DTerrainSetColors(map, terrain, paint=level3DTerrainPaint(terrain
|
|
|
240
242
|
map.colors = map.heights.map((row, r)=> row.map((v, c)=> palette[paint.cells[r * map.columns + c]] || base));
|
|
241
243
|
}
|
|
242
244
|
|
|
245
|
+
// the sheet a level's blocks show when the game gave none with level3DVoxelSetup: the game's first image, as a
|
|
246
|
+
// VoxelMap takes it, or, for a game that has loaded no image, plain tiles made here, a color for each type, so a
|
|
247
|
+
// block map in a game with no art can still be seen and painted
|
|
248
|
+
let level3DVoxelPlain;
|
|
249
|
+
function level3DVoxelPlainTiles()
|
|
250
|
+
{
|
|
251
|
+
if (textureInfos[0]?.size.x || headlessMode || typeof OffscreenCanvas == 'undefined')
|
|
252
|
+
return tile();
|
|
253
|
+
if (!level3DVoxelPlain)
|
|
254
|
+
{
|
|
255
|
+
// 16 by 16 tiles of 16 pixels, each a color of its own with a darker edge, so the blocks read as blocks
|
|
256
|
+
const context = createCanvasContext(256);
|
|
257
|
+
for (let i = 0; i < 256; ++i)
|
|
258
|
+
{
|
|
259
|
+
const x = i % 16 * 16, y = (i / 16 | 0) * 16, hue = i * .618 % 1;
|
|
260
|
+
context.fillStyle = hsl(hue, .45, .4).toString();
|
|
261
|
+
context.fillRect(x, y, 16, 16);
|
|
262
|
+
context.fillStyle = hsl(hue, .5, .55).toString();
|
|
263
|
+
context.fillRect(x + 1, y + 1, 14, 14);
|
|
264
|
+
}
|
|
265
|
+
level3DVoxelPlain = new TextureInfo(context.canvas);
|
|
266
|
+
}
|
|
267
|
+
return tile(0, 16, level3DVoxelPlain, 0);
|
|
268
|
+
}
|
|
269
|
+
|
|
243
270
|
// the HeightMap of a level's terrain block, undefined when it has none or it is wrong
|
|
244
271
|
function level3DTerrainMake(terrain)
|
|
245
272
|
{
|
|
@@ -284,7 +311,24 @@ function level3DSceneApply(scene)
|
|
|
284
311
|
r.fogColor = color(scene.fogColor) || r.fogColor;
|
|
285
312
|
if (typeof scene.shadows === 'boolean')
|
|
286
313
|
r.shadows = scene.shadows;
|
|
314
|
+
if (typeof scene.lensFlare === 'boolean')
|
|
315
|
+
level3DSceneFlare(scene.lensFlare);
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
// 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
|
|
319
|
+
// earlier level's, and off takes away the one a scene made, never the game's
|
|
320
|
+
let level3DSunFlare;
|
|
321
|
+
function level3DSceneFlare(on)
|
|
322
|
+
{
|
|
323
|
+
if (!on)
|
|
324
|
+
{
|
|
325
|
+
level3DSunFlare && !level3DSunFlare.destroyed && level3DSunFlare.destroy();
|
|
326
|
+
level3DSunFlare = undefined;
|
|
327
|
+
}
|
|
328
|
+
else if (!level3DSunHasFlare())
|
|
329
|
+
level3DSunFlare = new LensFlare3D;
|
|
287
330
|
}
|
|
331
|
+
const level3DSunHasFlare = ()=> engineObjects.some((o)=> o instanceof LensFlare3D && !o.destroyed && !o.light);
|
|
288
332
|
|
|
289
333
|
// a vec3 of an array of three numbers, as the file has them, or the fallback
|
|
290
334
|
function level3DVector(value, fallback)
|
|
@@ -634,7 +678,8 @@ level3DAddType('Cylinder', function(pos, properties)
|
|
|
634
678
|
{ return level3DMakeShape(pos, properties, level3DCylinderMesh ||= buildCylinder()); },
|
|
635
679
|
{color: WHITE, tile: -1, solid: true});
|
|
636
680
|
// a light, a spotlight with a cone: cone is the angle in degrees from its forward out to the edge of its beam, the
|
|
637
|
-
// object's rotation aims it, softness is how much of the cone fades,
|
|
681
|
+
// object's rotation aims it, softness is how much of the cone fades, shadows makes it the one that casts them, and
|
|
682
|
+
// lensFlare gives it a lens flare; not flare, since a property is also set on the object, where flare is the flare
|
|
638
683
|
level3DAddType('Light', function(pos, properties)
|
|
639
684
|
{
|
|
640
685
|
const light = new Light3D(pos, properties.radius, properties.color, properties.intensity);
|
|
@@ -642,5 +687,6 @@ level3DAddType('Light', function(pos, properties)
|
|
|
642
687
|
light.coneSoftness = properties.softness;
|
|
643
688
|
if (properties.shadows && render3D)
|
|
644
689
|
render3D.shadowLight = light;
|
|
690
|
+
properties.lensFlare && light.addFlare();
|
|
645
691
|
return light;
|
|
646
|
-
}, {color: WHITE, radius: 5, intensity: 1, cone: 0, softness: .2, shadows: false});
|
|
692
|
+
}, {color: WHITE, radius: 5, intensity: 1, cone: 0, softness: .2, shadows: false, lensFlare: false});
|
package/plugins/textureSheet.js
CHANGED
|
@@ -303,6 +303,7 @@ function loadAtlas(imageSrc, jsonSrc, padding=textureSheetPadding)
|
|
|
303
303
|
for (const group of parseAtlas(data))
|
|
304
304
|
{
|
|
305
305
|
// reserve a block of full size cells, one per frame
|
|
306
|
+
if (!group.frames.length) continue; // a tag with no frames, the groups after it still load
|
|
306
307
|
const sourceSize = group.frames[0].sourceSize;
|
|
307
308
|
const blockSize = vec2(sourceSize.x*group.frames.length, sourceSize.y);
|
|
308
309
|
const added = textureSheetAdd(blockSize, sourceSize, padding);
|
package/plugins/tweenSystem.js
CHANGED
|
@@ -593,45 +593,50 @@ function tweenUpdate(gameDelta, realDelta)
|
|
|
593
593
|
const list = tweenUpdateList.length ? [] : tweenUpdateList, pass = ++tweenUpdatePass;
|
|
594
594
|
for (const t of tweenActive)
|
|
595
595
|
list.push(t);
|
|
596
|
-
|
|
596
|
+
// the list is let go however the walk ends: a callback that throws must not leave it held, or every
|
|
597
|
+
// update after would take it for an update still going and make a list of its own
|
|
598
|
+
try
|
|
597
599
|
{
|
|
598
|
-
|
|
599
|
-
// stopped, or started again by a callback this update, or during an update a callback ran inside it, which
|
|
600
|
-
// counts on from this one
|
|
601
|
-
if (!t.active || t.activePass >= pass) continue;
|
|
602
|
-
let dt;
|
|
603
|
-
if (enginePath)
|
|
600
|
+
for (let i = list.length; i--;)
|
|
604
601
|
{
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
t.
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
602
|
+
const t = list[i];
|
|
603
|
+
// stopped, or started again by a callback this update, or during an update a callback ran inside it, which
|
|
604
|
+
// counts on from this one
|
|
605
|
+
if (!t.active || t.activePass >= pass) continue;
|
|
606
|
+
let dt;
|
|
607
|
+
if (enginePath)
|
|
608
|
+
{
|
|
609
|
+
// a paused tween keeps count too, so it does not jump when resumed
|
|
610
|
+
dt = t.useRealTime ? timeReal - t.lastTimeReal : time - t.lastTime;
|
|
611
|
+
t.lastTime = time;
|
|
612
|
+
t.lastTimeReal = timeReal;
|
|
613
|
+
}
|
|
614
|
+
else
|
|
615
|
+
dt = t.useRealTime ? realDelta : gameDelta;
|
|
616
|
+
if (t.target?.destroyed) { t.stop(); continue; } // its object is gone, paused or not
|
|
617
|
+
if (t.paused || dt <= 0) continue;
|
|
614
618
|
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
619
|
+
t.life -= dt;
|
|
620
|
+
if (t.life > 1e-9) // the engine's deltas add up a rounding error short of the duration
|
|
621
|
+
{
|
|
622
|
+
t.callback(t.interp(t.life));
|
|
623
|
+
}
|
|
624
|
+
else
|
|
625
|
+
{
|
|
626
|
+
// Completion: fire end value, remove from active, start the next iteration
|
|
627
|
+
// of a loop or pingPong, or when there is none it has completed, fire onComplete
|
|
628
|
+
t.callback(t.interp(0));
|
|
629
|
+
if (!t.active || t.activePass >= pass)
|
|
630
|
+
continue; // stopped or restarted by its own callback, the run it was on ends without completing
|
|
631
|
+
tweenDeactivate(t);
|
|
632
|
+
const next = t.thenCallback;
|
|
633
|
+
t.thenCallback = undefined;
|
|
634
|
+
if (!(next && next()) && t.onComplete)
|
|
635
|
+
t.onComplete();
|
|
636
|
+
}
|
|
632
637
|
}
|
|
633
638
|
}
|
|
634
|
-
list.length = 0;
|
|
639
|
+
finally { list.length = 0; }
|
|
635
640
|
}
|
|
636
641
|
|
|
637
642
|
/** Stop every active tween, ending loops too, without calling their then-callbacks.
|
package/plugins/uiSystem.js
CHANGED
|
@@ -615,8 +615,7 @@ class UISystemPlugin
|
|
|
615
615
|
}
|
|
616
616
|
}
|
|
617
617
|
|
|
618
|
-
/** Destroy and remove all objects
|
|
619
|
-
* @memberof UISystem */
|
|
618
|
+
/** Destroy and remove all objects */
|
|
620
619
|
destroyObjects()
|
|
621
620
|
{
|
|
622
621
|
for (const o of this.uiObjects)
|
|
@@ -1137,8 +1136,10 @@ class UIObject
|
|
|
1137
1136
|
return void inputClearKey(0, 0, false, true, false);
|
|
1138
1137
|
}
|
|
1139
1138
|
}
|
|
1139
|
+
// the object that was the active one going into this update: one pressed and let go inside a
|
|
1140
|
+
// frame is clicked on the next, with its release, not on both
|
|
1140
1141
|
if (!uiSystem.activateOnPress)
|
|
1141
|
-
if (!mouseDown && this.isActiveObject() && this.interactive)
|
|
1142
|
+
if (!mouseDown && isActive && this.isActiveObject() && this.interactive)
|
|
1142
1143
|
this.click();
|
|
1143
1144
|
if (this.destroyed) return;
|
|
1144
1145
|
}
|
|
@@ -1881,7 +1882,10 @@ class UIVideo extends UIObject
|
|
|
1881
1882
|
if (this.destroyed)
|
|
1882
1883
|
return;
|
|
1883
1884
|
|
|
1885
|
+
// let go of the media too, a paused video keeps what it has loaded
|
|
1884
1886
|
this.video.pause();
|
|
1887
|
+
this.video.removeAttribute?.('src');
|
|
1888
|
+
this.video.load?.();
|
|
1885
1889
|
this.video.remove();
|
|
1886
1890
|
super.destroy();
|
|
1887
1891
|
}
|