CSS Tips:圆形文本排版

在 Web 设计中,排版是至关重要的一环,它直接影响到页面的整体美观度和可读性。而圆形文本排版则是一种独特而引人注目的设计技巧,它能够为 Web 页面注入一份别致和趣味。无论是用于标题、标语还是特殊信息的突出展示,圆形文本排版都能为你的 Web 页面增添一分独特的视觉吸引力。

今天,我们就来聊一聊如何实现圆形文本排版,让你能够轻松地在自己的 Web 项目中使用这一技巧。无需担心复杂的代码或专业的设计技能,我们将以通俗易懂的方式向你介绍实现圆形文本排版的不同技术方案以及技巧。

我们的目标

我们今天的目标非常的明确,实现上图中文本圆形排版的效果。假设你有一串文本(例如"CSS & SVG are awesome | CSS & SVG are awesome | "),它需要围绕着一个圆周形排列。

HTML 复制代码
<span class="text-ring">CSS & SVG are awesome | CSS & SVG are awesome | </span>

CSS 方案:transform

首先考虑 CSS 方案。根据排版特征,会想到使用 CSS 的 transform 特性来实现。

想象一下,如果我们要把想要的字符串排成一个圆形,那么有几个必要的步骤需要做:

  • 将一串字符串分解成单个字符(母)

  • 确保每个字符宽度相等

就第一点而言,如果不借助 JavaScript 脚本,那么只能人肉对字符串进行分解:

HTML 复制代码
<span class="text-ring">
    <span>C</span>
    <span>S</span>
    <span>S</span> 
    <span>&</span> 
    <span>S</span>
    <span>V</span>
    <span>G</span> 
    <span>a</span>
    <span>r</span>
    <span>e</span>
    <span>a</span>
    <span>w</span>
    <span>e</sapn>
    <span>s</span>
    <span>o</span>
    <span>m</span>
    <span>e</span>
    <span>|</span> 
    <span>C</span>
    <span>S</span>
    <span>S</span> 
    <span>&</span> 
    <span>S</span>
    <span>V</span>
    <span>G</span> 
    <span>a</span>
    <span>r</span>
    <span>e</span> 
    <span>a</span>
    <span>w</span>
    <span>e</span>
    <span>s</span>
    <span>o</span>
    <span>m</span>
    <span>e</span> 
    <span>|</span> 
</span>

先不考虑人肉分词的痛苦,就上面这样的 HTML 结构,对于辅助技术(例如屏幕阅读器)而言是非常不友好的。因此,为了使你的 Web 更具可访问性,需要对 HTML 结构稍微做一点点调整:

HTML 复制代码
<span class="text-ring">     
    <span aria-hidden="true">         
        <span>C</span>         
        <span>S</span>         
        <span>S</span>         
        <span>&</span>         
        <span>S</span>         
        <span>V</span>         
        <span>G</span>         
        <span>a</span>         
        <span>r</span>         
        <span>e</span>         
        <span>a</span>         
        <span>w</span>         
        <span>e</sapn>         
        <span>s</span>         
        <span>o</span>         
        <span>m</span>         
        <span>e</span>         
        <span>|</span>         
        <span>C</span>         
        <span>S</span>         
        <span>S</span>         
        <span>&</span>         
        <span>S</span>         
        <span>V</span>         
        <span>G</span>         
        <span>a</span>         
        <span>r</span>         
        <span>e</span>         
        <span>a</span>         
        <span>w</span>         
        <span>e</span>         
        <span>s</span>         
        <span>o</span>         
        <span>m</span>         
        <span>e</span>         
        <span>|</span>     
    </span>     
    <span class="sr-only">CSS & SVG are awesome | CSS & SVG are awesome | </span> 
</span> 
css 复制代码
.sr-only {
    position: absolute;
    width: 1px;
    height: 1px;
    padding: 0;
    margin: -1px;
    overflow: hidden;
    clip: rect(0, 0, 0, 0);
    white-space: nowrap;
    border-width: 0;
}

上面这段 CSS 代码仅在视觉上隐藏文本,但可以被屏幕阅读器访问到。这仅是 Web 中隐藏元素的技巧之一,如果你想了解更多有关于 Web 隐藏的技术,请移步阅读《Web 隐藏术》!

