IndexPage2.vue 11.5 KB
Newer Older
hucy's avatar
hucy committed
1 2 3 4 5 6
<!--
 * @FileDescription: vue-konva
 * @Author: hcy
 * @Date: 2023-03-29
-->
<script setup lang="ts">
hucy's avatar
hucy committed
7
import { ref, reactive, onMounted } from 'vue';
hucy's avatar
hucy committed
8
import { getBoundingBox } from 'src/common/utils';
hucy's avatar
hucy committed
9
import { scalePolygon } from './utils';
hucy's avatar
hucy committed
10 11 12
import Konva from 'konva';
import Decimal from 'decimal.js';

hucy's avatar
hucy committed
13
const startSelected = ref(true);
hucy's avatar
hucy committed
14

hucy's avatar
hucy committed
15 16 17
const state = reactive({
  stage: null as any,
  layer: null as any,
hucy's avatar
hucy committed
18 19
  group: new Map(),
  myMap: new Map(),
hucy's avatar
hucy committed
20 21 22 23 24 25 26
  // shape: null as any,
  //   boundingBox: null as any,
  //   box: null as any,
  shapePath: [
    {
      name: 'test1',
      color: '#00D2FF',
hucy's avatar
hucy committed
27
      selected: true,
hucy's avatar
hucy committed
28
      path: [
hucy's avatar
hucy committed
29 30 31 32
        { x: 100, y: 100, gap: 100 },
        { x: 200, y: 100, gap: 100 },
        { x: 200, y: 200, gap: 100 },
        { x: 0, y: 300, gap: 100 },
hucy's avatar
hucy committed
33 34
      ],
    },
hucy's avatar
hucy committed
35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67
    // {
    //   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 },
    //   ],
    // },
hucy's avatar
hucy committed
68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94
  ] 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);

hucy's avatar
hucy committed
95
  let index = 0;
hucy's avatar
hucy committed
96 97 98 99
  for (const item of state.shapePath) {
    let shape = createShape(item);
    if (shape) {
      state.layer.add(shape);
hucy's avatar
hucy committed
100 101
      state.group.set(shape._id, index); // 储存 state.shapePath的下标
      state.myMap.set(item.name, shape._id);
hucy's avatar
hucy committed
102
    }
hucy's avatar
hucy committed
103 104

    index++;
hucy's avatar
hucy committed
105 106 107 108 109 110 111 112 113
  }

  state.layer.on('dragmove', layerDragmove);
});

function createShape(itemData: any) {
  const path = itemData.path || [];
  const fillColor = itemData.color || '#000000';
  const name = itemData.name;
hucy's avatar
hucy committed
114
  const boundingBox = itemData.boundingBox || {};
hucy's avatar
hucy committed
115 116 117 118 119 120
  const boundingBoxPath = itemData.boundingBox?.path || [];

  if (boundingBoxPath.length < 2) {
    console.warn('小于两个点,不能构成多边形,没有边界框');
  } else {
    let group = new Konva.Group({
hucy's avatar
hucy committed
121 122
      x: 0,
      y: 0,
hucy's avatar
hucy committed
123 124 125
      draggable: true,
    });

hucy's avatar
hucy committed
126 127 128 129 130 131 132
    let rectBox = new Konva.Rect({
      x: boundingBoxPath[0].x,
      y: boundingBoxPath[0].y,
      width: boundingBox.width,
      height: boundingBox.height,
      stroke: 'red',
      // fill: 'grey',
hucy's avatar
hucy committed
133
      strokeWidth: 2,
hucy's avatar
hucy committed
134
    });
hucy's avatar
hucy committed
135

hucy's avatar
hucy committed
136
    // 如果添加到group,则坐标的相对位置时基于group的位置
hucy's avatar
hucy committed
137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161
    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,
    });
hucy's avatar
hucy committed
162

hucy's avatar
hucy committed
163
    /* 向外扩大 start */
hucy's avatar
hucy committed
164
    const extraPath = scalePolygon(path) as any[];
hucy's avatar
hucy committed
165
    // 扩展后的轮廓线
hucy's avatar
hucy committed
166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190
    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,
    });
hucy's avatar
hucy committed
191
    /* 向外扩大 end */
hucy's avatar
hucy committed
192 193 194

    // 先添加的边界框矩形,再添加的自定义图像,
    // 顺序和下面layerDragmove时获取children的顺序保持一致
hucy's avatar
hucy committed
195
    group.add(rectBox);
hucy's avatar
hucy committed
196 197
    group.add(shape);

hucy's avatar
hucy committed
198 199
    group.add(shape2);

hucy's avatar
hucy committed
200 201 202 203
    return group;
  }
}
function layerDragmove(e: any) {
hucy's avatar
hucy committed
204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227
  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;
  //   }
  // });
hucy's avatar
hucy committed
228 229 230 231 232 233 234 235 236
}

function handleData() {
  for (const item of state.shapePath) {
    item.path = removeAngleRepeat(item.path || []);
    item.boundingBox = getBoundingBox(item.path || []);
  }
}

hucy's avatar
hucy committed
237 238 239 240 241
/**
 * 1.一条线上有多个点,只保留起点和终点
 * 2.计算边距上的点
 * 注意!path的最后一个点不与第一个点重合
 */
