@octanejs/drei 0.0.1

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.
Files changed (152) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +11 -0
  3. package/UPSTREAM.md +92 -0
  4. package/package.json +96 -0
  5. package/src/config.ts +14 -0
  6. package/src/core/AccumulativeShadows.three.tsrx +313 -0
  7. package/src/core/AdaptivePerformance.three.tsrx +49 -0
  8. package/src/core/AsciiRenderer.three.tsrx +61 -0
  9. package/src/core/Bounds.three.tsrx +323 -0
  10. package/src/core/Bvh.three.tsrx +116 -0
  11. package/src/core/CameraControls.three.tsrx +181 -0
  12. package/src/core/CameraShake.three.tsrx +104 -0
  13. package/src/core/Caustics.three.tsrx +107 -0
  14. package/src/core/Center.three.tsrx +145 -0
  15. package/src/core/Clone.three.tsrx +167 -0
  16. package/src/core/Cloud.three.tsrx +314 -0
  17. package/src/core/ComputedAttribute.three.tsrx +27 -0
  18. package/src/core/ContactShadows.three.tsrx +106 -0
  19. package/src/core/CubeTexture.three.tsrx +32 -0
  20. package/src/core/CurveModifier.three.tsrx +37 -0
  21. package/src/core/Decal.three.tsrx +132 -0
  22. package/src/core/Detailed.three.tsrx +28 -0
  23. package/src/core/DetectGPU.three.tsrx +22 -0
  24. package/src/core/Effects.three.tsrx +123 -0
  25. package/src/core/Environment.three.tsrx +220 -0
  26. package/src/core/Example.three.tsrx +56 -0
  27. package/src/core/Fbo.three.tsrx +70 -0
  28. package/src/core/Fbx.three.tsrx +25 -0
  29. package/src/core/Fisheye.three.tsrx +82 -0
  30. package/src/core/GizmoHelper.three.tsrx +115 -0
  31. package/src/core/GizmoViewcube.three.tsrx +82 -0
  32. package/src/core/GizmoViewport.three.tsrx +122 -0
  33. package/src/core/Gltf.three.tsrx +62 -0
  34. package/src/core/GradientTexture.three.tsrx +66 -0
  35. package/src/core/Grid.three.tsrx +170 -0
  36. package/src/core/Helper.three.tsrx +56 -0
  37. package/src/core/Hud.three.tsrx +52 -0
  38. package/src/core/Image.three.tsrx +146 -0
  39. package/src/core/Instances.three.tsrx +332 -0
  40. package/src/core/Ktx2.three.tsrx +59 -0
  41. package/src/core/Lightformer.three.tsrx +75 -0
  42. package/src/core/MarchingCubes.three.tsrx +125 -0
  43. package/src/core/MeshPortalMaterial.three.tsrx +103 -0
  44. package/src/core/MeshReflectorMaterial.three.tsrx +67 -0
  45. package/src/core/MeshRefractionMaterial.three.tsrx +70 -0
  46. package/src/core/MeshTransmissionMaterial.three.tsrx +71 -0
  47. package/src/core/MotionPathControls.three.tsrx +139 -0
  48. package/src/core/MultiMaterial.three.tsrx +32 -0
  49. package/src/core/Outlines.three.tsrx +162 -0
  50. package/src/core/PerformanceMonitor.three.tsrx +144 -0
  51. package/src/core/PointerLockControls.three.tsrx +84 -0
  52. package/src/core/Points.three.tsrx +242 -0
  53. package/src/core/PositionalAudio.three.tsrx +76 -0
  54. package/src/core/Preload.three.tsrx +40 -0
  55. package/src/core/Progress.three.tsrx +47 -0
  56. package/src/core/RenderCubeTexture.three.tsrx +111 -0
  57. package/src/core/RenderTexture.three.tsrx +102 -0
  58. package/src/core/Resize.three.tsrx +59 -0
  59. package/src/core/RoundedBox.three.tsrx +106 -0
  60. package/src/core/Sampler.three.tsrx +116 -0
  61. package/src/core/ScreenSizer.three.tsrx +43 -0
  62. package/src/core/ScreenSpace.three.tsrx +21 -0
  63. package/src/core/Segments.three.tsrx +119 -0
  64. package/src/core/Sky.three.tsrx +63 -0
  65. package/src/core/Sparkles.three.tsrx +145 -0
  66. package/src/core/Splat.three.tsrx +52 -0
  67. package/src/core/SpotLight.three.tsrx +350 -0
  68. package/src/core/SpriteAnimator.three.tsrx +338 -0
  69. package/src/core/Stage.three.tsrx +112 -0
  70. package/src/core/Stars.three.tsrx +103 -0
  71. package/src/core/Stats.three.tsrx +35 -0
  72. package/src/core/StatsGl.three.tsrx +60 -0
  73. package/src/core/Svg.three.tsrx +82 -0
  74. package/src/core/Text.three.tsrx +96 -0
  75. package/src/core/Text3D.three.tsrx +90 -0
  76. package/src/core/Texture.three.tsrx +72 -0
  77. package/src/core/Trail.three.tsrx +167 -0
  78. package/src/core/TrailTexture.three.tsrx +171 -0
  79. package/src/core/TransformControls.three.tsrx +155 -0
  80. package/src/core/VideoTexture.three.tsrx +116 -0
  81. package/src/core/Wireframe.three.tsrx +92 -0
  82. package/src/core/basic-controls.three.tsrx +353 -0
  83. package/src/core/calculateScaleFactor.ts +42 -0
  84. package/src/core/cameras.three.tsrx +230 -0
  85. package/src/core/catalog-resource.ts +12 -0
  86. package/src/core/catalog-textures.three.tsrx +105 -0
  87. package/src/core/compiler-slot.ts +9 -0
  88. package/src/core/effects-utils.ts +8 -0
  89. package/src/core/environment-loader.ts +87 -0
  90. package/src/core/font-resource.ts +31 -0
  91. package/src/core/gltf-loader.ts +33 -0
  92. package/src/core/gradient-types.ts +4 -0
  93. package/src/core/index.ts +295 -0
  94. package/src/core/lines.three.tsrx +329 -0
  95. package/src/core/meshBounds.ts +26 -0
  96. package/src/core/motion-materials.three.tsrx +93 -0
  97. package/src/core/octane-children.ts +18 -0
  98. package/src/core/octane-element.ts +16 -0
  99. package/src/core/resource.ts +29 -0
  100. package/src/core/scene-motion.three.tsrx +183 -0
  101. package/src/core/shaderMaterial.ts +50 -0
  102. package/src/core/shadows.three.tsrx +164 -0
  103. package/src/core/shapes.three.tsrx +170 -0
  104. package/src/core/simple-materials.three.tsrx +124 -0
  105. package/src/core/softShadows.three.tsrx +151 -0
  106. package/src/core/splat-resource.ts +598 -0
  107. package/src/core/useAnimations.three.tsrx +57 -0
  108. package/src/core/useAspect.three.tsrx +10 -0
  109. package/src/core/useBoxProjectedEnv.three.tsrx +113 -0
  110. package/src/core/useCamera.three.tsrx +26 -0
  111. package/src/core/useContextBridge.three.tsrx +25 -0
  112. package/src/core/useDepthBuffer.three.tsrx +35 -0
  113. package/src/core/useEnvironment.three.tsrx +81 -0
  114. package/src/core/useFont.three.tsrx +12 -0
  115. package/src/core/useIntersect.three.tsrx +38 -0
  116. package/src/core/useSpriteLoader.three.tsrx +249 -0
  117. package/src/core/video-texture-resource.ts +14 -0
  118. package/src/index.ts +2 -0
  119. package/src/materials/BlurPass.ts +94 -0
  120. package/src/materials/CausticsMaterials.ts +197 -0
  121. package/src/materials/ConvolutionMaterial.ts +65 -0
  122. package/src/materials/MeshReflectorMaterial.ts +180 -0
  123. package/src/materials/MeshRefractionMaterial.ts +131 -0
  124. package/src/materials/MeshTransmissionMaterial.ts +182 -0
  125. package/src/materials/PortalMaterial.ts +38 -0
  126. package/src/materials/WireframeMaterial.ts +241 -0
  127. package/src/materials/makeSDFGenerator.ts +155 -0
  128. package/src/troika-three-text.d.ts +19 -0
  129. package/src/web/CycleRaycast.three.tsrx +81 -0
  130. package/src/web/DragControls.three.tsrx +82 -0
  131. package/src/web/FaceControls.three.tsrx +178 -0
  132. package/src/web/FaceLandmarker.three.tsrx +48 -0
  133. package/src/web/Facemesh.three.tsrx +464 -0
  134. package/src/web/Html.three.tsrx +390 -0
  135. package/src/web/KeyboardControls.three.tsrx +98 -0
  136. package/src/web/Loader.tsrx +61 -0
  137. package/src/web/PresentationControls.three.tsrx +63 -0
  138. package/src/web/ScrollControls.three.tsrx +120 -0
  139. package/src/web/Select.three.tsrx +155 -0
  140. package/src/web/View.three.tsrx +46 -0
  141. package/src/web/face-landmarker-resource.ts +18 -0
  142. package/src/web/facemesh-data.ts +54 -0
  143. package/src/web/index.ts +41 -0
  144. package/src/web/keyboard-controls-types.ts +33 -0
  145. package/src/web/media-textures.three.tsrx +50 -0
  146. package/src/web/pivotControls/annotation.tsrx +17 -0
  147. package/src/web/pivotControls/context.ts +38 -0
  148. package/src/web/pivotControls/index.three.tsrx +877 -0
  149. package/src/web/scroll-html-root.tsrx +27 -0
  150. package/src/web/useCursor.three.tsrx +19 -0
  151. package/src/web/useGesture.ts +27 -0
  152. package/status.json +14 -0
