├── .gitignore
├── README.md
├── package-lock.json
├── package.json
├── public
└── index.html
└── src
├── actions
└── index.js
├── components
├── App.css
├── App.js
├── StartMessage.css
├── StartMessage.js
├── ThreeDisplay.css
└── ThreeDisplay.js
├── containers
├── StartPage.js
└── ThreeApp.js
├── index.js
├── reducers
└── rootReducer.js
└── threeApp
├── threeActionHelpers.js
├── threeApp.js
└── threeHelpers.js
/.gitignore:
--------------------------------------------------------------------------------
1 | # See https://help.github.com/ignore-files/ for more about ignoring files.
2 |
3 | # dependencies
4 | /node_modules
5 |
6 | # testing
7 | /coverage
8 |
9 | # production
10 | /build
11 |
12 | # misc
13 | .DS_Store
14 | .env.local
15 | .env.development.local
16 | .env.test.local
17 | .env.production.local
18 |
19 | npm-debug.log*
20 | yarn-debug.log*
21 | yarn-error.log*
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | ## three.js - React - Redux
2 |
3 | Simple toy app built with [three.js](https://github.com/mrdoob/three.js), [React](https://github.com/facebook/react) and [Redux](https://github.com/reactjs/redux).
4 |
5 | This ist just a little experiment to figure out how to use Redux for mananging state and handling events in a three.js-based real-time 3D application.
6 |
7 | All three.js objects (scene, camera, renderer) live inside the ThreeApp container. This is the only place where mutations and side effects are supposed to happen. The scene gets updated every frame by `mapStateToScene()`. Similar functions could be used for applying state to the camera or selected parts of the three.js scene.
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "react-three-test",
3 | "version": "0.1.0",
4 | "private": true,
5 | "dependencies": {
6 | "react": "^15.6.1",
7 | "react-dom": "^15.6.1",
8 | "react-redux": "^5.0.6"
9 | },
10 | "devDependencies": {
11 | "react-scripts": "1.0.11",
12 | "redux": "^3.7.2",
13 | "three": "^0.86.0"
14 | },
15 | "scripts": {
16 | "start": "react-scripts start",
17 | "build": "react-scripts build",
18 | "test": "react-scripts test --env=jsdom",
19 | "eject": "react-scripts eject"
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/public/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | React, Redux & Three.js Test
6 |
7 |
8 |
9 |
10 |
11 |
12 |
--------------------------------------------------------------------------------
/src/actions/index.js:
--------------------------------------------------------------------------------
1 | export const fadeColor = e => {
2 | return {
3 | type: 'FADE_COLOR',
4 | e,
5 | }
6 | }
7 |
8 | export const switchColor = () => {
9 | return {
10 | type: 'SWITCH_COLOR',
11 | }
12 | }
13 |
14 | export const run = () => {
15 | return {
16 | type: 'RUN',
17 | }
18 | }
19 |
20 | export const update = timestamp => {
21 | return {
22 | type: 'UPDATE',
23 | timestamp,
24 | }
25 | }
--------------------------------------------------------------------------------
/src/components/App.css:
--------------------------------------------------------------------------------
1 | body {
2 | margin: 0;
3 | overflow: hidden;
4 | }
5 |
6 |
--------------------------------------------------------------------------------
/src/components/App.js:
--------------------------------------------------------------------------------
1 | import './App.css'
2 |
3 | import React from 'react'
4 |
5 | import StartPage from '../containers/StartPage'
6 | import ThreeApp from '../containers/ThreeApp'
7 |
8 | const App = props => (
9 |
10 |
11 |
12 |
13 | )
14 |
15 | export default App
16 |
--------------------------------------------------------------------------------
/src/components/StartMessage.css:
--------------------------------------------------------------------------------
1 | .start-message {
2 | background-color: azure;
3 | color: skyblue;
4 | font-family: sans-serif;
5 | font-size: 2em;
6 | height: 100%;
7 | padding-top: 200px;
8 | position: absolute;
9 | text-align: center;
10 | width: 100%;
11 | }
12 |
13 | .hidden {
14 | visibility: hidden;
15 | }
--------------------------------------------------------------------------------
/src/components/StartMessage.js:
--------------------------------------------------------------------------------
1 | import './StartMessage.css'
2 |
3 | import React from 'react'
4 |
5 | const StartMessage = ({ running, run }) => (
6 |
7 |
11 | Click to start!
12 |
13 |
14 | )
15 |
16 | export default StartMessage
17 |
--------------------------------------------------------------------------------
/src/components/ThreeDisplay.css:
--------------------------------------------------------------------------------
1 | .three-display{
2 | height: 100%;
3 | margin: 0;
4 | width: 100%;
5 | }
6 |
7 | .hidden {
8 | visibility: hidden;
9 | }
--------------------------------------------------------------------------------
/src/components/ThreeDisplay.js:
--------------------------------------------------------------------------------
1 | import './ThreeDisplay.css'
2 |
3 | import React from 'react'
4 | import {connect} from 'react-redux'
5 |
6 | import {fadeColor, switchColor} from '../actions'
7 |
8 | class ThreeDisplay extends React.Component {
9 | shouldComponentUpdate(nextProps) {
10 | const shouldUpdate = nextProps.lastAction === 'RUN' || 'UPDATE'
11 |
12 | if (!shouldUpdate) {
13 | // This never gets logged, even if nextProps.lastAction is not 'RUN' or 'UPDATE'
14 | console.log('ThreeDisplay will not update')
15 | console.log('Last action: ' + nextProps.lastAction)
16 | }
17 |
18 | return shouldUpdate
19 | }
20 |
21 | componentWillUpdate() {
22 | // This gets logged even if this.props.lastAction is not 'RUN' or 'UPDATE
23 | console.log('ThreeDisplay will update')
24 | console.log('Last action: ' + this.props.lastAction)
25 | }
26 |
27 | render() {
28 | return (
29 |
39 | )
40 | }
41 | }
42 |
43 | const mapStateToProps = state => {
44 | return {
45 | running: state.running,
46 | lastAction: state.lastAction
47 | }
48 | }
49 |
50 | const mapDispatchTopProps = dispatch => {
51 | return {
52 | fadeColor: e => dispatch(fadeColor(e)),
53 | switchColor: () => dispatch(switchColor())
54 | }
55 | }
56 |
57 | export default connect(mapStateToProps, mapDispatchTopProps)(ThreeDisplay)
58 |
--------------------------------------------------------------------------------
/src/containers/StartPage.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import { connect } from 'react-redux'
3 |
4 | import { run } from '../actions'
5 | import StartMessage from '../components/StartMessage'
6 |
7 | const StartPage = ({ running, run }) => (
8 |
9 |
13 |
14 | )
15 |
16 | const mapStateToProps = (state) => {
17 | return {
18 | running: state.running,
19 | }
20 | }
21 |
22 | const mapDispatchTopProps = (dispatch) => {
23 | return {
24 | run: () => dispatch(run()),
25 | }
26 | }
27 |
28 | export default connect(
29 | mapStateToProps,
30 | mapDispatchTopProps
31 | )(StartPage)
32 |
33 |
--------------------------------------------------------------------------------
/src/containers/ThreeApp.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import {connect} from 'react-redux'
3 |
4 | import {update} from '../actions'
5 |
6 | import ThreeDisplay from '../components/ThreeDisplay'
7 |
8 | import {
9 | getThreeCamera,
10 | getThreeRenderer,
11 | getThreeScene
12 | } from '../threeApp/threeApp'
13 |
14 | import {mapStateToScene} from '../threeApp/threeHelpers'
15 |
16 | class ThreeApp extends React.Component {
17 | renderNextFrame = () => {
18 | this.threeRenderer.render(this.scene, this.camera)
19 | requestAnimationFrame(timestamp => this.props.update(timestamp))
20 | }
21 |
22 | componentDidMount() {
23 | this.scene = getThreeScene()
24 | this.camera = getThreeCamera()
25 | this.threeRenderer = getThreeRenderer()
26 |
27 | this.renderNextFrame()
28 | }
29 |
30 | shouldComponentUpdate(nextProps) {
31 | return nextProps.lastAction === 'UPDATE'
32 | }
33 |
34 | componentWillUpdate() {
35 | mapStateToScene(this.props.sceneState, this.scene)
36 | this.renderNextFrame()
37 | }
38 |
39 | render() {
40 | return (
41 |
42 |
43 |
44 | )
45 | }
46 | }
47 |
48 | const mapStateToProps = state => {
49 | return {
50 | timestamp: state.timestamp,
51 | lastAction: state.lastAction,
52 | sceneState: state.scene
53 | }
54 | }
55 |
56 | const mapDispatchTopProps = dispatch => {
57 | return {
58 | update: timestamp => dispatch(update(timestamp))
59 | }
60 | }
61 |
62 | export default connect(mapStateToProps, mapDispatchTopProps)(ThreeApp)
63 |
--------------------------------------------------------------------------------
/src/index.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import { render } from 'react-dom'
3 | import { Provider } from 'react-redux'
4 | import { createStore } from 'redux'
5 |
6 | import App from './components/App'
7 | import rootReducer from './reducers/rootReducer'
8 | import { getInitialSceneState } from './threeApp/threeApp'
9 |
10 | const threeInitialState = getInitialSceneState();
11 |
12 | const initialState = {
13 | running: false,
14 | timestamp: 0,
15 | lastAction: '',
16 | scene: threeInitialState
17 | }
18 |
19 | const store = createStore(rootReducer, initialState)
20 |
21 | render(
22 |
23 |
24 | ,
25 | document.getElementById('root')
26 | )
--------------------------------------------------------------------------------
/src/reducers/rootReducer.js:
--------------------------------------------------------------------------------
1 | import { switchColor, fadeColor, updatePosition } from '../threeApp/threeActionHelpers'
2 |
3 | const rootReducer = (state, action) => {
4 | switch (action.type) {
5 | case 'FADE_COLOR': {
6 | const newState = fadeColor(state, action.e)
7 | return {
8 | ...newState,
9 | lastAction: 'FADE_COLOR'
10 | }
11 | }
12 | case 'SWITCH_COLOR': {
13 | const newState = switchColor(state)
14 | return {
15 | ...newState,
16 | lastAction: 'SWITCH_COLOR'
17 | }
18 | }
19 | case 'RUN':
20 | return {
21 | ...state,
22 | running: true,
23 | lastAction: 'RUN',
24 | }
25 | case 'UPDATE': {
26 | const newState = {
27 | ...state,
28 | timestamp: action.timestamp,
29 | lastAction: 'UPDATE',
30 | }
31 | return updatePosition(newState)
32 | }
33 | default:
34 | return state
35 | }
36 | }
37 |
38 | export default rootReducer;
--------------------------------------------------------------------------------
/src/threeApp/threeActionHelpers.js:
--------------------------------------------------------------------------------
1 | export const fadeColor = (state, e) => {
2 | const newState = { ...state }
3 | const newColors = state.scene.spheres.colors.map((color) => {
4 | color.r = e.nativeEvent.clientX / window.innerWidth
5 | color.b = e.nativeEvent.clientY / window.innerHeight
6 | return color
7 | })
8 | newState.colors = newColors
9 | return newState
10 | }
11 |
12 | export const switchColor = (state) => {
13 | const newState = { ...state }
14 | const newColors = state.scene.spheres.colors.map((color) => {
15 | color.g = Math.random() * 0.5 + 0.5
16 | return color
17 | })
18 | newState.colors = newColors
19 | return newState
20 | }
21 |
22 | export const updatePosition = (state) => {
23 | const newState = { ...state }
24 | const newPositions = state.scene.spheres.positions.map((position) => {
25 | position.z = 2 * Math.sin(state.timestamp / 1000 + (position.x / 10) + (position.y / 10))
26 | return position
27 | })
28 | newState.scene.spheres.positions = newPositions
29 | return newState
30 | }
--------------------------------------------------------------------------------
/src/threeApp/threeApp.js:
--------------------------------------------------------------------------------
1 | import * as THREE from 'three'
2 |
3 | export const getThreeCamera = () => {
4 | const camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1000)
5 | camera.position.x = 40
6 | camera.position.y = 40
7 | camera.position.z = 60
8 |
9 | return camera
10 | }
11 |
12 | export const getInitialSceneState = () => {
13 |
14 | const initialState = {
15 | spheres: {
16 | colors: [],
17 | positions: [],
18 | },
19 | }
20 |
21 | for (let y = 0; y < 20; y++) {
22 | for (let x = 0; x < 20; x++) {
23 | const positionX = x * 4
24 | const positionY = y * 4
25 | initialState.spheres.positions.push({ x: positionX, y: positionY, z: 0 })
26 |
27 | initialState.spheres.colors.push({r: 1, g: 0.65, b: 0})
28 | }
29 | }
30 | return initialState
31 | }
32 |
33 | export const getThreeRenderer = () => {
34 | const container = document.getElementById('container')
35 | const renderer = new THREE.WebGLRenderer({ antialias: true })
36 | renderer.setClearColor(0xf0ffff)
37 | renderer.setSize(window.innerWidth, window.innerHeight)
38 | container.appendChild(renderer.domElement)
39 |
40 | return renderer
41 | }
42 |
43 | export const getThreeScene = () => {
44 | const scene = new THREE.Scene()
45 |
46 | for (let y = 0; y < 20; y++) {
47 | for (let x = 0; x < 20; x++) {
48 | const geometry = new THREE.SphereGeometry(1, 32, 32)
49 | const material = new THREE.MeshBasicMaterial({ color: 0xffa500 })
50 | const sphere = new THREE.Mesh(geometry, material)
51 | sphere.position.x = x * 4
52 | sphere.position.y = y * 4
53 | sphere.name = x + (y * 20)
54 | scene.add(sphere)
55 | }
56 | }
57 | return scene
58 | }
59 |
--------------------------------------------------------------------------------
/src/threeApp/threeHelpers.js:
--------------------------------------------------------------------------------
1 | export const mapStateToScene = (sceneState, scene) => {
2 | for (let i = 0; i < sceneState.spheres.positions.length; i++) {
3 | const mesh = scene.getObjectByName(i)
4 | const color = sceneState.spheres.colors[i]
5 | const position = sceneState.spheres.positions[i]
6 | mesh.material.color = color
7 | mesh.position.set(position.x, position.y, position.z)
8 | }
9 | }
--------------------------------------------------------------------------------