深夜提醒

现在是深夜,建议您注意休息,不要熬夜哦~

🏮 🏮 🏮

新年快乐

祝君万事如意心想事成!

share-image
中
ESC

Integrating AMap and GPX Running-Track Visualization into the Blog's Hero Screen

English 阅读中文版

As a programmer who loves running, I have always wanted to present my marathon experiences on the blog in a unique way. Ordinary text and static images can capture a moment, but they cannot recreate the inner journey of those 21.0975 kilometers.

So I decided to build a dynamic running-track visualization on the blog’s hero screen: using AMap as the canvas, drawing the GPX track in real time, and showing heart rate, pace, cadence and other professional running data alongside it.

This article breaks down the implementation of this feature in detail.

1. Preview

The finished effect contains these core elements:

  • Full-screen immersive experience: the hero screen loads AMap in dark mode, paired with a typewriter-effect slogan.
  • Dynamic track drawing: the route line extends across the map over time, like a game of Snake.
  • Key milestones: automatically detects and labels key points such as 5km, 10km, half marathon and full marathon.
  • Real-time data panel: a floating window in the top-right shows elapsed time, distance, current pace, live heart rate and cadence.
  • Heart-rate trend chart: integrates Chart.js to draw the heart-rate curve dynamically.

2. Tech Stack and Preparation

  • Map service: AMap JS API v2.0 (supports 3D views and custom styles).
  • Data source: .gpx files exported from sports watches (such as COROS, Garmin); I use COROS here.
  • Chart library: Chart.js (lightweight, well suited to simple trend charts).
  • Framework: Hexo (EJS templating engine).

Applying for an AMap Key

First, register an account on the AMap Open Platform and create a Web (JS API) application to get a Key and a Security Code.

3. Core Implementation Steps

3.1 Page Layout

In layout.ejs, we need a full-screen container for the map and a data panel overlaid on top of it.

<div class="hero-splash" id="hero-splash">
<!-- Map background layer -->
<div class="marathon-bg">
<div id="amap-container"></div>
</div>

<!-- Data overlay panel -->
<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">
<!-- Distance, pace, heart rate, cadence and other data items -->
</div>
<!-- Heart-rate chart container -->
<div class="chart-container">
<canvas id="elevation-chart"></canvas>
</div>
</div>

<!-- Slogan content -->
<div class="hero-content">
<span id="typing-text"></span><span class="cursor">|</span>
</div>
</div>

On the CSS side, the key is to set .hero-splash to fixed positioning with the highest z-index so it covers everything else.

3.2 Parsing GPX Data

GPX is essentially XML. We use the browser’s native DOMParser to parse it. Besides the basic latitude/longitude (lat, lon), we also need to extract heart rate and cadence from <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];
// Parse extended data (handles namespaces from different vendors)
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);
// Note: some devices record single-sided cadence; multiply by 2 to convert to double-sided cadence
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 Initializing AMap and Drawing the Track

When initializing the map, for visual effect we disable all interactions (drag, zoom) and use the dark theme.

const map = new AMap.Map('amap-container', {
viewMode: '3D',
zoom: 10,
mapStyle: "amap://styles/dark", // Dark mode
zoomEnable: false,
dragEnable: false,
// ... other disabled options
});

The core idea for drawing the dynamic track is: create an empty Polyline, then keep pushing new points onto its path array inside a requestAnimationFrame loop.

// Create the dynamic polyline
const polyline = new AMap.Polyline({
path: [],
strokeColor: "#3366FF", // Get the theme color dynamically
strokeWeight: 6,
lineJoin: 'round',
lineCap: 'round',
zIndex: 50,
});
map.add(polyline);

function animate(timestamp) {
// Compute progress (0.0 - 1.0)
const progress = Math.min((timestamp - startTime) / duration, 1);
const ease = 1 - Math.pow(1 - progress, 4); // EaseOutQuart easing

// Take the path points for the current progress
const currentIdx = Math.floor(ease * pathArr.length);
const currentPath = pathArr.slice(0, currentIdx + 1);

// Update the polyline
if (currentPath.length > 0) {
polyline.setPath(currentPath);
}

// Update the data panel in sync (code omitted)
updateStats(currentIdx);

if (progress < 1) requestAnimationFrame(animate);
}

3.4 Dynamic Heart-Rate Chart

Use Chart.js to create a line chart. For performance we do not render every point but downsample instead.

const ctx = document.getElementById('elevation-chart').getContext('2d');
// Downsampling: take one point out of every 100
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, // Gradient fill
fill: true,
pointRadius: 0, // Hide data points, show only the line
tension: 0.4 // Smooth curve
}]
},
options: {
animation: false, // Disable the initial animation, since the data itself is moving
scales: { x: { display: false }, y: { display: false } }
}
});

4. Pitfalls and Optimizations

  1. AMap Polyline color: the AMap API does not support CSS variables (like var(--color)). The fix is to read the hex value of the CSS variable dynamically in JS with getComputedStyle.
  2. Cadence value off: the displayed cadence was only 80–90, far below the normal 170–180 for running. On investigation, the GPX records single-sided cadence (SPM); multiplying by 2 when displaying it fixes it.
  3. Slow CDN: Chart.js’s default CDN is slow to reach from within China; switching to cdnjs.cloudflare.com noticeably sped things up.
  4. Stacking overlay: the map container had a high z-index, making the custom “scroll down” arrow invisible. Setting the arrow’s z-index: 20 and adding a text shadow fixed it.

