mirror of
https://github.com/Such-Software/such.software.git
synced 2026-10-17 05:32:05 +02:00
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:
Binary file not shown.
|
After Width: | Height: | Size: 114 KiB |
Binary file not shown.
Binary file not shown.
|
After Width: | Height: | Size: 109 KiB |
Binary file not shown.
Binary file not shown.
|
Before Width: | Height: | Size: 131 KiB |
Binary file not shown.
@@ -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>
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user