const TAU = Math.PI * 2 const mapToEllipse = ({ x, y }, rx, ry, cosphi, sinphi, centerx, centery) => { x *= rx y *= ry const xp = cosphi * x - sinphi * y const yp = sinphi * x + cosphi * y return { x: xp + centerx, y: yp + centery } } const approxUnitArc = (ang1, ang2) => { const a = 4 / 3 * Math.tan(ang2 / 4) const x1 = Math.cos(ang1) const y1 = Math.sin(ang1) const x2 = Math.cos(ang1 + ang2) const y2 = Math.sin(ang1 + ang2) return [ { x: x1 - y1 * a, y: y1 + x1 * a }, { x: x2 + y2 * a, y: y2 - x2 * a }, { x: x2, y: y2 } ] } const vectorAngle = (ux, uy, vx, vy) => { const sign = (ux * vy - uy * vx < 0) ? -1 : 1 const umag = Math.sqrt(ux * ux + uy * uy) const vmag = Math.sqrt(ux * ux + uy * uy) const dot = ux * vx + uy * vy let div = dot / (umag * vmag) if (div > 1) { div = 1 } if (div < -1) { div = -1 } return sign * Math.acos(div) } const getArcCenter = ( px, py, cx, cy, rx, ry, largeArcFlag, sweepFlag, sinphi, cosphi, pxp, pyp ) => { const rxsq = Math.pow(rx, 2) const rysq = Math.pow(ry, 2) const pxpsq = Math.pow(pxp, 2) const pypsq = Math.pow(pyp, 2) let radicant = (rxsq * rysq) - (rxsq * pypsq) - (rysq * pxpsq) if (radicant < 0) { radicant = 0 } radicant /= (rxsq * pypsq) + (rysq * pxpsq) radicant = Math.sqrt(radicant) * (largeArcFlag === sweepFlag ? -1 : 1) const centerxp = radicant * rx / ry * pyp const centeryp = radicant * -ry / rx * pxp const centerx = cosphi * centerxp - sinphi * centeryp + (px + cx) / 2 const centery = sinphi * centerxp + cosphi * centeryp + (py + cy) / 2 const vx1 = (pxp - centerxp) / rx const vy1 = (pyp - centeryp) / ry const vx2 = (-pxp - centerxp) / rx const vy2 = (-pyp - centeryp) / ry let ang1 = vectorAngle(1, 0, vx1, vy1) let ang2 = vectorAngle(vx1, vy1, vx2, vy2) if (sweepFlag === 0 && ang2 > 0) { ang2 -= TAU } if (sweepFlag === 1 && ang2 < 0) { ang2 += TAU } return [ centerx, centery, ang1, ang2 ] } const arcToBezier = ({ px, py, cx, cy, rx, ry, xAxisRotation = 0, largeArcFlag = 0, sweepFlag = 0 }) => { const curves = [] if (rx === 0 || ry === 0) { return [] } const sinphi = Math.sin(xAxisRotation * TAU / 360) const cosphi = Math.cos(xAxisRotation * TAU / 360) const pxp = cosphi * (px - cx) / 2 + sinphi * (py - cy) / 2 const pyp = -sinphi * (px - cx) / 2 + cosphi * (py - cy) / 2 if (pxp === 0 && pyp === 0) { return [] } rx = Math.abs(rx) ry = Math.abs(ry) const lambda = Math.pow(pxp, 2) / Math.pow(rx, 2) + Math.pow(pyp, 2) / Math.pow(ry, 2) if (lambda > 1) { rx *= Math.sqrt(lambda) ry *= Math.sqrt(lambda) } let [ centerx, centery, ang1, ang2 ] = getArcCenter( px, py, cx, cy, rx, ry, largeArcFlag, sweepFlag, sinphi, cosphi, pxp, pyp ) const segments = Math.max(Math.ceil(Math.abs(ang2) / (TAU / 4)), 1) ang2 /= segments for (let i = 0; i < segments; i++) { curves.push(approxUnitArc(ang1, ang2)) ang1 += ang2 } return curves.map(curve => { const { x: x1, y: y1 } = mapToEllipse(curve[ 0 ], rx, ry, cosphi, sinphi, centerx, centery) const { x: x2, y: y2 } = mapToEllipse(curve[ 1 ], rx, ry, cosphi, sinphi, centerx, centery) const { x, y } = mapToEllipse(curve[ 2 ], rx, ry, cosphi, sinphi, centerx, centery) return { x1, y1, x2, y2, x, y } }) } function CacBezierCurveMidPos(tempSourcePos, tempTargetPos) { let dx = tempSourcePos.x - tempTargetPos.x, dy = tempSourcePos.y - tempTargetPos.y, dr = Math.sqrt(dx * dx + dy * dy); const curve = { type: 'arc', rx: dr, ry: dr, largeArcFlag: 0, sweepFlag: 1, xAxisRotation: 0, } const curves = arcToBezier({ px: tempSourcePos.x, py: tempSourcePos.y, cx: tempTargetPos.x, cy: tempTargetPos.y, rx: curve.rx, ry: curve.ry, xAxisRotation: curve.xAxisRotation, largeArcFlag: curve.largeArcFlag, sweepFlag: curve.sweepFlag, }); return curves[0]; } let canvas, stage; // Aliases let Stage = createjs.Stage, Sprite = createjs.Sprite, Bitmap = createjs.Bitmap, Shape = createjs.Shape, Text = createjs.Text, Graphics = createjs.Graphics, Container = createjs.Container; let edgeContainer = new Container(), arrowContainer = new Container(), nodeContainer = new Container(), textContainer = new Container(), imageContainer = new Container(), dragContainer = new Container(); // For scale limmit const SCALE_MAX = 100, SCALE_MIN = 0.2; // Fix canvas position offset const offsetX = 0, offsetY = 0; // Defalut node radius // Node shape default is circle // FIXME: Other shape‘s support let nodeWidth = 30; // Cache the shape that what your mouse click generate let point = {}; // Cache list // Have unique identifiers => id let nodeList = {},// Cache node edgeList = {},// Cache edge arrowList = {},// Cache arrow textList = {},// Cache text lineList = {},// Cache edge info bezierList = {},// Cache bezierCurve to Deal with 2 bezierCurve imageList = {},// Cache image maskList = {},// Cache mask circleList = {};// Cache init node shape info // Now is just for Bezier,TODO is for all(include straight use midPos to Calculate) // let midPos; // Fix Events trigger conflict between nativeEvent and CreatejsEvent let nodeFlag = false; let pinEffectFlag = false; let auraArray = [];//节点属性数组 let simulation;// D3-force simulation /** * CreateCharts is the core method for initialization * @param {Object} opts * opts defined some init options * For example:(after "..." means no necessary) * {container: document.getElementById('canvas'), * elements: { * nodes:[{ id: 'a' * ...width:10,color:'black'}, * { id: 'b' * ...width:30,color:'#d3d3d3'}], * edges:{ source:'a',target:'b' * ...curveStyle:'bezier',targetShape:'triangle',sourceShape:'circle'}} */ function CreateCharts({ container, elements }) { if (!container) return;// Todo => Throw error canvas = container; // Enable touch interactions if supported on the current device: createjs.Touch.enable(stage); // Enabled mouse over / out events stage = new Stage(canvas); // Auto update stage createjs.Ticker.timingMode = createjs.Ticker.RAF; createjs.Ticker.addEventListener("tick", stage); // Keep tracking the mouse even when it leaves the canvas stage.mouseMoveOutside = true; stage.enableMouseOver(10); let { nodes, edges } = elements; //Init canvas property canvas.height = window.innerHeight - offsetY; canvas.width = window.innerWidth - offsetX; canvas.style.background = '#d3d3d3'; initializeNodes(nodes, edges); // Init event initEvent(canvas); // Hierarchy order stage.addChild(edgeContainer); stage.addChild(arrowContainer); stage.addChild(nodeContainer);// Node above edge and arrow stage.addChild(textContainer);// Text above node stage.addChild(imageContainer); stage.addChild(dragContainer);// Hightest level } /** * InitializeNodes method use for layout and paint nodes * @param {Array} nodes * @param {Array} edges */ function initializeNodes(nodes, edges) { //D3-force Layout simulation = d3.forceSimulation(nodes) .force("charge", d3.forceManyBody()) //.force('charge', d3.forceManyBody().strength(-30).distanceMin(100)) .force('link', d3.forceLink(edges).id((d) => d.id).distance(600).strength(1))//节点的查找方式为其id值 .force('center', d3.forceCenter(canvas.width / 2, canvas.height / 2)) //.force("collide", d3.forceCollide().radius(function(d) { return d.width; }).strength(0.7).iterations(1)) .force("x", d3.forceX()) .force("y", d3.forceY()) //Init node and node text for (let i = 0, node; i< nodes.length; i++) { node = nodes[i]; nodeList[node.id] = node; //Draw node let graphics = new Shape(); let circle = graphics.graphics; if (node.color) { circle.beginFill(node.color); } else { let color = getRandomColor(); circle.beginFill(color); } if(node.text)drawText(node, node.x, node.y); if(node.imageSrc)drawImage(node, node.x, node.y); if(node.aura) initAura(node); let width = nodeWidth; if (node.width){ width = node.width; }else{ node.width = width; } circle.drawCircle(0, 0, width); circle.endFill(); graphics = setNode(graphics, node.id, pinEffectFlag); //Move the graph to its designated position graphics.x = node.x; graphics.y = node.y; //用缓存的话放大会失真 //graphics.cache(-width, -width, width*2, width*2); circleList[node.id] = graphics; nodeContainer.addChild(graphics); } console.log(auraArray); //Init edge text for (let i = 0, edge; i< edges.length; i++) { edge = edges[i]; if(edge.text)drawText(edge,0,0); } let ticked = function () { nodes.forEach(drawNode); edges.forEach(drawEdge); stage.update(); } let drawNode = function (node) { //只需要移动位置 circleList[node.id].x = node.x; circleList[node.id].y = node.y; if(node.text) updateText(node.id, { x: node.x, y: node.y }); if(node.imageSrc) updateImage(node.id, { x: node.x, y: node.y }) } let drawEdge = function (edge) { drawArrowAndEdge(edge, edge.source, edge.target); } simulation.on('tick', ticked); } function initAura(node){ let color = getRandomColor(); if(!auraArray[node.aura]) auraArray[node.aura] = {}; let auraProperty = auraArray[node.aura]; auraProperty.color = color; if(auraProperty.member===undefined) auraProperty.member = []; auraProperty.member.push(node); } /** * DrawImage method use for init node's image * @param {Object} nodeInfo * imageSrc=>node image's url * id=> use for set image id * textOpts => image style * @param x => image position X * @param y => image position y */ function drawImage({ imageSrc, id, width, imageOpts = {} }, x, y) { if (imageSrc === '' || imageSrc === undefined) return; if (imageList[id]) imageContainer.removeChild(imageList[id]); let image = new Image(); image.src = imageSrc; let imgWidth = image.width, imgHeight = image.height; //Calculate scale let scale = width/imgWidth*2; image.onload = function(){ let bmp = new createjs.Bitmap(image); bmp.scale = scale; let cGraphics = new Shape(); let circle = cGraphics.graphics; circle.beginFill(Graphics.getRGB(255, 255, 255, 0));//alpha let circleWidth = width-2; circle.drawCircle(0,0,circleWidth); circle.endFill(); //mask还是可以cache的 //cGraphics.cache(-circleWidth, -circleWidth, circleWidth*2, circleWidth*2); bmp.mask = cGraphics; maskList[id] = cGraphics; imageContainer.addChild(cGraphics); bmp.x = x-width/2; bmp.y = y-width/2; // Cache image imageList[id] = bmp; imageContainer.addChild(bmp); } } /** * updateImage method use for update node image position * @param {String} id * @param {Object} newPos */ function updateImage(id, newPos) { let bmp = imageList[id]; if(bmp){ bmp.x = newPos.x-nodeList[id].width; bmp.y = newPos.y-nodeList[id].width; if(maskList[id]){ let mask = maskList[id]; mask.x = newPos.x; mask.y = newPos.y; } } } /** * DrawText method use for init node text position * @param {Object} nodeInfo * text=>node text to show * id=> use for set text id * textOpts => text style * @param x => text position X * @param y => text position y */ function drawText({ text, id, textOpts = {} }, x, y) { if (text === '' || text === undefined) return; if (textList[id]) textContainer.removeChild(textList[id]); let nodeText = new Text(text, textOpts.font || '36px Arial', textOpts.color || 'white'); x += textOpts.disX? textOpts.disX: 0;//初始化这一步可以省略 y += textOpts.disY? textOpts.disY: 0;//因为后面每一帧都会刷新,初始化可以放在任意位置 nodeText.textAlign = 'center'; nodeText.textBaseline = 'middle'; nodeText = Object.assign(nodeText, textOpts); nodeText.x = x; nodeText.y = y; // Cache text textList[id] = nodeText; textContainer.addChild(nodeText); } /** // * UpdateText method use for update text when node position change * @param {String} id * @param {Object} newPos */ let updateText = function (id, newPos) { let text = textList[id]; newPos.x += text.disX? text.disX: 0; newPos.y += text.disY? text.disY: 0; text.x = newPos.x; text.y = newPos.y; } /** * SetNode method use for init node's event when dragging node * @param {Shape} graph * @param {String} id * @return {Shape} */ function setNode(graph, id, pinFlag) { let onDragEnd; if (pinFlag) { onDragEnd = function (event) { simulation.alphaTarget(0); //Sticky or not // nodeList[id].fx = null; // nodeList[id].fy = null; let target = event.target; target.dragging = false; // Set the interaction data to null target.data = null; // Put back the original container dragContainer.removeChild(this); dragContainer.removeChild(textList[id]); dragContainer.removeChild(imageList[id]); nodeContainer.addChild(this); textContainer.addChild(textList[id]); imageContainer.addChild(imageList[id]); nodeFlag = false; } } else { onDragEnd = function (event) { simulation.alphaTarget(0); //Sticky or not nodeList[id].fx = null; nodeList[id].fy = null; let target = event.target; target.dragging = false; // Set the interaction data to null target.data = null; // Put back the original container dragContainer.removeChild(this); dragContainer.removeChild(textList[id]); dragContainer.removeChild(imageList[id]); nodeContainer.addChild(this); textContainer.addChild(textList[id]); imageContainer.addChild(imageList[id]); nodeFlag = false; } } let onDragStart = function (event) { simulation.alphaTarget(0.3).restart(); event.stopPropagation();// FIXME nodeFlag = true; } let onDragMove = function (event) { let newPosition = stage.globalToLocal(event.stageX, event.stageY); this.x = newPosition.x; this.y = newPosition.y; updateNode(id, newPosition); // data drive document //updateEdge(id, newPosition);// change node pos enough if(textList[id])updateText(id, newPosition); textContainer.removeChild(textList[id]); nodeContainer.removeChild(this); imageContainer.removeChild(imageList[id]); dragContainer.addChild(this); dragContainer.addChild(textList[id]); dragContainer.addChild(imageList[id]); } //图钉效果 let onDbClick = function () { nodeList[id].fx = null; nodeList[id].fy = null; } let updateNode = function (id, newPos) { nodeList[id].x = newPos.x; nodeList[id].y = newPos.y; nodeList[id].fx = newPos.x; nodeList[id].fy = newPos.y; } graph.cursor = "pointer"; graph.on('mousedown', onDragStart); graph.on('pressup', onDragEnd); graph.on('pressmove', onDragMove); if (pinFlag) { graph.on('dblclick', onDbClick); } return graph; } function resetNodes() { for (let circle in circleList) { circleList[circle].removeAllEventListeners(); setNode(circleList[circle], circle, pinEffectFlag); } } /** * DrawArrowAndEdge method use for update the arrow and edge from node source and target position * @param {Object} data * @param {Object} source {x,y} * @param {Object} target {x,y} */ function drawArrowAndEdge(data, source, target) { // Remove old edge (drawArrowShape will remove old arrow) if (lineList[data.id]) edgeContainer.removeChild(lineList[data.id]); // Draw Arrow let newSourcePos, newTargetPos; if (data.targetShape) { if (data.curveStyle) { switch (data.curveStyle) { case "bezier": // CacBezierCurve let bMidPos = CacBezierCurveMidPos(source, target); let pos2 = { x: bMidPos.x2, y: bMidPos.y2 } if (textList[data.id]) updateText(data.id, { x: (bMidPos.x1 + bMidPos.x2) / 2, y: (bMidPos.y1 + bMidPos.y2) / 2 }); newTargetPos = drawArrowShape(data.id, data.targetShape, pos2, target, source, target, true); break; case "quadraticCurve": // QuadraticCurve let cMidPos = CacQuadraticCurveMidPos(source, target, 100); newTargetPos = drawArrowShape(data.id, data.targetShape, cMidPos, target, source, target, true); break; default: break; } } else { if (textList[data.id]) updateText(data.id, { x: (source.x + target.x) / 2, y: (source.y + target.y) / 2 }); newTargetPos = drawArrowShape(data.id, data.targetShape, source, target, source, target, true); } } if (data.sourceShape) { if (data.curveStyle) { switch (data.curveStyle) { case "bezier": // Cacular Third Bezier Curve's Mid pos let bMidPos = CacBezierCurveMidPos(source, target); let pos1 = { x: bMidPos.x1, y: bMidPos.y1 } if (textList[data.id]) updateText(data.id, { x: (bMidPos.x1 + bMidPos.x2) / 2, y: (bMidPos.y1 + bMidPos.y2) / 2 }); newSourcePos = drawArrowShape(data.id, data.sourceShape, source, pos1, source, target, false); break; case "quadraticCurve": // Cacular Second Bezier Curve's Mid pos let cMidPos = CacQuadraticCurveMidPos(source, target, 100); newSourcePos = drawArrowShape(data.id, data.sourceShape, source, cMidPos, source, target, false); break; default: break; } } else { if (textList[data.id]) updateText(data.id, { x: (source.x + target.x) / 2, y: (source.y + target.y) / 2 }); newSourcePos = drawArrowShape(data.id, data.sourceShape, source, target, source, target, false); } } let tempSourcePos = newSourcePos ? newSourcePos : source; let tempTargetPos = newTargetPos ? newTargetPos : target; //Draw edge let graphics = new Shape(); let line = graphics.graphics; if (data.lineMode === "dash") {//Todo => param line.setStrokeStyle(4).setStrokeDash([20, 10], 0).beginStroke("#FFF"); } else { line.setStrokeStyle(4).beginStroke("#FFF"); } line.moveTo(tempSourcePos.x, tempSourcePos.y); if (data.curveStyle) { switch (data.curveStyle) { case "bezier": // Cacular Third Bezier Curve's Mid pos let cPos = CacBezierCurveMidPos(tempSourcePos, tempTargetPos, 100); line.bezierCurveTo(cPos.x1, cPos.y1, cPos.x2, cPos.y2, cPos.x, cPos.y); break; case "quadraticCurve": // Cacular Second Bezier Curve's Mid pos let bPos = CacQuadraticCurveMidPos(tempSourcePos, tempTargetPos, 100); line.quadraticCurveTo(bPos.x, bPos.y, tempTargetPos.x, tempTargetPos.y); break; } } else { line.lineTo(tempTargetPos.x, tempTargetPos.y); } lineList[data.id] = graphics; edgeContainer.addChild(graphics); } /** * DrawArrowShape method use for draw the arrow from node source and target position * @param {String} id arrow's id * @param {String} shape arrow's shape * @param {Object} sourcePos {x,y} node source pos * @param {Object} targetPos {x,y} node target pos * @param {Object} source {x,y} arrow source pos * @param {Object} target {x,y} arrow target pos * @param {Bool} targetFlag draw source or target arrow */ function drawArrowShape(id, shape, sourcePos, targetPos, source, target, targetFlag) { switch (shape) { case 'circle': let c_nodeRadius = nodeWidth; if (!targetFlag && sourcePos.width) c_nodeRadius = sourcePos.width; if (targetFlag && targetPos.width) c_nodeRadius = targetPos.width; //Boundary determination => hide it if stick together if ((Math.abs(source.y - target.y) < c_nodeRadius * 1.5) && (Math.abs(source.x - target.x) < c_nodeRadius * 1.5)) { c_nodeRadius = 0; if (textList[id]) textList[id].visible = false; } let srcPos = targetFlag ? targetPos : sourcePos; let tgtPos = targetFlag ? sourcePos : targetPos; let c_angle = Math.atan(Math.abs(srcPos.y - tgtPos.y) / Math.abs(srcPos.x - tgtPos.x)) let circleWidth = c_nodeRadius / 2; // posX and posY is the circle's final position let posX = (c_nodeRadius + circleWidth) * Math.cos(c_angle), posY = (c_nodeRadius + circleWidth) * Math.sin(c_angle); // Discusses the relative position of target and source if (srcPos.x > tgtPos.x) {// Source node is right posX = srcPos.x - posX; } else { posX = srcPos.x + posX; } if (srcPos.y > tgtPos.y) {// Source node is Up posY = srcPos.y - posY; } else { posY = srcPos.y + posY; } //Draw circle let cGraphics = new Shape(); let circle = cGraphics.graphics; circle.beginFill("#66CCFF"); circle.drawCircle(0, 0, circleWidth); circle.endFill(); cGraphics.x = posX; cGraphics.y = posY; //updateArrow updateArrow(id, cGraphics, targetFlag); return { x: posX, y: posY } case 'triangle': //这个三角形默认按顶角为50°,两个底角为65°来算,两边长先按一半nodeWidth来算吧 let t_nodeRadius = nodeWidth; if (!targetFlag && sourcePos.width) t_nodeRadius = sourcePos.width; if (targetFlag && targetPos.width) t_nodeRadius = targetPos.width; //Boundary determination if ((Math.abs(source.y - target.y) < t_nodeRadius * 1.5) && (Math.abs(source.x - target.x) < t_nodeRadius * 1.5)) { t_nodeRadius = 0; if (textList[id]) textList[id].visible = false; } let t_srcPos = targetFlag ? sourcePos : targetPos; let t_tgtPos = targetFlag ? targetPos : sourcePos; let topAngle = Math.PI / 180 * 50,//角度转弧度,注意Math的那些方法的单位是弧度 sideEdge = t_nodeRadius,//瞅着合适,先凑合 halfBottomEdge = Math.sin(topAngle / 2) * sideEdge, centerEdge = Math.cos(topAngle / 2) * sideEdge; //angle是一样的,先按node中心算,arrow中心算之后再说,先todo(直线版看出不这个问题,曲线就崩了) let angle = Math.atan(Math.abs(t_srcPos.y - t_tgtPos.y) / Math.abs(t_srcPos.x - t_tgtPos.x)); let beginPosX = t_nodeRadius * Math.cos(angle), beginPosY = t_nodeRadius * Math.sin(angle), pos1X, pos1Y, pos2X, pos2Y, centerX = (t_nodeRadius + centerEdge) * Math.cos(angle), centerY = (t_nodeRadius + centerEdge) * Math.sin(angle); pos1X = pos2X = Math.sin(angle) * halfBottomEdge; pos1Y = pos2Y = Math.cos(angle) * halfBottomEdge;//简单的几何知识(手动抽搐😖) //还需要分类讨论target和source的左右位置的各种情况 //1234代表target相对source所在象限 if (t_srcPos.x > t_tgtPos.x) {//source节点在右 if (t_srcPos.y > t_tgtPos.y) {//下 ----> 1 beginPosX = t_tgtPos.x + beginPosX; beginPosY = t_tgtPos.y + beginPosY; centerX = t_tgtPos.x + centerX; centerY = t_tgtPos.y + centerY; pos1X = centerX + pos1X; pos1Y = centerY - pos1Y;//+ - pos2X = centerX - pos2X; pos2Y = centerY + pos2Y;//- + } else {//上 ----> 4 beginPosX = t_tgtPos.x + beginPosX; beginPosY = t_tgtPos.y - beginPosY; centerX = t_tgtPos.x + centerX; centerY = t_tgtPos.y - centerY; pos1X = centerX + pos1X; pos1Y = centerY + pos1Y;//+ + pos2X = centerX - pos2X; pos2Y = centerY - pos2Y;//- - } } else {//source节点在左 if (t_srcPos.y > t_tgtPos.y) {//下 ----> 2 beginPosX = t_tgtPos.x - beginPosX; beginPosY = t_tgtPos.y + beginPosY; centerX = t_tgtPos.x - centerX; centerY = t_tgtPos.y + centerY; pos1X = centerX - pos1X; pos1Y = centerY - pos1Y;//- - pos2X = centerX + pos2X; pos2Y = centerY + pos2Y;//+ + } else {//上 ----> 3 beginPosX = t_tgtPos.x - beginPosX; beginPosY = t_tgtPos.y - beginPosY; centerX = t_tgtPos.x - centerX; centerY = t_tgtPos.y - centerY; pos1X = centerX - pos1X; pos1Y = centerY + pos1Y;//- + pos2X = centerX + pos2X; pos2Y = centerY - pos2Y;//+ - } } //Draw triangle let tGraphics = new Shape(); let triangle = tGraphics.graphics; triangle.beginFill("#66CCFF"); //triangle.lineStyle(0, 0x66CCFF, 1); triangle.moveTo(beginPosX, beginPosY); triangle.lineTo(pos1X, pos1Y); triangle.lineTo(pos2X, pos2Y); triangle.endFill(); updateArrow(id, tGraphics, targetFlag); return { x: centerX, y: centerY } } } /** * @param {String} id arrow's id * @param {String} shape arrow's shape * @param {Bool} targetFlag source or target arrow */ function updateArrow(id, shape, targetFlag) { if (!arrowList[id]) arrowList[id] = {}; if (!targetFlag) {//Source arrow if (arrowList[id].sourceArrow) arrowContainer.removeChild(arrowList[id].sourceArrow); //save newArrow arrowList[id].sourceArrow = shape; } else {//Target arrow if (arrowList[id].targetArrow) arrowContainer.removeChild(arrowList[id].targetArrow); //save newArrow arrowList[id].targetArrow = shape; } arrowContainer.addChild(shape); } /** * InitEvent method use for init canvas's zoom and drag event * @param canvas canvas to init */ function initEvent(canvas) { // Scale/Zoom canvas.addEventListener('wheel', function (e) { if (e.deltaY < 0) { zooming(true, e.pageX, e.pageY); } else { zooming(false, e.pageX, e.pageY); } }); function zooming(zoomFlag, x, y) { //Current scale let scale = stage.scale; let point = toLocalPos(x, y); // //Zooming if (zoomFlag) { if (scale < SCALE_MAX) { scale += 0.1; //moving stage.x = stage.x - (point.x * 0.1), stage.y = stage.y - (point.y * 0.1); } } else { if (scale > SCALE_MIN) { scale -= 0.1; //moving stage.x = stage.x - (point.x * -0.1), stage.y = stage.y - (point.y * -0.1); } } stage.scale = scale; } // Drag/Move let movePosBegin = {}; let startMousePos = {}; let hitArea = new Shape(); let canvasDragging = false; // Could use bind canvas.addEventListener('mousedown', stagePointerDown); canvas.addEventListener('mouseup', stagePointerUp); canvas.addEventListener('mouseout', stagePointerUp); canvas.addEventListener('mousemove', stagePointerMove); canvas.addEventListener('contextmenu', onContextMenu); let menu = document.querySelector('.menu'); let pinEffectButton = document.querySelector('#pin'); pinEffectButton.addEventListener('click', pinEffect); function pinEffect() { pinEffectFlag = !pinEffectFlag; resetNodes(); hideMenu(); } function onContextMenu(e) { e.preventDefault(); e.stopPropagation(); showMenu(e.pageX - offsetX, e.pageY - offsetY); canvas.addEventListener('mousedown', onMouseDown); if (pinEffectFlag) { document.querySelector('#pin span').innerText = '关闭图钉效果'; } else { document.querySelector('#pin span').innerText = '启用图钉效果'; } } function onMouseDown(e) { hideMenu(); document.removeEventListener('mousedown', onMouseDown); } function showMenu(x, y) { menu.style.left = x + 'px'; menu.style.top = y + 'px'; menu.classList.add('show-menu'); } function hideMenu() { menu.classList.remove('show-menu'); } function stagePointerDown(event) { if (!nodeFlag) { canvasDragging = true; movePosBegin.x = stage.x; movePosBegin.y = stage.y; startMousePos.x = event.pageX; startMousePos.y = event.pageY; //Draw circle let r = 30 / stage.scale; drawCircle(startMousePos.x, startMousePos.y, r); } } function stagePointerUp(event) { canvasDragging = false; //Remove circle if (point.circle) dragContainer.removeChild(point.circle); } function stagePointerMove(event) { if (canvasDragging && !nodeFlag && event.which === 1) { //Move circle let x = event.pageX; let y = event.pageY; //Remove circle first if (point.circle) dragContainer.removeChild(point.circle); //Redraw circle //Current scale let scale = stage.scale; let r = 30 / scale; drawCircle(x, y, r); let offsetX = x - startMousePos.x,//差值 offsetY = y - startMousePos.y; stage.x = movePosBegin.x + offsetX; stage.y = movePosBegin.y + offsetY;//修正差值 } } } function getRandomColor(){ return "#"+("00000"+((Math.random()*16777215+0.5)>>0).toString(16)).slice(-6); } function toLocalPos(x, y) { let localPos = stage.globalToLocal(x - offsetX, y - offsetY); return localPos; } function drawCircle(x, y, r = 30) { //Draw circle let cGraphics = new Shape(); let circle = cGraphics.graphics; circle.beginFill(Graphics.getRGB(0, 0, 0, 0.2));//alpha circle.drawCircle(0, 0, r); circle.endFill(); let localPos = toLocalPos(x, y); cGraphics.x = localPos.x; cGraphics.y = localPos.y; point.circle = cGraphics; dragContainer.addChild(cGraphics); } function autoGenera(nodeNum){ let resObj = {},nodes = [],edges = []; for(let i=0;i<nodeNum;i++){ let data = {}; //Ascii => A => 65 data.id = String.fromCharCode(65+i); data.width = (Math.random()*30)+20; data.text = data.id; data.textOpts = {disY:data.width+20}; if(i%2===0){ data.aura = "HR" }else{ data.aura = "Sales" } // if(i%2===0)data.color="#9f5f9f"; if(i%2===0){ data.imageSrc = "http://ouib5enmf.bkt.clouddn.com/bz.jpeg" }else{ data.imageSrc = "http://ouib5enmf.bkt.clouddn.com/LG.png" } nodes.push(data); } for(let i=0;i<nodeNum-1;i++){ let data = {}; data.id = String.fromCharCode(65+nodeNum+i); data.source = nodes[Math.floor(Math.sqrt(i))].id; data.target = nodes[i + 1].id; data.text = data.id; data.textOpts = {color:'#000',outline:2} //if(i%2==0)data.curveStyle = 'bezier' if(i%3==0)data.lineMode = 'dash' if(i%2==0){ data.targetShape='triangle'; data.sourceShape='circle'; }else{ data.targetShape='circle'; data.sourceShape='triangle'; } edges.push(data); } resObj.nodes = nodes; resObj.edges = edges; return resObj; } CreateCharts({ container: document.getElementById('testCanvas'), elements: autoGenera(20) })
<div id="container"> <canvas id="testCanvas"></canvas> <menu class="menu"> <li class="menu-item" id="pin"> <button type="button" class="menu-btn"> <span class="menu-text">启用图钉效果</span> </button> </li> <li class="menu-item disabled"> <button type="button" class="menu-btn"> <span class="menu-text">测试禁用</span> </button> </li> <li class="menu-item submenu"> <button type="button" class="menu-btn"> <span class="menu-text">测试多级菜单</span> </button> <menu class="menu"> <li class="menu-item"> <button type="button" class="menu-btn"> <span class="menu-text">测试多级菜单</span> </button> </li> <li class="menu-item submenu"> <button type="button" class="menu-btn"> <span class="menu-text">测试多级菜单</span> </button> <menu class="menu"> <li class="menu-item"> <button type="button" class="menu-btn"> <span class="menu-text">测试多级菜单</span> </button> </li> </menu> </li> </menu> </li> </menu> </div> <script src="https://code.createjs.com/1.0.0/easeljs.min.js"></script> <script src="//d3js.org/d3.v4.min.js"></script>
/* Menu */ .menu { position: absolute; width: 200px; padding: 2px; top: 0; margin: 0; border: 1px solid #bbb; background: #eee; background: -webkit-linear-gradient(to bottom, #fff 0%, #e5e5e5 100px, #e5e5e5 100%); background: linear-gradient(to bottom, #fff 0%, #e5e5e5 100px, #e5e5e5 100%); z-index: 100; border-radius: 3px; box-shadow: 1px 1px 4px rgba(0,0,0,.2); opacity: 0; -webkit-transform: translate(0, 15px) scale(.95); transform: translate(0, 15px) scale(.95); transition: transform 0.1s ease-out, opacity 0.1s ease-out; pointer-events: none; } .menu-item { display: block; position: relative; margin: 0; padding: 0; white-space: nowrap; } .menu-btn { display: block; color: #444; font-family: 'Roboto', sans-serif; font-size: 13px; cursor: pointer; border: 1px solid transparent; white-space: nowrap; padding: 6px 8px; border-radius: 3px; } button.menu-btn { background: none; line-height: normal; overflow: visible; -webkit-user-select: none; -moz-user-select: none; -ms-user-select: none; width: 100%; text-align: left; } a.menu-btn { outline: 0 none; text-decoration: none; } .menu-text { margin-left: 25px; } .menu-btn .fa { position: absolute; left: 8px; top: 50%; -webkit-transform: translateY(-50%); transform: translateY(-50%); } .menu-item:hover > .menu-btn { color: #fff; outline: none; background-color: #2E3940; background: -webkit-linear-gradient(to bottom, #5D6D79, #2E3940); background: linear-gradient(to bottom, #5D6D79, #2E3940); border: 1px solid #2E3940; } .menu-item.disabled { opacity: .5; pointer-events: none; } .menu-item.disabled .menu-btn { cursor: default; } .menu-separator { display:block; margin: 7px 5px; height:1px; border-bottom: 1px solid #fff; background-color: #aaa; } .menu-item.submenu::after { content: ""; position: absolute; right: 6px; top: 50%; -webkit-transform: translateY(-50%); transform: translateY(-50%); border: 5px solid transparent; border-left-color: #808080; } .menu-item.submenu:hover::after { border-left-color: #fff; } .menu .menu { top: 4px; left: 99%; } .show-menu, .menu-item:hover > .menu { opacity: 1; -webkit-transform: translate(0, 0) scale(1); transform: translate(0, 0) scale(1); pointer-events: auto; } .menu-item:hover > .menu { -webkit-transition-delay: 300ms; transition-delay: 300ms; }