diff --git a/client/src/features/visualizer-threejs/Emitter.tsx b/client/src/features/visualizer-threejs/Emitter.tsx index 767dfee56..9c251e245 100644 --- a/client/src/features/visualizer-threejs/Emitter.tsx +++ b/client/src/features/visualizer-threejs/Emitter.tsx @@ -1,10 +1,10 @@ /* eslint-disable react/no-unknown-property */ import { useFrame, useThree } from "@react-three/fiber"; -import React, { RefObject, Dispatch, SetStateAction, useEffect, useRef } from "react"; +import React, { RefObject, Dispatch, SetStateAction, useEffect, useRef, useLayoutEffect } from "react"; import * as THREE from "three"; import { useConfigStore, useTangleStore } from "./store"; import { useRenderTangle } from "./useRenderTangle"; -import { getTangleDistances, getEmitterPositions } from "./utils"; +import { getTangleDistances, getEmitterPositions, generateRandomPeriods } from "./utils"; import { CanvasElement } from "./enums"; import useVisualizerTimer from "~/helpers/nova/hooks/useVisualizerTimer"; import { EMITTER_DEPTH, EMITTER_HEIGHT, EMITTER_WIDTH } from "./constants"; @@ -28,8 +28,19 @@ const Emitter: React.FC = ({ setRunListeners, emitterRef }: Emitte const setIsPlaying = useConfigStore((state) => state.setIsPlaying); const setInitialTime = useConfigStore((state) => state.setInitialTime); + const sinusoidPeriodsSum = useConfigStore((state) => state.sinusoidPeriodsSum); + const setSinusoidPeriodsSum = useConfigStore((state) => state.setSinusoidPeriodsSum); + const randomizedSinusoidPeriods = useConfigStore((state) => state.sinusoidRandomPeriods); + const setRandomizedSinusoidPeriods = useConfigStore((state) => state.setSinusoidRandomPeriods); + const tangleWrapperRef = useRef(null); + useLayoutEffect(() => { + const { periods, sum: periodsSum } = generateRandomPeriods(); + setRandomizedSinusoidPeriods(periods); + setSinusoidPeriodsSum(periodsSum); + }, []); + useEffect(() => { setZoom(currentZoom); }, [currentZoom]); @@ -52,7 +63,11 @@ const Emitter: React.FC = ({ setRunListeners, emitterRef }: Emitte */ useFrame(() => { const currentAnimationTime = getVisualizerTimeDiff(); - const { x, y } = getEmitterPositions(currentAnimationTime); + const { x, y } = getEmitterPositions({ + currentAnimationTime, + periods: randomizedSinusoidPeriods, + periodsSum: sinusoidPeriodsSum, + }); if (isPlaying) { if (emitterRef.current) { diff --git a/client/src/features/visualizer-threejs/VisualizerInstance.tsx b/client/src/features/visualizer-threejs/VisualizerInstance.tsx index 751d8e187..fcf5fc4c0 100644 --- a/client/src/features/visualizer-threejs/VisualizerInstance.tsx +++ b/client/src/features/visualizer-threejs/VisualizerInstance.tsx @@ -44,6 +44,7 @@ const VisualizerInstance: React.FC> = }) => { const [networkConfig] = useNetworkConfig(network); const themeMode = useGetThemeMode(); + const getCurrentAnimationTime = useVisualizerTimer(); const [runListeners, setRunListeners] = React.useState(false); @@ -72,14 +73,15 @@ const VisualizerInstance: React.FC> = const addToConfirmedBlocksSlot = useTangleStore((s) => s.addToConfirmedBlocksBySlot); const removeConfirmedBlocksSlot = useTangleStore((s) => s.removeConfirmedBlocksSlot); + const sinusoidPeriodsSum = useConfigStore((s) => s.sinusoidPeriodsSum); + const sinusoidRandomPeriods = useConfigStore((s) => s.sinusoidRandomPeriods); + const selectedFeedItem: TSelectFeedItemNova = clickedInstanceId ? blockMetadata.get(clickedInstanceId) ?? null : null; const resetConfigState = useTangleStore((s) => s.resetConfigState); const emitterRef = useRef(null); const [feedService, setFeedService] = React.useState(ServiceFactory.get(`feed-${network}`)); - const getCurrentAnimationTime = useVisualizerTimer(); - /** * Pause on tab or window change */ @@ -199,7 +201,11 @@ const VisualizerInstance: React.FC> = if (blockData) { const currentAnimationTime = getCurrentAnimationTime(); const bps = bpsCounter.getBPS(); - const initPosition = getBlockInitPosition(currentAnimationTime); + const initPosition = getBlockInitPosition({ + currentAnimationTime, + periods: sinusoidRandomPeriods, + periodsSum: sinusoidPeriodsSum, + }); const targetPosition = getBlockTargetPosition(initPosition, bps); bpsCounter.addBlock(); diff --git a/client/src/features/visualizer-threejs/blockPositions.ts b/client/src/features/visualizer-threejs/blockPositions.ts index c38e0cab2..82caa101c 100644 --- a/client/src/features/visualizer-threejs/blockPositions.ts +++ b/client/src/features/visualizer-threejs/blockPositions.ts @@ -1,4 +1,5 @@ import { EMITTER_WIDTH, EMITTER_X_POSITION_MULTIPLIER } from "./constants"; +import { ISinusoidalPositionParams } from "./interfaces"; import { getEmitterPositions, getGenerateDynamicYZPosition, getTangleDistances, randomIntFromInterval } from "./utils"; const generateYZPositions = getGenerateDynamicYZPosition(); @@ -22,9 +23,9 @@ export function getBlockTargetPosition(initPosition: IPos, bps: number): IPos { return { x, y, z }; } -export function getBlockInitPosition(currentAnimationTime: number): IPos { +export function getBlockInitPosition({ currentAnimationTime, periods, periodsSum }: ISinusoidalPositionParams): IPos { const { xTangleDistance } = getTangleDistances(); - const { x: xEmitterPos, y, z } = getEmitterPositions(currentAnimationTime); + const { x: xEmitterPos, y, z } = getEmitterPositions({ currentAnimationTime, periods, periodsSum }); const x = xEmitterPos + xTangleDistance / 2; return { x, y, z }; diff --git a/client/src/features/visualizer-threejs/constants.ts b/client/src/features/visualizer-threejs/constants.ts index 37b399ec5..94f527f66 100644 --- a/client/src/features/visualizer-threejs/constants.ts +++ b/client/src/features/visualizer-threejs/constants.ts @@ -26,8 +26,8 @@ export const PENDING_BLOCK_COLOR = new Color("#A6C3FC"); export const ACCEPTED_BLOCK_COLOR = new Color("#0101AB"); export const CONFIRMED_BLOCK_COLOR = new Color("#0000DB"); export const FINALIZED_BLOCK_COLOR = new Color("#0101FF"); -// TODO Remove accepted state once is added to the SDK (missing) -export const BLOCK_STATE_TO_COLOR = new Map([ + +export const BLOCK_STATE_TO_COLOR = new Map([ ["pending", PENDING_BLOCK_COLOR], ["accepted", ACCEPTED_BLOCK_COLOR], ["confirmed", CONFIRMED_BLOCK_COLOR], @@ -72,7 +72,6 @@ export const EMITTER_HEIGHT = 250; export const EMITTER_DEPTH = 250; // conic emitter - export const MIN_TANGLE_RADIUS = 100; export const MAX_TANGLE_RADIUS = 300; @@ -90,3 +89,8 @@ export const MAX_SINUSOIDAL_AMPLITUDE = 200; export const SINUSOIDAL_AMPLITUDE_ACCUMULATOR = 30; export const INITIAL_SINUSOIDAL_AMPLITUDE = 80; export const HALF_WAVE_PERIOD_SECONDS = 5; + +/* Values for randomizing the tangle */ +export const NUMBER_OF_RANDOM_PERIODS = 100; +export const MIN_SINUSOID_PERIOD = 5; +export const MAX_SINUSOID_PERIOD = 8; diff --git a/client/src/features/visualizer-threejs/interfaces.ts b/client/src/features/visualizer-threejs/interfaces.ts index 0efef94cb..55df2965f 100644 --- a/client/src/features/visualizer-threejs/interfaces.ts +++ b/client/src/features/visualizer-threejs/interfaces.ts @@ -10,3 +10,12 @@ export interface IThreeDimensionalPosition { y: number; z: number; } + +export interface ITimeBasedPositionParams { + currentAnimationTime: number; +} + +export interface ISinusoidalPositionParams extends ITimeBasedPositionParams { + periods: number[]; + periodsSum: number; +} diff --git a/client/src/features/visualizer-threejs/store/config.ts b/client/src/features/visualizer-threejs/store/config.ts index 02956518b..b3152e8cd 100644 --- a/client/src/features/visualizer-threejs/store/config.ts +++ b/client/src/features/visualizer-threejs/store/config.ts @@ -15,6 +15,11 @@ interface ConfigState { initialTime: number | null; setInitialTime: (initialTime: number) => void; + + sinusoidPeriodsSum: number; + setSinusoidPeriodsSum: (totalPeriodsSum: number) => void; + sinusoidRandomPeriods: number[]; + setSinusoidRandomPeriods: (randomizedPeriods: number[]) => void; } export const useConfigStore = create((set) => ({ @@ -73,4 +78,22 @@ export const useConfigStore = create((set) => ({ initialTime, })); }, + + /** + * Randomized periods for the tangle. + */ + sinusoidPeriodsSum: 0, + setSinusoidPeriodsSum: (totalPeriodsSum) => { + set((state) => ({ + ...state, + sinusoidPeriodsSum: totalPeriodsSum, + })); + }, + sinusoidRandomPeriods: [], + setSinusoidRandomPeriods: (randomizedPeriods) => { + set((state) => ({ + ...state, + sinusoidRandomPeriods: randomizedPeriods, + })); + }, })); diff --git a/client/src/features/visualizer-threejs/utils.ts b/client/src/features/visualizer-threejs/utils.ts index ddf39952b..f38a82354 100644 --- a/client/src/features/visualizer-threejs/utils.ts +++ b/client/src/features/visualizer-threejs/utils.ts @@ -8,7 +8,6 @@ import { MIN_BLOCK_NEAR_RADIUS, MAX_PREV_POINTS, MAX_POINT_RETRIES, - HALF_WAVE_PERIOD_SECONDS, MAX_BLOCK_INSTANCES, EMITTER_SPEED_MULTIPLIER, MAX_SINUSOIDAL_AMPLITUDE, @@ -18,8 +17,11 @@ import { CAMERA_Y_OFFSET, SINUSOIDAL_AMPLITUDE_ACCUMULATOR, INITIAL_SINUSOIDAL_AMPLITUDE, + NUMBER_OF_RANDOM_PERIODS, + MIN_SINUSOID_PERIOD, + MAX_SINUSOID_PERIOD, } from "./constants"; -import { ICameraAngles, IThreeDimensionalPosition } from "./interfaces"; +import type { ICameraAngles, ISinusoidalPositionParams, IThreeDimensionalPosition } from "./interfaces"; /** * Generates a random number within a specified range. @@ -231,16 +233,22 @@ export function getCameraAngles(): ICameraAngles { } /** - * Calculates the sinusoidal position for the emitter based on the current animation time. + * Calculates the sinusoidal position for the emitter based on the current animation time, + * considering random periods. * @returns the sinusoidal position */ -export function calculateSinusoidalAmplitude(currentAnimationTime: number): number { - const wavePeriod = HALF_WAVE_PERIOD_SECONDS * 2; - const currentWaveCount = Math.floor(currentAnimationTime / wavePeriod); +export function calculateSinusoidalAmplitude({ currentAnimationTime, periods, periodsSum }: ISinusoidalPositionParams): number { + const elapsedTime = currentAnimationTime % periodsSum; + const { period, accumulatedTime } = getCurrentPeriodValues(currentAnimationTime, periods, periodsSum); + + const startTimeOfCurrentPeriod = accumulatedTime - period; + const timeInCurrentPeriod = elapsedTime - startTimeOfCurrentPeriod; + + const currentWaveCount = Math.floor(elapsedTime / period); 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); + const yPosition = currentAmplitude * Math.sin((2 * Math.PI * timeInCurrentPeriod) / period); return yPosition; } @@ -257,9 +265,9 @@ export function calculateEmitterPositionX(currentAnimationTime: number): number * Calculates the emitter position based on the current animation time. * @returns the emitter X,Y,Z positions */ -export function getEmitterPositions(currentAnimationTime: number): IThreeDimensionalPosition { +export function getEmitterPositions({ currentAnimationTime, periods, periodsSum }: ISinusoidalPositionParams): IThreeDimensionalPosition { const x = calculateEmitterPositionX(currentAnimationTime); - const y = calculateSinusoidalAmplitude(currentAnimationTime); + const y = calculateSinusoidalAmplitude({ currentAnimationTime, periods, periodsSum }); return { x, y, z: 0 }; } @@ -271,3 +279,32 @@ export function getEmitterPositions(currentAnimationTime: number): IThreeDimensi export function positionToVector(position: IThreeDimensionalPosition) { return new Vector3(position.x, position.y, position.z); } + +export function generateRandomPeriods(): { periods: number[]; sum: number } { + let sum = 0; + const periods = Array.from({ length: NUMBER_OF_RANDOM_PERIODS }, () => { + const period = Number(randomNumberFromInterval(MIN_SINUSOID_PERIOD, MAX_SINUSOID_PERIOD).toFixed(4)); + sum += period; + return period; + }); + return { periods, sum }; +} + +type PeriodResult = { + period: number; + accumulatedTime: number; +}; + +function getCurrentPeriodValues(animationTime: number, periods: number[], totalSum: number): PeriodResult { + const effectiveTime = animationTime % totalSum; + + let accumulatedTime = 0; + for (let i = 0; i < periods.length; i++) { + accumulatedTime += periods[i]; + if (effectiveTime < accumulatedTime) { + return { period: periods[i], accumulatedTime }; + } + } + + return { period: periods[0], accumulatedTime: 0 }; +}