[譯] 用 React 製作線性代數教程示例:網格與箭頭

lsvih發表於2019-05-30

本文是“JavaScript 線性代數”教程的一部分。

最近我撰寫了這個線性代數系列的開篇之作。在新篇開始動筆前,我有了一個想法:使用 React 開發一個專案,來為這個系列的所有示例提供視覺化功能一定很好玩!本系列的所有程式碼都存放於此 GitHub 倉庫,本文相關程式碼的提交記錄位於此處

目標

在本系列剛開始寫作時,只有一個章節涉及了向量的基本運算。所以,目前實現一個能渲染二維座標網格以及能將向量視覺化為箭頭的元件就夠用了。本文最後做出的效果如下圖所示,你也可以在此處進行體驗。

二維空間中的基本向量運算

建立 React 專案

其實已經有關於建立 React 專案的最佳實踐指南文章可供參考,不過在本文中,我們將盡可能減少依賴的庫,並簡化對專案的配置。

create-react-app linear-algebra-demo
cd linear-algebra-demo
npm install --save react-sizeme styled-components
複製程式碼

上面的指令碼安裝了兩個庫。第一個庫 react-sizeme 可以實現當窗體大小發生變化時,重新渲染網格元件。第二個庫 styled-components 則能讓我們更輕鬆地編寫元件的樣式。此外,要用到我們正在開發的 linear-algebra 庫,需要在 package.json 中進行如下引用:

"dependencies": {
    "linear-algebra": "file:../library",
    ...
}
複製程式碼

專案結構

專案結構

本系列為每個示例都在 views 目錄中建立了各自的元件。我們在 index.js 中匯出一個以示例名稱為鍵、以對應元件為值的物件。

import { default as VectorLength } from './vector-length'
import { default as VectorScale } from './vector-scale'
import { default as VectorsAddition } from './vectors-addition'
import { default as VectorsSubtraction } from './vectors-subtraction'
import { default as VectorsDotProduct } from './vectors-dot-product'

export default {
  'vectors: addition': VectorsAddition,
  'vectors: subtraction': VectorsSubtraction,
  'vectors: length': VectorLength,
  'vectors: scale': VectorScale,
  'vectors: dot product': VectorsDotProduct
}
複製程式碼

接著在 Main 元件中匯入該物件,並在選單中展示出所有的鍵。當使用者通過選單選擇示例後,更新元件狀態,並渲染新的 view

import React from 'react'
import styled from 'styled-components'

import views from './views'
import MenuItem from './menu-item'

const Container = styled.div`
  ...
`

const Menu = styled.div`
  ...
`

class Main extends React.Component {
  constructor(props) {
    super(props)
    this.state = {
      view: Object.keys(views)[0]
    }
  }

  render() {
    const { view } = this.state
    const View = views[view]
    const viewsNames = Object.keys(views)
    const MenuItems = () =>
      viewsNames.map(name => (
        <MenuItem
          key={name}
          selected={name === view}
          text={name}
          onClick={() => this.setState({ view: name })}
        />
      ))
    return (
      <Container>
        <View />
        <Menu>
          <MenuItems />
        </Menu>
      </Container>
    )
  }
}

export default Main
複製程式碼

網格元件

為了在之後的示例中渲染向量和其它內容,我們設計了一個功能強大的元件,這個元件需要有這麼一種投影功能:將我們熟知的直角座標系(原點在中間,y 軸正向朝上)投影到 SVG 座標系(原點在左上角,y 軸正向朝下)中。

this.props.updateProject(vector => {
  // 在 vector 類中沒有任何用於縮放的方法,因此在這裡進行計算:
  const scaled = vector.scaleBy(step)
  const withNegatedY = new Vector(
    scaled.components[0],
    -scaled.components[1]
  )
  const middle = getSide(size) / 2
  return withNegatedY.add(new Vector(middle, middle))
})
複製程式碼

為了捕獲到網格元件容器的大小變動,我們使用 react-size 庫提供的函式將這個元件包裝起來:

...
import { withSize } from 'react-sizeme'
...

class Grid extends React.Component {
  updateProject = (size, cells) => {
    const step = getStepLen(size, cells)
    this.props.updateProject(() => /...)
  }

  componentWillReceiveProps({ size, cells }) {
    if (this.props.updateProject) {
      const newStepLen = getStepLen(size, cells)
      const oldStepLen = getStepLen(this.props.size, cells)
      if (newStepLen !== oldStepLen) {
        this.updateProject(size, cells)
      }
    }
  }

  componentDidMount() {
    if (this.props.updateProject) {
      this.updateProject(this.props.size, this.props.cells)
    }
  }
}

export default withSize({ monitorHeight: true })(Grid)
複製程式碼

