故事是這樣的,在網路上查到一個很酷的 CSS 3D 旋轉方塊教學

看到後禁不住手癢,馬上照著刻了一套洗巴辣擲骰子動畫:

嚴格來說,我的 HTML 5 CSS 跟 JavaScript 技能等級只到中上,過去要挑戰這種題目屬越級打怪,少不了爬文地獄加撞牆撞到飽。現在有了 Github Copliot 黑魔法,我已不是昔日的吳下阿蒙,居然只花一個小時就寫好惹~

給個方向起個頭,Copilot 接手把剩下十幾行補齊;有個模糊概念但沒細想實際要怎麼寫,打個註解 Copilot 幫你寫出來,連爬文時間都省了。只要你有能力鑑別程式碼好壞,把 Copilot 出錯或不足的地方改好,這種人與 AI 協作模式,比只說需求全權委託 AI 生成可靠,速度又比人工開發至少快一倍,是我心中最理想的 AI 輔助開發模式。

ChatGPT 跟 Github Copilot 推出後,寫程式這檔事已邁入另一個紀元。不會或不愛寫程式的人,正熱烈期盼再也不用碰鍵盤敲 Code 的一天;而我這種太久沒寫 Code 手會抖的重度成癮者,則完全相反,正盡情享受靠 AI 推平學習曲線與加快開發速度,把 Code 寫好寫滿,Coding 樂趣翻倍。感覺就像手上的武器從自動步槍升級到加特林,遇到低等怪閉著眼睛扣扳機直接掃光,把時間精力留下來對付魔王。

扯遠了。完整程式碼我已丟在 Github 了,想玩的話有線上展示。這篇主要分享我這回邊做邊學到的 HTML5 技巧,對專業前端攻城獅可能是常識,但對我這種前端知識停在十年前的老全端,每每看到 Github Copilot 示範,就不自主發出「靠! 原來可以這樣」的讚嘆。因此,花點時間整理並補上參考資料,讓知識更完整。

  1. 3D 立方體是用六個 <div> 元素開 transform-style: preserve-3d 轉 3D 呈現,再用 transform rotateY() 轉出前後左右四面,transform rotateX() 轉出頂面跟底面,接著調整 translateZ() 產生透視感,相關原理推薦這篇:Intro to CSS 3D transforms,文章有步驟分解說明。

  2. 骰子的六面點數是用 SVG 繪製,確保可無鋸齒縮放。猜猜我怎麼做出來的?這類小圖示簡單歸簡單,要刻意找免姓名標示授權版本都要找好久,攻城獅繪圖工具不熟自己畫得摸索半天,現在則可以請 GPT 生成:
    (GPT 一開始聽不懂指令沒做出圓角,但若懂點 SVG 原理加上 path 提示就會成功。會寫程式,調線條粗細、改點數自然不成問題。所以我不認同有 AI 以後大腦可以放空,技能跟知識愈足,同樣的 AI 工具用起來火力就是比別人強,還是能提高競爭力的)

  3. 現代瀏覽器已內建 CSS 巢狀 Seletor 支援及 calc() 計算、var() 變數
    巢狀 Selector (CSS Nesting)calc() 計算var() 變數 對我不算新鮮事,但在我的過時認知中得用 Sass/SCSS 寫再編譯成 css,現在除了一些 上古瀏覽器外,這些 CSS 功能已是瀏覽器標配了。

    :root {
      --dice-size: 50px; 
      --dice-bg-size: calc(var(--dice-size) * 1.28);
      --trans-z-face: calc(var(--dice-size) / 2);
      --trans-z-show: calc(var(--trans-z-face) * -1);
    }
    
    .dice-cube {
    
      * {
        box-sizing: border-box;
      }
    
      &.focus {
    
      }
    
      display: inline-block;
      width: var(--dice-size);
      perspective: calc(var(--dice-size) * 4);
    }
    
  4. 不用前端框架也能自訂 <my-component>
    是的,你可以像下面這樣自訂網頁元素,宣告一個 DiceCube 元件並設計方法跟屬性,註冊後便可在 HTML 用 <dice-cube id="d1"></dice-cube> 加入一顆骰子,用 document.getElementById('d1').roll('top'); 轉骰子,用 console.log(document.getElementById('d1').points); 讀點數,全程只用香草 JavaScript,不添加任何人工香料。

    class DiceCube extends HTMLElement {
      constructor() {
        super();
        this.innerHTML = `
          <div class="dice-cube">
            <div class="cube">
              <div class="cube__face cube__face--front"></div>
              <div class="cube__face cube__face--back"></div>
              <div class="cube__face cube__face--right"></div>
              <div class="cube__face cube__face--left"></div>
              <div class="cube__face cube__face--top"></div>
              <div class="cube__face cube__face--bottom"></div>
            </div>  
          </div>
        `;
        this.face = 'front';
      }
      get points() {
        return faces.indexOf(this.face) + 1;
      }      
      roll() {
        let cube = this.querySelector('.cube');
        cube.className = 'cube';
        cube.classList.add(`show-${face}`);
        this.face = face;
      }
    }
    customElements.define('dice-cube', DiceCube);
    
  5. 如何等 n 秒再繼續?善用 async、await 簡化程式寫法
    在上面的例子中,roll() 設定 CSS 樣式需等 0.25 秒動畫顯示最後點數,我的擲骰子亂數決定點數的方法 throw(),設計成連轉 20 次隨機點數演示過程,最後才決定結果,故要等待動畫播完再 throw() 第二次,一個直覺做法是呼叫 roll() 時卡住 0.25 秒後再跑下一行。
    過去 JavaScript 要搞非同步等待很麻煩,得靠複雜的 Queue、setTimeout 或 setInterval 實現。如今,善用 async、await,程式碼可以變得很簡單:

       roll(face) {
         let cube = this.querySelector('.cube');
         cube.className = 'cube';
         cube.classList.add(`show-${face}`);
         this.prevFace = this.side;
         this.face = face;
         // 傳回 Promise、0.22 秒後觸發 then
         return new Promise(resolve => { setTimeout(() => { resolve(); }, 220);  });
       }
       randomFace() {
         return faces[Math.floor(Math.random() * faces.length)];
       }
       // 宣告 async 以便使用 await 等待
       async throw() {
         const times = 20;
         for (let i = 0; i < times; i++) {
           let nextFace;
           do {
             nextFace = this.randomFace();
           } while (nextFace === this.face || // 排除與現來相同
             nextFace === this.prevFace || // 排除逆轉回上一面
             nextFace === oppsiteFaces[this.face] // 排除直接轉 180 度
           );
           // 呼叫 .roll() 並等待點數確認
           await this.roll(nextFace);
         }
         await this.roll(this.randomFace());
       }
    

    看似沒什麼用的 CSS 動畫,過程還是學到有用的技巧與知識,學程式,寫就對了。

    This article demostrating a CSS 3D dice animation using HTML5, CSS, and JavaScript, aided by Github Copilot. They highlight techniques such as CSS 3D transforms, SVG for scalable faces, modern CSS features, custom web components, and async/await for timing.


Comments

# by 後段班學生

身為後端只會 postback + razor 產前端的工程師在此下跪

# by 樂透無名

筆記

# by Anthony

可惜Windows 7只能用“上古瀏覽器”(Chrome 109/Firefox 115esr) 導致“3D 旋轉方塊教學”OK但“線上展示”骰子不見了 (css-nesting)

# by

瀏覽完文章,最缺乏的常識居然是骰子點數相對位置的規定

Post a comment