https://github.com/richplastow/react-three-starter-minimal
A simple template for reactive 3D web apps
https://github.com/richplastow/react-three-starter-minimal
Last synced: 3 months ago
JSON representation
A simple template for reactive 3D web apps
- Host: GitHub
- URL: https://github.com/richplastow/react-three-starter-minimal
- Owner: richplastow
- License: mit
- Created: 2023-01-16T22:01:22.000Z (over 2 years ago)
- Default Branch: main
- Last Pushed: 2023-02-04T22:27:01.000Z (over 2 years ago)
- Last Synced: 2025-01-06T01:10:48.086Z (5 months ago)
- Language: JavaScript
- Size: 1.11 MB
- Stars: 0
- Watchers: 3
- Forks: 0
- Open Issues: 0
-
Metadata Files:
- Readme: README.md
- License: LICENSE
Awesome Lists containing this project
README
# react-three-starter-minimal
A simple template for reactive 3D web apps
---
## How to create a react-three starter
### __1. Create the repo__
1. At GitHub, click the ‘+’ icon, and ‘New repository’
2. Name it, describe it, tick ‘Add a README file’, choose MIT license
3. Click ‘Create repository’
4. Click the ‘Code’ button, ‘Local’ tab, ‘SSH’, and the copy icon
5. In your Terminal, `cd` to wherever you work
6. `git clone ` and paste something like ‘[email protected]:kim/my-app.git’
7. `cd` into the new directory, eg `cd my-app`### __2. Create the .gitignore file__
```
.DS_Store
node_modules
node_modules.zip
```### __3. Create the package.json file__
1. Create a default __package.json__ file:
`npm init --yes`
2. Change the version to 0.0.1:
`sed -i.bu 's/: "1.0.0",/: "0.0.1",/' package.json`
3. Insert your name, email and domain:
`sed -i.bu 's/"author": "",/"author": "n (https:\/\/d)",/' package.json`
4. Change the license to MIT:
`sed -i.bu 's/: "ISC",/: "MIT",/' package.json`
5. Remove the ‘main’ property because this is an app not a library,
and also tell Node to use `import` not `require()` (avoids needing .mjs):
`sed -i.bu 's/"main": "index.js"/"type": "module"/' package.json`
6. Delete the temporary __package.json.bu__ file:
`rm package.json.bu`### __4. Create a simple server, for zero-build development on your local machine__
Create __server.js__ — `npm start` will look for this file:
```js
// server.js
// A simple server, intended for zero-build development on your local machine./* -------------------------- Imports and Constants ------------------------- */
import child_process from 'child_process';
import fs from 'fs';
import http from 'http';
import { dirname, resolve } from 'path';
import { Transform } from 'stream';
import { repairJsImports } from '@0bdx/semi-parser';const host = '127.0.0.1'; // localhost
const port = 4321;// `npm start --open` or `npm start -o` means we should open a browser window.
const doOpen = process.env.npm_config_open || process.env.npm_config_o;// `npm start --src` means we should use the 'src/' folder, not 'docs/'.
const dir = process.env.npm_config_src ? 'src' : 'docs';/* --------------------------------- Server --------------------------------- */
// Create and start the server.
http.createServer((req, res) => {// Proxy '/' to '/index.html', or '/sub/dir/' to '/sub/dir/index.html'.
// Get the mime type, or respond with a 404 if there's a problem.
const url = req.url.slice(-1) === '/' ? `${req.url}index.html` : req.url;
const ext = getExt(url); // undefined if there's no extension
if (! ext) return send404(res, `url '${url}' has no extension`);
const mime = getMime(ext); // undefined if the extension is not recognised
if (! mime) return send404(res, `extension '${ext}' is not recognised`);// Serve a JavaScript file, or respond with a 404 if it doesn't exist.
// repairJsImports() will make `export` and `import` paths browser-friendly.
if (ext === 'js' || ext === 'mjs') {// Proxy '__NODE_MODULES_PROXY__/a/b.js' to '../node_modules/a/b.js'.
// These kind of urls are usually generated by repairJsImports(), using
// the repairMap we passed in.
const proxyPos = url.indexOf('__NODE_MODULES_PROXY__');
const realLocation = proxyPos === -1 ? `${dir}${url}`
: `${dir}/../node_modules/${url.slice(proxyPos + 22)}`;// JavaScript source code must be read in its entirety, so that it can
// be semi-parsed by repairJsImports().
try {
const source = fs.readFileSync(realLocation)+'';
res.setHeader('Content-Type', mime);
res.write(repairJsImports(source, repairMap));
res.end();
} catch (error) { send404(res, 'Not Found') }
return;
}// Serve a non-JavaScript file, or respond with a 404 if it doesn't exist.
// Since we don’t need to semi-parse this file, we can stream it, which
// should improve performance, especially for large files.
const readStream = fs.createReadStream(`${dir}${url}`);
readStream.on('error', () => send404(res, 'Not Found') );
readStream.on('open', () => {
res.setHeader('Content-Type', mime);
readStream.pipe(res);
});}).listen(port, host, () => console.log(`${dir}/ 👉 http://${host}:${port}/`));
// Open a browser window, if the '--open' or '-o' command line option was set.
// Based on https://stackoverflow.com/a/49013356
// @TODO test on Windows and Linux
if (doOpen) {
const openCommand = process.platform === 'darwin' ? 'open'
: process.platform === 'win32' ? 'start' : 'xdg-open';
const fullUrl = `http://${host}:${port}/index.html`;
child_process.exec(
`${openCommand} ${fullUrl}`,
(error, _stdout, stderr) => console.log(
error || stderr || `Opened ${fullUrl} in your default browser.`)
);
}/* --------------------------------- Helpers -------------------------------- */
// Responds to a request with a 404 error, and a plain text message.
function send404(res, err) {
res.setHeader('Content-Type', 'text/plain');
res.statusCode = 404;
res.end(err.message || err);
console.error(err.message || err);
}// Gets the file extension from a url, or undefined if no extension is present.
function getExt(url) {
const exts = url.split('/').pop().split('.');
if (exts.length === 1) return void 0;
return exts.pop().toLowerCase();
}// Returns the mime type if the extension is recognised, or else undefined.
function getMime(ext) {
return {
css: 'text/css',
gif: 'image/gif',
htm: 'text/html',
html: 'text/html',
ico: 'image/x-icon',
jpeg: 'image/jpeg',
jpg: 'image/jpeg',
js: 'application/javascript',
json: 'application/json',
mjs: 'application/javascript',
otf: 'application/x-font-opentype',
png: 'image/png',
svg: 'image/svg+xml',
ttf: 'application/x-font-ttf',
txt: 'text/plain',
wasm: 'application/wasm',
webmanifest: 'application/manifest+json',
woff: 'application/font-woff',
woff2: 'application/font-woff2',
xml: 'text/xml',
}[ext];
}// Passed in to repairJsImports().
const repairMap = {
// htm: './__NODE_MODULES_PROXY__/htm/dist/htm.module.js',
htm: './__NODE_MODULES_PROXY__/htm/src/index.mjs',
};
```### __5. Create an initial static page, and bundle it for production__
Create the initial __src/index.html__ file:
```html
react-three-starter-minimal
body { background:#111; color:#ccc; font:20px/1.5 Arial }
Loading...
```
Create the initial __src/main.js__ file:
```js
document.querySelector('h1').innerText = 'Hello react-three-starter-minimal';
```Install Rollup, along with a couple of plugins, as dev dependencies.
`npm i rollup -D`
3.10.0 adds 2 packages, 2.5 MB, 29 items.`npm i rollup-plugin-copy -D`
3.4.0 adds 39?? packages, 829?? kB, 397?? items.
This will let Rollup copy files from src/ to docs/.`npm i @rollup/plugin-terser -D`
0.3.0 adds 16?? packages, 3.6?? MB, 143?? items.
Minifies JavaScipt bundles.Create the __scripts/rollup-production.config.js__ file:
```js
// Configuration used by Rollup during `npm run build:production`.import { nodeResolve } from '@rollup/plugin-node-resolve';
import { readFileSync } from 'fs';
import child_process from 'child_process';
import copy from 'rollup-plugin-copy';
import terser from '@rollup/plugin-terser';export default [
{
input: `src/main.js`,
output: {
banner: generateBanner(),
file: `docs/main.js`,
format: 'es',
},
plugins: [
copy({
targets: [
{ src:'src/index.html', dest:'docs' },
{ src:'src/favicon.ico', dest:'docs' },
{ src:'src/asset', dest:'docs' },
],
}),
nodeResolve(),
terser(),
],
},
];function generateBanner() {
const { author, license, name, version } =
JSON.parse(readFileSync('./package.json', 'utf-8'));
const firstCommitYear = getYearOfFirstGitCommit();
if (! firstCommitYear) process.exit(1);
const thisYear = new Date().getFullYear();
const year = ! firstCommitYear || firstCommitYear === thisYear
? thisYear : `${firstCommitYear} - ${thisYear}`;
return [
'/**',
` * ${name}`,
` * @version ${version}`,
` * @license Copyright (c) ${year} ${author}`,
` * SPDX-License-Identifier: ${license}`,
' */',
].join('\n');
}function getYearOfFirstGitCommit() {
const fn = 'Warning: getYearOfFirstGitCommit():';
try {
const stdout = child_process.execSync(
'git log $(git rev-list --max-parents=0 HEAD)').toString();
const matches = stdout.match(/Date:\s*([^\n\r]+)/);
if (matches === null) return console.warn(fn, 'Unexpected stdout:', stdout);
const date = new Date(matches[1]);
if (isNaN(date)) return console.warn(fn, 'Invalid date:', matches[1]);
return date.getFullYear();
} catch (err) {
return console.warn(fn, err.stderr.toString());
}
}
```Add this to the `"scripts"` object in the __package.json__ file:
`"build:production": "rollup -c scripts/rollup-production.config.js",`Run the Rollup build:
`npm run build:production`
After a few seconds, __docs/__ should be created. You should see that
__docs/main.js__ has been minified, and has a block comment at the top containing
values from the __package.json__ file.Point the local development server at the docs folder:
`npm start --open`
A browser window should automatically open http://127.0.0.1:4321/index.html,
showing the headline “Hello react-three-starter-minimal”.### __Use Rollup to create a browser-friendly ES module build of React Three Fiber__
The React Three Fiber module actually contains React and ThreeJS, and it can
also [take the place of ReactDOM.](
https://docs.pmnd.rs/react-three-fiber/api/canvas#custom-canvas)To install React Three Fiber:
`npm i @react-three/fiber`
8.10.0 adds 22 packages, 39 MB, 1,592 items.Look at __node_modules/@react-three/fiber/dist/react-three-fiber.esm.js__ —
notice that the ES module `import` paths are designed to be resolved by Node:
`import * as React from 'react';`
Your development browser won’t resolve that. It expects something like:
`import * as React from '../react/index.js';`We can use Rollup to concatenate all the imports into a single file, which your
development browser can import. [Vite calls this approach ‘dependency pre-bundling’.](
https://vitejs.dev/guide/dep-pre-bundling.html#dev-bundle)Install Rollup, along with some plugins, as dev dependencies.
`npm i rollup -D`
3.10.0 adds 2 packages, 2.5 MB, 29 items.`npm i @rollup/plugin-node-resolve -D`
15.0.1 adds 6 packages, 554 kB, 280 items.
This will let Rollup make sense of a path like 'react'.`npm i @rollup/plugin-commonjs -D`
24.0.0 adds 14 packages, 609 kB, 95 items.
This will let Rollup import packages which use `require('...')`.`npm i @rollup/plugin-terser -D`
0.3.0 adds 16 packages, 3.6 MB, 143 items.
Minifies JavaScipt bundles.Create the __rollup-dev-bundle.config.js__ file:
```js
// Configuration used by Rollup during `npm run build:dev-bundle`.
import commonjs from '@rollup/plugin-commonjs';
import { nodeResolve } from '@rollup/plugin-node-resolve';
import terser from '@rollup/plugin-terser';
export default [
{
input: `@react-three/fiber`,
output: {
file: `src/lib/react-three-fiber.min.js`,
format: 'es',
outro: 'const React = react.exports; export { React, THREE };',
},
plugins: [ commonjs(), nodeResolve(), terser() ],
},
];
```Add this to the `"scripts"` object in the __package.json__ file:
`"build:dev-bundle": "rollup -c rollup-dev-bundle.config.js",`Run the Rollup build:
`npm run build:dev-bundle`After a few seconds, __src/lib/react-three-fiber.js__ should be created.
Its last line should be the Rollup `outro`, giving us access to R3F’s internal
React and THREE:
`const React = react.exports; export { React, THREE };`### __Modify the [React Three Starter demo](https://docs.pmnd.rs/react-three-fiber/getting-started/introduction#what-does-it-look-like?) for a buildless development workflow__
We can’t use JSX in a buildless workflow, but [htm](
https://www.npmjs.com/package/htm) does a similar job:
`npm i htm`
3.1.1 adds 1 package, 88 kB, 50 items.
Consider installing Matt Bierner’s ‘lit-html’ VS Code extension, for syntax
highlighting and intellisense.Create the __src/main.js__ file:
```js
```
Create the __src/index.html__ file:
```html
react-three-starter-minimal
body { background:#111; color:#ccc; font:20px/1.5 Arial }
canvas { position:fixed; top:0; left:0; z-index:-1 }
react-three-starter-minimal
window.process = { env:{ production:true } }
import { extend, createRoot, events, React, THREE, useFrame }
from './lib/react-three-fiber.min.js';
const { createElement:h, useRef, useState } = React;
extend(THREE); // register the THREE namespace as elements
// Configure the root, inject events optionally, set camera, etc.
const root = createRoot(document.querySelector('canvas'));
root.configure({ events, camera: { position:[0,0,-10] } });// `createRoot` is not responsive, so handle resize here.
window.addEventListener('resize', () => root.configure({
size: { width: window.innerWidth, height: window.innerHeight } }));
window.dispatchEvent(new Event('resize')); // trigger resize// Define the Box component.
function Box(props) {
const mesh = useRef(); // direct access to the mesh
const [ hovered, setHover ] = useState(false); // set up hovered state
const [ active, setActive ] = useState(false); // set up active state// Subscribe Box to the render-loop, and rotate the mesh every frame.
useFrame((state, delta) => (mesh.current.rotation.x += delta));// Return the view - regular ThreeJS elements built by createElement().
return (
h('mesh', {
...props,
ref: mesh,
scale: active ? 1.5 : 1,
onClick: event => setActive(!active),
onPointerOver: event => setHover(true),
onPointerOut: event => setHover(false),
}, [
h('boxGeometry', { key:'bg', args:[1,1,1] }),
h('meshStandardMaterial', {
key:'msm', color:hovered ? 'hotpink' : 'orange' }),
]));
}root.render([ // start rendering the scene
h('ambientLight', { key:'al' }),
h('pointLight', { key:'pl', position:[10,10,10] }),
h(Box, { key:'b0', position:[-1.2,0,0] }),
h(Box, { key:'b1', position:[1.2,0,0] }),
]);
// root.unmount(); // unmount and dispose of memory
```
### __Use Rollup to create the production bundle__
`npm i rollup-plugin-copy -D`
3.4.0 adds 39 packages, 829 kB, 397 items.
This will let Rollup copy files from src/index.html to docs/index.html.@TODO the rest of this section