{"$schema":"https://ui.shadcn.com/schema/registry.json","homepage":"https://uicapsule.com/ui/background-pixel-stars","name":"background-pixel-stars","type":"registry:block","author":"Kaiyu Hsu <uicapsule@kyh.io>","dependencies":[],"devDependencies":[],"registryDependencies":[],"files":[{"type":"registry:file","path":"/background-pixel-stars.tsx","content":"\"use client\";\n\nimport { memo, useEffect, useRef } from \"react\";\n\n// 16-bit color palette (reduced color options)\nconst STAR_COLORS = [\n  \"#FFFFFF\", // White\n  \"#FFFFAA\", // Light yellow\n  \"#AAAAFF\", // Light blue\n  \"#FFAAAA\", // Light red\n  \"#AAFFAA\", // Light green\n  \"#FFAAFF\", // Light purple\n  \"#AAFFFF\", // Light cyan\n] as const;\n\n// Configuration constants\nconst starDensity = 0.00004; // Reduced density for larger stars\nconst twinkleProbability = 0.7;\nconst minTwinkleSpeed = 2;\nconst maxTwinkleSpeed = 4;\nconst pixelSize = 5;\nconst starRegenerationInterval = 5000; // Interval to regenerate stars (in ms)\nconst percentToRegenerate = 0.15; // Percentage of stars to regenerate at each interval\n\n// Shooting star configuration\nconst shootingStarPixelSize = 2;\nconst targetFps = 16; // 16 FPS for that retro feel\nconst frameInterval = 1000 / targetFps;\nconst shootingStarWidth = 4; // 4 pixels wide\nconst shootingStarHeight = 2; // 2 pixels high\nconst shootingStarMargin = 30; // How far off-canvas a star travels before it is culled\n\n// Type definitions\ntype BackgroundStar = {\n  x: number;\n  y: number;\n  color: string;\n  baseOpacity: number;\n  currentOpacity: number;\n  twinkle: boolean;\n  twinkleSpeed: number;\n  twinkleDirection: number; // -1 fading out, 1 fading in\n  twinkleTimer: number;\n};\n\ntype TrailPoint = {\n  x: number;\n  y: number;\n  opacity: number;\n};\n\ntype ShootingStar = {\n  x: number;\n  y: number;\n  angle: number;\n  speed: number;\n  distance: number;\n  trail: TrailPoint[];\n};\n\nconst randomStarColor = (): string => {\n  const colorIndex = Math.floor(Math.random() * STAR_COLORS.length);\n  return STAR_COLORS[colorIndex] ?? STAR_COLORS[0];\n};\n\n// A background star snapped to the pixel grid, at a random spot on the canvas.\nconst createBackgroundStar = (width: number, height: number): BackgroundStar => {\n  const shouldTwinkle = Math.random() < twinkleProbability;\n  const gridX = Math.floor(Math.random() * (width / pixelSize)) * pixelSize;\n  const gridY = Math.floor(Math.random() * (height / pixelSize)) * pixelSize;\n  const color = randomStarColor();\n  const baseOpacity = Math.random() * 0.5 + 0.5;\n\n  return {\n    x: gridX,\n    y: gridY,\n    color,\n    baseOpacity,\n    currentOpacity: baseOpacity,\n    twinkle: shouldTwinkle,\n    twinkleSpeed: minTwinkleSpeed + Math.random() * (maxTwinkleSpeed - minTwinkleSpeed),\n    twinkleDirection: -1, // -1 fading out, 1 fading in\n    twinkleTimer: 0,\n  };\n};\n\n// A shooting star entering from anywhere along the top edge. The angle spans\n// 45-135 degrees, where 90 is straight down, 45 down-right and 135 down-left.\nconst createShootingStar = (width: number): ShootingStar => ({\n  x: Math.random() * width,\n  y: 0,\n  angle: 45 + Math.random() * 90,\n  speed: Math.random() * 5 + 8,\n  distance: 0,\n  trail: [], // Empty trail initially\n});\n\nexport const BackgroundPixelStars = memo(() => {\n  const canvasRef = useRef<HTMLCanvasElement | null>(null);\n\n  useEffect(() => {\n    const canvas = canvasRef.current;\n    if (!canvas) return;\n\n    const ctx = canvas.getContext(\"2d\");\n    if (!ctx) return;\n\n    let backgroundStars: BackgroundStar[] = [];\n    let shootingStars: ShootingStar[] = [];\n\n    const initBackgroundStars = (): void => {\n      const area = canvas.width * canvas.height;\n      const numStars = Math.floor(area * starDensity);\n\n      backgroundStars = [];\n      for (let i = 0; i < numStars; i++) {\n        backgroundStars.push(createBackgroundStar(canvas.width, canvas.height));\n      }\n    };\n\n    // Swap out a slice of the field so the sky slowly reshuffles.\n    const regenerateBackgroundStars = (): void => {\n      if (backgroundStars.length === 0) return;\n\n      const numToRegenerate = Math.max(1, Math.floor(backgroundStars.length * percentToRegenerate));\n\n      for (let i = 0; i < numToRegenerate; i++) {\n        const randomIndex = Math.floor(Math.random() * backgroundStars.length);\n        backgroundStars[randomIndex] = createBackgroundStar(canvas.width, canvas.height);\n      }\n    };\n\n    const drawBackgroundStars = (): void => {\n      for (const star of backgroundStars) {\n        ctx.fillStyle = star.color;\n        ctx.globalAlpha = star.currentOpacity;\n        ctx.fillRect(star.x, star.y, pixelSize, pixelSize);\n\n        if (!star.twinkle) continue;\n\n        star.twinkleTimer += 1 / targetFps;\n\n        if (star.twinkleTimer >= star.twinkleSpeed) {\n          star.twinkleTimer = 0;\n          star.twinkleDirection *= -1; // Reverse direction\n        }\n\n        // Calculate new opacity based on discrete steps\n        const progress = star.twinkleTimer / star.twinkleSpeed;\n        if (progress < 0.5) {\n          star.currentOpacity =\n            star.twinkleDirection < 0 ? star.baseOpacity : star.baseOpacity * 0.3;\n        } else {\n          star.currentOpacity =\n            star.twinkleDirection < 0 ? star.baseOpacity * 0.3 : star.baseOpacity;\n        }\n      }\n    };\n\n    const updateShootingStars = (): void => {\n      shootingStars = shootingStars\n        .map((star) => {\n          // Calculate new position\n          const newX = star.x + star.speed * Math.cos((star.angle * Math.PI) / 180);\n          const newY = star.y + star.speed * Math.sin((star.angle * Math.PI) / 180);\n          const newDistance = star.distance + star.speed;\n\n          const newTrail = [...star.trail];\n\n          // Only add to trail every few frames for pixelated effect\n          if (newDistance % 8 < star.speed) {\n            newTrail.push({\n              x: star.x,\n              y: star.y,\n              opacity: 1.0,\n            });\n          }\n\n          // Update trail opacity and remove old trail pieces\n          const updatedTrail = newTrail\n            .map((point) => ({ ...point, opacity: point.opacity - 0.1 }))\n            .filter((point) => point.opacity > 0);\n\n          return {\n            ...star,\n            x: newX,\n            y: newY,\n            distance: newDistance,\n            trail: updatedTrail,\n          };\n        })\n        .filter(\n          (star) =>\n            // Remove stars that are out of bounds\n            star.x >= -shootingStarMargin &&\n            star.x <= canvas.width + shootingStarMargin &&\n            star.y >= -shootingStarMargin &&\n            star.y <= canvas.height + shootingStarMargin,\n        );\n    };\n\n    const drawShootingStars = (): void => {\n      for (const star of shootingStars) {\n        const radians = (star.angle * Math.PI) / 180;\n\n        // Draw trail\n        for (const point of star.trail) {\n          ctx.save();\n          ctx.translate(point.x, point.y);\n          ctx.rotate(radians);\n          ctx.translate(-point.x, -point.y);\n\n          ctx.fillStyle = `rgba(180, 242, 255, ${point.opacity})`;\n          ctx.fillRect(point.x, point.y, shootingStarPixelSize, shootingStarPixelSize);\n\n          ctx.restore();\n        }\n\n        // Draw star (pixelated representation)\n        ctx.save();\n        ctx.translate(star.x, star.y);\n        ctx.rotate(radians);\n        ctx.translate(-star.x, -star.y);\n\n        ctx.fillStyle = \"#ffffff\";\n        ctx.globalAlpha = 1.0;\n\n        for (let y = 0; y < shootingStarHeight; y++) {\n          for (let x = 0; x < shootingStarWidth; x++) {\n            // Skip some pixels for pixelated look\n            if ((x === 0 && y === 1) || (x === 3 && y === 0)) continue;\n\n            ctx.fillRect(\n              star.x + x * shootingStarPixelSize,\n              star.y + y * shootingStarPixelSize,\n              shootingStarPixelSize,\n              shootingStarPixelSize,\n            );\n          }\n        }\n\n        ctx.restore();\n      }\n    };\n\n    let frameId = 0;\n    let lastRenderTime = 0;\n\n    const animateCanvas = (timestamp: number): void => {\n      frameId = requestAnimationFrame(animateCanvas);\n\n      // Skip frames to limit to target FPS\n      if (timestamp - lastRenderTime < frameInterval) return;\n      lastRenderTime = timestamp;\n\n      ctx.clearRect(0, 0, canvas.width, canvas.height);\n\n      drawBackgroundStars();\n\n      if (shootingStars.length) {\n        updateShootingStars();\n        drawShootingStars();\n      }\n    };\n\n    const resizeCanvas = (): void => {\n      canvas.width = window.innerWidth;\n      canvas.height = window.innerHeight;\n      initBackgroundStars();\n    };\n\n    resizeCanvas();\n    frameId = requestAnimationFrame(animateCanvas);\n\n    // Spawn shooting stars on a random 2-6 second cadence.\n    let shootingStarTimer: ReturnType<typeof setTimeout> | undefined;\n    const spawnShootingStar = (): void => {\n      shootingStars = [...shootingStars, createShootingStar(canvas.width)];\n\n      const randomDelay = Math.random() * 4000 + 2000; // 2-6 seconds\n      shootingStarTimer = setTimeout(spawnShootingStar, randomDelay);\n    };\n    spawnShootingStar();\n\n    const regenerationInterval = setInterval(regenerateBackgroundStars, starRegenerationInterval);\n\n    window.addEventListener(\"resize\", resizeCanvas);\n\n    return () => {\n      cancelAnimationFrame(frameId);\n      clearInterval(regenerationInterval);\n      clearTimeout(shootingStarTimer);\n      window.removeEventListener(\"resize\", resizeCanvas);\n    };\n  }, []);\n\n  return <canvas ref={canvasRef} className=\"pointer-events-none fixed inset-0\" />;\n});\n","target":"uicapsule/background-pixel-stars/background-pixel-stars.tsx"},{"type":"registry:file","path":"/package.json","content":"{\n  \"name\": \"@uicapsule/background-pixel-stars\",\n  \"version\": \"0.1.0\",\n  \"private\": true,\n  \"type\": \"module\",\n  \"exports\": {\n    \"./preview\": \"./preview.tsx\"\n  },\n  \"scripts\": {\n    \"clean\": \"git clean -xdf .cache .turbo dist node_modules\"\n  },\n  \"dependencies\": {\n    \"react\": \"catalog:\",\n    \"react-dom\": \"catalog:\"\n  },\n  \"devDependencies\": {\n    \"@types/react\": \"catalog:\",\n    \"@types/react-dom\": \"catalog:\"\n  }\n}\n","target":"uicapsule/background-pixel-stars/package.json"},{"type":"registry:file","path":"/preview.tsx","content":"\"use client\";\n\nimport { BackgroundPixelStars } from \"./background-pixel-stars\";\n\nconst Preview = () => {\n  return (\n    <div className=\"h-dvh w-dvw bg-black bg-[url('https://zmdrwswxugswzmcokvff.supabase.co/storage/v1/object/public/vibedgames/bg.png')] bg-[size:10px]\">\n      <BackgroundPixelStars />\n    </div>\n  );\n};\n\nexport default Preview;\n","target":"uicapsule/background-pixel-stars/preview.tsx"}]}