feat(gallery): enhance work card glow effect with vivid color extraction

- Add makeVivid() HSL-based color saturation/lightness boost
- Colors extracted from preview images are now brightened and saturated
- Gallery shell CSS: tighter conic-gradient angle (finer ribbon)
- Added extra .gallery-glow-layer for outer halo bloom
- Slowed rotation from 2.8s to 3.6s for a more elegant flow
- Hover opacity and blur tuned for refined visual depth
This commit is contained in:
FengLee
2026-05-13 21:58:33 +08:00
parent f06c475034
commit 5c5cb6c907
2 changed files with 118 additions and 22 deletions

View File

@@ -190,6 +190,58 @@ function getSaturation(color: RgbColor): number {
return max === 0 ? 0 : (max - min) / max;
}
function rgbToHsl({ r, g, b }: RgbColor) {
const nr = r / 255, ng = g / 255, nb = b / 255;
const max = Math.max(nr, ng, nb), min = Math.min(nr, ng, nb);
let h = 0, s = 0;
const l = (max + min) / 2;
if (max !== min) {
const d = max - min;
s = l > 0.5 ? d / (2 - max - min) : d / (max + min);
switch (max) {
case nr: h = (ng - nb) / d + (ng < nb ? 6 : 0); break;
case ng: h = (nb - nr) / d + 2; break;
case nb: h = (nr - ng) / d + 4; break;
}
h /= 6;
}
return { h, s, l };
}
function hslToRgb({ h, s, l }: { h: number; s: number; l: number }): RgbColor {
let r = 0, g = 0, b = 0;
if (s === 0) {
r = g = b = l;
} else {
const hue2rgb = (p: number, q: number, t: number) => {
if (t < 0) t += 1;
if (t > 1) t -= 1;
if (t < 1 / 6) return p + (q - p) * 6 * t;
if (t < 1 / 2) return q;
if (t < 2 / 3) return p + (q - p) * (2 / 3 - t) * 6;
return p;
};
const q = l < 0.5 ? l * (1 + s) : l + s - l * s;
const p = 2 * l - q;
r = hue2rgb(p, q, h + 1 / 3);
g = hue2rgb(p, q, h);
b = hue2rgb(p, q, h - 1 / 3);
}
return { r: r * 255, g: g * 255, b: b * 255 };
}
function makeVivid(color: RgbColor): RgbColor {
const hsl = rgbToHsl(color);
const luminance = getLuminance(color);
const saturation = getSaturation(color);
// 确保亮度不低于中等亮度,饱和度不低于鲜艳阈值
const targetLightness = luminance < 0.45 ? 0.60 : Math.max(hsl.l, 0.55);
const targetSaturation = saturation < 0.45 ? 0.75 : Math.max(hsl.s, 0.55);
hsl.l = Math.min(0.85, targetLightness); // 上限 85% 避免过曝
hsl.s = Math.min(1.0, targetSaturation * 1.15); // 额外增饱和 15%
return hslToRgb(hsl);
}
function getImagePalette(img: HTMLImageElement): GalleryCardPalette | null {
try {
const canvas = document.createElement('canvas');
@@ -231,12 +283,13 @@ function getImagePalette(img: HTMLImageElement): GalleryCardPalette | null {
average.r /= total;
average.g /= total;
average.b /= total;
const vividAccent = makeVivid(strongest);
const imageIsDark = getLuminance(average) < 0.48;
const softAccent = mixRgb(strongest, imageIsDark ? { r: 255, g: 255, b: 255 } : { r: 0, g: 0, b: 0 }, imageIsDark ? 0.28 : 0.18);
const warmAccent = mixRgb(strongest, { r: 245, g: 166, b: 35 }, 0.36);
const softAccent = mixRgb(vividAccent, imageIsDark ? { r: 255, g: 255, b: 255 } : { r: 0, g: 0, b: 0 }, imageIsDark ? 0.26 : 0.18);
const warmAccent = mixRgb(vividAccent, { r: 245, g: 166, b: 35 }, 0.38);
return {
accent1: rgbToCss(strongest),
accent1: rgbToCss(vividAccent),
accent2: rgbToCss(softAccent),
accent3: rgbToCss(warmAccent),
actionBg: imageIsDark ? 'rgb(255 255 255 / 0.92)' : 'rgb(13 18 28 / 0.86)',
@@ -659,6 +712,7 @@ export default function GalleryPage() {
className="gallery-work-shell group"
style={getGalleryCardStyle(cardPalettes[work.id])}
>
<div className="gallery-glow-layer" aria-hidden="true" />
<Card
className={`${galleryGlassCard} gallery-work-card w-full overflow-hidden cursor-pointer !rounded-2xl !py-0`}
onClick={() => setSelectedWork(work)}
@@ -1052,5 +1106,5 @@ export default function GalleryPage() {
</div>
);
}

View File

@@ -235,16 +235,17 @@
conic-gradient(
from 0deg,
transparent 0deg,
transparent 38deg,
transparent 58deg,
var(--gallery-accent-1) 62deg,
var(--gallery-accent-2) 86deg,
var(--gallery-accent-2) 84deg,
var(--gallery-accent-3) 112deg,
transparent 142deg,
var(--gallery-accent-1) 140deg,
transparent 146deg,
transparent 360deg
);
opacity: 0;
transition: opacity 260ms ease;
animation: gallery-border-flow 2.8s linear infinite;
animation: gallery-border-flow 3.6s linear infinite;
animation-play-state: paused;
-webkit-mask:
linear-gradient(#000 0 0) content-box,
@@ -259,25 +260,26 @@
.gallery-work-shell::after {
content: "";
position: absolute;
inset: -5px;
inset: -6px;
z-index: -2;
padding: 5px;
border-radius: 1.28rem;
padding: 6px;
border-radius: 1.32rem;
background:
conic-gradient(
from 0deg,
transparent 0deg,
transparent 42deg,
var(--gallery-accent-1) 66deg,
var(--gallery-accent-2) 92deg,
var(--gallery-accent-3) 118deg,
transparent 152deg,
transparent 54deg,
var(--gallery-accent-1) 64deg,
var(--gallery-accent-2) 84deg,
var(--gallery-accent-3) 114deg,
var(--gallery-accent-1) 142deg,
transparent 150deg,
transparent 360deg
);
filter: blur(8px);
filter: blur(6px) saturate(1.4);
opacity: 0;
transition: opacity 320ms ease;
animation: gallery-border-flow 2.8s linear infinite;
animation: gallery-border-flow 3.6s linear infinite;
animation-play-state: paused;
-webkit-mask:
linear-gradient(#000 0 0) content-box,
@@ -289,10 +291,45 @@
mask-composite: exclude;
}
/* 外围辉光层 */
.gallery-work-shell .gallery-glow-layer {
content: "";
position: absolute;
inset: -14px;
z-index: -3;
border-radius: 1.6rem;
background:
conic-gradient(
from 0deg,
transparent 0deg,
transparent 56deg,
var(--gallery-accent-1) 66deg,
var(--gallery-accent-2) 86deg,
var(--gallery-accent-3) 114deg,
var(--gallery-accent-1) 142deg,
transparent 150deg,
transparent 360deg
);
filter: blur(18px) saturate(1.8);
opacity: 0;
transition: opacity 420ms ease;
animation: gallery-border-flow 3.6s linear infinite;
animation-play-state: paused;
-webkit-mask:
linear-gradient(#000 0 0) content-box,
linear-gradient(#000 0 0);
-webkit-mask-composite: xor;
mask:
linear-gradient(#000 0 0) content-box,
linear-gradient(#000 0 0);
mask-composite: exclude;
pointer-events: none;
}
.gallery-work-shell:hover {
z-index: 10;
transform: translateY(-6px) scale(1.025);
filter: saturate(1.04);
transform: translateY(-6px) scale(1.02);
filter: saturate(1.03);
}
.gallery-work-shell:hover::before {
@@ -301,7 +338,12 @@
}
.gallery-work-shell:hover::after {
opacity: 0.42;
opacity: 0.55;
animation-play-state: running;
}
.gallery-work-shell:hover .gallery-glow-layer {
opacity: 0.32;
animation-play-state: running;
}