littlejsengine 1.21.0 → 1.23.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/README.md +1 -1
- package/dist/littlejs.d.ts +1258 -141
- package/dist/littlejs.esm.js +5023 -429
- package/dist/littlejs.esm.min.js +22 -9
- package/dist/littlejs.js +4977 -430
- package/dist/littlejs.min.js +21 -8
- package/dist/littlejs.release.js +2759 -313
- package/package.json +3 -2
- package/plugins/gltf.js +7 -1
- package/plugins/lightSystem.js +50 -5
- package/plugins/math3d.js +42 -20
- package/plugins/parallax.js +174 -0
- package/plugins/particleEffects.js +760 -0
- package/plugins/pluginExport.js +40 -0
- package/plugins/postProcess.js +206 -28
- package/plugins/render3d.js +495 -188
- package/plugins/render3dEditor.js +1754 -80
- package/plugins/render3dExtras.js +294 -21
- package/plugins/render3dLevel.js +463 -6
- package/plugins/sceneSystem.js +3 -2
- package/src/engine.js +1 -1
- package/src/engineAudio.js +25 -8
- package/src/engineBuild.mjs +2 -0
- package/src/engineDraw.js +59 -4
- package/src/engineEditor.js +475 -37
- package/src/engineExport.js +7 -0
- package/src/engineInput.js +48 -6
- package/src/engineParticles.js +80 -11
- package/src/engineRelease.js +13 -2
- package/src/engineSettings.js +14 -0
- package/src/engineWebGL.js +27 -10
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "littlejsengine",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.23.0",
|
|
4
4
|
"description": "LittleJS - Tiny and Fast HTML5 Game Engine",
|
|
5
5
|
"main": "dist/littlejs.esm.js",
|
|
6
6
|
"types": "dist/littlejs.d.ts",
|
|
@@ -45,7 +45,8 @@
|
|
|
45
45
|
"build": "node src/engineBuild.mjs",
|
|
46
46
|
"build-docs": "node tools/buildDocs.mjs",
|
|
47
47
|
"build-release": "node tools/buildRelease.mjs",
|
|
48
|
-
"test": "node --test --import ./test/setup.mjs \"test/**/*.test.mjs\""
|
|
48
|
+
"test": "node --test --import ./test/setup.mjs \"test/**/*.test.mjs\"",
|
|
49
|
+
"test:browser": "node test/browser/run.mjs"
|
|
49
50
|
},
|
|
50
51
|
"engines": {
|
|
51
52
|
"node": "^20.19.0 || >=22.12.0"
|
package/plugins/gltf.js
CHANGED
|
@@ -361,7 +361,13 @@ async function loadGLTF(url)
|
|
|
361
361
|
const response = await fetch(url);
|
|
362
362
|
if (!response.ok)
|
|
363
363
|
throw new Error('loadGLTF failed: ' + url);
|
|
364
|
-
|
|
364
|
+
// the files beside it are beside where it came from, after any redirect; a blob or data url has nothing
|
|
365
|
+
// beside it, and a model in one is whole, or parsed with parseGLTF and a base of its own
|
|
366
|
+
const from = response.url || url;
|
|
367
|
+
let base = '';
|
|
368
|
+
if (!/^(blob|data):/i.test(from))
|
|
369
|
+
base = response.url ? new URL('.', from).href : from.slice(0, from.lastIndexOf('/') + 1);
|
|
370
|
+
return parseGLTF(await response.arrayBuffer(), base);
|
|
365
371
|
}
|
|
366
372
|
|
|
367
373
|
/** Parse a model from GLB bytes or glTF JSON, fetching the buffers and images it refers to
|
package/plugins/lightSystem.js
CHANGED
|
@@ -20,6 +20,8 @@
|
|
|
20
20
|
* shadow map through renderShadow(), which calls render() by default; obj.castShadow = false keeps it
|
|
21
21
|
* out (a floor TileLayer, a background), a draw's alpha sets how much light it blocks, and
|
|
22
22
|
* setShadowTransparent lets its color tint the light
|
|
23
|
+
* - Set light.glow for a soft hazy glow over a light, like a lamp at night; it is added over the lit scene after the
|
|
24
|
+
* lightmap, so it shows in the dark and sits in front of everything there
|
|
23
25
|
* - Must be constructed BEFORE PostProcessPlugin so post-process sees lit pixels
|
|
24
26
|
* @namespace LightSystem
|
|
25
27
|
*/
|
|
@@ -156,6 +158,7 @@ class LightSystemPlugin
|
|
|
156
158
|
// again on a context restore, the canvas may have changed since
|
|
157
159
|
if (lightSystem.textureSizeAuto)
|
|
158
160
|
lightSystem.textureSize = mainCanvasSize.copy();
|
|
161
|
+
lightSystem.clampTextureSizes();
|
|
159
162
|
|
|
160
163
|
// allocate the lightmap texture with null data at textureSize
|
|
161
164
|
lightSystem.texture = glContext.createTexture();
|
|
@@ -347,6 +350,7 @@ class LightSystemPlugin
|
|
|
347
350
|
const ls = lightSystem;
|
|
348
351
|
|
|
349
352
|
// make the resources the first time, and again when a size changed
|
|
353
|
+
ls.clampTextureSizes();
|
|
350
354
|
if (!ls.shadowMap || ls.shadowMapSize !== ls.shadowMapSizeAllocated
|
|
351
355
|
|| ls.shadowTextureSize !== ls.shadowTextureSizeAllocated)
|
|
352
356
|
{
|
|
@@ -421,13 +425,13 @@ class LightSystemPlugin
|
|
|
421
425
|
// an automatic size follows the canvas, so reallocate the lightmap when
|
|
422
426
|
// the canvas changed size, after the flush so the batch keeps its texture
|
|
423
427
|
const size = lightSystem.textureSize;
|
|
424
|
-
|
|
425
|
-
|
|
428
|
+
const wantX = glClampTextureSize(mainCanvasSize.x), wantY = glClampTextureSize(mainCanvasSize.y);
|
|
429
|
+
if (lightSystem.textureSizeAuto && (size.x !== wantX || size.y !== wantY))
|
|
426
430
|
{
|
|
427
|
-
lightSystem.textureSize =
|
|
431
|
+
lightSystem.textureSize = vec2(wantX, wantY);
|
|
428
432
|
glContext.bindTexture(glContext.TEXTURE_2D, lightSystem.texture);
|
|
429
433
|
glContext.texImage2D(glContext.TEXTURE_2D, 0, glContext.RGBA,
|
|
430
|
-
|
|
434
|
+
wantX, wantY, 0,
|
|
431
435
|
glContext.RGBA, glContext.UNSIGNED_BYTE, null);
|
|
432
436
|
// put back the texture the engine tracks, a draw in renderLight must not sample the lightmap
|
|
433
437
|
if (glActiveTexture)
|
|
@@ -534,8 +538,14 @@ class LightSystemPlugin
|
|
|
534
538
|
// is, and any debug text / future draw could sample the lightmap)
|
|
535
539
|
if (glActiveTexture)
|
|
536
540
|
glContext.bindTexture(glContext.TEXTURE_2D, glActiveTexture);
|
|
537
|
-
setAdditiveBlendMode(prevAdditive);
|
|
538
541
|
glSetInstancedMode(true);
|
|
542
|
+
|
|
543
|
+
// 7. the lights' glows, added over the lit scene so the darkness does not dim them
|
|
544
|
+
setAdditiveBlendMode();
|
|
545
|
+
for (const o of engineObjects)
|
|
546
|
+
o instanceof Light && !o.destroyed && o.renderGlow();
|
|
547
|
+
glFlush();
|
|
548
|
+
setAdditiveBlendMode(prevAdditive);
|
|
539
549
|
}
|
|
540
550
|
function lightSystemContextLost()
|
|
541
551
|
{
|
|
@@ -554,6 +564,19 @@ class LightSystemPlugin
|
|
|
554
564
|
}
|
|
555
565
|
}
|
|
556
566
|
|
|
567
|
+
/** Bring the sizes of the textures it makes down to what the device can make: shadowMapSize, shadowTextureSize
|
|
568
|
+
* and a textureSize given by hand; called before they are made, so a size too big falls back instead of failing */
|
|
569
|
+
clampTextureSizes()
|
|
570
|
+
{
|
|
571
|
+
this.shadowMapSize = glClampTextureSize(this.shadowMapSize);
|
|
572
|
+
this.shadowTextureSize = glClampTextureSize(this.shadowTextureSize);
|
|
573
|
+
const size = this.textureSize;
|
|
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
|
|
578
|
+
}
|
|
579
|
+
|
|
557
580
|
/** Draw a single Light's falloff blob into the currently bound lightmap.
|
|
558
581
|
* Called by Light.renderLight() during the plugin's render pass.
|
|
559
582
|
* @param {Light} light */
|
|
@@ -703,6 +726,14 @@ class Light extends EngineObject
|
|
|
703
726
|
* or torch that holds it, or the player carrying it, does not block it; it has to reach past that object's
|
|
704
727
|
* corners, about half its diagonal and a little more, or dark rays run out from them */
|
|
705
728
|
this.shadowCore = 0;
|
|
729
|
+
/** @property {number} - Size across of a soft hazy glow drawn over the light, like a lamp at night, 0 for
|
|
730
|
+
* none; it is added over the lit scene, in front of everything there */
|
|
731
|
+
this.glow = 0;
|
|
732
|
+
/** @property {number} - How fast the glow fades from its middle: 1 by default, .5 a wide haze, 2 a tight
|
|
733
|
+
* bright core */
|
|
734
|
+
this.glowFalloff = 1;
|
|
735
|
+
/** @type {TileInfo|undefined} */
|
|
736
|
+
this.glowTileInfo = undefined; // the whole glow texture, kept for the falloff it was made for
|
|
706
737
|
}
|
|
707
738
|
|
|
708
739
|
/** Lights are invisible in the main render pass — they only contribute
|
|
@@ -716,4 +747,18 @@ class Light extends EngineObject
|
|
|
716
747
|
{
|
|
717
748
|
lightSystem && lightSystem.drawLight(this);
|
|
718
749
|
}
|
|
750
|
+
|
|
751
|
+
/** Draw this light's glow, soft and round, its glow size across and in its color, added over the lit scene;
|
|
752
|
+
* called by LightSystemPlugin after the lightmap is applied */
|
|
753
|
+
renderGlow()
|
|
754
|
+
{
|
|
755
|
+
if (!(this.glow > 0)) return;
|
|
756
|
+
const size = vec2(this.glow);
|
|
757
|
+
if (!isOnScreen(this.pos, size)) return;
|
|
758
|
+
const texture = engineGlowTexture(this.glowFalloff);
|
|
759
|
+
if (!texture) return;
|
|
760
|
+
if (this.glowTileInfo?.textureInfo !== texture)
|
|
761
|
+
this.glowTileInfo = new TileInfo(vec2(), texture.size, texture);
|
|
762
|
+
drawTile(this.pos, size, this.glowTileInfo, this.color, 0, false, undefined, true, false);
|
|
763
|
+
}
|
|
719
764
|
}
|
package/plugins/math3d.js
CHANGED
|
@@ -338,15 +338,7 @@ class Matrix4
|
|
|
338
338
|
static rotation(euler, matrix=new Matrix4)
|
|
339
339
|
{
|
|
340
340
|
ASSERT_VECTOR3_VALID(euler);
|
|
341
|
-
|
|
342
|
-
const cy = cos(euler.y), sy = sin(euler.y);
|
|
343
|
-
const cz = cos(euler.z), sz = sin(euler.z);
|
|
344
|
-
const m = matrix.m;
|
|
345
|
-
// R = Ry * Rx * Rz written out, column major, every element set so a reused matrix comes out clean
|
|
346
|
-
m[0] = cy*cz + sy*sx*sz; m[1] = cx*sz; m[2] = -sy*cz + cy*sx*sz; m[3] = 0;
|
|
347
|
-
m[4] = -cy*sz + sy*sx*cz; m[5] = cx*cz; m[6] = sy*sz + cy*sx*cz; m[7] = 0;
|
|
348
|
-
m[8] = sy*cx; m[9] = -sx; m[10] = cy*cx; m[11] = 0;
|
|
349
|
-
m[12] = m[13] = m[14] = 0; m[15] = 1;
|
|
341
|
+
matrix4RotationAt(matrix.m, 0, euler);
|
|
350
342
|
return matrix;
|
|
351
343
|
}
|
|
352
344
|
|
|
@@ -594,24 +586,54 @@ class Matrix4
|
|
|
594
586
|
* @memberof Math3D
|
|
595
587
|
*/
|
|
596
588
|
function buildMatrix(pos, rotation, scale, matrix=new Matrix4)
|
|
589
|
+
{
|
|
590
|
+
ASSERT(matrix instanceof Matrix4, 'the matrix to write into must be a Matrix4');
|
|
591
|
+
matrix4Compose(matrix.m, 0, pos, rotation, scale);
|
|
592
|
+
return matrix;
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
// the rotation matrix of euler angles written into 16 floats of an array at k
|
|
596
|
+
function matrix4RotationAt(m, k, euler)
|
|
597
|
+
{
|
|
598
|
+
const cx = cos(euler.x), sx = sin(euler.x);
|
|
599
|
+
const cy = cos(euler.y), sy = sin(euler.y);
|
|
600
|
+
const cz = cos(euler.z), sz = sin(euler.z);
|
|
601
|
+
// R = Ry * Rx * Rz written out, column major, every element set so a reused matrix comes out clean
|
|
602
|
+
m[k] = cy*cz + sy*sx*sz; m[k+1] = cx*sz; m[k+2] = -sy*cz + cy*sx*sz; m[k+3] = 0;
|
|
603
|
+
m[k+4] = -cy*sz + sy*sx*cz; m[k+5] = cx*cz; m[k+6] = sy*sz + cy*sx*cz; m[k+7] = 0;
|
|
604
|
+
m[k+8] = sy*cx; m[k+9] = -sx; m[k+10] = cy*cx; m[k+11] = 0;
|
|
605
|
+
m[k+12] = m[k+13] = m[k+14] = 0; m[k+15] = 1;
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
// the transform buildMatrix makes, written into 16 floats of an array at k, so an instance buffer takes it straight in
|
|
609
|
+
// the rotation columns are scaled and the position dropped in, instead of multiplying three matrices, each value
|
|
610
|
+
// worked out whole and stored once; InstancedMesh3D.setTransforms writes the same expressions for many at a time
|
|
611
|
+
function matrix4Compose(m, k, pos, rotation, scale)
|
|
597
612
|
{
|
|
598
613
|
ASSERT(!pos || isVector3(pos), 'pos must be a Vector3', pos);
|
|
599
614
|
ASSERT(!scale || isVector3(scale), 'scale must be a Vector3', scale);
|
|
600
|
-
|
|
601
|
-
// scale the rotation columns and drop the position in, instead of multiplying three matrices
|
|
615
|
+
const scaleX = scale ? scale.x : 1, scaleY = scale ? scale.y : 1, scaleZ = scale ? scale.z : 1;
|
|
602
616
|
// an object that is not turned at all is most of a big scene, and identity is what the six
|
|
603
617
|
// trig calls would have worked out to anyway
|
|
604
|
-
|
|
605
|
-
turned ? Matrix4.rotation(rotation, matrix) : m.set(matrix4Identity);
|
|
606
|
-
if (scale)
|
|
618
|
+
if (rotation && (rotation.x || rotation.y || rotation.z))
|
|
607
619
|
{
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
620
|
+
ASSERT_VECTOR3_VALID(rotation);
|
|
621
|
+
const cx = cos(rotation.x), sx = sin(rotation.x);
|
|
622
|
+
const cy = cos(rotation.y), sy = sin(rotation.y);
|
|
623
|
+
const cz = cos(rotation.z), sz = sin(rotation.z);
|
|
624
|
+
// R = Ry * Rx * Rz written out, column major, as matrix4RotationAt, each column times its scale
|
|
625
|
+
m[k] = (cy*cz + sy*sx*sz) * scaleX; m[k+1] = cx*sz * scaleX; m[k+2] = (-sy*cz + cy*sx*sz) * scaleX;
|
|
626
|
+
m[k+4] = (-cy*sz + sy*sx*cz) * scaleY; m[k+5] = cx*cz * scaleY; m[k+6] = (sy*sz + cy*sx*cz) * scaleY;
|
|
627
|
+
m[k+8] = sy*cx * scaleZ; m[k+9] = -sx * scaleZ; m[k+10] = cy*cx * scaleZ;
|
|
611
628
|
}
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
629
|
+
else
|
|
630
|
+
{
|
|
631
|
+
m[k] = scaleX; m[k+1] = 0; m[k+2] = 0;
|
|
632
|
+
m[k+4] = 0; m[k+5] = scaleY; m[k+6] = 0;
|
|
633
|
+
m[k+8] = 0; m[k+9] = 0; m[k+10] = scaleZ;
|
|
634
|
+
}
|
|
635
|
+
m[k+3] = m[k+7] = m[k+11] = 0; m[k+15] = 1;
|
|
636
|
+
m[k+12] = pos ? pos.x : 0, m[k+13] = pos ? pos.y : 0, m[k+14] = pos ? pos.z : 0;
|
|
615
637
|
}
|
|
616
638
|
|
|
617
639
|
///////////////////////////////////////////////////////////////////////////////
|
|
@@ -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
|
+
}
|