├── .gitignore ├── README.md ├── config-overrides.js ├── package-lock.json ├── package.json ├── public ├── favicon.ico ├── index.html ├── manifest.json └── tyga.png ├── src ├── App.css ├── App.js ├── App1.js ├── App2.js ├── App3.js ├── App4.js ├── data.json ├── index.css ├── index.js ├── logo.svg ├── primitives │ ├── CanvasRoot.js │ ├── Context.js │ ├── Img.js │ ├── ScrollView.js │ ├── Text.js │ ├── View.js │ ├── draw-utils.js │ ├── draw.worker.js │ ├── index.js │ ├── layout-utils.js │ ├── layout.worker.js │ ├── style-utils.js │ ├── text-utils.js │ └── tree-utils.js └── serviceWorker.js └── yarn.lock /.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 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | This project was bootstrapped with [Create React App](https://github.com/facebook/create-react-app). 2 | 3 | ## Available Scripts 4 | 5 | In the project directory, you can run: 6 | 7 | ### `npm start` 8 | 9 | Runs the app in the development mode.
10 | Open [http://localhost:3000](http://localhost:3000) to view it in the browser. 11 | 12 | The page will reload if you make edits.
13 | You will also see any lint errors in the console. 14 | 15 | ### `npm test` 16 | 17 | Launches the test runner in the interactive watch mode.
18 | See the section about [running tests](https://facebook.github.io/create-react-app/docs/running-tests) for more information. 19 | 20 | ### `npm run build` 21 | 22 | Builds the app for production to the `build` folder.
23 | It correctly bundles React in production mode and optimizes the build for the best performance. 24 | 25 | The build is minified and the filenames include the hashes.
26 | Your app is ready to be deployed! 27 | 28 | See the section about [deployment](https://facebook.github.io/create-react-app/docs/deployment) for more information. 29 | 30 | ### `npm run eject` 31 | 32 | **Note: this is a one-way operation. Once you `eject`, you can’t go back!** 33 | 34 | If you aren’t satisfied with the build tool and configuration choices, you can `eject` at any time. This command will remove the single build dependency from your project. 35 | 36 | Instead, it will copy all the configuration files and the transitive dependencies (Webpack, Babel, ESLint, etc) right into your project so you have full control over them. All of the commands except `eject` will still work, but they will point to the copied scripts so you can tweak them. At this point you’re on your own. 37 | 38 | You don’t have to ever use `eject`. The curated feature set is suitable for small and middle deployments, and you shouldn’t feel obligated to use this feature. However we understand that this tool wouldn’t be useful if you couldn’t customize it when you are ready for it. 39 | 40 | ## Learn More 41 | 42 | You can learn more in the [Create React App documentation](https://facebook.github.io/create-react-app/docs/getting-started). 43 | 44 | To learn React, check out the [React documentation](https://reactjs.org/). 45 | 46 | ### Code Splitting 47 | 48 | This section has moved here: https://facebook.github.io/create-react-app/docs/code-splitting 49 | 50 | ### Analyzing the Bundle Size 51 | 52 | This section has moved here: https://facebook.github.io/create-react-app/docs/analyzing-the-bundle-size 53 | 54 | ### Making a Progressive Web App 55 | 56 | This section has moved here: https://facebook.github.io/create-react-app/docs/making-a-progressive-web-app 57 | 58 | ### Advanced Configuration 59 | 60 | This section has moved here: https://facebook.github.io/create-react-app/docs/advanced-configuration 61 | 62 | ### Deployment 63 | 64 | This section has moved here: https://facebook.github.io/create-react-app/docs/deployment 65 | 66 | ### `npm run build` fails to minify 67 | 68 | This section has moved here: https://facebook.github.io/create-react-app/docs/troubleshooting#npm-run-build-fails-to-minify 69 | -------------------------------------------------------------------------------- /config-overrides.js: -------------------------------------------------------------------------------- 1 | module.exports = function override(config, env) { 2 | config.module.rules.push({ 3 | test: /\.worker\.js$/, 4 | use: { loader: 'worker-loader' } 5 | }) 6 | 7 | config.output.globalObject = "this"; 8 | return config; 9 | } -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-blazing", 3 | "version": "0.1.0", 4 | "private": true, 5 | "dependencies": { 6 | "@rehooks/component-size": "^1.0.2", 7 | "@rehooks/window-size": "^1.0.2", 8 | "lodash.throttle": "^4.1.1", 9 | "pixi.js": "^4.8.5", 10 | "rbush": "^2.0.2", 11 | "react": "^16.8.1", 12 | "react-dom": "^16.8.1", 13 | "react-motion": "^0.5.2", 14 | "react-scripts": "2.1.3", 15 | "react-spring": "^8.0.12", 16 | "text-width": "^1.2.0", 17 | "yoga-js": "^1.4.2", 18 | "yoga-layout": "^1.9.3" 19 | }, 20 | "scripts": { 21 | "start": "react-app-rewired start", 22 | "build": "react-app-rewired build", 23 | "test": "react-app-rewired test", 24 | "eject": "react-scripts eject" 25 | }, 26 | "eslintConfig": { 27 | "extends": "react-app" 28 | }, 29 | "browserslist": [ 30 | ">0.2%", 31 | "not dead", 32 | "not ie <= 11", 33 | "not op_mini all" 34 | ], 35 | "devDependencies": { 36 | "react-app-rewired": "^2.1.0", 37 | "worker-loader": "^2.0.0" 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kenwheeler/react-canvas-experiment/6d0d43660e2e631e1e03d7b3d45ebff1b7bbea77/public/favicon.ico -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 10 | 11 | 15 | 16 | 25 | React Blazing 26 | 27 | 28 | 29 |
30 | 40 | 41 | 42 | -------------------------------------------------------------------------------- /public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "React App", 3 | "name": "Create React App Sample", 4 | "icons": [ 5 | { 6 | "src": "favicon.ico", 7 | "sizes": "64x64 32x32 24x24 16x16", 8 | "type": "image/x-icon" 9 | } 10 | ], 11 | "start_url": ".", 12 | "display": "standalone", 13 | "theme_color": "#000000", 14 | "background_color": "#ffffff" 15 | } 16 | -------------------------------------------------------------------------------- /public/tyga.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kenwheeler/react-canvas-experiment/6d0d43660e2e631e1e03d7b3d45ebff1b7bbea77/public/tyga.png -------------------------------------------------------------------------------- /src/App.css: -------------------------------------------------------------------------------- 1 | .App { 2 | text-align: center; 3 | } 4 | 5 | .App-logo { 6 | animation: App-logo-spin infinite 20s linear; 7 | height: 40vmin; 8 | } 9 | 10 | .App-header { 11 | background-color: #282c34; 12 | min-height: 100vh; 13 | display: flex; 14 | flex-direction: column; 15 | align-items: center; 16 | justify-content: center; 17 | font-size: calc(10px + 2vmin); 18 | color: white; 19 | } 20 | 21 | .App-link { 22 | color: #61dafb; 23 | } 24 | 25 | @keyframes App-logo-spin { 26 | from { 27 | transform: rotate(0deg); 28 | } 29 | to { 30 | transform: rotate(360deg); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/App.js: -------------------------------------------------------------------------------- 1 | import React, { useState, useRef, useEffect } from 'react'; 2 | import { CanvasRoot, View, Text, Img } from './primitives'; 3 | import { Spring } from 'react-spring/renderprops'; 4 | 5 | let view1 = { 6 | flex: 1, 7 | padding: 50, 8 | flexDirection: 'row', 9 | backgroundColor: 'mediumspringgreen', 10 | }; 11 | 12 | let view2 = { 13 | flex: 1, 14 | backgroundColor: 'tomato', 15 | borderColor: '#ffcc00', 16 | borderWidth: 10, 17 | padding: 40, 18 | overflow: 'hidden', 19 | }; 20 | 21 | let view3 = { 22 | flex: 1, 23 | borderColor: '#ffcc00', 24 | borderWidth: 10, 25 | padding: 20, 26 | alignItems: 'center', 27 | justifyContent: 'center', 28 | backgroundColor: 'rebeccapurple', 29 | overflow: 'hidden', 30 | }; 31 | 32 | let text = { 33 | fontFamily: 'Tahoma', 34 | fontSize: 16, 35 | lineHeight: 24, 36 | fontStyle: 'normal', 37 | color: 'white', 38 | }; 39 | 40 | function Hoverable(props) { 41 | let viewRef = useRef(); 42 | let [hovered, setHovered] = useState(false); 43 | 44 | return ( 45 | { 49 | setHovered(true); 50 | }} 51 | onMouseLeave={() => { 52 | setHovered(false); 53 | }} 54 | /> 55 | ); 56 | } 57 | 58 | export default function App() { 59 | let [toggled, setToggled] = useState(false); 60 | 61 | const handleClick = () => { 62 | setToggled(toggled => !toggled); 63 | }; 64 | 65 | return ( 66 | 67 | 68 | 69 | 73 | {props => ( 74 | 78 | 79 | The quick brown fox jumped over the log. The quick brown fox 80 | jumped over the log. 81 | 82 | 83 | )} 84 | 85 | 86 | 87 | 88 | 89 | ); 90 | } 91 | -------------------------------------------------------------------------------- /src/App1.js: -------------------------------------------------------------------------------- 1 | import React, { useState, useRef, useEffect } from 'react'; 2 | import { CanvasRoot, View, Text, Img } from './primitives'; 3 | import { Spring } from 'react-spring/renderprops'; 4 | 5 | let view1 = { 6 | flex: 1, 7 | padding: 50, 8 | flexDirection: 'row', 9 | backgroundColor: 'mediumspringgreen', 10 | }; 11 | 12 | let view2 = { 13 | flex: 1, 14 | backgroundColor: 'tomato', 15 | borderColor: '#ffcc00', 16 | borderWidth: 10, 17 | padding: 40, 18 | overflow: 'hidden', 19 | }; 20 | 21 | let view3 = { 22 | flex: 1, 23 | borderColor: '#ffcc00', 24 | borderWidth: 10, 25 | padding: 20, 26 | alignItems: 'center', 27 | justifyContent: 'center', 28 | backgroundColor: 'rebeccapurple', 29 | overflow: 'hidden', 30 | }; 31 | 32 | let text = { 33 | fontFamily: 'Tahoma', 34 | fontSize: 16, 35 | lineHeight: 24, 36 | fontStyle: 'normal', 37 | color: 'white', 38 | }; 39 | 40 | function Hoverable(props) { 41 | let viewRef = useRef(); 42 | let [hovered, setHovered] = useState(false); 43 | 44 | return ( 45 | { 49 | setHovered(true); 50 | }} 51 | onMouseLeave={() => { 52 | setHovered(false); 53 | }} 54 | /> 55 | ); 56 | } 57 | 58 | export default function App() { 59 | let [toggled, setToggled] = useState(false); 60 | 61 | const handleClick = () => { 62 | setToggled(toggled => !toggled); 63 | }; 64 | 65 | return ( 66 | 67 | 68 | 69 | 73 | {props => ( 74 | 78 | 79 | The quick brown fox jumped over the log. The quick brown fox 80 | jumped over the log. 81 | 82 | 83 | )} 84 | 85 | 86 | 87 | 88 | 89 | ); 90 | } 91 | -------------------------------------------------------------------------------- /src/App2.js: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useState } from 'react'; 2 | import { CanvasRoot, View, Text } from './primitives'; 3 | 4 | import data from './data.json'; 5 | 6 | const table = { 7 | flex: 0, 8 | flexDirection: 'column', 9 | backgroundColor: 'black', 10 | }; 11 | 12 | const tableRow = { 13 | flex: 0, 14 | flexDirection: 'row', 15 | width: '100%', 16 | backgroundColor: '#111', 17 | }; 18 | 19 | const tableHeader = { 20 | ...tableRow, 21 | backgroundColor: '#222', 22 | }; 23 | 24 | const tableColumn = { 25 | flex: 1, 26 | padding: 3, 27 | borderWidth: 0.5, 28 | borderColor: 'black', 29 | backgroundColor: '#111', 30 | }; 31 | 32 | const tableHeaderColumn = { 33 | ...tableColumn, 34 | backgroundColor: '#222', 35 | }; 36 | 37 | const text = { 38 | fontFamily: 'Tahoma', 39 | color: 'white', 40 | fontSize: '12', 41 | }; 42 | 43 | function HighlightCell(props) { 44 | let [highlighted, setHighlighted] = useState(false); 45 | 46 | useEffect(() => { 47 | setHighlighted(true); 48 | setTimeout(() => { 49 | setHighlighted(false); 50 | }, 1000); 51 | }, [props.value]); 52 | 53 | return ( 54 | 60 | {props.value.toString()} 61 | 62 | ); 63 | } 64 | 65 | const randomize = data => { 66 | let newData = { ...data }; 67 | 68 | let randomIndex = 69 | Math.floor(Math.random() * newData.datatable.data.length) + 1; 70 | let precision = 100; 71 | newData.datatable.data[randomIndex - 1][3] = 72 | Math.floor( 73 | Math.random() * (10 * precision - 1 * precision) + 1 * precision 74 | ) / 75 | (1 * precision); 76 | 77 | return newData; 78 | }; 79 | 80 | export default function App() { 81 | const [state, setState] = useState(data); 82 | 83 | useEffect(() => { 84 | let interval = setInterval(() => { 85 | setState(state => randomize(state)); 86 | }, 600); 87 | 88 | return () => { 89 | clearInterval(interval); 90 | }; 91 | }, []); 92 | 93 | function renderHeader() { 94 | return ( 95 | 96 | {state.datatable.columns.map(c => { 97 | return ( 98 | 99 | {c.name} 100 | 101 | ); 102 | })} 103 | 104 | ); 105 | } 106 | 107 | function renderData() { 108 | return ( 109 | <> 110 | {state.datatable.data.map(row => { 111 | return ( 112 | 113 | {row.map((col, index) => { 114 | return index === 3 ? ( 115 | 116 | ) : ( 117 | 0 123 | ? 'seagreen' 124 | : 'tomato' 125 | : undefined, 126 | }} 127 | key={index} 128 | > 129 | {col.toString()} 130 | 131 | ); 132 | })} 133 | 134 | ); 135 | })} 136 | 137 | ); 138 | } 139 | return ( 140 | 141 | 142 | {renderHeader()} 143 | {renderData()} 144 | 145 | 146 | ); 147 | } 148 | -------------------------------------------------------------------------------- /src/App3.js: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useState } from 'react'; 2 | import { CanvasRoot, View, Text } from './primitives'; 3 | 4 | let view1 = { 5 | flex: 0, 6 | padding: 50, 7 | width: 800, 8 | flexDirection: 'row', 9 | flexWrap: 'wrap', 10 | backgroundColor: 'mediumspringgreen', 11 | }; 12 | 13 | let items = Array.from(new Array(1000)); 14 | 15 | items = items.map(i => ({ 16 | color: getRandomColor() 17 | })) 18 | 19 | const randomizeColors = (a) => { 20 | return a.map(i => ({ 21 | color: getRandomColor() 22 | })); 23 | } 24 | 25 | function getRandomColor() { 26 | var letters = '0123456789ABCDEF'; 27 | var color = '#'; 28 | for (var i = 0; i < 6; i++) { 29 | color += letters[Math.floor(Math.random() * 16)]; 30 | } 31 | return color; 32 | } 33 | 34 | export default function App() { 35 | let [state, setState] = useState(items); 36 | 37 | useEffect(() => { 38 | let interval = setInterval(() => { 39 | setState(state => randomizeColors(state)); 40 | }, 100); 41 | 42 | return () => { 43 | clearInterval(interval); 44 | } 45 | }, []); 46 | 47 | return ( 48 | 49 | 50 | {state.map((i, index) => { 51 | return 52 | })} 53 | 54 | 55 | ); 56 | } 57 | -------------------------------------------------------------------------------- /src/App4.js: -------------------------------------------------------------------------------- 1 | import React, { useState, useRef, useEffect } from 'react'; 2 | import { CanvasRoot, View, Text, ScrollView } from './primitives'; 3 | 4 | const fontFamily = 'Helvetica Neue'; 5 | export default function App() { 6 | return ( 7 | 8 | 15 | 24 | 33 | 34 | 42 | React 43 | 44 | 45 | 46 | 54 | Docs 55 | 56 | 57 | 58 | 66 | Tutorial 67 | 68 | 69 | 70 | 78 | Blog 79 | 80 | 81 | 82 | 90 | Community 91 | 92 | 93 | 102 | 110 | 118 | v16.8.6 119 | 120 | 121 | 129 | 137 | Languages 138 | 139 | 140 | 148 | 156 | Github 157 | 158 | 159 | 160 | 161 | 162 | 171 | alert('heyyyy')} 173 | style={{ color: '#61dafb', fontSize: 60, fontWeight: 700 }} 174 | > 175 | HEY FULLSTACK 176 | 177 | 178 | 186 | A JavaScript library for building user interfaces 187 | 188 | 189 | 198 | 209 | 210 | Get Started 211 | 212 | 213 | 221 | Take The Tutorial › 222 | 223 | 224 | 225 | 234 | 235 | 236 | 244 | Declarative 245 | 246 | 247 | 255 | React makes it painless to create interactive UIs. Design simple 256 | views for each state in your application, and React will 257 | efficiently update and render just the right components when your 258 | data changes. 259 | 260 | 261 | 269 | Declarative views make your code more predictable and easier to 270 | debug. 271 | 272 | 273 | 274 | 275 | 276 | 284 | Component-Based 285 | 286 | 287 | 295 | Build encapsulated components that manage their own state, then 296 | compose them to make complex UIs. 297 | 298 | 299 | 307 | Since component logic is written in JavaScript instead of 308 | templates, you can easily pass rich data through your app and 309 | keep state out of the DOM. 310 | 311 | 312 | 313 | 314 | 315 | 323 | Learn Once, Write Anywhere 324 | 325 | 326 | 334 | We don’t make assumptions about the rest of your technology stack, 335 | so you can develop new features in React without rewriting 336 | existing code. 337 | 338 | 339 | 347 | React can also render on the server using Node and power mobile 348 | apps using React Native. 349 | 350 | 351 | 352 | 353 | 354 | 355 | ); 356 | } 357 | -------------------------------------------------------------------------------- /src/data.json: -------------------------------------------------------------------------------- 1 | { 2 | "datatable": { 3 | "data": [ 4 | [ 5 | "2014-01-02", 6 | "SPY", 7 | 955782116, 8 | 182.935913, 9 | 1531700525.6 10 | ], 11 | [ 12 | "2014-01-03", 13 | "SPY", 14 | 952482116, 15 | 182.879181, 16 | -603688512.9 17 | ], 18 | [ 19 | "2014-01-06", 20 | "SPY", 21 | 954182116, 22 | 182.422859, 23 | 310894607.7 24 | ], 25 | [ 26 | "2014-01-07", 27 | "SPY", 28 | 947882116, 29 | 183.533158, 30 | -1149264011.7 31 | ], 32 | [ 33 | "2014-01-08", 34 | "SPY", 35 | 944682116, 36 | 183.548301, 37 | -587306105.6 38 | ], 39 | [ 40 | "2014-01-09", 41 | "SPY", 42 | 943932116, 43 | 183.611559, 44 | -137661225.75 45 | ], 46 | [ 47 | "2014-01-10", 48 | "SPY", 49 | 940882116, 50 | 184.034274, 51 | -560015254.95 52 | ], 53 | [ 54 | "2014-01-13", 55 | "SPY", 56 | 943432116, 57 | 181.74065, 58 | 469287398.7 59 | ], 60 | [ 61 | "2014-01-14", 62 | "SPY", 63 | 936482116, 64 | 183.705563, 65 | -1263097517.5 66 | ], 67 | [ 68 | "2014-01-15", 69 | "SPY", 70 | 936532116, 71 | 184.657834, 72 | 9185278.15 73 | ], 74 | [ 75 | "2014-01-16", 76 | "SPY", 77 | 933632116, 78 | 184.413998, 79 | -535507718.6 80 | ], 81 | [ 82 | "2014-01-17", 83 | "SPY", 84 | 927732116, 85 | 183.697407, 86 | -1088042588.2 87 | ], 88 | [ 89 | "2011-01-18", 90 | "SPY", 91 | 948232116, 92 | 273.58, 93 | -1461888000 94 | ], 95 | [ 96 | "2011-01-19", 97 | "SPY", 98 | 948232116, 99 | 273.58, 100 | -1461888000 101 | ], 102 | [ 103 | "2011-01-20", 104 | "SPY", 105 | 948232116, 106 | 273.58, 107 | -1461888000 108 | ], 109 | [ 110 | "2011-01-21", 111 | "SPY", 112 | 948232116, 113 | 273.58, 114 | -1461888000 115 | ], 116 | [ 117 | "2011-01-22", 118 | "SPY", 119 | 948232116, 120 | 273.58, 121 | -1461888000 122 | ], 123 | [ 124 | "2011-01-23", 125 | "SPY", 126 | 948232116, 127 | 273.58, 128 | -1461888000 129 | ], 130 | [ 131 | "2011-01-24", 132 | "SPY", 133 | 948232116, 134 | 273.58, 135 | -1461888000 136 | ], 137 | [ 138 | "2011-01-25", 139 | "SPY", 140 | 948232116, 141 | 273.58, 142 | -1461888000 143 | ], 144 | [ 145 | "2011-01-26", 146 | "SPY", 147 | 948232116, 148 | 273.58, 149 | -1461888000 150 | ], 151 | [ 152 | "2011-01-27", 153 | "SPY", 154 | 948232116, 155 | 273.58, 156 | -1461888000 157 | ], 158 | [ 159 | "2011-01-28", 160 | "SPY", 161 | 948232116, 162 | 273.58, 163 | -1461888000 164 | ], 165 | [ 166 | "2011-01-29", 167 | "SPY", 168 | 948232116, 169 | 273.58, 170 | -1461888000 171 | ] 172 | ], 173 | "columns": [ 174 | { 175 | "name": "date", 176 | "type": "Date" 177 | }, 178 | { 179 | "name": "ticker", 180 | "type": "String" 181 | }, 182 | { 183 | "name": "shares_outstanding", 184 | "type": "BigDecimal(36,14)" 185 | }, 186 | { 187 | "name": "nav", 188 | "type": "BigDecimal(36,14)" 189 | }, 190 | { 191 | "name": "flow_daily", 192 | "type": "BigDecimal(36,14)" 193 | } 194 | ] 195 | }, 196 | "meta": { 197 | "next_cursor_id": null 198 | } 199 | } -------------------------------------------------------------------------------- /src/index.css: -------------------------------------------------------------------------------- 1 | body { 2 | margin: 0; 3 | padding: 0; 4 | font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", "Oxygen", 5 | "Ubuntu", "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue", 6 | sans-serif; 7 | -webkit-font-smoothing: antialiased; 8 | -moz-osx-font-smoothing: grayscale; 9 | } 10 | 11 | html, body, #root { 12 | height: 100%; 13 | } 14 | 15 | code { 16 | font-family: source-code-pro, Menlo, Monaco, Consolas, "Courier New", 17 | monospace; 18 | } 19 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import './index.css'; 4 | import App from './App'; 5 | import * as serviceWorker from './serviceWorker'; 6 | 7 | ReactDOM.render(, document.getElementById('root')); 8 | 9 | // If you want your app to work offline and load faster, you can change 10 | // unregister() to register() below. Note this comes with some pitfalls. 11 | // Learn more about service workers: http://bit.ly/CRA-PWA 12 | serviceWorker.unregister(); 13 | -------------------------------------------------------------------------------- /src/logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /src/primitives/CanvasRoot.js: -------------------------------------------------------------------------------- 1 | import React, { useRef, useEffect } from 'react'; 2 | import useComponentSize from '@rehooks/component-size'; 3 | import rbush from 'rbush'; 4 | 5 | import CanvasContext from './Context'; 6 | import { 7 | addToTree, 8 | removeFromTree, 9 | getChild, 10 | buildLayoutTree, 11 | } from './tree-utils'; 12 | 13 | import layoutWorker from './layout.worker'; 14 | import drawWorker from './draw.worker'; 15 | 16 | const { Provider } = CanvasContext; 17 | 18 | export default function CanvasRoot(props) { 19 | const containerRef = useRef(null); 20 | const canvasRef = useRef(null); 21 | const treeRef = useRef({ children: {}, props: null }); 22 | const layoutTreeRef = useRef({ children: {}, props: null }); 23 | const bufferRef = useRef([]); 24 | const layoutWorkerRef = useRef(null); 25 | const drawWorkerRef = useRef(null); 26 | const sizeRef = useRef(null); 27 | const hoverCacheRef = useRef([]); 28 | const eventMapRef = useRef({ 29 | onClick: {}, 30 | onMouseEnter: {}, 31 | onMouseMove: {}, 32 | onMouseLeave: {}, 33 | onMouseDown: {}, 34 | onMouseUp: {}, 35 | onResize: {}, 36 | onWheel: {}, 37 | }); 38 | const rBushRef = useRef(rbush()); 39 | 40 | let size = useComponentSize(containerRef); 41 | 42 | const dispatchEvent = (event, type) => { 43 | if (type === 'onResize') { 44 | let resizers = eventMapRef.current['onResize']; 45 | 46 | Object.keys(resizers).forEach(r => { 47 | eventMapRef.current['onResize'][r](size); 48 | }); 49 | } else { 50 | event.persist(); 51 | event.preventDefault(); 52 | let { x, y } = event.nativeEvent; 53 | let matches = rBushRef.current.search({ 54 | minX: x, 55 | minY: y, 56 | maxX: x, 57 | maxY: y, 58 | }); 59 | 60 | if (matches.length > 0) { 61 | matches = matches.sort((a, b) => { 62 | return a.depth > b.depth ? -1 : 1; 63 | }); 64 | 65 | if (type === 'onMouseMove') { 66 | let hoverCache = hoverCacheRef.current; 67 | 68 | let leaves = []; 69 | hoverCache.forEach((c, index) => { 70 | let target = matches.some(m => m.id === c); 71 | if (!target) { 72 | leaves.push({ index, id: c }); 73 | } 74 | }); 75 | 76 | leaves.forEach(l => { 77 | hoverCache.splice(l.index); 78 | if (eventMapRef.current['onMouseLeave'][l.id]) { 79 | eventMapRef.current['onMouseLeave'][l.id](event); 80 | } 81 | }); 82 | 83 | matches.forEach(match => { 84 | if (!hoverCache.includes(match.id)) { 85 | hoverCache.push(match.id); 86 | if (eventMapRef.current['onMouseEnter'][match.id]) { 87 | eventMapRef.current['onMouseEnter'][match.id](event); 88 | } 89 | } 90 | }); 91 | } 92 | 93 | matches.forEach(m => { 94 | if (eventMapRef.current[type][m.id]) { 95 | eventMapRef.current[type][m.id](event); 96 | } 97 | }); 98 | } 99 | } 100 | }; 101 | 102 | const getDimensions = (parent, id) => { 103 | let layout = layoutTreeRef.current; 104 | if (Object.keys(layout.children).length > 0) { 105 | const paths = parent.split('|'); 106 | let targetPath = { ...layout }; 107 | 108 | paths.forEach(path => { 109 | if (path !== 'CanvasRoot') { 110 | targetPath = targetPath.children[path]; 111 | } 112 | }); 113 | 114 | if (targetPath) { 115 | const target = targetPath.children[id]; 116 | return { 117 | x: target.x, 118 | y: target.y, 119 | height: target.height, 120 | width: target.width, 121 | }; 122 | } 123 | } 124 | return null; 125 | }; 126 | 127 | const updateRBush = () => { 128 | let tree = rBushRef.current; 129 | let layout = layoutTreeRef.current; 130 | tree.clear(); 131 | 132 | let items = []; 133 | 134 | function addChildren(children) { 135 | Object.keys(children).forEach(child => { 136 | let { x, y, width, height, depth } = children[child]; 137 | items.push({ 138 | minX: x, 139 | minY: y, 140 | maxX: x + width, 141 | maxY: y + height, 142 | id: child, 143 | depth, 144 | }); 145 | if (children[child].children) { 146 | addChildren(children[child].children); 147 | } 148 | }); 149 | } 150 | 151 | addChildren(layout.children); 152 | 153 | tree.load(items); 154 | }; 155 | 156 | const redraw = (parent, id, props) => { 157 | let arr = bufferRef.current; 158 | let child = getChild({ 159 | layoutTree: layoutTreeRef.current, 160 | parent, 161 | id, 162 | props, 163 | }); 164 | if (child) { 165 | arr.push({ parent, id, child }); 166 | bufferRef.current = arr; 167 | } 168 | }; 169 | 170 | const handleBuffer = () => { 171 | let { width, height } = sizeRef.current; 172 | if (bufferRef.current.length > 0) { 173 | layoutWorkerRef.current.postMessage({ 174 | operation: 'updateLayout', 175 | args: { buffer: bufferRef.current, width, height }, 176 | }); 177 | bufferRef.current = []; 178 | } 179 | requestAnimationFrame(handleBuffer); 180 | }; 181 | 182 | const addEvent = (id, props) => { 183 | Object.keys(props).forEach(p => { 184 | if (p in eventMapRef.current) { 185 | eventMapRef.current[p][id] = props[p]; 186 | } 187 | }); 188 | }; 189 | 190 | const removeEvent = (id, props) => { 191 | Object.keys(props).forEach(p => { 192 | if (p in eventMapRef.current) { 193 | delete eventMapRef.current[p][id]; 194 | } 195 | }); 196 | }; 197 | 198 | const registerNode = (parent, id, props, getProps, type) => { 199 | const tree = treeRef.current; 200 | addToTree({ tree, parent, id, props, getProps, type }); 201 | addEvent(id, props); 202 | }; 203 | 204 | const unregisterNode = (parent, id, props) => { 205 | let targetPath = treeRef.current; 206 | removeFromTree({ targetPath, parent, id }); 207 | removeEvent(id, props); 208 | }; 209 | 210 | const handleMessage = event => { 211 | layoutTreeRef.current = event.data; 212 | updateRBush(); 213 | drawWorkerRef.current.postMessage({ 214 | operation: 'updateTree', 215 | args: { tree: layoutTreeRef.current }, 216 | }); 217 | }; 218 | 219 | useEffect(() => { 220 | const { width, height } = size; 221 | 222 | layoutTreeRef.current = buildLayoutTree({ tree: treeRef.current }); 223 | layoutWorkerRef.current = new layoutWorker(); 224 | 225 | const tree = layoutTreeRef.current; 226 | layoutWorkerRef.current.addEventListener('message', handleMessage); 227 | layoutWorkerRef.current.postMessage({ 228 | operation: 'initializeLayout', 229 | args: { tree, width: width, height: height }, 230 | }); 231 | 232 | const offscreen = canvasRef.current.transferControlToOffscreen(); 233 | 234 | drawWorkerRef.current = new drawWorker(); 235 | drawWorkerRef.current.postMessage( 236 | { operation: 'init', canvas: offscreen }, 237 | [offscreen] 238 | ); 239 | 240 | requestAnimationFrame(handleBuffer); 241 | }, []); 242 | 243 | useEffect(() => { 244 | const { width, height } = size; 245 | drawWorkerRef.current.postMessage({ 246 | operation: 'resizeCanvas', 247 | args: { width, height, dpr: window.devicePixelRatio }, 248 | }); 249 | layoutWorkerRef.current.postMessage({ 250 | operation: 'recalcLayout', 251 | args: { width: width, height: height }, 252 | }); 253 | sizeRef.current = size; 254 | dispatchEvent(size, 'onResize'); 255 | }, [size]); 256 | 257 | return ( 258 | 267 |
268 | { 274 | dispatchEvent(e, 'onClick'); 275 | }} 276 | onMouseMove={e => { 277 | dispatchEvent(e, 'onMouseMove'); 278 | }} 279 | onMouseDown={e => { 280 | dispatchEvent(e, 'onMouseDown'); 281 | }} 282 | onMouseUp={e => { 283 | dispatchEvent(e, 'onMouseUp'); 284 | }} 285 | onWheel={e => { 286 | dispatchEvent(e, 'onWheel'); 287 | }} 288 | > 289 | {props.children || null} 290 | 291 |
292 |
293 | ); 294 | } 295 | -------------------------------------------------------------------------------- /src/primitives/Context.js: -------------------------------------------------------------------------------- 1 | import { createContext } from 'react'; 2 | 3 | export default createContext(); 4 | -------------------------------------------------------------------------------- /src/primitives/Img.js: -------------------------------------------------------------------------------- 1 | import { 2 | useContext, 3 | useEffect, 4 | useRef, 5 | useImperativeHandle, 6 | forwardRef, 7 | memo, 8 | } from 'react'; 9 | import CanvasContext from './Context'; 10 | 11 | var ID = function() { 12 | return ( 13 | '_' + 14 | Math.random() 15 | .toString(36) 16 | .substr(2, 9) 17 | ); 18 | }; 19 | 20 | export default memo( 21 | forwardRef(function Img(props, ref) { 22 | const hasDrawn = useRef(false); 23 | const idRef = useRef(ID()); 24 | const propsCache = useRef(props); 25 | const context = useContext(CanvasContext); 26 | 27 | if (!context) { 28 | throw new Error( 29 | 'CanvasRoot not found! View primitives are required to be inside of a CanvasRoot.' 30 | ); 31 | } 32 | 33 | const getDims = () => { 34 | return context.getDimensions(context.parent, idRef.current); 35 | }; 36 | 37 | useImperativeHandle(ref, () => ({ 38 | getDimensions: getDims, 39 | })); 40 | 41 | const getProps = () => { 42 | return propsCache.current; 43 | }; 44 | 45 | useEffect(() => { 46 | context.registerNode( 47 | context.parent, 48 | idRef.current, 49 | props, 50 | getProps, 51 | 'Image' 52 | ); 53 | setTimeout(() => { 54 | hasDrawn.current = true; 55 | }, 0); 56 | return () => { 57 | context.unregisterNode(context.parent, idRef.current, props); 58 | }; 59 | }, []); 60 | 61 | useEffect(() => { 62 | propsCache.current = props; 63 | if (hasDrawn.current === true) { 64 | context.redraw(context.parent, idRef.current, { 65 | style: props.style || {}, 66 | children: props.children || '', 67 | }); 68 | } 69 | }, [props.style, props.children]); 70 | 71 | return null; 72 | }) 73 | ); 74 | -------------------------------------------------------------------------------- /src/primitives/ScrollView.js: -------------------------------------------------------------------------------- 1 | import View from './View'; 2 | import Text from './Text'; 3 | import React, { 4 | forwardRef, 5 | memo, 6 | useState, 7 | useCallback, 8 | useRef, 9 | useEffect, 10 | } from 'react'; 11 | 12 | const styles = { 13 | container: { 14 | flex: 1, 15 | overflow: 'hidden', 16 | }, 17 | scrollbar: { 18 | width: 10, 19 | top: 0, 20 | bottom: 0, 21 | right: 0, 22 | position: 'absolute', 23 | backgroundColor: '#00000025', 24 | }, 25 | puck: { 26 | width: 10, 27 | height: 6, 28 | backgroundColor: '#00000050', 29 | position: 'absolute', 30 | }, 31 | }; 32 | 33 | export default memo( 34 | forwardRef(function ScrollView(props, ref) { 35 | let [pressed, setPressed] = useState(false); 36 | let [pressStart, setPressStart] = useState(0); 37 | let [y, setY] = useState(0); 38 | let [offset, setOffset] = useState(0); 39 | let [containerHeight, setContainerHeight] = useState(0); 40 | let contentRef = useRef(); 41 | let containerRef = useRef(); 42 | let pressedRef = useRef(false); 43 | let offsetRef = useRef(0); 44 | 45 | const handleMouseDown = e => { 46 | setPressed(true); 47 | pressedRef.current = true; 48 | setPressStart(e.clientY); 49 | }; 50 | 51 | const handleMouseUp = () => { 52 | pressedRef.current = false; 53 | setPressed(false); 54 | }; 55 | 56 | const handleMove = e => { 57 | if (pressedRef.current) { 58 | setY(y => { 59 | let newY = y - e.movementY; 60 | if (newY < offsetRef.current * -1) { 61 | newY = offsetRef.current * -1; 62 | } 63 | if (newY > 0) { 64 | newY = 0; 65 | } 66 | return newY; 67 | }); 68 | } 69 | }; 70 | 71 | const handleWheel = e => { 72 | let containerDimensions = containerRef.current.getDimensions(); 73 | let contentDimensions = contentRef.current.getDimensions(); 74 | let heightOffset = 0; 75 | if (containerDimensions && contentDimensions) { 76 | let contentHeight = contentDimensions.height; 77 | let containerHeight = containerDimensions.height; 78 | setContainerHeight(containerHeight); 79 | heightOffset = contentHeight - containerHeight; 80 | setOffset(contentHeight - containerHeight); 81 | offsetRef.current = heightOffset; 82 | } 83 | 84 | setY(y => { 85 | if (heightOffset <= 0) { 86 | return 0; 87 | } 88 | let newY = y + e.deltaY; 89 | if (newY < heightOffset * -1) { 90 | newY = heightOffset * -1; 91 | } 92 | if (newY > 0) { 93 | newY = 0; 94 | } 95 | return newY; 96 | }); 97 | }; 98 | 99 | const handleResize = size => { 100 | let containerDimensions = containerRef.current.getDimensions(); 101 | let containerHeight = containerDimensions.height; 102 | setContainerHeight(containerHeight); 103 | let contentDimensions = contentRef.current.getDimensions(); 104 | if (containerDimensions && contentDimensions) { 105 | let contentHeight = contentDimensions.height; 106 | setOffset(contentHeight - containerHeight); 107 | } 108 | }; 109 | 110 | useEffect(() => { 111 | let containerDimensions = containerRef.current.getDimensions(); 112 | let containerHeight = containerDimensions 113 | ? containerDimensions.height 114 | : 10; 115 | setContainerHeight(containerHeight); 116 | }); 117 | 118 | const puckHeight = 119 | containerHeight - containerHeight * (offset / containerHeight); 120 | const puckTop = y * -1; 121 | 122 | return ( 123 | 131 | 139 | {props.children} 140 | 141 | 142 | {/* 143 | 152 | */} 153 | 154 | ); 155 | }) 156 | ); 157 | -------------------------------------------------------------------------------- /src/primitives/Text.js: -------------------------------------------------------------------------------- 1 | import { 2 | useContext, 3 | useEffect, 4 | useRef, 5 | useImperativeHandle, 6 | forwardRef, 7 | memo, 8 | } from 'react'; 9 | import CanvasContext from './Context'; 10 | 11 | var ID = function() { 12 | return ( 13 | '_' + 14 | Math.random() 15 | .toString(36) 16 | .substr(2, 9) 17 | ); 18 | }; 19 | 20 | export default memo( 21 | forwardRef(function Text(props, ref) { 22 | const hasDrawn = useRef(false); 23 | const idRef = useRef(ID()); 24 | const propsCache = useRef(props); 25 | const context = useContext(CanvasContext); 26 | 27 | if (!context) { 28 | throw new Error( 29 | 'CanvasRoot not found! View primitives are required to be inside of a CanvasRoot.' 30 | ); 31 | } 32 | 33 | if (typeof props.children !== 'string') { 34 | throw new Error('Text primitives require string based children'); 35 | } 36 | 37 | const getDims = () => { 38 | return context.getDimensions(context.parent, idRef.current); 39 | }; 40 | 41 | useImperativeHandle(ref, () => ({ 42 | getDimensions: getDims, 43 | })); 44 | 45 | const getProps = () => { 46 | return propsCache.current; 47 | }; 48 | 49 | useEffect(() => { 50 | context.registerNode( 51 | context.parent, 52 | idRef.current, 53 | props, 54 | getProps, 55 | 'Text' 56 | ); 57 | setTimeout(() => { 58 | hasDrawn.current = true; 59 | }, 0); 60 | return () => { 61 | context.unregisterNode(context.parent, idRef.current, props); 62 | }; 63 | }, []); 64 | 65 | useEffect(() => { 66 | propsCache.current = props; 67 | if (hasDrawn.current === true) { 68 | context.redraw(context.parent, idRef.current, { 69 | style: props.style || {}, 70 | children: props.children || '', 71 | }); 72 | } 73 | }, [props.style, props.children]); 74 | 75 | return null; 76 | }) 77 | ); 78 | -------------------------------------------------------------------------------- /src/primitives/View.js: -------------------------------------------------------------------------------- 1 | import React, { 2 | useContext, 3 | useEffect, 4 | useRef, 5 | forwardRef, 6 | useImperativeHandle, 7 | memo, 8 | } from 'react'; 9 | import CanvasContext from './Context'; 10 | const { Provider } = CanvasContext; 11 | 12 | var ID = function() { 13 | return ( 14 | '_' + 15 | Math.random() 16 | .toString(36) 17 | .substr(2, 9) 18 | ); 19 | }; 20 | 21 | export default memo( 22 | forwardRef(function View(props, ref) { 23 | const hasDrawn = useRef(false); 24 | const idRef = useRef(ID()); 25 | const propsCache = useRef(props); 26 | const context = useContext(CanvasContext); 27 | const childContext = { 28 | parent: context.parent + '|' + idRef.current, 29 | registerNode: context.registerNode, 30 | getDimensions: context.getDimensions, 31 | redraw: context.redraw, 32 | unregisterNode: context.unregisterNode, 33 | }; 34 | 35 | const getDims = () => { 36 | // Use array of numbers for parent path 37 | return context.getDimensions(context.parent, idRef.current); 38 | }; 39 | 40 | useImperativeHandle(ref, () => ({ 41 | getDimensions: getDims, 42 | })); 43 | 44 | if (!context) { 45 | throw new Error( 46 | 'CanvasRoot not found! View primitives are required to be inside of a CanvasRoot.' 47 | ); 48 | } 49 | 50 | const getProps = () => { 51 | return propsCache.current; 52 | }; 53 | 54 | useEffect(() => { 55 | context.registerNode( 56 | context.parent, 57 | idRef.current, 58 | props, 59 | getProps, 60 | 'View' 61 | ); 62 | setTimeout(() => { 63 | hasDrawn.current = true; 64 | }, 0); 65 | return () => { 66 | context.unregisterNode(context.parent, idRef.current, props); 67 | }; 68 | }, []); 69 | 70 | useEffect(() => { 71 | propsCache.current = props; 72 | if (hasDrawn.current === true) { 73 | context.redraw(context.parent, idRef.current, { style: props.style }); 74 | } 75 | }, [props.style, props.children]); 76 | 77 | return {props.children || null}; 78 | }) 79 | ); 80 | -------------------------------------------------------------------------------- /src/primitives/draw-utils.js: -------------------------------------------------------------------------------- 1 | export const drawChildTree = ({ ctx, children }) => { 2 | Object.keys(children).forEach((key, index) => { 3 | const child = children[key]; 4 | drawChild({ ctx, child }); 5 | }); 6 | }; 7 | 8 | export const drawChild = ({ ctx, child }) => { 9 | if (!child.width) { 10 | return; 11 | } 12 | let { x, y, width, height } = child; 13 | 14 | const props = child.props; 15 | 16 | if (child.type === 'View') { 17 | ctx.fillStyle = props.style.backgroundColor || 'transparent'; 18 | 19 | if (props.style.borderWidth) { 20 | ctx.strokeStyle = props.style.borderColor || 'transparent'; 21 | ctx.lineWidth = props.style.borderWidth || 0; 22 | ctx.strokeRect( 23 | x + props.style.borderWidth / 2, 24 | y + props.style.borderWidth / 2, 25 | width - props.style.borderWidth, 26 | height - props.style.borderWidth 27 | ); 28 | } 29 | if (props.style.borderBottomWidth) { 30 | ctx.strokeStyle = 31 | props.style.borderBottomColor || 32 | props.style.borderColor || 33 | 'transparent'; 34 | ctx.lineWidth = props.style.borderBottomWidth || 0; 35 | ctx.beginPath(); 36 | ctx.moveTo(x, y + height - props.style.borderBottomWidth / 2); 37 | ctx.lineTo(x + width, y + height - props.style.borderBottomWidth / 2); 38 | ctx.stroke(); 39 | } 40 | if (props.style.borderTopWidth) { 41 | ctx.strokeStyle = 42 | props.style.borderTopColor || props.style.borderColor || 'transparent'; 43 | ctx.lineWidth = props.style.borderTopWidth || 0; 44 | ctx.beginPath(); 45 | ctx.moveTo(x, y + props.style.borderTopWidth / 2); 46 | ctx.lineTo(x + width, y + props.style.borderTopWidth / 2); 47 | ctx.stroke(); 48 | } 49 | if (props.style.borderLeftWidth) { 50 | ctx.strokeStyle = 51 | props.style.borderLeftColor || props.style.borderColor || 'transparent'; 52 | ctx.lineWidth = props.style.borderLeftWidth || 0; 53 | ctx.beginPath(); 54 | ctx.moveTo(x + props.style.borderLeftWidth / 2, y); 55 | ctx.lineTo(x + props.style.borderLeftWidth / 2, y + height); 56 | ctx.stroke(); 57 | } 58 | if (props.style.borderRightWidth) { 59 | ctx.strokeStyle = 60 | props.style.borderRightColor || 61 | props.style.borderColor || 62 | 'transparent'; 63 | ctx.lineWidth = props.style.borderRightWidth || 0; 64 | ctx.beginPath(); 65 | ctx.moveTo(x + width - props.style.borderRightWidth / 2, y); 66 | ctx.lineTo(x + width - props.style.borderRightWidth / 2, y + height); 67 | ctx.stroke(); 68 | } 69 | 70 | const widthOffset = props.style.borderWidth ? props.style.borderWidth : 0; 71 | const doubleWidthOffset = props.style.borderWidth 72 | ? props.style.borderWidth * 2 73 | : 0; 74 | 75 | ctx.beginPath(); 76 | ctx.rect( 77 | x + widthOffset, 78 | y + widthOffset, 79 | width - doubleWidthOffset, 80 | height - doubleWidthOffset 81 | ); 82 | ctx.fill(); 83 | 84 | if (props.style.overflow === 'hidden') { 85 | ctx.save(); 86 | ctx.clip(); 87 | } 88 | 89 | if (child.children && Object.keys(child.children).length) { 90 | drawChildTree({ ctx, children: child.children }); 91 | } 92 | 93 | if (props.style.overflow === 'hidden') { 94 | ctx.restore(); 95 | } 96 | } else if (child.type === 'Text') { 97 | const { fontFamily, fontSize, fontWeight, color } = props.style; 98 | ctx.font = `normal ${fontWeight || 'normal'} ${fontSize || 99 | 14}px ${fontFamily || 'Arial'}`; 100 | ctx.fillStyle = color || 'black'; 101 | ctx.textBaseline = 'top'; 102 | ctx.direction = 'ltr'; 103 | ctx.textAlign = (props.style && props.style.textAlign) || 'left'; 104 | child.lines && 105 | child.lines.forEach(line => { 106 | ctx.fillText(line.text, x, y + line.y); 107 | }); 108 | } 109 | }; 110 | -------------------------------------------------------------------------------- /src/primitives/draw.worker.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-restricted-globals */ 2 | 3 | import { drawChildTree } from './draw-utils'; 4 | 5 | let canvas; 6 | let ctx; 7 | let tree; 8 | let sizeBuffer = null; 9 | 10 | self.addEventListener('message', handleMessage); 11 | 12 | function drawLoop() { 13 | if (ctx && tree) { 14 | if (sizeBuffer) { 15 | canvas.width = sizeBuffer.width * sizeBuffer.dpr; 16 | canvas.height = sizeBuffer.height * sizeBuffer.dpr; 17 | ctx.scale(sizeBuffer.dpr, sizeBuffer.dpr); 18 | sizeBuffer = null; 19 | } 20 | drawChildTree({ ctx, children: tree.children }); 21 | } 22 | requestAnimationFrame(drawLoop); 23 | } 24 | 25 | function handleMessage(event) { 26 | const { operation, args } = event.data; 27 | 28 | if (operation === 'init') { 29 | canvas = event.data.canvas; 30 | ctx = canvas.getContext('2d', { 31 | alpha: false, 32 | }); 33 | } 34 | 35 | if (operation === 'updateTree') { 36 | if (!tree) { 37 | tree = args.tree; 38 | drawLoop(); 39 | } else { 40 | tree = args.tree; 41 | } 42 | } 43 | 44 | if (operation === 'resizeCanvas') { 45 | sizeBuffer = args; 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/primitives/index.js: -------------------------------------------------------------------------------- 1 | export { default as View } from './View'; 2 | export { default as Text } from './Text'; 3 | export { default as CanvasRoot } from './CanvasRoot'; 4 | export { default as ScrollView } from './ScrollView'; 5 | -------------------------------------------------------------------------------- /src/primitives/layout-utils.js: -------------------------------------------------------------------------------- 1 | import yoga, { Node } from 'yoga-layout'; 2 | import { applyStyles } from './style-utils'; 3 | import { wrapText } from './text-utils'; 4 | 5 | let offscreen, ctx; 6 | 7 | function checkChildren(layoutTree, yogaTree, width, height) { 8 | Object.keys(layoutTree.children).forEach(child => { 9 | let layoutChild = layoutTree.children && layoutTree.children[child]; 10 | let yogaChild = yogaTree.children && yogaTree.children[child]; 11 | if (layoutChild.type === 'Text' && layoutChild && yogaChild) { 12 | let yogaParent = yogaChild.node.getParent(); 13 | let parentStyles = layoutTree.props.style; 14 | let { width: parentWidth } = yogaParent.getComputedLayout(); 15 | let fixedParentWidth = 16 | parentWidth - 17 | (parentStyles.borderWidth ? parentStyles.borderWidth * 2 : 0); 18 | fixedParentWidth = 19 | fixedParentWidth - 20 | (parentStyles.padding ? parentStyles.padding * 2 : 0); 21 | const { 22 | fontFamily, 23 | fontSize, 24 | fontStyle, 25 | color, 26 | } = layoutChild.props.style; 27 | ctx.font = `${fontStyle || 'normal'} ${fontSize || 14}px ${fontFamily || 28 | 'Arial'}`; 29 | ctx.fillStyle = color || 'black'; 30 | ctx.textBaseline = 'top'; 31 | 32 | let lines = wrapText( 33 | ctx, 34 | layoutChild.text, 35 | 1, 36 | 1, 37 | fixedParentWidth || 100, 38 | layoutChild.props.style.lineHeight || 39 | layoutChild.props.style.fontSize || 40 | 14 41 | ); 42 | layoutChild.lines = lines; 43 | 44 | let lineWidths = []; 45 | 46 | lines.forEach(line => { 47 | const { width } = ctx.measureText(line.text); 48 | lineWidths.push(width); 49 | }); 50 | 51 | const fattestLine = Math.max.apply(Math, lineWidths); 52 | const totalHeight = 53 | lines.length * 54 | (layoutChild.props.style.lineHeight || 55 | layoutChild.props.style.fontSize || 56 | 14); 57 | 58 | yogaChild.node.setWidth(fattestLine); 59 | yogaChild.node.setHeight(totalHeight); 60 | } else { 61 | checkChildren(layoutChild, yogaChild, width, height); 62 | } 63 | }); 64 | } 65 | 66 | function updateTextLayout(layoutTree, yogaTree, width, height) { 67 | checkChildren(layoutTree, yogaTree, width, height); 68 | } 69 | 70 | export const recalcLayout = (layoutTree, yogaTree, { width, height }) => { 71 | yogaTree.node.calculateLayout(width, height, yoga.DIRECTION_LTR); 72 | updateTextLayout(layoutTree, yogaTree, width, height); 73 | yogaTree.node.calculateLayout(width, height, yoga.DIRECTION_LTR); 74 | const { 75 | left, 76 | top, 77 | width: cwidth, 78 | height: cheight, 79 | } = yogaTree.node.getComputedLayout(); 80 | layoutTree.x = left; 81 | layoutTree.y = top; 82 | layoutTree.width = cwidth; 83 | layoutTree.height = cheight; 84 | layoutTree.depth = 1; 85 | computeChildren(layoutTree, yogaTree, { x: left, y: top, z: 1 }); 86 | return { yogaTree, layoutTree }; 87 | }; 88 | 89 | export const updateLayout = ( 90 | layoutTree, 91 | yogaTree, 92 | { buffer, width, height } 93 | ) => { 94 | buffer.forEach(({ parent, id, child }) => { 95 | const paths = parent.split('|'); 96 | let targetPath = layoutTree; 97 | let yogaPath = yogaTree; 98 | 99 | paths.forEach(path => { 100 | if (path !== 'CanvasRoot') { 101 | targetPath = targetPath.children[path]; 102 | yogaPath = yogaPath.children[path]; 103 | } 104 | }); 105 | 106 | let yogaTarget = yogaPath.children[id]; 107 | 108 | targetPath.children[id] = child; 109 | if (child.type === 'View') { 110 | applyStyles(yogaTarget.node, child.props.style || {}); 111 | } else if (child.type === 'Text') { 112 | let offscreen = new OffscreenCanvas(width, height); 113 | let ctx = offscreen.getContext('2d'); 114 | 115 | const { fontFamily, fontSize, fontStyle, color } = child.props.style; 116 | ctx.font = `${fontStyle || 'normal'} ${fontSize || 14}px ${fontFamily || 117 | 'Arial'}`; 118 | ctx.fillStyle = color || 'black'; 119 | ctx.textBaseline = 'top'; 120 | 121 | let text = ctx.measureText(child.props.children); 122 | yogaTarget.node.setWidth(text.width); 123 | yogaTarget.node.setHeight(14); 124 | } 125 | }); 126 | 127 | return recalcLayout(layoutTree, yogaTree, { width, height }); 128 | }; 129 | 130 | export const initializeLayout = ( 131 | layoutTree, 132 | yogaTree, 133 | { tree, width, height } 134 | ) => { 135 | offscreen = new OffscreenCanvas(width, height); 136 | ctx = offscreen.getContext('2d', { alpha: false }); 137 | const root = Node.create(); 138 | root.setWidthAuto(); 139 | root.setHeightAuto(); 140 | 141 | yogaTree.node = root; 142 | 143 | if (tree.children) { 144 | layoutChildren({ 145 | layoutRoot: layoutTree, 146 | yogaRoot: yogaTree, 147 | root, 148 | children: tree.children, 149 | width, 150 | height, 151 | }); 152 | } 153 | 154 | yogaTree.node.calculateLayout(width, height, yoga.DIRECTION_LTR); 155 | const { 156 | left, 157 | top, 158 | width: cwidth, 159 | height: cheight, 160 | } = yogaTree.node.getComputedLayout(); 161 | layoutTree.x = left; 162 | layoutTree.y = top; 163 | layoutTree.width = cwidth; 164 | layoutTree.height = cheight; 165 | layoutTree.depth = 1; 166 | computeChildren(layoutTree, yogaTree, { x: left, y: top, z: 1 }); 167 | return { yogaTree, layoutTree }; 168 | }; 169 | 170 | const computeChildren = ( 171 | layoutRoot, 172 | yogaRoot, 173 | offset = { x: 0, y: 0, z: 1 } 174 | ) => { 175 | layoutRoot.children = layoutRoot.children || {}; 176 | Object.keys(yogaRoot.children).forEach(key => { 177 | const layoutChild = layoutRoot.children[key]; 178 | let child = yogaRoot.children[key]; 179 | const { left, top, width, height } = child.node.getComputedLayout(); 180 | layoutChild.x = offset.x + left; 181 | layoutChild.y = offset.y + top; 182 | layoutChild.width = width; 183 | layoutChild.height = height; 184 | layoutChild.depth = offset.z + 1; 185 | if (child.children) { 186 | computeChildren(layoutChild, child, { 187 | x: layoutChild.x, 188 | y: layoutChild.y, 189 | z: offset.z + 1, 190 | }); 191 | } 192 | }); 193 | }; 194 | 195 | const layoutChildren = ({ 196 | layoutRoot, 197 | yogaRoot, 198 | root, 199 | children, 200 | width, 201 | height, 202 | }) => { 203 | yogaRoot.children = yogaRoot.children || {}; 204 | layoutRoot.children = layoutRoot.children || {}; 205 | Object.keys(children).forEach((key, index, arr) => { 206 | const child = children[key]; 207 | const yogaChild = (yogaRoot.children[key] = {}); 208 | const layoutChild = (layoutRoot.children[key] = {}); 209 | layoutRoot.children[key].props = child.props; 210 | layoutRoot.children[key].type = child.type; 211 | layoutRoot.children[key].text = child.text; 212 | if (child.type === 'View') { 213 | yogaChild.node = Node.create(); 214 | applyStyles(yogaChild.node, child.props.style || {}); 215 | root.insertChild(yogaChild.node, index); 216 | if (child.children) { 217 | layoutChildren({ 218 | layoutRoot: layoutRoot.children[key], 219 | yogaRoot: yogaChild, 220 | root: yogaChild.node, 221 | children: child.children, 222 | width, 223 | height, 224 | }); 225 | } else { 226 | layoutRoot.children[key].children = {}; 227 | } 228 | } else if (child.type === 'Text') { 229 | yogaChild.node = Node.create(); 230 | 231 | let offscreen = new OffscreenCanvas(width, height); 232 | let ctx = offscreen.getContext('2d'); 233 | 234 | const { 235 | fontFamily, 236 | fontSize, 237 | fontStyle, 238 | color, 239 | } = layoutChild.props.style; 240 | ctx.font = `${fontStyle || 'normal'} ${fontSize || 14}px ${fontFamily || 241 | 'Arial'}`; 242 | ctx.fillStyle = color || 'black'; 243 | ctx.textBaseline = 'top'; 244 | 245 | let text = ctx.measureText(layoutChild.text); 246 | yogaChild.node.setWidth(text.width); 247 | yogaChild.node.setHeight(14); 248 | root.insertChild(yogaChild.node, index); 249 | layoutRoot.children[key].children = {}; 250 | } 251 | }); 252 | }; 253 | -------------------------------------------------------------------------------- /src/primitives/layout.worker.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-restricted-globals */ 2 | // import { addToTree, removeFromTree } from './tree-utils'; 3 | import { initializeLayout, recalcLayout, updateLayout } from './layout-utils'; 4 | 5 | let layoutTree = {}; 6 | let yogaTree = {}; 7 | 8 | self.addEventListener('message', handleMessage); 9 | 10 | function handleMessage(event) { 11 | const { operation, args } = event.data; 12 | 13 | if (operation === 'initializeLayout') { 14 | let layout = initializeLayout(layoutTree, yogaTree, args); 15 | layoutTree = layout.layoutTree; 16 | yogaTree = layout.yogaTree; 17 | self.postMessage(layoutTree); 18 | } 19 | 20 | if (operation === 'recalcLayout') { 21 | let layout = recalcLayout(layoutTree, yogaTree, args); 22 | layoutTree = layout.layoutTree; 23 | yogaTree = layout.yogaTree; 24 | self.postMessage(layoutTree); 25 | } 26 | 27 | if (operation === 'updateLayout') { 28 | let layout = updateLayout(layoutTree, yogaTree, args); 29 | layoutTree = layout.layoutTree; 30 | yogaTree = layout.yogaTree; 31 | self.postMessage(layoutTree); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/primitives/style-utils.js: -------------------------------------------------------------------------------- 1 | import yoga from 'yoga-layout'; 2 | 3 | export const alignMap = { 4 | center: yoga.ALIGN_CENTER, 5 | 'flex-start': yoga.ALIGN_FLEX_START, 6 | 'flex-end': yoga.ALIGN_FLEX_END, 7 | stretch: yoga.ALIGN_STRETCH, 8 | 'space-between': yoga.ALIGN_SPACE_BETWEEN, 9 | 'space-around': yoga.ALIGN_SPACE_AROUND, 10 | baseline: yoga.ALIGN_BASELINE, 11 | auto: yoga.ALIGN_AUTO, 12 | }; 13 | 14 | export const justifyMap = { 15 | center: yoga.JUSTIFY_CENTER, 16 | 'flex-start': yoga.JUSTIFY_FLEX_START, 17 | 'flex-end': yoga.JUSTIFY_FLEX_END, 18 | 'space-between': yoga.JUSTIFY_SPACE_BETWEEN, 19 | 'space-around': yoga.JUSTIFY_SPACE_AROUND, 20 | 'space-evenly': yoga.JUSTIFY_SPACE_EVENLY, 21 | }; 22 | 23 | export const directionMap = { 24 | row: yoga.FLEX_DIRECTION_ROW, 25 | column: yoga.FLEX_DIRECTION_COLUMN, 26 | 'column-reverse': yoga.FLEX_DIRECTION_COLUMN_REVERSE, 27 | 'row-reverse': yoga.FLEX_DIRECTION_ROW_REVERSE, 28 | }; 29 | 30 | export const wrapMap = { 31 | wrap: yoga.WRAP_WRAP, 32 | nowrap: yoga.WRAP_NO_WRAP, 33 | 'wrap-reverse': yoga.WRAP_WRAP_REVERSE, 34 | }; 35 | 36 | export const overflowMap = { 37 | hidden: yoga.OVERFLOW_HIDDEN, 38 | scroll: yoga.OVERFLOW_SCROLL, 39 | visible: yoga.OVERFLOW_VISIBLE, 40 | }; 41 | 42 | export const positionMap = { 43 | absolute: yoga.POSITION_TYPE_ABSOLUTE, 44 | relative: yoga.POSITION_TYPE_RELATIVE, 45 | }; 46 | 47 | export const edgeMap = { 48 | left: yoga.EDGE_LEFT, 49 | top: yoga.EDGE_TOP, 50 | right: yoga.EDGE_RIGHT, 51 | bottom: yoga.EDGE_BOTTOM, 52 | start: yoga.EDGE_START, 53 | end: yoga.EDGE_END, 54 | horizontal: yoga.EDGE_HORIZONTAL, 55 | vertical: yoga.EDGE_VERTICAL, 56 | all: yoga.EDGE_ALL, 57 | }; 58 | 59 | export const displayMap = { 60 | flex: yoga.DISPLAY_FLEX, 61 | none: yoga.DISPLAY_NONE, 62 | }; 63 | 64 | export function applyStyles(node, styles) { 65 | Object.keys(styles).forEach(style => { 66 | switch (style) { 67 | case 'flex': 68 | node.setFlex(styles[style]); 69 | break; 70 | case 'display': 71 | node.setDisplay(displayMap[styles[style]]); 72 | break; 73 | case 'height': 74 | if (val === 'auto') { 75 | node.setHeightAuto(); 76 | } else { 77 | node.setHeight(styles[style]); 78 | } 79 | break; 80 | case 'heightPct': 81 | node.setHeightPercentage(styles[style]); 82 | break; 83 | case 'width': 84 | let val = styles[style]; 85 | if (val === 'auto') { 86 | node.setWidthAuto(); 87 | } else { 88 | node.setWidth(styles[style]); 89 | } 90 | break; 91 | case 'widthPct': 92 | node.setWidthPercent(styles[style]); 93 | break; 94 | case 'padding': 95 | node.setPadding(yoga.EDGE_ALL, styles[style]); 96 | break; 97 | case 'paddingPct': 98 | node.setPaddingPercent(yoga.EDGE_ALL, styles[style]); 99 | break; 100 | case 'margin': 101 | node.setMargin(yoga.EDGE_ALL, styles[style]); 102 | break; 103 | case 'alignItems': 104 | node.setAlignItems(alignMap[styles[style]]); 105 | break; 106 | case 'justifyContent': 107 | node.setJustifyContent(justifyMap[styles[style]]); 108 | break; 109 | case 'flexDirection': 110 | node.setFlexDirection(directionMap[styles[style]]); 111 | break; 112 | case 'flexWrap': 113 | node.setFlexWrap(wrapMap[styles[style]]); 114 | break; 115 | case 'flexGrow': 116 | node.setFlexGrow(styles[style]); 117 | break; 118 | case 'flexShrink': 119 | node.setFlexShrink(styles[style]); 120 | break; 121 | case 'flexBasis': 122 | node.setFlexBasis(styles[style]); 123 | break; 124 | case 'flexBasisPct': 125 | node.setFlexBasisPercent(styles[style]); 126 | break; 127 | case 'borderWidth': 128 | node.setBorder(edgeMap.all, styles[style]); 129 | break; 130 | case 'borderLeftWidth': 131 | node.setBorder(edgeMap.left, styles[style]); 132 | break; 133 | case 'borderRightWidth': 134 | node.setBorder(edgeMap.right, styles[style]); 135 | break; 136 | case 'borderTopWidth': 137 | node.setBorder(edgeMap.top, styles[style]); 138 | break; 139 | case 'borderBottomWidth': 140 | node.setBorder(edgeMap.bottom, styles[style]); 141 | break; 142 | case 'top': 143 | node.setPosition(edgeMap.top, styles[style]); 144 | break; 145 | case 'bottom': 146 | node.setPosition(edgeMap.bottom, styles[style]); 147 | break; 148 | case 'left': 149 | node.setPosition(edgeMap.left, styles[style]); 150 | break; 151 | case 'right': 152 | node.setPosition(edgeMap.right, styles[style]); 153 | break; 154 | case 'position': 155 | node.setPositionType(positionMap[styles[style]]); 156 | break; 157 | case 'paddingTop': 158 | node.setPadding(edgeMap.top, styles[style]); 159 | break; 160 | case 'paddingLeft': 161 | node.setPadding(edgeMap.left, styles[style]); 162 | break; 163 | case 'paddingRight': 164 | node.setPadding(edgeMap.right, styles[style]); 165 | break; 166 | case 'paddingBottom': 167 | node.setPadding(edgeMap.bottom, styles[style]); 168 | break; 169 | case 'marginTop': 170 | node.setMargin(edgeMap.top, styles[style]); 171 | break; 172 | case 'marginLeft': 173 | node.setMargin(edgeMap.left, styles[style]); 174 | break; 175 | case 'marginRight': 176 | node.setMargin(edgeMap.right, styles[style]); 177 | break; 178 | case 'marginBottom': 179 | node.setMargin(edgeMap.bottom, styles[style]); 180 | break; 181 | default: 182 | break; 183 | } 184 | }); 185 | } 186 | -------------------------------------------------------------------------------- /src/primitives/text-utils.js: -------------------------------------------------------------------------------- 1 | export function wrapText(context, text, x, y, maxWidth, lineHeight) { 2 | var words = text.split(' '), 3 | line = '', 4 | i, 5 | test, 6 | metrics, 7 | lines = []; 8 | 9 | for (i = 0; i < words.length; i++) { 10 | test = words[i]; 11 | metrics = context.measureText(test); 12 | while (metrics.width > maxWidth) { 13 | // Determine how much of the word will fit 14 | test = test.substring(0, test.length - 1); 15 | metrics = context.measureText(test); 16 | } 17 | if (words[i] != test) { 18 | words.splice(i + 1, 0, words[i].substr(test.length)); 19 | words[i] = test; 20 | } 21 | 22 | test = line + words[i] + ' '; 23 | metrics = context.measureText(test); 24 | 25 | if (metrics.width > maxWidth && i > 0) { 26 | lines.push({ text: line, y }); 27 | line = words[i] + ' '; 28 | y += lineHeight; 29 | } else { 30 | line = test; 31 | } 32 | } 33 | 34 | lines.push({ text: line, y }); 35 | return lines; 36 | } 37 | -------------------------------------------------------------------------------- /src/primitives/tree-utils.js: -------------------------------------------------------------------------------- 1 | export const addToTree = ({ tree, parent, id, props, getProps, type }) => { 2 | const paths = parent.split('|'); 3 | let currentPath = tree; 4 | paths.forEach(p => { 5 | if (p === 'CanvasRoot') { 6 | currentPath = tree; 7 | } else { 8 | if (!currentPath.children[p]) { 9 | currentPath.children[p] = { 10 | children: {}, 11 | props: null, 12 | }; 13 | } 14 | currentPath = currentPath.children[p]; 15 | } 16 | }); 17 | currentPath.children[id] = { 18 | children: currentPath.children[id] ? currentPath.children[id].children : {}, 19 | props: props, 20 | type: type, 21 | getProps: getProps, 22 | }; 23 | }; 24 | 25 | export const removeFromTree = ({ targetPath, parent, id }) => { 26 | const paths = parent.split('|'); 27 | 28 | paths.forEach(path => { 29 | if (path !== 'CanvasRoot') { 30 | targetPath = targetPath.children[path]; 31 | } 32 | }); 33 | 34 | if (targetPath[id]) { 35 | const yogaNode = targetPath[id].node; 36 | const nodeParent = targetPath[id].node.getParent(); 37 | nodeParent.removeChild(yogaNode); 38 | delete targetPath[id]; 39 | } 40 | }; 41 | 42 | export const getChild = ({ layoutTree, parent, id, props }) => { 43 | const paths = parent.split('|'); 44 | let targetPath = { ...layoutTree }; 45 | 46 | paths.forEach(path => { 47 | if (path !== 'CanvasRoot') { 48 | targetPath = targetPath.children[path]; 49 | } 50 | }); 51 | 52 | const target = targetPath.children[id]; 53 | if (!target) { 54 | return null; 55 | } 56 | let { style, children } = props; 57 | 58 | target.props.style = style || {}; 59 | if (target.type === 'Text') { 60 | target.text = 61 | children && typeof children === 'string' ? children.toString() : null; 62 | } 63 | return target; 64 | }; 65 | 66 | export const buildLayoutTree = ({ tree }) => { 67 | let layoutTree = {}; 68 | 69 | function addChild(root, layoutRoot) { 70 | if (root.getProps) { 71 | let { style, children } = root.getProps(); 72 | layoutRoot.props = {}; 73 | layoutRoot.props.style = style || {}; 74 | if (root.type === 'Text') { 75 | layoutRoot.text = 76 | children && typeof children === 'string' ? children.toString() : null; 77 | } 78 | } 79 | if (root.type) { 80 | layoutRoot.type = root.type; 81 | } 82 | if (root.children) { 83 | Object.keys(root.children).forEach(key => { 84 | if (!layoutRoot.children) { 85 | layoutRoot.children = []; 86 | } 87 | layoutRoot.children[key] = {}; 88 | addChild(root.children[key], layoutRoot.children[key]); 89 | }); 90 | } 91 | } 92 | 93 | addChild(tree, layoutTree); 94 | 95 | return layoutTree; 96 | }; 97 | -------------------------------------------------------------------------------- /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 http://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.1/8 is 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 http://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 http://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 | .then(response => { 105 | // Ensure service worker exists, and that we really are getting a JS file. 106 | const contentType = response.headers.get('content-type'); 107 | if ( 108 | response.status === 404 || 109 | (contentType != null && contentType.indexOf('javascript') === -1) 110 | ) { 111 | // No service worker found. Probably a different app. Reload the page. 112 | navigator.serviceWorker.ready.then(registration => { 113 | registration.unregister().then(() => { 114 | window.location.reload(); 115 | }); 116 | }); 117 | } else { 118 | // Service worker found. Proceed as normal. 119 | registerValidSW(swUrl, config); 120 | } 121 | }) 122 | .catch(() => { 123 | console.log( 124 | 'No internet connection found. App is running in offline mode.' 125 | ); 126 | }); 127 | } 128 | 129 | export function unregister() { 130 | if ('serviceWorker' in navigator) { 131 | navigator.serviceWorker.ready.then(registration => { 132 | registration.unregister(); 133 | }); 134 | } 135 | } 136 | --------------------------------------------------------------------------------