0. 作品介紹

作者 吳俊成 東門國小

主題:資訊科技-晶體管科技-二極體 (適合國小學童的教學動畫) 構想 畫面中先將三個半導體重要元素 (硼B,矽Si,磷P) 的基本資訊顯示出來,如 : 原子序中、中文名稱、元素符號、平均原子量、各層電子。 使用者可以將 硼B,矽Si,磷P 等三個元素來做 "PN二極體" 的參雜後的通電完整過程。 模擬並畫出自由電子與電洞,在二極體兩端施以3V的電壓,計算出電流流動時的大小,同時希望可以呈現 "二極體兩邊的壓降V"、"即時電流mA","發熱狀態"。 操作說明 參雜時,以 矽Si 的基本單位為 1,兩個滑桿,範圍從 0.1 ~ 10,分別在 "硼B(P型半導體)"、"磷P(N型半導體)" 的兩端,使用者可以拉動代表參雜進的量。 拉動時,動畫即時呈現 "自由電子" 與 "電洞" 的數量。 畫面中設計一個開始通電的按鈕,點選按鈕後,就同時以動畫並標註數值,將 "二極體通電完整過程" 呈現出來,讓學生了解二極體晶體管科技的原理。


44 1 0
遊戲
示範影片

1. AI 提示詞

把這段指令貼給 AI,讓它幫你生成程式碼。

我想要做一個適合國小學童的教學動畫
主題:資訊科技-晶體管科技-二極體

我的構想
請先使用三個附件圖檔,並將元素的基本資訊顯示出來,如 : 原子序中、中文名稱、元素符號、平均原子量、各層電子。
使用者可以將 硼B,矽Si,磷P 等三個元素 (請使用三個附件圖檔) 來做 "PN二極體" 的參雜後的通電完整過程。模擬並畫出自由電子與電洞,在二極體兩端施以3V的電壓,並參考 "
二極體順向篇壓 : 指將外加電壓的正極接至PN接面的P型端(陽極),負極接至N型端(陰極),約 0.6 ~ 0.7V",計算出電流流動時的大小,同時希望可以呈現 "二極體兩邊的壓降V"、"即時電流mA","發熱狀態"。

操作說明
參雜時,以 矽Si 的基本單位為 1,需要請你分別作兩個滑桿,範圍從 0.1 ~ 10,分別在 硼B(P型半導體)、磷P(N型半導體)兩端,讓使用者可以拉動代表參雜進的量,拉動時,動畫即時呈現 "自由電子" 與 "電洞" 的數量,並設計一個開始通電的按鈕,點選按鈕後,就同時以動畫並標註數值,將 "二極體通電完整過程" 呈現出來,讓學生了解二極體晶體管科技的原理。

整體的畫面請你設計,我希望是 "嚴謹的科學探討風格"。

我不知道是否需要建立 Google 試算表,若需要,請你告訴我需要的資料表與欄位。
我希望有些資料我可以直接在試算表更改首頁可以直接呈現。
要把它變成程式碼我要發佈。
請完整敘述操作步驟 越詳細越好。

作業環境
Google 試算表
Google Apps Script

我看不懂程式碼,請給我 "後端gs完整程式碼"、"前端index完整程式碼"。

2. 資料表規則

請照下面規則建立分頁與欄位。

第1分頁: ElementData A 欄: 中文名稱 元素符號 原子序 平均原子量 各層電子(由內向外) B 欄: 矽 Si 14 28.085 2 8 4 C 欄: 硼 B 5 10.81 2 3 D 欄: 磷 P 15 30.974 2 8 5 第2分頁: SimulationConfig A 欄: 設定項目 數值 單位 說明 B 欄: 外加電壓 (V_source) 3 V 使用者指定外加電壓 (教學設為 3V) C 欄: 二極體順向壓降 (Vf) 0.7 V PN二極體順向導通壓降 (典型值 0.6~0.7V) D 欄: 外部電阻 (R_external) 100 Ω 教學用外部電阻,用於計算電流 E 欄: 基準參雜濃度 (Doping_Base) 1.00E+15 cm^-3 基準參雜濃度,用於計算電子和電洞數 F 欄: 發熱狀態閾值 (高) (Heat_High) 0.2 W 若消耗功率大於此值,則顯示為高熱狀態 (紅色) G 欄: 發熱狀態閾值 (中) (Heat_Medium) 0.1 W 若消耗功率大於此值,則顯示為中熱狀態 (橘色)

📄 解析結果(自動表格)

