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)
|
* webm: /showcase/vignettes/<slug>.webm (VP9 + alpha)
|
||||||
* poster: /showcase/vignettes/<slug>.png (transparent RGBA PNG)
|
* 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.
|
// 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: "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: "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: "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 }) {
|
function BentoCard({ item, delay }: { item: Item; delay: number }) {
|
||||||
@@ -78,7 +78,12 @@ function BentoCard({ item, delay }: { item: Item; delay: number }) {
|
|||||||
</div>
|
</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`}>
|
<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 ? (
|
{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
|
<Image
|
||||||
src={item.image}
|
src={item.image}
|
||||||
|
|||||||
@@ -1,12 +1,17 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
|
import { useTheme } from 'next-themes';
|
||||||
import { useEffect, useRef, useState } from 'react';
|
import { useEffect, useRef, useState } from 'react';
|
||||||
|
|
||||||
type HoverVignetteProps = {
|
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;
|
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;
|
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;
|
className?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -32,15 +37,23 @@ type HoverVignetteProps = {
|
|||||||
* the tile's link/click/tap or steals focus. It also adds no layout (absolutely
|
* 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.
|
* 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 wrapRef = useRef<HTMLDivElement>(null);
|
||||||
const videoRef = useRef<HTMLVideoElement>(null);
|
const videoRef = useRef<HTMLVideoElement>(null);
|
||||||
const [allowMotion, setAllowMotion] = useState(false);
|
const [allowMotion, setAllowMotion] = useState(false);
|
||||||
|
const [mounted, setMounted] = useState(false);
|
||||||
|
const { resolvedTheme } = useTheme();
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setAllowMotion(!window.matchMedia('(prefers-reduced-motion: reduce)').matches);
|
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(() => {
|
useEffect(() => {
|
||||||
if (!allowMotion) return;
|
if (!allowMotion) return;
|
||||||
const wrap = wrapRef.current;
|
const wrap = wrapRef.current;
|
||||||
@@ -82,7 +95,7 @@ export function HoverVignette({ webm, poster, className }: HoverVignetteProps) {
|
|||||||
trigger.removeEventListener('focusout', stop);
|
trigger.removeEventListener('focusout', stop);
|
||||||
video.pause();
|
video.pause();
|
||||||
};
|
};
|
||||||
}, [allowMotion]);
|
}, [allowMotion, activeWebm]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
@@ -91,16 +104,17 @@ export function HoverVignette({ webm, poster, className }: HoverVignetteProps) {
|
|||||||
className={`pointer-events-none absolute inset-0 z-[15] ${className ?? ''}`}
|
className={`pointer-events-none absolute inset-0 z-[15] ${className ?? ''}`}
|
||||||
>
|
>
|
||||||
<video
|
<video
|
||||||
|
key={activeWebm}
|
||||||
ref={videoRef}
|
ref={videoRef}
|
||||||
className="h-full w-full object-contain p-2"
|
className="h-full w-full object-contain p-2"
|
||||||
poster={poster}
|
poster={activePoster}
|
||||||
muted
|
muted
|
||||||
loop
|
loop
|
||||||
playsInline
|
playsInline
|
||||||
preload="none"
|
preload="none"
|
||||||
tabIndex={-1}
|
tabIndex={-1}
|
||||||
>
|
>
|
||||||
<source src={webm} type='video/webm; codecs="vp9"' />
|
<source src={activeWebm} type='video/webm; codecs="vp9"' />
|
||||||
</video>
|
</video>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
Reference in New Issue
Block a user