人肉分词是痛苦的,我们可以借助诸如 SplitText.jsLettering.js 脚本库来分词,既简便又高效。以 Lettering.js 为例:

HTML 复制代码
<span class="text-ring">
    <span class="letters">CSS & SVG are awesome | CSS & SVG are awesome | </span>
</span>
JavaScript 复制代码
const lettering = new Lettering('.letters');
lettering.letters();
CSS 复制代码
.letters > * {
    outline: 1px solid red;
}

两行简单的 JavaScript 代码,就能帮助你分好词,而且 Web 可访问相关的事情也帮你处理好了:

注意,Lettering.js 还会考虑字符串的空格,并且也会分离出来。

分好词之后,就需要考虑第二件事情,每个字符(字母)需要等宽。通过将字体设置为等宽字体(如 Courier NewCouriermonospace),可以确保所有字母都具有相同的宽度。这样,在排列字母时就会保持等宽,使得整个文本看起来更加整齐和一致。

CSS 复制代码
.letters {
    font-family: "Courier New", Courier, monospace;
    font-size: 2rem;
    font-weight: 900;
    text-transform: uppercase;
  
    * {
        outline: 1px solid red;
    }
}

这里有一个巧妙的技巧,将每个字符的 font-size 设置为 1ch ,这个 ch 单位可以定义边的宽度。注意,1ch 等于我们字体中 0 (阿拉伯数字零)的宽度,再加上每个字符使用是等宽字体,这意味着边的宽度就是每个字符的宽度。

CSS 复制代码
.text-ring {
    font-size: clamp(2rem, 4vw + 3rem, 4rem);
}

.letters {
    font-family: "Courier New", Courier, monospace;
    font-size: 1ch; /* 等于 0 的宽度 */
    font-weight: 900;
    text-transform: uppercase;
  
    * {
        outline: 1px solid red;
    }
}

另外,我们还在它的父容器 .text-ring 上使用 CSS 的比较函数 clamp() 定义 font-size 字号,文本字号会根据视窗大小进行调整,但其范围始终在 2rem ~ 4rem 之间。这是一个很有意思的功能,如果你想知道其中的原理,请移步阅读 《CSS 的比较函数:min() 、max() 和 clamp() 》 ,有关于 ch 单位更详细的介绍,可以移步阅读《现代 CSS 中的相对单位》!

现在,让我们把每个方框(每个字符所在框)变长,就像自行车车轮的辐条一样。

CSS 复制代码
.letters {
    /* ... */
    
    * {
        display:inline-block;
        vertical-align: bottom;
        height: 200px;
    }
}

然后把所有这些辐条捆绑在一起,使它们全部叠在一起。你可以使用传统的绝对定位将所有元素叠在一起,但我在这里应用了一个比较新的方案。我使用了一种现代 Web 布局技术,即 CSS 网格布局 ,将 .letters 声明为一个网格容器,然后在其子元素上使用 grid-area 将它们放置在同一个网格区域,实现所有元素叠在一起的效果:

CSS 复制代码
.letters {
    font-family: "Courier New", Courier, monospace;
    font-size: 1ch;
    font-weight: 900;
    text-transform: uppercase;
    display: grid;
  
    * {
        outline: 1px solid red;
        height: 200px;
        grid-area: 1 / 1 / -1 / -1;
    }
}

现在想象一下,我们把这些辐条的末端固定在一个中心轴上。我们将每个辐条都比前一个稍微旋转一点。如果我们逆时针旋转父元素并删除我们的红色指引线,我们就可以在圆上排列出一些文字!旋转角度是需要做一点数学计算的,每个字母在圆周上所占的弧度是相同的。这意味着,我们可以根据字符数数量计算出每个字符旋转的角度。在我们这个示例中,字符数的数量是 48 ,包括空格符,那么每个字符的旋转角度就是 7.5deg

