Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Add controls feature to tweak the visualizer easier #1180

Merged
merged 34 commits into from
Feb 27, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
34 commits
Select commit Hold shift + click to select a range
4621302
feat(visualizer): randomize sinusoidal period times
VmMad Feb 21, 2024
d474eab
chore: remove comments
VmMad Feb 21, 2024
02bb2c3
feat(visualizer): randomize sinusoid amplitudes
VmMad Feb 21, 2024
bf80194
refactor: rename HALF_PERIOD constants to PERIOD
VmMad Feb 22, 2024
8d6c88d
Merge branch 'feat/nova-visualizer/randomize-period' into feat/nova-v…
VmMad Feb 22, 2024
58bd427
Merge branch 'dev' into feat/nova-visualizer/paused-data-output
VmMad Feb 22, 2024
f3013c7
Merge branch 'dev' into feat/nova-visualizer/randomize-amplitude
VmMad Feb 22, 2024
df6d034
feat(visualizer): add tangle tilting factor
VmMad Feb 22, 2024
ca31490
Merge branch 'feat/nova-visualizer/randomize-amplitude' into feat/nov…
VmMad Feb 22, 2024
0fa4dc0
Merge branch 'dev' into feat/nova-visualizer/add-emitter-tilting
VmMad Feb 22, 2024
ac9bd5d
feat: improve spray
VmMad Feb 22, 2024
9cefde3
feat: controls for visualizer (PoC)
panteleymonchuk Feb 22, 2024
9778b0b
feat: add all fields, cover by feature flag.
panteleymonchuk Feb 23, 2024
2bd7073
feat: control values by list.
panteleymonchuk Feb 23, 2024
1d9fb10
feat: add "reset" btn.
panteleymonchuk Feb 23, 2024
eb79ebf
fix: camera zoom
VmMad Feb 23, 2024
fb0d405
Merge branch 'dev' into feat/nova-visualizer/add-emitter-tilting
VmMad Feb 23, 2024
0dd05cc
fix: import `features` bug
panteleymonchuk Feb 23, 2024
d2c64d9
Merge remote-tracking branch 'origin/feat/nova-visualizer/add-emitter…
panteleymonchuk Feb 23, 2024
e5371fe
feat: dynamic zoom.
panteleymonchuk Feb 23, 2024
d58ec08
fix: clean code.
panteleymonchuk Feb 23, 2024
2d7b6a1
fix: spray shape
VmMad Feb 23, 2024
79214b7
feat: commit suggestion for client/src/features/visualizer-threejs/Co…
panteleymonchuk Feb 26, 2024
d5947d4
feat: commit suggestion for client/src/features/visualizer-threejs/Co…
panteleymonchuk Feb 26, 2024
e04a744
fix: review comments
panteleymonchuk Feb 26, 2024
cba1854
Merge remote-tracking branch 'origin/feat/nova-visualizer/add-emitter…
panteleymonchuk Feb 26, 2024
c21b1d4
Merge branch 'dev' into feat/nova-visualizer/add-emitter-tilting
begonaalvarezd Feb 26, 2024
b63059c
Merge remote-tracking branch 'origin/feat/nova-visualizer/add-emitter…
panteleymonchuk Feb 26, 2024
561e632
fix: add label color for dark mode.
panteleymonchuk Feb 26, 2024
a8d945d
Merge branch 'dev' into feat/issues-1171-add-controls-for-visualizer
panteleymonchuk Feb 26, 2024
7b132b2
feat: add emitterSpeedMultiplier
panteleymonchuk Feb 22, 2024
7c32232
fix: change types.
panteleymonchuk Feb 27, 2024
eee1634
Merge remote-tracking branch 'origin/dev' into feat/issues-1171-add-c…
begonaalvarezd Feb 27, 2024
3cbb37c
feat: move visualizer config controls to bottom
begonaalvarezd Feb 27, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions client/src/features/visualizer-threejs/CameraControls.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,14 +9,28 @@ import { VISUALIZER_PADDINGS } from "./constants";
const CAMERA_ANGLES = getCameraAngles();

