summaryrefslogtreecommitdiff
path: root/src/ui
diff options
context:
space:
mode:
Diffstat (limited to 'src/ui')
-rw-r--r--src/ui/App.jsx10
-rw-r--r--src/ui/Controls.jsx171
-rw-r--r--src/ui/TooltipSlider.tsx95
3 files changed, 272 insertions, 4 deletions
diff --git a/src/ui/App.jsx b/src/ui/App.jsx
index eaca9a4..fbbbd35 100644
--- a/src/ui/App.jsx
+++ b/src/ui/App.jsx
@@ -7,6 +7,7 @@ import * as React from "react";
import { useState, useEffect, useRef } from "react";
import Relabi from "../relabi";
import { Kalimba, Drums } from "../lib/instruments";
+import Controls from "./Controls.jsx";
export default function App() {
const [relabi, setRelabi] = useState();
@@ -19,10 +20,10 @@ export default function App() {
if (!relabi) {
const relabiGenerator = new Relabi({
waves: [
- { type: "sine", frequency: 0.75 },
- { type: "sine", frequency: 1.0 },
- { type: "sine", frequency: 1.617 },
- { type: "sine", frequency: 3.141 },
+ { shape: "sine", frequency: 0.75 },
+ { shape: "sine", frequency: 1.0 },
+ { shape: "sine", frequency: 1.617 },
+ { shape: "sine", frequency: 3.141 },
],
bounds: [
{
@@ -86,6 +87,7 @@ export default function App() {
}}
>
<div ref={relabiRef} />
+ <Controls relabi={relabi} />
</div>
);
}
diff --git a/src/ui/Controls.jsx b/src/ui/Controls.jsx
new file mode 100644
index 0000000..f999e4f
--- /dev/null
+++ b/src/ui/Controls.jsx
@@ -0,0 +1,171 @@
+/**
+ * Relabi controls
+ * @module src/ui/Controls.jsx;
+ */
+
+import * as React from "react";
+import { useCallback, useRef } from "react";
+
+import Slider from "rc-slider";
+import TooltipSlider, { handleRender } from "./TooltipSlider.tsx";
+import "rc-slider/assets/index.css";
+
+const WAVE_SHAPE_NAMES = ["sine", "triangle", "saw", "square"];
+
+export default function Controls({ relabi }) {
+ /**
+ * Handle updating a slider
+ */
+ const onChange = useCallback(
+ (type, index) => (value) => {
+ if (type === "bound") {
+ relabi.bounds[index].level = value;
+ }
+ if (type === "frequency") {
+ relabi.waves[index].frequency = Math.exp(value);
+ }
+ if (type === "shape") {
+ relabi.waves[index].shape = WAVE_SHAPE_NAMES[value];
+ }
+ },
+ [relabi]
+ );
+
+ return (
+ <ControlRow>
+ <ControlBox>
+ <Label>Bounds</Label>
+ <SliderRow>
+ {relabi?.bounds.map((bound, index) => (
+ <ControlSlider
+ rel={`bound_${index}`}
+ type="bound"
+ index={index}
+ onChange={onChange}
+ min={-1}
+ max={1}
+ step={0.01}
+ defaultValue={bound.level}
+ color={bound.color}
+ reverse
+ />
+ ))}
+ </SliderRow>
+ </ControlBox>
+
+ <ControlBox>
+ <Label>Wave speeds</Label>
+ <SliderRow>
+ {relabi?.waves.map((wave, index) => (
+ <ControlSlider
+ rel={`frequency_${index}`}
+ type="frequency"
+ index={index}
+ onChange={onChange}
+ min={-2}
+ max={3}
+ step={0.001}
+ defaultValue={Math.log(wave.frequency)}
+ color={"#8a8"}
+ />
+ ))}
+ </SliderRow>
+ </ControlBox>
+
+ <ControlBox>
+ <Label>Wave shapes</Label>
+ <SliderRow>
+ {relabi?.waves.map((wave, index) => (
+ <ControlSlider
+ rel={`shape_${index}`}
+ type="shape"
+ index={index}
+ onChange={onChange}
+ min={0}
+ max={3}
+ step={1}
+ defaultValue={WAVE_SHAPE_NAMES.indexOf(wave.shape)}
+ color={"#998"}
+ tipFormatter={(value) => WAVE_SHAPE_NAMES[value]}
+ />
+ ))}
+ </SliderRow>
+ </ControlBox>
+ </ControlRow>
+ );
+}
+
+/**
+ * UI Elements
+ */
+const ControlRow = ({ children }) => (
+ <div
+ style={{
+ display: "flex",
+ flexDirection: "row",
+ marginTop: "1rem",
+ }}
+ >
+ {children}
+ </div>
+);
+
+const ControlBox = ({ children }) => (
+ <div
+ style={{
+ marginLeft: "1rem",
+ }}
+ >
+ {children}
+ </div>
+);
+
+const SliderRow = ({ children }) => (
+ <div
+ style={{
+ display: "flex",
+ flexDireciton: "row",
+ height: "10rem",
+ marginTop: "0.5rem",
+ }}
+ >
+ {children}
+ </div>
+);
+
+const Label = ({ children }) => (
+ <div style={{ fontSize: "0.75rem" }}>{children}</div>
+);
+
+const ControlSlider = ({
+ type,
+ index,
+ min,
+ max,
+ step,
+ reverse,
+ defaultValue,
+ color,
+ onChange,
+ tipFormatter,
+}) => (
+ <div style={{ marginRight: "0.5rem" }}>
+ <TooltipSlider
+ vertical
+ min={min}
+ max={max}
+ step={step}
+ reverse={reverse}
+ defaultValue={defaultValue}
+ onChange={onChange(type, index)}
+ handleStyle={{
+ backgroundColor: color,
+ borderColor: color,
+ opacity: 1,
+ }}
+ trackStyle={{ backgroundColor: "#888", width: "2px" }}
+ railStyle={{ backgroundColor: "#888", width: "2px" }}
+ tipFormatter={tipFormatter || ((value) => value)}
+ />
+ </div>
+);
diff --git a/src/ui/TooltipSlider.tsx b/src/ui/TooltipSlider.tsx
new file mode 100644
index 0000000..0bb8350
--- /dev/null
+++ b/src/ui/TooltipSlider.tsx
@@ -0,0 +1,95 @@
+/**
+ * rc-tooltip tooltip slider
+ */
+
+import * as React from "react";
+import "rc-tooltip/assets/bootstrap.css";
+import Slider from "rc-slider";
+import type { SliderProps } from "rc-slider";
+import raf from "rc-util/lib/raf";
+import Tooltip from "rc-tooltip";
+
+const HandleTooltip = (props: {
+ value: number;
+ children: React.ReactElement;
+ visible: boolean;
+ tipFormatter?: (value: number) => React.ReactNode;
+}) => {
+ const {
+ value,
+ children,
+ visible,
+ tipFormatter = (val) => `${val} %`,
+ ...restProps
+ } = props;
+
+ const tooltipRef = React.useRef<any>();
+ const rafRef = React.useRef<number | null>(null);
+
+ function cancelKeepAlign() {
+ raf.cancel(rafRef.current!);
+ }
+
+ function keepAlign() {
+ rafRef.current = raf(() => {
+ // tooltipRef.current?.forcePopupAlign();
+ });
+ }
+
+ React.useEffect(() => {
+ if (visible) {
+ keepAlign();
+ } else {
+ cancelKeepAlign();
+ }
+
+ return cancelKeepAlign;
+ }, [value, visible]);
+
+ return (
+ <Tooltip
+ placement="top"
+ overlay={tipFormatter(value)}
+ overlayInnerStyle={{ minHeight: "auto" }}
+ ref={tooltipRef}
+ visible={visible}
+ {...restProps}
+ >
+ {children}
+ </Tooltip>
+ );
+};
+
+export const handleRender: SliderProps["handleRender"] = (node, props) => {
+ return (
+ <HandleTooltip value={props.value} visible={props.dragging}>
+ {node}
+ </HandleTooltip>
+ );
+};
+
+const TooltipSlider = ({
+ tipFormatter,
+ tipProps,
+ ...props
+}: SliderProps & {
+ tipFormatter?: (value: number) => React.ReactNode;
+ tipProps: any;
+}) => {
+ const tipHandleRender: SliderProps["handleRender"] = (node, handleProps) => {
+ return (
+ <HandleTooltip
+ value={handleProps.value}
+ visible={handleProps.dragging}
+ tipFormatter={tipFormatter}
+ {...tipProps}
+ >
+ {node}
+ </HandleTooltip>
+ );
+ };
+
+ return <Slider {...props} handleRender={tipHandleRender} />;
+};
+
+export default TooltipSlider;