EP33 - ex5. SVG

更新於 2024/10/10閱讀時間約 23 分鐘
哇!開始用SVG畫圖了~越來越實用了~
設計一些實用的可縮放向量圖形~
應該可以做一些簡單的動畫吧!

這次有四個檔案,App.vue、AxisLabel.vue、PolyGraph.vue、util.js

App.vue

<!--
An SVG graph
-->

<script setup>
import PolyGraph from './PolyGraph.vue'
import { ref, reactive } from 'vue'

const newLabel = ref('')
const stats = reactive([
{ label: 'A', value: 100 },
{ label: 'B', value: 100 },
{ label: 'C', value: 100 },
{ label: 'D', value: 100 },
{ label: 'E', value: 100 },
{ label: 'F', value: 100 }
])

function add(e) {
e.preventDefault()
if (!newLabel.value) return
stats.push({
label: newLabel.value,
value: 100
})
newLabel.value = ''
}

function remove(stat) {
if (stats.length > 3) {
stats.splice(stats.indexOf(stat), 1)
} else {
alert("Can't delete more!")
}
}
</script>

<template>
<svg width="200" height="200">
<PolyGraph :stats="stats"></PolyGraph>
</svg>

<!-- controls -->
<div v-for="stat in stats">
<label>{{stat.label}}</label>
<input type="range" v-model="stat.value" min="0" max="100">
<span>{{stat.value}}</span>
<button @click="remove(stat)" class="remove">X</button>
</div>

<form id="add">
<input name="newlabel" v-model="newLabel">
<button @click="add">Add a Stat</button>
</form>

<pre id="raw">{{ stats }}</pre>
</template>

<style>
polygon {
fill: #42b983;
opacity: 0.75;
}

circle {
fill: transparent;
stroke: #999;
}

text {
font-size: 10px;
fill: #666;
}

label {
display: inline-block;
margin-left: 10px;
width: 20px;
}

#raw {
position: absolute;
top: 0;
left: 300px;
}
</style>

<script setup>

import PolyGraph from './PolyGraph.vue'
import { ref, reactive } from 'vue'
  • PolyGraph 是我們將用於繪製多邊形圖的子組件。
  • refreactive 是 Vue 3 的響應性 API,用來創建響應性數據。
const newLabel = ref('')
const stats = reactive([
{ label: 'A', value: 100 },
{ label: 'B', value: 100 },
{ label: 'C', value: 100 },
{ label: 'D', value: 100 },
{ label: 'E', value: 100 },
{ label: 'F', value: 100 }
])
  • newLabel 是一個響應性引用,用來儲存新添加的標籤。
  • stats 是一個響應性物件,用來儲存每個標籤及其對應的值。
function add(e) {
e.preventDefault()
if (!newLabel.value) return
stats.push({
label: newLabel.value,
value: 100
})
newLabel.value = ''
}
  • add 函數用來添加新的標籤到 stats 中。
  • e.preventDefault() 用來防止表單的默認提交行為。
  • 如果 newLabel 不是空的,就將其添加到 stats 中,並重置 newLabel
function remove(stat) {
if (stats.length > 3) {
stats.splice(stats.indexOf(stat), 1)
} else {
alert("Can't delete more!")
}
}
  • remove 函數用來從 stats 中移除標籤。
  • stats 長度超過 3 時,允許移除;否則,顯示警告信息。

<template>

<svg width="200" height="200">
<PolyGraph :stats="stats"></PolyGraph>
</svg>
  • 使用 svg 元素來顯示多邊形圖。
  • PolyGraph 組件接收 stats 作為道具,繪製圖表。
  <div v-for="stat in stats">
<label>{{stat.label}}</label>
<input type="range" v-model="stat.value" min="0" max="100">
<span>{{stat.value}}</span>
<button @click="remove(stat)" class="remove">X</button>
</div>
  • 使用 v-for 遍歷 stats,顯示每個標籤及其對應的控制項。
  • input 元素用來調整標籤的值。
  • button 用來移除標籤。
  <form id="add">
<input name="newlabel" v-model="newLabel">
<button @click="add">Add a Stat</button>
</form>
  • 表單用來添加新標籤。
  • input 用來輸入新標籤的名稱,綁定到 newLabel
  • button 用來觸發 add 函數,添加新標籤。
  <pre id="raw">{{ stats }}</pre>
  • 使用 <pre> 元素顯示 stats 的原始數據,以便於調試。

