多彩编程 多彩编程MZPH · CODE BLOG
ARTICLE DETAIL

文章详情

深耕前端与后端开发技术的一线实战笔记与踩坑复盘。

用Flutter在鸿蒙上呈现洛伦兹吸引子:跨平台3D可视化的完整实践

用Flutter在鸿蒙上呈现洛伦兹吸引子:跨平台3D可视化的完整实践 混沌科学有一种独特的美感它藏在看似随机的轨迹里又露出数学规律的马脚。奇异吸引子就是把这种美感视觉化的最佳载体之一。我最近在折腾一个新方向用Flutter把洛伦兹系统Lorenz System的数字重构搬到鸿蒙端做成交互式的3D轨迹可视化。这个项目本身不复杂但把数学物理模型、跨平台框架、国产操作系统三者拧在一起确实踩了不少坑也攒了不少可以复用的经验。这篇东西既是对混沌科学之痕的一次数字再现也是用实际案例复盘一下Flutter对接鸿蒙生态的开发路径。如果你正在做Flutter跨平台开发同时对鸿蒙适配有好奇或者单纯想看看洛伦兹吸引子那对标志性的蝴蝶双翼是怎么在手机屏幕上呈现的这篇文章都值得往下看。我会从数学原理解析、Flutter工程架构、鸿蒙端适配、3D渲染实现到性能优化完整拆一遍这个项目的落地过程。1. 项目缘起与整体设计思路1.1 为什么偏偏选奇异吸引子做可视化奇异吸引子Strange Attractor是混沌理论里最迷人的概念。简单说它是一个动力学系统在相空间中长时间演化的极限集合。和普通吸引子比如一个稳定焦点、一条极限环不同奇异吸引子本身具有分形结构——无穷嵌套的自相似几何同时系统对初始条件极度敏感稍微挪动起点后面的轨迹就完全分道扬镳。这就是蝴蝶效应的数学来源。洛伦兹系统是最经典的奇异吸引子例子。1963年Edward Lorenz在研究大气对流模型时从三个极度简化的一阶常微分方程中发现了这种确定性混沌行为。这三个方程生成了那对著名的蝴蝶翅膀轨迹既不是周期性重复也不会完全发散到无穷远处而是在一个有界区域内永不停歇地游走。视觉上极具冲击力数学上又是非线性动力学的入门必修课拿来做成App内的可视化再合适不过。从开发角度看洛伦兹系统做数字重构有几个天然优势状态空间是三维的只需要处理(x, y, z)三个变量几何映射直观。微分方程形式简单不需要复杂的物理引擎或碰撞检测一个整数积分器就能迭代。视觉核心诉求是保留轨迹的连续性天然适合用Canvas逐帧绘制点线。交互维度丰富旋转视角、缩放、调速、调节参数σ、ρ、β都能带来完全不同的轨迹形态。所以这个项目本质上是一个数学可视化 交互式动画的工具型App单页即可承载不需要复杂业务模块。1.2 技术选型Flutter和鸿蒙的碰撞逻辑一开始就敲定要覆盖多端那跨平台框架基本就锁定Flutter或React Native。考虑到后续还要做大量Canvas级自定义绘制、逐帧动画以及Shader效果Flutter的Skia/Impeller渲染管线比RN的JS桥接方案更贴合需求。真正让我下决心的点是Flutter在鸿蒙端的社区方案已经可用不需要走WebView套壳的老路。鸿蒙HarmonyOS NEXT的话官方主推ArkTS但现在Flutter也已经有了针对OpenHarmony和鸿蒙的社区SDK分支flutter_flutter仓库的ohos分支支持直接编译为鸿蒙的hap包。这意味着我可以用一套Dart代码同时产出Android、iOS、Web、鸿蒙四个平台的产物。对个人项目和中小团队来说这是极香的性价比方案。你如果做的是纯鸿蒙应用那肯定首选ArkTS了但凡是要覆盖多端这个前提Flutter的生态成熟度和包体积控制还是更占优。架构上我也没有引入太多重型框架状态管理用的Provider渲染全走CustomPainter。这两个组合在Flutter社区已经被大量验证写起来顺手、调试直观而且恰好热门搜索词里flutter provider 怎么用和flutter组件通信都指向这个方向——一会儿我在讲工程实现的时候会展开它们的具体用法。整条技术链路就是用Dart实现洛伦兹系统的数值积分器RK4。用Provider管理模拟状态、相机视角、粒子属性和播放控制。用CustomPainter每帧重绘投影后的轨迹路径。用鸿蒙Flutter分支打包成hap跑在HarmonyOS设备上。2. 洛伦兹系统的数学内核与数值解法2.1 方程组背后的物理意义洛伦兹系统的标准形式是三个耦合的一阶常微分方程dx/dt σ(y - x)dy/dt x(ρ - z) - ydz/dt xy - βz三个参数各有物理对应σ普朗特数与流体粘性相关ρ瑞利数代表温差驱动的强度β和几何尺度相关。经典取值是σ10ρ28β8/3。这组取值下系统进入混沌状态轨迹在相空间中围绕两个不稳定焦点交替盘旋形成那对蝴蝶翅膀。我不打算写一堆纯数学推导但至少要知道每个项大致在干什么。x方向的变化率由y与x的差值驱动相当于一种反馈耦合y方向同时受x对z的调制和自身衰减影响z方向则由xy的乘积决定增长再线性衰减。z的变化是二次非线性的源头没有这个xy项系统就退化成普通的线性系统永远不可能产生混沌。2.2 为什么要用RK4而不是欧拉法数值积分最naive的方案是显式欧拉法根据当前梯度走一小步。公式是x_{n1} x_n Δt * f(x_n, y_n, z_n)欧拉法实现极其简单但它有一个致命问题对这类非线性混沌系统数值误差会随迭代指数级放大。你算出来的轨迹可能跟真实系统完全不是一回事甚至在某些步长下发散。更关键的是欧拉法有系统性偏差轨迹会漂移长期演化后画出来的吸引子会走形。四阶龙格-库塔法RK4是稳定性和计算复杂度之间的黄金平衡点。它每个步长内采样四次斜率加权平均后得到更精确的增量。误差阶数比欧拉法高出三个量级而每个步长只需要多算三次右侧函数性能完全可接受。RK4的标准公式k1 f(t_n, y_n) k2 f(t_n h/2, y_n h/2 * k1) k3 f(t_n h/2, y_n h/2 * k2) k4 f(t_n h, y_n h * k3) y_{n1} y_n h/6 * (k1 2*k2 2*k3 k4)对于洛伦兹系统h时间步长我取0.01然后每几个步长才渲染一帧。这个取值是在轨迹精度和动画帧率之间磨出来的经验值后面性能部分我会细讲。2.3 Dart实现RK4核心代码用Dart写RK4一点都不绕直接翻译公式就行。注意一定要用double类型用float会在长时间模拟后累积明显误差。洛伦兹系统对初值极其敏感初始条件的微小差异会指数放大浮点精度在这种场景下不是洁癖是实打实的正确性需求。class LorenzSystem { double sigma; double rho; double beta; LorenzSystem({ this.sigma 10.0, this.rho 28.0, this.beta 8.0 / 3.0, }); void derivative(double x, double y, double z, Listdouble output) { output[0] sigma * (y - x); output[1] x * (rho - z) - y; output[2] x * y - beta * z; } void stepRK4(double x, double y, double z, double dt, Listdouble result) { final k1 Listdouble.filled(3, 0); final k2 Listdouble.filled(3, 0); final k3 Listdouble.filled(3, 0); final k4 Listdouble.filled(3, 0); derivative(x, y, z, k1); derivative(x dt / 2 * k1[0], y dt / 2 * k1[1], z dt / 2 * k1[2], k2); derivative(x dt / 2 * k2[0], y dt / 2 * k2[1], z dt / 2 * k2[2], k3); derivative(x dt * k3[0], y dt * k3[1], z dt * k3[2], k4); result[0] x dt / 6 * (k1[0] 2 * k2[0] 2 * k3[0] k4[0]); result[1] y dt / 6 * (k1[1] 2 * k2[1] 2 * k3[1] k4[1]); result[2] z dt / 6 * (k1[2] 2 * k2[2] 2 * k3[2] k4[2]); } }实际调用时我维护一个轨迹列表最开始塞入初始位置通常取(1.0, 1.0, 1.0)附近。模拟循环里每次调用stepRK4把新点追加到队列末尾。跑长之后做滑动窗口截断只保留最近N个点避免内存和绘制压力无限上升。这里有一个容易踩的坑初始值的选择会影响刚开始一段的路径走向。洛伦兹系统在开始时会有一段瞬态没进入吸引子区域之前轨迹可能乱跑。如果你希望画面一开始就在蝴蝶翅膀区域内可以从标准吸引子附近的点启动比如(0.1, 0.1, 0.1)演化一会儿再取末端点当起点效果会干净很多。3. Flutter工程架构与鸿蒙端适配3.1 项目目录分层与模块划分虽然只是一个单页可视化应用我还是把代码分层做了方便后面加功能或者换端。目录结构大致长这样lib/ ├── main.dart # 入口、Provider挂载 ├── models/ │ └── lorenz_system.dart # 洛伦兹数学内核 ├── providers/ │ ├── simulation_provider.dart # 模拟状态管理 │ └── view_provider.dart # 视角与交互状态 ├── painters/ │ ├── trajectory_painter.dart # 轨迹绘制 │ └── color_mapper.dart # 色彩映射 ├── screens/ │ └── home_screen.dart # 主页布局 └── widgets/ ├── control_panel.dart # 参数控制面板 └── attractor_view.dart # 可视化画布models层放纯数学不依赖任何Flutter框架代码——这样单测好写将来如果要用纯ArkTS重写也能直接照搬逻辑。providers层负责衔接数据与UIpainters层只干渲染一件事。这种职责分离在调试手感上差别很大出问题先定位是数学算错了、状态没刷、还是画错了不用在一个类里翻几百行。3.2 Provider状态管理的实际用法Provider在Flutter生态里算是国民级状态管理方案。它解决的核心问题就是组件间共享状态和跨组件通信。回到热搜词里那个经典问题flutter provider 怎么用这里正好借项目讲清楚。我的做法是定义两个ChangeNotifier子类class SimulationProvider extends ChangeNotifier { final LorenzSystem _system LorenzSystem(); final ListOffset3D _trajectory []; bool _isRunning true; double _speed 1.0; int _maxPoints 1500; void step(double dt) { _trajectory.add(next); if (_trajectory.length _maxPoints) { _trajectory.removeRange(0, _trajectory.length - _maxPoints); } notifyListeners(); } void toggleRunning() { _isRunning !_isRunning; notifyListeners(); } void reset() { _trajectory.clear(); notifyListeners(); } }ViewProvider负责相机偏航角yaw、俯仰角pitch、缩放比例scale和是否自动旋转等纯视图状态。分割这两个Provider的原因是它们的更新频率不一样模拟数据是帧率级刷新60fps甚至更高视角状态是交互事件驱动混在同一个ChangeNotifier里会导致不必要的全量重建。Provider还有一个容易被忽略的优点它天然支持局部刷新。在UI里我只在需要监听状态改变的地方套Consumer或Selector画布区域的CustomPaint接收trajectory和视角参数其他控件各看各的不会因为模拟器每帧notify导致整个页面重排。这就是组件通信的Provider式解法——不依赖全局事件总线而是把共享状态提升到Provider层由框架精确派发更新。主入口挂Provider也很简单void main() { runApp( MultiProvider( providers: [ ChangeNotifierProvider(create: (_) SimulationProvider()), ChangeNotifierProvider(create: (_) ViewProvider()), ], child: const HarmonyLorenzApp(), ), ); }有小伙伴可能会问为什么不用Riverpod或者Bloc这种规模的工具类AppProvider的样板代码最少概念也最简单。不想为了状态管理引入一套需要复杂学习成本的框架把精力留给渲染和数学才是正确的取舍。3.3 鸿蒙端适配与hap打包鸿蒙侧的适配是整个项目里坑最密集的部分。社区HarmonyOS对Flutter的支持目前主要依赖OpenHarmony的Flutter SDK分支flutter_flutter的ohos分支。它和标准Flutter SDK的差异在于替换了平台通道层的原生实现走鸿蒙的Ability/API体系。编译产物是hap鸿蒙应用包格式而不是apk或ipa。需要DevEco Studio配合构建不能用Android Studio直接一把梭。基本步骤大概是从仓库拉取ohos分支的Flutter SDK。配置Flutter的FLUTTER_STORAGE_BASE_URL等环境变量指向镜像源。在已有Flutter工程里执行flutter create --platforms ohos .或者手工添加鸿蒙工程骨架。用DevEco Studio打开生成出来的ohos目录写应用签名、配置权限。构建出hap包通过鸿蒙开发工具或者命令行安装到真机。我踩得最深的坑在版本匹配上。Flutter的ohos分支迭代很快某次我升级了Flutter SDK后DevEco工程里的native代码没同步更新结果编译通过、一启动就崩日志里全是符号找不到的问题。后来学乖了每次升级SDK就把ohos目录整个重新生成一次再调整自定义原生代码。还有一点如果你要在鸿蒙App里访问网络或者调用系统API注意鸿蒙的权限声明形式跟Android不一样module.json5里声明而不是AndroidManifest而且部分Android的权限项在鸿蒙上根本不识别需要替换成对应的鸿蒙权限名。我这个项目暂时不需要联网和系统API规避掉了这层麻烦。如果你要接定位、蓝牙这类能力建议先查OpenHarmony API还是HarmonyOS API二者有差异。4. 3D轨迹可视化与手势交互实现4.1 CustomPainter如何实现3D投影CustomPainter是Flutter里最灵活的绘制入口本质上就是给你一个Canvas对象所有绘制都在上面发生。3D图形做投影的核心是把洛伦兹系统的三维坐标(x, y, z)转换成屏幕上的二维坐标(canvasX, canvasY)。我用的方式是最朴素的透视投影加旋转矩阵。先做坐标归一化让吸引子的几何中心落在画布中心再乘以一个缩放因子适配屏幕尺寸。接着把三维点绕着Y轴和X轴旋转旋转角度由ViewProvider里的yaw和pitch决定。最后做一个简单的透视除法离相机远的点收缩产生纵深错觉。旋转矩阵长这样绕Y轴再绕X轴x x * cos(yaw) z * sin(yaw) y x * sin(yaw) * sin(pitch) y * cos(pitch) - z * cos(yaw) * sin(pitch) z -x * sin(yaw) * cos(pitch) y * sin(pitch) z * cos(yaw) * cos(pitch)然后在CustomPainter的paint方法里把轨迹里的每个三维点都走一遍这个变换得到二维列表再起Path绘制。核心代码如下省略了归一化和透视细节class TrajectoryPainter extends CustomPainter { final ListOffset3D trajectory; final double yaw; // 偏航角 final double pitch; // 俯仰角 final double scale; // 缩放 final Color startColor; final Color endColor; override void paint(Canvas canvas, Size size) { if (trajectory.isEmpty) return; final paint Paint() ..style PaintingStyle.stroke ..strokeWidth 1.2 ..strokeCap StrokeCap.round; final centerX size.width / 2; final centerY size.height / 2; for (int i 1; i trajectory.length; i) { final prev _project(trajectory[i - 1], size, centerX, centerY); final curr _project(trajectory[i], size, centerX, centerY); paint.color _colorMapper.colorForIndex(i); // 渐变 canvas.drawLine(prev, curr, paint); } } override bool shouldRepaint(covariant TrajectoryPainter oldDelegate) { return oldDelegate.trajectory ! trajectory || oldDelegate.yaw ! yaw || oldDelegate.pitch ! pitch || oldDelegate.scale ! scale; } }这里逐个画线段而不是把一个完整的polyline一条路径画完是为了让每条线段能获得独立颜色轨迹尾部呈现从蓝到橙的新变效果。代价是绘制指令数量增多对性能有压力。优化方案后面专门讲。4.2 手势识别与视角控制视角交互我用的是GestureDetector里的onPanUpdate和onScaleEnd加上一个简单的自动旋转开关。手势操作里的关键点松手之后如果还想要惯性旋转一般要自己维护一个速度衰减但为了控制体量我这里只做了直接映射。单指拖动同时修改yaw和pitch。横向拖yaw纵向拖pitch。双指缩放用scale属性控制整体缩放限制在0.1到5.0区间防止用户把轨迹拉到视野外面去。双击一键重置视角和缩放回到默认角度。其实还有一个很细节的交互值得做拖动的时候给轨迹边缘加一点动态模糊或者模拟相机曝光效果。但CustomPainter逐帧全量重绘本身成本就高过度设计反而会把帧率拉垮。所以最终保留的是干净利落的旋转缩放把视觉重心完全留给吸引子本身的形态。4.3 颜色渐变的数学映射颜色映射我写了一个ColorMapper类把轨迹点的位置或者索引映射到HSV色相空间避免那种生硬的纯色线。具体做法是根据当前点在轨迹队列里的位置比例0到1让色相从210度蓝渐变到30度橙黄。整体颜色从冷到暖模拟粒子从刚进入轨迹到稳定游走的能量变化。更好的做法是按照局部速度来映射颜色。洛伦兹系统在某些区域运动快外侧大回环某些区域慢围绕焦点盘旋用速度给轨迹着色视觉上能凸显吸引子的层次结构。代价是每次模拟迭代都要额外算速度模长对性能有一丢丢损耗。我最终使用了按速度着色的方案拖尾颜色会有明显的明暗交替非常出效果。颜色映射实现不复杂Color colorForSpeed(double speed, double minSpeed, double maxSpeed) { final t ((speed - minSpeed) / (maxSpeed - minSpeed)).clamp(0.0, 1.0); final hue 240.0 - 60.0 * t; // 从蓝色向黄绿色偏移 return HSVColor.fromAHSV(1.0, hue, 0.85, 1.0).toColor(); }5. 性能优化实战把帧率稳住5.1 理解Flutter的渲染管线与Impeller在做性能优化之前搞清楚Flutter的渲染后端很重要。Flutter在Android和桌面端逐步用Impeller替换了Skia的绘制管线。Impeller预编译着色器、消除首帧卡顿、降低绘制API开销对高频CustomPainter重绘场景是一个实打实的提升。在鸿蒙的ohos分支上渲染后端目前仍然是Skia为主性能特性和Android上不太一样。这意味着同一个项目在Android真机上可能很流畅换到鸿蒙真机就会出现掉帧——并不是代码写错了而是渲染后端不同、驱动适配也有差异。定位性能问题时不能只看逻辑层还要意识到渲染后端的差别。5.2 数据量vs绘制量的平衡洛伦兹轨迹绘制的性能瓶颈几乎不在数学迭代上——RK4迭代几千步在Dart里毫秒级就能跑完——瓶颈全在Canvas绘制上。每条线段一个drawLine调用1500条线段就是1500次绘制指令。在Skia后端这些指令的提交和栅格化是帧率杀手。我的优化策略是降采样绘制轨迹点队列最多保留1500个数学点。绘制时每隔1个点采样一次或者按缩放比例动态决定步长。用户拉近看细节时多画点拉远看全貌时少画点。轨迹不一定要逐帧追加新点时全量重绘。我一个常见的优化是每10个模拟步长再刷新一次UI肉眼几乎觉察不出轨迹的滞后但CPU和GPU压力降一个量级。这一步的取舍逻辑是你的眼睛关注的是轨迹的形状和流畅度而不是单个数学点的位置精度。少画一半点形状几乎不变帧率却能翻倍。5.3 预计算轨迹与动态画法的取舍还有一个更激进的方案在模拟开始前先用后台线程预计算几千步轨迹存进列表然后进入渲染阶段每天只做投影变换和绘制不再实时做RK4迭代。这个方案的优点轨迹长度固定渲染全量性能好预测缺点丢失了实时演化感轨迹不能交互式地生长。我最后做了一个混合模式初始阶段前1000步实时模拟并绘制让用户看到轨迹从初始点逐步卷成蝴蝶形态的过程这个非常有观赏性。之后进入循环模式轨迹已经足够长继续模拟每次追加新点、丢掉旧点轨迹形态保持基本稳定画面呈现动态流动感。参数调节用户一旦改了σ、ρ、β立刻清空轨迹重新进入初始阶段。这个设计保留了趣味性又兼顾了性能到时候如果你复刻这个项目可以重点体会下这个模式的切换逻辑。5.4 渲染隔离与局部重绘Flutter里CustomPainter的repaint范围控制是个容易被忽视的优化点。如果你的画布和参数控制面板在同一个widget里每帧重绘画布的代价会被连带放大控制面板的动画也受影响。我用RepaintBoundary把画布区域包起来并结合CustomPainter的repaint参数传入一个Listenable比如ViewProvider。这样只有当视角或模拟状态变化时画布才重绘其他UI区域完全不受影响。另外shouldRepaint一定要写准确。我之前图省事直接返回true结果每次视图刷新整个painter都要重新执行一遍哪怕数据没变。精准比较trajectory和视角参数后才返回true能让重绘次数大幅降低。6. 常见问题与调试记录6.1 鸿蒙端构建与运行问题下面的表格是我在鸿蒙真机上调试过程中遇到的几个典型问题整理成速查表方便后面直接对照排查。问题现象根因解决方案编译通过启动即崩溃日志报符号找不到Flutter SDK升级后ohos原生工程未重新生成删掉ohos目录重新flutter create --platforms ohos并同步原生层代码连接不上鸿蒙真机driver/adb设备未被识别或鸿蒙调试模式未开启检查鸿蒙开发者模式、USB调试开关用hdc工具替代adbhap安装失败报签名错误DevEco工程未配置应用证书在DevEco里配置调试证书并勾选自动签名画面空白但日志无异常CustomPainter的transform计算出NaN坐标检查归一化时是否除零给缩放因子添加下限保护有一个特别容易忽略的点鸿蒙手机连接电脑不一定支持标准ADB部分版本只支持华为的hdcOpenHarmony Device Connector。你如果非华为电脑连鸿蒙手机大概率会遇到adb devices看不到设备的情况。这时候直接装最新版DevEco Studio自带hdc工具命令行优先用hdc而不是adb。6.2 数值发散与NaN问题调试中我遇到过几次经典问题一是轨迹异常发散点坐标暴涨到天文数字canvas上什么也画不出来。原因是参数被调到了混沌区间之外系统不动点失去稳定性轨迹一路奔向无穷。解决办法在模型层对x、y、z做范围钳制超过一定绝对值就自动重置模拟。二是坐标全是NaN导致Canvas直接罢工。根因是归一化时除以了零——初始轨迹为空时min/max坐标都是零除法直接炸了。代码里加了一个安全判断如果坐标范围小于1e-9就不做归一化直接返回原始值。6.3 状态刷新过频导致的界面卡顿使用Provider时有一个隐藏的性能坑ChangeNotifier的notifyListeners是同步的模拟器每帧调用它时所有监听者都会立刻重建。如果某个Consumer包的范围太大重建成本直接拉满。最初我把整个home_screen放在一个Consumer里监听SimulationProvider改完就卡。后来重构为画布区域单独监听模拟数据和视角数据控制面板的各个滑块单独Selector监听响应的参数。这样滑块拖动时只有局部重建画布该重绘还是重绘互不干扰。这就是组件通信里最小依赖原则的实践——不是所有状态更新都要通知到全局能把更新范围缩小就缩小。6.4 绘制轨迹断线与拖尾不流畅另一个常见的视觉问题是轨迹线段之间出现微小断点。原因有两个相邻点前后帧的投影位置因为浮点精度问题产生抖动。解决投影前把坐标先放大一定倍数比如乘1000做完投影除以1000减少浮点数误差比例。但这个方案治标不治本更好的做法是确保模拟和投影都用double并且不要反复读写全精度坐标。自定义Paint的strokeWidth太细加上缩放缩小后线条看起来断断续续。建议把strokeWidth设置成1.0~1.5之间并根据缩放scale动态调整大于1.0。拖尾不流畅通常不是绘制问题而是模拟步长和帧率不匹配。如果你的模拟速度太快每帧新增几十个点视觉上就变成闪烁的点阵而非连续轨迹。解决方法是限制每帧新增点数量如果本帧积压太多就只新画一部分其余留到下帧。这个限流策略让轨迹生长的动画节奏非常均匀。一些额外的体会这个项目做到后面我对混沌图形在移动端的表现力有了新的理解。奇异吸引子表面看起来复杂内核却是一套确定性方程组一个简单迭代器就能生成无限复杂的图案。这种简单规则产生复杂行为的特质和信息可视化、生成艺术天然契合。接下来如果继续演进我可以往几个方向延展增加双摆系统、Logistic映射等高阶混沌可视化模块加Web端导出截图/动画GIF用Shader做GPU实时粒子路径渲染甚至接一版支持ArkTS与鸿蒙元服务的轻量版本。如果你对数学可视化、生成艺术或者跨平台开发有兴趣这个项目的代码量不大却把数学建模、状态管理、绘制渲染、性能优化、原生适配全链路走了一遍。最难的往往不是某一环的技术攻坚而是把这些环节精密地咬合在一起。好的可视化会让你感觉不到技术栈的分界只剩混沌科学的美感在屏幕上游走。做出来之后每次看到蝴蝶翅膀在手里旋转缩放我都觉得这些坑值得踩。
返回列表