Skip to content

Commit

Permalink
feat: Add controls feature to tweak the visualizer easier (#1180)
Browse files Browse the repository at this point in the history
* feat(visualizer): randomize sinusoidal period times

* chore: remove comments

* feat(visualizer): randomize sinusoid amplitudes

* refactor: rename HALF_PERIOD constants to PERIOD

* feat(visualizer): add tangle tilting factor

* feat: improve spray

* feat: controls for visualizer (PoC)

* feat: add all fields, cover by feature flag.

* feat: control values by list.

* feat: add "reset" btn.

Signed-off-by: Eugene Panteleymonchuk <[email protected]>

* fix: camera zoom

* fix: import `features` bug

Signed-off-by: Eugene Panteleymonchuk <[email protected]>

* feat: dynamic zoom.

Signed-off-by: Eugene Panteleymonchuk <[email protected]>

* fix: clean code.

Signed-off-by: Eugene Panteleymonchuk <[email protected]>

* fix: spray shape

* feat: commit suggestion for client/src/features/visualizer-threejs/ConfigControls.tsx

Co-authored-by: JCNoguera <[email protected]>

* feat: commit suggestion for client/src/features/visualizer-threejs/ConfigControls.tsx

Co-authored-by: JCNoguera <[email protected]>

* fix: review comments

Signed-off-by: Eugene Panteleymonchuk <[email protected]>

* fix: add label color for dark mode.

Signed-off-by: Eugene Panteleymonchuk <[email protected]>

* feat: add emitterSpeedMultiplier

Signed-off-by: Eugene Panteleymonchuk <[email protected]>

* fix: change types.

Signed-off-by: Eugene Panteleymonchuk <[email protected]>

* feat: move visualizer config controls to bottom

---------

Signed-off-by: Eugene Panteleymonchuk <[email protected]>
Co-authored-by: JCNoguera <[email protected]>
Co-authored-by: JCNoguera <[email protected]>
Co-authored-by: Begoña Álvarez de la Cruz <[email protected]>
  • Loading branch information
4 people authored Feb 27, 2024
1 parent 837c0be commit 0876eb9
Show file tree
Hide file tree
Showing 9 changed files with 416 additions and 64 deletions.
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.
*/
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 (
<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

0 comments on commit 0876eb9

Please sign in to comment.