Diff
checker
텍스트
텍스트
이미지
문서
Excel
폴더
Legal
Enterprise
데스크톱
요금제
로그인
데스크톱 앱 다운로드
텍스트 비교
두 텍스트 파일의 차이점을 찾아보세요
도구
기록
실시간 편집
공백 변경 숨기기
변경 없는 행 숨기기
줄바꿈 비활성화
레이아웃
나란히 보기
합쳐 보기
비교 단위
스마트
단어
글자
텍스트 스타일
모양 변경
구문 강조
언어 선택
제외
텍스트 변환
첫 변경으로
수정
Diffchecker Desktop
가장 안전하게 Diffchecker를 사용하는 방법. 데스크톱 앱을 사용하면 비교 데이터가 외부로 전송되지 않습니다!
데스크톱 앱 받기
lil-gui 0.11.0 → 0.16.0
생성일
5년 전
비교 결과 만료 없음
초기화
내보내기
공유
설명
243 삭제
행
총
삭제
글자
총
삭제
이 기능을 계속 사용하려면 업그레이드해 주세요
Diff
checker
Pro
요금제 보기
1069 행
복사
226 추가
행
총
추가
글자
총
추가
이 기능을 계속 사용하려면 업그레이드해 주세요
Diff
checker
Pro
요금제 보기
1054 행
복사
/**
/**
* 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
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;
}
}
복사
복사됨
복사
복사됨
/**
* 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
.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 );
복사
복사됨
복사
복사됨
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.
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() {
복사
복사됨
복사
복사됨
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 m
ouse
listeners
//
M
ouse
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 );
//
T
ouch
drag
//
Bind t
ouch
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 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
저장된 비교 결과
원본
파일 열기
/** * 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 };
수정본
파일 열기
/** * 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 };
비교하기