CSS 复制代码
.letters {
    font-family: "Courier New", Courier, monospace;
    font-size: 1ch;
    font-weight: 900;
    text-transform: uppercase;
    display: grid;
  
    * {
        outline: 1px solid red;
        height: 200px;
        grid-area: 1 / 1 / -1 / -1;
        transform-origin: bottom center;
        
        &:nth-child(1) {
            rotate: calc((1 - 1) * 7.5deg);
        }
        
        &:nth-child(2) {
            rotate: calc((2 - 1) * 7.5deg);
        }
        
        &:nth-child(3) {
            rotate: calc((3 - 1) * 7.5deg);
        }
        
        &:nth-child(4) {
            rotate: calc((4 - 1) * 7.5deg);
        }
        
        &:nth-child(5) {
            rotate: calc((5 - 1) * 7.5deg);
        }
        
        &:nth-child(6) {
            rotate: calc((6 - 1) * 7.5deg);
        }
        
        &:nth-child(7) {
            rotate: calc((7 - 1) * 7.5deg);
        }
        
        &:nth-child(8) {
            rotate: calc((8 - 1) * 7.5deg);
        }
        
        &:nth-child(9) {
            rotate: calc((9 - 1) * 7.5deg);
        }
        
        &:nth-child(10) {
          rotate: calc((10 - 1) * 7.5deg);
        }
        
        &:nth-child(11) {
            rotate: calc((11 - 1) * 7.5deg);
        }
        
        &:nth-child(12) {
            rotate: calc((12 - 1) * 7.5deg);
        }
        
        &:nth-child(13) {
            rotate: calc((13 - 1) * 7.5deg);
        }
        
        &:nth-child(14) {
            rotate: calc((14 - 1) * 7.5deg);
        }
        
        &:nth-child(15) {
            rotate: calc((15 - 1) * 7.5deg);
        }
        
        &:nth-child(16) {
            rotate: calc((16 - 1) * 7.5deg);
        }
        
        &:nth-child(17) {
            rotate: calc((17 - 1) * 7.5deg);
        }
        
        &:nth-child(18) {
            rotate: calc((18 - 1) * 7.5deg);
        }
        
        &:nth-child(19) {
            rotate: calc((19 - 1) * 7.5deg);
        }
        
        &:nth-child(20) {
            rotate: calc((20 - 1) * 7.5deg);
        }
        
        &:nth-child(21) {
            rotate: calc((21 - 1) * 7.5deg);
        }
        
        &:nth-child(22) {
            rotate: calc((22 - 1) * 7.5deg);
        }
        
        &:nth-child(23) {
            rotate: calc((23 - 1) * 7.5deg);
        }
        
        &:nth-child(24) {
            rotate: calc((24 - 1) * 7.5deg);
        }
        
        &:nth-child(25) {
            rotate: calc((25 - 1) * 7.5deg);
        }
        
        &:nth-child(26) {
            rotate: calc((26 - 1) * 7.5deg);
        }
        
        &:nth-child(27) {
            rotate: calc((27 - 1) * 7.5deg);
        }
        
        &:nth-child(28) {
            rotate: calc((28 - 1) * 7.5deg);
        }
        
        &:nth-child(29) {
            rotate: calc((29 - 1) * 7.5deg);
        }
        
        &:nth-child(30) {
            rotate: calc((30 - 1) * 7.5deg);
        }
        
        &:nth-child(31) {
            rotate: calc((31 - 1) * 7.5deg);
        }
        
        &:nth-child(32) {
            rotate: calc((32 - 1) * 7.5deg);
        }
        
        &:nth-child(33) {
           rotate: calc((33 - 1) * 7.5deg);
        }
        
        &:nth-child(34) {
            rotate: calc((34 - 1) * 7.5deg);
        }
        
        &:nth-child(35) {
            rotate: calc((35 - 1) * 7.5deg);
        }
        
        &:nth-child(36) {
            rotate: calc((36 - 1) * 7.5deg);
        }
        
        &:nth-child(37) {
            rotate: calc((37 - 1) * 7.5deg);
        }
        
        &:nth-child(38) {
            rotate: calc((38 - 1) * 7.5deg);
        }
        
        &:nth-child(39) {
            rotate: calc((39 - 1) * 7.5deg);
        }
        
        &:nth-child(40) {
            rotate: calc((40 - 1) * 7.5deg);
        }
        
        &:nth-child(41) {
            rotate: calc((41 - 1) * 7.5deg);
        }
        
        &:nth-child(42) {
            rotate: calc((42 - 1) * 7.5deg);
        }
        
        &:nth-child(43) {
            rotate: calc((43 - 1) * 7.5deg);
        }
        
        &:nth-child(44) {
            rotate: calc((44 - 1) * 7.5deg);
        }
        
        &:nth-child(45) {
            rotate: calc((45 - 1) * 7.5deg);
        }
        
        &:nth-child(46) {
            rotate: calc((46 - 1) * 7.5deg);
        }
        
        &:nth-child(47) {
            rotate: calc((47 - 1) * 7.5deg);
        }
        
        &:nth-child(48) {
            rotate: calc((48 - 1) * 7.5deg);
        }
    }
}

