有一個需求是這樣的。
一個組件里若干個區塊。區塊數量不定。
區塊里面是一個正六邊形組件,而這個用 SVG 和 canvas 都可以。我選擇 canvas。
所以就變成了在 react 中使用 canvas 的問題。
canvas 和 SVG 有一個很大的不同。
SVG 是標簽,所以HTML怎么整,SVG 就怎么整。
而 canvas 是一套相對獨立的 web API,以 canvas 標簽為容器(HTML接口)。
所以在 react 中處理 canvas 類似於在 react 中處理第三方DOM庫。比如那些需要依賴 jQuery 的各種UI組件庫。
關於這個可以看 react 文檔中的與第三方庫協同。
組件文件的結構和上一個文章類似。
import React from 'react' class Polygon extends React.Component {} class polygonContainer extends React.Component {} export default polygonContainer
然后是 canvas 組件。
class Polygon extends React.Component { constructor(props){ super(props) this.state = { } } componentDidMount() { console.log("=== componentDidMount Polygon ===") this.init(this.props.data, this.props.sn) } componentDidUpdate() { console.log("=== componentDidUpdate Polygon ===") this.init(this.props.data, this.props.sn) } init = (item, sn) => { const getR = () => { return Math.min(size.width, size.height) * 0.375 } const getWordCoor = (index, centerCoor, sub, fontSize, fontLength) => { const getXCoor = (index, centerCoor, fontSize, fontLength) => { const standand = -1 return (centerCoor.x + fontLength / 2 * (index === 0 ? fontSize : (fontSize / 2)) * standand) } const getYCoor = (index, centerCoor, sub) => { const standand = index === 0 ? -0.3 : 0.6 return (centerCoor.y + sub * standand) } console.log(getXCoor(index, centerCoor, fontSize, fontLength)) return { x: getXCoor(index, centerCoor, fontSize, fontLength), y: getYCoor(index, centerCoor, sub) } } const getStrokeColor = (sn) => { return sn === 5 ? 'rgb(255, 114, 0)' : 'rgb(232, 172, 4)' } const getFillColor = (sn) => { return sn === 5 ? 'rgb(255, 192, 0)' : 'rgb(4, 154, 79)' } const canvas = document.getElementById("canvas" + sn); const size = { width: parseInt(this.props.size.width), height: parseInt(this.props.size.height), } canvas.width = size.width; canvas.height = size.height; const cc = canvas.getContext("2d"); // 多邊形 const coorArray = [] cc.beginPath(); for (var i = 0 ; i < 6 ; i++) { var x = Math.cos((i * 60)/180 * Math.PI) * getR() + (size.width / 2) ; var y = -Math.sin((i * 60)/180 * Math.PI) * getR() + (size.height / 2); coorArray.push({x, y}) cc.lineTo(x,y); } cc.closePath(); cc.lineWidth = 2; cc.fillStyle = getFillColor(sn); cc.fill(); cc.strokeStyle = getStrokeColor(sn); cc.stroke(); // 文字 const centerCoor = { x: (coorArray[0].x + coorArray[3].x) / 2, y: coorArray[0].y } const sub = coorArray[0].y - coorArray[1].y const wordCoorArray = [ getWordCoor(0, centerCoor, sub, 14, item.name.length), getWordCoor(1, centerCoor, sub, 20, item.data.toString().length) ] cc.font="14px Arial" cc.strokeStyle = "#fff"; cc.fillStyle = "#fff"; cc.fillText(item.name, wordCoorArray[0].x, wordCoorArray[0].y); cc.font="20px Arial" cc.fillText(item.data, wordCoorArray[1].x, wordCoorArray[1].y); } render(){ const item = this.props.data const size = this.props.size const sn = this.props.sn const getColor = (item) => { return item.color } return ( <canvas id={'canvas' + sn}></canvas> ); } }
有幾點需要說明一下。
- 因為 componentDidUpdate 鈎子中 有 init 方法,所以 init 方法中不能再給 state 賦值,否則會觸發無限循環。如果需要存值,則需要想別的辦法。
-
getWordCoor 是計算文字位置的方法。六邊形里面有文字內容。
- canvas 對象是通過 document.getElementById 獲取的,而一個頁面中肯定有多個 canvas ,此時就必須做出區分。我的方法是傳一個序列號 sn (index + 1),當然生成 ID 是更好的做法。
- 響應式的樣式對 canvas 是無效的。必須手動賦像素值。也就是說必須手動計算 size 。計算 size 的方法在父組件里面。
- for 循環是用來繪制路徑的,就是個數學問題,Math 對象里有三角函數簡化了一些運算。順便把中心點坐標和六邊形各個點的坐標存了一下。
- canvas 繪制方法不需要說了,百度一下即可。
然后是容器組件。
// 六邊形測試 import React from 'react' // import Styles from './polygonContainer.less' class Polygon extends React.Component { constructor(props){ super(props) this.state = { } } componentDidMount() { console.log("=== componentDidMount Polygon ===") this.init(this.props.data, this.props.sn) } componentDidUpdate() { console.log("=== componentDidUpdate Polygon ===") this.init(this.props.data, this.props.sn) } init = (item, sn) => { // console.log(item) // console.log(sn) const getR = () => { return Math.min(size.width, size.height) * 0.375 } const getWordCoor = (index, centerCoor, sub, fontSize, fontLength) => { const getXCoor = (index, centerCoor, fontSize, fontLength) => { const standand = -1 return (centerCoor.x + fontLength / 2 * (index === 0 ? fontSize : (fontSize / 2)) * standand) } const getYCoor = (index, centerCoor, sub) => { const standand = index === 0 ? -0.3 : 0.6 return (centerCoor.y + sub * standand) } console.log(getXCoor(index, centerCoor, fontSize, fontLength)) return { x: getXCoor(index, centerCoor, fontSize, fontLength), y: getYCoor(index, centerCoor, sub) } } const getStrokeColor = (sn) => { return sn === 5 ? 'rgb(255, 114, 0)' : 'rgb(232, 172, 4)' } const getFillColor = (sn) => { return sn === 5 ? 'rgb(255, 192, 0)' : 'rgb(4, 154, 79)' } const canvas = document.getElementById("canvas" + sn); const size = { width: parseInt(this.props.size.width), height: parseInt(this.props.size.height), } // console.log(size) canvas.width = size.width; canvas.height = size.height; const cc = canvas.getContext("2d"); // 多邊形 const coorArray = [] cc.beginPath(); for (var i = 0 ; i < 6 ; i++) { var x = Math.cos((i * 60)/180 * Math.PI) * getR() + (size.width / 2) ; var y = -Math.sin((i * 60)/180 * Math.PI) * getR() + (size.height / 2); coorArray.push({x, y}) cc.lineTo(x,y); } cc.closePath(); cc.lineWidth = 2; cc.fillStyle = getFillColor(sn); cc.fill(); cc.strokeStyle = getStrokeColor(sn); cc.stroke(); // 文字 const centerCoor = { x: (coorArray[0].x + coorArray[3].x) / 2, y: coorArray[0].y } const sub = coorArray[0].y - coorArray[1].y // console.log(centerCoor) // console.log(coorArray) const wordCoorArray = [ getWordCoor(0, centerCoor, sub, 14, item.name.length), getWordCoor(1, centerCoor, sub, 20, item.data.toString().length) ] // console.log(wordCoorArray) cc.font="14px Arial" cc.strokeStyle = "#fff"; cc.fillStyle = "#fff"; cc.fillText(item.name, wordCoorArray[0].x, wordCoorArray[0].y); cc.font="20px Arial" cc.fillText(item.data, wordCoorArray[1].x, wordCoorArray[1].y); } render(){ const item = this.props.data const size = this.props.size const sn = this.props.sn // console.log("Polygon render === ", size) const getColor = (item) => { return item.color } return ( <canvas id={'canvas' + sn}></canvas> // <div>asd</div> ); } } class polygonContainer extends React.Component { constructor(props){ super(props) this.state = { curcity:"" } } componentDidMount() { console.log("componentDidMount") console.log(new Date().getTime()) this.setState({ curcity:this.props.curcity }) } componentDidUpdate(){ console.log("componentDidUpdate") console.log(new Date().getTime()) } // total 總數 SN 序列號 getSize = () => { const pc = document.getElementById('pc') if (!pc) { return null } else { // const length = this.getDataBar().data.sData.length const base = { width:document.getElementById('pc').offsetWidth, height:document.getElementById('pc').offsetHeight } return function (total, SN) { // console.log(base) const standand = 2 const oneRowStd = 3 const ceil = Math.ceil(total / standand) const floor = Math.floor(total / standand) const basicHeight = (total > oneRowStd) ? (base.height / standand) : (base.height) // console.log(ceil, floor) // console.log(total, SN) if (SN <= ceil) { return { width:(total > oneRowStd) ? (base.width / ceil) : (base.width / total), height:basicHeight } } else { // console.log(123) // console.log((total > oneRowStd) ? (base.width / floor) : (base.width / total)) return { width:(total > oneRowStd) ? (base.width / floor) : (base.width / total) , height:basicHeight } } } } } theStyle = () => { const baseFlex = { display: 'flex', justifyContent: 'center', alignItems: 'center' } return { main:{ ...baseFlex, width:'100%', height:'100%', color:"#fff" }, tem:{ ...baseFlex, flex:"auto", color:'#fff' }, shellA:{ ...baseFlex, width:'100%', height:'100%' }, shellB:{ ...baseFlex, width:'100%', height:'50%' } } } getDataBar = () => { if (this.props.curcity && this.props.curcity === 'all') { return { data:{ sData:[ { name: 'a', data: 510 }, { name: 'a', data: 46 }, { name: 'a', data: 471 }, { name: 'a', data: 631 }, { name: 'a', data: 924 }, { name: 'a', data: 582 }, ] } } } else { return { data:{ sData:[ { name: 'a', data: 50 }, { name: 'a', data: 469 }, { name: 'a', data: 41 }, { name: 'a', data: 31 }, { name: 'a', data: 4 }, { name: 'a', data: 825 }, ] } } } } getContainer = () => { const size = this.getSize() if (!size) { return "" } const theStyle = this.theStyle() const dataBar = this.getDataBar() const Container = ((dataBar) => { const flexMatrix = [ [0,0], [1,0], [2,0], [3,0], [2,2], [3,2], [3,3], [4,3], [4,4], [5,4], [5,5], [6,5], [6,6] ] const sData = dataBar.data.sData const length = sData.length const matrix = flexMatrix[length] ? flexMatrix[length] : flexMatrix[12] if (matrix[0] === 0) { return "" } let temShell, temA, temB temA = sData.slice(0, matrix[0]).map((item, index) => <div style={theStyle.tem} key={index.toString()}> <Polygon data={item} sn={index + 1} size={size(length, (index + 1))} /> </div> ); if (matrix[1] === 0) { temB = "" } else { temB = sData.slice(matrix[0], (matrix[0] + matrix[1])).map((item, index) => <div style={theStyle.tem} key={index.toString()}> <Polygon data={item} sn={index + 1 + matrix[0]} size={size(length, (index + 1 + matrix[0]))} /> </div> ); } if (matrix[1] === 0) { temShell = <div style={theStyle.shellA} > {temA} </div> } else { temShell = [0,0].map((item, index) => <div style={theStyle.shellB} key={"temShell" + index.toString()}> {index === 0 ? temA : temB} </div> ); document.getElementById('pc').style.flexWrap = "wrap" } return temShell })(dataBar) return Container } render(){ const theStyle = this.theStyle() const curcity = this.state.curcity // const dataBar = this.props.dataBar return ( <div style={theStyle.main} id="pc"> { this.getContainer() } </div> ); } } export default polygonContainer
稍微說明一下。
- getSize 是計算區塊大小的方法。這個方法返回一個 size 方法,在 getContainer 方法中輸出 JSX 的時候會調用 size 方法得到寬高。
- 關於布局的問題(為什么寫了個雙層數組?)之前的文章里寫過,不再贅述。
- 關於數據綁定的機制。通過 props 來綁定。
以上。