上周接了个大屏的活儿,客户指着一块实时监控面板说,这几条折线太平了,想让数据"活"起来,有种光在管子里跑的感觉。我一开始偷懒,把smooth: true打开,觉得曲线圆润一点更高级,结果流动动画一加上就露馅了——光段跑到弯道的地方忽长忽短,有的地方密得像一串珠子,有的地方又断成一大截。前后折腾了两个晚上,最后把 smooth 关掉,改成老老实实的折线,问题反而全没了。
echarts 折线图流动特效这个需求,看着简单,其实坑不少。它不像柱状图加个渐变色就完事,也不是动画库里随便调个 API——你要自己把"数据点 → 像素坐标 → 路径长度 → 虚线相位"这一整条链子串起来。而"非平滑曲线"这个限定词,恰恰是让整件事从"能看"变成"好看"的关键变量。这篇就把我从踩坑到跑通的全过程写下来,包括两套实现方案、参数怎么配、以及几个能让你少熬一个晚上的排查技巧。适合已经会用 echarts 画基础折线、想往视觉动效方向再走一步的人看,前端和可视化方向的同学都能直接抄作业。
1. 需求拆解:为什么非要"非平滑"的流动折线
1.1 大屏和实时监控里,流动线到底解决什么问题
先说清楚这个特效存在的理由。普通的静态折线图,信息是完整的,但它缺一个东西——方向感。当一块大屏上同时挂着七八个图表,人的视线是散的,眼睛会到处乱跳,最后只能靠颜色和面积去猜哪个是重点。流动线的作用就是在这片"静止的图海"里制造一处动态焦点,它会强行把视线拉到这条线的走向上,从起点一路带到终点。
除了引导视线,流动线还有一层隐含语义:它在暗示"这条数据是活的"。哪怕你的数据其实是五分钟前拉一次,只要线在动,观看者潜意识里就会认为它在实时跳动。所以这类效果特别常见于实时流量趋势、能耗曲线、产线节拍、订单量波动这些场景——这些业务本身就带"持续变化"的属性,用流动线来表达是贴合的,不会显得为了炫技而炫技。
反过来说,如果是季度营收对比、年度目标完成度这种"结论已经固定"的数据,硬套流动特效就会很奇怪,看的人会一直等它停下来。这是我做了几次之后才想明白的一条经验:动效是语义的一部分,不是装饰。选错了语义,再漂亮的动画也是减分项。
1.2 smooth 打开之后,流动效果为什么反而变差
回到标题里的关键词——非平滑曲线。我最初的想法很朴素:曲线圆润一点不是更好看吗?所以上来就写了smooth: true,然后在这个基础上做虚线滚动,结果就是开头描述的那种灾难现场。
原因得从 echarts 怎么画平滑曲线说起。当你打开 smooth,echarts 并不是简单地把折角磨圆,而是把相邻的数据点重新拟合成一条三次贝塞尔曲线。这条曲线的形状由前后几个点的位置共同决定,端点之间的实际弧长早就不是两点间的直线距离了。而虚线滚动这件事,本质上是沿路径的弧长做等距切分,弧长一旦不均匀,光段的视觉长度自然就忽大忽小。
更要命的是拐点处。折线的拐点是一个明确的角,光段在那里会干脆利落地转向,视觉上是"有骨头"的。平滑曲线在拐点附近的曲率变化非常剧烈,虚线经过这段时会出现一种很别扭的打滑感——你会感觉光段在那个位置被拉了一下,像是走在冰面上。我试过用更小的段长去缓解,结果只是把打滑变成了高频抖动,更难看。
还有一个纯工程上的理由:非平滑折线的总长度可以直接用勾股定理逐段累加,一个循环就出结果;平滑曲线要算贝塞尔弧长,得做数值积分或者采样逼近,精度和性能都要额外操心。我做过一次测试,同样 200 个点的数据,平滑曲线的路径长度采样计算在低端设备上会带来两到三毫秒的额外开销,虽然不多,但在每秒 60 帧的重绘循环里就是实打实的压力。
所以结论很明确:做流动特效就老老实实关掉 smooth。想要"柔和"的观感,可以靠圆角 lineJoin、稍宽的点符号、渐变色去补,别指望平滑曲线来救。
1.3 三条实现路线的取舍
把需求想清楚之后,我梳理出三条技术上都能走通的路,各自适用面差别挺大,先上表对比。
| 方案 | 核心思路 | 实现难度 | 流畅度 | 兼容性 | 适合场景 |
|---|---|---|---|---|---|
| A. 双层 series + 定时改 type | 叠两条折线,上层虚线,定时器不停改变lineStyle.type数组 | 低 | 一般,相位从起点算,流动不自然 | 最好,不依赖渲染器 | 要求不高、快速出效果 |
| B. 自定义图形 + lineDashOffset | 用 zrender 叠加一条 polyline,自己控制虚线偏移 | 中 | 很好,帧率完全可控 | 好,Canvas/SVG 都能用 | 大屏主力方案,推荐 |
| C. SVG 渲染器 + CSS 动画 | 切到 SVG 渲染,给 path 注入 CSS keyframes | 低 | 极好,走浏览器合成层 | 一般,依赖 DOM 结构,版本升级可能失效 | 线条少、追求极致丝滑 |
方案 A 我最早试过,思路是在setInterval里不断给lineStyle.type塞一个变化的首段长度。问题在于虚线图案的相位是按路径起点算的,改首段长度只会让第一段实线在起点"生长"和"收缩",看起来像脉冲,不像流动。除非你叠很多层去遮掩,代码反而更乱。
方案 B 是最后落地用的,可控性最强,参数全在自己手里,想做多快就做多快。方案 C 最省代码也最顺滑,因为 CSS 动画跑在浏览器合成层,JS 主线程卡一下它也不受影响,代价是要去 DOM 里捞 path 元素,稳定性差一点。
下面重点讲 B,再把 C 作为备选补上。
2. 原理层:虚线相位滚动是怎么骗过眼睛的
2.1 底纹加光段,一层线叠出的层次
先说视觉层面的设计逻辑。如果你只画一条虚线让它动,效果是不成立的——静态看就是一条花纹线,动起来也只是花纹在平移,很难让人读出"数据在流动"。真正有效果的做法是两层叠加:
- 底层:一条完整、连续的实线,颜色压低饱和度或者降低不透明度,线宽稍微放大一点。它的职责是交代"路在哪",让眼睛知道整条轨迹的形状。
- 上层:一条窄一点的亮色虚线,同样是完整路径,但只呈现出若干段光斑。它的职责是"跑"。
两层的数据必须完全一致,smooth也必须同时关掉,否则像素路径对不上,光段会从底线上飘出去。这一点看起来是废话,但我真的见过有人只给上层关了平滑,结果两层差了七八个像素,特别明显。
在暗色大屏上,底层线还可以加一点shadowBlur做辉光,会显得更有科技感。不过这里有个度:shadowBlur在 Canvas 上是很贵的,尤其是路径很长的时候,每次重绘都要重新计算阴影。我的做法是底层线不加阴影,只在最上面叠一条极细的、带一点点模糊的高亮线,或者干脆只在关键节点上放effectScatter增加点缀。
2.2 dashOffset 到底在移动什么
这是整个特效的核心,值得掰开讲。
Canvas 和 SVG 处理虚线的方式是一样的,都基于一个叫dash 数组的概念。[a, b]的含义是:沿路径从头开始,先画 a 长度的实线,再空 b 长度,然后无限循环。如果给的是[a, b, c, d],那就按 a 实、b 空、c 实、d 空、再回到 a 实这样循环。
那lineDashOffset是什么?它是整个图案沿路径方向平移的距离。你可以把它想象成有一条无限长的虚线条纹布,现在把它贴在路径这条"轨道"上,offset 就是你向左或向右拉动这块布的距离。布的图案本身没变,只是贴合的位置变了。
这里有个很容易搞反的地方:offset 的数值增加,光段视觉上是往起点方向退的。想要光段朝数据终点方向跑,就得让 offset 递减,或者取负值。我第一次写的时候方向搞反了,光段倒着跑,看了半天还以为是自己速度设错了。
再有一个必须记住的结论:图案的周期 T 等于 dash 数组各项之和,对于[a, b]就是a + b。当 offset 变化了整数倍 T 的时候,视觉上和没变是完全一样的。所以 offset 只需要在[0, T)这个区间里循环就够了。别让它无限增长下去——虽然短时间内浮点精度不会有问题,但跑一整天的看板,这个数字会涨到很大,一旦出现精度丢失,光段就会开始轻微抖动,这种问题排查起来非常让人抓狂。
2.3 非平滑折线的路径长度好算在哪
前面提过,非平滑折线就是若干直线段的拼接,总长度可以直接累加:
function pathLength(points) { let total = 0; for (let i = 1; i < points.length; i++) { const dx = points[i][0] - points[i - 1][0]; const dy = points[i][1] - points[i - 1][1]; total += Math.sqrt(dx * dx + dy * dy); } return total; }这段代码没有一行是多余的,也不会有任何精度争议。而如果用平滑曲线,同样的功能得写成贝塞尔弧长的采样逼近,采样点少了误差大,采样点多了性能吃不消,还要处理"采样密度不均匀导致长直段被低估"的问题。对于动效这种对实时性有要求的场景,能不给自己找麻烦就别找。
顺带说一个和路径长度相关的调参心得:你可以算一下总长度 / (段长 + 间隔)这个比值。如果结果接近整数,说明图案的首尾会刚好对齐,整条线看起来会有一点"规律感",某些场景下反而显得假;不整除的时候,图案在末尾会被截断,视觉上更自然一点。我一般会稍微调一下段长,把比值从整数挪开几个百分点。
3. 主力方案:自定义图形驱动 lineDashOffset
3.1 先把数据点换算成屏幕像素
echarts 的坐标系和数据坐标是两套东西,你要拿到真正能画线的像素坐标,得用convertToPixel。这个 API 需要一个坐标系来源,所以必须先有一个 series 存在。
const chart = echarts.init(document.getElementById('main')); const rawData = [120, 180, 96, 224, 158, 266, 140]; const categories = ['1月', '2月', '3月', '4月', '5月', '6月', '7月']; chart.setOption({ grid: { left: 48, right: 32, top: 32, bottom: 40 }, xAxis: { type: 'category', boundaryGap: false, data: categories, axisLine: { lineStyle: { color: 'rgba(120,180,255,0.3)' } } }, yAxis: { type: 'value', splitLine: { lineStyle: { color: 'rgba(120,180,255,0.1)' } } }, series: [ { id: 'baseLine', type: 'line', smooth: false, symbol: 'none', data: rawData, lineStyle: { color: 'rgba(90,160,255,0.28)', width: 6, cap: 'round' }, z: 2 } ] });拿到坐标系之后,就可以转换坐标了。注意 xAxis 是 category 类型时,convertToPixel的第一个参数传的是索引而不是类目名:
function getPixelPoints() { return rawData.map((v, i) => chart.convertToPixel({ seriesId: 'baseLine' }, [i, v])); }如果你的 yAxis 设了inverse: true,不用担心,convertToPixel会自己处理翻转,你拿到的就是正确的屏幕坐标。数据里如果有 null(断点),转换结果会是NaN或者[NaN, NaN],这种情况必须先把数组按断点切段,每一段单独成一条路径,否则整条线都会画不出来。
3.2 用 zrender 叠一条自定义 polyline
拿到像素点之后,下一步是绕过 echarts 的 series 体系,直接在 zrender 层加图形。好处是它完全不受 series 动画生命周期的干扰,setOption也不会把它冲掉(除非你调用了clear)。
const zr = chart.getZr(); const flowLine = new echarts.graphic.Polyline({ shape: { points: getPixelPoints() }, style: { stroke: '#00e5ff', lineWidth: 3, lineCap: 'round', lineJoin: 'round', lineDash: [16, 10], lineDashOffset: 0, fill: 'none' }, silent: true, z: 10 }); zr.add(flowLine);几个配置的用意说一下。silent: true很关键,如果不加,这条覆盖在上面的线会拦截鼠标事件,导致底层折线的 tooltip 和 hover 高亮全部失效,用户会以为你的图表坏了。z: 10保证它盖在底层线上面,但如果你的图里有zlevel更高的东西,还得再调。
lineCap和lineJoin都设成round,是为了让光段的端点是圆的。这点在非平滑折线的拐点处特别重要——直角端点会显得很生硬,圆头会让整个效果柔和不少。
如果你用的 echarts 版本里没有导出Polyline,退路是直接引入 zrender 包,用new zrender.Polyline({...}),构造参数完全一样。或者用echarts.graphic.extendShape自己定义一个带buildPath的图形类,用ctx.moveTo和ctx.lineTo逐段画,原理是一样的。
3.3 让光段匀速跑起来
动画循环我建议老老实实用requestAnimationFrame,并且基于时间戳计算偏移量,而不是每次自增一个固定值。
const SPEED = 70; // 像素每秒 const DASH_CYCLE = 16 + 10; // 一个虚线周期的长度 let startTime = null; let rafId = null; function tick(ts) { if (startTime === null) startTime = ts; const elapsed = (ts - startTime) / 1000; // 取负值让光段朝数据终点方向跑,取模避免数值无限增大 const offset = -((elapsed * SPEED) % DASH_CYCLE); flowLine.style.lineDashOffset = offset; flowLine.dirty(); rafId = requestAnimationFrame(tick); } rafId = requestAnimationFrame(tick);为什么一定要基于时间戳?因为requestAnimationFrame的回调间隔不是恒定的。在 60Hz 屏幕上是 16.7ms 左右,但页面卡顿时会掉到 30Hz 甚至更低。如果你写的是"每帧加 1.2 像素",那么在掉帧的时候动画就会明显变慢,跟其他动效就对不上了。用ts算出来的偏移量,无论帧率怎么变,光段在单位时间内走过的路程都是恒定的,观感就很稳。
另外注意flowLine.dirty()这行。zrender 有自己的刷新循环,标记 dirty 之后它会在下一个刷新时机重绘,不需要手动调zr.refresh()。手动 refresh 会强制同步重绘,在高频循环里反而拖性能。
3.4 图表尺寸变化之后的重建
这一块是很多人会漏掉的。浏览器窗口一变化,chart.resize()会重新计算坐标系,数据点对应的像素位置就变了,但你之前加的那条 zrender 图形还停留在旧坐标上,结果就是光段和底线彻底分家。
window.addEventListener('resize', () => { chart.resize(); // resize 之后坐标系才更新,这里要重新取点 flowLine.attr('shape', { points: getPixelPoints() }); flowLine.dirty(); });attr可以直接更新 shape,比 remove 掉再 add 一条新线要轻。但要注意调用时机,chart.resize()内部可能是异步渲染的,稳妥一点可以套一个chart.on('finished', ...)或者干脆放到setTimeout(..., 0)里。
如果数据本身也会更新,那就更简单了——数据一变就重新setOption,然后调一次上面的重建逻辑即可。我在项目里把这些都包成了一个rebuildFlow()函数,数据源、resize、主题切换三个地方都调它,省心。
4. 轻量方案:SVG 渲染器配合 CSS 动画
4.1 什么时候值得切到 SVG 渲染
echarts 初始化时第三个参数可以指定渲染器:
const chart = echarts.init(dom, null, { renderer: 'svg' });默认是 Canvas。SVG 渲染的好处是每个图形都是真实 DOM 元素,你能用 CSS 直接控制它,动画交给浏览器合成层去做,JS 主线程就算在忙别的也不会影响它。代价是元素多了之后性能急剧下降——线条数超过几百条、或者数据点成千上万的时候,SVG 会明显比 Canvas 卡。
所以我的判断标准是:图表里需要流动的折线不超过 3 条,且单个系列的数据点不超过 200 个,就可以用 SVG 方案。超出这个范围,还是老老实实用 Canvas 加 zrender 手动控制。
4.2 把 CSS 动画注入到指定的 path 上
具体操作是先给 SVG 注入关键帧样式,再去 DOM 里把目标路径捞出来:
const styleEl = document.createElement('style'); styleEl.textContent = ` @keyframes dashFlow { from { stroke-dashoffset: 0; } to { stroke-dashoffset: -260; } } .flow-line-anim { stroke-dasharray: 16 10; animation: dashFlow 3s linear infinite; } `; document.head.appendChild(styleEl); const FLOW_COLOR = '#00e5ff'; function attachFlowClass() { const svgRoot = chart.getDom().querySelector('svg'); if (!svgRoot) return; const paths = svgRoot.querySelectorAll('path'); paths.forEach((p) => { const stroke = (p.getAttribute('stroke') || '').toLowerCase(); if (stroke === FLOW_COLOR) { p.classList.add('flow-line-anim'); } }); } chart.on('finished', attachFlowClass);这里用颜色去匹配是比较好用的一招。因为 echarts 在 SVG 模式下会把lineStyle.color直接写到stroke属性上,只要这个颜色在你的图里是唯一的,筛选就很准。比按元素顺序取要靠谱得多,不会因为图例换了个位置就失效。
要注意-260这个偏移量必须是一个周期的整数倍,16 + 10 = 26,所以 260 正好是 10 个周期,动画循环的时候才不会有跳跃感。这个数字我见过不少人随手写,改成 -200 就会在每次循环结束时"顿"一下,肉眼能看出来。
4.3 两套方案的实际对比
跑下来我的感受是,SVG 方案的代码量大概是 Canvas 方案的三分之一,观感也更顺,因为浏览器合成层的动画天然比 JS 逐帧驱动更稳。但它的两个短板很致命:一是数据量大就崩,二是 echarts 版本升级后 SVG 的 DOM 结构有可能会调整,届时那套基于颜色筛选的逻辑可能要重写。
Canvas 方案反过来,代码多一些,但你能完全掌控节奏,甚至能做"光段跑到某个节点时停顿一下"这种定制化效果。我在正式项目里最终选的基本都是 Canvas 方案,SVG 方案更多用在演示页面或者个人小项目上。
5. 参数调优:让光段看起来"顺"而不是"抖"
5.1 段长、间隔、宽度、速度的配比
这几个参数没有放之四海皆准的答案,但有一张我常用的起步参考表,可以先照着配再微调:
| 参数 | 推荐区间 | 说明 |
|---|---|---|
| 光段长度 a | 10 ~ 22 px | 太短显得碎,太长像整条线在闪 |
| 间隔长度 b | 8 ~ 18 px | 一般取 a 的 0.8 ~ 1.2 倍 |
| 光段线宽 | 2 ~ 4 px | 比底层线窄一半左右,层次才明显 |
| 底层线宽 | 5 ~ 8 px | 压暗、加透明度 |
| 流动速度 | 50 ~ 100 px/s | 大屏远看取偏快,桌面端取偏慢 |
| 线端点 | round | 拐点处不生硬 |
速度这块有个经验值:如果一条线的总长度在 600px 左右,速度取 70px/s,光段大约 8、9 秒走完全程,这个节奏在大多数场景下最舒服。再快就会让人心慌,再慢又会觉得卡住了。大屏的话因为观看距离远,可以往 90 到 110 提一提。
段长和线宽的比例也要协调。线宽 3px 的时候段长低于 8px,光段就缩成一个个小方块了,圆角几乎看不出来;线宽 5px 的时候段长超过 30px,又会感觉整段是连续的,流动感变弱。我一般按段长 ≈ 线宽 × 4这个粗略关系去估。
5.2 多个系列同时流动怎么处理
一张图里如果三条线都在跑,很容易看花眼,所以要做区分。我通常用三种做法:
第一种是错开相位。给每条线的lineDashOffset加一个不同的初始值,比如第二条从-8开始,第三条从-16开始,防止它们的光段在同一时刻经过同一个横向位置,画面就不那么呆板。
第二种是拉开速度差。主数据用 80px/s,次要数据用 55px/s,视觉上会自然形成主次关系。不过要注意速度差别太大也会显得乱,控制在 1.5 倍以内比较稳。
第三种是做主次区分。真正重要的那条线用饱和度高、亮度高的颜色,其余两条直接用底纹线处理,不加流动。这也是我做得越多越偏爱的方案——一条会跑的线是焦点,三条会跑的线是噪音。
5.3 暗色背景下的发光处理
大屏基本是深色底,这时候光段加一点发光会好看很多。zrender 的 style 里可以直接给shadowBlur和shadowColor:
style: { stroke: '#00e5ff', lineWidth: 3, lineDash: [16, 10], shadowBlur: 8, shadowColor: 'rgba(0, 229, 255, 0.9)' }但这里要提醒一句:shadowBlur在 Canvas 上非常吃性能,尤其是在每帧都在重绘的循环里。我实测过,一条 200 点的折线加上 shadowBlur 之后,单帧耗时从 1.2ms 涨到了 5ms 上下,在低配一体机上就直接掉帧了。
替代方案是叠一条更宽的、低透明度的同色线做"假发光",成本几乎为零:
const glowLine = new echarts.graphic.Polyline({ shape: { points: getPixelPoints() }, style: { stroke: 'rgba(0, 229, 255, 0.25)', lineWidth: 9, lineCap: 'round', lineJoin: 'round', fill: 'none' }, silent: true, z: 9 });把glowLine放在flowLine下面一层,视觉上同样有扩散感,性能却好得多。这个技巧我在好几个项目里都用过,客户根本看不出来区别。
6. 排查手册:那些让我熬夜的坑
6.1 光段一卡一卡,看着像在闪
这是最常见的反馈,出现频率极高。原因基本可以归到三类。
第一类是用了setInterval驱动。setInterval的触发时机和浏览器绘制不同步,很容易出现两次更新挤在同一帧、下一帧又没更新。改成requestAnimationFrame立刻就好。
第二类是每帧都调setOption。有人为了改变偏移量,写了个定时器不停chart.setOption({...}),这等于每一帧都触发一次完整的图表更新流程,渲染、布局、生命周期全走一遍,不卡才怪。正确做法就是前面说的,直接改图形样式然后标记 dirty。
第三类是速度值设得太大。光段在相邻两帧之间位移超过半个周期,就会产生"跳格"的视觉错觉,看起来就像在闪烁。判断方法很简单:速度 × 帧间隔如果超过(a + b) / 2,就该降速或者把段长调大了。
6.2 拐点处出现颜色错位或者毛刺
如果发现光段在经过拐点时和底线对不齐,先检查两层的像素点是不是同一份。我踩过一次坑:底层用的是[i, v]转换,上层用的是[categories[i], v]转换,category 轴下这两种写法在结果上确实都能跑,但在边界情况下会差半个像素,放大看就是错位。
另一个可能是lineJoin没设成round。非平滑折线的拐点是真正的尖角,虚线经过时会形成斜接(miter),当夹角很小的时候斜接会向外延伸出去,看起来就是一根小刺。设了round之后就没了。
6.3 数据量大之后明显掉帧
数据点超过 500 个的时候,瓶颈通常不在虚线本身,而在每帧全量重绘整个 zrender 画布。这时有几个优化方向可以试:把large: true打开让 echarts 走大数据渲染路径;把光段动画的重绘范围限制在局部(zrender 的dirtyRect概念);或者干脆降低动画帧率,比如每两帧更新一次偏移量,人眼几乎察觉不到区别,但耗时直接砍半。
还有一个容易忽略的点:坐标轴的animation。如果你在数据更新时没关掉坐标轴过渡动画,每次更新都会有一段时间的轴动画和你的光段动画同时跑,两个循环叠加起来,性能压力翻倍。用animation: false或者只在必要的时候开就够了。
6.4 页面切到后台再回来,动画就断了
浏览器为了省电,标签页不可见时会暂停requestAnimationFrame,这时候elapsed是停住的。等你切回来,如果代码里用的是累计elapsed,光段会突然跳到一个很远的位置,看起来像"猛蹿"一下。
解决办法是记录最后一次的时间戳,在恢复时重置startTime:
document.addEventListener('visibilitychange', () => { if (document.visibilityState === 'visible') { startTime = null; // 下一帧重新取基准时间 } });这样切回来之后动画是从当前位置平滑续上的,不会有跳变。这个小细节不影响功能,但会直接影响观感,尤其是那种长年挂在大屏上不关的看板。
6.5 快速自查清单
把上面这些整理成一张表,出问题的时候可以按顺序过一遍:
| 现象 | 优先排查项 |
|---|---|
| 光段闪烁、跳格 | 是否用 rAF 驱动、速度是否过大 |
| 光段和底线错位 | 两层像素点是否同源、lineJoin 是否为 round |
| 整体卡顿 | 是否每帧 setOption、shadowBlur 是否过重 |
| 切后台回来跳变 | 是否重置了动画基准时间戳 |
| 光段长度忽长忽短 | smooth 是否被打开、路径是否分段处理了 null |
| 鼠标 hover 没反应 | 上层图形是否设了 silent: true |
7. 几点实际用下来的体会
这套东西我在三个项目里复用下来,最大的感受是:别把动画参数写死在代码里。段长、速度、颜色这几个值,我后来全部提到了一个配置对象里,因为不同客户对大屏的观感偏好差别非常大,有人觉得快才带感,有人觉得慢才高级。做成配置之后,调参从"改代码重新部署"变成"改一个 JSON",效率完全不一样。
另外一个体会是关于取舍的。刚开始做这类效果的时候,我总想把所有能加的动效都加上——流动线、呼吸灯、粒子、扫描光。结果做出来很热闹,但客户看完只说了一句"有点花"。后来我把动效减到只剩一条流动主线,其余全部回归静态,反而被夸说"清爽、专业"。视觉这件事,做加法容易,做减法难,一个页面里能有一处真正抓住眼睛的动效,比铺满十处要有效得多。
如果你手上的场景是地图上的飞线,那是另一套东西,走的是地理坐标系下的轨迹系列加上effect配置,原理和本文这条折线不一样,别把两者的参数互相套用,会绕远路。而如果只是普通的柱线混合图里想加一条流动线,那本文这套流程可以原封不动搬过去,只要记得把convertToPixel里的 seriesId 换成对应折线系列的 id 就行。