<style>

<style scoped>
polygon {
fill: #42b983;
opacity: 0.75;
}

circle {
fill: transparent;
stroke: #999;
}

text {
font-size: 10px;
fill: #666;
}

label {
display: inline-block;
margin-left: 10px;
width: 20px;
}

#raw {
position: absolute;
top: 0;
left: 300px;
}
</style>
  • polygon 樣式用來設定多邊形的顏色和透明度。
  • circle 樣式用來設定圓形的填充和邊框。
  • text 樣式用來設定文本的字體大小和顏色。
  • label 樣式用來設定標籤的顯示方式。
  • #raw 樣式用來設定顯示原始數據的位置和樣式。

AxisLabel.vue

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

const props = defineProps({
stat: Object,
index: Number,
total: Number
})

const point = computed(() =>
valueToPoint(+props.stat.value + 10, props.index, props.total)
)
</script>

<template>
<text :x="point.x" :y="point.y">{{stat.label}}</text>
</template>

<script setup>

import { computed } from 'vue'
import { valueToPoint } from './util.js'
  • 從 Vue 中引入 computed 函數,用來創建計算屬性。
  • util.js 文件中引入 valueToPoint 函數,用來將值轉換為座標點。
const props = defineProps({
stat: Object,
index: Number,
total: Number
})

使用 defineProps 定義組件的 props,包括:

  • stat:一個物件,表示統計數據。
  • index:一個數字,表示當前統計數據在列表中的索引。
  • total:一個數字,表示統計數據的總數。
const point = computed(() =>
valueToPoint(+props.stat.value + 10, props.index, props.total)
)
  • 使用 computed 函數創建計算屬性 point
  • valueToPoint 函數接收三個參數:
    • +props.stat.value + 10:將統計數據的值轉換為數字並加上 10。
    • props.index:當前統計數據的索引。
    • props.total:統計數據的總數。
  • point 是一個響應性物件,包含計算出來的 xy 座標。

<template>

<template>
<text :x="point.x" :y="point.y">{{props.stat.label}}</text>
</template>
  • 使用 <text> 元素在 SVG 中顯示文本。
  • :x="point.x":綁定 x 屬性到計算屬性 pointx 值。
  • :y="point.y":綁定 y 屬性到計算屬性 pointy 值。
  • {{props.stat.label}}:顯示 props.stat 中的標籤文本。

PolyGraph.vue

<script setup>
import AxisLabel from './AxisLabel.vue'
import { computed } from 'vue'
import { valueToPoint } from './util.js'

const props = defineProps({
stats: Array
})

const points = computed(() => {
const total = props.stats.length
return props.stats
.map((stat, i) => {
const { x, y } = valueToPoint(stat.value, i, total)
return `${x},${y}`
})
.join(' ')
})
</script>

<template>
<g>
<polygon :points="points"></polygon>
<circle cx="100" cy="100" r="80"></circle>
<axis-label
v-for="(stat, index) in stats"
:stat="stat"
:index="index"
:total="stats.length"
>
</axis-label>
</g>
</template>

<style>
polygon {
fill: #42b983;
opacity: 0.75;
}

circle {
fill: transparent;
stroke: #999;
}
</style>

<script setup>

import AxisLabel from './AxisLabel.vue'
import { computed } from 'vue'
import { valueToPoint } from './util.js'
  • 從當前目錄中的 AxisLabel.vue 文件中引入 AxisLabel 組件。
  • 從 Vue 中引入 computed 函數,用來創建計算屬性。
  • 從當前目錄中的 util.js 文件中引入 valueToPoint 函數,用來將值轉換為座標點。
const props = defineProps({
stats: Array
})

使用 defineProps 定義組件的 props,包括:

  • stats:一個陣列,表示統計數據。
const points = computed(() => {
const total = props.stats.length
return props.stats
.map((stat, i) => {
const { x, y } = valueToPoint(stat.value, i, total)
return `${x},${y}`
})
.join(' ')
})
  • 使用 computed 函數創建計算屬性 points
  • total 變數計算 stats 陣列的長度,表示統計數據的總數。
  • props.stats.map 遍歷 stats 陣列,對每個 stat 和其索引 i 進行以下操作:
    • 使用 valueToPoint 函數計算出 stat.value 對應的 xy 座標。
    • 返回格式為 ${x},${y} 的字串。
  • join(' ') 方法將所有座標字串用空格連接起來,形成 SVG polygon 元素需要的 points 屬性值。

