被问过很多次一个问题:为什么矢量图画出来的圆是实心的?问的人多半刚接触矢量图形,他们在设计软件里拖出一个圆形,原本期待得到数学课本里那种精致的空心线圈,结果屏幕上一个实心圆饼糊在画布上。这不是软件抽风,也不是设置错了,而是对“圆”在矢量世界里的身份产生了误会——矢量图形里的圆,默认是一枚印章:有面、可填色,而不只是一条轮廓线。这篇文章要把这件事彻底讲清楚:圆在电脑里到底怎么存储,默认填充从哪来,底层渲染时内部像素如何被涂色,以及你如何才能得到真正的空心圆。内容适合刚学设计软件的人、写SVG和前端动画的开发者,以及所有好奇“为什么它不按我预期显示”的图形爱好者。看完之后,你不仅能解决空心实心的困惑,还会顺带理解填充规则这个矢量渲染的底层逻辑。1. 从“几何课的圆”到“工具箱里的圆”:两种画圆逻辑1.1 数学定义里,圆只有一条线几何课上圆的定义是:到定点距离等于定长的所有点的集合。它是一条曲线,本身没有宽度。你拿圆规在纸上画,笔尖带出的那圈线就是圆,圆内部那张白纸属于这个圆,但它不会被染色。在这里,“圆的内部”只是一个概念区域,没有任何颜色附着。这个认知太根深蒂固了,以至于很多人第一次在设计软件里画圆时,会默认“我应该得到一圈线”。结果工具给了你一个饼,你觉得它错了,其实是你和软件对“圆”的理解不在一个频道上。1.2 矢量工具的圆,更像一枚印章打开任何主流矢量设计软件,选椭圆工具,按住 Shift 拖一个正圆,得到的是一个带底色的实心圆。这个行为和数学课本里的圆不一样,反而更像文具店卖的印章:印章表面刻的是圆形图案,沾上印泥往纸上一压,出来的一定是一个实心圆斑。印章不会只压出一个圈,除非你刻意只在边缘上色。设计软件里的椭圆工具就是这枚印章:它默认给你一个已经涂好印泥的面,而不是一笔画出来的线圈。为什么?因为绝大多数设计场景里,人们需要的是“形状”,不是“线条”。图标、按钮、色块、头像占位,本质都是可填色的面。如果工具默认画一个只有轮廓、没有填充色的空心圆,在白色画布上它虽然存在,但视觉上又细又弱,还得额外一步才能变成可用的色块。软件不知道你要什么,它只能给一个稳妥默认值——有面积、有颜色、看得见、也能被选中和上色。所以回答标题的第一层:矢量图画出来的圆默认是实心的,因为矢量绘图体系把圆当作一个“封闭面”来处理,而封闭面默认是带填充的。这背后不是某个软件拍脑袋的决定,是整套矢量渲染逻辑从根上就定下的。顺便说一句,工程绘图软件里的圆默认反而是空心的线条,因为工程图纸关心的是边界和尺寸,不关心面;而设计工具的定位是视觉产出,必然优先给你一个“能上色的面”。两种工具逻辑没有对错,只是目标不同。2. 电脑里的圆其实不是“圆”:路径、锚点和闭合路径2.1 矢量图形的本质是一份数学配方位图存的是成百上千像素点的颜色,矢量图存的是“怎么把这个图形画出来”的数学配方。一个圆,在矢量描述中不是一堆像素排成圆圈,而是一条由锚点和贝塞尔曲线构成的路径。路径上每个点都有坐标,点与点之间的曲线由控制点决定。拿 SVG 举例,一个圆可以写成很简洁的形式:<circle cx="100" cy="100" r="50" />cx、cy 是圆心坐标,r 是半径。但这只是方便人写的语法糖,浏览器内部仍然会把<circle>转成一条路径来处理。如果换成<path>手写,大致是这样:<path d="M100,50 A50,50 0 1,1 99.9,50 Z" />意思是:从 (100,50) 这个点出发,用半径为50的圆弧画一圈,最后闭合。矢量图形的核心优势也来自这个数学属性:无限放大不模糊。因为配方不是像素,放大后重新按当前尺寸计算即可。2.2 路径本身没有宽度,也没有面积这是理解“空心和实心”最关键的一步:路径只是一串坐标和曲线指令,它没有“视觉宽度”这个属性。你眼睛看到的线条宽度,其实是渲染时加在路径上的“描边”(stroke)产生的,类似用一支笔沿着路径走一圈。而路径围出的内部区域,则由“填充”(fill)负责。把路径比作施工图纸上的墙线:墙线本身只是在图纸上表达位置,墙刷成什么颜色、墙要砌多厚,还需要额外说明。SVG、设计软件里的图形数据,就是“施工图纸+材料清单”,实心还是空心,取决于材料清单上的两项怎么写。还有个细节容易被忽略:路径的几何位置不等于描边的可见边界。描边默认沿路径中心线向内外各扩展一半。半径 50 的圆,加 10px 的粗描边后,外边界变成 55,内边界变成 45。做严格对齐时,如果你按半径 50 去对边界,会发现怎么都差几个像素,原因就在这。2.3 闭合路径:从“一条线”变成“一个区域”的关键圆最特殊的地方在于,它天然是一条闭合路径:起点和终点连在一起,围成一个完整的环。只有闭合路径才有清晰的“内部”概念,也才谈得上填充。一条不闭合的曲线,比如一段弧线、一条折线,通常只能描边,不会被填充。把闭合路径想象成一块围栏。围栏只是边界,围出的院子才是内部场地。围栏本身要显色,得刷漆(描边);院子要显色,得铺地面材料(填充)。如果你想一眼判断矢量图形是空心还是实心,本质上就是看:它只给围栏刷了漆,还是把院子也铺满了颜色,还是两件事都做了。所以“画一个圆”这个动作,在矢量世界里其实是两步:先描述出那条闭合的围栏,再决定围栏刷不刷漆、院子铺不铺材料。默认情况下,材料清单上写着“院子铺满黑色”,于是你得到实心圆。想得到空心圆,下一步的操作应该是:把铺地面材料这一项改成“无”。3. 实心圆到底由谁决定:默认填充值、内部判定规则和光栅化3.1 答案的第一层:默认值如果你在 SVG 里写了一个<circle>,没有给它任何样式指令,规范给出的默认值是fill="black"和stroke="none"。意思很直白:默认就是黑色实心圆,没有描边。这就是“为什么矢量图画出来的圆是实心的”最字面的答案——不是因为你画错了,而是因为你还没接手之前,填充就已经默认打开了。设计软件同理:拖出来的圆形会自动带一个填充色,通常是当前选中的主色,或者默认的黑色、灰色,而描边默认关闭。所以不管你画矩形、圆形还是多边形,默认一律实心。想改变状态,就得显式去碰那两个开关。3.2 答案的第二层:什么算“内部”填充不是无脑把路径围出的区域糊一遍颜色。遇到两个圆叠在一起、一个五角星自交、或者一条路径自己绕了好几圈,“内部”到底指哪里,需要一套明确的规矩。图形渲染里最常见的是两条规则:奇偶规则(evenodd):从某个点向任意方向引一条射线,统计它与路径相交的次数。次数是奇数,点在内部;是偶数,在外部。非零环绕规则(nonzero):把路径方向也算进去,统计路径绕这个点转了多少圈,累计值非零,就认为该点属于内部。这套规则初听抽象,实际在图形设计中天天遇到。比如做一个甜甜圈形状,大圆套小圆。用奇偶规则,里圈内部会被判定为外部,形成中间透明的环;用非零环绕规则,如果两个圆方向相反,也能挖出洞;但如果你画两个同方向的圆环路径,里圈可能被填成实心。也就是说,“两个圆合在一起到底是空心还是实心”,方向也参与决定。3.3 答案的第三层:屏幕上的圆来自光栅化数学配方自己不会发光。浏览器或设计软件要把矢量图形显示到屏幕上,必须先把数学描述转换成像素,这个过程叫光栅化。渲染器对画布上每个像素问一个问题:这个像素在不在路径定义的区域内?如果在内部,就用填充色给它上色;如果正好在边界附近,就按覆盖比例混入颜色,这就是抗锯齿,让边缘显得平滑。屏幕本身就是像素阵,所谓“光滑的圆形轮廓”,其实是无数像素在边界处用透明度渐变伪装出来的。一个圆最终是实心还是空心,从底层看就是光栅化阶段执行了哪几套指令:一套指令说“内部全部涂上填充色”,另一套说“沿着路径描一圈边”。三个层面可以汇总成下表:层面决定因素空心圆配置属性层fill、stroke、stroke-widthfill="none",stroke 有颜色有粗细规则层路径是否闭合、奇偶/非零环绕规则闭合路径 + 内部被判定为无填充像素层光栅化器对内部像素和边界像素的处理内部不上色,只对边界像素描边理解这三层之后你会发现,实心圆不是一个笼统的“结果”,而是一套渲染流程的产物。改任何一层,你看到的东西都会跟着变。4. 亲手得到一枚空心圆:SVG和设计工具的实操记录4.1 用 SVG 写一个标准空心圆打开任意文本编辑器,新建一个文件,后缀名写成.svg:<svg width="200" height="200" xmlns="http://www.w3.org/2000/svg"> <circle cx="100" cy="100" r="50" fill="none" stroke="#333" stroke-width="3" /> </svg>fill="none"就是关掉填充,stroke="#333"打开描边,stroke-width="3"决定轮廓粗细。在浏览器里打开这个文件,你会得到一个标准空心圆:内部完全透明,只有一圈灰色轮廓。这里有个容易混淆的选择:有人会把fill="none"写成fill="transparent"。视觉上两者通常看不出区别,但语义不同——none是“完全不渲染填充区域”,transparent是“渲染一个全透明的区域”。这意味着在部分 Web 场景下,transparent的图形即使看着透明,仍然会拦截鼠标事件,而none不会。这个差异后面讲坑的时候会再提。4.2 设计软件里的三步操作在你手头的矢量设计工具中,操作套路几乎一致:用椭圆工具画一个圆。此时属性面板上“填充”一栏通常亮着一个色块。单击填充色块,在弹出的颜色面板里找到“无”或“透明”选项并选中,形状内部颜色立即消失。此时如果屏幕上什么都看不见,是因为描边还没打开。找到“描边”一栏,选一个颜色,再设置粗细,比如 3px、5px。一条干净的空心圆就出来了。操作本身不难,难的是建立意识:填充和描边是两个完全独立的属性,你只是把其中一个关掉、另一个打开而已。4.3 关掉填充后看起来还是实心?自查清单我见过不少人在这一步被绕进去:明明设置了“无填充”,圆在屏幕上还是像一个实心圆。碰到这种情况,按下面几条逐项查:描边是不是设得太粗了。半径 50、描边 24,视觉上就是一个粗环,观感和实心圆很像。是不是加了投影、内阴影、外发光等效果。这些效果会往圆内部铺一层颜色,造成“里面被填了”的错觉。画布底色是不是和填充色相同。你设了白色填充,画布也是白色,透明与否肉眼根本分不清。把画布临时切换成深色再看一眼。图层堆叠里是不是还有别的实心圆压在下面。图层面板里逐个隐藏排查。真正检验空心圆是否成立,最可靠的办法不是用眼看,而是导出 SVG 后用文本编辑器打开,看代码里是不是写着fill="none"。如果写着,那它就是真空心,和背景无关。5. 空与实之间的进阶玩法:环形、镂空和伪实心5.1 环形加载器:粗描边当填充用做前端加载动画的人一定熟悉这个场景:一个圆环进度条,往往不是画一个空心圆再往里面填内容,而是直接用一条粗描边配合stroke-dasharray产生弧线效果。<circle cx="100" cy="100" r="40" fill="none" stroke="#ddd" stroke-width="10" /> <circle cx="100" cy="100" r="40" fill="none" stroke="#09c" stroke-width="10" stroke-dasharray="251.2" stroke-dashoffset="62.8" transform="rotate(-90 100 100)" />第一个圆是灰色底环,第二个圆用虚线描边切成有长度的弧线,只显示四分之三圈,做出进度效果。这里的“圆环”并不是图形上真的有洞,而是填充关掉之后,粗描边自己形成了一个环。这种“伪实心”画法在进度类组件里几乎随处可见,优点是代码简单、可控性强。5.2 用填充规则做真正的镂空想做一个中间透明、外围有色的甜甜圈,不靠粗描边,也可以用两个圆合并成一条路径,再指定合适的填充规则。以 SVG 为例:<path fill-rule="evenodd" d="M100,20 A80,80 0 1,1 99.9,20 Z M100,45 A35,35 0 1,1 99.9,45 Z" />外圆半径 80,内圆半径 35,fill-rule="evenodd"会把小圆内部判为“外部”,结果就是一个环。如果改成fill-rule="nonzero",结果取决于两个子路径的方向:同向时里圈可能被填实,反向时也能形成洞。第 3 章那两套规则在这里直接变成了生产力:同一份路径数据,改一个属性,空心实心就互换。实际项目中,饼图和环形图的差异也来自这里。普通饼图是一堆扇形,每个扇形是闭合路径,默认实心。要做成市场份额那种甜甜圈图,最省事的方式是画一个粗环作为基底,再把扇形数据叠加进去。理解填充规则后,遇到“这里明明重叠了,为什么出来一个洞”的瞬间,你会自然地想到去查路径方向,而不是在那儿瞎试参数。5.3 白色填充不是空心最后必须提醒一个高频误区:为了得到空心圆,把填充设成白色。视觉效果上在白底上确实看不见颜色,但它不是空心,是“填充了白色”。一旦把这份文件放进深色页面,或者导出到深色背景上,圆心里立刻冒出一个实心白饼。同理,把填充设成当前背景色也是野路子,经不起场景切换。真正的空心在源码里应该是fill="none",这是语义层面的“没有填充”,在任何背景下都透明。判断标准只有一条:切换到浅色、深色两种背景,分别看一眼中间区域是否始终透明。能做到这一点的,才配叫空心圆。6. 被“实心圆”背刺的几个日常场景与排查链路6.1 场景一:下载的 SVG 明明有洞,导入后成了实心饼朋友发来一个圆环图标,材质是 SVG。拖进设计软件后,中间被填上了颜色。第一步别怀疑软件坏了,先用文本编辑器直接打开 SVG 文件,搜索fill和stroke的值。常见原因有两种:一是原文件里写的是fill="transparent",而导入工具对透明填充的解析不够稳定;二是原文件用 mask 或 clip-path 实现了镂空效果,导入器不支持这些特性,遮罩被丢弃,底层的实心圆就原形毕露了。处理办法是优先选择纯路径加fill-rule="evenodd"的版本,实在不行就用两个圆配合布尔运算重建一次环。6.2 场景二:网页里的“透明圆”挡住了点击给页面加了一个装饰层,里面画了一个完全透明的圆形,结果这个圆覆盖的按钮全部点不动。原因就是前面反复提到的:视觉上透明不等于图形不存在。只要填充属性还保留着,哪怕颜色是transparent,这个图形也会参与命中测试,像一面看不见的墙挡在按钮上方。解决办法是按需调整pointer-events属性,或者把填充真正设为none。但注意,不同浏览器对none填充路径的事件命中行为也有细微差异,涉及复杂叠层时,最好真机测试一遍交互。6.3 场景三:导出 PNG 后圆心被“默许”填充你以为导出的是空心圆,打开 PNG 一看,中央多了一块颜色。这种情况最常见的原因是图层顺序:空心圆下面还压着一个实心形状,导出时一视同仁地被渲染了出来。其次是白色填充骗眼睛:画布背景是白色,填充也是白色,看着像透明,导出到透明背景被替换成黑或白时,原形毕露。排查也简单:把画布背景临时改成深色,再预览一次。如果深色下中央出现明显的浅色饼状区域,说明填充并没有关干净,回到源码确认fill="none"。6.4 一套通用的排查链路当你再次碰到“实心圆不对劲”时,按这个顺序走,基本能定位问题:看源码:用文本编辑器打开源文件,搜索fill和stroke,确认实际值,不要依赖脑海里的记忆。看设计面板:选中形状,把填充色块、描边色块都点开,确认填充是不是“无/透明”,描边是不是有颜色。改背景:把画布背景临时改成醒目的深色,区分“真透明”和“白色填充”。查叠加:图层面板里检查下方是否还有别的实心形状,检查有没有投影、内发光等效果。查导出:导出设置里确认“透明背景”开关,注意 PNG 本身不支持透明度时会默认填入白色或黑色。按这个链路排查,你会发现绝大多数“实心圆 Bug”都不是 Bug,只是填充、描边、背景、图层这几件事没有对齐。尤其是第 5 步,很多人做完空心圆,栽在最后的导出设置上,这属于典型的“会画不会导出”。结合我自己的体会,这类问题最治本的办法不是记界面,而是花十分钟用文本编辑器写一个最简单的 SVG 圆环,然后挨个改这几个属性:r、fill、fill-opacity、stroke、stroke-width,每改一次就刷新一次浏览器。做完这组小实验,你会彻底理解“矢量图形不是画出来的,而是描述出来的”这句话。以后再遇到空心变实心、实心变镂空、透明区域挡住点击这些问题,第一反应就不再是“软件又坏了”,而是去查那几个决定渲染结果的属性到底写了什么。掌握了这个思路,实心圆就不再是坑,反而会变成你手里的工具。
被问过很多次一个问题:为什么矢量图画出来的圆是实心的?问的人多半刚接触矢量图形,他们在设计软件里拖出一个圆形,原本期待得到数学课本里那种精致的空心线圈,结果屏幕上一个实心圆饼糊在画布上。这不是软件抽风,也不是设置错了,而是对“圆”在矢量世界里的身份产生了误会——矢量图形里的圆,默认是一枚印章:有面、可填色,而不只是一条轮廓线。这篇文章要把这件事彻底讲清楚:圆在电脑里到底怎么存储,默认填充从哪来,底层渲染时内部像素如何被涂色,以及你如何才能得到真正的空心圆。内容适合刚学设计软件的人、写SVG和前端动画的开发者,以及所有好奇“为什么它不按我预期显示”的图形爱好者。看完之后,你不仅能解决空心实心的困惑,还会顺带理解填充规则这个矢量渲染的底层逻辑。1. 从“几何课的圆”到“工具箱里的圆”:两种画圆逻辑1.1 数学定义里,圆只有一条线几何课上圆的定义是:到定点距离等于定长的所有点的集合。它是一条曲线,本身没有宽度。你拿圆规在纸上画,笔尖带出的那圈线就是圆,圆内部那张白纸属于这个圆,但它不会被染色。在这里,“圆的内部”只是一个概念区域,没有任何颜色附着。这个认知太根深蒂固了,以至于很多人第一次在设计软件里画圆时,会默认“我应该得到一圈线”。结果工具给了你一个饼,你觉得它错了,其实是你和软件对“圆”的理解不在一个频道上。1.2 矢量工具的圆,更像一枚印章打开任何主流矢量设计软件,选椭圆工具,按住 Shift 拖一个正圆,得到的是一个带底色的实心圆。这个行为和数学课本里的圆不一样,反而更像文具店卖的印章:印章表面刻的是圆形图案,沾上印泥往纸上一压,出来的一定是一个实心圆斑。印章不会只压出一个圈,除非你刻意只在边缘上色。设计软件里的椭圆工具就是这枚印章:它默认给你一个已经涂好印泥的面,而不是一笔画出来的线圈。为什么?因为绝大多数设计场景里,人们需要的是“形状”,不是“线条”。图标、按钮、色块、头像占位,本质都是可填色的面。如果工具默认画一个只有轮廓、没有填充色的空心圆,在白色画布上它虽然存在,但视觉上又细又弱,还得额外一步才能变成可用的色块。软件不知道你要什么,它只能给一个稳妥默认值——有面积、有颜色、看得见、也能被选中和上色。所以回答标题的第一层:矢量图画出来的圆默认是实心的,因为矢量绘图体系把圆当作一个“封闭面”来处理,而封闭面默认是带填充的。这背后不是某个软件拍脑袋的决定,是整套矢量渲染逻辑从根上就定下的。顺便说一句,工程绘图软件里的圆默认反而是空心的线条,因为工程图纸关心的是边界和尺寸,不关心面;而设计工具的定位是视觉产出,必然优先给你一个“能上色的面”。两种工具逻辑没有对错,只是目标不同。2. 电脑里的圆其实不是“圆”:路径、锚点和闭合路径2.1 矢量图形的本质是一份数学配方位图存的是成百上千像素点的颜色,矢量图存的是“怎么把这个图形画出来”的数学配方。一个圆,在矢量描述中不是一堆像素排成圆圈,而是一条由锚点和贝塞尔曲线构成的路径。路径上每个点都有坐标,点与点之间的曲线由控制点决定。拿 SVG 举例,一个圆可以写成很简洁的形式:<circle cx="100" cy="100" r="50" />cx、cy 是圆心坐标,r 是半径。但这只是方便人写的语法糖,浏览器内部仍然会把<circle>转成一条路径来处理。如果换成<path>手写,大致是这样:<path d="M100,50 A50,50 0 1,1 99.9,50 Z" />意思是:从 (100,50) 这个点出发,用半径为50的圆弧画一圈,最后闭合。矢量图形的核心优势也来自这个数学属性:无限放大不模糊。因为配方不是像素,放大后重新按当前尺寸计算即可。2.2 路径本身没有宽度,也没有面积这是理解“空心和实心”最关键的一步:路径只是一串坐标和曲线指令,它没有“视觉宽度”这个属性。你眼睛看到的线条宽度,其实是渲染时加在路径上的“描边”(stroke)产生的,类似用一支笔沿着路径走一圈。而路径围出的内部区域,则由“填充”(fill)负责。把路径比作施工图纸上的墙线:墙线本身只是在图纸上表达位置,墙刷成什么颜色、墙要砌多厚,还需要额外说明。SVG、设计软件里的图形数据,就是“施工图纸+材料清单”,实心还是空心,取决于材料清单上的两项怎么写。还有个细节容易被忽略:路径的几何位置不等于描边的可见边界。描边默认沿路径中心线向内外各扩展一半。半径 50 的圆,加 10px 的粗描边后,外边界变成 55,内边界变成 45。做严格对齐时,如果你按半径 50 去对边界,会发现怎么都差几个像素,原因就在这。2.3 闭合路径:从“一条线”变成“一个区域”的关键圆最特殊的地方在于,它天然是一条闭合路径:起点和终点连在一起,围成一个完整的环。只有闭合路径才有清晰的“内部”概念,也才谈得上填充。一条不闭合的曲线,比如一段弧线、一条折线,通常只能描边,不会被填充。把闭合路径想象成一块围栏。围栏只是边界,围出的院子才是内部场地。围栏本身要显色,得刷漆(描边);院子要显色,得铺地面材料(填充)。如果你想一眼判断矢量图形是空心还是实心,本质上就是看:它只给围栏刷了漆,还是把院子也铺满了颜色,还是两件事都做了。所以“画一个圆”这个动作,在矢量世界里其实是两步:先描述出那条闭合的围栏,再决定围栏刷不刷漆、院子铺不铺材料。默认情况下,材料清单上写着“院子铺满黑色”,于是你得到实心圆。想得到空心圆,下一步的操作应该是:把铺地面材料这一项改成“无”。3. 实心圆到底由谁决定:默认填充值、内部判定规则和光栅化3.1 答案的第一层:默认值如果你在 SVG 里写了一个<circle>,没有给它任何样式指令,规范给出的默认值是fill="black"和stroke="none"。意思很直白:默认就是黑色实心圆,没有描边。这就是“为什么矢量图画出来的圆是实心的”最字面的答案——不是因为你画错了,而是因为你还没接手之前,填充就已经默认打开了。设计软件同理:拖出来的圆形会自动带一个填充色,通常是当前选中的主色,或者默认的黑色、灰色,而描边默认关闭。所以不管你画矩形、圆形还是多边形,默认一律实心。想改变状态,就得显式去碰那两个开关。3.2 答案的第二层:什么算“内部”填充不是无脑把路径围出的区域糊一遍颜色。遇到两个圆叠在一起、一个五角星自交、或者一条路径自己绕了好几圈,“内部”到底指哪里,需要一套明确的规矩。图形渲染里最常见的是两条规则:奇偶规则(evenodd):从某个点向任意方向引一条射线,统计它与路径相交的次数。次数是奇数,点在内部;是偶数,在外部。非零环绕规则(nonzero):把路径方向也算进去,统计路径绕这个点转了多少圈,累计值非零,就认为该点属于内部。这套规则初听抽象,实际在图形设计中天天遇到。比如做一个甜甜圈形状,大圆套小圆。用奇偶规则,里圈内部会被判定为外部,形成中间透明的环;用非零环绕规则,如果两个圆方向相反,也能挖出洞;但如果你画两个同方向的圆环路径,里圈可能被填成实心。也就是说,“两个圆合在一起到底是空心还是实心”,方向也参与决定。3.3 答案的第三层:屏幕上的圆来自光栅化数学配方自己不会发光。浏览器或设计软件要把矢量图形显示到屏幕上,必须先把数学描述转换成像素,这个过程叫光栅化。渲染器对画布上每个像素问一个问题:这个像素在不在路径定义的区域内?如果在内部,就用填充色给它上色;如果正好在边界附近,就按覆盖比例混入颜色,这就是抗锯齿,让边缘显得平滑。屏幕本身就是像素阵,所谓“光滑的圆形轮廓”,其实是无数像素在边界处用透明度渐变伪装出来的。一个圆最终是实心还是空心,从底层看就是光栅化阶段执行了哪几套指令:一套指令说“内部全部涂上填充色”,另一套说“沿着路径描一圈边”。三个层面可以汇总成下表:层面决定因素空心圆配置属性层fill、stroke、stroke-widthfill="none",stroke 有颜色有粗细规则层路径是否闭合、奇偶/非零环绕规则闭合路径 + 内部被判定为无填充像素层光栅化器对内部像素和边界像素的处理内部不上色,只对边界像素描边理解这三层之后你会发现,实心圆不是一个笼统的“结果”,而是一套渲染流程的产物。改任何一层,你看到的东西都会跟着变。4. 亲手得到一枚空心圆:SVG和设计工具的实操记录4.1 用 SVG 写一个标准空心圆打开任意文本编辑器,新建一个文件,后缀名写成.svg:<svg width="200" height="200" xmlns="http://www.w3.org/2000/svg"> <circle cx="100" cy="100" r="50" fill="none" stroke="#333" stroke-width="3" /> </svg>fill="none"就是关掉填充,stroke="#333"打开描边,stroke-width="3"决定轮廓粗细。在浏览器里打开这个文件,你会得到一个标准空心圆:内部完全透明,只有一圈灰色轮廓。这里有个容易混淆的选择:有人会把fill="none"写成fill="transparent"。视觉上两者通常看不出区别,但语义不同——none是“完全不渲染填充区域”,transparent是“渲染一个全透明的区域”。这意味着在部分 Web 场景下,transparent的图形即使看着透明,仍然会拦截鼠标事件,而none不会。这个差异后面讲坑的时候会再提。4.2 设计软件里的三步操作在你手头的矢量设计工具中,操作套路几乎一致:用椭圆工具画一个圆。此时属性面板上“填充”一栏通常亮着一个色块。单击填充色块,在弹出的颜色面板里找到“无”或“透明”选项并选中,形状内部颜色立即消失。此时如果屏幕上什么都看不见,是因为描边还没打开。找到“描边”一栏,选一个颜色,再设置粗细,比如 3px、5px。一条干净的空心圆就出来了。操作本身不难,难的是建立意识:填充和描边是两个完全独立的属性,你只是把其中一个关掉、另一个打开而已。4.3 关掉填充后看起来还是实心?自查清单我见过不少人在这一步被绕进去:明明设置了“无填充”,圆在屏幕上还是像一个实心圆。碰到这种情况,按下面几条逐项查:描边是不是设得太粗了。半径 50、描边 24,视觉上就是一个粗环,观感和实心圆很像。是不是加了投影、内阴影、外发光等效果。这些效果会往圆内部铺一层颜色,造成“里面被填了”的错觉。画布底色是不是和填充色相同。你设了白色填充,画布也是白色,透明与否肉眼根本分不清。把画布临时切换成深色再看一眼。图层堆叠里是不是还有别的实心圆压在下面。图层面板里逐个隐藏排查。真正检验空心圆是否成立,最可靠的办法不是用眼看,而是导出 SVG 后用文本编辑器打开,看代码里是不是写着fill="none"。如果写着,那它就是真空心,和背景无关。5. 空与实之间的进阶玩法:环形、镂空和伪实心5.1 环形加载器:粗描边当填充用做前端加载动画的人一定熟悉这个场景:一个圆环进度条,往往不是画一个空心圆再往里面填内容,而是直接用一条粗描边配合stroke-dasharray产生弧线效果。<circle cx="100" cy="100" r="40" fill="none" stroke="#ddd" stroke-width="10" /> <circle cx="100" cy="100" r="40" fill="none" stroke="#09c" stroke-width="10" stroke-dasharray="251.2" stroke-dashoffset="62.8" transform="rotate(-90 100 100)" />第一个圆是灰色底环,第二个圆用虚线描边切成有长度的弧线,只显示四分之三圈,做出进度效果。这里的“圆环”并不是图形上真的有洞,而是填充关掉之后,粗描边自己形成了一个环。这种“伪实心”画法在进度类组件里几乎随处可见,优点是代码简单、可控性强。5.2 用填充规则做真正的镂空想做一个中间透明、外围有色的甜甜圈,不靠粗描边,也可以用两个圆合并成一条路径,再指定合适的填充规则。以 SVG 为例:<path fill-rule="evenodd" d="M100,20 A80,80 0 1,1 99.9,20 Z M100,45 A35,35 0 1,1 99.9,45 Z" />外圆半径 80,内圆半径 35,fill-rule="evenodd"会把小圆内部判为“外部”,结果就是一个环。如果改成fill-rule="nonzero",结果取决于两个子路径的方向:同向时里圈可能被填实,反向时也能形成洞。第 3 章那两套规则在这里直接变成了生产力:同一份路径数据,改一个属性,空心实心就互换。实际项目中,饼图和环形图的差异也来自这里。普通饼图是一堆扇形,每个扇形是闭合路径,默认实心。要做成市场份额那种甜甜圈图,最省事的方式是画一个粗环作为基底,再把扇形数据叠加进去。理解填充规则后,遇到“这里明明重叠了,为什么出来一个洞”的瞬间,你会自然地想到去查路径方向,而不是在那儿瞎试参数。5.3 白色填充不是空心最后必须提醒一个高频误区:为了得到空心圆,把填充设成白色。视觉效果上在白底上确实看不见颜色,但它不是空心,是“填充了白色”。一旦把这份文件放进深色页面,或者导出到深色背景上,圆心里立刻冒出一个实心白饼。同理,把填充设成当前背景色也是野路子,经不起场景切换。真正的空心在源码里应该是fill="none",这是语义层面的“没有填充”,在任何背景下都透明。判断标准只有一条:切换到浅色、深色两种背景,分别看一眼中间区域是否始终透明。能做到这一点的,才配叫空心圆。6. 被“实心圆”背刺的几个日常场景与排查链路6.1 场景一:下载的 SVG 明明有洞,导入后成了实心饼朋友发来一个圆环图标,材质是 SVG。拖进设计软件后,中间被填上了颜色。第一步别怀疑软件坏了,先用文本编辑器直接打开 SVG 文件,搜索fill和stroke的值。常见原因有两种:一是原文件里写的是fill="transparent",而导入工具对透明填充的解析不够稳定;二是原文件用 mask 或 clip-path 实现了镂空效果,导入器不支持这些特性,遮罩被丢弃,底层的实心圆就原形毕露了。处理办法是优先选择纯路径加fill-rule="evenodd"的版本,实在不行就用两个圆配合布尔运算重建一次环。6.2 场景二:网页里的“透明圆”挡住了点击给页面加了一个装饰层,里面画了一个完全透明的圆形,结果这个圆覆盖的按钮全部点不动。原因就是前面反复提到的:视觉上透明不等于图形不存在。只要填充属性还保留着,哪怕颜色是transparent,这个图形也会参与命中测试,像一面看不见的墙挡在按钮上方。解决办法是按需调整pointer-events属性,或者把填充真正设为none。但注意,不同浏览器对none填充路径的事件命中行为也有细微差异,涉及复杂叠层时,最好真机测试一遍交互。6.3 场景三:导出 PNG 后圆心被“默许”填充你以为导出的是空心圆,打开 PNG 一看,中央多了一块颜色。这种情况最常见的原因是图层顺序:空心圆下面还压着一个实心形状,导出时一视同仁地被渲染了出来。其次是白色填充骗眼睛:画布背景是白色,填充也是白色,看着像透明,导出到透明背景被替换成黑或白时,原形毕露。排查也简单:把画布背景临时改成深色,再预览一次。如果深色下中央出现明显的浅色饼状区域,说明填充并没有关干净,回到源码确认fill="none"。6.4 一套通用的排查链路当你再次碰到“实心圆不对劲”时,按这个顺序走,基本能定位问题:看源码:用文本编辑器打开源文件,搜索fill和stroke,确认实际值,不要依赖脑海里的记忆。看设计面板:选中形状,把填充色块、描边色块都点开,确认填充是不是“无/透明”,描边是不是有颜色。改背景:把画布背景临时改成醒目的深色,区分“真透明”和“白色填充”。查叠加:图层面板里检查下方是否还有别的实心形状,检查有没有投影、内发光等效果。查导出:导出设置里确认“透明背景”开关,注意 PNG 本身不支持透明度时会默认填入白色或黑色。按这个链路排查,你会发现绝大多数“实心圆 Bug”都不是 Bug,只是填充、描边、背景、图层这几件事没有对齐。尤其是第 5 步,很多人做完空心圆,栽在最后的导出设置上,这属于典型的“会画不会导出”。结合我自己的体会,这类问题最治本的办法不是记界面,而是花十分钟用文本编辑器写一个最简单的 SVG 圆环,然后挨个改这几个属性:r、fill、fill-opacity、stroke、stroke-width,每改一次就刷新一次浏览器。做完这组小实验,你会彻底理解“矢量图形不是画出来的,而是描述出来的”这句话。以后再遇到空心变实心、实心变镂空、透明区域挡住点击这些问题,第一反应就不再是“软件又坏了”,而是去查那几个决定渲染结果的属性到底写了什么。掌握了这个思路,实心圆就不再是坑,反而会变成你手里的工具。