Skip to content

Commit

Permalink
feat(visualizer): update block colors (#1193)
Browse files Browse the repository at this point in the history
* feat(visualizer): update block colors

* chore: rename visualizer component to NovaVisualizer

* feat: adapt color legend to combile multi colors

---------

Co-authored-by: Begoña Alvarez <[email protected]>
  • Loading branch information
VmMad and begonaalvarezd authored Feb 28, 2024
1 parent 0590809 commit e895097
Show file tree
Hide file tree
Showing 11 changed files with 194 additions and 65 deletions.
2 changes: 1 addition & 1 deletion client/src/app/routes.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ import StardustSearch from "./routes/stardust/Search";
import StardustStatisticsPage from "./routes/stardust/statistics/StatisticsPage";
import StardustTransactionPage from "./routes/stardust/TransactionPage";
import { Visualizer as StardustVisualizer } from "./routes/stardust/Visualizer";
import NovaVisualizer from "../features/visualizer-threejs/VisualizerInstance";
import NovaVisualizer from "../features/visualizer-threejs/NovaVisualizer";
import StreamsV0 from "./routes/StreamsV0";
import { StreamsV0RouteProps } from "./routes/StreamsV0RouteProps";
import { VisualizerRouteProps } from "./routes/VisualizerRouteProps";
Expand Down
11 changes: 11 additions & 0 deletions client/src/features/visualizer-threejs/NovaVisualizer.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import React from "react";
import { useGetThemeMode } from "~/helpers/hooks/useGetThemeMode.js";
import { VisualizerRouteProps } from "~/app/routes/VisualizerRouteProps.js";
import { RouteComponentProps } from "react-router-dom";
import VisualizerInstance from "./VisualizerInstance";

export default function NovaVisualizer(props: RouteComponentProps<VisualizerRouteProps>): React.JSX.Element {
const theme = useGetThemeMode();

return <VisualizerInstance key={theme} {...props} />;
}
49 changes: 28 additions & 21 deletions client/src/features/visualizer-threejs/VisualizerInstance.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,15 +5,7 @@ import { Perf } from "r3f-perf";
import React, { useEffect, useRef } from "react";
import { RouteComponentProps } from "react-router-dom";
import * as THREE from "three";
import {
FAR_PLANE,
features,
NEAR_PLANE,
DIRECTIONAL_LIGHT_INTENSITY,
PENDING_BLOCK_COLOR,
VISUALIZER_BACKGROUND,
BLOCK_STATE_TO_COLOR,
} from "./constants";
import { FAR_PLANE, features, NEAR_PLANE, DIRECTIONAL_LIGHT_INTENSITY, VISUALIZER_BACKGROUND } from "./constants";
import Emitter from "./Emitter";
import { useTangleStore, useConfigStore } from "./store";
import { BPSCounter } from "./BPSCounter";
Expand All @@ -30,7 +22,7 @@ import { IFeedBlockData } from "~/models/api/nova/feed/IFeedBlockData";
import CameraControls from "./CameraControls";
import useVisualizerTimer from "~/helpers/nova/hooks/useVisualizerTimer";
import { getBlockInitPosition, getBlockTargetPosition } from "./blockPositions";
import { getCurrentTiltValue } from "./utils";
import { getBlockColorByState, getCurrentTiltValue } from "./utils";
import useSearchStore from "~features/visualizer-threejs/store/search";
import { useSearch } from "~features/visualizer-threejs/hooks/useSearch";
import "./Visualizer.scss";
Expand Down Expand Up @@ -69,6 +61,9 @@ const VisualizerInstance: React.FC<RouteComponentProps<VisualizerRouteProps>> =
const clickedInstanceId = useTangleStore((s) => s.clickedInstanceId);
const matchingBlockIds = useSearchStore((state) => state.matchingBlockIds);

const blockIdToState = useTangleStore((s) => s.blockIdToState);
const setBlockIdToBlockState = useTangleStore((s) => s.setBlockIdToBlockState);

// Confirmed or accepted blocks by slot
const confirmedBlocksBySlot = useTangleStore((s) => s.confirmedBlocksBySlot);
const addToConfirmedBlocksSlot = useTangleStore((s) => s.addToConfirmedBlocksBySlot);
Expand Down Expand Up @@ -219,7 +214,8 @@ const VisualizerInstance: React.FC<RouteComponentProps<VisualizerRouteProps>> =
bpsCounter.start();
}

blockMetadata.set(blockData.blockId, { ...blockData, treeColor: PENDING_BLOCK_COLOR });
const blockColor = getBlockColorByState(themeMode, "pending");
blockMetadata.set(blockData.blockId, { ...blockData, treeColor: blockColor });

