#
tokens: 13661/50000 12/13 files (page 1/2)
lines: off (toggle) GitHub
raw markdown copy
This is page 1 of 2. Use http://codebase.md/snehil-shah/libresprite-mcp?page={x} to view the full context.

# Directory Structure

```
├── .github
│   └── workflows
│       └── release.yml
├── .gitignore
├── .python-version
├── ARCHITECTURE.md
├── assets
│   ├── architecture.svg
│   ├── connect.png
│   ├── demo.mp4
│   ├── design
│   │   └── architecture.drawio
│   └── scripts-folder.png
├── LICENSE
├── pyproject.toml
├── README.md
├── remote
│   └── mcp.js
├── src
│   └── libresprite_mcp
│       ├── __init__.py
│       ├── libresprite_proxy.py
│       ├── mcp_server.py
│       └── resources
│           ├── examples.txt
│           └── reference.txt
└── uv.lock
```

# Files

--------------------------------------------------------------------------------
/.python-version:
--------------------------------------------------------------------------------

```
3.11

```

--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------

```
# Python-generated files
__pycache__/
*.py[oc]
build/
dist/
wheels/
*.egg-info

# Virtual environments
.venv

```

--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------

```markdown
# LibreSprite-MCP

[![Install MCP Server](https://cursor.com/deeplink/mcp-install-light.svg)](https://cursor.com/install-mcp?name=libresprite&config=JTdCJTIyY29tbWFuZCUyMiUzQSUyMnV2eCUyMGxpYnJlc3ByaXRlLW1jcCUyMiU3RA%3D%3D)
[![PyPI version](https://img.shields.io/pypi/v/libresprite-mcp)](https://pypi.org/project/libresprite-mcp/)

> Prompt your way into LibreSprite

Model Context Protocol (MCP) server for prompt-assisted editing, designing, and scripting inside LibreSprite.

https://github.com/user-attachments/assets/71440bba-16a5-4ee2-af10-2c346978a290

## Prerequisites

[`uv`](https://docs.astral.sh/uv/) is the recommended way to install and use this server. Here are quick one-liners to install it if you haven't:

- **Windows**: (run as administrator)

    ```powershell
    powershell -ExecutionPolicy ByPass -c "irm https://astral.sh/uv/install.ps1 | iex"
    ```

- **Unix**:

    ```bash
    curl -LsSf https://astral.sh/uv/install.sh | sh
    ```

More on [installing `uv`](https://docs.astral.sh/uv/getting-started/installation/).

The package is published on [PyPI](https://pypi.org/project/librespsrite-mcp/), so feel free to consume it any other way you prefer (`pipx`, etc)

## Usage

### Step 1: Setting up the client

Add the MCP server with the following entrypoint command (or something else if you are not using `uv`) to your MCP client:

```bash
uvx libresprite-mcp
```

#### Examples:

- **Claude Desktop & Cursor**

    Edit _Claude > Settings > Developer > Edit Config > claude_desktop_config.json_ or _.cursor > mcp.json_ to include the server:
    
    ```json
    {
        "mcpServers": {
            // ...existing servers...
            "libresprite": {
                "type": "stdio",
                "command": "uvx",
                "args": [
                    "libresprite-mcp"
                ]
            }
            // ...existing servers...
        }
    }
    ```

    You can also use this fancy badge to make it quick:
  
    [![Install MCP Server](https://cursor.com/deeplink/mcp-install-dark.svg)](https://cursor.com/install-mcp?name=libresprite&config=JTdCJTIyY29tbWFuZCUyMiUzQSUyMnV2eCUyMGxpYnJlc3ByaXRlLW1jcCUyMiU3RA%3D%3D)

> [!NOTE]
> You will have to restart Claude Desktop to load the MCP Server.

### Step 2: Setting up LibreSprite

Download the latest stable remote script `mcp.js` from [releases](https://github.com/Snehil-Shah/libresprite-mcp/releases/latest) and add it to LibreSprite's scripts folder:

![scripts-folder](https://raw.githubusercontent.com/Snehil-Shah/libresprite-mcp/main/assets/scripts-folder.png)

### Step 3: Connect and use

Run the `mcp.js` script (that you see in the screenshot above), and make sure your MCP server is running (Claude Desktop/Cursor is loaded and running). If all went well, you should see the following screen:

![connect-button](https://raw.githubusercontent.com/Snehil-Shah/libresprite-mcp/main/assets/connect.png)

Click the "Connect" button and you can now start talking to Claude about your next big pixel-art project!

## Some pointers

- You can only run one instance of the MCP server at a time.
- The server expects port `64823` to be free.
- The server has a hacky and brittle implementation (see [ARCHITECTURE](https://github.com/Snehil-Shah/libresprite-mcp/blob/main/ARCHITECTURE.md)), and is not extensively tested.
- The MCP resources are kinda low quality with unclear API reference and limited examples, leaving the LLM confused at times. If you're a LibreSprite expert, we need your help.

***

```

--------------------------------------------------------------------------------
/pyproject.toml:
--------------------------------------------------------------------------------

```toml
[project]
name = "libresprite-mcp"
version = "0.1.3"
description = "Prompt your way into LibreSprite"
readme = "README.md"
authors = [
    { name = "Snehil Shah", email = "[email protected]" }
]
requires-python = ">=3.11"
dependencies = [
    "flask>=3.1.1",
    "mcp[cli]>=1.12.2",
]

[project.scripts]
libresprite-mcp = "libresprite_mcp:main"

[build-system]
requires = ["uv_build>=0.8.3,<0.9.0"]
build-backend = "uv_build"

```

--------------------------------------------------------------------------------
/ARCHITECTURE.md:
--------------------------------------------------------------------------------

```markdown
# Architecture

Unlike Aseprite, LibreSprite lacks a command-line interface for doing things. The only thing it provides to programmatically control its interface is a JavaScript scripting interface. These scripts run in a very limited context and the closest thing I could find around networking was the `storage.fetch` method which can make a network request.

To make it work, I wrote a relay server that acts as a convenient proxy for the MCP server to interact with LibreSprite, while a remote script running inside LibreSprite polls the endpoints for scripts to execute.

![architecture](https://raw.githubusercontent.com/Snehil-Shah/libresprite-mcp/main/assets/architecture.svg)
```

--------------------------------------------------------------------------------
/src/libresprite_mcp/__init__.py:
--------------------------------------------------------------------------------

