svg 实现带圆角边框的文本

2,038 阅读4分钟

想实现一个带圆角边框的文本,如果直接用DOM,那只用设置一个divborder-radius,然后包裹文本即可,例如如下一个简单的例子:

<!-- CSS -->
<style>
    .radius {
      width: 80px;
      height: 40px;
      line-height: 40px;
      font-size: 16px;
      text-align: center;
      border: 1px solid black;
      border-radius: 20px;
    }
</style>

<!-- HTML -->
<div class='radius'>
    圆角边框
</div>

image.png

但是如果想使用svg实现并非这么简单!需要对<rect><text>进行组合。我就简单介绍下自己实现的过程吧。

一、简单介绍 <rect><text>元素

1.1 <rect>

rect元素是 SVG 的一个基本形状,用来创建矩形,有以下私有属性:

  • x, y:用于设置矩形左上角位置。
  • width, height: 用于设置矩形的宽高
  • rx, ry: 用于设置圆角矩形的弧度

下面绘制一个简单的圆角矩形

<svg height="200" height="200" viewbox="0 0 200 200" xmlns="http://www.w3.org/2000/svg">
  <rect x='0' y='0' rx="20" ry="20" width="100" height="40" fill="transparent" stroke="black"/>
</svg>

image.png

1.2 <text>

text元素定义了一个由文字组成的图形。有以下私有属性:

  • x, y:文本的位置
    • x: 由text-anchor决定文本何处在这个位置
    • y: 文本的基线
  • dx, dy:文本在x和y方向上的偏移量
  • text-anchor:描述该文本与所给点的对齐方式 (开头、中间、末尾对齐)。
    • start: 文本字符串的开始位置即当前文本的初始位置。
    • middle: 文本字符串的中间位置即当前文本的初始位置
    • end: 文本字符串的末尾即当前文本的初始位置
  • rotate:用于设置文本的旋转角度
  • textLength:用于设置文本所占的宽度,如果大于文本自身宽度,则拉伸文本间隔,如果小于文本自身宽度,则压缩文本。
  • lengthAdjust:当设置了textLength后,该属性控制文本如何拉伸到该textLength属性定义的长度。可设置spacing | spacingAndGlyphs,默认为spacing
<svg height="200" height="200" viewbox="0 0 200 200" xmlns="http://www.w3.org/2000/svg">
      <path d="M0 20, L 100 20" stroke="black"/>
      <text x="0" y="20" font-size="16px">abcdefj</text>
</svg>

image.png

二、SVG 实现带圆角边框的文本

2.1 规定位置

因为想要实现的效果是文本在矩形框居中对齐,所以这里将 (x, y) 规定为矩形边框和文本的中心位置

那么怎么使得矩形和文本的中心落在(x, y)上呢?

2.2 绘制矩形

如上所述,<rect>的(x, y)属性指的是矩形左上角的位置,那么要想让矩形的中心移动到(x, y)位置,只需要将矩形向左移动宽度的一半,向上移动高度的一半即可。具体可通过transform属性来实现,即:

transform="translate(-width / 2, -height / 2)"

此外,dx, dy的值通常设置为高度的一半。

那么假设中心位置为(x, y),矩形宽高为(w, h),绘制矩形的代码如下

<rect x='x' y='y' rx="h/2" ry="h/2" width="w" height="h" transform="translate(-w/2, -h/2)" fill="transparent" stroke="black"/>

2.3 绘制文本

如上所述,<text> 的(x, y)属性默认指的是文本开头位置,但是设置text-anchor="middle"可以使得文本与(x, y)居中对齐。但这仅是横向居中,对于纵向我们需要使用dy属性向下平移使得文本纵向居中,平移多少,通常设置为font-size/2上下浮动1-2。

绘制文本代码如下:

<text x='x' y='y' dy='3' fill='#757685' fontSize="10" textAnchor='middle'>

那么经过如上几步,我们就可以绘制出一个带圆角边框的文本了:

<svg height="200" height="200" viewbox="0 0 200 200" xmlns="http://www.w3.org/2000/svg">
      <rect x='40' y='40' rx="10" ry="10" width="50" height="20" transform="translate(-25, -10)" fill="transparent" stroke="black"/>
      <text x='40' y='40' dy='5' fill='#757685' fontSize="12" text-anchor='middle'>圆角</text>
</svg>

image.png

三、圆角边框随文本长度自适应

上述虽然绘制出了带圆角边框的文本,但是所有属性都是静态的,如果文本边长那么就会溢出边框了。那我们如何实现圆角边框随着文本长度自适应呢。

3.1 获取文本长度

既然要自适应文本长度,那么首先肯定要获取文本的长度。原先菜鸟的我以为只需要fontSize * str.length即可,这对中文还行得通,但是对于英文字符,每个字符的宽度是不一样的,那么对于每个字符的宽度计算也是不一样的。

