<!-- * canvas静态路径绘制 --> <script lang="ts" setup> import { onMounted, ref, reactive, onBeforeUnmount, watch, nextTick, } from 'vue'; import { isEmpty, cloneDeep } from 'src/common/utils'; import { CanvasData } from '../models'; interface Props { width: number; height: number; data: CanvasData[]; border?: boolean; center?: boolean; // 是否居将内容居中展示 drag?: boolean; // 是否可拖动画布 zoom?: boolean; // 是否开启鼠标滚轮缩放 bgUrl?: string; bgWidth?: number; bgHeight?: number; } const props = withDefaults(defineProps<Props>(), { width: 700, height: 700, data: () => [], border: true, center: false, drag: false, zoom: false, bgUrl: '', bgWidth: 0, bgHeight: 0, }); const canvasDom = ref<any>(null); const boxDom = ref<any>(null); const img = ref<any>(new Image()); img.value.src = require('./imgs/arrow.png'); const step = 20; const defaultLineWidth = 10; // 线条宽度,默认为10 const firstObserver = ref(1); // 初始化时的尺寸变化 const timer = ref<any>(null); // 防抖 const wheelDelta = ref<number>(1); // 放大的倍数,初始值1,既不放大,也不缩小 const lastWheelDelta = ref<number>(1); const wheelDeltaStep = 0.2; // 鼠标每滚动一下,放大的倍数增长 const cursor = ref('default'); // 鼠标样式 const state = reactive({ ctx: null as any, lineWidth: defaultLineWidth, styBasic: { lineWidth: defaultLineWidth, strokeStyle: '#00BB52', lineJoin: 'round', lineCap: 'round', } as any, observer: null as any, // 监听元素尺寸变化 translateX: 0, translateY: 0, lastTranslateX: 0, lastTranslateY: 0, canvasW: props.bgWidth || props.width, canvasH: props.bgHeight || props.height, mousePointX: 0, // 鼠标缩放点的位置 mousePointY: 0, // mousePointScaleX: 0, // 鼠标乘以wheelDelta的位置 // mousePointScaleY: 0, }); watch( () => props.data, () => { dataChange(); } ); watch( () => [props.bgWidth, props.bgHeight], ([newW, newH]) => { bgSizeChange(newW, newH); } ); onMounted(() => { img.value.onload = function () { initCanvas(); getCanvas(); }; }); onBeforeUnmount(() => { offDisconnect(); }); function innitCtx() { let ctx = canvasDom.value.getContext('2d'); state.ctx = ctx; // 监听元素尺寸变化 offDisconnect(); state.observer = new ResizeObserver(handleResize); state.observer.observe(boxDom.value); } function initCanvas() { if (canvasDom.value) { innitCtx(); } else { setTimeout(() => { initCanvas(); }, 1000); } } function getCanvas(type = '') { let DATA = cloneDeep(props.data); DATA.map((item: any) => { item.data.map((ele: any) => { ele.x *= wheelDelta.value; ele.y *= wheelDelta.value; }); }); console.log('数据', DATA); // 重置画布偏移位置 // state.translateX = 0; // state.translateY = 0; // state.lastTranslateX = 0; // state.lastTranslateY = 0; const reData1 = DATA.map((item: any) => { return item.data; }); let reData2 = [] as any[]; reData1.map((item: any) => { reData2.push(...item); }); let resData3 = [] as any[]; let arrX = [] as number[]; let arrY = [] as number[]; reData2.map((item: any) => { resData3.push(item.x, item.y); arrX.push(item.x); arrY.push(item.y); }); // 将内容居中展示 if (type === 'onmousewheel') { // 当鼠标滚动放大缩小时不做居中展示处理,要另外计算以鼠标缩放点为中心的偏移值 } else { if (props.center) { const maxX = Math.max(...arrX); // x的最大值 const minX = Math.min(...arrX); // x的最小值 const maxY = Math.max(...arrY); // y的最大值 const minY = Math.min(...arrY); // y的最大值 const absX = maxX - minX; const absY = maxY - minY; const relativeX = absX / 2; // x的相对中心坐标 const relativeY = absY / 2; // y的相对中心坐标 const viewCenterX = props.width / 2; // 可视区域的中心x坐标 const viewCenterY = props.height / 2; // 可视区域的中心y坐标 const diffX = viewCenterX - relativeX - minX; // 中心点坐标的差值 const diffY = 0 - (viewCenterY - relativeY - minY); state.translateX = diffX; state.translateY = 0 - diffY; state.lastTranslateX = state.translateX; state.lastTranslateY = state.translateY; } } clearCanvas(); if (props.bgWidth > 0 && props.bgHeight > 0) { } else { const max = Math.max(...resData3); // 取最大数据作为cnavas的宽高 state.canvasW = max + 100; // 额外空间,要考虑算上线条的宽度 state.canvasH = max + 100; // 额外空间,要考虑算上线条的宽度 } nextTick(() => { DATA.map((item: CanvasData) => { drawCanvaIten(item); }); }); } function drawCanvaIten(data: CanvasData) { const _data = data.data; const style = data.style as any; // data里面至少有2个点才能连线 if (_data.length > 1) { for (let n = 0; n < _data.length - 1; n++) { const x1 = _data[n].x; const y1 = _data[n].y; const x2 = _data[n + 1].x; const y2 = _data[n + 1].y; const A = y1 - y2; const B = x1 - x2; const ctx = state.ctx; let _step: number = step; // 箭头间相隔的间距 // 设置样式 state.lineWidth = defaultLineWidth; for (const key in state.styBasic) { ctx[key] = state.styBasic[key]; } if (!isEmpty(style)) { state.lineWidth = style.lineWidth || defaultLineWidth; _step = style.step || step; for (const key in style) { ctx[key] = style[key]; } } ctx.save(); ctx.beginPath(); ctx.moveTo(x1, y1); ctx.lineTo(x2, y2); ctx.closePath(); ctx.stroke(); ctx.restore(); let abs_x = Math.abs(A); let abs_y = Math.abs(B); let angle = Math.atan2(A, B); let theta = 0 - angle * (180 / Math.PI) - 90; // 求斜边长度 let size = Math.sqrt(abs_x * abs_x + abs_y * abs_y); // 把斜边分成了几段 let part = Number(parseInt(String(size / _step))); let du = 90 - theta; // console.log('斜边长度', size, '斜边分为了几段', part, '旋转度', du); // let X = Math.sin(theta * (Math.PI / 180)) * size; // let Y = Math.cos(theta * (Math.PI / 180)) * size; // console.log('长', X, '高', Y); for (let i = 0; i <= part; i++) { let part_c = _step * i; let part_x = x1 + Math.sin(theta * (Math.PI / 180)) * part_c; let part_y = y1 + Math.cos(theta * (Math.PI / 180)) * part_c; ctx.save(); ctx.translate(part_x, part_y); ctx.rotate((Math.PI / 180) * du); const doubleLineWidth = state.lineWidth * 2; ctx.drawImage( img.value, 0 - state.lineWidth, 0 - state.lineWidth, doubleLineWidth, doubleLineWidth ); ctx.restore(); } } } } function dataChange() { console.log('数据变了'); if (state.ctx) { clearCanvas(); getCanvas(); } } function onMove(ev: any) { if (!props.drag) return; state.translateX = ev.offset.x + state.lastTranslateX; state.translateY = ev.offset.y + state.lastTranslateY; if (ev.isFinal) { state.lastTranslateX = state.translateX; state.lastTranslateY = state.translateY; } } function clearCanvas() { if (state.ctx) { state.ctx.clearRect(0, 0, state.canvasW, state.canvasW); } } function handleResize() { if (firstObserver.value > 1) { console.log('尺寸变了'); getCanvas(); } firstObserver.value++; } function bgSizeChange(w: any, h: any) { console.log('背景图片的尺寸变了'); state.canvasW = w * wheelDelta.value; state.canvasH = h * wheelDelta.value; getCanvas(); } // 缩放 function onMousewheel(ev: any) { if (!props.zoom) return; cursor.value = 'progress'; state.mousePointX = ev.layerX; state.mousePointY = ev.layerY; if (ev.wheelDelta > 0) { wheelDelta.value += wheelDeltaStep; console.log('放大'); } else { // 如果是0.51 则 0.51-0.2 = 0.31 ,所以缩小的最小倍数是0.3倍左右 wheelDelta.value = wheelDelta.value < 0.5 ? 0.3 : wheelDelta.value - wheelDeltaStep; console.log('缩小'); } debounce(() => { const relativeX = state.mousePointX - state.translateX; const relativeY = state.mousePointY - state.translateY; const diffScale = wheelDelta.value / lastWheelDelta.value; const relativeScaleX = relativeX * diffScale; const relativeScaleY = relativeY * diffScale; // state.mousePointScaleX = relativeScaleX + state.translateX; // state.mousePointScaleY = relativeScaleY + state.translateY; const diffTranslateX = relativeScaleX - relativeX; const diffTranslateY = relativeScaleY - relativeY; state.translateX -= diffTranslateX; state.translateY -= diffTranslateY; // console.log( // 'x=', // state.mousePointX, // 'y=', // state.mousePointY, // 'relativeX=', // relativeX, // 'relativeY=', // relativeY, // 'relativeScaleX=', // relativeScaleX, // 'relativeScaleY=', // relativeScaleY, // 'diffTranslateX=', // diffTranslateX, // 'diffTranslateY=', // diffTranslateY // ); console.log('倍数', wheelDelta.value); lastWheelDelta.value = wheelDelta.value; state.lastTranslateX = state.translateX; state.lastTranslateY = state.translateY; // 如果设置了背景图片尺寸 if (props.bgWidth > 0 && props.bgHeight > 0) { state.canvasW = props.bgWidth * wheelDelta.value; state.canvasH = props.bgHeight * wheelDelta.value; } getCanvas('onmousewheel'); cursor.value = 'default'; }, 500); } function offDisconnect() { if (state.observer) { state.observer.disconnect(); state.observer = null; } } // 防抖 // 触发事件 n 秒后执行函数,如果在 n 秒内又触发了事件,则会重新计算函数执行时间。 function debounce(func: any, time: number) { // setTimeout需是全局变量 if (timer.value) { clearTimeout(timer.value); timer.value = null; } timer.value = setTimeout(() => { func(); clearTimeout(timer.value); timer.value = null; }, time); } </script> <template> <div class="hcy-canvas-box" :style="{ width: props.width + 'px', height: props.height + 'px', cursor: cursor, }" :class="[props.border ? 'has-border' : 'hasno-border']" ref="boxDom" @mousewheel.prevent="onMousewheel" > <!-- <div class="text-box" :style="{ top: state.mousePointY + 'px', left: state.mousePointX + 'px' }" ></div> --> <canvas ref="canvasDom" id="canvas" :width="state.canvasW" :height="state.canvasH" v-touch-pan.prevent.mouse="onMove" :style="{ transform: `translate(${state.translateX}px, ${state.translateY}px)`, backgroundImage: `url(${props.bgUrl})`, }" ></canvas> </div> </template> <style lang="scss" scoped> .text-box { width: 10px; height: 10px; background-color: red; position: absolute; } .hcy-canvas-box { position: relative; box-sizing: content-box; overflow: hidden; } .has-border { border: 1px solid; } .hasno-border { border: 1px solid transparent; } #canvas { background-color: rgb(248, 246, 241); background-size: 100% 100%; } </style>