uniapp上手机端二维图片轨迹路线回放
template view classbox !-- 地图容器背景底图 轨迹画布重叠 -- view classtrajectory-card !-- 底层场地背景图 -- image classmap-bg src/static/ceshi.png modeaspectFill/image !-- 顶层画布绘制轨迹、人物绑定点击事件拿物理坐标 -- canvas idtrackCanvas canvas-idtrackCanvas classtrack-canvas taphandleCanvasTap/canvas /view !-- 回放控制按钮组 -- view styledisplay:flex;gap:20rpx;margin:30rpx van-button clickhandleClick重新播放/van-button van-button typeprimary clickpausePlay{{ isPause ? 继续 : 暂停 }}/van-button /view !-- 回放进度条 -- view stylemargin:0 20rpx view当前绘制进度{{ progressNum }}%/view view stylewidth:100%;height:20rpx;background:#eee;border-radius:10rpx;margin-top:10rpx; view styleheight:100%;background:#025ADD;border-radius:10rpx; :style{width:progressNum%}/view /view /view /view /template script import { uwbPosition } from ../../api/index.js // 场地四个角落对应的真实UWB物理坐标X东/Y北Y上大下小 const COORD { leftTopX: 0.35, leftTopY: 167.57, rightTopX: 83.27, rightTopY: 167.57, leftBottomX: 0.35, leftBottomY: 128.39, rightBottomX: 83.27, rightBottomY: 128.39 } export default { data() { return { delay: 30, // 轨迹点绘制间隔(ms)500ms画一个点位 timer: null, // 定时器标识用于逐帧渲染轨迹 canvasContainerSize: { width: 0, height: 0 }, // 画布真实宽高 trajectoryPoints: [], // 后端返回全部UWB轨迹点位数组 canvasCtx: null, // canvas绘图上下文所有绘制操作依赖它 currentIndex: 0, // 当前渲染到第几个轨迹点 progressNum: 0, // 回放进度百分比 0~100 isPause: false, // 新增暂停标记 false播放 true暂停 } }, // 页面初次渲染完成加载接口初始化画布 onReady() { // 请求后端UWB轨迹接口获取人员历史坐标 uwbPosition({ cardId: 614959, startTime: 2026-07-24 09:08:49, endTime: 2026-07-24 23:53:58 }).then(res { // 接口赋值轨迹数组无数据则为空数组 this.trajectoryPoints res.data.data || [] }) // 获取画布绘制上下文uni canvas固定写法 this.canvasCtx uni.createCanvasContext(trackCanvas, this) }, // 页面销毁清除定时器防止内存泄漏、后台持续渲染 onUnload() { if (this.timer) clearTimeout(this.timer) }, methods: { /** * 真实物理坐标(UWB) → 画布像素坐标 * param {Number} realX 真实X东 * param {Number} realY 真实Y北 * param {Number} canvasW 画布宽度 * param {Number} canvasH 画布高度 * returns {Object} {x,y} 画布像素点位 */ realToCanvas(realX, realY, canvasW, canvasH) { // 提取场地物理边界极值 const worldMaxX COORD.rightTopX; const worldMinX COORD.leftTopX; const worldMaxY COORD.leftTopY; const worldMinY COORD.leftBottomY; // 场地总物理宽、总物理高 const worldWidth worldMaxX - worldMinX; const worldHeight worldMaxY - worldMinY; // 计算X轴比例当前X占场地总宽度百分比 let xRatio (realX - worldMinX) / worldWidth; // Y轴翻转物理Y顶部大、画布Y顶部0反向换算比例 let yRatio (worldMaxY - realY) / worldHeight; // 限制比例0~1防止坐标超出画布看不见 xRatio Math.max(0, Math.min(1, xRatio)); yRatio Math.max(0, Math.min(1, yRatio)); // 比例 × 画布尺寸 最终画布像素 const canvasX xRatio * canvasW; const canvasY yRatio * canvasH; return { x: canvasX, y: canvasY } }, /** * 画布点击像素坐标 → 真实UWB物理坐标反向换算打点标定用 * param {Number} canvasX 画布点击X像素 * param {Number} canvasY 画布点击Y像素 * param {Number} canvasW 画布宽 * param {Number} canvasH 画布高 * returns {Object} {card_x,card_y} 真实物理坐标 */ canvasToReal(canvasX, canvasY, canvasW, canvasH) { const worldMaxX COORD.rightTopX; const worldMinX COORD.leftTopX; const worldMaxY COORD.leftTopY; const worldMinY COORD.leftBottomY; const worldWidth worldMaxX - worldMinX; const worldHeight worldMaxY - worldMinY; // 点击位置占画布比例 const xRatio canvasX / canvasW; const yRatio canvasY / canvasH; // 比例换算回真实物理XY const realX worldMinX xRatio * worldWidth; const realY worldMaxY - yRatio * worldHeight; return { card_x: realX, card_y: realY } }, // 画布点击触发弹出当前点位真实物理坐标 async handleCanvasTap(e) { // 先获取画布宽高 await this.getCanvasContainerRealSize() const { width, height } this.canvasContainerSize // e.detail.x/y 点击相对画布的像素坐标 const canvasX e.detail.x const canvasY e.detail.y // 反向换算物理坐标 const realPos this.canvasToReal(canvasX, canvasY, width, height) console.log(点击像素, canvasX, canvasY, 物理坐标, realPos) uni.showModal({ title: 点位物理坐标, content: X:${realPos.card_x.toFixed(2)} Y:${realPos.card_y.toFixed(2)} }) }, // 获取canvas画布渲染后的真实宽高rpx转像素实际尺寸 getCanvasContainerRealSize() { return new Promise(resolve { uni.createSelectorQuery().in(this) .select(#trackCanvas) .boundingClientRect(rect { // 存在尺寸则赋值保存 if (rect?.width) this.canvasContainerSize rect resolve() }).exec() }) }, // 暂停/继续切换 pausePlay() { // 切换暂停状态 this.isPause !this.isPause // 如果从暂停切为播放继续执行下一帧绘制 if (!this.isPause) { this.drawStep(this.canvasContainerSize.width, this.canvasContainerSize.height, this.allPointDataCache) } }, // 回放按钮点击入口初始化参数、转换全部坐标、启动逐帧绘制 async handleClick() { // 清除上一次回放定时器防止多重回放叠加 if (this.timer) clearTimeout(this.timer) this.timer null this.currentIndex 0 this.progressNum 0 this.isPause false // 重新播放默认解除暂停 // 获取画布尺寸 await this.getCanvasContainerRealSize() const { width, height } this.canvasContainerSize // 画布尺寸获取失败弹窗提示 if (!width || !height) return uni.showToast({ title: 画布获取失败, icon: none }) // 轨迹点位不足2个无法画线直接终止 if (this.trajectoryPoints.length 2) return uni.showToast({ title: 轨迹数据不足, icon: none }) // 一次性把所有后端物理坐标转为画布像素避免逐帧重复计算性能优化 const allPointData this.trajectoryPoints.map(item { const pos this.realToCanvas(Number(item.card_x), Number(item.card_y), width, height) return { ...item, px: pos } }) // 缓存全部点位暂停恢复时使用 this.allPointDataCache allPointData // 启动逐帧绘制 this.drawStep(width, height, allPointData) }, /** * 逐帧递归绘制轨迹核心渲染函数 * param {Number} canvasW 画布宽 * param {Number} canvasH 画布高 * param {Array} allPointData 全部转换完成的轨迹点位 */ drawStep(canvasW, canvasH, allPointData) { // 如果是暂停状态直接终止不再继续下一帧 if (this.isPause) return const allPx allPointData.map(d d.px) const total allPx.length // 计算当前回放进度百分比 this.progressNum Math.round((this.currentIndex / (total - 1)) * 100) // 截取从起点到当前索引的所有点位逐步延长轨迹线 const drawDataList allPointData.slice(0, this.currentIndex 1) const drawPoints drawDataList.map(d d.px) // 当前最新点位绘制人物头像 const lastData drawDataList[drawDataList.length - 1] const lastPos lastData.px const ctx this.canvasCtx // 清空整幅画布避免上次绘制残留 ctx.clearRect(0, 0, canvasW, canvasH) // 轨迹点2个才绘制连线 if (drawPoints.length 2) { ctx.beginPath() // 第一个点位移动画笔起点其余点位连线 drawPoints.forEach((p, i) i 0 ? ctx.moveTo(p.x, p.y) : ctx.lineTo(p.x, p.y)) ctx.setStrokeStyle(#130C6D) // 轨迹线颜色 ctx.setLineWidth(2) // 线条宽度 ctx.lineCap round // 线条端点圆角 ctx.lineJoin round // 线条拐角圆角 ctx.stroke() // 执行绘制线条 } // 保存当前绘图样式防止文字/图片样式污染 ctx.save() // 人物头像图片路径 const avatarImg /static/xiaoren.png const imgW 24 // 头像宽 const imgH 24 // 头像高 // 头像坐标居中在轨迹点上 const imgX lastPos.x - imgW / 2 const imgY lastPos.y - imgH / 2 // 绘制头像图片 ctx.drawImage(avatarImg, imgX, imgY, imgW, imgH) // 人员名称文字配置 const userName lastData.name || 丁丁 ctx.font bold 14px sans-serif ctx.textAlign center // 文字居中对齐 const textX lastPos.x const textY imgY - 10 // 文字放在头像上方 // 黑色文字描边增强可读性 ctx.strokeStyle #000000 ctx.lineWidth 2 ctx.strokeText(userName, textX, textY) // 白色填充文字 ctx.fillStyle #ffffff ctx.fillText(userName, textX, textY) // 恢复绘图样式 ctx.restore() // 将所有绘制指令渲染到画布上 ctx.draw() // 未绘制完所有点位定时器递归调用自身继续绘制下一个点 if (this.currentIndex allPx.length - 1) { this.currentIndex this.timer setTimeout(() { this.drawStep(canvasW, canvasH, allPointData) }, this.delay) } else { // 全部点位绘制完成弹出成功提示 uni.showToast({ title: 轨迹绘制完成, icon: success }) } } } } /script style scoped .box { background: gray; height: 100vh; } .trajectory-card { margin: 20rpx 0; width: 100%; height: 600rpx; border-radius: 15rpx; position: relative; overflow: hidden; background: #fff; } /* 背景图、画布铺满容器完全重叠 */ .map-bg, .track-canvas { width: 100%; height: 100%; position: absolute; left: 0; top: 0; } /style/static/xiaoren.png是人物图片uwbPosition是接口里返回的数据

