<!-- * @FileDescription: vue-konva * @Author: hcy * @Date: 2023-03-29 --> <script setup lang="ts"> import { ref, reactive, onMounted } from 'vue'; import { getBoundingBox } from 'src/common/utils'; import { scalePolygon } from './utils'; import Konva from 'konva'; import Decimal from 'decimal.js'; const startSelected = ref(true); const state = reactive({ stage: null as any, layer: null as any, group: new Map(), myMap: new Map(), // shape: null as any, // boundingBox: null as any, // box: null as any, shapePath: [ { name: 'test1', color: '#00D2FF', selected: true, path: [ { x: 100, y: 100, gap: 10 }, { x: 200, y: 100, gap: 20 }, { x: 200, y: 200, gap: 30 }, { x: 0, y: 300, gap: 40 }, ], }, // { // name: 'test2', // color: '#4CAF50', // selected: true, // path: [ // { x: 300, y: 200 }, // { x: 200, y: 400 }, // { x: 100, y: 600 }, // { x: 400, y: 600 }, // { x: 500, y: 200 }, // ], // }, // { // name: 'test3', // color: '#FF8A80', // selected: true, // path: [ // { x: 600, y: 200 }, // { x: 500, y: 400 }, // { x: 300, y: 100 }, // ], // }, // { // name: 'test4', // color: '#FFC107', // selected: true, // path: [ // { x: 1000, y: 0 }, // { x: 1100, y: 0 }, // { x: 1100, y: 100 }, // { x: 1000, y: 100 }, // ], // }, ] as any[], }); const stageSize = reactive({ width: 1200, height: 800, gridGap: 100, }); onMounted(() => { // const hit = Collide2D.collidePointPoint(100, 100, 100, 100); // console.log('hit', Collide2D); drawGrid(); handleData(); console.log('数据', state.shapePath); state.stage = new Konva.Stage({ container: 'stage-container', width: stageSize.width, height: stageSize.height, }); state.layer = new Konva.Layer(); state.stage.add(state.layer); let index = 0; for (const item of state.shapePath) { let shape = createShape(item); if (shape) { state.layer.add(shape); state.group.set(shape._id, index); // 储存 state.shapePath的下标 state.myMap.set(item.name, shape._id); } index++; } state.layer.on('dragmove', layerDragmove); }); function createShape(itemData: any) { const path = itemData.path || []; const fillColor = itemData.color || '#000000'; const name = itemData.name; const boundingBox = itemData.boundingBox || {}; const boundingBoxPath = itemData.boundingBox?.path || []; if (boundingBoxPath.length < 2) { console.warn('小于两个点,不能构成多边形,没有边界框'); } else { let group = new Konva.Group({ x: 0, y: 0, draggable: true, }); let rectBox = new Konva.Rect({ x: boundingBoxPath[0].x, y: boundingBoxPath[0].y, width: boundingBox.width, height: boundingBox.height, stroke: 'red', // fill: 'grey', strokeWidth: 2, }); // 如果添加到group,则坐标的相对位置时基于group的位置 let shape: any = new Konva.Shape({ sceneFunc: function (context, shape) { context.beginPath(); let index = 0; for (const i of path) { const x = i.x; const y = i.y; if (index === 0) { context.moveTo(x, y); } else { context.lineTo(x, y); } // context.quadraticCurveTo(150, 100, 260, 170); 弧线 index++; } context.closePath(); // (!) Konva specific method, it is very important context.fillStrokeShape(shape); }, fill: fillColor, name, // stroke: 'white', // strokeWidth: 10, }); /* 向外扩大 start */ const extraPath = scalePolygon(path) as any[]; // 扩展后的轮廓线 const shape2: any = new Konva.Shape({ sceneFunc: function (context, shape) { context.beginPath(); let index = 0; for (const i of extraPath) { const x = i.x; const y = i.y; if (index === 0) { context.moveTo(x, y); } else { context.lineTo(x, y); } // context.quadraticCurveTo(150, 100, 260, 170); 弧线 index++; } context.closePath(); // (!) Konva specific method, it is very important context.fillStrokeShape(shape); }, // fill: fillColor, name, stroke: '#21BA45', strokeWidth: 2, }); /* 向外扩大 end */ // 先添加的边界框矩形,再添加的自定义图像, // 顺序和下面layerDragmove时获取children的顺序保持一致 group.add(rectBox); group.add(shape); group.add(shape2); return group; } } function layerDragmove(e: any) { const target = e.target; const targetRectAttr = target.children[0].attrs; // const targetShapeAttr = target.children[1].attrs; const diffX = target.attrs.x; const diffY = target.attrs.y; const taIndex = state.group.get(target._id); const ta = state.shapePath[taIndex]; const _boundingBox = ta.boundingBox; _boundingBox.min_x = targetRectAttr.x + diffX; _boundingBox.min_y = targetRectAttr.y + diffY; // _boundingBox.max_x += diffX; // _boundingBox.min_y += diffY; // _boundingBox.max_y += diffY; // state.layer.children.forEach(function (group: any) { // // do not check intersection with itself // // 不检查与自身相交 // if (group._id == target._id) { // return; // } // }); } function handleData() { for (const item of state.shapePath) { item.path = removeAngleRepeat(item.path || []); item.boundingBox = getBoundingBox(item.path || []); } } /** * 1.一条线上有多个点,只保留起点和终点 * 2.计算边距上的点 * 注意!path的最后一个点不与第一个点重合 */ function removeAngleRepeat(path: any[]) { let length = path.length; if (length >= 2) { let myMap = new Map(); // 计算A点两条邻边相交的点A' // 计算过A点垂直于边距线上的点,这样的点计算两个,也就是A点两条邻边上各一点 // let index = 1; for (const item of path) { let x1, y1, x2, y2; // let last_x, last_y, gap_1, gap_2; if (index < length) { x1 = item.x; y1 = item.y; x2 = path[index].x; y2 = path[index].y; // 边距 start1 // gap_1 = item.gap || 0; // if (index === 1) { // // 第一个点,另一条邻边是与最后一个点组成 // last_x = path[length - 1].x; // last_y = path[length - 1].y; // gap_2 = path[length - 1].gap; // } else { // last_x = path[index - 2].x; // last_y = path[index - 2].y; // gap_2 = path[index - 2].gap; // } // 边距 end1 } else { // 最后一个点连第一个点 x1 = item.x; y1 = item.y; x2 = path[0].x; y2 = path[0].y; // 边距 start2 // gap_1 = item.gap || 0; // gap_2 = path[index - 2].gap; // last_x = path[index - 2].x; // last_y = path[index - 2].y; // 边距 end2 } // toAngle:false弧度 true角度 let du = getAngle(x1, y1, x2, y2, true); item.du = du; if (!myMap.has(du)) { myMap.set(du, item); // 边距 start3 // const du2 = getAngle(x1, y1, last_x, last_y, true); // const gapParams = { // x1, // y1, // gap_1, // x2, // y2, // last_x, // last_y, // gap_2, // du1: du, // du2, // }; // handleGap(gapParams); // 边距 end3 } // console.log('du', du); index++; } let newArr: any[] = []; for (const value of myMap.values()) { newArr.push(value); } return newArr; } else { console.warn('小于两个点,不能形成夹角'); return path; } } /** * 计算从A点[x1,y1]到B点[x2,y2]的直线,与水平线形成的夹角 * 计算规则为以A为旋转点,将AB线顺时针旋转到-X轴形成的夹角 * @param {Object} x1 * @param {Object} y1 * @param {Object} x2 * @param {Object} y2 * @param {Boolean} toAngle 默认false【弧度值】,true【角度值】 */ function getAngle( x1: number, y1: number, x2: number, y2: number, toAngle = false ) { let x = Decimal.sub(x1, x2); let y = Decimal.sub(y1, y2); if (!x.toNumber() && !y.toNumber()) { return 0; } // 弧度 radian = 角度 * Math.PI / 180 // 角度 angle = 弧度 * 180 / Math.PI let res; // 角度值 // let angle = (180 + (Math.atan2(-y, -x) * 180) / Math.PI + 360) % 360; let _atan2 = Decimal.atan2(y.negated(), x.negated()); let _angle = Decimal.div(Decimal.mul(_atan2, 180), Math.PI); let angle = Decimal.mod(Decimal.add(180, _angle).plus(360), 360); res = Decimal.sub(360, angle); if (!toAngle) { // 弧度值 res = Decimal.mul(res, Math.PI).div(180); } return res.toNumber(); } // /** // * 计算边距 // * param里面包含3个点的坐标,及2条边距 // * A [x1,y1] 当前目标点 // * B [x2,y2] 在A点下一条邻边上的点 // * C [last_x,last_y] 在A点上一条邻边上的点 // * gap_1 AB上的边距; du1 AB与-X轴的夹角,传入角度值 // * gap_2 AC上的边距; du2 AC与-X轴的夹角,传入角度值 // */ // function handleGap(param: any) { // const { x1, y1, x2, y2, last_x, last_y, gap_1, gap_2, du1, du2 } = param; // // console.log('du1', du1); // } /** * 批量选择 */ function batchSelection(value: boolean) { if (value) { startSelected.value = true; } else { startSelected.value = false; state.shapePath.forEach((i) => (i.selected = true)); } } /** * 绘制网格线 */ function drawGrid() { let canvas: any = document.getElementById('canvas-grid'); let pen = canvas.getContext('2d'); // 绘制网格 const step = stageSize.gridGap; const h = stageSize.height; const w = stageSize.width; const w_l = w / step; const h_l = h / step; // 横着的线 for (let i = 0; i <= h_l; i++) { pen.beginPath(); pen.moveTo(0, i * step); pen.lineTo(w, i * step); pen.stroke(); } // 竖着的线 for (let i = 0; i <= w_l; i++) { pen.beginPath(); pen.moveTo(i * step, 0); pen.lineTo(i * step, h); pen.stroke(); } } </script> <template> <div class="konva-main-page container-height center"> <div class="column"> <q-toggle v-model="startSelected" label="批量选择" @update:model-value="batchSelection" /> </div> <div class="canvas-box"> <canvas id="canvas-grid" :width="stageSize.width" :height="stageSize.height" style="position: absolute" ></canvas> <div id="stage-container"></div> <!-- ======================== 复选框 ======================== --> <template v-for="(item, index) in state.shapePath" :key="index"> <q-checkbox v-model="item.selected" dense class="my-checkbox" v-if="startSelected && item.boundingBox" :style="{ left: item.boundingBox.min_x + 'px', top: item.boundingBox.min_y + 'px', }" /> </template> </div> </div> </template> <style lang="scss" scoped> .konva-main-page { } .canvas-box { box-sizing: border-box; width: 1200px; height: 800px; border: 1px solid #000; // background: pink; position: relative; } .my-checkbox { position: absolute; } </style>