@kajws/galaxy-js
Version:
A JavaScript port of the classic galaxy screensaver with 3D particle simulation
405 lines (404 loc) • 12.9 kB
JavaScript
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
};