最近有個需求:手上有現成圖片(例如:投影片截圖、廣告 DM) 想快速轉成網頁,圖片上的特定項目希望加上超連結,點下去要能開啟指定網頁。

把整張圖當設計稿切版拆圖塊轉成 HTML 網頁當然是最完美的做法,但如果求快求簡單,則古老的 HTML <map> 元素 是個解法,在圖檔上方宣告幾何形狀,加上連結就搞定了。

手工算位置輸座標有點累,這部分應能找到現成軟體支援滑鼠操作拉形狀設連結,但使用者希望圖片可依頁寬縮放,的座標只吃數字不吃百分比。這樣的話,還不如以圖為背景,用 postion: absolute 配合百分比的 top、left、width、height 放一些

更簡單;我的應用情境單純,都用矩形就夠(萬一要不同形狀再說),至於設定點擊區域大小及位置。

我懶得 Survey 支援以上構想的現成軟體或工具,心想反正是 Github Copilot 擅長的 HTML/CSS/JavaScript 領域,自己寫也不會太費力。就醬,一個好用小工具誕生了。

直接看展示。

為圖檔加上點擊熱區

操作方式很簡單,提供一個圖檔連結當成底圖,在要設點擊熱區的地方按 Ctrl 點滑鼠左鍵,拉出所需要的大小。左方清單可查看已設定的熱區,透過數字欄位可微調位置跟大小,為每個熱區指定好連結或 JavaScript 指令,按匯出可得到一個可操作的示範網頁,再整合到網站專案就大功告成囉~

完整程式碼如下,有需要的同學請自取修改應用:

<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Image Map</title>
    <style>
        .rectangle {
            border: 3px dashed red;
            position: absolute;
            pointer-events: none;

        }

        #app {
            display: flex;
            flex-direction: row;

            [lang] {
                display: none;
            }

            .zh {
                [lang='zh'] {
                    display: inline;
                }
            }

            .en {
                [lang='en'] {
                    display: inline;
                }
            }

            .menu {
                width: 350px;
                flex-shrink: 0;
                background-color: #f0f0f0;
                padding: 8px;

                >div {
                    margin-bottom: 8px;
                }

                .actions {
                    a {
                        cursor: pointer;
                        text-decoration: underline;
                        color: blue;
                        margin-right: 12px;
                    }
                }
            }

            .canvas {
                flex-grow: 1;

                .hotspot.active {
                    border: 2pt solid red;
                }
            }
        }

        .editor {
            height: 135px;
            background-color: #ddd;
            padding: 6px;

            [type=number] {
                width: 60px;
            }
            label {
                cursor: pointer;
            }
            table {
                width: 100%;
                border-collapse: collapse;
            }

            td {
                border: 1px solid #888;

                &.hdr {
                    width: 50px;
                    text-align: right;
                    background-color: #ccc;
                }

                padding: 3px;
            }
        }

        .list {
            .item {
                padding: 6px 12px;
                background-color: 1px solid #444;
                border: 1pt solid #ddd;
                color: #666;
                &.active {
                    color: brown;
                    font-weight: bold;
                }

            }
        }
    </style>
    <style id="hotspot-style">
        .canvas {
            width: 100%;
            overflow: hidden;
            position: relative;

            img {
                width: 100%;
                /* visibility: hidden; */
                pointer-events: none;
            }

            .link {
                position: absolute;

            }
        }

        .hotspot {
            position: absolute;
            pointer-events: all;
            cursor: pointer;

            &:hover {
                border-color: purple;
                background-color: cyan;
                opacity: 0.25;
            }
        }
    </style>

    <script src="https://unpkg.com/vue@3"></script>
</head>

