让你的用户头像更具艺术感,实现一个自动生成唯一渐变色的头像组件

让你的用户头像更具艺术感,实现一个自动生成唯一渐变色的头像组件

十二月 17, 2023 评论 10 阅读 895 点赞 0 收藏 0

实现过程

Avatar 组件

首先我们封装一个 Avatar 组件,这里我引用了 ChakraUI 组件库:

const Avatar: React.FC<{ name: string } & AvatarProps> = ({
  name,
  ...rest
}) => {
  return (
    <Box w="12" h="12" p="0" {...rest}>
      <ChakraAvatar {...getGradientStyle(name)} name={name} />
    </Box>
  );
};

export default Avatar;

样式生成函数

这一步还是很简单的,在组件的 props 中我使用了一个 getGradientStyle(name) 函数用于获取头像组件的样式,下面我们来实现这个函数:

const getGradientStyle: (text: string) => AvatarProps = (text) => {
  const color1 = getRandomColor(text, 1);
  const color2 = getRandomColor(text, 0.7);

  return {
    backgroundImage: `linear-gradient(135deg, ${color1}, ${color2})`,
    color: "white",
    display: "flex",
    justifyContent: "center",
    alignItems: "center",
    fontWeight: "bold",
    borderRadius: "10px",
    background: "transparent",
    fontFamily: "Helvetica, Arial, sans-serif",
  };
};

随机颜色函数

这个函数返回一个包含渐变和其他属性的样式对象,这里面还有一个核心的函数 getRandomColor,这个函数可以基于字符串生成颜色,并且可以自己传入透明度,下面说说这个函数是如何实现的:

function getRandomColor(str: string, alpha: number) {
  let asciiSum = 0;
  for (let i = 0; i < str.length; i++) {
    asciiSum += str.charCodeAt(i);
  }
  const red = Math.abs(Math.sin(asciiSum) * 256).toFixed(0);
  const green = Math.abs(Math.sin(asciiSum + 1) * 256).toFixed(0);
  const blue = Math.abs(Math.sin(asciiSum + 2) * 256).toFixed(0);
  return `rgba(${red}, ${green}, ${blue}, ${alpha})`;
}

解析一下 getRandomColor 函数的执行过程:

  1. 首先遍历字符串 str 中的每个字符,计算它们的 ASCII 码值之和 asciiSum
  2. 使用这个数字作为参数,通过 Math.sin 函数生成一个介于 -1 和 1 之间的正弦值。再通过 Math.sin
  3. 将这个正弦值乘以 256 并四舍五入,得到一个介于 0 和 255 之间的整数,作为 rgba 颜色值的红色、绿色、蓝色分量。
  4. 最后,将传入的透明度 alpha 与颜色值一起组成一个 rgba 颜色值字符串并返回。

实现的效果如下图:

字体阴影

基本的一个头像已经做好了,但是我们还需要补充一些细节。为了让字体在浅色背景下也可以看清楚,我们可以为字体加上一点字体阴影。

textShadow: "1px 1px 3px rgba(0, 0, 0, 0.2)"

添加前后的对比可以看下图,重点是像 "王" 字这种比较浅色的是不是一下就清晰多了。

添加前:

添加后:

背景纹理

现在的头像背景效果已经蛮不错了,但是只是一个渐变背景我还是觉得太单调了,如果里面能够增加一点纹理就好了。于是我又添加了一个水波纹的效果,实现的代码如下:

return {
    backgroundImage: `linear-gradient(135deg, ${color1}, ${color2})`,
    color: "white",
    display: "flex",
    justifyContent: "center",
    alignItems: "center",
    fontWeight: "bold",
    borderRadius: "10px",
    background: "transparent",
    textShadow: "1px 1px 3px rgba(0, 0, 0, 0.2)",
    fontFamily: "Helvetica, Arial, sans-serif",
    position: "relative",
    _before: {
      content: `""`,
      position: "absolute",
      left: "0",
      top: "0",
      w: "full",
      h: "full",
      borderRadius: "10px",
      backgroundColor: "white",
      backgroundImage: `repeating-radial-gradient( circle at 0 0, transparent 0, #ffffff 9px ), repeating-linear-gradient( ${color2}, ${color1} )`,
      zIndex: "-1",
    },
  };

在原有背景的基础上,我增加了 before 伪元素,将它的位置大小与背景重叠,然后通过 backgroundImage 设置了背景的纹理:

