# Shoe Configurator

A super small configurator that shows you how to make GLTF assets dynamic.

Demo: https://pmndrs.github.io/examples/examples/shoe-configurator
Source: https://github.com/pmndrs/examples/tree/main/examples/shoe-configurator
Scaffold: npx degit pmndrs/examples/examples/shoe-configurator
Published: 2020-12-11
Authors: Paul Henschel
Tags: state, gltf, configurator
Ported from: https://codesandbox.io/s/qxjoj
Dependencies: @react-three/drei@10.7.8, @react-three/fiber@9.6.1, react@19.2.8, react-colorful@^5.6.1, react-dom@19.2.8, three@0.165.0, valtio@^1.13.2
Binary files, in the repository but not below: src/shoe-draco.glb

## src/App.tsx

```tsx
import { useRef, useState, useEffect } from "react";
import { Canvas, useFrame } from "@react-three/fiber";
import {
  useGLTF,
  ContactShadows,
  Environment,
  OrbitControls,
} from "@react-three/drei";
import { HexColorPicker } from "react-colorful";
import { proxy, useSnapshot } from "valtio";
import * as THREE from "three";
import { type GLTF } from "three-stdlib";

import shoeModel from "./shoe-draco.glb?url";

type Items = Record<string, string>;

const state = proxy<{ current: string | null; items: Items }>({
  current: null,
  items: {
    laces: "#fff",
    mesh: "#fff",
    caps: "#fff",
    inner: "#fff",
    sole: "#fff",
    stripes: "#fff",
    band: "#fff",
    patch: "#fff",
  },
});

type GLTFResult = GLTF & {
  nodes: {
    shoe: THREE.Mesh;
    shoe_1: THREE.Mesh;
    shoe_2: THREE.Mesh;
    shoe_3: THREE.Mesh;
    shoe_4: THREE.Mesh;
    shoe_5: THREE.Mesh;
    shoe_6: THREE.Mesh;
    shoe_7: THREE.Mesh;
  };
  materials: {
    laces: THREE.MeshStandardMaterial;
    mesh: THREE.MeshStandardMaterial;
    caps: THREE.MeshStandardMaterial;
    inner: THREE.MeshStandardMaterial;
    sole: THREE.MeshStandardMaterial;
    stripes: THREE.MeshStandardMaterial;
    band: THREE.MeshStandardMaterial;
    patch: THREE.MeshStandardMaterial;
  };
};

export default function App() {
  return (
    <>
      <Canvas shadows camera={{ position: [0, 0, 4], fov: 45 }}>
        <ambientLight intensity={0.7 * Math.PI} />
        <spotLight
          intensity={0.5 * Math.PI}
          decay={0}
          angle={0.1}
          penumbra={1}
          position={[10, 15, 10]}
          castShadow
        />
        <Shoe />
        <Environment preset="city" />
        <ContactShadows
          position={[0, -0.8, 0]}
          opacity={0.25}
          scale={10}
          blur={1.5}
          far={0.8}
        />
        <OrbitControls
          minPolarAngle={Math.PI / 2}
          maxPolarAngle={Math.PI / 2}
          enableZoom={false}
          enablePan={false}
        />
      </Canvas>
      <Picker />
    </>
  );
}

function Shoe() {
  const ref = useRef<THREE.Group>(null!);
  const snap = useSnapshot(state);
  const { nodes, materials } = useGLTF(shoeModel) as unknown as GLTFResult;
  const [hovered, set] = useState<string | null>(null);

  useFrame((state) => {
    const t = state.clock.getElapsedTime();
    ref.current.rotation.set(
      Math.cos(t / 4) / 8,
      Math.sin(t / 4) / 8,
      -0.2 - (1 + Math.sin(t / 1.5)) / 20,
    );
    ref.current.position.y = (1 + Math.sin(t / 1.5)) / 10;
  });

  useEffect(() => {
    // hovered is only read here when truthy (see the `if (hovered)` check below)
    const cursor = `<svg width="64" height="64" fill="none" xmlns="http://www.w3.org/2000/svg"><g clip-path="url(#clip0)"><path fill="rgba(255, 255, 255, 0.5)" d="M29.5 54C43.031 54 54 43.031 54 29.5S43.031 5 29.5 5 5 15.969 5 29.5 15.969 54 29.5 54z" stroke="#000"/><g filter="url(#filter0_d)"><path d="M29.5 47C39.165 47 47 39.165 47 29.5S39.165 12 29.5 12 12 19.835 12 29.5 19.835 47 29.5 47z" fill="${snap.items[hovered as string]}"/></g><path d="M2 2l11 2.947L4.947 13 2 2z" fill="#000"/><text fill="#000" style="#fff-space:pre" font-family="Inter var, sans-serif" font-size="10" letter-spacing="-.01em"><tspan x="35" y="63">${hovered}</tspan></text></g><defs><clipPath id="clip0"><path fill="#fff" d="M0 0h64v64H0z"/></clipPath><filter id="filter0_d" x="6" y="8" width="47" height="47" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB"><feFlood flood-opacity="0" result="BackgroundImageFix"/><feColorMatrix in="SourceAlpha" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0"/><feOffset dy="2"/><feGaussianBlur stdDeviation="3"/><feColorMatrix values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.15 0"/><feBlend in2="BackgroundImageFix" result="effect1_dropShadow"/><feBlend in="SourceGraphic" in2="effect1_dropShadow" result="shape"/></filter></defs></svg>`;
    const auto = `<svg width="64" height="64" fill="none" xmlns="http://www.w3.org/2000/svg"><path fill="rgba(255, 255, 255, 0.5)" d="M29.5 54C43.031 54 54 43.031 54 29.5S43.031 5 29.5 5 5 15.969 5 29.5 15.969 54 29.5 54z" stroke="#000"/><path d="M2 2l11 2.947L4.947 13 2 2z" fill="#000"/></svg>`;
    if (hovered) {
      document.body.style.cursor = `url('data:image/svg+xml;base64,${btoa(cursor)}'), auto`;
      return () => {
        document.body.style.cursor = `url('data:image/svg+xml;base64,${btoa(auto)}'), auto`;
      };
    }
  }, [hovered]);

  return (
    <group
      ref={ref}
      onPointerOver={(e) => (
        e.stopPropagation(),
        // e.object is one of the shoe meshes above, each with a single named material
        set(((e.object as THREE.Mesh).material as THREE.Material).name)
      )}
      onPointerOut={(e) => e.intersections.length === 0 && set(null)}
      onPointerMissed={() => (state.current = null)}
      onClick={(e) => (
        e.stopPropagation(),
        (state.current = (
          (e.object as THREE.Mesh).material as THREE.Material
        ).name)
      )}
    >
      <mesh
        receiveShadow
        castShadow
        geometry={nodes.shoe.geometry}
        material={materials.laces}
        material-color={snap.items.laces}
      />
      <mesh
        receiveShadow
        castShadow
        geometry={nodes.shoe_1.geometry}
        material={materials.mesh}
        material-color={snap.items.mesh}
      />
      <mesh
        receiveShadow
        castShadow
        geometry={nodes.shoe_2.geometry}
        material={materials.caps}
        material-color={snap.items.caps}
      />
      <mesh
        receiveShadow
        castShadow
        geometry={nodes.shoe_3.geometry}
        material={materials.inner}
        material-color={snap.items.inner}
      />
      <mesh
        receiveShadow
        castShadow
        geometry={nodes.shoe_4.geometry}
        material={materials.sole}
        material-color={snap.items.sole}
      />
      <mesh
        receiveShadow
        castShadow
        geometry={nodes.shoe_5.geometry}
        material={materials.stripes}
        material-color={snap.items.stripes}
      />
      <mesh
        receiveShadow
        castShadow
        geometry={nodes.shoe_6.geometry}
        material={materials.band}
        material-color={snap.items.band}
      />
      <mesh
        receiveShadow
        castShadow
        geometry={nodes.shoe_7.geometry}
        material={materials.patch}
        material-color={snap.items.patch}
      />
    </group>
  );
}

function Picker() {
  const snap = useSnapshot(state);
  return (
    <div style={{ display: snap.current ? "block" : "none" }}>
      <HexColorPicker
        className="picker"
        color={snap.items[snap.current as string]}
        onChange={(color) => (state.items[snap.current as string] = color)}
      />
      <h1>{snap.current}</h1>
    </div>
  );
}
```

## src/index.tsx

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

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

## src/styles.css

```css
@import url("https://rsms.me/inter/inter.css");

html,
body,
#root {
  position: relative;
  margin: 0;
  padding: 0;
  overflow: hidden;
  outline: none;
  width: 100vw;
  height: 100vh;
}

body {
  background: white;
  font-family: "Inter var", sans-serif;
  display: flex;
  justify-content: center;
}

#root {
  width: 76ch;
}

h1 {
  position: absolute;
  top: 43px;
  left: 140px;
  padding: 0;
  margin: 40px;
  font-size: 5em;
  font-weight: 400;
  line-height: 0.7em;
  letter-spacing: -2px;
  color: #272730;
}

.picker {
  position: absolute !important;
  top: 74px;
  left: 70px;
  width: 90px !important;
  height: 90px !important;
}
```

## src/vite-env.d.ts

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