├── .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 | You need to enable JavaScript to run this app.
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 |
--------------------------------------------------------------------------------