2024-10-10|閱讀時間 ‧ 約 0 分鐘

EP33 - ex5. SVG

哇!開始用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 坐標的物件,表示在圓形坐標系中的點。
這題的CSS,我有改過一些~有興趣可以再看一下github
讓我想到這個範例可以改一改拿來當作獵人的念能力分佈圖
2024連載再開!希望能活著看到結局~看到庫拉皮卡下船www
分享至
成為作者繼續創作的動力吧!
© 2024 vocus All rights reserved.