1j01 / jspaint

🎨 Classic MS Paint, REVIVED + ✨Extras
https://jspaint.app/about
MIT License
7.17k stars 562 forks source link

[Feature] Layers #251

Open lafoxxx92 opened 2 years ago

lafoxxx92 commented 2 years ago

Right now it's capped at 1 (like the original). Photoshop goes up to 100, and even that is sometimes too small. More layers would make this product superior.

123ldk commented 4 months ago

For exporting, since there are concerns on file format compatibility, one could have an option to export the combined image + the layers of said images. Ie:

image_combined.png image_layer1.png image_layer2.png

That way, if one wants to put the layers into a different program, it's just a matter of ordering the images properly. A bit unsophisticated, but I don't think it really needs anything beyond this to be useful. (Maybe putting the layers into a folder for convenience)

1j01 commented 4 months ago

@123ldk To download multiple files, an archive format is needed, and ZIP is standard. Indeed, a ZIP with PNG files is a good idea and is the basis of the OpenRaster specification which is a ZIP with PNG files and an XML file for metadata.

I'd recommend Photopea to anyone who wants layers for now; it supports PSD and SVG, along with many sophisticated features.

For layer support in JS Paint, my main concern is undo history. It's a similar story for animations (GIF, APNG), multi-size icons (ICO, ICNS), layered images (ORA, PSD), and multi-page documents (PDF, DjVu), as well as comic strips (if one prefers a panel-by-panel workflow for creating comics). We need:

The good news is that if we can do one of these features, we can do all of them pretty easily, which would be amazing. The bad news is that it affects a large portion of the code, as all the tools and image operations integrate with undo history and the canvas. (There's 147 mentions of main_canvas in the codebase, for instance, and 73 for main_ctx.)

I'd also like to improve the undo/redo system for multiplayer support and for efficiency, with region updates instead of whole canvas snapshots, which might be good to implement at the same time, although it's somewhat orthogonal. Either feature would likely make the other feature easier, as either would require moving from a whole state snapshot to a targeted update.

Currently an undo state ("history node") looks like:

{
    parent = null, // the state before this state (its basis), or null if this is the first state
    futures = [], // the states branching off from this state (its children)
    timestamp = Date.now(), // when this state was created
    soft = false, // indicates that undo should skip this state; it can still be accessed with the History window
    image_data = null, // the image data for the canvas (TODO: region updates)
    selection_image_data = null, // the image data for the selection, if any
    selection_x, // the x position of the selection, if any
    selection_y, // the y position of the selection, if any
    textbox_text, // the text in the textbox, if any
    textbox_x, // the x position of the textbox, if any
    textbox_y, // the y position of the textbox, if any
    textbox_width, // the width of the textbox, if any
    textbox_height, // the height of the textbox, if any
    text_tool_font = null, // the font of the Text tool (important to restore a textbox-containing state, but persists without a textbox)
    tool_transparent_mode, // whether transparent mode is on for Select/Free-Form Select/Text tools; otherwise box is opaque
    foreground_color, // selected foreground color (left click)
    background_color, // selected background color (right click)
    ternary_color, // selected ternary color (ctrl+click)
    name, // the name of the operation, for the history window, e.g. localize("Resize Canvas")
    icon = null, // an Image representation of the operation type shown in the history window, e.g. get_help_folder_icon("p_blank.png")
}

and history nodes are only created via the undoable function, except for the root node (when creating or opening a document), although history nodes can also be modified with make_or_update_undoable, used for smearing selections.

Designing the app to treat sub-images as textures would allow even more possibilities, like painting onto 3D models, as well as symmetry, tessellation, and even an infinite canvas, as each of those features could be implemented as painting onto different sorts of geometry. For instance an infinite canvas could generate a field of squares/triangles with unique texture IDs/UV coordinates, and tessellations and symmetries would use some repeated UV coordinates such that the brush affects multiple areas on the screen via a single area in the texture.

(Some inspiration: this infinite zoom which generates 2D geometry for THREE.js to render)

I've outlined some of this in the Extended editing section of the todo.

My next focus is to fix my automated tests, and improve the maintainability of the codebase with things like autoformatting and typechecking, maybe add some new tests. Then I can feel better about changing core aspects of the app.