Skip to content

Commit

Permalink
feat: polish visualizer zoom and controls (#929)
Browse files Browse the repository at this point in the history
* feat: polish zoom to fit tangle & limit user controls

* feat: add sinusodial to tangle distances

* chore: add comments for the tangle mesh

* chore: remove unnecessary export

* chore: remove unnecessary files

* fix: initial movement of camera
  • Loading branch information
VmMad authored Jan 9, 2024
1 parent e87ee9f commit f2af7fe
Show file tree
Hide file tree
Showing 8 changed files with 198 additions and 52 deletions.
39 changes: 39 additions & 0 deletions client/src/features/visualizer-threejs/CameraControls.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import { CameraControls as DreiCameraControls } from "@react-three/drei";
import { getCameraAngles } from "./utils";
import React, { useEffect } from "react";
import { useThree } from "@react-three/fiber";
import { CanvasElement } from "./enums";
import { useTangleStore } from "./store";
import { VISUALIZER_PADDINGS } from "./constants";

const CameraControls = () => {
const [shouldLockZoom, setShouldLockZoom] = React.useState<boolean>(false);
const controls = React.useRef<DreiCameraControls>(null);

const CAMERA_ANGLES = getCameraAngles();

const zoom = useTangleStore((s) => s.zoom);
const get = useThree((state) => state.get);
const mesh = get().scene.getObjectByName(CanvasElement.TangleWrapperMesh);

// Set fixed zoom
useEffect(() => {
if (controls.current && shouldLockZoom) {
controls.current.maxZoom = zoom;
controls.current.minZoom = zoom;
}
}, [controls, zoom, shouldLockZoom]);

// Fix to TangleMesh
useEffect(() => {
if (controls.current && mesh) {
controls.current.fitToBox(mesh, false, { ...VISUALIZER_PADDINGS });
controls.current.setOrbitPoint(0, 0, 0);
setShouldLockZoom(true);
}
}, [controls, mesh]);

return <DreiCameraControls ref={controls} makeDefault {...CAMERA_ANGLES} />;
};

export default CameraControls;
54 changes: 32 additions & 22 deletions client/src/features/visualizer-threejs/Emitter.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,11 @@
import { useFrame, useThree } from "@react-three/fiber";
import React, { RefObject, Dispatch, SetStateAction, useEffect, useRef, useState } from "react";
import * as THREE from "three";
import { useBorderPositions } from "./hooks/useBorderPositions";
import { useConfigStore, useTangleStore } from "./store";
import { useRenderTangle } from "./useRenderTangle";
import { EMITTER_DEPTH, EMITTER_HEIGHT, EMITTER_WIDTH, MAX_AMPLITUDE, AMPLITUDE_ACCUMULATOR, HALF_WAVE_PERIOD_SECONDS } from './constants';
import { getNewSinusoidalPosition } from './utils';
import { getTangleDistances, getSinusoidalPosition } from './utils';
import { CanvasElement } from './enums';
import { EMITTER_SPEED_MULTIPLIER, EMITTER_DEPTH, EMITTER_HEIGHT, EMITTER_WIDTH, MAX_SINUSOIDAL_AMPLITUDE, SINUSOIDAL_AMPLITUDE_ACCUMULATOR, HALF_WAVE_PERIOD_SECONDS, INITIAL_SINUSOIDAL_AMPLITUDE } from './constants';

interface EmitterProps {
readonly setRunListeners: Dispatch<SetStateAction<boolean>>;
Expand All @@ -20,11 +20,14 @@ const Emitter: React.FC<EmitterProps> = ({
const setZoom = useTangleStore(s => s.setZoom);
const get = useThree(state => state.get);
const currentZoom = useThree(state => state.camera.zoom);
const { halfScreenWidth } = useBorderPositions();
const groupRef = useRef<THREE.Group>(null);
const camera = get().camera;

const { xTangleDistance, yTangleDistance } = getTangleDistances()
const isPlaying = useConfigStore(state => state.isPlaying);

const [animationTime, setAnimationTime] = useState<number>(0)
const [currentAmplitude, setCurrentAmplitude] = useState<number>(AMPLITUDE_ACCUMULATOR);
const [currentAmplitude, setCurrentAmplitude] = useState<number>(INITIAL_SINUSOIDAL_AMPLITUDE);

const previousRealTime = useRef<number>(0);
const previousPeakTime = useRef<number>(0);
Expand All @@ -40,11 +43,8 @@ const Emitter: React.FC<EmitterProps> = ({
}, [emitterRef]);

useFrame(() => {
const camera = get().camera;
const emitterObj = get().scene.getObjectByName("emitter");
if (camera && emitterObj) {
const EMITTER_PADDING_RIGHT = 150;
camera.position.x = emitterObj.position.x - halfScreenWidth + EMITTER_PADDING_RIGHT;
if (camera && groupRef.current) {
camera.position.x = groupRef.current.position.x;
}
});

Expand All @@ -57,7 +57,7 @@ const Emitter: React.FC<EmitterProps> = ({
const lastPeakHalfWaveCount = Math.floor(previousPeakTime.current / HALF_WAVE_PERIOD_SECONDS);

if (currentHalfWaveCount > lastPeakHalfWaveCount) {
setCurrentAmplitude(prev => Math.min(prev + AMPLITUDE_ACCUMULATOR, MAX_AMPLITUDE));
setCurrentAmplitude(prev => Math.min(prev + SINUSOIDAL_AMPLITUDE_ACCUMULATOR, MAX_SINUSOIDAL_AMPLITUDE));
previousPeakTime.current = animationTime;
}
}
Expand All @@ -66,24 +66,25 @@ const Emitter: React.FC<EmitterProps> = ({
* Emitter shift
*/
useFrame(({ clock }, delta) => {
const DELTA_MULTIPLIER = 80; // depends on this param we can manage speed of emitter

const currentRealTime = clock.getElapsedTime();
const realTimeDelta = currentRealTime - previousRealTime.current;
previousRealTime.current = currentRealTime;

if (isPlaying) {
updateAnimationTime(realTimeDelta);
checkAndHandleNewPeak();


if (groupRef.current) {
const { x } = groupRef.current.position;

const newXPos = x + (delta * EMITTER_SPEED_MULTIPLIER);

groupRef.current.position.x = newXPos;
}

if (emitterRef.current) {
const { x } = emitterRef.current.position;

const newXPos = x + (delta * DELTA_MULTIPLIER);
const newYPos = getNewSinusoidalPosition(animationTime, currentAmplitude);

const newYPos = getSinusoidalPosition(animationTime, currentAmplitude);
emitterRef.current.position.y = newYPos;
emitterRef.current.position.x = newXPos;
}
}
});
Expand All @@ -92,14 +93,23 @@ const Emitter: React.FC<EmitterProps> = ({
useRenderTangle();

return (
<group ref={groupRef}>
{/* TangleWrapper Mesh */}
<mesh name={CanvasElement.TangleWrapperMesh} position={[-(xTangleDistance / 2), 0, 0]}>
<boxGeometry args={[xTangleDistance, yTangleDistance, 0]} attach="geometry" />
<meshPhongMaterial opacity={0} wireframe={true} transparent attach="material" />
</mesh>

{/* Emitter Mesh */}
<mesh
ref={emitterRef}
name="emitter"
name={CanvasElement.EmitterMesh}
position={[0, 0, 0]}
>
<boxGeometry args={[EMITTER_WIDTH, EMITTER_HEIGHT, EMITTER_DEPTH]} />
<meshPhongMaterial transparent={true} opacity={0.6} />
</mesh>
</group>
);
};
export default Emitter;
34 changes: 18 additions & 16 deletions client/src/features/visualizer-threejs/VisualizerInstance.tsx
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
/* eslint-disable react/no-unknown-property */
import { CameraControls, OrthographicCamera } from "@react-three/drei";
import { Center } from "@react-three/drei";
import { Canvas } from "@react-three/fiber";
import { Perf } from "r3f-perf";
import React, { useEffect, useRef } from "react";
import { RouteComponentProps } from "react-router-dom";
import * as THREE from "three";
import { Box3 } from "three";
import { ACCEPTED_BLOCK_COLORS, DIRECTIONAL_LIGHT_INTENSITY, PENDING_BLOCK_COLOR, TIME_DIFF_COUNTER, VISUALIZER_BACKGROUND, ZOOM_DEFAULT } from "./constants";
import { ACCEPTED_BLOCK_COLORS, DIRECTIONAL_LIGHT_INTENSITY, FAR_PLANE, NEAR_PLANE, PENDING_BLOCK_COLOR, TIME_DIFF_COUNTER, VISUALIZER_BACKGROUND } from "./constants";
import Emitter from "./Emitter";
import { useTangleStore, useConfigStore } from "./store";
import { getGenerateY, randomIntFromInterval, timer } from "./utils";
Expand All @@ -19,8 +19,10 @@ import { NovaFeedClient } from "../../services/nova/novaFeedClient";
import { Wrapper } from "./wrapper/Wrapper";
import "./Visualizer.scss";
import { IFeedBlockMetadata } from "~/models/api/stardust/feed/IFeedBlockMetadata";
import { CanvasElement } from './enums';
import { useGetThemeMode } from '~/helpers/hooks/useGetThemeMode';
import { StardustFeedClient } from "~/services/stardust/stardustFeedClient";
import CameraControls from './CameraControls';

const features = {
statsEnabled: true,
Expand Down Expand Up @@ -226,27 +228,27 @@ const VisualizerInstance: React.FC<RouteComponentProps<VisualizerRouteProps>> =
isEdgeRenderingEnabled={isEdgeRenderingEnabled}
setEdgeRenderingEnabled={checked => setEdgeRenderingEnabled(checked)}
>
<Canvas ref={canvasRef}>
<OrthographicCamera
name="mainCamera"
makeDefault
near={1}
far={4000}
position={[0, 0, 1500]}
zoom={ZOOM_DEFAULT}
/>
<Canvas ref={canvasRef} orthographic camera={{
name: CanvasElement.MainCamera,
near: NEAR_PLANE,
far: FAR_PLANE,
position: [0, 0, 9000],
}}>
<color attach="background" args={[VISUALIZER_BACKGROUND[themeMode]]} />
<ambientLight />
<directionalLight position={[400, 700, 920]} intensity={DIRECTIONAL_LIGHT_INTENSITY} />
<Emitter
emitterRef={emitterRef}
setRunListeners={setRunListeners}
/>
{features.cameraControls && <CameraControls makeDefault />}
<Center>
<Emitter
emitterRef={emitterRef}
setRunListeners={setRunListeners}
/>
</Center>
{features.cameraControls && <CameraControls />}
{features.statsEnabled && <Perf />}
</Canvas>
</Wrapper>
);
};

export default VisualizerInstance;

35 changes: 31 additions & 4 deletions client/src/features/visualizer-threejs/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,8 +40,35 @@ export const COLORS = [
...ACCEPTED_BLOCK_COLORS,
]

// visualizer
// emitter
export const EMITTER_SPEED_MULTIPLIER = 80
export const EMITTER_PADDING_RIGHT = 150
export const VISUALIZER_SAFE_ZONE = 150

// camera
export const CAMERA_X_AXIS_MOVEMENT = 0.025
export const CAMERA_Y_AXIS_MOVEMENT = 0.035
export const CAMERA_X_OFFSET = 0
export const CAMERA_Y_OFFSET = 0.5

export const FAR_PLANE = 15000
export const NEAR_PLANE = 1

export const VISUALIZER_PADDINGS = {
paddingLeft: VISUALIZER_SAFE_ZONE,
paddingRight: VISUALIZER_SAFE_ZONE,
paddingBottom: VISUALIZER_SAFE_ZONE,
paddingTop: VISUALIZER_SAFE_ZONE,
}

// general
export const MIN_BLOCKS_PER_SECOND = 50
export const MAX_BLOCKS_PER_SECOND = 200

// time
export const MILLISECONDS_PER_SECOND = 1000

// visualizer
export const DIRECTIONAL_LIGHT_INTENSITY = 0.45;

export const VISUALIZER_BACKGROUND: Record<ThemeMode, string> = {
Expand All @@ -50,11 +77,11 @@ export const VISUALIZER_BACKGROUND: Record<ThemeMode, string> = {
}

// emitter

export const EMITTER_WIDTH = 30;
export const EMITTER_HEIGHT = 250;
export const EMITTER_DEPTH = 250;

export const MAX_AMPLITUDE = 200;
export const AMPLITUDE_ACCUMULATOR = 10;
export const MAX_SINUSOIDAL_AMPLITUDE = 200;
export const SINUSOIDAL_AMPLITUDE_ACCUMULATOR = 10;
export const INITIAL_SINUSOIDAL_AMPLITUDE = 50;
export const HALF_WAVE_PERIOD_SECONDS = 4;
6 changes: 6 additions & 0 deletions client/src/features/visualizer-threejs/enums.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,9 @@
export enum CanvasElement {
TangleWrapperMesh = 'TangleWrapperMesh',
EmitterMesh = 'EmmiterMesh',
MainCamera = 'MainCamera',
}

export enum ThemeMode {
Light = "light",
Dark = "dark"
Expand Down
6 changes: 6 additions & 0 deletions client/src/features/visualizer-threejs/interfaces.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
export interface ICameraAngles {
minAzimuthAngle: number
minPolarAngle: number
maxPolarAngle: number
maxAzimuthAngle: number
}
2 changes: 2 additions & 0 deletions client/src/features/visualizer-threejs/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,3 +6,5 @@ export type TFeedBlockAdd = (newBlock: IFeedBlockData) => void;
export type TFeedBlockMetadataUpdate = (metadataUpdate: {
[id: string]: IFeedBlockMetadata;
}) => void;

export type TangleMeshType = THREE.Mesh<THREE.PlaneGeometry, THREE.MeshBasicMaterial, THREE.Object3DEventMap>
Loading

0 comments on commit f2af7fe

Please sign in to comment.