当前位置: 首页 > news >正文

Konva 从入门到实践 - day3

下面是Konva 第 3 天的完整实现。我们在第 2 天可拖拽布局的基础上,增加设备间输送线以及沿输送线流动的动画货物,让 WCS 画面真正“动起来”。


第 3 天目标

  • 在两个设备之间绘制一条输送线(Konva.Line
  • 在输送线上添加一个移动的圆点,模拟货物流动
  • 货物点沿输送线往复运动,并可根据输送线状态改变颜色
  • 拖拽设备时,输送线自动跟随端点移动

完整可运行代码(Day 3)

<!DOCTYPEhtml><htmllang="zh-CN"><head><metacharset="UTF-8"><title>WCS 设备布局 - Day3 连线与流动动画</title><style>body{margin:0;padding:20px;background:#f0f2f5;font-family:sans-serif;}#container{border:1px solid #ccc;background:#fff;width:800px;height:600px;cursor:default;}.info{margin-top:10px;font-size:14px;color:#666;}</style></head><body><h2>仓库设备布局 - 输送线与流动动画</h2><divid="container"></div><divclass="info">拖拽设备时输送线自动跟随。圆点沿输送线流动,模拟货物运输。</div><scriptsrc="https://unpkg.com/konva@9/konva.min.js"></script><script>// ========== 1. 原始设备数据 ==========constlayoutData={layout:[{id:"1782803001807",deviceCode:"stacker",imgName:"ddj",left:480,top:275,width:50,height:40,angle:0,moveLength:200,selected:false},{id:"1782803143726",deviceCode:"conveyor",imgName:"ssx2",left:640,// 稍微调整位置,让两个设备有间距top:275,width:50,height:40,angle:0,moveLength:null,selected:false}]};// ========== 2. 创建画布 ==========conststage=newKonva.Stage({container:'container',width:800,height:600});constlayer=newKonva.Layer();stage.add(layer);// 全局引用letselectionRect=null;letselectedNode=null;constnodeStartPos=newMap();// 输送线相关letconveyorLine=null;// 输送线letcargoDot=null;// 流动圆点letanimation=null;// Konva.Animation 实例letflowDirection=1;// 流动方向:1 正向,-1 反向letflowProgress=0;// 0 ~ 1// ========== 3. 创建选中框 ==========functioncreateSelectionRect(){selectionRect=newKonva.Rect({stroke:'#1e90ff',strokeWidth:2,dash:[4,4],fill:'rgba(30, 144, 255, 0.1)',visible:false,listening:false});layer.add(selectionRect);}createSelectionRect();functionupdateSelectionRect(node){if(!node){selectionRect.visible(false);layer.batchDraw();return;}constbox=node.getClientRect({skipTransform:false});selectionRect.position({x:box.x,y:box.y});selectionRect.size({width:box.width,height:box.height});selectionRect.visible(true);layer.batchDraw();}functionselectNode(node){if(selectedNode===node)return;if(selectedNode){selectedNode.setAttr('selected',false);}selectedNode=node;if(node){node.setAttr('selected',true);updateSelectionRect(node);}else{updateSelectionRect(null);}}stage.on('click',(e)=>{if(e.target===stage)selectNode(null);});// ========== 4. 拖拽处理 ==========functiononDragStart(e){constnode=e.target;nodeStartPos.set(node.id(),{x:node.x(),y:node.y()});}functiononDragMove(e){constnode=e.target;constmoveLength=node.getAttr('moveLength');if(!moveLength)return;conststartPos=nodeStartPos.get(node.id());if(!startPos)return;constdx=node.x()-startPos.x;constdy=node.y()-startPos.y;constdist=Math.sqrt(dx*dx+dy*dy);if(dist>moveLength){constratio=moveLength/dist;node.position({x:startPos.x+dx*ratio,y:startPos.y+dy*ratio});}}functiononDragEnd(e){constnode=e.target;nodeStartPos.delete(node.id());if(selectedNode===node)updateSelectionRect(node);// 拖拽结束后更新输送线端点updateConveyorLine();}functionbindEvents(node){node.on('click',(e)=>{e.evt.stopPropagation();selectNode(node);});node.on('dragstart',onDragStart);node.on('dragmove',onDragMove);node.on('dragend',onDragEnd);}// ========== 5. 创建节点(同Day2) ==========functioncreateDeviceNode(device){returnnewPromise((resolve)=>{constimg=newwindow.Image();img.onload=()=>{constnode=newKonva.Image({id:device.id,image:img,x:device.left,y:device.top,width:device.width,height:device.height,rotation:device.angle,draggable:true,deviceCode:device.deviceCode,moveLength:device.moveLength,selected:false});resolve(node);};img.onerror=()=>{constnode=newKonva.Rect({id:device.id,x:device.left,y:device.top,width:device.width,height:device.height,fill:'#cccccc',stroke:'#333',strokeWidth:1,rotation:device.angle,draggable:true,deviceCode:device.deviceCode,moveLength:device.moveLength,selected:false});resolve(node);};img.src=`images/${device.imgName}.png`;});}// ========== 6. 输送线与动画 ==========// 获取设备节点的输出/输入连接点(这里取设备右边缘/左边缘中点)functiongetDeviceConnectPoints(){conststacker=stage.findOne('#1782803001807');constconveyor=stage.findOne('#1782803143726');if(!stacker||!conveyor)returnnull;conststackerBox=stacker.getClientRect({skipTransform:false});constconveyorBox=conveyor.getClientRect({skipTransform:false});// 堆垛机右边缘中点 -> 输送线左边缘中点return{startX:stackerBox.x+stackerBox.width,startY:stackerBox.y+stackerBox.height/2,endX:conveyorBox.x,endY:conveyorBox.y+conveyorBox.height/2};}functioncreateConveyorLine(){conveyorLine=newKonva.Line({stroke:'#2ecc71',strokeWidth:4,lineCap:'round',lineJoin:'round',points:[0,0,0,0],// 初始占位listening:false});layer.add(conveyorLine);// 流动货物圆点cargoDot=newKonva.Circle({radius:6,fill:'#e67e22',stroke:'#fff',strokeWidth:2,x:0,y:0,listening:false});layer.add(cargoDot);}// 根据端点更新输送线路径functionupdateConveyorLine(){if(!conveyorLine)return;constpoints=getDeviceConnectPoints();if(!points)return;conveyorLine.points([points.startX,points.startY,points.endX,points.endY]);layer.batchDraw();}// 启动流动动画functionstartFlowAnimation(){if(animation)return;animation=newKonva.Animation(()=>{if(!cargoDot||!conveyorLine)return;constpoints=getDeviceConnectPoints();if(!points)return;// 更新进度,0~1 往复constspeed=0.008;flowProgress+=speed*flowDirection;if(flowProgress>=1){flowProgress=1;flowDirection=-1;}elseif(flowProgress<=0){flowProgress=0;flowDirection=1;}// 线性插值计算当前位置constcx=points.startX+(points.endX-points.startX)*flowProgress;constcy=points.startY+(points.endY-points.startY)*flowProgress;cargoDot.position({x:cx,y:cy});layer.batchDraw();});animation.start();}functionstopFlowAnimation(){if(animation){animation.stop();animation=null;}}// ========== 7. 主渲染流程 ==========asyncfunctionrenderLayout(){constnodes=awaitPromise.all(layoutData.layout.map(device=>createDeviceNode(device)));nodes.forEach(node=>{bindEvents(node);layer.add(node);});// 创建输送线和货物点createConveyorLine();updateConveyorLine();// 启动流动动画startFlowAnimation();layer.batchDraw();console.log('Day3 就绪:输送线可随设备移动,货物点往复流动。');}renderLayout();</script></body></html>

关键实现解析

1. 输送线的绘制

  • 使用Konva.Line,将两点连成线段。
  • 连接点取设备包围盒的边缘中点:堆垛机右边缘中点 → 输送线左边缘中点。
  • 通过getDeviceConnectPoints()实时获取两个设备当前的包围盒,计算出端点坐标。
  • 当设备拖拽时(dragend),调用updateConveyorLine()更新线的坐标。

2. 流动动画

  • 创建一个Konva.Circle作为货物点。
  • 使用Konva.Animation驱动,每帧根据进度 (flowProgress) 沿输送线插值位置。
  • 进度在 0 和 1 之间往复变化,通过flowDirection控制方向,模拟货物来回运输。
  • 动画速度通过speed = 0.008控制,可按需调整。

3. 性能与更新

  • 将输送线和货物点都设为listening: false,避免干扰拖拽和点击。
  • dragend事件中更新输送线端点,保证实时跟随。
  • 动画帧内仅更新货物点位置并调用layer.batchDraw(),效率足够。

4. 状态样式(可扩展)

  • 当前默认输送线为绿色 (#2ecc71),货物点为橙色。
  • 你可以在后续根据状态改变颜色,例如:
    conveyorLine.stroke('#e74c3c');// 故障时变红cargoDot.fill('#ff0000');

测试步骤

  1. 确保 Day2 的图片仍在images文件夹(或占位矩形自动工作)。
  2. 打开页面,你会看到两个设备之间多了一条绿色线段。
  3. 一个橙色圆点沿输送线来回移动。
  4. 拖拽堆垛机或输送线,输送线会随设备端点实时更新,圆点运动路径也会同步变化。

第 3 天总结

你已经学会了:

  • 动态绘制Konva.Line并响应节点移动自动更新
  • 使用Konva.Animation创建逐帧动画
  • 线性插值实现沿路径运动
  • 动画速度与方向控制

现在画面已经具备基本的“动态监控”感。明天第 4 天我们将实现状态驱动的视觉变化(颜色、闪烁、文本),让设备能够响应后端数据报警。
如果运行中有任何问题或想调整动画样式,随时告诉我。

http://www.gsyq.cn/news/1623069.html

相关文章:

  • Expo:用 React 写一次代码,Android、iOS、网页全搞定
  • 半导体测试全流程详解:从CP到FT再到SLT,芯片出厂的最后一道关
  • Hatari:Atari ST/STE/TT/Falcon 模拟器,下载体验与功能操作揭秘
  • 封装工艺解析:芯片穿上的最后一件衣服,决定了性能与寿命
  • MAC地址详解:网络设备的身份证,唯一性背后的逻辑
  • 2026免费AI抠图工具完整指南:电脑手机网页离线软件汇总
  • Fable助力打造音乐可视化工具Waveloop:呈现独特音乐结构,代码与视频皆有亮点
  • 3行代码搞定页面截图,Bun.WebView真的简单
  • 15个VTube Studio插件开发工具:从零开始打造虚拟主播互动体验
  • CentOS 7.9 64位 PostgreSQL安装和配置指南
  • 2026多端AI抠图工具指南:免费付费网页电脑手机软件实操教程
  • 10分钟搞定Joy-Con手柄连接电脑:从蓝牙配到游戏畅玩的完整方案
  • StepCI:统一API测试框架,高效覆盖HTTP与GraphQL协议
  • 【鸿蒙ArkTS】极简登录注册页面+页面跳转+密码校验
  • 2026Word文件压缩至10M完整实操指南,含官方步骤、图片瘦身与清理隐藏内容技巧
  • Claude全方位揭秘:多产品特性、科研支持及常见问题解答
  • codex连接过程中遇到各种报错如何解决(持续更新中)
  • Anthropic 推出测试版 Claude Science:打造面向科学家的 AI 工作台
  • 图吧工具箱
  • 杰理之搭配3in1 dongle1.13.0出现lea连接异【篇】
  • 8051内部结构
  • 1688拍立淘图片搜索API完整文档
  • 10分钟快速搞定Joy-Con手柄连接电脑:终极配置指南
  • Arkime网络流量解密实战:解密TLS流量,提升安全监控与故障排查效率
  • 2026年SD-WAN演进:企业网络架构的下一站选择
  • 100G交换机吞吐下降20%——一次DPDK Hash Cache Locality优化实战(下)
  • 第08章:Docker 数据持久化
  • Selenium ActionChains 实战指南:从原理到高级交互自动化
  • 鸿蒙 ArkTS 最全完整版知识点总结
  • 2026 年干细胞存储怎么选?四家机构服务与技术全景解析