Skip to content

Commit

Permalink
feat(exercise): add exercise with boxes
Browse files Browse the repository at this point in the history
  • Loading branch information
cxspxr committed Dec 15, 2020
1 parent 9cab7b3 commit 16e654d
Show file tree
Hide file tree
Showing 10 changed files with 255 additions and 20 deletions.
6 changes: 6 additions & 0 deletions src/components/BoxesExercise/BoxesExercise.module.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
.boxes_exercise {
&__container {
width: 100vw;
height: 100vh;
}
}
167 changes: 167 additions & 0 deletions src/components/BoxesExercise/BoxesExercise.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,167 @@
/**
* Have to thank https://github.com/pmndrs/react-three-fiber for this exercise
* as it was able to adapt the showcase to our need
*/

import React, { useEffect, useMemo, useRef } from "react";
import { Box } from "@material-ui/core";
import * as THREE from "three";
import { Canvas, useFrame } from "react-three-fiber";
import exerciseStyles from "./BoxesExercise.module.scss";
import useGlobalColors from "../../hooks/useGlobalColors/useGlobalColors";
import { RGBColor } from "../../contexts/SettingsContext/SettingsContext";

/**
* Number of geometry boxes to be created
*/
export const number = 10;

/**
* Utility threejs object
*/
const tempObject = new THREE.Object3D();

/**
* Utility threejs color
*/
const tempColor = new THREE.Color();

/**
* Gets random positional arguments for threejs box
* @param i - factor number to be used in random computations
*/
export const getRandomPositionalArgs = (i: number) => {
const r = Math.random();
return {
position: [100 - Math.random() * 200, 100 - Math.random() * 200, i * 1.5],
scale: [5 + r * 5, 5 + r * 5, 1],
// eslint-disable-next-line @typescript-eslint/ban-ts-ignore
// @ts-ignore
rotation: [0, 0, THREE.Math.degToRad(Math.round(Math.random()) * 180)],
};
};

/**
* Gets random color in an array of 2 rgb strings
* @param colors - array of 2 rgb strings
*/
export const getRandomColor = (colors: string[]) => ({
color: colors[Math.round(Math.random())],
});

/**
* BoxesExercise content component props type
*/
type ContentProps = {
colors: [string, string];
};

/**
* BoxesExercise content component
* @param colors - a set of 2 rgb strings to be used as colors for threejs shapes
*/
export const Content: React.FC<ContentProps> = ({ colors: [leftLenseColor, rightLenseColor] }) => {
const thousandColors = useMemo(
() =>
new Array(1000)
.fill(null)
.map(() => [leftLenseColor, rightLenseColor][Math.floor(Math.random() * 2)]),
[leftLenseColor, rightLenseColor],
);
const colorArray = useMemo(
() =>
Float32Array.from(
new Array(1000).fill(null).flatMap((_, i) => tempColor.set(thousandColors[i]).toArray()),
),
// eslint-disable-next-line react-hooks/exhaustive-deps
[],
);

const ref = useRef<any>();

useFrame((state) => {
const time = state.clock.getElapsedTime();
ref.current!.rotation.x = Math.sin(time / 4);
ref.current!.rotation.y = Math.sin(time / 2);
let i = 0;
for (let x = 0; x < 10; x += 1)
for (let y = 0; y < 10; y += 1)
for (let z = 0; z < 10; z += 1) {
i += 1;
const id = i;
tempObject.position.set(5 - x, 5 - y, 5 - z);
tempObject.rotation.y =
Math.sin(x / 4 + time) + Math.sin(y / 4 + time) + Math.sin(z / 4 + time);
tempObject.rotation.z = tempObject.rotation.y * 2;
tempObject.scale.set(1, 1, 1);
tempObject.updateMatrix();
ref.current!.setMatrixAt(id, tempObject.matrix);
}
ref.current!.instanceMatrix.needsUpdate = true;
});

return (
<instancedMesh ref={ref} args={[null as any, null as any, 1000]}>
<boxBufferGeometry attach="geometry" args={[0.7, 0.7, 0.7]}>
<instancedBufferAttribute attachObject={["attributes", "color"]} args={[colorArray, 3]} />
</boxBufferGeometry>
<meshPhongMaterial attach="material" vertexColors={THREE.VertexColors as any} />
</instancedMesh>
);
};

