Skip to content

Personal Website and a useful Three.js/R3F helper library

Notifications You must be signed in to change notification settings

theCocoonStudio/home

Repository files navigation

izzyerlich.com

This is the project for the yet-unreleased izzyerlich.com website.

There is an associated library included in this repo which contains exports to modules used in the website.

Structure

/web: contains the website code
/src: contains the library and its exports

Exports

Note: this library is still unreleased as it is in development.

configureShaderMaterial(materials) : undefined

Uses shaderMaterial to create one or more THREE.ShaderMaterial R3F objects and extends the native R3F catalogue to include it. Simplifies dynamic usage of extend, which it wraps.

params:

  1. materials (required): Object containing materialConfigs for one or more materials. Each config is keyed by the desired (uppercase) material name.

    materialConfig structure:

    {
    // THREE.ShaderMaterial.uniforms
    uniforms: Object,
    // THREE.ShaderMaterial.vertexShader
    vert: String,
    // THREE.ShaderMaterial.fragmentshader
    frag: String
    }

example usage:

const FiberComponent = () => {
  // create materials before use
  useLayoutEffect(() => {
    configureShaderMaterial({
      CustomShaderMaterial: {
        uniforms: {
          scale: { value: new Vector2() },
          time: { value: null },
        },
        vert: `GLSL shader code`,
        frag: `GLSL shader code`,
      },
      AnotherShaderMaterial: {
        uniforms: {
          color: { value: new Color() },
          time: { value: null },
        },
        vert: `GLSL shader code`,
        frag: `GLSL shader code`,
      },
    })
  }, [])

  // use as builtin R3F object; uniforms can now be updated declaratively
  return (
    <mesh>
      <planeGeometry />
      <customShaderMaterial scale={scaleVale} time={timeVal} />
    </mesh>
  )
}

useFluidTexture(options, priority, fboArgs) : THREE.Texture

Outputs a fluid simulation to a THREE.Texture and returns it. Can be used as a THREE.Material .map, .alphaMap, or anywhere a texture would normally be used.

Adapted from fluid-three.

