源生JS、canvas画图,支持拖拽
2020-02-16 17:56
579 查看
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>工位图</title> </head> <style> * { margin: 0; padding: 0; } body { height: 100%; width: 100%; } #menu { width: 120px; text-align: center; position: absolute; } #menu ul { list-style: none; border-bottom: 1px solid #000000; border-left: 1px solid #000000; border-right: 1px solid #000000; } #menu ul li { border-top: 1px solid #000000; } #menu ul li:hover { background-color: #00a78e; color: #ffffff; cursor: pointer; } #addWorkstation, #addRoom { border: 1px solid #000000; width: 300px; height: 150px; display: none; position: absolute; background-color: #ffffff; } .ctrlClick { cursor: move; } </style> <body onselectstart="return false;"> <div id="workstationDiv"> <div style="display: none;left: 200px;top: 100px;" id="menu"> <ul> <li _id="#addWorkstation">添加工位</li> <li _id="#addRoom">添加房间</li> </ul> </div> <div id="addWorkstation"> <form id="workstationForm"> 宽: <input type="text" name="width" style="width: 50px" value="40"/> 高: <input type="text" style="width: 50px" value="25" name="height"/> 字大小: <input type="text" name="fontSize" value="12" style="width: 50px;"/><br /> 座位数: <input type="text" name="seat" value="3" style="width: 50px"/> 座位号: <input style="width: 50px" name="text" value="座位号"/><br /> 横: <input type="radio" name="position" value="0" checked/>竖: <input type="radio" name="position" value="1"/><br /> 单排: <input type="radio" name="row" value="1"/>双排: <input type="radio" name="row" value="2" checked/><br /> <!--方向: <span>左: <input type="radio" name="direction" value="left" checked/>右: <input type="radio" name="direction" value="right"/><br /></span>--> <span>上: <input type="radio" name="direction" value="top" checked/>下: <input type="radio" name="direction" value="bottom"/><br /></span> <input type="button" value="确定" id="clickWorkstation"/> </form> </div> <div id="addRoom"> <form id="roomForm"> 房间号: <input type="text" name="text" value="房间号"/><br /> 宽: <input type="text" style="width: 50px" name="width" value="150"/>高: <input type="text" style="width: 50px" name="height" value="150"/>字大小: <input type="text" name="fontSize" value="16" style="width: 50px;"/> <input type="button" value="确定" id="clickRoom"/> </form> </div> </div> </body> <script src="index.js"></script> </html>
(function(){ let _Workstation = function(){}; let work = window.Workstation = new _Workstation(), workstationDiv = document.getElementById("workstationDiv"), nodeArray = {},//所有未初始渲染前所有节点数据 dataNode = {}, //保存渲染后的所有实际节点数据 selectNodeId = '', //当前选中节点ID selectNode = {}, //当前选中节点 dragging = false, //是否选中节点, workstationSpace = 3, //座位的左右间距 seatR = 7, //座位圆半径 dragHoldX, dragHoldY, that = null; work.fn = window.Workstation.fn = _Workstation.prototype; /** * 初始化 */ work.fn.init = function(){ let canvasConfig = { id: 'workstation', width: 1000, height: 600, style: 'border: 1px solid #000000;' }, _this = that = this; let canvas = _this.canvas = _this.createElement("canvas", canvasConfig, workstationDiv); //生成canvas this.ctx = canvas.getContext("2d"); this.w = canvasConfig.width; this.h = canvasConfig.height; //阻止浏览器默认事件 document.oncontextmenu = function( evt ){ evt.preventDefault(); }; //canvas绑定点击事件 canvas.on('mousedown', function(evt){ let menu = workstationDiv.get("#menu"), addWorkstation = workstationDiv.get("#addWorkstation"), addRoom = workstationDiv.get('#addRoom'); addRoom.style.display = "none"; addWorkstation.style.display = "none"; if( evt.button == 2 ){//鼠标右击事件 menu.style.display = "block"; menu.style.left = evt.clientX + "px"; menu.style.top = evt.clientY + "px"; }else if( evt.button == 0 ){ //鼠标点击事件 menu.style.display = "none"; } menu = null; addRoom = null; addWorkstation = null; }); workstationDiv.get("#menu").on( 'click', 'li', function( evt ){ let _id = this.getAttribute('_id'), element = workstationDiv.get(_id); workstationDiv.get("#menu").style.display = "none"; element.style.display = "block"; element.style.left = evt.clientX + "px"; element.style.top = evt.clientY + "px"; _id = null; element = null; }); workstationDiv.get('#clickWorkstation').on('click', function(evt){ let data = this.parentNode.serialize(), //获取表单数据 key = _this.guid(); var mouseSite = _this.mouseSite( evt ), x = mouseSite.mouseX - data.width / 2, y = mouseSite.mouseY - data.height / 2; data['fontSize'] = data['fontSize'] || 10; data['font'] = data['fontSize'] + "px Arial"; nodeArray[key] = {...{ type: 'workstation', key: key, x: x, y: y }, ...data}; _this.drawScreen(); data = null; key = null; mouseSite = null; workstationDiv.get('#addWorkstation').style.display = "none"; }); workstationDiv.get('#clickRoom').on('click', function(evt){ let data = this.parentNode.serialize(),//获取表单数据 key = _this.guid(); var mouseSite = _this.mouseSite( evt ), x = mouseSite.mouseX - data.width / 2, y = mouseSite.mouseY - data.height / 2; data['fontSize'] = data['fontSize'] || 16; data['font'] = data['fontSize'] + "px Arial"; nodeArray[key] = {...{ type: 'room', key: key, x: x, y: y }, ...data}; _this.drawScreen(); data = null; key = null; mouseSite = null; workstationDiv.get('#addRoom').style.display = "none"; }); this.canvas.on("mousedown", this.mouseDownListener); }; /** * 获取鼠标在canvas上位置 */ work.fn.mouseSite = function( evt ){ let bRect = this.canvas.getBoundingClientRect(), mouseX = evt.clientX - bRect.left, mouseY = evt.clientY - bRect.top; return { mouseX: mouseX, mouseY: mouseY } }; /** * 绘制数据节点 */ work.fn.drawScreen = function(){ //清除画布 this.ctx.clearRect(0, 0, this.w, this.h); //在canvas上渲染节点 for(let key in nodeArray){ if(nodeArray.hasOwnProperty(key)){ let item = nodeArray[key]; this[item['type'] + 'Draw'](item); } } }; /** * 绘制房间节点 * @param data */ work.fn.roomDraw = function( data ){ let pKey = data['key']; data = this.calculateCoord(data); data['pKey'] = pKey; dataNode[pKey + "_" + pKey] = data; this.drawBase( data ); }; /** * 绘制工位节点 * @param data */ work.fn.workstationDraw = function( data ){ if(!data['textX'] || !data['textY']){ data = this.calculateCoord(data); } let seat = data['seat'] || 1, //每排座位数 row = data['row'] || 1, // 单双排 pKey = data['key']; for(let j = 0;j < row;j++){ //计算单双排 let item = {...data}, y = data['y'], seatData = {}, //座位圆坐标 width = data['width'], height = data['height']; y = height * j + y + (j * workstationSpace); //计算每排座位的 Y 坐标 item['y'] = y; if((row == 2 && j == 0) || (row == 1 && data['direction'] == 'top')){ seatData['y'] = y - seatR - 5; }else if( row == 2 && j == 1 || (row == 1 && data['direction'] == 'bottom')){ seatData['y'] = +height + y + seatR + 5; }else{ throw "参数错误: row = " + row; } for(let i = 0;i < seat;i++){ //计算座位数 let x = data['x'], key = this.guid( pKey ); x = width * i + x + (i * workstationSpace); //计算每个座位的 X 坐标 item['x'] = x; item['pKey'] = pKey; item['key'] = key; dataNode[pKey + "_" + key] = item; this.drawBase( item ); seatData['x'] = width / 2 + x; this.drawSeat(seatData); } } }; /** * 绘制座位 * @param data */ work.fn.drawSeat = function( data ){ this.ctx.beginPath(); this.ctx.arc(data['x'], data['y'], seatR, 0, 2*Math.PI); this.ctx.stroke(); }; /** * 绘制节点基础方法 * @param data */ work.fn.drawBase = function( data ){ this.ctx.fillStyle = '#000000'; this.ctx.strokeRect(data['x'], data['y'], data.width, data.height); this.ctx.font = data['font']; this.ctx.fillText(data['text'], +data['x'] + +data['textX'], +data['y'] + +data['textY'] - 3); }; /** * 计算文字在节点中的居中位置 * @param data * @returns {*} */ work.fn.calculateCoord = function( data ){ let text = data['text']; //计算文本在节点中的相对位置(居中位置) //计算文本居中位置后,缓存坐标,避免重复计算导致浏览器重复重拍,影响性能 let textWidthH = this.getTextWidthH(text, data['font']); data['textX'] = (data.width - textWidthH['width']) / 2; data['textY'] = +textWidthH['height'] + ((data.height - textWidthH['height']) / 2); return data; }; /** * 判断是否选中节点对象 * @param shape * @param mx * @param my * @returns {boolean} */ work.fn.hitTest = function( evt ) { let mouseSite = this.mouseSite( evt ); for (let key in nodeArray) { if(nodeArray.hasOwnProperty(key)) { let shape = nodeArray[key]; //判断点击的节点 if (mouseSite.mouseX > shape.x && mouseSite.mouseX < (shape.x + +shape.width) && mouseSite.mouseY > shape.y && mouseSite.mouseY < (shape.y + +shape.height)) { dragging = true; //判断鼠标是否保持在节点上 dragHoldX = mouseSite.mouseX - shape.x; dragHoldY = mouseSite.mouseY - shape.y; return key; } } } return false; }; /** * 鼠标按下事件 * @param evt * @returns {boolean} */ work.fn.mouseDownListener = function(evt) { if(!evt.ctrlKey){ that.canvas.off("mousemove", that.mouseMoveListener); that.canvas.off("mouseup", that.mouseUpListener ); return false; } if( selectNodeId = that.hitTest( evt) ){ let className = that.canvas.getAttribute('class') || ''; if(className.indexOf('ctrlClick') < 0){ that.canvas.className += ' ctrlClick'; } selectNode = nodeArray[selectNodeId]; that.canvas.on("mousemove", that.mouseMoveListener ); } that.canvas.off("mousedown", that.mouseDownListener); that.canvas.on("mouseup", that.mouseUpListener ); return false; }; /** * 鼠标释放事件 * @param evt */ work.fn.mouseUpListener = function(evt) { that.canvas.on("mousedown", that.mouseDownListener); that.canvas.off("mouseup", that.mouseUpListener); let className = that.canvas.getAttribute("class"); className && (that.canvas.className = className.replace(" ctrlClick","")); if (dragging) { dragging = false; that.canvas.off("mousemove", that.mouseMoveListener); } return false; }; /** * 鼠标移动事件 * @param evt */ work.fn.mouseMoveListener = function(evt) { if(dragging){ let posX, posY, minX = 0, maxX = that.w - selectNode.width, minY = 0, maxY = that.h - selectNode.height, mouseSite = that.mouseSite( evt );//获取鼠标点击坐标 //防止拖动到画布外面 posX = mouseSite.mouseX - dragHoldX; posX = (posX < minX) ? minX : ((posX > maxX) ? maxX : posX); posY = mouseSite.mouseY - dragHoldY; posY = (posY < minY) ? minY : ((posY > maxY) ? maxY : posY); nodeArray[selectNodeId].x = posX; nodeArray[selectNodeId].y = posY; that.drawScreen(); } }; /** * 获取UUID * @returns {string} */ work.fn.guid = function( pKey ){ var uid = 'xxxxxxxxxxxx4xxxyxxxxxxxxxxxxxxx'.replace(/[xy]/g,function(c){ var r=Math.random()*16|0,v=c=='x' ? r:(r&0x3|0x8); return v.toString(16).toLocaleUpperCase(); }); if( (pKey && dataNode[pKey + "_" + uid]) || nodeArray[uid] ){ console.log("--- uid存在,重新生成 ---"); this.guid(); } return uid; }; /** * 创建DOM元素 * @param elementName * @param attribute * @param child * @returns {*} */ work.fn.createElement = function(elementName, attribute, child){ if(!elementName){ throw "缺少创建DOM"; } let element = document.createElement(elementName); //创建DOM元素 if(attribute){ try{ for (let item in attribute){ element[item] = attribute[item]; } }catch (e) { throw e; } } if(child){ child.appendChild(element) } return element; }; /** * 获取文本真实长度(所占像素大小) */ work.fn.getTextWidthH = function(text, font){ let json = { width: 0, height: 0, }; if(!text){ return json; } let span = document.createElement('span'), style = 'position:absolute;color: #ffffff;'; if(font){ style += 'font: '+ font; } span.innerText = text; span.style = style; span.className = '_getTextWidthH'; document.querySelector('body').appendChild(span); span = document.querySelector('._getTextWidthH'); json['width'] = span.offsetWidth; json['height'] = span.offsetHeight; span.remove(); return json; }; /** * 事件委托 * @param eventName 事件名称 * @param selector 需要绑定事件的元素 * @param callback 绑定事件处理方法 */ let bindList = {}; //绑定事件列表 HTMLElement.prototype.on = function(eventName, selector, callback){ if(!eventName || !selector){ throw "eventName不能为空"; } let _selector = selector; eventName = eventName.toLowerCase(); if(typeof selector == 'function'){ callback = selector; _selector = selector = null; } let func = function (event) { let e = event || window.event, target = e.target || e.srcElement, targets = ''; if(selector){ //事件委托 targets = selector.split(','); for(let i = 0, len = targets.length; i < len; i++){ let item = targets[i].trim(), elements = this.get(item),//获取所有指定子元素 elementLen = elements.length; if(elementLen > 0){ //事件委托 for(let j = 0; j < elementLen; j++){ if (target === elements[j]) { callback.apply(target, [e]); break; } } } } }else{ //当前元素绑定事件 callback.apply(this, [e]); } }; if(!_selector){ _selector = this.getAttribute("id"); } if (!bindList[_selector]) { bindList[_selector] = {}; } if (!bindList[_selector][eventName]) { bindList[_selector][eventName] = {}; } this.addEventListener(eventName, func, false); bindList[_selector][eventName][callback] = func; }; /** * 移除事件 * @param eventName * @param selector * @returns {boolean} */ HTMLElement.prototype.off = function(eventName, selector, callback) { if(typeof selector == 'function'){ callback = selector; selector = this.getAttribute("id"); } if (!eventName) { throw "eventName不能为空"; } eventName = eventName.toLowerCase(); let fnNew = bindList[selector][eventName] ? bindList[selector][eventName][callback] : null; if (!fnNew) { return false; } this.removeEventListener(eventName, fnNew, false); bindList[selector][eventName][callback] = null; }; /** * 获取指定子元素 * @param selector * @returns {*} */ HTMLElement.prototype.get = function( selector ){ if(!selector){ throw 'selector 不能为空'; } let elements = this.querySelectorAll(selector); return elements.length == 1 ? elements[0] : elements; }; /** * 表单序列化 */ Object.prototype.serialize = function(){ var res = {}, //存放序列化后JSON对象 current = null, //当前循环内的表单控件 i, //表单NodeList的索引 len, //表单NodeList的长度 k, //select遍历索引 optionLen, //select遍历索引 option, //select循环体内option optionValue, //select的value form = this; //用form变量拿到当前的表单,易于辨识 for(i=0, len=form.elements.length; i<len; i++){ current = form.elements[i]; if(current.disabled) continue; switch(current.type){ //可忽略控件处理 case "file": //文件输入类型 case "submit": //提交按钮 case "button": //一般按钮 case "image": //图像形式的提交按钮 case "reset": //重置按钮 case undefined: //未定义 break; //单选,复选框 case "radio": case "checkbox": if(!current.checked) break; default: //一般表单控件处理 if(current.name && current.name.length){ res[current.name] = current.value; } } } return res; }; /** * 表单数据反序列化 */ Object.prototype.deserialize = function( data ){ if(!data){ throw '反序列化值不能为空'; } let form = this, current = null; try { for(let i=0, len=form.elements.length; i<len; i++){ current = form.elements[i]; switch(current.type){ //可忽略控件处理 case "file": //文件输入类型 case "submit": //提交按钮 case "button": //一般按钮 case "image": //图像形式的提交按钮 case "reset": //重置按钮 case undefined: //未定义 break; //单选,复选框 case "radio": case "checkbox": let value = data[current.name]; current.checked = false; if(value == current.value){ current.checked = true; } break; default: //一般表单控件处理 current.value = data[current.name]; } } }catch (e) { throw e; } }; /** * 判断是否是一个DOM元素 * @returns {boolean} */ Object.prototype.isDOM = function(){ return typeof HTMLElement === 'object' ? this instanceof HTMLElement : typeof this === 'object' && this.nodeType === 1 && typeof this.nodeName === 'string'; }; //删除前后空格 String.prototype.trim = function(){ return this.replace(/(^\s*)|(\s*$)/g, ""); }; work.init(); })();
- 点赞
- 收藏
- 分享
- 文章举报
![](https://g.csdnimg.cn/static/user-reg-year/1x/6.png)
相关文章推荐
- JSP数据交互
- JS 实现元素颜色跟随滚动条变化
- JavaScript声明全局变量三种方式的异同
- javascript 获取URL各个部分的功能
- 简单的选项卡(html + css + js)
- JS使图片在图片框中自适应,按比例缩放
- JS简易拖拽效果
- Js中 关于top、clientTop、scrollTop、offsetTop的用法
- js日期的常用操作
- JS实现以日历形式显示当前时间
- 完整显示当前日期和时间的JS代码(2007年2月25日星期日正午12:42:48)
- JS左侧菜单-04
- JS关闭当前页面的方法
- C#代码和javascript函数相互调用
- JavaScript学习笔记
- java C# javascript css 资源共享
- jsp页面使用jstl表示时,注意事项
- servlet到jsp页面出现乱码,原因
- XStream完美转换XML、JSON
- JavaScriptCore.framework基本用法(一)