相关新闻

TwinCAT 半导体设备测试程序架构设计:高性能、高灵活度的完整技术方案

TwinCAT 半导体设备测试程序架构设计:高性能、高灵活度的完整技术方案

摘要 本文详细阐述基于 Beckhoff TwinCAT 平台开发半导体设备自动化测试程序的完整技术方案。方案覆盖对工艺制程、配方、故障、报警、IO变量等核心要素的测试,并实现自动报告生成与日志记录。文章从技术架构、软件分层、通信驱动、UI界面四个维度进行深度设计分析…

2026/10/13 12:59:41 阅读更多 →
Rocky Linux on WSL 安装 Neo4j 完整方案

Rocky Linux on WSL 安装 Neo4j 完整方案

📋 方案概述本方案基于官方 RPM 仓库,在 Rocky Linux(运行于 WSL)系统上安装 Neo4j 图数据库。由于 WSL 的特殊网络环境,需要特别注意 IP 地址的获取和 Windows 宿主机的访问配置。🚀 详细安装步骤第一步&a…

2026/10/13 14:02:13 阅读更多 →
电商AI批量生图效率优化实践:从10张/日到100张/日的系统工程

电商AI批量生图效率优化实践:从10张/日到100张/日的系统工程

商AI单图生成耗时相对固定,单纯提升单图速度存在瓶颈。本文从系统工程角度,通过流程拆解、瓶颈分析、模板化改造、批量并行策略、质量分级管控,完成电商AI生图全链路效率优化。实测将单SKU平均作图时长由30min压缩至6.5min,日产出…

