Color comes from the grating equation
Drag the lamp angle and groove period to see where each diffraction order lands.
λ = d · sin θ / m
At small angles every order falls outside the visible range and the foil stays silver. As the angle grows the first order sweeps violet to red, then the second and third orders follow and overlap.
- Order 1795 nm
- Order 2397 nm
- Order 3265 nm
The pointer is the lamp
This section is the CSS fallback, following the same rules as the WebGL version: the lamp sits 300 px above the screen and orbits after 4 idle seconds.
Move the pointer: the large panel's bands turn around the lamp, the star lights two opposite radii, and glitter flashes facet by facet.
A material is a groove layout
Five materials share one grating function; brushed and pearl need per-pixel rendering, so see them in the live study above.
Film
Two crossed gratings averaged over brushed silver. The everyday holographic film.
Star
Concentric grooves: only the radius facing the lamp, and its opposite, shows color.
Glitter
Facets at random angles; only those whose grating meets the eye flash.
- WebGL →
Brushed
One straight grating with streaks along the grooves, stretching color into long bands. WebGL only.
- WebGL →
Pearl
Thin-film interference instead of a grating: varying thickness shifts color softly, like a soap bubble. WebGL only.
Components
CSS-fallback buttons, switch, redeem field, and card. Ink sits only on light foil.
No packs opened
On: diffraction over silver
Format: three groups of four
Palette
Sheet, silver, and ink are fixed; color comes only from the spectrum.
- Sheet#0c0b10
- Panel#141319
- Silver#c9c8d4
- Ink#16151c
- Text#ececf2
- Secondary#a9a8b6
Spectrum strip: silver in the middle where wavelengths are invisible, then orders one, two, and three outward
One WebGL context for the page
01
One hidden canvas
Browsers cap live WebGL contexts, so the page opens a single WebGL2 context and switches materials by uniform.
02
Render each element
Resize the viewport to the element, pass the lamp position, material, and tilt, then draw one full-screen triangle.
03
Copy into place
drawImage copies the result into the element's own 2D canvas; elements whose lamp, size, and tilt are unchanged are skipped.
Touch lands on the material
Press, toggle, and drag bend only the shading normal; the color jumps while the element stays put.
| Max tilt | 14° | Toward the press point |
|---|---|---|
| Stiffness | 320 | Return on release |
| Damping ratio | 0.32 | One overshoot |
| Toggle impulse | 260 | Switch, check, open a pack |
Do and don't
- Compute color with the grating equation λ = d * |dot(h, k)| / m, d = 1500 nm, summing orders 1-3 at weights 1, 0.5, 0.3
- Treat the pointer as a lamp 300 px above the screen shared by every foil element; after 4 idle seconds let it orbit slowly
- Seat the foil on a bg-[#0c0b10] print sheet with text-[#ececf2] body copy and text-[#a9a8b6] secondary text
- Use one WebGL2 context for the page; render each element in turn and copy it into its own 2D canvas with drawImage
- Ship a CSS fallback: window a 1024x1 spectrum strip by lamp direction, rotate a ::before layer to the groove angle, and screen it over silver
- Bend only the shading normal on press, toggle, and drag; spring back with stiffness 320 and damping ratio 0.32
- Print ink text only on foil-light; mark focus with focus-visible:outline-[#c9c8d4]
- Honor reduced motion: park the lamp at 72%, 22%, no orbit and no overshoot
- Do not fake foil with a fixed rainbow gradient and an animated background-position
- Do not open one WebGL context per element
- Do not flood the surface with rainbow; most of it should read as silver
- Do not place text on saturated spectral bands
- Do not cycle colors with hue-rotate animations
- Do not interleave layout reads with style writes; measure everything, then write
Reproduction prompt
A complete specification: grating, lamp, materials, single context, fallback, and checks. Compare the result with the live study above.
Create one polished, responsive, self-contained HTML file titled “Diffraction Foil”. It must work when opened directly in a modern browser. Use native HTML, CSS, JavaScript, WebGL2, and Canvas 2D only. Do not use libraries, frameworks, modules, build tools, image files, image URLs, or network requests other than one optional web font. Return the complete HTML and nothing else. The subject is holographic hot-stamping foil rendered by its physics, not a rainbow gradient. Color comes from a diffraction grating lit by a lamp that follows the pointer. Every rule below exists so the foil reads as metal that changes with angle. 1. Page Use a near-black print sheet (#0c0b10) with faint crop marks. At the top, a wide trading card with a 1.8:1 aspect ratio, about 72% of the stage height, rounded corners at 4.8% of the short side. The brand name “Lumen” is stamped in silver foil across the card; a thin glitter border runs inside the edge. Clicking or pressing Enter flips the card to a back face with a guilloché rosette. Below the card, lay out a compact component sheet on dark panels: a foil swatch row, a foil button, a switch, a slider, a progress bar, a badge, and a round seal. On screens under 680 pixels, use a portrait 63:88 card and a single-column sheet. 2. The lamp Treat the pointer as a small lamp 300 CSS pixels above the screen plane. Store its position in viewport pixels and ease toward the target every animation frame with factor 1 - exp(-dt * 16), clamping dt to 0..50 ms because frame timestamps can arrive out of order. When the pointer has not moved for 4 seconds, leaves the window, or the device has no hover, let the lamp drift on its own: x = width * (0.5 + 0.42 sin(0.21 t)), y = height * (0.4 + 0.32 sin(0.33 t + 1.2)), easing with 1 - exp(-dt * 2.5). With device orientation available, offset the lamp by up to 35% of the viewport from a slowly re-centering baseline (blend 2% per event toward the current beta and gamma, divide the difference by 25 degrees, clamp to -1..1). With prefers-reduced-motion, park the lamp at 72% x, 22% y and never orbit. 3. Spectral color Convert wavelength in nanometres to color with the Wyman, Sloan and Shirley 2013 piecewise-Gaussian fit of the CIE 1931 matching functions. X = 1.056 g(l, 599.8, 37.9, 31) + 0.362 g(l, 442, 16, 26.7) - 0.065 g(l, 501.1, 20.4, 26.2); Y = 0.821 g(l, 568.8, 46.9, 40.5) + 0.286 g(l, 530.9, 16.3, 31.1); Z = 1.217 g(l, 437, 11.8, 36) + 0.681 g(l, 459, 26, 13.8), where g(x, m, a, b) uses spread a below the mean and b above it. Return black outside 380..780 nm. Convert to linear sRGB with the standard matrix, pull each channel 34% toward Y so pure spectral colors do not break at the sRGB gamut edge, clamp negatives to zero, and scale by 0.8. Tone-map the final color with the Narkowicz ACES fit and encode to sRGB. 4. Diffraction grating For a point on the foil, take the unit direction to the lamp and the unit direction to the viewer (straight out of the screen). Project both onto the foil plane and add them; call the 2D sum h. The groove normal k is a unit vector in the plane. With groove period d = 1500 nm, order m reflects wavelength l = d * |dot(h, k)| / m. Sum orders 1, 2 and 3 with weights 1, 0.5 and 0.3, multiply by 1.6, then tone-map. Where |dot(h, k)| is small the foil shows silver; where it grows the bands sweep through violet, blue, green, yellow and red, then repeat in higher orders. Never paint a fixed gradient and animate its position. 5. Materials Implement each material as a different groove layout feeding the same grating function: - Film: two crossed gratings (k and k rotated 90 degrees), averaged, over brushed silver. - Brushed: one straight grating at a chosen angle plus anisotropic brushed-metal streaks along the grooves. - Star: concentric grooves, so k is the radial direction from the element center; only the radius pointing toward the lamp, and its opposite, lights up. - Glitter: a field of small facets with random groove angles and tilts on a dark base; a facet flashes only when its grating sends color toward the viewer. - Pearl: thin-film interference instead of a grating, thickness 300 + 260 noise nm, refractive index 1.38, twelve wavelength samples from 400 to 697 nm, normalized per channel. Add a soft specular highlight where the lamp reflects and keep the base silver between the colored bands. 6. One WebGL context for every element Browsers cap simultaneous WebGL contexts, so never create one per element. Create a single hidden WebGL2 canvas and one fragment shader with a material uniform. Each foil element owns a plain 2D canvas sized to its CSS box times devicePixelRatio (capped at 2). For each visible element, resize the shared viewport to that element, set its rectangle, the lamp position relative to it, its material and its current tilt, draw one full-screen triangle, then copy the result into the element canvas with drawImage. Skip elements whose lamp position, size, tilt and texture version have not changed since the last frame. Use IntersectionObserver so offscreen elements cost nothing. 7. CSS fallback Offer a WebGL / CSS toggle and fall back automatically when WebGL2 is unavailable. Precompute a 1024 by 1 spectrum strip for t from -1.25 to 1.25, coloring each texel with the grating at |t|. For every foil element, compute the vector from the lamp to its center in CSS pixels, set --k to its angle, and select a window of the strip: along k, t(x) = x / sqrt(x^2 + 300^2) at the element center and at both ends, widened 2.6 times so one element shows two or three colors. Paint the strip in a ::before layer rotated by --k with background-size and background-position from that window, blended with screen over a silver gradient (overlay on light variants that carry ink text). Add an ::after layer with a radial highlight at the lamp position and a tiled sparkle texture. Read every element rectangle first, then write all custom properties, to avoid layout thrashing. 8. Touch and press Pressing a foil control bends its surface: on pointer down, tilt the normal up to 14 degrees toward the pressed point; on release, spring back with stiffness 320 and damping ratio 0.32 so it overshoots once. Toggling a switch or checkbox adds a short angular impulse; dragging the slider tilts toward the drag direction. Only the shading normal changes; the element itself does not move. The card tilts a few degrees toward the pointer and flips on click with a spring. 9. Access and performance All controls are native buttons and inputs, reachable by keyboard, with visible focus rings. Ink text sits on light foil, never on saturated bands. With reduced motion, stop orbiting, remove spring overshoot, and keep a stable frame. Pause rendering while the document is hidden. Keep the page under one shared WebGL context and redraw only changed elements. 10. Inspect before returning Check these observable conditions: moving the pointer moves the rainbow bands across every foil element at once, centered on the lamp; leaving the pointer still for four seconds starts a slow orbit; the colors run violet to red as the angle grows and repeat in higher orders; star shows two opposite colored rays; glitter flashes individual facets; there is exactly one WebGL context; the CSS toggle keeps the same layout and still follows the lamp; reduced motion yields a still frame; DPR 2 doubles each element canvas backing store.
Checks
Each condition should be observable in the result.
| Lamp | Bands move across every foil element at once, centered on the lamp |
|---|---|
| Idle | After 4 idle seconds the lamp starts a slow orbit |
| Spectrum | Color runs violet to red as the angle grows and repeats in higher orders |
| Contexts | Exactly one WebGL context on the page |
| Fallback | The CSS fallback keeps the layout and still follows the lamp |
| Reduced motion | The lamp parks and the frame holds still |