Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion content/docs/components/radiant-lines.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ description: A hyperspace starfield background — colored streaks from the cent

import { RadiantLinesDemo } from "@/registry/radiant-lines/radiant-lines-demo"

Canvas starfield in the spirit of those GSAP hyperspace scroll demos: stars project from the center as streaks. Scroll velocity boosts warp; release and it eases back to a drift.
Canvas starfield in the spirit of those GSAP hyperspace scroll demos: stars project from the center as streaks. Scroll velocity boosts warp; `displacement` scales how far each star jumps. Release and it eases back to a drift.

<RadiantLinesDemo />

Expand Down Expand Up @@ -63,6 +63,7 @@ export function Hero() {
| --- | --- | --- |
| `colors` | `string[]` | coral / teal / amber / blue / pink / lime / slate |
| `starCount` | `number` | `420` |
| `displacement` | `number` | `1` — how far each star jumps on warp / scroll |
| `containerRef` | `RefObject<HTMLElement \| null>` | React — window scroll |
| `container` | `HTMLElement` | Svelte — window scroll |
| `className` | `string` | React only |
Expand Down
2 changes: 1 addition & 1 deletion public/r/radiant-lines-svelte.json

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion public/r/radiant-lines.json

Large diffs are not rendered by default.

13 changes: 12 additions & 1 deletion registry/radiant-lines/radiant-lines-demo.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import { RadiantLines } from "@/registry/radiant-lines/radiant-lines"

const DEFAULTS = {
starCount: 420,
displacement: 1,
colors: [
"#FF6B4A",
"#2DD4BF",
Expand Down Expand Up @@ -55,12 +56,13 @@ export function RadiantLinesDemo() {
>
<div
ref={scrollerRef}
className="no-scrollbar relative h-[56svh] w-full snap-y snap-mandatory overflow-y-auto overscroll-contain rounded-[inherit] bg-background"
className="relative no-scrollbar h-[56svh] w-full snap-y snap-mandatory overflow-y-auto overscroll-contain rounded-[inherit] bg-background"
>
<div className="pointer-events-none sticky top-0 h-[56svh]">
<RadiantLines
containerRef={scrollerRef}
starCount={props.starCount}
displacement={props.displacement}
colors={props.colors}
/>
</div>
Expand Down Expand Up @@ -94,6 +96,7 @@ export function RadiantLinesDemo() {
component="RadiantLines"
snippetProps={{
starCount: props.starCount,
displacement: props.displacement,
colors: props.colors,
}}
>
Expand Down Expand Up @@ -140,6 +143,14 @@ export function RadiantLinesDemo() {
step={20}
onChange={(v) => updateProp("starCount", v)}
/>
<ControlSlider
label="Displacement"
value={props.displacement}
min={0.2}
max={3}
step={0.1}
onChange={(v) => updateProp("displacement", v)}
/>
</ComponentControls>
</>
)
Expand Down
32 changes: 28 additions & 4 deletions registry/radiant-lines/radiant-lines-vanilla.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,12 @@ export type RadiantLinesOptions = {
colors?: string[]
/** How many stars. Default 420 */
starCount?: number
/**
* How far each star jumps when warping (idle drift and scroll).
* `1` matches the original ~60fps travel; higher values lengthen streaks.
* Default `1`.
*/
displacement?: number
/**
* Scroll container. Omit / `null` to use the window.
* Pass the overflow scroller when the component lives inside one.
Expand All @@ -29,6 +35,10 @@ const DEPTH = 1000
const STIFFNESS = 70
const DAMPING = 32
const MASS = 0.5
/** Original per-frame coefficient (`2.4`) converted to per-second at 60fps. */
const TRAVEL_PER_SEC = 2.4 * 60
/** Draw streaks as a 60fps step so filled heads keep a visible tail at any fps. */
const TRAIL_DT = 1 / 60

type Star = {
x: number
Expand All @@ -42,6 +52,11 @@ function paletteOf(colors?: string[]) {
return colors && colors.length > 0 ? colors : DEFAULT_COLORS
}

function displacementOf(value?: number) {
if (typeof value !== "number" || !Number.isFinite(value)) return 1
return Math.max(0, value)
}

function createStars(count: number, colors: string[]): Star[] {
const stars: Star[] = []
for (let i = 0; i < count; i++) {
Expand All @@ -60,7 +75,8 @@ function createStars(count: number, colors: string[]): Star[] {
/**
* Hyperspace starfield — colored streaks radiate from the center.
* Transparent canvas over `bg-background` (shadcn theme). Warp speed follows
* scroll velocity (down → inward, up → outward).
* scroll velocity (down → inward, up → outward). `displacement` scales how
* far each star travels (and how long the filled heads' streaks get).
*/
export function createRadiantLines(
canvas: HTMLCanvasElement,
Expand All @@ -69,6 +85,7 @@ export function createRadiantLines(
let options: RadiantLinesOptions = {
starCount: 420,
colors: DEFAULT_COLORS,
displacement: 1,
container: null,
...initial,
}
Expand Down Expand Up @@ -177,7 +194,8 @@ export function createRadiantLines(
applyWarp(0)
}

const accel = (STIFFNESS * (speedTarget - speed) - DAMPING * speedVel) / MASS
const accel =
(STIFFNESS * (speedTarget - speed) - DAMPING * speedVel) / MASS
speedVel += accel * dt
speed += speedVel * dt

Expand All @@ -192,13 +210,13 @@ export function createRadiantLines(
const warp = reduce ? 0.05 : speed
const focal = Math.max(w, h) * 0.5
const colors = paletteOf(options.colors)
const travel = warp * TRAVEL_PER_SEC * displacementOf(options.displacement)

ctx.clearRect(0, 0, w, h)

for (let i = 0; i < stars.length; i++) {
const star = stars[i]!
star.pz = star.z
star.z -= dir * warp * 2.4 * dt
star.z -= dir * travel * dt

if (star.z <= 1) {
star.z = DEPTH
Expand All @@ -212,6 +230,12 @@ export function createRadiantLines(
star.x = (Math.random() - 0.5) * DEPTH
star.y = (Math.random() - 0.5) * DEPTH
star.color = colors[Math.floor(Math.random() * colors.length)]!
} else {
// FPS-independent tail so filled heads keep a visible streak.
star.pz = Math.min(
DEPTH,
Math.max(1.01, star.z + dir * travel * TRAIL_DT)
)
}

const k = focal / star.z
Expand Down
3 changes: 3 additions & 0 deletions registry/radiant-lines/radiant-lines.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
class: className = "",
colors = DEFAULT_COLORS,
starCount = 420,
displacement = 1,
container,
}: Props = $props()

Expand All @@ -34,6 +35,7 @@
instance = createRadiantLines(canvas, {
colors,
starCount,
displacement,
container: container ?? null,
})
return () => {
Expand All @@ -46,6 +48,7 @@
instance?.setOptions({
colors,
starCount,
displacement,
container: container ?? null,
})
})
Expand Down
5 changes: 4 additions & 1 deletion registry/radiant-lines/radiant-lines.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ export function RadiantLines({
className,
colors,
starCount = 420,
displacement = 1,
containerRef,
}: RadiantLinesProps) {
const canvasRef = useRef<HTMLCanvasElement>(null)
Expand All @@ -44,6 +45,7 @@ export function RadiantLines({
instanceRef.current = createRadiantLines(canvas, {
colors,
starCount,
displacement,
container: containerRef?.current ?? null,
})
return () => {
Expand All @@ -58,9 +60,10 @@ export function RadiantLines({
instanceRef.current?.setOptions({
colors,
starCount,
displacement,
container: containerRef?.current ?? null,
})
}, [colors, starCount, containerRef])
}, [colors, starCount, displacement, containerRef])

return (
<div
Expand Down
Loading