JS实现环形文字

1,059 阅读2分钟

背景:实现一个环形导航需求,没有找到合适的插件,决定手写一个,以下为环形文字和背景的主要逻辑,环形导航要配置多个即可

实现方案

文字旋转:用绝对定位 + transform来实现
背景圆弧:用css的clip-path来实现

文字旋转

首先确定圆弧的半径r和文字开始的角度startDeg,然后循环遍历出文字所在位置和旋转角度

// str为要显示的文字
// clockwise是否为顺时针
// startDeg为开始角度
// textDeg 为每个文字所占的角度
for (let i = 0; i < str.length; i++) {
    const span = document.createElement('span');
    span.innerText = str[i];
    // 旋转角度
    let deg = 0;
    if (clockwise) {
      deg = startDeg - i * textDeg;
    } else {
      deg = startDeg + i * textDeg;
    }

    // 旋转角度值,需要用到Math.PI进行计算
    const angle = getAngle(deg);
    // 计算元素定位位置
    const left = `${Math.round(r + r * Math.cos(angle))}px`;
    const top = `${Math.round(r - r * Math.sin(angle))}px`;

    // 设置元素样式
    span.setAttribute('style', `
      width: ${fontSizeNum}px;
      height: ${fontSizeNum}px;
      line-height: ${fontSizeNum}px;
      text-align: center;
      font-size: ${fontSizeNum}px;
      color: ${color};
      position: absolute;
      left: ${left};
      top: ${top};
      transform-origin: right top;
      transform: rotate(${90 - deg - (i === 0 ? 0 : oneTextDeg)}deg);
      z-index: 30000;
      pointer-events: none; // 设置元素对鼠标事件不作反应
    `);
  }

圆弧背景

用到了css的clip-path 的path实现,参数跟svg的path一致,如下图:
需判断圆弧角度是否大于180度,大于180度large-arc-flag需设置为1 否则设置为0,不然切割出的来图形会不符合预期 image.png

// r圆弧半径
// with圆环的宽度
// startDeg 圆弧开始角度
// endDeg圆弧结束角度
const innerR = r - width;
// 外圆弧旋转角度值
const startAngle = getAngle(startDeg);
const endAngle = getAngle(endDeg);
// 计算外圆弧位置
const startLeft = `${Math.round(r + r * Math.cos(startAngle))}`;
const startTop = `${Math.round(r - r * Math.sin(startAngle))}`;
const endLeft = `${Math.round(r + r * Math.cos(endAngle))}`;
const endTop = `${Math.round(r - r * Math.sin(endAngle))}`;
// 计算内圆弧位置
const startLeftInner = `${Math.round(r + innerR * Math.cos(startAngle))}`;
const startTopInner = `${Math.round(r - innerR * Math.sin(startAngle))}`;
const endLeftInner = `${Math.round(r + innerR * Math.cos(endAngle))}`;
const endTopInner = `${Math.round(r - innerR * Math.sin(endAngle))}`;

// 创建dom元素
const div = document.createElement('div');
// 添加class属性
div.setAttribute('class', className);
const rotationDeg = Math.abs(endDeg - startDeg);
// 添加clip-path属性
const clipPathList = [
`M ${startLeft} ${startTop}`,
`A ${r} ${r} ${rotationDeg} ${rotationDeg > 180 ? 1 : 0} ${clockwise ? 1 : 0} ${endLeft} ${endTop}`,
`L ${endLeftInner} ${endTopInner}`,
`A ${innerR} ${innerR} ${rotationDeg} ${rotationDeg > 180 ? 1 : 0} ${clockwise ? 0 : 1} ${startLeftInner} ${startTopInner}`,
]
div.setAttribute('style', `
clip-path: path('${clipPathList.join(' ')}');
width: 100%;
height: 100%;
position: absolute;
z-index: 20000;
background-color: ${backgroundColor}`
);
// 设置背景颜色
div.style.backgroundColor = backgroundColor;

只有文字的效果

image.png

文字+背景图的效果

image.png

实现代码

github
npm包

如有其他实现方案或对代码的改进,欢迎各位大佬留言指导。