Diff
checker
Texto
Texto
Imágenes
Documentos
Excel
Carpetas
Legal
Enterprise
Aplicación de escritorio
Precios
Iniciar sesión
Descargar Diffchecker Desktop
Comparar texto
Encuentra la diferencia entre dos archivos de texto
Herramientas
Historial
Editor live
Ocultar espacios en blanco
Ocultar sin cambios
Sin ajuste de línea
Vista
Dividido
Unificado
Nivel de detalle
Inteligente
Palabra
Letra
Estilos de texto
Cambiar apariencia
Resaltado de sintaxis
Elegir sintaxis
Ignorar
Transformar texto
Ir al primer cambio
Editar entrada
Diffchecker Desktop
La forma más segura de usar Diffchecker. ¡Obtén la app de Diffchecker Desktop: tus diffs nunca salen de tu computadora!
Obtener Desktop
lil-gui 0.11.0 → 0.16.0
Creado
hace 5 años
El diff nunca expira
Borrar
Exportar
Compartir
Explicar
243 eliminaciones
Líneas
Total
Eliminado
Caracteres
Total
Eliminado
Para continuar usando esta función, actualice a
Diff
checker
Pro
Ver precios
1069 líneas
Copiar todo
226 adiciones
Líneas
Total
Añadido
Caracteres
Total
Añadido
Para continuar usando esta función, actualice a
Diff
checker
Pro
Ver precios
1054 líneas
Copiar todo
/**
/**
* lil-gui
* lil-gui
* https://lil-gui.georgealways.com
* https://lil-gui.georgealways.com
Copiar
Copiado
Copiar
Copiado
* @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' );
*
*
Copiar
Copiado
Copiar
Copiado
* 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 ) {
/**
/**
Copiar
Copiado
Copiar
Copiado
* Used to access the function bound to
c
hange
events. Don't modify this value directly.
* Used to access the function bound to
`onC
hange
`
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;
}
}
Copiar
Copiado
Copiar
Copiado
/**
* Calls the onChange methods of this controller and its parent GUI.
* @protected
*/
_callOnChange() {
_callOnChange() {
Copiar
Copiado
Copiar
Copiado
this.parent._callOnChange( this );
this.parent._callOnChange( this );
Copiar
Copiado
Copiar
Copiado
if ( this._onChange !== undefined ) {
if ( this._onChange !== undefined ) {
this._onChange.call( this, this.getValue() );
this._onChange.call( this, this.getValue() );
}
}
Copiar
Copiado
Copiar
Copiado
this._changed = true;
}
}
Copiar
Copiado
Copiar
Copiado
// 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 ) {
Copiar
Copiado
Copiar
Copiado
return
this
.on
Change( callback )
;
/**
* Used to access the function bound to `onFinishChange` events. Don't modify
this
value
* directly. Use the `controller
.on
Finish
Change( 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 );
Copiar
Copiado
Copiar
Copiado
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 );
Copiar
Copiado
Copiar
Copiado
this.$disable.
toggle
Attribute( 'disabled',
disabled
);
if ( disabled ) {
this.$disable.
set
Attribute( '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() {
Copiar
Copiado
Copiar
Copiado
this._listenCallbackID = requestAnimationFrame( this._listenCallback );
this._listenCallbackID = requestAnimationFrame( this._listenCallback );
Copiar
Copiado
Copiar
Copiado
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 );
Copiar
Copiado
Copiar
Copiado
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' );
Copiar
Copiado
Copiar
Copiado
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 );
Copiar
Copiado
Copiar
Copiado
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;
Copiar
Copiado
Copiar
Copiado
const onInputChange =
() => {
this.$input.addEventListener( 'input',
() => {
this._setValueFromHexString( this.$input.value );
this._setValueFromHexString( this.$input.value );
Copiar
Copiado
Copiar
Copiado
};
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', () => {
Copiar
Copiado
Copiar
Copiado
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();
Copiar
Copiado
Copiar
Copiado
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 );
Copiar
Copiado
Copiar
Copiado
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 ) {
Copiar
Copiado
Copiar
Copiado
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' );
Copiar
Copiado
Copiar
Copiado
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 ) );
};
};
Copiar
Copiado
Copiar
Copiado
//
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();
Copiar
Copiado
Copiar
Copiado
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();
Copiar
Copiado
Copiar
Copiado
increment( this.
_normalizeMouseWheel( e )
* this._step
);
increment( this.
_step * this.
_normalizeMouseWheel( e )
);
}
}
};
};
Copiar
Copiado
Copiar
Copiado
// 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();
Copiar
Copiado
Copiar
Copiado
this._callOnFinishChange();
};
};
Copiar
Copiado
Copiar
Copiado
this.$input.addEventListener( 'focus', onFocus );
this.$input.addEventListener( 'input', onInput );
this.$input.addEventListener( 'input', onInput );
Copiar
Copiado
Copiar
Copiado
this.$input.addEventListener( 'blur', onBlur );
this.$input.addEventListener( 'keydown', onKeyDown );
this.$input.addEventListener( 'keydown', onKeyDown );
Copiar
Copiado
Copiar
Copiado
this.$input.addEventListener( 'wheel', onWheel
, { passive: false } );
this.$input.addEventListener( 'wheel', onWheel
);
this.$input.addEventListener( 'mousedown', onMouseDown );
this.$input.addEventListener( 'focus', onFocus );
this.$input.addEventListener( 'blur', onBlur );
}
}
_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 );
};
};
Copiar
Copiado
Copiar
Copiado
//
Bind m
ouse
listeners
//
M
ouse
drag
// ---------------------------------------------------------------------
// ---------------------------------------------------------------------
const mouseDown = e => {
const mouseDown = e => {
Copiar
Copiado
Copiar
Copiado
this._setDraggingStyle( true );
setValueFromX( e.clientX );
setValueFromX( e.clientX );
Copiar
Copiado
Copiar
Copiado
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 = () => {
Copiar
Copiado
Copiar
Copiado
this._
setActiveStyle
( false );
this._callOnFinishChange();
this._
setDraggingStyle
( false );
window.removeEventListener( 'mousemove', mouseMove );
window.removeEventListener( 'mousemove', mouseMove );
window.removeEventListener( 'mouseup', mouseUp );
window.removeEventListener( 'mouseup', mouseUp );
};
};
Copiar
Copiado
Copiar
Copiado
this.$slider.addEventListener( 'mousedown', mouseDown );
//
T
ouch
drag
//
Bind t
ouch
listeners
// ---------------------------------------------------------------------
// ---------------------------------------------------------------------
let testingForScroll = false, prevClientX, prevClientY;
let testingForScroll = false, prevClientX, prevClientY;
Copiar
Copiado
Copiar
Copiado
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.
Copiar
Copiado
Copiar
Copiado
e.preventDefault();
beginTouchDrag( e );
setValueFromX( e.touches[ 0 ].clientX );
this._setActiveStyle( true );
testingForScroll = false;
}
}
Copiar
Copiado
Copiar
Copiado
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.
Copiar
Copiado
Copiar
Copiado
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 = () => {
Copiar
Copiado
Copiar
Copiado
this._
setActiveStyle
( false );
this._callOnFinishChange();
this._
setDraggingStyle
( false );
window.removeEventListener( 'touchmove', onTouchMove );
window.removeEventListener( 'touchmove', onTouchMove );
window.removeEventListener( 'touchend', onTouchEnd );
window.removeEventListener( 'touchend', onTouchEnd );
};
};
Copiar
Copiado
Copiar
Copiado
this.$slider.addEventListener( 'touchstart', onTouchStart );
//
Mouse
wheel
//
Bind
wheel
listeners
// ---------------------------------------------------------------------
// ---------------------------------------------------------------------
Copiar
Copiado
Copiar
Copiado
// 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();
Copiar
Copiado
Copiar
Copiado
// 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 );
Copiar
Copiado
Copiar
Copiado
// force the input to updateDisplay when it's focused
this.$input.value = this.getValue();
// debounce onFinishChange
clearTimeout( wheelFinishChangeTimeout );
wheelFinishChangeTimeout = setTimeout( callOnFinishChange, WHEEL_DEBOUNCE_TIME );
};
};
Copiar
Copiado
Copiar
Copiado
this.$slider.addEventListener( 'wheel', onWheel
, { passive: false }
);
this.$slider.addEventListener( 'mousedown', mouseDown );
this.$slider.addEventListener( 'touchstart', onTouchStart );
this.$slider.addEventListener( 'wheel', onWheel
);
}
}
Copiar
Copiado
Copiar
Copiado
_
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;
Copiar
Copiado
Copiar
Copiado
//
2019:
Safari and Chrome report weird non-integral values for a
n 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
Diferencias guardadas
Texto original
Abrir archivo
/** * lil-gui * https://lil-gui.georgealways.com * @version 0.11.0 * @author George Michael Brower * @license MIT */ /** * Base class for all controllers. */ class Controller { constructor( parent, object, property, className, widgetTag = 'div' ) { /** * The GUI that contains this controller. * @type {GUI} */ this.parent = parent; /** * The object this controller will modify. * @type {object} */ this.object = object; /** * The name of the property to control. * @type {string} */ this.property = property; /** * Used to determine if the controller is disabled. * Use `controller.disable( true|false )` to modify this value * @type {boolean} */ this._disabled = false; /** * The value of `object[ property ]` when the controller was created. * @type {any} */ this.initialValue = this.getValue(); /** * The outermost container DOM element for this controller. * @type {HTMLElement} */ this.domElement = document.createElement( 'div' ); this.domElement.classList.add( 'controller' ); this.domElement.classList.add( className ); /** * The DOM element that contains the controller's name. * @type {HTMLElement} */ this.$name = document.createElement( 'div' ); this.$name.classList.add( 'name' ); Controller.nextNameID = Controller.nextNameID || 0; this.$name.id = `lil-gui-name-${++Controller.nextNameID}`; /** * The DOM element that contains the controller's "widget" (which differs by controller type). * @type {HTMLElement} */ this.$widget = document.createElement( widgetTag ); this.$widget.classList.add( 'widget' ); /** * The DOM element that receives the disabled attribute when using disable() * @type {HTMLElement} */ this.$disable = this.$widget; this.domElement.appendChild( this.$name ); this.domElement.appendChild( this.$widget ); this.parent.children.push( this ); this.parent.controllers.push( this ); this.parent.$children.appendChild( this.domElement ); this._listenCallback = this._listenCallback.bind( this ); this.name( property ); } /** * Sets the name of the controller and its label in the GUI. * @param {string} name * @returns {this} */ name( name ) { /** * The controller's name. Use `controller.name( 'Name' )` to modify this value. * @type {string} */ this._name = name; this.$name.innerHTML = name; return this; } /** * 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 * controller. * @param {Function} callback * @returns {this} * @example * const controller = gui.add( object, 'property' ); * * controller.onChange( v => { * console.log( 'The value is now ' + v ); * console.assert( this === controller ); * } ); */ onChange( callback ) { /** * Used to access the function bound to change events. Don't modify this value directly. * Use the `controller.onChange( callback )` method instead. * @type {Function} */ this._onChange = callback; return this; } _callOnChange() { this.parent._callOnChange( this ); if ( this._onChange !== undefined ) { this._onChange.call( this, this.getValue() ); } } // Provided for compatability onFinishChange( callback ) { return this.onChange( callback ); } /** * Sets the controller back to its initial value. * @returns {this} */ reset() { this.setValue( this.initialValue ); return this; } /** * Enables this controller. * @param {boolean} enabled * @returns {this} * @example * controller.enable(); * controller.enable( false ); // disable * controller.enable( controller._disabled ); // toggle */ enable( enabled = true ) { return this.disable( !enabled ); } /** * Disables this controller. * @param {boolean} disabled * @returns {this} * @example * controller.disable(); * controller.disable( false ); // enable * controller.disable( !controller._disabled ); // toggle */ disable( disabled = true ) { if ( disabled === this._disabled ) return this; this._disabled = disabled; this.domElement.classList.toggle( 'disabled', disabled ); if ( disabled ) { this.$disable.setAttribute( 'disabled', 'disabled' ); } else { this.$disable.removeAttribute( 'disabled' ); } return this; } /** * 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. * * 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 * the GUI. * @example * // safe usage * * gui.add( object1, 'property' ).options( [ 'a', 'b', 'c' ] ); * gui.add( object2, 'property' ); * * // danger * * const c = gui.add( object1, 'property' ); * gui.add( object2, 'property' ); * * c.options( [ 'a', 'b', 'c' ] ); * // controller is now at the end of the GUI even though it was added first * * assert( c.parent.children.indexOf( c ) === -1 ) * // c references a controller that no longer exists * * @param {object|Array} options * @returns {Controller} */ options( options ) { const controller = this.parent.add( this.object, this.property, options ); controller.name( this._name ); this.destroy(); return controller; } /** * Sets the minimum value. Only works on number controllers. * @param {number} min * @returns {this} */ min( min ) { return this; } /** * Sets the maximum value. Only works on number controllers. * @param {number} max * @returns {this} */ max( max ) { return this; } /** * Sets the step. Only works on number controllers. * @param {number} step * @returns {this} */ step( step ) { return this; } /** * Calls `updateDisplay()` every animation frame. Pass `false` to stop listening. * @param {boolean} listen * @returns {this} */ listen( listen = true ) { /** * Used to determine if the controller is currently listening. Don't modify this value * directly. Use the `controller.listen( true|false )` method instead. * @type {boolean} */ this._listening = listen; if ( this._listenCallbackID !== undefined ) { cancelAnimationFrame( this._listenCallbackID ); this._listenCallbackID = undefined; } if ( this._listening ) { this._listenCallback(); } return this; } _listenCallback() { this._listenCallbackID = requestAnimationFrame( this._listenCallback ); 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 {any} */ getValue() { return this.object[ this.property ]; } /** * Sets the value of `object[ property ]`, invokes any `onChange` handlers and updates the display. * @param {any} value * @returns {this} */ setValue( value ) { this.object[ this.property ] = value; this._callOnChange(); this.updateDisplay(); return this; } /** * 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. * @returns {this} */ updateDisplay() { return this; } load( value ) { this.setValue( value ); } save() { return this.getValue(); } /** * Destroys this controller and removes it from the parent GUI. */ destroy() { this.parent.children.splice( this.parent.children.indexOf( this ), 1 ); this.parent.controllers.splice( this.parent.controllers.indexOf( this ), 1 ); this.parent.$children.removeChild( this.domElement ); } } class BooleanController extends Controller { constructor( parent, object, property ) { super( parent, object, property, 'boolean', 'label' ); this.$input = document.createElement( 'input' ); this.$input.setAttribute( 'type', 'checkbox' ); this.$widget.appendChild( this.$input ); this.$input.addEventListener( 'change', () => { this.setValue( this.$input.checked ); } ); this.$disable = this.$input; this.updateDisplay(); } updateDisplay() { this.$input.checked = this.getValue(); return this; } } function normalizeColorString( string ) { let match, result; if ( match = string.match( /(#|0x)?([a-f0-9]{6})/i ) ) { result = match[ 2 ]; } else if ( match = string.match( /rgb\(\s*(\d*)\s*,\s*(\d*)\s*,\s*(\d*)\s*\)/ ) ) { result = parseInt( match[ 1 ] ).toString( 16 ).padStart( 2, 0 ) + parseInt( match[ 2 ] ).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 ) ) { result = match[ 1 ] + match[ 1 ] + match[ 2 ] + match[ 2 ] + match[ 3 ] + match[ 3 ]; } if ( result ) { return '#' + result; } return false; } const STRING = { isPrimitive: true, match: v => typeof v === 'string', fromHexString: normalizeColorString, toHexString: normalizeColorString }; const INT = { isPrimitive: true, match: v => typeof v === 'number', fromHexString: string => parseInt( string.substring( 1 ), 16 ), toHexString: value => '#' + value.toString( 16 ).padStart( 6, 0 ) }; const ARRAY = { isPrimitive: false, match: Array.isArray, fromHexString( string, target, rgbScale = 1 ) { const int = INT.fromHexString( string ); target[ 0 ] = ( int >> 16 & 255 ) / 255 * rgbScale; target[ 1 ] = ( int >> 8 & 255 ) / 255 * rgbScale; target[ 2 ] = ( int & 255 ) / 255 * rgbScale; }, toHexString( [ r, g, b ], rgbScale = 1 ) { rgbScale = 255 / rgbScale; const int = ( r * rgbScale ) << 16 ^ ( g * rgbScale ) << 8 ^ ( b * rgbScale ) << 0; return INT.toHexString( int ); } }; const OBJECT = { isPrimitive: false, match: v => Object( v ) === v, fromHexString( string, target, rgbScale = 1 ) { const int = INT.fromHexString( string ); target.r = ( int >> 16 & 255 ) / 255 * rgbScale; target.g = ( int >> 8 & 255 ) / 255 * rgbScale; target.b = ( int & 255 ) / 255 * rgbScale; }, toHexString( { r, g, b }, rgbScale = 1 ) { rgbScale = 255 / rgbScale; const int = ( r * rgbScale ) << 16 ^ ( g * rgbScale ) << 8 ^ ( b * rgbScale ) << 0; return INT.toHexString( int ); } }; const FORMATS = [ STRING, INT, ARRAY, OBJECT ]; function getColorFormat( value ) { return FORMATS.find( format => format.match( value ) ); } class ColorController extends Controller { constructor( parent, object, property, rgbScale ) { super( parent, object, property, 'color' ); this.$input = document.createElement( 'input' ); this.$input.setAttribute( 'type', 'color' ); this.$input.setAttribute( 'tabindex', -1 ); this.$input.setAttribute( 'aria-labelledby', this.$name.id ); this.$text = document.createElement( 'input' ); this.$text.setAttribute( 'type', 'text' ); this.$text.setAttribute( 'spellcheck', 'false' ); this.$text.setAttribute( 'aria-labelledby', this.$name.id ); this.$display = document.createElement( 'div' ); this.$display.classList.add( 'display' ); this.$display.appendChild( this.$input ); this.$widget.appendChild( this.$display ); this.$widget.appendChild( this.$text ); this._format = getColorFormat( this.initialValue ); this._rgbScale = rgbScale; this._initialValueHexString = this.save(); this._textFocused = false; const onInputChange = () => { 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.$display.classList.remove( 'focus' ); } ); this.$text.addEventListener( 'input', () => { const tryParse = normalizeColorString( this.$text.value ); if ( tryParse ) { this._setValueFromHexString( tryParse ); } } ); this.$text.addEventListener( 'focus', () => { this._textFocused = true; this.$text.select(); } ); this.$text.addEventListener( 'blur', () => { this._textFocused = false; this.updateDisplay(); } ); this.$disable = this.$text; this.updateDisplay(); } reset() { this._setValueFromHexString( this._initialValueHexString ); return this; } _setValueFromHexString( value ) { if ( this._format.isPrimitive ) { const newValue = this._format.fromHexString( value ); this.setValue( newValue ); } else { this._format.fromHexString( value, this.getValue(), this._rgbScale ); this._callOnChange(); this.updateDisplay(); } } save() { return this._format.toHexString( this.getValue(), this._rgbScale ); } load( value ) { this._setValueFromHexString( value ); } updateDisplay() { this.$input.value = this._format.toHexString( this.getValue(), this._rgbScale ); if ( !this._textFocused ) { this.$text.value = this.$input.value.substring( 1 ); } this.$display.style.backgroundColor = this.$input.value; return this; } } class FunctionController extends Controller { constructor( parent, object, property ) { super( parent, object, property, 'function' ); // Buttons are the only case where widget contains name this.$button = document.createElement( 'button' ); this.$button.appendChild( this.$name ); this.$widget.appendChild( this.$button ); this.$button.addEventListener( 'click', e => { e.preventDefault(); this.getValue().call( this.object ); } ); // enables :active pseudo class on mobile this.$button.addEventListener( 'touchstart', () => { } ); this.$disable = this.$button; } } class NumberController extends Controller { constructor( parent, object, property, min, max, step ) { super( parent, object, property, 'number' ); this._initInput(); this.min( min ); this.max( max ); const stepExplicit = step !== undefined; this.step( stepExplicit ? step : this._getImplicitStep(), stepExplicit ); this.updateDisplay(); } min( min ) { this._min = min; this._onUpdateMinMax(); return this; } max( max ) { this._max = max; this._onUpdateMinMax(); return this; } step( step, explicit = true ) { this._step = step; this._stepExplicit = explicit; return this; } updateDisplay() { const value = this.getValue(); if ( this._hasSlider ) { const percent = ( value - this._min ) / ( this._max - this._min ); this.$fill.style.setProperty( 'width', percent * 100 + '%' ); } if ( !this._inputFocused ) { this.$input.value = value; } return this; } _initInput() { this.$input = document.createElement( 'input' ); this.$input.setAttribute( 'type', 'text' ); this.$input.setAttribute( 'inputmode', 'numeric' ); this.$input.setAttribute( 'aria-labelledby', this.$name.id ); this.$widget.appendChild( this.$input ); this.$disable = this.$input; const onInput = () => { const value = parseFloat( this.$input.value ); if ( isNaN( value ) ) return; this.setValue( this._clamp( value ) ); }; // invoked on wheel or arrow key up/down const increment = delta => { const value = parseFloat( this.$input.value ); if ( isNaN( value ) ) return; this._snapClampSetValue( value + delta ); // Force the input to updateDisplay when it's focused this.$input.value = this.getValue(); }; const onKeyDown = e => { if ( e.code === 'Enter' ) { this.$input.blur(); } if ( e.code === 'ArrowUp' ) { e.preventDefault(); increment( this._step * this._arrowKeyMultiplier( e ) ); } if ( e.code === 'ArrowDown' ) { e.preventDefault(); increment( -1 * this._step * this._arrowKeyMultiplier( e ) ); } }; const onWheel = e => { if ( this._inputFocused ) { e.preventDefault(); increment( this._normalizeMouseWheel( e ) * this._step ); } }; const onFocus = () => { this._inputFocused = true; }; const onBlur = () => { this._inputFocused = false; this.updateDisplay(); }; this.$input.addEventListener( 'focus', onFocus ); this.$input.addEventListener( 'input', onInput ); this.$input.addEventListener( 'blur', onBlur ); this.$input.addEventListener( 'keydown', onKeyDown ); this.$input.addEventListener( 'wheel', onWheel, { passive: false } ); } _initSlider() { this._hasSlider = true; // Build DOM // --------------------------------------------------------------------- this.$slider = document.createElement( 'div' ); this.$slider.classList.add( 'slider' ); this.$fill = document.createElement( 'div' ); this.$fill.classList.add( 'fill' ); this.$slider.appendChild( this.$fill ); this.$widget.insertBefore( this.$slider, this.$input ); this.domElement.classList.add( 'hasSlider' ); // Map clientX to value // --------------------------------------------------------------------- const map = ( v, a, b, c, d ) => { return ( v - a ) / ( b - a ) * ( d - c ) + c; }; const setValueFromX = clientX => { const rect = this.$slider.getBoundingClientRect(); let value = map( clientX, rect.left, rect.right, this._min, this._max ); this._snapClampSetValue( value ); }; // Bind mouse listeners // --------------------------------------------------------------------- const mouseDown = e => { setValueFromX( e.clientX ); this._setActiveStyle( true ); window.addEventListener( 'mousemove', mouseMove ); window.addEventListener( 'mouseup', mouseUp ); }; const mouseMove = e => { setValueFromX( e.clientX ); }; const mouseUp = () => { this._setActiveStyle( false ); window.removeEventListener( 'mousemove', mouseMove ); window.removeEventListener( 'mouseup', mouseUp ); }; this.$slider.addEventListener( 'mousedown', mouseDown ); // Bind touch listeners // --------------------------------------------------------------------- let testingForScroll = false, prevClientX, prevClientY; const onTouchStart = e => { if ( e.touches.length > 1 ) return; // 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. if ( this._hasScrollBar ) { prevClientX = e.touches[ 0 ].clientX; prevClientY = e.touches[ 0 ].clientY; testingForScroll = true; } else { // Otherwise, we can set the value straight away on touchstart. e.preventDefault(); setValueFromX( e.touches[ 0 ].clientX ); this._setActiveStyle( true ); testingForScroll = false; } window.addEventListener( 'touchmove', onTouchMove, { passive: false } ); window.addEventListener( 'touchend', onTouchEnd ); }; const onTouchMove = e => { if ( testingForScroll ) { const dx = e.touches[ 0 ].clientX - prevClientX; const dy = e.touches[ 0 ].clientY - prevClientY; if ( Math.abs( dx ) > Math.abs( dy ) ) { // We moved horizontally, set the value and stop checking. e.preventDefault(); setValueFromX( e.touches[ 0 ].clientX ); this._setActiveStyle( true ); testingForScroll = false; } else { // This was, in fact, an attempt to scroll. Abort. window.removeEventListener( 'touchmove', onTouchMove ); window.removeEventListener( 'touchend', onTouchEnd ); } } else { e.preventDefault(); setValueFromX( e.touches[ 0 ].clientX ); } }; const onTouchEnd = () => { this._setActiveStyle( false ); window.removeEventListener( 'touchmove', onTouchMove ); window.removeEventListener( 'touchend', onTouchEnd ); }; this.$slider.addEventListener( 'touchstart', onTouchStart ); // Bind wheel listeners // --------------------------------------------------------------------- const onWheel = e => { // ignore vertical wheels if there's a scrollbar const isVertical = Math.abs( e.deltaX ) < Math.abs( e.deltaY ); if ( isVertical && this._hasScrollBar ) return; e.preventDefault(); const delta = this._normalizeMouseWheel( e ) * this._step; this._snapClampSetValue( this.getValue() + delta ); }; this.$slider.addEventListener( 'wheel', onWheel, { passive: false } ); } _setActiveStyle( active ) { this.$slider.classList.toggle( 'active', active ); document.body.classList.toggle( 'lil-gui-slider-active', active ); } _getImplicitStep() { if ( this._hasMin && this._hasMax ) { return ( this._max - this._min ) / 1000; } return 0.1; } _onUpdateMinMax() { if ( !this._hasSlider && this._hasMin && this._hasMax ) { // 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 // update that too. if ( !this._stepExplicit ) { this.step( this._getImplicitStep(), false ); } this._initSlider(); this.updateDisplay(); } } _normalizeMouseWheel( e ) { let { deltaX, deltaY } = e; // 2019: Safari and Chrome report weird non-integral values for an actual // mouse with a wheel connected to my 2015 macbook, but still expose actual // lines scrolled via wheelDelta. if ( Math.floor( e.deltaY ) !== e.deltaY && e.wheelDelta ) { 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 ]; 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-weight: normal; font-style: normal; text-align: left; background-color: var(--background-color); color: var(--text-color); user-select: none; -webkit-user-select: none; touch-action: manipulation; --background-color: #1f1f1f; --text-color: #ebebeb; --title-background-color: #111111; --title-text-color: #ebebeb; --widget-color: #424242; --hover-color: #4f4f4f; --focus-color: #595959; --number-color: #2cc9ff; --string-color: #a2db3c; --font-size: 11px; --input-font-size: 11px; --font-family: -apple-system, BlinkMacSystemFont, "Lucida Grande", "Segoe UI", Roboto, Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol"; --font-family-mono: Menlo, Monaco, Consolas, "Droid Sans Mono", monospace, "Droid Sans Fallback"; --padding: 4px; --spacing: 4px; --widget-height: 20px; --name-width: 45%; --slider-knob-width: 2px; --slider-input-width: 27%; --color-input-width: 27%; --slider-input-min-width: 45px; --color-input-min-width: 45px; --folder-indent: 7px; --widget-padding: 0 0 0 3px; --widget-border-radius: 2px; --checkbox-size: calc(0.75 * var(--widget-height)); --scrollbar-width: 5px; } .lil-gui, .lil-gui * { box-sizing: border-box; margin: 0; } .lil-gui.root { width: var(--width, 245px); display: flex; flex-direction: column; } .lil-gui.root > .title { background: var(--title-background-color); color: var(--title-text-color); } .lil-gui.root > .children { overflow: auto; } .lil-gui.root > .children::-webkit-scrollbar { width: var(--scrollbar-width); height: var(--scrollbar-width); background: var(--background-color); } .lil-gui.root > .children::-webkit-scrollbar-thumb { border-radius: var(--scrollbar-width); background: var(--focus-color); } .lil-gui .lil-gui { --background-color: inherit; --text-color: inherit; --title-background-color: inherit; --title-text-color: inherit; --widget-color: inherit; --hover-color: inherit; --focus-color: inherit; --number-color: inherit; --string-color: inherit; --font-size: inherit; --input-font-size: inherit; --font-family: inherit; --font-family-mono: inherit; --padding: inherit; --spacing: inherit; --widget-height: inherit; --name-width: inherit; --slider-knob-width: inherit; --slider-input-width: inherit; --color-input-width: inherit; --slider-input-min-width: inherit; --color-input-min-width: inherit; --folder-indent: inherit; --widget-padding: inherit; --widget-border-radius: inherit; --checkbox-size: inherit; } @media (pointer: coarse) { .lil-gui.allow-touch-styles { --widget-height: 28px; --padding: 6px; --spacing: 6px; --font-size: 13px; --input-font-size: 16px; --folder-indent: 10px; --widget-padding: 0 0 0 3px; --scrollbar-width: 7px; --slider-input-min-width: 50px; --color-input-min-width: 65px; } } .lil-gui.force-touch-styles { --widget-height: 28px; --padding: 6px; --spacing: 6px; --font-size: 13px; --input-font-size: 16px; --folder-indent: 10px; --widget-padding: 0 0 0 3px; --scrollbar-width: 7px; --slider-input-min-width: 50px; --color-input-min-width: 65px; } .lil-gui.autoPlace { max-height: 100%; position: fixed; top: 0; right: 15px; z-index: 1001; } .lil-gui .controller { display: flex; align-items: center; padding: 0 var(--padding); margin: var(--spacing) 0; } .lil-gui .controller.disabled { opacity: 0.5; } .lil-gui .controller.disabled, .lil-gui .controller.disabled * { pointer-events: none !important; } .lil-gui .controller .name { min-width: var(--name-width); flex-shrink: 0; white-space: pre; padding-right: var(--spacing); line-height: var(--widget-height); } .lil-gui .controller .widget { position: relative; display: flex; align-items: center; width: 100%; min-height: var(--widget-height); } .lil-gui .controller.function .name { line-height: unset; padding: 0; } .lil-gui .controller.string input { color: var(--string-color); } .lil-gui .controller.boolean .widget { cursor: pointer; } .lil-gui .controller.color .display { width: 100%; height: var(--widget-height); border-radius: var(--widget-border-radius); position: relative; } @media (hover: hover) { .lil-gui .controller.color .display:hover:before { content: " "; display: block; position: absolute; border-radius: var(--widget-border-radius); border: 1px solid #fff9; left: 0; right: 0; top: 0; bottom: 0; } } .lil-gui .controller.color input[type=color] { opacity: 0; width: 100%; height: 100%; cursor: pointer; } .lil-gui .controller.color input[type=text] { margin-left: var(--spacing); font-family: var(--font-family-mono); min-width: var(--color-input-min-width); width: var(--color-input-width); flex-shrink: 0; } .lil-gui .controller.option select { opacity: 0; position: absolute; width: 100%; max-width: 100%; } .lil-gui .controller.option .display { position: relative; pointer-events: none; border-radius: var(--widget-border-radius); height: var(--widget-height); line-height: var(--widget-height); max-width: 100%; overflow: hidden; word-break: break-all; padding-left: 0.55em; padding-right: 1.75em; background: var(--widget-color); } @media (hover: hover) { .lil-gui .controller.option .display.focus { background: var(--focus-color); } } .lil-gui .controller.option .display.active { background: var(--focus-color); } .lil-gui .controller.option .display:after { font-family: "lil-gui"; content: "↕"; position: absolute; top: 0; right: 0; bottom: 0; padding-right: 0.375em; } .lil-gui .controller.option .widget, .lil-gui .controller.option select { cursor: pointer; } @media (hover: hover) { .lil-gui .controller.option .widget:hover .display { background: var(--hover-color); } } .lil-gui .controller.number input { color: var(--number-color); } .lil-gui .controller.number.hasSlider input { margin-left: var(--spacing); width: var(--slider-input-width); min-width: var(--slider-input-min-width); flex-shrink: 0; } .lil-gui .controller.number .slider { width: 100%; height: var(--widget-height); background-color: var(--widget-color); border-radius: var(--widget-border-radius); padding-right: var(--slider-knob-width); overflow: hidden; cursor: ew-resize; touch-action: pan-y; } @media (hover: hover) { .lil-gui .controller.number .slider:hover { background-color: var(--hover-color); } } .lil-gui .controller.number .slider.active { background-color: var(--focus-color); } .lil-gui .controller.number .slider.active .fill { opacity: 0.95; } .lil-gui .controller.number .fill { height: 100%; border-right: var(--slider-knob-width) solid var(--number-color); box-sizing: content-box; } .lil-gui-slider-active .lil-gui { --hover-color: var(--widget-color); } .lil-gui-slider-active * { cursor: ew-resize !important; } .lil-gui .title { --title-height: calc(var(--widget-height) + var(--spacing) * 1.25); height: var(--title-height); line-height: calc(var(--title-height) - 4px); font-weight: 600; padding: 0 var(--padding); -webkit-tap-highlight-color: transparent; cursor: pointer; outline: none; text-decoration-skip: objects; } .lil-gui .title:before { font-family: "lil-gui"; content: "▾"; padding-right: 2px; display: inline-block; } .lil-gui .title:active { background: var(--title-background-color); opacity: 0.75; } @media (hover: hover) { .lil-gui .title:hover { background: var(--title-background-color); opacity: 0.85; } .lil-gui .title:focus { text-decoration: underline var(--focus-color); } } .lil-gui.root > .title:focus { text-decoration: none !important; } .lil-gui.closed > .title:before { content: "▸"; } .lil-gui.closed > .children { transform: translateY(-7px); opacity: 0; } .lil-gui.closed:not(.transition) > .children { display: none; } .lil-gui.transition > .children { transition-duration: 300ms; transition-property: height, opacity, transform; transition-timing-function: cubic-bezier(0.215, 0.61, 0.355, 1); overflow: hidden; pointer-events: none; } .lil-gui .children:empty:before { content: "Empty"; padding: 0 var(--padding); margin: var(--spacing) 0; display: block; height: var(--widget-height); font-style: italic; line-height: var(--widget-height); opacity: 0.5; } .lil-gui.root > .children > .lil-gui > .title { border: 0 solid var(--widget-color); border-width: 1px 0; transition: border-color 300ms; } .lil-gui.root > .children > .lil-gui.closed > .title { border-bottom-color: transparent; } .lil-gui + .controller { border-top: 1px solid var(--widget-color); margin-top: 0; padding-top: var(--spacing); } .lil-gui .lil-gui .lil-gui > .title { border: none; } .lil-gui .lil-gui .lil-gui > .children { border: none; margin-left: var(--folder-indent); border-left: 2px solid var(--widget-color); } .lil-gui .lil-gui .controller { border: none; } .lil-gui input { -webkit-tap-highlight-color: transparent; border: 0; outline: none; font-family: var(--font-family); font-size: var(--input-font-size); border-radius: var(--widget-border-radius); height: var(--widget-height); background: var(--widget-color); color: var(--text-color); width: 100%; } @media (hover: hover) { .lil-gui input:hover { background: var(--hover-color); } .lil-gui input:active { background: var(--focus-color); } } .lil-gui input[type=text] { padding: var(--widget-padding); } .lil-gui input[type=text]:focus { background: var(--focus-color); } .lil-gui input[type=checkbox] { appearance: none; -webkit-appearance: none; height: var(--checkbox-size); width: var(--checkbox-size); border-radius: var(--widget-border-radius); text-align: center; } .lil-gui input[type=checkbox]:checked:before { font-family: "lil-gui"; content: "✓"; font-size: var(--checkbox-size); line-height: var(--checkbox-size); } @media (hover: hover) { .lil-gui input[type=checkbox]:focus { box-shadow: inset 0 0 0 1px var(--focus-color); } } .lil-gui button { -webkit-tap-highlight-color: transparent; outline: none; cursor: pointer; font-family: var(--font-family); font-size: var(--font-size); color: var(--text-color); width: 100%; height: var(--widget-height); text-transform: none; background: var(--widget-color); border-radius: var(--widget-border-radius); border: 1px solid var(--widget-color); text-align: center; line-height: calc(var(--widget-height) * 0.725); } @media (hover: hover) { .lil-gui button:hover { background: var(--hover-color); border-color: var(--hover-color); } .lil-gui button:focus { border-color: var(--focus-color); } } .lil-gui button:active { background: var(--focus-color); } @font-face { font-family: "lil-gui"; src: url("data:application/font-woff;charset=utf-8;base64,d09GRgABAAAAAAUsAAsAAAAACJwAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAABHU1VCAAABCAAAAH4AAADAImwmYE9TLzIAAAGIAAAAPwAAAGBKqH5SY21hcAAAAcgAAAD0AAACrukyyJBnbHlmAAACvAAAAF8AAACEIZ5WI2hlYWQAAAMcAAAAJwAAADZfcj23aGhlYQAAA0QAAAAYAAAAJAC5AHhobXR4AAADXAAAABAAAABMAZAAAGxvY2EAAANsAAAAFAAAACgCEgIybWF4cAAAA4AAAAAeAAAAIAEfABJuYW1lAAADoAAAASIAAAIK9SUU/XBvc3QAAATEAAAAZgAAAJCTcMc2eJxVjbEOgjAURU+hFRBK1dGRL+ALnAiToyMLEzFpnPz/eAshwSa97517c/MwwJmeB9kwPl+0cf5+uGPZXsqPu4nvZabcSZldZ6kfyWnomFY/eScKqZNWupKJO6kXN3K9uCVoL7iInPr1X5baXs3tjuMqCtzEuagm/AAlzQgPAAB4nGNgYRBlnMDAysDAYM/gBiT5oLQBAwuDJAMDEwMrMwNWEJDmmsJwgCFeXZghBcjlZMgFCzOiKOIFAB71Bb8AeJy1kjFuwkAQRZ+DwRAwBtNQRUGKQ8OdKCAWUhAgKLhIuAsVSpWz5Bbkj3dEgYiUIszqWdpZe+Z7/wB1oCYmIoboiwiLT2WjKl/jscrHfGg/pKdMkyklC5Zs2LEfHYpjcRoPzme9MWWmk3dWbK9ObkWkikOetJ554fWyoEsmdSlt+uR0pCJR34b6t/TVg1SY3sYvdf8vuiKrpyaDXDISiegp17p7579Gp3p++y7HPAiY9pmTibljrr85qSidtlg4+l25GLCaS8e6rRxNBmsnERunKbaOObRz7N72ju5vdAjYpBXHgJylOAVsMseDAPEP8LYoUHicY2BiAAEfhjAGJgZWBgZ7RnFRdnVJELCRlBSRlATJMoLV2DK4glSYs6ubq5vbKrJLSbGrgEmovDuDJVhe3VzcXFwNLCOILB/C4IuQ1xTn5FPilBTj5FPmBAB76woyAHicY2BkYGAA4sklsQ/j+W2+MnAzpDBgAyEMYUCSg4EJxAEAvVwFCgB4nGNgZGBgSGFggJMhDIwMqEAYAByHATJ4nGNgAIIUNEwmAABl3AGReJxjYAACIQYlBiMGJ3wQAEcQBEV4nGNgZGBgEGZgY2BiAAEQyQWEDAz/wXwGAAsPATIAAHicXdBNSsNAHAXwl35iA0UQXYnMShfS9GPZA7T7LgIu03SSpkwzYTIt1BN4Ak/gKTyAeCxfw39jZkjymzcvAwmAW/wgwHUEGDb36+jQQ3GXGot79L24jxCP4gHzF/EIr4jEIe7wxhOC3g2TMYy4Q7+Lu/SHuEd/ivt4wJd4wPxbPEKMX3GI5+DJFGaSn4qNzk8mcbKSR6xdXdhSzaOZJGtdapd4vVPbi6rP+cL7TGXOHtXKll4bY1Xl7EGnPtp7Xy2n00zyKLVHfkHBa4IcJ2oD3cgggWvt/V/FbDrUlEUJhTn/0azVWbNTNr0Ens8de1tceK9xZmfB1CPjOmPH4kitmvOubcNpmVTN3oFJyjzCvnmrwhJTzqzVj9jiSX911FjeAAB4nG3HMRKCMBBA0f0giiKi4DU8k0V2GWbIZDOh4PoWWvq6J5V8If9NVNQcaDhyouXMhY4rPTcG7jwYmXhKq8Wz+p762aNaeYXom2n3m2dLTVgsrCgFJ7OTmIkYbwIbC6vIB7WmFfAAAA==") format("woff"); }`; function _injectStyles( cssContent ) { const injected = document.createElement( 'style' ); injected.innerHTML = cssContent; const before = document.querySelector( 'head link[rel=stylesheet], head style' ); if ( before ) { document.head.insertBefore( injected, before ); } else { document.head.appendChild( injected ); } } let stylesInjected = false; class GUI { /** * Creates a panel that holds controllers. * @example * new GUI(); * new GUI( { container: document.getElementById( 'custom' ) } ); * * @param {object} [options] * @param {boolean} [options.autoPlace=true] * Adds the GUI to `document.body` and fixes it to the top right of the page. * * @param {HTMLElement} [options.container] * Adds the GUI to this DOM element. Overrides `autoPlace`. * * @param {number} [options.width=245] * Width of the GUI in pixels, usually set when name labels become too long. Note that you can make * name labels wider in CSS with `.lil‑gui { ‑‑name‑width: 55% }` * * @param {string} [options.title=Controls] * Name to display in the title bar. * * @param {boolean} [options.injectStyles=true] * Injects the default stylesheet into the page if this is the first GUI. * Pass `false` to use your own stylesheet. * * @param {number} [options.touchStyles=true] * Makes controllers larger on touch devices. Pass `false` to disable touch styles. * * @param {GUI} [options.parent] * Adds this GUI as a child in another GUI. Usually this is done for you by `addFolder()`. * */ constructor( { parent, autoPlace = parent === undefined, touchStyles = true, container, injectStyles = true, title = 'Controls', width } = {} ) { /** * The GUI containing this folder, or `undefined` if this is the root GUI. * @type {GUI} */ this.parent = parent; /** * The top level GUI containing this folder, or `this` if this is the root GUI. * @type {GUI} */ this.root = parent ? parent.root : this; /** * The list of controllers and folders contained by this GUI. * @type {Array<GUI|Controller>} */ this.children = []; /** * The list of controllers contained by this GUI. * @type {Array<Controller>} */ this.controllers = []; /** * The list of folders contained by this GUI. * @type {Array<GUI>} */ this.folders = []; /** * Used to determine if the GUI is closed. Use `gui.open()` or `gui.close()` to change this. * @type {boolean} * @readonly */ this._closed = false; /** * The outermost container element. * @type {HTMLElement} */ this.domElement = document.createElement( 'div' ); this.domElement.classList.add( 'lil-gui' ); /** * The DOM element that contains the title. * @type {HTMLElement} */ this.$title = document.createElement( 'div' ); this.$title.classList.add( 'title' ); this.$title.setAttribute( 'role', 'button' ); this.$title.setAttribute( 'aria-expanded', true ); this.$title.setAttribute( 'tabindex', 0 ); this.$title.addEventListener( 'click', () => this.openAnimated( this._closed ) ); this.$title.addEventListener( 'keydown', e => { if ( e.code === 'Enter' || e.code === 'Space' ) { e.preventDefault(); this.$title.click(); } } ); // enables :active pseudo class on mobile this.$title.addEventListener( 'touchstart', () => { } ); /** * The DOM element that contains children. * @type {HTMLElement} */ this.$children = document.createElement( 'div' ); this.$children.classList.add( 'children' ); this.domElement.appendChild( this.$title ); this.domElement.appendChild( this.$children ); this.title( title ); if ( this.parent ) { this.parent.children.push( this ); this.parent.folders.push( this ); this.parent.$children.appendChild( this.domElement ); // Stop the constructor early, everything onward only applies to root GUI's return; } this.domElement.classList.add( 'root' ); // Inject stylesheet if we haven't done that yet if ( !stylesInjected && injectStyles ) { _injectStyles( stylesheet ); stylesInjected = true; } if ( container ) { container.appendChild( this.domElement ); } else if ( autoPlace ) { this.domElement.classList.add( 'autoPlace' ); document.body.appendChild( this.domElement ); } if ( touchStyles ) { this.domElement.classList.add( 'allow-touch-styles' ); } if ( width ) { this.domElement.style.setProperty( '--width', width + 'px' ); } } /** * Adds a controller to the GUI, inferring controller type using the `typeof` operator. * @example * gui.add( object, 'property' ); * gui.add( object, 'number', 0, 100, 1 ); * gui.add( object, 'options', [ 1, 2, 3 ] ); * * @param {object} object The object the controller will modify. * @param {string} property Name of the property to control. * @param {number|object|Array} [$1] Minimum value for number controllers, or the set of * selectable values for a dropdown. * @param {number} [max] Maximum value for number controllers. * @param {number} [step] Step value for number controllers. * @returns {Controller} */ add( object, property, $1, max, step ) { if ( Object( $1 ) === $1 ) { return new OptionController( this, object, property, $1 ); } const initialValue = object[ property ]; switch ( typeof initialValue ) { case 'number': return new NumberController( this, object, property, $1, max, step ); case 'boolean': return new BooleanController( this, object, property ); case 'string': return new StringController( this, object, property ); case 'function': return new FunctionController( this, object, property ); } console.error( `Failed to add controller for "${property}"`, initialValue, object ); } /** * Adds a color controller to the GUI. * @example * params = { * cssColor: '#ff00ff', * rgbColor: { r: 0, g: 0.2, b: 0.4 }, * customRange: [ 0, 127, 255 ], * }; * * gui.addColor( params, 'cssColor' ); * gui.addColor( params, 'rgbColor' ); * gui.addColor( params, 'customRange', 255 ); * * @param {object} object The object the controller will modify. * @param {string} property Name of the property to control. * @param {number} rgbScale Maximum value for a color channel when using an RGB color. You may * need to set this to 255 if your colors are too dark. * @returns {Controller} */ addColor( object, property, rgbScale = 1 ) { return new ColorController( this, object, property, rgbScale ); } /** * Adds a folder to the GUI, which is just another GUI. This method returns * the nested GUI so you can add controllers to it. * @example * const folder = gui.addFolder( 'Position' ); * folder.add( position, 'x' ); * folder.add( position, 'y' ); * folder.add( position, 'z' ); * * @param {string} title Name to display in the folder's title bar. * @returns {GUI} */ addFolder( title ) { return new GUI( { parent: this, title } ); } /** * Recalls values that were saved with `gui.save()`. * @param {object} obj * @param {boolean} recursive Pass false to exclude folders descending from this GUI. * @returns {this} */ load( obj, recursive = true ) { if ( !( 'controllers' in obj ) ) { throw new Error( 'Invalid load object. Should contain a "controllers" key.' ); } this.controllers.forEach( c => { if ( c instanceof FunctionController ) return; if ( c._name in obj.controllers ) { c.load( obj.controllers[ c._name ] ); } } ); if ( recursive && obj.folders ) { this.folders.forEach( f => { if ( f._title in obj.folders ) { f.load( obj.folders[ f._title ] ); } } ); } return this; } /** * Returns an object mapping controller names to values. The object can be passed to `gui.load()` to * recall these values. * @example * { * controllers: { * prop1: 1, * prop2: 'value', * ... * }, * folders: { * folderName1: { controllers, folders }, * folderName2: { controllers, folders } * ... * } * } * * @param {boolean} recursive Pass false to exclude folders descending from this GUI. * @returns {object} */ save( recursive = true ) { const obj = { controllers: {}, folders: {} }; this.controllers.forEach( c => { if ( c instanceof FunctionController ) return; if ( c._name in obj.controllers ) { throw new Error( `Cannot save GUI with duplicate property "${c._name}"` ); } obj.controllers[ c._name ] = c.save(); } ); if ( recursive ) { this.folders.forEach( f => { if ( f._title in obj.folders ) { throw new Error( `Cannot save GUI with duplicate folder "${f._title}"` ); } obj.folders[ f._title ] = f.save(); } ); } return obj; } /** * Opens a GUI or folder. GUI and folders are open by default. * @param {boolean} open Pass false to close * @returns {this} * @example * gui.open(); // open * gui.open( false ); // close * gui.open( gui._closed ); // toggle */ open( open = true ) { this._closed = !open; this.$title.setAttribute( 'aria-expanded', !this._closed ); this.domElement.classList.toggle( 'closed', this._closed ); return this; } /** * Closes the GUI. * @returns {this} */ close() { return this.open( false ); } openAnimated( open = true ) { // set state immediately this._closed = !open; this.$title.setAttribute( 'aria-expanded', !this._closed ); // wait for next frame to measure $children requestAnimationFrame( () => { // explicitly set initial height for transition const initialHeight = this.$children.clientHeight; this.$children.style.height = initialHeight + 'px'; this.domElement.classList.add( 'transition' ); const onTransitionEnd = e => { if ( e.target !== this.$children ) return; this.$children.style.height = ''; this.domElement.classList.remove( 'transition' ); this.$children.removeEventListener( 'transitionend', onTransitionEnd ); }; this.$children.addEventListener( 'transitionend', onTransitionEnd ); // todo: this is wrong if children's scrollHeight makes for a gui taller than maxHeight const targetHeight = !open ? 0 : this.$children.scrollHeight; this.domElement.classList.toggle( 'closed', !open ); requestAnimationFrame( () => { this.$children.style.height = targetHeight + 'px'; } ); } ); return this; } /** * Change the title of this GUI. * @param {string} title * @returns {this} */ title( title ) { /** * Current title of the GUI. Use `gui.title( 'Title' )` to modify this value. * @type {string} */ this._title = title; this.$title.innerHTML = title; return this; } /** * Resets all controllers to their initial values. * @param {boolean} recursive Pass false to exclude folders descending from this GUI. * @returns {this} */ reset( recursive = true ) { const controllers = recursive ? this.controllersRecursive() : this.controllers; controllers.forEach( c => c.reset() ); return this; } /** * Pass a function to be called whenever a controller in this GUI changes. * @param {function({object:object, property:string, value:any, controller:Controller})} callback * @returns {this} * @example * gui.onChange( event => { * event.object // object that was modified * event.property // string, name of property * event.value // new value of controller * event.controller // controller that was modified * } ); */ onChange( callback ) { this._onChange = callback; return this; } _callOnChange( controller ) { if ( this.parent ) { this.parent._callOnChange( controller ); } if ( this._onChange !== undefined ) { this._onChange.call( this, { object: controller.object, property: controller.property, value: controller.getValue(), controller } ); } } /** * Destroys all DOM elements and event listeners associated with this GUI */ destroy() { if ( this.parent ) { this.parent.children.splice( this.parent.children.indexOf( this ), 1 ); this.parent.folders.splice( this.parent.folders.indexOf( this ), 1 ); } if ( this.domElement.parentElement ) { this.domElement.parentElement.removeChild( this.domElement ); } Array.from( this.children ).forEach( c => c.destroy() ); if ( this._onResize ) { window.removeEventListener( 'resize', this._onResize ); } } /** * Returns an array of controllers contained by this GUI and its descendents. * @returns {Controller[]} */ controllersRecursive() { let controllers = Array.from( this.controllers ); this.folders.forEach( f => { controllers = controllers.concat( f.controllersRecursive() ); } ); return controllers; } /** * Returns an array of folders contained by this GUI and its descendents. * @returns {GUI[]} */ foldersRecursive() { let folders = Array.from( this.folders ); this.folders.forEach( f => { folders = folders.concat( f.foldersRecursive() ); } ); return folders; } } export default GUI; export { BooleanController, ColorController, Controller, FunctionController, GUI, NumberController, OptionController, StringController };
Texto modificado
Abrir archivo
/** * lil-gui * https://lil-gui.georgealways.com * @version 0.16.0 * @author George Michael Brower * @license MIT */ /** * Base class for all controllers. */ class Controller { constructor( parent, object, property, className, widgetTag = 'div' ) { /** * The GUI that contains this controller. * @type {GUI} */ this.parent = parent; /** * The object this controller will modify. * @type {object} */ this.object = object; /** * The name of the property to control. * @type {string} */ this.property = property; /** * Used to determine if the controller is disabled. * Use `controller.disable( true|false )` to modify this value * @type {boolean} */ this._disabled = false; /** * The value of `object[ property ]` when the controller was created. * @type {any} */ this.initialValue = this.getValue(); /** * The outermost container DOM element for this controller. * @type {HTMLElement} */ this.domElement = document.createElement( 'div' ); this.domElement.classList.add( 'controller' ); this.domElement.classList.add( className ); /** * The DOM element that contains the controller's name. * @type {HTMLElement} */ this.$name = document.createElement( 'div' ); this.$name.classList.add( 'name' ); Controller.nextNameID = Controller.nextNameID || 0; this.$name.id = `lil-gui-name-${++Controller.nextNameID}`; /** * The DOM element that contains the controller's "widget" (which differs by controller type). * @type {HTMLElement} */ this.$widget = document.createElement( widgetTag ); this.$widget.classList.add( 'widget' ); /** * The DOM element that receives the disabled attribute when using disable() * @type {HTMLElement} */ this.$disable = this.$widget; this.domElement.appendChild( this.$name ); this.domElement.appendChild( this.$widget ); this.parent.children.push( this ); this.parent.controllers.push( this ); this.parent.$children.appendChild( this.domElement ); this._listenCallback = this._listenCallback.bind( this ); this.name( property ); } /** * Sets the name of the controller and its label in the GUI. * @param {string} name * @returns {this} */ name( name ) { /** * The controller's name. Use `controller.name( 'Name' )` to modify this value. * @type {string} */ this._name = name; this.$name.innerHTML = name; return this; } /** * 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 * controller. * @param {Function} callback * @returns {this} * @example * const controller = gui.add( object, 'property' ); * * controller.onChange( function( v ) { * console.log( 'The value is now ' + v ); * console.assert( this === controller ); * } ); */ onChange( callback ) { /** * Used to access the function bound to `onChange` events. Don't modify this value directly. * Use the `controller.onChange( callback )` method instead. * @type {Function} */ this._onChange = callback; return this; } /** * Calls the onChange methods of this controller and its parent GUI. * @protected */ _callOnChange() { this.parent._callOnChange( this ); if ( this._onChange !== undefined ) { this._onChange.call( this, this.getValue() ); } this._changed = true; } /** * 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 ) { /** * 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; 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. * @returns {this} */ reset() { this.setValue( this.initialValue ); this._callOnFinishChange(); return this; } /** * Enables this controller. * @param {boolean} enabled * @returns {this} * @example * controller.enable(); * controller.enable( false ); // disable * controller.enable( controller._disabled ); // toggle */ enable( enabled = true ) { return this.disable( !enabled ); } /** * Disables this controller. * @param {boolean} disabled * @returns {this} * @example * controller.disable(); * controller.disable( false ); // enable * controller.disable( !controller._disabled ); // toggle */ disable( disabled = true ) { if ( disabled === this._disabled ) return this; this._disabled = disabled; this.domElement.classList.toggle( 'disabled', disabled ); this.$disable.toggleAttribute( 'disabled', disabled ); return this; } /** * 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. * * 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 * the GUI. * @example * // safe usage * * gui.add( object1, 'property' ).options( [ 'a', 'b', 'c' ] ); * gui.add( object2, 'property' ); * * // danger * * const c = gui.add( object1, 'property' ); * gui.add( object2, 'property' ); * * c.options( [ 'a', 'b', 'c' ] ); * // controller is now at the end of the GUI even though it was added first * * assert( c.parent.children.indexOf( c ) === -1 ) * // c references a controller that no longer exists * * @param {object|Array} options * @returns {Controller} */ options( options ) { const controller = this.parent.add( this.object, this.property, options ); controller.name( this._name ); this.destroy(); return controller; } /** * Sets the minimum value. Only works on number controllers. * @param {number} min * @returns {this} */ min( min ) { return this; } /** * Sets the maximum value. Only works on number controllers. * @param {number} max * @returns {this} */ max( max ) { return this; } /** * Sets the step. Only works on number controllers. * @param {number} step * @returns {this} */ step( step ) { return this; } /** * Calls `updateDisplay()` every animation frame. Pass `false` to stop listening. * @param {boolean} listen * @returns {this} */ listen( listen = true ) { /** * Used to determine if the controller is currently listening. Don't modify this value * directly. Use the `controller.listen( true|false )` method instead. * @type {boolean} */ this._listening = listen; if ( this._listenCallbackID !== undefined ) { cancelAnimationFrame( this._listenCallbackID ); this._listenCallbackID = undefined; } if ( this._listening ) { this._listenCallback(); } return this; } _listenCallback() { this._listenCallbackID = requestAnimationFrame( this._listenCallback ); this.updateDisplay(); } /** * Returns `object[ property ]`. * @returns {any} */ getValue() { return this.object[ this.property ]; } /** * Sets the value of `object[ property ]`, invokes any `onChange` handlers and updates the display. * @param {any} value * @returns {this} */ setValue( value ) { this.object[ this.property ] = value; this._callOnChange(); this.updateDisplay(); return this; } /** * 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. * @returns {this} */ updateDisplay() { return this; } load( value ) { this.setValue( value ); this._callOnFinishChange(); return this; } save() { return this.getValue(); } /** * Destroys this controller and removes it from the parent GUI. */ destroy() { this.parent.children.splice( this.parent.children.indexOf( this ), 1 ); this.parent.controllers.splice( this.parent.controllers.indexOf( this ), 1 ); this.parent.$children.removeChild( this.domElement ); } } class BooleanController extends Controller { constructor( parent, object, property ) { super( parent, object, property, 'boolean', 'label' ); this.$input = document.createElement( 'input' ); this.$input.setAttribute( 'type', 'checkbox' ); this.$input.setAttribute( 'aria-labelledby', this.$name.id ); this.$widget.appendChild( this.$input ); this.$input.addEventListener( 'change', () => { this.setValue( this.$input.checked ); this._callOnFinishChange(); } ); this.$disable = this.$input; this.updateDisplay(); } updateDisplay() { this.$input.checked = this.getValue(); return this; } } function normalizeColorString( string ) { let match, result; if ( match = string.match( /(#|0x)?([a-f0-9]{6})/i ) ) { result = match[ 2 ]; } else if ( match = string.match( /rgb\(\s*(\d*)\s*,\s*(\d*)\s*,\s*(\d*)\s*\)/ ) ) { result = parseInt( match[ 1 ] ).toString( 16 ).padStart( 2, 0 ) + parseInt( match[ 2 ] ).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 ) ) { result = match[ 1 ] + match[ 1 ] + match[ 2 ] + match[ 2 ] + match[ 3 ] + match[ 3 ]; } if ( result ) { return '#' + result; } return false; } const STRING = { isPrimitive: true, match: v => typeof v === 'string', fromHexString: normalizeColorString, toHexString: normalizeColorString }; const INT = { isPrimitive: true, match: v => typeof v === 'number', fromHexString: string => parseInt( string.substring( 1 ), 16 ), toHexString: value => '#' + value.toString( 16 ).padStart( 6, 0 ) }; const ARRAY = { isPrimitive: false, match: Array.isArray, fromHexString( string, target, rgbScale = 1 ) { const int = INT.fromHexString( string ); target[ 0 ] = ( int >> 16 & 255 ) / 255 * rgbScale; target[ 1 ] = ( int >> 8 & 255 ) / 255 * rgbScale; target[ 2 ] = ( int & 255 ) / 255 * rgbScale; }, toHexString( [ r, g, b ], rgbScale = 1 ) { rgbScale = 255 / rgbScale; const int = ( r * rgbScale ) << 16 ^ ( g * rgbScale ) << 8 ^ ( b * rgbScale ) << 0; return INT.toHexString( int ); } }; const OBJECT = { isPrimitive: false, match: v => Object( v ) === v, fromHexString( string, target, rgbScale = 1 ) { const int = INT.fromHexString( string ); target.r = ( int >> 16 & 255 ) / 255 * rgbScale; target.g = ( int >> 8 & 255 ) / 255 * rgbScale; target.b = ( int & 255 ) / 255 * rgbScale; }, toHexString( { r, g, b }, rgbScale = 1 ) { rgbScale = 255 / rgbScale; const int = ( r * rgbScale ) << 16 ^ ( g * rgbScale ) << 8 ^ ( b * rgbScale ) << 0; return INT.toHexString( int ); } }; const FORMATS = [ STRING, INT, ARRAY, OBJECT ]; function getColorFormat( value ) { return FORMATS.find( format => format.match( value ) ); } class ColorController extends Controller { constructor( parent, object, property, rgbScale ) { super( parent, object, property, 'color' ); this.$input = document.createElement( 'input' ); this.$input.setAttribute( 'type', 'color' ); this.$input.setAttribute( 'tabindex', -1 ); this.$input.setAttribute( 'aria-labelledby', this.$name.id ); this.$text = document.createElement( 'input' ); this.$text.setAttribute( 'type', 'text' ); this.$text.setAttribute( 'spellcheck', 'false' ); this.$text.setAttribute( 'aria-labelledby', this.$name.id ); this.$display = document.createElement( 'div' ); this.$display.classList.add( 'display' ); this.$display.appendChild( this.$input ); this.$widget.appendChild( this.$display ); this.$widget.appendChild( this.$text ); this._format = getColorFormat( this.initialValue ); this._rgbScale = rgbScale; this._initialValueHexString = this.save(); this._textFocused = false; this.$input.addEventListener( 'input', () => { this._setValueFromHexString( this.$input.value ); } ); this.$input.addEventListener( 'blur', () => { this._callOnFinishChange(); } ); this.$text.addEventListener( 'input', () => { const tryParse = normalizeColorString( this.$text.value ); if ( tryParse ) { this._setValueFromHexString( tryParse ); } } ); this.$text.addEventListener( 'focus', () => { this._textFocused = true; this.$text.select(); } ); this.$text.addEventListener( 'blur', () => { this._textFocused = false; this.updateDisplay(); this._callOnFinishChange(); } ); this.$disable = this.$text; this.updateDisplay(); } reset() { this._setValueFromHexString( this._initialValueHexString ); return this; } _setValueFromHexString( value ) { if ( this._format.isPrimitive ) { const newValue = this._format.fromHexString( value ); this.setValue( newValue ); } else { this._format.fromHexString( value, this.getValue(), this._rgbScale ); this._callOnChange(); this.updateDisplay(); } } save() { return this._format.toHexString( this.getValue(), this._rgbScale ); } load( value ) { this._setValueFromHexString( value ); this._callOnFinishChange(); return this; } updateDisplay() { this.$input.value = this._format.toHexString( this.getValue(), this._rgbScale ); if ( !this._textFocused ) { this.$text.value = this.$input.value.substring( 1 ); } this.$display.style.backgroundColor = this.$input.value; return this; } } class FunctionController extends Controller { constructor( parent, object, property ) { super( parent, object, property, 'function' ); // Buttons are the only case where widget contains name this.$button = document.createElement( 'button' ); this.$button.appendChild( this.$name ); this.$widget.appendChild( this.$button ); this.$button.addEventListener( 'click', e => { e.preventDefault(); this.getValue().call( this.object ); } ); // enables :active pseudo class on mobile this.$button.addEventListener( 'touchstart', () => { } ); this.$disable = this.$button; } } class NumberController extends Controller { constructor( parent, object, property, min, max, step ) { super( parent, object, property, 'number' ); this._initInput(); this.min( min ); this.max( max ); const stepExplicit = step !== undefined; this.step( stepExplicit ? step : this._getImplicitStep(), stepExplicit ); this.updateDisplay(); } min( min ) { this._min = min; this._onUpdateMinMax(); return this; } max( max ) { this._max = max; this._onUpdateMinMax(); return this; } step( step, explicit = true ) { this._step = step; this._stepExplicit = explicit; return this; } updateDisplay() { const value = this.getValue(); if ( this._hasSlider ) { 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 ) { this.$input.value = value; } return this; } _initInput() { this.$input = document.createElement( 'input' ); this.$input.setAttribute( 'type', 'number' ); this.$input.setAttribute( 'step', 'any' ); this.$input.setAttribute( 'aria-labelledby', this.$name.id ); this.$widget.appendChild( this.$input ); this.$disable = this.$input; const onInput = () => { const value = parseFloat( this.$input.value ); if ( isNaN( value ) ) return; this.setValue( this._clamp( value ) ); }; // Keys & mouse wheel // --------------------------------------------------------------------- const increment = delta => { const value = parseFloat( this.$input.value ); if ( isNaN( value ) ) return; this._snapClampSetValue( value + delta ); // Force the input to updateDisplay when it's focused this.$input.value = this.getValue(); }; const onKeyDown = e => { if ( e.code === 'Enter' ) { this.$input.blur(); } if ( e.code === 'ArrowUp' ) { e.preventDefault(); increment( this._step * this._arrowKeyMultiplier( e ) ); } if ( e.code === 'ArrowDown' ) { e.preventDefault(); increment( this._step * this._arrowKeyMultiplier( e ) * -1 ); } }; const onWheel = e => { if ( this._inputFocused ) { e.preventDefault(); 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 = () => { this._inputFocused = true; }; const onBlur = () => { this._inputFocused = false; 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( 'blur', onBlur ); } _initSlider() { this._hasSlider = true; // Build DOM // --------------------------------------------------------------------- this.$slider = document.createElement( 'div' ); this.$slider.classList.add( 'slider' ); this.$fill = document.createElement( 'div' ); this.$fill.classList.add( 'fill' ); this.$slider.appendChild( this.$fill ); this.$widget.insertBefore( this.$slider, this.$input ); this.domElement.classList.add( 'hasSlider' ); // Map clientX to value // --------------------------------------------------------------------- const map = ( v, a, b, c, d ) => { return ( v - a ) / ( b - a ) * ( d - c ) + c; }; const setValueFromX = clientX => { const rect = this.$slider.getBoundingClientRect(); let value = map( clientX, rect.left, rect.right, this._min, this._max ); this._snapClampSetValue( value ); }; // Mouse drag // --------------------------------------------------------------------- const mouseDown = e => { this._setDraggingStyle( true ); setValueFromX( e.clientX ); window.addEventListener( 'mousemove', mouseMove ); window.addEventListener( 'mouseup', mouseUp ); }; const mouseMove = e => { setValueFromX( e.clientX ); }; const mouseUp = () => { this._callOnFinishChange(); this._setDraggingStyle( false ); window.removeEventListener( 'mousemove', mouseMove ); window.removeEventListener( 'mouseup', mouseUp ); }; // Touch drag // --------------------------------------------------------------------- let testingForScroll = false, prevClientX, prevClientY; const beginTouchDrag = e => { e.preventDefault(); this._setDraggingStyle( true ); setValueFromX( e.touches[ 0 ].clientX ); testingForScroll = false; }; const onTouchStart = e => { if ( e.touches.length > 1 ) return; // 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. if ( this._hasScrollBar ) { prevClientX = e.touches[ 0 ].clientX; prevClientY = e.touches[ 0 ].clientY; testingForScroll = true; } else { // Otherwise, we can set the value straight away on touchstart. beginTouchDrag( e ); } window.addEventListener( 'touchmove', onTouchMove ); window.addEventListener( 'touchend', onTouchEnd ); }; const onTouchMove = e => { if ( testingForScroll ) { const dx = e.touches[ 0 ].clientX - prevClientX; const dy = e.touches[ 0 ].clientY - prevClientY; if ( Math.abs( dx ) > Math.abs( dy ) ) { // We moved horizontally, set the value and stop checking. beginTouchDrag( e ); } else { // This was, in fact, an attempt to scroll. Abort. window.removeEventListener( 'touchmove', onTouchMove ); window.removeEventListener( 'touchend', onTouchEnd ); } } else { e.preventDefault(); setValueFromX( e.touches[ 0 ].clientX ); } }; const onTouchEnd = () => { this._callOnFinishChange(); this._setDraggingStyle( false ); window.removeEventListener( 'touchmove', onTouchMove ); window.removeEventListener( 'touchend', onTouchEnd ); }; // Mouse wheel // --------------------------------------------------------------------- // 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 => { // ignore vertical wheels if there's a scrollbar const isVertical = Math.abs( e.deltaX ) < Math.abs( e.deltaY ); if ( isVertical && this._hasScrollBar ) return; e.preventDefault(); // set value const delta = this._normalizeMouseWheel( e ) * this._step; 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( 'mousedown', mouseDown ); this.$slider.addEventListener( 'touchstart', onTouchStart ); this.$slider.addEventListener( 'wheel', onWheel ); } _setDraggingStyle( active, axis = 'horizontal' ) { if ( this.$slider ) { this.$slider.classList.toggle( 'active', active ); } document.body.classList.toggle( 'lil-gui-dragging', active ); document.body.classList.toggle( `lil-gui-${axis}`, active ); } _getImplicitStep() { if ( this._hasMin && this._hasMax ) { return ( this._max - this._min ) / 1000; } return 0.1; } _onUpdateMinMax() { if ( !this._hasSlider && this._hasMin && this._hasMax ) { // 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 // update that too. if ( !this._stepExplicit ) { this.step( this._getImplicitStep(), false ); } this._initSlider(); this.updateDisplay(); } } _normalizeMouseWheel( e ) { let { deltaX, deltaY } = e; // Safari and Chrome report weird non-integral values for a notched wheel, // but still expose actual lines scrolled via wheelDelta. Notched wheels // should behave the same way as arrow keys. if ( Math.floor( e.deltaY ) !== e.deltaY && e.wheelDelta ) { deltaX = 0; deltaY = -e.wheelDelta / 120; deltaY *= this._stepExplicit ? 1 : 10; } const wheel = deltaX + -deltaY; return wheel; } _arrowKeyMultiplier( e ) { let mult = this._stepExplicit ? 1 : 10; if ( e.shiftKey ) { mult *= 10; } else if ( e.altKey ) { mult /= 10; } return mult; } _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 ) { // either condition is false if min or max is undefined if ( value < this._min ) value = this._min; if ( value > this._max ) value = this._max; return 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._callOnFinishChange(); } ); 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 ]; 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.$input.addEventListener( 'blur', () => { this._callOnFinishChange(); } ); 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-weight: normal; font-style: normal; text-align: left; background-color: var(--background-color); color: var(--text-color); user-select: none; -webkit-user-select: none; touch-action: manipulation; --background-color: #1f1f1f; --text-color: #ebebeb; --title-background-color: #111111; --title-text-color: #ebebeb; --widget-color: #424242; --hover-color: #4f4f4f; --focus-color: #595959; --number-color: #2cc9ff; --string-color: #a2db3c; --font-size: 11px; --input-font-size: 11px; --font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Arial, sans-serif; --font-family-mono: Menlo, Monaco, Consolas, "Droid Sans Mono", monospace; --padding: 4px; --spacing: 4px; --widget-height: 20px; --name-width: 45%; --slider-knob-width: 2px; --slider-input-width: 27%; --color-input-width: 27%; --slider-input-min-width: 45px; --color-input-min-width: 45px; --folder-indent: 7px; --widget-padding: 0 0 0 3px; --widget-border-radius: 2px; --checkbox-size: calc(0.75 * var(--widget-height)); --scrollbar-width: 5px; } .lil-gui, .lil-gui * { box-sizing: border-box; margin: 0; padding: 0; } .lil-gui.root { width: var(--width, 245px); display: flex; flex-direction: column; } .lil-gui.root > .title { background: var(--title-background-color); color: var(--title-text-color); } .lil-gui.root > .children { overflow-x: hidden; overflow-y: auto; } .lil-gui.root > .children::-webkit-scrollbar { width: var(--scrollbar-width); height: var(--scrollbar-width); background: var(--background-color); } .lil-gui.root > .children::-webkit-scrollbar-thumb { border-radius: var(--scrollbar-width); background: var(--focus-color); } @media (pointer: coarse) { .lil-gui.allow-touch-styles { --widget-height: 28px; --padding: 6px; --spacing: 6px; --font-size: 13px; --input-font-size: 16px; --folder-indent: 10px; --scrollbar-width: 7px; --slider-input-min-width: 50px; --color-input-min-width: 65px; } } .lil-gui.force-touch-styles { --widget-height: 28px; --padding: 6px; --spacing: 6px; --font-size: 13px; --input-font-size: 16px; --folder-indent: 10px; --scrollbar-width: 7px; --slider-input-min-width: 50px; --color-input-min-width: 65px; } .lil-gui.autoPlace { max-height: 100%; position: fixed; top: 0; right: 15px; z-index: 1001; } .lil-gui .controller { display: flex; align-items: center; padding: 0 var(--padding); margin: var(--spacing) 0; } .lil-gui .controller.disabled { opacity: 0.5; } .lil-gui .controller.disabled, .lil-gui .controller.disabled * { pointer-events: none !important; } .lil-gui .controller > .name { min-width: var(--name-width); flex-shrink: 0; white-space: pre; padding-right: var(--spacing); line-height: var(--widget-height); } .lil-gui .controller .widget { position: relative; display: flex; align-items: center; width: 100%; min-height: var(--widget-height); } .lil-gui .controller.string input { color: var(--string-color); } .lil-gui .controller.boolean .widget { cursor: pointer; } .lil-gui .controller.color .display { width: 100%; height: var(--widget-height); border-radius: var(--widget-border-radius); position: relative; } @media (hover: hover) { .lil-gui .controller.color .display:hover:before { content: " "; display: block; position: absolute; border-radius: var(--widget-border-radius); border: 1px solid #fff9; top: 0; right: 0; bottom: 0; left: 0; } } .lil-gui .controller.color input[type=color] { opacity: 0; width: 100%; height: 100%; cursor: pointer; } .lil-gui .controller.color input[type=text] { margin-left: var(--spacing); font-family: var(--font-family-mono); min-width: var(--color-input-min-width); width: var(--color-input-width); flex-shrink: 0; } .lil-gui .controller.option select { opacity: 0; position: absolute; width: 100%; max-width: 100%; } .lil-gui .controller.option .display { position: relative; pointer-events: none; border-radius: var(--widget-border-radius); height: var(--widget-height); line-height: var(--widget-height); max-width: 100%; overflow: hidden; word-break: break-all; padding-left: 0.55em; padding-right: 1.75em; background: var(--widget-color); } @media (hover: hover) { .lil-gui .controller.option .display.focus { background: var(--focus-color); } } .lil-gui .controller.option .display.active { background: var(--focus-color); } .lil-gui .controller.option .display:after { font-family: "lil-gui"; content: "↕"; position: absolute; top: 0; right: 0; bottom: 0; padding-right: 0.375em; } .lil-gui .controller.option .widget, .lil-gui .controller.option select { cursor: pointer; } @media (hover: hover) { .lil-gui .controller.option .widget:hover .display { background: var(--hover-color); } } .lil-gui .controller.number input { color: var(--number-color); } .lil-gui .controller.number.hasSlider input { margin-left: var(--spacing); width: var(--slider-input-width); min-width: var(--slider-input-min-width); flex-shrink: 0; } .lil-gui .controller.number .slider { width: 100%; height: var(--widget-height); background-color: var(--widget-color); border-radius: var(--widget-border-radius); padding-right: var(--slider-knob-width); overflow: hidden; cursor: ew-resize; touch-action: pan-y; } @media (hover: hover) { .lil-gui .controller.number .slider:hover { background-color: var(--hover-color); } } .lil-gui .controller.number .slider.active { background-color: var(--focus-color); } .lil-gui .controller.number .slider.active .fill { opacity: 0.95; } .lil-gui .controller.number .fill { height: 100%; border-right: var(--slider-knob-width) solid var(--number-color); box-sizing: content-box; } .lil-gui-dragging .lil-gui { --hover-color: var(--widget-color); } .lil-gui-dragging * { cursor: ew-resize !important; } .lil-gui-dragging.lil-gui-vertical * { cursor: ns-resize !important; } .lil-gui .title { --title-height: calc(var(--widget-height) + var(--spacing) * 1.25); height: var(--title-height); line-height: calc(var(--title-height) - 4px); font-weight: 600; padding: 0 var(--padding); -webkit-tap-highlight-color: transparent; cursor: pointer; outline: none; text-decoration-skip: objects; } .lil-gui .title:before { font-family: "lil-gui"; content: "▾"; padding-right: 2px; display: inline-block; } .lil-gui .title:active { background: var(--title-background-color); opacity: 0.75; } @media (hover: hover) { body:not(.lil-gui-dragging) .lil-gui .title:hover { background: var(--title-background-color); opacity: 0.85; } .lil-gui .title:focus { text-decoration: underline var(--focus-color); } } .lil-gui.root > .title:focus { text-decoration: none !important; } .lil-gui.closed > .title:before { content: "▸"; } .lil-gui.closed > .children { transform: translateY(-7px); opacity: 0; } .lil-gui.closed:not(.transition) > .children { display: none; } .lil-gui.transition > .children { transition-duration: 300ms; transition-property: height, opacity, transform; transition-timing-function: cubic-bezier(0.2, 0.6, 0.35, 1); overflow: hidden; pointer-events: none; } .lil-gui .children:empty:before { content: "Empty"; padding: 0 var(--padding); margin: var(--spacing) 0; display: block; height: var(--widget-height); font-style: italic; line-height: var(--widget-height); opacity: 0.5; } .lil-gui.root > .children > .lil-gui > .title { border: 0 solid var(--widget-color); border-width: 1px 0; transition: border-color 300ms; } .lil-gui.root > .children > .lil-gui.closed > .title { border-bottom-color: transparent; } .lil-gui + .controller { border-top: 1px solid var(--widget-color); margin-top: 0; padding-top: var(--spacing); } .lil-gui .lil-gui .lil-gui > .title { border: none; } .lil-gui .lil-gui .lil-gui > .children { border: none; margin-left: var(--folder-indent); border-left: 2px solid var(--widget-color); } .lil-gui .lil-gui .controller { border: none; } .lil-gui input { -webkit-tap-highlight-color: transparent; border: 0; outline: none; font-family: var(--font-family); font-size: var(--input-font-size); border-radius: var(--widget-border-radius); height: var(--widget-height); background: var(--widget-color); color: var(--text-color); width: 100%; } @media (hover: hover) { .lil-gui input:hover { background: var(--hover-color); } .lil-gui input:active { background: var(--focus-color); } } .lil-gui input:disabled { opacity: 1; } .lil-gui input[type=text], .lil-gui input[type=number] { padding: var(--widget-padding); } .lil-gui input[type=text]:focus, .lil-gui input[type=number]:focus { background: var(--focus-color); } .lil-gui input::-webkit-outer-spin-button, .lil-gui input::-webkit-inner-spin-button { -webkit-appearance: none; margin: 0; } .lil-gui input[type=number] { -moz-appearance: textfield; } .lil-gui input[type=checkbox] { appearance: none; -webkit-appearance: none; height: var(--checkbox-size); width: var(--checkbox-size); border-radius: var(--widget-border-radius); text-align: center; cursor: pointer; } .lil-gui input[type=checkbox]:checked:before { font-family: "lil-gui"; content: "✓"; font-size: var(--checkbox-size); line-height: var(--checkbox-size); } @media (hover: hover) { .lil-gui input[type=checkbox]:focus { box-shadow: inset 0 0 0 1px var(--focus-color); } } .lil-gui button { -webkit-tap-highlight-color: transparent; outline: none; cursor: pointer; font-family: var(--font-family); font-size: var(--font-size); color: var(--text-color); width: 100%; height: var(--widget-height); text-transform: none; background: var(--widget-color); border-radius: var(--widget-border-radius); border: 1px solid var(--widget-color); text-align: center; line-height: calc(var(--widget-height) - 4px); } @media (hover: hover) { .lil-gui button:hover { background: var(--hover-color); border-color: var(--hover-color); } .lil-gui button:focus { border-color: var(--focus-color); } } .lil-gui button:active { background: var(--focus-color); } @font-face { font-family: "lil-gui"; src: url("data:application/font-woff;charset=utf-8;base64,d09GRgABAAAAAAUsAAsAAAAACJwAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAABHU1VCAAABCAAAAH4AAADAImwmYE9TLzIAAAGIAAAAPwAAAGBKqH5SY21hcAAAAcgAAAD0AAACrukyyJBnbHlmAAACvAAAAF8AAACEIZpWH2hlYWQAAAMcAAAAJwAAADZfcj2zaGhlYQAAA0QAAAAYAAAAJAC5AHhobXR4AAADXAAAABAAAABMAZAAAGxvY2EAAANsAAAAFAAAACgCEgIybWF4cAAAA4AAAAAeAAAAIAEfABJuYW1lAAADoAAAASIAAAIK9SUU/XBvc3QAAATEAAAAZgAAAJCTcMc2eJxVjbEOgjAURU+hFRBK1dGRL+ALnAiToyMLEzFpnPz/eAshwSa97517c/MwwJmeB9kwPl+0cf5+uGPZXsqPu4nvZabcSZldZ6kfyWnomFY/eScKqZNWupKJO6kXN3K9uCVoL7iInPr1X5baXs3tjuMqCtzEuagm/AAlzQgPAAB4nGNgYRBlnMDAysDAYM/gBiT5oLQBAwuDJAMDEwMrMwNWEJDmmsJwgCFeXZghBcjlZMgFCzOiKOIFAB71Bb8AeJy1kjFuwkAQRZ+DwRAwBtNQRUGKQ8OdKCAWUhAgKLhIuAsVSpWz5Bbkj3dEgYiUIszqWdpZe+Z7/wB1oCYmIoboiwiLT2WjKl/jscrHfGg/pKdMkyklC5Zs2LEfHYpjcRoPzme9MWWmk3dWbK9ObkWkikOetJ554fWyoEsmdSlt+uR0pCJR34b6t/TVg1SY3sYvdf8vuiKrpyaDXDISiegp17p7579Gp3p++y7HPAiY9pmTibljrr85qSidtlg4+l25GLCaS8e6rRxNBmsnERunKbaOObRz7N72ju5vdAjYpBXHgJylOAVsMseDAPEP8LYoUHicY2BiAAEfhiAGJgZWBgZ7RnFRdnVJELCQlBSRlATJMoLV2DK4glSYs6ubq5vbKrJLSbGrgEmovDuDJVhe3VzcXFwNLCOILB/C4IuQ1xTn5FPilBTj5FPmBAB4WwoqAHicY2BkYGAA4sk1sR/j+W2+MnAzpDBgAyEMQUCSg4EJxAEAwUgFHgB4nGNgZGBgSGFggJMhDIwMqEAYAByHATJ4nGNgAIIUNEwmAABl3AGReJxjYAACIQYlBiMGJ3wQAEcQBEV4nGNgZGBgEGZgY2BiAAEQyQWEDAz/wXwGAAsPATIAAHicXdBNSsNAHAXwl35iA0UQXYnMShfS9GPZA7T7LgIu03SSpkwzYTIt1BN4Ak/gKTyAeCxfw39jZkjymzcvAwmAW/wgwHUEGDb36+jQQ3GXGot79L24jxCP4gHzF/EIr4jEIe7wxhOC3g2TMYy4Q7+Lu/SHuEd/ivt4wJd4wPxbPEKMX3GI5+DJFGaSn4qNzk8mcbKSR6xdXdhSzaOZJGtdapd4vVPbi6rP+cL7TGXOHtXKll4bY1Xl7EGnPtp7Xy2n00zyKLVHfkHBa4IcJ2oD3cgggWvt/V/FbDrUlEUJhTn/0azVWbNTNr0Ens8de1tceK9xZmfB1CPjOmPH4kitmvOubcNpmVTN3oFJyjzCvnmrwhJTzqzVj9jiSX911FjeAAB4nG3HMRKCMBBA0f0giiKi4DU8k0V2GWbIZDOh4PoWWvq6J5V8If9NVNQcaDhyouXMhY4rPTcG7jwYmXhKq8Wz+p762aNaeYXom2n3m2dLTVgsrCgFJ7OTmIkYbwIbC6vIB7WmFfAAAA==") format("woff"); }`; function _injectStyles( cssContent ) { const injected = document.createElement( 'style' ); injected.innerHTML = cssContent; const before = document.querySelector( 'head link[rel=stylesheet], head style' ); if ( before ) { document.head.insertBefore( injected, before ); } else { document.head.appendChild( injected ); } } let stylesInjected = false; class GUI { /** * Creates a panel that holds controllers. * @example * new GUI(); * new GUI( { container: document.getElementById( 'custom' ) } ); * * @param {object} [options] * @param {boolean} [options.autoPlace=true] * Adds the GUI to `document.body` and fixes it to the top right of the page. * * @param {HTMLElement} [options.container] * Adds the GUI to this DOM element. Overrides `autoPlace`. * * @param {number} [options.width=245] * Width of the GUI in pixels, usually set when name labels become too long. Note that you can make * name labels wider in CSS with `.lil‑gui { ‑‑name‑width: 55% }` * * @param {string} [options.title=Controls] * Name to display in the title bar. * * @param {boolean} [options.injectStyles=true] * Injects the default stylesheet into the page if this is the first GUI. * Pass `false` to use your own stylesheet. * * @param {number} [options.touchStyles=true] * Makes controllers larger on touch devices. Pass `false` to disable touch styles. * * @param {GUI} [options.parent] * Adds this GUI as a child in another GUI. Usually this is done for you by `addFolder()`. * */ constructor( { parent, autoPlace = parent === undefined, container, width, title = 'Controls', injectStyles = true, touchStyles = true } = {} ) { /** * The GUI containing this folder, or `undefined` if this is the root GUI. * @type {GUI} */ this.parent = parent; /** * The top level GUI containing this folder, or `this` if this is the root GUI. * @type {GUI} */ this.root = parent ? parent.root : this; /** * The list of controllers and folders contained by this GUI. * @type {Array<GUI|Controller>} */ this.children = []; /** * The list of controllers contained by this GUI. * @type {Array<Controller>} */ this.controllers = []; /** * The list of folders contained by this GUI. * @type {Array<GUI>} */ this.folders = []; /** * Used to determine if the GUI is closed. Use `gui.open()` or `gui.close()` to change this. * @type {boolean} */ this._closed = false; /** * Used to determine if the GUI is hidden. Use `gui.show()` or `gui.hide()` to change this. * @type {boolean} */ this._hidden = false; /** * The outermost container element. * @type {HTMLElement} */ this.domElement = document.createElement( 'div' ); this.domElement.classList.add( 'lil-gui' ); /** * The DOM element that contains the title. * @type {HTMLElement} */ this.$title = document.createElement( 'div' ); this.$title.classList.add( 'title' ); this.$title.setAttribute( 'role', 'button' ); this.$title.setAttribute( 'aria-expanded', true ); this.$title.setAttribute( 'tabindex', 0 ); this.$title.addEventListener( 'click', () => this.openAnimated( this._closed ) ); this.$title.addEventListener( 'keydown', e => { if ( e.code === 'Enter' || e.code === 'Space' ) { e.preventDefault(); this.$title.click(); } } ); // enables :active pseudo class on mobile this.$title.addEventListener( 'touchstart', () => { } ); /** * The DOM element that contains children. * @type {HTMLElement} */ this.$children = document.createElement( 'div' ); this.$children.classList.add( 'children' ); this.domElement.appendChild( this.$title ); this.domElement.appendChild( this.$children ); this.title( title ); if ( touchStyles ) { this.domElement.classList.add( 'allow-touch-styles' ); } if ( this.parent ) { this.parent.children.push( this ); this.parent.folders.push( this ); this.parent.$children.appendChild( this.domElement ); // Stop the constructor early, everything onward only applies to root GUI's return; } this.domElement.classList.add( 'root' ); // Inject stylesheet if we haven't done that yet if ( !stylesInjected && injectStyles ) { _injectStyles( stylesheet ); stylesInjected = true; } if ( container ) { container.appendChild( this.domElement ); } else if ( autoPlace ) { this.domElement.classList.add( 'autoPlace' ); document.body.appendChild( this.domElement ); } if ( width ) { this.domElement.style.setProperty( '--width', width + 'px' ); } // Don't fire global key events while typing in the GUI: this.domElement.addEventListener( 'keydown', e => e.stopPropagation() ); this.domElement.addEventListener( 'keyup', e => e.stopPropagation() ); } /** * Adds a controller to the GUI, inferring controller type using the `typeof` operator. * @example * gui.add( object, 'property' ); * gui.add( object, 'number', 0, 100, 1 ); * gui.add( object, 'options', [ 1, 2, 3 ] ); * * @param {object} object The object the controller will modify. * @param {string} property Name of the property to control. * @param {number|object|Array} [$1] Minimum value for number controllers, or the set of * selectable values for a dropdown. * @param {number} [max] Maximum value for number controllers. * @param {number} [step] Step value for number controllers. * @returns {Controller} */ add( object, property, $1, max, step ) { if ( Object( $1 ) === $1 ) { return new OptionController( this, object, property, $1 ); } const initialValue = object[ property ]; switch ( typeof initialValue ) { case 'number': return new NumberController( this, object, property, $1, max, step ); case 'boolean': return new BooleanController( this, object, property ); case 'string': return new StringController( this, object, property ); case 'function': return new FunctionController( this, object, property ); } console.error( `gui.add failed property:`, property, ` object:`, object, ` value:`, initialValue ); } /** * Adds a color controller to the GUI. * @example * params = { * cssColor: '#ff00ff', * rgbColor: { r: 0, g: 0.2, b: 0.4 }, * customRange: [ 0, 127, 255 ], * }; * * gui.addColor( params, 'cssColor' ); * gui.addColor( params, 'rgbColor' ); * gui.addColor( params, 'customRange', 255 ); * * @param {object} object The object the controller will modify. * @param {string} property Name of the property to control. * @param {number} rgbScale Maximum value for a color channel when using an RGB color. You may * need to set this to 255 if your colors are too dark. * @returns {Controller} */ addColor( object, property, rgbScale = 1 ) { return new ColorController( this, object, property, rgbScale ); } /** * Adds a folder to the GUI, which is just another GUI. This method returns * the nested GUI so you can add controllers to it. * @example * const folder = gui.addFolder( 'Position' ); * folder.add( position, 'x' ); * folder.add( position, 'y' ); * folder.add( position, 'z' ); * * @param {string} title Name to display in the folder's title bar. * @returns {GUI} */ addFolder( title ) { return new GUI( { parent: this, title } ); } /** * Recalls values that were saved with `gui.save()`. * @param {object} obj * @param {boolean} recursive Pass false to exclude folders descending from this GUI. * @returns {this} */ load( obj, recursive = true ) { if ( obj.controllers ) { this.controllers.forEach( c => { if ( c instanceof FunctionController ) return; if ( c._name in obj.controllers ) { c.load( obj.controllers[ c._name ] ); } } ); } if ( recursive && obj.folders ) { this.folders.forEach( f => { if ( f._title in obj.folders ) { f.load( obj.folders[ f._title ] ); } } ); } return this; } /** * Returns an object mapping controller names to values. The object can be passed to `gui.load()` to * recall these values. * @example * { * controllers: { * prop1: 1, * prop2: 'value', * ... * }, * folders: { * folderName1: { controllers, folders }, * folderName2: { controllers, folders } * ... * } * } * * @param {boolean} recursive Pass false to exclude folders descending from this GUI. * @returns {object} */ save( recursive = true ) { const obj = { controllers: {}, folders: {} }; this.controllers.forEach( c => { if ( c instanceof FunctionController ) return; if ( c._name in obj.controllers ) { throw new Error( `Cannot save GUI with duplicate property "${c._name}"` ); } obj.controllers[ c._name ] = c.save(); } ); if ( recursive ) { this.folders.forEach( f => { if ( f._title in obj.folders ) { throw new Error( `Cannot save GUI with duplicate folder "${f._title}"` ); } obj.folders[ f._title ] = f.save(); } ); } return obj; } /** * Opens a GUI or folder. GUI and folders are open by default. * @param {boolean} open Pass false to close * @returns {this} * @example * gui.open(); // open * gui.open( false ); // close * gui.open( gui._closed ); // toggle */ open( open = true ) { this._closed = !open; this.$title.setAttribute( 'aria-expanded', !this._closed ); this.domElement.classList.toggle( 'closed', this._closed ); return this; } /** * Closes the GUI. * @returns {this} */ close() { return this.open( false ); } /** * Shows the GUI after it's been hidden. * @param {boolean} show * @returns {this} * @example * gui.show(); * gui.show( false ); // hide * gui.show( gui._hidden ); // toggle */ show( show = true ) { this._hidden = !show; this.domElement.style.display = this._hidden ? 'none' : ''; return this; } /** * Hides the GUI. * @returns {this} */ hide() { return this.show( false ); } openAnimated( open = true ) { // set state immediately this._closed = !open; this.$title.setAttribute( 'aria-expanded', !this._closed ); // wait for next frame to measure $children requestAnimationFrame( () => { // explicitly set initial height for transition const initialHeight = this.$children.clientHeight; this.$children.style.height = initialHeight + 'px'; this.domElement.classList.add( 'transition' ); const onTransitionEnd = e => { if ( e.target !== this.$children ) return; this.$children.style.height = ''; this.domElement.classList.remove( 'transition' ); this.$children.removeEventListener( 'transitionend', onTransitionEnd ); }; this.$children.addEventListener( 'transitionend', onTransitionEnd ); // todo: this is wrong if children's scrollHeight makes for a gui taller than maxHeight const targetHeight = !open ? 0 : this.$children.scrollHeight; this.domElement.classList.toggle( 'closed', !open ); requestAnimationFrame( () => { this.$children.style.height = targetHeight + 'px'; } ); } ); return this; } /** * Change the title of this GUI. * @param {string} title * @returns {this} */ title( title ) { /** * Current title of the GUI. Use `gui.title( 'Title' )` to modify this value. * @type {string} */ this._title = title; this.$title.innerHTML = title; return this; } /** * Resets all controllers to their initial values. * @param {boolean} recursive Pass false to exclude folders descending from this GUI. * @returns {this} */ reset( recursive = true ) { const controllers = recursive ? this.controllersRecursive() : this.controllers; controllers.forEach( c => c.reset() ); return this; } /** * Pass a function to be called whenever a controller in this GUI changes. * @param {function({object:object, property:string, value:any, controller:Controller})} callback * @returns {this} * @example * gui.onChange( event => { * event.object // object that was modified * event.property // string, name of property * event.value // new value of controller * event.controller // controller that was modified * } ); */ onChange( callback ) { /** * Used to access the function bound to `onChange` events. Don't modify this value * directly. Use the `gui.onChange( callback )` method instead. * @type {Function} */ this._onChange = callback; return this; } _callOnChange( controller ) { if ( this.parent ) { this.parent._callOnChange( controller ); } if ( this._onChange !== undefined ) { this._onChange.call( this, { object: controller.object, property: controller.property, value: controller.getValue(), controller } ); } } /** * Pass a function to be called whenever a controller in this GUI has finished changing. * @param {function({object:object, property:string, value:any, controller:Controller})} callback * @returns {this} * @example * gui.onFinishChange( event => { * event.object // object that was modified * event.property // string, name of property * event.value // new value of controller * event.controller // controller that was modified * } ); */ onFinishChange( callback ) { /** * Used to access the function bound to `onFinishChange` events. Don't modify this value * directly. Use the `gui.onFinishChange( callback )` method instead. * @type {Function} */ this._onFinishChange = callback; return this; } _callOnFinishChange( controller ) { if ( this.parent ) { this.parent._callOnFinishChange( controller ); } if ( this._onFinishChange !== undefined ) { this._onFinishChange.call( this, { object: controller.object, property: controller.property, value: controller.getValue(), controller } ); } } /** * Destroys all DOM elements and event listeners associated with this GUI */ destroy() { if ( this.parent ) { this.parent.children.splice( this.parent.children.indexOf( this ), 1 ); this.parent.folders.splice( this.parent.folders.indexOf( this ), 1 ); } if ( this.domElement.parentElement ) { this.domElement.parentElement.removeChild( this.domElement ); } Array.from( this.children ).forEach( c => c.destroy() ); } /** * Returns an array of controllers contained by this GUI and its descendents. * @returns {Controller[]} */ controllersRecursive() { let controllers = Array.from( this.controllers ); this.folders.forEach( f => { controllers = controllers.concat( f.controllersRecursive() ); } ); return controllers; } /** * Returns an array of folders contained by this GUI and its descendents. * @returns {GUI[]} */ foldersRecursive() { let folders = Array.from( this.folders ); this.folders.forEach( f => { folders = folders.concat( f.foldersRecursive() ); } ); return folders; } } export default GUI; export { BooleanController, ColorController, Controller, FunctionController, GUI, NumberController, OptionController, StringController };
Encontrar la diferencia