Workspace / component / backgrounds
Bokeh Background
paxfx add blocks/component-backgrounds-bokehlocal onlyTier: medium. Reduced motion: Discs render in fixed positions.
"use client";
import type { CSSProperties, ReactNode } from "react";
import { cn, useReducedMotion } from "@pax-sh/blocks";
export interface BokehBackgroundProps {
/** 0 to 1; overall visibility of the light discs. */
intensity?: number;
/** Multiplier on drift speed. 1 is the tuned default. */
speed?: number;
className?: string;
children?: ReactNode;
}
interface Disc {
x: number;
y: number;
size: number;
tone: "acid" | "warm" | "mute";
}
const TONES: Record<Disc["tone"], string> = {
acid: "color-mix(in srgb, var(--pax-acid,#c8ff00) 70%, transparent)",
warm: "rgba(255,244,214,0.9)",
mute: "color-mix(in srgb, var(--pax-mute,#6b645c) 40%, transparent)",
};
const FAR: Disc[] = [
{ x: 12, y: 22, size: 48, tone: "warm" },
{ x: 34, y: 70, size: 40, tone: "acid" },
{ x: 58, y: 16, size: 56, tone: "mute" },
{ x: 76, y: 62, size: 44, tone: "warm" },
{ x: 90, y: 30, size: 52, tone: "acid" },
{ x: 22, y: 88, size: 46, tone: "mute" },
];
const MID: Disc[] = [
{ x: 20, y: 44, size: 104, tone: "acid" },
{ x: 50, y: 78, size: 92, tone: "warm" },
{ x: 68, y: 34, size: 118, tone: "mute" },
{ x: 88, y: 74, size: 96, tone: "acid" },
];
const NEAR: Disc[] = [
{ x: 8, y: 74, size: 210, tone: "warm" },
{ x: 52, y: 10, size: 184, tone: "acid" },
{ x: 86, y: 52, size: 232, tone: "mute" },
];
/**
* Out-of-focus light discs at three depths drifting on independent axes.
* Depth is faked honestly: far discs are small, sharp-ish, and slow; near
* discs are large, heavily blurred, and drift furthest. Positions are a
* fixed deterministic set. Under reduced motion every disc holds its place.
*/
export function BokehBackground({
intensity = 0.8,
speed = 1,
className,
children,
}: BokehBackgroundProps) {
const reduced = useReducedMotion();
const clamped = Math.min(1, Math.max(0, intensity));
const dur = (seconds: number) => `${seconds / Math.max(0.1, speed)}s`;
const depth = (discs: Disc[], blur: number, opacity: number, animation: string) => (
<div
aria-hidden="true"
className="pointer-events-none absolute -inset-[10%] will-change-transform"
style={{ animation: reduced ? "none" : animation }}
>
{discs.map((disc, i) => {
const style: CSSProperties = {
left: `${disc.x}%`,
top: `${disc.y}%`,
width: disc.size,
height: disc.size,
background: `radial-gradient(circle, ${TONES[disc.tone]} 0%, transparent 72%)`,
filter: `blur(${blur}px)`,
opacity: opacity * clamped,
};
return (
<div
key={i}
className="absolute -translate-x-1/2 -translate-y-1/2 rounded-full"
style={style}
/>
);
})}
</div>
);
return (
<div
className={cn(
"relative isolate overflow-hidden rounded-2xl border border-[var(--pax-border,rgba(18,17,15,0.12))]",
"bg-[var(--pax-paper,#f6f2ea)] text-[var(--pax-ink,#12110f)]",
className,
)}
data-pax-asset="component-backgrounds-bokeh"
>
<style>{`
@keyframes pax-bokeh-drift-far {
from { transform: translate3d(-1.5%, 1%, 0); }
to { transform: translate3d(1.5%, -1%, 0); }
}
@keyframes pax-bokeh-drift-mid {
from { transform: translate3d(2%, 1.5%, 0); }
to { transform: translate3d(-2%, -1.5%, 0); }
}
@keyframes pax-bokeh-drift-near {
from { transform: translate3d(-3%, -2%, 0); }
to { transform: translate3d(3%, 2%, 0); }
}
`}</style>
{depth(FAR, 6, 0.7, `pax-bokeh-drift-far ${dur(36)} ease-in-out infinite alternate`)}
{depth(MID, 14, 0.65, `pax-bokeh-drift-mid ${dur(28)} ease-in-out infinite alternate`)}
{depth(NEAR, 30, 0.6, `pax-bokeh-drift-near ${dur(22)} ease-in-out infinite alternate`)}
<div className="relative z-[1]">{children}</div>
</div>
);
}