init the awkward code
204
static/html/jspaint/.eslintrc.js
Normal file
@@ -0,0 +1,204 @@
|
||||
module.exports = {
|
||||
"env": {
|
||||
"browser": true,
|
||||
"es2020": true,
|
||||
},
|
||||
"extends": "eslint:recommended",
|
||||
"globals": {
|
||||
"Atomics": "readonly",
|
||||
"SharedArrayBuffer": "readonly",
|
||||
"ClipboardItem": "readonly",
|
||||
|
||||
// libraries
|
||||
"$": "readonly",
|
||||
"jQuery": "readonly",
|
||||
"libtess": "readonly",
|
||||
"firebase": "readonly",
|
||||
"GIF": "readonly",
|
||||
"saveAs": "readonly",
|
||||
"Konami": "readonly",
|
||||
"YT": "readonly",
|
||||
"FontDetective": "readonly",
|
||||
"AnyPalette": "readonly",
|
||||
"ImageTracer": "readonly",
|
||||
|
||||
/*
|
||||
// const
|
||||
"MENU_DIVIDER": "readonly",
|
||||
"TAU": "writable",
|
||||
"is_pride_month": "writable",
|
||||
"default_tool": "writable",
|
||||
"default_canvas_width": "writable",
|
||||
"default_canvas_height": "writable",
|
||||
"default_magnification": "writable",
|
||||
"TOOL_FREE_FORM_SELECT": "writable",
|
||||
"TOOL_SELECT": "writable",
|
||||
"TOOL_ERASER": "writable",
|
||||
"TOOL_FILL": "writable",
|
||||
"TOOL_PICK_COLOR": "writable",
|
||||
"TOOL_MAGNIFIER": "writable",
|
||||
"TOOL_PENCIL": "writable",
|
||||
"TOOL_BRUSH": "writable",
|
||||
"TOOL_AIRBRUSH": "writable",
|
||||
"TOOL_TEXT": "writable",
|
||||
"TOOL_LINE": "writable",
|
||||
"TOOL_CURVE": "writable",
|
||||
"TOOL_RECTANGLE": "writable",
|
||||
"TOOL_POLYGON": "writable",
|
||||
"TOOL_ELLIPSE": "writable",
|
||||
"TOOL_ROUNDED_RECTANGLE": "writable",
|
||||
|
||||
// global state: options
|
||||
"brush_shape": "writable",
|
||||
"brush_size": "writable",
|
||||
"pencil_size": "writable",
|
||||
"eraser_size": "writable",
|
||||
"airbrush_size": "writable",
|
||||
"stroke_size": "writable",
|
||||
"stroke_color": "writable",
|
||||
"fill_color": "writable",
|
||||
"fill_color_k": "writable",
|
||||
"stroke_color_k": "writable",
|
||||
"tool_transparent_mode": "writable",
|
||||
"magnification": "writable",
|
||||
"transparency": "writable",
|
||||
"aliasing": "writable",
|
||||
"monochrome": "writable",
|
||||
"selected_colors": "writable",
|
||||
"palette": "writable",
|
||||
"polychrome_palette": "writable",
|
||||
"monochrome_palette": "writable",
|
||||
// global state: history
|
||||
"undos": "writable",
|
||||
"redos": "writable",
|
||||
"current_history_node": "writable",
|
||||
"root_history_node": "writable",
|
||||
"history_node_to_cancel_to": "writable",
|
||||
// global state
|
||||
"selection": "writable",
|
||||
"textbox": "writable",
|
||||
"pointer": "writable", // bad
|
||||
"pointer_start": "writable",
|
||||
"pointer_previous": "writable",
|
||||
"pointers": "writable",
|
||||
"saved": "writable",
|
||||
"file_name": "writable",
|
||||
"file_format": "writable",
|
||||
"system_file_handle": "writable",
|
||||
"selected_tool": "writable",
|
||||
"selected_tools": "writable",
|
||||
"default_tool": "writable",
|
||||
"shift": "writable",
|
||||
"ctrl": "writable",
|
||||
"my_canvas_width": "writable",
|
||||
"my_canvas_height": "writable",
|
||||
"pointer": "writable",
|
||||
// references that may contain state in some ways
|
||||
"main_canvas": "writable",
|
||||
"main_ctx": "writable",
|
||||
"helper_layer": "writable",
|
||||
"menus": "writable",
|
||||
"tools": "writable",
|
||||
"extra_tools": "writable",
|
||||
"$G": "writable",
|
||||
"$canvas": "writable",
|
||||
"$canvas_area": "writable",
|
||||
"$top": "writable",
|
||||
"$bottom": "writable",
|
||||
"$left": "writable",
|
||||
"$right": "writable",
|
||||
"$toolbox": "writable",
|
||||
"$colorbox": "writable",
|
||||
"MenuBar": "writable",
|
||||
"$ToolBox": "writable",
|
||||
"$ColorBox": "writable",
|
||||
"$Window": "writable",
|
||||
"$ToolWindow": "writable",
|
||||
"$DialogWindow": "writable",
|
||||
"Handles": "writable",
|
||||
"$ChooseShapeStyle": "writable",
|
||||
"OnCanvasSelection": "writable",
|
||||
"OnCanvasTextBox": "writable",
|
||||
|
||||
// functions
|
||||
"E": "writable",
|
||||
"get_rgba_from_color": "writable",
|
||||
"make_canvas": "writable",
|
||||
"debounce": "writable",
|
||||
"memoize_synchronous_function": "writable",
|
||||
"invert_rgb": "off",
|
||||
"get_theme": "writable",
|
||||
"get_tool_by_id": "writable",
|
||||
"redo": "writable",
|
||||
"undo": "writable",
|
||||
"undoable": "writable",
|
||||
"cancel": "writable",
|
||||
"deselect": "writable",
|
||||
"update_helper_layer": "writable",
|
||||
"show_error_message": "writable",
|
||||
"to_canvas_coords": "writable",
|
||||
"update_title": "writable",
|
||||
"get_help_folder_icon": "writable",
|
||||
"get_icon_for_tool": "writable",
|
||||
"draw_polygon": "writable",
|
||||
"draw_line": "writable",
|
||||
"draw_line_strip": "writable",
|
||||
"draw_bezier_curve": "writable",
|
||||
"draw_quadratic_curve": "writable",
|
||||
"draw_ellipse": "writable",
|
||||
"draw_rounded_rectangle": "writable",
|
||||
"draw_fill": "writable",
|
||||
"draw_noncontiguous_fill": "writable",
|
||||
"localize": "writable",
|
||||
"set_language": "writable",
|
||||
"get_language": "writable",
|
||||
"get_iso_language_name": "writable",
|
||||
"set_magnification": "writable",
|
||||
"make_css_cursor": "writable",
|
||||
"select_tool": "writable",
|
||||
"change_url_param": "writable",
|
||||
"set_theme": "writable",
|
||||
*/
|
||||
},
|
||||
"rules": {
|
||||
"no-undef": 0, // FOR NOW OKAY? there are just tons of globals at the moment
|
||||
"no-unused-vars": 0, // ditto
|
||||
|
||||
// "eqeqeq": "error",
|
||||
// "class-methods-use-this": "error",
|
||||
"no-alert": "error",
|
||||
"no-extend-native": "error",
|
||||
"no-extra-bind": "error",
|
||||
"no-invalid-this": "error",
|
||||
"no-new-func": "error",
|
||||
"no-eval": "error",
|
||||
"no-new-wrappers": "error",
|
||||
"no-proto": "error",
|
||||
"no-return-assign": "error",
|
||||
"no-return-await": "error",
|
||||
"no-script-url": "error",
|
||||
"no-self-compare": "error",
|
||||
"no-sequences": "error",
|
||||
"no-throw-literal": "error",
|
||||
"no-unmodified-loop-condition": "error",
|
||||
// "no-unused-expressions": "error", // a && a() used a lot, could switch to a?.() etc. https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Optional_chaining
|
||||
"no-useless-concat": "error",
|
||||
"prefer-promise-reject-errors": "error",
|
||||
"radix": "error",
|
||||
"require-await": "error",
|
||||
// "vars-on-top": "error",
|
||||
"wrap-iife": "error",
|
||||
"no-label-var": "error",
|
||||
// "no-shadow": "error",
|
||||
// "no-use-before-define": "error",
|
||||
|
||||
// To target specific variables to rename or otherwise address:
|
||||
"no-restricted-globals": ["error", "event", "canvas", "ctx", "colors", "i", "j", "k", "x", "y", "z", "width", "height", "w", "h"],
|
||||
|
||||
// Stylistic:
|
||||
// @TODO: https://eslint.org/docs/rules/#stylistic-issues
|
||||
// I want to see if I can merge some branches, maybe PRs first.
|
||||
// "array-bracket-spacing": "error",
|
||||
// "block-spacing": "error",
|
||||
}
|
||||
};
|
||||
1
static/html/jspaint/.github/FUNDING.yml
vendored
Normal file
@@ -0,0 +1 @@
|
||||
custom: ['https://www.paypal.me/IsaiahOdhner']
|
||||
18
static/html/jspaint/.gitignore
vendored
Normal file
@@ -0,0 +1,18 @@
|
||||
|
||||
# cypress-image-snapshot visual diffs
|
||||
__diff_output__
|
||||
|
||||
# cypress-image-snapshot images of the whole cypress UI
|
||||
*(failed).snap.png
|
||||
|
||||
# electron forge output
|
||||
out/
|
||||
|
||||
# npm
|
||||
node_modules/
|
||||
*.log
|
||||
*.log.*
|
||||
|
||||
# os crap
|
||||
Thumbs.db
|
||||
~*
|
||||
22
static/html/jspaint/.travis.yml
Normal file
@@ -0,0 +1,22 @@
|
||||
language: node_js
|
||||
node_js:
|
||||
- 10
|
||||
addons:
|
||||
apt:
|
||||
packages:
|
||||
# Ubuntu 16+ does not install this dependency by default, so we need to install it ourselves
|
||||
- libgconf-2-4
|
||||
cache:
|
||||
# Caches $HOME/.npm when npm ci is default script command
|
||||
# Caches node_modules in all other cases
|
||||
npm: true
|
||||
directories:
|
||||
# we also need to cache folder with Cypress binary
|
||||
- ~/.cache
|
||||
install:
|
||||
- npm ci
|
||||
before_script:
|
||||
- npm run test:start-server &
|
||||
- sleep 3 # hopefully the server will be listening by then
|
||||
script:
|
||||
- npm run lint && npm run cy:run
|
||||
28
static/html/jspaint/CHANGELOG.md
Normal file
@@ -0,0 +1,28 @@
|
||||
# Changelog
|
||||
All notable changes to this project will be documented in this file.
|
||||
|
||||
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
||||
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
||||
|
||||
## [Unreleased]
|
||||
|
||||
## [1.0.0] - 2022-08-02
|
||||
### Added
|
||||
- `systemHooks` API for overriding file dialogs, file saving/loading, and Set as Wallpaper commands
|
||||
- `systemHooks.showSaveFileDialog = async ({ formats, defaultFileName, defaultPath, defaultFileFormatID, getBlob, savedCallbackUnreliable, dialogTitle }) => { ... };`
|
||||
- `systemHooks.showOpenFileDialog = async ({ formats }) => { ... };`
|
||||
- `systemHooks.writeBlobToHandle = async (save_file_handle, blob) => { ... };`
|
||||
- `systemHooks.readBlobFromHandle = async (file_handle) => { ... };`
|
||||
- `systemHooks.setWallpaperTiled = (canvas) => { ... };`
|
||||
- `systemHooks.setWallpaperCentered = (canvas) => { ... };`
|
||||
- function `undoable({ name, icon }, actionFunction)` to make an action undoable, as far is it modifies the canvas
|
||||
- function `show_error_message(message, [error])` to show an error message dialog box, optionally with expandable error details
|
||||
- function `open_from_file(blob, source_file_handle)` to load a file from a blob and file handle pair (kinda quirky API)
|
||||
- function `set_theme(theme_file_name)` to switch themes
|
||||
- function `set_language(language_code)` to switch languages, prompting the user to reload the application
|
||||
- You can use `.main-canvas` selector to access the canvas element.
|
||||
- URL parameter `#load:<URL>` to load a file from a URL
|
||||
|
||||
[Unreleased]: https://github.com/1j01/jspaint/compare/v1.0.0...HEAD
|
||||
[1.1.0]: https://github.com/1j01/jspaint/compare/v1.0.0...v1.1.0
|
||||
[1.0.0]: https://github.com/1j01/jspaint/releases/tag/v1.0.0
|
||||
1
static/html/jspaint/CNAME
Normal file
@@ -0,0 +1 @@
|
||||
jspaint.app
|
||||
45
static/html/jspaint/CONTRIBUTING.md
Normal file
@@ -0,0 +1,45 @@
|
||||
# Contributing
|
||||
|
||||
## Pull Requests
|
||||
|
||||
Let me know before you work on something by opening an issue or commenting on an existing one.
|
||||
|
||||
Someone may be already working on it, or I may have specific plans or requirements.
|
||||
I don't want your effort to be wasted!
|
||||
|
||||
## Issues
|
||||
|
||||
[Bugs and feature requests are tracked on GitHub.](https://github.com/1j01/jspaint/issues)
|
||||
|
||||
Before opening an issue for a bug or feature request, search to see if it's already been reported.
|
||||
|
||||
You can also [email me](mailto:isaiahodhner@gmail.com) if you prefer.
|
||||
|
||||
## Windows 98
|
||||
|
||||
Note: JS Paint's GUI is primarily based on Paint from Windows 98.
|
||||
There's a nice [online emulator](https://copy.sh/v86/?profile=windows98)
|
||||
that you can play around with and use as a reference.
|
||||
|
||||
## Dev Setup
|
||||
|
||||
See [**Development Setup**](./README.md#Development-Setup) on the readme.
|
||||
|
||||
### Project Structure
|
||||
|
||||
- `index.html` and `app.js` are the main entry points for the app.
|
||||
- `functions.js` has functions that shouldn't own any global state, altho they very much modify global state, and there may be a few stateful global variables defined in there.
|
||||
- The project uses [jQuery](https://jquery.com/), and a convention of prefixing variables that hold jQuery objects with `$`
|
||||
- There are also some weird pseudo-classes like `$ColorBox` which extend and return jQuery objects. I don't recommend this pattern for new code.
|
||||
- Menu code and windowing code is in `lib/os-gui/` and should be kept in sync with the [os-gui](https://github.com/1j01/os-gui) project.
|
||||
- (Maybe I should version this using git-subrepo?)
|
||||
- Some window behavior specific to jspaint is in `$ToolWindow.js` and `$Component.js`
|
||||
- `image-manipulation.js` should contain just rendering related code, and ideally no dialogs except maybe some error messages.
|
||||
- Some image manipulation code is also in `tools.js` and `functions.js`
|
||||
- CSS is in `styles/`
|
||||
- Layout-important CSS is kept separate from theme CSS
|
||||
- Localization data is in `localization/`
|
||||
- As of writing there's no good way to contribute translations, but [get in touch!](https://github.com/1j01/jspaint/issues/80)
|
||||
|
||||
Any good IDE or code editor has a project-wide search (often with <kbd>Ctrl+Shift+F</kbd>). I use this all the time.
|
||||
I also use the "Intellisense" feature of [VS Code](https://code.visualstudio.com/) to jump to function definitions (an extra convenience over searching for function names).
|
||||
21
static/html/jspaint/LICENSE.txt
Normal file
@@ -0,0 +1,21 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2022 Isaiah Odhner
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
745
static/html/jspaint/README.md
Normal file
@@ -0,0 +1,745 @@
|
||||
|
||||
# [ JS Paint](https://jspaint.app)
|
||||
|
||||
A pixel-perfect web-based MS Paint remake and more... [Try it out!](https://jspaint.app)
|
||||
|
||||
JS Paint recreates every tool and menu of MS Paint, and even [little-known features](#did-you-know), to a high degree of fidelity.
|
||||
|
||||
It supports themes, additional file types, and accessibility features like Eye Gaze Mode and Speech Recognition.
|
||||
|
||||

|
||||
|
||||
Ah yes, good old Paint. Not the one with the [ribbons][]
|
||||
or the [new skeuomorphic one][Fresh Paint] with the interface that can take up nearly half the screen.
|
||||
(And not the even newer [Paint 3D][].)
|
||||
|
||||
[ribbons]: https://www.google.com/search?tbm=isch&q=MS+Paint+Windows+7+ribbons "Google Search: MS Paint Windows 7 ribbons"
|
||||
[Fresh Paint]: https://www.google.com/search?tbm=isch&q=MS+Fresh+Paint "Google Search: MS Fresh Paint"
|
||||
[Paint 3D]: https://www.microsoft.com/en-us/store/p/paint-3d-preview/9nblggh5fv99
|
||||
|
||||
Windows 95, 98, and XP were the golden years of Paint.
|
||||
You had a tool box and a color box, a foreground color and a background color,
|
||||
and that was all you needed.
|
||||
|
||||
Things were simple.
|
||||
|
||||
But we want to undo more than three actions.
|
||||
We want to edit transparent images.
|
||||
We can't just keep using the old Paint.
|
||||
|
||||
So that's why I'm making JS Paint.
|
||||
I want to bring good old Paint into the modern era.
|
||||
|
||||
|
||||
#### Current improvements include:
|
||||
|
||||
* Open source ([MIT licensed](LICENSE.txt))
|
||||
* Cross-platform
|
||||
* Mobile friendly
|
||||
* Touch support: use two fingers to pan the view, and pinch to zoom
|
||||
* Click/tap the selected colors area to swap the foreground and background colors
|
||||
* **View > Fullscreen** to toggle fullscreen mode, nice for small screens
|
||||
* Web features
|
||||
* **File > Load From URL...** to open an image from the Web.
|
||||
* **File > Upload to Imgur** to upload the current image to Imgur.
|
||||
* **Paste** supports loading from URLs.
|
||||
* 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: <https://jspaint.app/#load:https://i.imgur.com/zJMrWwb.png>
|
||||
* Rudimentary **multi-user** collaboration support.
|
||||
Start up a session at
|
||||
[jspaint.app/#session:multi-user-test](https://jspaint.app/#session:multi-user-test)
|
||||
and send the link to your friends!
|
||||
It isn't seamless; actions by other users interrupt what you're doing, and visa versa.
|
||||
Sessions are not private, and you may lose your work at any time.
|
||||
If you want better collaboration support, follow the development of [Mopaint](https://github.com/1j01/mopaint).
|
||||
* **Extras > Themes** to change the look of the app. Dark mode included.
|
||||
* [Eye Gaze Mode](https://jspaint.app/#eye-gaze-mode), for use with an eye tracker, head tracker, or other coarse input device, accessible from **Extras > Eye Gaze Mode**. With just a webcam, you can try it out with [Enable Viacam](https://eviacam.crea-si.com/) (head tracker) or [GazePointer](https://sourceforge.net/projects/gazepointer/) (eye tracker).
|
||||
* [Speech Recognition Mode](https://jspaint.app/#speech-recognition-mode).
|
||||
Using your voice you can select tools and colors, pan the view ("scroll down and to the left", or "go southwest", etc.), explore the menus (but you can activate any menu item without opening the menus first), interact with windows (including scrolling the history view with "scroll up"/"scroll down" etc.), dictate text with the Text tool, and even tell the application to sketch things (for instance, "draw a house")
|
||||
* Create an animated GIF from the current document history.
|
||||
Accessible from the Extras menu or with <kbd>Ctrl+Shift+G</kbd>.
|
||||
It's pretty nifty, you should try it out!
|
||||
You might want to limit the size of the image though.
|
||||
* Load and save [many different palette formats](#color-palette-formats) with **Colors > Get Colors** and **Colors > Save Colors**.
|
||||
(I made a library for this: <img src="images/anypalette-logo-128x128.png" height="16"> [AnyPalette.js](https://github.com/1j01/anypalette.js).)
|
||||
* You can also drag and drop palette files into the app to load.
|
||||
|
||||
Editing Features:
|
||||
|
||||
* Use Alt+Mousewheel to zoom in and out
|
||||
* Edit transparent images! To create a transparent image,
|
||||
go to **Image > Attributes...** and select Transparent,
|
||||
then OK, and then **Image > Clear Image** or use the Eraser tool.
|
||||
Images with *any* translucent pixels will open in Transparent mode.
|
||||
* You can crop the image by making a selection while holding <kbd>Ctrl</kbd>
|
||||
* Keyboard shortcuts for rotation: <kbd>Ctrl+.</kbd> and <kbd>Ctrl+,</kbd> (<kbd><</kbd> and <kbd>></kbd>)
|
||||
* Rotate by any arbitrary angle in **Image > Flip/Rotate**
|
||||
* In **Image > Stretch/Skew**, you can stretch more than 500% at once
|
||||
* Zoom to an arbitrary scale in **View > Zoom > Custom...**
|
||||
* Zoom to fit the canvas within the window with **View > Zoom > Zoom To Window**
|
||||
* Non-contiguous fill: Replace a color in the entire image by holding <kbd>Shift</kbd> when using the fill tool
|
||||
|
||||
Miscellaneous Improvements:
|
||||
|
||||
* [Vertical Color Box mode](https://jspaint.app/#vertical-color-box-mode), accessible from **Extras > Vertical Color Box**
|
||||
* You can use the Text tool at any zoom level (and it previews the exact pixels that will end up on the canvas).
|
||||
* Spellcheck is available in the textbox if your browser supports it.
|
||||
* Resize handles are easier to grab than in Windows 10's Paint.
|
||||
* Omits some Thumbnail view bugs, like the selection showing in the wrong place.
|
||||
* Unlimited undos/redos (as opposed to a measly 3 in Windows XP,
|
||||
or a measly 50 in Windows 7)
|
||||
* 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 > History** or <kbd>Ctrl+Shift+Y</kbd>
|
||||
* 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 > Save** or <kbd>Ctrl+S</kbd>! Manage backups with **File > Manage Storage**.
|
||||
|
||||
<!--
|
||||
Half-features:
|
||||
|
||||
* When you do **Edit > Paste From...** you can select transparent images.
|
||||
~~You can even paste a transparent animated GIF and then
|
||||
hold <kbd>Shift</kbd> while dragging the selection to
|
||||
smear it across the canvas *while it animates*!~~
|
||||
Update: This was [due to not-to-spec behavior in Chrome.](https://christianheilmann.com/2014/04/16/browser-inconsistencies-animated-gif-and-drawimage/)
|
||||
I may reimplement this in the future as I really liked this feature.
|
||||
* You can open SVG files, though only as a bitmap.
|
||||
(Note: it may open super large, or tiny. There's no option to choose a size when opening.)
|
||||
-->
|
||||
|
||||

|
||||
|
||||
|
||||
#### Limitations:
|
||||
|
||||
A few things with the tools aren't done yet.
|
||||
See [TODO.md](TODO.md#Tools)
|
||||
|
||||
Full 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.
|
||||
|
||||
In other browsers you can still can copy with <kbd>Ctrl+C</kbd>, cut with <kbd>Ctrl+X</kbd>, and paste with <kbd>Ctrl+V</kbd>,
|
||||
but data copied from JS Paint can only be pasted into other instances of JS Paint.
|
||||
External images can be pasted in.
|
||||
|
||||
|
||||
## Supported File Formats
|
||||
|
||||
### Image Formats
|
||||
|
||||
⚠️ Saving as JPEG will introduce artifacts that cause problems when using the Fill tool or transparent selections.
|
||||
|
||||
⚠️ Saving in some formats will reduce the number of colors in the image.
|
||||
|
||||
💡 Unlike in MS Paint, you can use **Edit > Undo** to revert color or quality reduction from saving.
|
||||
This doesn't undo saving the file, but allows you to then save in a different format with higher quality, using **File > Save As**.
|
||||
|
||||
💡 Saving as PNG is recommended as it gives small file sizes while retaining full quality.
|
||||
|
||||
| File Extension | Name | Read | Write | Read Palette | Write Palette |
|
||||
|-------------------------------|-------------------------------|:----:|:-----:|:------------:|:-------------:|
|
||||
| .png | [PNG][] | ✅ | ✅ | 🔜 | |
|
||||
| .bmp, .dib | [Monochrome Bitmap][BMP] | ✅ | ✅ | 🔜 | ✅ |
|
||||
| .bmp, .dib | [16 Color Bitmap][BMP] | ✅ | ✅ | 🔜 | ✅ |
|
||||
| .bmp, .dib | [256 Color Bitmap][BMP] | ✅ | ✅ | 🔜 | ✅ |
|
||||
| .bmp, .dib | [24-bit Bitmap][BMP] | ✅ | ✅ | N/A | N/A |
|
||||
| .tif, .tiff, .dng, .cr2, .nef | [TIFF][] (loads first page) | ✅ | ✅ | | |
|
||||
| .pdf | [PDF][] (loads first page) | ✅ | | | |
|
||||
| .webp | [WebP][] | 🌐 | 🌐 | | |
|
||||
| .gif | [GIF][] | 🌐 | 🌐 | | |
|
||||
| .jpeg, .jpg | [JPEG][] | 🌐 | 🌐 | N/A | N/A |
|
||||
| .svg | [SVG][] (only default size) | 🌐 | | | |
|
||||
| .ico | [ICO][] (only default size) | 🌐 | | | |
|
||||
|
||||
Capabilities marked with 🌐 are currently left up to the browser to support or not.
|
||||
If "Write" is marked with 🌐, the format will appear in the file type dropdown but may not work when you try to save.
|
||||
For opening files, see Wikipedia's [browser image format support table][] for more information.
|
||||
|
||||
Capabilities marked with 🔜 are coming soon, and N/A of course means not applicable.
|
||||
|
||||
"Read Palette" refers to loading the colors into the Colors box automatically (from an [indexed color][] image),
|
||||
and "Write Palette" refers to writing an [indexed color][] image.
|
||||
|
||||
[PNG]: https://en.wikipedia.org/wiki/Portable_Network_Graphics
|
||||
[BMP]: https://en.wikipedia.org/wiki/BMP_file_format
|
||||
[TIFF]: https://en.wikipedia.org/wiki/TIFF
|
||||
[PDF]: https://en.wikipedia.org/wiki/PDF
|
||||
[WebP]: https://en.wikipedia.org/wiki/WebP
|
||||
[GIF]: https://en.wikipedia.org/wiki/GIF
|
||||
[JPEG]: https://en.wikipedia.org/wiki/JPEG
|
||||
[SVG]: https://en.wikipedia.org/wiki/Scalable_Vector_Graphics
|
||||
[ICO]: https://en.wikipedia.org/wiki/ICO_(file_format)
|
||||
[indexed color]: https://en.wikipedia.org/wiki/Indexed_color
|
||||
[browser image format support table]: https://en.wikipedia.org/wiki/Comparison_of_web_browsers#Image_format_support
|
||||
|
||||
|
||||
### Color Palette Formats
|
||||
|
||||
With **Colors > Save Colors** and **Colors > Get Colors** you can save and load colors
|
||||
in many different formats, for compatibility with a wide range of programs.
|
||||
|
||||
If you want to add extensive palette support to another application, I've made this functionality available as a library:
|
||||
<img src="images/anypalette-logo-128x128.png" height="16"> [AnyPalette.js](https://github.com/1j01/anypalette.js)
|
||||
|
||||
| File Extension | Name | Programs | Read | Write |
|
||||
|-------------------|-----------------------------------|-----------------------------------------------------------------------------------|:-------:|:-------:|
|
||||
| .pal | [RIFF] Palette | [MS Paint] for Windows 95 and Windows NT 4.0 | ✅ | ✅ |
|
||||
| .gpl | [GIMP][Gimp] Palette | [Gimp], [Inkscape], [Krita], [KolourPaint], [Scribus], [CinePaint], [MyPaint] | ✅ | ✅ |
|
||||
| .aco | Adobe Color Swatch | Adobe [Photoshop] | ✅ | ✅ |
|
||||
| .ase | Adobe Swatch Exchange | Adobe [Photoshop], [InDesign], and [Illustrator] | ✅ | ✅ |
|
||||
| .txt | [Paint.NET] Palette | [Paint.NET] | ✅ | ✅ |
|
||||
| .act | Adobe Color Table | Adobe [Photoshop] and [Illustrator] | ✅ | ✅ |
|
||||
| .pal, .psppalette | [Paint Shop Pro] Palette | [Paint Shop Pro] (Jasc Software / Corel) | ✅ | ✅ |
|
||||
| .hpl | [Homesite] Palette | Allaire [Homesite] / Macromedia [ColdFusion] | ✅ | ✅ |
|
||||
| .cs | ColorSchemer | ColorSchemer Studio | ✅ | |
|
||||
| .pal | [StarCraft] Palette | [StarCraft] | ✅ | ✅ |
|
||||
| .wpe | [StarCraft] Terrain Palette | [StarCraft] | ✅ | ✅ |
|
||||
| .sketchpalette | [Sketch] Palette | [Sketch] | ✅ | ✅ |
|
||||
| .spl | [Skencil] Palette | [Skencil] (formerly called Sketch) | ✅ | ✅ |
|
||||
| .soc | StarOffice Colors | [StarOffice], [OpenOffice], [LibreOffice] | ✅ | ✅ |
|
||||
| .colors | KolourPaint Color Collection | [KolourPaint] | ✅ | ✅ |
|
||||
| .colors | Plasma Desktop Color Scheme | [KDE] Plasma Desktop | ✅ | |
|
||||
| .theme | Windows Theme | [Windows] Desktop | ✅ | |
|
||||
| .themepack | Windows Theme | [Windows] Desktop | ✅ | |
|
||||
| .css, .scss, .styl| Cascading StyleSheets | Web browsers / web pages | ✅ | ✅ |
|
||||
| .html, .svg, .js | any text files with CSS colors | Web browsers / web pages | ✅ | |
|
||||
|
||||
[RIFF]: https://en.wikipedia.org/wiki/Resource_Interchange_File_Format
|
||||
[MS Paint]: https://en.wikipedia.org/wiki/Microsoft_Paint
|
||||
[Paint.NET]: https://www.getpaint.net/
|
||||
[Paint Shop Pro]: https://www.paintshoppro.com/en/
|
||||
[StarCraft]: https://en.wikipedia.org/wiki/StarCraft
|
||||
[Homesite]: https://en.wikipedia.org/wiki/Macromedia_HomeSite
|
||||
[ColdFusion]: https://en.wikipedia.org/wiki/Adobe_ColdFusion
|
||||
[StarOffice]: https://en.wikipedia.org/wiki/StarOffice
|
||||
[OpenOffice]: https://www.openoffice.org/
|
||||
[LibreOffice]: https://www.libreoffice.org/
|
||||
[Sketch]: https://www.sketchapp.com/
|
||||
[Skencil]: https://skencil.org/
|
||||
[Photoshop]: https://www.adobe.com/products/photoshop.html
|
||||
[InDesign]: https://www.adobe.com/products/indesign.html
|
||||
[Illustrator]: https://www.adobe.com/products/illustrator.html
|
||||
[Gimp]: https://www.gimp.org/
|
||||
[Inkscape]: https://inkscape.org/en/
|
||||
[Krita]: https://www.calligra.org/krita/
|
||||
[KolourPaint]: http://kolourpaint.org/
|
||||
[KDE]: https://kde.org/
|
||||
[Windows]: https://en.wikipedia.org/wiki/Microsoft_Windows
|
||||
[Scribus]: https://www.scribus.net/
|
||||
[CinePaint]: http://www.cinepaint.org/
|
||||
[MyPaint]: http://mypaint.org/
|
||||
|
||||
|
||||
## Did you know?
|
||||
|
||||
* There's a black and white mode with *patterns* instead of colors in the palette,
|
||||
which you can get to from **Image > Attributes...**
|
||||
|
||||
* You can drag the color box and tool box around if you grab them by the right place.
|
||||
You can even drag them out into little windows.
|
||||
You can dock the windows back to the side by double-clicking on their title bars.
|
||||
|
||||
* In addition to the left-click foreground color and the right-click background color,
|
||||
there's a third color you can access by holding <kbd>Ctrl</kbd> while you draw.
|
||||
It starts out with no color so you'll need to hold <kbd>Ctrl</kbd> and select a color first.
|
||||
The fancy thing about this color slot is you can
|
||||
press and release <kbd>Ctrl</kbd> to switch colors *while drawing*.
|
||||
|
||||
* 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.
|
||||
Try scribbling with the Free-Form Select tool and then doing **Image > Invert**
|
||||
|
||||
* These Tips and Tricks from [a tutorial for MS Paint](https://www.albinoblacksheep.com/tutorial/mspaint)
|
||||
also work in JS Paint:
|
||||
|
||||
* [x] Brush Scaling (<kbd>+</kbd> & <kbd>-</kbd> on the number pad to adjust brush size)
|
||||
* [x] "Custom Brushes" (hold <kbd>Shift</kbd> and drag the selection to smear it)
|
||||
* [x] The 'Stamp' "Tool" (hold <kbd>Shift</kbd> and click the selection to stamp it)
|
||||
* [x] Image Scaling (<kbd>+</kbd> & <kbd>-</kbd> on the number pad to scale the selection by factors of 2)
|
||||
* [x] Color Replacement (right mouse button with Eraser to selectively replace the foreground color with the background color)
|
||||
* [x] The Grid (<kbd>Ctrl+G</kbd> & Zoom to 4x+)
|
||||
* [x] Quick Undo (Pressing a second mouse button cancels the action you were performing.
|
||||
I also made it redoable, in case you do it by accident!)
|
||||
* [ ] Scroll Wheel Bug (Hmm, let's maybe not recreate this?)
|
||||
|
||||
|
||||
## Desktop App
|
||||
|
||||
JS Paint can be installed as a PWA, altho it doesn't work offline.
|
||||
|
||||
(Also I built it into a desktop app with [Electron][] and [Electron Forge][], but this will use unnecessary system resources and is not recommended. You can follow [this issue](https://github.com/1j01/jspaint/issues/2) for the first release.)
|
||||
|
||||
[Electron]: https://electronjs.org/
|
||||
[Electron Forge]: https://electronforge.io/
|
||||
|
||||
|
||||
## Development Setup
|
||||
|
||||
[Clone the repo.](https://help.github.com/articles/cloning-a-repository/)
|
||||
|
||||
Install [Node.js][] if you don't have it, then open up a command prompt / terminal in the project directory.
|
||||
|
||||
### Testing
|
||||
|
||||
Run `npm run lint` to check for code problems.
|
||||
|
||||
Run `npm test` to run browser-based tests with Cypress. (It's slow to start up and run tests, unfortunately.)
|
||||
|
||||
Run `npm run accept` to accept any visual changes.
|
||||
This 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.
|
||||
If you use [GitHub Desktop](https://desktop.github.com/), you can view diffs of images, in four different modes.
|
||||
|
||||
To open the Cypress UI, first run `npm run test:start-server`, then concurrently `npm run cy:open`
|
||||
|
||||
Tests are also run in continuous integration [with Travis CI](https://travis-ci.org/1j01/jspaint).
|
||||
|
||||
### Web App (https://jspaint.app)
|
||||
|
||||
After you've installed dependencies with `npm i`,
|
||||
use `npm run dev` to start a live-reloading server.
|
||||
|
||||
Make sure any layout-important styles go in `layout.css`.
|
||||
When updating `layout.css`, a right-to-left version of the stylesheet is generated, using [RTLCSS](https://rtlcss.com/).
|
||||
You should test the RTL layout by changing the language to Arabic or Hebrew.
|
||||
Go to **Extras > Language > العربية** or **עברית**.
|
||||
See [Control Directives](https://rtlcss.com/learn/usage-guide/control-directives/) for how to control the RTL layout.
|
||||
|
||||
### Desktop App (Electron)
|
||||
|
||||
This is basically ready for release, but as of yet unreleased.
|
||||
|
||||
- Install dependencies with `npm i`
|
||||
- Start the electron app with `npm run electron:start`
|
||||
|
||||
[electron-debug][] is included, so you can use <kbd>F5</kbd>/<kbd>Ctrl+R</kbd> to reload and <kbd>F12</kbd>/<kbd>Ctrl+Shift+I</kbd> to open the devtools.
|
||||
|
||||
You can build for production with `npm run electron:make`
|
||||
|
||||
[Live Server]: https://github.com/1j01/live-server
|
||||
[Node.js]: https://nodejs.org/
|
||||
[electron-debug]: https://github.com/sindresorhus/electron-debug
|
||||
|
||||
## Deployment
|
||||
|
||||
JS Paint can be deployed using a regular web server.
|
||||
|
||||
Nothing needs to be compiled.
|
||||
|
||||
### CORS proxy
|
||||
|
||||
Optionally, 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:<URL>` feature with images that are not on the same domain.
|
||||
|
||||
By default it will use a [CORS Anywhere instance](https://jspaint-cors-proxy.herokuapp.com) set up to work with [jspaint.app](https://jspaint.app).
|
||||
|
||||
It is hosted for free on [Heroku](https://www.heroku.com/),
|
||||
and you can set up your own instance and configure it to work with your own domain.
|
||||
|
||||
You'll have to find and replace `https://jspaint-cors-proxy.herokuapp.com` with your own instance URL.
|
||||
|
||||
|
||||
### Multiplayer Support
|
||||
|
||||
Multiplayer support currently relies on Firebase,
|
||||
which is not open source software.
|
||||
|
||||
You 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,
|
||||
replacing the `config` passed to `initializeApp` with the config from the Firebase Console when you set up a Web App.
|
||||
|
||||
But the multiplayer mode is very shoddy so far.
|
||||
It should be replaced with something open source, more secure, more efficient, and more robust.
|
||||
|
||||
## Embed in your website
|
||||
|
||||
### Simple
|
||||
|
||||
Add this to your HTML:
|
||||
|
||||
```html
|
||||
<iframe src="https://jspaint.app" width="100%" height="100%"></iframe>
|
||||
```
|
||||
|
||||
#### Start with an image
|
||||
|
||||
You can have it load an image from a URL by adding `#load:<URL>` to the URL.
|
||||
|
||||
```html
|
||||
<iframe src="https://jspaint.app#load:https://jspaint.app/favicon.ico" width="100%" height="100%"></iframe>
|
||||
```
|
||||
|
||||
### Advanced
|
||||
|
||||
If you want to control JS Paint, how it saves/loads files, or access the canvas directly,
|
||||
there is an unstable API.
|
||||
|
||||
First you need to [clone the repo](https://help.github.com/articles/cloning-a-repository/),
|
||||
so you can point an `iframe` to your local copy.
|
||||
|
||||
The 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).
|
||||
|
||||
Having a local copy also means things won't break any time the API changes.
|
||||
|
||||
If 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:
|
||||
|
||||
```html
|
||||
<iframe src="jspaint/index.html" id="jspaint-iframe" width="100%" height="100%"></iframe>
|
||||
```
|
||||
|
||||
If 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"`.
|
||||
You can also use an absolute URL, like `src="https://example.com/cool-apps/jspaint/index.html"`.
|
||||
|
||||
#### Changing how files are saved/loaded
|
||||
|
||||
You can override the file saving and opening dialogs
|
||||
with JS Paint's `systemHooks` API.
|
||||
|
||||
```html
|
||||
<script>
|
||||
var iframe = document.getElementById('jspaint-iframe');
|
||||
var jspaint = iframe.contentWindow;
|
||||
// Wait for systemHooks object to exist (the iframe needs to load)
|
||||
waitUntil(()=> jspaint.systemHooks, 500, ()=> {
|
||||
// Hook in
|
||||
jspaint.systemHooks.showSaveFileDialog = async ({ formats, defaultFileName, defaultPath, defaultFileFormatID, getBlob, savedCallbackUnreliable, dialogTitle }) => { ... };
|
||||
jspaint.systemHooks.showOpenFileDialog = async ({ formats }) => { ... };
|
||||
jspaint.systemHooks.writeBlobToHandle = async (save_file_handle, blob) => { ... };
|
||||
jspaint.systemHooks.readBlobFromHandle = async (file_handle) => { ... };
|
||||
});
|
||||
// General function to wait for a condition to be met, checking at regular intervals
|
||||
function waitUntil(test, interval, callback) {
|
||||
if (test()) {
|
||||
callback();
|
||||
} else {
|
||||
setTimeout(waitUntil, interval, test, interval, callback);
|
||||
}
|
||||
}
|
||||
</script>
|
||||
```
|
||||
|
||||
A [Blob](https://developer.mozilla.org/en-US/docs/Web/API/Blob) represents the contents of a file in memory.
|
||||
|
||||
A file handle is anything that can identify a file.
|
||||
You get to own this concept, and define how to identify files.
|
||||
It could be anything from an index into an array, to a Dropbox file ID, to an IPFS URL, to a file path.
|
||||
It can be any type, or maybe it needs to be a string, I forget.
|
||||
|
||||
Once you have a concept of a file handle, you can implement file pickers using the system hooks, and functions to read and write files.
|
||||
|
||||
| Command | Hooks Used |
|
||||
| ------- | ---------- |
|
||||
| **File > Save As** | [`systemHooks.showSaveFileDialog`][], then when a file is picked, [`systemHooks.writeBlobToHandle`][] |
|
||||
| **File > Open** | [`systemHooks.showOpenFileDialog`][], then when a file is picked, [`systemHooks.readBlobFromHandle`][] |
|
||||
| **File > Save** | [`systemHooks.writeBlobToHandle`][] (or same as **File > Save As** if there's no file open yet) |
|
||||
| **Edit > Copy To** | [`systemHooks.showSaveFileDialog`][], then when a file is picked, [`systemHooks.writeBlobToHandle`][] |
|
||||
| **Edit > Paste From** | [`systemHooks.showOpenFileDialog`][], then when a file is picked, [`systemHooks.readBlobFromHandle`][] |
|
||||
| **File > Set As Wallpaper (Tiled)** | [`systemHooks.setWallpaperTiled`][] if defined, else [`systemHooks.setWallpaperCentered`][] if defined, else same as **File > Save As** |
|
||||
| **File > Set As Wallpaper (Centered)** | [`systemHooks.setWallpaperCentered`][] if defined, else same as **File > Save As** |
|
||||
| **Extras > Render History As GIF** | Same as **File > Save As** |
|
||||
| **Colors > Save Colors** | Same as **File > Save As** |
|
||||
| **Colors > Get Colors** | Same as **File > Open** |
|
||||
|
||||
#### Loading a file initially
|
||||
|
||||
To start the app with a file loaded for editing,
|
||||
wait for the app to load, then call [`systemHooks.readBlobFromHandle`][] with a file handle, and tell the app to load that file blob.
|
||||
|
||||
```js
|
||||
const file_handle = "initial-file-to-load";
|
||||
systemHooks.readBlobFromHandle(file_handle).then(file => {
|
||||
if (file) {
|
||||
contentWindow.open_from_file(file, file_handle);
|
||||
}
|
||||
}, (error) => {
|
||||
// Note: in some cases, this handler may not be called, and instead an error message is shown by readBlobFromHandle directly.
|
||||
contentWindow.show_error_message(`Failed to open file ${file_handle}`, error);
|
||||
});
|
||||
```
|
||||
|
||||
This is clumsy, and in the future there may be a query string parameter to load an initial file by its handle.
|
||||
(Note to self: it will need to wait for your system hooks to be registered, somehow.)
|
||||
|
||||
There's already a query string parameter to load from a URL:
|
||||
|
||||
```html
|
||||
<iframe src="https://jspaint.app?load:SOME_URL_HERE"></iframe>
|
||||
```
|
||||
|
||||
But this won't set up the file handle for saving.
|
||||
|
||||
|
||||
#### Integrating Set as Wallpaper
|
||||
|
||||
You can define two functions to set the wallpaper, which will be used by **File > Set As Wallpaper (Tiled)** and **File > Set As Wallpaper (Centered)**.
|
||||
|
||||
- [`systemHooks.setWallpaperTiled`][]` = (canvas) => { ... };`
|
||||
- [`systemHooks.setWallpaperCentered`][]` = (canvas) => { ... };`
|
||||
|
||||
If 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.
|
||||
|
||||
If you don't specify [`systemHooks.setWallpaperCentered`][], JS Paint will default to saving a file (`<original file name> wallpaper.png`) using [`systemHooks.showSaveFileDialog`][] and [`systemHooks.writeBlobToHandle`][].
|
||||
|
||||
Here's a full example supporting a persistent custom wallpaper as a background on the containing page:
|
||||
|
||||
```js
|
||||
const wallpaper = document.querySelector('body'); // or some other element
|
||||
|
||||
jspaint.systemHooks.setWallpaperCentered = (canvas) => {
|
||||
canvas.toBlob((blob) => {
|
||||
setDesktopWallpaper(blob, "no-repeat", true);
|
||||
});
|
||||
};
|
||||
jspaint.systemHooks.setWallpaperTiled = (canvas) => {
|
||||
canvas.toBlob((blob) => {
|
||||
setDesktopWallpaper(blob, "repeat", true);
|
||||
});
|
||||
};
|
||||
|
||||
function setDesktopWallpaper(file, repeat, saveToLocalStorage) {
|
||||
const blob_url = URL.createObjectURL(file);
|
||||
wallpaper.style.backgroundImage = `url(${blob_url})`;
|
||||
wallpaper.style.backgroundRepeat = repeat;
|
||||
wallpaper.style.backgroundPosition = "center";
|
||||
wallpaper.style.backgroundSize = "auto";
|
||||
if (saveToLocalStorage) {
|
||||
const fileReader = new FileReader();
|
||||
fileReader.onload = () => {
|
||||
localStorage.setItem("wallpaper-data-url", fileReader.result);
|
||||
localStorage.setItem("wallpaper-repeat", repeat);
|
||||
};
|
||||
fileReader.onerror = () => {
|
||||
console.error("Error reading file (for setting wallpaper)", file);
|
||||
};
|
||||
fileReader.readAsDataURL(file);
|
||||
}
|
||||
}
|
||||
|
||||
// Initialize the wallpaper from localStorage, if it exists
|
||||
try {
|
||||
const wallpaper_data_url = localStorage.getItem("wallpaper-data-url");
|
||||
const wallpaper_repeat = localStorage.getItem("wallpaper-repeat");
|
||||
if (wallpaper_data_url) {
|
||||
fetch(wallpaper_data_url).then(response => response.blob()).then(file => {
|
||||
setDesktopWallpaper(file, wallpaper_repeat, false);
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
}
|
||||
```
|
||||
|
||||
It's a little bit recursive, sorry; it could probably be done simpler.
|
||||
Like 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?)
|
||||
|
||||
#### Specifying the canvas size
|
||||
|
||||
You can load a file that has the desired dimensions.
|
||||
There's no special API for this at the moment.
|
||||
|
||||
See [Loading a file initially](#loading-a-file-initially).
|
||||
|
||||
#### Specifying the theme
|
||||
|
||||
You could change the theme programmatically:
|
||||
|
||||
```js
|
||||
var iframe = document.getElementById('jspaint-iframe');
|
||||
var jspaint = iframe.contentWindow;
|
||||
jspaint.set_theme("modern.css");
|
||||
```
|
||||
but this will break the user preference.
|
||||
|
||||
The **Extras > Themes** menu will still work, but the preference won't persist when reloading the page.
|
||||
|
||||
In the future there may be a query string parameter to specify the default theme. You could also fork jspaint to change the default theme.
|
||||
|
||||
#### Specifying the language
|
||||
|
||||
Similar to the theme, you can try to change the language programmatically:
|
||||
|
||||
```js
|
||||
var iframe = document.getElementById('jspaint-iframe');
|
||||
var jspaint = iframe.contentWindow;
|
||||
jspaint.set_language("ar");
|
||||
```
|
||||
but this will actually **ask the user to reload the application** to change languages.
|
||||
|
||||
The **Extras > Language** menu will still work, but the user will be bothered to change the language every time they reload the page.
|
||||
|
||||
In the future there may be a query string parameter to specify the default language. You could also fork jspaint to change the default language.
|
||||
|
||||
#### Adding custom menus
|
||||
|
||||
Not supported yet.
|
||||
You could fork jspaint and add your own menus.
|
||||
|
||||
#### Accessing the canvas directly
|
||||
|
||||
With access to the canvas, you can implement a live preview of your drawing, for example updating a texture in a game engine in realtime.
|
||||
|
||||
```js
|
||||
var iframe = document.getElementById('jspaint-iframe');
|
||||
// contentDocument here refers to the webpage loaded in the iframe, not the image document loaded in jspaint.
|
||||
// We're just reaching inside the iframe to get the canvas.
|
||||
var canvas = iframe.contentDocument.querySelector(".main-canvas");
|
||||
```
|
||||
|
||||
It'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.
|
||||
Instead use [`open_from_file`][].
|
||||
|
||||
#### Performing custom actions
|
||||
|
||||
If you want to make buttons or other UI to do things to the document, you should (probably) make it undoable.
|
||||
It's very easy, just wrap your action in a call to [`undoable`][].
|
||||
|
||||
```js
|
||||
var iframe = document.getElementById('jspaint-iframe');
|
||||
var jspaint = iframe.contentWindow;
|
||||
var icon = new Image();
|
||||
icon.src = "some-folder/some-image-15x11-pixels.png";
|
||||
jspaint.undoable({
|
||||
name: "Seam Carve",
|
||||
icon: icon, // optional
|
||||
}, function() {
|
||||
// do something to the canvas
|
||||
});
|
||||
```
|
||||
|
||||
#### <a href="#systemHooks.showSaveFileDialog" id="systemHooks.showSaveFileDialog">async function `systemHooks.showSaveFileDialog({ formats, defaultFileName, defaultPath, defaultFileFormatID, getBlob, savedCallbackUnreliable, dialogTitle })`</a>
|
||||
[`systemHooks.showSaveFileDialog`]: #systemHooks.showSaveFileDialog
|
||||
|
||||
Define this function to override the default save dialog.
|
||||
This is used both for saving images, as well as palette files, and animations.
|
||||
|
||||
Arguments:
|
||||
- `formats`: an array of objects representing types of files, with the following properties:
|
||||
- `formatID`: a string that uniquely identifies the format (may be the same as `mimeType`)
|
||||
- `mimeType`: the file format's designated [media type](https://en.wikipedia.org/wiki/Media_type), e.g. `"image/png"`
|
||||
- `name`: the file format's name, e.g. `"WebP"`
|
||||
- `nameWithExtensions`: the file format's name followed by a list of extensions, e.g. `"TIFF (*.tif;*.tiff)"`
|
||||
- `extensions`: an array of file extensions, excluding the dot, with the preferred extension first, e.g. `["bmp", "dib"]`
|
||||
- `defaultFileName`: a suggested file name, e.g. `"Untitled.png"` or the name of an open document.
|
||||
- `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.
|
||||
- `defaultFileFormatID`: the `formatID` of a file format to select by default.
|
||||
- `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.
|
||||
- `function savedCallbackUnreliable({ newFileName, newFileFormatID, newFileHandle, newBlob })`: a function you call when the user has saved the file. The `newBlob` should come from `getBlob(newFileFormatID)`.
|
||||
- `dialogTitle`: a title for the save dialog.
|
||||
|
||||
Note the inversion of control here:
|
||||
JS Paint calls your `systemHooks.showSaveFileDialog` function, and then you calls JS Paint's `getBlob` function.
|
||||
Once `getBlob` resolves, you can call the `savedCallbackUnreliable` function which is defined by JS Paint.
|
||||
(Hopefully I can clarify this in the future.)
|
||||
|
||||
Also note that this function is responsible for saving the file, not just picking a save location.
|
||||
You may reuse your `systemHooks.writeBlobToHandle` function if it's helpful.
|
||||
|
||||
#### <a href="#systemHooks.showOpenFileDialog" id="systemHooks.showOpenFileDialog">async function `systemHooks.showOpenFileDialog({ formats })`</a>
|
||||
[`systemHooks.showOpenFileDialog`]: #systemHooks.showOpenFileDialog
|
||||
|
||||
Define this function to override the default open dialog.
|
||||
This is used for opening images and palettes.
|
||||
|
||||
Arguments:
|
||||
- `formats`: same as `systemHooks.showSaveFileDialog`
|
||||
|
||||
Note that this function is responsible for loading the contents of the file, not just picking a file.
|
||||
You may reuse your `systemHooks.readBlobFromHandle` function if it's helpful.
|
||||
|
||||
#### <a href="#systemHooks.writeBlobToHandle" id="systemHooks.writeBlobToHandle">async function `systemHooks.writeBlobToHandle(fileHandle, blob)`</a>
|
||||
[`systemHooks.writeBlobToHandle`]: #systemHooks.writeBlobToHandle
|
||||
|
||||
Define this function to tell JS Paint how to save a file.
|
||||
|
||||
Arguments:
|
||||
- `fileHandle`: a file handle, as defined by your system, representing the file to write to.
|
||||
- `blob`: a `Blob` representing the file contents to save.
|
||||
|
||||
#### <a href="#systemHooks.readBlobFromHandle" id="systemHooks.readBlobFromHandle">async function `systemHooks.readBlobFromHandle(fileHandle)`</a>
|
||||
[`systemHooks.readBlobFromHandle`]: #systemHooks.readBlobFromHandle
|
||||
|
||||
Define this function to tell JS Paint how to load a file.
|
||||
|
||||
Arguments:
|
||||
- `fileHandle`: a file handle, as defined by your system, representing the file to read from.
|
||||
|
||||
#### <a href="#systemHooks.setWallpaperTiled" id="systemHooks.setWallpaperTiled">function `systemHooks.setWallpaperTiled(canvas)`</a>
|
||||
[`systemHooks.setWallpaperTiled`]: #systemHooks.setWallpaperTiled
|
||||
|
||||
Define this function to tell JS Paint how to set the wallpaper. See [Integrating Set as Wallpaper](#integrating-set-as-wallpaper) for an example.
|
||||
|
||||
Arguments:
|
||||
- `canvas`: a `HTMLCanvasElement` with the image to set as the wallpaper.
|
||||
|
||||
#### <a href="#systemHooks.setWallpaperCentered" id="systemHooks.setWallpaperCentered">function `systemHooks.setWallpaperCentered(canvas)`</a>
|
||||
[`systemHooks.setWallpaperCentered`]: #systemHooks.setWallpaperCentered
|
||||
|
||||
Define this function to tell JS Paint how to set the wallpaper. See [Integrating Set as Wallpaper](#integrating-set-as-wallpaper) for an example.
|
||||
|
||||
Arguments:
|
||||
- `canvas`: a `HTMLCanvasElement` with the image to set as the wallpaper.
|
||||
|
||||
#### <a href="#undoable" id="undoable">function `undoable({ name, icon }, actionFunction)`</a>
|
||||
[`undoable`]: #undoable
|
||||
|
||||
Use this to make an action undoable.
|
||||
|
||||
This function takes a snapshot of the canvas, and some other state, and then calls the `actionFunction` function.
|
||||
It creates an entry in the history so it can be undone.
|
||||
|
||||
Arguments:
|
||||
- `name`: a name for the action, e.g. `"Brush"` or `"Rotate Image 270°"`
|
||||
- `icon` (optional): an `Image` to display in the History window. It is recommended to be 15x11 pixels.
|
||||
- `actionFunction`: a function that takes no arguments, and modifies the canvas.
|
||||
|
||||
#### <a href="#show_error_message" id="show_error_message">function `show_error_message(message, [error])`</a>
|
||||
[`show_error_message`]: #show_error_message
|
||||
|
||||
Use this to show an error message dialog box, optionally with expandable error details.
|
||||
|
||||
Arguments:
|
||||
- `message`: plain text to show in the dialog box.
|
||||
- `error` (optional): an `Error` object to show in the dialog box, collapsed by default in a "Details" expandable section.
|
||||
|
||||
#### <a href="#open_from_file" id="open_from_file">function `open_from_file(blob, source_file_handle)`</a>
|
||||
[`open_from_file`]: #open_from_file
|
||||
|
||||
Use this to load a file into the app.
|
||||
|
||||
Arguments:
|
||||
- `blob`: a `Blob` object representing the file to load.
|
||||
- `source_file_handle`: a *corresponding* file handle for the file, as defined by your system.
|
||||
|
||||
Sorry for the quirky API.
|
||||
The 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.
|
||||
I decided to document it as the first version of the API, since I'll want a changelog when upgrading my usage of it anyways.
|
||||
|
||||
#### <a href="#set_theme" id="set_theme">function `set_theme(theme_file_name)`</a>
|
||||
[`set_theme`]: #set_theme
|
||||
|
||||
Use this to change the look of the application.
|
||||
|
||||
Arguments:
|
||||
- `theme_file_name`: the name of the theme file to load, one of:
|
||||
- `"classic.css"`: the Windows98 theme.
|
||||
- `"dark.css"`: the Dark theme.
|
||||
- `"modern.css"`: the Modern theme.
|
||||
- `"winter.css"`: the festive Winter theme.
|
||||
- `"occult.css"`: a Satanic theme.
|
||||
|
||||
#### <a href="#set_language" id="set_language">function `set_language(language_code)`</a>
|
||||
[`set_language`]: #set_language
|
||||
|
||||
You 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.
|
||||
And if that dialog isn't in the right language, well, they'll probably be confused.
|
||||
|
||||
Arguments:
|
||||
- `language_code`: the language code to use, e.g. `"en"` for English, `"zh"` for Traditional Chinese, `"zh-simplified"` for Simplified Chinese, etc.
|
||||
|
||||
#### Changelog
|
||||
|
||||
The API will change a lot, but changes will be documented in the [Changelog](CHANGELOG.md).
|
||||
|
||||
Not just a history of changes, but a migration/upgrading guide. <!-- These are some Ctrl+F keywords. -->
|
||||
|
||||
For general project news, click **Extras > Project News** in the app.
|
||||
|
||||
## License
|
||||
|
||||
JS Paint is free and open source software, licensed under the permissive [MIT license](https://opensource.org/licenses/MIT).
|
||||
|
||||
[](https://opensource.org/licenses/MIT)
|
||||
[](https://github.com/1j01/jspaint/stargazers)
|
||||
[](https://github.com/1j01/jspaint/network/members)
|
||||
|
||||
189
static/html/jspaint/TODO.md
Normal file
@@ -0,0 +1,189 @@
|
||||
|
||||
#  JS Paint Todo
|
||||
|
||||
### Help
|
||||
|
||||
* Link-esque things
|
||||
* Popups (I'd probably make the text within the popups selectable)
|
||||
* Related topics (I'd probably make this a heading with links instead of the weird context menu thing)
|
||||
* Add topics
|
||||
* In "Tips and Tricks" (which is just a lame section)
|
||||
* Transparency
|
||||
* Multi-user / collaboration / "To share the document On-Line" or whatever
|
||||
* Index
|
||||
* Search
|
||||
* Keyboard support
|
||||
|
||||
* Interactive tutorials?
|
||||
* Possibly hosted by Clippy, with [ClippyJS](https://www.smore.com/clippy-js)
|
||||
* Links the cat has a good "GetArtsy" animation, which would be good to use especially if talking about stamping and smearing selections
|
||||
* Highlight elements on the page
|
||||
* Be sure to cover undo/redo, and file saving
|
||||
|
||||
### Visual
|
||||
|
||||
* Fill bucket and airbrush cursors are supposed to invert the background in parts
|
||||
* Custom cursors in Edge; apparently they require `.cur` files? ugh
|
||||
|
||||
### Extended editing
|
||||
|
||||
* Optional fill tolerance (slider that you enable from a settings menu?)
|
||||
|
||||
* Transparency
|
||||
* Color opacity slider
|
||||
* Toggle between blend and copy (overwrite) modes
|
||||
* Maybe equivalize any rgba(X, X, X, 0) in fill algorithm?
|
||||
There'd still be the possibility of 1/255th opacity pixels,
|
||||
but if you're creating colors from the combination of a color picker and an opacity slider,
|
||||
you might naturally introduce differing zero-opacity color values a lot.
|
||||
|
||||
* Documents with multiple sub-images
|
||||
* Component to switch between sub-images
|
||||
* Deal with undo/redo for sub-images
|
||||
* Animated GIFs
|
||||
* Transparency ([jnordberg/gif.js issue #5](https://github.com/jnordberg/gif.js/issues/5))
|
||||
* Animated Transparent APNGs
|
||||
* APNG Library: [UPNG.js](https://github.com/photopea/UPNG.js/)
|
||||
* Multi-size Icons
|
||||
* Windows ICO ([jBinary can read](https://jdataview.github.io/jBinary.Repo/demo/#ico) and presumably write ICO files)
|
||||
* Mac ICNS
|
||||
* Layered images?
|
||||
* Photoshop PSD ([via psd.js](https://github.com/trevorlinton/psd.js))
|
||||
* OpenRaster ORA ([via ora.js](https://github.com/zsgalusz/ora.js/tree/master))
|
||||
* Paged Images?
|
||||
* PDF (via [pdf.js](https://github.com/mozilla/pdf.js)) (single page already supported)
|
||||
* DjVu (via [djvu.js](https://djvu.js.org/))
|
||||
* TIFF (via [utif.js](https://github.com/photopea/UTIF.js/)) (single page/frame already supported)
|
||||
|
||||
* Online (multi-user) and local (single-user) sessions
|
||||
* See [sessions.js](src/sessions.js)
|
||||
* Issues
|
||||
* There's no conflict resolution; user edits revert other user edits
|
||||
* It's not eventually consistent
|
||||
* Cursors from other users that go outside the parent can cause the page to be scrollable
|
||||
|
||||
* Symmetry, tesselation, painting texture on 3D models, and even an infinite canvas, all could be done with a shared system
|
||||
* For symmetry and tesselation, [geometry can be generated](), and then it can work the same as painting on a 3D model
|
||||
* An infinite canvas engine would generate simple square geometry, but would require support for multiple editable textures (also useful for 3D models)
|
||||
* And of course layers and animations and multi-size icons need a similar system (multiple sub-images)
|
||||
* For 3D model painting, it's important to note there's a few different possible approaches.
|
||||
1. UV-dynamic, like [Chameleon](https://www-ui.is.s.u-tokyo.ac.jp/~takeo/chameleon/chameleon.htm) & [Chameleon.js](https://tomtung.github.io/chameleon.js/) (can adapt texture resolution as you paint)
|
||||
2. UV-static
|
||||
1. Ray tracing the pointer to find texture coordinates (gives texture coordinate space scaled result by default)
|
||||
2. Screen-space drawing (gives screen space scaled result by default); I saw a good medium post or two about this
|
||||
* Also, some approaches might not extend to tesselation and symmetry. ["Very important, this means that we assume our uv has no overlapping triangles. So no \[tileable\] textures."](https://shahriyarshahrabi.medium.com/mesh-texture-painting-in-unity-using-shaders-8eb7fc31221c)
|
||||
* Existing 3D texturing systems:
|
||||
* Closed source project: https://discourse.threejs.org/t/a-fully-fledged-texture-painter-for-the-web/15678/16
|
||||
* Open source project with adaptive UVs: https://tomtung.github.io/chameleon.js/
|
||||
* Open source project with adaptive and static UV modes, for both painting and sculpting: https://github.com/stephomi/sculptgl (check Dynamic Topology > Activated)
|
||||
|
||||
* Save text and record transformations so the image can be saved as
|
||||
SVG (or HTML?) with invisible selectable transformed text elements?
|
||||
* Every time you move a selection, duplicate the text and create a clip-path for both parts?
|
||||
* Make only one of them audible for screen-readers
|
||||
|
||||
|
||||
### Device support
|
||||
|
||||
* Prevent text selection in buttons and history entries
|
||||
* Enlarge menus on touch devices
|
||||
* Enlarge window titlebar close buttons on touch devices
|
||||
* Magnifier: on touchscreens, wait until pointerup to zoom
|
||||
* To detect touchscreen usage, could keep track of whether the last pointermove had any buttons pressed... or use `pointerType`, right?
|
||||
* Alternative way to access "Color Eraser" feature without a secondary mouse button?
|
||||
* Alternative access to functionality that would normally require a keyboard (with a numpad!)
|
||||
* Numpad +/-: Increase/Decrease brush size, Double/Halve selection size, ...
|
||||
* Shift (toggles; rename contextually?):
|
||||
* Proportional Resize
|
||||
* Smear / Trail Selection
|
||||
* Snap to 8 directions
|
||||
* An isometric mode would also be good
|
||||
* Ctrl+Select: Crop tool or "Crop to selection" option
|
||||
* Don't drag toolbars out into windows with touch
|
||||
* Unless with two fingers perhaps
|
||||
* I might want to use multitouch on the tool buttons for MultiTools tho...
|
||||
|
||||
### Tools
|
||||
|
||||
* Select and Free-Form Select
|
||||
* Passive: create no undoables until you do something like move or invert the selection
|
||||
* You should be able to make a selection, then change the secondary color, then drag the selection cutting it out with the color you selected
|
||||
* Select and deselect with no actions in between should create no undoables
|
||||
* Proportionally resize selection while holding Shift
|
||||
* (or maybe by default? I feel like it should be the default, tbh.)
|
||||
|
||||
|
||||
* Text
|
||||
* If it would go over the edge of the canvas, reject the input (at least, that's what mspaint does)
|
||||
* Add padding left to text area when font has glyphs that extend left, like italic 'f' in Times New Roman
|
||||
* mspaint has access to font metrics
|
||||
* jspaint could render text to see when it would overflow
|
||||
* To do it efficiently,
|
||||
* Take all glyphs in the text
|
||||
* (And maybe a set of common letters like the alphabet)
|
||||
* Split with a library to handle Unicode (emojis etc.)
|
||||
* Uniquify
|
||||
* Place them *all on top of each other*, positioned absolutely, leaving room to the left of them to detect pixels
|
||||
* Scan the pixels at the left to find the maximum extent left
|
||||
* Could store, per font, what glyphs have been tested and what's the maximum extent detected, in order to not have to rerender these
|
||||
* "What glyphs have been tested" should be specific to font size and attributes, since an italic 'f' may extend more than a normal 'f' for instance
|
||||
* Store position of FontBox
|
||||
|
||||
|
||||
* Shape Styles
|
||||
* Shapes: respond to Ctrl (It's complicated)
|
||||
* Patterns (black and white mode, winter theme)
|
||||
* Check to make sure patterns are aligned properly for all the tools
|
||||
* There's supposed to be a mapping between color values and pattern fills, used by the text tool and for the palette when switching between modes (colors should be kept between going to black and white mode and back)
|
||||
|
||||
|
||||
### Desktop App (Electron)
|
||||
|
||||
* Create a landing page / home page for the desktop app (similar to https://desktop.webamp.org/ or https://desktop.github.com/) - (perhaps https://desktop.jspaint.app/) - and/or for JS Paint in general (perhaps https://jspaint.app/about/)
|
||||
|
||||
Electron boilerplate stuff:
|
||||
|
||||
* Remember window position/state
|
||||
* Set up autoupdating
|
||||
* Keep window hidden until loaded (`show: false`, [`ready-to-show`](https://electronjs.org/docs/api/browser-window#event-ready-to-show))
|
||||
|
||||
Security:
|
||||
|
||||
* context isolation
|
||||
* disable multiplayer???????????? should be fine
|
||||
|
||||
Functionality:
|
||||
|
||||
* Subwindows as separate windows
|
||||
* Document recovery without having to know about File > Manage Storage - pop up contextually with a dialog when you need it
|
||||
* Show link URLs when you hover over them, in the status bar (because we have a status bar! haha) (there's this API: [event: update-target-url](https://github.com/electron/electron/blob/master/docs/api/web-contents.md#event-update-target-url), which gave me the idea, or it could be implemented with mouse events)
|
||||
* Recent files (could also be implemented for 98.js.org in the future)
|
||||
* macOS:
|
||||
* `open-file` event
|
||||
* `win.setSheetOffset` with the menu bar height
|
||||
* test `setRepresentedFilename`, `setDocumentEdited`
|
||||
* Windows: maybe handle `session-end` event and ask to save?
|
||||
* Detect if file changes on disk, ask if you want to reload it?
|
||||
|
||||
### Also
|
||||
|
||||
* Anything marked `@TODO` or `@FIXME` in the source code
|
||||
|
||||
|
||||
* See [Issues on GitHub](https://github.com/1j01/jspaint/issues)
|
||||
|
||||
|
||||
* CSS
|
||||
* DRY, especially button styles, with [os-gui](https://github.com/1j01/os-gui)
|
||||
* Clearer `z-index` handling, maybe with CSS variables?
|
||||
|
||||
|
||||
* JS
|
||||
* Organize things into files better; "functions.js" is like ONE step above saying "code.js"
|
||||
* `$ToolWindow` has a `$Button` facility; `$DialogWindow` overrides it with essentially a better one; now there's `showMessageBox` too! and `$ToolWindow` is a wrapper for OS-GUI's `$Window`, and should be removed at some point; btw, should `show_error_message` functionality be folded into `showMessageBox`?
|
||||
* Make code clearer / improve code quality
|
||||
* https://codeclimate.com/github/1j01/jspaint
|
||||
|
||||
|
||||
* Images
|
||||
* Use a shared sprite sheet per theme (and optimize it I guess)
|
||||
BIN
static/html/jspaint/audio/chord.wav
Normal file
2
static/html/jspaint/browserconfig.xml
Normal file
@@ -0,0 +1,2 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<browserconfig><msapplication><tile><square70x70logo src="images/icons/ms-icon-70x70.png"/><square150x150logo src="images/icons/ms-icon-150x150.png"/><square310x310logo src="images/icons/ms-icon-310x310.png"/><TileColor>#008080</TileColor></tile></msapplication></browserconfig>
|
||||
5
static/html/jspaint/cypress.json
Normal file
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"projectId": "6im7v7",
|
||||
"baseUrl": "http://localhost:11822",
|
||||
"video": false
|
||||
}
|
||||
91
static/html/jspaint/cypress/cypress-image-snapshot-viewer.js
Normal file
@@ -0,0 +1,91 @@
|
||||
// ==UserScript==
|
||||
// @name Cypress Image Snapshot Viewer
|
||||
// @namespace https://github.com/1j01/
|
||||
// @version 0.1
|
||||
// @description Show diffs of screenshots within the Cypress Dashboard. Works with images from cypress-image-snapshot. To use, press D in the gallery, and then move the mouse over and out of the image.
|
||||
// @author Isaiah Odhner
|
||||
// @match https://dashboard.cypress.io/*
|
||||
// @grant none
|
||||
// @noframes
|
||||
// ==/UserScript==
|
||||
|
||||
(function () {
|
||||
'use strict';
|
||||
|
||||
let cleanUp = null;
|
||||
|
||||
function showDiffView(originalImg) {
|
||||
if (cleanUp) { cleanUp(); }
|
||||
|
||||
var screenshotWidth = originalImg.naturalWidth / 3;
|
||||
var screenshotHeight = originalImg.naturalHeight;
|
||||
originalImg.style.opacity = "0";
|
||||
var img = document.createElement("img");
|
||||
img.src = originalImg.src;
|
||||
img.style.position = "absolute";
|
||||
img.style.left = "0";
|
||||
img.style.pointerEvents = "all";
|
||||
img.draggable = false;
|
||||
img.addEventListener("mouseenter", () => {
|
||||
img.style.left = `${-2 * screenshotWidth}px`;
|
||||
});
|
||||
img.addEventListener("mouseleave", () => {
|
||||
img.style.left = "0";
|
||||
});
|
||||
var container = document.createElement("div");
|
||||
container.style.width = `${screenshotWidth}px`;
|
||||
container.style.height = `${screenshotHeight}px`;
|
||||
container.style.position = "relative";
|
||||
container.style.overflow = "hidden";
|
||||
container.style.margin = "auto";
|
||||
var outerContainer = document.createElement("div");
|
||||
outerContainer.style.position = "fixed";
|
||||
outerContainer.style.display = "flex";
|
||||
outerContainer.style.left = "0";
|
||||
outerContainer.style.right = "0";
|
||||
outerContainer.style.top = "0";
|
||||
outerContainer.style.bottom = "0";
|
||||
outerContainer.style.zIndex = "100000";
|
||||
outerContainer.style.pointerEvents = "none";
|
||||
|
||||
outerContainer.appendChild(container);
|
||||
container.appendChild(img);
|
||||
document.body.appendChild(outerContainer);
|
||||
|
||||
cleanUp = () => {
|
||||
originalImg.style.opacity = "";
|
||||
container.style.transformOrigin = "center center";
|
||||
container.style.transition = "opacity 0.2s ease, transform 0.2s ease";
|
||||
container.style.opacity = 0;
|
||||
container.style.transform = "scale(0.9)";
|
||||
setTimeout(() => {
|
||||
outerContainer.remove();
|
||||
}, 500);
|
||||
cleanUp = null;
|
||||
};
|
||||
}
|
||||
|
||||
addEventListener("keydown", e => {
|
||||
if (e.key === "d") {
|
||||
if (cleanUp) {
|
||||
cleanUp();
|
||||
} else {
|
||||
var originalImg = document.elementFromPoint(innerWidth / 2, innerHeight / 2);
|
||||
if (!originalImg || !originalImg.matches("img")) {
|
||||
console.warn("Didn't find an image in the middle of the page. Found", originalImg);
|
||||
return;
|
||||
}
|
||||
showDiffView(originalImg);
|
||||
}
|
||||
} else if (e.key === "Escape") {
|
||||
if (cleanUp) { cleanUp(); }
|
||||
}
|
||||
});
|
||||
|
||||
// mousedown is TAKEN - with stopPropagation, presumably
|
||||
// (useCapture doesn't help)
|
||||
addEventListener("pointerdown", (e) => {
|
||||
if (cleanUp) { cleanUp(); }
|
||||
});
|
||||
|
||||
}());
|
||||
5
static/html/jspaint/cypress/fixtures/example.json
Normal file
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"name": "Using fixtures to represent data",
|
||||
"email": "hello@cypress.io",
|
||||
"body": "Fixtures are a great way to mock data for responses to routes"
|
||||
}
|
||||
224
static/html/jspaint/cypress/integration/tool-tests.spec.js
Normal file
@@ -0,0 +1,224 @@
|
||||
/// <reference types="Cypress" />
|
||||
|
||||
context('tool tests', () => {
|
||||
// @TODO: make rounded tools render consistently across platforms
|
||||
const roundedToolsCompareOptions = {
|
||||
failureThreshold: 13,
|
||||
failureThresholdType: 'pixel'
|
||||
};
|
||||
|
||||
// beforeAll isn't a thing, and beforeEach applies also to tests declared above it,
|
||||
// so do this fake test + flag hack in order to execute some steps before the first test
|
||||
let before_first_real_test = true;
|
||||
it(`(fake test for setup)`, () => {
|
||||
cy.visit('/')
|
||||
cy.setResolution([800, 500]);
|
||||
cy.window().should('have.property', 'selected_colors'); // wait for app to be loaded
|
||||
before_first_real_test = false;
|
||||
});
|
||||
beforeEach(() => {
|
||||
if (before_first_real_test) return;
|
||||
cy.window().then({ timeout: 60000 }, async (win) => {
|
||||
win.selected_colors.foreground = "#000";
|
||||
win.selected_colors.background = "#fff";
|
||||
win.brush_shape = win.default_brush_shape;
|
||||
win.brush_size = win.default_brush_size
|
||||
win.eraser_size = win.default_eraser_size;
|
||||
win.airbrush_size = win.default_airbrush_size;
|
||||
win.pencil_size = win.default_pencil_size;
|
||||
win.stroke_size = win.default_stroke_size;
|
||||
win.clear();
|
||||
});
|
||||
});
|
||||
|
||||
const simulateGesture = (win, { start, end, shift, shiftToggleChance = 0.01, secondary, secondaryToggleChance, target }) => {
|
||||
target = target || win.$(".main-canvas")[0];
|
||||
let startWithinRect = target.getBoundingClientRect();
|
||||
let canvasAreaRect = win.$(".canvas-area")[0].getBoundingClientRect();
|
||||
|
||||
let startMinX = Math.max(startWithinRect.left, canvasAreaRect.left);
|
||||
let startMaxX = Math.min(startWithinRect.right, canvasAreaRect.right);
|
||||
let startMinY = Math.max(startWithinRect.top, canvasAreaRect.top);
|
||||
let startMaxY = Math.min(startWithinRect.bottom, canvasAreaRect.bottom);
|
||||
let startPointX = startMinX + start.x * (startMaxX - startMinX);
|
||||
let startPointY = startMinY + start.y * (startMaxY - startMinY);
|
||||
let endPointX = startMinX + end.x * (startMaxX - startMinX);
|
||||
let endPointY = startMinY + end.y * (startMaxY - startMinY);
|
||||
|
||||
const $cursor = win.$(`<img src="images/cursors/default.png" class="user-cursor"/>`);
|
||||
$cursor.css({
|
||||
position: "absolute",
|
||||
left: 0,
|
||||
top: 0,
|
||||
opacity: 0,
|
||||
zIndex: 5, // @#: z-index
|
||||
pointerEvents: "none",
|
||||
transition: "opacity 0.5s",
|
||||
});
|
||||
$cursor.appendTo(".jspaint");
|
||||
let triggerMouseEvent = (type, point) => {
|
||||
|
||||
const clientX = point.x;
|
||||
const clientY = point.y;
|
||||
// const el_over = win.document.elementFromPoint(clientX, clientY);
|
||||
const do_nothing = false;//!type.match(/move/) && (!el_over || !el_over.closest(".canvas-area"));
|
||||
$cursor.css({
|
||||
display: "block",
|
||||
position: "absolute",
|
||||
left: clientX,
|
||||
top: clientY,
|
||||
opacity: do_nothing ? 0.5 : 1,
|
||||
});
|
||||
if (do_nothing) {
|
||||
return;
|
||||
}
|
||||
|
||||
let event = new win.$.Event(type, {
|
||||
view: window,
|
||||
bubbles: true,
|
||||
cancelable: true,
|
||||
clientX,
|
||||
clientY,
|
||||
screenX: clientX,
|
||||
screenY: clientY,
|
||||
offsetX: point.x,
|
||||
offsetY: point.y,
|
||||
button: secondary ? 2 : 0,
|
||||
buttons: secondary ? 2 : 1,
|
||||
shiftKey: shift,
|
||||
});
|
||||
win.$(target).trigger(event);
|
||||
};
|
||||
|
||||
let t = 0;
|
||||
const stepsInGesture = 3;
|
||||
let pointForTime = (t) => {
|
||||
return {
|
||||
x: startPointX + (endPointX - startPointX) * t,
|
||||
y: startPointY + (endPointY - startPointY) * Math.pow(t, 0.3),
|
||||
};
|
||||
};
|
||||
|
||||
return new Promise((resolve) => {
|
||||
triggerMouseEvent("pointerenter", pointForTime(t)); // so dynamic cursors follow the simulation cursor
|
||||
triggerMouseEvent("pointerdown", pointForTime(t));
|
||||
let move = () => {
|
||||
t += 1 / stepsInGesture;
|
||||
// if (seededRandom() < shiftToggleChance) {
|
||||
// shift = !shift;
|
||||
// }
|
||||
// if (seededRandom() < secondaryToggleChance) {
|
||||
// secondary = !secondary;
|
||||
// }
|
||||
if (t > 1) {
|
||||
triggerMouseEvent("pointerup", pointForTime(t));
|
||||
|
||||
$cursor.remove();
|
||||
|
||||
resolve();
|
||||
} else {
|
||||
triggerMouseEvent("pointermove", pointForTime(t));
|
||||
/*gestureTimeoutID =*/ setTimeout(move, 10);
|
||||
}
|
||||
};
|
||||
triggerMouseEvent("pointerleave", pointForTime(t));
|
||||
move();
|
||||
});
|
||||
};
|
||||
|
||||
// const gesture = (points) => {
|
||||
// const options = { secondary: false, shift: false };
|
||||
// // @TODO: while loop
|
||||
// trigger("pointerenter", points[0].x, points[0].y, options);
|
||||
// trigger("pointerdown", points[0].x, points[0].y, options);
|
||||
// let i = 0;
|
||||
// for (; i < points.length; i++) {
|
||||
// trigger("pointermove", points[i].x, points[i].y, options);
|
||||
// }
|
||||
// i--;
|
||||
// trigger("pointerup", points[i].x, points[i].y, options);
|
||||
// };
|
||||
|
||||
// it('brush tool', () => {
|
||||
// cy.get(".tool[title='Brush']").click();
|
||||
// // gesture([{ x: 50, y: 50 }, { x: 100, y: 100 }]);
|
||||
// cy.get(".swatch:nth-child(21)").rightclick();
|
||||
// cy.window().then({ timeout: 8000 }, async (win) => {
|
||||
// for (let secondary = 0; secondary <= 1; secondary++) {
|
||||
// for (let b = 0; b < 12; b++) {
|
||||
// win.$(`.chooser > :nth-child(${b + 1})`).click();
|
||||
// const start = { x: 0.05 + b * 0.05, y: 0.1 + 0.1 * secondary };
|
||||
// const end = { x: start.x + 0.04, y: start.y + 0.04 };
|
||||
// await simulateGesture(win, { shift: false, secondary: !!secondary, start, end });
|
||||
// }
|
||||
// }
|
||||
// });
|
||||
// cy.matchImageSnapshot();
|
||||
// });
|
||||
|
||||
// @TODO: test transparent document mode
|
||||
it(`eraser tool`, () => {
|
||||
cy.get(`.tool[title='Eraser/Color Eraser']`).click();
|
||||
// gesture([{ x: 50, y: 50 }, { x: 100, y: 100 }]);
|
||||
cy.window().then({ timeout: 60000 }, async (win) => {
|
||||
for (let row = 0; row < 4; row++) {
|
||||
const secondary = !!(row % 2);
|
||||
const increaseSize = row >= 2;
|
||||
let $options = win.$(`.chooser > *`);
|
||||
for (let o = 0; o < $options.length; o++) {
|
||||
$options[o].click();
|
||||
if (increaseSize) {
|
||||
for (let i = 0; i < 5; i++) {
|
||||
win.$('body').trigger(new win.$.Event("keydown", { key: "NumpadPlus", keyCode: 107, which: 107 }));
|
||||
}
|
||||
}
|
||||
win.selected_colors.background = "#f0f";
|
||||
const start = { x: 0.05 + o * 0.05, y: 0.1 + 0.1 * row };
|
||||
const end = { x: start.x + 0.04, y: start.y + 0.04 };
|
||||
await simulateGesture(win, { shift: false, secondary: false, start, end });
|
||||
if (secondary) {
|
||||
// eslint-disable-next-line require-atomic-updates
|
||||
win.selected_colors.background = "#ff0";
|
||||
// eslint-disable-next-line require-atomic-updates
|
||||
win.selected_colors.foreground = "#f0f";
|
||||
const start = { x: 0.04 + o * 0.05, y: 0.11 + 0.1 * row };
|
||||
const end = { x: start.x + 0.03, y: start.y + 0.02 };
|
||||
await simulateGesture(win, { shift: false, secondary: true, start, end });
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
cy.get(".main-canvas").matchImageSnapshot();
|
||||
});
|
||||
|
||||
["Brush", "Pencil", "Rectangle", "Rounded Rectangle", "Ellipse", "Line"].forEach((toolName) => {
|
||||
it(`${toolName.toLowerCase()} tool`, () => {
|
||||
cy.get(`.tool[title='${toolName}']`).click();
|
||||
// gesture([{ x: 50, y: 50 }, { x: 100, y: 100 }]);
|
||||
cy.get(".swatch:nth-child(22)").rightclick();
|
||||
cy.window().then({ timeout: 60000 }, async (win) => {
|
||||
for (let row = 0; row < 4; row++) {
|
||||
const secondary = !!(row % 2);
|
||||
const increaseSize = row >= 2;
|
||||
let $options = win.$(`.chooser > *`);
|
||||
// Pencil has no options
|
||||
if ($options.length === 0) {
|
||||
$options = win.$("<dummy>");
|
||||
}
|
||||
for (let o = 0; o < $options.length; o++) {
|
||||
$options[o].click();
|
||||
if (increaseSize && (o === 0 || toolName === "Brush" || toolName === "Line")) {
|
||||
for (let i = 0; i < 5; i++) {
|
||||
win.$('body').trigger(new win.$.Event("keydown", { key: "NumpadPlus", keyCode: 107, which: 107 }));
|
||||
}
|
||||
}
|
||||
const start = { x: 0.05 + o * 0.05, y: 0.1 + 0.1 * row };
|
||||
const end = { x: start.x + 0.04, y: start.y + 0.04 };
|
||||
await simulateGesture(win, { shift: false, secondary: !!secondary, start, end });
|
||||
}
|
||||
}
|
||||
});
|
||||
cy.get(".main-canvas").matchImageSnapshot(toolName.match(/Rounded Rectangle|Ellipse/) ? roundedToolsCompareOptions : undefined);
|
||||
});
|
||||
});
|
||||
});
|
||||
181
static/html/jspaint/cypress/integration/visual-tests.spec.js
Normal file
@@ -0,0 +1,181 @@
|
||||
/// <reference types="Cypress" />
|
||||
|
||||
context('visual tests', () => {
|
||||
|
||||
const withTextCompareOptions = {
|
||||
failureThreshold: 0.05,
|
||||
failureThresholdType: 'percent' // not actually percent - fraction
|
||||
};
|
||||
const withMuchTextCompareOptions = {
|
||||
failureThreshold: 0.08,
|
||||
failureThresholdType: 'percent' // not actually percent - fraction
|
||||
};
|
||||
const toolboxCompareOptions = {
|
||||
failureThreshold: 40,
|
||||
failureThresholdType: 'pixel'
|
||||
};
|
||||
|
||||
const selectTheme = (themeName) => {
|
||||
cy.contains(".menu-button", "Extras").click();
|
||||
cy.contains(".menu-item", "Theme").click();
|
||||
cy.contains(".menu-item", themeName).click();
|
||||
cy.get(".status-text").click(); // close menu (@TODO: menus should probably always be closed when you select a menu item)
|
||||
cy.wait(1000); // give a bit of time for theme to load
|
||||
};
|
||||
|
||||
it('main screenshot', () => {
|
||||
cy.visit('/');
|
||||
cy.setResolution([760, 490]);
|
||||
cy.window().should('have.property', 'get_tool_by_id'); // wait for app to be loaded
|
||||
cy.matchImageSnapshot(withTextCompareOptions);
|
||||
});
|
||||
|
||||
it('brush selected', () => {
|
||||
cy.get('.tool[title="Brush"]').click();
|
||||
cy.get('.tools-component').matchImageSnapshot(toolboxCompareOptions);
|
||||
});
|
||||
it('select selected', () => {
|
||||
cy.get('.tool[title="Select"]').click();
|
||||
cy.get('.tools-component').matchImageSnapshot(toolboxCompareOptions);
|
||||
});
|
||||
it('magnifier selected', () => {
|
||||
cy.get('.tool[title="Magnifier"]').click();
|
||||
cy.get('.tools-component').matchImageSnapshot(toolboxCompareOptions);
|
||||
});
|
||||
it('airbrush selected', () => {
|
||||
cy.get('.tool[title="Airbrush"]').click();
|
||||
cy.get('.tools-component').matchImageSnapshot(toolboxCompareOptions);
|
||||
});
|
||||
it('eraser selected', () => {
|
||||
cy.get('.tool[title="Eraser/Color Eraser"]').click();
|
||||
cy.get('.tools-component').matchImageSnapshot(toolboxCompareOptions);
|
||||
});
|
||||
it('line selected', () => {
|
||||
cy.get('.tool[title="Line"]').click();
|
||||
cy.get('.tools-component').matchImageSnapshot(toolboxCompareOptions);
|
||||
});
|
||||
it('rectangle selected', () => {
|
||||
cy.get('.tool[title="Rectangle"]').click();
|
||||
cy.get('.tools-component').matchImageSnapshot(toolboxCompareOptions);
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
if (Cypress.$('.window:visible')[0]) {
|
||||
cy.get('.window:visible .window-close-button').click();
|
||||
cy.get('.window').should('not.be.visible');
|
||||
}
|
||||
});
|
||||
|
||||
it('image attributes window', () => {
|
||||
cy.get('body').type('{ctrl}e');
|
||||
cy.get('.window:visible').matchImageSnapshot(withMuchTextCompareOptions);
|
||||
});
|
||||
|
||||
it('flip and rotate window', () => {
|
||||
// @TODO: make menus more testable, with IDs
|
||||
cy.get('.menus > .menu-container:nth-child(4) > .menu-button > .menu-hotkey').click();
|
||||
cy.get('.menus > .menu-container:nth-child(4) > .menu-popup > table > tr:nth-child(1)').click();
|
||||
cy.get('.window:visible').matchImageSnapshot(withMuchTextCompareOptions);
|
||||
});
|
||||
|
||||
it('stretch and skew window', () => {
|
||||
// @TODO: make menus more testable, with IDs
|
||||
cy.get('.menus > .menu-container:nth-child(4) > .menu-button > .menu-hotkey').click();
|
||||
cy.get('.menus > .menu-container:nth-child(4) > .menu-popup > table > tr:nth-child(2)').click();
|
||||
// @TODO: wait for images to load and include images?
|
||||
cy.get('.window:visible').matchImageSnapshot(Object.assign({}, withTextCompareOptions, { blackout: ["img"] }));
|
||||
});
|
||||
|
||||
it('help window', () => {
|
||||
// @TODO: make menus more testable, with IDs
|
||||
cy.get('.menus > .menu-container:nth-child(6) > .menu-button > .menu-hotkey').click();
|
||||
cy.get('.menus > .menu-container:nth-child(6) > .menu-popup > table > tr:nth-child(1)').click();
|
||||
cy.get('.window:visible .folder', { timeout: 10000 }); // wait for sidebar contents to load
|
||||
// @TODO: wait for iframe to load
|
||||
cy.get('.window:visible').matchImageSnapshot(Object.assign({}, withTextCompareOptions, { blackout: ["iframe"] }));
|
||||
});
|
||||
|
||||
it('about window', () => {
|
||||
// @TODO: make menus more testable, with IDs
|
||||
cy.get('.menus > .menu-container:nth-child(6) > .menu-button > .menu-hotkey').click();
|
||||
cy.get('.menus > .menu-container:nth-child(6) > .menu-popup > table > tr:nth-child(3)').click();
|
||||
cy.get('.window:visible').matchImageSnapshot(Object.assign({}, withMuchTextCompareOptions, { blackout: ["img", "#maybe-outdated-line"] }));
|
||||
});
|
||||
|
||||
it('eye gaze mode', () => {
|
||||
cy.get('.tool[title="Select"]').click();
|
||||
cy.contains(".menu-button", "Extras").click();
|
||||
cy.contains(".menu-item", "Eye Gaze Mode").click();
|
||||
cy.wait(100);
|
||||
// cy.contains(".menu-button", "View").click();
|
||||
// cy.get("body").trigger("pointermove", { clientX: 200, clientY: 150 });
|
||||
cy.get(".status-text").click();
|
||||
cy.wait(100);
|
||||
cy.matchImageSnapshot(withTextCompareOptions);
|
||||
});
|
||||
|
||||
it('modern theme eye gaze mode', () => {
|
||||
selectTheme("Modern");
|
||||
// cy.contains(".menu-button", "View").click();
|
||||
// cy.get("body").trigger("pointermove", { clientX: 200, clientY: 150 });
|
||||
cy.wait(100);
|
||||
cy.matchImageSnapshot(withTextCompareOptions);
|
||||
});
|
||||
|
||||
it('modern theme', () => {
|
||||
cy.contains(".menu-button", "Extras").click();
|
||||
cy.contains(".menu-item", "Eye Gaze Mode").click();
|
||||
cy.wait(100);
|
||||
// cy.contains(".menu-button", "View").click();
|
||||
// cy.get("body").trigger("pointermove", { clientX: 200, clientY: 150 });
|
||||
cy.get(".status-text").click();
|
||||
cy.wait(100);
|
||||
cy.matchImageSnapshot(withTextCompareOptions);
|
||||
});
|
||||
|
||||
const test_edit_colors_dialog = (expand = true) => {
|
||||
cy.contains(".menu-button", "Colors").click();
|
||||
cy.contains(".menu-item", "Edit Colors").click();
|
||||
cy.wait(100);
|
||||
if (expand) {
|
||||
cy.contains("button", "Define Custom Colors >>").click();
|
||||
}
|
||||
cy.get('.window:visible').matchImageSnapshot(Object.assign({}, withTextCompareOptions));
|
||||
};
|
||||
it('modern theme edit colors dialog (expanded)', test_edit_colors_dialog);
|
||||
|
||||
it('winter theme', () => {
|
||||
selectTheme("Winter");
|
||||
// cy.contains(".menu-button", "View").click();
|
||||
// cy.get("body").trigger("pointermove", { clientX: 200, clientY: 150 });
|
||||
cy.wait(100);
|
||||
cy.matchImageSnapshot(withTextCompareOptions);
|
||||
});
|
||||
|
||||
it('winter theme edit colors dialog (expanded)', test_edit_colors_dialog);
|
||||
|
||||
it('winter theme vertical color box', () => {
|
||||
cy.wait(500);
|
||||
cy.contains(".menu-button", "Extras").click();
|
||||
cy.contains(".menu-item", "Vertical Color Box").click();
|
||||
cy.wait(500);
|
||||
cy.get(".status-text").click();
|
||||
cy.wait(100);
|
||||
cy.matchImageSnapshot(withTextCompareOptions);
|
||||
});
|
||||
|
||||
it('classic theme vertical color box', () => {
|
||||
selectTheme("Classic");
|
||||
cy.matchImageSnapshot(withTextCompareOptions);
|
||||
});
|
||||
|
||||
it('classic theme edit colors dialog', () => {
|
||||
test_edit_colors_dialog(false);
|
||||
});
|
||||
|
||||
it('modern theme vertical color box', () => {
|
||||
selectTheme("Modern");
|
||||
cy.matchImageSnapshot(withTextCompareOptions);
|
||||
});
|
||||
|
||||
});
|
||||
11
static/html/jspaint/cypress/plugins/index.js
Normal file
@@ -0,0 +1,11 @@
|
||||
// This function is called when a project is opened or re-opened (e.g. due to
|
||||
// the project's config changing)
|
||||
|
||||
const {
|
||||
addMatchImageSnapshotPlugin,
|
||||
} = require("cypress-image-snapshot/plugin");
|
||||
module.exports = (on, config) => {
|
||||
// `on` is used to hook into various events Cypress emits
|
||||
// `config` is the resolved Cypress config
|
||||
addMatchImageSnapshotPlugin(on, config);
|
||||
};
|
||||
|
After Width: | Height: | Size: 6.6 KiB |
|
After Width: | Height: | Size: 2.6 KiB |
|
After Width: | Height: | Size: 3.5 KiB |
|
After Width: | Height: | Size: 4.1 KiB |
|
After Width: | Height: | Size: 2.2 KiB |
|
After Width: | Height: | Size: 1.9 KiB |
|
After Width: | Height: | Size: 2.5 KiB |
|
After Width: | Height: | Size: 43 KiB |
|
After Width: | Height: | Size: 3.8 KiB |
|
After Width: | Height: | Size: 3.4 KiB |
|
After Width: | Height: | Size: 16 KiB |
|
After Width: | Height: | Size: 17 KiB |
|
After Width: | Height: | Size: 3.2 KiB |
|
After Width: | Height: | Size: 50 KiB |
|
After Width: | Height: | Size: 22 KiB |
|
After Width: | Height: | Size: 37 KiB |
|
After Width: | Height: | Size: 40 KiB |
|
After Width: | Height: | Size: 3.3 KiB |
|
After Width: | Height: | Size: 3.4 KiB |
|
After Width: | Height: | Size: 15 KiB |
|
After Width: | Height: | Size: 34 KiB |
|
After Width: | Height: | Size: 56 KiB |
|
After Width: | Height: | Size: 28 KiB |
|
After Width: | Height: | Size: 27 KiB |
|
After Width: | Height: | Size: 3.1 KiB |
|
After Width: | Height: | Size: 4.1 KiB |
|
After Width: | Height: | Size: 23 KiB |
|
After Width: | Height: | Size: 32 KiB |
|
After Width: | Height: | Size: 32 KiB |
|
After Width: | Height: | Size: 31 KiB |
14
static/html/jspaint/cypress/support/commands.js
Normal file
@@ -0,0 +1,14 @@
|
||||
import { addMatchImageSnapshotCommand } from 'cypress-image-snapshot/command';
|
||||
addMatchImageSnapshotCommand({
|
||||
failureThreshold: 0,
|
||||
failureThresholdType: 'pixel',
|
||||
customDiffConfig: { threshold: 0 },
|
||||
capture: 'viewport',
|
||||
});
|
||||
Cypress.Commands.add("setResolution", (size) => {
|
||||
if (Cypress._.isArray(size)) {
|
||||
cy.viewport(size[0], size[1]);
|
||||
} else {
|
||||
cy.viewport(size);
|
||||
}
|
||||
})
|
||||
7
static/html/jspaint/cypress/support/index.js
Normal file
@@ -0,0 +1,7 @@
|
||||
// ***********************************************************
|
||||
// This support/index.js is processed and
|
||||
// loaded automatically before your test files.
|
||||
//
|
||||
// https://on.cypress.io/configuration
|
||||
|
||||
import './commands'
|
||||
BIN
static/html/jspaint/favicon.ico
Normal file
|
After Width: | Height: | Size: 99 KiB |
BIN
static/html/jspaint/help/cloud-mask.png
Normal file
|
After Width: | Height: | Size: 78 KiB |
BIN
static/html/jspaint/help/clouds.jpg
Normal file
|
After Width: | Height: | Size: 185 KiB |
209
static/html/jspaint/help/coUA.css
Normal file
@@ -0,0 +1,209 @@
|
||||
/* Originally: Cascading Style Sheet for IE4.01 last updated 1-28-98 */
|
||||
|
||||
/* for scrollbars and selection color */
|
||||
@import "../lib/os-gui/windows-98.css";
|
||||
|
||||
body {
|
||||
background: #FFFFFF;
|
||||
/* background: var(--Info); */
|
||||
/* color: var(--InfoText); */
|
||||
color: var(--WindowText);
|
||||
font-size: 70%;
|
||||
font-family: Verdana, Arial, Helvetica, "MS Sans Serif";
|
||||
}
|
||||
|
||||
a:link {
|
||||
color: #0000CC;
|
||||
color: var(--HotTrackingColor);
|
||||
}
|
||||
|
||||
a:active {
|
||||
color: #996699;
|
||||
}
|
||||
|
||||
a:visited {
|
||||
color: #996699;
|
||||
}
|
||||
|
||||
p {
|
||||
margin-top: .6em;
|
||||
margin-bottom: .6em;
|
||||
}
|
||||
|
||||
p.bigfix {
|
||||
margin-top: -.4em;
|
||||
margin-bottom: 0em;
|
||||
}
|
||||
|
||||
p.margin {
|
||||
/* for SMS links */
|
||||
margin-left: 2em;
|
||||
margin-top: -1.75em;
|
||||
}
|
||||
|
||||
p.K2 {
|
||||
margin-top: 0em;
|
||||
margin-left: 10pt;
|
||||
}
|
||||
|
||||
/* HEADING TAGS */
|
||||
|
||||
h1 {
|
||||
font-size: 145%;
|
||||
margin-bottom: .5em;
|
||||
}
|
||||
|
||||
h2 {
|
||||
font-size: 125%;
|
||||
margin-top: 1.5em;
|
||||
margin-bottom: .5em;
|
||||
}
|
||||
|
||||
h3 {
|
||||
font-size: 110%;
|
||||
margin-top: 1.2em;
|
||||
margin-bottom: .5em;
|
||||
}
|
||||
|
||||
h4 {
|
||||
font-size: 105%;
|
||||
margin-top: 1.2em;
|
||||
margin-bottom: .5em;
|
||||
}
|
||||
|
||||
h5 {
|
||||
font-size: 100%;
|
||||
margin-top: 1.2em;
|
||||
margin-bottom: .5em;
|
||||
}
|
||||
|
||||
big {
|
||||
font-weight: bold;
|
||||
font-size: 105%;
|
||||
}
|
||||
|
||||
p.proclabel {
|
||||
/* procedure heading */
|
||||
font-weight: bold;
|
||||
font-size: 100%;
|
||||
margin-top: 1.2em;
|
||||
}
|
||||
|
||||
|
||||
/* LIST TAGS */
|
||||
|
||||
ol {
|
||||
margin-top: .6em;
|
||||
margin-bottom: 0em;
|
||||
margin-left: 4em; /* @FIXME */
|
||||
}
|
||||
|
||||
ul {
|
||||
margin-top: .6em;
|
||||
margin-bottom: 0em;
|
||||
}
|
||||
|
||||
ol ul {
|
||||
list-style: disc;
|
||||
margin-top: .6em;
|
||||
}
|
||||
|
||||
ul ul {
|
||||
list-style: disc;
|
||||
}
|
||||
|
||||
li {
|
||||
margin-bottom: .7em;
|
||||
margin-left: -2em;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/* TERM AND DEFINITION TAGS */
|
||||
|
||||
dl {
|
||||
margin-top: 0em;
|
||||
}
|
||||
|
||||
dt {
|
||||
font-weight: bold;
|
||||
margin-top: 1em;
|
||||
/* margin-left: 0em; */ /* for SMS terms */
|
||||
margin-left: 1.5em;
|
||||
}
|
||||
|
||||
dd {
|
||||
margin-bottom: 0em;
|
||||
/*not currently working*/
|
||||
margin-left: 1.5em;
|
||||
}
|
||||
|
||||
dl li {
|
||||
margin-bottom: .7em;
|
||||
} /*list item inside a term/def list*/
|
||||
|
||||
dl dl {
|
||||
margin-top: 0em;
|
||||
margin-left: 0em;
|
||||
} /*term/def list inside a term/def list*/
|
||||
|
||||
|
||||
|
||||
/* TABLE TAGS */
|
||||
|
||||
|
||||
table {
|
||||
font-size: 100%;
|
||||
margin-top: 1em;
|
||||
margin-bottom: 1em;
|
||||
}
|
||||
|
||||
th {
|
||||
text-align: left;
|
||||
vertical-align: bottom;
|
||||
background: #dddddd;
|
||||
}
|
||||
|
||||
th.center {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
tr {
|
||||
vertical-align: top;
|
||||
}
|
||||
|
||||
td {
|
||||
/* not used for K2 */
|
||||
vertical-align: top;
|
||||
background: #eeeeee;
|
||||
}
|
||||
|
||||
|
||||
/* MISC. TAGS */
|
||||
|
||||
pre {
|
||||
font-family: Courier;
|
||||
font-size: 125%;
|
||||
margin-top: 1.2em;
|
||||
margin-bottom: 1.5em;
|
||||
}
|
||||
|
||||
code {
|
||||
font-family: Courier;
|
||||
font-size: 125%;
|
||||
}
|
||||
|
||||
pre code {
|
||||
font-size: 100%;
|
||||
}
|
||||
|
||||
hr.sms {
|
||||
/* SMS specific rule used under procedure title */
|
||||
color: black;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
hr.iis {
|
||||
/* IIS specific - preceding copyright */
|
||||
color: black;
|
||||
}
|
||||
34
static/html/jspaint/help/default.html
Normal file
@@ -0,0 +1,34 @@
|
||||
<!DOCTYPE html>
|
||||
<html dir="ltr" lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>Welcome to Help</title>
|
||||
<link href="nobgcolor.css" rel="stylesheet" type="text/css">
|
||||
</head>
|
||||
<body>
|
||||
<div id="background-animation"></div>
|
||||
<hr id="os-logo-colorbar">
|
||||
<div id="foreground-contents">
|
||||
<a href="https://98.js.org/" target="_blank" id="os-logo-link">
|
||||
<img src="../images/98.js.org.svg" alt="98.js.org" id="os-logo" height="100">
|
||||
</a>
|
||||
<br>
|
||||
<p><font size="2"><b>Welcome to Help</b></font></p>
|
||||
<p>Use the Help system to learn more about <a href="https://98.js.org/" target="_blank">98.js.org</a> and JS Paint.</p>
|
||||
<ul>
|
||||
<li>Find answers to your questions.</li>
|
||||
<li><strike>Browse the online version of the <i>Getting Started</i> book.</strike></li>
|
||||
<li><strike>Connect to the Web to get software updates.</strike></li>
|
||||
<li><strike>Troubleshoot your system.</strike></li>
|
||||
</ul>
|
||||
<br>
|
||||
<br>
|
||||
<br>
|
||||
<br>
|
||||
<br>
|
||||
<br>
|
||||
<p><a href="memcopy.html">Original documentation © 1998 Microsoft Corporation, all rights reserved.</a></p>
|
||||
</div>
|
||||
<script src="vaporwave.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
BIN
static/html/jspaint/help/flag&clouds.gif
Normal file
|
After Width: | Height: | Size: 40 KiB |
17
static/html/jspaint/help/memcopy.html
Normal file
@@ -0,0 +1,17 @@
|
||||
<!DOCTYPE html>
|
||||
<html dir="ltr" lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>Legal Information</title>
|
||||
<link href="coUA.css" rel="stylesheet" type="text/css">
|
||||
</head>
|
||||
<body bgcolor="#FFFFFF" text="#000000">
|
||||
<h1 id="LegalInformation">Legal Information</h1>
|
||||
<p><strong>Microsoft Windows 98</strong></p>
|
||||
<p>Information in this document is subject to change without notice. The names of companies, products, people, characters, and/or data mentioned herein are fictitious and are in no way intended to represent any real individual, company, product, or event, unless otherwise noted. Complying with all applicable copyright laws is the responsibility of the user. No part of this document may be reproduced or transmitted in any form or by any means, electronic or mechanical, for any purpose, without the express written permission of Microsoft Corporation.</p>
|
||||
<p>Microsoft may have patents, patent applications, trademarks, copyrights, or other intellectual property rights covering subject matter in this document. Except as expressly provided in any written license agreement from Microsoft, the furnishing of this document does not give you any license to these patents, trademarks, copyrights, or other intellectual property.</p>
|
||||
<p>© 1998 Microsoft Corporation. All rights reserved.</p>
|
||||
<p>Microsoft, ActiveX, BackOffice, MS, MS-DOS, MSN, Windows, and Windows NT are either registered trademarks or trademarks of Microsoft Corporation in the U.S.A. and/or other countries.</p>
|
||||
<p>Other product and company names mentioned herein may be the trademarks of their respective owners.</p>
|
||||
</body>
|
||||
</html>
|
||||
189
static/html/jspaint/help/mspaint.hhc
Normal file
@@ -0,0 +1,189 @@
|
||||
<!DOCTYPE HTML PUBLIC "-//IETF//DTD HTML//EN">
|
||||
<HTML>
|
||||
<HEAD>
|
||||
<meta name="GENERATOR" content="Microsoft® HTML Help Workshop 4.00">
|
||||
<!-- Sitemap 1.0 -->
|
||||
</HEAD><BODY>
|
||||
<OBJECT type="text/site properties">
|
||||
<param name="Window Styles" value="0x800624">
|
||||
</OBJECT>
|
||||
<UL>
|
||||
<LI> <OBJECT type="text/sitemap">
|
||||
<param name="Name" value="Drawing Lines and Shapes">
|
||||
</OBJECT>
|
||||
<UL>
|
||||
<LI> <OBJECT type="text/sitemap">
|
||||
<param name="Name" value="Draw a straight line ">
|
||||
<param name="Local" value="paint_lines.html">
|
||||
</OBJECT>
|
||||
<LI> <OBJECT type="text/sitemap">
|
||||
<param name="Name" value="Draw a free-form line ">
|
||||
<param name="Local" value="paint_freeform_lines.html">
|
||||
</OBJECT>
|
||||
<LI> <OBJECT type="text/sitemap">
|
||||
<param name="Name" value="Draw a curved line">
|
||||
<param name="Local" value="paint_curves.html">
|
||||
</OBJECT>
|
||||
<LI> <OBJECT type="text/sitemap">
|
||||
<param name="Name" value="Draw an ellipse or circle ">
|
||||
<param name="Local" value="paint_ovals.html">
|
||||
</OBJECT>
|
||||
<LI> <OBJECT type="text/sitemap">
|
||||
<param name="Name" value="Draw a rectangle or square ">
|
||||
<param name="Local" value="paint_rectangles.html">
|
||||
</OBJECT>
|
||||
<LI> <OBJECT type="text/sitemap">
|
||||
<param name="Name" value="Draw a polygon ">
|
||||
<param name="Local" value="paint_polygons.html">
|
||||
</OBJECT>
|
||||
</UL>
|
||||
<LI> <OBJECT type="text/sitemap">
|
||||
<param name="Name" value="Putting Text in Pictures">
|
||||
</OBJECT>
|
||||
<UL>
|
||||
<LI> <OBJECT type="text/sitemap">
|
||||
<param name="Name" value="Type and format text ">
|
||||
<param name="Local" value="paint_text.html">
|
||||
</OBJECT>
|
||||
</UL>
|
||||
<LI> <OBJECT type="text/sitemap">
|
||||
<param name="Name" value="Working with Color">
|
||||
</OBJECT>
|
||||
<UL>
|
||||
<LI> <OBJECT type="text/sitemap">
|
||||
<param name="Name" value="Set the default foreground and background colors">
|
||||
<param name="Local" value="paint_set_default_colors.html">
|
||||
</OBJECT>
|
||||
<LI> <OBJECT type="text/sitemap">
|
||||
<param name="Name" value="Fill an area with color ">
|
||||
<param name="Local" value="paint_fill.html">
|
||||
</OBJECT>
|
||||
<LI> <OBJECT type="text/sitemap">
|
||||
<param name="Name" value="Paint with a brush ">
|
||||
<param name="Local" value="paint_brush.html">
|
||||
</OBJECT>
|
||||
<LI> <OBJECT type="text/sitemap">
|
||||
<param name="Name" value="Create an airbrush effect ">
|
||||
<param name="Local" value="paint_airbrush.html">
|
||||
</OBJECT>
|
||||
<LI> <OBJECT type="text/sitemap">
|
||||
<param name="Name" value="Create custom colors ">
|
||||
<param name="Local" value="paint_custom_colors.html">
|
||||
</OBJECT>
|
||||
<LI> <OBJECT type="text/sitemap">
|
||||
<param name="Name" value="Use black and white instead of color ">
|
||||
<param name="Local" value="paint_blackwhite.html">
|
||||
</OBJECT>
|
||||
<LI> <OBJECT type="text/sitemap">
|
||||
<param name="Name" value="Invert all the colors in a picture ">
|
||||
<param name="Local" value="paint_invert.html">
|
||||
</OBJECT>
|
||||
<LI> <OBJECT type="text/sitemap">
|
||||
<param name="Name" value="Change the color of an existing line ">
|
||||
<param name="Local" value="paint_change_color.html">
|
||||
</OBJECT>
|
||||
<LI> <OBJECT type="text/sitemap">
|
||||
<param name="Name" value="Copy color from one area or object to another ">
|
||||
<param name="Local" value="paint_not_in_color_box.html">
|
||||
</OBJECT>
|
||||
</UL>
|
||||
<LI> <OBJECT type="text/sitemap">
|
||||
<param name="Name" value="Erasing">
|
||||
</OBJECT>
|
||||
<UL>
|
||||
<LI> <OBJECT type="text/sitemap">
|
||||
<param name="Name" value="Erase a small area">
|
||||
<param name="Local" value="paint_erase_small.html">
|
||||
</OBJECT>
|
||||
<LI> <OBJECT type="text/sitemap">
|
||||
<param name="Name" value="Erase a large area">
|
||||
<param name="Local" value="paint_erase_large.html">
|
||||
</OBJECT>
|
||||
<LI> <OBJECT type="text/sitemap">
|
||||
<param name="Name" value="Erase an entire image">
|
||||
<param name="Local" value="paint_clear_image.html">
|
||||
</OBJECT>
|
||||
<LI> <OBJECT type="text/sitemap">
|
||||
<param name="Name" value="Undo changes ">
|
||||
<param name="Local" value="paint_undo.html">
|
||||
</OBJECT>
|
||||
</UL>
|
||||
<LI> <OBJECT type="text/sitemap">
|
||||
<param name="Name" value="Working with Part of the Picture">
|
||||
</OBJECT>
|
||||
<UL>
|
||||
<LI> <OBJECT type="text/sitemap">
|
||||
<param name="Name" value="Select part of a picture ">
|
||||
<param name="Local" value="paint_cutout_select.html">
|
||||
</OBJECT>
|
||||
<LI> <OBJECT type="text/sitemap">
|
||||
<param name="Name" value="Copy and paste part of a picture">
|
||||
<param name="Local" value="paint_cutout_copy_move.html">
|
||||
</OBJECT>
|
||||
<LI> <OBJECT type="text/sitemap">
|
||||
<param name="Name" value="Save part of a picture into a bitmap file ">
|
||||
<param name="Local" value="paint_cutout_save.html">
|
||||
</OBJECT>
|
||||
</UL>
|
||||
<LI> <OBJECT type="text/sitemap">
|
||||
<param name="Name" value="Changing How Your Picture Looks on the Screen">
|
||||
</OBJECT>
|
||||
<UL>
|
||||
<LI> <OBJECT type="text/sitemap">
|
||||
<param name="Name" value="Change the size of your picture">
|
||||
<param name="Local" value="paint_change_size.html">
|
||||
</OBJECT>
|
||||
<LI> <OBJECT type="text/sitemap">
|
||||
<param name="Name" value="Zoom in or out ">
|
||||
<param name="Local" value="paint_zoom.html">
|
||||
</OBJECT>
|
||||
<LI> <OBJECT type="text/sitemap">
|
||||
<param name="Name" value="Enlarge the size of the viewing area">
|
||||
<param name="Local" value="paint_enlarge_area.html">
|
||||
</OBJECT>
|
||||
<LI> <OBJECT type="text/sitemap">
|
||||
<param name="Name" value="Display gridlines ">
|
||||
<param name="Local" value="paint_grid.html">
|
||||
</OBJECT>
|
||||
<LI> <OBJECT type="text/sitemap">
|
||||
<param name="Name" value="Flip or rotate a picture ">
|
||||
<param name="Local" value="paint_flip_picture.html">
|
||||
</OBJECT>
|
||||
<LI> <OBJECT type="text/sitemap">
|
||||
<param name="Name" value="Stretch or skew an item">
|
||||
<param name="Local" value="paint_skew_picture.html">
|
||||
</OBJECT>
|
||||
</UL>
|
||||
<LI> <OBJECT type="text/sitemap">
|
||||
<param name="Name" value="Printing">
|
||||
</OBJECT>
|
||||
<UL>
|
||||
<LI> <OBJECT type="text/sitemap">
|
||||
<param name="Name" value="Print a picture ">
|
||||
<param name="Local" value="paint_print.html">
|
||||
</OBJECT>
|
||||
</UL>
|
||||
<LI> <OBJECT type="text/sitemap">
|
||||
<param name="Name" value="Using Paint with Other Programs">
|
||||
</OBJECT>
|
||||
<UL>
|
||||
<LI> <OBJECT type="text/sitemap">
|
||||
<param name="Name" value="Insert a bitmap file into the current picture">
|
||||
<param name="Local" value="paint_insert_file.html">
|
||||
</OBJECT>
|
||||
</UL>
|
||||
<LI> <OBJECT type="text/sitemap">
|
||||
<param name="Name" value="Tips and Tricks">
|
||||
</OBJECT>
|
||||
<UL>
|
||||
<LI> <OBJECT type="text/sitemap">
|
||||
<param name="Name" value="Use a picture as your desktop background ">
|
||||
<param name="Local" value="paint_wallpaper.html">
|
||||
</OBJECT>
|
||||
<LI> <OBJECT type="text/sitemap">
|
||||
<param name="Name" value="Display the tool box">
|
||||
<param name="Local" value="paint_toolbox.html">
|
||||
</OBJECT>
|
||||
</UL>
|
||||
</UL>
|
||||
</BODY></HTML>
|
||||
1553
static/html/jspaint/help/mspaint.hhk
Normal file
290
static/html/jspaint/help/nobgcolor.css
Normal file
@@ -0,0 +1,290 @@
|
||||
/* Originally: Cascading Style Sheet for IE4 build 1008+ */
|
||||
|
||||
/* for scrollbars and selection color */
|
||||
@import "../lib/os-gui/windows-98.css";
|
||||
|
||||
body {
|
||||
font-size: 70%;
|
||||
font-family: Verdana, Arial, Helvetica, "MS Sans Serif";
|
||||
}
|
||||
|
||||
html,
|
||||
body {
|
||||
margin: 0;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
#background-animation {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
#foreground-contents {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
margin-left: 35px;
|
||||
margin-top: 10px;
|
||||
overflow: auto;
|
||||
}
|
||||
#os-logo-link {
|
||||
text-decoration: none;
|
||||
}
|
||||
#os-logo-colorbar {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
right: 0;
|
||||
top: 79px;
|
||||
pointer-events: none;
|
||||
border: 0;
|
||||
height: 1px;
|
||||
/* Permalink - use to edit and share this gradient: http://colorzilla.com/gradient-editor/#ff3100+0,ff3100+14,f7df1e+14,f7df1e+29,63ce30+29,63ce30+43,009cff+43,009cff+43 */
|
||||
background: #ff3100; /* Old browsers */
|
||||
background: -moz-linear-gradient(left, #ff3100 0%, #ff3100 14%, #f7df1e 14%, #f7df1e 29%, #63ce30 29%, #63ce30 43%, #009cff 43%, #009cff 43%); /* FF3.6-15 */
|
||||
background: -webkit-linear-gradient(left, #ff3100 0%,#ff3100 14%,#f7df1e 14%,#f7df1e 29%,#63ce30 29%,#63ce30 43%,#009cff 43%,#009cff 43%); /* Chrome10-25,Safari5.1-6 */
|
||||
background: linear-gradient(to right, #ff3100 0%,#ff3100 14%,#f7df1e 14%,#f7df1e 29%,#63ce30 29%,#63ce30 43%,#009cff 43%,#009cff 43%); /* W3C, IE10+, FF16+, Chrome26+, Opera12+, Safari7+ */
|
||||
filter: progid:DXImageTransform.Microsoft.gradient( startColorstr='#ff3100', endColorstr='#009cff',GradientType=1 ); /* IE6-9 */
|
||||
}
|
||||
|
||||
a:link {
|
||||
color: #0000CC;
|
||||
}
|
||||
|
||||
a:active {
|
||||
color: #996699;
|
||||
}
|
||||
|
||||
a:visited {
|
||||
color: #996699;
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: 145%;
|
||||
margin-bottom: .5em;
|
||||
}
|
||||
|
||||
h2 {
|
||||
font-size: 125%;
|
||||
margin-top: 1.5em;
|
||||
margin-bottom: .5em;
|
||||
}
|
||||
|
||||
h3 {
|
||||
font-size: 110%;
|
||||
margin-top: 1.2em;
|
||||
margin-bottom: .5em;
|
||||
}
|
||||
|
||||
h4 {
|
||||
font-size: 105%;
|
||||
}
|
||||
|
||||
h5 {
|
||||
font-size: 100%;
|
||||
}
|
||||
|
||||
p.proclabel {
|
||||
font-weight: bold;
|
||||
font-size: 100%;
|
||||
}
|
||||
|
||||
p {
|
||||
margin-top: .6em;
|
||||
margin-bottom: .6em;
|
||||
}
|
||||
|
||||
li p {
|
||||
margin-top: 0;
|
||||
margin-bottom: .6em;
|
||||
}
|
||||
|
||||
ol {
|
||||
margin-top: .5em;
|
||||
margin-bottom: 0em;
|
||||
margin-left: 4em;
|
||||
}
|
||||
|
||||
ul {
|
||||
margin-top: .6em;
|
||||
margin-bottom: 0em;
|
||||
}
|
||||
|
||||
ol ul {
|
||||
list-style: disc;
|
||||
margin-top: 2em;
|
||||
}
|
||||
|
||||
li {
|
||||
padding-bottom: .7em;
|
||||
margin-left: -2em;
|
||||
}
|
||||
|
||||
dl ul {
|
||||
/* list item inside a def/term */
|
||||
margin-top: 2em;
|
||||
margin-bottom: 0em;
|
||||
}
|
||||
|
||||
dl {
|
||||
margin-top: -1em;
|
||||
}
|
||||
|
||||
ol dl {
|
||||
/* term/def list inside a numbered list */
|
||||
margin-top: -1.5em;
|
||||
margin-left: 0em;
|
||||
}
|
||||
|
||||
ol dl dl {
|
||||
/* term/def list inside a term/def list */
|
||||
margin-top: 0em;
|
||||
margin-left: .2em;
|
||||
}
|
||||
|
||||
dd {
|
||||
margin-bottom: 0em;
|
||||
/* not currently working */
|
||||
margin-left: 1.5em;
|
||||
}
|
||||
|
||||
dt {
|
||||
padding-top: 2em;
|
||||
font-weight: bold;
|
||||
margin-left: 1.5em;
|
||||
}
|
||||
|
||||
pre {
|
||||
margin-top: 0em;
|
||||
margin-bottom: 1.5em;
|
||||
font-family: Courier;
|
||||
font-size: 125%;
|
||||
}
|
||||
|
||||
table {
|
||||
font-size: 100%;
|
||||
margin-top: 1em;
|
||||
margin-bottom: 1em;
|
||||
}
|
||||
|
||||
th.center {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
th {
|
||||
text-align: left;
|
||||
background: #dddddd;
|
||||
margin: 3pt;
|
||||
vertical-align: bottom;
|
||||
}
|
||||
|
||||
tr {
|
||||
vertical-align: top;
|
||||
}
|
||||
|
||||
td {
|
||||
margin: 3pt;
|
||||
vertical-align: top;
|
||||
}
|
||||
/* MISC. TAGS */
|
||||
|
||||
hr.sms {
|
||||
/* SMS specific rule used under procedure title */
|
||||
color: red;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
hr.iis {
|
||||
color: red;
|
||||
}
|
||||
/*IIS specific - preceding copyright*/
|
||||
/* IE 4.0 TAGS */
|
||||
/*
|
||||
a:visited {
|
||||
color: #0000FF;
|
||||
}
|
||||
*/
|
||||
|
||||
p.dis {
|
||||
font-size: 6pt;
|
||||
}
|
||||
|
||||
ul.onestep {
|
||||
list-style: square;
|
||||
}
|
||||
|
||||
h5 {
|
||||
}
|
||||
|
||||
h5.active {
|
||||
background: #000000;
|
||||
color: #FFCC99;
|
||||
}
|
||||
/* -- subheading -- */
|
||||
|
||||
h5.subh {
|
||||
color: #660000;
|
||||
margin-bottom: -1em;
|
||||
margin-top: 1.5em;
|
||||
}
|
||||
/* -- procedure heading -- */
|
||||
|
||||
h5.proch {
|
||||
margin-bottom: 4pt;
|
||||
color: #003399;
|
||||
}
|
||||
|
||||
/* -- topic heading -- */
|
||||
h5.topich {
|
||||
color: #FF0033;
|
||||
margin-bottom: -1em;
|
||||
}
|
||||
|
||||
/* -- note 'n' tip heading -- */
|
||||
h5.note {
|
||||
margin-top: 2em;
|
||||
margin-bottom: -1em;
|
||||
color: #99CC99;
|
||||
}
|
||||
|
||||
/* -- related topics heading -- */
|
||||
h5.relh {
|
||||
margin-top: 2.25em;
|
||||
margin-bottom: -1em;
|
||||
color: #9933CC;
|
||||
}
|
||||
|
||||
/* -------------- miscellany 'n' stuff -------------- */
|
||||
/* intended to applied with <div></div> tags to groups of links */
|
||||
|
||||
.dectree {
|
||||
margin-left: 1.33em;
|
||||
margin-top: 1.5em;
|
||||
}
|
||||
|
||||
div.dectree p {
|
||||
margin-top: 1em;
|
||||
color: orange;
|
||||
}
|
||||
|
||||
div.dectree p:first-letter {
|
||||
font-size: 3em;
|
||||
color: orange;
|
||||
}
|
||||
|
||||
a:link .dectree p {
|
||||
color: yellow;
|
||||
}
|
||||
|
||||
.reltopics {
|
||||
line-height: .5em;
|
||||
margin-top: 2em;
|
||||
}
|
||||
|
||||
div.reltopics p {
|
||||
margin-top: 1em;
|
||||
}
|
||||
BIN
static/html/jspaint/help/onestep.gif
Normal file
|
After Width: | Height: | Size: 815 B |
36
static/html/jspaint/help/online_support.htm
Normal file
@@ -0,0 +1,36 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>Technical Support Online</title>
|
||||
<link href="nobgcolor.css" rel="stylesheet" type="text/css">
|
||||
</head>
|
||||
<body style="margin-left: 35px; margin-top: 80px;">
|
||||
<div id="background-animation"></div>
|
||||
<hr id="os-logo-colorbar">
|
||||
<div id="foreground-contents">
|
||||
<a href="https://98.js.org/" target="_blank" id="os-logo-link">
|
||||
<img src="../images/98.js.org.svg" alt="98.js.org" id="os-logo" height="100">
|
||||
</a>
|
||||
<br>
|
||||
<p><a id="windows_update" name="windows_update"></a><b><i>Additional help via the Internet</i></b></p>
|
||||
If you have a technical question on a 98.js.org product and can't find your answer in the product Help file or manual, take advantage of one of these online resources:
|
||||
<p></p>
|
||||
<ul>
|
||||
<li>
|
||||
Your primary source for support is the computer manufacturer who provided your software.
|
||||
Your computer manufacturer may provide a web site to help you find answers to technical questions.
|
||||
Check the documentation that came with your computer to determine the availability of an online support site.
|
||||
</li>
|
||||
<li>
|
||||
For the latest technical information on 98.js.org products, you can also find answers at GitHub.
|
||||
From README articles in the repositories, to Troubleshooting <small>that there aren't any</small> Wizards,
|
||||
the repositories on GitHub have the specific resources most likely to help you find the answer to your question.
|
||||
To begin your search, go to <a href="https://github.com/1j01/98" target="_blank">Support Online</a>.
|
||||
</li>
|
||||
</ul>
|
||||
<p></p>
|
||||
</div>
|
||||
<script src="vaporwave.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
BIN
static/html/jspaint/help/p_airb.gif
Normal file
|
After Width: | Height: | Size: 867 B |
BIN
static/html/jspaint/help/p_blank.png
Normal file
|
After Width: | Height: | Size: 126 B |
BIN
static/html/jspaint/help/p_brush.gif
Normal file
|
After Width: | Height: | Size: 852 B |
BIN
static/html/jspaint/help/p_color.png
Normal file
|
After Width: | Height: | Size: 120 B |
BIN
static/html/jspaint/help/p_curve.gif
Normal file
|
After Width: | Height: | Size: 835 B |
BIN
static/html/jspaint/help/p_cut.png
Normal file
|
After Width: | Height: | Size: 158 B |
BIN
static/html/jspaint/help/p_database.png
Normal file
|
After Width: | Height: | Size: 141 B |
BIN
static/html/jspaint/help/p_delete.png
Normal file
|
After Width: | Height: | Size: 160 B |
BIN
static/html/jspaint/help/p_erase.gif
Normal file
|
After Width: | Height: | Size: 857 B |
BIN
static/html/jspaint/help/p_eye.gif
Normal file
|
After Width: | Height: | Size: 853 B |
BIN
static/html/jspaint/help/p_fliph.png
Normal file
|
After Width: | Height: | Size: 163 B |
BIN
static/html/jspaint/help/p_flipv.png
Normal file
|
After Width: | Height: | Size: 155 B |
BIN
static/html/jspaint/help/p_free.gif
Normal file
|
After Width: | Height: | Size: 846 B |
BIN
static/html/jspaint/help/p_invert.png
Normal file
|
After Width: | Height: | Size: 139 B |
BIN
static/html/jspaint/help/p_line.gif
Normal file
|
After Width: | Height: | Size: 833 B |
BIN
static/html/jspaint/help/p_make_opaque.png
Normal file
|
After Width: | Height: | Size: 182 B |
BIN
static/html/jspaint/help/p_monochrome.png
Normal file
|
After Width: | Height: | Size: 100 B |
BIN
static/html/jspaint/help/p_opaq.gif
Normal file
|
After Width: | Height: | Size: 989 B |
BIN
static/html/jspaint/help/p_open.png
Normal file
|
After Width: | Height: | Size: 164 B |
BIN
static/html/jspaint/help/p_oval.gif
Normal file
|
After Width: | Height: | Size: 843 B |
BIN
static/html/jspaint/help/p_paint.gif
Normal file
|
After Width: | Height: | Size: 865 B |
BIN
static/html/jspaint/help/p_paste.png
Normal file
|
After Width: | Height: | Size: 198 B |
BIN
static/html/jspaint/help/p_pencil.gif
Normal file
|
After Width: | Height: | Size: 848 B |
BIN
static/html/jspaint/help/p_poly.gif
Normal file
|
After Width: | Height: | Size: 840 B |
BIN
static/html/jspaint/help/p_rect.gif
Normal file
|
After Width: | Height: | Size: 840 B |
BIN
static/html/jspaint/help/p_rotate_ccw.png
Normal file
|
After Width: | Height: | Size: 162 B |
BIN
static/html/jspaint/help/p_rotate_ccw_thin.png
Normal file
|
After Width: | Height: | Size: 151 B |
BIN
static/html/jspaint/help/p_rotate_cw.png
Normal file
|
After Width: | Height: | Size: 153 B |
BIN
static/html/jspaint/help/p_rotate_cw_thin.png
Normal file
|
After Width: | Height: | Size: 148 B |
BIN
static/html/jspaint/help/p_rrect.gif
Normal file
|
After Width: | Height: | Size: 848 B |
BIN
static/html/jspaint/help/p_save.png
Normal file
|
After Width: | Height: | Size: 156 B |
BIN
static/html/jspaint/help/p_sel.gif
Normal file
|
After Width: | Height: | Size: 837 B |
BIN
static/html/jspaint/help/p_skew_h.png
Normal file
|
After Width: | Height: | Size: 175 B |
BIN
static/html/jspaint/help/p_skew_v.png
Normal file
|
After Width: | Height: | Size: 187 B |
BIN
static/html/jspaint/help/p_stretch_both.png
Normal file
|
After Width: | Height: | Size: 170 B |
BIN
static/html/jspaint/help/p_stretch_h.png
Normal file
|
After Width: | Height: | Size: 156 B |
BIN
static/html/jspaint/help/p_stretch_v.png
Normal file
|
After Width: | Height: | Size: 162 B |
BIN
static/html/jspaint/help/p_trans.gif
Normal file
|
After Width: | Height: | Size: 978 B |