# Shopping

Demo: https://pmndrs.github.io/examples/examples/shopping
Source: https://github.com/pmndrs/examples/tree/main/examples/shopping
Scaffold: npx degit pmndrs/examples/examples/shopping
Published: 2022-09-18
Authors: Paul Henschel
Tags: outlines, postprocessing, bvh, mask
Ported from: https://codesandbox.io/s/4gy946
Dependencies: @react-three/drei@10.7.8, @react-three/fiber@9.6.1, @react-three/postprocessing@^3.0.4, react@19.2.8, react-dom@19.2.8, three@0.165.0
Binary files, in the repository but not below: src/Inter-Regular.woff, src/kitchen-transformed.glb

## src/App.tsx

```tsx
import * as THREE from "three";
import { easing } from "maath";
import { Canvas, useFrame, useThree } from "@react-three/fiber";
import { Sky, Bvh } from "@react-three/drei";
import {
  EffectComposer,
  Selection,
  Outline,
  N8AO,
  TiltShift2,
  ToneMapping,
} from "@react-three/postprocessing";
import { Scene } from "./Scene";

export const App = () => (
  <Canvas
    flat
    dpr={[1, 1.5]}
    gl={{ antialias: false }}
    camera={{ position: [0, 1, 6], fov: 25, near: 1, far: 20 }}
  >
    <ambientLight intensity={1.5 * Math.PI} />
    <Sky />
    <Bvh firstHitOnly>
      <Selection>
        <Effects />
        <Scene rotation={[0, Math.PI / 2, 0]} position={[0, -1, -0.85]} />
      </Selection>
    </Bvh>
  </Canvas>
);

function Effects() {
  const { size } = useThree();
  useFrame((state, delta) => {
    easing.damp3(
      state.camera.position,
      [
        state.pointer.x,
        1 + state.pointer.y / 2,
        8 + Math.atan(state.pointer.x * 2),
      ],
      0.3,
      delta,
    );
    state.camera.lookAt(state.camera.position.x * 0.9, 0, -4);
  });
  return (
    // `disableNormalPass` no longer exists in this postprocessing version; the normal pass is already disabled by default
    <EffectComposer stencilBuffer autoClear={false} multisampling={4}>
      <N8AO
        halfRes
        aoSamples={5}
        aoRadius={0.4}
        distanceFalloff={0.75}
        intensity={1}
      />
      <Outline
        // `postprocessing`'s OutlineEffect constructor types visibleEdgeColor
        // and hiddenEdgeColor as number, but it forwards straight into
        // `new THREE.Color(...)`, which also accepts a color name string like "white".
        visibleEdgeColor={"white" as unknown as number}
        hiddenEdgeColor={"white" as unknown as number}
        blur
        width={size.width * 1.25}
        edgeStrength={10}
      />
      <TiltShift2 samples={5} blur={0.1} />
      <ToneMapping />
    </EffectComposer>
  );
}
```

## src/Price.tsx

```tsx
import { useRef } from "react";
import { easing } from "maath";
import { useFrame, type ThreeElements } from "@react-three/fiber";
import { Text, Mask, useMask } from "@react-three/drei";
import * as THREE from "three";

import interFont from "./Inter-Regular.woff";

type PriceProps = ThreeElements["group"] & {
  value: number;
  currency?: string;
};

export const Price = ({ value, currency = "$", ...props }: PriceProps) => (
  <group {...props}>
    {[...`✨✨✨${value}`.slice(-4)].map((num, index) => (
      <Counter
        index={index}
        value={num === "✨" ? -1 : Number(num)}
        key={index}
        speed={0.1 * (4 - index)}
      />
    ))}
    <Text
      children={currency}
      anchorY="bottom"
      position={[4 * 1.1, -0.25, 0]}
      fontSize={1}
      font={interFont}
    />
    <Mask id={1}>
      <planeGeometry args={[10, 1.55]} />
    </Mask>
  </group>
);

function Counter({
  index,
  value,
  speed = 0.1,
}: {
  index: number;
  value: number;
  speed?: number;
}) {
  const ref = useRef<THREE.Group>(null!);
  const stencil = useMask(1);
  useFrame((state, delta) =>
    easing.damp(ref.current.position, "y", value * -2, speed, delta),
  );
  return (
    <group position-x={index * 1.1} ref={ref}>
      {[0, 1, 2, 3, 4, 5, 6, 7, 8, 9].map((number) => (
        <Text
          key={number}
          position={[0, number * 2, 0]}
          fontSize={2}
          font={interFont}
        >
          {number}
          <meshBasicMaterial {...stencil} />
        </Text>
      ))}
    </group>
  );
}
```

## src/Scene.tsx