把红色的辅助线去掉,我们就实现了一个圆形文本排版的效果:

注意,示例中我们使用的是 CSS 的单个变换属性 rotate ,并没有使用 transform 中的 rotate() 函数,但最终所达到的目的是一致的。

你甚至还可以给它添加一点动画:

CSS 复制代码
@keyframes rotate {
    to {
         rotate: 1turn;
    }
}


.text-ring {
    font-size: clamp(2rem, 4vw + 3rem, 4rem);
    animation: rotate 10s linear infinite;
    transform-origin: bottom center;
  
    &:hover {
        animation-play-state: paused;
    }
}

Demo 地址:codepen.io/airen/full/...

CSS 方案:transform + CSS 自定义属性

在上面的示例中,有一大堆的 CSS 代码在为每个字母设置旋转角度,并且是根据字母在字符串中的顺序来调整:

CSS 复制代码
span:nth-child(1) {
    rotate: calc((1 - 1) * 7.5deg);
}

/* 省略其他 */
span:nth-child(48) {
    rotate: calc((48 - 1) * 7.5deg);
}

注意,CSS 的结构选择器 :nth-child(n):nth-of-type(n) 中的 n 代表的是结构中的索引值,但它与其他语言中的索引值有所不同,它是从 1 开始的(其他程序语言索引值通常是从 0 开始)。第一个字母(:nth-child(1))的旋转角度是 0 ,并且根据字符总数(这个示例是 48),可以计算出每个字符在圆形上的位置,所在位置对应的放置角度是 360 ÷ 48 = 7.5deg

根据这个原理,每个字母的旋转角度将会以 7.5deg 递增:

  • 第一个字符(n=1),旋转角度是 0rotate=0deg),则好是 rotate = (n - 1) × 7.5deg = 0deg

  • 第二个字符(n=2),旋转角度 rotate = (n - 1) × 7.5deg = 7.5deg

  • 第三个字符(n=3),旋转角度 rotate = (n - 1) × 7.5deg = 15deg

  • 以此类推,第四十八个字符(n=48),旋转角度 rotate = (n - 1) × 7.5deg = 352.5deg

rotate 表达式,在 CSS 中可以使用 calc() 函数来描述:

CSS 复制代码
rotate = calc((n - 1) * 7.5deg)

这个计算将会随着字符串总数不断变化的,既然如此,我们就可以考虑使用 CSS 的自定义属性来替代可变化的参数,比如字符总数,索引值,圆半径等等:

CSS 复制代码
.letters {
    --total: 48;     /* 根据字符串长度来决定 */
    --radius: 200px; /* 圆半径 */
    --ratio: calc(360deg / var(--total)); /* 旋转比率 */
}

注意,--total 是会根据字符串长度动态变化的,每个字符的索引值也是会根据字符串长度变化的。加上我们分词是使用 JavaScript 来完成的。因此,我建议每个字符的索引值 --index 和字符串长度 --total 由 JavaScript 脚本来完成:

JavaScript 复制代码
const charts = document.querySelectorAll('.letters > span');
const letters = document.querySelector('.letters');
const chartsLen = charts.length;

letters.style.setProperty('--total', chartsLen);
charts.forEach((chart, index) => {
    chart.style.setProperty('--index',index)
})

借助 CSS 的变量,我们的 CSS 代码就可以得到较大的简化:

CSS 复制代码
.text-ring {
    font-size: clamp(2rem, 4vw + 3rem, 4rem);
}

.letters {
    --radius: 200px; /* 圆半径 */
    --ratio: calc(360deg / var(--total)); /* 旋转比率 */
    font-family: "Courier New", Courier, monospace;
    font-size: 1ch;
    font-weight: 900;
    text-transform: uppercase;
    display: grid;
  
  
    [style*=--index] {
        height: var(--radius);
        grid-area: 1 / 1 / -1 / -1;
        transform-origin: bottom center;
        rotate: calc(var(--index) * var(--ratio));
    }
}

