-
Notifications
You must be signed in to change notification settings - Fork 22
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: Add controls feature to tweak the visualizer easier (#1180)
* 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
1 parent
837c0be
commit 0876eb9
Showing
9 changed files
with
416 additions
and
64 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
47 changes: 47 additions & 0 deletions
47
client/src/features/visualizer-threejs/ConfigControls.scss
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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
261
client/src/features/visualizer-threejs/ConfigControls.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.