<body>
    <div id="app">
        <div class="menu" :class="{ zh: lang == 'zh', en: lang == 'en'}">
            <div class="actions">
                <span style="float: right">
                    &#127758;<a @click="lang='en'" v-show="lang=='zh'">En</a><a @click="lang='zh'" v-show="lang=='en'">中</a>
                </span>
                <button @click="exportHtml">
                    <span lang="en">Export HTML</span>
                    <span lang="zh">匯出 HTML 檔</span>
                </button>
            </div>
            <div class="editor">
                <table v-if="selectedHotspot">
                    <tr>
                        <td class="hdr">
                            <span lang="en">Link</span>
                            <span lang="zh">連結</span>
                        </td>
                        <td colspan="3">
                            <input type="text" v-model="selectedHotspot.link" placeholder="Link">
                            &nbsp;
                            <button @click="hotspots.splice(hotspots.indexOf(selectedHotspot), 1)">
                                <span lang="en">Delete</span>
                                <span lang="zh">刪除</span>
                            </button>
                        </td>
                    </tr>
                    <tr>
                        <td class="hdr">
                            <span lang="en">Options</span>
                            <span lang="zh"> 選項</span>
                        </td>
                        <td colspan="3">
                            <label><input type="checkbox" v-model="selectedHotspot.js">JavaScript</label>
                            &nbsp;
                            <label><input type="checkbox" v-model="selectedHotspot.ext">
                                <span lang="en">New window</span>
                                <span lang="zh">新視窗開啟</span>
                            </label>
                        </td>
                    </tr>
                    <tr>
                        <td class="hdr">X</td>
                        <td><input type="number" v-model="selectedHotspot.x" step="0.1"> %</td>
                        <td class="hdr">Y</td>
                        <td><input type="number" v-model="selectedHotspot.y" step="0.1"> %</td>
                    </tr>
                    <tr>
                        <td class="hdr">W</td>
                        <td><input type="number" v-model="selectedHotspot.width" step="0.1"> %</td>
                        <td class="hdr">H</td>
                        <td><input type="number" v-model="selectedHotspot.height" step="0.1"> %</td>
                    </tr>
                </table>
            </div>
            <div class="list">
                <span lang="en">Image:</span>
                <span lang="zh">圖檔:</span>
                <input v-model.lazy="imgPath" type="text" placeholder="Image path" style="width:250px">
                <div class="item" v-for="(hotspot,idx) in hotspots" :class="{active: hotspot === selectedHotspot}"
                    @click="selectedHotspot = hotspot">
                    <span lang="en">Block</span>
                    <span lang="zh">區塊</span>
                    {{idx}}. <span>{{hotspot.link}}</span>
                </div>
            </div>
        </div>
        <div class="canvas">
            <img :src="imgPath" style="pointer-events: none;" alt="eap" referrerpolicy="no-referrer">
            <div v-for="hotspot in hotspots" class="hotspot" :class="{active: hotspot === selectedHotspot}"
                @click="selectedHotspot = hotspot" :data-link="hotspot.link" :data-js="hotspot.js"
                :data-ext="hotspot.ext"
                :style="{left: hotspot.x + '%', top: hotspot.y + '%', width: hotspot.width + '%', height: hotspot.height + '%'}">
            </div>
        </div>
    </div>
    <script>
        class Hotspot {
            constructor(x, y, width, height, link) {
                this.x = x;
                this.y = y;
                this.width = width;
                this.height = height;
                this.link = link ?? '#';
                this.js = false;
                this.ext = false;
            }
        }
        const app = Vue.createApp({
            data() {
                return {
                    hotspots: [],
                    selectedHotspot: null,
                    lang: navigator.language.startsWith('zh') ? 'zh' : 'en',
                    imgPath: 'https://i.imgur.com/R8yWT5b.png'
                }
            },
            watch: {
                imgPath() {
                    this.hotspots = [];
                    this.selectedHotspot = null;
                }
            },
            methods: {
                exportHtml() {
                    const styles = document.getElementById('hotspot-style').innerText;
                    const canvasHtml = document.querySelector('.canvas').outerHTML;
                    const html = `[html]
[body]
<div><style>${styles}</style>${canvasHtml}</div>
[script]
document.addEventListener('click', (e) => {
    if (!e.target.classList.contains('hotspot')) return;
    const link = e.target.getAttribute('data-link');
    if (!link) return;
    const js = e.target.getAttribute('data-js') === 'true';
    const ext = e.target.getAttribute('data-ext') === 'true';
    if (js) eval(link);
    else if (ext) window.open(link);
    else location.href = link;
});
[/script]
[/body][/html]`.replace(/\[(\/?)(html|head|script|body)\]/g, '<$1$2>');
                    const blob = new Blob([html], {
                        type: 'text/html'
                    });
                    const url = URL.createObjectURL(blob);
                    const a = document.createElement('a');
                    a.href = url;
                    a.download = 'image-map.html';
                    a.click();
                }
            }
        })
        const vm = app.mount('#app');
    </script>
    <script>
        const canvas = document.querySelector('.canvas');
        let startX, startY, rect;
        let currRect = null;
        canvas.addEventListener('mousedown', (e) => {
            if (e.ctrlKey) {
                startX = e.offsetX;
                startY = e.offsetY;
                rect = document.createElement('div');
                rect.className = 'rectangle';
                rect.style.left = `${startX}px`;
                rect.style.top = `${startY}px`;
                canvas.appendChild(rect);
                currRect = rect;
            }
        });
        canvas.addEventListener('mouseup', (e) => {
            if (!currRect) return;
            const width = currRect.offsetWidth / canvas.offsetWidth * 100;
            const height = currRect.offsetHeight / canvas.offsetHeight * 100;
            const left = currRect.offsetLeft / canvas.offsetWidth * 100;
            const top = currRect.offsetTop / canvas.offsetHeight * 100;
            vm.hotspots.push(new Hotspot(left.toFixed(3), top.toFixed(3), width.toFixed(3), height.toFixed(3)));
            vm.selectedHotspot = vm.hotspots[vm.hotspots.length - 1];
            currRect.remove();
            currRect = null;
        });
        canvas.addEventListener('mousemove', (e) => {
            if (!currRect) return;
            const width = e.offsetX - startX;
            const height = e.offsetY - startY;
            rect.style.width = `${Math.abs(width)}px`;
            rect.style.height = `${Math.abs(height)}px`;
            rect.style.left = `${Math.min(startX, e.offsetX)}px`;
            rect.style.top = `${Math.min(startY, e.offsetY)}px`;
        });
    </script>
</body>

</html>

For quickly converting images into clickable web pages, consider leveraging a tool to define clickable areas with ease. This method allows for a simple and fast implementation, especially for rectangular regions, enabling quick integration of interactive elements.


Comments

Be the first to post a comment

Post a comment