全端工程師做的網頁以簡單質樸著稱(是說想華麗也華麗不起來),用到的視覺元素不多,以我自己為例,內部系統操作介面通常就 CSS 簡單配個色,頂多加些 16x16 規格小圖示讓網頁元素意義更容易理解,不需視覺設計人員支援,單兵就能完成戰鬥。(不只全端還一條龍呢)

很多時侯,圖示會帶來畫龍點睛的神奇效果,例如昨天展示的類別選取,若在檢視中類別前方加上資料夾打開的圖示(上圖),使用者更能理解到「原來點類別名稱會展開顯示項目呀」,比原來的版本(下圖)更直覺好上手。

這類小圖示需求,我一般優先考量用 Font Awesome,但 Font Awesome 免費版圖示選擇不多,圖案偏抽象,有時會找不到能貼切表達意圖的圖示。這時只能上網找到符合 16x16 規格的 SVG 或 PNG 圖示,還要留意風格一致及授權規定,往往花了不少時間還找不到合適的。

如果你有在使用 Visual Studio,這裡介紹一項好用資源,來自微軟的好用免費圖示庫 - Visual Studio Image Library。以 VS2022 Image Library 為例,其中包含 3,833 個圖示,提供 16x16 PNG 及 SVG 格式(SVG 為向量格式可自由縮放任意大小都不會出現馬賽克),範圍聚焦於表達按鈕或功能,剛好符合資料處理網頁的需求。

授權方面,依據其 EULA (End User License Agreement),只要是使用 Visual Studio 家族產品開發應用程式,便可在應用程式中使用這些圖示,甚至隨應用程式一起散佈,前題是你有善用它在應用程式發揮功能(add significant primary functionality to it in your applications),不能只把它當成主體散佈出去。換言之,只要是用 VS 開發,便可在系統中免費使用這批圖示。
註:VS Image Library EULA 的法律用詞跟其他我看過的軟體授權聲明相比並不算生澀,但仍然不好懂,若我的理解有誤,歡迎指正。

VS Image Library 有提供一分 HTML 索引(如上圖),方便你找圖示,但因網頁需載入及顯示 3,833 個 SVG,開啟跟檢視都很卡。為方便日常使用,我決定動手寫幾行程式,自製兩個小工具:關鍵字速查介面跟 CSS Sprite。
註:CSS Sprite 翻譯為 CSS 精靈圖/拼合圖/雪碧圖,指將一堆小圖檔合併成一張,用 CSS 控制可視範圍顯示特定圖案。好處是當需要大量不同圖示時,瀏覽器用一次下載取代數十上百次 HTTP 請求,可大幅善瀏覽及顯示效率。

以下是我的實作過程:

產生圖示資料清單

先寫一小段 JavaScript 將索引網頁 Visual Studio 2022 Image Library.html 的表格內容轉成 JSON 資料陣列。程式如下:

let row = 0;
let col = 0;
const imgsPerRow = 64;
const imgSize = 16;
// 取得所有圖片 KnownMoniker、描述
const data = [...document.body.querySelector('tbody').children].map(tr => {
    const tds = [...tr.children];
    const obj = {
        km: tds[1].innerText, //KnownMoniker
        desc: tds[2].innerText,
        row: row,
        col: col++
    };
    if (col == imgsPerRow) {
        col = 0;
        row++;
    }
    return obj;
});

如此我得到格式如下物件陣列:

