Konva 从入门到实践 - day3
下面是Konva 第 3 天的完整实现。我们在第 2 天可拖拽布局的基础上增加设备间输送线以及沿输送线流动的动画货物让 WCS 画面真正“动起来”。第 3 天目标在两个设备之间绘制一条输送线Konva.Line在输送线上添加一个移动的圆点模拟货物流动货物点沿输送线往复运动并可根据输送线状态改变颜色拖拽设备时输送线自动跟随端点移动完整可运行代码Day 3!DOCTYPEhtmlhtmllangzh-CNheadmetacharsetUTF-8titleWCS 设备布局 - Day3 连线与流动动画/titlestylebody{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/headbodyh2仓库设备布局 - 输送线与流动动画/h2dividcontainer/divdivclassinfo拖拽设备时输送线自动跟随。圆点沿输送线流动模拟货物运输。/divscriptsrchttps://unpkg.com/konva9/konva.min.js/scriptscript// 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. 创建画布 conststagenewKonva.Stage({container:container,width:800,height:600});constlayernewKonva.Layer();stage.add(layer);// 全局引用letselectionRectnull;letselectedNodenull;constnodeStartPosnewMap();// 输送线相关letconveyorLinenull;// 输送线letcargoDotnull;// 流动圆点letanimationnull;// Konva.Animation 实例letflowDirection1;// 流动方向1 正向-1 反向letflowProgress0;// 0 ~ 1// 3. 创建选中框 functioncreateSelectionRect(){selectionRectnewKonva.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;}constboxnode.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(selectedNodenode)return;if(selectedNode){selectedNode.setAttr(selected,false);}selectedNodenode;if(node){node.setAttr(selected,true);updateSelectionRect(node);}else{updateSelectionRect(null);}}stage.on(click,(e){if(e.targetstage)selectNode(null);});// 4. 拖拽处理 functiononDragStart(e){constnodee.target;nodeStartPos.set(node.id(),{x:node.x(),y:node.y()});}functiononDragMove(e){constnodee.target;constmoveLengthnode.getAttr(moveLength);if(!moveLength)return;conststartPosnodeStartPos.get(node.id());if(!startPos)return;constdxnode.x()-startPos.x;constdynode.y()-startPos.y;constdistMath.sqrt(dx*dxdy*dy);if(distmoveLength){constratiomoveLength/dist;node.position({x:startPos.xdx*ratio,y:startPos.ydy*ratio});}}functiononDragEnd(e){constnodee.target;nodeStartPos.delete(node.id());if(selectedNodenode)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){constimgnewwindow.Image();img.onload(){constnodenewKonva.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(){constnodenewKonva.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.srcimages/${device.imgName}.png;});}// 6. 输送线与动画 // 获取设备节点的输出/输入连接点这里取设备右边缘/左边缘中点functiongetDeviceConnectPoints(){conststackerstage.findOne(#1782803001807);constconveyorstage.findOne(#1782803143726);if(!stacker||!conveyor)returnnull;conststackerBoxstacker.getClientRect({skipTransform:false});constconveyorBoxconveyor.getClientRect({skipTransform:false});// 堆垛机右边缘中点 - 输送线左边缘中点return{startX:stackerBox.xstackerBox.width,startY:stackerBox.ystackerBox.height/2,endX:conveyorBox.x,endY:conveyorBox.yconveyorBox.height/2};}functioncreateConveyorLine(){conveyorLinenewKonva.Line({stroke:#2ecc71,strokeWidth:4,lineCap:round,lineJoin:round,points:[0,0,0,0],// 初始占位listening:false});layer.add(conveyorLine);// 流动货物圆点cargoDotnewKonva.Circle({radius:6,fill:#e67e22,stroke:#fff,strokeWidth:2,x:0,y:0,listening:false});layer.add(cargoDot);}// 根据端点更新输送线路径functionupdateConveyorLine(){if(!conveyorLine)return;constpointsgetDeviceConnectPoints();if(!points)return;conveyorLine.points([points.startX,points.startY,points.endX,points.endY]);layer.batchDraw();}// 启动流动动画functionstartFlowAnimation(){if(animation)return;animationnewKonva.Animation((){if(!cargoDot||!conveyorLine)return;constpointsgetDeviceConnectPoints();if(!points)return;// 更新进度0~1 往复constspeed0.008;flowProgressspeed*flowDirection;if(flowProgress1){flowProgress1;flowDirection-1;}elseif(flowProgress0){flowProgress0;flowDirection1;}// 线性插值计算当前位置constcxpoints.startX(points.endX-points.startX)*flowProgress;constcypoints.startY(points.endY-points.startY)*flowProgress;cargoDot.position({x:cx,y:cy});layer.batchDraw();});animation.start();}functionstopFlowAnimation(){if(animation){animation.stop();animationnull;}}// 7. 主渲染流程 asyncfunctionrenderLayout(){constnodesawaitPromise.all(layoutData.layout.map(devicecreateDeviceNode(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);测试步骤确保 Day2 的图片仍在images文件夹或占位矩形自动工作。打开页面你会看到两个设备之间多了一条绿色线段。一个橙色圆点沿输送线来回移动。拖拽堆垛机或输送线输送线会随设备端点实时更新圆点运动路径也会同步变化。第 3 天总结你已经学会了动态绘制Konva.Line并响应节点移动自动更新使用Konva.Animation创建逐帧动画线性插值实现沿路径运动动画速度与方向控制现在画面已经具备基本的“动态监控”感。明天第 4 天我们将实现状态驱动的视觉变化颜色、闪烁、文本让设备能够响应后端数据报警。如果运行中有任何问题或想调整动画样式随时告诉我。