kaplay 3000.1.17

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/src/math.ts ADDED
@@ -0,0 +1,1118 @@
1
+ import {
2
+ Point,
3
+ RNGValue,
4
+ LerpValue,
5
+ Vec2Args,
6
+ } from "./types"
7
+
8
+ export function deg2rad(deg: number): number {
9
+ return deg * Math.PI / 180
10
+ }
11
+
12
+ export function rad2deg(rad: number): number {
13
+ return rad * 180 / Math.PI
14
+ }
15
+
16
+ export function clamp(
17
+ val: number,
18
+ min: number,
19
+ max: number,
20
+ ): number {
21
+ if (min > max) {
22
+ return clamp(val, max, min)
23
+ }
24
+ return Math.min(Math.max(val, min), max)
25
+ }
26
+
27
+ export function lerp<V extends LerpValue>(
28
+ a: V,
29
+ b: V,
30
+ t: number,
31
+ ): V {
32
+ if (typeof a === "number" && typeof b === "number") {
33
+ return a + (b - a) * t as V
34
+ } else if (a instanceof Vec2 && b instanceof Vec2) {
35
+ return a.lerp(b, t) as V
36
+ } else if (a instanceof Color && b instanceof Color) {
37
+ return a.lerp(b, t) as V
38
+ }
39
+ throw new Error(`Bad value for lerp(): ${a}, ${b}. Only number, Vec2 and Color is supported.`)
40
+ }
41
+
42
+ export function map(
43
+ v: number,
44
+ l1: number,
45
+ h1: number,
46
+ l2: number,
47
+ h2: number,
48
+ ): number {
49
+ return l2 + (v - l1) / (h1 - l1) * (h2 - l2)
50
+ }
51
+
52
+ export function mapc(
53
+ v: number,
54
+ l1: number,
55
+ h1: number,
56
+ l2: number,
57
+ h2: number,
58
+ ): number {
59
+ return clamp(map(v, l1, h1, l2, h2), l2, h2)
60
+ }
61
+
62
+ export class Vec2 {
63
+ x: number = 0
64
+ y: number = 0
65
+ constructor(x: number = 0, y: number = x) {
66
+ this.x = x
67
+ this.y = y
68
+ }
69
+ static fromAngle(deg: number) {
70
+ const angle = deg2rad(deg)
71
+ return new Vec2(Math.cos(angle), Math.sin(angle))
72
+ }
73
+ static LEFT = new Vec2(-1, 0)
74
+ static RIGHT = new Vec2(1, 0)
75
+ static UP = new Vec2(0, -1)
76
+ static DOWN = new Vec2(0, 1)
77
+ clone(): Vec2 {
78
+ return new Vec2(this.x, this.y)
79
+ }
80
+ add(...args: Vec2Args): Vec2 {
81
+ const p2 = vec2(...args)
82
+ return new Vec2(this.x + p2.x, this.y + p2.y)
83
+ }
84
+ sub(...args: Vec2Args): Vec2 {
85
+ const p2 = vec2(...args)
86
+ return new Vec2(this.x - p2.x, this.y - p2.y)
87
+ }
88
+ scale(...args: Vec2Args): Vec2 {
89
+ const s = vec2(...args)
90
+ return new Vec2(this.x * s.x, this.y * s.y)
91
+ }
92
+ dist(...args: Vec2Args): number {
93
+ const p2 = vec2(...args)
94
+ return this.sub(p2).len()
95
+ }
96
+ sdist(...args: Vec2Args): number {
97
+ const p2 = vec2(...args)
98
+ return this.sub(p2).slen()
99
+ }
100
+ len(): number {
101
+ return Math.sqrt(this.dot(this))
102
+ }
103
+ slen(): number {
104
+ return this.dot(this)
105
+ }
106
+ unit(): Vec2 {
107
+ const len = this.len()
108
+ return len === 0 ? new Vec2(0) : this.scale(1 / len)
109
+ }
110
+ normal(): Vec2 {
111
+ return new Vec2(this.y, -this.x)
112
+ }
113
+ reflect(normal: Vec2) {
114
+ return this.sub(normal.scale(2 * this.dot(normal)))
115
+ }
116
+ project(on: Vec2) {
117
+ return on.scale(on.dot(this) / on.len())
118
+ }
119
+ reject(on: Vec2) {
120
+ return this.sub(this.project(on))
121
+ }
122
+ dot(p2: Vec2): number {
123
+ return this.x * p2.x + this.y * p2.y
124
+ }
125
+ cross(p2: Vec2): number {
126
+ return this.x * p2.y - this.y * p2.x
127
+ }
128
+ angle(...args: Vec2Args): number {
129
+ const p2 = vec2(...args)
130
+ return rad2deg(Math.atan2(this.y - p2.y, this.x - p2.x))
131
+ }
132
+ angleBetween(...args: Vec2Args): number {
133
+ const p2 = vec2(...args)
134
+ return rad2deg(Math.atan2(this.cross(p2), this.dot(p2)))
135
+ }
136
+ lerp(dest: Vec2, t: number): Vec2 {
137
+ return new Vec2(lerp(this.x, dest.x, t), lerp(this.y, dest.y, t))
138
+ }
139
+ slerp(dest: Vec2, t: number): Vec2 {
140
+ const cos = this.dot(dest)
141
+ const sin = this.cross(dest)
142
+ const angle = Math.atan2(sin, cos)
143
+ return this
144
+ .scale(Math.sin((1 - t) * angle))
145
+ .add(dest.scale(Math.sin(t * angle)))
146
+ .scale(1 / sin)
147
+ }
148
+ isZero(): boolean {
149
+ return this.x === 0 && this.y === 0
150
+ }
151
+ toFixed(n: number): Vec2 {
152
+ return new Vec2(Number(this.x.toFixed(n)), Number(this.y.toFixed(n)))
153
+ }
154
+ transform(m: Mat4): Vec2 {
155
+ return m.multVec2(this)
156
+ }
157
+ eq(other: Vec2): boolean {
158
+ return this.x === other.x && this.y === other.y
159
+ }
160
+ bbox(): Rect {
161
+ return new Rect(this, 0, 0)
162
+ }
163
+ toString(): string {
164
+ return `vec2(${this.x.toFixed(2)}, ${this.y.toFixed(2)})`
165
+ }
166
+ }
167
+
168
+ export function vec2(...args: Vec2Args): Vec2 {
169
+ if (args.length === 1) {
170
+ if (args[0] instanceof Vec2) {
171
+ return new Vec2(args[0].x, args[0].y)
172
+ } else if (Array.isArray(args[0]) && args[0].length === 2) {
173
+ return new Vec2(...args[0])
174
+ }
175
+ }
176
+ // @ts-ignore
177
+ return new Vec2(...args)
178
+ }
179
+
180
+ export class Color {
181
+
182
+ r: number = 255
183
+ g: number = 255
184
+ b: number = 255
185
+
186
+ constructor(r: number, g: number, b: number) {
187
+ this.r = clamp(r, 0, 255)
188
+ this.g = clamp(g, 0, 255)
189
+ this.b = clamp(b, 0, 255)
190
+ }
191
+
192
+ static fromArray(arr: number[]) {
193
+ return new Color(arr[0], arr[1], arr[2])
194
+ }
195
+
196
+ static fromHex(hex: string | number) {
197
+ if (typeof hex === "number") {
198
+ return new Color(
199
+ (hex >> 16) & 0xff,
200
+ (hex >> 8) & 0xff,
201
+ (hex >> 0) & 0xff,
202
+ )
203
+ } else if (typeof hex === "string") {
204
+ const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex)
205
+ return new Color(
206
+ parseInt(result[1], 16),
207
+ parseInt(result[2], 16),
208
+ parseInt(result[3], 16),
209
+ )
210
+ } else {
211
+ throw new Error("Invalid hex color format")
212
+ }
213
+ }
214
+
215
+ // TODO: use range of [0, 360] [0, 100] [0, 100]?
216
+ static fromHSL(h: number, s: number, l: number) {
217
+
218
+ if (s == 0){
219
+ return new Color(255 * l, 255 * l, 255 * l)
220
+ }
221
+
222
+ const hue2rgb = (p, q, t) => {
223
+ if (t < 0) t += 1
224
+ if (t > 1) t -= 1
225
+ if (t < 1 / 6) return p + (q - p) * 6 * t
226
+ if (t < 1 / 2) return q
227
+ if (t < 2 / 3) return p + (q - p) * (2/3 - t) * 6
228
+ return p
229
+ }
230
+
231
+ const q = l < 0.5 ? l * (1 + s) : l + s - l * s
232
+ const p = 2 * l - q
233
+ const r = hue2rgb(p, q, h + 1 / 3)
234
+ const g = hue2rgb(p, q, h)
235
+ const b = hue2rgb(p, q, h - 1 / 3)
236
+
237
+ return new Color(Math.round(r * 255), Math.round(g * 255), Math.round(b * 255))
238
+
239
+ }
240
+
241
+ static RED = new Color(255, 0, 0)
242
+ static GREEN = new Color(0, 255, 0)
243
+ static BLUE = new Color(0, 0, 255)
244
+ static YELLOW = new Color(255, 255, 0)
245
+ static MAGENTA = new Color(255, 0, 255)
246
+ static CYAN = new Color(0, 255, 255)
247
+ static WHITE = new Color(255, 255, 255)
248
+ static BLACK = new Color(0, 0, 0)
249
+
250
+ clone(): Color {
251
+ return new Color(this.r, this.g, this.b)
252
+ }
253
+
254
+ lighten(a: number): Color {
255
+ return new Color(this.r + a, this.g + a, this.b + a)
256
+ }
257
+
258
+ darken(a: number): Color {
259
+ return this.lighten(-a)
260
+ }
261
+
262
+ invert(): Color {
263
+ return new Color(255 - this.r, 255 - this.g, 255 - this.b)
264
+ }
265
+
266
+ mult(other: Color): Color {
267
+ return new Color(
268
+ this.r * other.r / 255,
269
+ this.g * other.g / 255,
270
+ this.b * other.b / 255,
271
+ )
272
+ }
273
+
274
+ lerp(dest: Color, t: number): Color {
275
+ return new Color(
276
+ lerp(this.r, dest.r, t),
277
+ lerp(this.g, dest.g, t),
278
+ lerp(this.b, dest.b, t),
279
+ )
280
+ }
281
+
282
+ toHSL(): [number, number, number] {
283
+ const r = this.r / 255
284
+ const g = this.g / 255
285
+ const b = this.b / 255
286
+ const max = Math.max(r, g, b), min = Math.min(r, g, b)
287
+ let h = (max + min) / 2
288
+ let s = h
289
+ const l = h
290
+ if (max == min) {
291
+ h = s = 0
292
+ } else {
293
+ const d = max - min
294
+ s = l > 0.5 ? d / (2 - max - min) : d / (max + min)
295
+ switch (max) {
296
+ case r: h = (g - b) / d + (g < b ? 6 : 0); break
297
+ case g: h = (b - r) / d + 2; break
298
+ case b: h = (r - g) / d + 4; break
299
+ }
300
+ h /= 6
301
+ }
302
+ return [ h, s, l ]
303
+ }
304
+
305
+ eq(other: Color): boolean {
306
+ return this.r === other.r
307
+ && this.g === other.g
308
+ && this.b === other.b
309
+
310
+ }
311
+
312
+ toString(): string {
313
+ return `rgb(${this.r}, ${this.g}, ${this.b})`
314
+ }
315
+
316
+ toHex(): string {
317
+ return "#" + ((1 << 24) + (this.r << 16) + (this.g << 8) + this.b).toString(16).slice(1)
318
+ }
319
+
320
+ }
321
+
322
+ export function rgb(...args): Color {
323
+ if (args.length === 0) {
324
+ return new Color(255, 255, 255)
325
+ } else if (args.length === 1) {
326
+ if (args[0] instanceof Color) {
327
+ return args[0].clone()
328
+ } else if (typeof args[0] === "string") {
329
+ return Color.fromHex(args[0])
330
+ } else if (Array.isArray(args[0]) && args[0].length === 3) {
331
+ return Color.fromArray(args[0])
332
+ }
333
+ }
334
+ // @ts-ignore
335
+ return new Color(...args)
336
+ }
337
+
338
+ export const hsl2rgb = (h, s, l) => Color.fromHSL(h, s, l)
339
+
340
+ export class Quad {
341
+ x: number = 0
342
+ y: number = 0
343
+ w: number = 1
344
+ h: number = 1
345
+ constructor(x: number, y: number, w: number, h: number) {
346
+ this.x = x
347
+ this.y = y
348
+ this.w = w
349
+ this.h = h
350
+ }
351
+ scale(other: Quad): Quad {
352
+ return new Quad(
353
+ this.x + this.w * other.x,
354
+ this.y + this.h * other.y,
355
+ this.w * other.w,
356
+ this.h * other.h,
357
+ )
358
+ }
359
+ pos() {
360
+ return new Vec2(this.x, this.y)
361
+ }
362
+ clone(): Quad {
363
+ return new Quad(this.x, this.y, this.w, this.h)
364
+ }
365
+ eq(other: Quad): boolean {
366
+ return this.x === other.x
367
+ && this.y === other.y
368
+ && this.w === other.w
369
+ && this.h === other.h
370
+ }
371
+ toString(): string {
372
+ return `quad(${this.x}, ${this.y}, ${this.w}, ${this.h})`
373
+ }
374
+ }
375
+
376
+ export function quad(x: number, y: number, w: number, h: number): Quad {
377
+ return new Quad(x, y, w, h)
378
+ }
379
+
380
+ export class Mat4 {
381
+
382
+ m: number[] = [
383
+ 1, 0, 0, 0,
384
+ 0, 1, 0, 0,
385
+ 0, 0, 1, 0,
386
+ 0, 0, 0, 1,
387
+ ]
388
+
389
+ constructor(m?: number[]) {
390
+ if (m) {
391
+ this.m = m
392
+ }
393
+ }
394
+
395
+ static translate(p: Vec2): Mat4 {
396
+ return new Mat4([
397
+ 1, 0, 0, 0,
398
+ 0, 1, 0, 0,
399
+ 0, 0, 1, 0,
400
+ p.x, p.y, 0, 1,
401
+ ])
402
+ }
403
+
404
+ static scale(s: Vec2): Mat4 {
405
+ return new Mat4([
406
+ s.x, 0, 0, 0,
407
+ 0, s.y, 0, 0,
408
+ 0, 0, 1, 0,
409
+ 0, 0, 0, 1,
410
+ ])
411
+ }
412
+
413
+ static rotateX(a: number): Mat4 {
414
+ a = deg2rad(-a)
415
+ const c = Math.cos(a)
416
+ const s = Math.sin(a)
417
+ return new Mat4([
418
+ 1, 0, 0, 0,
419
+ 0, c, -s, 0,
420
+ 0, s, c, 0,
421
+ 0, 0, 0, 1,
422
+ ])
423
+ }
424
+
425
+ static rotateY(a: number): Mat4 {
426
+ a = deg2rad(-a)
427
+ const c = Math.cos(a)
428
+ const s = Math.sin(a)
429
+ return new Mat4([
430
+ c, 0, s, 0,
431
+ 0, 1, 0, 0,
432
+ -s, 0, c, 0,
433
+ 0, 0, 0, 1,
434
+ ])
435
+ }
436
+
437
+ static rotateZ(a: number): Mat4 {
438
+ a = deg2rad(-a)
439
+ const c = Math.cos(a)
440
+ const s = Math.sin(a)
441
+ return new Mat4([
442
+ c, -s, 0, 0,
443
+ s, c, 0, 0,
444
+ 0, 0, 1, 0,
445
+ 0, 0, 0, 1,
446
+ ])
447
+ }
448
+
449
+ translate(p: Vec2) {
450
+ this.m[12] += this.m[0] * p.x + this.m[4] * p.y
451
+ this.m[13] += this.m[1] * p.x + this.m[5] * p.y
452
+ this.m[14] += this.m[2] * p.x + this.m[6] * p.y
453
+ this.m[15] += this.m[3] * p.x + this.m[7] * p.y
454
+ return this
455
+ }
456
+
457
+ scale(p: Vec2) {
458
+ this.m[0] *= p.x
459
+ this.m[4] *= p.y
460
+ this.m[1] *= p.x
461
+ this.m[5] *= p.y
462
+ this.m[2] *= p.x
463
+ this.m[6] *= p.y
464
+ this.m[3] *= p.x
465
+ this.m[7] *= p.y
466
+ return this
467
+ }
468
+
469
+ rotate(a: number): Mat4 {
470
+ a = deg2rad(-a)
471
+ const c = Math.cos(a)
472
+ const s = Math.sin(a)
473
+ const m0 = this.m[0]
474
+ const m1 = this.m[1]
475
+ const m4 = this.m[4]
476
+ const m5 = this.m[5]
477
+ this.m[0] = m0 * c + m1 * s
478
+ this.m[1] = -m0 * s + m1 * c
479
+ this.m[4] = m4 * c + m5 * s
480
+ this.m[5] = -m4 * s + m5 * c
481
+ return this
482
+ }
483
+
484
+ // TODO: in-place variant
485
+ mult(other: Mat4): Mat4 {
486
+ const out = []
487
+ for (let i = 0; i < 4; i++) {
488
+ for (let j = 0; j < 4; j++) {
489
+ out[i * 4 + j] =
490
+ this.m[0 * 4 + j] * other.m[i * 4 + 0] +
491
+ this.m[1 * 4 + j] * other.m[i * 4 + 1] +
492
+ this.m[2 * 4 + j] * other.m[i * 4 + 2] +
493
+ this.m[3 * 4 + j] * other.m[i * 4 + 3]
494
+ }
495
+ }
496
+ return new Mat4(out)
497
+ }
498
+
499
+ multVec2(p: Vec2): Vec2 {
500
+ return new Vec2(
501
+ p.x * this.m[0] + p.y * this.m[4] + this.m[12],
502
+ p.x * this.m[1] + p.y * this.m[5] + this.m[13],
503
+ )
504
+ }
505
+
506
+ getTranslation() {
507
+ return new Vec2(this.m[12], this.m[13])
508
+ }
509
+
510
+ getScale() {
511
+ if (this.m[0] != 0 || this.m[1] != 0) {
512
+ const det = this.m[0] * this.m[5] - this.m[1] * this.m[4]
513
+ const r = Math.sqrt(this.m[0] * this.m[0] + this.m[1] * this.m[1])
514
+ return new Vec2(r, det / r)
515
+ } else if (this.m[4] != 0 || this.m[5] != 0) {
516
+ const det = this.m[0] * this.m[5] - this.m[1] * this.m[4]
517
+ const s = Math.sqrt(this.m[4] * this.m[4] + this.m[5] * this.m[5])
518
+ return new Vec2(det / s, s)
519
+ } else {
520
+ return new Vec2(0, 0)
521
+ }
522
+ }
523
+
524
+ getRotation() {
525
+ if (this.m[0] != 0 || this.m[1] != 0) {
526
+ const r = Math.sqrt(this.m[0] * this.m[0] + this.m[1] * this.m[1])
527
+ return rad2deg(this.m[1] > 0 ? Math.acos(this.m[0] / r) : -Math.acos(this.m[0] / r))
528
+ } else if (this.m[4] != 0 || this.m[5] != 0) {
529
+ const s = Math.sqrt(this.m[4] * this.m[4] + this.m[5] * this.m[5])
530
+ return rad2deg(Math.PI / 2 - (this.m[5] > 0 ? Math.acos(-this.m[4] / s) : -Math.acos(this.m[4] / s)))
531
+ } else {
532
+ return 0
533
+ }
534
+ }
535
+
536
+ getSkew() {
537
+ if (this.m[0] != 0 || this.m[1] != 0) {
538
+ const r = Math.sqrt(this.m[0] * this.m[0] + this.m[1] * this.m[1])
539
+ return new Vec2(Math.atan(this.m[0] * this.m[4] + this.m[1] * this.m[5]) / (r * r), 0)
540
+ }
541
+ else if (this.m[4] != 0 || this.m[5] != 0) {
542
+ const s = Math.sqrt(this.m[4] * this.m[4] + this.m[5] * this.m[5])
543
+ return new Vec2(0, Math.atan(this.m[0] * this.m[4] + this.m[1] * this.m[5]) / (s * s))
544
+ }
545
+ else {
546
+ return new Vec2(0, 0)
547
+ }
548
+ }
549
+
550
+ invert(): Mat4 {
551
+
552
+ const out = []
553
+
554
+ const f00 = this.m[10] * this.m[15] - this.m[14] * this.m[11]
555
+ const f01 = this.m[9] * this.m[15] - this.m[13] * this.m[11]
556
+ const f02 = this.m[9] * this.m[14] - this.m[13] * this.m[10]
557
+ const f03 = this.m[8] * this.m[15] - this.m[12] * this.m[11]
558
+ const f04 = this.m[8] * this.m[14] - this.m[12] * this.m[10]
559
+ const f05 = this.m[8] * this.m[13] - this.m[12] * this.m[9]
560
+ const f06 = this.m[6] * this.m[15] - this.m[14] * this.m[7]
561
+ const f07 = this.m[5] * this.m[15] - this.m[13] * this.m[7]
562
+ const f08 = this.m[5] * this.m[14] - this.m[13] * this.m[6]
563
+ const f09 = this.m[4] * this.m[15] - this.m[12] * this.m[7]
564
+ const f10 = this.m[4] * this.m[14] - this.m[12] * this.m[6]
565
+ const f11 = this.m[5] * this.m[15] - this.m[13] * this.m[7]
566
+ const f12 = this.m[4] * this.m[13] - this.m[12] * this.m[5]
567
+ const f13 = this.m[6] * this.m[11] - this.m[10] * this.m[7]
568
+ const f14 = this.m[5] * this.m[11] - this.m[9] * this.m[7]
569
+ const f15 = this.m[5] * this.m[10] - this.m[9] * this.m[6]
570
+ const f16 = this.m[4] * this.m[11] - this.m[8] * this.m[7]
571
+ const f17 = this.m[4] * this.m[10] - this.m[8] * this.m[6]
572
+ const f18 = this.m[4] * this.m[9] - this.m[8] * this.m[5]
573
+
574
+ out[0] = this.m[5] * f00 - this.m[6] * f01 + this.m[7] * f02
575
+ out[4] = -(this.m[4] * f00 - this.m[6] * f03 + this.m[7] * f04)
576
+ out[8] = this.m[4] * f01 - this.m[5] * f03 + this.m[7] * f05
577
+ out[12] = -(this.m[4] * f02 - this.m[5] * f04 + this.m[6] * f05)
578
+
579
+ out[1] = -(this.m[1] * f00 - this.m[2] * f01 + this.m[3] * f02)
580
+ out[5] = this.m[0] * f00 - this.m[2] * f03 + this.m[3] * f04
581
+ out[9] = -(this.m[0] * f01 - this.m[1] * f03 + this.m[3] * f05)
582
+ out[13] = this.m[0] * f02 - this.m[1] * f04 + this.m[2] * f05
583
+
584
+ out[2] = this.m[1] * f06 - this.m[2] * f07 + this.m[3] * f08
585
+ out[6] = -(this.m[0] * f06 - this.m[2] * f09 + this.m[3] * f10)
586
+ out[10] = this.m[0] * f11 - this.m[1] * f09 + this.m[3] * f12
587
+ out[14] = -(this.m[0] * f08 - this.m[1] * f10 + this.m[2] * f12)
588
+
589
+ out[3] = -(this.m[1] * f13 - this.m[2] * f14 + this.m[3] * f15)
590
+ out[7] = this.m[0] * f13 - this.m[2] * f16 + this.m[3] * f17
591
+ out[11] = -(this.m[0] * f14 - this.m[1] * f16 + this.m[3] * f18)
592
+ out[15] = this.m[0] * f15 - this.m[1] * f17 + this.m[2] * f18
593
+
594
+ const det =
595
+ this.m[0] * out[0] +
596
+ this.m[1] * out[4] +
597
+ this.m[2] * out[8] +
598
+ this.m[3] * out[12]
599
+
600
+ for (let i = 0; i < 4; i++) {
601
+ for (let j = 0; j < 4; j++) {
602
+ out[i * 4 + j] *= (1.0 / det)
603
+ }
604
+ }
605
+
606
+ return new Mat4(out)
607
+
608
+ }
609
+
610
+ clone(): Mat4 {
611
+ return new Mat4([...this.m])
612
+ }
613
+
614
+ toString(): string {
615
+ return this.m.toString()
616
+ }
617
+
618
+ }
619
+
620
+ export function wave(lo: number, hi: number, t: number, f = (t) => -Math.cos(t)): number {
621
+ return lo + (f(t) + 1) / 2 * (hi - lo)
622
+ }
623
+
624
+ // basic ANSI C LCG
625
+ const A = 1103515245
626
+ const C = 12345
627
+ const M = 2147483648
628
+
629
+ export class RNG {
630
+ seed: number
631
+ constructor(seed: number) {
632
+ this.seed = seed
633
+ }
634
+ gen(): number {
635
+ this.seed = (A * this.seed + C) % M
636
+ return this.seed / M
637
+ }
638
+ genNumber(a: number, b: number): number {
639
+ return a + this.gen() * (b - a)
640
+ }
641
+ genVec2(a: Vec2, b?: Vec2): Vec2 {
642
+ return new Vec2(
643
+ this.genNumber(a.x, b.x),
644
+ this.genNumber(a.y, b.y),
645
+ )
646
+ }
647
+ genColor(a: Color, b: Color): Color {
648
+ return new Color(
649
+ this.genNumber(a.r, b.r),
650
+ this.genNumber(a.g, b.g),
651
+ this.genNumber(a.b, b.b),
652
+ )
653
+ }
654
+ genAny<T = RNGValue>(...args: T[]): T {
655
+ if (args.length === 0) {
656
+ return this.gen() as T
657
+ } else if (args.length === 1) {
658
+ if (typeof args[0] === "number") {
659
+ return this.genNumber(0, args[0]) as T
660
+ } else if (args[0] instanceof Vec2) {
661
+ return this.genVec2(vec2(0, 0), args[0]) as T
662
+ } else if (args[0] instanceof Color) {
663
+ return this.genColor(rgb(0, 0, 0), args[0]) as T
664
+ }
665
+ } else if (args.length === 2) {
666
+ if (typeof args[0] === "number" && typeof args[1] === "number") {
667
+ return this.genNumber(args[0], args[1]) as T
668
+ } else if (args[0] instanceof Vec2 && args[1] instanceof Vec2) {
669
+ return this.genVec2(args[0], args[1]) as T
670
+ } else if (args[0] instanceof Color && args[1] instanceof Color) {
671
+ return this.genColor(args[0], args[1]) as T
672
+ }
673
+ }
674
+ }
675
+ }
676
+
677
+ // TODO: let user pass seed
678
+ const defRNG = new RNG(Date.now())
679
+
680
+ export function randSeed(seed?: number): number {
681
+ if (seed != null) {
682
+ defRNG.seed = seed
683
+ }
684
+ return defRNG.seed
685
+ }
686
+
687
+ export function rand(...args) {
688
+ // @ts-ignore
689
+ return defRNG.genAny(...args)
690
+ }
691
+
692
+ // TODO: randi() to return 0 / 1?
693
+ export function randi(...args: number[]) {
694
+ return Math.floor(rand(...args))
695
+ }
696
+
697
+ export function chance(p: number): boolean {
698
+ return rand() <= p
699
+ }
700
+
701
+ export function choose<T>(list: T[]): T {
702
+ return list[randi(list.length)]
703
+ }
704
+
705
+ // TODO: better name
706
+ export function testRectRect2(r1: Rect, r2: Rect): boolean {
707
+ return r1.pos.x + r1.width >= r2.pos.x
708
+ && r1.pos.x <= r2.pos.x + r2.width
709
+ && r1.pos.y + r1.height >= r2.pos.y
710
+ && r1.pos.y <= r2.pos.y + r2.height
711
+ }
712
+
713
+ export function testRectRect(r1: Rect, r2: Rect): boolean {
714
+ return r1.pos.x + r1.width > r2.pos.x
715
+ && r1.pos.x < r2.pos.x + r2.width
716
+ && r1.pos.y + r1.height > r2.pos.y
717
+ && r1.pos.y < r2.pos.y + r2.height
718
+ }
719
+
720
+ // TODO: better name
721
+ export function testLineLineT(l1: Line, l2: Line): number | null {
722
+
723
+ if ((l1.p1.x === l1.p2.x && l1.p1.y === l1.p2.y) || (l2.p1.x === l2.p2.x && l2.p1.y === l2.p2.y)) {
724
+ return null
725
+ }
726
+
727
+ const denom = ((l2.p2.y - l2.p1.y) * (l1.p2.x - l1.p1.x) - (l2.p2.x - l2.p1.x) * (l1.p2.y - l1.p1.y))
728
+
729
+ // parallel
730
+ if (denom === 0) {
731
+ return null
732
+ }
733
+
734
+ const ua = ((l2.p2.x - l2.p1.x) * (l1.p1.y - l2.p1.y) - (l2.p2.y - l2.p1.y) * (l1.p1.x - l2.p1.x)) / denom
735
+ const ub = ((l1.p2.x - l1.p1.x) * (l1.p1.y - l2.p1.y) - (l1.p2.y - l1.p1.y) * (l1.p1.x - l2.p1.x)) / denom
736
+
737
+ // is the intersection on the segments
738
+ if (ua < 0 || ua > 1 || ub < 0 || ub > 1) {
739
+ return null
740
+ }
741
+
742
+ return ua
743
+
744
+ }
745
+
746
+ export function testLineLine(l1: Line, l2: Line): Vec2 | null {
747
+ const t = testLineLineT(l1, l2)
748
+ if (!t) return null
749
+ return vec2(
750
+ l1.p1.x + t * (l1.p2.x - l1.p1.x),
751
+ l1.p1.y + t * (l1.p2.y - l1.p1.y),
752
+ )
753
+ }
754
+
755
+ export function testRectLine(r: Rect, l: Line): boolean {
756
+ if (testRectPoint(r, l.p1) || testRectPoint(r, l.p2)) {
757
+ return true
758
+ }
759
+ const pts = r.points()
760
+ return !!testLineLine(l, new Line(pts[0], pts[1]))
761
+ || !!testLineLine(l, new Line(pts[1], pts[2]))
762
+ || !!testLineLine(l, new Line(pts[2], pts[3]))
763
+ || !!testLineLine(l, new Line(pts[3], pts[0]))
764
+ }
765
+
766
+ export function testRectPoint2(r: Rect, pt: Point): boolean {
767
+ return pt.x >= r.pos.x
768
+ && pt.x <= r.pos.x + r.width
769
+ && pt.y >= r.pos.y
770
+ && pt.y <= r.pos.y + r.height
771
+ }
772
+
773
+ export function testRectPoint(r: Rect, pt: Point): boolean {
774
+ return pt.x > r.pos.x
775
+ && pt.x < r.pos.x + r.width
776
+ && pt.y > r.pos.y
777
+ && pt.y < r.pos.y + r.height
778
+ }
779
+
780
+ export function testRectCircle(r: Rect, c: Circle): boolean {
781
+ const nx = Math.max(r.pos.x, Math.min(c.center.x, r.pos.x + r.width))
782
+ const ny = Math.max(r.pos.y, Math.min(c.center.y, r.pos.y + r.height))
783
+ const nearestPoint = vec2(nx, ny)
784
+ return nearestPoint.sdist(c.center) <= c.radius * c.radius
785
+ }
786
+
787
+ export function testRectPolygon(r: Rect, p: Polygon): boolean {
788
+ return testPolygonPolygon(p, new Polygon(r.points()))
789
+ }
790
+
791
+ export function testLinePoint(l: Line, pt: Vec2): boolean {
792
+ const v1 = pt.sub(l.p1)
793
+ const v2 = l.p2.sub(l.p1)
794
+
795
+ // Check if sine is 0, in that case lines are parallel.
796
+ // If not parallel, the point cannot lie on the line.
797
+ if (Math.abs(v1.cross(v2)) > Number.EPSILON) {
798
+ return false
799
+ }
800
+
801
+ // Scalar projection of v1 on v2
802
+ const t = v1.dot(v2) / v2.dot(v2)
803
+ // Since t is percentual distance of pt from line.p1 on the line,
804
+ // it should be between 0% and 100%
805
+ return t >= 0 && t <= 1
806
+ }
807
+
808
+ export function testLineCircle(l: Line, circle: Circle): boolean {
809
+ const v = l.p2.sub(l.p1)
810
+ const a = v.dot(v)
811
+ const centerToOrigin = l.p1.sub(circle.center)
812
+ const b = 2 * v.dot(centerToOrigin)
813
+ const c = centerToOrigin.dot(centerToOrigin) - circle.radius * circle.radius
814
+ // Calculate the discriminant of ax^2 + bx + c
815
+ const dis = b * b - 4 * a * c
816
+
817
+ // No root
818
+ if ((a <= Number.EPSILON) || (dis < 0)) {
819
+ return false
820
+ }
821
+ // One possible root
822
+ else if (dis == 0) {
823
+ const t = -b / (2 * a)
824
+ if (t >= 0 && t <= 1) {
825
+ return true
826
+ }
827
+ }
828
+ // Two possible roots
829
+ else {
830
+ const t1 = (-b + Math.sqrt(dis)) / (2 * a)
831
+ const t2 = (-b - Math.sqrt(dis)) / (2 * a)
832
+ if ((t1 >= 0 && t1 <= 1) || (t2 >= 0 && t2 <= 1)) {
833
+ return true
834
+ }
835
+ }
836
+
837
+ // Check if line is completely within the circle
838
+ // We only need to check one point, since the line didn't cross the circle
839
+ return testCirclePoint(circle, l.p1)
840
+ }
841
+
842
+ export function testLinePolygon(l: Line, p: Polygon): boolean {
843
+
844
+ // test if line is inside
845
+ if (testPolygonPoint(p, l.p1) || testPolygonPoint(p, l.p2)) {
846
+ return true
847
+ }
848
+
849
+ // test each line
850
+ for (let i = 0; i < p.pts.length; i++) {
851
+ const p1 = p.pts[i]
852
+ const p2 = p.pts[(i + 1) % p.pts.length]
853
+ if (testLineLine(l, new Line(p1, p2))) {
854
+ return true
855
+ }
856
+ }
857
+
858
+ return false
859
+
860
+ }
861
+
862
+ export function testCirclePoint(c: Circle, p: Point): boolean {
863
+ return c.center.sdist(p) < c.radius * c.radius
864
+ }
865
+
866
+ export function testCircleCircle(c1: Circle, c2: Circle): boolean {
867
+ return c1.center.sdist(c2.center) < (c1.radius + c2.radius) * (c1.radius + c2.radius)
868
+ }
869
+
870
+ export function testCirclePolygon(c: Circle, p: Polygon): boolean {
871
+ // For each edge check for intersection
872
+ let prev = p.pts[p.pts.length - 1]
873
+ for (const cur of p.pts) {
874
+ if (testLineCircle(new Line(prev, cur), c)) {
875
+ return true
876
+ }
877
+ prev = cur
878
+ }
879
+
880
+ // Check if the polygon is completely within the circle
881
+ // We only need to check one point, since the polygon didn't cross the circle
882
+ if (testCirclePoint(c, p.pts[0])) {
883
+ return true
884
+ }
885
+
886
+ // Check if the circle is completely within the polygon
887
+ return testPolygonPoint(p, c.center)
888
+ }
889
+
890
+ export function testPolygonPolygon(p1: Polygon, p2: Polygon): boolean {
891
+ for (let i = 0; i < p1.pts.length; i++) {
892
+ if (testLinePolygon(new Line(p1.pts[i], p1.pts[(i + 1) % p1.pts.length]), p2)) {
893
+ return true
894
+ }
895
+ }
896
+ return false
897
+ }
898
+
899
+ // https://wrf.ecse.rpi.edu/Research/Short_Notes/pnpoly.html
900
+ export function testPolygonPoint(poly: Polygon, pt: Point): boolean {
901
+
902
+ let c = false
903
+ const p = poly.pts
904
+
905
+ for (let i = 0, j = p.length - 1; i < p.length; j = i++) {
906
+ if (
907
+ ((p[i].y > pt.y) != (p[j].y > pt.y))
908
+ && (pt.x < (p[j].x - p[i].x) * (pt.y - p[i].y) / (p[j].y - p[i].y) + p[i].x)
909
+ ) {
910
+ c = !c
911
+ }
912
+ }
913
+
914
+ return c
915
+
916
+ }
917
+
918
+ export function testPointPoint(p1: Point, p2: Point): boolean {
919
+ return p1.x === p2.x && p1.y === p2.y
920
+ }
921
+
922
+ export class Line {
923
+ p1: Vec2
924
+ p2: Vec2
925
+ constructor(p1: Vec2, p2: Vec2) {
926
+ this.p1 = p1.clone()
927
+ this.p2 = p2.clone()
928
+ }
929
+ transform(m: Mat4): Line {
930
+ return new Line(m.multVec2(this.p1), m.multVec2(this.p2))
931
+ }
932
+ bbox(): Rect {
933
+ return Rect.fromPoints(this.p1, this.p2)
934
+ }
935
+ area(): number {
936
+ return this.p1.dist(this.p2)
937
+ }
938
+ clone(): Line {
939
+ return new Line(this.p1, this.p2)
940
+ }
941
+ }
942
+
943
+ // TODO: use x: number y: number (x, y, width, height)
944
+ export class Rect {
945
+ pos: Vec2
946
+ width: number
947
+ height: number
948
+ constructor(pos: Vec2, width: number, height: number) {
949
+ this.pos = pos.clone()
950
+ this.width = width
951
+ this.height = height
952
+ }
953
+ static fromPoints(p1: Vec2, p2: Vec2): Rect {
954
+ return new Rect(p1.clone(), p2.x - p1.x, p2.y - p1.y)
955
+ }
956
+ center(): Vec2 {
957
+ return new Vec2(this.pos.x + this.width / 2, this.pos.y + this.height / 2)
958
+ }
959
+ points(): [Vec2, Vec2, Vec2, Vec2] {
960
+ return [
961
+ this.pos,
962
+ this.pos.add(this.width, 0),
963
+ this.pos.add(this.width, this.height),
964
+ this.pos.add(0, this.height),
965
+ ]
966
+ }
967
+ transform(m: Mat4): Polygon {
968
+ return new Polygon(this.points().map((pt) => m.multVec2(pt)))
969
+ }
970
+ bbox(): Rect {
971
+ return this.clone()
972
+ }
973
+ area(): number {
974
+ return this.width * this.height
975
+ }
976
+ clone(): Rect {
977
+ return new Rect(this.pos.clone(), this.width, this.height)
978
+ }
979
+ distToPoint(p: Vec2): number {
980
+ return Math.sqrt(this.sdistToPoint(p))
981
+ }
982
+ sdistToPoint(p: Vec2): number {
983
+ const min = this.pos
984
+ const max = this.pos.add(this.width, this.height)
985
+ const dx = Math.max(min.x - p.x, 0, p.x - max.x)
986
+ const dy = Math.max(min.y - p.y, 0, p.y - max.y)
987
+ return dx * dx + dy * dy
988
+ }
989
+ }
990
+
991
+ export class Circle {
992
+ center: Vec2
993
+ radius: number
994
+ constructor(center: Vec2, radius: number) {
995
+ this.center = center.clone()
996
+ this.radius = radius
997
+ }
998
+ transform(tr: Mat4): Ellipse {
999
+ return new Ellipse(this.center, this.radius, this.radius).transform(tr)
1000
+ }
1001
+ bbox(): Rect {
1002
+ return Rect.fromPoints(
1003
+ this.center.sub(vec2(this.radius)),
1004
+ this.center.add(vec2(this.radius)),
1005
+ )
1006
+ }
1007
+ area(): number {
1008
+ return this.radius * this.radius * Math.PI
1009
+ }
1010
+ clone(): Circle {
1011
+ return new Circle(this.center, this.radius)
1012
+ }
1013
+ }
1014
+
1015
+ export class Ellipse {
1016
+ center: Vec2
1017
+ radiusX: number
1018
+ radiusY: number
1019
+ constructor(center: Vec2, rx: number, ry: number) {
1020
+ this.center = center.clone()
1021
+ this.radiusX = rx
1022
+ this.radiusY = ry
1023
+ }
1024
+ transform(tr: Mat4): Ellipse {
1025
+ return new Ellipse(
1026
+ tr.multVec2(this.center),
1027
+ tr.m[0] * this.radiusX,
1028
+ tr.m[5] * this.radiusY,
1029
+ )
1030
+ }
1031
+ bbox(): Rect {
1032
+ return Rect.fromPoints(
1033
+ this.center.sub(vec2(this.radiusX, this.radiusY)),
1034
+ this.center.add(vec2(this.radiusX, this.radiusY)),
1035
+ )
1036
+ }
1037
+ area(): number {
1038
+ return this.radiusX * this.radiusY * Math.PI
1039
+ }
1040
+ clone(): Ellipse {
1041
+ return new Ellipse(this.center, this.radiusX, this.radiusY)
1042
+ }
1043
+ }
1044
+
1045
+ export class Polygon {
1046
+ pts: Vec2[]
1047
+ constructor(pts: Vec2[]) {
1048
+ if (pts.length < 3) {
1049
+ throw new Error("Polygons should have at least 3 vertices")
1050
+ }
1051
+ this.pts = pts
1052
+ }
1053
+ transform(m: Mat4): Polygon {
1054
+ return new Polygon(this.pts.map((pt) => m.multVec2(pt)))
1055
+ }
1056
+ bbox(): Rect {
1057
+ const p1 = vec2(Number.MAX_VALUE)
1058
+ const p2 = vec2(-Number.MAX_VALUE)
1059
+ for (const pt of this.pts) {
1060
+ p1.x = Math.min(p1.x, pt.x)
1061
+ p2.x = Math.max(p2.x, pt.x)
1062
+ p1.y = Math.min(p1.y, pt.y)
1063
+ p2.y = Math.max(p2.y, pt.y)
1064
+ }
1065
+ return Rect.fromPoints(p1, p2)
1066
+ }
1067
+ area(): number {
1068
+ let total = 0
1069
+ const l = this.pts.length
1070
+ for (let i = 0; i < l; i++) {
1071
+ const p1 = this.pts[i]
1072
+ const p2 = this.pts[(i + 1) % l]
1073
+ total += (p1.x * p2.y * 0.5)
1074
+ total -= (p2.x * p1.y * 0.5)
1075
+ }
1076
+ return Math.abs(total)
1077
+ }
1078
+ clone(): Polygon {
1079
+ return new Polygon(this.pts.map((pt) => pt.clone()))
1080
+ }
1081
+ }
1082
+
1083
+ export function sat(p1: Polygon, p2: Polygon): Vec2 | null {
1084
+ let overlap = Number.MAX_VALUE
1085
+ let displacement = vec2(0)
1086
+ for (const poly of [p1, p2]) {
1087
+ for (let i = 0; i < poly.pts.length; i++) {
1088
+ const a = poly.pts[i]
1089
+ const b = poly.pts[(i + 1) % poly.pts.length]
1090
+ const axisProj = b.sub(a).normal().unit()
1091
+ let min1 = Number.MAX_VALUE
1092
+ let max1 = -Number.MAX_VALUE
1093
+ for (let j = 0; j < p1.pts.length; j++) {
1094
+ const q = p1.pts[j].dot(axisProj)
1095
+ min1 = Math.min(min1, q)
1096
+ max1 = Math.max(max1, q)
1097
+ }
1098
+ let min2 = Number.MAX_VALUE
1099
+ let max2 = -Number.MAX_VALUE
1100
+ for (let j = 0; j < p2.pts.length; j++) {
1101
+ const q = p2.pts[j].dot(axisProj)
1102
+ min2 = Math.min(min2, q)
1103
+ max2 = Math.max(max2, q)
1104
+ }
1105
+ const o = Math.min(max1, max2) - Math.max(min1, min2)
1106
+ if (o < 0) {
1107
+ return null
1108
+ }
1109
+ if (o < Math.abs(overlap)) {
1110
+ const o1 = max2 - min1
1111
+ const o2 = min2 - max1
1112
+ overlap = Math.abs(o1) < Math.abs(o2) ? o1 : o2
1113
+ displacement = axisProj.scale(overlap)
1114
+ }
1115
+ }
1116
+ }
1117
+ return displacement
1118
+ }