React封装一个纯CSS实现的水滴样式的盒子

时间:2022-12-25 14:06:06

背景

刷B站刷到一个纯css实现的水滴效果的视频
感觉真不错,决定封装一个具有水滴效果的盒子(DIV)

React封装一个纯CSS实现的水滴样式的盒子React封装一个纯CSS实现的水滴样式的盒子

涉及知识点

  1. CSS样式,核心是这个和box-shadow阴影,实现水滴boder和阴影效果。
  2. JS控制CSS样式
  3. 16进制的颜色(#的写法)与rgba的写法的转换

代码

类型定义:

type embellishmentType = {
  position?: {
    top?: string;
    left?: string;
  };
  backgroundColor?: string;
};

interface WaterDropletProps {
  width?: number;
  height?: number;
  className?: string;
  borderRadius?: string;  
  backgroundColor?: string; 
  embellishment?: embellishmentType;  
  shadowColor?: string;  
  children?: React.ReactNode;
}
WaterDropletProps
参数 说明 类型 默认值
width 该盒子的宽 number 350
width 该盒子的高 number 350
className 该盒子的样式名,方便使用时增加额外样式 string
borderRadius 水滴状的borderRadius string 52% 48% 33% 67% / 38% 45% 55% 62%
backgroundColor 该盒子的背景颜色 string #eff0f2
embellishment 水滴上两个小点缀的属性,包括位置及背景颜色 embellishmentType #eff0f2
shadowColor 该盒子的阴影颜色 string rgba(0, 0, 0, 0.05)
children 子元素 React.ReactNode
embellishmentType
参数 说明 类型 默认值
position 位置 {left:string;top:string} {left:‘22.85%’,top:‘14.28%’}
backgroundColor 背景色 string #ffffff

DOM结构

return (
    <div
      style={{ borderRadius, backgroundColor }}
      className={`al-mixed-box-water-droplet ${className}`}
      ref={waterRef}
    >
      {children}
    </div>
  );

三个方法,分别是颜色的hex转rgba,rgba转hex,以及通过正则判断是否是rgba的写法

/**
 * @description: rgba => hex
 * @return {*}
 */
export const rgbaToHex = (val: string, alpha?: number) => {
  //RGB(A)颜色转换为HEX十六进制的颜色值
  let r,
    g,
    b,
    a,
    regRgba = /rgba?\((\d{1,3}),(\d{1,3}),(\d{1,3})(,([.\d]+))?\)/, //判断rgb颜色值格式的正则表达式,如rgba(255,20,10,.54)
    rsa = val.replace(/\s+/g, "").match(regRgba);
  if (!!rsa) {
    r = parseInt(rsa[1]).toString(16);
    r = r.length === 1 ? "0" + r : r;
    g = (+rsa[2]).toString(16);
    g = g.length === 1 ? "0" + g : g;
    b = (+rsa[3]).toString(16);
    b = b.length === 1 ? "0" + b : b;
    a = +(rsa[5] ? rsa[5] : alpha ?? 1) * 255;
    return {
      hex: "#" + r + g + b,
      r: parseInt(r, 16),
      g: parseInt(g, 16),
      b: parseInt(b, 16),
      alpha: rsa[5] ? rsa[5] : alpha ?? 1,
      hexa:
        "#" +
        r +
        g +
        b +
        (a.toString(16).split(".")[0].length === 1
          ? "0" + a.toString(16).split(".")[0]
          : a.toString(16).split(".")[0]),
    };
  } else {
    return { hex: "无效", alpha: 100 };
  }
};

/**
 * @description: hex => rgba
 * @param {string} val
 * @return {*}
 */
export const hexToRgba = (val: string) => {
  // 16进制颜色值的正则
  let reg = /^#([0-9a-fA-f]{3}|[0-9a-fA-f]{6}|[0-9a-fA-f]{8})$/;
  // 把颜色值变成小写
  let color = val.toLowerCase();
  let result = "";
  if (reg.test(color)) {
    // 如果只有3位的值,需变成8位,如:#fff => #ffffffff
    if (color.length === 4) {
      let colorNew = "#";
      for (let i = 1; i < 4; i += 1) {
        colorNew += color.slice(i, i + 1).concat(color.slice(i, i + 1));
      }
      color = colorNew + "ff";
    }
    // 如果只有6位,需要变成8位,如:#ffffff => #ffffffff
    if (color.length === 7) {
      color = color + "ff";
    }
    // 处理8位的颜色值,转为RGBA
    let colorChange = [];
    for (let i = 1; i < 9; i += 2) {
      if (i >= 7) {
        colorChange.push(parseInt("0x" + color.slice(i, i + 2)) / 255);
      } else colorChange.push(parseInt("0x" + color.slice(i, i + 2)));
    }
    result = "rgba(" + colorChange.join(",") + ")";
    return {
      rgba: result,
      r: colorChange[0],
      g: colorChange[1],
      b: colorChange[2],
      a: colorChange[3],
    };
  } else {
    result = "error";
    return { rgba: result };
  }
};

/**
 * @description: 检查是否符合rgba的格式
 * @param {string} val
 * @return {*}
 */
export const regRgbaFormat = (val: string) => {
  let regRgba = /rgba?\((\d{1,3}),(\d{1,3}),(\d{1,3})(,([.\d]+))?\)/;
  return regRgba.test(val);
};

CSS样式

通过var设置的样式变量,以及calc的样式计算

.al-mixed-box-water-droplet {
  width: var(--droplet-width);
  height: var(--droplet-height);
  position: relative;
  overflow: hidden;
  box-shadow: inset var(--box-shadow-20) var(--box-shadow-20)
      var(--box-shadow-20) var(--shadowColor),
    var(--box-shadow-25) var(--box-shadow-35) var(--box-shadow-20)
      var(--shadowColor),
    var(--box-shadow-25) var(--box-shadow-30) var(--box-shadow-30)
      var(--shadowColor),
    inset calc(-1 * var(--box-shadow-20)) calc(-1 * var(--box-shadow-20))
      var(--box-shadow-25) rgba(255, 255, 255, 0.8);
  --embellishmentL: 22.85%;
  --embellishmentT: 14.28%;
  --embellishmentBKC: #ffffff;
  --shadowColor: rgba(0, 0, 0, 0.05);
  --box-shadow-20: calc(var(--droplet-width) * 20 / 350);
  --box-shadow-25: calc(var(--droplet-width) * 25 / 350);
  --box-shadow-30: calc(var(--droplet-width) * 30 / 350);
  --box-shadow-35: calc(var(--droplet-width) * 35 / 350);
}
.al-mixed-box-water-droplet::before,
::after {
  content: "";
  position: absolute;
  left: var(--embellishmentL);
  top: var(--embellishmentT);
  width: var(--embellishmentWH);
  height: var(--embellishmentWH);
  background-color: var(--embellishmentBKC);
  border-radius: 50%;
  opacity: 0.9;
}
.al-mixed-box-water-droplet::after {
  width: calc(var(--embellishmentWH) / 2);
  height: calc(var(--embellishmentWH) / 2);
  transform: translate(200%, 200%);
}

JS控制CSS样式

通过.style.setProperty()改变css中的变量来达到控制css样式目的

  useEffect(() => {
    // set water droplet width and height
    waterRef.current?.style.setProperty("--droplet-width", width + "px");
    waterRef.current?.style.setProperty("--droplet-height", height + "px");
    // set embellishment width and height
    waterRef.current?.style.setProperty(
      "--embellishmentWH",
      `${Math.sqrt(Math.pow(width, 2) + Math.pow(height, 2)) / 14}px`
    );
    // set embellishment position and background color
    if (embellishment) {
      waterRef.current?.style.setProperty(
        "--embellishmentL",
        embellishment.position?.left ?? null
      );
      waterRef.current?.style.setProperty(
        "--embellishmentT",
        embellishment.position?.top ?? null
      );
      waterRef.current?.style.setProperty(
        "--embellishmentBKC",
        embellishment.backgroundColor ?? null
      );
    }
    // set shadow color
    if (shadowColor) {
      let newShadowColor = "";
      if (
        shadowColor.includes("#") &&
        hexToRgba(shadowColor).rgba !== "error"
      ) {
        newShadowColor = hexToRgba(shadowColor).rgba;
      } else if (regRgbaFormat(shadowColor)) {
        newShadowColor = hexToRgba(rgbaToHex(shadowColor, 0.05).hexa!).rgba;
      } else {
        console.warn("Please check the color format————", shadowColor);
      }
      if (newShadowColor !== "") {
        waterRef.current?.style.setProperty("--shadowColor", newShadowColor);
      }
    }
  }, [width, height, embellishment, shadowColor]);

完整代码

/*
 * @Author: atwLee
 * @Date: 2022-12-24 10:15:57
 * @LastEditors: atwLee
 * @LastEditTime: 2022-12-24 21:01:00
 * @Description: 水滴形状的box
 * @FilePath: /mixed/src/packages/box/water-droplet/index.tsx
 */
import { useEffect, useRef } from "react";
import { hexToRgba, regRgbaFormat, rgbaToHex } from "../../utils";
import "./index.css";

type embellishmentType = {
  position?: {
    top?: string;
    left?: string;
  };
  backgroundColor?: string;
};

interface WaterDropletProps {
  width?: number; // 该盒子的宽
  height?: number; // 该盒子的高
  className?: string; // 该盒子的样式名,方便使用时增加额外样式
  borderRadius?: string; // 水滴状的borderRadius
  backgroundColor?: string; // 该盒子的背景颜色
  embellishment?: embellishmentType; // 水滴上两个小点缀的属性,包括位置及背景颜色
  shadowColor?: string; // 阴影的颜色
  children?: React.ReactNode; // 子元素
}

function WaterDroplet(props: WaterDropletProps) {
  const {
    width = 350,
    height = 350,
    children,
    className = "",
    borderRadius = "52% 48% 33% 67% / 38% 45% 55% 62%",
    backgroundColor = "#eff0f2",
    embellishment,
    shadowColor,
  } = props;

  const waterRef = useRef<HTMLDivElement>(null);

  useEffect(() => {
    // set water droplet width and height
    waterRef.current?.style.setProperty("--droplet-width", width + "px");
    waterRef.current?.style.setProperty("--droplet-height", height + "px");
    // set embellishment width and height
    waterRef.current?.style.setProperty(
      "--embellishmentWH",
      `${Math.sqrt(Math.pow(width, 2) + Math.pow(height, 2)) / 14}px`
    );
    // set embellishment position and background color
    if (embellishment) {
      waterRef.current?.style.setProperty(
        "--embellishmentL",
        embellishment.position?.left ?? null
      );
      waterRef.current?.style.setProperty(
        "--embellishmentT",
        embellishment.position?.top ?? null
      );
      waterRef.current?.style.setProperty(
        "--embellishmentBKC",
        embellishment.backgroundColor ?? null
      );
    }
    // set shadow color
    if (shadowColor) {
      let newShadowColor = "";
      if (
        shadowColor.includes("#") &&
        hexToRgba(shadowColor).rgba !== "error"
      ) {
        newShadowColor = hexToRgba(shadowColor).rgba;
      } else if (regRgbaFormat(shadowColor)) {
        newShadowColor = hexToRgba(rgbaToHex(shadowColor, 0.05).hexa!).rgba;
      } else {
        console.warn("Please check the color format————", shadowColor);
      }
      if (newShadowColor !== "") {
        waterRef.current?.style.setProperty("--shadowColor", newShadowColor);
      }
    }
  }, [width, height, embellishment, shadowColor]);

  return (
    <div
      style={{ borderRadius, backgroundColor }}
      className={`al-mixed-box-water-droplet ${className}`}
      ref={waterRef}
    >
      {children}
    </div>
  );
}

export default WaterDroplet;

使用

import { WaterDroplet } from "./packages/box";
function App() {
  return (
    <div className="App">
      <WaterDroplet
        // width={120}
        // height={120}
        // embellishment={{
        //   position: { top: "15px", left: "30px" },
        //   backgroundColor: "rgba(255,255,255,0.45)",
        // }}
        // borderRadius={"34% 66% 65% 35% / 57% 58% 42% 43%"}
        // backgroundColor="#c61dff"
        // shadowColor="rgba(190,1,254,0.1)"
        width={120}
        height={120}
        embellishment={{
          position: { top: "35px", left: "30px" },
          backgroundColor: "rgba(255,255,255,0.45)",
        }}
        borderRadius={"75% 25% 68% 32% / 29% 70% 30% 71%"}
        backgroundColor="#01b4ff"
        shadowColor="rgb(1,180,255)"
      >
        <div>you code</div>
      </WaterDroplet>
    </div>
  );
}

export default App;
React封装一个纯CSS实现的水滴样式的盒子

属性换成上边那一套就是紫色的水滴样子,没有属性默认是白色的样子


感谢观看!!!