```python
"""
Main entry point for the Libresprite MCP server application.
"""

import click
import logging
from .libresprite_proxy import LibrespriteProxy
from .mcp_server import MCPServer

def main() -> None:
    """Main entry point for the application."""
    # Disable unwanted logging to avoid messing with stdio
    logging.disable(logging.WARN)
    # HACK: https://stackoverflow.com/a/57086684
    def secho(text, file=None, nl=None, err=None, color=None, **styles):
        pass
    def echo(text, file=None, nl=None, err=None, color=None, **styles):
        pass
    click.echo = echo
    click.secho = secho

    # Initialize and start HTTP relay server
    libresprite_proxy = LibrespriteProxy(port=64823)
    libresprite_proxy.start()

    # Initialize and run MCP server (this blocks)
    mcp_server = MCPServer(libresprite_proxy)
    mcp_server.run(transport='stdio')

if __name__ == '__main__':
    main()
```

--------------------------------------------------------------------------------
/src/libresprite_mcp/mcp_server.py:
--------------------------------------------------------------------------------

```python
"""
MCP server implementation that exposes tools for interacting with the libresprite-proxy server.
"""

import os
from mcp.server.fastmcp import FastMCP, Context
from .libresprite_proxy import LibrespriteProxy


class MCPServer:
    """The LibreSprite MCP Server."""

    def __init__(self, libresprite_proxy: LibrespriteProxy, server_name: str = "libresprite"):
        # Cache the libresprite proxy instance
        self._libresprite_proxy = libresprite_proxy

        # Initialize FastMCP
        self.mcp = FastMCP(server_name)

        # Setup MCP tools, prompts, and resources
        self._setup_tools()
        self._setup_resources()
        self._setup_prompts()

    def _setup_tools(self):
        """Setup MCP tools."""

        @self.mcp.tool()
        def run_script(script: str, ctx: Context) -> str:
            """
            Run a JavaScript script inside Libresprite.

            IMPORTANT: Make sure you are well versed with the documentation and examples provided in the resources `docs:reference` and `docs:examples`.

            Args:
                script: The script to execute

            Returns:
                Console output
            """
            return self._libresprite_proxy.run_script(script, ctx)

    def _setup_resources(self):
        """Setup MCP resources."""

        base_dir = os.path.dirname(os.path.abspath(__file__))

        @self.mcp.resource("docs://reference")
        def read_reference() -> str:
            """Read the libresprite command reference documentation."""
            doc_path = os.path.join(base_dir, "resources", "reference.txt")
            try:
                with open(doc_path, "r", encoding="utf-8") as f:
                    return f.read()
            except Exception as e:
                return f"Error reading reference.txt: {e}"

        @self.mcp.resource("docs://examples")
        def read_examples() -> str:
            """Read example scripts using libresprite commands."""
            example_path = os.path.join(base_dir, "resources", "examples.txt")
            try:
                with open(example_path, "r", encoding="utf-8") as f:
                    return f.read()
            except Exception as e:
                return f"Error reading examples.txt: {e}"

    def _setup_prompts(self):
        """Setup MCP prompts."""

        @self.mcp.prompt(title="libresprite")
        def libresprite(prompt: str) -> str:
            """
            Prompt template to use the libresprite tool with proper context conditioning.

            Args:
                prompt: User prompt

            Returns:
                Prompt to process
            """
            return f"""
            Libresprite is a program for creating and editing pixel art and animations using JavaScript.

            Before proceeding, please ensure you are well versed with the documentation and examples provided in the resources `docs:reference` and `docs:examples`.

            You can use the `run_script` tool to execute JavaScript scripts in the context of libresprite.

            Here's what you need to do using the above tools and resources:

            {prompt}
            """

    def run(self, transport: str = 'stdio'):
        """Run the MCP server."""
        self.mcp.run(transport=transport)
```

--------------------------------------------------------------------------------
/src/libresprite_mcp/libresprite_proxy.py:
--------------------------------------------------------------------------------

```python
"""
Proxy server for Libresprite.
"""

import threading
from typing import Optional
from mcp.server.fastmcp import Context
from flask import Flask, jsonify, request

class LibrespriteProxy:
    """
    Proxy server for Libresprite.

    This is a relay server that acts as a bridge between the MCP server and the remote libresprite script.
    From the POV of the MCP server, this fully abstracts libresprite into a convenient IO interface to execute remote scripts.
    """

    def __init__(self, port: int):
        # Stores current script
        self._script: str | None = None

        # Stores output of the script execution
        self._output: str | None = None

        # Stores flag indicating if execution is in progress
        self._lock: bool = False

        # Initialize event handlers
        self._script_event = threading.Event()
        self._output_event = threading.Event()

        # Relay server configuration
        self.port = port
        self.app = Flask(__name__)

        # Initialize server routes
        self._setup_routes()
        self._server_thread: Optional[threading.Thread] = None

    def _setup_routes(self):
        """Setup Flask routes."""

        @self.app.get('/')
        def get_script():
            """Get the current script."""
            if self._script is None:
                self._script_event.wait()
                self._script_event.clear()
            script = self._script
            self._script = None
            return jsonify({"script": script})

        @self.app.post('/')
        def post_output():
            """Post execution output."""
            if not self._lock:
                # ignore random requests
                return jsonify({"status": "ignored"})
            req = request.get_json(force=True, silent=True)
            if req:
                output = req.get('output')
            else:
                return jsonify({"status": "invalid"})
            self._output = output
            self._output_event.set()
            return jsonify({"status": "success"})

        @self.app.get('/ping')
        def ping():
            """Ping endpoint for health checks."""
            return jsonify({"status": "pong"})

    def _run_server(self):
        """Run the Flask server."""
        self.app.run(
            host='localhost',
            port=self.port,
            debug=False,
            use_reloader=False
        )

    def start(self):
        """Start the HTTP server in a background thread."""
        if self._server_thread and self._server_thread.is_alive():
            return

        self._server_thread = threading.Thread(
            target=self._run_server,
            daemon=True
        )
        self._server_thread.start()

    def run_script(self, script: str, ctx: Context) -> str:
        """
        Run a script in the execution context.

        WARNING: This method is synchronous and blocking.

        Args:
            script: The script to execute
        """
        # This proxy only allows one script to be executed at a time
        if self._lock:
            ctx.error("Script execution is already in progress...")
            raise RuntimeError("Script execution is already in progress.")

        # Sending the script
        ctx.info("Sending script to libresprite...")
        self._lock = True
        self._script = script
        self._script_event.set()

        # Waiting for execution
        if not self._output_event.wait(timeout=15):
            ctx.warning("This is taking longer than usual, make sure the user has the Libresprite application with the remote script running?")
        self._output_event.wait()
        self._output_event.clear()

        # Return the output
        ctx.info("Script execution completed, checking the logs...")
        output = self._output
        self._output = None
        self._lock = False
        return output
```

--------------------------------------------------------------------------------
/.github/workflows/release.yml:
--------------------------------------------------------------------------------