2026/10/12 14:05:34 阅读更多 →

最新新闻

Java面试如何从“背八股”到“技术深度”?一线模拟面试复盘与追问链解析

Java面试如何从“背八股”到“技术深度”?一线模拟面试复盘与追问链解析

1. 面试官视角的一次模拟:为什么背题高分选手反而被刷有位程序员朋友备考了两个月,把网上能搜到的Java面试题背得滚瓜烂熟,结果在一场模拟面试里连前面十分钟都没撑过。他描述某个项目的技术方案时,堆了一串名词,却说不…

2026/10/13 14:02:53 阅读更多 →
智能体权限安全实战:AI拥有操作能力后,如何守住权限边界?

智能体权限安全实战:AI拥有操作能力后,如何守住权限边界?

最近在做一个Agent项目时,权限配置把我折腾得不轻。模型的对话能力早就不是瓶颈了,真正让人夜里睡不着的是:当AI拿着工具去操作真实系统时,你怎么保证它只做该做的事?这周把一个跨平台的智能体接进内部工单系统试跑&am…

2026/10/13 14:02:53 阅读更多 →
AI落地场景验证的五大基础设施:算力、数据、模型、平台与场景

AI落地场景验证的五大基础设施:算力、数据、模型、平台与场景

最近一年我接触了不少AI落地项目,一个特别明显的感受是:大家终于不再纠结“大模型能不能做这件事”,而是开始纠结“在我们这个场景里,它能不能稳定、可控、划算地用起来”。这两句话之间隔着的那段路,就是场景验证。标…

2026/10/13 14:02:53 阅读更多 →
从HTML网页到可查询知识图谱:规则抽取与可视化实战

从HTML网页到可查询知识图谱:规则抽取与可视化实战

