web based infinite canvas
1export type Vec2 = { x: number; y: number };
2
3export const Vec2 = {
4 /**
5 * Add two vectors
6 */
7 add(a: Vec2, b: Vec2): Vec2 {
8 return { x: a.x + b.x, y: a.y + b.y };
9 },
10
11 /**
12 * Subtract vector b from vector a
13 */
14 sub(a: Vec2, b: Vec2): Vec2 {
15 return { x: a.x - b.x, y: a.y - b.y };
16 },
17
18 /**
19 * Multiply vector by scalar
20 */
21 mulScalar(v: Vec2, s: number): Vec2 {
22 return { x: v.x * s, y: v.y * s };
23 },
24
25 /**
26 * Calculate length (magnitude) of vector
27 */
28 len(v: Vec2): number {
29 return Math.hypot(v.x, v.y);
30 },
31
32 /**
33 * Calculate squared length (faster, no sqrt)
34 */
35 lenSq(v: Vec2): number {
36 return v.x * v.x + v.y * v.y;
37 },
38
39 /**
40 * Normalize vector to unit length
41 * Returns zero vector if input length is zero
42 */
43 normalize(v: Vec2): Vec2 {
44 const length = Vec2.len(v);
45 if (length === 0) {
46 return { x: 0, y: 0 };
47 }
48 return { x: v.x / length, y: v.y / length };
49 },
50
51 /**
52 * Calculate dot product of two vectors
53 */
54 dot(a: Vec2, b: Vec2): number {
55 return a.x * b.x + a.y * b.y;
56 },
57
58 /**
59 * Calculate distance between two points
60 */
61 dist(a: Vec2, b: Vec2): number {
62 return Vec2.len(Vec2.sub(a, b));
63 },
64
65 /**
66 * Calculate squared distance (faster, no sqrt)
67 */
68 distSq(a: Vec2, b: Vec2): number {
69 return Vec2.lenSq(Vec2.sub(a, b));
70 },
71
72 /**
73 * Check if two vectors are approximately equal
74 */
75 equals(a: Vec2, b: Vec2, epsilon = 1e-10): boolean {
76 return Math.abs(a.x - b.x) <= epsilon && Math.abs(a.y - b.y) <= epsilon;
77 },
78
79 /**
80 * Create a new vector
81 */
82 create(x: number, y: number): Vec2 {
83 return { x, y };
84 },
85
86 /**
87 * Clone a vector
88 */
89 clone(v: Vec2): Vec2 {
90 return { x: v.x, y: v.y };
91 },
92};
93
94export type Box2 = { min: Vec2; max: Vec2 };
95
96export const Box2 = {
97 /**
98 * Create a bounding box from an array of points
99 */
100 fromPoints(points: Vec2[]): Box2 {
101 if (points.length === 0) {
102 return { min: { x: 0, y: 0 }, max: { x: 0, y: 0 } };
103 }
104
105 let minX = points[0].x;
106 let minY = points[0].y;
107 let maxX = points[0].x;
108 let maxY = points[0].y;
109
110 for (let index = 1; index < points.length; index++) {
111 const p = points[index];
112 if (p.x < minX) minX = p.x;
113 if (p.y < minY) minY = p.y;
114 if (p.x > maxX) maxX = p.x;
115 if (p.y > maxY) maxY = p.y;
116 }
117
118 return { min: { x: minX, y: minY }, max: { x: maxX, y: maxY } };
119 },
120
121 /**
122 * Create a box from center and size
123 */
124 fromCenterSize(center: Vec2, width: number, height: number): Box2 {
125 const halfW = width / 2;
126 const halfH = height / 2;
127 return { min: { x: center.x - halfW, y: center.y - halfH }, max: { x: center.x + halfW, y: center.y + halfH } };
128 },
129
130 /**
131 * Create a box from min/max coordinates
132 */
133 create(minX: number, minY: number, maxX: number, maxY: number): Box2 {
134 return { min: { x: minX, y: minY }, max: { x: maxX, y: maxY } };
135 },
136
137 /**
138 * Check if a point is inside the box
139 */
140 containsPoint(box: Box2, point: Vec2): boolean {
141 return (point.x >= box.min.x && point.x <= box.max.x && point.y >= box.min.y && point.y <= box.max.y);
142 },
143
144 /**
145 * Check if two boxes intersect
146 */
147 intersectsBox(a: Box2, b: Box2): boolean {
148 return !(a.max.x < b.min.x || a.min.x > b.max.x || a.max.y < b.min.y || a.min.y > b.max.y);
149 },
150
151 /**
152 * Check if box a completely contains box b
153 */
154 containsBox(a: Box2, b: Box2): boolean {
155 return (b.min.x >= a.min.x && b.max.x <= a.max.x && b.min.y >= a.min.y && b.max.y <= a.max.y);
156 },
157
158 /**
159 * Get the width of the box
160 */
161 width(box: Box2): number {
162 return box.max.x - box.min.x;
163 },
164
165 /**
166 * Get the height of the box
167 */
168 height(box: Box2): number {
169 return box.max.y - box.min.y;
170 },
171
172 /**
173 * Get the center point of the box
174 */
175 center(box: Box2): Vec2 {
176 return { x: (box.min.x + box.max.x) / 2, y: (box.min.y + box.max.y) / 2 };
177 },
178
179 /**
180 * Get the area of the box
181 */
182 area(box: Box2): number {
183 return Box2.width(box) * Box2.height(box);
184 },
185
186 /**
187 * Expand box to include a point
188 */
189 expandToPoint(box: Box2, point: Vec2): Box2 {
190 return {
191 min: { x: Math.min(box.min.x, point.x), y: Math.min(box.min.y, point.y) },
192 max: { x: Math.max(box.max.x, point.x), y: Math.max(box.max.y, point.y) },
193 };
194 },
195
196 /**
197 * Clone a box
198 */
199 clone(box: Box2): Box2 {
200 return { min: { ...box.min }, max: { ...box.max } };
201 },
202};
203
204/**
205 * 3x3 matrix stored in column-major order for 2D affine transforms
206 * Layout:
207 * [a c tx]
208 * [b d ty]
209 * [0 0 1]
210 *
211 * Stored as: [a, b, 0, c, d, 0, tx, ty, 1]
212 */
213export type Mat3 = [number, number, number, number, number, number, number, number, number];
214
215export const Mat3 = {
216 /**
217 * Create an identity matrix
218 */
219 identity(): Mat3 {
220 return [1, 0, 0, 0, 1, 0, 0, 0, 1];
221 },
222
223 /**
224 * Create a translation matrix
225 */
226 translate(tx: number, ty: number): Mat3 {
227 return [1, 0, 0, 0, 1, 0, tx, ty, 1];
228 },
229
230 /**
231 * Create a scale matrix
232 */
233 scale(sx: number, sy: number): Mat3 {
234 return [sx, 0, 0, 0, sy, 0, 0, 0, 1];
235 },
236
237 /**
238 * Create a rotation matrix
239 * @param theta - angle in radians
240 */
241 rotate(theta: number): Mat3 {
242 const c = Math.cos(theta);
243 const s = Math.sin(theta);
244 return [c, s, 0, -s, c, 0, 0, 0, 1];
245 },
246
247 /**
248 * Multiply two matrices: result = a * b
249 * Order matters: transformations are applied right to left
250 */
251 multiply(a: Mat3, b: Mat3): Mat3 {
252 const a00 = a[0], a01 = a[1], a02 = a[2];
253 const a10 = a[3], a11 = a[4], a12 = a[5];
254 const a20 = a[6], a21 = a[7], a22 = a[8];
255
256 const b00 = b[0], b01 = b[1], b02 = b[2];
257 const b10 = b[3], b11 = b[4], b12 = b[5];
258 const b20 = b[6], b21 = b[7], b22 = b[8];
259
260 return [
261 a00 * b00 + a10 * b01 + a20 * b02,
262 a01 * b00 + a11 * b01 + a21 * b02,
263 a02 * b00 + a12 * b01 + a22 * b02,
264
265 a00 * b10 + a10 * b11 + a20 * b12,
266 a01 * b10 + a11 * b11 + a21 * b12,
267 a02 * b10 + a12 * b11 + a22 * b12,
268
269 a00 * b20 + a10 * b21 + a20 * b22,
270 a01 * b20 + a11 * b21 + a21 * b22,
271 a02 * b20 + a12 * b21 + a22 * b22,
272 ];
273 },
274
275 /**
276 * Transform a point by a matrix
277 */
278 transformPoint(m: Mat3, p: Vec2): Vec2 {
279 const x = m[0] * p.x + m[3] * p.y + m[6];
280 const y = m[1] * p.x + m[4] * p.y + m[7];
281 return { x, y };
282 },
283
284 /**
285 * Invert a matrix
286 * Returns null if matrix is not invertible
287 */
288 invert(m: Mat3): Mat3 | null {
289 const a00 = m[0], a01 = m[1], a02 = m[2];
290 const a10 = m[3], a11 = m[4], a12 = m[5];
291 const a20 = m[6], a21 = m[7], a22 = m[8];
292
293 const b01 = a22 * a11 - a12 * a21;
294 const b11 = -a22 * a10 + a12 * a20;
295 const b21 = a21 * a10 - a11 * a20;
296
297 const det = a00 * b01 + a01 * b11 + a02 * b21;
298
299 if (Math.abs(det) < 1e-10) {
300 return null;
301 }
302
303 const invDet = 1 / det;
304
305 return [
306 b01 * invDet,
307 (-a22 * a01 + a02 * a21) * invDet,
308 (a12 * a01 - a02 * a11) * invDet,
309
310 b11 * invDet,
311 (a22 * a00 - a02 * a20) * invDet,
312 (-a12 * a00 + a02 * a10) * invDet,
313
314 b21 * invDet,
315 (-a21 * a00 + a01 * a20) * invDet,
316 (a11 * a00 - a01 * a10) * invDet,
317 ];
318 },
319
320 /**
321 * Get the determinant of a matrix
322 */
323 determinant(m: Mat3): number {
324 const a00 = m[0], a01 = m[1], a02 = m[2];
325 const a10 = m[3], a11 = m[4], a12 = m[5];
326 const a20 = m[6], a21 = m[7], a22 = m[8];
327
328 return (a00 * (a22 * a11 - a12 * a21) + a01 * (-a22 * a10 + a12 * a20) + a02 * (a21 * a10 - a11 * a20));
329 },
330
331 /**
332 * Clone a matrix
333 */
334 clone(m: Mat3): Mat3 {
335 return [...m] as Mat3;
336 },
337
338 /**
339 * Check if two matrices are approximately equal
340 */
341 equals(a: Mat3, b: Mat3, epsilon = 1e-10): boolean {
342 for (let index = 0; index < 9; index++) {
343 if (Math.abs(a[index] - b[index]) >= epsilon) {
344 return false;
345 }
346 }
347 return true;
348 },
349
350 /**
351 * Create a combined transform matrix
352 * Applies in order: translate -> rotate -> scale
353 */
354 fromTransform(tx: number, ty: number, rotation: number, sx: number, sy: number): Mat3 {
355 const c = Math.cos(rotation);
356 const s = Math.sin(rotation);
357
358 return [c * sx, s * sx, 0, -s * sy, c * sy, 0, tx, ty, 1];
359 },
360};