# Flying Bananas

Shows how to form self-contained components with their own state and user interaction.

Demo: https://pmndrs.github.io/examples/examples/flying-bananas
Source: https://github.com/pmndrs/examples/tree/main/examples/flying-bananas
Scaffold: npx degit pmndrs/examples/examples/flying-bananas
Published: 2021-09-29
Authors: Paul Henschel
Tags: lod, gltf, dof, postprocessing
Ported from: https://codesandbox.io/s/2ycs3
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, styled-components@^6.1.1, three@0.165.0
Binary files, in the repository but not below: src/AyerPoster-Medium.ttf, src/AyerPoster-Medium.woff, src/AyerPoster-Medium.woff2, src/banana-v1-transformed.glb, src/banana-v2-transformed.glb
Too large to inline, in the repository: src/layout/VelvetBanana.tsx (90 kB)

## src/Bananas.tsx

```tsx
import * as THREE from "three";
import { useRef, useState } from "react";
import { Canvas, useThree, useFrame } from "@react-three/fiber";
// https://github.com/pmndrs/drei
import { useGLTF, Detailed, Environment } from "@react-three/drei";
// https://github.com/pmndrs/react-postprocessing
// https://github.com/vanruesc/postprocessing
import {
  EffectComposer,
  DepthOfField,
  ToneMapping,
} from "@react-three/postprocessing";

import { type GLTF } from "three-stdlib";

import bananaModel from "./banana-v1-transformed.glb?url";

type GLTFResult = GLTF & {
  nodes: {
    banana_high: THREE.Mesh;
    banana_mid: THREE.Mesh;
    banana_low: THREE.Mesh;
  };
  materials: {
    skin: THREE.MeshStandardMaterial;
  };
};

function Banana({
  index,
  z,
  speed,
}: {
  index: number;
  z: number;
  speed: number;
}) {
  const ref = useRef<THREE.LOD>(null!);
  // useThree gives you access to the R3F state model
  const { viewport, camera } = useThree();
  // getCurrentViewport is a helper that calculates the size of the viewport
  const { width, height } = viewport.getCurrentViewport(camera, [0, 0, -z]);
  // useGLTF is an abstraction around R3F's useLoader(GLTFLoader, url)
  // It can automatically handle draco and meshopt-compressed assets without you having to
  // worry about binaries and such ...
  const { nodes, materials } = useGLTF(bananaModel) as unknown as GLTFResult;
  // By the time we're here the model is loaded, this is possible through React suspense

  // Local component state, it is safe to mutate because it's fixed data
  const [data] = useState({
    // Randomly distributing the objects along the vertical
    y: THREE.MathUtils.randFloatSpread(height * 2),
    // This gives us a random value between -1 and 1, we will multiply it with the viewport width
    x: THREE.MathUtils.randFloatSpread(2),
    // How fast objects spin, randFlost gives us a value between min and max, in this case 8 and 12
    spin: THREE.MathUtils.randFloat(8, 12),
    // Some random rotations, Math.PI represents 360 degrees in radian
    rX: Math.random() * Math.PI,
    rZ: Math.random() * Math.PI,
  });

  // useFrame executes 60 times per second
  useFrame((state, dt) => {
    // Make the X position responsive, slowly scroll objects up at the Y, distribute it along the Z
    // dt is the delta, the time between this frame and the previous, we can use it to be independent of the screens refresh rate
    // We cap dt at 0.1 because now it can't accumulate while the user changes the tab, it will simply stop
    if (dt < 0.1)
      ref.current.position.set(
        index === 0 ? 0 : data.x * width,
        (data.y += dt * speed),
        -z,
      );
    // Rotate the object around
    ref.current.rotation.set(
      (data.rX += dt / data.spin),
      Math.sin(index * 1000 + state.clock.elapsedTime / 10) * Math.PI,
      (data.rZ += dt / data.spin),
    );
    // If they're too far up, set them back to the bottom
    if (data.y > height * (index === 0 ? 4 : 1))
      data.y = -(height * (index === 0 ? 4 : 1));
  });

  // Using drei's detailed is a nice trick to reduce the vertex count because
  // we don't need high resolution for objects in the distance. The model contains 3 decimated meshes ...
  return (
    <Detailed ref={ref} distances={[0, 65, 80]}>
      <mesh
        geometry={nodes.banana_high.geometry}
        material={materials.skin}
        material-emissive="#ff9f00"
      />
      <mesh
        geometry={nodes.banana_mid.geometry}
        material={materials.skin}
        material-emissive="#ff9f00"
      />
      <mesh
        geometry={nodes.banana_low.geometry}
        material={materials.skin}
        material-emissive="#ff9f00"
      />
    </Detailed>
  );
}

export default function Bananas({
  speed = 1,
  count = 80,
  depth = 80,
  easing = (x: number) => Math.sqrt(1 - Math.pow(x - 1, 2)),
}: {
  speed?: number;
  count?: number;
  depth?: number;
  easing?: (x: number) => number;
}) {
  return (
    // No need for antialias (faster), dpr clamps the resolution to 1.5 (also faster than full resolution)
    // As of three > r154 if postprocessing is used the canvas can not have tonemapping (which is what "flat" is, no tonemapping)
    <Canvas
      flat
      gl={{ antialias: false }}
      dpr={[1, 1.5]}
      camera={{ position: [0, 0, 10], fov: 20, near: 0.01, far: depth + 15 }}
    >
      <color attach="background" args={["#ffbf40"]} />
      {/* As of three > r153 lights work differently in threejs, to get similar results as before you have to add decay={0} */}
      <spotLight
        position={[10, 20, 10]}
        penumbra={1}
        decay={0}
        intensity={3}
        color="orange"
      />
      {/* Using cubic easing here to spread out objects a little more interestingly, i wanted a sole big object up front ... */}
      {Array.from(
        { length: count },
        (_, i) => <Banana key={i} index={i} z={Math.round(easing(i / count) * depth)} speed={speed} /> /* prettier-ignore */,
      )}
      <Environment preset="sunset" />
      {/* Multisampling (MSAA) is WebGL2 antialeasing, we don't need it (faster)
          The normal-pass is not required either, saves a bit of performance */}
      {/* `disableNormalPass` no longer exists in this postprocessing version; the normal pass is already disabled by default */}
      <EffectComposer multisampling={0}>
        <DepthOfField
          target={[0, 0, 60]}
          focalLength={0.4}
          bokehScale={14}
          height={700}
        />
        {/* As of three > r154 tonemapping is not applied on rendertargets any longer, it requires a pass */}
        <ToneMapping />
      </EffectComposer>
    </Canvas>
  );
}
```

