@editframe/react 0.37.3-beta → 0.38.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/components/TimelineRoot.d.ts +19 -12
- package/dist/components/TimelineRoot.js +34 -34
- package/dist/components/TimelineRoot.js.map +1 -1
- package/dist/gui/OverlayItem.js.map +1 -1
- package/dist/gui/Scrubber.d.ts +2 -0
- package/dist/gui/Scrubber.js.map +1 -1
- package/dist/{elements → gui}/ThumbnailStrip.d.ts +1 -1
- package/dist/{elements → gui}/ThumbnailStrip.js +2 -4
- package/dist/gui/ThumbnailStrip.js.map +1 -0
- package/dist/gui/TimelineRuler.js.map +1 -1
- package/dist/gui/TrimHandles.d.ts +11 -0
- package/dist/gui/TrimHandles.js +18 -0
- package/dist/gui/TrimHandles.js.map +1 -0
- package/dist/hooks/create-element.d.ts +10 -3
- package/dist/hooks/create-element.js +4 -1
- package/dist/hooks/create-element.js.map +1 -1
- package/dist/hooks/useMediaInfo.d.ts +15 -0
- package/dist/hooks/useMediaInfo.js +59 -0
- package/dist/hooks/useMediaInfo.js.map +1 -0
- package/dist/hooks/usePanZoomTransform.js.map +1 -1
- package/dist/hooks/useTimingInfo.d.ts +2 -2
- package/dist/hooks/useTimingInfo.js +12 -11
- package/dist/hooks/useTimingInfo.js.map +1 -1
- package/dist/index.d.ts +5 -3
- package/dist/index.js +4 -2
- package/dist/r3f/CompositionCanvas.d.ts +32 -0
- package/dist/r3f/CompositionCanvas.js +94 -0
- package/dist/r3f/CompositionCanvas.js.map +1 -0
- package/dist/r3f/OffscreenCompositionCanvas.d.ts +28 -0
- package/dist/r3f/OffscreenCompositionCanvas.js +119 -0
- package/dist/r3f/OffscreenCompositionCanvas.js.map +1 -0
- package/dist/r3f/index.d.ts +5 -0
- package/dist/r3f/index.js +5 -0
- package/dist/r3f/renderOffscreen.d.ts +27 -0
- package/dist/r3f/renderOffscreen.js +291 -0
- package/dist/r3f/renderOffscreen.js.map +1 -0
- package/dist/r3f/worker-protocol.d.ts +39 -0
- package/dist/server.d.ts +11 -0
- package/dist/server.js +11 -0
- package/package.json +45 -11
- package/tsdown.config.ts +1 -0
- package/dist/elements/ThumbnailStrip.js.map +0 -1
package/dist/index.js
CHANGED
|
@@ -4,7 +4,6 @@ import { Captions, CaptionsActiveWord, CaptionsAfterActiveWord, CaptionsBeforeAc
|
|
|
4
4
|
import { Text, TextSegment } from "./elements/Text.js";
|
|
5
5
|
import { Image } from "./elements/Image.js";
|
|
6
6
|
import { Surface } from "./elements/Surface.js";
|
|
7
|
-
import { ThumbnailStrip } from "./elements/ThumbnailStrip.js";
|
|
8
7
|
import { Timegroup } from "./elements/Timegroup.js";
|
|
9
8
|
import { TimelineRoot } from "./components/TimelineRoot.js";
|
|
10
9
|
import { Video } from "./elements/Video.js";
|
|
@@ -24,12 +23,15 @@ import { Pause } from "./gui/Pause.js";
|
|
|
24
23
|
import { Play } from "./gui/Play.js";
|
|
25
24
|
import { Preview } from "./gui/Preview.js";
|
|
26
25
|
import { Scrubber } from "./gui/Scrubber.js";
|
|
26
|
+
import { ThumbnailStrip } from "./gui/ThumbnailStrip.js";
|
|
27
|
+
import { TrimHandles } from "./gui/TrimHandles.js";
|
|
27
28
|
import { TimelineRuler } from "./gui/TimelineRuler.js";
|
|
28
29
|
import { ToggleLoop } from "./gui/ToggleLoop.js";
|
|
29
30
|
import { TogglePlay } from "./gui/TogglePlay.js";
|
|
30
31
|
import { Workbench } from "./gui/Workbench.js";
|
|
31
32
|
import { useTimingInfo } from "./hooks/useTimingInfo.js";
|
|
33
|
+
import { useMediaInfo } from "./hooks/useMediaInfo.js";
|
|
32
34
|
import { usePanZoomTransform } from "./hooks/usePanZoomTransform.js";
|
|
33
35
|
import { elementNeedsFitScale, needsFitScale } from "@editframe/elements";
|
|
34
36
|
|
|
35
|
-
export { Audio, Captions, CaptionsActiveWord, CaptionsAfterActiveWord, CaptionsBeforeActiveWord, CaptionsSegment, Configuration, Controls, Dial, Filmstrip, FitScale, FocusOverlay, Image, OverlayItem, OverlayLayer, PanZoom, Pause, Play, Preview, ResizableBox, Scrubber, Surface, Text, TextSegment, ThumbnailStrip, TimeDisplay, Timegroup, TimelineRoot, TimelineRuler, ToggleLoop, TogglePlay, TransformHandles, Video, Waveform, Workbench, elementNeedsFitScale, needsFitScale, usePanZoomTransform, useTimingInfo };
|
|
37
|
+
export { Audio, Captions, CaptionsActiveWord, CaptionsAfterActiveWord, CaptionsBeforeActiveWord, CaptionsSegment, Configuration, Controls, Dial, Filmstrip, FitScale, FocusOverlay, Image, OverlayItem, OverlayLayer, PanZoom, Pause, Play, Preview, ResizableBox, Scrubber, Surface, Text, TextSegment, ThumbnailStrip, TimeDisplay, Timegroup, TimelineRoot, TimelineRuler, ToggleLoop, TogglePlay, TransformHandles, TrimHandles, Video, Waveform, Workbench, elementNeedsFitScale, needsFitScale, useMediaInfo, usePanZoomTransform, useTimingInfo };
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import * as React$1 from "react";
|
|
2
|
+
import * as react_jsx_runtime0 from "react/jsx-runtime";
|
|
3
|
+
import { CanvasProps } from "@react-three/fiber";
|
|
4
|
+
|
|
5
|
+
//#region src/r3f/CompositionCanvas.d.ts
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Hook to read the current composition time inside an R3F scene.
|
|
9
|
+
* Must be used within a `<CompositionCanvas>`.
|
|
10
|
+
*
|
|
11
|
+
* @returns { timeMs, durationMs } — current time and total duration in ms
|
|
12
|
+
*/
|
|
13
|
+
declare function useCompositionTime(): {
|
|
14
|
+
timeMs: number;
|
|
15
|
+
durationMs: number;
|
|
16
|
+
};
|
|
17
|
+
interface CompositionCanvasProps extends Omit<CanvasProps, "frameloop"> {
|
|
18
|
+
/** Extra styles for the container div */
|
|
19
|
+
containerStyle?: React$1.CSSProperties;
|
|
20
|
+
/** Extra className for the container div */
|
|
21
|
+
containerClassName?: string;
|
|
22
|
+
}
|
|
23
|
+
declare function CompositionCanvas({
|
|
24
|
+
children,
|
|
25
|
+
containerStyle,
|
|
26
|
+
containerClassName,
|
|
27
|
+
gl: glProp,
|
|
28
|
+
...canvasProps
|
|
29
|
+
}: CompositionCanvasProps): react_jsx_runtime0.JSX.Element;
|
|
30
|
+
//#endregion
|
|
31
|
+
export { CompositionCanvas, CompositionCanvasProps, useCompositionTime };
|
|
32
|
+
//# sourceMappingURL=CompositionCanvas.d.ts.map
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import { createContext, useContext, useEffect, useLayoutEffect, useRef, useState } from "react";
|
|
2
|
+
import { flushSync } from "react-dom";
|
|
3
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
4
|
+
import { Canvas, useFrame, useThree } from "@react-three/fiber";
|
|
5
|
+
|
|
6
|
+
//#region src/r3f/CompositionCanvas.tsx
|
|
7
|
+
const CompositionTimeContext = createContext({
|
|
8
|
+
timeMs: 0,
|
|
9
|
+
durationMs: 0
|
|
10
|
+
});
|
|
11
|
+
/**
|
|
12
|
+
* Hook to read the current composition time inside an R3F scene.
|
|
13
|
+
* Must be used within a `<CompositionCanvas>`.
|
|
14
|
+
*
|
|
15
|
+
* @returns { timeMs, durationMs } — current time and total duration in ms
|
|
16
|
+
*/
|
|
17
|
+
function useCompositionTime() {
|
|
18
|
+
return useContext(CompositionTimeContext);
|
|
19
|
+
}
|
|
20
|
+
function GLSync() {
|
|
21
|
+
const { gl } = useThree();
|
|
22
|
+
useFrame(() => {
|
|
23
|
+
gl.getContext().finish();
|
|
24
|
+
});
|
|
25
|
+
return null;
|
|
26
|
+
}
|
|
27
|
+
function InvalidateOnTimeChange({ timeMs }) {
|
|
28
|
+
const { invalidate } = useThree();
|
|
29
|
+
useLayoutEffect(() => {
|
|
30
|
+
invalidate();
|
|
31
|
+
}, [timeMs, invalidate]);
|
|
32
|
+
return null;
|
|
33
|
+
}
|
|
34
|
+
function CompositionCanvas({ children, containerStyle, containerClassName, gl: glProp,...canvasProps }) {
|
|
35
|
+
const [timeMs, setTimeMs] = useState(0);
|
|
36
|
+
const [durationMs, setDurationMs] = useState(0);
|
|
37
|
+
const containerRef = useRef(null);
|
|
38
|
+
useEffect(() => {
|
|
39
|
+
const el = containerRef.current;
|
|
40
|
+
if (!el) return;
|
|
41
|
+
const tg = el.closest("ef-timegroup");
|
|
42
|
+
if (!tg) {
|
|
43
|
+
console.warn("[CompositionCanvas] No ef-timegroup ancestor found. Wrap CompositionCanvas inside a <Timegroup>.");
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
if (tg.durationMs) setDurationMs(tg.durationMs);
|
|
47
|
+
return tg.addFrameTask?.(({ ownCurrentTimeMs, durationMs: dur }) => {
|
|
48
|
+
flushSync(() => {
|
|
49
|
+
setTimeMs(ownCurrentTimeMs);
|
|
50
|
+
setDurationMs(dur);
|
|
51
|
+
});
|
|
52
|
+
});
|
|
53
|
+
}, []);
|
|
54
|
+
const mergedGl = typeof glProp === "object" ? {
|
|
55
|
+
preserveDrawingBuffer: true,
|
|
56
|
+
...glProp
|
|
57
|
+
} : glProp ?? { preserveDrawingBuffer: true };
|
|
58
|
+
return /* @__PURE__ */ jsx("div", {
|
|
59
|
+
ref: containerRef,
|
|
60
|
+
className: containerClassName,
|
|
61
|
+
style: {
|
|
62
|
+
position: "absolute",
|
|
63
|
+
inset: 0,
|
|
64
|
+
width: "100%",
|
|
65
|
+
height: "100%",
|
|
66
|
+
...containerStyle
|
|
67
|
+
},
|
|
68
|
+
children: /* @__PURE__ */ jsx(Canvas, {
|
|
69
|
+
frameloop: "demand",
|
|
70
|
+
gl: mergedGl,
|
|
71
|
+
...canvasProps,
|
|
72
|
+
style: {
|
|
73
|
+
width: "100%",
|
|
74
|
+
height: "100%",
|
|
75
|
+
...canvasProps.style
|
|
76
|
+
},
|
|
77
|
+
children: /* @__PURE__ */ jsxs(CompositionTimeContext.Provider, {
|
|
78
|
+
value: {
|
|
79
|
+
timeMs,
|
|
80
|
+
durationMs
|
|
81
|
+
},
|
|
82
|
+
children: [
|
|
83
|
+
/* @__PURE__ */ jsx(GLSync, {}),
|
|
84
|
+
/* @__PURE__ */ jsx(InvalidateOnTimeChange, { timeMs }),
|
|
85
|
+
children
|
|
86
|
+
]
|
|
87
|
+
})
|
|
88
|
+
})
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
//#endregion
|
|
93
|
+
export { CompositionCanvas, useCompositionTime };
|
|
94
|
+
//# sourceMappingURL=CompositionCanvas.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"CompositionCanvas.js","names":[],"sources":["../../src/r3f/CompositionCanvas.tsx"],"sourcesContent":["/**\n * CompositionCanvas — R3F Canvas that automatically bridges\n * Editframe composition time into the 3D scene.\n *\n * Handles: addFrameTask → React state, preserveDrawingBuffer,\n * gl.finish(), frameloop=\"demand\", and invalidation.\n *\n * Usage:\n * ```tsx\n * <Timegroup mode=\"fixed\" duration=\"14s\">\n * <CompositionCanvas shadows>\n * <MyScene />\n * </CompositionCanvas>\n * </Timegroup>\n * ```\n *\n * Inside scene components, use `useCompositionTime()` to read the\n * current composition time in milliseconds.\n */\n\nimport * as React from \"react\";\nimport {\n createContext,\n useContext,\n useEffect,\n useLayoutEffect,\n useRef,\n useState,\n} from \"react\";\nimport { Canvas, useThree, useFrame } from \"@react-three/fiber\";\nimport { flushSync } from \"react-dom\";\nimport type { CanvasProps } from \"@react-three/fiber\";\n\n/* ━━ Context for composition time ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ */\n\nconst CompositionTimeContext = createContext<{\n timeMs: number;\n durationMs: number;\n}>({ timeMs: 0, durationMs: 0 });\n\n/**\n * Hook to read the current composition time inside an R3F scene.\n * Must be used within a `<CompositionCanvas>`.\n *\n * @returns { timeMs, durationMs } — current time and total duration in ms\n */\nexport function useCompositionTime() {\n return useContext(CompositionTimeContext);\n}\n\n/* ━━ Internal: GL sync for renderToVideo ━━━━━━━━━━━━━━━━━━━━━━━━━━ */\n\nfunction GLSync() {\n const { gl } = useThree();\n useFrame(() => {\n gl.getContext().finish();\n });\n return null;\n}\n\n/* ━━ Internal: invalidate on time change ━━━━━━━━━━━━━━━━━━━━━━━━━━ */\n\nfunction InvalidateOnTimeChange({ timeMs }: { timeMs: number }) {\n const { invalidate } = useThree();\n // useLayoutEffect fires synchronously during flushSync, ensuring\n // invalidate() runs before the addFrameTask callback returns.\n useLayoutEffect(() => {\n invalidate();\n }, [timeMs, invalidate]);\n return null;\n}\n\n/* ━━ CompositionCanvas ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ */\n\nexport interface CompositionCanvasProps extends Omit<CanvasProps, \"frameloop\"> {\n /** Extra styles for the container div */\n containerStyle?: React.CSSProperties;\n /** Extra className for the container div */\n containerClassName?: string;\n}\n\nexport function CompositionCanvas({\n children,\n containerStyle,\n containerClassName,\n gl: glProp,\n ...canvasProps\n}: CompositionCanvasProps) {\n const [timeMs, setTimeMs] = useState(0);\n const [durationMs, setDurationMs] = useState(0);\n const containerRef = useRef<HTMLDivElement>(null);\n\n useEffect(() => {\n const el = containerRef.current;\n if (!el) return;\n\n // Walk up to find the ef-timegroup ancestor\n const tg = el.closest(\"ef-timegroup\") as\n | (HTMLElement & {\n addFrameTask?: (\n cb: (info: {\n ownCurrentTimeMs: number;\n durationMs: number;\n }) => void,\n ) => () => void;\n durationMs?: number;\n })\n | null;\n\n if (!tg) {\n console.warn(\n \"[CompositionCanvas] No ef-timegroup ancestor found. \" +\n \"Wrap CompositionCanvas inside a <Timegroup>.\",\n );\n return;\n }\n\n if (tg.durationMs) setDurationMs(tg.durationMs);\n\n const cleanup = tg.addFrameTask?.(\n ({ ownCurrentTimeMs, durationMs: dur }) => {\n // flushSync commits the state update synchronously so the\n // useLayoutEffect → invalidate() fires before we return.\n // R3F's demand render then runs useFrame subscribers (which\n // update instancedMesh matrices, cameras, etc.) and gl.render\n // in a single pass — no duplicate GPU work.\n flushSync(() => {\n setTimeMs(ownCurrentTimeMs);\n setDurationMs(dur);\n });\n },\n );\n\n return cleanup;\n }, []);\n\n // Merge user gl options with required defaults\n const mergedGl =\n typeof glProp === \"object\"\n ? { preserveDrawingBuffer: true, ...glProp }\n : (glProp ?? { preserveDrawingBuffer: true });\n\n return (\n <div\n ref={containerRef}\n className={containerClassName}\n style={{\n position: \"absolute\",\n inset: 0,\n width: \"100%\",\n height: \"100%\",\n ...containerStyle,\n }}\n >\n <Canvas\n frameloop=\"demand\"\n gl={mergedGl}\n {...canvasProps}\n style={{ width: \"100%\", height: \"100%\", ...canvasProps.style }}\n >\n <CompositionTimeContext.Provider value={{ timeMs, durationMs }}>\n <GLSync />\n <InvalidateOnTimeChange timeMs={timeMs} />\n {children}\n </CompositionTimeContext.Provider>\n </Canvas>\n </div>\n );\n}\n"],"mappings":";;;;;;AAmCA,MAAM,yBAAyB,cAG5B;CAAE,QAAQ;CAAG,YAAY;CAAG,CAAC;;;;;;;AAQhC,SAAgB,qBAAqB;AACnC,QAAO,WAAW,uBAAuB;;AAK3C,SAAS,SAAS;CAChB,MAAM,EAAE,OAAO,UAAU;AACzB,gBAAe;AACb,KAAG,YAAY,CAAC,QAAQ;GACxB;AACF,QAAO;;AAKT,SAAS,uBAAuB,EAAE,UAA8B;CAC9D,MAAM,EAAE,eAAe,UAAU;AAGjC,uBAAsB;AACpB,cAAY;IACX,CAAC,QAAQ,WAAW,CAAC;AACxB,QAAO;;AAYT,SAAgB,kBAAkB,EAChC,UACA,gBACA,oBACA,IAAI,OACJ,GAAG,eACsB;CACzB,MAAM,CAAC,QAAQ,aAAa,SAAS,EAAE;CACvC,MAAM,CAAC,YAAY,iBAAiB,SAAS,EAAE;CAC/C,MAAM,eAAe,OAAuB,KAAK;AAEjD,iBAAgB;EACd,MAAM,KAAK,aAAa;AACxB,MAAI,CAAC,GAAI;EAGT,MAAM,KAAK,GAAG,QAAQ,eAAe;AAYrC,MAAI,CAAC,IAAI;AACP,WAAQ,KACN,mGAED;AACD;;AAGF,MAAI,GAAG,WAAY,eAAc,GAAG,WAAW;AAgB/C,SAdgB,GAAG,gBAChB,EAAE,kBAAkB,YAAY,UAAU;AAMzC,mBAAgB;AACd,cAAU,iBAAiB;AAC3B,kBAAc,IAAI;KAClB;IAEL;IAGA,EAAE,CAAC;CAGN,MAAM,WACJ,OAAO,WAAW,WACd;EAAE,uBAAuB;EAAM,GAAG;EAAQ,GACzC,UAAU,EAAE,uBAAuB,MAAM;AAEhD,QACE,oBAAC;EACC,KAAK;EACL,WAAW;EACX,OAAO;GACL,UAAU;GACV,OAAO;GACP,OAAO;GACP,QAAQ;GACR,GAAG;GACJ;YAED,oBAAC;GACC,WAAU;GACV,IAAI;GACJ,GAAI;GACJ,OAAO;IAAE,OAAO;IAAQ,QAAQ;IAAQ,GAAG,YAAY;IAAO;aAE9D,qBAAC,uBAAuB;IAAS,OAAO;KAAE;KAAQ;KAAY;;KAC5D,oBAAC,WAAS;KACV,oBAAC,0BAA+B,SAAU;KACzC;;KAC+B;IAC3B;GACL"}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import * as React$1 from "react";
|
|
2
|
+
import * as react_jsx_runtime0 from "react/jsx-runtime";
|
|
3
|
+
import { CanvasProps } from "@react-three/fiber";
|
|
4
|
+
|
|
5
|
+
//#region src/r3f/OffscreenCompositionCanvas.d.ts
|
|
6
|
+
|
|
7
|
+
interface OffscreenCompositionCanvasProps {
|
|
8
|
+
/** Web worker that will handle R3F rendering */
|
|
9
|
+
worker: Worker;
|
|
10
|
+
/** Fallback content for browsers without OffscreenCanvas support (Safari) */
|
|
11
|
+
fallback?: React$1.ReactNode;
|
|
12
|
+
/** Extra styles for the container div */
|
|
13
|
+
containerStyle?: React$1.CSSProperties;
|
|
14
|
+
/** Extra className for the container div */
|
|
15
|
+
containerClassName?: string;
|
|
16
|
+
/** Canvas props to forward to @react-three/offscreen Canvas (shadows, dpr, gl, camera, scene, etc.) */
|
|
17
|
+
canvasProps?: Omit<CanvasProps, "frameloop">;
|
|
18
|
+
}
|
|
19
|
+
declare function OffscreenCompositionCanvas({
|
|
20
|
+
worker,
|
|
21
|
+
fallback,
|
|
22
|
+
containerStyle,
|
|
23
|
+
containerClassName,
|
|
24
|
+
canvasProps
|
|
25
|
+
}: OffscreenCompositionCanvasProps): react_jsx_runtime0.JSX.Element;
|
|
26
|
+
//#endregion
|
|
27
|
+
export { OffscreenCompositionCanvas, OffscreenCompositionCanvasProps };
|
|
28
|
+
//# sourceMappingURL=OffscreenCompositionCanvas.d.ts.map
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
import { useEffect, useRef, useState } from "react";
|
|
2
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
3
|
+
import { Canvas } from "@react-three/offscreen";
|
|
4
|
+
|
|
5
|
+
//#region src/r3f/OffscreenCompositionCanvas.tsx
|
|
6
|
+
function waitForBitmap(worker, requestId) {
|
|
7
|
+
return new Promise((resolve, reject) => {
|
|
8
|
+
const timeout = setTimeout(() => {
|
|
9
|
+
worker.removeEventListener("message", handler);
|
|
10
|
+
reject(/* @__PURE__ */ new Error(`[OffscreenCompositionCanvas] Timeout waiting for frame ${requestId}`));
|
|
11
|
+
}, 5e3);
|
|
12
|
+
const handler = (e) => {
|
|
13
|
+
if (e.data.type === "frameRendered" && e.data.requestId === requestId) {
|
|
14
|
+
clearTimeout(timeout);
|
|
15
|
+
worker.removeEventListener("message", handler);
|
|
16
|
+
resolve(e.data.bitmap);
|
|
17
|
+
} else if (e.data.type === "error") {
|
|
18
|
+
clearTimeout(timeout);
|
|
19
|
+
worker.removeEventListener("message", handler);
|
|
20
|
+
reject(/* @__PURE__ */ new Error(`[OffscreenCompositionCanvas] Worker error: ${e.data.message}`));
|
|
21
|
+
}
|
|
22
|
+
};
|
|
23
|
+
worker.addEventListener("message", handler);
|
|
24
|
+
});
|
|
25
|
+
}
|
|
26
|
+
function OffscreenCompositionCanvas({ worker, fallback, containerStyle, containerClassName, canvasProps }) {
|
|
27
|
+
const containerRef = useRef(null);
|
|
28
|
+
const captureCanvasRef = useRef(null);
|
|
29
|
+
const [dimensions, setDimensions] = useState({
|
|
30
|
+
width: 0,
|
|
31
|
+
height: 0
|
|
32
|
+
});
|
|
33
|
+
useEffect(() => {
|
|
34
|
+
const container = containerRef.current;
|
|
35
|
+
if (!container) return;
|
|
36
|
+
const observer = new ResizeObserver((entries) => {
|
|
37
|
+
for (const entry of entries) {
|
|
38
|
+
const { width, height } = entry.contentRect;
|
|
39
|
+
setDimensions({
|
|
40
|
+
width,
|
|
41
|
+
height
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
});
|
|
45
|
+
observer.observe(container);
|
|
46
|
+
return () => observer.disconnect();
|
|
47
|
+
}, []);
|
|
48
|
+
useEffect(() => {
|
|
49
|
+
const container = containerRef.current;
|
|
50
|
+
if (!container) return;
|
|
51
|
+
const tg = container.closest("ef-timegroup");
|
|
52
|
+
if (!tg) {
|
|
53
|
+
console.warn("[OffscreenCompositionCanvas] No ef-timegroup ancestor found. Wrap OffscreenCompositionCanvas inside a <Timegroup>.");
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
if (!tg.addFrameTask) {
|
|
57
|
+
console.warn("[OffscreenCompositionCanvas] ef-timegroup does not have addFrameTask method");
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
let nextRequestId = 0;
|
|
61
|
+
return tg.addFrameTask(async ({ ownCurrentTimeMs, durationMs }) => {
|
|
62
|
+
const requestId = nextRequestId++;
|
|
63
|
+
worker.postMessage({
|
|
64
|
+
type: "renderFrame",
|
|
65
|
+
timeMs: ownCurrentTimeMs,
|
|
66
|
+
durationMs,
|
|
67
|
+
requestId
|
|
68
|
+
});
|
|
69
|
+
try {
|
|
70
|
+
const bitmap = await waitForBitmap(worker, requestId);
|
|
71
|
+
const captureCanvas = captureCanvasRef.current;
|
|
72
|
+
if (captureCanvas) {
|
|
73
|
+
const ctx = captureCanvas.getContext("2d");
|
|
74
|
+
if (ctx) {
|
|
75
|
+
if (captureCanvas.width !== bitmap.width || captureCanvas.height !== bitmap.height) {
|
|
76
|
+
captureCanvas.width = bitmap.width;
|
|
77
|
+
captureCanvas.height = bitmap.height;
|
|
78
|
+
}
|
|
79
|
+
ctx.drawImage(bitmap, 0, 0);
|
|
80
|
+
}
|
|
81
|
+
bitmap.close();
|
|
82
|
+
}
|
|
83
|
+
} catch (error) {
|
|
84
|
+
console.error("[OffscreenCompositionCanvas] Frame render error:", error);
|
|
85
|
+
}
|
|
86
|
+
});
|
|
87
|
+
}, [worker]);
|
|
88
|
+
return /* @__PURE__ */ jsxs("div", {
|
|
89
|
+
ref: containerRef,
|
|
90
|
+
className: containerClassName,
|
|
91
|
+
style: {
|
|
92
|
+
position: "absolute",
|
|
93
|
+
inset: 0,
|
|
94
|
+
width: "100%",
|
|
95
|
+
height: "100%",
|
|
96
|
+
...containerStyle
|
|
97
|
+
},
|
|
98
|
+
children: [/* @__PURE__ */ jsx(Canvas, {
|
|
99
|
+
worker,
|
|
100
|
+
fallback,
|
|
101
|
+
...canvasProps,
|
|
102
|
+
style: {
|
|
103
|
+
width: "100%",
|
|
104
|
+
height: "100%",
|
|
105
|
+
...canvasProps?.style
|
|
106
|
+
}
|
|
107
|
+
}), /* @__PURE__ */ jsx("canvas", {
|
|
108
|
+
ref: captureCanvasRef,
|
|
109
|
+
"data-offscreen-capture": "true",
|
|
110
|
+
style: { display: "none" },
|
|
111
|
+
width: dimensions.width,
|
|
112
|
+
height: dimensions.height
|
|
113
|
+
})]
|
|
114
|
+
});
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
//#endregion
|
|
118
|
+
export { OffscreenCompositionCanvas };
|
|
119
|
+
//# sourceMappingURL=OffscreenCompositionCanvas.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"OffscreenCompositionCanvas.js","names":["OffscreenCanvas"],"sources":["../../src/r3f/OffscreenCompositionCanvas.tsx"],"sourcesContent":["/**\n * OffscreenCompositionCanvas — R3F Canvas that renders in a web worker via OffscreenCanvas.\n *\n * This component integrates with Editframe's timeline system by:\n * - Registering an addFrameTask that sends time updates to the worker\n * - Receiving rendered frames (ImageBitmap) from the worker\n * - Drawing frames onto a hidden capture canvas for video export\n *\n * The worker handles all R3F rendering, keeping the main thread free and enabling\n * rendering to continue even when the browser tab is hidden.\n *\n * Usage:\n * ```tsx\n * const worker = new Worker(new URL('./scene-worker.ts', import.meta.url), { type: 'module' });\n *\n * <Timegroup mode=\"fixed\" duration=\"14s\">\n * <OffscreenCompositionCanvas\n * worker={worker}\n * fallback={<MainThreadFallback />}\n * canvasProps={{ shadows: true, dpr: [1, 2] }}\n * />\n * </Timegroup>\n * ```\n */\n\nimport * as React from \"react\";\nimport { useEffect, useRef, useState } from \"react\";\nimport { Canvas as OffscreenCanvas } from \"@react-three/offscreen\";\nimport type { CanvasProps } from \"@react-three/fiber\";\nimport type { EFTimegroup } from \"@editframe/elements\";\n\n/* ━━ Types ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ */\n\nexport interface OffscreenCompositionCanvasProps {\n /** Web worker that will handle R3F rendering */\n worker: Worker;\n /** Fallback content for browsers without OffscreenCanvas support (Safari) */\n fallback?: React.ReactNode;\n /** Extra styles for the container div */\n containerStyle?: React.CSSProperties;\n /** Extra className for the container div */\n containerClassName?: string;\n /** Canvas props to forward to @react-three/offscreen Canvas (shadows, dpr, gl, camera, scene, etc.) */\n canvasProps?: Omit<CanvasProps, \"frameloop\">;\n}\n\n/* ━━ Helper: Wait for bitmap from worker ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ */\n\nfunction waitForBitmap(\n worker: Worker,\n requestId: number,\n): Promise<ImageBitmap> {\n return new Promise((resolve, reject) => {\n const timeout = setTimeout(() => {\n worker.removeEventListener(\"message\", handler);\n reject(\n new Error(\n `[OffscreenCompositionCanvas] Timeout waiting for frame ${requestId}`,\n ),\n );\n }, 5000); // 5 second timeout\n\n const handler = (e: MessageEvent) => {\n if (e.data.type === \"frameRendered\" && e.data.requestId === requestId) {\n clearTimeout(timeout);\n worker.removeEventListener(\"message\", handler);\n resolve(e.data.bitmap);\n } else if (e.data.type === \"error\") {\n clearTimeout(timeout);\n worker.removeEventListener(\"message\", handler);\n reject(\n new Error(\n `[OffscreenCompositionCanvas] Worker error: ${e.data.message}`,\n ),\n );\n }\n };\n\n worker.addEventListener(\"message\", handler);\n });\n}\n\n/* ━━ Component ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ */\n\nexport function OffscreenCompositionCanvas({\n worker,\n fallback,\n containerStyle,\n containerClassName,\n canvasProps,\n}: OffscreenCompositionCanvasProps) {\n const containerRef = useRef<HTMLDivElement>(null);\n const captureCanvasRef = useRef<HTMLCanvasElement>(null);\n const [dimensions, setDimensions] = useState({ width: 0, height: 0 });\n\n /* ━━ Resize observer to keep capture canvas in sync ━━━━━━━━━━━━━━━━━ */\n\n useEffect(() => {\n const container = containerRef.current;\n if (!container) return;\n\n const observer = new ResizeObserver((entries) => {\n for (const entry of entries) {\n const { width, height } = entry.contentRect;\n setDimensions({ width, height });\n }\n });\n\n observer.observe(container);\n return () => observer.disconnect();\n }, []);\n\n /* ━━ addFrameTask integration ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ */\n\n useEffect(() => {\n const container = containerRef.current;\n if (!container) return;\n\n // Walk up to find the parent ef-timegroup\n const tg = container.closest(\"ef-timegroup\") as\n | (HTMLElement & EFTimegroup)\n | null;\n\n if (!tg) {\n console.warn(\n \"[OffscreenCompositionCanvas] No ef-timegroup ancestor found. \" +\n \"Wrap OffscreenCompositionCanvas inside a <Timegroup>.\",\n );\n return;\n }\n\n if (!tg.addFrameTask) {\n console.warn(\n \"[OffscreenCompositionCanvas] ef-timegroup does not have addFrameTask method\",\n );\n return;\n }\n\n let nextRequestId = 0;\n\n const cleanup = tg.addFrameTask(\n async ({ ownCurrentTimeMs, durationMs }) => {\n const requestId = nextRequestId++;\n\n // Send render request to worker\n worker.postMessage({\n type: \"renderFrame\",\n timeMs: ownCurrentTimeMs,\n durationMs,\n requestId,\n });\n\n try {\n // Wait for worker to finish rendering and return pixels\n const bitmap = await waitForBitmap(worker, requestId);\n\n // Draw onto capture canvas so serialization pipeline can read pixels\n const captureCanvas = captureCanvasRef.current;\n if (captureCanvas) {\n const ctx = captureCanvas.getContext(\"2d\");\n if (ctx) {\n // Resize capture canvas to match bitmap\n if (\n captureCanvas.width !== bitmap.width ||\n captureCanvas.height !== bitmap.height\n ) {\n captureCanvas.width = bitmap.width;\n captureCanvas.height = bitmap.height;\n }\n\n // Draw the bitmap\n ctx.drawImage(bitmap, 0, 0);\n }\n\n // Close bitmap to free memory\n bitmap.close();\n }\n } catch (error) {\n console.error(\n \"[OffscreenCompositionCanvas] Frame render error:\",\n error,\n );\n }\n },\n );\n\n return cleanup;\n }, [worker]);\n\n /* ━━ Render ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ */\n\n return (\n <div\n ref={containerRef}\n className={containerClassName}\n style={{\n position: \"absolute\",\n inset: 0,\n width: \"100%\",\n height: \"100%\",\n ...containerStyle,\n }}\n >\n {/* Display canvas - handled by @react-three/offscreen */}\n <OffscreenCanvas\n worker={worker}\n fallback={fallback}\n {...canvasProps}\n style={{ width: \"100%\", height: \"100%\", ...canvasProps?.style }}\n />\n\n {/* Hidden capture canvas for video export */}\n <canvas\n ref={captureCanvasRef}\n data-offscreen-capture=\"true\"\n style={{ display: \"none\" }}\n width={dimensions.width}\n height={dimensions.height}\n />\n </div>\n );\n}\n"],"mappings":";;;;;AAgDA,SAAS,cACP,QACA,WACsB;AACtB,QAAO,IAAI,SAAS,SAAS,WAAW;EACtC,MAAM,UAAU,iBAAiB;AAC/B,UAAO,oBAAoB,WAAW,QAAQ;AAC9C,0BACE,IAAI,MACF,0DAA0D,YAC3D,CACF;KACA,IAAK;EAER,MAAM,WAAW,MAAoB;AACnC,OAAI,EAAE,KAAK,SAAS,mBAAmB,EAAE,KAAK,cAAc,WAAW;AACrE,iBAAa,QAAQ;AACrB,WAAO,oBAAoB,WAAW,QAAQ;AAC9C,YAAQ,EAAE,KAAK,OAAO;cACb,EAAE,KAAK,SAAS,SAAS;AAClC,iBAAa,QAAQ;AACrB,WAAO,oBAAoB,WAAW,QAAQ;AAC9C,2BACE,IAAI,MACF,8CAA8C,EAAE,KAAK,UACtD,CACF;;;AAIL,SAAO,iBAAiB,WAAW,QAAQ;GAC3C;;AAKJ,SAAgB,2BAA2B,EACzC,QACA,UACA,gBACA,oBACA,eACkC;CAClC,MAAM,eAAe,OAAuB,KAAK;CACjD,MAAM,mBAAmB,OAA0B,KAAK;CACxD,MAAM,CAAC,YAAY,iBAAiB,SAAS;EAAE,OAAO;EAAG,QAAQ;EAAG,CAAC;AAIrE,iBAAgB;EACd,MAAM,YAAY,aAAa;AAC/B,MAAI,CAAC,UAAW;EAEhB,MAAM,WAAW,IAAI,gBAAgB,YAAY;AAC/C,QAAK,MAAM,SAAS,SAAS;IAC3B,MAAM,EAAE,OAAO,WAAW,MAAM;AAChC,kBAAc;KAAE;KAAO;KAAQ,CAAC;;IAElC;AAEF,WAAS,QAAQ,UAAU;AAC3B,eAAa,SAAS,YAAY;IACjC,EAAE,CAAC;AAIN,iBAAgB;EACd,MAAM,YAAY,aAAa;AAC/B,MAAI,CAAC,UAAW;EAGhB,MAAM,KAAK,UAAU,QAAQ,eAAe;AAI5C,MAAI,CAAC,IAAI;AACP,WAAQ,KACN,qHAED;AACD;;AAGF,MAAI,CAAC,GAAG,cAAc;AACpB,WAAQ,KACN,8EACD;AACD;;EAGF,IAAI,gBAAgB;AAgDpB,SA9CgB,GAAG,aACjB,OAAO,EAAE,kBAAkB,iBAAiB;GAC1C,MAAM,YAAY;AAGlB,UAAO,YAAY;IACjB,MAAM;IACN,QAAQ;IACR;IACA;IACD,CAAC;AAEF,OAAI;IAEF,MAAM,SAAS,MAAM,cAAc,QAAQ,UAAU;IAGrD,MAAM,gBAAgB,iBAAiB;AACvC,QAAI,eAAe;KACjB,MAAM,MAAM,cAAc,WAAW,KAAK;AAC1C,SAAI,KAAK;AAEP,UACE,cAAc,UAAU,OAAO,SAC/B,cAAc,WAAW,OAAO,QAChC;AACA,qBAAc,QAAQ,OAAO;AAC7B,qBAAc,SAAS,OAAO;;AAIhC,UAAI,UAAU,QAAQ,GAAG,EAAE;;AAI7B,YAAO,OAAO;;YAET,OAAO;AACd,YAAQ,MACN,oDACA,MACD;;IAGN;IAGA,CAAC,OAAO,CAAC;AAIZ,QACE,qBAAC;EACC,KAAK;EACL,WAAW;EACX,OAAO;GACL,UAAU;GACV,OAAO;GACP,OAAO;GACP,QAAQ;GACR,GAAG;GACJ;aAGD,oBAACA;GACS;GACE;GACV,GAAI;GACJ,OAAO;IAAE,OAAO;IAAQ,QAAQ;IAAQ,GAAG,aAAa;IAAO;IAC/D,EAGF,oBAAC;GACC,KAAK;GACL,0BAAuB;GACvB,OAAO,EAAE,SAAS,QAAQ;GAC1B,OAAO,WAAW;GAClB,QAAQ,WAAW;IACnB;GACE"}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import { OffscreenCompositionCanvas, OffscreenCompositionCanvasProps } from "./OffscreenCompositionCanvas.js";
|
|
2
|
+
import { CompositionCanvas, CompositionCanvasProps, useCompositionTime } from "./CompositionCanvas.js";
|
|
3
|
+
import { renderOffscreen } from "./renderOffscreen.js";
|
|
4
|
+
import { MainToWorkerMessage, RenderFramePayload, WorkerToMainMessage } from "./worker-protocol.js";
|
|
5
|
+
export { CompositionCanvas, type CompositionCanvasProps, type MainToWorkerMessage, OffscreenCompositionCanvas, type OffscreenCompositionCanvasProps, type RenderFramePayload, type WorkerToMainMessage, renderOffscreen, useCompositionTime };
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import { OffscreenCompositionCanvas } from "./OffscreenCompositionCanvas.js";
|
|
2
|
+
import { CompositionCanvas, useCompositionTime } from "./CompositionCanvas.js";
|
|
3
|
+
import { renderOffscreen } from "./renderOffscreen.js";
|
|
4
|
+
|
|
5
|
+
export { CompositionCanvas, OffscreenCompositionCanvas, renderOffscreen, useCompositionTime };
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import * as React$1 from "react";
|
|
2
|
+
|
|
3
|
+
//#region src/r3f/renderOffscreen.d.ts
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Render a React Three Fiber scene in a web worker with offscreen canvas.
|
|
7
|
+
*
|
|
8
|
+
* This extends @react-three/offscreen's render() with Editframe-specific features:
|
|
9
|
+
* - Time synchronization via timeStore and useCompositionTime hook
|
|
10
|
+
* - Frame-by-frame rendering on demand (renderFrame message)
|
|
11
|
+
* - Pixel capture and transfer back to main thread via ImageBitmap
|
|
12
|
+
*
|
|
13
|
+
* @param children - React node containing the R3F scene
|
|
14
|
+
*
|
|
15
|
+
* @example
|
|
16
|
+
* ```typescript
|
|
17
|
+
* // worker.ts
|
|
18
|
+
* import { renderOffscreen } from '@editframe/react/r3f';
|
|
19
|
+
* import { MyScene } from './MyScene';
|
|
20
|
+
*
|
|
21
|
+
* renderOffscreen(<MyScene />);
|
|
22
|
+
* ```
|
|
23
|
+
*/
|
|
24
|
+
declare function renderOffscreen(children: React$1.ReactNode): void;
|
|
25
|
+
//#endregion
|
|
26
|
+
export { renderOffscreen };
|
|
27
|
+
//# sourceMappingURL=renderOffscreen.d.ts.map
|
|
@@ -0,0 +1,291 @@
|
|
|
1
|
+
import "react";
|
|
2
|
+
import { createEvents, createRoot } from "@react-three/fiber";
|
|
3
|
+
import * as THREE from "three";
|
|
4
|
+
|
|
5
|
+
//#region src/r3f/renderOffscreen.ts
|
|
6
|
+
const EVENTS = {
|
|
7
|
+
onClick: ["click", false],
|
|
8
|
+
onContextMenu: ["contextmenu", false],
|
|
9
|
+
onDoubleClick: ["dblclick", false],
|
|
10
|
+
onWheel: ["wheel", true],
|
|
11
|
+
onPointerDown: ["pointerdown", true],
|
|
12
|
+
onPointerUp: ["pointerup", true],
|
|
13
|
+
onPointerLeave: ["pointerleave", true],
|
|
14
|
+
onPointerMove: ["pointermove", true],
|
|
15
|
+
onPointerCancel: ["pointercancel", true],
|
|
16
|
+
onLostPointerCapture: ["lostpointercapture", true]
|
|
17
|
+
};
|
|
18
|
+
function createPointerEvents(emitter) {
|
|
19
|
+
return (store) => {
|
|
20
|
+
const { handlePointer } = createEvents(store);
|
|
21
|
+
return {
|
|
22
|
+
priority: 1,
|
|
23
|
+
enabled: true,
|
|
24
|
+
compute(event, state) {
|
|
25
|
+
state.pointer.set(event.offsetX / state.size.width * 2 - 1, -(event.offsetY / state.size.height) * 2 + 1);
|
|
26
|
+
state.raycaster.setFromCamera(state.pointer, state.camera);
|
|
27
|
+
},
|
|
28
|
+
connected: void 0,
|
|
29
|
+
handlers: Object.keys(EVENTS).reduce((acc, key) => ({
|
|
30
|
+
...acc,
|
|
31
|
+
[key]: handlePointer(key)
|
|
32
|
+
}), {}),
|
|
33
|
+
connect: (target) => {
|
|
34
|
+
const { set, events } = store.getState();
|
|
35
|
+
events.disconnect?.();
|
|
36
|
+
set((state) => ({ events: {
|
|
37
|
+
...state.events,
|
|
38
|
+
connected: target
|
|
39
|
+
} }));
|
|
40
|
+
Object.entries(events?.handlers ?? []).forEach(([name, event]) => {
|
|
41
|
+
const [eventName] = EVENTS[name];
|
|
42
|
+
emitter.on(eventName, event);
|
|
43
|
+
});
|
|
44
|
+
},
|
|
45
|
+
disconnect: () => {
|
|
46
|
+
const { set, events } = store.getState();
|
|
47
|
+
if (events.connected) {
|
|
48
|
+
Object.entries(events.handlers ?? []).forEach(([name, event]) => {
|
|
49
|
+
const [eventName] = EVENTS[name];
|
|
50
|
+
emitter.off(eventName, event);
|
|
51
|
+
});
|
|
52
|
+
set((state) => ({ events: {
|
|
53
|
+
...state.events,
|
|
54
|
+
connected: void 0
|
|
55
|
+
} }));
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
};
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
const timeStore = {
|
|
62
|
+
timeMs: 0,
|
|
63
|
+
durationMs: 0,
|
|
64
|
+
listeners: /* @__PURE__ */ new Set(),
|
|
65
|
+
update(timeMs, durationMs) {
|
|
66
|
+
this.timeMs = timeMs;
|
|
67
|
+
this.durationMs = durationMs;
|
|
68
|
+
this.listeners.forEach((l) => l());
|
|
69
|
+
}
|
|
70
|
+
};
|
|
71
|
+
/**
|
|
72
|
+
* Render a React Three Fiber scene in a web worker with offscreen canvas.
|
|
73
|
+
*
|
|
74
|
+
* This extends @react-three/offscreen's render() with Editframe-specific features:
|
|
75
|
+
* - Time synchronization via timeStore and useCompositionTime hook
|
|
76
|
+
* - Frame-by-frame rendering on demand (renderFrame message)
|
|
77
|
+
* - Pixel capture and transfer back to main thread via ImageBitmap
|
|
78
|
+
*
|
|
79
|
+
* @param children - React node containing the R3F scene
|
|
80
|
+
*
|
|
81
|
+
* @example
|
|
82
|
+
* ```typescript
|
|
83
|
+
* // worker.ts
|
|
84
|
+
* import { renderOffscreen } from '@editframe/react/r3f';
|
|
85
|
+
* import { MyScene } from './MyScene';
|
|
86
|
+
*
|
|
87
|
+
* renderOffscreen(<MyScene />);
|
|
88
|
+
* ```
|
|
89
|
+
*/
|
|
90
|
+
function renderOffscreen(children) {
|
|
91
|
+
let root = null;
|
|
92
|
+
let offscreenCanvas = null;
|
|
93
|
+
let size = {
|
|
94
|
+
width: 0,
|
|
95
|
+
height: 0,
|
|
96
|
+
top: 0,
|
|
97
|
+
left: 0
|
|
98
|
+
};
|
|
99
|
+
let dpr = 1;
|
|
100
|
+
const handlers = /* @__PURE__ */ new Map();
|
|
101
|
+
const emitter = {
|
|
102
|
+
all: /* @__PURE__ */ new Map(),
|
|
103
|
+
on(type, handler) {
|
|
104
|
+
const s = handlers.get(type) ?? /* @__PURE__ */ new Set();
|
|
105
|
+
s.add(handler);
|
|
106
|
+
handlers.set(type, s);
|
|
107
|
+
},
|
|
108
|
+
off(type, handler) {
|
|
109
|
+
handlers.get(type)?.delete(handler);
|
|
110
|
+
},
|
|
111
|
+
emit(type, event) {
|
|
112
|
+
handlers.get(type)?.forEach((h) => h(event));
|
|
113
|
+
handlers.get("*")?.forEach((h) => h(type, event));
|
|
114
|
+
}
|
|
115
|
+
};
|
|
116
|
+
const handleInit = (payload) => {
|
|
117
|
+
const { props, drawingSurface: canvas, width, top, left, height, pixelRatio } = payload;
|
|
118
|
+
console.log("[renderOffscreen] Init received", {
|
|
119
|
+
width,
|
|
120
|
+
height,
|
|
121
|
+
pixelRatio
|
|
122
|
+
});
|
|
123
|
+
try {
|
|
124
|
+
if (root) root.unmount();
|
|
125
|
+
offscreenCanvas = canvas;
|
|
126
|
+
Object.assign(canvas, {
|
|
127
|
+
pageXOffset: left,
|
|
128
|
+
pageYOffset: top,
|
|
129
|
+
clientLeft: left,
|
|
130
|
+
clientTop: top,
|
|
131
|
+
clientWidth: width,
|
|
132
|
+
clientHeight: height,
|
|
133
|
+
style: { touchAction: "none" },
|
|
134
|
+
ownerDocument: canvas,
|
|
135
|
+
documentElement: canvas,
|
|
136
|
+
getBoundingClientRect() {
|
|
137
|
+
return size;
|
|
138
|
+
},
|
|
139
|
+
setAttribute() {},
|
|
140
|
+
setPointerCapture() {},
|
|
141
|
+
releasePointerCapture() {},
|
|
142
|
+
addEventListener(event, callback) {
|
|
143
|
+
emitter.on(event, callback);
|
|
144
|
+
},
|
|
145
|
+
removeEventListener(event, callback) {
|
|
146
|
+
emitter.off(event, callback);
|
|
147
|
+
}
|
|
148
|
+
});
|
|
149
|
+
root = createRoot(canvas);
|
|
150
|
+
root.configure({
|
|
151
|
+
events: createPointerEvents(emitter),
|
|
152
|
+
size: size = {
|
|
153
|
+
width,
|
|
154
|
+
height,
|
|
155
|
+
top,
|
|
156
|
+
left
|
|
157
|
+
},
|
|
158
|
+
dpr: dpr = Math.min(Math.max(1, pixelRatio), 2),
|
|
159
|
+
frameloop: "demand",
|
|
160
|
+
...props,
|
|
161
|
+
onCreated: (state) => {
|
|
162
|
+
if (props.eventPrefix) state.setEvents({ compute: (event, state$1) => {
|
|
163
|
+
const x = event[props.eventPrefix + "X"];
|
|
164
|
+
const y = event[props.eventPrefix + "Y"];
|
|
165
|
+
state$1.pointer.set(x / state$1.size.width * 2 - 1, -(y / state$1.size.height) * 2 + 1);
|
|
166
|
+
state$1.raycaster.setFromCamera(state$1.pointer, state$1.camera);
|
|
167
|
+
} });
|
|
168
|
+
}
|
|
169
|
+
});
|
|
170
|
+
console.log("[renderOffscreen] Rendering children");
|
|
171
|
+
root.render(children);
|
|
172
|
+
console.log("[renderOffscreen] Init complete");
|
|
173
|
+
} catch (e) {
|
|
174
|
+
console.error("[renderOffscreen] Init error:", e);
|
|
175
|
+
postMessage({
|
|
176
|
+
type: "error",
|
|
177
|
+
payload: e?.message
|
|
178
|
+
});
|
|
179
|
+
}
|
|
180
|
+
self.window = canvas;
|
|
181
|
+
};
|
|
182
|
+
const handleResize = ({ width, height, top, left }) => {
|
|
183
|
+
if (!root) return;
|
|
184
|
+
root.configure({
|
|
185
|
+
size: size = {
|
|
186
|
+
width,
|
|
187
|
+
height,
|
|
188
|
+
top,
|
|
189
|
+
left
|
|
190
|
+
},
|
|
191
|
+
dpr
|
|
192
|
+
});
|
|
193
|
+
};
|
|
194
|
+
const handleEvents = (payload) => {
|
|
195
|
+
emitter.emit(payload.eventName, {
|
|
196
|
+
...payload,
|
|
197
|
+
preventDefault() {},
|
|
198
|
+
stopPropagation() {}
|
|
199
|
+
});
|
|
200
|
+
};
|
|
201
|
+
const handleProps = (payload) => {
|
|
202
|
+
if (!root) return;
|
|
203
|
+
if (payload.dpr) dpr = payload.dpr;
|
|
204
|
+
root.configure({
|
|
205
|
+
size,
|
|
206
|
+
dpr,
|
|
207
|
+
...payload
|
|
208
|
+
});
|
|
209
|
+
};
|
|
210
|
+
const handleRenderFrame = async ({ timeMs, durationMs, requestId }) => {
|
|
211
|
+
console.log("[renderOffscreen] Render frame", {
|
|
212
|
+
timeMs,
|
|
213
|
+
requestId
|
|
214
|
+
});
|
|
215
|
+
try {
|
|
216
|
+
timeStore.update(timeMs, durationMs);
|
|
217
|
+
const state = root?.store?.getState?.();
|
|
218
|
+
if (!state) throw new Error("[renderOffscreen] No R3F root state available");
|
|
219
|
+
if (state?.gl && state?.scene && state?.camera) {
|
|
220
|
+
state.invalidate();
|
|
221
|
+
state.gl.render(state.scene, state.camera);
|
|
222
|
+
state.gl.getContext().finish();
|
|
223
|
+
} else throw new Error("[renderOffscreen] Missing gl/scene/camera in state");
|
|
224
|
+
if (!offscreenCanvas) throw new Error("[renderOffscreen] No offscreen canvas available");
|
|
225
|
+
const bitmap = await createImageBitmap(offscreenCanvas);
|
|
226
|
+
console.log("[renderOffscreen] Bitmap created", {
|
|
227
|
+
width: bitmap.width,
|
|
228
|
+
height: bitmap.height
|
|
229
|
+
});
|
|
230
|
+
self.postMessage({
|
|
231
|
+
type: "frameRendered",
|
|
232
|
+
requestId,
|
|
233
|
+
bitmap
|
|
234
|
+
}, [bitmap]);
|
|
235
|
+
} catch (e) {
|
|
236
|
+
console.error("[renderOffscreen] Frame render error:", e);
|
|
237
|
+
postMessage({
|
|
238
|
+
type: "error",
|
|
239
|
+
message: e?.message || "Unknown error in handleRenderFrame"
|
|
240
|
+
});
|
|
241
|
+
}
|
|
242
|
+
};
|
|
243
|
+
const handlerMap = {
|
|
244
|
+
resize: handleResize,
|
|
245
|
+
init: handleInit,
|
|
246
|
+
dom_events: handleEvents,
|
|
247
|
+
props: handleProps,
|
|
248
|
+
renderFrame: handleRenderFrame
|
|
249
|
+
};
|
|
250
|
+
self.onmessage = (event) => {
|
|
251
|
+
const { type, payload } = event.data;
|
|
252
|
+
const handler = handlerMap[type];
|
|
253
|
+
if (handler) handler(payload);
|
|
254
|
+
};
|
|
255
|
+
THREE.ImageLoader.prototype.load = function(url, onLoad, _onProgress, onError) {
|
|
256
|
+
if (this.path !== void 0) url = this.path + url;
|
|
257
|
+
url = this.manager.resolveURL(url);
|
|
258
|
+
const scope = this;
|
|
259
|
+
const cached = THREE.Cache.get(url);
|
|
260
|
+
if (cached !== void 0) {
|
|
261
|
+
scope.manager.itemStart(url);
|
|
262
|
+
if (onLoad) onLoad(cached);
|
|
263
|
+
scope.manager.itemEnd(url);
|
|
264
|
+
return cached;
|
|
265
|
+
}
|
|
266
|
+
fetch(url).then((res) => res.blob()).then((res) => createImageBitmap(res, {
|
|
267
|
+
premultiplyAlpha: "none",
|
|
268
|
+
colorSpaceConversion: "none"
|
|
269
|
+
})).then((bitmap) => {
|
|
270
|
+
THREE.Cache.add(url, bitmap);
|
|
271
|
+
if (onLoad) onLoad(bitmap);
|
|
272
|
+
scope.manager.itemEnd(url);
|
|
273
|
+
}).catch(onError);
|
|
274
|
+
return {};
|
|
275
|
+
};
|
|
276
|
+
self.window = {};
|
|
277
|
+
self.document = {};
|
|
278
|
+
self.Image = class {
|
|
279
|
+
constructor() {
|
|
280
|
+
this.height = 1;
|
|
281
|
+
this.width = 1;
|
|
282
|
+
}
|
|
283
|
+
set onload(callback) {
|
|
284
|
+
callback(true);
|
|
285
|
+
}
|
|
286
|
+
};
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
//#endregion
|
|
290
|
+
export { renderOffscreen };
|
|
291
|
+
//# sourceMappingURL=renderOffscreen.js.map
|