```yaml
name: Release

on:
  workflow_dispatch:

jobs:
  pypi:
    name: PyPI
    runs-on: ubuntu-latest
    permissions:
      id-token: write
    steps:
      - name: Checkout repository
        uses: actions/checkout@v4

      - name: Install uv
        uses: astral-sh/setup-uv@v6

      - name: Set up Python
        run: uv python install

      - name: Build package
        run: uv build

      - name: Publish package
        run: uv publish

  github:
    name: GitHub
    runs-on: ubuntu-latest
    needs: [pypi]
    permissions:
      contents: write
    steps:
      - name: Checkout repository
        uses: actions/checkout@v4
        with:
          fetch-depth: 0
          fetch-tags: true

      - name: Get version from pyproject.toml
        id: version
        run: |
          VERSION=$(grep "^version" pyproject.toml | sed 's/version = "\(.*\)"/\1/')
          echo "version=$VERSION" >> $GITHUB_OUTPUT

      - name: Check if version exists
        id: version_check
        run: |
          VERSION=${{ steps.version.outputs.version }}
          if git tag | grep -q "v$VERSION"; then
            echo "Version v$VERSION already exists, skipping release"
            echo "should_release=false" >> $GITHUB_OUTPUT
          else
            echo "New version v$VERSION detected, proceeding with release"
            echo "should_release=true" >> $GITHUB_OUTPUT
          fi

      - name: Generate changelog
        id: changelog
        if: ${{ steps.version_check.outputs.should_release == 'true' }}
        run: |
          LAST_TAG=$(git describe --tags --abbrev=0 2>/dev/null || echo "")

          if [ -z "$LAST_TAG" ]; then
            COMMITS=$(git log --pretty=format:"%s" --no-merges)
          else
            COMMITS=$(git log ${LAST_TAG}..HEAD --pretty=format:"%s" --no-merges)
          fi

          if [ -z "$COMMITS" ] || [ "$COMMITS" = "" ]; then
            echo "No commits since last tag, trying to get commits from previous tag..."
            PREV_TAG=$(git tag --sort=-version:refname | head -2 | tail -1)
            echo "Previous tag: $PREV_TAG"
            if [ -n "$PREV_TAG" ] && [ "$PREV_TAG" != "$LAST_TAG" ]; then
              COMMITS=$(git log ${PREV_TAG}..HEAD --pretty=format:"%s" --no-merges)
              echo "Commits since $PREV_TAG:"
              echo "$COMMITS"
            fi
          fi

          FEATURES=$(echo "$COMMITS" | grep -E "^feat" | sed 's/^/- /' || true)
          FIXES=$(echo "$COMMITS" | grep -E "^fix" | sed 's/^/- /' || true)
          CHORES=$(echo "$COMMITS" | grep -E "^chore" | sed 's/^/- /' || true)
          BUILDS=$(echo "$COMMITS" | grep -E "^build" | sed 's/^/- /' || true)
          DOCS=$(echo "$COMMITS" | grep -E "^docs" | sed 's/^/- /' || true)
          TEST=$(echo "$COMMITS" | grep -E "^test" | sed 's/^/- /' || true)
          OTHER_COMMITS=$(echo "$COMMITS" | grep -v -E "^(feat|fix|chore|build|docs|test)" | sed 's/^/- /' || true)

          CHANGELOG=""
          if [ -n "$FEATURES" ]; then
            CHANGELOG="${CHANGELOG}### Features"$'\n'"$FEATURES"$'\n\n'
          fi
          if [ -n "$FIXES" ]; then
            CHANGELOG="${CHANGELOG}### Bug Fixes"$'\n'"$FIXES"$'\n\n'
          fi
          if [ -n "$DOCS" ]; then
            CHANGELOG="${CHANGELOG}### Documentation"$'\n'"$DOCS"$'\n\n'
          fi
          if [ -n "$TEST" ]; then
            CHANGELOG="${CHANGELOG}### Tests"$'\n'"$TEST"$'\n\n'
          fi
          if [ -n "$CHORES" ] || [ -n "$BUILDS" ]; then
            CHANGELOG="${CHANGELOG}### Maintenance"$'\n'
            [ -n "$CHORES" ] && CHANGELOG="${CHANGELOG}$CHORES"$'\n'
            [ -n "$BUILDS" ] && CHANGELOG="${CHANGELOG}$BUILDS"$'\n'
          fi
          if [ -n "$OTHER_COMMITS" ]; then
            CHANGELOG="${CHANGELOG}### Other Changes"$'\n'"$OTHER_COMMITS"$'\n'
          fi

          {
            echo "changelog<<EOF"
            printf '%s' "$CHANGELOG"
            echo "EOF"
          } >> $GITHUB_OUTPUT

      - name: Create GitHub Release
        uses: softprops/action-gh-release@v1
        if: ${{ steps.version_check.outputs.should_release == 'true' }}
        with:
          tag_name: v${{ steps.version.outputs.version }}
          name: v${{ steps.version.outputs.version }}
          body: |
            Release of `libresprite-mcp` version ${{ steps.version.outputs.version }}.

            Find the official MCP distribution from [PyPI](https://pypi.org/project/libresprite-mcp/) and the remote script `mcp.js` attached to this release.

            For the installation instructions, refer to the project [README](https://github.com/Snehil-Shah/libresprite-mcp/blob/main/README.md).

            ## Changes
            ${{ steps.changelog.outputs.changelog }}
          files: remote/mcp.js
```

--------------------------------------------------------------------------------
/src/libresprite_mcp/resources/examples.txt:
--------------------------------------------------------------------------------

```
# Examples

## Random

```javascript
const col = app.pixelColor;
const img = app.activeImage;
const h = img.height;
const w = img.width;

for (var y = 0; y < h; ++y) {
    for (var x = 0; x < w; ++x) {
        const c = Math.random() * 256 >>> 0;
        img.putPixel(x, y, col.rgba(c, c, c, 255))
    }
}
```

## White to Alpha

```javascript
var col = app.pixelColor
var img = app.activeImage

for (y=0; y<img.height; ++y) {
  for (x=0; x<img.width; ++x) {
    var c = img.getPixel(x, y)
    var v = (col.rgbaR(c)+
             col.rgbaG(c)+
             col.rgbaB(c))/3

    img.putPixel(x, y,
                 col.rgba(col.rgbaR(c),
                          col.rgbaG(c),
                          col.rgbaB(c),
                          255-v))
  }
}
```

## PerLineOscillation

```javascript
var dialog;

function wrap(x, n) {
    if (x < 0)
	return ((x % n + n) % n) | 0;
    return ((x + n) % n) | 0;
}

