Create an Interactive 3D Web Application

January 30, 2022



Within the browser is the ability to make full cross platform 3d applications using only javascript! The great advantage to using a browser is that the lines of code written can be used in multiple places without catering to platform-specific implementation details, making the final app very portable and accessible by phone or desktop users.

The above boxes were created using three.js, which “is a lightweight cross-browser JavaScript library/API used to create and display animated 3D computer graphics on a Web browser.” To use three.js inside Gatsby and other React applications I used a React renderer called react-three-fiber.

Each box is made from the following code:

JSX

import React, {useEffect, useRef, useState } from "react"
import { useFrame } from "@react-three/fiber"

let hoverCount = 0 // shared hover counter

// Modified from original at https://docs.pmnd.rs/react-three-fiber/getting-started/introduction
const TestThreeBox = props => {
// This reference will give us direct access to the mesh so we can animate it
const mesh = useRef<JSX.IntrinsicElements["mesh"]>()

// Set up state for the hovered and active state
const [hovered, setHover] = useState(false) // Local hover state
const [active, setActive] = useState(false) // Local active (clicked) state
const [time, setTime] = useState(0) //Local animation time
const [color, setColor] = useState(getRandomColor()) //Local cube color
const [c, setC] = useState(props.cycles ? props.cycles : 120) // Number of animation cycles
const activeScale = 2

function getRandomColor() {
let val = Math.random()
let rcolor =
val < 0.75
? val < 0.5
? val < 0.25
? 0x369420
: 0x420369
: "hotpink"
: "skyblue"
return rcolor == color ? getRandomColor() : rcolor
}

let updateColor = (t) => {
if (t > c / 2) {
setColor(getRandomColor())
}
}

let tick = () => {
setTime((prevTime) => (prevTime + 1) % c) //loop every c ticks
let i = (time / c) * 2 * Math.PI //Scale current position in cycle from 0 to 2 pi
let scale =
(active ? activeScale : 1) +
(active ? activeScale : 1) * Math.sin(i) * 0.5
let tmp = mesh.current.scale
tmp["x"] = tmp["y"] = tmp["z"] = scale
}

useFrame(() => {
if (!hovered) {
mesh.current.rotation["x"] = mesh.current.rotation["y"] +=
c/100 * 0.01 * (active ? 0.5 : 2)

// continue until we hit a breakpoint at the beginning or middle of loop
if (time !== 1 && time !== c / 2 + 1) {
tick()
}
} else {
if (!active && time % (c / 8) == 1) updateColor(time)
tick()
}
})

useEffect(() => {
document.body.style.cursor = hoverCount > 0 ? "pointer" : "default"
}, [hoverCount])

return (
<mesh
{...props}
ref={mesh}
scale={active ? [2, 2, 2] : [1, 1, 1]}
onClick={(e) => {
e.stopPropagation()
setActive(!active)
}}
onPointerOver={(e) => {
e.stopPropagation()
hoverCount++
setHover(true)
}}
onPointerOut={(e) => {
hoverCount--
setHover(false)
}}
>
<boxBufferGeometry attach="geometry" args={[1, 1, 1]} />
<meshStandardMaterial
attach="material"
color={hovered || active ? color : "gold"}
/>
</mesh>
)
}

export {TestThreeBox}

The boxes then can be used inside any Canvas component, like so:

JSX

import { TestThreeBox } from "./test-three-box"
import { Canvas } from "@react-three/fiber"

const TestThreeABox = (props) => (
<Canvas>
<pointLight position={[0, -5, 10]} />
<TestThreeBox position={props.boxPos} />
</Canvas>
)

export {TestThreeABox}

The JSX Canvas element can then be inserted into any .mdx file. The code below is what generated the three boxes above:

JSX

import {TestThreeABox} from "../../components/threejs/test-three-canvas"

<div style={{display:'flex'}}>
<TestThreeABox boxPos={[0, 0, 1]}/>
<TestThreeABox boxPos={[0, 0, -1]}/>
<TestThreeABox boxPos={[0, 0, 1]}/>
</div>