This is a throwaway page built to explore an idea, not a product. Nothing here is a spec, a roadmap, or a commitment. All card artwork is AI-generated, the card list is invented, and the whole repository may be rewritten or deleted without notice.
Unofficial personal experiment. Not affiliated with, endorsed by, or representing any game or publisher.
Four cards, four optical models. Hover to tilt, click to enlarge, click anywhere off the card or press Esc to close. Vite + TypeScript; the output is a plain static site.
The page is dark only — index.html stamps data-color-mode="dark" and nothing
changes it at runtime. src/styles/theme.css still carries the light palette on
the bare :root, so dropping that attribute brings it back.
npm install
npm run dev
npm run build # typecheck + static build into dist/
npm run preview # serves the build exactly as it deploys, base path and allPublished at https://rim95dev.github.io/msu-cards by
.github/workflows/deploy.yml on every push to main. It needs Pages switched
on for the repository with GitHub Actions as the source, once.
vite.config.ts sets base: '/msu-cards/'. Rename the repository and that has
to change with it — every asset URL is derived from it, including the card
images looked up at runtime through import.meta.env.BASE_URL. npm run dev
will not catch a mismatch; npm run preview will.
The pointer is read in polar terms rather than as an x/y position: the angle from the card's center to the pointer, and how far out it sits.
--px, --py pointer across the card, 0..1
--dx, --dy the same, signed from the center: -1..1
--ang angle from center to pointer, degrees, 0 = right
--lean distance from center, 0 at the middle, 1 at the edge
--lean-y vertical distance only, 0 at the middle, 1 at top or bottom
--lit 0 when the pointer is away, 1 while it is on the card
--ang is what the surfaces are built on. Once a surface knows the angle it is
being viewed from, it can be written as a conic gradient or a hue rotation — so
light travels around the card rather than sliding across it, which is what
most of these materials actually do.
--lean is the other one worth reaching for: multiply anything by it and the
effect starts at zero face-on and peaks at the corners, which is how nearly every
real surface behaves.
Not every surface should use --ang, though. A treatment lit by a fixed source
needs to ignore it — otherwise the light is really the cursor, and it shows. Those
surfaces read --dy and --lean-y instead, so only tipping the card up and down
changes what you see, and sliding sideways under the lamp changes nothing.
Motion is exponential smoothing rather than a spring — each value closes a fixed fraction of its remaining gap per unit time, with no velocity term. Nothing can overshoot or ring, which is the point: these values drive light, and light does not bounce.
Closing an enlarged card is the exception, and it is deliberate. Opening is a reveal and can take its time; closing is a dismissal and should be over before you think about it. The card is aimed at 0.84 of its resting size on a very short time constant, and the moment it passes below 0.97 it is retargeted back to 1. That dip is what gives it a snap — a decay on its own always reads limp, because its tail is asymptotic and the last stretch crawls. The handover is triggered by the value rather than a timer, or it would land at a different point in the curve depending on how large the card had been.
Every surface is generated in CSS. There are no texture files.
| Surface | Model | What you should see |
|---|---|---|
matte |
Diffuse | Almost nothing. The control card |
brush |
Anisotropic reflection | Fine radial grain with two opposed highlight lobes swinging around |
oilslick |
Thin-film interference | The whole palette rotating as the angle changes, not sliding |
facet |
Cut crystal | Flat wedges that snap between values instead of sweeping. The cut belongs to the card — its origin never moves; only the light travelling across it does |
facet reads most clearly at a glance; brush is the most subtle and needs a
slow hover to show what it does.
A fifth surface, refract (chromatic dispersion under a fixed overhead light),
is still in src/styles/fx-refract.css and in the Surface union, but no card
uses it and main.ts no longer imports its stylesheet. Add a card with
surface: 'refract' and restore that import to bring it back.
Each src/styles/fx-*.css is self-contained and reads the four variables above.
Each also retunes --spec-size and --spec-strength, the shared highlight —
that pair alone is most of the difference between matte and polished.
Full specification: ASSETS.md.
| What | Where | Size |
|---|---|---|
| Card front — the whole card, frame and text included | public/img/cards/{id}.png |
734 × 1024, full bleed |
| Surface mask (optional, recommended) | public/img/masks/{id}.png |
734 × 1024 |
Four images: test1, test2, test3, test4. Cards are single-sided —
no card back. Anything missing is drawn as a placeholder naming the file it wants.
index.html
src/
main.ts builds the grid, owns the open card
card.ts one card: DOM, pointer readings, open/close
smooth.ts exponential smoothing
placeholder.ts stand-in art; delete once every card has real art
types.ts CardData, Surface
data/cards.ts the card list
styles/
theme.css page chrome, palette, light/dark
card.css card shell, film + spec layers, mask
fx-*.css one file per surface
public/img/ card art, masks, and the MetaMask mark
- Surfaces are composited against the artwork, not the page.
.tcard__facecarriesisolation: isolate, and removing it lets every film blend straight through to the page background. --angis deliberately not smoothed. An angle wraps at ±180°; interpolate it and the light takes the long way around every time the pointer crosses the center line.- A near-white card loses its light areas under
refractorfacet. That is what the masks inpublic/img/masks/are for. - Cards with no image log a 404. The viewer probes for the real file first. The noise goes away as art gets added.
- The wallet button is a mock and must stay one. It never touches
window.ethereum; clicking it raises a notice saying so. A button that opens a real wallet prompt for a card that cannot be minted is worse than no button. The page copy is written as though the product existed — the banner at the top, that notice, and the footer are what keep it honest.
MIT — see LICENSE.
public/img/metamask-fox.svg is the exception: it is MetaMask's mark, taken
byte-for-byte from the official pack at https://metamask.io/assets and not
covered by the license above. It is kept as its own file, unaltered, so it stays
easy to identify and to remove. Their guidance allows the mark to signal
MetaMask support — so if this ever stops being a mock and starts pretending to
connect, or if the button is dropped, take the file with it.