fix(bento): per-theme barns vignette + alpha-safe beta (survives transparent capture)

Two fixes to the barns hover vignette after seeing it live:
1. The beta was STRIPPED by the transparent capture: the flash/particle used
   additive blending, which writes ~no alpha, so it vanished in the alpha webm
   (only the opaque letters survived = that brief white "lightning"). Switched the
   flash to alpha-blend and pushed the absorption onto the opaque letters (strong
   blue surface-ripple pulse), so the beta now survives on BOTH light and dark.
2. Light + dark mode need different ink: a near-black outline vanishes on a dark
   tile; a light outline vanishes on a light tile. Render two baked assets
   (barns-light = dark ink, barns-dark = cream ink) and HoverVignette picks by
   resolved theme (next-themes). Added a clean rest poster (no active strike) via
   a BN_POSTER render so at-rest looks right.
This commit is contained in:
jwinterm
2026-07-08 12:21:32 -04:00
parent d4f65489b5
commit 90e7cb58ea
8 changed files with 28 additions and 9 deletions
@@ -52,7 +52,7 @@ type Item = {
* webm: /showcase/vignettes/<slug>.webm (VP9 + alpha)
* poster: /showcase/vignettes/<slug>.png (transparent RGBA PNG)
*/
vignette?: { webm: string; poster: string };
vignette?: { webm: string; poster: string; webmDark?: string; posterDark?: string };
};
// Six tiles, spans tiling the 3-column grid gap-free: (2,1) / (1,2) / (1,2) = three full rows.
@@ -63,7 +63,7 @@ const items: Item[] = [
{ name: "Wownerogue", tagline: "Provably-fair roguelike, synced to crypto block times.", color: "orange", span: 1, image: "/images/products/privacy-labs.png", alt: "Screenshot of the Wownerogue roguelike dungeon-crawler", href: "/products/wownerogue" },
{ name: "Bauhaus Echo", tagline: "Visual memory puzzle game.", color: "blue", span: 2, image: "/images/products/bauhaus-echo.png", alt: "Bauhaus Echo visual memory puzzle game", href: "/products/bauhaus-echo" },
{ name: "Vegan IQ", tagline: "Plant-based trivia game.", color: "green", span: 1, image: "/images/products/vegan-iq.png", alt: "Vegan IQ plant-based trivia game", href: "/products/vegan-iq" },
{ name: "Barns & Neutrons", tagline: "Cozy puzzle expedition across the real Table of Nuclides.", color: "amber", span: 2, image: "/images/products/barns-and-neutrons.svg", alt: "Barns & Neutrons, a game about the Table of Nuclides", href: "/products/barns-and-neutrons", vignette: { webm: "/showcase/vignettes/barns.webm", poster: "/showcase/vignettes/barns.png" } },
{ name: "Barns & Neutrons", tagline: "Cozy puzzle expedition across the real Table of Nuclides.", color: "amber", span: 2, image: "/images/products/barns-and-neutrons.svg", alt: "Barns & Neutrons, a game about the Table of Nuclides", href: "/products/barns-and-neutrons", vignette: { webm: "/showcase/vignettes/barns-light.webm", poster: "/showcase/vignettes/barns-light.png", webmDark: "/showcase/vignettes/barns-dark.webm", posterDark: "/showcase/vignettes/barns-dark.png" } },
];
function BentoCard({ item, delay }: { item: Item; delay: number }) {
@@ -78,7 +78,12 @@ function BentoCard({ item, delay }: { item: Item; delay: number }) {
</div>
<div className={`absolute inset-x-4 bottom-4 top-24 rounded-lg overflow-hidden z-10 bg-black/5 dark:bg-white/5 border ${imgBorder[item.color]} shadow-inner`}>
{feature ? (
<HoverVignette webm={item.vignette!.webm} poster={item.vignette!.poster} />
<HoverVignette
webm={item.vignette!.webm}
poster={item.vignette!.poster}
webmDark={item.vignette!.webmDark}
posterDark={item.vignette!.posterDark}
/>
) : (
<Image
src={item.image}
@@ -1,12 +1,17 @@
'use client';
import { useTheme } from 'next-themes';
import { useEffect, useRef, useState } from 'react';
type HoverVignetteProps = {
/** Alpha (VP9-with-alpha) WebM that plays on hover/focus, layered over the tile art. */
/** Alpha (VP9-with-alpha) WebM that plays on hover/focus, layered over the tile art (the LIGHT-mode asset). */
webm: string;
/** Transparent RGBA PNG shown by default and whenever the video can't/shouldn't play. */
/** Transparent RGBA PNG shown by default and whenever the video can't/shouldn't play (LIGHT-mode). */
poster: string;
/** Optional DARK-mode asset pair. Alpha vignettes are baked, so the ink/colour that reads on a light tile
* (dark outline) vanishes on a dark tile and vice-versa. When provided, these are used in dark mode. */
webmDark?: string;
posterDark?: string;
className?: string;
};
@@ -32,15 +37,23 @@ type HoverVignetteProps = {
* the tile's link/click/tap or steals focus. It also adds no layout (absolutely
* positioned, inset-0), so it can't shift the tile.
*/
export function HoverVignette({ webm, poster, className }: HoverVignetteProps) {
export function HoverVignette({ webm, poster, webmDark, posterDark, className }: HoverVignetteProps) {
const wrapRef = useRef<HTMLDivElement>(null);
const videoRef = useRef<HTMLVideoElement>(null);
const [allowMotion, setAllowMotion] = useState(false);
const [mounted, setMounted] = useState(false);
const { resolvedTheme } = useTheme();
useEffect(() => {
setAllowMotion(!window.matchMedia('(prefers-reduced-motion: reduce)').matches);
setMounted(true);
}, []);
// Pick the theme-matched asset (falls back to the light pair until mounted / when no dark asset is given).
const isDark = mounted && resolvedTheme === 'dark';
const activeWebm = isDark && webmDark ? webmDark : webm;
const activePoster = isDark && posterDark ? posterDark : poster;
useEffect(() => {
if (!allowMotion) return;
const wrap = wrapRef.current;
@@ -82,7 +95,7 @@ export function HoverVignette({ webm, poster, className }: HoverVignetteProps) {
trigger.removeEventListener('focusout', stop);
video.pause();
};
}, [allowMotion]);
}, [allowMotion, activeWebm]);
return (
<div
@@ -91,16 +104,17 @@ export function HoverVignette({ webm, poster, className }: HoverVignetteProps) {
className={`pointer-events-none absolute inset-0 z-[15] ${className ?? ''}`}
>
<video
key={activeWebm}
ref={videoRef}
className="h-full w-full object-contain p-2"
poster={poster}
poster={activePoster}
muted
loop
playsInline
preload="none"
tabIndex={-1}
>
<source src={webm} type='video/webm; codecs="vp9"' />
<source src={activeWebm} type='video/webm; codecs="vp9"' />
</video>
</div>
);