昨天終於把那張雷達圖畫出來了。
從 Day 11 開始累積的:
日常:5
效率:8
社交:3
心態:6
放鬆:9
職涯:4
到了 Day 12,已經不再只是躺在 JavaScript 裡的一堆數字。
它們真的變成了一個六角形。
而且還是我自己用 Canvas 畫出來的。
老實說,第一次看到它成功出現在網頁上的時候,還是有一點爽。
畢竟前面才在那邊算:
Math.cos()
Math.sin()
Math.PI
當時腦袋裡其實只有一個想法:
「到底為什麼畫個圖還要數學?」
結果現在圖真的出來了。
但盯著畫面看了一下之後,我又開始覺得……
好像少了什麼。
因為現在的雷達圖是直接出現的。
使用者做完測驗,結果一跳出來:
啪!
整張圖就在那裡。
沒有任何過程。
如果今天只是資料分析工具,這當然沒什麼問題。
但我們做的是「賽博紓壓診所」。
都已經走到這一步了,
如果最後只是把圖硬塞到畫面上,好像有點太安靜。
所以今天不新增什麼超大的功能。
我們先讓昨天那張圖:
動起來。
其實我一開始想的動畫很簡單。
就是:
原本:
●
● ●
● ●
●
變成:
/\
/ \
/ \
/ \
也就是不要讓雷達圖直接從完整狀態出現,
而是從中心慢慢往外長。
例如今天的「放鬆」是 9 分。
那就不要直接畫到 9。
而是:
0
↓
1
↓
2
↓
3
↓
……
↓
9
六個方向一起慢慢長出去。
這樣看起來就會比較像:
系統正在分析你的結果。
而不是:
系統已經算完了,請下一位。
這時候問題就來了。
Canvas 本身並不會自己動畫。
我們昨天寫的:
drawRadarChart(userScores);
基本上就是:
畫一次。
畫完就沒事了。
所以今天要做的事情其實很明確:
讓 Canvas 不只畫一次,而是一直重畫。
這次我不需要把 Day 12 那整大串 Canvas 程式重新貼一次。
因為六角形怎麼畫、角度怎麼算、座標怎麼找,昨天已經處理好了。
今天只改一件事情:
讓原本的分數可以慢慢增加。
首先新增一個動畫進度:
let animationProgress = 0;
這個東西可以把它想成目前動畫跑到哪裡。
一開始:
0
代表什麼都還沒長出來。
最後:
1
代表已經完成。
所以如果某個維度原本是:
8
那動畫進行到一半:
8 × 0.5 = 4
就只先畫到 4。
這樣就可以讓原本的雷達圖慢慢長出來。
drawRadarChart()昨天的函式是:
function drawRadarChart(scores) {
今天多塞一個參數:
function drawRadarChart(
scores,
progress = 1
) {
這樣如果沒有特別指定 progress,
它還是會正常畫完整的圖。
接著找到昨天計算資料半徑的地方:
const valueRadius =
radius
* value
/ maxScore;
改成:
const animatedValue =
value * progress;
const valueRadius =
radius
* animatedValue
/ maxScore;
這裡其實就是今天的核心。
原本:
value
直接拿來畫。
現在變成:
value
↓
animatedValue
↓
畫到 Canvas
例如:
原始分數:8
progress = 0
→ 0
progress = 0.25
→ 2
progress = 0.5
→ 4
progress = 0.75
→ 6
progress = 1
→ 8
所以同一張雷達圖,就可以從中心慢慢往外跑。
光有 progress 還是不夠。
因為如果我只執行:
drawRadarChart(
userScores,
0.5
);
它還是只會停在 50%。
我們需要讓瀏覽器一直幫我們更新。
這裡就輪到:
requestAnimationFrame()
登場。
我最後寫成:
let animationProgress = 0;
function animateRadarChart() {
animationProgress += 0.02;
if (animationProgress > 1) {
animationProgress = 1;
}
drawRadarChart(
userScores,
animationProgress
);
if (animationProgress < 1) {
requestAnimationFrame(
animateRadarChart
);
}
}
最後把原本:
drawRadarChart(userScores);
換成:
animateRadarChart();
就可以開始跑了。
requestAnimationFrame 到底在幹嘛?這個名字看起來有點長。
但它在這裡做的事情其實很直白:
「瀏覽器,下一次準備更新畫面的時候,再叫我一次。」
所以整個流程會變成:
animateRadarChart()
↓
progress + 0.02
↓
重新畫 Canvas
↓
還沒到 1?
↓
requestAnimationFrame()
↓
再跑一次
↓
progress + 0.02
↓
重新畫 Canvas
↓
……
↓
progress = 1
↓
結束
這也是為什麼雷達圖會有「長出來」的感覺。
不是 Canvas 突然會動。
而是我們一直在很短的時間內重新畫它。
做到這裡,我原本以為:
好,完成。
結果實際跑起來之後……
嗯。
有動。
但有點太規律。
它就是:
0
0.02
0.04
0.06
0.08
……
平均地往上加。
看起來很像程式正在很認真地執行一個迴圈。
不是不好。
就是……
太直了。
所以我又多做了一個小調整。
把原本的:
animationProgress
稍微轉換一下。
const easedProgress =
1 - Math.pow(
1 - animationProgress,
3
);
然後畫圖時:
drawRadarChart(
userScores,
easedProgress
);
這樣動畫前面會比較慢,
接近結束時再慢慢停下來。
簡單來說就是:
原本:
快 → 快 → 快 → 快
現在:
慢 → 中 → 快 → 慢
看起來就自然很多。
這種東西如果只看程式碼,
可能會覺得:
「不就一個公式?」
但真的放到畫面上,
差異真的還滿明顯的!
既然雷達圖都已經會慢慢出現了,
那如果文字一開始就全部塞出來,
又會有點破壞氣氛。
所以我乾脆讓結果等雷達圖跑完再出現。
先準備一個區塊:
<div class="result-summary">
<h2>🧠 分析完成</h2>
<p id="result-message">
正在分析你的腦內波長……
</p>
</div>
然後在動畫結束的地方:
if (animationProgress < 1) {
requestAnimationFrame(
animateRadarChart
);
}
else {
showResultMessage();
}
再寫一個:
function showResultMessage() {
const message =
document.getElementById(
"result-message"
);
message.innerText =
"分析完成,歡迎查看你的賽博狀態。";
}
這樣整個流程就變成:
完成題目
↓
開始分析
↓
雷達圖慢慢展開
↓
動畫完成
↓
顯示結果文字
至少比所有東西一次跳出來有層次一點。
既然都已經讓它延後出現了,
那我希望結果文字不要直接「啪」地跳出來。
所以可以利用 CSS 的 opacity 和 transform:
.result-summary {
margin-top: 20px;
padding: 20px;
border-radius: 15px;
background:
rgba(255, 255, 255, 0.05);
opacity: 0;
transform: translateY(10px);
transition:
opacity 0.6s ease,
transform 0.6s ease;
}
.result-summary.show {
opacity: 1;
transform: translateY(0);
}
JavaScript 再加上:
function showResultMessage() {
const result =
document.getElementById(
"result-message"
);
const box =
document.querySelector(
".result-summary"
);
result.innerText =
"分析完成,歡迎查看你的賽博狀態。";
box.classList.add("show");
}
這樣結果文字就會慢慢浮上來。
沒有什麼很誇張的特效。
但整個畫面的感覺就會比較完整。
這次其實沒有增加什麼很大的功能。
甚至跟 Day 12 比起來,Code 還少了不少。
但我覺得這一天滿有意思的。
因為以前我比較容易把重點放在:
「功能有沒有成功?」
例如:
可是做到現在,我開始會注意:
「使用者看到這個畫面的時候,感覺怎麼樣?」
這兩件事其實不太一樣。
同樣都是一張雷達圖。
一種是:
使用者做完測驗
↓
圖直接出現
另一種是:
使用者做完測驗
↓
開始分析
↓
雷達圖慢慢展開
↓
分析完成
↓
結果出現
資料完全沒有變。
但體驗就不一樣。
而且這次也讓我稍微體會到前端很煩、但又滿有趣的一點。
有時候為了讓畫面「看起來比較自然」,
可能只是多改幾行 Code。
可是這幾行 Code 放進去之後,
整個網站的感覺就會變了。
所以我現在比較不會覺得:
「這個動畫好像沒什麼必要。」
如果它能讓整個流程更順,
那它就有存在的理由。
當然,也不能什麼都加。
如果每個按鈕都在轉、每個文字都在飛,
最後大概會變成:
賽博紓壓診所:眼睛壓力測試中心。
這就跟原本目的背道而馳了。
所以這次我還是讓動畫保持簡單。
有動就好。
不要整個網站一起移動www
到今天為止,我們的測驗流程又往前走了一步。
從 Day 11 的:
回答問題
↓
累積六大維度分數
Day 12 的:
六大維度分數
↓
Canvas 雷達圖
Day 13 的:
雷達圖
↓
動畫
↓
結果資訊
原本冷冰冰的:
日常:5
效率:8
社交:3
心態:6
放鬆:9
職涯:4
現在已經開始有一點「結果頁」的樣子了。
但我做到這裡,又冒出一個新的問題。
現在我們只是把六個分數畫得比較漂亮。
可是……
這六個數字到底代表什麼?
如果使用者最後只看到:
放鬆 9 分
效率 8 分
社交 3 分
那好像還是有點可惜。
我開始想:
如果程式可以自己找出最高分和最低分呢?
如果它可以根據不同的分數組合,
自動產生一個專屬的「賽博診斷結果」呢?
甚至再搭配一張很像真的醫院開出來、
但內容其實完全不正經的:
惡搞處方籤。
這樣好像就開始有「賽博紓壓診所」的味道了。
所以 Day 14,我們就來處理這件事:
把六個分數真正轉換成一份專屬的病歷報告。
畢竟都已經來看診了,
總不能最後只拿到一張雷達圖就被趕出去吧?
下一篇 DAY 14,開始正式開藥。