├── 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 | 
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 |
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 |
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 |
--------------------------------------------------------------------------------