效果如下:

Demo 地址:codepen.io/airen/full/...

如果你对 CSS 自定义属性感兴趣,请移步阅读:

接着,我们来进一步优化。还记得前面我们给字符设置 font-size 的时候用了 CSS 相对单位 1ch ?我们可以利用这个 ch 单位的特性,重新设置圆的半径(替代前面示例中的固定长度)。这个技巧允许你改变字体大小时会以种能够缩放文本环的方式进行。很巧妙!

CSS 复制代码
.letters {
    --radius: 10; /* 圆半径 */
    --ratio: calc(360deg / var(--total)); /* 旋转比率 */
    --font-size: 2;
    font-family: "Courier New", Courier, monospace;
    font-size: calc(var(--font-size) * 1rem);
    font-weight: 900;
    text-transform: uppercase;
    display: grid;

    [style*="--index"] {
        grid-area: 1 / 1 / -1 / -1;
        transform-origin: bottom center;
        transform: 
            rotate(calc(var(--index) * var(--ratio)))
            translateY(calc(var(--radius) * -1ch));
    }
}

Demo 地址:codepen.io/airen/full/...

CSS 方案:三角函数

换个角度来看圆。实际上,它是围绕一个点的许多三角形。等宽字体意味着我们的圆的每一"边"都是相同宽度的。如果你有一条边的宽度和内角,你就可以计算出斜边。这个斜边就是我们的半径!

也就是说,我们可能通过三角函数计算出斜边(圆半径):

在这里我要说的是,现如今,现代 CSS 越来越强大,我们可以直接在 CSS 中使用三角函数。这也意味着,我们可以像下面这样使用 CSS 三角函数计算出圆的半径:

CSS 复制代码
.letters {
    --radius: 1;
    --ratio: calc(360deg / var(--total));
    --font-size: 2;
    --_radius: calc((var(--radius) / sin(var(--ratio))) * -1ch);

    font-family: "Courier New", Courier, monospace;
    font-size: calc(var(--font-size) * 1rem);
    font-weight: 900;
    text-transform: uppercase;
    display: grid;

    [style*="--index"] {
        grid-area: 1 / 1 / -1 / -1;
        transform-origin: bottom center;
        transform: 
            rotate(calc(var(--ratio) * var(--index)))
            translateY(var(--_radius));
    }
}

Demo 地址:codepen.io/airen/full/...

上面所展示的示例,都是基于 Lettering.js 对字符串进行分词。不过,我们也可以不依赖任何第三库,使用简单的 JavaScript 脚本,也可以实现类似的效果。

HTML 复制代码
<div class="circle"></div>
CSS 复制代码
.text-ring {
    font-family: "Courier New", Courier, monospace;
    font-size: calc(var(--font-size,2) * 1rem);
    font-weight: 900;
    text-transform: uppercase;
    display: grid;

    [style*="--index"] {
        grid-area: 1 / 1 / -1 / -1;
        transform-origin: bottom center;
    
        transform: 
            rotate(calc(360deg / var(--total) * var(--index)))
            translateY(calc(var(--radius, 5) * -1ch));
    }
}
JavaScript 复制代码
const TextRing = (text, textContainer) => {
    const CHARS = text.split("");
    const INNER_ANGLE = 360 / CHARS.length;
    const textRing = document.createElement("span");
    textRing.classList.add("text-ring");
    textRing.style.setProperty("--total", CHARS.length);
    textRing.style.setProperty(
        "--radius",
        1 / Math.sin(INNER_ANGLE / (180 / Math.PI))
    );

    CHARS.forEach((char, index) => {
        const charSpan = document.createElement("span");
        charSpan.style.setProperty("--index", index);
        charSpan.textContent = char;
        textRing.appendChild(charSpan);
    });

    textContainer.appendChild(textRing);

    return textRing;
};

const text = "CSS & SVG are awesome | CSS & SVG are awesome | ";
const circle = document.querySelector(".circle");
TextRing(text, circle);

Demo 地址:codepen.io/airen/full/...

