您的位置:首页 > Web前端 > JavaScript

源生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();
})();
  • 点赞
  • 收藏
  • 分享
  • 文章举报
忧郁的兜兜 发布了3 篇原创文章 · 获赞 0 · 访问量 89 私信 关注
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: