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