npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

schiller

v0.1.0

Published

Structural colour for arbitrary artwork. Thin-film and diffraction-grating iridescence on any image, canvas or SVG, with a light you move.

Downloads

146

Readme

schiller

Structural colour for the web. Give it an image, an SVG or a Three.js material, and it puts real iridescence on it — the kind that shifts as the light moves, because it is computed from angles rather than painted on.

See the nineteen specimens →

npm install schiller

Zero runtime dependencies. No build step. Plain ES modules.


What "structural colour" means

A red shirt is red because the dye swallows every other colour. Structural colour has no dye in it at all: the surface is built with features about as small as a light wave is long, and that structure sorts white light into colours. Grind it to powder and the colour disappears, because you have destroyed the shape.

You have seen it all your life — a soap bubble, a puddle with oil on it, the back of a CD, a butterfly's wing, the inside of a shell.

The important part, and the reason this is a shader rather than a texture: the answer depends on the angle you are looking from. Tilt a soap bubble and a different colour survives. So the colour is not a property of the object, it is a property of the object and where you are standing. Move, and it changes.

schiller is the mineralogist's word for exactly this — the blue flash inside labradorite, which is not a colour the stone contains but one its layers make.


Quick start

On an image, a canvas or an SVG

import { createSchiller } from 'schiller';

const canvas = document.querySelector('canvas');

const mica = await createSchiller(canvas, {
  source: '/butterfly.png',   // a URL, an SVG string, an <img>, or a <canvas>
  finish: 'wing',             // which material it is
  grain: 'contour',           // which way the microstructure runs
});

// Let the viewer move the light.
canvas.addEventListener('pointermove', (e) => mica.pointerMove(e));
canvas.addEventListener('pointerleave', () => mica.pointerLeave());

That is the whole thing. It animates on its own when nobody is touching it, and it respects prefers-reduced-motion.

Remember to mica.dispose() when the element goes away. Each instance holds a WebGL context and browsers only allow about sixteen.

On a Three.js material

import { MeshPhysicalMaterial } from 'three';
import { patchMica } from 'schiller/three';

const material = new MeshPhysicalMaterial({ color: 0x1b1d23, metalness: 1, roughness: 0.2 });
const handle = patchMica(material, { finish: 'dichroic' });

// Later, to move the light:
handle.setLight(x, y, z);

patchMica takes a material you already made and rewrites its shader in place. The library never imports Three — Three is an optional peer dependency, and you only need it for this entry point.


The finishes

Pick one by name. Each is a set of tuned numbers, not a texture.

| 2D — createSchiller | looks like | | --- | --- | | wing | a butterfly's wing | | nacre | mother-of-pearl | | pearl | a pearl's soft lustre | | dichroic | coated dichroic glass | | foil | a sweet wrapper | | disc | the back of a CD | | brushed | brushed metal | | micaDust | pearlescent powder | | glitter | coarse glitter | | holoFlake | holographic sticker | | carapace | a beetle's shell |

schiller/three has twenty, including nacreous, pearlGlaze, frost, tarnish, machined, holoGlass, dichroicCrystal, dichroicSheet, pressed and retro (retroreflective, like a number plate).

import { FINISHES } from 'schiller';
import { THREE_FINISHES } from 'schiller/three';

Grain — which way the structure runs

Structural colour needs a direction, and that direction is the second half of what makes something look like itself. Same finish, different grain, different material.

| grain | the microstructure follows | good for | | --- | --- | --- | | 'contour' | the shape's own outline | wings, shells, wrappers | | 'luminance' | the artwork's light and shade | anything already shaded | | 'angle' | one fixed direction (set angle) | brushed metal, machined parts | | 'particle' | nothing — discrete flakes | glitter, pearlescent powder |


Tuning it

Every finish is a starting point. setMaterial patches individual numbers.

mica.setMaterial({ sheen: 1.2, grainFreq: 180 });

The ones worth knowing:

| | what it does | | --- | --- | | sheen | how strong the effect is overall | | optic | 'film' (soap bubble) or 'grating' (CD) | | grainFreq | how fine the microstructure is | | grainDepth | how pronounced it is | | filmBase | which colour sits at the centre of the sweep | | filmVar | how far the hue travels | | aniso | how directional the highlight is | | tint | pulls everything toward one hue | | bloom | glow around the bright parts |

The one trap worth stating up front. The interference is added to whatever is underneath. A bright or already-saturated base leaves it nowhere to go, and the result is a white blur. If a finish looks washed out, darken the artwork before reaching for sheen.


Drivers

Optional helpers that wire the light to something. All of them return a detach function.

import { attachAll } from 'schiller/drivers';
const detach = attachAll(mica, canvas);

| | | | --- | --- | | attachPointer | the mouse or a finger | | attachScroll | scroll position | | attachTilt | the device gyroscope, with the iOS permission flow | | attachVisibility | pauses when off-screen — worth having | | attachAll | all of the above |


Browser support

Needs WebGL2 with EXT_color_buffer_float — Chrome, Edge, Firefox, and Safari 17+.

createSchiller throws FloatUnsupportedError when it is missing, so you can fall back to the plain artwork rather than showing nothing:

import { createSchiller, FloatUnsupportedError } from 'schiller';

try {
  mica = await createSchiller(canvas, { source });
} catch (err) {
  if (err instanceof FloatUnsupportedError) showStaticImage();
  else throw err;
}

API

createSchiller(canvas, options)

| option | default | | | --- | --- | --- | | source | required | URL, SVG string, <img>, <canvas>, ImageBitmap | | finish | 'wing' | which material | | grain | 'contour' | which way the structure runs | | angle | π/4 | direction, for grain: 'angle' | | ambient | 0.3 | fill light | | fieldSize | 1024 | resolution of the internal structure field | | idle | { amount: 0.34, period: 14 } | the drift when nobody is interacting | | respectReducedMotion | true | freeze the drift for those who ask |

Returns a handle:

mica.setLight(u, v)        // move the light; 0..1, v up
mica.setEngagement(0..1)   // 0 = resting drift, 1 = fully driven
mica.setFinish(name)
mica.setMaterial(patch)
mica.getMaterial()
mica.setGrain(mode, radians)
mica.setFold(0..1)         // fold about the vertical centre, for wings
mica.pointerMove(event)
mica.pointerLeave()
mica.pause() / mica.resume()
mica.dispose()             // idempotent; call on unmount
mica.frames                // frames rendered, for tests
mica.running
mica.reducedMotion

patchMica(material, options)

Patches a Three material in place. Returns { setLight, setFinish, set, uniforms }.

schiller/glsl

The raw shader chunks, if you are assembling your own pipeline.


See it working

useschiller.com — nineteen specimens, each on the object its finish imitates.

useschiller.com/try — drop in your own image and put any of the finishes on it. Runs entirely in the browser.


A note on where this is developed

The source is published here in full, and it is MIT licensed — use it in anything, commercial included, modify your copy, fork it if you want to.

What there is not is a public repository. This is one person's library rather than a community project, so there is nowhere to file an issue and no pull requests to open. If something is broken, the honest answer is that a fork is your fastest route, and the licence explicitly allows it.


Licence

MIT. See LICENSE.