有一个需求是这样的。
一个组件里若干个区块。区块数量不定。
区块里面是一个正六边形组件,而这个用 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 = { 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 = { 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 = { 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 { (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 { (total > oneRowStd) ? (base.width / floor) : (base.width / total) , height:basicHeight } } } } } theStyle = () => { const baseFlex = { display: 'flex', justifyContent: 'center', alignItems: 'center' } return { main:{ ...baseFlex, '100%', height:'100%', color:"#fff" }, tem:{ ...baseFlex, flex:"auto", color:'#fff' }, shellA:{ ...baseFlex, '100%', height:'100%' }, shellB:{ ...baseFlex, '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 来绑定。
以上。