Plugin development
Create your own plugins and share them with the world.
What is the plugin?
Tweakpane can be extended with the plugin system. For example, Essentials plugin adds support for various useful components to the pane.
import {Pane} from 'tweakpane';
import * as EssentialsPlugin from '@tweakpane/plugin-essentials';
const pane = new Pane();
// Register plugin to the pane
pane.registerPlugin(EssentialsPlugin);
// Add a FPS graph
const fpsGraph = pane.addBlade({
view: 'fpsgraph',
label: 'fps',
});
// ...
How it works
Let's learn about the basics of the plugin by looking at a practical example. The goal of this section is creating a plugin that provides a custom binding control for the option view: 'counter'
. This control can simply increment the bound value.
import {Pane} from 'tweakpane';
const PARAMS = {count: 0};
const pane = new Pane();
pane.registerPlugin(CounterInputPlugin);
// Add a custom binding control
// for `view: 'counter'`
pane.addBinding(PARAMS, 'count', {
view: 'counter',
});
Plugin pool
All views of the pane will be created using the plugin. The pane has a plugin pool which comes with pre-installed plugins by default.
Plugin pool
+------------------------------------+
| |
| plugins[0]: Point2dInputPlugin |
| plugins[1]: Point3dInputPlugin |
| plugins[2]: Point4dInputPlugin |
| plugins[3]: StringInputPlugin |
| plugins[4]: NumberInputPlugin |
| plugins[5]: StringColorInputPlugin |
| plugins[6]: ObjectColorInputPlugin |
| plugins[7]: NumberColorInputPlugin |
| plugins[8]: BooleanInputPlugin |
| ... |
| |
+------------------------------------+
registerPlugin()
can add specified plugins to the pool.
const CounterPluginBundle: TpPluginBundle = {
// Identifier of the plugin bundle
id: 'counter',
// Plugins that should be registered
plugins: [
CounterInputPlugin,
],
// Additional CSS for this bundle
css: `
.tp-counter {align-items: center; display: flex;}
.tp-counter div {color: #00ffd680; flex: 1;}
.tp-counter button {background-color: #00ffd6c0; border-radius: 2px; color: black; height: 20px; width: 20px;}
`,
};
pane.registerPlugin(CounterPluginBundle);
Plugin pool
+------------------------------------+
| |
| plugins[0]: CounterInputPlugin ... <-- Added
| |
| plugins[1]: Point2dInputPlugin |
| plugins[2]: Point3dInputPlugin |
| plugins[3]: Point4dInputPlugin |
| plugins[4]: StringInputPlugin |
| plugins[5]: NumberInputPlugin |
| plugins[6]: StringColorInputPlugin |
| plugins[7]: ObjectColorInputPlugin |
| plugins[8]: NumberColorInputPlugin |
| plugins[9]: BooleanInputPlugin |
| ... |
| |
+------------------------------------+
Plugin structure
Tweakpane plugin should be an object of the type TpPlugin
.
const CounterInputPlugin: TpPlugin = createPlugin({
id: 'counter',
type: 'input',
accept: () => {...},
binding: {
reader: () => {...},
writer: () => {...},
},
controller: () => {...},
});
When user calls addBinding()
, the pane passes the value and the options to each plugin to see if it can handle this binding by its accept()
.
+--------------------------------------------------------------------------------+
| Pane |
| Plugin pool |
| |
| +------------------+ |
| | plugins[0] | |
+------+ | Can you handle this binding? | | |
| User | *--> | addBinding() *------+------------------------------> | accept() ....... < NO.. |
+------+ | | | | |
| | +------------------+ |
| | |
| | +------------------+ |
| | | plugins[1] | |
| | How about you? | | |
| +------------------------------> | accept() ....... < NO.. |
| | | | |
| | +------------------+ |
| | |
| | +------------------+ |
| | | plugins[2] | |
| | How about you? | | |
| +------------------------------> | accept() ....... < YES! |
| | | |
| Please create a binding | | |
| for the specified value | | |
| *----------------------------> | binding.reader() | |
| | .writer() | |
| Please create a controller | | |
| (and a view) | | |
| for the bound value | | |
| *----------------------------> | controller() | |
| | | |
| +------------------+ |
| |
+--------------------------------------------------------------------------------+
type
There are 3 types of plugins for Tweakpane: input
, monitor
, and blade
.
Type | Description | Target |
---|---|---|
input |
Provides a view for binding | addBinding() |
monitor |
Provides a view for readonly binding | addBinding() with {readonly: true} |
blade |
Provides a view without binding | addBlade() |
From here on, we will choose input
to provide a custom view for the binding.
const CounterInputPlugin = {
// ...
type: 'input',
// ...
};
accept()
Decides whether the plugin accepts the specified parameters. Returns a typed value and parameters if the plugin accepts the specified parameters, or null
if the plugin sees them off and pass them to the next plugin. (Reference)
const CounterInputPlugin = {
// ...
accept(value: unknown, params: Record<string, unknown>) {
if (typeof value !== 'number') {
return null;
}
if (params.view !== 'counter') {
return null;
}
return {
initialValue: value,
params: params,
};
},
// ...
};
binding
Configurations of the binding. Tweakpane can convert the external (= input) value into an internal value for certain reasons. For example, string color value '#112233'
is inconvenient when modifying color components in various color spaces. Tweakpane converts it into the internal color class using this configuration.
If you want to handle a primitive value (number
, string
, or boolean
), you don't have to convert a value here. (Reference)
const CounterInputPlugin = {
// ...
binding: {
reader: () => (value: unknown) => Number(value),
writer: () => (target: BindingTarget, value: number) => {
target.write(value);
},
},
// ...
};
controller()
Create a controller and a view for the binding.
const CounterInputPlugin = {
// ...
controller(args) {
return new CounterController(args.document, {
value: args.value,
viewProps: args.viewProps,
});
},
// ...
};
Tweakpane adopts good old MVC structure. Controller
creates a view, handle user interaction, and update models (in this case, the bound value).
import {Value, ValueController, ViewProps} from '@tweakpane/core';
import {CounterView} from './view';
interface Config {
value: Value<number>;
viewProps: ViewProps;
}
export class CounterController implements ValueController<number, CounterView> {
public readonly value: Value<number>;
public readonly view: CounterView;
public readonly viewProps: ViewProps;
constructor(doc: Document, config: Config) {
// Models
this.value = config.value;
this.viewProps = config.viewProps;
// Create a view
this.view = new CounterView(doc, {
value: config.value,
viewProps: this.viewProps,
});
// Handle user interaction
this.view.buttonElement.addEventListener('click', () => {
// Update a model
this.value.rawValue += 1;
});
}
}
View
creates DOM elements and apply value changes to them.
import {Value, View, ViewProps} from '@tweakpane/core';
interface Config {
value: Value<number>;
viewProps: ViewProps;
}
export class CounterView implements View {
public readonly element: HTMLElement;
public readonly buttonElement: HTMLButtonElement;
constructor(doc: Document, config: Config) {
// Create view elements
this.element = doc.createElement('div');
this.element.classList.add('tp-counter');
// Apply value changes to the preview element
const previewElem = doc.createElement('div');
const value = config.value;
value.emitter.on('change', () => {
previewElem.textContent = String(value.rawValue);
});
previewElem.textContent = String(value.rawValue);
this.element.appendChild(previewElem);
// Create a button element for user interaction
const buttonElem = doc.createElement('button');
buttonElem.textContent = '+';
this.element.appendChild(buttonElem);
this.buttonElement = buttonElem;
}
}
Priority of the plugin
Newer plugins have higher priority and manually registered plugins have higher priority than pre-installed plugins.
Plugin pool
+------------------------------------+
| |
| plugins[0]: CounterInputPlugin | ^ Newer, higher priority
| | |
| plugins[1]: Point2dInputPlugin | |
| plugins[2]: Point3dInputPlugin | |
| plugins[3]: Point4dInputPlugin | |
| plugins[4]: StringInputPlugin | |
| plugins[5]: NumberInputPlugin | |
| plugins[6]: StringColorInputPlugin | |
| plugins[7]: ObjectColorInputPlugin | |
| plugins[8]: NumberColorInputPlugin | |
| plugins[9]: BooleanInputPlugin | |
| ... | * Older, lower priority
| |
+------------------------------------+
It implies that you should be cautious not to accept an excessibely wide range of parameters. view
option can be useful for this purpose.
Next step
That concludes the basics of the plugin structure. There are many other practices and useful methods for the plugin development. See @tweakpane/plugin-template for the first step of your development. Additionally, @tweakpane/plugin-essentials can also serve as a practical example.