- Linear Algebra with JavaScript: Animating Linear Transformations with ThreeJS
- Rodion Chachura
- The Nuggets translation Project
- Permanent link to this article: github.com/xitu/gold-m…
- Translator: lsvih
- Proofreader: LGH757079506, Stevens1995
JavaScript Linear Algebra: Animate linear transformations using ThreeJS
This article is part of the “JavaScript Linear Algebra” tutorial.
I recently completed an article on linear transformations using JavaScript and implemented 2D examples using an SVG grid. You can check out the previous post here. However, there is no example of three-dimensional space in that article, so this article will complete the deficiency of that article. You can view the GitHub repository for this article series here, and the commit associated with this article can be viewed here.
The target
In this article, we will make a component for visualizing linear transformations of objects in three dimensions. The end result is shown in the GIF below, or you can experience it on this page.
component
When we want to create 3D animations in the browser, the first thing that comes to mind is of course the three.js library. So let’s install it and another library that lets the user move the camera:
npm install --save three three-orbitcontrols
Copy the code
Let’s build a component that receives the matrix from the parent component’s properties and renders a transformation animation of the cube. The following code shows the structure of this component. We wrap this component using functions from the Gravitation-Components and React-Sizeme libraries to access color themes and detect component size changes.
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 = (a)= > {}
componentWillReceiveProps({ size: { width, height } }) {}
}
const WrappedScene = withTheme(withSize({ monitorHeight: true })(ThreeScene))
Copy the code
In the constructor, we initialize the state, including the size of the view. Therefore, when we receive a new status value, can be compared with the initial state in componentWillReceiveProps method. Since we need access to the actual DOM element to inject ThreeJS ‘renderer, we need to use the ref attribute in the render method:
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)} />
}
// ...
}
Copy the code
In the componentDidMount method, we initialize everything we need for the block transform animation. First, we create ThreeJS’s scene and position the camera. Then we create ThreeJS’s renderer, set its color and size, and add the renderer to the View component.
Next, create the objects you want to render: the axes, squares, and edges of the squares. Since we need to manually change the matrix, set the matrixAutoUpdate property of the squares and edges to false. Once you have created these objects, add them to the scene. To allow the user to move the camera position with the mouse, we also use OrbitControls.
The last thing to do is convert our library output matrix into ThreeJS format, and then get the function that returns the color and transformation matrix based on time. At componentWillUnmount, unanimate (that is, stop the Anime Frame) and remove the Renderer from the DOM.
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)
}
// ...
}
Copy the code
However, at this point we have not defined the animate function, so nothing will be rendered. First of all, we update on the edge of the cube and its transformation matrix, and update the color of the cube, then apply colours to a drawing and the window is called the requestAnimationFrame.
ComponentWillReceiveProps method receives the size of the current component, when it detects the component size changes, will update the status, change the size of the renderer, and adjust the orientation of the camera.
class ThreeScene extends React.Component {
// ...
animate = (a)= > {
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()
}
}
}
Copy the code
animation
To animate color changes and matrix transformations, write a function that returns the animation function. Before writing this function, we need to complete the following two converters: a function to convert our library matrix to a ThreeJS format matrix, and a function to convert RGB to HEX by referring to the code on StackOverflow:
import * as THREE from 'three'
import { Matrix } from 'linear-algebra/matrix'
export const toMatrix4 = matrix= > {
const matrix4 = newTHREE.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 newMatrix(... rows) }Copy the code
import * as THREE from 'three'
import { Matrix } from 'linear-algebra/matrix'
export const toMatrix4 = matrix= > {
const matrix4 = newTHREE.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 newMatrix(... rows) }Copy the code
color
First, the magnitude of each primary color (RGB) change needs to be calculated. The first call to getGetAnimatedColor returns a collection of new colors and timestamps; And when it is called later, the new RGB color of the current moment can be calculated according to the distance of color change and time consumption:
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 (a)= > {
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)})returnrgbToHex(... rgb) } }Copy the code
Linear transformation
To animate the linear transformation, we do the same thing we did in the previous section. We find the difference between before and after the matrix transformation first, and then in the animation function, according to the first call getGetAnimatedTransformation state, according to the time to update the state of each component:
export const getGetAnimatedTransformation = (fromMatrix, toMatrix, period) = > {
const distances = toMatrix.subtract(fromMatrix)
let start
return (a)= > {
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
}
}
Copy the code
If you find any mistakes in your translation or other areas that need to be improved, you are welcome to the Nuggets Translation Program to revise and PR your translation, and you can also get the corresponding reward points. The permanent link to this article at the beginning of this article is the MarkDown link to this article on GitHub.
The Nuggets Translation Project is a community that translates quality Internet technical articles from English sharing articles on nuggets. The content covers Android, iOS, front-end, back-end, blockchain, products, design, artificial intelligence and other fields. If you want to see more high-quality translation, please continue to pay attention to the Translation plan of Digging Gold, the official Weibo, Zhihu column.