<template>

<template>
<g>
<polygon :points="points"></polygon>
<circle cx="100" cy="100" r="80"></circle>
<axis-label
v-for="(stat, index) in stats"
:stat="stat"
:index="index"
:total="stats.length"
>
</axis-label>
</g>
</template>


SVG 元素群組 <g>

  • 使用 <g> 標籤將 SVG 元素分組,這是一個容器元素,用來組織其他 SVG 元素。

多邊形 <polygon>

  • 使用 <polygon> 元素繪製多邊形。
  • :points="points" 綁定 points 計算屬性作為多邊形的頂點座標。

圓形 <circle>

  • 使用 <circle> 元素繪製一個圓。
  • cx="100"cy="100" 設置圓心的 x 和 y 座標。
  • r="80" 設置圓的半徑。

自定義標籤組件 <axis-label>

  • 使用 <axis-label> 元素來顯示每個統計數據的標籤。
  • 使用 v-for 指令遍歷 stats 陣列,為每個 stat 創建一個 axis-label 元素。
  • :stat="stat" 綁定當前的 stat
  • :index="index" 綁定當前的索引。
  • :total="stats.length" 綁定統計數據的總數。
<style>
polygon {
fill: #42b983;
opacity: 0.75;
}

circle {
fill: transparent;
stroke: #999;
}
</style>
style部分,我從App.vue搬到子組件裡頭,因為我App.vue使用style scoped的關係唷!為什麼會使用呢?就是因為全域的style又會影響到其他祖先組件的內容了

util.js

export function valueToPoint(value, index, total) {
const x = 0
const y = -value * 0.8
const angle = ((Math.PI * 2) / total) * index
const cos = Math.cos(angle)
const sin = Math.sin(angle)
const tx = x * cos - y * sin + 100
const ty = x * sin + y * cos + 100
return {
x: tx,
y: ty
}
}

這個函數 valueToPoint 用來將某個值轉換成在圓形坐標系中的點,常用於繪製雷達圖或其他基於極坐標的圖形。下面是逐行解釋:

  • export function valueToPoint:定義並導出名為 valueToPoint 的函數。
  • value, index, total:這個函數接受三個參數:
    • value:要轉換的值。
    • index:當前點在多邊形中的索引。
    • total:多邊形的頂點總數。
  • const x = 0:定義一個 x 變量並初始化為 0。這表示在極坐標系中,所有點的初始 x 坐標都在 y 軸上。
  • const y = -value * 0.8:定義一個 y 變量並將其初始化為 -value * 0.8。這裡的 value 是要轉換的值,乘以 -0.8 是為了縮放並翻轉 y 坐標,使得值越大,點越遠離圓心。
  • const angle = ((Math.PI * 2) / total) * index:計算當前點的角度。
    • Math.PI * 2:表示完整的圓周(360度)。
    • / total:將圓周分成等份,每份的角度。
    • * index:將角度乘以當前點的索引,確定當前點在圓周上的位置。
  • const cos = Math.cos(angle):計算該角度的餘弦值,用於旋轉變換。
  • const sin = Math.sin(angle):計算該角度的正弦值,用於旋轉變換。
  • const tx = x * cos - y * sin + 100:計算變換後的 x 坐標,並將其平移 100 個單位,使得點在畫布中心(100, 100)周圍旋轉。
    • x * cos:x 坐標旋轉後的新 x 值。
    • - y * sin:y 坐標旋轉後的新 x 值。
    • + 100:將 x 坐標平移 100 個單位,使其位於畫布中心。
  • const ty = x * sin + y * cos + 100:計算變換後的 y 坐標,並將其平移 100 個單位。
    • x * sin:x 坐標旋轉後的新 y 值。
    • + y * cos:y 坐標旋轉後的新 y 值。
    • + 100:將 y 坐標平移 100 個單位,使其位於畫布中心。
  • return { x: tx, y: ty }:返回一個包含變換後 x 和 y 坐標的物件,表示在圓形坐標系中的點。
