# D. Bismut Furniture

Demo: https://pmndrs.github.io/examples/examples/dbismut-furniture
Source: https://github.com/pmndrs/examples/tree/main/examples/dbismut-furniture
Scaffold: npx degit pmndrs/examples/examples/dbismut-furniture
Published: 2023-03-27
Authors: Paul Henschel
Tags: transitions, spring, gltf, html
Ported from: https://codesandbox.io/s/62o18n
Dependencies: @react-spring/three@^10.1.2, @react-three/drei@10.7.8, @react-three/fiber@9.6.1, react@19.2.8, react-dom@19.2.8, three@0.165.0
Binary files, in the repository but not below: src/bench.glb, src/stool.glb, src/table.glb

## src/App.tsx

```tsx
import React, { useState, useMemo, useRef } from "react";
import { Scene } from "./Scene";

import stoolModel from "./stool.glb?url";
import tableModel from "./table.glb?url";
import benchModel from "./bench.glb?url";

const objects = ["Stool", "Table", "Bench"];
const models = [stoolModel, tableModel, benchModel];
const fakeObjects =
  "Lamp base. Stool. Outside table. Dish drainer. Closet door. Table. Square stool. Clothes rack. Bench. Bird feeder. Shelf. Handeplane surfing. Marking knife. Cutting board. Watch display".split(
    ". ",
  );

type ItemProps = {
  text: string;
  onHover: (index: number) => void;
  index: number;
  active: boolean;
};

const Item = ({ text, onHover, index, active }: ItemProps) => {
  const modelIdx = useMemo(() => objects.indexOf(text), [text]);
  const has3d = modelIdx > -1;
  return (
    <span
      className={active ? "active" : has3d ? "" : "item"}
      onPointerEnter={() => has3d && onHover(index)}
    >
      {text + "."}
    </span>
  );
};

export default function App() {
  const ref = useRef<HTMLDivElement>(null!);
  const [idx, setIdx] = useState(1);
  const modelIdx = objects.indexOf(fakeObjects[idx]);
  return (
    <>
      <div className="scene">
        <Scene shownIndex={modelIdx} models={models} target={ref} />
      </div>
      <div className="container" ref={ref}>
        {fakeObjects.map((o, i) => (
          <Item
            key={i}
            text={o}
            active={i === idx}
            index={i}
            onHover={setIdx}
          />
        ))}
      </div>
    </>
  );
}
```

## src/Scene.tsx

