424 lines
6.9 KiB
JavaScript
424 lines
6.9 KiB
JavaScript
import { EventDispatcher } from 'three';
|
|
|
|
class Value extends EventDispatcher {
|
|
|
|
constructor() {
|
|
|
|
super();
|
|
|
|
this.domElement = document.createElement( 'div' );
|
|
this.domElement.className = 'param-control';
|
|
|
|
this._onChangeFunction = null;
|
|
|
|
this.addEventListener( 'change', ( e ) => {
|
|
|
|
// defer to avoid issues when changing multiple values in the same call stack
|
|
|
|
requestAnimationFrame( () => {
|
|
|
|
if ( this._onChangeFunction ) this._onChangeFunction( e.value );
|
|
|
|
} );
|
|
|
|
} );
|
|
|
|
}
|
|
|
|
setValue( /*val*/ ) {
|
|
|
|
this.dispatchChange();
|
|
|
|
return this;
|
|
|
|
}
|
|
|
|
getValue() {
|
|
|
|
return null;
|
|
|
|
}
|
|
|
|
dispatchChange() {
|
|
|
|
this.dispatchEvent( { type: 'change', value: this.getValue() } );
|
|
|
|
}
|
|
|
|
onChange( callback ) {
|
|
|
|
this._onChangeFunction = callback;
|
|
|
|
return this;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
class ValueNumber extends Value {
|
|
|
|
constructor( { value = 0, step = 0.1, min = - Infinity, max = Infinity } ) {
|
|
|
|
super();
|
|
|
|
this.input = document.createElement( 'input' );
|
|
this.input.type = 'number';
|
|
this.input.value = value;
|
|
this.input.step = step;
|
|
this.input.min = min;
|
|
this.input.max = max;
|
|
this.input.addEventListener( 'change', this._onChangeValue.bind( this ) );
|
|
this.domElement.appendChild( this.input );
|
|
this.addDragHandler();
|
|
|
|
}
|
|
|
|
_onChangeValue() {
|
|
|
|
const value = parseFloat( this.input.value );
|
|
const min = parseFloat( this.input.min );
|
|
const max = parseFloat( this.input.max );
|
|
|
|
if ( value > max ) {
|
|
|
|
this.input.value = max;
|
|
|
|
} else if ( value < min ) {
|
|
|
|
this.input.value = min;
|
|
|
|
} else if ( isNaN( value ) ) {
|
|
|
|
this.input.value = min;
|
|
|
|
}
|
|
|
|
this.dispatchChange();
|
|
|
|
}
|
|
|
|
addDragHandler() {
|
|
|
|
let isDragging = false;
|
|
let startY, startValue;
|
|
|
|
this.input.addEventListener( 'mousedown', ( e ) => {
|
|
|
|
isDragging = true;
|
|
startY = e.clientY;
|
|
startValue = parseFloat( this.input.value );
|
|
document.body.style.cursor = 'ns-resize';
|
|
|
|
} );
|
|
|
|
document.addEventListener( 'mousemove', ( e ) => {
|
|
|
|
if ( isDragging ) {
|
|
|
|
const deltaY = startY - e.clientY;
|
|
const step = parseFloat( this.input.step ) || 1;
|
|
const min = parseFloat( this.input.min );
|
|
const max = parseFloat( this.input.max );
|
|
|
|
let stepSize = step;
|
|
|
|
if ( ! isNaN( max ) && isFinite( min ) ) {
|
|
|
|
stepSize = ( max - min ) / 100;
|
|
|
|
}
|
|
|
|
const change = deltaY * stepSize;
|
|
|
|
let newValue = startValue + change;
|
|
newValue = Math.max( min, Math.min( newValue, max ) );
|
|
|
|
const precision = ( String( step ).split( '.' )[ 1 ] || [] ).length;
|
|
this.input.value = newValue.toFixed( precision );
|
|
|
|
this.input.dispatchEvent( new Event( 'input' ) );
|
|
|
|
this.dispatchChange();
|
|
|
|
}
|
|
|
|
} );
|
|
|
|
document.addEventListener( 'mouseup', () => {
|
|
|
|
if ( isDragging ) {
|
|
|
|
isDragging = false;
|
|
document.body.style.cursor = 'default';
|
|
|
|
}
|
|
|
|
} );
|
|
|
|
}
|
|
|
|
getValue() {
|
|
|
|
return parseFloat( this.input.value );
|
|
|
|
}
|
|
|
|
}
|
|
|
|
class ValueCheckbox extends Value {
|
|
|
|
constructor( { value = false } ) {
|
|
|
|
super();
|
|
|
|
const label = document.createElement( 'label' );
|
|
label.className = 'custom-checkbox';
|
|
|
|
const checkbox = document.createElement( 'input' );
|
|
checkbox.type = 'checkbox';
|
|
checkbox.checked = value;
|
|
this.checkbox = checkbox;
|
|
|
|
const checkmark = document.createElement( 'span' );
|
|
checkmark.className = 'checkmark';
|
|
|
|
label.appendChild( checkbox );
|
|
label.appendChild( checkmark );
|
|
this.domElement.appendChild( label );
|
|
|
|
checkbox.addEventListener( 'change', () => {
|
|
|
|
this.dispatchChange();
|
|
|
|
} );
|
|
|
|
}
|
|
|
|
getValue() {
|
|
|
|
return this.checkbox.checked;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
class ValueSlider extends Value {
|
|
|
|
constructor( { value = 0, min = 0, max = 1, step = 0.01 } ) {
|
|
|
|
super();
|
|
|
|
this.slider = document.createElement( 'input' );
|
|
this.slider.type = 'range';
|
|
this.slider.min = min;
|
|
this.slider.max = max;
|
|
this.slider.step = step;
|
|
|
|
const numberValue = new ValueNumber( { value, min, max, step } );
|
|
this.numberInput = numberValue.input;
|
|
this.numberInput.style.width = '60px';
|
|
this.numberInput.style.flexShrink = '0';
|
|
|
|
this.slider.value = value;
|
|
|
|
this.domElement.append( this.slider, this.numberInput );
|
|
|
|
this.slider.addEventListener( 'input', () => {
|
|
|
|
this.numberInput.value = this.slider.value;
|
|
|
|
this.dispatchChange();
|
|
|
|
} );
|
|
|
|
numberValue.addEventListener( 'change', () => {
|
|
|
|
this.slider.value = parseFloat( this.numberInput.value );
|
|
|
|
this.dispatchChange();
|
|
|
|
} );
|
|
|
|
}
|
|
|
|
setValue( val ) {
|
|
|
|
this.slider.value = val;
|
|
this.numberInput.value = val;
|
|
|
|
return super.setValue( val );
|
|
|
|
}
|
|
|
|
getValue() {
|
|
|
|
return parseFloat( this.slider.value );
|
|
|
|
}
|
|
|
|
step( value ) {
|
|
|
|
this.slider.step = value;
|
|
this.numberInput.step = value;
|
|
|
|
return this;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
class ValueSelect extends Value {
|
|
|
|
constructor( { options = [], value = '' } ) {
|
|
|
|
super();
|
|
|
|
const select = document.createElement( 'select' );
|
|
|
|
const createOption = ( name, optionValue ) => {
|
|
|
|
const optionEl = document.createElement( 'option' );
|
|
optionEl.value = name;
|
|
optionEl.textContent = name;
|
|
|
|
if ( optionValue == value ) optionEl.selected = true;
|
|
|
|
select.appendChild( optionEl );
|
|
|
|
return optionEl;
|
|
|
|
};
|
|
|
|
if ( Array.isArray( options ) ) {
|
|
|
|
options.forEach( opt => createOption( opt, opt ) );
|
|
|
|
} else {
|
|
|
|
Object.entries( options ).forEach( ( [ key, value ] ) => createOption( key, value ) );
|
|
|
|
}
|
|
|
|
this.domElement.appendChild( select );
|
|
|
|
//
|
|
|
|
select.addEventListener( 'change', () => {
|
|
|
|
this.dispatchChange();
|
|
|
|
} );
|
|
|
|
this.options = options;
|
|
this.select = select;
|
|
|
|
}
|
|
|
|
getValue() {
|
|
|
|
const options = this.options;
|
|
|
|
if ( Array.isArray( options ) ) {
|
|
|
|
return options[ this.select.selectedIndex ];
|
|
|
|
} else {
|
|
|
|
return options[ this.select.value ];
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
class ValueColor extends Value {
|
|
|
|
constructor( { value = '#ffffff' } ) {
|
|
|
|
super();
|
|
|
|
const colorInput = document.createElement( 'input' );
|
|
colorInput.type = 'color';
|
|
colorInput.value = this._getColorHex( value );
|
|
this.colorInput = colorInput;
|
|
|
|
this._value = value;
|
|
|
|
colorInput.addEventListener( 'input', () => {
|
|
|
|
const colorValue = colorInput.value;
|
|
|
|
if ( this._value.isColor ) {
|
|
|
|
this._value.setHex( parseInt( colorValue.slice( 1 ), 16 ) );
|
|
|
|
} else {
|
|
|
|
this._value = colorValue;
|
|
|
|
}
|
|
|
|
this.dispatchChange();
|
|
|
|
} );
|
|
|
|
this.domElement.appendChild( colorInput );
|
|
|
|
}
|
|
|
|
_getColorHex( color ) {
|
|
|
|
if ( color.isColor ) {
|
|
|
|
color = color.getHex();
|
|
|
|
}
|
|
|
|
if ( typeof color === 'number' ) {
|
|
|
|
color = `#${ color.toString( 16 ) }`;
|
|
|
|
} else if ( color[ 0 ] !== '#' ) {
|
|
|
|
color = '#' + color;
|
|
|
|
}
|
|
|
|
return color;
|
|
|
|
}
|
|
|
|
getValue() {
|
|
|
|
let value = this._value;
|
|
|
|
if ( typeof value === 'string' ) {
|
|
|
|
value = parseInt( value.slice( 1 ), 16 );
|
|
|
|
}
|
|
|
|
return value;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
class ValueButton extends Value {
|
|
|
|
constructor( { text = 'Button', value = () => {} } ) {
|
|
|
|
super();
|
|
|
|
const button = document.createElement( 'button' );
|
|
button.textContent = text;
|
|
button.onclick = value;
|
|
this.domElement.appendChild( button );
|
|
|
|
}
|
|
|
|
}
|
|
|
|
export { Value, ValueNumber, ValueCheckbox, ValueSlider, ValueSelect, ValueColor, ValueButton };
|