我这里也是借鉴了一位大佬的代码,首先定义英文字符的宽度:

const LetterMap = {
  ' ': 0.3329986572265625,
  a: 0.5589996337890625,
  A: 0.6569992065429687,
  b: 0.58599853515625,
  B: 0.6769989013671875,
  c: 0.5469985961914062,
  C: 0.7279998779296875,
  d: 0.58599853515625,
  D: 0.705999755859375,
  e: 0.554998779296875,
  E: 0.63699951171875,
  f: 0.37299957275390627,
  F: 0.5769989013671875,
  g: 0.5909988403320312,
  G: 0.7479995727539063,
  h: 0.555999755859375,
  H: 0.7199996948242188,
  i: 0.255999755859375,
  I: 0.23699951171875,
  j: 0.26699981689453123,
  J: 0.5169998168945312,
  k: 0.5289993286132812,
  K: 0.6899993896484375,
  l: 0.23499908447265624,
  L: 0.5879989624023437,
  m: 0.854998779296875,
  M: 0.8819992065429687,
  n: 0.5589996337890625,
  N: 0.7189987182617188,
  o: 0.58599853515625,
  O: 0.7669998168945312,
  p: 0.58599853515625,
  P: 0.6419998168945312,
  q: 0.58599853515625,
  Q: 0.7669998168945312,
  r: 0.3649993896484375,
  R: 0.6759994506835938,
  s: 0.504998779296875,
  S: 0.6319992065429687,
  t: 0.354998779296875,
  T: 0.6189987182617187,
  u: 0.5599990844726562,
  U: 0.7139999389648437,
  v: 0.48199920654296874,
  V: 0.6389999389648438,
  w: 0.754998779296875,
  W: 0.929998779296875,
  x: 0.5089996337890625,
  X: 0.63699951171875,
  y: 0.4959991455078125,
  Y: 0.66199951171875,
  z: 0.48699951171875,
  Z: 0.6239990234375,
  0: 0.6,
  1: 0.40099945068359377,
  2: 0.6,
  3: 0.6,
  4: 0.6,
  5: 0.6,
  6: 0.6,
  7: 0.5469985961914062,
  8: 0.6,
  9: 0.6,
  '[': 0.3329986572265625,
  ']': 0.3329986572265625,
  ',': 0.26399993896484375,
  '.': 0.26399993896484375,
  ';': 0.26399993896484375,
  ':': 0.26399993896484375,
  '{': 0.3329986572265625,
  '}': 0.3329986572265625,
  '\\': 0.5,
  '|': 0.19499969482421875,
  '=': 0.604998779296875,
  '+': 0.604998779296875,
  '-': 0.604998779296875,
  _: 0.5,
  '`': 0.3329986572265625,
  ' ~': 0.8329986572265625,
  '!': 0.3329986572265625,
  '@': 0.8579986572265625,
  '#': 0.6,
  $: 0.6,
  '%': 0.9699996948242188,
  '^': 0.517999267578125,
  '&': 0.7259994506835937,
  '*': 0.505999755859375,
  '(': 0.3329986572265625,
  ')': 0.3329986572265625,
  '<': 0.604998779296875,
  '>': 0.604998779296875,
  '/': 0.5,
  '?': 0.53699951171875,
  '"': 0.33699951171875
};

然后根据上述定义的字符宽度来计算整个文本的长度:

// 计算非中文字符宽度
const getLetterWidth = (letter, fontSize) => fontSize * (LetterMap[letter] || 1);

// 计算文本宽度
const getTextWidth = (text, fontSize) => {
  // 中文匹配正则
  const pattern = new RegExp('[\u4E00-\u9FA5]+');
  // 文本宽度
  const textWidth = text.split('').reduce((pre, curLetter) => {
    // 单个字符宽度
    const letterWidth = pattern.test(curLetter) ? fontSize : getLetterWidth(curLetter, fontSize);
    return pre + letterWidth;
  }, 0);
  return textWidth;
};

3.2 确定矩形大小

确定了文本的字体大小(font-size)和文本宽度后,我们就可以确定文本外部圆角边框的大小了。
为了防止边框和文本会重合,边框的大小要稍微大于文本的大小,即:

  • 圆角矩形宽度:文本宽度 + x
  • 圆角矩形高度:文本字体大小 + y

至于 x , y 的值,可自行按个人喜好设定,但是个人建议:x>=12, y>=10

我这里对于 x, y 值得设定如下:

  • x = font-size + 12
  • y = 12

3.3 绘制

假设我们要在某个SVG元素内部

  • (200, 200) 的位置
  • font-size=14
  • 字体颜色 #fff
  • 矩形背景色:#9b95c9
  • 矩形边框颜色:#C5C5D1

的带圆角边框文本 “圆角文本”

  1. 首先我们定义一个创建svg元素的函数,与创建dom元素稍微不同,需要使用document.createElementNS()函数:
