lil-gui › Examples
:
XY Controller

XY Controller

This example demonstrates how to create a controller that targets an object. It assumes you've read Multiline Controller, which covers the basics of controller creation.

Imagine you need to edit a pair of numbers in the range of 0 to 1. You could just use two sliders, but sometimes it's nicer to edit both values simultaneously.

We're going to create a custom controller called XYController. It draws a square field with a handle that you can drag in two dimensions.

This controller adds a method called gui.addXY( obj, prop ). It expects obj.prop to be an object in the format { x: 0.5, y: 0.5 }.

This page:
import GUI from 'lil-gui';
import './XYController.js';

const gui = new GUI();

const obj = {
	prop: { x: 0.5, y: 0.5 }
};

gui.addXY( obj, 'prop' );

Drag the controller's handle and watch how the debug reacts.

Controller debug:
value: 
onChange
onFinishChange

In the code above, we don't want our controller to change the value of obj.prop—we only want it to modify its properties: x and y. Since we're dealing with an object instead of a primitive, we should take care not to destroy references to that object elsewhere in the code.

The following is the source of XYController. It extends CustomController just like we did in the previous example, but also highlights the extra requirements for controllers that target objects.

./XYController.js
import CustomController from 'lil-gui/extras/CustomController.js';

export default class XYController extends CustomController {

	$constructor() {

		// Create some DOM elements and add them to $widget.
		// -------------------------------------------------

		this.area = document.createElement( 'div' );
		this.area.className = 'area';

		this.handle = document.createElement( 'div' );
		this.handle.className = 'handle';

		const axisX = document.createElement( 'div' );
		axisX.className = 'axis x';

		const axisY = document.createElement( 'div' );
		axisY.className = 'axis y';

		this.area.appendChild( axisX );
		this.area.appendChild( axisY );
		this.area.appendChild( this.handle );

		this.$widget.appendChild( this.area );

		// Add interactivity to the handle. Touch support is omitted for brevity.
		// ----------------------------------------------------------------------

		this.handle.addEventListener( 'mousedown', () => {
			window.addEventListener( 'mousemove', onMouseMove );
			window.addEventListener( 'mouseup', onMouseUp );
		} );

		const onMouseMove = event => {

			const rect = this.area.getBoundingClientRect();

			const x = normalize( event.clientX, rect.left, rect.right );
			const y = normalize( event.clientY, rect.bottom, rect.top );

			// Assigning this.$value to a new object would destroy references. Object controllers
			// should usually modify $value's properties instead of assigning it outright.

			this.$value.x = x;
			this.$value.y = y;

			// Always call $onChange after modifying $value. It updates the display
			// and fires change events. (This happens automatically when assigning $value)

			this.$onChange();

		};

		const onMouseUp = () => {

			this.$onFinishChange();

			window.removeEventListener( 'mousemove', onMouseMove );
			window.removeEventListener( 'mouseup', onMouseUp );

		};

		const normalize = ( value, min, max ) => {
			const t = ( value - min ) / ( max - min );
			return Math.min( Math.max( 0, t ), 1 );
		};

	}

	$updateDisplay() {
		this.handle.style.left = this.$value.x * 100 + '%';
		this.handle.style.top = ( 1 - this.$value.y ) * 100 + '%';
	}

	// $copy is used by save() and load() to store and read values. It should copy
	// all relevant properties from the second object to the first.
	$copy( to, from ) {
		to.x = from.x;
		to.y = from.y;
	}

	// $compare is used by listen() to determine when to update the display. It should
	// return true if all properties match between two objects.
	$compare( a, b ) {
		return a.x === b.x && a.y === b.y;
	}

}

// Custom controllers give the GUI a method called "add" + $id.
XYController.$id = 'XY';

// See Styling Custom Controllers for a guide on custom controller CSS.
XYController.$style =
`.lil-gui .controller.XY .area {
	overflow: hidden;
	position: relative;
	width: 100%;
	aspect-ratio: 1;
	background: var(--widget-color);
	border-radius: var(--widget-border-radius);
}

.lil-gui .controller.XY .handle { 
	position: absolute;
	width: 10%;
	height: 10%;
	margin-left: -5%;
	margin-top: -5%;
	background: var(--text-color);
	border-radius: 100%;
	cursor: pointer;
}

.lil-gui .controller.XY .axis { 
	position: absolute;
	background: var(--focus-color);
}

.lil-gui .controller.XY .axis.x { 
	top: 50%;
	width: 100%;
	height: 1px;
}

.lil-gui .controller.XY .axis.y { 
	left: 50%;
	height: 100%;
	width: 1px;
}`;

CustomController.register( XYController );

$onChange

$onChange is analogous to $onFinishChange, but it should be called every time the value is changed. It doesn't appear in the previous example because it's called automatically when assigning this.$value.

We can't detect changes to the properties of $value, so the $onChange method tells the controller to update its display and fire change events. You should call this method every time you modify $value.

$copy

$copy( to, from ) should apply the properties of the second argument to the first. You don't need to create a new object or return a value.

Unless we implement $copy, calls to gui.load() and save() can destroy object references. By default, these methods assume you're controlling a primitive, and use the assignment operator.

$compare

$compare( a, b ) should compare the properties of a and b and return true if they're all equivalent.

Unless we implement $compare, our controller won't update after calling listen(). Normally, listen uses the === operator to detect value changes, but this won't work for object types. For objects, listen uses $compare to see if any properties have changed since the previous frame, and updates the display accordingly.

Summary

Here are the extra steps needed to make an object controller.

Controllers targeting primitives can ignore these requirements.


For a guide on CSS variables and lil-gui's DOM, see Styling Custom Controllers.