web based infinite canvas
at main 360 lines 8.3 kB view raw
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};