Guide to Creating relottie Plugins
Learn how to create custom relottie plugins to transform and analyze Lottie animations. This guide covers plugin fundamentals, structure, typing, and provides a practical example.
Guide: Creating relottie Plugins
relottie's power comes from its plugin ecosystem. While you can use existing plugins for common tasks like analysis and metadata extraction, you'll often want to create custom plugins to implement specific transformations or logic tailored to your needs.
Plugin Fundamentals
A relottie plugin is essentially a function that integrates with the unified processing pipeline. Specifically, most custom plugins you write will be transformer plugins. These plugins receive the LAST (Lottie Abstract Syntax Tree) and can modify it, or they can inspect it and attach data to the VFile being processed.
Basic Structure
A transformer plugin typically follows this structure:
import type { Plugin, Transformer } from "unified";
import type { Root } from "@lottiefiles/last";
import type { VFile, Data } from "vfile"; // For interacting with the virtual file
// 1. Define an interface for your plugin's options (optional)
export interface MyPluginOptions {
someSetting?: boolean;
valueToUse?: string;
}
// 2. Define an interface for any data your plugin might attach to VFile.data (optional)
export interface MyPluginFileData extends Data {
myPluginResult?: {
info: string;
count: number;
};
}
// 3. The plugin function itself
// It typically accepts options and returns the core transformer function.
const myCustomPlugin: Plugin<[MyPluginOptions?], Root> = (
options: MyPluginOptions = {} // Provide default options if needed
) => {
// Default values for options can be set here
const { someSetting = false, valueToUse = "default" } = options;
// 4. The transformer function: this does the actual work on the tree and file
const transformer: Transformer<Root, Root> = (tree: Root, file: VFile) => {
// Logic to inspect and/or modify the 'tree' (LAST) goes here.
// You have access to 'options' (e.g., 'someSetting', 'valueToUse').
// Example: Attaching data to the VFile
const pluginDataStore: MyPluginFileData = (file.data as MyPluginFileData) || {};
pluginDataStore.myPluginResult = {
info: `Processed with value: ${valueToUse}`,
count: 0,
};
// Make sure to assign it back if file.data was initially undefined
file.data = pluginDataStore;
// If modifying the tree, you'd directly manipulate 'tree' object.
// e.g., using unist-util-visit to find and change nodes.
};
return transformer;
};
export default myCustomPlugin;Outer Function (
myCustomPlugin):Takes optional configuration (
options). It's good practice to define an interface for these options (MyPluginOptions).Returns the actual
transformerfunction.
Plugin<[MyPluginOptions?], Root>Signature:This is a generic type from
unified.The first type parameter (
[MyPluginOptions?]) is an array defining the expected types for the options passed to.use(plugin, ...options).?makes the options themselves optional for the.usecall.The second type parameter (
Root) specifies the type of the AST node the plugin primarily operates on (usuallyRootfor the entire Lottie tree).
Inner Transformer Function (
transformer):This is where the core logic resides. It receives:
tree: TheRootnode of the LAST.file: The VFile object associated with the current processing pipeline. This is crucial for:Accessing File Path:
file.path(if the input was a file or path was provided).Reporting Messages:
file.message('Something happened', node)can be used to report warnings or errors, optionally associated with a specific ASTnode(for positional information).Sharing Data:
file.datais a key-value store where plugins can attach information. For example,relottie-metadataattaches its findings tofile.data.metadata. Your plugin can read data set by previous plugins or write its own. Define an interface (e.g.,MyPluginFileData) for type safety if your plugin usesfile.data.
The transformer can directly modify the
treeobject. These modifications are then passed to the next plugin or to the stringifier.
Typing Your Plugins (TypeScript)
Using TypeScript and correctly typing your plugins is strongly recommended for clarity, maintainability, and preventing errors:
Use
PluginandTransformerfromunified: Import these types and use their generics as shown above.Use
lastNode Types: Import specific node types (Root,Attribute,Element,Primitive,Parentetc.) from@lottiefiles/lastfor type safety when working with nodes within your transformer (e.g., when usingunist-util-visit).Define Option and
VFile.dataInterfaces: Clearly type any options your plugin accepts and any data it adds toVFile.data.
Example: Text Layer Modifier and Reporter Plugin
Let's create a new plugin that performs two actions:
Modifies the text content of all text layers by appending a suffix.
Collects the original and new text values and reports them via
VFile.data.
This example uses unist-util-visit for tree traversal.
modify-text-layers-plugin.ts:
import type { Plugin, Transformer } from "unified";
import type { Root, Element, ObjectNode, Attribute, Primitive, Parent } from "@lottiefiles/last";
import { TITLES } from "@lottiefiles/last";
import { visit, CONTINUE, EXIT } from "unist-util-visit";
import type { VFile, Data } from "vfile";
// 1. Plugin Options Interface
export interface ModifyTextOptions {
suffix?: string;
ignoreCase?: boolean; // Example of another option
}
// 2. VFile Data Interface for this plugin's output
export interface TextLayersData extends Data {
textModifications?: {
original: string;
modified: string;
layerName: string;
}[];
}
// 3. The Plugin
const modifyTextLayersPlugin: Plugin<[ModifyTextOptions?], Root> = (options = {}) => {
const { suffix = " - Modified!" } = options; // Default suffix
// 4. The Transformer Function
const transformer: Transformer<Root, Root> = (tree: Root, file: VFile) => {
const modifications: TextLayersData["textModifications"] = [];
let textDocumentNode: ObjectNode | undefined; // To store 't.d' object node
// Traverse to find text layers and their text content
visit(tree, "element", (layerNode: Element) => {
// Check if it's a text layer (ty: 5)
// A more robust check would be to find the 'ty' attribute within the layerNode first.
// This simplified check assumes a common structure.
let isTextLayer = false;
let layerName = "Unknown Layer";
if (layerNode.title === TITLES.object.layerText) {
isTextLayer = true;
}
// Attempt to get layer name (nm attribute)
visit(layerNode, "attribute", (attrNode: Attribute) => {
if (attrNode.title === TITLES.string.name) {
layerName = ((attrNode.children[0] as Primitive)?.value as string) || layerName;
return EXIT; // Found name, stop search in this layerNode for name
}
return CONTINUE;
});
if (isTextLayer) {
// Find the Text Document Data: text -> documentData (t.d)
visit(layerNode, "element", (textElementNode: Element) => {
if (textElementNode.title === TITLES.element.textData) {
// Found 't' (Text Properties) element
visit(textElementNode, "element", (docDataElementNode: Element) => {
if (docDataElementNode.title === TITLES.element.textDocumentData) {
// Found 'd' (Document Data) element which contains 'k' (keyframes)
textDocumentNode = docDataElementNode.children[0] as ObjectNode;
return EXIT; // Found t.d, stop this inner visit
}
return CONTINUE;
});
return EXIT; // Processed textData element
}
return CONTINUE;
});
if (textDocumentNode) {
// The actual text is often in the first keyframe: textDocumentData -> keyframes -> 0 -> startValue -> text (k[0].s.t)
// This structure can be complex. For simplicity, we'll look for a direct 't' attribute if it exists,
// or a common path. A robust plugin would handle various text data structures.
// This is a simplified traversal looking for 't' (text value) attribute within document data.
visit(textDocumentNode, "attribute", (textAttr: Attribute) => {
if (textAttr.title === TITLES.string.text) {
const textPrimitive = textAttr.children[0] as Primitive | undefined;
if (textPrimitive && typeof textPrimitive.value === "string") {
const originalText = textPrimitive.value;
textPrimitive.value += suffix;
modifications.push({
original: originalText,
modified: textPrimitive.value,
layerName,
});
}
return EXIT; // Text attribute processed for this layer
}
return CONTINUE;
});
}
}
textDocumentNode = undefined; // Reset for next layer
return CONTINUE; // Continue to next layer or node
});
// Attach collected data to VFile
if (modifications.length > 0) {
const pluginVFileData = (file.data as TextLayersData) || {};
pluginVFileData.textModifications = modifications;
file.data = pluginVFileData;
console.log(`Plugin: Modified ${modifications.length} text layer(s).`);
}
};
return transformer;
};
export default modifyTextLayersPlugin;Using the plugin:
import { relottie } from "@lottiefiles/relottie";
import modifyTextLayersPlugin, { type TextLayersData, type ModifyTextOptions } from "./modify-text-layers-plugin";
const inputLottieWithText =
'{"v":"5.5.2", "nm":"Text Example", "fr":30, "w":500, "h":100, "layers":[
{"ddd":0,"ind":1,"ty":5,"nm":"First Text Layer","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[250,50,0],"ix":2},"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"t":{"d":{"k":[{"s":{"s":24,"f":"Arial","t":"Hello World","j":0,"tr":0,"lh":28.8,"ls":0,"fc":[0,0,0]},"t":0}]},"ip":0,"op":90,"st":0,"bm":0}},
{"ddd":0,"ind":2,"ty":5,"nm":"Second Text Layer","sr":1,"ks":{},"ao":0,"t":{"d":{"k":[{"s":{"s":18,"f":"Roboto","t":"Another line of text"},"t":0}]},"ip":0,"op":90,"st":0,"bm":0}}
]}';
async function runTextModification() {
try {
const processor = relottie().use(modifyTextLayersPlugin, { suffix: " (Updated)" } as ModifyTextOptions);
const file = await processor.process(inputLottieWithText);
console.log("\\nFinal Lottie JSON:");
console.log(String(file));
const fileData = file.data as TextLayersData;
if (fileData.textModifications && fileData.textModifications.length > 0) {
console.log("\\nText Modifications Report:");
fileData.textModifications.forEach(mod => {
console.log(`- Layer '${mod.layerName}':`);
console.log(` Original: "${mod.original}"`);
console.log(` Modified: "${mod.modified}"`);
});
}
} catch (error) {
console.error("Error during text modification:", error);
}
}
runTextModification();Expected Output (simplified):
Plugin: Modified 2 text layer(s).
Final Lottie JSON:
{ ... "t":"Hello World (Updated)" ... "t":"Another line of text (Updated)" ...}
Text Modifications Report:
- Layer 'First Text Layer':
Original: "Hello World"
Modified: "Hello World (Updated)"
- Layer 'Second Text Layer':
Original: "Another line of text"
Modified: "Another line of text (Updated)"This example covers:
Defining plugin options (
ModifyTextOptions).Defining a structure for data to be attached to
VFile(TextLayersData).Using
unist-util-visitto find relevant nodes (text layers, then text data attributes).Modifying the
valueofPrimitivetext nodes.Storing results of the modification in
file.data.Accessing this data after processing.
Note: Accessing text data in Lottie (specifically t.d.k[0].s.t) can be complex due to keyframes and expressions. This example uses a simplified traversal. A production-ready plugin would need to handle the Lottie text data structure more robustly.
Key Steps in Plugin Development
Define the Goal: What specific transformation or analysis should the plugin perform?
Identify Target Nodes: Determine which
lastnode types and properties you need to interact with by consulting the LAST Specification.Choose Traversal Strategy: Use
unist-util-visitor otherunistutilities to find the target nodes.Implement Logic: Write the code within the transformer function to modify the tree or extract data.
Define Options (Optional): Create an interface for any configuration the plugin might need.
Type Everything: Use TypeScript and the provided types (
Plugin,Root,Attribute, etc.) for robustness.Test: Thoroughly test your plugin with various Lottie file inputs.
Further Exploration
unifiedDocumentation: Explore the unified documentation for more advanced plugin concepts (though focus on transformer plugins for relottie modifications).unistUtilities: Discover more tree utilities in the unist ecosystem.Existing Plugins: Examine the source code of official plugins like
relottie-metadataorrelottie-extract-featuresfor patterns and inspiration.