[
    {
        "km": "Abbreviation",
        "desc": "Refer to icon name.",
        "row": 0,
        "col": 0
    },
    {
        "km": "AboutBox",
        "desc": "Refer to icon name.",
        "row": 0,
        "col": 1
    },
//... 共 3,833 筆

用 Canvas 拼裝成一張大圖

建立 HTML5 Canvas 將所有 PNG 依序繪製到畫布上,並同步產生 CSS 樣式。

瀏覽器同時能發出連線數有上限,處理快四千個圖檔要點時間,為此我還放了進度顯示以舒緩等待情緒:(謎:是有多沒耐性?)

<!DOCTYPE html>
<html>

<head>
    <meta charset="utf-8" />
    <title>Make Image Sprite</title>
    <script src="data.js"></script>
</head>

<body>
    <div id=status></div>
    <script>
        const css = [];
        css.push(`.ti { display: inline-block; width: 16px; height: 16px; background-image: url('sprite.png'); }`);
        const imgsPerRow = 64;
        const imgSize = 16;
        let count = data.length;
        let loaded = 0;
        // 建立足以容納所有圖片的 canvas
        const canvas = document.createElement('canvas');
        // 64 個圖示為一列,計算所需列數
        const totalRows = Math.ceil(data.length / imgsPerRow) + Math.sign(data.length / imgsPerRow);
        // 計算 canvas 所需寬、高
        canvas.width = imgsPerRow * imgSize;
        canvas.height = totalRows * imgSize;
        const ctx = canvas.getContext('2d');
        document.body.appendChild(canvas);
        const queue = data.slice();
        // 逐一載入圖片,並繪製到 canvas 上
        for (let r = 0; r < totalRows; r++) {
            for (let c = 0; c < imgsPerRow; c++) {
                const img = new Image();
                if (queue.length == 0) break;
                const obj = queue.shift();
                img.src = `./images/${obj.km}.png`;
                img.onload = () => {
                    const x = c * imgSize;
                    const y = r * imgSize;
                    ctx.drawImage(img, x, y, imgSize, imgSize);
                    css.push(`.ti-${obj.km} { background-position: -${x}px -${y}px; }`);
                    loaded++;
                    document.getElementById('status').innerHTML = `${loaded} of ${count}`;
                }
            }
        }
    </script>
</body>

</html>

3833 個圖示全部畫完要花快十分鐘,但因為是一次工,就不動腦筋研究如何加速了。

懶得寫存檔程式,完成後將拼成的大圖另存成 sprite.png,CSS 則用 F12 console.log() 後手工另存成 sprite.css。

試用 CSS Sprite

有了 sprite.css,我們只需寫 <span class="ti ti-FolderOpened"></span> 就能在該處插入 16x16 的 FolderOpened.png,超方便:

VS 圖示速查工具

最後,來寫幾行程式用 Vue.js 實現關鍵字速查:

<!DOCType html>
<html>
<head>
    <meta charset="utf-8" />
    <title>VS2022 Image Library Index</title>
    <script src="data.js"></script>
    <script src="https://unpkg.com/vue@next"></script>
    <link rel="stylesheet" href="sprite.css" />
    <style>
        .list {
            display: flex;
            flex-wrap: wrap;
        }
        .list > div {
            min-width: 250px;
        }
        .op {
            margin-bottom: 10px;
        }
        span.clear {
            cursor: pointer;
            vertical-align: middle;
            margin-left: 3px;
        }
    </style>
</head>
<body>
    <div id="app">
        <h3>Visual Studio 2022 Image Library Index</h3>
        <div class="op">
            <input type="text" v-model="Keywd" placeholder="Keyword" />
            <span class="ti ti-Cancel clear" @click="Keywd=''" v-show="Keywd"></span>
        </div>
        <div class="list">
            <div v-for="item in FilteredData">
                <span :class="`ti ti-${item.km}`"></span>
                {{item.km}}
            </div>
        </div>
    </div>
    <script>
        var app = Vue.createApp({
            data() {
                return {
                    Keywd: '',
                    Data: data.map(item => { 
                        return { 
                            km: item.km, 
                            text: `${item.km.toLowerCase()} ${(item.desc || '').toLowerCase() }`
                        }; 
                    })
                }
            },
            computed: {
                FilteredData() {
                    const keywd = this.Keywd.trim().toLowerCase();
                    return this.Data.filter(item => item.text.includes(keywd));
                }
            }
        });
        var vm = app.mount('#app');
    </script>
</body>
</html>

鄉親吶,瞧瞧這快如閃電的查詢速度,這樣的索引查詢用起來才順手。

至於檔案大小,sprite.png 799KB,sprite.css 約 223KB,二者合計不到 1MB。如果要優化,則可以篩選圖示拿掉完全用不到的項目,讓圖檔跟 CSS 都小一些。若不想花時間,這個尺寸以區域網路的傳輸速度跟個人電腦運算能力,直接在內部網站使用應該也是沒問題的。

就醬,全端工具箱再添好用工具一枚。

Tutorial of how to create CSS sprite for VS 2022 image library icons.


Comments

# by Ellis

感謝黑大教學,實在好用~ 想問@___@"一下,是否 vus.js v-for 處的 :key 是 item.km ?

# by Jeffrey

to Ellis, 是的,程式有調整,那段理論上可以不用,感謝提醒。

Post a comment