an aurora borealis palette any colour enters and travels, for glows and trails
npx shadcn add @ag/auroraAlready installed? Preview an update
npx shadcn add @ag/aurora --dry-runYour installed source stays unchanged until you update it. How updates work
the demo above, verbatim
"use client"
import { Sample } from "@/app/samples"
import { aurora } from "@/registry/base-nova/lib/aurora"
import { Travel } from "@/registry/base-nova/lib/shade.demo"
// #region aurora
/** Three people's own colours, each entering the aurora at its own place: the aurora's
* green, a person in blue, a person in orange (off the palette, so it enters at its
* nearest end). Left to right is the travel; above the whole aurora (span 1, one face
* alone), below a short travel (span 0.3, a crowd, each near its own colour). */
const PEOPLE = [
{ name: "green", css: "oklch(0.87 0.19 150)", hue: 150 },
{ name: "blue", css: "oklch(0.72 0.15 245)", hue: 245 },
{ name: "orange", css: "oklch(0.78 0.16 60)", hue: 60 },
]
const WHOLE = aurora(1)
const CROWD = aurora(0.3)
// #endregion
export default function AuroraDemo() {
return (
<Sample
name="aurora"
glance="wide"
title="one colour, its aurora"
description="span 1 above, 0.3 below"
with="aurora"
>
<div className="dark space-y-3 rounded-xl bg-black p-4 text-foreground">
{PEOPLE.map((p) => (
<div key={p.name} className="flex items-center gap-4">
<p className="w-12 font-mono text-muted-foreground text-xs">
{p.name}
</p>
<div className="flex-1 space-y-1.5">
<Travel shade={WHOLE} base={p} />
<Travel shade={CROWD} base={p} />
</div>
</div>
))}
</div>
</Sample>
)
}the source, every file an install writes
import type { Shade } from "@/registry/base-nova/lib/shade"
// AURORA: a colour effect (a lib/shade), the palette an aurora borealis moves through,
// entered from any one colour.
//
// An aurora's colours are its altitude and its energy: yellow-green at the lowest,
// brightest edge, green through most of the curtain, then aqua and blue, violet, and
// magenta at the top and where it flares. A curtain does not wear one of them, it
// TRAVELS them: brighter, more energetic light sits further along, and as light rises
// (and fades) it moves on. The palette is that order of hues (`AURORA.hues`).
//
// Any colour can enter it: a person's own colour finds its nearest place on the
// palette and moves from there. `span` says how much of the palette it may travel: 1
// the whole aurora (green to magenta, for one face alone on a stage), smaller the hues
// nearest its own (so a crowd of faces, each in its own colour, stays told apart). At
// the end of the palette it turns back, never wrapping from magenta to yellow-green.
//
// `x` is how far along the travel a thing is: 0 its own colour, 1 the farthest it may
// go (a peak, the oldest of a trail, the top of a glow). Lightness and chroma stay the
// base's, eased a little with the travel, so a pale colour makes a pale aurora.
//
// The colours are CSS relative colours (`oklch(from <base> …)`): the same in a
// stylesheet, an inline style or a canvas (resolve them once through
// `getComputedStyle` where a canvas needs plain colours). A browser without relative
// colours keeps the base.
export const AURORA = {
/** The aurora's hues in the order it travels them (OKLCH degrees): yellow-green,
* green, aqua, blue, violet, magenta. */
hues: [128, 150, 182, 245, 292, 330] as readonly number[],
/** Lightness eased away over the whole travel; chroma kept at its far end. */
lightness: 0.08,
chroma: 0.85,
}
/** The signed shortest turn from hue `a` to hue `b`, degrees. */
const turn = (a: number, b: number) => ((b - a + 540) % 360) - 180
/** Where a hue sits on the palette (0 .. hues.length - 1, fractional): between the two
* hues it falls between, else at the end it is nearest. Exact, so travel starts AT the
* base's own hue, never with a jump to the nearest palette hue. */
export function placeOf(hue: number): number {
const hues = AURORA.hues
for (let i = 0; i < hues.length - 1; i++) {
const span = turn(hues[i] as number, hues[i + 1] as number)
const along = turn(hues[i] as number, hue)
if (
Math.sign(along) === Math.sign(span) &&
Math.abs(along) <= Math.abs(span)
)
return i + along / span
}
const first = Math.abs(turn(hue, hues[0] as number))
const last = Math.abs(turn(hue, hues[hues.length - 1] as number))
return first <= last ? 0 : hues.length - 1
}
/** The hue at palette position `p` (fractional), between its two hues the short way. */
function hueAt(p: number): number {
const hues = AURORA.hues
const i = Math.min(hues.length - 2, Math.floor(p))
const a = hues[i] as number
return (a + turn(a, hues[i + 1] as number) * (p - i) + 360) % 360
}
/** The aurora's hue `x` (0..1) along a travel of `span` (0..1 of the palette) from
* `baseHue`: forward from its nearest place, turning back at the palette's end. */
export function auroraHue(baseHue: number, x: number, span: number): number {
const last = AURORA.hues.length - 1
let p = placeOf(baseHue) + Math.max(0, x) * Math.max(0, span) * last
if (p > last) p = Math.max(0, 2 * last - p)
return hueAt(p)
}
/** The aurora's colour `x` along its travel, entered from `base` (any CSS colour) whose
* hue is `baseHue`. A base off the palette (an orange) enters at its nearest end; a
* base on it travels from its own hue exactly. */
export function auroraColor(
base: string,
baseHue: number,
x: number,
span: number,
): string {
const t = Math.min(1, Math.max(0, x))
const hue = auroraHue(baseHue, t, span)
const dl = -AURORA.lightness * t
const c = 1 - (1 - AURORA.chroma) * t
return `oklch(from ${base} calc(l + ${dl.toFixed(4)}) calc(c * ${c.toFixed(4)}) ${hue.toFixed(2)})`
}
/** The aurora as a shade (lib/shade), travelling `span` of the palette (default the
* whole of it): what a drawing that takes a shade wears. */
export function aurora(span = 1): Shade {
if (!(span >= 0 && span <= 1))
throw new Error(`aurora: span ${span}; it is a share of the palette, 0..1`)
return {
name: `aurora ${span}`,
color: (base, baseHue, x) => auroraColor(base, baseHue, x, span),
}
}