Skip to main content
Glama

Storyden

by Southclaws
Starfield.tsx6.54 kB
"use client"; import { useEffect, useRef } from "react"; import { Renderer, Camera, Geometry, Program, Mesh } from "ogl"; const defaultColors = ["#ffffff", "#ffffff", "#ffffff"]; const hexToRgb = (hex: string) => { hex = hex.replace(/^#/, ""); if (hex.length === 3) { hex = hex .split("") .map((c) => c + c) .join(""); } const int = parseInt(hex, 16); const r = ((int >> 16) & 255) / 255; const g = ((int >> 8) & 255) / 255; const b = (int & 255) / 255; return [r, g, b]; }; const vertex = /* glsl */ ` attribute vec3 position; attribute vec4 random; attribute vec3 color; uniform mat4 modelMatrix; uniform mat4 viewMatrix; uniform mat4 projectionMatrix; uniform float uTime; uniform float uSpread; uniform float uBaseSize; uniform float uSizeRandomness; varying vec4 vRandom; varying vec3 vColor; void main() { vRandom = random; vColor = color; vec3 pos = position * uSpread; pos.z *= 10.0; vec4 mPos = modelMatrix * vec4(pos, 1.0); float t = uTime; mPos.x += sin(t * random.z + 6.28 * random.w) * mix(0.1, 1.5, random.x); mPos.y += sin(t * random.y + 6.28 * random.x) * mix(0.1, 1.5, random.w); mPos.z += sin(t * random.w + 6.28 * random.y) * mix(0.1, 1.5, random.z); vec4 mvPos = viewMatrix * mPos; gl_PointSize = (uBaseSize * (1.0 + uSizeRandomness * (random.x - 0.5))) / length(mvPos.xyz); gl_Position = projectionMatrix * mvPos; } `; const fragment = /* glsl */ ` precision highp float; uniform float uTime; uniform float uAlphaParticles; varying vec4 vRandom; varying vec3 vColor; void main() { vec2 uv = gl_PointCoord.xy; float d = length(uv - vec2(0.5)); if(uAlphaParticles < 0.5) { if(d > 0.5) { discard; } gl_FragColor = vec4(vColor + 0.2 * sin(uv.yxx + uTime + vRandom.y * 6.28), 1.0); } else { float circle = smoothstep(0.5, 0.4, d) * 0.8; gl_FragColor = vec4(vColor + 0.2 * sin(uv.yxx + uTime + vRandom.y * 6.28), circle); } } `; type Props = { particleCount?: number; particleSpread?: number; speed?: number; particleColors?: string[]; moveParticlesOnHover?: boolean; particleHoverFactor?: number; alphaParticles?: boolean; particleBaseSize?: number; sizeRandomness?: number; cameraDistance?: number; disableRotation?: boolean; className?: string; }; export const Starfield = ({ particleCount = 200, particleSpread = 10, speed = 0.1, particleColors, particleHoverFactor = 1, alphaParticles = false, particleBaseSize = 100, sizeRandomness = 1, cameraDistance = 20, disableRotation = false, className, }: Props) => { const containerRef = useRef<HTMLDivElement>(null); const mouseRef = useRef({ x: 0, y: 0 }); useEffect(() => { const container = containerRef.current; if (!container) return; const renderer = new Renderer({ depth: false, alpha: true }); const gl = renderer.gl; container.appendChild(gl.canvas); gl.clearColor(0, 0, 0, 0); const camera = new Camera(gl, { fov: 15 }); camera.position.set(0, 0, cameraDistance); const resize = () => { const width = container.clientWidth; const height = container.clientHeight; renderer.setSize(width, height); camera.perspective({ aspect: gl.canvas.width / gl.canvas.height, }); }; window.addEventListener("resize", resize, false); resize(); const handleMouseMove = (e: MouseEvent) => { const rect = container.getBoundingClientRect(); const x = ((e.clientX - rect.left) / rect.width) * 2 - 1; const y = -(((e.clientY - rect.top) / rect.height) * 2 - 1); mouseRef.current = { x, y }; }; const count = particleCount; const positions = new Float32Array(count * 3); const randoms = new Float32Array(count * 4); const colors = new Float32Array(count * 3); const palette = particleColors && particleColors.length > 0 ? particleColors : defaultColors; for (let i = 0; i < count; i++) { let x, y, z, len; do { x = Math.random() * 2 - 1; y = Math.random() * 2 - 1; z = Math.random() * 2 - 1; len = x * x + y * y + z * z; } while (len > 1 || len === 0); const r = Math.cbrt(Math.random()); positions.set([x * r, y * r, z * r], i * 3); randoms.set( [Math.random(), Math.random(), Math.random(), Math.random()], i * 4 ); const col = hexToRgb(palette[Math.floor(Math.random() * palette.length)]); colors.set(col, i * 3); } const geometry = new Geometry(gl, { position: { size: 3, data: positions }, random: { size: 4, data: randoms }, color: { size: 3, data: colors }, }); const program = new Program(gl, { vertex, fragment, uniforms: { uTime: { value: 0 }, uSpread: { value: particleSpread }, uBaseSize: { value: particleBaseSize }, uSizeRandomness: { value: sizeRandomness }, uAlphaParticles: { value: alphaParticles ? 1 : 0 }, }, transparent: true, depthTest: false, }); const particles = new Mesh(gl, { mode: gl.POINTS, geometry, program }); let animationFrameId: number; let lastTime = performance.now(); let elapsed = 0; const update: FrameRequestCallback = (t) => { animationFrameId = requestAnimationFrame(update); const delta = t - lastTime; lastTime = t; elapsed += delta * speed; program.uniforms.uTime.value = elapsed * 0.001; if (!disableRotation) { particles.rotation.x = Math.sin(elapsed * 0.0002) * 0.1; particles.rotation.y = Math.cos(elapsed * 0.0005) * 0.15; particles.rotation.z += 0.01 * speed; } renderer.render({ scene: particles, camera }); }; animationFrameId = requestAnimationFrame(update); return () => { window.removeEventListener("resize", resize); cancelAnimationFrame(animationFrameId); if (container.contains(gl.canvas)) { container.removeChild(gl.canvas); } }; // eslint-disable-next-line react-hooks/exhaustive-deps }, [ particleCount, particleSpread, speed, particleHoverFactor, alphaParticles, particleBaseSize, sizeRandomness, cameraDistance, disableRotation, ]); return ( <div ref={containerRef} className={`particles-container ${className}`} aria-hidden /> ); };

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/Southclaws/storyden'

If you have feedback or need assistance with the MCP directory API, please join our Discord server