From 59655f3fd1c195569b9616d2973313233342d271 Mon Sep 17 00:00:00 2001 From: JCNoguera <88061365+VmMad@users.noreply.github.com> Date: Mon, 19 Feb 2024 13:18:21 +0100 Subject: [PATCH 1/3] fix: update client to the latest iota-sdk changes (#1145) --- api/src/initServices.ts | 11 ++++++----- setup_nova.sh | 2 +- 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/api/src/initServices.ts b/api/src/initServices.ts index 6a9f02792..93f93bf15 100644 --- a/api/src/initServices.ts +++ b/api/src/initServices.ts @@ -211,14 +211,15 @@ function initNovaServices(networkConfig: INetwork): void { logger.verbose(`Initializing Nova services for ${networkConfig.network}`); const novaClientParams: INovaClientOptions = { - primaryNode: networkConfig.provider, + primaryNodes: [networkConfig.provider], }; if (networkConfig.permaNodeEndpoint) { - novaClientParams.nodes = [networkConfig.permaNodeEndpoint]; - // Client with permanode needs the ignoreNodeHealth as chronicle is considered "not healthy" by the sdk - // Related: https://github.com/iotaledger/inx-chronicle/issues/1302 - novaClientParams.ignoreNodeHealth = true; + const chronicleNode = { + url: networkConfig.permaNodeEndpoint, + permanode: true, + }; + novaClientParams.primaryNodes.push(chronicleNode); const chronicleService = new ChronicleService(networkConfig); ServiceFactory.register(`chronicle-${networkConfig.network}`, () => chronicleService); diff --git a/setup_nova.sh b/setup_nova.sh index 3a84bcb80..149b19584 100755 --- a/setup_nova.sh +++ b/setup_nova.sh @@ -1,6 +1,6 @@ #!/bin/bash SDK_DIR="iota-sdk" -TARGET_COMMIT="8f0ff5e1e899a0d960ddfea09237739a88c3bcf1" +TARGET_COMMIT="257bcff80bf0336f571f9a226ebde1acd8974104" if [ ! -d "$SDK_DIR" ]; then git clone -b 2.0 git@github.com:iotaledger/iota-sdk.git From fc127e18ad64692487f42c06b728d6360dcc1ea0 Mon Sep 17 00:00:00 2001 From: Eugene P Date: Tue, 20 Feb 2024 17:50:03 +0200 Subject: [PATCH 2/3] feat: update stats panel (#1139) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: update ui for visualizer's stats. Signed-off-by: Eugene Panteleymonchuk * feat: update stats panel. Signed-off-by: Eugene Panteleymonchuk * feat: update stats panel. update styles, use hook. Signed-off-by: Eugene Panteleymonchuk * feat: create networkStats hook, update components to prevent rerender. Signed-off-by: Eugene Panteleymonchuk --------- Signed-off-by: Eugene Panteleymonchuk Co-authored-by: Begoña Álvarez de la Cruz --- .../visualizer-threejs/Visualizer.scss | 68 ------------------ .../visualizer-threejs/VisualizerInstance.tsx | 2 +- .../visualizer-threejs/wrapper/KeyPanel.scss | 69 +++++++++++++++++-- .../visualizer-threejs/wrapper/KeyPanel.tsx | 12 ++-- .../visualizer-threejs/wrapper/StatsPanel.tsx | 31 +++------ .../visualizer-threejs/wrapper/Wrapper.tsx | 7 +- .../src/helpers/nova/hooks/useNetworkStats.ts | 52 ++++++++++++++ 7 files changed, 139 insertions(+), 102 deletions(-) create mode 100644 client/src/helpers/nova/hooks/useNetworkStats.ts diff --git a/client/src/features/visualizer-threejs/Visualizer.scss b/client/src/features/visualizer-threejs/Visualizer.scss index f5eb1b5c5..59c4e2d5f 100644 --- a/client/src/features/visualizer-threejs/Visualizer.scss +++ b/client/src/features/visualizer-threejs/Visualizer.scss @@ -83,74 +83,6 @@ } } - .stats-panel-container { - display: flex; - position: absolute; - z-index: 1; - top: 90px !important; - left: 20px !important; - bottom: auto !important; - justify-content: left !important; - align-items: center; - pointer-events: none; - - .stats-panel { - background: var(--body-background); - - .card--value, - .card--label { - text-align: left; - } - .card--label { - justify-content: flex-start; - } - .card--content { - padding: 0; - } - .stats-panel__info { - padding: 0 10px; - display: inline-block; - } - } - - @include tablet-down { - top: 60px; - left: 20px; - bottom: auto; - justify-content: left; - - .stats-panel { - .card--value, - .card--label { - text-align: left; - } - .card--label { - justify-content: flex-start; - } - .card--content { - padding: 0; - } - .stats-panel__info { - padding: 0 10px; - display: inline-block; - } - } - } - - @include phone-down { - left: 10px; - .stats-panel { - .card--value, - .card--label { - font-size: 12px; - } - .stats-panel__info:last-of-type { - display: block; - } - } - } - } - .info-panel { background: var(--body-background); display: flex; diff --git a/client/src/features/visualizer-threejs/VisualizerInstance.tsx b/client/src/features/visualizer-threejs/VisualizerInstance.tsx index 64a86fc0d..a6580f175 100644 --- a/client/src/features/visualizer-threejs/VisualizerInstance.tsx +++ b/client/src/features/visualizer-threejs/VisualizerInstance.tsx @@ -34,7 +34,7 @@ import CameraControls from "./CameraControls"; import "./Visualizer.scss"; const features = { - statsEnabled: true, + statsEnabled: false, cameraControls: true, }; diff --git a/client/src/features/visualizer-threejs/wrapper/KeyPanel.scss b/client/src/features/visualizer-threejs/wrapper/KeyPanel.scss index 354e27b6a..5e4521164 100644 --- a/client/src/features/visualizer-threejs/wrapper/KeyPanel.scss +++ b/client/src/features/visualizer-threejs/wrapper/KeyPanel.scss @@ -4,7 +4,7 @@ @import "../../../scss/variables"; @import "../../../scss/themes"; -.key-panel-container { +.info-container { display: flex; position: absolute; z-index: 1; @@ -13,19 +13,23 @@ left: 30px; justify-content: center; pointer-events: none; + gap: 20px; - .key-panel { + .card { background: var(--body-background); + padding: 16px 32px; + } + + .key-panel-list { display: flex; flex-direction: row; flex-wrap: wrap; - padding: 16px; + gap: 32px; .key-panel-item { display: flex; flex-direction: row; align-items: center; - margin: 0 16px; @include desktop-down { width: 110px; @@ -48,4 +52,61 @@ } } } + + .stats-panel-container { + display: flex; + z-index: 1; + align-items: center; + pointer-events: none; + + .card--label { + justify-content: flex-start; + } + .card--content { + padding: 0; + } + .stats-panel__info { + justify-content: center; + display: flex; + flex-direction: column; + align-items: center; + } + + @include tablet-down { + top: 60px; + left: 20px; + bottom: auto; + justify-content: left; + + .stats-panel { + .card--value, + .card--label { + text-align: left; + } + .card--label { + justify-content: flex-start; + } + .card--content { + padding: 0; + } + .stats-panel__info { + padding: 0 10px; + display: inline-block; + } + } + } + + @include phone-down { + left: 10px; + .stats-panel { + .card--value, + .card--label { + font-size: 12px; + } + .stats-panel__info:last-of-type { + display: block; + } + } + } + } } diff --git a/client/src/features/visualizer-threejs/wrapper/KeyPanel.tsx b/client/src/features/visualizer-threejs/wrapper/KeyPanel.tsx index f2affbbea..416743eb9 100644 --- a/client/src/features/visualizer-threejs/wrapper/KeyPanel.tsx +++ b/client/src/features/visualizer-threejs/wrapper/KeyPanel.tsx @@ -1,9 +1,10 @@ -import React from "react"; +import React, { memo } from "react"; import { BlockState } from "@iota/sdk-wasm-nova/web"; import "./KeyPanel.scss"; +import StatsPanel from "~features/visualizer-threejs/wrapper/StatsPanel"; -export const KeyPanel: React.FC = () => { +export const KeyPanel = ({ network }: { network: string }) => { const statuses: { label: string; state: BlockState; @@ -42,8 +43,8 @@ export const KeyPanel: React.FC = () => { ]; return ( -
-
+
+
{statuses.map((s) => { return (
@@ -58,6 +59,9 @@ export const KeyPanel: React.FC = () => { ); })}
+
); }; + +export default memo(KeyPanel); diff --git a/client/src/features/visualizer-threejs/wrapper/StatsPanel.tsx b/client/src/features/visualizer-threejs/wrapper/StatsPanel.tsx index 00e45b823..f6ac0bc01 100644 --- a/client/src/features/visualizer-threejs/wrapper/StatsPanel.tsx +++ b/client/src/features/visualizer-threejs/wrapper/StatsPanel.tsx @@ -1,29 +1,18 @@ -import React from "react"; -import { useNetworkStats } from "~helpers/stardust/hooks/useNetworkStats"; - -export const StatsPanel: React.FC<{ readonly blocksCount: number; readonly network: string }> = ({ blocksCount, network }) => { - const [blocksPerSecond, confirmedBlocksPerSecond, confirmedBlocksPerSecondPercent] = useNetworkStats(network); +import React, { memo } from "react"; +import { useNetworkStats } from "~helpers/nova/hooks/useNetworkStats"; +export const StatsPanel = ({ network }: { network: string }) => { + const [blocksPerSecond] = useNetworkStats(network); return (
-
-
-
-
Blocks
-
{blocksCount}
-
-
-
BPS / CBPS
-
- {blocksPerSecond} / {confirmedBlocksPerSecond} -
-
-
-
Referenced Rate
-
{confirmedBlocksPerSecondPercent}
-
+
+
+
BPS
+
{blocksPerSecond}
); }; + +export default memo(StatsPanel); diff --git a/client/src/features/visualizer-threejs/wrapper/Wrapper.tsx b/client/src/features/visualizer-threejs/wrapper/Wrapper.tsx index 84c8d0b31..37d5d4f45 100644 --- a/client/src/features/visualizer-threejs/wrapper/Wrapper.tsx +++ b/client/src/features/visualizer-threejs/wrapper/Wrapper.tsx @@ -2,10 +2,9 @@ import React from "react"; import Modal from "~/app/components/Modal"; import { TSelectFeedItemNova, TSelectNode } from "~/app/types/visualizer.types"; import { INetwork } from "~/models/config/INetwork"; -import { KeyPanel } from "./KeyPanel"; +import KeyPanel from "./KeyPanel"; import mainHeader from "~assets/modals/visualizer/main-header.json"; import { SelectedFeedInfo } from "./SelectedFeedInfo"; -import { StatsPanel } from "./StatsPanel"; export const Wrapper = ({ blocksCount, @@ -68,9 +67,9 @@ export const Wrapper = ({ )}
- + {selectedFeedItem && } - +
); diff --git a/client/src/helpers/nova/hooks/useNetworkStats.ts b/client/src/helpers/nova/hooks/useNetworkStats.ts new file mode 100644 index 000000000..a56a9a788 --- /dev/null +++ b/client/src/helpers/nova/hooks/useNetworkStats.ts @@ -0,0 +1,52 @@ +import { useEffect, useState } from "react"; +import { useIsMounted } from "~helpers/hooks/useIsMounted"; +import { ServiceFactory } from "~factories/serviceFactory"; +import { NOVA } from "~models/config/protocolVersion"; +import { NovaApiClient } from "~services/nova/novaApiClient"; + +/** + * Periodicaly refresh network stats. + * @param network The network in context. + * @returns The network stats. + */ +export function useNetworkStats(network: string): [string] { + const isMounted = useIsMounted(); + const [apiClient] = useState(ServiceFactory.get(`api-client-${NOVA}`)); + const [updateTimerId, setUpdateTimerId] = useState(null); + const [blocksPerSecond, setBlocksPerSecond] = useState("--"); + + useEffect(() => { + if (network) { + updateNetworkStats(); + } + + return () => { + if (updateTimerId) { + clearTimeout(updateTimerId); + setUpdateTimerId(null); + } + }; + }, [network]); + + const updateNetworkStats = () => { + if (isMounted && apiClient && network) { + apiClient + .stats({ + network, + includeHistory: true, + }) + .then((ips) => { + const itemsPerSecond = ips.itemsPerSecond ?? 0; + setBlocksPerSecond(itemsPerSecond >= 0 ? itemsPerSecond.toFixed(2) : "--"); + }) + .catch((err) => { + console.error(err); + }) + .finally(() => { + setUpdateTimerId(setTimeout(async () => updateNetworkStats(), 4000)); + }); + } + }; + + return [blocksPerSecond]; +} 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 3/3] 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; + }; +}