Pi.js Plugin System

Pi.js features a powerful plugin system that allows you to extend the library’s functionality without modifying the core code. Plugins can add new commands, screen-specific features, and custom functionality seamlessly.

What are Plugins?

Plugins are modular extensions that can:

  • Add new commands to the Pi.js API
  • Add screen-specific commands
  • Extend screen data with custom properties
  • Hook into screen initialization and cleanup
  • Share an API with other plugins through services
  • Provide additional utilities and features

Plugins work seamlessly with both build formats:

  • ESM (ES Modules)
  • IIFE (Immediately Invoked Function Expression for <script> tags)

Using Existing Plugins

The full build of Pi.js already includes the gamepad, keyboard, pointer, polygons, and sound plugins, so they need no extra script tags or imports. The lite build includes none of them; load the standalone plugins you need after it. Loading a plugin that is already registered throws DUPLICATE_PLUGIN.

Browser (IIFE with <script> tags)

The simplest way to use plugins in the browser:

<!DOCTYPE html>
<html>
<head>
	<meta charset="utf-8">
	<title>Pi.js with Plugin</title>
</head>
<body>
	<!-- Load Pi.js -->
	<script src="build/pi.min.js"></script>
	
	<!-- Load plugin (auto-registers itself) -->
	<script src="build/plugins/my-plugin/my-plugin.min.js"></script>
	
	<script>
		$.ready( () => {
			$.screen( "300x200" );
			
			// Use plugin commands
			$.myPluginCommand( "hello" );
		} );
	</script>
</body>
</html>

ES Modules (ESM)

For modern JavaScript projects, import Pi.js first and then import the plugin for its side effect. The plugin registers itself when window.pi already exists:

import pi from "./build/pi.esm.min.js";

// The plugin registers itself
import "./build/plugins/my-plugin/my-plugin.esm.min.js";

pi.ready( () => {
	pi.screen( "300x200" );
	pi.myPluginCommand( "hello" );
} );

The plugin’s default export is its init function, for when you want to register it yourself. Import the plugin before Pi.js so that it cannot register itself, then call registerPlugin(). Do not do both for the same plugin: registering a name twice throws DUPLICATE_PLUGIN.

import myPlugin from "./build/plugins/my-plugin/my-plugin.esm.min.js";
import pi from "./build/pi.esm.min.js";

// Register the plugin
pi.registerPlugin( {
	"name": "my-plugin",
	"init": myPlugin
} );

Creating Your Own Plugin

Basic Plugin Structure

Create a new directory in plugins/ with your plugin name:

plugins/
  my-plugin/
    index.js           # Source code
    README.md          # Plugin documentation
    package.json       # Optional: Plugin metadata

Minimal Plugin Example

plugins/my-plugin/index.js:

/**
 * My Custom Plugin for Pi.js
 * 
 * @param {Object} pluginApi - Plugin API provided by Pi.js
 */
function myPlugin( pluginApi ) {
	
	// Add a simple command
	pluginApi.addCommand( "hello", hello, false, [ "message" ] );
	
	function hello( options ) {
		const message = options.message || "Hello, Pi.js!";
		console.log( message );
		return message;
	}
	
	// Add a screen command
	pluginApi.addCommand( "drawCustomShape", drawCustomShape, true, [ "x", "y", "size" ] );
	
	function drawCustomShape( screenData, options ) {
		const x = options.x || 0;
		const y = options.y || 0;
		const size = options.size || 10;
		
		screenData.api.rect( x, y, size, size );
	}
}

// Auto-register in IIFE mode
if( typeof window !== "undefined" && window.pi ) {
	window.pi.registerPlugin( {
		"name": "my-plugin",
		"version": "1.0.0",
		"description": "My custom plugin",
		"init": myPlugin
	} );
}

// usage:
$.screen( "300x200" );
$.drawCustomShape( 25, 50, 50 );

Advanced Plugin Example

plugins/advanced-plugin/index.js:

/**
 * Advanced Plugin Example
 * 
 * Demonstrates screen data, initialization hooks, and utilities.
 */
function advancedPlugin( pluginApi ) {
	
	// Add custom data to each screen
	pluginApi.addScreenDataItem( "myPluginState", {
		"counter": 0,
		"enabled": true
	} );
	
	// Add initialization hook
	pluginApi.addScreenInitFunction( ( screenData ) => {
		console.log( `Advanced plugin initialized for screen ${screenData.id}` );
		
		// Set up initial state
		screenData.myPluginState.counter = 0;
	} );
	
	// Add cleanup hook
	pluginApi.addScreenCleanupFunction( ( screenData ) => {
		console.log( `Advanced plugin cleanup for screen ${screenData.id}` );
	} );
	
	// Add command that uses screen data
	pluginApi.addCommand( "incrementCounter", incrementCounter, true, [] );
	
	function incrementCounter( screenData ) {
		screenData.myPluginState.counter++;
		return screenData.myPluginState.counter;
	}
	
	// Add command that accesses the main API
	pluginApi.addCommand( "getApiVersion", getApiVersion, false, [] );
	
	function getApiVersion() {
		const api = pluginApi.getApi();
		return api.version;
	}
	
	// Use utility functions
	pluginApi.addCommand( "randomColor", randomColor, false, [] );
	
	function randomColor() {
		const utils = pluginApi.utils;
		const r = Math.floor( utils.rndRange( 0, 256 ) );
		const g = Math.floor( utils.rndRange( 0, 256 ) );
		const b = Math.floor( utils.rndRange( 0, 256 ) );
		return utils.rgbToColor( r, g, b, 255 );
	}
}