## src/index.tsx

```tsx
/*
Gumroad tutorial: https://0xca0a.gumroad.com/l/B4N4N4S
-------------------------------------------------------------------------------
Model auto-generated by: https://github.com/pmndrs/gltfjsx
Author: thesidekick (https://sketchfab.com/thesidekick)
License: CC-BY-4.0 (http://creativecommons.org/licenses/by/4.0/)
Source: https://sketchfab.com/3d-models/banana-dda3a1f707a94c52bed79578e120937c
Title: Banana
*/

import { createRoot } from "react-dom/client";
import { Suspense, useState } from "react";
import "./styles.css";
import Overlay from "./layout/Overlay";
import { FadeIn, LeftMiddle } from "./layout/styles";

import Bananas from "./Bananas";
// Comment the above and uncomment the following to import the WebGL BG lazily for faster loading times
// const Bananas = lazy(() => import('./Bananas'))

function App() {
  const [speed, set] = useState(1);
  return (
    <>
      <Suspense fallback={null}>
        <Bananas speed={speed} />
        <FadeIn />
      </Suspense>
      <Overlay />
      <LeftMiddle>
        <input
          type="range"
          min="0"
          max="10"
          value={speed}
          step="1"
          onChange={(e) => set(Number(e.target.value))}
        />
      </LeftMiddle>
    </>
  );
}

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

## src/layout/Overlay.tsx

```tsx
import {
  Container,
  TopLeft,
  BottomLeft,
  BottomRight,
  Hamburger,
} from "./styles";
import { VelvetBanana } from "./VelvetBanana";