hucy's avatar
hucy committed
242 243 244 245 246 247
function removeAngleRepeat(path: any[]) {
  let length = path.length;

  if (length >= 2) {
    let myMap = new Map();

hucy's avatar
hucy committed
248 249 250
    // 计算A点两条邻边相交的点A'
    // 计算过A点垂直于边距线上的点,这样的点计算两个,也就是A点两条邻边上各一点
    //
hucy's avatar
hucy committed
251 252 253
    let index = 1;
    for (const item of path) {
      let x1, y1, x2, y2;
hucy's avatar
hucy committed
254
      // let last_x, last_y, gap_1, gap_2;
hucy's avatar
hucy committed
255 256 257 258 259
      if (index < length) {
        x1 = item.x;
        y1 = item.y;
        x2 = path[index].x;
        y2 = path[index].y;
hucy's avatar
hucy committed
260 261 262 263 264 265 266 267 268 269 270 271 272 273

        // 边距 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
hucy's avatar
hucy committed
274 275 276 277 278 279
      } else {
        // 最后一个点连第一个点
        x1 = item.x;
        y1 = item.y;
        x2 = path[0].x;
        y2 = path[0].y;
hucy's avatar
hucy committed
280 281 282 283 284 285 286

        // 边距 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
hucy's avatar
hucy committed
287
      }
hucy's avatar
hucy committed
288
      // toAngle:false弧度 true角度
hucy's avatar
hucy committed
289
      let du = getAngle(x1, y1, x2, y2, true);
hucy's avatar
hucy committed
290
      item.du = du;
hucy's avatar
hucy committed
291 292 293

      if (!myMap.has(du)) {
        myMap.set(du, item);
hucy's avatar
hucy committed
294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310

        // 边距 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
hucy's avatar
hucy committed
311 312 313 314 315 316 317 318 319 320 321 322 323
      }

      // console.log('du', du);
      index++;
    }

    let newArr: any[] = [];
    for (const value of myMap.values()) {
      newArr.push(value);
    }

    return newArr;
  } else {
hucy's avatar
hucy committed
324
    console.warn('小于两个点,不能形成夹角');
hucy's avatar
hucy committed
325 326 327 328 329
    return path;
  }
}

/**
hucy's avatar
hucy committed
330 331
 * 计算从A点[x1,y1]到B点[x2,y2]的直线,与水平线形成的夹角
 * 计算规则为以A为旋转点,将AB线顺时针旋转到-X轴形成的夹角
hucy's avatar
hucy committed
332 333 334 335
 * @param {Object} x1
 * @param {Object} y1
 * @param {Object} x2
 * @param {Object} y2
hucy's avatar
hucy committed
336
 * @param {Boolean} toAngle 默认false【弧度值】,true【角度值】
hucy's avatar
hucy committed
337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355
 */
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;

hucy's avatar
hucy committed
356
  // 角度值
hucy's avatar
hucy committed
357 358 359 360 361 362 363 364
  // 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) {
hucy's avatar
hucy committed
365
    // 弧度值
hucy's avatar
hucy committed
366 367 368 369 370
    res = Decimal.mul(res, Math.PI).div(180);
  }
  return res.toNumber();
}

hucy's avatar
hucy committed
371 372 373 374 375 376 377 378 379
// /**
//  * 计算边距
//  * 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轴的夹角,传入角度值
//  */
hucy's avatar
hucy committed
380 381 382 383 384
// function handleGap(param: any) {
//   const { x1, y1, x2, y2, last_x, last_y, gap_1, gap_2, du1, du2 } = param;
//   // console.log('du1', du1);
// }

hucy's avatar
hucy committed
385 386 387
/**
 * 批量选择
 */
hucy's avatar
hucy committed
388 389 390 391 392 393 394 395 396
function batchSelection(value: boolean) {
  if (value) {
    startSelected.value = true;
  } else {
    startSelected.value = false;
    state.shapePath.forEach((i) => (i.selected = true));
  }
}

hucy's avatar
hucy committed
397 398 399
/**
 * 绘制网格线
 */
hucy's avatar
hucy committed
400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428
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">
hucy's avatar
hucy committed
429 430 431 432 433 434 435
    <div class="column">
      <q-toggle
        v-model="startSelected"
        label="批量选择"
        @update:model-value="batchSelection"
      />
    </div>
hucy's avatar
hucy committed
436 437 438 439 440 441 442 443
    <div class="canvas-box">
      <canvas
        id="canvas-grid"
        :width="stageSize.width"
        :height="stageSize.height"
        style="position: absolute"
      ></canvas>
      <div id="stage-container"></div>
hucy's avatar
hucy committed
444
      <!-- ======================== 复选框 ======================== -->
hucy's avatar
hucy committed
445 446 447 448 449 450 451 452 453 454 455 456
      <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>
hucy's avatar
hucy committed
457 458 459 460 461 462 463 464 465 466 467 468
    </div>
  </div>
</template>

<style lang="scss" scoped>
.konva-main-page {
}
.canvas-box {
  box-sizing: border-box;
  width: 1200px;
  height: 800px;
  border: 1px solid #000;
hucy's avatar
hucy committed
469
  // background: pink;
hucy's avatar
hucy committed
470 471 472 473
  position: relative;
}
.my-checkbox {
  position: absolute;
hucy's avatar
hucy committed
474 475
}
</style>