├── thumbnail.png ├── public ├── cyan.jpg ├── pink.jpg ├── level.glb └── index.html ├── .prettierrc ├── src ├── components │ ├── Cube.stories.js │ ├── Sudo.stories.js │ ├── Level.stories.js │ ├── Cactus.stories.js │ ├── Camera.stories.js │ ├── Pyramid.stories.js │ ├── Cactus.js │ ├── Controls.stories.js │ ├── Camera.js │ ├── Level.js │ ├── Controls.js │ ├── Pyramid.js │ ├── Sudo.js │ └── Cube.js ├── App.stories.js ├── index.js ├── styles.css └── App.js ├── .codesandbox └── workspace.json ├── .storybook ├── main.js └── preview.js ├── .gitignore ├── README.md └── package.json /thumbnail.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/winkerVSbecks/threejs-journey-level-1-with-r3f-storybook/HEAD/thumbnail.png -------------------------------------------------------------------------------- /public/cyan.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/winkerVSbecks/threejs-journey-level-1-with-r3f-storybook/HEAD/public/cyan.jpg -------------------------------------------------------------------------------- /public/pink.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/winkerVSbecks/threejs-journey-level-1-with-r3f-storybook/HEAD/public/pink.jpg -------------------------------------------------------------------------------- /public/level.glb: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/winkerVSbecks/threejs-journey-level-1-with-r3f-storybook/HEAD/public/level.glb -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "printWidth": 160, 3 | "tabWidth": 2, 4 | "useTabs": false, 5 | "semi": false, 6 | "singleQuote": true, 7 | "trailingComma": "all", 8 | "bracketSpacing": true, 9 | "jsxBracketSameLine": true, 10 | "fluid": false 11 | } -------------------------------------------------------------------------------- /src/components/Cube.stories.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import Cube from './Cube' 3 | 4 | export default { 5 | title: 'Components/Cube', 6 | component: Cube, 7 | } 8 | 9 | export const Default = () => 10 | Default.storyName = 'Cube' 11 | -------------------------------------------------------------------------------- /src/components/Sudo.stories.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import Sudo from './Sudo' 3 | 4 | export default { 5 | title: 'Components/Sudo', 6 | component: Sudo, 7 | } 8 | 9 | export const Default = () => 10 | Default.storyName = 'Sudo' 11 | -------------------------------------------------------------------------------- /src/components/Level.stories.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import Level from './Level' 3 | 4 | export default { 5 | title: 'Components/Level', 6 | component: Level, 7 | } 8 | 9 | export const Default = () => 10 | Default.storyName = 'Level' 11 | -------------------------------------------------------------------------------- /src/components/Cactus.stories.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import Cactus from './Cactus' 3 | 4 | export default { 5 | title: 'Components/Cactus', 6 | component: Cactus, 7 | } 8 | 9 | export const Default = () => 10 | Default.storyName = 'Cactus' 11 | -------------------------------------------------------------------------------- /src/components/Camera.stories.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import Camera from './Camera' 3 | 4 | export default { 5 | title: 'Components/Camera', 6 | component: Camera, 7 | } 8 | 9 | export const Default = () => 10 | Default.storyName = 'Camera' 11 | -------------------------------------------------------------------------------- /src/components/Pyramid.stories.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import Pyramid from './Pyramid' 3 | 4 | export default { 5 | title: 'Components/Pyramid', 6 | component: Pyramid, 7 | } 8 | 9 | export const Default = () => 10 | Default.storyName = 'Pyramid' 11 | -------------------------------------------------------------------------------- /src/App.stories.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import App from './App' 3 | 4 | export default { 5 | title: 'App/App', 6 | component: App, 7 | args: { 8 | noStage: true, 9 | }, 10 | } 11 | 12 | export const Default = () => 13 | Default.storyName = 'App' 14 | -------------------------------------------------------------------------------- /.codesandbox/workspace.json: -------------------------------------------------------------------------------- 1 | { 2 | "responsive-preview": { 3 | "Mobile": [ 4 | 320, 5 | 675 6 | ], 7 | "Tablet": [ 8 | 1024, 9 | 765 10 | ], 11 | "Desktop": [ 12 | 1400, 13 | 800 14 | ], 15 | "Desktop HD": [ 16 | 1920, 17 | 1080 18 | ] 19 | } 20 | } -------------------------------------------------------------------------------- /.storybook/main.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | "stories": [ 3 | "../src/**/*.stories.mdx", 4 | "../src/**/*.stories.@(js|jsx|ts|tsx)" 5 | ], 6 | "addons": [ 7 | "@storybook/addon-links", 8 | "@storybook/addon-essentials", 9 | "@storybook/preset-create-react-app" 10 | ], 11 | "framework": "@storybook/react" 12 | } -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | ThreeJS Journey - Level 1 7 | 8 | 9 |
10 | 11 | 12 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import { render } from 'react-dom' 2 | import { Suspense } from 'react' 3 | import { Loader } from '@react-three/drei' 4 | import './styles.css' 5 | import App from './App' 6 | 7 | render( 8 | <> 9 | 10 | 11 | 12 | 13 | , 14 | document.getElementById('root'), 15 | ) 16 | -------------------------------------------------------------------------------- /src/components/Cactus.js: -------------------------------------------------------------------------------- 1 | import { MeshWobbleMaterial, useGLTF } from '@react-three/drei' 2 | 3 | export default function Cactus() { 4 | const { nodes, materials } = useGLTF('/level.glb') 5 | return ( 6 | 7 | 8 | 9 | ) 10 | } 11 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # production 12 | /build 13 | 14 | # misc 15 | .DS_Store 16 | .env.local 17 | .env.development.local 18 | .env.test.local 19 | .env.production.local 20 | 21 | npm-debug.log* 22 | yarn-debug.log* 23 | yarn-error.log* 24 | 25 | build-storybook.log -------------------------------------------------------------------------------- /src/components/Controls.stories.js: -------------------------------------------------------------------------------- 1 | import { Box } from '@react-three/drei' 2 | import React from 'react' 3 | import Controls from './Controls' 4 | 5 | export default { 6 | title: 'Components/Controls', 7 | component: Controls, 8 | args: { noControls: true }, 9 | } 10 | 11 | export const Default = () => ( 12 | 13 | 14 | 15 | 16 | 17 | ) 18 | Default.storyName = 'Controls' 19 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Bruno Simon's first journey scene in React Three Fiber and Storybook 2 | 3 | ![](/thumbnail.png) 4 | 5 | [Cody Bennett](https://mobile.twitter.com/Cody_J_Bennett) transcribed [Bruno Simon's](https://bruno-simon.com/) first journey scene into React. [@0xca0a](https://mobile.twitter.com/0xca0a) ran it through gltfjsx and componentized it further. 6 | 7 | I broke it all down into stories so that you can play with each component in isolation. 8 | 9 | [Full context](https://mobile.twitter.com/0xca0a/status/1466178640383778817) 10 | -------------------------------------------------------------------------------- /src/styles.css: -------------------------------------------------------------------------------- 1 | body, 2 | html, 3 | #root { 4 | width: 100%; 5 | height: 100%; 6 | margin: 0; 7 | padding: 0; 8 | overflow: hidden; 9 | } 10 | 11 | #root { 12 | filter: saturate(1.15) hue-rotate(345deg); 13 | } 14 | 15 | #root * { 16 | display: flex; 17 | align-items: center; 18 | justify-content: center; 19 | } 20 | 21 | @keyframes fade-in { 22 | from { 23 | opacity: 0; 24 | } 25 | to { 26 | opacity: 1; 27 | } 28 | } 29 | 30 | canvas { 31 | opacity: 0; 32 | touch-action: none; 33 | cursor: grab; 34 | animation: fade-in 1s ease 0.3s forwards; 35 | } 36 | 37 | canvas:active { 38 | cursor: grabbing; 39 | } 40 | -------------------------------------------------------------------------------- /src/components/Camera.js: -------------------------------------------------------------------------------- 1 | import { useEffect } from 'react' 2 | import { useGLTF } from '@react-three/drei' 3 | import { useSpring, a } from '@react-spring/three' 4 | 5 | export default function Camera() { 6 | const { nodes } = useGLTF('/level.glb') 7 | const [spring, api] = useSpring(() => ({ 'rotation-z': 0, config: { friction: 40 } }), []) 8 | useEffect(() => { 9 | let timeout 10 | const wander = () => { 11 | api.start({ 'rotation-z': Math.random() }) 12 | timeout = setTimeout(wander, (1 + Math.random() * 5) * 1000) 13 | } 14 | wander() 15 | return () => clearTimeout(timeout) 16 | }, []) 17 | return 18 | } 19 | -------------------------------------------------------------------------------- /src/components/Level.js: -------------------------------------------------------------------------------- 1 | import { useThree } from '@react-three/fiber' 2 | import { useGLTF } from '@react-three/drei' 3 | import { useSpring } from '@react-spring/three' 4 | 5 | export default function Level() { 6 | const { nodes } = useGLTF('/level.glb') 7 | const { camera } = useThree() 8 | useSpring( 9 | () => ({ 10 | from: { y: camera.position.y + 5 }, 11 | to: { y: camera.position.y }, 12 | config: { friction: 100 }, 13 | onChange: ({ value }) => ((camera.position.y = value.y), camera.lookAt(0, 0, 0)), 14 | }), 15 | [], 16 | ) 17 | return 18 | } 19 | -------------------------------------------------------------------------------- /.storybook/preview.js: -------------------------------------------------------------------------------- 1 | import React, { Suspense } from 'react' 2 | import { Canvas } from '@react-three/fiber' 3 | import { Stage, Center, OrbitControls } from '@react-three/drei' 4 | import '../src/styles.css' 5 | 6 | export const parameters = { 7 | layout: 'fullscreen', 8 | actions: { argTypesRegex: '^on[A-Z].*' }, 9 | } 10 | 11 | export const decorators = [ 12 | (StoryFn, options) => ( 13 | 14 | {options.args.noStage ? ( 15 | 16 | ) : ( 17 | 18 | 19 | 20 |
21 | 22 |
23 | {options.args.noControls ? null : } 24 |
25 |
26 | )} 27 |
28 | ), 29 | ] 30 | -------------------------------------------------------------------------------- /src/components/Controls.js: -------------------------------------------------------------------------------- 1 | import * as THREE from 'three' 2 | import { useThree } from '@react-three/fiber' 3 | import { useSpring, a } from '@react-spring/three' 4 | import { useDrag } from '@use-gesture/react' 5 | 6 | export default function Controls({ children, minPolarAngle = Math.PI / -4, maxPolarAngle = Math.PI / 2, rotation = [0, 0, 0] }) { 7 | const { size, gl } = useThree() 8 | const [spring, api] = useSpring(() => ({ rotation, config: { tension: 350, mass: 2, friction: 20 } })) 9 | useDrag( 10 | ({ movement: [x], down }) => { 11 | const [y, , z] = rotation 12 | x = THREE.MathUtils.clamp(x / size.width, minPolarAngle, maxPolarAngle) 13 | api.start({ rotation: down ? [y, x * 1.25, z] : rotation }) 14 | }, 15 | { target: gl.domElement }, 16 | ) 17 | return {children} 18 | } 19 | -------------------------------------------------------------------------------- /src/components/Pyramid.js: -------------------------------------------------------------------------------- 1 | import { useEffect } from 'react' 2 | import { useGLTF, useTexture } from '@react-three/drei' 3 | import { useSpring, a } from '@react-spring/three' 4 | 5 | export default function Pyramid() { 6 | const { nodes } = useGLTF('/level.glb') 7 | const matcap = useTexture('/cyan.jpg') 8 | const [spring, api] = useSpring(() => ({ rotation: [0, 0, 0], config: { friction: 80 } }), []) 9 | useEffect(() => { 10 | let timeout 11 | const rotate = () => { 12 | api.start({ rotation: [(Math.random() - 0.5) * Math.PI * 3, 0, (Math.random() - 0.5) * Math.PI * 3] }) 13 | timeout = setTimeout(rotate, (0.5 + Math.random() * 2) * 1000) 14 | } 15 | rotate() 16 | return () => void clearTimeout(timeout) 17 | }, []) 18 | return ( 19 | 20 | 21 | 22 | ) 23 | } 24 | -------------------------------------------------------------------------------- /src/App.js: -------------------------------------------------------------------------------- 1 | import { Canvas } from '@react-three/fiber' 2 | import { EffectComposer, Bloom } from '@react-three/postprocessing' 3 | import Controls from './components/Controls' 4 | import Level from './components/Level' 5 | import Sudo from './components/Sudo' 6 | import Camera from './components/Camera' 7 | import Cactus from './components/Cactus' 8 | import Cube from './components/Cube' 9 | import Pyramid from './components/Pyramid' 10 | 11 | export default function App() { 12 | return ( 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | ) 31 | } 32 | -------------------------------------------------------------------------------- /src/components/Sudo.js: -------------------------------------------------------------------------------- 1 | import { useEffect } from 'react' 2 | import { useGLTF } from '@react-three/drei' 3 | import { useSpring, a } from '@react-spring/three' 4 | 5 | export default function Sudo() { 6 | const { nodes } = useGLTF('/level.glb') 7 | const [spring, api] = useSpring(() => ({ rotation: [0, 0, 0], config: { friction: 40 } }), []) 8 | useEffect(() => { 9 | let timeout 10 | const wander = () => { 11 | api.start({ rotation: [0.8 + Math.random() * 0.4, 0.25 + Math.random() * 0.25, 0] }) 12 | timeout = setTimeout(wander, (1 + Math.random() * 3) * 1000) 13 | } 14 | wander() 15 | return () => clearTimeout(timeout) 16 | }, []) 17 | return ( 18 | <> 19 | 20 | 21 | 22 | 23 | 24 | 25 | ) 26 | } 27 | -------------------------------------------------------------------------------- /src/components/Cube.js: -------------------------------------------------------------------------------- 1 | import { useState, useEffect } from 'react' 2 | import { useGLTF, useTexture } from '@react-three/drei' 3 | import { useSpring, a } from '@react-spring/three' 4 | 5 | export default function Cube() { 6 | const { nodes } = useGLTF('/level.glb') 7 | const matcap = useTexture('/pink.jpg') 8 | const [floating, setFloating] = useState(false) 9 | const [rotation, setRotation] = useState([0, 0, 0]) 10 | const positionSpring = useSpring({ position: [0, floating ? 0.2 : 0, 0], config: { friction: 80 } }) 11 | const rotationSpring = useSpring({ rotation, config: { friction: 40 } }) 12 | useEffect(() => { 13 | let timeout 14 | let rotation = [0, 0] 15 | const bounce = () => { 16 | rotation[0] += Math.ceil(Math.random() * 3) 17 | rotation[1] += Math.ceil(Math.random() * 3) 18 | setFloating((v) => !v) 19 | setRotation([rotation[0] * Math.PI * 0.5, rotation[1] * Math.PI * 0.5, 0]) 20 | timeout = setTimeout(bounce, 1.5 * 1000) 21 | } 22 | bounce() 23 | return () => clearTimeout(timeout) 24 | }, []) 25 | return ( 26 | 27 | 28 | 29 | 30 | 31 | ) 32 | } 33 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "threejs-journey-level-1", 3 | "version": "1.0.0", 4 | "description": "A recreation of the first level from https://threejs-journey.com.", 5 | "keywords": [ 6 | "threejs", 7 | "webgl", 8 | "models", 9 | "react-three-fiber" 10 | ], 11 | "main": "src/index.js", 12 | "dependencies": { 13 | "@react-spring/three": "9.3.1", 14 | "@react-three/drei": "7.25.1", 15 | "@react-three/fiber": "7.0.21", 16 | "@react-three/postprocessing": "2.0.5", 17 | "@use-gesture/react": "10.1.6", 18 | "react": "17.0.2", 19 | "react-dom": "17.0.2", 20 | "react-scripts": "4.0.3", 21 | "three": "0.135.0" 22 | }, 23 | "devDependencies": { 24 | "@babel/runtime": "7.13.8", 25 | "@storybook/addon-actions": "^6.4.4", 26 | "@storybook/addon-essentials": "^6.4.4", 27 | "@storybook/addon-links": "^6.4.4", 28 | "@storybook/node-logger": "^6.4.4", 29 | "@storybook/preset-create-react-app": "^3.2.0", 30 | "@storybook/react": "^6.4.4", 31 | "typescript": "4.1.3" 32 | }, 33 | "scripts": { 34 | "start": "react-scripts start", 35 | "build": "react-scripts build", 36 | "test": "react-scripts test --env=jsdom", 37 | "eject": "react-scripts eject", 38 | "storybook": "start-storybook -p 6006 -s public", 39 | "build-storybook": "build-storybook -s public" 40 | }, 41 | "browserslist": [ 42 | ">0.2%", 43 | "not dead", 44 | "not ie <= 11", 45 | "not op_mini all" 46 | ], 47 | "eslintConfig": { 48 | "overrides": [ 49 | { 50 | "files": [ 51 | "**/*.stories.*" 52 | ], 53 | "rules": { 54 | "import/no-anonymous-default-export": "off" 55 | } 56 | } 57 | ] 58 | } 59 | } 60 | --------------------------------------------------------------------------------