feat: visual polish & UX — fade-in, theme toggle, scroll-to-top, footer, cursor, badge
- Add FadeIn component (IntersectionObserver scroll-reveal + load animation) - Add dark/light theme toggle (useSyncExternalStore + localStorage) - Add scroll-to-top button (appears after 300px scroll) - Add configurable footer with social links (gallery.yaml) - Add crosshair cursor on photo grid items - Add 'X photos' count badge on subpage cover images - Add light theme color palette in tokens.css
This commit is contained in:
@@ -11,8 +11,8 @@ import { thumbHashToRGBA, thumbHashToDataURL } from 'thumbhash';
|
||||
* suitable for next/image's blurDataURL prop.
|
||||
*/
|
||||
export function thumbHashToBlurDataUrl(base64: string): string {
|
||||
const bytes = Uint8Array.from(atob(base64), (c) => c.charCodeAt(0));
|
||||
return thumbHashToDataURL(bytes);
|
||||
const bytes = Uint8Array.from(atob(base64), (c) => c.charCodeAt(0));
|
||||
return thumbHashToDataURL(bytes);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -20,23 +20,29 @@ export function thumbHashToBlurDataUrl(base64: string): string {
|
||||
* Computes the weighted average of all non-transparent pixels.
|
||||
*/
|
||||
export function thumbHashToDominantHex(base64: string): string {
|
||||
const bytes = Uint8Array.from(atob(base64), (c) => c.charCodeAt(0));
|
||||
const { w, h, rgba } = thumbHashToRGBA(bytes);
|
||||
const bytes = Uint8Array.from(atob(base64), (c) => c.charCodeAt(0));
|
||||
const { w, h, rgba } = thumbHashToRGBA(bytes);
|
||||
|
||||
let r = 0, g = 0, b = 0, total = 0;
|
||||
for (let i = 0; i < w * h; i++) {
|
||||
const a = rgba[i * 4 + 3];
|
||||
if (a > 0) {
|
||||
const weight = a / 255;
|
||||
r += rgba[i * 4] * weight;
|
||||
g += rgba[i * 4 + 1] * weight;
|
||||
b += rgba[i * 4 + 2] * weight;
|
||||
total += weight;
|
||||
}
|
||||
let r = 0,
|
||||
g = 0,
|
||||
b = 0,
|
||||
total = 0;
|
||||
for (let i = 0; i < w * h; i++) {
|
||||
const a = rgba[i * 4 + 3];
|
||||
if (a > 0) {
|
||||
const weight = a / 255;
|
||||
r += rgba[i * 4] * weight;
|
||||
g += rgba[i * 4 + 1] * weight;
|
||||
b += rgba[i * 4 + 2] * weight;
|
||||
total += weight;
|
||||
}
|
||||
}
|
||||
|
||||
if (total === 0) return '#888888';
|
||||
if (total === 0) return '#888888';
|
||||
|
||||
const avg = (v: number) => Math.round(v / total).toString(16).padStart(2, '0');
|
||||
return `#${avg(r)}${avg(g)}${avg(b)}`;
|
||||
const avg = (v: number) =>
|
||||
Math.round(v / total)
|
||||
.toString(16)
|
||||
.padStart(2, '0');
|
||||
return `#${avg(r)}${avg(g)}${avg(b)}`;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user