iT邦幫忙

2026 iThome 鐵人賽

DAY 24
0

昨天把六場全馬疊成線圖及色塊圖,六條資料同時攤在畫面上。

圖回答的是整場的狀況長什麼樣,但它秀不出另一種問題:跑到第 90 分鐘的時候,誰在前面?以及該場的配速及距離目前是多少。

所以今天做回放—六場同時起跑,按時間軸重播一次。


先分享之前的一個練習

在大綱中原本有規劃到「WebSocket vs SSE:即時推播技術選型」。

當初這樣排是因為之前想要練習 websocket,想說可以結合馬拉松資料的推播,來即時重現跑步當下的配速及心律,

但「即時」兩個字,回放看起來就是即時更新的東西,和馬拉松已發生的資料相比有點矛盾。

實作到一半才發現:沒有任何東西需要推播。

六場的逐秒資料在頁面載入的時候就全部進到瀏覽器裡了,回放做的事情,只是按時間軸重新讀一遍已經在記憶體裡的陣列

伺服器沒有東西可以推。

推播是為了「資料在別的地方、而且會變」—即時比賽的定位、股價、聊天訊息,而我的資料是四個月前跑完的,它不會再改變。

所以需要的只有一個東西:一個跟著螢幕更新率走的計時器。


requestAnimationFrame,不是 setInterval

直覺會寫成這樣:

setInterval(() => { t += 1; }, 1000 / 60);

會遇到兩個問題:

第一,間隔不保證準。 setInterval 只承諾至少多久之後執行,實際上要排隊等主執行緒,畫面在忙的時候他可能就會被延遲,而遲到的量不會被補回來。

第二,分頁切到背景會被降頻。 Chrome 的文件寫得很明確:從 Chrome 11 開始,背景分頁的每個 timer「no more than once per second」。

所以切去別的分頁三十秒再回來,回放的時間軸只前進了三十次而不是一千八百次—畫面上的時間會跟以為的對不上。

(Chrome 88 之後還有更重的:分頁隱藏超過五分鐘又沒有聲音的話,變成一分鐘才檢查一次。)

requestAnimationFrame 兩個都解決了:它跟著螢幕的更新頻率走,而且背景分頁直接暫停,回來接著跑。


但 rAF 不能每幀加固定值

function tick() {
  t += 1 / 60;          // ❌
  requestAnimationFrame(tick);
}

這行假設 rAF 一秒會呼叫 60 次,但它一秒呼叫幾次,是使用者的螢幕更新率決定的,不是這邊決定的。

120Hz 的螢幕一秒收到 120 次呼叫,這樣寫會播兩倍快。省電模式降到 30Hz 就變半速。

正確的做法是用 delta time—問「距離上一幀過了多久」:

function tick(now: number) {
  if (!playing.value) return;
  // 第一幀沒有前一幀可以比,先記下來就好
  const dt = last ? (now - last) / 1000 : 0;
  last = now;
  t.value += dt * speed.value;
  ...
}

now 是 rAF 傳進來的時間戳,這樣不管螢幕幾赫茲,一秒就是一秒

至於 last ? ... : 0 也是必要的:第一幀沒有前一幀,不處理的話 now - undefined 會得到 NaN,然後整個時間軸壞掉。


每一秒要顯示什麼

圖上六場各有一個記號,隨著播放各自往右走。

但只有點在動的話,你看得到誰在前面,看不到他當下跑得多快,所以清單那邊要顯示即時資訊。

第一版直接讀那一秒的值:

const s = stateAt(race.points, t);   // 二分搜尋,逐秒陣列有一萬多筆
// s.p 就是那一秒的配速

結果數字閃到看不懂。

因為 GPS 算出來的瞬時配速抖得很兇—同一段路,逐秒的值可能在 4:30 和 5:10 之間跳。60 倍速播放的時候,那個數字每秒變一百次。

所以改成取區間平均:

/** 某場比賽在第 sec 秒前後 window 秒的平均配速與心率。
 *
 *  不能直接讀那一秒的值 — GPS 算出來的瞬時配速抖得嚴重,
 *  播放的時候數字會閃到看不懂,取一個區間平均才讀得出來。 */
export function windowAt(pts, sec, window = 30) { ... }

前後 30 秒 ,這個數字是量出來的—窗口越寬數字越穩,但也越慢反應:

窗口 數字每秒跳動 落後真實變化
10 秒 1.00 秒/km 5 秒
30 秒 0.58 15 秒
60 秒 0.31 30 秒
120 秒 0.17 60 秒

30 秒是自己測試幾次所決定的,60 秒更穩,但一場全馬裡三十秒的落後已經看得出來。

而且畫面上要標出來—清單底下寫著「距離 · 配速 · 心率(30 秒平均)」,那不是那一秒的真實值,提醒使用者情況。


那份清單同時在做三件事

之前提過六條線需要能個別關掉,所以有一份清單當圖例兼選擇器,回放的時候,那份清單整列換內容

沒播放的時候,顯示的是這場比賽的結果:

● 東京馬        3:22:07    4:44    12.2°
● 臺北馬 24     3:28:41    4:54    15.6°

播放的時候,換成當下的狀態,而且依距離即時重排

1  ● 東京馬      12.40k   4:41   162
2  ● 臺北馬 24   11.82k   4:58   171
3  ● 台東 CT      10.05k   5:34   168

因為溫度只有起跑時有記錄,所以起跑後不顯示溫度,顯示當下的心率資料。

https://ithelp.ithome.com.tw/upload/images/20260909/20183598rRBvywUOeD.jpg

當看到起跑後六場各自顯示這確認速度及排序順序,真的有種說不出的感動

每一場都是自己努力的過程,而自己也持續超越自己!


明天

到今天為止我們把六場、七萬多個點、一秒一秒放回去,比較是把比賽攤開來看。

明天反過來—把一整場壓成一個數字 VDOT,用一場成績反推「跑力」,

再拿那個數字去預測其他距離,並且跟 Garmin 自己算的預測值對答案。



上一篇
Day 23|訓練 Dashboard 手機版:平均取樣拉長三倍,線反而更亂
系列文
教練看不到的那六天|從 FIT 檔到 3D 軌跡,馬拉松訓練資料 Dashboard24
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言