UNPKG

@kajws/galaxy-js

Version:

A JavaScript port of the classic galaxy screensaver with 3D particle simulation

405 lines (404 loc) 12.9 kB
class S { constructor(t, s, n, o) { if (this.stars = [], this.data = {}, !t) throw new Error("Velocity is required"); if (!s) throw new Error("Position is required"); if (!n) throw new Error("Rotation is required"); this.vel = t, this.pos = s, this.rotation = n, this.mass = o; } } class E { constructor(t, s, n = 0) { if (this.data = {}, !t) throw new Error("Position is required"); if (!s) throw new Error("Velocity is required"); this.pos = t, this.vel = s, this.mass = n; } } const q = (d, t) => { for (const s of d) for (let n = 0; n < s.stars.length; n++) t(s.stars[n], s, n); }, b = Math.PI * 2, p = () => Math.random() * b; class r { /** * Creates a new 3D vector. * * @param x - X coordinate (default: 0) * @param y - Y coordinate (default: 0) * @param z - Z coordinate (default: 0) */ constructor(t = 0, s = 0, n = 0) { this.x = t, this.y = s, this.z = n; } /** * Creates a new 3D vector with all components set to 0. * * @returns A new vector with all components set to 0 */ static zero() { return new r(0, 0, 0); } /** * Creates a random vector with components between 0 and 1, then scales it. * * This is useful for generating random positions or directions in 3D space. * Each component is a random value between 0 and 1, then multiplied by the factor. * * @param factor - Scaling factor to multiply the random components by (default: 1) * @returns A new random vector */ static random(t = 1) { return new r(Math.random(), Math.random(), Math.random()).mul(t); } /** * Creates a random vector centered around the origin. * * This generates a random vector with components between -0.5 and 0.5, * then scales it by the factor. This is useful for generating random * positions around a center point. * * @param factor - Scaling factor to multiply the centered random components by (default: 1) * @returns A new random centered vector */ static randomCentered(t = 1) { return r.random().sub(0.5).mul(t); } /** * Calculates the magnitude (length) of this vector. * * The magnitude is the distance from the origin to the point represented by this vector. * It's calculated using the Pythagorean theorem: √(x² + y² + z²) * * @returns The magnitude (length) of the vector */ get magnitude() { return Math.sqrt(this.x ** 2 + this.y ** 2 + this.z ** 2); } /** * Creates a copy of this vector. * * Since vectors are immutable, this is useful when you need to * create a new vector with the same values as an existing one. * * @returns A new vector with the same x, y, z values */ copy() { return new r(this.x, this.y, this.z); } /** * Subtracts another vector or scalar from this vector. * * Vector subtraction: (x1, y1, z1) - (x2, y2, z2) = (x1-x2, y1-y2, z1-z2) * Scalar subtraction: (x, y, z) - s = (x-s, y-s, z-s) * * @param other - Vector or scalar to subtract * @returns A new vector representing the difference */ sub(t) { return typeof t == "number" ? new r(this.x - t, this.y - t, this.z - t) : new r(this.x - t.x, this.y - t.y, this.z - t.z); } /** * Adds another vector or scalar to this vector. * * Vector addition: (x1, y1, z1) + (x2, y2, z2) = (x1+x2, y1+y2, z1+z2) * Scalar addition: (x, y, z) + s = (x+s, y+s, z+s) * * @param other - Vector or scalar to add * @returns A new vector representing the sum */ add(t) { return typeof t == "number" ? new r(this.x + t, this.y + t, this.z + t) : new r(this.x + t.x, this.y + t.y, this.z + t.z); } /** * Multiplies this vector by another vector or scalar. * * Vector multiplication (component-wise): (x1, y1, z1) * (x2, y2, z2) = (x1*x2, y1*y2, z1*z2) * Scalar multiplication: (x, y, z) * s = (x*s, y*s, z*s) * * Note: This is component-wise multiplication, not the dot product. * For dot product, you would use: vec1.x * vec2.x + vec1.y * vec2.y + vec1.z * vec2.z * * @param other - Vector or scalar to multiply by * @returns A new vector representing the product */ mul(t) { return typeof t == "number" ? new r(this.x * t, this.y * t, this.z * t) : new r(this.x * t.x, this.y * t.y, this.z * t.z); } /** * Divides this vector by another vector or scalar. * * Vector division (component-wise): (x1, y1, z1) / (x2, y2, z2) = (x1/x2, y1/y2, z1/z2) * Scalar division: (x, y, z) / s = (x/s, y/s, z/s) * * @param other - Vector or scalar to divide by * @returns A new vector representing the quotient */ div(t) { return typeof t == "number" ? new r(this.x / t, this.y / t, this.z / t) : new r(this.x / t.x, this.y / t.y, this.z / t.z); } /** * Normalizes this vector (makes it a unit vector). * * A normalized vector has a magnitude of 1 and points in the same direction as the original. * This is useful when you need a direction vector without caring about magnitude. * * Formula: normalized = vector / magnitude * * @returns A new normalized vector (magnitude = 1) */ normalize() { return this.div(this.magnitude); } } class u { /** * Creates a new 3x3 matrix from the provided data. * * @param data - A 2D array representing the matrix values * @throws Error if the data doesn't form a valid 3x3 matrix */ constructor(t) { if (t.length !== 3) throw new Error(`Matrix must have 3 rows, got ${t.length}`); for (let s = 0; s < t.length; s++) if (t[s].length !== 3) throw new Error(`Row ${s} must have 3 columns, got ${t[s].length}`); this.data = t.map((s) => [...s]); } /** * Creates the identity matrix. * * The identity matrix is a special matrix that: * - Has 1s on the main diagonal (top-left to bottom-right) * - Has 0s everywhere else * - When multiplied by any vector, returns the same vector unchanged * * Identity matrix structure: * [1 0 0] * [0 1 0] * [0 0 1] * * @returns A new identity matrix */ static identity() { return new u([ [1, 0, 0], // First row: [1, 0, 0] [0, 1, 0], // Second row: [0, 1, 0] [0, 0, 1] // Third row: [0, 0, 1] ]); } /** * Creates a rotation matrix from Euler angles. * * This method creates a rotation matrix that combines: * - x: Rotation around the x-axis (roll) * - y: Rotation around the y-axis (pitch) * - z: Rotation around the z-axis (yaw) * * The resulting matrix can be used to rotate 3D vectors in space. * * @param x - Rotation angle around x-axis in radians * @param y - Rotation angle around y-axis in radians * @param z - Rotation angle around z-axis in radians * @returns A new rotation matrix */ static fromEuler(t, s, n) { const o = Math.cos(t), a = Math.sin(t), e = Math.cos(s), c = Math.sin(s), i = Math.cos(n), h = Math.sin(n); return new u([ [i * e, i * c * a - h * o, i * c * o + h * a], [h * e, h * c * a + i * o, h * c * o - i * a], [-c, e * a, e * o] ]); } /** * Creates a random rotation matrix. * * This is useful for generating random orientations in 3D space, * such as for stars or other objects in a galaxy simulation. * * @returns A new matrix with random rotation angles */ static randomRotation() { const t = p(), s = p(), n = p(); return u.fromEuler(t, s, n); } /** * Creates a rotation matrix from an axis and an angle. * * @param axis - The axis of rotation * @param angle - The angle of rotation in radians * @returns A new rotation matrix */ static fromAxisAngle(t, s) { const n = t.x, o = t.y, a = t.z, e = Math.cos(s), c = Math.sin(s), i = 1 - e, h = i * n * n + e, f = i * n * o - a * c, y = i * n * a + o * c, m = i * o * n + a * c, w = i * o * o + e, l = i * o * a - n * c, x = i * a * n - o * c, M = i * a * o + n * c, P = i * a * a + e; return new u([ [h, f, y], [m, w, l], [x, M, P] ]); } /** * Transforms a 3D vector by multiplying it with this matrix. * * Matrix-vector multiplication formula: * result.x = matrix[0][0] * vec.x + matrix[1][0] * vec.y + matrix[2][0] * vec.z * result.y = matrix[0][1] * vec.x + matrix[1][1] * vec.y + matrix[2][1] * vec.z * result.z = matrix[0][2] * vec.x + matrix[1][2] * vec.y + matrix[2][2] * vec.z * * @param vec - The 3D vector to transform * @returns A new transformed vector */ transform(t) { return new r( // X component: dot product of first matrix column with vector this.data[0][0] * t.x + this.data[1][0] * t.y + this.data[2][0] * t.z, // Y component: dot product of second matrix column with vector this.data[0][1] * t.x + this.data[1][1] * t.y + this.data[2][1] * t.z, // Z component: dot product of third matrix column with vector this.data[0][2] * t.x + this.data[1][2] * t.y + this.data[2][2] * t.z ); } /** * Multiplies this matrix by another matrix. * * Matrix multiplication is not commutative (A * B ≠ B * A). * The result represents the combined effect of applying both transformations. * * Matrix multiplication formula: * result[i][j] = sum(k=0 to 2) of this[i][k] * other[k][j] * * @param other - The matrix to multiply with * @returns A new matrix representing the product */ mul(t) { const s = []; for (let n = 0; n < 3; n++) { s[n] = []; for (let o = 0; o < 3; o++) { s[n][o] = 0; for (let a = 0; a < 3; a++) s[n][o] += this.data[n][a] * t.data[a][o]; } } return new u(s); } /** * Gets the element at the specified row and column position. * * Matrix indexing is zero-based: * - Row 0: top row * - Row 1: middle row * - Row 2: bottom row * - Column 0: left column * - Column 1: middle column * - Column 2: right column * * @param row - Row index (0-2) * @param col - Column index (0-2) * @returns The matrix element at the specified position */ get(t, s) { return this.data[t][s]; } /** * Sets the element at the specified row and column position. * * @param row - Row index (0-2) * @param col - Column index (0-2) * @param value - The value to set at the specified position */ set(t, s, n) { this.data[t][s] = n; } /** * Returns a copy of the matrix data as a 2D array. * * This creates a deep copy, so modifying the returned array * won't affect the original matrix. * * @returns A 2D array representation of the matrix */ toArray() { return this.data.map((t) => [...t]); } /** * Converts the matrix to Euler angles. * * @returns A new vector with the Euler angles */ toEuler() { return new r( Math.atan2(this.data[2][1], this.data[2][2]), Math.asin(-this.data[2][0]), Math.atan2(this.data[1][0], this.data[0][0]) ); } } const z = 5e-3, g = 1e-3, C = (d) => { q(d, (t) => { let s = r.zero(); for (const o of d) { const a = o.pos.sub(t.pos), e = a.magnitude ** 2; if (e === 0) continue; const c = Math.sqrt(e), i = g * o.mass / (c * e), h = a.mul(i); s = s.add(h); } const n = s.mul(z); t.vel = t.vel.add(n), t.pos = t.pos.add(t.vel.mul(z)); }); for (const t of d) { let s = r.zero(); for (const o of d) { if (t === o) continue; const a = o.pos.sub(t.pos), e = a.magnitude ** 2; if (e === 0) continue; const c = Math.sqrt(e), i = g * o.mass / (c * e), h = a.mul(i); s = s.add(h); } const n = s.mul(z); t.vel = t.vel.add(n); } for (const t of d) t.pos = t.pos.add(t.vel.mul(z)); }, A = (d) => { const { minStarCount: t = 1500, maxStarCount: s, minGalaxyRadius: n = 1, maxGalaxyRadius: o, maxInitialSpeed: a = 4, rewindTimeSteps: e = 3, initialCollisionAvoidanceOffset: c = 1.5 } = d, i = r.randomCentered(a), f = i.mul(-e).add(r.randomCentered(c)), y = r.random(Math.PI), m = s ? Math.random() * (s - t) + t : t, w = o ? Math.random() * (o - n) + n : n, l = new S(i, f, y, m), x = u.fromEuler( l.rotation.x, l.rotation.y, l.rotation.z ); for (let M = 0; M < m; M++) l.stars.push(G(l, x, w)); return l; }, G = (d, t, s = 1) => { const n = 2 * Math.PI * Math.random(), o = Math.random() * s; let a = Math.random() * Math.exp(-2 * (o / s)) / 5 * s; Math.random() < 0.5 && (a = -a); const e = Math.sqrt(o ** 2 + a ** 2), c = Math.sqrt(d.mass * g / e), i = Math.sin(n), h = Math.cos(n), f = new r( o * h, o * i, a ), y = new r(-c * i, c * h, 0), m = t.transform(f).add(d.pos), w = t.transform(y).add(d.vel); return new E(m, w); }; export { A as createRandomGalaxy, C as updateGalaxies };