iT邦幫忙

2026 iThome 鐵人賽

DAY 10
0
Modern Web

從實務需求出發:前端視覺與 UI 互動開發實踐系列 第 10 篇

【Day 10】SVG 實作:路線圖(上) 在 path 上找一個家

  • 分享至 

  • xImage
  •  

之前接到製作進度地圖的任務,地圖上有個不規則的曲線,以及代表步驟的數個點。我不疑有他,匯出一張帶有曲線的圖片當底圖,再直白地用 position、top 與 left 把一個個地點定位好。做完了!讚!

正當我帶著保溫杯,出了辦公室,喝著咖啡還唱著歌,突然就被麻匪劫了!

越想越不對勁,這個地圖是動態的。在設計稿的其他頁面中有 UI 能增減步驟數量,而這邊的設計稿畫的是最多地點時的狀況。然後還有一條會沿著路徑,畫到完成步驟的虛線。這下好了,步驟定位還可以靠 CSS 硬幹,但是動態增減的路徑,還真的要靠操作 SVG 來解決。

製作路線圖會用到「SVG 座標」與「SVG DOM API」兩個知識點,寫起來有點多。今天先了解什麼是 SVG 的座標吧!

知識點介紹

<path> 中的 d 是什麼?

d 是用來定義這個路徑要怎麼畫,前幾天作為範例的星星中,就能看到  d 是一長串的英文與數字,這些其實就是一個個的繪圖指令與座標。常見的繪圖指令有:

  • M:Move to,移動到指定座標,提筆不畫線。
  • L:Line to,畫直線到指定座標。
  • H:Horizontal Line to,畫水平直線到指定的 x 座標。
  • V:Vertical Line to,畫垂直直線到指定的 y 座標。
  • C / S:Cubic Bézier Curve / Smooth Cubic Bézier Curve,畫三次貝茲曲線。
  • Q / T:Quadratic Bézier Curve / Smooth Quadratic Bézier Curve,畫二次貝茲曲線。
  • A:Arc,畫橢圓弧。
  • Z:Close Path,封閉路徑,將終點與起點連起來。

由此可知,path 是由一連串的筆畫組成。所以反過來想,我們也能一段段地把它拆解獨立出來。

補充 - 貝茲曲線

以 C 為例,類似使用設計軟體畫曲線的操作,決定起點與終點,再拉控制點調整曲線弧度。

<path d="M40 220 C80 30 320 30 360 220" ... />
         └─起點─┘ └控制1┘└控制2┘└─終點─┘

https://ithelp.ithome.com.tw/upload/images/20260925/20184345eSmlnbPVbT.png

viewBox 與座標

d 裡的座標並不是 px,而是 SVG 自己的座標系。以 <svg viewBox="0 0 900 600"> 為例,viewBox 的
四個值分別是 min-x min-y width height,意思是「這張畫布的座標,從 (0, 0) 到 (900, 600)」。
無論寬高如何變化,SVG 內部的圖形都是參考畫布設定,等比例縮放。也就是說我們可以取 <path> 的繪圖座標,推算步驟圓點的定位。

與容器切齊,避免 preserveAspectRatio

上面的公式有個前提:<svg> 元素的長寬比,要和 viewBox 一樣。如果不一樣,例如把 900 × 600 的 viewBox 塞進一個正方形,SVG 預設(preserveAspectRatio="xMidYMid meet")會像 object-fit: contain 一樣,等比縮放並置中,上下多出留白。這時用百分比算出來的位置就會偏掉。

最簡單的解法,是讓容器比例和 viewBox 一致:在 CSS 下aspect-ratio: 900 / 600 指定長寬比,這樣就沒有留白。


動手做

步驟 1:取得路線 SVG

先從設計稿匯出 SVG。下面這個範例是一條的半透明路線。把路徑 d 與 viewBox 設定成變數,方便等等使用。

<script setup>
	const ROADMAP_PATH_D = 'M109.5 78.7C147.1 84.3 161.5 156.3 171.1 268.3C180.7 380.3 195.1 434.7 243.1 439.5C275.1 442.7 291.1 448.3 319.1 412.3C363.1 356.3 427.1 244.3 486.3 213.9C515.1 199.5 535.1 196.3 547.1 224.3C559.1 252.3 561.5 324.3 564.7 396.3C567.9 468.3 571.1 516.3 619.1 522.7C667.1 529.1 699.1 476.3 801.5 360.3';
    
    const VIEWBOX = { width: 900, height: 600 };
</script>

