vue实现画笔回放canvas转视频播放功能
作者:禾小毅
这篇文章主要介绍了vue实现画笔回放,canvas转视频播放功能,本文通过实例代码给大家介绍的非常详细,对大家的学习或工作具有一定的参考借鉴价值,需要的朋友可以参考下
示例图:
一、vue2版本
<template> <div class="canvas-video"> <canvas ref="myCanvasByVideo" class="myCanvas" id="myCanvasByVideo" :width="width" :height="height" ></canvas> <div class="btnDiv"> <div v-if="!isPlayVideo && !isStartVideo" class="playback" @click.stop="playVideo" > <div>回放</div> <img src="@/assets/image/play.png" alt="" /> </div> <div v-if="isPlayVideo && isStartVideo" class="playback" @click.stop="pauseVideo" > <div>暂停</div> <img src="@/assets/image/pause.png" alt="" /> </div> <div v-if="!isPlayVideo && isStartVideo" class="playback" @click.stop="continueVideo" > <div>继续</div> <img src="@/assets/image/play.png" alt="" /> </div> <div class="rocket"> <img v-show="isRocket" src="@/assets/image/rocket.png" alt="" @click="playRocket" /> <img v-show="!isRocket" src="@/assets/image/rocket_noChoose.png" alt="" @click="playRocket" /> </div> <div class="mySlider"> <el-slider v-model="nowTime" :max="allTime" @change="changeVideoSilder" ></el-slider> </div> <div class="myTime"> {{ getFormatTime(nowTime) }} / {{ getFormatTime(allTime) }} </div> <div class="mySpeed"> <div @click.stop="isShowSpeedBox = !isShowSpeedBox"> {{ speedList.filter((item) => item.value === nowSpeed)[0].name }} </div> <div class="speedList" v-show="isShowSpeedBox"> <div class="speedItem" :class="item.value === nowSpeed ? 'active' : ''" v-for="(item, index) in speedList" :key="index" @click.stop="changeSpeed(item.value)" > {{ item.name }} </div> </div> </div> </div> </div> </template> <script> export default { name: "canvasVideo", components: {}, props: { width: { type: Number, default: 500, }, height: { type: Number, default: 500, }, lineWidth: { type: Number, default: 1, }, backgroundColor: { type: String, default: "black", }, color: { type: String, default: "red", }, pointData: { type: Array, default: [ [ { x: 144.42779541015625, y: 112.7576904296875, time: 1702536449825, }, ], ], }, }, data() { return { canvasHistory: null, myCanvasByVideo: null, // 视频播放画布 ctxByVideo: null, // 视频播放画布 drawLineTimer: null, drawStepTimer: null, isPlayVideo: false, // 是否正在播放 isStartVideo: false, // 是否开始播放 nowTime: 0, // 当前播放时间 allTime: 0, // 回放总时间 nowPoints: [], // 当前学生视频的所有绘制点坐标 indexStep: 0, // 当前播放绘制线条的下标 indexPoint: 0, // 当前播放绘制点的下标 nowTimer: null, // 计算当前播放时间的定时器 isShowSpeedBox: false, // 是否展示速度调整列表 nowSpeed: 1, // 当前速度 speedList: [ { name: "3X", value: 3, }, { name: "2X", value: "2", }, { name: "1.5X", value: 1.5, }, { name: "1X", value: 1, }, { name: "0.5X", value: 0.5, }, ], isRocket: false, // 是否快速播放 gdbl: 2.4583, // 两种纸的坐标对应比例 小纸 2.4583 = 4720/1920 大纸 2.9167 = 5600/1920 }; }, mounted() { this.nowPoints = this.pointData this.initCanvasByVideo(); this.allTime = Math.ceil( (this.nowPoints[this.nowPoints.length - 1][ this.nowPoints[this.nowPoints.length - 1].length - 1 ].time - this.nowPoints[0][0].time) / 1000 ); // 最后一个坐标的时间 - 第一个坐标的时间 }, methods: { // 快速播放 async playRocket() { // 把所有状态清零 this.isPlayVideo = false; this.isStartVideo = false; this.indexStep = 0; this.indexPoint = 0; if (this.nowTimer) { clearInterval(this.nowTimer); } if (this.drawStepTimer) { clearTimeout(this.drawStepTimer); } if (this.drawLineTimer) { clearTimeout(this.drawLineTimer); } this.nowTime = 0; if (this.isRocket) { this.isRocket = false; return; } this.isPlayVideo = false; this.isRocket = true; this.ctxByVideo.strokeStyle = this.color; this.ctxByVideo.translate(0.5, 0.5); // 抗锯齿(好像没得卵用) this.ctxByVideo.antialias = "smooth"; // 抗锯齿(好像没得卵用) this.ctxByVideo.imageSmoothingEnabled = true; // 抗锯齿(好像没得卵用) this.ctxByVideo.lineJoin = "round"; // 设置圆角 this.ctxByVideo.lineWidth = this.lineWidth; // 设置粗细 this.ctxByVideo.shadowBlur = 1; this.ctxByVideo.shadowColor = this.color; this.ctxByVideo.clearRect( 0, 0, this.myCanvasByVideo.width, this.myCanvasByVideo.height ); // 清空 if (this.drawStepTimer) { clearTimeout(this.drawStepTimer); } if (this.drawLineTimer) { clearTimeout(this.drawLineTimer); } for (let j = 0; j < this.nowPoints.length; j++) { this.indexStep = j; this.ctxByVideo.beginPath(); let timeout = 0; if (j !== 0) { if (this.nowPoints[j].length && this.nowPoints[j - 1].length) { timeout = this.nowPoints[j][0].time - this.nowPoints[j - 1][this.nowPoints[j - 1].length - 1].time; } } await this.drawStepByRocket(this.nowPoints[j], 50); } this.isRocket = false; }, //答题笔迹 async playVideo() { if (this.isRocket) { this.isPlayVideo = false; clearInterval(this.nowTimer); clearTimeout(this.drawLineTimer); clearTimeout(this.drawStepTimer); this.myCanvasByVideo.width = this.myCanvasByVideo.width; // 清空 this.canvasHistory = null; // 清空操作 this.nowTime = 0; this.isPlayVideo = false; this.isStartVideo = false; this.indexStep = 0; this.indexPoint = 0; clearInterval(this.nowTimer); this.canvasHistory = null; if (this.nowTimer) { clearInterval(this.nowTimer); } if (this.drawStepTimer) { clearTimeout(this.drawStepTimer); } if (this.drawLineTimer) { clearTimeout(this.drawLineTimer); } } this.isRocket = false; this.isPlayVideo = true; this.isStartVideo = true; this.ctxByVideo.strokeStyle = this.color; this.ctxByVideo.translate(0.5, 0.5); // 抗锯齿(好像没得卵用) this.ctxByVideo.antialias = "smooth"; // 抗锯齿(好像没得卵用) this.ctxByVideo.imageSmoothingEnabled = true; // 抗锯齿(好像没得卵用) this.ctxByVideo.lineJoin = "round"; // 设置圆角 this.ctxByVideo.lineWidth = this.lineWidth; // 设置粗细 this.ctxByVideo.shadowBlur = 1; this.ctxByVideo.shadowColor = this.color; this.ctxByVideo.clearRect( 0, 0, this.myCanvasByVideo.width, this.myCanvasByVideo.height ); // 清空 this.nowTimer = setInterval(() => { if (this.nowTime < this.allTime) { this.nowTime++; } }, 1000 / this.nowSpeed); for (let j = 0; j < this.nowPoints.length; j++) { this.indexStep = j; this.ctxByVideo.beginPath(); let timeout = 0; if (j !== 0) { if (this.nowPoints[j].length && this.nowPoints[j - 1].length) { timeout = this.nowPoints[j][0].time - this.nowPoints[j - 1][this.nowPoints[j - 1].length - 1].time; } } await this.drawStep(this.nowPoints[j], timeout / this.nowSpeed); } this.nowTime = this.allTime; this.isPlayVideo = false; clearInterval(this.nowTimer); }, // 暂停播放 pauseVideo() { this.isPlayVideo = false; clearInterval(this.nowTimer); clearTimeout(this.drawLineTimer); clearTimeout(this.drawStepTimer); this.ctxByVideo.stroke(); }, // 继续播放 async continueVideo() { if (this.nowTime === this.allTime) { // 播放完了重新播放 this.nowTime = 0; this.playVideo(); return; } this.isPlayVideo = true; let startIndex = JSON.parse(JSON.stringify(this.indexStep)); this.nowTimer = setInterval(() => { if (this.nowTime < this.allTime) { this.nowTime++; } }, 1000 / this.nowSpeed); console.log("从这开始", this.indexStep, this.nowPoints); // this.ctx.moveTo(0, 0) // this.ctx.lineTo(500, 500) // this.ctx.stroke(); // this.ctx.closePath(); for (let j = startIndex; j < this.nowPoints.length; j++) { this.indexStep = j; let timeout = 0; if (j !== 0) { if (this.nowPoints[j].length && this.nowPoints[j - 1].length) { timeout = this.nowPoints[j][0].time - this.nowPoints[j - 1][this.nowPoints[j - 1].length - 1].time; } } await this.drawStep(this.nowPoints[j], timeout / this.nowSpeed); } this.nowTime = this.allTime; clearInterval(this.nowTimer); this.isPlayVideo = false; }, // 改变进度条(根据当前时间获取) async changeVideoSilder() { console.log("改变了"); clearInterval(this.nowTimer); clearTimeout(this.drawLineTimer); clearTimeout(this.drawStepTimer); this.ctxByVideo.clearRect( 0, 0, this.myCanvasByVideo.width, this.myCanvasByVideo.height ); // 清空 this.ctxByVideo.strokeStyle = this.color; this.ctxByVideo.translate(0.5, 0.5); // 抗锯齿(好像没得卵用) this.ctxByVideo.antialias = "smooth"; // 抗锯齿(好像没得卵用) this.ctxByVideo.imageSmoothingEnabled = true; // 抗锯齿(好像没得卵用) this.ctxByVideo.lineJoin = "round"; // 设置圆角 this.ctxByVideo.lineWidth = this.lineWidth; // 设置粗细 this.ctxByVideo.shadowBlur = 1; this.ctxByVideo.shadowColor = this.color; let allTime = 0; // 直接把进度条当前定位之前的画出来不加任何延时 here: for (let i = 0; i < this.nowPoints.length; i++) { this.ctxByVideo.beginPath(); if (i) { allTime += this.nowPoints[i][0].time - this.nowPoints[i - 1][this.nowPoints[i - 1].length - 1].time; console.log("1级时间", allTime, i); } for (let j = 0; j < this.nowPoints[i].length; j++) { if (j) { allTime += this.nowPoints[i][j].time - this.nowPoints[i][j - 1].time; console.log("2级时间", allTime, i, j); } if (j !== this.nowPoints[i].length - 1) { this.ctxByVideo.moveTo( this.nowPoints[i][j].x / this.gdbl, this.nowPoints[i][j].y / this.gdbl ); this.ctxByVideo.lineTo( this.nowPoints[i][j + 1].x / this.gdbl, this.nowPoints[i][j + 1].y / this.gdbl ); this.ctxByVideo.stroke(); this.ctxByVideo.closePath(); } if (allTime >= this.nowTime * 1000) { this.indexStep = i; this.indexPoint = j; break here; } } } if (this.isPlayVideo) { // 如果是播放状态 则继续播放 this.continueVideo(); } }, drawStep(data, time) { return new Promise((resolve) => { this.drawStepTimer = setTimeout(async () => { let startIndex = JSON.parse(JSON.stringify(this.indexPoint)); for (let i = startIndex; i < data.length - 1; i++) { this.indexPoint = i; let timeLine = data[i + 1].time - data[i].time; if (timeLine) { // 点阵笔有很多点位不同但时间相同的点 不做异步处理 await this.drawLine( data[i].x / this.gdbl, data[i].y / this.gdbl, data[i + 1].x / this.gdbl, data[i + 1].y / this.gdbl, (data[i].x + data[i + 1].x) / 2 / this.gdbl, (data[i].y + data[i + 1].y) / 2 / this.gdbl, timeLine / this.nowSpeed ); } else { this.ctxByVideo.moveTo( data[i].x / this.gdbl, data[i].y / this.gdbl ); this.ctxByVideo.lineTo( data[i + 1].x / this.gdbl, data[i + 1].y / this.gdbl ); this.ctxByVideo.stroke(); this.ctxByVideo.closePath(); } } this.indexPoint = 0; this.ctxByVideo.stroke(); resolve(); }, time); }); }, // 相同速度播放 drawStepByRocket(data, time) { return new Promise((resolve) => { this.drawStepTimer = setTimeout(async () => { let startIndex = JSON.parse(JSON.stringify(this.indexPoint)); for (let i = startIndex; i < data.length - 1; i++) { this.indexPoint = i; let timeLine = data[i + 1].time - data[i].time; if (timeLine) { // 点阵笔有很多点位不同但时间相同的点 不做异步处理 await this.drawLine( data[i].x / this.gdbl, data[i].y / this.gdbl, data[i + 1].x / this.gdbl, data[i + 1].y / this.gdbl, (data[i].x + data[i + 1].x) / 2 / this.gdbl, (data[i].y + data[i + 1].y) / 2 / this.gdbl, 5 ); } else { this.ctxByVideo.moveTo( data[i].x / this.gdbl, data[i].y / this.gdbl ); this.ctxByVideo.lineTo( data[i + 1].x / this.gdbl, data[i + 1].y / this.gdbl ); this.ctxByVideo.stroke(); this.ctxByVideo.closePath(); } } this.indexPoint = 0; this.ctxByVideo.stroke(); resolve(); }, time); }); }, drawLine(x1, y1, x2, y2, controlX, controlY, time) { return new Promise((resolve) => { this.drawLineTimer = setTimeout(() => { this.ctxByVideo.moveTo(x1, y1); this.ctxByVideo.lineTo(x2, y2); // this.ctx.quadraticCurveTo(controlX, controlY, x2, y2) this.ctxByVideo.stroke(); this.ctxByVideo.closePath(); resolve(); }, time); }); }, // 改变播放速度 changeSpeed(speed) { this.nowSpeed = speed; this.isShowSpeedBox = false; if (this.nowTimer) { clearInterval(this.nowTimer); if (this.isPlayVideo) { this.nowTimer = setInterval(() => { if (this.nowTime < this.allTime) { this.nowTime++; } }, 1000 / this.nowSpeed); } } }, getFormatTime(second) { this.formatSecond(second); }, initCanvasByVideo() { this.myCanvasByVideo = document.getElementById("myCanvasByVideo"); this.ctxByVideo = this.myCanvasByVideo.getContext("2d"); }, formatSecond(allSecond) { let minute = 0; let second = 0; second = allSecond % 60; if (second < 10) { second = "0" + second; } minute = Math.trunc(allSecond / 60); if (minute < 10) { minute = "0" + minute; } return minute + ":" + second; }, }, }; </script> <style lang="scss" scoped> .canvas-video { width: 100%; height: 100%; .myCanvas { background: black; } .btnDiv { display: flex; justify-content: center; margin-top: 5px; .playback { display: flex; justify-content: space-between; color: #ffffff; text-align: center; padding: 0 15px; border-radius: 4px; background: #00b386; height: 28px; line-height: 28px; cursor: pointer; img { width: 10px; height: 14px; margin-top: 7px; margin-left: 12px; } } .rocket { margin-top: 3px; margin-left: 10px; cursor: pointer; } .mySlider { width: 500px; margin-left: 20px; ::v-deep .el-slider__bar { border-radius: 17px; background: #00b386; } ::v-deep .el-slider__button { border: 1px solid #00b386; } ::v-deep .el-slider__runway { border-radius: 17px; background: #444; } } .myTime { margin-left: 14px; color: #a9a9a9; line-height: 35px; } .mySpeed { cursor: pointer; position: relative; border-radius: 2px; background: #ef8714; width: 60px; height: 20px; line-height: 20px; text-align: center; color: #ffffff; margin-left: 20px; margin-top: 5px; .speedList { position: absolute; bottom: 20px; border-radius: 2px; background: #232322; padding: 10px 0; width: 60px; .speedItem { width: 100%; text-align: center; margin-bottom: 10px; color: #ffffff; } .speedItem.active { color: #ef8714; font-size: 14px; } .speedItem:last-child { margin-bottom: 0px; } z-index: 2000; } } .slider-warpper { width: 320px; height: 16px; position: absolute; left: 280px; // background: #ef9e00 !important; display: flex; justify-content: center; align-items: center; flex-direction: column; .slider-content { width: 304px; height: 16px; background: #2d2d2d; border: 1px solid rgba(255, 186, 33, 0.16); border-radius: 22px; position: relative; .slider { border-radius: 22px; position: absolute; left: 0; top: 0; width: 30px; height: 16px; background: #ffba21; } } .persent { margin-left: 10px; color: #ffba21; font-size: 18px; font-weight: 600; letter-spacing: 1.26px; } } } } </style>
二、vue3版本
<template> <div class="canvas-video" :style="{width:props.width+'px',height:props.height+'px'}"> <img :src="props.backgroundColor" alt=""> <canvas ref="myCanvasByVideo" class="myCanvas" id="myCanvasByVideo" :width="props.width"></canvas> </div> <div class="btnDiv"> <div v-if="!state.isPlayVideo && !state.isStartVideo" class="playback" @click.stop="playVideo"> <div>回放</div> </div> <div v-if="state.isPlayVideo && state.isStartVideo" class="playback" @click.stop="pauseVideo"> <div>暂停</div> </div> <div v-if="!state.isPlayVideo && state.isStartVideo" class="playback" @click.stop="continueVideo"> <div>继续</div> </div> <div class="mySlider"> <el-slider v-model="state.nowTime" :max="state.allTime" @change="changeVideoSilder"></el-slider> </div> <div class="myTime">{{getFormatTime(state.nowTime)}} / {{getFormatTime(state.allTime)}}</div> <!-- <div class="rocket playback"> <div v-show="!state.isRocket" @click="playRocket">快速回播</div> <div v-show="state.isRocket" @click="playRocket">暂停播放</div> </div> --> <!-- <div class="mySpeed"> <div @click.stop="state.isShowSpeedBox = !state.isShowSpeedBox">{{state.speedList.filter((item:any) => item.value === state.nowSpeed)[0].name}}</div> <div class="speedList" v-show="state.isShowSpeedBox"> <div class="speedItem" :class="item.value === state.nowSpeed ? 'active' : ''" v-for="(item, index) in state.speedList" :key="index" @click.stop="changeSpeed(item.value)"> {{item.name}} </div> </div> </div> --> </div> </template> <script setup lang="ts"> import { onMounted, reactive } from 'vue' type TProps = { width: number height: number, lineWidth: number, backgroundColor: string, color: string, pointData: any } const props = withDefaults(defineProps<TProps>(), {}) const state = reactive<any>({ timeout:0, minute:0, second:0, canvasHistory: null, myCanvasByVideo: null, // 视频播放画布 ctxByVideo: null, // 视频播放画布 drawLineTimer: null, drawStepTimer: null, isPlayVideo: false, // 是否正在播放 isStartVideo: false, // 是否开始播放 nowTime: 0, // 当前播放时间 allTime: 0, // 回放总时间 nowPoints: [], // 当前学生视频的所有绘制点坐标 indexStep: 0, // 当前播放绘制线条的下标 indexPoint: 0, // 当前播放绘制点的下标 nowTimer: null, // 计算当前播放时间的定时器 isShowSpeedBox: false, // 是否展示速度调整列表 nowSpeed: 1, // 当前速度 speedList: [{ name: '3X', value: 3 }, { name: '2X', value: '2' }, { name: '1.5X', value: 1.5 }, { name: '1X', value: 1 }, { name: '0.5X', value: 0.5 }], isRocket: false, // 是否快速播放 gdbl: 2.4583 // 两种纸的坐标对应比例 小纸 2.4583 = 4720/1920 大纸 2.9167 = 5600/1920 }) onMounted(()=> { state.nowPoints = props.pointData initCanvasByVideo() state.allTime = Math.ceil((state.nowPoints[state.nowPoints.length - 1][state.nowPoints[state.nowPoints.length - 1].length - 1].time - state.nowPoints[0][0].time) / 1000) // 最后一个坐标的时间 - 第一个坐标的时间 }) // 快速播放 const playRocket = async () => { // 把所有状态清零 state.isPlayVideo = false state.isStartVideo = false state.indexStep = 0 state.indexPoint = 0 if (state.nowTimer) { clearInterval(state.nowTimer) } if (state.drawStepTimer) { clearTimeout(state.drawStepTimer) } if (state.drawLineTimer) { clearTimeout(state.drawLineTimer) } state.nowTime = 0 if (state.isRocket) { state.isRocket = false return } state.isPlayVideo = false state.isRocket = true state.ctxByVideo.strokeStyle = props.color state.ctxByVideo.translate(0.5, 0.5) // 抗锯齿(好像没得卵用) state.ctxByVideo.antialias = 'smooth' // 抗锯齿(好像没得卵用) state.ctxByVideo.imageSmoothingEnabled = true // 抗锯齿(好像没得卵用) state.ctxByVideo.lineJoin = 'round' // 设置圆角 state.ctxByVideo.lineWidth = props.lineWidth // 设置粗细 state.ctxByVideo.shadowBlur = 1; state.ctxByVideo.shadowColor = props.color; state.ctxByVideo.clearRect(0, 0, state.myCanvasByVideo.width, state.myCanvasByVideo.height); // 清空 if (state.drawStepTimer) { clearTimeout(state.drawStepTimer) } if (state.drawLineTimer) { clearTimeout(state.drawLineTimer) } for (let j = 0; j < state.nowPoints.length; j++) { state.indexStep = j state.ctxByVideo.beginPath(); state.timeout = 0 if (j !== 0) { if (state.nowPoints[j].length && state.nowPoints[j - 1].length) { state.timeout = state.nowPoints[j][0].time - state.nowPoints[j - 1][state.nowPoints[j - 1].length - 1].time } } await drawStepByRocket(state.nowPoints[j], 50) } state.isRocket = false } //答题笔迹 const playVideo = async () => { if (state.isRocket) { state.isPlayVideo = false clearInterval(state.nowTimer) clearTimeout(state.drawLineTimer) clearTimeout(state.drawStepTimer) state.myCanvasByVideo.width = state.myCanvasByVideo.width // 清空 state.canvasHistory = null // 清空操作 state.nowTime = 0 state.isPlayVideo = false state.isStartVideo = false state.indexStep = 0 state.indexPoint = 0 clearInterval(state.nowTimer) state.canvasHistory = null if (state.nowTimer) { clearInterval(state.nowTimer) } if (state.drawStepTimer) { clearTimeout(state.drawStepTimer) } if (state.drawLineTimer) { clearTimeout(state.drawLineTimer) } } state.isRocket = false state.isPlayVideo = true state.isStartVideo = true state.ctxByVideo.strokeStyle = props.color state.ctxByVideo.translate(0.5, 0.5) // 抗锯齿(好像没得卵用) state.ctxByVideo.antialias = 'smooth' // 抗锯齿(好像没得卵用) state.ctxByVideo.imageSmoothingEnabled = true // 抗锯齿(好像没得卵用) state.ctxByVideo.lineJoin = 'round' // 设置圆角 state.ctxByVideo.lineWidth = props.lineWidth // 设置粗细 state.ctxByVideo.shadowBlur = 1; state.ctxByVideo.shadowColor = props.color; state.ctxByVideo.clearRect(0, 0, state.myCanvasByVideo.width, state.myCanvasByVideo.height); // 清空 state.nowTimer = setInterval(()=> { if (state.nowTime < state.allTime) { state.nowTime++ } }, 1000 / state.nowSpeed) for (let j = 0; j < state.nowPoints.length; j++) { state.indexStep = j state.ctxByVideo.beginPath(); state.timeout = 0 if (j !== 0) { if (state.nowPoints[j].length && state.nowPoints[j - 1].length) { state.timeout = state.nowPoints[j][0].time - state.nowPoints[j - 1][state.nowPoints[j - 1].length - 1].time } } await drawStep(state.nowPoints[j], props.lineWidth / state.nowSpeed) } state.nowTime = JSON.parse(JSON.stringify(state.allTime)) state.isPlayVideo = false clearInterval(state.nowTimer) } // 暂停播放 const pauseVideo = () => { state.isPlayVideo = false clearInterval(state.nowTimer) clearTimeout(state.drawLineTimer) clearTimeout(state.drawStepTimer) state.ctxByVideo.stroke() } // 继续播放 const continueVideo = async () => { if (state.nowTime === state.allTime) { // 播放完了重新播放 state.nowTime = JSON.parse(JSON.stringify(state.allTime)) pauseVideo() state.isPlayVideo = false state.isStartVideo = false state.isRocket = true return } state.isPlayVideo = true let startIndex = JSON.parse(JSON.stringify(state.indexStep)) state.nowTimer = setInterval(()=> { if (state.nowTime < state.allTime) { state.nowTime++ } }, 1000 / state.nowSpeed) console.log('从这开始', state.indexStep, state.nowPoints) // state.ctx.moveTo(0, 0) // state.ctx.lineTo(500, 500) // state.ctx.stroke(); // state.ctx.closePath(); for (let j = startIndex; j < state.nowPoints.length; j++) { state.indexStep = j state.timeout = 0 if (j !== 0) { if (state.nowPoints[j].length && state.nowPoints[j - 1].length) { state.timeout = state.nowPoints[j][0].time - state.nowPoints[j - 1][state.nowPoints[j - 1].length - 1].time } } await drawStep(state.nowPoints[j], state.timeout / state.nowSpeed) } state.nowTime = JSON.parse(JSON.stringify(state.allTime)) clearInterval(state.nowTimer) state.isPlayVideo = false } // 改变进度条(根据当前时间获取) const changeVideoSilder = () => { console.log('改变了') clearInterval(state.nowTimer) clearTimeout(state.drawLineTimer) clearTimeout(state.drawStepTimer) state.ctxByVideo.clearRect(0, 0, state.myCanvasByVideo.width, state.myCanvasByVideo.height); // 清空 state.ctxByVideo.strokeStyle = props.color state.ctxByVideo.translate(0.5, 0.5) // 抗锯齿(好像没得卵用) state.ctxByVideo.antialias = 'smooth' // 抗锯齿(好像没得卵用) state.ctxByVideo.imageSmoothingEnabled = true // 抗锯齿(好像没得卵用) state.ctxByVideo.lineJoin = 'round' // 设置圆角 state.ctxByVideo.lineWidth = props.lineWidth // 设置粗细 state.ctxByVideo.shadowBlur = 1; state.ctxByVideo.shadowColor = props.color; let allTime = 0 // 直接把进度条当前定位之前的画出来不加任何延时 here: for (let i = 0;i < state.nowPoints.length;i++) { state.ctxByVideo.beginPath(); if (i) { allTime += (state.nowPoints[i][0].time - state.nowPoints[i - 1][state.nowPoints[i - 1].length - 1].time) // console.log('1级时间', allTime, i) } for (let j = 0;j < state.nowPoints[i].length;j++) { if (j) { allTime += (state.nowPoints[i][j].time - state.nowPoints[i][j - 1].time) // console.log('2级时间', allTime, i, j) } if (j !== state.nowPoints[i].length - 1) { state.ctxByVideo.moveTo(state.nowPoints[i][j].x / state.gdbl, state.nowPoints[i][j].y / state.gdbl) state.ctxByVideo.lineTo(state.nowPoints[i][j+1].x / state.gdbl, state.nowPoints[i][j+1].y / state.gdbl) state.ctxByVideo.stroke(); state.ctxByVideo.closePath(); } if (allTime >= (state.nowTime * 1000)) { state.indexStep = i state.indexPoint = j break here } } } if (state.isPlayVideo) { // 如果是播放状态 则继续播放 continueVideo() } } const drawStep = (data:any, time:any) => { return new Promise((resolve) => { state.drawStepTimer = setTimeout(async () => { let startIndex = JSON.parse(JSON.stringify(state.indexPoint)) for (let i = startIndex; i < data.length - 1; i++) { state.indexPoint = i let timeLine = data[i + 1].time - data[i].time if (timeLine) { // 点阵笔有很多点位不同但时间相同的点 不做异步处理 await drawLine(data[i].x / state.gdbl, data[i].y / state.gdbl, data[i + 1].x / state.gdbl, data[i + 1].y / state.gdbl, (data[i].x + data[i + 1].x) / 2 / state.gdbl, (data[i].y + data[i + 1].y) / 2 / state.gdbl, timeLine / state.nowSpeed) } else { state.ctxByVideo.moveTo(data[i].x / state.gdbl, data[i].y / state.gdbl) state.ctxByVideo.lineTo(data[i + 1].x / state.gdbl, data[i + 1].y / state.gdbl) state.ctxByVideo.stroke() state.ctxByVideo.closePath() } } state.indexPoint = 0 state.ctxByVideo.stroke(); resolve() }, time) }) } // 相同速度播放 const drawStepByRocket = (data:any, time:any) => { return new Promise((resolve) => { state.drawStepTimer = setTimeout(async () => { let startIndex = JSON.parse(JSON.stringify(state.indexPoint)) for (let i = startIndex; i < data.length - 1; i++) { state.indexPoint = i let timeLine = data[i + 1].time - data[i].time if (timeLine) { // 点阵笔有很多点位不同但时间相同的点 不做异步处理 await drawLine(data[i].x / state.gdbl, data[i].y / state.gdbl, data[i + 1].x / state.gdbl, data[i + 1].y / state.gdbl, (data[i].x + data[i + 1].x) / 2 / state.gdbl, (data[i].y + data[i + 1].y) / 2 / state.gdbl, 5) } else { state.ctxByVideo.moveTo(data[i].x / state.gdbl, data[i].y / state.gdbl) state.ctxByVideo.lineTo(data[i + 1].x / state.gdbl, data[i + 1].y / state.gdbl) state.ctxByVideo.stroke() state.ctxByVideo.closePath() } } state.indexPoint = 0 state.ctxByVideo.stroke(); resolve() }, time) }) } const drawLine = (x1:any, y1:any, x2:any, y2:any, controlX:any, controlY:any, time:any) => { return new Promise((resolve) => { state.drawLineTimer = setTimeout(() => { state.ctxByVideo.moveTo(x1, y1) state.ctxByVideo.lineTo(x2, y2) // state.ctx.quadraticCurveTo(controlX, controlY, x2, y2) state.ctxByVideo.stroke(); state.ctxByVideo.closePath(); resolve() }, time) }) } // 改变播放速度 const changeSpeed = (speed:any) => { state.nowSpeed = speed state.isShowSpeedBox = false if (state.nowTimer) { clearInterval(state.nowTimer) if (state.isPlayVideo) { state.nowTimer = setInterval(()=> { if (state.nowTime < state.allTime) { state.nowTime++ } }, 1000 / state.nowSpeed) } } } const getFormatTime = (allSecond:any) => { let minute:any = 0 let second:any = 0 second = allSecond % 60 if (second < 10) { second = '0' + second } minute = Math.trunc(allSecond / 60) if (minute < 10) { minute = '0' + minute } return minute + ':' + second } const initCanvasByVideo = () => { state.myCanvasByVideo = document.getElementById("myCanvasByVideo") state.ctxByVideo = state.myCanvasByVideo.getContext("2d") } </script> <style lang="scss" scoped> .canvas-video{ width: 100%; height: 100%; background: white; // .myCanvas{ // z-index: 99; // } img{ max-width: 100%; } } .btnDiv{ display: flex; align-items: center; justify-content: center; margin-top: 20px; .playback{ display: flex; justify-content: space-between; color: #ffffff; text-align: center; padding: 0 20px; height: 40px; line-height: 40px; border-radius: 4px; border: 1px solid #00B386; font-size: 20px; cursor: pointer; img{ width: 10px; height: 14px; margin-top: 7px; margin-left: 12px; } } .rocket{ margin-top: 3px; margin-left: 10px; cursor: pointer; } :deep(.mySlider){ width: 500px; margin-left: 20px; .el-slider__bar{ border-radius: 17px; background: #00B386; } .el-slider__button{ border: 1px solid #00B386; } .el-slider__runway{ border-radius: 17px; background: #444; } } .myTime{ margin-left: 14px; color: #A9A9A9; line-height: 35px; font-size: 18px; } .mySpeed{ cursor: pointer; position: relative; border-radius: 2px; background: #EF8714; width: 60px; height: 40px; line-height: 40px; text-align: center; color: #ffffff; margin: 5px 0 0 20px; font-size: 18px; .speedList{ position: absolute; bottom: 20px; border-radius: 2px; background: #232322; padding: 10px 0; width: 60px; .speedItem{ width: 100%; text-align: center; margin-bottom: 10px; color: #FFFFFF; } .speedItem.active{ color: #EF8714; font-size: 20px; } .speedItem:last-child{ margin-bottom: 0px; } z-index: 2000; } } .slider-warpper { width: 320px; height: 16px; position: absolute; left: 280px; // background: #ef9e00 !important; display: flex; justify-content: center; align-items: center; flex-direction: column; .slider-content { width: 304px; height: 16px; background: #2D2D2D; border: 1px solid rgba(255, 186, 33, 0.16); border-radius: 22px; position: relative; .slider { border-radius: 22px; position: absolute; left: 0; top: 0; width: 30px; height: 16px; background: #FFBA21; } } .persent { margin-left: 10px; color: #FFBA21; font-size: 18px; font-weight: 600; letter-spacing: 1.26px; } } } .dialog-btn{ display: flex; justify-content: center; .btn1{ border: 1px solid #00B386; background: #00B386; text-align: center; margin-right: 50px; color: #FFF; color: #fff; font-size: 18px; padding: 10px 65px; cursor: pointer; &:active{ opacity: 0.8; } } .btn2{ border: 1px solid #00B386; color: #fff; font-size: 18px; text-align: center; padding: 10px 65px; cursor: pointer; &:active{ opacity: 0.8; } } } </style>
三、页面调用
1、vue2
(1)安装 canvasvideo-vue
npm install canvasvideo-vue --save
(2)main.js引入
import { createApp } from 'vue' import App from './App.vue' import canvasVideo from "canvasvideo-vue" import "../node_modules/canvasvideo-vue/canvasVideo-vue.css" const app = createApp(App) app.use(canvasVideo) .mount("#app")
(3)页面调用
<canvas-video :width="1220" :height="500" :backgroundColor="'blue'" :color="'red'" :lineWidth="1" :pointData="backPlayList" />
2、vue3
<canvas-video :width="1220" :height="500" :backgroundColor="'blue'" :color="'red'" :lineWidth="1" :pointData="backPlayList" /> <script setup lang="ts"> import { ref } from 'vue' import CanvasVideo from "./components/CanvasVideo.vue" const backPlayList = ref<any[]>([]) // backPlayList 去接收处理后台返回的数据 </script>
四、数据结构
let datas = [ [ { "x": 144.42779541015625, "y": 112.7576904296875, "time": 1702536449825 }, { "x": 144.42779541015625, "y": 122.65827941894531, "time": 1702536449857 }, { "x": 144.42779541015625, "y": 128.27886962890625, "time": 1702536449871 }, { "x": 144.42779541015625, "y": 134.2593231201172, "time": 1702536449889 }, { "x": 144.42779541015625, "y": 146.72254943847656, "time": 1702536449939 }, { "x": 144.42779541015625, "y": 157.4418182373047, "time": 1702536449955 }, { "x": 145.78329467773438, "y": 162.1194610595703, "time": 1702536449971 }, { "x": 145.76043701171875, "y": 167.31605529785156, "time": 1702536449989 }, { "x": 146.4267578125, "y": 172.4877471923828, "time": 1702536450007 }, { "x": 146.4267578125, "y": 177.4159698486328, "time": 1702536450020 }, { "x": 146.4267578125, "y": 182.0648651123047, "time": 1702536450039 }, { "x": 147.09307861328125, "y": 187.20314025878906, "time": 1702536450053 }, { "x": 147.762451171875, "y": 192.70411682128906, "time": 1702536450070 }, { "x": 148.41329956054688, "y": 197.30494689941406, "time": 1702536450088 }, { "x": 149.09210205078125, "y": 202.69898986816406, "time": 1702536450103 }, { "x": 149.7584228515625, "y": 207.8609161376953, "time": 1702536450121 }, { "x": 150.42477416992188, "y": 212.64430236816406, "time": 1702536450140 }, { "x": 151.09112548828125, "y": 217.8363494873047, "time": 1702536450158 }, { "x": 151.09112548828125, "y": 223.17628479003906, "time": 1702536450178 }, { "x": 151.7574462890625, "y": 229.3798065185547, "time": 1702536450194 }, { "x": 151.7574462890625, "y": 234.6603240966797, "time": 1702536450204 }, { "x": 151.7574462890625, "y": 239.9730987548828, "time": 1702536450221 }, { "x": 151.7574462890625, "y": 245.2152862548828, "time": 1702536450246 }, { "x": 151.7574462890625, "y": 251.24928283691406, "time": 1702536450257 }, { "x": 151.09112548828125, "y": 257.2041473388672, "time": 1702536450270 }, { "x": 151.09112548828125, "y": 261.8277130126953, "time": 1702536450288 }, { "x": 151.09112548828125, "y": 267.2612762451172, "time": 1702536450308 }, { "x": 151.09112548828125, "y": 273.2233123779297, "time": 1702536450322 }, { "x": 151.09112548828125, "y": 279.8961639404297, "time": 1702536450342 }, { "x": 151.09112548828125, "y": 285.8682403564453, "time": 1702536450358 }, { "x": 152.39996337890625, "y": 291.18141174316406, "time": 1702536450370 }, { "x": 153.7384033203125, "y": 297.18141174316406, "time": 1702536450389 }, { "x": 155.09494018554688, "y": 303.9618377685547, "time": 1702536450405 }, { "x": 156.41146850585938, "y": 309.8865203857422, "time": 1702536450420 }, { "x": 158.44903564453125, "y": 314.70338439941406, "time": 1702536450442 }, { "x": 161.03179931640625, "y": 319.8107147216797, "time": 1702536450455 }, { "x": 163.73126220703125, "y": 325.8739776611328, "time": 1702536450473 }, { "x": 165.68637084960938, "y": 331.1151580810547, "time": 1702536450488 }, { "x": 168.38128662109375, "y": 336.5032501220703, "time": 1702536450503 }, { "x": 171.0692138671875, "y": 341.2170867919922, "time": 1702536450521 }, { "x": 173.73162841796875, "y": 345.2093963623047, "time": 1702536450538 }, { "x": 176.42041015625, "y": 348.57081604003906, "time": 1702536450554 }, { "x": 179.12631225585938, "y": 351.2758026123047, "time": 1702536450571 }, { "x": 181.71502685546875, "y": 353.8636932373047, "time": 1702536450588 }, { "x": 184.35678100585938, "y": 355.19776916503906, "time": 1702536450605 }, { "x": 187.10076904296875, "y": 356.5692901611328, "time": 1702536450621 }, { "x": 188.40576171875, "y": 357.2216033935547, "time": 1702536450636 } ], [ { "x": 278.3201904296875, "y": 225.33656311035156, "time": 1702536451411 }, { "x": 290.136962890625, "y": 225.3314971923828, "time": 1702536451431 }, { "x": 305.58203125, "y": 225.9976043701172, "time": 1702536451442 }, { "x": 319.9619140625, "y": 225.9976043701172, "time": 1702536451457 }, { "x": 331.8698425292969, "y": 226.6862030029297, "time": 1702536451472 }, { "x": 342.2525329589844, "y": 227.32032775878906, "time": 1702536451488 }, { "x": 351.42987060546875, "y": 227.32984924316406, "time": 1702536451504 }, { "x": 361.1939697265625, "y": 227.32984924316406, "time": 1702536451521 }, { "x": 370.506103515625, "y": 226.6363983154297, "time": 1702536451540 }, { "x": 377.578857421875, "y": 226.01075744628906, "time": 1702536451585 }, { "x": 396.9224853515625, "y": 223.3396759033203, "time": 1702536451589 }, { "x": 404.81201171875, "y": 222.0262908935547, "time": 1702536451603 }, { "x": 412.2171630859375, "y": 220.68153381347656, "time": 1702536451621 }, { "x": 418.94720458984375, "y": 219.33851623535156, "time": 1702536451637 }, { "x": 424.103515625, "y": 218.0503692626953, "time": 1702536451654 }, { "x": 429.489013671875, "y": 218.00428771972656, "time": 1702536451671 }, { "x": 433.5123291015625, "y": 218.00428771972656, "time": 1702536451687 }, { "x": 436.1656494140625, "y": 217.33815002441406, "time": 1702536451703 }, { "x": 438.26458740234375, "y": 217.33815002441406, "time": 1702536451720 }, { "x": 439.58154296875, "y": 217.33815002441406, "time": 1702536451740 }, { "x": 439.613525390625, "y": 217.33815002441406, "time": 1702536451761 } ], [ { "x": 359.6776123046875, "y": 148.3515167236328, "time": 1702536452241 }, { "x": 360.95751953125, "y": 166.9501495361328, "time": 1702536452258 }, { "x": 361.6524658203125, "y": 186.5827178955078, "time": 1702536452270 }, { "x": 362.3128662109375, "y": 203.9375762939453, "time": 1702536452288 }, { "x": 362.9931640625, "y": 218.0919647216797, "time": 1702536452306 }, { "x": 364.331787109375, "y": 232.8119354248047, "time": 1702536452320 }, { "x": 364.984130859375, "y": 247.2077178955078, "time": 1702536452337 }, { "x": 365.6175537109375, "y": 260.9715118408203, "time": 1702536452354 }, { "x": 366.2884521484375, "y": 275.6442413330078, "time": 1702536452371 }, { "x": 367.6258544921875, "y": 289.0420379638672, "time": 1702536452390 }, { "x": 368.8919677734375, "y": 301.1228790283203, "time": 1702536452403 }, { "x": 370.292724609375, "y": 313.0811309814453, "time": 1702536452421 }, { "x": 372.9156494140625, "y": 323.0240936279297, "time": 1702536452438 }, { "x": 374.9552001953125, "y": 330.4568634033203, "time": 1702536452455 }, { "x": 377.5841064453125, "y": 337.0873565673828, "time": 1702536452471 }, { "x": 378.9256591796875, "y": 342.3609161376953, "time": 1702536452488 }, { "x": 380.2994384765625, "y": 347.8545379638672, "time": 1702536452505 }, { "x": 381.62762451171875, "y": 351.8480682373047, "time": 1702536452520 }, { "x": 382.308837890625, "y": 354.4903106689453, "time": 1702536452545 }, { "x": 382.308837890625, "y": 354.55714416503906, "time": 1702536452556 } ], [ { "x": 586.8844604492188, "y": 151.2805633544922, "time": 1702536453141 }, { "x": 585.511962890625, "y": 165.02943420410156, "time": 1702536453157 }, { "x": 584.8742065429688, "y": 183.56068420410156, "time": 1702536453173 }, { "x": 584.8742065429688, "y": 201.2294158935547, "time": 1702536453188 }, { "x": 584.8742065429688, "y": 215.3689422607422, "time": 1702536453204 }, { "x": 584.8742065429688, "y": 230.58912658691406, "time": 1702536453222 }, { "x": 585.54052734375, "y": 243.74464416503906, "time": 1702536453239 }, { "x": 586.8107299804688, "y": 256.01344299316406, "time": 1702536453256 }, { "x": 587.5194091796875, "y": 267.7810821533203, "time": 1702536453271 }, { "x": 588.8597412109375, "y": 279.1865692138672, "time": 1702536453288 }, { "x": 590.2003784179688, "y": 289.90699768066406, "time": 1702536453304 }, { "x": 591.52001953125, "y": 301.1284942626953, "time": 1702536453321 }, { "x": 592.8444213867188, "y": 311.0792694091797, "time": 1702536453339 }, { "x": 594.8500366210938, "y": 320.5182342529297, "time": 1702536453355 }, { "x": 596.1693115234375, "y": 328.3832244873047, "time": 1702536453371 }, { "x": 597.5363159179688, "y": 336.58277893066406, "time": 1702536453390 }, { "x": 598.8732299804688, "y": 343.2628936767578, "time": 1702536453404 }, { "x": 600.2008666992188, "y": 348.56568908691406, "time": 1702536453420 }, { "x": 600.8662109375, "y": 353.1544647216797, "time": 1702536453438 }, { "x": 600.8662109375, "y": 356.4942169189453, "time": 1702536453454 }, { "x": 600.8662109375, "y": 358.5269012451172, "time": 1702536453472 }, { "x": 600.8662109375, "y": 358.5537872314453, "time": 1702536453482 } ], [ { "x": 679.638916015625, "y": 223.9750518798828, "time": 1702536453873 }, { "x": 698.049560546875, "y": 220.69044494628906, "time": 1702536453889 }, { "x": 715.046142578125, "y": 218.70338439941406, "time": 1702536453912 }, { "x": 731.1329956054688, "y": 217.30799865722656, "time": 1702536453922 }, { "x": 743.7023315429688, "y": 216.6453094482422, "time": 1702536453939 }, { "x": 755.0431518554688, "y": 216.6720428466797, "time": 1702536453957 }, { "x": 764.043701171875, "y": 216.6720428466797, "time": 1702536453972 }, { "x": 770.6744995117188, "y": 216.6720428466797, "time": 1702536453988 }, { "x": 776.098388671875, "y": 216.6720428466797, "time": 1702536454005 }, { "x": 780.066650390625, "y": 216.6720428466797, "time": 1702536454021 }, { "x": 783.4193115234375, "y": 216.6720428466797, "time": 1702536454041 }, { "x": 785.4308471679688, "y": 216.6720428466797, "time": 1702536454057 }, { "x": 786.7714233398438, "y": 216.6720428466797, "time": 1702536454070 }, { "x": 786.7733154296875, "y": 216.6720428466797, "time": 1702536454083 } ], [ { "x": 733.5531616210938, "y": 316.62501525878906, "time": 1702536454456 }, { "x": 756.4210815429688, "y": 315.2781524658203, "time": 1702536454472 }, { "x": 775.4960327148438, "y": 315.2565460205078, "time": 1702536454488 }, { "x": 790.3649291992188, "y": 317.1736297607422, "time": 1702536454506 }, { "x": 804.3414916992188, "y": 319.8348846435547, "time": 1702536454524 }, { "x": 815.0912475585938, "y": 323.1543731689453, "time": 1702536454540 }, { "x": 823.2052612304688, "y": 325.8422393798828, "time": 1702536454556 }, { "x": 829.9567260742188, "y": 328.5275115966797, "time": 1702536454571 }, { "x": 835.1862182617188, "y": 329.8536834716797, "time": 1702536454589 }, { "x": 839.3932495117188, "y": 330.5771026611328, "time": 1702536454606 }, { "x": 841.41259765625, "y": 330.5771026611328, "time": 1702536454618 } ], [ { "x": 783.1222534179688, "y": 122.69233703613281, "time": 1702536454959 }, { "x": 807.8834838867188, "y": 125.26026916503906, "time": 1702536454974 }, { "x": 833.0866088867188, "y": 128.6871337890625, "time": 1702536454989 }, { "x": 854.1300659179688, "y": 132.88661193847656, "time": 1702536455006 }, { "x": 869.70361328125, "y": 138.1724853515625, "time": 1702536455021 }, { "x": 882.982666015625, "y": 144.5460968017578, "time": 1702536455042 }, { "x": 895.916748046875, "y": 152.63514709472656, "time": 1702536455056 }, { "x": 906.02685546875, "y": 160.70143127441406, "time": 1702536455071 }, { "x": 916.085693359375, "y": 170.0878448486328, "time": 1702536455090 }, { "x": 925.06689453125, "y": 182.9326934814453, "time": 1702536455105 }, { "x": 932.54638671875, "y": 195.7455291748047, "time": 1702536455121 }, { "x": 937.921142578125, "y": 209.0699920654297, "time": 1702536455139 }, { "x": 941.3206787109375, "y": 221.79115295410156, "time": 1702536455159 }, { "x": 943.363037109375, "y": 235.3358612060547, "time": 1702536455171 }, { "x": 943.3616943359375, "y": 248.3235626220703, "time": 1702536455187 }, { "x": 942.05517578125, "y": 261.0668182373047, "time": 1702536455205 }, { "x": 938.763427734375, "y": 275.0695037841797, "time": 1702536455221 }, { "x": 933.5555419921875, "y": 287.5211639404297, "time": 1702536455238 }, { "x": 928.054443359375, "y": 298.5670928955078, "time": 1702536455255 }, { "x": 921.485107421875, "y": 307.7720489501953, "time": 1702536455271 }, { "x": 913.521728515625, "y": 316.4197235107422, "time": 1702536455288 }, { "x": 905.4949951171875, "y": 325.1339569091797, "time": 1702536455305 }, { "x": 896.376953125, "y": 333.6233673095703, "time": 1702536455321 }, { "x": 885.5185546875, "y": 342.4713592529297, "time": 1702536455339 }, { "x": 874.749755859375, "y": 350.5452117919922, "time": 1702536455356 }, { "x": 863.4857177734375, "y": 359.8206329345703, "time": 1702536455370 }, { "x": 852.8712768554688, "y": 367.1151580810547, "time": 1702536455391 }, { "x": 841.2532958984375, "y": 374.6466522216797, "time": 1702536455404 }, { "x": 829.65380859375, "y": 381.0709991455078, "time": 1702536455421 }, { "x": 817.5537109375, "y": 387.79103088378906, "time": 1702536455437 }, { "x": 805.4242553710938, "y": 393.8605499267578, "time": 1702536455454 }, { "x": 793.5094604492188, "y": 399.15431213378906, "time": 1702536455472 }, { "x": 782.9738159179688, "y": 403.1088409423828, "time": 1702536455489 }, { "x": 772.8463745117188, "y": 406.4949493408203, "time": 1702536455504 }, { "x": 764.2553100585938, "y": 409.1325225830078, "time": 1702536455522 }, { "x": 756.42041015625, "y": 411.1269073486328, "time": 1702536455540 }, { "x": 749.0506591796875, "y": 413.7377471923828, "time": 1702536455557 }, { "x": 742.9489135742188, "y": 416.42872619628906, "time": 1702536455572 }, { "x": 737.5338745117188, "y": 418.4693145751953, "time": 1702536455588 }, { "x": 733.5607299804688, "y": 420.4709014892578, "time": 1702536455605 }, { "x": 730.177734375, "y": 422.4792022705078, "time": 1702536455622 }, { "x": 728.816650390625, "y": 424.4844512939453, "time": 1702536455638 }, { "x": 728.80224609375, "y": 427.1183624267578, "time": 1702536455655 }, { "x": 732.04443359375, "y": 429.1317901611328, "time": 1702536455671 }, { "x": 739.4037475585938, "y": 431.8090362548828, "time": 1702536455688 }, { "x": 752.61962890625, "y": 435.11402893066406, "time": 1702536455712 }, { "x": 767.9056396484375, "y": 437.7860870361328, "time": 1702536455727 }, { "x": 784.0560302734375, "y": 439.1490020751953, "time": 1702536455738 }, { "x": 800.2537841796875, "y": 441.17530822753906, "time": 1702536455755 }, { "x": 820.0858154296875, "y": 443.8155059814453, "time": 1702536455772 }, { "x": 837.1102905273438, "y": 445.79103088378906, "time": 1702536455787 }, { "x": 852.6246948242188, "y": 447.1361846923828, "time": 1702536455803 }, { "x": 865.9520263671875, "y": 449.1322784423828, "time": 1702536455820 }, { "x": 876.6416015625, "y": 451.1309356689453, "time": 1702536455844 }, { "x": 886.6123046875, "y": 453.1099395751953, "time": 1702536455855 }, { "x": 894.085205078125, "y": 454.4803009033203, "time": 1702536455871 }, { "x": 899.9359130859375, "y": 455.77760314941406, "time": 1702536455888 }, { "x": 904.62646484375, "y": 458.4120635986328, "time": 1702536455904 }, { "x": 908.64892578125, "y": 460.4477081298828, "time": 1702536455920 }, { "x": 908.71240234375, "y": 460.4689483642578, "time": 1702536455930 } ] ]
到此这篇关于vue实现画笔回放,canvas转视频播放功能的文章就介绍到这了,更多相关vue canvas视频播放内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!