const effects = [
    // horizontal oscillation
    function(src, angle, width, height, scale) {
	const out = new Uint8Array(src.length);
	for (var y = 0; y < height; ++y) {
            var ox = Math.sin(y / scale + angle * Math.PI * 2) * scale | 0;
            for (var x = 0; x < width; ++x) {
		var oi = (y * width + x) * 4;
		var ii = (y * width + wrap(x + ox, width)) * 4;
		out[oi++] = src[ii++];
		out[oi++] = src[ii++];
		out[oi++] = src[ii++];
		out[oi++] = src[ii++];
            }
	}
	return out;
    },

    // same as above, but with anti-aliasing
    function(src, angle, width, height, scale) {
	const out = new Uint8Array(src.length);
	for (var y = 0; y < height; ++y) {
            var ox = Math.sin(y / scale + angle * Math.PI * 2) * scale;
	    var ox0 = ox | 0;
	    var ox1 = ox0 + 1;
	    var a = ox - ox0;
            for (var x = 0; x < width; ++x) {
		var oi = (y * width + x) * 4;
		var ii0 = (y * width + wrap(x + ox0, width)) * 4;
		var ii1 = (y * width + wrap(x + ox1, width)) * 4;

		out[oi++] = src[ii0++] * (1 - a) + src[ii1++] * a;
		out[oi++] = src[ii0++] * (1 - a) + src[ii1++] * a;
		out[oi++] = src[ii0++] * (1 - a) + src[ii1++] * a;
		out[oi++] = src[ii0++] * (1 - a) + src[ii1++] * a;
            }
	}
	return out;
    },

    // horizontal oscillation, RGB desync'd
    function(src, angle, width, height, scale) {
	const out = new Uint8Array(src.length);
	for (var y = 0; y < height; ++y) {
            for (var x = 0; x < width; ++x) {
		var d = 0;
		var oi = (y * width + x) * 4;
		var ox = Math.sin(y / scale + (angle + d) * Math.PI * 2) * scale | 0;
		var ii = (y * width + wrap(x + ox, width)) * 4;
		out[oi++] = src[ii++];

		d += 0.03;

		ox = Math.sin(y / scale + (angle + d) * Math.PI * 2) * scale | 0;
		ii = (y * width + wrap(x + ox, width)) * 4 + 1;
		out[oi++] = src[ii++];

		ox = Math.sin(y / scale + (angle + d) * Math.PI * 2) * scale | 0;
		ii = (y * width + wrap(x + ox, width)) * 4 + 2;
		out[oi++] = src[ii++];

		ox = Math.sin(y / scale + (angle + d) * Math.PI * 2) * scale | 0;
		ii = (y * width + wrap(x + ox, width)) * 4 + 3;
		out[oi++] = src[ii++];
            }
	}
	return out;
    },

    // vertical oscillation
    function(src, angle, width, height, scale) {
	const out = new Uint8Array(src.length);
	for (var y = 0; y < height; ++y) {
            var oy = Math.sin(y / scale + angle * Math.PI * 2) * scale | 0;
            for (var x = 0; x < width; ++x) {
		var oi = (y * width + x) * 4;
		var ii = (wrap(y + oy, height) * width + x) * 4;
		out[oi++] = src[ii++];
		out[oi++] = src[ii++];
		out[oi++] = src[ii++];
		out[oi++] = src[ii++];
            }
	}
	return out;
    },

    // same as above, but with anti-aliasing
    function(src, angle, width, height, scale) {
	const out = new Uint8Array(src.length);
	for (var y = 0; y < height; ++y) {
            var oy = y + Math.sin(y / scale + angle * Math.PI * 2) * scale;
	    var oy0 = wrap(oy | 0, height) * width;
	    var ny = (y + 1) + Math.sin((y + 1) / scale + angle * Math.PI * 2) * scale;
	    var oy1 = wrap(ny | 0, height) * width;
	    var a = oy - Math.floor(oy);
            for (var x = 0; x < width; ++x) {
		var oi = (y * width + x) * 4;
		var ii0 = (oy0 + x) * 4;
		var ii1 = (oy1 + x) * 4;

		out[oi++] = src[ii0++] * (1 - a) + src[ii1++] * a;
		out[oi++] = src[ii0++] * (1 - a) + src[ii1++] * a;
		out[oi++] = src[ii0++] * (1 - a) + src[ii1++] * a;
		out[oi++] = src[ii0++] * (1 - a) + src[ii1++] * a;
            }
	}
	return out;
    }
];

const eventHandlers = {
    init:function(){
        if (!app.activeSprite) {
            app.createDialog('Error').addLabel('Need an image to oscillate.');
            return;
        }

        if (dialog)
            dialog.close();
        dialog = app.createDialog('dialog');
        dialog.addIntEntry("Frames to animate:", "frameCount", 0, 100);
        dialog.addBreak();
        dialog.addIntEntry("Wave size:", "waveSize", 3, 100);
        dialog.addBreak();
        dialog.addIntEntry("Effect:", "effect", 0, effects.length - 1);
        dialog.addBreak();
        dialog.addButton("Run", "run");
    },

    run_click:function(){
        dialog.close();
        dialog = null;

	app.command.setParameter("format", "rgb");
	app.command.ChangePixelFormat();
	app.command.clearParameters();

        const frameCount = storage.get("frameCount")|0;
        const waveSize = storage.get("waveSize")|0;
	const effect = storage.get("effect")|0;
	const oscillate = effects[effect] || effects[0];
        const sprite = app.activeSprite;
        const layerNumber = app.activeLayerNumber;
        const reference = sprite.layer(layerNumber).cel(0).image;
        const refWidth = reference.width;
        const refHeight = reference.height;
        const src = reference.getImageData();

        app.command.setParameter("content", "current");
        for (var i = 0; i < frameCount; ++i) {
            if (i)
                app.command.NewFrame();
            sprite.layer(layerNumber).cel(i).image.putImageData(oscillate(src, i / frameCount, refWidth, refHeight, waveSize));
        }
    }
};

function onEvent(eventName) {
    var handler = eventHandlers[eventName];
    if (typeof handler == 'function')
        handler();
}
```
```

--------------------------------------------------------------------------------
/remote/mcp.js:
--------------------------------------------------------------------------------

