aboutsummaryrefslogtreecommitdiffstatshomepage
path: root/ui/src/lib/effects/SaturnEffect.ts
diff options
context:
space:
mode:
Diffstat (limited to 'ui/src/lib/effects/SaturnEffect.ts')
-rw-r--r--ui/src/lib/effects/SaturnEffect.ts233
1 files changed, 118 insertions, 115 deletions
diff --git a/ui/src/lib/effects/SaturnEffect.ts b/ui/src/lib/effects/SaturnEffect.ts
index 42aee66..357da9d 100644
--- a/ui/src/lib/effects/SaturnEffect.ts
+++ b/ui/src/lib/effects/SaturnEffect.ts
@@ -6,7 +6,7 @@ export class SaturnEffect {
private ctx: CanvasRenderingContext2D;
private width: number = 0;
private height: number = 0;
-
+
// Data-oriented design for performance
// xyz: Float32Array where [i*3, i*3+1, i*3+2] corresponds to x, y, z
private xyz: Float32Array | null = null;
@@ -23,7 +23,7 @@ export class SaturnEffect {
private lastMouseX: number = 0;
private lastMouseTime: number = 0;
private mouseVelocities: number[] = []; // Store recent velocities for averaging
-
+
// Rotation speed control
private readonly baseSpeed: number = 0.005; // Original rotation speed
private currentSpeed: number = 0.005; // Current rotation speed (can be modified by mouse)
@@ -35,15 +35,15 @@ export class SaturnEffect {
constructor(canvas: HTMLCanvasElement) {
this.canvas = canvas;
- this.ctx = canvas.getContext('2d', {
+ this.ctx = canvas.getContext("2d", {
alpha: true,
- desynchronized: false // default is usually fine, 'desynchronized' can help latency but might flicker
+ desynchronized: false, // default is usually fine, 'desynchronized' can help latency but might flicker
})!;
-
+
// Initial resize will set up everything
this.resize(window.innerWidth, window.innerHeight);
this.initParticles();
-
+
this.animate = this.animate.bind(this);
this.animate();
}
@@ -60,24 +60,24 @@ export class SaturnEffect {
handleMouseMove(clientX: number) {
if (!this.isDragging) return;
-
+
const currentTime = performance.now();
const deltaTime = currentTime - this.lastMouseTime;
-
+
if (deltaTime > 0) {
const deltaX = clientX - this.lastMouseX;
const velocity = deltaX / deltaTime; // pixels per millisecond
-
+
// Store recent velocities (keep last 5 for smoothing)
this.mouseVelocities.push(velocity);
if (this.mouseVelocities.length > 5) {
this.mouseVelocities.shift();
}
-
+
// Apply direct rotation while dragging
this.angle += deltaX * 0.002;
}
-
+
this.lastMouseX = clientX;
this.lastMouseTime = currentTime;
}
@@ -98,22 +98,22 @@ export class SaturnEffect {
handleTouchMove(clientX: number) {
if (!this.isDragging) return;
-
+
const currentTime = performance.now();
const deltaTime = currentTime - this.lastMouseTime;
-
+
if (deltaTime > 0) {
const deltaX = clientX - this.lastMouseX;
const velocity = deltaX / deltaTime;
-
+
this.mouseVelocities.push(velocity);
if (this.mouseVelocities.length > 5) {
this.mouseVelocities.shift();
}
-
+
this.angle += deltaX * 0.002;
}
-
+
this.lastMouseX = clientX;
this.lastMouseTime = currentTime;
}
@@ -127,39 +127,40 @@ export class SaturnEffect {
private applyFlingVelocity() {
// Calculate average velocity from recent samples
- const avgVelocity = this.mouseVelocities.reduce((a, b) => a + b, 0) / this.mouseVelocities.length;
-
+ const avgVelocity =
+ this.mouseVelocities.reduce((a, b) => a + b, 0) / this.mouseVelocities.length;
+
// Threshold for considering it a "fling" (pixels per millisecond)
const flingThreshold = 0.3;
// Threshold for considering the rotation as "stopped" by user
const stopThreshold = 0.1;
-
+
if (Math.abs(avgVelocity) > flingThreshold) {
// User flung it - start rotating again
this.isStopped = false;
-
+
// Determine new direction based on fling direction
const newDirection = avgVelocity > 0 ? 1 : -1;
-
+
// If direction changed, update it permanently
if (newDirection !== this.rotationDirection) {
this.rotationDirection = newDirection;
}
-
+
// Calculate speed boost based on fling strength
// Map velocity to speed multiplier (stronger fling = faster rotation)
const speedMultiplier = Math.min(
this.maxSpeedMultiplier,
- this.minSpeedMultiplier + Math.abs(avgVelocity) * 10
+ this.minSpeedMultiplier + Math.abs(avgVelocity) * 10,
);
-
+
this.currentSpeed = this.baseSpeed * speedMultiplier;
} else if (Math.abs(avgVelocity) < stopThreshold) {
// User gently released - keep it stopped
this.isStopped = true;
this.currentSpeed = 0;
}
- // If velocity is between stopThreshold and flingThreshold,
+ // If velocity is between stopThreshold and flingThreshold,
// keep current state (don't change isStopped)
}
@@ -167,17 +168,17 @@ export class SaturnEffect {
const dpr = window.devicePixelRatio || 1;
this.width = width;
this.height = height;
-
+
this.canvas.width = width * dpr;
this.canvas.height = height * dpr;
this.canvas.style.width = `${width}px`;
this.canvas.style.height = `${height}px`;
-
+
this.ctx.scale(dpr, dpr);
// Dynamic scaling based on screen size
const minDim = Math.min(width, height);
- this.scaleFactor = minDim * 0.45;
+ this.scaleFactor = minDim * 0.45;
}
initParticles() {
@@ -197,65 +198,68 @@ export class SaturnEffect {
// 1. Planet
for (let i = 0; i < planetCount; i++) {
- const theta = Math.random() * Math.PI * 2;
- const phi = Math.acos((Math.random() * 2) - 1);
- const r = 1.0;
-
- // x, y, z
- this.xyz[idx * 3] = r * Math.sin(phi) * Math.cos(theta);
- this.xyz[idx * 3 + 1] = r * Math.sin(phi) * Math.sin(theta);
- this.xyz[idx * 3 + 2] = r * Math.cos(phi);
-
- this.types[idx] = 0; // 0 for planet
- idx++;
+ const theta = Math.random() * Math.PI * 2;
+ const phi = Math.acos(Math.random() * 2 - 1);
+ const r = 1.0;
+
+ // x, y, z
+ this.xyz[idx * 3] = r * Math.sin(phi) * Math.cos(theta);
+ this.xyz[idx * 3 + 1] = r * Math.sin(phi) * Math.sin(theta);
+ this.xyz[idx * 3 + 2] = r * Math.cos(phi);
+
+ this.types[idx] = 0; // 0 for planet
+ idx++;
}
// 2. Rings
- const ringInner = 1.4;
+ const ringInner = 1.4;
const ringOuter = 2.3;
-
+
for (let i = 0; i < ringCount; i++) {
- const angle = Math.random() * Math.PI * 2;
- const dist = Math.sqrt(Math.random() * (ringOuter*ringOuter - ringInner*ringInner) + ringInner*ringInner);
-
- // x, y, z
- this.xyz[idx * 3] = dist * Math.cos(angle);
- this.xyz[idx * 3 + 1] = (Math.random() - 0.5) * 0.05;
- this.xyz[idx * 3 + 2] = dist * Math.sin(angle);
-
- this.types[idx] = 1; // 1 for ring
- idx++;
+ const angle = Math.random() * Math.PI * 2;
+ const dist = Math.sqrt(
+ Math.random() * (ringOuter * ringOuter - ringInner * ringInner) + ringInner * ringInner,
+ );
+
+ // x, y, z
+ this.xyz[idx * 3] = dist * Math.cos(angle);
+ this.xyz[idx * 3 + 1] = (Math.random() - 0.5) * 0.05;
+ this.xyz[idx * 3 + 2] = dist * Math.sin(angle);
+
+ this.types[idx] = 1; // 1 for ring
+ idx++;
}
}
animate() {
this.ctx.clearRect(0, 0, this.width, this.height);
-
+
// Normal blending
- this.ctx.globalCompositeOperation = 'source-over';
-
+ this.ctx.globalCompositeOperation = "source-over";
+
// Update rotation speed - decay towards base speed while maintaining direction
if (!this.isDragging && !this.isStopped) {
if (this.currentSpeed > this.baseSpeed) {
// Gradually decay speed back to base speed
- this.currentSpeed = this.baseSpeed + (this.currentSpeed - this.baseSpeed) * this.speedDecayRate;
-
+ this.currentSpeed =
+ this.baseSpeed + (this.currentSpeed - this.baseSpeed) * this.speedDecayRate;
+
// Snap to base speed when close enough
if (this.currentSpeed - this.baseSpeed < 0.00001) {
this.currentSpeed = this.baseSpeed;
}
}
-
+
// Apply rotation with current speed and direction
this.angle += this.currentSpeed * this.rotationDirection;
}
- const cx = this.width * 0.6;
+ const cx = this.width * 0.6;
const cy = this.height * 0.5;
-
+
// Pre-calculate rotation matrices
const rotationY = this.angle;
- const rotationX = 0.4;
+ const rotationX = 0.4;
const rotationZ = 0.15;
const sinY = Math.sin(rotationY);
@@ -265,66 +269,66 @@ export class SaturnEffect {
const sinZ = Math.sin(rotationZ);
const cosZ = Math.cos(rotationZ);
- const fov = 1500;
+ const fov = 1500;
const scaleFactor = this.scaleFactor;
if (!this.xyz || !this.types) return;
for (let i = 0; i < this.count; i++) {
- const x = this.xyz[i * 3];
- const y = this.xyz[i * 3 + 1];
- const z = this.xyz[i * 3 + 2];
-
- // Apply Scale
- const px = x * scaleFactor;
- const py = y * scaleFactor;
- const pz = z * scaleFactor;
-
- // 1. Rotate Y
- const x1 = px * cosY - pz * sinY;
- const z1 = pz * cosY + px * sinY;
- // y1 = py
-
- // 2. Rotate X
- const y2 = py * cosX - z1 * sinX;
- const z2 = z1 * cosX + py * sinX;
- // x2 = x1
-
- // 3. Rotate Z
- const x3 = x1 * cosZ - y2 * sinZ;
- const y3 = y2 * cosZ + x1 * sinZ;
- const z3 = z2;
-
- const scale = fov / (fov + z3);
-
- if (z3 > -fov) {
- const x2d = cx + x3 * scale;
- const y2d = cy + y3 * scale;
-
- // Size calculation - slightly larger dots to compensate for lower count
- // Previously Planet 2.0 -> 2.4, Ring 1.3 -> 1.5
- const type = this.types[i];
- const sizeBase = type === 0 ? 2.4 : 1.5;
- const size = sizeBase * scale;
-
- // Opacity
- let alpha = (scale * scale * scale);
- if (alpha > 1) alpha = 1;
- if (alpha < 0.15) continue; // Skip very faint particles for performance
-
- // Optimization: Planet color vs Ring color
- if (type === 0) {
- // Planet: Warm White
- this.ctx.fillStyle = `rgba(255, 240, 220, ${alpha})`;
- } else {
- // Ring: Cool White
- this.ctx.fillStyle = `rgba(220, 240, 255, ${alpha})`;
- }
-
- // Render as squares (fillRect) instead of circles (arc)
- // This is significantly faster for software rendering and reduces GPU usage.
- this.ctx.fillRect(x2d, y2d, size, size);
+ const x = this.xyz[i * 3];
+ const y = this.xyz[i * 3 + 1];
+ const z = this.xyz[i * 3 + 2];
+
+ // Apply Scale
+ const px = x * scaleFactor;
+ const py = y * scaleFactor;
+ const pz = z * scaleFactor;
+
+ // 1. Rotate Y
+ const x1 = px * cosY - pz * sinY;
+ const z1 = pz * cosY + px * sinY;
+ // y1 = py
+
+ // 2. Rotate X
+ const y2 = py * cosX - z1 * sinX;
+ const z2 = z1 * cosX + py * sinX;
+ // x2 = x1
+
+ // 3. Rotate Z
+ const x3 = x1 * cosZ - y2 * sinZ;
+ const y3 = y2 * cosZ + x1 * sinZ;
+ const z3 = z2;
+
+ const scale = fov / (fov + z3);
+
+ if (z3 > -fov) {
+ const x2d = cx + x3 * scale;
+ const y2d = cy + y3 * scale;
+
+ // Size calculation - slightly larger dots to compensate for lower count
+ // Previously Planet 2.0 -> 2.4, Ring 1.3 -> 1.5
+ const type = this.types[i];
+ const sizeBase = type === 0 ? 2.4 : 1.5;
+ const size = sizeBase * scale;
+
+ // Opacity
+ let alpha = scale * scale * scale;
+ if (alpha > 1) alpha = 1;
+ if (alpha < 0.15) continue; // Skip very faint particles for performance
+
+ // Optimization: Planet color vs Ring color
+ if (type === 0) {
+ // Planet: Warm White
+ this.ctx.fillStyle = `rgba(255, 240, 220, ${alpha})`;
+ } else {
+ // Ring: Cool White
+ this.ctx.fillStyle = `rgba(220, 240, 255, ${alpha})`;
}
+
+ // Render as squares (fillRect) instead of circles (arc)
+ // This is significantly faster for software rendering and reduces GPU usage.
+ this.ctx.fillRect(x2d, y2d, size, size);
+ }
}
this.animationId = requestAnimationFrame(this.animate);
@@ -334,4 +338,3 @@ export class SaturnEffect {
cancelAnimationFrame(this.animationId);
}
}
-