From ff358c3456435998e14f7337f4542deeaeb59bad Mon Sep 17 00:00:00 2001
From: HsiangNianian <i@jyunko.cn>
Date: Thu, 15 Jan 2026 18:22:43 +0800
Subject: feat: Enhance text formatting in HomeView by adding support for
 italicized text using single asterisks

---
 ui/src/components/HomeView.svelte | 1 +
 1 file changed, 1 insertion(+)

(limited to 'ui/src/components/HomeView.svelte')

diff --git a/ui/src/components/HomeView.svelte b/ui/src/components/HomeView.svelte
index 7bb7e44..5119d00 100644
--- a/ui/src/components/HomeView.svelte
+++ b/ui/src/components/HomeView.svelte
@@ -65,6 +65,7 @@
           // Formatting helper
           const formatLine = (text: string) => text
               .replace(/\*\*(.*?)\*\*/g, '<strong class="text-zinc-200">$1</strong>')
+              .replace(/(?<!\*)\*([^*]+)\*(?!\*)/g, '<em class="text-zinc-400 italic">$1</em>')
               .replace(/`([^`]+)`/g, '<code class="bg-zinc-800 px-1 py-0.5 rounded text-xs text-zinc-300 font-mono border border-white/5 break-all whitespace-normal">$1</code>')
               .replace(/\[(.*?)\]\((.*?)\)/g, '<a href="$2" target="_blank" class="text-indigo-400 hover:text-indigo-300 hover:underline decoration-indigo-400/30 break-all">$1</a>');
 
-- 
cgit v1.2.3-70-g09d2


From 655d47745ce136603ff8222868b440e641fb9407 Mon Sep 17 00:00:00 2001
From: HsiangNianian <i@jyunko.cn>
Date: Thu, 15 Jan 2026 20:02:23 +0800
Subject: feat: Add interactive Saturn effect controls in HomeView and enhance
 ParticleBackground for external mouse handling

---
 ui/src/components/HomeView.svelte           |  69 ++++++++++++-
 ui/src/components/ParticleBackground.svelte |  15 ++-
 ui/src/lib/effects/SaturnEffect.ts          | 147 +++++++++++++++++++++++++++-
 3 files changed, 226 insertions(+), 5 deletions(-)

(limited to 'ui/src/components/HomeView.svelte')

diff --git a/ui/src/components/HomeView.svelte b/ui/src/components/HomeView.svelte
index 5119d00..2fa8390 100644
--- a/ui/src/components/HomeView.svelte
+++ b/ui/src/components/HomeView.svelte
@@ -3,6 +3,7 @@
   import { gameState } from '../stores/game.svelte';
   import { releasesState } from '../stores/releases.svelte';
   import { Calendar, ExternalLink } from 'lucide-svelte';
+  import { getSaturnEffect } from './ParticleBackground.svelte';
 
   type Props = {
     mouseX: number;
@@ -10,6 +11,60 @@
   };
   let { mouseX = 0, mouseY = 0 }: Props = $props();
 
+  // Saturn effect mouse interaction handlers
+  function handleSaturnMouseDown(e: MouseEvent) {
+    const effect = getSaturnEffect();
+    if (effect) {
+      effect.handleMouseDown(e.clientX);
+    }
+  }
+
+  function handleSaturnMouseMove(e: MouseEvent) {
+    const effect = getSaturnEffect();
+    if (effect) {
+      effect.handleMouseMove(e.clientX);
+    }
+  }
+
+  function handleSaturnMouseUp() {
+    const effect = getSaturnEffect();
+    if (effect) {
+      effect.handleMouseUp();
+    }
+  }
+
+  function handleSaturnMouseLeave() {
+    const effect = getSaturnEffect();
+    if (effect) {
+      effect.handleMouseUp();
+    }
+  }
+
+  function handleSaturnTouchStart(e: TouchEvent) {
+    if (e.touches.length === 1) {
+      const effect = getSaturnEffect();
+      if (effect) {
+        effect.handleTouchStart(e.touches[0].clientX);
+      }
+    }
+  }
+
+  function handleSaturnTouchMove(e: TouchEvent) {
+    if (e.touches.length === 1) {
+      const effect = getSaturnEffect();
+      if (effect) {
+        effect.handleTouchMove(e.touches[0].clientX);
+      }
+    }
+  }
+
+  function handleSaturnTouchEnd() {
+    const effect = getSaturnEffect();
+    if (effect) {
+      effect.handleTouchEnd();
+    }
+  }
+
   onMount(() => {
     releasesState.loadReleases();
   });
@@ -104,8 +159,18 @@
 <!-- Scrollable Container -->
 <div class="relative z-10 h-full {releasesState.isLoading ? 'overflow-hidden' : 'overflow-y-auto custom-scrollbar scroll-smooth'}">
   
-  <!-- Hero Section (Full Height) -->
-  <div class="min-h-full flex flex-col justify-end p-12 pb-32">
+  <!-- Hero Section (Full Height) - Interactive area for Saturn rotation -->
+  <!-- svelte-ignore a11y_no_static_element_interactions -->
+  <div 
+    class="min-h-full flex flex-col justify-end p-12 pb-32 cursor-grab active:cursor-grabbing select-none"
+    onmousedown={handleSaturnMouseDown}
+    onmousemove={handleSaturnMouseMove}
+    onmouseup={handleSaturnMouseUp}
+    onmouseleave={handleSaturnMouseLeave}
+    ontouchstart={handleSaturnTouchStart}
+    ontouchmove={handleSaturnTouchMove}
+    ontouchend={handleSaturnTouchEnd}
+  >
      <!-- 3D Floating Hero Text -->
       <div 
         class="transition-transform duration-200 ease-out origin-bottom-left"
diff --git a/ui/src/components/ParticleBackground.svelte b/ui/src/components/ParticleBackground.svelte
index 080f1f2..7644b1a 100644
--- a/ui/src/components/ParticleBackground.svelte
+++ b/ui/src/components/ParticleBackground.svelte
@@ -1,7 +1,17 @@
+<script lang="ts" module>
+  import { SaturnEffect } from "../lib/effects/SaturnEffect";
+  
+  // Global reference to the active Saturn effect for external control
+  let globalSaturnEffect: SaturnEffect | null = null;
+  
+  export function getSaturnEffect(): SaturnEffect | null {
+    return globalSaturnEffect;
+  }
+</script>
+
 <script lang="ts">
   import { onMount, onDestroy } from "svelte";
   import { ConstellationEffect } from "../lib/effects/ConstellationEffect";
-  import { SaturnEffect } from "../lib/effects/SaturnEffect";
   import { settingsState } from "../stores/settings.svelte";
 
   let canvas: HTMLCanvasElement;
@@ -16,8 +26,10 @@
 
     if (settingsState.settings.active_effect === "saturn") {
       activeEffect = new SaturnEffect(canvas);
+      globalSaturnEffect = activeEffect;
     } else {
       activeEffect = new ConstellationEffect(canvas);
+      globalSaturnEffect = null;
     }
     
     // Ensure correct size immediately
@@ -48,6 +60,7 @@
   
   onDestroy(() => {
     if (activeEffect) activeEffect.destroy();
+    globalSaturnEffect = null;
   });
 </script>
 
diff --git a/ui/src/lib/effects/SaturnEffect.ts b/ui/src/lib/effects/SaturnEffect.ts
index a370936..42aee66 100644
--- a/ui/src/lib/effects/SaturnEffect.ts
+++ b/ui/src/lib/effects/SaturnEffect.ts
@@ -18,6 +18,21 @@ export class SaturnEffect {
   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', { 
@@ -33,6 +48,121 @@ export class SaturnEffect {
     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;
@@ -104,8 +234,21 @@ export class SaturnEffect {
     // Normal blending
     this.ctx.globalCompositeOperation = 'source-over';
     
-    // Slower rotation (from 0.0015 to 0.0005)
-    this.angle += 0.0005;
+    // 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;
-- 
cgit v1.2.3-70-g09d2

