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

本文是「JavaScript 線性代數」教程的一部分。javascript

最近我撰寫了這個線性代數系列的開篇之做。在新篇開始動筆前,我有了一個想法:使用 React 開發一個項目,來爲這個系列的全部示例提供可視化功能必定很好玩!本系列的全部代碼都存放於此 GitHub 倉庫,本文相關代碼的提交記錄位於此處前端

目標

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

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

建立 React 項目

其實已經有關於建立 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 中進行以下引用:android

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

項目結構

項目結構

本系列爲每一個示例都在 views 目錄中建立了各自的組件。咱們在 index.js 中導出一個以示例名稱爲鍵、以對應組件爲值的對象。ios

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 組件中導入該對象,並在菜單中展現出全部的鍵。當用戶經過菜單選擇示例後,更新組件狀態,並渲染新的 viewgit

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 軸正向朝下)中。github

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 庫提供的函數將這個組件包裝起來:npm

...
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,以及一個用於在網格上呈現內容(如後面的箭頭組件)的函數 renderGridContentjson

...
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前端後端區塊鏈產品設計人工智能等領域,想要查看更多優質譯文請持續關注 掘金翻譯計劃官方微博知乎專欄

相關文章
相關標籤/搜索