From f2af7feedcf267484b14567d69653f8e8f5af67e Mon Sep 17 00:00:00 2001 From: JCNoguera <88061365+VmMad@users.noreply.github.com> Date: Tue, 9 Jan 2024 18:22:16 +0100 Subject: [PATCH] feat: polish visualizer zoom and controls (#929) * 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 --- .../visualizer-threejs/CameraControls.tsx | 39 ++++++++++ .../features/visualizer-threejs/Emitter.tsx | 54 ++++++++------ .../visualizer-threejs/VisualizerInstance.tsx | 34 +++++---- .../features/visualizer-threejs/constants.ts | 35 ++++++++- .../src/features/visualizer-threejs/enums.ts | 6 ++ .../features/visualizer-threejs/interfaces.ts | 6 ++ .../src/features/visualizer-threejs/types.ts | 2 + .../src/features/visualizer-threejs/utils.ts | 74 ++++++++++++++++--- 8 files changed, 198 insertions(+), 52 deletions(-) create mode 100644 client/src/features/visualizer-threejs/CameraControls.tsx create mode 100644 client/src/features/visualizer-threejs/interfaces.ts diff --git a/client/src/features/visualizer-threejs/CameraControls.tsx b/client/src/features/visualizer-threejs/CameraControls.tsx new file mode 100644 index 000000000..2419d39dc --- /dev/null +++ b/client/src/features/visualizer-threejs/CameraControls.tsx @@ -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(false); + const controls = React.useRef(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 ; +}; + +export default CameraControls; diff --git a/client/src/features/visualizer-threejs/Emitter.tsx b/client/src/features/visualizer-threejs/Emitter.tsx index ab857d004..8ddca7587 100644 --- a/client/src/features/visualizer-threejs/Emitter.tsx +++ b/client/src/features/visualizer-threejs/Emitter.tsx @@ -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>; @@ -20,11 +20,14 @@ const Emitter: React.FC = ({ 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(null); + const camera = get().camera; + + const { xTangleDistance, yTangleDistance } = getTangleDistances() const isPlaying = useConfigStore(state => state.isPlaying); const [animationTime, setAnimationTime] = useState(0) - const [currentAmplitude, setCurrentAmplitude] = useState(AMPLITUDE_ACCUMULATOR); + const [currentAmplitude, setCurrentAmplitude] = useState(INITIAL_SINUSOIDAL_AMPLITUDE); const previousRealTime = useRef(0); const previousPeakTime = useRef(0); @@ -40,11 +43,8 @@ const Emitter: React.FC = ({ }, [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; } }); @@ -57,7 +57,7 @@ const Emitter: React.FC = ({ 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; } } @@ -66,24 +66,25 @@ const Emitter: React.FC = ({ * 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; } } }); @@ -92,14 +93,23 @@ const Emitter: React.FC = ({ useRenderTangle(); return ( + + {/* TangleWrapper Mesh */} + + + + + + {/* Emitter Mesh */} + ); }; export default Emitter; diff --git a/client/src/features/visualizer-threejs/VisualizerInstance.tsx b/client/src/features/visualizer-threejs/VisualizerInstance.tsx index 398360846..9d87d9f7c 100644 --- a/client/src/features/visualizer-threejs/VisualizerInstance.tsx +++ b/client/src/features/visualizer-threejs/VisualizerInstance.tsx @@ -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"; @@ -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, @@ -226,23 +228,22 @@ const VisualizerInstance: React.FC> = isEdgeRenderingEnabled={isEdgeRenderingEnabled} setEdgeRenderingEnabled={checked => setEdgeRenderingEnabled(checked)} > - - + - - {features.cameraControls && } +
+ +
+ {features.cameraControls && } {features.statsEnabled && }
@@ -250,3 +251,4 @@ const VisualizerInstance: React.FC> = }; export default VisualizerInstance; + diff --git a/client/src/features/visualizer-threejs/constants.ts b/client/src/features/visualizer-threejs/constants.ts index 8d39d8644..4e90f9d6a 100644 --- a/client/src/features/visualizer-threejs/constants.ts +++ b/client/src/features/visualizer-threejs/constants.ts @@ -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 = { @@ -50,11 +77,11 @@ export const VISUALIZER_BACKGROUND: Record = { } // 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; diff --git a/client/src/features/visualizer-threejs/enums.ts b/client/src/features/visualizer-threejs/enums.ts index 360b12a6a..7eae791ef 100644 --- a/client/src/features/visualizer-threejs/enums.ts +++ b/client/src/features/visualizer-threejs/enums.ts @@ -1,3 +1,9 @@ +export enum CanvasElement { + TangleWrapperMesh = 'TangleWrapperMesh', + EmitterMesh = 'EmmiterMesh', + MainCamera = 'MainCamera', +} + export enum ThemeMode { Light = "light", Dark = "dark" diff --git a/client/src/features/visualizer-threejs/interfaces.ts b/client/src/features/visualizer-threejs/interfaces.ts new file mode 100644 index 000000000..96bcc5a71 --- /dev/null +++ b/client/src/features/visualizer-threejs/interfaces.ts @@ -0,0 +1,6 @@ +export interface ICameraAngles { + minAzimuthAngle: number + minPolarAngle: number + maxPolarAngle: number + maxAzimuthAngle: number +} diff --git a/client/src/features/visualizer-threejs/types.ts b/client/src/features/visualizer-threejs/types.ts index 2e8fa4a16..a3758fe73 100644 --- a/client/src/features/visualizer-threejs/types.ts +++ b/client/src/features/visualizer-threejs/types.ts @@ -6,3 +6,5 @@ export type TFeedBlockAdd = (newBlock: IFeedBlockData) => void; export type TFeedBlockMetadataUpdate = (metadataUpdate: { [id: string]: IFeedBlockMetadata; }) => void; + +export type TangleMeshType = THREE.Mesh diff --git a/client/src/features/visualizer-threejs/utils.ts b/client/src/features/visualizer-threejs/utils.ts index 3c38cda55..288fcc2ea 100644 --- a/client/src/features/visualizer-threejs/utils.ts +++ b/client/src/features/visualizer-threejs/utils.ts @@ -1,4 +1,5 @@ -import { STEP_Y_PX, TIME_DIFF_COUNTER, SECOND, HALF_WAVE_PERIOD_SECONDS } from "./constants"; +import { MAX_BLOCKS_PER_SECOND, MAX_BLOCK_INSTANCES, EMITTER_SPEED_MULTIPLIER, MIN_BLOCKS_PER_SECOND, CAMERA_X_AXIS_MOVEMENT, CAMERA_Y_AXIS_MOVEMENT, CAMERA_X_OFFSET, CAMERA_Y_OFFSET, HALF_WAVE_PERIOD_SECONDS, MAX_SINUSOIDAL_AMPLITUDE, SECOND, TIME_DIFF_COUNTER, STEP_Y_PX } from "./constants"; +import { ICameraAngles } from './interfaces'; /** * Generates a random number within a specified range. @@ -79,6 +80,7 @@ const getMaxYPosition = (bps: number) => { maxYPerTick: maxYPerTick > 0 ? maxYPerTick : 1 }; }; + const checkRules = (y: number, prev: number[]) => { let passAllChecks = true; if (prev.length === 0) { @@ -110,8 +112,7 @@ export const getGenerateY = ({ withRandom }: {withRandom?: boolean} = {}): (shif const generator = yCoordinateGenerator(); const prevY: number[] = []; const limitPrevY = 5; - const LIMIT_BPS = 48; - const { maxYPerTick: defaultMaxYPerTick } = getMaxYPosition(LIMIT_BPS); + const { maxYPerTick: defaultMaxYPerTick } = getMaxYPosition(MAX_BLOCKS_PER_SECOND); return (shift: number, bps: number) => { shift += 1; // This hack needs to avoid Y = 0 on the start of graph. @@ -122,7 +123,7 @@ export const getGenerateY = ({ withRandom }: {withRandom?: boolean} = {}): (shif currentShift = shift; } - if (bps < LIMIT_BPS) { + if (bps < MAX_BLOCKS_PER_SECOND) { let randomY = randomNumberFromInterval(-defaultMaxYPerTick, defaultMaxYPerTick); // check if not match with last value (and not near); @@ -148,12 +149,65 @@ export const getGenerateY = ({ withRandom }: {withRandom?: boolean} = {}): (shif }; }; -export function getNewSinusoidalPosition(time: number, amplitude: number): number { - const period = HALF_WAVE_PERIOD_SECONDS * 2; - const frequency = 1 / period; - const phase = (time % period) * frequency +/** + * Calculate the tangles distances + * @returns The axis distances + */ +export function getTangleDistances(): { + xTangleDistance: number; + yTangleDistance: number; +} { + /* We assume MAX BPS to get the max possible Y */ + const { maxYPerTick } = getMaxYPosition(MAX_BLOCKS_PER_SECOND); + + const MAX_TANGLE_DISTANCE_SECONDS = MAX_BLOCK_INSTANCES / MIN_BLOCKS_PER_SECOND; + + const MAX_BLOCK_DISTANCE = EMITTER_SPEED_MULTIPLIER * MAX_TANGLE_DISTANCE_SECONDS; + + const maxXDistance = MAX_BLOCK_DISTANCE + + /* Max Y Distance will be multiplied by 2 to position blocks in the negative and positive Y axis */ + const maxYDistance = (maxYPerTick * 2) + (MAX_SINUSOIDAL_AMPLITUDE * 2) + + /* TODO: add sinusoidal distances */ + + return { + xTangleDistance: maxXDistance, + yTangleDistance: maxYDistance + } + } - const newY = amplitude * Math.sin(phase * 2 * Math.PI); +export function getCameraAngles(): ICameraAngles { + const xAngle = Math.PI * CAMERA_X_AXIS_MOVEMENT + const yAngle = Math.PI * CAMERA_Y_AXIS_MOVEMENT - return newY; + const startingXAngle = Math.PI * CAMERA_X_OFFSET + const startingYAngle = Math.PI * CAMERA_Y_OFFSET + + // Divided by the two directions, positive and negative + const X_MOVEMENT = xAngle / 2 + const Y_MOVEMENT = yAngle / 2 + + const MIN_HORIZONTAL_ANGLE = startingXAngle - X_MOVEMENT + const MIN_VERTICAL_ANGLE = startingYAngle - Y_MOVEMENT + + const MAX_HORIZONTAL_ANGLE = startingXAngle + X_MOVEMENT + const MAX_VENTICAL_ANGLE = startingYAngle + Y_MOVEMENT + + return { + minAzimuthAngle: MIN_HORIZONTAL_ANGLE, + minPolarAngle: MIN_VERTICAL_ANGLE, + maxPolarAngle: MAX_VENTICAL_ANGLE, + maxAzimuthAngle: MAX_HORIZONTAL_ANGLE + } } + + export function getSinusoidalPosition(time: number, amplitude: number): number { + const period = HALF_WAVE_PERIOD_SECONDS * 2; + const frequency = 1 / period; + const phase = (time % period) * frequency + + const newY = amplitude * Math.sin(phase * 2 * Math.PI); + + return newY; + }