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 }
.
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.
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.
import CustomController from 'lil-gui/extras/CustomController.js';
export default class XYController extends CustomController {
$constructor() {
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 );
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 );
this.$value.x = x;
this.$value.y = y;
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( to, from ) {
to.x = from.x;
to.y = from.y;
}
$compare( a, b ) {
return a.x === b.x && a.y === b.y;
}
}
XYController.$id = 'XY';
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.
- Don't assign
this.$value
unless your intent is to reassign the targeted property.
- Always call
$onChange()
after modifying $value
.
- Implement
$copy
and $compare
.
Controllers targeting primitives can ignore these requirements.
For a guide on CSS variables and lil-gui's DOM, see Styling Custom Controllers.