<!-- * canvas 绘制动态路径 --> <script setup lang="ts"> import { ref, reactive, onMounted } from 'vue'; import { ComTitlePage } from 'src/components'; const img = ref<any>(new Image()); img.value.src = require('../icons/arrow.png'); const width = 900; const height = 600; const isShowGrid = ref(false); const step = 40; // 箭头相隔距离 const lineWidth = ref(16); // 线宽 const isRunning = ref(false); const bigIndex = ref(0); // 当前长线段的下标 const smallIndex = ref(0); // 当前小线段的下标 const carTranslateX = ref(0); const carTranslateY = ref(0); const carRotate = ref(0); const state = reactive({ canvasDom: null as HTMLCanvasElement | null, canvasCtx: null as CanvasRenderingContext2D | null, animationCanvasDom: null as HTMLCanvasElement | null, animationCanvasCtx: null as CanvasRenderingContext2D | null, data: [ { x: 400, y: 300 }, { x: 500, y: 300 }, { x: 500, y: 200 }, { x: 400, y: 200 }, { x: 300, y: 200 }, { x: 300, y: 400 }, { x: 500, y: 400 }, { x: 600, y: 400 }, { x: 600, y: 100 }, { x: 200, y: 100 }, { x: 200, y: 500 }, { x: 700, y: 500 }, { x: 700, y: 100 }, ], originRunData: [ { x: 400, y: 300 }, { x: 500, y: 300 }, { x: 500, y: 200 }, { x: 400, y: 200 }, { x: 300, y: 200 }, { x: 300, y: 400 }, { x: 500, y: 400 }, { x: 600, y: 400 }, ], runData: [], }); onMounted(() => { img.value.onload = function () { state.canvasDom = <HTMLCanvasElement>document.getElementById('canvas'); state.canvasCtx = state.canvasDom.getContext('2d'); state.animationCanvasDom = <HTMLCanvasElement>( document.getElementById('canvas-animation') ); state.animationCanvasCtx = state.animationCanvasDom.getContext('2d'); }; }); // 显示网格 function showGrid(value: boolean) { let canvas = <HTMLCanvasElement>document.getElementById('canvas-grid'); let pen = <CanvasRenderingContext2D>canvas.getContext('2d'); if (value) { drawGrid('#FD7013', width, height, pen); } else { pen.clearRect(0, 0, width, height); } } function onStart() { if (state.data.length < 2) return; // data里面至少有2个点才能连线 // data处理,把同一个放向上的点集合到一个点 console.log('原始数据', state.data); const data = setData(state.data); console.log('处理后', data); const ctx = <CanvasRenderingContext2D>state.canvasCtx; 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; ctx.save(); ctx.beginPath(); ctx.moveTo(x1, y1); ctx.lineTo(x2, y2); ctx.closePath(); ctx.lineWidth = lineWidth.value; ctx.lineCap = 'round'; ctx.lineJoin = 'round'; ctx.strokeStyle = '#78C0A8'; ctx.stroke(); ctx.restore(); const abs_x = Math.abs(A); const abs_y = Math.abs(B); const angle = Math.atan2(A, B); const theta = 0 - angle * (180 / Math.PI) - 90; // 求斜边长度 const size = Math.sqrt(abs_x * abs_x + abs_y * abs_y); // 把斜边分成了几段 const part = Number(parseInt(String(size / step))); const du = 90 - theta; for (let i = 0; i <= part; i++) { const part_c = step * i; const part_x = x1 + Math.sin(theta * (Math.PI / 180)) * part_c; const 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 = lineWidth.value * 2; ctx.drawImage( img.value, 0 - lineWidth.value, 0 - lineWidth.value, doubleLineWidth, doubleLineWidth ); ctx.restore(); } } } function onAnimation() { state.runData = setData(state.originRunData); console.log('runData ', state.runData); const ctx = <CanvasRenderingContext2D>state.animationCanvasCtx; const { runData } = state; if (runData.length < 2) return; // 这是一段长线段,动画需要把长段分成若干小段 const step = 2; // 动画每次绘制的步长 const arrowStep = 40; // 箭头相隔距离 const stepDiff = Math.floor(arrowStep / 2); // 每隔 stepDiff 个小段,绘制一个箭头 const x1 = runData[bigIndex.value].x; const y1 = runData[bigIndex.value].y; const x2 = runData[bigIndex.value + 1].x; const y2 = runData[bigIndex.value + 1].y; const A = x2 - x1; const B = y2 - y1; const angle = Math.atan2(B, A); // 两点间的弧度值 const c = Math.sqrt(Math.pow(A, 2) + Math.pow(B, 2)); // 斜边长度 const stepSize = Math.ceil(c / step); // 把斜边分为了几段 isRunning.value = true; window.requestAnimationFrame(() => draw(ctx, smallIndex.value, x1, y1, x2, y2, angle, stepSize, bigIndex.value) ); function draw( ctx: CanvasRenderingContext2D, i: number, x1: number, y1: number, x2: number, y2: number, angle: number, stepSize: number, index: number ) { if (isRunning.value) { const current_x1 = x1; const current_y1 = y1; let current_x2, current_y2; if (i === 0) { // 这里是覆盖了之前的路径,如果按分成的小段来画,中间连接不顺滑 current_x2 = x1 + Math.cos(angle) * ((i + 1) * step); current_y2 = y1 + Math.sin(angle) * ((i + 1) * step); } else if (i === stepSize - 1) { current_x2 = x2; current_y2 = y2; } else { current_x2 = x1 + Math.cos(angle) * ((i + 1) * step); current_y2 = y1 + Math.sin(angle) * ((i + 1) * step); } ctx.save(); ctx.beginPath(); ctx.moveTo(current_x1, current_y1); ctx.lineTo(current_x2, current_y2); // 小车的运动 // 减去小车宽高的一半 carTranslateX.value = current_x2 - 20; carTranslateY.value = current_y2 - 20; carRotate.value = (angle * 180) / Math.PI; // 箭头路径 if (i % stepDiff === 0) { const arrowCtx = <CanvasRenderingContext2D>state.canvasCtx; const doubleLineWidth = lineWidth.value * 2; // 旋转中心 const center = { x: current_x2, y: current_y2, }; const offset = rotateOriginOffset(width, center, angle); arrowCtx.save(); arrowCtx.translate(offset.x, offset.y); // 改变旋转中心 arrowCtx.rotate(angle); arrowCtx.drawImage( img.value, current_x2 - lineWidth.value, current_y2 - lineWidth.value, doubleLineWidth, doubleLineWidth ); // arrowCtx.strokeRect( // current_x2 - lineWidth.value, // current_y2 - lineWidth.value, // doubleLineWidth, // doubleLineWidth // ); arrowCtx.restore(); } ctx.lineWidth = lineWidth.value; ctx.lineCap = 'round'; ctx.lineJoin = 'round'; ctx.strokeStyle = '#EA7070'; ctx.stroke(); ctx.restore(); i++; if (i < stepSize) { window.requestAnimationFrame(() => draw(ctx, i, x1, y1, x2, y2, angle, stepSize, index) ); } else { if (index === runData.length - 2) { isRunning.value = false; console.log('运动结束'); } else { const x1 = runData[index + 1].x; const y1 = runData[index + 1].y; const x2 = runData[index + 2].x; const y2 = runData[index + 2].y; const A = x2 - x1; const B = y2 - y1; const angle = Math.atan2(B, A); // 两点间的弧度值 const c = Math.sqrt(Math.pow(A, 2) + Math.pow(B, 2)); // 斜边长度 const stepSize = Math.ceil(c / step); // 把斜边分为了几段 draw(ctx, 0, x1, y1, x2, y2, angle, stepSize, index + 1); } } } else { bigIndex.value = index; smallIndex.value = i; console.log('停止>>>', '当前长线段的下标', index, '当前小线段的下标', i); } } } function onStop() { isRunning.value = false; } // data处理,把同一个放向上的点集合到一个点 function setData(data: any[]) { let DATA = JSON.parse(JSON.stringify(data)); if (data.length < 2) return DATA; let arr = [] as any[]; for (let i = 0; i < DATA.length; i++) { const x1 = DATA[i].x; const y1 = DATA[i].y; if (i === 0) { // 第一个点要添加进去 arr.push({ x: x1, y: y1 }); } else if (i === DATA.length - 1) { // 最后一个点要添加进去 arr.push({ x: x1, y: y1 }); } else { const last_x = DATA[i - 1].x; const last_y = DATA[i - 1].y; const next_x = DATA[i + 1].x; const next_y = DATA[i + 1].y; const A = x1 - last_x; const B = y1 - last_y; const angle = Math.atan2(B, A); const C = next_x - x1; const D = next_y - y1; const angle2 = Math.atan2(D, C); // console.log('当前', x1, y1, angle, angle2); if (angle !== angle2 && !(x1 === next_x && y1 === next_y)) { arr.push({ x: x1, y: y1 }); } } } return arr; } function clearArrowCanvas() { const ctx = <CanvasRenderingContext2D>state.canvasCtx; ctx.clearRect(0, 0, width, height); } // 绘制网格 function drawGrid( color: string, w: number, h: number, pen: CanvasRenderingContext2D ) { const step = 100; const w_l = w / step; const h_l = h / step; pen.save(); // 横着的线 for (let i = 0; i <= h_l; i++) { pen.beginPath(); pen.strokeStyle = color; 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(); } pen.restore(); } // 重载 function onReload() { document.location.reload(); } /** * canvas旋转中心偏移值 * @param width canvas宽 * @param center 中心坐标点 center:{x:100,y:100} * @param arc 旋转的弧度值 */ function rotateOriginOffset( width: number, center: { x: number; y: number }, arc: any ) { const r1 = width - center.x; const xRes1 = Math.cos(arc) * r1; const yRes1 = Math.sin(arc) * r1; const x1 = center.x + xRes1; const y1 = center.y + yRes1; const x0 = width; const y0 = center.y; const c0 = Math.sqrt(Math.pow(x0, 2) + Math.pow(y0, 2)); const arc0 = Math.atan2(y0, x0); const arc_0 = arc0 + arc; const y2 = Math.sin(arc_0) * c0; const x2 = y2 / Math.tan(arc_0); const xLength = x1 - x2; const yLength = y1 - y2; return { x: xLength, y: yLength, }; } </script> <template> <div class="fit"> <com-title-page title="canvas 绘制动态路径" /> <div class="btns q-my-sm"> <div class="q-gutter-sm"> <q-btn color="primary" label="重载" @click="onReload" /> <q-toggle v-model="isShowGrid" label="网格" @update:model-value="showGrid" /> <q-btn color="primary" label="显示箭头静态路径" @click="onStart" /> <q-btn color="primary" label="清除箭头静态路径" @click="clearArrowCanvas" /> <q-btn style="background: #ea7070; color: white" label="动画" @click="onAnimation" /> <q-btn style="background: #ea7070; color: white" label="停止" @click="onStop" /> <q-btn :loading="isRunning" style="background: #ea7070; color: white" label="状态" /> </div> </div> <div class="content"> <div class="canvas-box" :style="{ width: width + 'px', height: height + 'px' }" > <canvas id="canvas-animation" :width="width" :height="height" style="position: absolute; top: 0; left: 0" ></canvas> <canvas id="canvas" :width="width" :height="height" style="position: absolute; top: 0; left: 0" ></canvas> <canvas id="canvas-grid" :width="width" :height="height" style="position: absolute; top: 0; left: 0" ></canvas> <!-- 小车图标图层 --> <div :style="{ position: 'absolute', top: 0, left: 0, width: width + 'px', height: height + 'px', }" > <div class="car" :style="{ transform: `translate(${carTranslateX}px, ${carTranslateY}px) rotate(${carRotate}deg)`, }" ></div> </div> </div> </div> </div> </template> <style lang="scss" scoped> .canvas-box { position: relative; box-sizing: border-box; border: 1px solid red; } .car { position: absolute; top: 0; left: 0; width: 40px; height: 40px; background-image: url('../icons/car.svg'); background-size: 100% 100%; } // .btns { // display: flex; // flex-flow: row wrap; // } // .btns > div { // margin-right: 10px; // } </style>