heliograph · v0.1 · 5 kB gzipped

One light
for the whole page.

Every shadow on the web is hand-tuned and none of them agree. Heliograph puts a single physical light above your page, then derives every shadow, highlight and rim from it — following your cursor, or the real sun where you are.

Get started

npm i @jihoo12/heliograph

z 4resting card
z 16popover
z 9gloss · hover me
z 36floating
01 · materials

Same light,
different stuff.

Materials only change how a surface answers the light — sharpness of the hotspot, brightness of the rim, how much colour bleeds in. Shadows stay consistent across all of them, because they come from the same source. Hover to lift, press to push down.

01Matte
02Satin
03Glass
04Gloss
05Metal
02 · daylight

Your site
has a time of day.

In sun mode Heliograph computes the solar altitude and azimuth for the visitor's location — guessed from the time zone, no permission prompt. Long amber shadows at 7 am, short crisp ones at noon, violet dusk, and a cool moon after dark. Try the slider in the dock.

∠

Real projection

Shadows are cast with similar triangles, not guessed. Taller elements throw longer, softer shadows, and the direction changes across the screen with a point light.

K

Colour temperature

Light colour comes from black-body Kelvin. Shadows are tinted by the sky, not black: blue at noon, violet at sunset, the way painters always did it.

⌁

Cheap to run

One requestAnimationFrame loop that sleeps when nothing moves, batched layout reads, off-screen elements skipped, and only changed variables written.

03 · quick start

Two attributes.
That's it.

Mark surfaces with data-hg="surface" and give them a height in pixels. Heliograph writes CSS variables; the bundled stylesheet turns them into shadows, highlights and rims — or use the variables yourself.

index.html
<link rel="stylesheet" href="heliograph.css">

<div data-hg="surface"
     data-hg-z="8"
     data-hg-z-hover="20"
     data-hg-material="glass">
  A card lit like it's real.
</div>

<h1 data-hg="text">Shadowed type</h1>
<div data-hg="lamp"></div> <!-- optional glow -->
main.js
import { createScene } from '@jihoo12/heliograph';

const scene = createScene({
  mode: 'pointer',     // or 'sun' | 'orbit' | 'fixed'
  light: { height: 700, color: 5600 },
});

// Let the real sun drive it — a day per minute
scene.set({ mode: 'sun', sun: { speed: 1440 } });

// Or pin it to golden hour
scene.setTime(18.5);

// Drive your own canvas / WebGL from the same light
scene.on('frame', (light) => draw(light));
04 · how it works

A light, a table,
similar triangles.

Your page is a table seen from above. Each surface hovers z pixels over it. A light sits at height h. Where its rays clip the surface's edge and land on the table is where the shadow goes — the same rule for every element, so they all agree.

h z shadow
offset = (C − L) · z ⁄ (h − z)

C is the element's centre, L the light's position on screen. For the sun, h → ∞ and the offset becomes z · cot(altitude) along the azimuth. Blur grows with z to fake the penumbra of an area light; four layers plus sky occlusion keep it soft.

What gets written

Per element, every frame something changes:

--hg-shadowlayered box-shadow
--hg-text-shadowfor data-hg="text"
--hg-lx --hg-lyspecular hotspot, local %
--hg-angledirection to the light, CSS deg
--hg-spec --hg-facing0–1 strengths

And once on <html>: --hg-light (rgb triple), --hg-intensity, --hg-sky-top, --hg-sky-bottom, --hg-shadow-rgb, --hg-night, --hg-light-x/y. This page's whole day/night palette is built from --hg-night.

05 · reference

The whole API.

Small on purpose. Everything else is CSS you already know.

AttributeMeaning
data-hg="surface"Casts a shadow, gets a specular hotspot and rim light. Uses ::before/::after.
data-hg="text"Casts a text-shadow. Combine: data-hg="surface text".
data-hg="lamp" · "sky"A glow that sits at the light; a background painted with the current sky. CSS only.
data-hg-z · -z-hover · -z-activeHeight in px at rest, hovered, pressed. Transitions are animated by the light loop.
data-hg-materialmatte · satin · gloss · glass · metal
OptionDefault
mode'pointer' · 'sun' · 'orbit' · 'fixed'
light{ x: .5, y: .15, height: 700, color: 5600, intensity: 1 }
sun{ date: null, lat: null, lon: null, speed: 0 } — nulls mean now / guessed from time zone
shadow{ strength: 1, softness: 1, color: null } — null colour = tinted by sky
sky · idle · smoothing35 · 'auto' · 7
Method
set(opts) · setTime(date | hour)Change anything live; jump the sun.
on('frame', fn)Get the light state each frame to drive canvas/WebGL. Returns unsubscribe.
add(el) · remove(el) · refresh() · destroy()New DOM is picked up automatically via MutationObserver.
--:--