// edges
const blockStrongParents = (blockData.block.body as BasicBlockBody).strongParents ?? [];
Expand All @@ -235,9 +231,11 @@ const VisualizerInstance: React.FC<RouteComponentProps<VisualizerRouteProps>> =
highlightSearchBlock(blockData.blockId);
}

setBlockIdToBlockState(blockData.blockId, "pending");

addBlock({
id: blockData.blockId,
color: PENDING_BLOCK_COLOR,
color: blockColor,
blockAddedTimestamp: currentAnimationTime,
targetPosition,
initPosition,
Expand All @@ -247,22 +245,29 @@ const VisualizerInstance: React.FC<RouteComponentProps<VisualizerRouteProps>> =

function onBlockMetadataUpdate(metadataUpdate: IBlockMetadata): void {
if (metadataUpdate?.blockState) {
const selectedColor = BLOCK_STATE_TO_COLOR.get(metadataUpdate.blockState);
const selectedColor = getBlockColorByState(themeMode, metadataUpdate.blockState);
if (selectedColor) {
const currentBlockMetadata = blockMetadata.get(metadataUpdate.blockId);
if (currentBlockMetadata) {
currentBlockMetadata.treeColor = selectedColor;
}
if (!matchingBlockIds.includes(metadataUpdate.blockId)) {
addToColorQueue(metadataUpdate.blockId, selectedColor);

const previousBlockState = blockIdToState.get(metadataUpdate.blockId);
const wasConfirmedBeforeAccepted = previousBlockState === "accepted" && metadataUpdate.blockState === "confirmed";

if (!wasConfirmedBeforeAccepted) {
setBlockIdToBlockState(metadataUpdate.blockId, metadataUpdate.blockState);
if (!matchingBlockIds.includes(metadataUpdate.blockId)) {
addToColorQueue(metadataUpdate.blockId, selectedColor);
}
}
}

const acceptedStates: BlockState[] = ["confirmed", "accepted"];
const acceptedStates: BlockState[] = ["confirmed", "accepted"];

if (acceptedStates.includes(metadataUpdate.blockState)) {
const slot = Utils.computeSlotIndex(metadataUpdate.blockId);
addToConfirmedBlocksSlot(metadataUpdate.blockId, slot);
if (acceptedStates.includes(metadataUpdate.blockState)) {
const slot = Utils.computeSlotIndex(metadataUpdate.blockId);
addToConfirmedBlocksSlot(metadataUpdate.blockId, slot);
}
}
}
}
Expand All @@ -272,9 +277,10 @@ const VisualizerInstance: React.FC<RouteComponentProps<VisualizerRouteProps>> =

if (blocks?.length) {
blocks.forEach((blockId) => {
const selectedColor = BLOCK_STATE_TO_COLOR.get("finalized");
const selectedColor = getBlockColorByState(themeMode, "finalized");
if (selectedColor) {
addToColorQueue(blockId, selectedColor);
setBlockIdToBlockState(blockId, "finalized");
}
});
}
Expand All @@ -294,6 +300,7 @@ const VisualizerInstance: React.FC<RouteComponentProps<VisualizerRouteProps>> =
setIsPlaying={setIsPlaying}
isEdgeRenderingEnabled={isEdgeRenderingEnabled}
setEdgeRenderingEnabled={(checked) => setEdgeRenderingEnabled(checked)}
themeMode={themeMode}
>
<Canvas
ref={canvasRef}
Expand Down
42 changes: 30 additions & 12 deletions client/src/features/visualizer-threejs/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,19 +21,37 @@ export const SECOND = 1000;
export const DATA_SENDER_TIME_INTERVAL = 500;

// colors
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");
export const SEARCH_RESULT_COLOR = new Color("#C026D3");
export const ACCEPTED_BLOCK_COLORS: Color[] = [new Color("#0101FF"), new Color("#0000DB"), new Color("#0101AB")];
export const CONFIRMED_BLOCK_COLOR = new Color("#3CE5E1");
export const FAILED_BLOCK_COLOR = new Color("#C026D3");
export const REJECTED_BLOCK_COLOR = FAILED_BLOCK_COLOR;
export const SEARCH_RESULT_COLOR = new Color("#1EC15A");
export const HOVERED_BLOCK_COLOR = SEARCH_RESULT_COLOR;

export const BLOCK_STATE_TO_COLOR = new Map<BlockState, Color>([
["pending", PENDING_BLOCK_COLOR],
["accepted", ACCEPTED_BLOCK_COLOR],
["confirmed", CONFIRMED_BLOCK_COLOR],
["finalized", FINALIZED_BLOCK_COLOR],
]);
// colors by theme
export const PENDING_BLOCK_COLOR_LIGHTMODE = new Color("#A6C3FC");
export const PENDING_BLOCK_COLOR_DARKMODE = new Color("#5C84FA");
export const FINALIZED_BLOCK_COLOR_LIGHTMODE = new Color("#5C84FA");
export const FINALIZED_BLOCK_COLOR_DARKMODE = new Color("#000081");

export const THEME_BLOCK_COLORS: Record<ThemeMode, Record<BlockState, Color | Color[]>> = {
[ThemeMode.Dark]: {
accepted: ACCEPTED_BLOCK_COLORS,
pending: PENDING_BLOCK_COLOR_DARKMODE,
confirmed: CONFIRMED_BLOCK_COLOR,
finalized: FINALIZED_BLOCK_COLOR_DARKMODE,
rejected: REJECTED_BLOCK_COLOR,
failed: FAILED_BLOCK_COLOR,
},
[ThemeMode.Light]: {
accepted: ACCEPTED_BLOCK_COLORS,
pending: PENDING_BLOCK_COLOR_LIGHTMODE,
confirmed: CONFIRMED_BLOCK_COLOR,
finalized: FINALIZED_BLOCK_COLOR_LIGHTMODE,
rejected: REJECTED_BLOCK_COLOR,
failed: FAILED_BLOCK_COLOR,
},
};

// emitter
export const EMITTER_SPEED_MULTIPLIER = 150;
Expand Down Expand Up @@ -64,7 +82,7 @@ export const DIRECTIONAL_LIGHT_INTENSITY = 0.45;

export const VISUALIZER_BACKGROUND: Record<ThemeMode, string> = {
[ThemeMode.Dark]: "#000000",
[ThemeMode.Light]: "#f2f2f2",
[ThemeMode.Light]: "#FFFFFF",
};

// emitter
Expand Down
49 changes: 42 additions & 7 deletions client/src/features/visualizer-threejs/store/tangle.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { devtools } from "zustand/middleware";
import { ZOOM_DEFAULT, SPRAY_DISTANCE } from "../constants";
import { IFeedBlockData } from "~models/api/nova/feed/IFeedBlockData";
import { IThreeDimensionalPosition } from "../interfaces";
import { BlockId, SlotIndex } from "@iota/sdk-wasm-nova/web";
import { BlockId, BlockState, SlotIndex } from "@iota/sdk-wasm-nova/web";
import { getVisualizerConfigValues } from "~features/visualizer-threejs/ConfigControls";

export interface IBlockAnimationPosition {
Expand Down Expand Up @@ -80,6 +80,9 @@ interface TangleState {
confirmedBlocksBySlot: Map<number, string[]>;
addToConfirmedBlocksBySlot: (blockId: BlockId, slot: SlotIndex) => void;
removeConfirmedBlocksSlot: (slot: SlotIndex) => void;

blockIdToState: Map<BlockId, BlockState>;
setBlockIdToBlockState: (blockId: BlockId, blockState: BlockState) => void;
}

const INITIAL_STATE = {
Expand All @@ -97,12 +100,32 @@ const INITIAL_STATE = {
bps: 0,
clickedInstanceId: null,
confirmedBlocksBySlot: new Map(),
blockIdToState: new Map(),
};

export const useTangleStore = create<TangleState>()(
devtools((set) => ({
...INITIAL_STATE,
resetConfigState: () => set(INITIAL_STATE),
resetConfigState: () =>
// hard cleanup of the store
set((state) => {
state.blockQueue = [];
state.edgeQueue = [];
state.colorQueue = [];
state.blockIdToEdges = new Map();
state.blockIdToIndex = new Map();
state.blockIdToPosition = new Map();
state.blockMetadata = new Map();
state.blockIdToAnimationPosition = new Map();
state.indexToBlockId = [];
state.zoom = ZOOM_DEFAULT;
state.forcedZoom = undefined;
state.bps = 0;
state.clickedInstanceId = null;
state.confirmedBlocksBySlot = new Map();
state.blockIdToState = new Map();
return state;
}),
updateBlockIdToAnimationPosition: (updatedPositions) => {
set((state) => {
updatedPositions.forEach((value, key) => {
Expand Down Expand Up @@ -196,15 +219,18 @@ export const useTangleStore = create<TangleState>()(
updateBlockIdToIndex: (blockId: string, index: number) => {
set((state) => {
state.blockIdToIndex.set(blockId, index);
if (state.indexToBlockId[index]) {
const previousBlockId = state.indexToBlockId[index];
if (previousBlockId) {
// Clean up map from old blockIds
state.blockIdToIndex.delete(state.indexToBlockId[index]);
state.blockIdToIndex.delete(previousBlockId);
// Clean up old block edges
state.blockIdToEdges.delete(state.indexToBlockId[index]);
state.blockIdToEdges.delete(previousBlockId);
// Clean up old block position
state.blockIdToPosition.delete(state.indexToBlockId[index]);
state.blockIdToPosition.delete(previousBlockId);
// Clean up old block metadata
state.blockMetadata.delete(state.indexToBlockId[index]);
state.blockMetadata.delete(previousBlockId);
// Cleanup old block state
state.blockIdToState.delete(previousBlockId);
}

const nextIndexToBlockId = [...state.indexToBlockId];
Expand Down Expand Up @@ -268,5 +294,14 @@ export const useTangleStore = create<TangleState>()(
};
});
},
setBlockIdToBlockState(blockId, blockState) {
set((state) => {
state.blockIdToState.set(blockId, blockState);
return {
...state,
blockIdToState: state.blockIdToState,
};
});
},
})),
);
12 changes: 12 additions & 0 deletions client/src/features/visualizer-threejs/useRenderTangle.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,8 @@ export const useRenderTangle = () => {
const blockIdToAnimationPosition = useTangleStore((s) => s.blockIdToAnimationPosition);
const updateBlockIdToAnimationPosition = useTangleStore((s) => s.updateBlockIdToAnimationPosition);

const resetTangleStore = useTangleStore((s) => s.resetConfigState);

const getVisualizerTimeDiff = useVisualizerTimer();

const assignBlockToMesh = (block: IBlockState) => {
Expand Down Expand Up @@ -197,4 +199,14 @@ export const useRenderTangle = () => {
setUpdateAnimationPositionQueue(updateAnimationPositionQueue);
}
}, [isPlaying, updateAnimationPositionQueue]);

/**
* Cleanup scene
*/
useEffect(() => {
return () => {
scene.remove(tangleMeshRef.current);
resetTangleStore();
};
}, [scene, tangleMeshRef]);
};
14 changes: 14 additions & 0 deletions client/src/features/visualizer-threejs/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,12 @@ import {
MAX_PREV_POINTS,
MAX_POINT_RETRIES,
MIN_BLOCK_NEAR_RADIUS,
THEME_BLOCK_COLORS,
} from "./constants";
import type { ICameraAngles, ISinusoidalPositionParams, IThreeDimensionalPosition, ITwoDimensionalPosition } from "./interfaces";
import { getVisualizerConfigValues } from "~features/visualizer-threejs/ConfigControls";
import { BlockState } from "@iota/sdk-wasm-nova/web";
import { ThemeMode } from "./enums";

/**
* Generates a random number within a specified range.
Expand Down Expand Up @@ -384,3 +387,14 @@ export function getCurrentTiltValue(animationTime: number, tilts: number[]): num

return currentTilt;
}

export function getBlockColorByState(theme: ThemeMode, blockState: BlockState): THREE.Color {
const targetColor = THEME_BLOCK_COLORS[theme][blockState];

if (Array.isArray(targetColor)) {
const index = randomIntFromInterval(0, targetColor.length - 1);
return targetColor[index];
}

return targetColor;
}
29 changes: 29 additions & 0 deletions client/src/features/visualizer-threejs/wrapper/ColorPanel.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import React, { memo } from "react";

const ColorPanel = ({ label, color }: { label: string; color: string | string[] }): React.JSX.Element => (
<div className="key-panel-item">
{Array.isArray(color) ? (
<div className="key-panel-item-multi-color">
{color.map((c, index) => (
<div
key={`${label}-${index}`}
className="key-marker"
style={{
backgroundColor: c,
}}
/>
))}
</div>
) : (
<div
className="key-marker"
style={{
backgroundColor: color,
}}
/>
)}
<div className="key-label">{label}</div>
</div>
);

export default memo(ColorPanel);
8 changes: 8 additions & 0 deletions client/src/features/visualizer-threejs/wrapper/KeyPanel.scss
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,14 @@
border-radius: 50%;
}

.key-panel-item-multi-color {
display: flex;
flex-direction: row;
.key-marker:not(:last-of-type) {
margin-right: 4px;
}
}

.key-label {
@include font-size(14px);

Expand Down
Loading

0 comments on commit e895097

Please sign in to comment.