raw-image
這題的CSS,我有改過一些~有興趣可以再看一下github
讓我想到這個範例可以改一改拿來當作獵人的念能力分佈圖
2024連載再開!希望能活著看到結局~看到庫拉皮卡下船www
avatar-img
2會員
71內容數
分享生活趣事~
留言0
查看全部
avatar-img
發表第一個留言支持創作者!
卡關的人生 的其他內容
Tree View 是一種適合於建立父子組件關係的結構,能夠展示多層級的數據,特別適合遞回渲染。這個組件允許用戶雙擊項目將其轉換為文件夾,並通過 TreeItem 組件遞歸渲染子項目。
在 Vue 3 中使用計算屬性來實現表格的排序和篩選功能。通過創建一個可重用的表格組件(DemoGrid),可以輕鬆管理和顯示外部數據。
這段代碼示範了如何從 GitHub 的 API 抓取最新的 Vue.js 提交數據並顯示在網頁上。這是後端 API 串接的一個範例,展示了如何動態地獲取和顯示數據。
看官方文件好累,終於要來看實用範例摟!這篇要做一個Markdown編輯器,真的假的?!
使用 defineAsyncComponent 函數可實現此功能,它接受一個返回 Promise 的加載函數。在大型應用中,組件可以按需加載,並且可與 ES 模塊的動態導入結合使用。還可以使用高級選項處理加載和錯誤狀態,例如設置加載組件和超時設定。
在 Vue 中,當需要將數據從父組件傳遞到深層嵌套的子組件時,使用 props 會導致屬性過度傳遞 (Prop Drilling),這樣即使某些中間組件不需要這些數據,也必須聲明並傳遞它們。這種情況會使代碼難以維護。Provide 和 Inject 是解決這個問題的工具。
Tree View 是一種適合於建立父子組件關係的結構,能夠展示多層級的數據,特別適合遞回渲染。這個組件允許用戶雙擊項目將其轉換為文件夾,並通過 TreeItem 組件遞歸渲染子項目。
在 Vue 3 中使用計算屬性來實現表格的排序和篩選功能。通過創建一個可重用的表格組件(DemoGrid),可以輕鬆管理和顯示外部數據。
這段代碼示範了如何從 GitHub 的 API 抓取最新的 Vue.js 提交數據並顯示在網頁上。這是後端 API 串接的一個範例,展示了如何動態地獲取和顯示數據。
看官方文件好累,終於要來看實用範例摟!這篇要做一個Markdown編輯器,真的假的?!
使用 defineAsyncComponent 函數可實現此功能,它接受一個返回 Promise 的加載函數。在大型應用中,組件可以按需加載,並且可與 ES 模塊的動態導入結合使用。還可以使用高級選項處理加載和錯誤狀態,例如設置加載組件和超時設定。
在 Vue 中,當需要將數據從父組件傳遞到深層嵌套的子組件時,使用 props 會導致屬性過度傳遞 (Prop Drilling),這樣即使某些中間組件不需要這些數據,也必須聲明並傳遞它們。這種情況會使代碼難以維護。Provide 和 Inject 是解決這個問題的工具。
你可能也想看
Google News 追蹤
Thumbnail
*合作聲明與警語: 本文係由國泰世華銀行邀稿。 證券服務係由國泰世華銀行辦理共同行銷證券經紀開戶業務,定期定額(股)服務由國泰綜合證券提供。   剛出社會的時候,很常在各種 Podcast 或 YouTube 甚至是在朋友間聊天,都會聽到各種市場動態、理財話題,像是:聯準會降息或是近期哪些科
Thumbnail
如何在 Vite 專案中安裝和設置 TypeScript 及路徑別名的步驟,包括安裝必要的依賴、配置 vite.config.js、tsconfig.json 的設置,及如何創建類型聲明文件來正確識別 .vue 文件。
Thumbnail
浮動(float)是早期用來創建佈局的技術。元素可以向左或向右浮動,旁邊的元素會環繞浮動元素。浮動元素通常用於圖文混排或簡單的兩欄佈局。
Thumbnail
各位使用 Vue.js 開發的小夥伴們,你們都怎麼實作父子層組件資料的雙向綁定呢?如果你還在寫 prop + emit 的話,不妨進來看看吧。
Thumbnail
一般在使用 TypeScript 的時候,大家都有遇過定義列舉資料的情境吧。 不過不管是 enum 和 literal 的方式其實都有些小缺點,以下推薦一個個人認為體驗更好的方式。
Thumbnail
切換頁面卡卡有很多種原因,這裡舉的例子只針對元件太大的情境。 除了想辦法拆分外,還有一個方法就是利用 Vue 的 Async Component。
Thumbnail
※ 視圖模板 視圖模板(View Templates) 是在 MVC 架構中負責展示數據的 HTML 文件,包含模板語法,用於在渲染時插入實際數據。它們的主要目的是分離數據與展示邏輯,讓代碼更加模塊化和易於維護。 視圖模板設計和使用的核心理念,就是「重複的事情不要重複做、效益最大化、有效利用資源
VIPER(View Interactor Presenter Entities Router) View 負責顯示資料。 Interactor 負責管理model。 Presenter 負責處理View的業務邏輯。 Entities 負責data model。
Thumbnail
因為最近想嘗試編碼風格,於是就選了一套比較"不嚴格"的輔助工具來摸索。 編輯器 VS CODE 框架 VUE3 打包工具 VITE 編碼風格 Standard 環境 version { "nodejs":"v18.18.0", "npm":"9.8.1" }
Thumbnail
平常我們在 html 上常看到的例如 v-for、v-model 等等... 也是VUE已經幫我們定義好的指令,而這次我們可以依這自己的需求來建立。 此功能屬於較進階的功能,因此實戰中會比較少見,市面上還是有不少完善的套件能達到同樣效果,建議可以先往這方面察找
Thumbnail
*合作聲明與警語: 本文係由國泰世華銀行邀稿。 證券服務係由國泰世華銀行辦理共同行銷證券經紀開戶業務,定期定額(股)服務由國泰綜合證券提供。   剛出社會的時候,很常在各種 Podcast 或 YouTube 甚至是在朋友間聊天,都會聽到各種市場動態、理財話題,像是:聯準會降息或是近期哪些科
Thumbnail
如何在 Vite 專案中安裝和設置 TypeScript 及路徑別名的步驟,包括安裝必要的依賴、配置 vite.config.js、tsconfig.json 的設置,及如何創建類型聲明文件來正確識別 .vue 文件。
Thumbnail
浮動(float)是早期用來創建佈局的技術。元素可以向左或向右浮動,旁邊的元素會環繞浮動元素。浮動元素通常用於圖文混排或簡單的兩欄佈局。
Thumbnail
各位使用 Vue.js 開發的小夥伴們,你們都怎麼實作父子層組件資料的雙向綁定呢?如果你還在寫 prop + emit 的話,不妨進來看看吧。
Thumbnail
一般在使用 TypeScript 的時候,大家都有遇過定義列舉資料的情境吧。 不過不管是 enum 和 literal 的方式其實都有些小缺點,以下推薦一個個人認為體驗更好的方式。
Thumbnail
切換頁面卡卡有很多種原因,這裡舉的例子只針對元件太大的情境。 除了想辦法拆分外,還有一個方法就是利用 Vue 的 Async Component。
Thumbnail
※ 視圖模板 視圖模板(View Templates) 是在 MVC 架構中負責展示數據的 HTML 文件,包含模板語法,用於在渲染時插入實際數據。它們的主要目的是分離數據與展示邏輯,讓代碼更加模塊化和易於維護。 視圖模板設計和使用的核心理念,就是「重複的事情不要重複做、效益最大化、有效利用資源
VIPER(View Interactor Presenter Entities Router) View 負責顯示資料。 Interactor 負責管理model。 Presenter 負責處理View的業務邏輯。 Entities 負責data model。
Thumbnail
因為最近想嘗試編碼風格,於是就選了一套比較"不嚴格"的輔助工具來摸索。 編輯器 VS CODE 框架 VUE3 打包工具 VITE 編碼風格 Standard 環境 version { "nodejs":"v18.18.0", "npm":"9.8.1" }
Thumbnail
平常我們在 html 上常看到的例如 v-for、v-model 等等... 也是VUE已經幫我們定義好的指令,而這次我們可以依這自己的需求來建立。 此功能屬於較進階的功能,因此實戰中會比較少見,市面上還是有不少完善的套件能達到同樣效果,建議可以先往這方面察找