From 84ba540dd02c2ccc344fb9906d5c851fa8d5350a Mon Sep 17 00:00:00 2001 From: Luc van Kampen Date: Mon, 4 Mar 2024 15:27:25 +0000 Subject: [PATCH] Introduce Gradient Avatar Fallback (#7) --- app/[slug]/page.tsx | 7 +- components/ProfileAvatar/MeshGradient.tsx | 183 +++++++++++++++++++++ components/ProfileAvatar/ProfileAvatar.tsx | 27 +++ package.json | 1 + pnpm-lock.yaml | 7 + 5 files changed, 220 insertions(+), 5 deletions(-) create mode 100644 components/ProfileAvatar/MeshGradient.tsx create mode 100644 components/ProfileAvatar/ProfileAvatar.tsx diff --git a/app/[slug]/page.tsx b/app/[slug]/page.tsx index 483c79e..625ce86 100644 --- a/app/[slug]/page.tsx +++ b/app/[slug]/page.tsx @@ -12,6 +12,7 @@ import { XMTPSection } from '../../components/XMTP/section'; import { useEnstate } from '../../hooks/useEnstate'; import { useIYKRef } from '../../hooks/useIYKRef'; import { useWarpcast } from '../../hooks/useWarpcast'; +import { ProfileAvatar } from '../../components/ProfileAvatar/ProfileAvatar'; const theme2Class = { frensday2023: 'theme-frensday2023', @@ -51,11 +52,7 @@ export default async function ({
- profile +
{event == 'frensday2023' && (
diff --git a/components/ProfileAvatar/MeshGradient.tsx b/components/ProfileAvatar/MeshGradient.tsx new file mode 100644 index 0000000..1591b47 --- /dev/null +++ b/components/ProfileAvatar/MeshGradient.tsx @@ -0,0 +1,183 @@ +import Random from 'seedrandom'; + +// Generate a random hue from 0 - 360 +const getColor = (random): number => { + return Math.round(random() * 360); +}; + +const getPercent = (value: number, random): number => { + return Math.round((random() * (value * 100)) % 100); +}; + +const getHashPercent = ( + value: number, + hash: number, + length: number +): number => { + return Math.round(((hash / length) * (value * 100)) % 100); +}; + +const hexToHSL = (hex?: string): number | undefined => { + if (!hex) return undefined; + + hex = hex.replace(/#/g, ''); + + if (hex.length === 3) { + hex = hex + .split('') + .map((hex) => { + return hex + hex; + }) + .join(''); + } + + const result = /^([\da-f]{2})([\da-f]{2})([\da-f]{2})[\da-z]*$/i.exec(hex); + + if (!result) { + return undefined; + } + + let r = Number.parseInt(result[1], 16); + let g = Number.parseInt(result[2], 16); + let b = Number.parseInt(result[3], 16); + + (r /= 255), (g /= 255), (b /= 255); + const max = Math.max(r, g, b), + min = Math.min(r, g, b); + let h = (max + min) / 2; + + if (max == min) { + h = 0; + } else { + const d = max - min; + + switch (max) { + case r: + h = (g - b) / d + (g < b ? 6 : 0); + break; + case g: + h = (b - r) / d + 2; + break; + case b: + h = (r - g) / d + 4; + break; + } + + h /= 6; + } + + h = Math.round(360 * h); + + return h; +}; + +const genColors = (length: number, initialHue: number): string[] => { + return Array.from({ length }, (_, index) => { + // analogous colors + complementary colors + // https://uxplanet.org/how-to-use-a-split-complementary-color-scheme-in-design-a6c3f1e22644 + + // base color + if (index === 0) { + return `hsl(${initialHue}, 100%, 74%)`; + } + + // analogous colors + if (index < length / 1.4) { + return `hsl(${ + initialHue - + 30 * (1 - 2 * (index % 2)) * (index > 2 ? index / 2 : index) + }, 100%, ${64 - index * (1 - 2 * (index % 2)) * 1.75}%)`; + } + + // complementary colors + return `hsl(${initialHue - 150 * (1 - 2 * (index % 2))}, 100%, ${ + 66 - index * (1 - 2 * (index % 2)) * 1.25 + }%)`; + }); +}; + +const genGrad = ( + random, + length: number, + colors: string[], + hash?: number +): string[] => { + return Array.from({ length }, (_, index) => { + return `radial-gradient(at ${ + hash + ? getHashPercent(index, hash, length) + : getPercent(index, random) + }% ${ + hash + ? getHashPercent(index * 10, hash, length) + : getPercent(index * 10, random) + }%, ${colors[index]} 0px, transparent 55%)\n`; + }); +}; + +const genStops = ( + random, + length: number, + baseColor?: number, + hash?: number +) => { + // get the color for the radial gradient + const colors = genColors(length, baseColor ? baseColor : getColor(random)); + // generate the radial gradient + const proprieties = genGrad( + random, + length, + colors, + hash ? hash : undefined + ); + + return [colors[0], proprieties.join(',')]; +}; + +export const generateMeshGradient = ( + length: number, + baseColor?: string, + hash?: number +) => { + const { random } = Math; + + const [bgColor, bgImage] = genStops( + random, + length, + hexToHSL(baseColor) ? hexToHSL(baseColor) : undefined, + hash ? hash : undefined + ); + + return `background-color: ${bgColor}; background-image:${bgImage}`; +}; + +export const generateJSXMeshGradient = ( + random, + length: number, + baseColor?: string, + hash?: number +) => { + const [bgColor, bgImage] = genStops( + random, + length, + hexToHSL(baseColor) ? hexToHSL(baseColor) : undefined, + hash ? hash : undefined + ); + + return { backgroundColor: bgColor, backgroundImage: bgImage }; +}; + +export const generateMeshGradientFromName = (name: string) => { + const random = Random.alea(name); + const length = Math.round(random() * 5 + 2); + + return generateJSXMeshGradient(random, length); +}; + +export const GradientAvatar = ({ name }: { name: string }) => { + const random = Random.alea(name); + const length = Math.round(random() * 5 + 2); + const meshGradient = generateJSXMeshGradient(random, length); + + return
; +}; diff --git a/components/ProfileAvatar/ProfileAvatar.tsx b/components/ProfileAvatar/ProfileAvatar.tsx new file mode 100644 index 0000000..85700fa --- /dev/null +++ b/components/ProfileAvatar/ProfileAvatar.tsx @@ -0,0 +1,27 @@ +'use client'; + +import clsx from 'clsx'; +import { FC, useMemo, useState } from 'react'; + +import { generateMeshGradientFromName } from './MeshGradient'; + +export const ProfileAvatar: FC<{ name: string, avatar?: string }> = ({ name, avatar }) => { + const [failedToLoad, setFailedToLoad] = useState(false); + const mesh = useMemo(() => generateMeshGradientFromName(name), [name]); + + return ( +
+
+
+ setFailedToLoad(true)} + /> +
+ ); +}; diff --git a/package.json b/package.json index 474f269..84b5e1d 100644 --- a/package.json +++ b/package.json @@ -22,6 +22,7 @@ "postcss-nested": "^6.0.1", "react": "^18.2.0", "react-icons": "^5.0.1", + "seedrandom": "^3.0.5", "short-number": "^1.0.7", "tailwindcss": "^3.3.3" }, diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 7d9da58..2019cdf 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -32,6 +32,9 @@ dependencies: react-icons: specifier: ^5.0.1 version: 5.0.1(react@18.2.0) + seedrandom: + specifier: ^3.0.5 + version: 3.0.5 short-number: specifier: ^1.0.7 version: 1.0.7 @@ -2290,6 +2293,10 @@ packages: loose-envify: 1.4.0 dev: false + /seedrandom@3.0.5: + resolution: {integrity: sha512-8OwmbklUNzwezjGInmZ+2clQmExQPvomqjL7LFqOYqtmuxRgQYqOD3mHaU+MvZn5FLUeVxVfQjwLZW/n/JFuqg==} + dev: false + /semver@5.7.2: resolution: {integrity: sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==} hasBin: true