diff --git a/changelog.md b/changelog.md index 83950d1f..87341956 100644 --- a/changelog.md +++ b/changelog.md @@ -1,5 +1,76 @@ # Changelog +## **0.5.0** - 2024-06-12 + +### Added: +* Added tools + * Shape tool (hotkey *R*) + * Script brush (hotkey *Q*) +* Polygon Select now has a "close polygon" shortcut (*Shift + Left Click*) +* Images can now be pasted from the system clipboard +* Added themes: + * Asylum + * Ramallah +* Added "snap to target pixel" behaviour (*Shift + Enter*) +* Stretch operation can now be snapped to the pixel grid by holding *Shift* +* Added relative frame durations so some frames can be displayed for more or less time than others +* Added history dialog (*Shift + Y*) where users can browse all cached project states and revert to any of them +* Added playback controls to preview dialogs +* Added keyboard controls to the preview window's playback controls + +### Changed: +* Optimized selection overlays +* Optimized selection logic +* Optimized search algorithm (Wand and Fill tools) +* Now discards intermediate (granular) project states after five new checkpoint project states +* Updated themes: + * Zo + * Neon + * Bunkering +* Move selection and pick up selection action previews are rendered in a different style +* Changed frame limit from 100 to 300 and layer limit from 100 to 50 +* Increased maximum canvas bounds to 1920 x 1080 pixels +* Improved selection transform node manipulation + +### Fixed: +* Bug: PIXEL_GRID_ON_BY_DEFAULT setting flag is not persistent + +### API Changes: +* Added: + * `project` frame duration functions: + * ```js + P.get_frame_duration(int i) -> float + ``` + * ```js + P.get_frame_durations() -> float[] + ``` + * ```js + P.set_frame_duration(int i, float frame_duration); + ``` + +* Changed: + * Separated selection from scope in color actions + * Modified `scope` enumeration: + ```js + 0: PROJECT + 1: LAYER + 2: FRAME + 3: LAYER_FRAME + ``` + * Changed `project` color action function signatures: + * ```js + P.palettize(palette pal, int scope, bool include_disabled, bool ignore_selection); + ``` + * ```js + P.extract_to_pal(palette pal, int scope, bool include_disabled, bool ignore_selection); + ``` + * ```js + P.hsv_shift(int scope, bool include_disabled, bool ignore_selection, int h_shift, N s_shift, N v_shift); + ``` + * ```js + P.color_script(int scope, bool include_disabled, bool ignore_selection, string script_path); + ``` + ## **0.4.2** - 2024-05-26 ### Added: @@ -46,11 +117,11 @@ * Added HSV level shifting * Added "Flatten project" layer action * Added themes - * Added "Zo" theme inspired by Haiti + * Added "Zo" theme inspired by Haitian culture and African diasporic religions * Added "Neon" theme - * Added "Bunkering" theme inspired by the environmental degradation the Niger Delta + * Added "Bunkering" theme inspired by the environmental degradation of the Niger Delta * Added transparency checkerboard to the color slider core -* Dialog menus can now be closed by pressing {Enter} if the precondition is passing +* Dialog menus can now be closed by pressing *Enter* if the precondition is passing * Added settings for windowed program size * Added flag to include or exclude disabled layers in color actions diff --git a/res/blurbs/__changelog.txt b/res/blurbs/__changelog.txt index 318c86ed..38859301 100644 --- a/res/blurbs/__changelog.txt +++ b/res/blurbs/__changelog.txt @@ -1,3 +1,61 @@ +{0.5.0} - The Performance Update - 2024-06-12 + +Added: +> Added tools + > Shape tool (hotkey {R}) + > Script brush (hotkey {Q}) +> Polygon Select now has a "close polygon" shortcut ({Shift + Left Click}) +> Images can now be pasted from the system clipboard +> Added themes: + > Asylum + > Ramallah +> Added "snap to target pixel" behaviour ({Shift + Enter}) +> Stretch operation can now be snapped to the pixel grid by holding {Shift} +> Added relative frame durations so some frames can be displayed for more or less time than others +> Added history dialog ({Shift + Y}) where users can browse all cached project states and revert + to any of them +> Added playback controls to preview dialogs +> Added keyboard controls to the preview window's playback controls + +Changed: +> Optimized selection overlays +> Optimized selection logic +> Optimized search algorithm (Wand and Fill tools) +> Now discards intermediate (granular) project states after five new checkpoint project states +> Updated themes: + > Zo + > Neon + > Bunkering +> Move selection and pick up selection action previews are rendered in a different style +> Changed frame limit from 100 to 300 and layer limit from 100 to 50 +> Increased maximum canvas bounds to 1920 x 1080 pixels +> Improved selection transform node manipulation + +Fixed: +> Bug: PIXEL_GRID_ON_BY_DEFAULT setting flag is not persistent + +API Changes: + Added: + > {project} frame duration functions: + > P.{get_frame_duration}(int i) -> float + > P.{get_frame_durations}() -> float[] + > P.{set_frame_duration}(int i, float frame_duration); + + Changed: + > Separated selection from scope in color actions + > Modified {scope} enumeration: + {0: PROJECT} + {1: LAYER} + {2: FRAME} + {3: LAYER_FRAME} + + > Changed {project} color action function signatures: + > P.{palettize}(palette pal, int scope, bool include_disabled, bool ignore_selection); + > P.{extract_to_pal}(palette pal, int scope, bool include_disabled, bool ignore_selection); + > P.{hsv_shift}(int scope, bool include_disabled, bool ignore_selection, + int h_shift, N s_shift, N v_shift); + > P.{color_script}(int scope, bool include_disabled, bool ignore_selection, string script_path); + {0.4.2} - 2024-05-26 Added: @@ -41,9 +99,9 @@ Added: > Added HSV level shifting > Added "Flatten project" layer action > Added themes - > Added "Zo" theme inspired by Haiti + > Added "Zo" theme inspired by Haitian culture and African diasporic religions > Added "Neon" theme - > Added "Bunkering" theme inspired by the environmental degradation the Niger Delta + > Added "Bunkering" theme inspired by the environmental degradation of the Niger Delta > Added transparency checkerboard to the color slider core > Dialog menus can now be closed by pressing {Enter} if the precondition is passing > Added settings for windowed program size diff --git a/res/blurbs/__general.txt b/res/blurbs/__general.txt index bd863e67..672d6d85 100644 --- a/res/blurbs/__general.txt +++ b/res/blurbs/__general.txt @@ -1,6 +1,7 @@ Window / layout: Toggle fullscreen / windowed mode: {Escape} Recenter canvas in workspace: {Enter} +Center the canvas around the target pixel: {Shift + Enter} Toggle show all UI / minimal UI mode: {Ctrl + Shift + A} Set checkerboard and pixel grid cell dimensions to project/selection bounds: {Ctrl + B} diff --git a/res/blurbs/__roadmap.txt b/res/blurbs/__roadmap.txt index b6498e22..eb7db26a 100644 --- a/res/blurbs/__roadmap.txt +++ b/res/blurbs/__roadmap.txt @@ -1,17 +1,7 @@ This is a rough outline of planned features and when to expect them. Everything you see here is subject to change. -{0.5.0} - The Performance Update - Late May 2024 - -Add: -> Paste images from the system clipboard - -Change: -> Implement massive performance optimizations to make the program faster and less demanding - on the machine -> Overhaul the move selection and pick up selection tools - -Fix: -> Memory issues and related crashes - -{1.0.0} - Official Release - Early June 2024 +{Q3 2024} +> Timeline: frames and layers combined in a single UI panel +> Lossless video export +> Presets for common scriptable behaviours diff --git a/res/blurbs/frame_properties.txt b/res/blurbs/frame_properties.txt new file mode 100644 index 00000000..95f235f4 --- /dev/null +++ b/res/blurbs/frame_properties.txt @@ -0,0 +1,3 @@ +Shortcut: {Shift + F} + +Displays a dialog where the frame's properties can be modified diff --git a/res/blurbs/history.txt b/res/blurbs/history.txt new file mode 100644 index 00000000..8b47f730 --- /dev/null +++ b/res/blurbs/history.txt @@ -0,0 +1,3 @@ +Shortcut: {Shift + Y} + +Opens a dialog that lists all the retained major project states, any of which can be reverted to. diff --git a/res/blurbs/layer_settings.txt b/res/blurbs/layer_settings.txt index a6f7bbfd..352bfa54 100644 --- a/res/blurbs/layer_settings.txt +++ b/res/blurbs/layer_settings.txt @@ -1,5 +1,5 @@ Shortcut: {Shift + L} -Opens all the layer's settings and properties in one place +Displays a dialog where the layer's settings and properties can be modified The layer's opacity can be adjusted from here. diff --git a/res/blurbs/move_selection.txt b/res/blurbs/move_selection.txt index e1fec6d8..8aa239f2 100644 --- a/res/blurbs/move_selection.txt +++ b/res/blurbs/move_selection.txt @@ -8,6 +8,7 @@ Move: {Click & Drag} or {Arrow Keys} * Only works with {Click & Drag}, not with {Arrow Keys} Stretch: {Click & Drag} on one of the transform nodes +If the pixel grid is on, hold {Shift} to snap the stretch operation to the pixel grid. Rotate: {Click & Drag} around one of the transform nodes Modify the rotation operation to snap to the nearest 45-degree angle by holding {Shift}. diff --git a/res/blurbs/pick_up_selection.txt b/res/blurbs/pick_up_selection.txt index ef290fda..36e2dbf7 100644 --- a/res/blurbs/pick_up_selection.txt +++ b/res/blurbs/pick_up_selection.txt @@ -8,6 +8,7 @@ Move: {Click & Drag} or {Arrow Keys} * Only works with {Click & Drag}, not with {Arrow Keys} Stretch: {Click & Drag} on one of the transform nodes +If the pixel grid is on, hold {Shift} to snap the stretch operation to the pixel grid. Rotate: {Click & Drag} around one of the transform nodes Modify the rotation operation to snap to the nearest 45-degree angle by holding {Shift}. diff --git a/res/blurbs/pixel_grid_on.txt b/res/blurbs/pixel_grid_on.txt index 43859762..94239d48 100644 --- a/res/blurbs/pixel_grid_on.txt +++ b/res/blurbs/pixel_grid_on.txt @@ -1,7 +1 @@ Toggle pixel grid on/off: {Ctrl + G} - -The pixel grid can only be displayed at a minimum zoom level of {400%}. The amount of pixels between -each grid along the X and Y axes can be set in the program settings. - -Due to performance considerations, the pixel grid can currently only be turned on for projects with -a maximum canvas size of {128x128 pixels}. diff --git a/res/blurbs/polygon_select.txt b/res/blurbs/polygon_select.txt index e139fd63..08041528 100644 --- a/res/blurbs/polygon_select.txt +++ b/res/blurbs/polygon_select.txt @@ -5,6 +5,7 @@ the bounds of the selection. To close the polygon, place a vertex down on the sa the shape began. Place vertex: {Left Click} +Close polygon: {Shift + Left Click} Remove previous vertex: {Right Click} Reset: {Middle Click} diff --git a/res/blurbs/script_brush.txt b/res/blurbs/script_brush.txt new file mode 100644 index 00000000..b1fb53d9 --- /dev/null +++ b/res/blurbs/script_brush.txt @@ -0,0 +1,12 @@ +Shortcut: {Q} + +The script brush paints applies a color script transformation to the pixel that it paints over. + +Use existing color as input: {Click & Drag} + +Use primary color as input: {Shift + Left Click + Drag} +Use secondary color as input: {Shift + Right Click + Drag} + +Hold {Ctrl} to ignore transparent pixels of the layer-frame. + +Increment/decrement brush width: {Arrow Keys} or {Shift + Scroll Wheel} diff --git a/res/blurbs/shade_brush.txt b/res/blurbs/shade_brush.txt index 2d5eb1a1..d028b034 100644 --- a/res/blurbs/shade_brush.txt +++ b/res/blurbs/shade_brush.txt @@ -1,6 +1,6 @@ Shortcut: {D} -Works like the regular {brush} tool, but shifts the color of a pixel that is {drawn over} to one of +Works like the regular {Brush} tool, but shifts the color of a pixel that is {painted over} to one of the colors next to it in the current palette {if the pixel's initial color is in the palette}. Replace with left-adjacent color in palette: {Left Click & Drag} diff --git a/res/blurbs/shape_tool.txt b/res/blurbs/shape_tool.txt new file mode 100644 index 00000000..a07588ef --- /dev/null +++ b/res/blurbs/shape_tool.txt @@ -0,0 +1,10 @@ +Shortcut: {R} + +The shape tool plots a shape of a determined pixel width. + +Primary color: {Left Click & Drag} +Secondary color: {Right Click & Drag} + +Snap a rectangle to a square or an ellipse to a circle by holding {Shift}. + +Increment/decrement brush width: {Arrow Keys} or {Shift + Scroll Wheel} diff --git a/res/blurbs/text_tool.txt b/res/blurbs/text_tool.txt index 965f36f6..45bbf509 100644 --- a/res/blurbs/text_tool.txt +++ b/res/blurbs/text_tool.txt @@ -10,7 +10,7 @@ Start typing: {Click} Increment/decrement font scale factor: {Down/Up Arrow Keys} or {Shift + Scroll Wheel} Navigate to previous/next font: {Left/Right Arrow Keys} Toggle text alignment: {Ctrl + K} -Upload new font: {Shift + F} +Upload new font: {Shift + T} (While typing) Modify text: < type as normal > diff --git a/res/cursors/move_selection_diag_bl.png b/res/cursors/move_selection_diag_bl.png index da617bdc..2b3e3376 100644 Binary files a/res/cursors/move_selection_diag_bl.png and b/res/cursors/move_selection_diag_bl.png differ diff --git a/res/cursors/move_selection_diag_tl.png b/res/cursors/move_selection_diag_tl.png index 3e262e04..01629cad 100644 Binary files a/res/cursors/move_selection_diag_tl.png and b/res/cursors/move_selection_diag_tl.png differ diff --git a/res/cursors/no_script.png b/res/cursors/no_script.png new file mode 100644 index 00000000..3514dbb5 Binary files /dev/null and b/res/cursors/no_script.png differ diff --git a/res/cursors/pick_up_selection_diag_bl.png b/res/cursors/pick_up_selection_diag_bl.png index 2ea40bbf..9c65e84d 100644 Binary files a/res/cursors/pick_up_selection_diag_bl.png and b/res/cursors/pick_up_selection_diag_bl.png differ diff --git a/res/cursors/pick_up_selection_diag_tl.png b/res/cursors/pick_up_selection_diag_tl.png index 181359bf..37b558f4 100644 Binary files a/res/cursors/pick_up_selection_diag_tl.png and b/res/cursors/pick_up_selection_diag_tl.png differ diff --git a/res/icons/frame_properties.png b/res/icons/frame_properties.png new file mode 100644 index 00000000..215aca22 Binary files /dev/null and b/res/icons/frame_properties.png differ diff --git a/res/icons/history.png b/res/icons/history.png new file mode 100644 index 00000000..e19197da Binary files /dev/null and b/res/icons/history.png differ diff --git a/res/icons/script_brush.png b/res/icons/script_brush.png new file mode 100644 index 00000000..2060ae1e Binary files /dev/null and b/res/icons/script_brush.png differ diff --git a/res/icons/shape_tool.png b/res/icons/shape_tool.png new file mode 100644 index 00000000..2976b5f4 Binary files /dev/null and b/res/icons/shape_tool.png differ diff --git a/res/program b/res/program index f804f876..4ecf1198 100644 --- a/res/program +++ b/res/program @@ -1,5 +1,5 @@ name:{Stipple Effect} -version:{0.4.2} +version:{0.5.0} devbuild:{false} -native_standard:{1.1} +native_standard:{1.2} palette_standard:{1.0} diff --git a/res/themes/asylum/splash_0.png b/res/themes/asylum/splash_0.png new file mode 100644 index 00000000..2c611d65 Binary files /dev/null and b/res/themes/asylum/splash_0.png differ diff --git a/res/themes/asylum/splash_1.png b/res/themes/asylum/splash_1.png new file mode 100644 index 00000000..528aaa56 Binary files /dev/null and b/res/themes/asylum/splash_1.png differ diff --git a/res/themes/asylum/splash_10.png b/res/themes/asylum/splash_10.png new file mode 100644 index 00000000..e246efdf Binary files /dev/null and b/res/themes/asylum/splash_10.png differ diff --git a/res/themes/asylum/splash_11.png b/res/themes/asylum/splash_11.png new file mode 100644 index 00000000..cd8a0225 Binary files /dev/null and b/res/themes/asylum/splash_11.png differ diff --git a/res/themes/asylum/splash_12.png b/res/themes/asylum/splash_12.png new file mode 100644 index 00000000..5493b667 Binary files /dev/null and b/res/themes/asylum/splash_12.png differ diff --git a/res/themes/asylum/splash_13.png b/res/themes/asylum/splash_13.png new file mode 100644 index 00000000..a5f85356 Binary files /dev/null and b/res/themes/asylum/splash_13.png differ diff --git a/res/themes/asylum/splash_14.png b/res/themes/asylum/splash_14.png new file mode 100644 index 00000000..e600d0b1 Binary files /dev/null and b/res/themes/asylum/splash_14.png differ diff --git a/res/themes/asylum/splash_15.png b/res/themes/asylum/splash_15.png new file mode 100644 index 00000000..e600d0b1 Binary files /dev/null and b/res/themes/asylum/splash_15.png differ diff --git a/res/themes/asylum/splash_16.png b/res/themes/asylum/splash_16.png new file mode 100644 index 00000000..e600d0b1 Binary files /dev/null and b/res/themes/asylum/splash_16.png differ diff --git a/res/themes/asylum/splash_17.png b/res/themes/asylum/splash_17.png new file mode 100644 index 00000000..e600d0b1 Binary files /dev/null and b/res/themes/asylum/splash_17.png differ diff --git a/res/themes/asylum/splash_18.png b/res/themes/asylum/splash_18.png new file mode 100644 index 00000000..e600d0b1 Binary files /dev/null and b/res/themes/asylum/splash_18.png differ diff --git a/res/themes/asylum/splash_19.png b/res/themes/asylum/splash_19.png new file mode 100644 index 00000000..e600d0b1 Binary files /dev/null and b/res/themes/asylum/splash_19.png differ diff --git a/res/themes/asylum/splash_2.png b/res/themes/asylum/splash_2.png new file mode 100644 index 00000000..0c67d1ac Binary files /dev/null and b/res/themes/asylum/splash_2.png differ diff --git a/res/themes/asylum/splash_3.png b/res/themes/asylum/splash_3.png new file mode 100644 index 00000000..4e52dd0c Binary files /dev/null and b/res/themes/asylum/splash_3.png differ diff --git a/res/themes/asylum/splash_4.png b/res/themes/asylum/splash_4.png new file mode 100644 index 00000000..a95ee872 Binary files /dev/null and b/res/themes/asylum/splash_4.png differ diff --git a/res/themes/asylum/splash_5.png b/res/themes/asylum/splash_5.png new file mode 100644 index 00000000..d571266a Binary files /dev/null and b/res/themes/asylum/splash_5.png differ diff --git a/res/themes/asylum/splash_6.png b/res/themes/asylum/splash_6.png new file mode 100644 index 00000000..ee3217b8 Binary files /dev/null and b/res/themes/asylum/splash_6.png differ diff --git a/res/themes/asylum/splash_7.png b/res/themes/asylum/splash_7.png new file mode 100644 index 00000000..5e8f4017 Binary files /dev/null and b/res/themes/asylum/splash_7.png differ diff --git a/res/themes/asylum/splash_8.png b/res/themes/asylum/splash_8.png new file mode 100644 index 00000000..5e8f4017 Binary files /dev/null and b/res/themes/asylum/splash_8.png differ diff --git a/res/themes/asylum/splash_9.png b/res/themes/asylum/splash_9.png new file mode 100644 index 00000000..d8876056 Binary files /dev/null and b/res/themes/asylum/splash_9.png differ diff --git a/res/themes/bunkering/splash_0.png b/res/themes/bunkering/splash_0.png new file mode 100644 index 00000000..d11ec6cb Binary files /dev/null and b/res/themes/bunkering/splash_0.png differ diff --git a/res/themes/bunkering/splash_1.png b/res/themes/bunkering/splash_1.png new file mode 100644 index 00000000..28ba1dbe Binary files /dev/null and b/res/themes/bunkering/splash_1.png differ diff --git a/res/themes/bunkering/splash_2.png b/res/themes/bunkering/splash_2.png new file mode 100644 index 00000000..6d095b2b Binary files /dev/null and b/res/themes/bunkering/splash_2.png differ diff --git a/res/themes/bunkering/splash_3.png b/res/themes/bunkering/splash_3.png new file mode 100644 index 00000000..db030bb2 Binary files /dev/null and b/res/themes/bunkering/splash_3.png differ diff --git a/res/themes/bunkering/splash_4.png b/res/themes/bunkering/splash_4.png new file mode 100644 index 00000000..51a81779 Binary files /dev/null and b/res/themes/bunkering/splash_4.png differ diff --git a/res/themes/bunkering/splash_5.png b/res/themes/bunkering/splash_5.png new file mode 100644 index 00000000..1cc78813 Binary files /dev/null and b/res/themes/bunkering/splash_5.png differ diff --git a/res/themes/bunkering/splash_6.png b/res/themes/bunkering/splash_6.png new file mode 100644 index 00000000..e08cca59 Binary files /dev/null and b/res/themes/bunkering/splash_6.png differ diff --git a/res/themes/bunkering/splash_7.png b/res/themes/bunkering/splash_7.png new file mode 100644 index 00000000..79f5310c Binary files /dev/null and b/res/themes/bunkering/splash_7.png differ diff --git a/res/themes/bunkering/splash_8.png b/res/themes/bunkering/splash_8.png new file mode 100644 index 00000000..e3b6288f Binary files /dev/null and b/res/themes/bunkering/splash_8.png differ diff --git a/res/themes/bunkering/splash_9.png b/res/themes/bunkering/splash_9.png new file mode 100644 index 00000000..339d2991 Binary files /dev/null and b/res/themes/bunkering/splash_9.png differ diff --git a/res/themes/neon/splash_0.png b/res/themes/neon/splash_0.png new file mode 100644 index 00000000..5650df43 Binary files /dev/null and b/res/themes/neon/splash_0.png differ diff --git a/res/themes/neon/splash_1.png b/res/themes/neon/splash_1.png new file mode 100644 index 00000000..5d070384 Binary files /dev/null and b/res/themes/neon/splash_1.png differ diff --git a/res/themes/neon/splash_2.png b/res/themes/neon/splash_2.png new file mode 100644 index 00000000..18c9c2d7 Binary files /dev/null and b/res/themes/neon/splash_2.png differ diff --git a/res/themes/neon/splash_3.png b/res/themes/neon/splash_3.png new file mode 100644 index 00000000..c70bf1f4 Binary files /dev/null and b/res/themes/neon/splash_3.png differ diff --git a/res/themes/neon/splash_4.png b/res/themes/neon/splash_4.png new file mode 100644 index 00000000..bcdeb873 Binary files /dev/null and b/res/themes/neon/splash_4.png differ diff --git a/res/themes/neon/splash_5.png b/res/themes/neon/splash_5.png new file mode 100644 index 00000000..c70bf1f4 Binary files /dev/null and b/res/themes/neon/splash_5.png differ diff --git a/res/themes/neon/splash_6.png b/res/themes/neon/splash_6.png new file mode 100644 index 00000000..18c9c2d7 Binary files /dev/null and b/res/themes/neon/splash_6.png differ diff --git a/res/themes/neon/splash_7.png b/res/themes/neon/splash_7.png new file mode 100644 index 00000000..5d070384 Binary files /dev/null and b/res/themes/neon/splash_7.png differ diff --git a/res/themes/ramallah/keffiyeh.png b/res/themes/ramallah/keffiyeh.png new file mode 100644 index 00000000..dd51629c Binary files /dev/null and b/res/themes/ramallah/keffiyeh.png differ diff --git a/res/themes/ramallah/splash_0.png b/res/themes/ramallah/splash_0.png new file mode 100644 index 00000000..ce3f7368 Binary files /dev/null and b/res/themes/ramallah/splash_0.png differ diff --git a/res/themes/ramallah/splash_1.png b/res/themes/ramallah/splash_1.png new file mode 100644 index 00000000..de02bc64 Binary files /dev/null and b/res/themes/ramallah/splash_1.png differ diff --git a/res/themes/ramallah/splash_2.png b/res/themes/ramallah/splash_2.png new file mode 100644 index 00000000..9c2cb911 Binary files /dev/null and b/res/themes/ramallah/splash_2.png differ diff --git a/res/themes/ramallah/splash_3.png b/res/themes/ramallah/splash_3.png new file mode 100644 index 00000000..26c3887f Binary files /dev/null and b/res/themes/ramallah/splash_3.png differ diff --git a/res/themes/ramallah/splash_4.png b/res/themes/ramallah/splash_4.png new file mode 100644 index 00000000..a2c80401 Binary files /dev/null and b/res/themes/ramallah/splash_4.png differ diff --git a/res/themes/ramallah/splash_5.png b/res/themes/ramallah/splash_5.png new file mode 100644 index 00000000..90ce771c Binary files /dev/null and b/res/themes/ramallah/splash_5.png differ diff --git a/res/themes/ramallah/splash_6.png b/res/themes/ramallah/splash_6.png new file mode 100644 index 00000000..a2c80401 Binary files /dev/null and b/res/themes/ramallah/splash_6.png differ diff --git a/res/themes/ramallah/splash_7.png b/res/themes/ramallah/splash_7.png new file mode 100644 index 00000000..26c3887f Binary files /dev/null and b/res/themes/ramallah/splash_7.png differ diff --git a/res/themes/ramallah/splash_8.png b/res/themes/ramallah/splash_8.png new file mode 100644 index 00000000..9c2cb911 Binary files /dev/null and b/res/themes/ramallah/splash_8.png differ diff --git a/res/themes/ramallah/splash_9.png b/res/themes/ramallah/splash_9.png new file mode 100644 index 00000000..de02bc64 Binary files /dev/null and b/res/themes/ramallah/splash_9.png differ diff --git a/res/themes/zo/splash_0.png b/res/themes/zo/splash_0.png new file mode 100644 index 00000000..a782804f Binary files /dev/null and b/res/themes/zo/splash_0.png differ diff --git a/res/themes/zo/splash_1.png b/res/themes/zo/splash_1.png new file mode 100644 index 00000000..c568b6dd Binary files /dev/null and b/res/themes/zo/splash_1.png differ diff --git a/res/themes/zo/splash_10.png b/res/themes/zo/splash_10.png new file mode 100644 index 00000000..d950e27b Binary files /dev/null and b/res/themes/zo/splash_10.png differ diff --git a/res/themes/zo/splash_11.png b/res/themes/zo/splash_11.png new file mode 100644 index 00000000..a957af5d Binary files /dev/null and b/res/themes/zo/splash_11.png differ diff --git a/res/themes/zo/splash_2.png b/res/themes/zo/splash_2.png new file mode 100644 index 00000000..4c7760e3 Binary files /dev/null and b/res/themes/zo/splash_2.png differ diff --git a/res/themes/zo/splash_3.png b/res/themes/zo/splash_3.png new file mode 100644 index 00000000..e4c78748 Binary files /dev/null and b/res/themes/zo/splash_3.png differ diff --git a/res/themes/zo/splash_4.png b/res/themes/zo/splash_4.png new file mode 100644 index 00000000..c9379fe0 Binary files /dev/null and b/res/themes/zo/splash_4.png differ diff --git a/res/themes/zo/splash_5.png b/res/themes/zo/splash_5.png new file mode 100644 index 00000000..37053559 Binary files /dev/null and b/res/themes/zo/splash_5.png differ diff --git a/res/themes/zo/splash_6.png b/res/themes/zo/splash_6.png new file mode 100644 index 00000000..0649b6f4 Binary files /dev/null and b/res/themes/zo/splash_6.png differ diff --git a/res/themes/zo/splash_7.png b/res/themes/zo/splash_7.png new file mode 100644 index 00000000..0c57ceae Binary files /dev/null and b/res/themes/zo/splash_7.png differ diff --git a/res/themes/zo/splash_8.png b/res/themes/zo/splash_8.png new file mode 100644 index 00000000..36c1106b Binary files /dev/null and b/res/themes/zo/splash_8.png differ diff --git a/res/themes/zo/splash_9.png b/res/themes/zo/splash_9.png new file mode 100644 index 00000000..4a018004 Binary files /dev/null and b/res/themes/zo/splash_9.png differ diff --git a/res/themes/zo/veve.png b/res/themes/zo/veve.png new file mode 100644 index 00000000..46780532 Binary files /dev/null and b/res/themes/zo/veve.png differ diff --git a/res/tooltips/frame_properties.txt b/res/tooltips/frame_properties.txt new file mode 100644 index 00000000..ff358cfe --- /dev/null +++ b/res/tooltips/frame_properties.txt @@ -0,0 +1 @@ +Frame properties... | {Shift + F} diff --git a/res/tooltips/history.txt b/res/tooltips/history.txt new file mode 100644 index 00000000..3847c0b7 --- /dev/null +++ b/res/tooltips/history.txt @@ -0,0 +1 @@ +History... | {Shift + Y} diff --git a/res/tooltips/new_font.txt b/res/tooltips/new_font.txt index 7f32fa16..dab81ae7 100644 --- a/res/tooltips/new_font.txt +++ b/res/tooltips/new_font.txt @@ -1 +1 @@ -New font... | {Shift + F} +New font... | {Shift + T} diff --git a/res/tooltips/script_brush.txt b/res/tooltips/script_brush.txt new file mode 100644 index 00000000..a91cc4f9 --- /dev/null +++ b/res/tooltips/script_brush.txt @@ -0,0 +1 @@ +Script Brush | {Q} diff --git a/res/tooltips/shape_tool.txt b/res/tooltips/shape_tool.txt new file mode 100644 index 00000000..1a5f583c --- /dev/null +++ b/res/tooltips/shape_tool.txt @@ -0,0 +1 @@ +Shape Tool | {R} diff --git a/roadmap.md b/roadmap.md index 51b01063..b1f24221 100644 --- a/roadmap.md +++ b/roadmap.md @@ -3,17 +3,7 @@ This is a rough outline of planned features and when to expect them. Everything you see here is subject to change. -## **0.5.0** - The Performance Update - Late May 2024 - -### Add: -* Paste images from the system clipboard - -### Change: -* Implement massive performance optimizations to make the program faster and less demanding -on the machine -* Overhaul the move selection and pick up selection tools - -### Fix: -* Memory issues and related crashes - -## **1.0.0** - Official Release - Early June 2024 +## Q3 2024 +* Timeline: frames and layers combined in a single UI panel +* Lossless video export +* Presets for common scriptable behaviours diff --git a/src/com/jordanbunke/stipple_effect/StippleEffect.java b/src/com/jordanbunke/stipple_effect/StippleEffect.java index d02dfc6a..18b3594f 100644 --- a/src/com/jordanbunke/stipple_effect/StippleEffect.java +++ b/src/com/jordanbunke/stipple_effect/StippleEffect.java @@ -41,6 +41,7 @@ import com.jordanbunke.stipple_effect.visual.*; import com.jordanbunke.stipple_effect.visual.theme.SEColors; import com.jordanbunke.stipple_effect.visual.theme.Theme; +import com.jordanbunke.stipple_effect.visual.theme.logic.ThemeLogic; import java.awt.*; import java.io.File; @@ -299,7 +300,7 @@ private void updateStatus(final String message) { final GameImage statusUpdate = new GameImage(w, h); statusUpdate.fillRectangle( - Settings.getTheme().panelBackground.get(), 0, 0, w, h); + Settings.getTheme().panelBackground, 0, 0, w, h); statusUpdate.draw(text, 2 * Layout.BUTTON_BORDER_PX, Layout.TEXT_Y_OFFSET); @@ -316,7 +317,7 @@ private GameWindow makeWindow() { final GameWindow window = new GameWindow( PROGRAM_NAME + " " + getVersion(), size.width(), size.height(), - GraphicsUtils.loadIcon(IconCodes.PROGRAM), + GraphicsUtils.readIconAsset(IconCodes.PROGRAM), true, false, !windowed); window.hideCursor(); return window; @@ -465,6 +466,9 @@ private void processNonStateKeyPresses(final InputEventLogger eventLogger) { this::openAutomationScript); } else if (eventLogger.isPressed(Key.SHIFT)) { // Shift + ? + eventLogger.checkForMatchingKeyStroke( + GameKeyEvent.newKeyStroke(Key.Y, GameKeyEvent.Action.PRESS), + DialogAssembly::setDialogToHistory); eventLogger.checkForMatchingKeyStroke( GameKeyEvent.newKeyStroke(Key.C, GameKeyEvent.Action.PRESS), this::swapColors); @@ -514,7 +518,7 @@ private void processNonStateKeyPresses(final InputEventLogger eventLogger) { getSelectedPalette()); }); eventLogger.checkForMatchingKeyStroke( - GameKeyEvent.newKeyStroke(Key.F, GameKeyEvent.Action.PRESS), + GameKeyEvent.newKeyStroke(Key.T, GameKeyEvent.Action.PRESS), () -> { if (tool.equals(TextTool.get())) DialogAssembly.setDialogToNewFont(); @@ -579,6 +583,9 @@ private void processNonStateKeyPresses(final InputEventLogger eventLogger) { eventLogger.checkForMatchingKeyStroke( GameKeyEvent.newKeyStroke(Key.D, GameKeyEvent.Action.PRESS), () -> setTool(ShadeBrush.get())); + eventLogger.checkForMatchingKeyStroke( + GameKeyEvent.newKeyStroke(Key.Q, GameKeyEvent.Action.PRESS), + () -> setTool(ScriptBrush.get())); eventLogger.checkForMatchingKeyStroke( GameKeyEvent.newKeyStroke(Key.G, GameKeyEvent.Action.PRESS), () -> setTool(GradientTool.get())); @@ -594,6 +601,9 @@ private void processNonStateKeyPresses(final InputEventLogger eventLogger) { eventLogger.checkForMatchingKeyStroke( GameKeyEvent.newKeyStroke(Key.L, GameKeyEvent.Action.PRESS), () -> setTool(LineTool.get())); + eventLogger.checkForMatchingKeyStroke( + GameKeyEvent.newKeyStroke(Key.R, GameKeyEvent.Action.PRESS), + () -> setTool(ShapeTool.get())); eventLogger.checkForMatchingKeyStroke( GameKeyEvent.newKeyStroke(Key.T, GameKeyEvent.Action.PRESS), () -> setTool(TextTool.get())); @@ -684,18 +694,19 @@ private void updateToolTip() { private GameImage drawToolTip() { final Theme t = Settings.getTheme(); + final Color cText = ThemeLogic.intuitTextColor(t.panelBackground, true); final String[] lines = ParserUtils.getToolTip(toolTipCode); - final TextBuilder tb = GraphicsUtils.uiText(t.textLight.get()); + final TextBuilder tb = GraphicsUtils.uiText(cText); for (int l = 0; l < lines.length; l++) { final String[] segments = ParserUtils.extractHighlight(lines[l]); for (int i = 0; i < segments.length; i++) { if (i % 2 == 0) - tb.setColor(t.textLight.get()); + tb.setColor(cText); else - tb.setColor(t.textShortcut.get()); + tb.setColor(t.textShortcut); tb.addText(segments[i]); } @@ -709,8 +720,8 @@ private GameImage drawToolTip() { h = text.getHeight() - (6 * Layout.BUTTON_BORDER_PX); final GameImage tt = new GameImage(w, h); - tt.fillRectangle(t.panelBackground.get(), 0, 0, w, h); - tt.drawRectangle(t.panelDivisions.get(), 2f, 1, 1, w - 2, h - 2); + tt.fillRectangle(t.panelBackground, 0, 0, w, h); + tt.drawRectangle(t.panelDivisions, 2f, 1, 1, w - 2, h - 2); tt.draw(text, 3 * Layout.BUTTON_BORDER_PX, Layout.TEXT_Y_OFFSET); return tt.submit(); @@ -782,7 +793,7 @@ public void render(final GameImage canvas) { // borders final float strokeWidth = 2f; - canvas.setColor(Settings.getTheme().panelDivisions.get()); + canvas.setColor(Settings.getTheme().panelDivisions); canvas.drawLine(strokeWidth, fp.x, fp.y, fp.x, tobp.y); // projects and frame separation canvas.drawLine(strokeWidth, pp.x, tobp.y, Layout.width(), tobp.y); // top segments and middle separation canvas.drawLine(strokeWidth, tp.x, wp.y, lp.x, wp.y); // tool options bar and tools/workspace separation @@ -792,7 +803,7 @@ public void render(final GameImage canvas) { canvas.drawLine(strokeWidth, lp.x, lp.y, lp.x, bbp.y); // workspace/option bar and right segments separation if (dialog != null) { - canvas.fillRectangle(Settings.getTheme().dialogVeil.get(), + canvas.fillRectangle(Settings.getTheme().dialogVeil, 0, 0, Layout.width(), Layout.height()); dialog.render(canvas); } @@ -825,7 +836,7 @@ public void debugRender(final GameImage canvas, final GameDebugger debugger) { private GameImage drawColorsSegment() { final GameImage colors = new GameImage(Layout.getColorsWidth(), Layout.getColorsHeight()); - colors.fillRectangle(Settings.getTheme().panelBackground.get(), 0, 0, + colors.fillRectangle(Settings.getTheme().panelBackground, 0, 0, Layout.getColorsWidth(), Layout.getColorsHeight()); return colors.submit(); @@ -834,7 +845,7 @@ private GameImage drawColorsSegment() { private GameImage drawTools() { final GameImage tools = new GameImage(Layout.getToolsWidth(), Layout.getToolsHeight()); - tools.fillRectangle(Settings.getTheme().panelBackground.get(), 0, 0, + tools.fillRectangle(Settings.getTheme().panelBackground, 0, 0, Layout.getToolsWidth(), Layout.getToolsHeight()); return tools.submit(); @@ -845,7 +856,7 @@ private GameImage drawToolOptionsBar() { Layout.getToolOptionsBarWidth(), Layout.TOOL_OPTIONS_BAR_H); toolOptionsBar.fillRectangle( - Settings.getTheme().panelBackground.get(), 0, 0, + Settings.getTheme().panelBackground, 0, 0, Layout.getToolOptionsBarWidth(), Layout.TOOL_OPTIONS_BAR_H); return toolOptionsBar.submit(); @@ -855,7 +866,7 @@ private GameImage drawLayers() { final GameImage layers = new GameImage(Layout.getLayersWidth(), Layout.getLayersHeight()); layers.fillRectangle( - Settings.getTheme().panelBackground.get(), 0, 0, + Settings.getTheme().panelBackground, 0, 0, Layout.getLayersWidth(), Layout.getLayersHeight()); return layers.submit(); @@ -865,7 +876,7 @@ private GameImage drawProjects() { final GameImage projects = new GameImage(Layout.getProjectsWidth(), Layout.getTopPanelHeight()); projects.fillRectangle( - Settings.getTheme().panelBackground.get(), 0, 0, + Settings.getTheme().panelBackground, 0, 0, Layout.getProjectsWidth(), Layout.getTopPanelHeight()); return projects.submit(); @@ -875,7 +886,7 @@ private GameImage drawFrames() { final GameImage frames = new GameImage(Layout.getFramesWidth(), Layout.getTopPanelHeight()); frames.fillRectangle( - Settings.getTheme().panelBackground.get(), 0, 0, + Settings.getTheme().panelBackground, 0, 0, Layout.getFramesWidth(), Layout.getTopPanelHeight()); return frames.submit(); @@ -885,7 +896,7 @@ private GameImage drawBottomBar() { final GameImage bottomBar = new GameImage(Layout.width(), Layout.BOTTOM_BAR_H); bottomBar.fillRectangle( - Settings.getTheme().panelBackground.get(), 0, 0, + Settings.getTheme().panelBackground, 0, 0, Layout.width(), Layout.BOTTOM_BAR_H); return bottomBar.submit(); @@ -1444,7 +1455,6 @@ public void remakeWindow() { public void autoAssignPickUpSelection() { this.tool = PickUpSelection.get(); - getContext().redrawSelectionOverlay(); rebuildToolButtonMenu(); } @@ -1464,7 +1474,6 @@ else if (was.equals(TextTool.get()) && tool.equals(PickUpSelection.get())) PickUpSelection.get().engage(getContext()); - getContext().redrawSelectionOverlay(); rebuildToolButtonMenu(); } diff --git a/src/com/jordanbunke/stipple_effect/layer/SELayer.java b/src/com/jordanbunke/stipple_effect/layer/SELayer.java index a1343d70..9f94c59a 100644 --- a/src/com/jordanbunke/stipple_effect/layer/SELayer.java +++ b/src/com/jordanbunke/stipple_effect/layer/SELayer.java @@ -2,8 +2,8 @@ import com.jordanbunke.delta_time.image.GameImage; import com.jordanbunke.delta_time.image.ImageProcessing; -import com.jordanbunke.delta_time.utility.math.Coord2D; import com.jordanbunke.stipple_effect.StippleEffect; +import com.jordanbunke.stipple_effect.selection.Selection; import com.jordanbunke.stipple_effect.utility.Constants; import com.jordanbunke.stipple_effect.utility.DialogVals; import com.jordanbunke.stipple_effect.utility.math.StitchSplitMath; @@ -12,7 +12,6 @@ import java.util.ArrayList; import java.util.Arrays; import java.util.List; -import java.util.Set; import java.util.function.BinaryOperator; public final class SELayer { @@ -113,7 +112,7 @@ public SELayer returnFrameReplaced(final GameImage edit, final int frameIndex) { } public SELayer returnStamped( - final GameImage edit, final Set pixels, final int frameIndex + final GameImage edit, final Selection selection, final int frameIndex ) { final List frames = new ArrayList<>(this.frames); final GameImage content = getFrame(frameIndex); @@ -121,10 +120,9 @@ public SELayer returnStamped( final GameImage composed = new GameImage(content); final int w = composed.getWidth(), h = composed.getHeight(); - pixels.stream().filter(p -> p.x >= 0 && p.x < w && - p.y >= 0 && p.y < h).forEach(p -> { - final Color c = edit.getColorAt(p.x, p.y); - composed.setRGB(p.x, p.y, c.getRGB()); + selection.pixelAlgorithm(w, h, (x, y) -> { + final Color c = edit.getColorAt(x, y); + composed.setRGB(x, y, c.getRGB()); }); composed.free(); diff --git a/src/com/jordanbunke/stipple_effect/palette/PaletteLoader.java b/src/com/jordanbunke/stipple_effect/palette/PaletteLoader.java index 4305caf2..2c3e8e93 100644 --- a/src/com/jordanbunke/stipple_effect/palette/PaletteLoader.java +++ b/src/com/jordanbunke/stipple_effect/palette/PaletteLoader.java @@ -2,7 +2,7 @@ import com.jordanbunke.delta_time.image.GameImage; import com.jordanbunke.delta_time.io.ResourceLoader; -import com.jordanbunke.delta_time.utility.math.Coord2D; +import com.jordanbunke.stipple_effect.selection.Selection; import com.jordanbunke.stipple_effect.utility.Constants; import com.jordanbunke.stipple_effect.visual.theme.SEColors; @@ -10,7 +10,6 @@ import java.nio.file.Path; import java.util.ArrayList; import java.util.List; -import java.util.Set; public class PaletteLoader { // palette codes @@ -48,22 +47,21 @@ private static Color[] paletteColorsFromCode(final String code) { public static void addPaletteColorsFromImage( final GameImage image, final List colors, - final Set pixels + final Selection selection ) { for (int y = 0; y < image.getHeight(); y++) for (int x = 0; x < image.getWidth(); x++) { + if (colors.size() >= Constants.MAX_PALETTE_SIZE) + return; + final Color c = image.getColorAt(x, y); if (c.getAlpha() == 0) continue; - if (!colors.contains(c) && (pixels == null || - pixels.contains(new Coord2D(x, y)))) { + if (!colors.contains(c) && (selection == null || + selection.selected(x, y))) colors.add(c); - - if (colors.size() >= Constants.MAX_PALETTE_SIZE) - return; - } } } } diff --git a/src/com/jordanbunke/stipple_effect/preview/PreviewPlayback.java b/src/com/jordanbunke/stipple_effect/preview/PreviewPlayback.java new file mode 100644 index 00000000..9a8eb2af --- /dev/null +++ b/src/com/jordanbunke/stipple_effect/preview/PreviewPlayback.java @@ -0,0 +1,54 @@ +package com.jordanbunke.stipple_effect.preview; + +import com.jordanbunke.delta_time.events.GameKeyEvent; +import com.jordanbunke.delta_time.events.Key; +import com.jordanbunke.delta_time.io.InputEventLogger; +import com.jordanbunke.stipple_effect.project.PlaybackInfo; +import com.jordanbunke.stipple_effect.utility.Constants; + +public interface PreviewPlayback { + void toFirstFrame(); + void toLastFrame(); + void previousFrame(); + void nextFrame(); + + PlaybackInfo getPlaybackInfo(); + + default void processKeys(final InputEventLogger eventLogger) { + final PlaybackInfo playbackInfo = getPlaybackInfo(); + + if (eventLogger.isPressed(Key.CTRL) && eventLogger.isPressed(Key.SHIFT)) { + // CTRL + SHIFT + ? + eventLogger.checkForMatchingKeyStroke( + GameKeyEvent.newKeyStroke(Key.SPACE, GameKeyEvent.Action.PRESS), + this::previousFrame); + } else if (eventLogger.isPressed(Key.CTRL)) { + // CTRL + ? + eventLogger.checkForMatchingKeyStroke( + GameKeyEvent.newKeyStroke(Key.SPACE, GameKeyEvent.Action.PRESS), + this::nextFrame); + eventLogger.checkForMatchingKeyStroke( + GameKeyEvent.newKeyStroke(Key.LEFT_ARROW, GameKeyEvent.Action.PRESS), + this::toFirstFrame); + eventLogger.checkForMatchingKeyStroke( + GameKeyEvent.newKeyStroke(Key.RIGHT_ARROW, GameKeyEvent.Action.PRESS), + this::toLastFrame); + eventLogger.checkForMatchingKeyStroke( + GameKeyEvent.newKeyStroke(Key.ENTER, GameKeyEvent.Action.PRESS), + playbackInfo::toggleMode); + } else if (eventLogger.isPressed(Key.SHIFT)) { + // SHIFT + ? + eventLogger.checkForMatchingKeyStroke( + GameKeyEvent.newKeyStroke(Key.LEFT_ARROW, GameKeyEvent.Action.PRESS), + () -> playbackInfo.incrementFps(-Constants.PLAYBACK_FPS_INC)); + eventLogger.checkForMatchingKeyStroke( + GameKeyEvent.newKeyStroke(Key.RIGHT_ARROW, GameKeyEvent.Action.PRESS), + () -> playbackInfo.incrementFps(Constants.PLAYBACK_FPS_INC)); + } else { + // single key presses + eventLogger.checkForMatchingKeyStroke( + GameKeyEvent.newKeyStroke(Key.SPACE, GameKeyEvent.Action.PRESS), + playbackInfo::togglePlaying); + } + } +} diff --git a/src/com/jordanbunke/stipple_effect/preview/PreviewWindow.java b/src/com/jordanbunke/stipple_effect/preview/PreviewWindow.java index 6464cd0c..131eb973 100644 --- a/src/com/jordanbunke/stipple_effect/preview/PreviewWindow.java +++ b/src/com/jordanbunke/stipple_effect/preview/PreviewWindow.java @@ -4,10 +4,7 @@ import com.jordanbunke.delta_time._core.Program; import com.jordanbunke.delta_time._core.ProgramContext; import com.jordanbunke.delta_time.debug.GameDebugger; -import com.jordanbunke.delta_time.events.GameEvent; -import com.jordanbunke.delta_time.events.GameMouseScrollEvent; -import com.jordanbunke.delta_time.events.GameWindowEvent; -import com.jordanbunke.delta_time.events.WindowMovedEvent; +import com.jordanbunke.delta_time.events.*; import com.jordanbunke.delta_time.image.GameImage; import com.jordanbunke.delta_time.io.FileIO; import com.jordanbunke.delta_time.io.InputEventLogger; @@ -40,7 +37,6 @@ import com.jordanbunke.stipple_effect.visual.menu_elements.IconButton; import com.jordanbunke.stipple_effect.visual.menu_elements.IncrementalRangeElements; import com.jordanbunke.stipple_effect.visual.menu_elements.scrollable.PreviewHousingBox; -import com.jordanbunke.stipple_effect.visual.theme.SEColors; import java.nio.file.Path; import java.util.Arrays; @@ -48,7 +44,7 @@ import java.util.function.Supplier; import java.util.stream.IntStream; -public class PreviewWindow implements ProgramContext { +public class PreviewWindow implements ProgramContext, PreviewPlayback { public static final int BORDER = 1, MENU_X_ALLOTMENT_PX = Layout.BUTTON_INC * 10, MENU_Y_ALLOTMENT_PX = (Layout.BUTTON_INC * 4) + @@ -215,7 +211,7 @@ private Menu makeMenu() { (fpsButtonY - Layout.BUTTON_OFFSET) + Layout.TEXT_Y_OFFSET, 1, Constants.MIN_PLAYBACK_FPS, Constants.MAX_PLAYBACK_FPS, playbackInfo::setFps, playbackInfo::getFps, - i -> i, sv -> sv, sv -> sv + " fps", "XXX fps"); + i -> i, sv -> sv, sv -> sv + " FPS", "XXX FPS"); mb.addAll(fps.decButton, fps.incButton, fps.slider, fps.value); final IconButton decZoom = IconButton.make(IconCodes.DECREMENT, @@ -238,12 +234,11 @@ private static DynamicLabel labelAfterLastButton( final MenuElement lastButton, final Supplier getter, final String widestCase ) { - return new DynamicLabel( + return DynamicLabel.make( lastButton.getRenderPosition().displace( Layout.BUTTON_DIM + Layout.CONTENT_BUFFER_PX, -Layout.BUTTON_OFFSET + Layout.TEXT_Y_OFFSET), - MenuElement.Anchor.LEFT_TOP, Settings.getTheme().textLight.get(), - getter, widestCase); + getter, DynamicLabel.getWidth(widestCase)); } private void setZoom(final float zoom) { @@ -256,11 +251,11 @@ private void setZoom(final float zoom) { } private void zoomIn() { - setZoom(zoom * Constants.ZOOM_CHANGE_LEVEL); + setZoom(zoom + 1f); } private void zoomOut() { - setZoom(zoom / Constants.ZOOM_CHANGE_LEVEL); + setZoom(zoom - 1f); } @Override @@ -268,6 +263,9 @@ public void process(final InputEventLogger eventLogger) { menu.process(eventLogger); housingBox.process(eventLogger); + if (frameCount > 1) + processKeys(eventLogger); + mousePos = eventLogger.getAdjustedMousePosition(); final List unprocessed = eventLogger.getUnprocessedEvents(); @@ -314,8 +312,13 @@ public void update(final double deltaTime) { } private void animate(final double deltaTime) { + final double duration = script == null + ? context.getState().getFrameDurations().get(frameIndex) + : Constants.DEFAULT_FRAME_DURATION; + if (playbackInfo.isPlaying()) { - final boolean nextFrameDue = playbackInfo.checkIfNextFrameDue(deltaTime); + final boolean nextFrameDue = + playbackInfo.checkIfNextFrameDue(deltaTime, duration); if (nextFrameDue) frameIndex = playbackInfo.nextAnimationFrameForPreview( @@ -373,8 +376,11 @@ private void windowUpdateCheck() { final int w = content[frameIndex].getWidth(), h = content[frameIndex].getHeight(); - if (canvasW != w || canvasH != h) + if (canvasW != w || canvasH != h) { updateWindow(); + // refocus Stipple Effect window - only here, where resizing is implicit + StippleEffect.get().window.focus(); + } } private void updateWindow() { @@ -409,7 +415,7 @@ private GameWindow makeWindow() { final GameWindow window = new GameWindow("Preview: " + context.projectInfo.getFormattedName(false, false), - width, height, GraphicsUtils.loadIcon(IconCodes.PROGRAM), + width, height, GraphicsUtils.readIconAsset(IconCodes.PROGRAM), false, false, false); window.hideCursor(); window.setPosition(winX, winY); @@ -424,7 +430,7 @@ private PreviewHousingBox makeHousingBox() { @Override public void render(final GameImage canvas) { - canvas.fill(Settings.getTheme().panelBackground.get()); + canvas.fill(Settings.getTheme().panelBackground); refreshPreviewImage(); @@ -440,7 +446,7 @@ private void refreshPreviewImage() { final GameImage canvasContents = new GameImage(w + (2 * BORDER), h + (2 * BORDER)); - canvasContents.drawRectangle(SEColors.black(), 2f, BORDER, BORDER, w, h); + canvasContents.drawRectangle(Settings.getTheme().panelDivisions, 2f, BORDER, BORDER, w, h); canvasContents.draw(context.getCheckerboard(), BORDER, BORDER, w, h); canvasContents.draw(content[frameIndex], BORDER, BORDER, w, h); @@ -500,4 +506,29 @@ private void importPreview() { StippleEffect.get().scheduleJob(() -> StippleEffect.get().addContext(project, true)); } + + @Override + public void toFirstFrame() { + frameIndex = 0; + } + + @Override + public void toLastFrame() { + frameIndex = frameCount - 1; + } + + @Override + public void previousFrame() { + frameIndex = frameIndex == 0 ? frameCount - 1 : frameIndex - 1; + } + + @Override + public void nextFrame() { + frameIndex = (frameIndex + 1) % frameCount; + } + + @Override + public PlaybackInfo getPlaybackInfo() { + return playbackInfo; + } } diff --git a/src/com/jordanbunke/stipple_effect/project/PlaybackInfo.java b/src/com/jordanbunke/stipple_effect/project/PlaybackInfo.java index 85bf5071..532302c6 100644 --- a/src/com/jordanbunke/stipple_effect/project/PlaybackInfo.java +++ b/src/com/jordanbunke/stipple_effect/project/PlaybackInfo.java @@ -143,7 +143,11 @@ private void updateMillisPerFrame() { millisPerFrame = Constants.MILLIS_IN_SECOND / fps; } - public boolean checkIfNextFrameDue(final double deltaTime) { + public boolean checkIfNextFrameDue( + final double deltaTime, final double frameDuration + ) { + final int millisForThisFrame = (int)(millisPerFrame * frameDuration); + nanosAccumulated += deltaTime; while (nanosAccumulated >= NANOS_IN_MILLI) { @@ -151,8 +155,8 @@ public boolean checkIfNextFrameDue(final double deltaTime) { millisAccumulated++; } - if (millisAccumulated >= millisPerFrame) { - millisAccumulated -= millisPerFrame; + if (millisAccumulated >= millisForThisFrame) { + millisAccumulated -= millisForThisFrame; return true; } diff --git a/src/com/jordanbunke/stipple_effect/project/ProjectInfo.java b/src/com/jordanbunke/stipple_effect/project/ProjectInfo.java index ef873342..4e9d6ed1 100644 --- a/src/com/jordanbunke/stipple_effect/project/ProjectInfo.java +++ b/src/com/jordanbunke/stipple_effect/project/ProjectInfo.java @@ -1,14 +1,16 @@ package com.jordanbunke.stipple_effect.project; -import com.jordanbunke.anim.AnimWriter; -import com.jordanbunke.anim.GIFWriter; -import com.jordanbunke.anim.MP4Writer; +import com.jordanbunke.anim.data.AnimBuilder; +import com.jordanbunke.anim.writers.AnimWriter; +import com.jordanbunke.anim.writers.GIFWriter; +import com.jordanbunke.anim.writers.MP4Writer; import com.jordanbunke.delta_time.image.GameImage; import com.jordanbunke.delta_time.image.ImageProcessing; import com.jordanbunke.delta_time.io.GameImageIO; import com.jordanbunke.delta_time.utility.math.MathPlus; import com.jordanbunke.stipple_effect.StippleEffect; import com.jordanbunke.stipple_effect.selection.SelectionMode; +import com.jordanbunke.stipple_effect.state.ProjectState; import com.jordanbunke.stipple_effect.stip.ParserSerializer; import com.jordanbunke.stipple_effect.utility.Constants; import com.jordanbunke.stipple_effect.utility.DialogVals; @@ -18,6 +20,7 @@ import com.jordanbunke.stipple_effect.visual.DialogAssembly; import java.nio.file.Path; +import java.util.List; import java.util.function.BinaryOperator; public class ProjectInfo { @@ -131,14 +134,27 @@ public void save() { switch (saveType) { case GIF, MP4 -> { - final GameImage[] images = new GameImage[framesToSave]; + final ProjectState state = context.getState(); + final List frameDurations = state.getFrameDurations(); + + final int standardDurationMillis = Constants.MILLIS_IN_SECOND / fps; + + final AnimBuilder ab = new AnimBuilder(); for (int i = 0; i < framesToSave; i++) { - images[i] = context.getState().draw( - false, false, f0 + i); + final int frameIndex = f0 + i; + + final int frameDurationMillis = + (int)(standardDurationMillis * + frameDurations.get(frameIndex)); + + GameImage img = context.getState().draw( + false, false, frameIndex); if (scaleUp > 1) - images[i] = ImageProcessing.scale(images[i], scaleUp); + img = ImageProcessing.scale(img, scaleUp); + + ab.addFrame(img, frameDurationMillis); } final AnimWriter writer = saveType == SaveType.GIF @@ -147,8 +163,7 @@ public void save() { final Thread animSaverThread = new Thread(() -> { final Path filepath = buildFilepath(); - writer.write(filepath, images, - Constants.MILLIS_IN_SECOND / fps); + writer.write(filepath, ab.build()); StatusUpdates.saved(filepath); }); diff --git a/src/com/jordanbunke/stipple_effect/project/RenderInfo.java b/src/com/jordanbunke/stipple_effect/project/RenderInfo.java index c9733110..726aaf9c 100644 --- a/src/com/jordanbunke/stipple_effect/project/RenderInfo.java +++ b/src/com/jordanbunke/stipple_effect/project/RenderInfo.java @@ -1,6 +1,7 @@ package com.jordanbunke.stipple_effect.project; import com.jordanbunke.delta_time.utility.math.Coord2D; +import com.jordanbunke.delta_time.utility.math.MathPlus; import com.jordanbunke.stipple_effect.StippleEffect; import com.jordanbunke.stipple_effect.state.ProjectState; import com.jordanbunke.stipple_effect.tools.ToolWithBreadth; @@ -9,51 +10,64 @@ public class RenderInfo { private Coord2D anchor; - private float zoomFactor; + private ZoomLevel zoomLevel; private boolean pixelGridOn; public RenderInfo(final int imageWidth, final int imageHeight) { this.anchor = new Coord2D(imageWidth / 2, imageHeight / 2); - this.zoomFactor = Constants.DEF_ZOOM; + this.zoomLevel = ZoomLevel.fromZ(Constants.DEF_ZOOM); this.pixelGridOn = Settings.isPixelGridOnByDefault(); } public void zoomIn(final Coord2D targetPixel) { - setZoomFactor(zoomFactor * Constants.ZOOM_CHANGE_LEVEL); - adjustAnchorFromZoom(targetPixel); + zoom(zoomLevel.in(), targetPixel); } - public void zoomOut() { - setZoomFactor(zoomFactor / Constants.ZOOM_CHANGE_LEVEL); + public void zoomOut(final Coord2D targetPixel) { + zoom(zoomLevel.out(), targetPixel); } - private void adjustAnchorFromZoom(final Coord2D targetPixel) { - if (!targetPixel.equals(Constants.NO_VALID_TARGET)) { - final Coord2D adjusted = new Coord2D((anchor.x + targetPixel.x) / 2, - (anchor.y + targetPixel.y) / 2); + private void zoom(final ZoomLevel to, final Coord2D targetPixel) { + final float before = zoomLevel.z; + + setZoomLevel(to); + adjustAnchorFromZoom(targetPixel, before, zoomLevel.z); + } + + private void adjustAnchorFromZoom( + final Coord2D tp, final float before, final float after + ) { + if (!tp.equals(Constants.NO_VALID_TARGET)) { + final int pixelXDiff = (int)((tp.x - anchor.x) * before), + pixelYDiff = (int)((tp.y - anchor.y) * before); + + final Coord2D adjusted = before == after + ? anchor.displace((int)Math.signum(pixelXDiff), + (int)Math.signum(pixelYDiff)) + : new Coord2D(tp.x - Math.round(pixelXDiff / after), + tp.y - Math.round(pixelYDiff / after)); + setAnchor(adjusted); } } - public void setZoomFactor(final float zoomFactor) { - final float was = this.zoomFactor; + public void setZoomLevel(final ZoomLevel zoomLevel) { + final boolean redrawOverlays = this.zoomLevel != zoomLevel && + zoomLevel.z > Constants.ZOOM_FOR_OVERLAY; - this.zoomFactor = Math.max(Constants.MIN_ZOOM, - Math.min(zoomFactor, Constants.MAX_ZOOM)); + this.zoomLevel = zoomLevel; - if (this.zoomFactor != was && - this.zoomFactor >= Constants.ZOOM_FOR_OVERLAY) { + if (redrawOverlays) ToolWithBreadth.redrawToolOverlays(); - StippleEffect.get().getContext().redrawSelectionOverlay(); - } } public void setAnchor(final Coord2D anchor) { final ProjectState state = StippleEffect.get().getContext().getState(); final int w = state.getImageWidth(), h = state.getImageHeight(); - this.anchor = new Coord2D(Math.max(0, Math.min(anchor.x, w)), - Math.max(0, Math.min(anchor.y, h))); + this.anchor = new Coord2D( + MathPlus.bounded(0, anchor.x, w), + MathPlus.bounded(0, anchor.y, h)); } public void incrementAnchor(final Coord2D delta) { @@ -71,13 +85,15 @@ public void setPixelGrid(final boolean pixelGridOn) { } public String getZoomText() { - return zoomFactor >= 1 / 4f - ? (int)(zoomFactor * 100) + "%" - : (zoomFactor * 100) + "%"; + return zoomLevel.toString(); } public float getZoomFactor() { - return zoomFactor; + return zoomLevel.z; + } + + public ZoomLevel getZoomLevel() { + return zoomLevel; } public Coord2D getAnchor() { diff --git a/src/com/jordanbunke/stipple_effect/project/SEContext.java b/src/com/jordanbunke/stipple_effect/project/SEContext.java index 30e7f28b..eeacc9ff 100644 --- a/src/com/jordanbunke/stipple_effect/project/SEContext.java +++ b/src/com/jordanbunke/stipple_effect/project/SEContext.java @@ -4,6 +4,7 @@ import com.jordanbunke.delta_time.image.GameImage; import com.jordanbunke.delta_time.io.InputEventLogger; import com.jordanbunke.delta_time.scripting.ast.nodes.function.HeadFuncNode; +import com.jordanbunke.delta_time.utility.math.Bounds2D; import com.jordanbunke.delta_time.utility.math.Coord2D; import com.jordanbunke.stipple_effect.StippleEffect; import com.jordanbunke.stipple_effect.layer.LayerHelper; @@ -11,6 +12,7 @@ import com.jordanbunke.stipple_effect.layer.SELayer; import com.jordanbunke.stipple_effect.palette.Palette; import com.jordanbunke.stipple_effect.palette.PaletteLoader; +import com.jordanbunke.stipple_effect.preview.PreviewWindow; import com.jordanbunke.stipple_effect.scripting.SEInterpreter; import com.jordanbunke.stipple_effect.selection.*; import com.jordanbunke.stipple_effect.state.Operation; @@ -22,8 +24,6 @@ import com.jordanbunke.stipple_effect.utility.math.StitchSplitMath; import com.jordanbunke.stipple_effect.utility.settings.Settings; import com.jordanbunke.stipple_effect.visual.DialogAssembly; -import com.jordanbunke.stipple_effect.visual.GraphicsUtils; -import com.jordanbunke.stipple_effect.preview.PreviewWindow; import com.jordanbunke.stipple_effect.visual.theme.Theme; import java.awt.*; @@ -44,8 +44,9 @@ public class SEContext { private boolean inWorkspaceBounds; private Coord2D targetPixel; - private GameImage selectionOverlay, checkerboard; - private Map pixelGridMap; + private GameImage checkerboard; + private SelectionOverlay selectionOverlay; + private Map pixelGridMap; public SEContext( final int imageWidth, final int imageHeight @@ -66,21 +67,17 @@ public SEContext( targetPixel = Constants.NO_VALID_TARGET; inWorkspaceBounds = false; + selectionOverlay = new SelectionOverlay(); + redrawCanvasAuxiliaries(); } public void redrawSelectionOverlay() { - final Set selection = getState().getSelection(); - final Tool tool = StippleEffect.get().getTool(); - - final boolean movable = Tool.canMoveSelectionBounds(tool) || - tool.equals(Wand.get()); + selectionOverlay = new SelectionOverlay(getState().getSelection()); + } - selectionOverlay = getState().hasSelection() - ? GraphicsUtils.drawSelectionOverlay( - renderInfo.getZoomFactor(), selection, - movable, tool instanceof MoverTool) - : GameImage.dummy(); + public void updateOverlayOffset() { + selectionOverlay.updateTL(getState().getSelection()); } private Coord2D[] getImageRenderBounds( @@ -123,7 +120,7 @@ public GameImage drawWorkspace() { // background workspace.fillRectangle( - Settings.getTheme().workspaceBackground.get(), 0, 0, ww, wh); + Settings.getTheme().workspaceBackground, 0, 0, ww, wh); // math final float zoomFactor = renderInfo.getZoomFactor(); @@ -150,8 +147,8 @@ public GameImage drawWorkspace() { // pixel grid if (renderInfo.isPixelGridOn() && couldRenderPixelGrid()) { - final GameImage pixelGrid = pixelGridMap - .getOrDefault(zoomFactor, GameImage.dummy()); + final GameImage pixelGrid = pixelGridMap.getOrDefault( + renderInfo.getZoomLevel(), GameImage.dummy()); final int z = (int) zoomFactor; if (pixelGrid.getWidth() > GameImage.dummy().getWidth()) @@ -181,25 +178,24 @@ public GameImage drawWorkspace() { // selection overlay - drawing box if (tool instanceof OverlayTool overlayTool && overlayTool.isDrawing()) { - final Coord2D tl = overlayTool.getTopLeft(); final GameImage overlay = overlayTool.getSelectionOverlay(); workspace.draw(overlay, - (render.x + (int)(tl.x * zoomFactor)) - - Constants.OVERLAY_BORDER_PX, - (render.y + (int)(tl.y * zoomFactor)) - - Constants.OVERLAY_BORDER_PX); + render.x - Constants.OVERLAY_BORDER_PX, + render.y - Constants.OVERLAY_BORDER_PX); } // persistent selection overlay - if (getState().hasSelection()) { - final Coord2D tl = SelectionUtils.topLeft(getState().getSelection()); - - workspace.draw(selectionOverlay, - (render.x + (int)(tl.x * zoomFactor)) - - Constants.OVERLAY_BORDER_PX, - (render.y + (int)(tl.y * zoomFactor)) - - Constants.OVERLAY_BORDER_PX); + if (getState().hasSelection() && + !(tool instanceof MoverTool mt && mt.isMoving())) { + final boolean movable = Tool.canMoveSelectionBounds(tool) || + tool.equals(Wand.get()); + + final GameImage selectionAsset = + selectionOverlay.draw(zoomFactor, render, ww, wh, + movable, tool instanceof MoverTool); + + workspace.draw(selectionAsset, 0, 0); } } @@ -207,11 +203,15 @@ public GameImage drawWorkspace() { } public void animate(final double deltaTime) { + final ProjectState s = getState(); + if (playbackInfo.isPlaying()) { - final boolean nextFrameDue = playbackInfo.checkIfNextFrameDue(deltaTime); + final boolean nextFrameDue = + playbackInfo.checkIfNextFrameDue(deltaTime, + s.getFrameDurations().get(s.getFrameIndex())); if (nextFrameDue) - playbackInfo.executeAnimation(getState()); + playbackInfo.executeAnimation(s); } } @@ -259,7 +259,7 @@ private void processTools(final InputEventLogger eventLogger) { if (tool instanceof SnappableTool st) st.setSnap(eventLogger.isPressed(Key.SHIFT)); - if (tool instanceof MoverTool mt) + if (tool instanceof MoverTool mt) mt.setSnapToggled(eventLogger.isPressed(Key.CTRL)); for (GameEvent e : eventLogger.getUnprocessedEvents()) { @@ -374,7 +374,7 @@ public void processAdditionalMouseEvents(final InputEventLogger eventLogger) { Settings.Code.INVERT_ZOOM_DIRECTION) < 0) renderInfo.zoomIn(targetPixel); else - renderInfo.zoomOut(); + renderInfo.zoomOut(targetPixel); } } } @@ -513,6 +513,9 @@ private void processCompoundKeyInputs(final InputEventLogger eventLogger) { eventLogger.checkForMatchingKeyStroke( GameKeyEvent.newKeyStroke(Key.BACKSPACE, GameKeyEvent.Action.PRESS), () -> fillSelection(true)); + eventLogger.checkForMatchingKeyStroke( + GameKeyEvent.newKeyStroke(Key.ENTER, GameKeyEvent.Action.PRESS), + this::snapToTargetPixel); eventLogger.checkForMatchingKeyStroke( GameKeyEvent.newKeyStroke(Key.SPACE, GameKeyEvent.Action.PRESS), () -> { @@ -525,6 +528,9 @@ private void processCompoundKeyInputs(final InputEventLogger eventLogger) { eventLogger.checkForMatchingKeyStroke( GameKeyEvent.newKeyStroke(Key.L, GameKeyEvent.Action.PRESS), () -> DialogAssembly.setDialogToLayerSettings(getState().getLayerEditIndex())); + eventLogger.checkForMatchingKeyStroke( + GameKeyEvent.newKeyStroke(Key.F, GameKeyEvent.Action.PRESS), + () -> DialogAssembly.setDialogToFrameProperties(getState().getFrameIndex())); eventLogger.checkForMatchingKeyStroke( GameKeyEvent.newKeyStroke(Key._9, GameKeyEvent.Action.PRESS), () -> outlineSelection(Outliner.getSingleOutlineMask())); @@ -781,70 +787,86 @@ private void processSingleKeyInputs(final InputEventLogger eventLogger) { () -> renderInfo.zoomIn(targetPixel)); eventLogger.checkForMatchingKeyStroke( GameKeyEvent.newKeyStroke(Key.DOWN_ARROW, GameKeyEvent.Action.PRESS), - renderInfo::zoomOut); + () -> renderInfo.zoomOut(targetPixel)); } } // special case where shifting would constitute snap and is permitted - if (!eventLogger.isPressed(Key.CTRL) && tool instanceof MoverTool mt && + if (!eventLogger.isPressed(Key.CTRL) && tool instanceof MoverTool mt && getState().hasSelection()) { + final Selection selection = getState().getSelection(); + final int w = selection.bounds.width(), + h = selection.bounds.height(); + eventLogger.checkForMatchingKeyStroke( GameKeyEvent.newKeyStroke(Key.UP_ARROW, GameKeyEvent.Action.PRESS), - () -> mt.getMoverFunction(this).accept(new Coord2D( - 0, -1 * (mt.isSnap() ? SelectionUtils.height( - getState().getSelection()) : 1)), true)); + () -> mt.applyMove(this, + new Coord2D(0, -1 * (mt.isSnap() ? h : 1)))); eventLogger.checkForMatchingKeyStroke( GameKeyEvent.newKeyStroke(Key.DOWN_ARROW, GameKeyEvent.Action.PRESS), - () -> mt.getMoverFunction(this).accept(new Coord2D( - 0, mt.isSnap() ? SelectionUtils.height( - getState().getSelection()) : 1), true)); + () -> mt.applyMove(this, + new Coord2D(0, mt.isSnap() ? h : 1))); eventLogger.checkForMatchingKeyStroke( GameKeyEvent.newKeyStroke(Key.LEFT_ARROW, GameKeyEvent.Action.PRESS), - () -> mt.getMoverFunction(this).accept(new Coord2D( - -1 * (mt.isSnap() ? SelectionUtils.width( - getState().getSelection()) : 1), 0), true)); + () -> mt.applyMove(this, + new Coord2D(-1 * (mt.isSnap() ? w : 1), 0))); eventLogger.checkForMatchingKeyStroke( GameKeyEvent.newKeyStroke(Key.RIGHT_ARROW, GameKeyEvent.Action.PRESS), - () -> mt.getMoverFunction(this).accept(new Coord2D( - mt.isSnap() ? SelectionUtils.width( - getState().getSelection()) : 1, 0), true)); + () -> mt.applyMove(this, + new Coord2D(mt.isSnap() ? w : 1, 0))); } } - private Coord2D getMouseOffsetInWorkspace(final InputEventLogger eventLogger) { - final Coord2D - m = eventLogger.getAdjustedMousePosition(), - wp = Layout.getWorkspacePosition(); - return new Coord2D(m.x - wp.x, m.y - wp.y); + private Coord2D getMouseOffsetInWorkspace(final Coord2D mousePosition) { + final Coord2D wp = Layout.getWorkspacePosition(); + return new Coord2D(mousePosition.x - wp.x, mousePosition.y - wp.y); } private void setInWorkspaceBounds(final InputEventLogger eventLogger) { - final Coord2D workspaceM = getMouseOffsetInWorkspace(eventLogger); + final Coord2D workspaceM = getMouseOffsetInWorkspace( + eventLogger.getAdjustedMousePosition()); inWorkspaceBounds = workspaceM.x > 0 && workspaceM.x < Layout.getWorkspaceWidth() && workspaceM.y > 0 && workspaceM.y < Layout.getWorkspaceHeight(); } - private void setTargetPixel(final InputEventLogger eventLogger) { - final Coord2D workspaceM = getMouseOffsetInWorkspace(eventLogger); + public Coord2D pixelFromMousePos(final Coord2D mousePosition) { + final Coord2D workspaceM = getMouseOffsetInWorkspace(mousePosition); - if (inWorkspaceBounds) { - final int w = getState().getImageWidth(), - h = getState().getImageHeight(); - final float zoomFactor = renderInfo.getZoomFactor(); - final Coord2D render = getImageRenderPositionInWorkspace(), - bottomRight = new Coord2D(render.x + (int)(zoomFactor * w), - render.y + (int)(zoomFactor * h)); - final int targetX = (int)(((workspaceM.x - render.x) / - (double)(bottomRight.x - render.x)) * w) - - (workspaceM.x < render.x ? 1 : 0), - targetY = (int)(((workspaceM.y - render.y) / - (double)(bottomRight.y - render.y)) * h) - - (workspaceM.y < render.y ? 1 : 0); - - targetPixel = new Coord2D(targetX, targetY); - } else - targetPixel = Constants.NO_VALID_TARGET; + final int w = getState().getImageWidth(), + h = getState().getImageHeight(); + final float zoomFactor = renderInfo.getZoomFactor(); + final Coord2D render = getImageRenderPositionInWorkspace(), + bottomRight = new Coord2D(render.x + (int)(zoomFactor * w), + render.y + (int)(zoomFactor * h)); + final int targetX = (int)(((workspaceM.x - render.x) / + (double)(bottomRight.x - render.x)) * w) - + (workspaceM.x < render.x ? 1 : 0), + targetY = (int)(((workspaceM.y - render.y) / + (double)(bottomRight.y - render.y)) * h) - + (workspaceM.y < render.y ? 1 : 0); + + return new Coord2D(targetX, targetY); + } + + public Coord2D modelMousePosForPixel( + final Coord2D pixel + ) { + if (pixel.equals(Constants.NO_VALID_TARGET)) + return pixel; + + final float zoomFactor = renderInfo.getZoomFactor(); + final Coord2D wp = Layout.getWorkspacePosition(), + render = wp.displace(getImageRenderPositionInWorkspace()); + + return render.displace((int)(zoomFactor * pixel.x), + (int)(zoomFactor * pixel.y)); + } + + private void setTargetPixel(final InputEventLogger eventLogger) { + targetPixel = inWorkspaceBounds + ? pixelFromMousePos(eventLogger.getAdjustedMousePosition()) + : Constants.NO_VALID_TARGET; } private Coord2D getImageRenderPositionInWorkspace() { @@ -875,7 +897,7 @@ public void redrawCheckerboard() { for (int x = 0; x < w; x += cbx) { for (int y = 0; y < h; y += cby) { final Color c = ((x / cbx) + (y / cby)) % 2 == 0 - ? t.checkerboard1.get() : t.checkerboard2.get(); + ? t.checkerboard1 : t.checkerboard2; image.fillRectangle(c, x, y, cbx, cby); } @@ -923,8 +945,8 @@ public void redrawPixelGrid() { final int w = getState().getImageWidth(), h = getState().getImageHeight(); - for (float fZ = Constants.MIN_ZOOM; fZ <= Constants.MAX_ZOOM; fZ *= 2f) { - final int z = (int) fZ, altPx = Math.max(2, + for (final ZoomLevel zl : ZoomLevel.values()) { + final int z = (int) zl.z, altPx = Math.max(2, z / Layout.PIXEL_GRID_COLOR_ALT_DIVS); final boolean tooSmall = z == 0, @@ -976,7 +998,7 @@ public void redrawPixelGrid() { for (int y = 0; y < h; y += pgy) image.draw(horzGridLine, 0, y * z); - pixelGridMap.put(fZ, image.submit()); + pixelGridMap.put(zl, image.submit()); } } @@ -987,12 +1009,17 @@ public void snapToCenterOfImage() { getState().getImageHeight() / 2)); } + public void snapToTargetPixel() { + if (isTargetingPixelOnCanvas()) + renderInfo.setAnchor(targetPixel); + } + // copy - (not a state change unlike cut and paste) public void copy() { if (getState().hasSelection()) { SEClipboard.get().sendSelectionToClipboard(getState()); StatusUpdates.sendToClipboard(true, - SEClipboard.get().getContents().getPixels()); + getState().getSelection().size()); } else StatusUpdates.clipboardSendFailed(true); } @@ -1002,10 +1029,10 @@ private void setPixelGridAndCheckerboard() { final int w, h; if (fromSelection) { - final Set selection = getState().getSelection(); + final Selection selection = getState().getSelection(); - w = SelectionUtils.width(selection); - h = SelectionUtils.height(selection); + w = selection.bounds.width(); + h = selection.bounds.height(); } else { w = getState().getImageWidth(); h = getState().getImageHeight(); @@ -1027,23 +1054,26 @@ private void setPixelGridAndCheckerboard() { // contents to palette public void contentsToPalette(final Palette palette) { + dropContentsToLayer(false, false); + final DialogVals.Scope scope = DialogVals.getScope(); final List colors = new ArrayList<>(); final ProjectState state = getState(); final boolean includeDisabledLayers = DialogVals.isIncludeDisabledLayers(); + final Selection selection = state.hasSelection() && + !DialogVals.isIgnoreSelection() ? state.getSelection() : null; switch (scope) { - case SELECTION -> extractColorsFromSelection(colors); case LAYER_FRAME -> extractColorsFromFrame(colors, state, - state.getFrameIndex(), state.getLayerEditIndex()); + state.getFrameIndex(), state.getLayerEditIndex(), selection); case LAYER -> { final int frameCount = state.getFrameCount(); for (int i = 0; i < frameCount; i++) extractColorsFromFrame(colors, state, - i, state.getLayerEditIndex()); + i, state.getLayerEditIndex(), selection); } case FRAME -> { final int layerCount = state.getLayers().size(); @@ -1052,7 +1082,7 @@ public void contentsToPalette(final Palette palette) { if (includeDisabledLayers || state.getLayers().get(i).isEnabled()) extractColorsFromFrame(colors, state, - state.getFrameIndex(), i); + state.getFrameIndex(), i, selection); } case PROJECT -> { final int frameCount = state.getFrameCount(), @@ -1062,7 +1092,7 @@ public void contentsToPalette(final Palette palette) { for (int l = 0; l < layerCount; l++) if (includeDisabledLayers || state.getLayers().get(l).isEnabled()) - extractColorsFromFrame(colors, state, f, l); + extractColorsFromFrame(colors, state, f, l, selection); } } @@ -1072,39 +1102,17 @@ public void contentsToPalette(final Palette palette) { private void extractColorsFromFrame( final List colors, final ProjectState state, - final int frameIndex, final int layerIndex + final int frameIndex, final int layerIndex, + final Selection selection ) { final List layers = new ArrayList<>(state.getLayers()); final SELayer layer = layers.get(layerIndex); PaletteLoader.addPaletteColorsFromImage( - layer.getFrame(frameIndex), colors, null); - } - - private void extractColorsFromSelection( - final List colors - ) { - if (getState().hasSelection()) { - final int w = getState().getImageWidth(), - h = getState().getImageHeight(); - - final Set selection = getState().getSelection(); - final GameImage canvas = getState().getActiveLayerFrame(), - source = switch (getState().getSelectionMode()) { - case CONTENTS -> getState().getSelectionContents() - .getContentForCanvas(w, h); - case BOUNDS -> { - final SelectionContents contents = - new SelectionContents(canvas, selection); - yield contents.getContentForCanvas(w, h); - } - }; - - PaletteLoader.addPaletteColorsFromImage(source, colors, selection); - } + layer.getFrame(frameIndex), colors, selection); } - // rpeviewed state changes - not state changes in and of themselves + // previewed state changes - not state changes in and of themselves public ProjectState prepHSVShift() { final ProjectState state = getState(); @@ -1135,30 +1143,34 @@ public ProjectState prepColorScript(final HeadFuncNode script) { public ProjectState runColorAlgorithm( final Function internal ) { + dropContentsToLayer(false, false); + final DialogVals.Scope scope = DialogVals.getScope(); ProjectState state = getState(); final boolean includeDisabledLayers = DialogVals.isIncludeDisabledLayers(); + final Selection selection = state.hasSelection() && + !DialogVals.isIgnoreSelection() ? state.getSelection() : null; final Map map = new HashMap<>(); try { return switch (scope) { - case SELECTION -> runCAOnSelection(internal, map); + // case SELECTION -> runCAOnSelection(internal, map); case LAYER_FRAME -> runCAOnFrame(internal, map, state, - state.getFrameIndex(), state.getLayerEditIndex()); + state.getFrameIndex(), state.getLayerEditIndex(), selection); case LAYER -> { if (state.getEditingLayer().areFramesLinked()) { yield runCAOnFrame(internal, map, state, state.getFrameIndex(), - state.getLayerEditIndex()); + state.getLayerEditIndex(), selection); } else { final int frameCount = state.getFrameCount(); for (int i = 0; i < frameCount; i++) state = runCAOnFrame(internal, map, state, - i, state.getLayerEditIndex()); + i, state.getLayerEditIndex(), selection); yield state; } @@ -1170,7 +1182,7 @@ yield runCAOnFrame(internal, map, state, if (includeDisabledLayers || state.getLayers().get(i).isEnabled()) state = runCAOnFrame(internal, map, state, - state.getFrameIndex(), i); + state.getFrameIndex(), i, selection); yield state; } @@ -1185,10 +1197,11 @@ yield runCAOnFrame(internal, map, state, if (state.getLayers().get(l).areFramesLinked()) { state = runCAOnFrame(internal, map, state, - state.getFrameIndex(), l); + state.getFrameIndex(), l, selection); } else { for (int f = 0; f < frameCount; f++) - state = runCAOnFrame(internal, map, state, f, l); + state = runCAOnFrame(internal, map, + state, f, l, selection); } } @@ -1204,48 +1217,22 @@ private ProjectState runCAOnFrame( final Function internal, final Map map, final ProjectState state, - final int frameIndex, final int layerIndex + final int frameIndex, final int layerIndex, + final Selection selection ) { final List layers = new ArrayList<>(state.getLayers()); final SELayer layer = layers.get(layerIndex); final GameImage source = layer.getFrame(frameIndex), - edit = ColorMath.algo(internal, map, source); + edit = ColorMath.algo(internal, map, source, selection); - final SELayer replacement = layer.returnFrameReplaced(edit, frameIndex); + final SELayer replacement = + layer.returnFrameReplaced(edit, frameIndex); layers.set(layerIndex, replacement); return state.changeLayers(layers).changeIsCheckpoint(false); } - private ProjectState runCAOnSelection( - final Function internal, - final Map map - ) { - if (getState().hasSelection()) { - final boolean dropAndRaise = getState().getSelectionMode() == - SelectionMode.CONTENTS; - - if (dropAndRaise) - dropContentsToLayer(false, false); - - final Set selection = getState().getSelection(); - final List layers = new ArrayList<>( - getState().getLayers()); - final SELayer layer = getState().getEditingLayer(); - final int frameIndex = getState().getFrameIndex(); - - final GameImage source = layer.getFrame(frameIndex), - edit = ColorMath.algo(internal, map, source, selection); - - final SELayer replacement = layer.returnFrameReplaced(edit, frameIndex); - layers.set(getState().getLayerEditIndex(), replacement); - - return getState().changeLayers(layers); - } else - return getState(); - } - // state changes - process all actions here and feed through state manager // palettize @@ -1276,78 +1263,9 @@ public void outlineSelection(final int[] sideMask) { ToolWithMode.setGlobal(false); ToolWithMode.setMode(ToolWithMode.Mode.SINGLE); - editSelection(Outliner.outline( - getState().getSelection(), sideMask), true); - } - } - - public void resetContentOriginal() { - if (getState().hasSelection() && getState().getSelectionMode() == - SelectionMode.CONTENTS) { - final SelectionContents reset = getState() - .getSelectionContents().returnDisplaced(new Coord2D()); - - final ProjectState result = getState() - .changeSelectionContents(reset) - .changeIsCheckpoint(true); - stateManager.performAction(result, - Operation.RESET_SELECTION_CONTENTS); - } - } - - // move selection contents - public void moveSelectionContents( - final Coord2D displacement, final boolean checkpoint - ) { - if (getState().hasSelection() && getState().getSelectionMode() == - SelectionMode.CONTENTS) { - final SelectionContents moved = getState() - .getSelectionContents().returnDisplaced(displacement); - - final ProjectState result = getState() - .changeSelectionContents(moved) - .changeIsCheckpoint(checkpoint); - stateManager.performAction(result, - Operation.MOVE_SELECTION_CONTENTS); - } - } - - // stretch selection contents - public void stretchSelectionContents( - final Set initialSelection, final Coord2D change, - final MoverTool.Direction direction, final boolean checkpoint - ) { - if (getState().hasSelection() && getState().getSelectionMode() == - SelectionMode.CONTENTS) { - final SelectionContents stretched = - getState().getSelectionContents() - .returnStretched(initialSelection, change, direction); - - final ProjectState result = getState() - .changeSelectionContents(stretched) - .changeIsCheckpoint(checkpoint); - stateManager.performAction(result, - Operation.STRETCH_SELECTION_CONTENTS); - } - } - - // rotate selection contents - public void rotateSelectionContents( - final Set initialSelection, final double deltaR, - final Coord2D pivot, final boolean[] offset, final boolean checkpoint - ) { - if (getState().hasSelection() && getState().getSelectionMode() == - SelectionMode.CONTENTS) { - final SelectionContents rotated = - getState().getSelectionContents() - .returnRotated(initialSelection, - deltaR, pivot, offset); + final Selection selection = getState().getSelection(); - final ProjectState result = getState() - .changeSelectionContents(rotated) - .changeIsCheckpoint(checkpoint); - stateManager.performAction(result, - Operation.ROTATE_SELECTION_CONTENTS); + editSelection(Outliner.outline(selection, sideMask), true); } } @@ -1368,7 +1286,7 @@ public void reflectSelectionContents(final boolean horizontal) { .changeSelectionContents(reflected) .changeIsCheckpoint(!raiseAndDrop); stateManager.performAction(result, - Operation.REFLECT_SELECTION_CONTENTS); + Operation.TRANSFORM_SELECTION_CONTENTS); if (raiseAndDrop) dropContentsToLayer(true, false); @@ -1379,10 +1297,8 @@ public void reflectSelectionContents(final boolean horizontal) { public void moveSelectionBounds(final Coord2D displacement, final boolean checkpoint) { if (getState().hasSelection() && getState().getSelectionMode() == SelectionMode.BOUNDS) { - final Set selection = getState().getSelection(); - - final Set moved = selection.stream().map(s -> - s.displace(displacement)).collect(Collectors.toSet()); + final Selection moved = getState().getSelection() + .displace(displacement); final ProjectState result = getState() .changeSelectionBounds(moved) @@ -1392,42 +1308,6 @@ public void moveSelectionBounds(final Coord2D displacement, final boolean checkp } } - // stretch selection - public void stretchSelectionBounds( - final Set initialSelection, final Coord2D change, - final MoverTool.Direction direction, final boolean checkpoint - ) { - if (getState().hasSelection() && getState().getSelectionMode() == - SelectionMode.BOUNDS) { - final Set stretched = SelectionUtils - .stretchedPixels(initialSelection, change, direction); - - final ProjectState result = getState() - .changeSelectionBounds(stretched) - .changeIsCheckpoint(checkpoint); - stateManager.performAction(result, - Operation.STRETCH_SELECTION_BOUNDS); - } - } - - // rotate selection - public void rotateSelectionBounds( - final Set initialSelection, final double deltaR, - final Coord2D pivot, final boolean[] offset, final boolean checkpoint - ) { - if (getState().hasSelection() && getState().getSelectionMode() == - SelectionMode.BOUNDS) { - final Set rotated = SelectionUtils - .rotatedPixels(initialSelection, deltaR, pivot, offset); - - final ProjectState result = getState() - .changeSelectionBounds(rotated) - .changeIsCheckpoint(checkpoint); - stateManager.performAction(result, - Operation.ROTATE_SELECTION_BOUNDS); - } - } - // reflect selection public void reflectSelection(final boolean horizontal) { if (getState().hasSelection()) { @@ -1437,14 +1317,14 @@ public void reflectSelection(final boolean horizontal) { if (dropAndRaise) dropContentsToLayer(false, false); - final Set reflected = SelectionUtils - .reflectedPixels(getState().getSelection(), horizontal); + final Selection reflected = SelectionUtils.reflectedPixels( + getState().getSelection(), horizontal); final ProjectState result = getState() .changeSelectionBounds(reflected) .changeIsCheckpoint(!dropAndRaise); stateManager.performAction(result, - Operation.REFLECT_SELECTION_BOUNDS); + Operation.TRANSFORM_SELECTION_BOUNDS); if (dropAndRaise) raiseSelectionToContents(true); @@ -1460,11 +1340,11 @@ public void cropToSelection() { if (drop) dropContentsToLayer(false, false); - final Set selection = getState().getSelection(); + final Selection selection = getState().getSelection(); - final Coord2D tl = SelectionUtils.topLeft(selection), - br = SelectionUtils.bottomRight(selection); - final int w = br.x - tl.x, h = br.y - tl.y; + final Coord2D tl = selection.topLeft; + final int w = selection.bounds.width(), + h = selection.bounds.height(); final List layers = getState().getLayers().stream() .map(layer -> layer.returnPadded( @@ -1484,26 +1364,28 @@ public void cropToSelection() { public void cut() { if (getState().hasSelection()) { SEClipboard.get().sendSelectionToClipboard(getState()); + final int pixelCount = getState().getSelection().size(); deleteSelectionContents(true); - StatusUpdates.sendToClipboard(false, - SEClipboard.get().getContents().getPixels()); + StatusUpdates.sendToClipboard(false, pixelCount); } else StatusUpdates.clipboardSendFailed(false); } // paste public void paste(final boolean newLayer) { - if (SEClipboard.get().hasContents()) { + if (SEClipboard.get().hasContent()) { if (getState().hasSelectionContents()) dropContentsToLayer(false, true); - final SelectionContents toPaste = SEClipboard.get().getContents(); + final SelectionContents toPaste = SEClipboard.get().getContent(); if (newLayer) addLayer(); - final Coord2D tl = SelectionUtils.topLeft(toPaste.getPixels()), - br = SelectionUtils.bottomRight(toPaste.getPixels()); + final Selection selection = toPaste.getSelection(); + final Coord2D tl = selection.topLeft, + br = tl.displace(selection.bounds.width(), + selection.bounds.height()); final int w = getState().getImageWidth(), h = getState().getImageHeight(); final int x, y; @@ -1530,25 +1412,17 @@ public void raiseSelectionToContents(final boolean checkpoint) { final int w = getState().getImageWidth(), h = getState().getImageHeight(); - Set selection = new HashSet<>(getState().getSelection()); + Selection selection = getState().getSelection(); - if (selection.isEmpty()) { - selection = new HashSet<>(); - - for (int x = 0; x < w; x++) - for (int y = 0; y < h; y++) - selection.add(new Coord2D(x, y)); - } + if (!selection.hasSelection()) + selection = Selection.allInBounds(w, h); final GameImage canvas = getState().getActiveLayerFrame(); final SelectionContents selectionContents = - new SelectionContents(canvas, selection); + SelectionContents.make(canvas, selection); final boolean[][] eraseMask = new boolean[w][h]; - - selection.stream().filter(s -> s.x >= 0 && s.y >= 0 && - s.x < w && s.y < h).forEach(s -> eraseMask[s.x][s.y] = true); - + selection.pixelAlgorithm(w, h, (x, y) -> eraseMask[x][y] = true); erase(eraseMask, false); final ProjectState result = getState() @@ -1565,11 +1439,12 @@ public void dropContentsToLayer(final boolean checkpoint, final boolean deselect final SelectionContents contents = getState().getSelectionContents(); - stampImage(contents.getContentForCanvas(w, h), contents.getPixels()); + stampImage(contents.getContentForCanvas(w, h), + contents.getSelection()); final ProjectState result = getState() - .changeSelectionBounds(deselect ? new HashSet<>() - : new HashSet<>(contents.getPixels())) + .changeSelectionBounds(deselect + ? Selection.EMPTY : contents.getSelection()) .changeIsCheckpoint(checkpoint); stateManager.performAction(result, Operation.DROP); } @@ -1578,23 +1453,19 @@ public void dropContentsToLayer(final boolean checkpoint, final boolean deselect // delete selection contents public void deleteSelectionContents(final boolean deselect) { if (getState().hasSelection()) { - final Set selection = getState().getSelection(); + final Selection selection = getState().getSelection(); - if (!selection.isEmpty()) { + if (selection.hasSelection()) { final int w = getState().getImageWidth(), h = getState().getImageHeight(); final boolean[][] eraseMask = new boolean[w][h]; - selection.forEach(s -> { - if (s.x >= 0 && s.x < w && s.y >= 0 && s.y < h) - eraseMask[s.x][s.y] = true; - }); - + selection.pixelAlgorithm(w, h, (x, y) -> eraseMask[x][y] = true); erase(eraseMask, false); } stateManager.performAction(getState().changeSelectionBounds( - new HashSet<>(deselect ? Set.of() : selection)), + deselect ? Selection.EMPTY : selection), Operation.DELETE_SELECTION_CONTENTS); } } @@ -1608,18 +1479,15 @@ public void fillSelection(final boolean secondary) { if (dropAndRaise) dropContentsToLayer(false, false); - final Set selection = getState().getSelection(); - final int w = getState().getImageWidth(), h = getState().getImageHeight(); final GameImage edit = new GameImage(w, h); final int c = (secondary ? StippleEffect.get().getSecondary() : StippleEffect.get().getPrimary()).getRGB(); - selection.stream().filter(s -> s.x >= 0 && s.x < w && - s.y >= 0 && s.y < h) - .forEach(s -> edit.setRGB(s.x, s.y, c)); + final Selection selection = getState().getSelection(); + selection.pixelAlgorithm(w, h, (x, y) -> edit.setRGB(x, y, c)); stampImage(edit, selection); if (dropAndRaise) @@ -1636,7 +1504,7 @@ public void deselect(final boolean checkpoint) { dropContentsToLayer(checkpoint, true); else { final ProjectState result = getState().changeSelectionBounds( - new HashSet<>()).changeIsCheckpoint(checkpoint); + Selection.EMPTY).changeIsCheckpoint(checkpoint); stateManager.performAction(result, Operation.DESELECT); } } @@ -1653,11 +1521,7 @@ public void selectAll() { final int w = getState().getImageWidth(), h = getState().getImageHeight(); - final Set selection = new HashSet<>(); - - for (int x = 0; x < w; x++) - for (int y = 0; y < h; y++) - selection.add(new Coord2D(x, y)); + final Selection selection = Selection.allInBounds(w, h); final ProjectState result = getState() .changeSelectionBounds(selection) @@ -1679,16 +1543,18 @@ public void invertSelection() { final int w = getState().getImageWidth(), h = getState().getImageHeight(); - final Set willBe = new HashSet<>(), - was = getState().getSelection(); + final Selection was = getState().getSelection(); + final boolean[][] invertedMatrix = new boolean[w][h]; for (int x = 0; x < w; x++) for (int y = 0; y < h; y++) - if (!was.contains(new Coord2D(x, y))) - willBe.add(new Coord2D(x, y)); + if (!was.selected(x, y)) + invertedMatrix[x][y] = true; + + final Selection selection = Selection.of(invertedMatrix); final ProjectState result = getState() - .changeSelectionBounds(willBe) + .changeSelectionBounds(selection) .changeIsCheckpoint(!dropAndRaise); stateManager.performAction(result, Operation.SELECT); @@ -1697,26 +1563,26 @@ public void invertSelection() { } // edit selection - public void editSelection(final Set edit, final boolean checkpoint) { + public void editSelection(final Selection edit, final boolean checkpoint) { final boolean drop = getState().hasSelection() && getState().getSelectionMode() == SelectionMode.CONTENTS; if (drop) dropContentsToLayer(false, false); - final Set selection = new HashSet<>(); final ToolWithMode.Mode mode = ToolWithMode.getMode(); - if (mode == ToolWithMode.Mode.ADDITIVE || mode == ToolWithMode.Mode.SUBTRACTIVE) - selection.addAll(getState().getSelection()); + final Selection initial, selection; - if (mode == ToolWithMode.Mode.SUBTRACTIVE) - selection.removeAll(edit); - else - selection.addAll(edit); + initial = mode.inheritSelection() + ? getState().getSelection() : Selection.EMPTY; + selection = mode == ToolWithMode.Mode.SUBTRACTIVE + ? Selection.difference(initial, edit) + : Selection.union(initial, edit); - final ProjectState result = getState().changeSelectionBounds( - selection).changeIsCheckpoint(checkpoint); + final ProjectState result = getState() + .changeSelectionBounds(selection) + .changeIsCheckpoint(checkpoint); stateManager.performAction(result, Operation.SELECT); } @@ -1742,7 +1608,7 @@ public void pad( .map(layer -> layer.returnPadded(left, top, w, h)).toList(); final ProjectState result = getState().resize(w, h, layers) - .changeSelectionBounds(new HashSet<>()); + .changeSelectionBounds(Selection.EMPTY); stateManager.performAction(result, Operation.PAD); snapToCenterOfImage(); @@ -1765,7 +1631,7 @@ public void resize(final int rw, final int rh) { .map(layer -> layer.returnResized(rw, rh)).toList(); final ProjectState result = getState().resize(rw, rh, layers) - .changeSelectionBounds(new HashSet<>()); + .changeSelectionBounds(Selection.EMPTY); stateManager.performAction(result, Operation.RESIZE); snapToCenterOfImage(); @@ -1783,7 +1649,7 @@ public void stitch() { frameHeight, frameCount)).toList(); final ProjectState result = getState().stitch(w, h, - layers).changeSelectionBounds(new HashSet<>()); + layers).changeSelectionBounds(Selection.EMPTY); stateManager.performAction(result, Operation.STITCH); snapToCenterOfImage(); @@ -1799,7 +1665,7 @@ public void split() { final ProjectState result = getState().split( DialogVals.getFrameWidth(), DialogVals.getFrameHeight(), - layers, frameCount).changeSelectionBounds(new HashSet<>()); + layers, frameCount).changeSelectionBounds(Selection.EMPTY); stateManager.performAction(result, Operation.SPLIT); snapToCenterOfImage(); @@ -1817,9 +1683,9 @@ public void setLayerFromScript( stateManager.performAction(result, Operation.EDIT_IMAGE); } - public void stampImage(final GameImage edit, final Set pixels) { + public void stampImage(final GameImage edit, final Selection selection) { editImage(f -> getState().getEditingLayer() - .returnStamped(edit, pixels, f), false); + .returnStamped(edit, selection, f), false); } public void paintOverImage(final GameImage edit) { @@ -1850,6 +1716,19 @@ private void editImage( } // FRAME MANIPULATION + // change frame duration + public void changeFrameDuration( + final double duration, final int frameIndex + ) { + final List frameDurations = + new ArrayList<>(getState().getFrameDurations()); + frameDurations.set(frameIndex, duration); + + final ProjectState result = + getState().changeFrameDurations(frameDurations); + stateManager.performAction(result, Operation.CHANGE_FRAME_DURATION); + } + // move frame forward public void moveFrameForward() { final int frameIndex = getState().getFrameIndex(), @@ -1862,8 +1741,13 @@ public void moveFrameForward() { layers.replaceAll(l -> l.returnFrameMovedForward(frameIndex)); - final ProjectState result = getState().changeFrames(layers, - toIndex, frameCount); + final List frameDurations = + new ArrayList<>(getState().getFrameDurations()); + final double movedDuration = frameDurations.remove(frameIndex); + frameDurations.add(toIndex, movedDuration); + + final ProjectState result = getState().changeFrames( + layers, toIndex, frameCount, frameDurations); stateManager.performAction(result, Operation.MOVE_FRAME_FORWARD); StatusUpdates.movedFrame(frameIndex, toIndex, frameCount); } else if (!Layout.isFramesPanelShowing()) { @@ -1883,8 +1767,13 @@ public void moveFrameBack() { layers.replaceAll(l -> l.returnFrameMovedBack(frameIndex)); - final ProjectState result = getState().changeFrames(layers, - toIndex, frameCount); + final List frameDurations = + new ArrayList<>(getState().getFrameDurations()); + final double movedDuration = frameDurations.remove(frameIndex); + frameDurations.add(toIndex, movedDuration); + + final ProjectState result = getState().changeFrames( + layers, toIndex, frameCount, frameDurations); stateManager.performAction(result, Operation.MOVE_FRAME_BACK); StatusUpdates.movedFrame(frameIndex, toIndex, frameCount); } else if (!Layout.isFramesPanelShowing()) { @@ -1904,8 +1793,12 @@ public void addFrame() { frameCount = getState().getFrameCount() + 1; layers.replaceAll(l -> l.returnAddedFrame(addIndex, w, h)); - final ProjectState result = getState().changeFrames(layers, - addIndex, frameCount); + final List frameDurations = + new ArrayList<>(getState().getFrameDurations()); + frameDurations.add(addIndex, Constants.DEFAULT_FRAME_DURATION); + + final ProjectState result = getState().changeFrames( + layers, addIndex, frameCount, frameDurations); stateManager.performAction(result, Operation.ADD_FRAME); if (!Layout.isFramesPanelShowing()) @@ -1926,8 +1819,12 @@ public void duplicateFrame() { layers.replaceAll(l -> l.returnDuplicatedFrame(frameIndex)); + final List frameDurations = + new ArrayList<>(getState().getFrameDurations()); + frameDurations.add(frameIndex + 1, frameDurations.get(frameIndex)); + final ProjectState result = getState().changeFrames( - layers, frameIndex + 1, frameCount); + layers, frameIndex + 1, frameCount, frameDurations); stateManager.performAction(result, Operation.DUPLICATE_FRAME); if (!Layout.isFramesPanelShowing()) @@ -1948,8 +1845,12 @@ public void removeFrame() { layers.replaceAll(l -> l.returnRemovedFrame(frameIndex)); - final ProjectState result = getState().changeFrames(layers, - frameIndex - 1, frameCount); + final List frameDurations = + new ArrayList<>(getState().getFrameDurations()); + frameDurations.remove(frameIndex); + + final ProjectState result = getState().changeFrames( + layers, frameIndex - 1, frameCount, frameDurations); stateManager.performAction(result, Operation.REMOVE_FRAME); if (!Layout.isFramesPanelShowing()) @@ -2314,16 +2215,17 @@ public String getImageSizeText() { } public String getSelectionText() { - final Set selection = getState().getSelection(); - final Coord2D tl = SelectionUtils.topLeft(selection), - br = SelectionUtils.bottomRight(selection), - bounds = SelectionUtils.bounds(selection); + final Selection selection = getState().getSelection(); + final Bounds2D bounds = selection.bounds; + final Coord2D tl = selection.topLeft, + br = selection.topLeft.displace( + bounds.width(), bounds.height()); final boolean multiple = selection.size() > 1; - return selection.isEmpty() ? "No selection" : "Selection: " + + return !selection.hasSelection() ? "No selection" : "Selection: " + selection.size() + "px " + (multiple ? "from " : "at ") + tl + - (multiple ? (" to " + br + "; " + bounds.x + "x" + bounds.y + - " bounding box") : ""); + (multiple ? (" to " + br + "; " + bounds.width() + "x" + + bounds.height() + " bounding box") : ""); } public ProjectState getState() { diff --git a/src/com/jordanbunke/stipple_effect/project/ZoomLevel.java b/src/com/jordanbunke/stipple_effect/project/ZoomLevel.java new file mode 100644 index 00000000..923f5ad7 --- /dev/null +++ b/src/com/jordanbunke/stipple_effect/project/ZoomLevel.java @@ -0,0 +1,82 @@ +package com.jordanbunke.stipple_effect.project; + +public enum ZoomLevel { + MIN(0.0625f), + MINUS_5(0.125f), MINUS_4(0.25f), + MINUS_3(0.375f), MINUS_2(0.5f), MINUS_1(0.75f), + NONE(1f), + PLUS_1(2f), PLUS_2(3f), PLUS_3(4f), + PLUS_4(5f), PLUS_5(6f), PLUS_6(8f), + PLUS_7(10f), PLUS_8(12f), PLUS_9(16f), + PLUS_10(24f), PLUS_11(32f), PLUS_12(48f), + MAX(64f); + + public final float z; + + ZoomLevel(final float z) { + this.z = z; + } + + public static ZoomLevel fromZ(final float z) { + for (final ZoomLevel zl : ZoomLevel.values()) + if (zl.z == z) + return zl; + + return NONE; + } + + public ZoomLevel out() { + return switch (this) { + case MAX -> PLUS_12; + case PLUS_12 -> PLUS_11; + case PLUS_11 -> PLUS_10; + case PLUS_10 -> PLUS_9; + case PLUS_9 -> PLUS_8; + case PLUS_8 -> PLUS_7; + case PLUS_7 -> PLUS_6; + case PLUS_6 -> PLUS_5; + case PLUS_5 -> PLUS_4; + case PLUS_4 -> PLUS_3; + case PLUS_3 -> PLUS_2; + case PLUS_2 -> PLUS_1; + case PLUS_1 -> NONE; + case NONE -> MINUS_1; + case MINUS_1 -> MINUS_2; + case MINUS_2 -> MINUS_3; + case MINUS_3 -> MINUS_4; + case MINUS_4 -> MINUS_5; + default -> MIN; + }; + } + + public ZoomLevel in() { + return switch (this) { + case PLUS_11 -> PLUS_12; + case PLUS_10 -> PLUS_11; + case PLUS_9 -> PLUS_10; + case PLUS_8 -> PLUS_9; + case PLUS_7 -> PLUS_8; + case PLUS_6 -> PLUS_7; + case PLUS_5 -> PLUS_6; + case PLUS_4 -> PLUS_5; + case PLUS_3 -> PLUS_4; + case PLUS_2 -> PLUS_3; + case PLUS_1 -> PLUS_2; + case NONE -> PLUS_1; + case MINUS_1 -> NONE; + case MINUS_2 -> MINUS_1; + case MINUS_3 -> MINUS_2; + case MINUS_4 -> MINUS_3; + case MINUS_5 -> MINUS_4; + case MIN -> MINUS_5; + default -> MAX; + }; + } + + @Override + public String toString() { + return z >= MINUS_2.z + ? (int)(z * 100) + "%" + : (z * 100) + "%"; + } +} diff --git a/src/com/jordanbunke/stipple_effect/scripting/ext_ast_nodes/expression/ScopedExpressionNode.java b/src/com/jordanbunke/stipple_effect/scripting/ext_ast_nodes/expression/ScopedExpressionNode.java index cbcf001b..38d7b118 100644 --- a/src/com/jordanbunke/stipple_effect/scripting/ext_ast_nodes/expression/ScopedExpressionNode.java +++ b/src/com/jordanbunke/stipple_effect/scripting/ext_ast_nodes/expression/ScopedExpressionNode.java @@ -8,7 +8,7 @@ import com.jordanbunke.stipple_effect.scripting.util.Scope; public abstract class ScopedExpressionNode extends ExtExpressionNode { - protected Scope scope; + protected final Scope scope; public ScopedExpressionNode( final TextPosition position, diff --git a/src/com/jordanbunke/stipple_effect/scripting/ext_ast_nodes/expression/global/FillNode.java b/src/com/jordanbunke/stipple_effect/scripting/ext_ast_nodes/expression/global/FillNode.java index c93fbb94..8c205ca6 100644 --- a/src/com/jordanbunke/stipple_effect/scripting/ext_ast_nodes/expression/global/FillNode.java +++ b/src/com/jordanbunke/stipple_effect/scripting/ext_ast_nodes/expression/global/FillNode.java @@ -6,10 +6,9 @@ import com.jordanbunke.delta_time.scripting.ast.nodes.types.TypeNode; import com.jordanbunke.delta_time.scripting.ast.symbol_table.SymbolTable; import com.jordanbunke.delta_time.scripting.util.TextPosition; -import com.jordanbunke.delta_time.utility.math.Coord2D; +import com.jordanbunke.stipple_effect.selection.Selection; import java.awt.*; -import java.util.Set; public final class FillNode extends SearchNode { public static final String NAME = "fill"; @@ -31,11 +30,12 @@ public GameImage evaluate(final SymbolTable symbolTable) { final double tol = (double) vs[4]; final boolean global = (boolean) vs[5], diag = (boolean) vs[6]; - final Set pixels = search(img, x, y, tol, global, diag); + final Selection selection = search(img, x, y, tol, global, diag); final GameImage filled = new GameImage(img); + final int w = filled.getWidth(), h = filled.getHeight(); - for (Coord2D pixel : pixels) - filled.setRGB(pixel.x, pixel.y, color.getRGB()); + selection.pixelAlgorithm(w, h, + (px, py) -> filled.setRGB(px, py, color.getRGB())); return filled; } diff --git a/src/com/jordanbunke/stipple_effect/scripting/ext_ast_nodes/expression/global/FillSelectionNode.java b/src/com/jordanbunke/stipple_effect/scripting/ext_ast_nodes/expression/global/FillSelectionNode.java index c1943c15..dcb55647 100644 --- a/src/com/jordanbunke/stipple_effect/scripting/ext_ast_nodes/expression/global/FillSelectionNode.java +++ b/src/com/jordanbunke/stipple_effect/scripting/ext_ast_nodes/expression/global/FillSelectionNode.java @@ -6,12 +6,11 @@ import com.jordanbunke.delta_time.scripting.ast.nodes.types.TypeNode; import com.jordanbunke.delta_time.scripting.ast.symbol_table.SymbolTable; import com.jordanbunke.delta_time.scripting.util.TextPosition; -import com.jordanbunke.delta_time.utility.math.Coord2D; import com.jordanbunke.stipple_effect.StippleEffect; import com.jordanbunke.stipple_effect.scripting.util.ScriptSelectionUtils; +import com.jordanbunke.stipple_effect.selection.Selection; import java.awt.*; -import java.util.Set; public class FillSelectionNode extends GlobalExpressionNode { public static final String NAME = "fill_selection"; @@ -48,13 +47,13 @@ public GameImage evaluate(final SymbolTable symbolTable) { final GameImage img = (GameImage) vs[0], res = new GameImage(img.getWidth(), img.getHeight()); final Color c = (Color) vs[1]; - final Set pixels = systemSelection + final Selection selection = systemSelection ? StippleEffect.get().getContext().getState().getSelection() : ScriptSelectionUtils.convertSelection( (ScriptSet) vs[2], img.getWidth(), img.getHeight()); - for (Coord2D pixel : pixels) - res.setRGB(pixel.x, pixel.y, c.getRGB()); + selection.unboundedPixelAlgorithm( + (x, y) -> res.setRGB(x, y, c.getRGB())); return res; } diff --git a/src/com/jordanbunke/stipple_effect/scripting/ext_ast_nodes/expression/global/OutlineNode.java b/src/com/jordanbunke/stipple_effect/scripting/ext_ast_nodes/expression/global/OutlineNode.java index b0964970..1df68791 100644 --- a/src/com/jordanbunke/stipple_effect/scripting/ext_ast_nodes/expression/global/OutlineNode.java +++ b/src/com/jordanbunke/stipple_effect/scripting/ext_ast_nodes/expression/global/OutlineNode.java @@ -7,10 +7,11 @@ import com.jordanbunke.delta_time.scripting.ast.nodes.types.TypeNode; import com.jordanbunke.delta_time.scripting.ast.symbol_table.SymbolTable; import com.jordanbunke.delta_time.scripting.util.TextPosition; -import com.jordanbunke.delta_time.utility.math.Coord2D; import com.jordanbunke.stipple_effect.scripting.util.ScriptSelectionUtils; import com.jordanbunke.stipple_effect.selection.Outliner; +import com.jordanbunke.stipple_effect.selection.Selection; +import java.util.HashSet; import java.util.Set; public final class OutlineNode extends GlobalExpressionNode { @@ -27,7 +28,7 @@ public OutlineNode( @Override public ScriptSet evaluate(final SymbolTable symbolTable) { final Object[] vs = arguments.getValues(symbolTable); - final Set selection = + final Selection selection = ScriptSelectionUtils.convertSelection((ScriptSet) vs[0]); final int[] sideMask = ((ScriptArray) vs[1]).stream() .mapToInt(s -> (int) s).toArray(); @@ -36,8 +37,12 @@ public ScriptSet evaluate(final SymbolTable symbolTable) { sideMask, arguments.args()[1])) return null; - return new ScriptSet(Outliner.outline(selection, sideMask) - .stream().map(c -> ScriptArray.of(c.x, c.y))); + final Selection outlined = Outliner.outline(selection, sideMask); + final Set pixels = new HashSet<>(); + outlined.unboundedPixelAlgorithm( + (x, y) -> pixels.add(ScriptArray.of(x, y))); + + return new ScriptSet(pixels.stream().map(c -> c)); } @Override diff --git a/src/com/jordanbunke/stipple_effect/scripting/ext_ast_nodes/expression/global/PresetOutlineNode.java b/src/com/jordanbunke/stipple_effect/scripting/ext_ast_nodes/expression/global/PresetOutlineNode.java index 75d400ec..c25be581 100644 --- a/src/com/jordanbunke/stipple_effect/scripting/ext_ast_nodes/expression/global/PresetOutlineNode.java +++ b/src/com/jordanbunke/stipple_effect/scripting/ext_ast_nodes/expression/global/PresetOutlineNode.java @@ -8,11 +8,12 @@ import com.jordanbunke.delta_time.scripting.ast.symbol_table.SymbolTable; import com.jordanbunke.delta_time.scripting.util.ScriptErrorLog; import com.jordanbunke.delta_time.scripting.util.TextPosition; -import com.jordanbunke.delta_time.utility.math.Coord2D; import com.jordanbunke.stipple_effect.scripting.util.ScriptSelectionUtils; import com.jordanbunke.stipple_effect.selection.Outliner; +import com.jordanbunke.stipple_effect.selection.Selection; import com.jordanbunke.stipple_effect.utility.Constants; +import java.util.HashSet; import java.util.Set; public final class PresetOutlineNode extends GlobalExpressionNode { @@ -47,7 +48,7 @@ public static PresetOutlineNode dbl( @Override public Object evaluate(final SymbolTable symbolTable) { final Object[] vs = arguments.getValues(symbolTable); - final Set selection = + final Selection selection = ScriptSelectionUtils.convertSelection((ScriptSet) vs[0]); final int side = (int) vs[1], MAX = Constants.MAX_OUTLINE_PX; @@ -63,8 +64,13 @@ public Object evaluate(final SymbolTable symbolTable) { final int[] sideMask = new int[] { side, side, side, side, side, side, side, side }; - return new ScriptSet(Outliner.outline(selection, sideMask) - .stream().map(c -> ScriptArray.of(c.x, c.y))); + final Selection outlined = Outliner.outline(selection, sideMask); + final Set pixels = new HashSet<>(); + + outlined.unboundedPixelAlgorithm( + (x, y) -> pixels.add(ScriptArray.of(x, y))); + + return new ScriptSet(pixels.stream().map(c -> c)); } @Override diff --git a/src/com/jordanbunke/stipple_effect/scripting/ext_ast_nodes/expression/global/SearchNode.java b/src/com/jordanbunke/stipple_effect/scripting/ext_ast_nodes/expression/global/SearchNode.java index bbe302be..72a01577 100644 --- a/src/com/jordanbunke/stipple_effect/scripting/ext_ast_nodes/expression/global/SearchNode.java +++ b/src/com/jordanbunke/stipple_effect/scripting/ext_ast_nodes/expression/global/SearchNode.java @@ -5,11 +5,9 @@ import com.jordanbunke.delta_time.scripting.ast.nodes.types.TypeNode; import com.jordanbunke.delta_time.scripting.util.TextPosition; import com.jordanbunke.delta_time.utility.math.Coord2D; +import com.jordanbunke.stipple_effect.selection.Selection; import com.jordanbunke.stipple_effect.tools.ToolThatSearches; -import java.util.HashSet; -import java.util.Set; - public abstract class SearchNode extends GlobalExpressionNode { protected SearchNode( final TextPosition position, final ExpressionNode[] args, @@ -18,12 +16,12 @@ protected SearchNode( super(position, args, expectedTypes); } - protected final Set search( + protected final Selection search( final GameImage image, final int x, final int y, final double tol, final boolean global, final boolean diag ) { if (x < 0 || x >= image.getWidth() || y < 0 || y >= image.getHeight()) - return new HashSet<>(); + return Selection.EMPTY; final double tolBef = ToolThatSearches.getTolerance(); final boolean globalBef = ToolThatSearches.isGlobal(), @@ -33,13 +31,12 @@ protected final Set search( ToolThatSearches.setSearchDiag(diag); ToolThatSearches.setTolerance(tol); - final Set pixels = - ToolThatSearches.search(image, new Coord2D(x, y)); + final Selection res = ToolThatSearches.search(image, new Coord2D(x, y)); ToolThatSearches.setGlobal(globalBef); ToolThatSearches.setSearchDiag(diagBef); ToolThatSearches.setTolerance(tolBef); - return pixels; + return res; } } diff --git a/src/com/jordanbunke/stipple_effect/scripting/ext_ast_nodes/expression/global/WandNode.java b/src/com/jordanbunke/stipple_effect/scripting/ext_ast_nodes/expression/global/WandNode.java index b78f4ec8..882bc785 100644 --- a/src/com/jordanbunke/stipple_effect/scripting/ext_ast_nodes/expression/global/WandNode.java +++ b/src/com/jordanbunke/stipple_effect/scripting/ext_ast_nodes/expression/global/WandNode.java @@ -8,8 +8,9 @@ import com.jordanbunke.delta_time.scripting.ast.nodes.types.TypeNode; import com.jordanbunke.delta_time.scripting.ast.symbol_table.SymbolTable; import com.jordanbunke.delta_time.scripting.util.TextPosition; -import com.jordanbunke.delta_time.utility.math.Coord2D; +import com.jordanbunke.stipple_effect.selection.Selection; +import java.util.HashSet; import java.util.Set; public final class WandNode extends SearchNode { @@ -31,10 +32,13 @@ public ScriptSet evaluate(final SymbolTable symbolTable) { final double tol = (double) vs[3]; final boolean global = (boolean) vs[4], diag = (boolean) vs[5]; - final Set pixels = search(img, x, y, tol, global, diag); + final Selection selection = search(img, x, y, tol, global, diag); - return new ScriptSet(pixels.stream() - .map(c -> ScriptArray.of(c.x, c.y))); + final Set pixels = new HashSet<>(); + selection.unboundedPixelAlgorithm( + (px, py) -> pixels.add(ScriptArray.of(px, py))); + + return new ScriptSet(pixels.stream().map(c -> c)); } @Override diff --git a/src/com/jordanbunke/stipple_effect/scripting/ext_ast_nodes/expression/layer/GetFrameNode.java b/src/com/jordanbunke/stipple_effect/scripting/ext_ast_nodes/expression/layer/GetFrameNode.java index 8c9dc362..3eaa0697 100644 --- a/src/com/jordanbunke/stipple_effect/scripting/ext_ast_nodes/expression/layer/GetFrameNode.java +++ b/src/com/jordanbunke/stipple_effect/scripting/ext_ast_nodes/expression/layer/GetFrameNode.java @@ -34,7 +34,7 @@ else if (frameIndex < 0) ScriptErrorLog.fireError(ScriptErrorLog.Message.CUSTOM_RT, arguments.args()[0].getPosition(), "The frame index (" + frameIndex + ") is negative"); - else if (frameIndex >= layer.project().getState().getFrameCount()) + else if (frameIndex >= fc) ScriptErrorLog.fireError(ScriptErrorLog.Message.CUSTOM_RT, arguments.args()[0].getPosition(), "The frame index (" + frameIndex + ") is " + diff --git a/src/com/jordanbunke/stipple_effect/scripting/ext_ast_nodes/expression/project/GetFrameDurationNode.java b/src/com/jordanbunke/stipple_effect/scripting/ext_ast_nodes/expression/project/GetFrameDurationNode.java new file mode 100644 index 00000000..8a143501 --- /dev/null +++ b/src/com/jordanbunke/stipple_effect/scripting/ext_ast_nodes/expression/project/GetFrameDurationNode.java @@ -0,0 +1,51 @@ +package com.jordanbunke.stipple_effect.scripting.ext_ast_nodes.expression.project; + +import com.jordanbunke.delta_time.scripting.ast.nodes.expression.ExpressionNode; +import com.jordanbunke.delta_time.scripting.ast.nodes.types.TypeNode; +import com.jordanbunke.delta_time.scripting.ast.symbol_table.SymbolTable; +import com.jordanbunke.delta_time.scripting.util.ScriptErrorLog; +import com.jordanbunke.delta_time.scripting.util.TextPosition; +import com.jordanbunke.stipple_effect.project.SEContext; + +public class GetFrameDurationNode extends ProjectExpressionNode { + public static final String NAME = "get_frame_duration"; + + public GetFrameDurationNode( + final TextPosition position, + final ExpressionNode scope, final ExpressionNode[] args + ) { + super(position, scope, args); + } + + @Override + public Double evaluate(final SymbolTable symbolTable) { + final SEContext project = getProject(symbolTable); + final int frameIndex = (int) arguments.getValues(symbolTable)[0], + fc = project.getState().getFrameCount(); + + if (frameIndex < 0) + ScriptErrorLog.fireError(ScriptErrorLog.Message.CUSTOM_RT, + arguments.args()[0].getPosition(), + "The frame index (" + frameIndex + ") is negative"); + else if (frameIndex >= fc) + ScriptErrorLog.fireError(ScriptErrorLog.Message.CUSTOM_RT, + arguments.args()[0].getPosition(), + "The frame index (" + frameIndex + ") is " + + (frameIndex == fc ? "greater than " : "equal to ") + + "the frame count (" + fc + ")"); + else + return project.getState().getFrameDurations().get(frameIndex); + + return null; + } + + @Override + public TypeNode getType(final SymbolTable symbolTable) { + return TypeNode.getFloat(); + } + + @Override + protected String callName() { + return NAME; + } +} diff --git a/src/com/jordanbunke/stipple_effect/scripting/ext_ast_nodes/expression/project/GetFrameDurationsNode.java b/src/com/jordanbunke/stipple_effect/scripting/ext_ast_nodes/expression/project/GetFrameDurationsNode.java new file mode 100644 index 00000000..590f4cd0 --- /dev/null +++ b/src/com/jordanbunke/stipple_effect/scripting/ext_ast_nodes/expression/project/GetFrameDurationsNode.java @@ -0,0 +1,34 @@ +package com.jordanbunke.stipple_effect.scripting.ext_ast_nodes.expression.project; + +import com.jordanbunke.delta_time.scripting.ast.collection.ScriptArray; +import com.jordanbunke.delta_time.scripting.ast.nodes.expression.ExpressionNode; +import com.jordanbunke.delta_time.scripting.ast.nodes.types.TypeNode; +import com.jordanbunke.delta_time.scripting.ast.symbol_table.SymbolTable; +import com.jordanbunke.delta_time.scripting.util.TextPosition; + +public class GetFrameDurationsNode extends ProjectExpressionNode { + public static final String NAME = "get_frame_durations"; + + public GetFrameDurationsNode( + final TextPosition position, + final ExpressionNode scope, final ExpressionNode[] args + ) { + super(position, scope, args); + } + + @Override + public ScriptArray evaluate(final SymbolTable symbolTable) { + return new ScriptArray(getProject(symbolTable).getState() + .getFrameDurations().stream().map(c -> c)); + } + + @Override + public TypeNode getType(final SymbolTable symbolTable) { + return TypeNode.arrayOf(TypeNode.getFloat()); + } + + @Override + protected String callName() { + return NAME; + } +} diff --git a/src/com/jordanbunke/stipple_effect/scripting/ext_ast_nodes/expression/project/IsSelectedNode.java b/src/com/jordanbunke/stipple_effect/scripting/ext_ast_nodes/expression/project/IsSelectedNode.java index a9d9b3ff..cd76916f 100644 --- a/src/com/jordanbunke/stipple_effect/scripting/ext_ast_nodes/expression/project/IsSelectedNode.java +++ b/src/com/jordanbunke/stipple_effect/scripting/ext_ast_nodes/expression/project/IsSelectedNode.java @@ -5,7 +5,6 @@ import com.jordanbunke.delta_time.scripting.ast.nodes.types.TypeNode; import com.jordanbunke.delta_time.scripting.ast.symbol_table.SymbolTable; import com.jordanbunke.delta_time.scripting.util.TextPosition; -import com.jordanbunke.delta_time.utility.math.Coord2D; import com.jordanbunke.stipple_effect.project.SEContext; public final class IsSelectedNode extends ProjectExpressionNode { @@ -25,7 +24,7 @@ public Boolean evaluate(final SymbolTable symbolTable) { final SEContext project = getProject(symbolTable); final int x = (int) vs[0], y = (int) vs[1]; - return project.getState().getSelection().contains(new Coord2D(x, y)); + return project.getState().getSelection().selected(x, y); } @Override diff --git a/src/com/jordanbunke/stipple_effect/scripting/ext_ast_nodes/expression/project/SelectionGetter.java b/src/com/jordanbunke/stipple_effect/scripting/ext_ast_nodes/expression/project/SelectionGetter.java index 8da4a3db..b047028c 100644 --- a/src/com/jordanbunke/stipple_effect/scripting/ext_ast_nodes/expression/project/SelectionGetter.java +++ b/src/com/jordanbunke/stipple_effect/scripting/ext_ast_nodes/expression/project/SelectionGetter.java @@ -8,6 +8,9 @@ import com.jordanbunke.delta_time.scripting.util.TextPosition; import com.jordanbunke.stipple_effect.project.SEContext; +import java.util.HashSet; +import java.util.Set; + public final class SelectionGetter extends ProjectExpressionNode { public static final String GET = "get_selection", HAS = "has_selection"; @@ -40,9 +43,11 @@ public static SelectionGetter newHas( public Object evaluate(final SymbolTable symbolTable) { final SEContext project = getProject(symbolTable); - return get - ? new ScriptSet(project.getState().getSelection().stream() - .map(c -> ScriptArray.of(c.x, c.y))) + final Set pixels = new HashSet<>(); + project.getState().getSelection().unboundedPixelAlgorithm( + (x, y) -> pixels.add(ScriptArray.of(x, y))); + + return get ? new ScriptSet(pixels.stream().map(c -> c)) : project.getState().hasSelection(); } diff --git a/src/com/jordanbunke/stipple_effect/scripting/ext_ast_nodes/statement/ScopedStatementNode.java b/src/com/jordanbunke/stipple_effect/scripting/ext_ast_nodes/statement/ScopedStatementNode.java index d22a9dd5..167df393 100644 --- a/src/com/jordanbunke/stipple_effect/scripting/ext_ast_nodes/statement/ScopedStatementNode.java +++ b/src/com/jordanbunke/stipple_effect/scripting/ext_ast_nodes/statement/ScopedStatementNode.java @@ -8,7 +8,7 @@ import com.jordanbunke.stipple_effect.scripting.util.Scope; public abstract class ScopedStatementNode extends ExtStatementNode { - protected Scope scope; + protected final Scope scope; public ScopedStatementNode( final TextPosition position, diff --git a/src/com/jordanbunke/stipple_effect/scripting/ext_ast_nodes/statement/layer/SetFrameNode.java b/src/com/jordanbunke/stipple_effect/scripting/ext_ast_nodes/statement/layer/SetFrameNode.java index a49dc901..5fa5282f 100644 --- a/src/com/jordanbunke/stipple_effect/scripting/ext_ast_nodes/statement/layer/SetFrameNode.java +++ b/src/com/jordanbunke/stipple_effect/scripting/ext_ast_nodes/statement/layer/SetFrameNode.java @@ -5,14 +5,11 @@ import com.jordanbunke.delta_time.scripting.ast.nodes.types.TypeNode; import com.jordanbunke.delta_time.scripting.ast.symbol_table.SymbolTable; import com.jordanbunke.delta_time.scripting.util.TextPosition; -import com.jordanbunke.delta_time.utility.math.Coord2D; import com.jordanbunke.stipple_effect.layer.SELayer; import com.jordanbunke.stipple_effect.scripting.util.LayerRep; +import com.jordanbunke.stipple_effect.selection.Selection; import com.jordanbunke.stipple_effect.utility.StatusUpdates; -import java.util.HashSet; -import java.util.Set; - public final class SetFrameNode extends LayerStatementNode { public static final String SET_NAME = "set_frame", EDIT_NAME = "edit_frame"; @@ -61,7 +58,7 @@ protected void operation( StatusUpdates.scriptActionNotPermitted(attempt, "the frame index (" + frameIndex + ") is negative", arguments.args()[0].getPosition()); - else if (frameIndex >= layer.project().getState().getFrameCount()) + else if (frameIndex >= fc) StatusUpdates.scriptActionNotPermitted(attempt, "the frame index (" + frameIndex + ") is " + (frameIndex == fc ? "greater than " : "equal to ") + @@ -73,17 +70,13 @@ else if (aw != ew || ah != eh) ") do not match the project canvas bounds (" + ew + "x" + eh + ")", arguments.args()[1].getPosition()); else { - final Set pixels = new HashSet<>(); final int w = layer.project().getState().getImageWidth(), h = layer.project().getState().getImageHeight(); - - for (int x = 0; x < w; x++) - for (int y = 0; y < h; y++) - pixels.add(new Coord2D(x, y)); + final Selection selection = Selection.allInBounds(w, h); final SELayer old = evalLayer(symbolTable), replacement = set - ? old.returnStamped(content, pixels, frameIndex) + ? old.returnStamped(content, selection, frameIndex) : old.returnPaintedOver(content, frameIndex); layer.project().setLayerFromScript(replacement, layer.index()); diff --git a/src/com/jordanbunke/stipple_effect/scripting/ext_ast_nodes/statement/layer/WipeFrameNode.java b/src/com/jordanbunke/stipple_effect/scripting/ext_ast_nodes/statement/layer/WipeFrameNode.java index 8c92660c..2131c825 100644 --- a/src/com/jordanbunke/stipple_effect/scripting/ext_ast_nodes/statement/layer/WipeFrameNode.java +++ b/src/com/jordanbunke/stipple_effect/scripting/ext_ast_nodes/statement/layer/WipeFrameNode.java @@ -34,7 +34,7 @@ protected void operation( StatusUpdates.scriptActionNotPermitted(attempt, "the frame index (" + frameIndex + ") is negative", arguments.args()[0].getPosition()); - else if (frameIndex >= layer.project().getState().getFrameCount()) + else if (frameIndex >= fc) StatusUpdates.scriptActionNotPermitted(attempt, "the frame index (" + frameIndex + ") is " + (frameIndex == fc ? "greater than " : "equal to ") + diff --git a/src/com/jordanbunke/stipple_effect/scripting/ext_ast_nodes/statement/project/ColorScriptNode.java b/src/com/jordanbunke/stipple_effect/scripting/ext_ast_nodes/statement/project/ColorScriptNode.java index e7eef709..7f12e9d7 100644 --- a/src/com/jordanbunke/stipple_effect/scripting/ext_ast_nodes/statement/project/ColorScriptNode.java +++ b/src/com/jordanbunke/stipple_effect/scripting/ext_ast_nodes/statement/project/ColorScriptNode.java @@ -25,7 +25,7 @@ public ColorScriptNode( final ExpressionNode scope, final ExpressionNode[] args ) { super(position, scope, args, TypeNode.getInt(), - TypeNode.getBool(), TypeNode.getString()); + TypeNode.getBool(), TypeNode.getBool(), TypeNode.getString()); } @Override @@ -34,8 +34,9 @@ public FuncControlFlow execute(final SymbolTable symbolTable) { final Object[] vs = arguments.getValues(symbolTable); final int scopeIndex = (int) vs[0]; - final boolean includeDisabled = (boolean) vs[1]; - final String scriptFP = (String) vs[2], + final boolean includeDisabled = (boolean) vs[1], + ignoreSelection = (boolean) vs[2]; + final String scriptFP = (String) vs[3], attempt = "run the color script at \"" + scriptFP + "\""; if (scopeIndex < 0 || scopeIndex >= DialogVals.Scope.values().length) @@ -54,16 +55,18 @@ public FuncControlFlow execute(final SymbolTable symbolTable) { if (!SEInterpreter.validateColorScript(colorScript)) StatusUpdates.scriptActionNotPermitted(attempt, "the script is not a valid color script", - arguments.args()[2].getPosition()); + arguments.args()[3].getPosition()); else { final DialogVals.Scope scope = DialogVals.Scope.values()[scopeIndex]; final DialogVals.Scope scopeWas = DialogVals.getScope(); - final boolean includeWas = DialogVals.isIncludeDisabledLayers(); + final boolean includeWas = DialogVals.isIncludeDisabledLayers(), + ignoreWas = DialogVals.isIgnoreSelection(); DialogVals.setScope(scope); DialogVals.setIncludeDisabledLayers(includeDisabled); + DialogVals.setIgnoreSelection(ignoreSelection); final ProjectState res = project.prepColorScript(colorScript); project.getStateManager() @@ -72,6 +75,7 @@ public FuncControlFlow execute(final SymbolTable symbolTable) { // reset to dialog values DialogVals.setScope(scopeWas); DialogVals.setIncludeDisabledLayers(includeWas); + DialogVals.setIgnoreSelection(ignoreWas); } } diff --git a/src/com/jordanbunke/stipple_effect/scripting/ext_ast_nodes/statement/project/HSVShiftNode.java b/src/com/jordanbunke/stipple_effect/scripting/ext_ast_nodes/statement/project/HSVShiftNode.java index 590476d2..d2e42896 100644 --- a/src/com/jordanbunke/stipple_effect/scripting/ext_ast_nodes/statement/project/HSVShiftNode.java +++ b/src/com/jordanbunke/stipple_effect/scripting/ext_ast_nodes/statement/project/HSVShiftNode.java @@ -6,6 +6,8 @@ import com.jordanbunke.delta_time.scripting.util.FuncControlFlow; import com.jordanbunke.delta_time.scripting.util.TextPosition; import com.jordanbunke.stipple_effect.project.SEContext; +import com.jordanbunke.stipple_effect.scripting.ext_ast_nodes.type.NumTypeNode; +import com.jordanbunke.stipple_effect.scripting.util.ShiftOrScale; import com.jordanbunke.stipple_effect.state.Operation; import com.jordanbunke.stipple_effect.state.ProjectState; import com.jordanbunke.stipple_effect.utility.Constants; @@ -20,8 +22,8 @@ public HSVShiftNode( final ExpressionNode scope, final ExpressionNode[] args ) { super(position, scope, args, TypeNode.getInt(), - TypeNode.getBool(), TypeNode.getInt(), - TypeNode.getFloat(), TypeNode.getFloat()); + TypeNode.getBool(), TypeNode.getBool(), + TypeNode.getInt(), NumTypeNode.get(), NumTypeNode.get()); } @Override @@ -29,9 +31,12 @@ public FuncControlFlow execute(final SymbolTable symbolTable) { final SEContext project = getProject(symbolTable); final Object[] vs = arguments.getValues(symbolTable); - final int scopeIndex = (int) vs[0], hShift = (int) vs[2]; - final boolean includeDisabled = (boolean) vs[1]; - final double sShift = (double) vs[3], vShift = (double) vs[4]; + final int scopeIndex = (int) vs[0], hShift = (int) vs[3]; + final boolean includeDisabled = (boolean) vs[1], + ignoreSelection = (boolean) vs[2]; + final ShiftOrScale sShift = new ShiftOrScale(vs[4]), + vShift = new ShiftOrScale(vs[5]); + final String attempt = "apply an HSV level shift"; if (scopeIndex < 0 || scopeIndex >= DialogVals.Scope.values().length) @@ -47,36 +52,42 @@ public FuncControlFlow execute(final SymbolTable symbolTable) { hShift + ") is out of bounds (" + Constants.MIN_HUE_SHIFT + "<= h_shift <= " + Constants.MAX_HUE_SHIFT + ")", - arguments.args()[2].getPosition()); - else if (sShift < Constants.MIN_SV_SHIFT || - sShift > Constants.MAX_SV_SHIFT) - StatusUpdates.scriptActionNotPermitted(attempt, - "the saturation shift (" + sShift + - ") is out of bounds (" + - Constants.MIN_SV_SHIFT + "<= s_shift <= " + - Constants.MAX_SV_SHIFT + ")", - arguments.args()[3].getPosition()); - else if (vShift < Constants.MIN_SV_SHIFT || - vShift > Constants.MAX_SV_SHIFT) - StatusUpdates.scriptActionNotPermitted(attempt, - "the value shift (" + vShift + - ") is out of bounds (" + - Constants.MIN_SV_SHIFT + "<= v_shift <= " + - Constants.MAX_SV_SHIFT + ")", arguments.args()[3].getPosition()); + else if (sShift.outOfBounds()) + sShift.oobNotification("saturation", attempt, + arguments.args()[4].getPosition()); + else if (vShift.outOfBounds()) + vShift.oobNotification("value", attempt, + arguments.args()[5].getPosition()); else { final DialogVals.Scope scope = DialogVals.Scope.values()[scopeIndex]; final DialogVals.Scope scopeWas = DialogVals.getScope(); - final boolean includeWas = DialogVals.isIncludeDisabledLayers(); + final boolean includeWas = DialogVals.isIncludeDisabledLayers(), + ignoreWas = DialogVals.isIgnoreSelection(); DialogVals.setScope(scope); DialogVals.setIncludeDisabledLayers(includeDisabled); + DialogVals.setIgnoreSelection(ignoreSelection); DialogVals.setHueShift(hShift); - DialogVals.setSatShift(sShift); - DialogVals.setValueShift(vShift); + + if (sShift.isShifting != DialogVals.isShiftingSat()) + DialogVals.toggleShiftingSat(); + + if (sShift.isShifting) + DialogVals.setSatShift(sShift.shift); + else + DialogVals.setSatScale(sShift.scale); + + if (vShift.isShifting != DialogVals.isShiftingValue()) + DialogVals.toggleShiftingValue(); + + if (vShift.isShifting) + DialogVals.setValueShift(vShift.shift); + else + DialogVals.setValueScale(vShift.scale); final ProjectState res = project.prepHSVShift(); project.getStateManager() @@ -85,6 +96,7 @@ else if (vShift < Constants.MIN_SV_SHIFT || // reset to dialog values DialogVals.setScope(scopeWas); DialogVals.setIncludeDisabledLayers(includeWas); + DialogVals.setIgnoreSelection(ignoreWas); } } diff --git a/src/com/jordanbunke/stipple_effect/scripting/ext_ast_nodes/statement/project/PaletteActionNode.java b/src/com/jordanbunke/stipple_effect/scripting/ext_ast_nodes/statement/project/PaletteActionNode.java index 7db68545..8db46ba1 100644 --- a/src/com/jordanbunke/stipple_effect/scripting/ext_ast_nodes/statement/project/PaletteActionNode.java +++ b/src/com/jordanbunke/stipple_effect/scripting/ext_ast_nodes/statement/project/PaletteActionNode.java @@ -22,7 +22,7 @@ private PaletteActionNode( final ExpressionNode[] args, final boolean palettize ) { super(position, scope, args, PaletteTypeNode.get(), - TypeNode.getInt(), TypeNode.getBool()); + TypeNode.getInt(), TypeNode.getBool(), TypeNode.getBool()); this.palettize = palettize; } @@ -48,7 +48,8 @@ public FuncControlFlow execute(final SymbolTable symbolTable) { final Object[] vs = arguments.getValues(symbolTable); final Palette palette = (Palette) vs[0]; final int scopeIndex = (int) vs[1]; - final boolean includeDisabled = (boolean) vs[2]; + final boolean includeDisabled = (boolean) vs[2], + ignoreSelection = (boolean) vs[3]; if (scopeIndex < 0 || scopeIndex >= DialogVals.Scope.values().length) StatusUpdates.scriptActionNotPermitted( @@ -58,13 +59,16 @@ public FuncControlFlow execute(final SymbolTable symbolTable) { ") is not a valid index for this enumeration", arguments.args()[1].getPosition()); else { - final DialogVals.Scope scope = DialogVals.Scope.values()[scopeIndex]; + final DialogVals.Scope scope = + DialogVals.Scope.values()[scopeIndex]; final DialogVals.Scope scopeWas = DialogVals.getScope(); - final boolean includeWas = DialogVals.isIncludeDisabledLayers(); + final boolean includeWas = DialogVals.isIncludeDisabledLayers(), + ignoreWas = DialogVals.isIgnoreSelection(); DialogVals.setScope(scope); DialogVals.setIncludeDisabledLayers(includeDisabled); + DialogVals.setIgnoreSelection(ignoreSelection); if (palettize) project.palettize(palette); @@ -81,6 +85,7 @@ else if (palette.isMutable()) // reset to dialog values DialogVals.setScope(scopeWas); DialogVals.setIncludeDisabledLayers(includeWas); + DialogVals.setIgnoreSelection(ignoreWas); } return FuncControlFlow.cont(); diff --git a/src/com/jordanbunke/stipple_effect/scripting/ext_ast_nodes/statement/project/SetFrameDurationNode.java b/src/com/jordanbunke/stipple_effect/scripting/ext_ast_nodes/statement/project/SetFrameDurationNode.java new file mode 100644 index 00000000..0594bb72 --- /dev/null +++ b/src/com/jordanbunke/stipple_effect/scripting/ext_ast_nodes/statement/project/SetFrameDurationNode.java @@ -0,0 +1,61 @@ +package com.jordanbunke.stipple_effect.scripting.ext_ast_nodes.statement.project; + +import com.jordanbunke.delta_time.scripting.ast.nodes.expression.ExpressionNode; +import com.jordanbunke.delta_time.scripting.ast.nodes.types.TypeNode; +import com.jordanbunke.delta_time.scripting.ast.symbol_table.SymbolTable; +import com.jordanbunke.delta_time.scripting.util.FuncControlFlow; +import com.jordanbunke.delta_time.scripting.util.TextPosition; +import com.jordanbunke.stipple_effect.project.SEContext; +import com.jordanbunke.stipple_effect.utility.Constants; +import com.jordanbunke.stipple_effect.utility.StatusUpdates; + +public class SetFrameDurationNode extends ProjectStatementNode { + public static final String NAME = "set_frame_duration"; + + public SetFrameDurationNode( + final TextPosition position, + final ExpressionNode scope, final ExpressionNode[] args + ) { + super(position, scope, args, TypeNode.getInt(), TypeNode.getFloat()); + } + + @Override + public FuncControlFlow execute(final SymbolTable symbolTable) { + final SEContext project = getProject(symbolTable); + + final Object[] vs = arguments.getValues(symbolTable); + final int frameIndex = (int) vs[0], + fc = project.getState().getFrameCount(); + final double frameDuration = (double) vs[1]; + + final String attempt = "set the relative duration of frame " + + (frameIndex + 1); + + if (frameIndex < 0) + StatusUpdates.scriptActionNotPermitted(attempt, + "the frame index (" + frameIndex + ") is negative", + arguments.args()[0].getPosition()); + else if (frameIndex >= fc) + StatusUpdates.scriptActionNotPermitted(attempt, + "the frame index (" + frameIndex + ") is " + + (frameIndex == fc ? "greater than " : "equal to ") + + "the frame count (" + fc + ")", + arguments.args()[0].getPosition()); + else if (frameDuration < Constants.MIN_FRAME_DURATION || + frameDuration > Constants.MAX_FRAME_DURATION) + StatusUpdates.scriptActionNotPermitted(attempt, + "the frame duration (" + frameDuration + + ") is out of bounds; " + Constants.MIN_FRAME_DURATION + + " <= frame_duration <= " + Constants.MAX_FRAME_DURATION, + arguments.args()[1].getPosition()); + else + project.changeFrameDuration(frameDuration, frameIndex); + + return FuncControlFlow.cont(); + } + + @Override + protected String callName() { + return NAME; + } +} diff --git a/src/com/jordanbunke/stipple_effect/scripting/ext_ast_nodes/statement/project/SetSelectionNode.java b/src/com/jordanbunke/stipple_effect/scripting/ext_ast_nodes/statement/project/SetSelectionNode.java index fdc7b034..1417b699 100644 --- a/src/com/jordanbunke/stipple_effect/scripting/ext_ast_nodes/statement/project/SetSelectionNode.java +++ b/src/com/jordanbunke/stipple_effect/scripting/ext_ast_nodes/statement/project/SetSelectionNode.java @@ -6,11 +6,9 @@ import com.jordanbunke.delta_time.scripting.ast.symbol_table.SymbolTable; import com.jordanbunke.delta_time.scripting.util.FuncControlFlow; import com.jordanbunke.delta_time.scripting.util.TextPosition; -import com.jordanbunke.delta_time.utility.math.Coord2D; import com.jordanbunke.stipple_effect.project.SEContext; import com.jordanbunke.stipple_effect.scripting.util.ScriptSelectionUtils; - -import java.util.Set; +import com.jordanbunke.stipple_effect.selection.Selection; public final class SetSelectionNode extends ProjectStatementNode { public static final String NAME = "set_selection"; @@ -29,11 +27,11 @@ public FuncControlFlow execute(final SymbolTable symbolTable) { final SEContext project = getProject(symbolTable); final int w = project.getState().getImageWidth(), h = project.getState().getImageHeight(); - final Set pixels = ScriptSelectionUtils + final Selection selection = ScriptSelectionUtils .convertSelection((ScriptSet) vs[0], w, h); project.deselect(false); - project.editSelection(pixels, true); + project.editSelection(selection, true); return FuncControlFlow.cont(); } diff --git a/src/com/jordanbunke/stipple_effect/scripting/ext_ast_nodes/type/NumTypeNode.java b/src/com/jordanbunke/stipple_effect/scripting/ext_ast_nodes/type/NumTypeNode.java new file mode 100644 index 00000000..680e7f0a --- /dev/null +++ b/src/com/jordanbunke/stipple_effect/scripting/ext_ast_nodes/type/NumTypeNode.java @@ -0,0 +1,50 @@ +package com.jordanbunke.stipple_effect.scripting.ext_ast_nodes.type; + +import com.jordanbunke.delta_time.scripting.ast.nodes.types.TypeNode; +import com.jordanbunke.delta_time.scripting.util.TextPosition; + +/** + * This class represents a generalization of the DeltaScript {@code int} and {@code float} types + * */ +public final class NumTypeNode extends TypeNode { + private static final NumTypeNode INSTANCE; + + private NumTypeNode() { + super(TextPosition.N_A); + } + + static { + INSTANCE = new NumTypeNode(); + } + + public static NumTypeNode get() { + return INSTANCE; + } + + @Override + public boolean hasSize() { + return false; + } + + @Override + public boolean complies(final Object o) { + return false; + } + + @Override + public boolean equals(final Object o) { + return o instanceof TypeNode && + TypeNode.getInt().equals(o) || + TypeNode.getFloat().equals(o); + } + + @Override + public int hashCode() { + return 0; + } + + @Override + public String toString() { + return null; + } +} diff --git a/src/com/jordanbunke/stipple_effect/scripting/util/Arguments.java b/src/com/jordanbunke/stipple_effect/scripting/util/Arguments.java index 30799eb0..da76220d 100644 --- a/src/com/jordanbunke/stipple_effect/scripting/util/Arguments.java +++ b/src/com/jordanbunke/stipple_effect/scripting/util/Arguments.java @@ -36,7 +36,7 @@ public void semanticErrorCheck( for (int i = 0; i < expectedArgTypes.length; i++) { final TypeNode expected = expectedArgTypes[i], argType = argTypes[i]; - if (!argType.equals(expected)) + if (!expected.equals(argType)) ScriptErrorLog.fireError(ScriptErrorLog.Message.ARG_NOT_TYPE, args[i].getPosition(), "function", expected.toString(), argType.toString()); diff --git a/src/com/jordanbunke/stipple_effect/scripting/util/SENodeDelegator.java b/src/com/jordanbunke/stipple_effect/scripting/util/SENodeDelegator.java index 3c3cf08c..eeb5f0d7 100644 --- a/src/com/jordanbunke/stipple_effect/scripting/util/SENodeDelegator.java +++ b/src/com/jordanbunke/stipple_effect/scripting/util/SENodeDelegator.java @@ -139,6 +139,10 @@ public static ExpressionNode scopedFunctionExpression( PaletteColorSetGetterNode.colors(position, scope, args); case PaletteColorSetGetterNode.INCLUDED -> PaletteColorSetGetterNode.included(position, scope, args); + case GetFrameDurationNode.NAME -> + new GetFrameDurationNode(position, scope, args); + case GetFrameDurationsNode.NAME -> + new GetFrameDurationsNode(position, scope, args); // extend here default -> new IllegalExpressionNode(position, "No scoped function \"" + fID + "\" with " + @@ -234,6 +238,8 @@ public static StatementNode scopedFunctionStatement( MutablePaletteColorOpNode.moveLeft(position, scope, args); case MutablePaletteColorOpNode.MOVE_RIGHT -> MutablePaletteColorOpNode.moveRight(position, scope, args); + case SetFrameDurationNode.NAME -> + new SetFrameDurationNode(position, scope, args); // extend here default -> new IllegalStatementNode(position, "No scoped function \"" + fID + "\" with " + diff --git a/src/com/jordanbunke/stipple_effect/scripting/util/ScriptSelectionUtils.java b/src/com/jordanbunke/stipple_effect/scripting/util/ScriptSelectionUtils.java index ff75e4f8..8676d078 100644 --- a/src/com/jordanbunke/stipple_effect/scripting/util/ScriptSelectionUtils.java +++ b/src/com/jordanbunke/stipple_effect/scripting/util/ScriptSelectionUtils.java @@ -6,6 +6,7 @@ import com.jordanbunke.delta_time.scripting.util.ScriptErrorLog; import com.jordanbunke.delta_time.utility.math.Coord2D; import com.jordanbunke.stipple_effect.selection.Outliner; +import com.jordanbunke.stipple_effect.selection.Selection; import com.jordanbunke.stipple_effect.utility.Constants; import java.util.HashSet; @@ -13,20 +14,20 @@ import java.util.function.BiPredicate; public class ScriptSelectionUtils { - public static Set convertSelection( + public static Selection convertSelection( final ScriptSet input, final int w, final int h ) { return convertSelection(input, (x, y) -> x >= 0 && x < w && y >= 0 && y < h); } - public static Set convertSelection( + public static Selection convertSelection( final ScriptSet input ) { return convertSelection(input, (x, y) -> true); } - private static Set convertSelection( + private static Selection convertSelection( final ScriptSet input, BiPredicate coordCondition ) { final Set pixels = new HashSet<>(); @@ -38,7 +39,7 @@ private static Set convertSelection( pixels.add(new Coord2D(x, y)); }); - return pixels; + return Selection.fromPixels(pixels); } public static boolean invalidSideMask( diff --git a/src/com/jordanbunke/stipple_effect/scripting/util/ShiftOrScale.java b/src/com/jordanbunke/stipple_effect/scripting/util/ShiftOrScale.java new file mode 100644 index 00000000..c1dd81c7 --- /dev/null +++ b/src/com/jordanbunke/stipple_effect/scripting/util/ShiftOrScale.java @@ -0,0 +1,45 @@ +package com.jordanbunke.stipple_effect.scripting.util; + +import com.jordanbunke.delta_time.scripting.util.TextPosition; +import com.jordanbunke.stipple_effect.utility.Constants; +import com.jordanbunke.stipple_effect.utility.StatusUpdates; + +public class ShiftOrScale { + public final boolean isShifting; + public final int shift, min, max; + public final double scale; + + public ShiftOrScale(final Object v) { + isShifting = v instanceof Integer; + + shift = isShifting ? (int) v : 0; + scale = isShifting ? 0.0 : (double) v; + min = isShifting ? Constants.MIN_SV_SHIFT : 0; + max = isShifting + ? Constants.MAX_SV_SHIFT + : (int) Constants.MAX_SV_SCALE; + } + + public boolean outOfBounds() { + if (isShifting) + return shift < min || shift > max; + + return scale < min || scale > max; + } + + public void oobNotification( + final String property, final String attempt, + final TextPosition position + ) { + final String op = isShifting ? "shift" : "scale", + variable = property.charAt(0) + "_" + op, + tail = isShifting ? "" : ".0"; + + StatusUpdates.scriptActionNotPermitted( + attempt, "the " + property + " " + op + " (" + + (isShifting ? shift : scale) + + ") is out of bounds (" + min + tail + + " <= " + variable + " <= " + max + tail + ")", + position); + } +} diff --git a/src/com/jordanbunke/stipple_effect/selection/Outliner.java b/src/com/jordanbunke/stipple_effect/selection/Outliner.java index 7c09b703..a606291a 100644 --- a/src/com/jordanbunke/stipple_effect/selection/Outliner.java +++ b/src/com/jordanbunke/stipple_effect/selection/Outliner.java @@ -5,6 +5,7 @@ import java.util.*; import java.util.function.Function; +import java.util.function.Predicate; public class Outliner { public static final int NUM_DIRS = Direction.values().length, @@ -53,8 +54,8 @@ public static int[] getDoubleOutlineMask() { return new int[] { 1, 1, 1, 1, 1, 1, 1, 1 }; } - public static Set outline( - final Set selection, final int[] sideMask + public static Selection outline( + final Selection selection, final int[] sideMask ) { if (sideMask.length != NUM_DIRS) return selection; @@ -89,7 +90,7 @@ public static Set outline( continue; for (int s = 0; s < bound && - selection.contains(shifted) == internal; s++) { + selection.selected(shifted) == internal; s++) { outline.add(shifted); shifted = shifted.displace(shift); } @@ -123,18 +124,18 @@ public static Set outline( } final Function pass = - c -> internal == selection.contains(c); + c -> internal == selection.selected(c); shiftCands.stream().filter(cand -> (int) Math.round( Coord2D.unitDistanceBetween(pixel, cand)) <= (end - 1) && pass.apply(cand)).forEach(outline::add); } } - return outline; + return Selection.fromPixels(outline); } private static Map> calculateFrontier( - final Set selection, final int[] sideMask + final Selection selection, final int[] sideMask ) { final Map> frontier = new HashMap<>(); @@ -144,20 +145,27 @@ private static Map> calculateFrontier( internal = sideMask[dir.ordinal()] < 0; final Coord2D rc = dir.relativeCoordinate(); - selection.stream().filter(px -> { - if (selection.contains(px.displace(rc))) + final Predicate check = px -> { + if (selection.selected(px.displace(rc))) return false; if (diag) { final boolean - compX = selection.contains(px.displace(rc.x, 0)), - compY = selection.contains(px.displace(0, rc.y)); + compX = selection.selected(px.displace(rc.x, 0)), + compY = selection.selected(px.displace(0, rc.y)); return internal ? compX && compY : !(compX || compY); } return true; - }).forEach(dirFrontier::add); + }; + + selection.unboundedPixelAlgorithm((x, y) -> { + final Coord2D px = new Coord2D(x, y); + + if (check.test(px)) + dirFrontier.add(px); + }); frontier.put(dir, dirFrontier); } diff --git a/src/com/jordanbunke/stipple_effect/selection/RotateFunction.java b/src/com/jordanbunke/stipple_effect/selection/RotateFunction.java deleted file mode 100644 index 6e4c4958..00000000 --- a/src/com/jordanbunke/stipple_effect/selection/RotateFunction.java +++ /dev/null @@ -1,11 +0,0 @@ -package com.jordanbunke.stipple_effect.selection; - -import com.jordanbunke.delta_time.utility.math.Coord2D; - -import java.util.Set; - -@FunctionalInterface -public interface RotateFunction { - void accept(final Set initialSelection, final double deltaR, - final Coord2D pivot, final boolean[] offset, final boolean checkpoint); -} diff --git a/src/com/jordanbunke/stipple_effect/selection/SEClipboard.java b/src/com/jordanbunke/stipple_effect/selection/SEClipboard.java index 6d9efafd..d1c9856a 100644 --- a/src/com/jordanbunke/stipple_effect/selection/SEClipboard.java +++ b/src/com/jordanbunke/stipple_effect/selection/SEClipboard.java @@ -1,19 +1,28 @@ package com.jordanbunke.stipple_effect.selection; +import com.jordanbunke.delta_time.image.GameImage; +import com.jordanbunke.delta_time.io.ClipboardData; +import com.jordanbunke.delta_time.io.ClipboardUtils; +import com.jordanbunke.stipple_effect.StippleEffect; import com.jordanbunke.stipple_effect.state.ProjectState; +import java.awt.datatransfer.DataFlavor; +import java.awt.datatransfer.Transferable; + public class SEClipboard { private static final SEClipboard INSTANCE; + private static final DataFlavor NATIVE_DATA_FLAVOR; private SelectionContents contents; - private SEClipboard() { contents = null; } static { INSTANCE = new SEClipboard(); + NATIVE_DATA_FLAVOR = new DataFlavor(SelectionContents.class, + StippleEffect.PROGRAM_NAME + " selection"); } public static SEClipboard get() { @@ -22,17 +31,49 @@ public static SEClipboard get() { public void sendSelectionToClipboard(final ProjectState state) { contents = switch (state.getSelectionMode()) { - case BOUNDS -> new SelectionContents( + case BOUNDS -> SelectionContents.make( state.getActiveLayerFrame(), state.getSelection()); case CONTENTS -> state.getSelectionContents(); }; + + ClipboardUtils.setContent( + new ClipboardData(contents, NATIVE_DATA_FLAVOR)); } - public boolean hasContents() { - return contents != null; + public boolean hasContent() { + return getContent() != null; } - public SelectionContents getContents() { - return contents; + public SelectionContents getContent() { + final Transferable content = ClipboardUtils.getContent(); + + if (content == null) + return null; + + if (content.isDataFlavorSupported(DataFlavor.imageFlavor)) { + final GameImage img = ClipboardUtils.getImage(); + + if (img == null) + return null; + + final int w = img.getWidth(), h = img.getHeight(); + final Selection all = Selection.allInBounds(w, h); + + // repackage clipboard contents as selection contents + final SelectionContents res = SelectionContents.make(img, all); + ClipboardUtils.setContent( + new ClipboardData(res, NATIVE_DATA_FLAVOR)); + + return res; + } else if (content.isDataFlavorSupported(NATIVE_DATA_FLAVOR)) { + try { + return (SelectionContents) content + .getTransferData(NATIVE_DATA_FLAVOR); + } catch (Exception e) { + return null; + } + } + + return null; } } diff --git a/src/com/jordanbunke/stipple_effect/selection/Selection.java b/src/com/jordanbunke/stipple_effect/selection/Selection.java new file mode 100644 index 00000000..b098566e --- /dev/null +++ b/src/com/jordanbunke/stipple_effect/selection/Selection.java @@ -0,0 +1,323 @@ +package com.jordanbunke.stipple_effect.selection; + +import com.jordanbunke.delta_time.utility.math.Bounds2D; +import com.jordanbunke.delta_time.utility.math.Coord2D; + +import java.util.Set; +import java.util.function.BiConsumer; + +/** + * Represents a collection of selected pixels + * */ +public final class Selection { + public static final Selection EMPTY; + + private static final int UNKNOWN = -1; + + static { + EMPTY = new Selection(new boolean[][] { { false } }); + } + + public final Coord2D topLeft; + public final Bounds2D bounds; + private final boolean[][] matrix; + + private int size; + + private Selection(final Coord2D topLeft, + final Bounds2D bounds, final boolean[][] matrix) { + this.topLeft = topLeft; + this.bounds = bounds; + this.matrix = matrix; + + size = UNKNOWN; + } + + private Selection(final Coord2D topLeft, final boolean[][] matrix) { + this(topLeft, new Bounds2D(matrix.length, matrix[0].length), matrix); + } + + private Selection(final boolean[][] matrix) { + this(new Coord2D(), matrix); + } + + public static Selection allInBounds(final int width, final int height) { + final boolean[][] matrix = new boolean[width][height]; + + for (int x = 0; x < width; x++) + for (int y = 0; y < height; y++) + matrix[x][y] = true; + + final Selection selection = new Selection(matrix); + selection.size = width * height; + + return selection; + } + + public static Selection of(final boolean[][] matrix) { + if (matrix.length == 0 || matrix[0].length == 0) + return EMPTY; + + return new Selection(matrix).crop(); + } + + public static Selection atOf(final Coord2D topLeft, + final boolean[][] matrix) { + if (matrix.length == 0 || matrix[0].length == 0) + return EMPTY; + + return new Selection(topLeft, matrix); + } + + public static Selection forRotation(final Coord2D topLeft, + final boolean[][] matrix) { + return new Selection(topLeft, matrix).crop(); + } + + public static Selection fromPixels(final Set pixels) { + if (pixels == null || pixels.isEmpty()) + return EMPTY; + + final Coord2D topLeft = SelectionUtils.topLeft(pixels), + bottomRight = SelectionUtils.bottomRight(pixels); + final Bounds2D bounds = new Bounds2D(bottomRight.x - topLeft.x, + bottomRight.y - topLeft.y); + final boolean[][] matrix = new boolean[bounds.width()][bounds.height()]; + + for (final Coord2D pixel : pixels) + matrix[pixel.x - topLeft.x][pixel.y - topLeft.y] = true; + + final Selection selection = new Selection(topLeft, bounds, matrix); + selection.size = pixels.size(); + + return selection; + } + + public static Selection union(final Selection a, final Selection b) { + if (!a.hasSelection()) + return b; + + if (!b.hasSelection()) + return a; + + final Coord2D topLeft = new Coord2D( + Math.min(a.topLeft.x, b.topLeft.x), + Math.min(a.topLeft.y, b.topLeft.y) + ), bottomRight = new Coord2D( + Math.max(a.bottomRight().x, b.bottomRight().x), + Math.max(a.bottomRight().y, b.bottomRight().y) + ); + + final int w = bottomRight.x - topLeft.x, + h = bottomRight.y - topLeft.y; + + final boolean[][] res = new boolean[w][h]; + + for (int x = 0; x < w; x++) + for (int y = 0; y < h; y++) { + final Coord2D px = new Coord2D(topLeft.x + x, topLeft.y + y); + + res[x][y] = a.selected(px) || b.selected(px); + } + + return new Selection(topLeft, new Bounds2D(w, h), res).crop(); + } + + public static Selection intersection(final Selection a, final Selection b) { + final Coord2D topLeft = new Coord2D( + Math.max(a.topLeft.x, b.topLeft.x), + Math.max(a.topLeft.y, b.topLeft.y) + ), bottomRight = new Coord2D( + Math.min(a.bottomRight().x, b.bottomRight().x), + Math.min(a.bottomRight().y, b.bottomRight().y) + ); + + final int w = bottomRight.x - topLeft.x, + h = bottomRight.y - topLeft.y; + + final boolean[][] res = new boolean[w][h]; + + for (int x = 0; x < w; x++) + for (int y = 0; y < h; y++) { + final Coord2D px = new Coord2D(topLeft.x + x, topLeft.y + y); + + res[x][y] = a.selected(px) && b.selected(px); + } + + return new Selection(topLeft, new Bounds2D(w, h), res).crop(); + } + + public static Selection difference(final Selection a, final Selection b) { + final boolean[][] res = + new boolean[a.bounds.width()][a.bounds.height()]; + + for (int x = 0; x < a.matrix.length; x++) + for (int y = 0; y < a.matrix[x].length; y++) + res[x][y] = a.matrix[x][y] && + !b.selected(a.topLeft.x + x, a.topLeft.y + y); + + return new Selection(a.topLeft, a.bounds, res).crop(); + } + + private Coord2D bottomRight() { + return topLeft.displace(bounds.width(), bounds.height()); + } + + private Selection crop() { + if (!hasSelection()) + return EMPTY; + + final Coord2D cropTL = cropTL(), cropBR = cropBR(); + final int w = bounds.width() - (cropTL.x + cropBR.x), + h = bounds.height() - (cropTL.y + cropBR.y); + final boolean[][] res = new boolean[w][h]; + + for (int x = 0; x < w; x++) + System.arraycopy(matrix[x + cropTL.x], cropTL.y, res[x], 0, h); + + return new Selection(topLeft.displace(cropTL), res); + } + + private Coord2D cropTL() { + final int w = bounds.width(); + int leftmost = w, highest = bounds.height(); + + for (int x = 0; x < w; x++) + for (int y = 0; y < highest; y++) + if (matrix[x][y]) { + leftmost = Math.min(leftmost, x); + highest = y; + } + + return new Coord2D(leftmost, highest); + } + + private Coord2D cropBR() { + final int w = bounds.width(), h = bounds.height(); + int rightmost = 0, lowest = 0; + + for (int x = w - 1; x >= 0; x--) + for (int y = h - 1; y >= lowest; y--) + if (matrix[x][y]) { + rightmost = Math.max(rightmost, x); + lowest = y; + } + + return new Coord2D((w - 1) - rightmost, (h - 1) - lowest); + } + + public Selection displace(final Coord2D displacement) { + return new Selection(topLeft.displace(displacement), bounds, matrix); + } + + public boolean selected(final Coord2D pixel) { + return selected(pixel.x, pixel.y); + } + + public boolean selected(final int x, final int y) { + final int mx = x - topLeft.x, my = y - topLeft.y; + + return inBounds(x, y) && matrix[mx][my]; + } + + private boolean inBounds(final int x, final int y) { + return x >= topLeft.x && y >= topLeft.y && + x < topLeft.x + bounds.width() && + y < topLeft.y + bounds.height(); + } + + public boolean hasSelection() { + if (equals(EMPTY)) + return false; + + if (size != UNKNOWN) + return size > 0; + + final int w = bounds.width(), h = bounds.height(); + + for (int x = 0; x < w; x++) + for (int y = 0; y < h; y++) + if (matrix[x][y]) + return true; + + return false; + } + + public void pixelAlgorithm( + final int w, final int h, + final BiConsumer algorithm + ) { + pixelAlgorithm(w, h, true, algorithm); + } + + public void unboundedPixelAlgorithm( + final BiConsumer algorithm + ) { + pixelAlgorithm(0, 0, false, algorithm); + } + + private void pixelAlgorithm( + final int w, final int h, final boolean bounded, + final BiConsumer algorithm + ) { + if (bounded) { + final int initX = Math.max(0, topLeft.x), + initY = Math.max(0, topLeft.y), + endX = Math.min(w, topLeft.x + bounds.width()), + endY = Math.min(h, topLeft.y + bounds.height()); + + for (int x = initX; x < endX; x++) + for (int y = initY; y < endY; y++) + if (selected(x, y)) + algorithm.accept(x, y); + } else + for (int x = 0; x < matrix.length; x++) + for (int y = 0; y < matrix[x].length; y++) + if (matrix[x][y]) + algorithm.accept(topLeft.x + x, topLeft.y + y); + } + + public int size() { + if (size == UNKNOWN) + count(); + + return size; + } + + private void count() { + int count = 0; + + for (int x = 0; x < bounds.width(); x++) + for (int y = 0; y < bounds.height(); y++) + if (matrix[x][y]) + count++; + + size = count; + } + + @Override + public boolean equals(final Object o) { + if (o instanceof Selection s && topLeft.equals(s.topLeft) && + bounds.equals(s.bounds)) { + if (matrix.length == s.matrix.length) { + for (int x = 0; x < matrix.length; x++) { + if (matrix[x].length != s.matrix.length) + return false; + + for (int y = 0; y < matrix[x].length; y++) + if (matrix[x][y] != s.matrix[x][y]) + return false; + } + + return true; + } + } + + return false; + } + + @Override + public int hashCode() { + return topLeft.x + topLeft.y + bounds.width() + bounds.height(); + } +} diff --git a/src/com/jordanbunke/stipple_effect/selection/SelectionContents.java b/src/com/jordanbunke/stipple_effect/selection/SelectionContents.java index ba59767a..34aeec5e 100644 --- a/src/com/jordanbunke/stipple_effect/selection/SelectionContents.java +++ b/src/com/jordanbunke/stipple_effect/selection/SelectionContents.java @@ -6,62 +6,53 @@ import com.jordanbunke.stipple_effect.utility.math.Geometry; import java.awt.*; -import java.util.Set; -import java.util.stream.Collectors; public class SelectionContents { private static final int X = 0, Y = 1; private final GameImage content; - private final Coord2D topLeft; - private final Set pixels; + private final Selection selection; private final SelectionContents original; - public SelectionContents( - final GameImage canvas, final Set selection + public static SelectionContents make( + final GameImage canvas, final Selection selection ) { - pixels = selection.stream().filter(s -> s.x >= 0 && s.y >= 0 && - s.x < canvas.getWidth() && - s.y < canvas.getHeight()).collect(Collectors.toSet()); + final GameImage content = makeContentFromSelection(canvas, selection); + final SelectionContents original = + new SelectionContents(content, selection); - topLeft = SelectionUtils.topLeft(pixels); - content = makeContentFromSelection(canvas, pixels); - - original = new SelectionContents(content, topLeft, pixels); + return new SelectionContents(content, selection, original); } private SelectionContents( - final GameImage content, final Coord2D topLeft, - final Set pixels + final GameImage content, final Selection selection ) { - this(content, topLeft, pixels, - new SelectionContents(content, topLeft, pixels, null)); + this(content, selection, + new SelectionContents(content, selection, null)); } private SelectionContents( - final GameImage content, final Coord2D topLeft, - final Set pixels, final SelectionContents original + final GameImage content, final Selection selection, + final SelectionContents original ) { this.content = content; - this.topLeft = topLeft; - this.pixels = pixels; - + this.selection = selection; this.original = original; } - private GameImage makeContentFromSelection( - final GameImage canvas, final Set pixels + private static GameImage makeContentFromSelection( + final GameImage canvas, final Selection selection ) { - final Coord2D tl = SelectionUtils.topLeft(pixels), - br = SelectionUtils.bottomRight(pixels); + final Coord2D tl = selection.topLeft; - final int w = br.x - tl.x, h = br.y - tl.y; + final int w = selection.bounds.width(), + h = selection.bounds.height(); final GameImage content = new GameImage(w, h); for (int x = 0; x < w; x++) { for (int y = 0; y < h; y++) { - if (!pixels.contains(new Coord2D(tl.x + x, tl.y + y))) + if (!selection.selected(tl.x + x, tl.y + y)) continue; final Color c = canvas.getColorAt(tl.x + x, tl.y + y); @@ -73,67 +64,65 @@ private GameImage makeContentFromSelection( } public SelectionContents returnDisplaced(final Coord2D displacement) { - final Set displacedPixels = pixels.stream().map(px -> - px.displace(displacement)).collect(Collectors.toSet()); - return new SelectionContents(new GameImage(content), - topLeft.displace(displacement), displacedPixels); + selection.displace(displacement)); } public SelectionContents returnStretched( - final Set initialSelection, final Coord2D change, + final Selection initialSelection, final Coord2D change, final MoverTool.Direction direction ) { - final Set pixels = SelectionUtils.stretchedPixels( + final Selection stretched = SelectionUtils.stretchedPixels( initialSelection, change, direction); - if (pixels.isEmpty()) - return new SelectionContents(GameImage.dummy(), topLeft, - pixels, original == null ? this : original); - - final Coord2D tl = SelectionUtils.topLeft(pixels), - br = SelectionUtils.bottomRight(pixels); + if (!stretched.hasSelection()) + return new SelectionContents(GameImage.dummy(), + Selection.EMPTY, original == null ? this : original); - final int w = Math.max(1, br.x - tl.x), - h = Math.max(1, br.y - tl.y); + final int w = stretched.bounds.width(), + h = stretched.bounds.height(); + final Coord2D tl = stretched.topLeft; final GameImage content = new GameImage(w, h), sampleFrom = original == null ? this.content : original.content; + final int sw = sampleFrom.getWidth(), sh = sampleFrom.getHeight(); + for (int x = 0; x < w; x++) for (int y = 0; y < h; y++) - if (pixels.contains(tl.displace(x, y))) { - final int sampleX = (int)((x / (double) w) * sampleFrom.getWidth()), - sampleY = (int)((y / (double) h) * sampleFrom.getHeight()); + if (stretched.selected(tl.displace(x, y))) { + final int sampleX = Math.min(sw - 1, + (int)Math.round((x / (double) w) * sw)), + sampleY = Math.min(sh - 1, + (int)Math.round((y / (double) h) * sh)); content.dot(sampleFrom.getColorAt(sampleX, sampleY), x, y); } - return new SelectionContents(content.submit(), tl, pixels, - original == null ? this : original); + return new SelectionContents(content.submit(), + stretched, original == null ? this : original); } public SelectionContents returnRotated( - final Set initialSelection, + final Selection initialSelection, final double deltaR, final Coord2D pivot, final boolean[] offset ) { - final Set pixels = SelectionUtils.rotatedPixels( + final Selection rotated = SelectionUtils.rotatedPixels( initialSelection, deltaR, pivot, offset); - if (pixels.isEmpty()) - return new SelectionContents(GameImage.dummy(), topLeft, - pixels, original == null ? this : original); - - final Coord2D tl = SelectionUtils.topLeft(pixels), - br = SelectionUtils.bottomRight(pixels); + if (!rotated.hasSelection()) + return new SelectionContents(GameImage.dummy(), + Selection.EMPTY, original == null ? this : original); - final int w = Math.max(1, br.x - tl.x), - h = Math.max(1, br.y - tl.y); + final int w = rotated.bounds.width(), + h = rotated.bounds.height(); + final Coord2D tl = rotated.topLeft; final GameImage content = new GameImage(w, h), sampleFrom = original == null ? this.content : original.content; - final Coord2D sampleTL = original == null ? topLeft : original.topLeft; + final Coord2D sampleTL = original == null + ? selection.topLeft : original.selection.topLeft; final double[] realPivot = new double[] { pivot.x + (offset[X] ? -0.5 : 0d), @@ -142,7 +131,7 @@ public SelectionContents returnRotated( for (int x = 0; x < w; x++) for (int y = 0; y < h; y++) - if (pixels.contains(tl.displace(x, y))) { + if (rotated.selected(tl.displace(x, y))) { final int gx = tl.x + x, gy = tl.y + y; final double distance = Math.sqrt( @@ -166,45 +155,30 @@ public SelectionContents returnRotated( content.dot(sampleFrom.getColorAt(sampleX, sampleY), x, y); } - return new SelectionContents(content.submit(), tl, pixels, - original == null ? this : original); + return new SelectionContents(content.submit(), + rotated, original == null ? this : original); } public SelectionContents returnReflected( - final Set initialSelection, final boolean horizontal + final Selection initialSelection, final boolean horizontal ) { - final Set pixels = + final Selection reflected = SelectionUtils.reflectedPixels(initialSelection, horizontal); - if (pixels.isEmpty()) - return new SelectionContents(GameImage.dummy(), topLeft, pixels); - - final Coord2D tl = SelectionUtils.topLeft(pixels), - br = SelectionUtils.bottomRight(pixels), - middle = new Coord2D((tl.x + br.x) / 2, (tl.y + br.y) / 2); - final boolean[] offset = new boolean[] { - (tl.x + br.x) % 2 == 0, - (tl.y + br.y) % 2 == 0 - }; + if (!reflected.hasSelection()) + return new SelectionContents(GameImage.dummy(), Selection.EMPTY); - final int w = Math.max(1, br.x - tl.x), - h = Math.max(1, br.y - tl.y); + final int w = reflected.bounds.width(), + h = reflected.bounds.height(); + final Coord2D tl = reflected.topLeft; final GameImage content = new GameImage(w, h); for (int x = 0; x < w; x++) for (int y = 0; y < h; y++) - if (pixels.contains(tl.displace(x, y))) { - final Coord2D pixel = tl.displace(x, y), - was = new Coord2D( - horizontal ? middle.x + (middle.x - - pixel.x) - (offset[X] ? 1 : 0) : pixel.x, - horizontal ? pixel.y : middle.y + - (middle.y - pixel.y) - (offset[Y] ? 1 : 0) - ); - - final int sampleX = was.x - topLeft.x, - sampleY = was.y - topLeft.y; + if (reflected.selected(tl.displace(x, y))) { + final int sampleX = horizontal ? (w - 1) - x : x, + sampleY = horizontal ? y : (h - 1) - y; if (sampleX >= 0 && sampleY >= 0 && sampleX < this.content.getWidth() && @@ -212,16 +186,16 @@ public SelectionContents returnReflected( content.dot(this.content.getColorAt(sampleX, sampleY), x, y); } - return new SelectionContents(content.submit(), tl, pixels); + return new SelectionContents(content.submit(), reflected); } public GameImage getContentForCanvas(final int w, final int h) { final GameImage contentForCanvas = new GameImage(w, h); - contentForCanvas.draw(content, topLeft.x, topLeft.y); + contentForCanvas.draw(content, selection.topLeft.x, selection.topLeft.y); return contentForCanvas.submit(); } - public Set getPixels() { - return pixels; + public Selection getSelection() { + return selection; } } diff --git a/src/com/jordanbunke/stipple_effect/selection/SelectionOverlay.java b/src/com/jordanbunke/stipple_effect/selection/SelectionOverlay.java new file mode 100644 index 00000000..604a0c97 --- /dev/null +++ b/src/com/jordanbunke/stipple_effect/selection/SelectionOverlay.java @@ -0,0 +1,220 @@ +package com.jordanbunke.stipple_effect.selection; + +import com.jordanbunke.delta_time.image.GameImage; +import com.jordanbunke.delta_time.utility.math.Coord2D; +import com.jordanbunke.stipple_effect.project.ZoomLevel; +import com.jordanbunke.stipple_effect.utility.Constants; +import com.jordanbunke.stipple_effect.utility.settings.Settings; +import com.jordanbunke.stipple_effect.visual.GraphicsUtils; + +import java.awt.*; +import java.util.HashSet; +import java.util.List; +import java.util.Set; +import java.util.function.Function; +import java.util.function.Predicate; + +public final class SelectionOverlay { + private static final int TL = 0, BR = 1, DIM = 2, TRP = 3, BOUNDS = 4; + + // directional frontiers + private final Set left, right, top, bottom; + private final GameImage filled; + private Coord2D tl; + private final int w, h; + + private float lastZ; + private Coord2D lastTL; + private int lastWW, lastWH; + private Coord2D lastRender; + private boolean wasFilled, couldTransform; + private GameImage last; + + public SelectionOverlay() { + this(Selection.EMPTY); + } + + public SelectionOverlay(final Selection selection) { + lastZ = ZoomLevel.MIN.z; + lastTL = new Coord2D(); + lastRender = new Coord2D(); + lastWW = 0; + lastWH = 0; + + left = new HashSet<>(); + right = new HashSet<>(); + top = new HashSet<>(); + bottom = new HashSet<>(); + + tl = selection.topLeft; + + w = selection.bounds.width(); + h = selection.bounds.height(); + + filled = new GameImage(w, h); + final Color fillC = Settings.getTheme().selectionFill; + filled.setColor(fillC); + + selection.unboundedPixelAlgorithm((x, y) -> { + final Coord2D px = new Coord2D(x, y); + + if (!selection.selected(x - 1, y)) left.add(px); + if (!selection.selected(x + 1, y)) right.add(px); + if (!selection.selected(x, y - 1)) top.add(px); + if (!selection.selected(x, y + 1)) bottom.add(px); + + filled.dot(x - tl.x, y - tl.y); + }); + + filled.free(); + } + + public void updateTL(final Selection selection) { + lastTL = tl; + tl = selection.topLeft; + + if (!lastTL.equals(tl)) { + final Coord2D displacement = new Coord2D( + tl.x - lastTL.x, tl.y - lastTL.y); + + final List> dirs = List.of(left, right, top, bottom); + + for (Set dir : dirs) { + final Set bank = new HashSet<>(dir); + dir.clear(); + + bank.forEach(p -> dir.add(p.displace(displacement))); + } + } + } + + public GameImage draw( + final float z, final Coord2D render, final int ww, final int wh, + final boolean isFilled, final boolean canTransform + ) { + if (z < Constants.ZOOM_FOR_OVERLAY) + return GameImage.dummy(); + + final boolean sameSelection = wasFilled == isFilled && + couldTransform == canTransform && lastTL.equals(tl), + sameWindow = lastZ == z && lastRender.equals(render) && + lastWW == ww && lastWH == wh; + + if (last != null && sameSelection && sameWindow) + return last; + + final GameImage overlay = new GameImage(ww, wh); + + final Coord2D fillRender = render.displace( + (int)(tl.x * z), (int)(tl.y * z)); + + if (fillRender.x > ww || fillRender.y > wh || + fillRender.x + (int)(w * z) <= 0 || + fillRender.y + (int)(h * z) <= 0) + return GameImage.dummy(); + + final Coord2D[] bounds = getRenderBounds(z, fillRender, ww, wh); + + if (isFilled) + overlay.draw(filled.section(bounds[TL], bounds[BR]), + bounds[TRP].x, bounds[TRP].y, + (int)(bounds[DIM].x * z), (int)(bounds[DIM].y * z)); + + final Color inside = Settings.getTheme().buttonOutline, + outside = Settings.getTheme().highlightOutline; + + final int zint = (int) z; + final Predicate inBounds = p -> { + final Coord2D adj = p.displace(-tl.x, -tl.y); + + return adj.x >= bounds[TL].x && adj.y >= bounds[TL].y && + adj.x <= bounds[BR].x && adj.y <= bounds[BR].y; + }; + final Function renderCoord = c -> + render.displace(c.x * zint, c.y * zint); + + left.stream().filter(inBounds).forEach(p -> { + final Coord2D c = renderCoord.apply(p); + + overlay.fillRectangle(inside, c.x, c.y, 1, zint); + overlay.fillRectangle(outside, c.x - 1, c.y, 1, zint); + }); + right.stream().filter(inBounds).forEach(p -> { + final Coord2D c = renderCoord.apply(p).displace(zint, 0); + + overlay.fillRectangle(inside, c.x - 1, c.y, 1, zint); + overlay.fillRectangle(outside, c.x, c.y, 1, zint); + }); + top.stream().filter(inBounds).forEach(p -> { + final Coord2D c = renderCoord.apply(p); + + overlay.fillRectangle(inside, c.x, c.y, zint, 1); + overlay.fillRectangle(outside, c.x, c.y - 1, zint, 1); + }); + bottom.stream().filter(inBounds).forEach(p -> { + final Coord2D c = renderCoord.apply(p).displace(0, zint); + + overlay.fillRectangle(inside, c.x, c.y - 1, zint, 1); + overlay.fillRectangle(outside, c.x, c.y, zint, 1); + }); + + if (canTransform) { + final GameImage NODE = GraphicsUtils.TRANSFORM_NODE; + + final int BEG = 0, MID = 1, END = 2, VALS = 3; + final int[] xs = new int[VALS], ys = new int[VALS]; + + xs[BEG] = fillRender.x - (NODE.getWidth() / 2); + ys[BEG] = fillRender.y - (NODE.getWidth() / 2); + + xs[END] = xs[BEG] + (w * zint); + ys[END] = ys[BEG] + (h * zint); + + xs[MID] = (xs[BEG] + xs[END]) / 2; + ys[MID] = (ys[BEG] + ys[END]) / 2; + + for (int x = BEG; x <= END; x++) + for (int y = BEG; y <= END; y++) + if (x != MID || y != MID) + overlay.draw(NODE, xs[x], ys[y]); + } + + lastZ = z; + lastRender = render; + lastWW = ww; + lastWH = wh; + + wasFilled = isFilled; + couldTransform = canTransform; + lastTL = tl; + + last = overlay.submit(); + + return overlay; + } + + private Coord2D[] getRenderBounds( + final float z, final Coord2D render, final int ww, final int wh + ) { + final Coord2D[] bounds = new Coord2D[BOUNDS]; + + final int tlx, tly, brx, bry; + + final float modX = render.x % z, modY = render.y % z; + + tlx = Math.max((int)(-render.x / z), 0); + tly = Math.max((int)(-render.y / z), 0); + brx = Math.min((int)((ww - render.x) / z) + 1, w); + bry = Math.min((int)((wh - render.y) / z) + 1, h); + + bounds[TL] = new Coord2D(tlx, tly); + bounds[BR] = new Coord2D(brx, bry); + bounds[DIM] = new Coord2D(brx - tlx, bry - tly); + bounds[TRP] = new Coord2D( + tlx == 0 ? render.x : (int) modX, + tly == 0 ? render.y : (int) modY + ); + + return bounds; + } +} diff --git a/src/com/jordanbunke/stipple_effect/selection/SelectionUtils.java b/src/com/jordanbunke/stipple_effect/selection/SelectionUtils.java index 6ebcc904..e8b22266 100644 --- a/src/com/jordanbunke/stipple_effect/selection/SelectionUtils.java +++ b/src/com/jordanbunke/stipple_effect/selection/SelectionUtils.java @@ -4,14 +4,14 @@ import com.jordanbunke.stipple_effect.tools.MoverTool; import com.jordanbunke.stipple_effect.utility.math.Geometry; +import java.util.Collection; import java.util.HashSet; import java.util.Set; -import java.util.stream.Collectors; public class SelectionUtils { private static final int X = 0, Y = 1; - public static Coord2D topLeft(final Set pixels) { + public static Coord2D topLeft(final Collection pixels) { int lowestX = Integer.MAX_VALUE, lowestY = Integer.MAX_VALUE; for (Coord2D pixel : pixels) { @@ -22,7 +22,7 @@ public static Coord2D topLeft(final Set pixels) { return new Coord2D(lowestX, lowestY); } - public static Coord2D bottomRight(final Set pixels) { + public static Coord2D bottomRight(final Collection pixels) { int lowestX = Integer.MIN_VALUE, lowestY = Integer.MIN_VALUE; for (Coord2D pixel : pixels) { @@ -33,29 +33,14 @@ public static Coord2D bottomRight(final Set pixels) { return new Coord2D(lowestX, lowestY); } - public static int height(final Set pixels) { - return bounds(pixels).y; - } - - public static int width(final Set pixels) { - return bounds(pixels).x; - } - - public static Coord2D bounds(final Set pixels) { - final Coord2D tl = topLeft(pixels), br = bottomRight(pixels); - - return new Coord2D(br.x - tl.x, br.y - tl.y); - } - - public static Set stretchedPixels( - final Set oldPixels, final Coord2D change, + public static Selection stretchedPixels( + final Selection oldPixels, final Coord2D change, final MoverTool.Direction direction ) { - final Coord2D oldTL = SelectionUtils.topLeft(oldPixels), - oldBR = SelectionUtils.bottomRight(oldPixels); - - final int oldW = oldBR.x - oldTL.x, - oldH = oldBR.y - oldTL.y; + final int oldW = oldPixels.bounds.width(), + oldH = oldPixels.bounds.height(); + final Coord2D oldTL = oldPixels.topLeft, + oldBR = oldTL.displace(oldW, oldH); final Coord2D tl = switch (direction) { case TL, T, L -> oldTL.displace(change); @@ -73,36 +58,47 @@ public static Set stretchedPixels( final int w = br.x - tl.x, h = br.y - tl.y; - final Set pixels = new HashSet<>(); + if (w < 1 || h < 1) + return Selection.EMPTY; + + final boolean[][] matrix = new boolean[w][h]; for (int x = 0; x < w; x++) - for (int y = 0; y < h; y++){ + for (int y = 0; y < h; y++) { final Coord2D oldPixel = oldTL.displace( (int)((x / (double) w) * oldW), (int)((y / (double) h) * oldH)); - if (oldPixels.contains(oldPixel)) - pixels.add(tl.displace(x, y)); + matrix[x][y] = oldPixels.selected(oldPixel); } - return pixels; + return Selection.atOf(tl, matrix); } - public static Set rotatedPixels( - final Set initialSelection, + public static Selection rotatedPixels( + final Selection initialSelection, final double deltaR, final Coord2D pivot, final boolean[] offset ) { final double[] realPivot = new double[] { pivot.x + (offset[X] ? -0.5 : 0d), pivot.y + (offset[Y] ? -0.5 : 0d) }; - final Set pixels = new HashSet<>(); - initialSelection.forEach(i -> { + final int initW = initialSelection.bounds.width(), + initH = initialSelection.bounds.height(); + final Coord2D initTL = initialSelection.topLeft, + initBR = initTL.displace(initW, initH), + initTR = new Coord2D(initBR.x, initTL.y), + initBL = new Coord2D(initTL.x, initBR.y); + + final Set initCorners = Set.of(initTL, initBR, initTR, initBL), + rotatedCorners = new HashSet<>(); + + initCorners.forEach(c -> { final double distance = Math.sqrt( - Math.pow(realPivot[X] - i.x, 2) + - Math.pow(realPivot[Y] - i.y, 2)), - angle = Geometry.calculateAngleInRad(i.x, i.y, + Math.pow(realPivot[X] - c.x, 2) + + Math.pow(realPivot[Y] - c.y, 2)), + angle = Geometry.calculateAngleInRad(c.x, c.y, realPivot[X], realPivot[Y]), newAngle = angle + deltaR; @@ -113,16 +109,18 @@ public static Set rotatedPixels( (int)Math.round(realPivot[X] + deltaX), (int)Math.round(realPivot[Y] + deltaY) ); - pixels.add(newPixel); + + rotatedCorners.add(newPixel); }); - final Coord2D tl = topLeft(pixels), br = bottomRight(pixels); + final Coord2D tl = topLeft(rotatedCorners), + br = bottomRight(rotatedCorners); + final int w = br.x - tl.x, h = br.y - tl.y; + + final boolean[][] matrix = new boolean[w][h]; for (int x = tl.x; x < br.x; x++) { for (int y = tl.y; y < br.y; y++) { - if (pixels.contains(new Coord2D(x, y))) - continue; - final double distance = Math.sqrt( Math.pow(realPivot[X] - x, 2) + Math.pow(realPivot[Y] - y, 2)), @@ -138,31 +136,33 @@ public static Set rotatedPixels( (int)Math.round(realPivot[Y] + deltaY) ); - if (initialSelection.contains(oldPixel)) - pixels.add(new Coord2D(x, y)); + if (initialSelection.selected(oldPixel)) + matrix[x - tl.x][y - tl.y] = true; } } - return pixels; + return Selection.forRotation(tl, matrix); } - public static Set reflectedPixels( - final Set initialSelection, final boolean horizontal + public static Selection reflectedPixels( + final Selection initialSelection, final boolean horizontal ) { - final Coord2D tl = topLeft(initialSelection), - br = bottomRight(initialSelection), - middle = new Coord2D((tl.x + br.x) / 2, - (tl.y + br.y) / 2); - final boolean[] offset = new boolean[] { - (tl.x + br.x) % 2 == 0, - (tl.y + br.y) % 2 == 0 - }; + final int w = initialSelection.bounds.width(), + h = initialSelection.bounds.height(); + final Coord2D tl = initialSelection.topLeft; + + final boolean[][] matrix = new boolean[w][h]; + + for (int x = 0; x < w; x++) + for (int y = 0; y < h; y++) { + final boolean was = initialSelection + .selected(tl.displace(x, y)); + + final int refX = horizontal ? (w - 1) - x : x, + refY = horizontal ? y : (h - 1) - y; + matrix[refX][refY] = was; + } - return initialSelection.stream().map(i -> new Coord2D( - horizontal ? middle.x + (middle.x - i.x) - - (offset[X] ? 1 : 0) : i.x, - horizontal ? i.y : middle.y + - (middle.y - i.y) - (offset[Y] ? 1 : 0) - )).collect(Collectors.toSet()); + return Selection.atOf(tl, matrix); } } diff --git a/src/com/jordanbunke/stipple_effect/selection/StretcherFunction.java b/src/com/jordanbunke/stipple_effect/selection/StretcherFunction.java deleted file mode 100644 index 70257734..00000000 --- a/src/com/jordanbunke/stipple_effect/selection/StretcherFunction.java +++ /dev/null @@ -1,12 +0,0 @@ -package com.jordanbunke.stipple_effect.selection; - -import com.jordanbunke.delta_time.utility.math.Coord2D; -import com.jordanbunke.stipple_effect.tools.MoverTool; - -import java.util.Set; - -@FunctionalInterface -public interface StretcherFunction { - void accept(final Set initialSelection, final Coord2D change, - final MoverTool.Direction direction, final boolean checkpoint); -} diff --git a/src/com/jordanbunke/stipple_effect/state/Operation.java b/src/com/jordanbunke/stipple_effect/state/Operation.java index ac7f24f4..e14aa3c7 100644 --- a/src/com/jordanbunke/stipple_effect/state/Operation.java +++ b/src/com/jordanbunke/stipple_effect/state/Operation.java @@ -7,6 +7,7 @@ public enum Operation { // frame operations ADD_FRAME, DUPLICATE_FRAME, REMOVE_FRAME, MOVE_FRAME_BACK, MOVE_FRAME_FORWARD, + CHANGE_FRAME_DURATION, // layer operations ADD_LAYER, DUPLICATE_LAYER, REMOVE_LAYER, MOVE_LAYER_DOWN, MOVE_LAYER_UP, @@ -16,11 +17,10 @@ public enum Operation { LAYER_VISIBILITY_CHANGE, LAYER_LINKING_CHANGE, LAYER_OPACITY_CHANGE, // selection operations RESET_SELECTION_CONTENTS, MOVE_SELECTION_CONTENTS, - STRETCH_SELECTION_CONTENTS, ROTATE_SELECTION_CONTENTS, - REFLECT_SELECTION_CONTENTS, PASTE, RAISE, DROP, + TRANSFORM_SELECTION_CONTENTS, + PASTE, RAISE, DROP, DELETE_SELECTION_CONTENTS, - MOVE_SELECTION_BOUNDS, STRETCH_SELECTION_BOUNDS, - ROTATE_SELECTION_BOUNDS, REFLECT_SELECTION_BOUNDS, + MOVE_SELECTION_BOUNDS, TRANSFORM_SELECTION_BOUNDS, DESELECT, SELECT, // canvas edit operations PALETTIZE, EDIT_IMAGE, @@ -37,24 +37,26 @@ public boolean triggersCanvasAuxiliaryRedraw() { public boolean triggersSelectionOverlayRedraw() { return switch (this) { case RESET_SELECTION_CONTENTS, - ROTATE_SELECTION_CONTENTS, - REFLECT_SELECTION_CONTENTS, - STRETCH_SELECTION_CONTENTS, - ROTATE_SELECTION_BOUNDS, - REFLECT_SELECTION_BOUNDS, - STRETCH_SELECTION_BOUNDS, + TRANSFORM_SELECTION_CONTENTS, + TRANSFORM_SELECTION_BOUNDS, PASTE, DROP, SELECT -> true; default -> false; }; } + public boolean triggersOverlayOffsetUpdate() { + return switch (this) { + case MOVE_SELECTION_BOUNDS, + MOVE_SELECTION_CONTENTS -> true; + default -> false; + }; + } + public boolean constitutesEdit() { return switch (this) { case NONE, RAISE, DROP, DESELECT, SELECT, MOVE_SELECTION_BOUNDS, - ROTATE_SELECTION_BOUNDS, - STRETCH_SELECTION_BOUNDS, - REFLECT_SELECTION_BOUNDS -> false; + TRANSFORM_SELECTION_BOUNDS -> false; default -> true; }; } @@ -71,4 +73,10 @@ public ActionType getActionType() { default -> ActionType.CANVAS; }; } + + @Override + public String toString() { + return String.valueOf(name().charAt(0)).toUpperCase() + + name().substring(1).replace("_", " ").toLowerCase(); + } } diff --git a/src/com/jordanbunke/stipple_effect/state/ProjectState.java b/src/com/jordanbunke/stipple_effect/state/ProjectState.java index 06182136..cee66324 100644 --- a/src/com/jordanbunke/stipple_effect/state/ProjectState.java +++ b/src/com/jordanbunke/stipple_effect/state/ProjectState.java @@ -1,19 +1,18 @@ package com.jordanbunke.stipple_effect.state; import com.jordanbunke.delta_time.image.GameImage; -import com.jordanbunke.delta_time.utility.math.Coord2D; import com.jordanbunke.stipple_effect.StippleEffect; import com.jordanbunke.stipple_effect.layer.SELayer; +import com.jordanbunke.stipple_effect.selection.Selection; import com.jordanbunke.stipple_effect.selection.SelectionContents; import com.jordanbunke.stipple_effect.selection.SelectionMode; +import com.jordanbunke.stipple_effect.tools.PickUpSelection; import com.jordanbunke.stipple_effect.tools.Tool; import com.jordanbunke.stipple_effect.utility.Constants; +import com.jordanbunke.stipple_effect.visual.theme.SEColors; -import java.awt.*; import java.util.ArrayList; -import java.util.HashSet; import java.util.List; -import java.util.Set; public class ProjectState { private boolean checkpoint; @@ -23,6 +22,7 @@ public class ProjectState { // FRAME private final int frameCount; + private final List frameDurations; private int frameIndex; // LAYER @@ -31,7 +31,7 @@ public class ProjectState { // SELECTION private final SelectionMode selectionMode; - private final Set selection; + private final Selection selection; private final SelectionContents selectionContents; public static ProjectState makeNew( @@ -52,37 +52,44 @@ public static ProjectState makeFromRasterFile( public static ProjectState makeFromNativeFile( final int imageWidth, final int imageHeight, - final List layers, final int frameCount + final List layers, + final int frameCount, final List frameDurations ) { - return new ProjectState(imageWidth, imageHeight, layers, frameCount); + return new ProjectState(imageWidth, imageHeight, layers, 0, + frameCount, frameDurations, 0, + SelectionMode.BOUNDS, Selection.EMPTY, null, true); } private ProjectState( final int imageWidth, final int imageHeight, final List layers, final int frameCount ) { - this(imageWidth, imageHeight, layers, 0, frameCount, 0, - SelectionMode.BOUNDS, new HashSet<>(), null, true); + this(imageWidth, imageHeight, layers, 0, + frameCount, defaultFrameDurations(frameCount), 0, + SelectionMode.BOUNDS, Selection.EMPTY, null, true); } private ProjectState( final int imageWidth, final int imageHeight, final List layers, final int layerEditIndex, - final int frameCount, final int frameIndex, + final int frameCount, final List frameDurations, + final int frameIndex, final SelectionMode selectionMode, - final Set selection, + final Selection selection, final SelectionContents selectionContents ) { - this(imageWidth, imageHeight, layers, layerEditIndex, frameCount, - frameIndex, selectionMode, selection, selectionContents, true); + this(imageWidth, imageHeight, layers, layerEditIndex, + frameCount, frameDurations, frameIndex, + selectionMode, selection, selectionContents, true); } private ProjectState( final int imageWidth, final int imageHeight, final List layers, final int layerEditIndex, - final int frameCount, final int frameIndex, + final int frameCount, final List frameDurations, + final int frameIndex, final SelectionMode selectionMode, - final Set selection, + final Selection selection, final SelectionContents selectionContents, final boolean checkpoint ) { @@ -93,6 +100,8 @@ private ProjectState( this.layerEditIndex = layerEditIndex; this.frameCount = frameCount; + this.frameDurations = frameDurations.size() == frameCount + ? frameDurations : defaultFrameDurations(frameCount); this.frameIndex = Math.max(0, Math.min(frameIndex, frameCount - 1)); this.selectionMode = selectionMode; @@ -103,13 +112,22 @@ private ProjectState( this.operation = Operation.NONE; } + public static List defaultFrameDurations(final int frameCount) { + final List frameDurations = new ArrayList<>(); + + while (frameDurations.size() < frameCount) + frameDurations.add(Constants.DEFAULT_FRAME_DURATION); + + return frameDurations; + } + public ProjectState changeIsCheckpoint( final boolean checkpoint ) { return new ProjectState(imageWidth, imageHeight, - new ArrayList<>(layers), layerEditIndex, - frameCount, frameIndex, selectionMode, - new HashSet<>(selection), selectionContents, + layers, layerEditIndex, + frameCount, frameDurations, frameIndex, + selectionMode, selection, selectionContents, checkpoint); } @@ -117,18 +135,18 @@ public ProjectState changeSelectionContents( final SelectionContents selectionContents ) { return new ProjectState(imageWidth, imageHeight, - new ArrayList<>(layers), layerEditIndex, - frameCount, frameIndex, SelectionMode.CONTENTS, - new HashSet<>(), selectionContents); + layers, layerEditIndex, + frameCount, frameDurations, frameIndex, + SelectionMode.CONTENTS, Selection.EMPTY, selectionContents); } public ProjectState changeSelectionBounds( - final Set selection + final Selection selection ) { return new ProjectState(imageWidth, imageHeight, - new ArrayList<>(layers), layerEditIndex, - frameCount, frameIndex, SelectionMode.BOUNDS, - selection, null); + layers, layerEditIndex, + frameCount, frameDurations, frameIndex, + SelectionMode.BOUNDS, selection, null); } public ProjectState changeLayers( @@ -136,27 +154,36 @@ public ProjectState changeLayers( ) { return new ProjectState(imageWidth, imageHeight, new ArrayList<>(layers), layerEditIndex, - frameCount, frameIndex, selectionMode, - new HashSet<>(selection), selectionContents); + frameCount, frameDurations, frameIndex, + selectionMode, selection, selectionContents); } public ProjectState changeLayers( final List layers, final int layerEditIndex ) { return new ProjectState(imageWidth, imageHeight, - new ArrayList<>(layers), layerEditIndex, - frameCount, frameIndex, selectionMode, - new HashSet<>(selection), selectionContents); + layers, layerEditIndex, + frameCount, frameDurations, frameIndex, + selectionMode, selection, selectionContents); } public ProjectState changeFrames( final List layers, final int frameIndex, - final int frameCount + final int frameCount, final List frameDurations ) { return new ProjectState(imageWidth, imageHeight, - new ArrayList<>(layers), layerEditIndex, - frameCount, frameIndex, selectionMode, - new HashSet<>(selection), selectionContents); + layers, layerEditIndex, + frameCount, frameDurations, frameIndex, + selectionMode, selection, selectionContents); + } + + public ProjectState changeFrameDurations( + final List frameDurations + ) { + return new ProjectState(imageWidth, imageHeight, + layers, layerEditIndex, + frameCount, frameDurations, frameIndex, + selectionMode, selection, selectionContents); } public ProjectState resize( @@ -164,9 +191,9 @@ public ProjectState resize( final List layers ) { return new ProjectState(imageWidth, imageHeight, - new ArrayList<>(layers), layerEditIndex, - frameCount, frameIndex, selectionMode, - new HashSet<>(selection), selectionContents); + layers, layerEditIndex, + frameCount, frameDurations, frameIndex, + selectionMode, selection, selectionContents); } public ProjectState stitch( @@ -174,9 +201,9 @@ public ProjectState stitch( final List layers ) { return new ProjectState(imageWidth, imageHeight, - new ArrayList<>(layers), layerEditIndex, - 1, 0, selectionMode, - new HashSet<>(selection), selectionContents); + layers, layerEditIndex, + 1, defaultFrameDurations(1), 0, + selectionMode, selection, selectionContents); } public ProjectState split( @@ -184,9 +211,9 @@ public ProjectState split( final List layers, final int frameCount ) { return new ProjectState(imageWidth, imageHeight, - new ArrayList<>(layers), layerEditIndex, - frameCount, 0, selectionMode, - new HashSet<>(selection), selectionContents); + layers, layerEditIndex, + frameCount, defaultFrameDurations(frameCount), 0, + selectionMode, selection, selectionContents); } public GameImage draw( @@ -216,15 +243,16 @@ public GameImage draw( // is being moved final boolean previewCondition = inProjectRender && layer.equals(getEditingLayer()) && hasSelection() && - selectionMode == SelectionMode.CONTENTS; + selectionMode == SelectionMode.CONTENTS && + !(tool.equals(PickUpSelection.get()) && + PickUpSelection.get().isMoving()); if (previewCondition) { - final Set pixels = selectionContents.getPixels(); + final Selection selection = selectionContents.getSelection(); + final int rgb = SEColors.transparent().getRGB(); - pixels.stream().filter(px -> px.x >= 0 && px.y >= 0 && - px.x < imageWidth && px.y < imageHeight - ).forEach(px -> layerImage.setRGB(px.x, px.y, - new Color(0, 0, 0, 0).getRGB())); + selection.pixelAlgorithm(imageWidth, imageHeight, + (x, y) -> layerImage.setRGB(x, y, rgb)); } image.draw(layerImage.submit()); @@ -336,15 +364,15 @@ public Operation getOperation() { public boolean hasSelection() { return switch (selectionMode) { - case BOUNDS -> !selection.isEmpty(); + case BOUNDS -> selection.hasSelection(); case CONTENTS -> hasSelectionContents(); }; } - public Set getSelection() { + public Selection getSelection() { return switch (selectionMode) { case BOUNDS -> selection; - case CONTENTS -> selectionContents.getPixels(); + case CONTENTS -> selectionContents.getSelection(); }; } @@ -365,7 +393,7 @@ public SELayer getEditingLayer() { } public GameImage getActiveLayerFrame() { - return layers.get(layerEditIndex).getFrame(frameIndex); + return getEditingLayer().getFrame(frameIndex); } public int getLayerEditIndex() { @@ -384,6 +412,10 @@ public int getFrameIndex() { return frameIndex; } + public List getFrameDurations() { + return frameDurations; + } + public int getImageWidth() { return imageWidth; } diff --git a/src/com/jordanbunke/stipple_effect/state/StateManager.java b/src/com/jordanbunke/stipple_effect/state/StateManager.java index 9a33c236..7fa70168 100644 --- a/src/com/jordanbunke/stipple_effect/state/StateManager.java +++ b/src/com/jordanbunke/stipple_effect/state/StateManager.java @@ -4,15 +4,19 @@ import com.jordanbunke.stipple_effect.project.SEContext; import com.jordanbunke.stipple_effect.utility.Constants; import com.jordanbunke.stipple_effect.utility.StatusUpdates; -import com.jordanbunke.stipple_effect.utility.settings.Settings; -import java.util.*; +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; +import java.util.Set; public class StateManager { private int index; private final List states; public StateManager(final ProjectState initialState) { + initialState.markAsCheckpoint(false); + this.states = new ArrayList<>(List.of(initialState)); index = 0; } @@ -77,8 +81,7 @@ public void performAction( while (states.size() > index + 1) states.remove(states.size() - 1); - if (Settings.isDumpStates()) - manageMemory(); + clearOldNonCheckpointStates(); // add to state stack and set as active state states.add(resultantState); @@ -87,43 +90,24 @@ public void performAction( updateStateMetadataAndAssets(resultantState); } - private void manageMemory() { - final long BYTES_IN_KB = 0x400L; // 1024 - final int GC_EVERY_X_DUMPS = 10; - - final Runtime r = Runtime.getRuntime(); - final long mem = r.freeMemory(); - - if (mem < Constants.DUMP_STATES_MEM_THRESHOLD && - canDumpState()) { - final long entryMem = mem / BYTES_IN_KB; - int dumped = 0; - - while (canDumpState() && - r.freeMemory() < Constants.DUMP_STATES_MEM_THRESHOLD * - Constants.DUMP_STATES_CUSHION_FACTOR) { - states.remove(0); - dumped++; - - if (dumped % GC_EVERY_X_DUMPS == 0) - System.gc(); - } + private void clearOldNonCheckpointStates() { + int checkpointsReached = 0; - System.gc(); - final long exitMem = r.freeMemory() / BYTES_IN_KB; + for (int i = states.size() - 1; i >= 0; i--) { + final boolean checkpoint = states.get(i).isCheckpoint(); - StatusUpdates.dumpedStates(dumped, exitMem - entryMem); + if (checkpoint) + checkpointsReached++; + else if (checkpointsReached >= Constants.CHECKPOINTS_DUMP_THRESHOLD) + states.remove(i); } } - private boolean canDumpState() { - return states.size() > Constants.MIN_NUM_STATES && index > 0; - } - private void updateStateMetadataAndAssets(final int was) { final int inc = (int) Math.signum(index - was); boolean edited = false, redrawSelectionOverlay = false, + updateOverlayOffset = false, redrawCanvasAuxiliaries = false; final Set triggeredActions = new HashSet<>(); @@ -135,6 +119,8 @@ private void updateStateMetadataAndAssets(final int was) { edited |= s.getOperation().constitutesEdit(); redrawSelectionOverlay |= s.getOperation() .triggersSelectionOverlayRedraw(); + updateOverlayOffset |= s.getOperation() + .triggersOverlayOffsetUpdate(); redrawCanvasAuxiliaries |= s.getOperation() .triggersCanvasAuxiliaryRedraw(); @@ -148,6 +134,8 @@ private void updateStateMetadataAndAssets(final int was) { if (redrawSelectionOverlay) c.redrawSelectionOverlay(); + else if (updateOverlayOffset) + c.updateOverlayOffset(); if (redrawCanvasAuxiliaries) c.redrawCanvasAuxiliaries(); @@ -173,6 +161,8 @@ private void markAsEditedIfEdited(final ProjectState state) { private void redrawSelectionOverlayIfTriggered(final ProjectState state) { if (state.getOperation().triggersSelectionOverlayRedraw()) StippleEffect.get().getContext().redrawSelectionOverlay(); + else if (state.getOperation().triggersOverlayOffsetUpdate()) + StippleEffect.get().getContext().updateOverlayOffset(); } private void redrawCanvasAuxiliariesIfTriggered(final ProjectState state) { @@ -181,6 +171,34 @@ private void redrawCanvasAuxiliariesIfTriggered(final ProjectState state) { } public ProjectState getState() { + return getState(index); + } + + public ProjectState getState(final int index) { return states.get(index); } + + public void setState(final int index, final SEContext c) { + this.index = index; + + c.projectInfo.markAsEdited(); + c.redrawSelectionOverlay(); + c.redrawCanvasAuxiliaries(); + + ActionType.MAJOR.consequence(); + } + + public int relativePosition(final int reference) { + return reference - index; + } + + public List getCheckpoints() { + final List checkpoints = new ArrayList<>(); + + for (int i = 0; i < states.size(); i++) + if (getState(i).isCheckpoint()) + checkpoints.add(i); + + return checkpoints; + } } diff --git a/src/com/jordanbunke/stipple_effect/stip/ParserSerializer.java b/src/com/jordanbunke/stipple_effect/stip/ParserSerializer.java index b8f4fa1f..5f0a1a3d 100644 --- a/src/com/jordanbunke/stipple_effect/stip/ParserSerializer.java +++ b/src/com/jordanbunke/stipple_effect/stip/ParserSerializer.java @@ -43,6 +43,7 @@ public class ParserSerializer { LAYER_ONION_SKIN_TAG = "onion_skin", LAYER_OPACITY_TAG = "opacity", FRAME_COUNT_TAG = "frame_count", + FRAME_DURATIONS_TAG = "frame_durations", FRAMES_TAG = "frames", LINKED_LAYER_TAG = "linked_layer", FRAME_TAG = "frame", @@ -135,6 +136,7 @@ private static ProjectState deserializeProjectState(final String contents) { final List layers = new ArrayList<>(); int frameCount = 1, w = 1, h = 1; + List frameDurations = ProjectState.defaultFrameDurations(frameCount); double fileStandard = FS_INITIAL; for (SerialBlock block : stateBlocks) { @@ -151,6 +153,14 @@ private static ProjectState deserializeProjectState(final String contents) { fileStandard = Double.parseDouble(block.value()); case FRAME_COUNT_TAG -> frameCount = Integer.parseInt(block.value()); + case FRAME_DURATIONS_TAG -> { + final String[] vals = block.value().split(CONTENT_SEPARATOR); + + frameDurations = new ArrayList<>(); + + for (String val : vals) + frameDurations.add(Double.parseDouble(val)); + } case LAYERS_TAG -> { final SerialBlock[] layerBlocks = deserializeBlocksAtDepthLevel(block.value()); @@ -162,7 +172,7 @@ private static ProjectState deserializeProjectState(final String contents) { } } - return ProjectState.makeFromNativeFile(w, h, layers, frameCount); + return ProjectState.makeFromNativeFile(w, h, layers, frameCount, frameDurations); } private static SELayer deserializeLayer( @@ -346,6 +356,12 @@ private static String serializeProjectState(final ProjectState state) { final int w = state.getImageWidth(), h = state.getImageHeight(), frameCount = state.getFrameCount(); + final List frameDurations = state.getFrameDurations(); + final String durationsText = frameDurations.size() == 1 + ? String.valueOf(frameDurations.get(0)) + : frameDurations.stream().map(String::valueOf) + .reduce((a, b) -> a + CONTENT_SEPARATOR + b) + .orElse("1.0"); // dims definition openWithTag(sb, DIMENSION_TAG).append(w).append(CONTENT_SEPARATOR) @@ -355,6 +371,10 @@ private static String serializeProjectState(final ProjectState state) { openWithTag(sb, FRAME_COUNT_TAG).append(frameCount) .append(ENCLOSER_CLOSE).append(NL); + // frame durations definition + openWithTag(sb, FRAME_DURATIONS_TAG).append(durationsText) + .append(ENCLOSER_CLOSE).append(NL); + // layers tag opener openWithTag(sb, LAYERS_TAG).append(NL); diff --git a/src/com/jordanbunke/stipple_effect/tools/AbstractBrush.java b/src/com/jordanbunke/stipple_effect/tools/AbstractBrush.java new file mode 100644 index 00000000..9b482838 --- /dev/null +++ b/src/com/jordanbunke/stipple_effect/tools/AbstractBrush.java @@ -0,0 +1,109 @@ +package com.jordanbunke.stipple_effect.tools; + +import com.jordanbunke.delta_time.events.GameMouseEvent; +import com.jordanbunke.delta_time.image.GameImage; +import com.jordanbunke.delta_time.utility.math.Coord2D; +import com.jordanbunke.stipple_effect.StippleEffect; +import com.jordanbunke.stipple_effect.project.SEContext; +import com.jordanbunke.stipple_effect.selection.Selection; +import com.jordanbunke.stipple_effect.utility.Constants; + +import java.awt.*; +import java.util.HashSet; +import java.util.Set; + +public sealed abstract class AbstractBrush + extends ToolWithBreadth + permits Brush, ShadeBrush, ScriptBrush { + private boolean painting; + private Set painted; + + AbstractBrush() { + painting = false; + painted = new HashSet<>(); + } + + @Override + public void onMouseDown(final SEContext context, final GameMouseEvent me) { + if (StippleEffect.get().hasPaletteContents() && + !context.getTargetPixel().equals(Constants.NO_VALID_TARGET) && + me.button != GameMouseEvent.Button.MIDDLE) { + painting = true; + setColorGetter(context, me); + painted = new HashSet<>(); + + reset(); + context.getState().markAsCheckpoint(false); + } + } + + abstract void setColorGetter(final SEContext context, final GameMouseEvent me); + + @Override + public void update(final SEContext context, final Coord2D mousePosition) { + final Coord2D tp = context.getTargetPixel(); + + if (painting && !tp.equals(Constants.NO_VALID_TARGET)) { + final int w = context.getState().getImageWidth(), + h = context.getState().getImageHeight(); + final Selection selection = context.getState().getSelection(); + + if (isUnchanged(context)) + return; + + if (!stillSameFrame(context)) + painted.clear(); + + final GameImage edit = new GameImage(w, h), + current = context.getState().getActiveLayerFrame(); + populateAround(edit, current, tp, selection, w, h); + + fillLineSpace(getLastTP(), tp, (x, y) -> populateAround(edit, + current, getLastTP().displace(x, y), selection, w, h)); + + context.paintOverImage(edit.submit()); + updateLast(context); + } + } + + private void populateAround( + final GameImage edit, final GameImage current, + final Coord2D tp, final Selection selection, + final int w, final int h + ) { + final int halfB = breadthOffset(); + final boolean[][] mask = breadthMask(); + final boolean empty = !selection.hasSelection(); + + for (int x = 0; x < mask.length; x++) + for (int y = 0; y < mask[x].length; y++) { + final Coord2D b = new Coord2D(x + (tp.x - halfB), + y + (tp.y - halfB)); + + if (b.x < 0 || b.x >= w || b.y < 0 || b.y >= h) + continue; + + if (!(empty || selection.selected(b))) + continue; + + final Color existing = current.getColorAt(b.x, b.y); + if (mask[x][y] && !painted.contains(b) && + paintCondition(existing, b.x, b.y)) { + edit.dot(getColor(existing, b.x, b.y), b.x, b.y); + painted.add(b); + } + } + } + + abstract boolean paintCondition(final Color existing, final int x, final int y); + abstract Color getColor(final Color existing, final int x, final int y); + + @Override + public void onMouseUp(final SEContext context, final GameMouseEvent me) { + if (painting) { + painting = false; + context.getState().markAsCheckpoint(true); + me.markAsProcessed(); + } + } +} diff --git a/src/com/jordanbunke/stipple_effect/tools/BoxSelect.java b/src/com/jordanbunke/stipple_effect/tools/BoxSelect.java index e5eafb3f..a3e115a4 100644 --- a/src/com/jordanbunke/stipple_effect/tools/BoxSelect.java +++ b/src/com/jordanbunke/stipple_effect/tools/BoxSelect.java @@ -5,6 +5,7 @@ import com.jordanbunke.delta_time.utility.math.Coord2D; import com.jordanbunke.stipple_effect.StippleEffect; import com.jordanbunke.stipple_effect.project.SEContext; +import com.jordanbunke.stipple_effect.selection.Selection; import com.jordanbunke.stipple_effect.selection.SelectionUtils; import com.jordanbunke.stipple_effect.utility.Constants; import com.jordanbunke.stipple_effect.utility.settings.Settings; @@ -72,12 +73,10 @@ public void onMouseDown(final SEContext context, final GameMouseEvent me) { if (ToolWithMode.getMode() == Mode.SINGLE) context.deselect(false); - final Set bounds = new HashSet<>(Set.of(tp)); - pivotTP = tp; endTP = tp; topLeft = tp; - bottomRight = SelectionUtils.bottomRight(bounds); + bottomRight = tp.displace(1, 1); updateToolContentPreview(context); } @@ -107,11 +106,6 @@ public void update(final SEContext context, final Coord2D mousePosition) { bottomRight = snapToClosestGridPosition(context, SelectionUtils.bottomRight(bounds), false); - bounds.clear(); - for (int x = topLeft.x; x < bottomRight.x; x++) - for (int y = topLeft.y; y < bottomRight.y; y++) - bounds.add(new Coord2D(x, y)); - updateToolContentPreview(context); } } @@ -125,13 +119,19 @@ public void onMouseUp(final SEContext context, final GameMouseEvent me) { final int w = context.getState().getImageWidth(), h = context.getState().getImageHeight(); - final Set box = new HashSet<>(); + final int mw = bottomRight.x - topLeft.x, + mh = bottomRight.y - topLeft.y; + final boolean[][] matrix = new boolean[mw][mh]; + + for (int x = 0; x < mw; x++) + for (int y = 0; y < mh; y++) { + final int px = topLeft.x + x, py = topLeft.y + y; - for (int x = topLeft.x; x < bottomRight.x; x++) - for (int y = topLeft.y; y < bottomRight.y; y++) - if (x >= 0 && x < w && y >= 0 && y < h) - box.add(new Coord2D(x, y)); + if (px >= 0 && px < w && py >= 0 && py < h) + matrix[x][y] = true; + } + final Selection box = Selection.atOf(topLeft, matrix); context.editSelection(box, true); } } diff --git a/src/com/jordanbunke/stipple_effect/tools/BreadthTool.java b/src/com/jordanbunke/stipple_effect/tools/BreadthTool.java index 6dfa5f1b..6be21d7c 100644 --- a/src/com/jordanbunke/stipple_effect/tools/BreadthTool.java +++ b/src/com/jordanbunke/stipple_effect/tools/BreadthTool.java @@ -8,24 +8,23 @@ default int breadthOffset() { } default boolean[][] breadthMask() { - final int remainder = getBreadth() % 2, halfB = breadthOffset(); + final int b = getBreadth(), + remainder = b % 2, halfB = breadthOffset(); final boolean[][] mask = new boolean[halfB * 2][halfB * 2]; + final boolean odd = remainder == 1; + // 3 is a special case + final double threshold = odd ? (b == 3 ? 1. : b / 2.) : (double) b; + for (int x = 0; x < halfB * 2; x++) for (int y = 0; y < halfB * 2; y++) { - final double distance = remainder == 1 + final double distance = odd ? Coord2D.unitDistanceBetween(new Coord2D(x, y), new Coord2D(halfB, halfB)) : Coord2D.unitDistanceBetween( new Coord2D((2 * x) + 1, (2 * y) + 1), new Coord2D(halfB * 2, halfB * 2)); - double threshold = remainder == 1 - ? getBreadth() / 2. : (double) getBreadth(); - - // special case - if (getBreadth() == 3) - threshold = Math.floor(threshold); if (distance <= threshold) mask[x][y] = true; diff --git a/src/com/jordanbunke/stipple_effect/tools/Brush.java b/src/com/jordanbunke/stipple_effect/tools/Brush.java index 4980bc3b..8f78541d 100644 --- a/src/com/jordanbunke/stipple_effect/tools/Brush.java +++ b/src/com/jordanbunke/stipple_effect/tools/Brush.java @@ -1,32 +1,23 @@ package com.jordanbunke.stipple_effect.tools; import com.jordanbunke.delta_time.events.GameMouseEvent; -import com.jordanbunke.delta_time.image.GameImage; -import com.jordanbunke.delta_time.utility.math.Coord2D; import com.jordanbunke.stipple_effect.project.SEContext; -import com.jordanbunke.stipple_effect.utility.Constants; import com.jordanbunke.stipple_effect.visual.theme.SEColors; import java.awt.*; -import java.util.HashSet; -import java.util.Set; import java.util.function.BiFunction; -public final class Brush extends ToolWithBreadth { +public final class Brush extends AbstractBrush { private static final Brush INSTANCE; - private boolean painting; private BiFunction c; - private Set painted; static { INSTANCE = new Brush(); } private Brush() { - painting = false; c = (x, y) -> SEColors.def(); - painted = new HashSet<>(); } public static Brush get() { @@ -39,78 +30,17 @@ public String getName() { } @Override - public void onMouseDown( - final SEContext context, final GameMouseEvent me - ) { - if (!context.getTargetPixel().equals(Constants.NO_VALID_TARGET) && - me.button != GameMouseEvent.Button.MIDDLE) { - painting = true; - c = ToolThatDraws.getColorMode(me); - painted = new HashSet<>(); - - reset(); - context.getState().markAsCheckpoint(false); - } + void setColorGetter(final SEContext context, final GameMouseEvent me) { + c = ToolThatDraws.getColorMode(me); } @Override - public void update( - final SEContext context, final Coord2D mousePosition - ) { - final Coord2D tp = context.getTargetPixel(); - - if (painting && !tp.equals(Constants.NO_VALID_TARGET)) { - final int w = context.getState().getImageWidth(), - h = context.getState().getImageHeight(); - final Set selection = context.getState().getSelection(); - - if (isUnchanged(context)) - return; - - if (!stillSameFrame(context)) - painted.clear(); - - final GameImage edit = new GameImage(w, h); - populateAround(edit, tp, selection); - - fillLineSpace(getLastTP(), tp, (x, y) -> populateAround( - edit, getLastTP().displace(x, y), selection)); - - context.paintOverImage(edit.submit()); - updateLast(context); - } - } - - private void populateAround( - final GameImage edit, final Coord2D tp, - final Set selection - ) { - final int halfB = breadthOffset(); - final boolean[][] mask = breadthMask(); - - for (int x = 0; x < mask.length; x++) - for (int y = 0; y < mask[x].length; y++) { - final Coord2D b = new Coord2D(x + (tp.x - halfB), - y + (tp.y - halfB)); - - if (!(selection.isEmpty() || selection.contains(b))) - continue; - - if (mask[x][y] && !painted.contains(b)) { - edit.dot(c.apply(b.x, b.y), b.x, b.y); - painted.add(b); - } - } + boolean paintCondition(final Color existing, final int x, final int y) { + return true; } @Override - public void onMouseUp( - final SEContext context, final GameMouseEvent me - ) { - if (painting) { - painting = false; - context.getState().markAsCheckpoint(true); - me.markAsProcessed(); - } + Color getColor(final Color existing, final int x, final int y) { + return c.apply(x, y); } } diff --git a/src/com/jordanbunke/stipple_effect/tools/BrushSelect.java b/src/com/jordanbunke/stipple_effect/tools/BrushSelect.java index d7479b81..15d7a410 100644 --- a/src/com/jordanbunke/stipple_effect/tools/BrushSelect.java +++ b/src/com/jordanbunke/stipple_effect/tools/BrushSelect.java @@ -4,19 +4,15 @@ import com.jordanbunke.delta_time.image.GameImage; import com.jordanbunke.delta_time.utility.math.Coord2D; import com.jordanbunke.stipple_effect.project.SEContext; -import com.jordanbunke.stipple_effect.selection.SelectionUtils; +import com.jordanbunke.stipple_effect.selection.Selection; import com.jordanbunke.stipple_effect.utility.Constants; import com.jordanbunke.stipple_effect.visual.GraphicsUtils; -import java.util.HashSet; -import java.util.Set; -import java.util.stream.Collectors; - public final class BrushSelect extends ToolWithBreadth implements OverlayTool { private static final BrushSelect INSTANCE; private boolean selecting; - private Set selection; + private boolean[][] pixels; private GameImage selectionOverlay; static { @@ -25,7 +21,7 @@ public final class BrushSelect extends ToolWithBreadth implements OverlayTool { private BrushSelect() { selecting = false; - selection = new HashSet<>(); + pixels = new boolean[0][]; selectionOverlay = GameImage.dummy(); } @@ -46,20 +42,23 @@ public String getName() { private void drawSelection(final SEContext context) { selectionOverlay = GraphicsUtils.drawSelectionOverlay( - context.renderInfo.getZoomFactor(), selection, false, false); + pixels.length, pixels.length == 0 ? 0 : pixels[0].length, + context.renderInfo.getZoomFactor(), pixels); } @Override public void onMouseDown(final SEContext context, final GameMouseEvent me) { if (!context.getTargetPixel().equals(Constants.NO_VALID_TARGET)) { + final int w = context.getState().getImageWidth(), + h = context.getState().getImageHeight(); + selecting = true; - selection = new HashSet<>(); + pixels = new boolean[w][h]; if (ToolWithMode.getMode() == ToolWithMode.Mode.SINGLE) context.deselect(false); reset(); - context.getState().markAsCheckpoint(false); } selectionOverlay = GameImage.dummy(); @@ -88,13 +87,17 @@ private void populateAround(final Coord2D tp) { final boolean[][] mask = breadthMask(); for (int x = 0; x < mask.length; x++) - for (int y = 0; y < mask[x].length; y++) { - final Coord2D b = new Coord2D(x + (tp.x - halfB), - y + (tp.y - halfB)); + for (int y = 0; y < mask[x].length; y++) + if (mask[x][y]) { + final int px = x + (tp.x - halfB), + py = y + (tp.y - halfB); - if (mask[x][y]) - selection.add(b); - } + if (inBounds(px, py)) pixels[px][py] = true; + } + } + + private boolean inBounds(final int x, final int y) { + return x >= 0 && y >= 0 && x < pixels.length && y < pixels[x].length; } @Override @@ -103,23 +106,10 @@ public void onMouseUp(final SEContext context, final GameMouseEvent me) { selecting = false; me.markAsProcessed(); - // filter selection by image bounds - final int w = context.getState().getImageWidth(), - h = context.getState().getImageHeight(); - - final Set bounded = selection.stream() - .filter(p -> p.x >= 0 && p.y >= 0 && p.x < w && p.y < h) - .collect(Collectors.toSet()); - - context.editSelection(bounded, true); + context.editSelection(Selection.of(pixels), true); } } - @Override - public Coord2D getTopLeft() { - return SelectionUtils.topLeft(selection); - } - @Override public GameImage getSelectionOverlay() { return selectionOverlay; diff --git a/src/com/jordanbunke/stipple_effect/tools/Eraser.java b/src/com/jordanbunke/stipple_effect/tools/Eraser.java index 379548aa..6d554f91 100644 --- a/src/com/jordanbunke/stipple_effect/tools/Eraser.java +++ b/src/com/jordanbunke/stipple_effect/tools/Eraser.java @@ -3,10 +3,9 @@ import com.jordanbunke.delta_time.events.GameMouseEvent; import com.jordanbunke.delta_time.utility.math.Coord2D; import com.jordanbunke.stipple_effect.project.SEContext; +import com.jordanbunke.stipple_effect.selection.Selection; import com.jordanbunke.stipple_effect.utility.Constants; -import java.util.Set; - public final class Eraser extends ToolWithBreadth { private static final Eraser INSTANCE; @@ -45,7 +44,7 @@ public void update(final SEContext context, final Coord2D mousePosition) { if (erasing && !tp.equals(Constants.NO_VALID_TARGET)) { final int w = context.getState().getImageWidth(), h = context.getState().getImageHeight(); - final Set selection = context.getState().getSelection(); + final Selection selection = context.getState().getSelection(); if (isUnchanged(context)) return; @@ -62,17 +61,18 @@ public void update(final SEContext context, final Coord2D mousePosition) { } private void populateAround( - final boolean[][] eraseMask, final Coord2D tp, Set selection + final boolean[][] eraseMask, final Coord2D tp, Selection selection ) { final int halfB = breadthOffset(); final boolean[][] mask = breadthMask(); + final boolean empty = !selection.hasSelection(); for (int x = 0; x < mask.length; x++) for (int y = 0; y < mask[x].length; y++) { final Coord2D e = new Coord2D(x + (tp.x - halfB), y + (tp.y - halfB)); - if (!(selection.isEmpty() || selection.contains(e))) + if (!(empty || selection.selected(e))) continue; if (mask[x][y] && e.x >= 0 && e.y >= 0 && diff --git a/src/com/jordanbunke/stipple_effect/tools/Fill.java b/src/com/jordanbunke/stipple_effect/tools/Fill.java index 42de33d7..49ee3a3b 100644 --- a/src/com/jordanbunke/stipple_effect/tools/Fill.java +++ b/src/com/jordanbunke/stipple_effect/tools/Fill.java @@ -5,10 +5,9 @@ import com.jordanbunke.delta_time.utility.math.Coord2D; import com.jordanbunke.stipple_effect.StippleEffect; import com.jordanbunke.stipple_effect.project.SEContext; +import com.jordanbunke.stipple_effect.selection.Selection; import java.awt.*; -import java.util.Set; -import java.util.stream.Collectors; public final class Fill extends ToolThatSearches { private static final Fill INSTANCE; @@ -44,18 +43,19 @@ public void onMouseDown(final SEContext context, final GameMouseEvent me) { : StippleEffect.get().getSecondary(); // search - final Set selection = context.getState().getSelection(), - matched = search(image, tp).stream() - .filter(m -> selection.isEmpty() || selection.contains(m)) - .collect(Collectors.toSet()); + final Selection selection = context.getState().getSelection(), + matched = search(image, tp), + filtered = selection.hasSelection() + ? Selection.intersection(selection, matched) + : matched; // assemble edit mask final GameImage edit = new GameImage(w, h); final int rgb = fillColor.getRGB(); - matched.forEach(m -> edit.setRGB(m.x, m.y, rgb)); + filtered.pixelAlgorithm(w, h, (x, y) -> edit.setRGB(x, y, rgb)); - context.stampImage(edit.submit(), matched); + context.stampImage(edit.submit(), filtered); context.getState().markAsCheckpoint(true); } } diff --git a/src/com/jordanbunke/stipple_effect/tools/GeometryTool.java b/src/com/jordanbunke/stipple_effect/tools/GeometryTool.java new file mode 100644 index 00000000..3075ed61 --- /dev/null +++ b/src/com/jordanbunke/stipple_effect/tools/GeometryTool.java @@ -0,0 +1,132 @@ +package com.jordanbunke.stipple_effect.tools; + +import com.jordanbunke.delta_time.events.GameMouseEvent; +import com.jordanbunke.delta_time.image.GameImage; +import com.jordanbunke.delta_time.utility.math.Coord2D; +import com.jordanbunke.stipple_effect.StippleEffect; +import com.jordanbunke.stipple_effect.project.SEContext; +import com.jordanbunke.stipple_effect.selection.Selection; +import com.jordanbunke.stipple_effect.utility.Constants; +import com.jordanbunke.stipple_effect.visual.theme.SEColors; + +import java.awt.*; +import java.util.HashSet; +import java.util.Set; + +public sealed abstract class GeometryTool extends ToolWithBreadth + implements SnappableTool permits LineTool, ShapeTool { + private boolean drawing, snap; + private Color c; + private GameImage toolContentPreview; + private Coord2D anchor; + private Set included; + + protected GeometryTool() { + drawing = false; + snap = false; + c = SEColors.def(); + toolContentPreview = GameImage.dummy(); + + anchor = Constants.NO_VALID_TARGET; + included = new HashSet<>(); + } + + @Override + public void onMouseDown(final SEContext context, final GameMouseEvent me) { + final Coord2D tp = context.getTargetPixel(); + + if (!tp.equals(Constants.NO_VALID_TARGET) && + me.button != GameMouseEvent.Button.MIDDLE) { + drawing = true; + c = me.button == GameMouseEvent.Button.LEFT + ? StippleEffect.get().getPrimary() + : StippleEffect.get().getSecondary(); + toolContentPreview = GameImage.dummy(); + + reset(); + anchor = tp; + included = new HashSet<>(); + } + } + + @Override + public void update(final SEContext context, final Coord2D mousePosition) { + final Coord2D tp = context.getTargetPixel(); + + if (drawing && !tp.equals(Constants.NO_VALID_TARGET)) { + final int w = context.getState().getImageWidth(), + h = context.getState().getImageHeight(); + final Selection selection = context.getState().getSelection(); + + if (tp.equals(getLastTP())) + return; + + toolContentPreview = new GameImage(w, h); + included = new HashSet<>(); + + final Coord2D endpoint = isSnap() + ? snappedEndpoint(anchor, tp) : tp; + geoDefinition(anchor, endpoint, selection); + + updateLast(context); + } + } + + abstract Coord2D snappedEndpoint(final Coord2D anchor, final Coord2D tp); + abstract void geoDefinition(final Coord2D anchor, final Coord2D endpoint, + final Selection selection); + + @Override + public void onMouseUp(final SEContext context, final GameMouseEvent me) { + if (drawing) { + drawing = false; + + context.paintOverImage(toolContentPreview); + + context.getState().markAsCheckpoint(true); + me.markAsProcessed(); + } + } + + protected void populateAround( + final Coord2D tp, final Selection selection + ) { + final int halfB = breadthOffset(); + final boolean[][] mask = breadthMask(); + final boolean empty = !selection.hasSelection(); + + for (int x = 0; x < mask.length; x++) + for (int y = 0; y < mask[x].length; y++) { + final Coord2D b = new Coord2D(x + (tp.x - halfB), + y + (tp.y - halfB)); + + if (!(empty || selection.selected(b))) + continue; + + if (mask[x][y] && !included.contains(b)) { + toolContentPreview.dot(c, b.x, b.y); + included.add(b); + } + } + } + + @Override + public void setSnap(final boolean snap) { + this.snap = snap; + } + + @Override + public boolean hasToolContentPreview() { + return drawing; + } + + @Override + public GameImage getToolContentPreview() { + return toolContentPreview; + } + + @Override + public boolean isSnap() { + return snap; + } +} diff --git a/src/com/jordanbunke/stipple_effect/tools/GradientTool.java b/src/com/jordanbunke/stipple_effect/tools/GradientTool.java index c9ae0374..a08e6045 100644 --- a/src/com/jordanbunke/stipple_effect/tools/GradientTool.java +++ b/src/com/jordanbunke/stipple_effect/tools/GradientTool.java @@ -9,6 +9,7 @@ import com.jordanbunke.funke.core.ConcreteProperty; import com.jordanbunke.stipple_effect.StippleEffect; import com.jordanbunke.stipple_effect.project.SEContext; +import com.jordanbunke.stipple_effect.selection.Selection; import com.jordanbunke.stipple_effect.utility.Constants; import com.jordanbunke.stipple_effect.utility.EnumUtils; import com.jordanbunke.stipple_effect.utility.Layout; @@ -36,7 +37,7 @@ public final class GradientTool extends ToolWithBreadth private Coord2D anchor; private List> gradientStages; private Shape shape; - private Set accessible; + private Selection accessible; public enum Shape { LINEAR, RADIAL, SPIRAL; @@ -66,7 +67,7 @@ private GradientTool() { shape = Shape.LINEAR; - accessible = new HashSet<>(); + accessible = Selection.EMPTY; toolContentPreview = GameImage.dummy(); @@ -108,7 +109,8 @@ private void initializeMask(final SEContext context) { final Color maskColor = masked && anchorInBounds(w, h) ? frame.getColorAt(anchor.x, anchor.y) : null; - accessible = maskColor != null ? search(frame, maskColor) : new HashSet<>(); + accessible = maskColor != null + ? search(frame, maskColor) : Selection.EMPTY; } @Override @@ -137,9 +139,8 @@ private void updateBrushMode( final SEContext context, final Coord2D tp, final int w, final int h ) { - final Set - selection = context.getState().getSelection(), - gradientStage = new HashSet<>(); + final Selection selection = context.getState().getSelection(); + final Set gradientStage = new HashSet<>(); populateAround(gradientStage, tp, selection); fillLineSpace(getLastTP(), tp, (x, y) -> populateAround( @@ -170,17 +171,18 @@ private void updateGlobalMode( private void populateAround( final Set gradientStage, final Coord2D tp, - final Set selection + final Selection selection ) { final int halfB = breadthOffset(); final boolean[][] mask = breadthMask(); + final boolean empty = !selection.hasSelection(); for (int x = 0; x < mask.length; x++) for (int y = 0; y < mask[x].length; y++) { final Coord2D b = new Coord2D(x + (tp.x - halfB), y + (tp.y - halfB)); - if (!(selection.isEmpty() || selection.contains(b))) + if (!(empty || selection.selected(b))) continue; if (mask[x][y]) @@ -192,20 +194,19 @@ private void drawGlobalGradient( final Coord2D endpoint, final SEContext context, final int w, final int h ) { - final Set selection = context.getState().getSelection(); - final boolean hasSelection = !selection.isEmpty(); + final Selection selection = context.getState().getSelection(); + final boolean hasSelection = selection.hasSelection(); for (int x = 0; x < w; x++) { for (int y = 0; y < h; y++) { final Coord2D pos = new Coord2D(x, y); - if (hasSelection && !selection.contains(pos)) + if (hasSelection && !selection.selected(pos)) continue; final double c = shape.cGetter().apply(pos, endpoint); final boolean satisfiesMask = !masked || - (anchorInBounds(w, h) && - accessible.contains(new Coord2D(x, y))), + (anchorInBounds(w, h) && accessible.selected(x, y)), satisfiesScope = isCValid(c), replacePixel = satisfiesMask && satisfiesScope; @@ -272,7 +273,7 @@ private boolean anchorInBounds( anchor.y >= 0 && anchor.y < h; } - private Set search( + private Selection search( final GameImage frame, final Color maskColor ) { return contiguous @@ -404,7 +405,7 @@ public MenuElementGrouping buildToolOptionsBar() { // dithered label final TextLabel ditheredLabel = TextLabel.make( - new Coord2D(getDitherTextX(), Layout.optionsBarTextY()), + new Coord2D(getAfterBreadthTextX(), Layout.optionsBarTextY()), "Dithered"); // dithered checkbox diff --git a/src/com/jordanbunke/stipple_effect/tools/LineTool.java b/src/com/jordanbunke/stipple_effect/tools/LineTool.java index fe4e7438..09004a37 100644 --- a/src/com/jordanbunke/stipple_effect/tools/LineTool.java +++ b/src/com/jordanbunke/stipple_effect/tools/LineTool.java @@ -1,39 +1,19 @@ package com.jordanbunke.stipple_effect.tools; -import com.jordanbunke.delta_time.events.GameMouseEvent; -import com.jordanbunke.delta_time.image.GameImage; import com.jordanbunke.delta_time.utility.math.Coord2D; -import com.jordanbunke.stipple_effect.StippleEffect; -import com.jordanbunke.stipple_effect.project.SEContext; +import com.jordanbunke.stipple_effect.selection.Selection; import com.jordanbunke.stipple_effect.utility.Constants; import com.jordanbunke.stipple_effect.utility.math.Geometry; -import com.jordanbunke.stipple_effect.visual.theme.SEColors; -import java.awt.*; -import java.util.HashSet; -import java.util.Set; - -public final class LineTool extends ToolWithBreadth implements SnappableTool { +public final class LineTool extends GeometryTool implements SnappableTool { private static final LineTool INSTANCE; - private boolean drawing, snap; - private Color c; - private GameImage toolContentPreview; - private Coord2D anchor; - private Set included; - static { INSTANCE = new LineTool(); } private LineTool() { - drawing = false; - snap = false; - c = SEColors.def(); - toolContentPreview = GameImage.dummy(); - - anchor = Constants.NO_VALID_TARGET; - included = new HashSet<>(); + super(); } public static LineTool get() { @@ -41,120 +21,29 @@ public static LineTool get() { } @Override - public void onMouseDown(final SEContext context, final GameMouseEvent me) { - final Coord2D tp = context.getTargetPixel(); - - if (!tp.equals(Constants.NO_VALID_TARGET) && - me.button != GameMouseEvent.Button.MIDDLE) { - drawing = true; - c = me.button == GameMouseEvent.Button.LEFT - ? StippleEffect.get().getPrimary() - : StippleEffect.get().getSecondary(); - toolContentPreview = GameImage.dummy(); - - reset(); - anchor = tp; - included = new HashSet<>(); - } - } - - @Override - public void update(final SEContext context, final Coord2D mousePosition) { - final Coord2D tp = context.getTargetPixel(); - - if (drawing && !tp.equals(Constants.NO_VALID_TARGET)) { - final int w = context.getState().getImageWidth(), - h = context.getState().getImageHeight(); - final Set selection = context.getState().getSelection(); + Coord2D snappedEndpoint(final Coord2D anchor, final Coord2D tp) { + final double angle = Geometry.normalizeAngle( + Geometry.calculateAngleInRad(tp, anchor)), + distance = Coord2D.unitDistanceBetween(anchor, tp), + snapped = Geometry.snapAngle(angle, Constants._15_SNAP_INC); - if (tp.equals(getLastTP())) - return; - - toolContentPreview = new GameImage(w, h); - included = new HashSet<>(); - - final Coord2D endpoint; - - if (isSnap()) { - final double angle = Geometry.normalizeAngle( - Geometry.calculateAngleInRad(tp, anchor)), - distance = Coord2D.unitDistanceBetween(anchor, tp), - snapped = Geometry.snapAngle(angle, Constants._15_SNAP_INC); - endpoint = Geometry.projectPoint(anchor, snapped, distance); - } else - endpoint = tp; - - populateAround(toolContentPreview, anchor, selection); - populateAround(toolContentPreview, endpoint, selection); - - fillLineSpace(anchor, endpoint, (x, y) -> populateAround( - toolContentPreview, anchor.displace(x, y), selection)); - - updateLast(context); - } + return Geometry.projectPoint(anchor, snapped, distance); } @Override - public void onMouseUp(final SEContext context, final GameMouseEvent me) { - if (drawing) { - drawing = false; - - context.paintOverImage(toolContentPreview); - - context.getState().markAsCheckpoint(true); - me.markAsProcessed(); - } - } - - private void populateAround( - final GameImage edit, final Coord2D tp, - final Set selection + void geoDefinition( + final Coord2D anchor, final Coord2D endpoint, + final Selection selection ) { - final int halfB = breadthOffset(); - final boolean[][] mask = breadthMask(); - - for (int x = 0; x < mask.length; x++) - for (int y = 0; y < mask[x].length; y++) { - final Coord2D b = new Coord2D(x + (tp.x - halfB), - y + (tp.y - halfB)); - - if (!(selection.isEmpty() || selection.contains(b))) - continue; - - if (mask[x][y] && !included.contains(b)) { - edit.dot(c, b.x, b.y); - included.add(b); - } - } - } - - @Override - public void setSnap(final boolean snap) { - this.snap = snap; - } - - @Override - public boolean hasToolContentPreview() { - return drawing; - } + populateAround(anchor, selection); + populateAround(endpoint, selection); - @Override - public GameImage getToolContentPreview() { - return toolContentPreview; - } - - @Override - public boolean isSnap() { - return snap; + fillLineSpace(anchor, endpoint, (x, y) -> + populateAround(anchor.displace(x, y), selection)); } @Override public String getName() { return "Line Tool"; } - - @Override - public String getBottomBarText() { - return getName() + " (" + getBreadth() + " px)"; - } } diff --git a/src/com/jordanbunke/stipple_effect/tools/MoveSelection.java b/src/com/jordanbunke/stipple_effect/tools/MoveSelection.java index 721a4e44..46c27159 100644 --- a/src/com/jordanbunke/stipple_effect/tools/MoveSelection.java +++ b/src/com/jordanbunke/stipple_effect/tools/MoveSelection.java @@ -1,13 +1,21 @@ package com.jordanbunke.stipple_effect.tools; +import com.jordanbunke.delta_time.image.GameImage; import com.jordanbunke.delta_time.utility.math.Coord2D; import com.jordanbunke.stipple_effect.project.SEContext; -import com.jordanbunke.stipple_effect.selection.RotateFunction; -import com.jordanbunke.stipple_effect.selection.StretcherFunction; +import com.jordanbunke.stipple_effect.selection.Selection; +import com.jordanbunke.stipple_effect.selection.SelectionMode; +import com.jordanbunke.stipple_effect.selection.SelectionUtils; +import com.jordanbunke.stipple_effect.state.Operation; +import com.jordanbunke.stipple_effect.state.ProjectState; +import com.jordanbunke.stipple_effect.utility.settings.Settings; +import com.jordanbunke.stipple_effect.visual.theme.Theme; -import java.util.function.BiConsumer; +import java.awt.*; +import java.util.HashSet; +import java.util.Set; -public final class MoveSelection extends MoverTool { +public final class MoveSelection extends MoverTool { private static final MoveSelection INSTANCE; static { @@ -23,24 +31,85 @@ public String getName() { return "Move selection"; } + @Override + boolean canBeMoved(final SEContext context) { + return context.getState().hasSelection() && + context.getState().getSelectionMode() == SelectionMode.BOUNDS; + } + + @Override + Selection move( + final SEContext context, final Coord2D displacement + ) { + return context.getState().getSelection().displace(displacement); + } + + @Override + Selection stretch( + final SEContext context, final Selection initial, + final Coord2D change, final Direction direction + ) { + return SelectionUtils.stretchedPixels(initial, change, direction); + } @Override - public BiConsumer getMoverFunction(final SEContext context) { - return context::moveSelectionBounds; + Selection rotate( + final SEContext context, final Selection initial, + final double deltaR, final Coord2D pivot, final boolean[] offset + ) { + return SelectionUtils.rotatedPixels(initial, deltaR, pivot, offset); } @Override - StretcherFunction getStretcherFunction(final SEContext context) { - return context::stretchSelectionBounds; + void applyTransformation( + final SEContext context, final Selection selection, + final boolean transform + ) { + final ProjectState result = context.getState() + .changeSelectionBounds(selection); + context.getStateManager().performAction(result, transform + ? Operation.TRANSFORM_SELECTION_BOUNDS + : Operation.MOVE_SELECTION_BOUNDS); } @Override - RotateFunction getRotateFunction(final SEContext context) { - return context::rotateSelectionBounds; + GameImage updateToolContentPreview( + final SEContext context, final Selection transformation + ) { + final int w = context.getState().getImageWidth(), + h = context.getState().getImageHeight(); + + final GameImage toolContentPreview = new GameImage(w, h); + + final Set frontier = new HashSet<>(); + + transformation.pixelAlgorithm(w, h, (x, y) -> { + final boolean + left = !transformation.selected(x - 1, y), + right = !transformation.selected(x + 1, y), + top = !transformation.selected(x, y - 1), + bottom = !transformation.selected(x, y + 1), + tl = !transformation.selected(x - 1, y - 1), + tr = !transformation.selected(x + 1, y - 1), + bl = !transformation.selected(x - 1, y + 1), + br = !transformation.selected(x + 1, y + 1); + + if (left || right || top || bottom || tl || tr || bl || br) + frontier.add(new Coord2D(x, y)); + }); + + final Theme t = Settings.getTheme(); + final Color fill = t.highlightOverlay, outline = t.highlightOutline; + + transformation.pixelAlgorithm(w, h, + (x, y) -> toolContentPreview.dot(fill, x, y)); + frontier.forEach(p -> toolContentPreview.dot(outline, p.x, p.y)); + + return toolContentPreview.submit(); } @Override - Runnable getMouseUpConsequence(final SEContext context) { - return () -> context.getState().markAsCheckpoint(true); + public boolean previewScopeIsGlobal() { + return true; } } diff --git a/src/com/jordanbunke/stipple_effect/tools/MoverTool.java b/src/com/jordanbunke/stipple_effect/tools/MoverTool.java index 23cb4aaa..a9e7b870 100644 --- a/src/com/jordanbunke/stipple_effect/tools/MoverTool.java +++ b/src/com/jordanbunke/stipple_effect/tools/MoverTool.java @@ -1,21 +1,18 @@ package com.jordanbunke.stipple_effect.tools; import com.jordanbunke.delta_time.events.GameMouseEvent; +import com.jordanbunke.delta_time.image.GameImage; +import com.jordanbunke.delta_time.utility.math.Bounds2D; import com.jordanbunke.delta_time.utility.math.Coord2D; import com.jordanbunke.delta_time.utility.math.MathPlus; import com.jordanbunke.stipple_effect.project.SEContext; -import com.jordanbunke.stipple_effect.selection.RotateFunction; -import com.jordanbunke.stipple_effect.selection.SelectionUtils; -import com.jordanbunke.stipple_effect.selection.StretcherFunction; +import com.jordanbunke.stipple_effect.selection.Selection; import com.jordanbunke.stipple_effect.utility.Constants; +import com.jordanbunke.stipple_effect.utility.ToolTaskHandler; import com.jordanbunke.stipple_effect.utility.math.Geometry; import com.jordanbunke.stipple_effect.utility.settings.Settings; -import java.util.HashSet; -import java.util.Set; -import java.util.function.BiConsumer; - -public sealed abstract class MoverTool extends Tool implements SnappableTool +public sealed abstract class MoverTool extends Tool implements SnappableTool permits MoveSelection, PickUpSelection { public enum TransformType { NONE, MOVE, STRETCH, ROTATE @@ -32,20 +29,34 @@ public enum Direction { } } + private static final double CONVERSION = 180 / Math.PI, PRECISION = 1e2, + CIRCLE_DEG = 360d, SEMI_CIRCLE_DEG = CIRCLE_DEG / 2; + + private ToolTaskHandler handler; + private boolean snap = false, snapToggled = false; private TransformType transformType, prospectiveType; private Direction direction; - private Set startSelection; + private Selection startSelection; + private T transformation; private Coord2D startMousePosition, lastMousePosition, startTopLeft, startBottomRight, startTP, lastTP; + private Coord2D cachedDisp; + private double cachedAngle; + + private GameImage toolContentPreview; + public MoverTool() { + handler = ToolTaskHandler.dummy(); + transformType = TransformType.NONE; prospectiveType = TransformType.NONE; direction = Direction.NA; - startSelection = new HashSet<>(); + startSelection = Selection.EMPTY; + transformation = null; startMousePosition = new Coord2D(); lastMousePosition = new Coord2D(); @@ -53,36 +64,56 @@ public MoverTool() { lastTP = Constants.NO_VALID_TARGET; startTopLeft = Constants.NO_VALID_TARGET; startBottomRight = Constants.NO_VALID_TARGET; + + cachedDisp = Constants.NO_VALID_TARGET; + cachedAngle = 0d; + + toolContentPreview = GameImage.dummy(); } - public abstract BiConsumer getMoverFunction(final SEContext context); - abstract StretcherFunction getStretcherFunction(final SEContext context); - abstract RotateFunction getRotateFunction(final SEContext context); - abstract Runnable getMouseUpConsequence(final SEContext context); + abstract boolean canBeMoved(final SEContext context); + abstract T move(final SEContext context, final Coord2D displacement); + abstract T stretch( + final SEContext context, final Selection initial, + final Coord2D change, final Direction direction + ); + abstract T rotate( + final SEContext context, final Selection initial, + final double deltaR, final Coord2D pivot, final boolean[] offset + ); + abstract void applyTransformation( + final SEContext context, final T transformation, final boolean transform + ); + abstract GameImage updateToolContentPreview( + final SEContext context, final T transformation + ); + + public final void applyMove(final SEContext context, final Coord2D displacement) { + applyTransformation(context, move(context, displacement), false); + } public TransformType determineTransformType( - final SEContext context + final SEContext context, final Coord2D mp ) { if (!context.getState().hasSelection()) return TransformType.NONE; else { - final Set selection = context.getState().getSelection(); + final Selection selection = context.getState().getSelection(); - startTopLeft = SelectionUtils.topLeft(selection); - startBottomRight = SelectionUtils.bottomRight(selection); + final int w = selection.bounds.width(), + h = selection.bounds.height(); + startTopLeft = selection.topLeft; + startBottomRight = startTopLeft.displace(w, h); - final Coord2D tp = context.getTargetPixel(); + final Coord2D tlmp = context.modelMousePosForPixel(startTopLeft), + brmp = context.modelMousePosForPixel(startBottomRight); - if (tp.equals(Constants.NO_VALID_TARGET)) - return TransformType.MOVE; - - final int left = startTopLeft.x, right = startBottomRight.x, - top = startTopLeft.y, bottom = startBottomRight.y; - - final int leftProx = Math.abs(left - tp.x), - rightProx = Math.abs(right - tp.x), - topProx = Math.abs(top - tp.y), - bottomProx = Math.abs(bottom - tp.y); + final int left = tlmp.x, right = brmp.x, + top = tlmp.y, bottom = brmp.y, + leftProx = Math.abs(left - mp.x), + rightProx = Math.abs(right - mp.x), + topProx = Math.abs(top - mp.y), + bottomProx = Math.abs(bottom - mp.y); boolean atLeft = leftProx <= Constants.STRETCH_PX_THRESHOLD, atRight = rightProx <= Constants.STRETCH_PX_THRESHOLD, @@ -96,7 +127,7 @@ public TransformType determineTransformType( } else if (atBottom) { direction = Direction.BL; return TransformType.STRETCH; - } else if (tp.y > top && tp.y < bottom) { + } else if (mp.y > top && mp.y < bottom) { direction = Direction.L; return TransformType.STRETCH; } @@ -107,27 +138,27 @@ public TransformType determineTransformType( } else if (atBottom) { direction = Direction.BR; return TransformType.STRETCH; - } else if (tp.y > top && tp.y < bottom) { + } else if (mp.y > top && mp.y < bottom) { direction = Direction.R; return TransformType.STRETCH; } - } else if (atTop && tp.x > left && tp.x < right) { + } else if (atTop && mp.x > left && mp.x < right) { direction = Direction.T; return TransformType.STRETCH; - } else if (atBottom && tp.x > left && tp.x < right) { + } else if (atBottom && mp.x > left && mp.x < right) { direction = Direction.B; return TransformType.STRETCH; } - atLeft = tp.x < left && leftProx <= Constants.ROTATE_PX_THRESHOLD; - atRight = tp.x > right && rightProx <= Constants.ROTATE_PX_THRESHOLD; - atTop = tp.y < top && topProx <= Constants.ROTATE_PX_THRESHOLD; - atBottom = tp.y > bottom && bottomProx <= Constants.ROTATE_PX_THRESHOLD; + atLeft = mp.x < left && leftProx <= Constants.ROTATE_PX_THRESHOLD; + atRight = mp.x > right && rightProx <= Constants.ROTATE_PX_THRESHOLD; + atTop = mp.y < top && topProx <= Constants.ROTATE_PX_THRESHOLD; + atBottom = mp.y > bottom && bottomProx <= Constants.ROTATE_PX_THRESHOLD; final int middleX = left + ((right - left) / 2), middleY = top + ((bottom - top) / 2); - final boolean atMiddleX = Math.abs(tp.x - middleX) < Constants.ROTATE_PX_THRESHOLD, - atMiddleY = Math.abs(tp.y - middleY) < Constants.ROTATE_PX_THRESHOLD; + final boolean atMiddleX = Math.abs(mp.x - middleX) < Constants.ROTATE_PX_THRESHOLD, + atMiddleY = Math.abs(mp.y - middleY) < Constants.ROTATE_PX_THRESHOLD; if (atLeft || atRight) { if (atTop) { @@ -184,16 +215,23 @@ public String getCursorCode() { @Override public void onMouseDown(final SEContext context, final GameMouseEvent me) { - transformType = determineTransformType(context); + transformType = determineTransformType(context, me.mousePosition); prospectiveType = TransformType.NONE; - if (context.getState().hasSelection()) { - startSelection = new HashSet<>(context.getState().getSelection()); + if (canBeMoved(context)) { + startSelection = context.getState().getSelection(); + transformation = move(context, new Coord2D()); + toolContentPreview = updateToolContentPreview(context, transformation); startMousePosition = me.mousePosition; lastMousePosition = me.mousePosition; startTP = context.getTargetPixel(); lastTP = startTP; + + switch (transformType) { + case MOVE -> cachedDisp = new Coord2D(); + case ROTATE -> cachedAngle = 0d; + } } } @@ -204,11 +242,15 @@ public void update(final SEContext context, final Coord2D mousePosition) { if (mousePosition.equals(lastMousePosition)) return; + if (isMoving() && !context.getState().hasSelection()) + transformType = TransformType.NONE; + switch (transformType) { - case NONE -> prospectiveType = determineTransformType(context); + case NONE -> prospectiveType = + determineTransformType(context, mousePosition); case MOVE -> { - final Set selection = context.getState().getSelection(); - final Coord2D topLeft = SelectionUtils.topLeft(selection); + final Selection selection = context.getState().getSelection(); + final Coord2D topLeft = selection.topLeft; Coord2D displacement = new Coord2D( -(int)((startMousePosition.x - mousePosition.x) / zoomFactor), @@ -218,46 +260,51 @@ public void update(final SEContext context, final Coord2D mousePosition) { if (isSnap()) { if (!snapToggled) { // displace in multiples of own bounds - final Coord2D bounds = SelectionUtils.bounds(selection); + final Bounds2D bounds = selection.bounds; - final int snappedX = bounds.x * (int)Math.round( - displacement.x / (double) bounds.x), - snappedY = bounds.y * (int)Math.round( - displacement.y / (double) bounds.y); + final int snappedX = bounds.width() * (int)Math.round( + displacement.x / (double) bounds.width()), + snappedY = bounds.height() * (int)Math.round( + displacement.y / (double) bounds.height()); displacement = new Coord2D(snappedX, snappedY); } else if (canSnapToGrid(context)) { // snap to top left of pixel grid final int px = Settings.getPixelGridXPixels(), py = Settings.getPixelGridYPixels(); - final Coord2D prospective = topLeft.displace(displacement), - gridPos = new Coord2D( - (prospective.x / px) * px, - (prospective.y / py) * py), - shift = new Coord2D( - gridPos.x - prospective.x, - gridPos.y - prospective.y); - displacement = displacement.displace(shift); + final Coord2D tp = context.getTargetPixel(), + gridPos = new Coord2D( + ((tp.x / px) - (tp.x < 0 ? 1 : 0)) * px, + ((tp.y / py) - (tp.y < 0 ? 1 : 0)) * py); + displacement = new Coord2D( + gridPos.x - topLeft.x, gridPos.y - topLeft.y); } } - getMoverFunction(context).accept(displacement, false); + final Coord2D disp = displacement; + + handler = ToolTaskHandler.update(handler, () -> { + transformation = move(context, disp); + toolContentPreview = + updateToolContentPreview(context, transformation); + }, transformType, context, displacement); + + cachedDisp = disp; } case STRETCH -> { - final Coord2D delta = new Coord2D( - (int)((mousePosition.x - startMousePosition.x) / zoomFactor), - (int)((mousePosition.y - startMousePosition.y) / zoomFactor) - ); - - final Coord2D change = switch (direction) { - case NA -> new Coord2D(); - case B, T -> new Coord2D(0, delta.y); - case L, R -> new Coord2D(delta.x, 0); - default -> new Coord2D(delta.x, delta.y); - }; - - getStretcherFunction(context).accept(startSelection, - change, direction, false); + final Coord2D change = snapStretchToClosestGridPosition(context, + new Coord2D((int)((mousePosition.x - + startMousePosition.x) / zoomFactor), + (int)((mousePosition.y - + startMousePosition.y) / zoomFactor)), + direction); + + handler = ToolTaskHandler.update(handler, () -> { + transformation = stretch(context, + startSelection, change, direction); + toolContentPreview = + updateToolContentPreview(context, transformation); + }, transformType, context, startSelection, change, direction); } case ROTATE -> { final Coord2D tp = context.getTargetPixel(); @@ -286,10 +333,15 @@ public void update(final SEContext context, final Coord2D mousePosition) { Geometry.angleDiff(angle, b), Direction.values()); - getRotateFunction(context).accept(startSelection, - deltaR, pivot, offset, false); + handler = ToolTaskHandler.update(handler, () -> { + transformation = rotate(context, + startSelection, deltaR, pivot, offset); + toolContentPreview = + updateToolContentPreview(context, transformation); + }, transformType, context, startSelection, deltaR, pivot, offset); lastTP = tp; + cachedAngle = deltaR; } } } @@ -299,13 +351,83 @@ public void update(final SEContext context, final Coord2D mousePosition) { @Override public void onMouseUp(final SEContext context, final GameMouseEvent me) { - if (transformType != TransformType.NONE) { + if (isMoving() && transformation != null) { + applyTransformation(context, transformation, + transformType != TransformType.MOVE); + transformType = TransformType.NONE; - getMouseUpConsequence(context).run(); me.markAsProcessed(); } } + public boolean isMoving() { + return transformType != TransformType.NONE; + } + + @Override + public boolean hasToolContentPreview() { + return isMoving(); + } + + @Override + public GameImage getToolContentPreview() { + return toolContentPreview; + } + + private Coord2D snapStretchToClosestGridPosition( + final SEContext context, final Coord2D delta, + final Direction direction + ) { + if (!(isSnap() && canSnapToGrid(context))) + return switch (direction) { + case NA -> new Coord2D(); + case B, T -> new Coord2D(0, delta.y); + case L, R -> new Coord2D(delta.x, 0); + default -> delta; + }; + + final int pgX = Settings.getPixelGridXPixels(), + pgY = Settings.getPixelGridYPixels(); + + final int tlx = startTopLeft.x + switch (direction) { + case L, TL, BL -> delta.x; + default -> 0; + }, tly = startTopLeft.y + switch (direction) { + case T, TL, TR -> delta.y; + default -> 0; + }, brx = startBottomRight.x + switch (direction) { + case R, TR, BR -> delta.x; + default -> 0; + }, bry = startBottomRight.y + switch (direction) { + case B, BL, BR -> delta.y; + default -> 0; + }, tlGridX = (int)(tlx / (double) pgX), + tlGridY = (int)(tly / (double) pgY), + brGridX = (int)(brx / (double) pgX) + (brx % pgX == 0 ? 0 : 1), + brGridY = (int)(bry / (double) pgY) + (bry % pgY == 0 ? 0 : 1), + tlxSnapped = tlGridX * pgX, tlySnapped = tlGridY * pgY, + brxSnapped = brGridX * pgX, brySnapped = brGridY * pgY; + + final Coord2D tlDelta = new Coord2D( + tlxSnapped - startTopLeft.x, + tlySnapped - startTopLeft.y), + brDelta = new Coord2D( + brxSnapped - startBottomRight.x, + brySnapped - startBottomRight.y); + + return switch (direction) { + case NA -> new Coord2D(); + case TL -> tlDelta; + case T -> new Coord2D(0, tlDelta.y); + case TR -> new Coord2D(brDelta.x, tlDelta.y); + case L -> new Coord2D(tlDelta.x, 0); + case R -> new Coord2D(brDelta.x, 0); + case BR -> brDelta; + case B -> new Coord2D(0, brDelta.y); + case BL -> new Coord2D(tlDelta.x, brDelta.y); + }; + } + private boolean canSnapToGrid(final SEContext context) { return context.renderInfo.isPixelGridOn() && context.couldRenderPixelGrid(); @@ -324,4 +446,37 @@ public void setSnap(final boolean snap) { public boolean isSnap() { return snap; } + + @Override + public String getBottomBarText() { + return switch (transformType) { + case MOVE -> { + final boolean movementX = cachedDisp.x != 0, + movementY = cachedDisp.y != 0; + + if (!(movementX || movementY)) + yield "No movement"; + + final int x = Math.abs(cachedDisp.x), + y = Math.abs(cachedDisp.y); + + final String px = "px ", + dirX = px + (cachedDisp.x > 0 ? "right" : "left"), + dirY = px + (cachedDisp.y > 0 ? "down" : "up"); + + yield "Moved " + (movementX ? x + dirX : "") + + (movementX && movementY ? ", " : "") + + (movementY ? y + dirY : ""); + } + case ROTATE -> { + final double degrees = cachedAngle * CONVERSION, + closest = degrees > SEMI_CIRCLE_DEG + ? -(CIRCLE_DEG - degrees) : degrees, + rounded = (int)(closest * PRECISION) / PRECISION; + + yield "Rotated " + rounded + " deg"; + } + default -> super.getBottomBarText(); + }; + } } diff --git a/src/com/jordanbunke/stipple_effect/tools/OverlayTool.java b/src/com/jordanbunke/stipple_effect/tools/OverlayTool.java index 3b263155..31131a6e 100644 --- a/src/com/jordanbunke/stipple_effect/tools/OverlayTool.java +++ b/src/com/jordanbunke/stipple_effect/tools/OverlayTool.java @@ -1,10 +1,8 @@ package com.jordanbunke.stipple_effect.tools; import com.jordanbunke.delta_time.image.GameImage; -import com.jordanbunke.delta_time.utility.math.Coord2D; public interface OverlayTool { - Coord2D getTopLeft(); GameImage getSelectionOverlay(); boolean isDrawing(); } diff --git a/src/com/jordanbunke/stipple_effect/tools/Pencil.java b/src/com/jordanbunke/stipple_effect/tools/Pencil.java index f540760a..9bdc907f 100644 --- a/src/com/jordanbunke/stipple_effect/tools/Pencil.java +++ b/src/com/jordanbunke/stipple_effect/tools/Pencil.java @@ -4,11 +4,11 @@ import com.jordanbunke.delta_time.image.GameImage; import com.jordanbunke.delta_time.utility.math.Coord2D; import com.jordanbunke.stipple_effect.project.SEContext; +import com.jordanbunke.stipple_effect.selection.Selection; import com.jordanbunke.stipple_effect.utility.Constants; import com.jordanbunke.stipple_effect.visual.theme.SEColors; import java.awt.*; -import java.util.Set; import java.util.function.BiFunction; public final class Pencil extends ToolThatDraws { @@ -57,14 +57,14 @@ public void update( if (drawing && !tp.equals(Constants.NO_VALID_TARGET)) { final int w = context.getState().getImageWidth(), h = context.getState().getImageHeight(); - final Set selection = context.getState().getSelection(); + final Selection selection = context.getState().getSelection(); if (isUnchanged(context)) return; final GameImage edit = new GameImage(w, h); - if (selection.isEmpty() || selection.contains(tp)) + if (!selection.hasSelection() || selection.selected(tp)) edit.dot(c.apply(tp.x, tp.y), tp.x, tp.y); fillLineSpace(getLastTP(), tp, (x, y) -> @@ -92,7 +92,7 @@ public boolean hasToolOptionsBar() { } @Override - int getDitherTextX() { + int getAfterBreadthTextX() { return getFirstOptionLabelPosition().x; } } diff --git a/src/com/jordanbunke/stipple_effect/tools/PickUpSelection.java b/src/com/jordanbunke/stipple_effect/tools/PickUpSelection.java index 5858fe76..f5c08654 100644 --- a/src/com/jordanbunke/stipple_effect/tools/PickUpSelection.java +++ b/src/com/jordanbunke/stipple_effect/tools/PickUpSelection.java @@ -1,13 +1,15 @@ package com.jordanbunke.stipple_effect.tools; +import com.jordanbunke.delta_time.image.GameImage; import com.jordanbunke.delta_time.utility.math.Coord2D; import com.jordanbunke.stipple_effect.project.SEContext; -import com.jordanbunke.stipple_effect.selection.RotateFunction; -import com.jordanbunke.stipple_effect.selection.StretcherFunction; +import com.jordanbunke.stipple_effect.selection.Selection; +import com.jordanbunke.stipple_effect.selection.SelectionContents; +import com.jordanbunke.stipple_effect.selection.SelectionMode; +import com.jordanbunke.stipple_effect.state.Operation; +import com.jordanbunke.stipple_effect.state.ProjectState; -import java.util.function.BiConsumer; - -public final class PickUpSelection extends MoverTool { +public final class PickUpSelection extends MoverTool { private static final PickUpSelection INSTANCE; static { @@ -23,31 +25,77 @@ public String getName() { return "Pick up selection"; } - public void engage(final SEContext context) { - context.raiseSelectionToContents(false); + @Override + boolean canBeMoved(final SEContext context) { + return context.getState().hasSelection() && + context.getState().getSelectionMode() == SelectionMode.CONTENTS; } - public void disengage(final SEContext context) { - context.dropContentsToLayer(false, false); + @Override + SelectionContents move( + final SEContext context, final Coord2D displacement + ) { + if (canBeMoved(context)) + return context.getState().getSelectionContents() + .returnDisplaced(displacement); + + return null; } @Override - public BiConsumer getMoverFunction(final SEContext context) { - return context::moveSelectionContents; + SelectionContents stretch( + final SEContext context, final Selection initial, + final Coord2D change, final Direction direction + ) { + if (canBeMoved(context)) + return context.getState().getSelectionContents() + .returnStretched(initial, change, direction); + + return null; } @Override - StretcherFunction getStretcherFunction(final SEContext context) { - return context::stretchSelectionContents; + SelectionContents rotate( + final SEContext context, final Selection initial, + final double deltaR, final Coord2D pivot, final boolean[] offset + ) { + if (canBeMoved(context)) + return context.getState().getSelectionContents() + .returnRotated(initial, deltaR, pivot, offset); + + return null; } @Override - RotateFunction getRotateFunction(final SEContext context) { - return context::rotateSelectionContents; + void applyTransformation( + final SEContext context, final SelectionContents transformation, + final boolean transform + ) { + final ProjectState result = context.getState() + .changeSelectionContents(transformation); + context.getStateManager().performAction(result, transform + ? Operation.TRANSFORM_SELECTION_CONTENTS + : Operation.MOVE_SELECTION_CONTENTS); + } + + public void engage(final SEContext context) { + context.raiseSelectionToContents(false); + } + + public void disengage(final SEContext context) { + context.dropContentsToLayer(false, false); } @Override - Runnable getMouseUpConsequence(final SEContext context) { - return context::resetContentOriginal; + GameImage updateToolContentPreview( + final SEContext context, final SelectionContents transformation + ) { + final int w = context.getState().getImageWidth(), + h = context.getState().getImageHeight(); + + if (transformation == null) + return GameImage.dummy(); + + return transformation.getContentForCanvas(w, h); } } diff --git a/src/com/jordanbunke/stipple_effect/tools/PolygonSelect.java b/src/com/jordanbunke/stipple_effect/tools/PolygonSelect.java index 2afb81f7..1800d496 100644 --- a/src/com/jordanbunke/stipple_effect/tools/PolygonSelect.java +++ b/src/com/jordanbunke/stipple_effect/tools/PolygonSelect.java @@ -5,11 +5,13 @@ import com.jordanbunke.delta_time.utility.math.Coord2D; import com.jordanbunke.delta_time.utility.math.MathPlus; import com.jordanbunke.stipple_effect.project.SEContext; +import com.jordanbunke.stipple_effect.selection.Selection; import com.jordanbunke.stipple_effect.selection.SelectionUtils; import com.jordanbunke.stipple_effect.utility.Constants; import com.jordanbunke.stipple_effect.utility.math.LineMath; import com.jordanbunke.stipple_effect.utility.math.LineSegment; import com.jordanbunke.stipple_effect.utility.settings.Settings; +import com.jordanbunke.stipple_effect.visual.theme.SEColors; import com.jordanbunke.stipple_effect.visual.theme.Theme; import java.awt.*; @@ -18,10 +20,10 @@ import java.util.List; import java.util.Set; -public final class PolygonSelect extends ToolWithMode { +public final class PolygonSelect extends ToolWithMode implements SnappableTool { private static final PolygonSelect INSTANCE; - private boolean selecting; + private boolean selecting, finishing, wasFinishing; private Coord2D lastTP; private List vertices; private Set edges; @@ -33,6 +35,8 @@ public final class PolygonSelect extends ToolWithMode { private PolygonSelect() { selecting = false; + finishing = false; + wasFinishing = false; lastTP = Constants.NO_VALID_TARGET; vertices = new ArrayList<>(); edges = new HashSet<>(); @@ -76,11 +80,19 @@ else if (me.button == GameMouseEvent.Button.RIGHT && selecting) { if (selecting) { // add to selection - addEdge(getLastVertex(), tp); - vertices.add(tp); + if (finishing) { + final Coord2D first = vertices.get(0); + addEdge(getLastVertex(), first); + vertices.add(first); - if (tp.equals(vertices.get(0))) finish(context); + } else { + addEdge(getLastVertex(), tp); + vertices.add(tp); + + if (tp.equals(vertices.get(0))) + finish(context); + } } else if (tp.x >= 0 && tp.x < w && tp.y >= 0 && tp.y < h) { // Start selection // bounds check only necessary for first vertex @@ -98,29 +110,31 @@ else if (me.button == GameMouseEvent.Button.RIGHT && selecting) { public void update(final SEContext context, final Coord2D mousePosition) { final Coord2D tp = context.getTargetPixel(); - if (!selecting || tp.equals(Constants.NO_VALID_TARGET) || tp.equals(lastTP)) + if (!selecting || tp.equals(Constants.NO_VALID_TARGET) || + (tp.equals(lastTP) && finishing == wasFinishing)) return; updateToolContentPreview(context); lastTP = tp; + wasFinishing = finishing; } private void finish(final SEContext context) { // define bounding box - final Coord2D tl = SelectionUtils.topLeft( - new HashSet<>(vertices)).displace(-1, -1), - br = SelectionUtils.bottomRight( - new HashSet<>(vertices)).displace(1, 1); + final Coord2D tl = SelectionUtils.topLeft(vertices) + .displace(-1, -1), + br = SelectionUtils.bottomRight(vertices) + .displace(1, 1); // define selection and populate - final Set selection = new HashSet<>(edges), + final Set pixels = new HashSet<>(edges), removalCandidates = new HashSet<>(); for (int x = tl.x; x < br.x; x++) { for (int y = tl.y; y < br.y; y++) { final Coord2D pixel = new Coord2D(x, y); - if (selection.contains(pixel)) + if (pixels.contains(pixel)) continue; final LineSegment raycast = new LineSegment(pixel, tl); @@ -187,7 +201,7 @@ private void finish(final SEContext context) { edgeEncounters -= (2 * doubleBackVertices.size()); if (edgeEncounters % 2 == 1) - selection.add(pixel); + pixels.add(pixel); } } @@ -196,17 +210,17 @@ private void finish(final SEContext context) { final Set toRemove = new HashSet<>(); for (Coord2D pixel : removalCandidates) - if (selection.contains(pixel) && !( - selection.contains(pixel.displace(-1, 0)) && - selection.contains(pixel.displace(1, 0)) && - selection.contains(pixel.displace(0, -1)) && - selection.contains(pixel.displace(0, 1)))) + if (pixels.contains(pixel) && !( + pixels.contains(pixel.displace(-1, 0)) && + pixels.contains(pixel.displace(1, 0)) && + pixels.contains(pixel.displace(0, -1)) && + pixels.contains(pixel.displace(0, 1)))) toRemove.add(pixel); - selection.removeAll(toRemove); + pixels.removeAll(toRemove); // edit selection - context.editSelection(selection, true); + context.editSelection(Selection.fromPixels(pixels), true); reset(); } @@ -215,10 +229,12 @@ private void updateToolContentPreview(final SEContext context) { final int w = context.getState().getImageWidth(), h = context.getState().getImageHeight(); - final Coord2D tp = context.getTargetPixel(), first = vertices.get(0); + final Coord2D tp = context.getTargetPixel(), + first = vertices.get(0), + prospect = finishing ? first : tp; - final Color border = tp.equals(first) - ? t.highlightOutline.get() : t.highlightOverlay.get(); + final Color border = prospect.equals(first) + ? t.highlightOutline : t.highlightOverlay; toolContentPreview = new GameImage(w, h); @@ -228,7 +244,7 @@ private void updateToolContentPreview(final SEContext context) { continue; final Color c = (x + y) % 2 == 0 - ? t.textLight.get() : t.textDark.get(); + ? SEColors.black() : SEColors.white(); toolContentPreview.dot(c, first.x + x, first.y + y); } @@ -236,10 +252,10 @@ private void updateToolContentPreview(final SEContext context) { edges.forEach(e -> toolContentPreview.dot(border, e.x, e.y)); - defineLine(getLastVertex(), tp).forEach(next -> + defineLine(getLastVertex(), prospect).forEach(next -> toolContentPreview.dot(border, next.x, next.y)); vertices.forEach(v -> - toolContentPreview.dot(t.highlightOutline.get(), v.x, v.y)); + toolContentPreview.dot(t.highlightOutline, v.x, v.y)); } private void addEdge(final Coord2D v1, final Coord2D v2) { @@ -294,4 +310,14 @@ public boolean previewScopeIsGlobal() { public GameImage getToolContentPreview() { return toolContentPreview; } + + @Override + public void setSnap(final boolean finishing) { + this.finishing = finishing; + } + + @Override + public boolean isSnap() { + return finishing; + } } diff --git a/src/com/jordanbunke/stipple_effect/tools/ScriptBrush.java b/src/com/jordanbunke/stipple_effect/tools/ScriptBrush.java new file mode 100644 index 00000000..d7de5a12 --- /dev/null +++ b/src/com/jordanbunke/stipple_effect/tools/ScriptBrush.java @@ -0,0 +1,126 @@ +package com.jordanbunke.stipple_effect.tools; + +import com.jordanbunke.delta_time.events.GameMouseEvent; +import com.jordanbunke.delta_time.menu.menu_elements.button.SimpleMenuButton; +import com.jordanbunke.delta_time.menu.menu_elements.container.MenuElementGrouping; +import com.jordanbunke.delta_time.scripting.ast.nodes.function.HeadFuncNode; +import com.jordanbunke.delta_time.utility.math.Coord2D; +import com.jordanbunke.stipple_effect.StippleEffect; +import com.jordanbunke.stipple_effect.project.SEContext; +import com.jordanbunke.stipple_effect.scripting.SEInterpreter; +import com.jordanbunke.stipple_effect.utility.DialogVals; +import com.jordanbunke.stipple_effect.utility.Layout; +import com.jordanbunke.stipple_effect.visual.GraphicsUtils; +import com.jordanbunke.stipple_effect.visual.SECursor; +import com.jordanbunke.stipple_effect.visual.menu_elements.DynamicLabel; +import com.jordanbunke.stipple_effect.visual.menu_elements.TextLabel; + +import java.awt.*; +import java.util.function.Function; + +public final class ScriptBrush extends AbstractBrush + implements SnappableTool, ToggleModeTool { + private static final ScriptBrush INSTANCE; + + private Function c; + private boolean valid, ignoreTransparent, fromSystem, secondary; + + static { + INSTANCE = new ScriptBrush(); + } + + private ScriptBrush() { + c = null; + valid = false; + ignoreTransparent = false; + fromSystem = false; + secondary = false; + } + + public static ScriptBrush get() { + return INSTANCE; + } + + @Override + public String getName() { + return "Script Brush"; + } + + @Override + public String getCursorCode() { + return valid ? super.getCursorCode() : SECursor.NO_SCRIPT; + } + + @Override + public void onMouseDown(final SEContext context, final GameMouseEvent me) { + if (valid) + super.onMouseDown(context, me); + } + + public void updateScript(final boolean valid, final HeadFuncNode script) { + c = valid ? c -> (Color) SEInterpreter.get().run(script, c) : null; + this.valid = valid; + } + + @Override + void setColorGetter(final SEContext context, final GameMouseEvent me) { + secondary = me.button == GameMouseEvent.Button.RIGHT; + } + + @Override + boolean paintCondition(final Color existing, final int x, final int y) { + return !ignoreTransparent || existing.getAlpha() > 0; + } + + @Override + Color getColor(final Color existing, final int x, final int y) { + return c.apply(fromSystem ? (secondary + ? StippleEffect.get().getSecondary() + : StippleEffect.get().getPrimary()) + : existing); + } + + @Override + public MenuElementGrouping buildToolOptionsBar() { + final MenuElementGrouping inherited = super.buildToolOptionsBar(); + + // script label + final TextLabel scriptLabel = TextLabel.make( + new Coord2D(getAfterBreadthTextX(), Layout.optionsBarTextY()), + "Color script"); + + // upload script button + final SimpleMenuButton scriptButton = + GraphicsUtils.makeStandardTextButton("Upload", + new Coord2D(Layout.optionsBarNextElementX( + scriptLabel, false), + Layout.getToolOptionsBarPosition().y + + ((Layout.TOOL_OPTIONS_BAR_H - + Layout.STD_TEXT_BUTTON_H) / 2)), + StippleEffect.get()::openColorScript); + + // script feedback label + final DynamicLabel scriptFeedback = DynamicLabel.make(new Coord2D( + Layout.optionsBarNextElementX(scriptButton, false), + Layout.optionsBarTextY()), DialogVals::colorScriptMessage, + Layout.getToolOptionsBarWidth()); + + return new MenuElementGrouping(inherited, + scriptLabel, scriptButton, scriptFeedback); + } + + @Override + public void setSnap(final boolean is) { + fromSystem = is; + } + + @Override + public boolean isSnap() { + return fromSystem; + } + + @Override + public void setMode(final boolean is) { + ignoreTransparent = is; + } +} diff --git a/src/com/jordanbunke/stipple_effect/tools/ShadeBrush.java b/src/com/jordanbunke/stipple_effect/tools/ShadeBrush.java index 6c9d0275..8abbc37d 100644 --- a/src/com/jordanbunke/stipple_effect/tools/ShadeBrush.java +++ b/src/com/jordanbunke/stipple_effect/tools/ShadeBrush.java @@ -1,24 +1,17 @@ package com.jordanbunke.stipple_effect.tools; import com.jordanbunke.delta_time.events.GameMouseEvent; -import com.jordanbunke.delta_time.image.GameImage; -import com.jordanbunke.delta_time.utility.math.Coord2D; import com.jordanbunke.stipple_effect.StippleEffect; import com.jordanbunke.stipple_effect.palette.Palette; import com.jordanbunke.stipple_effect.project.SEContext; -import com.jordanbunke.stipple_effect.utility.Constants; import java.awt.*; -import java.util.HashSet; -import java.util.Set; import java.util.function.Function; -public final class ShadeBrush extends ToolWithBreadth { +public final class ShadeBrush extends AbstractBrush { private static final ShadeBrush INSTANCE; - private boolean painting; private Function c; - private Set painted; private Palette palette; static { @@ -26,9 +19,7 @@ public final class ShadeBrush extends ToolWithBreadth { } private ShadeBrush() { - painting = false; c = c -> c; - painted = new HashSet<>(); palette = null; } @@ -42,82 +33,19 @@ public String getName() { } @Override - public void onMouseDown(final SEContext context, final GameMouseEvent me) { - if (StippleEffect.get().hasPaletteContents() && - !context.getTargetPixel().equals(Constants.NO_VALID_TARGET) && - me.button != GameMouseEvent.Button.MIDDLE) { - painting = true; - palette = StippleEffect.get().getSelectedPalette(); - c = me.button == GameMouseEvent.Button.LEFT - ? palette::nextLeft : palette::nextRight; - painted = new HashSet<>(); - - reset(); - context.getState().markAsCheckpoint(false); - } + void setColorGetter(final SEContext context, final GameMouseEvent me) { + palette = StippleEffect.get().getSelectedPalette(); + c = me.button == GameMouseEvent.Button.LEFT + ? palette::nextLeft : palette::nextRight; } @Override - public void update(final SEContext context, final Coord2D mousePosition) { - final Coord2D tp = context.getTargetPixel(); - - if (painting && !tp.equals(Constants.NO_VALID_TARGET)) { - final int w = context.getState().getImageWidth(), - h = context.getState().getImageHeight(); - final Set selection = context.getState().getSelection(); - - if (isUnchanged(context)) - return; - - if (!stillSameFrame(context)) - painted.clear(); - - final GameImage edit = new GameImage(w, h), - current = context.getState().getActiveLayerFrame(); - populateAround(edit, current, tp, selection, w, h); - - fillLineSpace(getLastTP(), tp, (x, y) -> populateAround(edit, - current, getLastTP().displace(x, y), selection, w, h)); - - context.paintOverImage(edit.submit()); - updateLast(context); - } - } - - private void populateAround( - final GameImage edit, final GameImage current, - final Coord2D tp, final Set selection, - final int w, final int h - ) { - final int halfB = breadthOffset(); - final boolean[][] mask = breadthMask(); - - for (int x = 0; x < mask.length; x++) - for (int y = 0; y < mask[x].length; y++) { - final Coord2D b = new Coord2D(x + (tp.x - halfB), - y + (tp.y - halfB)); - - if (b.x < 0 || b.x >= w || b.y < 0 || b.y >= h) - continue; - - if (!(selection.isEmpty() || selection.contains(b))) - continue; - - final Color candidate = current.getColorAt(b.x, b.y); - if (mask[x][y] && !painted.contains(b) && - palette.isIncluded(candidate)) { - edit.dot(c.apply(candidate), b.x, b.y); - painted.add(b); - } - } + boolean paintCondition(final Color existing, final int x, final int y) { + return palette.isIncluded(existing); } @Override - public void onMouseUp(final SEContext context, final GameMouseEvent me) { - if (painting) { - painting = false; - context.getState().markAsCheckpoint(true); - me.markAsProcessed(); - } + Color getColor(final Color existing, final int x, final int y) { + return c.apply(existing); } } diff --git a/src/com/jordanbunke/stipple_effect/tools/ShapeTool.java b/src/com/jordanbunke/stipple_effect/tools/ShapeTool.java new file mode 100644 index 00000000..ab556ce7 --- /dev/null +++ b/src/com/jordanbunke/stipple_effect/tools/ShapeTool.java @@ -0,0 +1,140 @@ +package com.jordanbunke.stipple_effect.tools; + +import com.jordanbunke.delta_time.menu.menu_elements.container.MenuElementGrouping; +import com.jordanbunke.delta_time.utility.math.Coord2D; +import com.jordanbunke.stipple_effect.selection.Selection; +import com.jordanbunke.stipple_effect.utility.Layout; +import com.jordanbunke.stipple_effect.visual.menu_elements.Dropdown; +import com.jordanbunke.stipple_effect.visual.menu_elements.TextLabel; + +import java.util.HashSet; +import java.util.Set; + +public final class ShapeTool extends GeometryTool implements SnappableTool { + private static final ShapeTool INSTANCE; + + private boolean ellipse; + + static { + INSTANCE = new ShapeTool(); + } + + private ShapeTool() { + super(); + + ellipse = false; + } + + public static ShapeTool get() { + return INSTANCE; + } + + @Override + Coord2D snappedEndpoint(final Coord2D anchor, final Coord2D tp) { + final Coord2D distance = tp.displace(-anchor.x, -anchor.y); + + final int magX = Math.abs(distance.x), magY = Math.abs(distance.y); + + return magX > magY + ? anchor.displace(distance.x, distance.y > 0 ? magX : -magX) + : anchor.displace(distance.x > 0 ? magY : -magY, distance.y); + } + + @Override + void geoDefinition( + final Coord2D anchor, final Coord2D endpoint, + final Selection selection + ) { + if (ellipse) { + final Set points = new HashSet<>(); + + final int w = Math.abs(anchor.x - endpoint.x), + h = Math.abs(anchor.y - endpoint.y); + + final double a = w / 2d, b = h / 2d; + + final Coord2D left = new Coord2D( + Math.min(anchor.x, endpoint.x), + (anchor.y + endpoint.y) / 2), + right = left.displace(w, 0); + + points.add(left); + points.add(right); + + for (int x = 0; x <= a; x++) { + final int y = (int) Math.round(Math.sqrt(Math.pow(b, 2) - + ((Math.pow(b, 2) * Math.pow(x, 2)) / Math.pow(a, 2)))); + + points.add(new Coord2D(left.x + (int) a + x, left.y + y)); + points.add(new Coord2D(left.x + (int) a - x, left.y + y)); + points.add(new Coord2D(left.x + (int) a + x, left.y - y)); + points.add(new Coord2D(left.x + (int) a - x, left.y - y)); + } + + for (int y = 0; y <= b; y++) { + final int x = (int) Math.round(Math.sqrt(Math.pow(a, 2) - + ((Math.pow(a, 2) * Math.pow(y, 2)) / Math.pow(b, 2)))); + + points.add(new Coord2D(left.x + (int) a + x, left.y + y)); + points.add(new Coord2D(left.x + (int) a - x, left.y + y)); + points.add(new Coord2D(left.x + (int) a + x, left.y - y)); + points.add(new Coord2D(left.x + (int) a - x, left.y - y)); + } + + points.forEach(p -> populateAround(p, selection)); + } else { + final Coord2D corner3 = new Coord2D(anchor.x, endpoint.y), + corner4 = new Coord2D(endpoint.x, anchor.y); + + populateAround(anchor, selection); + populateAround(endpoint, selection); + populateAround(corner3, selection); + populateAround(corner4, selection); + + fillLineSpace(anchor, corner3, (x, y) -> + populateAround(anchor.displace(x, y), selection)); + fillLineSpace(corner3, endpoint, (x, y) -> + populateAround(corner3.displace(x, y), selection)); + fillLineSpace(endpoint, corner4, (x, y) -> + populateAround(endpoint.displace(x, y), selection)); + fillLineSpace(corner4, anchor, (x, y) -> + populateAround(corner4.displace(x, y), selection)); + } + } + + public void setEllipse(final boolean ellipse) { + this.ellipse = ellipse; + } + + @Override + public String getBottomBarText() { + return (ellipse ? "Ellipse " : "Rectangle ") + + "Tool (" + getBreadth() + " px)"; + } + + @Override + public String getName() { + return "Shape Tool"; + } + + @Override + public MenuElementGrouping buildToolOptionsBar() { + final MenuElementGrouping inherited = super.buildToolOptionsBar(); + + // shape label + final TextLabel shapeLabel = TextLabel.make( + new Coord2D(getAfterBreadthTextX(), Layout.optionsBarTextY()), + "Shape"); + + // shape dropdown + final Dropdown shapeDropdown = Dropdown.forToolOptionsBar( + Layout.optionsBarNextElementX(shapeLabel, false), + new String[] { "Rectangle", "Ellipse" }, + new Runnable[] { + () -> setEllipse(false), () -> setEllipse(true) + }, () -> ellipse ? 1 : 0); + + return new MenuElementGrouping( + inherited, shapeLabel, shapeDropdown); + } +} diff --git a/src/com/jordanbunke/stipple_effect/tools/StipplePencil.java b/src/com/jordanbunke/stipple_effect/tools/StipplePencil.java index db0b9ab8..8c6fd9df 100644 --- a/src/com/jordanbunke/stipple_effect/tools/StipplePencil.java +++ b/src/com/jordanbunke/stipple_effect/tools/StipplePencil.java @@ -5,9 +5,9 @@ import com.jordanbunke.delta_time.utility.math.Coord2D; import com.jordanbunke.stipple_effect.StippleEffect; import com.jordanbunke.stipple_effect.project.SEContext; +import com.jordanbunke.stipple_effect.selection.Selection; import java.awt.*; -import java.util.Set; public final class StipplePencil extends Tool { private static final StipplePencil INSTANCE; @@ -38,7 +38,7 @@ public void onMouseDown( final int w = context.getState().getImageWidth(), h = context.getState().getImageHeight(); final Coord2D tp = context.getTargetPixel(); - final Set selection = context.getState().getSelection(); + final Selection selection = context.getState().getSelection(); final Color c = me.button == GameMouseEvent.Button.LEFT ? StippleEffect.get().getPrimary() @@ -47,7 +47,7 @@ public void onMouseDown( final GameImage edit = new GameImage(w, h); - if (selection.isEmpty() || selection.contains(tp)) + if (!selection.hasSelection() || selection.selected(tp)) edit.dot(c, tp.x, tp.y); context.paintOverImage(edit.submit()); diff --git a/src/com/jordanbunke/stipple_effect/tools/Tool.java b/src/com/jordanbunke/stipple_effect/tools/Tool.java index 5fdc04bb..cd0f87d9 100644 --- a/src/com/jordanbunke/stipple_effect/tools/Tool.java +++ b/src/com/jordanbunke/stipple_effect/tools/Tool.java @@ -10,14 +10,14 @@ import com.jordanbunke.stipple_effect.visual.menu_elements.TextLabel; public abstract class Tool { - private final GameImage icon, highlightedIcon, selectedIcon; + private GameImage icon, highlightedIcon, selectedIcon; public static Tool[] getAll() { return new Tool[] { Hand.get(), Zoom.get(), StipplePencil.get(), Pencil.get(), - Brush.get(), ShadeBrush.get(), Eraser.get(), - GradientTool.get(), LineTool.get(), + Brush.get(), ShadeBrush.get(), ScriptBrush.get(), Eraser.get(), + GradientTool.get(), LineTool.get(), ShapeTool.get(), TextTool.get(), Fill.get(), ColorPicker.get(), Wand.get(), BrushSelect.get(), BoxSelect.get(), PolygonSelect.get(), MoveSelection.get(), PickUpSelection.get() @@ -34,11 +34,13 @@ public static boolean canMoveSelectionBounds( } Tool() { + refreshIcons(); + } + + public final void refreshIcons() { this.icon = fetchIcon(); - this.highlightedIcon = new GameImage(GraphicsUtils.HIGHLIGHT_OVERLAY); - highlightedIcon.draw(icon); - highlightedIcon.free(); + this.highlightedIcon = GraphicsUtils.highlightIconButton(icon); this.selectedIcon = new GameImage(GraphicsUtils.SELECT_OVERLAY); selectedIcon.draw(icon); diff --git a/src/com/jordanbunke/stipple_effect/tools/ToolThatDraws.java b/src/com/jordanbunke/stipple_effect/tools/ToolThatDraws.java index 299eb500..2771764c 100644 --- a/src/com/jordanbunke/stipple_effect/tools/ToolThatDraws.java +++ b/src/com/jordanbunke/stipple_effect/tools/ToolThatDraws.java @@ -228,7 +228,7 @@ public MenuElementGrouping buildToolOptionsBar() { // bias label final TextLabel biasLabel = TextLabel.make( - new Coord2D(getDitherTextX(), Layout.optionsBarTextY()), + new Coord2D(getAfterBreadthTextX(), Layout.optionsBarTextY()), "Combination mode bias"); // bias content @@ -254,9 +254,8 @@ public MenuElementGrouping buildToolOptionsBar() { return new MenuElementGrouping( super.buildToolOptionsBar(), biasLabel, biasElems.decButton, biasElems.incButton, - biasElems.slider, biasElems.value - ); + biasElems.slider, biasElems.value); } - abstract int getDitherTextX(); + abstract int getAfterBreadthTextX(); } diff --git a/src/com/jordanbunke/stipple_effect/tools/ToolThatSearches.java b/src/com/jordanbunke/stipple_effect/tools/ToolThatSearches.java index 9a9be6a9..727d7ef8 100644 --- a/src/com/jordanbunke/stipple_effect/tools/ToolThatSearches.java +++ b/src/com/jordanbunke/stipple_effect/tools/ToolThatSearches.java @@ -5,6 +5,7 @@ import com.jordanbunke.delta_time.utility.math.Coord2D; import com.jordanbunke.delta_time.utility.math.MathPlus; import com.jordanbunke.funke.core.ConcreteProperty; +import com.jordanbunke.stipple_effect.selection.Selection; import com.jordanbunke.stipple_effect.utility.Constants; import com.jordanbunke.stipple_effect.utility.Layout; import com.jordanbunke.stipple_effect.utility.math.ColorMath; @@ -69,7 +70,7 @@ public static void setSearchDiag(final boolean searchDiag) { ToolThatSearches.searchDiag = searchDiag; } - public static Set search(final GameImage image, final Coord2D target) { + public static Selection search(final GameImage image, final Coord2D target) { final Color initial = image.getColorAt(target.x, target.y); return ToolWithMode.isGlobal() @@ -77,20 +78,24 @@ public static Set search(final GameImage image, final Coord2D target) { : contiguousSearch(image, initial, target); } - public static Set contiguousSearch( + public static Selection contiguousSearch( final GameImage image, final Color initial, final Coord2D target ) { hasCheckedMap.clear(); - final Set matched = new HashSet<>(), searched = new HashSet<>(); final int w = image.getWidth(), h = image.getHeight(); + final boolean[][] searched = new boolean[w][h]; + if (target.x < 0 || target.x >= w || target.y < 0 || target.y >= h) + return Selection.EMPTY; + + final boolean[][] matched = new boolean[w][h]; final Stack searching = new Stack<>(); searching.push(target); while (!searching.isEmpty()) { final Coord2D active = searching.pop(); - searched.add(active); + searched[active.x][active.y] = true; final Color pixel = image.getColorAt(active.x, active.y); @@ -99,46 +104,46 @@ public static Set contiguousSearch( hasCheckedMap.put(pixel, result); if (result) { - matched.add(active); + matched[active.x][active.y] = true; // neighbours - if (active.x > 0 && !searched.contains(active.displace(-1, 0))) + if (active.x > 0 && !searched[active.x - 1][active.y]) searching.add(active.displace(-1, 0)); - if (active.x + 1 < w && !searched.contains(active.displace(1, 0))) + if (active.x + 1 < w && !searched[active.x + 1][active.y]) searching.add(active.displace(1, 0)); - if (active.y > 0 && !searched.contains(active.displace(0, -1))) + if (active.y > 0 && !searched[active.x][active.y - 1]) searching.add(active.displace(0, -1)); - if (active.y + 1 < h && !searched.contains(active.displace(0, 1))) + if (active.y + 1 < h && !searched[active.x][active.y + 1]) searching.add(active.displace(0, 1)); // diagonals if (searchDiag) { if (active.x > 0 && active.y > 0 && - !searched.contains(active.displace(-1, -1))) + !searched[active.x - 1][active.y - 1]) searching.add(active.displace(-1, -1)); if (active.x > 0 && active.y + 1 < h && - !searched.contains(active.displace(-1, 1))) + !searched[active.x - 1][active.y + 1]) searching.add(active.displace(-1, 1)); if (active.x + 1 < w && active.y > 0 && - !searched.contains(active.displace(1, -1))) + !searched[active.x + 1][active.y - 1]) searching.add(active.displace(1, -1)); if (active.x + 1 < w && active.y + 1 < h && - !searched.contains(active.displace(1, 1))) + !searched[active.x + 1][active.y + 1]) searching.add(active.displace(1, 1)); } } } - return matched; + return Selection.of(matched); } - public static Set globalSearch( + public static Selection globalSearch( final GameImage image, final Color initial ) { hasCheckedMap.clear(); - final Set matched = new HashSet<>(); final int w = image.getWidth(), h = image.getHeight(); + final boolean[][] matched = new boolean[w][h]; for (int x = 0; x < w; x++) { for (int y = 0; y < h; y++) { @@ -149,11 +154,11 @@ public static Set globalSearch( hasCheckedMap.put(pixel, result); if (result) - matched.add(new Coord2D(x, y)); + matched[x][y] = true; } } - return matched; + return Selection.of(matched); } public static boolean pixelMatchesToleranceCondition( diff --git a/src/com/jordanbunke/stipple_effect/tools/ToolWithBreadth.java b/src/com/jordanbunke/stipple_effect/tools/ToolWithBreadth.java index 0ebee0ea..7f4821d1 100644 --- a/src/com/jordanbunke/stipple_effect/tools/ToolWithBreadth.java +++ b/src/com/jordanbunke/stipple_effect/tools/ToolWithBreadth.java @@ -6,6 +6,7 @@ import com.jordanbunke.stipple_effect.StippleEffect; import com.jordanbunke.stipple_effect.utility.Constants; import com.jordanbunke.stipple_effect.utility.Layout; +import com.jordanbunke.stipple_effect.utility.settings.Settings; import com.jordanbunke.stipple_effect.visual.GraphicsUtils; import com.jordanbunke.stipple_effect.visual.SECursor; import com.jordanbunke.stipple_effect.visual.theme.SEColors; @@ -15,18 +16,19 @@ import java.awt.*; import java.util.Arrays; -public sealed abstract class ToolWithBreadth extends ToolThatDraws implements BreadthTool - permits Brush, Eraser, BrushSelect, ShadeBrush, LineTool, GradientTool { +public sealed abstract class ToolWithBreadth + extends ToolThatDraws implements BreadthTool + permits AbstractBrush, Eraser, BrushSelect, GeometryTool, GradientTool { private int breadth; private GameImage overlay; // formatting only - private int ditherTextX; + private int afterBreadthTextX; ToolWithBreadth() { - breadth = Constants.DEFAULT_BRUSH_BREADTH; + breadth = Settings.getDefaultToolBreadth(); - ditherTextX = 0; + afterBreadthTextX = 0; } public static void redrawToolOverlays() { @@ -42,7 +44,7 @@ public void drawOverlay() { this.overlay = GraphicsUtils.drawOverlay(mask.length, mask[0].length, StippleEffect.get().getContext().renderInfo.getZoomFactor(), - (x, y) -> mask[x][y], inside, outside, false, false); + mask, inside, outside); } @Override @@ -97,7 +99,7 @@ public MenuElementGrouping buildToolOptionsBar() { Math.pow(sv, 3))) / SLIDER_MULT, b -> b + " px", Constants.MAX_BREADTH + " px"); - ditherTextX = Layout.optionsBarNextElementX(breadth.value, true); + afterBreadthTextX = Layout.optionsBarNextElementX(breadth.value, true); return new MenuElementGrouping(super.buildToolOptionsBar(), breadthLabel, breadth.decButton, breadth.incButton, @@ -105,7 +107,7 @@ public MenuElementGrouping buildToolOptionsBar() { } @Override - int getDitherTextX() { - return ditherTextX; + int getAfterBreadthTextX() { + return afterBreadthTextX; } } diff --git a/src/com/jordanbunke/stipple_effect/tools/ToolWithMode.java b/src/com/jordanbunke/stipple_effect/tools/ToolWithMode.java index d041b0db..bebe5df7 100644 --- a/src/com/jordanbunke/stipple_effect/tools/ToolWithMode.java +++ b/src/com/jordanbunke/stipple_effect/tools/ToolWithMode.java @@ -6,7 +6,11 @@ public abstract class ToolWithMode extends Tool { private static boolean global = false; public enum Mode { - SINGLE, ADDITIVE, SUBTRACTIVE + SINGLE, ADDITIVE, SUBTRACTIVE; + + public boolean inheritSelection() { + return this != SINGLE; + } } @Override diff --git a/src/com/jordanbunke/stipple_effect/tools/Wand.java b/src/com/jordanbunke/stipple_effect/tools/Wand.java index f28911a4..4b529fe8 100644 --- a/src/com/jordanbunke/stipple_effect/tools/Wand.java +++ b/src/com/jordanbunke/stipple_effect/tools/Wand.java @@ -5,8 +5,6 @@ import com.jordanbunke.delta_time.utility.math.Coord2D; import com.jordanbunke.stipple_effect.project.SEContext; -import java.util.Set; - public final class Wand extends ToolThatSearches { private static final Wand INSTANCE; @@ -35,9 +33,7 @@ public void onMouseDown(final SEContext context, final GameMouseEvent me) { final GameImage image = context.getState().getActiveLayerFrame(); // search - final Set matched = search(image, tp); - - context.editSelection(matched, true); + context.editSelection(search(image, tp), true); } } } diff --git a/src/com/jordanbunke/stipple_effect/tools/Zoom.java b/src/com/jordanbunke/stipple_effect/tools/Zoom.java index 6c798fc2..00c1ea54 100644 --- a/src/com/jordanbunke/stipple_effect/tools/Zoom.java +++ b/src/com/jordanbunke/stipple_effect/tools/Zoom.java @@ -1,6 +1,7 @@ package com.jordanbunke.stipple_effect.tools; import com.jordanbunke.delta_time.events.GameMouseEvent; +import com.jordanbunke.delta_time.utility.math.Coord2D; import com.jordanbunke.stipple_effect.project.SEContext; public final class Zoom extends Tool { @@ -28,9 +29,11 @@ public String getName() { @Override public void onMouseDown(final SEContext context, final GameMouseEvent me) { + final Coord2D tp = context.getTargetPixel(); + switch (me.button) { - case LEFT -> context.renderInfo.zoomIn(context.getTargetPixel()); - case RIGHT -> context.renderInfo.zoomOut(); + case LEFT -> context.renderInfo.zoomIn(tp); + case RIGHT -> context.renderInfo.zoomOut(tp); } } } diff --git a/src/com/jordanbunke/stipple_effect/utility/Constants.java b/src/com/jordanbunke/stipple_effect/utility/Constants.java index 5c09b860..630675f1 100644 --- a/src/com/jordanbunke/stipple_effect/utility/Constants.java +++ b/src/com/jordanbunke/stipple_effect/utility/Constants.java @@ -2,6 +2,7 @@ import com.jordanbunke.delta_time.utility.math.Coord2D; import com.jordanbunke.stipple_effect.project.ProjectInfo; +import com.jordanbunke.stipple_effect.project.ZoomLevel; import java.nio.file.Path; @@ -53,16 +54,16 @@ public class Constants { GENERIC_APPROVAL_TEXT = "Confirm", CLOSE_DIALOG_TEXT = "Close"; public static final int DEFAULT_CANVAS_W = 32, DEFAULT_CANVAS_H = 32, - MIN_CANVAS_W = 1, MIN_CANVAS_H = 1, MAX_CANVAS_W = 800, MAX_CANVAS_H = 800, + MIN_CANVAS_W = 1, MIN_CANVAS_H = 1, MAX_CANVAS_W = 1920, MAX_CANVAS_H = 1080, OVERLAY_BORDER_PX = 6, SPLASH_TIMEOUT_SECS = 1800, NO_SELECTION = -1, MAX_PALETTE_SIZE = 300; - public static final int DEFAULT_BRUSH_BREADTH = 3, + public static final int DEFAULT_BRUSH_BREADTH = 1, MIN_BREADTH = 1, MAX_BREADTH = 100, BREADTH_INC = 5, MIN_FONT_SCALE = 1, MAX_FONT_SCALE = 10, - DEFAULT_FONT_PX_SPACING = 2, MIN_FONT_PX_SPACING = 0, MAX_FONT_PX_SPACING = 10, - MAX_NUM_LAYERS = 100, MAX_NUM_FRAMES = 100, MAX_OUTLINE_PX = 10, - MIN_NUM_STATES = 100, DUMP_STATES_CUSHION_FACTOR = 2, + DEFAULT_FONT_PX_SPACING = 2, MIN_FONT_PX_SPACING = -10, MAX_FONT_PX_SPACING = 50, + MAX_NUM_LAYERS = 50, MAX_NUM_FRAMES = 300, MAX_OUTLINE_PX = 10, + CHECKPOINTS_DUMP_THRESHOLD = 5, MILLIS_IN_SECOND = 1000, MIN_PLAYBACK_FPS = 1, MAX_PLAYBACK_FPS = 24, PLAYBACK_FPS_INC = 1, DEFAULT_PLAYBACK_FPS = 10, TIMER_MAX_OUT = 4, MILLIS_PER_TIMER_INC = 100, @@ -70,18 +71,22 @@ public class Constants { HUE_SCALE = 360, SAT_SCALE = RGBA_SCALE, VALUE_SCALE = RGBA_SCALE, STATUS_UPDATE_DURATION_MILLIS = 5000, TOOL_TIP_MILLIS_THRESHOLD = 500, MIN_SCALE_UP = 1, MAX_SCALE_UP = 20, DEFAULT_SAVE_SCALE_UP = MIN_SCALE_UP, - MAX_NAME_LENGTH = 40, STRETCH_PX_THRESHOLD = 2, ROTATE_PX_THRESHOLD = 5, - MIN_HUE_SHIFT = -180, MAX_HUE_SHIFT = 180; + MAX_NAME_LENGTH = 40, STRETCH_PX_THRESHOLD = 10, ROTATE_PX_THRESHOLD = 25, + MAX_HUE_SHIFT = HUE_SCALE / 2, MIN_HUE_SHIFT = -MAX_HUE_SHIFT, + MAX_SV_SHIFT = SAT_SCALE, MIN_SV_SHIFT = -MAX_SV_SHIFT; - public static final long DUMP_STATES_MEM_THRESHOLD = 0x6400000L; // 100 MB - - public static final float MIN_ZOOM = 1 / 16f, MAX_ZOOM = 64f, DEF_ZOOM = 4f, - ZOOM_FOR_OVERLAY = 1f, ZOOM_FOR_GRID = DEF_ZOOM, - NO_ZOOM = 1f, ZOOM_CHANGE_LEVEL = 2f, MAX_PREVIEW_ZOOM = 8f; + public static final float + DEF_ZOOM = ZoomLevel.PLUS_3.z, + ZOOM_FOR_OVERLAY = ZoomLevel.NONE.z, + ZOOM_FOR_GRID = DEF_ZOOM, + NO_ZOOM = ZoomLevel.NONE.z, + MAX_PREVIEW_ZOOM = ZoomLevel.PLUS_8.z; public static final double EXACT_COLOR_MATCH = 0d, DEFAULT_TOLERANCE = EXACT_COLOR_MATCH, MAX_TOLERANCE = 1d, SMALL_TOLERANCE_INC = 0.01, BIG_TOLERANCE_INC = SMALL_TOLERANCE_INC * 10, UNBIASED = 0.5d, MIN_BIAS = 0d, MAX_BIAS = 1d, BIAS_INC = 0.01, - MIN_SV_SHIFT = 0d, MAX_SV_SHIFT = 50d; + MIN_SV_SCALE = 0d, MAX_SV_SCALE = 50d, + DEFAULT_FRAME_DURATION = 1d, + MIN_FRAME_DURATION = 0.1, MAX_FRAME_DURATION = 20d; } diff --git a/src/com/jordanbunke/stipple_effect/utility/DialogVals.java b/src/com/jordanbunke/stipple_effect/utility/DialogVals.java index c0095c70..6465d77a 100644 --- a/src/com/jordanbunke/stipple_effect/utility/DialogVals.java +++ b/src/com/jordanbunke/stipple_effect/utility/DialogVals.java @@ -6,6 +6,7 @@ import com.jordanbunke.stipple_effect.palette.PaletteSorter; import com.jordanbunke.stipple_effect.scripting.SEInterpreter; import com.jordanbunke.stipple_effect.selection.Outliner; +import com.jordanbunke.stipple_effect.tools.ScriptBrush; import com.jordanbunke.stipple_effect.visual.SEFonts; import java.nio.file.Path; @@ -21,18 +22,19 @@ public class DialogVals { importColumns = 1, importRows = 1, resizeWidth = Constants.DEFAULT_CANVAS_W, resizeHeight = Constants.DEFAULT_CANVAS_H, - padLeft = 0, padRight = 0, padTop = 0, padBottom = 0, + padLeft = 0, padRight = 0, padTop = 0, padBottom = 0, padAll = 0, newFontPixelSpacing = Constants.DEFAULT_FONT_PX_SPACING, framesPerDim = 1, frameWidth = Constants.DEFAULT_CANVAS_W, frameHeight = Constants.DEFAULT_CANVAS_H, splitColumns = 1, splitRows = 1, globalOutline = 0, - hueShift = 0; + hueShift = 0, satShift = 0, valueShift = 0; private static double layerOpacity = Constants.OPAQUE, + frameDuration = Constants.DEFAULT_FRAME_DURATION, resizeScale = 1d, - satShift = 1d, valueShift = 1d, + satScale = 1d, valueScale = 1d, resizeScaleX = resizeScale, resizeScaleY = resizeScale; private static boolean hasLatinEx = false, @@ -42,7 +44,10 @@ public class DialogVals { resizePreserveAspectRatio = false, sortPaletteBackwards = false, includeDisabledLayers = false, - colorScriptValid = false; + ignoreSelection = false, + colorScriptValid = false, + shiftingSat = false, + shiftingValue = false; private static int[] outlineSideMask = Outliner.getSingleOutlineMask(); private static String layerName = "", @@ -52,7 +57,7 @@ public class DialogVals { private static InfoScreen infoScreen = InfoScreen.ABOUT; private static SettingScreen settingScreen = SettingScreen.DEFAULTS; private static PaletteSorter paletteSorter = PaletteSorter.HUE; - private static Scope scope = Scope.SELECTION; + private static Scope scope = Scope.PROJECT; private static UploadStatus asciiStatus = UploadStatus.UNATTEMPTED, latinExStatus = UploadStatus.UNATTEMPTED; @@ -134,7 +139,7 @@ public String toString() { } public enum SettingScreen { - DEFAULTS, CONTROLS, VISUAL, ADVANCED; + DEFAULTS, CONTROLS, VISUAL; public String getTitle() { return (this == DEFAULTS ? this + " and startup" : this) + " settings"; @@ -147,7 +152,7 @@ public String toString() { } public enum Scope { - SELECTION, PROJECT, LAYER_FRAME, LAYER, FRAME; + PROJECT, LAYER, FRAME, LAYER_FRAME; public boolean considersLayers() { return this == FRAME || this == PROJECT; @@ -164,8 +169,17 @@ public String toString() { public static void setColorScript(final HeadFuncNode colorScript) { DialogVals.colorScript = colorScript; - DialogVals.colorScriptValid = SEInterpreter - .validateColorScript(DialogVals.colorScript); + colorScriptValid = SEInterpreter.validateColorScript(DialogVals.colorScript); + + ScriptBrush.get().updateScript(colorScriptValid, colorScript); + } + + public static void toggleShiftingSat() { + shiftingSat = !shiftingSat; + } + + public static void toggleShiftingValue() { + shiftingValue = !shiftingValue; } public static void setPaletteFolder(final Path paletteFolder) { @@ -215,6 +229,10 @@ public static void setLayerOpacity(final double layerOpacity) { DialogVals.layerOpacity = layerOpacity; } + public static void setFrameDuration(final double frameDuration) { + DialogVals.frameDuration = frameDuration; + } + public static void setResizeScale(final double resizeScale) { DialogVals.resizeScale = resizeScale; } @@ -231,14 +249,22 @@ public static void setHueShift(final int hueShift) { DialogVals.hueShift = hueShift; } - public static void setSatShift(final double satShift) { + public static void setSatShift(final int satShift) { DialogVals.satShift = satShift; } - public static void setValueShift(final double valueShift) { + public static void setValueShift(final int valueShift) { DialogVals.valueShift = valueShift; } + public static void setSatScale(final double satScale) { + DialogVals.satScale = satScale; + } + + public static void setValueScale(final double valueScale) { + DialogVals.valueScale = valueScale; + } + public static void setNewProjectHeight(final int newProjectHeight) { DialogVals.newProjectHeight = newProjectHeight; } @@ -297,6 +323,15 @@ public static void setImportWidth( } } + public static void setPadAll(final int padAll) { + DialogVals.padAll = padAll; + + setPadLeft(padAll); + setPadRight(padAll); + setPadTop(padAll); + setPadBottom(padAll); + } + public static void setPadBottom(final int padBottom) { DialogVals.padBottom = padBottom; } @@ -415,6 +450,10 @@ public static void setIncludeDisabledLayers(final boolean includeDisabledLayers) DialogVals.includeDisabledLayers = includeDisabledLayers; } + public static void setIgnoreSelection(final boolean ignoreSelection) { + DialogVals.ignoreSelection = ignoreSelection; + } + public static void setResizePreserveAspectRatio( final boolean resizePreserveAspectRatio ) { @@ -561,18 +600,34 @@ public static Scope getScope() { return scope; } + public static boolean isShiftingSat() { + return shiftingSat; + } + + public static boolean isShiftingValue() { + return shiftingValue; + } + public static int getHueShift() { return hueShift; } - public static double getSatShift() { + public static int getSatShift() { return satShift; } - public static double getValueShift() { + public static int getValueShift() { return valueShift; } + public static double getSatScale() { + return satScale; + } + + public static double getValueScale() { + return valueScale; + } + public static int getImportFrameHeight() { return importFrameHeight; } @@ -597,6 +652,10 @@ public static int getNewProjectWidth() { return newProjectWidth; } + public static int getPadAll() { + return padAll; + } + public static int getPadBottom() { return padBottom; } @@ -639,6 +698,10 @@ public static double getLayerOpacity() { return layerOpacity; } + public static double getFrameDuration() { + return frameDuration; + } + public static double getResizeScale() { return resizeScale; } @@ -683,6 +746,10 @@ public static boolean isIncludeDisabledLayers() { return includeDisabledLayers; } + public static boolean isIgnoreSelection() { + return ignoreSelection; + } + public static String getFontName() { return fontName; } diff --git a/src/com/jordanbunke/stipple_effect/utility/IconCodes.java b/src/com/jordanbunke/stipple_effect/utility/IconCodes.java index 0e72d23b..5ddcf8b5 100644 --- a/src/com/jordanbunke/stipple_effect/utility/IconCodes.java +++ b/src/com/jordanbunke/stipple_effect/utility/IconCodes.java @@ -16,12 +16,14 @@ public class IconCodes { GRANULAR_UNDO = "granular_undo", GRANULAR_REDO = "granular_redo", REDO = "redo", + HISTORY = "history", EXIT_PROGRAM = "exit_program", NEW_FRAME = "new_frame", DUPLICATE_FRAME = "duplicate_frame", REMOVE_FRAME = "remove_frame", MOVE_FRAME_FORWARD = "move_frame_forward", MOVE_FRAME_BACK = "move_frame_back", + FRAME_PROPERTIES = "frame_properties", TO_FIRST_FRAME = "to_first_frame", PREVIOUS = "previous", NEXT = "next", diff --git a/src/com/jordanbunke/stipple_effect/utility/Layout.java b/src/com/jordanbunke/stipple_effect/utility/Layout.java index 08349180..f81b9f69 100644 --- a/src/com/jordanbunke/stipple_effect/utility/Layout.java +++ b/src/com/jordanbunke/stipple_effect/utility/Layout.java @@ -48,7 +48,7 @@ public final class Layout { DIALOG_CONTENT_BIG_W_ALLOWANCE = LONG_NAME_TEXTBOX_W - DIALOG_CONTENT_SMALL_W_ALLOWANCE, SMALL_TEXT_BOX_W = 80, STD_TEXT_BUTTON_W = 88, STD_TEXT_BUTTON_H = 25, STD_TEXT_BUTTON_INC = STD_TEXT_BUTTON_H + BUTTON_OFFSET, BUTTON_TEXT_OFFSET_Y = -4, - COLOR_SELECTOR_OFFSET_Y = 120, COLOR_TEXTBOX_AVG_C_THRESHOLD = 100, COLOR_TEXTBOX_W = 116, + COLOR_SELECTOR_OFFSET_Y = 120, COLOR_TEXTBOX_W = 116, SLIDER_OFF_DIM = 20, SLIDER_BALL_DIM = 20, SLIDER_THINNING = 4, FULL_COLOR_SLIDER_W = RIGHT_PANEL_W - (SLIDER_BALL_DIM + 10), HALF_COLOR_SLIDER_W = (RIGHT_PANEL_W / 2) - (SLIDER_BALL_DIM + 10), diff --git a/src/com/jordanbunke/stipple_effect/utility/ParserUtils.java b/src/com/jordanbunke/stipple_effect/utility/ParserUtils.java index 299b3e03..f2bfde0c 100644 --- a/src/com/jordanbunke/stipple_effect/utility/ParserUtils.java +++ b/src/com/jordanbunke/stipple_effect/utility/ParserUtils.java @@ -8,7 +8,9 @@ import com.jordanbunke.stipple_effect.utility.settings.Settings; import com.jordanbunke.stipple_effect.visual.GraphicsUtils; import com.jordanbunke.stipple_effect.visual.theme.Theme; +import com.jordanbunke.stipple_effect.visual.theme.logic.ThemeLogic; +import java.awt.*; import java.nio.file.Path; import java.util.ArrayList; import java.util.List; @@ -18,16 +20,19 @@ public class ParserUtils { public static GameImage generateStatusEffectText(final String message) { final Theme t = Settings.getTheme(); - final TextBuilder tb = GraphicsUtils.uiText(t.textLight.get()); + final Color main = ThemeLogic.intuitTextColor(t.panelBackground, true), + accent = ThemeLogic.intuitTextColor(t.panelBackground, false); + + final TextBuilder tb = GraphicsUtils.uiText(t.textLight); final String[] segments = extractHighlight(message, Constants.OPEN_COLOR, Constants.CLOSE_COLOR); for (int i = 0; i < segments.length; i++) { if (i % 2 == 0) - tb.setColor(t.textLight.get()); + tb.setColor(main); else { - tb.setColor(t.affixTextLight.get()); + tb.setColor(accent); tb.addText("#"); tb.setColor(ParserSerializer.deserializeColor(segments[i])); } diff --git a/src/com/jordanbunke/stipple_effect/utility/StatusUpdates.java b/src/com/jordanbunke/stipple_effect/utility/StatusUpdates.java index 2357af74..8f23b7ea 100644 --- a/src/com/jordanbunke/stipple_effect/utility/StatusUpdates.java +++ b/src/com/jordanbunke/stipple_effect/utility/StatusUpdates.java @@ -2,14 +2,12 @@ import com.jordanbunke.delta_time.scripting.util.ScriptErrorLog; import com.jordanbunke.delta_time.scripting.util.TextPosition; -import com.jordanbunke.delta_time.utility.math.Coord2D; import com.jordanbunke.stipple_effect.StippleEffect; import com.jordanbunke.stipple_effect.palette.Palette; import com.jordanbunke.stipple_effect.stip.ParserSerializer; import java.awt.*; import java.nio.file.Path; -import java.util.Set; public class StatusUpdates { private static void send(final String update) { @@ -334,13 +332,6 @@ public static void stateChangeFailed(final boolean triedUndo) { (triedUndo ? "beginning" : "end") + " of project state stack"); } - public static void dumpedStates( - final int dumped, final long kbsFreed - ) { - send("Dumped " + dumped + " state" + (dumped > 1 ? "s" : "") + - " due to low memory; freed " + kbsFreed + " KBs of memory"); - } - public static void setCheckAndGridToBounds( final int width, final int height, final boolean fromSelection ) { @@ -351,11 +342,10 @@ public static void setCheckAndGridToBounds( } public static void sendToClipboard( - final boolean copied, final Set selection + final boolean copied, final int pixelCount ) { - send((copied ? "Copied" : "Cut") + " " + selection.size() + - " pixels " + (copied ? "" : "from the canvas ") + - "to " + StippleEffect.PROGRAM_NAME + "'s clipboard"); + send((copied ? "Copied" : "Cut") + " " + pixelCount + " pixels " + + (copied ? "" : "from the canvas ") + "to the clipboard"); } public static void clipboardSendFailed( diff --git a/src/com/jordanbunke/stipple_effect/utility/ToolTaskHandler.java b/src/com/jordanbunke/stipple_effect/utility/ToolTaskHandler.java new file mode 100644 index 00000000..28ac53fe --- /dev/null +++ b/src/com/jordanbunke/stipple_effect/utility/ToolTaskHandler.java @@ -0,0 +1,44 @@ +package com.jordanbunke.stipple_effect.utility; + +public final class ToolTaskHandler { + private final Object[] constraints; + private final Thread thread; + + public ToolTaskHandler(final Runnable task, final Object... constraints) { + this.thread = new Thread(task); + thread.start(); + + this.constraints = constraints; + } + + public static ToolTaskHandler dummy() { + return new ToolTaskHandler(() -> {}); + } + + public boolean done() { + return !thread.isAlive(); + } + + public boolean shouldKill(final Object... constraints) { + if (constraints.length != this.constraints.length) + return true; + + for (int i = 0; i < constraints.length; i++) + if (!this.constraints[i].equals(constraints[i])) + return false; + + return true; + } + + public static ToolTaskHandler update( + final ToolTaskHandler current, final Runnable task, + final Object... updatedConstraints + ) { + if (current.done() || current.shouldKill(updatedConstraints)) { + current.thread.interrupt(); + return new ToolTaskHandler(task, updatedConstraints); + } + + return current; + } +} diff --git a/src/com/jordanbunke/stipple_effect/utility/math/ColorMath.java b/src/com/jordanbunke/stipple_effect/utility/math/ColorMath.java index 8eb84f90..6ee71934 100644 --- a/src/com/jordanbunke/stipple_effect/utility/math/ColorMath.java +++ b/src/com/jordanbunke/stipple_effect/utility/math/ColorMath.java @@ -1,14 +1,13 @@ package com.jordanbunke.stipple_effect.utility.math; import com.jordanbunke.delta_time.image.GameImage; -import com.jordanbunke.delta_time.utility.math.Coord2D; import com.jordanbunke.delta_time.utility.math.MathPlus; +import com.jordanbunke.stipple_effect.selection.Selection; import com.jordanbunke.stipple_effect.utility.Constants; import com.jordanbunke.stipple_effect.utility.DialogVals; import java.awt.*; import java.util.Map; -import java.util.Set; import java.util.function.Function; public class ColorMath { @@ -22,15 +21,7 @@ public enum LastHSVEdit { public static GameImage algo( final Function internal, final Map map, - final GameImage source - ) { - return algo(internal, map, source, null); - } - - public static GameImage algo( - final Function internal, - final Map map, - final GameImage source, final Set pixels + final GameImage source, final Selection selection ) throws RuntimeException { final int w = source.getWidth(), h = source.getHeight(); final GameImage res = new GameImage(w, h); @@ -55,7 +46,7 @@ public static GameImage algo( map.put(c, cp); } - if (pixels != null && !pixels.contains(new Coord2D(x, y))) + if (selection != null && !selection.selected(x, y)) res.setRGB(x, y, c.getRGB()); else if (cp != null) res.setRGB(x, y, cp.getRGB()); @@ -69,19 +60,20 @@ public static Color shiftHSV(final Color input) { final double h = rgbToHue(input), s = rgbToSat(input), v = rgbToValue(input); - final int shiftH = DialogVals.getHueShift(); - final double shiftS = DialogVals.getSatShift(), + final int shiftH = DialogVals.getHueShift(), + shiftS = DialogVals.getSatShift(), shiftV = DialogVals.getValueShift(); - - double nh = h + normalize(shiftH, Constants.HUE_SCALE); - - if (nh < 0d) - nh += 1d; - else if (nh >= 1d) - nh -= 1d; - - final double ns = MathPlus.bounded(0d, s * shiftS, 1d), - nv = MathPlus.bounded(0d, v * shiftV, 1d); + final double scaleS = DialogVals.getSatScale(), + scaleV = DialogVals.getValueScale(); + + final double nh = + normalize(h + scaleDown(shiftH, Constants.HUE_SCALE)), + ns = MathPlus.bounded(0d, (DialogVals.isShiftingSat() + ? s + scaleDown(shiftS, Constants.SAT_SCALE) + : s * scaleS), 1d), + nv = MathPlus.bounded(0d, (DialogVals.isShiftingValue() + ? v + scaleDown(shiftV, Constants.VALUE_SCALE) + : v * scaleV), 1d); return fromHSV(nh, ns, nv, input.getAlpha()); } @@ -131,33 +123,47 @@ public static double diff(final Color a, final Color b) { } public static int hueGetter(final Color c) { - return scale(lastHSVEdit == LastHSVEdit.NONE + return scaleUp(lastHSVEdit == LastHSVEdit.NONE ? rgbToHue(c) : lastHue, Constants.HUE_SCALE); } public static int satGetter(final Color c) { - return scale(lastHSVEdit == LastHSVEdit.NONE + return scaleUp(lastHSVEdit == LastHSVEdit.NONE ? rgbToSat(c) : lastSat, Constants.SAT_SCALE); } public static int valueGetter(final Color c) { - return scale(lastHSVEdit == LastHSVEdit.NONE + return scaleUp(lastHSVEdit == LastHSVEdit.NONE ? rgbToValue(c) : lastValue, Constants.VALUE_SCALE); } - public static int scale(final double value, final int scaleMax) { + private static int scaleUp(final double value, final int scaleMax) { return MathPlus.bounded(0, (int) Math.round(value * scaleMax), scaleMax); } - private static double normalize(final int value, final int scaleMax) { + private static double scaleDown(final int value, final int scaleMax) { return value / (double) scaleMax; } + private static double normalize(final double n) { + final double UNIT = 1d; + + double res = n; + + while (res < 0d) + res += UNIT; + + while (res >= UNIT) + res -= UNIT; + + return res; + } + public static double rgbToHue(final Color c) { final int R = 0, G = 1, B = 2; final double[] rgb = rgbAsArray(c); final double max = getMaxOfRGB(rgb), range = getRangeOfRGB(rgb), - multiplier = normalize(Constants.HUE_SCALE / 6, Constants.HUE_SCALE); + multiplier = scaleDown(Constants.HUE_SCALE / 6, Constants.HUE_SCALE); if (range == 0d) return 0d; @@ -201,7 +207,7 @@ public static double rgbToValue(final Color c) { public static Color hueAdjustedColor(final int hue, final Color c) { final double saturation = getHSVAttribute(ColorMath::rgbToSat, c, lastSat), value = getHSVAttribute(ColorMath::rgbToValue, c, lastValue), - nHue = normalize(hue, Constants.HUE_SCALE); + nHue = scaleDown(hue, Constants.HUE_SCALE); lastHue = nHue; return fromHSV(nHue, saturation, value, c.getAlpha()); @@ -210,7 +216,7 @@ public static Color hueAdjustedColor(final int hue, final Color c) { public static Color satAdjustedColor(final int saturation, final Color c) { final double hue = getHSVAttribute(ColorMath::rgbToHue, c, lastHue), value = getHSVAttribute(ColorMath::rgbToValue, c, lastValue), - nSat = normalize(saturation, Constants.SAT_SCALE); + nSat = scaleDown(saturation, Constants.SAT_SCALE); lastSat = nSat; return fromHSV(hue, nSat, value, c.getAlpha()); @@ -219,7 +225,7 @@ public static Color satAdjustedColor(final int saturation, final Color c) { public static Color valueAdjustedColor(final int value, final Color c) { final double saturation = getHSVAttribute(ColorMath::rgbToSat, c, lastSat), hue = getHSVAttribute(ColorMath::rgbToHue, c, lastHue), - nValue = normalize(value, Constants.VALUE_SCALE); + nValue = scaleDown(value, Constants.VALUE_SCALE); lastValue = nValue; return fromHSV(hue, saturation, nValue, c.getAlpha()); @@ -238,28 +244,28 @@ public static Color fromHSV( final double value, final int alpha ) { final double c = saturation * value, - x = c * (1d - Math.abs(((hue / normalize( + x = c * (1d - Math.abs(((hue / scaleDown( Constants.HUE_SCALE / 6, Constants.HUE_SCALE)) % 2) - 1)), m = value - c, r, g, b; - if (hue < normalize((int)(Constants.HUE_SCALE * (1 / 6d)), Constants.HUE_SCALE)) { + if (hue < scaleDown((int)(Constants.HUE_SCALE * (1 / 6d)), Constants.HUE_SCALE)) { r = c; g = x; b = 0d; - } else if (hue < normalize((int)(Constants.HUE_SCALE * (2 / 6d)), Constants.HUE_SCALE)) { + } else if (hue < scaleDown((int)(Constants.HUE_SCALE * (2 / 6d)), Constants.HUE_SCALE)) { r = x; g = c; b = 0d; - } else if (hue < normalize((int)(Constants.HUE_SCALE * (3 / 6d)), Constants.HUE_SCALE)) { + } else if (hue < scaleDown((int)(Constants.HUE_SCALE * (3 / 6d)), Constants.HUE_SCALE)) { r = 0d; g = c; b = x; - } else if (hue < normalize((int)(Constants.HUE_SCALE * (4 / 6d)), Constants.HUE_SCALE)) { + } else if (hue < scaleDown((int)(Constants.HUE_SCALE * (4 / 6d)), Constants.HUE_SCALE)) { r = 0d; g = x; b = c; - } else if (hue < normalize((int)(Constants.HUE_SCALE * (5 / 6d)), Constants.HUE_SCALE)) { + } else if (hue < scaleDown((int)(Constants.HUE_SCALE * (5 / 6d)), Constants.HUE_SCALE)) { r = x; g = 0d; b = c; @@ -270,9 +276,9 @@ public static Color fromHSV( } return new Color( - scale(r + m, Constants.RGBA_SCALE), - scale(g + m, Constants.RGBA_SCALE), - scale(b + m, Constants.RGBA_SCALE), + scaleUp(r + m, Constants.RGBA_SCALE), + scaleUp(g + m, Constants.RGBA_SCALE), + scaleUp(b + m, Constants.RGBA_SCALE), alpha); } diff --git a/src/com/jordanbunke/stipple_effect/utility/settings/Settings.java b/src/com/jordanbunke/stipple_effect/utility/settings/Settings.java index 2505420b..a8585c28 100644 --- a/src/com/jordanbunke/stipple_effect/utility/settings/Settings.java +++ b/src/com/jordanbunke/stipple_effect/utility/settings/Settings.java @@ -12,6 +12,7 @@ import com.jordanbunke.stipple_effect.utility.settings.types.IntSettingType; import com.jordanbunke.stipple_effect.utility.settings.types.StringSettingType; import com.jordanbunke.stipple_effect.visual.DialogAssembly; +import com.jordanbunke.stipple_effect.visual.GraphicsUtils; import com.jordanbunke.stipple_effect.visual.SEFonts; import com.jordanbunke.stipple_effect.visual.theme.Theme; import com.jordanbunke.stipple_effect.visual.theme.Themes; @@ -29,7 +30,6 @@ public enum Code { INVERT_BREADTH_DIRECTION(new Setting<>(BooleanSettingType.get(), false)), INVERT_TOLERANCE_DIRECTION(new Setting<>(BooleanSettingType.get(), false)), INVERT_FONT_SIZE_DIRECTION(new Setting<>(BooleanSettingType.get(), false)), - DUMP_STATES(new Setting<>(BooleanSettingType.get(), true)), // int settings WINDOWED_W(new Setting<>( @@ -74,6 +74,8 @@ public enum Code { IntSettingType.get(), Constants.DEFAULT_CANVAS_W)), DEFAULT_CANVAS_H_PX(new Setting<>( IntSettingType.get(), Constants.DEFAULT_CANVAS_H)), + DEFAULT_TOOL_BREADTH(new Setting<>( + IntSettingType.get(), Constants.DEFAULT_BRUSH_BREADTH)), // string settings DEFAULT_INDEX_PREFIX(new Setting<>( @@ -97,8 +99,10 @@ public enum Code { THEME(new Setting<>( new EnumSettingType<>(Themes.class), Themes.DEFAULT, theme -> { - StippleEffect.get().getContexts() - .forEach(SEContext::redrawCheckerboard); + GraphicsUtils.refreshAssets(); + + StippleEffect.get().getContexts().forEach( + SEContext::redrawCheckerboard); StippleEffect.get().rebuildAllMenus(); })); @@ -111,14 +115,12 @@ Code(final Setting setting) { public String toString() { final String name = name(); - return name.toLowerCase().replace("default", "def"); + return name.toLowerCase(); } public static Code fromString(final String code) { try { - final String formattedCode = code - .replace("def_", "default_").toUpperCase(); - return Code.valueOf(formattedCode); + return Code.valueOf(code.toUpperCase()); } catch (IllegalArgumentException e) { return null; } @@ -220,10 +222,6 @@ public static void setInvertFontSizeDirection(final boolean invertFontSizeDirect Code.INVERT_FONT_SIZE_DIRECTION.set(invertFontSizeDirection); } - public static void setDumpStates(final boolean dumpStates) { - Code.DUMP_STATES.set(dumpStates); - } - public static void setWindowedWidth( final int windowedWidth ) { @@ -272,6 +270,12 @@ public static void setDefaultCanvasHPixels( Code.DEFAULT_CANVAS_H_PX.set(defaultCanvasHPixels); } + public static void setDefaultToolBreadth( + final int defaultToolBreadth + ) { + Code.DEFAULT_TOOL_BREADTH.set(defaultToolBreadth); + } + public static void setDefaultIndexPrefix( final String defIndexPrefix ) { @@ -317,10 +321,6 @@ public static boolean checkIsInvertFontSizeDirection() { return (boolean) Code.INVERT_FONT_SIZE_DIRECTION.setting.check(); } - public static boolean checkIsDumpStates() { - return (boolean) Code.DUMP_STATES.setting.check(); - } - public static int checkWindowedWidth() { return (int) Code.WINDOWED_W.setting.check(); } @@ -353,6 +353,10 @@ public static int checkDefaultCanvasHPixels() { return (int) Code.DEFAULT_CANVAS_H_PX.setting.check(); } + public static int checkDefaultToolBreadth() { + return (int) Code.DEFAULT_TOOL_BREADTH.setting.check(); + } + public static String checkDefaultIndexPrefix() { return (String) Code.DEFAULT_INDEX_PREFIX.setting.check(); } @@ -378,10 +382,6 @@ public static boolean isPixelGridOnByDefault() { return (boolean) Code.PIXEL_GRID_ON_BY_DEFAULT.setting.get(); } - public static boolean isDumpStates() { - return (boolean) Code.DUMP_STATES.setting.get(); - } - public static int getWindowedWidth() { return (int) Code.WINDOWED_W.setting.get(); } @@ -414,6 +414,10 @@ public static int getDefaultCanvasHPixels() { return (int) Code.DEFAULT_CANVAS_H_PX.setting.get(); } + public static int getDefaultToolBreadth() { + return (int) Code.DEFAULT_TOOL_BREADTH.setting.get(); + } + public static String getDefaultIndexPrefix() { return (String) Code.DEFAULT_INDEX_PREFIX.setting.get(); } diff --git a/src/com/jordanbunke/stipple_effect/visual/DialogAssembly.java b/src/com/jordanbunke/stipple_effect/visual/DialogAssembly.java index 5e0e0791..895f9020 100644 --- a/src/com/jordanbunke/stipple_effect/visual/DialogAssembly.java +++ b/src/com/jordanbunke/stipple_effect/visual/DialogAssembly.java @@ -8,7 +8,6 @@ import com.jordanbunke.delta_time.menu.MenuBuilder; import com.jordanbunke.delta_time.menu.menu_elements.MenuElement; import com.jordanbunke.delta_time.menu.menu_elements.button.SimpleMenuButton; -import com.jordanbunke.delta_time.menu.menu_elements.button.SimpleToggleMenuButton; import com.jordanbunke.delta_time.menu.menu_elements.container.MenuElementGrouping; import com.jordanbunke.delta_time.menu.menu_elements.ext.scroll.Scrollable; import com.jordanbunke.delta_time.menu.menu_elements.invisible.GatewayMenuElement; @@ -25,11 +24,12 @@ import com.jordanbunke.stipple_effect.layer.SELayer; import com.jordanbunke.stipple_effect.palette.Palette; import com.jordanbunke.stipple_effect.palette.PaletteSorter; +import com.jordanbunke.stipple_effect.project.PlaybackInfo; import com.jordanbunke.stipple_effect.project.ProjectInfo; import com.jordanbunke.stipple_effect.project.SEContext; import com.jordanbunke.stipple_effect.selection.Outliner; import com.jordanbunke.stipple_effect.selection.SEClipboard; -import com.jordanbunke.stipple_effect.selection.SelectionUtils; +import com.jordanbunke.stipple_effect.selection.Selection; import com.jordanbunke.stipple_effect.state.Operation; import com.jordanbunke.stipple_effect.state.ProjectState; import com.jordanbunke.stipple_effect.stip.ParserSerializer; @@ -40,13 +40,12 @@ import com.jordanbunke.stipple_effect.utility.settings.Settings; import com.jordanbunke.stipple_effect.visual.menu_elements.Checkbox; import com.jordanbunke.stipple_effect.visual.menu_elements.*; -import com.jordanbunke.stipple_effect.visual.menu_elements.dialog.ApproveDialogButton; -import com.jordanbunke.stipple_effect.visual.menu_elements.dialog.DynamicTextbox; -import com.jordanbunke.stipple_effect.visual.menu_elements.dialog.OutlineTextbox; -import com.jordanbunke.stipple_effect.visual.menu_elements.dialog.Textbox; +import com.jordanbunke.stipple_effect.visual.menu_elements.dialog.*; import com.jordanbunke.stipple_effect.visual.menu_elements.scrollable.VerticalScrollBox; +import com.jordanbunke.stipple_effect.visual.theme.SEColors; import com.jordanbunke.stipple_effect.visual.theme.Theme; import com.jordanbunke.stipple_effect.visual.theme.Themes; +import com.jordanbunke.stipple_effect.visual.theme.logic.ThemeLogic; import java.awt.*; import java.io.File; @@ -54,13 +53,14 @@ import java.nio.file.Path; import java.util.List; import java.util.*; +import java.util.concurrent.atomic.AtomicInteger; import java.util.function.BiConsumer; import java.util.function.Consumer; import java.util.function.Function; import java.util.function.Supplier; public class DialogAssembly { - private static final int LINE_ABOVE = -2; + private static final int LINE_ABOVE = -2, AFTER_COMMON_COLOR_ACTION_ROW = 4; public static void setDialogToSave() { final SEContext c = StippleEffect.get().getContext(); @@ -222,7 +222,7 @@ public static void setDialogToSave() { fpsLabel.getY(), 1, Constants.MIN_PLAYBACK_FPS, Constants.MAX_PLAYBACK_FPS, c.projectInfo::setFps, c.projectInfo::getFps, - i -> i, sv -> sv, i -> i + " fps", "XXX fps"); + i -> i, sv -> sv, i -> i + " FPS", "XXX FPS"); // GIF and MP4 contents final MenuElementGrouping gifContents = new MenuElementGrouping( @@ -383,14 +383,14 @@ public static void setDialogToResize() { Constants.MIN_CANVAS_W, Constants.MAX_CANVAS_W, rw -> DialogVals.setResizeWidth(rw, w, h, DialogVals.isResizePreserveAspectRatio()), - DialogVals::getResizeWidth, 3), + DialogVals::getResizeWidth, 4), heightTextbox = makeDialogPixelDynamicTextbox( heightLabel, DialogAssembly::getDialogContentOffsetFollowingLabel, Constants.MIN_CANVAS_H, Constants.MAX_CANVAS_H, rh -> DialogVals.setResizeHeight(rh, w, h, DialogVals.isResizePreserveAspectRatio()), - DialogVals::getResizeHeight, 3); + DialogVals::getResizeHeight, 4); final ThinkingMenuElement resizeDecider = new ThinkingMenuElement(() -> { if (DialogVals.getResizeBy() == DialogVals.ResizeBy.PIXELS) @@ -434,25 +434,25 @@ public static void setDialogToPad() { final int w = c.getState().getImageWidth(), h = c.getState().getImageHeight(); - DialogVals.setPadLeft(0); - DialogVals.setPadBottom(0); - DialogVals.setPadTop(0); - DialogVals.setPadRight(0); + DialogVals.setPadAll(0); final MenuBuilder mb = new MenuBuilder(); // text labels final TextLabel context = makeDialogLeftLabel(0, "Current size: " + w + "x" + h), - leftLabel = TextLabel.make(textBelowPos(context, 1), "Left:"), + allLabel = TextLabel.make(textBelowPos(context, 1), "All:"), + leftLabel = TextLabel.make(textBelowPos(allLabel, 1), "Left:"), rightLabel = TextLabel.make(textBelowPos(leftLabel), "Right:"), topLabel = TextLabel.make(textBelowPos(rightLabel), "Top:"), bottomLabel = TextLabel.make(textBelowPos(topLabel), "Bottom:"), explanation = makeValidDimensionsBottomLabel(); - mb.addAll(context, explanation, leftLabel, rightLabel, - topLabel, bottomLabel); + mb.addAll(context, explanation, allLabel, + leftLabel, rightLabel, topLabel, bottomLabel); // pad textboxes + final Textbox allTextbox = makeDialogPadTextBox(allLabel, + i -> true, DialogVals::setPadAll, DialogVals::getPadAll); final Textbox leftTextbox = makeDialogPadTextBox(leftLabel, i -> { final int pw = i + DialogVals.getPadRight() + w; return pw <= Constants.MAX_CANVAS_W && pw > 0; @@ -469,7 +469,8 @@ public static void setDialogToPad() { final int ph = i + DialogVals.getPadTop() + h; return ph <= Constants.MAX_CANVAS_W && ph > 0; }, DialogVals::setPadBottom, DialogVals::getPadBottom); - mb.addAll(leftTextbox, rightTextbox, topTextbox, bottomTextbox); + mb.addAll(allTextbox, leftTextbox, rightTextbox, + topTextbox, bottomTextbox); // size preview final String PREVIEW_PREFIX = "Preview size: "; @@ -695,16 +696,14 @@ public static void setDialogToNewProject() { final int NO_CLIPBOARD = 0; final int initialW, initialH, clipboardW, clipboardH; - final boolean hasClipboard = SEClipboard.get().hasContents(); + final boolean hasClipboard = SEClipboard.get().hasContent(); if (hasClipboard) { - final Set clipboard = SEClipboard.get() - .getContents().getPixels(); - final Coord2D tl = SelectionUtils.topLeft(clipboard), - br = SelectionUtils.bottomRight(clipboard); + final Selection clipboard = SEClipboard.get() + .getContent().getSelection(); - clipboardW = br.x - tl.x; - clipboardH = br.y - tl.y; + clipboardW = clipboard.bounds.width(); + clipboardH = clipboard.bounds.height(); initialW = clipboardW; initialH = clipboardH; @@ -748,12 +747,12 @@ public static void setDialogToNewProject() { widthLabel, DialogAssembly::getDialogContentOffsetFollowingLabel, Constants.MIN_CANVAS_W, initialW, Constants.MAX_CANVAS_W, "px", DialogVals::setNewProjectWidth, - DialogVals::getNewProjectWidth, 3); + DialogVals::getNewProjectWidth, 4); final DynamicTextbox heightTextbox = makeDialogDynamicTextbox( heightLabel, DialogAssembly::getDialogContentOffsetFollowingLabel, Constants.MIN_CANVAS_H, initialW, Constants.MAX_CANVAS_H, "px", DialogVals::setNewProjectHeight, - DialogVals::getNewProjectHeight, 3); + DialogVals::getNewProjectHeight, 4); final MenuElementGrouping contents = new MenuElementGrouping( presetLabel, defaultPreset, clipboardPreset, @@ -764,6 +763,123 @@ public static void setDialogToNewProject() { "Create", () -> StippleEffect.get().newProject(), true)); } + public static void setDialogToHistory() { + final SEContext c = StippleEffect.get().getContext(); + final MenuBuilder mb = new MenuBuilder(); + + final Coord2D thirdDisp = new Coord2D(Layout.getDialogWidth() / 3, 0); + + final TextLabel stateHeader = makeDialogLeftLabel(0, "Project state stack:"), + causeHeader = TextLabel.make( + stateHeader.getPosition().displace(thirdDisp), + "Preceding operation:"); + + mb.addAll(stateHeader, causeHeader); + + AtomicInteger i = new AtomicInteger(-1); + final List checkpoints = c.getStateManager().getCheckpoints(); + final int size = checkpoints.size(); + + final MenuBuilder smb = new MenuBuilder(); + MenuElement bottomLabel = new PlaceholderMenuElement(); + + for (int ci = 0; ci < size; ci++) { + final int checkpoint = checkpoints.get((size - 1) - ci); + + final ProjectState state = c.getStateManager().getState(checkpoint); + + final int relative = c.getStateManager().relativePosition(checkpoint), + abs = Math.abs(relative); + + final Color col; + final String text; + + if (relative < 0) { + col = SEColors.red(); + text = abs + " state" + (abs > 1 ? "s" : "") + " behind"; + } else if (relative == 0) { + col = ThemeLogic.intuitTextColor( + Settings.getTheme().panelBackground, true); + text = "[ CURRENT ]"; + } else { + col = SEColors.green(); + text = abs + " state" + (abs > 1 ? "s" : "") + " ahead"; + } + + final TextLabel stateLabel = + TextLabel.make(getDialogLeftContentPositionForRow(ci + 1), + text, col), + causeLabel = TextLabel.make( + stateLabel.getPosition().displace(thirdDisp), + state.getOperation().toString()); + smb.addAll(stateLabel, causeLabel); + + if (relative != 0) { + final SelectStateButton selectButton = SelectStateButton.make( + getDialogLeftContentPositionForRow(ci + 1) + .displace(thirdDisp.x * 2, Layout.DIALOG_CONTENT_COMP_OFFSET_Y), + () -> i.set(checkpoint), () -> i.get() != checkpoint); + smb.add(selectButton); + } + + bottomLabel = stateLabel; + } + + final int scrollerEndY = (Layout.getCanvasMiddle().y + + Layout.getDialogHeight() / 2) - ((2 * Layout.CONTENT_BUFFER_PX) + + Layout.STD_TEXT_BUTTON_H); + + final Coord2D scrollerPos = Layout.getDialogPosition().displace(0, + (int)(3.5 * Layout.STD_TEXT_BUTTON_INC) + + Layout.TEXT_Y_OFFSET - Layout.BUTTON_DIM); + final Bounds2D scrollerDims = new Bounds2D(Layout.getDialogWidth(), + scrollerEndY - scrollerPos.y); + + final int realBottomY = bottomLabel.getRenderPosition().y + + bottomLabel.getHeight() + Layout.STD_TEXT_BUTTON_H; + + mb.add(new VerticalScrollBox(scrollerPos, scrollerDims, + Arrays.stream(smb.build().getMenuElements()) + .map(Scrollable::new).toArray(Scrollable[]::new), + realBottomY, 0)); + + final Supplier precondition = () -> i.get() >= 0; + + final DynamicLabel selectedLabel = + makeDialogLeftDynamicLabelAtBottom(() -> { + if (precondition.get()) { + final int relative = c.getStateManager().relativePosition(i.get()), + abs = Math.abs(relative); + + final String before = "Selected state ", + after = "the current state", middle; + + if (relative < 0) + middle = abs + " state" + + (abs > 1 ? "s" : "") + " behind "; + else if (relative == 0) + middle = ""; + else + middle = abs + " state" + + (abs > 1 ? "s" : "") + " ahead of "; + + return before + middle + after; + } else + return "No selection"; + }); + mb.add(selectedLabel); + + setDialog(assembleDialog("History of " + + c.projectInfo.getFormattedName(false, false) + "...", + new MenuElementGrouping(mb.build().getMenuElements()), + precondition, "Revert...", + () -> setDialogToPreviewAction( + c.getStateManager().getState(i.get()), + () -> c.getStateManager().setState(i.get(), c), + DialogAssembly::setDialogToHistory, + "project state reversion"), false)); + } + public static void setDialogToNewFont() { DialogVals.setNewFontPixelSpacing(Constants.DEFAULT_FONT_PX_SPACING, false); DialogVals.setHasLatinEx(false, false); @@ -813,7 +929,7 @@ public static void setDialogToNewFont() { spacingLabel, DialogAssembly::getDialogContentOffsetFollowingLabel, Constants.MIN_FONT_PX_SPACING, Constants.MAX_FONT_PX_SPACING, DialogVals::setNewFontPixelSpacing, - DialogVals::getNewFontPixelSpacing, 2); + DialogVals::getNewFontPixelSpacing, 3); mb.addAll(spacingLabel, spacingTextbox); // character-specific final TextLabel charSpecificLabel = @@ -833,10 +949,8 @@ public static void setDialogToNewFont() { GraphicsUtils.makeStandardTextButton("Upload", getDialogContentOffsetFollowingLabel(asciiLabel), SEFonts::uploadASCIISourceFile); - final DynamicLabel asciiConfirmation = new DynamicLabel( + final DynamicLabel asciiConfirmation = DynamicLabel.make( getDialogRightContentPositionForRow(lines), - MenuElement.Anchor.LEFT_TOP, - Settings.getTheme().textLight.get(), () -> DialogVals.getAsciiStatus().getMessage(), Layout.getDialogWidth()); mb.addAll(asciiLabel, asciiButton, asciiConfirmation); @@ -855,10 +969,8 @@ public static void setDialogToNewFont() { GraphicsUtils.makeStandardTextButton("Upload", getDialogContentOffsetFollowingLabel(latinExLabel), SEFonts::uploadLatinExtendedSourceFile); - final DynamicLabel latinExConfirmation = new DynamicLabel( + final DynamicLabel latinExConfirmation = DynamicLabel.make( getDialogRightContentPositionForRow(lines + 1), - MenuElement.Anchor.LEFT_TOP, - Settings.getTheme().textLight.get(), () -> DialogVals.getLatinExStatus().getMessage(), Layout.getDialogWidth()); final MenuElementGrouping latinExContent = new MenuElementGrouping( @@ -955,7 +1067,7 @@ private static void setDialogToAYS( final String actionLabel, final String consequence, final Runnable onApprove ) { - final GameImage warningText = GraphicsUtils.uiText() + final GameImage warningText = GraphicsUtils.uiText(simpleTextColor()) .addText(consequence).build().draw(); final StaticMenuElement warning = new StaticMenuElement( Layout.getCanvasMiddle(), new Bounds2D(warningText.getWidth(), @@ -1091,18 +1203,9 @@ public static void setDialogToPanelManager() { retrievalFunctionMap.get(labelTexts[i]); if (isProject) { - final String[] toggleText = - new String[] { "Expanded", "Collapsed" }; - final GameImage[] bases = makeToggleButtonSet(toggleText); - - final SimpleToggleMenuButton toggle = new SimpleToggleMenuButton( + final TextToggleButton toggle = TextToggleButton.make( getDialogContentOffsetFollowingLabel(label), - new Bounds2D(Layout.DIALOG_CONTENT_SMALL_W_ALLOWANCE, - Layout.STD_TEXT_BUTTON_H), - MenuElement.Anchor.LEFT_TOP, true, bases, - Arrays.stream(bases) - .map(GraphicsUtils::drawHighlightedButton) - .toArray(GameImage[]::new), + new String[] { "Expanded", "Collapsed" }, new Runnable[] { () -> adj.accept(false), () -> adj.accept(true) @@ -1131,9 +1234,22 @@ public static void setDialogToHSVShift() { makeCommonColorOperationElements(mb, c); - final TextLabel hueLabel = makeDialogLeftLabel(3, "Shift hue:"), - satLabel = makeDialogLeftLabel(4, "Shift sat.:"), - valueLabel = makeDialogLeftLabel(5, "Shift value:"); + final TextLabel hueLabel = makeDialogLeftLabel( + AFTER_COMMON_COLOR_ACTION_ROW, "Shift hue:"); + + final String T_SHIFT = "Shift ", T_SCALE = "Scale ", + T_SAT = "sat:", T_VAL = "value:"; + + final DynamicLabel satLabel = makeDynamicLabel( + textBelowPos(hueLabel), + () -> (DialogVals.isShiftingSat() + ? T_SHIFT : T_SCALE) + T_SAT, + T_SCALE + T_SAT), + valueLabel = makeDynamicLabel( + textBelowPos(satLabel), + () -> (DialogVals.isShiftingValue() + ? T_SHIFT : T_SCALE) + T_VAL, + T_SCALE + T_VAL); final IncrementalRangeElements h = IncrementalRangeElements.makeForInt(hueLabel, @@ -1144,7 +1260,7 @@ public static void setDialogToHSVShift() { i -> i, i -> i, String::valueOf, "-XXX"); mb.addAll(hueLabel, h.decButton, h.incButton, h.slider, h.value); - final double MIN = Constants.MIN_SV_SHIFT, MAX = Constants.MAX_SV_SHIFT; + final double MIN = Constants.MIN_SV_SCALE, MAX = Constants.MAX_SV_SCALE; final int STEPS = 5; final double[] bounds = new double[] { MIN, 1d, 2d, 5d, 10d, MAX }, increments = new double[] { 0.05, 0.1, 0.25, 0.5, 2.5 }; @@ -1161,36 +1277,36 @@ public static void setDialogToHSVShift() { } final Runnable satDecrement = () -> { - final double was = DialogVals.getSatShift(); + final double was = DialogVals.getSatScale(); for (int i = 0; i < STEPS; i++) if (was <= bounds[i + 1]) { - DialogVals.setSatShift(Math.max(was - increments[i], MIN)); + DialogVals.setSatScale(Math.max(was - increments[i], MIN)); break; } }, satIncrement = () -> { - final double was = DialogVals.getSatShift(); + final double was = DialogVals.getSatScale(); for (int i = 0; i < STEPS; i++) if (was < bounds[i + 1]) { - DialogVals.setSatShift(Math.min(was + increments[i], MAX)); + DialogVals.setSatScale(Math.min(was + increments[i], MAX)); break; } }, valueDecrement = () -> { - final double was = DialogVals.getValueShift(); + final double was = DialogVals.getValueScale(); for (int i = 0; i < STEPS; i++) if (was <= bounds[i + 1]) { - DialogVals.setValueShift( + DialogVals.setValueScale( Math.max(was - increments[i], MIN)); break; } }, valueIncrement = () -> { - final double was = DialogVals.getValueShift(); + final double was = DialogVals.getValueScale(); for (int i = 0; i < STEPS; i++) if (was < bounds[i + 1]) { - DialogVals.setValueShift( + DialogVals.setValueScale( Math.min(was + increments[i], MAX)); break; } @@ -1235,41 +1351,87 @@ public static void setDialogToHSVShift() { Function svfFormat = d -> { final int _20x = (int) Math.round(d * 20); - return "x" + (_20x / 20d); + return (_20x / 20d) + "x"; }; - final IncrementalRangeElements s = + final MenuElementGrouping satScale = new MenuElementGrouping( IncrementalRangeElements.makeForDouble(satLabel, satLabel.getY() + Layout.DIALOG_CONTENT_COMP_OFFSET_Y, satLabel.getY(), satDecrement, satIncrement, - Constants.MIN_SV_SHIFT, Constants.MAX_SV_SHIFT, - DialogVals::setSatShift, DialogVals::getSatShift, + Constants.MIN_SV_SCALE, Constants.MAX_SV_SCALE, + DialogVals::setSatScale, DialogVals::getSatScale, svfToSlider, svfFromSlider, svfFormat, - "x" + "X".repeat(20)); - mb.addAll(satLabel, s.decButton, s.incButton, s.slider, s.value); + "x" + "X".repeat(20)).getAll()), + satShift = new MenuElementGrouping( + IncrementalRangeElements.makeForInt(satLabel, + satLabel.getY() + Layout.DIALOG_CONTENT_COMP_OFFSET_Y, + satLabel.getY(), + 1, Constants.MIN_SV_SHIFT, Constants.MAX_SV_SHIFT, + DialogVals::setSatShift, DialogVals::getSatShift, + i -> i, i -> i, String::valueOf, "XXXX").getAll()); + + final ThinkingMenuElement satManager = new ThinkingMenuElement( + () -> DialogVals.isShiftingSat() ? satShift : satScale); + + mb.addAll(satLabel, satManager); - final IncrementalRangeElements v = + final MenuElementGrouping valueScale = new MenuElementGrouping( IncrementalRangeElements.makeForDouble(valueLabel, valueLabel.getY() + Layout.DIALOG_CONTENT_COMP_OFFSET_Y, valueLabel.getY(), valueDecrement, valueIncrement, - Constants.MIN_SV_SHIFT, Constants.MAX_SV_SHIFT, - DialogVals::setValueShift, DialogVals::getValueShift, + Constants.MIN_SV_SCALE, Constants.MAX_SV_SCALE, + DialogVals::setValueScale, DialogVals::getValueScale, svfToSlider, svfFromSlider, svfFormat, - "x" + "X".repeat(20)); - mb.addAll(valueLabel, v.decButton, v.incButton, v.slider, v.value); + "x" + "X".repeat(20)).getAll()), + valueShift = new MenuElementGrouping( + IncrementalRangeElements.makeForInt(valueLabel, + valueLabel.getY() + Layout.DIALOG_CONTENT_COMP_OFFSET_Y, + valueLabel.getY(), + 1, Constants.MIN_SV_SHIFT, Constants.MAX_SV_SHIFT, + DialogVals::setValueShift, DialogVals::getValueShift, + i -> i, i -> i, String::valueOf, "XXXX").getAll()); + + final ThinkingMenuElement valueManager = new ThinkingMenuElement( + () -> DialogVals.isShiftingValue() ? valueShift : valueScale); + + mb.addAll(valueLabel, valueManager); + + final String RESET = "Reset"; + final Coord2D firstResetPos = + getDialogRightContentPositionForRow(AFTER_COMMON_COLOR_ACTION_ROW) + .displace(0, Layout.DIALOG_CONTENT_COMP_OFFSET_Y); final SimpleMenuButton resetHue = - GraphicsUtils.makeStandardTextButton("Reset", - getDialogRightContentPositionForRow(3), + GraphicsUtils.makeStandardTextButton(RESET, firstResetPos, () -> DialogVals.setHueShift(0)), - resetSat = GraphicsUtils.makeStandardTextButton("Reset", - getDialogRightContentPositionForRow(4), - () -> DialogVals.setSatShift(1d)), - resetValue = GraphicsUtils.makeStandardTextButton("Reset", - getDialogRightContentPositionForRow(5), - () -> DialogVals.setValueShift(1d)); - - mb.addAll(resetHue, resetSat, resetValue); + resetSat = GraphicsUtils.makeStandardTextButton(RESET, + textBelowPos(resetHue), + () -> { + DialogVals.setSatScale(1d); + DialogVals.setSatShift(0); + }), + resetValue = GraphicsUtils.makeStandardTextButton(RESET, + textBelowPos(resetSat), + () -> { + DialogVals.setValueScale(1d); + DialogVals.setValueShift(0); + }); + + final String[] toggleTexts = new String[] { "Scale", "Shift" }; + final Runnable[] toggleActions = new Runnable[] { () -> {}, () -> {} }; + final TextToggleButton + toggleSat = TextToggleButton.make( + getDialogContentToRightOfContent(resetSat), + toggleTexts, toggleActions, + () -> DialogVals.isShiftingSat() ? 1 : 0, + DialogVals::toggleShiftingSat), + toggleValue = TextToggleButton.make( + getDialogContentToRightOfContent(resetValue), + toggleTexts, toggleActions, + () -> DialogVals.isShiftingValue() ? 1 : 0, + DialogVals::toggleShiftingValue); + + mb.addAll(resetHue, resetSat, resetValue, toggleSat, toggleValue); setDialog(assembleDialog("Shift color levels...", new MenuElementGrouping(mb.build().getMenuElements()), @@ -1293,13 +1455,13 @@ public static void setDialogToColorScript() { makeCommonColorOperationElements(mb, c); final TextLabel scriptLabel = makeDialogLeftLabel( - 3, "Script file:"); + AFTER_COMMON_COLOR_ACTION_ROW, "Script file:"); final SimpleMenuButton scriptButton = GraphicsUtils.makeStandardTextButton("Upload", getDialogContentOffsetFollowingLabel(scriptLabel), StippleEffect.get()::openColorScript); final DynamicLabel scriptConfirmation = makeDynamicLabel( - getDialogRightContentPositionForRow(3), + getDialogRightContentPositionForRow(AFTER_COMMON_COLOR_ACTION_ROW), DialogVals::colorScriptMessage, "X".repeat(50)); mb.addAll(scriptLabel, scriptButton, scriptConfirmation); @@ -1316,6 +1478,20 @@ private static void setDialogToPreviewAction( final ProjectState preview, final Runnable backButtonAction, final String previewAppend ) { + + final SEContext c = StippleEffect.get().getContext(); + + setDialogToPreviewAction(preview, () -> { + preview.markAsCheckpoint(false); + c.getStateManager() + .performAction(preview, Operation.EDIT_IMAGE); + }, backButtonAction, "preview of " + previewAppend); + } + + private static void setDialogToPreviewAction( + final ProjectState preview, final Runnable onApproval, + final Runnable backButtonAction, final String previewAppend + ) { if (preview == null) { setDialogToScriptErrors(); return; @@ -1376,19 +1552,92 @@ private static void setDialogToPreviewAction( previewContent[i] = composed.submit(); } - final AnimationMenuElement previewAnim = new AnimationMenuElement( + final ActionPreviewer previewer = new ActionPreviewer( new Coord2D(Layout.getCanvasMiddle().x, - textBelowPos(backButton, 1).y), new Bounds2D(pw, ph), - MenuElement.Anchor.CENTRAL_TOP, 20, previewContent); - mb.add(previewAnim); + textBelowPos(backButton, 1).y), + new Bounds2D(pw, ph), previewContent, + preview.getFrameDurations().stream() + .mapToDouble(d -> d).toArray()); + mb.add(previewer); + + final PlaybackInfo playbackInfo = previewer.getPlaybackInfo(); + + if (fc > 1) { + // frame and playback mode controls + final MenuElement firstFrame = + GraphicsUtils.generateIconButton(IconCodes.TO_FIRST_FRAME, + backButton.getPosition() + .displace(0, Layout.DIALOG_CONTENT_INC_Y), + () -> true, previewer::toFirstFrame), + previousFrame = GraphicsUtils.generateIconButton( + IconCodes.PREVIOUS, firstFrame.getRenderPosition() + .displace(Layout.BUTTON_INC, 0), + () -> true, previewer::previousFrame), + playStop = GraphicsUtils.generateIconToggleButton( + previousFrame.getRenderPosition() + .displace(Layout.BUTTON_INC, 0), + new String[] { IconCodes.PLAY, IconCodes.STOP }, + new Runnable[] { + playbackInfo::play, playbackInfo::stop + }, () -> playbackInfo.isPlaying() ? 1 : 0, () -> {}, + () -> true, IconCodes.PLAY), + nextFrame = GraphicsUtils.generateIconButton( + IconCodes.NEXT, playStop.getRenderPosition() + .displace(Layout.BUTTON_INC, 0), + () -> true, previewer::nextFrame), + lastFrame = GraphicsUtils.generateIconButton( + IconCodes.TO_LAST_FRAME, nextFrame.getRenderPosition() + .displace(Layout.BUTTON_INC, 0), + () -> true, previewer::toLastFrame); + + final PlaybackInfo.Mode[] validModes = new PlaybackInfo.Mode[] { + PlaybackInfo.Mode.FORWARDS, PlaybackInfo.Mode.BACKWARDS, + PlaybackInfo.Mode.LOOP, PlaybackInfo.Mode.PONG_FORWARDS + }; + final MenuElement playbackModeButton = + GraphicsUtils.generateIconToggleButton( + lastFrame.getRenderPosition().displace( + Layout.BUTTON_INC, 0), + Arrays.stream(validModes) + .map(PlaybackInfo.Mode::getIconCode) + .toArray(String[]::new), + Arrays.stream(validModes) + .map(mode -> (Runnable) () -> {}) + .toArray(Runnable[]::new), + () -> playbackInfo.getMode().buttonIndex(), + playbackInfo::toggleMode, () -> true, IconCodes.LOOP); + final DynamicLabel frameTracker = makeDynamicLabel( + playbackModeButton.getRenderPosition().displace( + Layout.BUTTON_DIM + Layout.CONTENT_BUFFER_PX, + -Layout.BUTTON_OFFSET + Layout.TEXT_Y_OFFSET), + () -> "Frm. " + (previewer.getFrameIndex() + 1) + + "/" + previewer.getFrameCount(), + "Frm. XXX/XXX"); + + mb.addAll(firstFrame, previousFrame, playStop, nextFrame, + lastFrame, playbackModeButton, frameTracker); + + // playback speed (FPS) + final StaticMenuElement fpsReference = new StaticMenuElement( + getRightColumnFromLeftDisplacement( + firstFrame.getPosition()) + .displace(-(Layout.BUTTON_DIM + + Layout.CONTENT_BUFFER_PX), 0), + Layout.ICON_DIMS, MenuElement.Anchor.LEFT_TOP, + GameImage.dummy()); + final int fpsButtonY = firstFrame.getY(); + final IncrementalRangeElements fps = + IncrementalRangeElements.makeForInt(fpsReference, fpsButtonY, + (fpsButtonY - Layout.BUTTON_OFFSET) + Layout.TEXT_Y_OFFSET, + 1, Constants.MIN_PLAYBACK_FPS, Constants.MAX_PLAYBACK_FPS, + playbackInfo::setFps, playbackInfo::getFps, + i -> i, sv -> sv, sv -> sv + " FPS", "XXX FPS"); + mb.addAll(fps.decButton, fps.incButton, fps.slider, fps.value); + } setDialog(assembleDialog("Preview of " + previewAppend, new MenuElementGrouping(mb.build().getMenuElements()), - () -> true, "Apply", () -> { - preview.markAsCheckpoint(false); - c.getStateManager() - .performAction(preview, Operation.EDIT_IMAGE); - }, true)); + () -> true, "Apply", onApproval, true)); } public static void setDialogToSavePalette(final Palette palette) { @@ -1529,21 +1778,17 @@ public static void setDialogToProgramSettings() { Settings.resetAssignments(); Arrays.stream(DialogVals.SettingScreen.values()).forEach(ss -> { - final GameImage baseSS = GraphicsUtils.drawTextButton( - Layout.STD_TEXT_BUTTON_W, ss.toString(), false), - highlighedSS = GraphicsUtils.drawHighlightedButton(baseSS); - final Coord2D ssPos = Layout.getDialogPosition().displace( Layout.CONTENT_BUFFER_PX + (ss.ordinal() * (Layout.STD_TEXT_BUTTON_W + Layout.BUTTON_OFFSET)), Layout.CONTENT_BUFFER_PX + (int)(1.5 * Layout.STD_TEXT_BUTTON_INC)); - mb.add(new SimpleMenuButton(ssPos, - new Bounds2D(baseSS.getWidth(), baseSS.getHeight()), - MenuElement.Anchor.LEFT_TOP, true, - () -> DialogVals.setSettingScreen(ss), - baseSS, highlighedSS)); + mb.add(SelectableListItemButton.make(ssPos, + Layout.STD_TEXT_BUTTON_W, ss.toString(), ss.ordinal(), + () -> DialogVals.getSettingScreen().ordinal(), + i -> DialogVals.setSettingScreen( + DialogVals.SettingScreen.values()[i]))); }); // decision logic @@ -1617,6 +1862,55 @@ public static void setDialogToLayerSettings(final int index) { }, true)); } + public static void setDialogToFrameProperties(final int index) { + final SEContext c = StippleEffect.get().getContext(); + final MenuBuilder mb = new MenuBuilder(); + + DialogVals.setFrameDuration(c.getState().getFrameDurations().get(index)); + + final TextLabel durationLabel = makeDialogLeftLabel(0, "Frame duration:"); + + final double STEP = 0.1, DIV = 10d, + MIN = Constants.MIN_FRAME_DURATION, + MAX = Constants.MAX_FRAME_DURATION; + final Runnable fDecrement = () -> { + final double was = DialogVals.getFrameDuration(), + v = Math.max(MIN, was - STEP); + + DialogVals.setFrameDuration(Math.round(v * DIV) / DIV); + }, fIncrement = () -> { + final double was = DialogVals.getFrameDuration(), + v = Math.min(MAX, was + STEP); + + DialogVals.setFrameDuration(Math.round(v * DIV) / DIV); + }; + final IncrementalRangeElements duration = + IncrementalRangeElements.makeForDouble(durationLabel, + durationLabel.getY() + + Layout.DIALOG_CONTENT_COMP_OFFSET_Y, + durationLabel.getY(), fDecrement, fIncrement, + MIN, MAX, DialogVals::setFrameDuration, + DialogVals::getFrameDuration, + o -> (int)(o * DIV), sv -> sv / DIV, + o -> o + "x", "XXXXx"); + + final SimpleMenuButton resetDuration = + GraphicsUtils.makeStandardTextButton("Reset", + getDialogRightContentPositionForRow(0) + .displace(0, Layout.DIALOG_CONTENT_COMP_OFFSET_Y), + () -> DialogVals.setFrameDuration( + Constants.DEFAULT_FRAME_DURATION)); + + mb.addAll(durationLabel, duration.decButton, duration.incButton, + duration.slider, duration.value, resetDuration); + + setDialog(assembleDialog("Frame " + (index + 1) + " | Properties", + new MenuElementGrouping(mb.build().getMenuElements()), + () -> true, Constants.GENERIC_APPROVAL_TEXT, + () -> c.changeFrameDuration(DialogVals.getFrameDuration(), index), + true)); + } + public static void setDialogToSplashScreen() { final MenuBuilder mb = new MenuBuilder(); final Theme t = Settings.getTheme(); @@ -1632,13 +1926,13 @@ public static void setDialogToSplashScreen() { final GameImage background = new GameImage(w, h); background.free(); - background.fillRectangle(t.splashBackground.get(), 0, 0, w, h); + background.fillRectangle(t.splashBackground, 0, 0, w, h); mb.add(new SimpleMenuButton(new Coord2D(), new Bounds2D(w, h), MenuElement.Anchor.LEFT_TOP, true, () -> StippleEffect.get().clearDialog(), background, background)); // version - final GameImage version = GraphicsUtils.uiText(t.splashText.get()) + final GameImage version = GraphicsUtils.uiText(t.splashText) .addText(StippleEffect.getVersion()).build().draw(); mb.add(new StaticMenuElement(new Coord2D(w / 2, h), @@ -1646,7 +1940,7 @@ public static void setDialogToSplashScreen() { MenuElement.Anchor.CENTRAL_BOTTOM, version)); // gateway - final GameImage ctc = GraphicsUtils.uiText(t.splashFlashingText.get()) + final GameImage ctc = GraphicsUtils.uiText(t.splashFlashingText) .addText("Click anywhere to continue").build().draw(); mb.add(new AnimationMenuElement(new Coord2D(w - Layout.CONTENT_BUFFER_PX, h), @@ -1655,14 +1949,15 @@ public static void setDialogToSplashScreen() { ctc, GameImage.dummy())); // animation frames - final GameImage[] frames = SplashLoader.loadAnimationFrames(); + final GameImage[] frames = t.logic.loadSplash(); mb.add(new AnimationMenuElement(Layout.getCanvasMiddle(), new Bounds2D(frames[0].getWidth(), frames[0].getHeight()), - MenuElement.Anchor.CENTRAL, 5, frames)); + MenuElement.Anchor.CENTRAL, t.logic.ticksPerSplashFrame(), + frames)); // subtitle - final GameImage subtitle = GraphicsUtils.uiText(t.splashText.get()) - .addText("Pixel art editor and animator").addLineBreak() + final GameImage subtitle = GraphicsUtils.uiText(t.splashText) + .addText(t.subtitle).addLineBreak() .addText("Jordan Bunke, 2023-2024") .build().draw(); @@ -1680,39 +1975,48 @@ private static void setDialog(final Menu dialog) { private static void makeCommonColorOperationElements( final MenuBuilder mb, final SEContext c ) { - final List vs = - Arrays.stream(DialogVals.Scope.values()) - .filter(s -> s != DialogVals.Scope.SELECTION || - c.getState().hasSelection()).toList(); + final DialogVals.Scope[] vs = DialogVals.Scope.values(); - final DialogVals.Scope was = DialogVals.getScope(); - final boolean hasScope = vs.contains(was); - - if (!hasScope) - DialogVals.setScope(vs.get(0)); - - final int initialIndex = vs.indexOf(DialogVals.getScope()); + final int initialIndex = DialogVals.getScope().ordinal(); + final boolean hasSelection = c.getState().hasSelection(); final TextLabel scopeLabel = makeDialogLeftLabel(0, "Scope:"), - flagLabel = TextLabel.make(textBelowPos(scopeLabel), - "Include disabled layers?"); + disabledLayersLabel = TextLabel.make( + textBelowPos(scopeLabel), + "Include disabled layers?"), + ignoreSelectionLabel = TextLabel.make( + textBelowPos(disabledLayersLabel), + "Ignore selection?"); final Dropdown dropdown = Dropdown.forDialog( getDialogContentOffsetFollowingLabel(scopeLabel), Layout.DIALOG_CONTENT_BIG_W_ALLOWANCE, - vs.stream().map(DialogVals.Scope::toString) + Arrays.stream(vs).map(DialogVals.Scope::toString) .toArray(String[]::new), - vs.stream().map(s -> (Runnable) () -> DialogVals.setScope(s)) + Arrays.stream(vs).map(s -> (Runnable) () -> DialogVals.setScope(s)) .toArray(Runnable[]::new), () -> initialIndex); - final Checkbox checkbox = new Checkbox( - getDialogContentOffsetFollowingLabel(flagLabel), - new ConcreteProperty<>(DialogVals::isIncludeDisabledLayers, - DialogVals::setIncludeDisabledLayers)); - final GatewayMenuElement flagGate = new GatewayMenuElement( - new MenuElementGrouping(flagLabel, checkbox), - () -> DialogVals.getScope().considersLayers()); + final Checkbox disabledLayersCheckbox = new Checkbox( + getDialogContentOffsetFollowingLabel(disabledLayersLabel), + new ConcreteProperty<>( + DialogVals::isIncludeDisabledLayers, + DialogVals::setIncludeDisabledLayers)), + ignoreSelectionCheckbox = new Checkbox( + getDialogContentOffsetFollowingLabel( + ignoreSelectionLabel), + new ConcreteProperty<>( + DialogVals::isIgnoreSelection, + DialogVals::setIgnoreSelection)); + final GatewayMenuElement disabledLayersGate = + new GatewayMenuElement( + new MenuElementGrouping( + disabledLayersLabel, disabledLayersCheckbox), + () -> DialogVals.getScope().considersLayers()), + ignoreSelectionGate = new GatewayMenuElement( + new MenuElementGrouping( + ignoreSelectionLabel, ignoreSelectionCheckbox), + () -> hasSelection); - mb.addAll(scopeLabel, dropdown, flagGate); + mb.addAll(scopeLabel, dropdown, disabledLayersGate, ignoreSelectionGate); } private static void makeStitchElementsForSaveSpriteSheet( @@ -1979,14 +2283,6 @@ else if (folderPathName.length() + File.separator.length() + }); } - private static GameImage[] makeToggleButtonSet( - final String... buttonTexts) { - return Arrays.stream(buttonTexts) - .map(t -> GraphicsUtils.drawTextButton( - Layout.DIALOG_CONTENT_SMALL_W_ALLOWANCE, t, false)) - .toArray(GameImage[]::new); - } - private static DynamicTextbox makeDialogPixelDynamicTextbox( final MenuElement label, final Function offsetFunction, @@ -2079,8 +2375,8 @@ private static DynamicLabel makeDynamicLabel( final Coord2D position, final Supplier getter, final String widestTextCase ) { - return new DynamicLabel(position, MenuElement.Anchor.LEFT_TOP, - Settings.getTheme().textLight.get(), getter, widestTextCase); + return DynamicLabel.make(position, getter, + DynamicLabel.getWidth(widestTextCase)); } private static TextLabel makeValidDimensionsBottomLabel() { @@ -2096,10 +2392,8 @@ private static DynamicLabel makeDialogLeftDynamicLabelAtBottom( .displace(0, -(Layout.DIALOG_CONTENT_INC_Y + Layout.CONTENT_BUFFER_PX)).y; - return new DynamicLabel( + return DynamicLabel.make( new Coord2D(Layout.getDialogContentInitial().x, y), - MenuElement.Anchor.LEFT_TOP, - Settings.getTheme().textLight.get(), getter, Layout.getDialogWidth()); } @@ -2177,6 +2471,11 @@ private static String cutOffAtNextSpace(String s, int i) { return s; } + private static Color simpleTextColor() { + return ThemeLogic.intuitTextColor( + Settings.getTheme().panelBackground, true); + } + private static VerticalScrollBox assembleScroller( final DialogVals.SettingScreen settingScreen ) { @@ -2188,7 +2487,7 @@ private static VerticalScrollBox assembleScroller( Layout.CONTENT_BUFFER_PX + Layout.BUTTON_BORDER_PX, (int)(3.5 * Layout.STD_TEXT_BUTTON_INC)); mb.add(TextLabel.make(titlePosition, settingScreen.getTitle(), - t.textMenuHeading.get(), 2d)); + t.textMenuHeading, 2d)); final int initialYIndex = 4; // initialize in every execution path @@ -2208,8 +2507,11 @@ private static VerticalScrollBox assembleScroller( "Width:"), newProjectHeightLabel = makeDialogRightLabel( newProjectWidthLabel, "Height:"), - frameAffixLabel = TextLabel.make( + defaultToolBreadthLabel = TextLabel.make( textBelowPos(newProjectWidthLabel, 1), + "Default tool breadth:"), + frameAffixLabel = TextLabel.make( + textBelowPos(defaultToolBreadthLabel, 1), "Default separate PNGs frame affixes"), prefixLabel = TextLabel.make( textBelowPos(frameAffixLabel), @@ -2234,13 +2536,19 @@ private static VerticalScrollBox assembleScroller( DialogAssembly::getDialogContentOffsetFollowingLabel, Constants.MIN_CANVAS_W, Constants.MAX_CANVAS_W, Settings::setDefaultCanvasWPixels, - Settings::checkDefaultCanvasWPixels, 3), + Settings::checkDefaultCanvasWPixels, 4), heightTextbox = makeDialogPixelDynamicTextbox( newProjectHeightLabel, DialogAssembly::getDialogContentOffsetFollowingLabel, Constants.MIN_CANVAS_H, Constants.MAX_CANVAS_H, Settings::setDefaultCanvasHPixels, - Settings::checkDefaultCanvasHPixels, 3); + Settings::checkDefaultCanvasHPixels, 4), + toolBreadthTextbox = makeDialogPixelDynamicTextbox( + defaultToolBreadthLabel, + DialogAssembly::getDialogContentOffsetFollowingLabel, + Constants.MIN_BREADTH, Constants.MAX_BREADTH, + Settings::setDefaultToolBreadth, + Settings::checkDefaultToolBreadth, 3); final String FA_EXAMPLE = "Example: base_name", FA_MAX_VALUE = "WWWWW"; final DynamicLabel frameAffixExample = makeDynamicLabel( @@ -2261,9 +2569,10 @@ private static VerticalScrollBox assembleScroller( pixelGridDefaultLabel, pixelGridCheckbox, defaultNewProjectSizeLabel, newProjectWidthLabel, widthTextbox, - newProjectHeightLabel, heightTextbox) - .addAll(frameAffixLabel, prefixLabel, suffixLabel, - frameAffixExample, prefixTextbox, suffixTextbox); + newProjectHeightLabel, heightTextbox, + defaultToolBreadthLabel, toolBreadthTextbox, + frameAffixLabel, prefixLabel, suffixLabel, + frameAffixExample, prefixTextbox, suffixTextbox); // update as new settings are added to category yield frameAffixExample; @@ -2324,7 +2633,7 @@ private static VerticalScrollBox assembleScroller( "Theme:"), windowedSizeLabel = TextLabel.make( textBelowPos(fontLabel, 1), - "Program size when windowed"), + "Windowed program size"), windowedWidthLabel = TextLabel.make( textBelowPos(windowedSizeLabel), "Window width:"), @@ -2453,30 +2762,6 @@ private static VerticalScrollBox assembleScroller( // update as new settings are added to category yield pixelGridLimits2; } - case ADVANCED -> { - final TextLabel dumpStatesLabel = makeDialogLeftLabel( - initialYIndex, - "Dump old project states when memory is low?"), - dsContext1 = TextLabel.make(textBelowPos(dumpStatesLabel), - "This will dump previous project state iterations from"), - dsContext2 = TextLabel.make(textBelowPos(dsContext1), - "the state stack, making them inaccessible via undo/redo."), - dsContext3 = TextLabel.make(textBelowPos(dsContext2), - "Disabling this may lead the program to crash;"), - dsContext4 = TextLabel.make(textBelowPos(dsContext3), - "this instability will be addressed in future updates."); - - final Checkbox dumpStatesCheckbox = new Checkbox( - getDialogContentOffsetFollowingLabel(dumpStatesLabel), - new ConcreteProperty<>( - Settings::checkIsDumpStates, - Settings::setDumpStates)); - - mb.addAll(dumpStatesLabel, dumpStatesCheckbox, - dsContext1, dsContext2, dsContext3, dsContext4); - - yield dsContext4; - } }; // scrolling container @@ -2515,7 +2800,7 @@ private static VerticalScrollBox assembleScroller( final double titleSize = 2d; final TextLabel headingLabel = TextLabel.make(contentStart.displace( 0, initialbottomY), infoScreen.getTitle(), - Settings.getTheme().textMenuHeading.get(), titleSize); + Settings.getTheme().textMenuHeading, titleSize); initialbottomY += (int)(incY * titleSize) + Layout.BUTTON_INC; contentAssembler.add(headingLabel); @@ -2613,7 +2898,8 @@ private static int assembleInfoScreenContents( final TextLabel name = TextLabel.make(contentStart.displace( (hasIcon ? Layout.BUTTON_INC : 0) + Layout.CONTENT_BUFFER_PX, bottomY + Layout.TEXT_Y_OFFSET - Layout.BUTTON_BORDER_PX), - headings[i], hasIcon ? t.textShortcut.get() : t.affixTextLight.get()); + headings[i], hasIcon ? t.textShortcut + : ThemeLogic.intuitTextColor(t.panelBackground, false)); contentAssembler.add(name); bottomY += incY; @@ -2629,8 +2915,9 @@ private static int assembleInfoScreenContents( final TextLabel segmentText = TextLabel.make( contentStart.displace(indent + offsetX, bottomY + Layout.TEXT_Y_OFFSET), - lineSegments[j], j % 2 == 1 - ? t.textShortcut.get() : t.textLight.get()); + lineSegments[j], j % 2 == 1 ? t.textShortcut + : ThemeLogic.intuitTextColor( + t.panelBackground, true)); contentAssembler.add(segmentText); offsetX += segmentText.getWidth() + Layout.BUTTON_BORDER_PX; @@ -2773,14 +3060,16 @@ private static int assembleProjectInfoScreenContents( IconCodes.UNDO, IconCodes.GRANULAR_UNDO, IconCodes.GRANULAR_REDO, - IconCodes.REDO + IconCodes.REDO, + IconCodes.HISTORY }, new String[] { "Info", "Open panel manager", "Program Settings", "New Project", "Import", "Save", "Save As...", "Resize", "Pad", "Stitch or split frames", "Preview", "Automation script", - "Undo", "Granular Undo", "Granular Redo", "Redo" + "Undo", "Granular Undo", "Granular Redo", "Redo", + "History" }, contentAssembler, contentStart, initialBottomY ); } @@ -2846,6 +3135,7 @@ private static int assembleFramesInfoScreen( IconCodes.REMOVE_FRAME, IconCodes.MOVE_FRAME_FORWARD, IconCodes.MOVE_FRAME_BACK, + IconCodes.FRAME_PROPERTIES, IconCodes.TO_FIRST_FRAME, IconCodes.PREVIOUS, IconCodes.NEXT, @@ -2863,6 +3153,7 @@ private static int assembleFramesInfoScreen( "Remove frame", "Move frame forward", "Move frame back", + "Frame properties", "Navigate to first frame", "To previous frame", "To next frame", @@ -2897,9 +3188,9 @@ private static Menu assembleInfoDialog() { // background final GameImage backgroundImage = new GameImage(dialogW, Layout.height() - (2 * Layout.BUTTON_DIM)); - backgroundImage.fillRectangle(Settings.getTheme().panelBackground.get(), + backgroundImage.fillRectangle(Settings.getTheme().panelBackground, 0, 0, backgroundImage.getWidth(), backgroundImage.getHeight()); - backgroundImage.drawRectangle(Settings.getTheme().buttonOutline.get(), + backgroundImage.drawRectangle(Settings.getTheme().buttonOutline, 2f * Layout.BUTTON_BORDER_PX, 0, 0, backgroundImage.getWidth(), backgroundImage.getHeight()); @@ -2918,7 +3209,7 @@ private static Menu assembleInfoDialog() { // close button final GameImage baseImage = GraphicsUtils.drawTextButton( Layout.STD_TEXT_BUTTON_W, "Close", false), - highlightedImage = GraphicsUtils.drawHighlightedButton(baseImage); + highlightedImage = GraphicsUtils.highlightButton(baseImage); final Coord2D cancelPos = background.getRenderPosition() .displace(background.getWidth(), background.getHeight()) @@ -2932,21 +3223,18 @@ private static Menu assembleInfoDialog() { // contents Arrays.stream(DialogVals.InfoScreen.values()).forEach(is -> { - final GameImage baseIS = GraphicsUtils.drawTextButton( - Layout.STD_TEXT_BUTTON_W, is.toString(), false), - highlighedIS = GraphicsUtils.drawHighlightedButton(baseIS); - final Coord2D isPos = background.getRenderPosition().displace( Layout.CONTENT_BUFFER_PX + (is.ordinal() * (Layout.STD_TEXT_BUTTON_W + Layout.BUTTON_OFFSET)), Layout.CONTENT_BUFFER_PX + (int)(1.5 * Layout.STD_TEXT_BUTTON_INC)); - mb.add(new SimpleMenuButton(isPos, - new Bounds2D(baseIS.getWidth(), baseIS.getHeight()), - MenuElement.Anchor.LEFT_TOP, true, - () -> DialogVals.setInfoScreen(is), - baseIS, highlighedIS)); + mb.add(SelectableListItemButton.make( + isPos, Layout.STD_TEXT_BUTTON_W, + is.toString(), is.ordinal(), + () -> DialogVals.getInfoScreen().ordinal(), + i -> DialogVals.setInfoScreen( + DialogVals.InfoScreen.values()[i]))); }); final int scrollerEndY = (background.getRenderPosition().y + @@ -2977,7 +3265,7 @@ private static Menu assembleDialog( // background final GameImage backgroundImage = new GameImage( Layout.getDialogWidth(), Layout.getDialogHeight()); - backgroundImage.fillRectangle(Settings.getTheme().panelBackground.get(), + backgroundImage.fillRectangle(Settings.getTheme().panelBackground, 0, 0, Layout.getDialogWidth(), Layout.getDialogHeight()); final StaticMenuElement background = @@ -2997,7 +3285,7 @@ private static Menu assembleDialog( Layout.STD_TEXT_BUTTON_W, approveText.equals( Constants.CLOSE_DIALOG_TEXT) ? Constants.CLOSE_DIALOG_TEXT : "Cancel", false), - highlightedImage = GraphicsUtils.drawHighlightedButton(baseImage); + highlightedImage = GraphicsUtils.highlightButton(baseImage); final Coord2D cancelPos = background.getRenderPosition() .displace(background.getWidth(), background.getHeight()) @@ -3026,7 +3314,7 @@ private static Menu assembleDialog( // border final GameImage borderImage = new GameImage( Layout.getDialogWidth(), Layout.getDialogHeight()); - borderImage.drawRectangle(Settings.getTheme().buttonOutline.get(), + borderImage.drawRectangle(Settings.getTheme().buttonOutline, 2f * Layout.BUTTON_BORDER_PX, 0, 0, Layout.getDialogWidth(), Layout.getDialogHeight()); diff --git a/src/com/jordanbunke/stipple_effect/visual/GraphicsUtils.java b/src/com/jordanbunke/stipple_effect/visual/GraphicsUtils.java index 81f0d825..0b8d9a05 100644 --- a/src/com/jordanbunke/stipple_effect/visual/GraphicsUtils.java +++ b/src/com/jordanbunke/stipple_effect/visual/GraphicsUtils.java @@ -10,76 +10,69 @@ import com.jordanbunke.delta_time.text.TextBuilder; import com.jordanbunke.delta_time.utility.math.Bounds2D; import com.jordanbunke.delta_time.utility.math.Coord2D; -import com.jordanbunke.stipple_effect.selection.SelectionUtils; +import com.jordanbunke.stipple_effect.tools.Tool; import com.jordanbunke.stipple_effect.utility.Constants; import com.jordanbunke.stipple_effect.utility.IconCodes; import com.jordanbunke.stipple_effect.utility.Layout; import com.jordanbunke.stipple_effect.utility.settings.Settings; -import com.jordanbunke.stipple_effect.visual.theme.Theme; import com.jordanbunke.stipple_effect.visual.menu_elements.IconButton; import com.jordanbunke.stipple_effect.visual.menu_elements.IconToggleButton; +import com.jordanbunke.stipple_effect.visual.theme.SEColors; +import com.jordanbunke.stipple_effect.visual.theme.Theme; import java.awt.*; import java.nio.file.Path; -import java.util.HashSet; -import java.util.Set; -import java.util.function.BiFunction; +import java.util.Arrays; +import java.util.HashMap; +import java.util.Map; import java.util.function.Supplier; -import java.util.stream.Collectors; public class GraphicsUtils { - public static final GameImage - HIGHLIGHT_OVERLAY = loadIcon("highlighted"), - SELECT_OVERLAY = loadIcon("selected"), - TRANSFORM_NODE = ResourceLoader.loadImageResource( - Constants.MISC_FOLDER.resolve("transform_node.png")), - CHECKMARK = ResourceLoader.loadImageResource( - Constants.MISC_FOLDER.resolve("checkmark.png")); - - public static TextBuilder uiText() { - return uiText(Settings.getTheme().textLight.get()); - } + public static GameImage HIGHLIGHT_OVERLAY, + SELECT_OVERLAY, TRANSFORM_NODE, CHECKMARK; + private static final Map iconMap; - public static TextBuilder uiText(final Color color) { - return uiText(color, 1d); - } + static { + TRANSFORM_NODE = loadUtil("transform_node"); + CHECKMARK = loadUtil("checkmark"); - public static TextBuilder uiText(final Color color, final double textSize) { - return new TextBuilder(textSize, Text.Orientation.CENTER, color, - Settings.getProgramFont().associated()); - } + iconMap = new HashMap<>(); + + final Theme theme = Settings.getTheme(); - public static Color buttonBorderColor(final boolean selected) { - return selected ? Settings.getTheme().highlightOutline.get() - : Settings.getTheme().buttonOutline.get(); + HIGHLIGHT_OVERLAY = theme.logic.highlightedIconOverlay(); + SELECT_OVERLAY = theme.logic.selectedIconOverlay(); } - public static GameImage drawScrollBoxBackground( - final int w, final int h - ) { - final GameImage background = new GameImage(w, h); - background.fillRectangle( - Settings.getTheme().scrollBackground.get(), 0, 0, w, h); - return background.submit(); + private static GameImage loadUtil(final String code) { + final GameImage asset = ResourceLoader.loadImageResource( + Constants.MISC_FOLDER.resolve(code + ".png")); + + return Settings.getTheme().logic.transformIcon(asset); } - public static GameImage drawCheckbox( - final boolean isHighlighted, final boolean isChecked - ) { - final int w = Layout.ICON_DIMS.width(), h = Layout.ICON_DIMS.height(); + public static void refreshAssets() { + final Theme theme = Settings.getTheme(); - final GameImage checkbox = new GameImage(w, h); - checkbox.fillRectangle(Settings.getTheme().buttonBody.get(), 0, 0, w, h); + TRANSFORM_NODE = loadUtil("transform_node"); + CHECKMARK = loadUtil("checkmark"); - if (isChecked) - checkbox.draw(CHECKMARK); + HIGHLIGHT_OVERLAY = theme.logic.highlightedIconOverlay(); + SELECT_OVERLAY = theme.logic.selectedIconOverlay(); - final Color frame = GraphicsUtils.buttonBorderColor(false); - checkbox.drawRectangle(frame, 2f * Layout.BUTTON_BORDER_PX, - 0, 0, w, h); + iconMap.clear(); - return isHighlighted ? drawHighlightedButton(checkbox.submit()) - : checkbox.submit(); + SECursor.refresh(); + Arrays.stream(Tool.getAll()).forEach(Tool::refreshIcons); + } + + public static TextBuilder uiText(final Color color) { + return uiText(color, 1d); + } + + public static TextBuilder uiText(final Color color, final double textSize) { + return new TextBuilder(textSize, Text.Orientation.CENTER, color, + Settings.getProgramFont().associated()); } public static GameImage drawTextbox( @@ -90,8 +83,8 @@ public static GameImage drawTextbox( ) { final Theme t = Settings.getTheme(); - final Color accent = typing ? t.highlightOutline.get() : t.buttonOutline.get(), - background = valid ? t.buttonBody.get() : t.invalid.get(); + final Color accent = typing ? t.highlightOutline : t.buttonOutline, + background = valid ? t.buttonBody : t.invalid; return drawTextbox(dimensions.x, prefix, text, suffix, cursorIndex, selectionIndex, highlighted, accent, background); @@ -104,142 +97,38 @@ public static GameImage drawTextbox( final boolean highlighted, final Color accentColor, final Color backgroundColor ) { - final Color mainTextC = textButtonColorFromBackgroundColor( - backgroundColor, true), - affixTextC = textButtonColorFromBackgroundColor( - backgroundColor, false); - - final int left = Math.min(cursorIndex, selectionIndex), - right = Math.max(cursorIndex, selectionIndex); - - final boolean hasSelection = left != right, - cursorAtRight = cursorIndex == right; - - final int height = Layout.STD_TEXT_BUTTON_H, - px = Layout.BUTTON_BORDER_PX; - - final GameImage nhi = new GameImage(width, height); - nhi.fillRectangle(backgroundColor, 0, 0, width, height); - - // text and cursor - - final String preSel = text.substring(0, left), - sel = text.substring(left, right), - postSel = text.substring(right); - final GameImage - prefixImage = uiText(affixTextC).addText(prefix).build().draw(), - suffixImage = uiText(affixTextC).addText(suffix).build().draw(), - preSelImage = uiText(mainTextC).addText(preSel).build().draw(), - selImage = uiText(mainTextC).addText(sel).build().draw(), - postSelImage = uiText(mainTextC).addText(postSel).build().draw(); - - Coord2D textPos = new Coord2D(2 * px, Layout.BUTTON_TEXT_OFFSET_Y); - - // possible prefix - nhi.draw(prefixImage, textPos.x, textPos.y); - if (!prefix.isEmpty()) - textPos = textPos.displace(prefixImage.getWidth() + px, 0); - - // main text prior to possible selection - nhi.draw(preSelImage, textPos.x, textPos.y); - if (!preSel.isEmpty()) - textPos = textPos.displace(preSelImage.getWidth() + px, 0); - - // possible selection text - if (hasSelection) { - if (!cursorAtRight) - textPos = textPos.displace(2 * px, 0); - - nhi.draw(selImage, textPos.x, textPos.y); - nhi.fillRectangle(Settings.getTheme().highlightOverlay.get(), - textPos.x - px, 0, selImage.getWidth() + (2 * px), height); - textPos = textPos.displace(selImage.getWidth() + px, 0); - } - - // cursor - nhi.fillRectangle(mainTextC, textPos.x - (cursorAtRight - ? 0 : selImage.getWidth() + (3 * px)), 0, px, height); - if (cursorAtRight) - textPos = textPos.displace(2 * px, 0); - - // main text following possible selection - nhi.draw(postSelImage, textPos.x, textPos.y); - if (!postSel.isEmpty()) - textPos = textPos.displace(postSelImage.getWidth() + px, 0); - - // possible suffix - nhi.draw(suffixImage, textPos.x, textPos.y); - - // border - nhi.drawRectangle(accentColor, 2f * px, - 0, 0, width, height); - - // highlighting - if (highlighted) - return drawHighlightedButton(nhi.submit()); - else - return nhi.submit(); + return Settings.getTheme().logic.drawTextbox( + width, prefix, text, suffix, cursorIndex, selectionIndex, + highlighted, accentColor, backgroundColor); } - public static GameImage drawDropDownButton( - final int width, final String text, - final boolean isSelected + public static GameImage drawDropdownButton( + final int width, final String text, final boolean selected ) { - final GameImage base = drawTextButton(width, text, isSelected, - Settings.getTheme().buttonBody.get(), true, true); + final GameImage base = drawTextButton( + width, text, selected, ButtonType.DD_HEAD); - final GameImage icon = GraphicsUtils.loadIcon(isSelected - ? IconCodes.COLLAPSE : IconCodes.EXPAND); + final GameImage icon = loadIcon( + selected ? IconCodes.COLLAPSE : IconCodes.EXPAND); - base.draw(icon, base.getWidth() - (Layout.BUTTON_INC), Layout.BUTTON_BORDER_PX); + base.draw(icon, base.getWidth() - (Layout.BUTTON_INC), + Layout.BUTTON_BORDER_PX); return base.submit(); } public static GameImage drawTextButton( final int width, final String text, - final boolean selected, final Color backgroundColor, - final boolean leftAligned, final boolean drawBorder - ) { - final Color textColor = textButtonColorFromBackgroundColor( - backgroundColor, true); - final GameImage textImage = GraphicsUtils.uiText(textColor) - .addText(text).build().draw(); - - final int w = Math.max(width, textImage.getWidth() + - (4 * Layout.BUTTON_BORDER_PX)), - h = Layout.STD_TEXT_BUTTON_H; - - final GameImage nhi = new GameImage(w, h); - nhi.fillRectangle(backgroundColor, 0, 0, w, h); - - final int x = leftAligned - ? (2 * Layout.BUTTON_BORDER_PX) - : (w - textImage.getWidth()) / 2; - - nhi.draw(textImage, x, Layout.BUTTON_TEXT_OFFSET_Y); - - if (drawBorder) { - final Color frame = GraphicsUtils.buttonBorderColor(selected); - nhi.drawRectangle(frame, 2f * Layout.BUTTON_BORDER_PX, 0, 0, w, h); - } - - return nhi.submit(); - } - - public static GameImage drawTextButton( - final int width, final String text, - final boolean selected, final Color backgroundColor + final boolean selected, final ButtonType type ) { - return drawTextButton(width, text, selected, - backgroundColor, false, true); + return Settings.getTheme().logic + .drawTextButton(width, text, selected, type); } public static GameImage drawTextButton( final int width, final String text, final boolean selected ) { - return drawTextButton(width, text, selected, - Settings.getTheme().buttonBody.get()); + return drawTextButton(width, text, selected, ButtonType.STANDARD); } public static SimpleMenuButton makeStandardTextButton( @@ -249,36 +138,23 @@ public static SimpleMenuButton makeStandardTextButton( drawTextButton(Layout.STD_TEXT_BUTTON_W, text, false); return new SimpleMenuButton(pos, new Bounds2D(Layout.STD_TEXT_BUTTON_W, Layout.STD_TEXT_BUTTON_H), MenuElement.Anchor.LEFT_TOP, - true, onClick, base, drawHighlightedButton(base)); + true, onClick, base, highlightButton(base)); } public static SimpleMenuButton makeBespokeTextButton( final String text, final Coord2D pos, final Runnable onClick ) { - final int w = GraphicsUtils.uiText(Settings.getTheme().textDark.get()) + final int w = uiText(SEColors.def()) .addText(text).build().draw() .getWidth() + Layout.CONTENT_BUFFER_PX; final GameImage base = drawTextButton(w, text, false); return new SimpleMenuButton(pos, new Bounds2D(w, Layout.STD_TEXT_BUTTON_H), MenuElement.Anchor.LEFT_TOP, - true, onClick, base, drawHighlightedButton(base)); + true, onClick, base, highlightButton(base)); } - private static Color textButtonColorFromBackgroundColor( - final Color b, final boolean main - ) { - return (b.getRed() + b.getGreen() + b.getBlue()) / 3 > - Layout.COLOR_TEXTBOX_AVG_C_THRESHOLD - ? (main ? Settings.getTheme().textDark.get() - : Settings.getTheme().affixTextDark.get()) - : (main ? Settings.getTheme().textLight.get() - : Settings.getTheme().affixTextLight.get()); - } - - public static GameImage drawSelectedTextBox( - final GameImage bounds - ) { + public static GameImage drawSelectedTextbox(final GameImage bounds) { final GameImage selected = new GameImage(bounds); final int w = selected.getWidth(); selected.draw(loadIcon(IconCodes.BULLET_POINT), @@ -287,58 +163,23 @@ public static GameImage drawSelectedTextBox( return selected.submit(); } - public static GameImage drawHighlightedButton( - final GameImage nhi - ) { - final GameImage hi = new GameImage(nhi); - final int w = hi.getWidth(), h = hi.getHeight(); - hi.fillRectangle(Settings.getTheme().highlightOverlay.get(), 0, 0, w, h); - hi.drawRectangle(Settings.getTheme().buttonOutline.get(), - 2f * Layout.BUTTON_BORDER_PX, 0, 0, w, h); - - return hi.submit(); + public static GameImage highlightButton(final GameImage nhi) { + return Settings.getTheme().logic.highlightButton(nhi); } public static GameImage drawSelectionOverlay( - final double z, final Set selection, - final boolean filled, final boolean canTransform + final int w, final int h, + final double z, final boolean[][] mask ) { - final Coord2D tl = SelectionUtils.topLeft(selection), - br = SelectionUtils.bottomRight(selection); - - final Set adjusted = selection.stream() - .map(p -> p.displace(-tl.x, -tl.y)) - .collect(Collectors.toSet()); - final int w = br.x - tl.x, h = br.y - tl.y; - - return drawOverlay(w, h, z, adjusted, - Settings.getTheme().buttonOutline.get(), - Settings.getTheme().highlightOutline.get(), - filled, canTransform); + return drawOverlay(w, h, z, mask, + Settings.getTheme().buttonOutline, + Settings.getTheme().highlightOutline); } public static GameImage drawOverlay( final int w, final int h, final double z, - final BiFunction maskValidator, - final Color inside, final Color outside, - final boolean filled, final boolean canTransform - ) { - final Set mask = new HashSet<>(); - - for (int x = 0; x < w; x++) - for (int y = 0; y < h; y++) - if (maskValidator.apply(x, y)) - mask.add(new Coord2D(x, y)); - - return drawOverlay(w, h, z, mask, inside, outside, - filled, canTransform); - } - - private static GameImage drawOverlay( - final int w, final int h, final double z, - final Set selection, - final Color inside, final Color outside, - final boolean filled, final boolean canTransform + final boolean[][] mask, + final Color inside, final Color outside ) { final int zoomInc = (int)Math.max(Constants.ZOOM_FOR_OVERLAY, z), scaleUpW = Math.max(1, w * zoomInc), @@ -348,78 +189,64 @@ private static GameImage drawOverlay( scaleUpW + (2 * Constants.OVERLAY_BORDER_PX), scaleUpH + (2 * Constants.OVERLAY_BORDER_PX)); - selection.stream().filter( - p -> p.x >= 0 && p.x < w && p.y >= 0 && p.y < h - ).forEach(pixel -> { - boolean leftFrontier = false, - rightFrontier = false, - topFrontier = false, - bottomFrontier = false; - - // frontier defined as unmarked or off canvas - if (pixel.x - 1 < 0 || !selection.contains(pixel.displace(-1, 0))) - leftFrontier = true; - if (pixel.x + 1 >= w || !selection.contains(pixel.displace(1, 0))) - rightFrontier = true; - if (pixel.y - 1 < 0 || !selection.contains(pixel.displace(0, -1))) - topFrontier = true; - if (pixel.y + 1 >= h || !selection.contains(pixel.displace(0, 1))) - bottomFrontier = true; - - final Coord2D o = new Coord2D( - Constants.OVERLAY_BORDER_PX + (zoomInc * pixel.x), - Constants.OVERLAY_BORDER_PX + (zoomInc * pixel.y) - ); - - if (filled) - overlay.fillRectangle(Settings.getTheme().selectionFill.get(), - o.x, o.y, zoomInc, zoomInc); - - if (leftFrontier) { - overlay.fillRectangle(inside, o.x, o.y, 1, zoomInc); - overlay.fillRectangle(outside, o.x - 1, o.y, 1, zoomInc); - } - if (rightFrontier) { - overlay.fillRectangle(inside, (o.x + zoomInc) - 1, o.y, 1, zoomInc); - overlay.fillRectangle(outside, o.x + zoomInc, o.y, 1, zoomInc); - } - if (topFrontier) { - overlay.fillRectangle(inside, o.x, o.y, zoomInc, 1); - overlay.fillRectangle(outside, o.x, o.y - 1, zoomInc, 1); - } - if (bottomFrontier) { - overlay.fillRectangle(inside, o.x, (o.y + zoomInc) - 1, zoomInc, 1); - overlay.fillRectangle(outside, o.x, o.y + zoomInc, zoomInc, 1); + populateOverlay(mask, zoomInc, overlay, inside, outside); + + return overlay.submit(); + } + + private static void populateOverlay( + final boolean[][] mask, final int zoomInc, + final GameImage frontier, + final Color inside, final Color outside + ) { + for (int x = 0; x < mask.length; x++) + for (int y = 0; y < mask[x].length; y++) { + if (!mask[x][y]) + continue; + + final boolean left = x == 0 || !mask[x - 1][y], + right = x == mask.length - 1 || !mask[x + 1][y], + top = y == 0 || !mask[x][y - 1], + bottom = y == mask[x].length - 1 || !mask[x][y + 1]; + + final Coord2D o = new Coord2D( + Constants.OVERLAY_BORDER_PX + (zoomInc * x), + Constants.OVERLAY_BORDER_PX + (zoomInc * y)); + + if (left) { + frontier.fillRectangle(inside, o.x, o.y, 1, zoomInc); + frontier.fillRectangle(outside, o.x - 1, o.y, 1, zoomInc); + } + if (right) { + frontier.fillRectangle(inside, (o.x + zoomInc) - 1, o.y, 1, zoomInc); + frontier.fillRectangle(outside, o.x + zoomInc, o.y, 1, zoomInc); + } + if (top) { + frontier.fillRectangle(inside, o.x, o.y, zoomInc, 1); + frontier.fillRectangle(outside, o.x, o.y - 1, zoomInc, 1); + } + if (bottom) { + frontier.fillRectangle(inside, o.x, (o.y + zoomInc) - 1, zoomInc, 1); + frontier.fillRectangle(outside, o.x, o.y + zoomInc, zoomInc, 1); + } } - }); - - if (canTransform) { - final Coord2D tl = SelectionUtils.topLeft(selection), - br = SelectionUtils.bottomRight(selection); - - final int BEG = 0, MID = 1, END = 2; - final int[] xs = new int[] { - tl.x * zoomInc, - (int)(((tl.x + br.x) / 2d) * zoomInc), - br.x * zoomInc - }, ys = new int[] { - tl.y * zoomInc, - (int)(((tl.y + br.y) / 2d) * zoomInc), - br.y * zoomInc - }; + } - for (int x = BEG; x <= END; x++) - for (int y = BEG; y <= END; y++) - if (x != MID || y != MID) - overlay.draw(TRANSFORM_NODE, xs[x], ys[y]); - } + public static GameImage loadIcon(final String code) { + if (iconMap.containsKey(code)) + return new GameImage(iconMap.get(code)); - return overlay.submit(); + final GameImage asset = readIconAsset(code), + themed = Settings.getTheme().logic.transformIcon(asset); + + iconMap.put(code, themed); + + return themed; } - public static GameImage loadIcon(final String iconID) { + public static GameImage readIconAsset(final String code) { final Path iconFile = Constants.ICON_FOLDER.resolve( - iconID.toLowerCase() + ".png"); + code.toLowerCase() + ".png"); return ResourceLoader.loadImageResource(iconFile); } @@ -437,7 +264,7 @@ public static MenuElement generateIconButton( final IconButton icon = IconButton.make(iconID, position, behaviour); final StaticMenuElement stub = new StaticMenuElement(position, Layout.ICON_DIMS, MenuElement.Anchor.LEFT_TOP, - greyscaleVersionOf(loadIcon(iconID))); + Settings.getTheme().logic.unclickableIcon(loadIcon(iconID))); return new ThinkingMenuElement(() -> precondition.get() ? icon : stub); } @@ -452,25 +279,27 @@ public static MenuElement generateIconToggleButton( codes, behaviours, updateIndexLogic, global); final StaticMenuElement stub = new StaticMenuElement(position, Layout.ICON_DIMS, MenuElement.Anchor.LEFT_TOP, - greyscaleVersionOf(loadIcon(stubIconCode))); + Settings.getTheme().logic.unclickableIcon(loadIcon(stubIconCode))); return new ThinkingMenuElement(() -> precondition.get() ? icon : stub); } - private static GameImage greyscaleVersionOf(final GameImage image) { - final int w = image.getWidth(), h = image.getHeight(); - final GameImage greyscale = new GameImage(w, h); + public static Color greyscale(final Color orig) { + if (orig.getAlpha() == 0) + return orig; + + final int avg = (orig.getRed() + orig.getGreen() + orig.getBlue()) / 3; + return new Color(avg, avg, avg, orig.getAlpha()); + } - for (int x = 0; x < w; x++) { - for (int y = 0; y < h; y++) { - final Color cWas = image.getColorAt(x, y); - final int rgbAvg = (cWas.getRed() + cWas.getGreen() + cWas.getBlue()) / 3; - final Color cIs = new Color(rgbAvg, rgbAvg, rgbAvg, cWas.getAlpha()); + public enum ButtonType { + STANDARD, STUB, DD_HEAD, DD_OPTION; - greyscale.dot(cIs, x, y); - } + public boolean isDropdown() { + return switch (this) { + case DD_HEAD, DD_OPTION -> true; + default -> false; + }; } - - return greyscale.submit(); } } diff --git a/src/com/jordanbunke/stipple_effect/visual/MenuAssembly.java b/src/com/jordanbunke/stipple_effect/visual/MenuAssembly.java index 53e52d6f..76edaecf 100644 --- a/src/com/jordanbunke/stipple_effect/visual/MenuAssembly.java +++ b/src/com/jordanbunke/stipple_effect/visual/MenuAssembly.java @@ -19,13 +19,13 @@ import com.jordanbunke.stipple_effect.preview.PreviewWindow; import com.jordanbunke.stipple_effect.project.PlaybackInfo; import com.jordanbunke.stipple_effect.project.SEContext; +import com.jordanbunke.stipple_effect.project.ZoomLevel; import com.jordanbunke.stipple_effect.selection.SelectionMode; import com.jordanbunke.stipple_effect.tools.Tool; import com.jordanbunke.stipple_effect.utility.Constants; import com.jordanbunke.stipple_effect.utility.EnumUtils; import com.jordanbunke.stipple_effect.utility.IconCodes; import com.jordanbunke.stipple_effect.utility.Layout; -import com.jordanbunke.stipple_effect.utility.settings.Settings; import com.jordanbunke.stipple_effect.visual.menu_elements.*; import com.jordanbunke.stipple_effect.visual.menu_elements.colors.ColorSelector; import com.jordanbunke.stipple_effect.visual.menu_elements.colors.ColorTextbox; @@ -62,7 +62,8 @@ public static Menu buildProjectsMenu() { IconCodes.STITCH_SPLIT_FRAMES, IconCodes.PREVIEW, IconCodes.AUTOMATION_SCRIPT, IconCodes.UNDO, IconCodes.GRANULAR_UNDO, - IconCodes.GRANULAR_REDO, IconCodes.REDO + IconCodes.GRANULAR_REDO, IconCodes.REDO, + IconCodes.HISTORY }, getPreconditions( () -> true, @@ -78,7 +79,8 @@ public static Menu buildProjectsMenu() { () -> c.getStateManager().canUndo(), () -> c.getStateManager().canUndo(), () -> c.getStateManager().canRedo(), - () -> c.getStateManager().canRedo()), + () -> c.getStateManager().canRedo(), + () -> true), new Runnable[] { DialogAssembly::setDialogToProgramSettings, DialogAssembly::setDialogToNewProject, @@ -93,7 +95,8 @@ public static Menu buildProjectsMenu() { () -> c.getStateManager().undoToCheckpoint(), () -> c.getStateManager().undo(true), () -> c.getStateManager().redo(true), - () -> c.getStateManager().redoToCheckpoint() + () -> c.getStateManager().redoToCheckpoint(), + DialogAssembly::setDialogToHistory }, Layout.getProjectsPosition()); // exit program button @@ -191,6 +194,9 @@ public static Menu buildFramesMenu() { IconCodes.MOVE_FRAME_FORWARD, // gap between frame operations and navigation/playback Constants.ICON_ID_GAP_CODE, + IconCodes.FRAME_PROPERTIES, + // gap between frame operations and navigation/playback + Constants.ICON_ID_GAP_CODE, IconCodes.TO_FIRST_FRAME, IconCodes.PREVIOUS, // gap for play/stop button @@ -206,6 +212,8 @@ public static Menu buildFramesMenu() { () -> c.getState().canMoveFrameForward(), () -> false, // placeholder () -> true, + () -> false, // placeholder + () -> true, () -> true, () -> false, // placeholder () -> true, @@ -217,6 +225,9 @@ public static Menu buildFramesMenu() { c::moveFrameBack, c::moveFrameForward, () -> {}, // placeholder + () -> DialogAssembly.setDialogToFrameProperties( + c.getState().getFrameIndex()), + () -> {}, // placeholder () -> c.getState().setFrameIndex(0), () -> c.getState().previousFrame(), () -> {}, // placeholder @@ -229,9 +240,9 @@ public static Menu buildFramesMenu() { .displace(Layout.getFramesWidth(), 0), () -> Layout.setFramesPanelShowing(false)); - final int PLAY_STOP_INDEX = 8, - PLAYBACK_MODE_INDEX = 11, - AFTER_PLAYBACK_MODE = 13; + final int PLAY_STOP_INDEX = 10, + PLAYBACK_MODE_INDEX = 13, + AFTER_PLAYBACK_MODE = 15; // play/stop as toggle final Coord2D playStopTogglePos = Layout.getFramesPosition().displace( @@ -259,8 +270,8 @@ public static Menu buildFramesMenu() { Layout.ICON_BUTTON_OFFSET_Y, Layout.TEXT_Y_OFFSET, 1, Constants.MIN_PLAYBACK_FPS, Constants.MAX_PLAYBACK_FPS, c.playbackInfo::setFps, c.playbackInfo::getFps, - fps -> fps, fps -> fps, fps -> fps + " fps", - "XXX fps"); + fps -> fps, fps -> fps, fps -> fps + " FPS", + "XXX FPS"); mb.addAll(playbackLabel, playback.decButton, playback.incButton, playback.slider, playback.value); @@ -276,26 +287,14 @@ public static Menu buildFramesMenu() { int realRightX = firstPos.x; for (int i = 0; i < amount; i++) { - final GameImage baseImage = GraphicsUtils - .drawTextButton(Layout.FRAME_BUTTON_W, - String.valueOf(i + 1), false), - highlightedImage = GraphicsUtils - .drawHighlightedButton(baseImage), - selectedImage = GraphicsUtils - .drawTextButton(Layout.FRAME_BUTTON_W, - String.valueOf(i + 1), true); - final Coord2D pos = firstPos.displace( i * (Layout.FRAME_BUTTON_W + Layout.BUTTON_OFFSET), 0); - final Bounds2D dims = new Bounds2D(baseImage.getWidth(), baseImage.getHeight()); - - frameElements.add(new SelectableListItemButton(pos, dims, - MenuElement.Anchor.LEFT_TOP, - baseImage, highlightedImage, selectedImage, + frameElements.add(SelectableListItemButton.make( + pos, Layout.FRAME_BUTTON_W, String.valueOf(i + 1), i, () -> c.getState().getFrameIndex(), s -> c.getState().setFrameIndex(s))); - realRightX = pos.x + dims.width(); + realRightX = pos.x + Layout.FRAME_BUTTON_W; } mb.add(new HorizontalScrollBox(firstPos, @@ -408,21 +407,12 @@ public static Menu buildLayersMenu() { ? name.substring(0, Layout.LAYER_NAME_LENGTH_CUTOFF) + "..." : name; - final GameImage baseImage = GraphicsUtils - .drawTextButton(Layout.LAYER_BUTTON_W, text, false), - highlightedImage = GraphicsUtils - .drawHighlightedButton(baseImage), - selectedImage = GraphicsUtils - .drawTextButton(Layout.LAYER_BUTTON_W, text, true); - final Coord2D pos = firstPos.displace(0, (amount - (i + 1)) * Layout.STD_TEXT_BUTTON_INC); - final Bounds2D dims = new Bounds2D(baseImage.getWidth(), baseImage.getHeight()); - layerButtons.add(new SelectableListItemButton(pos, dims, - MenuElement.Anchor.LEFT_TOP, - baseImage, highlightedImage, selectedImage, - i, () -> c.getState().getLayerEditIndex(), + layerButtons.add(SelectableListItemButton.make( + pos, Layout.LAYER_BUTTON_W, text, i, + () -> c.getState().getLayerEditIndex(), s -> c.getState().setLayerEditIndex(s))); final int index = i; @@ -446,7 +436,7 @@ public static Menu buildLayersMenu() { layerButtons.add(IconButton.make(IconCodes.LAYER_SETTINGS, lsPos, () -> DialogAssembly.setDialogToLayerSettings(index))); - realBottomY = pos.y + dims.height(); + realBottomY = pos.y + Layout.STD_TEXT_BUTTON_H; } final int initialOffsetY = layerButtonYDisplacement(amount); @@ -594,7 +584,7 @@ public static Menu buildColorsMenu() { colorTextBox.getHeight()); final GatewayMenuElement highlight = new GatewayMenuElement( new StaticMenuElement(textBoxPos, dims, MenuElement.Anchor.CENTRAL_TOP, - GraphicsUtils.drawSelectedTextBox( + GraphicsUtils.drawSelectedTextbox( new GameImage(dims.width(), dims.height()))), () -> StippleEffect.get().getColorIndex() == index); mb.add(highlight); @@ -721,7 +711,7 @@ private static void addPaletteMenuElements(final MenuBuilder mb) { new Bounds2D(contentWidth, Layout.STD_TEXT_BUTTON_H), MenuElement.Anchor.LEFT_TOP, GraphicsUtils.drawTextButton( contentWidth, "No palettes", false, - Settings.getTheme().stubButtonBody.get()))); + GraphicsUtils.ButtonType.STUB))); // palette buttons if (hasPaletteContents) { @@ -851,10 +841,9 @@ public static Menu buildBottomBarMenu() { final Indicator toolIndicator = new Indicator(new Coord2D( Layout.BUTTON_OFFSET, bottomBarButtonY), IconCodes.IND_TOOL); - final DynamicLabel toolLabel = new DynamicLabel(new Coord2D( + final DynamicLabel toolLabel = DynamicLabel.make(new Coord2D( Layout.optionsBarNextElementX(toolIndicator, false), - bottomBarTextY), MenuElement.Anchor.LEFT_TOP, - Settings.getTheme().textLight.get(), + bottomBarTextY), () -> StippleEffect.get().getTool().getBottomBarText(), Layout.getBottomBarToolWidth()); mb.addAll(toolIndicator, toolLabel); @@ -863,10 +852,9 @@ public static Menu buildBottomBarMenu() { final Indicator targetIndicator = new Indicator(new Coord2D( Layout.getBottomBarTargetPixelX(), bottomBarButtonY), IconCodes.IND_TARGET); - final DynamicLabel targetLabel = new DynamicLabel(new Coord2D( + final DynamicLabel targetLabel = DynamicLabel.make(new Coord2D( Layout.optionsBarNextElementX(targetIndicator, false), - bottomBarTextY), MenuElement.Anchor.LEFT_TOP, - Settings.getTheme().textLight.get(), + bottomBarTextY), c::getTargetPixelText, Layout.getBottomBarTargetPixelWidth()); mb.addAll(targetIndicator, targetLabel); @@ -874,36 +862,36 @@ public static Menu buildBottomBarMenu() { final Indicator boundsIndicator = new Indicator(new Coord2D( Layout.getBottomBarCanvasSizeX(), bottomBarButtonY), IconCodes.IND_BOUNDS); - final DynamicLabel boundsLabel = new DynamicLabel(new Coord2D( + final DynamicLabel boundsLabel = DynamicLabel.make(new Coord2D( Layout.optionsBarNextElementX(boundsIndicator, false), - bottomBarTextY), MenuElement.Anchor.LEFT_TOP, - Settings.getTheme().textLight.get(), + bottomBarTextY), c::getImageSizeText, Layout.getBottomBarCanvasSizeWidth()); mb.addAll(boundsIndicator, boundsLabel); // zoom - final float BASE = 2f; final Indicator zoomIndicator = new Indicator( new Coord2D(Layout.getBottomBarZoomPercentageX(), bottomBarButtonY), IconCodes.IND_ZOOM); final IncrementalRangeElements zoom = IncrementalRangeElements.makeForFloat(zoomIndicator, Layout.getBottomBarPosition().y + Layout.BUTTON_OFFSET, - bottomBarTextY, c.renderInfo::zoomOut, + bottomBarTextY, + () -> c.renderInfo.zoomOut(Constants.NO_VALID_TARGET), () -> c.renderInfo.zoomIn(Constants.NO_VALID_TARGET), - Constants.MIN_ZOOM, Constants.MAX_ZOOM, - c.renderInfo::setZoomFactor, c.renderInfo::getZoomFactor, - f -> (int) (Math.log(f) / Math.log(BASE)), - sv -> (float) Math.pow(BASE, sv), + ZoomLevel.MIN.z, ZoomLevel.MAX.z, + z -> c.renderInfo.setZoomLevel(ZoomLevel.fromZ(z)), + c.renderInfo::getZoomFactor, + z -> ZoomLevel.fromZ(z).ordinal(), + sv -> ZoomLevel.values()[sv].z, f -> c.renderInfo.getZoomText(), "XXX.XX%"); mb.addAll(zoomIndicator, zoom.decButton, zoom.incButton, zoom.slider, zoom.value); // selection - mb.add(new DynamicLabel(new Coord2D(Layout.width() - + mb.add(DynamicLabel.make(new Coord2D(Layout.width() - (Layout.CONTENT_BUFFER_PX + (2 * Layout.BUTTON_INC)), bottomBarTextY), - MenuElement.Anchor.RIGHT_TOP, Settings.getTheme().textLight.get(), - c::getSelectionText, Layout.width() - + MenuElement.Anchor.RIGHT_TOP, c::getSelectionText, + Layout.width() - (Layout.getBottomBarZoomSliderX() + Layout.getUISliderWidth()))); // help button diff --git a/src/com/jordanbunke/stipple_effect/visual/SECursor.java b/src/com/jordanbunke/stipple_effect/visual/SECursor.java index 9b10875d..17284345 100644 --- a/src/com/jordanbunke/stipple_effect/visual/SECursor.java +++ b/src/com/jordanbunke/stipple_effect/visual/SECursor.java @@ -3,13 +3,15 @@ import com.jordanbunke.delta_time.image.GameImage; import com.jordanbunke.delta_time.io.ResourceLoader; import com.jordanbunke.stipple_effect.utility.Constants; +import com.jordanbunke.stipple_effect.utility.settings.Settings; import java.util.HashMap; import java.util.Map; import java.util.Set; public class SECursor { - public static final String MAIN_CURSOR = "main", RETICLE = "reticle", + public static final String MAIN_CURSOR = "main", + RETICLE = "reticle", NO_SCRIPT = "no_script", HAND_OPEN = "hand_open", HAND_GRAB = "hand_grab"; private static final String BRUSH = "brush", ERASER = "eraser", @@ -54,17 +56,17 @@ public class SECursor { POLYGON_SELECT_ADDITIVE = "polygon_select_additive", POLYGON_SELECT_SUBTRACTIVE = "polygon_select_subtractive", POLYGON_SELECT_SINGLE = "polygon_select_single", - PENCIL_SELECT_GLOBAL = "pencil_select_global", - PENCIL_SELECT_ADDITIVE = "pencil_select_additive", - PENCIL_SELECT_SUBTRACTIVE = "pencil_select_subtractive", - PENCIL_SELECT_SINGLE = "pencil_select_single", +// PENCIL_SELECT_GLOBAL = "pencil_select_global", +// PENCIL_SELECT_ADDITIVE = "pencil_select_additive", +// PENCIL_SELECT_SUBTRACTIVE = "pencil_select_subtractive", +// PENCIL_SELECT_SINGLE = "pencil_select_single", RETICLE_ADDITIVE = "reticle_additive", RETICLE_SUBTRACTIVE = "reticle_subtractive", RETICLE_SINGLE = "reticle_single", ZOOM = "zoom"; private static final Set CURSOR_CODES = Set.of( - MAIN_CURSOR, RETICLE, HAND_GRAB, HAND_OPEN, + MAIN_CURSOR, RETICLE, NO_SCRIPT, HAND_GRAB, HAND_OPEN, BRUSH, ERASER, PENCIL, STIPPLE_PENCIL, COLOR_PICKER, LINE_TOOL, TEXT_TOOL, TEXT_TOOL_TYPING, MOVE_SELECTION, MOVE_SELECTION_VERT, @@ -87,20 +89,28 @@ public class SECursor { BOX_SELECT_SINGLE, BOX_SELECT_SUBTRACTIVE, POLYGON_SELECT_ADDITIVE, POLYGON_SELECT_GLOBAL, POLYGON_SELECT_SINGLE, POLYGON_SELECT_SUBTRACTIVE, - PENCIL_SELECT_ADDITIVE, PENCIL_SELECT_GLOBAL, - PENCIL_SELECT_SINGLE, PENCIL_SELECT_SUBTRACTIVE, +// PENCIL_SELECT_ADDITIVE, PENCIL_SELECT_GLOBAL, +// PENCIL_SELECT_SINGLE, PENCIL_SELECT_SUBTRACTIVE, RETICLE_ADDITIVE, RETICLE_SUBTRACTIVE, RETICLE_SINGLE, ZOOM); private static final Map CURSOR_MAP = new HashMap<>(); static { - for (String code : CURSOR_CODES) - CURSOR_MAP.put(code, ResourceLoader.loadImageResource( - Constants.CURSOR_FOLDER.resolve(code + ".png"))); + refresh(); } public static GameImage fetchCursor(final String cursorCode) { return CURSOR_MAP.getOrDefault(cursorCode, CURSOR_MAP.get(MAIN_CURSOR)); } + + public static void refresh() { + for (String code : CURSOR_CODES) { + final GameImage asset = ResourceLoader.loadImageResource( + Constants.CURSOR_FOLDER.resolve(code + ".png")), + cursor = Settings.getTheme().logic.transformIcon(asset); + + CURSOR_MAP.put(code, cursor); + } + } } diff --git a/src/com/jordanbunke/stipple_effect/visual/SEFonts.java b/src/com/jordanbunke/stipple_effect/visual/SEFonts.java index 02e9c44d..526988b3 100644 --- a/src/com/jordanbunke/stipple_effect/visual/SEFonts.java +++ b/src/com/jordanbunke/stipple_effect/visual/SEFonts.java @@ -121,7 +121,7 @@ public static void attemptPreviewUpdate() { final String[] previewText = ParserUtils.getBlurb( IconCodes.FONT_EXAMPLE_TEXT); final TextBuilder tb = new TextBuilder(1d, Text.Orientation.LEFT, - Settings.getTheme().textLight.get(), buildNewFont()); + Settings.getTheme().textLight, buildNewFont()); Arrays.stream(previewText).forEach(line -> { tb.addText(line); tb.addLineBreak(); diff --git a/src/com/jordanbunke/stipple_effect/visual/SplashLoader.java b/src/com/jordanbunke/stipple_effect/visual/SplashLoader.java deleted file mode 100644 index 249a1249..00000000 --- a/src/com/jordanbunke/stipple_effect/visual/SplashLoader.java +++ /dev/null @@ -1,27 +0,0 @@ -package com.jordanbunke.stipple_effect.visual; - -import com.jordanbunke.delta_time.image.GameImage; -import com.jordanbunke.delta_time.image.ImageProcessing; -import com.jordanbunke.delta_time.io.ResourceLoader; - -import java.nio.file.Path; - -public class SplashLoader { - private static final Path SPLASH_FOLDER = Path.of("splash"); - - private static final int ANIM_FRAMES = 25, SCALE_UP = 3; - private static final String BASE_NAME = "logo_anim_"; - - public static GameImage[] loadAnimationFrames() { - final GameImage[] frames = new GameImage[ANIM_FRAMES]; - - for (int i = 0; i < ANIM_FRAMES; i++) { - final Path framePath = SPLASH_FOLDER.resolve(BASE_NAME + i + ".png"); - - frames[i] = ImageProcessing.scale(ResourceLoader - .loadImageResource(framePath), SCALE_UP); - } - - return frames; - } -} diff --git a/src/com/jordanbunke/stipple_effect/visual/menu_elements/Checkbox.java b/src/com/jordanbunke/stipple_effect/visual/menu_elements/Checkbox.java index f6b20da6..f88bc4a1 100644 --- a/src/com/jordanbunke/stipple_effect/visual/menu_elements/Checkbox.java +++ b/src/com/jordanbunke/stipple_effect/visual/menu_elements/Checkbox.java @@ -4,13 +4,13 @@ import com.jordanbunke.delta_time.utility.math.Coord2D; import com.jordanbunke.funke.core.Property; import com.jordanbunke.stipple_effect.utility.Layout; -import com.jordanbunke.stipple_effect.visual.GraphicsUtils; +import com.jordanbunke.stipple_effect.utility.settings.Settings; public class Checkbox extends AbstractCheckbox { public Checkbox( final Coord2D position, final Property property ) { super(position, Layout.ICON_DIMS, Anchor.LEFT_TOP, property, - GraphicsUtils::drawCheckbox); + Settings.getTheme().logic::drawCheckbox); } } diff --git a/src/com/jordanbunke/stipple_effect/visual/menu_elements/Dropdown.java b/src/com/jordanbunke/stipple_effect/visual/menu_elements/Dropdown.java index 7cf1a97a..f112e9b6 100644 --- a/src/com/jordanbunke/stipple_effect/visual/menu_elements/Dropdown.java +++ b/src/com/jordanbunke/stipple_effect/visual/menu_elements/Dropdown.java @@ -10,7 +10,6 @@ import com.jordanbunke.delta_time.utility.math.Bounds2D; import com.jordanbunke.delta_time.utility.math.Coord2D; import com.jordanbunke.stipple_effect.utility.Layout; -import com.jordanbunke.stipple_effect.utility.settings.Settings; import com.jordanbunke.stipple_effect.visual.GraphicsUtils; import com.jordanbunke.stipple_effect.visual.menu_elements.scrollable.VerticalScrollBox; @@ -93,13 +92,13 @@ protected VerticalScrollBox makeDDContainer(final Coord2D position) { final GameImage nhi = GraphicsUtils.drawTextButton( buttonWidth, getLabelTextFor(i), false, - Settings.getTheme().dropdownOptionBody.get(), true, false); + GraphicsUtils.ButtonType.DD_OPTION); scrollables[i] = new SimpleMenuButton( position.displace(0, i * Layout.STD_TEXT_BUTTON_H), new Bounds2D(buttonWidth, Layout.STD_TEXT_BUTTON_H), Anchor.LEFT_TOP, true, () -> select(index), - nhi, GraphicsUtils.drawHighlightedButton(nhi)); + nhi, GraphicsUtils.highlightButton(nhi)); } final Bounds2D dimensions = new Bounds2D(getWidth(), @@ -117,12 +116,12 @@ protected SimpleToggleMenuButton makeDDButton() { final String text = getCurrentLabelText(); final GameImage[] bases = new GameImage[] { - GraphicsUtils.drawDropDownButton(getWidth(), text, false), - GraphicsUtils.drawDropDownButton(getWidth(), text, true) + GraphicsUtils.drawDropdownButton(getWidth(), text, false), + GraphicsUtils.drawDropdownButton(getWidth(), text, true) }; final GameImage[] highlighted = Arrays.stream(bases) - .map(GraphicsUtils::drawHighlightedButton) + .map(GraphicsUtils::highlightButton) .toArray(GameImage[]::new); return new SimpleToggleMenuButton(new Coord2D(getX(), getY()), diff --git a/src/com/jordanbunke/stipple_effect/visual/menu_elements/DynamicLabel.java b/src/com/jordanbunke/stipple_effect/visual/menu_elements/DynamicLabel.java index 393813a7..8d3d632b 100644 --- a/src/com/jordanbunke/stipple_effect/visual/menu_elements/DynamicLabel.java +++ b/src/com/jordanbunke/stipple_effect/visual/menu_elements/DynamicLabel.java @@ -5,28 +5,43 @@ import com.jordanbunke.delta_time.utility.math.Bounds2D; import com.jordanbunke.delta_time.utility.math.Coord2D; import com.jordanbunke.stipple_effect.utility.Layout; +import com.jordanbunke.stipple_effect.utility.settings.Settings; import com.jordanbunke.stipple_effect.visual.GraphicsUtils; +import com.jordanbunke.stipple_effect.visual.theme.SEColors; +import com.jordanbunke.stipple_effect.visual.theme.logic.ThemeLogic; import java.awt.*; import java.util.function.Supplier; public class DynamicLabel extends AbstractDynamicLabel { - public DynamicLabel( + private DynamicLabel( final Coord2D position, final Anchor anchor, final Color textColor, final Supplier getter, - final String widestCase + final int widthAllowance ) { - super(position, widestCase, Layout.DYNAMIC_LABEL_H, + super(position, new Bounds2D(widthAllowance, Layout.DYNAMIC_LABEL_H), anchor, textColor, getter, DynamicLabel::draw); } - public DynamicLabel( - final Coord2D position, final Anchor anchor, - final Color textColor, final Supplier getter, + public static DynamicLabel make( + final Coord2D position, final Supplier getter, final int widthAllowance ) { - super(position, new Bounds2D(widthAllowance, Layout.DYNAMIC_LABEL_H), - anchor, textColor, getter, DynamicLabel::draw); + return make(position, Anchor.LEFT_TOP, getter, widthAllowance); + } + + public static DynamicLabel make( + final Coord2D position, final Anchor anchor, + final Supplier getter, final int widthAllowance + ) { + return new DynamicLabel(position, anchor, + ThemeLogic.intuitTextColor( + Settings.getTheme().panelBackground, true), + getter, widthAllowance); + } + + public static int getWidth(final String maxWidthCase) { + return draw(maxWidthCase, SEColors.def()).getWidth(); } private static GameImage draw(final String text, final Color textColor) { diff --git a/src/com/jordanbunke/stipple_effect/visual/menu_elements/DynamicTextButton.java b/src/com/jordanbunke/stipple_effect/visual/menu_elements/DynamicTextButton.java index 62e7047f..06b7b9bc 100644 --- a/src/com/jordanbunke/stipple_effect/visual/menu_elements/DynamicTextButton.java +++ b/src/com/jordanbunke/stipple_effect/visual/menu_elements/DynamicTextButton.java @@ -52,6 +52,6 @@ public void debugRender(GameImage canvas, GameDebugger debugger) { private void updateAssets() { baseImage = GraphicsUtils.drawTextButton(getWidth(), text, false); - highlightedImage = GraphicsUtils.drawHighlightedButton(baseImage); + highlightedImage = GraphicsUtils.highlightButton(baseImage); } } diff --git a/src/com/jordanbunke/stipple_effect/visual/menu_elements/IncrementalRangeElements.java b/src/com/jordanbunke/stipple_effect/visual/menu_elements/IncrementalRangeElements.java index bf72af2e..e6788fdc 100644 --- a/src/com/jordanbunke/stipple_effect/visual/menu_elements/IncrementalRangeElements.java +++ b/src/com/jordanbunke/stipple_effect/visual/menu_elements/IncrementalRangeElements.java @@ -8,7 +8,6 @@ import com.jordanbunke.stipple_effect.utility.setting_group.FloatToolSettingType; import com.jordanbunke.stipple_effect.utility.setting_group.IntToolSettingType; import com.jordanbunke.stipple_effect.utility.setting_group.ToolSettingType; -import com.jordanbunke.stipple_effect.utility.settings.Settings; import com.jordanbunke.stipple_effect.visual.menu_elements.scrollable.HorizontalSlider; import java.util.function.Consumer; @@ -82,11 +81,10 @@ private DynamicLabel makeValue( final Function valueFormatter, final String widestTextCase ) { - return new DynamicLabel(new Coord2D( + return DynamicLabel.make(new Coord2D( Layout.optionsBarNextElementX(slider, false), - textY), MenuElement.Anchor.LEFT_TOP, - Settings.getTheme().textLight.get(), - () -> valueFormatter.apply(getter.get()), widestTextCase); + textY), () -> valueFormatter.apply(getter.get()), + DynamicLabel.getWidth(widestTextCase)); } public static IncrementalRangeElements makeForInt( @@ -138,4 +136,8 @@ public static IncrementalRangeElements makeForFloat( setter, getter, toSliderConversion, fromSliderConversion, valueFormatter, widestTextCase); } + + public MenuElement[] getAll() { + return new MenuElement[] { decButton, incButton, slider, value }; + } } diff --git a/src/com/jordanbunke/stipple_effect/visual/menu_elements/ProjectButton.java b/src/com/jordanbunke/stipple_effect/visual/menu_elements/ProjectButton.java index 62df8f47..6a5e137e 100644 --- a/src/com/jordanbunke/stipple_effect/visual/menu_elements/ProjectButton.java +++ b/src/com/jordanbunke/stipple_effect/visual/menu_elements/ProjectButton.java @@ -7,6 +7,7 @@ import com.jordanbunke.stipple_effect.project.SEContext; import com.jordanbunke.stipple_effect.utility.Layout; import com.jordanbunke.stipple_effect.visual.GraphicsUtils; +import com.jordanbunke.stipple_effect.visual.theme.SEColors; import java.util.function.Consumer; import java.util.function.Supplier; @@ -24,8 +25,7 @@ private ProjectButton( final Supplier selectedIndexGetter, final Consumer selectFunction ) { - super(position, dimensions, Anchor.LEFT_TOP, - null, null, null, + super(position, dimensions, null, null, null, index, selectedIndexGetter, selectFunction); this.project = project; @@ -41,7 +41,8 @@ public static ProjectButton make( final String maxText = "* " + project.projectInfo .getFormattedName(false, true); - final int paddedTextWidth = GraphicsUtils.uiText() + // placeholder calculation + final int paddedTextWidth = GraphicsUtils.uiText(SEColors.black()) .addText(maxText).build().draw().getWidth() + Layout.PROJECT_NAME_BUTTON_PADDING_W; @@ -70,7 +71,7 @@ private void updateImages() { image = GraphicsUtils.drawTextButton(getWidth(), project.projectInfo.getFormattedName(true, true), isSelected()); - highlighted = GraphicsUtils.drawHighlightedButton(image); + highlighted = GraphicsUtils.highlightButton(image); } @Override diff --git a/src/com/jordanbunke/stipple_effect/visual/menu_elements/SelectableListItemButton.java b/src/com/jordanbunke/stipple_effect/visual/menu_elements/SelectableListItemButton.java index 02f253b2..3e578cfb 100644 --- a/src/com/jordanbunke/stipple_effect/visual/menu_elements/SelectableListItemButton.java +++ b/src/com/jordanbunke/stipple_effect/visual/menu_elements/SelectableListItemButton.java @@ -5,6 +5,7 @@ import com.jordanbunke.delta_time.menu.menu_elements.button.MenuButton; import com.jordanbunke.delta_time.utility.math.Bounds2D; import com.jordanbunke.delta_time.utility.math.Coord2D; +import com.jordanbunke.stipple_effect.visual.GraphicsUtils; import java.util.function.Consumer; import java.util.function.Supplier; @@ -18,13 +19,13 @@ public class SelectableListItemButton extends MenuButton { private boolean selected = false; - public SelectableListItemButton( - final Coord2D position, final Bounds2D dimensions, final Anchor anchor, + protected SelectableListItemButton( + final Coord2D position, final Bounds2D dimensions, final GameImage baseImage, final GameImage highlightedImage, final GameImage selectedImage, final int index, final Supplier selectedIndexGetter, final Consumer selectFunction ) { - super(position, dimensions, anchor, true, () -> selectFunction.accept(index)); + super(position, dimensions, Anchor.LEFT_TOP, true, () -> selectFunction.accept(index)); this.baseImage = baseImage; this.highlightedImage = highlightedImage; @@ -36,6 +37,22 @@ public SelectableListItemButton( updateSelection(); } + public static SelectableListItemButton make( + final Coord2D position, final int width, + final String text, final int index, + final Supplier selectedIndexGetter, + final Consumer selectFunction + ) { + final GameImage base = GraphicsUtils.drawTextButton(width, text, false), + highlighted = GraphicsUtils.highlightButton(base), + selected = GraphicsUtils.drawTextButton(width, text, true); + + return new SelectableListItemButton(position, + new Bounds2D(base.getWidth(), base.getHeight()), + base, highlighted, selected, index, + selectedIndexGetter, selectFunction); + } + private void updateSelection() { selected = selectedIndexGetter.get() == index; } diff --git a/src/com/jordanbunke/stipple_effect/visual/menu_elements/TextLabel.java b/src/com/jordanbunke/stipple_effect/visual/menu_elements/TextLabel.java index 0ea82e5a..a0f7c547 100644 --- a/src/com/jordanbunke/stipple_effect/visual/menu_elements/TextLabel.java +++ b/src/com/jordanbunke/stipple_effect/visual/menu_elements/TextLabel.java @@ -6,6 +6,7 @@ import com.jordanbunke.delta_time.utility.math.Coord2D; import com.jordanbunke.stipple_effect.utility.settings.Settings; import com.jordanbunke.stipple_effect.visual.GraphicsUtils; +import com.jordanbunke.stipple_effect.visual.theme.logic.ThemeLogic; import java.awt.*; @@ -18,7 +19,8 @@ private TextLabel(final Coord2D position, final GameImage image) { public static TextLabel make( final Coord2D position, final String text ) { - return make(position, text, Settings.getTheme().textLight.get()); + return make(position, text, ThemeLogic.intuitTextColor( + Settings.getTheme().panelBackground, true)); } public static TextLabel make( @@ -31,7 +33,8 @@ public static TextLabel make( final Coord2D position, final String text, final Color c, final double textSize ) { - final GameImage label = GraphicsUtils.uiText(c, textSize).addText(text).build().draw(); + final GameImage label = GraphicsUtils.uiText(c, textSize) + .addText(text).build().draw(); return new TextLabel(position, label); } } diff --git a/src/com/jordanbunke/stipple_effect/visual/menu_elements/colors/ColorComponent.java b/src/com/jordanbunke/stipple_effect/visual/menu_elements/colors/ColorComponent.java index eacd8dd2..46a4b924 100644 --- a/src/com/jordanbunke/stipple_effect/visual/menu_elements/colors/ColorComponent.java +++ b/src/com/jordanbunke/stipple_effect/visual/menu_elements/colors/ColorComponent.java @@ -10,7 +10,6 @@ import com.jordanbunke.delta_time.utility.math.Coord2D; import com.jordanbunke.stipple_effect.utility.IconCodes; import com.jordanbunke.stipple_effect.utility.Layout; -import com.jordanbunke.stipple_effect.utility.settings.Settings; import com.jordanbunke.stipple_effect.visual.GraphicsUtils; import com.jordanbunke.stipple_effect.visual.menu_elements.DynamicLabel; import com.jordanbunke.stipple_effect.visual.menu_elements.TextLabel; @@ -56,13 +55,12 @@ public ColorComponent( // label elements.add(TextLabel.make( startingPos.displace(indent, Layout.COLOR_LABEL_OFFSET_Y), - label, Settings.getTheme().textLight.get())); + label)); // value - elements.add(new DynamicLabel(startingPos.displace( + elements.add(DynamicLabel.make(startingPos.displace( width - indent, Layout.COLOR_LABEL_OFFSET_Y), - Anchor.RIGHT_TOP, Settings.getTheme().textLight.get(), - () -> String.valueOf(getter.get()), + Anchor.RIGHT_TOP, () -> String.valueOf(getter.get()), Layout.DYNAMIC_LABEL_W_ALLOWANCE)); // increment and decrement buttons diff --git a/src/com/jordanbunke/stipple_effect/visual/menu_elements/colors/ColorSlider.java b/src/com/jordanbunke/stipple_effect/visual/menu_elements/colors/ColorSlider.java index ef934e88..d9c10026 100644 --- a/src/com/jordanbunke/stipple_effect/visual/menu_elements/colors/ColorSlider.java +++ b/src/com/jordanbunke/stipple_effect/visual/menu_elements/colors/ColorSlider.java @@ -48,8 +48,8 @@ public void update(final double deltaTime) { public GameImage drawSliderCore(final int w, final int h) { final GameImage sliderCore = new GameImage(w, h); - final Color light = Settings.getTheme().checkerboard1.get(), - dark = Settings.getTheme().checkerboard2.get(); + final Color light = Settings.getTheme().checkerboard1, + dark = Settings.getTheme().checkerboard2; final int sectionalW = Layout.SLIDER_OFF_DIM / 4; final int sections = (w / sectionalW) + 1; @@ -74,7 +74,7 @@ public GameImage drawSliderCore(final int w, final int h) { @Override public Color getSliderBallCoreColor() { final GameImage core = new GameImage(1, 1); - core.dot(Settings.getTheme().buttonBody.get(), 0, 0); + core.dot(Settings.getTheme().buttonBody, 0, 0); core.dot(spectralFunction.apply(getValue()), 0, 0); core.free(); diff --git a/src/com/jordanbunke/stipple_effect/visual/menu_elements/colors/ColorTextbox.java b/src/com/jordanbunke/stipple_effect/visual/menu_elements/colors/ColorTextbox.java index 816faeb0..f144a2e1 100644 --- a/src/com/jordanbunke/stipple_effect/visual/menu_elements/colors/ColorTextbox.java +++ b/src/com/jordanbunke/stipple_effect/visual/menu_elements/colors/ColorTextbox.java @@ -49,8 +49,8 @@ private static TextboxDrawingFunction getFDraw() { selectionIndex, valid, highlighted, typing) -> { final int width = dimensions.x; final Color accent = typing - ? Settings.getTheme().highlightOutline.get() - : Settings.getTheme().buttonOutline.get(), + ? Settings.getTheme().highlightOutline + : Settings.getTheme().buttonOutline, background = ColorTextbox.getColorFromHexCode(text); return GraphicsUtils.drawTextbox( diff --git a/src/com/jordanbunke/stipple_effect/visual/menu_elements/colors/PaletteColorButton.java b/src/com/jordanbunke/stipple_effect/visual/menu_elements/colors/PaletteColorButton.java index b689b07c..c9fdf024 100644 --- a/src/com/jordanbunke/stipple_effect/visual/menu_elements/colors/PaletteColorButton.java +++ b/src/com/jordanbunke/stipple_effect/visual/menu_elements/colors/PaletteColorButton.java @@ -104,7 +104,7 @@ private void updateAssets() { base.draw(GraphicsUtils.loadIcon(IconCodes.EXCLUDED_FROM_PALETTE)); nh = base.submit(); - hi = GraphicsUtils.drawHighlightedButton(nh); + hi = GraphicsUtils.highlightButton(nh); } @Override diff --git a/src/com/jordanbunke/stipple_effect/visual/menu_elements/dialog/ActionPreviewer.java b/src/com/jordanbunke/stipple_effect/visual/menu_elements/dialog/ActionPreviewer.java new file mode 100644 index 00000000..aeb3a09b --- /dev/null +++ b/src/com/jordanbunke/stipple_effect/visual/menu_elements/dialog/ActionPreviewer.java @@ -0,0 +1,100 @@ +package com.jordanbunke.stipple_effect.visual.menu_elements.dialog; + +import com.jordanbunke.delta_time.debug.GameDebugger; +import com.jordanbunke.delta_time.image.GameImage; +import com.jordanbunke.delta_time.io.InputEventLogger; +import com.jordanbunke.delta_time.menu.menu_elements.MenuElement; +import com.jordanbunke.delta_time.utility.math.Bounds2D; +import com.jordanbunke.delta_time.utility.math.Coord2D; +import com.jordanbunke.stipple_effect.preview.PreviewPlayback; +import com.jordanbunke.stipple_effect.project.PlaybackInfo; +import com.jordanbunke.stipple_effect.utility.Constants; + +import java.util.Arrays; + +public class ActionPreviewer extends MenuElement implements PreviewPlayback { + private final GameImage[] frames; + private final double[] frameDurations; + private final PlaybackInfo playbackInfo; + private final int frameCount; + + private int frameIndex; + + public ActionPreviewer( + final Coord2D position, final Bounds2D dimensions, + final GameImage[] frames, final double[] frameDurations + ) { + super(position, dimensions, Anchor.CENTRAL_TOP, true); + + this.frames = frames; + frameCount = frames.length; + + if (frameDurations.length != frameCount) { + this.frameDurations = Arrays.stream(frames) + .mapToDouble(i -> Constants.DEFAULT_FRAME_DURATION).toArray(); + } else { + this.frameDurations = frameDurations; + } + + playbackInfo = PlaybackInfo.forPreview(); + + frameIndex = 0; + } + + @Override + public void process(final InputEventLogger eventLogger) { + if (frameCount > 1) + processKeys(eventLogger); + } + + @Override + public void update(final double deltaTime) { + animate(deltaTime); + } + + private void animate(final double deltaTime) { + if (playbackInfo.isPlaying()) { + final boolean nextFrameDue = + playbackInfo.checkIfNextFrameDue(deltaTime, frameDurations[frameIndex]); + + if (nextFrameDue) + frameIndex = playbackInfo.nextAnimationFrameForPreview(frameIndex, frameCount); + } + } + + @Override + public void render(final GameImage canvas) { + draw(frames[frameIndex], canvas); + } + + @Override + public void debugRender(GameImage canvas, GameDebugger debugger) {} + + public int getFrameIndex() { + return frameIndex; + } + + public int getFrameCount() { + return frameCount; + } + + public PlaybackInfo getPlaybackInfo() { + return playbackInfo; + } + + public void toFirstFrame() { + frameIndex = 0; + } + + public void toLastFrame() { + frameIndex = frameCount - 1; + } + + public void previousFrame() { + frameIndex = frameIndex == 0 ? frameCount - 1 : frameIndex - 1; + } + + public void nextFrame() { + frameIndex = (frameIndex + 1) % frameCount; + } +} diff --git a/src/com/jordanbunke/stipple_effect/visual/menu_elements/dialog/ApproveDialogButton.java b/src/com/jordanbunke/stipple_effect/visual/menu_elements/dialog/ApproveDialogButton.java index 78d9d754..320b860f 100644 --- a/src/com/jordanbunke/stipple_effect/visual/menu_elements/dialog/ApproveDialogButton.java +++ b/src/com/jordanbunke/stipple_effect/visual/menu_elements/dialog/ApproveDialogButton.java @@ -10,7 +10,6 @@ import com.jordanbunke.delta_time.utility.math.Coord2D; import com.jordanbunke.stipple_effect.StippleEffect; import com.jordanbunke.stipple_effect.utility.Permissions; -import com.jordanbunke.stipple_effect.utility.settings.Settings; import com.jordanbunke.stipple_effect.visual.GraphicsUtils; import java.util.function.Supplier; @@ -32,10 +31,10 @@ public ApproveDialogButton( super(position, dimensions, anchor, true); base = GraphicsUtils.drawTextButton(dimensions.width(), text, false); - highlighted = GraphicsUtils.drawHighlightedButton(base); + highlighted = GraphicsUtils.highlightButton(base); notMet = GraphicsUtils.drawTextButton( dimensions.width(), text, false, - Settings.getTheme().stubButtonBody.get()); + GraphicsUtils.ButtonType.STUB); this.clearDialog = clearDialog; diff --git a/src/com/jordanbunke/stipple_effect/visual/menu_elements/OutlineDirectionWatcher.java b/src/com/jordanbunke/stipple_effect/visual/menu_elements/dialog/OutlineDirectionWatcher.java similarity index 70% rename from src/com/jordanbunke/stipple_effect/visual/menu_elements/OutlineDirectionWatcher.java rename to src/com/jordanbunke/stipple_effect/visual/menu_elements/dialog/OutlineDirectionWatcher.java index 22717005..aa715dc7 100644 --- a/src/com/jordanbunke/stipple_effect/visual/menu_elements/OutlineDirectionWatcher.java +++ b/src/com/jordanbunke/stipple_effect/visual/menu_elements/dialog/OutlineDirectionWatcher.java @@ -1,9 +1,8 @@ -package com.jordanbunke.stipple_effect.visual.menu_elements; +package com.jordanbunke.stipple_effect.visual.menu_elements.dialog; import com.jordanbunke.delta_time.debug.GameDebugger; import com.jordanbunke.delta_time.image.GameImage; -import com.jordanbunke.delta_time.io.InputEventLogger; -import com.jordanbunke.delta_time.menu.menu_elements.MenuElement; +import com.jordanbunke.delta_time.menu.menu_elements.button.MenuButtonStub; import com.jordanbunke.delta_time.utility.math.Coord2D; import com.jordanbunke.stipple_effect.selection.Outliner; import com.jordanbunke.stipple_effect.utility.DialogVals; @@ -11,11 +10,11 @@ import com.jordanbunke.stipple_effect.utility.Layout; import com.jordanbunke.stipple_effect.visual.GraphicsUtils; -public class OutlineDirectionWatcher extends MenuElement { +public final class OutlineDirectionWatcher extends MenuButtonStub { private final Outliner.Direction direction; private int lastValue; - private GameImage image; + private GameImage image, highlighted; public OutlineDirectionWatcher( final Coord2D position, final Outliner.Direction direction @@ -24,11 +23,14 @@ public OutlineDirectionWatcher( this.direction = direction; lastValue = DialogVals.getThisOutlineSide(this.direction.ordinal()); - image = updateImage(); + + updateImage(); } @Override - public void process(final InputEventLogger eventLogger) {} + public void execute() { + DialogVals.setThisOutlineSide(direction.ordinal(), 0); + } @Override public void update(final double deltaTime) { @@ -36,11 +38,12 @@ public void update(final double deltaTime) { if (value != lastValue) { lastValue = value; - image = updateImage(); + + updateImage(); } } - private GameImage updateImage() { + private void updateImage() { final String code = lastValue == 0 ? IconCodes.NO_OUTLINE : (lastValue > 0 ? IconCodes.OUTLINE_PREFIX + @@ -48,12 +51,15 @@ private GameImage updateImage() { : IconCodes.OUTLINE_PREFIX + direction.opposite().name().toLowerCase()); - return GraphicsUtils.loadIcon(code); + image = GraphicsUtils.loadIcon(code); + + highlighted = GraphicsUtils.highlightButton( + GraphicsUtils.loadIcon(IconCodes.NO_OUTLINE)); } @Override public void render(final GameImage canvas) { - draw(image, canvas); + draw(isHighlighted() ? highlighted : image, canvas); } @Override diff --git a/src/com/jordanbunke/stipple_effect/visual/menu_elements/dialog/OutlineTextbox.java b/src/com/jordanbunke/stipple_effect/visual/menu_elements/dialog/OutlineTextbox.java index 340559ff..27b22e14 100644 --- a/src/com/jordanbunke/stipple_effect/visual/menu_elements/dialog/OutlineTextbox.java +++ b/src/com/jordanbunke/stipple_effect/visual/menu_elements/dialog/OutlineTextbox.java @@ -5,7 +5,6 @@ import com.jordanbunke.stipple_effect.utility.Constants; import com.jordanbunke.stipple_effect.utility.DialogVals; import com.jordanbunke.stipple_effect.utility.Layout; -import com.jordanbunke.stipple_effect.visual.menu_elements.OutlineDirectionWatcher; import java.util.function.Consumer; import java.util.function.Supplier; diff --git a/src/com/jordanbunke/stipple_effect/visual/menu_elements/dialog/SelectStateButton.java b/src/com/jordanbunke/stipple_effect/visual/menu_elements/dialog/SelectStateButton.java new file mode 100644 index 00000000..962c6ecd --- /dev/null +++ b/src/com/jordanbunke/stipple_effect/visual/menu_elements/dialog/SelectStateButton.java @@ -0,0 +1,68 @@ +package com.jordanbunke.stipple_effect.visual.menu_elements.dialog; + +import com.jordanbunke.delta_time.image.GameImage; +import com.jordanbunke.delta_time.io.InputEventLogger; +import com.jordanbunke.delta_time.menu.menu_elements.button.SimpleMenuButton; +import com.jordanbunke.delta_time.utility.math.Bounds2D; +import com.jordanbunke.delta_time.utility.math.Coord2D; +import com.jordanbunke.stipple_effect.utility.Layout; +import com.jordanbunke.stipple_effect.visual.GraphicsUtils; + +import java.util.function.Supplier; + +public class SelectStateButton extends SimpleMenuButton { + private final Supplier selectableChecker; + + private final GameImage stub; + + private boolean selectable; + + private SelectStateButton( + final Coord2D position, final Runnable onClick, + final Supplier selectableChecker, + final GameImage base, final GameImage highlighted, + final GameImage stub + ) { + super(position, new Bounds2D(Layout.STD_TEXT_BUTTON_W, + Layout.STD_TEXT_BUTTON_H), Anchor.LEFT_TOP, + true, onClick, base, highlighted); + + this.stub = stub; + this.selectableChecker = selectableChecker; + + update(0d); + } + + public static SelectStateButton make( + final Coord2D position, final Runnable onClick, + final Supplier selectableChecker + ) { + final GameImage base = GraphicsUtils.drawTextButton( + Layout.STD_TEXT_BUTTON_W, "Select", false), + highlighted = GraphicsUtils.highlightButton(base), + stub = GraphicsUtils.drawTextButton( + Layout.STD_TEXT_BUTTON_W, "Selected", false, + GraphicsUtils.ButtonType.STUB); + + return new SelectStateButton(position, onClick, + selectableChecker, base, highlighted, stub); + } + + @Override + public void update(final double deltaTime) { + selectable = selectableChecker.get(); + } + + @Override + public void render(final GameImage canvas) { + if (selectable) + super.render(canvas); + else + draw(stub, canvas); + } + + @Override + public void process(final InputEventLogger eventLogger) { + if (selectable) super.process(eventLogger); + } +} diff --git a/src/com/jordanbunke/stipple_effect/visual/menu_elements/dialog/TextToggleButton.java b/src/com/jordanbunke/stipple_effect/visual/menu_elements/dialog/TextToggleButton.java new file mode 100644 index 00000000..ef66fa0f --- /dev/null +++ b/src/com/jordanbunke/stipple_effect/visual/menu_elements/dialog/TextToggleButton.java @@ -0,0 +1,48 @@ +package com.jordanbunke.stipple_effect.visual.menu_elements.dialog; + +import com.jordanbunke.delta_time.image.GameImage; +import com.jordanbunke.delta_time.menu.menu_elements.button.SimpleToggleMenuButton; +import com.jordanbunke.delta_time.utility.math.Bounds2D; +import com.jordanbunke.delta_time.utility.math.Coord2D; +import com.jordanbunke.stipple_effect.utility.Layout; +import com.jordanbunke.stipple_effect.visual.GraphicsUtils; + +import java.util.Arrays; +import java.util.function.Supplier; + +public final class TextToggleButton extends SimpleToggleMenuButton { + private TextToggleButton( + final Coord2D position, + final GameImage[] bases, final GameImage[] highlights, + final Runnable[] chosenBehaviours, + final Supplier updateIndexLogic, + final Runnable globalBehaviour + ) { + super(position, new Bounds2D(Layout.STD_TEXT_BUTTON_W, + Layout.STD_TEXT_BUTTON_H), Anchor.LEFT_TOP, true, + bases, highlights, chosenBehaviours, + updateIndexLogic, globalBehaviour); + } + + public static TextToggleButton make( + final Coord2D position, final String[] texts, + final Runnable[] chosenBehaviours, + final Supplier updateIndexLogic, + final Runnable globalBehaviour + ) { + final GameImage[] bases = makeToggleButtonSet(texts), + highlights = Arrays.stream(bases) + .map(GraphicsUtils::highlightButton) + .toArray(GameImage[]::new); + + return new TextToggleButton(position, bases, highlights, + chosenBehaviours, updateIndexLogic, globalBehaviour); + } + + private static GameImage[] makeToggleButtonSet(final String... buttonTexts) { + return Arrays.stream(buttonTexts) + .map(t -> GraphicsUtils.drawTextButton( + Layout.STD_TEXT_BUTTON_W, t, false)) + .toArray(GameImage[]::new); + } +} diff --git a/src/com/jordanbunke/stipple_effect/visual/menu_elements/scrollable/HorizontalScrollBox.java b/src/com/jordanbunke/stipple_effect/visual/menu_elements/scrollable/HorizontalScrollBox.java index 32cc6c2a..494fc0f9 100644 --- a/src/com/jordanbunke/stipple_effect/visual/menu_elements/scrollable/HorizontalScrollBox.java +++ b/src/com/jordanbunke/stipple_effect/visual/menu_elements/scrollable/HorizontalScrollBox.java @@ -5,7 +5,7 @@ import com.jordanbunke.delta_time.utility.math.Bounds2D; import com.jordanbunke.delta_time.utility.math.Coord2D; import com.jordanbunke.stipple_effect.utility.Layout; -import com.jordanbunke.stipple_effect.visual.GraphicsUtils; +import com.jordanbunke.stipple_effect.utility.settings.Settings; public class HorizontalScrollBox extends AbstractHorizontalScrollBox { public HorizontalScrollBox( @@ -14,7 +14,7 @@ public HorizontalScrollBox( final int realRightX, final int initialOffsetX ) { super(position, dimensions, menuElements, - GraphicsUtils::drawScrollBoxBackground, + Settings.getTheme().logic::drawScrollBoxBackground, Layout.PX_PER_SCROLL, realRightX, initialOffsetX); } diff --git a/src/com/jordanbunke/stipple_effect/visual/menu_elements/scrollable/HorizontalSlider.java b/src/com/jordanbunke/stipple_effect/visual/menu_elements/scrollable/HorizontalSlider.java index 6a4cb61a..89e09d0d 100644 --- a/src/com/jordanbunke/stipple_effect/visual/menu_elements/scrollable/HorizontalSlider.java +++ b/src/com/jordanbunke/stipple_effect/visual/menu_elements/scrollable/HorizontalSlider.java @@ -36,13 +36,11 @@ public void drawSlider(final GameImage slider) { sh = getHeight() - (2 * Layout.SLIDER_THINNING); // slider core - slider.draw(drawSliderCore(sd, sh), Layout.SLIDER_BALL_DIM / 2, Layout.SLIDER_THINNING); + slider.draw(drawSliderCore(sd, sh), + Layout.SLIDER_BALL_DIM / 2, Layout.SLIDER_THINNING); // slider outline - slider.drawRectangle(Settings.getTheme().buttonOutline.get(), - Layout.BUTTON_BORDER_PX, Layout.SLIDER_BALL_DIM / 2, - Layout.SLIDER_THINNING + (Layout.BUTTON_BORDER_PX / 2), sd, - getHeight() - (Layout.BUTTON_BORDER_PX + (2 * Layout.SLIDER_THINNING))); + Settings.getTheme().logic.drawHorizontalSliderOutline(slider); } @Override diff --git a/src/com/jordanbunke/stipple_effect/visual/menu_elements/scrollable/Slider.java b/src/com/jordanbunke/stipple_effect/visual/menu_elements/scrollable/Slider.java index edcc071a..07e80b2c 100644 --- a/src/com/jordanbunke/stipple_effect/visual/menu_elements/scrollable/Slider.java +++ b/src/com/jordanbunke/stipple_effect/visual/menu_elements/scrollable/Slider.java @@ -8,7 +8,7 @@ import com.jordanbunke.funke.core.ConcreteProperty; import com.jordanbunke.stipple_effect.utility.Layout; import com.jordanbunke.stipple_effect.utility.settings.Settings; -import com.jordanbunke.stipple_effect.visual.GraphicsUtils; +import com.jordanbunke.stipple_effect.visual.theme.Theme; import java.awt.*; import java.util.function.Consumer; @@ -28,50 +28,31 @@ public Slider( } @Override - public void debugRender(final GameImage canvas, final GameDebugger debugger) { - - } + public void debugRender(final GameImage canvas, final GameDebugger debugger) {} // graphical public GameImage[] drawSliderBalls() { - final int BASE = 0, HIGHLIGHTED = 1, SELECTED = 2; - final int sbd = Layout.SLIDER_BALL_DIM; + final Theme t = Settings.getTheme(); + final Color fill = getSliderBallCoreColor(); - final int sbcd = sbd - (2 * Layout.BUTTON_BORDER_PX); - - final GameImage[] sliderBalls = new GameImage[] { - new GameImage(sbd, sbd), - GameImage.dummy(), - new GameImage(sbd, sbd) + return new GameImage[] { + t.logic.drawSliderBall(false, false, fill), + t.logic.drawSliderBall(false, true, fill), + t.logic.drawSliderBall(true, false, fill) }; - - sliderBalls[BASE].fillRectangle( - GraphicsUtils.buttonBorderColor(false), 0, 0, sbd, sbd); - sliderBalls[SELECTED].fillRectangle( - GraphicsUtils.buttonBorderColor(true), 0, 0, sbd, sbd); - - sliderBalls[BASE].fillRectangle(getSliderBallCoreColor(), - Layout.BUTTON_BORDER_PX, Layout.BUTTON_BORDER_PX, sbcd, sbcd); - sliderBalls[SELECTED].fillRectangle(getSliderBallCoreColor(), - Layout.BUTTON_BORDER_PX, Layout.BUTTON_BORDER_PX, sbcd, sbcd); - - sliderBalls[HIGHLIGHTED] = - GraphicsUtils.drawHighlightedButton(sliderBalls[BASE]); - - return sliderBalls; } public GameImage drawSliderCore(final int w, final int h) { final GameImage sliderCore = new GameImage(w, h); sliderCore.fillRectangle( - Settings.getTheme().defaultSliderCore.get(), 0, 0, w, h); + Settings.getTheme().defaultSliderCore, 0, 0, w, h); return sliderCore.submit(); } public Color getSliderBallCoreColor() { - return Settings.getTheme().defaultSliderBall.get(); + return Settings.getTheme().defaultSliderBall; } public abstract void drawSlider(final GameImage slider); diff --git a/src/com/jordanbunke/stipple_effect/visual/menu_elements/scrollable/VerticalScrollBox.java b/src/com/jordanbunke/stipple_effect/visual/menu_elements/scrollable/VerticalScrollBox.java index e4c96b8d..30219b37 100644 --- a/src/com/jordanbunke/stipple_effect/visual/menu_elements/scrollable/VerticalScrollBox.java +++ b/src/com/jordanbunke/stipple_effect/visual/menu_elements/scrollable/VerticalScrollBox.java @@ -5,7 +5,7 @@ import com.jordanbunke.delta_time.utility.math.Bounds2D; import com.jordanbunke.delta_time.utility.math.Coord2D; import com.jordanbunke.stipple_effect.utility.Layout; -import com.jordanbunke.stipple_effect.visual.GraphicsUtils; +import com.jordanbunke.stipple_effect.utility.settings.Settings; public class VerticalScrollBox extends AbstractVerticalScrollBox { public VerticalScrollBox( @@ -14,7 +14,7 @@ public VerticalScrollBox( final int realBottomY, final int initialOffsetY ) { super(position, dimensions, menuElements, - GraphicsUtils::drawScrollBoxBackground, + Settings.getTheme().logic::drawScrollBoxBackground, Layout.PX_PER_SCROLL, realBottomY, initialOffsetY); } diff --git a/src/com/jordanbunke/stipple_effect/visual/menu_elements/scrollable/VerticalSlider.java b/src/com/jordanbunke/stipple_effect/visual/menu_elements/scrollable/VerticalSlider.java index ed46c9b9..eb8e5810 100644 --- a/src/com/jordanbunke/stipple_effect/visual/menu_elements/scrollable/VerticalSlider.java +++ b/src/com/jordanbunke/stipple_effect/visual/menu_elements/scrollable/VerticalSlider.java @@ -25,15 +25,11 @@ public void drawSlider(final GameImage slider) { sw = getWidth() - (2 * Layout.SLIDER_THINNING); // slider core - slider.draw(drawSliderCore(sw, sd), Layout.SLIDER_THINNING, Layout.SLIDER_BALL_DIM / 2); + slider.draw(drawSliderCore(sw, sd), + Layout.SLIDER_THINNING, Layout.SLIDER_BALL_DIM / 2); // slider outline - slider.drawRectangle(Settings.getTheme().buttonOutline.get(), - Layout.BUTTON_BORDER_PX, - Layout.SLIDER_THINNING + (Layout.BUTTON_BORDER_PX / 2), - Layout.SLIDER_BALL_DIM / 2, - getWidth() - (Layout.BUTTON_BORDER_PX + (2 * Layout.SLIDER_THINNING)), sd - ); + Settings.getTheme().logic.drawVerticalSliderOutline(slider); } @Override diff --git a/src/com/jordanbunke/stipple_effect/visual/theme/SEColors.java b/src/com/jordanbunke/stipple_effect/visual/theme/SEColors.java index a8a10150..29e13042 100644 --- a/src/com/jordanbunke/stipple_effect/visual/theme/SEColors.java +++ b/src/com/jordanbunke/stipple_effect/visual/theme/SEColors.java @@ -8,8 +8,11 @@ public class SEColors { BLACK = new Color(0, 0, 0), WHITE = new Color(255, 255, 255), LIGHT_GREY = new Color(192, 192, 192), + LIGHTEST_GREY = new Color(224, 224, 224), + TRANSLUCENT_GREY_1 = new Color(192, 192, 192, 100), + TRANSLUCENT_GREY_2 = new Color(128, 128, 128, 50), NAVY = new Color(40, 40, 60), - GREY = new Color(127, 127, 127), + GREY = new Color(128, 128, 128), DARK_GREY = new Color(55, 55, 55), DARK_RED = new Color(100, 20, 20), VEIL = new Color(192, 192, 192, 200), @@ -20,13 +23,23 @@ public class SEColors { FOLIAGE = new Color(18, 45, 4), GOLD = new Color(210, 169, 34), DARK_GOLD = new Color(77, 60, 10), - TRANSLUCENT_GOLD = new Color(210, 169, 34, 100), + TRANSLUCENT_GOLD_1 = new Color(210, 169, 34, 100), + TRANSLUCENT_GOLD_2 = new Color(210, 169, 34, 50), RED = new Color(255, 0, 0), + BOURGOGNE = new Color(143, 10, 37), + HAITI_RED = new Color(210, 16, 52), + HAITI_BLUE = new Color(1, 32, 160), + CEU = new Color(97, 121, 213), + DEEP_OCEAN = new Color(21, 29, 80), PURPLE = new Color(100, 0, 125), + HAITI_PURPLE = new Color(106, 24, 106), + CHITIN = new Color(144, 79, 161), + NAIJ = new Color(34, 94, 34), VERDANT = new Color(30, 255, 15), - BONE = new Color(224, 210, 181), - CRACKED = new Color(231, 223, 212), - CREAM = new Color(216, 182, 138), + RADAR = new Color(61, 39, 73), + TRANSLUCENT_VERDANT_1 = new Color(30, 255, 15, 100), + TRANSLUCENT_VERDANT_2 = new Color(30, 255, 15, 50), + SAND = new Color(216, 182, 138), TRANSPARENT = new Color(0, 0, 0, 0); public static Color black() { @@ -44,4 +57,12 @@ public static Color transparent() { public static Color def() { return BLACK; } + + public static Color red() { + return RED; + } + + public static Color green() { + return new Color(0, 0xA0, 0); + } } diff --git a/src/com/jordanbunke/stipple_effect/visual/theme/Theme.java b/src/com/jordanbunke/stipple_effect/visual/theme/Theme.java index ad54f702..2b550910 100644 --- a/src/com/jordanbunke/stipple_effect/visual/theme/Theme.java +++ b/src/com/jordanbunke/stipple_effect/visual/theme/Theme.java @@ -1,10 +1,11 @@ package com.jordanbunke.stipple_effect.visual.theme; -import java.awt.Color; -import java.util.function.Supplier; +import com.jordanbunke.stipple_effect.visual.theme.logic.ThemeLogic; + +import java.awt.*; public class Theme { - public final Supplier + public final Color textLight, textDark, affixTextLight, affixTextDark, textMenuHeading, textShortcut, @@ -16,6 +17,9 @@ public class Theme { splashText, splashFlashingText, splashBackground, checkerboard1, checkerboard2; + public final ThemeLogic logic; + public final String subtitle; + Theme( final Color textLight, final Color textDark, final Color affixTextLight, final Color affixTextDark, @@ -40,38 +44,43 @@ public class Theme { final Color splashBackground, final Color checkerboard1, - final Color checkerboard2 + final Color checkerboard2, + + final ThemeLogic logic, final String subtitle ) { - this.textLight = () -> textLight; - this.textDark = () -> textDark; - this.affixTextLight = () -> affixTextLight; - this.affixTextDark = () -> affixTextDark; - this.textMenuHeading = () -> textMenuHeading; - this.textShortcut = () -> textShortcut; - - this.workspaceBackground = () -> workspaceBackground; - this.panelBackground = () -> panelBackground; - this.panelDivisions = () -> panelDivisions; - - this.scrollBackground = () -> scrollBackground; - this.dialogVeil = () -> dialogVeil; - this.selectionFill = () -> selectionFill; - this.buttonBody = () -> buttonBody; - this.stubButtonBody = () -> stubButtonBody; - this.dropdownOptionBody = () -> dropdownOptionBody; - this.defaultSliderCore = () -> defaultSliderCore; - this.defaultSliderBall = () -> defaultSliderBall; - this.buttonOutline = () -> buttonOutline; - - this.highlightOutline = () -> highlightOutline; - this.highlightOverlay = () -> highlightOverlay; - this.invalid = () -> invalid; - - this.splashText = () -> splashText; - this.splashFlashingText = () -> splashFlashingText; - this.splashBackground = () -> splashBackground; - - this.checkerboard1 = () -> checkerboard1; - this.checkerboard2 = () -> checkerboard2; + this.textLight = textLight; + this.textDark = textDark; + this.affixTextLight = affixTextLight; + this.affixTextDark = affixTextDark; + this.textMenuHeading = textMenuHeading; + this.textShortcut = textShortcut; + + this.workspaceBackground = workspaceBackground; + this.panelBackground = panelBackground; + this.panelDivisions = panelDivisions; + + this.scrollBackground = scrollBackground; + this.dialogVeil = dialogVeil; + this.selectionFill = selectionFill; + this.buttonBody = buttonBody; + this.stubButtonBody = stubButtonBody; + this.dropdownOptionBody = dropdownOptionBody; + this.defaultSliderCore = defaultSliderCore; + this.defaultSliderBall = defaultSliderBall; + this.buttonOutline = buttonOutline; + + this.highlightOutline = highlightOutline; + this.highlightOverlay = highlightOverlay; + this.invalid = invalid; + + this.splashText = splashText; + this.splashFlashingText = splashFlashingText; + this.splashBackground = splashBackground; + + this.checkerboard1 = checkerboard1; + this.checkerboard2 = checkerboard2; + + this.logic = logic; + this.subtitle = subtitle; } } diff --git a/src/com/jordanbunke/stipple_effect/visual/theme/ThemeBuilder.java b/src/com/jordanbunke/stipple_effect/visual/theme/ThemeBuilder.java index a4f0b624..ad1b774c 100644 --- a/src/com/jordanbunke/stipple_effect/visual/theme/ThemeBuilder.java +++ b/src/com/jordanbunke/stipple_effect/visual/theme/ThemeBuilder.java @@ -1,5 +1,7 @@ package com.jordanbunke.stipple_effect.visual.theme; +import com.jordanbunke.stipple_effect.visual.theme.logic.*; + import java.awt.*; import static com.jordanbunke.stipple_effect.visual.theme.SEColors.*; @@ -15,6 +17,8 @@ public class ThemeBuilder { highlightOutline, highlightOverlay, invalid, splashText, splashFlashingText, splashBackground, checkerboard1, checkerboard2; + private ThemeLogic logic; + private String subtitle; private ThemeBuilder() { // DEFAULTS @@ -53,6 +57,11 @@ private ThemeBuilder() { // checkerboard checkerboard1 = WHITE; checkerboard2 = LIGHT_GREY; + + // theme logic + logic = DefaultThemeLogic.get(); + + subtitle = "For indie game developers by an indie game developer"; } public Theme build() { @@ -65,7 +74,7 @@ public Theme build() { defaultSliderCore, defaultSliderBall, buttonOutline, highlightOutline, highlightOverlay, invalid, splashText, splashFlashingText, splashBackground, - checkerboard1, checkerboard2); + checkerboard1, checkerboard2, logic, subtitle); } public static Theme def() { @@ -73,76 +82,229 @@ public static Theme def() { } public static Theme zo() { - return new ThemeBuilder() - .setPanelBackground(BONE) - .setPanelDivisions(DARK_RED) - .setTextLight(PURPLE) - .setTextDark(DARK_GOLD) - .setAffixTextDark(BLACK) - .setAffixTextLight(WHITE) - .setTextShortcut(RED) - .setTextMenuHeading(DARK_RED) - .setDropdownOptionBody(GOLD) - .setDefaultSliderCore(WHITE) - .setDefaultSliderBall(CREAM) - .setWorkspaceBackground(DARK_RED) - .setScrollBackground(CRACKED) - .setButtonOutline(DARK_RED) - .setHighlightOutline(RED) - .setButtonBody(CREAM) - .setSplashBackground(DARK_RED) - .setSplashText(RED) - .setSplashFlashingText(GOLD) - .setHighlightOverlay(TRANSLUCENT_GOLD) - .setDialogVeil(TRANSLUCENT_GOLD) - .setCheckerboard1(BONE) - .setCheckerboard2(CREAM) - .setStubButtonBody(RED) - .setInvalid(RED) - .build(); + final ThemeBuilder zoTB = new ThemeBuilder() + .setLogic(ZoThemeLogic.get()) + .setSubtitle("Honour your ancestors!"); + + // backgrounds + zoTB.setPanelBackground(BOURGOGNE) + .setWorkspaceBackground(DEEP_OCEAN) + .setScrollBackground(TRANSPARENT) + .setSplashBackground(HAITI_PURPLE); + + // text + zoTB.setTextLight(GOLD) + .setTextDark(BLACK) + .setAffixTextDark(HAITI_BLUE) + .setAffixTextLight(HAITI_RED) + .setTextMenuHeading(CEU) + .setTextShortcut(CEU) + .setSplashText(GOLD) + .setSplashFlashingText(GOLD); + + // UI element bodies + zoTB.setDefaultSliderCore(HAITI_BLUE) + .setDefaultSliderBall(HAITI_RED) + .setDropdownOptionBody(BLACK) + .setButtonBody(HAITI_RED) + .setInvalid(BLACK) + .setStubButtonBody(BOURGOGNE); + + // UI element outlines + zoTB.setButtonOutline(BLACK) + .setHighlightOutline(GOLD) + .setPanelDivisions(HAITI_PURPLE); + + // selection + zoTB.setSelectionFill(TRANSLUCENT_GOLD_2) + .setHighlightOverlay(TRANSLUCENT_GOLD_1); + + zoTB.setCheckerboard1(GREY).setCheckerboard2(MID_DARK_GREY); + + return zoTB.setDialogVeil(TRANSLUCENT_GOLD_1).build(); } public static Theme neon() { - return new ThemeBuilder() - .setPanelBackground(BLACK) - .setPanelDivisions(WHITE) - .setTextLight(WHITE) + final ThemeBuilder neonTB = new ThemeBuilder() + .setLogic(NeonThemeLogic.get()) + .setSubtitle("Night owls welcome!"); + + // backgrounds + neonTB.setPanelBackground(BLACK) + .setWorkspaceBackground(RADAR) + .setScrollBackground(BLACK) + .setSplashBackground(BLACK); + + // text + neonTB.setTextLight(WHITE) .setTextDark(DARK_GREY) .setAffixTextDark(PURPLE) .setAffixTextLight(VERDANT) .setTextMenuHeading(PASTEL_BLUE) + .setSplashFlashingText(PURPLE) + .setSplashText(WHITE); + + // UI element bodies + neonTB.setDefaultSliderCore(BLACK) + .setDefaultSliderBall(BLACK) .setDropdownOptionBody(RED) - .setDefaultSliderCore(BLACK) - .setDefaultSliderBall(PURPLE) - .setWorkspaceBackground(DARK_GREY) - .setScrollBackground(BLACK) - .setButtonOutline(WHITE) - .setHighlightOutline(VERDANT) .setButtonBody(PURPLE) - .setSplashBackground(BLACK) - .setSplashText(WHITE) - .setSplashFlashingText(PURPLE) - .setStubButtonBody(BLACK) - .build(); + .setInvalid(BLACK) + .setStubButtonBody(BLACK); + + // UI element outlines + neonTB.setButtonOutline(CHITIN) + .setPanelDivisions(PURPLE) + .setHighlightOutline(VERDANT); + + // selection + neonTB.setSelectionFill(TRANSLUCENT_VERDANT_2) + .setHighlightOverlay(TRANSLUCENT_VERDANT_1); + + neonTB.setCheckerboard1(MID_DARK_GREY) + .setCheckerboard2(DARK_GREY); + + return neonTB.build(); } public static Theme bunkering() { - return new ThemeBuilder() - .setTextShortcut(GOLD) - .setWorkspaceBackground(DARK_GOLD) + final ThemeBuilder bunkerTB = new ThemeBuilder() + .setLogic(BunkeringThemeLogic.get()) + .setSubtitle("Save the Niger Delta!"); + + // backgrounds + bunkerTB.setSplashBackground(NAIJ) .setPanelBackground(DARK_OIL) - .setScrollBackground(FOLIAGE) - .setSelectionFill(TRANSLUCENT_GOLD) - .setButtonBody(BLACK) - .setStubButtonBody(GREY) - .setDefaultSliderCore(FOLIAGE) - .setButtonOutline(DARK_GOLD) + .setWorkspaceBackground(FOLIAGE) + .setScrollBackground(TRANSPARENT); + + // text + bunkerTB.setTextLight(GOLD) + .setTextDark(DARK_OIL) + .setAffixTextDark(BLACK) + .setAffixTextLight(WHITE) + .setTextMenuHeading(NAIJ) + .setTextShortcut(NAIJ) + .setSplashText(WHITE) + .setSplashFlashingText(GOLD); + + // UI element bodies + bunkerTB.setDefaultSliderCore(NAIJ) + .setDefaultSliderBall(WHITE) + .setDropdownOptionBody(NAIJ) + .setStubButtonBody(RED) + .setInvalid(RED) + .setButtonBody(BLACK); + + // UI element outlines + bunkerTB.setButtonOutline(DARK_GOLD) .setHighlightOutline(GOLD) - .setHighlightOverlay(TRANSLUCENT_GOLD) - .setSplashText(GOLD) - .setSplashBackground(DARK_OIL) - .setCheckerboard1(GREY) - .setCheckerboard2(MID_DARK_GREY).build(); + .setPanelDivisions(GOLD); + + // selection + bunkerTB.setHighlightOverlay(TRANSLUCENT_GOLD_1) + .setSelectionFill(TRANSLUCENT_GOLD_2); + + bunkerTB.setCheckerboard2(MID_DARK_GREY).setCheckerboard1(GREY); + + return bunkerTB.build(); + } + + public static Theme asylum() { + final ThemeBuilder asylumTB = new ThemeBuilder() + .setLogic(AsylumThemeLogic.get()) + .setSubtitle("Take care of your mental health!"); + + // backgrounds + asylumTB.setSplashBackground(BLACK) + .setPanelBackground(WHITE) + .setWorkspaceBackground(LIGHT_GREY) + .setScrollBackground(WHITE); + + // text + asylumTB.setTextLight(WHITE) + .setTextDark(BLACK) + .setAffixTextDark(DARK_GREY) + .setAffixTextLight(LIGHT_GREY) + .setTextMenuHeading(DARK_GREY) + .setTextShortcut(GREY) + .setSplashText(WHITE) + .setSplashFlashingText(WHITE); + + // UI element bodies + asylumTB.setDefaultSliderCore(WHITE) + .setDefaultSliderBall(LIGHT_GREY) + .setDropdownOptionBody(BLACK) + .setStubButtonBody(WHITE) + .setInvalid(WHITE) + .setButtonBody(MID_DARK_GREY); + + // UI element outlines + asylumTB.setButtonOutline(BLACK) + .setHighlightOutline(GREY) + .setPanelDivisions(BLACK); + + // selection + asylumTB.setHighlightOverlay(TRANSLUCENT_GREY_1) + .setSelectionFill(TRANSLUCENT_GREY_2); + + asylumTB.setCheckerboard2(WHITE).setCheckerboard1(LIGHTEST_GREY); + + return asylumTB.build(); + } + + public static Theme ramallah() { + final ThemeBuilder ramallahTB = new ThemeBuilder() + .setLogic(RamallahThemeLogic.get()) + .setSubtitle("Be on the right side of history!"); + + // backgrounds + ramallahTB.setSplashBackground(SAND) + .setPanelBackground(WHITE) + .setWorkspaceBackground(DARK_RED) + .setScrollBackground(TRANSPARENT); + + // text + ramallahTB.setTextLight(WHITE) + .setTextDark(BLACK) + .setAffixTextDark(DARK_GREY) + .setAffixTextLight(LIGHTEST_GREY) + .setTextMenuHeading(NAIJ) + .setTextShortcut(RED) + .setSplashText(BLACK) + .setSplashFlashingText(DARK_RED); + + // UI element bodies + ramallahTB.setDefaultSliderCore(NAIJ) + .setDefaultSliderBall(NAIJ) + .setDropdownOptionBody(NAIJ) + .setStubButtonBody(GREY) + .setInvalid(GREY) + .setButtonBody(BLACK); + + // UI element outlines + ramallahTB.setButtonOutline(WHITE) + .setHighlightOutline(RED) + .setPanelDivisions(BLACK); + + // selection + ramallahTB.setHighlightOverlay(VEIL) + .setSelectionFill(VEIL); + + ramallahTB.setCheckerboard1(GREY) + .setCheckerboard2(MID_DARK_GREY); + + return ramallahTB.setDialogVeil(DARK_RED).build(); + } + + public ThemeBuilder setLogic(final ThemeLogic logic) { + this.logic = logic; + return this; + } + + public ThemeBuilder setSubtitle(final String subtitle) { + this.subtitle = subtitle; + return this; } public ThemeBuilder setTextLight(final Color textLight) { diff --git a/src/com/jordanbunke/stipple_effect/visual/theme/Themes.java b/src/com/jordanbunke/stipple_effect/visual/theme/Themes.java index 5c843326..114d0d9f 100644 --- a/src/com/jordanbunke/stipple_effect/visual/theme/Themes.java +++ b/src/com/jordanbunke/stipple_effect/visual/theme/Themes.java @@ -6,7 +6,9 @@ public enum Themes { DEFAULT(ThemeBuilder.def()), ZO(ThemeBuilder.zo()), NEON(ThemeBuilder.neon()), - BUNKERING(ThemeBuilder.bunkering()); + BUNKERING(ThemeBuilder.bunkering()), + ASYLUM(ThemeBuilder.asylum()), + RAMALLAH(ThemeBuilder.ramallah()); private final Theme theme; diff --git a/src/com/jordanbunke/stipple_effect/visual/theme/logic/AsylumThemeLogic.java b/src/com/jordanbunke/stipple_effect/visual/theme/logic/AsylumThemeLogic.java new file mode 100644 index 00000000..932898c5 --- /dev/null +++ b/src/com/jordanbunke/stipple_effect/visual/theme/logic/AsylumThemeLogic.java @@ -0,0 +1,126 @@ +package com.jordanbunke.stipple_effect.visual.theme.logic; + +import com.jordanbunke.delta_time.image.GameImage; +import com.jordanbunke.delta_time.image.ImageProcessing; +import com.jordanbunke.delta_time.io.ResourceLoader; +import com.jordanbunke.stipple_effect.utility.Layout; +import com.jordanbunke.stipple_effect.utility.settings.Settings; +import com.jordanbunke.stipple_effect.visual.GraphicsUtils; +import com.jordanbunke.stipple_effect.visual.theme.SEColors; +import com.jordanbunke.stipple_effect.visual.theme.Theme; + +import java.awt.*; +import java.nio.file.Path; + +public final class AsylumThemeLogic extends ThemeLogic { + private static final AsylumThemeLogic INSTANCE; + + private final Path FOLDER; + + static { + INSTANCE = new AsylumThemeLogic(); + } + + private AsylumThemeLogic() { + FOLDER = THEMES_FOLDER.resolve("asylum"); + } + + public static AsylumThemeLogic get() { + return INSTANCE; + } + + @Override + public GameImage transformIcon(final GameImage asset) { + return pixelWiseTransformation(asset, c -> c.equals(SEColors.white()) + ? SEColors.transparent() : GraphicsUtils.greyscale(c)); + } + + @Override + public GameImage selectedIconOverlay() { + return pixelWiseTransformation( + super.selectedIconOverlay(), + GraphicsUtils::greyscale); + } + + @Override + public GameImage highlightedIconOverlay() { + return pixelWiseTransformation( + super.highlightedIconOverlay(), + GraphicsUtils::greyscale); + } + + @Override + public GameImage unclickableIcon(final GameImage baseIcon) { + return new GameImage(baseIcon.getWidth(), baseIcon.getHeight()); + } + + @Override + public GameImage drawTextButton( + final int width, final String text, final boolean selected, + final GraphicsUtils.ButtonType type + ) { + final Theme t = Settings.getTheme(); + + final Color backgroundColor = switch (type) { + case DD_OPTION -> t.dropdownOptionBody; + case STUB -> t.stubButtonBody; + default -> t.buttonBody; + }; + + final boolean leftAligned = type.isDropdown(); + final boolean drawBorder = type != GraphicsUtils.ButtonType.DD_OPTION; + + final Color textColor = intuitTextColor(backgroundColor, true); + final GameImage textImage = GraphicsUtils.uiText(textColor) + .addText(text).build().draw(); + + final int w = Math.max(width, textImage.getWidth() + + (4 * Layout.BUTTON_BORDER_PX)), + h = Layout.STD_TEXT_BUTTON_H; + + final GameImage nhi = new GameImage(w, h); + nhi.fillRectangle(backgroundColor, 0, 0, w, h); + + final int textX = leftAligned + ? (2 * Layout.BUTTON_BORDER_PX) + : (w - textImage.getWidth()) / 2; + + if (selected) { + final int INC = Layout.BUTTON_BORDER_PX; + final Color c = t.highlightOutline; + + for (int x = INC; x < w - INC; x += INC) + for (int y = INC; y < h - INC; y += INC) + if (((x / INC) + (y / INC)) % 2 == 0) + nhi.fillRectangle(c, x, y, INC, INC); + } + + nhi.draw(textImage, textX, Layout.BUTTON_TEXT_OFFSET_Y); + + if (drawBorder) { + final Color frame = buttonBorderColor(selected); + nhi.drawRectangle(frame, 2f * Layout.BUTTON_BORDER_PX, 0, 0, w, h); + } + + return nhi.submit(); + } + + @Override + public GameImage[] loadSplash() { + final int SPLASH_FRAMES = 20, SPLASH_SCALE_UP = 5; + final String SPLASH_FRAME_BASE = "splash_"; + + final GameImage[] frames = new GameImage[SPLASH_FRAMES]; + + for (int i = 0; i < SPLASH_FRAMES; i++) { + final Path framePath = FOLDER.resolve( + SPLASH_FRAME_BASE + i + ".png"); + + frames[i] = ImageProcessing.scale( + ResourceLoader.loadImageResource(framePath), + SPLASH_SCALE_UP); + } + + return frames; + } +} diff --git a/src/com/jordanbunke/stipple_effect/visual/theme/logic/BunkeringThemeLogic.java b/src/com/jordanbunke/stipple_effect/visual/theme/logic/BunkeringThemeLogic.java new file mode 100644 index 00000000..43893f03 --- /dev/null +++ b/src/com/jordanbunke/stipple_effect/visual/theme/logic/BunkeringThemeLogic.java @@ -0,0 +1,96 @@ +package com.jordanbunke.stipple_effect.visual.theme.logic; + +import com.jordanbunke.delta_time.image.GameImage; +import com.jordanbunke.delta_time.image.ImageProcessing; +import com.jordanbunke.delta_time.io.ResourceLoader; +import com.jordanbunke.stipple_effect.utility.Constants; +import com.jordanbunke.stipple_effect.utility.math.ColorMath; +import com.jordanbunke.stipple_effect.utility.settings.Settings; +import com.jordanbunke.stipple_effect.visual.GraphicsUtils; +import com.jordanbunke.stipple_effect.visual.theme.SEColors; +import com.jordanbunke.stipple_effect.visual.theme.Theme; + +import java.awt.*; +import java.nio.file.Path; + +public final class BunkeringThemeLogic extends ThemeLogic { + private static final BunkeringThemeLogic INSTANCE; + + private final Path FOLDER; + + static { + INSTANCE = new BunkeringThemeLogic(); + } + + private BunkeringThemeLogic() { + FOLDER = THEMES_FOLDER.resolve("bunkering"); + } + + public static BunkeringThemeLogic get() { + return INSTANCE; + } + + @Override + public GameImage transformIcon(final GameImage asset) { + final Color tint = Settings.getTheme().highlightOutline; + final double hue = ColorMath.rgbToHue(tint), + sat = ColorMath.rgbToSat(tint), + SAT_THRESHOLD = 0.15; + + return pixelWiseTransformation(asset, c -> { + if (c.equals(SEColors.white())) + return tint; + else if (c.getAlpha() == Constants.RGBA_SCALE && + ColorMath.rgbToSat(c) < SAT_THRESHOLD) { + return ColorMath.fromHSV(hue, sat, + ColorMath.rgbToValue(c), c.getAlpha()); + } + + return c; + }); + } + + @Override + public GameImage highlightedIconOverlay() { + return overlayTransformation(super.highlightedIconOverlay()); + } + + @Override + public GameImage selectedIconOverlay() { + return overlayTransformation(super.selectedIconOverlay()); + } + + private GameImage overlayTransformation(final GameImage asset) { + final Color tint = Settings.getTheme().highlightOutline; + return hueFromColorTransformation(asset, tint); + } + + @Override + public GameImage unclickableIcon(final GameImage baseIcon) { + final Theme t = Settings.getTheme(); + + return pixelWiseTransformation(baseIcon, + c -> c.equals(t.highlightOutline) + ? SEColors.transparent() + : GraphicsUtils.greyscale(c)); + } + + @Override + public GameImage[] loadSplash() { + final int SPLASH_FRAMES = 10, SPLASH_SCALE_UP = 6; + final String SPLASH_FRAME_BASE = "splash_"; + + final GameImage[] frames = new GameImage[SPLASH_FRAMES]; + + for (int i = 0; i < SPLASH_FRAMES; i++) { + final Path framePath = FOLDER.resolve( + SPLASH_FRAME_BASE + i + ".png"); + + frames[i] = ImageProcessing.scale( + ResourceLoader.loadImageResource(framePath), + SPLASH_SCALE_UP); + } + + return frames; + } +} diff --git a/src/com/jordanbunke/stipple_effect/visual/theme/logic/DefaultThemeLogic.java b/src/com/jordanbunke/stipple_effect/visual/theme/logic/DefaultThemeLogic.java new file mode 100644 index 00000000..57d49bd3 --- /dev/null +++ b/src/com/jordanbunke/stipple_effect/visual/theme/logic/DefaultThemeLogic.java @@ -0,0 +1,46 @@ +package com.jordanbunke.stipple_effect.visual.theme.logic; + +import com.jordanbunke.delta_time.image.GameImage; +import com.jordanbunke.delta_time.image.ImageProcessing; +import com.jordanbunke.delta_time.io.ResourceLoader; + +import java.nio.file.Path; + +public final class DefaultThemeLogic extends ThemeLogic { + private static final Path SPLASH_FOLDER = Path.of("splash"); + private static final int SPLASH_FRAMES = 25, SPLASH_SCALE_UP = 3; + private static final String SPLASH_FRAME_BASE = "logo_anim_"; + + private static final DefaultThemeLogic INSTANCE; + + static { + INSTANCE = new DefaultThemeLogic(); + } + + private DefaultThemeLogic() {} + + public static DefaultThemeLogic get() { + return INSTANCE; + } + + @Override + public GameImage transformIcon(final GameImage asset) { + return asset; + } + + @Override + public GameImage[] loadSplash() { + final GameImage[] frames = new GameImage[SPLASH_FRAMES]; + + for (int i = 0; i < SPLASH_FRAMES; i++) { + final Path framePath = SPLASH_FOLDER.resolve( + SPLASH_FRAME_BASE + i + ".png"); + + frames[i] = ImageProcessing.scale( + ResourceLoader.loadImageResource(framePath), + SPLASH_SCALE_UP); + } + + return frames; + } +} diff --git a/src/com/jordanbunke/stipple_effect/visual/theme/logic/NeonThemeLogic.java b/src/com/jordanbunke/stipple_effect/visual/theme/logic/NeonThemeLogic.java new file mode 100644 index 00000000..30cf4fd7 --- /dev/null +++ b/src/com/jordanbunke/stipple_effect/visual/theme/logic/NeonThemeLogic.java @@ -0,0 +1,172 @@ +package com.jordanbunke.stipple_effect.visual.theme.logic; + +import com.jordanbunke.delta_time.image.GameImage; +import com.jordanbunke.delta_time.image.ImageProcessing; +import com.jordanbunke.delta_time.io.ResourceLoader; +import com.jordanbunke.stipple_effect.utility.Layout; +import com.jordanbunke.stipple_effect.utility.math.ColorMath; +import com.jordanbunke.stipple_effect.utility.settings.Settings; +import com.jordanbunke.stipple_effect.visual.GraphicsUtils; +import com.jordanbunke.stipple_effect.visual.theme.SEColors; + +import java.awt.*; +import java.nio.file.Path; + +public final class NeonThemeLogic extends ThemeLogic { + private static final NeonThemeLogic INSTANCE; + + private final Path FOLDER; + + static { + INSTANCE = new NeonThemeLogic(); + } + + private NeonThemeLogic() { + FOLDER = THEMES_FOLDER.resolve("neon"); + } + + public static NeonThemeLogic get() { + return INSTANCE; + } + + @Override + public GameImage highlightedIconOverlay() { + return transformIcon(GraphicsUtils.readIconAsset("highlighted")); + } + + @Override + public GameImage selectedIconOverlay() { + return transformIcon(GraphicsUtils.readIconAsset("selected")); + } + + @Override + Color transformIconPixel(final GameImage asset, final int x, final int y) { + final Color orig = asset.getColorAt(x, y); + + if (orig.getAlpha() == 0) + return orig; + else { + final int w = asset.getWidth(), h = asset.getHeight(); + + final boolean white = orig.equals(SEColors.white()), + border = x == 0 || y == 0 || x + 1 == w || y + 1 == h; + + boolean transparentNeighbour = border; + + final int SIDES = 9; + for (int i = 0; !transparentNeighbour && i < SIDES; i++) { + final int deltaX = (i / 3) - 1, deltaY = (i % 3) - 1; + + if (deltaX == 0 && deltaY == 0) + continue; + + transparentNeighbour = isTransparent(asset, + x + deltaX, y + deltaY); + } + + if (white && (border || transparentNeighbour)) + return Settings.getTheme().highlightOutline; + else if (white) + return SEColors.green(); + + final double PURPLE_HUE = 5 / 6.0; + return ColorMath.fromHSV(PURPLE_HUE, ColorMath.rgbToSat(orig), + ColorMath.rgbToValue(orig), orig.getAlpha()); + } + } + + private boolean isTransparent( + final GameImage asset, final int x, final int y + ) { + final Color c = asset.getColorAt(x, y); + + return c.getAlpha() == 0; + } + + @Override + public void drawHorizontalSliderOutline(final GameImage vesselWithCore) { + final Color c = Settings.getTheme().buttonOutline; + + final int SECTION_PX = Layout.BUTTON_BORDER_PX, + length = SECTION_PX * 3, + w = vesselWithCore.getWidth(), + h = vesselWithCore.getHeight(); + + final int x = Layout.SLIDER_BALL_DIM / 2, rx = w - x, + y = Layout.SLIDER_THINNING + (Layout.BUTTON_BORDER_PX / 2); + + vesselWithCore.setColor(c); + + // left bound + vesselWithCore.drawLine(SECTION_PX, x, y, x, h - y); + vesselWithCore.drawLine(SECTION_PX, x, y, x + length, y); + vesselWithCore.drawLine(SECTION_PX, x, h - y, x + length, h - y); + + // right bound + vesselWithCore.drawLine(SECTION_PX, rx, y, rx, h - y); + vesselWithCore.drawLine(SECTION_PX, rx, y, rx - length, y); + vesselWithCore.drawLine(SECTION_PX, rx, h - y, rx - length, h - y); + } + + @Override + public void drawVerticalSliderOutline(final GameImage vesselWithCore) { + final Color c = Settings.getTheme().buttonOutline; + + final int SECTION_PX = Layout.BUTTON_BORDER_PX, + length = SECTION_PX * 3, + w = vesselWithCore.getWidth(), + h = vesselWithCore.getHeight(); + + final int y = Layout.SLIDER_BALL_DIM / 2, by = h - y, + x = Layout.SLIDER_THINNING + (Layout.BUTTON_BORDER_PX / 2); + + vesselWithCore.setColor(c); + + // top bound + vesselWithCore.drawLine(SECTION_PX, x, y, w - x, y); + vesselWithCore.drawLine(SECTION_PX, x, y, x, y + length); + vesselWithCore.drawLine(SECTION_PX, w - x, y, w - x, y + length); + + // bottom bound + vesselWithCore.drawLine(SECTION_PX, x, by, w - x, by); + vesselWithCore.drawLine(SECTION_PX, x, by, x, by - length); + vesselWithCore.drawLine(SECTION_PX, w - x, by, w - x, by - length); + } + + @Override + public GameImage highlightSliderBall(final GameImage baseSliderBall) { + final int w = baseSliderBall.getWidth(), + h = baseSliderBall.getHeight(); + final GameImage highlighted = new GameImage(w, h); + + highlighted.fillRectangle( + Settings.getTheme().buttonBody, 0, 0, w, h); + + return highlighted.submit(); + } + + @Override + public GameImage unclickableIcon(final GameImage baseIcon) { + return pixelWiseTransformation(baseIcon, + c -> new Color(c.getRed(), 0, 0, c.getAlpha())); + } + + @Override + public GameImage[] loadSplash() { + final int SPLASH_FRAMES = 8, SPLASH_SCALE_UP = 6; + final String SPLASH_FRAME_BASE = "splash_"; + + final GameImage[] frames = new GameImage[SPLASH_FRAMES]; + + for (int i = 0; i < SPLASH_FRAMES; i++) { + final Path framePath = FOLDER.resolve( + SPLASH_FRAME_BASE + i + ".png"); + + frames[i] = ImageProcessing.scale( + ResourceLoader.loadImageResource(framePath), + SPLASH_SCALE_UP); + } + + return frames; + } +} diff --git a/src/com/jordanbunke/stipple_effect/visual/theme/logic/RamallahThemeLogic.java b/src/com/jordanbunke/stipple_effect/visual/theme/logic/RamallahThemeLogic.java new file mode 100644 index 00000000..9ce68b7a --- /dev/null +++ b/src/com/jordanbunke/stipple_effect/visual/theme/logic/RamallahThemeLogic.java @@ -0,0 +1,81 @@ +package com.jordanbunke.stipple_effect.visual.theme.logic; + +import com.jordanbunke.delta_time.image.GameImage; +import com.jordanbunke.delta_time.image.ImageProcessing; +import com.jordanbunke.delta_time.io.ResourceLoader; + +import java.awt.*; +import java.nio.file.Path; + +public final class RamallahThemeLogic extends ThemeLogic { + private static final RamallahThemeLogic INSTANCE; + + private final Path FOLDER; + private final GameImage KEFFIYEH; + + static { + INSTANCE = new RamallahThemeLogic(); + } + + private RamallahThemeLogic() { + FOLDER = THEMES_FOLDER.resolve("ramallah"); + KEFFIYEH = ResourceLoader.loadImageResource(FOLDER.resolve("keffiyeh.png")); + } + + public static RamallahThemeLogic get() { + return INSTANCE; + } + + @Override + public GameImage highlightedIconOverlay() { + return overlayTransformation(super.highlightedIconOverlay()); + } + + @Override + public GameImage selectedIconOverlay() { + return overlayTransformation(super.selectedIconOverlay()); + } + + private GameImage overlayTransformation(final GameImage asset) { + return pixelWiseTransformation(asset, c -> c.getAlpha() == 0 + ? c : new Color(0, 0, 0, c.getAlpha())); + } + + @Override + public GameImage drawScrollBoxBackground(final int w, final int h) { + final GameImage scrollBox = new GameImage(w, h); + + final int vw = KEFFIYEH.getWidth(), vh = KEFFIYEH.getHeight(), + timesX = w / vw, timesY = h / vh; + + for (int x = 0; x <= timesX; x++) + for (int y = 0; y <= timesY; y++) + scrollBox.draw(KEFFIYEH, x * vw, y * vh); + + return scrollBox.submit(); + } + + @Override + public GameImage[] loadSplash() { + final int SPLASH_FRAMES = 10, SPLASH_SCALE_UP = 3; + final String SPLASH_FRAME_BASE = "splash_"; + + final GameImage[] frames = new GameImage[SPLASH_FRAMES]; + + for (int i = 0; i < SPLASH_FRAMES; i++) { + final Path framePath = FOLDER.resolve( + SPLASH_FRAME_BASE + i + ".png"); + + frames[i] = ImageProcessing.scale( + ResourceLoader.loadImageResource(framePath), + SPLASH_SCALE_UP); + } + + return frames; + } + + @Override + public int ticksPerSplashFrame() { + return 20; + } +} diff --git a/src/com/jordanbunke/stipple_effect/visual/theme/logic/ThemeLogic.java b/src/com/jordanbunke/stipple_effect/visual/theme/logic/ThemeLogic.java new file mode 100644 index 00000000..9543b9c2 --- /dev/null +++ b/src/com/jordanbunke/stipple_effect/visual/theme/logic/ThemeLogic.java @@ -0,0 +1,317 @@ +package com.jordanbunke.stipple_effect.visual.theme.logic; + +import com.jordanbunke.delta_time.image.GameImage; +import com.jordanbunke.delta_time.utility.math.Coord2D; +import com.jordanbunke.stipple_effect.utility.Layout; +import com.jordanbunke.stipple_effect.utility.math.ColorMath; +import com.jordanbunke.stipple_effect.utility.settings.Settings; +import com.jordanbunke.stipple_effect.visual.GraphicsUtils; +import com.jordanbunke.stipple_effect.visual.theme.SEColors; +import com.jordanbunke.stipple_effect.visual.theme.Theme; + +import java.awt.*; +import java.nio.file.Path; +import java.util.function.Function; + +public abstract class ThemeLogic { + protected static final Path THEMES_FOLDER = Path.of("themes"); + + public GameImage transformIcon(final GameImage asset) { + return contextualTransformation(asset, this::transformIconPixel); + } + + Color transformIconPixel( + final GameImage asset, final int x, final int y + ) { + return asset.getColorAt(x, y); + } + + public GameImage unclickableIcon(final GameImage baseIcon) { + return pixelWiseTransformation(baseIcon, c -> c.equals(SEColors.white()) + ? SEColors.transparent() : GraphicsUtils.greyscale(c)); + } + + public abstract GameImage[] loadSplash(); + + public int ticksPerSplashFrame() { + return 5; + } + + public GameImage drawScrollBoxBackground(final int w, final int h) { + final GameImage background = new GameImage(w, h); + background.fillRectangle( + Settings.getTheme().scrollBackground, 0, 0, w, h); + return background.submit(); + } + + public GameImage drawCheckbox( + final boolean highlighted, final boolean checked + ) { + final int w = Layout.ICON_DIMS.width(), h = Layout.ICON_DIMS.height(); + + final GameImage checkbox = new GameImage(w, h); + checkbox.fillRectangle(Settings.getTheme().buttonBody, 0, 0, w, h); + + if (checked) + checkbox.draw(GraphicsUtils.CHECKMARK); + + final Color frame = buttonBorderColor(false); + checkbox.drawRectangle(frame, 2f * Layout.BUTTON_BORDER_PX, + 0, 0, w, h); + + return highlighted ? highlightButton(checkbox.submit()) + : checkbox.submit(); + } + + public GameImage drawSliderBall( + final boolean selected, final boolean highlighted, final Color fill + ) { + final int sbd = Layout.SLIDER_BALL_DIM, + sbcd = sbd - (2 * Layout.BUTTON_BORDER_PX); + + final GameImage ball = new GameImage(sbd, sbd); + final Color border = buttonBorderColor(selected); + + ball.fillRectangle(border, 0, 0, sbd, sbd); + ball.fillRectangle(fill, Layout.BUTTON_BORDER_PX, + Layout.BUTTON_BORDER_PX, sbcd, sbcd); + + ball.free(); + + return highlighted ? highlightSliderBall(ball) : ball; + } + + public void drawHorizontalSliderOutline(final GameImage vesselWithCore) { + final int sd = vesselWithCore.getWidth() - Layout.SLIDER_BALL_DIM; + + vesselWithCore.drawRectangle(Settings.getTheme().buttonOutline, + Layout.BUTTON_BORDER_PX, Layout.SLIDER_BALL_DIM / 2, + Layout.SLIDER_THINNING + (Layout.BUTTON_BORDER_PX / 2), sd, + vesselWithCore.getHeight() - (Layout.BUTTON_BORDER_PX + + (2 * Layout.SLIDER_THINNING))); + } + + public void drawVerticalSliderOutline(final GameImage vesselWithCore) { + final int sd = vesselWithCore.getHeight() - Layout.SLIDER_BALL_DIM; + + vesselWithCore.drawRectangle(Settings.getTheme().buttonOutline, + Layout.BUTTON_BORDER_PX, + Layout.SLIDER_THINNING + (Layout.BUTTON_BORDER_PX / 2), + Layout.SLIDER_BALL_DIM / 2, + vesselWithCore.getWidth() - (Layout.BUTTON_BORDER_PX + + (2 * Layout.SLIDER_THINNING)), sd); + } + + public GameImage drawTextButton(final int width, final String text, + final boolean selected, final GraphicsUtils.ButtonType type) { + final Theme t = Settings.getTheme(); + + final Color backgroundColor = switch (type) { + case DD_OPTION -> t.dropdownOptionBody; + case STUB -> t.stubButtonBody; + default -> t.buttonBody; + }; + + final boolean leftAligned = type.isDropdown(); + final boolean drawBorder = type != GraphicsUtils.ButtonType.DD_OPTION; + + final Color textColor = intuitTextColor(backgroundColor, true); + final GameImage textImage = GraphicsUtils.uiText(textColor) + .addText(text).build().draw(); + + final int w = Math.max(width, textImage.getWidth() + + (4 * Layout.BUTTON_BORDER_PX)), + h = Layout.STD_TEXT_BUTTON_H; + + final GameImage nhi = new GameImage(w, h); + nhi.fillRectangle(backgroundColor, 0, 0, w, h); + + final int x = leftAligned + ? (2 * Layout.BUTTON_BORDER_PX) + : (w - textImage.getWidth()) / 2; + + nhi.draw(textImage, x, Layout.BUTTON_TEXT_OFFSET_Y); + + if (drawBorder) { + final Color frame = buttonBorderColor(selected); + nhi.drawRectangle(frame, 2f * Layout.BUTTON_BORDER_PX, 0, 0, w, h); + } + + return nhi.submit(); + } + + public GameImage highlightSliderBall(final GameImage baseSliderBall) { + return highlightButton(baseSliderBall); + } + + public GameImage highlightButton(final GameImage baseButton) { + final GameImage hi = new GameImage(baseButton); + final int w = hi.getWidth(), h = hi.getHeight(); + hi.fillRectangle(Settings.getTheme().highlightOverlay, 0, 0, w, h); + hi.drawRectangle(Settings.getTheme().buttonOutline, + 2f * Layout.BUTTON_BORDER_PX, 0, 0, w, h); + + return hi.submit(); + } + + public Color buttonBorderColor(final boolean selected) { + final Theme t = Settings.getTheme(); + + return selected ? t.highlightOutline : t.buttonOutline; + } + + public GameImage drawTextbox( + final int width, + final String prefix, final String text, final String suffix, + final int cursorIndex, final int selectionIndex, + final boolean highlighted, final Color accentColor, + final Color backgroundColor + ) { + final Color mainTextC = intuitTextColor(backgroundColor, true), + affixTextC = intuitTextColor(backgroundColor, false); + + final int left = Math.min(cursorIndex, selectionIndex), + right = Math.max(cursorIndex, selectionIndex); + + final boolean hasSelection = left != right, + cursorAtRight = cursorIndex == right; + + final int height = Layout.STD_TEXT_BUTTON_H, + px = Layout.BUTTON_BORDER_PX; + + final GameImage nhi = new GameImage(width, height); + nhi.fillRectangle(backgroundColor, 0, 0, width, height); + + // text and cursor + + final String preSel = text.substring(0, left), + sel = text.substring(left, right), + postSel = text.substring(right); + final GameImage prefixImage = GraphicsUtils.uiText(affixTextC) + .addText(prefix).build().draw(), + suffixImage = GraphicsUtils.uiText(affixTextC) + .addText(suffix).build().draw(), + preSelImage = GraphicsUtils.uiText(mainTextC) + .addText(preSel).build().draw(), + selImage = GraphicsUtils.uiText(mainTextC) + .addText(sel).build().draw(), + postSelImage = GraphicsUtils.uiText(mainTextC) + .addText(postSel).build().draw(); + + Coord2D textPos = new Coord2D(2 * px, Layout.BUTTON_TEXT_OFFSET_Y); + + // possible prefix + nhi.draw(prefixImage, textPos.x, textPos.y); + if (!prefix.isEmpty()) + textPos = textPos.displace(prefixImage.getWidth() + px, 0); + + // main text prior to possible selection + nhi.draw(preSelImage, textPos.x, textPos.y); + if (!preSel.isEmpty()) + textPos = textPos.displace(preSelImage.getWidth() + px, 0); + + // possible selection text + if (hasSelection) { + if (!cursorAtRight) + textPos = textPos.displace(2 * px, 0); + + nhi.draw(selImage, textPos.x, textPos.y); + nhi.fillRectangle(Settings.getTheme().highlightOverlay, + textPos.x - px, 0, selImage.getWidth() + (2 * px), height); + textPos = textPos.displace(selImage.getWidth() + px, 0); + } + + // cursor + nhi.fillRectangle(mainTextC, textPos.x - (cursorAtRight + ? 0 : selImage.getWidth() + (3 * px)), 0, px, height); + if (cursorAtRight) + textPos = textPos.displace(2 * px, 0); + + // main text following possible selection + nhi.draw(postSelImage, textPos.x, textPos.y); + if (!postSel.isEmpty()) + textPos = textPos.displace(postSelImage.getWidth() + px, 0); + + // possible suffix + nhi.draw(suffixImage, textPos.x, textPos.y); + + // border + nhi.drawRectangle(accentColor, 2f * px, + 0, 0, width, height); + + // highlighting + if (highlighted) + return highlightButton(nhi.submit()); + else + return nhi.submit(); + } + + public GameImage highlightedIconOverlay() { + return GraphicsUtils.readIconAsset("highlighted"); + } + + public GameImage selectedIconOverlay() { + return GraphicsUtils.readIconAsset("selected"); + } + + // helpers from here + public static Color intuitTextColor( + Color background, final boolean main + ) { + final Theme t = Settings.getTheme(); + + if (background.getAlpha() == 0) + background = t.panelBackground; + + final Color light = main ? t.textLight : t.affixTextLight, + dark = main ? t.textDark : t.affixTextDark; + + final double diffL = ColorMath.diff(background, light), + diffD = ColorMath.diff(background, dark); + + return diffD > diffL ? dark : light; + } + + public static GameImage hueFromColorTransformation( + final GameImage asset, final Color ref + ) { + final double hue = ColorMath.rgbToHue(ref); + + return pixelWiseTransformation(asset, c -> c.getAlpha() == 0 ? c + : ColorMath.fromHSV(hue, ColorMath.rgbToSat(c), + ColorMath.rgbToValue(c), c.getAlpha())); + } + + public static GameImage contextualTransformation( + final GameImage input, final ColorProducer f + ) { + final GameImage output = new GameImage(input); + + final int w = output.getWidth(), h = output.getHeight(); + + for (int x = 0; x < w; x++) + for (int y = 0; y < h; y++) + output.setRGB(x, y, f.apply(input, x, y).getRGB()); + + return output.submit(); + } + + public static GameImage pixelWiseTransformation( + final GameImage input, final Function f + ) { + final GameImage output = new GameImage(input); + + final int w = output.getWidth(), h = output.getHeight(); + + for (int x = 0; x < w; x++) + for (int y = 0; y < h; y++) + output.setRGB(x, y, f.apply(input.getColorAt(x, y)).getRGB()); + + return output.submit(); + } + + @FunctionalInterface + interface ColorProducer { + Color apply(final GameImage input, final int x, final int y); + } +} diff --git a/src/com/jordanbunke/stipple_effect/visual/theme/logic/ZoThemeLogic.java b/src/com/jordanbunke/stipple_effect/visual/theme/logic/ZoThemeLogic.java new file mode 100644 index 00000000..2f13ce70 --- /dev/null +++ b/src/com/jordanbunke/stipple_effect/visual/theme/logic/ZoThemeLogic.java @@ -0,0 +1,122 @@ +package com.jordanbunke.stipple_effect.visual.theme.logic; + +import com.jordanbunke.delta_time.image.GameImage; +import com.jordanbunke.delta_time.image.ImageProcessing; +import com.jordanbunke.delta_time.io.ResourceLoader; +import com.jordanbunke.stipple_effect.utility.Layout; +import com.jordanbunke.stipple_effect.utility.settings.Settings; +import com.jordanbunke.stipple_effect.visual.GraphicsUtils; +import com.jordanbunke.stipple_effect.visual.theme.Theme; + +import java.awt.*; +import java.nio.file.Path; + +public final class ZoThemeLogic extends ThemeLogic { + private static final ZoThemeLogic INSTANCE; + + private final Path FOLDER; + private final GameImage VEVE; + + static { + INSTANCE = new ZoThemeLogic(); + } + + private ZoThemeLogic() { + FOLDER = THEMES_FOLDER.resolve("zo"); + VEVE = ResourceLoader.loadImageResource(FOLDER.resolve("veve.png")); + } + + public static ZoThemeLogic get() { + return INSTANCE; + } + + @Override + public GameImage highlightedIconOverlay() { + return overlayTransformation(super.highlightedIconOverlay()); + } + + @Override + public GameImage selectedIconOverlay() { + return overlayTransformation(super.selectedIconOverlay()); + } + + private GameImage overlayTransformation(final GameImage asset) { + final Color tint = Settings.getTheme().highlightOutline; + return hueFromColorTransformation(asset, tint); + } + + @Override + public GameImage drawTextButton( + final int width, final String text, final boolean selected, + final GraphicsUtils.ButtonType type + ) { + final Theme t = Settings.getTheme(); + + final Color backgroundColor = switch (type) { + case DD_OPTION -> t.dropdownOptionBody; + case STUB -> t.stubButtonBody; + default -> t.buttonBody; + }; + + final boolean leftAligned = type.isDropdown(); + final boolean drawBorder = type != GraphicsUtils.ButtonType.DD_OPTION; + + final Color textColor = selected ? t.highlightOutline + : intuitTextColor(backgroundColor, true); + final GameImage textImage = GraphicsUtils.uiText(textColor) + .addText(text).build().draw(); + + final int w = Math.max(width, textImage.getWidth() + + (4 * Layout.BUTTON_BORDER_PX)), + h = Layout.STD_TEXT_BUTTON_H; + + final GameImage nhi = new GameImage(w, h); + nhi.fillRectangle(backgroundColor, 0, 0, w, h); + + final int x = leftAligned + ? (2 * Layout.BUTTON_BORDER_PX) + : (w - textImage.getWidth()) / 2; + + nhi.draw(textImage, x, Layout.BUTTON_TEXT_OFFSET_Y); + + if (drawBorder) { + final Color frame = buttonBorderColor(selected); + nhi.drawRectangle(frame, 2f * Layout.BUTTON_BORDER_PX, 0, 0, w, h); + } + + return nhi.submit(); + } + + @Override + public GameImage drawScrollBoxBackground(final int w, final int h) { + final GameImage scrollBox = new GameImage(w, h); + + final int vw = VEVE.getWidth(), vh = VEVE.getHeight(), + timesX = w / vw, timesY = h / vh; + + for (int x = 0; x <= timesX; x++) + for (int y = 0; y <= timesY; y++) + scrollBox.draw(VEVE, x * vw, y * vh); + + return scrollBox.submit(); + } + + @Override + public GameImage[] loadSplash() { + final int SPLASH_FRAMES = 12, SPLASH_SCALE_UP = 5; + final String SPLASH_FRAME_BASE = "splash_"; + + final GameImage[] frames = new GameImage[SPLASH_FRAMES]; + + for (int i = 0; i < SPLASH_FRAMES; i++) { + final Path framePath = FOLDER.resolve( + SPLASH_FRAME_BASE + i + ".png"); + + frames[i] = ImageProcessing.scale( + ResourceLoader.loadImageResource(framePath), + SPLASH_SCALE_UP); + } + + return frames; + } +}