params:

  1. options (optional): Object with simulation options; excluded options will use default values:

    const options = {
       iterations_poisson = 32,
       iterations_viscous = 32,
       mouse_force = 20,
       resolution = 0.5,
       cursor_size = 100,
       viscous = 30,
       isBounce = false,
       dt = 0.014,
       isViscous = false,
       BFECC = true,
       forceCallback
    }

    where forceCallback is a callback used to determine, at each frame, what (if any) external force to add to the simulation. It has following signature:

    (delta: Number, clock: THREE.Clock, pointer: THREE.Vector2, pointerDiff: THREE.Vector2) :
     {
       force: THREE.Vector2, // normalized, centric coords: [-1, 1]
       center: THREE.Vector2, // normalized, centric coords: [-1, 1]
     }`

    At each frame, the simulation runs the callback and adds a force to the fluid in a rectangular area centered at center. The aspect of the rectangle equals the FBO aspect and scaled by cursor_size. Note that cursor_size refers to the number of simulation cells, the total number of which is equal to resolution multiplied by the FBO width or height. The strength of the force at each fluid "cell" is determined by mouse_force and scaled by the returned force.

    forceCallback takes useFrame's delta, clock, and pointer as parameters, as well as pointerDiff, representing the pointer diff for the given frame.

  2. priority (optional): Number (integer) representing the render priority in the internally-used useFrame hook. Default is -1.

  3. fboArgs (optional): Array containing arguments to useFBO, used to defined the THREE.WebGLRenderTarget that contains the output texture. Default options are analogous to those of useFBO.

example usage:

const FiberComponent = () => {
  const texture = useFluidTexture()
  return (
    <mesh>
      <planeGeometry />
      <meshBasicMaterial map={texture} />
    </mesh>
  )
}

use2DBounds(object3DRef, Use2DBoundsOptions) : Use2DBoundsResults

Note: if you need to output your scene to a subset of the canvas, gl.scissor is likely more suitable (and performant, given that the resolution and buffer sizes will be smaller). This component from the drei team is a turnkey solution.

A convenient hook for aligning objects in a 3D scene to positions on the canvas or to a THREE.Box2 corresponding to an HTML element contained within the canvas:

  • use the returned data to position the element yourself or let the hook do the work for you
  • works imperatively under the hood so canvas/viewport/object changes are automatically reflected in the result
  • position, rotation, and scale changes are automaticaly dampened using maath/easing
  • built in easing is fully customizeable
  • full customization of results: the new position, rotation, and scale can be updated based on the calculated targets before being applied using optional callbacks
  • easily pause the hook to optimize performance if you know your app won't need it

params:

  1. object3DRef (required): React.Ref containing a THREE.Object3D to bind. Note that this is a ref so React won't waste compute resources on lifecycle updates unless needed.

  2. Use2DBoundsOptions (optional): Object with config options:

     {
       // React.ref to an HTML element contained fully within the Canvas. If trackingElement is true, the hook uses the element as the 2D bounds.
       trackingElementRef,
       // If false, the hook uses the Canvas as 2D bounds. If true, the hook uses the element as 2D bounds. Setting this to true assumes that the Canvas is set to fill the document exactly.
       trackingElement = false,
       // React.ref to a custom camera to use for calculations. The default camera is used if undefined.
       customCameraRef,
       // Number (integer) representing the render priority in the internally-used useFrame (https://r3f.docs.pmnd.rs/api/hooks#taking-over-the-render-loop) hook. Default is undefined.
       renderPriority,
       // A float between 0.0 and 1.0 representing a target x-position within the horizontal bounds. A value of 0.0 sets the target x-position at the left bound, while 1.0 at the right.
       left = 0.5,
       // A float between 0.0 and 1.0 representing a target y-position within the vertical bounds. A value of 0.0 sets the target y-position at the top bound, while 1.0 at the bottom.
       top = 0.5,
       // If true, calculation stops. The calculation is always carried out at least once when the component mounts and when the target bounds change (i.e., it is Reactive), even if pause is initially set to true.
       pause = true,
       // If true, the hook calculates and updates the bound object's position (and optionally, scale and rotation) each frame, and returns the calculated target results. If false, the target results are returned without any modification to the object.
       damp = true,
       // Args to pass to damp() functions. See https://github.com/pmndrs/maath/blob/main/README.md#easing for reference and defaults.
       damping: {
         smoothTime,
         delta = THREE.Clock.getDelta(),
         maxSpeed,
         easing,
         eps
       },
       /* Overridden by setting computeScale. If true, the hook calculates the object's new scale so that its new width is the width of the bounds less the left and right margin (if set), while the new height is the height of the bounds less the top and bottom margin (if set). z-values of the scale are not changed.
    
       If object3DRef.current.geometry.parameters does not contain .width and .height values, set geometrySize as the base, unscaled geometry dimensions. If neither is defined, a default of 1.0 is used for the base geometry dimensions. Geometries with width and height parameters include THREE.PlaneGeometry and THREE.BoxGeometry. In contrast, @drei/RoundedBox's geometry does not include these parameters. */
       scaleToFitWidth = true,
    
       /* A callback function to further customize the final target result. It has the following signature:
    
       computePosition(obj3d: THREE.Object3D,  intermediateResults: Use2DBoundsResults, camera: THREE.Camera) : THREE.Vector3,
    
       where obj3D is a direct reference to the bound object, intermediateResults contains the target calculations for the frame before your updates, and camera is the camera used by the hook. The callback should return a THREE.Vector3 for the final target position. If not configured, the new target position will be fully reflected by other configuration options.
    
       Note that this function is called each frame, so try to limit the computation as much as possible for performance or use pause = true when you can. */
       computePosition,
       /* A callback function to further customize the final target result. It has the following signature:
    
       computeScale(obj3d: THREE.Object3D,  intermediateResults: Use2DBoundsResults, camera: THREE.Camera) : THREE.Vector3,
    
       where obj3D is a direct reference to the bound object, intermediateResults contains the target calculations for the frame before your updates, and camera is the camera used by the hook. The callback should return a THREE.Vector3 for the final target scale. If not configured, the new target scale will be fully reflected by scaleToFitWidth. If scaleToFitWidth = false, computeScale is undefined, scale is not calculated.
    
       Setting this callback overrides scaleToFitWidth = true.
    
       Note that this function is called each frame, so try to limit the computation as much as possible for performance or use pause = true when you can. */
       computeScale,
       /* A callback function to further customize the final target result. It has the following signature:
    
       computeRotation(obj3d: THREE.Object3D,  intermediateResults: Use2DBoundsResults, camera: THREE.Camera) : THREE.Euler,
    
       where obj3D is a direct reference to the bound object, intermediateResults contains the target calculations for the frame before your updates, and camera is the camera used by the hook. The callback should return a THREE.Euler for the final target rotation. If not configured, the hook does not calculate rotation.
    
       Note that this function is called each frame, so try to limit the computation as much as possible for performance or use pause = true when you can. */
       computeRotation,
       /* A THREE.Vector4 containing values for the top, right, bottom, and left margin, respectively. Default is 0 for all values and values can be negative. The margin is applied internally only when setting the object's new scale (if scaleToFitWidth = true), so if you're not using scaleToFitWidth = true, it is your responsibility to account for margin application using the intermediateResults parameter in the computeScale callback (and in computePosition, if your use case requires it). Vector units should be float proportions relative to the horizontal/vertical bound lengths if using UNITS.WU or integers if using UNITS.PX. E.g., 0.1 WU for the top margin will set it at 0.1 * the height of the bounds, while 100 PX would set it to 100 / ppwu. If your object is using any textures, make sure to account for the change in aspect ratio when setting the texture/FBO dimensions. */
       margin,
       /* Either UNITS.PX or UNITS.WU (default). UNITS is a named, top-level export.  */
       marginUnits
     }

return value:

Use2DBoundsResults, an object with the following properties:

{
  // Pixels-per-world-unit: Conversion factor for both x and y dimensions to convert from pixels to world-units and vice-versa. The two values should, in practice, be idential. 1 wu = ppwu pixels
  ppwu: THREE.Vector2,
  // The min and max bounds of the camera's viewport at the object's distance along the viewing angle, expressed in three.js world units. See THREE.PerspectiveCamera.getViewBounds().
  viewBounds: {
    min: THREE.Vector2,
    max: THREE.Vector2
  },
  // If trackingElement = true, the min and max bounds of the HTML element set as trackingElementRef.current, at the object's distance along the viewing angle, expressed in three.js world units. If false, identical to viewBounds.
  bounds: {
    min: THREE.Vector2,
    max: THREE.Vector2,
  },
  // calculated target position, scale, and rotation. If scale or rotation aren't included in calculations, they are set as the object's current scale and rotation, respectively.
  targets: {
    position: THREE.Vector3,
    scale: THREE.Vector3,
    rotation: THREE.Euler,
  },
  //  The object's distance to the camera along the camera's viewing angle. This number will always be greater than or equal to zero.
  distance: Number,
  // The margin values passed-in to the margin config option, converted to world units (UNITS.WU) for usage in compute callbacks or outside of the hook. Note that the returned value is in UNITS.WU regardless of how the marginUnits config option is set.
  margin: THREE.Vector4
}

Important: internally, the Use2DBoundsResults is stored in a React.ref, since calculations are performed every frame. If you choose to update the object's properties yourself (by setting damp = false) or use the return values for other calculations, make sure not to tie them to the React lifecycle.

❌ Incorrect usage:

<mesh position={results.targets.position} />

✅ Better (only an example):

const { targets: position } = use2DBounds(ref)
const setPosition = useCallback(() => {
  ref.current.position.copy(position)
  ref.current.matrixWorldNeedsUpdate = true
}, [])
return <mesh ref={ref} onBeforeRender={setPosition} />

example usage:

const FiberComponent = ({ trackingElementRef }) => {
  const ref = useRef()
  // center the mesh at the trackingElement centre and scale it proportionally so that its width fills the element.
  use2DBounds(ref, {
    trackingElement: true,
    trackingElementRef: trackingElementRef,
  })
  return (
    <mesh ref={ref}>
      <planeGeometry />
      <meshBasicMaterial map={texture} />
    </mesh>
  )
}

useProgress(count, time, pause, id, prefix, renderPriority) : { progress: Number[], setElapsed: Function }

A convenient hook to easily create linear-gradient()-based CSS progress bars in a three.js/r3f scene.

This hook requires a very basic markup/css setup. See example usage for details.

params:

  1. count (required): Number (integer) representing the number of items whose progress is tracked. For example, you can use this hook to create a carousel UI element, wherein count would represent the number of items in the carousel.

  2. time (required): Number representing the number of seconds to go from progress = 0.0 to progress = 1.0.

  3. pause (optional): Boolean representing whether to pause progress tracking. Default is false.

  4. id (optional): String representing an HTML element id whose direct children are individual elements to be styled. The number of children of the element should equal count. Default is "progress".

  5. prefix (optional): String representing a CSS variable prefix to use in order to update the style of the progress-tracking elements. Default is "p".

  6. renderPriority (optional): Number (integer) representing the render priority in the internally-used useFrame hook. Default is undefined.

return value:

An Object with the following properties:

  • progress: An Array with length configured by the count param representing with values corresponding to the progress configured by the time param and dependent on the elapsed time and the pause param. After count * time unpaused seconds have elapsed, the progress is reset. E.g:

    const { progress } = useProgress(5, 5)
    
    // unpaused time elapsed: 2.5 seconds
    // progress array: [0.5, 0, 0, 0, 0]
    
    // unpaused time elapsed: 17 seconds
    // progress array: [1.0, 1.0, 1.0, 0.4, 0]
    
    // unpaused time elapsed: 35 seconds (reset after 25 seconds)
    // progress array: [1.0, 1.0, 0, 0, 0]

    Important: internally, the returned array is stored in a React.ref, since calculations are performed every frame. If you use the returned values, do not tie them into the React lifecycle much like you would not set state in a r3f useFrame callback. If you want to imperatively update the progress value, use setElapsed.

  • setElapsed: A Function that takes a single Number as an argument. Used to imperatively update the progress outside of the hook. E.g.:

    const { setElapsed } = useProgress(5, 5)
    
    useEffect(() => {
      // if page === 1 then elapsed = 5.
      // progress array set to: [1, 0, 0, 0, 0]
    
      // if page === 4.5 then elapsed = 22.5.
      // progress array set to: [1, 1, 1, 1, 0.5]
      setElapsed(page * 5)
    }, [page])

example usage:

Custom carousel UI element, automatically updated:

/* App.jsx */
const Carousel = () => {
  return (
    <div id='carousel'>
      <div />
      <div />
      <div />
    </div>
  )
}
const FiberComponent = () => {
  // 3 carousel items, 3 seconds each
  useProgress(3, 3.0, false, 'carousel')
  /* return ... */
}

const App = () => {
  return (
    <>
      /* ... */
      <Canvas>
        <FiberComponent />
      </Canvas>
      <Carousel>
      /* ... */
    </>
  )
}
/* App.css */
@property --p0 {
  syntax: '<length-percentage>';
  inherits: false;
  initial-value: 0%;
}
@property --p1 {
  syntax: '<length-percentage>';
  inherits: false;
  initial-value: 0%;
}
@property --p2 {
  syntax: '<length-percentage>';
  inherits: false;
  initial-value: 0%;
}
#carousel {
  width: 800px;
  height: 10px;
  display: flex;
  flex-wrap: nowrap;
  justify-content: space-between;
  align-items: stretch;
  column-gap: 16px;
}

#carousel > * {
  flex-shrink: 1;
  flex-grow: 1;
}

#carousel > *:nth-child(1) {
  --p0: 0%;
  background: linear-gradient(
    to right,
    #fff 0%,
    #fff var(--p0),
    #000 var(--p0)
  );
}
#carousel > *:nth-child(2) {
  --p1: 0%;
  background: linear-gradient(
    to right,
    #fff 0%,
    #fff var(--p1),
    #000 var(--p1)
  );
}
#carousel > *:nth-child(3) {
  --p2: 0%;
  background: linear-gradient(
    to right,
    #fff 0%,
    #fff var(--p2),
    #000 var(--p2)
  );
}

Custom carousel UI element sets progress on item click. Custom prefix.

/* App.jsx */

// const Carousel defined as in previous example
const FiberComponent = () => {
  const { setElapsed } = useProgress(3, 5.0, false, 'carousel', 'carousel')
  return (
    <>
      <FirstCarouselItem onClick={setElapsed(0.0)}>
      <SecondCarouselItem onClick={setElapsed(5.0)}>
      <ThirdCarouselItem onClick={setElapsed(10.0)}>
    </>
  )
}

// const App defined as in previous example
/* App.css */
@property --carousel0 {
  syntax: '<length-percentage>';
  inherits: false;
  initial-value: 0%;
}
@property --carousel1 {
  syntax: '<length-percentage>';
  inherits: false;
  initial-value: 0%;
}
@property --carousel2 {
  syntax: '<length-percentage>';
  inherits: false;
  initial-value: 0%;
}

/* ... */

#carousel > *:nth-child(1) {
  --carousel0: 0%; /* custom prefix */
  background: linear-gradient(
    to right,
    #fff 0%,
    #fff var(--carousel0),
    #000 var(--carousel0)
  );
  transition: --carousel0 1s; /* animation with CSS */
}
/* ... */

About

Personal Website and a useful Three.js/R3F helper library

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published