```tsx
import * as THREE from "three";
import {
  useRef,
  useState,
  useEffect,
  useMemo,
  type RefObject,
  type JSX,
} from "react";
import {
  Canvas,
  useFrame,
  createPortal,
  useThree,
  type ObjectMap,
  type ThreeElements,
} from "@react-three/fiber";
import {
  PerspectiveCamera,
  ScreenQuad,
  useGLTF,
  useFBO,
} from "@react-three/drei";
import { a, useSprings, type SpringValue } from "@react-spring/three";
import { type GLTF } from "three-stdlib";
import { CrossFadeMaterial } from "./XFadeMaterial";

type ModelData = GLTF & ObjectMap;

type SpringTransitionProps = {
  rotation: SpringValue<number[]>;
  scale: SpringValue<number[]>;
};

// The installed @react-three/drei types dropped the legacy `multisample`
// FBO setting that this example still passes; widen the settings type
// locally instead of changing the values.
type FBOOptions = NonNullable<Parameters<typeof useFBO>[2]> & {
  multisample?: boolean;
};

const transitions = {
  from: { rotation: [0, -Math.PI / 10, 0], scale: [0.8, 0.8, 0.8] },
  enter: { rotation: [0, 0, 0], scale: [1, 1, 1] },
  leave: { rotation: [0, Math.PI / 10, 0], scale: [0.8, 0.8, 0.8] },
};

const sceneStyle = { width: "100vw", height: "56vw" };
const enter = transitions.enter;
const leave = transitions.leave;

// @react-spring/three's animated() wrapper computes a deeply recursive prop
// type for the generic `primitive` element that trips up the type checker
// ("Type instantiation is excessively deep"); widen the prop type locally
// instead of changing the values.
type AnimatedPrimitiveProps = ThreeElements["primitive"] &
  SpringTransitionProps;

const AnimatedPrimitive = a.primitive as unknown as (
  props: AnimatedPrimitiveProps,
) => JSX.Element;

type ModelProps = SpringTransitionProps & {
  model: ModelData;
};

function Model({ model, ...props }: ModelProps) {
  const ref = useRef<THREE.Group>(null!);
  const [rEuler, rQuaternion] = useMemo(
    () => [new THREE.Euler(), new THREE.Quaternion()],
    [],
  );
  useFrame((state) => {
    rEuler.set(
      0,
      (state.mouse.x * Math.PI) / 150,
      (-state.mouse.y * Math.PI) / 150,
    );
    ref.current.quaternion.slerp(rQuaternion.setFromEuler(rEuler), 0.1);
  });
  return (
    <group ref={ref}>
      <spotLight
        intensity={0.7 * Math.PI}
        decay={0}
        position={[8, 6, -4]}
        penumbra={0}
      />
      <AnimatedPrimitive {...props} object={model.scene} />
    </group>
  );
}

type RenderSceneProps = SpringTransitionProps & {
  target: THREE.WebGLRenderTarget;
  model: ModelData;
  camRef: RefObject<THREE.PerspectiveCamera>;
};

function RenderScene({ target, model, camRef, ...props }: RenderSceneProps) {
  const scene = useMemo(() => new THREE.Scene(), []);
  useFrame((state) => {
    state.gl.setRenderTarget(target);
    state.gl.render(scene, camRef.current);
  }, 0);
  return createPortal(<Model model={model} {...props} />, scene);
}

type ModelsProps = {
  shownIndex: number;
  models: string[];
};

function Models({ shownIndex, models }: ModelsProps) {
  const _models = useGLTF(models);
  const [idxesInScenes] = useState([
    shownIndex,
    (shownIndex + 1) % models.length,
  ]);
  const hiddenTxt = useRef(1);
  const shownTxt = useMemo(() => {
    if (idxesInScenes.indexOf(shownIndex) < 0)
      idxesInScenes[hiddenTxt.current] = shownIndex;
    const idx = idxesInScenes.indexOf(shownIndex);
    hiddenTxt.current = idx ? 0 : 1;
    return idx;
  }, [shownIndex, idxesInScenes]);

  const t0 = useFBO({ stencilBuffer: false, multisample: true } as FBOOptions);
  const t1 = useFBO({ stencilBuffer: false, multisample: true } as FBOOptions);
  const targets = [t0, t1];
  const camRef = useRef<THREE.PerspectiveCamera>(null!);

  useFrame((state) => {
    state.gl.setRenderTarget(null);
    state.gl.render(state.scene, state.camera);
  }, 1);

  const [springs, api] = useSprings(
    2,
    (i) => transitions[i === 0 ? "enter" : "from"],
  );
  const regress = useThree((state) => state.performance.regress);

  useEffect(() => {
    api.start((i) => {
      const isEntering = i === shownTxt;
      const t = isEntering ? enter : leave;
      return { ...t, onChange: () => regress() };
    });
  }, [api, shownTxt, regress]);

  return (
    <>
      <PerspectiveCamera
        ref={camRef}
        position={[-2.71, 1.34, 1.8]}
        rotation={[-0.74, -1.14, -0.7]}
        far={9}
        fov={37.1}
      />
      <ScreenQuad>
        <CrossFadeMaterial
          attach="material"
          texture1={t0.texture}
          texture2={t1.texture}
          shownTxt={shownTxt}
        />
      </ScreenQuad>
      {springs.map((props, i) => (
        <RenderScene
          key={i}
          target={targets[i]}
          model={_models[idxesInScenes[i]]}
          camRef={camRef}
          {...props}
        />
      ))}
    </>
  );
}

type SceneProps = {
  models: string[];
  shownIndex?: number;
  target: RefObject<HTMLDivElement>;
};

export function Scene({ models, shownIndex = 0, target }: SceneProps) {
  return (
    <Canvas
      orthographic
      gl={{ antialias: false }}
      eventSource={target.current}
      style={sceneStyle}
    >
      <Models shownIndex={shownIndex} models={models} />
    </Canvas>
  );
}
```

## src/XFadeMaterial.tsx