当然,你也可以将其封装成 Web 组件。例如,将其封装成 React 组件:

HTML 复制代码
<div id="root"></div>
CSS 复制代码
.text-ring {
    font-family: "Courier New", Courier, monospace;
    font-size: calc(var(--font-size, 2) * 1rem);
    font-weight: 900;
    text-transform: uppercase;
    display: grid;

    [style*="--index"] {
        grid-area: 1 / 1 / -1 / -1;
        transform-origin: bottom center;
    
        transform: 
            rotate(calc(360deg / var(--total) * var(--index)))
            translateY(calc(var(--radius, 5) * -1ch));
    }
}
JavaScript 复制代码
import React from "https://cdn.skypack.dev/react@17.0.1"
import { render } from "https://cdn.skypack.dev/react-dom@17.0.1"

const TextRing = ({children, side}) => {
    const CHARS = children.split('')
    const INNER_ANGLE = 360 / CHARS.length
    return (
        <span
            className="text-ring"
            style={{'--total': CHARS.length,'--radius': side / Math.sin(INNER_ANGLE / (180 / Math.PI))}}
        >
            {CHARS.map((char, index) => (
                <span style={{'--index': index }}>
                    {char}
                </span>
            ))}
        </span>
    )
}

const App = () => {
    return (
        <TextRing side={2.1}>CSS & SVG are awesome | CSS & SVG are awesome | </TextRing>
    )
}

render(<App/>, document.querySelector('#root'))

Demo 地址:codepen.io/airen/full/...

要是你使用 Vue 的话,可以像下面这样使用:

HTML 复制代码
<div id="app">
    <text-ring side="2.1">CSS & SVG are awesome | CSS & SVG are awesome | </text-ring>
</div>
CSS 复制代码
.text-ring {
    font-family: "Courier New", Courier, monospace;
    font-size: calc(var(--font-size, 2) * 1rem);
    font-weight: 900;
    text-transform: uppercase;
    display: grid;

    [style*="--index"] {
        grid-area: 1 / 1 / -1 / -1;
        transform-origin: bottom center;
    
        transform: 
            rotate(calc(360deg / var(--total) * var(--index)))
            translateY(calc(var(--radius, 5) * -1ch));
     }
}
JavaScript 复制代码
import { createApp } from "https://unpkg.com/vue@3/dist/vue.esm-browser.js";

const TextRing = {
    props: {
        side: {
            type: Number,
            required: true
        },
        children: {
            type: String,
            required: true
        }
    },
    
    computed: {
        chars() {
            return this.$slots.default()[0].children.split("");
        },
        innerAngle() {
            return 360 / this.chars.length;
        }
    },
    
    template: `
        <span :style="{
            '--total': chars.length,
            '--radius': side / Math.sin(innerAngle * Math.PI / 180)
        }" class="text-ring">
            <span v-for="(char, index) in chars" :style="{'--index': index}" :key="index">
                {{ char }}
            </span>
        </span>
    `
};

createApp({
    components: {
        TextRing
    }
}).mount("#app");

Demo 地址:codepen.io/airen/full/...

完美吧!现在你拥有了 CSS 版本、JavaScript 版本、React 和 Vue 构建的 TextRing 组件,这些方法都可以实现圆形文本排版的效果。

除此之外,还有 SVG 的方案。接下来,我们再来聊聊 SVG 中是如何实现圆形文本排版的!感兴趣的话,请继续往下阅读!

SVG 方案:<text><textPath>

上面所展示的都是 CSS 的解决方案。除此之外,我们还可以使用 SVG 的 <text><textPath> 来实现圆形文本排版。

在大多数情况下,它表现得相当不错。简单地说,SVG 的 <textPath><path> 元素的结合可以轻易的使文本呈圆形排列:

  • textPath 元素指定文本应该跟随的形状

  • path 元素实际上指定了 textPath 使用的形状的坐标

XML 复制代码
<svg class="text-ring" viewBox="0 0 200 200">
    <defs>
        <path id="circle" d="M 100, 100 m -75, 0 a 75, 75 0 1, 0 150, 0 a 75, 75 0 1, 0 -150, 0 " />
    </defs>
    
    <text width="100%" lengthAdjust="spacingAndGlyphs" font-stretch="expanded">
        <textPath alignment-baseline="top" xlink:href="#circle" class="text">
            CSS & SVG are awesome | CSS & SVG are awesome |
        </textPath>
    </text>