```tsx
import { useState, useCallback, Suspense } from "react";
import { debounce } from "lodash";
import * as THREE from "three";
import { type ThreeElements, type ThreeEvent } from "@react-three/fiber";
import { useGLTF, useEnvironment, Text } from "@react-three/drei";
import { Select } from "@react-three/postprocessing";
import { type GLTF } from "three-stdlib";
import { Price } from "./Price";

import kitchenModel from "./kitchen-transformed.glb?url";
import interFont from "./Inter-Regular.woff?url";

type GLTFResult = GLTF & {
  nodes: {
    vase1: THREE.Mesh;
    bottle: THREE.Mesh;
    walls_1: THREE.Mesh;
    walls_2: THREE.Mesh;
    plant_1: THREE.Mesh;
    plant_2: THREE.Mesh;
    cuttingboard: THREE.Mesh;
    bowl: THREE.Mesh;
    carpet: THREE.Mesh;
    table: THREE.Mesh;
    chairs_1: THREE.Mesh;
    chairs_2: THREE.Mesh;
    vase: THREE.Mesh;
    lamp_socket: THREE.Mesh;
    lamp: THREE.Mesh;
    lamp_cord: THREE.Mesh;
    kitchen: THREE.Mesh;
    sink: THREE.Mesh;
  };
  materials: {
    gray: THREE.MeshStandardMaterial;
    floor: THREE.MeshStandardMaterial;
    walls: THREE.MeshStandardMaterial;
    potted_plant_01_leaves: THREE.MeshStandardMaterial;
    potted_plant_01_pot: THREE.MeshStandardMaterial;
    carpet: THREE.MeshStandardMaterial;
    plastic: THREE.MeshStandardMaterial;
    chrome: THREE.MeshStandardMaterial;
  };
};

/*
Auto-generated by: https://github.com/pmndrs/gltfjsx
Command: npx gltfjsx@6.2.16 kitchen.glb --transform
Files: kitchen.glb [134.46MB] > kitchen-transformed.glb [2.1MB] (98%)
*/
export function Scene(props: ThreeElements["group"]) {
  // Load model
  const { nodes, materials } = useGLTF(kitchenModel) as unknown as GLTFResult;
  // Load environment (using it only on the chairs, for reflections)
  const env = useEnvironment({ preset: "city" });
  // Hover state
  const [hovered, hover] = useState<string | null>(null);
  // Debounce hover a bit to stop the ticker from being erratic
  const debouncedHover = useCallback(debounce(hover, 30), []);
  const over = (name: string) => (e: ThreeEvent<PointerEvent>) => (
    e.stopPropagation(),
    debouncedHover(name)
  );
  // Get the priced item
  const price =
    (
      {
        KNOXHULT: 5999,
        BRÖNDEN: 433,
        SKAFTET: 77,
        FANBYN: 255,
        VOXLÖV: 1699,
        LIVSVERK: 44,
      } as Record<string, number>
    )[String(hovered)] ?? 5999;
  return (
    <>
      <group {...props}>
        <mesh
          geometry={nodes.vase1.geometry}
          material={materials.gray}
          material-envMap={env}
        />
        <mesh
          geometry={nodes.bottle.geometry}
          material={materials.gray}
          material-envMap={env}
        />
        <mesh geometry={nodes.walls_1.geometry} material={materials.floor} />
        <mesh geometry={nodes.walls_2.geometry} material={materials.walls} />
        <mesh
          geometry={nodes.plant_1.geometry}
          material={materials.potted_plant_01_leaves}
        />
        <mesh
          geometry={nodes.plant_2.geometry}
          material={materials.potted_plant_01_pot}
        />
        <mesh
          geometry={nodes.cuttingboard.geometry}
          material={materials.walls}
        />
        <mesh geometry={nodes.bowl.geometry} material={materials.walls} />
        <Select
          enabled={hovered === "BRÖNDEN"}
          onPointerOver={over("BRÖNDEN")}
          onPointerOut={() => debouncedHover(null)}
        >
          <mesh geometry={nodes.carpet.geometry} material={materials.carpet} />
        </Select>
        <Select
          enabled={hovered === "VOXLÖV"}
          onPointerOver={over("VOXLÖV")}
          onPointerOut={() => debouncedHover(null)}
        >
          <mesh
            geometry={nodes.table.geometry}
            material={materials.walls}
            material-envMap={env}
            material-envMapIntensity={0.5}
          />
        </Select>
        <Select
          enabled={hovered === "FANBYN"}
          onPointerOver={over("FANBYN")}
          onPointerOut={() => debouncedHover(null)}
        >
          <mesh geometry={nodes.chairs_1.geometry} material={materials.walls} />
          <mesh
            geometry={nodes.chairs_2.geometry}
            material={materials.plastic}
            material-color="#1a1a1a"
            material-envMap={env}
          />
        </Select>
        <Select
          enabled={hovered === "LIVSVERK"}
          onPointerOver={over("LIVSVERK")}
          onPointerOut={() => debouncedHover(null)}
        >
          <mesh
            geometry={nodes.vase.geometry}
            material={materials.gray}
            material-envMap={env}
          />
        </Select>
        <Select
          enabled={hovered === "SKAFTET"}
          onPointerOver={over("SKAFTET")}
          onPointerOut={() => debouncedHover(null)}
        >
          <mesh
            geometry={nodes.lamp_socket.geometry}
            material={materials.gray}
            material-envMap={env}
          />
          <mesh geometry={nodes.lamp.geometry} material={materials.gray} />
          <mesh
            geometry={nodes.lamp_cord.geometry}
            material={materials.gray}
            material-envMap={env}
          />
        </Select>
        <mesh geometry={nodes.kitchen.geometry} material={materials.walls} />
        <mesh
          geometry={nodes.sink.geometry}
          material={materials.chrome}
          material-envMap={env}
        />
      </group>
      <Suspense fallback={null}>
        <Text
          position={[1, 1.25, 0]}
          color="black"
          fontSize={0.15}
          font={interFont}
          letterSpacing={-0.05}
        >
          {hovered ? hovered : "KNOXHULT"}
        </Text>
      </Suspense>

      {/** Forward the price to the ticker component */}
      <Price value={price} position={[-2, 0.3, -3.25]} />
    </>
  );
}
```

## src/index.tsx

```tsx
import { createRoot } from "react-dom/client";

import { App } from "./App";
import "./styles.css";

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

## src/styles.css

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

html,
body,
#root {
  width: 100%;
  height: 100%;
  margin: 0;
  padding: 0;
  overflow: hidden;
}

body {
  position: fixed;
  overflow: hidden;
}
```

## src/vite-env.d.ts

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