├── public ├── robots.txt ├── logo512x512.png ├── favicon512x512.ico ├── manifest.json └── index.html ├── .prettierrc.yaml ├── .gitignore ├── src ├── index.js ├── demoProps.js ├── templates │ ├── cameraViewParams.js │ └── boxModelParams.js ├── components │ ├── NumericInputField.js │ └── layouts.js ├── App.js ├── serviceWorker.js └── box.js ├── package.json ├── LICENSE ├── README.md └── prototype-script.py /public/robots.txt: -------------------------------------------------------------------------------- 1 | # https://www.robotstxt.org/robotstxt.html 2 | User-agent: * 3 | Disallow: 4 | -------------------------------------------------------------------------------- /public/logo512x512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mithi/hello-tiny-box/HEAD/public/logo512x512.png -------------------------------------------------------------------------------- /public/favicon512x512.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mithi/hello-tiny-box/HEAD/public/favicon512x512.ico -------------------------------------------------------------------------------- /.prettierrc.yaml: -------------------------------------------------------------------------------- 1 | printWidth: 90 2 | tabWidth: 4 3 | quoteProps: "consistent" 4 | arrowParens: "avoid" 5 | semi: false 6 | singleQuote: false 7 | jsxSingleQuote: false 8 | trailingComma: "es5" 9 | bracketSpacing: true 10 | jsxBracketSameLine: false 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 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import React from "react" 2 | import ReactDOM from "react-dom" 3 | import App from "./App" 4 | import * as serviceWorker from "./serviceWorker" 5 | 6 | ReactDOM.render( 7 | 8 | 9 | , 10 | document.getElementById("root") 11 | ) 12 | 13 | // If you want your app to work offline and load faster, you can change 14 | // unregister() to register() below. Note this comes with some pitfalls. 15 | // Learn more about service workers: https://bit.ly/CRA-PWA 16 | serviceWorker.unregister() 17 | -------------------------------------------------------------------------------- /public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "Tiny Box", 3 | "name": "Hello Tiny Box !", 4 | "icons": [ 5 | { 6 | "src": "favicon64x64.ico", 7 | "sizes": "64x64 32x32 24x24 16x16", 8 | "type": "image/x-icon" 9 | }, 10 | { 11 | "src": "logo512x512.png", 12 | "type": "image/png", 13 | "sizes": "512x512" 14 | } 15 | ], 16 | "start_url": ".", 17 | "display": "standalone", 18 | "theme_color": "#000000", 19 | "background_color": "#ffffff" 20 | } 21 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "hello-tiny-box", 3 | "version": "0.1.0", 4 | "private": true, 5 | "dependencies": { 6 | "@material-ui/core": "^4.11.0", 7 | "@material-ui/icons": "^4.9.1", 8 | "bare-minimum-2d": "^0.2.0", 9 | "gh-pages": "^3.1.0", 10 | "react": "^16.13.1", 11 | "react-dom": "^16.13.1", 12 | "react-scripts": "3.4.2" 13 | }, 14 | "homepage": "http://mithi.github.io/hello-tiny-box", 15 | "scripts": { 16 | "start": "react-scripts start", 17 | "build": "react-scripts build", 18 | "predeploy": "npm run build", 19 | "deploy": "gh-pages -d build" 20 | }, 21 | "eslintConfig": { 22 | "extends": "react-app" 23 | }, 24 | "browserslist": { 25 | "production": [ 26 | ">0.2%", 27 | "not dead", 28 | "not op_mini all" 29 | ], 30 | "development": [ 31 | "last 1 chrome version", 32 | "last 1 firefox version", 33 | "last 1 safari version" 34 | ] 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | Hello Tiny Box! Manipulate a 3d Box 12 | 16 | 20 | 21 | 22 | 23 |
24 | 25 | 26 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Mithi Sevilla 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /src/demoProps.js: -------------------------------------------------------------------------------- 1 | const container = { 2 | color: "#333333", 3 | opacity: 1.0, 4 | xRange: 200, 5 | yRange: 200, 6 | } 7 | 8 | const newBoxPoint = (x, y, z, id) => { 9 | return { 10 | x: [x], 11 | y: [50 + y], 12 | color: "#FF0000", 13 | opacity: 1.0, 14 | size: (10 * z) / 100, 15 | type: "points", 16 | id, 17 | } 18 | } 19 | 20 | const newCamPoint = (x, y, z, zoom, id) => { 21 | return { 22 | x: [x], 23 | y: [-30 + y], 24 | color: "#FFFFFF", 25 | opacity: zoom / 10, 26 | size: (10 * z) / 100, 27 | type: "points", 28 | id, 29 | } 30 | } 31 | const newPlotParams = (cam, box) => { 32 | const { rx, ry, rz, tx, ty, tz, sx, sy, sz } = box 33 | 34 | const rBox = newBoxPoint(rx + 50, ry, rz, "rbox") 35 | const sBox = newBoxPoint(sx, sy, sz, "sbox") 36 | const tBox = newBoxPoint(tx - 50, ty, tz, "tbox") 37 | 38 | const { rx: crx, ry: cry, rz: crz, tx: ctx, ty: cty, tz: ctz, zoom } = cam 39 | const rCam = newCamPoint(crx + 30, cry, crz, zoom, "rcam") 40 | const tCam = newCamPoint(ctx - 30, cty, ctz, zoom, "tcam") 41 | const data = [rBox, sBox, tBox, rCam, tCam] 42 | return { data, container } 43 | } 44 | 45 | export { newPlotParams } 46 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Hello Tiny Box 📦 2 | 3 | > Manipulate a three-dimensional box. 4 | 5 | - This project is inspired by [Gabriel Gambetta's Computer Graphics from Scratch](https://www.gabrielgambetta.com/computer-graphics-from-scratch/scene-setup.html) online book 6 | 7 | - [`mithi.github.io/hello-tiny-box`](https://mithi.github.io/hello-tiny-box/) 8 | 9 | - See also: My [`quick and dirty Python script`](./prototype-script.py) 10 | 11 | ![User Interface Screenshot](https://user-images.githubusercontent.com/1670421/90424712-258d9f80-e0f1-11ea-972d-01f738102517.png) 12 | 13 | ## Contributing 14 | 15 | PRs welcome! Please read the [contributing guidelines](https://github.com/mithi/hexapod/blob/master/CONTRIBUTING.md) and the [commit style guide](https://github.com/mithi/hexapod/wiki/A-Commit-Style-Guide)! 16 | 17 | ## References 18 | 19 | - [x] [Scratch a Pixel 2.0: Finding the 2D pixel coordinates of a 3D Point Explained from Beginning to End](https://www.scratchapixel.com/lessons/3d-basic-rendering/computing-pixel-coordinates-of-3d-point/mathematics-computing-2d-coordinates-of-3d-points) 20 | 21 | - [x] [Gabriel Gambeta: Computer Graphics from scratch (Perspective Projection)](https://www.gabrielgambetta.com/computer-graphics-from-scratch/perspective-projection.html) 22 | 23 | - [x] [David J. Eck: Introduction to Computer Graphics (Projection and Viewing), Hobart and William Smith Colleges](http://math.hws.edu/graphicsbook/c3/s3.html) 24 | 25 | - [x] [Jeremiah: 3D Game Engine Programming (Understanding the View Matrix)](https://www.3dgep.com/understanding-the-view-matrix/) 26 | 27 | - [x] [Etay Meiri: OLDEV Model OpenGL Tutorial (Camera Space)](http://ogldev.org/www/tutorial13/tutorial13.html) 28 | 29 | - [x] [Plotly: 3D Camera Controls in Python](https://plotly.com/python/3d-camera-controls/) 30 | -------------------------------------------------------------------------------- /src/templates/cameraViewParams.js: -------------------------------------------------------------------------------- 1 | const INIT_STATE = { 2 | rx: 0, 3 | ry: 0, 4 | rz: 0, 5 | tx: 0, 6 | ty: 0, 7 | tz: 0, 8 | zoom: 1, 9 | } 10 | 11 | /** THE PROPS REQUIRED FOR EACH INPUT FIELD **/ 12 | const STATE_PROPS = { 13 | rx: { 14 | rangeParams: { 15 | minVal: -90, 16 | maxVal: 90, 17 | stepVal: 0.5, 18 | }, 19 | label: "rotX", 20 | id: "camera-view-rot-x", 21 | }, 22 | ry: { 23 | rangeParams: { 24 | minVal: -90, 25 | maxVal: 90, 26 | stepVal: 0.5, 27 | }, 28 | label: "rotY", 29 | id: "camera-view-rot-z", 30 | }, 31 | rz: { 32 | rangeParams: { 33 | minVal: -90, 34 | maxVal: 90, 35 | stepVal: 0.5, 36 | }, 37 | label: "rotZ", 38 | id: "camera-view-rot-y", 39 | }, 40 | tx: { 41 | rangeParams: { 42 | minVal: -10, 43 | maxVal: 10, 44 | stepVal: 0.1, 45 | }, 46 | label: "t.X", 47 | id: "camera-view-trans-x", 48 | }, 49 | ty: { 50 | rangeParams: { 51 | minVal: -10, 52 | maxVal: 10, 53 | stepVal: 0.1, 54 | }, 55 | label: "t.Y", 56 | id: "camera-view-trans-y", 57 | }, 58 | tz: { 59 | rangeParams: { 60 | minVal: -10, 61 | maxVal: 10, 62 | stepVal: 0.1, 63 | }, 64 | label: "t.Z", 65 | id: "camera-view-trans-z", 66 | }, 67 | zoom: { 68 | rangeParams: { 69 | minVal: 0, 70 | maxVal: 10, 71 | stepVal: 0.1, 72 | }, 73 | label: "zoom", 74 | id: "camera-view-zoom", 75 | }, 76 | } 77 | 78 | const ID_TO_KEY_MAP = { 79 | [STATE_PROPS.rx.id]: "rx", 80 | [STATE_PROPS.ry.id]: "ry", 81 | [STATE_PROPS.rz.id]: "rz", 82 | [STATE_PROPS.tx.id]: "tx", 83 | [STATE_PROPS.ty.id]: "ty", 84 | [STATE_PROPS.tz.id]: "tz", 85 | [STATE_PROPS.zoom.id]: "zoom", 86 | } 87 | 88 | export default { INIT_STATE, STATE_PROPS, ID_TO_KEY_MAP } 89 | -------------------------------------------------------------------------------- /src/components/NumericInputField.js: -------------------------------------------------------------------------------- 1 | import React, { useState, useRef } from "react" 2 | import TextField from "@material-ui/core/TextField" 3 | 4 | const cleanValue = (newValue, ref, { minVal, maxVal, stepVal }) => { 5 | const validity = ref.current.validity 6 | const isValid = false 7 | 8 | if (validity.badInput) { 9 | return { isValid, message: "NaN" } 10 | } 11 | 12 | if (validity.rangeOverflow) { 13 | return { isValid, message: `max=${maxVal}` } 14 | } 15 | 16 | if (validity.rangeUnderflow) { 17 | return { isValid, message: `min=${minVal}` } 18 | } 19 | 20 | if (validity.stepMismatch) { 21 | return { isValid, message: `step=${stepVal}` } 22 | } 23 | 24 | if (!ref.current.checkValidity()) { 25 | return { isValid, message: "Error" } 26 | } 27 | 28 | const numberValue = parseFloat(newValue) 29 | 30 | if (isNaN(numberValue)) { 31 | return { isValid, message: "NAN" } 32 | } 33 | 34 | return { isValid: true, message: "", value: numberValue } 35 | } 36 | 37 | const InputField = ({ id, label, value, setField, rangeParams }) => { 38 | const { minVal, maxVal, stepVal } = rangeParams 39 | 40 | const [message, setMessage] = useState("") 41 | const ref = useRef(null) 42 | 43 | const handleChange = newValue => { 44 | const { isValid, value: cleanedValue, message: newMessage } = cleanValue( 45 | newValue, 46 | ref, 47 | rangeParams 48 | ) 49 | if (isValid) { 50 | setField(id, cleanedValue) 51 | } 52 | 53 | setMessage(newMessage) 54 | } 55 | 56 | return ( 57 |
58 | handleChange(e.target.value)} 72 | helperText={message} 73 | /> 74 |
75 | ) 76 | } 77 | 78 | export default InputField 79 | -------------------------------------------------------------------------------- /src/templates/boxModelParams.js: -------------------------------------------------------------------------------- 1 | const INIT_STATE = { 2 | rx: 0, 3 | ry: 0, 4 | rz: 0, 5 | tx: 0, 6 | ty: 0, 7 | tz: 0, 8 | sx: 1, 9 | sy: 1, 10 | sz: 1, 11 | color: 0, 12 | } 13 | 14 | /** THE PROPS REQUIRED FOR EACH INPUT FIELD **/ 15 | const STATE_PROPS = { 16 | rx: { 17 | rangeParams: { 18 | minVal: -180, 19 | maxVal: 180, 20 | stepVal: 0.5, 21 | }, 22 | label: "rotX", 23 | id: "cube-state-rot-x", 24 | }, 25 | ry: { 26 | rangeParams: { 27 | minVal: -180, 28 | maxVal: 180, 29 | stepVal: 0.5, 30 | }, 31 | label: "rotY", 32 | id: "cube-state-rot-z", 33 | }, 34 | rz: { 35 | rangeParams: { 36 | minVal: -180, 37 | maxVal: 180, 38 | stepVal: 0.5, 39 | }, 40 | label: "rotZ", 41 | id: "cube-state-rot-y", 42 | }, 43 | 44 | // --------------------------- 45 | 46 | tx: { 47 | rangeParams: { 48 | minVal: -100, 49 | maxVal: 100, 50 | stepVal: 0.1, 51 | }, 52 | label: "t.X", 53 | id: "cube-state-trans-x", 54 | }, 55 | ty: { 56 | rangeParams: { 57 | minVal: -100, 58 | maxVal: 100, 59 | stepVal: 0.1, 60 | }, 61 | label: "t.Y", 62 | id: "cube-state-trans-y", 63 | }, 64 | tz: { 65 | rangeParams: { 66 | minVal: -100, 67 | maxVal: 100, 68 | stepVal: 0.1, 69 | }, 70 | label: "t.Z", 71 | id: "cube-state-trans-z", 72 | }, 73 | 74 | // --------------------------- 75 | 76 | sx: { 77 | rangeParams: { 78 | minVal: 0, 79 | maxVal: 10, 80 | stepVal: 0.1, 81 | }, 82 | label: "s.X", 83 | id: "cube-state-scale-x", 84 | }, 85 | 86 | sy: { 87 | rangeParams: { 88 | minVal: 0, 89 | maxVal: 10, 90 | stepVal: 0.1, 91 | }, 92 | label: "s.Y", 93 | id: "cube-state-scale-y", 94 | }, 95 | sz: { 96 | rangeParams: { 97 | minVal: 0, 98 | maxVal: 10, 99 | stepVal: 0.1, 100 | }, 101 | label: "s.Z", 102 | id: "cube-state-scale-z", 103 | }, 104 | // --------------------------- 105 | 106 | color: { 107 | label: "color", 108 | id: "cube-color", 109 | }, 110 | } 111 | 112 | const ID_TO_KEY_MAP = { 113 | [STATE_PROPS.rx.id]: "rx", 114 | [STATE_PROPS.ry.id]: "ry", 115 | [STATE_PROPS.rz.id]: "rz", 116 | 117 | [STATE_PROPS.tx.id]: "tx", 118 | [STATE_PROPS.ty.id]: "ty", 119 | [STATE_PROPS.tz.id]: "tz", 120 | 121 | [STATE_PROPS.sx.id]: "sx", 122 | [STATE_PROPS.sy.id]: "sy", 123 | [STATE_PROPS.sz.id]: "sz", 124 | 125 | [STATE_PROPS.color.id]: "color", 126 | } 127 | 128 | export default { INIT_STATE, STATE_PROPS, ID_TO_KEY_MAP } 129 | -------------------------------------------------------------------------------- /src/components/layouts.js: -------------------------------------------------------------------------------- 1 | import React from "react" 2 | import { ThemeProvider } from "@material-ui/styles" 3 | import { 4 | Grid, 5 | CssBaseline, 6 | Typography, 7 | createMuiTheme, 8 | CardContent, 9 | Slider, 10 | Card, 11 | Radio, 12 | RadioGroup, 13 | FormControlLabel, 14 | Box, 15 | } from "@material-ui/core" 16 | 17 | const theme = createMuiTheme({ 18 | palette: { 19 | type: "dark", 20 | }, 21 | }) 22 | 23 | const SliderInputField = ({ id, label, value, rangeParams, setField }) => { 24 | const handleChange = (_, newValue) => setField(id, newValue) 25 | return ( 26 | <> 27 | {label} 28 | 38 | 39 | ) 40 | } 41 | 42 | const ControlCard = ({ title, children }) => ( 43 | 44 | 45 | 46 | 47 | {title} 48 | 49 | {children} 50 | 51 | 52 | 53 | ) 54 | 55 | const InputGroup3 = ({ children }) => { 56 | return ( 57 | 58 | {children.map(child => ( 59 | 60 | {child} 61 | 62 | ))} 63 | 64 | ) 65 | } 66 | 67 | const ToggleRadioCard = ({ radioValue, onChange, option1Label, option2Label }) => ( 68 | 69 | onChange(e.target.value)}> 70 | } label={option1Label} /> 71 | } label={option2Label} /> 72 | 73 | 74 | ) 75 | 76 | class Layout extends React.Component { 77 | static Side = ({ children }) => ( 78 | 79 | {children} 80 | 81 | ) 82 | static Main = ({ children }) => ( 83 | 84 | 92 |
{children}
93 |
94 |
95 | ) 96 | 97 | render() { 98 | return ( 99 | 100 | 101 | 102 | {this.props.children} 103 | 104 | 105 | ) 106 | } 107 | } 108 | 109 | export { Layout, ControlCard, SliderInputField, InputGroup3, ToggleRadioCard } 110 | -------------------------------------------------------------------------------- /src/App.js: -------------------------------------------------------------------------------- 1 | import React, { useState } from "react" 2 | import NumericInputField from "./components/NumericInputField" 3 | import { 4 | Layout, 5 | ControlCard, 6 | SliderInputField, 7 | InputGroup3, 8 | ToggleRadioCard, 9 | } from "./components/layouts" 10 | import CAM from "./templates/cameraViewParams" 11 | import BOX from "./templates/boxModelParams" 12 | import BareMinimum2d from "bare-minimum-2d" 13 | import { renderScene } from "./box" 14 | import { Button } from "@material-ui/core" 15 | import GitHubIcon from "@material-ui/icons/GitHub" 16 | 17 | // A helper to build the a set of required props... props that would be 18 | // be passed to components like SLIDER or INPUT TEXT FIELD 19 | const consolidateProp = (currentState, stateProps, setFunction) => { 20 | /** 21 | consolidatedProps = { 22 | rx: { 23 | id, 24 | label, 25 | rangeParams: {maxVal, minVal, stepVal}, 26 | value, 27 | setField, 28 | }, 29 | ry: { ... } 30 | .... 31 | } 32 | **/ 33 | const consolidatedProps = Object.keys(stateProps).reduce( 34 | (props, key) => ({ 35 | ...props, 36 | [key]: { 37 | ...stateProps[key], 38 | value: currentState[key], 39 | setField: setFunction, 40 | }, 41 | }), 42 | {} 43 | ) 44 | return consolidatedProps 45 | } 46 | 47 | const CameraControlView = ({ camProps }) => ( 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | ) 59 | 60 | const BoxModelControlView = ({ boxProps }) => ( 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | {/*COLOR STATE IS NOT YET IMPLEMENTED FOR NOW */} 76 | 77 | ) 78 | 79 | const App = () => { 80 | const [cameraViewState, setCameraViewState] = useState(CAM.INIT_STATE) 81 | const [boxModelState, setBoxModelState] = useState(BOX.INIT_STATE) 82 | const [isCameraView, setControlUi] = React.useState("true") 83 | 84 | const setCameraViewField = (id, newValue) => { 85 | setCameraViewState({ ...cameraViewState, [CAM.ID_TO_KEY_MAP[id]]: newValue }) 86 | } 87 | const setBoxModelField = (id, newValue) => { 88 | setBoxModelState({ ...boxModelState, [BOX.ID_TO_KEY_MAP[id]]: newValue }) 89 | } 90 | 91 | const showCamera = isCameraView === "true" 92 | const camProps = consolidateProp(cameraViewState, CAM.STATE_PROPS, setCameraViewField) 93 | const boxProps = consolidateProp(boxModelState, BOX.STATE_PROPS, setBoxModelField) 94 | const plotProps = renderScene(boxModelState, cameraViewState) 95 | 96 | return ( 97 | 98 | 99 | 100 | 101 | 102 | 103 | 109 | 112 | 115 |
116 | 117 | 124 | 125 |
126 |
127 |
128 | ) 129 | } 130 | 131 | export default App 132 | -------------------------------------------------------------------------------- /src/serviceWorker.js: -------------------------------------------------------------------------------- 1 | // This optional code is used to register a service worker. 2 | // register() is not called by default. 3 | 4 | // This lets the app load faster on subsequent visits in production, and gives 5 | // it offline capabilities. However, it also means that developers (and users) 6 | // will only see deployed updates on subsequent visits to a page, after all the 7 | // existing tabs open on the page have been closed, since previously cached 8 | // resources are updated in the background. 9 | 10 | // To learn more about the benefits of this model and instructions on how to 11 | // opt-in, read https://bit.ly/CRA-PWA 12 | 13 | const isLocalhost = Boolean( 14 | window.location.hostname === 'localhost' || 15 | // [::1] is the IPv6 localhost address. 16 | window.location.hostname === '[::1]' || 17 | // 127.0.0.0/8 are considered localhost for IPv4. 18 | window.location.hostname.match( 19 | /^127(?:\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}$/ 20 | ) 21 | ); 22 | 23 | export function register(config) { 24 | if (process.env.NODE_ENV === 'production' && 'serviceWorker' in navigator) { 25 | // The URL constructor is available in all browsers that support SW. 26 | const publicUrl = new URL(process.env.PUBLIC_URL, window.location.href); 27 | if (publicUrl.origin !== window.location.origin) { 28 | // Our service worker won't work if PUBLIC_URL is on a different origin 29 | // from what our page is served on. This might happen if a CDN is used to 30 | // serve assets; see https://github.com/facebook/create-react-app/issues/2374 31 | return; 32 | } 33 | 34 | window.addEventListener('load', () => { 35 | const swUrl = `${process.env.PUBLIC_URL}/service-worker.js`; 36 | 37 | if (isLocalhost) { 38 | // This is running on localhost. Let's check if a service worker still exists or not. 39 | checkValidServiceWorker(swUrl, config); 40 | 41 | // Add some additional logging to localhost, pointing developers to the 42 | // service worker/PWA documentation. 43 | navigator.serviceWorker.ready.then(() => { 44 | console.log( 45 | 'This web app is being served cache-first by a service ' + 46 | 'worker. To learn more, visit https://bit.ly/CRA-PWA' 47 | ); 48 | }); 49 | } else { 50 | // Is not localhost. Just register service worker 51 | registerValidSW(swUrl, config); 52 | } 53 | }); 54 | } 55 | } 56 | 57 | function registerValidSW(swUrl, config) { 58 | navigator.serviceWorker 59 | .register(swUrl) 60 | .then(registration => { 61 | registration.onupdatefound = () => { 62 | const installingWorker = registration.installing; 63 | if (installingWorker == null) { 64 | return; 65 | } 66 | installingWorker.onstatechange = () => { 67 | if (installingWorker.state === 'installed') { 68 | if (navigator.serviceWorker.controller) { 69 | // At this point, the updated precached content has been fetched, 70 | // but the previous service worker will still serve the older 71 | // content until all client tabs are closed. 72 | console.log( 73 | 'New content is available and will be used when all ' + 74 | 'tabs for this page are closed. See https://bit.ly/CRA-PWA.' 75 | ); 76 | 77 | // Execute callback 78 | if (config && config.onUpdate) { 79 | config.onUpdate(registration); 80 | } 81 | } else { 82 | // At this point, everything has been precached. 83 | // It's the perfect time to display a 84 | // "Content is cached for offline use." message. 85 | console.log('Content is cached for offline use.'); 86 | 87 | // Execute callback 88 | if (config && config.onSuccess) { 89 | config.onSuccess(registration); 90 | } 91 | } 92 | } 93 | }; 94 | }; 95 | }) 96 | .catch(error => { 97 | console.error('Error during service worker registration:', error); 98 | }); 99 | } 100 | 101 | function checkValidServiceWorker(swUrl, config) { 102 | // Check if the service worker can be found. If it can't reload the page. 103 | fetch(swUrl, { 104 | headers: { 'Service-Worker': 'script' }, 105 | }) 106 | .then(response => { 107 | // Ensure service worker exists, and that we really are getting a JS file. 108 | const contentType = response.headers.get('content-type'); 109 | if ( 110 | response.status === 404 || 111 | (contentType != null && contentType.indexOf('javascript') === -1) 112 | ) { 113 | // No service worker found. Probably a different app. Reload the page. 114 | navigator.serviceWorker.ready.then(registration => { 115 | registration.unregister().then(() => { 116 | window.location.reload(); 117 | }); 118 | }); 119 | } else { 120 | // Service worker found. Proceed as normal. 121 | registerValidSW(swUrl, config); 122 | } 123 | }) 124 | .catch(() => { 125 | console.log( 126 | 'No internet connection found. App is running in offline mode.' 127 | ); 128 | }); 129 | } 130 | 131 | export function unregister() { 132 | if ('serviceWorker' in navigator) { 133 | navigator.serviceWorker.ready 134 | .then(registration => { 135 | registration.unregister(); 136 | }) 137 | .catch(error => { 138 | console.error(error.message); 139 | }); 140 | } 141 | } 142 | -------------------------------------------------------------------------------- /prototype-script.py: -------------------------------------------------------------------------------- 1 | """ 2 | 3 | 4 | ____ __ __ ____ 5 | /\ _`\ /'__`\ /\ \ /\ _`\ 6 | \ \ \/\ \ _ __ __ __ __ __ __ /\_\L\ \ \_\ \ \ \ \L\ \ ___ __ _ 7 | \ \ \ \ \/\`'__\/'__`\ /\ \/\ \/\ \ /'__`\ \/_/_\_<_ /'_` \ \ \ _ <' / __`\/\ \/'\ 8 | \ \ \_\ \ \ \//\ \L\.\_\ \ \_/ \_/ \ /\ \L\.\_ /\ \L\ \/\ \L\ \ \ \ \L\ \/\ \L\ \/> (thetaDegrees * Math.PI) / 180 2 | const getSinCos = theta => [Math.sin(radians(theta)), Math.cos(radians(theta))] 3 | const dot = (a, b) => a.x * b.x + a.y * b.y + a.z * b.z 4 | 5 | const vectorLength = v => Math.sqrt(dot(v, v)) 6 | const vectorFromTo = (a, b) => new Vector(b.x - a.x, b.y - a.y, b.z - a.z) 7 | const scaleVector = (v, d) => new Vector(d * v.x, d * v.y, d * v.z) 8 | 9 | const cross = (a, b) => { 10 | const x = a.y * b.z - a.z * b.y 11 | const y = a.z * b.x - a.x * b.z 12 | const z = a.x * b.y - a.y * b.x 13 | return new Vector(x, y, z) 14 | } 15 | 16 | const getNormalofThreePoints = (a, b, c) => { 17 | const ba = vectorFromTo(b, a) 18 | const bc = vectorFromTo(b, c) 19 | const n = cross(ba, bc) 20 | const len_n = vectorLength(n) 21 | const unit_n = scaleVector(n, 1 / len_n) 22 | 23 | return unit_n 24 | } 25 | 26 | const uniformMatrix4x4 = d => { 27 | const dRow = [d, d, d, d] 28 | return [dRow.slice(), dRow.slice(), dRow.slice(), dRow.slice()] 29 | } 30 | 31 | const multiply4x4 = (matrixA, matrixB) => { 32 | let resultMatrix = uniformMatrix4x4(null) 33 | 34 | for (let i = 0; i < 4; i++) { 35 | for (let j = 0; j < 4; j++) { 36 | resultMatrix[i][j] = 37 | matrixA[i][0] * matrixB[0][j] + 38 | matrixA[i][1] * matrixB[1][j] + 39 | matrixA[i][2] * matrixB[2][j] + 40 | matrixA[i][3] * matrixB[3][j] 41 | } 42 | } 43 | 44 | return resultMatrix 45 | } 46 | 47 | function rotX(theta, tx = 0, ty = 0, tz = 0) { 48 | const [s, c] = getSinCos(theta) 49 | 50 | return [ 51 | [1, 0, 0, tx], 52 | [0, c, -s, ty], 53 | [0, s, c, tz], 54 | [0, 0, 0, 1], 55 | ] 56 | } 57 | 58 | function rotY(theta) { 59 | const [s, c] = getSinCos(theta) 60 | return [ 61 | [c, 0, s, 0], 62 | [0, 1, 0, 0], 63 | [-s, 0, c, 0], 64 | [0, 0, 0, 1], 65 | ] 66 | } 67 | 68 | function rotZ(theta) { 69 | const [s, c] = getSinCos(theta) 70 | return [ 71 | [c, -s, 0, 0], 72 | [s, c, 0, 0], 73 | [0, 0, 1, 0], 74 | [0, 0, 0, 1], 75 | ] 76 | } 77 | 78 | const rotXYZ = eulerVec => { 79 | const rx = rotX(eulerVec.x) 80 | const ry = rotY(eulerVec.y) 81 | const rz = rotZ(eulerVec.z) 82 | const rxy = multiply4x4(rx, ry) 83 | const rxyz = multiply4x4(rxy, rz) 84 | return rxyz 85 | } 86 | 87 | class Vector { 88 | constructor(x, y, z, name) { 89 | this.x = x 90 | this.y = y 91 | this.z = z 92 | this.name = name 93 | } 94 | 95 | getTransformedPoint(transformMatrix) { 96 | const [r0, r1, r2] = transformMatrix.slice(0, 3) 97 | const [r00, r01, r02, tx] = r0 98 | const [r10, r11, r12, ty] = r1 99 | const [r20, r21, r22, tz] = r2 100 | 101 | const newX = this.x * r00 + this.y * r01 + this.z * r02 + tx 102 | const newY = this.x * r10 + this.y * r11 + this.z * r12 + ty 103 | const newZ = this.x * r20 + this.y * r21 + this.z * r22 + tz 104 | return new Vector(newX, newY, newZ, this.name) 105 | } 106 | } 107 | 108 | const tMatrix = translation => [ 109 | [1, 0, 0, translation.x], 110 | [0, 1, 0, translation.y], 111 | [0, 0, 1, translation.z], 112 | [0, 0, 0, 1], 113 | ] 114 | 115 | const sMatrix = s => [ 116 | [s.x, 0, 0, 0], 117 | [0, s.y, 0, 0], 118 | [0, 0, s.z, 0], 119 | [0, 0, 0, 1], 120 | ] 121 | 122 | /* 123 | E4------F5 y 124 | |`. | `. | 125 | | `A0-----B1 *----- x 126 | | | | | \ 127 | G6--|--H7 | \ 128 | `. | `. | z 129 | `C2-----D3 130 | */ 131 | class NormalUnitCube { 132 | CENTER = new Vector(0, 0, 0, "cube-center") // cube-center 133 | POINTS = [ 134 | new Vector(-1, +1, +1, "front-top-left"), // A0 135 | new Vector(+1, +1, +1, "front-top-right"), // B1 136 | new Vector(-1, -1, +1, "front-bottom-left"), // C2 137 | new Vector(+1, -1, +1, "front-bottom-right"), // D3 138 | new Vector(-1, +1, -1, "back-top-left"), // E4 139 | new Vector(+1, +1, -1, "back-top-right"), // F5 140 | new Vector(-1, -1, -1, "back-bottom-left"), // G6 141 | new Vector(+1, -1, -1, "back-bottom-right"), // H7 142 | ] 143 | } 144 | 145 | class Cube { 146 | UNIT_CUBE = new NormalUnitCube() 147 | constructor( 148 | eulerVec = new Vector(0, 0, 0), 149 | scale = new Vector(1, 1, 1), 150 | translateVec = new Vector(0, 0, 0) 151 | ) { 152 | const rMatrix = rotXYZ(eulerVec) 153 | const s = scale 154 | const t = translateVec 155 | this.wrtWorldMatrix = multiply4x4(tMatrix(t), multiply4x4(sMatrix(s), rMatrix)) 156 | this.points = this.UNIT_CUBE.POINTS 157 | } 158 | } 159 | 160 | const getWorldWrtCameraMatrix = ( 161 | translateVec = Vector(0, 0, 0), 162 | eulerVec = Vector(0, 0, 0) 163 | ) => { 164 | const r = rotXYZ(eulerVec) 165 | const t = translateVec 166 | // Inverse of rotations matrix 167 | // inverse_matrix = rotateCameraMatrixInverse * translateCameraMatrixInverse 168 | // world_to_camera_matrix 169 | return [ 170 | [r[0][0], r[1][0], r[2][0], -t.x], 171 | [r[0][1], r[1][1], r[2][1], -t.y], 172 | [r[0][2], r[1][2], r[2][2], -t.z], 173 | [0, 0, 0, 1], 174 | ] 175 | } 176 | const getProjectedPoint = (point, projectionConstant) => { 177 | return new Vector( 178 | (point.x / point.z) * projectionConstant, 179 | (point.y / point.z) * projectionConstant, 180 | projectionConstant, 181 | point.name 182 | ) 183 | } 184 | 185 | const renderCube = (cube, cubeWrtCameraMatrix, projectionConstant) => { 186 | let projectedPoints = [] 187 | let pointsWrtCamera = [] 188 | cube.points.forEach(point => { 189 | const pointWrtCamera = point.getTransformedPoint(cubeWrtCameraMatrix) 190 | const projectedPoint = getProjectedPoint(pointWrtCamera, projectionConstant) 191 | pointsWrtCamera.push(pointWrtCamera) 192 | projectedPoints.push(projectedPoint) 193 | }) 194 | 195 | return [pointsWrtCamera, projectedPoints] 196 | } 197 | 198 | // RENDER SCENE 199 | 200 | const renderScene = (box, cam) => { 201 | const Z_TRANSLATE_OFFSET = 5 202 | const PROJECTION_CONSTANT = 300 * cam.zoom 203 | const CAMERA_POSITION = new Vector(cam.tx, cam.ty, cam.tz + Z_TRANSLATE_OFFSET) 204 | const CAMERA_ORIENTATION = new Vector(cam.rx, cam.ry, cam.rz) 205 | const worldWrtCameraMatrix = getWorldWrtCameraMatrix( 206 | CAMERA_POSITION, 207 | CAMERA_ORIENTATION 208 | ) 209 | // euler orientation rotation 210 | const r = new Vector(box.rx, box.ry, box.rz) 211 | // translate vector 212 | const t = new Vector(box.tx, box.ty, box.tz) 213 | // scale magnitude 214 | const s = new Vector(box.sx, box.sy, box.sz) 215 | 216 | const cube = new Cube(r, s, t) 217 | const cubeWrtCameraMatrix = multiply4x4(worldWrtCameraMatrix, cube.wrtWorldMatrix) 218 | const [pointsWrtCamera, projectedPoints] = renderCube( 219 | cube, 220 | cubeWrtCameraMatrix, 221 | PROJECTION_CONSTANT 222 | ) 223 | 224 | const isFrontFacing = whichPlanesFrontFacing(pointsWrtCamera) 225 | return drawBox(projectedPoints, isFrontFacing) 226 | } 227 | 228 | /* 229 | E4------F5 y 230 | |`. | `. | 231 | | `A0-----B1 *----- x 232 | | | | | \ 233 | G6--|--H7 | \ 234 | `. | `. | z 235 | `C2-----D3 236 | 237 | face 1 - A0, B1, D3 | C2 (front) 238 | face 2 - B1, F5, H7 | D3 (front right) 239 | face 3 - F5, E4, G6 | H7 (front left) 240 | face 4 - E4, A0, C2 | G6 (back) 241 | face 5 - E4, F5, B1 | A0 (top) 242 | face 6 - C2 , D3, H7 | G6 |(bottom) 243 | 244 | IMPORTANT! 245 | The second point (ie B1 of set [A0, B1, D3, C2] 246 | is the center of A0, B1, D3 which is where we will 247 | compute the normal of the plane 248 | */ 249 | 250 | // use back face culling to figure out which 251 | // faces are in front 252 | 253 | const POINT_FACE_SET = [ 254 | [0, 1, 3, 2], 255 | [1, 5, 7, 3], 256 | [5, 4, 6, 7], 257 | [4, 0, 2, 6], 258 | [4, 5, 1, 0], 259 | [2, 3, 7, 6], 260 | ] 261 | // returns an array of booleans with six elements 262 | // returns if the respective planes defined by the for each set of points (POINT_FACE_SET) 263 | // are front facing or not 264 | const whichPlanesFrontFacing = pointsWrtCamera => { 265 | const p = pointsWrtCamera 266 | return POINT_FACE_SET.map(pointIds => { 267 | const [a, b, c] = pointIds 268 | 269 | const n = getNormalofThreePoints(p[a], p[b], p[c]) 270 | // v is vector from point p[b] 271 | // to cameraOriginPoint Vector(0, 0, 0) 272 | const v = new Vector(-p[b].x, -p[b].y, -p[b].z) 273 | const isFrontFacing = dot(n, v) > 0.0 274 | 275 | return isFrontFacing 276 | }) 277 | } 278 | 279 | const drawBox = (projectedPoints, isFrontFacing) => { 280 | const p = projectedPoints 281 | const container = { 282 | color: "#333333", 283 | opacity: 1.0, 284 | xRange: 600, 285 | yRange: 600, 286 | } 287 | 288 | const COLORS = ["#32ff7e", "#e056fd", "#E91E63", "#fa8231", "#fff200", "#ff3838"] 289 | const OPACITY = [0.75, 0.75, 0.75, 0.75, 0.75, 0.75] 290 | 291 | let data = [] 292 | isFrontFacing.forEach((isFront, index) => { 293 | const [a, b, c, d] = POINT_FACE_SET[index] 294 | const plane = { 295 | x: [p[a].x, p[b].x, p[c].x, p[d].x], 296 | y: [p[a].y, p[b].y, p[c].y, p[d].y], 297 | borderColor: "#0652DD", 298 | borderOpacity: 1.0, 299 | fillColor: COLORS[index], 300 | fillOpacity: OPACITY[index], 301 | borderSize: 8, 302 | type: "polygon", 303 | id: `plane-${index}`, 304 | } 305 | const points = { 306 | x: [p[a].x, p[b].x, p[c].x, p[d].x], 307 | y: [p[a].y, p[b].y, p[c].y, p[d].y], 308 | color: "#0652DD", 309 | opacity: 1.0, 310 | size: 15, 311 | type: "points", 312 | id: `points-${index}`, 313 | } 314 | 315 | data = isFront ? [...data, plane, points] : [plane, points, ...data] 316 | }) 317 | 318 | return { data, container } 319 | } 320 | 321 | export { Cube, renderScene, drawBox } 322 | --------------------------------------------------------------------------------