@@ -0,0 +1,390 @@
1
+ /** @jsxImportSource @octanejs/three/intrinsics */
2
+ import {
3
+ DOMRegion,
4
+ type RefObject,
5
+ type ThreeElements,
6
+ type ThreeRef,
7
+ useFrame,
8
+ useThree,
9
+ } from '@octanejs/three';
10
+ import {
11
+ useImperativeHandle,
12
+ useLayoutEffect,
13
+ useMemo,
14
+ useRef,
15
+ useState,
16
+ } from '@octanejs/three/renderer';
17
+ import type { OctaneNode } from 'octane';
18
+ import type { RendererRegion } from 'octane/universal';
19
+ import {
20
+ Camera,
21
+ DoubleSide,
22
+ Group,
23
+ Matrix4,
24
+ Mesh,
25
+ Object3D,
26
+ OrthographicCamera,
27
+ PerspectiveCamera,
28
+ Raycaster,
29
+ Vector2,
30
+ Vector3,
31
+ WebGLRenderer,
32
+ } from 'three';
33
+
34
+ const v1 = new Vector3();
35
+ const v2 = new Vector3();
36
+ const v3 = new Vector3();
37
+ const v4 = new Vector2();
38
+
39
+ export function defaultCalculatePosition(
40
+ el: Object3D,
41
+ camera: Camera,
42
+ size: { width: number; height: number },
43
+ ): [number, number] {
44
+ const objectPos = v1.setFromMatrixPosition(el.matrixWorld).project(camera);
45
+ return [
46
+ objectPos.x * (size.width / 2) + size.width / 2,
47
+ -objectPos.y * (size.height / 2) + size.height / 2,
48
+ ];
49
+ }
50
+
51
+ export type CalculatePosition = typeof defaultCalculatePosition;
52
+ export type PointerEventsProperties = CSSStyleDeclaration['pointerEvents'];
53
+
54
+ export interface HtmlProps extends Omit<ThreeElements['group'], 'children' | 'ref'> {
55
+ children?: OctaneNode;
56
+ ref?: ThreeRef<HTMLDivElement>;
57
+ prepend?: boolean;
58
+ center?: boolean;
59
+ fullscreen?: boolean;
60
+ eps?: number;
61
+ portal?: RefObject<HTMLElement | null>;
62
+ distanceFactor?: number;
63
+ sprite?: boolean;
64
+ transform?: boolean;
65
+ zIndexRange?: [number, number] | number[];
66
+ calculatePosition?: CalculatePosition;
67
+ as?: keyof HTMLElementTagNameMap | string;
68
+ wrapperClass?: string;
69
+ className?: string;
70
+ style?: Partial<CSSStyleDeclaration> & Record<string, unknown>;
71
+ pointerEvents?: PointerEventsProperties;
72
+ occlude?: RefObject<Object3D | null>[] | boolean | 'raycast' | 'blending';
73
+ onOcclude?: (hidden: boolean) => void;
74
+ material?: OctaneNode;
75
+ geometry?: OctaneNode;
76
+ castShadow?: boolean;
77
+ receiveShadow?: boolean;
78
+ }
79
+
80
+ function isObjectBehindCamera(el: Object3D, camera: Camera): boolean {
81
+ const delta = v1
82
+ .setFromMatrixPosition(el.matrixWorld)
83
+ .sub(v2.setFromMatrixPosition(camera.matrixWorld));
84
+ return delta.angleTo(camera.getWorldDirection(v3)) > Math.PI / 2;
85
+ }
86
+
87
+ function isObjectVisible(
88
+ el: Object3D,
89
+ camera: Camera,
90
+ raycaster: Raycaster,
91
+ occlude: Object3D[],
92
+ ): boolean {
93
+ const position = v1.setFromMatrixPosition(el.matrixWorld);
94
+ const projected = position.clone().project(camera);
95
+ raycaster.setFromCamera(v4.set(projected.x, projected.y), camera);
96
+ const intersection = raycaster.intersectObjects(occlude, true)[0];
97
+ return !intersection || position.distanceTo(raycaster.ray.origin) < intersection.distance;
98
+ }
99
+
100
+ function objectScale(el: Object3D, camera: Camera): number {
101
+ if (camera instanceof OrthographicCamera) return camera.zoom;
102
+ if (!(camera instanceof PerspectiveCamera)) return 1;
103
+ const distance = v1
104
+ .setFromMatrixPosition(el.matrixWorld)
105
+ .distanceTo(v2.setFromMatrixPosition(camera.matrixWorld));
106
+ return 1 / (2 * Math.tan((camera.fov * Math.PI) / 360) * distance);
107
+ }
108
+
109
+ function objectZIndex(el: Object3D, camera: Camera, range: readonly number[]): number | undefined {
110
+ if (!(camera instanceof PerspectiveCamera || camera instanceof OrthographicCamera)) return undefined;
111
+ const distance = v1
112
+ .setFromMatrixPosition(el.matrixWorld)
113
+ .distanceTo(v2.setFromMatrixPosition(camera.matrixWorld));
114
+ const slope = (range[1] - range[0]) / (camera.far - camera.near);
115
+ return Math.round(slope * distance + range[1] - slope * camera.far);
116
+ }
117
+
118
+ const epsilon = (value: number) => (Math.abs(value) < 1e-10 ? 0 : value);
119
+
120
+ function cssMatrix(matrix: Matrix4, multipliers: number[], prepend = ''): string {
121
+ return `${prepend}matrix3d(${matrix.elements
122
+ .map((value, index) => epsilon(value * multipliers[index]))
123
+ .join(',')})`;
124
+ }
125
+
126
+ const cameraMultipliers = [1, -1, 1, 1, 1, -1, 1, 1, 1, -1, 1, 1, 1, -1, 1, 1];
127
+ const objectMultipliers = (factor: number) => [
128
+ 1 / factor, 1 / factor, 1 / factor, 1,
129
+ -1 / factor, -1 / factor, -1 / factor, -1,
130
+ 1 / factor, 1 / factor, 1 / factor, 1,
131
+ 1, 1, 1, 1,
132
+ ];
133
+
134
+ const NON_TRANSFORM_VERTEX_SHADER = `
135
+ /*
136
+ This shader is from the THREE's SpriteMaterial.
137
+ We need to turn the backing plane into a Sprite
138
+ (make it always face the camera) if "transfrom"
139
+ is false.
140
+ */
141
+ #include <common>
142
+
143
+ void main() {
144
+ vec2 center = vec2(0., 1.);
145
+ float rotation = 0.0;
146
+
147
+ // This is somewhat arbitrary, but it seems to work well
148
+ // Need to figure out how to derive this dynamically if it even matters
149
+ float size = 0.03;
150
+
151
+ vec4 mvPosition = modelViewMatrix * vec4( 0.0, 0.0, 0.0, 1.0 );
152
+ vec2 scale;
153
+ scale.x = length( vec3( modelMatrix[ 0 ].x, modelMatrix[ 0 ].y, modelMatrix[ 0 ].z ) );
154
+ scale.y = length( vec3( modelMatrix[ 1 ].x, modelMatrix[ 1 ].y, modelMatrix[ 1 ].z ) );
155
+
156
+ bool isPerspective = isPerspectiveMatrix( projectionMatrix );
157
+ if ( isPerspective ) scale *= - mvPosition.z;
158
+
159
+ vec2 alignedPosition = ( position.xy - ( center - vec2( 0.5 ) ) ) * scale * size;
160
+ vec2 rotatedPosition;
161
+ rotatedPosition.x = cos( rotation ) * alignedPosition.x - sin( rotation ) * alignedPosition.y;
162
+ rotatedPosition.y = sin( rotation ) * alignedPosition.x + cos( rotation ) * alignedPosition.y;
163
+ mvPosition.xy += rotatedPosition;
164
+
165
+ gl_Position = projectionMatrix * mvPosition;
166
+ }
167
+ `;
168
+
169
+ const TRANSPARENT_FRAGMENT_SHADER = `
170
+ void main() {
171
+ gl_FragColor = vec4(0.0, 0.0, 0.0, 0.0);
172
+ }
173
+ `;
174
+
175
+ function assignStyle(element: HTMLElement, style: HtmlProps['style']): void {
176
+ if (!style) return;
177
+ for (const [key, value] of Object.entries(style)) {
178
+ if (value !== undefined && value !== null) (element.style as any)[key] = value;
179
+ }
180
+ }
181
+
182
+ export function Html({
183
+ children,
184
+ ref: forwardedRef,
185
+ eps = 0.001,
186
+ style,
187
+ className,
188
+ prepend,
189
+ center,
190
+ fullscreen,
191
+ portal,
192
+ distanceFactor,
193
+ sprite = false,
194
+ transform = false,
195
+ occlude,
196
+ onOcclude,
197
+ castShadow,
198
+ receiveShadow,
199
+ material,
200
+ geometry,
201
+ zIndexRange = [16777271, 0],
202
+ calculatePosition = defaultCalculatePosition,
203
+ as = 'div',
204
+ wrapperClass,
205
+ pointerEvents = 'auto',
206
+ ...props
207
+ }: HtmlProps) @{
208
+ const state = useThree();
209
+ const [el] = useState(() => document.createElement(as));
210
+ const [transformOuter] = useState(() => document.createElement('div'));
211
+ const [transformInner] = useState(() => document.createElement('div'));
212
+ const group = useRef<Group | null>(null);
213
+ const content = useRef<HTMLDivElement | null>(null);
214
+ const mesh = useRef<Mesh | null>(null);
215
+ const visible = useRef(true);
216
+ const oldZoom = useRef(0);
217
+ const oldPosition = useRef<[number, number]>([0, 0]);
218
+ const meshSized = useRef(false);
219
+ const isRaycastOcclusion = useMemo(
220
+ () => Boolean(occlude && occlude !== 'blending'),
221
+ [occlude],
222
+ );
223
+ const canvas = (state.gl as unknown as WebGLRenderer).domElement;
224
+ const target = (portal?.current ?? state.events.connected ?? canvas.parentNode) as HTMLElement;
225
+ const regionTarget = transform ? transformInner : el;
226
+
227
+ useImperativeHandle(
228
+ (forwardedRef ?? null) as any,
229
+ () => content.current!,
230
+ [el],
231
+ );
232
+
233
+ useLayoutEffect(() => {
234
+ const canvas = (state.gl as unknown as WebGLRenderer).domElement as HTMLCanvasElement;
235
+ if (occlude === 'blending') {
236
+ canvas.style.zIndex = `${Math.floor(zIndexRange[0] / 2)}`;
237
+ canvas.style.position = 'absolute';
238
+ canvas.style.pointerEvents = 'none';
239
+ } else {
240
+ canvas.style.zIndex = '';
241
+ canvas.style.position = '';
242
+ canvas.style.pointerEvents = '';
243
+ }
244
+ }, [state.gl, occlude, zIndexRange]);
245
+
246
+ useLayoutEffect(() => {
247
+ state.scene.updateMatrixWorld();
248
+ if (transform) {
249
+ el.style.cssText = 'position:absolute;top:0;left:0;pointer-events:none;overflow:hidden;';
250
+ if (transformOuter.parentElement !== el) el.append(transformOuter);
251
+ if (transformInner.parentElement !== transformOuter) transformOuter.append(transformInner);
252
+ }
253
+ else {
254
+ transformOuter.remove();
255
+ const position = group.current
256
+ ? calculatePosition(group.current, state.camera, state.size)
257
+ : [0, 0];
258
+ el.style.cssText = `position:absolute;top:0;left:0;transform:translate3d(${position[0]}px,${position[1]}px,0);transform-origin:0 0;`;
259
+ }
260
+ if (prepend) target.prepend(el);
261
+ else target.append(el);
262
+ return () => el.remove();
263
+ }, [target, transform, prepend, el, transformOuter, transformInner, state.scene, state.camera, state.size, calculatePosition]);
264
+
265
+ useLayoutEffect(() => {
266
+ if (wrapperClass) el.className = wrapperClass;
267
+ meshSized.current = false;
268
+ const node = content.current;
269
+ if (!node) return;
270
+ node.className = className ?? '';
271
+ if (transform) {
272
+ transformOuter.style.cssText = `position:absolute;top:0;left:0;width:${state.size.width}px;height:${state.size.height}px;transform-style:preserve-3d;pointer-events:none;`;
273
+ transformInner.style.cssText = `position:absolute;pointer-events:${pointerEvents};`;
274
+ node.style.cssText = '';
275
+ } else {
276
+ node.style.cssText = `position:absolute;transform:${center ? 'translate3d(-50%,-50%,0)' : 'none'};`;
277
+ }
278
+ if (!transform && fullscreen) {
279
+ node.style.top = `${-state.size.height / 2}px`;
280
+ node.style.left = `${-state.size.width / 2}px`;
281
+ node.style.width = `${state.size.width}px`;
282
+ node.style.height = `${state.size.height}px`;
283
+ }
284
+ assignStyle(node, style);
285
+ }, [el, transformOuter, transformInner, wrapperClass, className, style, center, fullscreen, transform, pointerEvents, state.size]);
286
+
287
+ useFrame(() => {
288
+ const object = group.current;
289
+ if (!object) return;
290
+ state.camera.updateMatrixWorld();
291
+ object.updateWorldMatrix(true, false);
292
+ const position = transform ? oldPosition.current : calculatePosition(object, state.camera, state.size);
293
+ if (
294
+ transform ||
295
+ Math.abs(oldZoom.current - state.camera.zoom) > eps ||
296
+ Math.abs(oldPosition.current[0] - position[0]) > eps ||
297
+ Math.abs(oldPosition.current[1] - position[1]) > eps
298
+ ) {
299
+ let shown = !isObjectBehindCamera(object, state.camera);
300
+ if (shown && isRaycastOcclusion) {
301
+ const targets = Array.isArray(occlude)
302
+ ? occlude.map((item) => item.current).filter((item): item is Object3D => item !== null)
303
+ : [state.scene];
304
+ shown = isObjectVisible(object, state.camera, state.raycaster, targets);
305
+ }
306
+ if (visible.current !== shown) {
307
+ visible.current = shown;
308
+ if (onOcclude) onOcclude(!shown);
309
+ else el.style.display = shown ? 'block' : 'none';
310
+ }
311
+ const half = Math.floor(zIndexRange[0] / 2);
312
+ const range = occlude
313
+ ? isRaycastOcclusion ? [zIndexRange[0], half] : [half - 1, 0]
314
+ : zIndexRange;
315
+ el.style.zIndex = `${objectZIndex(object, state.camera, range)}`;
316
+ if (transform) {
317
+ const fov = state.camera.projectionMatrix.elements[5] * state.size.height / 2;
318
+ const camera = state.camera as OrthographicCamera;
319
+ const cameraTransform = camera.isOrthographicCamera
320
+ ? `scale(${fov})translate(${epsilon(-(camera.right + camera.left) / 2)}px,${epsilon((camera.top + camera.bottom) / 2)}px)`
321
+ : `translateZ(${fov}px)`;
322
+ let matrix = object.matrixWorld;
323
+ if (sprite) {
324
+ matrix = state.camera.matrixWorldInverse.clone().transpose().copyPosition(matrix).scale(object.scale);
325
+ matrix.elements[3] = matrix.elements[7] = matrix.elements[11] = 0;
326
+ matrix.elements[15] = 1;
327
+ }
328
+ el.style.width = `${state.size.width}px`;
329
+ el.style.height = `${state.size.height}px`;
330
+ el.style.perspective = camera.isOrthographicCamera ? '' : `${fov}px`;
331
+ transformOuter.style.transform = `${cameraTransform}${cssMatrix(state.camera.matrixWorldInverse, cameraMultipliers)}translate(${state.size.width / 2}px,${state.size.height / 2}px)`;
332
+ transformInner.style.transform = cssMatrix(
333
+ matrix,
334
+ objectMultipliers(1 / ((distanceFactor || 10) / 400)),
335
+ 'translate(-50%,-50%)',
336
+ );
337
+ } else {
338
+ const scale = distanceFactor === undefined ? 1 : objectScale(object, state.camera) * distanceFactor;
339
+ el.style.transform = `translate3d(${position[0]}px,${position[1]}px,0) scale(${scale})`;
340
+ }
341
+ oldPosition.current = position;
342
+ oldZoom.current = state.camera.zoom;
343
+ }
344
+
345
+ const plane = mesh.current;
346
+ const node = content.current;
347
+ if (!isRaycastOcclusion && plane && !meshSized.current) {
348
+ if (transform) {
349
+ const measured = transformOuter.children[0] as HTMLElement | undefined;
350
+ if (measured?.clientWidth && measured.clientHeight) {
351
+ const camera = state.camera as OrthographicCamera;
352
+ if (camera.isOrthographicCamera || geometry) {
353
+ if (props.scale) {
354
+ if (!Array.isArray(props.scale)) plane.scale.setScalar(1 / (props.scale as number));
355
+ else if (props.scale instanceof Vector3) plane.scale.copy(props.scale.clone().divideScalar(1));
356
+ else plane.scale.set(1 / props.scale[0], 1 / props.scale[1], 1 / props.scale[2]);
357
+ }
358
+ } else {
359
+ const ratio = (distanceFactor || 10) / 400;
360
+ plane.scale.set(measured.clientWidth * ratio, measured.clientHeight * ratio, 1);
361
+ }
362
+ meshSized.current = true;
363
+ }
364
+ } else {
365
+ if (node?.clientWidth && node.clientHeight) {
366
+ const ratio = 1 / state.viewport.factor;
367
+ plane.scale.set(node.clientWidth * ratio, node.clientHeight * ratio, 1);
368
+ meshSized.current = true;
369
+ }
370
+ plane.lookAt(state.camera.position);
371
+ }
372
+ }
373
+ });
374
+
375
+ <group {...props} ref={group}>
376
+ <DOMRegion target={regionTarget} region={children as RendererRegion} containerRef={content} />
377
+ @if (occlude && !isRaycastOcclusion) {
378
+ <mesh ref={mesh} castShadow={castShadow} receiveShadow={receiveShadow}>
379
+ @if (geometry) { {geometry} } @else { <planeGeometry /> }
380
+ @if (material) { {material} } @else {
381
+ <shaderMaterial
382
+ side={DoubleSide}
383
+ vertexShader={transform ? undefined : NON_TRANSFORM_VERTEX_SHADER}
384
+ fragmentShader={TRANSPARENT_FRAGMENT_SHADER}
385
+ />
386
+ }
387
+ </mesh>
388
+ }
389
+ </group>
390
+ }
@@ -0,0 +1,98 @@
1
+ import { create } from '@octanejs/zustand/universal';
2
+ import { subscribeWithSelector } from '@octanejs/zustand/middleware';
3
+ import { createContext, type OctaneNode, useContext, useEffect, useMemo } from 'octane';
4
+ import type {
5
+ KeyboardControlsApi,
6
+ KeyboardControlsState,
7
+ UseKeyboardControls,
8
+ } from './keyboard-controls-types.js';
9
+
10
+ export type KeyboardControlsEntry<T extends string = string> = {
11
+ name: T;
12
+ keys: string[];
13
+ up?: boolean;
14
+ };
15
+
16
+ export type KeyboardControlsProps = {
17
+ map: KeyboardControlsEntry[];
18
+ children: OctaneNode;
19
+ onChange?: (name: string, pressed: boolean, state: KeyboardControlsState) => void;
20
+ domElement?: HTMLElement;
21
+ };
22
+
23
+ const KeyboardContext = createContext<KeyboardControlsApi>(null!);
24
+
25
+ export function KeyboardControls({ map, children, onChange, domElement }: KeyboardControlsProps) @{
26
+ const key = map.map((item) => item.name + item.keys).join('-');
27
+ const useControls = useMemo(
28
+ () => create(
29
+ subscribeWithSelector(
30
+ () => map.reduce(
31
+ (previous, current) => ({ ...previous, [current.name]: false }),
32
+ {} as KeyboardControlsState,
33
+ ),
34
+ ),
35
+ ),
36
+ [key],
37
+ );
38
+ const api = useMemo<KeyboardControlsApi>(
39
+ () => [useControls.subscribe, useControls.getState, useControls],
40
+ [key],
41
+ );
42
+ const set = useControls.setState;
43
+ useEffect(() => {
44
+ const config = map.map(
45
+ ({ name, keys, up }) => ({
46
+ keys,
47
+ up,
48
+ fn: (value: boolean) => {
49
+ set({ [name]: value });
50
+ onChange?.(name, value, api[1]());
51
+ },
52
+ }),
53
+ );
54
+ const keyMap = config.reduce<
55
+ Record<string, { fn(value: boolean): void; pressed: boolean; up: boolean }>
56
+ >((output, { keys, fn, up = true }) => {
57
+ keys.forEach((value) => (output[value] = { fn, pressed: false, up }));
58
+ return output;
59
+ }, {});
60
+ const downHandler = ({ key: value, code }: KeyboardEvent) => {
61
+ const object = keyMap[value] || keyMap[code];
62
+ if (!object) return;
63
+ const { fn, pressed, up } = object;
64
+ object.pressed = true;
65
+ if (up || !pressed) fn(true);
66
+ };
67
+ const upHandler = ({ key: value, code }: KeyboardEvent) => {
68
+ const object = keyMap[value] || keyMap[code];
69
+ if (!object) return;
70
+ const { fn, up } = object;
71
+ object.pressed = false;
72
+ if (up) fn(false);
73
+ };
74
+ const source = domElement || window;
75
+ source.addEventListener('keydown', downHandler as EventListener, { passive: true });
76
+ source.addEventListener('keyup', upHandler as EventListener, { passive: true });
77
+ return () => {
78
+ source.removeEventListener('keydown', downHandler as EventListener);
79
+ source.removeEventListener('keyup', upHandler as EventListener);
80
+ };
81
+ }, [domElement, key]);
82
+ <KeyboardContext.Provider value={api}>{children}</KeyboardContext.Provider>
83
+ }
84
+
85
+ type Selector<T extends string = string> = (state: KeyboardControlsState<T>) => boolean;
86
+
87
+ export const useKeyboardControls: UseKeyboardControls = (
88
+ ...rest: [selector?: Selector | symbol, slot?: symbol]
89
+ ) => {
90
+ const [subscribe, get, store] = useContext(KeyboardContext);
91
+ const tail = rest.at(-1);
92
+ const slot =
93
+ typeof tail === 'symbol' ? tail : undefined;
94
+ const selector =
95
+ typeof rest[0] === 'function' ? rest[0] : undefined;
96
+ if (selector) return (store as any)(selector, slot);
97
+ return [subscribe, get];
98
+ };
@@ -0,0 +1,61 @@
1
+ import { useCallback, useEffect, useRef, useState, useSyncExternalStore } from 'octane';
2
+ import { useProgress } from '../core/Progress.three.tsrx';
3
+
4
+ type Style = Record<string, string | number>;
5
+ interface LoaderOptions {
6
+ containerStyles: Style;
7
+ innerStyles: Style;
8
+ barStyles: Style;
9
+ dataStyles: Style;
10
+ dataInterpolation: (progress: number) => string;
11
+ initialState: (active: boolean) => boolean;
12
+ }
13
+
14
+ const defaultDataInterpolation = (progress: number) => `Loading ${progress.toFixed(2)}%`;
15
+
16
+ export function Loader({
17
+ containerStyles,
18
+ innerStyles,
19
+ barStyles,
20
+ dataStyles,
21
+ dataInterpolation = defaultDataInterpolation,
22
+ initialState = (active: boolean) => active,
23
+ }: Partial<LoaderOptions>) {
24
+ const { active, progress } = useSyncExternalStore(useProgress.subscribe, useProgress.getState, useProgress.getInitialState);
25
+ const progressRef = useRef(0);
26
+ const rafRef = useRef(0);
27
+ const progressSpanRef = useRef<HTMLSpanElement | null>(null);
28
+ const [shown, setShown] = useState(initialState(active));
29
+ useEffect(() => {
30
+ let timeout: ReturnType<typeof setTimeout> | undefined;
31
+ if (active !== shown) timeout = setTimeout(() => setShown(active), 300);
32
+ return () => { if (timeout !== undefined) clearTimeout(timeout); };
33
+ }, [shown, active]);
34
+ const updateProgress = useCallback(() => {
35
+ if (!progressSpanRef.current) return;
36
+ progressRef.current += (progress - progressRef.current) / 2;
37
+ if (progressRef.current > 0.95 * progress || progress === 100) progressRef.current = progress;
38
+ progressSpanRef.current.innerText = dataInterpolation(progressRef.current);
39
+ if (progressRef.current < progress) rafRef.current = requestAnimationFrame(updateProgress);
40
+ }, [dataInterpolation, progress]);
41
+ useEffect(() => {
42
+ updateProgress();
43
+ return () => cancelAnimationFrame(rafRef.current);
44
+ }, [updateProgress]);
45
+ if (!shown) return null;
46
+ return <div style={{ ...styles.container, opacity: active ? 1 : 0, ...containerStyles }}>
47
+ <div>
48
+ <div style={{ ...styles.inner, ...innerStyles }}>
49
+ <div style={{ ...styles.bar, transform: `scaleX(${progress / 100})`, ...barStyles }} />
50
+ <span ref={progressSpanRef} style={{ ...styles.data, ...dataStyles }} />
51
+ </div>
52
+ </div>
53
+ </div>;
54
+ }
55
+
56
+ const styles: Record<string, Style> = {
57
+ container: { position: 'absolute', top: 0, left: 0, width: '100%', height: '100%', background: '#171717', display: 'flex', alignItems: 'center', justifyContent: 'center', transition: 'opacity 300ms ease', zIndex: 1000 },
58
+ inner: { width: 100, height: 3, background: '#272727', textAlign: 'center' },
59
+ bar: { height: 3, width: '100%', background: 'white', transition: 'transform 200ms', transformOrigin: 'left center' },
60
+ data: { display: 'inline-block', position: 'relative', fontVariantNumeric: 'tabular-nums', marginTop: '0.8em', color: '#f0f0f0', fontSize: '0.6em', fontFamily: '-apple-system, BlinkMacSystemFont, "Inter", "Segoe UI", "Helvetica Neue", Helvetica, Arial, Roboto, Ubuntu, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol"', whiteSpace: 'nowrap' },
61
+ };
@@ -0,0 +1,63 @@
1
+ /** @jsxImportSource @octanejs/three/intrinsics */
2
+ import { useFrame, useThree } from '@octanejs/three';
3
+ import { useEffect, useMemo, useRef, useState } from '@octanejs/three/renderer';
4
+ import { easing } from 'maath';
5
+ import type { OctaneNode } from 'octane';
6
+ import { Group, MathUtils } from 'three';
7
+ import { useGesture } from './useGesture.js';
8
+
9
+ export type PresentationControlProps = {
10
+ snap?: Boolean | number;
11
+ global?: boolean;
12
+ cursor?: boolean;
13
+ speed?: number;
14
+ zoom?: number;
15
+ rotation?: [number, number, number];
16
+ polar?: [number, number];
17
+ azimuth?: [number, number];
18
+ damping?: number;
19
+ enabled?: boolean;
20
+ children?: OctaneNode;
21
+ domElement?: HTMLElement;
22
+ };
23
+
24
+ export function PresentationControls({
25
+ enabled = true, snap, global, domElement, cursor = true, children, speed = 1, rotation = [0, 0, 0], zoom = 1,
26
+ polar = [0, Math.PI / 2], azimuth = [-Infinity, Infinity], damping = 0.25,
27
+ }: PresentationControlProps) @{
28
+ const events = useThree((state) => state.events);
29
+ const gl = useThree((state) => state.gl);
30
+ const canvas = gl.domElement as HTMLElement;
31
+ const size = useThree((state) => state.size);
32
+ const target = domElement || events.connected || canvas;
33
+ const rPolar = useMemo(() => [rotation[0] + polar[0], rotation[0] + polar[1]] as [number, number], [rotation[0], polar[0], polar[1]]);
34
+ const rAzimuth = useMemo(() => [rotation[1] + azimuth[0], rotation[1] + azimuth[1]] as [number, number], [rotation[1], azimuth[0], azimuth[1]]);
35
+ const rInitial = useMemo(() => [MathUtils.clamp(rotation[0], ...rPolar), MathUtils.clamp(rotation[1], ...rAzimuth), rotation[2]], [rotation[0], rotation[1], rotation[2], rPolar, rAzimuth]);
36
+ useEffect(() => {
37
+ if (global && cursor && enabled) {
38
+ target.style.cursor = 'grab'; canvas.style.cursor = '';
39
+ return () => { target.style.cursor = 'default'; canvas.style.cursor = 'default'; };
40
+ }
41
+ }, [global, cursor, target, enabled]);
42
+ const [animation] = useState({ scale: 1, rotation: rInitial, damping });
43
+ const ref = useRef<Group | null>(null);
44
+ useFrame((_state, delta) => {
45
+ if (!ref.current) return;
46
+ easing.damp3(ref.current.scale, animation.scale, animation.damping, delta);
47
+ easing.dampE(ref.current.rotation, animation.rotation as any, animation.damping, delta);
48
+ });
49
+ const bind = useGesture({
50
+ onHover: ({ last }: any) => { if (cursor && !global && enabled) target.style.cursor = last ? 'auto' : 'grab'; },
51
+ onDrag: ({ down, delta: [deltaX, deltaY], memo: [oldY, oldX] = animation.rotation || rInitial }: any) => {
52
+ if (!enabled) return [deltaY, deltaX];
53
+ if (cursor) target.style.cursor = down ? 'grabbing' : 'grab';
54
+ const x = MathUtils.clamp(oldX + (deltaX / size.width) * Math.PI * speed, ...rAzimuth);
55
+ const y = MathUtils.clamp(oldY + (deltaY / size.height) * Math.PI * speed, ...rPolar);
56
+ animation.scale = down && y > rPolar[1] / 2 ? zoom : 1;
57
+ animation.rotation = snap && !down ? rInitial : [y, x, 0];
58
+ animation.damping = snap && !down && typeof snap !== 'boolean' ? snap as number : damping;
59
+ return [y, x];
60
+ },
61
+ }, { target: global ? target : undefined });
62
+ <group ref={ref} {...((bind?.() ?? {}) as any)}>{children}</group>
63
+ }