const CameraControls = () => {
const { camera } = useThree();
const controls = React.useRef<DreiCameraControls>(null);
const [shouldLockZoom, setShouldLockZoom] = useState<boolean>(false);

const scene = useThree((state) => state.scene);
const zoom = useTangleStore((state) => state.zoom);
const forcedZoom = useTangleStore((state) => state.forcedZoom);
const mesh = scene.getObjectByName(CanvasElement.TangleWrapperMesh) as THREE.Mesh | undefined;
const canvasDimensions = useConfigStore((state) => state.dimensions);

useEffect(() => {
if (!forcedZoom) return;

(async () => {
if (camera && controls.current) {
controls.current.minZoom = forcedZoom;
controls.current.minZoom = forcedZoom;
await controls.current.zoomTo(forcedZoom, true);
}
})();
}, [forcedZoom]);

/**
* Fits the camera to the TangleMesh.
*/
Expand Down
47 changes: 47 additions & 0 deletions client/src/features/visualizer-threejs/ConfigControls.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
.controls-container {
font-family:
"Metropolis Regular",
-apple-system,
BlinkMacSystemFont,
"Segoe UI",
Roboto,
Helvetica,
Arial,
sans-serif,
"Apple Color Emoji",
"Segoe UI Emoji",
"Segoe UI Symbol";
background: var(--body-background);
border: 1px solid var(--border-color);
padding: 8px 16px;
border-radius: 8px;
.controls__list {
color: var(--type-color);
display: flex;
gap: 8px;
flex-wrap: wrap;
}
.controls__item {
width: 20%;
display: flex;
flex-direction: column;

input {
width: 100%;
}
}
.controls__error {
font-size: 12px;
margin-top: 4px;
}
.controls__actions {
margin-top: 16px;
display: flex;
gap: 8px;
}

input {
background: var(--body-background);
padding: 8px 16px;
}
}
261 changes: 261 additions & 0 deletions client/src/features/visualizer-threejs/ConfigControls.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,261 @@
import React, { useState } from "react";
import {
MIN_SINUSOID_PERIOD,
MAX_SINUSOID_PERIOD,
MIN_SINUSOID_AMPLITUDE,
MAX_SINUSOID_AMPLITUDE,
MIN_TILT_FACTOR_DEGREES,
MAX_TILT_FACTOR_DEGREES,
TILT_DURATION_SECONDS,
EMITTER_SPEED_MULTIPLIER,
features,
} from "./constants";
import { useTangleStore } from "~features/visualizer-threejs/store";
import "./ConfigControls.scss";

enum VisualizerConfig {
MinSinusoidPeriod = "minSinusoidPeriod",
MaxSinusoidPeriod = "maxSinusoidPeriod",
MinSinusoidAmplitude = "minSinusoidAmplitude",
MaxSinusoidAmplitude = "maxSinusoidAmplitude",
MinTiltDegrees = "minTiltDegrees",
MaxTiltDegrees = "maxTiltDegrees",
TiltDurationSeconds = "tiltDurationSeconds",
EmitterSpeedMultiplier = "emitterSpeedMultiplier",
}

const VISUALIZER_CONFIG_LOCAL_STORAGE_KEY = "visualizerConfigs";

const DEFAULT_VISUALIZER_CONFIG_VALUES: Record<VisualizerConfig, number> = {
[VisualizerConfig.MinSinusoidPeriod]: MIN_SINUSOID_PERIOD,
[VisualizerConfig.MaxSinusoidPeriod]: MAX_SINUSOID_PERIOD,
[VisualizerConfig.MinSinusoidAmplitude]: MIN_SINUSOID_AMPLITUDE,
[VisualizerConfig.MaxSinusoidAmplitude]: MAX_SINUSOID_AMPLITUDE,
[VisualizerConfig.MinTiltDegrees]: MIN_TILT_FACTOR_DEGREES,
[VisualizerConfig.MaxTiltDegrees]: MAX_TILT_FACTOR_DEGREES,
[VisualizerConfig.TiltDurationSeconds]: TILT_DURATION_SECONDS,
[VisualizerConfig.EmitterSpeedMultiplier]: EMITTER_SPEED_MULTIPLIER,
};

/**
* Retrieves a value from localStorage and parses it as JSON.
*/
VmMad marked this conversation as resolved.
Show resolved Hide resolved
export const getVisualizerConfigValues = (): Record<VisualizerConfig, number> => {
if (features.controlsVisualiserEnabled) {
const item = localStorage.getItem(VISUALIZER_CONFIG_LOCAL_STORAGE_KEY);
return item ? JSON.parse(item) : DEFAULT_VISUALIZER_CONFIG_VALUES;
} else {
localStorage.removeItem(VISUALIZER_CONFIG_LOCAL_STORAGE_KEY);
return DEFAULT_VISUALIZER_CONFIG_VALUES;
}
};

/**
* Saves a value to localStorage as a JSON string.
*/
function setToLocalStorage(value: Record<VisualizerConfig, number>) {
localStorage.setItem(VISUALIZER_CONFIG_LOCAL_STORAGE_KEY, JSON.stringify(value));
}

/**
* Checks if config for visualizer inputs exists in localStorage.
*/
function controlsExistInLocalStorage(): boolean {
return !!localStorage.getItem(VISUALIZER_CONFIG_LOCAL_STORAGE_KEY);
}

export const ConfigControls = () => {
const forcedZoom = useTangleStore((state) => state.forcedZoom);
const setForcedZoom = useTangleStore((state) => state.setForcedZoom);
const forcedZoomInit = forcedZoom !== undefined ? String(forcedZoom) : forcedZoom;
const [localZoom, setLocalZoom] = useState<string | undefined>(forcedZoomInit);

const [visualizerConfigValues, setVisualizerConfigValues] = useState<Record<VisualizerConfig, number>>(() => {
return getVisualizerConfigValues() || DEFAULT_VISUALIZER_CONFIG_VALUES; // Use getFromLocalStorage to retrieve the state
});
const [showResetButton, setShowResetButton] = useState(() => {
return controlsExistInLocalStorage();
});

const [errors, setErrors] = useState<{
[k: string]: string;
}>({});

const inputs: {
key: VisualizerConfig;
label: string;
min: number;
max: number;
}[] = [
{
key: VisualizerConfig.MinSinusoidPeriod,
label: "Min sinusoid period",
min: 1,
max: 7,
},
{
key: VisualizerConfig.MaxSinusoidPeriod,
label: "Max sinusoid period",
min: 8,
max: 15,
},
{
key: VisualizerConfig.MinSinusoidAmplitude,
label: "Min sinusoid amplitude",
min: 50,
max: 199,
},
{
key: VisualizerConfig.MaxSinusoidAmplitude,
label: "Max sinusoid amplitude",
min: 200,
max: 500,
},
{
key: VisualizerConfig.MinTiltDegrees,
label: "Min tilt factor degrees",
min: 0,
max: 90,
},
{
key: VisualizerConfig.MaxTiltDegrees,
label: "Max tilt factor degrees",
min: 0,
max: 90,
},
{
key: VisualizerConfig.TiltDurationSeconds,
label: "Tilt duration (seconds)",
min: 1,
max: 100,
},
{
key: VisualizerConfig.EmitterSpeedMultiplier,
label: "Emitter Speed Multiplier",
min: 0,
max: 1000,
},
];

const handleApply = () => {
if (Object.keys(errors).some((key) => errors[key])) {
// Handle the error case, e.g., display a message
console.error("There are errors in the form.");
return;
}

setToLocalStorage(visualizerConfigValues);
location.reload();
};

const handleChange = (key: VisualizerConfig, val: string) => {
const input = inputs.find((input) => input.key === key);
if (!input) return;

if (!val) {
setErrors((prevErrors) => ({ ...prevErrors, [key]: "Value is required" }));
setVisualizerConfigValues((prevState) => ({ ...prevState, [key]: "" }));
return;
}

const numericValue = Number(val);
if (numericValue < input.min || numericValue > input.max) {
setErrors((prevErrors) => ({ ...prevErrors, [key]: `Value must be between ${input.min} and ${input.max}` }));
} else {
setErrors((prevErrors) => ({ ...prevErrors, [key]: "" }));
}

setVisualizerConfigValues((prevState) => ({ ...prevState, [key]: numericValue }));
};

if (!features.controlsVisualiserEnabled) {
return null;
}

return (
VmMad marked this conversation as resolved.
Show resolved Hide resolved
<div className={"controls-container"}>
<div className="controls__list">
{inputs.map((i) => {
return (
<div key={i.key} className="controls__item">
<label>{i.label}</label>
<input
type="number"
value={visualizerConfigValues[i.key]}
onChange={(e) => handleChange(i.key, e.target.value)}
/>
{!!errors[i.key] && <div className={"controls__error"}>{errors[i.key]}</div>}
</div>
);
})}
</div>

<div className="controls__actions">
<button type={"button"} onClick={handleApply}>
Apply
</button>
{showResetButton && (
<button
onClick={() => {
localStorage.removeItem(VISUALIZER_CONFIG_LOCAL_STORAGE_KEY);
setShowResetButton(false);
location.reload();
}}
>
Reset
</button>
)}
</div>

<div className="controls__list">
<div style={{ marginTop: "16px" }} className={"controls__item"}>
<label style={{ display: "block" }}>Zoom</label>
<input
value={localZoom === undefined ? "" : localZoom}
onChange={(e) => {
const input = e.target.value;
setErrors((prevErrors) => ({ ...prevErrors, zoom: "" }));

if (!input) {
setLocalZoom(undefined);
return;
}

const numberRegExp = /^-?\d+(\.|\.\d*|\d*)?$/;
if (numberRegExp.test(input)) {
if (input.endsWith(".")) {
setLocalZoom(input);
} else {
const value = parseFloat(input);
if (value > 2) {
setErrors((prevErrors) => ({ ...prevErrors, zoom: "Value must be between 0 and 2" }));

setLocalZoom(String(2));
return;
}
setLocalZoom(input);
}
}
}}
/>
{!!errors["zoom"] && <div className={"controls__error"}>{errors["zoom"]}</div>}
<div className="controls__actions">
<button
type={"button"}
onClick={() => {
if (localZoom === undefined || localZoom.endsWith(".")) {
return;
}
setForcedZoom(parseFloat(localZoom));
}}
>
Apply
</button>
</div>
</div>
</div>
</div>
);
};

export default React.memo(ConfigControls);
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { RouteComponentProps } from "react-router-dom";
import * as THREE from "three";
import {
FAR_PLANE,
features,
NEAR_PLANE,
DIRECTIONAL_LIGHT_INTENSITY,
PENDING_BLOCK_COLOR,
Expand Down Expand Up @@ -34,11 +35,6 @@ import useSearchStore from "~features/visualizer-threejs/store/search";
import { useSearch } from "~features/visualizer-threejs/hooks/useSearch";
import "./Visualizer.scss";

const features = {
statsEnabled: false,
cameraControls: true,
};

const VisualizerInstance: React.FC<RouteComponentProps<VisualizerRouteProps>> = ({
match: {
params: { network },
Expand Down
6 changes: 6 additions & 0 deletions client/src/features/visualizer-threejs/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -100,3 +100,9 @@ export const NUMBER_OF_RANDOM_TILTINGS = 100;
export const TILT_DURATION_SECONDS = 4;
export const MAX_TILT_FACTOR_DEGREES = 16;
export const MIN_TILT_FACTOR_DEGREES = 1;

export const features = {
statsEnabled: false,
cameraControls: true,
controlsVisualiserEnabled: true,
};
Loading
Loading