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/CHANGELOG.md +1050 -0
- package/LICENSE +19 -0
- package/README.md +209 -0
- package/dist/global.d.ts +237 -0
- package/dist/global.js +0 -0
- package/dist/kaboom.cjs +56 -0
- package/dist/kaboom.cjs.map +7 -0
- package/dist/kaboom.d.ts +5364 -0
- package/dist/kaboom.js +57 -0
- package/dist/kaboom.js.map +7 -0
- package/dist/kaboom.mjs +56 -0
- package/dist/kaboom.mjs.map +7 -0
- package/package.json +62 -0
- package/src/app.ts +906 -0
- package/src/assets/bean.png +0 -0
- package/src/assets/boom.png +0 -0
- package/src/assets/burp.mp3 +0 -0
- package/src/assets/index.d.ts +9 -0
- package/src/assets/ka.png +0 -0
- package/src/assets.ts +131 -0
- package/src/easings.ts +94 -0
- package/src/gamepad.json +111 -0
- package/src/gfx.ts +524 -0
- package/src/kaboom.ts +6539 -0
- package/src/math.ts +1118 -0
- package/src/texPacker.ts +73 -0
- package/src/types.ts +5364 -0
- package/src/utils.ts +525 -0
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
|
+
}
|