简介:这是一套从非结构化文本自动构造知识图的端到端管道,名为 KnowledgeGraph_Builder,面向 NLP 爱好者、算法工程师及知识图谱研究者。项目借助 spaCy、Stanford CoreNLP、Huggingface Neural Coref 和句子转换器等工具,实现了命…

2026/10/13 14:02:53 阅读更多 →
从大厂面经切入Agent开发:用真实场景重构AI工程能力

从大厂面经切入Agent开发:用真实场景重构AI工程能力

1. 项目概述:这不是刷题,而是一场面向真实工程的Agent能力重构“从面经开始的agent开发学习”——这个标题乍看像程序员求职路上的临时抱佛脚,实则藏着一条被多数人忽略的隐性路径:面经不是终点,而是Agent开发最密集、…

2026/10/13 14:02:53 阅读更多 →
Logstash离线插件安装实战:依赖解析、打包与验证全流程

Logstash离线插件安装实战:依赖解析、打包与验证全流程

1. 为什么Logstash插件必须离线安装——一个被低估的生产环境现实Logstash作为ELK栈中承上启下的数据管道核心,其价值从来不在“能跑起来”,而在于“能在哪跑、怎么稳跑、出问题时能否快速恢复”。我见过太多团队在测试环境用bin/logstash-plugin instal…

2026/10/13 14:01:51 阅读更多 →

日新闻

物联网系统集成避坑指南:从选型到验收的关键要点

物联网系统集成避坑指南:从选型到验收的关键要点

做了这么多年物联网系统集成的项目,我最大的感受是,大部分坑不是出在技术前沿,而是出在一些看起来很平常的地方。设备选了便宜的、协议没问清楚、现场没摸透、验收标准写得含糊——这些细枝末节,最后都会变成延期、超支和扯皮的大…

2026/10/13 0:00:57 阅读更多 →
ESP32舵机控制全攻略:从PWM原理到高级玩法与常见故障排查

ESP32舵机控制全攻略:从PWM原理到高级玩法与常见故障排查

如果你玩ESP32有一阵子了,很难不碰到舵机控制这道坎。做智能小车要转方向,做机械臂要抬关节,做云台要偏航俯仰,背后都是同一个东西在干活:舵机。而我见过太多人卡在“代码能编译、舵机就是不听话”这个阶段&#xff0c…

2026/10/13 0:00:57 阅读更多 →
CAN总线原理与STM32实战:抗干扰、仲裁、错误处理全解析

CAN总线原理与STM32实战:抗干扰、仲裁、错误处理全解析

1. 为什么CAN总线至今仍是工业现场的“硬通货”?——从汽车底盘到智能温室的真实需求你有没有拆过一辆老款燃油车的中控台?或者调试过某高校实验室里那台跑着Modbus协议却频频丢帧的PLC控制柜?又或者,在某个农业物联网项目里&…

2026/10/13 0:00:57 阅读更多 →

周新闻

流感时间序列预测实战:ARIMA/LSTM全流程拆解与避坑指南

流感时间序列预测实战:ARIMA/LSTM全流程拆解与避坑指南

简介:基于 ARIMA、LSTM、Transformer 等模型的流感时间序列预测 Python 源码,面向计算机相关专业课程设计与期末大作业学生,以及项目实战学习者。内容覆盖预处理、平稳性检验、定阶、残差分析、多模型对比预测的完整时序建模流程,…

2026/10/13 2:54:56 阅读更多 →
影刀RPA新手教程:键盘模拟输入实战——输入文本与模拟按键的区别

影刀RPA新手教程:键盘模拟输入实战——输入文本与模拟按键的区别

影刀RPA新手教程:键盘模拟输入实战——输入文本与模拟按键的区别 做影刀RPA自动化,十个新手有八个栽在"往输入框里填东西"这件事上:要么填不进去,要么填了一半,要么直接把原来内容追加在后面。这背后的根因&…

2026/10/12 0:16:38 阅读更多 →
影刀RPA新手教程:阅文起点小说数据采集实战——书籍信息与章节内容

影刀RPA新手教程:阅文起点小说数据采集实战——书籍信息与章节内容

影刀RPA新手教程:阅文起点小说数据采集实战——书籍信息与章节内容 1. 认识影刀:什么场景该用RPA采小说数据 起点中文网的页面结构相对稳定——分类榜单、书籍详情、章节内容三块独立页面,跳转链路清晰。这种场景非常适合影刀自动化&#x…

2026/10/13 1:58:08 阅读更多 →

月新闻

我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频

我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/12 21:05:09 阅读更多 →
Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证

Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/12 14:48:49 阅读更多 →
黑夜航拍船只数据集训练YOLOV5模型全流程解析

黑夜航拍船只数据集训练YOLOV5模型全流程解析

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/12 18:11:17 阅读更多 →