本文是“JavaScript 线性代数”教程的一部分。javascript
最近我完成了一篇关于使用 JavaScript 进行线性变换的文章,并用 SVG 网格实现了 2D 的示例。你能够在此处查看以前的文章。可是,那篇文章没有三维空间的示例,所以本文将补全那篇文章的缺失。你能够在此处查看本系列文章的 GitHub 仓库,与本文相关的 commit 能够在此处查看。前端
在本文中,咱们将制做一个组件,用于对三维空间的对象的线性变换进行可视化。最终效果以下面的动图所示,或者你也能够在此网页体验。java
当咱们要在浏览器中制做 3D 动画时,第一个想到的固然就是 three.js 库啦。因此让咱们来安装它以及另外一个可让用户移动摄像机的库:react
npm install --save three three-orbitcontrols
复制代码
下面构建一个组件,它能够由父组件的属性中接收矩阵,而且渲染一个立方体的转换动画。下面代码展现了这个组件的结构。咱们用 styled-components 和 react-sizeme 库中的函数对这个组件进行了包装,以访问颜色主题和检测组件尺寸的变化。android
import React from 'react'
import { withTheme } from 'styled-components'
import { withSize } from 'react-sizeme'
class ThreeScene extends React.Component {
constructor(props) {}
render() {}
componentDidMount() {}
componentWillUnmount() {}
animate = () => {}
componentWillReceiveProps({ size: { width, height } }) {}
}
const WrappedScene = withTheme(withSize({ monitorHeight: true })(ThreeScene))
复制代码
在构造函数中,咱们对状态进行了初始化,其中包括了视图的大小。所以,咱们当接收新的状态值时,能够在 componentWillReceiveProps 方法中与初始状态进行对比。因为须要访问实际的 DOM 元素以注入 ThreeJS 的 renderer,所以须要在 render 方法中用到 ref 属性:ios
const View = styled.div` width: 100%; height: 100%; `
class ThreeScene extends React.Component {
// ...
constructor(props) {
super(props)
this.state = {
width: 0,
height: 0
}
}
render() {
return <View ref={el => (this.view = el)} /> } // ... } 复制代码
在 componentDidMount 方法中,咱们对方块变换动画所须要的全部东西都进行了初始化。首先,咱们建立了 ThreeJS 的场景(scene)并肯定好摄像机(camera)的位置,而后咱们建立了 ThreeJS 的 renderer,为它设置好了颜色及大小,最后将 renderer 加入到 View 组件中。git
接下来建立须要进行渲染的对象:坐标轴、方块以及方块的边。因为咱们须要手动改变矩阵,所以将方块和边的 matrixAutoUpdate 属性设为 false。建立好这些对象后,将它们加入场景(scene)中。为了让用户能够经过鼠标来移动摄像机位置,咱们还用到了 OrbitControls。github
最后要作的,就是将咱们的库输出的矩阵转换成 ThreeJS 的格式,而后获取根据时间返回颜色和转换矩阵的函数。在 componentWillUnmount,取消动画(即中止 anime frame)并从 DOM 移除 renderer。npm
class ThreeScene extends React.Component {
// ...
componentDidMount() {
const {
size: { width, height },
matrix,
theme
} = this.props
this.setState({ width, height })
this.scene = new THREE.Scene()
this.camera = new THREE.PerspectiveCamera(100, width / height)
this.camera.position.set(1, 1, 4)
this.renderer = new THREE.WebGLRenderer({ antialias: true })
this.renderer.setClearColor(theme.color.background)
this.renderer.setSize(width, height)
this.view.appendChild(this.renderer.domElement)
const initialColor = theme.color.red
const axes = new THREE.AxesHelper(4)
const geometry = new THREE.BoxGeometry(1, 1, 1)
this.segments = new THREE.LineSegments(
new THREE.EdgesGeometry(geometry),
new THREE.LineBasicMaterial({ color: theme.color.mainText })
)
this.cube = new THREE.Mesh(
geometry,
new THREE.MeshBasicMaterial({ color: initialColor })
)
this.objects = [this.cube, this.segments]
this.objects.forEach(obj => (obj.matrixAutoUpdate = false))
this.scene.add(this.cube, axes, this.segments)
this.controls = new OrbitControls(this.camera)
this.getAnimatedColor = getGetAnimatedColor(
initialColor,
theme.color.blue,
PERIOD
)
const fromMatrix = fromMatrix4(this.cube.matrix)
const toMatrix = matrix.toDimension(4)
this.getAnimatedTransformation = getGetAnimatedTransformation(
fromMatrix,
toMatrix,
PERIOD
)
this.frameId = requestAnimationFrame(this.animate)
}
componentWillUnmount() {
cancelAnimationFrame(this.frameId)
this.view.removeChild(this.renderer.domElement)
}
// ...
}
复制代码
不过此时咱们尚未定义 animate 函数,所以什么也不会渲染。首先,咱们更新立方体及其边缘的转换矩阵,而且更新立方体的颜色,而后进行渲染而且调用 window.requestAnimationFrame
。后端
componentWillReceiveProps 方法将接收当前组件的大小,当它检测到组件尺寸发生了变化时,会更新状态,改变 renderer 的尺寸,并调整 camera 的方位。
class ThreeScene extends React.Component {
// ...
animate = () => {
const transformation = this.getAnimatedTransformation()
const matrix4 = toMatrix4(transformation)
this.cube.material.color.set(this.getAnimatedColor())
this.objects.forEach(obj => obj.matrix.set(...matrix4.toArray()))
this.renderer.render(this.scene, this.camera)
this.frameId = window.requestAnimationFrame(this.animate)
}
componentWillReceiveProps({ size: { width, height } }) {
if (this.state.width !== width || this.state.height !== height) {
this.setState({ width, height })
this.renderer.setSize(width, height)
this.camera.aspect = width / height
this.camera.updateProjectionMatrix()
}
}
}
复制代码
为了将颜色变化以及矩阵变换作成动画,须要写个函数来返回动画函数。在写这块函数前,咱们先要完成如下两种转换器:将咱们库的矩阵转换为 ThreeJS 格式矩阵的函数,以及参考 StackOverflow 上代码的将 RGB 转换为 hex 的函数:
import * as THREE from 'three'
import { Matrix } from 'linear-algebra/matrix'
export const toMatrix4 = matrix => {
const matrix4 = new THREE.Matrix4()
matrix4.set(...matrix.components())
return matrix4
}
export const fromMatrix4 = matrix4 => {
const components = matrix4.toArray()
const rows = new Array(4)
.fill(0)
.map((_, i) => components.slice(i * 4, (i + 1) * 4))
return new Matrix(...rows)
}
复制代码
import * as THREE from 'three'
import { Matrix } from 'linear-algebra/matrix'
export const toMatrix4 = matrix => {
const matrix4 = new THREE.Matrix4()
matrix4.set(...matrix.components())
return matrix4
}
export const fromMatrix4 = matrix4 => {
const components = matrix4.toArray()
const rows = new Array(4)
.fill(0)
.map((_, i) => components.slice(i * 4, (i + 1) * 4))
return new Matrix(...rows)
}
复制代码
首先,须要计算每种原色(RGB)变化的幅度。第一次调用 getGetAnimatedColor 时会返回新的色彩与时间戳的集合;并在后续被调用时,经过颜色变化的距离以及时间的耗费,能够计算出当前时刻新的 RGB 颜色:
import { hexToRgb, rgbToHex } from './generic'
export const getGetAnimatedColor = (fromColor, toColor, period) => {
const fromRgb = hexToRgb(fromColor)
const toRgb = hexToRgb(toColor)
const distances = fromRgb.map((fromPart, index) => {
const toPart = toRgb[index]
return fromPart <= toPart ? toPart - fromPart : 255 - fromPart + toPart
})
let start
return () => {
if (!start) {
start = Date.now()
}
const now = Date.now()
const timePassed = now - start
if (timePassed > period) return toColor
const animatedDistance = timePassed / period
const rgb = fromRgb.map((fromPart, index) => {
const distance = distances[index]
const step = distance * animatedDistance
return Math.round((fromPart + step) % 255)
})
return rgbToHex(...rgb)
}
}
复制代码
为了给线性变换作出动画效果,一样要进行上节的操做。咱们首先找到矩阵变换先后的区别,而后在动画函数中,根据第一次调用 getGetAnimatedTransformation 时的状态,根据时间来更新各个组件的状态:
export const getGetAnimatedTransformation = (fromMatrix, toMatrix, period) => {
const distances = toMatrix.subtract(fromMatrix)
let start
return () => {
if (!start) {
start = Date.now()
}
const now = Date.now()
const timePassed = now - start
if (timePassed > period) return toMatrix
const animatedDistance = timePassed / period
const newMatrix = fromMatrix.map((fromComponent, i, j) => {
const distance = distances.rows[i][j]
const step = distance * animatedDistance
return fromComponent + step
})
return newMatrix
}
}
复制代码
若是发现译文存在错误或其余须要改进的地方,欢迎到 掘金翻译计划 对译文进行修改并 PR,也可得到相应奖励积分。文章开头的 本文永久连接 即为本文在 GitHub 上的 MarkDown 连接。
掘金翻译计划 是一个翻译优质互联网技术文章的社区,文章来源为 掘金 上的英文分享文章。内容覆盖 Android、iOS、前端、后端、区块链、产品、设计、人工智能等领域,想要查看更多优质译文请持续关注 掘金翻译计划、官方微博、知乎专栏。