// Auto-register in IIFE mode
if( typeof window !== "undefined" && window.pi ) {
	window.pi.registerPlugin( {
		"name": "advanced-plugin",
		"version": "1.0.0",
		"description": "Advanced plugin demonstrating all features",
		"init": advancedPlugin
	} );
}

// Usage:
$.screen( "300x200" );
for( let i = 0; i < 5; i += 1 ) {
	$.setColor( $.randomColor() );
	$.print( "Random Color" );
}

Plugin API Reference

Registration

registerPlugin( name, init, version, description, dependencies )

Register a plugin with Pi.js. It is usually called with an object.

  • name (string): Unique plugin name
  • init (function): Initialization function that receives pluginApi
  • version (string): Optional version
  • description (string): Optional description
  • dependencies (array): Optional names of plugins that must initialize first

A plugin stays pending until every plugin it depends on has initialized. Registering a name that is already registered throws DUPLICATE_PLUGIN. If init throws, registerPlugin() throws PLUGIN_INIT_FAILED, nothing the plugin added is installed, and its name can be registered again.

The pluginApi object passed to your plugin’s init function provides the methods below. Call the add... methods while init runs; calling one after init returns throws REGISTRATION_CLOSED.

Commands

addCommand( name, fn, isScreen, parameterNames, isScreenOptional )

Add a global command to the Pi.js API.

  • name (string): Command name (will be available as pi.commandName())
  • fn (function): Command function that receives options object
  • isScreen (boolean): Whether this is a screen-specific command
  • parameterNames (array): Array of parameter names
  • isScreenOptional (boolean): Optional. Whether a screen command can run without an active screen

Example:

pluginApi.addCommand( "myCommand", myFn, false, [ "param1", "param2" ] );

function myFn( options ) {
	console.log( options.param1, options.param2 );
}

// Usage:
$.myCommand( "a", "b" );  // Positional
$.myCommand( { "param1": "a", "param2": "b" } );  // Named

Screen Data

addScreenDataItem( name, value )

Add custom data to all screens. The value is cloned for each screen that exists when the plugin initializes and for each screen created later.

Example:

pluginApi.addScreenDataItem( "myData", {
	"value": 0,
	"enabled": true
} );

// Access in commands:
function myCommand( screenData ) {
	screenData.myData.value++;
}
addScreenDataItemGetter( name, fn )

Add dynamic screen data that’s generated for each screen that exists when the plugin initializes and for each screen created later.

Lifecycle Hooks

addScreenInitFunction( fn )

Register a function to be called once for each screen that exists when the plugin initializes and once for each screen created later.

Example:

pluginApi.addScreenInitFunction( ( screenData ) => {
	screenData.myData.initialized = true;
} );
addScreenPreCleanupFunction( fn )

Register a function to be called when a screen is removed, before the renderer and other modules clean up. Use it to cancel work the screen owns and to release resources that still need the screen’s textures. Do not draw or start new work on the screen being removed.

addScreenCleanupFunction( fn )

Register a function to be called when a screen is removed.

Example:

pluginApi.addScreenCleanupFunction( ( screenData ) => {
	// Clean up resources
	screenData.myData = null;
} );

Utilities

getApi()

Get reference to the main Pi.js API object.

Example:

const api = pluginApi.getApi();
console.log( api.version );
utils

Access to Pi.js utility functions:

  • parseOptions( args, parameterNames ) – Parse function arguments
  • isFunction( fn ) – Check if value is a function
  • isDomElement( el ) – Check if value is a DOM element
  • isObjectLiteral( obj ) – Check if value is a plain object
  • convertToColor( color ) – Convert color formats
  • rgbToColor( r, g, b, a ) – Create color object
  • clamp( num, min, max ) – Clamp number to range
  • inRange( point, hitBox ) – Check point in rectangle
  • rndRange( min, max ) – Random number in range
  • degreesToRadian( deg ) – Convert degrees to radians
  • radiansToDegrees( rad ) – Convert radians to degrees
  • padL( str, len, c ) – Pad string left
  • pad( str, len, c ) – Pad string center
  • getInt( val, def ) – Parse integer with default
  • And more…

Services

A service is an object that one plugin publishes for the plugins that depend on it. The sound plugin provides a service that the sound-advanced plugin is built on.

provideService( service )

Publish one service object. Call it while init runs; a call after init returns throws SERVICE_PROVIDE_CLOSED. A plugin provides at most one service, so a second call throws DUPLICATE_SERVICE, and a value that is not an object throws INVALID_SERVICE. If init fails, the service is discarded.

getService( pluginName )