backgroundImage: \`repeating-radial-gradient( circle at 0 0, transparent 0, #ffffff 9px ), repeating-linear-gradient( ${color2}, ${color1} )

再通过将原本的背景色设置为 transparent 透明,将 before 的层级设置为 -1,将背后的纹理给透出去,实现的效果如下图:


好不好看这点仁者见仁,但是我觉得是精致了一点的。before 中的纹理是这样的:

是不是有点像阿尔卑斯糖呢 🍭。这个效果我是从一个背景生成网站中调整并生成的,网站地址在这:https://www.magicpattern.design/tools/css-backgrounds ,这个网站提供了很多好看的背景纹理,可以在线调整颜色和间距,预览效果,还能直接复制 CSS 到代码里使用。

最后再放一下 26 个字母生成的头像效果,不同字母生成的颜色差别还是相对比较大的。我觉着效果都还不错,即便是不太好看的颜色在背景纹理和渐变的加成下也还凑合能看:

性能优化

最后我们看回前面生成颜色的函数,在代码里有这么一段用于生成两个渐变色的逻辑:

const color1 = getRandomColor(text, 1);
const color2 = getRandomColor(text, 0.7);

但是这里我们仅仅是改变了透明度,颜色其实是不变的,那么去计算两次颜色就没有必要了,我们可以先获取颜色,然后再改透明度,避免重复计算颜色。修改的方式有很多种,如果是你,你会怎么改呢?我的调整方式是这样的:

function getRandomColor(str: string) {
  let asciiSum = 0;
  for (let i = 0; i < str.length; i++) {
    asciiSum += str.charCodeAt(i);
  }

  const red = Math.abs(Math.sin(asciiSum) * 256).toFixed(0);
  const green = Math.abs(Math.sin(asciiSum + 1) * 256).toFixed(0);
  const blue = Math.abs(Math.sin(asciiSum + 2) * 256).toFixed(0);
  return (alpha: number) => `rgba(${red}, ${green}, ${blue}, ${alpha})`;
}

const color = getRandomColor(text);
const color1 = color(1);
const color2 = color(0.7);

这里我将函数进行柯里化,将一个多参数的函数转换为一系列单参数的函数,每个函数接受一个参数并返回一个函数,最终返回值由最后一个函数计算得出。

我们可以通过对 getRandomColor() 函数进行一次调用来获取一个特定字符串对应的颜色生成函数,然后多次调用该生成函数并传入不同的透明度参数来生成不同的颜色。这是柯里化的一个常见应用场景。

最终完整代码如下:

import { Avatar as ChakraAvatar, AvatarProps } from "@chakra-ui/react";

function getRandomColor(str: string) {
  let asciiSum = 0;
  for (let i = 0; i < str.length; i++) {
    asciiSum += str.charCodeAt(i);
  }

  const red = Math.abs(Math.sin(asciiSum) * 256).toFixed(0);
  const green = Math.abs(Math.sin(asciiSum + 1) * 256).toFixed(0);
  const blue = Math.abs(Math.sin(asciiSum + 2) * 256).toFixed(0);
  return (alpha: number) => `rgba(${red}, ${green}, ${blue}, ${alpha})`;
}

const getGradientStyle: (text: string) => AvatarProps = (text) => {
  const color = getRandomColor(text);
  const color1 = color(1);
  const color2 = color(0.7);

  return {
    backgroundImage: `linear-gradient(135deg, ${color1}, ${color2})`,
    color: "white",
    display: "flex",
    justifyContent: "center",
    alignItems: "center",
    fontWeight: "bold",
    borderRadius: "10px",
    background: "transparent",
    textShadow: "1px 1px 3px rgba(0, 0, 0, 0.2)",
    fontFamily: "Helvetica, Arial, sans-serif",
    position: "relative",
    _before: {
      content: `""`,
      position: "absolute",
      left: "0",
      top: "0",
      w: "full",
      h: "full",
      borderRadius: "10px",
      backgroundColor: "white",
      backgroundImage: `repeating-radial-gradient( circle at 0 0, transparent 0, #ffffff 9px ), repeating-linear-gradient( ${color2}, ${color1} )`,
      zIndex: "-1",
    },
  };
};

const Avatar: React.FC<{ name: string } & AvatarProps> = ({
  name,
  ...rest
}) => {
  return <ChakraAvatar {...getGradientStyle(name)} {...rest} name={name} />;
};

export default Avatar;
*
*
*