iT邦幫忙

2026 iThome 鐵人賽

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

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

Day19. 找到彩蛋的客製化視窗 & 防止重覆觸發

  • 分享至 

  • xImage
  •  

昨天已經能正確判斷「點到的是不是彩蛋」,但用的是 alert() 來跳出資訊
介面非常陽春,而且每點一次彩蛋就跳一次,照理說出現過後就代表一切結束,不應該再讓使用者繼續點擊跟跳出找到資訊。今天就來處理以下兩件事:

  1. 找到彩蛋之後,不再重覆觸發恭喜訊息
  2. 換成自己設計的浮動提示框,加一點動畫效果

步驟一:防止重複觸發,沿用昨天的 userData 技巧

昨天用 characterArmature.userData.isEgg = true 貼了一個「這是彩蛋」的標籤。今天用同樣的手法,再貼一個「有沒有被找到過」的標籤:

let characterArmature;   // ★ 提升到外層,點擊事件才拿得到

loader.load('blender_output.glb', (gltf) => {
  scene.add(gltf.scene);

  characterArmature = gltf.scene.getObjectByName('CharacterArmature');
  if (characterArmature) {
    characterArmature.userData.isEgg = true;
    characterArmature.userData.found = false;   // ★ 新增:一開始還沒被找到
  }

  // ...(Box3、相機定位、controls.target 都不動)
});

跟昨天不一樣的地方:characterArmature 這個變數要搬到 loader.load 外面宣告,因為點擊事件(在外層)要能修改它的 userData.found,不能只留在 callback 裡面用完就丟。

步驟二:找到彩蛋的那個節點本身,不是布林值

昨天寫的 isPartOfEgg(hit) 只回傳 true/false,今天需要的是「找到的話,把那個節點本身傳回來」,才能去改它的 userData.found:

function findEggRoot(object) {
  if (object.userData.isEgg) return object;
  let root = null;
  object.traverseAncestors((ancestor) => {
    if (ancestor.userData.isEgg) root = ancestor;
  });
  return root;
}

跟昨天的 isPartOfEgg 幾乎一樣,只是回傳值從 true/false 改成「那個物件」或 null。

步驟三:客製化的浮動提示框

不用 alert(),改用一個藏在頁面裡、平常看不到的 <div>,需要時才讓它淡入顯示:

HTML(加在 index.html 的 <body> 裡):

<div id="toast"></div>

CSS:

#toast {
  position: fixed;
  top: 40px;
  left: 50%;
  transform: translateX(-50%) translateY(-20px) scale(0.9);
  background: rgba(0, 0, 0, 0.85);
  color: #fff;
  padding: 16px 32px;
  border-radius: 12px;
  font-size: 20px;
  opacity: 0;
  pointer-events: none;
  transition: opacity 0.4s ease, transform 0.4s ease;
  z-index: 1000;
}
#toast.show {
  opacity: 1;
  transform: translateX(-50%) translateY(0) scale(1);
}

依 MDN 的 CSS transitions 說明,transition 這個屬性,是讓瀏覽器在某個 CSS 數值改變時,自動畫出「中間過程」的動畫,不用自己一幀一幀計算。 這裡設定的是 opacity(透明度)和 transform(位置/縮放)這兩個屬性,只要加上 .show 這個 class,CSS 數值從「隱藏的樣子」變成「顯示的樣子」,瀏覽器就會自動補完中間 0.4 秒的漸變過程,看起來才會有「淡入、往下滑」的動畫感,不是一瞬間硬生生跳出來。

JavaScript(控制什麼時候顯示、顯示多久):

const toast = document.getElementById('toast');
let toastTimer;

function showToast(message, duration = 2500) {
  toast.textContent = message;
  toast.classList.add('show');

  clearTimeout(toastTimer);
  toastTimer = setTimeout(() => {
    toast.classList.remove('show');
  }, duration);
}
  • toast.classList.add('show'):加上 .show 這個 class,觸發前面 CSS 寫好的動畫
  • setTimeout(...):等 2.5 秒之後,自動把 .show 拿掉,提示框淡出消失
  • clearTimeout(toastTimer):如果提示框還在顯示中,又被叫出來一次,先取消上一次的倒數計時,避免兩次顯示的計時器互相搶著把它關掉

完整程式碼:點擊事件整合

window.addEventListener('click', (event) => {
  pointer.x = (event.clientX / window.innerWidth) * 2 - 1;
  pointer.y = -(event.clientY / window.innerHeight) * 2 + 1;

  raycaster.setFromCamera(pointer, camera);
  const intersects = raycaster.intersectObjects(scene.children, true);

  if (intersects.length === 0) return;

  const hit = intersects[0].object;
  const eggRoot = findEggRoot(hit);

  if (!eggRoot) {
    console.log('點到公園本體:', hit.name);
    return;
  }

  // ★ 關鍵:已經找到過,就不要再觸發
  if (eggRoot.userData.found) {
    console.log('這個彩蛋已經找過了');
    return;
  }

  eggRoot.userData.found = true;   // ★ 標記成「已找到」
  showToast('🎉 恭喜你找到彩蛋了!');
});

防止重複觸發的關鍵只有這三行:

if (eggRoot.userData.found) {
  return;   // 已經找過,直接結束,不執行下面顯示提示的程式碼
}
eggRoot.userData.found = true;

第一次點擊:found 是 false,跳過 if,往下執行,顯示提示、並把 found 改成 true。
第二次點擊同一個彩蛋:found 已經是 true,if 成立,直接 return,不會再跳提示。

最終結果樣貌

https://ithelp.ithome.com.tw/upload/images/20261003/20112674LMOSWtdYzF.png

如圖所示,找到彩蛋的提示做好了,

接下來要加計分或計時功能,讓這個不只是「找到就跳提示」,而是可以紀錄多久找出來。讓這個專案變得更有遊戲感。

參考連結


上一篇
Day18. 判斷點擊是否命中彩蛋
下一篇
Day 20. 計時與進度顯示: 增添遊戲感
系列文
從空拍到3D展示 共 22 篇
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言