react實戰 : react 與 canvas

有一個需求是這樣的。html

一個組件裏若干個區塊。區塊數量不定。react

區塊裏面是一個正六邊形組件,而這個用 SVG 和 canvas 均可以。我選擇 canvas。web

因此就變成了在 react 中使用 canvas 的問題。shell

 

canvas 和 SVG 有一個很大的不一樣。canvas

SVG 是標籤,因此HTML怎麼整,SVG 就怎麼整。數組

而 canvas 是一套相對獨立的 web API,以 canvas 標籤爲容器(HTML接口)。less

因此在 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 來綁定。

 

以上。

相關文章
相關標籤/搜索