实现渐变背景的气泡框

使用css实现渐变色背景的气泡框

2024-07-19 12:01:00

#实现渐变背景的气泡框

传统的气泡框一般使用一个圆角div+三角形来模拟,在背景色为普通颜色时,分别设置背景色即可达到理想的效果。然而将背景色改为渐变色时,分别给两部分元素设置背景色,无法设置出连续的颜色,所以原先的实现方式不适用。

#初始方案

#实现逻辑

  1. 画一个渐变色背景的矩形
  2. 使用和背景色一致的颜色遮罩盖住无需展示的部分,模拟三角形和圆角
    1. 远离箭头的矩形正常设置border-radius
    2. 靠近箭头的矩形圆角使用两个遮罩来模拟,使用clip-path的path函数实现
    3. 使用clip-path的polygon(多边形)函数截取三角形的反转部分,遮住非三角形的部分。

示例代码:

import classNames from "classnames";
import type { PropsWithChildren } from "react";

import styles from "./styles.module.css";

type ArrowDirection = "left" | "right";

interface SimpleLinearPopoverProps {
  arrowDirection?: ArrowDirection;
  parrentBackground?: string;
}

const SimpleLinearPopover: React.FC<PropsWithChildren<SimpleLinearPopoverProps>> = ({
  arrowDirection = "left",
  parrentBackground = "#fff",
  children,
}) => {
  return (
    <div
      className={classNames(
        styles["simple-popover-container"],
        styles[`simple-popover-container-direction-${arrowDirection}`]
      )}
      style={{
        "--parent-background-color": parrentBackground,
      }}
    >
      <div className={styles["simple-popover-content"]}>{children}</div>
      <div className={styles["simple-popover-arrow"]}></div>
    </div>
  );
};

export default SimpleLinearPopover;
.simple-popover-container {
  position: relative;
  width: fit-content;
  max-width: 100%;
  padding: 12px 24px 12px 16px;
  word-break: break-all;
  color: #fff;
  background: linear-gradient(137deg, #efe2b7 0%, #a4d1b1 90%);
  border-radius: 12px;
  white-space: pre-wrap;
}

.simple-popover-container.simple-popover-container-direction-left {
  transform: rotateY(180deg);
}
.simple-popover-container.simple-popover-container-direction-right {
  transform: unset;
}

.simple-popover-content {
  overflow: auto;
}

.simple-popover-container.simple-popover-container-direction-left
  .simple-popover-content {
  transform: rotateY(180deg);
}

/* 上圆角 */
.simple-popover-container:before {
  position: absolute;
  top: -2px;
  right: 6px;
  content: "";
  width: 14px;
  height: 14px;
  background: var(--parent-background-color);
  clip-path: path("M 0 0 L 0 2 A 12 12 0 0 1 12 14 L 14 14 L 14 0 0 0 Z");
}

/* 下圆角 */
.simple-popover-container:after {
  position: absolute;
  bottom: -2px;
  right: 6px;
  content: "";
  width: 14px;
  height: 14px;
  background: var(--parent-background-color);
  clip-path: path("M 14 0 L 12 0 A 12 12 0 0 1 0 12 L 0 14 L 14 14 14 0 Z");
}

/* 箭头 */
.simple-popover-arrow:after {
  position: absolute;
  top: 0;
  right: -2px;
  content: "";
  width: 10px;
  height: 100%;
  background: var(--parent-background-color);
  clip-path: polygon(
    100% 0,
    0 0,
    0 12px,
    8px 21px,
    0 30px,
    0 100%,
    100% 100%,
    100% 21px
  );
}

效果如下:

hello!
hello!

#局限

只能适用于背景色为纯色的场景,对背景为图片或带透明叠加效果的场景不适用

#改良方案

仅用一个div+clip-path,将气泡框描绘出来,由于path函数中只能声明固定坐标点而不能用百分比,而气泡框的宽高需要根据内容自适应,因此需要监听div的大小变化,计算出path的路径。

示例代码:

import classNames from "classnames";
import { PropsWithChildren, useLayoutEffect, useRef, useState } from "react";
import styles from "./styles.module.css";

type ArrowDirection = "left" | "right";

interface LinearPopoverProps {
  arrowDirection?: ArrowDirection;
}

const LinearPopover: React.FC<PropsWithChildren<LinearPopoverProps>> = ({
  arrowDirection = "left",
  children,
}) => {
  const containerRef = useRef<HTMLDivElement>(null);

  useLayoutEffect(() => {
    if (containerRef.current) {
      const resizeObserver = new ResizeObserver((entries) => {
        for (const entry of entries) {
          if (entry.contentRect) {
            const { width, height } = entry.target.getBoundingClientRect();
            const path = genPopoverPath(width, height);
            containerRef.current!.style["clip-path"] = `path('${path}')`;
          }
        }
      });
      resizeObserver.observe(containerRef.current);
      return () => {
        resizeObserver.disconnect();
      };
    }
  }, []);

  return (
    <div
      ref={containerRef}
      className={classNames(
        styles["popover-container"],
        styles[`popover-container-direction-${arrowDirection}`]
      )}
    >
      <div className="popover-container-content">{children}</div>
    </div>
  );
};

function genPopoverPath(width: number, height: number) {
  const borderRadius = 12;
  const arrowWidth = 10;
  const path = `M 0 ${borderRadius}
   A ${borderRadius} ${borderRadius} 0 0 1 ${borderRadius} 0 
   L ${width - borderRadius - arrowWidth} 0 
   A ${borderRadius} ${borderRadius} 0 0 1 ${
    width - arrowWidth
  } ${borderRadius} 
   L  ${width - arrowWidth} ${borderRadius} 
   L ${width} 21 
   L ${width - arrowWidth} 30 
   L ${width - arrowWidth} ${height - borderRadius} 
   A ${borderRadius} ${borderRadius} 0 0 1 ${
    width - borderRadius - arrowWidth
  } ${height} 
   L ${borderRadius} ${height}  
   A ${borderRadius} ${borderRadius} 0 0 1 0 ${height - borderRadius}
   L 0 ${borderRadius} Z`;

  return path.replaceAll("\n", "");
}

export default LinearPopover;

.popover-container {
  position: relative;
  width: fit-content;
  max-width: 100%;
  padding: 12px 24px 12px 16px;
  word-break: break-all;
  color: #fff;
  background: linear-gradient(137deg, #efe2b7 0%, #a4d1b1 90%);
  white-space: pre-wrap;
}

.popover-container.popover-container-direction-right {
  transform: rotateY(180deg);
}
.popover-container.popover-container-direction-right
  .popover-container-content {
  transform: rotateY(180deg);
}

效果如下:

hello!
hello!

#参考资料

path椭圆曲线