iT邦幫忙

2026 iThome 鐵人賽

DAY 13
0
自我挑戰組

賽博紓壓診所:從零打造純前端心理測驗與互動紓壓網頁系列 第 13 篇

Day 13:讓雷達圖動起來:從靜態結果到真正的互動感

  • 分享至 

  • xImage
  •  

前言

昨天終於把那張雷達圖畫出來了。

從 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 不只畫一次,而是一直重畫。


動手寫 Code:讓雷達圖開始長大

這次我不需要把 Day 12 那整大串 Canvas 程式重新貼一次。

因為六角形怎麼畫、角度怎麼算、座標怎麼找,昨天已經處理好了。

今天只改一件事情:

讓原本的分數可以慢慢增加。

首先新增一個動畫進度:

let animationProgress = 0;

這個東西可以把它想成目前動畫跑到哪裡。

一開始:

0

代表什麼都還沒長出來。

最後:

1

代表已經完成。

所以如果某個維度原本是:

8

那動畫進行到一半:

8 × 0.5 = 4

就只先畫到 4。

這樣就可以讓原本的雷達圖慢慢長出來。


1. 修改原本的 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

所以同一張雷達圖,就可以從中心慢慢往外跑。


2. 讓它自己一直重畫

光有 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();

就可以開始跑了。


3. requestAnimationFrame 到底在幹嘛?

這個名字看起來有點長。

但它在這裡做的事情其實很直白:

「瀏覽器,下一次準備更新畫面的時候,再叫我一次。」

所以整個流程會變成:

animateRadarChart()
        ↓
progress + 0.02
        ↓
重新畫 Canvas
        ↓
還沒到 1?
        ↓
requestAnimationFrame()
        ↓
再跑一次
        ↓
progress + 0.02
        ↓
重新畫 Canvas
        ↓
……
        ↓
progress = 1
        ↓
結束

這也是為什麼雷達圖會有「長出來」的感覺。

不是 Canvas 突然會動。

而是我們一直在很短的時間內重新畫它。


4. 但第一次跑完,我又覺得怪怪的

做到這裡,我原本以為:

好,完成。

結果實際跑起來之後……

嗯。

有動。

但有點太規律。

它就是:

0
0.02
0.04
0.06
0.08
……

平均地往上加。

看起來很像程式正在很認真地執行一個迴圈。

不是不好。

就是……

太直了。

所以我又多做了一個小調整。

把原本的:

animationProgress

稍微轉換一下。

const easedProgress =
    1 - Math.pow(
        1 - animationProgress,
        3
    );

然後畫圖時:

drawRadarChart(
    userScores,
    easedProgress
);

這樣動畫前面會比較慢,

接近結束時再慢慢停下來。

簡單來說就是:

原本:

快 → 快 → 快 → 快


現在:

慢 → 中 → 快 → 慢

看起來就自然很多。

這種東西如果只看程式碼,

可能會覺得:

「不就一個公式?」

但真的放到畫面上,

差異真的還滿明顯的!


5. 順便讓結果文字晚一點出現

既然雷達圖都已經會慢慢出現了,

那如果文字一開始就全部塞出來,

又會有點破壞氣氛。

所以我乾脆讓結果等雷達圖跑完再出現。

先準備一個區塊:

<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 =
        "分析完成,歡迎查看你的賽博狀態。";

}

這樣整個流程就變成:

完成題目
    ↓
開始分析
    ↓
雷達圖慢慢展開
    ↓
動畫完成
    ↓
顯示結果文字

至少比所有東西一次跳出來有層次一點。


6. 最後再補一點 CSS

既然都已經讓它延後出現了,

那我希望結果文字不要直接「啪」地跳出來。

所以可以利用 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,開始正式開藥。


上一篇
Day 12:Canvas 畫布實戰:把六大維度真的畫出來
下一篇
Day 14:靈魂!雷達讓圖學會「對症下藥」
系列文
賽博紓壓診所:從零打造純前端心理測驗與互動紓壓網頁 共 19 篇
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言