預覽

眾所周知 Cax 既能開發游戲、又能開發圖表。本文將從餅圖開始 Wechart 的圖表之旅。
Wechart 完全基於 Group 體系構建(自定義 Element) ,易維護,可擴展,任何場景可插拔使用。
快速開始
創建餅圖實例:
const pie = new Pie([
{ name: 'WeChat', value: 10 },
{ name: 'Canvas', value: 15 },
{ name: 'Cax', value: 23 },
{ name: 'Tencent', value: 7 },
{ name: 'Wepay', value: 22 }
], {
processing: (item) => {
return item.value
},
x: 200,
y: 200,
r: 160,
circleColor: 'white',
textOffsetY: -12,
font: '20px Arial',
color: (index) => {
return ['#4BC0C0', '#FF6485', '#FFA07A', '#ADACB9', '#A37AC1'][index]
},
label: (item) => {
return item.name
},
tooltip: (item) => {
return item.name + '<br/>' + item.value
}
}
)
上面各項配置項很清晰明了,不做解釋,開發者可自行修改參數看餅圖的變化,下面把餅圖添加到舞台:
const stage = new cax.Stage(640, 400, 'body')
stage.add(pie)
stage.update()
stage 是最大的容器,通過 add 方法往里面加對象,然后 update 舞台就能顯示。
顯示和隱藏餅圖:
pie.show()
pie.hide()
實現原理
看到上面的 DEMO 可以會有幾方面技術需要講解:
- Pie 對象和 Group 的關系
- Cax 扇形繪制
- 展開和收縮動畫實現
- 文字和文字走線顯示在對應扇形的中間
- 顯示兼容 PC 和 Mobile
- 交互兼容 PC 和 Mobile
- 漸變和點擊彈出和移除收縮實現
- Tooltip 實現
Pie 對象和 Group 的關系
先看 cax 內置的 Group 對象, Group 用於分組, group 也可以嵌套 group,父容器的屬性會疊加在子屬性上, 比如:
- group 的 x 是 100, group 里的 bitmap 的 x 是 200, 最后 bitmap 渲染到 stage 上的 x 是 300
- group 的 alpha 是 0.7, group 里的 bitmap 的 alpha 是 0.6, 最后 bitmap 渲染到 stage 上的 alpha 是 0.42
const group = new cax.Group()
const rect = new cax.Rect(100, 100 {
fillStyle: 'black'
})
group.add(rect)
stage.add(group)
stage.update()
Pie 對象正是自定義 Element,繼承自 Group:
class Pie extends Group {
constructor (data, option) {
super()
一般情況下,稍微復雜組合體都建議使用繼承自 Group,這樣利於擴展也方便管理自身內部的元件。
可以看到小游戲的 DEMO 里的 Player、Bullet、Enemy、Background 全都是繼承自 Group。
扇形繪制
Cax 內置 Graphics,可以使用連綴 Canvas API 的方式繪制圖形:
const sector = new cax.Graphics()
sector
.beginPath()
.moveTo(0, 0)
.arc(0, 0, 30, 0, Math.PI/2)
.closePath()
.fillStyle('green')
.fill()
.strokeStyle('red')
.lineWidth(2)
.stroke()
stage.add(sector)
這里假設你已經創建好了舞台。效果如下:

所以一個餅圖就是把圓分成若干個扇形。怎么分? arc 方法傳入動態數據:
let current = 0
data.forEach((item, index) => {
const sector = new cax.Graphics()
sector
.beginPath()
.moveTo(0, 0)
.arc(0, 0, 30, current, current += Math.PI * 2 * item.value / totalValue)
.closePath()
.fillStyle('green')
.fill()
.strokeStyle('red')
.lineWidth(2)
.stroke()
})
其中 totalValue 為所有 item.value 的和。可以看到上面是平分一個圓。那么怎么平分一個扇形?能運動平分的角度嗎?
展開和收縮動畫實現
看這行代碼:
.arc(0, 0, 30, current, current += Math.PI * 2 * item.value / totalValue)
把 Math.PI * 2 改成 totalAngle 動態變量就可以!
let totalAngle = 0
...
...
.arc(0, 0, 30, current, current += totalAngle * item.value / totalValue)
運動 totalAngle 並且進行重繪:
cax.To.get(option)
.to({ totalAngle: Math.PI * 2 }, option.duration, option.easing)
.progress((object) => {
current = option.begin
sectorGroup.forEach((item, index) => {
item
.clear()
.beginPath()
.moveTo(0, 0)
.arc(0, 0, r, current, current += object.totalAngle * option.processing(item) / totalValue)
.closePath()
.fillStyle(option.color(index))
.fill()
.strokeStyle(option.circleColor)
.lineWidth(2)
.stroke()
.closePath()
})
})
...
...
...
使用 cax 內置的 to2to 運動能力。這里需要提醒的是,progress 方法會不斷地執行,為了防止 sector 的 graphics path 不斷疊加,在循環執行的代碼里一定要調用 clear 來清除 graphics 的以前的 Canvas 繪制命令。
文字和文字走線
文字和走線分四種情況:

if (angle >= 0 && angle < Math.PI / 2) {
} else if (angle >= Math.PI / 2 && angle < Math.PI) {
} else if (angle >= Math.PI && angle < Math.PI + Math.PI / 2) {
} else
}
需要注意的是:
- 落在左邊的文字的 x 坐標需要減去文件的寬度。 Cax 內置的 Text 可以使用 getWidth() 方法獲取到文字的寬度
- 走線的第一根線角度也分兩種情況,1、3象限平行,2、4象限平行,走線的第二根先角度都是平行於 y 軸(如上圖所示,相同顏色圈中的線是平行的)
顯示與交互兼容 PC 和 Mobile
從 javascript 里會發現 canvas 的寬高是 640*400:
const stage = new cax.Stage(640, 400, 'body')
就和我們平時使用兩倍圖一樣,在移動端通過 media 把 canvas 變成一半寬度:
@media screen and (max-width: 500px) {
canvas {
width : 320px
}
}
這個時候會出現一個問題!因為 cax 會把 canvas 上的事件過度給 cax 內置對象,事件發生的坐標因為 canvas 寬高的變化而變化了, 移動端點擊事件觸發位置不准確了!這個時候需要 scaleEventPoint 方法來校正坐標:
if (window.innerWidth <= 500) {
stage.scaleEventPoint(0.5, 0.5)
}
搞定!這樣不管是在 PC 鼠標還是移動 Mobile 觸摸都能精准觸發事件。
漸變和點擊彈出和移除收縮實現
function fadeIn(obj) {
obj.alpha = 0
To.get(obj).to({ alpha: 1 }, 600).start()
}
function fadeOut(obj) {
obj.alpha = 1
To.get(obj).to({ alpha: 0 }, 600).start()
}
function bounceIn(obj, from, to) {
from = from || 0
obj.from = from
To.get(obj).to({ scaleX: to || 1, scaleY: to || 1 }, 300, cax.easing.bounceOut).start()
}
function bounceOut(obj, from, to) {
from = from || 1
obj.from = from
To.get(obj).to({ scaleX: to || 0, scaleY: to || 0 }, 300, cax.easing.bounceOut).start()
}
基於 cax 內置的 to2to 動畫引擎封裝了四個方法。
Tooltip 實現
sector.hover(function (evt) {
bounceIn(sector, 1, 1.1)
tooltip.style.left = (evt.pureEvent.pageX + 5) + 'px'
tooltip.style.top = (evt.pureEvent.pageY + 5) + 'px'
tooltip.innerHTML = option.tooltip(data[index])
tooltip.style.display = 'block'
}, function (evt) {
bounceOut(sector, 1.1, 1)
tooltip.style.display = 'none'
}, function (evt) {
tooltip.style.left = (evt.pureEvent.pageX + 5) + 'px'
tooltip.style.top = (evt.pureEvent.pageY + 5) + 'px'
})
Cax 內置對象擁有 hover(over, out, move) 方法來監聽鼠標或者手指 over、out 和 move。
Tooltip 也是完全基於 DOM 來實現的,這樣可以浮在 Canvas 外面,而不會限制在 Canvas 里面。
Star && Follower
誰在使用?

License
MIT