Return the service of a plugin named in this plugin’s dependencies. Dependencies initialize first, so the call works inside init. It throws SERVICE_NOT_AVAILABLE when the plugin is not a declared dependency, is not initialized, or provided no service.

Example:

$.registerPlugin( {
	"name": "mixer",
	"init": ( pluginApi ) => {
		pluginApi.provideService( {
			"setLevel": ( level ) => {
				console.log( `Level: ${level}` );
			}
		} );
	}
} );

$.registerPlugin( {
	"name": "audio-tools",
	"dependencies": [ "mixer" ],
	"init": ( pluginApi ) => {
		const mixer = pluginApi.getService( "mixer" );
		pluginApi.addCommand( "mute", mute, false, [] );

		function mute() {
			mixer.setLevel( 0 );
		}
	}
} );

// Usage:
$.mute();

Building Plugins

Automatic Build

Plugins are built automatically when you build Pi.js:

node scripts/build.js

This will:

  1. Build Pi.js in both formats (ESM and IIFE)
  2. Automatically discover and build all plugins in the plugins/ directory
  3. Generate both minified and unminified versions
  4. Output plugin builds to build/plugins/<plugin-name>/
  5. Skip any directories without an index.js file

Build Individual Plugin

To build a single plugin during development:

node scripts/build-plugin.js my-plugin

This is useful when you’re actively developing a plugin and don’t want to rebuild Pi.js every time.

Using the Source File Directly

For simple plugins, you can just use the index.js source file directly without building. Modern browsers support ES modules. Import Pi.js first, and the auto-register block at the end of the file registers the plugin:

<script type="module">
	import pi from "./build/pi.esm.min.js";

	// The plugin registers itself
	import "./plugins/my-plugin/index.js";

	pi.ready( () => {
		pi.screen( "300x200" );
		pi.drawCustomShape( 25, 50, 50 );
	} );
</script>

Best Practices

1. Naming Conventions

  • Use descriptive, unique command names to avoid conflicts
  • Prefix commands with your plugin name if needed: myPlugin_command()
  • Follow Pi.js naming conventions (camelCase)

2. Error Handling

  • Validate parameters in your commands
  • Throw descriptive errors with error codes
  • Use try-catch for async operations

Example:

function myCommand( options ) {
	if( typeof options.value !== "number" ) {
		const error = new TypeError( "myCommand: value must be a number" );
		error.code = "INVALID_VALUE";
		throw error;
	}
	// ... rest of implementation
}

3. Documentation

  • Include JSDoc comments for all functions
  • Create a README.md for your plugin
  • Document all commands and their parameters
  • Provide usage examples

4. Compatibility

  • Support both build formats (ESM and IIFE)
  • Test in browser environments
  • Don’t depend on external libraries unless necessary
  • Use Pi.js utilities when available

5. Resource Management

  • Clean up resources in cleanup hooks
  • Avoid memory leaks
  • Don’t modify core Pi.js objects
  • Use screen data for per-screen state

6. Testing

  • Test your plugin with different screen configurations
  • Test command parameter variations
  • Test with multiple screens active
  • Test screen removal and cleanup

Example Plugins

Simple Drawing Plugin

function drawingTools( pluginApi ) {
	
	// Draw a star
	pluginApi.addCommand( "star", star, true, [ "x", "y", "radius", "points" ] );
	
	function star( screenData, options ) {
		const x = options.x || 0;
		const y = options.y || 0;
		const radius = options.radius || 50;
		const points = options.points || 5;
		const innerRadius = radius * 0.5;
		
		let px1, py1;
		for( let i = 0; i < points * 2 + 1; i++ ) {
			const r = i % 2 === 0 ? radius : innerRadius;
			const angle = ( i * Math.PI ) / points;
			const px2 = x + r * Math.cos( angle - Math.PI / 2 );
			const py2 = y + r * Math.sin( angle - Math.PI / 2 );
			
			if( i !== 0 ) {
				screenData.api.line( px1, py1, px2, py2 );
			}
			px1 = px2;
			py1 = py2;
		}
	}
}

if( typeof window !== "undefined" && window.$ ) {
	$.registerPlugin( {
		"name": "drawing-tools",
		"version": "1.0.0",
		"init": drawingTools
	} );
}

// Usage:
$.screen( "300x200" );
$.setColor( 14 );
$.star( 150, 100, 90, 5 );

Getting Plugin List

You can check which plugins are registered. Each entry has the plugin’s name, version, description, initialized flag, and state: "pending" while it waits for a dependency, "initialized", or "failed".

const plugins = $.getPlugins();
$.screen( "300x200" );
for( const plugin of plugins ) {
	console.log( plugin );
	$.print( plugin.name );
}

Support

For questions or issues:

  • Check the main Pi.js documentation
  • Review the source code in src/core/plugins.js
  • Look at example plugins in the plugins directory
  • Examine how core modules use the command system

Contributing

To contribute a plugin:

  1. Create your plugin in the plugins/ directory
  2. Follow the coding conventions in the main Pi.js style guide
  3. Include comprehensive documentation
  4. Test thoroughly
  5. Submit a pull request