Commit fe24dabc authored by hucy's avatar hucy

canvas 绘制动态路径箭头

parent 5b4aac46
## 2022-11-14
- canvas 绘制动态路径箭头
## 2022-11-08
- requestAnimationFrame 学习
- promise 学习
- canvas 绘制路径箭头
...@@ -4,6 +4,7 @@ import MyForm from './MyForm.vue'; ...@@ -4,6 +4,7 @@ import MyForm from './MyForm.vue';
import MyTooltip from './MyTooltip.vue'; import MyTooltip from './MyTooltip.vue';
import CanvasStaticRoute from './canvas-static-route/CanvasStaticRoute.vue'; import CanvasStaticRoute from './canvas-static-route/CanvasStaticRoute.vue';
import ChartCarouselWarp from './chart-carousel-warp/ChartCarouselWarp.vue'; import ChartCarouselWarp from './chart-carousel-warp/ChartCarouselWarp.vue';
import TitlePage from './title-page/TitlePage.vue';
export { export {
DateTimePick as ComDateTimePick, DateTimePick as ComDateTimePick,
...@@ -12,6 +13,7 @@ export { ...@@ -12,6 +13,7 @@ export {
MyTooltip as ComTooltip, MyTooltip as ComTooltip,
CanvasStaticRoute as ComCanvasStaticRoute, CanvasStaticRoute as ComCanvasStaticRoute,
ChartCarouselWarp as ComChartCarouselWarp, ChartCarouselWarp as ComChartCarouselWarp,
TitlePage as ComTitlePage,
}; };
export default { export default {
...@@ -21,4 +23,5 @@ export default { ...@@ -21,4 +23,5 @@ export default {
MyTooltip, MyTooltip,
CanvasStaticRoute, CanvasStaticRoute,
ChartCarouselWarp, ChartCarouselWarp,
TitlePage,
}; };
<!--
* 通用标题栏
-->
<script setup lang="ts">
interface Props {
title: string;
}
const props = withDefaults(defineProps<Props>(), {
title: '',
});
</script>
<template>
<div class="com-title">{{ props.title }}</div>
</template>
<style lang="scss" scoped>
.com-title {
width: 100%;
min-height: 50px;
padding: 0 5px;
color: #5e4f4c;
font-family: 'Gill Sans Extrabold', sans-serif;
box-sizing: border-box;
border-bottom: 4px solid #5e4f4c;
font-size: 20px;
display: flex;
flex-flow: row wrap;
align-items: center;
justify-content: flex-start;
}
</style>
...@@ -12,11 +12,14 @@ export default { ...@@ -12,11 +12,14 @@ export default {
// import MorphBox from './element/MorphBox.vue'; // import MorphBox from './element/MorphBox.vue';
// import CarouselWarp from './element/CarouselWarp.vue'; // import CarouselWarp from './element/CarouselWarp.vue';
// import DataResponsive from './element/DataResponsive.vue'; // import DataResponsive from './element/DataResponsive.vue';
// import CanvasStudy from './element/CanvasStudyCopy1.vue'; // import CanvasStudy from './element/CanvasStudy.vue';
// import CanvasStudy from './element/CanvasCurve.vue'; // import CanvasStudy from './element/CanvasCurve.vue';
import SubComponents from './element/AsyncAwait.vue'; // import SubComponents from './element/AsyncAwait.vue';
// import SubComponents from './element/RequestAnimationFrame.vue';
// import SubComponents from './element/equipment/IndexPage.vue'; // import SubComponents from './element/equipment/IndexPage.vue';
import SubComponents from './element/canvas-router-map/CanvasRouterMap.vue'; // canvas 绘制动态路径
</script> </script>
<template> <template>
<div class="page6 container-height"> <div class="page6 container-height">
......
...@@ -70,8 +70,10 @@ function draw(ctx: any, r: number, pen: any) { ...@@ -70,8 +70,10 @@ function draw(ctx: any, r: number, pen: any) {
const centerY2 = center.y; const centerY2 = center.y;
ctx.beginPath(); ctx.beginPath();
ctx.arc(centerX2, centerY2, 4, 0, Math.PI * 2); ctx.arc(centerX2, centerY2, 4, 0, Math.PI * 2);
ctx.strokeStyle = '#FC4422'; ctx.fillStyle = '#78C0A8';
ctx.stroke(); ctx.fill();
// ctx.strokeStyle = '#FC4422';
// ctx.stroke();
ctx.restore(); ctx.restore();
// 圆弧上点的坐标 // 圆弧上点的坐标
......
<!--
* RequestAnimationFrame 学习
-->
<script setup lang="ts">
import { onMounted } from 'vue';
onMounted(() => {
const element = document.getElementById(
'some-element-you-want-to-animate'
) as HTMLElement;
let translateX = 0;
// 每秒执行30次,则执行1次需要的时间1/30(单位:秒)
function step() {
translateX++;
if (translateX > 30) {
return;
} else {
element.style.transform = 'translateX(' + translateX + 'px)';
element.innerText = String(translateX);
setTimeout(() => {
window.requestAnimationFrame(step);
}, (1 / 30) * 1000);
}
}
window.requestAnimationFrame(step);
});
</script>
<template>
<div>
<div>RequestAnimationFrame</div>
<div id="some-element-you-want-to-animate"></div>
</div>
</template>
<style lang="scss" scoped>
#some-element-you-want-to-animate {
width: 200px;
height: 100px;
background-color: pink;
}
</style>
<!--
* 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>
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1667983950515" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="2606" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200"><path d="M512 192c212.064 0 320 107.936 320 320s-107.936 320-320 320S192 724.064 192 512 299.936 192 512 192z m37.024 216.128a32 32 0 0 0 0 45.248l22.912 22.944h-184.288a32 32 0 1 0 0 64h187.52l-26.144 26.176a32 32 0 0 0 45.248 45.28l67.904-67.904a32 32 0 0 0 7.296-33.92 32 32 0 0 0-7.296-33.952l-67.904-67.872a32 32 0 0 0-45.248 0z" fill="#78C0A8" p-id="2607"></path><path d="M512 32C191.936 32 32 191.936 32 512c0 320.064 159.936 480 480 480 320.064 0 480-159.936 480-480C992 191.936 832.064 32 512 32z m0 64c284.736 0 416 131.264 416 416s-131.264 416-416 416S96 796.736 96 512 227.264 96 512 96z" fill="#78C0A8" fill-opacity=".148" p-id="2608"></path></svg>
\ No newline at end of file
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment