{"id":13402823,"url":"https://github.com/1j01/jspaint","last_synced_at":"2026-04-10T05:01:18.691Z","repository":{"id":13903792,"uuid":"16602433","full_name":"1j01/jspaint","owner":"1j01","description":"🎨 Classic MS Paint, ＲＥＶＩＶＥＤ + ✨Extras","archived":false,"fork":false,"pushed_at":"2025-01-24T20:02:35.000Z","size":34506,"stargazers_count":7416,"open_issues_count":109,"forks_count":591,"subscribers_count":89,"default_branch":"master","last_synced_at":"2025-04-23T18:56:24.578Z","etag":null,"topics":["app","canvas","drawing","editor","html5","image","image-editing","image-editor","image-manipulation","javascript","jspaint","ms-paint","mspaint","online","paint","painting","remake","retro","web-app","web-application"],"latest_commit_sha":null,"homepage":"https://jspaint.app/about","language":"JavaScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/1j01.png","metadata":{"files":{"readme":"README.md","changelog":"CHANGELOG.md","contributing":"CONTRIBUTING.md","funding":".github/FUNDING.yml","license":"LICENSE.txt","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null},"funding":{"custom":["https://www.paypal.me/IsaiahOdhner"]}},"created_at":"2014-02-07T02:47:16.000Z","updated_at":"2025-04-22T22:00:39.000Z","dependencies_parsed_at":"2023-02-11T23:45:48.037Z","dependency_job_id":"dbb3d479-ce23-4278-bef5-ce4296a5763f","html_url":"https://github.com/1j01/jspaint","commit_stats":null,"previous_names":[],"tags_count":1,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/1j01%2Fjspaint","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/1j01%2Fjspaint/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/1j01%2Fjspaint/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/1j01%2Fjspaint/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/1j01","download_url":"https://codeload.github.com/1j01/jspaint/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":253969248,"owners_count":21992263,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["app","canvas","drawing","editor","html5","image","image-editing","image-editor","image-manipulation","javascript","jspaint","ms-paint","mspaint","online","paint","painting","remake","retro","web-app","web-application"],"created_at":"2024-07-30T19:01:21.336Z","updated_at":"2026-04-10T05:01:18.679Z","avatar_url":"https://github.com/1j01.png","language":"JavaScript","funding_links":["https://www.paypal.me/IsaiahOdhner"],"categories":["Libraries","JavaScript","\u003cimg src=\"media/icons8-windows-11-48.png\" alt=\"logo\" width=\"36\"/\u003e  WINDOWS","html5"],"sub_categories":["Drawingboard"],"readme":"\n# [![](images/icons/32x32.png) JS Paint](https://jspaint.app)\n\nA pixel-perfect web-based MS Paint remake and more... [Try it out!](https://jspaint.app)\nThen join the [Discord server](https://discord.gg/jxQBK3k8tx) to share your art!\n\nJS Paint recreates every tool and menu of MS Paint, and even [little-known features](#did-you-know), to a high degree of fidelity.\n\nIt supports themes, additional file types, and accessibility features like a Dwell Clicker and Speech Recognition.\n\n![Screenshot](images/meta/main-screenshot.png)\n\nAh yes, good old Paint. Not the one with the [ribbons][]\nor the [new skeuomorphic one][Fresh Paint] with the interface that can take up nearly half the screen.\n(And not the even newer [Paint 3D][].)\n\n[ribbons]: https://www.google.com/search?tbm=isch\u0026q=MS+Paint+Windows+7+ribbons \"Google Search: MS Paint Windows 7 ribbons\"\n[Fresh Paint]: https://www.google.com/search?tbm=isch\u0026q=MS+Fresh+Paint \"Google Search: MS Fresh Paint\"\n[Paint 3D]: https://www.microsoft.com/en-us/store/p/paint-3d-preview/9nblggh5fv99\n\nWindows 95, 98, and XP were the golden years of Paint.\nYou had a tool box and a color box, a foreground color and a background color,\nand that was all you needed.\n\nThings were simple.\n\nBut we want to undo more than three actions.\nWe want to edit transparent images.\nWe can't just keep using the old Paint.\n\nSo that's why I'm making JS Paint.\nI want to bring good old Paint into the modern era.\n\n\n#### Current improvements include:\n\n* Open source ([MIT licensed](LICENSE.txt))\n* Cross-platform\n* Mobile friendly\n  * Touch support: use two fingers to pan the view, and pinch to zoom\n  * Click/tap the selected colors area to swap the foreground and background colors\n  * **View \u003e Fullscreen** to toggle fullscreen mode, nice for small screens\n  * **Extras \u003e Quick Undo Button** to add a floating undo button for easier access\n    * (this may be enabled by default in the future for touch devices)\n* Web features\n  * **File \u003e Load From URL...** to open an image from the Web.\n  * **File \u003e Upload to Imgur** to upload the current image to Imgur.\n  * **Paste** supports loading from URLs.\n  * You can create links that will open an image from the Web in JS Paint. For example, this link will start with an isometric grid as a template: \u003chttps://jspaint.app/#load:https://i.imgur.com/zJMrWwb.png\u003e\n  * Rudimentary **multi-user** collaboration support.\n    Start up a session at\n    [jspaint.app/#session:multi-user-test](https://jspaint.app/#session:multi-user-test)\n    and send the link to your friends!\n    It isn't seamless; actions by other users interrupt what you're doing, and visa versa.\n    Sessions are not private, and you may lose your work at any time.\n    If you want better collaboration support, follow the development of [Mopaint](https://github.com/1j01/mopaint).\n* **Extras \u003e Themes** to change the look of the app.\n  * Dark and light variants\n  * Vector tool icons handcrafted to match the pixel art versions, for both Modern and Classic themes\n  * Occult theme, in the spirit of Halloween\n  * Winter theme, with a special color palette including candy cane stripes, and advent calendar style flaps revealing pixel art for each tool\n  * Bubblegum theme, featuring *Business Pink* color scheme and AI-generated icons\n* **Extras \u003e Enlarge UI** to make buttons and menus bigger, for usage with an eye tracker, head tracker, or other course input devices. May also work well for a tablet, but not so much for a phone at the moment.\n* **Extras \u003e Dwell Clicker** to click automatically by hovering in one spot, for usage with an eye tracker or head tracker.\n  * Hovered buttons are highlighted, and the click is performed after a delay.\n  * Supports dragging windows and drawing on the canvas.\n  * With just a webcam, you can try it out with [Enable Viacam](https://eviacam.crea-si.com/) (head tracker), [GazePointer](https://sourceforge.net/projects/gazepointer/) (eye tracker), or the built in [\u003cimg src=\"images/tracky-mouse-16x16.png\" width=\"16\" height=\"16\"\u003e Tracky Mouse](https://trackymouse.js.org/) head tracker using **Extras \u003e Head Tracker**.\n  * This feature can be easily added to other web applications, using the [\u003cimg src=\"images/tracky-mouse-16x16.png\" width=\"16\" height=\"16\"\u003e Tracky Mouse API](https://www.npmjs.com/package/tracky-mouse).\n* **Extras \u003e Speech Recognition** to control the app with your voice.\n  * Select tools and colors (\"fill tool\", \"orange\", etc.)\n  * Pan the view (\"scroll down and to the left\", or \"go southwest\", etc.)\n  * Explore the menus, or activate any menu item without opening the menus first\n  * Interact with windows\n  * Dictate text with the Text tool\n  \u003c!-- (Broken due to no-longer-free service) * Even tell the application to sketch things (for instance, \"draw a house\") --\u003e\n* Create an animated GIF from the current document history.\n  Accessible from the Extras menu or with \u003ckbd\u003eCtrl+Shift+G\u003c/kbd\u003e.\n  It's pretty nifty, you should try it out!\n  You might want to limit the size of the image though.\n* Load and save [many different palette formats](#color-palette-formats) with **Colors \u003e Get Colors** and **Colors \u003e Save Colors**.\n  (I made a library for this: \u003cimg src=\"images/anypalette-logo-128x128.png\" height=\"16\"\u003e [AnyPalette.js](https://github.com/1j01/anypalette.js).)\n  * You can also drag and drop palette files into the app to load.\n\nEditing Features:\n\n* Use Alt+Mousewheel to zoom in and out\n* Edit transparent images! To create a transparent image,\n  go to **Image \u003e Attributes...** and select Transparent,\n  then OK, and then **Image \u003e Clear Image** or use the Eraser tool.\n  Images with *any* translucent pixels will open in Transparent mode.\n* You can crop the image by making a selection while holding \u003ckbd\u003eCtrl\u003c/kbd\u003e\n* Keyboard shortcuts for rotation: \u003ckbd\u003eCtrl+.\u003c/kbd\u003e and \u003ckbd\u003eCtrl+,\u003c/kbd\u003e (\u003ckbd\u003e\u003c\u003c/kbd\u003e and \u003ckbd\u003e\u003e\u003c/kbd\u003e)\n* Rotate by any arbitrary angle in **Image \u003e Flip/Rotate**\n* In **Image \u003e Stretch/Skew**, you can stretch more than 500% at once\n* Zoom to an arbitrary scale in **View \u003e Zoom \u003e Custom...**\n* Zoom to fit the canvas within the window with **View \u003e Zoom \u003e Zoom To Window**\n* Non-contiguous fill: Replace a color in the entire image by holding \u003ckbd\u003eShift\u003c/kbd\u003e when using the fill tool\n\nMiscellaneous Improvements:\n\n* [Vertical Color Box mode](https://jspaint.app/#vertical-color-box-mode), accessible from **Extras \u003e Vertical Color Box**\n* You can use the Text tool at any zoom level (and it previews the exact pixels that will end up on the canvas).\n* Spellcheck is available in the textbox if your browser supports it.\n* Resize handles are easier to grab than in Windows 10's Paint.\n* Omits some Thumbnail view bugs, like the selection showing in the wrong place.\n* Unlimited undos/redos (as opposed to a measly 3 in Windows XP,\n  or a measly 50 in Windows 7)\n* Undo history is *nonlinear*, which means if you undo and do something other than redo, the redos aren't discarded. Instead, a new branch is created in the *history tree*. Jump to any point in history with **Edit \u003e History** or \u003ckbd\u003eCtrl+Shift+Y\u003c/kbd\u003e\n* Automatically keeps a backup of your image. Only one backup per image tho, which doesn't give you a lot of safety. Remember to save with **File \u003e Save** or \u003ckbd\u003eCtrl+S\u003c/kbd\u003e! Manage backups with **File \u003e Manage Storage**.\n\n\u003c!--\nHalf-features:\n\n* When you do **Edit \u003e Paste From...** you can select transparent images.\n  ~~You can even paste a transparent animated GIF and then\n  hold \u003ckbd\u003eShift\u003c/kbd\u003e while dragging the selection to\n  smear it across the canvas *while it animates*!~~\n  Update: This was [due to not-to-spec behavior in Chrome.](https://christianheilmann.com/2014/04/16/browser-inconsistencies-animated-gif-and-drawimage/)\n  I may reimplement this in the future as I really liked this feature.\n* You can open SVG files, though only as a bitmap.\n  (Note: it may open super large, or tiny. There's no option to choose a size when opening.)\n--\u003e\n\n![JS Paint drawing of JS Paint on a phone](images/meta/mobipaint.png)\n\n\n#### Limitations:\n\nA few things with the tools aren't done yet.\nSee [TODO.md](TODO.md#Tools)\n\nFull clipboard support in the web app requires a browser supporting the [Async Clipboard API w/ Images](https://developers.google.com/web/updates/2019/07/image-support-for-async-clipboard), namely Chrome 76+ at the time of writing.\n\nIn other browsers you can still copy with \u003ckbd\u003eCtrl+C\u003c/kbd\u003e, cut with \u003ckbd\u003eCtrl+X\u003c/kbd\u003e, and paste with \u003ckbd\u003eCtrl+V\u003c/kbd\u003e,\nbut data copied from JS Paint can only be pasted into other instances of JS Paint.\nExternal images can be pasted in.\n\n\n## Supported File Formats\n\n### Image Formats\n\n⚠️ Saving as JPEG will introduce artifacts that cause problems when using the Fill tool or transparent selections.\n\n⚠️ Saving in some formats will reduce the number of colors in the image.\n\n💡 Unlike in MS Paint, you can use **Edit \u003e Undo** to revert color or quality reduction from saving.\nThis doesn't undo saving the file, but allows you to then save in a different format with higher quality, using **File \u003e Save As**.\n\n💡 Saving as PNG is recommended as it gives small file sizes while retaining full quality.\n\n| File Extension                | Name                          | Read | Write | Read Palette | Write Palette |\n|-------------------------------|-------------------------------|:----:|:-----:|:------------:|:-------------:|\n| .png                          | [PNG][]                       |  ✅  |  ✅   |      🔜      |               |\n| .bmp, .dib                    | [Monochrome Bitmap][BMP]      |  ✅  |  ✅   |      🔜      |      ✅       |\n| .bmp, .dib                    | [16 Color Bitmap][BMP]        |  ✅  |  ✅   |      🔜      |      ✅       |\n| .bmp, .dib                    | [256 Color Bitmap][BMP]       |  ✅  |  ✅   |      🔜      |      ✅       |\n| .bmp, .dib                    | [24-bit Bitmap][BMP]          |  ✅  |  ✅   |      N/A     |      N/A      |\n| .tif, .tiff, .dng, .cr2, .nef | [TIFF][] (loads first page)   |  ✅  |  ✅   |              |               |\n| .pdf                          | [PDF][] (loads first page)    |  ✅  |       |              |               |\n| .webp                         | [WebP][]                      |  🌐  |  🌐   |              |               |\n| .gif                          | [GIF][]                       |  🌐  |  🌐   |              |               |\n| .jpeg, .jpg                   | [JPEG][]                      |  🌐  |  🌐   |      N/A     |      N/A      |\n| .svg                          | [SVG][] (only default size)   |  🌐  |       |              |               |\n| .ico                          | [ICO][] (only default size)   |  🌐  |       |              |               |\n\nCapabilities marked with 🌐 are currently left up to the browser to support or not.\nIf \"Write\" is marked with 🌐, the format will appear in the file type dropdown but may not work when you try to save.\nFor opening files, see Wikipedia's [browser image format support table][] for more information.\n\nCapabilities marked with 🔜 may be coming soon, and N/A means not applicable.\n\n\"Read Palette\" refers to loading the colors into the Colors box automatically (from an [indexed color][] image),\nand \"Write Palette\" refers to writing an [indexed color][] image.\n\n[PNG]: https://en.wikipedia.org/wiki/Portable_Network_Graphics\n[BMP]: https://en.wikipedia.org/wiki/BMP_file_format\n[TIFF]: https://en.wikipedia.org/wiki/TIFF\n[PDF]: https://en.wikipedia.org/wiki/PDF\n[WebP]: https://en.wikipedia.org/wiki/WebP\n[GIF]: https://en.wikipedia.org/wiki/GIF\n[JPEG]: https://en.wikipedia.org/wiki/JPEG\n[SVG]: https://en.wikipedia.org/wiki/Scalable_Vector_Graphics\n[ICO]: https://en.wikipedia.org/wiki/ICO_(file_format)\n[indexed color]: https://en.wikipedia.org/wiki/Indexed_color\n[browser image format support table]: https://en.wikipedia.org/wiki/Comparison_of_web_browsers#Image_format_support\n\n\n### Color Palette Formats\n\nWith **Colors \u003e Save Colors** and **Colors \u003e Get Colors** you can save and load colors\nin many different formats, for compatibility with a wide range of programs.\n\nIf you want to add extensive palette support to another application, I've made this functionality available as a library:\n\u003cimg src=\"images/anypalette-logo-128x128.png\" height=\"16\"\u003e [AnyPalette.js](https://github.com/1j01/anypalette.js)\n\n| File Extension    | Name                              | Programs                                                                          |   Read  |  Write  |\n|-------------------|-----------------------------------|-----------------------------------------------------------------------------------|:-------:|:-------:|\n| .pal              | [RIFF] Palette                    | [MS Paint] for Windows 95 and Windows NT 4.0                                      |   ✅   |   ✅    |\n| .gpl              | [GIMP][Gimp] Palette              | [Gimp], [Inkscape], [Krita], [KolourPaint], [Scribus], [CinePaint], [MyPaint]     |   ✅   |   ✅    |\n| .aco              | Adobe Color Swatch                | Adobe [Photoshop]                                                                 |   ✅   |   ✅    |\n| .ase              | Adobe Swatch Exchange             | Adobe [Photoshop], [InDesign], and [Illustrator]                                  |   ✅   |   ✅    |\n| .txt              | [Paint.NET] Palette               | [Paint.NET]                                                                       |   ✅   |   ✅    |\n| .act              | Adobe Color Table                 | Adobe [Photoshop] and [Illustrator]                                               |   ✅   |   ✅    |\n| .pal, .psppalette | [Paint Shop Pro] Palette          | [Paint Shop Pro] (Jasc Software / Corel)                                          |   ✅   |   ✅    |\n| .hpl              | [Homesite] Palette                | Allaire [Homesite] / Macromedia [ColdFusion]                                      |   ✅   |   ✅    |\n| .cs               | ColorSchemer                      | ColorSchemer Studio                                                               |   ✅   |         |\n| .pal              | [StarCraft] Palette               | [StarCraft]                                                                       |   ✅   |   ✅    |\n| .wpe              | [StarCraft] Terrain Palette       | [StarCraft]                                                                       |   ✅   |   ✅    |\n| .sketchpalette    | [Sketch] Palette                  | [Sketch]                                                                          |   ✅   |   ✅    |\n| .spl              | [Skencil] Palette                 | [Skencil] (formerly called Sketch)                                                |   ✅   |   ✅    |\n| .soc              | StarOffice Colors                 | [StarOffice], [OpenOffice], [LibreOffice]                                         |   ✅   |   ✅    |\n| .colors           | KolourPaint Color Collection      | [KolourPaint]                                                                     |   ✅   |   ✅    |\n| .colors           | Plasma Desktop Color Scheme       | [KDE] Plasma Desktop                                                              |   ✅   |         |\n| .theme            | Windows Theme                     | [Windows] Desktop                                                                 |   ✅   |         |\n| .themepack        | Windows Theme                     | [Windows] Desktop                                                                 |   ✅   |         |\n| .css, .scss, .styl| Cascading StyleSheets             | Web browsers / web pages                                                          |   ✅   |   ✅    |\n| .html, .svg, .js  | any text files with CSS colors    | Web browsers / web pages                                                          |   ✅   |         |\n\n[RIFF]: https://en.wikipedia.org/wiki/Resource_Interchange_File_Format\n[MS Paint]: https://en.wikipedia.org/wiki/Microsoft_Paint\n[Paint.NET]: https://www.getpaint.net/\n[Paint Shop Pro]: https://www.paintshoppro.com/en/\n[StarCraft]: https://en.wikipedia.org/wiki/StarCraft\n[Homesite]: https://en.wikipedia.org/wiki/Macromedia_HomeSite\n[ColdFusion]: https://en.wikipedia.org/wiki/Adobe_ColdFusion\n[StarOffice]: https://en.wikipedia.org/wiki/StarOffice\n[OpenOffice]: https://www.openoffice.org/\n[LibreOffice]: https://www.libreoffice.org/\n[Sketch]: https://www.sketchapp.com/\n[Skencil]: https://skencil.org/\n[Photoshop]: https://www.adobe.com/products/photoshop.html\n[InDesign]: https://www.adobe.com/products/indesign.html\n[Illustrator]: https://www.adobe.com/products/illustrator.html\n[Gimp]: https://www.gimp.org/\n[Inkscape]: https://inkscape.org/en/\n[Krita]: https://www.calligra.org/krita/\n[KolourPaint]: http://kolourpaint.org/\n[KDE]: https://kde.org/\n[Windows]: https://en.wikipedia.org/wiki/Microsoft_Windows\n[Scribus]: https://www.scribus.net/\n[CinePaint]: http://www.cinepaint.org/\n[MyPaint]: http://mypaint.org/\n\n\n## Did you know?\n\n* There's a black and white mode with *patterns* instead of colors in the palette,\n  which you can get to from **Image \u003e Attributes...**\n\n* You can drag the color box and tool box around if you grab them by the right place.\n  You can even drag them out into little windows.\n  You can dock the windows back to the side by double-clicking on their title bars.\n\n* In addition to the left-click foreground color and the right-click background color,\n  there's a third color you can access by holding \u003ckbd\u003eCtrl\u003c/kbd\u003e while you draw.\n  It starts out with no color so you'll need to hold \u003ckbd\u003eCtrl\u003c/kbd\u003e and select a color first.\n  The fancy thing about this color slot is you can\n  press and release \u003ckbd\u003eCtrl\u003c/kbd\u003e to switch colors *while drawing*.\n\n* You can apply image transformations like Flip/Rotate, Stretch/Skew or Invert (in the Image menu) either to the whole image or to a selection.\n  Try scribbling with the Free-Form Select tool and then doing **Image \u003e Invert**\n\n* These Tips and Tricks from [a tutorial for MS Paint](https://www.albinoblacksheep.com/tutorial/mspaint)\n  also work in JS Paint:\n\n\t* [x] Brush Scaling (\u003ckbd\u003e+\u003c/kbd\u003e \u0026 \u003ckbd\u003e-\u003c/kbd\u003e on the number pad to adjust brush size)\n\t* [x] \"Custom Brushes\" (hold \u003ckbd\u003eShift\u003c/kbd\u003e and drag the selection to smear it)\n\t* [x] The 'Stamp' \"Tool\" (hold \u003ckbd\u003eShift\u003c/kbd\u003e and click the selection to stamp it)\n\t* [x] Image Scaling (\u003ckbd\u003e+\u003c/kbd\u003e \u0026 \u003ckbd\u003e-\u003c/kbd\u003e on the number pad to scale the selection by factors of 2)\n\t* [x] Color Replacement (right mouse button with Eraser to selectively replace the foreground color with the background color)\n\t* [x] The Grid (\u003ckbd\u003eCtrl+G\u003c/kbd\u003e \u0026 Zoom to 4x+)\n\t* [x] Quick Undo (Pressing a second mouse button cancels the action you were performing.\n\t      I also made it redoable, in case you do it by accident!)\n\t* [ ] Scroll Wheel Bug (Hmm, let's maybe not recreate this?)\n\n\n## Desktop App\n\n### PWA\n\nJS Paint can be installed as a Progressive Web App (PWA), although it doesn't work offline yet.\nLook for the install prompt in the address bar.\n\nPWA features:\n- No address bar; middle-ground between web and native\n- Cross-platform (macOS, Windows, Linux, Android, iOS)\n- Basic file integration:\n\t- File \u003e Open\n\t- File \u003e Save downloads the file after asking for a filename and format\n\t- Drag and drop files onto the window to open them\n\nMissing features:\n\n- \u003cdetails\u003e\u003csummary\u003eDirectly saving to files is implemented but not enabled currently.\u003c/summary\u003eI was concerned about data loss for two reasons: 1. the change in behavior of File \u003e Save / Ctrl+S from effectively acting as Save As to overwriting files directly, although I made a warning dialog for this, with a don't show again option; 2. there was a bad bug with saved files ending up completely empty (zero bytes), which I don't know if was a bug in my code or in Chrome.\u003c/details\u003e\n- \u003cdetails\u003e\u003csummary\u003eOffline support is not implemented.\u003c/summary\u003eI've taken a few stabs at this, and \u003ca href=\"https://github.com/1j01/jspaint/pull/144\"\u003eI'm not the only one\u003c/a\u003e, but there are some huge caveats, such as the development server not being able to live-reload without disabling the service worker.\u003c/details\u003e\n\n### Electron\n\nI've also built it into a desktop app with [Electron][] and [Electron Forge][].\nYou can download it from the [releases page][].\n\n![JS Paint running as a desktop app on macOS](images/meta/electron-app-screenshot-mac.png)\n\nElectron app features:\n- Native-like experience (runs in a window with no address bar)\n- Cross-platform (macOS, Windows, Linux)\n- Clipboard support\n- Files can be opened in various ways:\n  - **File \u003e Open**\n  - Drag and drop onto window\n  - Drag and drop onto dock icon on macOS\n  - Drag and drop onto desktop shortcut\n  - **Right Click \u003e Open With** in file manager (macOS and Linux)\n    - On Windows, you can manually paste the path to the executable into the Open With dialog, which you can find by right clicking the app in the taskbar, then right clicking the app's name, and selecting Properties. In the Shortcut tab, the Target field is the path to the executable. Once you open the app in this way, the app will show up in the Open With list, and if you select \"Always\", it will become the default app for that file type.\n  - Command line: type `jspaint path/to/file.png` in the terminal\n- **File \u003e Save** will save directly to the file\n- **File \u003e Set As Wallpaper (Tiled)** and **File \u003e Set As Wallpaper (Centered)**\n- On macOS, an icon representing the currently open file is shown in the titlebar. You can drag this icon into other applications, for example to include the image you're editing in an email. The icon is dimmed while there are unsaved changes.\n\n\u003cdetails\u003e\u003csummary\u003eElectron app limitations\u003c/summary\u003e\n\n- Basics:\n  - Execution is blocked by default on Mac and Windows\n    - On macOS you need to Ctrl+click the file and then say Open\n    - On Windows, you need to say \"More info\" and then \"Run anyway\"\n    - I would need to pay a fee for code signing to avoid this. It's basically *security by extortion*.\n  - There are no automatic updates. Apparently I would need to pay a fee for code signing to get this free service.\n    - That said, **Help \u003e About Paint** can tell you if JS Paint is out of date, at least in terms of news updates.\n  - Electron is out of date. It may, for instance, contain image decoding vulnerabilities that have since been fixed. However, I've taken precautions to sandbox the app and restrict write access to a list of files explicitly opened in the app, the list being controlled by the main process, separate from the renderer process which would handle image decoding.\n  - Only a single editor window can be opened at once.\n  - The File menu's recent files list is not implemented, nor are [OS-specific jump menus](https://www.electronjs.org/docs/latest/tutorial/recent-documents).\n- Minor details:\n  - A very confusing message is shown if you edit a document before clicking an Open link in the Manage Storage dialog.\n  - WebGL error messages tell you to refresh without offering a way to reload; also, calling the app a web page feels unpolished\n  - The File \u003e Open dialog does not have an All Files (\\*.\\*) option, and the list of file types supported is not exhaustive; for example, AVIF images can be loaded but only by drag and drop\n  - Drag and drop shows two \"Save changes to X?\" dialogs on top of each other?\n- I'm not sure if all of these are still issues, need to retest them:\n  - Quit doesn't exit app completely, only closes the window if it's open... intended behavior? shouldn't right click \u003e Quit really quit? https://stackoverflow.com/questions/44316306/how-to-quit-electron-app-on-mac\n  - Quit doesn't show/focus window when save changes prompt is shown on maybe mac/linux\n  - Ctrl+C doesn't exit on mac/linux https://github.com/electron/electron/issues/5273\n    - this is because of `editor_window.on(\"close\")` calling `preventDefault` and may be a feature but needs to show/focus the window\n    - https://stackoverflow.com/questions/75362687/electron-js-processes-do-not-exit-on-app-quit\n  - Opening an SVG file also isn't working via command line argument (dragging onto the shortcut in File Explorer) even though dragging and dropping into the window works.  \n    - Seems to load load SVG as a palette... Is this what I was running into?\n\n\u003c/details\u003e\n\n[Electron]: https://electronjs.org/\n[Electron Forge]: https://electronforge.io/\n[releases page]: https://github.com/1j01/jspaint/releases/\n\n\n## Development Setup\n\n[Clone the repo.](https://help.github.com/articles/cloning-a-repository/)\n\nInstall [Node.js][] if you don't have it, then open up a command prompt / terminal in the project directory.\n\n### Quality Assurance\n\nRun `npm run lint` to check for spelling errors, type errors, code style issues, and other problems.\n\nRun `npm run format` to automatically fix formatting issues, or `npx eslint --fix` to fix all auto-fixable issues.\n\nThe formatting rules are configured for compatibility with VS Code's built-in formatter.\n\nRun `npm test` to run browser-based tests with Cypress. (It's slow to start up and run tests, unfortunately.)\n\nRun `npm run accept` to accept any visual changes.\nThis unfortunately re-runs all the tests, rather than accepting results of the previous test, so you could end up with different results than the previous test.\nIf you use [GitHub Desktop](https://desktop.github.com/), you can view diffs of images, in four different modes.\n\nTo open the Cypress UI, first run `npm run test:start-server`, then concurrently `npm run cy:open`\n\n### Web App (https://jspaint.app)\n\nAfter you've installed dependencies with `npm i`,\nuse `npm run dev` to start a live-reloading server.\n\nMake sure any layout-important styles go in `layout.css`.\nWhen updating `layout.css`, a right-to-left version of the stylesheet is generated, using [RTLCSS](https://rtlcss.com/).  \nYou should test the RTL layout by changing the language to Arabic or Hebrew.\nGo to **Extras \u003e Language \u003e العربية** or **עברית**.  \nSee [Control Directives](https://rtlcss.com/learn/usage-guide/control-directives/) for how to control the RTL layout.\n\nThere is a VS Code launch task for attaching to Chrome for debugging.\nSee `.vscode/launch.json` for usage instructions.\n\n### Desktop App (Electron)\n\n- Install dependencies with `npm i`\n- Start the electron app with `npm run electron:start`\n\n[electron-debug][] is included, so you can use \u003ckbd\u003eF5\u003c/kbd\u003e/\u003ckbd\u003eCtrl+R\u003c/kbd\u003e to reload and \u003ckbd\u003eF12\u003c/kbd\u003e/\u003ckbd\u003eCtrl+Shift+I\u003c/kbd\u003e to open the devtools.\n\nYou can build for production with `npm run electron:make`\n\nThere is a VS Code launch task for debugging the Electron main process.\nFor the renderer process, you can use the embedded Chrome DevTools.\n\nIt is also possible to run JS Paint without Electron Forge, using `npx jspaint`; this is nice for testing the CLI, e.g. `npx jspaint --help` or `npx jspaint images/98.js.org.svg`\n\nNew releases are built in GitHub Actions. See [Release Process](./release-process.md).\n\n[Live Server]: https://github.com/1j01/live-server\n[Node.js]: https://nodejs.org/\n[electron-debug]: https://github.com/sindresorhus/electron-debug\n\n## Deployment\n\nJS Paint can be deployed using a regular web server.\n\nNothing needs to be compiled.\n\n### CORS proxy\n\nOptionally, you can set up a [CORS Anywhere](https://github.com/Rob--W/cors-anywhere) server, for loading images from the web, if you paste a URL into JS Paint, or use the `#load:\u003cURL\u003e` feature with images that are not on the same domain.\n\nBy default it will use a [CORS Anywhere instance](https://jspaint-cors-proxy.herokuapp.com) set up to work with [jspaint.app](https://jspaint.app).\n\nIt is hosted for free on [Heroku](https://www.heroku.com/),\nand you can set up your own instance and configure it to work with your own domain.\n\nYou'll have to find and replace `https://jspaint-cors-proxy.herokuapp.com` with your own instance URL.\n\n\n### Multiplayer Support\n\nMultiplayer support currently relies on Firebase,\nwhich is not open source software.\n\nYou could create a [Firebase Realtime Database](https://firebase.google.com/docs/database/web/start) instance and edit JS Paint's `sessions.js` to point to it,\nreplacing the `config` passed to `initializeApp` with the config from the Firebase Console when you set up a Web App.\n\nBut the multiplayer mode is very shoddy so far.\nIt should be replaced with something open source, more secure, more efficient, and more robust.\n\n## Embed in your website\n\n### Simple\n\nAdd this to your HTML:\n\n```html\n\u003ciframe src=\"https://jspaint.app\" width=\"100%\" height=\"100%\"\u003e\u003c/iframe\u003e\n```\n\n#### Start with an image\n\nYou can have it load an image from a URL by adding `#load:\u003cURL\u003e` to the URL.\n\n```html\n\u003ciframe src=\"https://jspaint.app#load:https://jspaint.app/favicon.ico\" width=\"100%\" height=\"100%\"\u003e\u003c/iframe\u003e\n```\n\n### Advanced\n\nIf you want to control JS Paint, how it saves/loads files, or access the canvas directly,\nthere is an unstable API.\n\nFirst you need to [clone the repo](https://help.github.com/articles/cloning-a-repository/),\nso you can point an `iframe` to your local copy.\n\nThe local copy of JS Paint has to be hosted on the same web server as the containing page, or more specifically, it has to share the [same origin](https://en.wikipedia.org/wiki/Same-origin_policy).\n\nHaving a local copy also means things won't break any time the API changes.\n\nIf JS Paint is cloned to a folder called `jspaint`, which lives in the same folder as the page you want to embed it in, you can use this:\n\n```html\n\u003ciframe src=\"jspaint/index.html\" id=\"jspaint-iframe\" width=\"100%\" height=\"100%\"\u003e\u003c/iframe\u003e\n```\n\nIf it lives somewhere else, you may need to add `../` to the start of the path, to go up a level. For example, `src=\"../../apps/jspaint/index.html\"`.\nYou can also use an absolute URL, like `src=\"https://example.com/cool-apps/jspaint/index.html\"`.\n\n#### Changing how files are saved/loaded\n\nYou can override the file saving and opening dialogs\nwith JS Paint's `systemHooks` API.\n\n```html\n\u003cscript\u003e\nvar iframe = document.getElementById(\"jspaint-iframe\");\nvar jspaint = iframe.contentWindow;\n// Wait for systemHooks object to exist (the iframe needs to load)\nwaitUntil(()=\u003e jspaint.systemHooks, 500, ()=\u003e {\n\t// Hook in\n\tjspaint.systemHooks.showSaveFileDialog = async ({ formats, defaultFileName, defaultPath, defaultFileFormatID, getBlob, savedCallbackUnreliable, dialogTitle }) =\u003e { ... };\n\tjspaint.systemHooks.showOpenFileDialog = async ({ formats }) =\u003e { ... };\n\tjspaint.systemHooks.writeBlobToHandle = async (save_file_handle, blob) =\u003e { ... };\n\tjspaint.systemHooks.readBlobFromHandle = async (file_handle) =\u003e { ... };\n});\n// General function to wait for a condition to be met, checking at regular intervals\nfunction waitUntil(test, interval, callback) {\n\tif (test()) {\n\t\tcallback();\n\t} else {\n\t\tsetTimeout(waitUntil, interval, test, interval, callback);\n\t}\n}\n\u003c/script\u003e\n```\n\nA [Blob](https://developer.mozilla.org/en-US/docs/Web/API/Blob) represents the contents of a file in memory.\n\nA file handle is anything that can identify a file.\nYou get to own this concept, and define how to identify files.\nIt could be anything from an index into an array, to a Dropbox file ID, to an IPFS URL, to a file path.\nIt can be any type, or maybe it needs to be a string, I forget.\n\nOnce you have a concept of a file handle, you can implement file pickers using the system hooks, and functions to read and write files.\n\n| Command | Hooks Used |\n| ------- | ---------- |\n| **File \u003e Save As** | [`systemHooks.showSaveFileDialog`][], then when a file is picked, [`systemHooks.writeBlobToHandle`][] |\n| **File \u003e Open** | [`systemHooks.showOpenFileDialog`][], then when a file is picked, [`systemHooks.readBlobFromHandle`][] |\n| **File \u003e Save** | [`systemHooks.writeBlobToHandle`][] (or same as **File \u003e Save As** if there's no file open yet) |\n| **Edit \u003e Copy To** | [`systemHooks.showSaveFileDialog`][], then when a file is picked, [`systemHooks.writeBlobToHandle`][] |\n| **Edit \u003e Paste From** | [`systemHooks.showOpenFileDialog`][], then when a file is picked, [`systemHooks.readBlobFromHandle`][] |\n| **File \u003e Set As Wallpaper (Tiled)** | [`systemHooks.setWallpaperTiled`][] if defined, else [`systemHooks.setWallpaperCentered`][] if defined, else same as **File \u003e Save As** |\n| **File \u003e Set As Wallpaper (Centered)** | [`systemHooks.setWallpaperCentered`][] if defined, else same as **File \u003e Save As** |\n| **Extras \u003e Render History As GIF** | Same as **File \u003e Save As** |\n| **Colors \u003e Save Colors** | Same as **File \u003e Save As** |\n| **Colors \u003e Get Colors** | Same as **File \u003e Open** |\n\n#### Loading a file initially\n\nTo start the app with a file loaded for editing,\nwait for the app to load, then call [`systemHooks.readBlobFromHandle`][] with a file handle, and tell the app to load that file blob.\n\n```js\nconst file_handle = \"initial-file-to-load\";\nsystemHooks.readBlobFromHandle(file_handle).then(file =\u003e {\n\tif (file) {\n\t\tcontentWindow.open_from_file(file, file_handle);\n\t}\n}, (error) =\u003e {\n\t// Note: in some cases, this handler may not be called, and instead an error message is shown by readBlobFromHandle directly.\n\tcontentWindow.show_error_message(`Failed to open file ${file_handle}`, error);\n});\n```\n\nThis is clumsy, and in the future there may be a query string parameter to load an initial file by its handle.\n(Note to self: it will need to wait for your system hooks to be registered, somehow.)\n\nThere's already a query string parameter to load from a URL:\n\n```html\n\u003ciframe src=\"https://jspaint.app?load:SOME_URL_HERE\"\u003e\u003c/iframe\u003e\n```\n\nBut this won't set up the file handle for saving.\n\n\n#### Integrating Set as Wallpaper\n\nYou can define two functions to set the wallpaper, which will be used by **File \u003e Set As Wallpaper (Tiled)** and **File \u003e Set As Wallpaper (Centered)**.\n\n- [`systemHooks.setWallpaperTiled`][]` = (canvas) =\u003e { ... };`\n- [`systemHooks.setWallpaperCentered`][]` = (canvas) =\u003e { ... };`\n\nIf you define only [`systemHooks.setWallpaperCentered`][], JS Paint will attempt to guess your screen's dimensions and tile the image, applying it by calling your [`systemHooks.setWallpaperCentered`][] function.\n\nIf you don't specify [`systemHooks.setWallpaperCentered`][], JS Paint will default to saving a file (`\u003coriginal file name\u003e wallpaper.png`) using [`systemHooks.showSaveFileDialog`][] and [`systemHooks.writeBlobToHandle`][].\n\nHere's a full example supporting a persistent custom wallpaper as a background on the containing page:\n\n```js\nconst wallpaper = document.querySelector(\"body\"); // or some other element\n\njspaint.systemHooks.setWallpaperCentered = (canvas) =\u003e {\n\tcanvas.toBlob((blob) =\u003e {\n\t\tsetDesktopWallpaper(blob, \"no-repeat\", true);\n\t});\n};\njspaint.systemHooks.setWallpaperTiled = (canvas) =\u003e {\n\tcanvas.toBlob((blob) =\u003e {\n\t\tsetDesktopWallpaper(blob, \"repeat\", true);\n\t});\n};\n\nfunction setDesktopWallpaper(file, repeat, saveToLocalStorage) {\n\tconst blob_url = URL.createObjectURL(file);\n\twallpaper.style.backgroundImage = `url(${blob_url})`;\n\twallpaper.style.backgroundRepeat = repeat;\n\twallpaper.style.backgroundPosition = \"center\";\n\twallpaper.style.backgroundSize = \"auto\";\n\tif (saveToLocalStorage) {\n\t\tconst fileReader = new FileReader();\n\t\tfileReader.onload = () =\u003e {\n\t\t\tlocalStorage.setItem(\"wallpaper-data-url\", fileReader.result);\n\t\t\tlocalStorage.setItem(\"wallpaper-repeat\", repeat);\n\t\t};\n\t\tfileReader.onerror = () =\u003e {\n\t\t\tconsole.error(\"Error reading file (for setting wallpaper)\", file);\n\t\t};\n\t\tfileReader.readAsDataURL(file);\n\t}\n}\n\n// Initialize the wallpaper from localStorage, if it exists\ntry {\n\tconst wallpaper_data_url = localStorage.getItem(\"wallpaper-data-url\");\n\tconst wallpaper_repeat = localStorage.getItem(\"wallpaper-repeat\");\n\tif (wallpaper_data_url) {\n\t\tfetch(wallpaper_data_url).then(response =\u003e response.blob()).then(file =\u003e {\n\t\t\tsetDesktopWallpaper(file, wallpaper_repeat, false);\n\t\t});\n\t}\n} catch (error) {\n\tconsole.error(error);\n}\n```\n\nIt's a little bit recursive, sorry; it could probably be done simpler.\nLike by just using data URLs. (Actually, I think I wanted to use blob URLs just so that it doesn't bloat the DOM inspector with a super long URL. Which is really a devtools UX bug. Maybe they've improved this?)\n\n#### Specifying the canvas size\n\nYou can load a file that has the desired dimensions.\nThere's no special API for this at the moment.\n\nSee [Loading a file initially](#loading-a-file-initially).\n\n#### Specifying the theme\n\nYou could change the theme programmatically:\n\n```js\nvar iframe = document.getElementById(\"jspaint-iframe\");\nvar jspaint = iframe.contentWindow;\njspaint.set_theme(\"modern.css\");\n```\nbut this will break the user preference.\n\nThe **Extras \u003e Themes** menu will still work, but the preference won't persist when reloading the page.\n\nIn the future there may be a query string parameter to specify the default theme. You could also fork jspaint to change the default theme.\n\n#### Specifying the language\n\nSimilar to the theme, you can try to change the language programmatically:\n\n```js\nvar iframe = document.getElementById(\"jspaint-iframe\");\nvar jspaint = iframe.contentWindow;\njspaint.set_language(\"ar\");\n```\nbut this will actually **ask the user to reload the application** to change languages.\n\nThe **Extras \u003e Language** menu will still work, but the user will be bothered to change the language every time they reload the page.\n\nIn the future there may be a query string parameter to specify the default language. You could also fork jspaint to change the default language.\n\n#### Adding custom menus\n\nNot supported yet.\nYou could fork jspaint and add your own menus.\n\n#### Accessing the canvas directly\n\nWith access to the canvas, you can implement a live preview of your drawing, for example updating a texture in a game engine in realtime.\n\n```js\nvar iframe = document.getElementById(\"jspaint-iframe\");\n// contentDocument here refers to the webpage loaded in the iframe, not the image document loaded in jspaint.\n// We're just reaching inside the iframe to get the canvas.\nvar canvas = iframe.contentDocument.querySelector(\".main-canvas\");\n```\n\nIt's recommended **not** to use this for loading a document, as it won't change the document title, or reset undo/redo history, among other things.\nInstead use [`open_from_file`][].\n\n#### Performing custom actions\n\nIf you want to make buttons or other UI to do things to the document, you should (probably) make it undoable.\nIt's very easy, just wrap your action in a call to [`undoable`][].\n\n```js\nvar iframe = document.getElementById(\"jspaint-iframe\");\nvar jspaint = iframe.contentWindow;\nvar icon = new Image();\nicon.src = \"some-folder/some-image-15x11-pixels.png\";\njspaint.undoable({\n\tname: \"Seam Carve\",\n\ticon: icon, // optional\n}, function() {\n\t// do something to the canvas\n});\n```\n\n#### \u003ca href=\"#systemHooks.showSaveFileDialog\" id=\"systemHooks.showSaveFileDialog\"\u003easync function `systemHooks.showSaveFileDialog({ formats, defaultFileName, defaultPath, defaultFileFormatID, getBlob, savedCallbackUnreliable, dialogTitle })`\u003c/a\u003e\n[`systemHooks.showSaveFileDialog`]: #systemHooks.showSaveFileDialog\n\nDefine this function to override the default save dialog.\nThis is used both for saving images, as well as palette files, and animations.\n\nArguments:\n- `formats`: an array of objects representing types of files, with the following properties:\n\t- `formatID`: a string that uniquely identifies the format (may be the same as `mimeType`)\n\t- `mimeType` (optional): the file format's designated [media type](https://en.wikipedia.org/wiki/Media_type), e.g. `\"image/png\"` (palette formats do not have this property)\n\t- `name`: the file format's name, e.g. `\"WebP\"`\n\t- `nameWithExtensions`: the file format's name followed by a list of extensions, e.g. `\"TIFF (*.tif;*.tiff)\"`\n\t- `extensions`: an array of file extensions, excluding the dot, with the preferred extension first, e.g. `[\"bmp\", \"dib\"]`\n- `defaultFileName` (optional): a suggested file name, e.g. `\"Untitled.png\"` or the name of an open document.\n- `defaultPath` (optional): a file handle for a document that was opened, so you can save to the same folder easily. Misnomer: this may not be a path, it depends on how you define file handles.\n- `defaultFileFormatID` (optional): the `formatID` of a file format to select by default.\n- `async function getBlob(formatID)`: a function you call to get a file in one of the supported formats. It takes a `formatID` and returns a `Promise` that resolves with a `Blob` representing the file contents to save.\n- `function savedCallbackUnreliable({ newFileName, newFileFormatID, newFileHandle, newBlob })` (optional): a function you call when the user has saved the file. The `newBlob` should come from `getBlob(newFileFormatID)`.\n- `dialogTitle` (optional): a title for the save dialog.\n\nNote the inversion of control here:\nJS Paint calls your `systemHooks.showSaveFileDialog` function, and then you call JS Paint's `getBlob` function.\nOnce `getBlob` resolves, you can call the `savedCallbackUnreliable` function which is defined by JS Paint.\n(Hopefully I can clarify this in the future.)\n\nAlso note that this function is responsible for saving the file, not just picking a save location.\nYou may reuse your `systemHooks.writeBlobToHandle` function if it's helpful.\n\n#### \u003ca href=\"#systemHooks.showOpenFileDialog\" id=\"systemHooks.showOpenFileDialog\"\u003easync function `systemHooks.showOpenFileDialog({ formats })`\u003c/a\u003e\n[`systemHooks.showOpenFileDialog`]: #systemHooks.showOpenFileDialog\n\nDefine this function to override the default open dialog.\nThis is used for opening images and palettes.\n\nArguments:\n- `formats`: same as `systemHooks.showSaveFileDialog`\n\nNote that this function is responsible for loading the contents of the file, not just picking a file.\nYou may reuse your `systemHooks.readBlobFromHandle` function if it's helpful.\n\n#### \u003ca href=\"#systemHooks.writeBlobToHandle\" id=\"systemHooks.writeBlobToHandle\"\u003easync function `systemHooks.writeBlobToHandle(fileHandle, blob)`\u003c/a\u003e\n[`systemHooks.writeBlobToHandle`]: #systemHooks.writeBlobToHandle\n\nDefine this function to tell JS Paint how to save a file.\n\nArguments:\n- `fileHandle`: a file handle, as defined by your system, representing the file to write to.\n- `blob`: a `Blob` representing the file contents to save.\n\nReturns:\n- `Promise` that resolves with `true` if the file was definitely saved successfully, `false` if an error occurred or the user canceled, or `undefined` if it is not known whether the file was saved successfully, as is the case with file downloading with `\u003ca href=\"...\" download=\"...\"\u003e`. The promise should not reject; errors should be handled by showing an error message and returning `false`.\n\n#### \u003ca href=\"#systemHooks.readBlobFromHandle\" id=\"systemHooks.readBlobFromHandle\"\u003easync function `systemHooks.readBlobFromHandle(fileHandle)`\u003c/a\u003e\n[`systemHooks.readBlobFromHandle`]: #systemHooks.readBlobFromHandle\n\nDefine this function to tell JS Paint how to load a file.\n\nArguments:\n- `fileHandle`: a file handle, as defined by your system, representing the file to read from.\n\n#### \u003ca href=\"#systemHooks.setWallpaperTiled\" id=\"systemHooks.setWallpaperTiled\"\u003efunction `systemHooks.setWallpaperTiled(canvas)`\u003c/a\u003e\n[`systemHooks.setWallpaperTiled`]: #systemHooks.setWallpaperTiled\n\nDefine this function to tell JS Paint how to set the wallpaper. See [Integrating Set as Wallpaper](#integrating-set-as-wallpaper) for an example.\n\nArguments:\n- `canvas`: a `HTMLCanvasElement` with the image to set as the wallpaper.\n\n#### \u003ca href=\"#systemHooks.setWallpaperCentered\" id=\"systemHooks.setWallpaperCentered\"\u003efunction `systemHooks.setWallpaperCentered(canvas)`\u003c/a\u003e\n[`systemHooks.setWallpaperCentered`]: #systemHooks.setWallpaperCentered\n\nDefine this function to tell JS Paint how to set the wallpaper. See [Integrating Set as Wallpaper](#integrating-set-as-wallpaper) for an example.\n\nArguments:\n- `canvas`: a `HTMLCanvasElement` with the image to set as the wallpaper.\n\n#### \u003ca href=\"#undoable\" id=\"undoable\"\u003efunction `undoable({ name, icon }, actionFunction)`\u003c/a\u003e\n[`undoable`]: #undoable\n\nUse this to make an action undoable.\n\nThis function takes a snapshot of the canvas, and some other state, and then calls the `actionFunction` function.\nIt creates an entry in the history so it can be undone.\n\nArguments:\n- `name`: a name for the action, e.g. `\"Brush\"` or `\"Rotate Image 270°\"`\n- `icon` (optional): an `Image` to display in the History window. It is recommended to be 15x11 pixels.\n- `actionFunction`: a function that takes no arguments, and modifies the canvas.\n\n#### \u003ca href=\"#show_error_message\" id=\"show_error_message\"\u003efunction `show_error_message(message, [error])`\u003c/a\u003e\n[`show_error_message`]: #show_error_message\n\nUse this to show an error message dialog box, optionally with expandable error details.\n\nArguments:\n- `message`: plain text to show in the dialog box.\n- `error` (optional): an `Error` object to show in the dialog box, collapsed by default in a \"Details\" expandable section.\n\n#### \u003ca href=\"#open_from_file\" id=\"open_from_file\"\u003efunction `open_from_file(blob, source_file_handle)`\u003c/a\u003e\n[`open_from_file`]: #open_from_file\n\nUse this to load a file into the app.\n\nArguments:\n- `blob`: a `Blob` object representing the file to load.\n- `source_file_handle`: a *corresponding* file handle for the file, as defined by your system.\n\nSorry for the quirky API.\nThe API is new, and parts of it have not been designed at all. This was just a hack that I came to depend on, reaching into the internals of JS Paint to load a file.\nI decided to document it as the first version of the API, since I'll want a changelog when upgrading my usage of it anyways.\n\n#### \u003ca href=\"#set_theme\" id=\"set_theme\"\u003efunction `set_theme(theme_file_name)`\u003c/a\u003e\n[`set_theme`]: #set_theme\n\nUse this to change the look of the application.\n\nArguments:\n- `theme_file_name`: the name of the theme file to load, one of:\n\t- `\"classic.css\"`: the Windows98 theme.\n\t- `\"dark.css\"`: the Dark theme.\n\t- `\"modern.css\"`: the Modern theme.\n\t- `\"winter.css\"`: the festive Winter theme.\n\t- `\"occult.css\"`: a Satanic theme.\n\n#### \u003ca href=\"#set_language\" id=\"set_language\"\u003efunction `set_language(language_code)`\u003c/a\u003e\n[`set_language`]: #set_language\n\nYou can kind of use this to change the language of the application. But actually it will show a prompt to the user to change the language, because the application needs to reload to apply the change.\nAnd if that dialog isn't in the right language, well, they'll probably be confused.\n\nArguments:\n- `language_code`: the language code to use, e.g. `\"en\"` for English, `\"zh\"` for Traditional Chinese, `\"zh-simplified\"` for Simplified Chinese, etc.\n\n#### Changelog\n\nThe API will change a lot, but changes will be documented in the [Changelog](CHANGELOG.md).\n\nNot just a history of changes, but a migration/upgrading guide. \u003c!-- These are some Ctrl+F keywords. --\u003e\n\nFor general project news, click **Extras \u003e Project News** in the app.\n\n## License\n\nJS Paint is free and open source software, licensed under the permissive [MIT license](https://opensource.org/licenses/MIT).\n\n[![License](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)\n[![GitHub Repo stars](https://img.shields.io/github/stars/1j01/jspaint?label=GitHub%20Stars\u0026style=social)](https://github.com/1j01/jspaint/stargazers)\n[![GitHub forks](https://img.shields.io/github/forks/1j01/jspaint?style=social)](https://github.com/1j01/jspaint/network/members)\n\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2F1j01%2Fjspaint","html_url":"https://awesome.ecosyste.ms/projects/github.com%2F1j01%2Fjspaint","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2F1j01%2Fjspaint/lists"}