const createSvgChildElement = (tagName) => {
    return document.createElementNS('http://www.w3.org/2000/svg', tagName);
}
  1. 将绘制圆角文本封装成一个函数,如下:
const drawRadiusText = (svgEl, x, y, text, fontsize, color, backgroundColor, borderColor) => {
    // 创建 rect 和 text 元素
    const rectEl = createSvgChildElement('rect');
    const textEl = createSvgChildElement('text');
    
    // 设置rect元素属性
    rectEl.setAttribute('x', x);
    rectEl.setAttribute('y', y);
    rectEl.setAttribute('width', getTextWidth(text, fontsize) + fontsize + 12);
    rectEl.setAttribute('height', fontsize + 12);
    rectEl.setAttribute('rx', rectEl.getAttribute('height') / 2)
    rectEl.setAttribute('ry', rectEl.getAttribute('height') / 2)
    rectEl.setAttribute('fill', backgroundColor);
    rectEl.setAttribute('stroke', borderColor);
    rectEl.setAttribute('transform', `translate(-${rectEl.getAttribute('width') / 2}, -${rectEl.getAttribute('height')/ 2})`);

    // 设置text元素属性
    textEl.setAttribute('x', x);
    textEl.setAttribute('y', y);
    textEl.setAttribute('dy', fontsize / 2 - 2);
    textEl.setAttribute('fontSize', fontsize);
    textEl.setAttribute('fill', color);
    textEl.setAttribute('text-anchor', 'middle');
    textEl.innerHTML = text;
    
    // 向svg中添加rect和text子元素
    // 注意:rect一定要在text之前添加,矩形有背景色时,会遮挡文本
    svgEl.appendChild(rectEl);
    svgEl.appendChild(textEl);
}

注意:rect一定要在text之前添加,矩形有背景色时,会遮挡文本

  1. 获取svg元素,并在其中指定位置绘制
const svgEl = document.querySelector('svg');
drawRadiusText(svgEl, 200, 200, "圆角文本", 14, '#fff', '#9b95c9', '#C5C5D1');

绘制效果如下。外面黑色边框是svg元素的大小,即400x400,圆角文本刚好绘制在中心(200, 200)处:

image.png

SVG元素HTML代码如下:

<svg width="400" height="400" version="1.1" xmlns="http://www.w3.org/2000/svg">
</svg>

到此绘制一个随文本宽度动态改变的带圆角边框文本就完成了。

四、React 中实现

目前写原生JS已经很少了,React 或 Vue 来编写前端页面的,那么在React中如何实现呢。其实区别不大,就是在useEffect 或者 componentDidAmount中获取带圆角边框文本的配置(config),然后动态渲染即可。

由于在渲染页面结构时,js还未加载,为了防止一些不必要的警告或错误(例如:config为null),所以我们等获取到config后再渲染。代码如下:

<svg width="400" height="400" viewBox="0 0 400 400" version="1.1" xmlns="http://www.w3.org/2000/svg">
    {config && drawRadiusText(config)}
</svg>

完整代码如下(其中获取文本宽度函数参考上文):

import React, {useState, useEffect} from "react";


export default function RadiusText() {
    const [config, setConfig] = useState(null);  // 圆角文本属性

    useEffect(() => {
        setConfig({
            x: 200,
            y: 200, 
            text: '圆角文本',
            fontSize: 14, 
            color: '#fff', 
            backgroundColor: '#9b95c9', 
            borderColor: '#C5C5D1'
        })
    }, [])

    const drawRadiusText = (config) => {
        const {x, y, text, fontSize, color, backgroundColor, borderColor} = config;
        const rectWidth = getTextWidth(text, fontSize) + fontSize + 12;
        const rectHeight = fontSize + 12;
        console.log(rectWidth, rectHeight)
        const rectProps = {
            x,
            y,
            width: rectWidth,
            height: rectHeight,
            rx: rectHeight / 2,
            ry: rectHeight / 2,
            fill: backgroundColor,
            stroke: borderColor,
            transform: `translate(-${rectWidth / 2}, -${rectHeight / 2})`
        };
        const textProps = {
            x,
            y,
            dy: fontSize / 2 - 2,
            fontSize,
            fill: color,
            textAnchor: 'middle'
        };

        return (
            <g>
                <rect {...rectProps} />
                <text {...textProps}>{text}</text>
            </g>
        );
    }

    return (
        <div className="radius-text">
            <svg width="400" height="400" viewBox="0 0 400 400" version="1.1" xmlns="http://www.w3.org/2000/svg">
                {config && drawRadiusText(config)}
            </svg>
        </div>
    )
}

image.png

本文到这里就结束了,是自己对于使用svg绘制带圆角边框文本的一些思考。当然代码中还有很多能够改进的地方,希望大佬们提出指正。