Skip to content

Commit

Permalink
Introduce Gradient Avatar Fallback (#7)
Browse files Browse the repository at this point in the history
  • Loading branch information
lucemans authored Mar 4, 2024
1 parent 3bcf569 commit 84ba540
Show file tree
Hide file tree
Showing 5 changed files with 220 additions and 5 deletions.
7 changes: 2 additions & 5 deletions app/[slug]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down Expand Up @@ -51,11 +52,7 @@ export default async function ({
<div className="w-full flex flex-col gap-2 items-center justify-center">
<div className="flex items-center relative w-full pt-8 pb-2">
<div className="mx-auto w-40 h-40 aspect-square border bg-white rounded-full overflow-hidden">
<img
src={enstate.avatar}
alt="profile"
className="w-full h-full"
/>
<ProfileAvatar name={enstate.name} avatar={enstate.avatar} />
</div>
{event == 'frensday2023' && (
<div className="absolute inset-0">
Expand Down
183 changes: 183 additions & 0 deletions components/ProfileAvatar/MeshGradient.tsx
Original file line number Diff line number Diff line change
@@ -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 <div className="w-full h-full" style={meshGradient} />;
};
27 changes: 27 additions & 0 deletions components/ProfileAvatar/ProfileAvatar.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<div className="relative aspect-square size-full">
<div className="bg-ens-light-background-secondary absolute inset-0 size-full"></div>
<div className="absolute inset-0 size-full" style={mesh}></div>
<img
src={avatar || 'https://enstate.rs/i/' + name}
className={clsx(
'avatar-image absolute inset-0 size-full',
failedToLoad && 'hidden'
)}
alt=""
onError={() => setFailedToLoad(true)}
/>
</div>
);
};
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
},
Expand Down
7 changes: 7 additions & 0 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

0 comments on commit 84ba540

Please sign in to comment.