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:
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user