5. Conclusion

Through this bit of tinkering, I not only made the blog homepage look “hardcore” and full of personal character, but also looked back on my own running journey through technology. Whenever I open the homepage and watch that winding track light up, watch the heart-rate curve rise and fall, it feels as if I am back on the course.

“Life is a marathon; finishing safely is what matters most.”

I hope this tutorial gives some inspiration to those of you who love sports and coding as much as I do.

文章作者:阿文
文章链接: https://www.awen.me/post/3b9a6720.html
版权声明:本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明来自 阿文的博客!
本文于 2026-01-28 21:30 发布,已超过半年(261天),请注意甄别内容是否已过期。

评论

0 条评论
😀😃😄 😁😅😂 🤣😊😇 🙂🙃😉 😌😍🥰 😘😗😙 😚😋😛 😝😜🤪 🤨🧐🤓 😎🥸🤩 🥳😏😒 😞😔😟 😕🙁☹️ 😣😖😫 😩🥺😢 😭😤😠 😡🤬🤯 😳🥵🥶 😱😨😰 😥😓🤗 🤔🤭🤫 🤥😶😐 😑😬🙄 😯😦😧 😮😲🥱 😴🤤😪 😵🤐🥴 🤢🤮🤧 😷🤒🤕 🤑🤠😈 👿👹👺 🤡💩👻 💀☠️👽 👾🤖🎃 😺😸😹 😻😼😽 🙀😿😾 👍👎👏 🙌👐🤲 🤝🤜🤛 ✌️🤞🤟 🤘👌🤏 👈👉👆 👇☝️✋ 🤚🖐️🖖 👋🤙💪 🦾🖕✍️ 🙏💅🤳 💯💢💥 💫💦💨 🕳️💣💬 👁️‍🗨️🗨️🗯️ 💭💤❤️ 🧡💛💚 💙💜🖤 🤍🤎💔 ❣️💕💞 💓💗💖 💘💝💟 ☮️✝️☪️ 🕉️☸️✡️ 🔯🕎☯️ ☦️🛐⛎ ♈♉♊ ♋♌♍ ♎♏♐ ♑♒♓ 🆔⚛️🉑 ☢️☣️📴 📳🈶🈚 🈸🈺🈷️ ✴️🆚💮 🉐㊙️㊗️ 🈴🈵🈹 🈲🅰️🅱️ 🆎🆑🅾️ 🆘❌⭕ 🛑⛔📛 🚫💯💢 ♨️🚷🚯 🚳🚱🔞 📵🚭❗ ❕❓❔ ‼️⁉️🔅 🔆〽️⚠️ 🚸🔱⚜️ 🔰♻️✅ 🈯💹❇️ ✳️❎🌐 💠Ⓜ️🌀 💤🏧🚾 ♿🅿️🈳 🈂🛂🛃 🛄🛅🛗 🚀🛸🚁 🚉🚆🚅 ✈️🛫🛬 🛩️💺🛰️
加载中...

留言反馈

😀😃😄 😁😅😂 🤣😊😇 🙂🙃😉 😌😍🥰 😘😗😙 😚😋😛 😝😜🤪 🤨🧐🤓 😎🥸🤩 🥳😏😒 😞😔😟 😕🙁☹️ 😣😖😫 😩🥺😢 😭😤😠 😡🤬🤯 😳🥵🥶 😱😨😰 😥😓🤗 🤔🤭🤫 🤥😶😐 😑😬🙄 😯😦😧 😮😲🥱 😴🤤😪 😵🤐🥴 🤢🤮🤧 😷🤒🤕 🤑🤠😈 👿👹👺 🤡💩👻 💀☠️👽 👾🤖🎃 😺😸😹 😻😼😽 🙀😿😾 👍👎👏 🙌👐🤲 🤝🤜🤛 ✌️🤞🤟 🤘👌🤏 👈👉👆 👇☝️✋ 🤚🖐️🖖 👋🤙💪 🦾🖕✍️ 🙏💅🤳 💯💢💥 💫💦💨 🕳️💣💬 👁️‍🗨️🗨️🗯️ 💭💤❤️ 🧡💛💚 💙💜🖤 🤍🤎💔 ❣️💕💞 💓💗💖 💘💝💟 ☮️✝️☪️ 🕉️☸️✡️ 🔯🕎☯️ ☦️🛐⛎ ♈♉♊ ♋♌♍ ♎♏♐ ♑♒♓ 🆔⚛️🉑 ☢️☣️📴 📳🈶🈚 🈸🈺🈷️ ✴️🆚💮 🉐㊙️㊗️ 🈴🈵🈹 🈲🅰️🅱️ 🆎🆑🅾️ 🆘❌⭕ 🛑⛔📛 🚫💯💢 ♨️🚷🚯 🚳🚱🔞 📵🚭❗ ❕❓❔ ‼️⁉️🔅 🔆〽️⚠️ 🚸🔱⚜️ 🔰♻️✅ 🈯💹❇️ ✳️❎🌐 💠Ⓜ️🌀 💤🏧🚾 ♿🅿️🈳 🈂🛂🛃 🛄🛅🛗 🚀🛸🚁 🚉🚆🚅 ✈️🛫🛬 🛩️💺🛰️