分頁:ElementData
A 欄 B 欄 C 欄 D 欄
中文名稱 元素符號 原子序 平均原子量 各層電子(由內向外) 矽 Si 14 28.085 2 8 4 硼 B 5 10.81 2 3 磷 P 15 30.974 2 8 5
分頁:SimulationConfig
A 欄 B 欄 C 欄 D 欄 E 欄 F 欄 G 欄
設定項目 數值 單位 說明 外加電壓 (V_source) 3 V 使用者指定外加電壓 (教學設為 3V) 二極體順向壓降 (Vf) 0.7 V PN二極體順向導通壓降 (典型值 0.6~0.7V) 外部電阻 (R_external) 100 Ω 教學用外部電阻,用於計算電流 基準參雜濃度 (Doping_Base) 1.00E+15 cm^-3 基準參雜濃度,用於計算電子和電洞數 發熱狀態閾值 (高) (Heat_High) 0.2 W 若消耗功率大於此值,則顯示為高熱狀態 (紅色) 發熱狀態閾值 (中) (Heat_Medium) 0.1 W 若消耗功率大於此值,則顯示為中熱狀態 (橘色)

3. 開啟 Apps Script

從試算表開 Apps Script,才能直接用 SpreadsheetApp 讀寫資料。

  1. 試算表上方選單:擴充功能 → Apps Script
  2. 刪掉預設的程式碼(或全部選取覆蓋)
  3. 保留/建立檔案:
    • Code.gs
    • index.html

4. 貼上後端(Code.gs)

把後端程式貼到 Apps Script 的 Code.gs。

Code.gs(後端)
function doGet() {
  try {
    var template = HtmlService.createTemplateFromFile('Index');
    template.data = getSheetData(); 
    return template.evaluate()
        .setTitle('資訊科技:二極體物理模擬 (修正版)')
        .setXFrameOptionsMode(HtmlService.XFrameOptionsMode.ALLOWALL)
        .addMetaTag('viewport', 'width=device-width, initial-scale=1');
  } catch (e) {
    return HtmlService.createHtmlOutput("系統載入失敗:" + e.toString());
  }
}

function getSheetData() {
  var ss = SpreadsheetApp.getActiveSpreadsheet();
  var elementSheet = ss.getSheetByName('ElementData');
  var elementValues = elementSheet.getDataRange().getValues();
  var elements = {};
  for (var i = 1; i < elementValues.length; i++) {
    var row = elementValues[i];
    elements[row[1]] = {
      name: row[0],
      atomicNumber: row[2],
      atomicWeight: row[3],
      electrons: row[4]
    };
  }
  var configSheet = ss.getSheetByName('SimulationConfig');
  var configValues = configSheet.getDataRange().getValues();
  var config = {};
  for (var j = 1; j < configValues.length; j++) {
    var cRow = configValues[j];
    config[cRow[0]] = cRow[1];
  }
  return { elements: elements, config: config };
}
貼到 Apps Script 的 Code.gs 檔案

5. 貼上前端(index.html)

把前端程式貼到 Apps Script 的 index.html。

