littlejsengine 1.18.2 → 1.18.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/COPYRIGHT.txt +38 -0
- package/FAQ.md +633 -0
- package/LICENSE +5 -27
- package/README.md +19 -1
- package/dist/littlejs.d.ts +550 -57
- package/dist/littlejs.esm.js +2208 -492
- package/dist/littlejs.esm.min.js +1 -1
- package/dist/littlejs.js +2165 -480
- package/dist/littlejs.min.js +1 -1
- package/dist/littlejs.release.js +2166 -481
- package/package.json +8 -1
- package/plugins/box2d.js +36 -9
- package/{src/engineMedals.js → plugins/medalSystem.js} +248 -196
- package/plugins/pathFinder.js +758 -0
- package/plugins/pluginExport.js +26 -0
- package/plugins/tween.js +509 -0
- package/plugins/tweenSystem.js +509 -0
- package/plugins/uiSystem.js +151 -12
- package/src/engine.js +9 -145
- package/src/engineAudio.js +8 -5
- package/src/engineBuild.mjs +4 -1
- package/src/engineDebug.js +1 -1
- package/src/engineDraw.js +154 -5
- package/src/engineExport.js +17 -12
- package/src/engineInput.js +27 -1
- package/src/engineLogo.js +146 -0
- package/src/engineMath.js +10 -3
- package/src/engineObject.js +11 -5
- package/src/engineParticles.js +6 -3
- package/src/engineRelease.js +1 -1
- package/src/engineSettings.js +17 -47
- package/src/engineUtilities.js +7 -2
- package/src/engineWebGL.js +31 -8
- package/src/jsconfig.json +3 -1
- package/.claude/settings.local.json +0 -7
- package/.github/workflows/test.yml +0 -17
- package/AI.md +0 -172
- package/CLAUDE.md +0 -1
- package/examples/box2d/game.js +0 -192
- package/examples/box2d/gameObjects.js +0 -564
- package/examples/box2d/index.html +0 -9
- package/examples/box2d/scenes.js +0 -194
- package/examples/box2d/tiles.png +0 -0
- package/examples/breakout/game.js +0 -175
- package/examples/breakout/gameObjects.js +0 -147
- package/examples/breakout/index.html +0 -8
- package/examples/breakout/tiles.png +0 -0
- package/examples/breakoutTutorial/README.md +0 -521
- package/examples/breakoutTutorial/game.js +0 -191
- package/examples/breakoutTutorial/images/1.png +0 -0
- package/examples/breakoutTutorial/images/10.png +0 -0
- package/examples/breakoutTutorial/images/11.png +0 -0
- package/examples/breakoutTutorial/images/2.png +0 -0
- package/examples/breakoutTutorial/images/3.png +0 -0
- package/examples/breakoutTutorial/images/4.png +0 -0
- package/examples/breakoutTutorial/images/5.png +0 -0
- package/examples/breakoutTutorial/images/6.png +0 -0
- package/examples/breakoutTutorial/images/7.png +0 -0
- package/examples/breakoutTutorial/images/8.png +0 -0
- package/examples/breakoutTutorial/images/9.png +0 -0
- package/examples/breakoutTutorial/index.html +0 -8
- package/examples/electron/build.mjs +0 -127
- package/examples/electron/electron.js +0 -35
- package/examples/electron/game.js +0 -54
- package/examples/electron/index.html +0 -13
- package/examples/electron/package.json +0 -5
- package/examples/electron/tiles.png +0 -0
- package/examples/empty/game.js +0 -51
- package/examples/empty/index.html +0 -5
- package/examples/empty/tiles.png +0 -0
- package/examples/favicon.png +0 -0
- package/examples/games.jpg +0 -0
- package/examples/htmlMenu/game.js +0 -82
- package/examples/htmlMenu/index.html +0 -45
- package/examples/htmlMenu/tiles.png +0 -0
- package/examples/index.html +0 -800
- package/examples/logo.png +0 -0
- package/examples/logo2.png +0 -0
- package/examples/module/build.mjs +0 -124
- package/examples/module/game.js +0 -132
- package/examples/module/index.html +0 -10
- package/examples/module/tiles.png +0 -0
- package/examples/particles/index.html +0 -426
- package/examples/particles/tiles.png +0 -0
- package/examples/platformer/data/gameLevelData.tmx +0 -143
- package/examples/platformer/data/gameLevelData.tsx +0 -4
- package/examples/platformer/game.js +0 -147
- package/examples/platformer/gameCharacter.js +0 -309
- package/examples/platformer/gameEffects.js +0 -278
- package/examples/platformer/gameLevel.js +0 -205
- package/examples/platformer/gameLevelData.json +0 -171
- package/examples/platformer/gameObjects.js +0 -378
- package/examples/platformer/gamePlayer.js +0 -35
- package/examples/platformer/index.html +0 -9
- package/examples/platformer/tiles.png +0 -0
- package/examples/platformer/tilesLevel.png +0 -0
- package/examples/puzzle/game.js +0 -331
- package/examples/puzzle/index.html +0 -8
- package/examples/puzzle/tiles.png +0 -0
- package/examples/screenshot.jpg +0 -0
- package/examples/shorts/animation.js +0 -18
- package/examples/shorts/base.html +0 -54
- package/examples/shorts/blending.js +0 -14
- package/examples/shorts/box2d.js +0 -51
- package/examples/shorts/box2dCar.js +0 -59
- package/examples/shorts/box2dPool.js +0 -107
- package/examples/shorts/box2dTileLayer.js +0 -48
- package/examples/shorts/cameraDrag.js +0 -22
- package/examples/shorts/clock.js +0 -21
- package/examples/shorts/colors.js +0 -25
- package/examples/shorts/debugDraw.js +0 -37
- package/examples/shorts/empty.js +0 -30
- package/examples/shorts/flappyGame.js +0 -55
- package/examples/shorts/fontImage.js +0 -17
- package/examples/shorts/fps.js +0 -90
- package/examples/shorts/helloWorld.js +0 -11
- package/examples/shorts/hillGlideGame.js +0 -63
- package/examples/shorts/input.js +0 -64
- package/examples/shorts/landerGame.js +0 -57
- package/examples/shorts/maze.js +0 -48
- package/examples/shorts/medals.js +0 -51
- package/examples/shorts/music.js +0 -78
- package/examples/shorts/musicPlayer.js +0 -137
- package/examples/shorts/nineSlice.js +0 -40
- package/examples/shorts/parallax.js +0 -72
- package/examples/shorts/particles.js +0 -28
- package/examples/shorts/piano.js +0 -44
- package/examples/shorts/platformer.js +0 -45
- package/examples/shorts/pongGame.js +0 -41
- package/examples/shorts/postProcess.js +0 -62
- package/examples/shorts/sequencer.js +0 -124
- package/examples/shorts/shader.js +0 -29
- package/examples/shorts/shapes.js +0 -21
- package/examples/shorts/slidingPuzzle.js +0 -52
- package/examples/shorts/song.mp3 +0 -0
- package/examples/shorts/sound.js +0 -36
- package/examples/shorts/spaceGame.js +0 -58
- package/examples/shorts/spriteAtlas.js +0 -31
- package/examples/shorts/starfield.js +0 -15
- package/examples/shorts/texture.js +0 -16
- package/examples/shorts/tileLayer.js +0 -48
- package/examples/shorts/tileRaycast.js +0 -39
- package/examples/shorts/tiles.png +0 -0
- package/examples/shorts/tiltedView.js +0 -63
- package/examples/shorts/timers.js +0 -53
- package/examples/shorts/topDown.js +0 -38
- package/examples/shorts/uiSystem.js +0 -61
- package/examples/shorts/video.webm +0 -0
- package/examples/shorts/videoPlayer.js +0 -34
- package/examples/starter/build.bat +0 -7
- package/examples/starter/build.mjs +0 -126
- package/examples/starter/game.js +0 -137
- package/examples/starter/index.html +0 -35
- package/examples/starter/tiles.png +0 -0
- package/examples/stress/index.html +0 -173
- package/examples/style.css +0 -150
- package/examples/typescript/build.mjs +0 -60
- package/examples/typescript/game.js +0 -100
- package/examples/typescript/game.ts +0 -132
- package/examples/typescript/index.html +0 -10
- package/examples/typescript/tiles.png +0 -0
- package/examples/typescript/tsconfig.json +0 -17
- package/examples/uiSystem/game.js +0 -139
- package/examples/uiSystem/index.html +0 -10
- package/examples/uiSystem/tiles.png +0 -0
- package/jsconfig.json +0 -12
- package/plugins/desktop.ini +0 -2
- package/reference.md +0 -447
- package/test/math.test.mjs +0 -774
- package/test/setup.mjs +0 -22
- package/test/smoke.test.mjs +0 -258
- package/test/util.test.mjs +0 -80
package/test/math.test.mjs
DELETED
|
@@ -1,774 +0,0 @@
|
|
|
1
|
-
import { test } from 'node:test';
|
|
2
|
-
import assert from 'node:assert/strict';
|
|
3
|
-
import {
|
|
4
|
-
PI, clamp, percent, percentLerp, lerp, mod, smoothStep, nearestPowerOfTwo, oscillate,
|
|
5
|
-
distanceWrap, lerpWrap, distanceAngle, lerpAngle,
|
|
6
|
-
isOverlapping, isIntersecting, lineTest,
|
|
7
|
-
vec2, Vector2, rgb, hsl, Color,
|
|
8
|
-
RandomGenerator,
|
|
9
|
-
rand, randInt, randBool, randSign, randVec2, randInCircle, randColor,
|
|
10
|
-
} from '../dist/littlejs.esm.js';
|
|
11
|
-
|
|
12
|
-
// small tolerance for floating point comparisons
|
|
13
|
-
const EPS = 1e-9;
|
|
14
|
-
const near = (a, b, eps=EPS) => Math.abs(a - b) <= eps;
|
|
15
|
-
|
|
16
|
-
test('clamp', () =>
|
|
17
|
-
{
|
|
18
|
-
assert.equal(clamp(0.5), 0.5);
|
|
19
|
-
assert.equal(clamp(-1), 0);
|
|
20
|
-
assert.equal(clamp(2), 1);
|
|
21
|
-
assert.equal(clamp(5, 0, 10), 5);
|
|
22
|
-
assert.equal(clamp(-5, 0, 10), 0);
|
|
23
|
-
assert.equal(clamp(15, 0, 10), 10);
|
|
24
|
-
});
|
|
25
|
-
|
|
26
|
-
test('percent', () =>
|
|
27
|
-
{
|
|
28
|
-
assert.equal(percent(5, 0, 10), 0.5);
|
|
29
|
-
assert.equal(percent(0, 0, 10), 0);
|
|
30
|
-
assert.equal(percent(10, 0, 10), 1);
|
|
31
|
-
assert.equal(percent(-5, 0, 10), 0); // clamped
|
|
32
|
-
assert.equal(percent(15, 0, 10), 1); // clamped
|
|
33
|
-
assert.equal(percent(5, 5, 5), 0); // division by zero guard
|
|
34
|
-
});
|
|
35
|
-
|
|
36
|
-
test('lerp', () =>
|
|
37
|
-
{
|
|
38
|
-
assert.equal(lerp(0, 10, 0), 0);
|
|
39
|
-
assert.equal(lerp(0, 10, 1), 10);
|
|
40
|
-
assert.equal(lerp(0, 10, 0.5), 5);
|
|
41
|
-
assert.equal(lerp(0, 10, -1), 0); // percent clamped
|
|
42
|
-
assert.equal(lerp(0, 10, 2), 10); // percent clamped
|
|
43
|
-
});
|
|
44
|
-
|
|
45
|
-
test('percentLerp', () =>
|
|
46
|
-
{
|
|
47
|
-
// equivalent to lerp(lerpA, lerpB, percent(value, percentA, percentB))
|
|
48
|
-
assert.equal(percentLerp(0, 0, 10, 100, 200), 100); // at percentA
|
|
49
|
-
assert.equal(percentLerp(10, 0, 10, 100, 200), 200); // at percentB
|
|
50
|
-
assert.equal(percentLerp(5, 0, 10, 100, 200), 150); // midpoint
|
|
51
|
-
assert.equal(percentLerp(-5, 0, 10, 100, 200), 100); // below range, clamped
|
|
52
|
-
assert.equal(percentLerp(99, 0, 10, 100, 200), 200); // above range, clamped
|
|
53
|
-
});
|
|
54
|
-
|
|
55
|
-
test('mod', () =>
|
|
56
|
-
{
|
|
57
|
-
assert.equal(mod(5, 3), 2);
|
|
58
|
-
assert.equal(mod(-1, 3), 2); // negative handled
|
|
59
|
-
assert.equal(mod(-4, 3), 2);
|
|
60
|
-
assert.equal(mod(0, 3), 0);
|
|
61
|
-
assert.equal(mod(0.5), 0.5); // default divisor = 1
|
|
62
|
-
});
|
|
63
|
-
|
|
64
|
-
test('smoothStep', () =>
|
|
65
|
-
{
|
|
66
|
-
assert.equal(smoothStep(0), 0);
|
|
67
|
-
assert.equal(smoothStep(1), 1);
|
|
68
|
-
assert.equal(smoothStep(0.5), 0.5);
|
|
69
|
-
assert(smoothStep(0.25) < 0.25); // slower start
|
|
70
|
-
assert(smoothStep(0.75) > 0.75); // faster end (relative to linear)
|
|
71
|
-
});
|
|
72
|
-
|
|
73
|
-
test('nearestPowerOfTwo', () =>
|
|
74
|
-
{
|
|
75
|
-
assert.equal(nearestPowerOfTwo(1), 1);
|
|
76
|
-
assert.equal(nearestPowerOfTwo(3), 4);
|
|
77
|
-
assert.equal(nearestPowerOfTwo(100), 128);
|
|
78
|
-
assert.equal(nearestPowerOfTwo(16), 16);
|
|
79
|
-
});
|
|
80
|
-
|
|
81
|
-
test('distanceWrap / lerpWrap', () =>
|
|
82
|
-
{
|
|
83
|
-
assert(near(distanceWrap(0.1, 0.9), 0.2)); // shortest path wraps forward
|
|
84
|
-
assert(near(distanceWrap(0.9, 0.1), -0.2));
|
|
85
|
-
assert(near(distanceWrap(0, 0), 0));
|
|
86
|
-
// lerpWrap returns the un-wrapped interpolated value: 0.9 + 0.5*0.2 = 1.0
|
|
87
|
-
// (1.0 is equivalent to 0 modulo wrapSize — caller can re-wrap if needed)
|
|
88
|
-
assert(near(lerpWrap(0.9, 0.1, 0.5), 1.0));
|
|
89
|
-
});
|
|
90
|
-
|
|
91
|
-
test('distanceAngle / lerpAngle', () =>
|
|
92
|
-
{
|
|
93
|
-
assert(near(distanceAngle(0, 2*PI), 0)); // same direction after wrap
|
|
94
|
-
assert(near(distanceAngle(0.1, -0.1), 0.2));
|
|
95
|
-
// lerpAngle picks shortest path but returns un-wrapped value.
|
|
96
|
-
// from -PI+0.1 to PI-0.1: shortest path is backward (through -PI), distance = -0.2
|
|
97
|
-
// so result = (-PI+0.1) + 0.5*(-0.2) = -PI
|
|
98
|
-
assert(near(lerpAngle(-PI+0.1, PI-0.1, 0.5), -PI, 1e-6));
|
|
99
|
-
});
|
|
100
|
-
|
|
101
|
-
test('isOverlapping AABB', () =>
|
|
102
|
-
{
|
|
103
|
-
// two unit boxes centered at origin overlap
|
|
104
|
-
assert.equal(isOverlapping(vec2(0, 0), vec2(1, 1), vec2(0, 0), vec2(1, 1)), true);
|
|
105
|
-
// adjacent boxes do NOT overlap (edge case, uses strict inequality on one side)
|
|
106
|
-
assert.equal(isOverlapping(vec2(0, 0), vec2(1, 1), vec2(2, 0), vec2(1, 1)), false);
|
|
107
|
-
// partial overlap
|
|
108
|
-
assert.equal(isOverlapping(vec2(0, 0), vec2(2, 2), vec2(1, 1), vec2(2, 2)), true);
|
|
109
|
-
// far apart
|
|
110
|
-
assert.equal(isOverlapping(vec2(0, 0), vec2(1, 1), vec2(10, 10), vec2(1, 1)), false);
|
|
111
|
-
});
|
|
112
|
-
|
|
113
|
-
test('isIntersecting line-vs-AABB', () =>
|
|
114
|
-
{
|
|
115
|
-
// line passes through box
|
|
116
|
-
assert.equal(isIntersecting(vec2(-5, 0), vec2(5, 0), vec2(0, 0), vec2(2, 2)), true);
|
|
117
|
-
// line misses box
|
|
118
|
-
assert.equal(isIntersecting(vec2(-5, 10), vec2(5, 10), vec2(0, 0), vec2(2, 2)), false);
|
|
119
|
-
// line starts inside
|
|
120
|
-
assert.equal(isIntersecting(vec2(0, 0), vec2(10, 0), vec2(0, 0), vec2(2, 2)), true);
|
|
121
|
-
});
|
|
122
|
-
|
|
123
|
-
///////////////////////////////////////////////////////////////////////////////
|
|
124
|
-
// lineTest (grid-DDA raycast)
|
|
125
|
-
|
|
126
|
-
test('lineTest returns undefined when nothing hits', () =>
|
|
127
|
-
{
|
|
128
|
-
const hit = lineTest(vec2(0.5, 0.5), vec2(10.5, 0.5), () => false);
|
|
129
|
-
assert.equal(hit, undefined);
|
|
130
|
-
});
|
|
131
|
-
|
|
132
|
-
test('lineTest returns undefined for zero-length line', () =>
|
|
133
|
-
{
|
|
134
|
-
// early return: totalLength is 0
|
|
135
|
-
const hit = lineTest(vec2(5, 5), vec2(5, 5), () => true);
|
|
136
|
-
assert.equal(hit, undefined);
|
|
137
|
-
});
|
|
138
|
-
|
|
139
|
-
test('lineTest finds first hit along horizontal ray', () =>
|
|
140
|
-
{
|
|
141
|
-
// ray across cells (0,0) -> (10,0) at y=0.5, predicate hits at x=3
|
|
142
|
-
const hit = lineTest(vec2(0.5, 0.5), vec2(10.5, 0.5), pos => pos.x === 3);
|
|
143
|
-
assert(hit !== undefined);
|
|
144
|
-
assert.equal(Math.floor(hit.x), 3); // hit is inside cell 3
|
|
145
|
-
assert.equal(Math.floor(hit.y), 0);
|
|
146
|
-
});
|
|
147
|
-
|
|
148
|
-
test('lineTest stops at the first hit, not a later one', () =>
|
|
149
|
-
{
|
|
150
|
-
const visited = [];
|
|
151
|
-
const hit = lineTest(vec2(0.5, 0.5), vec2(10.5, 0.5), pos =>
|
|
152
|
-
{
|
|
153
|
-
visited.push(pos.x);
|
|
154
|
-
return pos.x === 2 || pos.x === 5;
|
|
155
|
-
});
|
|
156
|
-
assert(hit !== undefined);
|
|
157
|
-
assert.equal(Math.floor(hit.x), 2); // earlier hit wins
|
|
158
|
-
// ray never advances past cell 2, and stops *on* cell 2 (not just never-visits-5)
|
|
159
|
-
assert(!visited.some(x => x > 2));
|
|
160
|
-
assert.equal(visited.at(-1), 2);
|
|
161
|
-
});
|
|
162
|
-
|
|
163
|
-
test('lineTest writes normal for horizontal hit', () =>
|
|
164
|
-
{
|
|
165
|
-
// ray moves +x, hits on the left face of the hit cell -> normal (-1, 0)
|
|
166
|
-
const normal = vec2();
|
|
167
|
-
const hit = lineTest(vec2(0.5, 0.5), vec2(10.5, 0.5), pos => pos.x === 3, normal);
|
|
168
|
-
assert(hit !== undefined);
|
|
169
|
-
assert.equal(normal.x, -1);
|
|
170
|
-
assert.equal(normal.y, 0);
|
|
171
|
-
});
|
|
172
|
-
|
|
173
|
-
test('lineTest writes normal for vertical hit (downward ray)', () =>
|
|
174
|
-
{
|
|
175
|
-
// ray moves -y, last step was in y, stepY = -1 -> normal (0, -stepY) = (0, 1)
|
|
176
|
-
const normal = vec2();
|
|
177
|
-
const hit = lineTest(vec2(0.5, 0.5), vec2(0.5, -10.5), pos => pos.y === -3, normal);
|
|
178
|
-
assert(hit !== undefined);
|
|
179
|
-
assert.equal(Math.floor(hit.y), -3);
|
|
180
|
-
assert.equal(normal.x, 0);
|
|
181
|
-
assert.equal(normal.y, 1);
|
|
182
|
-
});
|
|
183
|
-
|
|
184
|
-
test('lineTest returns immediately if start cell hits', () =>
|
|
185
|
-
{
|
|
186
|
-
let calls = 0;
|
|
187
|
-
const hit = lineTest(vec2(2.5, 2.5), vec2(10, 10), pos =>
|
|
188
|
-
{
|
|
189
|
-
calls++;
|
|
190
|
-
return pos.x === 2 && pos.y === 2;
|
|
191
|
-
});
|
|
192
|
-
assert(hit !== undefined);
|
|
193
|
-
assert.equal(calls, 1); // only checked the start cell
|
|
194
|
-
assert.equal(Math.floor(hit.x), 2);
|
|
195
|
-
assert.equal(Math.floor(hit.y), 2);
|
|
196
|
-
});
|
|
197
|
-
|
|
198
|
-
test('vec2 constructor and factory', () =>
|
|
199
|
-
{
|
|
200
|
-
let v = vec2();
|
|
201
|
-
assert.equal(v.x, 0); assert.equal(v.y, 0);
|
|
202
|
-
v = vec2(5);
|
|
203
|
-
assert.equal(v.x, 5); assert.equal(v.y, 5); // single arg copies x to y
|
|
204
|
-
v = vec2(3, 4);
|
|
205
|
-
assert.equal(v.x, 3); assert.equal(v.y, 4);
|
|
206
|
-
v = new Vector2();
|
|
207
|
-
assert.equal(v.x, 0); assert.equal(v.y, 0);
|
|
208
|
-
});
|
|
209
|
-
|
|
210
|
-
test('Vector2 arithmetic', () =>
|
|
211
|
-
{
|
|
212
|
-
const a = vec2(2, 3);
|
|
213
|
-
const b = vec2(4, 5);
|
|
214
|
-
assert.deepEqual({ x: a.add(b).x, y: a.add(b).y }, { x: 6, y: 8 });
|
|
215
|
-
assert.deepEqual({ x: a.subtract(b).x, y: a.subtract(b).y }, { x: -2, y: -2 });
|
|
216
|
-
assert.deepEqual({ x: a.multiply(b).x, y: a.multiply(b).y }, { x: 8, y: 15 });
|
|
217
|
-
assert.deepEqual({ x: a.scale(3).x, y: a.scale(3).y }, { x: 6, y: 9 });
|
|
218
|
-
// immutability: a should be unchanged
|
|
219
|
-
assert.equal(a.x, 2); assert.equal(a.y, 3);
|
|
220
|
-
});
|
|
221
|
-
|
|
222
|
-
test('Vector2 length / distance / dot / cross', () =>
|
|
223
|
-
{
|
|
224
|
-
const v = vec2(3, 4);
|
|
225
|
-
assert.equal(v.length(), 5);
|
|
226
|
-
assert.equal(v.lengthSquared(), 25);
|
|
227
|
-
assert.equal(v.distance(vec2(0, 0)), 5);
|
|
228
|
-
assert.equal(v.distanceSquared(vec2(0, 0)), 25);
|
|
229
|
-
assert.equal(vec2(1, 0).dot(vec2(1, 0)), 1);
|
|
230
|
-
assert.equal(vec2(1, 0).dot(vec2(0, 1)), 0);
|
|
231
|
-
assert.equal(vec2(1, 0).cross(vec2(0, 1)), 1);
|
|
232
|
-
});
|
|
233
|
-
|
|
234
|
-
test('Vector2 normalize', () =>
|
|
235
|
-
{
|
|
236
|
-
const n = vec2(3, 4).normalize();
|
|
237
|
-
assert(near(n.length(), 1));
|
|
238
|
-
const scaled = vec2(3, 4).normalize(10);
|
|
239
|
-
assert(near(scaled.length(), 10));
|
|
240
|
-
// zero vector returns (0, length) — the up-direction at requested length.
|
|
241
|
-
// Pin both components, not just length (which would pass for any unit vector).
|
|
242
|
-
const zero = vec2(0, 0).normalize();
|
|
243
|
-
assert.equal(zero.x, 0); assert.equal(zero.y, 1);
|
|
244
|
-
const zero5 = vec2(0, 0).normalize(5);
|
|
245
|
-
assert.equal(zero5.x, 0); assert.equal(zero5.y, 5);
|
|
246
|
-
});
|
|
247
|
-
|
|
248
|
-
test('Vector2 angle conventions (up = 0)', () =>
|
|
249
|
-
{
|
|
250
|
-
// angle() returns atan2(x, y), so up (+y) is 0
|
|
251
|
-
assert(near(vec2(0, 1).angle(), 0));
|
|
252
|
-
assert(near(vec2(1, 0).angle(), PI/2));
|
|
253
|
-
// setAngle: x = length*sin(angle), y = length*cos(angle)
|
|
254
|
-
const v = new Vector2().setAngle(0, 5);
|
|
255
|
-
assert(near(v.x, 0)); assert(near(v.y, 5));
|
|
256
|
-
const w = new Vector2().setAngle(PI/2, 3);
|
|
257
|
-
assert(near(w.x, 3)); assert(near(w.y, 0));
|
|
258
|
-
});
|
|
259
|
-
|
|
260
|
-
test('Vector2 rotate preserves length', () =>
|
|
261
|
-
{
|
|
262
|
-
const v = vec2(1, 0);
|
|
263
|
-
const r = v.rotate(PI/2);
|
|
264
|
-
assert(near(r.length(), 1));
|
|
265
|
-
// rotate by 2pi returns roughly the original
|
|
266
|
-
const full = v.rotate(2*PI);
|
|
267
|
-
assert(near(full.x, 1)); assert(near(full.y, 0));
|
|
268
|
-
});
|
|
269
|
-
|
|
270
|
-
test('Vector2 floor / abs / mod / area', () =>
|
|
271
|
-
{
|
|
272
|
-
assert.equal(vec2(1.7, -2.3).floor().x, 1);
|
|
273
|
-
assert.equal(vec2(1.7, -2.3).floor().y, -3);
|
|
274
|
-
assert.equal(vec2(-1.5, 2.5).abs().x, 1.5);
|
|
275
|
-
assert.equal(vec2(-1.5, 2.5).abs().y, 2.5);
|
|
276
|
-
const m = vec2(5.5, -0.5).mod(1);
|
|
277
|
-
assert(near(m.x, 0.5));
|
|
278
|
-
assert(near(m.y, 0.5));
|
|
279
|
-
assert.equal(vec2(3, 4).area(), 12);
|
|
280
|
-
});
|
|
281
|
-
|
|
282
|
-
test('Vector2 lerp', () =>
|
|
283
|
-
{
|
|
284
|
-
const r = vec2(0, 0).lerp(vec2(10, 20), 0.5);
|
|
285
|
-
assert.equal(r.x, 5); assert.equal(r.y, 10);
|
|
286
|
-
const zero = vec2(0, 0).lerp(vec2(10, 20), 0);
|
|
287
|
-
assert.equal(zero.x, 0); assert.equal(zero.y, 0);
|
|
288
|
-
});
|
|
289
|
-
|
|
290
|
-
test('Color factory + defaults', () =>
|
|
291
|
-
{
|
|
292
|
-
const w = rgb();
|
|
293
|
-
assert.equal(w.r, 1); assert.equal(w.g, 1); assert.equal(w.b, 1); assert.equal(w.a, 1);
|
|
294
|
-
const red = rgb(1, 0, 0);
|
|
295
|
-
assert.equal(red.r, 1); assert.equal(red.g, 0); assert.equal(red.b, 0); assert.equal(red.a, 1);
|
|
296
|
-
});
|
|
297
|
-
|
|
298
|
-
test('Color arithmetic', () =>
|
|
299
|
-
{
|
|
300
|
-
const a = new Color(0.5, 0.5, 0.5, 1);
|
|
301
|
-
const b = new Color(0.25, 0.25, 0.25, 0);
|
|
302
|
-
const sum = a.add(b);
|
|
303
|
-
assert.equal(sum.r, 0.75);
|
|
304
|
-
assert.equal(sum.a, 1);
|
|
305
|
-
const scaled = a.scale(2);
|
|
306
|
-
assert.equal(scaled.r, 1);
|
|
307
|
-
assert.equal(scaled.a, 2); // alpha also scaled
|
|
308
|
-
const scaled2 = a.scale(2, 0.5);
|
|
309
|
-
assert.equal(scaled2.r, 1);
|
|
310
|
-
assert.equal(scaled2.a, 0.5); // alpha scaled separately
|
|
311
|
-
});
|
|
312
|
-
|
|
313
|
-
test('Color lerp', () =>
|
|
314
|
-
{
|
|
315
|
-
const a = new Color(0, 0, 0, 1);
|
|
316
|
-
const b = new Color(1, 1, 1, 1);
|
|
317
|
-
const mid = a.lerp(b, 0.5);
|
|
318
|
-
assert.equal(mid.r, 0.5); assert.equal(mid.g, 0.5); assert.equal(mid.b, 0.5);
|
|
319
|
-
});
|
|
320
|
-
|
|
321
|
-
test('Color hex round-trip', () =>
|
|
322
|
-
{
|
|
323
|
-
const red = new Color().setHex('#ff0000');
|
|
324
|
-
assert(near(red.r, 1)); assert(near(red.g, 0)); assert(near(red.b, 0));
|
|
325
|
-
const short = new Color().setHex('#f00');
|
|
326
|
-
assert(near(short.r, 1)); assert(near(short.g, 0)); assert(near(short.b, 0));
|
|
327
|
-
// toString format is locked: lowercase hex, always 6 or 8 chars plus '#'.
|
|
328
|
-
// If a future change switches to uppercase, update both the function and these tests.
|
|
329
|
-
assert.equal(rgb(1, 0, 0).toString(false), '#ff0000');
|
|
330
|
-
assert.equal(rgb(0, 0, 0).toString(false), '#000000');
|
|
331
|
-
});
|
|
332
|
-
|
|
333
|
-
test('Color rgb↔hsl round-trip', () =>
|
|
334
|
-
{
|
|
335
|
-
const red = rgb(1, 0, 0);
|
|
336
|
-
const [h, s, l, a] = red.HSLA();
|
|
337
|
-
const back = hsl(h, s, l, a);
|
|
338
|
-
assert(near(back.r, 1, 1e-6));
|
|
339
|
-
assert(near(back.g, 0, 1e-6));
|
|
340
|
-
assert(near(back.b, 0, 1e-6));
|
|
341
|
-
});
|
|
342
|
-
|
|
343
|
-
test('Color clamp', () =>
|
|
344
|
-
{
|
|
345
|
-
const clamped = new Color(2, -1, 0.5, 1.5).clamp();
|
|
346
|
-
assert.equal(clamped.r, 1);
|
|
347
|
-
assert.equal(clamped.g, 0);
|
|
348
|
-
assert.equal(clamped.b, 0.5);
|
|
349
|
-
assert.equal(clamped.a, 1);
|
|
350
|
-
});
|
|
351
|
-
|
|
352
|
-
test('RandomGenerator determinism', () =>
|
|
353
|
-
{
|
|
354
|
-
const r1 = new RandomGenerator(12345);
|
|
355
|
-
const r2 = new RandomGenerator(12345);
|
|
356
|
-
for (let i = 0; i < 10; i++)
|
|
357
|
-
assert.equal(r1.float(), r2.float());
|
|
358
|
-
|
|
359
|
-
// different seed => different sequence
|
|
360
|
-
const r3 = new RandomGenerator(54321);
|
|
361
|
-
const a = new RandomGenerator(12345).float();
|
|
362
|
-
const b = r3.float();
|
|
363
|
-
assert.notEqual(a, b);
|
|
364
|
-
});
|
|
365
|
-
|
|
366
|
-
test('RandomGenerator range bounds', () =>
|
|
367
|
-
{
|
|
368
|
-
const r = new RandomGenerator(42);
|
|
369
|
-
for (let i = 0; i < 100; i++)
|
|
370
|
-
{
|
|
371
|
-
const x = r.float();
|
|
372
|
-
assert(x >= 0 && x < 1);
|
|
373
|
-
}
|
|
374
|
-
for (let i = 0; i < 100; i++)
|
|
375
|
-
{
|
|
376
|
-
const n = r.int(10);
|
|
377
|
-
assert(n >= 0 && n < 10);
|
|
378
|
-
assert.equal(Math.floor(n), n);
|
|
379
|
-
}
|
|
380
|
-
});
|
|
381
|
-
|
|
382
|
-
test('rand / randInt bounds', () =>
|
|
383
|
-
{
|
|
384
|
-
// rand() => [0, 1) — valueB(0) + Math.random()*(valueA(1)-valueB(0))
|
|
385
|
-
for (let i = 0; i < 100; i++)
|
|
386
|
-
{
|
|
387
|
-
const x = rand();
|
|
388
|
-
assert(x >= 0 && x < 1);
|
|
389
|
-
}
|
|
390
|
-
// rand(valueA, valueB) returns valueB + rand()*(valueA - valueB).
|
|
391
|
-
// Inclusivity flips with arg order:
|
|
392
|
-
// rand(10, 5) = 5 + r*5 => [5, 10) -- valueB inclusive, valueA exclusive
|
|
393
|
-
// rand(5, 10) = 10 + r*-5 => (5, 10] -- valueA exclusive, valueB inclusive
|
|
394
|
-
for (let i = 0; i < 100; i++)
|
|
395
|
-
{
|
|
396
|
-
const y = rand(10, 5);
|
|
397
|
-
assert(y >= 5 && y < 10);
|
|
398
|
-
const z = rand(5, 10);
|
|
399
|
-
assert(z > 5 && z <= 10);
|
|
400
|
-
}
|
|
401
|
-
for (let i = 0; i < 100; i++)
|
|
402
|
-
{
|
|
403
|
-
const n = randInt(4);
|
|
404
|
-
assert([0, 1, 2, 3].includes(n));
|
|
405
|
-
}
|
|
406
|
-
});
|
|
407
|
-
|
|
408
|
-
test('randBool distribution', () =>
|
|
409
|
-
{
|
|
410
|
-
let trues = 0;
|
|
411
|
-
for (let i = 0; i < 200; i++)
|
|
412
|
-
if (randBool()) trues++;
|
|
413
|
-
// Fair coin: mean=100, stddev≈7.07 for Binomial(200, 0.5).
|
|
414
|
-
// Window [60, 140] is ~5.6σ from the mean — false positive rate ~1e-8,
|
|
415
|
-
// but will still catch a function that's meaningfully biased.
|
|
416
|
-
assert(trues >= 60 && trues <= 140,
|
|
417
|
-
`randBool produced ${trues}/200 trues — outside 60..140 window`);
|
|
418
|
-
// chance=1 always true, chance=0 always false
|
|
419
|
-
for (let i = 0; i < 20; i++)
|
|
420
|
-
{
|
|
421
|
-
assert.equal(randBool(1), true);
|
|
422
|
-
assert.equal(randBool(0), false);
|
|
423
|
-
}
|
|
424
|
-
});
|
|
425
|
-
|
|
426
|
-
test('randSign returns exactly -1 or 1', () =>
|
|
427
|
-
{
|
|
428
|
-
const signs = new Set();
|
|
429
|
-
for (let i = 0; i < 100; i++)
|
|
430
|
-
{
|
|
431
|
-
const s = randSign();
|
|
432
|
-
assert(s === -1 || s === 1);
|
|
433
|
-
signs.add(s);
|
|
434
|
-
}
|
|
435
|
-
assert.deepEqual([...signs].sort(), [-1, 1]);
|
|
436
|
-
});
|
|
437
|
-
|
|
438
|
-
test('randVec2 length', () =>
|
|
439
|
-
{
|
|
440
|
-
// default length 1
|
|
441
|
-
for (let i = 0; i < 50; i++)
|
|
442
|
-
assert(near(randVec2().length(), 1, 1e-9));
|
|
443
|
-
// custom length
|
|
444
|
-
for (let i = 0; i < 50; i++)
|
|
445
|
-
assert(near(randVec2(7).length(), 7, 1e-9));
|
|
446
|
-
});
|
|
447
|
-
|
|
448
|
-
test('randInCircle stays within radius', () =>
|
|
449
|
-
{
|
|
450
|
-
for (let i = 0; i < 200; i++)
|
|
451
|
-
{
|
|
452
|
-
const v = randInCircle(3);
|
|
453
|
-
assert(v.length() <= 3 + EPS);
|
|
454
|
-
}
|
|
455
|
-
// radius 0 returns origin
|
|
456
|
-
const zero = randInCircle(0);
|
|
457
|
-
assert.equal(zero.x, 0); assert.equal(zero.y, 0);
|
|
458
|
-
});
|
|
459
|
-
|
|
460
|
-
test('randColor returns a valid Color', () =>
|
|
461
|
-
{
|
|
462
|
-
for (let i = 0; i < 20; i++)
|
|
463
|
-
{
|
|
464
|
-
const c = randColor();
|
|
465
|
-
assert(c instanceof Color);
|
|
466
|
-
assert(c.isValid());
|
|
467
|
-
}
|
|
468
|
-
// with explicit bounds, each channel stays between the two colors
|
|
469
|
-
const a = rgb(0.2, 0.2, 0.2, 1);
|
|
470
|
-
const b = rgb(0.8, 0.8, 0.8, 1);
|
|
471
|
-
for (let i = 0; i < 50; i++)
|
|
472
|
-
{
|
|
473
|
-
const c = randColor(a, b);
|
|
474
|
-
assert(c.r >= 0.2 && c.r <= 0.8);
|
|
475
|
-
assert(c.g >= 0.2 && c.g <= 0.8);
|
|
476
|
-
assert(c.b >= 0.2 && c.b <= 0.8);
|
|
477
|
-
}
|
|
478
|
-
});
|
|
479
|
-
|
|
480
|
-
///////////////////////////////////////////////////////////////////////////////
|
|
481
|
-
// oscillate
|
|
482
|
-
|
|
483
|
-
test('oscillate sine (default)', () =>
|
|
484
|
-
{
|
|
485
|
-
// sine: value = -cos(phase * 2pi), output = amp/2 * (value + 1) ∈ [0, amp]
|
|
486
|
-
// phase=0 -> value=-1 -> 0
|
|
487
|
-
// phase=0.5 -> value=1 -> amp
|
|
488
|
-
// phase=0.25 -> value=0 -> amp/2
|
|
489
|
-
assert(near(oscillate(1, 2, 0), 0));
|
|
490
|
-
assert(near(oscillate(1, 2, 0.5), 2));
|
|
491
|
-
assert(near(oscillate(1, 2, 0.25), 1));
|
|
492
|
-
// must stay in [0, amplitude]
|
|
493
|
-
for (let i = 0; i <= 20; i++)
|
|
494
|
-
{
|
|
495
|
-
const v = oscillate(1, 1, i/20);
|
|
496
|
-
assert(v >= 0 - EPS && v <= 1 + EPS);
|
|
497
|
-
}
|
|
498
|
-
});
|
|
499
|
-
|
|
500
|
-
test('oscillate triangle', () =>
|
|
501
|
-
{
|
|
502
|
-
// triangle (type 1) at phase=0 -> peaks at amp; phase=0.5 -> 0
|
|
503
|
-
assert(near(oscillate(1, 1, 0, 0, 1), 1));
|
|
504
|
-
assert(near(oscillate(1, 1, 0.5, 0, 1), 0));
|
|
505
|
-
// must stay in [0, amplitude]
|
|
506
|
-
for (let i = 0; i <= 20; i++)
|
|
507
|
-
{
|
|
508
|
-
const v = oscillate(1, 1, i/20, 0, 1);
|
|
509
|
-
assert(v >= 0 - EPS && v <= 1 + EPS);
|
|
510
|
-
}
|
|
511
|
-
});
|
|
512
|
-
|
|
513
|
-
test('oscillate square', () =>
|
|
514
|
-
{
|
|
515
|
-
// square (type 2): -1 for phase<0.5, +1 for phase>=0.5
|
|
516
|
-
// so output 0 for phase<0.5, amp for phase>=0.5
|
|
517
|
-
assert.equal(oscillate(1, 1, 0, 0, 2), 0);
|
|
518
|
-
assert.equal(oscillate(1, 1, 0.25, 0, 2), 0);
|
|
519
|
-
assert.equal(oscillate(1, 1, 0.5, 0, 2), 1);
|
|
520
|
-
assert.equal(oscillate(1, 1, 0.9, 0, 2), 1);
|
|
521
|
-
});
|
|
522
|
-
|
|
523
|
-
test('oscillate sawtooth', () =>
|
|
524
|
-
{
|
|
525
|
-
// sawtooth (type 3): value = 2*phase - 1, output = amp/2*(value+1) = amp*phase
|
|
526
|
-
assert(near(oscillate(1, 1, 0, 0, 3), 0));
|
|
527
|
-
assert(near(oscillate(1, 1, 0.5, 0, 3), 0.5));
|
|
528
|
-
// phase wraps at 1, so exactly at phase=1 we get 0 again via mod
|
|
529
|
-
assert(near(oscillate(1, 1, 1, 0, 3), 0));
|
|
530
|
-
});
|
|
531
|
-
|
|
532
|
-
///////////////////////////////////////////////////////////////////////////////
|
|
533
|
-
// Vector2 gaps
|
|
534
|
-
|
|
535
|
-
test('Vector2 copy / setFrom immutability', () =>
|
|
536
|
-
{
|
|
537
|
-
const a = vec2(3, 4);
|
|
538
|
-
const b = a.copy();
|
|
539
|
-
b.x = 99;
|
|
540
|
-
assert.equal(a.x, 3); // original untouched
|
|
541
|
-
const c = new Vector2().setFrom(a);
|
|
542
|
-
assert.equal(c.x, 3); assert.equal(c.y, 4);
|
|
543
|
-
});
|
|
544
|
-
|
|
545
|
-
test('Vector2 divide', () =>
|
|
546
|
-
{
|
|
547
|
-
const r = vec2(6, 8).divide(vec2(2, 4));
|
|
548
|
-
assert.equal(r.x, 3); assert.equal(r.y, 2);
|
|
549
|
-
});
|
|
550
|
-
|
|
551
|
-
test('Vector2 clampLength', () =>
|
|
552
|
-
{
|
|
553
|
-
// already within limit: returns a copy at same length
|
|
554
|
-
const a = vec2(3, 4).clampLength(10);
|
|
555
|
-
assert.equal(a.x, 3); assert.equal(a.y, 4);
|
|
556
|
-
// exceeds limit: scales down to limit
|
|
557
|
-
const b = vec2(3, 4).clampLength(1);
|
|
558
|
-
assert(near(b.length(), 1));
|
|
559
|
-
});
|
|
560
|
-
|
|
561
|
-
test('Vector2 reflect', () =>
|
|
562
|
-
{
|
|
563
|
-
// (1, -1) hitting a floor (normal 0,1) should reflect to (1, 1)
|
|
564
|
-
const r = vec2(1, -1).reflect(vec2(0, 1), 1);
|
|
565
|
-
assert(near(r.x, 1)); assert(near(r.y, 1));
|
|
566
|
-
// restitution 0.5 halves the bounce
|
|
567
|
-
const half = vec2(0, -2).reflect(vec2(0, 1), 0.5);
|
|
568
|
-
assert(near(half.y, 1)); // -2 -> +1, not +2
|
|
569
|
-
// restitution 0 absorbs the normal component
|
|
570
|
-
const absorbed = vec2(1, -1).reflect(vec2(0, 1), 0);
|
|
571
|
-
assert(near(absorbed.x, 1));
|
|
572
|
-
assert(near(absorbed.y, 0));
|
|
573
|
-
});
|
|
574
|
-
|
|
575
|
-
test('Vector2 setDirection / direction (cardinal 0..3)', () =>
|
|
576
|
-
{
|
|
577
|
-
// setDirection: 0=up(+y), 1=right(+x), 2=down(-y), 3=left(-x)
|
|
578
|
-
const up = new Vector2().setDirection(0, 1);
|
|
579
|
-
assert(near(up.x, 0)); assert(near(up.y, 1));
|
|
580
|
-
const right = new Vector2().setDirection(1, 1);
|
|
581
|
-
assert(near(right.x, 1)); assert(near(right.y, 0));
|
|
582
|
-
const down = new Vector2().setDirection(2, 1);
|
|
583
|
-
assert(near(down.x, 0)); assert(near(down.y, -1));
|
|
584
|
-
const left = new Vector2().setDirection(3, 1);
|
|
585
|
-
assert(near(left.x, -1)); assert(near(left.y, 0));
|
|
586
|
-
// direction() is the inverse for clean cardinal vectors
|
|
587
|
-
assert.equal(vec2(0, 1).direction(), 0);
|
|
588
|
-
assert.equal(vec2(1, 0).direction(), 1);
|
|
589
|
-
assert.equal(vec2(0, -1).direction(), 2);
|
|
590
|
-
assert.equal(vec2(-1, 0).direction(), 3);
|
|
591
|
-
});
|
|
592
|
-
|
|
593
|
-
test('Vector2 snap', () =>
|
|
594
|
-
{
|
|
595
|
-
// snap divides by the grid, floors, multiplies back
|
|
596
|
-
// snap(2): bucket size 0.5 (1/2). 1.3 -> floor(1.3*2)/2 = floor(2.6)/2 = 2/2 = 1.0
|
|
597
|
-
const r = vec2(1.3, 1.9).snap(2);
|
|
598
|
-
assert(near(r.x, 1.0));
|
|
599
|
-
assert(near(r.y, 1.5));
|
|
600
|
-
});
|
|
601
|
-
|
|
602
|
-
test('Vector2 arrayCheck (tile-grid bounds)', () =>
|
|
603
|
-
{
|
|
604
|
-
const size = vec2(10, 5);
|
|
605
|
-
assert.equal(vec2(0, 0).arrayCheck(size), true);
|
|
606
|
-
assert.equal(vec2(9, 4).arrayCheck(size), true);
|
|
607
|
-
assert.equal(vec2(10, 4).arrayCheck(size), false); // x out (exclusive upper)
|
|
608
|
-
assert.equal(vec2(9, 5).arrayCheck(size), false); // y out (exclusive upper)
|
|
609
|
-
assert.equal(vec2(-1, 0).arrayCheck(size), false); // x negative
|
|
610
|
-
assert.equal(vec2(0, -1).arrayCheck(size), false); // y negative
|
|
611
|
-
});
|
|
612
|
-
|
|
613
|
-
test('Vector2 isValid', () =>
|
|
614
|
-
{
|
|
615
|
-
assert.equal(vec2(0, 0).isValid(), true);
|
|
616
|
-
assert.equal(vec2(-100, 3.14).isValid(), true);
|
|
617
|
-
const bad = vec2(1, 2);
|
|
618
|
-
bad.x = NaN;
|
|
619
|
-
assert.equal(bad.isValid(), false);
|
|
620
|
-
bad.x = 1; bad.y = NaN;
|
|
621
|
-
assert.equal(bad.isValid(), false);
|
|
622
|
-
});
|
|
623
|
-
|
|
624
|
-
///////////////////////////////////////////////////////////////////////////////
|
|
625
|
-
// Color gaps
|
|
626
|
-
|
|
627
|
-
test('Color copy / setFrom / set', () =>
|
|
628
|
-
{
|
|
629
|
-
const a = new Color(0.25, 0.5, 0.75, 0.5);
|
|
630
|
-
const b = a.copy();
|
|
631
|
-
b.r = 0;
|
|
632
|
-
assert.equal(a.r, 0.25); // original untouched
|
|
633
|
-
const c = new Color().setFrom(a);
|
|
634
|
-
assert.equal(c.r, 0.25); assert.equal(c.a, 0.5);
|
|
635
|
-
c.set(1, 0, 0, 1);
|
|
636
|
-
assert.equal(c.r, 1); assert.equal(c.g, 0);
|
|
637
|
-
});
|
|
638
|
-
|
|
639
|
-
test('Color divide', () =>
|
|
640
|
-
{
|
|
641
|
-
const r = new Color(1, 0.5, 0.25, 1).divide(new Color(2, 1, 0.5, 1));
|
|
642
|
-
assert.equal(r.r, 0.5); assert.equal(r.g, 0.5); assert.equal(r.b, 0.5); assert.equal(r.a, 1);
|
|
643
|
-
});
|
|
644
|
-
|
|
645
|
-
test('Color rgbaInt for simple cases', () =>
|
|
646
|
-
{
|
|
647
|
-
// packed layout: R in lowest byte, then G, B, A
|
|
648
|
-
assert.equal(rgb(0, 0, 0, 0).rgbaInt(), 0);
|
|
649
|
-
assert.equal(rgb(1, 0, 0, 0).rgbaInt(), 255);
|
|
650
|
-
assert.equal(rgb(0, 1, 0, 0).rgbaInt(), 255 << 8);
|
|
651
|
-
assert.equal(rgb(0, 0, 1, 0).rgbaInt(), 255 << 16);
|
|
652
|
-
});
|
|
653
|
-
|
|
654
|
-
test('Color HSLA() at corner hues', () =>
|
|
655
|
-
{
|
|
656
|
-
// pure primaries have saturation 1 and lightness 0.5
|
|
657
|
-
const [hR, sR, lR] = rgb(1, 0, 0).HSLA();
|
|
658
|
-
assert(near(hR, 0)); assert(near(sR, 1)); assert(near(lR, 0.5));
|
|
659
|
-
const [hG, sG, lG] = rgb(0, 1, 0).HSLA();
|
|
660
|
-
assert(near(hG, 1/3)); assert(near(sG, 1)); assert(near(lG, 0.5));
|
|
661
|
-
const [hB, sB, lB] = rgb(0, 0, 1).HSLA();
|
|
662
|
-
assert(near(hB, 2/3)); assert(near(sB, 1)); assert(near(lB, 0.5));
|
|
663
|
-
// grayscale: saturation 0, hue irrelevant (reports 0)
|
|
664
|
-
const [, sBlack, lBlack] = rgb(0, 0, 0).HSLA();
|
|
665
|
-
assert.equal(sBlack, 0); assert.equal(lBlack, 0);
|
|
666
|
-
const [, sWhite, lWhite] = rgb(1, 1, 1).HSLA();
|
|
667
|
-
assert.equal(sWhite, 0); assert.equal(lWhite, 1);
|
|
668
|
-
});
|
|
669
|
-
|
|
670
|
-
test('Color isValid', () =>
|
|
671
|
-
{
|
|
672
|
-
assert.equal(new Color(0.5, 0.5, 0.5, 1).isValid(), true);
|
|
673
|
-
const bad = new Color(0, 0, 0, 1);
|
|
674
|
-
bad.r = NaN;
|
|
675
|
-
assert.equal(bad.isValid(), false);
|
|
676
|
-
});
|
|
677
|
-
|
|
678
|
-
test('Color mutate bounds', () =>
|
|
679
|
-
{
|
|
680
|
-
// mutate uses Math.random directly so we can't seed it — test shape only.
|
|
681
|
-
const base = rgb(0.5, 0.5, 0.5, 0.5);
|
|
682
|
-
for (let i = 0; i < 50; i++)
|
|
683
|
-
{
|
|
684
|
-
const m = base.mutate(0.1, 0.05);
|
|
685
|
-
// all channels stay in [0, 1] (mutate calls .clamp() at the end)
|
|
686
|
-
assert(m.r >= 0 && m.r <= 1);
|
|
687
|
-
assert(m.g >= 0 && m.g <= 1);
|
|
688
|
-
assert(m.b >= 0 && m.b <= 1);
|
|
689
|
-
assert(m.a >= 0 && m.a <= 1);
|
|
690
|
-
// each channel is within `amount` of the base (before clamp, but base+amount stays in range here)
|
|
691
|
-
assert(Math.abs(m.r - 0.5) <= 0.1 + EPS);
|
|
692
|
-
assert(Math.abs(m.g - 0.5) <= 0.1 + EPS);
|
|
693
|
-
assert(Math.abs(m.b - 0.5) <= 0.1 + EPS);
|
|
694
|
-
assert(Math.abs(m.a - 0.5) <= 0.05 + EPS);
|
|
695
|
-
}
|
|
696
|
-
// alphaAmount defaults to 0: alpha unchanged
|
|
697
|
-
const m = base.mutate(0.1);
|
|
698
|
-
assert.equal(m.a, 0.5);
|
|
699
|
-
});
|
|
700
|
-
|
|
701
|
-
///////////////////////////////////////////////////////////////////////////////
|
|
702
|
-
// RandomGenerator methods
|
|
703
|
-
|
|
704
|
-
test('RandomGenerator bool / sign', () =>
|
|
705
|
-
{
|
|
706
|
-
const r = new RandomGenerator(1);
|
|
707
|
-
let trues = 0, signs = new Set();
|
|
708
|
-
for (let i = 0; i < 100; i++)
|
|
709
|
-
{
|
|
710
|
-
if (r.bool()) trues++;
|
|
711
|
-
signs.add(r.sign());
|
|
712
|
-
}
|
|
713
|
-
// chance=0.5 default — expect neither 0 nor 100 trues on 100 draws
|
|
714
|
-
assert(trues > 20 && trues < 80);
|
|
715
|
-
assert.deepEqual([...signs].sort(), [-1, 1]);
|
|
716
|
-
});
|
|
717
|
-
|
|
718
|
-
test('RandomGenerator floatSign range and sign', () =>
|
|
719
|
-
{
|
|
720
|
-
const r = new RandomGenerator(5);
|
|
721
|
-
const signs = new Set();
|
|
722
|
-
for (let i = 0; i < 100; i++)
|
|
723
|
-
{
|
|
724
|
-
const v = r.floatSign(2, 1);
|
|
725
|
-
// magnitude is in [1, 2), signed either way
|
|
726
|
-
assert(Math.abs(v) >= 1 && Math.abs(v) < 2);
|
|
727
|
-
signs.add(Math.sign(v));
|
|
728
|
-
}
|
|
729
|
-
assert.deepEqual([...signs].sort(), [-1, 1]);
|
|
730
|
-
});
|
|
731
|
-
|
|
732
|
-
test('RandomGenerator angle range', () =>
|
|
733
|
-
{
|
|
734
|
-
const r = new RandomGenerator(7);
|
|
735
|
-
for (let i = 0; i < 100; i++)
|
|
736
|
-
{
|
|
737
|
-
const a = r.angle();
|
|
738
|
-
assert(a >= -PI && a < PI);
|
|
739
|
-
}
|
|
740
|
-
});
|
|
741
|
-
|
|
742
|
-
test('RandomGenerator vec2', () =>
|
|
743
|
-
{
|
|
744
|
-
const r = new RandomGenerator(9);
|
|
745
|
-
const v = r.vec2(5);
|
|
746
|
-
assert(v instanceof Vector2);
|
|
747
|
-
assert(v.x >= 0 && v.x < 5);
|
|
748
|
-
assert(v.y >= 0 && v.y < 5);
|
|
749
|
-
});
|
|
750
|
-
|
|
751
|
-
test('RandomGenerator randColor determinism', () =>
|
|
752
|
-
{
|
|
753
|
-
const a = new RandomGenerator(123).randColor();
|
|
754
|
-
const b = new RandomGenerator(123).randColor();
|
|
755
|
-
assert.equal(a.r, b.r);
|
|
756
|
-
assert.equal(a.g, b.g);
|
|
757
|
-
assert.equal(a.b, b.b);
|
|
758
|
-
assert.equal(a.a, b.a);
|
|
759
|
-
});
|
|
760
|
-
|
|
761
|
-
test('RandomGenerator mutateColor', () =>
|
|
762
|
-
{
|
|
763
|
-
const base = rgb(0.5, 0.5, 0.5, 1);
|
|
764
|
-
const r = new RandomGenerator(42);
|
|
765
|
-
const m = r.mutateColor(base, 0.1, 0);
|
|
766
|
-
// result clamped to [0,1] and each channel within amount of base (0.1)
|
|
767
|
-
assert(m.r >= 0 && m.r <= 1);
|
|
768
|
-
assert(m.g >= 0 && m.g <= 1);
|
|
769
|
-
assert(m.b >= 0 && m.b <= 1);
|
|
770
|
-
assert.equal(m.a, 1); // alphaAmount=0, so alpha unchanged
|
|
771
|
-
assert(Math.abs(m.r - 0.5) <= 0.1 + EPS);
|
|
772
|
-
assert(Math.abs(m.g - 0.5) <= 0.1 + EPS);
|
|
773
|
-
assert(Math.abs(m.b - 0.5) <= 0.1 + EPS);
|
|
774
|
-
});
|