```javascript
/**
 * Source: https://github.com/Snehil-Shah/libresprite-mcp
 */

// Cache the global context.
const global = this;

/**
 * Model Context Protocol (MCP) remote script that interacts with the libresprite-mcp server.
 *
 * NOTE: Defined as an IIFE to avoid global namespace pollution.
 */
(function MCP() {
    // CONSTANTS //

    /**
     * URL to the relay server exposing the next command.
     */
    const RELAY_SERVER_URL = 'http://localhost:64823';

    /**
     * Delay between polling requests. (in number of rendering cycles)
     */
    const POLL_DELAY = 120;


    // VARIABLES //

    /**
     * Flag indicating extension state.
     *
     * @type {boolean}
     */
    let active = false;

    /**
     * Flag indicating whether polling is active.
     *
     * @type {boolean}
     */
    let polling = false;

    /**
     * Flag indicating whether the client is connected to the server.
     *
     * @type {boolean}
     */
    let connected = false;

    /**
     * Stores stdout.
     *
     * @type {string}
     */
    let output = '';

    /**
     * Function to get response from storage in the next cycle.
     *
     * @type {Function|null}
     */
    let _get_response = null;

    /**
     * Function to post response to storage in the next cycle.
     *
     * @type {Function|null}
     */
    let _post_response = null;

    /**
     * Dialog instance for UI.
     */
    let dialog = null;


    // FUNCTIONS //

    /**
     * Global `console.log`.
     */
    const _clientLogger = global.console.log;

    // Override global console object to capture stdout.
    const console = Object.assign({}, global.console);

    /**
     * Modified `console.log` that captures output before logging.
     */
    console.log = function() {
        var args = Array.prototype.slice.call(arguments);
        output += args.join(' ') + '\n';
        _clientLogger.apply(null, args);
    }

    /**
     * Makes a GET request.
     *
     * @private
     * @param {string} url - url to fetch
     * @param {Function} cb - callback function to handle the response
     */
    function _get(url, cb) {
        storage.fetch(url, '_get_response');
        _get_response = function() {
            const status = storage.get('_get_response' + '_status');
            const string = storage.get('_get_response');
            cb({
                string,
                status
            });
        };
    }

    /**
     * Makes a POST request.
     *
     * @private
     * @param {string} url - url to fetch
     * @param {string} body - request body
     * @param {Function} cb - callback function to handle the response
     */
    function _post(url, body, cb) {
        storage.fetch(url, '_post_response', "", "POST", body, "Content-Type", "application/json");
        _post_response = function() {
            const status = storage.get('_post_response' + '_status');
            const string = storage.get('_post_response');
            cb({
                string,
                status
            });
        }
    }

    /**
     * Makes a GET request.
     *
     * @param {string} url - url to fetch
     * @param {Function} cb - callback function to handle the response
     */
    function get(url, cb) {
        _get(url, function(rsp) {
            var data, error = rsp.status != 200 ? 'status:' + rsp.status : 0;
            try {
                if (!error)
                    data = JSON.parse(rsp.string);
            } catch (ex) {
                error = ex;
            }
            cb(data, error);
        });
    }

    /**
     * Makes a POST request.
     *
     * @param {string} url - url to fetch
     * @param {Object} body - request body
     * @param {Function} cb - callback function to handle the response
     */
    function post(url, body, cb) {
        _post(url, body, function(rsp) {
            var data, error = rsp.status != 200 ? 'status:' + rsp.status : 0;
            try {
                if (!error)
                    data = JSON.parse(rsp.string);
                else error += rsp.string;
            } catch (ex) {
                error = ex;
            }
            cb(data, error);
        });
    }

    /**
     * Pings for server health.
     *
     * This yields a "bad_health" event if the server is unreachable,
     * or an "init" event if the server is reachable.
     */
    function checkServerHealth() {
        get(RELAY_SERVER_URL + '/ping', function(data, error) {
            if (error) {
                connected = false;
                app.yield("bad_health", POLL_DELAY);
                return;
            }
            if (data && data.status === 'pong') {
                connected = true;
                app.yield("good_health");
            } else {
                connected = false;
                app.yield("bad_health", POLL_DELAY);
            }

        });
    }

    /**
     * Fetches the next script from the server.
     *
     * @param {Function} cb - script handler
     */
    function getScript(cb) {
        get(RELAY_SERVER_URL, function(data, error) {
            if (error) {
                // The post request will log the error.
                cb('');
                return;
            }
            cb((data && data.script) ? data.script : '');
        });
    }

    /**
     * Posts the output to the server.
     */
    function postOutput() {
        const body = JSON.stringify({output: output});
        post(RELAY_SERVER_URL, body, function(data, error) {
            // NOTE: This is the last interaction with the MCP server for a tool call and hence we ensure updates to the UI and connection status.
            if (error) {
                _clientLogger('The MCP server was shut down.');
                connected = false;
                paintUI();
                app.yield("bad_health", POLL_DELAY);
                return;
            }
            if ( !data ) {
                _clientLogger('Something went wrong. Please report it on https://github.com/Snehil-Shah/libresprite-mcp/issues.');
                return;
            }
            if ( data.status === 'invalid' )
                _clientLogger('Something is wrong. Please report it on https://github.com/Snehil-Shah/libresprite-mcp/issues.');
            // Other status types can be ignored...
            // Continue polling...
            if (!polling) {
                return;
            }
            app.yield("poll", POLL_DELAY);
        });
    }

    /**
     * Runs a script in the current context.
     *
     * @param {string} script - script to run
     */
    function runScript(script) {
        if (!script) {
            return;
        }
        try {
            // Execute in global scope with our custom logger...
            new Function('console', script)(console);
        } catch (e) {
            console.log('Error in script:', e.message);
        }
    }

    /**
     * Fetches, executes, and posts the output for the next script.
     *
     * NOTE: This is the entry point for the polling loop.
     */
    function exec() {
        if (!polling) return;
        getScript(script => {
            output = ''; // sanity reset
            runScript(script);
            postOutput();
        });
    }

    /**
     * Starts the polling loop.
     */
    function startPolling() {
        if (polling) return;
        polling = true;
        exec();
    }

    /**
     * Stops the polling loop.
     */
    function stopPolling() {
        polling = false;
    }

    /**
     * Paints the UI dialog based on the current state.
     */
    function paintUI() {
        let label;
        if (!connected) {
            label = 'Discovering MCP servers... Make sure the libresprite-mcp server is running.';
        } else if (polling) {
            label = 'Connected to the libresprite-mcp server!';
        } else {
            label = 'Found an active libresprite-mcp server, "Connect" when you are ready!';
        }
        if (dialog) {
            dialog.close();
        }
        dialog = app.createDialog();
        dialog.title = 'libresprite-mcp';
        dialog.addLabel(label);
        dialog.addBreak();
        dialog.canClose = !connected || !polling;
        if ( connected ) {
            dialog.addButton(
                polling ? 'Disconnect': 'Connect',
                'toggle'
            );
        }
    }


    // MAIN //

    /**
     * Event handler.
     *
     * @global
     * @param {string} event
     */
    function onEvent(event) {
        switch (event) {
            /**
             * Initialize script.
             */
            case 'init':
                active = true;
                checkServerHealth();
                paintUI();
                return;
            /**
             * Cleanup script.
             */
            case '_close':
                active = false;
                connected = false;
                polling = false;
                return;
            /**
             * Events triggered by initial health checks.
             */
            case 'bad_health':
                if (!active) {
                    // The extension was closed, stop recursion...
                    return;
                }
                checkServerHealth();
                return;
            case 'good_health':
                paintUI();
                return;
            /**
             * UI operation.
             */
            case 'toggle_click':
                if (polling) {
                    stopPolling();
                } else {
                    startPolling();
                }
                paintUI();
                return;
            /**
             * Successful 'GET' event response triggered by `storage.fetch`.
             */
            case '_get_response_fetch':
                _get_response && _get_response();
                _get_response = null;
                return;
            /**
             * Successful 'POST' event response triggered by `storage.fetch`.
             */
            case '_post_response_fetch':
                _post_response && _post_response();
                _post_response = null;
                return;
            /**
             * Event triggered to continue polling the endpoint.
             */
            case 'poll':
                if (!active) {
                    // The extension was closed, stop recursion...
                    // NOTE: This is a sanity check and should never be executed given the close button is not visible during polling
                    stopPolling();
                    return;
                }
                exec();
                return;
            default:
                // No action for unknown events
                break;
        }
    }
    global.onEvent = onEvent;
})();
```

