作为一名热爱跑步的程序员,我一直想把自己的马拉松经历以一种独特的方式呈现在博客上。普通的文字和静态图片虽然能记录当下,但无法还原那 21.0975 公里的心路历程。
于是,我决定在博客的首屏实现一个动态的运动轨迹可视化效果:以高德地图为画布,实时绘制 GPX 轨迹,并同步展示心率、配速、步频等专业运动数据。
本文将详细拆解这一功能的实现过程。

1. 效果预览
最终实现的效果包含以下核心要素:
- 全屏沉浸式体验:首屏加载高德地图深色模式,配合打字机效果的 Slogan。
- 动态轨迹绘制:轨迹线随着时间推移像贪吃蛇一样在地图上延伸。
- 关键里程碑:自动识别并标注 5km, 10km, 半马, 全马等关键节点。
- 实时数据面板:右上角悬浮窗实时显示运动时间、距离、当前配速、实时心率和步频。
- 心率趋势图:集成 Chart.js 动态绘制心率曲线。
2. 技术选型与准备
- 地图服务:高德地图 JS API v2.0(支持 3D 视图和自定义样式)。
- 数据源:运动手表(如 COROS, Garmin)导出的
.gpx 文件,我这里使用的是COROS。
- 图表库:Chart.js(轻量级,适合绘制简单的趋势图)。
- 框架:Hexo (EJS 模板引擎)。
申请高德地图 Key
首先需要在高德开放平台注册账号并创建 Web 端 (JS API) 应用,获取 Key 和 安全密钥 (Security Code)。
3. 核心实现步骤
3.1 页面布局 (Layout)
在 layout.ejs 中,我们需要一个全屏的容器来放置地图,以及覆盖在上面的数据面板。
<div class="hero-splash" id="hero-splash"> <div class="marathon-bg"> <div id="amap-container"></div> </div> <div class="stats-overlay" id="stats-overlay"> <div class="stats-header"> <div class="stats-title">运动数据</div> <div class="stats-time" id="stats-time">00:00:00</div> </div> <div class="stats-grid"> </div> <div class="chart-container"> <canvas id="elevation-chart"></canvas> </div> </div> <div class="hero-content"> <span id="typing-text"></span><span class="cursor">|</span> </div> </div>
|
CSS 方面,关键是将 .hero-splash 设置为 fixed 定位并拥有最高的 z-index,确保它覆盖在所有内容之上。
3.2 解析 GPX 数据
GPX 本质上是 XML 格式。我们使用浏览器原生的 DOMParser 来解析它。除了基础的经纬度 (lat, lon),我们还需要提取 <extensions> 中的心率和步频数据。
fetch('/marathon.gpx') .then(response => response.text()) .then(str => { const parser = new DOMParser(); const xmlDoc = parser.parseFromString(str, "text/xml"); const trkpts = xmlDoc.getElementsByTagName("trkpt"); const points = []; const stats = []; for (let i = 0; i < trkpts.length; i++) { const pt = trkpts[i]; const extensions = pt.getElementsByTagName("extensions")[0]; let hr = 0, cadence = 0; if (extensions) { const hrTag = extensions.getElementsByTagName("gpxdata:hr")[0] || extensions.getElementsByTagName("ns3:hr")[0]; const cadTag = extensions.getElementsByTagName("gpxdata:cadence")[0] || extensions.getElementsByTagName("ns3:cadence")[0]; if (hrTag) hr = parseInt(hrTag.textContent); if (cadTag) cadence = parseInt(cadTag.textContent) * 2; } points.push({ lat: parseFloat(pt.getAttribute("lat")), lon: parseFloat(pt.getAttribute("lon")) }); stats.push({ time: new Date(pt.getElementsByTagName("time")[0].textContent).getTime(), hr: hr, cadence: cadence }); } renderAMapPath(points, stats); });
|
3.3 高德地图初始化与轨迹绘制
初始化地图时,为了视觉效果,我们禁用了所有交互(拖拽、缩放),并使用了深色主题。
const map = new AMap.Map('amap-container', { viewMode: '3D', zoom: 10, mapStyle: "amap://styles/dark", zoomEnable: false, dragEnable: false, });
|
绘制动态轨迹的核心思路是:创建一个空的 Polyline,然后在 requestAnimationFrame 循环中不断向其路径数组添加新的点。
const polyline = new AMap.Polyline({ path: [], strokeColor: "#3366FF", strokeWeight: 6, lineJoin: 'round', lineCap: 'round', zIndex: 50, }); map.add(polyline);
function animate(timestamp) { const progress = Math.min((timestamp - startTime) / duration, 1); const ease = 1 - Math.pow(1 - progress, 4); const currentIdx = Math.floor(ease * pathArr.length); const currentPath = pathArr.slice(0, currentIdx + 1); if (currentPath.length > 0) { polyline.setPath(currentPath); } updateStats(currentIdx); if (progress < 1) requestAnimationFrame(animate); }
|
3.4 动态心率图表
使用 Chart.js 创建一个折线图。为了性能,我们不需要渲染所有点,而是进行降采样(Sample)。
const ctx = document.getElementById('elevation-chart').getContext('2d');
const sampleRate = Math.ceil(stats.length / 100); const chartData = stats.filter((_, i) => i % sampleRate === 0).map(s => s.hr);
new Chart(ctx, { type: 'line', data: { labels: chartLabels, datasets: [{ data: chartData, borderColor: themeColor, backgroundColor: gradient, fill: true, pointRadius: 0, tension: 0.4 }] }, options: { animation: false, scales: { x: { display: false }, y: { display: false } } } });
|
4. 遇到的坑与优化
- 高德地图 Polyline 颜色:高德 API 不支持 CSS 变量(如
var(--color))。解决方案是在 JS 中使用 getComputedStyle 动态读取 CSS 变量的十六进制值。
- 步频数据偏差:发现显示的步频只有 80-90,远低于正常跑步的 170-180。经排查,GPX 记录的是单边步频 (SPM),展示时乘以 2 即可修复。
- CDN 加载慢:Chart.js 默认 CDN 在国内访问较慢,替换为
cdnjs.cloudflare.com 后速度显著提升。
- 层级遮挡:地图容器层级较高,导致自定义的“向下滚动”箭头不可见。通过设置箭头
z-index: 20 并添加文字阴影解决了此问题。
5. 结语
通过这次折腾,不仅让博客首页变得“硬核”且充满个人特色,也通过技术手段回顾了自己的跑步历程。每当打开首页,看着那条蜿蜒的轨迹线被点亮,看着心率曲线的起伏,仿佛又回到了赛道上。
“人生是一场马拉松,安全完赛才是最重要的。”
希望这个教程能给同样喜欢运动和编码的你一些灵感。
文章作者:阿文
版权声明:本博客所有文章除特别声明外,均采用
CC BY-NC-SA 4.0 许可协议。转载请注明来自
阿文的博客!