From da0d79f0db873c08fab3bc85023167e174d18b0e Mon Sep 17 00:00:00 2001 From: 苏向夜 Date: Mon, 19 Jan 2026 14:17:32 +0800 Subject: chore(ui): refactor workspace to monorepo --- ui/src/lib/effects/SaturnEffect.ts | 340 ------------------------------------- 1 file changed, 340 deletions(-) delete mode 100644 ui/src/lib/effects/SaturnEffect.ts (limited to 'ui/src/lib/effects/SaturnEffect.ts') diff --git a/ui/src/lib/effects/SaturnEffect.ts b/ui/src/lib/effects/SaturnEffect.ts deleted file mode 100644 index 357da9d..0000000 --- a/ui/src/lib/effects/SaturnEffect.ts +++ /dev/null @@ -1,340 +0,0 @@ -// Optimized Saturn Effect for low-end hardware -// Uses TypedArrays for memory efficiency and reduced particle density - -export class SaturnEffect { - private canvas: HTMLCanvasElement; - 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; - // types: Uint8Array where 0 = planet, 1 = ring - private types: Uint8Array | null = null; - private count: number = 0; - - private animationId: number = 0; - private angle: number = 0; - private scaleFactor: number = 1; - - // Mouse interaction properties - private isDragging: boolean = false; - 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) - private rotationDirection: number = 1; // 1 for clockwise, -1 for counter-clockwise - private readonly speedDecayRate: number = 0.992; // How fast speed returns to normal (closer to 1 = slower decay) - private readonly minSpeedMultiplier: number = 1; // Minimum speed is baseSpeed - private readonly maxSpeedMultiplier: number = 50; // Maximum speed is 50x baseSpeed - private isStopped: boolean = false; // Whether the user has stopped the rotation - - constructor(canvas: HTMLCanvasElement) { - this.canvas = canvas; - this.ctx = canvas.getContext("2d", { - alpha: true, - 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(); - } - - // Public methods for external mouse event handling - // These can be called from any element that wants to control the Saturn rotation - - handleMouseDown(clientX: number) { - this.isDragging = true; - this.lastMouseX = clientX; - this.lastMouseTime = performance.now(); - this.mouseVelocities = []; - } - - 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; - } - - handleMouseUp() { - if (this.isDragging && this.mouseVelocities.length > 0) { - this.applyFlingVelocity(); - } - this.isDragging = false; - } - - handleTouchStart(clientX: number) { - this.isDragging = true; - this.lastMouseX = clientX; - this.lastMouseTime = performance.now(); - this.mouseVelocities = []; - } - - 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; - } - - handleTouchEnd() { - if (this.isDragging && this.mouseVelocities.length > 0) { - this.applyFlingVelocity(); - } - this.isDragging = false; - } - - private applyFlingVelocity() { - // Calculate average velocity from recent samples - 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.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, - // keep current state (don't change isStopped) - } - - resize(width: number, height: number) { - 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; - } - - initParticles() { - // Significantly reduced particle count for CPU optimization - // Planet: 1800 -> 1000 - // Rings: 5000 -> 2500 - // Total approx 3500 vs 6800 previously (approx 50% reduction) - const planetCount = 1000; - const ringCount = 2500; - this.count = planetCount + ringCount; - - // Use TypedArrays for better memory locality - this.xyz = new Float32Array(this.count * 3); - this.types = new Uint8Array(this.count); - - let idx = 0; - - // 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++; - } - - // 2. Rings - 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++; - } - } - - animate() { - this.ctx.clearRect(0, 0, this.width, this.height); - - // Normal blending - 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; - - // 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 cy = this.height * 0.5; - - // Pre-calculate rotation matrices - const rotationY = this.angle; - const rotationX = 0.4; - const rotationZ = 0.15; - - const sinY = Math.sin(rotationY); - const cosY = Math.cos(rotationY); - const sinX = Math.sin(rotationX); - const cosX = Math.cos(rotationX); - const sinZ = Math.sin(rotationZ); - const cosZ = Math.cos(rotationZ); - - 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); - } - } - - this.animationId = requestAnimationFrame(this.animate); - } - - destroy() { - cancelAnimationFrame(this.animationId); - } -} -- cgit v1.2.3-70-g09d2