lil-gui 0.11.0 → 0.16.0
232 removals
Words removed | 659 |
Total words | 3407 |
Words removed (%) | 19.34 |
1069 lines
215 additions
Words added | 650 |
Total words | 3398 |
Words added (%) | 19.13 |
1054 lines
/**
/**
* lil-gui
* lil-gui
* https://lil-gui.georgealways.com
* https://lil-gui.georgealways.com
* @version 0.11.0
* @version 0.16.0
* @author George Michael Brower
* @author George Michael Brower
* @license MIT
* @license MIT
*/
*/
/**
/**
* Base class for all controllers.
* Base class for all controllers.
*/
*/
class Controller {
class Controller {
constructor( parent, object, property, className, widgetTag = 'div' ) {
constructor( parent, object, property, className, widgetTag = 'div' ) {
/**
/**
* The GUI that contains this controller.
* The GUI that contains this controller.
* @type {GUI}
* @type {GUI}
*/
*/
this.parent = parent;
this.parent = parent;
/**
/**
* The object this controller will modify.
* The object this controller will modify.
* @type {object}
* @type {object}
*/
*/
this.object = object;
this.object = object;
/**
/**
* The name of the property to control.
* The name of the property to control.
* @type {string}
* @type {string}
*/
*/
this.property = property;
this.property = property;
/**
/**
* Used to determine if the controller is disabled.
* Used to determine if the controller is disabled.
* Use `controller.disable( true|false )` to modify this value
* Use `controller.disable( true|false )` to modify this value
* @type {boolean}
* @type {boolean}
*/
*/
this._disabled = false;
this._disabled = false;
/**
/**
* The value of `object[ property ]` when the controller was created.
* The value of `object[ property ]` when the controller was created.
* @type {any}
* @type {any}
*/
*/
this.initialValue = this.getValue();
this.initialValue = this.getValue();
/**
/**
* The outermost container DOM element for this controller.
* The outermost container DOM element for this controller.
* @type {HTMLElement}
* @type {HTMLElement}
*/
*/
this.domElement = document.createElement( 'div' );
this.domElement = document.createElement( 'div' );
this.domElement.classList.add( 'controller' );
this.domElement.classList.add( 'controller' );
this.domElement.classList.add( className );
this.domElement.classList.add( className );
/**
/**
* The DOM element that contains the controller's name.
* The DOM element that contains the controller's name.
* @type {HTMLElement}
* @type {HTMLElement}
*/
*/
this.$name = document.createElement( 'div' );
this.$name = document.createElement( 'div' );
this.$name.classList.add( 'name' );
this.$name.classList.add( 'name' );
Controller.nextNameID = Controller.nextNameID || 0;
Controller.nextNameID = Controller.nextNameID || 0;
this.$name.id = `lil-gui-name-${++Controller.nextNameID}`;
this.$name.id = `lil-gui-name-${++Controller.nextNameID}`;
/**
/**
* The DOM element that contains the controller's "widget" (which differs by controller type).
* The DOM element that contains the controller's "widget" (which differs by controller type).
* @type {HTMLElement}
* @type {HTMLElement}
*/
*/
this.$widget = document.createElement( widgetTag );
this.$widget = document.createElement( widgetTag );
this.$widget.classList.add( 'widget' );
this.$widget.classList.add( 'widget' );
/**
/**
* The DOM element that receives the disabled attribute when using disable()
* The DOM element that receives the disabled attribute when using disable()
* @type {HTMLElement}
* @type {HTMLElement}
*/
*/
this.$disable = this.$widget;
this.$disable = this.$widget;
this.domElement.appendChild( this.$name );
this.domElement.appendChild( this.$name );
this.domElement.appendChild( this.$widget );
this.domElement.appendChild( this.$widget );
this.parent.children.push( this );
this.parent.children.push( this );
this.parent.controllers.push( this );
this.parent.controllers.push( this );
this.parent.$children.appendChild( this.domElement );
this.parent.$children.appendChild( this.domElement );
this._listenCallback = this._listenCallback.bind( this );
this._listenCallback = this._listenCallback.bind( this );
this.name( property );
this.name( property );
}
}
/**
/**
* Sets the name of the controller and its label in the GUI.
* Sets the name of the controller and its label in the GUI.
* @param {string} name
* @param {string} name
* @returns {this}
* @returns {this}
*/
*/
name( name ) {
name( name ) {
/**
/**
* The controller's name. Use `controller.name( 'Name' )` to modify this value.
* The controller's name. Use `controller.name( 'Name' )` to modify this value.
* @type {string}
* @type {string}
*/
*/
this._name = name;
this._name = name;
this.$name.innerHTML = name;
this.$name.innerHTML = name;
return this;
return this;
}
}
/**
/**
* Pass a function to be called whenever the value is modified by this controller.
* Pass a function to be called whenever the value is modified by this controller.
* The function receives the new value as its first parameter. The value of `this` will be the
* The function receives the new value as its first parameter. The value of `this` will be the
* controller.
* controller.
* @param {Function} callback
* @param {Function} callback
* @returns {this}
* @returns {this}
* @example
* @example
* const controller = gui.add( object, 'property' );
* const controller = gui.add( object, 'property' );
*
*
* controller.onChange( v => {
* controller.onChange( function( v ) {
* console.log( 'The value is now ' + v );
* console.log( 'The value is now ' + v );
* console.assert( this === controller );
* console.assert( this === controller );
* } );
* } );
*/
*/
onChange( callback ) {
onChange( callback ) {
/**
/**
* Used to access the function bound to change events. Don't modify this value directly.
* Used to access the function bound to `onChange` events. Don't modify this value directly.
* Use the `controller.onChange( callback )` method instead.
* Use the `controller.onChange( callback )` method instead.
* @type {Function}
* @type {Function}
*/
*/
this._onChange = callback;
this._onChange = callback;
return this;
return this;
}
}
/**
* Calls the onChange methods of this controller and its parent GUI.
* @protected
*/
_callOnChange() {
_callOnChange() {
this.parent._callOnChange( this );
this.parent._callOnChange( this );
if ( this._onChange !== undefined ) {
if ( this._onChange !== undefined ) {
this._onChange.call( this, this.getValue() );
this._onChange.call( this, this.getValue() );
}
}
this._changed = true;
}
}
// Provided for compatability
/**
* Pass a function to be called after this controller has been modified and loses focus.
* @param {Function} callback
* @returns {this}
* @example
* const controller = gui.add( object, 'property' );
*
* controller.onFinishChange( function( v ) {
* console.log( 'Changes complete: ' + v );
* console.assert( this === controller );
* } );
*/
onFinishChange( callback ) {
onFinishChange( callback ) {
return this.onChange( callback );
/**
* Used to access the function bound to `onFinishChange` events. Don't modify this value
* directly. Use the `controller.onFinishChange( callback )` method instead.
* @type {Function}
*/
this._onFinishChange = callback;
Text moved from lines 1025-1027
return this;
}
/**
* Should be called by Controller when its widgets lose focus.
* @protected
*/
_callOnFinishChange() {
if ( this._changed ) {
this.parent._callOnFinishChange( this );
if ( this._onFinishChange !== undefined ) {
this._onFinishChange.call( this, this.getValue() );
}
}
this._changed = false;
}
}
/**
/**
* Sets the controller back to its initial value.
* Sets the controller back to its initial value.
* @returns {this}
* @returns {this}
*/
*/
reset() {
reset() {
this.setValue( this.initialValue );
this.setValue( this.initialValue );
this._callOnFinishChange();
return this;
return this;
}
}
/**
/**
* Enables this controller.
* Enables this controller.
* @param {boolean} enabled
* @param {boolean} enabled
* @returns {this}
* @returns {this}
* @example
* @example
* controller.enable();
* controller.enable();
* controller.enable( false ); // disable
* controller.enable( false ); // disable
* controller.enable( controller._disabled ); // toggle
* controller.enable( controller._disabled ); // toggle
*/
*/
enable( enabled = true ) {
enable( enabled = true ) {
return this.disable( !enabled );
return this.disable( !enabled );
}
}
/**
/**
* Disables this controller.
* Disables this controller.
* @param {boolean} disabled
* @param {boolean} disabled
* @returns {this}
* @returns {this}
* @example
* @example
* controller.disable();
* controller.disable();
* controller.disable( false ); // enable
* controller.disable( false ); // enable
* controller.disable( !controller._disabled ); // toggle
* controller.disable( !controller._disabled ); // toggle
*/
*/
disable( disabled = true ) {
disable( disabled = true ) {
if ( disabled === this._disabled ) return this;
if ( disabled === this._disabled ) return this;
this._disabled = disabled;
this._disabled = disabled;
this.domElement.classList.toggle( 'disabled', disabled );
this.domElement.classList.toggle( 'disabled', disabled );
this.$disable.toggleAttribute( 'disabled', disabled );
if ( disabled ) {
this.$disable.setAttribute( 'disabled', 'disabled' );
} else {
this.$disable.removeAttribute( 'disabled' );
}
return this;
return this;
}
}
/**
/**
* Destroys this controller and replaces it with a new option controller. Provided as a more
* Destroys this controller and replaces it with a new option controller. Provided as a more
* descriptive syntax for `gui.add`, but primarily for compatibility with dat.gui.
* descriptive syntax for `gui.add`, but primarily for compatibility with dat.gui.
*
*
* Use caution, as this method will destroy old references to this controller. It will also
* Use caution, as this method will destroy old references to this controller. It will also
* change controller order if called out of sequence, moving the option controller to the end of
* change controller order if called out of sequence, moving the option controller to the end of
* the GUI.
* the GUI.
* @example
* @example
* // safe usage
* // safe usage
*
*
* gui.add( object1, 'property' ).options( [ 'a', 'b', 'c' ] );
* gui.add( object1, 'property' ).options( [ 'a', 'b', 'c' ] );
* gui.add( object2, 'property' );
* gui.add( object2, 'property' );
*
*
* // danger
* // danger
*
*
* const c = gui.add( object1, 'property' );
* const c = gui.add( object1, 'property' );
* gui.add( object2, 'property' );
* gui.add( object2, 'property' );
*
*
* c.options( [ 'a', 'b', 'c' ] );
* c.options( [ 'a', 'b', 'c' ] );
* // controller is now at the end of the GUI even though it was added first
* // controller is now at the end of the GUI even though it was added first
*
*
* assert( c.parent.children.indexOf( c ) === -1 )
* assert( c.parent.children.indexOf( c ) === -1 )
* // c references a controller that no longer exists
* // c references a controller that no longer exists
*
*
* @param {object|Array} options
* @param {object|Array} options
* @returns {Controller}
* @returns {Controller}
*/
*/
options( options ) {
options( options ) {
const controller = this.parent.add( this.object, this.property, options );
const controller = this.parent.add( this.object, this.property, options );
controller.name( this._name );
controller.name( this._name );
this.destroy();
this.destroy();
return controller;
return controller;
}
}
/**
/**
* Sets the minimum value. Only works on number controllers.
* Sets the minimum value. Only works on number controllers.
* @param {number} min
* @param {number} min
* @returns {this}
* @returns {this}
*/
*/
min( min ) {
min( min ) {
return this;
return this;
}
}
/**
/**
* Sets the maximum value. Only works on number controllers.
* Sets the maximum value. Only works on number controllers.
* @param {number} max
* @param {number} max
* @returns {this}
* @returns {this}
*/
*/
max( max ) {
max( max ) {
return this;
return this;
}
}
/**
/**
* Sets the step. Only works on number controllers.
* Sets the step. Only works on number controllers.
* @param {number} step
* @param {number} step
* @returns {this}
* @returns {this}
*/
*/
step( step ) {
step( step ) {
return this;
return this;
}
}
/**
/**
* Calls `updateDisplay()` every animation frame. Pass `false` to stop listening.
* Calls `updateDisplay()` every animation frame. Pass `false` to stop listening.
* @param {boolean} listen
* @param {boolean} listen
* @returns {this}
* @returns {this}
*/
*/
listen( listen = true ) {
listen( listen = true ) {
/**
/**
* Used to determine if the controller is currently listening. Don't modify this value
* Used to determine if the controller is currently listening. Don't modify this value
* directly. Use the `controller.listen( true|false )` method instead.
* directly. Use the `controller.listen( true|false )` method instead.
* @type {boolean}
* @type {boolean}
*/
*/
this._listening = listen;
this._listening = listen;
if ( this._listenCallbackID !== undefined ) {
if ( this._listenCallbackID !== undefined ) {
cancelAnimationFrame( this._listenCallbackID );
cancelAnimationFrame( this._listenCallbackID );
this._listenCallbackID = undefined;
this._listenCallbackID = undefined;
}
}
if ( this._listening ) {
if ( this._listening ) {
this._listenCallback();
this._listenCallback();
}
}
return this;
return this;
}
}
_listenCallback() {
_listenCallback() {
this._listenCallbackID = requestAnimationFrame( this._listenCallback );
this._listenCallbackID = requestAnimationFrame( this._listenCallback );
this.updateDisplay();
const value = this.getValue();
// Only update the DOM if the value has changed. Controllers that control non-primitive data
// types get updated every frame to avoid the complexity of comparing objects
if ( value !== this._listenValuePrev || Object( value ) === value ) {
this.updateDisplay();
}
this._listenValuePrev = value;
}
}
/**
/**
* Returns `object[ property ]`.
* Returns `object[ property ]`.
* @returns {any}
* @returns {any}
*/
*/
getValue() {
getValue() {
return this.object[ this.property ];
return this.object[ this.property ];
}
}
/**
/**
* Sets the value of `object[ property ]`, invokes any `onChange` handlers and updates the display.
* Sets the value of `object[ property ]`, invokes any `onChange` handlers and updates the display.
* @param {any} value
* @param {any} value
* @returns {this}
* @returns {this}
*/
*/
setValue( value ) {
setValue( value ) {
this.object[ this.property ] = value;
this.object[ this.property ] = value;
this._callOnChange();
this._callOnChange();
this.updateDisplay();
this.updateDisplay();
return this;
return this;
}
}
/**
/**
* Updates the display to keep it in sync with the current value. Useful for updating your
* Updates the display to keep it in sync with the current value. Useful for updating your
* controllers when their values have been modified outside of the GUI.
* controllers when their values have been modified outside of the GUI.
* @returns {this}
* @returns {this}
*/
*/
updateDisplay() {
updateDisplay() {
return this;
return this;
}
}
load( value ) {
load( value ) {
this.setValue( value );
this.setValue( value );
this._callOnFinishChange();
return this;
}
}
save() {
save() {
return this.getValue();
return this.getValue();
}
}
/**
/**
* Destroys this controller and removes it from the parent GUI.
* Destroys this controller and removes it from the parent GUI.
*/
*/
destroy() {
destroy() {
this.parent.children.splice( this.parent.children.indexOf( this ), 1 );
this.parent.children.splice( this.parent.children.indexOf( this ), 1 );
this.parent.controllers.splice( this.parent.controllers.indexOf( this ), 1 );
this.parent.controllers.splice( this.parent.controllers.indexOf( this ), 1 );
this.parent.$children.removeChild( this.domElement );
this.parent.$children.removeChild( this.domElement );
}
}
}
}
class BooleanController extends Controller {
class BooleanController extends Controller {
constructor( parent, object, property ) {
constructor( parent, object, property ) {
super( parent, object, property, 'boolean', 'label' );
super( parent, object, property, 'boolean', 'label' );
this.$input = document.createElement( 'input' );
this.$input = document.createElement( 'input' );
this.$input.setAttribute( 'type', 'checkbox' );
this.$input.setAttribute( 'type', 'checkbox' );
this.$input.setAttribute( 'aria-labelledby', this.$name.id );
this.$widget.appendChild( this.$input );
this.$widget.appendChild( this.$input );
this.$input.addEventListener( 'change', () => {
this.$input.addEventListener( 'change', () => {
this.setValue( this.$input.checked );
this.setValue( this.$input.checked );
this._callOnFinishChange();
} );
} );
this.$disable = this.$input;
this.$disable = this.$input;
this.updateDisplay();
this.updateDisplay();
}
}
updateDisplay() {
updateDisplay() {
this.$input.checked = this.getValue();
this.$input.checked = this.getValue();
return this;
return this;
}
}
}
}
function normalizeColorString( string ) {
function normalizeColorString( string ) {
let match, result;
let match, result;
if ( match = string.match( /(#|0x)?([a-f0-9]{6})/i ) ) {
if ( match = string.match( /(#|0x)?([a-f0-9]{6})/i ) ) {
result = match[ 2 ];
result = match[ 2 ];
} else if ( match = string.match( /rgb\(\s*(\d*)\s*,\s*(\d*)\s*,\s*(\d*)\s*\)/ ) ) {
} else if ( match = string.match( /rgb\(\s*(\d*)\s*,\s*(\d*)\s*,\s*(\d*)\s*\)/ ) ) {
result = parseInt( match[ 1 ] ).toString( 16 ).padStart( 2, 0 )
result = parseInt( match[ 1 ] ).toString( 16 ).padStart( 2, 0 )
+ parseInt( match[ 2 ] ).toString( 16 ).padStart( 2, 0 )
+ parseInt( match[ 2 ] ).toString( 16 ).padStart( 2, 0 )
+ parseInt( match[ 3 ] ).toString( 16 ).padStart( 2, 0 );
+ parseInt( match[ 3 ] ).toString( 16 ).padStart( 2, 0 );
} else if ( match = string.match( /^#?([a-f0-9])([a-f0-9])([a-f0-9])$/i ) ) {
} else if ( match = string.match( /^#?([a-f0-9])([a-f0-9])([a-f0-9])$/i ) ) {
result = match[ 1 ] + match[ 1 ] + match[ 2 ] + match[ 2 ] + match[ 3 ] + match[ 3 ];
result = match[ 1 ] + match[ 1 ] + match[ 2 ] + match[ 2 ] + match[ 3 ] + match[ 3 ];
}
}
if ( result ) {
if ( result ) {
return '#' + result;
return '#' + result;
}
}
return false;
return false;
}
}
const STRING = {
const STRING = {
isPrimitive: true,
isPrimitive: true,
match: v => typeof v === 'string',
match: v => typeof v === 'string',
fromHexString: normalizeColorString,
fromHexString: normalizeColorString,
toHexString: normalizeColorString
toHexString: normalizeColorString
};
};
const INT = {
const INT = {
isPrimitive: true,
isPrimitive: true,
match: v => typeof v === 'number',
match: v => typeof v === 'number',
fromHexString: string => parseInt( string.substring( 1 ), 16 ),
fromHexString: string => parseInt( string.substring( 1 ), 16 ),
toHexString: value => '#' + value.toString( 16 ).padStart( 6, 0 )
toHexString: value => '#' + value.toString( 16 ).padStart( 6, 0 )
};
};
const ARRAY = {
const ARRAY = {
isPrimitive: false,
isPrimitive: false,
match: Array.isArray,
match: Array.isArray,
fromHexString( string, target, rgbScale = 1 ) {
fromHexString( string, target, rgbScale = 1 ) {
const int = INT.fromHexString( string );
const int = INT.fromHexString( string );
target[ 0 ] = ( int >> 16 & 255 ) / 255 * rgbScale;
target[ 0 ] = ( int >> 16 & 255 ) / 255 * rgbScale;
target[ 1 ] = ( int >> 8 & 255 ) / 255 * rgbScale;
target[ 1 ] = ( int >> 8 & 255 ) / 255 * rgbScale;
target[ 2 ] = ( int & 255 ) / 255 * rgbScale;
target[ 2 ] = ( int & 255 ) / 255 * rgbScale;
},
},
toHexString( [ r, g, b ], rgbScale = 1 ) {
toHexString( [ r, g, b ], rgbScale = 1 ) {
rgbScale = 255 / rgbScale;
rgbScale = 255 / rgbScale;
const int = ( r * rgbScale ) << 16 ^
const int = ( r * rgbScale ) << 16 ^
( g * rgbScale ) << 8 ^
( g * rgbScale ) << 8 ^
( b * rgbScale ) << 0;
( b * rgbScale ) << 0;
return INT.toHexString( int );
return INT.toHexString( int );
}
}
};
};
const OBJECT = {
const OBJECT = {
isPrimitive: false,
isPrimitive: false,
match: v => Object( v ) === v,
match: v => Object( v ) === v,
fromHexString( string, target, rgbScale = 1 ) {
fromHexString( string, target, rgbScale = 1 ) {
const int = INT.fromHexString( string );
const int = INT.fromHexString( string );
target.r = ( int >> 16 & 255 ) / 255 * rgbScale;
target.r = ( int >> 16 & 255 ) / 255 * rgbScale;
target.g = ( int >> 8 & 255 ) / 255 * rgbScale;
target.g = ( int >> 8 & 255 ) / 255 * rgbScale;
target.b = ( int & 255 ) / 255 * rgbScale;
target.b = ( int & 255 ) / 255 * rgbScale;
},
},
toHexString( { r, g, b }, rgbScale = 1 ) {
toHexString( { r, g, b }, rgbScale = 1 ) {
rgbScale = 255 / rgbScale;
rgbScale = 255 / rgbScale;
const int = ( r * rgbScale ) << 16 ^
const int = ( r * rgbScale ) << 16 ^
( g * rgbScale ) << 8 ^
( g * rgbScale ) << 8 ^
( b * rgbScale ) << 0;
( b * rgbScale ) << 0;
return INT.toHexString( int );
return INT.toHexString( int );
}
}
};
};
const FORMATS = [ STRING, INT, ARRAY, OBJECT ];
const FORMATS = [ STRING, INT, ARRAY, OBJECT ];
function getColorFormat( value ) {
function getColorFormat( value ) {
return FORMATS.find( format => format.match( value ) );
return FORMATS.find( format => format.match( value ) );
}
}
class ColorController extends Controller {
class ColorController extends Controller {
constructor( parent, object, property, rgbScale ) {
constructor( parent, object, property, rgbScale ) {
super( parent, object, property, 'color' );
super( parent, object, property, 'color' );
this.$input = document.createElement( 'input' );
this.$input = document.createElement( 'input' );
this.$input.setAttribute( 'type', 'color' );
this.$input.setAttribute( 'type', 'color' );
this.$input.setAttribute( 'tabindex', -1 );
this.$input.setAttribute( 'tabindex', -1 );
this.$input.setAttribute( 'aria-labelledby', this.$name.id );
this.$input.setAttribute( 'aria-labelledby', this.$name.id );
this.$text = document.createElement( 'input' );
this.$text = document.createElement( 'input' );
this.$text.setAttribute( 'type', 'text' );
this.$text.setAttribute( 'type', 'text' );
this.$text.setAttribute( 'spellcheck', 'false' );
this.$text.setAttribute( 'spellcheck', 'false' );
this.$text.setAttribute( 'aria-labelledby', this.$name.id );
this.$text.setAttribute( 'aria-labelledby', this.$name.id );
this.$display = document.createElement( 'div' );
this.$display = document.createElement( 'div' );
this.$display.classList.add( 'display' );
this.$display.classList.add( 'display' );
this.$display.appendChild( this.$input );
this.$display.appendChild( this.$input );
this.$widget.appendChild( this.$display );
this.$widget.appendChild( this.$display );
this.$widget.appendChild( this.$text );
this.$widget.appendChild( this.$text );
this._format = getColorFormat( this.initialValue );
this._format = getColorFormat( this.initialValue );
this._rgbScale = rgbScale;
this._rgbScale = rgbScale;
this._initialValueHexString = this.save();
this._initialValueHexString = this.save();
this._textFocused = false;
this._textFocused = false;
const onInputChange = () => {
this.$input.addEventListener( 'input', () => {
this._setValueFromHexString( this.$input.value );
this._setValueFromHexString( this.$input.value );
};
this.$input.addEventListener( 'change', onInputChange );
this.$input.addEventListener( 'input', onInputChange );
this.$input.addEventListener( 'focus', () => {
this.$display.classList.add( 'focus' );
} );
} );
this.$input.addEventListener( 'blur', () => {
this.$input.addEventListener( 'blur', () => {
this.$display.classList.remove( 'focus' );
this._callOnFinishChange();
} );
} );
this.$text.addEventListener( 'input', () => {
this.$text.addEventListener( 'input', () => {
const tryParse = normalizeColorString( this.$text.value );
const tryParse = normalizeColorString( this.$text.value );
if ( tryParse ) {
if ( tryParse ) {
this._setValueFromHexString( tryParse );
this._setValueFromHexString( tryParse );
}
}
} );
} );
this.$text.addEventListener( 'focus', () => {
this.$text.addEventListener( 'focus', () => {
this._textFocused = true;
this._textFocused = true;
this.$text.select();
this.$text.select();
} );
} );
this.$text.addEventListener( 'blur', () => {
this.$text.addEventListener( 'blur', () => {
this._textFocused = false;
this._textFocused = false;
this.updateDisplay();
this.updateDisplay();
this._callOnFinishChange();
} );
} );
this.$disable = this.$text;
this.$disable = this.$text;
this.updateDisplay();
this.updateDisplay();
}
}
reset() {
reset() {
this._setValueFromHexString( this._initialValueHexString );
this._setValueFromHexString( this._initialValueHexString );
return this;
return this;
}
}
_setValueFromHexString( value ) {
_setValueFromHexString( value ) {
if ( this._format.isPrimitive ) {
if ( this._format.isPrimitive ) {
const newValue = this._format.fromHexString( value );
const newValue = this._format.fromHexString( value );
this.setValue( newValue );
this.setValue( newValue );
} else {
} else {
this._format.fromHexString( value, this.getValue(), this._rgbScale );
this._format.fromHexString( value, this.getValue(), this._rgbScale );
this._callOnChange();
this._callOnChange();
this.updateDisplay();
this.updateDisplay();
}
}
}
}
save() {
save() {
return this._format.toHexString( this.getValue(), this._rgbScale );
return this._format.toHexString( this.getValue(), this._rgbScale );
}
}
load( value ) {
load( value ) {
this._setValueFromHexString( value );
this._setValueFromHexString( value );
this._callOnFinishChange();
return this;
}
}
updateDisplay() {
updateDisplay() {
this.$input.value = this._format.toHexString( this.getValue(), this._rgbScale );
this.$input.value = this._format.toHexString( this.getValue(), this._rgbScale );
if ( !this._textFocused ) {
if ( !this._textFocused ) {
this.$text.value = this.$input.value.substring( 1 );
this.$text.value = this.$input.value.substring( 1 );
}
}
this.$display.style.backgroundColor = this.$input.value;
this.$display.style.backgroundColor = this.$input.value;
return this;
return this;
}
}
}
}
class FunctionController extends Controller {
class FunctionController extends Controller {
constructor( parent, object, property ) {
constructor( parent, object, property ) {
super( parent, object, property, 'function' );
super( parent, object, property, 'function' );
// Buttons are the only case where widget contains name
// Buttons are the only case where widget contains name
this.$button = document.createElement( 'button' );
this.$button = document.createElement( 'button' );
this.$button.appendChild( this.$name );
this.$button.appendChild( this.$name );
this.$widget.appendChild( this.$button );
this.$widget.appendChild( this.$button );
this.$button.addEventListener( 'click', e => {
this.$button.addEventListener( 'click', e => {
e.preventDefault();
e.preventDefault();
this.getValue().call( this.object );
this.getValue().call( this.object );
} );
} );
// enables :active pseudo class on mobile
// enables :active pseudo class on mobile
this.$button.addEventListener( 'touchstart', () => { } );
this.$button.addEventListener( 'touchstart', () => { } );
this.$disable = this.$button;
this.$disable = this.$button;
}
}
}
}
class NumberController extends Controller {
class NumberController extends Controller {
constructor( parent, object, property, min, max, step ) {
constructor( parent, object, property, min, max, step ) {
super( parent, object, property, 'number' );
super( parent, object, property, 'number' );
this._initInput();
this._initInput();
this.min( min );
this.min( min );
this.max( max );
this.max( max );
const stepExplicit = step !== undefined;
const stepExplicit = step !== undefined;
this.step( stepExplicit ? step : this._getImplicitStep(), stepExplicit );
this.step( stepExplicit ? step : this._getImplicitStep(), stepExplicit );
this.updateDisplay();
this.updateDisplay();
}
}
min( min ) {
min( min ) {
this._min = min;
this._min = min;
this._onUpdateMinMax();
this._onUpdateMinMax();
return this;
return this;
}
}
max( max ) {
max( max ) {
this._max = max;
this._max = max;
this._onUpdateMinMax();
this._onUpdateMinMax();
return this;
return this;
}
}
step( step, explicit = true ) {
step( step, explicit = true ) {
this._step = step;
this._step = step;
this._stepExplicit = explicit;
this._stepExplicit = explicit;
return this;
return this;
}
}
updateDisplay() {
updateDisplay() {
const value = this.getValue();
const value = this.getValue();
if ( this._hasSlider ) {
if ( this._hasSlider ) {
const percent = ( value - this._min ) / ( this._max - this._min );
this.$fill.style.setProperty( 'width', percent * 100 + '%' );
let percent = ( value - this._min ) / ( this._max - this._min );
percent = Math.max( 0, Math.min( percent, 1 ) );
this.$fill.style.width = percent * 100 + '%';
}
}
if ( !this._inputFocused ) {
if ( !this._inputFocused ) {
this.$input.value = value;
this.$input.value = value;
}
}
return this;
return this;
}
}
_initInput() {
_initInput() {
this.$input = document.createElement( 'input' );
this.$input = document.createElement( 'input' );
this.$input.setAttribute( 'type', 'text' );
this.$input.setAttribute( 'type', 'number' );
this.$input.setAttribute( 'inputmode', 'numeric' );
this.$input.setAttribute( 'step', 'any' );
this.$input.setAttribute( 'aria-labelledby', this.$name.id );
this.$input.setAttribute( 'aria-labelledby', this.$name.id );
this.$widget.appendChild( this.$input );
this.$widget.appendChild( this.$input );
this.$disable = this.$input;
this.$disable = this.$input;
const onInput = () => {
const onInput = () => {
const value = parseFloat( this.$input.value );
const value = parseFloat( this.$input.value );
if ( isNaN( value ) ) return;
if ( isNaN( value ) ) return;
this.setValue( this._clamp( value ) );
this.setValue( this._clamp( value ) );
};
};
// invoked on wheel or arrow key up/down
// Keys & mouse wheel
// ---------------------------------------------------------------------
const increment = delta => {
const increment = delta => {
const value = parseFloat( this.$input.value );
const value = parseFloat( this.$input.value );
if ( isNaN( value ) ) return;
if ( isNaN( value ) ) return;
this._snapClampSetValue( value + delta );
this._snapClampSetValue( value + delta );
// Force the input to updateDisplay when it's focused
// Force the input to updateDisplay when it's focused
this.$input.value = this.getValue();
this.$input.value = this.getValue();
};
};
const onKeyDown = e => {
const onKeyDown = e => {
if ( e.code === 'Enter' ) {
if ( e.code === 'Enter' ) {
this.$input.blur();
this.$input.blur();
}
}
if ( e.code === 'ArrowUp' ) {
if ( e.code === 'ArrowUp' ) {
e.preventDefault();
e.preventDefault();
increment( this._step * this._arrowKeyMultiplier( e ) );
increment( this._step * this._arrowKeyMultiplier( e ) );
}
}
if ( e.code === 'ArrowDown' ) {
if ( e.code === 'ArrowDown' ) {
e.preventDefault();
e.preventDefault();
increment( -1 * this._step * this._arrowKeyMultiplier( e ) );
increment( this._step * this._arrowKeyMultiplier( e ) * -1 );
}
}
};
};
const onWheel = e => {
const onWheel = e => {
if ( this._inputFocused ) {
if ( this._inputFocused ) {
e.preventDefault();
e.preventDefault();
increment( this._normalizeMouseWheel( e ) * this._step );
increment( this._step * this._normalizeMouseWheel( e ) );
}
}
};
};
// Vertical drag
// ---------------------------------------------------------------------
let testingForVerticalDrag = false,
initClientX,
initClientY,
prevClientY,
initValue,
dragDelta;
// Once the mouse is dragged more than DRAG_THRESH px on any axis, we decide
// on the user's intent: horizontal means highlight, vertical means drag.
const DRAG_THRESH = 5;
const onMouseDown = e => {
initClientX = e.clientX;
initClientY = prevClientY = e.clientY;
testingForVerticalDrag = true;
initValue = this.getValue();
dragDelta = 0;
window.addEventListener( 'mousemove', onMouseMove );
window.addEventListener( 'mouseup', onMouseUp );
};
const onMouseMove = e => {
if ( testingForVerticalDrag ) {
const dx = e.clientX - initClientX;
const dy = e.clientY - initClientY;
if ( Math.abs( dy ) > DRAG_THRESH ) {
e.preventDefault();
this.$input.blur();
testingForVerticalDrag = false;
this._setDraggingStyle( true, 'vertical' );
} else if ( Math.abs( dx ) > DRAG_THRESH ) {
onMouseUp();
}
}
// This isn't an else so that the first move counts towards dragDelta
if ( !testingForVerticalDrag ) {
const dy = e.clientY - prevClientY;
dragDelta -= dy * this._step * this._arrowKeyMultiplier( e );
// Clamp dragDelta so we don't have 'dead space' after dragging past bounds.
// We're okay with the fact that bounds can be undefined here.
if ( initValue + dragDelta > this._max ) {
dragDelta = this._max - initValue;
} else if ( initValue + dragDelta < this._min ) {
dragDelta = this._min - initValue;
}
this._snapClampSetValue( initValue + dragDelta );
}
prevClientY = e.clientY;
};
const onMouseUp = () => {
this._setDraggingStyle( false, 'vertical' );
this._callOnFinishChange();
window.removeEventListener( 'mousemove', onMouseMove );
window.removeEventListener( 'mouseup', onMouseUp );
};
// Focus state & onFinishChange
// ---------------------------------------------------------------------
const onFocus = () => {
const onFocus = () => {
this._inputFocused = true;
this._inputFocused = true;
};
};
const onBlur = () => {
const onBlur = () => {
this._inputFocused = false;
this._inputFocused = false;
this.updateDisplay();
this.updateDisplay();
this._callOnFinishChange();
};
};
this.$input.addEventListener( 'input', onInput );
this.$input.addEventListener( 'keydown', onKeyDown );
this.$input.addEventListener( 'wheel', onWheel );
this.$input.addEventListener( 'mousedown', onMouseDown );
this.$input.addEventListener( 'focus', onFocus );
this.$input.addEventListener( 'focus', onFocus );
this.$input.addEventListener( 'input', onInput );
this.$input.addEventListener( 'blur', onBlur );
this.$input.addEventListener( 'blur', onBlur );
this.$input.addEventListener( 'keydown', onKeyDown );
this.$input.addEventListener( 'wheel', onWheel, { passive: false } );
}
}
_initSlider() {
_initSlider() {
this._hasSlider = true;
this._hasSlider = true;
// Build DOM
// Build DOM
// ---------------------------------------------------------------------
// ---------------------------------------------------------------------
this.$slider = document.createElement( 'div' );
this.$slider = document.createElement( 'div' );
this.$slider.classList.add( 'slider' );
this.$slider.classList.add( 'slider' );
this.$fill = document.createElement( 'div' );
this.$fill = document.createElement( 'div' );
this.$fill.classList.add( 'fill' );
this.$fill.classList.add( 'fill' );
this.$slider.appendChild( this.$fill );
this.$slider.appendChild( this.$fill );
this.$widget.insertBefore( this.$slider, this.$input );
this.$widget.insertBefore( this.$slider, this.$input );
this.domElement.classList.add( 'hasSlider' );
this.domElement.classList.add( 'hasSlider' );
// Map clientX to value
// Map clientX to value
// ---------------------------------------------------------------------
// ---------------------------------------------------------------------
const map = ( v, a, b, c, d ) => {
const map = ( v, a, b, c, d ) => {
return ( v - a ) / ( b - a ) * ( d - c ) + c;
return ( v - a ) / ( b - a ) * ( d - c ) + c;
};
};
const setValueFromX = clientX => {
const setValueFromX = clientX => {
const rect = this.$slider.getBoundingClientRect();
const rect = this.$slider.getBoundingClientRect();
let value = map( clientX, rect.left, rect.right, this._min, this._max );
let value = map( clientX, rect.left, rect.right, this._min, this._max );
this._snapClampSetValue( value );
this._snapClampSetValue( value );
};
};
// Bind mouse listeners
// Mouse drag
// ---------------------------------------------------------------------
// ---------------------------------------------------------------------
const mouseDown = e => {
const mouseDown = e => {
this._setDraggingStyle( true );
setValueFromX( e.clientX );
setValueFromX( e.clientX );
this._setActiveStyle( true );
window.addEventListener( 'mousemove', mouseMove );
window.addEventListener( 'mousemove', mouseMove );
window.addEventListener( 'mouseup', mouseUp );
window.addEventListener( 'mouseup', mouseUp );
};
};
const mouseMove = e => {
const mouseMove = e => {
setValueFromX( e.clientX );
setValueFromX( e.clientX );
};
};
const mouseUp = () => {
const mouseUp = () => {
this._setActiveStyle( false );
this._callOnFinishChange();
this._setDraggingStyle( false );
window.removeEventListener( 'mousemove', mouseMove );
window.removeEventListener( 'mousemove', mouseMove );
window.removeEventListener( 'mouseup', mouseUp );
window.removeEventListener( 'mouseup', mouseUp );
};
};
this.$slider.addEventListener( 'mousedown', mouseDown );
// Touch drag
// Bind touch listeners
// ---------------------------------------------------------------------
// ---------------------------------------------------------------------
let testingForScroll = false, prevClientX, prevClientY;
let testingForScroll = false, prevClientX, prevClientY;
const beginTouchDrag = e => {
e.preventDefault();
this._setDraggingStyle( true );
setValueFromX( e.touches[ 0 ].clientX );
testingForScroll = false;
};
const onTouchStart = e => {
const onTouchStart = e => {
if ( e.touches.length > 1 ) return;
if ( e.touches.length > 1 ) return;
// If we're in a scrollable container, we should wait for the first
// If we're in a scrollable container, we should wait for the first
// touchmove to see if the user is trying to slide or scroll.
// touchmove to see if the user is trying to slide or scroll.
if ( this._hasScrollBar ) {
if ( this._hasScrollBar ) {
prevClientX = e.touches[ 0 ].clientX;
prevClientX = e.touches[ 0 ].clientX;
prevClientY = e.touches[ 0 ].clientY;
prevClientY = e.touches[ 0 ].clientY;
testingForScroll = true;
testingForScroll = true;
} else {
} else {
// Otherwise, we can set the value straight away on touchstart.
// Otherwise, we can set the value straight away on touchstart.
e.preventDefault();
beginTouchDrag( e );
setValueFromX( e.touches[ 0 ].clientX );
this._setActiveStyle( true );
testingForScroll = false;
}
}
window.addEventListener( 'touchmove', onTouchMove, { passive: false } );
window.addEventListener( 'touchmove', onTouchMove );
window.addEventListener( 'touchend', onTouchEnd );
window.addEventListener( 'touchend', onTouchEnd );
};
};
const onTouchMove = e => {
const onTouchMove = e => {
if ( testingForScroll ) {
if ( testingForScroll ) {
const dx = e.touches[ 0 ].clientX - prevClientX;
const dx = e.touches[ 0 ].clientX - prevClientX;
const dy = e.touches[ 0 ].clientY - prevClientY;
const dy = e.touches[ 0 ].clientY - prevClientY;
if ( Math.abs( dx ) > Math.abs( dy ) ) {
if ( Math.abs( dx ) > Math.abs( dy ) ) {
// We moved horizontally, set the value and stop checking.
// We moved horizontally, set the value and stop checking.
e.preventDefault();
beginTouchDrag( e );
setValueFromX( e.touches[ 0 ].clientX );
this._setActiveStyle( true );
testingForScroll = false;
} else {
} else {
// This was, in fact, an attempt to scroll. Abort.
// This was, in fact, an attempt to scroll. Abort.
window.removeEventListener( 'touchmove', onTouchMove );
window.removeEventListener( 'touchmove', onTouchMove );
window.removeEventListener( 'touchend', onTouchEnd );
window.removeEventListener( 'touchend', onTouchEnd );
}
}
} else {
} else {
e.preventDefault();
e.preventDefault();
setValueFromX( e.touches[ 0 ].clientX );
setValueFromX( e.touches[ 0 ].clientX );
}
}
};
};
const onTouchEnd = () => {
const onTouchEnd = () => {
this._setActiveStyle( false );
this._callOnFinishChange();
this._setDraggingStyle( false );
window.removeEventListener( 'touchmove', onTouchMove );
window.removeEventListener( 'touchmove', onTouchMove );
window.removeEventListener( 'touchend', onTouchEnd );
window.removeEventListener( 'touchend', onTouchEnd );
};
};
this.$slider.addEventListener( 'touchstart', onTouchStart );
// Mouse wheel
// Bind wheel listeners
// ---------------------------------------------------------------------
// ---------------------------------------------------------------------
// We have to use a debounced function to call onFinishChange because
// there's no way to tell when the user is "done" mouse-wheeling.
const callOnFinishChange = this._callOnFinishChange.bind( this );
const WHEEL_DEBOUNCE_TIME = 400;
let wheelFinishChangeTimeout;
const onWheel = e => {
const onWheel = e => {
// ignore vertical wheels if there's a scrollbar
// ignore vertical wheels if there's a scrollbar
const isVertical = Math.abs( e.deltaX ) < Math.abs( e.deltaY );
const isVertical = Math.abs( e.deltaX ) < Math.abs( e.deltaY );
if ( isVertical && this._hasScrollBar ) return;
if ( isVertical && this._hasScrollBar ) return;
e.preventDefault();
e.preventDefault();
// set value
const delta = this._normalizeMouseWheel( e ) * this._step;
const delta = this._normalizeMouseWheel( e ) * this._step;
this._snapClampSetValue( this.getValue() + delta );
this._snapClampSetValue( this.getValue() + delta );
// force the input to updateDisplay when it's focused
this.$input.value = this.getValue();
// debounce onFinishChange
clearTimeout( wheelFinishChangeTimeout );
wheelFinishChangeTimeout = setTimeout( callOnFinishChange, WHEEL_DEBOUNCE_TIME );
};
};
this.$slider.addEventListener( 'wheel', onWheel, { passive: false } );
this.$slider.addEventListener( 'mousedown', mouseDown );
this.$slider.addEventListener( 'touchstart', onTouchStart );
this.$slider.addEventListener( 'wheel', onWheel );
}
}
_setActiveStyle( active ) {
_setDraggingStyle( active, axis = 'horizontal' ) {
this.$slider.classList.toggle( 'active', active );
if ( this.$slider ) {
document.body.classList.toggle( 'lil-gui-slider-active', active );
this.$slider.classList.toggle( 'active', active );
}
document.body.classList.toggle( 'lil-gui-dragging', active );
document.body.classList.toggle( `lil-gui-${axis}`, active );
}
}
_getImplicitStep() {
_getImplicitStep() {
if ( this._hasMin && this._hasMax ) {
if ( this._hasMin && this._hasMax ) {
return ( this._max - this._min ) / 1000;
return ( this._max - this._min ) / 1000;
}
}
return 0.1;
return 0.1;
}
}
_onUpdateMinMax() {
_onUpdateMinMax() {
if ( !this._hasSlider && this._hasMin && this._hasMax ) {
if ( !this._hasSlider && this._hasMin && this._hasMax ) {
// If this is the first time we're hearing about min and max
// If this is the first time we're hearing about min and max
// and we haven't explicitly stated what our step is, let's
// and we haven't explicitly stated what our step is, let's
// update that too.
// update that too.
if ( !this._stepExplicit ) {
if ( !this._stepExplicit ) {
this.step( this._getImplicitStep(), false );
this.step( this._getImplicitStep(), false );
}
}
this._initSlider();
this._initSlider();
this.updateDisplay();
this.updateDisplay();
}
}
}
}
_normalizeMouseWheel( e ) {
_normalizeMouseWheel( e ) {
let { deltaX, deltaY } = e;
let { deltaX, deltaY } = e;
// 2019: Safari and Chrome report weird non-integral values for an actual
// Safari and Chrome report weird non-integral values for a notched wheel,
// mouse with a wheel connected to my 2015 macbook, but still expose actual
// but still expose actual lines scrolled via wheelDelta. Notched wheels
// lines scrolled via wheelDelta.
// should behave the same way as arrow keys.
if ( Math.floor( e.deltaY ) !== e.deltaY && e.wheelDelta ) {
if ( Math.floor( e.deltaY ) !== e.deltaY && e.whe
deltaX = 0;
deltaY = -e.wheelDelta / 120;
}
const wheel = deltaX + -deltaY;
return wheel;
}
_arrowKeyMultiplier( e ) {
if ( this._stepExplicit ) {
return e.shiftKey ? 10 : 1;
} else if ( e.shiftKey ) {
return 100;
} else if ( e.altKey ) {
return 1;
}
return 10;
}
_snap( value ) {
// This would be the logical way to do things, but floating point errors.
// return Math.round( value / this._step ) * this._step;
// Using inverse step solves a lot of them, but not all
// const inverseStep = 1 / this._step;
// return Math.round( value * inverseStep ) / inverseStep;
// Not happy about this, but haven't seen it break.
const r = Math.round( value / this._step ) * this._step;
return parseFloat( r.toPrecision( 15 ) );
}
_clamp( value ) {
const min = this._hasMin ? this._min : -Infinity;
const max = this._hasMax ? this._max : Infinity;
return Math.max( min, Math.min( max, value ) );
}
_snapClampSetValue( value ) {
this.setValue( this._clamp( this._snap( value ) ) );
}
get _hasScrollBar() {
const root = this.parent.root.$children;
return root.scrollHeight > root.clientHeight;
}
get _hasMin() {
return this._min !== undefined;
}
get _hasMax() {
return this._max !== undefined;
}
}
class OptionController extends Controller {
constructor( parent, object, property, options ) {
super( parent, object, property, 'option' );
this.$select = document.createElement( 'select' );
this.$select.setAttribute( 'aria-labelledby', this.$name.id );
this.$display = document.createElement( 'div' );
this.$display.classList.add( 'display' );
this._values = Array.isArray( options ) ? options : Object.values( options );
this._names = Array.isArray( options ) ? options : Object.keys( options );
this._names.forEach( name => {
const $option = document.createElement( 'option' );
$option.innerHTML = name;
this.$select.appendChild( $option );
} );
this.$select.addEventListener( 'change', () => {
this.setValue( this._values[ this.$select.selectedIndex ] );
} );
this.$select.addEventListener( 'focus', () => {
this.$display.classList.add( 'focus' );
} );
this.$select.addEventListener( 'blur', () => {
this.$display.classList.remove( 'focus' );
} );
this.$widget.appendChild( this.$select );
this.$widget.appendChild( this.$display );
this.$disable = this.$select;
this.updateDisplay();
}
updateDisplay() {
const value = this.getValue();
const index = this._values.indexOf( value );
this.$select.selectedIndex = index;
this.$display.innerHTML = index === -1 ? value : this._names[ index ];
Text moved to lines 166-168
return this;
}
}
class StringController extends Controller {
constructor( parent, object, property ) {
super( parent, object, property, 'string' );
this.$input = document.createElement( 'input' );
this.$input.setAttribute( 'type', 'text' );
this.$input.setAttribute( 'aria-labelledby', this.$name.id );
this.$input.addEventListener( 'input', () => {
this.setValue( this.$input.value );
} );
this.$input.addEventListener( 'keydown', e => {
if ( e.code === 'Enter' ) {
this.$input.blur();
}
} );
this.$widget.appendChild( this.$input );
this.$disable = this.$input;
this.updateDisplay();
}
updateDisplay() {
this.$input.value = this.getValue();
return this;
}
}
const stylesheet = `.lil-gui {
font-family: var(--font-family);
font-size: var(--font-size);
line-height: 1;
font-we