--------------------------------------------------------------------------------
/src/libresprite_mcp/resources/reference.txt:
--------------------------------------------------------------------------------

```
# [class Sprite]
## Properties:
   - `palette`: read-only. Returns the sprite's palette.
   - `selection`: Placeholder. Do not use.
   - `height`: read+write. Returns and sets the height of the sprite.
   - `width`: read+write. Returns and sets the width of the sprite.
   - `filename`: read-only. Returns the file name of the sprite.
   - `colorMode`: read-only. Returns the sprite's ColorMode.
   - `layerCount`: read-only. Returns the amount of layers in the sprite.

## Methods:
   - `loadPalette(fileName)`:
     - fileName: The name of the palette file to load
      returns: Nothing
      loads a palette file.

   - `crop(x, y, width, height)`:
     - x: The left-most edge of the crop.
     - y: The top-most edge of the crop.
     - width: The width of the cropped area.
     - height: The height of the cropped area.
      returns: Nothing
      crops the sprite to the specified dimensions.

   - `saveAs(fileName, asCopy)`:
     - fileName: String. The new name of the file
     - asCopy: If true, the file is saved as a copy. Requires fileName to be specified.
      returns: Nothing
      saves the sprite.

   - `resize(width, height)`:
     - width: The new width.
     - height: The new height.
      returns: Nothing
      resizes the sprite.

   - `save()`:
      returns: Nothing
      saves the sprite.

   - `commit()`:
      returns: Nothing
      commits the current transaction.

   - `layer(layerNumber)`:
     - layerNumber: The number of they layer, starting with zero from the bottom.
      returns: a Layer object or null if invalid.
      allows you to access a given layer.



# global storage [class Storage]
## No Properties.

## Methods:
   - `decodeBase64()`:
      returns: Nothing

   - `get()`:
      returns: Nothing

   - `save()`:
      returns: Nothing

   - `set()`:
      returns: Nothing

   - `fetch()`:
      returns: Nothing

   - `load()`:
      returns: Nothing

   - `unload()`:
      returns: Nothing



# [class PalettelistboxWidget]
## Properties:
   - `selected`:
   - `id`:

## Methods:
   - `addPalette()`:
      returns: Nothing



# [class pixelColor]
## No Properties.

## Methods:
   - `grayaA(color)`:
     - color: A 32-bit color in 888 RGBA format
      returns: The alpha component of the color
      Extracts the alpha (opacity) from a 32-bit color

   - `grayaV(color)`:
     - color: A 32-bit color in 888 RGBA format
      returns: The luminance Value of the color
      Extracts the luminance from a 32-bit color

   - `rgbaA(color)`:
     - color: A 32-bit color in 8888 RGBA format
      returns: The alpha component of the color
      Extracts the alpha channel from a 32-bit color

   - `graya(gray, alpha)`:
     - gray: The luminance of color
     - alpha: The alpha (opacity) of the color)
      returns: The color with the given luminance/opacity

   - `rgba(r, g, b, a)`:
     - r: red, 0-255.
     - g: green, 0-255.
     - b: blue, 0-255.
     - a: alpha (opacity), 0-255.
      returns: A 32-bit color in 8888 RGBA format.
      Converts R, G, B, A values into a single 32-bit RGBA color.

   - `rgbaG(color)`:
     - color: A 32-bit color in 8888 RGBA format
      returns: The green component of the color
      Extracts the green channel from a 32-bit color

   - `rgbaB(color)`:
     - color: A 32-bit color in 8888 RGBA format
      returns: The blue component of the color
      Extracts the blue channel from a 32-bit color

   - `rgbaR(color)`:
     - color: A 32-bit color in 8888 RGBA format
      returns: The red component of the color
      Extracts the red channel from a 32-bit color



# [class LabelWidget]
## Properties:
   - `text`:
   - `id`:

## No Methods.


# [class Layer]
## Properties:
   - `flags`: read-only. Returns all flags OR'd together as an int
   - `isContinuous`: read-only. Prefer to link cels when the user copies them.
   - `celCount`: read-only. Returns the number of cels.
   - `isMovable`: read-only. Returns true if the layer is movable.
   - `isVisible`: read+write. Gets/sets whether the layer is visible or not.
   - `isTransparent`: read-only. Returns true if the layer is a non-background image layer.
   - `isBackground`: read-only. Returns true if the layer is a background layer.
   - `isImage`: read-only. Returns true if the layer is an image, false if it is a folder.
   - `isEditable`: read+write. Gets/sets whether the layer is editable (unlocked) or not (locked).
   - `name`: read+write. The name of the layer.

## Methods:
   - `cel(index)`:
     - index: The number of the Cel
      returns: A Cel object or null if an invalid index is passed
      retrieves a Cel



# [class IntentryWidget]
## Properties:
   - `value`:
   - `min`:
   - `max`:
   - `id`:

## No Methods.


# [class Image]
## Properties:
   - `format`: read-only. The PixelFormat of the image.
   - `stride`: read-only. The number of bytes per image row.
   - `height`: read-only. The height of the image.
   - `width`: read-only. The width of the image.

## Methods:
   - `putPixel(x, y, color)`:
     - x: integer
     - y: integer
     - color: a 32-bit color in 8888 RGBA format.
      returns: Nothing
      writes the color onto the image at the the given coordinate.

   - `getImageData()`:
      returns: All pixels in a Uint8Array
      creates an array containing all of the image's pixels.

   - `putImageData(data)`:
     - data: All of the pixels in the image.
      returns: Nothing
      writes the given pixels onto the image. Must be the same size as the image.

   - `getPNGData()`:
      returns: The image as a Base64-encoded PNG string.
      Encodes the image as a PNG.

   - `clear(color)`:
     - color: a 32-bit color in 8888 RGBA format.
      returns: Nothing
      clears the image with the specified color.

   - `getPixel(x, y)`:
     - x: integer
     - y: integer
      returns: a color value
      reads a color from the given coordinate of the image.



# [class PN2ui6DialogE]
## Properties:
   - `canClose`: write only. Determines if the user can close the dialog window.
   - `title`: read+write. Sets the title of the dialog window.
   - `id`:

## Methods:
   - `addBreak()`:
      returns: Nothing

   - `addPaletteListBox()`:
      returns: Nothing

   - `addEntry()`:
      returns: Nothing

   - `addButton()`:
      returns: Nothing

   - `add()`:
      returns: Nothing

   - `close()`:
      returns: Nothing

   - `addLabel()`:
      returns: Nothing

   - `addDropDown()`:
      returns: Nothing

   - `addIntEntry()`:
      returns: Nothing

   - `get()`:
      returns: Nothing



# global console [class Console]
## No Properties.

## Methods:
   - `assert()`:
      returns: Nothing

   - `log()`:
      returns: Nothing



# [class ButtonWidget]
## Properties:
   - `text`:
   - `id`:

## No Methods.


# [class command]
## No Properties.

## Methods:
   - `Zoom()`:
      returns: Nothing
      Zoom in

   - `ToggleFullscreen()`:
      returns: Nothing
      Toggle Fullscreen

   - `Timeline()`:
      returns: Nothing
      Switch Timeline

   - `TiledMode()`:
      returns: Nothing
      Tiled Mode

   - `SymmetryMode()`:
      returns: Nothing
      Symmetry Mode

   - `SpriteSize()`:
      returns: Nothing
      Sprite Size

   - `SnapToGrid()`:
      returns: Nothing
      Snap to Grid

   - `ShowLayerEdges()`:
      returns: Nothing
      Show Layer Edges

   - `TogglePreview()`:
      returns: Nothing
      Toggle Preview

   - `ShowGrid()`:
      returns: Nothing
      Show Grid

   - `ShowExtras()`:
      returns: Nothing
      Show Extras

   - `ShowBrushPreview()`:
      returns: Nothing
      Show Brush Preview

   - `Share()`:
      returns: Nothing
      Share

   - `SetSameInk()`:
      returns: Nothing
      Same Ink in All Tools

   - `SetInkType()`:
      returns: Nothing
      Set Ink Type: Simple Ink

   - `SetColorSelector()`:
      returns: Nothing
      Set Color Selector: Color Spectrum

   - `SelectionAsGrid()`:
      returns: Nothing
      Selection as Grid

   - `SelectTile()`:
      returns: Nothing
      Select Tile

   - `Scroll()`:
      returns: Nothing
      Scroll 0 pixels left

   - `SavePalette()`:
      returns: Nothing
      Save Palette

   - `SaveFileCopyAs()`:
      returns: Nothing
      Save File Copy As

   - `SaveFileAs()`:
      returns: Nothing
      Save File As

   - `SaveFile()`:
      returns: Nothing
      Save File

   - `SetLoopSection()`:
      returns: Nothing
      Set Loop Section

   - `RunScript()`:
      returns: Nothing
      Run Script

   - `ReverseFrames()`:
      returns: Nothing
      Reverse Frames

   - `ReselectMask()`:
      returns: Nothing
      Reselect Mask

   - `RescanScripts()`:
      returns: Nothing
      Rescan Scripts

   - `ReplaceColor()`:
      returns: Nothing
      Replace Color

   - `RepeatLastExport()`:
      returns: Nothing
      Repeat Last Export

   - `PlayAnimation()`:
      returns: Nothing
      Play Animation

   - `PixelPerfectMode()`:
      returns: Nothing
      Switch Pixel Perfect Mode

   - `Options()`:
      returns: Nothing
      Options

   - `OpenFile()`:
      returns: Nothing
      Open Sprite

   - `NewSpriteFromSelection()`:
      returns: Nothing
      New Sprite From Selection

   - `MaskByColor()`:
      returns: Nothing
      Mask By Color

   - `NewFrameTag()`:
      returns: Nothing
      New Frame Tag

   - `NewFile()`:
      returns: Nothing
      New File

   - `UndoHistory()`:
      returns: Nothing
      Undo History

   - `GotoNextLayer()`:
      returns: Nothing
      Go to Next Layer

   - `NewBrush()`:
      returns: Nothing
      New Brush

   - `OpenInFolder()`:
      returns: Nothing
      Open In Folder

   - `ClearCel()`:
      returns: Nothing
      Clear Cel

   - `MoveMask()`:
      returns: Nothing
      Move Selection Boundaries 0 pixels left

   - `FrameTagProperties()`:
      returns: Nothing
      Frame Tag Properties

   - `AddColor()`:
      returns: Nothing
      Add Foreground Color to Palette

   - `MoveCel()`:
      returns: Nothing
      Move Cel

   - `MergeDownLayer()`:
      returns: Nothing
      Merge Down Layer

   - `MaskAll()`:
      returns: Nothing
      Mask All

   - `SaveMask()`:
      returns: Nothing
      Save Mask

   - `LoadMask()`:
      returns: Nothing
      LoadMask

   - `LayerProperties()`:
      returns: Nothing
      Layer Properties

   - `LayerFromBackground()`:
      returns: Nothing
      Layer From Background

   - `SetPaletteEntrySize()`:
      returns: Nothing
      Set Palette Entry Size

   - `Launch()`:
      returns: Nothing
      Launch

   - `KeyboardShortcuts()`:
      returns: Nothing
      Keyboard Shortcuts

   - `InvertMask()`:
      returns: Nothing
      Invert Mask

   - `InvertColor()`:
      returns: Nothing
      Invert Color

   - `GridSettings()`:
      returns: Nothing
      Grid Settings

   - `GotoPreviousTab()`:
      returns: Nothing
      Go to Previous tab

   - `ScrollCenter()`:
      returns: Nothing
      Scroll to center of canvas

   - `GotoPreviousLayer()`:
      returns: Nothing
      Go to Previous Layer

   - `GotoPreviousFrameWithSameTag()`:
      returns: Nothing
      Go to Previous Frame with same tag

   - `GotoPreviousFrame()`:
      returns: Nothing
      Go to Previous Frame

   - `GotoNextTab()`:
      returns: Nothing
      Go to Next Tab

   - `AutocropSprite()`:
      returns: Nothing
      Trim Sprite

   - `ImportSpriteSheet()`:
      returns: Nothing
      Import Sprite Sheet

   - `ShowPixelGrid()`:
      returns: Nothing
      Show Pixel Grid

   - `Home()`:
      returns: Nothing
      Home

   - `UnlinkCel()`:
      returns: Nothing
      Unlink Cel

   - `GotoNextFrameWithSameTag()`:
      returns: Nothing
      Go to Next Frame with same tag

   - `CropSprite()`:
      returns: Nothing
      Crop Sprite

   - `GotoLastFrame()`:
      returns: Nothing
      Go to Last Frame

   - `OpenWithApp()`:
      returns: Nothing
      Open With Associated Application

   - `GotoFirstFrame()`:
      returns: Nothing
      Go to First Frame

   - `RemoveFrameTag()`:
      returns: Nothing
      Remove Frame Tag

   - `NewFrame()`:
      returns: Nothing
      New Frame

   - `FullscreenPreview()`:
      returns: Nothing
      Fullscreen Preview

   - `SpriteProperties()`:
      returns: Nothing
      Sprite Properties

   - `NewLayer()`:
      returns: Nothing
      New Layer

   - `FrameProperties()`:
      returns: Nothing
      Frame Properties

   - `DeselectMask()`:
      returns: Nothing
      Deselect Mask

   - `AlternateTouchbar()`:
      returns: Nothing
      Alternate Touchbar

   - `ExportSpriteSheet()`:
      returns: Nothing
      Export Sprite Sheet

   - `NewLayerSet()`:
      returns: Nothing
      New Layer Set

   - `ModifySelection()`:
      returns: Nothing
      Expand Selection

   - `Paste()`:
      returns: Nothing
      Paste

   - `DiscardBrush()`:
      returns: Nothing
      Discard Brush

   - `BackgroundFromLayer()`:
      returns: Nothing
      BackgroundFromLayer

   - `DuplicateView()`:
      returns: Nothing
      Duplicate View

   - `About()`:
      returns: Nothing
      About

   - `DeveloperConsole()`:
      returns: Nothing
      Developer Console

   - `DuplicateSprite()`:
      returns: Nothing
      Duplicate Sprite

   - `LinkCels()`:
      returns: Nothing
      Links Cels

   - `CopyMerged()`:
      returns: Nothing
      Copy Merged

   - `MaskContent()`:
      returns: Nothing
      Mask Content

   - `DuplicateLayer()`:
      returns: Nothing
      Duplicate Layer

   - `CopyCel()`:
      returns: Nothing
      Copy Cel

   - `Refresh()`:
      returns: Nothing
      Refresh

   - `Copy()`:
      returns: Nothing
      Copy

   - `RemoveFrame()`:
      returns: Nothing
      Remove Frame

   - `SetPalette()`:
      returns: Nothing
      Set Palette

   - `OpenScriptsFolder()`:
      returns: Nothing
      Open Scripts Folder

   - `FlattenLayers()`:
      returns: Nothing
      Flatten Layers

   - `Eyedropper()`:
      returns: Nothing
      Eyedropper

   - `PaletteSize()`:
      returns: Nothing
      Palette Size

   - `ConvolutionMatrix()`:
      returns: Nothing
      Convolution Matrix

   - `clearParameters()`:
      returns: Nothing

   - `Cut()`:
      returns: Nothing
      Cut

   - `PaletteEditor()`:
      returns: Nothing
      Palette Editor

   - `RemoveLayer()`:
      returns: Nothing
      Remove Layer

   - `Clear()`:
      returns: Nothing
      Clear

   - `Exit()`:
      returns: Nothing
      Exit

   - `ColorQuantization()`:
      returns: Nothing
      Create Palette from Current Sprite (Color Quantization)

   - `AlternateToolbar()`:
      returns: Nothing
      Alternate Toolbar

   - `ChangeColor()`:
      returns: Nothing
      Color

   - `ChangeBrush()`:
      returns: Nothing
      Brush

   - `Cancel()`:
      returns: Nothing
      Cancel Current Operation

   - `SwitchColors()`:
      returns: Nothing
      Switch Colors

   - `ShowOnionSkin()`:
      returns: Nothing
      Show Onion Skin

   - `ChangePixelFormat()`:
      returns: Nothing
      Change Pixel Format

   - `ColorCurve()`:
      returns: Nothing
      Color Curve

   - `PasteText()`:
      returns: Nothing
      Insert Text

   - `CelProperties()`:
      returns: Nothing
      Cel Properties

   - `Despeckle()`:
      returns: Nothing
      Despeckle

   - `CloseAllFiles()`:
      returns: Nothing
      Close All Files

   - `LoadPalette()`:
      returns: Nothing
      Load Palette

   - `CanvasSize()`:
      returns: Nothing
      Canvas Size

   - `Undo()`:
      returns: Nothing
      Undo

   - `LayerVisibility()`:
      returns: Nothing
      Layer Visibility

   - `Flip()`:
      returns: Nothing
      Flip Canvas Horizontal

   - `Rotate()`:
      returns: Nothing
      Rotate Sprite 0°

   - `Redo()`:
      returns: Nothing
      Redo

   - `AlternateTimeline()`:
      returns: Nothing
      Alternate Timeline

   - `ShowSelectionEdges()`:
      returns: Nothing
      Show Selection Edges

   - `GotoFrame()`:
      returns: Nothing
      Go to Frame

   - `CloseFile()`:
      returns: Nothing
      Close File

   - `ToggleTouchbar()`:
      returns: Nothing
      Toggle Touchbar

   - `GotoNextFrame()`:
      returns: Nothing
      Go to Next Frame

   - `AdvancedMode()`:
      returns: Nothing
      Advanced Mode

   - `setParameter()`:
      returns: Nothing



# [class EntryWidget]
## Properties:
   - `value`:
   - `maxsize`:
   - `id`:

## No Methods.


# [class Palette]
## Properties:
   - `length`:

## Methods:
   - `set()`:
      returns: Nothing

   - `get()`:
      returns: Nothing



# [class Document]
## Properties:
   - `sprite`:

## Methods:
   - `close()`:
      returns: Nothing



# global ColorMode [class ColorMode]
## Properties:
   - `BITMAP`:
   - `INDEXED`:
   - `GRAYSCALE`:
   - `RGB`:

## No Methods.


# [class Cel]
## Properties:
   - `frame`:
   - `image`:
   - `y`:
   - `x`:

## Methods:
   - `setPosition()`:
      returns: Nothing



# global app [class App]
## Properties:
   - `platform`: read-only. Returns one of: emscripten, windows, macos, android, linux.
   - `version`: read-only. Returns LibreSprite's current version as a string.
   - `activeDocument`: read-only. Returns the currently active Document.
   - `command`: read-only. Returns an object with functions for running commands.
   - `activeSprite`: read-only. Returns the currently active Sprite.
   - `activeLayerNumber`: read-only. Returns the number of the current layer.
   - `activeImage`: read-only, can be null. Returns the current layer/frame's image.
   - `pixelColor`: read-only. Returns an object with functions for color conversion.
   - `activeFrameNumber`: read-only. Returns the number of the currently active animation frame.

## Methods:
   - `launch()`:
      returns: Nothing

   - `open()`:
      returns: Nothing
      Opens a document for editing

   - `yield(event)`:
     - event: Name of the event to be raised. The default is yield.
      returns: Nothing
      Schedules a yield event on the next frame

   - `createDialog()`:
      returns: Nothing
      Creates a dialog window

   - `documentation()`:
      returns: Nothing
      Prints this text.
```
Page 1/2FirstPrevNextLast