iT邦幫忙

2026 iThome 鐵人賽

DAY 26
0
佛心分享-IT 人自學之術

從空拍到3D展示系列 第 26 篇

Day 26. 部署前的準備

  • 分享至 

  • xImage
  •  

昨天整理了載入進度條的原理:GLTFLoader.load 的第三個參數 onProgress,下載過程中會反覆被呼叫,收到的 ProgressEvent 有 loaded、total、lengthComputable 三個欄位。

今天把這些原理變成程式碼,加進我放在 GitHub 上整理過的專案。

我的 .glb 模型網頁打開後,需要有一些載入時間,所以在模型載入完成前,會看到畫面一片黑。為了讓使用者知道什麼時候載好,昨天的進度條功能就很適合放進來使用

  1. 載入的時候,讓使用者知道進行到哪裡。
  2. 載入失敗的時候,讓使用者知道出了問題,而不是看著一片空白。

一、設計:畫面要顯示什麼

昨天提到,不是每次都知道檔案總共多大(要看伺服器有沒有回傳 Content-Length),所以要處理兩種情況:

情況 文字 長條
算得出總大小 載入中… 50% 依百分比變長
檔案載入到 100% 檔案載入完成,處理模型中… 滿
算不出總大小 載入中… 已載入 10.0 MB 來回滑動(不代表百分比)
載入失敗 載入失敗,請重新整理,或查看 Console 的錯誤訊息 空

為什麼 100% 要多一個「處理模型中」?因為上面說的第 ② 階段:檔案載入到 100% 之後,還要解析 .glb,解析完才呼叫 onLoad。如果這時文字還停在「100%」,使用者會以為卡住了。


二、要改哪些檔案

我的程式已經拆成幾個模組,這次要改的是:

檔案 負責的事 這次的改動
index.html 網頁結構 加一個進度條區塊
style.css 樣式 加進度條的樣式
ui.js 畫面上的顯示 加三個函式:更新、隱藏、顯示錯誤
main.js 把各個模組接起來 把這三個函式接到載入流程上

loader.js 不用動:它只負責建立 GLTFLoader,進度要怎麼處理,是在呼叫 load 的時候傳進去的。

進度條的顯示邏輯放在 ui.js,是因為它和之前的提示訊息、結果面板一樣,都是「畫面上要顯示什麼」。

三、一步一步改

以下標有 ★ 的都是今天新加的。

步驟 1:index.html 加進度條區塊

在 #hud 後面加:

<!-- ★ 新增:載入進度條 -->
<div id="loading">
    <div class="loading-card">
        <p id="loading-text">載入中…</p>
        <div class="loading-track"><div id="loading-bar"></div></div>
    </div>
</div>
  • #loading-text:顯示文字。
  • .loading-track:灰色的底軌。
  • #loading-bar:裡面會變長的那一條。

步驟 2:style.css 加樣式

/* ★ 新增:載入進度條 */
#loading {
  position: fixed;
  inset: 0;                      /* 蓋滿整個視窗 */
  z-index: 10;                   /* 疊在畫布與其他面板上面 */
  display: flex;
  align-items: center;
  justify-content: center;
  background: rgba(0, 0, 0, 0.75);
  color: #fff;
  transition: opacity 0.4s ease;
}
#loading.hidden {
  opacity: 0;
  pointer-events: none;          /* 載入完成後,不擋住下面的畫布 */
}
.loading-card {
  width: min(80vw, 360px);
  text-align: center;
}
#loading-text {
  margin: 0 0 12px;
  font-size: 18px;
  font-variant-numeric: tabular-nums;
  white-space: pre-line;    /* 文字裡的換行字元 \n 要顯示成換行,若沒有這行設定\n 沒有作用 */
}
.loading-track {
  height: 10px;
  border-radius: 5px;
  background: rgba(255, 255, 255, 0.25);
  overflow: hidden;              /* 讓裡面的長條不會超出圓角 */
}
#loading-bar {
  width: 0%;
  height: 100%;
  background: #4ade80;
  transition: width 0.2s ease;
}
/* 算不出總大小時:長條改成來回滑動,不代表百分比 */
#loading-bar.indeterminate {
  width: 40%;
  transition: none;
  animation: loading-slide 1.2s ease-in-out infinite;
}
@keyframes loading-slide {
  from { transform: translateX(-100%); }
  to   { transform: translateX(250%); }
}

