Konva 从入门到实践 - day2

Konva 从入门到实践 - day2 下面是我们Konva 第 2 天的完整步骤。在第 1 天静态渲染的基础上我们将增加拖拽、移动范围限制、选中高亮三个核心交互让布局开始具备“编辑”能力。第 2 天目标每个设备可以被拖拽移动堆垛机moveLength 200只能在水平或垂直方向移动且移动总距离不超过 200 像素点击设备时显示蓝色选中框点击空白区域取消选中选中后显示Transformer可选便于缩放/旋转但我们先聚焦移动限制完整可运行代码Day 2!DOCTYPEhtmlhtmllangzh-CNheadmetacharsetUTF-8titleWCS 设备布局 - Day2 拖拽与选中/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拖拽设备试试。堆垛机只能在竖直/水平方向移动且总路程不超过 200px。点击设备可选中蓝色框。/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,// 堆垛机最大移动距离plcMax:null,plcMin:null,selected:false},{id:1782803143726,deviceCode:conveyor,imgName:ssx2,left:540,top:240,width:50,height:40,angle:0,moveLength:null,// 输送线不限制移动plcMax:null,plcMin:null,selected:false}]};// 2. 创建画布 conststagenewKonva.Stage({container:container,width:800,height:600});constlayernewKonva.Layer();stage.add(layer);// 用于存放选中框的引用letselectionRectnull;// 当前选中的设备节点letselectedNodenull;// 记录每个设备拖拽开始时的原始位置用于计算移动距离constnodeStartPosnewMap();// key: id, value: {x, y}// 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();// 4. 更新选中框的位置和大小 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();}// 5. 选中/取消选中逻辑 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);}});// 6. 辅助函数根据配置创建节点 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;});}// 7. 拖拽开始记录初始位置 functiononDragStart(e){constnodee.target;// 记录拖拽前的原始位置用于限制移动距离nodeStartPos.set(node.id(),{x:node.x(),y:node.y()});}// 8. 拖拽移动实时限制位置 functiononDragMove(e){constnodee.target;constmoveLengthnode.getAttr(moveLength);// 可能为 null// 如果 moveLength 为 null不限制移动if(moveLengthnull)return;conststartPosnodeStartPos.get(node.id());if(!startPos)return;constnewXnode.x();constnewYnode.y();// 计算位移差constdxnewX-startPos.x;constdynewY-startPos.y;// 限制最大移动距离欧几里得距离constdistMath.sqrt(dx*dxdy*dy);if(distmoveLength){// 将位移缩放到允许的最大长度constratiomoveLength/dist;constclampedDxdx*ratio;constclampedDydy*ratio;node.position({x:startPos.xclampedDx,y:startPos.yclampedDy});}// 可选也可以分别限制 X 和 Y 方向根据需求修改。// 这里我们使用距离限制同时也可以强制只在水平或垂直方向移动堆垛机通常单向// 如果你希望堆垛机只能在 X 方向移动比如沿巷道可以这样// if (node.getAttr(deviceCode) stacker) {// node.y(startPos.y); // 锁死 Y 轴// }}// 9. 拖拽结束清理记录更新选中框 functiononDragEnd(e){constnodee.target;nodeStartPos.delete(node.id());// 如果当前选中的正是这个节点更新选中框位置if(selectedNodenode){updateSelectionRect(node);}}// 10. 绑定事件到每个节点 functionbindEvents(node){node.on(click,(e){// 阻止事件冒泡避免触发 stage 的 click取消选中e.evt.stopPropagation();selectNode(node);});node.on(dragstart,onDragStart);node.on(dragmove,onDragMove);node.on(dragend,onDragEnd);}// 11. 主渲染流程 asyncfunctionrenderLayout(){constnodesawaitPromise.all(layoutData.layout.map(devicecreateDeviceNode(device)));nodes.forEach(node{bindEvents(node);layer.add(node);});layer.batchDraw();console.log(交互已就绪。可拖拽设备点击选中。);}renderLayout();/script/body/html步骤详解1. 开启拖拽在创建节点时设置draggable: trueKonva 会自动处理基本的鼠标/触摸拖拽。2. 移动范围限制关键思路用dragstart记录节点起始坐标(startX, startY)在dragmove中计算当前位移若moveLength有值则限制移动距离不超过该值。const dist Math.sqrt(dx*dx dy*dy)计算欧几里得距离若超限按比例缩放位移clampedDx dx * (maxDist/dist)堆垛机方向锁定可选如果实际场景中堆垛机只能沿巷道水平或垂直移动你可以像注释里那样在dragmove中强制node.y(startPos.y)或node.x(startPos.x)。这里先用距离限制你可以根据实际需求调整。3. 选中与高亮点击节点时调用selectNode(node)同时停止事件冒泡防止触碰到 Stage 的取消选中。点击空白区域e.target stage取消选中。选中框使用一个半透明蓝色虚线矩形 (selectionRect)根据node.getClientRect()计算包围盒自动适应缩放/旋转后的范围。设置为listening: false避免干扰拖拽。4. 选中框跟随缩放/旋转getClientRect({ skipTransform: false })返回的是节点经过所有变换缩放、旋转等后的外接矩形因此即使设备旋转了选中框也能正确包裹。测试步骤确保images文件夹下有ddj.png和ssx2.png或使用占位矩形。打开 HTML 页面你会看到两个设备。拖拽输送线没有moveLength限制它可以自由移动。拖拽堆垛机它的移动距离会被限制在以起点为圆心、半径 200px 的圆内距离限制。如果你注释了距离限制并开启方向锁定它就只能水平或垂直移动。点击设备出现蓝色虚线框点击空白处取消。第 2 天总结你已经学会了使用draggable开启拖拽通过dragstart/dragmove/dragend事件控制移动行为根据业务属性moveLength动态约束移动范围实现自定义选中高亮正确处理事件冒泡明天第 3 天我们将加入连线与流动动画让输送线“活”起来。如果今天代码中有任何报错或想调整限制规则随时告诉我。