From 4c3af8cf08143926df54cc49427a1a3214617e4e Mon Sep 17 00:00:00 2001 From: JCNoguera <88061365+VmMad@users.noreply.github.com> Date: Wed, 21 Feb 2024 11:34:53 +0100 Subject: [PATCH] fix: resumed visualizer not plotting correctly data while it was paused (#1140) * feat: handle pausing visualizer * refactor: improve play/pause feature * chore: remove unused useFrame and improve initialTime comment --- .../features/visualizer-threejs/Emitter.tsx | 82 +++------ .../visualizer-threejs/VisualizerInstance.tsx | 35 ++-- .../visualizer-threejs/blockPositions.ts | 31 ++++ .../features/visualizer-threejs/interfaces.ts | 6 + .../visualizer-threejs/store/config.ts | 29 ++++ .../visualizer-threejs/store/tangle.ts | 45 ++--- .../visualizer-threejs/useRenderTangle.tsx | 155 +++++++++--------- .../src/features/visualizer-threejs/utils.ts | 65 ++++++-- .../visualizer-threejs/wrapper/Wrapper.tsx | 1 - .../helpers/nova/hooks/useVisualizerTimer.ts | 16 ++ 10 files changed, 274 insertions(+), 191 deletions(-) create mode 100644 client/src/features/visualizer-threejs/blockPositions.ts create mode 100644 client/src/helpers/nova/hooks/useVisualizerTimer.ts diff --git a/client/src/features/visualizer-threejs/Emitter.tsx b/client/src/features/visualizer-threejs/Emitter.tsx index 4c5925565..767dfee56 100644 --- a/client/src/features/visualizer-threejs/Emitter.tsx +++ b/client/src/features/visualizer-threejs/Emitter.tsx @@ -4,40 +4,31 @@ import React, { RefObject, Dispatch, SetStateAction, useEffect, useRef } from "r import * as THREE from "three"; import { useConfigStore, useTangleStore } from "./store"; import { useRenderTangle } from "./useRenderTangle"; -import { getTangleDistances, getSinusoidalPosition } from "./utils"; +import { getTangleDistances, getEmitterPositions } 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"; +import useVisualizerTimer from "~/helpers/nova/hooks/useVisualizerTimer"; +import { EMITTER_DEPTH, EMITTER_HEIGHT, EMITTER_WIDTH } from "./constants"; interface EmitterProps { readonly setRunListeners: Dispatch>; readonly emitterRef: RefObject; } +const { xTangleDistance, yTangleDistance } = getTangleDistances(); + const Emitter: React.FC = ({ setRunListeners, emitterRef }: EmitterProps) => { + const getVisualizerTimeDiff = useVisualizerTimer(); + const setZoom = useTangleStore((s) => s.setZoom); const get = useThree((state) => state.get); const currentZoom = useThree((state) => state.camera.zoom); - const groupRef = useRef(null); const camera = get().camera; - const { xTangleDistance, yTangleDistance } = getTangleDistances(); const isPlaying = useConfigStore((state) => state.isPlaying); const setIsPlaying = useConfigStore((state) => state.setIsPlaying); + const setInitialTime = useConfigStore((state) => state.setInitialTime); - const animationTime = useRef(0); - const currentAmplitude = useRef(INITIAL_SINUSOIDAL_AMPLITUDE); - - const previousRealTime = useRef(0); - const previousPeakTime = useRef(0); + const tangleWrapperRef = useRef(null); useEffect(() => { setZoom(currentZoom); @@ -47,6 +38,7 @@ const Emitter: React.FC = ({ setRunListeners, emitterRef }: Emitte if (emitterRef?.current) { setIsPlaying(true); setRunListeners(true); + setInitialTime(Date.now()); } return () => { @@ -55,60 +47,38 @@ const Emitter: React.FC = ({ setRunListeners, emitterRef }: Emitte }; }, [emitterRef?.current]); - useFrame(() => { - if (camera && groupRef.current) { - camera.position.x = groupRef.current.position.x; - } - }); - - function updateAnimationTime(realTimeDelta: number): void { - animationTime.current += realTimeDelta; - } - - function checkAndHandleNewPeak(): void { - const currentHalfWaveCount = Math.floor(animationTime.current / HALF_WAVE_PERIOD_SECONDS); - const lastPeakHalfWaveCount = Math.floor(previousPeakTime.current / HALF_WAVE_PERIOD_SECONDS); - - if (currentHalfWaveCount > lastPeakHalfWaveCount) { - currentAmplitude.current = Math.min(currentAmplitude.current + SINUSOIDAL_AMPLITUDE_ACCUMULATOR, MAX_SINUSOIDAL_AMPLITUDE); - previousPeakTime.current = animationTime.current; - } - } - /** * Emitter shift */ - useFrame(({ clock }, delta) => { - const currentRealTime = clock.getElapsedTime(); - const realTimeDelta = currentRealTime - previousRealTime.current; - previousRealTime.current = currentRealTime; + useFrame(() => { + const currentAnimationTime = getVisualizerTimeDiff(); + const { x, y } = getEmitterPositions(currentAnimationTime); 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) { + emitterRef.current.position.x = x; + emitterRef.current.position.y = y; } - if (emitterRef.current) { - const newYPos = getSinusoidalPosition(animationTime.current, currentAmplitude.current); - emitterRef.current.position.y = newYPos; + if (tangleWrapperRef.current) { + tangleWrapperRef.current.position.x = x - xTangleDistance / 2; } } + + if (tangleWrapperRef.current && camera) { + camera.position.x = tangleWrapperRef.current.position.x + xTangleDistance / 2; + } }); // The Tangle rendering hook useRenderTangle(); return ( - + <> {/* TangleWrapper Mesh */} - + - + {/* Emitter Mesh */} @@ -116,7 +86,7 @@ const Emitter: React.FC = ({ setRunListeners, emitterRef }: Emitte - + ); }; export default Emitter; diff --git a/client/src/features/visualizer-threejs/VisualizerInstance.tsx b/client/src/features/visualizer-threejs/VisualizerInstance.tsx index a6580f175..751d8e187 100644 --- a/client/src/features/visualizer-threejs/VisualizerInstance.tsx +++ b/client/src/features/visualizer-threejs/VisualizerInstance.tsx @@ -5,19 +5,16 @@ 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 { FAR_PLANE, NEAR_PLANE, DIRECTIONAL_LIGHT_INTENSITY, PENDING_BLOCK_COLOR, VISUALIZER_BACKGROUND, - EMITTER_X_POSITION_MULTIPLIER, BLOCK_STATE_TO_COLOR, } from "./constants"; import Emitter from "./Emitter"; import { useTangleStore, useConfigStore } from "./store"; -import { getGenerateDynamicYZPosition, randomIntFromInterval } from "./utils"; import { BPSCounter } from "./BPSCounter"; import { VisualizerRouteProps } from "../../app/routes/VisualizerRouteProps"; import { ServiceFactory } from "../../factories/serviceFactory"; @@ -32,6 +29,8 @@ import { BasicBlockBody, Utils, type IBlockMetadata, type BlockState, type SlotI import { IFeedBlockData } from "~/models/api/nova/feed/IFeedBlockData"; import CameraControls from "./CameraControls"; import "./Visualizer.scss"; +import useVisualizerTimer from "~/helpers/nova/hooks/useVisualizerTimer"; +import { getBlockInitPosition, getBlockTargetPosition } from "./blockPositions"; const features = { statsEnabled: false, @@ -44,7 +43,6 @@ const VisualizerInstance: React.FC> = }, }) => { const [networkConfig] = useNetworkConfig(network); - const generateYZPositions = getGenerateDynamicYZPosition(); const themeMode = useGetThemeMode(); const [runListeners, setRunListeners] = React.useState(false); @@ -80,6 +78,8 @@ const VisualizerInstance: React.FC> = const emitterRef = useRef(null); const [feedService, setFeedService] = React.useState(ServiceFactory.get(`feed-${network}`)); + const getCurrentAnimationTime = useVisualizerTimer(); + /** * Pause on tab or window change */ @@ -166,6 +166,7 @@ const VisualizerInstance: React.FC> = if (!runListeners) { return; } + setIsPlaying(true); return () => { @@ -195,21 +196,14 @@ const VisualizerInstance: React.FC> = * @param blockData The new block data */ const onNewBlock = (blockData: IFeedBlockData) => { - const emitterObj = emitterRef.current; - if (emitterObj && blockData && isPlaying) { - const emitterBox = new Box3().setFromObject(emitterObj); - - const emitterCenter = new THREE.Vector3(); - emitterBox.getCenter(emitterCenter); - - const { y, z } = generateYZPositions(bpsCounter.getBPS(), emitterCenter); - const minX = emitterBox.min.x - (emitterBox.max.x - emitterBox.min.x) * EMITTER_X_POSITION_MULTIPLIER; - const maxX = emitterBox.max.x + (emitterBox.max.x - emitterBox.min.x) * EMITTER_X_POSITION_MULTIPLIER; - - const x = randomIntFromInterval(minX, maxX); - const targetPosition = { x, y, z }; + if (blockData) { + const currentAnimationTime = getCurrentAnimationTime(); + const bps = bpsCounter.getBPS(); + const initPosition = getBlockInitPosition(currentAnimationTime); + const targetPosition = getBlockTargetPosition(initPosition, bps); bpsCounter.addBlock(); + if (!bpsCounter.getBPS()) { bpsCounter.start(); } @@ -229,12 +223,9 @@ const VisualizerInstance: React.FC> = addBlock({ id: blockData.blockId, color: PENDING_BLOCK_COLOR, + blockAddedTimestamp: getCurrentAnimationTime(), targetPosition, - initPosition: { - x: emitterCenter.x, - y: emitterCenter.y, - z: emitterCenter.z, - }, + initPosition, }); } }; diff --git a/client/src/features/visualizer-threejs/blockPositions.ts b/client/src/features/visualizer-threejs/blockPositions.ts new file mode 100644 index 000000000..c38e0cab2 --- /dev/null +++ b/client/src/features/visualizer-threejs/blockPositions.ts @@ -0,0 +1,31 @@ +import { EMITTER_WIDTH, EMITTER_X_POSITION_MULTIPLIER } from "./constants"; +import { getEmitterPositions, getGenerateDynamicYZPosition, getTangleDistances, randomIntFromInterval } from "./utils"; + +const generateYZPositions = getGenerateDynamicYZPosition(); + +interface IPos { + x: number; + y: number; + z: number; +} +export function getBlockTargetPosition(initPosition: IPos, bps: number): IPos { + const { y, z } = generateYZPositions(bps, initPosition); + + const emitterMinX = initPosition.x - EMITTER_WIDTH / 2; + const emitterMaxX = initPosition.x + EMITTER_WIDTH / 2; + + const minX = emitterMinX - (emitterMaxX - emitterMinX) * EMITTER_X_POSITION_MULTIPLIER; + const maxX = emitterMaxX + (emitterMaxX - emitterMinX) * EMITTER_X_POSITION_MULTIPLIER; + + const x = randomIntFromInterval(minX, maxX); + + return { x, y, z }; +} + +export function getBlockInitPosition(currentAnimationTime: number): IPos { + const { xTangleDistance } = getTangleDistances(); + const { x: xEmitterPos, y, z } = getEmitterPositions(currentAnimationTime); + const x = xEmitterPos + xTangleDistance / 2; + + return { x, y, z }; +} diff --git a/client/src/features/visualizer-threejs/interfaces.ts b/client/src/features/visualizer-threejs/interfaces.ts index fc7faf8d0..0efef94cb 100644 --- a/client/src/features/visualizer-threejs/interfaces.ts +++ b/client/src/features/visualizer-threejs/interfaces.ts @@ -4,3 +4,9 @@ export interface ICameraAngles { maxPolarAngle: number; maxAzimuthAngle: number; } + +export interface IThreeDimensionalPosition { + x: number; + y: number; + z: number; +} diff --git a/client/src/features/visualizer-threejs/store/config.ts b/client/src/features/visualizer-threejs/store/config.ts index 36d4fb1da..02956518b 100644 --- a/client/src/features/visualizer-threejs/store/config.ts +++ b/client/src/features/visualizer-threejs/store/config.ts @@ -7,8 +7,14 @@ interface ConfigState { isPlaying: boolean; setIsPlaying: (isPlaying: boolean) => void; + inView: boolean; + setInView: (inView: boolean) => void; + isEdgeRenderingEnabled: boolean; setEdgeRenderingEnabled: (isEdgeRenderingEnabled: boolean) => void; + + initialTime: number | null; + setInitialTime: (initialTime: number) => void; } export const useConfigStore = create((set) => ({ @@ -34,6 +40,17 @@ export const useConfigStore = create((set) => ({ })); }, + /** + * Is canvas in view + */ + inView: false, + setInView: (inView) => { + set((state) => ({ + ...state, + inView, + })); + }, + /** * Is edge rendering enabled */ @@ -44,4 +61,16 @@ export const useConfigStore = create((set) => ({ isEdgeRenderingEnabled, })); }, + + /** + * The initial time when the emitter was mounted. + * Used for all animations based on time. + */ + initialTime: null, + setInitialTime: (initialTime) => { + set((state) => ({ + ...state, + initialTime, + })); + }, })); diff --git a/client/src/features/visualizer-threejs/store/tangle.ts b/client/src/features/visualizer-threejs/store/tangle.ts index 9633cb195..4fb7e376d 100644 --- a/client/src/features/visualizer-threejs/store/tangle.ts +++ b/client/src/features/visualizer-threejs/store/tangle.ts @@ -3,19 +3,17 @@ import { create } from "zustand"; import { devtools } from "zustand/middleware"; import { ZOOM_DEFAULT, ANIMATION_TIME_SECONDS } from "../constants"; import { IFeedBlockData } from "~models/api/nova/feed/IFeedBlockData"; +import { IThreeDimensionalPosition } from "../interfaces"; import { BlockId, SlotIndex } from "@iota/sdk-wasm-nova/web"; -interface IPosition { - x: number; - y: number; - z: number; +export interface IBlockAnimationPosition { + initPosition: IThreeDimensionalPosition; + targetPosition: IThreeDimensionalPosition; + blockAddedTimestamp: number; + elapsedTime: number; } -export interface IBlockInitPosition extends IPosition { - duration: number; -} - -export interface BlockState { +export interface IBlockState extends Omit { id: string; color: Color; } @@ -32,15 +30,21 @@ interface EdgeEntry { interface TangleState { // Queue for "add block" operation to the canvas - blockQueue: BlockState[]; - addToBlockQueue: (newBlock: BlockState & { initPosition: IPosition; targetPosition: IPosition }) => void; + blockQueue: IBlockState[]; + addToBlockQueue: ( + newBlock: IBlockState & { + initPosition: IThreeDimensionalPosition; + targetPosition: IThreeDimensionalPosition; + blockAddedTimestamp: number; + }, + ) => void; removeFromBlockQueue: (blockIds: string[]) => void; edgeQueue: Edge[]; addToEdgeQueue: (blockId: string, parents: string[]) => void; removeFromEdgeQueue: (edges: Edge[]) => void; - colorQueue: Pick[]; + colorQueue: Pick[]; addToColorQueue: (blockId: string, color: Color) => void; removeFromColorQueue: (blockIds: string[]) => void; @@ -49,7 +53,6 @@ interface TangleState { blockIdToEdges: Map; blockIdToPosition: Map; blockMetadata: Map; - blockIdToAnimationPosition: Map; indexToBlockId: string[]; updateBlockIdToIndex: (blockId: string, index: number) => void; @@ -63,7 +66,9 @@ interface TangleState { clickedInstanceId: string | null; setClickedInstanceId: (instanceId: string | null) => void; - updateBlockIdToAnimationPosition: (updatedPositions: Map) => void; + blockIdToAnimationPosition: Map; + updateBlockIdToAnimationPosition: (updatedPositions: Map) => void; + resetConfigState: () => void; // Confirmed/accepted blocks by slot @@ -99,7 +104,7 @@ export const useTangleStore = create()( }); for (const [key, value] of state.blockIdToAnimationPosition) { - if (value.duration > ANIMATION_TIME_SECONDS) { + if (value.elapsedTime > ANIMATION_TIME_SECONDS) { state.blockIdToAnimationPosition.delete(key); } } @@ -110,16 +115,18 @@ export const useTangleStore = create()( }, addToBlockQueue: (block) => { set((state) => { - const { initPosition, targetPosition, ...blockRest } = block; + const { initPosition, targetPosition, blockAddedTimestamp, ...blockRest } = block; state.blockIdToPosition.set(block.id, [targetPosition.x, targetPosition.y, targetPosition.z]); state.blockIdToAnimationPosition.set(block.id, { - ...initPosition, - duration: 0, + initPosition, + blockAddedTimestamp, + targetPosition, + elapsedTime: 0, }); return { ...state, - blockQueue: [...state.blockQueue, blockRest], + blockQueue: [...state.blockQueue, { initPosition, targetPosition, blockAddedTimestamp, ...blockRest }], }; }); }, diff --git a/client/src/features/visualizer-threejs/useRenderTangle.tsx b/client/src/features/visualizer-threejs/useRenderTangle.tsx index 4e6e93604..14f8c3b27 100644 --- a/client/src/features/visualizer-threejs/useRenderTangle.tsx +++ b/client/src/features/visualizer-threejs/useRenderTangle.tsx @@ -1,10 +1,12 @@ -import { useThree } from "@react-three/fiber"; +import { useFrame, useThree } from "@react-three/fiber"; import { useEffect, useRef } from "react"; import * as THREE from "three"; -import { MAX_BLOCK_INSTANCES, NODE_SIZE_DEFAULT, ANIMATION_TIME_SECONDS } from "./constants"; +import { ANIMATION_TIME_SECONDS, MAX_BLOCK_INSTANCES, NODE_SIZE_DEFAULT } from "./constants"; import { useMouseMove } from "./hooks/useMouseMove"; -import { BlockState, IBlockInitPosition, useConfigStore, useTangleStore } from "./store"; +import { IBlockState, IBlockAnimationPosition, useConfigStore, useTangleStore } from "./store"; import { useRenderEdges } from "./useRenderEdges"; +import useVisualizerTimer from "~/helpers/nova/hooks/useVisualizerTimer"; +import { positionToVector } from "./utils"; const SPHERE_GEOMETRY = new THREE.SphereGeometry(NODE_SIZE_DEFAULT, 32, 16); const SPHERE_MATERIAL = new THREE.MeshPhongMaterial(); @@ -14,8 +16,8 @@ const INITIAL_SPHERE_SCALE = 0.7; export const useRenderTangle = () => { const tangleMeshRef = useRef(new THREE.InstancedMesh(SPHERE_GEOMETRY, SPHERE_MATERIAL, MAX_BLOCK_INSTANCES)); const objectIndexRef = useRef(0); - const clearBlocksRef = useRef<() => void>(); const { scene } = useThree(); + const isPlaying = useConfigStore((s) => s.isPlaying); const blockQueue = useTangleStore((s) => s.blockQueue); const removeFromBlockQueue = useTangleStore((s) => s.removeFromBlockQueue); @@ -25,31 +27,11 @@ export const useRenderTangle = () => { const blockIdToIndex = useTangleStore((s) => s.blockIdToIndex); const updateBlockIdToIndex = useTangleStore((s) => s.updateBlockIdToIndex); - const blockIdToPosition = useTangleStore((s) => s.blockIdToPosition); const blockIdToAnimationPosition = useTangleStore((s) => s.blockIdToAnimationPosition); + const getVisualizerTimeDiff = useVisualizerTimer(); - function updateInstancedMeshPosition( - instancedMesh: THREE.InstancedMesh, - index: number, - nextPosition: THREE.Vector3, - ) { - const matrix = new THREE.Matrix4(); - const position = new THREE.Vector3(); - const quaternion = new THREE.Quaternion(); - const scale = new THREE.Vector3(); - instancedMesh.getMatrixAt(index, matrix); - matrix.decompose(position, quaternion, scale); - matrix.compose(nextPosition, quaternion, scale); - instancedMesh.setMatrixAt(index, matrix); - instancedMesh.instanceMatrix.needsUpdate = true; - } - - const assignBlockToMesh = (block: BlockState) => { - const initPosition = blockIdToAnimationPosition.get(block.id); - - if (!initPosition) return; - - SPHERE_TEMP_OBJECT.position.set(initPosition.x, initPosition.y, initPosition.z); + const assignBlockToMesh = (block: IBlockState) => { + SPHERE_TEMP_OBJECT.position.copy(positionToVector(block.initPosition)); SPHERE_TEMP_OBJECT.scale.setScalar(INITIAL_SPHERE_SCALE); SPHERE_TEMP_OBJECT.updateMatrix(); @@ -72,55 +54,25 @@ export const useRenderTangle = () => { useRenderEdges(); useMouseMove({ tangleMeshRef }); - /** Spray animation */ - useEffect(() => { - const PERIOD = 24; // ms - - const int = setInterval(() => { - const isPlaying = useConfigStore.getState().isPlaying; - if (!isPlaying) { - return; - } - const blockIdToAnimationPosition = useTangleStore.getState().blockIdToAnimationPosition; - const updateBlockIdToAnimationPosition = useTangleStore.getState().updateBlockIdToAnimationPosition; - const delta = PERIOD / 1000; - - const updatedAnimationPositions: Map = new Map(); - blockIdToAnimationPosition.forEach(({ x, y, z, duration: currentTime }, blockId) => { - const nextTime = currentTime + delta; - const startPositionVector = new THREE.Vector3(x, y, z); - const endPositionVector = new THREE.Vector3(...(blockIdToPosition.get(blockId) as [number, number, number])); - const interpolationFactor = Math.min(nextTime / ANIMATION_TIME_SECONDS, 1); // set 1 as max value - - const targetPositionVector = new THREE.Vector3(); - targetPositionVector.lerpVectors(startPositionVector, endPositionVector, interpolationFactor); - updatedAnimationPositions.set(blockId, { x, y, z, duration: nextTime }); - const index = blockIdToIndex.get(blockId); - if (index) { - updateInstancedMeshPosition(tangleMeshRef.current, index, targetPositionVector); - } - }); - updateBlockIdToAnimationPosition(updatedAnimationPositions); - }, PERIOD); - - return () => { - clearInterval(int); - blockIdToAnimationPosition.clear(); - blockIdToPosition.clear(); - }; - }, []); - - useEffect(() => { - const intervalCallback = () => { - if (clearBlocksRef.current) { - clearBlocksRef.current(); - } - }; - const timer = setInterval(intervalCallback, 500); - - return () => clearInterval(timer); - }, []); + function updateInstancedMeshPosition( + instancedMesh: THREE.InstancedMesh, + index: number, + nextPosition: THREE.Vector3, + ) { + const matrix = new THREE.Matrix4(); + const position = new THREE.Vector3(); + const quaternion = new THREE.Quaternion(); + const scale = new THREE.Vector3(); + instancedMesh.getMatrixAt(index, matrix); + matrix.decompose(position, quaternion, scale); + matrix.compose(nextPosition, quaternion, scale); + instancedMesh.setMatrixAt(index, matrix); + instancedMesh.instanceMatrix.needsUpdate = true; + } + /** + * Setup and add the tangle mesh to the scene + */ useEffect(() => { if (tangleMeshRef?.current) { tangleMeshRef.current.instanceMatrix.setUsage(THREE.DynamicDrawUsage); @@ -137,6 +89,9 @@ export const useRenderTangle = () => { } }, [tangleMeshRef]); + /** + * Add blocks to the tangle + */ useEffect(() => { if (blockQueue.length === 0) { return; @@ -152,16 +107,21 @@ export const useRenderTangle = () => { } } - if (tangleMeshRef.current.instanceColor) { - tangleMeshRef.current.instanceColor.needsUpdate = true; - } + if (isPlaying) { + if (tangleMeshRef.current.instanceColor) { + tangleMeshRef.current.instanceColor.needsUpdate = true; + } - tangleMeshRef.current.instanceMatrix.needsUpdate = true; - tangleMeshRef.current.computeBoundingSphere(); + tangleMeshRef.current.instanceMatrix.needsUpdate = true; + tangleMeshRef.current.computeBoundingSphere(); + } removeFromBlockQueue(addedIds); - }, [blockQueue, blockIdToAnimationPosition]); + }, [blockQueue, blockIdToAnimationPosition, isPlaying]); + /** + * Update block colors + */ useEffect(() => { if (colorQueue.length > 0) { const removeIds: string[] = []; @@ -182,4 +142,37 @@ export const useRenderTangle = () => { removeFromColorQueue(removeIds); } }, [colorQueue, blockIdToIndex]); + + /** + * Spray animation + */ + useFrame(() => { + const isPlaying = useConfigStore.getState().isPlaying; + + if (!isPlaying) { + return; + } + + const blockIdToAnimationPosition = useTangleStore.getState().blockIdToAnimationPosition; + const updateBlockIdToAnimationPosition = useTangleStore.getState().updateBlockIdToAnimationPosition; + + const updatedAnimationPositions: Map = new Map(); + + blockIdToAnimationPosition.forEach(({ initPosition, targetPosition, blockAddedTimestamp }, blockId) => { + const currentAnimationTime = getVisualizerTimeDiff(); + const elapsedTime = currentAnimationTime - blockAddedTimestamp; + const positionBasedOnTime = Math.min(elapsedTime / ANIMATION_TIME_SECONDS, 1); + const targetPositionVector = new THREE.Vector3(); + + targetPositionVector.lerpVectors(positionToVector(initPosition), positionToVector(targetPosition), positionBasedOnTime); + updatedAnimationPositions.set(blockId, { initPosition, elapsedTime, targetPosition, blockAddedTimestamp }); + + const index = blockIdToIndex.get(blockId); + if (index) { + updateInstancedMeshPosition(tangleMeshRef.current, index, targetPositionVector); + } + }); + + updateBlockIdToAnimationPosition(updatedAnimationPositions); + }); }; diff --git a/client/src/features/visualizer-threejs/utils.ts b/client/src/features/visualizer-threejs/utils.ts index 575b7d53d..ddf39952b 100644 --- a/client/src/features/visualizer-threejs/utils.ts +++ b/client/src/features/visualizer-threejs/utils.ts @@ -1,3 +1,4 @@ +import { Vector3 } from "three"; import { BLOCK_STEP_PX, MIN_BLOCKS_PER_SECOND, @@ -15,9 +16,10 @@ import { CAMERA_Y_AXIS_MOVEMENT, CAMERA_X_OFFSET, CAMERA_Y_OFFSET, + SINUSOIDAL_AMPLITUDE_ACCUMULATOR, + INITIAL_SINUSOIDAL_AMPLITUDE, } from "./constants"; -import { Vector3 } from "three"; -import { ICameraAngles } from "./interfaces"; +import { ICameraAngles, IThreeDimensionalPosition } from "./interfaces"; /** * Generates a random number within a specified range. @@ -94,7 +96,14 @@ function getLinearRadius(bps: number): number { * Generates a random point on a circle. * @returns the random point on a circle. */ -function getDynamicRandomYZPoints(bps: number, initialPosition: Vector3 = new Vector3(0, 0, 0)): IBlockTanglePosition { +function getDynamicRandomYZPoints( + bps: number, + initialPosition: IThreeDimensionalPosition = { + x: 0, + y: 0, + z: 0, + }, +): IBlockTanglePosition { const theta = Math.random() * (2 * Math.PI); const maxRadius = getLinearRadius(bps); @@ -123,7 +132,11 @@ function pointPassesAllChecks(point: IBlockTanglePosition, prevPoints: IBlockTan * Retries to generate a point until it passes all the checks. * @returns the point that passes all the checks. */ -function generateAValidRandomPoint(bps: number, initialPosition: Vector3, prevPoints: IBlockTanglePosition[]): IBlockTanglePosition { +function generateAValidRandomPoint( + bps: number, + initialPosition: IThreeDimensionalPosition, + prevPoints: IBlockTanglePosition[], +): IBlockTanglePosition { let trialPoint: IBlockTanglePosition; let passAllChecks = false; let retries = 0; @@ -149,7 +162,7 @@ function generateAValidRandomPoint(bps: number, initialPosition: Vector3, prevPo export function getGenerateDynamicYZPosition(): typeof getDynamicRandomYZPoints { const prevPoints: IBlockTanglePosition[] = []; - return (bps: number, initialPosition: Vector3 = new Vector3(0, 0, 0)): IBlockTanglePosition => { + return (bps: number, initialPosition: IThreeDimensionalPosition = { x: 0, y: 0, z: 0 }): IBlockTanglePosition => { const validPoint = generateAValidRandomPoint(bps, initialPosition, prevPoints); const randomYNumber = randomNumberFromInterval(0, BLOCK_STEP_PX / 20); @@ -218,15 +231,43 @@ export function getCameraAngles(): ICameraAngles { } /** - * Calculates the sinusoidal position for the emitter + * Calculates the sinusoidal position for the emitter based on the current animation time. * @returns the sinusoidal position */ -export function getSinusoidalPosition(time: number, amplitude: number): number { - const period = HALF_WAVE_PERIOD_SECONDS * 2; - const frequency = 1 / period; - const phase = (time % period) * frequency; +export function calculateSinusoidalAmplitude(currentAnimationTime: number): number { + const wavePeriod = HALF_WAVE_PERIOD_SECONDS * 2; + const currentWaveCount = Math.floor(currentAnimationTime / wavePeriod); + const accumulatedAmplitude = currentWaveCount * SINUSOIDAL_AMPLITUDE_ACCUMULATOR; + const currentAmplitude = Math.min(INITIAL_SINUSOIDAL_AMPLITUDE + accumulatedAmplitude, MAX_SINUSOIDAL_AMPLITUDE); + + const yPosition = currentAmplitude * Math.sin((2 * Math.PI * currentAnimationTime) / wavePeriod); + + return yPosition; +} + +/** + * Calculates the emitter position based on the current animation time. + * @returns the emitter position + */ +export function calculateEmitterPositionX(currentAnimationTime: number): number { + return currentAnimationTime * EMITTER_SPEED_MULTIPLIER; +} - const newY = amplitude * Math.sin(phase * 2 * Math.PI); +/** + * Calculates the emitter position based on the current animation time. + * @returns the emitter X,Y,Z positions + */ +export function getEmitterPositions(currentAnimationTime: number): IThreeDimensionalPosition { + const x = calculateEmitterPositionX(currentAnimationTime); + const y = calculateSinusoidalAmplitude(currentAnimationTime); + return { x, y, z: 0 }; +} - return newY; +/** + * Converts a position object to a Vector3 object. + * @param position - The position object to convert. + * @returns A Vector3 object representing the position. + */ +export function positionToVector(position: IThreeDimensionalPosition) { + return new Vector3(position.x, position.y, position.z); } diff --git a/client/src/features/visualizer-threejs/wrapper/Wrapper.tsx b/client/src/features/visualizer-threejs/wrapper/Wrapper.tsx index 37d5d4f45..e2796f4d7 100644 --- a/client/src/features/visualizer-threejs/wrapper/Wrapper.tsx +++ b/client/src/features/visualizer-threejs/wrapper/Wrapper.tsx @@ -67,7 +67,6 @@ export const Wrapper = ({ )} - {selectedFeedItem && } diff --git a/client/src/helpers/nova/hooks/useVisualizerTimer.ts b/client/src/helpers/nova/hooks/useVisualizerTimer.ts new file mode 100644 index 000000000..1153ba831 --- /dev/null +++ b/client/src/helpers/nova/hooks/useVisualizerTimer.ts @@ -0,0 +1,16 @@ +import { useConfigStore } from "~/features/visualizer-threejs/store"; + +export default function useVisualizerTimer() { + const initialTime = useConfigStore((state) => state.initialTime); + + return () => { + if (!initialTime) { + return 0; + } + + const currentTime = Date.now(); + const diff = (currentTime - initialTime) / 1_000; + + return diff; + }; +}