#
tokens: 18814/50000 12/13 files (page 1/2)
lines: on (toggle) GitHub
raw markdown copy reset
This is page 1 of 2. Use http://codebase.md/snehil-shah/libresprite-mcp?lines=true&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:
--------------------------------------------------------------------------------

```
1 | 3.11
2 | 
```

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

```
 1 | # Python-generated files
 2 | __pycache__/
 3 | *.py[oc]
 4 | build/
 5 | dist/
 6 | wheels/
 7 | *.egg-info
 8 | 
 9 | # Virtual environments
10 | .venv
11 | 
```

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

```markdown
 1 | # LibreSprite-MCP
 2 | 
 3 | [![Install MCP Server](https://cursor.com/deeplink/mcp-install-light.svg)](https://cursor.com/install-mcp?name=libresprite&config=JTdCJTIyY29tbWFuZCUyMiUzQSUyMnV2eCUyMGxpYnJlc3ByaXRlLW1jcCUyMiU3RA%3D%3D)
 4 | [![PyPI version](https://img.shields.io/pypi/v/libresprite-mcp)](https://pypi.org/project/libresprite-mcp/)
 5 | 
 6 | > Prompt your way into LibreSprite
 7 | 
 8 | Model Context Protocol (MCP) server for prompt-assisted editing, designing, and scripting inside LibreSprite.
 9 | 
10 | https://github.com/user-attachments/assets/71440bba-16a5-4ee2-af10-2c346978a290
11 | 
12 | ## Prerequisites
13 | 
14 | [`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:
15 | 
16 | - **Windows**: (run as administrator)
17 | 
18 |     ```powershell
19 |     powershell -ExecutionPolicy ByPass -c "irm https://astral.sh/uv/install.ps1 | iex"
20 |     ```
21 | 
22 | - **Unix**:
23 | 
24 |     ```bash
25 |     curl -LsSf https://astral.sh/uv/install.sh | sh
26 |     ```
27 | 
28 | More on [installing `uv`](https://docs.astral.sh/uv/getting-started/installation/).
29 | 
30 | 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)
31 | 
32 | ## Usage
33 | 
34 | ### Step 1: Setting up the client
35 | 
36 | Add the MCP server with the following entrypoint command (or something else if you are not using `uv`) to your MCP client:
37 | 
38 | ```bash
39 | uvx libresprite-mcp
40 | ```
41 | 
42 | #### Examples:
43 | 
44 | - **Claude Desktop & Cursor**
45 | 
46 |     Edit _Claude > Settings > Developer > Edit Config > claude_desktop_config.json_ or _.cursor > mcp.json_ to include the server:
47 |     
48 |     ```json
49 |     {
50 |         "mcpServers": {
51 |             // ...existing servers...
52 |             "libresprite": {
53 |                 "type": "stdio",
54 |                 "command": "uvx",
55 |                 "args": [
56 |                     "libresprite-mcp"
57 |                 ]
58 |             }
59 |             // ...existing servers...
60 |         }
61 |     }
62 |     ```
63 | 
64 |     You can also use this fancy badge to make it quick:
65 |   
66 |     [![Install MCP Server](https://cursor.com/deeplink/mcp-install-dark.svg)](https://cursor.com/install-mcp?name=libresprite&config=JTdCJTIyY29tbWFuZCUyMiUzQSUyMnV2eCUyMGxpYnJlc3ByaXRlLW1jcCUyMiU3RA%3D%3D)
67 | 
68 | > [!NOTE]
69 | > You will have to restart Claude Desktop to load the MCP Server.
70 | 
71 | ### Step 2: Setting up LibreSprite
72 | 
73 | 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:
74 | 
75 | ![scripts-folder](https://raw.githubusercontent.com/Snehil-Shah/libresprite-mcp/main/assets/scripts-folder.png)
76 | 
77 | ### Step 3: Connect and use
78 | 
79 | 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:
80 | 
81 | ![connect-button](https://raw.githubusercontent.com/Snehil-Shah/libresprite-mcp/main/assets/connect.png)
82 | 
83 | Click the "Connect" button and you can now start talking to Claude about your next big pixel-art project!
84 | 
85 | ## Some pointers
86 | 
87 | - You can only run one instance of the MCP server at a time.
88 | - The server expects port `64823` to be free.
89 | - 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.
90 | - 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.
91 | 
92 | ***
93 | 
```

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

```toml
 1 | [project]
 2 | name = "libresprite-mcp"
 3 | version = "0.1.3"
 4 | description = "Prompt your way into LibreSprite"
 5 | readme = "README.md"
 6 | authors = [
 7 |     { name = "Snehil Shah", email = "[email protected]" }
 8 | ]
 9 | requires-python = ">=3.11"
10 | dependencies = [
11 |     "flask>=3.1.1",
12 |     "mcp[cli]>=1.12.2",
13 | ]
14 | 
15 | [project.scripts]
16 | libresprite-mcp = "libresprite_mcp:main"
17 | 
18 | [build-system]
19 | requires = ["uv_build>=0.8.3,<0.9.0"]
20 | build-backend = "uv_build"
21 | 
```

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

```markdown
1 | # Architecture
2 | 
3 | 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.
4 | 
5 | 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.
6 | 
7 | ![architecture](https://raw.githubusercontent.com/Snehil-Shah/libresprite-mcp/main/assets/architecture.svg)
```

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

```python
 1 | """
 2 | Main entry point for the Libresprite MCP server application.
 3 | """
 4 | 
 5 | import click
 6 | import logging
 7 | from .libresprite_proxy import LibrespriteProxy
 8 | from .mcp_server import MCPServer
 9 | 
10 | def main() -> None:
11 |     """Main entry point for the application."""
12 |     # Disable unwanted logging to avoid messing with stdio
13 |     logging.disable(logging.WARN)
14 |     # HACK: https://stackoverflow.com/a/57086684
15 |     def secho(text, file=None, nl=None, err=None, color=None, **styles):
16 |         pass
17 |     def echo(text, file=None, nl=None, err=None, color=None, **styles):
18 |         pass
19 |     click.echo = echo
20 |     click.secho = secho
21 | 
22 |     # Initialize and start HTTP relay server
23 |     libresprite_proxy = LibrespriteProxy(port=64823)
24 |     libresprite_proxy.start()
25 | 
26 |     # Initialize and run MCP server (this blocks)
27 |     mcp_server = MCPServer(libresprite_proxy)
28 |     mcp_server.run(transport='stdio')
29 | 
30 | if __name__ == '__main__':
31 |     main()
```

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

```python
 1 | """
 2 | MCP server implementation that exposes tools for interacting with the libresprite-proxy server.
 3 | """
 4 | 
 5 | import os
 6 | from mcp.server.fastmcp import FastMCP, Context
 7 | from .libresprite_proxy import LibrespriteProxy
 8 | 
 9 | 
10 | class MCPServer:
11 |     """The LibreSprite MCP Server."""
12 | 
13 |     def __init__(self, libresprite_proxy: LibrespriteProxy, server_name: str = "libresprite"):
14 |         # Cache the libresprite proxy instance
15 |         self._libresprite_proxy = libresprite_proxy
16 | 
17 |         # Initialize FastMCP
18 |         self.mcp = FastMCP(server_name)
19 | 
20 |         # Setup MCP tools, prompts, and resources
21 |         self._setup_tools()
22 |         self._setup_resources()
23 |         self._setup_prompts()
24 | 
25 |     def _setup_tools(self):
26 |         """Setup MCP tools."""
27 | 
28 |         @self.mcp.tool()
29 |         def run_script(script: str, ctx: Context) -> str:
30 |             """
31 |             Run a JavaScript script inside Libresprite.
32 | 
33 |             IMPORTANT: Make sure you are well versed with the documentation and examples provided in the resources `docs:reference` and `docs:examples`.
34 | 
35 |             Args:
36 |                 script: The script to execute
37 | 
38 |             Returns:
39 |                 Console output
40 |             """
41 |             return self._libresprite_proxy.run_script(script, ctx)
42 | 
43 |     def _setup_resources(self):
44 |         """Setup MCP resources."""
45 | 
46 |         base_dir = os.path.dirname(os.path.abspath(__file__))
47 | 
48 |         @self.mcp.resource("docs://reference")
49 |         def read_reference() -> str:
50 |             """Read the libresprite command reference documentation."""
51 |             doc_path = os.path.join(base_dir, "resources", "reference.txt")
52 |             try:
53 |                 with open(doc_path, "r", encoding="utf-8") as f:
54 |                     return f.read()
55 |             except Exception as e:
56 |                 return f"Error reading reference.txt: {e}"
57 | 
58 |         @self.mcp.resource("docs://examples")
59 |         def read_examples() -> str:
60 |             """Read example scripts using libresprite commands."""
61 |             example_path = os.path.join(base_dir, "resources", "examples.txt")
62 |             try:
63 |                 with open(example_path, "r", encoding="utf-8") as f:
64 |                     return f.read()
65 |             except Exception as e:
66 |                 return f"Error reading examples.txt: {e}"
67 | 
68 |     def _setup_prompts(self):
69 |         """Setup MCP prompts."""
70 | 
71 |         @self.mcp.prompt(title="libresprite")
72 |         def libresprite(prompt: str) -> str:
73 |             """
74 |             Prompt template to use the libresprite tool with proper context conditioning.
75 | 
76 |             Args:
77 |                 prompt: User prompt
78 | 
79 |             Returns:
80 |                 Prompt to process
81 |             """
82 |             return f"""
83 |             Libresprite is a program for creating and editing pixel art and animations using JavaScript.
84 | 
85 |             Before proceeding, please ensure you are well versed with the documentation and examples provided in the resources `docs:reference` and `docs:examples`.
86 | 
87 |             You can use the `run_script` tool to execute JavaScript scripts in the context of libresprite.
88 | 
89 |             Here's what you need to do using the above tools and resources:
90 | 
91 |             {prompt}
92 |             """
93 | 
94 |     def run(self, transport: str = 'stdio'):
95 |         """Run the MCP server."""
96 |         self.mcp.run(transport=transport)
```

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

```python
  1 | """
  2 | Proxy server for Libresprite.
  3 | """
  4 | 
  5 | import threading
  6 | from typing import Optional
  7 | from mcp.server.fastmcp import Context
  8 | from flask import Flask, jsonify, request
  9 | 
 10 | class LibrespriteProxy:
 11 |     """
 12 |     Proxy server for Libresprite.
 13 | 
 14 |     This is a relay server that acts as a bridge between the MCP server and the remote libresprite script.
 15 |     From the POV of the MCP server, this fully abstracts libresprite into a convenient IO interface to execute remote scripts.
 16 |     """
 17 | 
 18 |     def __init__(self, port: int):
 19 |         # Stores current script
 20 |         self._script: str | None = None
 21 | 
 22 |         # Stores output of the script execution
 23 |         self._output: str | None = None
 24 | 
 25 |         # Stores flag indicating if execution is in progress
 26 |         self._lock: bool = False
 27 | 
 28 |         # Initialize event handlers
 29 |         self._script_event = threading.Event()
 30 |         self._output_event = threading.Event()
 31 | 
 32 |         # Relay server configuration
 33 |         self.port = port
 34 |         self.app = Flask(__name__)
 35 | 
 36 |         # Initialize server routes
 37 |         self._setup_routes()
 38 |         self._server_thread: Optional[threading.Thread] = None
 39 | 
 40 |     def _setup_routes(self):
 41 |         """Setup Flask routes."""
 42 | 
 43 |         @self.app.get('/')
 44 |         def get_script():
 45 |             """Get the current script."""
 46 |             if self._script is None:
 47 |                 self._script_event.wait()
 48 |                 self._script_event.clear()
 49 |             script = self._script
 50 |             self._script = None
 51 |             return jsonify({"script": script})
 52 | 
 53 |         @self.app.post('/')
 54 |         def post_output():
 55 |             """Post execution output."""
 56 |             if not self._lock:
 57 |                 # ignore random requests
 58 |                 return jsonify({"status": "ignored"})
 59 |             req = request.get_json(force=True, silent=True)
 60 |             if req:
 61 |                 output = req.get('output')
 62 |             else:
 63 |                 return jsonify({"status": "invalid"})
 64 |             self._output = output
 65 |             self._output_event.set()
 66 |             return jsonify({"status": "success"})
 67 | 
 68 |         @self.app.get('/ping')
 69 |         def ping():
 70 |             """Ping endpoint for health checks."""
 71 |             return jsonify({"status": "pong"})
 72 | 
 73 |     def _run_server(self):
 74 |         """Run the Flask server."""
 75 |         self.app.run(
 76 |             host='localhost',
 77 |             port=self.port,
 78 |             debug=False,
 79 |             use_reloader=False
 80 |         )
 81 | 
 82 |     def start(self):
 83 |         """Start the HTTP server in a background thread."""
 84 |         if self._server_thread and self._server_thread.is_alive():
 85 |             return
 86 | 
 87 |         self._server_thread = threading.Thread(
 88 |             target=self._run_server,
 89 |             daemon=True
 90 |         )
 91 |         self._server_thread.start()
 92 | 
 93 |     def run_script(self, script: str, ctx: Context) -> str:
 94 |         """
 95 |         Run a script in the execution context.
 96 | 
 97 |         WARNING: This method is synchronous and blocking.
 98 | 
 99 |         Args:
100 |             script: The script to execute
101 |         """
102 |         # This proxy only allows one script to be executed at a time
103 |         if self._lock:
104 |             ctx.error("Script execution is already in progress...")
105 |             raise RuntimeError("Script execution is already in progress.")
106 | 
107 |         # Sending the script
108 |         ctx.info("Sending script to libresprite...")
109 |         self._lock = True
110 |         self._script = script
111 |         self._script_event.set()
112 | 
113 |         # Waiting for execution
114 |         if not self._output_event.wait(timeout=15):
115 |             ctx.warning("This is taking longer than usual, make sure the user has the Libresprite application with the remote script running?")
116 |         self._output_event.wait()
117 |         self._output_event.clear()
118 | 
119 |         # Return the output
120 |         ctx.info("Script execution completed, checking the logs...")
121 |         output = self._output
122 |         self._output = None
123 |         self._lock = False
124 |         return output
```

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

```yaml
  1 | name: Release
  2 | 
  3 | on:
  4 |   workflow_dispatch:
  5 | 
  6 | jobs:
  7 |   pypi:
  8 |     name: PyPI
  9 |     runs-on: ubuntu-latest
 10 |     permissions:
 11 |       id-token: write
 12 |     steps:
 13 |       - name: Checkout repository
 14 |         uses: actions/checkout@v4
 15 | 
 16 |       - name: Install uv
 17 |         uses: astral-sh/setup-uv@v6
 18 | 
 19 |       - name: Set up Python
 20 |         run: uv python install
 21 | 
 22 |       - name: Build package
 23 |         run: uv build
 24 | 
 25 |       - name: Publish package
 26 |         run: uv publish
 27 | 
 28 |   github:
 29 |     name: GitHub
 30 |     runs-on: ubuntu-latest
 31 |     needs: [pypi]
 32 |     permissions:
 33 |       contents: write
 34 |     steps:
 35 |       - name: Checkout repository
 36 |         uses: actions/checkout@v4
 37 |         with:
 38 |           fetch-depth: 0
 39 |           fetch-tags: true
 40 | 
 41 |       - name: Get version from pyproject.toml
 42 |         id: version
 43 |         run: |
 44 |           VERSION=$(grep "^version" pyproject.toml | sed 's/version = "\(.*\)"/\1/')
 45 |           echo "version=$VERSION" >> $GITHUB_OUTPUT
 46 | 
 47 |       - name: Check if version exists
 48 |         id: version_check
 49 |         run: |
 50 |           VERSION=${{ steps.version.outputs.version }}
 51 |           if git tag | grep -q "v$VERSION"; then
 52 |             echo "Version v$VERSION already exists, skipping release"
 53 |             echo "should_release=false" >> $GITHUB_OUTPUT
 54 |           else
 55 |             echo "New version v$VERSION detected, proceeding with release"
 56 |             echo "should_release=true" >> $GITHUB_OUTPUT
 57 |           fi
 58 | 
 59 |       - name: Generate changelog
 60 |         id: changelog
 61 |         if: ${{ steps.version_check.outputs.should_release == 'true' }}
 62 |         run: |
 63 |           LAST_TAG=$(git describe --tags --abbrev=0 2>/dev/null || echo "")
 64 | 
 65 |           if [ -z "$LAST_TAG" ]; then
 66 |             COMMITS=$(git log --pretty=format:"%s" --no-merges)
 67 |           else
 68 |             COMMITS=$(git log ${LAST_TAG}..HEAD --pretty=format:"%s" --no-merges)
 69 |           fi
 70 | 
 71 |           if [ -z "$COMMITS" ] || [ "$COMMITS" = "" ]; then
 72 |             echo "No commits since last tag, trying to get commits from previous tag..."
 73 |             PREV_TAG=$(git tag --sort=-version:refname | head -2 | tail -1)
 74 |             echo "Previous tag: $PREV_TAG"
 75 |             if [ -n "$PREV_TAG" ] && [ "$PREV_TAG" != "$LAST_TAG" ]; then
 76 |               COMMITS=$(git log ${PREV_TAG}..HEAD --pretty=format:"%s" --no-merges)
 77 |               echo "Commits since $PREV_TAG:"
 78 |               echo "$COMMITS"
 79 |             fi
 80 |           fi
 81 | 
 82 |           FEATURES=$(echo "$COMMITS" | grep -E "^feat" | sed 's/^/- /' || true)
 83 |           FIXES=$(echo "$COMMITS" | grep -E "^fix" | sed 's/^/- /' || true)
 84 |           CHORES=$(echo "$COMMITS" | grep -E "^chore" | sed 's/^/- /' || true)
 85 |           BUILDS=$(echo "$COMMITS" | grep -E "^build" | sed 's/^/- /' || true)
 86 |           DOCS=$(echo "$COMMITS" | grep -E "^docs" | sed 's/^/- /' || true)
 87 |           TEST=$(echo "$COMMITS" | grep -E "^test" | sed 's/^/- /' || true)
 88 |           OTHER_COMMITS=$(echo "$COMMITS" | grep -v -E "^(feat|fix|chore|build|docs|test)" | sed 's/^/- /' || true)
 89 | 
 90 |           CHANGELOG=""
 91 |           if [ -n "$FEATURES" ]; then
 92 |             CHANGELOG="${CHANGELOG}### Features"$'\n'"$FEATURES"$'\n\n'
 93 |           fi
 94 |           if [ -n "$FIXES" ]; then
 95 |             CHANGELOG="${CHANGELOG}### Bug Fixes"$'\n'"$FIXES"$'\n\n'
 96 |           fi
 97 |           if [ -n "$DOCS" ]; then
 98 |             CHANGELOG="${CHANGELOG}### Documentation"$'\n'"$DOCS"$'\n\n'
 99 |           fi
100 |           if [ -n "$TEST" ]; then
101 |             CHANGELOG="${CHANGELOG}### Tests"$'\n'"$TEST"$'\n\n'
102 |           fi
103 |           if [ -n "$CHORES" ] || [ -n "$BUILDS" ]; then
104 |             CHANGELOG="${CHANGELOG}### Maintenance"$'\n'
105 |             [ -n "$CHORES" ] && CHANGELOG="${CHANGELOG}$CHORES"$'\n'
106 |             [ -n "$BUILDS" ] && CHANGELOG="${CHANGELOG}$BUILDS"$'\n'
107 |           fi
108 |           if [ -n "$OTHER_COMMITS" ]; then
109 |             CHANGELOG="${CHANGELOG}### Other Changes"$'\n'"$OTHER_COMMITS"$'\n'
110 |           fi
111 | 
112 |           {
113 |             echo "changelog<<EOF"
114 |             printf '%s' "$CHANGELOG"
115 |             echo "EOF"
116 |           } >> $GITHUB_OUTPUT
117 | 
118 |       - name: Create GitHub Release
119 |         uses: softprops/action-gh-release@v1
120 |         if: ${{ steps.version_check.outputs.should_release == 'true' }}
121 |         with:
122 |           tag_name: v${{ steps.version.outputs.version }}
123 |           name: v${{ steps.version.outputs.version }}
124 |           body: |
125 |             Release of `libresprite-mcp` version ${{ steps.version.outputs.version }}.
126 | 
127 |             Find the official MCP distribution from [PyPI](https://pypi.org/project/libresprite-mcp/) and the remote script `mcp.js` attached to this release.
128 | 
129 |             For the installation instructions, refer to the project [README](https://github.com/Snehil-Shah/libresprite-mcp/blob/main/README.md).
130 | 
131 |             ## Changes
132 |             ${{ steps.changelog.outputs.changelog }}
133 |           files: remote/mcp.js
```

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

```
  1 | # Examples
  2 | 
  3 | ## Random
  4 | 
  5 | ```javascript
  6 | const col = app.pixelColor;
  7 | const img = app.activeImage;
  8 | const h = img.height;
  9 | const w = img.width;
 10 | 
 11 | for (var y = 0; y < h; ++y) {
 12 |     for (var x = 0; x < w; ++x) {
 13 |         const c = Math.random() * 256 >>> 0;
 14 |         img.putPixel(x, y, col.rgba(c, c, c, 255))
 15 |     }
 16 | }
 17 | ```
 18 | 
 19 | ## White to Alpha
 20 | 
 21 | ```javascript
 22 | var col = app.pixelColor
 23 | var img = app.activeImage
 24 | 
 25 | for (y=0; y<img.height; ++y) {
 26 |   for (x=0; x<img.width; ++x) {
 27 |     var c = img.getPixel(x, y)
 28 |     var v = (col.rgbaR(c)+
 29 |              col.rgbaG(c)+
 30 |              col.rgbaB(c))/3
 31 | 
 32 |     img.putPixel(x, y,
 33 |                  col.rgba(col.rgbaR(c),
 34 |                           col.rgbaG(c),
 35 |                           col.rgbaB(c),
 36 |                           255-v))
 37 |   }
 38 | }
 39 | ```
 40 | 
 41 | ## PerLineOscillation
 42 | 
 43 | ```javascript
 44 | var dialog;
 45 | 
 46 | function wrap(x, n) {
 47 |     if (x < 0)
 48 | 	return ((x % n + n) % n) | 0;
 49 |     return ((x + n) % n) | 0;
 50 | }
 51 | 
 52 | const effects = [
 53 |     // horizontal oscillation
 54 |     function(src, angle, width, height, scale) {
 55 | 	const out = new Uint8Array(src.length);
 56 | 	for (var y = 0; y < height; ++y) {
 57 |             var ox = Math.sin(y / scale + angle * Math.PI * 2) * scale | 0;
 58 |             for (var x = 0; x < width; ++x) {
 59 | 		var oi = (y * width + x) * 4;
 60 | 		var ii = (y * width + wrap(x + ox, width)) * 4;
 61 | 		out[oi++] = src[ii++];
 62 | 		out[oi++] = src[ii++];
 63 | 		out[oi++] = src[ii++];
 64 | 		out[oi++] = src[ii++];
 65 |             }
 66 | 	}
 67 | 	return out;
 68 |     },
 69 | 
 70 |     // same as above, but with anti-aliasing
 71 |     function(src, angle, width, height, scale) {
 72 | 	const out = new Uint8Array(src.length);
 73 | 	for (var y = 0; y < height; ++y) {
 74 |             var ox = Math.sin(y / scale + angle * Math.PI * 2) * scale;
 75 | 	    var ox0 = ox | 0;
 76 | 	    var ox1 = ox0 + 1;
 77 | 	    var a = ox - ox0;
 78 |             for (var x = 0; x < width; ++x) {
 79 | 		var oi = (y * width + x) * 4;
 80 | 		var ii0 = (y * width + wrap(x + ox0, width)) * 4;
 81 | 		var ii1 = (y * width + wrap(x + ox1, width)) * 4;
 82 | 
 83 | 		out[oi++] = src[ii0++] * (1 - a) + src[ii1++] * a;
 84 | 		out[oi++] = src[ii0++] * (1 - a) + src[ii1++] * a;
 85 | 		out[oi++] = src[ii0++] * (1 - a) + src[ii1++] * a;
 86 | 		out[oi++] = src[ii0++] * (1 - a) + src[ii1++] * a;
 87 |             }
 88 | 	}
 89 | 	return out;
 90 |     },
 91 | 
 92 |     // horizontal oscillation, RGB desync'd
 93 |     function(src, angle, width, height, scale) {
 94 | 	const out = new Uint8Array(src.length);
 95 | 	for (var y = 0; y < height; ++y) {
 96 |             for (var x = 0; x < width; ++x) {
 97 | 		var d = 0;
 98 | 		var oi = (y * width + x) * 4;
 99 | 		var ox = Math.sin(y / scale + (angle + d) * Math.PI * 2) * scale | 0;
100 | 		var ii = (y * width + wrap(x + ox, width)) * 4;
101 | 		out[oi++] = src[ii++];
102 | 
103 | 		d += 0.03;
104 | 
105 | 		ox = Math.sin(y / scale + (angle + d) * Math.PI * 2) * scale | 0;
106 | 		ii = (y * width + wrap(x + ox, width)) * 4 + 1;
107 | 		out[oi++] = src[ii++];
108 | 
109 | 		ox = Math.sin(y / scale + (angle + d) * Math.PI * 2) * scale | 0;
110 | 		ii = (y * width + wrap(x + ox, width)) * 4 + 2;
111 | 		out[oi++] = src[ii++];
112 | 
113 | 		ox = Math.sin(y / scale + (angle + d) * Math.PI * 2) * scale | 0;
114 | 		ii = (y * width + wrap(x + ox, width)) * 4 + 3;
115 | 		out[oi++] = src[ii++];
116 |             }
117 | 	}
118 | 	return out;
119 |     },
120 | 
121 |     // vertical oscillation
122 |     function(src, angle, width, height, scale) {
123 | 	const out = new Uint8Array(src.length);
124 | 	for (var y = 0; y < height; ++y) {
125 |             var oy = Math.sin(y / scale + angle * Math.PI * 2) * scale | 0;
126 |             for (var x = 0; x < width; ++x) {
127 | 		var oi = (y * width + x) * 4;
128 | 		var ii = (wrap(y + oy, height) * width + x) * 4;
129 | 		out[oi++] = src[ii++];
130 | 		out[oi++] = src[ii++];
131 | 		out[oi++] = src[ii++];
132 | 		out[oi++] = src[ii++];
133 |             }
134 | 	}
135 | 	return out;
136 |     },
137 | 
138 |     // same as above, but with anti-aliasing
139 |     function(src, angle, width, height, scale) {
140 | 	const out = new Uint8Array(src.length);
141 | 	for (var y = 0; y < height; ++y) {
142 |             var oy = y + Math.sin(y / scale + angle * Math.PI * 2) * scale;
143 | 	    var oy0 = wrap(oy | 0, height) * width;
144 | 	    var ny = (y + 1) + Math.sin((y + 1) / scale + angle * Math.PI * 2) * scale;
145 | 	    var oy1 = wrap(ny | 0, height) * width;
146 | 	    var a = oy - Math.floor(oy);
147 |             for (var x = 0; x < width; ++x) {
148 | 		var oi = (y * width + x) * 4;
149 | 		var ii0 = (oy0 + x) * 4;
150 | 		var ii1 = (oy1 + x) * 4;
151 | 
152 | 		out[oi++] = src[ii0++] * (1 - a) + src[ii1++] * a;
153 | 		out[oi++] = src[ii0++] * (1 - a) + src[ii1++] * a;
154 | 		out[oi++] = src[ii0++] * (1 - a) + src[ii1++] * a;
155 | 		out[oi++] = src[ii0++] * (1 - a) + src[ii1++] * a;
156 |             }
157 | 	}
158 | 	return out;
159 |     }
160 | ];
161 | 
162 | const eventHandlers = {
163 |     init:function(){
164 |         if (!app.activeSprite) {
165 |             app.createDialog('Error').addLabel('Need an image to oscillate.');
166 |             return;
167 |         }
168 | 
169 |         if (dialog)
170 |             dialog.close();
171 |         dialog = app.createDialog('dialog');
172 |         dialog.addIntEntry("Frames to animate:", "frameCount", 0, 100);
173 |         dialog.addBreak();
174 |         dialog.addIntEntry("Wave size:", "waveSize", 3, 100);
175 |         dialog.addBreak();
176 |         dialog.addIntEntry("Effect:", "effect", 0, effects.length - 1);
177 |         dialog.addBreak();
178 |         dialog.addButton("Run", "run");
179 |     },
180 | 
181 |     run_click:function(){
182 |         dialog.close();
183 |         dialog = null;
184 | 
185 | 	app.command.setParameter("format", "rgb");
186 | 	app.command.ChangePixelFormat();
187 | 	app.command.clearParameters();
188 | 
189 |         const frameCount = storage.get("frameCount")|0;
190 |         const waveSize = storage.get("waveSize")|0;
191 | 	const effect = storage.get("effect")|0;
192 | 	const oscillate = effects[effect] || effects[0];
193 |         const sprite = app.activeSprite;
194 |         const layerNumber = app.activeLayerNumber;
195 |         const reference = sprite.layer(layerNumber).cel(0).image;
196 |         const refWidth = reference.width;
197 |         const refHeight = reference.height;
198 |         const src = reference.getImageData();
199 | 
200 |         app.command.setParameter("content", "current");
201 |         for (var i = 0; i < frameCount; ++i) {
202 |             if (i)
203 |                 app.command.NewFrame();
204 |             sprite.layer(layerNumber).cel(i).image.putImageData(oscillate(src, i / frameCount, refWidth, refHeight, waveSize));
205 |         }
206 |     }
207 | };
208 | 
209 | function onEvent(eventName) {
210 |     var handler = eventHandlers[eventName];
211 |     if (typeof handler == 'function')
212 |         handler();
213 | }
214 | ```
```

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

```javascript
  1 | /**
  2 |  * Source: https://github.com/Snehil-Shah/libresprite-mcp
  3 |  */
  4 | 
  5 | // Cache the global context.
  6 | const global = this;
  7 | 
  8 | /**
  9 |  * Model Context Protocol (MCP) remote script that interacts with the libresprite-mcp server.
 10 |  *
 11 |  * NOTE: Defined as an IIFE to avoid global namespace pollution.
 12 |  */
 13 | (function MCP() {
 14 |     // CONSTANTS //
 15 | 
 16 |     /**
 17 |      * URL to the relay server exposing the next command.
 18 |      */
 19 |     const RELAY_SERVER_URL = 'http://localhost:64823';
 20 | 
 21 |     /**
 22 |      * Delay between polling requests. (in number of rendering cycles)
 23 |      */
 24 |     const POLL_DELAY = 120;
 25 | 
 26 | 
 27 |     // VARIABLES //
 28 | 
 29 |     /**
 30 |      * Flag indicating extension state.
 31 |      *
 32 |      * @type {boolean}
 33 |      */
 34 |     let active = false;
 35 | 
 36 |     /**
 37 |      * Flag indicating whether polling is active.
 38 |      *
 39 |      * @type {boolean}
 40 |      */
 41 |     let polling = false;
 42 | 
 43 |     /**
 44 |      * Flag indicating whether the client is connected to the server.
 45 |      *
 46 |      * @type {boolean}
 47 |      */
 48 |     let connected = false;
 49 | 
 50 |     /**
 51 |      * Stores stdout.
 52 |      *
 53 |      * @type {string}
 54 |      */
 55 |     let output = '';
 56 | 
 57 |     /**
 58 |      * Function to get response from storage in the next cycle.
 59 |      *
 60 |      * @type {Function|null}
 61 |      */
 62 |     let _get_response = null;
 63 | 
 64 |     /**
 65 |      * Function to post response to storage in the next cycle.
 66 |      *
 67 |      * @type {Function|null}
 68 |      */
 69 |     let _post_response = null;
 70 | 
 71 |     /**
 72 |      * Dialog instance for UI.
 73 |      */
 74 |     let dialog = null;
 75 | 
 76 | 
 77 |     // FUNCTIONS //
 78 | 
 79 |     /**
 80 |      * Global `console.log`.
 81 |      */
 82 |     const _clientLogger = global.console.log;
 83 | 
 84 |     // Override global console object to capture stdout.
 85 |     const console = Object.assign({}, global.console);
 86 | 
 87 |     /**
 88 |      * Modified `console.log` that captures output before logging.
 89 |      */
 90 |     console.log = function() {
 91 |         var args = Array.prototype.slice.call(arguments);
 92 |         output += args.join(' ') + '\n';
 93 |         _clientLogger.apply(null, args);
 94 |     }
 95 | 
 96 |     /**
 97 |      * Makes a GET request.
 98 |      *
 99 |      * @private
100 |      * @param {string} url - url to fetch
101 |      * @param {Function} cb - callback function to handle the response
102 |      */
103 |     function _get(url, cb) {
104 |         storage.fetch(url, '_get_response');
105 |         _get_response = function() {
106 |             const status = storage.get('_get_response' + '_status');
107 |             const string = storage.get('_get_response');
108 |             cb({
109 |                 string,
110 |                 status
111 |             });
112 |         };
113 |     }
114 | 
115 |     /**
116 |      * Makes a POST request.
117 |      *
118 |      * @private
119 |      * @param {string} url - url to fetch
120 |      * @param {string} body - request body
121 |      * @param {Function} cb - callback function to handle the response
122 |      */
123 |     function _post(url, body, cb) {
124 |         storage.fetch(url, '_post_response', "", "POST", body, "Content-Type", "application/json");
125 |         _post_response = function() {
126 |             const status = storage.get('_post_response' + '_status');
127 |             const string = storage.get('_post_response');
128 |             cb({
129 |                 string,
130 |                 status
131 |             });
132 |         }
133 |     }
134 | 
135 |     /**
136 |      * Makes a GET request.
137 |      *
138 |      * @param {string} url - url to fetch
139 |      * @param {Function} cb - callback function to handle the response
140 |      */
141 |     function get(url, cb) {
142 |         _get(url, function(rsp) {
143 |             var data, error = rsp.status != 200 ? 'status:' + rsp.status : 0;
144 |             try {
145 |                 if (!error)
146 |                     data = JSON.parse(rsp.string);
147 |             } catch (ex) {
148 |                 error = ex;
149 |             }
150 |             cb(data, error);
151 |         });
152 |     }
153 | 
154 |     /**
155 |      * Makes a POST request.
156 |      *
157 |      * @param {string} url - url to fetch
158 |      * @param {Object} body - request body
159 |      * @param {Function} cb - callback function to handle the response
160 |      */
161 |     function post(url, body, cb) {
162 |         _post(url, body, function(rsp) {
163 |             var data, error = rsp.status != 200 ? 'status:' + rsp.status : 0;
164 |             try {
165 |                 if (!error)
166 |                     data = JSON.parse(rsp.string);
167 |                 else error += rsp.string;
168 |             } catch (ex) {
169 |                 error = ex;
170 |             }
171 |             cb(data, error);
172 |         });
173 |     }
174 | 
175 |     /**
176 |      * Pings for server health.
177 |      *
178 |      * This yields a "bad_health" event if the server is unreachable,
179 |      * or an "init" event if the server is reachable.
180 |      */
181 |     function checkServerHealth() {
182 |         get(RELAY_SERVER_URL + '/ping', function(data, error) {
183 |             if (error) {
184 |                 connected = false;
185 |                 app.yield("bad_health", POLL_DELAY);
186 |                 return;
187 |             }
188 |             if (data && data.status === 'pong') {
189 |                 connected = true;
190 |                 app.yield("good_health");
191 |             } else {
192 |                 connected = false;
193 |                 app.yield("bad_health", POLL_DELAY);
194 |             }
195 | 
196 |         });
197 |     }
198 | 
199 |     /**
200 |      * Fetches the next script from the server.
201 |      *
202 |      * @param {Function} cb - script handler
203 |      */
204 |     function getScript(cb) {
205 |         get(RELAY_SERVER_URL, function(data, error) {
206 |             if (error) {
207 |                 // The post request will log the error.
208 |                 cb('');
209 |                 return;
210 |             }
211 |             cb((data && data.script) ? data.script : '');
212 |         });
213 |     }
214 | 
215 |     /**
216 |      * Posts the output to the server.
217 |      */
218 |     function postOutput() {
219 |         const body = JSON.stringify({output: output});
220 |         post(RELAY_SERVER_URL, body, function(data, error) {
221 |             // 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.
222 |             if (error) {
223 |                 _clientLogger('The MCP server was shut down.');
224 |                 connected = false;
225 |                 paintUI();
226 |                 app.yield("bad_health", POLL_DELAY);
227 |                 return;
228 |             }
229 |             if ( !data ) {
230 |                 _clientLogger('Something went wrong. Please report it on https://github.com/Snehil-Shah/libresprite-mcp/issues.');
231 |                 return;
232 |             }
233 |             if ( data.status === 'invalid' )
234 |                 _clientLogger('Something is wrong. Please report it on https://github.com/Snehil-Shah/libresprite-mcp/issues.');
235 |             // Other status types can be ignored...
236 |             // Continue polling...
237 |             if (!polling) {
238 |                 return;
239 |             }
240 |             app.yield("poll", POLL_DELAY);
241 |         });
242 |     }
243 | 
244 |     /**
245 |      * Runs a script in the current context.
246 |      *
247 |      * @param {string} script - script to run
248 |      */
249 |     function runScript(script) {
250 |         if (!script) {
251 |             return;
252 |         }
253 |         try {
254 |             // Execute in global scope with our custom logger...
255 |             new Function('console', script)(console);
256 |         } catch (e) {
257 |             console.log('Error in script:', e.message);
258 |         }
259 |     }
260 | 
261 |     /**
262 |      * Fetches, executes, and posts the output for the next script.
263 |      *
264 |      * NOTE: This is the entry point for the polling loop.
265 |      */
266 |     function exec() {
267 |         if (!polling) return;
268 |         getScript(script => {
269 |             output = ''; // sanity reset
270 |             runScript(script);
271 |             postOutput();
272 |         });
273 |     }
274 | 
275 |     /**
276 |      * Starts the polling loop.
277 |      */
278 |     function startPolling() {
279 |         if (polling) return;
280 |         polling = true;
281 |         exec();
282 |     }
283 | 
284 |     /**
285 |      * Stops the polling loop.
286 |      */
287 |     function stopPolling() {
288 |         polling = false;
289 |     }
290 | 
291 |     /**
292 |      * Paints the UI dialog based on the current state.
293 |      */
294 |     function paintUI() {
295 |         let label;
296 |         if (!connected) {
297 |             label = 'Discovering MCP servers... Make sure the libresprite-mcp server is running.';
298 |         } else if (polling) {
299 |             label = 'Connected to the libresprite-mcp server!';
300 |         } else {
301 |             label = 'Found an active libresprite-mcp server, "Connect" when you are ready!';
302 |         }
303 |         if (dialog) {
304 |             dialog.close();
305 |         }
306 |         dialog = app.createDialog();
307 |         dialog.title = 'libresprite-mcp';
308 |         dialog.addLabel(label);
309 |         dialog.addBreak();
310 |         dialog.canClose = !connected || !polling;
311 |         if ( connected ) {
312 |             dialog.addButton(
313 |                 polling ? 'Disconnect': 'Connect',
314 |                 'toggle'
315 |             );
316 |         }
317 |     }
318 | 
319 | 
320 |     // MAIN //
321 | 
322 |     /**
323 |      * Event handler.
324 |      *
325 |      * @global
326 |      * @param {string} event
327 |      */
328 |     function onEvent(event) {
329 |         switch (event) {
330 |             /**
331 |              * Initialize script.
332 |              */
333 |             case 'init':
334 |                 active = true;
335 |                 checkServerHealth();
336 |                 paintUI();
337 |                 return;
338 |             /**
339 |              * Cleanup script.
340 |              */
341 |             case '_close':
342 |                 active = false;
343 |                 connected = false;
344 |                 polling = false;
345 |                 return;
346 |             /**
347 |              * Events triggered by initial health checks.
348 |              */
349 |             case 'bad_health':
350 |                 if (!active) {
351 |                     // The extension was closed, stop recursion...
352 |                     return;
353 |                 }
354 |                 checkServerHealth();
355 |                 return;
356 |             case 'good_health':
357 |                 paintUI();
358 |                 return;
359 |             /**
360 |              * UI operation.
361 |              */
362 |             case 'toggle_click':
363 |                 if (polling) {
364 |                     stopPolling();
365 |                 } else {
366 |                     startPolling();
367 |                 }
368 |                 paintUI();
369 |                 return;
370 |             /**
371 |              * Successful 'GET' event response triggered by `storage.fetch`.
372 |              */
373 |             case '_get_response_fetch':
374 |                 _get_response && _get_response();
375 |                 _get_response = null;
376 |                 return;
377 |             /**
378 |              * Successful 'POST' event response triggered by `storage.fetch`.
379 |              */
380 |             case '_post_response_fetch':
381 |                 _post_response && _post_response();
382 |                 _post_response = null;
383 |                 return;
384 |             /**
385 |              * Event triggered to continue polling the endpoint.
386 |              */
387 |             case 'poll':
388 |                 if (!active) {
389 |                     // The extension was closed, stop recursion...
390 |                     // NOTE: This is a sanity check and should never be executed given the close button is not visible during polling
391 |                     stopPolling();
392 |                     return;
393 |                 }
394 |                 exec();
395 |                 return;
396 |             default:
397 |                 // No action for unknown events
398 |                 break;
399 |         }
400 |     }
401 |     global.onEvent = onEvent;
402 | })();
```

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

```
  1 | # [class Sprite]
  2 | ## Properties:
  3 |    - `palette`: read-only. Returns the sprite's palette.
  4 |    - `selection`: Placeholder. Do not use.
  5 |    - `height`: read+write. Returns and sets the height of the sprite.
  6 |    - `width`: read+write. Returns and sets the width of the sprite.
  7 |    - `filename`: read-only. Returns the file name of the sprite.
  8 |    - `colorMode`: read-only. Returns the sprite's ColorMode.
  9 |    - `layerCount`: read-only. Returns the amount of layers in the sprite.
 10 | 
 11 | ## Methods:
 12 |    - `loadPalette(fileName)`:
 13 |      - fileName: The name of the palette file to load
 14 |       returns: Nothing
 15 |       loads a palette file.
 16 | 
 17 |    - `crop(x, y, width, height)`:
 18 |      - x: The left-most edge of the crop.
 19 |      - y: The top-most edge of the crop.
 20 |      - width: The width of the cropped area.
 21 |      - height: The height of the cropped area.
 22 |       returns: Nothing
 23 |       crops the sprite to the specified dimensions.
 24 | 
 25 |    - `saveAs(fileName, asCopy)`:
 26 |      - fileName: String. The new name of the file
 27 |      - asCopy: If true, the file is saved as a copy. Requires fileName to be specified.
 28 |       returns: Nothing
 29 |       saves the sprite.
 30 | 
 31 |    - `resize(width, height)`:
 32 |      - width: The new width.
 33 |      - height: The new height.
 34 |       returns: Nothing
 35 |       resizes the sprite.
 36 | 
 37 |    - `save()`:
 38 |       returns: Nothing
 39 |       saves the sprite.
 40 | 
 41 |    - `commit()`:
 42 |       returns: Nothing
 43 |       commits the current transaction.
 44 | 
 45 |    - `layer(layerNumber)`:
 46 |      - layerNumber: The number of they layer, starting with zero from the bottom.
 47 |       returns: a Layer object or null if invalid.
 48 |       allows you to access a given layer.
 49 | 
 50 | 
 51 | 
 52 | # global storage [class Storage]
 53 | ## No Properties.
 54 | 
 55 | ## Methods:
 56 |    - `decodeBase64()`:
 57 |       returns: Nothing
 58 | 
 59 |    - `get()`:
 60 |       returns: Nothing
 61 | 
 62 |    - `save()`:
 63 |       returns: Nothing
 64 | 
 65 |    - `set()`:
 66 |       returns: Nothing
 67 | 
 68 |    - `fetch()`:
 69 |       returns: Nothing
 70 | 
 71 |    - `load()`:
 72 |       returns: Nothing
 73 | 
 74 |    - `unload()`:
 75 |       returns: Nothing
 76 | 
 77 | 
 78 | 
 79 | # [class PalettelistboxWidget]
 80 | ## Properties:
 81 |    - `selected`:
 82 |    - `id`:
 83 | 
 84 | ## Methods:
 85 |    - `addPalette()`:
 86 |       returns: Nothing
 87 | 
 88 | 
 89 | 
 90 | # [class pixelColor]
 91 | ## No Properties.
 92 | 
 93 | ## Methods:
 94 |    - `grayaA(color)`:
 95 |      - color: A 32-bit color in 888 RGBA format
 96 |       returns: The alpha component of the color
 97 |       Extracts the alpha (opacity) from a 32-bit color
 98 | 
 99 |    - `grayaV(color)`:
100 |      - color: A 32-bit color in 888 RGBA format
101 |       returns: The luminance Value of the color
102 |       Extracts the luminance from a 32-bit color
103 | 
104 |    - `rgbaA(color)`:
105 |      - color: A 32-bit color in 8888 RGBA format
106 |       returns: The alpha component of the color
107 |       Extracts the alpha channel from a 32-bit color
108 | 
109 |    - `graya(gray, alpha)`:
110 |      - gray: The luminance of color
111 |      - alpha: The alpha (opacity) of the color)
112 |       returns: The color with the given luminance/opacity
113 | 
114 |    - `rgba(r, g, b, a)`:
115 |      - r: red, 0-255.
116 |      - g: green, 0-255.
117 |      - b: blue, 0-255.
118 |      - a: alpha (opacity), 0-255.
119 |       returns: A 32-bit color in 8888 RGBA format.
120 |       Converts R, G, B, A values into a single 32-bit RGBA color.
121 | 
122 |    - `rgbaG(color)`:
123 |      - color: A 32-bit color in 8888 RGBA format
124 |       returns: The green component of the color
125 |       Extracts the green channel from a 32-bit color
126 | 
127 |    - `rgbaB(color)`:
128 |      - color: A 32-bit color in 8888 RGBA format
129 |       returns: The blue component of the color
130 |       Extracts the blue channel from a 32-bit color
131 | 
132 |    - `rgbaR(color)`:
133 |      - color: A 32-bit color in 8888 RGBA format
134 |       returns: The red component of the color
135 |       Extracts the red channel from a 32-bit color
136 | 
137 | 
138 | 
139 | # [class LabelWidget]
140 | ## Properties:
141 |    - `text`:
142 |    - `id`:
143 | 
144 | ## No Methods.
145 | 
146 | 
147 | # [class Layer]
148 | ## Properties:
149 |    - `flags`: read-only. Returns all flags OR'd together as an int
150 |    - `isContinuous`: read-only. Prefer to link cels when the user copies them.
151 |    - `celCount`: read-only. Returns the number of cels.
152 |    - `isMovable`: read-only. Returns true if the layer is movable.
153 |    - `isVisible`: read+write. Gets/sets whether the layer is visible or not.
154 |    - `isTransparent`: read-only. Returns true if the layer is a non-background image layer.
155 |    - `isBackground`: read-only. Returns true if the layer is a background layer.
156 |    - `isImage`: read-only. Returns true if the layer is an image, false if it is a folder.
157 |    - `isEditable`: read+write. Gets/sets whether the layer is editable (unlocked) or not (locked).
158 |    - `name`: read+write. The name of the layer.
159 | 
160 | ## Methods:
161 |    - `cel(index)`:
162 |      - index: The number of the Cel
163 |       returns: A Cel object or null if an invalid index is passed
164 |       retrieves a Cel
165 | 
166 | 
167 | 
168 | # [class IntentryWidget]
169 | ## Properties:
170 |    - `value`:
171 |    - `min`:
172 |    - `max`:
173 |    - `id`:
174 | 
175 | ## No Methods.
176 | 
177 | 
178 | # [class Image]
179 | ## Properties:
180 |    - `format`: read-only. The PixelFormat of the image.
181 |    - `stride`: read-only. The number of bytes per image row.
182 |    - `height`: read-only. The height of the image.
183 |    - `width`: read-only. The width of the image.
184 | 
185 | ## Methods:
186 |    - `putPixel(x, y, color)`:
187 |      - x: integer
188 |      - y: integer
189 |      - color: a 32-bit color in 8888 RGBA format.
190 |       returns: Nothing
191 |       writes the color onto the image at the the given coordinate.
192 | 
193 |    - `getImageData()`:
194 |       returns: All pixels in a Uint8Array
195 |       creates an array containing all of the image's pixels.
196 | 
197 |    - `putImageData(data)`:
198 |      - data: All of the pixels in the image.
199 |       returns: Nothing
200 |       writes the given pixels onto the image. Must be the same size as the image.
201 | 
202 |    - `getPNGData()`:
203 |       returns: The image as a Base64-encoded PNG string.
204 |       Encodes the image as a PNG.
205 | 
206 |    - `clear(color)`:
207 |      - color: a 32-bit color in 8888 RGBA format.
208 |       returns: Nothing
209 |       clears the image with the specified color.
210 | 
211 |    - `getPixel(x, y)`:
212 |      - x: integer
213 |      - y: integer
214 |       returns: a color value
215 |       reads a color from the given coordinate of the image.
216 | 
217 | 
218 | 
219 | # [class PN2ui6DialogE]
220 | ## Properties:
221 |    - `canClose`: write only. Determines if the user can close the dialog window.
222 |    - `title`: read+write. Sets the title of the dialog window.
223 |    - `id`:
224 | 
225 | ## Methods:
226 |    - `addBreak()`:
227 |       returns: Nothing
228 | 
229 |    - `addPaletteListBox()`:
230 |       returns: Nothing
231 | 
232 |    - `addEntry()`:
233 |       returns: Nothing
234 | 
235 |    - `addButton()`:
236 |       returns: Nothing
237 | 
238 |    - `add()`:
239 |       returns: Nothing
240 | 
241 |    - `close()`:
242 |       returns: Nothing
243 | 
244 |    - `addLabel()`:
245 |       returns: Nothing
246 | 
247 |    - `addDropDown()`:
248 |       returns: Nothing
249 | 
250 |    - `addIntEntry()`:
251 |       returns: Nothing
252 | 
253 |    - `get()`:
254 |       returns: Nothing
255 | 
256 | 
257 | 
258 | # global console [class Console]
259 | ## No Properties.
260 | 
261 | ## Methods:
262 |    - `assert()`:
263 |       returns: Nothing
264 | 
265 |    - `log()`:
266 |       returns: Nothing
267 | 
268 | 
269 | 
270 | # [class ButtonWidget]
271 | ## Properties:
272 |    - `text`:
273 |    - `id`:
274 | 
275 | ## No Methods.
276 | 
277 | 
278 | # [class command]
279 | ## No Properties.
280 | 
281 | ## Methods:
282 |    - `Zoom()`:
283 |       returns: Nothing
284 |       Zoom in
285 | 
286 |    - `ToggleFullscreen()`:
287 |       returns: Nothing
288 |       Toggle Fullscreen
289 | 
290 |    - `Timeline()`:
291 |       returns: Nothing
292 |       Switch Timeline
293 | 
294 |    - `TiledMode()`:
295 |       returns: Nothing
296 |       Tiled Mode
297 | 
298 |    - `SymmetryMode()`:
299 |       returns: Nothing
300 |       Symmetry Mode
301 | 
302 |    - `SpriteSize()`:
303 |       returns: Nothing
304 |       Sprite Size
305 | 
306 |    - `SnapToGrid()`:
307 |       returns: Nothing
308 |       Snap to Grid
309 | 
310 |    - `ShowLayerEdges()`:
311 |       returns: Nothing
312 |       Show Layer Edges
313 | 
314 |    - `TogglePreview()`:
315 |       returns: Nothing
316 |       Toggle Preview
317 | 
318 |    - `ShowGrid()`:
319 |       returns: Nothing
320 |       Show Grid
321 | 
322 |    - `ShowExtras()`:
323 |       returns: Nothing
324 |       Show Extras
325 | 
326 |    - `ShowBrushPreview()`:
327 |       returns: Nothing
328 |       Show Brush Preview
329 | 
330 |    - `Share()`:
331 |       returns: Nothing
332 |       Share
333 | 
334 |    - `SetSameInk()`:
335 |       returns: Nothing
336 |       Same Ink in All Tools
337 | 
338 |    - `SetInkType()`:
339 |       returns: Nothing
340 |       Set Ink Type: Simple Ink
341 | 
342 |    - `SetColorSelector()`:
343 |       returns: Nothing
344 |       Set Color Selector: Color Spectrum
345 | 
346 |    - `SelectionAsGrid()`:
347 |       returns: Nothing
348 |       Selection as Grid
349 | 
350 |    - `SelectTile()`:
351 |       returns: Nothing
352 |       Select Tile
353 | 
354 |    - `Scroll()`:
355 |       returns: Nothing
356 |       Scroll 0 pixels left
357 | 
358 |    - `SavePalette()`:
359 |       returns: Nothing
360 |       Save Palette
361 | 
362 |    - `SaveFileCopyAs()`:
363 |       returns: Nothing
364 |       Save File Copy As
365 | 
366 |    - `SaveFileAs()`:
367 |       returns: Nothing
368 |       Save File As
369 | 
370 |    - `SaveFile()`:
371 |       returns: Nothing
372 |       Save File
373 | 
374 |    - `SetLoopSection()`:
375 |       returns: Nothing
376 |       Set Loop Section
377 | 
378 |    - `RunScript()`:
379 |       returns: Nothing
380 |       Run Script
381 | 
382 |    - `ReverseFrames()`:
383 |       returns: Nothing
384 |       Reverse Frames
385 | 
386 |    - `ReselectMask()`:
387 |       returns: Nothing
388 |       Reselect Mask
389 | 
390 |    - `RescanScripts()`:
391 |       returns: Nothing
392 |       Rescan Scripts
393 | 
394 |    - `ReplaceColor()`:
395 |       returns: Nothing
396 |       Replace Color
397 | 
398 |    - `RepeatLastExport()`:
399 |       returns: Nothing
400 |       Repeat Last Export
401 | 
402 |    - `PlayAnimation()`:
403 |       returns: Nothing
404 |       Play Animation
405 | 
406 |    - `PixelPerfectMode()`:
407 |       returns: Nothing
408 |       Switch Pixel Perfect Mode
409 | 
410 |    - `Options()`:
411 |       returns: Nothing
412 |       Options
413 | 
414 |    - `OpenFile()`:
415 |       returns: Nothing
416 |       Open Sprite
417 | 
418 |    - `NewSpriteFromSelection()`:
419 |       returns: Nothing
420 |       New Sprite From Selection
421 | 
422 |    - `MaskByColor()`:
423 |       returns: Nothing
424 |       Mask By Color
425 | 
426 |    - `NewFrameTag()`:
427 |       returns: Nothing
428 |       New Frame Tag
429 | 
430 |    - `NewFile()`:
431 |       returns: Nothing
432 |       New File
433 | 
434 |    - `UndoHistory()`:
435 |       returns: Nothing
436 |       Undo History
437 | 
438 |    - `GotoNextLayer()`:
439 |       returns: Nothing
440 |       Go to Next Layer
441 | 
442 |    - `NewBrush()`:
443 |       returns: Nothing
444 |       New Brush
445 | 
446 |    - `OpenInFolder()`:
447 |       returns: Nothing
448 |       Open In Folder
449 | 
450 |    - `ClearCel()`:
451 |       returns: Nothing
452 |       Clear Cel
453 | 
454 |    - `MoveMask()`:
455 |       returns: Nothing
456 |       Move Selection Boundaries 0 pixels left
457 | 
458 |    - `FrameTagProperties()`:
459 |       returns: Nothing
460 |       Frame Tag Properties
461 | 
462 |    - `AddColor()`:
463 |       returns: Nothing
464 |       Add Foreground Color to Palette
465 | 
466 |    - `MoveCel()`:
467 |       returns: Nothing
468 |       Move Cel
469 | 
470 |    - `MergeDownLayer()`:
471 |       returns: Nothing
472 |       Merge Down Layer
473 | 
474 |    - `MaskAll()`:
475 |       returns: Nothing
476 |       Mask All
477 | 
478 |    - `SaveMask()`:
479 |       returns: Nothing
480 |       Save Mask
481 | 
482 |    - `LoadMask()`:
483 |       returns: Nothing
484 |       LoadMask
485 | 
486 |    - `LayerProperties()`:
487 |       returns: Nothing
488 |       Layer Properties
489 | 
490 |    - `LayerFromBackground()`:
491 |       returns: Nothing
492 |       Layer From Background
493 | 
494 |    - `SetPaletteEntrySize()`:
495 |       returns: Nothing
496 |       Set Palette Entry Size
497 | 
498 |    - `Launch()`:
499 |       returns: Nothing
500 |       Launch
501 | 
502 |    - `KeyboardShortcuts()`:
503 |       returns: Nothing
504 |       Keyboard Shortcuts
505 | 
506 |    - `InvertMask()`:
507 |       returns: Nothing
508 |       Invert Mask
509 | 
510 |    - `InvertColor()`:
511 |       returns: Nothing
512 |       Invert Color
513 | 
514 |    - `GridSettings()`:
515 |       returns: Nothing
516 |       Grid Settings
517 | 
518 |    - `GotoPreviousTab()`:
519 |       returns: Nothing
520 |       Go to Previous tab
521 | 
522 |    - `ScrollCenter()`:
523 |       returns: Nothing
524 |       Scroll to center of canvas
525 | 
526 |    - `GotoPreviousLayer()`:
527 |       returns: Nothing
528 |       Go to Previous Layer
529 | 
530 |    - `GotoPreviousFrameWithSameTag()`:
531 |       returns: Nothing
532 |       Go to Previous Frame with same tag
533 | 
534 |    - `GotoPreviousFrame()`:
535 |       returns: Nothing
536 |       Go to Previous Frame
537 | 
538 |    - `GotoNextTab()`:
539 |       returns: Nothing
540 |       Go to Next Tab
541 | 
542 |    - `AutocropSprite()`:
543 |       returns: Nothing
544 |       Trim Sprite
545 | 
546 |    - `ImportSpriteSheet()`:
547 |       returns: Nothing
548 |       Import Sprite Sheet
549 | 
550 |    - `ShowPixelGrid()`:
551 |       returns: Nothing
552 |       Show Pixel Grid
553 | 
554 |    - `Home()`:
555 |       returns: Nothing
556 |       Home
557 | 
558 |    - `UnlinkCel()`:
559 |       returns: Nothing
560 |       Unlink Cel
561 | 
562 |    - `GotoNextFrameWithSameTag()`:
563 |       returns: Nothing
564 |       Go to Next Frame with same tag
565 | 
566 |    - `CropSprite()`:
567 |       returns: Nothing
568 |       Crop Sprite
569 | 
570 |    - `GotoLastFrame()`:
571 |       returns: Nothing
572 |       Go to Last Frame
573 | 
574 |    - `OpenWithApp()`:
575 |       returns: Nothing
576 |       Open With Associated Application
577 | 
578 |    - `GotoFirstFrame()`:
579 |       returns: Nothing
580 |       Go to First Frame
581 | 
582 |    - `RemoveFrameTag()`:
583 |       returns: Nothing
584 |       Remove Frame Tag
585 | 
586 |    - `NewFrame()`:
587 |       returns: Nothing
588 |       New Frame
589 | 
590 |    - `FullscreenPreview()`:
591 |       returns: Nothing
592 |       Fullscreen Preview
593 | 
594 |    - `SpriteProperties()`:
595 |       returns: Nothing
596 |       Sprite Properties
597 | 
598 |    - `NewLayer()`:
599 |       returns: Nothing
600 |       New Layer
601 | 
602 |    - `FrameProperties()`:
603 |       returns: Nothing
604 |       Frame Properties
605 | 
606 |    - `DeselectMask()`:
607 |       returns: Nothing
608 |       Deselect Mask
609 | 
610 |    - `AlternateTouchbar()`:
611 |       returns: Nothing
612 |       Alternate Touchbar
613 | 
614 |    - `ExportSpriteSheet()`:
615 |       returns: Nothing
616 |       Export Sprite Sheet
617 | 
618 |    - `NewLayerSet()`:
619 |       returns: Nothing
620 |       New Layer Set
621 | 
622 |    - `ModifySelection()`:
623 |       returns: Nothing
624 |       Expand Selection
625 | 
626 |    - `Paste()`:
627 |       returns: Nothing
628 |       Paste
629 | 
630 |    - `DiscardBrush()`:
631 |       returns: Nothing
632 |       Discard Brush
633 | 
634 |    - `BackgroundFromLayer()`:
635 |       returns: Nothing
636 |       BackgroundFromLayer
637 | 
638 |    - `DuplicateView()`:
639 |       returns: Nothing
640 |       Duplicate View
641 | 
642 |    - `About()`:
643 |       returns: Nothing
644 |       About
645 | 
646 |    - `DeveloperConsole()`:
647 |       returns: Nothing
648 |       Developer Console
649 | 
650 |    - `DuplicateSprite()`:
651 |       returns: Nothing
652 |       Duplicate Sprite
653 | 
654 |    - `LinkCels()`:
655 |       returns: Nothing
656 |       Links Cels
657 | 
658 |    - `CopyMerged()`:
659 |       returns: Nothing
660 |       Copy Merged
661 | 
662 |    - `MaskContent()`:
663 |       returns: Nothing
664 |       Mask Content
665 | 
666 |    - `DuplicateLayer()`:
667 |       returns: Nothing
668 |       Duplicate Layer
669 | 
670 |    - `CopyCel()`:
671 |       returns: Nothing
672 |       Copy Cel
673 | 
674 |    - `Refresh()`:
675 |       returns: Nothing
676 |       Refresh
677 | 
678 |    - `Copy()`:
679 |       returns: Nothing
680 |       Copy
681 | 
682 |    - `RemoveFrame()`:
683 |       returns: Nothing
684 |       Remove Frame
685 | 
686 |    - `SetPalette()`:
687 |       returns: Nothing
688 |       Set Palette
689 | 
690 |    - `OpenScriptsFolder()`:
691 |       returns: Nothing
692 |       Open Scripts Folder
693 | 
694 |    - `FlattenLayers()`:
695 |       returns: Nothing
696 |       Flatten Layers
697 | 
698 |    - `Eyedropper()`:
699 |       returns: Nothing
700 |       Eyedropper
701 | 
702 |    - `PaletteSize()`:
703 |       returns: Nothing
704 |       Palette Size
705 | 
706 |    - `ConvolutionMatrix()`:
707 |       returns: Nothing
708 |       Convolution Matrix
709 | 
710 |    - `clearParameters()`:
711 |       returns: Nothing
712 | 
713 |    - `Cut()`:
714 |       returns: Nothing
715 |       Cut
716 | 
717 |    - `PaletteEditor()`:
718 |       returns: Nothing
719 |       Palette Editor
720 | 
721 |    - `RemoveLayer()`:
722 |       returns: Nothing
723 |       Remove Layer
724 | 
725 |    - `Clear()`:
726 |       returns: Nothing
727 |       Clear
728 | 
729 |    - `Exit()`:
730 |       returns: Nothing
731 |       Exit
732 | 
733 |    - `ColorQuantization()`:
734 |       returns: Nothing
735 |       Create Palette from Current Sprite (Color Quantization)
736 | 
737 |    - `AlternateToolbar()`:
738 |       returns: Nothing
739 |       Alternate Toolbar
740 | 
741 |    - `ChangeColor()`:
742 |       returns: Nothing
743 |       Color
744 | 
745 |    - `ChangeBrush()`:
746 |       returns: Nothing
747 |       Brush
748 | 
749 |    - `Cancel()`:
750 |       returns: Nothing
751 |       Cancel Current Operation
752 | 
753 |    - `SwitchColors()`:
754 |       returns: Nothing
755 |       Switch Colors
756 | 
757 |    - `ShowOnionSkin()`:
758 |       returns: Nothing
759 |       Show Onion Skin
760 | 
761 |    - `ChangePixelFormat()`:
762 |       returns: Nothing
763 |       Change Pixel Format
764 | 
765 |    - `ColorCurve()`:
766 |       returns: Nothing
767 |       Color Curve
768 | 
769 |    - `PasteText()`:
770 |       returns: Nothing
771 |       Insert Text
772 | 
773 |    - `CelProperties()`:
774 |       returns: Nothing
775 |       Cel Properties
776 | 
777 |    - `Despeckle()`:
778 |       returns: Nothing
779 |       Despeckle
780 | 
781 |    - `CloseAllFiles()`:
782 |       returns: Nothing
783 |       Close All Files
784 | 
785 |    - `LoadPalette()`:
786 |       returns: Nothing
787 |       Load Palette
788 | 
789 |    - `CanvasSize()`:
790 |       returns: Nothing
791 |       Canvas Size
792 | 
793 |    - `Undo()`:
794 |       returns: Nothing
795 |       Undo
796 | 
797 |    - `LayerVisibility()`:
798 |       returns: Nothing
799 |       Layer Visibility
800 | 
801 |    - `Flip()`:
802 |       returns: Nothing
803 |       Flip Canvas Horizontal
804 | 
805 |    - `Rotate()`:
806 |       returns: Nothing
807 |       Rotate Sprite 0°
808 | 
809 |    - `Redo()`:
810 |       returns: Nothing
811 |       Redo
812 | 
813 |    - `AlternateTimeline()`:
814 |       returns: Nothing
815 |       Alternate Timeline
816 | 
817 |    - `ShowSelectionEdges()`:
818 |       returns: Nothing
819 |       Show Selection Edges
820 | 
821 |    - `GotoFrame()`:
822 |       returns: Nothing
823 |       Go to Frame
824 | 
825 |    - `CloseFile()`:
826 |       returns: Nothing
827 |       Close File
828 | 
829 |    - `ToggleTouchbar()`:
830 |       returns: Nothing
831 |       Toggle Touchbar
832 | 
833 |    - `GotoNextFrame()`:
834 |       returns: Nothing
835 |       Go to Next Frame
836 | 
837 |    - `AdvancedMode()`:
838 |       returns: Nothing
839 |       Advanced Mode
840 | 
841 |    - `setParameter()`:
842 |       returns: Nothing
843 | 
844 | 
845 | 
846 | # [class EntryWidget]
847 | ## Properties:
848 |    - `value`:
849 |    - `maxsize`:
850 |    - `id`:
851 | 
852 | ## No Methods.
853 | 
854 | 
855 | # [class Palette]
856 | ## Properties:
857 |    - `length`:
858 | 
859 | ## Methods:
860 |    - `set()`:
861 |       returns: Nothing
862 | 
863 |    - `get()`:
864 |       returns: Nothing
865 | 
866 | 
867 | 
868 | # [class Document]
869 | ## Properties:
870 |    - `sprite`:
871 | 
872 | ## Methods:
873 |    - `close()`:
874 |       returns: Nothing
875 | 
876 | 
877 | 
878 | # global ColorMode [class ColorMode]
879 | ## Properties:
880 |    - `BITMAP`:
881 |    - `INDEXED`:
882 |    - `GRAYSCALE`:
883 |    - `RGB`:
884 | 
885 | ## No Methods.
886 | 
887 | 
888 | # [class Cel]
889 | ## Properties:
890 |    - `frame`:
891 |    - `image`:
892 |    - `y`:
893 |    - `x`:
894 | 
895 | ## Methods:
896 |    - `setPosition()`:
897 |       returns: Nothing
898 | 
899 | 
900 | 
901 | # global app [class App]
902 | ## Properties:
903 |    - `platform`: read-only. Returns one of: emscripten, windows, macos, android, linux.
904 |    - `version`: read-only. Returns LibreSprite's current version as a string.
905 |    - `activeDocument`: read-only. Returns the currently active Document.
906 |    - `command`: read-only. Returns an object with functions for running commands.
907 |    - `activeSprite`: read-only. Returns the currently active Sprite.
908 |    - `activeLayerNumber`: read-only. Returns the number of the current layer.
909 |    - `activeImage`: read-only, can be null. Returns the current layer/frame's image.
910 |    - `pixelColor`: read-only. Returns an object with functions for color conversion.
911 |    - `activeFrameNumber`: read-only. Returns the number of the currently active animation frame.
912 | 
913 | ## Methods:
914 |    - `launch()`:
915 |       returns: Nothing
916 | 
917 |    - `open()`:
918 |       returns: Nothing
919 |       Opens a document for editing
920 | 
921 |    - `yield(event)`:
922 |      - event: Name of the event to be raised. The default is yield.
923 |       returns: Nothing
924 |       Schedules a yield event on the next frame
925 | 
926 |    - `createDialog()`:
927 |       returns: Nothing
928 |       Creates a dialog window
929 | 
930 |    - `documentation()`:
931 |       returns: Nothing
932 |       Prints this text.
```
Page 1/2FirstPrevNextLast