export default function Overlay() {
  return (
    <Container>
      <TopLeft>
        <h1>
          LANDING
          <br />
          PAGES —
        </h1>
        <p>In React & Threejs —</p>
      </TopLeft>
      <BottomLeft>
        A runtime deconstruction of{" "}
        <a href="https://playful.software">playful.software</a>
      </BottomLeft>
      <BottomRight>
        Inspiration and ideas
        <br />
        Fundamentals
        <br />
        Finding models
        <br />
        Preparing them for the web
        <br />
        Displaying and changing models
        <br />
        Animation fundamentals
        <br />
        Effects and making things look good
        <br />
        Performance and time to load
        <br />
      </BottomRight>
      <Hamburger>
        <div />
        <div />
        <div />
      </Hamburger>
      <VelvetBanana />
    </Container>
  );
}
```

## src/layout/styles.ts

```ts
import styled, { keyframes } from "styled-components";

export const fade = keyframes`
  from { opacity: 1; }
  to { opacity: 0; }
`;

export const FadeIn = styled.div`
  position: absolute;
  top: 0;
  left: 0;
  width: 100%;
  height: 100%;
  pointer-events: none;
  background: #ffd863;
  animation: ${fade} 4s normal forwards ease-in-out;
`;

export const Container = styled.div`
  font-family: "Inter";
  font-size: 16px;
  & h1 {
    padding: 0;
    margin: 0 0 0.05em 0;
    font-family: "Ayer Poster", serif;
    font-weight: 400;
    font-size: min(18vw, 14em);
    line-height: 0.85em;
  }
`;

export const TopLeft = styled.div`
  position: absolute;
  top: 5vw;
  left: 5vw;
`;

export const BottomLeft = styled.div`
  position: absolute;
  bottom: 5vw;
  left: 5vw;
  width: 30ch;
  max-width: 40%;
`;

export const BottomRight = styled.div`
  position: absolute;
  bottom: 5vw;
  right: 5vw;
  width: 35ch;
  max-width: 40%;
  line-height: 1em;
  letter-spacing: -0.01em;
  text-align: right;
`;

export const Hamburger = styled.div`
  position: absolute;
  display: flex;
  flex-direction: column;
  top: 5vw;
  right: 5vw;
  & > div {
    position: relative;
    width: 24px;
    height: 2px;
    background: #252525;
    margin-bottom: 6px;
  }
`;

export const LeftMiddle = styled.div`
  position: absolute;
  bottom: 50%;
  right: 5vw;
  font-family: "Inter";
  font-weight: 400;
  line-height: 1em;
  letter-spacing: -0.01em;
  font-size: 12px;
  transform: rotate(90deg) translate3d(50%, 0, 0);
  transform-origin: 100% 50%;
`;
```

## src/styles.css

```css
@import url("https://rsms.me/inter/inter.css");
@font-face {
  font-family: "Ayer Poster";
  src:
    local("Ayer Poster Medium"),
    local("Ayer-Poster-Medium"),
    url("AyerPoster-Medium.woff2") format("woff2"),
    url("AyerPoster-Medium.woff") format("woff"),
    url("AyerPoster-Medium.ttf") format("truetype");
  font-weight: 500;
  font-style: normal;
}

* {
  box-sizing: border-box;
}

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

body {
  background: #ffd863;
}

a {
  color: black;
}

input[type="range"] {
  display: block;
  -webkit-appearance: none;
  -moz-appearance: none;
  background: black;
  border-radius: 5px;
  width: 100%;
  height: 2px;
  outline: 0;
}

input[type="range"]::-webkit-slider-thumb {
  -webkit-appearance: none;
  background-color: #000;
  width: 25px;
  height: 25px;
  border-radius: 50%;
  cursor: pointer;
  transition: 0.3s ease-in-out;
}

input[type="range"]::-webkit-slider-thumb:active {
  transform: scale(1);
}
```

## src/vite-env.d.ts

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