幾個重點:

  • inset: 0 加 position: fixed:蓋滿整個視窗,跟結果面板的做法一樣。
  • z-index: 10:數字越大,疊得越上面。進度條要蓋住畫布和左上角的計時。
  • #loading.hidden 裡的 pointer-events: none:載入完成後,進度條雖然變透明了,還在原地,如果不加這行,它會擋住滑鼠,畫布就拖不動了。
  • .indeterminate:長條寬度固定 40%,用動畫從左邊滑到右邊。translateX(-100%) 是往左移自己的一個寬度(從底軌左邊外面開始),250% 是往右移 2.5 個寬度,剛好滑到底軌的右端。

@keyframes 是什麼?

依 MDN 的說明:

The @keyframes CSS at-rule controls the intermediate steps in a CSS animation sequence by defining styles for keyframes (or waypoints) along the animation sequence.

意思是:@keyframes 用來定義動畫的「關鍵畫面」,中間的過程由瀏覽器自動補。名稱 loading-slide 是自己取的。

這裡的起點是 translateX(-100%):translateX 是水平移動,-100% 是往左移「自己寬度的 100%」,長條剛好整個在底軌左邊外面(被 overflow: hidden 藏起來)。終點是 250%:往右移自己寬度的 2.5 倍,因為長條寬度是底軌的 40%,2.5 倍剛好等於底軌的 100%,長條就整個滑出右邊。

跑完一輪(到 to)之後,會直接跳回起點,再從左邊重新跑一輪,加上 infinite 後會一直重複左到右的動畫。因為跳回去的位置,長條是在底軌外面(被藏起來),所以眼睛看不到跳回去的瞬間,看起來就是「從左邊一次又一次滑進來」。

如果想要真的「來回」,在 animation 後面加 alternate:

animation: loading-slide 1.2s ease-in-out infinite alternate;

MDN 對 alternate 的說明是:

The animation reverses direction each cycle, with the first iteration being played forwards.

也就是第一輪由左往右,第二輪倒過來由右往左,一直交替。不過這樣起點和終點的長條都在底軌外面,會有一小段時間完全看不到長條,要不要用 alternate 看個人喜好,可以兩種都試試看。

步驟 3:ui.js 加三個函式

在檔案最後面加:

// ★ 新增:載入進度條
const loading = document.getElementById('loading');
const loadingText = document.getElementById('loading-text');
const loadingBar = document.getElementById('loading-bar');

export function updateLoading(event) {
  if (event.lengthComputable) {
    // 算得出總大小:顯示百分比
    const percent = Math.min(100, Math.floor((event.loaded / event.total) * 100));
    loadingBar.classList.remove('indeterminate');
    loadingBar.style.width = `${percent}%`;
    loadingText.textContent = percent < 100 ? `載入中… ${percent}%` : '檔案載入完成,處理模型中…';
  } else {
    // 算不出總大小:只能顯示已載入多少 MB,長條改成來回滑動
    const mb = (event.loaded / (1024 * 1024)).toFixed(1);
    loadingBar.classList.add('indeterminate');
    loadingBar.style.width = '';
    loadingText.textContent = `載入中… 已載入 ${mb} MB`;
  }
}

export function hideLoading() {
  loading.classList.add('hidden');
}

export function showLoadError() {
  loadingBar.classList.remove('indeterminate');
  loadingBar.style.width = '0%';
  loadingText.textContent = '載入失敗,請重新整理,或查看 Console 的錯誤訊息';
}

逐行說明 updateLoading:

  • event.lengthComputable:昨天說明過,是 true 才能算百分比。
  • Math.floor(...):無條件捨去,讓 99.9% 顯示成 99%,不會在還沒完成時就顯示 100%。
  • Math.min(100, ...):保險。萬一已載入量超過總大小,百分比也不會超過 100。
  • percent < 100 ? ... : ...:到 100% 就改顯示「處理模型中」,原因在第一節。
  • loadingBar.style.width = '':在「算不出總大小」那一支,要把前面設定過的寬度清掉。直接寫在元素上的 style.width 優先於樣式表,不清掉的話,.indeterminate 裡的 width: 40% 就套不上。
  • (1024 * 1024):1 MB 等於 1,048,576 位元組。
  • .toFixed(1):保留一位小數。

