├── .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 |
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 |