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 | [](https://cursor.com/install-mcp?name=libresprite&config=JTdCJTIyY29tbWFuZCUyMiUzQSUyMnV2eCUyMGxpYnJlc3ByaXRlLW1jcCUyMiU3RA%3D%3D) 4 | [](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 | [](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 |  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 |  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 |  ``` -------------------------------------------------------------------------------- /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. ```