為了便於在不同的示例中使用這個網格元件,我們編寫了一個 GridExample 元件,它可以接收兩個引數:一個用於渲染資訊(例如向量的名稱)的函式 renderInformation,以及一個用於在網格上呈現內容(如後面的箭頭元件)的函式 renderGridContent

...
import Grid from './grid'
...
class Main extends React.Component {
  constructor(props) {
    super(props)
    this.state = {
      project: undefined
    }
  }
  render() {
    const { project } = this.state
    const { renderInformation, renderGridContent } = this.props
    const Content = () => {
      if (project && renderGridContent) {
        return renderGridContent({ project })
      }
      return null
    }
    const Information = () => {
      if (renderInformation) {
        return renderInformation()
      }
      return null
    }
    return (
      <Container>
        <Grid cells={10} updateProject={project => this.setState({ project })}>
          <Content />
        </Grid>
        <InfoContainer>
          <Information />
        </InfoContainer>
      </Container>
    )
  }
}

export default Main
複製程式碼

這樣就能在 view 中使用這個元件了。下面以向量的加法為例測試一下:

import React from 'react'
import { withTheme } from 'styled-components'
import { Vector } from 'linear-algebra/vector'

import GridExample from '../grid-example'
import Arrow from '../arrow'
import VectorView from '../vector'

const VectorsAddition = ({ theme }) => {
  const one = new Vector(0, 5)
  const other = new Vector(6, 2)
  const oneName = 'v⃗'
  const otherName = 'w⃗'
  const oneColor = theme.color.green
  const otherColor = theme.color.red
  const sum = one.add(other)
  const sumColor = theme.color.blue
  const sumText = `${oneName} + ${otherName}`

  const renderInformation = () => (
    <>
      <VectorView components={one.components} name={oneName} color={oneColor} />
      <VectorView
        components={other.components}
        name={otherName}
        color={otherColor}
      />
      <VectorView components={sum.components} name={sumText} color={sumColor} />
    </>
  )
  const renderGridContent = ({ project }) => (
    <>
      <Arrow project={project} vector={one} text={oneName} color={oneColor} />
      <Arrow
        project={project}
        vector={other}
        text={otherName}
        color={otherColor}
      />
      <Arrow project={project} vector={sum} text={sumText} color={sumColor} />
    </>
  )
  const props = { renderInformation, renderGridContent }

  return <GridExample {...props} />
}

export default withTheme(VectorsAddition)

複製程式碼

箭頭元件

箭頭元件由 3 個 SVG 元素組成:line 用於顯示箭頭的線、polygon 用於顯示箭頭的頭、text 用於顯示向量名稱。此外,我們需要接收 project 函式,用於將箭頭放在網格中正確的位置上。

import React from 'react'
import styled from 'styled-components'
import { Vector } from 'linear-algebra/vector'

const Arrow = styled.line`
  stroke-width: 2px;
  stroke: ${p => p.color};
`

const Head = styled.polygon`
  fill: ${p => p.color};
`

const Text = styled.text`
  font-size: 24px;
  fill: ${p => p.color};
`

export default ({ vector, text, color, project }) => {
  const direction = vector.normalize()

  const headStart = direction.scaleBy(vector.length() - 0.6)
  const headSide = new Vector(
    direction.components[1],
    -direction.components[0]
  ).scaleBy(0.2)
  const headPoints = [
    headStart.add(headSide),
    headStart.subtract(headSide),
    vector
  ]
    .map(project)
    .map(v => v.components)

  const projectedStart = project(new Vector(0, 0))
  const projectedEnd = project(vector)

  const PositionedText = () => {
    if (!text) return null
    const { components } = project(vector.withLength(vector.length() + 0.2))
    return (
      <Text color={color} x={components[0]} y={components[1]}>
        {text}
      </Text>
    )
  }
  return (
    <g>
      <Arrow
        color={color}
        x1={projectedStart.components[0]}
        y1={projectedStart.components[1]}
        x2={projectedEnd.components[0]}
        y2={projectedEnd.components[1]}
      />
      <Head color={color} points={headPoints} />
      <PositionedText />
    </g>
  )
}
複製程式碼

通過結合 ReactSVG 可以做更多有意思的事。在本系列的後面章節中,我們會給這個視覺化示例新增更多的功能。最後推薦另一篇類似的文章:使用 ReactSVG 製作複雜的條形圖

如果發現譯文存在錯誤或其他需要改進的地方,歡迎到 掘金翻譯計劃 對譯文進行修改並 PR,也可獲得相應獎勵積分。文章開頭的 本文永久連結 即為本文在 GitHub 上的 MarkDown 連結。


掘金翻譯計劃 是一個翻譯優質網際網路技術文章的社群,文章來源為 掘金 上的英文分享文章。內容覆蓋 AndroidiOS前端後端區塊鏈產品設計人工智慧等領域,想要檢視更多優質譯文請持續關注 掘金翻譯計劃官方微博知乎專欄

相關文章