步驟 4:main.js 接上三個函式

三處改動。

第一處,匯入:

import { hideResult, showResult, showToast, updateLoading, hideLoading, showLoadError } from './ui.js';  // ★ 多匯入三個

第二處和第三處,在 loader.load 裡:

loader.load(
  '3d_unseen_true.glb',
  (gltf) => {
    // ……原本載入完成後做的事,都不用動……

    hideLoading();                                   // ★ 模型處理完成,收起進度條
  },
  updateLoading,                                     // ★ 原本是 undefined,載入檔案的過程中會反覆呼叫
  (error) => {
    console.error('載入失敗', error);
    showLoadError();                                 // ★ 失敗時,進度條改顯示錯誤訊息
  }
);
  • 第三個參數 undefined 換成 updateLoading。注意這裡沒有加括號:是把函式本身交出去,讓 GLTFLoader 在每次有進度時自己呼叫,加了括號就變成現在立刻執行一次。
  • hideLoading() 放在 onLoad 的最後面,代表模型解析完、也加進場景之後,才把進度條收起來。
  • 第四個參數原本只有 console.error,現在多了一行,讓使用者在畫面上也看得到失敗。

四、整個流程怎麼運作

  1. 網頁打開,#loading 一開始就顯示,文字是「載入中…」。
  2. loader.load(...) 開始載入 .glb 檔案。
  3. 每載入一段,GLTFLoader 就呼叫一次 updateLoading,文字與長條跟著更新。
  4. 檔案載入完,開始解析,文字變成「檔案載入完成,處理模型中…」。
  5. 解析完,onLoad 被呼叫,場景加好、彩蛋標好,最後呼叫 hideLoading(),進度條淡出。
  6. 如果中途失敗,onError 被呼叫,進度條改顯示錯誤訊息,不會消失。

五、怎麼驗證

  1. 用平常的方式在本機開網頁。本機載入太快,進度條一閃而過,可以按 F12 打開開發者工具,在 Network 分頁把網路速度調成較慢的選項,再重新整理,就看得清楚。
  2. 觀察文字是否從 0% 增加到 100%,最後變成「處理模型中」,再消失。
  3. 驗證失敗情況:暫時把 main.js 裡的 '3d_unseen_true.glb' 改成不存在的檔名,重新整理,應該看到「載入失敗」,Console 也會有錯誤訊息。測完記得改回來。
  4. 「算不出總大小」那一支,在本機通常不容易觸發,要等部署到網路上,才知道伺服器有沒有回傳 Content-Length。
  • 完整成果 (點選為 YT 影片)
    Yes

  • 更換不存在的檔名,確認失敗時,有對應的處理
    https://ithelp.ithome.com.tw/upload/images/20261011/20112674aLm5v4tuQk.png


小結

今天把昨天的原理做成了程式:

  • ui.js 負責顯示(更新、隱藏、顯示錯誤),main.js 負責接線,loader.js 不用動。
  • 兩種情況都處理:算得出總大小顯示百分比,算不出就顯示已載入的 MB。
  • 檔案載入完成之後,多一段「處理模型中」,因為 GLTFLoader 是檔案載入完才解析,而解析這段沒有進度可以顯示。
  • 在本機,進度條可能一閃而過,等待多半在「處理」;真正發揮作用的是部署後,使用者要透過網路載入檔案的時候。實際比例我用小實驗量。

接下來

下一步要把專案放到 GitHub Pages,讓任何人打開網址就能玩。部署之後,才會知道伺服器有沒有回傳檔案總大小,進度條會顯示成百分比還是 MB。


上一篇
Day 25. 進度條原理與手機測試準備
下一篇
Day 27. 專案部署到 Github Pages 並實測
系列文
從空拍到3D展示 共 28 篇
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言