<?xml version="1.0" encoding="utf-8" standalone="yes"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
  <channel>
    <title>JavaScript on Wayne X.Y. Blog</title>
    <link>/tags/javascript/</link>
    <description>Recent content in JavaScript on Wayne X.Y. Blog</description>
    <generator>Hugo</generator>
    <language>zh-TW</language>
    <lastBuildDate>Tue, 05 May 2026 00:00:00 +0000</lastBuildDate>
    <atom:link href="/tags/javascript/index.xml" rel="self" type="application/rss+xml" />
    <item>
      <title>VocabularyTest：打造自己的英文單字練習工具</title>
      <link>/zh/2026/05/05/vocabulary-test/</link>
      <pubDate>Tue, 05 May 2026 00:00:00 +0000</pubDate>
      <guid>/zh/2026/05/05/vocabulary-test/</guid>
      <description>&lt;h2 id=&#34;為什麼做這個工具&#34;&gt;為什麼做這個工具？&lt;/h2&gt;&#xA;&lt;p&gt;學英文最痛的環節大概就是背單字。市面上有不少 App 可以用，但總覺得少了點什麼——要嘛廣告太多，要嘛介面複雜，要嘛沒辦法塞進自己想練的詞彙。&lt;/p&gt;&#xA;&lt;p&gt;所以我乾脆自己做一個。&lt;/p&gt;&#xA;&lt;p&gt;&lt;strong&gt;&lt;a href=&#34;https://vocabulary-test.wayne-xy.com/&#34;&gt;VocabularyTest&lt;/a&gt;&lt;/strong&gt; 是我最近完成的 Side Project，一個輕量的英文單字測驗網頁應用，目標很簡單：打開就能練，練完就能走。&lt;/p&gt;&#xA;&lt;hr&gt;&#xA;&lt;h2 id=&#34;主要功能&#34;&gt;主要功能&lt;/h2&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;&lt;strong&gt;題數選擇&lt;/strong&gt;：開始測驗前可選擇 10、20、30 題或全部單字&lt;/li&gt;&#xA;&lt;li&gt;&lt;strong&gt;即時回饋&lt;/strong&gt;：每題作答後，正確選項變綠色，答錯的選項變橘紅色，立刻知道哪裡錯&lt;/li&gt;&#xA;&lt;li&gt;&lt;strong&gt;錯題複習&lt;/strong&gt;：測驗結束後，若答錯 4 題以上可進入錯題重測，針對弱點加強&lt;/li&gt;&#xA;&lt;li&gt;&lt;strong&gt;單字列表&lt;/strong&gt;：切換到 📋 頁籤可瀏覽所有單字，支援關鍵字搜尋、詞性篩選、A–Z / Z–A 排序，以及列表 / 格狀兩種顯示模式&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;hr&gt;&#xA;&lt;h2 id=&#34;如何匯入單字&#34;&gt;如何匯入單字&lt;/h2&gt;&#xA;&lt;p&gt;應用程式讀取 &lt;code&gt;.md&lt;/code&gt; 或 &lt;code&gt;.txt&lt;/code&gt; 格式的檔案，每一筆單字須依照以下格式撰寫：&lt;/p&gt;&#xA;&lt;pre tabindex=&#34;0&#34;&gt;&lt;code&gt;- **單字**，[音標]，詞性，中文意思&#xA;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;實際範例：&lt;/p&gt;&#xA;&lt;pre tabindex=&#34;0&#34;&gt;&lt;code&gt;- **absolute**，[ˈæb.sə.luːt]，adj，絕對的、完全的&#xA;- **ambiguous**，[æmˈbɪɡ.ju.əs]，adj，模糊的、不明確的&#xA;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;中文全形逗號 &lt;code&gt;，&lt;/code&gt; 與英文半形逗號 &lt;code&gt;,&lt;/code&gt; 都可辨識，格式相當彈性。整理好單字檔後，直接上傳到網頁即可開始測驗。&lt;/p&gt;&#xA;&lt;hr&gt;&#xA;&lt;h2 id=&#34;技術架構&#34;&gt;技術架構&lt;/h2&gt;&#xA;&lt;p&gt;整個專案以純前端實作，部署在 Cloudflare Pages 上。單字解析、測驗邏輯與介面渲染各自拆分成獨立模組（&lt;code&gt;parser.js&lt;/code&gt;、&lt;code&gt;quiz.js&lt;/code&gt;、&lt;code&gt;ui.js&lt;/code&gt;），方便日後維護與擴充。&lt;/p&gt;&#xA;&lt;hr&gt;&#xA;&lt;h2 id=&#34;試試看&#34;&gt;試試看&lt;/h2&gt;&#xA;&lt;p&gt;直接前往：&lt;strong&gt;&lt;a href=&#34;https://vocabulary-test.wayne-xy.com/&#34;&gt;vocabulary-test.wayne-xy.com&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;</description>
    </item>
    <item>
      <title>在 Hugo 部落格中加入互動式旅行地圖 (Leaflet &#43; JSON)</title>
      <link>/zh/2026/03/10/travel-map-leaflet-hugo/</link>
      <pubDate>Tue, 10 Mar 2026 00:00:00 +0000</pubDate>
      <guid>/zh/2026/03/10/travel-map-leaflet-hugo/</guid>
      <description>&lt;h1 id=&#34;在-hugo-部落格中加入互動式旅行地圖-leaflet--json&#34;&gt;在 Hugo 部落格中加入互動式旅行地圖 (Leaflet + JSON)&lt;/h1&gt;&#xA;&lt;p&gt;最近想在部落格的 Travel 頁面頂部加上一個可以標記去過哪些地方的互動式地圖，幾經思考後決定使用開源且輕量的 Leaflet.js，搭配自訂的 JSON 檔案來管理景點和顏色。&lt;/p&gt;&#xA;&lt;hr&gt;&#xA;&lt;h2 id=&#34;-實作目標&#34;&gt;🗺️ 實作目標&lt;/h2&gt;&#xA;&lt;ol&gt;&#xA;&lt;li&gt;在 &lt;a href=&#34;/life/travel/&#34;&gt;&lt;code&gt;/life/travel&lt;/code&gt;&lt;/a&gt; 的頂部固定一個地圖，顯示去過的國家與地點。&lt;/li&gt;&#xA;&lt;li&gt;透過 JSON 檔案集中管理景點資料 (包含中英文名稱、經緯度、以及專屬顏色)。&lt;/li&gt;&#xA;&lt;li&gt;使用 Leaflet.js 渲染地圖，並根據 JSON 自動計算邊界縮放 (&lt;code&gt;fitBounds&lt;/code&gt;) 以容納所有標記。&lt;/li&gt;&#xA;&lt;li&gt;將地圖封裝成 Hugo Shortcode &lt;code&gt;&lt;link rel=&#34;stylesheet&#34; href=&#34;https://unpkg.com/leaflet@1.9.4/dist/leaflet.css&#34;&#xA;    integrity=&#34;sha256-p4NxAoJBhIIN+hmNHrzRCf9tD/miZyoHS5obTRR9BMY=&#34; crossorigin=&#34;&#34; /&gt;&#xA;&lt;script src=&#34;https://unpkg.com/leaflet@1.9.4/dist/leaflet.js&#34;&#xA;    integrity=&#34;sha256-20nQCchB9co0qIjJZRGuk2/Z9VM+kNiyxNV1lvTlZBo=&#34; crossorigin=&#34;&#34;&gt;&lt;/script&gt;&#xA;&#xA;&lt;div id=&#34;travel-map&#34; style=&#34;height: 400px; width: 100%; border-radius: 8px; margin-bottom: 2rem; z-index: 1;&#34;&gt;&lt;/div&gt;&#xA;&#xA;&lt;script&gt;&#xA;    document.addEventListener(&#34;DOMContentLoaded&#34;, function () {&#xA;        &#xA;        delete L.Icon.Default.prototype._getIconUrl;&#xA;        L.Icon.Default.mergeOptions({&#xA;            iconRetinaUrl: &#39;https://unpkg.com/leaflet@1.9.4/dist/images/marker-icon-2x.png&#39;,&#xA;            iconUrl: &#39;https://unpkg.com/leaflet@1.9.4/dist/images/marker-icon.png&#39;,&#xA;            shadowUrl: &#39;https://unpkg.com/leaflet@1.9.4/dist/images/marker-shadow.png&#39;,&#xA;        });&#xA;&#xA;        var travelData = [{&#34;color&#34;:&#34;red&#34;,&#34;lat&#34;:25.033,&#34;lng&#34;:121.5654,&#34;name_en&#34;:&#34;Taipei (Taiwan)&#34;,&#34;name_zh&#34;:&#34;台北 (台灣)&#34;},{&#34;color&#34;:&#34;red&#34;,&#34;lat&#34;:33.5902,&#34;lng&#34;:130.4017,&#34;name_en&#34;:&#34;Fukuoka City&#34;,&#34;name_zh&#34;:&#34;福岡市&#34;},{&#34;color&#34;:&#34;red&#34;,&#34;lat&#34;:33.2662,&#34;lng&#34;:131.3537,&#34;name_en&#34;:&#34;Yufuin&#34;,&#34;name_zh&#34;:&#34;由布院&#34;},{&#34;color&#34;:&#34;red&#34;,&#34;lat&#34;:33.2794,&#34;lng&#34;:131.5015,&#34;name_en&#34;:&#34;Beppu&#34;,&#34;name_zh&#34;:&#34;別府&#34;},{&#34;color&#34;:&#34;red&#34;,&#34;lat&#34;:33.173,&#34;lng&#34;:131.226,&#34;name_en&#34;:&#34;Kokonoe&#34;,&#34;name_zh&#34;:&#34;九重&#34;},{&#34;color&#34;:&#34;red&#34;,&#34;lat&#34;:33.0805,&#34;lng&#34;:131.1441,&#34;name_en&#34;:&#34;Kurokawa Onsen&#34;,&#34;name_zh&#34;:&#34;黑川溫泉&#34;},{&#34;color&#34;:&#34;red&#34;,&#34;lat&#34;:32.8062,&#34;lng&#34;:130.7058,&#34;name_en&#34;:&#34;Kumamoto&#34;,&#34;name_zh&#34;:&#34;熊本&#34;},{&#34;color&#34;:&#34;red&#34;,&#34;lat&#34;:33.1654,&#34;lng&#34;:130.4137,&#34;name_en&#34;:&#34;Yanagawa&#34;,&#34;name_zh&#34;:&#34;柳川&#34;},{&#34;color&#34;:&#34;red&#34;,&#34;lat&#34;:33.5215,&#34;lng&#34;:130.5348,&#34;name_en&#34;:&#34;Dazaifu&#34;,&#34;name_zh&#34;:&#34;太宰府&#34;}];&#xA;    var lang = &#34;zh&#34;;&#xA;&#xA;    var map = L.map(&#39;travel-map&#39;, {&#xA;        scrollWheelZoom: true&#xA;    });&#xA;&#xA;    L.tileLayer(&#39;https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png&#39;, {&#xA;        attribution: &#39;&amp;copy; &lt;a href=&#34;https://www.openstreetmap.org/copyright&#34;&gt;OpenStreetMap&lt;/a&gt; contributors&#39;&#xA;    }).addTo(map);&#xA;&#xA;    var bounds = [];&#xA;&#xA;    if (travelData &amp;&amp; travelData.length &gt; 0) {&#xA;        travelData.forEach(function (loc) {&#xA;            var name = lang === &#34;en&#34; ? loc.name_en : loc.name_zh;&#xA;            var markerOptions = {};&#xA;            if (loc.color) {&#xA;                markerOptions.icon = new L.Icon({&#xA;                    iconUrl: &#39;https://raw.githubusercontent.com/pointhi/leaflet-color-markers/master/img/marker-icon-2x-&#39; + loc.color + &#39;.png&#39;,&#xA;                    shadowUrl: &#39;https://unpkg.com/leaflet@1.9.4/dist/images/marker-shadow.png&#39;,&#xA;                    iconSize: [25, 41],&#xA;                    iconAnchor: [12, 41],&#xA;                    popupAnchor: [1, -34],&#xA;                    shadowSize: [41, 41]&#xA;                });&#xA;            }&#xA;            var marker = L.marker([loc.lat, loc.lng], markerOptions).addTo(map)&#xA;                .bindPopup(&#34;&lt;b&gt;&#34; + name + &#34;&lt;/b&gt;&#34;);&#xA;            bounds.push([loc.lat, loc.lng]);&#xA;        });&#xA;        map.fitBounds(bounds, { padding: [50, 50] });&#xA;    } else {&#xA;        map.setView([23.6978, 120.9605], 5); &#xA;    }&#xA;});&#xA;&lt;/script&gt;&lt;/code&gt; 方便在不同語系頁面重複使用。&lt;/li&gt;&#xA;&lt;/ol&gt;&#xA;&lt;h2 id=&#34;-建立資料檔-data-layer&#34;&gt;💾 建立資料檔 (Data Layer)&lt;/h2&gt;&#xA;&lt;p&gt;首先，在 Hugo 專案的 &lt;code&gt;data/&lt;/code&gt; 目錄下建立一個 &lt;code&gt;travel.json&lt;/code&gt;，之後如果有新增旅程，只要修改這裡就可以，不用動到程式碼：&lt;/p&gt;</description>
    </item>
    <item>
      <title>在網頁中打造微型世界：Playground 像素藝術與尋路實作</title>
      <link>/zh/2026/02/26/playground-pixel-art-implementation/</link>
      <pubDate>Thu, 26 Feb 2026 00:00:00 +0000</pubDate>
      <guid>/zh/2026/02/26/playground-pixel-art-implementation/</guid>
      <description>&lt;h1 id=&#34;在網頁中打造微型世界playground-像素藝術與尋路實作&#34;&gt;在網頁中打造微型世界：Playground 像素藝術與尋路實作&lt;/h1&gt;&#xA;&lt;p&gt;&#xA;  &lt;img src=&#34;/img/playground/playground_overview.png&#34; alt=&#34;Playground Overview&#34;&gt;&#xA;&#xA;&lt;/p&gt;&#xA;&lt;p&gt;前陣子 coding 時看到 github 上面一個有趣的專案叫做 &lt;a href=&#34;https://github.com/pablodelucca/pixel-agents&#34;&gt;Pixel Agents&lt;/a&gt;，能夠把 AI coding 時，將 agent 模擬成像素風格的小人物，並把處理過程模擬成在辦公室工作的樣子，看起來很可愛😆，也因為我很喜歡像素風格，就想說能不能在自己的網站也呈現類似的感覺，於是就有了這個 &lt;a href=&#34;/playground/&#34;&gt;Playground&lt;/a&gt;。&lt;/p&gt;&#xA;&lt;p&gt;這篇文章將會分享這個小天地背後的實作細節，包含如何用 HTML5 Canvas 渲染出銳利的像素畫、角色狀態的設計，以及他們是如何在房間內找到路徑（BFS 尋路演算法）的。&lt;/p&gt;&#xA;&lt;p&gt;場景素材則是來自 &lt;a href=&#34;https://limezu.itch.io/moderninteriors&#34;&gt;Modern Interiors&lt;/a&gt;。&lt;/p&gt;&#xA;&lt;hr&gt;&#xA;&lt;h2 id=&#34;-核心架構設計canvas-與銳利的像素&#34;&gt;🎨 核心架構設計：Canvas 與銳利的像素&lt;/h2&gt;&#xA;&lt;p&gt;整個 Playground 的基礎是建立在 HTML5 的 &lt;code&gt;&amp;lt;canvas&amp;gt;&lt;/code&gt; 標籤上。為了忠實呈現早期遊戲機那種清晰的「顆粒感」，我們必須確保圖片放大後不會變得模糊。&lt;/p&gt;&#xA;&lt;p&gt;在繪圖素材上，角色的單格尺寸其實非常小，僅有 &lt;code&gt;16x32&lt;/code&gt; 像素（寬 16 像素，高 32 像素），而基礎的地磚區塊大小 (Tile Size) 則是 &lt;code&gt;16x16&lt;/code&gt; 像素。&lt;/p&gt;&#xA;&lt;p&gt;如果我們直接把畫布依原尺寸顯示在網頁上，由於現代螢幕解析度極高，使用者可能要拿放大鏡才看得到角色。因此，我們需要在 CSS 中把它放大：&lt;/p&gt;&#xA;&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#282a36;-moz-tab-size:4;-o-tab-size:4;tab-size:4;&#34;&gt;&lt;code class=&#34;language-css&#34; data-lang=&#34;css&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;#playgroundCanvas {&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#ff79c6&#34;&gt;width&lt;/span&gt;: &lt;span style=&#34;color:#bd93f9&#34;&gt;832&lt;/span&gt;&lt;span style=&#34;color:#8be9fd&#34;&gt;px&lt;/span&gt;;     &lt;span style=&#34;color:#6272a4&#34;&gt;/* 原始寬度 416 放大了兩倍 */&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#ff79c6&#34;&gt;height&lt;/span&gt;: &lt;span style=&#34;color:#bd93f9&#34;&gt;480&lt;/span&gt;&lt;span style=&#34;color:#8be9fd&#34;&gt;px&lt;/span&gt;;    &lt;span style=&#34;color:#6272a4&#34;&gt;/* 原始高度 240 放大了兩倍 */&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#ff79c6&#34;&gt;max-width&lt;/span&gt;: &lt;span style=&#34;color:#bd93f9&#34;&gt;100&lt;/span&gt;&lt;span style=&#34;color:#8be9fd&#34;&gt;%&lt;/span&gt;;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    image-rendering: &lt;span style=&#34;color:#ff79c6&#34;&gt;pixelated&lt;/span&gt;; &lt;span style=&#34;color:#6272a4&#34;&gt;/* 關鍵：保持像素銳利邊緣 */&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    image-rendering: &lt;span style=&#34;color:#ff79c6&#34;&gt;crisp-edges&lt;/span&gt;;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;}&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;透過 &lt;code&gt;image-rendering: pixelated;&lt;/code&gt;，瀏覽器在縮放 Canvas 時會使用最近鄰插值 (Nearest-neighbor interpolation)，從而保留了我們想要的 8-bit 復古感。&lt;/p&gt;</description>
    </item>
  </channel>
</rss>