</svg>
CSS 复制代码
.text-ring {
    display: block;
    width: 50vw;
    aspect-ratio: 1;
}

text {
    font-family: "Courier New", Courier, monospace;
    font-weight: 900;
    text-transform: uppercase;
}

Demo 地址:codepen.io/airen/full/...

现在,我们有几种方法可以定义圆的确切大小和坐标。其中一种方法是在图形设计软件(如 Figma、Sketch 或 Illustrator)中绘制一个圆,将将其导出来 <path> ,而不是 <circle>

注意,输出为 <path> 很重要,因为 <textPath> 依赖的是 <path> 。如果你不能确定导出的圆形是 <circle> 还是 <path> ,那么可以使用转换工具来确保你输出的是 <path>

Tool:thednp.github.io/svg-path-co...

当然,你还可以按照下面这种方式将一个 <circle> 转换为 <path> :

XML 复制代码
<path
    d="
        M (CENTER_X - RADIUS),CENTER_Y
        a RADIUS,RADIUS 0 1,1 (2 * RADIUS),0
        RADIUS,RADIUS 0 1,1 (-2 * RADIUS),0
  "
/>
  • CENTER_X 等同于 <circle> 元素的 cx

  • CENTER_Y 等同于 <circle> 元素的 cy

  • RADIUS 等同于 <circle> 元素的 r

假设你从 Figma 设计软件中使用圆形工具,绘制了一个圆形,导出的 SVG 代码如下:

XML 复制代码
<svg width="1024" height="1024" viewBox="0 0 1024 1024" >
    <circle cx="512" cy="512" r="511.5" stroke="black" fill="none" />
</svg>

根据上面所列公式,将 <circle> 可以转换为下面这个 <path> :

XML 复制代码
 < path     
     d = "     
         M (CENTER_X - RADIUS),CENTER_Y     
         a RADIUS,RADIUS 0 1,1 (2 * RADIUS),0     
         RADIUS,RADIUS 0 1,1 (-2 * RADIUS),0 "  />

<!-- 👇 -->
< path     
    d = "    
        M (512 - 511.5),512     
        a 511.5,511.5 0 1,1 (2 * 511.5),0     
        511.5 ,511.5 0 1,1 (-2 * 511.5),0 "  />

<!-- 👇 -->
< path     
    d = "     
        M .5,512     
        a 511.5,511.5 0 1,1 1023,0     
        511.5 ,511.5 0 1,1 -1023,0 "  /> 

将其放到整个 SVG 中:

XML 复制代码
<svg class="text-ring" viewBox="0 0 1024 1024">
    <defs>
        < path  id = "circle d ="  M  .5 , 512  a  511.5 , 511.5  0  1 , 1  1023 , 0 511.5 , 511.5  0  1 , 1  -1023 , 0 " />
    </defs>
    
    <text width="100%" lengthAdjust="spacingAndGlyphs" font-stretch="expanded">
        <textPath alignment-baseline="top" xlink:href="#circle" class="text">
            CSS & SVG are awesome | CSS & SVG are awesome |
        </textPath>
    </text>
</svg>

这并不是我们所要的效果。你需要选择是否使用 <textPath>textLength 属性。这可以将文本分布在路径周围。其值将是圆的周长:

XML 复制代码
 < textPath  href = "#circularPath"  textLength = {Math.floor(Math.PI * 2 * RADIUS )}>     Your text here! </ textPath > 

应用到上面的实例中,textLength 的值大约是 3213

XML 复制代码
<svg class="text-ring" viewBox="0 0 1024 1024">
    <defs>
        <path id="circle" d=" M .5,512 a 511.5,511.5 0 1,1 1023,0 511.5,511.5 0 1,1 -1023,0 " />
    </defs>
    
    <text width="100%" lengthAdjust="spacingAndGlyphs" font-stretch="expanded">
        <textPath alignment-baseline="top" textLength="3213" xlink:href="#circle" class="text">
            CSS & SVG are awesome | CSS & SVG are awesome |
        </textPath>
    </text>
</svg>

