├── .env ├── .gitignore ├── deploy.sh ├── package.json ├── patches └── r3f6+6.2.3.patch ├── public └── index.html ├── src ├── components │ ├── three-fiber-custom │ │ ├── events.js │ │ └── index.jsx │ ├── three-fiber-offscreen │ │ ├── consts.js │ │ ├── index.jsx │ │ ├── offscreen-canvas.jsx │ │ └── worker │ │ │ ├── comp.jsx │ │ │ ├── events.js │ │ │ └── index.js │ ├── three-fiber │ │ └── index.jsx │ ├── three-offscreen │ │ ├── index.jsx │ │ └── worker.js │ └── three │ │ └── index.jsx ├── index.js └── styles.css └── yarn.lock /.env: -------------------------------------------------------------------------------- 1 | PUBLIC_URL=/offscreen-canvas-demo/ 2 | GENERATE_SOURCEMAP=false 3 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | dist/ 3 | build/ 4 | node_modules/ 5 | -------------------------------------------------------------------------------- /deploy.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | 3 | # 发生错误时终止 4 | set -e 5 | 6 | # 构建 7 | npm run build 8 | 9 | # 进入构建文件夹 10 | cd build 11 | cp index.html 404.html 12 | 13 | git init 14 | git checkout -b main 15 | git add -A 16 | git commit -m 'deploy' 17 | 18 | git push -f git@github.com:F-loat/offscreen-canvas-demo.git main:gh-pages 19 | 20 | cd - 21 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "offscreen-canvas-demo", 3 | "version": "1.0.0", 4 | "main": "src/index.js", 5 | "dependencies": { 6 | "@react-three/fiber": "^8.0.27", 7 | "@types/three": "^0.140.0", 8 | "mitt": "^3.0.0", 9 | "mobx": "^6.6.0", 10 | "r3f6": "npm:@react-three/fiber@^6.0.0", 11 | "react": "^18.1.0", 12 | "react-dom": "^18.1.0", 13 | "react-scripts": "^5.0.1", 14 | "three": "^0.140.0" 15 | }, 16 | "devDependencies": { 17 | "patch-package": "^6.4.7", 18 | "typescript": "3.8.3" 19 | }, 20 | "scripts": { 21 | "dev": "react-scripts start", 22 | "build": "PUBLIC_URL=/offscreen-canvas-demo/ react-scripts build", 23 | "postinstall": "patch-package" 24 | }, 25 | "browserslist": [ 26 | ">0.2%", 27 | "not dead", 28 | "not ie <= 11", 29 | "not op_mini all" 30 | ] 31 | } 32 | -------------------------------------------------------------------------------- /patches/r3f6+6.2.3.patch: -------------------------------------------------------------------------------- 1 | diff --git a/node_modules/r3f6/dist/react-three-fiber.esm.js b/node_modules/r3f6/dist/react-three-fiber.esm.js 2 | index 768ff87..deec684 100644 3 | --- a/node_modules/r3f6/dist/react-three-fiber.esm.js 4 | +++ b/node_modules/r3f6/dist/react-three-fiber.esm.js 5 | @@ -1258,7 +1258,7 @@ const createStore = (applyProps, invalidate, advance, props) => { 6 | 7 | 8 | gl.setPixelRatio(viewport.dpr); 9 | - gl.setSize(size.width, size.height); 10 | + gl.setSize(size.width, size.height, false); 11 | }, state => [state.viewport.dpr, state.size], shallow); 12 | const state = rootState.getState(); // Update size 13 | 14 | @@ -1778,4 +1778,4 @@ reconciler.injectIntoDevTools({ 15 | version: '17.0.2' 16 | }); 17 | 18 | -export { Canvas, threeTypes as ReactThreeFiber, roots as _roots, act, addAfterEffect, addEffect, addTail, advance, applyProps, context, createPortal, dispose, createPointerEvents as events, extend, invalidate, reconciler, render, unmountComponentAtNode, useFrame, useGraph, useLoader, useThree }; 19 | +export { Canvas, threeTypes as ReactThreeFiber, roots as _roots, act, addAfterEffect, addEffect, addTail, advance, applyProps, context, createPortal, dispose, createPointerEvents as events, createEvents, extend, invalidate, reconciler, render, unmountComponentAtNode, useFrame, useGraph, useLoader, useThree }; 20 | -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | React App 9 | 10 | 11 | 14 |
15 | 16 | -------------------------------------------------------------------------------- /src/components/three-fiber-custom/events.js: -------------------------------------------------------------------------------- 1 | import { createEvents } from '@react-three/fiber' 2 | 3 | const DOM_EVENTS = { 4 | onClick: ['click', false], 5 | onContextMenu: ['contextmenu', false], 6 | onDoubleClick: ['dblclick', false], 7 | onWheel: ['wheel', true], 8 | onPointerDown: ['pointerdown', true], 9 | onPointerUp: ['pointerup', true], 10 | onPointerLeave: ['pointerleave', true], 11 | onPointerMove: ['pointermove', true], 12 | onPointerCancel: ['pointercancel', true], 13 | onLostPointerCapture: ['lostpointercapture', true], 14 | } 15 | 16 | /** Default R3F event manager for web */ 17 | export function createPointerEvents(store) { 18 | const { handlePointer } = createEvents(store) 19 | 20 | return { 21 | priority: 1, 22 | enabled: true, 23 | compute(event, state) { 24 | // https://github.com/pmndrs/react-three-fiber/pull/782 25 | // Events trigger outside of canvas when moved, use offsetX/Y by default and allow overrides 26 | state.pointer.set((event.offsetX / state.size.width) * 2 - 1, -(event.offsetY / state.size.height) * 2 + 1) 27 | state.raycaster.setFromCamera(state.pointer, state.camera) 28 | }, 29 | 30 | connected: undefined, 31 | handlers: Object.keys(DOM_EVENTS).reduce( 32 | (acc, key) => ({ ...acc, [key]: handlePointer(key) }), 33 | {}, 34 | ), 35 | connect: (target) => { 36 | const { set, events } = store.getState() 37 | events.disconnect?.() 38 | set((state) => ({ events: { ...state.events, connected: target } })) 39 | Object.entries(events?.handlers ?? []).forEach(([name, event]) => { 40 | const [eventName, passive] = DOM_EVENTS[name] 41 | target.addEventListener(eventName, event, { passive }) 42 | }) 43 | }, 44 | disconnect: () => { 45 | const { set, events } = store.getState() 46 | if (events.connected) { 47 | Object.entries(events.handlers ?? []).forEach(([name, event]) => { 48 | if (events && events.connected instanceof HTMLElement) { 49 | const [eventName] = DOM_EVENTS[name] 50 | events.connected.removeEventListener(eventName, event) 51 | } 52 | }) 53 | set((state) => ({ events: { ...state.events, connected: undefined } })) 54 | } 55 | }, 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /src/components/three-fiber-custom/index.jsx: -------------------------------------------------------------------------------- 1 | import React, { useRef, useState, useEffect } from 'react'; 2 | import * as THREE from 'three' 3 | import { extend, useFrame, createRoot } from '@react-three/fiber' 4 | import { createPointerEvents } from './events' 5 | 6 | extend(THREE) 7 | 8 | function Cube(props) { 9 | // This reference will give us direct access to the mesh 10 | const mesh = useRef() 11 | // Set up state for the hovered and active state 12 | const [hovered, setHover] = useState(false) 13 | const [active, setActive] = useState(false) 14 | // Subscribe this component to the render-loop, rotate the mesh every frame 15 | useFrame((state, delta) => { 16 | mesh.current.rotation.x += 0.01 17 | mesh.current.rotation.y += 0.01 18 | }) 19 | // Return view, these are regular three.js elements expressed in JSX 20 | return ( 21 | setActive(!active)} 26 | onPointerOver={() => setHover(true)} 27 | onPointerOut={() => setHover(false)}> 28 | 29 | 30 | 31 | ) 32 | } 33 | 34 | const App = () => { 35 | return ( 36 | <> 37 | 38 | 39 | 40 | 41 | ); 42 | } 43 | 44 | const AppWraper = () => { 45 | const canvasRef = useRef() 46 | 47 | useEffect(() => { 48 | const canvas = canvasRef.current; 49 | const root = createRoot(canvasRef.current) 50 | 51 | root.configure({ 52 | events: createPointerEvents, 53 | size: { 54 | width: canvas.clientWidth, 55 | height: canvas.clientHeight, 56 | } 57 | }) 58 | 59 | root.render() 60 | 61 | return () => { 62 | location.reload() 63 | } 64 | }, []) 65 | 66 | return ( 67 | 68 | ) 69 | } 70 | 71 | export default AppWraper 72 | -------------------------------------------------------------------------------- /src/components/three-fiber-offscreen/consts.js: -------------------------------------------------------------------------------- 1 | export const DOM_EVENTS = { 2 | onClick: ['click', false], 3 | onContextMenu: ['contextmenu', false], 4 | onDoubleClick: ['dblclick', false], 5 | onWheel: ['wheel', true], 6 | onPointerDown: ['pointerdown', true], 7 | onPointerUp: ['pointerup', true], 8 | onPointerLeave: ['pointerleave', true], 9 | onPointerMove: ['pointermove', true], 10 | onPointerCancel: ['pointercancel', true], 11 | onLostPointerCapture: ['lostpointercapture', true], 12 | } 13 | -------------------------------------------------------------------------------- /src/components/three-fiber-offscreen/index.jsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react' 2 | import OffscreenCanvas from './offscreen-canvas' 3 | 4 | const worker = new Worker(new URL('./worker/index.js', import.meta.url)) 5 | 6 | const App = () => { 7 | const [position, setPosition] = useState([0, 0, 0]); 8 | 9 | const handleClick = () => { 10 | setPosition([Math.random() * 2 - 1, Math.random() * 2 - 1, 0]) 11 | } 12 | 13 | return ( 14 | 19 | ) 20 | } 21 | 22 | export default App 23 | -------------------------------------------------------------------------------- /src/components/three-fiber-offscreen/offscreen-canvas.jsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useRef } from 'react'; 2 | import { DOM_EVENTS } from './consts' 3 | 4 | const OffscreenCanvas = ({ 5 | onClick, 6 | worker, 7 | ...props 8 | }) => { 9 | const canvasRef = useRef(); 10 | 11 | useEffect(() => { 12 | if (!worker) return; 13 | 14 | const canvas = canvasRef.current; 15 | const offscreen = canvasRef.current.transferControlToOffscreen(); 16 | 17 | worker.postMessage( { 18 | type: 'init', 19 | payload: { 20 | props, 21 | drawingSurface: offscreen, 22 | width: canvas.clientWidth, 23 | height: canvas.clientHeight, 24 | pixelRatio: window.devicePixelRatio, 25 | } 26 | }, [ offscreen ] ); 27 | 28 | Object.values(DOM_EVENTS).forEach(([eventName, passive]) => { 29 | canvas.addEventListener(eventName, (event) => { 30 | worker.postMessage({ 31 | type: 'dom_events', 32 | payload: { 33 | eventName, 34 | clientX: event.clientX, 35 | clientY: event.clientY, 36 | offsetX: event.offsetX, 37 | offsetY: event.offsetY, 38 | x: event.x, 39 | y: event.y, 40 | } 41 | }); 42 | }, { passive }) 43 | }) 44 | 45 | const handleResize = () => { 46 | worker.postMessage({ 47 | type: 'resize', 48 | payload: { 49 | width: canvas.clientWidth, 50 | height: canvas.clientHeight, 51 | } 52 | }) 53 | } 54 | 55 | window.addEventListener('resize', handleResize) 56 | 57 | return () => { 58 | window.removeEventListener('resize', handleResize) 59 | } 60 | }, [worker]) 61 | 62 | useEffect(() => { 63 | if (!worker) return; 64 | worker.postMessage({ 65 | type: 'props', 66 | payload: props 67 | }) 68 | }, [props]) 69 | 70 | return ( 71 | 72 | ); 73 | } 74 | 75 | export default OffscreenCanvas 76 | -------------------------------------------------------------------------------- /src/components/three-fiber-offscreen/worker/comp.jsx: -------------------------------------------------------------------------------- 1 | import React, { useRef, useState } from 'react'; 2 | import { useFrame } from '@react-three/fiber' 3 | 4 | function Cube(props) { 5 | const mesh = useRef() 6 | 7 | const [hovered, setHover] = useState(false) 8 | const [active, setActive] = useState(false) 9 | 10 | useFrame((state, delta) => { 11 | mesh.current.rotation.x += 0.01 12 | mesh.current.rotation.y += 0.01 13 | }) 14 | 15 | return ( 16 | setActive(!active)} 21 | onPointerOver={() => setHover(true)} 22 | onPointerOut={() => setHover(false)}> 23 | 24 | 25 | 26 | ) 27 | } 28 | 29 | const Comp = ({ position }) => { 30 | return ( 31 | <> 32 | 33 | 34 | 35 | 36 | ); 37 | } 38 | 39 | export default Comp 40 | -------------------------------------------------------------------------------- /src/components/three-fiber-offscreen/worker/events.js: -------------------------------------------------------------------------------- 1 | import mitt from 'mitt' 2 | import { createEvents } from '@react-three/fiber' 3 | import { DOM_EVENTS } from '../consts' 4 | 5 | export const emitter = mitt() 6 | 7 | /** R3F event manager for web offscreen canvas */ 8 | export function createPointerEvents(store) { 9 | const { handlePointer } = createEvents(store) 10 | 11 | return { 12 | priority: 1, 13 | enabled: true, 14 | compute(event, state) { 15 | // https://github.com/pmndrs/react-three-fiber/pull/782 16 | // Events trigger outside of canvas when moved, use offsetX/Y by default and allow overrides 17 | state.pointer.set((event.offsetX / state.size.width) * 2 - 1, -(event.offsetY / state.size.height) * 2 + 1) 18 | state.raycaster.setFromCamera(state.pointer, state.camera) 19 | }, 20 | 21 | connected: undefined, 22 | handlers: Object.keys(DOM_EVENTS).reduce( 23 | (acc, key) => ({ ...acc, [key]: handlePointer(key) }), 24 | {}, 25 | ), 26 | connect: (target) => { 27 | const { set, events } = store.getState() 28 | events.disconnect?.() 29 | set((state) => ({ events: { ...state.events, connected: target } })) 30 | Object.entries(events?.handlers ?? []).forEach(([name, event]) => { 31 | const [eventName] = DOM_EVENTS[name] 32 | emitter.on(eventName, event) 33 | }) 34 | }, 35 | disconnect: () => { 36 | const { set, events } = store.getState() 37 | if (events.connected) { 38 | Object.entries(events.handlers ?? []).forEach(([name, event]) => { 39 | const [eventName] = DOM_EVENTS[name] 40 | emitter.off(eventName, event) 41 | }) 42 | emitter.emit('disconnect') 43 | set((state) => ({ events: { ...state.events, connected: undefined } })) 44 | } 45 | }, 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/components/three-fiber-offscreen/worker/index.js: -------------------------------------------------------------------------------- 1 | import { useState, useEffect } from 'react' 2 | import * as THREE from 'three' 3 | import { extend, createRoot } from '@react-three/fiber' 4 | // import { extend, render } from 'r3f6' 5 | import { emitter, createPointerEvents } from './events' 6 | import Comp from './comp' 7 | 8 | extend(THREE) 9 | 10 | let root; 11 | 12 | const CompWrapper = (initialProps) => { 13 | const [store, setStore] = useState({}) 14 | const [props, setProps] = useState(initialProps) 15 | 16 | useEffect(() => { 17 | emitter.on('props', p => { 18 | setProps(p) 19 | setStore({ props: p }) 20 | }) 21 | return () => { 22 | emitter.off('props', setProps) 23 | } 24 | }, []) 25 | 26 | return 27 | } 28 | 29 | const handleInit = (payload) => { 30 | const { props, drawingSurface: canvas, width, height, pixelRatio } = payload; 31 | 32 | root = createRoot(canvas) 33 | 34 | root.configure({ 35 | events: createPointerEvents, 36 | size: { 37 | width, 38 | height, 39 | updateStyle: false 40 | }, 41 | dpr: pixelRatio, 42 | }) 43 | 44 | root.render() 45 | 46 | // r3f6 47 | // render(, canvas, { 48 | // events: createPointerEvents, 49 | // size: { 50 | // width, 51 | // height, 52 | // updateStyle: false 53 | // }, 54 | // dpr: pixelRatio, 55 | // }) 56 | } 57 | 58 | const handleResize = ({ width, height }) => { 59 | if (!root) return; 60 | root.configure({ 61 | size: { 62 | width, 63 | height, 64 | updateStyle: false 65 | }, 66 | }) 67 | } 68 | 69 | const handleEvents = (payload) => { 70 | emitter.emit(payload.eventName, payload) 71 | emitter.on('disconnect', () => { 72 | self.postMessage({ type: 'dom_events_disconnect' }) 73 | }) 74 | } 75 | 76 | const handleProps = (payload) => { 77 | emitter.emit('props', payload) 78 | } 79 | 80 | const handlerMap = { 81 | 'resize': handleResize, 82 | 'init': handleInit, 83 | 'dom_events': handleEvents, 84 | 'props': handleProps, 85 | } 86 | 87 | self.onmessage = (event) => { 88 | const { type, payload } = event.data 89 | const handler = handlerMap[type] 90 | if (handler) handler(payload) 91 | } 92 | 93 | self.window = {} 94 | -------------------------------------------------------------------------------- /src/components/three-fiber/index.jsx: -------------------------------------------------------------------------------- 1 | import React, { useRef, useState } from 'react'; 2 | import { Canvas, useFrame } from '@react-three/fiber' 3 | 4 | function Cube(props) { 5 | // This reference will give us direct access to the mesh 6 | const mesh = useRef() 7 | // Set up state for the hovered and active state 8 | const [hovered, setHover] = useState(false) 9 | const [active, setActive] = useState(false) 10 | // Subscribe this component to the render-loop, rotate the mesh every frame 11 | useFrame((state, delta) => { 12 | mesh.current.rotation.x += 0.01 13 | mesh.current.rotation.y += 0.01 14 | }) 15 | // Return view, these are regular three.js elements expressed in JSX 16 | return ( 17 | setActive(!active)} 22 | onPointerOver={() => setHover(true)} 23 | onPointerOut={() => setHover(false)}> 24 | 25 | 26 | 27 | ) 28 | } 29 | 30 | const App = () => { 31 | return ( 32 | 33 | 34 | 35 | 36 | 37 | ); 38 | } 39 | 40 | export default App 41 | -------------------------------------------------------------------------------- /src/components/three-offscreen/index.jsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect } from 'react'; 2 | 3 | const App = () => { 4 | const canvasRef = React.useRef(); 5 | 6 | useEffect(() => { 7 | const canvas = canvasRef.current; 8 | const offscreen = canvasRef.current.transferControlToOffscreen(); 9 | 10 | const worker = new Worker(new URL('./worker.js', import.meta.url)); 11 | 12 | worker.postMessage( { 13 | drawingSurface: offscreen, 14 | width: canvas.clientWidth, 15 | height: canvas.clientHeight, 16 | pixelRatio: window.devicePixelRatio, 17 | }, [ offscreen ] ); 18 | }, []); 19 | 20 | return ( 21 | 22 | ); 23 | } 24 | 25 | export default App 26 | -------------------------------------------------------------------------------- /src/components/three-offscreen/worker.js: -------------------------------------------------------------------------------- 1 | import * as THREE from "three" 2 | 3 | self.onmessage = function ( message ) { 4 | const { drawingSurface: canvas, width, height, pixelRatio } = message.data 5 | 6 | const scene = new THREE.Scene(); 7 | 8 | const camera = new THREE.PerspectiveCamera( 75, width / height, 0.1, 1000 ); 9 | camera.position.z = 5; 10 | 11 | const renderer = new THREE.WebGLRenderer( { 12 | antialias: true, 13 | canvas, 14 | }); 15 | renderer.setPixelRatio( pixelRatio ); 16 | renderer.setSize( width, height, false ); 17 | renderer.setClearColor( 0xffffff, 1 ); 18 | 19 | const geometry = new THREE.BoxGeometry( 1, 1, 1 ); 20 | const material = new THREE.MeshStandardMaterial( { color: 'orange' } ); 21 | const cube = new THREE.Mesh( geometry, material ); 22 | cube.position.set( 0, 0, 0 ) 23 | scene.add( cube ); 24 | 25 | const ambientLight = new THREE.AmbientLight(); 26 | scene.add( ambientLight ); 27 | 28 | const pointLight = new THREE.PointLight(); 29 | pointLight.position.set( 10, 10, 10 ); 30 | scene.add( pointLight ); 31 | 32 | function animate() { 33 | requestAnimationFrame( animate ); 34 | 35 | cube.rotation.x += 0.01; 36 | cube.rotation.y += 0.01; 37 | 38 | renderer.render( scene, camera ); 39 | }; 40 | 41 | animate(); 42 | }; 43 | -------------------------------------------------------------------------------- /src/components/three/index.jsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useRef } from 'react'; 2 | import * as THREE from 'three' 3 | 4 | const App = () => { 5 | const canvasRef = useRef(); 6 | const hoveredRef = useRef(false) 7 | 8 | const init = ({ 9 | drawingSurface: canvas, 10 | width, 11 | height, 12 | pixelRatio, 13 | }) => { 14 | const scene = new THREE.Scene(); 15 | 16 | const camera = new THREE.PerspectiveCamera( 75, width / height, 0.1, 1000 ); 17 | camera.position.z = 5; 18 | 19 | const renderer = new THREE.WebGLRenderer( { 20 | antialias: true, 21 | canvas, 22 | }); 23 | renderer.setPixelRatio( pixelRatio ); 24 | renderer.setSize( width, height, false ); 25 | renderer.setClearColor( 0xffffff, 1 ); 26 | 27 | const geometry = new THREE.BoxGeometry( 1, 1, 1 ); 28 | const material = new THREE.MeshStandardMaterial( { color: 'orange' } ); 29 | const cube = new THREE.Mesh( geometry, material ); 30 | cube.position.set( 0, 0, 0 ) 31 | scene.add( cube ); 32 | 33 | const ambientLight = new THREE.AmbientLight(); 34 | scene.add( ambientLight ); 35 | 36 | const pointLight = new THREE.PointLight(); 37 | pointLight.position.set( 10, 10, 10 ); 38 | scene.add( pointLight ); 39 | 40 | function animate() { 41 | requestAnimationFrame( animate ); 42 | 43 | cube.rotation.x += 0.01; 44 | cube.rotation.y += 0.01; 45 | material.color.set(hoveredRef.current ? 'hotpink' : 'orange') 46 | 47 | renderer.render( scene, camera ); 48 | }; 49 | 50 | animate(); 51 | } 52 | 53 | useEffect(() => { 54 | const canvas = canvasRef.current; 55 | 56 | init({ 57 | drawingSurface: canvas, 58 | width: canvas.clientWidth, 59 | height: canvas.clientHeight, 60 | pixelRatio: window.devicePixelRatio, 61 | }) 62 | 63 | return () => { 64 | location.reload() 65 | } 66 | }, []); 67 | 68 | return ( 69 | hoveredRef.current = !hoveredRef.current} 72 | /> 73 | ); 74 | } 75 | 76 | export default App 77 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import { createRoot } from 'react-dom/client' 2 | import { Suspense, useState } from 'react' 3 | import Three from './components/three' 4 | import ThreeOffscreen from './components/three-offscreen' 5 | import ThreeFiber from './components/three-fiber' 6 | import ThreeFiberCustom from './components/three-fiber-custom' 7 | import ThreeFiberOffscreen from './components/three-fiber-offscreen' 8 | import './styles.css' 9 | 10 | const { pathname } = location 11 | 12 | function jank() { 13 | for ( let i = 0; i < 10000000; i ++ ) { 14 | Math.random() 15 | } 16 | } 17 | 18 | const routes = { 19 | '/offscreen-canvas-demo/three': Three, 20 | '/offscreen-canvas-demo/three-offscreen': ThreeOffscreen, 21 | '/offscreen-canvas-demo/three-fiber': ThreeFiber, 22 | '/offscreen-canvas-demo/three-fiber-custom': ThreeFiberCustom, 23 | '/offscreen-canvas-demo/three-fiber-offscreen': ThreeFiberOffscreen 24 | } 25 | 26 | const Comp = routes[pathname] 27 | 28 | let interval = null 29 | 30 | const Root = () => { 31 | const [count, setCount] = useState(0) 32 | 33 | const toggleJank = () => { 34 | if (interval === null) { 35 | interval = setInterval(() => { 36 | jank() 37 | setCount(count => count + 1) 38 | }, 1000 / 60) 39 | } else { 40 | clearInterval(interval) 41 | interval = null 42 | setCount(0) 43 | } 44 | } 45 | 46 | return ( 47 | 48 | {Comp ? : ( 49 |
50 | 51 | 52 |
53 | )} 54 |
55 | 58 | {count ?
{count}
: null} 59 |
60 | 71 |
72 | ) 73 | } 74 | 75 | createRoot(document.getElementById('root')).render() 76 | -------------------------------------------------------------------------------- /src/styles.css: -------------------------------------------------------------------------------- 1 | html, body { 2 | margin: 0; 3 | height: 100%; 4 | color: #333; 5 | } 6 | 7 | #root, canvas { 8 | width: 100%; 9 | height: 100%; 10 | overflow: hidden; 11 | } 12 | 13 | .flex { 14 | display: flex; 15 | } 16 | 17 | .flex-1 { 18 | flex: 1 1 0%; 19 | } 20 | 21 | .counter { 22 | position: fixed; 23 | right: 20px; 24 | top: 20px; 25 | text-align: right; 26 | } 27 | 28 | .counter button { 29 | border: none; 30 | padding: 12px; 31 | border-radius: 4px; 32 | cursor: pointer; 33 | } 34 | 35 | .nav { 36 | position: fixed; 37 | bottom: 20px; 38 | right: 20px; 39 | color: #aaa; 40 | } 41 | 42 | a { 43 | color: #aaa; 44 | } 45 | 46 | a:hover { 47 | color: #666; 48 | } 49 | 50 | .highlight { 51 | color: red; 52 | } 53 | --------------------------------------------------------------------------------