/**
* @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 />);