/**
* BoxesExercise threejs lights component
*/
export const Lights: React.FC = () => {
return (
<group>
<ambientLight />
<pointLight position={[150, 150, 150]} intensity={0.55} />
</group>
);
};

/**
* BoxesExercise component props type
*/
export type BoxesExerciseProps = {
colors: [RGBColor, RGBColor];
};

/**
* BoxesExercise component
* @param leftLense - RGBColor object of left lense color
* @param rightLense - RGBColor object of right lense color
*/
const BoxesExercise: React.FC<BoxesExerciseProps> = ({ colors: [leftLense, rightLense] }) => {
const [
[, setGlobalBackground, resetGlobalBackground],
[, setGlobalColor, resetGlobalColor],
] = useGlobalColors();

const rightLenseRGBString = `rgb(${rightLense.r}, ${rightLense.g}, ${rightLense.b})`;
const leftLenseRGBString = `rgb(${leftLense.r}, ${leftLense.g}, ${leftLense.b})`;

useEffect(() => {
setGlobalColor({ r: 255, g: 255, b: 255 });
setGlobalBackground({ r: 0, g: 0, b: 0 });

return () => {
resetGlobalBackground();
resetGlobalColor();
};
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);

return (
<Box className={exerciseStyles.boxes_exercise__container}>
<Canvas shadowMap camera={{ position: [0, 0, 15], fov: 100, near: 5, far: 20 }}>
<Lights />
<Content colors={[leftLenseRGBString, rightLenseRGBString]} />
</Canvas>
</Box>
);
};

export default BoxesExercise;
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,13 @@ import React from "react";
import { mount, ReactWrapper } from "enzyme";
import { Box } from "@material-ui/core";
import { Canvas } from "react-three-fiber";
import Exercise, { ExerciseProps } from "../Exercise";
import BoxesExercise, { BoxesExerciseProps } from "../BoxesExercise";
import useGlobalColors from "../../../hooks/useGlobalColors/useGlobalColors";

jest.mock("../../../hooks/useGlobalColors/useGlobalColors");

describe("Exercise component", () => {
const defaultProps: ExerciseProps = {
describe("BoxesExercise component", () => {
const defaultProps: BoxesExerciseProps = {
colors: [
{ r: 255, g: 0, b: 0 },
{ r: 0, g: 255, b: 255 },
Expand All @@ -21,7 +21,7 @@ describe("Exercise component", () => {
beforeEach(() => {
jest.spyOn(React, "useEffect");
// eslint-disable-next-line react/jsx-props-no-spreading
wrapper = mount(<Exercise {...defaultProps} />);
wrapper = mount(<BoxesExercise {...defaultProps} />);
});

afterEach(() => {
Expand Down
3 changes: 3 additions & 0 deletions src/components/BoxesExercise/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"main": "BoxesExercise.tsx"
}
3 changes: 0 additions & 3 deletions src/components/Exercise/package.json

This file was deleted.

Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
.exercise {
.shuffle_exercise {
&__navigation {
&__toggle {
cursor: pointer;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import { Box } from "@material-ui/core";
import * as THREE from "three";
import { useSprings, a } from "@react-spring/three";
import { Canvas } from "react-three-fiber";
import exerciseStyles from "./Exercise.module.scss";
import exerciseStyles from "./ShuffleExercise.module.scss";
import useGlobalColors from "../../hooks/useGlobalColors/useGlobalColors";
import { RGBColor } from "../../contexts/SettingsContext/SettingsContext";

Expand Down Expand Up @@ -41,14 +41,14 @@ export const getRandomColor = (colors: string[]) => ({
});

/**
* Exercise content component props type
* ShuffleExercise content component props type
*/
type ContentProps = {
colors: [string, string];
};

/**
* Exercise content component
* ShuffleExercise content component
* @param colors - a set of 2 rgb strings to be used as colors for threejs shapes
*/
export const Content: React.FC<ContentProps> = ({ colors }) => {
Expand Down Expand Up @@ -104,7 +104,7 @@ export const Content: React.FC<ContentProps> = ({ colors }) => {
};

/**
* Exercise threejs lights component
* ShuffleExercise threejs lights component
*/
export const Lights: React.FC = () => {
return (
Expand All @@ -125,18 +125,18 @@ export const Lights: React.FC = () => {
};

/**
* Exercise component props type
* ShuffleExercise component props type
*/
export type ExerciseProps = {
export type ShuffleExerciseProps = {
colors: [RGBColor, RGBColor];
};

/**
* Exercise component
* ShuffleExercise component
* @param leftLense - RGBColor object of left lense color
* @param rightLense - RGBColor object of right lense color
*/
const Exercise: React.FC<ExerciseProps> = ({ colors: [leftLense, rightLense] }) => {
const ShuffleExercise: React.FC<ShuffleExerciseProps> = ({ colors: [leftLense, rightLense] }) => {
const [
[, setGlobalBackground, resetGlobalBackground],
[, setGlobalColor, resetGlobalColor],
Expand All @@ -157,7 +157,7 @@ const Exercise: React.FC<ExerciseProps> = ({ colors: [leftLense, rightLense] })
}, []);

return (
<Box className={exerciseStyles.exercise__container}>
<Box className={exerciseStyles.shuffle_exercise__container}>
<Canvas shadowMap camera={{ position: [0, 0, 100], fov: 100 }}>
<Lights />
<Content colors={[leftLenseRGBString, rightLenseRGBString]} />
Expand All @@ -166,4 +166,4 @@ const Exercise: React.FC<ExerciseProps> = ({ colors: [leftLense, rightLense] })
);
};

export default Exercise;
export default ShuffleExercise;
53 changes: 53 additions & 0 deletions src/components/ShuffleExercise/__tests__/ShuffleExercise.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
import React from "react";
import { mount, ReactWrapper } from "enzyme";
import { Box } from "@material-ui/core";
import { Canvas } from "react-three-fiber";
import ShuffleExercise, { ShuffleExerciseProps } from "../ShuffleExercise";
import useGlobalColors from "../../../hooks/useGlobalColors/useGlobalColors";

jest.mock("../../../hooks/useGlobalColors/useGlobalColors");

describe("ShuffleExercise component", () => {
const defaultProps: ShuffleExerciseProps = {
colors: [
{ r: 255, g: 0, b: 0 },
{ r: 0, g: 255, b: 255 },
],
};

describe("implementation", () => {
let wrapper: ReactWrapper;

beforeEach(() => {
jest.spyOn(React, "useEffect");
// eslint-disable-next-line react/jsx-props-no-spreading
wrapper = mount(<ShuffleExercise {...defaultProps} />);
});

afterEach(() => {
jest.clearAllMocks();
});

it("uses global colors helpers", () => {
expect(useGlobalColors).toBeCalledTimes(1);
});

it("uses side effects", () => {
expect(React.useEffect).toBeCalled();
});

it("renders an exercise container", () => {
expect(wrapper.find(Box).length).toEqual(1);
});

it("renders a three fiber canvas", () => {
expect(wrapper.find(Canvas).length).toEqual(1);
});
});

describe("user behavior", () => {
it("is not possible to describe user behavior since exercise is not yet interactive", () => {
expect(true).toEqual(true);
});
});
});
3 changes: 3 additions & 0 deletions src/components/ShuffleExercise/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"main": "ShuffleExercise.tsx"
}
10 changes: 8 additions & 2 deletions src/hooks/useNavigation/useNavigation.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,9 @@ import introLevels from "../../components/IntroLevel/introLevels";
// eslint-disable-next-line import/no-cycle
import ColorSettingsLevel from "../../components/ColorSettingsLevel/ColorSettingsLevel";
// eslint-disable-next-line import/no-cycle
import Exercise from "../../components/Exercise/Exercise";
import ShuffleExercise from "../../components/ShuffleExercise/ShuffleExercise";
// eslint-disable-next-line import/no-cycle
import BoxesExercise from "../../components/BoxesExercise/BoxesExercise";
// eslint-disable-next-line import/no-cycle
import IntroLevel from "../../components/IntroLevel/IntroLevel";

Expand Down Expand Up @@ -93,7 +95,11 @@ const useNavigation = (): UseNavigationReturn => {
},
{
type: "ExerciseLevel",
component: <Exercise colors={[leftLense, rightLense]} />,
component: <ShuffleExercise colors={[leftLense, rightLense]} />,
},
{
type: "ExerciseLevel",
component: <BoxesExercise colors={[leftLense, rightLense]} />,
},
];

Expand Down

0 comments on commit 16e654d

Please sign in to comment.