@14islands/r3f-scroll-rig
Version:
Progressively enhance any React website with WebGL using @react-three/fiber
1 lines • 96.8 kB
Source Map (JSON)
{"version":3,"file":"scrollrig.cjs","sources":["../src/hooks/useIsomorphicLayoutEffect.ts","../src/config.ts","../src/store.ts","../src/components/ResizeManager.ts","../src/components/PerspectiveCamera.tsx","../src/components/OrthographicCamera.tsx","../src/utils/helpers.ts","../src/renderer-api.ts","../src/components/GlobalCanvas.tsx","../src/hooks/useScrollRig.ts","../src/components/GlobalChildren.tsx","../src/components/GlobalRenderer.tsx","../src/components/CanvasErrorBoundary.tsx","../src/components/DebugMesh.tsx","../src/hooks/useWindowSize.ts","../src/utils/math.ts","../src/scrollbar/useScrollbar.ts","../src/hooks/useTracker.ts","../src/components/ScrollScene.tsx","../src/components/ViewportScrollScene.tsx","../src/hooks/useCanvas.ts","../src/components/UseCanvas.tsx","../src/hooks/useImageAsTexture.ts","../src/scrollbar/SmoothScrollbar.tsx","../src/components/R3FSmoothScrollbar.tsx","../src/index.ts"],"sourcesContent":["import { useEffect, useLayoutEffect as vanillaUseLayoutEffect } from 'react'\n\nexport const isBrowser = typeof window !== 'undefined'\n\nexport const useLayoutEffect = isBrowser ? vanillaUseLayoutEffect : useEffect\n","// Global config\n\n// avoid Three types to ease tree shaking\ntype PreloadCallback = (gl: any, scene: any, camera: any) => void\n\nexport const config = {\n // Execution order for useFrames (highest = last render)\n PRIORITY_PRELOAD: 0,\n PRIORITY_SCISSORS: 1,\n PRIORITY_VIEWPORTS: 1,\n PRIORITY_GLOBAL: 1000,\n\n DEFAULT_SCALE_MULTIPLIER: 1,\n\n // Global rendering props\n preloadQueue: [] as PreloadCallback[],\n}\n","import create from 'zustand'\nimport { config } from './config'\nimport type Lenis from 'lenis'\n\nimport { ScrollCallback, ScrollData } from './scrollbar/SmoothScrollbarTypes'\n\ninterface ScrollRigStore {\n debug: boolean\n scaleMultiplier: number\n globalRender: boolean\n globalPriority: number\n globalClearDepth: boolean\n globalRenderQueue: false | any[]\n clearGlobalRenderQueue: () => void\n isCanvasAvailable: boolean\n hasSmoothScrollbar: boolean\n canvasChildren: Record<string, any | undefined>\n updateCanvas: (key: string, newProps: any) => void\n renderToCanvas: (key: string, mesh: any, props: any) => void\n removeFromCanvas: (key: string, dispose: boolean) => void\n pageReflow: number\n requestReflow: () => void\n scroll: ScrollData\n __lenis: Lenis | undefined\n scrollTo: (target: any) => void\n onScroll: (cb: ScrollCallback) => () => void\n}\n\nconst useCanvasStore = create<ScrollRigStore>((set) => ({\n // //////////////////////////////////////////////////////////////////////////\n // GLOBAL ScrollRig STATE\n // //////////////////////////////////////////////////////////////////////////\n debug: false,\n scaleMultiplier: config.DEFAULT_SCALE_MULTIPLIER,\n\n globalRender: true,\n globalPriority: config.PRIORITY_GLOBAL,\n globalClearDepth: false,\n\n globalRenderQueue: false,\n clearGlobalRenderQueue: () => set(() => ({ globalRenderQueue: false })),\n\n // true if WebGL initialized without errors\n isCanvasAvailable: false,\n\n // true if <VirtualScrollbar> is currently enabled\n hasSmoothScrollbar: false,\n\n // map of all components to render on the global canvas\n canvasChildren: {},\n\n // add component to canvas\n renderToCanvas: (key, mesh, props = {}) =>\n set(({ canvasChildren }) => {\n // check if already mounted\n if (Object.getOwnPropertyDescriptor(canvasChildren, key)) {\n // increase usage count\n canvasChildren[key].instances += 1\n canvasChildren[key].props.inactive = false\n return { canvasChildren }\n } else {\n // otherwise mount it\n const obj = { ...canvasChildren, [key]: { mesh, props, instances: 1 } }\n return { canvasChildren: obj }\n }\n }),\n\n // pass new props to a canvas component\n updateCanvas: (key, newProps) =>\n // @ts-ignore\n set(({ canvasChildren }) => {\n if (!canvasChildren[key]) return\n const {\n [key]: { mesh, props, instances },\n } = canvasChildren\n const obj = {\n ...canvasChildren,\n [key]: { mesh, props: { ...props, ...newProps }, instances },\n }\n // console.log('updateCanvas', key, { ...props, ...newProps })\n return { canvasChildren: obj }\n }),\n\n // remove component from canvas\n removeFromCanvas: (key, dispose = true) =>\n set(({ canvasChildren }) => {\n // check if remove or reduce instances\n if (canvasChildren[key]?.instances > 1) {\n // reduce usage count\n canvasChildren[key].instances -= 1\n return { canvasChildren }\n } else {\n if (dispose) {\n // unmount since no longer used\n const { [key]: _omit, ...obj } = canvasChildren // make a separate copy of the obj and omit\n return { canvasChildren: obj }\n } else {\n // or tell it that it is \"inactive\"\n canvasChildren[key].instances = 0\n canvasChildren[key].props.inactive = true\n return { canvasChildren: { ...canvasChildren } }\n }\n }\n }),\n\n // Used to ask components to re-calculate their positions after a layout reflow\n pageReflow: 0,\n requestReflow: () => {\n set((state) => {\n return { pageReflow: state.pageReflow + 1 }\n })\n },\n\n // keep track of scrollbar\n scroll: {\n y: 0,\n x: 0,\n limit: 0,\n velocity: 0,\n progress: 0,\n direction: 0,\n scrollDirection: undefined,\n },\n __lenis: undefined,\n scrollTo: () => {},\n onScroll: () => () => {},\n}))\n\nexport { useCanvasStore }\n","import { useEffect } from 'react'\nimport { ResizeObserver as Polyfill } from '@juggle/resize-observer'\n\nimport { useCanvasStore } from '../store'\n\n/**\n * Trigger reflow when WebFonts loaded\n */\nexport const ResizeManager = () => {\n const requestReflow = useCanvasStore((state) => state.requestReflow)\n const debug = useCanvasStore((state) => state.debug)\n\n // reflow on webfont loaded to prevent misalignments\n useEffect(() => {\n const ResizeObserver = window.ResizeObserver || Polyfill\n\n // watch out for any random height change\n let observer = new ResizeObserver(() => {\n requestReflow()\n debug && console.log('ResizeManager', 'document.body height changed')\n })\n observer.observe(document.body)\n return () => {\n observer?.disconnect()\n }\n }, [])\n\n return null\n}\n","import React, { useRef, forwardRef, useMemo, useImperativeHandle } from 'react'\nimport { useThree } from '@react-three/fiber'\nimport { PerspectiveCamera as PerspectiveCameraImpl } from 'three'\n\nimport { useLayoutEffect } from '../hooks/useIsomorphicLayoutEffect'\nimport { useCanvasStore } from '../store'\n\ntype Props = JSX.IntrinsicElements['perspectiveCamera'] & {\n makeDefault?: boolean\n margin?: number\n}\n\nconst DEFAULT_FOV = 50\n\nexport const PerspectiveCamera = forwardRef(({ makeDefault = false, margin = 0, ...props }: Props, ref) => {\n const set = useThree((state) => state.set)\n const camera = useThree((state) => state.camera)\n const size = useThree((state) => state.size)\n const viewport = useThree((state) => state.viewport)\n const cameraRef = useRef<PerspectiveCameraImpl>(null!)\n useImperativeHandle(ref, () => cameraRef.current)\n\n const pageReflow = useCanvasStore((state) => state.pageReflow)\n const scaleMultiplier = useCanvasStore((state) => state.scaleMultiplier)\n\n // Calculate FoV or distance to match DOM size\n const { fov, distance, aspect } = useMemo(() => {\n const width = (size.width + margin * 2) * scaleMultiplier\n const height = (size.height + margin * 2) * scaleMultiplier\n const aspect = width / height\n\n // check props vs defaults\n let fov = props.fov || DEFAULT_FOV\n let distance = (props?.position as number[])?.[2]\n\n // calculate either FoV or distance to match scale\n if (distance) {\n // calculate FoV based on distance\n fov = 2 * (180 / Math.PI) * Math.atan(height / (2 * distance))\n } else {\n // calculate distance for specified FoV\n const ratio = Math.tan(((fov / 2.0) * Math.PI) / 180.0) * 2.0\n distance = height / ratio\n }\n\n return { fov, distance, aspect }\n }, [size, scaleMultiplier, pageReflow])\n\n // Update camera projection and R3F viewport\n useLayoutEffect(() => {\n cameraRef.current.lookAt(0, 0, 0)\n cameraRef.current.updateProjectionMatrix()\n // https://github.com/react-spring/@react-three/fiber/issues/178\n // Update matrix world since the renderer is a frame late\n cameraRef.current.updateMatrixWorld()\n // update r3f viewport which is lagging on resize\n set((state) => ({ viewport: { ...state.viewport, ...viewport.getCurrentViewport(camera) } }))\n }, [size, scaleMultiplier, pageReflow])\n\n useLayoutEffect(() => {\n if (makeDefault) {\n const oldCam = camera\n set(() => ({ camera: cameraRef.current! }))\n return () => set(() => ({ camera: oldCam }))\n }\n // The camera should not be part of the dependency list because this components camera is a stable reference\n // that must exchange the default, and clean up after itself on unmount.\n }, [cameraRef, makeDefault, set])\n\n return (\n <perspectiveCamera\n ref={cameraRef}\n position={[0, 0, distance]}\n onUpdate={(self) => self.updateProjectionMatrix()}\n near={0.1}\n aspect={aspect}\n fov={fov}\n far={distance * 2}\n {...props}\n />\n )\n})\n","import React, { useRef, forwardRef, useMemo, useImperativeHandle } from 'react'\nimport { OrthographicCamera as OrthographicCameraImpl } from 'three'\nimport { useThree } from '@react-three/fiber'\n\nimport { useLayoutEffect } from '../hooks/useIsomorphicLayoutEffect'\nimport { useCanvasStore } from '../store'\n\ntype Props = JSX.IntrinsicElements['orthographicCamera'] & {\n makeDefault?: boolean\n margin?: number\n}\nexport const OrthographicCamera = forwardRef(({ makeDefault = false, margin = 0, ...props }: Props, ref) => {\n const set = useThree((state) => state.set)\n const camera = useThree((state) => state.camera)\n const size = useThree((state) => state.size)\n\n const pageReflow = useCanvasStore((state) => state.pageReflow)\n const scaleMultiplier = useCanvasStore((state) => state.scaleMultiplier)\n\n const distance = useMemo(() => {\n const width = size.width * scaleMultiplier\n const height = size.height * scaleMultiplier\n return Math.max(width, height)\n }, [size, pageReflow, scaleMultiplier])\n\n const cameraRef = useRef<OrthographicCameraImpl>(null!)\n useImperativeHandle(ref, () => cameraRef.current)\n useLayoutEffect(() => {\n cameraRef.current.lookAt(0, 0, 0)\n cameraRef.current.updateProjectionMatrix()\n // https://github.com/react-spring/@react-three/fiber/issues/178\n // Update matrix world since the renderer is a frame late\n cameraRef.current.updateMatrixWorld()\n }, [distance, size])\n\n useLayoutEffect(() => {\n if (makeDefault) {\n const oldCam = camera\n set(() => ({ camera: cameraRef.current! }))\n return () => set(() => ({ camera: oldCam }))\n }\n // The camera should not be part of the dependency list because this components camera is a stable reference\n // that must exchange the default, and clean up after itself on unmount.\n }, [cameraRef, makeDefault, set])\n\n return (\n <orthographicCamera\n left={(size.width * scaleMultiplier) / -2 - margin * scaleMultiplier}\n right={(size.width * scaleMultiplier) / 2 + margin * scaleMultiplier}\n top={(size.height * scaleMultiplier) / 2 + margin * scaleMultiplier}\n bottom={(size.height * scaleMultiplier) / -2 - margin * scaleMultiplier}\n far={distance * 2}\n position={[0, 0, distance]}\n near={0.001}\n ref={cameraRef}\n onUpdate={(self) => self.updateProjectionMatrix()}\n {...props}\n />\n )\n})\n","import { Object3D } from 'three'\n\ntype CulledObject = {\n wasFrustumCulled?: boolean\n wasVisible?: boolean\n} & Object3D\n\n// Use to override Frustum temporarily to pre-upload textures to GPU\nexport function setAllCulled(obj: CulledObject, overrideCulled: boolean) {\n if (!obj) return\n if (overrideCulled === false) {\n obj.wasFrustumCulled = obj.frustumCulled\n obj.wasVisible = obj.visible\n obj.visible = true\n obj.frustumCulled = false\n } else {\n obj.visible = !!obj.wasVisible\n obj.frustumCulled = !!obj.wasFrustumCulled\n }\n obj.children.forEach((child) => setAllCulled(child, overrideCulled))\n}\n","import { config } from './config'\nimport { Vector2, WebGLRenderer, Scene, Camera } from 'three'\nimport { invalidate } from '@react-three/fiber'\n\nimport { setAllCulled } from './utils/helpers'\nimport { useCanvasStore } from './store'\n\nconst viewportSize = new Vector2()\n\n// Flag that we need global rendering (full screen)\nexport const requestRender = (layers = [0]) => {\n useCanvasStore.getState().globalRenderQueue = useCanvasStore.getState().globalRenderQueue || [0]\n useCanvasStore.getState().globalRenderQueue = [...(useCanvasStore.getState().globalRenderQueue || []), ...layers]\n}\n\nexport const renderScissor = ({\n gl,\n scene,\n camera,\n left,\n top,\n width,\n height,\n layer = 0,\n autoClear = false,\n clearDepth = false,\n}: any) => {\n if (!scene || !camera) return\n gl.autoClear = autoClear\n gl.setScissor(left, top, width, height)\n gl.setScissorTest(true)\n camera.layers.set(layer)\n clearDepth && gl.clearDepth()\n gl.render(scene, camera)\n gl.setScissorTest(false)\n}\n\nexport const renderViewport = ({\n gl,\n scene,\n camera,\n left,\n top,\n width,\n height,\n layer = 0,\n scissor = true,\n autoClear = false,\n clearDepth = false,\n}: any) => {\n if (!scene || !camera) return\n gl.getSize(viewportSize)\n gl.autoClear = autoClear\n gl.setViewport(left, top, width, height)\n gl.setScissor(left, top, width, height)\n gl.setScissorTest(scissor)\n camera.layers.set(layer)\n clearDepth && gl.clearDepth()\n gl.render(scene, camera)\n gl.setScissorTest(false)\n gl.setViewport(0, 0, viewportSize.x, viewportSize.y)\n}\n\nexport const preloadScene = (\n { scene, camera, layer = 0 }: { scene?: Scene; camera?: Camera; layer?: number },\n callback?: () => void\n) => {\n config.preloadQueue.push((gl: WebGLRenderer, globalScene: Scene, globalCamera: Camera) => {\n gl.setScissorTest(false)\n setAllCulled(scene || globalScene, false)\n ;(camera || globalCamera).layers.set(layer)\n gl.render(scene || globalScene, camera || globalCamera)\n setAllCulled(scene || globalScene, true)\n callback && callback()\n })\n // auto trigger a new frame for the preload\n invalidate()\n}\n","import React, { ReactNode, startTransition } from 'react'\nimport { Canvas, Props } from '@react-three/fiber'\nimport { ResizeObserver as Polyfill } from '@juggle/resize-observer'\nimport { parse } from 'query-string'\n\nimport { useLayoutEffect } from '../hooks/useIsomorphicLayoutEffect'\nimport { useCanvasStore } from '../store'\nimport { ResizeManager } from './ResizeManager'\nimport { PerspectiveCamera } from './PerspectiveCamera'\nimport { OrthographicCamera } from './OrthographicCamera'\n\nimport { GlobalChildren } from './GlobalChildren'\nimport { GlobalRenderer } from './GlobalRenderer'\nimport { CanvasErrorBoundary } from './CanvasErrorBoundary'\n\nimport { config } from '../config'\nimport { version } from '../../package.json'\n\nlet polyfill: new (callback: ResizeObserverCallback) => ResizeObserver\nif (typeof window !== 'undefined') {\n polyfill = window.ResizeObserver || Polyfill\n}\n\ninterface IGlobalCanvas extends Omit<Props, 'children'> {\n children?: ReactNode | ((globalChildren: ReactNode) => ReactNode)\n as?: any\n orthographic?: boolean\n onError?: (props: any) => void\n camera?: any\n // state\n debug?: boolean\n scaleMultiplier?: number\n globalRender?: boolean\n globalPriority?: number\n globalClearDepth?: boolean\n}\n\nconst GlobalCanvasImpl = ({\n children,\n as = Canvas,\n gl,\n style,\n orthographic,\n camera,\n debug,\n scaleMultiplier = config.DEFAULT_SCALE_MULTIPLIER,\n globalRender = true,\n globalPriority = config.PRIORITY_GLOBAL,\n globalClearDepth = false,\n ...props\n}: Omit<IGlobalCanvas, 'onError'>) => {\n const useGlobalRenderer = useCanvasStore((state) => state.globalRender)\n\n // enable debug mode\n useLayoutEffect(() => {\n if (typeof window !== 'undefined') {\n // @ts-ignore\n window.__r3f_scroll_rig = version\n }\n\n // Querystring overridess\n const qs = parse(window.location.search)\n\n // show debug statements\n if (debug || typeof qs.debug !== 'undefined') {\n useCanvasStore.setState({ debug: true })\n console.info('@14islands/r3f-scroll-rig@' + version)\n }\n }, [debug])\n\n // update state\n useLayoutEffect(() => {\n // update as transition so we don't interrupt active suspenses\n startTransition(() => {\n useCanvasStore.setState({\n scaleMultiplier,\n globalRender,\n globalPriority,\n globalClearDepth,\n })\n })\n }, [scaleMultiplier, globalPriority, globalRender, globalClearDepth])\n\n const As = as\n\n return (\n <As\n id=\"ScrollRig-canvas\"\n // use our own default camera\n camera={{\n manual: true,\n }}\n // Some sane defaults\n gl={{\n // https://blog.tojicode.com/2013/12/failifmajorperformancecaveat-with-great.html\n failIfMajorPerformanceCaveat: true, // skip webgl if slow device\n ...gl,\n }}\n // polyfill old iOS safari\n resize={{ scroll: false, debounce: 0, polyfill }}\n // default styles\n style={{\n position: 'fixed',\n top: 0,\n left: 0,\n right: 0,\n height: '100vh', // use 100vh to avoid resize on iOS when url bar goes away\n ...style,\n }}\n // allow to override anything of the above\n {...props}\n >\n {/* @ts-ignore */}\n {!orthographic && <PerspectiveCamera manual makeDefault {...camera} />}\n {/* @ts-ignore */}\n {orthographic && <OrthographicCamera manual makeDefault {...camera} />}\n\n {useGlobalRenderer && <GlobalRenderer />}\n\n {typeof children === 'function' ? children(<GlobalChildren />) : <GlobalChildren>{children}</GlobalChildren>}\n\n <ResizeManager />\n </As>\n )\n}\n\nexport const GlobalCanvas = ({ children, onError, ...props }: IGlobalCanvas) => {\n useLayoutEffect(() => {\n document.documentElement.classList.add('js-has-global-canvas')\n useCanvasStore.setState({ isCanvasAvailable: true })\n }, [])\n\n return (\n // @ts-ignore\n <CanvasErrorBoundary\n onError={(err: any) => {\n onError && onError(err)\n useCanvasStore.setState({ isCanvasAvailable: false }) /* WebGL failed to init */\n document.documentElement.classList.remove('js-has-global-canvas')\n document.documentElement.classList.add('js-global-canvas-error')\n }}\n >\n <GlobalCanvasImpl {...props}>{children}</GlobalCanvasImpl>\n <noscript>\n <style>\n {`\n .ScrollRig-visibilityHidden,\n .ScrollRig-transparentColor {\n visibility: unset;\n color: unset;\n }\n `}\n </style>\n </noscript>\n </CanvasErrorBoundary>\n )\n}\n","import { useEffect } from 'react'\n\nimport { useCanvasStore } from '../store'\nimport { preloadScene, requestRender, renderScissor, renderViewport } from '../renderer-api'\n\nexport interface ScrollRigState {\n debug: boolean\n isCanvasAvailable: boolean\n hasSmoothScrollbar: boolean\n scaleMultiplier: number\n preloadScene: typeof preloadScene\n requestRender: typeof requestRender\n renderScissor: typeof renderScissor\n renderViewport: typeof renderViewport\n reflow: () => void\n}\n\n/**\n * Public interface for ScrollRig\n */\nexport const useScrollRig = () => {\n const isCanvasAvailable = useCanvasStore((state) => state.isCanvasAvailable)\n const hasSmoothScrollbar = useCanvasStore((state) => state.hasSmoothScrollbar)\n const requestReflow = useCanvasStore((state) => state.requestReflow)\n const pageReflow = useCanvasStore((state) => state.pageReflow)\n const debug = useCanvasStore((state) => state.debug)\n const scaleMultiplier = useCanvasStore((state) => state.scaleMultiplier)\n\n useEffect(() => {\n if (debug) {\n // @ts-ignore\n window._scrollRig = window._scrollRig || {}\n // @ts-ignore\n window._scrollRig.reflow = requestReflow\n }\n }, [])\n\n return {\n // boolean state\n debug,\n isCanvasAvailable,\n hasSmoothScrollbar,\n // scale\n scaleMultiplier,\n // render API\n preloadScene,\n requestRender,\n renderScissor,\n renderViewport,\n // recalc all tracker positions\n reflow: requestReflow,\n reflowCompleted: pageReflow,\n } as ScrollRigState\n}\n","import React, { Fragment, useEffect, ReactNode, cloneElement } from 'react'\nimport { invalidate, useThree } from '@react-three/fiber'\n\nimport { useCanvasStore } from '../store'\nimport { useScrollRig } from '../hooks/useScrollRig'\n\n/**\n * Renders global children from useCanvas hook\n */\nexport const GlobalChildren = ({ children }: { children?: ReactNode }) => {\n const gl = useThree((s) => s.gl)\n const canvasChildren = useCanvasStore((state) => state.canvasChildren)\n const scrollRig = useScrollRig()\n\n useEffect(() => {\n // render empty canvas automatically if all children were removed\n if (!Object.keys(canvasChildren).length) {\n scrollRig.debug && console.log('GlobalRenderer', 'auto render empty canvas')\n // clear leftover viewports etc from unmounted components\n gl.clear()\n // re-render global scene in case frameloop=\"demand\" to avoid empty canvas\n scrollRig.requestRender()\n invalidate()\n }\n }, [canvasChildren])\n\n scrollRig.debug && console.log('GlobalChildren', Object.keys(canvasChildren).length)\n return (\n <>\n {children}\n {Object.keys(canvasChildren).map((key) => {\n const { mesh, props } = canvasChildren[key]\n\n if (typeof mesh === 'function') {\n return <Fragment key={key}>{mesh({ key, ...scrollRig, ...props })}</Fragment>\n }\n\n return cloneElement(mesh, {\n key,\n ...props,\n })\n })}\n </>\n )\n}\n","import { useThree, useFrame, invalidate } from '@react-three/fiber'\n\nimport { useLayoutEffect } from '../hooks/useIsomorphicLayoutEffect'\nimport { config } from '../config'\nimport { useCanvasStore } from '../store'\nimport { useScrollRig } from '../hooks/useScrollRig'\n\n/**\n * Global render loop to avoid double renders on the same frame\n */\nexport const GlobalRenderer = () => {\n const gl = useThree((s) => s.gl)\n const frameloop = useThree((s) => s.frameloop)\n const globalClearDepth = useCanvasStore((state) => state.globalClearDepth)\n const globalPriority = useCanvasStore((state) => state.globalPriority)\n const scrollRig = useScrollRig()\n\n // https://threejs.org/docs/#api/en/renderers/WebGLRenderer.debug\n useLayoutEffect(() => {\n gl.debug.checkShaderErrors = scrollRig.debug\n }, [scrollRig.debug])\n\n // PRELOAD RENDER LOOP\n useFrame(({ camera, scene }) => {\n if (!config.preloadQueue.length) return\n // Render preload frames first and clear directly\n config.preloadQueue.forEach((render) => render(gl, scene, camera))\n // cleanup\n gl.clear()\n config.preloadQueue = []\n // trigger new frame to get correct visual state after all preloads\n scrollRig.debug && console.log('GlobalRenderer', 'preload complete. trigger global render')\n scrollRig.requestRender()\n invalidate()\n }, config.PRIORITY_PRELOAD)\n\n // GLOBAL RENDER LOOP\n useFrame(({ camera, scene }) => {\n const globalRenderQueue = useCanvasStore.getState().globalRenderQueue\n\n // Render if requested or if always on\n if (frameloop === 'always' || globalRenderQueue) {\n // render default layer, scene, camera\n camera.layers.disableAll()\n if (globalRenderQueue) {\n // @ts-ignore\n globalRenderQueue.forEach((layer) => {\n camera.layers.enable(layer)\n })\n } else {\n camera.layers.enable(0)\n }\n\n // render as HUD over any other renders by default\n globalClearDepth && gl.clearDepth()\n gl.render(scene, camera)\n }\n // cleanup for next frame\n useCanvasStore.getState().clearGlobalRenderQueue()\n }, globalPriority) // Take over rendering\n\n return null\n}\n","// @ts-nocheck\nimport { Component, ReactNode } from 'react'\n\ninterface ICanvasErrorBoundary {\n children: ReactNode\n onError: () => void\n}\n\nexport class CanvasErrorBoundary extends Component<{}, ICanvasErrorBoundary> {\n constructor(props) {\n super(props)\n this.state = { error: false }\n this.props = props\n }\n\n static getDerivedStateFromError(error) {\n // Update state so the next render will show the fallback UI.\n return { error }\n }\n\n // componentDidCatch(error, errorInfo) {\n // // You can also log the error to an error reporting service\n // // logErrorToMyService(error, errorInfo)\n // }\n\n render() {\n if (this.state.error) {\n this.props.onError && this.props.onError(this.state.error)\n return null\n }\n\n return this.props.children\n }\n}\n","import React from 'react'\nimport { Color } from 'three'\n\nexport const DebugMesh = ({ scale }: { scale: [x: number, y: number, z: number] }) => (\n <mesh scale={scale}>\n <planeGeometry />\n <shaderMaterial\n args={[\n {\n uniforms: {\n color: { value: new Color('hotpink') },\n },\n vertexShader: `\n void main() {\n gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);\n }\n `,\n fragmentShader: `\n uniform vec3 color;\n uniform float opacity;\n void main() {\n gl_FragColor.rgba = vec4(color, .5);\n }\n `,\n },\n ]}\n transparent\n />\n </mesh>\n)\n","import { useState, useEffect } from 'react'\nimport { ResizeObserver as Polyfill } from '@juggle/resize-observer'\nimport pkg from 'debounce'\n\nconst isBrowser = typeof window !== 'undefined'\nexport interface WindowSize {\n width: number\n height: number\n}\n\ntype ConfigProps = {\n debounce?: number\n}\n\n/*\n * Triggers a resize only if the Canvas DOM element changed dimensions - not on window resize event\n *\n * This is to avoid costly re-renders when the URL bar is scrolled away on mobile\n *\n * Based on: https://usehooks.com/useWindowSize/\n */\n\nexport function useWindowSize({ debounce = 0 }: ConfigProps = {}) {\n // Initialize state with undefined width/height so server and client renders match\n // Learn more here: https://joshwcomeau.com/react/the-perils-of-rehydration/\n const [windowSize, setWindowSize] = useState<WindowSize>({\n width: isBrowser ? window.innerWidth : Infinity,\n height: isBrowser ? window.innerHeight : Infinity,\n })\n\n useEffect(() => {\n // check if we can find a canvas - if so, base size on canvas instead of window\n // since 100vh !== window.innerHeight on mobile\n const canvasEl = document.getElementById('ScrollRig-canvas')\n\n // Handler to call on window resize\n function handleResize() {\n const width = canvasEl ? canvasEl.clientWidth : window.innerWidth\n const height = canvasEl ? canvasEl.clientHeight : window.innerHeight\n\n if (width !== windowSize.width || height !== windowSize.height) {\n // Set window width/height to state\n setWindowSize({\n width,\n height,\n })\n }\n }\n\n const debouncedResize = pkg.debounce(handleResize, debounce)\n\n // Add event listener\n const ResizeObserver = window.ResizeObserver || Polyfill\n let observer: ResizeObserver\n if (canvasEl) {\n observer = new ResizeObserver(debouncedResize)\n observer.observe(canvasEl)\n } else {\n window.addEventListener('resize', debouncedResize)\n }\n // Call handler right away so state gets updated with initial window size\n handleResize()\n // Remove event listener on cleanup\n return () => {\n window.removeEventListener('resize', debouncedResize)\n observer?.disconnect()\n }\n }, [windowSize, setWindowSize])\n\n return windowSize\n}\n","// Linear mapping from range <a1, a2> to range <b1, b2>\nexport function mapLinear(x: number, a1: number, a2: number, b1: number, b2: number) {\n return b1 + ((x - a1) * (b2 - b1)) / (a2 - a1)\n}\n","import { useCanvasStore } from '../store'\n\n/**\n * Public interface for ScrollRig\n */\nexport const useScrollbar = () => {\n const enabled = useCanvasStore((state) => state.hasSmoothScrollbar)\n const scroll = useCanvasStore((state) => state.scroll)\n const scrollTo = useCanvasStore((state) => state.scrollTo)\n const onScroll = useCanvasStore((state) => state.onScroll)\n const __lenis = useCanvasStore((state) => state.__lenis)\n\n return {\n enabled,\n scroll,\n scrollTo,\n onScroll,\n __lenis,\n }\n}\n","// https://www.typescriptlang.org/docs/handbook/modules.html#ambient-modules\n/// <reference path=\"../types/global.ts\" />\n\nimport { useRef, useCallback, useEffect, useMemo, useState, MutableRefObject } from 'react'\nimport { useInView } from 'react-intersection-observer'\nimport { useWindowSize } from './useWindowSize'\nimport vecn from 'vecn'\n\nimport { useLayoutEffect } from '../hooks/useIsomorphicLayoutEffect'\nimport { mapLinear } from '../utils/math'\nimport { useCanvasStore } from '../store'\nimport { useScrollbar } from '../scrollbar/useScrollbar'\nimport type { ScrollData } from '../scrollbar/SmoothScrollbarTypes'\n\nimport type { Rect, Bounds, TrackerOptions, Tracker, ScrollState, UpdateCallback } from './useTrackerTypes'\n\nfunction updateBounds(bounds: Bounds, rect: Rect, scroll: ScrollData, size: any) {\n bounds.top = rect.top - (scroll.y || 0)\n bounds.bottom = rect.bottom - (scroll.y || 0)\n bounds.left = rect.left - (scroll.x || 0)\n bounds.right = rect.right - (scroll.x || 0)\n bounds.width = rect.width\n bounds.height = rect.height\n // move coordinate system so 0,0 is at center of screen\n bounds.x = bounds.left + rect.width * 0.5 - size.width * 0.5\n bounds.y = bounds.top + rect.height * 0.5 - size.height * 0.5\n bounds.positiveYUpBottom = size.height - bounds.bottom // inverse Y\n}\n\nfunction updatePosition(position: vec3, bounds: Bounds, scaleMultiplier: number) {\n position.x = bounds.x * scaleMultiplier\n position.y = -1 * bounds.y * scaleMultiplier\n}\n\n/**\n * Returns the current Scene position of the DOM element\n * based on initial getBoundingClientRect and scroll delta from start\n */\nfunction useTracker(track: MutableRefObject<HTMLElement>, options?: TrackerOptions): Tracker {\n const size = useWindowSize()\n const { scroll, onScroll } = useScrollbar()\n const scaleMultiplier = useCanvasStore((state) => state.scaleMultiplier)\n const pageReflow = useCanvasStore((state) => state.pageReflow)\n const debug = useCanvasStore((state) => state.debug)\n\n // extend defaults with optional options\n const { rootMargin, threshold, autoUpdate, wrapper } = useMemo(() => {\n const target = { rootMargin: '0%', threshold: 0, autoUpdate: true } as TrackerOptions\n const opts = options || {}\n Object.keys(opts).map((key: string, index) => {\n if (opts[key] !== undefined) target[key] = opts[key]\n })\n return target\n }, [options])\n\n // check if element is in viewport\n const { ref, inView: inViewport } = useInView({ rootMargin, threshold })\n\n // bind useInView ref to current tracking element\n useLayoutEffect(() => {\n ref(track.current)\n }, [track, track?.current])\n\n // Using state so it's reactive\n const [scale, setScale] = useState<vec3>(vecn.vec3(0, 0, 0))\n\n // Using ref because\n const scrollState: ScrollState = useRef({\n inViewport: false,\n progress: -1,\n visibility: -1,\n viewport: -1,\n }).current\n\n // DOM rect (initial position in pixels offset by scroll value on page load)\n // Using ref so we can calculate bounds & position without a re-render\n const rect = useRef({\n top: 0,\n bottom: 0,\n left: 0,\n right: 0,\n width: 0,\n height: 0,\n }).current\n\n // expose internal ref as a reactive state as well\n const [reactiveRect, setReactiveRect] = useState<Rect>(rect)\n\n // bounding rect in pixels - updated by scroll\n const bounds = useRef({\n top: 0,\n bottom: 0,\n left: 0,\n right: 0,\n width: 0,\n height: 0,\n x: 0,\n y: 0,\n positiveYUpBottom: 0,\n }).current\n\n // position in viewport units - updated by scroll\n const position = useRef(vecn.vec3(0, 0, 0)).current\n\n // Calculate bounding Rect as soon as it's available\n useLayoutEffect(() => {\n const _rect = track.current?.getBoundingClientRect()\n if (!_rect) return\n const initialY = wrapper ? (wrapper as HTMLElement).scrollTop : window.scrollY\n const initialX = wrapper ? (wrapper as HTMLElement).scrollLeft : window.scrollX\n rect.top = _rect.top + initialY\n rect.bottom = _rect.bottom + initialY\n rect.left = _rect.left + initialX\n rect.right = _rect.right + initialX\n rect.width = _rect.width\n rect.height = _rect.height\n setReactiveRect({ ...rect })\n setScale(vecn.vec3(rect?.width * scaleMultiplier, rect?.height * scaleMultiplier, 1))\n debug &&\n console.log(\n 'useTracker.getBoundingClientRect:',\n rect,\n 'intialScroll:',\n { initialY, initialX },\n 'size:',\n size,\n 'pageReflow:',\n pageReflow\n )\n }, [track, size, pageReflow, scaleMultiplier, debug])\n\n const update = useCallback(\n ({ onlyUpdateInViewport = false, scroll: overrideScroll }: UpdateCallback = {}) => {\n if (!track.current || (onlyUpdateInViewport && !scrollState.inViewport)) {\n return\n }\n\n const _scroll = overrideScroll || scroll\n\n updateBounds(bounds, rect, _scroll, size)\n updatePosition(position, bounds, scaleMultiplier)\n\n // scrollState setup based on scroll direction\n const isHorizontal = _scroll.scrollDirection === 'horizontal'\n const sizeProp = isHorizontal ? 'width' : 'height'\n const startProp = isHorizontal ? 'left' : 'top'\n\n // calculate progress of passing through viewport (0 = just entered, 1 = just exited)\n const pxInside = size[sizeProp] - bounds[startProp]\n scrollState.progress = mapLinear(pxInside, 0, size[sizeProp] + bounds[sizeProp], 0, 1) // percent of total visible distance\n scrollState.visibility = mapLinear(pxInside, 0, bounds[sizeProp], 0, 1) // percent of item height in view\n scrollState.viewport = mapLinear(pxInside, 0, size[sizeProp], 0, 1) // percent of window height scrolled since visible\n },\n [track, size, scaleMultiplier, scroll]\n )\n\n // update scrollState in viewport\n useLayoutEffect(() => {\n scrollState.inViewport = inViewport\n // update once more in case it went out of view\n update({ onlyUpdateInViewport: false })\n debug && console.log('useTracker.inViewport:', inViewport, 'update()')\n }, [inViewport])\n\n // re-run if the callback updated\n useLayoutEffect(() => {\n update({ onlyUpdateInViewport: false })\n debug && console.log('useTracker.update on resize/reflow')\n }, [update, pageReflow])\n\n // auto-update on scroll\n useEffect(() => {\n if (autoUpdate) return onScroll((_scroll) => update({ onlyUpdateInViewport: true }))\n }, [autoUpdate, update, onScroll])\n\n return {\n // Reactive props\n scale, // reactive scene scale - includes z-axis so it can be spread onto mesh directly\n inViewport, // reactive prop for when inside viewport\n // Non-reactive props (only updates on window resize)\n // Child values are updated on scroll\n rect: reactiveRect, // Dom rect\n bounds, // scrolled bounding rect in pixels\n position, // scrolled element position in viewport units\n scrollState, // scroll progress stats - not reactive\n // Utilities\n update, // optional - manually update tracker\n }\n}\n\nexport { useTracker }\n","import React, { useEffect, useState, useRef, MutableRefObject, ReactNode } from 'react'\nimport { Scene, Group } from 'three'\nimport { useFrame, createPortal, useThree } from '@react-three/fiber'\n\nimport { useLayoutEffect } from '../hooks/useIsomorphicLayoutEffect'\nimport { config } from '../config'\nimport { useCanvasStore } from '../store'\nimport { useScrollRig } from '../hooks/useScrollRig'\nimport { DebugMesh } from './DebugMesh'\nimport { useTracker } from '../hooks/useTracker'\nimport type { ScrollState } from '../hooks/useTrackerTypes'\n\nexport interface ScrollSceneChildProps {\n track: MutableRefObject<HTMLElement>\n margin: number\n priority: number\n scale: vec3\n scrollState: ScrollState\n inViewport: boolean\n scene: Scene\n}\n\ninterface IScrollScene {\n track: MutableRefObject<HTMLElement>\n children: (state: ScrollSceneChildProps) => ReactNode\n margin?: number\n inViewportMargin?: string\n inViewportThreshold?: number\n visible?: boolean\n hideOffscreen?: boolean\n scissor?: boolean\n debug?: boolean\n as?: string\n priority?: number\n scene?: Scene\n}\n\n/**\n * Generic THREE.js Scene that tracks the dimensions and position of a DOM element while scrolling\n * Scene is positioned and scaled exactly above DOM element\n *\n * @author david@14islands.com\n */\nfunction ScrollScene({\n track,\n children,\n margin = 0, // Margin outside scissor to avoid clipping vertex displacement (px)\n inViewportMargin,\n inViewportThreshold,\n visible = true,\n hideOffscreen = true,\n scissor = false,\n debug = false,\n as = 'scene',\n priority = config.PRIORITY_SCISSORS,\n scene,\n ...props\n}: IScrollScene) {\n const globalScene = useThree((s) => s.scene)\n const contentRef = useRef<Group>()\n const [portalScene] = useState<Scene | null>(scene || (scissor ? new Scene() : null))\n const { requestRender, renderScissor } = useScrollRig()\n const globalRender = useCanvasStore((state) => state.globalRender)\n\n const { bounds, scale, position, scrollState, inViewport } = useTracker(track, {\n rootMargin: inViewportMargin,\n threshold: inViewportThreshold,\n })\n\n // Hide content when outside of viewport if `hideOffscreen` or set to `visible` prop\n useLayoutEffect(() => {\n if (!contentRef.current) return\n contentRef.current.visible = hideOffscreen ? inViewport && visible : visible\n }, [inViewport, hideOffscreen, visible])\n\n // move content into place visibility or scale changes\n useEffect(() => {\n if (!contentRef.current) return\n contentRef.current.position.y = position.y\n contentRef.current.position.x = position.x\n }, [scale, inViewport]) // scale updates on resize\n\n // RENDER FRAME\n useFrame(\n ({ gl, camera }) => {\n if (!contentRef.current) return\n\n if (contentRef.current.visible) {\n // move content\n contentRef.current.position.y = position.y\n contentRef.current.position.x = position.x\n\n if (scissor) {\n renderScissor({\n gl,\n portalScene,\n camera,\n left: bounds.left - margin,\n top: bounds.positiveYUpBottom - margin,\n width: bounds.width + margin * 2,\n height: bounds.height + margin * 2,\n })\n } else {\n requestRender()\n }\n }\n },\n globalRender ? priority : undefined\n )\n\n const InlineElement: any = as\n const content = (\n <InlineElement ref={contentRef}>\n {(!children || debug) && scale && <DebugMesh scale={scale} />}\n {children &&\n scale &&\n children({\n // inherited props\n track,\n margin,\n scene: portalScene || globalScene,\n // new props from tracker\n scale,\n scrollState,\n inViewport,\n // useFrame render priority (in case children need to run after)\n priority: priority,\n // tunnel the rest\n ...props,\n })}\n </InlineElement>\n )\n\n // render in portal if requested\n return portalScene ? createPortal(content, portalScene) : content\n}\n\nexport { ScrollScene }\n","import React, { useEffect, useState, useCallback, MutableRefObject, ReactNode } from 'react'\nimport { Scene } from 'three'\nimport { useFrame, createPortal, useThree } from '@react-three/fiber'\n\nimport { useLayoutEffect } from '../hooks/useIsomorphicLayoutEffect'\nimport { config } from '../config'\nimport { useScrollRig } from '../hooks/useScrollRig'\nimport { DebugMesh } from './DebugMesh'\nimport { useTracker } from '../hooks/useTracker'\nimport type { Tracker } from '../hooks/useTrackerTypes'\nimport { PerspectiveCamera } from './PerspectiveCamera'\nimport { OrthographicCamera } from './OrthographicCamera'\nimport type { ScrollState } from '../hooks/useTrackerTypes'\n\ninterface IViewportScrollScene {\n track: MutableRefObject<HTMLElement>\n children: (state: ViewportScrollSceneChildProps) => ReactNode\n margin?: number\n inViewportMargin?: string\n inViewportThreshold?: number\n visible?: boolean\n hideOffscreen?: boolean\n debug?: boolean\n orthographic?: boolean\n priority?: number\n hud?: boolean // clear depth to render on top\n camera?: any\n}\n\nexport interface ViewportScrollSceneChildProps {\n track: MutableRefObject<HTMLElement>\n margin: number\n priority: number\n scale: vec3\n scrollState: ScrollState\n inViewport: boolean\n}\n\n/**\n * Generic THREE.js Scene that tracks the dimensions and position of a DOM element while scrolling\n * Scene is rendered into a GL viewport matching the DOM position for better performance\n *\n * Adapted to @react-three/fiber from https://threejsfundamentals.org/threejs/lessons/threejs-multiple-scenes.html\n * @author david@14islands.com\n */\nconst Viewport = ({\n track,\n children,\n margin = 0, // Margin outside viewport to avoid clipping vertex displacement (px)\n visible = true,\n hideOffscreen = true,\n debug = false,\n orthographic = false,\n priority = config.PRIORITY_VIEWPORTS,\n inViewport,\n bounds,\n scale,\n scrollState,\n camera,\n hud,\n position, // pick out in order to not pass down to child (should be safe to spread props on child)\n rect, // pick out in order to not pass down to child (should be safe to spread props on child)\n ...props\n}: IViewportScrollScene & Tracker) => {\n const scene = useThree((s) => s.scene)\n const get = useThree((state) => state.get)\n const setEvents = useThree((state) => state.setEvents)\n\n const { renderViewport } = useScrollRig()\n\n // Hide scene when outside of viewport if `hideOffscreen` or set to `visible` prop\n useLayoutEffect(() => {\n scene.visible = hideOffscreen ? inViewport && visible : visible\n }, [inViewport, hideOffscreen, visible])\n\n // From: https://github.com/pmndrs/drei/blob/d22fe0f58fd596c7bfb60a7a543cf6c80da87624/src/web/View.tsx#L80\n useEffect(() => {\n // Connect the event layer to the tracking element\n const old = get().events.connected\n setEvents({ connected: track.current })\n return () => setEvents({ connected: old })\n }, [])\n\n // RENDER FRAME\n useFrame(({ gl, scene, camera }) => {\n // Render scene to viewport using local camera and limit updates using scissor test\n if (scene.visible) {\n renderViewport({\n gl,\n scene,\n camera,\n left: bounds.left - margin,\n top: bounds.positiveYUpBottom - margin,\n width: bounds.width + margin * 2,\n height: bounds.height + margin * 2,\n clearDepth: !!hud,\n })\n }\n }, priority)\n\n return (\n <>\n {!orthographic && <PerspectiveCamera manual margin={margin} makeDefault {...camera} />}\n {orthographic && <OrthographicCamera manual margin={margin} makeDefault {...camera} />}\n {(!children || debug) && scale && <DebugMesh scale={scale} />}\n {children &&\n // scene &&\n scale &&\n children({\n // inherited props\n track,\n margin,\n // tracker props\n scale,\n scrollState,\n inViewport,\n // useFrame render priority (in case children need to run after)\n priority,\n // tunnel the rest\n ...props,\n })}\n </>\n )\n}\n\nfunction ViewportScrollScene({\n track,\n margin = 0, // Margin outside viewport to avoid clipping vertex displacement (px)\n inViewportMargin,\n inViewportThreshold,\n priority,\n ...props\n}: IViewportScrollScene) {\n const [scene] = useState(() => new Scene())\n\n const { bounds, ...trackerProps } = useTracker(track, {\n rootMargin: inViewportMargin,\n threshold: inViewportThreshold,\n })\n\n // From: https://github.com/pmndrs/drei/blob/d22fe0f58fd596c7bfb60a7a543cf6c80da87624/src/web/View.tsx#L80\n const compute = useCallback(\n (event: any, state: any) => {\n // limit events to DOM element bounds\n if (track.current && event.target === track.current) {\n const { width, height, left, top } = bounds\n const mWidth = width + margin * 2\n const mHeight = height + margin * 2\n const x = event.clientX - left + margin\n const y = event.clientY - top + margin\n state.pointer.set((x / mWidth) * 2 - 1, -(y / mHeight) * 2 + 1)\n state.raycaster.setFromCamera(state.pointer, state.camera)\n }\n },\n [bounds]\n )\n\n return (\n bounds &&\n createPortal(\n <Viewport track={track} bounds={bounds} priority={priority} margin={margin} {...props} {...trackerProps} />,\n scene,\n // @ts-ignore\n { events: { compute, priority }, size: { width: bounds.width, height: bounds.height } }\n )\n )\n}\n\nexport { ViewportScrollScene }\n","import { useEffect, useMemo, useCallback, ReactNode } from 'react'\nimport { MathUtils } from 'three'\n\nimport { useCanvasStore } from '../store'\nimport { useLayoutEffect } from '../hooks/useIsomorphicLayoutEffect'\n\nimport { ScrollRigState } from '../hooks/useScrollRig'\n/**\n * Adds THREE.js object to the GlobalCanvas while the component is mounted\n * @param {object} object THREE.js object3d\n */\nfunction useCanvas(\n object: ReactNode | ((props: ScrollRigState) => ReactNode),\n props: any = {},\n { key, dispose = true }: { key?: string; dispose?: boolean } = {}\n) {\n const updateCanvas = useCanvasStore((state) => state.updateCanvas)\n const renderToCanvas = useCanvasStore((state) => state.renderToCanvas)\n const removeFromCanvas = useCanvasStore((state) => state.removeFromCanvas)\n\n // auto generate uuid v4 key\n const uniqueKey = useMemo(() => key || MathUtils.generateUUID(), [])\n\n // render to canvas if not mounted already\n useLayoutEffect(() => {\n renderToCanvas(uniqueKey, object, { ...props, inactive: false })\n }, [uniqueKey])\n\n // remove from canvas if no usage (after render so new users have time to register)\n useEffect(() => {\n return () => {\n removeFromCanvas(uniqueKey, dispose)\n }\n }, [uniqueKey])\n\n // return function that can set new props on the canvas component\n const set = useCallback(\n (props: any) => {\n updateCanvas(uniqueKey, props)\n },\n [updateCanvas, uniqueKey]\n )\n\n // auto update props when they change\n useEffect(() => {\n set(props)\n }, [...Object.values(props)])\n\n return set\n}\n\nexport { useCanvas }\n","import { forwardRef, ReactNode } from 'react'\nimport { useCanvas } from '../hooks/useCanvas'\n\nimport { ScrollRigState } from '../hooks/useScrollRig'\n\ninterface IUseCanvas {\n children: ReactNode | ((props: ScrollRigState) => ReactNode)\n id?: string // persistent layout id\n dispose?: boolean // dispose on unmount\n [key: string]: any // Any props to reactively tunnel to the child\n}\n\nconst UseCanvas = forwardRef(({ children, id, dispose = true, ...props }: IUseCanvas, ref) => {\n if (!children) return null\n // auto update canvas with all props\n useCanvas(children, { ...props, id, ref }, { key: id, dispose })\n return null\n})\n\nexport { UseCanvas }\n","import { useEffect, RefObject, useMemo, useState } from 'react'\nimport { useThree, useLoader } from '@react-three/fiber'\nimport { Texture, CanvasTexture, ImageBitmapLoader, TextureLoader, DefaultLoadingManager } from 'three'\nimport { suspend } from 'suspend-react'\nimport supportsWebP from 'supports-webp'\nimport equal from 'fast-deep-equal'\n\nimport { useWindowSize } from './useWindowSize'\nimport { useCanvasStore } from '../store'\n\n/**\n * Create Threejs Texture from DOM image tag\n *\n * - Supports <picture> and `srcset` - uses `currentSrc` to get the responsive image source\n *\n * - Supports lazy-loading image - suspends until first load event. Warning: the GPU upload can cause jank\n *\n * - Relies on browser cache to avoid loading image twice. We let the <img> tag load the image and suspend until it's ready.\n *\n * - NOTE: You must add the `crossOrigin` attribute\n * <img src=\"\" alt=\"\" crossOrigin=\"anonymous\"/>\n */\n\nlet hasWebpSupport: boolean = false\n// this test is fast - \"should\" run before first image is requested\nsupportsWebP.then((supported) => {\n hasWebpSupport = supported\n})\n\nfunction useTextureLoader() {\n // Use an ImageBitmapLoader if imageBitmaps