你现在看到的效果并不怎么完美。你需要对 SVG 的坐标做一些调整,这就需要你对 SVG 的坐标系统有一定的认识或对这方面知识有所了解,否则你将会碰到一些困难。在这里我假设你对这方面的知识有所了解,如果从未接触过这方面的知识,请关注我后续课程的更新。

XML 复制代码
<svg class="text-ring" viewBox="-256 -256 1536 1536">
    <defs>
        <path id="circle" d=" M .5,512 a 512,512 0 1,1 1024,0 512,512 0 1,1 -1024,0 " />
    </defs>
    
    <text width="100%" lengthAdjust="spacingAndGlyphs" font-stretch="expanded">
        <textPath alignment-baseline="top" textLength="3216" xlink:href="#circle" class="text">
            CSS & SVG are awesome | CSS & SVG are awesome |
        </textPath>
    </text>
</svg>

调整之后,你将看到的效果如下:

Demo 地址:codepen.io/airen/full/...

是不是爽多了。

小结

阅读到这里,你是否会觉得圆形文本排版也是很简单的。在这里,我们介绍了多种不同方式实现圆形文本排版的技术方案:CSS 方案和 SVG 方案。

对于 CSS 方案,不管是 HTML + CSS,React 还是 Vue ,它们是具有共同点的,只是实现手段不同:

  • 需要对字符串进行分词,你可以人肉将字符串分词,这种方式是痛苦的,局限性也是很明显的;你可以借助第三方 JavaScript 库来分词,这种方式轻松简便,但需要依赖一个 JavaScript 库;你也可以自己编写 JavaSript 脚本来分词

  • 需要使用 CSS 的变换来将每个词排在一个圆形上,这里有多种不同的方式,最原始的 CSS 变换(transform),改进型的 CSS 变换加 CSS 自定义属性,先进型的 CSS 变换加 CSS 三角函数。 虽然实现方式不同,但原理是相似的

  • 需要将文本设置为等宽字体

基于上述方式,你可以快速的将其封装成 React和 Vue 组件,实现重复性使用。

除此之外,SVG 的 <text><textPath><path> 相结合,能轻易实现圆形文本排版,但你需要对 SVG 相关的知识有所了解,否则你会碰到一些痛苦的事情,例如文本被截剪了,文本太小了等等。如果你对这方面知识感兴趣,可以关注我在这方面的相关更新!

当然,你也可以利用 CSS 的 transtion 和 animation 给圆形文本排版添加一些动画效果

最后,希望今天所介绍的小技巧对你实际工作有所帮助!如果你喜欢的话,请关注我!我将不间断的向大家更新有关于 CSS 相关的知识,包括理论基础,小技巧,案例分析,动画等等!

如果你对 CSS 方面的技巧感兴趣,请移步阅读:


如果你觉得该教程对你有所帮助,请给我点个赞。要是你喜欢 CSS ,或者想进一步了解和掌握 CSS 相关的知识,请关注我的专栏,或者移步阅读下面这些系列教程:

相关推荐
gqkmiss6 分钟前
Chrome 浏览器 131 版本开发者工具(DevTools)更新内容
前端·chrome·浏览器·chrome devtools
Summer不秃12 分钟前
Flutter之使用mqtt进行连接和信息传输的使用案例
前端·flutter
旭日猎鹰16 分钟前
Flutter踩坑记录(二)-- GestureDetector+Expanded点击无效果
前端·javascript·flutter
Viktor_Ye22 分钟前
高效集成易快报与金蝶应付单的方案
java·前端·数据库
hummhumm24 分钟前
第 25 章 - Golang 项目结构
java·开发语言·前端·后端·python·elasticsearch·golang
乐闻x1 小时前
Vue.js 性能优化指南:掌握 keep-alive 的使用技巧
前端·vue.js·性能优化
一条晒干的咸魚1 小时前
【Web前端】创建我的第一个 Web 表单
服务器·前端·javascript·json·对象·表单
Amd7941 小时前
Nuxt.js 应用中的 webpack:compiled 事件钩子
前端·webpack·开发·编译·nuxt.js·事件·钩子
生椰拿铁You1 小时前
09 —— Webpack搭建开发环境
前端·webpack·node.js
狸克先生1 小时前
如何用AI写小说(二):Gradio 超简单的网页前端交互
前端·人工智能·chatgpt·交互