哇!開始用SVG畫圖了~越來越實用了~
設計一些實用的可縮放向量圖形~
應該可以做一些簡單的動畫吧!
這次有四個檔案,App.vue、AxisLabel.vue、PolyGraph.vue、util.js
<!--
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
是我們將用於繪製多邊形圖的子組件。ref
和 reactive
是 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
樣式用來設定顯示原始數據的位置和樣式。<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'
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
是一個響應性物件,包含計算出來的 x
和 y
座標。<template>
<template>
<text :x="point.x" :y="point.y">{{props.stat.label}}</text>
</template>
<text>
元素在 SVG 中顯示文本。:x="point.x"
:綁定 x
屬性到計算屬性 point
的 x
值。:y="point.y"
:綁定 y
屬性到計算屬性 point
的 y
值。{{props.stat.label}}
:顯示 props.stat
中的標籤文本。<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
組件。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
對應的 x
和 y
座標。${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>
<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又會影響到其他祖先組件的內容了
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 坐標的物件,表示在圓形坐標系中的點。這題的CSS,我有改過一些~有興趣可以再看一下github摟
讓我想到這個範例可以改一改拿來當作獵人的念能力分佈圖
2024連載再開!希望能活著看到結局~看到庫拉皮卡下船www