Source: r3f.js

/**
 * @file
 *
 * Summary.
 * <p>Two rotating cubes using {@link https://react.dev React}
 * without {@link https://nodejs.org/en Nodejs}.</p>
 * When the mouse is hovered onto a cube, its color changes from orange to hotpink.<br>
 * When a cube is clicked, its scale is toggled from 1 to 1.5 and its color changes.
 *
 * <p>This is a very simple script, and for running it the "normal" way,
 * it would be necessary to install the React ecosystem
 * (downloading at least 400 Mb of {@link https://www.npmjs.com npm} packages) and
 * either deploying it elsewhere, or having a local
 * {@link https://www.apache.org Apache} server configured.</p>
 *
 * @author Paulo Roma
 * @since 10/10/2024
 * @see <a href="/cwdc/14-react/r3f/cubes/r3f.js">source</a>
 * @see <a href="/cwdc/14-react/r3f/cubes/r3f.html">link</a>
 * @see {@link https://codesandbox.io/p/sandbox/sfypdx original code}
 */

import { createRoot } from "react-dom/client";
import React, { useRef, useState, useEffect } from "react";
import { Canvas, useFrame } from "@react-three/fiber";
import { Bounds, OrbitControls } from "@react-three/drei";

/**
 * Box component.
 * @param {Object} props information that you pass to a JSX tag.
 * @param {Array<Number>} props.position box position.
 * @param {String} props.name box name.
 * @returns {ThreeElements} view as regular three.js elements expressed in JSX.
 */
function Box(props) {
  // This reference will give us direct access to the mesh
  const meshRef = useRef();

  // Set up state for the clicked and active state
  const [clicked, setClick] = useState(false);
  const [active, setActive] = useState(false);
  const colors = {
    0: "red",
    2: "green",
    4: "blue",
    1: "cyan",
    3: "magenta",
    5: "yellow",
    6: "orange",
    7: "hotpink",
  };
  const ncolors = Object.keys(colors).length - 2;
  const [color, setColor] = useState(false);
  const root = document.querySelector(":root");
  const output = document.querySelector("#output");

  const nextColor = (c) => (c >= ncolors ? 0 : (+c + 1) % ncolors);

  // Subscribe this component to the render-loop, to rotate the mesh in each frame.
  useFrame((state, delta) => (meshRef.current.rotation.x += delta));

  /**
   * <p>React {@link https://react.dev/reference/react/useState useState}
   * hook is asynchronous!</p>
   * <p>Basically, you don't get update value right after updating state.</p>
   *
   * The {@link https://react.dev/reference/react/useEffect useEffect}
   * hook executes after the function returns
   * the generated component instance within it,
   * which means that any ref or state will be assigned before
   * the useEffect hook gets called.
   *
   * <p>This code will always use the latest value of clicked,
   * which will be used in the next draw.</p>
   *
   * @function useEffect
   * @global
   *
   * @see {@link https://making.close.com/posts/state-management-with-async-functions The Pitfalls of useState with Asynchronous Functions in React}
   * @see {@link https://dev.to/shareef/react-usestate-hook-is-asynchronous-1hia React useState hook is asynchronous!}
   */
  useEffect(() => {
    const cor = color === false ? ncolors : nextColor(color);
    setColor(cor);
    root.style.setProperty("--txtColor", colors[cor]);
    output.innerHTML = `Clicked (useEffect): ${clicked} <br /> name: ${meshRef.current.name}, color: ${cor} → ${colors[cor]}`;
    console.log(
      `Clicked (useEffect): ${clicked}, name: ${meshRef.current.name}, color: ${cor} → ${colors[cor]}`,
    );
  }, [clicked]);

  return (
    <mesh
      {...props}
      ref={meshRef}
      scale={active ? 1.5 : 1}
      onClick={(event) => {
        const cubeName = event.object.name;
        setActive(!active);
        // either way does work
        if (cubeName === "cube1") {
          setClick(!clicked);
        } else {
          // functional update
          setColor((prevColor) => nextColor(prevColor));
          const cor = nextColor(color);
          root.style.setProperty("--txtColor", colors[cor]);
          output.innerHTML = `Clicked (functional update): ${true} <br\ > name: ${cubeName}, color: ${cor} → ${colors[cor]}`;
        }
      }}
      onPointerOver={(event) => {
        const cubeName = event.object.name;
        setColor(ncolors + 1);
        root.style.setProperty("--txtColor", colors[ncolors + 1]);
        output.innerHTML = `Hovered: ${true} <br \> name: ${cubeName}, color: ${
          ncolors + 1
        } → ${colors[ncolors + 1]}`;
      }}
      onPointerOut={(event) => {
        const cubeName = event.object.name;
        setColor(ncolors);
        root.style.setProperty("--txtColor", colors[ncolors]);
        output.innerHTML = `Hovered: ${false} <br \> name: ${cubeName}, color: ${ncolors} → ${colors[ncolors]}`;
      }}
    >
      <boxGeometry args={[1, 1, 1]} />
      <meshStandardMaterial color={colors[color]} />
    </mesh>
  );
}

/**
 * <p>Returns a {@link https://legacy.reactjs.org/docs/introducing-jsx.html JSX}
 * element with a R3F canvas.</p>
 * In R3F, {@link external:react.useRef useRef()}
 * can be used to encapsulate a reference to an instance
 * of an object, as its current value.<br>
 * This reference can then be passed to a component as a
 * {@link https://react.dev/learn/passing-props-to-a-component prop}.
 * @module
 * @function App
 * @returns {HTMLCanvasElement} R3F {@link external:react-three/fiber Canvas}.
 */
const App = () => {
  return (
    <Canvas camera={{ fov: 35, position: [0, 0, 4] }}>
      <OrbitControls />
      <ambientLight intensity={Math.PI / 2} />
      <spotLight
        position={[10, 10, 10]}
        angle={0.15}
        penumbra={1}
        decay={0}
        intensity={Math.PI}
      />
      <pointLight position={[-10, -10, -10]} decay={0} intensity={Math.PI} />
      <Bounds fit clip margin={1.2} damping={0}>
        <Box position={[-1.2, 0, 0]} name={"cube1"} />
        <Box position={[1.2, 0, 0]} name={"cube2"} />
      </Bounds>
    </Canvas>
  );
};

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