littlejsengine 1.15.9 → 1.16.2
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/dist/littlejs.d.ts +154 -158
- package/dist/littlejs.esm.js +573 -570
- package/dist/littlejs.esm.min.js +1 -1
- package/dist/littlejs.js +553 -553
- package/dist/littlejs.min.js +1 -1
- package/dist/littlejs.release.js +392 -396
- package/examples/box2d/game.js +1 -1
- package/examples/box2d/gameObjects.js +5 -5
- package/examples/breakout/game.js +3 -3
- package/examples/electron/game.js +1 -2
- package/examples/electron/index.html +2 -2
- package/examples/htmlMenu/game.js +0 -1
- package/examples/index.html +1 -1
- package/examples/logo.png +0 -0
- package/examples/logo2.png +0 -0
- package/examples/module/game.js +1 -2
- package/examples/particles/index.html +1 -1
- package/examples/platformer/game.js +4 -4
- package/examples/platformer/gameEffects.js +1 -1
- package/examples/puzzle/game.js +2 -2
- package/examples/shorts/base.html +4 -3
- package/examples/shorts/box2dPool.js +2 -2
- package/examples/shorts/empty.js +1 -1
- package/examples/shorts/{systemFont.js → fontImage.js} +3 -3
- package/examples/shorts/fps.js +1 -1
- package/examples/shorts/helloWorld.js +2 -2
- package/examples/shorts/nineSlice.js +2 -2
- package/examples/shorts/slidingPuzzle.js +1 -1
- package/examples/starter/game.js +1 -2
- package/examples/starter/index.html +3 -2
- package/examples/stress/index.html +1 -1
- package/examples/typescript/game.js +1 -2
- package/examples/typescript/game.ts +1 -2
- package/package.json +1 -1
- package/plugins/box2d.js +8 -8
- package/plugins/drawUtilities.js +6 -6
- package/plugins/postProcess.js +13 -20
- package/plugins/uiSystem.js +16 -4
- package/reference.md +9 -11
- package/src/engine.js +40 -54
- package/src/engineAudio.js +5 -7
- package/src/engineBuild.js +1 -0
- package/src/engineDebug.js +163 -161
- package/src/engineDraw.js +108 -130
- package/src/engineExport.js +20 -17
- package/src/engineInput.js +4 -7
- package/src/engineMath.js +1201 -0
- package/src/engineMedals.js +7 -7
- package/src/engineObject.js +5 -4
- package/src/engineRelease.js +2 -4
- package/src/engineSettings.js +22 -32
- package/src/engineTileLayer.js +9 -10
- package/src/engineUtilities.js +34 -1187
- package/src/engineWebGL.js +13 -15
|
@@ -0,0 +1,1201 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* LittleJS Utility Classes and Functions
|
|
3
|
+
* - General purpose math library
|
|
4
|
+
* - Vector2 - fast, simple, easy 2D vector class
|
|
5
|
+
* - Color - holds a rgba color with some math functions
|
|
6
|
+
* - Timer - tracks time automatically
|
|
7
|
+
* - RandomGenerator - seeded random number generator
|
|
8
|
+
* @namespace Math
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
'use strict';
|
|
12
|
+
|
|
13
|
+
/** The value of PI
|
|
14
|
+
* @type {number}
|
|
15
|
+
* @default Math.PI
|
|
16
|
+
* @memberof Math */
|
|
17
|
+
const PI = Math.PI;
|
|
18
|
+
|
|
19
|
+
/** Returns absolute value of value passed in
|
|
20
|
+
* @param {number} value
|
|
21
|
+
* @return {number}
|
|
22
|
+
* @memberof Math */
|
|
23
|
+
const abs = Math.abs;
|
|
24
|
+
|
|
25
|
+
/** Returns floored value of value passed in
|
|
26
|
+
* @param {number} value
|
|
27
|
+
* @return {number}
|
|
28
|
+
* @memberof Math */
|
|
29
|
+
const floor = Math.floor;
|
|
30
|
+
|
|
31
|
+
/** Returns ceiled value of value passed in
|
|
32
|
+
* @param {number} value
|
|
33
|
+
* @return {number}
|
|
34
|
+
* @memberof Math */
|
|
35
|
+
const ceil = Math.ceil;
|
|
36
|
+
|
|
37
|
+
/** Returns rounded value passed in
|
|
38
|
+
* @param {number} value
|
|
39
|
+
* @return {number}
|
|
40
|
+
* @memberof Math */
|
|
41
|
+
const round = Math.round;
|
|
42
|
+
|
|
43
|
+
/** Returns lowest value passed in
|
|
44
|
+
* @param {...number} values
|
|
45
|
+
* @return {number}
|
|
46
|
+
* @memberof Math */
|
|
47
|
+
const min = Math.min;
|
|
48
|
+
|
|
49
|
+
/** Returns highest value passed in
|
|
50
|
+
* @param {...number} values
|
|
51
|
+
* @return {number}
|
|
52
|
+
* @memberof Math */
|
|
53
|
+
const max = Math.max;
|
|
54
|
+
|
|
55
|
+
/** Returns the sign of value passed in
|
|
56
|
+
* @param {number} value
|
|
57
|
+
* @return {number}
|
|
58
|
+
* @memberof Math */
|
|
59
|
+
const sign = Math.sign;
|
|
60
|
+
|
|
61
|
+
/** Returns hypotenuse of values passed in
|
|
62
|
+
* @param {...number} values
|
|
63
|
+
* @return {number}
|
|
64
|
+
* @memberof Math */
|
|
65
|
+
const hypot = Math.hypot;
|
|
66
|
+
|
|
67
|
+
/** Returns log2 of value passed in
|
|
68
|
+
* @param {number} value
|
|
69
|
+
* @return {number}
|
|
70
|
+
* @memberof Math */
|
|
71
|
+
const log2 = Math.log2;
|
|
72
|
+
|
|
73
|
+
/** Returns sin of value passed in
|
|
74
|
+
* @param {number} value
|
|
75
|
+
* @return {number}
|
|
76
|
+
* @memberof Math */
|
|
77
|
+
const sin = Math.sin;
|
|
78
|
+
|
|
79
|
+
/** Returns cos of value passed in
|
|
80
|
+
* @param {number} value
|
|
81
|
+
* @return {number}
|
|
82
|
+
* @memberof Math */
|
|
83
|
+
const cos = Math.cos;
|
|
84
|
+
|
|
85
|
+
/** Returns tan of value passed in
|
|
86
|
+
* @param {number} value
|
|
87
|
+
* @return {number}
|
|
88
|
+
* @memberof Math */
|
|
89
|
+
const tan = Math.tan;
|
|
90
|
+
|
|
91
|
+
/** Returns atan2 of values passed in
|
|
92
|
+
* @param {number} y
|
|
93
|
+
* @param {number} x
|
|
94
|
+
* @return {number}
|
|
95
|
+
* @memberof Math */
|
|
96
|
+
const atan2 = Math.atan2;
|
|
97
|
+
|
|
98
|
+
/** Returns first parm modulo the second param, but adjusted so negative numbers work as expected
|
|
99
|
+
* @param {number} dividend
|
|
100
|
+
* @param {number} [divisor]
|
|
101
|
+
* @return {number}
|
|
102
|
+
* @memberof Math */
|
|
103
|
+
function mod(dividend, divisor=1) { return ((dividend % divisor) + divisor) % divisor; }
|
|
104
|
+
|
|
105
|
+
/** Clamps the value between max and min
|
|
106
|
+
* @param {number} value
|
|
107
|
+
* @param {number} [min]
|
|
108
|
+
* @param {number} [max]
|
|
109
|
+
* @return {number}
|
|
110
|
+
* @memberof Math */
|
|
111
|
+
function clamp(value, min=0, max=1) { return value < min ? min : value > max ? max : value; }
|
|
112
|
+
|
|
113
|
+
/** Returns what percentage the value is between valueA and valueB
|
|
114
|
+
* @param {number} value
|
|
115
|
+
* @param {number} valueA
|
|
116
|
+
* @param {number} valueB
|
|
117
|
+
* @return {number}
|
|
118
|
+
* @memberof Math */
|
|
119
|
+
function percent(value, valueA, valueB)
|
|
120
|
+
{ return (valueB-=valueA) ? clamp((value-valueA)/valueB) : 0; }
|
|
121
|
+
|
|
122
|
+
/** Linearly interpolates between values passed in using percent
|
|
123
|
+
* @param {number} valueA
|
|
124
|
+
* @param {number} valueB
|
|
125
|
+
* @param {number} percent
|
|
126
|
+
* @return {number}
|
|
127
|
+
* @memberof Math */
|
|
128
|
+
function lerp(valueA, valueB, percent)
|
|
129
|
+
{ return valueA + clamp(percent) * (valueB-valueA); }
|
|
130
|
+
|
|
131
|
+
/** Gets percent between percentA and percentB and linearly interpolates between lerpA and lerpB
|
|
132
|
+
* A shortcut for lerp(lerpA, lerpB, percent(value, percentA, percentB))
|
|
133
|
+
* @param {number} value
|
|
134
|
+
* @param {number} percentA
|
|
135
|
+
* @param {number} percentB
|
|
136
|
+
* @param {number} lerpA
|
|
137
|
+
* @param {number} lerpB
|
|
138
|
+
* @return {number}
|
|
139
|
+
* @memberof Math */
|
|
140
|
+
function percentLerp(value, percentA, percentB, lerpA, lerpB)
|
|
141
|
+
{ return lerp(lerpA, lerpB, percent(value, percentA, percentB)); }
|
|
142
|
+
|
|
143
|
+
/** Returns signed wrapped distance between the two values passed in
|
|
144
|
+
* @param {number} valueA
|
|
145
|
+
* @param {number} valueB
|
|
146
|
+
* @param {number} [wrapSize]
|
|
147
|
+
* @return {number}
|
|
148
|
+
* @memberof Math */
|
|
149
|
+
function distanceWrap(valueA, valueB, wrapSize=1)
|
|
150
|
+
{ const d = (valueA - valueB) % wrapSize; return d*2 % wrapSize - d; }
|
|
151
|
+
|
|
152
|
+
/** Linearly interpolates between values passed in with wrapping
|
|
153
|
+
* @param {number} valueA
|
|
154
|
+
* @param {number} valueB
|
|
155
|
+
* @param {number} percent
|
|
156
|
+
* @param {number} [wrapSize]
|
|
157
|
+
* @return {number}
|
|
158
|
+
* @memberof Math */
|
|
159
|
+
function lerpWrap(valueA, valueB, percent, wrapSize=1)
|
|
160
|
+
{ return valueA + clamp(percent) * distanceWrap(valueB, valueA, wrapSize); }
|
|
161
|
+
|
|
162
|
+
/** Returns signed wrapped distance between the two angles passed in
|
|
163
|
+
* @param {number} angleA
|
|
164
|
+
* @param {number} angleB
|
|
165
|
+
* @return {number}
|
|
166
|
+
* @memberof Math */
|
|
167
|
+
function distanceAngle(angleA, angleB) { return distanceWrap(angleA, angleB, 2*PI); }
|
|
168
|
+
|
|
169
|
+
/** Linearly interpolates between the angles passed in with wrapping
|
|
170
|
+
* @param {number} angleA
|
|
171
|
+
* @param {number} angleB
|
|
172
|
+
* @param {number} percent
|
|
173
|
+
* @return {number}
|
|
174
|
+
* @memberof Math */
|
|
175
|
+
function lerpAngle(angleA, angleB, percent) { return lerpWrap(angleA, angleB, percent, 2*PI); }
|
|
176
|
+
|
|
177
|
+
/** Applies smoothstep function to the percentage value
|
|
178
|
+
* @param {number} percent
|
|
179
|
+
* @return {number}
|
|
180
|
+
* @memberof Math */
|
|
181
|
+
function smoothStep(percent) { return percent * percent * (3 - 2 * percent); }
|
|
182
|
+
|
|
183
|
+
/** Checks if the value passed in is a power of two
|
|
184
|
+
* @param {number} value
|
|
185
|
+
* @return {boolean}
|
|
186
|
+
* @memberof Math */
|
|
187
|
+
function isPowerOfTwo(value) { return !(value & (value - 1)); }
|
|
188
|
+
|
|
189
|
+
/** Returns the nearest power of two not less than the value
|
|
190
|
+
* @param {number} value
|
|
191
|
+
* @return {number}
|
|
192
|
+
* @memberof Math */
|
|
193
|
+
function nearestPowerOfTwo(value) { return 2**ceil(log2(value)); }
|
|
194
|
+
|
|
195
|
+
/** Returns true if two axis aligned bounding boxes are overlapping
|
|
196
|
+
* this can be used for simple collision detection between objects
|
|
197
|
+
* @param {Vector2} posA - Center of box A
|
|
198
|
+
* @param {Vector2} sizeA - Size of box A
|
|
199
|
+
* @param {Vector2} posB - Center of box B
|
|
200
|
+
* @param {Vector2} [sizeB=(0,0)] - Size of box B, uses a point if undefined
|
|
201
|
+
* @return {boolean} - True if overlapping
|
|
202
|
+
* @memberof Math */
|
|
203
|
+
function isOverlapping(posA, sizeA, posB, sizeB=vec2())
|
|
204
|
+
{
|
|
205
|
+
const dx = (posA.x - posB.x)*2;
|
|
206
|
+
const dy = (posA.y - posB.y)*2;
|
|
207
|
+
const sx = sizeA.x + sizeB.x;
|
|
208
|
+
const sy = sizeA.y + sizeB.y;
|
|
209
|
+
return dx >= -sx && dx < sx && dy >= -sy && dy < sy;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
/** Returns true if a line segment is intersecting an axis aligned box
|
|
213
|
+
* @param {Vector2} start - Start of raycast
|
|
214
|
+
* @param {Vector2} end - End of raycast
|
|
215
|
+
* @param {Vector2} pos - Center of box
|
|
216
|
+
* @param {Vector2} size - Size of box
|
|
217
|
+
* @return {boolean} - True if intersecting
|
|
218
|
+
* @memberof Math */
|
|
219
|
+
function isIntersecting(start, end, pos, size)
|
|
220
|
+
{
|
|
221
|
+
// Liang-Barsky algorithm
|
|
222
|
+
const boxMin = pos.subtract(size.scale(.5));
|
|
223
|
+
const boxMax = boxMin.add(size);
|
|
224
|
+
const delta = end.subtract(start);
|
|
225
|
+
const a = start.subtract(boxMin);
|
|
226
|
+
const b = start.subtract(boxMax);
|
|
227
|
+
const p = [-delta.x, delta.x, -delta.y, delta.y];
|
|
228
|
+
const q = [a.x, -b.x, a.y, -b.y];
|
|
229
|
+
let tMin = 0, tMax = 1;
|
|
230
|
+
for (let i = 4; i--;)
|
|
231
|
+
{
|
|
232
|
+
if (p[i])
|
|
233
|
+
{
|
|
234
|
+
const t = q[i] / p[i];
|
|
235
|
+
if (p[i] < 0)
|
|
236
|
+
{
|
|
237
|
+
if (t > tMax) return false;
|
|
238
|
+
tMin = max(t, tMin);
|
|
239
|
+
}
|
|
240
|
+
else
|
|
241
|
+
{
|
|
242
|
+
if (t < tMin) return false;
|
|
243
|
+
tMax = min(t, tMax);
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
else if (q[i] < 0)
|
|
247
|
+
return false;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
return true;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
/** Returns an oscillating wave between 0 and amplitude with frequency of 1 Hz by default
|
|
254
|
+
* @param {number} [frequency] - Frequency of the wave in Hz
|
|
255
|
+
* @param {number} [amplitude] - Amplitude (max height) of the wave
|
|
256
|
+
* @param {number} [t=time] - Value to use for time of the wave
|
|
257
|
+
* @param {number} [offset] - Value to use for time offset of the wave
|
|
258
|
+
* @return {number} - Value waving between 0 and amplitude
|
|
259
|
+
* @memberof Math */
|
|
260
|
+
function wave(frequency=1, amplitude=1, t=time, offset=0)
|
|
261
|
+
{ return amplitude/2 * (1 - cos(offset + t*frequency*2*PI)); }
|
|
262
|
+
|
|
263
|
+
/**
|
|
264
|
+
* Check if object is a valid number, not NaN or undefined, but it may be infinite
|
|
265
|
+
* @param {any} n
|
|
266
|
+
* @return {boolean}
|
|
267
|
+
* @memberof Math */
|
|
268
|
+
function isNumber(n) { return typeof n === 'number' && !isNaN(n); }
|
|
269
|
+
|
|
270
|
+
/**
|
|
271
|
+
* Check if object is a valid string or can be converted to one
|
|
272
|
+
* @param {any} s
|
|
273
|
+
* @return {boolean}
|
|
274
|
+
* @memberof Math */
|
|
275
|
+
function isString(s) { return s !== undefined && s !== null && typeof s.toString() === 'string'; }
|
|
276
|
+
|
|
277
|
+
/**
|
|
278
|
+
* Check if object is an array
|
|
279
|
+
* @param {any} a
|
|
280
|
+
* @return {boolean}
|
|
281
|
+
* @memberof Math */
|
|
282
|
+
function isArray(a) { return Array.isArray(a); }
|
|
283
|
+
|
|
284
|
+
/**
|
|
285
|
+
* @callback LineTestFunction - Checks if a position is colliding
|
|
286
|
+
* @param {Vector2} pos
|
|
287
|
+
* @memberof Draw
|
|
288
|
+
*/
|
|
289
|
+
|
|
290
|
+
/**
|
|
291
|
+
* Casts a ray and returns position of the first collision found, or undefined if none are found
|
|
292
|
+
* @param {Vector2} posStart
|
|
293
|
+
* @param {Vector2} posEnd
|
|
294
|
+
* @param {LineTestFunction} testFunction - Check if colliding
|
|
295
|
+
* @param {Vector2} [normal] - Optional vector to store the normal
|
|
296
|
+
* @return {Vector2|undefined} - Position of the collision or undefined if none found
|
|
297
|
+
* @memberof Math */
|
|
298
|
+
function lineTest(posStart, posEnd, testFunction, normal)
|
|
299
|
+
{
|
|
300
|
+
ASSERT(isVector2(posStart), 'posStart must be a vec2');
|
|
301
|
+
ASSERT(isVector2(posEnd), 'posEnd must be a vec2');
|
|
302
|
+
ASSERT(typeof testFunction === 'function', 'testFunction must be a function');
|
|
303
|
+
ASSERT(!normal || isVector2(normal), 'normal must be a vec2');
|
|
304
|
+
|
|
305
|
+
// get ray direction and length
|
|
306
|
+
const dx = posEnd.x - posStart.x;
|
|
307
|
+
const dy = posEnd.y - posStart.y;
|
|
308
|
+
const totalLength = hypot(dx, dy);
|
|
309
|
+
if (!totalLength) return;
|
|
310
|
+
|
|
311
|
+
// current integer cell we are in
|
|
312
|
+
const pos = posStart.floor();
|
|
313
|
+
|
|
314
|
+
// normalize ray direction
|
|
315
|
+
const dirX = dx / totalLength;
|
|
316
|
+
const dirY = dy / totalLength;
|
|
317
|
+
|
|
318
|
+
// step direction in grid
|
|
319
|
+
const stepX = sign(dirX);
|
|
320
|
+
const stepY = sign(dirY);
|
|
321
|
+
|
|
322
|
+
// distance along the ray to cross one full cell in X or Y
|
|
323
|
+
const tDeltaX = dirX ? abs(1 / dirX) : Infinity;
|
|
324
|
+
const tDeltaY = dirY ? abs(1 / dirY) : Infinity;
|
|
325
|
+
|
|
326
|
+
// distance along the ray from start to the first grid boundary
|
|
327
|
+
const nextGridX = stepX > 0 ? pos.x + 1 : pos.x;
|
|
328
|
+
const nextGridY = stepY > 0 ? pos.y + 1 : pos.y;
|
|
329
|
+
const tMaxX = dirX ? (nextGridX - posStart.x) / dirX : Infinity;
|
|
330
|
+
const tMaxY = dirY ? (nextGridY - posStart.y) / dirY : Infinity;
|
|
331
|
+
|
|
332
|
+
// use line drawing algorithm to test for collisions
|
|
333
|
+
let t = 0, tX = tMaxX, tY = tMaxY, wasX = tDeltaX < tDeltaY;
|
|
334
|
+
while (t < totalLength)
|
|
335
|
+
{
|
|
336
|
+
if (testFunction(pos))
|
|
337
|
+
{
|
|
338
|
+
// set hit point
|
|
339
|
+
const hitPos = vec2(posStart.x + dirX*t, posStart.y + dirY*t);
|
|
340
|
+
|
|
341
|
+
// move inside of tile if on positive edge
|
|
342
|
+
const e = 1e-9;
|
|
343
|
+
if (wasX)
|
|
344
|
+
{
|
|
345
|
+
if (stepX < 0)
|
|
346
|
+
hitPos.x -= e;
|
|
347
|
+
}
|
|
348
|
+
if (stepY < 0)
|
|
349
|
+
hitPos.y -= e;
|
|
350
|
+
|
|
351
|
+
// set normal
|
|
352
|
+
if (normal)
|
|
353
|
+
wasX ? normal.set(-stepX,0) : normal.set(0,-stepY);
|
|
354
|
+
return hitPos;
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
// advance to the next grid boundary
|
|
358
|
+
if (wasX = tX < tY)
|
|
359
|
+
{
|
|
360
|
+
pos.x += stepX;
|
|
361
|
+
t = tX;
|
|
362
|
+
tX += tDeltaX;
|
|
363
|
+
}
|
|
364
|
+
else
|
|
365
|
+
{
|
|
366
|
+
pos.y += stepY;
|
|
367
|
+
t = tY;
|
|
368
|
+
tY += tDeltaY;
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
///////////////////////////////////////////////////////////////////////////////
|
|
374
|
+
|
|
375
|
+
/** Random global functions
|
|
376
|
+
* @namespace Random */
|
|
377
|
+
|
|
378
|
+
/** Returns a random value between the two values passed in
|
|
379
|
+
* @param {number} [valueA]
|
|
380
|
+
* @param {number} [valueB]
|
|
381
|
+
* @return {number}
|
|
382
|
+
* @memberof Random */
|
|
383
|
+
function rand(valueA=1, valueB=0) { return valueB + Math.random() * (valueA-valueB); }
|
|
384
|
+
|
|
385
|
+
/** Returns a floored random value between the two values passed in
|
|
386
|
+
* The upper bound is exclusive. (If 2 is passed in, result will be 0 or 1)
|
|
387
|
+
* @param {number} valueA
|
|
388
|
+
* @param {number} [valueB]
|
|
389
|
+
* @return {number}
|
|
390
|
+
* @memberof Random */
|
|
391
|
+
function randInt(valueA, valueB=0) { return floor(rand(valueA,valueB)); }
|
|
392
|
+
|
|
393
|
+
/** Randomly returns true or false given the chance of true passed in
|
|
394
|
+
* @param {number} [chance]
|
|
395
|
+
* @return {boolean}
|
|
396
|
+
* @memberof Random */
|
|
397
|
+
function randBool(chance=.5) { return rand() < chance; }
|
|
398
|
+
|
|
399
|
+
/** Randomly returns either -1 or 1
|
|
400
|
+
* @return {number}
|
|
401
|
+
* @memberof Random */
|
|
402
|
+
function randSign() { return randInt(2) * 2 - 1; }
|
|
403
|
+
|
|
404
|
+
/** Returns a random Vector2 with the passed in length
|
|
405
|
+
* @param {number} [length]
|
|
406
|
+
* @return {Vector2}
|
|
407
|
+
* @memberof Random */
|
|
408
|
+
function randVec2(length=1) { return new Vector2().setAngle(rand(2*PI), length); }
|
|
409
|
+
|
|
410
|
+
/** Returns a random Vector2 within a circular shape
|
|
411
|
+
* @param {number} [radius]
|
|
412
|
+
* @param {number} [minRadius]
|
|
413
|
+
* @return {Vector2}
|
|
414
|
+
* @memberof Random */
|
|
415
|
+
function randInCircle(radius=1, minRadius=0)
|
|
416
|
+
{ return radius > 0 ? randVec2(radius * rand(minRadius / radius, 1)**.5) : new Vector2; }
|
|
417
|
+
|
|
418
|
+
/** Returns a random color between the two passed in colors, combine components if linear
|
|
419
|
+
* @param {Color} [colorA=(1,1,1,1)]
|
|
420
|
+
* @param {Color} [colorB=(0,0,0,1)]
|
|
421
|
+
* @param {boolean} [linear]
|
|
422
|
+
* @return {Color}
|
|
423
|
+
* @memberof Random */
|
|
424
|
+
function randColor(colorA=new Color, colorB=new Color(0,0,0,1), linear=false)
|
|
425
|
+
{
|
|
426
|
+
return linear ? colorA.lerp(colorB, rand()) :
|
|
427
|
+
new Color(rand(colorA.r,colorB.r), rand(colorA.g,colorB.g), rand(colorA.b,colorB.b), rand(colorA.a,colorB.a));
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
///////////////////////////////////////////////////////////////////////////////
|
|
431
|
+
|
|
432
|
+
/**
|
|
433
|
+
* Seeded random number generator
|
|
434
|
+
* - Can be used to create a deterministic random number sequence
|
|
435
|
+
* @memberof Engine
|
|
436
|
+
* @example
|
|
437
|
+
* let r = new RandomGenerator(123); // random number generator with seed 123
|
|
438
|
+
* let a = r.float(); // random value between 0 and 1
|
|
439
|
+
* let b = r.int(10); // random integer between 0 and 9
|
|
440
|
+
* r.seed = 123; // reset the seed
|
|
441
|
+
* let c = r.float(); // the same value as a
|
|
442
|
+
*/
|
|
443
|
+
class RandomGenerator
|
|
444
|
+
{
|
|
445
|
+
/** Create a random number generator with the seed passed in
|
|
446
|
+
* @param {number} [seed] - Starting seed or engine default seed */
|
|
447
|
+
constructor(seed = 123456789)
|
|
448
|
+
{
|
|
449
|
+
/** @property {number} - random seed */
|
|
450
|
+
this.seed = seed;
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
/** Returns a seeded random value between the two values passed in
|
|
454
|
+
* @param {number} [valueA]
|
|
455
|
+
* @param {number} [valueB]
|
|
456
|
+
* @return {number} */
|
|
457
|
+
float(valueA=1, valueB=0)
|
|
458
|
+
{
|
|
459
|
+
// xorshift algorithm
|
|
460
|
+
this.seed ^= this.seed << 13;
|
|
461
|
+
this.seed ^= this.seed >>> 17;
|
|
462
|
+
this.seed ^= this.seed << 5;
|
|
463
|
+
return valueB + (valueA - valueB) * ((this.seed >>> 0) / 2**32);
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
/** Returns a floored seeded random value the two values passed in
|
|
467
|
+
* @param {number} valueA
|
|
468
|
+
* @param {number} [valueB]
|
|
469
|
+
* @return {number} */
|
|
470
|
+
int(valueA, valueB=0) { return floor(this.float(valueA, valueB)); }
|
|
471
|
+
|
|
472
|
+
/** Randomly returns true or false given the chance of true passed in
|
|
473
|
+
* @param {number} [chance]
|
|
474
|
+
* @return {boolean} */
|
|
475
|
+
bool(chance=.5) { return this.float() < chance; }
|
|
476
|
+
|
|
477
|
+
/** Randomly returns either -1 or 1 deterministically
|
|
478
|
+
* @return {number} */
|
|
479
|
+
sign() { return this.float() > .5 ? 1 : -1; }
|
|
480
|
+
|
|
481
|
+
/** Returns a seeded random value between the two values passed in with a random sign
|
|
482
|
+
* @param {number} [valueA]
|
|
483
|
+
* @param {number} [valueB]
|
|
484
|
+
* @return {number} */
|
|
485
|
+
floatSign(valueA=1, valueB=0) { return this.float(valueA, valueB) * this.sign(); }
|
|
486
|
+
|
|
487
|
+
/** Returns a random angle between -PI and PI
|
|
488
|
+
* @return {number} */
|
|
489
|
+
angle() { return this.float(-PI, PI); }
|
|
490
|
+
|
|
491
|
+
/** Returns a seeded vec2 with size between the two values passed in
|
|
492
|
+
* @param {number} valueA
|
|
493
|
+
* @param {number} [valueB]
|
|
494
|
+
* @return {Vector2} */
|
|
495
|
+
vec2(valueA=1, valueB=0)
|
|
496
|
+
{ return vec2(this.float(valueA, valueB), this.float(valueA, valueB)); }
|
|
497
|
+
|
|
498
|
+
/** Returns a random color between the two passed in colors, combine components if linear
|
|
499
|
+
* @param {Color} [colorA=(1,1,1,1)]
|
|
500
|
+
* @param {Color} [colorB=(0,0,0,1)]
|
|
501
|
+
* @param {boolean} [linear]
|
|
502
|
+
* @return {Color} */
|
|
503
|
+
randColor(colorA=new Color, colorB=new Color(0,0,0,1), linear=false)
|
|
504
|
+
{
|
|
505
|
+
return linear ? colorA.lerp(colorB, this.float()) :
|
|
506
|
+
new Color(
|
|
507
|
+
this.float(colorA.r,colorB.r),
|
|
508
|
+
this.float(colorA.g,colorB.g),
|
|
509
|
+
this.float(colorA.b,colorB.b),
|
|
510
|
+
this.float(colorA.a,colorB.a));
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
/** Returns a new color that has each component randomly adjusted
|
|
514
|
+
* @param {Color} color
|
|
515
|
+
* @param {number} [amount]
|
|
516
|
+
* @param {number} [alphaAmount]
|
|
517
|
+
* @return {Color} */
|
|
518
|
+
mutateColor(color, amount=.05, alphaAmount=0)
|
|
519
|
+
{
|
|
520
|
+
ASSERT_NUMBER_VALID(amount);
|
|
521
|
+
ASSERT_NUMBER_VALID(alphaAmount);
|
|
522
|
+
return new Color
|
|
523
|
+
(
|
|
524
|
+
color.r + this.float(amount, -amount),
|
|
525
|
+
color.g + this.float(amount, -amount),
|
|
526
|
+
color.b + this.float(amount, -amount),
|
|
527
|
+
color.a + this.float(alphaAmount, -alphaAmount)
|
|
528
|
+
).clamp();
|
|
529
|
+
}
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
///////////////////////////////////////////////////////////////////////////////
|
|
533
|
+
|
|
534
|
+
/**
|
|
535
|
+
* Create a 2d vector, can take 1 or 2 scalar values
|
|
536
|
+
* @param {number} [x]
|
|
537
|
+
* @param {number} [y] - if y is undefined, x is used for both
|
|
538
|
+
* @return {Vector2}
|
|
539
|
+
* @example
|
|
540
|
+
* let a = vec2(0, 1); // vector with coordinates (0, 1)
|
|
541
|
+
* a = vec2(5); // set a to (5, 5)
|
|
542
|
+
* b = vec2(); // set b to (0, 0)
|
|
543
|
+
* @memberof Math */
|
|
544
|
+
function vec2(x=0, y) { return new Vector2(x, y === undefined ? x : y); }
|
|
545
|
+
|
|
546
|
+
/**
|
|
547
|
+
* Check if object is a valid Vector2
|
|
548
|
+
* @param {any} v
|
|
549
|
+
* @return {boolean}
|
|
550
|
+
* @memberof Math */
|
|
551
|
+
function isVector2(v) { return v instanceof Vector2 && v.isValid(); }
|
|
552
|
+
|
|
553
|
+
// vector2 asserts
|
|
554
|
+
function ASSERT_VECTOR2_VALID(v) { ASSERT(isVector2(v), 'Vector2 is invalid.', v); }
|
|
555
|
+
function ASSERT_NUMBER_VALID(n) { ASSERT(isNumber(n), 'Number is invalid.', n); }
|
|
556
|
+
function ASSERT_VECTOR2_NORMAL(v)
|
|
557
|
+
{
|
|
558
|
+
ASSERT_VECTOR2_VALID(v);
|
|
559
|
+
ASSERT(abs(v.lengthSquared()-1) < .01, 'Vector2 is not normal.', v);
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
/**
|
|
563
|
+
* 2D Vector object with vector math library
|
|
564
|
+
* - Functions do not change this so they can be chained together
|
|
565
|
+
* @memberof Engine
|
|
566
|
+
* @example
|
|
567
|
+
* let a = new Vector2(2, 3); // vector with coordinates (2, 3)
|
|
568
|
+
* let b = new Vector2; // vector with coordinates (0, 0)
|
|
569
|
+
* let c = vec2(4, 2); // use the vec2 function to make a Vector2
|
|
570
|
+
* let d = a.add(b).scale(5); // operators can be chained
|
|
571
|
+
*/
|
|
572
|
+
class Vector2
|
|
573
|
+
{
|
|
574
|
+
/** Create a 2D vector with the x and y passed in, can also be created with vec2()
|
|
575
|
+
* @param {number} [x] - X axis location
|
|
576
|
+
* @param {number} [y] - Y axis location */
|
|
577
|
+
constructor(x=0, y=0)
|
|
578
|
+
{
|
|
579
|
+
/** @property {number} - X axis location */
|
|
580
|
+
this.x = x;
|
|
581
|
+
/** @property {number} - Y axis location */
|
|
582
|
+
this.y = y;
|
|
583
|
+
ASSERT(this.isValid(), 'Constructed Vector2 is invalid.', this);
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
/** Sets values of this vector and returns self
|
|
587
|
+
* @param {number} [x] - X axis location
|
|
588
|
+
* @param {number} [y] - Y axis location
|
|
589
|
+
* @return {Vector2} */
|
|
590
|
+
set(x=0, y=0)
|
|
591
|
+
{
|
|
592
|
+
this.x = x;
|
|
593
|
+
this.y = y;
|
|
594
|
+
ASSERT_VECTOR2_VALID(this);
|
|
595
|
+
return this;
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
/** Sets this vector from another vector and returns self
|
|
599
|
+
* @param {Vector2} v - other vector
|
|
600
|
+
* @return {Vector2} */
|
|
601
|
+
setFrom(v) { return this.set(v.x, v.y); }
|
|
602
|
+
|
|
603
|
+
/** Returns a new vector that is a copy of this
|
|
604
|
+
* @return {Vector2} */
|
|
605
|
+
copy() { return new Vector2(this.x, this.y); }
|
|
606
|
+
|
|
607
|
+
/** Returns a copy of this vector plus the vector passed in
|
|
608
|
+
* @param {Vector2} v - other vector
|
|
609
|
+
* @return {Vector2} */
|
|
610
|
+
add(v) { return new Vector2(this.x + v.x, this.y + v.y);}
|
|
611
|
+
|
|
612
|
+
/** Returns a copy of this vector minus the vector passed in
|
|
613
|
+
* @param {Vector2} v - other vector
|
|
614
|
+
* @return {Vector2} */
|
|
615
|
+
subtract(v) { return new Vector2(this.x - v.x, this.y - v.y); }
|
|
616
|
+
|
|
617
|
+
/** Returns a copy of this vector times the vector passed in
|
|
618
|
+
* @param {Vector2} v - other vector
|
|
619
|
+
* @return {Vector2} */
|
|
620
|
+
multiply(v) { return new Vector2(this.x * v.x, this.y * v.y); }
|
|
621
|
+
|
|
622
|
+
/** Returns a copy of this vector divided by the vector passed in
|
|
623
|
+
* @param {Vector2} v - other vector
|
|
624
|
+
* @return {Vector2} */
|
|
625
|
+
divide(v) { return new Vector2(this.x / v.x, this.y / v.y); }
|
|
626
|
+
|
|
627
|
+
/** Returns a copy of this vector scaled by the vector passed in
|
|
628
|
+
* @param {number} s - scale
|
|
629
|
+
* @return {Vector2} */
|
|
630
|
+
scale(s) { return new Vector2(this.x * s, this.y * s); }
|
|
631
|
+
|
|
632
|
+
/** Returns the length of this vector
|
|
633
|
+
* @return {number} */
|
|
634
|
+
length() { return this.lengthSquared()**.5; }
|
|
635
|
+
|
|
636
|
+
/** Returns the length of this vector squared
|
|
637
|
+
* @return {number} */
|
|
638
|
+
lengthSquared() { return this.x**2 + this.y**2; }
|
|
639
|
+
|
|
640
|
+
/** Returns the distance from this vector to vector passed in
|
|
641
|
+
* @param {Vector2} v - other vector
|
|
642
|
+
* @return {number} */
|
|
643
|
+
distance(v) { return this.distanceSquared(v)**.5; }
|
|
644
|
+
|
|
645
|
+
/** Returns the distance squared from this vector to vector passed in
|
|
646
|
+
* @param {Vector2} v - other vector
|
|
647
|
+
* @return {number} */
|
|
648
|
+
distanceSquared(v) { return (this.x - v.x)**2 + (this.y - v.y)**2; }
|
|
649
|
+
|
|
650
|
+
/** Returns a new vector in same direction as this one with the length passed in
|
|
651
|
+
* @param {number} [length]
|
|
652
|
+
* @return {Vector2} */
|
|
653
|
+
normalize(length=1)
|
|
654
|
+
{
|
|
655
|
+
const l = this.length();
|
|
656
|
+
return l ? this.scale(length/l) : new Vector2(0, length);
|
|
657
|
+
}
|
|
658
|
+
|
|
659
|
+
/** Returns a new vector clamped to length passed in
|
|
660
|
+
* @param {number} [length]
|
|
661
|
+
* @return {Vector2} */
|
|
662
|
+
clampLength(length=1)
|
|
663
|
+
{
|
|
664
|
+
const l = this.length();
|
|
665
|
+
return l > length ? this.scale(length/l) : this.copy();
|
|
666
|
+
}
|
|
667
|
+
|
|
668
|
+
/** Returns the dot product of this and the vector passed in
|
|
669
|
+
* @param {Vector2} v - other vector
|
|
670
|
+
* @return {number} */
|
|
671
|
+
dot(v) { return this.x*v.x + this.y*v.y; }
|
|
672
|
+
|
|
673
|
+
/** Returns the cross product of this and the vector passed in
|
|
674
|
+
* @param {Vector2} v - other vector
|
|
675
|
+
* @return {number} */
|
|
676
|
+
cross(v) { return this.x*v.y - this.y*v.x; }
|
|
677
|
+
|
|
678
|
+
/** Returns a copy this vector reflected by the surface normal
|
|
679
|
+
* @param {Vector2} normal - surface normal (should be normalized)
|
|
680
|
+
* @param {number} restitution - how much to bounce, 1 is perfect bounce, 0 is no bounce
|
|
681
|
+
* @return {Vector2} */
|
|
682
|
+
reflect(normal, restitution=1)
|
|
683
|
+
{ return this.subtract(normal.scale((1+restitution)*this.dot(normal))); }
|
|
684
|
+
|
|
685
|
+
/** Returns the clockwise angle of this vector, up is angle 0
|
|
686
|
+
* @return {number} */
|
|
687
|
+
angle() { return atan2(this.x, this.y); }
|
|
688
|
+
|
|
689
|
+
/** Sets this vector with clockwise angle and length passed in
|
|
690
|
+
* @param {number} [angle]
|
|
691
|
+
* @param {number} [length]
|
|
692
|
+
* @return {Vector2} */
|
|
693
|
+
setAngle(angle=0, length=1)
|
|
694
|
+
{
|
|
695
|
+
ASSERT_NUMBER_VALID(angle);
|
|
696
|
+
ASSERT_NUMBER_VALID(length);
|
|
697
|
+
this.x = length*sin(angle);
|
|
698
|
+
this.y = length*cos(angle);
|
|
699
|
+
return this;
|
|
700
|
+
}
|
|
701
|
+
|
|
702
|
+
/** Returns copy of this vector rotated by the clockwise angle passed in
|
|
703
|
+
* @param {number} angle
|
|
704
|
+
* @return {Vector2} */
|
|
705
|
+
rotate(angle)
|
|
706
|
+
{
|
|
707
|
+
ASSERT_NUMBER_VALID(angle);
|
|
708
|
+
const c = cos(-angle), s = sin(-angle);
|
|
709
|
+
return new Vector2(this.x*c - this.y*s, this.x*s + this.y*c);
|
|
710
|
+
}
|
|
711
|
+
|
|
712
|
+
/** Sets this this vector to point in the specified integer direction (0-3), corresponding to multiples of 90 degree rotation
|
|
713
|
+
* @param {number} [direction]
|
|
714
|
+
* @param {number} [length]
|
|
715
|
+
* @return {Vector2} */
|
|
716
|
+
setDirection(direction, length=1)
|
|
717
|
+
{
|
|
718
|
+
ASSERT_NUMBER_VALID(direction);
|
|
719
|
+
ASSERT_NUMBER_VALID(length);
|
|
720
|
+
direction = mod(direction, 4);
|
|
721
|
+
ASSERT(direction===0 || direction===1 || direction===2 || direction===3,
|
|
722
|
+
'Vector2.setDirection() direction must be an integer between 0 and 3.');
|
|
723
|
+
|
|
724
|
+
this.x = direction%2 ? direction-1 ? -length : length : 0;
|
|
725
|
+
this.y = direction%2 ? 0 : direction ? -length : length;
|
|
726
|
+
return this;
|
|
727
|
+
}
|
|
728
|
+
|
|
729
|
+
/** Returns the integer direction of this vector, corresponding to multiples of 90 degree rotation (0-3)
|
|
730
|
+
* @return {number} */
|
|
731
|
+
direction()
|
|
732
|
+
{ return abs(this.x) > abs(this.y) ? this.x < 0 ? 3 : 1 : this.y < 0 ? 2 : 0; }
|
|
733
|
+
|
|
734
|
+
/** Returns a copy of this vector with absolute values
|
|
735
|
+
* @return {Vector2} */
|
|
736
|
+
abs() { return new Vector2(abs(this.x), abs(this.y)); }
|
|
737
|
+
|
|
738
|
+
/** Returns a copy of this vector with each axis floored
|
|
739
|
+
* @return {Vector2} */
|
|
740
|
+
floor() { return new Vector2(floor(this.x), floor(this.y)); }
|
|
741
|
+
|
|
742
|
+
/** Returns new vec2 with modded values
|
|
743
|
+
* @param {number} [divisor]
|
|
744
|
+
* @return {Vector2} */
|
|
745
|
+
mod(divisor=1)
|
|
746
|
+
{ return new Vector2(mod(this.x, divisor), mod(this.y, divisor)); }
|
|
747
|
+
|
|
748
|
+
/** Returns the area this vector covers as a rectangle
|
|
749
|
+
* @return {number} */
|
|
750
|
+
area() { return abs(this.x * this.y); }
|
|
751
|
+
|
|
752
|
+
/** Returns true if this vector is (0,0)
|
|
753
|
+
* @return {boolean} */
|
|
754
|
+
isZero() { return !this.x && !this.y; }
|
|
755
|
+
|
|
756
|
+
/** Returns a new vector that is p percent between this and the vector passed in
|
|
757
|
+
* @param {Vector2} v - other vector
|
|
758
|
+
* @param {number} percent
|
|
759
|
+
* @return {Vector2} */
|
|
760
|
+
lerp(v, percent)
|
|
761
|
+
{
|
|
762
|
+
ASSERT_VECTOR2_VALID(v);
|
|
763
|
+
ASSERT_NUMBER_VALID(percent);
|
|
764
|
+
const p = clamp(percent);
|
|
765
|
+
return new Vector2(v.x*p + this.x*(1-p), v.y*p + this.y*(1-p));
|
|
766
|
+
}
|
|
767
|
+
|
|
768
|
+
/** Returns true if this vector is within the bounds of an array size passed in
|
|
769
|
+
* @param {Vector2} arraySize
|
|
770
|
+
* @return {boolean} */
|
|
771
|
+
arrayCheck(arraySize)
|
|
772
|
+
{ return this.x >= 0 && this.y >= 0 && this.x < arraySize.x && this.y < arraySize.y; }
|
|
773
|
+
|
|
774
|
+
/** Returns this vector expressed as a string
|
|
775
|
+
* @param {number} digits - precision to display
|
|
776
|
+
* @return {string} */
|
|
777
|
+
toString(digits=3)
|
|
778
|
+
{
|
|
779
|
+
ASSERT_NUMBER_VALID(digits);
|
|
780
|
+
if (this.isValid())
|
|
781
|
+
return `(${(this.x<0?'':' ') + this.x.toFixed(digits)},${(this.y<0?'':' ') + this.y.toFixed(digits)} )`;
|
|
782
|
+
else
|
|
783
|
+
return `(${this.x}, ${this.y})`;
|
|
784
|
+
}
|
|
785
|
+
|
|
786
|
+
/** Checks if this is a valid vector
|
|
787
|
+
* @return {boolean} */
|
|
788
|
+
isValid() { return isNumber(this.x) && isNumber(this.y); }
|
|
789
|
+
}
|
|
790
|
+
|
|
791
|
+
///////////////////////////////////////////////////////////////////////////////
|
|
792
|
+
|
|
793
|
+
/**
|
|
794
|
+
* Create a color object with RGBA values, white by default
|
|
795
|
+
* @param {number} [r=1] - red
|
|
796
|
+
* @param {number} [g=1] - green
|
|
797
|
+
* @param {number} [b=1] - blue
|
|
798
|
+
* @param {number} [a=1] - alpha
|
|
799
|
+
* @return {Color}
|
|
800
|
+
* @memberof Math
|
|
801
|
+
*/
|
|
802
|
+
function rgb(r, g, b, a) { return new Color(r, g, b, a); }
|
|
803
|
+
|
|
804
|
+
/**
|
|
805
|
+
* Create a color object with HSLA values, white by default
|
|
806
|
+
* @param {number} [h=0] - hue
|
|
807
|
+
* @param {number} [s=0] - saturation
|
|
808
|
+
* @param {number} [l=1] - lightness
|
|
809
|
+
* @param {number} [a=1] - alpha
|
|
810
|
+
* @return {Color}
|
|
811
|
+
* @memberof Math */
|
|
812
|
+
function hsl(h, s, l, a) { return new Color().setHSLA(h, s, l, a); }
|
|
813
|
+
|
|
814
|
+
/**
|
|
815
|
+
* Check if object is a valid Color
|
|
816
|
+
* @param {any} c
|
|
817
|
+
* @return {boolean}
|
|
818
|
+
* @memberof Math */
|
|
819
|
+
function isColor(c) { return c instanceof Color && c.isValid(); }
|
|
820
|
+
|
|
821
|
+
// color asserts
|
|
822
|
+
function ASSERT_COLOR_VALID(c) { ASSERT(isColor(c), 'Color is invalid.', c); }
|
|
823
|
+
|
|
824
|
+
/**
|
|
825
|
+
* Color object (red, green, blue, alpha) with some helpful functions
|
|
826
|
+
* @memberof Engine
|
|
827
|
+
* @example
|
|
828
|
+
* let a = new Color; // white
|
|
829
|
+
* let b = new Color(1, 0, 0); // red
|
|
830
|
+
* let c = new Color(0, 0, 0, 0); // transparent black
|
|
831
|
+
* let d = rgb(0, 0, 1); // blue using rgb color
|
|
832
|
+
* let e = hsl(.3, 1, .5); // green using hsl color
|
|
833
|
+
*/
|
|
834
|
+
class Color
|
|
835
|
+
{
|
|
836
|
+
/** Create a color with the rgba components passed in, white by default
|
|
837
|
+
* @param {number} [r] - red
|
|
838
|
+
* @param {number} [g] - green
|
|
839
|
+
* @param {number} [b] - blue
|
|
840
|
+
* @param {number} [a] - alpha*/
|
|
841
|
+
constructor(r=1, g=1, b=1, a=1)
|
|
842
|
+
{
|
|
843
|
+
/** @property {number} - Red */
|
|
844
|
+
this.r = r;
|
|
845
|
+
/** @property {number} - Green */
|
|
846
|
+
this.g = g;
|
|
847
|
+
/** @property {number} - Blue */
|
|
848
|
+
this.b = b;
|
|
849
|
+
/** @property {number} - Alpha */
|
|
850
|
+
this.a = a;
|
|
851
|
+
ASSERT(this.isValid(), 'Constructed Color is invalid.', this);
|
|
852
|
+
}
|
|
853
|
+
|
|
854
|
+
/** Sets values of this color and returns self
|
|
855
|
+
* @param {number} [r] - red
|
|
856
|
+
* @param {number} [g] - green
|
|
857
|
+
* @param {number} [b] - blue
|
|
858
|
+
* @param {number} [a] - alpha
|
|
859
|
+
* @return {Color} */
|
|
860
|
+
set(r=1, g=1, b=1, a=1)
|
|
861
|
+
{
|
|
862
|
+
this.r = r;
|
|
863
|
+
this.g = g;
|
|
864
|
+
this.b = b;
|
|
865
|
+
this.a = a;
|
|
866
|
+
ASSERT_COLOR_VALID(this);
|
|
867
|
+
return this;
|
|
868
|
+
}
|
|
869
|
+
|
|
870
|
+
/** Sets this color from another color and returns self
|
|
871
|
+
* @param {Color} c - other color
|
|
872
|
+
* @return {Color} */
|
|
873
|
+
setFrom(c) { return this.set(c.r, c.g, c.b, c.a); }
|
|
874
|
+
|
|
875
|
+
/** Returns a new color that is a copy of this
|
|
876
|
+
* @return {Color} */
|
|
877
|
+
copy() { return new Color(this.r, this.g, this.b, this.a); }
|
|
878
|
+
|
|
879
|
+
/** Returns a copy of this color plus the color passed in
|
|
880
|
+
* @param {Color} c - other color
|
|
881
|
+
* @return {Color} */
|
|
882
|
+
add(c) { return new Color(this.r+c.r, this.g+c.g, this.b+c.b, this.a+c.a); }
|
|
883
|
+
|
|
884
|
+
/** Returns a copy of this color minus the color passed in
|
|
885
|
+
* @param {Color} c - other color
|
|
886
|
+
* @return {Color} */
|
|
887
|
+
subtract(c) { return new Color(this.r-c.r, this.g-c.g, this.b-c.b, this.a-c.a); }
|
|
888
|
+
|
|
889
|
+
/** Returns a copy of this color times the color passed in
|
|
890
|
+
* @param {Color} c - other color
|
|
891
|
+
* @return {Color} */
|
|
892
|
+
multiply(c) { return new Color(this.r*c.r, this.g*c.g, this.b*c.b, this.a*c.a); }
|
|
893
|
+
|
|
894
|
+
/** Returns a copy of this color divided by the color passed in
|
|
895
|
+
* @param {Color} c - other color
|
|
896
|
+
* @return {Color} */
|
|
897
|
+
divide(c) { return new Color(this.r/c.r, this.g/c.g, this.b/c.b, this.a/c.a); }
|
|
898
|
+
|
|
899
|
+
/** Returns a copy of this color scaled by the value passed in, alpha can be scaled separately
|
|
900
|
+
* @param {number} scale
|
|
901
|
+
* @param {number} [alphaScale=scale]
|
|
902
|
+
* @return {Color} */
|
|
903
|
+
scale(scale, alphaScale=scale)
|
|
904
|
+
{ return new Color(this.r*scale, this.g*scale, this.b*scale, this.a*alphaScale); }
|
|
905
|
+
|
|
906
|
+
/** Returns a copy of this color clamped to the valid range between 0 and 1
|
|
907
|
+
* @return {Color} */
|
|
908
|
+
clamp() { return new Color(clamp(this.r), clamp(this.g), clamp(this.b), clamp(this.a)); }
|
|
909
|
+
|
|
910
|
+
/** Returns a new color that is p percent between this and the color passed in
|
|
911
|
+
* @param {Color} c - other color
|
|
912
|
+
* @param {number} percent
|
|
913
|
+
* @return {Color} */
|
|
914
|
+
lerp(c, percent)
|
|
915
|
+
{
|
|
916
|
+
ASSERT_COLOR_VALID(c);
|
|
917
|
+
ASSERT_NUMBER_VALID(percent);
|
|
918
|
+
const p = clamp(percent);
|
|
919
|
+
return new Color(
|
|
920
|
+
c.r*p + this.r*(1-p),
|
|
921
|
+
c.g*p + this.g*(1-p),
|
|
922
|
+
c.b*p + this.b*(1-p),
|
|
923
|
+
c.a*p + this.a*(1-p));
|
|
924
|
+
}
|
|
925
|
+
|
|
926
|
+
/** Sets this color given a hue, saturation, lightness, and alpha
|
|
927
|
+
* @param {number} [h] - hue
|
|
928
|
+
* @param {number} [s] - saturation
|
|
929
|
+
* @param {number} [l] - lightness
|
|
930
|
+
* @param {number} [a] - alpha
|
|
931
|
+
* @return {Color} */
|
|
932
|
+
setHSLA(h=0, s=0, l=1, a=1)
|
|
933
|
+
{
|
|
934
|
+
h = mod(h,1);
|
|
935
|
+
s = clamp(s);
|
|
936
|
+
l = clamp(l);
|
|
937
|
+
const q = l < .5 ? l*(1+s) : l+s-l*s, p = 2*l-q,
|
|
938
|
+
f = (p, q, t)=>
|
|
939
|
+
(t = mod(t,1))*6 < 1 ? p+(q-p)*6*t :
|
|
940
|
+
t*2 < 1 ? q :
|
|
941
|
+
t*3 < 2 ? p+(q-p)*(4-t*6) : p;
|
|
942
|
+
this.r = f(p, q, h + 1/3);
|
|
943
|
+
this.g = f(p, q, h);
|
|
944
|
+
this.b = f(p, q, h - 1/3);
|
|
945
|
+
this.a = a;
|
|
946
|
+
ASSERT_COLOR_VALID(this);
|
|
947
|
+
return this;
|
|
948
|
+
}
|
|
949
|
+
|
|
950
|
+
/** Returns this color expressed in hsla format
|
|
951
|
+
* @return {Array<number>} */
|
|
952
|
+
HSLA()
|
|
953
|
+
{
|
|
954
|
+
const r = clamp(this.r);
|
|
955
|
+
const g = clamp(this.g);
|
|
956
|
+
const b = clamp(this.b);
|
|
957
|
+
const a = clamp(this.a);
|
|
958
|
+
const maxC = max(r, g, b);
|
|
959
|
+
const minC = min(r, g, b);
|
|
960
|
+
const l = (maxC + minC) / 2;
|
|
961
|
+
let h = 0, s = 0;
|
|
962
|
+
if (maxC !== minC)
|
|
963
|
+
{
|
|
964
|
+
let d = maxC - minC;
|
|
965
|
+
s = l > .5 ? d / (2 - maxC - minC) : d / (maxC + minC);
|
|
966
|
+
if (r === maxC)
|
|
967
|
+
h = (g - b) / d + (g < b ? 6 : 0);
|
|
968
|
+
else if (g === maxC)
|
|
969
|
+
h = (b - r) / d + 2;
|
|
970
|
+
else if (b === maxC)
|
|
971
|
+
h = (r - g) / d + 4;
|
|
972
|
+
}
|
|
973
|
+
return [h / 6, s, l, a];
|
|
974
|
+
}
|
|
975
|
+
|
|
976
|
+
/** Returns a new color that has each component randomly adjusted
|
|
977
|
+
* @param {number} [amount]
|
|
978
|
+
* @param {number} [alphaAmount]
|
|
979
|
+
* @return {Color} */
|
|
980
|
+
mutate(amount=.05, alphaAmount=0)
|
|
981
|
+
{
|
|
982
|
+
ASSERT_NUMBER_VALID(amount);
|
|
983
|
+
ASSERT_NUMBER_VALID(alphaAmount);
|
|
984
|
+
return new Color
|
|
985
|
+
(
|
|
986
|
+
this.r + rand(amount, -amount),
|
|
987
|
+
this.g + rand(amount, -amount),
|
|
988
|
+
this.b + rand(amount, -amount),
|
|
989
|
+
this.a + rand(alphaAmount, -alphaAmount)
|
|
990
|
+
).clamp();
|
|
991
|
+
}
|
|
992
|
+
|
|
993
|
+
/** Returns this color expressed as a hex color code
|
|
994
|
+
* @param {boolean} [useAlpha] - if alpha should be included in result
|
|
995
|
+
* @return {string} */
|
|
996
|
+
toString(useAlpha = true)
|
|
997
|
+
{
|
|
998
|
+
if (debug && !this.isValid())
|
|
999
|
+
return '#000';
|
|
1000
|
+
const toHex = (c)=> ((c=clamp(c)*255|0)<16 ? '0' : '') + c.toString(16);
|
|
1001
|
+
return '#' + toHex(this.r) + toHex(this.g) + toHex(this.b) + (useAlpha ? toHex(this.a) : '');
|
|
1002
|
+
}
|
|
1003
|
+
|
|
1004
|
+
/** Set this color from a hex code
|
|
1005
|
+
* @param {string} hex - html hex code
|
|
1006
|
+
* @return {Color} */
|
|
1007
|
+
setHex(hex)
|
|
1008
|
+
{
|
|
1009
|
+
ASSERT(isString(hex), 'Color hex code must be a string');
|
|
1010
|
+
ASSERT(hex[0] === '#', 'Color hex code must start with #');
|
|
1011
|
+
ASSERT([4,5,7,9].includes(hex.length), 'Invalid hex');
|
|
1012
|
+
|
|
1013
|
+
if (hex.length < 6)
|
|
1014
|
+
{
|
|
1015
|
+
const fromHex = (c)=> clamp(parseInt(hex[c],16)/15);
|
|
1016
|
+
this.r = fromHex(1);
|
|
1017
|
+
this.g = fromHex(2);
|
|
1018
|
+
this.b = fromHex(3);
|
|
1019
|
+
this.a = hex.length === 5 ? fromHex(4) : 1;
|
|
1020
|
+
}
|
|
1021
|
+
else
|
|
1022
|
+
{
|
|
1023
|
+
const fromHex = (c)=> clamp(parseInt(hex.slice(c,c+2),16)/255);
|
|
1024
|
+
this.r = fromHex(1);
|
|
1025
|
+
this.g = fromHex(3);
|
|
1026
|
+
this.b = fromHex(5);
|
|
1027
|
+
this.a = hex.length === 9 ? fromHex(7) : 1;
|
|
1028
|
+
}
|
|
1029
|
+
|
|
1030
|
+
ASSERT_COLOR_VALID(this);
|
|
1031
|
+
return this;
|
|
1032
|
+
}
|
|
1033
|
+
|
|
1034
|
+
/** Returns this color expressed as 32 bit RGBA value
|
|
1035
|
+
* @return {number} */
|
|
1036
|
+
rgbaInt()
|
|
1037
|
+
{
|
|
1038
|
+
const r = clamp(this.r)*255|0;
|
|
1039
|
+
const g = clamp(this.g)*255<<8;
|
|
1040
|
+
const b = clamp(this.b)*255<<16;
|
|
1041
|
+
const a = clamp(this.a)*255<<24;
|
|
1042
|
+
return r + g + b + a;
|
|
1043
|
+
}
|
|
1044
|
+
|
|
1045
|
+
/** Checks if this is a valid color
|
|
1046
|
+
* @return {boolean} */
|
|
1047
|
+
isValid()
|
|
1048
|
+
{ return isNumber(this.r) && isNumber(this.g) && isNumber(this.b) && isNumber(this.a); }
|
|
1049
|
+
}
|
|
1050
|
+
|
|
1051
|
+
///////////////////////////////////////////////////////////////////////////////
|
|
1052
|
+
// Default Colors
|
|
1053
|
+
|
|
1054
|
+
/** Color - White #ffffff
|
|
1055
|
+
* @type {Color}
|
|
1056
|
+
* @memberof Math */
|
|
1057
|
+
const WHITE = debugProtectConstant(rgb());
|
|
1058
|
+
|
|
1059
|
+
/** Color - Clear White #757474ff with 0 alpha
|
|
1060
|
+
* @type {Color}
|
|
1061
|
+
* @memberof Math */
|
|
1062
|
+
const CLEAR_WHITE = debugProtectConstant(rgb(1,1,1,0));
|
|
1063
|
+
|
|
1064
|
+
/** Color - Black #000000
|
|
1065
|
+
* @type {Color}
|
|
1066
|
+
* @memberof Math */
|
|
1067
|
+
const BLACK = debugProtectConstant(rgb(0,0,0));
|
|
1068
|
+
|
|
1069
|
+
/** Color - Clear Black #000000 with 0 alpha
|
|
1070
|
+
* @type {Color}
|
|
1071
|
+
* @memberof Math */
|
|
1072
|
+
const CLEAR_BLACK = debugProtectConstant(rgb(0,0,0,0));
|
|
1073
|
+
|
|
1074
|
+
/** Color - Gray #808080
|
|
1075
|
+
* @type {Color}
|
|
1076
|
+
* @memberof Math */
|
|
1077
|
+
const GRAY = debugProtectConstant(rgb(.5,.5,.5));
|
|
1078
|
+
|
|
1079
|
+
/** Color - Red #ff0000
|
|
1080
|
+
* @type {Color}
|
|
1081
|
+
* @memberof Math */
|
|
1082
|
+
const RED = debugProtectConstant(rgb(1,0,0));
|
|
1083
|
+
|
|
1084
|
+
/** Color - Orange #ff8000
|
|
1085
|
+
* @type {Color}
|
|
1086
|
+
* @memberof Math */
|
|
1087
|
+
const ORANGE = debugProtectConstant(rgb(1,.5,0));
|
|
1088
|
+
|
|
1089
|
+
/** Color - Yellow #ffff00
|
|
1090
|
+
* @type {Color}
|
|
1091
|
+
* @memberof Math */
|
|
1092
|
+
const YELLOW = debugProtectConstant(rgb(1,1,0));
|
|
1093
|
+
|
|
1094
|
+
/** Color - Green #00ff00
|
|
1095
|
+
* @type {Color}
|
|
1096
|
+
* @memberof Math */
|
|
1097
|
+
const GREEN = debugProtectConstant(rgb(0,1,0));
|
|
1098
|
+
|
|
1099
|
+
/** Color - Cyan #00ffff
|
|
1100
|
+
* @type {Color}
|
|
1101
|
+
* @memberof Math */
|
|
1102
|
+
const CYAN = debugProtectConstant(rgb(0,1,1));
|
|
1103
|
+
|
|
1104
|
+
/** Color - Blue #0000ff
|
|
1105
|
+
* @type {Color}
|
|
1106
|
+
* @memberof Math */
|
|
1107
|
+
const BLUE = debugProtectConstant(rgb(0,0,1));
|
|
1108
|
+
|
|
1109
|
+
/** Color - Purple #8000ff
|
|
1110
|
+
* @type {Color}
|
|
1111
|
+
* @memberof Math */
|
|
1112
|
+
const PURPLE = debugProtectConstant(rgb(.5,0,1));
|
|
1113
|
+
|
|
1114
|
+
/** Color - Magenta #ff00ff
|
|
1115
|
+
* @type {Color}
|
|
1116
|
+
* @memberof Math */
|
|
1117
|
+
const MAGENTA = debugProtectConstant(rgb(1,0,1));
|
|
1118
|
+
|
|
1119
|
+
///////////////////////////////////////////////////////////////////////////////
|
|
1120
|
+
|
|
1121
|
+
/**
|
|
1122
|
+
* Timer object tracks how long has passed since it was set
|
|
1123
|
+
* @memberof Engine
|
|
1124
|
+
* @example
|
|
1125
|
+
* let a = new Timer; // creates a timer that is not set
|
|
1126
|
+
* a.set(3); // sets the timer to 3 seconds
|
|
1127
|
+
*
|
|
1128
|
+
* let b = new Timer(1); // creates a timer with 1 second left
|
|
1129
|
+
* b.unset(); // unset the timer
|
|
1130
|
+
*/
|
|
1131
|
+
class Timer
|
|
1132
|
+
{
|
|
1133
|
+
/** Create a timer object set time passed in
|
|
1134
|
+
* @param {number} [timeLeft] - How much time left before the timer
|
|
1135
|
+
* @param {boolean} [useRealTime] - Should the timer keep running even when the game is paused? (useful for UI) */
|
|
1136
|
+
constructor(timeLeft, useRealTime=false)
|
|
1137
|
+
{
|
|
1138
|
+
ASSERT(timeLeft === undefined || isNumber(timeLeft), 'Constructed Timer is invalid.', timeLeft);
|
|
1139
|
+
this.useRealTime = useRealTime;
|
|
1140
|
+
const globalTime = this.getGlobalTime();
|
|
1141
|
+
this.time = timeLeft === undefined ? undefined : globalTime + timeLeft;
|
|
1142
|
+
this.setTime = timeLeft;
|
|
1143
|
+
}
|
|
1144
|
+
|
|
1145
|
+
/** Set the timer with seconds passed in
|
|
1146
|
+
* @param {number} [timeLeft] - How much time left before the timer is elapsed in seconds */
|
|
1147
|
+
set(timeLeft=0)
|
|
1148
|
+
{
|
|
1149
|
+
ASSERT(isNumber(timeLeft), 'Timer is invalid.', timeLeft);
|
|
1150
|
+
const globalTime = this.getGlobalTime();
|
|
1151
|
+
this.time = globalTime + timeLeft;
|
|
1152
|
+
this.setTime = timeLeft;
|
|
1153
|
+
}
|
|
1154
|
+
|
|
1155
|
+
/** Set if the timer should keep running even when the game is paused
|
|
1156
|
+
* @param {boolean} [useRealTime] */
|
|
1157
|
+
setUseRealTime(useRealTime=true)
|
|
1158
|
+
{
|
|
1159
|
+
ASSERT(!this.isSet(), 'Cannot change global time setting while timer is set.');
|
|
1160
|
+
this.useRealTime = useRealTime;
|
|
1161
|
+
}
|
|
1162
|
+
|
|
1163
|
+
/** Unset the timer */
|
|
1164
|
+
unset() { this.time = undefined; }
|
|
1165
|
+
|
|
1166
|
+
/** Returns true if set
|
|
1167
|
+
* @return {boolean} */
|
|
1168
|
+
isSet() { return this.time !== undefined; }
|
|
1169
|
+
|
|
1170
|
+
/** Returns true if set and has not elapsed
|
|
1171
|
+
* @return {boolean} */
|
|
1172
|
+
active() { return this.getGlobalTime() < this.time; }
|
|
1173
|
+
|
|
1174
|
+
/** Returns true if set and elapsed
|
|
1175
|
+
* @return {boolean} */
|
|
1176
|
+
elapsed() { return this.getGlobalTime() >= this.time; }
|
|
1177
|
+
|
|
1178
|
+
/** Get how long since elapsed, returns 0 if not set (returns negative if currently active)
|
|
1179
|
+
* @return {number} */
|
|
1180
|
+
get() { return this.isSet()? this.getGlobalTime() - this.time : 0; }
|
|
1181
|
+
|
|
1182
|
+
/** Get percentage elapsed based on time it was set to, returns 0 if not set
|
|
1183
|
+
* @return {number} */
|
|
1184
|
+
getPercent() { return this.isSet()? 1-percent(this.time - this.getGlobalTime(), 0, this.setTime) : 0; }
|
|
1185
|
+
|
|
1186
|
+
/** Get the time this timer was set to, returns 0 if not set
|
|
1187
|
+
* @return {number} */
|
|
1188
|
+
getSetTime() { return this.isSet() ? this.setTime : 0; }
|
|
1189
|
+
|
|
1190
|
+
/** Get the current global time this timer is based on
|
|
1191
|
+
* @return {number} */
|
|
1192
|
+
getGlobalTime() { return this.useRealTime ? timeReal : time; }
|
|
1193
|
+
|
|
1194
|
+
/** Returns this timer expressed as a string
|
|
1195
|
+
* @return {string} */
|
|
1196
|
+
toString() { return this.isSet() ? abs(this.get()) + ' seconds ' + (this.get()<0 ? 'before' : 'after' ) : 'unset'; }
|
|
1197
|
+
|
|
1198
|
+
/** Get how long since elapsed, returns 0 if not set (returns negative if currently active)
|
|
1199
|
+
* @return {number} */
|
|
1200
|
+
valueOf() { return this.get(); }
|
|
1201
|
+
}
|