index.html(前端)
<!DOCTYPE html>
<html>
<head>
  <base target="_top">
  <style>
    :root {
      --p-color: #ff4d4d;
      --n-color: #3399ff;
      --bg-dark: #1a1a1a;
      --panel-bg: #2d3436;
    }
    body { font-family: "Segoe UI", "Microsoft JhengHei", sans-serif; background: #0f1416; color: #dfe6e9; margin: 0; padding: 20px; }
    .container { max-width: 1100px; margin: 0 auto; background: var(--panel-bg); padding: 25px; border-radius: 15px; box-shadow: 0 15px 50px rgba(0,0,0,0.5); }
    
    .header { text-align: center; border-bottom: 1px solid #636e72; margin-bottom: 20px; padding-bottom: 10px; }
    .header h1 { color: #00cec9; margin: 0; font-size: 24px; letter-spacing: 2px; }

    .element-grid { display: grid; grid-template-columns: repeat(3, 1fr); gap: 15px; margin-bottom: 20px; }
    .card { background: #34495e; padding: 12px; border-radius: 8px; border-left: 5px solid #bdc3c7; }
    .card.si { border-color: #00b894; }
    .card.b { border-color: var(--p-color); }
    .card.p { border-color: var(--n-color); }
    .card h3 { margin: 0 0 5px 0; font-size: 18px; color: #ecf0f1; }
    .card p { margin: 2px 0; font-size: 12px; color: #bdc3c7; }

    .control-panel { display: flex; justify-content: space-between; align-items: center; background: #1e272e; padding: 20px; border-radius: 10px; margin-bottom: 20px; }
    .slider-group { width: 35%; text-align: center; }
    .slider-group label { display: block; margin-bottom: 10px; font-weight: bold; color: #f1c40f; }
    input[type=range] { width: 100%; cursor: pointer; }
    
    .btn-main { padding: 15px 40px; border: none; border-radius: 5px; background: #d63031; color: white; font-weight: bold; cursor: pointer; transition: 0.3s; font-size: 16px; }
    .btn-main.on { background: #00b894; box-shadow: 0 0 20px rgba(0,184,148,0.4); }

    .canvas-wrapper { position: relative; border: 2px solid #636e72; border-radius: 5px; overflow: hidden; background: #000; }
    #simCanvas { display: block; margin: 0 auto; }

    .status-grid { display: grid; grid-template-columns: repeat(4, 1fr); gap: 10px; margin-top: 20px; }
    .status-item { background: #1e272e; padding: 15px; border-radius: 8px; text-align: center; border: 1px solid #3d464d; }
    .status-label { font-size: 12px; color: #95a5a6; display: block; margin-bottom: 5px; }
    .status-value { font-size: 20px; font-weight: bold; color: #00d2d3; }
    .heat-bar { grid-column: span 4; padding: 10px; border-radius: 5px; text-align: center; font-weight: bold; margin-top: 10px; transition: 0.5s; background: #3d464d; }
  </style>
</head>
<body>

<div class="container">
  <div class="header">
    <h1>二極體 (Diode) 載子流動與參雜濃度模擬系統</h1>
  </div>

  <div class="element-grid" id="elementContainer"></div>

  <div class="control-panel">
    <div class="slider-group">
      <label>硼(B) 濃度 [P-Type]: <span id="b_lab">1.0</span></label>
      <input type="range" id="sliderB" min="0.1" max="10" step="0.1" value="1">
    </div>
    
    <button id="powerBtn" class="btn-main">啟動外加電壓 (3V)</button>

    <div class="slider-group">
      <label>磷(P) 濃度 [N-Type]: <span id="p_lab">1.0</span></label>
      <input type="range" id="sliderP" min="0.1" max="10" step="0.1" value="1">
    </div>
  </div>

  <div class="canvas-wrapper">
    <canvas id="simCanvas" width="900" height="300"></canvas>
  </div>

  <div class="status-grid">
    <div class="status-item"><span class="status-label">二極體兩端壓降</span><span class="status-value">0.7 V</span></div>
    <div class="status-item"><span class="status-label">即時電流大小</span><span class="status-value" id="cur_val">0.0 mA</span></div>
    <div class="status-item"><span class="status-label">電洞再結合率</span><span class="status-value" id="recomb_val">0%</span></div>
    <div class="status-item"><span class="status-label">載子瓶頸係數</span><span class="status-value" id="bottle_val">100%</span></div>
    <div id="heatStatus" class="heat-bar">系統待機中...</div>
  </div>
</div>

<script>
  const appData = <?!= JSON.stringify(data) ?>;
  const elements = appData.elements;
  
  const eCont = document.getElementById('elementContainer');
  ['Si', 'B', 'P'].forEach(s => {
    const d = elements[s];
    const c = document.createElement('div');
    c.className = `card ${s.toLowerCase()}`;
    c.innerHTML = `<h3>${s} ${d.name}</h3>
                   <p>原子序:${d.atomicNumber} | 原子量:${d.atomicWeight}</p>
                   <p>電子排布:${d.electrons}</p>`;
    eCont.appendChild(c);
  });

  const canvas = document.getElementById('simCanvas');
  const ctx = canvas.getContext('2d');
  let isPowered = false;
  let holes = [];
  let electrons = [];
  let sparks = []; 

  class Carrier {
    constructor(type, x, y) {
      this.type = type;
      this.x = x;
      this.y = y;
    }
    draw() {
      ctx.beginPath();
      ctx.arc(this.x, this.y, 5, 0, Math.PI*2);
      ctx.fillStyle = this.type === 'h' ? '#ff4d4d' : '#3399ff';
      ctx.fill();
      ctx.fillStyle = "white";
      ctx.font = "bold 9px Arial";
      ctx.textAlign = "center";
      ctx.fillText(this.type === 'h' ? '+' : '-', this.x, this.y+3);
    }
  }

  function spawnCarriers() {
    holes = [];
    electrons = [];
    const nB = parseFloat(document.getElementById('sliderB').value);
    const nP = parseFloat(document.getElementById('sliderP').value);
    for(let i=0; i < nB * 15; i++) holes.push(new Carrier('h', Math.random()*400 + 20, Math.random()*260 + 20));
    for(let i=0; i < nP * 15; i++) electrons.push(new Carrier('e', Math.random()*400 + 480, Math.random()*260 + 20));
  }

  function update() {
    ctx.fillStyle = "#000";
    ctx.fillRect(0,0,900,300);

    ctx.fillStyle = "rgba(255, 77, 77, 0.1)"; ctx.fillRect(0,0,450,300);
    ctx.fillStyle = "rgba(51, 153, 255, 0.1)"; ctx.fillRect(450,0,450,300);
    ctx.strokeStyle = "#444"; ctx.setLineDash([10,10]);
    ctx.beginPath(); ctx.moveTo(450,0); ctx.lineTo(450,300); ctx.stroke();
    ctx.setLineDash([]);

    if (isPowered) {
      const nB = parseFloat(document.getElementById('sliderB').value);
      const nP = parseFloat(document.getElementById('sliderP').value);
      
      // 計算物理指標
      const bottleneck = Math.min(nB, nP) / Math.max(nB, nP);
      // 再結合率模擬:受雙方濃度乘積影響,100為基準
      const recombRate = (nB * nP) / 100; 
      const currentFactor = (nB + nP) / 2 * (0.5 + 0.5 * bottleneck);
      const currentMA = ((3.0 - 0.7) / 100) * 1000 * currentFactor;
      
      // 更新顯示數值 (修正處)
      document.getElementById('cur_val').innerText = currentMA.toFixed(1) + " mA";
      document.getElementById('bottle_val').innerText = (bottleneck * 100).toFixed(0) + "%";
      document.getElementById('recomb_val').innerText = Math.min(100, (recombRate * 1000)).toFixed(1) + "%";
      
      const moveSpeed = 0.5 + (currentMA / 50);

      holes.forEach(h => {
        h.x += moveSpeed * (Math.random() + 0.5);
        if (h.x > 460) h.x = 20; 
      });

      electrons.forEach(e => {
        e.x -= moveSpeed * (Math.random() + 0.5);
        if (e.x < 440) e.x = 880; 
      });

      // 再結合動畫觸發
      holes.forEach((h) => {
        electrons.forEach((e) => {
          if (Math.abs(h.x - e.x) < 12 && Math.abs(h.y - e.y) < 12 && h.x > 420 && h.x < 480) {
            if (Math.random() < 0.1) { // 隨機觸發閃光,避免畫面太亂
              sparks.push({x: h.x, y: h.y, life: 10});
              h.x = 20; e.x = 880; 
            }
          }
        });
      });

      updateHeatUI(currentMA);
    }

    holes.forEach(h => h.draw());
    electrons.forEach(e => e.draw());
    
    sparks = sparks.filter(s => s.life > 0);
    sparks.forEach(s => {
      ctx.beginPath(); ctx.arc(s.x, s.y, 15 - s.life, 0, Math.PI*2);
      ctx.fillStyle = `rgba(255, 255, 255, ${s.life/10})`; ctx.fill();
      s.life--;
    });

    requestAnimationFrame(update);
  }

  function updateHeatUI(ma) {
    const h = document.getElementById('heatStatus');
    if (ma > 40) {
      h.innerText = "發熱狀態:極高 (過熱中)";
      h.style.background = "#eb4d4b";
    } else if (ma > 15) {
      h.innerText = "發熱狀態:中等";
      h.style.background = "#f0932b";
    } else {
      h.innerText = "發熱狀態:低";
      h.style.background = "#6ab04c";
    }
  }

  document.getElementById('powerBtn').onclick = function() {
    isPowered = !isPowered;
    this.innerText = isPowered ? "切斷電壓 (OFF)" : "啟動外加電壓 (3V)";
    this.classList.toggle('on');
    if(!isPowered) {
      document.getElementById('cur_val').innerText = "0.0 mA";
      document.getElementById('recomb_val').innerText = "0%";
      document.getElementById('heatStatus').style.background = "#3d464d";
    }
  };

  document.getElementById('sliderB').oninput = function() {
    document.getElementById('b_lab').innerText = parseFloat(this.value).toFixed(1);
    spawnCarriers();
  };
  document.getElementById('sliderP').oninput = function() {
    document.getElementById('p_lab').innerText = parseFloat(this.value).toFixed(1);
    spawnCarriers();
  };

  spawnCarriers();
  update();
</script>
</body>
</html>
貼到 Apps Script 的 index.html 檔案

6. 部署

部署成 Web App 後,就會拿到可分享網址。

  1. Apps Script 右上:部署 → 新部署
  2. 類型選:Web app
  3. 執行身分:你
  4. 存取權限:任何人
  5. 完成後複製網址

💬 評論區

老師們的交流與提問

載入評論中...