ARTICLE DETAIL

资讯详情

深耕编程入门与网站建设的一线实战洞察。

前端词云图实战:自定义形状与3D球形滚动方案解析

前端词云图实战:自定义形状与3D球形滚动方案解析 简介面向前端开发与数据可视化人群的一套词云图实现资源解决自定义图片形状生成词云以及3D球形动态滚动效果的需求。包内提供三套可运行的实现方案ECharts自定义图片形状词云纯JS编写的3D球体滚动词云支持鼠标悬浮位置定向滚动以及基于HTML5 Canvas画布实现的3D动态词云可自由缩放画布大小覆盖从静态质感文字到动态交互展示的多种视觉方向便于按需选用或组合改造。压缩包共8个文件大小78KB核心为3个JavaScript脚本含jQuery、tagcanvas等第三方库与自定义逻辑另搭配2个HTML演示页面、1个CSS样式表、1张参考图片和1份说明文档目录结构简单可直接运行demo并对照源码理解实现脉络。资源已有4293人学习无论是快速落地一个酷炫词云效果还是研究ECharts与Canvas的3D实现手法都能获得可复用的代码与清晰的实现思路。 做前端的同学应该都被产品或者视觉提过这样的需求把几十上百个关键词标签做成一张词云图而且形状还得是固定的——比如品牌 logo、人物剪影、或者一个图标轮廓。我最早接到这个需求时老大说得轻描淡写“就一个词云图嘛形状按这个来。”结果真正动手才发现要在 echarts 上实现指定形状的词语排列根本不是拖个图表组件那么简单。再加上后续又衍生了 3D 球形滚动的展示版本以及对不同分辨率屏幕的缩放适配整个项目的技术跨度从 echarts 配置延展到了原生 canvas 3D 投影。这篇文章把我从“确定需求”到“三个版本全部落地”的完整过程整理出来包含 echarts 自定义形状词云的实现细节、原生 js 的 3D 球体滚动方案以及基于 htmlCanvas 的缩放 3D 球形滚动方案。里面所有代码思想都可以直接迁移复用同时也把我在实际调试中踩过的那些坑一并写清楚希望对准备做词云图或 3D 标签展示的同行有帮助。1. 需求场景与三条技术路线的取舍1.1 自定义形状词云到底在解决什么问题普通词云图就是让关键词在一个矩形盒子里随机排列大小代表权重颜色做区分。它最大的问题在于“没有记忆点”——满屏都是字视觉重心完全靠字体大小撑。而指定形状的词云图不一样它是先用一张模板图片将整个画布划分出“文字可以出现的区域”和“禁止文字越界的区域”然后让词云算法在这些允许区域内做排版。实际业务里这种需求往往出现在大屏展示、品牌宣传页、或者报告封面这类强视觉的场景。比如客户提供的模板是一棵树的形状那关键词排出来就要填充成一棵树模板是个圆形 logo那词语就得全部落在圆内。视觉上词频高的词大一点、居中一点低频词小一点、散落在边缘整体层次和形状同时满足。这个需求单纯用 echarts 默认的矩形词云是办不到的必须做自定义形状。一开始我们打算让美术直接排一张静态图后来发现关键词列表是接口动态返回的权重也是实时算的图片方案根本跟不上数据变化只能走前端动态生成这条路。1.2 三种方案的选型对比同一个需求我在实际开发中做了三个版本对应三种不同的使用场景第一个版本是基于 echarts 的自定义形状词云核心依赖是echarts-wordcloud插件。它适合“数据动态更新、需要图表完整交互能力”的场景比如用户点击某个关键词触发下钻查询。这个版本的优点是成熟稳定、有现成的 tooltip 和点击事件缺点是 echarts 本体加插件体积偏大。第二个版本是用原生 js 实现的 3D 球体滚动词云不依赖任何第三方图表库。它把关键词分布在三维球面上通过鼠标拖拽或自动旋转让词逐个转出来。这个方案适合做产品首页的 3D 标签墙、活动页氛围展示视觉冲击力很强。第三个版本是在第二个版本基础上针对移动端和多分辨率适配做的增强使用 htmlCanvas 完成 3D 坐标到 2D 屏幕坐标的投影换算并加入缩放因子来适配不同屏宽。三种方案从开发量、体积、视觉效果和适用范围上都各有取舍下面逐一拆解实现细节。方案核心依赖适用场景开发量性能成本echarts 自定义形状echarts echarts-wordcloud需要交互、数据动态刷新低中原生 js 3D 球体滚动无视觉展示、标签墙中低htmlCanvas 缩放 3D无跨端、多分辨率适配中高低2. echarts 自定义形状词云版的落地实现2.1 形状模板的生成原理echarts-wordcloud 的自定义形状核心是通过maskImage参数传入一个 Image 对象。词云算法会读取这张图片的像素信息把透明区域视为“禁区”不透明区域视为“可绘制文字区域”。也就是说真正决定文字排布位置的不是图片本身而是图片的透明通道。所以第一步是把设计稿提供的模板处理成带透明背景的 PNG。这一步有三个细节要注意模板只保留形状轮廓不要带颜色和渐变因为最终显示的是文字不是图片。轮廓边缘要尽量清晰模糊边缘会导致文字排列边界不确定视觉效果脏。图片尺寸不要太大建议宽度控制在 600 到 900 像素之间词云算法需要按像素遍历图越大计算越慢。拿到模板后在代码里这样加载const maskImage new Image(); maskImage.src ./tree-mask.png; // 替换成实际模板路径 maskImage.onload function () { initWordCloud(maskImage); };这里有一个容易忽略的点模板图片必须是同源的否则 canvas 会被污染echarts-wordcloud 在读取像素时会直接抛安全错误。如果模板放在 CDN 上一定要确认 CDN 返回了正确的Access-Control-Allow-Origin头同时给Image对象加上crossOrigin anonymousconst maskImage new Image(); maskImage.crossOrigin anonymous; maskImage.src https://cdn.example.com/mask.png;2.2 核心参数与踩坑点mount 上initWordCloud后完整的 option 大概是这样的function initWordCloud(maskImage) { const chart echarts.init(document.getElementById(word-cloud)); const data [ { name: 数据可视化, value: 100 }, { name: 词云图, value: 80 }, { name: 前端开发, value: 60 }, // ... 更多数据 ]; const option { series: [{ type: wordCloud, shape: circle, maskImage: maskImage, width: 100%, height: 100%, sizeRange: [12, 60], rotationRange: [-90, 90], rotationStep: 45, gridSize: 4, drawOutOfBound: false, textStyle: { fontFamily: PingFang SC, Microsoft YaHei, sans-serif, fontWeight: bold, color: function () { return rgb( [ Math.round(Math.random() * 160 64), Math.round(Math.random() * 160 64), Math.round(Math.random() * 160 64) ].join(,) ); } }, emphasis: { textStyle: { color: #ff4d4f } }, data: data }] }; chart.setOption(option); chart.on(click, function (params) { console.log(点击了词项, params.name); }); }参数层面有几个直接影响效果的关键项我的实测建议如下gridSize控制的是文字排布时的“粒度”值越小文字排布越紧凑、越能贴合形状边缘但计算量成倍上升。我这边项目数据量在 60 到 120 个词之间gridSize设为 4 比较合适超过 200 个词建议调到 6否则首次渲染会卡顿 1 到 2 秒。sizeRange的两端分别对应最小词字号和最大词字号。最大字号越大词的视觉层级越清晰但也更容易溢出模板边缘需要结合drawOutOfBound: false把越界词强制丢弃。rotationRange控制允许旋转的角度范围。中文词建议控制在[-45, 45]且rotationStep用 45 的倍数这样字体方向有变化但不至于让用户歪着头看。全英文场景可以放开到[-90, 90]。实际做的时候还踩过一个比较隐蔽的坑模板图片某些轮廓内部有“空白透明孔洞”而视觉上这里应该显示文字。原因是设计给的 PNG 是从字体库里导出的字形文字内部笔画封闭区域是透明的。词云算法只认非透明区域于是这些封闭笔画变成了空洞词排进去字形形状就断了。解决思路有两个——要么用轮廓填充后的纯色版本要么在代码里做一个像素膨胀处理。function inflateMask(imageData, inflateRadius) { const data imageData.data; const width imageData.width; const height imageData.height; const alpha new Uint8Array(width * height); // 先把 alpha 通道提取出来 for (let i 3; i data.length; i 4) { alpha[(i - 3) / 4] data[i]; } // 简单膨胀如果一个透明像素周围 inflateRadius 内有非透明像素则视为非透明 for (let x 0; x width; x) { for (let y 0; y height; y) { const idx y * width x; if (alpha[idx] 0) continue; for (let dx -inflateRadius; dx inflateRadius; dx) { for (let dy -inflateRadius; dy inflateRadius; dy) { const nx x dx; const ny y dy; if (nx 0 || ny 0 || nx width || ny height) continue; if (alpha[ny * width nx] 0) { alpha[idx] 255; } } } } } for (let i 0; i alpha.length; i) { data[i * 4 3] alpha[i]; } return imageData; }这段代码的思路很简单把 alpha 通道里“空洞”边缘的透明像素根据周围不透明像素做填充。inflateRadius 取 2 到 4 效果就比较理想再大就会明显改变轮廓形态反而失真了。3. 原生 js 版 3D 球体滚动词云3.1 球面坐标分布从二维到三维echarts 版的词云做出来之后产品那边提出一个新的要求能不能让词像行星一样围着一个球体表面滚动这个需求本质上不是“词云”而是“3D 标签球”。我没有直接上 three.js因为这个场景只有文字标签的布局和旋转没有复杂光照和材质需求用原生 canvas 就能实现还能省下约 90KB 的依赖体积。3D 球体词云的第一步是把词分布到球面上。如果直接用随机角度生成球面坐标会导致两极粒子密集、赤道稀疏视觉上不均匀。正确做法是使用球面均匀分布的思路按纬线方向每层固定数量来分配。这里我用的是螺旋点法也就是把球面按层展开每一层在经度方向上旋转一个固定角度function generateSpherePoints(count, radius) { const points []; const phi Math.PI * (3 - Math.sqrt(5)); // 黄金角 for (let i 0; i count; i) { const y 1 - (i / (count - 1)) * 2; // y 从 1 到 -1保证均匀 const radiusAtY Math.sqrt(1 - y * y); const theta phi * i; points.push({ x: radius * radiusAtY * Math.cos(theta), y: radius * y, z: radius * radiusAtY * Math.sin(theta) }); } return points; }这个算法的核心是黄金角3 - Math.sqrt(5)它保证每个点相对前一个点在经度方向进行固定角度的旋转出来的点分布非常均匀肉眼几乎看不到聚集现象。词数量在 20 到 200 之间效果都很好。3.2 旋转与透视投影的实现球面上的点生成完毕后接下来要做两件事让整个球体持续旋转以及把三维坐标投影到二维 canvas 屏幕坐标上。旋转我用绕 Y 轴和 X 轴两个方向叠加。每次更新坐标时先计算当前点绕 Y 轴旋转后的坐标再在此基础上绕 X 轴旋转叠加出自由滚动的效果function rotateY(point, angle) { const cos Math.cos(angle); const sin Math.sin(angle); return { x: point.x * cos point.z * sin, y: point.y, z: point.x * -sin point.z * cos }; } function rotateX(point, angle) { const cos Math.cos(angle); const sin Math.sin(angle); return { x: point.x, y: point.y * cos - point.z * sin, z: point.y * sin point.z * cos }; }做完旋转后的三维坐标需要投影到屏幕上。这里我使用的是透视投影公式不需要引入矩阵库手写也就几行function project(point, width, height, fov, viewDistance) { const scale viewDistance / (viewDistance - point.z); return { x: width / 2 point.x * scale, y: height / 2 point.y * scale, scale: scale }; }对应动画主循环里每帧执行的操作是清空画布 → 更新旋转角度 → 把所有词按新的旋转矩阵计算坐标 → 做透视投影 → 按 z 轴深度排序 → 逐个绘制文字。关于 z 轴排序必须注意否则会出现在球体背面离观察者更远的词反而盖住前面词的情况。排序规则是 z 值小的在后方、先画z 值大的在前方、后画const projectData words.map(word { let p rotateY(word.point, angleY); p rotateX(p, angleX); const screen project(p, width, height, 600, 3); return { ...word, ...screen }; }); projectData.sort((a, b) a.z - b.z); projectData.forEach(item { ctx.font ${Math.round(item.weight * item.scale)}px sans-serif; ctx.fillStyle rgba(255, 255, 255, ${Math.min(1, item.scale)}); ctx.textAlign center; ctx.fillText(item.name, item.x, item.y); });这里字体大小用词频权重乘以 scale 系数距离远的词因为 scale 小字也更小且更透明就产生了 3D 的纵深感。要让 3D 滚动更自然还可以加入鼠标交互比较简单的做法是在鼠标按下时记录水平移动距离把它映射为球体额外的旋转速度canvas.addEventListener(mousemove, (e) { if (e.buttons 1) { const dx e.movementX; const dy e.movementY; targetAngleY dx * 0.005; targetAngleX dy * 0.005; } });当前帧的 angleY 和 angleX 往 target 方向做线性插值这样手指松开后球体还会顺着惯性滑过一点角度交互手感会明显加分。4. htmlCanvas 跨端缩放版 3D 滚动词云4.1 缩放适配的技术要点做完原生 js 版 3D 球体词云以后问题很快来了项目需要投到不同尺寸的屏幕上大屏的物理分辨率可能是 3840×2160移动端可能只有 375×667。直接套用固定宽高的 canvas要么在大屏上模糊要么在手机上显示不全。于是就有了第三个版本用 htmlCanvas 实现带有缩放因子的 3D 球形滚动。这个方案的核心是把 canvas 的绘制尺寸和实际展示尺寸分开。canvas 的width和height设置成整个品牌词的独立“绘制分辨率”再用 CSS 把它缩放展示到不同的容器中。只要绘制分辨率足够高缩小时清晰度不会损失放大时最多也只是模糊到比例一致的程度。我的做法是维护一个全局 scale 变量它由外部传入的目标屏幕宽度和基准设计宽度计算得到const designWidth 1440; // 设计稿基准宽度 const designHeight 900; function resizeCanvas(container) { const rect container.getBoundingClientRect(); const containerWidth rect.width; const containerHeight rect.height; const scale Math.min(containerWidth / designWidth, containerHeight / designHeight); canvas.width designWidth; canvas.height designHeight; canvas.style.width containerWidth px; canvas.style.height containerHeight px; canvas.style.transform scale(${scale}); // 投影时的视野距离也随缩放变化保证 3D 效果一致 viewDistance 3 * scale; }用 Math.min 是为了让画布在宽屏和窄屏上都保持完整的球体出现在画面里而不是为了填满宽度把上下裁掉。如果你希望球体填满整个屏幕可以改成 Math.max但那样会在比例不一致时裁掉一部分边缘。4.2 与 echarts 版本的数据结构和渲染差异在 canvas 3D 版本里不需要像 echarts 那样定义完整的option数据结构。数据源一般是一个简单的数组const words [ { name: 词云图, weight: 80 }, { name: 滚动效果, weight: 60 }, { name: 3D 投影, weight: 75 }, // ... ];weight 字段直接对应字体大小。渲染时通过words.length生成球面上的点接着把 weight 映射为字体大小。这里我建议把字体大小控制在 12 到 72 像素之间避免 weight 差异极大时出现超出画布或看不清的问题const fontSize 14 (word.weight / 100) * 48;如果你要在这个 canvas 版本里实现“点击词跳转”需要注意 canvas 不像 DOM 元素那样本身能识别点击目标。你需要手动计算鼠标位置把它转换到 canvas 的绘制坐标体系然后遍历所有已渲染的词判断鼠标点是否落在某个词的包围盒里。我曾经简化过这个逻辑只按词的中心点距离做判定实测在词数量较多时点小词总是被旁边的大词抢走。正确做法是每个词在绘制时记录x、y、width、height这组文本度量信息点击时逐个做“矩形包含”判断并且因为 canvas 可能被 CSS 缩放鼠标位置也要除以缩放比例还原到绘制坐标再比较。5. 性能优化与常见问题排查5.1 卡顿与模糊问题3D 球体词云虽然看起来炫但容易忽略的是它每帧都在做全量重绘。词量为 80 个时40 次重绘每帧的性能开销在低端手机上大概 8 到 12 毫秒还勉强能接受。但词量超过 150 个时每帧重绘时间可能飙到 20 毫秒以上掉帧就会明显。我常用三个性能优化手段第一个是降低重绘频率。3D 球体旋转不需要每帧都刷新到满帧 60fps对视觉展示来说 30fps 已经足够平滑。可以用时间戳控制如果距上次渲染不足 33 毫秒就直接跳过。第二个是离屏 canvas 缓存。如果球的旋转范围不大可以把相邻两帧之间不变的那部分文字预先绘制到一个离屏 canvas 上新帧只绘制变化的那几个词。这个优化对词数量特别多的情况提升很明显但实现复杂度高一般在词数超过 200 时才值得做。第三个是减少阴影、模糊等特效。canvas 的shadowBlur和全局透明度叠加在每帧全量重绘时是性能杀手能用纯色就用纯色能不加阴影就不加阴影。模糊问题也一样常见。如果 canvas 的绘制分辨率小于物理分辨率在 Retina 屏上就会模糊。解决方法是设置 canvas 宽高时乘以window.devicePixelRatioconst dpr window.devicePixelRatio || 1; canvas.width designWidth * dpr; canvas.height designHeight * dpr; canvas.getContext(2d).setTransform(dpr, 0, 0, dpr, 0, 0);所有绘制逻辑保持用设计稿宽高做基准setTransform会自动把坐标放大到物理像素这种方法在高分屏上文字会明显锐利很多。5.2 字体、文字重叠与隐藏词的排查做词云和 3D 词云经常遇到几个看似简单但很折腾人的问题。第一个是字体不对。页面上定义了font-family但 canvas 绘制时不一定生效特别是在自定义字体未加载完成时就执行了fillText。解决办法是在document.fonts.ready之后再初始化画布document.fonts.ready.then(() { initCanvas(); });或者直接监听document.fonts.load(bold 14px PingFang SC)的 Promise 完成再调用初始化。第二个是文字重叠。这个在 echarts 自定义形状版里也会出现因为词云算法本质是贪心放置如果gridSize设置太大词与词之间的缝隙减少就会出现肉眼可见的贴着挤压甚至算不出合适的位置。我的经验是出现重叠先调大gridSize同时把rotationRange缩小或固定为 0减少旋转角度带来的碰撞复杂度一般就能解决。第三个是某些词完全不显示。在 echarts 自定义形状版里先确认drawOutOfBound是不是设为了false因为模板形状边缘外的词会被主动丢弃数据权重小的词最容易消失。可以把丢弃的词数量通过console.log打印出来对比若丢弃过多说明模板内有效区域太小需要扩大模板的轮廓。写在最后做这个项目的整体感受是数据可视化里最磨人的不是技术难点而是“效果和性能之间的反复取舍”。echarts 自定义形状版功能全、交互好但体积大、首次计算有延迟原生 js 3D 球体版视觉效果最直接也最可控但所有交互都要手写htmlCanvas 缩放版解决的是前两者都忽视的适配问题。三个版本凑在一起基本覆盖了词云在 Web 前端最常见的落地形态。如果让我给人建议我会说如果只是要在后台管理系统里放一个能用的词云直接用 echarts maskImage 就够了如果是给官网首页做展示墙用原生 js 手写 3D 球体是性价比最高的方案一旦你的用户遍布各种屏幕尺寸千万别省掉 dpr 适配和缩放因子的代码。踩过一遍坑之后你会发现这些逻辑都不是一次性写完就结束的后续数据量变了、模板换了、屏幕比例变了都得回来重新调参所以一开始就把渲染逻辑和业务数据解耦得干净点后面能省很多事情。本文还有配套的精品资源点击获取
返回列表