有没有办法在 ThreeJs 中使用 dat.gui.js 构建范围滑块?
Is there a way to build range slider with dat.gui.js in ThreeJs?
我想在我的设计中有一个范围滑块,虽然我有一个数字滑块,但我想要 select 一个要传递的数字范围,如下图所示。
除了在 dat.gui.js 文件中进行更改并添加范围滑块功能之外,还有其他选择吗?
不,那是不可能的。控制器的创建完全基于 value-type 其中 is handled here。您将不得不破解它,制作一个 pull-request 或只使用两个滑块 (min/max)。
我已经在 React 中实现了一个 dat-gui 范围滑块(使用 material-ui)。也许这会对尝试创建自己的人有所帮助,所以我想我会 post 在这里。 Here's what the slider looks like
import React, { useEffect, useRef, useCallback } from 'react';
import cx from 'classnames';
import { makeStyles, Theme, createStyles } from '@material-ui/core';
import clamp from 'lodash/clamp';
import useEventCallback from 'utils/useEventCallback';
function asc(a: number, b: number) {
return a - b;
}
function findClosest(values: any, currentValue: number) {
const { index: closestIndex } = values.reduce((acc: { distance: number; } | null, value: number, index: number) => {
const distance = Math.abs(currentValue - value);
if (acc === null || distance < acc.distance || distance === acc.distance) {
return {
distance,
index,
};
}
return acc;
}, null);
return closestIndex;
}
function valueToPercent(value: number, min: number, max: number) {
return ((value - min) * 100) / (max - min);
}
function percentToValue(percent: number, min: number, max: number) {
return (max - min) * percent + min;
}
function getDecimalPrecision(num: number) {
// This handles the case when num is very small (0.00000001), js will turn this into 1e-8.
// When num is bigger than 1 or less than -1 it won't get converted to this notation so it's fine.
if (Math.abs(num) < 1) {
const parts = num.toExponential().split('e-');
const matissaDecimalPart = parts[0].split('.')[1];
return (
(matissaDecimalPart ? matissaDecimalPart.length : 0) +
parseInt(parts[1], 10)
);
}
const decimalPart = num.toString().split('.')[1];
return decimalPart ? decimalPart.length : 0;
}
function roundValueToStep(value: number, step: number) {
const nearest = Math.round(value / step) * step;
return Number(nearest.toFixed(getDecimalPrecision(step)));
}
function setValueIndex({ values, source, newValue, index }: any) {
// Performance shortcut
if (values[index] === newValue) {
return source;
}
const output = [...values];
output[index] = newValue;
return output;
}
const axisProps = {
offset: (percent: number) => ({ left: `${percent}%` }),
leap: (percent: number) => ({ width: `${percent}%` }),
};
const trackMouse = (event: React.MouseEvent) => ({ x: event.clientX, y: event.clientY, });
const useStyles = makeStyles((theme: Theme) =>
createStyles({
root: {
width: '100%',
boxSizing: 'border-box',
display: 'inline-block',
cursor: 'ew-resize',
touchAction: 'none',
border: '3px solid #1a1a1a',
},
slider: {
display: 'block',
position: 'relative',
backgroundColor: '#141414',
backgroundImage: `linear-gradient(90deg, ${theme.palette.primary.light}, ${theme.palette.primary.light})`, //#2fa1d6, #2fa1d6)",
backgroundRepeat: 'no-repeat',
height: '14px',
},
})
)
interface Props {
values: number[];
min: number;
max: number;
step: number;
className?: string;
color?: string;
defaultValue?: number[];
disabled?: boolean;
onChange?: (event: React.MouseEvent, value: any) => void;
onChangeCommitted?: (event: React.MouseEvent, value: any) => void;
onMouseDown?: (event: React.MouseEvent) => void;
}
export default function RangeSlider({
className,
color = 'primary',
defaultValue,
disabled = false,
max,
min,
onChange,
onChangeCommitted,
onMouseDown,
step,
values: valuesProp,
...other
}: Props) {
const classes = useStyles();
const sliderRef = useRef<any>();
const previousIndex = useRef<any>();
let values = [...valuesProp].sort(asc);
values = values.map((value: number) => clamp(value, min, max));
const getNewValue = useCallback(
({ mouse, move = false, values: values2, source }) => {
const { current: slider } = sliderRef;
const { width, left } = slider.getBoundingClientRect();
const percent = (mouse.x - left) / width;
let newValue;
newValue = percentToValue(percent, min, max);
newValue = roundValueToStep(newValue, step);
newValue = clamp(newValue, min, max);
let activeIndex = 0;
if (!move) {
activeIndex = findClosest(values2, newValue);
} else {
activeIndex = previousIndex.current;
}
const previousValue = newValue;
newValue = setValueIndex({
values: values2,
source,
newValue,
index: activeIndex,
}).sort(asc);
activeIndex = newValue.indexOf(previousValue);
previousIndex.current = activeIndex;
return { newValue, activeIndex };
},
[max, min, step]
);
const handleMouseMove = useEventCallback((event: React.MouseEvent) => {
const mouse = trackMouse(event);
const { newValue } = getNewValue({
mouse,
move: true,
values,
source: valuesProp,
});
if (onChange) {
onChange(event, newValue);
}
});
const handleMouseEnter = useEventCallback((event: React.MouseEvent) => {
// If the slider was being interacted with but the mouse went off the window
// and then re-entered while unclicked then end the interaction.
if (event.buttons === 0) {
handleMouseEnd(event);
}
});
const handleMouseEnd = useEventCallback((event: React.MouseEvent) => {
const mouse = trackMouse(event);
const { newValue } = getNewValue({
mouse,
values,
source: valuesProp,
});
if (onChangeCommitted) {
onChangeCommitted(event, newValue);
}
window.removeEventListener('mousemove', handleMouseMove);
window.removeEventListener('mouseup', handleMouseEnd);
window.removeEventListener('mouseenter', handleMouseEnter);
});
useEffect(() => {
return () => {
window.removeEventListener('mousemove', handleMouseMove);
window.removeEventListener('mouseup', handleMouseEnd);
window.removeEventListener('mouseenter', handleMouseEnter);
};
}, [disabled, handleMouseEnter, handleMouseEnd, handleMouseMove]);
const handleMouseDown = useEventCallback((event: React.MouseEvent) => {
if (onMouseDown) {
onMouseDown(event);
}
if (disabled) {
return;
}
event.preventDefault();
const mouse = trackMouse(event);
const { newValue } = getNewValue({
mouse,
values,
source: valuesProp,
});
if (onChange) {
onChange(event, newValue);
}
window.addEventListener('mousemove', handleMouseMove);
window.addEventListener('mouseenter', handleMouseEnter);
window.addEventListener('mouseup', handleMouseEnd);
});
const sliderOffset = valueToPercent(values[0], min, max);
const sliderLeap = valueToPercent(values[values.length - 1], min, max) - sliderOffset;
const widthBackground = axisProps.leap(sliderLeap).width;
const sliderStyle = {
...axisProps.offset(sliderOffset),
...axisProps.leap(sliderLeap),
backgroundSize: `${widthBackground}% 100%`,
};
return (
<span
ref={sliderRef}
className={cx(classes.root, className)}
onMouseDown={handleMouseDown}
{...other}
>
<span className={classes.slider} style={sliderStyle} />
{values.map((value, index) => {
const percent = valueToPercent(value, min, max);
const style = axisProps.offset(percent);
return (
<span
key={index}
role="slider"
style={style}
data-index={index}
/>
);
})}
</span>
);
}
RangeSlider.defaultProps = {
min: 0,
max: 100,
step: 1
}
我想在我的设计中有一个范围滑块,虽然我有一个数字滑块,但我想要 select 一个要传递的数字范围,如下图所示。
除了在 dat.gui.js 文件中进行更改并添加范围滑块功能之外,还有其他选择吗?
不,那是不可能的。控制器的创建完全基于 value-type 其中 is handled here。您将不得不破解它,制作一个 pull-request 或只使用两个滑块 (min/max)。
我已经在 React 中实现了一个 dat-gui 范围滑块(使用 material-ui)。也许这会对尝试创建自己的人有所帮助,所以我想我会 post 在这里。 Here's what the slider looks like
import React, { useEffect, useRef, useCallback } from 'react';
import cx from 'classnames';
import { makeStyles, Theme, createStyles } from '@material-ui/core';
import clamp from 'lodash/clamp';
import useEventCallback from 'utils/useEventCallback';
function asc(a: number, b: number) {
return a - b;
}
function findClosest(values: any, currentValue: number) {
const { index: closestIndex } = values.reduce((acc: { distance: number; } | null, value: number, index: number) => {
const distance = Math.abs(currentValue - value);
if (acc === null || distance < acc.distance || distance === acc.distance) {
return {
distance,
index,
};
}
return acc;
}, null);
return closestIndex;
}
function valueToPercent(value: number, min: number, max: number) {
return ((value - min) * 100) / (max - min);
}
function percentToValue(percent: number, min: number, max: number) {
return (max - min) * percent + min;
}
function getDecimalPrecision(num: number) {
// This handles the case when num is very small (0.00000001), js will turn this into 1e-8.
// When num is bigger than 1 or less than -1 it won't get converted to this notation so it's fine.
if (Math.abs(num) < 1) {
const parts = num.toExponential().split('e-');
const matissaDecimalPart = parts[0].split('.')[1];
return (
(matissaDecimalPart ? matissaDecimalPart.length : 0) +
parseInt(parts[1], 10)
);
}
const decimalPart = num.toString().split('.')[1];
return decimalPart ? decimalPart.length : 0;
}
function roundValueToStep(value: number, step: number) {
const nearest = Math.round(value / step) * step;
return Number(nearest.toFixed(getDecimalPrecision(step)));
}
function setValueIndex({ values, source, newValue, index }: any) {
// Performance shortcut
if (values[index] === newValue) {
return source;
}
const output = [...values];
output[index] = newValue;
return output;
}
const axisProps = {
offset: (percent: number) => ({ left: `${percent}%` }),
leap: (percent: number) => ({ width: `${percent}%` }),
};
const trackMouse = (event: React.MouseEvent) => ({ x: event.clientX, y: event.clientY, });
const useStyles = makeStyles((theme: Theme) =>
createStyles({
root: {
width: '100%',
boxSizing: 'border-box',
display: 'inline-block',
cursor: 'ew-resize',
touchAction: 'none',
border: '3px solid #1a1a1a',
},
slider: {
display: 'block',
position: 'relative',
backgroundColor: '#141414',
backgroundImage: `linear-gradient(90deg, ${theme.palette.primary.light}, ${theme.palette.primary.light})`, //#2fa1d6, #2fa1d6)",
backgroundRepeat: 'no-repeat',
height: '14px',
},
})
)
interface Props {
values: number[];
min: number;
max: number;
step: number;
className?: string;
color?: string;
defaultValue?: number[];
disabled?: boolean;
onChange?: (event: React.MouseEvent, value: any) => void;
onChangeCommitted?: (event: React.MouseEvent, value: any) => void;
onMouseDown?: (event: React.MouseEvent) => void;
}
export default function RangeSlider({
className,
color = 'primary',
defaultValue,
disabled = false,
max,
min,
onChange,
onChangeCommitted,
onMouseDown,
step,
values: valuesProp,
...other
}: Props) {
const classes = useStyles();
const sliderRef = useRef<any>();
const previousIndex = useRef<any>();
let values = [...valuesProp].sort(asc);
values = values.map((value: number) => clamp(value, min, max));
const getNewValue = useCallback(
({ mouse, move = false, values: values2, source }) => {
const { current: slider } = sliderRef;
const { width, left } = slider.getBoundingClientRect();
const percent = (mouse.x - left) / width;
let newValue;
newValue = percentToValue(percent, min, max);
newValue = roundValueToStep(newValue, step);
newValue = clamp(newValue, min, max);
let activeIndex = 0;
if (!move) {
activeIndex = findClosest(values2, newValue);
} else {
activeIndex = previousIndex.current;
}
const previousValue = newValue;
newValue = setValueIndex({
values: values2,
source,
newValue,
index: activeIndex,
}).sort(asc);
activeIndex = newValue.indexOf(previousValue);
previousIndex.current = activeIndex;
return { newValue, activeIndex };
},
[max, min, step]
);
const handleMouseMove = useEventCallback((event: React.MouseEvent) => {
const mouse = trackMouse(event);
const { newValue } = getNewValue({
mouse,
move: true,
values,
source: valuesProp,
});
if (onChange) {
onChange(event, newValue);
}
});
const handleMouseEnter = useEventCallback((event: React.MouseEvent) => {
// If the slider was being interacted with but the mouse went off the window
// and then re-entered while unclicked then end the interaction.
if (event.buttons === 0) {
handleMouseEnd(event);
}
});
const handleMouseEnd = useEventCallback((event: React.MouseEvent) => {
const mouse = trackMouse(event);
const { newValue } = getNewValue({
mouse,
values,
source: valuesProp,
});
if (onChangeCommitted) {
onChangeCommitted(event, newValue);
}
window.removeEventListener('mousemove', handleMouseMove);
window.removeEventListener('mouseup', handleMouseEnd);
window.removeEventListener('mouseenter', handleMouseEnter);
});
useEffect(() => {
return () => {
window.removeEventListener('mousemove', handleMouseMove);
window.removeEventListener('mouseup', handleMouseEnd);
window.removeEventListener('mouseenter', handleMouseEnter);
};
}, [disabled, handleMouseEnter, handleMouseEnd, handleMouseMove]);
const handleMouseDown = useEventCallback((event: React.MouseEvent) => {
if (onMouseDown) {
onMouseDown(event);
}
if (disabled) {
return;
}
event.preventDefault();
const mouse = trackMouse(event);
const { newValue } = getNewValue({
mouse,
values,
source: valuesProp,
});
if (onChange) {
onChange(event, newValue);
}
window.addEventListener('mousemove', handleMouseMove);
window.addEventListener('mouseenter', handleMouseEnter);
window.addEventListener('mouseup', handleMouseEnd);
});
const sliderOffset = valueToPercent(values[0], min, max);
const sliderLeap = valueToPercent(values[values.length - 1], min, max) - sliderOffset;
const widthBackground = axisProps.leap(sliderLeap).width;
const sliderStyle = {
...axisProps.offset(sliderOffset),
...axisProps.leap(sliderLeap),
backgroundSize: `${widthBackground}% 100%`,
};
return (
<span
ref={sliderRef}
className={cx(classes.root, className)}
onMouseDown={handleMouseDown}
{...other}
>
<span className={classes.slider} style={sliderStyle} />
{values.map((value, index) => {
const percent = valueToPercent(value, min, max);
const style = axisProps.offset(percent);
return (
<span
key={index}
role="slider"
style={style}
data-index={index}
/>
);
})}
</span>
);
}
RangeSlider.defaultProps = {
min: 0,
max: 100,
step: 1
}