<template>
    <svg viewBox="0 0 900 600">
        <path
            :d="ROADMAP_PATH_D"
            fill="none"
            stroke="#8b0000"
            stroke-width="40"
            stroke-linecap="round"
            opacity="0.4"
        />
    </svg>
</template>

https://ithelp.ithome.com.tw/upload/images/20260925/20184345jZygVZUbfq.png

步驟 2:建立定位容器

關卡節點是 HTML 元素,要疊在 SVG 上面,所以需要一個容器當定位基準。

<template>
    <div class="roadmap">
        <svg viewBox="0 0 900 600">
            <path ... />
        </svg>
    </div>
</template>

<style scoped>
.roadmap {
    position: relative;        /* 讓子元素以它為定位基準 */
    width: 100%;
    max-width: 900px;
    aspect-ratio: 900 / 600;   /* 與 viewBox 同比例 */
}

</style>

利用 aspect-ratio: 900 / 600 讓容器、SVG、viewBox 三者比例一致,這樣 SVG 裡就不會出現 preserveAspectRatio 造成的留白。

步驟 3:找出關卡座標

從 d 裡挑出每段曲線的終點。我們先用 <circle> 在 SVG 裡畫出來,確認座標真的落在路線上。

<script setup>
// 從 d 裡挑出的曲線段終點,當作關卡座標(viewBox 座標)

const LEVEL_POINTS = [
    { x: 109.5, y: 78.7 },
    { x: 243.1, y: 439.5 },
    { x: 486.3, y: 213.9 },
    { x: 564.7, y: 396.3 },
    { x: 619.1, y: 522.7 },
    { x: 801.5, y: 360.3 },
]
</script>

<template>
    <div class="roadmap">
        <svg class="roadmap__svg" viewBox="0 0 900 600">
            <path ... />
            <!-- 驗證用,確認後刪除 -->
            <circle
                v-for="(point, index) in LEVEL_POINTS"
                :key="index"
                :cx="point.x"
                :cy="point.y"
                r="10"
                fill="blue"
            />
        </svg>
    </div>
</template>

就能看到 6 個藍點,全都落在路線上。
https://ithelp.ithome.com.tw/upload/images/20260925/201843457KfuYQfAb3.png

  • 在 SVG 內部,直接用 viewBox 座標就能定位,完全不用換算。
  • 問題是 <circle> 能做的樣式有限。如果節點要放文字、圖片、hover 效果,用 HTML 會方便得多,所以接下來要把座標換算給 HTML 用。

步驟 4:換算成百分比,放上 HTML 節點

先刪掉驗證用的 <circle>,改用 HTML 的 <div>:

<script setup>
import { computed } from 'vue'
// ...

const nodes = computed(() =>
    LEVEL_POINTS.map(({ x, y }) => ({
        style: {
            left: `${(x / VIEWBOX.width) * 100}%`,
            top: `${(y / VIEWBOX.height) * 100}%`,
        },
    }))
)
</script>

<template>
    <div class="roadmap">
        <svg ...>...</svg>

        <div
            v-for="(node, index) in nodes"
            :key="index"
            class="roadmap__node"
            :style="node.style"
        >
            {{ index + 1 }}
        </div>
    </div>
</template>

<style scoped>
/* ... */
.roadmap__node {
    position: absolute;
    width: 40px;
    line-height: 40px;
    text-align: center;
    border-radius: 50%;
    color: #fff;
    background: #bbb;
    transform: translate(-50%, -50%); /* 讓節點中心對齊座標點 */
}
</style>

node 定位計算方式:

  • 水平位置 = x / viewBox 寬 × 100%
  • 垂直位置 = y / viewBox 高 × 100%

單位之所以用百分比而不是 px,是為了讓容器縮放時節點會自動跟著移動。以第一個點為例:109.5 / 900 ≈ 12%、78.7 / 600 ≈ 13%。
https://ithelp.ithome.com.tw/upload/images/20260925/201843453rtpaI5FIQ.png


.
.
.
這樣寫比原本用 position 定位還要準確,定位的點一定會在路徑上。不過目前座標是手動設定,還沒處理步驟數量變動,以及那條要畫到目前進度的虛線。

明天,我們會用 SVG DOM API 測量 path,再用 JavaScript 將圖形資訊換算成變動的定位點和進度線結束處。


上一篇
【Day 09】SVG 實作:化繁為簡,圖形元件化
下一篇
【Day 11】SVG 實作:路線圖(下) 把 path 當切糕,下刀兒!
系列文
從實務需求出發:前端視覺與 UI 互動開發實踐 共 17 篇
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言