Skip to content
bgHub
Gallery

Ultraviolet Warp Starfield

Canvas 2DMedium

A steady warp starfield rushing outward. Canvas 2D.

#stars#warp#canvas#warp#ultraviolet
Live preview
1 file
// @ts-nocheck
import { useEffect, useRef } from "react";

// Render <Starfield /> inside a container with position: relative.
// Both schemes are inlined; the effect below follows prefers-color-scheme.
const opts = {"colors":["#a855f7","#6366f1","#d8b4fe"],"bg":"#08040d","light":{"bg":"#f8f4fd","colors":["#7e22ce","#4338ca","#9333ea"]}};
const SPEED = 0.9;
const COUNT = 500;

type Star = { x: number; y: number; z: number; pz: number; c: string };

export function Starfield() {
  const canvasRef = useRef<HTMLCanvasElement>(null);

  useEffect(() => {
    const canvas = canvasRef.current;
    if (!canvas) return;
    const ctx = canvas.getContext("2d");
    if (!ctx) return;
    const host = canvas.parentElement;
    if (!host) return;

/* Both schemes ship in opts; `light` is absent in the live preview, which
   resolves the scheme itself, so nothing is observed there. */
const schemeMq =
  opts.light && typeof matchMedia === "function"
    ? matchMedia("(prefers-color-scheme: light)")
    : null;
const schemeNow = () => (schemeMq && schemeMq.matches ? opts.light : opts);
/** The scheme the scene or particle set was first built from. */
const SCHEME_BUILT = schemeNow();
let COLORS = SCHEME_BUILT.colors, BG = SCHEME_BUILT.bg;
const onScheme = () => {
  const s = schemeNow();
  COLORS = s.colors; BG = s.bg;
  if (typeof seed === "function") seed();
  if (typeof applyScheme === "function") applyScheme();
};
if (schemeMq) schemeMq.addEventListener("change", onScheme);

    let w = 0, h = 0, dpr = 1, cx = 0, cy = 0;
    let stars: Star[] = [];

    const reset = (s: Star) => {
      s.x = Math.random() * 2 - 1;
      s.y = Math.random() * 2 - 1;
      s.z = Math.random();
      s.pz = s.z;
      s.c = COLORS[(Math.random() * COLORS.length) | 0];
    };
    const seed = () => {
      stars = Array.from({ length: COUNT }, () => {
        const s = { x: 0, y: 0, z: 0, pz: 0, c: COLORS[0] };
        reset(s);
        return s;
      });
    };
    const resize = () => {
      w = host.clientWidth; h = host.clientHeight;
      dpr = Math.min(window.devicePixelRatio || 1, 2);
      canvas.width = w * dpr; canvas.height = h * dpr;
      ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
      cx = w / 2; cy = h / 2;
      seed();
    };

    const dz = 0.005 * SPEED;
    let raf = 0;
    const frame = () => {
      ctx.fillStyle = BG; ctx.fillRect(0, 0, w, h);
      for (const s of stars) {
        s.pz = s.z; s.z -= dz;
        if (s.z <= 0.02) { reset(s); continue; }
        const sx = (s.x / s.z) * cx + cx;
        const sy = (s.y / s.z) * cy + cy;
        const px = (s.x / s.pz) * cx + cx;
        const py = (s.y / s.pz) * cy + cy;
        ctx.globalAlpha = Math.min(1, 1 - s.z);
        ctx.strokeStyle = s.c;
        ctx.lineWidth = (1 - s.z) * 2.4;
        ctx.beginPath(); ctx.moveTo(px, py); ctx.lineTo(sx, sy); ctx.stroke();
      }
      ctx.globalAlpha = 1;
      raf = requestAnimationFrame(frame);
    };

    const ro = new ResizeObserver(resize);
    ro.observe(host); resize(); frame();
    return () => { cancelAnimationFrame(raf); ro.disconnect(); if (schemeMq) schemeMq.removeEventListener("change", onScheme); };
  }, []);

  return (
    <canvas ref={canvasRef} style={{ position: "absolute", inset: 0, width: "100%", height: "100%", display: "block" }} />
  );
}

Copy or download any file. The HTML build loads Three.js from a CDN via an import map, so it runs by just opening index.html. The React build is react-three-fiber; setup notes live at the top of each file.

Variants

40 of this pattern · page 1 of 4