```tsx
import * as THREE from "three";
import { useRef } from "react";
import {
  extend,
  useThree,
  useFrame,
  type ThreeElement,
} from "@react-three/fiber";
import { shaderMaterial } from "@react-three/drei";

export const XFadeMaterial = shaderMaterial(
  {
    texture1: null,
    texture2: null,
    shownTxt: -1,
    resolution: new THREE.Vector2(),
  },
  `void main() {
    gl_Position = vec4(position,1.0);
  }`,
  `#ifdef FXAA
    #pragma glslify: fxaa = require(glsl-fxaa)
  #endif

  varying vec2 v_texCoord0;
  uniform vec2 resolution;
  uniform sampler2D texture1;
  uniform sampler2D texture2;
  uniform float shownTxt;

  void main() {
    #ifdef FXAA
      vec4 _texture1 = fxaa(texture1, gl_FragCoord.xy, resolution);
      vec4 _texture2 = fxaa(texture2, gl_FragCoord.xy, resolution);
    #else
      vec2 uv = gl_FragCoord.xy / resolution;
      vec4 _texture1 = texture2D(texture1, uv);
      vec4 _texture2 = texture2D(texture2, uv);
    #endif
    float opacity = shownTxt < 0. ? 1. + shownTxt : 1.;
    float _shownTxt = shownTxt < 0. ? 0. : shownTxt;
    vec4 finalTexture = mix(_texture1, _texture2, _shownTxt);
    finalTexture =  vec4(finalTexture.rgb, opacity * finalTexture.a);
    gl_FragColor = finalTexture;
  }`,
);

extend({ XFadeMaterial });

export type XFadeMaterialProps = Omit<
  ThreeElement<typeof XFadeMaterial>,
  "texture1" | "texture2" | "shownTxt"
> & {
  texture1: THREE.Texture;
  texture2: THREE.Texture;
  shownTxt?: number;
};

export function CrossFadeMaterial({
  shownTxt = -1,
  ...props
}: XFadeMaterialProps) {
  const ref = useRef<InstanceType<typeof XFadeMaterial>>(null!);
  const { size, gl } = useThree();
  const dpr = gl.getPixelRatio();
  useFrame(() => {
    ref.current.shownTxt = THREE.MathUtils.lerp(
      ref.current.shownTxt,
      shownTxt,
      0.2,
    );
  });
  return (
    <xFadeMaterial
      ref={ref}
      {...props}
      resolution={[size.width * dpr, size.height * dpr]}
      defines={{ FXAA: !gl.capabilities.isWebGL2 }}
    />
  );
}
```

## src/index.tsx

```tsx
// Original: https://twitter.com/davidbismut/status/1347910911311405058

import { createRoot } from "react-dom/client";
import React from "react";
import "./styles.css";
import App from "./App";

createRoot(document.getElementById("root")!).render(<App />);
```

## src/react-three-fiber-jsx.d.ts

```ts
import type { ThreeElements } from "@react-three/fiber";
import type { XFadeMaterialProps } from "./XFadeMaterial";

declare global {
  namespace JSX {
    interface IntrinsicElements extends ThreeElements {}
    interface IntrinsicElements {
      xFadeMaterial: XFadeMaterialProps;
    }
  }
}

declare module "react" {
  namespace JSX {
    interface IntrinsicElements extends ThreeElements {}
    interface IntrinsicElements {
      xFadeMaterial: XFadeMaterialProps;
    }
  }
}

declare module "react/jsx-runtime" {
  namespace JSX {
    interface IntrinsicElements extends ThreeElements {}
    interface IntrinsicElements {
      xFadeMaterial: XFadeMaterialProps;
    }
  }
}
```

## src/styles.css

```css
* {
  box-sizing: border-box;
}

html,
body {
  margin: 0;
  padding: 0;
  width: 100%;
  height: 100%;
}

body {
  background: #d6d5cf;
  font-family: system-ui, sans-serif;
}

#root {
  width: 100%;
  height: 100%;
  display: flex;
  justify-content: center;
  align-items: center;
}

.container {
  position: relative;
  z-index: 10;
  font-weight: 600;
  font-size: 2.5em;
  width: 90%;
}

.scene {
  position: fixed;
  display: flex;
  align-items: center;
  justify-content: center;
  inset: 0;
}

.container > span {
  cursor: pointer;
  margin-right: 0.15em;
}

.item {
  opacity: 0.2;
}

span.active,
.item:hover {
  text-decoration-skip-ink: none;
  text-underline-offset: 0.07em;
  text-decoration: underline;
}
```

## src/vite-env.d.ts

```ts
/// <reference types="vite/client" />
```
