From 82ea3133f39f8ea155e13b122034131e33daf8ae Mon Sep 17 00:00:00 2001 From: Matt DeFano Date: Sat, 9 Feb 2019 08:12:53 -0800 Subject: [PATCH] Release/0.4.0 (#37) * Feature/dft refactor (#36) * Initial refactoring * Removed used PaintTool class * Layer rendering * Improvements * Fixed imagelayer render algorithm * Bug fixes * Improvements * Added GraphicsContext * Checkpoint * Improved tool builder; package refactoring * Reverted introduction of delegate pattern * Added configurable properties to ants * Improvements to filltransform * Refactored builder inner classes * Eliminated surfaceInteractionObserver template method * Added AirbrushToolTest * Added FillToolTest * Airbrush tool behavior improvements * Enabling JUnit5 tests * Fix for headless test failure * Fixing Optional issues * Enabled Jacoco * README and transfer handler improvements * Readme fixes * More README updates * Added tests * ImageLayerTest * Completed test for ImageLayer * Improved test method names * Refactored structure of tools * Added BasicToolTest * Added BoundsToolTest * Improving BoundsTool test coverage * Improved parameter name in SurfaceInteractionObserver * Added LinearTool test * Fixing Sonar bugs * Enabling Sonar branches * Added PathToolTest * #35: Fixed polyline issue * Readme updates and refactoring * Deleted tester * Javadoc fixes --- .gitignore | 3 +- .travis.yml | 7 +- README.md | 170 +- pom.xml | 71 +- .../algo/fill/DefaultBoundaryFunction.java | 20 - .../jmonet/algo/fill/DefaultFillFunction.java | 27 - .../defano/jmonet/algo/fill/FillFunction.java | 20 - .../jmonet/canvas/AbstractPaintCanvas.java | 26 +- .../defano/jmonet/canvas/JMonetCanvas.java | 71 +- .../com/defano/jmonet/canvas/PaintCanvas.java | 1 + .../com/defano/jmonet/canvas/Scratch.java | 165 +- .../com/defano/jmonet/canvas/Undoable.java | 57 + .../jmonet/canvas/layer/ImageLayer.java | 84 +- .../jmonet/canvas/layer/ImageLayerSet.java | 1 + .../jmonet/canvas/layer/LayeredImage.java | 13 +- .../canvas/observable/ObservableSurface.java | 2 + .../SurfaceInteractionObserver.java | 63 +- .../jmonet/canvas/paint/PaintFactory.java | 1 + .../canvas/surface/AbstractPaintSurface.java | 51 +- .../DefaultSurfaceScrollController.java | 6 +- .../jmonet/canvas/surface/GridSurface.java | 10 +- .../jmonet/canvas/surface/PaintSurface.java | 19 +- .../canvas/surface/ScalableSurface.java | 14 + .../canvas/surface/ScanlineSurface.java | 22 +- .../jmonet/canvas/surface/SwingSurface.java | 16 + .../CanvasClipboardActionListener.java | 9 +- .../jmonet/clipboard/CanvasFocusDelegate.java | 8 +- .../clipboard/CanvasTransferDelegate.java | 10 +- .../clipboard/CanvasTransferHandler.java | 40 +- .../clipboard/DefaultCanvasFocusDelegate.java | 24 + .../jmonet/clipboard/TransferableImage.java | 50 + .../jmonet/context/AwtGraphicsContext.java | 692 +++++++ .../jmonet/context/GraphicsContext.java | 1767 +++++++++++++++++ .../jmonet/model/FixedQuadrilateral.java | 1 + .../jmonet/model/FlexQuadrilateral.java | 27 +- .../defano/jmonet/model/PaintToolType.java | 20 +- .../com/defano/jmonet/tools/AirbrushTool.java | 38 +- .../com/defano/jmonet/tools/ArrowTool.java | 16 +- .../com/defano/jmonet/tools/CurveTool.java | 27 +- .../com/defano/jmonet/tools/EraserTool.java | 23 +- .../com/defano/jmonet/tools/FillTool.java | 110 +- .../jmonet/tools/FreeformShapeTool.java | 32 +- .../com/defano/jmonet/tools/LassoTool.java | 27 +- .../com/defano/jmonet/tools/LineTool.java | 17 +- .../defano/jmonet/tools/MagnifierTool.java | 128 +- .../{SelectionTool.java => MarqueeTool.java} | 32 +- .../com/defano/jmonet/tools/OvalTool.java | 21 +- .../defano/jmonet/tools/PaintbrushTool.java | 29 +- .../com/defano/jmonet/tools/PencilTool.java | 51 +- .../defano/jmonet/tools/PerspectiveTool.java | 22 +- .../com/defano/jmonet/tools/PolygonTool.java | 29 +- .../defano/jmonet/tools/ProjectionTool.java | 22 +- .../defano/jmonet/tools/RectangleTool.java | 25 +- .../com/defano/jmonet/tools/RotateTool.java | 45 +- .../jmonet/tools/RoundRectangleTool.java | 26 +- .../defano/jmonet/tools/RubberSheetTool.java | 22 +- .../com/defano/jmonet/tools/ScaleTool.java | 32 +- .../com/defano/jmonet/tools/ShapeTool.java | 33 +- .../com/defano/jmonet/tools/SlantTool.java | 26 +- .../com/defano/jmonet/tools/TextTool.java | 173 +- .../attributes}/BoundaryFunction.java | 9 +- .../jmonet/tools/attributes/FillFunction.java | 29 + .../tools/attributes/MarkPredicate.java | 27 + .../attributes/ObservableToolAttributes.java | 64 + .../tools/attributes/RxToolAttributes.java | 300 +++ .../tools/attributes/ToolAttributes.java | 224 +++ .../jmonet/tools/base/AbstractBoundsTool.java | 99 - .../jmonet/tools/base/AbstractLineTool.java | 69 - .../jmonet/tools/base/AbstractPathTool.java | 84 - .../tools/base/AbstractPolylineTool.java | 151 -- .../defano/jmonet/tools/base/BasicTool.java | 127 ++ .../defano/jmonet/tools/base/BoundsTool.java | 94 + .../jmonet/tools/base/BoundsToolDelegate.java | 35 + .../defano/jmonet/tools/base/LinearTool.java | 65 + .../jmonet/tools/base/LinearToolDelegate.java | 25 + .../defano/jmonet/tools/base/PathTool.java | 63 + .../jmonet/tools/base/PathToolDelegate.java | 43 + .../jmonet/tools/base/PolylineTool.java | 123 ++ .../tools/base/PolylineToolDelegate.java | 44 + ...tSelectionTool.java => SelectionTool.java} | 132 +- .../tools/base/SelectionToolDelegate.java | 55 + .../tools/base/StrokedCursorPathTool.java | 16 +- .../com/defano/jmonet/tools/base/Tool.java | 85 + ...tTransformTool.java => TransformTool.java} | 102 +- .../tools/base/TransformToolDelegate.java | 55 + .../jmonet/tools/brushes/ShapeStroke.java | 3 +- .../jmonet/tools/brushes/StampStroke.java | 7 +- .../tools/builder/BasicStrokeBuilder.java | 150 ++ .../jmonet/tools/builder/PaintTool.java | 378 ---- .../tools/builder/PaintToolBuilder.java | 368 +++- .../tools/builder/ShapeStrokeBuilder.java | 291 +++ .../jmonet/tools/builder/StrokeBuilder.java | 422 ---- .../{util => cursors}/CursorFactory.java | 13 +- .../jmonet/tools/cursors/CursorManager.java | 29 + .../tools/cursors/SwingCursorManager.java | 30 + .../tools/selection/MutableSelection.java | 4 +- .../jmonet/tools/selection/Selection.java | 8 +- .../TransformableCanvasSelection.java | 4 +- .../TransformableImageSelection.java | 17 +- .../selection/TransformableSelection.java | 14 +- .../defano/jmonet/tools/util/ImageUtils.java | 5 + .../jmonet/tools/util/MarchingAnts.java | 129 +- .../tools/util/MarchingAntsObserver.java | 4 +- .../util/{Geometry.java => MathUtils.java} | 33 +- .../affine/FlipHorizontalTransform.java | 2 +- .../affine/FlipVerticalTransform.java | 2 +- .../transform/affine/RotateLeftTransform.java | 2 +- .../affine/RotateRightTransform.java | 2 +- .../dither/AbstractDitherer.java | 4 +- .../dither/AtkinsonDitherer.java | 2 +- .../dither/BurkesDitherer.java | 2 +- .../{algo => transform}/dither/Ditherer.java | 4 +- .../dither/FloydSteinbergDitherer.java | 2 +- .../dither/JarvisJudiceNinkeDitherer.java | 2 +- .../dither/NullDitherer.java | 2 +- .../dither/SierraDitherer.java | 2 +- .../dither/SierraLiteDitherer.java | 2 +- .../dither/SierraTwoDitherer.java | 2 +- .../dither/StuckiDitherer.java | 2 +- .../dither/quant/ColorReductionQuantizer.java | 2 +- .../dither/quant/GrayscaleQuantizer.java | 2 +- .../dither/quant/MonochromaticQuantizer.java | 2 +- .../dither/quant/QuantizationFunction.java | 2 +- .../transform/image/ApplyAffineTransform.java | 2 +- .../transform/image/ApplyPixelTransform.java | 4 +- .../image/BufferedImageOpTransform.java | 2 +- .../image/ColorReductionTransform.java | 10 +- .../transform/image/ConvolutionTransform.java | 2 +- .../transform/image/FillTransform.java | 46 +- .../transform/image/FloodFillTransform.java | 70 +- .../image/GreyscaleReductionTransform.java | 10 +- .../transform/image/ImageTransform.java | 2 +- .../transform/image/PixelTransform.java | 6 +- .../transform/image/ProjectionTransform.java | 80 +- .../transform/image/RubbersheetTransform.java | 80 +- .../transform/image/ScaleTransform.java | 2 +- .../transform/image/SlantTransform.java | 2 +- .../transform/image/StaticImageTransform.java | 2 +- .../image/StaticImageTransformable.java | 2 +- .../transform/image/Transformable.java | 16 +- .../pixel/BrightnessPixelTransform.java | 6 +- .../transform/pixel/InvertPixelTransform.java | 6 +- .../pixel/RemoveAlphaPixelTransform.java | 6 +- .../pixel/TransparencyPixelTransform.java | 8 +- .../jmonet/canvas/layer/ImageLayerTest.java | 245 +++ .../defano/jmonet/tools/AirbrushToolTest.java | 46 + .../defano/jmonet/tools/ArrowToolTest.java | 24 + .../defano/jmonet/tools/EraserToolTest.java | 46 + .../com/defano/jmonet/tools/FillToolTest.java | 59 + .../com/defano/jmonet/tools/LineToolTest.java | 37 + .../defano/jmonet/tools/PencilToolTest.java | 97 + .../jmonet/tools/RectangleToolTest.java | 50 + .../jmonet/tools/base/BaseToolTest.java | 51 + .../jmonet/tools/base/BasicToolTest.java | 99 + .../jmonet/tools/base/BoundsToolTest.java | 171 ++ .../jmonet/tools/base/ColorMatcher.java | 41 + .../jmonet/tools/base/CursorMatcher.java | 26 + .../jmonet/tools/base/LinearToolTest.java | 99 + .../defano/jmonet/tools/base/MockitoTest.java | 12 + .../jmonet/tools/base/MockitoToolTest.java | 75 + .../jmonet/tools/base/PathToolTest.java | 76 + .../jmonet/tools/base/PolylineToolTest.java | 102 + .../jmonet/tools/base/ShapeMatcher.java | 73 + .../jmonet/tools/util/MathUtilsTest.java | 28 + .../pixel/BrightnessPixelTransformTest.java | 45 + .../pixel/InvertPixelTransformTest.java | 23 + .../pixel/RemoveAlphaPixelTransformTest.java | 43 + 167 files changed, 8118 insertions(+), 2479 deletions(-) delete mode 100644 src/main/java/com/defano/jmonet/algo/fill/DefaultBoundaryFunction.java delete mode 100644 src/main/java/com/defano/jmonet/algo/fill/DefaultFillFunction.java delete mode 100644 src/main/java/com/defano/jmonet/algo/fill/FillFunction.java create mode 100644 src/main/java/com/defano/jmonet/canvas/Undoable.java create mode 100644 src/main/java/com/defano/jmonet/clipboard/DefaultCanvasFocusDelegate.java create mode 100644 src/main/java/com/defano/jmonet/clipboard/TransferableImage.java create mode 100644 src/main/java/com/defano/jmonet/context/AwtGraphicsContext.java create mode 100644 src/main/java/com/defano/jmonet/context/GraphicsContext.java rename src/main/java/com/defano/jmonet/tools/{SelectionTool.java => MarqueeTool.java} (52%) rename src/main/java/com/defano/jmonet/{algo/fill => tools/attributes}/BoundaryFunction.java (82%) create mode 100644 src/main/java/com/defano/jmonet/tools/attributes/FillFunction.java create mode 100644 src/main/java/com/defano/jmonet/tools/attributes/MarkPredicate.java create mode 100644 src/main/java/com/defano/jmonet/tools/attributes/ObservableToolAttributes.java create mode 100644 src/main/java/com/defano/jmonet/tools/attributes/RxToolAttributes.java create mode 100644 src/main/java/com/defano/jmonet/tools/attributes/ToolAttributes.java delete mode 100644 src/main/java/com/defano/jmonet/tools/base/AbstractBoundsTool.java delete mode 100644 src/main/java/com/defano/jmonet/tools/base/AbstractLineTool.java delete mode 100644 src/main/java/com/defano/jmonet/tools/base/AbstractPathTool.java delete mode 100644 src/main/java/com/defano/jmonet/tools/base/AbstractPolylineTool.java create mode 100644 src/main/java/com/defano/jmonet/tools/base/BasicTool.java create mode 100644 src/main/java/com/defano/jmonet/tools/base/BoundsTool.java create mode 100644 src/main/java/com/defano/jmonet/tools/base/BoundsToolDelegate.java create mode 100644 src/main/java/com/defano/jmonet/tools/base/LinearTool.java create mode 100644 src/main/java/com/defano/jmonet/tools/base/LinearToolDelegate.java create mode 100644 src/main/java/com/defano/jmonet/tools/base/PathTool.java create mode 100644 src/main/java/com/defano/jmonet/tools/base/PathToolDelegate.java create mode 100644 src/main/java/com/defano/jmonet/tools/base/PolylineTool.java create mode 100644 src/main/java/com/defano/jmonet/tools/base/PolylineToolDelegate.java rename src/main/java/com/defano/jmonet/tools/base/{AbstractSelectionTool.java => SelectionTool.java} (79%) create mode 100644 src/main/java/com/defano/jmonet/tools/base/SelectionToolDelegate.java create mode 100644 src/main/java/com/defano/jmonet/tools/base/Tool.java rename src/main/java/com/defano/jmonet/tools/base/{AbstractTransformTool.java => TransformTool.java} (57%) create mode 100644 src/main/java/com/defano/jmonet/tools/base/TransformToolDelegate.java create mode 100644 src/main/java/com/defano/jmonet/tools/builder/BasicStrokeBuilder.java delete mode 100644 src/main/java/com/defano/jmonet/tools/builder/PaintTool.java create mode 100644 src/main/java/com/defano/jmonet/tools/builder/ShapeStrokeBuilder.java rename src/main/java/com/defano/jmonet/tools/{util => cursors}/CursorFactory.java (91%) create mode 100644 src/main/java/com/defano/jmonet/tools/cursors/CursorManager.java create mode 100644 src/main/java/com/defano/jmonet/tools/cursors/SwingCursorManager.java rename src/main/java/com/defano/jmonet/tools/util/{Geometry.java => MathUtils.java} (92%) rename src/main/java/com/defano/jmonet/{algo => }/transform/affine/FlipHorizontalTransform.java (90%) rename src/main/java/com/defano/jmonet/{algo => }/transform/affine/FlipVerticalTransform.java (90%) rename src/main/java/com/defano/jmonet/{algo => }/transform/affine/RotateLeftTransform.java (92%) rename src/main/java/com/defano/jmonet/{algo => }/transform/affine/RotateRightTransform.java (92%) rename src/main/java/com/defano/jmonet/{algo => transform}/dither/AbstractDitherer.java (98%) rename src/main/java/com/defano/jmonet/{algo => transform}/dither/AtkinsonDitherer.java (94%) rename src/main/java/com/defano/jmonet/{algo => transform}/dither/BurkesDitherer.java (94%) rename src/main/java/com/defano/jmonet/{algo => transform}/dither/Ditherer.java (86%) rename src/main/java/com/defano/jmonet/{algo => transform}/dither/FloydSteinbergDitherer.java (93%) rename src/main/java/com/defano/jmonet/{algo => transform}/dither/JarvisJudiceNinkeDitherer.java (96%) rename src/main/java/com/defano/jmonet/{algo => transform}/dither/NullDitherer.java (86%) rename src/main/java/com/defano/jmonet/{algo => transform}/dither/SierraDitherer.java (95%) rename src/main/java/com/defano/jmonet/{algo => transform}/dither/SierraLiteDitherer.java (92%) rename src/main/java/com/defano/jmonet/{algo => transform}/dither/SierraTwoDitherer.java (94%) rename src/main/java/com/defano/jmonet/{algo => transform}/dither/StuckiDitherer.java (96%) rename src/main/java/com/defano/jmonet/{algo => transform}/dither/quant/ColorReductionQuantizer.java (97%) rename src/main/java/com/defano/jmonet/{algo => transform}/dither/quant/GrayscaleQuantizer.java (96%) rename src/main/java/com/defano/jmonet/{algo => transform}/dither/quant/MonochromaticQuantizer.java (93%) rename src/main/java/com/defano/jmonet/{algo => transform}/dither/quant/QuantizationFunction.java (94%) rename src/main/java/com/defano/jmonet/{algo => }/transform/image/ApplyAffineTransform.java (97%) rename src/main/java/com/defano/jmonet/{algo => }/transform/image/ApplyPixelTransform.java (91%) rename src/main/java/com/defano/jmonet/{algo => }/transform/image/BufferedImageOpTransform.java (93%) rename src/main/java/com/defano/jmonet/{algo => }/transform/image/ColorReductionTransform.java (84%) rename src/main/java/com/defano/jmonet/{algo => }/transform/image/ConvolutionTransform.java (94%) rename src/main/java/com/defano/jmonet/{algo => }/transform/image/FillTransform.java (55%) rename src/main/java/com/defano/jmonet/{algo => }/transform/image/FloodFillTransform.java (62%) rename src/main/java/com/defano/jmonet/{algo => }/transform/image/GreyscaleReductionTransform.java (78%) rename src/main/java/com/defano/jmonet/{algo => }/transform/image/ImageTransform.java (94%) rename src/main/java/com/defano/jmonet/{algo => }/transform/image/PixelTransform.java (60%) rename src/main/java/com/defano/jmonet/{algo => }/transform/image/ProjectionTransform.java (50%) rename src/main/java/com/defano/jmonet/{algo => }/transform/image/RubbersheetTransform.java (50%) rename src/main/java/com/defano/jmonet/{algo => }/transform/image/ScaleTransform.java (94%) rename src/main/java/com/defano/jmonet/{algo => }/transform/image/SlantTransform.java (96%) rename src/main/java/com/defano/jmonet/{algo => }/transform/image/StaticImageTransform.java (94%) rename src/main/java/com/defano/jmonet/{algo => }/transform/image/StaticImageTransformable.java (93%) rename src/main/java/com/defano/jmonet/{algo => }/transform/image/Transformable.java (91%) rename src/main/java/com/defano/jmonet/{algo => }/transform/pixel/BrightnessPixelTransform.java (87%) rename src/main/java/com/defano/jmonet/{algo => }/transform/pixel/InvertPixelTransform.java (68%) rename src/main/java/com/defano/jmonet/{algo => }/transform/pixel/RemoveAlphaPixelTransform.java (87%) rename src/main/java/com/defano/jmonet/{algo => }/transform/pixel/TransparencyPixelTransform.java (79%) create mode 100644 src/test/java/com/defano/jmonet/canvas/layer/ImageLayerTest.java create mode 100644 src/test/java/com/defano/jmonet/tools/AirbrushToolTest.java create mode 100644 src/test/java/com/defano/jmonet/tools/ArrowToolTest.java create mode 100644 src/test/java/com/defano/jmonet/tools/EraserToolTest.java create mode 100644 src/test/java/com/defano/jmonet/tools/FillToolTest.java create mode 100644 src/test/java/com/defano/jmonet/tools/LineToolTest.java create mode 100644 src/test/java/com/defano/jmonet/tools/PencilToolTest.java create mode 100644 src/test/java/com/defano/jmonet/tools/RectangleToolTest.java create mode 100644 src/test/java/com/defano/jmonet/tools/base/BaseToolTest.java create mode 100644 src/test/java/com/defano/jmonet/tools/base/BasicToolTest.java create mode 100644 src/test/java/com/defano/jmonet/tools/base/BoundsToolTest.java create mode 100644 src/test/java/com/defano/jmonet/tools/base/ColorMatcher.java create mode 100644 src/test/java/com/defano/jmonet/tools/base/CursorMatcher.java create mode 100644 src/test/java/com/defano/jmonet/tools/base/LinearToolTest.java create mode 100644 src/test/java/com/defano/jmonet/tools/base/MockitoTest.java create mode 100644 src/test/java/com/defano/jmonet/tools/base/MockitoToolTest.java create mode 100644 src/test/java/com/defano/jmonet/tools/base/PathToolTest.java create mode 100644 src/test/java/com/defano/jmonet/tools/base/PolylineToolTest.java create mode 100644 src/test/java/com/defano/jmonet/tools/base/ShapeMatcher.java create mode 100644 src/test/java/com/defano/jmonet/tools/util/MathUtilsTest.java create mode 100644 src/test/java/com/defano/jmonet/transform/pixel/BrightnessPixelTransformTest.java create mode 100644 src/test/java/com/defano/jmonet/transform/pixel/InvertPixelTransformTest.java create mode 100644 src/test/java/com/defano/jmonet/transform/pixel/RemoveAlphaPixelTransformTest.java diff --git a/.gitignore b/.gitignore index ea5ff732..022fcdc6 100644 --- a/.gitignore +++ b/.gitignore @@ -9,4 +9,5 @@ *.iml target/* *.bin -.gradle/* \ No newline at end of file +.gradle/* +/src/main/java/com/defano/jmonet/Tester.java diff --git a/.travis.yml b/.travis.yml index 83305c9a..0cb2382a 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,4 +1,9 @@ language: java + +before_install: + - "export DISPLAY=:99.0" + - "sh -e /etc/init.d/xvfb start" + jdk: - oraclejdk8 @@ -13,4 +18,4 @@ script: mvn test javadoc:javadoc after_success: - chmod +x .travis/push-javadocs.sh - .travis/push-javadocs.sh -- mvn sonar:sonar -Dsonar.organization=defano-github -Dsonar.host.url=https://sonarcloud.io -Dsonar.login=$SONAR_KEY +- mvn sonar:sonar -Dsonar.organization=defano-github -Dsonar.host.url=https://sonarcloud.io -Dsonar.login=$SONAR_KEY -Dsonar.branch.name=$TRAVIS_BRANCH diff --git a/README.md b/README.md index 7d333f48..c7f58262 100644 --- a/README.md +++ b/README.md @@ -1,22 +1,22 @@ # JMonet -[Getting Started](#getting-started) | [Tools](#paint-tools) | [Transforms](#image-transforms) | [Brushes](#creating-complex-brush-shapes) | [Cut, Copy and Paste](#cut-copy-and-paste) | [FAQs](#frequently-asked-questions) +[Getting Started](#getting-started) | [Tools](#paint-tools) | [Transforms](#image-transforms) | [Brushes](#creating-complex-brush-shapes) | [Cut, Copy and Paste](#cut-copy-and-paste) | [Observables](#observable-attributes-with-rxjava) | [FAQs](#frequently-asked-questions) -An easy-to-use toolkit for incorporating paint tools like those found in [MacPaint](https://en.wikipedia.org/wiki/MacPaint) or [Microsoft Paint](https://en.wikipedia.org/wiki/Microsoft_Paint) into a Java Swing or JavaFX application (does not support Android). +An easy-to-use toolkit for incorporating paint tools like those found in [MacPaint](https://en.wikipedia.org/wiki/MacPaint) or [Microsoft Paint](https://en.wikipedia.org/wiki/Microsoft_Paint) into a Java Swing or JavaFX application. Sorry, JMonet is not compatible with Android. This project provides the paint capabilities found in [WyldCard](https://github.com/defano/wyldcard) (an open-sourced clone of Apple's HyperCard). -[![Build Status](https://travis-ci.org/defano/jmonet.svg?branch=master)](https://travis-ci.org/defano/jmonet) +[![Build Status](https://travis-ci.org/defano/jmonet.svg?branch=master)](https://travis-ci.org/defano/jmonet) [![Sonar Status](https://sonarcloud.io/api/project_badges/measure?project=com.defano.jmonet%3Ajmonet&metric=alert_status)](https://sonarcloud.io/dashboard?id=com.defano.jmonet%3Ajmonet) ## Features -* Common suite of paint tools with observable attributes for colors and patterns, line sizes, anti-aliasing modes, etc. -* Includes image transform tools like scale, rotate, flip, shear, perspective and projection, plus the ability to adjust color depth, transparency and brightness. -* Canvas can be scaled and displayed within a scrollable pane; tools can be snapped to a grid. -* Supports multi-operation undo and redo, plus cut, copy and paste integration with the system clipboard. +* Familiar suite of paint tools with powerful, RxJava-observable attributes for brush strokes, colors, patterns, line sizes, etc. +* Built-in image transform tools like scale, rotate, flip, shear, perspective and projection. Easy to incorporate third-party image filters (like emboss or trace edges) implemented as a `Kernel` or `BufferedImageOp`. +* Paint canvas can be scaled and displayed within a scrollable pane; tools can be snapped to a grid. +* Multi-operation undo and redo, with easy-to-implement cut, copy and paste integration. * Paint and edit 24-bit, true-color images with alpha transparency; images are backed by a standard Java `BufferedImage` object making it easy to import and export graphics. -* Lightweight toolkit integrates easily into Swing and JavaFX applications and utilizes [ReactiveX](https://github.com/ReactiveX/RxJava) for observables. +* Lightweight toolkit published to Maven Central integrates easily into Swing and JavaFX applications. ## Paint Tools @@ -66,13 +66,13 @@ Icon | Tool | Description #### 1. Install the library -JMonet is published to Maven Central; include the library in your Maven project's POM, like: +JMonet is published to Maven Central. Simply include the library in your Maven project's POM, like: ``` com.defano.jmonet jmonet - 0.3.3 + 0.4.0 ``` @@ -80,17 +80,17 @@ JMonet is published to Maven Central; include the library in your Maven project' ``` repositories { - mavenCentral() + mavenCentral() } dependencies { - compile 'com.defano.jmonet:jmonet:0.3.3' + compile 'com.defano.jmonet:jmonet:0.4.0' } ``` #### 2. Create a canvas -JMonet integrates easily into Java Swing and JavaFX applications. Simply create a canvas node or panel and add it to a window in your application: +JMonet integrates easily into Java Swing and JavaFX applications. Create a canvas node or panel and add it to a window in your application: In Swing applications: @@ -172,15 +172,13 @@ Transform Method | Description `rotateLeft` | Rotates the selection 90 degrees counter-clockwise. `rotateRight` | Rotates the selection 90 degrees clockwise. -Each of these transforms is implemented as a standalone class in the `com.defano.jmonet.algo.transform` package hierarchy, making it easy to apply these programmatically (offline) to a `BufferedImage` object. +Each of these transforms is implemented as a standalone class in the `com.defano.jmonet.transform` package hierarchy, making it easy to apply these programmatically (offline) to a `BufferedImage` object. ## Creating complex brush shapes -A stroke represents the size and shape of the "pen" used to mark the outline of a shape or path. +A `Stroke` represents the size and shape of the "pen" used to draw the outline of a shape or path. -JMonet's `ShapeStroke` class can produce a stroke of any arbitrary shape (even the shape of text). A builder class (`StrokeBuilder`) provides a convenient mechanism for creating both `BasicStroke` or `ShapeStroke` objects. Note that `StrokeBuilder` replaces the `BasicBrush` enumeration present in older versions of the library. - -To produce a stroke in the shape of a vertical line, 20 pixels tall: +JMonet's `StrokeBuilder` class can generate complex brush strokes of any arbitrary shape (even the shape of text). For example, to produce a stroke in the shape of a vertical line, 20 pixels tall: ``` StrokeBuilder.withShape() @@ -204,7 +202,7 @@ StrokeBuilder.withShape() .ofCircle(48) // draw circle .outlined(6) // ... outlined with 6px border, not filled .ofRectangle(6, 60) // draw slash (6px wide, 60px long) - .rotated(-45) // ... rotate it 45 degrees + .rotated(-45) // ... rotate it -45 degrees .build(); ``` @@ -219,46 +217,28 @@ StrokeBuilder.withBasicStroke() ## Cut, Copy and Paste -JMonet makes it easy to integrate cut, copy and paste functions into your app that utilize the operating system's clipboard so that you can copy and paste graphics from within your own application or between other applications on your system. +JMonet makes it easy to integrate cut, copy and paste functions into an application that utilizes the operating system's clipboard. With this, you can copy and paste graphics from within your own application or between it and other applications. A bit of integration is required to connect these functions to the UI elements in your app (like menu items or toolbar buttons) that a user will interact with. -#### 1. Route actions to the canvas - -The JMonet canvas will not receive cut, copy or paste actions until we register an `ActionListener` that routes those actions to it. Typically, only the user interface element that has focus receives such events, but because a canvas has no clear concept of focus, it's up to you to decide when the canvas should respond to cut, copy and paste actions. - -Create an `ActionListener` to send actions to the canvas: - -``` -CanvasClipboardActionListener myActionListener = new CanvasClipboardActionListener(new CanvasFocusDelegate() { - @Override - public AbstractPaintCanvas getCanvasInFocus() { - - // Should our canvas handle cut, copy and paste commands right now? - if (isMyCanvasInFocus()) { - return myCanvas; - } +#### 1. Create an `ActionListener` to route cut-copy-paste actions to the canvas - // If not, return null - return null; - } -}); -``` +Consistent with Swing's `ActionListener` pattern, the JMonet canvas will not receive cut, copy or paste actions until we register a listener class that routes clipboard actions to it. The `CanvasClipboardActionListener` class will attempt to route actions to whichever canvas currently in focus. If you dislike this behavior you may pass an implementation of `CanvasFocusDelegate` into its constructor, or simply write your own `ActionListener` instead. -Then, add this `ActionListener` to whichever user interface elements will generate cut, copy and paste events. Most commonly this would be added to menu items but could also be used with buttons on a toolbar, for example: +Add your `CanvasClipboardActionListener` to whichever user interface elements will generate cut, copy and paste events. Most commonly, this would include menu items in the menu bar, but could also be used with buttons on a toolbar. For example: ``` JMenuItem myCopyMenuItem = new JMenuItem(new DefaultEditorKit.CopyAction()); copyMenuItem.setName("Copy"); copyMenuItem.setActionCommand((String) TransferHandler.getCopyAction().getValue(Action.NAME)) -copyMenuItem.addActionListener(myActionListener); +copyMenuItem.addActionListener(new CanvasClipboardActionListener()); myEditMenu.add(myCopyMenuItem); ``` #### 2. Add the transfer handler -Your application needs to tell JMonet what to do when the user has invoked the cut, copy or paste action. Typically, this simply involves getting or setting the selection defined by a selection tool (i.e., any tool which subclasses `AbstractSelectionTool`). Of course, you're free to provide alternate behavior (like copying the entire canvas, instead of just the selection). +Next, your application needs to tell JMonet what it should do when it receives a cut, copy or paste action. This is accomplished by setting a `TransferHandler` on the `JMonetCanvas`. The JMonet library comes with a utility class, `CanvasTransferHandler`, that eliminates much of the boilerplate typically associated with integrating cut, copy and paste. The code below provides an implementation that cuts, copies and pastes the active selection. @@ -268,8 +248,8 @@ The code below provides an implementation that cuts, copies and pastes the activ myCanvas.setTransferHandler(new CanvasTransferHandler(myCanvas, new CanvasTransferDelegate() { @Override public BufferedImage copySelection() { - if (myActiveTool instanceof AbstractSelectionTool) { - return ((AbstractSelectionTool) myActiveTool).getSelectedImage(); + if (myActiveTool instanceof SelectionTool) { + return ((SelectionTool) myActiveTool).getSelectedImage(); } // Nothing available to copy if active tool isn't a selection tool @@ -279,8 +259,8 @@ The code below provides an implementation that cuts, copies and pastes the activ @Override public void deleteSelection() { // Nothing to do if active tool isn't a selection tool - if (activeTool instanceof AbstractSelectionTool) { - ((AbstractSelectionTool) activeTool).deleteSelection(); + if (activeTool instanceof SelectionTool) { + ((SelectionTool) activeTool).deleteSelection(); } } @@ -299,13 +279,51 @@ The code below provides an implementation that cuts, copies and pastes the activ ``` -## Frequently asked questions +## Observable attributes with RxJava + +JMonet uses RxJava to provide observable attributes. This makes it easy to keep application menus, tool bars and palettes in sync with the state of your paint tools. As one control modifies an attribute, other controls (and the tool itself) will be notified of the change. + +Lets imagine we have a `JCheckBoxMenuItem` in our menu bar and a `JCheckBox` button on a tool palette, both of which can be used to enable or disable the draw centered paint tool attribute. Here's how to use RxJava to achieve that: + +#### 1. Create an observable object + +Since a single menu item often controls an attribute for all tools, you'll probably want to model this as a Singleton that's easily accessible from different areas of your program. + +``` +// BehaviorSubject is a simple kind of Observable, see JavaRx documentation for details +BehaviorSubject drawCenteredObservable = BehaviorSubject.createDefault(true); +``` + +#### 2. Wire the `Observable` to the menu item and checkbox button + +``` +JCheckBoxMenuItem menuItem = new JCheckBoxMenuItem(); +JCheckBox checkbox = new JCheckBox(); + +// onNext() sets the value seen by other observers +menuItem.addActionListener(e -> drawCenteredObservable.onNext(menuItem.isSelected())); +checkbox.addActionListener(a -> drawCenteredObservable.onNext(checkbox.isSelected()); + +// The subscribe() lambda fires each time the observed value changes +drawCenteredObservable.subscribe(drawCentered -> menuItem.setSelected(drawCentered)); +drawCenteredObservable.subscribe(drawCentered -> checkbox.setSelected(drawCentered)); +``` + +Note that the `.subscribe()` method returns a `Disposable` object. You should maintain a reference to this object and invoke `.dispose()` on it when you no longer wish to observe this attribute. + +#### 3. Inject the `Observable` into the tool -#### I don't get it. Doesn't Java's `Graphics` already let me draw stuff? +The paint tool will react to changing values provided by the `Observable`. Clicking the checkbox or toggling the menu item will affect the behavior of this tool in realtime. + +``` +PaintToolBuilder.create(PaintToolType.RECTANGLE) + .withDrawCenteredObservable(drawCenteredObservable) + .makeActiveOnCanvas(myCanvas) + .build(); -If you're not building an app that lets users paint lines and shapes with the mouse, this probably isn't for you. +``` -Java's `Graphics` context does indeed provide routines for stroking and filling primitive shapes, but there's a quite a bit of work involved to map mouse and keyboard events into these calls the way a "paint" app expects. Getting selections, scale, grids, and transforms to work correctly is a bit more complex than merely delegating to `AffineTransform`, too. +## Frequently asked questions #### How do I save my artwork? @@ -334,7 +352,7 @@ LassoTool currentTool; ProjectionTool newTool = PaintToolBuilder.create(...); currentTool.morphSelection(newTool); // newTool now has currentTool's selection -currentTool.deactivate(); // ... but currentTool is still active. +currentTool.deactivate(); // currentTool is still active, fix that ``` When morphing a Lasso selection to a transform tool, the selection bounds will become rectangular, but only the pixels originally encircled by the Lasso will be affected by the transform. @@ -359,19 +377,43 @@ Then, apply [one of their filters](http://www.jhlabs.com/ip/filters/index.html) myCanvas.operate(new SolarizeFilter()); ``` +#### I have multiple canvases open at the same time. How can I make a tool active on different canvases simultaneously. + +Strictly speaking, a JMonet paint tool can only be active on one canvas at a time. But that needn't stand in your way. As focus changes from one canvas to the next, simply re-activate the tool on the newly focused canvas to achieve your desired behavior. + +On each canvas that you want to share a tool, you might do something like: + +``` +thisCanvas.addFocusListener(new FocusListener() { + @Override + public void focusGained(FocusEvent e) { + theActiveTool.activate(thisCanvas); // implicitly deactivates from last canvas + } + + @Override + public void focusLost(FocusEvent e) { + // Nothing to do + } +}); +``` + +#### I created a custom stroke using a line shape but it doesn't work. What gives? + +The area defined by the stroke's shape is where the "pen" will produce ink. A line (or a curve) has no area and therefore produces no paint on the canvas. Use a thin `Rectangle2D` as your stroke shape instead. + #### Can I create my own tools? -Of course! Tools are typically subclassed from one of the abstract tool classes in the `paint.tools.base` package. These abstract classes handle UI events for the most common tool behaviors: +Of course! Tools are typically subclassed from one of the base tool classes in the `com.defano.jmonet.tools.base` package. These classes handle UI events for the most common tool behaviors, delegating to tool-specific subclasses for rendering: -Tool Base | Description --------------------------|------------ -`PaintTool` | Base class from which all paint tools are derived. Holds references to attribute providers and implements empty mouse and keyboard event handlers (_template pattern_; allows tools to override only those methods they care about). -`AbstractBoundsTool` | Click-and-drag to define a rectangular boundary. Examples: Rectangle, Oval, Round Rectangle, Shape tools. -`AbstractLineTool` | Click-and-drag to define a line between two points. Example: Line tool. -`AbstractPathTool` | Click-and-drag to define a free-form path on the canvas. Examples: Paintbrush, pencil, eraser tools. -`AbstractPolylineTool` | Click, click, click, double-click to define segments in a polygon or curve. Examples: Curve, Polygon tools. -`AbstractSelectionTool` | Most complex of the tool bases; click-and-drag to define a shape to be drawn with marching ants allowing the user to move or modify the underlying graphic. Examples: Selection, Lasso, Rotate tools. -`AbstractTransformTool` | Click-and-drag to select a rectangular boundary drawn with drag handles at each corner which can moved by the user. Example: Slant, projection, perspective and rubber sheet tools. +Tool Base | Delegate Class | Description +------------------|-------------------------|--------------------- +`BasicTool` | | Base class from which all paint tools are derived. Holds references to attribute providers and implements empty mouse and keyboard event handlers (_template pattern_; allows tools to override only those methods they care about). +`BoundsTool` | `BoundsToolDelegate` | Click-and-drag to define a rectangular boundary. Examples: Rectangle, Oval, Round Rectangle, Shape tools. +`LinearTool` | `LinearToolDelegate` | Click-and-drag to define a line between two points. Example: Line tool. +`PathTool` | `PathToolDelegate` | Click-and-drag to define a free-form path on the canvas. Examples: Paintbrush, pencil, eraser tools. +`PolylineTool` | `PolylineToolDelegate` | Click, click, click, double-click to define segments in a polygon or curve. Examples: Curve, Polygon tools. +`SelectionTool` | `SelectionToolDelegate` | Most complex of the tool bases; click-and-drag to define a shape to be drawn with marching ants allowing the user to move or modify the underlying graphic. Examples: Selection, Lasso, Rotate tools. +`TransformTool` | `TransformToolDelegate` | Click-and-drag to select a rectangular boundary drawn with drag handles at each corner which can moved by the user. Example: Slant, projection, perspective and rubber sheet tools. #### My canvas isn't getting garbage collected. This library has a memory leak. @@ -382,7 +424,3 @@ When you're done with a canvas, call `.dispose()` on the canvas object to allow #### What about vector graphic tools (i.e., "draw" apps)? Sorry, that's not the intent of this library. That said, many pieces of this library could be leveraged for such a tool... - -#### I created a custom stroke using a line shape but it doesn't work. What gives? - -The area defined by the stroke's shape is where the "pen" will produce ink. A line (or a curve) has no area and therefore produces no paint on the canvas. Use a thin `Rectangle2D` instead. diff --git a/pom.xml b/pom.xml index 442e7f05..efa5a426 100644 --- a/pom.xml +++ b/pom.xml @@ -4,10 +4,8 @@ xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> 4.0.0 - JMonet - A rudimentary toolkit for incorporating MacPaint-like tools into a Java Swing or JavaFX application. - + A toolkit for incorporating MacPaint-like tools into a Java Swing or JavaFX application. https://github.com/defano/jmonet @@ -35,7 +33,7 @@ jar com.defano.jmonet jmonet - 0.3.3 + 0.4.0 UTF-8 @@ -56,6 +54,32 @@ 2.1.16 + + org.junit.jupiter + junit-jupiter-api + 5.1.0 + test + + + + org.junit.vintage + junit-vintage-engine + 5.1.0 + test + + + + org.mockito + mockito-all + 1.10.19 + test + + + + com.google.inject + guice + 4.1.0 + @@ -71,6 +95,45 @@ + + + org.apache.maven.plugins + maven-surefire-plugin + 2.21.0 + + + org.junit.platform + junit-platform-surefire-provider + 1.2.0-M1 + + + org.junit.jupiter + junit-jupiter-engine + 5.1.0 + + + + + + org.jacoco + jacoco-maven-plugin + 0.8.2 + + + + prepare-agent + + + + report + test + + report + + + + + org.apache.maven.plugins maven-source-plugin diff --git a/src/main/java/com/defano/jmonet/algo/fill/DefaultBoundaryFunction.java b/src/main/java/com/defano/jmonet/algo/fill/DefaultBoundaryFunction.java deleted file mode 100644 index b0bb06cf..00000000 --- a/src/main/java/com/defano/jmonet/algo/fill/DefaultBoundaryFunction.java +++ /dev/null @@ -1,20 +0,0 @@ -package com.defano.jmonet.algo.fill; - -import java.awt.*; -import java.awt.image.BufferedImage; - -/** - * Default boundary function; fills all fully-transparent pixels. - */ -public class DefaultBoundaryFunction implements BoundaryFunction { - - /** - * {@inheritDoc} - */ - @Override - public boolean isBoundary(BufferedImage canvas, BufferedImage scratch, int x, int y) { - Color canvasPixel = new Color(canvas.getRGB(x, y), true); - Color scratchPixel = new Color(scratch.getRGB(x, y), true); - return canvasPixel.getAlpha() != 0 || scratchPixel.getAlpha() != 0; - } -} diff --git a/src/main/java/com/defano/jmonet/algo/fill/DefaultFillFunction.java b/src/main/java/com/defano/jmonet/algo/fill/DefaultFillFunction.java deleted file mode 100644 index 0c6628bf..00000000 --- a/src/main/java/com/defano/jmonet/algo/fill/DefaultFillFunction.java +++ /dev/null @@ -1,27 +0,0 @@ -package com.defano.jmonet.algo.fill; - -import java.awt.*; -import java.awt.image.BufferedImage; - -/** - * Default fill function; fills pixels with solid color or texture. - */ -public class DefaultFillFunction implements FillFunction { - - /** - * {@inheritDoc} - */ - @Override - public void fill(BufferedImage image, int x, int y, Paint fillPaint) { - - if (fillPaint instanceof Color) { - image.setRGB (x, y, ((Color) fillPaint).getRGB()); - } else if (fillPaint instanceof TexturePaint) { - BufferedImage texture = ((TexturePaint) fillPaint).getImage(); - int rgb = texture.getRGB(x % texture.getWidth(), y % texture.getHeight()); - image.setRGB (x, y, rgb); - } else { - throw new IllegalArgumentException("Don't know how to fill with paint " + fillPaint); - } - } -} diff --git a/src/main/java/com/defano/jmonet/algo/fill/FillFunction.java b/src/main/java/com/defano/jmonet/algo/fill/FillFunction.java deleted file mode 100644 index 3efe686e..00000000 --- a/src/main/java/com/defano/jmonet/algo/fill/FillFunction.java +++ /dev/null @@ -1,20 +0,0 @@ -package com.defano.jmonet.algo.fill; - -import java.awt.*; -import java.awt.image.BufferedImage; - -/** - * A function for filling a single pixel on the canvas. - */ -public interface FillFunction { - - /** - * Fills a single pixel in a image with a given paint or texture. - * - * @param image The image whose pixel should be filled. - * @param x The x coordinate of the point / pixel to fill - * @param y The y coordinate of the point / pixel to fill - * @param fillPaint The paint to apply to the given pixel. - */ - void fill(BufferedImage image, int x, int y, Paint fillPaint); -} diff --git a/src/main/java/com/defano/jmonet/canvas/AbstractPaintCanvas.java b/src/main/java/com/defano/jmonet/canvas/AbstractPaintCanvas.java index f23b5554..d1d09d39 100644 --- a/src/main/java/com/defano/jmonet/canvas/AbstractPaintCanvas.java +++ b/src/main/java/com/defano/jmonet/canvas/AbstractPaintCanvas.java @@ -4,8 +4,8 @@ import com.defano.jmonet.canvas.layer.ImageLayerSet; import com.defano.jmonet.canvas.observable.CanvasCommitObserver; import com.defano.jmonet.canvas.surface.AbstractPaintSurface; -import com.defano.jmonet.tools.util.Geometry; -import io.reactivex.Observable; +import com.defano.jmonet.context.GraphicsContext; +import com.defano.jmonet.tools.util.MathUtils; import io.reactivex.subjects.BehaviorSubject; import java.awt.*; @@ -50,6 +50,14 @@ public Dimension getCanvasSize() { return getSurfaceDimension(); } + /** + * {@inheritDoc} + */ + @Override + public Component getComponent() { + return this; + } + /** * Marks this component safe for garbage collection; removes registered listeners and components. */ @@ -78,7 +86,7 @@ public ImageLayer[] getImageLayers() { @Override public void clearCanvas() { Rectangle clear = new Rectangle(new Point(), getCanvasSize()); - Graphics2D g2 = scratch.getRemoveScratchGraphics(null, clear); + GraphicsContext g2 = scratch.getRemoveScratchGraphics(null, clear); g2.setColor(Color.WHITE); g2.fill(clear); @@ -107,15 +115,15 @@ public void setCanvasBackground(Paint paint) { @Override public Point convertViewPointToModel(Point p) { Point error = getScrollError(); - int gridSpacing = getGridSpacingObservable().blockingFirst(); + int gridSpacing = getGridSpacing(); double scale = getScaleObservable().blockingFirst(); int x = p.x - error.x; - x = Geometry.round(x, (int) (gridSpacing * scale)); + x = MathUtils.nearestFloor(x, (int) (gridSpacing * scale)); x = (int) (x / scale); int y = p.y - error.y; - y = Geometry.round(y, (int) (gridSpacing * scale)); + y = MathUtils.nearestFloor(y, (int) (gridSpacing * scale)); y = (int) (y / scale); return new Point(x, y); @@ -164,8 +172,8 @@ public void setGridSpacing(int grid) { * {@inheritDoc} */ @Override - public Observable getGridSpacingObservable() { - return gridSpacingSubject; + public int getGridSpacing() { + return gridSpacingSubject.blockingFirst(); } /** @@ -204,8 +212,6 @@ public Point getScrollError() { return new Point((int) (viewRect.x % scale), (int) (viewRect.y % scale)); } - - protected void fireCanvasCommitObservers(PaintCanvas canvas, ImageLayerSet imageLayerSet, BufferedImage canvasImage) { for (CanvasCommitObserver thisObserver : observers) { thisObserver.onCommit(canvas, imageLayerSet, canvasImage); diff --git a/src/main/java/com/defano/jmonet/canvas/JMonetCanvas.java b/src/main/java/com/defano/jmonet/canvas/JMonetCanvas.java index f08ca948..07d10ec9 100644 --- a/src/main/java/com/defano/jmonet/canvas/JMonetCanvas.java +++ b/src/main/java/com/defano/jmonet/canvas/JMonetCanvas.java @@ -1,9 +1,11 @@ package com.defano.jmonet.canvas; -import com.defano.jmonet.algo.transform.image.ApplyPixelTransform; -import com.defano.jmonet.algo.transform.image.PixelTransform; -import com.defano.jmonet.algo.transform.image.StaticImageTransform; -import com.defano.jmonet.algo.transform.image.Transformable; +import com.defano.jmonet.context.AwtGraphicsContext; +import com.defano.jmonet.context.GraphicsContext; +import com.defano.jmonet.transform.image.ApplyPixelTransform; +import com.defano.jmonet.transform.image.PixelTransform; +import com.defano.jmonet.transform.image.StaticImageTransform; +import com.defano.jmonet.transform.image.Transformable; import com.defano.jmonet.canvas.layer.ImageLayer; import com.defano.jmonet.canvas.layer.ImageLayerSet; import com.defano.jmonet.canvas.layer.LayeredImage; @@ -20,7 +22,8 @@ /** * A paint canvas with a built-in undo and redo buffer. */ -public class JMonetCanvas extends AbstractPaintCanvas implements LayerSetObserver, Transformable { +@SuppressWarnings("unused") +public class JMonetCanvas extends AbstractPaintCanvas implements LayerSetObserver, Transformable, Undoable { // Maximum number of allowable undo operations private final int maxUndoBufferDepth; @@ -46,6 +49,7 @@ public class JMonetCanvas extends AbstractPaintCanvas implements LayerSetObserve * @param initialImage The image to be displayed in the canvas. * @param undoBufferDepth The depth of the undo buffer (number of undo operations) */ + @SuppressWarnings("WeakerAccess") public JMonetCanvas(BufferedImage initialImage, int undoBufferDepth) { super(new Dimension(initialImage.getWidth(), initialImage.getHeight())); this.maxUndoBufferDepth = Math.max(1, undoBufferDepth); @@ -59,6 +63,7 @@ public JMonetCanvas(BufferedImage initialImage, int undoBufferDepth) { * @param dimension The size of the canvas. * @param undoBufferDepth The depth of the undo buffer (number of undo operations) */ + @SuppressWarnings("WeakerAccess") public JMonetCanvas(Dimension dimension, int undoBufferDepth) { this(new BufferedImage(dimension.width, dimension.height, BufferedImage.TYPE_INT_ARGB), undoBufferDepth); } @@ -82,12 +87,8 @@ public JMonetCanvas(Dimension dimension) { this(dimension, 12); } - /** - * Undoes the previous committed change. May be called successively to revert committed changes one-by-one until - * the undo buffer is exhausted. - * - * @return The ChangeSet that was undone by this operation, or null if there were no undoable changes. - */ + /** {@inheritDoc} */ + @Override public ImageLayerSet undo() { if (hasUndoableChanges()) { @@ -103,11 +104,8 @@ public ImageLayerSet undo() { return null; } - /** - * Reverts the previous undo; has no effect if a commit was made following the previous undo. - * - * @return True if the redo was successful, false if there is no undo available to revert. - */ + /** {@inheritDoc} */ + @Override public boolean redo() { if (hasRedoableChanges()) { @@ -138,47 +136,32 @@ public ImageLayerSet peek(int index) { return undoBuffer.get(undoBufferPointer.blockingFirst() - index); } - /** - * Determines if a commit is available to be undone. - * - * @return True if {@link #undo()} will succeed; false otherwise. - */ + /** {@inheritDoc} */ + @Override public boolean hasUndoableChanges() { return undoBufferPointer.blockingFirst() >= 0; } - /** - * Determines if an undo is available to revert. - * - * @return True if {@link #redo()} will succeed; false otherwise. - */ + /** {@inheritDoc} */ + @Override public boolean hasRedoableChanges() { return undoBufferPointer.blockingFirst() < undoBuffer.size() - 1; } - /** - * Gets the maximum depth of the undo buffer. - * - * @return The maximum number of undos that are supported by this PaintCanvas. - */ + /** {@inheritDoc} */ + @Override public int getMaxUndoBufferDepth() { return maxUndoBufferDepth; } - /** - * Gets the number of changes that can be "undone". - * - * @return The depth of undo buffer. - */ + /** {@inheritDoc} */ + @Override public int getUndoBufferDepth() { return undoBufferPointer.blockingFirst() + 1; } - /** - * Gets the number of changes that can be "redone". - * - * @return The depth of the redo buffer. - */ + /** {@inheritDoc} */ + @Override public int getRedoBufferDepth() { return undoBuffer.size() - undoBufferPointer.blockingFirst() - 1; } @@ -334,9 +317,9 @@ private void overlayImage(BufferedImage source, BufferedImage destination, Alpha * @param destination The image on which to draw them */ private void overlayImage(LayeredImage layeredImage, BufferedImage destination) { - Graphics2D g2d = (Graphics2D) destination.getGraphics(); - layeredImage.paint(g2d, null, null); - g2d.dispose(); + GraphicsContext g = new AwtGraphicsContext((Graphics2D) destination.getGraphics()); + layeredImage.paint(g, 1.0, null); + g.dispose(); } /** diff --git a/src/main/java/com/defano/jmonet/canvas/PaintCanvas.java b/src/main/java/com/defano/jmonet/canvas/PaintCanvas.java index 632fc0fa..fe887c7b 100644 --- a/src/main/java/com/defano/jmonet/canvas/PaintCanvas.java +++ b/src/main/java/com/defano/jmonet/canvas/PaintCanvas.java @@ -11,6 +11,7 @@ /** * A canvas that can be drawn upon by the paint tools. */ +@SuppressWarnings({"unused", "UnusedReturnValue"}) public interface PaintCanvas extends PaintSurface, ScaledLayeredImage { /** diff --git a/src/main/java/com/defano/jmonet/canvas/Scratch.java b/src/main/java/com/defano/jmonet/canvas/Scratch.java index b9e4290f..bd2944e7 100644 --- a/src/main/java/com/defano/jmonet/canvas/Scratch.java +++ b/src/main/java/com/defano/jmonet/canvas/Scratch.java @@ -2,7 +2,9 @@ import com.defano.jmonet.canvas.layer.ImageLayer; import com.defano.jmonet.canvas.layer.ImageLayerSet; -import com.defano.jmonet.tools.builder.PaintTool; +import com.defano.jmonet.context.AwtGraphicsContext; +import com.defano.jmonet.context.GraphicsContext; +import com.defano.jmonet.tools.base.Tool; import java.awt.*; import java.awt.image.BufferedImage; @@ -22,13 +24,21 @@ * {@link AlphaComposite#DST_OUT}. *

* The dimension of the scratch buffer always matches the dimension of the canvas, but for performance, the scratch - * buffer uses clipping regions to paint only the portion of the buffer that the tool has modified. + * buffer manages a dirty region to paint only the portion of the buffer that the tool has modified. */ public class Scratch { + // Region of the addScratch and removeScratch that have been dirtied by tools (null implies no changes) + private Rectangle addScratchDirtyRgn, removeScratchDirtyRgn; + + // Dimension of the scratch buffer private int width, height; + + // Scratch buffer data private BufferedImage addScratch, removeScratch; - private Graphics2D addScratchGraphics, removeScratchGraphics; + + // Graphics context created from the buffers + private GraphicsContext addScratchGraphics, removeScratchGraphics; /** * Creates a scratch unbound to any tool with a given dimension. @@ -55,13 +65,13 @@ public void setSize(int width, int height) { BufferedImage newAddScratch = new BufferedImage(width, height, BufferedImage.TYPE_INT_ARGB); Graphics newAddScratchGraphics = newAddScratch.getGraphics(); newAddScratchGraphics.drawImage(addScratch, 0, 0, null); - setAddScratch(newAddScratch); + setAddScratch(newAddScratch, null); newAddScratchGraphics.dispose(); BufferedImage newRemoveScratch = new BufferedImage(width, height, BufferedImage.TYPE_INT_ARGB); Graphics newRemoveScratchGraphics = newRemoveScratch.getGraphics(); newRemoveScratchGraphics.drawImage(removeScratch, 0, 0, null); - setRemoveScratch(newRemoveScratch); + setRemoveScratch(newRemoveScratch, null); newRemoveScratchGraphics.dispose(); } @@ -74,27 +84,37 @@ public Dimension getSize() { return new Dimension(this.width, this.height); } + /** + * Gets the bounds (location and dimension) of the scratch buffer. Note the buffer is always located at the orgin + * (0,0). + * + * @return The bounding rectangle of the scratch buffer. + */ + public Rectangle getBounds() { + return new Rectangle(0, 0, this.width, this.height); + } + /** * Clears the scratch buffer (both add and remove buffers), restoring it to its original, unmodified (fully * transparent) state. */ public void clear() { - clearAdd(); - clearRemove(); + clearAddScratch(); + clearRemoveScratch(); } /** * Clears the remove-scratch buffer, restoring it to its original, unmodified (fully transparent) state. */ - public void clearRemove() { - setRemoveScratch(new BufferedImage(width, height, BufferedImage.TYPE_INT_ARGB)); + public void clearRemoveScratch() { + setRemoveScratch(new BufferedImage(width, height, BufferedImage.TYPE_INT_ARGB), null); } /** * Clears the add-scratch buffer, restoring it to its original, unmodified (fully transparent) state. */ - public void clearAdd() { - setAddScratch(new BufferedImage(width, height, BufferedImage.TYPE_INT_ARGB)); + public void clearAddScratch() { + setAddScratch(new BufferedImage(width, height, BufferedImage.TYPE_INT_ARGB), null); } /** @@ -112,8 +132,8 @@ public void clearAdd() { * modified. * @return The remove-scratch buffer ready for use. */ - public Graphics2D getRemoveScratchGraphics(PaintTool tool, Stroke stroke, Shape shape) { - return getRemoveScratchGraphics(tool, getBounds(stroke, shape)); + public GraphicsContext getRemoveScratchGraphics(Tool tool, Stroke stroke, Shape shape) { + return getRemoveScratchGraphics(tool, getShapeBounds(stroke, shape)); } /** @@ -130,11 +150,11 @@ public Graphics2D getRemoveScratchGraphics(PaintTool tool, Stroke stroke, Shape * modified. * @return The remove-scratch buffer ready for use. */ - public Graphics2D getRemoveScratchGraphics(PaintTool tool, Shape bounds) { - setClip(bounds, removeScratchGraphics); + public GraphicsContext getRemoveScratchGraphics(Tool tool, Shape bounds) { + removeScratchDirtyRgn = updateDirtiedRgn(bounds, removeScratchDirtyRgn); if (tool != null) { - tool.applyRenderingHints(removeScratchGraphics); + removeScratchGraphics.setAntialiasingMode(tool.getAttributes().getAntiAliasing()); } return removeScratchGraphics; @@ -155,8 +175,8 @@ public Graphics2D getRemoveScratchGraphics(PaintTool tool, Shape bounds) { * modified. * @return The remove-scratch buffer ready for use. */ - public Graphics2D getAddScratchGraphics(PaintTool tool, Stroke stroke, Shape shape) { - return getAddScratchGraphics(tool, getBounds(stroke, shape)); + public GraphicsContext getAddScratchGraphics(Tool tool, Stroke stroke, Shape shape) { + return getAddScratchGraphics(tool, getShapeBounds(stroke, shape)); } /** @@ -173,11 +193,11 @@ public Graphics2D getAddScratchGraphics(PaintTool tool, Stroke stroke, Shape sha * modified. * @return The remove-scratch buffer ready for use. */ - public Graphics2D getAddScratchGraphics(PaintTool tool, Shape bounds) { - setClip(bounds, addScratchGraphics); + public GraphicsContext getAddScratchGraphics(Tool tool, Shape bounds) { + addScratchDirtyRgn = updateDirtiedRgn(bounds, addScratchDirtyRgn); if (tool != null) { - tool.applyRenderingHints(addScratchGraphics); + addScratchGraphics.setAntialiasingMode(tool.getAttributes().getAntiAliasing()); } return addScratchGraphics; @@ -185,18 +205,20 @@ public Graphics2D getAddScratchGraphics(PaintTool tool, Shape bounds) { /** * Replaces the current add-scratch buffer with a provided image. Set the buffer's clipping region to the bounds - * of the image. + * provided. * * @param addScratch The new add-scratch image. + * @param dirtyRgn The dirty region of the new buffer; null means no area is dirty, whereas a rectangle whose dimensions + * equal those of the image means the entire buffer is dirty. */ - public void setAddScratch(BufferedImage addScratch) { + public void setAddScratch(BufferedImage addScratch, Rectangle dirtyRgn) { if (addScratchGraphics != null) { addScratchGraphics.dispose(); } this.addScratch = addScratch; - this.addScratchGraphics = this.addScratch.createGraphics(); - setClip(new Rectangle(0, 0, addScratch.getWidth(), addScratch.getHeight()), addScratchGraphics); + this.addScratchGraphics = new AwtGraphicsContext(this.addScratch.createGraphics()); + this.addScratchDirtyRgn = dirtyRgn; } /** @@ -204,15 +226,17 @@ public void setAddScratch(BufferedImage addScratch) { * of the image. * * @param removeScratch The new remove-scratch image. + * @param dirtyRgn The dirty region of the new buffer; null means no area is dirty, whereas a rectangle whose dimensions + * equal those of the image means the entire buffer is dirty. */ - public void setRemoveScratch(BufferedImage removeScratch) { + public void setRemoveScratch(BufferedImage removeScratch, Rectangle dirtyRgn) { if (removeScratchGraphics != null) { removeScratchGraphics.dispose(); } this.removeScratch = removeScratch; - this.removeScratchGraphics = this.removeScratch.createGraphics(); - setClip(new Rectangle(0, 0, addScratch.getWidth(), addScratch.getHeight()), removeScratchGraphics); + this.removeScratchGraphics = new AwtGraphicsContext(this.removeScratch.createGraphics()); + this.removeScratchDirtyRgn = dirtyRgn; } /** @@ -221,13 +245,15 @@ public void setRemoveScratch(BufferedImage removeScratch) { * @return The remove-scratch, as a {@link ImageLayer} */ public ImageLayer getRemoveScratchLayer() { - Rectangle minBounds = removeScratchGraphics.getClipBounds(); - if (minBounds == null || minBounds.isEmpty()) { + + if (removeScratchDirtyRgn == null || removeScratchDirtyRgn.isEmpty()) { return null; } - BufferedImage subimage = removeScratch.getSubimage(minBounds.x, minBounds.y, minBounds.width, minBounds.height); - return new ImageLayer(minBounds.getLocation(), subimage, AlphaComposite.getInstance(AlphaComposite.DST_OUT, 1.0f)); + return new ImageLayer( + removeScratchDirtyRgn.getLocation(), + removeScratch.getSubimage(removeScratchDirtyRgn.x, removeScratchDirtyRgn.y, removeScratchDirtyRgn.width, removeScratchDirtyRgn.height), + AlphaComposite.getInstance(AlphaComposite.DST_OUT, 1.0f)); } /** @@ -236,14 +262,15 @@ public ImageLayer getRemoveScratchLayer() { * @return The add-scratch, as a {@link ImageLayer} */ public ImageLayer getAddScratchLayer() { - Rectangle minBounds = addScratchGraphics.getClipBounds(); - if (minBounds == null || minBounds.isEmpty()) { + if (addScratchDirtyRgn == null || addScratchDirtyRgn.isEmpty()) { return null; } - BufferedImage subimage = addScratch.getSubimage(minBounds.x, minBounds.y, minBounds.width, minBounds.height); - return new ImageLayer(minBounds.getLocation(), subimage, AlphaComposite.getInstance(AlphaComposite.SRC_OVER, 1.0f)); + return new ImageLayer( + addScratchDirtyRgn.getLocation(), + addScratch.getSubimage(addScratchDirtyRgn.x, addScratchDirtyRgn.y, addScratchDirtyRgn.width, addScratchDirtyRgn.height), + AlphaComposite.getInstance(AlphaComposite.SRC_OVER, 1.0f)); } /** @@ -254,17 +281,53 @@ public ImageLayer getAddScratchLayer() { public ImageLayerSet getLayerSet() { ImageLayerSet imageLayerSet = new ImageLayerSet(); - if (removeScratchGraphics.getClipBounds() != null) { + if (removeScratchDirtyRgn != null) { imageLayerSet.addLayer(getRemoveScratchLayer()); } - if (addScratchGraphics.getClipBounds() != null) { + if (addScratchDirtyRgn != null) { imageLayerSet.addLayer(getAddScratchLayer()); } return imageLayerSet; } + /** + * A convenience method to erase paint from all pixels bounded by a given shape and stroke, taking into account + * canvas background and erase color. + * + * @param tool The tool that is erasing + * @param shape The shape to be erased + * @param stroke The stroke of the shape being erased + */ + public void erase(Tool tool, Shape shape, Stroke stroke) { + Paint erasePaint = tool.getAttributes().getEraseColor(); + + GraphicsContext g = erasePaint == null ? + getRemoveScratchGraphics(tool, stroke, shape) : + getAddScratchGraphics(tool, stroke, shape); + + g.setStroke(stroke); + g.setPaint(erasePaint == null ? tool.getCanvas().getCanvasBackground() : erasePaint); + g.draw(shape); + } + + /** + * Gets a rectangle identifying a sub-region of the scratch buffer that has been modified and needs to be repainted. + * @return The region of the scratch buffer that has been marked as dirty by tools. + */ + public Rectangle getDirtyRegion() { + if (addScratchDirtyRgn == null) { + return removeScratchDirtyRgn; + } + + if (removeScratchDirtyRgn == null) { + return addScratchDirtyRgn; + } + + return addScratchDirtyRgn.union(removeScratchDirtyRgn); + } + /** * Calculates the bounds of shape stroked by a given stroke. * @@ -272,7 +335,7 @@ public ImageLayerSet getLayerSet() { * @param shape The shape to be stroked and bounds calculated. * @return The smallest rectangle that encloses the stroked shape. */ - private Rectangle getBounds(Stroke stroke, Shape shape) { + private Rectangle getShapeBounds(Stroke stroke, Shape shape) { if (shape == null) { return new Rectangle(); } @@ -281,24 +344,22 @@ private Rectangle getBounds(Stroke stroke, Shape shape) { } /** - * Sets the scratch buffer's clipping region to union of the existing clipping region and the bounds of the given - * shape. + * Returns the union of the bounds of the given shape with a given dirty region rectangle, intersected by the bounds + * of this buffer. * * @param shape The shape representing the bounds to be added to the existing region - * @param context The graphics context whose clipping region should be set. + * @param dirtyRgn A rectangle identifying a region of the scratch buffer that has been dirtied + * @return A union of the previously dirtied region and the newly dirtied region */ - private void setClip(Shape shape, Graphics2D context) { - + private Rectangle updateDirtiedRgn(Shape shape, Rectangle dirtyRgn) { if (shape != null) { - Rectangle clip = shape.getBounds().intersection(new Rectangle(0, 0, width, height)); - Rectangle bounds = context.getClipBounds(); - - if (bounds == null) { - context.setClip(clip.x, clip.y, clip.width, clip.height); - } else if (!bounds.contains(clip)) { - Rectangle union = bounds.union(clip); - context.setClip(union.x, union.y, union.width, union.height); + if (dirtyRgn == null || dirtyRgn.isEmpty()) { + dirtyRgn = getBounds().intersection(shape.getBounds()); + } else { + dirtyRgn = getBounds().intersection(dirtyRgn.union(shape.getBounds())); } } + + return dirtyRgn; } } diff --git a/src/main/java/com/defano/jmonet/canvas/Undoable.java b/src/main/java/com/defano/jmonet/canvas/Undoable.java new file mode 100644 index 00000000..a6e6ba75 --- /dev/null +++ b/src/main/java/com/defano/jmonet/canvas/Undoable.java @@ -0,0 +1,57 @@ +package com.defano.jmonet.canvas; + +import com.defano.jmonet.canvas.layer.ImageLayerSet; + +public interface Undoable { + + /** + * Undoes the previous committed change. May be called successively to revert committed changes one-by-one until + * the undo buffer is exhausted. + * + * @return The ChangeSet that was undone by this operation, or null if there were no undoable changes. + */ + ImageLayerSet undo(); + + /** + * Reverts the previous undo; has no effect if a commit was made following the previous undo. + * + * @return True if the redo was successful, false if there is no undo available to revert. + */ + boolean redo(); + + /** + * Determines if a commit is available to be undone. + * + * @return True if {@link #undo()} will succeed; false otherwise. + */ + boolean hasUndoableChanges(); + + /** + * Determines if an undo is available to revert. + * + * @return True if {@link #redo()} will succeed; false otherwise. + */ + boolean hasRedoableChanges(); + + /** + * Gets the maximum depth of the undo buffer. + * + * @return The maximum number of undos that are supported by this PaintCanvas. + */ + int getMaxUndoBufferDepth(); + + /** + * Gets the number of changes that can be "undone". + * + * @return The depth of undo buffer. + */ + int getUndoBufferDepth(); + + /** + * Gets the number of changes that can be "redone". + * + * @return The depth of the redo buffer. + */ + int getRedoBufferDepth(); + +} diff --git a/src/main/java/com/defano/jmonet/canvas/layer/ImageLayer.java b/src/main/java/com/defano/jmonet/canvas/layer/ImageLayer.java index 39052c55..2035f5d4 100644 --- a/src/main/java/com/defano/jmonet/canvas/layer/ImageLayer.java +++ b/src/main/java/com/defano/jmonet/canvas/layer/ImageLayer.java @@ -1,16 +1,18 @@ package com.defano.jmonet.canvas.layer; +import com.defano.jmonet.context.GraphicsContext; + import java.awt.*; import java.awt.image.BufferedImage; /** - * A layer in a layered image. + * An image layer comprising a portion of a {@link LayeredImage}. */ public class ImageLayer { private final Point location; private final BufferedImage image; - private final AlphaComposite composite; + private final Composite composite; /** * Creates a layer in which the given image is drawn atop a destination image using {@link AlphaComposite#SRC_OVER} @@ -29,7 +31,8 @@ public ImageLayer(BufferedImage image) { * @param image The image comprising this layer * @param composite The composite mode this layer is drawn with */ - public ImageLayer(BufferedImage image, AlphaComposite composite) { + @SuppressWarnings("unused") + public ImageLayer(BufferedImage image, Composite composite) { this(new Point(0, 0), image, composite); } @@ -41,51 +44,53 @@ public ImageLayer(BufferedImage image, AlphaComposite composite) { * @param image The image to be drawn * @param composite The composite mode to draw with */ - public ImageLayer(Point location, BufferedImage image, AlphaComposite composite) { + public ImageLayer(Point location, BufferedImage image, Composite composite) { this.location = location; this.image = image; this.composite = composite; } /** - * Draws the visible portion of this image layer onto a graphics context at scale. + * Draws this image layer onto a graphics context at scale, painting only the pixels bound by a clipping rectangle. + * + * Note that the coordinates and bounds of the clipping rectangle are specified in scaled coordinates; that is, + * the clipping rectangle is specified in terms of the graphics context, not this image layer's buffer. Only pixels + * bound by the clipping rectangle should be painted on the graphics context. This may result in some or none of + * this layer being rendered depending on its size and location. * * @param g The graphics context on which to draw. - * @param scale The scale at which to draw the image (1.0 or null means no scaling). When null, no scaling will be - * provided. - * @param clip The clipping rectangle; only the portion of this image bounded by this rectangle will be drawn. When - * null, the entire image will be drawn. Note that this rectangle is represented in scaled coordinate - * space. This, the rect (10,10), (100,100) when scale is 2.0 refers to the this layer's - * sub image (5,5),(50,50). + * @param scale The scale at which to draw the image; 1.0 means no scaling. + * @param clip The clipping rectangle, represented in scaled coordinates. Only the portion of this image bounded by + * this rectangle will be drawn. When null, the entire image will be drawn. */ - public void paint(Graphics2D g, Double scale, Rectangle clip) { + public void paint(GraphicsContext g, double scale, Rectangle clip) { g.setComposite(composite); + // When a clipping region is not specified, draw the entire image layer if (clip == null) { - clip = new Rectangle(0, 0, image.getWidth(), image.getHeight()); - } - - if (scale == null) { - scale = 1.0; + clip = new Rectangle(0, 0, (int) ((location.x + image.getWidth()) * scale), (int) ((location.y + image.getHeight()) * scale)); } - // Clipping rectangle is in scaled coordinate space; descale to model coordinates - Rectangle unscaledClipRgn = new Rectangle((int) (clip.x / scale), (int) (clip.y / scale), (int) (clip.width / scale), (int) (clip.height / scale)); - - // Unscaled bounding box of where image will be drawn on graphics context - Rectangle imageBounds = new Rectangle(location.x, location.y, image.getWidth(), image.getHeight()); - - // Portion of unscaled draw region that is also within the clipping rectangle - Rectangle drawBounds = imageBounds.intersection(unscaledClipRgn); - - // Slightly overdraw the image (to prevent clipping on bottom and right-most row/column) - drawBounds.setSize(drawBounds.width + 2, drawBounds.height + 2); - - // Draw source geometry from image into destination geometry of graphics context - g.drawImage(image, - 0, 0, (int) (scale * drawBounds.width), (int) (scale * drawBounds.height), - drawBounds.x, drawBounds.y, drawBounds.x + drawBounds.width, drawBounds.y + drawBounds.height, - null); + Rectangle unscaledClip = new Rectangle ( + (int) (clip.x / scale), + (int) (clip.y / scale), + (int) (clip.width / scale), + (int) (clip.height / scale) + ); + + // Source: Rectangle defining the portion of this ImageLayer that will be painted + int x1 = Math.max(0, unscaledClip.x - location.x); + int y1 = Math.max(0, unscaledClip.y - location.y); + int x2 = x1 + Math.min(image.getWidth(), unscaledClip.width); + int y2 = y1 + Math.min(image.getHeight(), unscaledClip.height); + + // Destination: Bounds of the graphics context that will be painted. + int dx1 = (int)(scale * Math.max(0, location.x - unscaledClip.x)); + int dy1 = (int)(scale * Math.max(0, location.y - unscaledClip.y)); + int dx2 = dx1 + (int)(Math.min(image.getWidth(), unscaledClip.width) * scale); + int dy2 = dy1 + (int)(Math.min(image.getHeight(), unscaledClip.height) * scale); + + g.drawImage(image, dx1, dy1, dx2, dy2, x1, y1, x2, y2, null); } /** @@ -111,7 +116,7 @@ public BufferedImage getImage() { * * @return The overlay alpha mode. */ - public AlphaComposite getComposite() { + public Composite getComposite() { return composite; } @@ -120,7 +125,12 @@ public AlphaComposite getComposite() { * * @return The image size */ - public Dimension getSize() { - return new Dimension(image.getWidth(null), image.getHeight(null)); + public Dimension getDisplayedSize() { + return new Dimension(location.x + image.getWidth(), location.y + image.getHeight()); } + + public Dimension getStoredSize() { + return new Dimension(image.getWidth(), image.getHeight()); + } + } diff --git a/src/main/java/com/defano/jmonet/canvas/layer/ImageLayerSet.java b/src/main/java/com/defano/jmonet/canvas/layer/ImageLayerSet.java index 17d51aa6..d61971aa 100644 --- a/src/main/java/com/defano/jmonet/canvas/layer/ImageLayerSet.java +++ b/src/main/java/com/defano/jmonet/canvas/layer/ImageLayerSet.java @@ -85,6 +85,7 @@ public void addLayerSetObserver(LayerSetObserver observer) { * @param observer The observer to remove. * @return True if the observer exists and was removed; false otherwise. */ + @SuppressWarnings("UnusedReturnValue") public boolean removeLayerSetObserver(LayerSetObserver observer) { return observers.remove(observer); } diff --git a/src/main/java/com/defano/jmonet/canvas/layer/LayeredImage.java b/src/main/java/com/defano/jmonet/canvas/layer/LayeredImage.java index 4e1a504a..ab7d02e3 100644 --- a/src/main/java/com/defano/jmonet/canvas/layer/LayeredImage.java +++ b/src/main/java/com/defano/jmonet/canvas/layer/LayeredImage.java @@ -1,10 +1,13 @@ package com.defano.jmonet.canvas.layer; +import com.defano.jmonet.context.AwtGraphicsContext; +import com.defano.jmonet.context.GraphicsContext; + import java.awt.*; import java.awt.image.BufferedImage; /** - * An image that is composed of multiple layers rendered atop one another. + * An image that is composed of multiple layers rendered one atop another. */ public interface LayeredImage { @@ -24,8 +27,8 @@ default BufferedImage render() { Dimension size = getSize(); BufferedImage rendering = new BufferedImage(size.width, size.height, BufferedImage.TYPE_INT_ARGB); - Graphics2D g = rendering.createGraphics(); - paint(g, null, null); + GraphicsContext g = new AwtGraphicsContext(rendering.createGraphics()); + paint(g, 1.0, null); g.dispose(); return rendering; @@ -38,7 +41,7 @@ default BufferedImage render() { * @param scale The scale at which to draw the image * @param clip The clipping rectangle describing the bounds of the graphics context that should be painted */ - default void paint(Graphics2D g, Double scale, Rectangle clip) { + default void paint(GraphicsContext g, Double scale, Rectangle clip) { for (ImageLayer thisLayer : getImageLayers()) { if (thisLayer != null) { thisLayer.paint(g, scale, clip); @@ -55,7 +58,7 @@ default Dimension getSize() { int height = 0, width = 0; for (ImageLayer thisLayer : getImageLayers()) { - Dimension layerDimension = thisLayer.getSize(); + Dimension layerDimension = thisLayer.getDisplayedSize(); height = Math.max(height, layerDimension.height); width = Math.max(width, layerDimension.width); } diff --git a/src/main/java/com/defano/jmonet/canvas/observable/ObservableSurface.java b/src/main/java/com/defano/jmonet/canvas/observable/ObservableSurface.java index ebd92298..b5dea059 100644 --- a/src/main/java/com/defano/jmonet/canvas/observable/ObservableSurface.java +++ b/src/main/java/com/defano/jmonet/canvas/observable/ObservableSurface.java @@ -19,5 +19,7 @@ public interface ObservableSurface { * @param listener The observer to be removed * @return True if the listener was previously registered as an observer and was successfully removed */ + @SuppressWarnings("UnusedReturnValue") boolean removeSurfaceInteractionObserver(SurfaceInteractionObserver listener); + } diff --git a/src/main/java/com/defano/jmonet/canvas/observable/SurfaceInteractionObserver.java b/src/main/java/com/defano/jmonet/canvas/observable/SurfaceInteractionObserver.java index 67f42485..2937ecc2 100644 --- a/src/main/java/com/defano/jmonet/canvas/observable/SurfaceInteractionObserver.java +++ b/src/main/java/com/defano/jmonet/canvas/observable/SurfaceInteractionObserver.java @@ -9,49 +9,58 @@ * An observer of mouse and keyboard events taking place on a {@link ScaledLayeredImage}. * Converts mouse events to the coordinate space of the image represented by the ScaledLayeredImage. */ -@SuppressWarnings("EmptyMethod") +@SuppressWarnings({"EmptyMethod", "unused"}) public interface SurfaceInteractionObserver extends KeyListener { + default Cursor getDefaultCursor() { + return new Cursor(Cursor.DEFAULT_CURSOR); + } + /** * Invoked when the mouse button has been clicked (pressed * and released) on the canvas. * * @param e The mouse event that occurred - * @param imageLocation the location (relative to the image) where the event occurred + * @param imageLocation The location (relative to the canvas) where the event occurred, taking into account scale, + * grid and scroll pane complications as appropriate. */ - void mouseClicked(MouseEvent e, Point imageLocation); + default void mouseClicked(MouseEvent e, Point imageLocation) {} /** * Invoked when a mouse button has been pressed on a component. * * @param e The mouse event that occurred - * @param imageLocation the location (relative to the image) where the event occurred + * @param imageLocation The location (relative to the canvas) where the event occurred, taking into account scale, + * grid and scroll pane complications as appropriate. */ - void mousePressed(MouseEvent e, Point imageLocation); + default void mousePressed(MouseEvent e, Point imageLocation) {} /** * Invoked when a mouse button has been released on a component. * * @param e The mouse event that occurred - * @param imageLocation the location (relative to the image) where the event occurred + * @param canvasLoc The location (relative to the canvas) where the event occurred, taking into account scale, + * grid and scroll pane complications as appropriate. */ - void mouseReleased(MouseEvent e, Point imageLocation); + default void mouseReleased(MouseEvent e, Point canvasLoc) {} /** * Invoked when the mouse enters a component. * * @param e The mouse event that occurred - * @param imageLocation the location (relative to the image) where the event occurred + * @param canvasLoc The location (relative to the canvas) where the event occurred, taking into account scale, + * grid and scroll pane complications as appropriate. */ - void mouseEntered(MouseEvent e, Point imageLocation); + default void mouseEntered(MouseEvent e, Point canvasLoc) {} /** * Invoked when the mouse exits a component. * * @param e The mouse event that occurred - * @param imageLocation the location (relative to the image) where the event occurred + * @param canvasLoc The location (relative to the canvas) where the event occurred, taking into account scale, + * grid and scroll pane complications as appropriate. */ - void mouseExited(MouseEvent e, Point imageLocation); + default void mouseExited(MouseEvent e, Point canvasLoc) {} /** * Invoked when a mouse button is pressed on a component and then @@ -65,16 +74,40 @@ public interface SurfaceInteractionObserver extends KeyListener { * Drag&Drop operation. * * @param e The mouse event that occurred - * @param imageLocation the location (relative to the image) where the event occurred + * @param canvasLoc The location (relative to the canvas) where the event occurred, taking into account scale, + * grid and scroll pane complications as appropriate. */ - void mouseDragged(MouseEvent e, Point imageLocation); + default void mouseDragged(MouseEvent e, Point canvasLoc) {} /** * Invoked when the mouse cursor has been moved onto a component * but no buttons have been pushed. * * @param e The mouse event that occurred - * @param imageLocation the location (relative to the image) where the event occurred + * @param canvasLoc The location (relative to the canvas) where the event occurred, taking into account scale, + * grid and scroll pane complications as appropriate. + */ + default void mouseMoved(MouseEvent e, Point canvasLoc) {} + + /** + * Invoked when a key has been typed. + * See the class description for {@link KeyEvent} for a definition of + * a key typed event. */ - void mouseMoved(MouseEvent e, Point imageLocation); + default void keyTyped(KeyEvent e) {} + + /** + * Invoked when a key has been pressed. + * See the class description for {@link KeyEvent} for a definition of + * a key pressed event. + */ + default void keyPressed(KeyEvent e) {} + + /** + * Invoked when a key has been released. + * See the class description for {@link KeyEvent} for a definition of + * a key released event. + */ + default void keyReleased(KeyEvent e) {} + } diff --git a/src/main/java/com/defano/jmonet/canvas/paint/PaintFactory.java b/src/main/java/com/defano/jmonet/canvas/paint/PaintFactory.java index 5e5ee610..a0de0ef0 100644 --- a/src/main/java/com/defano/jmonet/canvas/paint/PaintFactory.java +++ b/src/main/java/com/defano/jmonet/canvas/paint/PaintFactory.java @@ -7,6 +7,7 @@ /** * A utility for producing {@link Paint} in commonly-needed colors and textures. */ +@SuppressWarnings("unused") public class PaintFactory { /** diff --git a/src/main/java/com/defano/jmonet/canvas/surface/AbstractPaintSurface.java b/src/main/java/com/defano/jmonet/canvas/surface/AbstractPaintSurface.java index 76386ce2..87a310a6 100644 --- a/src/main/java/com/defano/jmonet/canvas/surface/AbstractPaintSurface.java +++ b/src/main/java/com/defano/jmonet/canvas/surface/AbstractPaintSurface.java @@ -2,6 +2,8 @@ import com.defano.jmonet.canvas.layer.ScaledLayeredImage; import com.defano.jmonet.canvas.observable.SurfaceInteractionObserver; +import com.defano.jmonet.context.AwtGraphicsContext; +import com.defano.jmonet.context.GraphicsContext; import io.reactivex.Observable; import io.reactivex.subjects.BehaviorSubject; @@ -13,14 +15,18 @@ import java.util.List; /** - * A surface that paints a layered image. + * A JComponent that renders a {@code LayeredImage} when painted, and that registers + * itself as a listener of mouse and key events delegating interesting events to a set of + * {@link SurfaceInteractionObserver} objects. */ public abstract class AbstractPaintSurface extends JComponent implements PaintSurface, KeyListener, MouseListener, MouseMotionListener, KeyEventDispatcher, ScaledLayeredImage { private final static Color CLEAR_COLOR = new Color(0, 0, 0, 0); + private final BehaviorSubject scaleSubject = BehaviorSubject.createDefault(1.0); private final List interactionListeners = new ArrayList<>(); + private Dimension surfaceDimension = new Dimension(); private double scanlineThreadhold = 6.0; private Color scanlineColor = new Color(0xF5, 0xF5, 0xF5); @@ -121,8 +127,8 @@ public void setScale(double scale) { // Modify scroll position to "zoom in" or "zoom out" on the center of the previous scroll view bounds if (scrollController != null) { scrollController.setScrollPosition(new Point( - Math.max(0, (int) ((prevScrollRect.x + prevScrollRect.width / 2) * scale / prevScale - prevScrollRect.width / 2)), - Math.max(0, (int) ((prevScrollRect.y + prevScrollRect.height / 2) * scale / prevScale - prevScrollRect.height / 2)) + Math.max(0, (int) ((prevScrollRect.x + prevScrollRect.width / 2.0) * scale / prevScale - prevScrollRect.width / 2.0)), + Math.max(0, (int) ((prevScrollRect.y + prevScrollRect.height / 2.0) * scale / prevScale - prevScrollRect.height / 2.0)) )); } @@ -130,6 +136,28 @@ public void setScale(double scale) { repaint(); } + /** + * {@inheritDoc} + */ + @Override + public void repaint(Rectangle r) { + + // Sub-region repainting not available when scanlines are rendered; must repaint entire surface + if (r == null || isScanlinesVisible()) { + super.repaint(); + } + + // When calculating sub-region, need to take scale into account + else { + double scale = getScale(); + super.repaint( + (int)(r.x * scale), + (int)(r.y * scale), + (int)(r.width * scale), + (int)(r.height * scale)); + } + } + /** * {@inheritDoc} */ @@ -205,12 +233,11 @@ public void paintComponent(Graphics g) { super.paintComponent(g); Rectangle clip = g.getClipBounds(); - if (clip != null && !clip.isEmpty() && isVisible()) { - // Draw visible portion of this surface's image into a buffer + // Draw visible portion of this surface's image into a buffer (does not modify this graphics context) BufferedImage buffer = new BufferedImage(clip.width, clip.height, BufferedImage.TYPE_INT_ARGB); - Graphics2D g2d = buffer.createGraphics(); + GraphicsContext g2d = new AwtGraphicsContext(buffer.createGraphics()); g2d.setBackground(CLEAR_COLOR); g2d.clearRect(clip.x, clip.y, clip.width, clip.height); paint(g2d, getScale(), clip); @@ -224,7 +251,7 @@ public void paintComponent(Graphics g) { } // Draw the paint image - g.drawImage(buffer, clip.x, clip.y, null); + g.drawImage(buffer, clip.x, clip.y, clip.width, clip.height, null); } // DO NOT dispose the graphics context in this method. @@ -243,7 +270,15 @@ public void addSurfaceInteractionObserver(SurfaceInteractionObserver listener) { */ @Override public boolean removeSurfaceInteractionObserver(SurfaceInteractionObserver listener) { - return interactionListeners.remove(listener); + boolean removed = interactionListeners.remove(listener); + + Cursor nextCursor = interactionListeners.isEmpty() ? + Cursor.getDefaultCursor() : + interactionListeners.get(interactionListeners.size() - 1).getDefaultCursor(); + + SwingUtilities.invokeLater(() -> setCursor(nextCursor)); + + return removed; } /** diff --git a/src/main/java/com/defano/jmonet/canvas/surface/DefaultSurfaceScrollController.java b/src/main/java/com/defano/jmonet/canvas/surface/DefaultSurfaceScrollController.java index 254b43e0..b1d844d4 100644 --- a/src/main/java/com/defano/jmonet/canvas/surface/DefaultSurfaceScrollController.java +++ b/src/main/java/com/defano/jmonet/canvas/surface/DefaultSurfaceScrollController.java @@ -1,5 +1,7 @@ package com.defano.jmonet.canvas.surface; +import com.defano.jmonet.tools.MagnifierTool; + import javax.swing.*; import java.awt.*; @@ -7,7 +9,9 @@ import static javax.swing.ScrollPaneConstants.VERTICAL_SCROLLBAR_AS_NEEDED; /** - * Provides basic scroll behavior when an AbstractPaintSurface is the viewport of a {@link JScrollPane}. + * Provides basic scroll behavior when an AbstractPaintSurface is the viewport of a {@link JScrollPane}. This is + * required so that tools (like {@link MagnifierTool} can query and change the scroll position + * of the JScrollPane containing the canvas the tool is painting on. *

* This default implementation is sufficient for cases when the surface is added to a {@link JScrollPane} as the view * port. If the surface (canvas) is embedded is some container with additional components or decorations a custom diff --git a/src/main/java/com/defano/jmonet/canvas/surface/GridSurface.java b/src/main/java/com/defano/jmonet/canvas/surface/GridSurface.java index 8160df0b..cfcb3430 100644 --- a/src/main/java/com/defano/jmonet/canvas/surface/GridSurface.java +++ b/src/main/java/com/defano/jmonet/canvas/surface/GridSurface.java @@ -1,11 +1,9 @@ package com.defano.jmonet.canvas.surface; -import io.reactivex.Observable; -import io.reactivex.subjects.BehaviorSubject; - /** * A surface that supports a snap-to-grid property. */ +@SuppressWarnings("unused") public interface GridSurface { /** @@ -16,10 +14,10 @@ public interface GridSurface { void setGridSpacing(int grid); /** - * Gets an observable grid spacing property. + * Gets the grid spacing property. * - * @return The grid spacing {@link BehaviorSubject} + * @return The grid spacing */ - Observable getGridSpacingObservable(); + int getGridSpacing(); } diff --git a/src/main/java/com/defano/jmonet/canvas/surface/PaintSurface.java b/src/main/java/com/defano/jmonet/canvas/surface/PaintSurface.java index bace777c..6eee966d 100644 --- a/src/main/java/com/defano/jmonet/canvas/surface/PaintSurface.java +++ b/src/main/java/com/defano/jmonet/canvas/surface/PaintSurface.java @@ -8,9 +8,10 @@ * A component that can be painted and that provides methods for drawing at scale, snap-to-grid behavior, scrolling, * observables, and embedding Swing components. */ -public interface PaintSurface extends ScanlineSurface, GridSurface, SwingSurface, ObservableSurface, ScrollableSurface, - Disposable { - +@SuppressWarnings("unused") +public interface PaintSurface + extends ScanlineSurface, GridSurface, SwingSurface, ObservableSurface, ScrollableSurface, Disposable +{ /** * Specifies the un-scaled size of this painting surface. This determines the size of the image (document) that can * be painted by a user. This does not specify the size of the Swing component or otherwise adjust layout or @@ -28,10 +29,20 @@ public interface PaintSurface extends ScanlineSurface, GridSurface, SwingSurface Dimension getSurfaceDimension(); /** - * Causes the surface to be repainted by Swing. + * Causes the entire surface to be repainted by Swing. Note that repainting large regions is computationally + * expensive, whenever possible tools should repaint the smallest sub-region possible using the + * {@link #repaint(Rectangle)} method. */ void repaint(); + /** + * Causes a section of the surface to be repainted by Swing. This is the minimum rectangle that will be repainted; + * there is no + * + * @param r The region of this surface to be repainted. + */ + void repaint(Rectangle r); + /** * Determines if the canvas is visible. * diff --git a/src/main/java/com/defano/jmonet/canvas/surface/ScalableSurface.java b/src/main/java/com/defano/jmonet/canvas/surface/ScalableSurface.java index 01c8a42b..9d3672c7 100644 --- a/src/main/java/com/defano/jmonet/canvas/surface/ScalableSurface.java +++ b/src/main/java/com/defano/jmonet/canvas/surface/ScalableSurface.java @@ -73,6 +73,20 @@ default Rectangle scaleRectangle(Rectangle r) { ); } + default Rectangle convertViewRectToModel(Rectangle r) { + Point topLeft = convertViewPointToModel(r.getLocation()); + Point bottomRight = convertViewPointToModel(new Point(r.x + r.width, r.y + r.height)); + + return new Rectangle(topLeft.x, topLeft.y, bottomRight.x - topLeft.x, bottomRight.y - topLeft.y); + } + + default Rectangle convertModelRectToView(Rectangle r) { + Point topLeft = convertModelPointToView(r.getLocation()); + Point bottomRight = convertModelPointToView(new Point(r.x + r.width, r.y + r.height)); + + return new Rectangle(topLeft.x, topLeft.y, bottomRight.x - topLeft.x, bottomRight.y - topLeft.y); + } + /** * Converts a point on the visible surface (view) to the equivalent point within the surface's image (model), * taking into account scale, grid and scroll pane complications as appropriate. diff --git a/src/main/java/com/defano/jmonet/canvas/surface/ScanlineSurface.java b/src/main/java/com/defano/jmonet/canvas/surface/ScanlineSurface.java index f2c4766d..d87a8860 100644 --- a/src/main/java/com/defano/jmonet/canvas/surface/ScanlineSurface.java +++ b/src/main/java/com/defano/jmonet/canvas/surface/ScanlineSurface.java @@ -1,15 +1,17 @@ package com.defano.jmonet.canvas.surface; +import com.defano.jmonet.context.GraphicsContext; + import java.awt.*; /** - * A surface that supports painting "scan lines", that is, a thin line separating rows and columns of pixels that, + * A surface that supports painting "scan lines", that is, a thin line separating rows and columns of pixels that * when visible, produces a matrix effect. *

* Scan lines are typically visible only once the scale factor of an image exceeds some threshold (as rendering - * scanlines with without scaling results in all scan lines and no image pixels). + * scanlines with without scaling would result in all scan lines and no image pixels). */ -@SuppressWarnings("SpellCheckingInspection") +@SuppressWarnings({"SpellCheckingInspection", "unused"}) public interface ScanlineSurface extends ScalableSurface { /** @@ -57,16 +59,24 @@ public interface ScanlineSurface extends ScalableSurface { */ void setScanlineComposite(AlphaComposite scanlineComposite); + /** + * Determines if scanlines are presently being rendered (based on the current scale and scanline threshold) + * @return True if scanlines are visible; false otherwise + */ + default boolean isScanlinesVisible() { + return getScale() > getScanlineScaleThreadhold(); + } + /** * Paints scanlines on the given graphics context. * * @param g The graphics context * @param size The size of region onto which scanlines should extend */ - default void paintScanlines(Graphics2D g, Dimension size) { - double scale = getScale(); + default void paintScanlines(GraphicsContext g, Dimension size) { + if (isScanlinesVisible()) { + double scale = getScale(); - if (scale > getScanlineScaleThreadhold()) { g.setPaint(getScanlineColor()); g.setComposite(getScanlineComposite()); g.setStroke(new BasicStroke(1)); diff --git a/src/main/java/com/defano/jmonet/canvas/surface/SwingSurface.java b/src/main/java/com/defano/jmonet/canvas/surface/SwingSurface.java index 848f4b79..7dcb531e 100644 --- a/src/main/java/com/defano/jmonet/canvas/surface/SwingSurface.java +++ b/src/main/java/com/defano/jmonet/canvas/surface/SwingSurface.java @@ -1,20 +1,36 @@ package com.defano.jmonet.canvas.surface; +import javax.swing.*; import java.awt.*; /** * A surface to which Swing components can be added and removed. */ public interface SwingSurface { + /** * Adds a component to the surface. + * * @param component The component to be added. */ void addComponent(Component component); /** * Removes a component from the surface; has no effect if the given component is not a child of this surface. + * * @param component The component to remove */ void removeComponent(Component component); + + /** + * Returns the ActionMap used to determine what + * Action to fire for particular KeyStroke + * binding. The returned ActionMap, unless otherwise + * set, will have the ActionMap from the UI set as the parent. + * + * @return the ActionMap containing the key/action bindings + */ + ActionMap getActionMap(); + + Component getComponent(); } diff --git a/src/main/java/com/defano/jmonet/clipboard/CanvasClipboardActionListener.java b/src/main/java/com/defano/jmonet/clipboard/CanvasClipboardActionListener.java index d49a9723..4fc400cd 100644 --- a/src/main/java/com/defano/jmonet/clipboard/CanvasClipboardActionListener.java +++ b/src/main/java/com/defano/jmonet/clipboard/CanvasClipboardActionListener.java @@ -1,6 +1,6 @@ package com.defano.jmonet.clipboard; -import com.defano.jmonet.canvas.AbstractPaintCanvas; +import com.defano.jmonet.canvas.PaintCanvas; import javax.swing.*; import java.awt.event.ActionEvent; @@ -9,6 +9,7 @@ /** * A listener of clipboard-related actions (cut, copy and paste). */ +@SuppressWarnings("unused") public class CanvasClipboardActionListener implements ActionListener { private final CanvasFocusDelegate delegate; @@ -17,10 +18,14 @@ public CanvasClipboardActionListener(CanvasFocusDelegate delegate) { this.delegate = delegate; } + public CanvasClipboardActionListener() { + this(new DefaultCanvasFocusDelegate()); + } + /** {@inheritDoc} */ @Override public void actionPerformed(ActionEvent e) { - AbstractPaintCanvas focusedCanvas = delegate == null ? null : delegate.getCanvasInFocus(); + PaintCanvas focusedCanvas = delegate == null ? null : delegate.getCanvasInFocus(); if (focusedCanvas != null) { Action a = focusedCanvas.getActionMap().get(e.getActionCommand()); diff --git a/src/main/java/com/defano/jmonet/clipboard/CanvasFocusDelegate.java b/src/main/java/com/defano/jmonet/clipboard/CanvasFocusDelegate.java index 7b4f5091..ebe07ca3 100644 --- a/src/main/java/com/defano/jmonet/clipboard/CanvasFocusDelegate.java +++ b/src/main/java/com/defano/jmonet/clipboard/CanvasFocusDelegate.java @@ -1,16 +1,18 @@ package com.defano.jmonet.clipboard; -import com.defano.jmonet.canvas.AbstractPaintCanvas; +import com.defano.jmonet.canvas.PaintCanvas; /** - * A determiner of which JMonet canvas presently has focus. + * A determiner of which JMonet canvas presently has focus; used by the {@link CanvasClipboardActionListener} to + * indicate which canvas (if any) should receive actions. */ public interface CanvasFocusDelegate { + /** * Invoked to retrieve the canvas currently in focus, which should receive cut, copy and paste actions. * * @return The canvas that should receive cut, copy and paste commands, or null if no canvas is currently in * focus. */ - AbstractPaintCanvas getCanvasInFocus(); + PaintCanvas getCanvasInFocus(); } diff --git a/src/main/java/com/defano/jmonet/clipboard/CanvasTransferDelegate.java b/src/main/java/com/defano/jmonet/clipboard/CanvasTransferDelegate.java index 1ea68fdc..a9253c7d 100644 --- a/src/main/java/com/defano/jmonet/clipboard/CanvasTransferDelegate.java +++ b/src/main/java/com/defano/jmonet/clipboard/CanvasTransferDelegate.java @@ -1,6 +1,6 @@ package com.defano.jmonet.clipboard; -import com.defano.jmonet.tools.base.AbstractSelectionTool; +import com.defano.jmonet.tools.MarqueeTool; import java.awt.*; import java.awt.image.BufferedImage; @@ -12,7 +12,7 @@ public interface CanvasTransferDelegate { /** * Invoked when the user issued to "Copy" command. A typical implementation of this method will delegate - * to {@link AbstractSelectionTool#getSelectedImage()}. + * to {@link com.defano.jmonet.tools.base.SelectionTool#getSelectedImage()}. * * @return The image to be copied to the clipboard, or null if there is no selection to be copied. */ @@ -20,7 +20,7 @@ public interface CanvasTransferDelegate { /** * Invoked to delete the current selection as a result of completing a "Cut" command. A typical implementation - * of this method will delegate to {@link AbstractSelectionTool#deleteSelection()}. + * of this method will delegate to {@link com.defano.jmonet.tools.base.SelectionTool#deleteSelection()}. * * Note that a "Cut" command is comprised of a {@link #copySelection()} this method. */ @@ -28,8 +28,8 @@ public interface CanvasTransferDelegate { /** * Invoked to paste the given image onto the canvas. A typical implementation of this method might activate the - * {@link com.defano.jmonet.tools.SelectionTool} on the canvas, then invoke - * {@link com.defano.jmonet.tools.SelectionTool#createSelection(BufferedImage, Point)} to make the pasted image + * {@link MarqueeTool} on the canvas, then invoke + * {@link MarqueeTool#createSelection(BufferedImage, Point)} to make the pasted image * the current selection. * * @param image The image to paste onto the focused canvas. diff --git a/src/main/java/com/defano/jmonet/clipboard/CanvasTransferHandler.java b/src/main/java/com/defano/jmonet/clipboard/CanvasTransferHandler.java index b6bac7c7..9cb1c0c5 100644 --- a/src/main/java/com/defano/jmonet/clipboard/CanvasTransferHandler.java +++ b/src/main/java/com/defano/jmonet/clipboard/CanvasTransferHandler.java @@ -59,6 +59,11 @@ public boolean importData(TransferHandler.TransferSupport info) { return false; } + /** + * Converts an image to a BufferedImage of type ARGB. + * @param source The image to convert + * @return The resulting BufferedImage + */ private BufferedImage toBufferedImage(Image source) { if (source instanceof BufferedImage) { return (BufferedImage) source; @@ -76,39 +81,4 @@ private BufferedImage toBufferedImage(Image source) { } - private static class TransferableImage implements Transferable { - - private final BufferedImage image; - - private TransferableImage(BufferedImage image) { - this.image = image; - } - - public static TransferableImage from(BufferedImage image) { - return image == null ? null : new TransferableImage(image); - } - - /** {@inheritDoc} */ - @Override - public DataFlavor[] getTransferDataFlavors() { - return new DataFlavor[]{DataFlavor.imageFlavor}; - } - - /** {@inheritDoc} */ - @Override - public boolean isDataFlavorSupported(DataFlavor flavor) { - return flavor == DataFlavor.imageFlavor; - } - - /** {@inheritDoc} */ - @Override - public Object getTransferData(DataFlavor flavor) throws UnsupportedFlavorException { - if (flavor == DataFlavor.imageFlavor) { - return image; - } - - throw new UnsupportedFlavorException(flavor); - } - } - } diff --git a/src/main/java/com/defano/jmonet/clipboard/DefaultCanvasFocusDelegate.java b/src/main/java/com/defano/jmonet/clipboard/DefaultCanvasFocusDelegate.java new file mode 100644 index 00000000..7f95d845 --- /dev/null +++ b/src/main/java/com/defano/jmonet/clipboard/DefaultCanvasFocusDelegate.java @@ -0,0 +1,24 @@ +package com.defano.jmonet.clipboard; + +import com.defano.jmonet.canvas.PaintCanvas; + +import javax.swing.*; +import java.awt.*; + +/** + * A basic implementation of {@link CanvasFocusDelegate} that returns {@link PaintCanvas} component currently in focus + * (according to the {@link javax.swing.FocusManager}, or null, if no canvas currently has focus. + */ +public class DefaultCanvasFocusDelegate implements CanvasFocusDelegate { + + @Override + public PaintCanvas getCanvasInFocus() { + Component focusOwner = FocusManager.getCurrentManager().getFocusOwner(); + + if (focusOwner instanceof PaintCanvas) { + return (PaintCanvas) focusOwner; + } + + return null; + } +} diff --git a/src/main/java/com/defano/jmonet/clipboard/TransferableImage.java b/src/main/java/com/defano/jmonet/clipboard/TransferableImage.java new file mode 100644 index 00000000..0a5d2c9f --- /dev/null +++ b/src/main/java/com/defano/jmonet/clipboard/TransferableImage.java @@ -0,0 +1,50 @@ +package com.defano.jmonet.clipboard; + +import java.awt.datatransfer.DataFlavor; +import java.awt.datatransfer.Transferable; +import java.awt.datatransfer.UnsupportedFlavorException; +import java.awt.image.BufferedImage; + +/** + * A {@link Transferable} wrapper for BufferedImages, used to facilitate cut-copy-paste operations on JMonet images. + */ +class TransferableImage implements Transferable { + + private final BufferedImage image; + + private TransferableImage(BufferedImage image) { + this.image = image; + } + + /** + * Creates a TransferableImage from a standard BufferedImage. + * + * @param image The BufferedImage for transfer. + * @return The TransferableImage + */ + public static TransferableImage from(BufferedImage image) { + return image == null ? null : new TransferableImage(image); + } + + /** {@inheritDoc} */ + @Override + public DataFlavor[] getTransferDataFlavors() { + return new DataFlavor[]{DataFlavor.imageFlavor}; + } + + /** {@inheritDoc} */ + @Override + public boolean isDataFlavorSupported(DataFlavor flavor) { + return flavor == DataFlavor.imageFlavor; + } + + /** {@inheritDoc} */ + @Override + public Object getTransferData(DataFlavor flavor) throws UnsupportedFlavorException { + if (flavor == DataFlavor.imageFlavor) { + return image; + } + + throw new UnsupportedFlavorException(flavor); + } +} diff --git a/src/main/java/com/defano/jmonet/context/AwtGraphicsContext.java b/src/main/java/com/defano/jmonet/context/AwtGraphicsContext.java new file mode 100644 index 00000000..12b55e45 --- /dev/null +++ b/src/main/java/com/defano/jmonet/context/AwtGraphicsContext.java @@ -0,0 +1,692 @@ +package com.defano.jmonet.context; + +import com.defano.jmonet.model.Interpolation; + +import java.awt.*; +import java.awt.font.FontRenderContext; +import java.awt.font.GlyphVector; +import java.awt.geom.AffineTransform; +import java.awt.image.BufferedImage; +import java.awt.image.BufferedImageOp; +import java.awt.image.ImageObserver; +import java.awt.image.RenderedImage; +import java.awt.image.renderable.RenderableImage; +import java.text.AttributedCharacterIterator; +import java.util.Map; + +/** + * An implementation of {@link GraphicsContext} that delegates to an AWT {@link Graphics2D} object. + */ +public class AwtGraphicsContext implements GraphicsContext { + + private final Graphics2D g; + + public AwtGraphicsContext(Graphics2D g) { + this.g = g; + } + + /** + * {@inheritDoc} + */ + @Override + public void setAntialiasingMode(Interpolation mode) { + switch (mode) { + case NONE: + g.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_OFF); + g.setRenderingHint(RenderingHints.KEY_TEXT_ANTIALIASING, RenderingHints.VALUE_TEXT_ANTIALIAS_OFF); + break; + case DEFAULT: + g.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_DEFAULT); + g.setRenderingHint(RenderingHints.KEY_INTERPOLATION, RenderingHints.VALUE_INTERPOLATION_NEAREST_NEIGHBOR); + g.setRenderingHint(RenderingHints.KEY_TEXT_ANTIALIASING, RenderingHints.VALUE_TEXT_ANTIALIAS_DEFAULT); + break; + case NEAREST_NEIGHBOR: + g.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON); + g.setRenderingHint(RenderingHints.KEY_INTERPOLATION, RenderingHints.VALUE_INTERPOLATION_NEAREST_NEIGHBOR); + g.setRenderingHint(RenderingHints.KEY_TEXT_ANTIALIASING, RenderingHints.VALUE_TEXT_ANTIALIAS_ON); + break; + case BICUBIC: + g.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON); + g.setRenderingHint(RenderingHints.KEY_INTERPOLATION, RenderingHints.VALUE_INTERPOLATION_BICUBIC); + g.setRenderingHint(RenderingHints.KEY_TEXT_ANTIALIASING, RenderingHints.VALUE_TEXT_ANTIALIAS_ON); + break; + case BILINEAR: + g.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON); + g.setRenderingHint(RenderingHints.KEY_INTERPOLATION, RenderingHints.VALUE_INTERPOLATION_BILINEAR); + g.setRenderingHint(RenderingHints.KEY_TEXT_ANTIALIASING, RenderingHints.VALUE_TEXT_ANTIALIAS_ON); + break; + } + } + + /** + * {@inheritDoc} + */ + @Override + public void dispose() { + g.dispose(); + } + + /** + * {@inheritDoc} + */ + @Override + public void draw(Shape s) { + g.draw(s); + } + + /** + * {@inheritDoc} + */ + @Override + public boolean drawImage(Image img, AffineTransform xform, ImageObserver obs) { + return g.drawImage(img, xform, obs); + } + + /** + * {@inheritDoc} + */ + @Override + public void drawImage(BufferedImage img, BufferedImageOp op, int x, int y) { + g.drawImage(img, op, x, y); + } + + /** + * {@inheritDoc} + */ + @Override + public void drawRenderedImage(RenderedImage img, AffineTransform xform) { + g.drawRenderedImage(img, xform); + } + + /** + * {@inheritDoc} + */ + @Override + public void drawRenderableImage(RenderableImage img, AffineTransform xform) { + g.drawRenderableImage(img, xform); + } + + /** + * {@inheritDoc} + */ + @Override + public void drawString(String str, int x, int y) { + g.drawString(str, x, y); + } + + /** + * {@inheritDoc} + */ + @Override + public void drawString(String str, float x, float y) { + g.drawString(str, x, y); + } + + /** + * {@inheritDoc} + */ + @Override + public void drawString(AttributedCharacterIterator iterator, int x, int y) { + g.drawString(iterator, x, y); + } + + /** + * {@inheritDoc} + */ + @Override + public void drawString(AttributedCharacterIterator iterator, float x, float y) { + g.drawString(iterator, x, y); + } + + /** + * {@inheritDoc} + */ + @Override + public void drawGlyphVector(GlyphVector glyphVector, float x, float y) { + g.drawGlyphVector(glyphVector, x, y); + } + + /** + * {@inheritDoc} + */ + @Override + public void fill(Shape s) { + g.fill(s); + } + + /** + * {@inheritDoc} + */ + @Override + public boolean hit(Rectangle rect, Shape s, boolean onStroke) { + return g.hit(rect, s, onStroke); + } + + /** + * {@inheritDoc} + */ + @Override + public GraphicsConfiguration getDeviceConfiguration() { + return g.getDeviceConfiguration(); + } + + /** + * {@inheritDoc} + */ + @Override + public void setRenderingHint(RenderingHints.Key hintKey, Object hintValue) { + g.setRenderingHint(hintKey, hintValue); + } + + /** + * {@inheritDoc} + */ + @Override + public Object getRenderingHint(RenderingHints.Key hintKey) { + return g.getRenderingHint(hintKey); + } + + /** + * {@inheritDoc} + */ + @Override + public void addRenderingHints(Map hints) { + g.addRenderingHints(hints); + } + + /** + * {@inheritDoc} + */ + @Override + public RenderingHints getRenderingHints() { + return g.getRenderingHints(); + } + + /** + * {@inheritDoc} + */ + @Override + public void setRenderingHints(Map hints) { + g.setRenderingHints(hints); + } + + /** + * {@inheritDoc} + */ + @Override + public void translate(int x, int y) { + g.translate(x, y); + } + + /** + * {@inheritDoc} + */ + @Override + public void translate(double tx, double ty) { + g.translate(tx, ty); + } + + /** + * {@inheritDoc} + */ + @Override + public void rotate(double theta) { + g.rotate(theta); + } + + /** + * {@inheritDoc} + */ + @Override + public void rotate(double theta, double x, double y) { + g.rotate(theta, x, y); + } + + /** + * {@inheritDoc} + */ + @Override + public void scale(double sx, double sy) { + g.scale(sx, sy); + } + + /** + * {@inheritDoc} + */ + @Override + public void shear(double shx, double shy) { + g.shear(shx, shy); + } + + /** + * {@inheritDoc} + */ + @Override + public void transform(AffineTransform tx) { + g.transform(tx); + } + + /** + * {@inheritDoc} + */ + @Override + public AffineTransform getTransform() { + return g.getTransform(); + } + + /** + * {@inheritDoc} + */ + @Override + public void setTransform(AffineTransform tx) { + g.setTransform(tx); + } + + /** + * {@inheritDoc} + */ + @Override + public Paint getPaint() { + return g.getPaint(); + } + + /** + * {@inheritDoc} + */ + @Override + public void setPaint(Paint paint) { + g.setPaint(paint); + } + + /** + * {@inheritDoc} + */ + @Override + public Composite getComposite() { + return g.getComposite(); + } + + /** + * {@inheritDoc} + */ + @Override + public void setComposite(Composite comp) { + g.setComposite(comp); + } + + /** + * {@inheritDoc} + */ + @Override + public Color getBackground() { + return g.getBackground(); + } + + /** + * {@inheritDoc} + */ + @Override + public void setBackground(Color color) { + g.setBackground(color); + } + + /** + * {@inheritDoc} + */ + @Override + public Stroke getStroke() { + return g.getStroke(); + } + + /** + * {@inheritDoc} + */ + @Override + public void setStroke(Stroke s) { + g.setStroke(s); + } + + /** + * {@inheritDoc} + */ + @Override + public void clip(Shape s) { + g.clip(s); + } + + /** + * {@inheritDoc} + */ + @Override + public FontRenderContext getFontRenderContext() { + return g.getFontRenderContext(); + } + + /** + * {@inheritDoc} + */ + @Override + public Color getColor() { + return g.getColor(); + } + + /** + * {@inheritDoc} + */ + @Override + public void setColor(Color c) { + g.setColor(c); + } + + /** + * {@inheritDoc} + */ + @Override + public void setPaintMode() { + g.setPaintMode(); + } + + /** + * {@inheritDoc} + */ + @Override + public void setXORMode(Color c1) { + g.setXORMode(c1); + } + + /** + * {@inheritDoc} + */ + @Override + public Font getFont() { + return g.getFont(); + } + + /** + * {@inheritDoc} + */ + @Override + public void setFont(Font font) { + g.setFont(font); + } + + /** + * {@inheritDoc} + */ + @Override + public FontMetrics getFontMetrics() { + return g.getFontMetrics(); + } + + /** + * {@inheritDoc} + */ + @Override + public FontMetrics getFontMetrics(Font f) { + return g.getFontMetrics(f); + } + + /** + * {@inheritDoc} + */ + @Override + public Rectangle getClipBounds() { + return g.getClipBounds(); + } + + /** + * {@inheritDoc} + */ + @Override + public void clipRect(int x, int y, int width, int height) { + g.clipRect(x, y, width, height); + } + + /** + * {@inheritDoc} + */ + @Override + public void setClip(int x, int y, int width, int height) { + g.setClip(x, y, width, height); + } + + /** + * {@inheritDoc} + */ + @Override + public Shape getClip() { + return g.getClip(); + } + + /** + * {@inheritDoc} + */ + @Override + public void setClip(Shape clip) { + g.setClip(clip); + } + + /** + * {@inheritDoc} + */ + @Override + public void copyArea(int x, int y, int width, int height, int dx, int dy) { + g.copyArea(x, y, width, height, dx, dy); + } + + /** + * {@inheritDoc} + */ + @Override + public void drawLine(int x1, int y1, int x2, int y2) { + g.drawLine(x1, y1, x2, y2); + } + + /** + * {@inheritDoc} + */ + @Override + public void fillRect(int x, int y, int width, int height) { + g.fillRect(x, y, width, height); + } + + /** + * {@inheritDoc} + */ + @Override + public void drawRect(int x, int y, int width, int height) { + g.drawRect(x, y, width, height); + } + + /** + * {@inheritDoc} + */ + @Override + public void clearRect(int x, int y, int width, int height) { + g.clearRect(x, y, width, height); + } + + /** + * {@inheritDoc} + */ + @Override + public void drawRoundRect(int x, int y, int width, int height, int arcWidth, int arcHeight) { + g.drawRoundRect(x, y, width, height, arcWidth, arcHeight); + } + + /** + * {@inheritDoc} + */ + @Override + public void fillRoundRect(int x, int y, int width, int height, int arcWidth, int arcHeight) { + g.fillRoundRect(x, y, width, height, arcWidth, arcHeight); + } + + /** + * {@inheritDoc} + */ + @Override + public void draw3DRect(int x, int y, int width, int height, boolean raised) { + g.draw3DRect(x, y, width, height, raised); + } + + /** + * {@inheritDoc} + */ + @Override + public void fill3DRect(int x, int y, int width, int height, boolean raised) { + g.fill3DRect(x, y, width, height, raised); + } + + /** + * {@inheritDoc} + */ + @Override + public void drawOval(int x, int y, int width, int height) { + g.drawOval(x, y, width, height); + } + + /** + * {@inheritDoc} + */ + @Override + public void fillOval(int x, int y, int width, int height) { + g.fillOval(x, y, width, height); + } + + /** + * {@inheritDoc} + */ + @Override + public void drawArc(int x, int y, int width, int height, int startAngle, int arcAngle) { + g.drawArc(x, y, width, height, startAngle, arcAngle); + } + + /** + * {@inheritDoc} + */ + @Override + public void fillArc(int x, int y, int width, int height, int startAngle, int arcAngle) { + g.fillArc(x, y, width, height, startAngle, arcAngle); + } + + /** + * {@inheritDoc} + */ + @Override + public void drawPolyline(int[] xPoints, int[] yPoints, int nPoints) { + g.drawPolyline(xPoints, yPoints, nPoints); + } + + /** + * {@inheritDoc} + */ + @Override + public void drawPolygon(int[] xPoints, int[] yPoints, int nPoints) { + g.drawPolygon(xPoints, yPoints, nPoints); + } + + /** + * {@inheritDoc} + */ + @Override + public void drawPolygon(Polygon p) { + g.drawPolygon(p); + } + + /** + * {@inheritDoc} + */ + @Override + public void fillPolygon(int[] xPoints, int[] yPoints, int nPoints) { + g.fillPolygon(xPoints, yPoints, nPoints); + } + + /** + * {@inheritDoc} + */ + @Override + public void fillPolygon(Polygon p) { + g.fillPolygon(p); + } + + /** + * {@inheritDoc} + */ + @Override + public void drawChars(char[] data, int offset, int length, int x, int y) { + g.drawChars(data, offset, length, x, y); + } + + /** + * {@inheritDoc} + */ + @Override + public void drawBytes(byte[] data, int offset, int length, int x, int y) { + g.drawBytes(data, offset, length, x, y); + } + + /** + * {@inheritDoc} + */ + @Override + public boolean drawImage(Image img, int x, int y, ImageObserver observer) { + return g.drawImage(img, x, y, observer); + } + + /** + * {@inheritDoc} + */ + @Override + public boolean drawImage(Image img, int x, int y, int width, int height, ImageObserver observer) { + return g.drawImage(img, x, y, width, height, observer); + } + + /** + * {@inheritDoc} + */ + @Override + public boolean drawImage(Image img, int x, int y, Color bgcolor, ImageObserver observer) { + return g.drawImage(img, x, y, bgcolor, observer); + } + + /** + * {@inheritDoc} + */ + @Override + public boolean drawImage(Image img, int x, int y, int width, int height, Color bgcolor, ImageObserver observer) { + return g.drawImage(img, x, y, width, height, bgcolor, observer); + } + + /** + * {@inheritDoc} + */ + @Override + public boolean drawImage(Image img, int dx1, int dy1, int dx2, int dy2, int sx1, int sy1, int sx2, int sy2, ImageObserver observer) { + return g.drawImage(img, dx1, dy1, dx2, dy2, sx1, sy1, sx2, sy2, observer); + } + + /** + * {@inheritDoc} + */ + @Override + public boolean drawImage(Image img, int dx1, int dy1, int dx2, int dy2, int sx1, int sy1, int sx2, int sy2, Color bgcolor, ImageObserver observer) { + return g.drawImage(img, dx1, dy1, dx2, dy2, sx1, sy1, sx2, sy2, bgcolor, observer); + } + + /** + * {@inheritDoc} + */ + @Override + public boolean hitClip(int x, int y, int width, int height) { + return g.hitClip(x, y, width, height); + } + + /** + * {@inheritDoc} + */ + @Override + public Rectangle getClipBounds(Rectangle r) { + return g.getClipBounds(r); + } +} diff --git a/src/main/java/com/defano/jmonet/context/GraphicsContext.java b/src/main/java/com/defano/jmonet/context/GraphicsContext.java new file mode 100644 index 00000000..5a38677c --- /dev/null +++ b/src/main/java/com/defano/jmonet/context/GraphicsContext.java @@ -0,0 +1,1767 @@ +package com.defano.jmonet.context; + +import com.defano.jmonet.model.Interpolation; + +import java.awt.*; +import java.awt.font.FontRenderContext; +import java.awt.font.GlyphVector; +import java.awt.font.TextAttribute; +import java.awt.geom.AffineTransform; +import java.awt.image.BufferedImage; +import java.awt.image.BufferedImageOp; +import java.awt.image.ImageObserver; +import java.awt.image.RenderedImage; +import java.awt.image.renderable.RenderableImage; +import java.text.AttributedCharacterIterator; +import java.util.Map; + +/** + * A proxy/adapter to {@link Graphics2D} used to improve testability. Provides access to all methods in a wrapped + * {@link Graphics2D} object (including inherited methods from {@link Graphics}). + */ +@SuppressWarnings({"unused", "CStyleArrayDeclaration"}) +public interface GraphicsContext { + + /** + * Applies the antialiasing interpolation mode to this graphics context. See {@link Interpolation} for details + * about the different modes. + * + * @param mode The antialiasing interpolation mode. + */ + void setAntialiasingMode(Interpolation mode); + + /** + * Strokes the outline of a Shape using the settings of the + * current Graphics2D context. The rendering attributes + * applied include the Clip, Transform, + * Paint, Composite and + * Stroke attributes. + * + * @param s the Shape to be rendered + * @see #setStroke + * @see #setPaint + * @see java.awt.Graphics#setColor + * @see #transform + * @see #setTransform + * @see #clip + * @see #setClip + * @see #setComposite + */ + void draw(Shape s); + + /** + * Renders an image, applying a transform from image space into user space + * before drawing. + * The transformation from user space into device space is done with + * the current Transform in the Graphics2D. + * The specified transformation is applied to the image before the + * transform attribute in the Graphics2D context is applied. + * The rendering attributes applied include the Clip, + * Transform, and Composite attributes. + * Note that no rendering is done if the specified transform is + * noninvertible. + * + * @param img the specified image to be rendered. + * This method does nothing if img is null. + * @param xform the transformation from image space into user space + * @param obs the {@link ImageObserver} + * to be notified as more of the Image + * is converted + * @return true if the Image is + * fully loaded and completely rendered, or if it's null; + * false if the Image is still being loaded. + * @see #transform + * @see #setTransform + * @see #setComposite + * @see #clip + * @see #setClip + */ + boolean drawImage(Image img, + AffineTransform xform, + ImageObserver obs); + + /** + * Renders a BufferedImage that is + * filtered with a + * {@link BufferedImageOp}. + * The rendering attributes applied include the Clip, + * Transform + * and Composite attributes. This is equivalent to: + *

+     * img1 = op.filter(img, null);
+     * drawImage(img1, new AffineTransform(1f,0f,0f,1f,x,y), null);
+     * 
+ * + * @param op the filter to be applied to the image before rendering + * @param img the specified BufferedImage to be rendered. + * This method does nothing if img is null. + * @param x the x coordinate of the location in user space where + * the upper left corner of the image is rendered + * @param y the y coordinate of the location in user space where + * the upper left corner of the image is rendered + * @see #transform + * @see #setTransform + * @see #setComposite + * @see #clip + * @see #setClip + */ + void drawImage(BufferedImage img, + BufferedImageOp op, + int x, + int y); + + /** + * Renders a {@link RenderedImage}, + * applying a transform from image + * space into user space before drawing. + * The transformation from user space into device space is done with + * the current Transform in the Graphics2D. + * The specified transformation is applied to the image before the + * transform attribute in the Graphics2D context is applied. + * The rendering attributes applied include the Clip, + * Transform, and Composite attributes. Note + * that no rendering is done if the specified transform is + * noninvertible. + * + * @param img the image to be rendered. This method does + * nothing if img is null. + * @param xform the transformation from image space into user space + * @see #transform + * @see #setTransform + * @see #setComposite + * @see #clip + * @see #setClip + */ + void drawRenderedImage(RenderedImage img, + AffineTransform xform); + + /** + * Renders a + * {@link RenderableImage}, + * applying a transform from image space into user space before drawing. + * The transformation from user space into device space is done with + * the current Transform in the Graphics2D. + * The specified transformation is applied to the image before the + * transform attribute in the Graphics2D context is applied. + * The rendering attributes applied include the Clip, + * Transform, and Composite attributes. Note + * that no rendering is done if the specified transform is + * noninvertible. + *

+ * Rendering hints set on the Graphics2D object might + * be used in rendering the RenderableImage. + * If explicit control is required over specific hints recognized by a + * specific RenderableImage, or if knowledge of which hints + * are used is required, then a RenderedImage should be + * obtained directly from the RenderableImage + * and rendered using + * {@link #drawRenderedImage(RenderedImage, AffineTransform) drawRenderedImage}. + * + * @param img the image to be rendered. This method does + * nothing if img is null. + * @param xform the transformation from image space into user space + * @see #transform + * @see #setTransform + * @see #setComposite + * @see #clip + * @see #setClip + * @see #drawRenderedImage + */ + void drawRenderableImage(RenderableImage img, + AffineTransform xform); + + /** + * Renders the text of the specified String, using the + * current text attribute state in the Graphics2D context. + * The baseline of the + * first character is at position (xy) in + * the User Space. + * The rendering attributes applied include the Clip, + * Transform, Paint, Font and + * Composite attributes. For characters in script + * systems such as Hebrew and Arabic, the glyphs can be rendered from + * right to left, in which case the coordinate supplied is the + * location of the leftmost character on the baseline. + * + * @param str the string to be rendered + * @param x the x coordinate of the location where the + * String should be rendered + * @param y the y coordinate of the location where the + * String should be rendered + * @throws NullPointerException if str is + * null + * @see java.awt.Graphics#drawBytes + * @see java.awt.Graphics#drawChars + * @since JDK1.0 + */ + void drawString(String str, int x, int y); + + /** + * Renders the text specified by the specified String, + * using the current text attribute state in the Graphics2D context. + * The baseline of the first character is at position + * (xy) in the User Space. + * The rendering attributes applied include the Clip, + * Transform, Paint, Font and + * Composite attributes. For characters in script systems + * such as Hebrew and Arabic, the glyphs can be rendered from right to + * left, in which case the coordinate supplied is the location of the + * leftmost character on the baseline. + * + * @param str the String to be rendered + * @param x the x coordinate of the location where the + * String should be rendered + * @param y the y coordinate of the location where the + * String should be rendered + * @throws NullPointerException if str is + * null + * @see #setPaint + * @see java.awt.Graphics#setColor + * @see java.awt.Graphics#setFont + * @see #setTransform + * @see #setComposite + * @see #setClip + */ + void drawString(String str, float x, float y); + + /** + * Renders the text of the specified iterator applying its attributes + * in accordance with the specification of the {@link TextAttribute} class. + *

+ * The baseline of the first character is at position + * (xy) in User Space. + * For characters in script systems such as Hebrew and Arabic, + * the glyphs can be rendered from right to left, in which case the + * coordinate supplied is the location of the leftmost character + * on the baseline. + * + * @param iterator the iterator whose text is to be rendered + * @param x the x coordinate where the iterator's text is to be + * rendered + * @param y the y coordinate where the iterator's text is to be + * rendered + * @throws NullPointerException if iterator is + * null + * @see #setPaint + * @see java.awt.Graphics#setColor + * @see #setTransform + * @see #setComposite + * @see #setClip + */ + void drawString(AttributedCharacterIterator iterator, + int x, int y); + + /** + * Renders the text of the specified iterator applying its attributes + * in accordance with the specification of the {@link TextAttribute} class. + *

+ * The baseline of the first character is at position + * (xy) in User Space. + * For characters in script systems such as Hebrew and Arabic, + * the glyphs can be rendered from right to left, in which case the + * coordinate supplied is the location of the leftmost character + * on the baseline. + * + * @param iterator the iterator whose text is to be rendered + * @param x the x coordinate where the iterator's text is to be + * rendered + * @param y the y coordinate where the iterator's text is to be + * rendered + * @throws NullPointerException if iterator is + * null + * @see #setPaint + * @see java.awt.Graphics#setColor + * @see #setTransform + * @see #setComposite + * @see #setClip + */ + void drawString(AttributedCharacterIterator iterator, + float x, float y); + + /** + * Renders the text of the specified + * {@link GlyphVector} using + * the Graphics2D context's rendering attributes. + * The rendering attributes applied include the Clip, + * Transform, Paint, and + * Composite attributes. The GlyphVector + * specifies individual glyphs from a {@link Font}. + * The GlyphVector can also contain the glyph positions. + * This is the fastest way to render a set of characters to the + * screen. + * + * @param g the GlyphVector to be rendered + * @param x the x position in User Space where the glyphs should + * be rendered + * @param y the y position in User Space where the glyphs should + * be rendered + * @throws NullPointerException if g is null. + * @see java.awt.Font#createGlyphVector + * @see java.awt.font.GlyphVector + * @see #setPaint + * @see java.awt.Graphics#setColor + * @see #setTransform + * @see #setComposite + * @see #setClip + */ + void drawGlyphVector(GlyphVector g, float x, float y); + + /** + * Fills the interior of a Shape using the settings of the + * Graphics2D context. The rendering attributes applied + * include the Clip, Transform, + * Paint, and Composite. + * + * @param s the Shape to be filled + * @see #setPaint + * @see java.awt.Graphics#setColor + * @see #transform + * @see #setTransform + * @see #setComposite + * @see #clip + * @see #setClip + */ + void fill(Shape s); + + /** + * Checks whether or not the specified Shape intersects + * the specified {@link Rectangle}, which is in device + * space. If onStroke is false, this method checks + * whether or not the interior of the specified Shape + * intersects the specified Rectangle. If + * onStroke is true, this method checks + * whether or not the Stroke of the specified + * Shape outline intersects the specified + * Rectangle. + * The rendering attributes taken into account include the + * Clip, Transform, and Stroke + * attributes. + * + * @param rect the area in device space to check for a hit + * @param s the Shape to check for a hit + * @param onStroke flag used to choose between testing the + * stroked or the filled shape. If the flag is true, the + * Stroke outline is tested. If the flag is + * false, the filled Shape is tested. + * @return true if there is a hit; false + * otherwise. + * @see #setStroke + * @see #fill + * @see #draw + * @see #transform + * @see #setTransform + * @see #clip + */ + boolean hit(Rectangle rect, + Shape s, + boolean onStroke); + + /** + * Returns the device configuration associated with this + * Graphics2D. + * + * @return the device configuration of this Graphics2D. + */ + GraphicsConfiguration getDeviceConfiguration(); + + /** + * Sets the value of a single preference for the rendering algorithms. + * Hint categories include controls for rendering quality and overall + * time/quality trade-off in the rendering process. Refer to the + * RenderingHints class for definitions of some common + * keys and values. + * + * @param hintKey the key of the hint to be set. + * @param hintValue the value indicating preferences for the specified + * hint category. + * @see #getRenderingHint(RenderingHints.Key) + * @see RenderingHints + */ + void setRenderingHint(RenderingHints.Key hintKey, Object hintValue); + + /** + * Returns the value of a single preference for the rendering algorithms. + * Hint categories include controls for rendering quality and overall + * time/quality trade-off in the rendering process. Refer to the + * RenderingHints class for definitions of some common + * keys and values. + * + * @param hintKey the key corresponding to the hint to get. + * @return an object representing the value for the specified hint key. + * Some of the keys and their associated values are defined in the + * RenderingHints class. + * @see RenderingHints + * @see #setRenderingHint(RenderingHints.Key, Object) + */ + Object getRenderingHint(RenderingHints.Key hintKey); + + /** + * Sets the values of an arbitrary number of preferences for the + * rendering algorithms. + * Only values for the rendering hints that are present in the + * specified Map object are modified. + * All other preferences not present in the specified + * object are left unmodified. + * Hint categories include controls for rendering quality and + * overall time/quality trade-off in the rendering process. + * Refer to the RenderingHints class for definitions of + * some common keys and values. + * + * @param hints the rendering hints to be set + * @see RenderingHints + */ + void addRenderingHints(Map hints); + + /** + * Gets the preferences for the rendering algorithms. Hint categories + * include controls for rendering quality and overall time/quality + * trade-off in the rendering process. + * Returns all of the hint key/value pairs that were ever specified in + * one operation. Refer to the + * RenderingHints class for definitions of some common + * keys and values. + * + * @return a reference to an instance of RenderingHints + * that contains the current preferences. + * @see RenderingHints + * @see #setRenderingHints(Map) + */ + RenderingHints getRenderingHints(); + + /** + * Replaces the values of all preferences for the rendering + * algorithms with the specified hints. + * The existing values for all rendering hints are discarded and + * the new set of known hints and values are initialized from the + * specified {@link Map} object. + * Hint categories include controls for rendering quality and + * overall time/quality trade-off in the rendering process. + * Refer to the RenderingHints class for definitions of + * some common keys and values. + * + * @param hints the rendering hints to be set + * @see #getRenderingHints + * @see RenderingHints + */ + void setRenderingHints(Map hints); + + /** + * Translates the origin of the Graphics2D context to the + * point (xy) in the current coordinate system. + * Modifies the Graphics2D context so that its new origin + * corresponds to the point (xy) in the + * Graphics2D context's former coordinate system. All + * coordinates used in subsequent rendering operations on this graphics + * context are relative to this new origin. + * + * @param x the specified x coordinate + * @param y the specified y coordinate + * @since JDK1.0 + */ + void translate(int x, int y); + + /** + * Concatenates the current + * Graphics2D Transform + * with a translation transform. + * Subsequent rendering is translated by the specified + * distance relative to the previous position. + * This is equivalent to calling transform(T), where T is an + * AffineTransform represented by the following matrix: + *

+     *          [   1    0    tx  ]
+     *          [   0    1    ty  ]
+     *          [   0    0    1   ]
+     * 
+ * + * @param tx the distance to translate along the x-axis + * @param ty the distance to translate along the y-axis + */ + void translate(double tx, double ty); + + /** + * Concatenates the current Graphics2D + * Transform with a rotation transform. + * Subsequent rendering is rotated by the specified radians relative + * to the previous origin. + * This is equivalent to calling transform(R), where R is an + * AffineTransform represented by the following matrix: + *
+     *          [   cos(theta)    -sin(theta)    0   ]
+     *          [   sin(theta)     cos(theta)    0   ]
+     *          [       0              0         1   ]
+     * 
+ * Rotating with a positive angle theta rotates points on the positive + * x axis toward the positive y axis. + * + * @param theta the angle of rotation in radians + */ + void rotate(double theta); + + /** + * Concatenates the current Graphics2D + * Transform with a translated rotation + * transform. Subsequent rendering is transformed by a transform + * which is constructed by translating to the specified location, + * rotating by the specified radians, and translating back by the same + * amount as the original translation. This is equivalent to the + * following sequence of calls: + *
+     *          translate(x, y);
+     *          rotate(theta);
+     *          translate(-x, -y);
+     * 
+ * Rotating with a positive angle theta rotates points on the positive + * x axis toward the positive y axis. + * + * @param theta the angle of rotation in radians + * @param x the x coordinate of the origin of the rotation + * @param y the y coordinate of the origin of the rotation + */ + void rotate(double theta, double x, double y); + + /** + * Concatenates the current Graphics2D + * Transform with a scaling transformation + * Subsequent rendering is resized according to the specified scaling + * factors relative to the previous scaling. + * This is equivalent to calling transform(S), where S is an + * AffineTransform represented by the following matrix: + *
+     *          [   sx   0    0   ]
+     *          [   0    sy   0   ]
+     *          [   0    0    1   ]
+     * 
+ * + * @param sx the amount by which X coordinates in subsequent + * rendering operations are multiplied relative to previous + * rendering operations. + * @param sy the amount by which Y coordinates in subsequent + * rendering operations are multiplied relative to previous + * rendering operations. + */ + void scale(double sx, double sy); + + /** + * Concatenates the current Graphics2D + * Transform with a shearing transform. + * Subsequent renderings are sheared by the specified + * multiplier relative to the previous position. + * This is equivalent to calling transform(SH), where SH + * is an AffineTransform represented by the following + * matrix: + *
+     *          [   1   shx   0   ]
+     *          [  shy   1    0   ]
+     *          [   0    0    1   ]
+     * 
+ * + * @param shx the multiplier by which coordinates are shifted in + * the positive X axis direction as a function of their Y coordinate + * @param shy the multiplier by which coordinates are shifted in + * the positive Y axis direction as a function of their X coordinate + */ + void shear(double shx, double shy); + + /** + * Composes an AffineTransform object with the + * Transform in this Graphics2D according + * to the rule last-specified-first-applied. If the current + * Transform is Cx, the result of composition + * with Tx is a new Transform Cx'. Cx' becomes the + * current Transform for this Graphics2D. + * Transforming a point p by the updated Transform Cx' is + * equivalent to first transforming p by Tx and then transforming + * the result by the original Transform Cx. In other + * words, Cx'(p) = Cx(Tx(p)). A copy of the Tx is made, if necessary, + * so further modifications to Tx do not affect rendering. + * + * @param Tx the AffineTransform object to be composed with + * the current Transform + * @see #setTransform + * @see AffineTransform + */ + void transform(AffineTransform Tx); + + /** + * Returns a copy of the current Transform in the + * Graphics2D context. + * + * @return the current AffineTransform in the + * Graphics2D context. + * @see #transform + * @see #setTransform + */ + AffineTransform getTransform(); + + /** + * Overwrites the Transform in the Graphics2D context. + * WARNING: This method should never be used to apply a new + * coordinate transform on top of an existing transform because the + * Graphics2D might already have a transform that is + * needed for other purposes, such as rendering Swing + * components or applying a scaling transformation to adjust for the + * resolution of a printer. + *

To add a coordinate transform, use the + * transform, rotate, scale, + * or shear methods. The setTransform + * method is intended only for restoring the original + * Graphics2D transform after rendering, as shown in this + * example: + *

+     * // Get the current transform
+     * AffineTransform saveAT = g2.getTransform();
+     * // Perform transformation
+     * g2d.transform(...);
+     * // Render
+     * g2d.draw(...);
+     * // Restore original transform
+     * g2d.setTransform(saveAT);
+     * 
+ * + * @param Tx the AffineTransform that was retrieved + * from the getTransform method + * @see #transform + * @see #getTransform + * @see AffineTransform + */ + void setTransform(AffineTransform Tx); + + /** + * Returns the current Paint of the + * Graphics2D context. + * + * @return the current Graphics2D Paint, + * which defines a color or pattern. + * @see #setPaint + * @see java.awt.Graphics#setColor + */ + Paint getPaint(); + + /** + * Sets the Paint attribute for the + * Graphics2D context. Calling this method + * with a null Paint object does + * not have any effect on the current Paint attribute + * of this Graphics2D. + * + * @param paint the Paint object to be used to generate + * color during the rendering process, or null + * @see java.awt.Graphics#setColor + * @see #getPaint + * @see GradientPaint + * @see TexturePaint + */ + void setPaint(Paint paint); + + /** + * Returns the current Composite in the + * Graphics2D context. + * + * @return the current Graphics2D Composite, + * which defines a compositing style. + * @see #setComposite + */ + Composite getComposite(); + + /** + * Sets the Composite for the Graphics2D context. + * The Composite is used in all drawing methods such as + * drawImage, drawString, draw, + * and fill. It specifies how new pixels are to be combined + * with the existing pixels on the graphics device during the rendering + * process. + *

If this Graphics2D context is drawing to a + * Component on the display screen and the + * Composite is a custom object rather than an + * instance of the AlphaComposite class, and if + * there is a security manager, its checkPermission + * method is called with an AWTPermission("readDisplayPixels") + * permission. + * + * @param comp the Composite object to be used for rendering + * @throws SecurityException if a custom Composite object is being + * used to render to the screen and a security manager + * is set and its checkPermission method + * does not allow the operation. + * @see java.awt.Graphics#setXORMode + * @see java.awt.Graphics#setPaintMode + * @see #getComposite + * @see AlphaComposite + * @see SecurityManager#checkPermission + * @see java.awt.AWTPermission + */ + void setComposite(Composite comp); + + /** + * Returns the background color used for clearing a region. + * + * @return the current Graphics2D Color, + * which defines the background color. + * @see #setBackground + */ + Color getBackground(); + + /** + * Sets the background color for the Graphics2D context. + * The background color is used for clearing a region. + * When a Graphics2D is constructed for a + * Component, the background color is + * inherited from the Component. Setting the background color + * in the Graphics2D context only affects the subsequent + * clearRect calls and not the background color of the + * Component. To change the background + * of the Component, use appropriate methods of + * the Component. + * + * @param color the background color that is used in + * subsequent calls to clearRect + * @see #getBackground + * @see java.awt.Graphics#clearRect + */ + void setBackground(Color color); + + /** + * Returns the current Stroke in the + * Graphics2D context. + * + * @return the current Graphics2D Stroke, + * which defines the line style. + * @see #setStroke + */ + Stroke getStroke(); + + /** + * Sets the Stroke for the Graphics2D context. + * + * @param s the Stroke object to be used to stroke a + * Shape during the rendering process + * @see BasicStroke + * @see #getStroke + */ + void setStroke(Stroke s); + + /** + * Intersects the current Clip with the interior of the + * specified Shape and sets the Clip to the + * resulting intersection. The specified Shape is + * transformed with the current Graphics2D + * Transform before being intersected with the current + * Clip. This method is used to make the current + * Clip smaller. + * To make the Clip larger, use setClip. + * The user clip modified by this method is independent of the + * clipping associated with device bounds and visibility. If no clip has + * previously been set, or if the clip has been cleared using + * {@link Graphics#setClip(Shape) setClip} with a null + * argument, the specified Shape becomes the new + * user clip. + * + * @param s the Shape to be intersected with the current + * Clip. If s is null, + * this method clears the current Clip. + */ + void clip(Shape s); + + /** + * Get the rendering context of the Font within this + * Graphics2D context. + * The {@link FontRenderContext} + * encapsulates application hints such as anti-aliasing and + * fractional metrics, as well as target device specific information + * such as dots-per-inch. This information should be provided by the + * application when using objects that perform typographical + * formatting, such as Font and + * TextLayout. This information should also be provided + * by applications that perform their own layout and need accurate + * measurements of various characteristics of glyphs such as advance + * and line height when various rendering hints have been applied to + * the text rendering. + * + * @return a reference to an instance of FontRenderContext. + * @see java.awt.font.FontRenderContext + * @see java.awt.Font#createGlyphVector + * @see java.awt.font.TextLayout + * @since 1.2 + */ + + FontRenderContext getFontRenderContext(); + + /** + * Gets this graphics context's current color. + * + * @return this graphics context's current color. + * @see java.awt.Color + * @see java.awt.Graphics#setColor(Color) + */ + Color getColor(); + + /** + * Sets this graphics context's current color to the specified + * color. All subsequent graphics operations using this graphics + * context use this specified color. + * + * @param c the new rendering color. + * @see java.awt.Color + * @see java.awt.Graphics#getColor + */ + void setColor(Color c); + + /** + * Sets the paint mode of this graphics context to overwrite the + * destination with this graphics context's current color. + * This sets the logical pixel operation function to the paint or + * overwrite mode. All subsequent rendering operations will + * overwrite the destination with the current color. + */ + void setPaintMode(); + + /** + * Sets the paint mode of this graphics context to alternate between + * this graphics context's current color and the new specified color. + * This specifies that logical pixel operations are performed in the + * XOR mode, which alternates pixels between the current color and + * a specified XOR color. + *

+ * When drawing operations are performed, pixels which are the + * current color are changed to the specified color, and vice versa. + *

+ * Pixels that are of colors other than those two colors are changed + * in an unpredictable but reversible manner; if the same figure is + * drawn twice, then all pixels are restored to their original values. + * + * @param c1 the XOR alternation color + */ + void setXORMode(Color c1); + + /** + * Gets the current font. + * + * @return this graphics context's current font. + * @see java.awt.Font + * @see java.awt.Graphics#setFont(Font) + */ + Font getFont(); + + /** + * Sets this graphics context's font to the specified font. + * All subsequent text operations using this graphics context + * use this font. A null argument is silently ignored. + * + * @param font the font. + * @see java.awt.Graphics#getFont + * @see java.awt.Graphics#drawString(java.lang.String, int, int) + * @see java.awt.Graphics#drawBytes(byte[], int, int, int, int) + * @see java.awt.Graphics#drawChars(char[], int, int, int, int) + */ + void setFont(Font font); + + /** + * Gets the font metrics of the current font. + * + * @return the font metrics of this graphics + * context's current font. + * @see java.awt.Graphics#getFont + * @see java.awt.FontMetrics + * @see java.awt.Graphics#getFontMetrics(Font) + */ + FontMetrics getFontMetrics(); + + /** + * Gets the font metrics for the specified font. + * + * @param f the specified font + * @return the font metrics for the specified font. + * @see java.awt.Graphics#getFont + * @see java.awt.FontMetrics + * @see java.awt.Graphics#getFontMetrics() + */ + FontMetrics getFontMetrics(Font f); + + + /** + * Returns the bounding rectangle of the current clipping area. + * This method refers to the user clip, which is independent of the + * clipping associated with device bounds and window visibility. + * If no clip has previously been set, or if the clip has been + * cleared using setClip(null), this method returns + * null. + * The coordinates in the rectangle are relative to the coordinate + * system origin of this graphics context. + * + * @return the bounding rectangle of the current clipping area, + * or null if no clip is set. + * @see java.awt.Graphics#getClip + * @see java.awt.Graphics#clipRect + * @see java.awt.Graphics#setClip(int, int, int, int) + * @see java.awt.Graphics#setClip(Shape) + * @since JDK1.1 + */ + Rectangle getClipBounds(); + + /** + * Intersects the current clip with the specified rectangle. + * The resulting clipping area is the intersection of the current + * clipping area and the specified rectangle. If there is no + * current clipping area, either because the clip has never been + * set, or the clip has been cleared using setClip(null), + * the specified rectangle becomes the new clip. + * This method sets the user clip, which is independent of the + * clipping associated with device bounds and window visibility. + * This method can only be used to make the current clip smaller. + * To set the current clip larger, use any of the setClip methods. + * Rendering operations have no effect outside of the clipping area. + * + * @param x the x coordinate of the rectangle to intersect the clip with + * @param y the y coordinate of the rectangle to intersect the clip with + * @param width the width of the rectangle to intersect the clip with + * @param height the height of the rectangle to intersect the clip with + * @see #setClip(int, int, int, int) + * @see #setClip(Shape) + */ + void clipRect(int x, int y, int width, int height); + + /** + * Sets the current clip to the rectangle specified by the given + * coordinates. This method sets the user clip, which is + * independent of the clipping associated with device bounds + * and window visibility. + * Rendering operations have no effect outside of the clipping area. + * + * @param x the x coordinate of the new clip rectangle. + * @param y the y coordinate of the new clip rectangle. + * @param width the width of the new clip rectangle. + * @param height the height of the new clip rectangle. + * @see java.awt.Graphics#clipRect + * @see java.awt.Graphics#setClip(Shape) + * @see java.awt.Graphics#getClip + * @since JDK1.1 + */ + void setClip(int x, int y, int width, int height); + + /** + * Gets the current clipping area. + * This method returns the user clip, which is independent of the + * clipping associated with device bounds and window visibility. + * If no clip has previously been set, or if the clip has been + * cleared using setClip(null), this method returns + * null. + * + * @return a Shape object representing the + * current clipping area, or null if + * no clip is set. + * @see java.awt.Graphics#getClipBounds + * @see java.awt.Graphics#clipRect + * @see java.awt.Graphics#setClip(int, int, int, int) + * @see java.awt.Graphics#setClip(Shape) + * @since JDK1.1 + */ + Shape getClip(); + + /** + * Sets the current clipping area to an arbitrary clip shape. + * Not all objects that implement the Shape + * interface can be used to set the clip. The only + * Shape objects that are guaranteed to be + * supported are Shape objects that are + * obtained via the getClip method and via + * Rectangle objects. This method sets the + * user clip, which is independent of the clipping associated + * with device bounds and window visibility. + * + * @param clip the Shape to use to set the clip + * @see java.awt.Graphics#getClip() + * @see java.awt.Graphics#clipRect + * @see java.awt.Graphics#setClip(int, int, int, int) + * @since JDK1.1 + */ + void setClip(Shape clip); + + /** + * Copies an area of the component by a distance specified by + * dx and dy. From the point specified + * by x and y, this method + * copies downwards and to the right. To copy an area of the + * component to the left or upwards, specify a negative value for + * dx or dy. + * If a portion of the source rectangle lies outside the bounds + * of the component, or is obscured by another window or component, + * copyArea will be unable to copy the associated + * pixels. The area that is omitted can be refreshed by calling + * the component's paint method. + * + * @param x the x coordinate of the source rectangle. + * @param y the y coordinate of the source rectangle. + * @param width the width of the source rectangle. + * @param height the height of the source rectangle. + * @param dx the horizontal distance to copy the pixels. + * @param dy the vertical distance to copy the pixels. + */ + void copyArea(int x, int y, int width, int height, + int dx, int dy); + + /** + * Draws a line, using the current color, between the points + * (x1, y1) and (x2, y2) + * in this graphics context's coordinate system. + * + * @param x1 the first point's x coordinate. + * @param y1 the first point's y coordinate. + * @param x2 the second point's x coordinate. + * @param y2 the second point's y coordinate. + */ + void drawLine(int x1, int y1, int x2, int y2); + + /** + * Fills the specified rectangle. + * The left and right edges of the rectangle are at + * x and x + width - 1. + * The top and bottom edges are at + * y and y + height - 1. + * The resulting rectangle covers an area + * width pixels wide by + * height pixels tall. + * The rectangle is filled using the graphics context's current color. + * + * @param x the x coordinate + * of the rectangle to be filled. + * @param y the y coordinate + * of the rectangle to be filled. + * @param width the width of the rectangle to be filled. + * @param height the height of the rectangle to be filled. + * @see java.awt.Graphics#clearRect + * @see java.awt.Graphics#drawRect + */ + void fillRect(int x, int y, int width, int height); + + /** + * Draws the outline of the specified rectangle. + * The left and right edges of the rectangle are at + * x and x + width. + * The top and bottom edges are at + * y and y + height. + * The rectangle is drawn using the graphics context's current color. + * + * @param x the x coordinate + * of the rectangle to be drawn. + * @param y the y coordinate + * of the rectangle to be drawn. + * @param width the width of the rectangle to be drawn. + * @param height the height of the rectangle to be drawn. + * @see java.awt.Graphics#fillRect + * @see java.awt.Graphics#clearRect + */ + void drawRect(int x, int y, int width, int height); + + /** + * Clears the specified rectangle by filling it with the background + * color of the current drawing surface. This operation does not + * use the current paint mode. + *

+ * Beginning with Java 1.1, the background color + * of offscreen images may be system dependent. Applications should + * use setColor followed by fillRect to + * ensure that an offscreen image is cleared to a specific color. + * + * @param x the x coordinate of the rectangle to clear. + * @param y the y coordinate of the rectangle to clear. + * @param width the width of the rectangle to clear. + * @param height the height of the rectangle to clear. + * @see java.awt.Graphics#fillRect(int, int, int, int) + * @see java.awt.Graphics#drawRect + * @see java.awt.Graphics#setColor(java.awt.Color) + * @see java.awt.Graphics#setPaintMode + * @see java.awt.Graphics#setXORMode(java.awt.Color) + */ + void clearRect(int x, int y, int width, int height); + + /** + * Draws an outlined round-cornered rectangle using this graphics + * context's current color. The left and right edges of the rectangle + * are at x and x + width, + * respectively. The top and bottom edges of the rectangle are at + * y and y + height. + * + * @param x the x coordinate of the rectangle to be drawn. + * @param y the y coordinate of the rectangle to be drawn. + * @param width the width of the rectangle to be drawn. + * @param height the height of the rectangle to be drawn. + * @param arcWidth the horizontal diameter of the arc + * at the four corners. + * @param arcHeight the vertical diameter of the arc + * at the four corners. + * @see java.awt.Graphics#fillRoundRect + */ + void drawRoundRect(int x, int y, int width, int height, + int arcWidth, int arcHeight); + + /** + * Fills the specified rounded corner rectangle with the current color. + * The left and right edges of the rectangle + * are at x and x + width - 1, + * respectively. The top and bottom edges of the rectangle are at + * y and y + height - 1. + * + * @param x the x coordinate of the rectangle to be filled. + * @param y the y coordinate of the rectangle to be filled. + * @param width the width of the rectangle to be filled. + * @param height the height of the rectangle to be filled. + * @param arcWidth the horizontal diameter + * of the arc at the four corners. + * @param arcHeight the vertical diameter + * of the arc at the four corners. + * @see java.awt.Graphics#drawRoundRect + */ + void fillRoundRect(int x, int y, int width, int height, + int arcWidth, int arcHeight); + + /** + * Draws a 3-D highlighted outline of the specified rectangle. + * The edges of the rectangle are highlighted so that they + * appear to be beveled and lit from the upper left corner. + *

+ * The colors used for the highlighting effect are determined + * based on the current color. + * The resulting rectangle covers an area that is + * width + 1 pixels wide + * by height + 1 pixels tall. + * + * @param x the x coordinate of the rectangle to be drawn. + * @param y the y coordinate of the rectangle to be drawn. + * @param width the width of the rectangle to be drawn. + * @param height the height of the rectangle to be drawn. + * @param raised a boolean that determines whether the rectangle + * appears to be raised above the surface + * or sunk into the surface. + * @see java.awt.Graphics#fill3DRect + */ + void draw3DRect(int x, int y, int width, int height, + boolean raised); + + /** + * Paints a 3-D highlighted rectangle filled with the current color. + * The edges of the rectangle will be highlighted so that it appears + * as if the edges were beveled and lit from the upper left corner. + * The colors used for the highlighting effect will be determined from + * the current color. + * + * @param x the x coordinate of the rectangle to be filled. + * @param y the y coordinate of the rectangle to be filled. + * @param width the width of the rectangle to be filled. + * @param height the height of the rectangle to be filled. + * @param raised a boolean value that determines whether the + * rectangle appears to be raised above the surface + * or etched into the surface. + * @see java.awt.Graphics#draw3DRect + */ + void fill3DRect(int x, int y, int width, int height, + boolean raised); + + /** + * Draws the outline of an oval. + * The result is a circle or ellipse that fits within the + * rectangle specified by the x, y, + * width, and height arguments. + *

+ * The oval covers an area that is + * width + 1 pixels wide + * and height + 1 pixels tall. + * + * @param x the x coordinate of the upper left + * corner of the oval to be drawn. + * @param y the y coordinate of the upper left + * corner of the oval to be drawn. + * @param width the width of the oval to be drawn. + * @param height the height of the oval to be drawn. + * @see java.awt.Graphics#fillOval + */ + void drawOval(int x, int y, int width, int height); + + /** + * Fills an oval bounded by the specified rectangle with the + * current color. + * + * @param x the x coordinate of the upper left corner + * of the oval to be filled. + * @param y the y coordinate of the upper left corner + * of the oval to be filled. + * @param width the width of the oval to be filled. + * @param height the height of the oval to be filled. + * @see java.awt.Graphics#drawOval + */ + void fillOval(int x, int y, int width, int height); + + /** + * Draws the outline of a circular or elliptical arc + * covering the specified rectangle. + *

+ * The resulting arc begins at startAngle and extends + * for arcAngle degrees, using the current color. + * Angles are interpreted such that 0 degrees + * is at the 3 o'clock position. + * A positive value indicates a counter-clockwise rotation + * while a negative value indicates a clockwise rotation. + *

+ * The center of the arc is the center of the rectangle whose origin + * is (xy) and whose size is specified by the + * width and height arguments. + *

+ * The resulting arc covers an area + * width + 1 pixels wide + * by height + 1 pixels tall. + *

+ * The angles are specified relative to the non-square extents of + * the bounding rectangle such that 45 degrees always falls on the + * line from the center of the ellipse to the upper right corner of + * the bounding rectangle. As a result, if the bounding rectangle is + * noticeably longer in one axis than the other, the angles to the + * start and end of the arc segment will be skewed farther along the + * longer axis of the bounds. + * + * @param x the x coordinate of the + * upper-left corner of the arc to be drawn. + * @param y the y coordinate of the + * upper-left corner of the arc to be drawn. + * @param width the width of the arc to be drawn. + * @param height the height of the arc to be drawn. + * @param startAngle the beginning angle. + * @param arcAngle the angular extent of the arc, + * relative to the start angle. + * @see java.awt.Graphics#fillArc + */ + void drawArc(int x, int y, int width, int height, + int startAngle, int arcAngle); + + /** + * Fills a circular or elliptical arc covering the specified rectangle. + *

+ * The resulting arc begins at startAngle and extends + * for arcAngle degrees. + * Angles are interpreted such that 0 degrees + * is at the 3 o'clock position. + * A positive value indicates a counter-clockwise rotation + * while a negative value indicates a clockwise rotation. + *

+ * The center of the arc is the center of the rectangle whose origin + * is (xy) and whose size is specified by the + * width and height arguments. + *

+ * The resulting arc covers an area + * width + 1 pixels wide + * by height + 1 pixels tall. + *

+ * The angles are specified relative to the non-square extents of + * the bounding rectangle such that 45 degrees always falls on the + * line from the center of the ellipse to the upper right corner of + * the bounding rectangle. As a result, if the bounding rectangle is + * noticeably longer in one axis than the other, the angles to the + * start and end of the arc segment will be skewed farther along the + * longer axis of the bounds. + * + * @param x the x coordinate of the + * upper-left corner of the arc to be filled. + * @param y the y coordinate of the + * upper-left corner of the arc to be filled. + * @param width the width of the arc to be filled. + * @param height the height of the arc to be filled. + * @param startAngle the beginning angle. + * @param arcAngle the angular extent of the arc, + * relative to the start angle. + * @see java.awt.Graphics#drawArc + */ + void fillArc(int x, int y, int width, int height, + int startAngle, int arcAngle); + + /** + * Draws a sequence of connected lines defined by + * arrays of x and y coordinates. + * Each pair of (xy) coordinates defines a point. + * The figure is not closed if the first point + * differs from the last point. + * + * @param xPoints an array of x points + * @param yPoints an array of y points + * @param nPoints the total number of points + * @see java.awt.Graphics#drawPolygon(int[], int[], int) + * @since JDK1.1 + */ + void drawPolyline(int xPoints[], int yPoints[], + int nPoints); + + /** + * Draws a closed polygon defined by + * arrays of x and y coordinates. + * Each pair of (xy) coordinates defines a point. + *

+ * This method draws the polygon defined by nPoint line + * segments, where the first nPoint - 1 + * line segments are line segments from + * (xPoints[i - 1], yPoints[i - 1]) + * to (xPoints[i], yPoints[i]), for + * 1 ≤ i ≤ nPoints. + * The figure is automatically closed by drawing a line connecting + * the final point to the first point, if those points are different. + * + * @param xPoints a an array of x coordinates. + * @param yPoints a an array of y coordinates. + * @param nPoints a the total number of points. + * @see java.awt.Graphics#fillPolygon + * @see java.awt.Graphics#drawPolyline + */ + void drawPolygon(int xPoints[], int yPoints[], + int nPoints); + + /** + * Draws the outline of a polygon defined by the specified + * Polygon object. + * + * @param p the polygon to draw. + * @see java.awt.Graphics#fillPolygon + * @see java.awt.Graphics#drawPolyline + */ + void drawPolygon(Polygon p); + + /** + * Fills a closed polygon defined by + * arrays of x and y coordinates. + *

+ * This method draws the polygon defined by nPoint line + * segments, where the first nPoint - 1 + * line segments are line segments from + * (xPoints[i - 1], yPoints[i - 1]) + * to (xPoints[i], yPoints[i]), for + * 1 ≤ i ≤ nPoints. + * The figure is automatically closed by drawing a line connecting + * the final point to the first point, if those points are different. + *

+ * The area inside the polygon is defined using an + * even-odd fill rule, also known as the alternating rule. + * + * @param xPoints a an array of x coordinates. + * @param yPoints a an array of y coordinates. + * @param nPoints a the total number of points. + * @see java.awt.Graphics#drawPolygon(int[], int[], int) + */ + void fillPolygon(int xPoints[], int yPoints[], + int nPoints); + + /** + * Fills the polygon defined by the specified Polygon object with + * the graphics context's current color. + *

+ * The area inside the polygon is defined using an + * even-odd fill rule, also known as the alternating rule. + * + * @param p the polygon to fill. + * @see java.awt.Graphics#drawPolygon(int[], int[], int) + */ + void fillPolygon(Polygon p); + + /** + * Draws the text given by the specified character array, using this + * graphics context's current font and color. The baseline of the + * first character is at position (xy) in this + * graphics context's coordinate system. + * + * @param data the array of characters to be drawn + * @param offset the start offset in the data + * @param length the number of characters to be drawn + * @param x the x coordinate of the baseline of the text + * @param y the y coordinate of the baseline of the text + * @throws NullPointerException if data is null. + * @throws IndexOutOfBoundsException if offset or + * lengthis less than zero, or + * offset+length is greater than the length of the + * data array. + * @see java.awt.Graphics#drawBytes + * @see java.awt.Graphics#drawString + */ + void drawChars(char data[], int offset, int length, int x, int y); + + /** + * Draws the text given by the specified byte array, using this + * graphics context's current font and color. The baseline of the + * first character is at position (xy) in this + * graphics context's coordinate system. + *

+ * Use of this method is not recommended as each byte is interpreted + * as a Unicode code point in the range 0 to 255, and so can only be + * used to draw Latin characters in that range. + * + * @param data the data to be drawn + * @param offset the start offset in the data + * @param length the number of bytes that are drawn + * @param x the x coordinate of the baseline of the text + * @param y the y coordinate of the baseline of the text + * @throws NullPointerException if data is null. + * @throws IndexOutOfBoundsException if offset or + * lengthis less than zero, or offset+length + * is greater than the length of the data array. + * @see java.awt.Graphics#drawChars + * @see java.awt.Graphics#drawString + */ + void drawBytes(byte data[], int offset, int length, int x, int y); + + /** + * Draws as much of the specified image as is currently available. + * The image is drawn with its top-left corner at + * (xy) in this graphics context's coordinate + * space. Transparent pixels in the image do not affect whatever + * pixels are already there. + *

+ * This method returns immediately in all cases, even if the + * complete image has not yet been loaded, and it has not been dithered + * and converted for the current output device. + *

+ * If the image has completely loaded and its pixels are + * no longer being changed, then + * drawImage returns true. + * Otherwise, drawImage returns false + * and as more of + * the image becomes available + * or it is time to draw another frame of animation, + * the process that loads the image notifies + * the specified image observer. + * + * @param img the specified image to be drawn. This method does + * nothing if img is null. + * @param x the x coordinate. + * @param y the y coordinate. + * @param observer object to be notified as more of + * the image is converted. + * @return false if the image pixels are still changing; + * true otherwise. + * @see java.awt.Image + * @see java.awt.image.ImageObserver + * @see java.awt.image.ImageObserver#imageUpdate(java.awt.Image, int, int, int, int, int) + */ + boolean drawImage(Image img, int x, int y, + ImageObserver observer); + + /** + * Draws as much of the specified image as has already been scaled + * to fit inside the specified rectangle. + *

+ * The image is drawn inside the specified rectangle of this + * graphics context's coordinate space, and is scaled if + * necessary. Transparent pixels do not affect whatever pixels + * are already there. + *

+ * This method returns immediately in all cases, even if the + * entire image has not yet been scaled, dithered, and converted + * for the current output device. + * If the current output representation is not yet complete, then + * drawImage returns false. As more of + * the image becomes available, the process that loads the image notifies + * the image observer by calling its imageUpdate method. + *

+ * A scaled version of an image will not necessarily be + * available immediately just because an unscaled version of the + * image has been constructed for this output device. Each size of + * the image may be cached separately and generated from the original + * data in a separate image production sequence. + * + * @param img the specified image to be drawn. This method does + * nothing if img is null. + * @param x the x coordinate. + * @param y the y coordinate. + * @param width the width of the rectangle. + * @param height the height of the rectangle. + * @param observer object to be notified as more of + * the image is converted. + * @return false if the image pixels are still changing; + * true otherwise. + * @see java.awt.Image + * @see java.awt.image.ImageObserver + * @see java.awt.image.ImageObserver#imageUpdate(java.awt.Image, int, int, int, int, int) + */ + boolean drawImage(Image img, int x, int y, + int width, int height, + ImageObserver observer); + + /** + * Draws as much of the specified image as is currently available. + * The image is drawn with its top-left corner at + * (xy) in this graphics context's coordinate + * space. Transparent pixels are drawn in the specified + * background color. + *

+ * This operation is equivalent to filling a rectangle of the + * width and height of the specified image with the given color and then + * drawing the image on top of it, but possibly more efficient. + *

+ * This method returns immediately in all cases, even if the + * complete image has not yet been loaded, and it has not been dithered + * and converted for the current output device. + *

+ * If the image has completely loaded and its pixels are + * no longer being changed, then + * drawImage returns true. + * Otherwise, drawImage returns false + * and as more of + * the image becomes available + * or it is time to draw another frame of animation, + * the process that loads the image notifies + * the specified image observer. + * + * @param img the specified image to be drawn. This method does + * nothing if img is null. + * @param x the x coordinate. + * @param y the y coordinate. + * @param bgcolor the background color to paint under the + * non-opaque portions of the image. + * @param observer object to be notified as more of + * the image is converted. + * @return false if the image pixels are still changing; + * true otherwise. + * @see java.awt.Image + * @see java.awt.image.ImageObserver + * @see java.awt.image.ImageObserver#imageUpdate(java.awt.Image, int, int, int, int, int) + */ + boolean drawImage(Image img, int x, int y, + Color bgcolor, + ImageObserver observer); + + /** + * Draws as much of the specified image as has already been scaled + * to fit inside the specified rectangle. + *

+ * The image is drawn inside the specified rectangle of this + * graphics context's coordinate space, and is scaled if + * necessary. Transparent pixels are drawn in the specified + * background color. + * This operation is equivalent to filling a rectangle of the + * width and height of the specified image with the given color and then + * drawing the image on top of it, but possibly more efficient. + *

+ * This method returns immediately in all cases, even if the + * entire image has not yet been scaled, dithered, and converted + * for the current output device. + * If the current output representation is not yet complete then + * drawImage returns false. As more of + * the image becomes available, the process that loads the image notifies + * the specified image observer. + *

+ * A scaled version of an image will not necessarily be + * available immediately just because an unscaled version of the + * image has been constructed for this output device. Each size of + * the image may be cached separately and generated from the original + * data in a separate image production sequence. + * + * @param img the specified image to be drawn. This method does + * nothing if img is null. + * @param x the x coordinate. + * @param y the y coordinate. + * @param width the width of the rectangle. + * @param height the height of the rectangle. + * @param bgcolor the background color to paint under the + * non-opaque portions of the image. + * @param observer object to be notified as more of + * the image is converted. + * @return false if the image pixels are still changing; + * true otherwise. + * @see java.awt.Image + * @see java.awt.image.ImageObserver + * @see java.awt.image.ImageObserver#imageUpdate(java.awt.Image, int, int, int, int, int) + */ + boolean drawImage(Image img, int x, int y, + int width, int height, + Color bgcolor, + ImageObserver observer); + + /** + * Draws as much of the specified area of the specified image as is + * currently available, scaling it on the fly to fit inside the + * specified area of the destination drawable surface. Transparent pixels + * do not affect whatever pixels are already there. + *

+ * This method returns immediately in all cases, even if the + * image area to be drawn has not yet been scaled, dithered, and converted + * for the current output device. + * If the current output representation is not yet complete then + * drawImage returns false. As more of + * the image becomes available, the process that loads the image notifies + * the specified image observer. + *

+ * This method always uses the unscaled version of the image + * to render the scaled rectangle and performs the required + * scaling on the fly. It does not use a cached, scaled version + * of the image for this operation. Scaling of the image from source + * to destination is performed such that the first coordinate + * of the source rectangle is mapped to the first coordinate of + * the destination rectangle, and the second source coordinate is + * mapped to the second destination coordinate. The subimage is + * scaled and flipped as needed to preserve those mappings. + * + * @param img the specified image to be drawn. This method does + * nothing if img is null. + * @param dx1 the x coordinate of the first corner of the + * destination rectangle. + * @param dy1 the y coordinate of the first corner of the + * destination rectangle. + * @param dx2 the x coordinate of the second corner of the + * destination rectangle. + * @param dy2 the y coordinate of the second corner of the + * destination rectangle. + * @param sx1 the x coordinate of the first corner of the + * source rectangle. + * @param sy1 the y coordinate of the first corner of the + * source rectangle. + * @param sx2 the x coordinate of the second corner of the + * source rectangle. + * @param sy2 the y coordinate of the second corner of the + * source rectangle. + * @param observer object to be notified as more of the image is + * scaled and converted. + * @return false if the image pixels are still changing; + * true otherwise. + * @see java.awt.Image + * @see java.awt.image.ImageObserver + * @see java.awt.image.ImageObserver#imageUpdate(java.awt.Image, int, int, int, int, int) + * @since JDK1.1 + */ + boolean drawImage(Image img, + int dx1, int dy1, int dx2, int dy2, + int sx1, int sy1, int sx2, int sy2, + ImageObserver observer); + + /** + * Draws as much of the specified area of the specified image as is + * currently available, scaling it on the fly to fit inside the + * specified area of the destination drawable surface. + *

+ * Transparent pixels are drawn in the specified background color. + * This operation is equivalent to filling a rectangle of the + * width and height of the specified image with the given color and then + * drawing the image on top of it, but possibly more efficient. + *

+ * This method returns immediately in all cases, even if the + * image area to be drawn has not yet been scaled, dithered, and converted + * for the current output device. + * If the current output representation is not yet complete then + * drawImage returns false. As more of + * the image becomes available, the process that loads the image notifies + * the specified image observer. + *

+ * This method always uses the unscaled version of the image + * to render the scaled rectangle and performs the required + * scaling on the fly. It does not use a cached, scaled version + * of the image for this operation. Scaling of the image from source + * to destination is performed such that the first coordinate + * of the source rectangle is mapped to the first coordinate of + * the destination rectangle, and the second source coordinate is + * mapped to the second destination coordinate. The subimage is + * scaled and flipped as needed to preserve those mappings. + * + * @param img the specified image to be drawn. This method does + * nothing if img is null. + * @param dx1 the x coordinate of the first corner of the + * destination rectangle. + * @param dy1 the y coordinate of the first corner of the + * destination rectangle. + * @param dx2 the x coordinate of the second corner of the + * destination rectangle. + * @param dy2 the y coordinate of the second corner of the + * destination rectangle. + * @param sx1 the x coordinate of the first corner of the + * source rectangle. + * @param sy1 the y coordinate of the first corner of the + * source rectangle. + * @param sx2 the x coordinate of the second corner of the + * source rectangle. + * @param sy2 the y coordinate of the second corner of the + * source rectangle. + * @param bgcolor the background color to paint under the + * non-opaque portions of the image. + * @param observer object to be notified as more of the image is + * scaled and converted. + * @return false if the image pixels are still changing; + * true otherwise. + * @see java.awt.Image + * @see java.awt.image.ImageObserver + * @see java.awt.image.ImageObserver#imageUpdate(java.awt.Image, int, int, int, int, int) + * @since JDK1.1 + */ + boolean drawImage(Image img, + int dx1, int dy1, int dx2, int dy2, + int sx1, int sy1, int sx2, int sy2, + Color bgcolor, + ImageObserver observer); + + /** + * Disposes of this graphics context and releases + * any system resources that it is using. + * A Graphics object cannot be used after + * disposehas been called. + *

+ * When a Java program runs, a large number of Graphics + * objects can be created within a short time frame. + * Although the finalization process of the garbage collector + * also disposes of the same system resources, it is preferable + * to manually free the associated resources by calling this + * method rather than to rely on a finalization process which + * may not run to completion for a long period of time. + *

+ * Graphics objects which are provided as arguments to the + * paint and update methods + * of components are automatically released by the system when + * those methods return. For efficiency, programmers should + * call dispose when finished using + * a Graphics object only if it was created + * directly from a component or another Graphics object. + * + * @see java.awt.Graphics#finalize + * @see java.awt.Component#paint + * @see java.awt.Component#update + * @see java.awt.Component#getGraphics + * @see java.awt.Graphics#create + */ + void dispose(); + + /** + * Returns true if the specified rectangular area might intersect + * the current clipping area. + * The coordinates of the specified rectangular area are in the + * user coordinate space and are relative to the coordinate + * system origin of this graphics context. + * This method may use an algorithm that calculates a result quickly + * but which sometimes might return true even if the specified + * rectangular area does not intersect the clipping area. + * The specific algorithm employed may thus trade off accuracy for + * speed, but it will never return false unless it can guarantee + * that the specified rectangular area does not intersect the + * current clipping area. + * The clipping area used by this method can represent the + * intersection of the user clip as specified through the clip + * methods of this graphics context as well as the clipping + * associated with the device or image bounds and window visibility. + * + * @param x the x coordinate of the rectangle to test against the clip + * @param y the y coordinate of the rectangle to test against the clip + * @param width the width of the rectangle to test against the clip + * @param height the height of the rectangle to test against the clip + * @return true if the specified rectangle intersects + * the bounds of the current clip; false + * otherwise. + */ + boolean hitClip(int x, int y, int width, int height); + + /** + * Returns the bounding rectangle of the current clipping area. + * The coordinates in the rectangle are relative to the coordinate + * system origin of this graphics context. This method differs + * from {@link #getClipBounds() getClipBounds} in that an existing + * rectangle is used instead of allocating a new one. + * This method refers to the user clip, which is independent of the + * clipping associated with device bounds and window visibility. + * If no clip has previously been set, or if the clip has been + * cleared using setClip(null), this method returns the + * specified Rectangle. + * + * @param r the rectangle where the current clipping area is + * copied to. Any current values in this rectangle are + * overwritten. + * @return the bounding rectangle of the current clipping area. + */ + Rectangle getClipBounds(Rectangle r); +} diff --git a/src/main/java/com/defano/jmonet/model/FixedQuadrilateral.java b/src/main/java/com/defano/jmonet/model/FixedQuadrilateral.java index 649f7c5e..3b3d655c 100644 --- a/src/main/java/com/defano/jmonet/model/FixedQuadrilateral.java +++ b/src/main/java/com/defano/jmonet/model/FixedQuadrilateral.java @@ -5,6 +5,7 @@ /** * A model of a quadrilateral with fixed dimensions. */ +@SuppressWarnings("unused") public class FixedQuadrilateral implements Quadrilateral { private final Point topLeft, topRight, bottomLeft, bottomRight; diff --git a/src/main/java/com/defano/jmonet/model/FlexQuadrilateral.java b/src/main/java/com/defano/jmonet/model/FlexQuadrilateral.java index 8919ea89..7b2d206b 100644 --- a/src/main/java/com/defano/jmonet/model/FlexQuadrilateral.java +++ b/src/main/java/com/defano/jmonet/model/FlexQuadrilateral.java @@ -1,6 +1,6 @@ package com.defano.jmonet.model; -import com.defano.jmonet.tools.util.Geometry; +import com.defano.jmonet.tools.util.MathUtils; import java.awt.*; @@ -27,6 +27,7 @@ public class FlexQuadrilateral implements Quadrilateral { * @param bottomLeft The bottom-left corner * @param bottomRight The bottom-right corner */ + @SuppressWarnings("WeakerAccess") public FlexQuadrilateral(Point topLeft, Point topRight, Point bottomLeft, Point bottomRight) { this.topLeft = topLeft; this.topRight = topRight; @@ -64,9 +65,9 @@ public Point getTopLeft() { */ public void setTopLeft(Point topLeft) { if (topLeft.x < getRight() && - Geometry.isAbove(getBottomLeftTopRightDiagonal(), topLeft) && - Geometry.isAbove(getBottomLine(), topLeft) && - Geometry.isBelow(getRightLine(), topLeft)) { + MathUtils.isAbove(getBottomLeftTopRightDiagonal(), topLeft) && + MathUtils.isAbove(getBottomLine(), topLeft) && + MathUtils.isBelow(getRightLine(), topLeft)) { this.topLeft = topLeft; } } @@ -88,9 +89,9 @@ public Point getTopRight() { */ public void setTopRight(Point topRight) { if (topRight.x > getLeft() && topRight.y < getBottom() && - Geometry.isAbove(getTopLeftBottomRightDiagonal(), topRight) && - Geometry.isAbove(getLeftLine(), topRight) && - Geometry.isAbove(getBottomLine(), topRight)) { + MathUtils.isAbove(getTopLeftBottomRightDiagonal(), topRight) && + MathUtils.isAbove(getLeftLine(), topRight) && + MathUtils.isAbove(getBottomLine(), topRight)) { this.topRight = topRight; } } @@ -112,9 +113,9 @@ public Point getBottomLeft() { */ public void setBottomLeft(Point bottomLeft) { if (bottomLeft.x < getRight() && bottomLeft.y > getTop() && - Geometry.isBelow(getTopLeftBottomRightDiagonal(), bottomLeft) && - Geometry.isBelow(getRightLine(), bottomLeft) && - Geometry.isBelow(getTopLine(), bottomLeft)) { + MathUtils.isBelow(getTopLeftBottomRightDiagonal(), bottomLeft) && + MathUtils.isBelow(getRightLine(), bottomLeft) && + MathUtils.isBelow(getTopLine(), bottomLeft)) { this.bottomLeft = bottomLeft; } } @@ -136,9 +137,9 @@ public Point getBottomRight() { */ public void setBottomRight(Point bottomRight) { if (bottomRight.x > getLeft() && bottomRight.y > getTop() && - Geometry.isBelow(getBottomLeftTopRightDiagonal(), bottomRight) && - Geometry.isAbove(getLeftLine(), bottomRight) && - Geometry.isBelow(getTopLine(), bottomRight)) { + MathUtils.isBelow(getBottomLeftTopRightDiagonal(), bottomRight) && + MathUtils.isAbove(getLeftLine(), bottomRight) && + MathUtils.isBelow(getTopLine(), bottomRight)) { this.bottomRight = bottomRight; } } diff --git a/src/main/java/com/defano/jmonet/model/PaintToolType.java b/src/main/java/com/defano/jmonet/model/PaintToolType.java index fb8f7064..bcdacdee 100644 --- a/src/main/java/com/defano/jmonet/model/PaintToolType.java +++ b/src/main/java/com/defano/jmonet/model/PaintToolType.java @@ -1,7 +1,7 @@ package com.defano.jmonet.model; import com.defano.jmonet.tools.*; -import com.defano.jmonet.tools.builder.PaintTool; +import com.defano.jmonet.tools.base.Tool; /** * An enumeration of paint tools provided by this library. @@ -18,7 +18,7 @@ public enum PaintToolType { POLYGON(PolygonTool.class), SHAPE(ShapeTool.class), FREEFORM(FreeformShapeTool.class), - SELECTION(SelectionTool.class), + SELECTION(MarqueeTool.class), LASSO(LassoTool.class), TEXT(TextTool.class), FILL(FillTool.class), @@ -32,22 +32,14 @@ public enum PaintToolType { PERSPECTIVE(PerspectiveTool.class), RUBBERSHEET(RubberSheetTool.class); - private final Class toolClass; + private final Class toolClass; - PaintToolType(Class clazz) { + PaintToolType(Class clazz) { this.toolClass = clazz; } - /** - * Creates a new instance of this type of {@link PaintTool}. - * @return A new {@link PaintTool}. - */ - public PaintTool getToolInstance() { - try { - return toolClass.newInstance(); - } catch (InstantiationException | IllegalAccessException e) { - throw new IllegalStateException("Failed to instantiate PaintTool.", e); - } + public Class getToolClass() { + return toolClass; } /** diff --git a/src/main/java/com/defano/jmonet/tools/AirbrushTool.java b/src/main/java/com/defano/jmonet/tools/AirbrushTool.java index 18a9fb46..784cbd88 100644 --- a/src/main/java/com/defano/jmonet/tools/AirbrushTool.java +++ b/src/main/java/com/defano/jmonet/tools/AirbrushTool.java @@ -1,8 +1,11 @@ package com.defano.jmonet.tools; import com.defano.jmonet.canvas.Scratch; +import com.defano.jmonet.context.GraphicsContext; import com.defano.jmonet.model.PaintToolType; +import com.defano.jmonet.tools.base.PathToolDelegate; import com.defano.jmonet.tools.base.StrokedCursorPathTool; +import com.defano.jmonet.tools.util.MathUtils; import java.awt.*; import java.awt.geom.Line2D; @@ -10,28 +13,47 @@ /** * A tool that paints translucent textured paint on the canvas. */ -public class AirbrushTool extends StrokedCursorPathTool { +public class AirbrushTool extends StrokedCursorPathTool implements PathToolDelegate { - public AirbrushTool() { + /** + * Tool must be constructed via {@link com.defano.jmonet.tools.builder.PaintToolBuilder} to handle dependency + * injection. + */ + AirbrushTool() { super(PaintToolType.AIRBRUSH); + setDelegate(this); } /** {@inheritDoc} */ @Override - protected void startPath(Scratch scratch, Stroke stroke, Paint fillPaint, Point initialPoint) { + public void startPath(Scratch scratch, Stroke stroke, Paint strokePaint, Point initialPoint) { // Nothing to do } /** {@inheritDoc} */ @Override - protected void addPoint(Scratch scratch, Stroke stroke, Paint fillPaint, Point lastPoint, Point thisPoint) { + public void addPoint(Scratch scratch, Stroke stroke, Paint strokePaint, Point lastPoint, Point thisPoint) { Line2D line = new Line2D.Float(lastPoint, thisPoint); - Graphics2D g = scratch.getAddScratchGraphics(this, stroke, line); + GraphicsContext g = scratch.getAddScratchGraphics(this, stroke, line); g.setStroke(stroke); - g.setPaint(fillPaint); - g.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_OVER, getIntensityObservable().blockingFirst().floatValue())); - g.draw(line); + g.setPaint(strokePaint); + + if (getAttributes().isPathInterpolated()) { + g.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_OVER, (float) getAttributes().getIntensity() / 10.0f)); + for (Point p : MathUtils.linearInterpolation(lastPoint, thisPoint, 1)) { + g.draw(new Line2D.Float(p, p)); + } + } else { + g.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_OVER, (float) getAttributes().getIntensity())); + g.draw(line); + } + + } + + @Override + public void completePath(Scratch scratch, Stroke stroke, Paint strokePaint, Paint fillPaint) { + // Nothing to do } } diff --git a/src/main/java/com/defano/jmonet/tools/ArrowTool.java b/src/main/java/com/defano/jmonet/tools/ArrowTool.java index 6a17eeb8..01fa4dc1 100644 --- a/src/main/java/com/defano/jmonet/tools/ArrowTool.java +++ b/src/main/java/com/defano/jmonet/tools/ArrowTool.java @@ -1,17 +1,25 @@ package com.defano.jmonet.tools; import com.defano.jmonet.model.PaintToolType; -import com.defano.jmonet.tools.builder.PaintTool; +import com.defano.jmonet.tools.base.BasicTool; import java.awt.*; /** * A no-op tool; does not modify the canvas in any way. */ -public class ArrowTool extends PaintTool { +public class ArrowTool extends BasicTool { - public ArrowTool() { + /** + * Tool must be constructed via {@link com.defano.jmonet.tools.builder.PaintToolBuilder} to handle dependency + * injection. + */ + ArrowTool() { super(PaintToolType.ARROW); - setToolCursor(new Cursor(Cursor.DEFAULT_CURSOR)); + } + + @Override + public Cursor getDefaultCursor() { + return Cursor.getDefaultCursor(); } } diff --git a/src/main/java/com/defano/jmonet/tools/CurveTool.java b/src/main/java/com/defano/jmonet/tools/CurveTool.java index b4d0226e..174dd829 100644 --- a/src/main/java/com/defano/jmonet/tools/CurveTool.java +++ b/src/main/java/com/defano/jmonet/tools/CurveTool.java @@ -1,8 +1,10 @@ package com.defano.jmonet.tools; import com.defano.jmonet.canvas.Scratch; +import com.defano.jmonet.context.GraphicsContext; import com.defano.jmonet.model.PaintToolType; -import com.defano.jmonet.tools.base.AbstractPolylineTool; +import com.defano.jmonet.tools.base.PolylineTool; +import com.defano.jmonet.tools.base.PolylineToolDelegate; import java.awt.*; import java.awt.geom.Path2D; @@ -10,18 +12,23 @@ /** * A tool for drawing quadratic (Bezier) curves on the canvas. */ -public class CurveTool extends AbstractPolylineTool { +public class CurveTool extends PolylineTool implements PolylineToolDelegate { - public CurveTool() { + /** + * Tool must be constructed via {@link com.defano.jmonet.tools.builder.PaintToolBuilder} to handle dependency + * injection. + */ + CurveTool() { super(PaintToolType.CURVE); + setDelegate(this); } /** {@inheritDoc} */ @Override - protected void strokePolyline(Scratch scratch, Stroke stroke, Paint paint, int[] xPoints, int[] yPoints) { + public void strokePolyline(Scratch scratch, Stroke stroke, Paint paint, int[] xPoints, int[] yPoints) { Shape curve = renderCurvePath(xPoints, yPoints); - Graphics2D g = scratch.getAddScratchGraphics(this, stroke, curve); + GraphicsContext g = scratch.getAddScratchGraphics(this, stroke, curve); g.setPaint(paint); g.setStroke(stroke); g.draw(curve); @@ -29,8 +36,10 @@ protected void strokePolyline(Scratch scratch, Stroke stroke, Paint paint, int[] /** {@inheritDoc} */ @Override - protected void strokePolygon(Scratch scratch, Stroke stroke, Paint strokePaint, int[] xPoints, int[] yPoints) { - Graphics2D g = scratch.getAddScratchGraphics(this, null); + public void strokePolygon(Scratch scratch, Stroke stroke, Paint strokePaint, int[] xPoints, int[] yPoints) { + Shape curve = renderCurvePath(xPoints, yPoints); + + GraphicsContext g = scratch.getAddScratchGraphics(this, curve); g.setPaint(strokePaint); g.setStroke(stroke); g.draw(renderCurvePath(xPoints, yPoints)); @@ -38,8 +47,8 @@ protected void strokePolygon(Scratch scratch, Stroke stroke, Paint strokePaint, /** {@inheritDoc} */ @Override - protected void fillPolygon(Scratch scratch, Paint fillPaint, int[] xPoints, int[] yPoints) { - // Not fillable + public void fillPolygon(Scratch scratch, Paint fillPaint, int[] xPoints, int[] yPoints) { + // Not fillable; nothing to do } private Shape renderCurvePath(int[] xPoints, int[] yPoints) { diff --git a/src/main/java/com/defano/jmonet/tools/EraserTool.java b/src/main/java/com/defano/jmonet/tools/EraserTool.java index 3360a61f..fcc49240 100644 --- a/src/main/java/com/defano/jmonet/tools/EraserTool.java +++ b/src/main/java/com/defano/jmonet/tools/EraserTool.java @@ -2,6 +2,7 @@ import com.defano.jmonet.canvas.Scratch; import com.defano.jmonet.model.PaintToolType; +import com.defano.jmonet.tools.base.PathToolDelegate; import com.defano.jmonet.tools.base.StrokedCursorPathTool; import java.awt.*; @@ -10,21 +11,31 @@ /** * Tool that erases pixels from the canvas by turning them back to fully transparent. */ -public class EraserTool extends StrokedCursorPathTool { +public class EraserTool extends StrokedCursorPathTool implements PathToolDelegate { - public EraserTool() { + /** + * Tool must be constructed via {@link com.defano.jmonet.tools.builder.PaintToolBuilder} to handle dependency + * injection. + */ + EraserTool() { super(PaintToolType.ERASER); + setDelegate(this); } /** {@inheritDoc} */ @Override - protected void startPath(Scratch scratch, Stroke stroke, Paint fillPaint, Point initialPoint) { - erase(scratch, new Line2D.Float(initialPoint, initialPoint), stroke); + public void startPath(Scratch scratch, Stroke stroke, Paint strokePaint, Point initialPoint) { + scratch.erase(this, new Line2D.Float(initialPoint, initialPoint), stroke); } /** {@inheritDoc} */ @Override - protected void addPoint(Scratch scratch, Stroke stroke, Paint fillPaint, Point lastPoint, Point thisPoint) { - erase(scratch, new Line2D.Float(lastPoint, thisPoint), stroke); + public void addPoint(Scratch scratch, Stroke stroke, Paint strokePaint, Point lastPoint, Point thisPoint) { + scratch.erase(this, new Line2D.Float(lastPoint, thisPoint), stroke); + } + + @Override + public void completePath(Scratch scratch, Stroke stroke, Paint strokePaint, Paint fillPaint) { + // Nothing to do } } diff --git a/src/main/java/com/defano/jmonet/tools/FillTool.java b/src/main/java/com/defano/jmonet/tools/FillTool.java index 8e128224..ab5cd552 100644 --- a/src/main/java/com/defano/jmonet/tools/FillTool.java +++ b/src/main/java/com/defano/jmonet/tools/FillTool.java @@ -1,109 +1,59 @@ package com.defano.jmonet.tools; -import com.defano.jmonet.algo.fill.*; -import com.defano.jmonet.algo.transform.image.FloodFillTransform; import com.defano.jmonet.model.PaintToolType; -import com.defano.jmonet.tools.builder.PaintTool; -import com.defano.jmonet.tools.util.CursorFactory; +import com.defano.jmonet.tools.attributes.ToolAttributes; +import com.defano.jmonet.tools.base.BasicTool; +import com.defano.jmonet.tools.builder.PaintToolBuilder; +import com.defano.jmonet.tools.cursors.CursorFactory; +import com.defano.jmonet.transform.image.FloodFillTransform; +import com.google.inject.Inject; import java.awt.*; import java.awt.event.MouseEvent; -import java.awt.image.BufferedImage; +import java.util.Optional; /** * Tool that performs a flood-fill of all transparent pixels. */ -public class FillTool extends PaintTool { +@SuppressWarnings("unused") +public class FillTool extends BasicTool { - private Cursor fillCursor = CursorFactory.makeBucketCursor(); - private BoundaryFunction boundaryFunction = new DefaultBoundaryFunction(); - private FillFunction fillFunction = new DefaultFillFunction(); + @Inject + private FloodFillTransform floodFill; - public FillTool() { + /** + * Tool must be constructed via {@link PaintToolBuilder} to handle dependency + * injection. + */ + FillTool() { super(PaintToolType.FILL); } + @Override + public Cursor getDefaultCursor() { + return CursorFactory.makeBucketCursor(); + } + /** {@inheritDoc} */ @Override public void mousePressed(MouseEvent e, Point imageLocation) { + ToolAttributes attributes = getAttributes(); + Optional fillPaint = attributes.getFillPaint(); // Nothing to do if no fill paint is specified - if (getFillPaint().isPresent()) { + if (fillPaint.isPresent()) { getScratch().clear(); - BufferedImage filled = new FloodFillTransform(getFillPaint().get(), imageLocation, fillFunction, boundaryFunction).apply(getCanvas().getCanvasImage()); - getScratch().setAddScratch(filled); + floodFill.setFillPaint(fillPaint.get()); + floodFill.setOrigin(imageLocation); + floodFill.setFill(attributes.getFillFunction()); + floodFill.setBoundaryFunction(attributes.getBoundaryFunction()); + + getScratch().setAddScratch(floodFill.apply(getCanvas().getCanvasImage()), new Rectangle(getCanvas().getCanvasSize())); getCanvas().commit(); getCanvas().repaint(); } } - /** {@inheritDoc} */ - @Override - public void mouseMoved(MouseEvent e, Point imageLocation) { - setToolCursor(getFillCursor()); - } - - /** - * Gets the cursor associated with this tool. - * @return The cursor associated with this tool. - */ - public Cursor getFillCursor() { - return fillCursor; - } - - /** - * Sets the cursor associated with this tool. - * @param fillCursor The cursor to be displayed when the fill tool is active. - */ - public void setFillCursor(Cursor fillCursor) { - this.fillCursor = fillCursor; - setToolCursor(fillCursor); - } - - /** - * Gets the function used to detect when paint flooding a region has reached a boundary. See - * BoundaryFunction for details. - * - * @return The current boundary function in use. - */ - public BoundaryFunction getBoundaryFunction() { - return boundaryFunction; - } - - /** - * Sets the function used to detect when paint flooding a region has reached a boundary. See - * BoundaryFunction for details. - * - * @param boundaryFunction The boundary function to use. - */ - public void setBoundaryFunction(BoundaryFunction boundaryFunction) { - this.boundaryFunction = boundaryFunction; - - if (boundaryFunction == null) { - this.boundaryFunction = new DefaultBoundaryFunction(); - } - } - - /** - * Gets the function used to color the canvas with paint flooding a region. See {@link FillFunction} for details. - * @return The fill function being used. - */ - public FillFunction getFillFunction() { - return fillFunction; - } - - /** - * Sets the function used to color the canvas with paint flooding a region. See {@link FillFunction} for details. - * @param fillFunction The fill function to use - */ - public void setFillFunction(FillFunction fillFunction) { - this.fillFunction = fillFunction; - - if (fillFunction == null) { - this.fillFunction = new DefaultFillFunction(); - } - } - } diff --git a/src/main/java/com/defano/jmonet/tools/FreeformShapeTool.java b/src/main/java/com/defano/jmonet/tools/FreeformShapeTool.java index 7cd9d58d..0e4262fb 100644 --- a/src/main/java/com/defano/jmonet/tools/FreeformShapeTool.java +++ b/src/main/java/com/defano/jmonet/tools/FreeformShapeTool.java @@ -1,8 +1,11 @@ package com.defano.jmonet.tools; import com.defano.jmonet.canvas.Scratch; +import com.defano.jmonet.context.GraphicsContext; import com.defano.jmonet.model.PaintToolType; -import com.defano.jmonet.tools.base.AbstractPathTool; +import com.defano.jmonet.tools.base.PathTool; +import com.defano.jmonet.tools.base.PathToolDelegate; +import com.defano.jmonet.tools.builder.PaintToolBuilder; import java.awt.*; import java.awt.geom.Path2D; @@ -11,46 +14,51 @@ * Tool allowing user to draw a free-form path (like a paintbrush) that is closed upon completion * and can thusly be filled with paint. */ -public class FreeformShapeTool extends AbstractPathTool { +public class FreeformShapeTool extends PathTool implements PathToolDelegate { private Path2D path; - public FreeformShapeTool() { + /** + * Tool must be constructed via {@link PaintToolBuilder} to handle dependency + * injection. + */ + FreeformShapeTool() { super(PaintToolType.FREEFORM); + setDelegate(this); } /** {@inheritDoc} */ @Override - protected void startPath(Scratch scratch, Stroke stroke, Paint fillPaint, Point initialPoint) { + public void startPath(Scratch scratch, Stroke stroke, Paint strokePaint, Point initialPoint) { path = new Path2D.Double(); path.moveTo(initialPoint.getX(), initialPoint.getY()); } /** {@inheritDoc} */ @Override - protected void addPoint(Scratch scratch, Stroke stroke, Paint fillPaint, Point lastPoint, Point thisPoint) { + public void addPoint(Scratch scratch, Stroke stroke, Paint strokePaint, Point lastPoint, Point thisPoint) { path.lineTo(thisPoint.getX(), thisPoint.getY()); - Graphics2D g = scratch.getAddScratchGraphics(this, stroke, path); + GraphicsContext g = scratch.getAddScratchGraphics(this, stroke, path); g.setStroke(stroke); - g.setPaint(getStrokePaint()); + g.setPaint(getAttributes().getStrokePaint()); g.draw(path); } /** {@inheritDoc} */ @Override - protected void completePath(Scratch scratch, Stroke stroke, Paint fillPaint) { + public void completePath(Scratch scratch, Stroke stroke, Paint strokePaint, Paint fillPaint) { path.closePath(); - Graphics2D g = scratch.getAddScratchGraphics(this, stroke, path); + GraphicsContext g = scratch.getAddScratchGraphics(this, stroke, path); - if (getFillPaint().isPresent()) { - g.setPaint(getFillPaint().get()); + if (fillPaint != null) { + g.setPaint(fillPaint); g.fill(path); } g.setStroke(stroke); - g.setPaint(getStrokePaint()); + g.setPaint(strokePaint); g.draw(path); } } diff --git a/src/main/java/com/defano/jmonet/tools/LassoTool.java b/src/main/java/com/defano/jmonet/tools/LassoTool.java index c8c7e0c6..85686227 100644 --- a/src/main/java/com/defano/jmonet/tools/LassoTool.java +++ b/src/main/java/com/defano/jmonet/tools/LassoTool.java @@ -1,10 +1,11 @@ package com.defano.jmonet.tools; import com.defano.jmonet.model.PaintToolType; -import com.defano.jmonet.tools.base.AbstractSelectionTool; +import com.defano.jmonet.tools.base.SelectionTool; +import com.defano.jmonet.tools.base.SelectionToolDelegate; import com.defano.jmonet.tools.selection.TransformableCanvasSelection; import com.defano.jmonet.tools.selection.TransformableSelection; -import com.defano.jmonet.tools.util.CursorFactory; +import com.defano.jmonet.tools.cursors.CursorFactory; import java.awt.*; import java.awt.geom.AffineTransform; @@ -13,30 +14,36 @@ /** * Selection tool allowing the user to draw a free-form selection path on the canvas. */ -public class LassoTool extends AbstractSelectionTool implements TransformableSelection, TransformableCanvasSelection { +public class LassoTool extends SelectionTool implements TransformableSelection, TransformableCanvasSelection, SelectionToolDelegate { private Path2D selectionBounds; - public LassoTool() { + /** + * Tool must be constructed via {@link com.defano.jmonet.tools.builder.PaintToolBuilder} to handle dependency + * injection. + */ + LassoTool() { super(PaintToolType.LASSO); - super.setBoundaryCursor(CursorFactory.makeLassoCursor()); + + setDelegate(this); + setBoundaryCursor(CursorFactory.makeLassoCursor()); } /** {@inheritDoc} */ @Override - public void resetSelection() { + public void clearSelectionFrame() { selectionBounds = null; } /** {@inheritDoc} */ @Override - public void setSelectionOutline(Rectangle bounds) { + public void setSelectionFrame(Shape bounds) { selectionBounds = new Path2D.Double(bounds); } /** {@inheritDoc} */ @Override - protected void addPointToSelectionFrame(Point initialPoint, Point newPoint, boolean isShiftKeyDown) { + public void addPointToSelectionFrame(Point initialPoint, Point newPoint, boolean isShiftKeyDown) { if (selectionBounds == null) { selectionBounds = new Path2D.Double(); selectionBounds.moveTo(initialPoint.getX(), initialPoint.getY()); @@ -47,7 +54,7 @@ protected void addPointToSelectionFrame(Point initialPoint, Point newPoint, bool /** {@inheritDoc} */ @Override - protected void closeSelectionFrame(Point finalPoint) { + public void closeSelectionFrame(Point finalPoint) { selectionBounds.closePath(); } @@ -59,7 +66,7 @@ public Shape getSelectionFrame() { /** {@inheritDoc} */ @Override - public void translateSelection(int xDelta, int yDelta) { + public void translateSelectionFrame(int xDelta, int yDelta) { selectionBounds.transform(AffineTransform.getTranslateInstance(xDelta, yDelta)); } } diff --git a/src/main/java/com/defano/jmonet/tools/LineTool.java b/src/main/java/com/defano/jmonet/tools/LineTool.java index cffdc8aa..9341bea9 100644 --- a/src/main/java/com/defano/jmonet/tools/LineTool.java +++ b/src/main/java/com/defano/jmonet/tools/LineTool.java @@ -1,8 +1,10 @@ package com.defano.jmonet.tools; import com.defano.jmonet.canvas.Scratch; +import com.defano.jmonet.context.GraphicsContext; import com.defano.jmonet.model.PaintToolType; -import com.defano.jmonet.tools.base.AbstractLineTool; +import com.defano.jmonet.tools.base.LinearTool; +import com.defano.jmonet.tools.base.LinearToolDelegate; import java.awt.*; import java.awt.geom.Line2D; @@ -10,18 +12,23 @@ /** * Tool that draws straight lines on the canvas. */ -public class LineTool extends AbstractLineTool { +public class LineTool extends LinearTool implements LinearToolDelegate { - public LineTool() { + /** + * Tool must be constructed via {@link com.defano.jmonet.tools.builder.PaintToolBuilder} to handle dependency + * injection. + */ + LineTool() { super(PaintToolType.LINE); + setDelegate(this); } /** {@inheritDoc} */ @Override - protected void drawLine(Scratch scratch, Stroke stroke, Paint paint, int x1, int y1, int x2, int y2) { + public void drawLine(Scratch scratch, Stroke stroke, Paint paint, int x1, int y1, int x2, int y2) { Line2D line = new Line2D.Float(x1, y1, x2, y2); - Graphics2D g = scratch.getAddScratchGraphics(this, stroke, line); + GraphicsContext g = scratch.getAddScratchGraphics(this, stroke, line); g.setPaint(paint); g.setStroke(stroke); g.draw(line); diff --git a/src/main/java/com/defano/jmonet/tools/MagnifierTool.java b/src/main/java/com/defano/jmonet/tools/MagnifierTool.java index c3553c45..e9baa84f 100644 --- a/src/main/java/com/defano/jmonet/tools/MagnifierTool.java +++ b/src/main/java/com/defano/jmonet/tools/MagnifierTool.java @@ -1,10 +1,12 @@ package com.defano.jmonet.tools; +import com.defano.jmonet.canvas.observable.SurfaceInteractionObserver; import com.defano.jmonet.canvas.surface.SurfaceScrollController; import com.defano.jmonet.model.PaintToolType; -import com.defano.jmonet.tools.builder.PaintTool; -import com.defano.jmonet.tools.util.CursorFactory; +import com.defano.jmonet.tools.attributes.ToolAttributes; +import com.defano.jmonet.tools.base.BasicTool; +import com.defano.jmonet.tools.cursors.CursorFactory; import javax.swing.*; import java.awt.*; @@ -15,27 +17,29 @@ * Tool that changes the scale factor of the canvas and adjusts the scroll position to re-center the scaled image * in the scrollable viewport. */ -public class MagnifierTool extends PaintTool { +public class MagnifierTool extends BasicTool implements SurfaceInteractionObserver { private Cursor zoomCursor = CursorFactory.makeZoomCursor(); private Cursor zoomInCursor = CursorFactory.makeZoomInCursor(); private Cursor zoomOutCursor = CursorFactory.makeZoomOutCursor(); - private double minimumScale = 1.0; - private double maximumScale = 32.0; - private double magnificationStep = 2; - private boolean recenter = true; - - public MagnifierTool() { + /** + * Tool must be constructed via {@link com.defano.jmonet.tools.builder.PaintToolBuilder} to handle dependency + * injection. + */ + MagnifierTool() { super(PaintToolType.MAGNIFIER); SwingUtilities.invokeLater(() -> setToolCursor(zoomInCursor)); } + @Override + public Cursor getDefaultCursor() { + return zoomInCursor; + } + /** {@inheritDoc} */ @Override public void keyPressed(KeyEvent e) { - super.keyPressed(e); - if (e.isMetaDown() || e.isControlDown() || e.isAltDown()) { setToolCursor(zoomCursor); } else if (e.isShiftDown()) { @@ -48,7 +52,6 @@ public void keyPressed(KeyEvent e) { /** {@inheritDoc} */ @Override public void keyReleased(KeyEvent e) { - super.keyReleased(e); setToolCursor(e.isShiftDown() ? zoomOutCursor : zoomInCursor); } @@ -65,7 +68,7 @@ public void mousePressed(MouseEvent e, Point clickLoc) { } SurfaceScrollController scrollController = getCanvas().getSurfaceScrollController(); - if (recenter && scrollController != null) { + if (getAttributes().isRecenterOnMagnify() && scrollController != null) { recenter(scrollController, clickLoc); } } @@ -83,17 +86,23 @@ private void recenter(SurfaceScrollController controller, Point point) { /** * Multiplies the scale of the canvas this tool is currently active on by the value returned from - * {@link #getMagnificationStep()}. Does not change the scroll position of the canvas. + * {@link ToolAttributes#getMagnificationStep()}. Does not change the scroll position of the canvas. */ public void zoomIn() { + double maximumScale = getAttributes().getMaximumScale(); + double magnificationStep = getAttributes().getMagnificationStep(); + getCanvas().setScale(Math.min(maximumScale, getCanvas().getScale() * magnificationStep)); } /** * Divides the scale of the canvas this tool is currently active on by the value returned from - * {@link #getMagnificationStep()}. Does not change the scroll position of the canvas. + * {@link ToolAttributes#getMagnificationStep()}. Does not change the scroll position of the canvas. */ public void zoomOut() { + double minimumScale = getAttributes().getMinimumScale(); + double magnificationStep = getAttributes().getMagnificationStep(); + getCanvas().setScale(Math.max(minimumScale, getCanvas().getScale() / magnificationStep)); } @@ -103,6 +112,7 @@ public void zoomOut() { * * @return The reset-zoom cursor. */ + @SuppressWarnings("unused") public Cursor getZoomCursor() { return zoomCursor; } @@ -113,6 +123,7 @@ public Cursor getZoomCursor() { * * @param zoomCursor The reset-zoom cursor. */ + @SuppressWarnings("unused") public void setZoomCursor(Cursor zoomCursor) { this.zoomCursor = zoomCursor; } @@ -123,6 +134,7 @@ public void setZoomCursor(Cursor zoomCursor) { * * @return The zoom-in cursor. */ + @SuppressWarnings("unused") public Cursor getZoomInCursor() { return zoomInCursor; } @@ -133,6 +145,7 @@ public Cursor getZoomInCursor() { * * @param zoomInCursor The zoom-in cursor. */ + @SuppressWarnings("unused") public void setZoomInCursor(Cursor zoomInCursor) { this.zoomInCursor = zoomInCursor; } @@ -143,6 +156,7 @@ public void setZoomInCursor(Cursor zoomInCursor) { * * @return The zoom-out cursor. */ + @SuppressWarnings("unused") public Cursor getZoomOutCursor() { return zoomOutCursor; } @@ -153,91 +167,9 @@ public Cursor getZoomOutCursor() { * * @param zoomOutCursor The zoom-out cursor. */ + @SuppressWarnings("unused") public void setZoomOutCursor(Cursor zoomOutCursor) { this.zoomOutCursor = zoomOutCursor; } - /** - * Gets the value by which the canvas scale factor is multiplied or divided when zooming in or zooming out. For - * example, when this value is 2.0, zooming in causes the scale factor to be multiplied by 2 thus scaling the - * canvas from its original size to 2x, then 4x, then 8x and so forth. - * - * @return The zoom in/out scale multiple - */ - public double getMagnificationStep() { - return magnificationStep; - } - - /** - * Sets the value by which the canvas scale factor is multiplied or divided when zooming in or zooming out. For - * example, when this value is 2.0, zooming in causes the scale factor to be multiplied by 2 thus scaling the - * canvas from its original size to 2x, then 4x, then 8x and so forth. - * - * @param magnificationStep The zoom in/out scale multiple - */ - public void setMagnificationStep(double magnificationStep) { - this.magnificationStep = magnificationStep; - } - - /** - * Gets whether the canvas scroll position should be updated when zooming in or out to recenter the pixel that was - * clicked with the magnifier tool. Has no effect when the active canvas is not embedded in a scroll pane and - * managed via a {@link SurfaceScrollController}. - * - * @return True if the clicked pixel will be centered in the scroll pane when zooming in or out; false otherwise. - */ - public boolean isRecenter() { - return recenter; - } - - /** - * Sets whether the canvas scroll position should be updated when zooming in or out to recenter the pixel that was - * clicked with the magnifier tool. Has no effect when the active canvas is not embedded in a scroll pane and - * managed via a {@link SurfaceScrollController}. - * - * @param recenter True to cause the clicked pixel to be centered in the scroll pane when zooming in or out. - */ - public void setRecenter(boolean recenter) { - this.recenter = recenter; - } - - /** - * Gets the minimum allowable scale factor that this tool can adjust the active canvas to. For example, when set - * to 1.0 the magnifier tool will not be able to zoom out (shrink the image) past its normal size. - * - * @return The minimum allowable scale factor this tool will adjust to. - */ - public double getMinimumScale() { - return minimumScale; - } - - /** - * Sets the minimum allowable scale factor that this tool can adjust the active canvas to. For example, when set - * to 1.0 the magnifier tool will not be able to zoom out (shrink the image) past its normal size. - - * @param minimumScale The minimum allowable scale factor this tool will adjust to. - */ - public void setMinimumScale(double minimumScale) { - this.minimumScale = minimumScale; - } - - /** - * Gets the maximum allowable scale factor that this tool can adjust the active canvas to. For example, when set - * to 32.0 the magnifier tool will not be able to magnify the canvas greater than 32x. - * - * @return The maximum allowable scale factor this tool will adjust to. - */ - public double getMaximumScale() { - return maximumScale; - } - - /** - * Gets the maximum allowable scale factor that this tool can adjust the active canvas to. For example, when set - * to 32.0 the magnifier tool will not be able to magnify the canvas greater than 32x. - * - * @param maximumScale The maximum allowable scale factor this tool will adjust to. - */ - public void setMaximumScale(double maximumScale) { - this.maximumScale = maximumScale; - } } diff --git a/src/main/java/com/defano/jmonet/tools/SelectionTool.java b/src/main/java/com/defano/jmonet/tools/MarqueeTool.java similarity index 52% rename from src/main/java/com/defano/jmonet/tools/SelectionTool.java rename to src/main/java/com/defano/jmonet/tools/MarqueeTool.java index 25245a82..68aeae69 100644 --- a/src/main/java/com/defano/jmonet/tools/SelectionTool.java +++ b/src/main/java/com/defano/jmonet/tools/MarqueeTool.java @@ -1,22 +1,28 @@ package com.defano.jmonet.tools; import com.defano.jmonet.model.PaintToolType; -import com.defano.jmonet.tools.base.AbstractSelectionTool; +import com.defano.jmonet.tools.base.SelectionTool; +import com.defano.jmonet.tools.base.SelectionToolDelegate; import com.defano.jmonet.tools.selection.TransformableCanvasSelection; import com.defano.jmonet.tools.selection.TransformableSelection; -import com.defano.jmonet.tools.util.Geometry; +import com.defano.jmonet.tools.util.MathUtils; import java.awt.*; /** * A tool for drawing a rectangular selection on the canvas. */ -public class SelectionTool extends AbstractSelectionTool implements TransformableSelection, TransformableCanvasSelection { +public class MarqueeTool extends SelectionTool implements TransformableSelection, TransformableCanvasSelection, SelectionToolDelegate { private Rectangle selectionBounds; - public SelectionTool() { + /** + * Tool must be constructed via {@link com.defano.jmonet.tools.builder.PaintToolBuilder} to handle dependency + * injection. + */ + MarqueeTool() { super(PaintToolType.SELECTION); + setDelegate(this); } /** {@inheritDoc} */ @@ -27,40 +33,40 @@ public Shape getSelectionFrame() { /** {@inheritDoc} */ @Override - protected void addPointToSelectionFrame(Point initialPoint, Point newPoint, boolean isShiftKeyDown) { + public void addPointToSelectionFrame(Point initialPoint, Point newPoint, boolean isShiftKeyDown) { selectionBounds = new Rectangle(initialPoint); selectionBounds.add(newPoint); // #24: Disallow constraint if it results in selection outside canvas bounds Rectangle canvasBounds = new Rectangle(0, 0, getCanvas().getCanvasSize().width, getCanvas().getCanvasSize().height); - if (isShiftKeyDown && canvasBounds.contains(Geometry.square(initialPoint, newPoint))) { - selectionBounds = Geometry.square(initialPoint, newPoint); + if (isShiftKeyDown && canvasBounds.contains(MathUtils.square(initialPoint, newPoint))) { + selectionBounds = MathUtils.square(initialPoint, newPoint); } else { - selectionBounds = Geometry.rectangle(initialPoint, newPoint); + selectionBounds = MathUtils.rectangle(initialPoint, newPoint); } } /** {@inheritDoc} */ @Override - protected void closeSelectionFrame(Point finalPoint) { + public void closeSelectionFrame(Point finalPoint) { // Nothing to do } /** {@inheritDoc} */ @Override - public void resetSelection() { + public void clearSelectionFrame() { selectionBounds = null; } /** {@inheritDoc} */ @Override - public void setSelectionOutline(Rectangle bounds) { - selectionBounds = bounds; + public void setSelectionFrame(Shape bounds) { + selectionBounds = bounds.getBounds(); } /** {@inheritDoc} */ @Override - public void translateSelection(int xDelta, int yDelta) { + public void translateSelectionFrame(int xDelta, int yDelta) { selectionBounds.setLocation(selectionBounds.x + xDelta, selectionBounds.y + yDelta); } diff --git a/src/main/java/com/defano/jmonet/tools/OvalTool.java b/src/main/java/com/defano/jmonet/tools/OvalTool.java index c2d8bbd7..2876aad1 100644 --- a/src/main/java/com/defano/jmonet/tools/OvalTool.java +++ b/src/main/java/com/defano/jmonet/tools/OvalTool.java @@ -1,8 +1,10 @@ package com.defano.jmonet.tools; import com.defano.jmonet.canvas.Scratch; +import com.defano.jmonet.context.GraphicsContext; import com.defano.jmonet.model.PaintToolType; -import com.defano.jmonet.tools.base.AbstractBoundsTool; +import com.defano.jmonet.tools.base.BoundsTool; +import com.defano.jmonet.tools.base.BoundsToolDelegate; import java.awt.*; import java.awt.geom.Ellipse2D; @@ -10,18 +12,23 @@ /** * Tool for drawing outlined or filled ovals/circles on the canvas. */ -public class OvalTool extends AbstractBoundsTool { +public class OvalTool extends BoundsTool implements BoundsToolDelegate { - public OvalTool() { + /** + * Tool must be constructed via {@link com.defano.jmonet.tools.builder.PaintToolBuilder} to handle dependency + * injection. + */ + OvalTool() { super(PaintToolType.OVAL); + setDelegate(this); } /** {@inheritDoc} */ @Override - protected void strokeBounds(Scratch scratch, Stroke stroke, Paint paint, Rectangle bounds, boolean isShiftDown) { + public void strokeBounds(Scratch scratch, Stroke stroke, Paint paint, Rectangle bounds, boolean isShiftDown) { Ellipse2D oval = new Ellipse2D.Float(bounds.x, bounds.y, bounds.width, bounds.height); - Graphics2D g = scratch.getAddScratchGraphics(this, stroke, oval); + GraphicsContext g = scratch.getAddScratchGraphics(this, stroke, oval); g.setStroke(stroke); g.setPaint(paint); g.draw(oval); @@ -29,8 +36,8 @@ protected void strokeBounds(Scratch scratch, Stroke stroke, Paint paint, Rectang /** {@inheritDoc} */ @Override - protected void fillBounds(Scratch scratch, Paint fill, Rectangle bounds, boolean isShiftDown) { - Graphics2D g = scratch.getAddScratchGraphics(this, null); + public void fillBounds(Scratch scratch, Paint fill, Rectangle bounds, boolean isShiftDown) { + GraphicsContext g = scratch.getAddScratchGraphics(this, null); g.setPaint(fill); g.fillOval(bounds.x, bounds.y, bounds.width, bounds.height); } diff --git a/src/main/java/com/defano/jmonet/tools/PaintbrushTool.java b/src/main/java/com/defano/jmonet/tools/PaintbrushTool.java index 4d7a77ea..aaa51d6a 100644 --- a/src/main/java/com/defano/jmonet/tools/PaintbrushTool.java +++ b/src/main/java/com/defano/jmonet/tools/PaintbrushTool.java @@ -1,7 +1,9 @@ package com.defano.jmonet.tools; import com.defano.jmonet.canvas.Scratch; +import com.defano.jmonet.context.GraphicsContext; import com.defano.jmonet.model.PaintToolType; +import com.defano.jmonet.tools.base.PathToolDelegate; import com.defano.jmonet.tools.base.StrokedCursorPathTool; import java.awt.*; @@ -10,31 +12,42 @@ /** * Tool for drawing free-form, textured paths on the canvas. */ -public class PaintbrushTool extends StrokedCursorPathTool { +public class PaintbrushTool extends StrokedCursorPathTool implements PathToolDelegate { - public PaintbrushTool() { + /** + * Tool must be constructed via {@link com.defano.jmonet.tools.builder.PaintToolBuilder} to handle dependency + * injection. + */ + PaintbrushTool() { super(PaintToolType.PAINTBRUSH); + setDelegate(this); } /** {@inheritDoc} */ @Override - protected void startPath(Scratch scratch, Stroke stroke, Paint fillPaint, Point initialPoint) { + public void startPath(Scratch scratch, Stroke stroke, Paint strokePaint, Point initialPoint) { Line2D line = new Line2D.Float(initialPoint, initialPoint); - Graphics2D g = scratch.getAddScratchGraphics(this, stroke, line); + GraphicsContext g = scratch.getAddScratchGraphics(this, stroke, line); g.setStroke(stroke); - g.setPaint(fillPaint); + g.setPaint(strokePaint); g.draw(line); + } /** {@inheritDoc} */ @Override - protected void addPoint(Scratch scratch, Stroke stroke, Paint fillPaint, Point lastPoint, Point thisPoint) { + public void addPoint(Scratch scratch, Stroke stroke, Paint strokePaint, Point lastPoint, Point thisPoint) { Line2D line = new Line2D.Float(lastPoint, thisPoint); - Graphics2D g = scratch.getAddScratchGraphics(this, stroke, line); + GraphicsContext g = scratch.getAddScratchGraphics(this, stroke, line); g.setStroke(stroke); - g.setPaint(fillPaint); + g.setPaint(strokePaint); g.draw(line); } + + @Override + public void completePath(Scratch scratch, Stroke stroke, Paint strokePaint, Paint fillPaint) { + // Nothing to do + } } diff --git a/src/main/java/com/defano/jmonet/tools/PencilTool.java b/src/main/java/com/defano/jmonet/tools/PencilTool.java index 8481aea6..c5688a37 100644 --- a/src/main/java/com/defano/jmonet/tools/PencilTool.java +++ b/src/main/java/com/defano/jmonet/tools/PencilTool.java @@ -1,9 +1,12 @@ package com.defano.jmonet.tools; import com.defano.jmonet.canvas.Scratch; +import com.defano.jmonet.context.GraphicsContext; import com.defano.jmonet.model.PaintToolType; -import com.defano.jmonet.tools.base.AbstractPathTool; -import com.defano.jmonet.tools.util.CursorFactory; +import com.defano.jmonet.tools.base.PathTool; +import com.defano.jmonet.tools.attributes.ToolAttributes; +import com.defano.jmonet.tools.base.PathToolDelegate; +import com.defano.jmonet.tools.cursors.CursorFactory; import java.awt.*; import java.awt.geom.Line2D; @@ -11,46 +14,60 @@ /** * Tool for drawing or erasing a single-pixel, free-form path on the canvas. */ -public class PencilTool extends AbstractPathTool { +public class PencilTool extends PathTool implements PathToolDelegate { + // Flag indicating whether pencil is operating in eraser mode private boolean isErasing = false; - public PencilTool() { + /** + * Tool must be constructed via {@link com.defano.jmonet.tools.builder.PaintToolBuilder} to handle dependency + * injection. + */ + PencilTool() { super(PaintToolType.PENCIL); - setToolCursor(CursorFactory.makePencilCursor()); + setDelegate(this); + } + + @Override + public Cursor getDefaultCursor() { + return CursorFactory.makePencilCursor(); } /** {@inheritDoc} */ @Override - protected void startPath(Scratch scratch, Stroke stroke, Paint fillPaint, Point initialPoint) { + public void startPath(Scratch scratch, Stroke stroke, Paint strokePaint, Point initialPoint) { + ToolAttributes attributes = getAttributes(); + Color pixel = new Color(getCanvas().getCanvasImage().getRGB(initialPoint.x, initialPoint.y), true); - if (getEraseColor() == null) { - isErasing = pixel.getAlpha() >= 128; - } else { - Color eraseColor = getEraseColor(); - isErasing = eraseColor.getRed() != pixel.getRed() || eraseColor.getBlue() != pixel.getBlue() || eraseColor.getGreen() != pixel.getGreen(); - } + // Pencil erases when user begins stoke over a "marked" pixel, otherwise pencil marks canvas + isErasing = attributes.getMarkPredicate().isMarked(pixel, attributes.getEraseColor()); - renderStroke(scratch, fillPaint, new Line2D.Float(initialPoint, initialPoint)); + renderStroke(scratch, strokePaint, new Line2D.Float(initialPoint, initialPoint)); } /** {@inheritDoc} */ @Override - protected void addPoint(Scratch scratch, Stroke stroke, Paint fillPaint, Point lastPoint, Point thisPoint) { - renderStroke(scratch, fillPaint, new Line2D.Float(lastPoint, thisPoint)); + public void addPoint(Scratch scratch, Stroke stroke, Paint strokePaint, Point lastPoint, Point thisPoint) { + renderStroke(scratch, strokePaint, new Line2D.Float(lastPoint, thisPoint)); + } + + @Override + public void completePath(Scratch scratch, Stroke stroke, Paint strokePaint, Paint fillPaint) { + // Nothing to do } private void renderStroke(Scratch scratch, Paint fillPaint, Line2D line) { if (isErasing) { - erase(scratch, line, new BasicStroke(1)); + scratch.erase(this, line, new BasicStroke(1)); } else { - Graphics2D g = scratch.getAddScratchGraphics(this, new BasicStroke(1), line); + GraphicsContext g = scratch.getAddScratchGraphics(this, new BasicStroke(1), line); g.setStroke(new BasicStroke(1)); g.setPaint(fillPaint); g.draw(line); } } + } diff --git a/src/main/java/com/defano/jmonet/tools/PerspectiveTool.java b/src/main/java/com/defano/jmonet/tools/PerspectiveTool.java index c903fe32..8e9e601a 100644 --- a/src/main/java/com/defano/jmonet/tools/PerspectiveTool.java +++ b/src/main/java/com/defano/jmonet/tools/PerspectiveTool.java @@ -1,24 +1,30 @@ package com.defano.jmonet.tools; -import com.defano.jmonet.algo.transform.image.ProjectionTransform; +import com.defano.jmonet.tools.base.TransformToolDelegate; +import com.defano.jmonet.transform.image.ProjectionTransform; import com.defano.jmonet.model.FlexQuadrilateral; import com.defano.jmonet.model.PaintToolType; -import com.defano.jmonet.tools.base.AbstractTransformTool; +import com.defano.jmonet.tools.base.TransformTool; import java.awt.*; /** * Tool for making either the left or right side appear closer/further away than the other. */ -public class PerspectiveTool extends AbstractTransformTool { +public class PerspectiveTool extends TransformTool implements TransformToolDelegate { - public PerspectiveTool() { + /** + * Tool must be constructed via {@link com.defano.jmonet.tools.builder.PaintToolBuilder} to handle dependency + * injection. + */ + PerspectiveTool() { super(PaintToolType.PERSPECTIVE); + setTransformToolDelegate(this); } /** {@inheritDoc} */ @Override - protected void moveTopLeft(FlexQuadrilateral quadrilateral, Point newPosition, boolean isShiftDown) { + public void moveTopLeft(FlexQuadrilateral quadrilateral, Point newPosition, boolean isShiftDown) { int bottomLeft = quadrilateral.getBottomLeft().y - (newPosition.y - quadrilateral.getTopLeft().y); quadrilateral.setBottomLeft(new Point(quadrilateral.getBottomLeft().x, bottomLeft)); @@ -29,7 +35,7 @@ protected void moveTopLeft(FlexQuadrilateral quadrilateral, Point newPosition, b /** {@inheritDoc} */ @Override - protected void moveTopRight(FlexQuadrilateral quadrilateral, Point newPosition, boolean isShiftDown) { + public void moveTopRight(FlexQuadrilateral quadrilateral, Point newPosition, boolean isShiftDown) { int bottomRight = quadrilateral.getBottomRight().y - (newPosition.y - quadrilateral.getTopRight().y); quadrilateral.setBottomRight(new Point(quadrilateral.getBottomRight().x, bottomRight)); @@ -40,7 +46,7 @@ protected void moveTopRight(FlexQuadrilateral quadrilateral, Point newPosition, /** {@inheritDoc} */ @Override - protected void moveBottomLeft(FlexQuadrilateral quadrilateral, Point newPosition, boolean isShiftDown) { + public void moveBottomLeft(FlexQuadrilateral quadrilateral, Point newPosition, boolean isShiftDown) { int topLeft = quadrilateral.getTopLeft().y - (newPosition.y - quadrilateral.getBottomLeft().y); quadrilateral.setTopLeft(new Point(quadrilateral.getTopLeft().x, topLeft)); @@ -51,7 +57,7 @@ protected void moveBottomLeft(FlexQuadrilateral quadrilateral, Point newPosition /** {@inheritDoc} */ @Override - protected void moveBottomRight(FlexQuadrilateral quadrilateral, Point newPosition, boolean isShiftDown) { + public void moveBottomRight(FlexQuadrilateral quadrilateral, Point newPosition, boolean isShiftDown) { int topRight = quadrilateral.getTopRight().y - (newPosition.y - quadrilateral.getBottomRight().y); quadrilateral.setTopRight(new Point(quadrilateral.getTopRight().x, topRight)); diff --git a/src/main/java/com/defano/jmonet/tools/PolygonTool.java b/src/main/java/com/defano/jmonet/tools/PolygonTool.java index 0f1ccd72..e2f1bc12 100644 --- a/src/main/java/com/defano/jmonet/tools/PolygonTool.java +++ b/src/main/java/com/defano/jmonet/tools/PolygonTool.java @@ -1,8 +1,10 @@ package com.defano.jmonet.tools; import com.defano.jmonet.canvas.Scratch; +import com.defano.jmonet.context.GraphicsContext; import com.defano.jmonet.model.PaintToolType; -import com.defano.jmonet.tools.base.AbstractPolylineTool; +import com.defano.jmonet.tools.base.PolylineTool; +import com.defano.jmonet.tools.base.PolylineToolDelegate; import java.awt.*; import java.awt.geom.Path2D; @@ -10,18 +12,23 @@ /** * Tool to draw outlined or filled irregular polygons on the canvas. */ -public class PolygonTool extends AbstractPolylineTool { +public class PolygonTool extends PolylineTool implements PolylineToolDelegate { - public PolygonTool() { + /** + * Tool must be constructed via {@link com.defano.jmonet.tools.builder.PaintToolBuilder} to handle dependency + * injection. + */ + PolygonTool() { super(PaintToolType.POLYGON); + setDelegate(this); } /** {@inheritDoc} */ @Override - protected void strokePolyline(Scratch scratch, Stroke stroke, Paint paint, int[] xPoints, int[] yPoints) { + public void strokePolyline(Scratch scratch, Stroke stroke, Paint paint, int[] xPoints, int[] yPoints) { Path2D poly = getPolylineShape(xPoints, yPoints, xPoints.length); - Graphics2D g = scratch.getAddScratchGraphics(this, stroke, poly); + GraphicsContext g = scratch.getAddScratchGraphics(this, stroke, poly); g.setPaint(paint); g.setStroke(stroke); g.draw(poly); @@ -29,10 +36,10 @@ protected void strokePolyline(Scratch scratch, Stroke stroke, Paint paint, int[] /** {@inheritDoc} */ @Override - protected void strokePolygon(Scratch scratch, Stroke stroke, Paint strokePaint, int[] xPoints, int[] yPoints) { + public void strokePolygon(Scratch scratch, Stroke stroke, Paint strokePaint, int[] xPoints, int[] yPoints) { Path2D polygon = getPolygonShape(xPoints, yPoints, xPoints.length); - Graphics2D g = scratch.getAddScratchGraphics(this, stroke, polygon); + GraphicsContext g = scratch.getAddScratchGraphics(this, stroke, polygon); g.setStroke(stroke); g.setPaint(strokePaint); g.draw(polygon); @@ -40,19 +47,19 @@ protected void strokePolygon(Scratch scratch, Stroke stroke, Paint strokePaint, /** {@inheritDoc} */ @Override - protected void fillPolygon(Scratch scratch, Paint fillPaint, int[] xPoints, int[] yPoints) { - Graphics2D g = scratch.getAddScratchGraphics(this, null); + public void fillPolygon(Scratch scratch, Paint fillPaint, int[] xPoints, int[] yPoints) { + GraphicsContext g = scratch.getAddScratchGraphics(this, null); g.setPaint(fillPaint); g.fillPolygon(xPoints, yPoints, xPoints.length); } - private Path2D getPolygonShape(int xPoints[], int yPoints[], int points) { + private Path2D getPolygonShape(int[] xPoints, int[] yPoints, int points) { Path2D poly = getPolylineShape(xPoints, yPoints, points); poly.closePath(); return poly; } - private Path2D getPolylineShape(int xPoints[], int yPoints[], int points) { + private Path2D getPolylineShape(int[] xPoints, int[] yPoints, int points) { Path2D poly = new Path2D.Double(); if (points > 0) { diff --git a/src/main/java/com/defano/jmonet/tools/ProjectionTool.java b/src/main/java/com/defano/jmonet/tools/ProjectionTool.java index e3a0310c..970db998 100644 --- a/src/main/java/com/defano/jmonet/tools/ProjectionTool.java +++ b/src/main/java/com/defano/jmonet/tools/ProjectionTool.java @@ -1,45 +1,51 @@ package com.defano.jmonet.tools; -import com.defano.jmonet.algo.transform.image.ProjectionTransform; +import com.defano.jmonet.tools.base.TransformToolDelegate; +import com.defano.jmonet.transform.image.ProjectionTransform; import com.defano.jmonet.model.FlexQuadrilateral; import com.defano.jmonet.model.PaintToolType; -import com.defano.jmonet.tools.base.AbstractTransformTool; +import com.defano.jmonet.tools.base.TransformTool; import java.awt.*; /** * Tool for performing a projection of a selected image onto an arbitrary quadrilateral. */ -public class ProjectionTool extends AbstractTransformTool { +public class ProjectionTool extends TransformTool implements TransformToolDelegate { - public ProjectionTool() { + /** + * Tool must be constructed via {@link com.defano.jmonet.tools.builder.PaintToolBuilder} to handle dependency + * injection. + */ + ProjectionTool() { super(PaintToolType.PROJECTION); + setTransformToolDelegate(this); } /** {@inheritDoc} */ @Override - protected void moveTopLeft(FlexQuadrilateral quadrilateral, Point newPosition, boolean isShiftDown) { + public void moveTopLeft(FlexQuadrilateral quadrilateral, Point newPosition, boolean isShiftDown) { quadrilateral.setTopLeft(newPosition); setSelectedImage(new ProjectionTransform(quadrilateral.translate(getSelectedImageLocation().x, getSelectedImageLocation().y)).apply(getOriginalImage())); } /** {@inheritDoc} */ @Override - protected void moveTopRight(FlexQuadrilateral quadrilateral, Point newPosition, boolean isShiftDown) { + public void moveTopRight(FlexQuadrilateral quadrilateral, Point newPosition, boolean isShiftDown) { quadrilateral.setTopRight(newPosition); setSelectedImage(new ProjectionTransform(quadrilateral.translate(getSelectedImageLocation().x, getSelectedImageLocation().y)).apply(getOriginalImage())); } /** {@inheritDoc} */ @Override - protected void moveBottomLeft(FlexQuadrilateral quadrilateral, Point newPosition, boolean isShiftDown) { + public void moveBottomLeft(FlexQuadrilateral quadrilateral, Point newPosition, boolean isShiftDown) { quadrilateral.setBottomLeft(newPosition); setSelectedImage(new ProjectionTransform(quadrilateral.translate(getSelectedImageLocation().x, getSelectedImageLocation().y)).apply(getOriginalImage())); } /** {@inheritDoc} */ @Override - protected void moveBottomRight(FlexQuadrilateral quadrilateral, Point newPosition, boolean isShiftDown) { + public void moveBottomRight(FlexQuadrilateral quadrilateral, Point newPosition, boolean isShiftDown) { quadrilateral.setBottomRight(newPosition); setSelectedImage(new ProjectionTransform(quadrilateral.translate(getSelectedImageLocation().x, getSelectedImageLocation().y)).apply(getOriginalImage())); } diff --git a/src/main/java/com/defano/jmonet/tools/RectangleTool.java b/src/main/java/com/defano/jmonet/tools/RectangleTool.java index 9b7d3fd8..2986c286 100644 --- a/src/main/java/com/defano/jmonet/tools/RectangleTool.java +++ b/src/main/java/com/defano/jmonet/tools/RectangleTool.java @@ -1,26 +1,33 @@ package com.defano.jmonet.tools; import com.defano.jmonet.canvas.Scratch; +import com.defano.jmonet.context.GraphicsContext; import com.defano.jmonet.model.PaintToolType; -import com.defano.jmonet.tools.base.AbstractBoundsTool; +import com.defano.jmonet.tools.base.BoundsTool; +import com.defano.jmonet.tools.base.BoundsToolDelegate; import java.awt.*; /** * Draws outlined or filled rectangles/squares on the canvas. */ -public class RectangleTool extends AbstractBoundsTool { +public class RectangleTool extends BoundsTool implements BoundsToolDelegate { - public RectangleTool() { + /** + * Tool must be constructed via {@link com.defano.jmonet.tools.builder.PaintToolBuilder} to handle dependency + * injection. + */ + RectangleTool() { super(PaintToolType.RECTANGLE); + setDelegate(this); } /** {@inheritDoc} */ @Override - protected void strokeBounds(Scratch scratch, Stroke stroke, Paint paint, Rectangle bounds, boolean isShiftDown) { + public void strokeBounds(Scratch scratch, Stroke stroke, Paint paint, Rectangle bounds, boolean isShiftDown) { Rectangle rectangle = new Rectangle(bounds.x, bounds.y, bounds.width, bounds.height); - Graphics2D g = scratch.getAddScratchGraphics(this, stroke, rectangle); + GraphicsContext g = scratch.getAddScratchGraphics(this, stroke, rectangle); g.setStroke(stroke); g.setPaint(paint); g.draw(rectangle); @@ -28,9 +35,11 @@ protected void strokeBounds(Scratch scratch, Stroke stroke, Paint paint, Rectang /** {@inheritDoc} */ @Override - protected void fillBounds(Scratch scratch, Paint fill, Rectangle bounds, boolean isShiftDown) { - Graphics2D g = scratch.getAddScratchGraphics(this, null); + public void fillBounds(Scratch scratch, Paint fill, Rectangle bounds, boolean isShiftDown) { + Rectangle rectangle = new Rectangle(bounds.x, bounds.y, bounds.width, bounds.height); + + GraphicsContext g = scratch.getAddScratchGraphics(this, rectangle); g.setPaint(fill); - g.fillRect(bounds.x, bounds.y, bounds.width, bounds.height); + g.fill(rectangle); } } diff --git a/src/main/java/com/defano/jmonet/tools/RotateTool.java b/src/main/java/com/defano/jmonet/tools/RotateTool.java index 8a2be668..9b04784d 100644 --- a/src/main/java/com/defano/jmonet/tools/RotateTool.java +++ b/src/main/java/com/defano/jmonet/tools/RotateTool.java @@ -1,9 +1,11 @@ package com.defano.jmonet.tools; -import com.defano.jmonet.algo.transform.image.ApplyAffineTransform; +import com.defano.jmonet.context.GraphicsContext; import com.defano.jmonet.model.PaintToolType; -import com.defano.jmonet.tools.base.AbstractSelectionTool; -import com.defano.jmonet.tools.util.Geometry; +import com.defano.jmonet.tools.base.SelectionTool; +import com.defano.jmonet.tools.base.SelectionToolDelegate; +import com.defano.jmonet.tools.util.MathUtils; +import com.defano.jmonet.transform.image.ApplyAffineTransform; import java.awt.*; import java.awt.event.MouseEvent; @@ -13,7 +15,7 @@ /** * Tool for selecting a bounding box and free-rotating the selected image about its center-point. */ -public class RotateTool extends AbstractSelectionTool { +public class RotateTool extends SelectionTool implements SelectionToolDelegate { private Point centerpoint; // Point around which image rotates private Point dragLocation; // Location of the drag handle @@ -27,8 +29,13 @@ public class RotateTool extends AbstractSelectionTool { private boolean rotating = false; // Drag-rotate in progress - public RotateTool() { + /** + * Tool must be constructed via {@link com.defano.jmonet.tools.builder.PaintToolBuilder} to handle dependency + * injection. + */ + RotateTool() { super(PaintToolType.ROTATE); + setDelegate(this); } /** @@ -73,17 +80,17 @@ public void mousePressed(MouseEvent e, Point imageLocation) { * {@inheritDoc} */ @Override - public void mouseDragged(MouseEvent e, Point imageLocation) { + public void mouseDragged(MouseEvent e, Point canvasLoc) { if (hasSelection() && rotating) { setDirty(); // Mutating the selected image // Calculate the rotation angle - dragLocation = imageLocation; - double degrees = Geometry.angle(centerpoint.x, centerpoint.y, dragLocation.x, dragLocation.y); + dragLocation = canvasLoc; + double degrees = MathUtils.angle(centerpoint.x, centerpoint.y, dragLocation.x, dragLocation.y); if (e.isShiftDown()) { - degrees = Geometry.round(degrees, getConstrainedAngle()); + degrees = MathUtils.nearestRound(degrees, getAttributes().getConstrainedAngle()); } double angle = Math.toRadians(degrees); @@ -95,7 +102,7 @@ public void mouseDragged(MouseEvent e, Point imageLocation) { // Rotate the selected canvas image setSelectedImage(new ApplyAffineTransform(AffineTransform.getRotateInstance(angle, originalImage.getWidth() / 2.0, originalImage.getHeight() / 2.0)).apply(originalImage)); } else { - super.mouseDragged(e, imageLocation); + super.mouseDragged(e, canvasLoc); } } @@ -103,7 +110,7 @@ public void mouseDragged(MouseEvent e, Point imageLocation) { * {@inheritDoc} */ @Override - public void resetSelection() { + public void clearSelectionFrame() { selectionBounds = null; originalSelectionBounds = null; centerpoint = null; @@ -116,15 +123,15 @@ public void resetSelection() { * {@inheritDoc} */ @Override - public void setSelectionOutline(Rectangle bounds) { - selectionBounds = bounds; + public void setSelectionFrame(Shape bounds) { + selectionBounds = bounds.getBounds(); } /** * {@inheritDoc} */ @Override - protected void addPointToSelectionFrame(Point initialPoint, Point newPoint, boolean isShiftKeyDown) { + public void addPointToSelectionFrame(Point initialPoint, Point newPoint, boolean isShiftKeyDown) { int handleSize = 8; Rectangle selectionRectangle = new Rectangle(initialPoint); @@ -157,7 +164,7 @@ public Shape getSelectionFrame() { * {@inheritDoc} */ @Override - public void translateSelection(int xDelta, int yDelta) { + public void translateSelectionFrame(int xDelta, int yDelta) { // Nothing to do; user can't move selection selectionBounds = AffineTransform.getTranslateInstance(xDelta, yDelta).createTransformedShape(selectionBounds); originalSelectionBounds = AffineTransform.getTranslateInstance(xDelta, yDelta).createTransformedShape(originalSelectionBounds); @@ -178,7 +185,7 @@ protected Point getSelectedImageLocation() { return getSelectionFrame().getBounds().getLocation(); } else { Rectangle enlargedBounds = originalImage.getRaster().getBounds(); - Geometry.center(enlargedBounds, originalSelectionBounds.getBounds()); + MathUtils.center(enlargedBounds, originalSelectionBounds.getBounds()); return enlargedBounds.getLocation(); } } @@ -200,7 +207,7 @@ private BufferedImage square(BufferedImage image) { throw new IllegalArgumentException("Image to square cannot be null."); } - int diagonal = (int) Math.ceil(Math.sqrt(image.getHeight() * image.getHeight() + image.getWidth() * image.getWidth())); + int diagonal = (int) Math.ceil(Math.sqrt((double)(image.getHeight() * image.getHeight()) + (double)(image.getWidth() * image.getWidth()))); int deltaX = diagonal - image.getWidth(); int deltaY = diagonal - image.getHeight(); @@ -208,7 +215,7 @@ private BufferedImage square(BufferedImage image) { BufferedImage enlarged = new BufferedImage(diagonal, diagonal, image.getType()); Graphics2D g = enlarged.createGraphics(); - g.drawImage(image, AffineTransform.getTranslateInstance(deltaX / 2, deltaY / 2), null); + g.drawImage(image, AffineTransform.getTranslateInstance(deltaX / 2.0, deltaY / 2.0), null); g.dispose(); return enlarged; @@ -222,7 +229,7 @@ public void redrawSelection(boolean includeFrame) { super.redrawSelection(includeFrame); // Draw the drag handle on the selection - Graphics2D g = getCanvas().getScratch().getAddScratchGraphics(this, null); + GraphicsContext g = getCanvas().getScratch().getAddScratchGraphics(this, null); g.setColor(Color.black); g.fill(dragHandle); g.dispose(); diff --git a/src/main/java/com/defano/jmonet/tools/RoundRectangleTool.java b/src/main/java/com/defano/jmonet/tools/RoundRectangleTool.java index d014b007..cbb655d6 100644 --- a/src/main/java/com/defano/jmonet/tools/RoundRectangleTool.java +++ b/src/main/java/com/defano/jmonet/tools/RoundRectangleTool.java @@ -1,8 +1,10 @@ package com.defano.jmonet.tools; import com.defano.jmonet.canvas.Scratch; +import com.defano.jmonet.context.GraphicsContext; import com.defano.jmonet.model.PaintToolType; -import com.defano.jmonet.tools.base.AbstractBoundsTool; +import com.defano.jmonet.tools.base.BoundsTool; +import com.defano.jmonet.tools.base.BoundsToolDelegate; import java.awt.*; import java.awt.geom.RoundRectangle2D; @@ -10,19 +12,24 @@ /** * Tool for drawing outlined or filled rounded-rectangles on the canvas. */ -public class RoundRectangleTool extends AbstractBoundsTool { +public class RoundRectangleTool extends BoundsTool implements BoundsToolDelegate { - public RoundRectangleTool() { + /** + * Tool must be constructed via {@link com.defano.jmonet.tools.builder.PaintToolBuilder} to handle dependency + * injection. + */ + RoundRectangleTool() { super(PaintToolType.ROUND_RECTANGLE); + setDelegate(this); } /** {@inheritDoc} */ @Override - protected void strokeBounds(Scratch scratch, Stroke stroke, Paint paint, Rectangle bounds, boolean isShiftDown) { - int cornerRadius = getCornerRadiusObservable().blockingFirst(); + public void strokeBounds(Scratch scratch, Stroke stroke, Paint paint, Rectangle bounds, boolean isShiftDown) { + int cornerRadius = getAttributes().getCornerRadius(); RoundRectangle2D roundRect = new RoundRectangle2D.Double(bounds.x, bounds.y, bounds.width, bounds.height, cornerRadius, cornerRadius); - Graphics2D g = scratch.getAddScratchGraphics(this, stroke, roundRect); + GraphicsContext g = scratch.getAddScratchGraphics(this, stroke, roundRect); g.setPaint(paint); g.setStroke(stroke); g.draw(roundRect); @@ -30,10 +37,11 @@ protected void strokeBounds(Scratch scratch, Stroke stroke, Paint paint, Rectang /** {@inheritDoc} */ @Override - protected void fillBounds(Scratch scratch, Paint fill, Rectangle bounds, boolean isShiftDown) { - Graphics2D g = scratch.getAddScratchGraphics(this, null); - int cornerRadius = getCornerRadiusObservable().blockingFirst(); + public void fillBounds(Scratch scratch, Paint fill, Rectangle bounds, boolean isShiftDown) { + int cornerRadius = getAttributes().getCornerRadius(); + RoundRectangle2D roundRect = new RoundRectangle2D.Double(bounds.x, bounds.y, bounds.width, bounds.height, cornerRadius, cornerRadius); + GraphicsContext g = scratch.getAddScratchGraphics(this, roundRect); g.setPaint(fill); g.fillRoundRect(bounds.x, bounds.y, bounds.width, bounds.height, cornerRadius, cornerRadius); } diff --git a/src/main/java/com/defano/jmonet/tools/RubberSheetTool.java b/src/main/java/com/defano/jmonet/tools/RubberSheetTool.java index 4b9b641e..3be142a2 100644 --- a/src/main/java/com/defano/jmonet/tools/RubberSheetTool.java +++ b/src/main/java/com/defano/jmonet/tools/RubberSheetTool.java @@ -1,45 +1,51 @@ package com.defano.jmonet.tools; -import com.defano.jmonet.algo.transform.image.RubbersheetTransform; +import com.defano.jmonet.tools.base.TransformToolDelegate; +import com.defano.jmonet.transform.image.RubbersheetTransform; import com.defano.jmonet.model.FlexQuadrilateral; import com.defano.jmonet.model.PaintToolType; -import com.defano.jmonet.tools.base.AbstractTransformTool; +import com.defano.jmonet.tools.base.TransformTool; import java.awt.*; /** * Tool for performing a rubber sheet projection of the image. */ -public class RubberSheetTool extends AbstractTransformTool { +public class RubberSheetTool extends TransformTool implements TransformToolDelegate { - public RubberSheetTool() { + /** + * Tool must be constructed via {@link com.defano.jmonet.tools.builder.PaintToolBuilder} to handle dependency + * injection. + */ + RubberSheetTool() { super(PaintToolType.RUBBERSHEET); + setTransformToolDelegate(this); } /** {@inheritDoc} */ @Override - protected void moveTopLeft(FlexQuadrilateral quadrilateral, Point newPosition, boolean isShiftDown) { + public void moveTopLeft(FlexQuadrilateral quadrilateral, Point newPosition, boolean isShiftDown) { quadrilateral.setTopLeft(newPosition); setSelectedImage(new RubbersheetTransform(quadrilateral.translate(getSelectedImageLocation().x, getSelectedImageLocation().y)).apply(getOriginalImage())); } /** {@inheritDoc} */ @Override - protected void moveTopRight(FlexQuadrilateral quadrilateral, Point newPosition, boolean isShiftDown) { + public void moveTopRight(FlexQuadrilateral quadrilateral, Point newPosition, boolean isShiftDown) { quadrilateral.setTopRight(newPosition); setSelectedImage(new RubbersheetTransform(quadrilateral.translate(getSelectedImageLocation().x, getSelectedImageLocation().y)).apply(getOriginalImage())); } /** {@inheritDoc} */ @Override - protected void moveBottomLeft(FlexQuadrilateral quadrilateral, Point newPosition, boolean isShiftDown) { + public void moveBottomLeft(FlexQuadrilateral quadrilateral, Point newPosition, boolean isShiftDown) { quadrilateral.setBottomLeft(newPosition); setSelectedImage(new RubbersheetTransform(quadrilateral.translate(getSelectedImageLocation().x, getSelectedImageLocation().y)).apply(getOriginalImage())); } /** {@inheritDoc} */ @Override - protected void moveBottomRight(FlexQuadrilateral quadrilateral, Point newPosition, boolean isShiftDown) { + public void moveBottomRight(FlexQuadrilateral quadrilateral, Point newPosition, boolean isShiftDown) { quadrilateral.setBottomRight(newPosition); setSelectedImage(new RubbersheetTransform(quadrilateral.translate(getSelectedImageLocation().x, getSelectedImageLocation().y)).apply(getOriginalImage())); } diff --git a/src/main/java/com/defano/jmonet/tools/ScaleTool.java b/src/main/java/com/defano/jmonet/tools/ScaleTool.java index e9d04f8d..6ecf6f1f 100644 --- a/src/main/java/com/defano/jmonet/tools/ScaleTool.java +++ b/src/main/java/com/defano/jmonet/tools/ScaleTool.java @@ -1,10 +1,11 @@ package com.defano.jmonet.tools; -import com.defano.jmonet.algo.transform.image.ScaleTransform; +import com.defano.jmonet.tools.base.TransformToolDelegate; +import com.defano.jmonet.transform.image.ScaleTransform; import com.defano.jmonet.model.FlexQuadrilateral; import com.defano.jmonet.model.PaintToolType; -import com.defano.jmonet.tools.base.AbstractTransformTool; -import com.defano.jmonet.tools.util.Geometry; +import com.defano.jmonet.tools.base.TransformTool; +import com.defano.jmonet.tools.util.MathUtils; import java.awt.*; import java.awt.geom.Rectangle2D; @@ -12,17 +13,22 @@ /** * A tool for scaling and resizing the rectangular bounds of an image. */ -public class ScaleTool extends AbstractTransformTool { +public class ScaleTool extends TransformTool implements TransformToolDelegate { - public ScaleTool() { + /** + * Tool must be constructed via {@link com.defano.jmonet.tools.builder.PaintToolBuilder} to handle dependency + * injection. + */ + ScaleTool() { super(PaintToolType.SCALE); + setTransformToolDelegate(this); } /** {@inheritDoc} */ @Override - protected void moveTopLeft(FlexQuadrilateral quadrilateral, Point newPosition, boolean isShiftDown) { + public void moveTopLeft(FlexQuadrilateral quadrilateral, Point newPosition, boolean isShiftDown) { if (isShiftDown) { - newPosition = Geometry.extrapolate(originalQuad().getBottomRightTopLeftDiagonal(), quadrilateral.getBottomRight(), newPosition); + newPosition = MathUtils.extrapolate(originalQuad().getBottomRightTopLeftDiagonal(), quadrilateral.getBottomRight(), newPosition); } quadrilateral.getTopLeft().setLocation(newPosition); @@ -43,9 +49,9 @@ protected void moveTopLeft(FlexQuadrilateral quadrilateral, Point newPosition, b /** {@inheritDoc} */ @Override - protected void moveTopRight(FlexQuadrilateral quadrilateral, Point newPosition, boolean isShiftDown) { + public void moveTopRight(FlexQuadrilateral quadrilateral, Point newPosition, boolean isShiftDown) { if (isShiftDown) { - newPosition = Geometry.extrapolate(originalQuad().getBottomLeftTopRightDiagonal(), quadrilateral.getBottomLeft(), newPosition); + newPosition = MathUtils.extrapolate(originalQuad().getBottomLeftTopRightDiagonal(), quadrilateral.getBottomLeft(), newPosition); } quadrilateral.getTopRight().setLocation(newPosition); @@ -66,9 +72,9 @@ protected void moveTopRight(FlexQuadrilateral quadrilateral, Point newPosition, /** {@inheritDoc} */ @Override - protected void moveBottomLeft(FlexQuadrilateral quadrilateral, Point newPosition, boolean isShiftDown) { + public void moveBottomLeft(FlexQuadrilateral quadrilateral, Point newPosition, boolean isShiftDown) { if (isShiftDown) { - newPosition = Geometry.extrapolate(originalQuad().getTopRightBottomLeftDiagonal(), quadrilateral.getTopRight(), newPosition); + newPosition = MathUtils.extrapolate(originalQuad().getTopRightBottomLeftDiagonal(), quadrilateral.getTopRight(), newPosition); } quadrilateral.getBottomLeft().setLocation(newPosition); @@ -89,9 +95,9 @@ protected void moveBottomLeft(FlexQuadrilateral quadrilateral, Point newPosition /** {@inheritDoc} */ @Override - protected void moveBottomRight(FlexQuadrilateral quadrilateral, Point newPosition, boolean isShiftDown) { + public void moveBottomRight(FlexQuadrilateral quadrilateral, Point newPosition, boolean isShiftDown) { if (isShiftDown) { - newPosition = Geometry.extrapolate(originalQuad().getTopLeftBottomRightDiagonal(), quadrilateral.getTopLeft(), newPosition); + newPosition = MathUtils.extrapolate(originalQuad().getTopLeftBottomRightDiagonal(), quadrilateral.getTopLeft(), newPosition); } quadrilateral.getBottomRight().setLocation(newPosition); diff --git a/src/main/java/com/defano/jmonet/tools/ShapeTool.java b/src/main/java/com/defano/jmonet/tools/ShapeTool.java index 683be799..d0cc9c18 100644 --- a/src/main/java/com/defano/jmonet/tools/ShapeTool.java +++ b/src/main/java/com/defano/jmonet/tools/ShapeTool.java @@ -1,9 +1,11 @@ package com.defano.jmonet.tools; import com.defano.jmonet.canvas.Scratch; +import com.defano.jmonet.context.GraphicsContext; import com.defano.jmonet.model.PaintToolType; -import com.defano.jmonet.tools.base.AbstractBoundsTool; -import com.defano.jmonet.tools.util.Geometry; +import com.defano.jmonet.tools.base.BoundsTool; +import com.defano.jmonet.tools.base.BoundsToolDelegate; +import com.defano.jmonet.tools.util.MathUtils; import java.awt.*; @@ -11,18 +13,23 @@ * Tool for drawing regular polygons ("shapes") based on a configurable number of sides. For example, triangles, * squares, pentagons, hexagons, etc. */ -public class ShapeTool extends AbstractBoundsTool { +public class ShapeTool extends BoundsTool implements BoundsToolDelegate { - public ShapeTool() { + /** + * Tool must be constructed via {@link com.defano.jmonet.tools.builder.PaintToolBuilder} to handle dependency + * injection. + */ + ShapeTool() { super(PaintToolType.SHAPE); + setDelegate(this); } /** {@inheritDoc} */ @Override - protected void strokeBounds(Scratch scratch, Stroke stroke, Paint paint, Rectangle bounds, boolean isShiftDown) { - Polygon poly = Geometry.polygon(initialPoint, getShapeSides(), getRadius(), getRotationAngle(isShiftDown)); + public void strokeBounds(Scratch scratch, Stroke stroke, Paint paint, Rectangle bounds, boolean isShiftDown) { + Polygon poly = MathUtils.polygon(getInitialPoint(), getAttributes().getShapeSides(), getRadius(), getRotationAngle(isShiftDown)); - Graphics2D g = scratch.getAddScratchGraphics(this, stroke, poly); + GraphicsContext g = scratch.getAddScratchGraphics(this, stroke, poly); g.setStroke(stroke); g.setPaint(paint); g.draw(poly); @@ -30,21 +37,21 @@ protected void strokeBounds(Scratch scratch, Stroke stroke, Paint paint, Rectang /** {@inheritDoc} */ @Override - protected void fillBounds(Scratch scratch, Paint fill, Rectangle bounds, boolean isShiftDown) { - Graphics2D g = scratch.getAddScratchGraphics(this, null); + public void fillBounds(Scratch scratch, Paint fill, Rectangle bounds, boolean isShiftDown) { + GraphicsContext g = scratch.getAddScratchGraphics(this, null); g.setPaint(fill); - g.fill(Geometry.polygon(initialPoint, getShapeSides(), getRadius(), getRotationAngle(isShiftDown))); + g.fill(MathUtils.polygon(getInitialPoint(), getAttributes().getShapeSides(), getRadius(), getRotationAngle(isShiftDown))); } private double getRadius() { - return Geometry.distance(initialPoint, currentPoint); + return MathUtils.distance(getInitialPoint(), getCurrentPoint()); } private double getRotationAngle(boolean isShiftDown) { - double degrees = Geometry.angle(initialPoint.x, initialPoint.y, currentPoint.x, currentPoint.y); + double degrees = MathUtils.angle(getInitialPoint().x, getInitialPoint().y, getCurrentPoint().x, getCurrentPoint().y); if (isShiftDown) { - degrees = Geometry.round(degrees, getConstrainedAngle()); + degrees = MathUtils.nearestRound(degrees, getAttributes().getConstrainedAngle()); } return Math.toRadians(degrees); diff --git a/src/main/java/com/defano/jmonet/tools/SlantTool.java b/src/main/java/com/defano/jmonet/tools/SlantTool.java index 690b3a37..bc191638 100644 --- a/src/main/java/com/defano/jmonet/tools/SlantTool.java +++ b/src/main/java/com/defano/jmonet/tools/SlantTool.java @@ -1,25 +1,31 @@ package com.defano.jmonet.tools; -import com.defano.jmonet.algo.transform.image.SlantTransform; +import com.defano.jmonet.tools.base.TransformToolDelegate; +import com.defano.jmonet.transform.image.SlantTransform; import com.defano.jmonet.model.PaintToolType; -import com.defano.jmonet.tools.base.AbstractTransformTool; import com.defano.jmonet.model.FlexQuadrilateral; -import com.defano.jmonet.tools.util.Geometry; +import com.defano.jmonet.tools.base.TransformTool; +import com.defano.jmonet.tools.util.MathUtils; import java.awt.*; /** * Tool for drawing a rectangular selection boundary with drag-handles to shear/slant the image from the top or bottom. */ -public class SlantTool extends AbstractTransformTool { +public class SlantTool extends TransformTool implements TransformToolDelegate { - public SlantTool() { + /** + * Tool must be constructed via {@link com.defano.jmonet.tools.builder.PaintToolBuilder} to handle dependency + * injection. + */ + SlantTool() { super(PaintToolType.SLANT); + setTransformToolDelegate(this); } /** {@inheritDoc} */ @Override - protected void moveTopLeft(FlexQuadrilateral quadrilateral, Point newPosition, boolean isShiftDown) { + public void moveTopLeft(FlexQuadrilateral quadrilateral, Point newPosition, boolean isShiftDown) { quadrilateral.getTopRight().x += newPosition.x - quadrilateral.getTopLeft().x; quadrilateral.getTopLeft().x = newPosition.x; @@ -29,7 +35,7 @@ protected void moveTopLeft(FlexQuadrilateral quadrilateral, Point newPosition, b /** {@inheritDoc} */ @Override - protected void moveTopRight(FlexQuadrilateral quadrilateral, Point newPosition, boolean isShiftDown) { + public void moveTopRight(FlexQuadrilateral quadrilateral, Point newPosition, boolean isShiftDown) { quadrilateral.getTopLeft().x += newPosition.x - quadrilateral.getTopRight().x; quadrilateral.getTopRight().x = newPosition.x; @@ -39,7 +45,7 @@ protected void moveTopRight(FlexQuadrilateral quadrilateral, Point newPosition, /** {@inheritDoc} */ @Override - protected void moveBottomLeft(FlexQuadrilateral quadrilateral, Point newPosition, boolean isShiftDown) { + public void moveBottomLeft(FlexQuadrilateral quadrilateral, Point newPosition, boolean isShiftDown) { quadrilateral.getBottomRight().x += newPosition.x - quadrilateral.getBottomLeft().x; quadrilateral.getBottomLeft().x = newPosition.x; @@ -49,7 +55,7 @@ protected void moveBottomLeft(FlexQuadrilateral quadrilateral, Point newPosition /** {@inheritDoc} */ @Override - protected void moveBottomRight(FlexQuadrilateral quadrilateral, Point newPosition, boolean isShiftDown) { + public void moveBottomRight(FlexQuadrilateral quadrilateral, Point newPosition, boolean isShiftDown) { quadrilateral.getBottomLeft().x += newPosition.x - quadrilateral.getBottomRight().x; quadrilateral.getBottomRight().x = newPosition.x; @@ -59,6 +65,6 @@ protected void moveBottomRight(FlexQuadrilateral quadrilateral, Point newPositio private double getTheta(FlexQuadrilateral quadrilateral) { Point p = new Point(quadrilateral.getBottomLeft().x, quadrilateral.getTopLeft().y); - return Geometry.theta(quadrilateral.getBottomLeft(), p, quadrilateral.getTopLeft()); + return MathUtils.theta(quadrilateral.getBottomLeft(), p, quadrilateral.getTopLeft()); } } diff --git a/src/main/java/com/defano/jmonet/tools/TextTool.java b/src/main/java/com/defano/jmonet/tools/TextTool.java index ee2eccb2..c3e5d071 100644 --- a/src/main/java/com/defano/jmonet/tools/TextTool.java +++ b/src/main/java/com/defano/jmonet/tools/TextTool.java @@ -2,8 +2,11 @@ import com.defano.jmonet.canvas.PaintCanvas; +import com.defano.jmonet.canvas.observable.SurfaceInteractionObserver; +import com.defano.jmonet.context.AwtGraphicsContext; +import com.defano.jmonet.context.GraphicsContext; import com.defano.jmonet.model.PaintToolType; -import com.defano.jmonet.tools.builder.PaintTool; +import com.defano.jmonet.tools.base.BasicTool; import io.reactivex.disposables.Disposable; import io.reactivex.functions.Consumer; import io.reactivex.schedulers.Schedulers; @@ -18,105 +21,128 @@ /** * Tool for drawing rasterized text on the canvas. */ -public class TextTool extends PaintTool implements Consumer { +public class TextTool extends BasicTool implements SurfaceInteractionObserver { + + private final JTextArea textArea = new JTextArea(); + private final TextAreaMouseListener textAreaMouseListener = new TextAreaMouseListener(); - private JTextArea textArea; - private Point textModelLocation; private Disposable fontSubscription; private Disposable fontColorSubscription; public TextTool() { super(PaintToolType.TEXT); - setToolCursor(new Cursor(Cursor.TEXT_CURSOR)); } - /** {@inheritDoc} */ + @Override + public Cursor getDefaultCursor() { + return new Cursor(Cursor.TEXT_CURSOR); + } + + /** + * {@inheritDoc} + */ @Override public void deactivate() { - fontSubscription.dispose(); - fontColorSubscription.dispose(); + + // Stop listening to tool font changes + if (fontSubscription != null) { + fontSubscription.dispose(); + } + + // Stop listening to tool font color changes + if (fontColorSubscription != null) { + fontColorSubscription.dispose(); + } + + // Stop listening for mouse-clicks in edit area + textArea.removeMouseListener(textAreaMouseListener); if (isEditing()) { - commitTextImage(); - removeTextArea(); + finishEditing(); } super.deactivate(); } - /** {@inheritDoc} */ + /** + * {@inheritDoc} + */ @Override public void activate(PaintCanvas canvas) { super.activate(canvas); - textArea = new JTextArea(); - textArea.setVisible(true); - textArea.setOpaque(false); - textArea.setBackground(new Color(0, 0, 0, 0)); - textArea.setForeground(getFontColor()); - textArea.addMouseListener(new MouseAdapter() { - @Override - public void mousePressed(MouseEvent e) { - SwingUtilities.invokeLater(TextTool.this::completeEditing); - } - }); - - fontSubscription = getFontObservable() + // Monitor for clicks inside text edit area + textArea.addMouseListener(textAreaMouseListener); + + // Monitor for changes to tool font selection + fontSubscription = getAttributes().getFontObservable() .subscribeOn(Schedulers.computation()) - .subscribe(font -> textArea.setFont(font)); + .subscribe(textArea::setFont); - fontColorSubscription = getFontColorObservable() + // Monitor for changes to tool font color selection + fontColorSubscription = getAttributes().getFontColorObservable() .subscribeOn(Schedulers.computation()) - .subscribe(color -> textArea.setForeground(color)); + .subscribe(textArea::setForeground); } - /** {@inheritDoc} */ + /** + * {@inheritDoc} + */ @Override public void mousePressed(MouseEvent e, Point imageLocation) { - if (!isEditing()) { - getScratch().clear(); - - textModelLocation = new Point(imageLocation.x,imageLocation.y - getFontAscent()); - Point textViewLocation = getCanvas().convertModelPointToView(textModelLocation); - Dimension scaledSize = new Dimension( - (int)(getCanvas().getCanvasSize().width * getCanvas().getScale()) - textViewLocation.x, - (int)(getCanvas().getCanvasSize().height * getCanvas().getScale()) - textViewLocation.y); - - Rectangle textBounds = new Rectangle(textViewLocation, scaledSize); - - addTextArea(textBounds); - getCanvas().repaint(); - } else { - completeEditing(); + if (isEditing()) { + finishEditing(); } + + addTextArea(imageLocation); } - /** {@inheritDoc} */ + /** + * {@inheritDoc} + */ @Override - public void mouseMoved(MouseEvent e, Point imageLocation) { + public void mouseMoved(MouseEvent e, Point canvasLoc) { setToolCursor(getToolCursor()); } /** * Determines if the text tool is currently editing an active selection of text. + * * @return True when an active, mutable selection of text is being edited by the user, false otherwise. */ public boolean isEditing() { return textArea.getParent() != null; } + /** + * Removes the text area component from the canvas. + */ private void removeTextArea() { getCanvas().removeComponent(textArea); } - private void addTextArea(Rectangle bounds) { - textArea.setVisible(true); + /** + * Configures and adds the JTextArea component to the canvas at the specified bounds. The top-left point of these + * bounds are typically equal to the location where the mouse was pressed, and the bottom-right point is equal to + * the bottom-right point of the paint canvas. + * + * @param modelLocation Canvas image location where the text area should be added. + */ + private void addTextArea(Point modelLocation) { + Rectangle bounds = calculateTextAreaBounds(modelLocation); + + // Configure text area properties + textArea.setBounds(bounds.x, bounds.y, bounds.width, bounds.height); textArea.setBorder(new EmptyBorder(0, 0, 0, 0)); textArea.setText(""); - textArea.setBounds(bounds.x, bounds.y, bounds.width, bounds.height); textArea.setFont(getScaledFont()); + textArea.setOpaque(false); + textArea.setBackground(new Color(0, 0, 0, 0)); // completely transparent + textArea.setForeground(getAttributes().getFontColor()); + textArea.setVisible(true); + // Add it to the canvas and give it focus getCanvas().addComponent(textArea); textArea.requestFocus(); } @@ -128,11 +154,11 @@ private BufferedImage rasterizeText() { textArea.setSelectionEnd(0); textArea.getCaret().setVisible(false); - textArea.setFont(getFont()); + textArea.setFont(getAttributes().getFont()); BufferedImage image = new BufferedImage(textArea.getWidth(), textArea.getHeight(), BufferedImage.TYPE_INT_ARGB); Graphics2D g = (Graphics2D) image.getGraphics(); - applyRenderingHints(g); + new AwtGraphicsContext(g).setAntialiasingMode(getAttributes().getAntiAliasing()); textArea.printAll(g); g.dispose(); @@ -141,42 +167,59 @@ private BufferedImage rasterizeText() { return image; } - private void commitTextImage() { + private void commit() { // Don't commit if user hasn't entered any text - if (textArea.getText().trim().length() > 0) { + if (!textArea.getText().trim().isEmpty()) { BufferedImage text = rasterizeText(); + Point textModelLocation = getCanvas().convertViewPointToModel(textArea.getLocation()); - Graphics g = getScratch().getAddScratchGraphics(this, new Rectangle(textModelLocation.x, textModelLocation.y, textArea.getWidth(), textArea.getHeight())); + GraphicsContext g = getScratch().getAddScratchGraphics(this, new Rectangle(textModelLocation.x, textModelLocation.y, textArea.getWidth(), textArea.getHeight())); g.drawImage(text, textModelLocation.x, textModelLocation.y, null); getCanvas().commit(); } } private Font getScaledFont() { - return new Font(getFont().getFamily(), getFont().getStyle(), (int) (getFont().getSize() * getCanvas().getScaleObservable().blockingFirst())); + return new Font( + getAttributes().getFont().getFamily(), + getAttributes().getFont().getStyle(), + (int) (getAttributes().getFont().getSize() * getCanvas().getScaleObservable().blockingFirst()) + ); } private int getFontAscent() { - Graphics g = getScratch().getAddScratchGraphics(this, null); - FontMetrics metrics = g.getFontMetrics(getFont()); + GraphicsContext g = getScratch().getAddScratchGraphics(this, null); + FontMetrics metrics = g.getFontMetrics(getAttributes().getFont()); return metrics.getAscent(); } - private void completeEditing() { - commitTextImage(); + private void finishEditing() { + commit(); removeTextArea(); } - @Override - public void accept(Object o) { - if (o instanceof Font) { - textArea.setFont((Font) o); - } + private Rectangle calculateTextAreaBounds(Point imageLocation) { - if (o instanceof Color) { - textArea.setForeground((Color) o); + // Account for font ascent (characters will be drawn up from this location) + Point textModelLocation = new Point(imageLocation.x, imageLocation.y - getFontAscent()); + Point textViewLocation = getCanvas().convertModelPointToView(textModelLocation); + + Dimension scaledSize = new Dimension( + (int) (getCanvas().getCanvasSize().width * getCanvas().getScale()) - textViewLocation.x, + (int) (getCanvas().getCanvasSize().height * getCanvas().getScale()) - textViewLocation.y + ); + + return new Rectangle(textViewLocation, scaledSize); + } + + private class TextAreaMouseListener extends MouseAdapter { + @Override + public void mousePressed(MouseEvent e) { + Point location = getCanvas().convertViewPointToModel(SwingUtilities.convertPoint(textArea, e.getPoint(), getCanvas().getComponent())); + SwingUtilities.invokeLater(() -> TextTool.this.mousePressed(e, location)); } } + } diff --git a/src/main/java/com/defano/jmonet/algo/fill/BoundaryFunction.java b/src/main/java/com/defano/jmonet/tools/attributes/BoundaryFunction.java similarity index 82% rename from src/main/java/com/defano/jmonet/algo/fill/BoundaryFunction.java rename to src/main/java/com/defano/jmonet/tools/attributes/BoundaryFunction.java index 4a0f40a9..afac43f6 100644 --- a/src/main/java/com/defano/jmonet/algo/fill/BoundaryFunction.java +++ b/src/main/java/com/defano/jmonet/tools/attributes/BoundaryFunction.java @@ -1,5 +1,6 @@ -package com.defano.jmonet.algo.fill; +package com.defano.jmonet.tools.attributes; +import java.awt.*; import java.awt.image.BufferedImage; /** @@ -30,5 +31,9 @@ public interface BoundaryFunction { * boundary) */ @SuppressWarnings("BooleanMethodIsAlwaysInverted") - boolean isBoundary(BufferedImage canvas, BufferedImage scratch, int x, int y); + default boolean isBoundary(BufferedImage canvas, BufferedImage scratch, int x, int y) { + Color canvasPixel = new Color(canvas.getRGB(x, y), true); + Color scratchPixel = new Color(scratch.getRGB(x, y), true); + return canvasPixel.getAlpha() != 0 || scratchPixel.getAlpha() != 0; + } } diff --git a/src/main/java/com/defano/jmonet/tools/attributes/FillFunction.java b/src/main/java/com/defano/jmonet/tools/attributes/FillFunction.java new file mode 100644 index 00000000..aea026e7 --- /dev/null +++ b/src/main/java/com/defano/jmonet/tools/attributes/FillFunction.java @@ -0,0 +1,29 @@ +package com.defano.jmonet.tools.attributes; + +import java.awt.*; +import java.awt.image.BufferedImage; + +/** + * A function for filling a single pixel on the canvas. + */ +public interface FillFunction { + + /** + * Fills a single pixel in an image with a given paint or texture. + * + * @param image The image whose pixel should be filled. + * @param x The x coordinate of the point / pixel to fill + * @param y The y coordinate of the point / pixel to fill + * @param fillPaint The paint to apply to the given pixel. + */ + default void fill(BufferedImage image, int x, int y, Paint fillPaint) { + if (fillPaint instanceof Color) { + image.setRGB (x, y, ((Color) fillPaint).getRGB()); + } else if (fillPaint instanceof TexturePaint) { + BufferedImage texture = ((TexturePaint) fillPaint).getImage(); + image.setRGB (x, y, texture.getRGB(x % texture.getWidth(), y % texture.getHeight())); + } else { + throw new IllegalArgumentException("Don't know how to fill using this kind of paint: " + fillPaint); + } + } +} diff --git a/src/main/java/com/defano/jmonet/tools/attributes/MarkPredicate.java b/src/main/java/com/defano/jmonet/tools/attributes/MarkPredicate.java new file mode 100644 index 00000000..b987d6de --- /dev/null +++ b/src/main/java/com/defano/jmonet/tools/attributes/MarkPredicate.java @@ -0,0 +1,27 @@ +package com.defano.jmonet.tools.attributes; + +import java.awt.*; + +/** + * An interface for determining if a given pixel constitutes a "mark" on a canvas. Used, for example, when determining + * if the pencil tool should draw or erase (when starting on a marked pixel the pencil erases, when starting on an + * unmarked or blank pixel, it draws). + */ +public interface MarkPredicate { + + /** + * Determines if a pixel of a given color is considered to be a mark on the canvas in contrast to an "erase color" + * that may optionally define the color that erased pixels are changed to. + * + * @param pixel A non-null value, indicating the present color value of the pixel being queried. + * @param eraseColor An nullable value, indicating the present erase color in the context of this query. + * @return True if this pixel should be treated as a mark; false otherwise. + */ + default boolean isMarked(Color pixel, Color eraseColor) { + if (eraseColor == null) { + return pixel.getAlpha() >= 128; + } else { + return eraseColor.getRed() != pixel.getRed() || eraseColor.getBlue() != pixel.getBlue() || eraseColor.getGreen() != pixel.getGreen(); + } + } +} diff --git a/src/main/java/com/defano/jmonet/tools/attributes/ObservableToolAttributes.java b/src/main/java/com/defano/jmonet/tools/attributes/ObservableToolAttributes.java new file mode 100644 index 00000000..980cfd1c --- /dev/null +++ b/src/main/java/com/defano/jmonet/tools/attributes/ObservableToolAttributes.java @@ -0,0 +1,64 @@ +package com.defano.jmonet.tools.attributes; + +import com.defano.jmonet.model.Interpolation; +import io.reactivex.Observable; + +import java.awt.*; +import java.util.Optional; + +public interface ObservableToolAttributes { + + void setEraseColorObservable(Observable> observable); + Observable> getEraseColorObservable(); + + void setFillPaintObservable(Observable> observable); + Observable> getFillPaintObservable(); + + void setStrokeObservable(Observable observable); + Observable getStrokeObservable(); + + void setStrokePaintObservable(Observable observable); + Observable getStrokePaintObservable(); + + void setShapeSidesObservable(Observable observable); + Observable getShapeSidesObservable(); + + void setFontObservable(Observable observable); + Observable getFontObservable(); + + void setFontColorObservable(Observable observable); + Observable getFontColorObservable(); + + void setIntensityObservable(Observable observable); + Observable getIntensityObservable(); + + void setDrawCenteredObservable(Observable observable); + Observable getDrawCenteredObservable(); + + void setDrawMultipleObservable(Observable observable); + Observable getDrawMultipleObservable(); + + void setCornerRadiusObservable(Observable observable); + Observable getCornerRadiusObservable(); + + void setConstrainedAngleObservable(Observable observable); + Observable getConstrainedAngleObservable(); + + void setAntiAliasingObservable(Observable observable); + Observable getAntiAliasingObservable(); + + void setMinimumScaleObservable(Observable observable); + Observable getMinimumScaleObservable(); + + void setMaximumScaleObservable(Observable observable); + Observable getMaximumScaleObservable(); + + void setMagnificationStepObservable(Observable observable); + Observable getMagnificationStepObservable(); + + void setRecenterOnMagnifyObservable(Observable observable); + Observable getRecenterOnMagnifyObservable(); + + void setPathInterpolationObservable(Observable observable); + Observable getPathInterpolationObservable(); +} diff --git a/src/main/java/com/defano/jmonet/tools/attributes/RxToolAttributes.java b/src/main/java/com/defano/jmonet/tools/attributes/RxToolAttributes.java new file mode 100644 index 00000000..699d771f --- /dev/null +++ b/src/main/java/com/defano/jmonet/tools/attributes/RxToolAttributes.java @@ -0,0 +1,300 @@ +package com.defano.jmonet.tools.attributes; + +import com.defano.jmonet.model.Interpolation; +import io.reactivex.Observable; +import io.reactivex.subjects.BehaviorSubject; + +import java.awt.*; +import java.util.Optional; + +public class RxToolAttributes implements ObservableToolAttributes, ToolAttributes { + + // Non-observable attributes + private MarkPredicate markPredicate = new MarkPredicate() {}; + private FillFunction fillFunction = new FillFunction() {}; + private BoundaryFunction boundaryFunction = new BoundaryFunction() {}; + + // Observable attributes + private Observable antiAliasingObservable = BehaviorSubject.createDefault(Interpolation.NONE); + private Observable constrainedAngleObservable = BehaviorSubject.createDefault(15); + private Observable strokeObservable = BehaviorSubject.createDefault(new BasicStroke(2)); + private Observable strokePaintObservable = BehaviorSubject.createDefault(Color.BLACK); + private Observable> fillPaintObservable = BehaviorSubject.createDefault(Optional.empty()); + private Observable> eraseColorObservable = BehaviorSubject.createDefault(Optional.empty()); + private Observable shapeSidesObservable = BehaviorSubject.createDefault(5); + private Observable fontObservable = BehaviorSubject.createDefault(new Font("Courier", Font.PLAIN, 14)); + private Observable fontColorObservable = BehaviorSubject.createDefault(Color.BLACK); + private Observable intensityObservable = BehaviorSubject.createDefault(0.1); + private Observable drawMultipleObservable = BehaviorSubject.createDefault(false); + private Observable drawCenteredObservable = BehaviorSubject.createDefault(false); + private Observable cornerRadiusObservable = BehaviorSubject.createDefault(10); + private Observable minimumScaleObservable = BehaviorSubject.createDefault(1.0); + private Observable maximumScaleObservable = BehaviorSubject.createDefault(32.0); + private Observable magnificationStepObservable = BehaviorSubject.createDefault(2.0); + private Observable recenterOnMagnifyObservable = BehaviorSubject.createDefault(true); + private Observable pathInterpolationObservable = BehaviorSubject.createDefault(true); + + @Override + public void setMinimumScaleObservable(Observable observable) { + this.minimumScaleObservable = observable; + } + + @Override + public Observable getMinimumScaleObservable() { + return minimumScaleObservable; + } + + @Override + public void setMaximumScaleObservable(Observable observable) { + this.maximumScaleObservable = observable; + } + + @Override + public Observable getMaximumScaleObservable() { + return maximumScaleObservable; + } + + @Override + public void setMagnificationStepObservable(Observable observable) { + this.magnificationStepObservable = observable; + } + + @Override + public Observable getMagnificationStepObservable() { + return magnificationStepObservable; + } + + @Override + public void setRecenterOnMagnifyObservable(Observable observable) { + this.recenterOnMagnifyObservable = observable; + } + + @Override + public Observable getRecenterOnMagnifyObservable() { + return recenterOnMagnifyObservable; + } + + /** {@inheritDoc} */ + @Override + public void setFontColorObservable(Observable fontColorObservable) { + this.fontColorObservable = fontColorObservable; + } + + /** {@inheritDoc} */ + @Override + public void setStrokePaintObservable(Observable strokePaintObservable) { + if (strokePaintObservable != null) { + this.strokePaintObservable = strokePaintObservable; + } + } + + /** {@inheritDoc} */ + @Override + public void setStrokeObservable(Observable strokeObservable) { + if (strokeObservable != null) { + this.strokeObservable = strokeObservable; + } + } + + /** {@inheritDoc} */ + @Override + public void setShapeSidesObservable(Observable shapeSidesObservable) { + if (shapeSidesObservable != null) { + this.shapeSidesObservable = shapeSidesObservable; + } + } + + /** {@inheritDoc} */ + @Override + public void setFontObservable(Observable fontObservable) { + if (fontObservable != null) { + this.fontObservable = fontObservable; + } + } + + /** {@inheritDoc} */ + @Override + public void setFillPaintObservable(Observable> fillPaintObservable) { + this.fillPaintObservable = fillPaintObservable; + } + + /** {@inheritDoc} */ + @Override + public void setEraseColorObservable(Observable> eraseColorObservable) { + this.eraseColorObservable = eraseColorObservable; + } + + /** {@inheritDoc} */ + @Override + public void setIntensityObservable(Observable intensityObservable) { + this.intensityObservable = intensityObservable; + } + + /** {@inheritDoc} */ + @Override + public void setDrawMultipleObservable(Observable drawMultipleObservable) { + this.drawMultipleObservable = drawMultipleObservable; + } + + /** {@inheritDoc} */ + @Override + public void setDrawCenteredObservable(Observable drawCenteredObservable) { + this.drawCenteredObservable = drawCenteredObservable; + } + + /** {@inheritDoc} */ + @Override + public void setCornerRadiusObservable(Observable cornerRadiusObservable) { + this.cornerRadiusObservable = cornerRadiusObservable; + } + + /** {@inheritDoc} */ + @Override + public void setConstrainedAngleObservable(Observable constrainedAngleObservable) { + this.constrainedAngleObservable = constrainedAngleObservable; + } + + /** {@inheritDoc} */ + @Override + public void setAntiAliasingObservable(Observable antiAliasingObservable) { + this.antiAliasingObservable = antiAliasingObservable; + } + + /** {@inheritDoc} */ + @Override + public void setPathInterpolationObservable(Observable observable) { + this.pathInterpolationObservable = observable; + } + + /** {@inheritDoc} */ + @Override + public Observable> getFillPaintObservable() { + return fillPaintObservable; + } + + /** {@inheritDoc} */ + @Override + public Observable> getEraseColorObservable() { + return eraseColorObservable; + } + + /** {@inheritDoc} */ + @Override + public Observable getStrokeObservable() { + return strokeObservable; + } + + /** {@inheritDoc} */ + @Override + public Observable getStrokePaintObservable() { + return strokePaintObservable; + } + + /** {@inheritDoc} */ + @Override + public Observable getShapeSidesObservable() { + return shapeSidesObservable; + } + + /** {@inheritDoc} */ + @Override + public Observable getFontObservable() { + return fontObservable; + } + + /** {@inheritDoc} */ + @Override + public Observable getFontColorObservable() { + return fontColorObservable; + } + + /** {@inheritDoc} */ + @Override + public Observable getIntensityObservable() { + return intensityObservable; + } + + /** {@inheritDoc} */ + @Override + public Observable getDrawCenteredObservable() { + return drawCenteredObservable; + } + + /** {@inheritDoc} */ + @Override + public Observable getDrawMultipleObservable() { + return drawMultipleObservable; + } + + /** {@inheritDoc} */ + @Override + public Observable getCornerRadiusObservable() { + return cornerRadiusObservable; + } + + /** {@inheritDoc} */ + @Override + public Observable getConstrainedAngleObservable() { + return constrainedAngleObservable; + } + + /** {@inheritDoc} */ + @Override + public Observable getAntiAliasingObservable() { + return antiAliasingObservable; + } + + /** {@inheritDoc} */ + @Override + public Observable getPathInterpolationObservable() { + return pathInterpolationObservable; + } + + /** {@inheritDoc} */ + @Override + public MarkPredicate getMarkPredicate() { + return markPredicate; + } + + /** {@inheritDoc} */ + @Override + public void setMarkPredicate(MarkPredicate markPredicate) { + if (markPredicate == null) { + throw new IllegalArgumentException("Mark predicate cannot be null."); + } + + this.markPredicate = markPredicate; + } + + /** {@inheritDoc} */ + @Override + public BoundaryFunction getBoundaryFunction() { + return boundaryFunction; + } + + /** {@inheritDoc} */ + @Override + public void setBoundaryFunction(BoundaryFunction boundaryFunction) { + if (fillFunction == null) { + throw new IllegalArgumentException("Boundary function cannot be null."); + } + + this.boundaryFunction = boundaryFunction; + } + + /** {@inheritDoc} */ + @Override + public FillFunction getFillFunction() { + return fillFunction; + } + + /** {@inheritDoc} */ + @Override + public void setFillFunction(FillFunction fillFunction) { + if (fillFunction == null) { + throw new IllegalArgumentException("Fill function cannot be null."); + } + + this.fillFunction = fillFunction; + } +} diff --git a/src/main/java/com/defano/jmonet/tools/attributes/ToolAttributes.java b/src/main/java/com/defano/jmonet/tools/attributes/ToolAttributes.java new file mode 100644 index 00000000..cbea566a --- /dev/null +++ b/src/main/java/com/defano/jmonet/tools/attributes/ToolAttributes.java @@ -0,0 +1,224 @@ +package com.defano.jmonet.tools.attributes; + +import com.defano.jmonet.canvas.surface.SurfaceScrollController; +import com.defano.jmonet.model.Interpolation; + +import java.awt.*; +import java.util.Optional; + +/** + * A set of attributes provided to all JMonet tools (although not every tool will use every attribute). + */ +public interface ToolAttributes extends ObservableToolAttributes { + + int MIN_SHAPE_SIDES = 3; + int MAX_SHAPE_SIDES = 64; + + /** + * Gets the stroke (the pen or brush outline) drawn by the tool. + * @return The active stroke. + */ + default Stroke getStroke() { + return getStrokeObservable().blockingFirst(); + } + + /** + * Gets the paint used to stroke or outline the item drawn by the tool. + * @return The stroke paint. + */ + default Paint getStrokePaint() { + return getStrokePaintObservable().blockingFirst(); + } + + /** + * Gets the paint used to fill the item drawn by the tool. + * @return The fill paint. + */ + default Optional getFillPaint() { + return getFillPaintObservable().blockingFirst(); + } + + /** + * Gets the text font drawn by the tool. + * @return The text font. + */ + default Font getFont() { + return getFontObservable().blockingFirst(); + } + + /** + * Gets the color of the text drawn by the tool. + * @return The text font color. + */ + default Color getFontColor() { + return getFontColorObservable().blockingFirst(); + } + + /** + * Gets the number of sides of the shape drawn by this tool. + * @return The number of shape sides. + */ + default int getShapeSides() { + int sides = getShapeSidesObservable().blockingFirst(); + + return sides < MIN_SHAPE_SIDES ? MIN_SHAPE_SIDES : + sides > MAX_SHAPE_SIDES ? MAX_SHAPE_SIDES : + sides; + } + + /** + * Gets the color of the pixel that erased pixels are changed to, or null if erased pixels should be fully + * transparent (default behavior). + * + * @return The erase color or null + */ + default Color getEraseColor() { + return getEraseColorObservable().blockingFirst().orElse(null); + } + + /** + * Gets the angle (in degrees) that the tool should be constrained to when the shift-key is held down. + * @return The constrained angle, in degrees. + */ + default int getConstrainedAngle() { + return getConstrainedAngleObservable().blockingFirst(); + } + + /** + * Gets the intensity (opacity) of the paint drawn by this tool. A value in the range [0.0, 1.0] where higher values + * represent greater paint intensity. + * + * @return The tool intensity. + */ + default double getIntensity() { + return getIntensityObservable().blockingFirst(); + } + + /** + * Gets the state of draw multiple flag. + * @return The draw multiple flag. + */ + default boolean isDrawMultiple() { + return getDrawMultipleObservable().blockingFirst(); + } + + /** + * Gets the state of the draw centered flag. + * @return The draw centered flag. + */ + default boolean isDrawCentered() { + return getDrawCenteredObservable().blockingFirst(); + } + + /** + * Gets the radius, in pixels, of the rounded corners drawn by this tool. + * @return The corner radius. + */ + default int getCornerRadius() { + return getCornerRadiusObservable().blockingFirst(); + } + + /** + * Gets the anti-aliasing interpolation mode used by this tool. + * @return The anti-aliasing interpolation mode. + */ + default Interpolation getAntiAliasing() { + return getAntiAliasingObservable().blockingFirst(); + } + + /** + * Gets the maximum allowable scale factor that this tool can adjust the active canvas to. For example, when set + * to 32.0 the magnifier tool will not be able to magnify the canvas greater than 32x. + * + * @return The maximum allowable scale factor this tool will adjust to. + */ + default double getMaximumScale() { + return getMaximumScaleObservable().blockingFirst(); + } + + /** + * Gets the minimum allowable scale factor that this tool can adjust the active canvas to. For example, when set + * to 1.0 the magnifier tool will not be able to zoom out (shrink the image) past its normal size. + * + * @return The minimum allowable scale factor this tool will adjust to. + */ + default double getMinimumScale() { + return getMinimumScaleObservable().blockingFirst(); + } + + /** + * Gets whether the canvas scroll position should be updated when zooming in or out to recenter the pixel that was + * clicked with the magnifier tool. Has no effect when the active canvas is not embedded in a scroll pane and + * managed via a {@link SurfaceScrollController}. + * + * @return True if the clicked pixel will be centered in the scroll pane when zooming in or out; false otherwise. + */ + default boolean isRecenterOnMagnify() { + return getRecenterOnMagnifyObservable().blockingFirst(); + } + + /** + * Gets the value by which the canvas scale factor is multiplied or divided when zooming in or zooming out. For + * example, when this value is 2.0, zooming in causes the scale factor to be multiplied by 2 thus scaling the + * canvas from its original size to 2x, then 4x, then 8x and so forth. + * + * @return The zoom in/out scale multiple + */ + default double getMagnificationStep() { + return getMagnificationStepObservable().blockingFirst(); + } + + /** + * Indicates whether path interpolation is enabled; when true, certain paths (like those used by the airbrush) are + * interpolated, resulting in a smoother (albeit more computationally expensive) rendering. + * + * @return True when path interpolation is enabled. + */ + default boolean isPathInterpolated() { + return getPathInterpolationObservable().blockingFirst(); + } + + /** + * Gets the predicate function used to determine if a canvas pixel is considered "marked," not blank (for example, + * used in determining if the pencil tool should mark or erase). + * + * @return The current mark predicate function + */ + MarkPredicate getMarkPredicate(); + + /** + * Sets the predicate function used to determine if a canvas pixel is considered "marked," not blank (for example, + * used in determining if the pencil tool should mark or erase). + * + * @param markPredicate The mark predicate function + */ + void setMarkPredicate(MarkPredicate markPredicate); + + /** + * Gets the function used to detect when paint flooding a region has reached a boundary. See + * BoundaryFunction for details. + * + * @return The current boundary function in use. + */ + BoundaryFunction getBoundaryFunction(); + + /** + * Sets the function used to detect when paint flooding a region has reached a boundary. See + * BoundaryFunction for details. + * + * @param boundaryFunction The boundary function to use. + */ + void setBoundaryFunction(BoundaryFunction boundaryFunction); + + /** + * Gets the function used to color the canvas with paint flooding a region. See {@link FillFunction} for details. + * @return The fill function being used. + */ + FillFunction getFillFunction(); + + /** + * Sets the function used to color the canvas with paint flooding a region. See {@link FillFunction} for details. + * @param fillFunction The fill function to use + */ + void setFillFunction(FillFunction fillFunction); +} diff --git a/src/main/java/com/defano/jmonet/tools/base/AbstractBoundsTool.java b/src/main/java/com/defano/jmonet/tools/base/AbstractBoundsTool.java deleted file mode 100644 index 2e5661eb..00000000 --- a/src/main/java/com/defano/jmonet/tools/base/AbstractBoundsTool.java +++ /dev/null @@ -1,99 +0,0 @@ -package com.defano.jmonet.tools.base; - -import com.defano.jmonet.canvas.Scratch; -import com.defano.jmonet.model.PaintToolType; -import com.defano.jmonet.tools.builder.PaintTool; -import com.defano.jmonet.tools.util.Geometry; - -import java.awt.*; -import java.awt.event.MouseEvent; - -/** - * Mouse and keyboard event handler for tools that define a bounding box by clicking and dragging - * the mouse from the top-left point of the bounds to the bottom-right point. - * - * When the shift key is held down the bounding box is constrained to a square whose height and width is equal to the - * larger of the two dimensions defined by the mouse location. - */ -public abstract class AbstractBoundsTool extends PaintTool { - - protected Point initialPoint; - protected Point currentPoint; - - public AbstractBoundsTool(PaintToolType type) { - super(type); - setToolCursor(new Cursor(Cursor.CROSSHAIR_CURSOR)); - } - - /** - * Draws the stroke (outline) of a shape described by a rectangular boundary. - * - * @param scratch The scratch buffer on which to draw - * @param stroke The stroke with which to draw - * @param paint The paint with which to draw - * @param bounds The bounds of the shape to draw - * @param isShiftDown True to indicate that the user is holding the shift key; implementers may use this flag to - * constrain the bounds or otherwise modify the tool behavior. - */ - protected abstract void strokeBounds(Scratch scratch, Stroke stroke, Paint paint, Rectangle bounds, boolean isShiftDown); - - /** - * Fills a shape described by a rectangular boundary. - * - * @param scratch The scratch buffer on which to draw - * @param fill The paint with which to fill the shape - * @param bounds The bounds of the shape to draw - * @param isShiftDown True to indicate that the user is holding the shift key; implementers may use this flag to - * constrain the bounds or otherwise modify the tool behavior. - */ - protected abstract void fillBounds(Scratch scratch, Paint fill, Rectangle bounds, boolean isShiftDown); - - /** {@inheritDoc} */ - @Override - public void mousePressed(MouseEvent e, Point imageLocation) { - initialPoint = imageLocation; - } - - /** {@inheritDoc} */ - @Override - public void mouseDragged(MouseEvent e, Point imageLocation) { - currentPoint = imageLocation; - - if (!getDrawMultipleObservable().blockingFirst()) { - getScratch().clear(); - } - - Point originPoint = new Point(initialPoint); - - if (getDrawCenteredObservable().blockingFirst()) { - int height = currentPoint.y - initialPoint.y; - int width = currentPoint.x - initialPoint.x; - - originPoint.x = initialPoint.x - width / 2; - originPoint.y = initialPoint.y - height / 2; - } - - Rectangle bounds = e.isShiftDown() ? - Geometry.square(originPoint, currentPoint) : - Geometry.rectangle(originPoint, currentPoint); - - if (getFillPaint().isPresent()) { - fillBounds(getScratch(), getFillPaint().get(), new Rectangle(bounds.x, bounds.y, bounds.width, bounds.height), e.isShiftDown()); - } - - strokeBounds(getScratch(), getStroke(), getStrokePaint(), new Rectangle(bounds.x, bounds.y, bounds.width, bounds.height), e.isShiftDown()); - getCanvas().repaint(); - } - - /** {@inheritDoc} */ - @Override - public void mouseReleased(MouseEvent e, Point imageLocation) { - getCanvas().commit(); - } - - /** {@inheritDoc} */ - @Override - public void mouseMoved(MouseEvent e, Point imageLocation) { - setToolCursor(getToolCursor()); - } -} diff --git a/src/main/java/com/defano/jmonet/tools/base/AbstractLineTool.java b/src/main/java/com/defano/jmonet/tools/base/AbstractLineTool.java deleted file mode 100644 index 65cbefed..00000000 --- a/src/main/java/com/defano/jmonet/tools/base/AbstractLineTool.java +++ /dev/null @@ -1,69 +0,0 @@ -package com.defano.jmonet.tools.base; - -import com.defano.jmonet.canvas.Scratch; -import com.defano.jmonet.model.PaintToolType; -import com.defano.jmonet.tools.builder.PaintTool; -import com.defano.jmonet.tools.util.Geometry; - -import java.awt.*; -import java.awt.event.MouseEvent; - -/** - * Mouse and keyboard handler for tools defining a line drawn between two points. When the shift key is held down, the - * defined line is constrained to the nearest 15 degree angle. - */ -public abstract class AbstractLineTool extends PaintTool { - - private Point initialPoint; - - public AbstractLineTool(PaintToolType type) { - super(type); - setToolCursor(new Cursor(Cursor.CROSSHAIR_CURSOR)); - } - - /** - * Draws a line from (x1, y1) to (x2, y2) on the given graphics context. - * - * @param scratch The scratch buffer on which to draw - * @param stroke The stroke with which to draw - * @param paint The paint with which to draw - * @param x1 First x coordinate of the line - * @param y1 First y coordinate of the line - * @param x2 Second x coordinate of the line - * @param y2 Second y coordinate of the line - */ - protected abstract void drawLine(Scratch scratch, Stroke stroke, Paint paint, int x1, int y1, int x2, int y2); - - /** {@inheritDoc} */ - @Override - public void mouseMoved(MouseEvent e, Point imageLocation) { - setToolCursor(getToolCursor()); - } - - /** {@inheritDoc} */ - @Override - public void mousePressed(MouseEvent e, Point imageLocation) { - initialPoint = imageLocation; - } - - /** {@inheritDoc} */ - @Override - public void mouseDragged(MouseEvent e, Point imageLocation) { - getScratch().clear(); - - Point currentLoc = imageLocation; - - if (e.isShiftDown()) { - currentLoc = Geometry.line(initialPoint, currentLoc, getConstrainedAngle()); - } - - drawLine(getScratch(), getStroke(), getStrokePaint(), initialPoint.x, initialPoint.y, currentLoc.x, currentLoc.y); - getCanvas().repaint(); - } - - /** {@inheritDoc} */ - @Override - public void mouseReleased(MouseEvent e, Point imageLocation) { - getCanvas().commit(); - } -} diff --git a/src/main/java/com/defano/jmonet/tools/base/AbstractPathTool.java b/src/main/java/com/defano/jmonet/tools/base/AbstractPathTool.java deleted file mode 100644 index 99604c81..00000000 --- a/src/main/java/com/defano/jmonet/tools/base/AbstractPathTool.java +++ /dev/null @@ -1,84 +0,0 @@ -package com.defano.jmonet.tools.base; - -import com.defano.jmonet.canvas.Scratch; -import com.defano.jmonet.model.PaintToolType; -import com.defano.jmonet.tools.builder.PaintTool; - -import java.awt.*; -import java.awt.event.MouseEvent; - -/** - * Mouse and keyboard handler for tools that define a free-form path on the canvas by clicking and dragging. - */ -public abstract class AbstractPathTool extends PaintTool { - - protected Point lastPoint; - - /** - * Begins drawing a path on the given graphics context. - * - * @param scratch The scratch buffer on which to draw. - * @param stroke The stroke with which to draw - * @param fillPaint The paint with which to draw - * @param initialPoint The first point defined on the path - */ - protected abstract void startPath(Scratch scratch, Stroke stroke, Paint fillPaint, Point initialPoint); - - /** - * Adds a point to the path begun via a call to {@link #startPath(Scratch, Stroke, Paint, Point)}. - * - * @param scratch The scratch buffer on which to draw. - * @param stroke The stroke with which to draw - * @param fillPaint The paint with which to draw - * @param lastPoint The last point added to the current path - * @param thisPoint The new point to add to the current path - */ - protected abstract void addPoint(Scratch scratch, Stroke stroke, Paint fillPaint, Point lastPoint, Point thisPoint); - - /** - * Completes the path begun via a call to {@link #startPath(Scratch, Stroke, Paint, Point)}. - * - * @param scratch The scratch buffer on which to draw. - * @param stroke The stroke with which to draw - * @param fillPaint The paint with which to draw - */ - protected void completePath(Scratch scratch, Stroke stroke, Paint fillPaint) {} - - public AbstractPathTool(PaintToolType type) { - super(type); - setToolCursor(new Cursor(Cursor.CROSSHAIR_CURSOR)); - } - - /** {@inheritDoc} */ - @Override - public void mouseMoved(MouseEvent e, Point imageLocation) { - setToolCursor(getToolCursor()); - } - - /** {@inheritDoc} */ - @Override - public void mousePressed(MouseEvent e, Point imageLocation) { - getScratch().clear(); - - startPath(getScratch(), getStroke(), getStrokePaint(), imageLocation); - lastPoint = imageLocation; - getCanvas().repaint(); - } - - /** {@inheritDoc} */ - @Override - public void mouseDragged(MouseEvent e, Point imageLocation) { - addPoint(getScratch(), getStroke(), getStrokePaint(), lastPoint, imageLocation); - - lastPoint = imageLocation; - getCanvas().repaint(); - } - - /** {@inheritDoc} */ - @Override - public void mouseReleased(MouseEvent e, Point imageLocation) { - completePath(getScratch(), getStroke(), getStrokePaint()); - - getCanvas().commit(getScratch().getLayerSet()); - } -} diff --git a/src/main/java/com/defano/jmonet/tools/base/AbstractPolylineTool.java b/src/main/java/com/defano/jmonet/tools/base/AbstractPolylineTool.java deleted file mode 100644 index a2f92bff..00000000 --- a/src/main/java/com/defano/jmonet/tools/base/AbstractPolylineTool.java +++ /dev/null @@ -1,151 +0,0 @@ -package com.defano.jmonet.tools.base; - -import com.defano.jmonet.canvas.Scratch; -import com.defano.jmonet.model.PaintToolType; -import com.defano.jmonet.tools.builder.PaintTool; -import com.defano.jmonet.tools.util.Geometry; - -import java.awt.*; -import java.awt.event.KeyEvent; -import java.awt.event.MouseEvent; -import java.util.ArrayList; -import java.util.List; - -/** - * Mouse and keyboard handler for tools that define multiple segments/points along a path. Click to define a point, - * double-click to close/complete the shape. When the shift key is held down, line segments will be constrained to - * the nearest 15-degree angle. - */ -public abstract class AbstractPolylineTool extends PaintTool { - - private final List points = new ArrayList<>(); - private Point currentPoint = null; - - /** - * Draws one or more sides (edges) of a polygon which is not filled and may not be closed. - * - * @param scratch The scratch buffer on which to draw. - * @param stroke The current stroke context. - * @param strokePaint The current paint context. - * @param xPoints An array of x points, see {@link Graphics2D#drawPolyline(int[], int[], int)} - * @param yPoints An array of y points, see {@link Graphics2D#drawPolyline(int[], int[], int)} - */ - protected abstract void strokePolyline(Scratch scratch, Stroke stroke, Paint strokePaint, int[] xPoints, int[] yPoints); - - /** - * Draws one or more sides (edges) of a polygon, closing the shape as needed. - * - * @param scratch The scratch buffer on which to draw. - * @param stroke The current stroke context. - * @param strokePaint The current paint context. - * @param xPoints An array of x points, see {@link Graphics2D#drawPolygon(int[], int[], int)} (int[], int[], int)} - * @param yPoints An array of y points, see {@link Graphics2D#drawPolygon(int[], int[], int)} (int[], int[], int)} - */ - protected abstract void strokePolygon(Scratch scratch, Stroke stroke, Paint strokePaint, int[] xPoints, int[] yPoints); - - /** - * Draws a filled polygon. - * - * @param scratch The scratch buffer on which to draw. - * @param fillPaint The paint with which to fill the polyfon - * @param xPoints An array of x points, see {@link Graphics2D#fillPolygon(int[], int[], int)} (int[], int[], int)} - * @param yPoints An array of y points, see {@link Graphics2D#fillPolygon(int[], int[], int)} (int[], int[], int)} - */ - protected abstract void fillPolygon(Scratch scratch, Paint fillPaint, int[] xPoints, int[] yPoints); - - public AbstractPolylineTool(PaintToolType type) { - super(type); - setToolCursor(new Cursor(Cursor.CROSSHAIR_CURSOR)); - } - - /** {@inheritDoc} */ - @Override - public void mouseMoved(MouseEvent e, Point imageLocation) { - setToolCursor(getToolCursor()); - - // Nothing to do if initial point is not yet established - if (points.size() == 0) { - return; - } - - if (e.isShiftDown()) { - Point lastPoint = points.get(points.size() - 1); - currentPoint = Geometry.line(lastPoint, e.getPoint(), getConstrainedAngle()); - points.add(currentPoint); - } else { - currentPoint = imageLocation; - points.add(currentPoint); - } - - int[] xs = points.stream().mapToInt(i -> i.x).toArray(); - int[] ys = points.stream().mapToInt(i -> i.y).toArray(); - - getScratch().clear(); - strokePolyline(getScratch(), getStroke(), getStrokePaint(), xs, ys); - getCanvas().repaint(); - - points.remove(points.size() - 1); - } - - /** {@inheritDoc} */ - @Override - public void mousePressed(MouseEvent e, Point imageLocation) { - - // User double-clicked; complete the polygon - if (e.getClickCount() > 1 && points.size() > 1) { - points.add(currentPoint); - commitPolygon(); - } - - // First click (creating initial point) - else if (currentPoint == null) { - points.add(imageLocation); - } - - // Single click with initial point established - else { - points.add(currentPoint); - } - } - - private void commitPolygon() { - getScratch().clear(); - - int[] xs = points.stream().mapToInt(i -> i.x).toArray(); - int[] ys = points.stream().mapToInt(i -> i.y).toArray(); - - points.clear(); - currentPoint = null; - - if (getFillPaint().isPresent()) { - fillPolygon(getScratch(), getFillPaint().get(), xs, ys); - } - - strokePolygon(getScratch(), getStroke(), getStrokePaint(), xs, ys); - getCanvas().commit(); - } - - private void commitPolyline() { - getScratch().clear(); - - int[] xs = points.stream().mapToInt(i -> i.x).toArray(); - int[] ys = points.stream().mapToInt(i -> i.y).toArray(); - - points.clear(); - currentPoint = null; - - strokePolyline(getScratch(), getStroke(), getStrokePaint(), xs, ys); - getCanvas().commit(); - } - - /** {@inheritDoc} */ - @Override - public void keyPressed(KeyEvent e) { - - // Ignore escape unless at least one point has been defined - if (e.getKeyCode() == KeyEvent.VK_ESCAPE && points.size() > 0) { - points.add(currentPoint); - commitPolyline(); - } - } -} diff --git a/src/main/java/com/defano/jmonet/tools/base/BasicTool.java b/src/main/java/com/defano/jmonet/tools/base/BasicTool.java new file mode 100644 index 00000000..da52d48d --- /dev/null +++ b/src/main/java/com/defano/jmonet/tools/base/BasicTool.java @@ -0,0 +1,127 @@ +package com.defano.jmonet.tools.base; + +import com.defano.jmonet.canvas.PaintCanvas; +import com.defano.jmonet.canvas.Scratch; +import com.defano.jmonet.canvas.observable.SurfaceInteractionObserver; +import com.defano.jmonet.model.Interpolation; +import com.defano.jmonet.model.PaintToolType; +import com.defano.jmonet.tools.attributes.ToolAttributes; +import com.defano.jmonet.tools.cursors.CursorManager; +import com.google.inject.Inject; + +import java.awt.*; + +/** + * A base tool class providing configuration-specific attributes (like stroke and fill), activation state management + * (which canvas the tool is active on, if any) and a reference to the tool's delegate. + * + * @param The object type of the delegate class used by this tool. + */ +public class BasicTool implements Tool, SurfaceInteractionObserver { + + private final PaintToolType toolType; + private PaintCanvas canvas; + private DelegateType delegate; + + @Inject private ToolAttributes toolAttributes; + @Inject private CursorManager cursorManager; + + public BasicTool(PaintToolType toolType) { + this.toolType = toolType; + } + + /** {@inheritDoc} */ + @Override + public void activate(PaintCanvas canvas) { + deactivate(); + this.canvas = canvas; + this.canvas.addSurfaceInteractionObserver(this); + + setToolCursor(getDefaultCursor()); + } + + /** {@inheritDoc} */ + @Override + public void deactivate() { + if (canvas != null) { + canvas.removeSurfaceInteractionObserver(this); + } + + canvas = null; + } + + /** {@inheritDoc} */ + @Override + public boolean isActive() { + return canvas != null; + } + + /** {@inheritDoc} */ + @Override + public Cursor getToolCursor() { + return cursorManager == null ? null : cursorManager.getToolCursor(); + } + + /** {@inheritDoc} */ + @Override + public void setToolCursor(Cursor toolCursor) { + if (cursorManager != null) { + cursorManager.setToolCursor(toolCursor, canvas); + } + } + + /** {@inheritDoc} */ + @Override + public PaintCanvas getCanvas() { + if (canvas == null) { + throw new IllegalStateException("Tool is not active on a canvas. Please call activate() first or use the makeActiveOnCanvas() builder option."); + } + + return canvas; + } + + /** {@inheritDoc} */ + @Override + public Scratch getScratch() { + Scratch scratch = getCanvas().getScratch(); + + Interpolation antialiasing = getAttributes().getAntiAliasing(); + scratch.getAddScratchGraphics(this, null).setAntialiasingMode(antialiasing); + scratch.getRemoveScratchGraphics(this, null).setAntialiasingMode(antialiasing); + + return scratch; + } + + /** {@inheritDoc} */ + @Override + public PaintToolType getPaintToolType() { + return toolType; + } + + /** {@inheritDoc} */ + @Override + public ToolAttributes getAttributes() { + return toolAttributes; + } + + /** + * Gets a reference to the tool delegate. + * @return The tool delegate + */ + @SuppressWarnings("WeakerAccess") + public DelegateType getDelegate() { + if (delegate == null) { + throw new IllegalStateException("Bug! Must invoke setDelegate() before activating the tool."); + } + + return delegate; + } + + /** + * Sets a reference to the tool delegate. + * @param delegate The tool delegate + */ + public void setDelegate(DelegateType delegate) { + this.delegate = delegate; + } +} diff --git a/src/main/java/com/defano/jmonet/tools/base/BoundsTool.java b/src/main/java/com/defano/jmonet/tools/base/BoundsTool.java new file mode 100644 index 00000000..80e0678a --- /dev/null +++ b/src/main/java/com/defano/jmonet/tools/base/BoundsTool.java @@ -0,0 +1,94 @@ +package com.defano.jmonet.tools.base; + +import com.defano.jmonet.canvas.observable.SurfaceInteractionObserver; +import com.defano.jmonet.model.PaintToolType; +import com.defano.jmonet.tools.util.MathUtils; + +import java.awt.*; +import java.awt.event.MouseEvent; + +/** + * A tool that draws shapes by defined by a bounding box, like rectangles, round-rectangles, and ovals. + * + * Click to define the first point in the bounding box, then drag to define the second. There are no restrictions + * on the relative location of the two points. + */ +public class BoundsTool extends BasicTool implements SurfaceInteractionObserver { + + private Point initialPoint; + private Point currentPoint; + + public BoundsTool(PaintToolType type) { + super(type); + } + + /** {@inheritDoc} */ + @Override + public Cursor getDefaultCursor() { + return new Cursor(Cursor.CROSSHAIR_CURSOR); + } + + /** {@inheritDoc} */ + @Override + public void mousePressed(MouseEvent e, Point imageLocation) { + initialPoint = imageLocation; + } + + /** {@inheritDoc} */ + @Override + public void mouseDragged(MouseEvent e, Point canvasLoc) { + currentPoint = canvasLoc; + + if (!getAttributes().isDrawMultiple()) { + getScratch().clear(); + } + + Point originPoint = new Point(initialPoint); + + if (getAttributes().isDrawCentered()) { + int height = currentPoint.y - initialPoint.y; + int width = currentPoint.x - initialPoint.x; + + originPoint.x = initialPoint.x - width / 2; + originPoint.y = initialPoint.y - height / 2; + } + + Rectangle bounds = e.isShiftDown() ? + MathUtils.square(originPoint, currentPoint) : + MathUtils.rectangle(originPoint, currentPoint); + + getAttributes().getFillPaint().ifPresent(paint -> + getDelegate().fillBounds(getScratch(), paint, new Rectangle(bounds.x, bounds.y, bounds.width, bounds.height), e.isShiftDown())); + + getDelegate().strokeBounds(getScratch(), getAttributes().getStroke(), getAttributes().getStrokePaint(), new Rectangle(bounds.x, bounds.y, bounds.width, bounds.height), e.isShiftDown()); + getCanvas().repaint(); + } + + /** {@inheritDoc} */ + @Override + public void mouseReleased(MouseEvent e, Point canvasLoc) { + getCanvas().commit(); + } + + /** {@inheritDoc} */ + @Override + public void mouseMoved(MouseEvent e, Point canvasLoc) { + setToolCursor(getToolCursor()); + } + + /** + * Gets the initial point defined by this tool, or null if no point has yet been defined. + * @return The initial point + */ + public Point getInitialPoint() { + return initialPoint; + } + + /** + * Gets the last point defined by this tool, or null if no point has yet been defined. + * @return The current point. + */ + public Point getCurrentPoint() { + return currentPoint; + } +} diff --git a/src/main/java/com/defano/jmonet/tools/base/BoundsToolDelegate.java b/src/main/java/com/defano/jmonet/tools/base/BoundsToolDelegate.java new file mode 100644 index 00000000..e91cb573 --- /dev/null +++ b/src/main/java/com/defano/jmonet/tools/base/BoundsToolDelegate.java @@ -0,0 +1,35 @@ +package com.defano.jmonet.tools.base; + +import com.defano.jmonet.canvas.Scratch; + +import java.awt.*; + +/** + * A delegate class responsible for rendering the shape drawn by a {@link BoundsTool}. + */ +public interface BoundsToolDelegate { + + /** + * Draws the stroke (outline) of a shape described by a rectangular boundary. + * + * @param scratch The scratch buffer on which to draw + * @param stroke The stroke with which to draw + * @param paint The paint with which to draw + * @param bounds The bounds of the shape to draw + * @param isShiftDown True to indicate that the user is holding the shift key; implementers may use this flag to + * constrain the bounds or otherwise modify the tool behavior. + */ + void strokeBounds(Scratch scratch, Stroke stroke, Paint paint, Rectangle bounds, boolean isShiftDown); + + /** + * Fills a shape described by a rectangular boundary. + * + * @param scratch The scratch buffer on which to draw + * @param fill The paint with which to fill the shape + * @param bounds The bounds of the shape to draw + * @param isShiftDown True to indicate that the user is holding the shift key; implementers may use this flag to + * constrain the bounds or otherwise modify the tool behavior. + */ + void fillBounds(Scratch scratch, Paint fill, Rectangle bounds, boolean isShiftDown); + +} diff --git a/src/main/java/com/defano/jmonet/tools/base/LinearTool.java b/src/main/java/com/defano/jmonet/tools/base/LinearTool.java new file mode 100644 index 00000000..b99b5ad4 --- /dev/null +++ b/src/main/java/com/defano/jmonet/tools/base/LinearTool.java @@ -0,0 +1,65 @@ +package com.defano.jmonet.tools.base; + +import com.defano.jmonet.canvas.observable.SurfaceInteractionObserver; +import com.defano.jmonet.model.PaintToolType; +import com.defano.jmonet.tools.util.MathUtils; + +import java.awt.*; +import java.awt.event.MouseEvent; + +/** + * A tool for drawing shapes defined by two points on the canvas, like lines. + * + * Click the mouse to define the first point, then drag to define the second point. + */ +public class LinearTool extends BasicTool implements SurfaceInteractionObserver { + + private Point initialPoint; + + public LinearTool(PaintToolType type) { + super(type); + } + + /** {@inheritDoc} */ + @Override + public Cursor getDefaultCursor() { + return new Cursor(Cursor.CROSSHAIR_CURSOR); + } + + /** {@inheritDoc} */ + @Override + public void mouseMoved(MouseEvent e, Point canvasLoc) { + setToolCursor(getToolCursor()); + } + + /** {@inheritDoc} */ + @Override + public void mousePressed(MouseEvent e, Point imageLocation) { + initialPoint = imageLocation; + } + + /** {@inheritDoc} */ + @Override + public void mouseDragged(MouseEvent e, Point canvasLoc) { + getScratch().clear(); + + Point currentLoc = canvasLoc; + + if (e.isShiftDown()) { + currentLoc = MathUtils.line(initialPoint, currentLoc, getAttributes().getConstrainedAngle()); + } + + getDelegate().drawLine(getScratch(), getAttributes().getStroke(), getAttributes().getStrokePaint(), initialPoint.x, initialPoint.y, currentLoc.x, currentLoc.y); + getCanvas().repaint(); + } + + /** {@inheritDoc} */ + @Override + public void mouseReleased(MouseEvent e, Point canvasLoc) { + getCanvas().commit(); + } + + public Point getInitialPoint() { + return this.initialPoint; + } +} diff --git a/src/main/java/com/defano/jmonet/tools/base/LinearToolDelegate.java b/src/main/java/com/defano/jmonet/tools/base/LinearToolDelegate.java new file mode 100644 index 00000000..d9dafe18 --- /dev/null +++ b/src/main/java/com/defano/jmonet/tools/base/LinearToolDelegate.java @@ -0,0 +1,25 @@ +package com.defano.jmonet.tools.base; + +import com.defano.jmonet.canvas.Scratch; + +import java.awt.*; + +/** + * A delegate class responsible for rendering shapes drawn by the {@link LinearTool}. + */ +public interface LinearToolDelegate { + + /** + * Draws a line from (x1, y1) to (x2, y2) on the given graphics context. + * + * @param scratch The scratch buffer on which to draw + * @param stroke The stroke with which to draw + * @param paint The paint with which to draw + * @param x1 First x coordinate of the line + * @param y1 First y coordinate of the line + * @param x2 Second x coordinate of the line + * @param y2 Second y coordinate of the line + */ + void drawLine(Scratch scratch, Stroke stroke, Paint paint, int x1, int y1, int x2, int y2); + +} diff --git a/src/main/java/com/defano/jmonet/tools/base/PathTool.java b/src/main/java/com/defano/jmonet/tools/base/PathTool.java new file mode 100644 index 00000000..8eb0ab50 --- /dev/null +++ b/src/main/java/com/defano/jmonet/tools/base/PathTool.java @@ -0,0 +1,63 @@ +package com.defano.jmonet.tools.base; + +import com.defano.jmonet.canvas.observable.SurfaceInteractionObserver; +import com.defano.jmonet.model.PaintToolType; + +import java.awt.*; +import java.awt.event.MouseEvent; + +/** + * A tool for drawing shapes defined by an arbitrary path (series of points) drawn on the canvas, like a paintbrush, + * eraser, or pencil. + * + * Click and drag the mouse around the canvas to define the path of the shape to be rendered. + */ +public class PathTool extends BasicTool implements SurfaceInteractionObserver { + + private Point lastPoint; + + public PathTool(PaintToolType type) { + super(type); + } + + /** {@inheritDoc} */ + @Override + public Cursor getDefaultCursor() { + return new Cursor(Cursor.CROSSHAIR_CURSOR); + } + + /** {@inheritDoc} */ + @Override + public void mouseMoved(MouseEvent e, Point canvasLoc) { + setToolCursor(getToolCursor()); + } + + /** {@inheritDoc} */ + @Override + public void mousePressed(MouseEvent e, Point imageLocation) { + getScratch().clear(); + + getDelegate().startPath(getScratch(), getAttributes().getStroke(), getAttributes().getStrokePaint(), imageLocation); + lastPoint = imageLocation; + + getCanvas().repaint(getScratch().getDirtyRegion()); + } + + /** {@inheritDoc} */ + @Override + public void mouseDragged(MouseEvent e, Point canvasLoc) { + getDelegate().addPoint(getScratch(), getAttributes().getStroke(), getAttributes().getStrokePaint(), lastPoint, canvasLoc); + lastPoint = canvasLoc; + + // While mouse is down, only repaint the modified area of the canvas + getCanvas().repaint(getScratch().getDirtyRegion()); + } + + /** {@inheritDoc} */ + @Override + public void mouseReleased(MouseEvent e, Point canvasLoc) { + getDelegate().completePath(getScratch(), getAttributes().getStroke(), getAttributes().getStrokePaint(), getAttributes().getFillPaint().orElse(null)); + getCanvas().commit(getScratch().getLayerSet()); + } + +} diff --git a/src/main/java/com/defano/jmonet/tools/base/PathToolDelegate.java b/src/main/java/com/defano/jmonet/tools/base/PathToolDelegate.java new file mode 100644 index 00000000..111cf9d0 --- /dev/null +++ b/src/main/java/com/defano/jmonet/tools/base/PathToolDelegate.java @@ -0,0 +1,43 @@ +package com.defano.jmonet.tools.base; + +import com.defano.jmonet.canvas.Scratch; + +import java.awt.*; + +/** + * A delegate class responsible for rendering shapes drawn by the {@link PathToolDelegate}. + */ +public interface PathToolDelegate { + + /** + * Begins drawing a path on the given graphics context. + * + * @param scratch The scratch buffer on which to draw. + * @param stroke The stroke with which to draw + * @param strokePaint The paint with which to draw + * @param initialPoint The first point defined on the path + */ + void startPath(Scratch scratch, Stroke stroke, Paint strokePaint, Point initialPoint); + + /** + * Adds a point to the path begun via a call to {@link #startPath(Scratch, Stroke, Paint, Point)}. + * + * @param scratch The scratch buffer on which to draw. + * @param stroke The stroke with which to draw + * @param strokePaint The paint with which to draw + * @param lastPoint The last point added to the current path + * @param thisPoint The new point to add to the current path + */ + void addPoint(Scratch scratch, Stroke stroke, Paint strokePaint, Point lastPoint, Point thisPoint); + + /** + * Completes the path begun via a call to {@link #startPath(Scratch, Stroke, Paint, Point)}. + * + * @param scratch The scratch buffer on which to draw. + * @param stroke The stroke with which to draw + * @param strokePaint The paint with which to render the stroke + * @param fillPaint The paint with which to fill the shape, null to indicate shape should not be filled + */ + void completePath(Scratch scratch, Stroke stroke, Paint strokePaint, Paint fillPaint); + +} diff --git a/src/main/java/com/defano/jmonet/tools/base/PolylineTool.java b/src/main/java/com/defano/jmonet/tools/base/PolylineTool.java new file mode 100644 index 00000000..9b9729bf --- /dev/null +++ b/src/main/java/com/defano/jmonet/tools/base/PolylineTool.java @@ -0,0 +1,123 @@ +package com.defano.jmonet.tools.base; + +import com.defano.jmonet.canvas.observable.SurfaceInteractionObserver; +import com.defano.jmonet.model.PaintToolType; +import com.defano.jmonet.tools.util.MathUtils; + +import java.awt.*; +import java.awt.event.KeyEvent; +import java.awt.event.MouseEvent; +import java.util.ArrayList; +import java.util.List; + +/** + * A tool that draws shapes defined by a series of interconnected lines, where the end of the current line denotes the + * starting point of the next line. Double-clicking completes the sequence, allowing for the shape to be closed and + * filled. + * + * See {@link com.defano.jmonet.tools.CurveTool} and {@link com.defano.jmonet.tools.PolygonTool} as examples. + */ +public class PolylineTool extends BasicTool implements SurfaceInteractionObserver { + + private final List points = new ArrayList<>(); + private Point currentPoint = null; + + public PolylineTool(PaintToolType toolType) { + super(toolType); + } + + /** {@inheritDoc} */ + @Override + public Cursor getDefaultCursor() { + return new Cursor(Cursor.CROSSHAIR_CURSOR); + } + + /** {@inheritDoc} */ + @Override + public void mouseMoved(MouseEvent e, Point canvasLoc) { + setToolCursor(getToolCursor()); + + // Nothing to do if initial point is not yet established + if (points.size() == 0) { + return; + } + + if (e.isShiftDown()) { + Point lastPoint = points.get(points.size() - 1); + currentPoint = MathUtils.line(lastPoint, canvasLoc, getAttributes().getConstrainedAngle()); + points.add(currentPoint); + } else { + currentPoint = canvasLoc; + points.add(currentPoint); + } + + int[] xs = points.stream().mapToInt(i -> i.x).toArray(); + int[] ys = points.stream().mapToInt(i -> i.y).toArray(); + + getScratch().clear(); + getDelegate().strokePolyline(getScratch(), getAttributes().getStroke(), getAttributes().getStrokePaint(), xs, ys); + getCanvas().repaint(); + + points.remove(points.size() - 1); + } + + /** {@inheritDoc} */ + @Override + public void mousePressed(MouseEvent e, Point imageLocation) { + // User double-clicked; complete the polygon + if (e.getClickCount() > 1 && points.size() > 1) { + points.add(currentPoint); + commitPolygon(); + } + + // First click (creating initial point) + else if (currentPoint == null) { + points.add(imageLocation); + } + + // Single click with initial point established + else { + points.add(currentPoint); + } + } + + private void commitPolygon() { + getScratch().clear(); + + int[] xs = points.stream().mapToInt(i -> i.x).toArray(); + int[] ys = points.stream().mapToInt(i -> i.y).toArray(); + + points.clear(); + currentPoint = null; + + getAttributes().getFillPaint().ifPresent(fillPaint -> + getDelegate().fillPolygon(getScratch(), fillPaint, xs, ys)); + + getDelegate().strokePolygon(getScratch(), getAttributes().getStroke(), getAttributes().getStrokePaint(), xs, ys); + getCanvas().commit(); + } + + private void commitPolyline() { + getScratch().clear(); + + int[] xs = points.stream().mapToInt(i -> i.x).toArray(); + int[] ys = points.stream().mapToInt(i -> i.y).toArray(); + + points.clear(); + currentPoint = null; + + getDelegate().strokePolyline(getScratch(), getAttributes().getStroke(), getAttributes().getStrokePaint(), xs, ys); + getCanvas().commit(); + } + + /** {@inheritDoc} */ + @Override + public void keyPressed(KeyEvent e) { + // Ignore escape unless at least one point has been defined + if (e.getKeyCode() == KeyEvent.VK_ESCAPE && points.size() > 0) { + points.add(currentPoint); + commitPolyline(); + } + } + +} diff --git a/src/main/java/com/defano/jmonet/tools/base/PolylineToolDelegate.java b/src/main/java/com/defano/jmonet/tools/base/PolylineToolDelegate.java new file mode 100644 index 00000000..5f92eb13 --- /dev/null +++ b/src/main/java/com/defano/jmonet/tools/base/PolylineToolDelegate.java @@ -0,0 +1,44 @@ +package com.defano.jmonet.tools.base; + +import com.defano.jmonet.canvas.Scratch; + +import java.awt.*; + +/** + * A delegate class responsible for rendering shapes drawn by the {@link PolylineTool}. + */ +public interface PolylineToolDelegate { + + /** + * Draws one or more sides (edges) of a polygon which is not filled and may not be closed. + * + * @param scratch The scratch buffer on which to draw. + * @param stroke The current stroke context. + * @param strokePaint The current paint context. + * @param xPoints An array of x points, see {@link Graphics2D#drawPolyline(int[], int[], int)} + * @param yPoints An array of y points, see {@link Graphics2D#drawPolyline(int[], int[], int)} + */ + void strokePolyline(Scratch scratch, Stroke stroke, Paint strokePaint, int[] xPoints, int[] yPoints); + + /** + * Draws one or more sides (edges) of a polygon, closing the shape as needed. + * + * @param scratch The scratch buffer on which to draw. + * @param stroke The current stroke context. + * @param strokePaint The current paint context. + * @param xPoints An array of x points, see {@link Graphics2D#drawPolygon(int[], int[], int)} (int[], int[], int)} + * @param yPoints An array of y points, see {@link Graphics2D#drawPolygon(int[], int[], int)} (int[], int[], int)} + */ + void strokePolygon(Scratch scratch, Stroke stroke, Paint strokePaint, int[] xPoints, int[] yPoints); + + /** + * Draws a filled polygon. + * + * @param scratch The scratch buffer on which to draw. + * @param fillPaint The paint with which to fill the polyfon + * @param xPoints An array of x points, see {@link Graphics2D#fillPolygon(int[], int[], int)} (int[], int[], int)} + * @param yPoints An array of y points, see {@link Graphics2D#fillPolygon(int[], int[], int)} (int[], int[], int)} + */ + void fillPolygon(Scratch scratch, Paint fillPaint, int[] xPoints, int[] yPoints); + +} diff --git a/src/main/java/com/defano/jmonet/tools/base/AbstractSelectionTool.java b/src/main/java/com/defano/jmonet/tools/base/SelectionTool.java similarity index 79% rename from src/main/java/com/defano/jmonet/tools/base/AbstractSelectionTool.java rename to src/main/java/com/defano/jmonet/tools/base/SelectionTool.java index 08f42039..f54c8e35 100644 --- a/src/main/java/com/defano/jmonet/tools/base/AbstractSelectionTool.java +++ b/src/main/java/com/defano/jmonet/tools/base/SelectionTool.java @@ -1,12 +1,15 @@ package com.defano.jmonet.tools.base; -import com.defano.jmonet.canvas.layer.ImageLayerSet; import com.defano.jmonet.canvas.PaintCanvas; +import com.defano.jmonet.canvas.layer.ImageLayerSet; +import com.defano.jmonet.canvas.observable.CanvasCommitObserver; +import com.defano.jmonet.canvas.observable.SurfaceInteractionObserver; +import com.defano.jmonet.context.GraphicsContext; import com.defano.jmonet.model.PaintToolType; +import com.defano.jmonet.tools.PerspectiveTool; import com.defano.jmonet.tools.RotateTool; -import com.defano.jmonet.tools.builder.PaintTool; import com.defano.jmonet.tools.selection.MutableSelection; -import com.defano.jmonet.tools.util.Geometry; +import com.defano.jmonet.tools.util.MathUtils; import com.defano.jmonet.tools.util.ImageUtils; import com.defano.jmonet.tools.util.MarchingAnts; import com.defano.jmonet.tools.util.MarchingAntsObserver; @@ -20,9 +23,10 @@ import java.util.Optional; /** - * Mouse and keyboard handler for drawing selections (free-form or bounded) on the canvas. + * A complex tool that draws a bounding shape (typically a rectangle, but may be any closed shape) and provides the + * ability to pick up and move or transform the pixels bound by the selection shape. */ -public abstract class AbstractSelectionTool extends PaintTool implements MarchingAntsObserver, MutableSelection { +public class SelectionTool extends BasicTool implements CanvasCommitObserver, MarchingAntsObserver, MutableSelection, SurfaceInteractionObserver { private final BehaviorSubject> selectedImage = BehaviorSubject.createDefault(Optional.empty()); @@ -33,26 +37,17 @@ public abstract class AbstractSelectionTool extends PaintTool implements Marchin private boolean isMovingSelection = false; private boolean dirty = false; - public AbstractSelectionTool(PaintToolType type) { + public SelectionTool(PaintToolType type) { super(type); } /** - * Invoked to indicate that the user has defined a new point on the selection path. - * - * @param initialPoint The first point defined by the user (i.e., where the mouse was initially pressed) - * @param newPoint A new point to append to the selection path (i.e., where the mouse is now) - * @param isShiftKeyDown When true, indicates user is holding the shift key down - */ - protected abstract void addPointToSelectionFrame(Point initialPoint, Point newPoint, boolean isShiftKeyDown); - - /** - * Invoked to indicate that the given point should be considered the last point in the selection path, and the - * path shape should be closed. - * - * @param finalPoint The final point on the selection path. + * {@inheritDoc} */ - protected abstract void closeSelectionFrame(Point finalPoint); + @Override + public Cursor getDefaultCursor() { + return new Cursor(Cursor.CROSSHAIR_CURSOR); + } /** * Creates a selection bounded by the given rectangle. Equivalent to the user clicking and dragging from the top- @@ -65,9 +60,9 @@ public void createSelection(Rectangle bounds) { completeSelection(); } - addPointToSelectionFrame(bounds.getLocation(), new Point(bounds.x + bounds.width, bounds.y + bounds.height), false); + getDelegate().addPointToSelectionFrame(bounds.getLocation(), new Point(bounds.x + bounds.width, bounds.y + bounds.height), false); getSelectionFromCanvas(); - closeSelectionFrame(new Point(bounds.x + bounds.width, bounds.y + bounds.height)); + getDelegate().closeSelectionFrame(new Point(bounds.x + bounds.width, bounds.y + bounds.height)); } /** @@ -86,11 +81,11 @@ public void createSelection(BufferedImage image, Point location) { // Make an ARGB copy of the image (input may not have alpha channel) BufferedImage argbImage = ImageUtils.argbCopy(image); - Graphics2D g = getCanvas().getScratch().getAddScratchGraphics(this, new Rectangle(location, new Dimension(image.getWidth(), image.getHeight()))); + GraphicsContext g = getCanvas().getScratch().getAddScratchGraphics(this, new Rectangle(location, new Dimension(image.getWidth(), image.getHeight()))); g.drawImage(argbImage, location.x, location.y, null); - addPointToSelectionFrame(location.getLocation(), new Point(location.x + argbImage.getWidth(), location.y + argbImage.getHeight()), false); - closeSelectionFrame(new Point(location.x + argbImage.getWidth(), location.y + argbImage.getHeight())); + getDelegate().addPointToSelectionFrame(location.getLocation(), new Point(location.x + argbImage.getWidth(), location.y + argbImage.getHeight()), false); + getDelegate().closeSelectionFrame(new Point(location.x + argbImage.getWidth(), location.y + argbImage.getHeight())); selectedImage.onNext(Optional.of(argbImage)); // Don't call setDirty(), doing so will remove underlying pixels from the canvas @@ -101,10 +96,10 @@ public void createSelection(BufferedImage image, Point location) { * {@inheritDoc} */ @Override - public void mouseMoved(MouseEvent e, Point imageLocation) { + public void mouseMoved(MouseEvent e, Point canvasLoc) { // Update tool cursor - if (hasSelectionFrame() && getSelectionFrame().contains(imageLocation)) { + if (hasSelectionFrame() && getSelectionFrame().contains(canvasLoc)) { setToolCursor(getMovementCursor()); } else { setToolCursor(getBoundaryCursor()); @@ -139,20 +134,20 @@ public void mousePressed(MouseEvent e, Point imageLocation) { * {@inheritDoc} */ @Override - public void mouseDragged(MouseEvent e, Point imageLocation) { + public void mouseDragged(MouseEvent e, Point canvasLoc) { // User is moving an existing selection if (hasSelection() && isMovingSelection) { setDirty(); - translateSelection(imageLocation.x - lastPoint.x, imageLocation.y - lastPoint.y); + translateSelection(canvasLoc.x - lastPoint.x, canvasLoc.y - lastPoint.y); redrawSelection(true); - lastPoint = imageLocation; + lastPoint = canvasLoc; } // User is defining a new selection rectangle else { Rectangle canvasBounds = new Rectangle(new Point(), getCanvas().getCanvasSize()); - addPointToSelectionFrame(initialPoint, Geometry.constrainToBounds(imageLocation, canvasBounds), e.isShiftDown()); + getDelegate().addPointToSelectionFrame(initialPoint, MathUtils.constrainToBounds(canvasLoc, canvasBounds), e.isShiftDown()); getScratch().clear(); drawSelectionFrame(); @@ -164,11 +159,11 @@ public void mouseDragged(MouseEvent e, Point imageLocation) { * {@inheritDoc} */ @Override - public void mouseReleased(MouseEvent e, Point imageLocation) { + public void mouseReleased(MouseEvent e, Point canvasLoc) { // User released mouse after defining a selection if (!hasSelection() && hasSelectionFrame()) { getSelectionFromCanvas(); - closeSelectionFrame(imageLocation); + getDelegate().closeSelectionFrame(canvasLoc); } } @@ -188,13 +183,15 @@ public void activate(PaintCanvas canvas) { */ @Override public void deactivate() { - super.deactivate(); - // Need to remove selection frame when tool is no longer active completeSelection(); - getCanvas().removeCanvasCommitObserver(this); + if (isActive()) { + getCanvas().removeCanvasCommitObserver(this); + } + MarchingAnts.getInstance().removeObserver(this); + super.deactivate(); } /** @@ -203,11 +200,12 @@ public void deactivate() { * the caller will want to deactivate this tool immediately after calling this method. *

* For example, this allows the user to draw a selection with the lasso tool and then transform it with a transform - * tool (i.e., {@link com.defano.jmonet.tools.PerspectiveTool} without having to redefine the selection bounds. + * tool (i.e., {@link PerspectiveTool} without having to redefine the selection bounds. * * @param to The tool that the current selection should be transferred to. */ - public void morphSelection(AbstractSelectionTool to) { + @SuppressWarnings("unused") + public void morphSelection(SelectionTool to) { if (hasSelection()) { @@ -268,12 +266,12 @@ private void abortSelection() { dirty = false; resetSelection(); - getScratch().clearAdd(); + getScratch().clearAddScratch(); getCanvas().repaint(); } /** - * Drops the selected image onto the canvas (committing the change) and clears the selection outline. This has the + * Drops the selected image onto the canvas (committing the change) and clears the selection frame. This has the * effect of completing a select-and-move operation. */ private void completeSelection() { @@ -327,7 +325,7 @@ protected boolean hasSelectionFrame() { */ protected void getSelectionFromCanvas() { Shape selectionBounds = getSelectionFrame(); - BufferedImage maskedSelection = crop(getCanvas().getCanvasImage()); + BufferedImage maskedSelection = getSelectionCroppedCopy(getCanvas().getCanvasImage()); BufferedImage trimmedSelection = maskedSelection.getSubimage(selectionBounds.getBounds().x, selectionBounds.getBounds().y, selectionBounds.getBounds().width, selectionBounds.getBounds().height); selectedImage.onNext(Optional.of(trimmedSelection)); @@ -354,7 +352,7 @@ public void eraseSelectedPixelsFromCanvas() { Shape selectionFrame = getSelectionFrame(); // Clear image underneath selection - Graphics2D g = getCanvas().getScratch().getRemoveScratchGraphics(this, selectionFrame); + GraphicsContext g = getCanvas().getScratch().getRemoveScratchGraphics(this, selectionFrame); g.setColor(Color.WHITE); g.fill(selectionFrame); @@ -368,11 +366,11 @@ public void eraseSelectedPixelsFromCanvas() { @SuppressWarnings("OptionalGetWithoutIsPresent") @Override public void redrawSelection(boolean includeFrame) { - getScratch().clearAdd(); + getScratch().clearAddScratch(); // Don't draw the selected image when clean, doing so double-paints the selection and adjusts translucency if (hasSelection() && isDirty()) { - Graphics2D g = getCanvas().getScratch().getAddScratchGraphics(this, getSelectionFrame()); + GraphicsContext g = getCanvas().getScratch().getAddScratchGraphics(this, getSelectionFrame()); g.drawImage(selectedImage.getValue().get(), getSelectedImageLocation().x, getSelectedImageLocation().y, null); } @@ -397,16 +395,16 @@ protected Point getSelectedImageLocation() { } /** - * Renders the selection outline (marching ants) on the canvas. + * Renders the selection frame (marching ants) on the canvas. */ protected void drawSelectionFrame() { Shape selectionFrame = getSelectionFrame(); - Graphics2D g = getScratch().getAddScratchGraphics(this, MarchingAnts.getInstance().getMarchingAnts(), selectionFrame); - g.setColor(Color.WHITE); + GraphicsContext g = getScratch().getAddScratchGraphics(this, MarchingAnts.getInstance().getMarchingAnts(), selectionFrame); + g.setColor(MarchingAnts.getInstance().getPathColor()); g.draw(selectionFrame); g.setStroke(MarchingAnts.getInstance().getMarchingAnts()); - g.setColor(Color.BLACK); + g.setColor(MarchingAnts.getInstance().getAntColor()); g.draw(selectionFrame); } @@ -429,9 +427,9 @@ private void commitSelection() { if (hasSelection()) { // Re-render the scratch buffer without the selection frame (don't want to commit marching ants to canvas) - getScratch().clearAdd(); + getScratch().clearAddScratch(); if (hasSelection()) { - Graphics2D g = getCanvas().getScratch().getAddScratchGraphics(this, getSelectionFrame()); + GraphicsContext g = getCanvas().getScratch().getAddScratchGraphics(this, getSelectionFrame()); g.drawImage(selectedImage.getValue().get(), getSelectedImageLocation().x, getSelectedImageLocation().y, null); } @@ -515,6 +513,38 @@ public void onAntsMoved(Stroke ants) { } } + /** + * {@inheritDoc} + */ + @Override + public void resetSelection() { + getDelegate().clearSelectionFrame(); + } + + /** + * {@inheritDoc} + */ + @Override + public void setSelectionOutline(Shape bounds) { + getDelegate().setSelectionFrame(bounds); + } + + /** + * {@inheritDoc} + */ + @Override + public void translateSelection(int xDelta, int yDelta) { + getDelegate().translateSelectionFrame(xDelta, yDelta); + } + + /** + * {@inheritDoc} + */ + @Override + public Shape getSelectionFrame() { + return getDelegate().getSelectionFrame(); + } + /** * Gets the cursor used to drag or move the selection on the canvas. * @@ -529,6 +559,7 @@ public Cursor getMovementCursor() { * * @param movementCursor The movement cursor */ + @SuppressWarnings("unused") public void setMovementCursor(Cursor movementCursor) { this.movementCursor = movementCursor; } @@ -550,4 +581,5 @@ public Cursor getBoundaryCursor() { public void setBoundaryCursor(Cursor boundaryCursor) { this.boundaryCursor = boundaryCursor; } + } diff --git a/src/main/java/com/defano/jmonet/tools/base/SelectionToolDelegate.java b/src/main/java/com/defano/jmonet/tools/base/SelectionToolDelegate.java new file mode 100644 index 00000000..04339286 --- /dev/null +++ b/src/main/java/com/defano/jmonet/tools/base/SelectionToolDelegate.java @@ -0,0 +1,55 @@ +package com.defano.jmonet.tools.base; + +import java.awt.*; + +/** + * A delegate class responsible for rendering selections drawn by the {@link SelectionTool}. + */ +public interface SelectionToolDelegate { + + /** + * Gets the current selection frame, or null, if there is no active selection. + * + * @return The shape of the active selection frame, or null if there is no selection. + */ + Shape getSelectionFrame(); + + /** + * Resets the selection frame such that subsequent calls to {@link #getSelectionFrame()} return null. + */ + void clearSelectionFrame(); + + /** + * Creates or replaces the selection frame with the given shape. Note that tools that do not support arbitrary + * selection shapes (like the marquee tool) will use the bounds of the given shape instead. + * + * @param bounds The shape or bounds of the new selection frame. + */ + void setSelectionFrame(Shape bounds); + + /** + * Translates the location of the current selection frame, if one exists. + * + * @param xDelta The number of pixels to translate in the x-direction + * @param yDelta The number of pixels to translate in the y-direction + */ + void translateSelectionFrame(int xDelta, int yDelta); + + /** + * Invoked to indicate that the user has defined a new point on the selection path. + * + * @param initialPoint The first point defined by the user (i.e., where the mouse was initially pressed) + * @param newPoint A new point to append to the selection path (i.e., where the mouse is now) + * @param isShiftKeyDown When true, indicates user is holding the shift key down + */ + void addPointToSelectionFrame(Point initialPoint, Point newPoint, boolean isShiftKeyDown); + + /** + * Invoked to indicate that the given point should be considered the last point in the selection path, and the + * path shape should be closed. + * + * @param finalPoint The final point on the selection path. + */ + void closeSelectionFrame(Point finalPoint); + +} diff --git a/src/main/java/com/defano/jmonet/tools/base/StrokedCursorPathTool.java b/src/main/java/com/defano/jmonet/tools/base/StrokedCursorPathTool.java index cc5840c6..f5cf02b2 100644 --- a/src/main/java/com/defano/jmonet/tools/base/StrokedCursorPathTool.java +++ b/src/main/java/com/defano/jmonet/tools/base/StrokedCursorPathTool.java @@ -2,13 +2,17 @@ import com.defano.jmonet.canvas.PaintCanvas; import com.defano.jmonet.model.PaintToolType; -import com.defano.jmonet.tools.util.CursorFactory; +import com.defano.jmonet.tools.cursors.CursorFactory; import io.reactivex.Observable; import io.reactivex.disposables.Disposable; import java.awt.*; -public abstract class StrokedCursorPathTool extends AbstractPathTool { +/** + * A {@link PathTool} whose mouse cursor tracks the tool's stroke and paint, like the paintbrush tool. + */ +@SuppressWarnings("unused") +public class StrokedCursorPathTool extends PathTool { private Disposable subscription; private boolean strokeTrackingCursorEnabled = true; @@ -18,6 +22,7 @@ public StrokedCursorPathTool(PaintToolType type) { super(type); } + /** {@inheritDoc} */ @Override public void deactivate() { super.deactivate(); @@ -26,15 +31,16 @@ public void deactivate() { } } + /** {@inheritDoc} */ @Override public void activate(PaintCanvas canvas) { super.activate(canvas); - subscription = Observable.merge(getStrokeObservable(), getStrokePaintObservable(), canvas.getScaleObservable()).subscribe(o -> + subscription = Observable.merge(getAttributes().getStrokeObservable(), getAttributes().getStrokePaintObservable(), canvas.getScaleObservable()).subscribe(o -> { if (strokeTrackingCursorEnabled) { setToolCursor(CursorFactory.makeBrushCursor( - getStroke(), - getStrokePaint(), + getAttributes().getStroke(), + getAttributes().getStrokePaint(), strokeTrackingCursorScaled ? canvas.getScale() : 1.0) ); } diff --git a/src/main/java/com/defano/jmonet/tools/base/Tool.java b/src/main/java/com/defano/jmonet/tools/base/Tool.java new file mode 100644 index 00000000..974a37a5 --- /dev/null +++ b/src/main/java/com/defano/jmonet/tools/base/Tool.java @@ -0,0 +1,85 @@ +package com.defano.jmonet.tools.base; + +import com.defano.jmonet.canvas.PaintCanvas; +import com.defano.jmonet.canvas.Scratch; +import com.defano.jmonet.model.PaintToolType; +import com.defano.jmonet.tools.attributes.ToolAttributes; + +import java.awt.*; + +public interface Tool { + /** + * Activates this tool on a given canvas. + *

+ * A paint tool does not "paint" on the canvas until it is activated. Typically, only one tool is active on + * a canvas at any given time, but there is no technical limitation preventing multiple tools from being active + * at once. While a canvas may have multiple active tools drawing on it, a tool can only be active on a single + * canvas. + *

+ * Use {@link #deactivate()} to stop this tool from painting on the canvas. + * + * @param canvas The paint canvas on which to activate the tool. + */ + void activate(PaintCanvas canvas); + + /** + * Deactivates the tool on the canvas. Invoking this method on a tool this is not presently activated has no effect. + * A deactivated tool no longer affects the canvas and all listeners / observers are un-subscribed making the tool + * available for garbage collection. + */ + void deactivate(); + + /** + * Determines if this tool is presently active on a canvas. A tool is considered active after a call to + * {@link #activate(PaintCanvas)} has been made, but before a call to {@link #deactivate()}. + * + * @return True if the tool is active, false otherwise. + */ + boolean isActive(); + + /** + * Gets the default mouse cursor used when painting with this tool. Note that specific tools may provide methods + * for getting and setting auxiliary cursors that are active during tool-specific states, too. + * + * @return The default tool cursor. + */ + Cursor getToolCursor(); + + /** + * Sets the default mouse cursor used when painting with this tool. + * + * @param toolCursor The default mouse cursor. + */ + void setToolCursor(Cursor toolCursor); + + /** + * Gets the canvas on which this tool is currently painting, or null, if the tool has not been activated (via a + * call to {@link #activate(PaintCanvas)}). + * + * @return The canvas this tool is painting on, or null + */ + PaintCanvas getCanvas(); + + /** + * Gets the scratch buffer of the canvas that this tool is presently activated on, applying this tool's anti- + * aliasing mode to the scratch buffer's graphics context. + * + * @return The active scratch buffer. + */ + Scratch getScratch(); + + /** + * Gets the type of this tool. + * + * @return The tool type. + */ + PaintToolType getPaintToolType(); + + /** + * Gets the set of tool attributes bound to this tool (like paint color, fill mode, line size, etc.) + * + * @return The set of observable tool attributes. + */ + ToolAttributes getAttributes(); + +} diff --git a/src/main/java/com/defano/jmonet/tools/base/AbstractTransformTool.java b/src/main/java/com/defano/jmonet/tools/base/TransformTool.java similarity index 57% rename from src/main/java/com/defano/jmonet/tools/base/AbstractTransformTool.java rename to src/main/java/com/defano/jmonet/tools/base/TransformTool.java index bbdce673..bcd92f84 100644 --- a/src/main/java/com/defano/jmonet/tools/base/AbstractTransformTool.java +++ b/src/main/java/com/defano/jmonet/tools/base/TransformTool.java @@ -1,6 +1,8 @@ package com.defano.jmonet.tools.base; +import com.defano.jmonet.canvas.observable.SurfaceInteractionObserver; import com.defano.jmonet.model.FlexQuadrilateral; +import com.defano.jmonet.context.GraphicsContext; import com.defano.jmonet.model.PaintToolType; import java.awt.*; @@ -8,66 +10,24 @@ import java.awt.image.BufferedImage; /** - * Mouse and keyboard handler for tools that define a bounding box with flexible corners that can be dragged into - * a desired position and shape. + * A {@link SelectionTool} whose rectanglular selection frame can be modified by dragging handles that are rendered by + * the tool at each corner of the frame's bounds. */ -public abstract class AbstractTransformTool extends AbstractSelectionTool { +public class TransformTool extends SelectionTool implements SurfaceInteractionObserver, SelectionToolDelegate { private final static int HANDLE_SIZE = 8; private BufferedImage originalImage; private Rectangle selectionBounds; private FlexQuadrilateral transformBounds; + private TransformToolDelegate transformToolDelegate; private Rectangle topLeftHandle, topRightHandle, bottomRightHandle, bottomLeftHandle; private boolean dragTopLeft, dragTopRight, dragBottomRight, dragBottomLeft; - /** - * Invoked to indicate that the user has dragged/moved the top-left handle of the transform quadrilateral to a - * new position. - * - * @param quadrilateral The quadrilateral representing the transform bounds. - * @param newPosition The new location of the affected drag handle. - * @param isShiftDown True to indicate user is holding shift down; implementers may optionally use this flag - * to constrain drag movement or apply some other feature of the transform. - */ - protected abstract void moveTopLeft(FlexQuadrilateral quadrilateral, Point newPosition, boolean isShiftDown); - - /** - * Invoked to indicate that the user has dragged/moved the top-right handle of the transform quadrilateral to a - * new position. - * - * @param quadrilateral The quadrilateral representing the transform bounds. - * @param newPosition The new location of the affected drag handle. - * @param isShiftDown True to indicate user is holding shift down; implementers may optionally use this flag - * to constrain drag movement or apply some other feature of the transform. - */ - protected abstract void moveTopRight(FlexQuadrilateral quadrilateral, Point newPosition, boolean isShiftDown); - - /** - * Invoked to indicate that the user has dragged/moved the bottom-left handle of the transform quadrilateral to a - * new position. - * - * @param quadrilateral The quadrilateral representing the transform bounds. - * @param newPosition The new location of the affected drag handle. - * @param isShiftDown True to indicate user is holding shift down; implementers may optionally use this flag - * to constrain drag movement or apply some other feature of the transform. - */ - protected abstract void moveBottomLeft(FlexQuadrilateral quadrilateral, Point newPosition, boolean isShiftDown); - - /** - * Invoked to indicate that the user has dragged/moved the bottom-right handle of the transform quadrilateral to a - * new position. - * - * @param quadrilateral The quadrilateral representing the transform bounds. - * @param newPosition The new location of the affected drag handle. - * @param isShiftDown True to indicate user is holding shift down; implementers may optionally use this flag - * to constrain drag movement or apply some other feature of the transform. - */ - protected abstract void moveBottomRight(FlexQuadrilateral quadrilateral, Point newPosition, boolean isShiftDown); - - public AbstractTransformTool(PaintToolType type) { + public TransformTool(PaintToolType type) { super(type); + setDelegate(this); } /** {@inheritDoc} */ @@ -102,8 +62,7 @@ public void mousePressed(MouseEvent e, Point imageLocation) { /** {@inheritDoc} */ @Override - public void mouseDragged(MouseEvent e, Point imageLocation) { - + public void mouseDragged(MouseEvent e, Point canvasLoc) { // Selection exists, see if we're dragging a handle if (hasSelection()) { @@ -112,47 +71,47 @@ public void mouseDragged(MouseEvent e, Point imageLocation) { } if (dragTopLeft) { - moveTopLeft(transformBounds, imageLocation, e.isShiftDown()); + getTransformToolDelegate().moveTopLeft(transformBounds, canvasLoc, e.isShiftDown()); redrawSelection(true); } else if (dragTopRight) { - moveTopRight(transformBounds, imageLocation, e.isShiftDown()); + getTransformToolDelegate().moveTopRight(transformBounds, canvasLoc, e.isShiftDown()); redrawSelection(true); } else if (dragBottomLeft) { - moveBottomLeft(transformBounds, imageLocation, e.isShiftDown()); + getTransformToolDelegate().moveBottomLeft(transformBounds, canvasLoc, e.isShiftDown()); redrawSelection(true); } else if (dragBottomRight) { - moveBottomRight(transformBounds, imageLocation, e.isShiftDown()); + getTransformToolDelegate().moveBottomRight(transformBounds, canvasLoc, e.isShiftDown()); redrawSelection(true); } else { - super.mouseDragged(e, imageLocation); + super.mouseDragged(e, canvasLoc); } } // No selection, delegate to selection tool to define selection else { - super.mouseDragged(e, imageLocation); + super.mouseDragged(e, canvasLoc); } } /** {@inheritDoc} */ @Override - public void mouseReleased(MouseEvent e, Point imageLocation) { + public void mouseReleased(MouseEvent e, Point canvasLoc) { // User is completing selection if (!hasSelection()) { - super.mouseReleased(e, imageLocation); + super.mouseReleased(e, canvasLoc); // Grab a copy of the selected image before we begin transforming it originalImage = getSelectedImage(); } else { - super.mouseReleased(e, imageLocation); + super.mouseReleased(e, canvasLoc); } } /** {@inheritDoc} */ @Override - protected void addPointToSelectionFrame(Point initialPoint, Point newPoint, boolean isShiftKeyDown) { + public void addPointToSelectionFrame(Point initialPoint, Point newPoint, boolean isShiftKeyDown) { selectionBounds = new Rectangle(initialPoint); selectionBounds.add(newPoint); @@ -168,13 +127,13 @@ protected void addPointToSelectionFrame(Point initialPoint, Point newPoint, bool /** {@inheritDoc} */ @Override - protected void closeSelectionFrame(Point finalPoint) { + public void closeSelectionFrame(Point finalPoint) { transformBounds = new FlexQuadrilateral(selectionBounds); } /** {@inheritDoc} */ @Override - public void resetSelection() { + public void clearSelectionFrame() { selectionBounds = null; transformBounds = null; @@ -183,7 +142,7 @@ public void resetSelection() { /** {@inheritDoc} */ @Override - public void setSelectionOutline(Rectangle bounds) { + public void setSelectionFrame(Shape bounds) { transformBounds = new FlexQuadrilateral(bounds); } @@ -195,7 +154,7 @@ public Shape getSelectionFrame() { /** {@inheritDoc} */ @Override - public void translateSelection(int xDelta, int yDelta) { + public void translateSelectionFrame(int xDelta, int yDelta) { selectionBounds.setLocation(selectionBounds.x + xDelta, selectionBounds.y + yDelta); transformBounds.getBottomLeft().x += xDelta; transformBounds.getBottomLeft().y += yDelta; @@ -219,7 +178,7 @@ protected void drawSelectionFrame() { if (hasSelection() && transformBounds != null) { // Render drag handles on selection bounds - Graphics2D g = getCanvas().getScratch().getAddScratchGraphics(this, null); + GraphicsContext g = getCanvas().getScratch().getAddScratchGraphics(this, null); g.setPaint(Color.BLACK); topLeftHandle = new Rectangle(transformBounds.getTopLeft().x, transformBounds.getTopLeft().y, HANDLE_SIZE, HANDLE_SIZE); @@ -234,4 +193,17 @@ protected void drawSelectionFrame() { } } + @SuppressWarnings("WeakerAccess") + public TransformToolDelegate getTransformToolDelegate() { + if (transformToolDelegate == null) { + throw new IllegalStateException("Bug! Must invoke setTransformToolDelegate() before activating the tool."); + } + + return transformToolDelegate; + } + + @SuppressWarnings("WeakerAccess") + public void setTransformToolDelegate(TransformToolDelegate transformToolDelegate) { + this.transformToolDelegate = transformToolDelegate; + } } diff --git a/src/main/java/com/defano/jmonet/tools/base/TransformToolDelegate.java b/src/main/java/com/defano/jmonet/tools/base/TransformToolDelegate.java new file mode 100644 index 00000000..abaf5dcd --- /dev/null +++ b/src/main/java/com/defano/jmonet/tools/base/TransformToolDelegate.java @@ -0,0 +1,55 @@ +package com.defano.jmonet.tools.base; + +import com.defano.jmonet.model.FlexQuadrilateral; + +import java.awt.*; + +/** + * A delegate responsible for handling changes made to the transformed selection frame. + */ +public interface TransformToolDelegate { + + /** + * Invoked to indicate that the user has dragged/moved the top-left handle of the transform quadrilateral to a + * new position. Transforms the selection frame and bounded pixels accordingly. + * + * @param quadrilateral The quadrilateral representing the transform bounds. + * @param newPosition The new location of the affected drag handle. + * @param isShiftDown True to indicate user is holding shift down; implementers may optionally use this flag + * to constrain drag movement or apply some other feature of the transform. + */ + void moveTopLeft(FlexQuadrilateral quadrilateral, Point newPosition, boolean isShiftDown); + + /** + * Invoked to indicate that the user has dragged/moved the top-right handle of the transform quadrilateral to a + * new position. Transforms the selection frame and bounded pixels accordingly. + * + * @param quadrilateral The quadrilateral representing the transform bounds. + * @param newPosition The new location of the affected drag handle. + * @param isShiftDown True to indicate user is holding shift down; implementers may optionally use this flag + * to constrain drag movement or apply some other feature of the transform. + */ + void moveTopRight(FlexQuadrilateral quadrilateral, Point newPosition, boolean isShiftDown); + + /** + * Invoked to indicate that the user has dragged/moved the bottom-left handle of the transform quadrilateral to a + * new position. Transforms the selection frame and bounded pixels accordingly. + * + * @param quadrilateral The quadrilateral representing the transform bounds. + * @param newPosition The new location of the affected drag handle. + * @param isShiftDown True to indicate user is holding shift down; implementers may optionally use this flag + * to constrain drag movement or apply some other feature of the transform. + */ + void moveBottomLeft(FlexQuadrilateral quadrilateral, Point newPosition, boolean isShiftDown); + + /** + * Invoked to indicate that the user has dragged/moved the bottom-right handle of the transform quadrilateral to a + * new position. Transforms the selection frame and bounded pixels accordingly. + * + * @param quadrilateral The quadrilateral representing the transform bounds. + * @param newPosition The new location of the affected drag handle. + * @param isShiftDown True to indicate user is holding shift down; implementers may optionally use this flag + * to constrain drag movement or apply some other feature of the transform. + */ + void moveBottomRight(FlexQuadrilateral quadrilateral, Point newPosition, boolean isShiftDown); +} diff --git a/src/main/java/com/defano/jmonet/tools/brushes/ShapeStroke.java b/src/main/java/com/defano/jmonet/tools/brushes/ShapeStroke.java index 8727c540..fc017898 100644 --- a/src/main/java/com/defano/jmonet/tools/brushes/ShapeStroke.java +++ b/src/main/java/com/defano/jmonet/tools/brushes/ShapeStroke.java @@ -20,6 +20,7 @@ public class ShapeStroke extends StampStroke { * * @param shape The shape of the brush */ + @SuppressWarnings("unused") public ShapeStroke(Shape shape) { this.shapes.add(shape); } @@ -35,7 +36,7 @@ public ShapeStroke(Collection shapes) { public void stampPoint(GeneralPath path, Point point) { for (Shape shape : shapes) { Shape stamp = AffineTransform - .getTranslateInstance(point.x - (shape.getBounds().width / 2) - shape.getBounds().x, point.y - (shape.getBounds().height / 2) - shape.getBounds().y) + .getTranslateInstance(point.x - (shape.getBounds().width / 2.0) - shape.getBounds().x, point.y - (shape.getBounds().height / 2.0) - shape.getBounds().y) .createTransformedShape(shape); path.append(stamp, false); diff --git a/src/main/java/com/defano/jmonet/tools/brushes/StampStroke.java b/src/main/java/com/defano/jmonet/tools/brushes/StampStroke.java index 785bd60d..514e8cff 100644 --- a/src/main/java/com/defano/jmonet/tools/brushes/StampStroke.java +++ b/src/main/java/com/defano/jmonet/tools/brushes/StampStroke.java @@ -1,6 +1,6 @@ package com.defano.jmonet.tools.brushes; -import com.defano.jmonet.tools.util.Geometry; +import com.defano.jmonet.tools.util.MathUtils; import java.awt.*; import java.awt.geom.FlatteningPathIterator; @@ -62,7 +62,7 @@ public Shape createStrokedShape(Shape shape) { */ private void stampLine(GeneralPath path, Point start, Point end) { if (start != null && interval > 0) { - for (Point interpolated : Geometry.linearInterpolation(start, end, interval)) { + for (Point interpolated : MathUtils.linearInterpolation(start, end, interval)) { stampPoint(path, interpolated); } } @@ -76,6 +76,7 @@ private void stampLine(GeneralPath path, Point start, Point end) { * * @return The interpolation interval. */ + @SuppressWarnings("unused") public int getInterpolationInterval() { return interval; } @@ -104,6 +105,7 @@ public void setInterpolationInterval(int interval) { * * @return The current flatness; default is 1 */ + @SuppressWarnings("unused") public double getFlatness() { return flatness; } @@ -121,6 +123,7 @@ public double getFlatness() { * * @param flatness The flatness, in pixels. Value must be greater than or equal to 0. */ + @SuppressWarnings("unused") public void setFlatness(double flatness) { this.flatness = flatness; } diff --git a/src/main/java/com/defano/jmonet/tools/builder/BasicStrokeBuilder.java b/src/main/java/com/defano/jmonet/tools/builder/BasicStrokeBuilder.java new file mode 100644 index 00000000..e9e7eb47 --- /dev/null +++ b/src/main/java/com/defano/jmonet/tools/builder/BasicStrokeBuilder.java @@ -0,0 +1,150 @@ +package com.defano.jmonet.tools.builder; + +import java.awt.*; +import java.util.ArrayList; + +/** + * Builds {@link BasicStroke} objects. + */ +@SuppressWarnings("unused") +public class BasicStrokeBuilder { + + private float width = 1; + private int cap = BasicStroke.CAP_ROUND; + private int join = BasicStroke.JOIN_ROUND; + private final ArrayList dash = new ArrayList<>(); + private float dashPhase = 0; + private float miterLimit = 1; + + /** + * Use {@link StrokeBuilder#withBasicStroke()} to get an instance of this class. + */ + BasicStrokeBuilder() {} + + /** + * Specifies the width, in pixels, of the stroke. + * @param width The width + * @return The builder + */ + public BasicStrokeBuilder ofWidth(float width) { + this.width = width; + return this; + } + + /** + * Ends unclosed subpaths and dash segments with a round decoration that has a radius equal to half of the + * width of the pen. + * + * @return The builder + */ + public BasicStrokeBuilder withRoundCap() { + this.cap = BasicStroke.CAP_ROUND; + return this; + } + + /** + * Ends unclosed subpaths and dash segments with no added decoration. + * + * @return The builder + */ + public BasicStrokeBuilder withButtCap() { + this.cap = BasicStroke.CAP_BUTT; + return this; + } + + /** + * Ends unclosed subpaths and dash segments with a square projection that extends beyond the end of the segment + * to a distance equal to half of the line width. + * + * @return The builder + */ + public BasicStrokeBuilder withSquareCap() { + this.cap = BasicStroke.CAP_SQUARE; + return this; + } + + /** + * Joins path segments by rounding off the corner at a radius of half the line width. + * + * @return The builder + */ + public BasicStrokeBuilder withRoundJoin() { + this.join = BasicStroke.JOIN_ROUND; + return this; + } + + /** + * Joins path segments by extending their outside edges until they meet. + * + * @return The builder + */ + public BasicStrokeBuilder withMiterJoin() { + this.join = BasicStroke.JOIN_MITER; + return this; + } + + /** + * Joins path segments by connecting the outer corners of their wide outlines with a straight segment. + * + * @return The builder + */ + public BasicStrokeBuilder withBevelJoin() { + this.join = BasicStroke.JOIN_BEVEL; + return this; + } + + /** + * Adds a dash pattern element to the stroke. + * + * For example, specifying '5' as the argument to this method produces a dashed/dotted line where every 5 pixels + * are filled, and every 5 pixels are not. Invoking this method subsequent times appends values to the dash + * pattern. Calling a second time with '2' produces a line where every 5 pixels are filled followed by 2 that + * are not. + * + * @param dashLength The length of the dash pattern element to append + * @return The Builder + */ + public BasicStrokeBuilder withDash(float dashLength) { + this.dash.add(dashLength); + return this; + } + + /** + * The limit to trim the miter join; must be greater than or equal to 1.0f. + * + * @param miterLimit The miter limit + * @return The builder + */ + public BasicStrokeBuilder withMiterLimit(float miterLimit) { + this.miterLimit = miterLimit; + return this; + } + + /** + * The offset at which to start the dashing pattern. + * + * @param dashPhase The dash phase offset + * @return The builder + */ + public BasicStrokeBuilder withDashPhase(float dashPhase) { + this.dashPhase = dashPhase; + return this; + } + + /** + * Creates the {@link BasicStroke} as specified. + * + * @return The stroke + */ + public BasicStroke build() { + if (dash.isEmpty()) { + return new BasicStroke(width, cap, join, miterLimit); + } else { + float[] dashArray = new float[dash.size()]; + for (int index = 0; index < dash.size(); index++) { + dashArray[index] = dash.get(index); + } + return new BasicStroke(width, cap, join, miterLimit, dashArray, dashPhase); + } + } +} diff --git a/src/main/java/com/defano/jmonet/tools/builder/PaintTool.java b/src/main/java/com/defano/jmonet/tools/builder/PaintTool.java deleted file mode 100644 index 180438f1..00000000 --- a/src/main/java/com/defano/jmonet/tools/builder/PaintTool.java +++ /dev/null @@ -1,378 +0,0 @@ -package com.defano.jmonet.tools.builder; - -import com.defano.jmonet.canvas.layer.ImageLayerSet; -import com.defano.jmonet.canvas.PaintCanvas; -import com.defano.jmonet.canvas.Scratch; -import com.defano.jmonet.canvas.observable.CanvasCommitObserver; -import com.defano.jmonet.canvas.observable.SurfaceInteractionObserver; -import com.defano.jmonet.model.Interpolation; -import com.defano.jmonet.model.PaintToolType; -import io.reactivex.Observable; -import io.reactivex.subjects.BehaviorSubject; - -import javax.swing.*; -import java.awt.*; -import java.awt.event.KeyEvent; -import java.awt.event.MouseEvent; -import java.awt.image.BufferedImage; -import java.util.Optional; - -/** - * A base tool class holding common attribute providers (like stroke, fill and font), plus empty, template methods for - * keyboard and mouse events. - */ -public abstract class PaintTool implements SurfaceInteractionObserver, CanvasCommitObserver { - - private PaintCanvas canvas; - private final PaintToolType type; - private Cursor toolCursor; - private int constrainedAngle = 15; - - private Observable strokeObservable = BehaviorSubject.createDefault(new BasicStroke(2)); - private Observable strokePaintObservable = BehaviorSubject.createDefault(Color.BLACK); - private Observable> fillPaintObservable = BehaviorSubject.createDefault(Optional.empty()); - private Observable> eraseColorObservable = BehaviorSubject.createDefault(Optional.empty()); - private Observable shapeSidesObservable = BehaviorSubject.createDefault(5); - private Observable fontObservable = BehaviorSubject.createDefault(new Font("Courier", Font.PLAIN, 14)); - private Observable fontColorObservable = BehaviorSubject.createDefault(Color.BLACK); - private Observable intensityObservable = BehaviorSubject.createDefault(0.1); - private Observable drawMultipleObservable = BehaviorSubject.createDefault(false); - private Observable drawCenteredObservable = BehaviorSubject.createDefault(false); - private Observable cornerRadiusObservable = BehaviorSubject.createDefault(10); - private Observable antiAliasingObservable = BehaviorSubject.createDefault(Interpolation.BILINEAR); - - public PaintTool(PaintToolType type) { - this.type = type; - } - - /** - * Activates the tool on a given canvas. - * - * A paint tool does not "paint" on the canvas until it is activated. Typically, only one tool is active on - * a canvas at any given time, but there is no technical limitation preventing multiple tools from being active - * at once. - * - * Use {@link #deactivate()} to stop this tool from painting on the canvas. - * - * @param canvas The paint canvas on which to activate the tool. - */ - public void activate (PaintCanvas canvas) { - this.canvas = canvas; - this.canvas.addSurfaceInteractionObserver(this); - SwingUtilities.invokeLater(() -> canvas.setCursor(toolCursor)); - } - - /** - * Deactivates the tool on the canvas. A deactivated tool no longer affects the canvas and all listeners / observers - * are un-subscribed making the tool available for garbage collection. - */ - public void deactivate() { - if (canvas != null) { - canvas.removeSurfaceInteractionObserver(this); - SwingUtilities.invokeLater(() -> canvas.setCursor(new Cursor(Cursor.DEFAULT_CURSOR))); - } - } - - /** {@inheritDoc} */ - @Override - public void onCommit(PaintCanvas canvas, ImageLayerSet imageLayerSet, BufferedImage canvasImage) { - // Nothing to do - } - - /** - * Gets the type of this tool. - * @return The tool type. - */ - public PaintToolType getToolType() { - return this.type; - } - - /** - * Gets the canvas on which this tool is currently painting, or null, if not active. - * @return The canvas this tool is painting on, or null - */ - public PaintCanvas getCanvas() { - return canvas; - } - - /** - * Gets the scratch buffer of the canvas that this tool is presently activated on, applying this tool's anti- - * aliasing mode to scratch buffer's graphics context. - * - * @return The active scratch buffer. - */ - public Scratch getScratch() { - if (getCanvas() == null) { - throw new IllegalStateException("Tool is not active on a canvas."); - } - - Scratch scratch = getCanvas().getScratch(); - applyRenderingHints(scratch.getAddScratchGraphics(this, null)); - applyRenderingHints(scratch.getRemoveScratchGraphics(this, null)); - - return scratch; - } - - void setFontColorObservable(Observable fontColorObservable) { - this.fontColorObservable = fontColorObservable; - } - - void setStrokePaintObservable(Observable strokePaintObservable) { - if (strokePaintObservable != null) { - this.strokePaintObservable = strokePaintObservable; - } - } - - void setStrokeObservable(Observable strokeObservable) { - if (strokeObservable != null) { - this.strokeObservable = strokeObservable; - } - } - - void setShapeSidesObservable(Observable shapeSidesObservable) { - if (shapeSidesObservable != null) { - this.shapeSidesObservable = shapeSidesObservable; - } - } - - void setFontObservable(Observable fontObservable) { - if (fontObservable != null) { - this.fontObservable = fontObservable; - } - } - - void setFillPaintObservable(Observable> fillPaintObservable) { - this.fillPaintObservable = fillPaintObservable; - } - - /** - * Provides an observable color specifying - * @param eraseColorObservable The color that erased pixels should be assigned (i.e., white). - */ - void setEraseColorObservable(Observable> eraseColorObservable) { - this.eraseColorObservable = eraseColorObservable; - } - - void setIntensityObservable(Observable intensityObservable) { - this.intensityObservable = intensityObservable; - } - - void setDrawMultipleObservable(Observable drawMultipleObservable) { - this.drawMultipleObservable = drawMultipleObservable; - } - - void setDrawCenteredObservable(Observable drawCenteredObservable) { - this.drawCenteredObservable = drawCenteredObservable; - } - - public Stroke getStroke() { - return strokeObservable.blockingFirst(); - } - - public Optional getFillPaint() { - return fillPaintObservable.blockingFirst(); - } - - public Font getFont() { - return fontObservable.blockingFirst(); - } - - public int getShapeSides() { - return shapeSidesObservable.blockingFirst() < 3 ? 3 : - shapeSidesObservable.blockingFirst() > 20 ? 20 : - shapeSidesObservable.blockingFirst(); - } - - public Paint getStrokePaint() { - try { - return strokePaintObservable.blockingFirst(); - } catch (NullPointerException e) { - return Color.BLACK; - } - } - - public Color getEraseColor() { - return eraseColorObservable.blockingFirst().orElse(null); - } - - public Color getFontColor() { - return fontColorObservable.blockingFirst(); - } - - public Observable> getFillPaintObservable() { - return fillPaintObservable; - } - - public Observable> getEraseColorObservable() { - return eraseColorObservable; - } - - public Observable getStrokeObservable() { - return strokeObservable; - } - - public Observable getStrokePaintObservable() { - return strokePaintObservable; - } - - public Observable getShapeSidesObservable() { - return shapeSidesObservable; - } - - public Observable getFontObservable() { - return fontObservable; - } - - public Observable getFontColorObservable() { - return fontColorObservable; - } - - public Observable getIntensityObservable() { - return intensityObservable; - } - - public Observable getDrawCenteredObservable() { - return drawCenteredObservable; - } - - public Observable getDrawMultipleObservable() { - return drawMultipleObservable; - } - - public Cursor getToolCursor() { - return toolCursor; - } - - public void setToolCursor(Cursor toolCursor) { - this.toolCursor = toolCursor; - - if (this.canvas != null) { - SwingUtilities.invokeLater(() -> canvas.setCursor(toolCursor)); - } - } - - public Observable getCornerRadiusObservable() { - return cornerRadiusObservable; - } - - public void setCornerRadiusObservable(Observable cornerRadiusObservable) { - this.cornerRadiusObservable = cornerRadiusObservable; - } - - public int getConstrainedAngle() { - return constrainedAngle; - } - - public void setConstrainedAngle(int constrainedAngle) { - this.constrainedAngle = constrainedAngle; - } - - public Observable getAntiAliasingObservable() { - return antiAliasingObservable; - } - - public void setAntiAliasingObservable(Observable antiAliasingObservable) { - this.antiAliasingObservable = antiAliasingObservable; - } - - public void applyRenderingHints(Graphics2D g2d) { - switch (antiAliasingObservable.blockingFirst()) { - case NONE: - g2d.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_OFF); - g2d.setRenderingHint(RenderingHints.KEY_TEXT_ANTIALIASING, RenderingHints.VALUE_TEXT_ANTIALIAS_OFF); - break; - case DEFAULT: - g2d.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_DEFAULT); - g2d.setRenderingHint(RenderingHints.KEY_INTERPOLATION, RenderingHints.VALUE_INTERPOLATION_NEAREST_NEIGHBOR); - g2d.setRenderingHint(RenderingHints.KEY_TEXT_ANTIALIASING, RenderingHints.VALUE_TEXT_ANTIALIAS_DEFAULT); - break; - case NEAREST_NEIGHBOR: - g2d.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON); - g2d.setRenderingHint(RenderingHints.KEY_INTERPOLATION, RenderingHints.VALUE_INTERPOLATION_NEAREST_NEIGHBOR); - g2d.setRenderingHint(RenderingHints.KEY_TEXT_ANTIALIASING, RenderingHints.VALUE_TEXT_ANTIALIAS_ON); - break; - case BICUBIC: - g2d.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON); - g2d.setRenderingHint(RenderingHints.KEY_INTERPOLATION, RenderingHints.VALUE_INTERPOLATION_BICUBIC); - g2d.setRenderingHint(RenderingHints.KEY_TEXT_ANTIALIASING, RenderingHints.VALUE_TEXT_ANTIALIAS_ON); - break; - case BILINEAR: - g2d.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON); - g2d.setRenderingHint(RenderingHints.KEY_INTERPOLATION, RenderingHints.VALUE_INTERPOLATION_BILINEAR); - g2d.setRenderingHint(RenderingHints.KEY_TEXT_ANTIALIASING, RenderingHints.VALUE_TEXT_ANTIALIAS_ON); - break; - } - } - - protected void erase(Scratch scratch, Shape shape, Stroke stroke) { - Paint erasePaint = getEraseColor(); - - Graphics2D g = erasePaint == null ? - scratch.getRemoveScratchGraphics(this, stroke, shape) : - scratch.getAddScratchGraphics(this, stroke, shape); - - g.setStroke(stroke); - g.setPaint(erasePaint == null ? getCanvas().getCanvasBackground() : erasePaint); - g.draw(shape); - } - - /** {@inheritDoc} */ - @Override - public void mouseClicked(MouseEvent e, Point imageLocation) { - // Nothing to do; override in subclasses. - } - - /** {@inheritDoc} */ - @Override - public void mousePressed(MouseEvent e, Point imageLocation) { - // Nothing to do; override in subclasses. - } - - /** {@inheritDoc} */ - @Override - public void mouseReleased(MouseEvent e, Point imageLocation) { - // Nothing to do; override in subclasses. - } - - /** {@inheritDoc} */ - @Override - public void mouseEntered(MouseEvent e, Point imageLocation) { - // Nothing to do; override in subclasses. - } - - /** {@inheritDoc} */ - @Override - public void mouseExited(MouseEvent e, Point imageLocation) { - // Nothing to do; override in subclasses. - } - - /** {@inheritDoc} */ - @Override - public void mouseDragged(MouseEvent e, Point imageLocation) { - // Nothing to do; override in subclasses. - } - - /** {@inheritDoc} */ - @Override - public void mouseMoved(MouseEvent e, Point imageLocation) { - // Nothing to do; override in subclasses. - } - - /** {@inheritDoc} */ - @Override - public void keyTyped(KeyEvent e) { - // Nothing to do; override in subclasses. - } - - /** {@inheritDoc} */ - @Override - public void keyPressed(KeyEvent e) { - // Nothing to do; override in subclasses. - } - - /** {@inheritDoc} */ - @Override - public void keyReleased(KeyEvent e) { - // Nothing to do; override in subclasses. - } -} diff --git a/src/main/java/com/defano/jmonet/tools/builder/PaintToolBuilder.java b/src/main/java/com/defano/jmonet/tools/builder/PaintToolBuilder.java index e67e8178..c8a86b3b 100644 --- a/src/main/java/com/defano/jmonet/tools/builder/PaintToolBuilder.java +++ b/src/main/java/com/defano/jmonet/tools/builder/PaintToolBuilder.java @@ -1,10 +1,19 @@ package com.defano.jmonet.tools.builder; +import com.defano.jmonet.tools.attributes.*; import com.defano.jmonet.canvas.JFXPaintCanvasNode; import com.defano.jmonet.canvas.PaintCanvas; import com.defano.jmonet.model.Interpolation; import com.defano.jmonet.model.PaintToolType; -import com.defano.jmonet.tools.base.AbstractBoundsTool; +import com.defano.jmonet.tools.FillTool; +import com.defano.jmonet.tools.PolygonTool; +import com.defano.jmonet.tools.TextTool; +import com.defano.jmonet.tools.base.BoundsTool; +import com.defano.jmonet.tools.cursors.CursorManager; +import com.defano.jmonet.tools.cursors.SwingCursorManager; +import com.defano.jmonet.tools.base.Tool; +import com.google.inject.AbstractModule; +import com.google.inject.Guice; import io.reactivex.Observable; import io.reactivex.subjects.BehaviorSubject; @@ -14,11 +23,18 @@ /** * A utility for building paint tools. */ +@SuppressWarnings({"unused", "WeakerAccess"}) public class PaintToolBuilder { private final PaintToolType type; + // Non-observable attributes private PaintCanvas canvas; + private MarkPredicate markPredicate; + private FillFunction fillFunction; + private BoundaryFunction boundaryFunction; + + // Observable attributes private Observable strokeObservable; private Observable strokePaintObservable; private Observable> fillPaintObservable = BehaviorSubject.createDefault(Optional.empty()); @@ -31,6 +47,12 @@ public class PaintToolBuilder { private Observable drawCenteredObservable; private Observable cornerRadiusObservable; private Observable antiAliasingObservable; + private Observable constrainedAngleObservable; + private Observable minimumScaleObservable; + private Observable maximumScaleObservable; + private Observable magnificationStepObservable; + private Observable recenterOnMagnifyObservable; + private Observable pathInterpolationObservable; /** * Constructs a builder for the specified tool type. Use {@link #create(PaintToolType)} to retrieve an instance @@ -54,42 +76,49 @@ public static PaintToolBuilder create(PaintToolType toolType) { /** * Makes the newly built tool active on the given canvas. The tool can be activated manually (instead of via this - * method by invoking {@link PaintTool#activate(PaintCanvas)}). + * method by invoking {@link Tool#activate(PaintCanvas)}). * * @param jfxPaintCanvasNode The JavaFX canvas on which to activate the tool * @return The PaintToolBuilder */ public PaintToolBuilder makeActiveOnCanvas(JFXPaintCanvasNode jfxPaintCanvasNode) { + if (this.canvas != null) { + throw new IllegalStateException("Paint tool may only be active on one canvas at a time."); + } + this.canvas = jfxPaintCanvasNode.getCanvas(); return this; } /** * Makes the newly built tool active on the given canvas. The tool can be activated manually (instead of via this - * method by invoking {@link PaintTool#activate(PaintCanvas)}). + * method by invoking {@link Tool#activate(PaintCanvas)}). * * @param canvas The Swing canvas on which to activate the tool * @return The PaintToolBuilder */ public PaintToolBuilder makeActiveOnCanvas(PaintCanvas canvas) { + if (this.canvas != null) { + throw new IllegalStateException("Paint tool may only be active on one canvas at a time."); + } + this.canvas = canvas; return this; } /** - * Specifies the font painted by the tool. Applies only to the {@link com.defano.jmonet.tools.TextTool}. + * Specifies the font painted by the tool. Applies only to the {@link TextTool}. * * @param font The font to paint * @return The PaintToolBuilder */ public PaintToolBuilder withFont(Font font) { - this.fontObservable = BehaviorSubject.createDefault(font); - return this; + return withFontObservable(BehaviorSubject.createDefault(font)); } /** * Specifies an observable provider of the font painted by the tool. Applies only to the - * {@link com.defano.jmonet.tools.TextTool}. + * {@link TextTool}. * * @param fontProvider The font to paint * @return The PaintToolBuilder @@ -100,19 +129,18 @@ public PaintToolBuilder withFontObservable(Observable fontProvider) { } /** - * Specifies the color of text painted by the tool. Applies only to the {@link com.defano.jmonet.tools.TextTool}). + * Specifies the color of text painted by the tool. Applies only to the {@link TextTool}). * * @param color The text color * @return The PaintToolBuilder */ public PaintToolBuilder withFontColor(Color color) { - this.fontColorObservable = BehaviorSubject.createDefault(color); - return this; + return withFontColorObservable(BehaviorSubject.createDefault(color)); } /** * Specifies an observable provider of the color of the text painted by the tool. Applies only to the - * {@link com.defano.jmonet.tools.TextTool}. + * {@link TextTool}. * * @param colorProvider The color of the text to paint * @return The PaintToolBuilder @@ -123,19 +151,18 @@ public PaintToolBuilder withFontColorObservable(Observable colorProvider) } /** - * Specifies the number of sides drawn by the tool. Applies only to the {@link com.defano.jmonet.tools.PolygonTool}. + * Specifies the number of sides drawn by the tool. Applies only to the {@link PolygonTool}. * * @param sides The number of sides drawn on a regular polygon * @return The PaintToolBuilder */ public PaintToolBuilder withShapeSides(int sides) { - this.shapeSidesObservable = BehaviorSubject.createDefault(sides); - return this; + return withShapeSidesObservable(BehaviorSubject.createDefault(sides)); } /** * Specifies an observable provider of the number of sides drawn by the tool. Applies only to the - * {@link com.defano.jmonet.tools.PolygonTool}. + * {@link PolygonTool}. * * @param shapeSidesProvider The number of sides drawn on a regular polygon * @return The PaintToolBuilder @@ -147,14 +174,13 @@ public PaintToolBuilder withShapeSidesObservable(Observable shapeSidesP /** * Specifies the stroke to be drawn by the tool. The stroke represents the shape of "pen" used to draw paths, lines, - * and brush strokes. Use StrokeBuilder to create complex strokes. + * and brush strokes. Use {@link StrokeBuilder} to create custom strokes. * * @param stroke The stroke to be drawn by this tool * @return Ths PaintToolBuilder */ public PaintToolBuilder withStroke(Stroke stroke) { - this.strokeObservable = BehaviorSubject.createDefault(stroke); - return this; + return withStrokeObservable(BehaviorSubject.createDefault(stroke)); } /** @@ -177,8 +203,7 @@ public PaintToolBuilder withStrokeObservable(Observable strokeProvider) * @return The PaintToolBuilder */ public PaintToolBuilder withStrokePaint(Paint strokePaint) { - this.strokePaintObservable = BehaviorSubject.createDefault(strokePaint); - return this; + return withStrokePaintObservable(BehaviorSubject.createDefault(strokePaint)); } /** @@ -200,8 +225,7 @@ public PaintToolBuilder withStrokePaintObservable(Observable strokePaintP * @return The PaintToolBuilder */ public PaintToolBuilder withFillPaint(Paint paint) { - this.fillPaintObservable = BehaviorSubject.createDefault(paint == null ? Optional.empty() : Optional.of(paint)); - return this; + return withFillPaintObservable(BehaviorSubject.createDefault(paint == null ? Optional.empty() : Optional.of(paint))); } /** @@ -220,29 +244,30 @@ public PaintToolBuilder withFillPaintObservable(Observable> pain * Specifies the color that pixels are changed to when they're erased (via the eraser or pencil tools). Specify null * for fully-transparent (default behavior). *

- * Note that this color does not affect the color of "void" pixels that are left when selecting a region and moving - * or deleting it. Further note that the default boundary behavior associated with - * {@link com.defano.jmonet.tools.FillTool} looks for fully transparent pixels, thus, when changing the erase color + * Note that this value does not affect the color of "void" pixels that are left behind when selecting a region and + * moving or deleting it. Further note that the default boundary behavior associated with + * {@link FillTool} looks for fully transparent pixels to be filled, thus, when changing the erase color * to a non-null value, erased pixels will not be filled by this tool (install a custom - * BoundaryFunction if such behavior is desired). + * {@link BoundaryFunction} using {@link #withBoundaryFunction(BoundaryFunction)} if such behavior is desired). * * @param paint The color that erased pixels should become; null means fully transparent. * @return The PaintToolBuilder */ public PaintToolBuilder withEraseColor(Color paint) { - this.erasePaintObservable = BehaviorSubject.createDefault(paint == null ? Optional.empty() : Optional.of(paint)); - return this; + return withEraseColorObservable( + BehaviorSubject.createDefault(paint == null ? Optional.empty() : Optional.of(paint)) + ); } /** * Specifies an observable provider of the paint that pixels are changed to when they're erased (via the eraser or - * pencil tools). Specify {@link Optional#empty()} for fully-transparent (default behavior). + * pencil tools). Specify {@link Optional#empty()} for fully-transparent (the default behavior). *

- * Note that this color does not affect the color of "void" pixels that are left when selecting a region and moving - * or deleting it. Further note that the default boundary behavior associated with - * {@link com.defano.jmonet.tools.FillTool} looks for fully transparent pixels, thus, when changing the erase color + * Note that this value does not affect the color of "void" pixels that are left behind when selecting a region and + * moving or deleting it. Further note that the default boundary behavior associated with + * {@link FillTool} looks for fully transparent pixels to be filled, thus, when changing the erase color * to a non-null value, erased pixels will not be filled by this tool (install a custom - * BoundaryFunction if such behavior is desired). + * {@link BoundaryFunction} using {@link #withBoundaryFunction(BoundaryFunction)} if such behavior is desired). * * @param erasePaintObservable Observable providing the color that erased pixels should become; null means fully * transparent. @@ -254,20 +279,19 @@ public PaintToolBuilder withEraseColorObservable(Observable> era } /** - * Specifies the intensity with which the tool paints. Used only by the - * {@link com.defano.jmonet.tools.AirbrushTool}. + * Specifies the intensity with which the airbrush paints. Has no effect on other tools. * * @param intensity A value between 0.0 and 1.0 where 0 is no intensity (tool produces no paint) and 1.0 is full * intensity. * @return The PaintToolBuilder */ public PaintToolBuilder withIntensity(double intensity) { - this.intensityObservable = BehaviorSubject.createDefault(intensity); - return this; + return withIntensityObservable(BehaviorSubject.createDefault(intensity)); } /** - * Specifies an observable provider of the intensity with which the tool paints. See {@link #withIntensity(double)}. + * Specifies an observable provider of the intensity with which the airbrush paints. See + * {@link #withIntensity(double)}. * * @param intensityObservable A value between 0.0 and 1.0 where 0 is no intensity (tool produces no paint) and 1.0 * is full intensity. @@ -280,7 +304,7 @@ public PaintToolBuilder withIntensityObservable(Observable intensityObse /** * Specifies an observable provider of a boolean value indicating whether the tool defines bounds by dragging - * from the center-out, or from top-left to bottom-right. + * from the center-out, or from top-left to bottom-right. Affects tools extending {@link BoundsTool}. * * @param drawCenteredObservable True to define bounds from the center-out; false for top-left to bottom-right * @return The PaintToolBuilder @@ -292,18 +316,18 @@ public PaintToolBuilder withDrawCenteredObservable(Observable drawCente /** * Specifies whether the tool defines bounds by dragging from the center-out, or from the top-left to bottom-right. + * Affects tools extending {@link BoundsTool}. * * @param drawCentered True to define bounds from the center-out; false for top-left to bottom-right * @return The PaintToolBuilder */ public PaintToolBuilder withDrawCentered(boolean drawCentered) { - this.drawCenteredObservable = BehaviorSubject.createDefault(drawCentered); - return this; + return withDrawCenteredObservable(BehaviorSubject.createDefault(drawCentered)); } /** * Specifies an observable provider of a boolean value indicating whether the tool will draw multiple shapes or - * just one. + * just one. Affects tools extending {@link BoundsTool}. * * @param drawMultipleObservable True to draw multiple shapes; false to draw just one. * @return The PaintToolBuilder @@ -315,24 +339,33 @@ public PaintToolBuilder withDrawMultipleObservable(Observable drawMulti /** * Specifies whether the tool should draw a single shape, or a trace of multiple shapes as the mouse is dragged. - * Affects tools extending {@link AbstractBoundsTool}. + * Affects tools extending {@link BoundsTool}. * * @param drawMultiple True to draw multiple shapes; false to draw just one. * @return The PaintToolBuilder */ public PaintToolBuilder withDrawMultiple(boolean drawMultiple) { - this.drawMultipleObservable = BehaviorSubject.createDefault(drawMultiple); - return this; + return withDrawMultipleObservable(BehaviorSubject.createDefault(drawMultiple)); } /** - * Specifies the height and width of the corner used for round rectangles. + * Specifies the height and width of the corner used when drawing round rectangles. Has no effect on other tools. * * @param cornerRadius The height and width of the corner radius * @return The PaintToolBuilder */ public PaintToolBuilder withCornerRadius(int cornerRadius) { - this.cornerRadiusObservable = BehaviorSubject.createDefault(cornerRadius); + return withCornerRadiusObservable(BehaviorSubject.createDefault(cornerRadius)); + } + + /** + * Specifies the height and width of the corner used when drawing round rectangles. Has no effect on other tools. + * + * @param observable An observable of the height and width of the corner radius + * @return The PaintToolBuilder + */ + public PaintToolBuilder withCornerRadiusObservable(Observable observable) { + this.cornerRadiusObservable = observable; return this; } @@ -343,8 +376,7 @@ public PaintToolBuilder withCornerRadius(int cornerRadius) { * @return The PaintToolBuilder */ public PaintToolBuilder withAntiAliasing(Interpolation mode) { - this.antiAliasingObservable = BehaviorSubject.createDefault(mode); - return this; + return withAntiAliasingObservable(BehaviorSubject.createDefault(mode)); } /** @@ -359,60 +391,259 @@ public PaintToolBuilder withAntiAliasingObservable(Observable obs } /** - * Creates a paint tool as previously configured. + * Specifies an observable indicating whether the airbrush's path interpolation is enabled. When enabled, the + * airbrush will paint a smooth series of single-pixel "stamps" rather than a line between captured mouse points. + * Enabled by default; has no effect on other tools. + * + * @param observable The path interpolation observable + * @return The PaintToolBuilder. + */ + public PaintToolBuilder withPathInterpolationObservable(Observable observable) { + this.pathInterpolationObservable = observable; + return this; + } + + /** + * Specifies whether the airbrush's path interpolation is enabled. When enabled, the airbrush will paint a smooth + * series of single-pixel "stamps" rather than a line between captured mouse points. Enabled by default; has no + * effect on other tools. + * + * @param enabled When true, path interpolation will be used. + * @return The PaintToolBuilder. + */ + public PaintToolBuilder withPathInterpolation(boolean enabled) { + return withPathInterpolationObservable(BehaviorSubject.createDefault(enabled)); + } + + /** + * Specifies an observable providing the constrained angle (in degrees) to use with this tool. The constrained angle + * is used to snap drawn shapes and selections (like lines, polygons and rotations) to the nearest multiple of this + * value when the shift key is held down. + * + * @param observable The constrained angle, in degrees. + * @return The PaintToolBuilder + */ + public PaintToolBuilder withConstrainedAngleObservable(Observable observable) { + this.constrainedAngleObservable = observable; + return this; + } + + /** + * Specifies the constrained angle (in degrees) to use with this tool. The constrained angle is used to snap drawn + * shapes and selections (like lines, polygons and rotations) to the nearest multiple of this value when the shift + * key is held down. + * + * @param angle The constrained angle, in degrees. + * @return The PaintToolBuilder + */ + public PaintToolBuilder withConstrainedAngle(int angle) { + return withConstrainedAngleObservable(BehaviorSubject.createDefault(angle)); + } + + /** + * Specifies an observable providing the maximum scale value. See {@link #withMaximumScale(double)} for details. + * + * @param observable The maximum scale observable. + * @return The PaintToolBuilder. + */ + public PaintToolBuilder withMaximumScaleObservable(Observable observable) { + this.maximumScaleObservable = observable; + return this; + } + + /** + * Specifies the maximum allowable scale value that the magnifier tool will magnify to. Has no effect on other + * tools. + * + * @param maximumScale The maximum scale value that the magnifier will apply to the canvas. + * @return The PaintToolBuilder. + */ + public PaintToolBuilder withMaximumScale(double maximumScale) { + return withMaximumScaleObservable(BehaviorSubject.createDefault(maximumScale)); + } + + /** + * Specifies an observable providing the minimum scale value. See {@link #withMinimumScale(double)} for details. + * + * @param observable The maximum scale observable. + * @return The PaintToolBuilder. + */ + public PaintToolBuilder withMinimumScaleObservable(Observable observable) { + this.minimumScaleObservable = observable; + return this; + } + + /** + * Specifies the minimum allowable scale value that the magnifier tool will magnify to. Has no effect on other + * tools. + * + * @param minimumScale The minimum scale value that the magnifier will apply to the canvas. + * @return The PaintToolBuilder. + */ + public PaintToolBuilder withMinimumScale(double minimumScale) { + return withMinimumScaleObservable(BehaviorSubject.createDefault(minimumScale)); + } + + /** + * Specifies an observable providing the magnification step multiple. See {@link #withMagnificationStep(double)} + * for details. + * + * @param observable The magnification step observable. + * @return The PaintToolBuilder. + */ + public PaintToolBuilder withMagnificationStepObservable(Observable observable) { + this.magnificationStepObservable = observable; + return this; + } + + /** + * Specifies a value by which the canvas's current scale will multiplied or divided each time the magnifier tool is + * invoked to zoom in or zoom out. + * + * @param magnificationStep The magnification step multiple. + * @return The PaintToolBuilder. + */ + public PaintToolBuilder withMagnificationStep(double magnificationStep) { + return withMagnificationStepObservable(BehaviorSubject.createDefault(magnificationStep)); + } + + public PaintToolBuilder withRecenterOnMagnifyObservable(Observable observable) { + this.recenterOnMagnifyObservable = observable; + return this; + } + + public PaintToolBuilder withRecenterOnMagnify(boolean recenterOnMagnify) { + return withRecenterOnMagnifyObservable(BehaviorSubject.createDefault(recenterOnMagnify)); + } + + /** + * Specifies the predicate function used to determine if a canvas pixel is considered "marked," not blank (for + * example, used in determining if the pencil tool should mark or erase). See {@link MarkPredicate} for details. + * + * @param markPredicate The mark predicate function + * @return The PaintToolBuilder + */ + public PaintToolBuilder withMarkPredicate(MarkPredicate markPredicate) { + this.markPredicate = markPredicate; + return this; + } + + /** + * Specifies the function used to color the canvas with paint flooding a region. See {@link FillFunction} for + * details. + * + * @param fillFunction The fill function to use + * @return The PaintToolBuilder + */ + public PaintToolBuilder withFillFunction(FillFunction fillFunction) { + this.fillFunction = fillFunction; + return this; + } + + /** + * Specifies the function used to detect when paint flooding a region has reached a boundary. See + * {@link BoundaryFunction} for details. + * + * @param boundaryFunction The boundary function to use. + * @return The PaintToolBuilder + */ + public PaintToolBuilder withBoundaryFunction(BoundaryFunction boundaryFunction) { + this.boundaryFunction = boundaryFunction; + return this; + } + + /** + * Creates a paint tool as configured by this builder. * * @return The built paint tool. */ - public PaintTool build() { + public Tool build() { - PaintTool selectedTool = type.getToolInstance(); + Tool selectedTool = Guice.createInjector(new ToolAssembly()).getInstance(type.getToolClass()); + ToolAttributes toolAttributes = selectedTool.getAttributes(); if (strokeObservable != null) { - selectedTool.setStrokeObservable(strokeObservable); + toolAttributes.setStrokeObservable(strokeObservable); } if (strokePaintObservable != null) { - selectedTool.setStrokePaintObservable(strokePaintObservable); + toolAttributes.setStrokePaintObservable(strokePaintObservable); } if (erasePaintObservable != null) { - selectedTool.setEraseColorObservable(erasePaintObservable); + toolAttributes.setEraseColorObservable(erasePaintObservable); } if (shapeSidesObservable != null) { - selectedTool.setShapeSidesObservable(shapeSidesObservable); + toolAttributes.setShapeSidesObservable(shapeSidesObservable); } if (fontObservable != null) { - selectedTool.setFontObservable(fontObservable); + toolAttributes.setFontObservable(fontObservable); } if (fillPaintObservable != null) { - selectedTool.setFillPaintObservable(fillPaintObservable); + toolAttributes.setFillPaintObservable(fillPaintObservable); } if (fontColorObservable != null) { - selectedTool.setFontColorObservable(fontColorObservable); + toolAttributes.setFontColorObservable(fontColorObservable); } if (intensityObservable != null) { - selectedTool.setIntensityObservable(intensityObservable); + toolAttributes.setIntensityObservable(intensityObservable); } if (drawMultipleObservable != null) { - selectedTool.setDrawMultipleObservable(drawMultipleObservable); + toolAttributes.setDrawMultipleObservable(drawMultipleObservable); } if (drawCenteredObservable != null) { - selectedTool.setDrawCenteredObservable(drawCenteredObservable); + toolAttributes.setDrawCenteredObservable(drawCenteredObservable); } if (cornerRadiusObservable != null) { - selectedTool.setCornerRadiusObservable(cornerRadiusObservable); + toolAttributes.setCornerRadiusObservable(cornerRadiusObservable); } if (antiAliasingObservable != null) { - selectedTool.setAntiAliasingObservable(antiAliasingObservable); + toolAttributes.setAntiAliasingObservable(antiAliasingObservable); + } + + if (constrainedAngleObservable != null) { + toolAttributes.setConstrainedAngleObservable(constrainedAngleObservable); + } + + if (maximumScaleObservable != null) { + toolAttributes.setMaximumScaleObservable(maximumScaleObservable); + } + + if (minimumScaleObservable != null) { + toolAttributes.setMinimumScaleObservable(minimumScaleObservable); + } + + if (magnificationStepObservable != null) { + toolAttributes.setMagnificationStepObservable(magnificationStepObservable); + } + + if (recenterOnMagnifyObservable != null) { + toolAttributes.setRecenterOnMagnifyObservable(recenterOnMagnifyObservable); + } + + if (pathInterpolationObservable != null) { + toolAttributes.setPathInterpolationObservable(pathInterpolationObservable); + } + + if (markPredicate != null) { + toolAttributes.setMarkPredicate(markPredicate); + } + + if (fillFunction != null) { + toolAttributes.setFillFunction(fillFunction); + } + + if (boundaryFunction != null) { + toolAttributes.setBoundaryFunction(boundaryFunction); } if (canvas != null) { @@ -421,4 +652,13 @@ public PaintTool build() { return selectedTool; } + + private static class ToolAssembly extends AbstractModule { + + @Override + protected void configure() { + bind(ToolAttributes.class).to(RxToolAttributes.class); + bind(CursorManager.class).to(SwingCursorManager.class); + } + } } diff --git a/src/main/java/com/defano/jmonet/tools/builder/ShapeStrokeBuilder.java b/src/main/java/com/defano/jmonet/tools/builder/ShapeStrokeBuilder.java new file mode 100644 index 00000000..4ec43188 --- /dev/null +++ b/src/main/java/com/defano/jmonet/tools/builder/ShapeStrokeBuilder.java @@ -0,0 +1,291 @@ +package com.defano.jmonet.tools.builder; + +import com.defano.jmonet.model.Quadrilateral; +import com.defano.jmonet.tools.brushes.ShapeStroke; +import com.defano.jmonet.tools.util.MathUtils; + +import javax.swing.*; +import java.awt.*; +import java.awt.geom.AffineTransform; +import java.awt.geom.Ellipse2D; +import java.awt.geom.Rectangle2D; +import java.awt.geom.RoundRectangle2D; +import java.util.ArrayList; + +/** + * Builds strokes in which every point on the stroked shape's line is stamped with a shape. + */ +@SuppressWarnings("unused") +public class ShapeStrokeBuilder { + + private final ArrayList shapes = new ArrayList<>(); + private int interpolatedInterval = 1; + + /** + * Use {@link StrokeBuilder#withShape()} to get an instance of this class + */ + ShapeStrokeBuilder() { + } + + /** + * Creates a circular stroke of a specified diameter. + * + * @param diameter The diameter of the circular stroke + * @return The builder + */ + public ShapeStrokeBuilder ofCircle(int diameter) { + shapes.add(new Ellipse2D.Double(0, 0, diameter, diameter)); + return this; + } + + /** + * Creates an oval stroke of a specified width and height. + * + * @param width The width of the oval + * @param height The height of the oval + * @return The builder + */ + public ShapeStrokeBuilder ofOval(int width, int height) { + shapes.add(new Ellipse2D.Float(0, 0, width, height)); + return this; + } + + /** + * Creates a regular polygon stroke. + * + * @param sides The number of sides of the polygon (i.e., 3 for a triangle, 8 for an octagon, etc.) + * @param sideLength The length, in pixels, of each side. + * @param rotationDegrees Degrees to rotate the orientation of the polygon + * @return The builder + */ + public ShapeStrokeBuilder ofRegularPolygon(int sides, int sideLength, double rotationDegrees) { + shapes.add(MathUtils.polygon(new Point(0, 0), sides, sideLength, Math.toRadians(rotationDegrees))); + return this; + } + + /** + * Creates a quadrilateral stroke. + * + * @param quadrilateral The quadrilateral + * @return The builder + */ + public ShapeStrokeBuilder ofQuadrilateral(Quadrilateral quadrilateral) { + shapes.add(quadrilateral.getShape()); + return this; + } + + /** + * Creates a rectangular stroke. + * + * @param width The width of the rectangle + * @param height The height of the rectangle + * @return The builder + */ + public ShapeStrokeBuilder ofRectangle(int width, int height) { + shapes.add(new Rectangle2D.Float(0, 0, width, height)); + return this; + } + + /** + * Creates a square stroke. + * + * @param size The length of each edge of the square, in pixels + * @return The builder + */ + public ShapeStrokeBuilder ofSquare(int size) { + shapes.add(new Rectangle2D.Float(0, 0, size, size)); + return this; + } + + /** + * Creates a round rectangle stroke. + * + * @param width The width of the round rectangle + * @param height The height of the round rectangle + * @param arc And height and width of the arc that rounds the rectangle + * @return The builder + */ + public ShapeStrokeBuilder ofRoundRectangle(int width, int height, int arc) { + shapes.add(new RoundRectangle2D.Float(0, 0, width, height, arc, arc)); + return this; + } + + /** + * Creates a horizontal line stroke. + * + * @param length The length of the line, in pixels + * @return The builder + */ + public ShapeStrokeBuilder ofHorizontalLine(int length) { + return ofLine(length, 2, 0); + } + + /** + * Creates a vertical line stroke. + * + * @param length The length of the line, in pixels + * @return The builder + */ + public ShapeStrokeBuilder ofVerticalLine(int length) { + return ofLine(length, 2, 90); + } + + /** + * Creates a linear stroke. + * + * @param length The length of the line + * @param width The width of the line + * @param rotationDegrees A rotation, in degrees, of the line. 0 degrees is horizontal; 90 degrees is vertical. + * @return The builder + */ + public ShapeStrokeBuilder ofLine(int length, int width, double rotationDegrees) { + shapes.add(AffineTransform.getRotateInstance(Math.toRadians(rotationDegrees)).createTransformedShape(new Rectangle2D.Float(0, 0, length, width))); + return this; + } + + /** + * Creates a stroke whose shape is a provided string (rendered in the system default font). + * + * @param text The text shape of the stroke + * @return The builder + */ + public ShapeStrokeBuilder ofText(String text) { + shapes.add(new JLabel().getFont().createGlyphVector(new JLabel().getFontMetrics(new JLabel().getFont()).getFontRenderContext(), text).getOutline()); + return this; + } + + /** + * Creates a stroke whose shape is a provided string, rendered in a provided font. + * + * @param text The text shape of the stroke + * @param font The font in which to render the text + * @return The builder + */ + public ShapeStrokeBuilder ofText(String text, Font font) { + shapes.add(font.createGlyphVector(new JLabel().getFontMetrics(font).getFontRenderContext(), text).getOutline()); + return this; + } + + /** + * Creates a stroke of an arbitrary shape. + * + * @param shape The shape of the stroke + * @return The builder + */ + public ShapeStrokeBuilder ofShape(Shape shape) { + shapes.add(shape); + return this; + } + + /** + * Rotates the last-specified stroke shape by a given amount. Throws an exception if invoked prior to specifying + * a shape. + * + * @param degrees Number of degrees to rotate the shape. + * @return The builder + */ + public ShapeStrokeBuilder rotated(double degrees) { + if (!shapes.isEmpty()) { + shapes.set(shapes.size() - 1, AffineTransform.getRotateInstance(Math.toRadians(degrees)).createTransformedShape(shapes.get(shapes.size() - 1))); + } else { + throw new IllegalStateException("Specify a shape before calling 'rotated'."); + } + return this; + } + + /** + * Shears (slants) the last-specified stroke shape. Throws an exception if invoked prior to specifying a shape. + * + * @param xShear The horizontal shear multiplier; 0 means no shear + * @param yShear The vertical shear multiplier; 0 means no shear + * @return The builder + */ + public ShapeStrokeBuilder sheared(double xShear, double yShear) { + if (!shapes.isEmpty()) { + shapes.set(shapes.size() - 1, AffineTransform.getShearInstance(xShear, yShear).createTransformedShape(shapes.get(shapes.size() - 1))); + } else { + throw new IllegalStateException("Specify a shape before calling 'sheared'."); + } + return this; + } + + /** + * Scales (enlarges or reduces) the last-specified stroke shape. Throws an exception if invoked prior to + * specifying a shape. + * + * @param xScale The horizontal scale multiplier (1.0 means no scale) + * @param yScale The vertical scale of the multiplier (1.0 means no scale) + * @return The builder + */ + public ShapeStrokeBuilder scaled(double xScale, double yScale) { + if (!shapes.isEmpty()) { + shapes.set(shapes.size() - 1, AffineTransform.getScaleInstance(xScale, yScale).createTransformedShape(shapes.get(shapes.size() - 1))); + } else { + throw new IllegalStateException("Specify a shape before calling 'scaled'."); + } + return this; + } + + /** + * Transforms the last specified stroke shape from a filled (solid) shape into an outlined shape. + * + * @param width The width, in pixels, of the outline + * @return The builder + */ + public ShapeStrokeBuilder outlined(float width) { + return stroked(new BasicStroke(width)); + } + + /** + * Transforms the last specified shape from a filled (solid) shape into a stroked shape that is stroked by the + * given stroke. That is, this method produces a stroke from another stroke. + *

+ * It is advised not to stroke a stroke with a {@link com.defano.jmonet.tools.brushes.StampStroke}, as doing + * so exponentially increases the drawing complexity of whatever shape is being stroked. + * + * @param stroke The stroke with which to stroke the current stroke. + * @return The builder. + */ + public ShapeStrokeBuilder stroked(Stroke stroke) { + if (!shapes.isEmpty()) { + shapes.set(shapes.size() - 1, stroke.createStrokedShape(shapes.get(shapes.size() - 1))); + } else { + throw new IllegalStateException("Specify a shape before calling 'stroked'."); + } + return this; + } + + /** + * When invoked, the stroke shape will be "stamped" only at each point where two paths join. Equivalent to invoking + * {@link #interpolatedInterval(int)} with a resolution of 0. + * + * @return The builder + */ + public ShapeStrokeBuilder withoutInterpolation() { + this.interpolatedInterval = 0; + return this; + } + + /** + * Specifies the interval at which each stroke will be stamped. A value of 1 indicates the shape will be stamped + * at each pixel along the drawn path. + * + * @param interval The number of pixels between each "stamp" interpolated between defined points in the drawn path. + * @return The builder + */ + public ShapeStrokeBuilder interpolatedInterval(int interval) { + this.interpolatedInterval = interval; + return this; + } + + /** + * Creates the stroke as specified. + * + * @return The built stroke + */ + public Stroke build() { + ShapeStroke brush = new ShapeStroke(shapes); + brush.setInterpolationInterval(interpolatedInterval); + return brush; + } +} diff --git a/src/main/java/com/defano/jmonet/tools/builder/StrokeBuilder.java b/src/main/java/com/defano/jmonet/tools/builder/StrokeBuilder.java index d0fe4e9b..154839a4 100644 --- a/src/main/java/com/defano/jmonet/tools/builder/StrokeBuilder.java +++ b/src/main/java/com/defano/jmonet/tools/builder/StrokeBuilder.java @@ -1,16 +1,8 @@ package com.defano.jmonet.tools.builder; -import com.defano.jmonet.model.Quadrilateral; import com.defano.jmonet.tools.brushes.ShapeStroke; -import com.defano.jmonet.tools.util.Geometry; -import javax.swing.*; import java.awt.*; -import java.awt.geom.AffineTransform; -import java.awt.geom.Ellipse2D; -import java.awt.geom.Rectangle2D; -import java.awt.geom.RoundRectangle2D; -import java.util.ArrayList; /** * A utility for building strokes, both {@link BasicStroke} and {@link ShapeStroke}. @@ -37,418 +29,4 @@ public static BasicStrokeBuilder withBasicStroke() { return new BasicStrokeBuilder(); } - /** - * Builds strokes in which every point on the stroked shape's line is stamped with a shape. - */ - public static class ShapeStrokeBuilder { - - private final ArrayList shapes = new ArrayList<>(); - private int interpolation = 1; - - /** - * Use {@link StrokeBuilder#withShape()} to get an instance of this class - */ - private ShapeStrokeBuilder() { - } - - /** - * Creates a circular stroke of a specified diameter. - * - * @param diameter The diameter of the circular stroke - * @return The builder - */ - public ShapeStrokeBuilder ofCircle(int diameter) { - shapes.add(new Ellipse2D.Double(0, 0, diameter, diameter)); - return this; - } - - /** - * Creates an oval stroke of a specified width and height. - * - * @param width The width of the oval - * @param height The height of the oval - * @return The builder - */ - public ShapeStrokeBuilder ofOval(int width, int height) { - shapes.add(new Ellipse2D.Float(0, 0, width, height)); - return this; - } - - /** - * Creates a regular polygon stroke. - * - * @param sides The number of sides of the polygon (i.e., 3 for a triangle, 8 for an octagon, etc.) - * @param sideLength The length, in pixels, of each side. - * @param rotationDegrees Degrees to rotate the orientation of the polygon - * @return The builder - */ - public ShapeStrokeBuilder ofRegularPolygon(int sides, int sideLength, double rotationDegrees) { - shapes.add(Geometry.polygon(new Point(0, 0), sides, sideLength, Math.toRadians(rotationDegrees))); - return this; - } - - /** - * Creates a quadrilateral stroke. - * - * @param quadrilateral The quadrilateral - * @return The builder - */ - public ShapeStrokeBuilder ofQuadrilateral(Quadrilateral quadrilateral) { - shapes.add(quadrilateral.getShape()); - return this; - } - - /** - * Creates a rectangular stroke. - * - * @param width The width of the rectangle - * @param height The height of the rectangle - * @return The builder - */ - public ShapeStrokeBuilder ofRectangle(int width, int height) { - shapes.add(new Rectangle2D.Float(0, 0, width, height)); - return this; - } - - /** - * Creates a square stroke. - * - * @param size The length of each edge of the square, in pixels - * @return The builder - */ - public ShapeStrokeBuilder ofSquare(int size) { - shapes.add(new Rectangle2D.Float(0, 0, size, size)); - return this; - } - - /** - * Creates a round rectangle stroke. - * - * @param width The width of the round rectangle - * @param height The height of the round rectangle - * @param arc And height and width of the arc that rounds the rectangle - * @return The builder - */ - public ShapeStrokeBuilder ofRoundRectangle(int width, int height, int arc) { - shapes.add(new RoundRectangle2D.Float(0, 0, width, height, arc, arc)); - return this; - } - - /** - * Creates a horizontal line stroke. - * - * @param length The length of the line, in pixels - * @return The builder - */ - public ShapeStrokeBuilder ofHorizontalLine(int length) { - return ofLine(length, 2, 0); - } - - /** - * Creates a vertical line stroke. - * - * @param length The length of the line, in pixels - * @return The builder - */ - public ShapeStrokeBuilder ofVerticalLine(int length) { - return ofLine(length, 2, 90); - } - - /** - * Creates a linear stroke. - * - * @param length The length of the line - * @param width The width of the line - * @param rotationDegrees A rotation, in degrees, of the line. 0 degrees is horizontal; 90 degrees is vertical. - * @return The builder - */ - public ShapeStrokeBuilder ofLine(int length, int width, double rotationDegrees) { - shapes.add(AffineTransform.getRotateInstance(Math.toRadians(rotationDegrees)).createTransformedShape(new Rectangle2D.Float(0, 0, length, width))); - return this; - } - - /** - * Creates a stroke whose shape is a provided string (rendered in the system default font). - * - * @param text The text shape of the stroke - * @return The builder - */ - public ShapeStrokeBuilder ofText(String text) { - shapes.add(new JLabel().getFont().createGlyphVector(new JLabel().getFontMetrics(new JLabel().getFont()).getFontRenderContext(), text).getOutline()); - return this; - } - - /** - * Creates a stroke whose shape is a provided string, rendered in a provided font. - * - * @param text The text shape of the stroke - * @param font The font in which to render the text - * @return The builder - */ - public ShapeStrokeBuilder ofText(String text, Font font) { - shapes.add(font.createGlyphVector(new JLabel().getFontMetrics(font).getFontRenderContext(), text).getOutline()); - return this; - } - - /** - * Creates a stroke of an arbitrary shape. - * - * @param shape The shape of the stroke - * @return The builder - */ - public ShapeStrokeBuilder ofShape(Shape shape) { - shapes.add(shape); - return this; - } - - /** - * Rotates the last-specified stroke shape by a given amount. Throws an exception if invoked prior to specifying - * a shape. - * - * @param degrees Number of degrees to rotate the shape. - * @return The builder - */ - public ShapeStrokeBuilder rotated(double degrees) { - if (!shapes.isEmpty()) { - shapes.set(shapes.size() - 1, AffineTransform.getRotateInstance(Math.toRadians(degrees)).createTransformedShape(shapes.get(shapes.size() - 1))); - } else { - throw new IllegalStateException("Specify a shape before calling 'rotated'."); - } - return this; - } - - /** - * Shears (slants) the last-specified stroke shape. Throws an exception if invoked prior to specifying a shape. - * - * @param xShear The horizontal shear multiplier; 0 means no shear - * @param yShear The vertical shear multiplier; 0 means no shear - * @return The builder - */ - public ShapeStrokeBuilder sheared(double xShear, double yShear) { - if (!shapes.isEmpty()) { - shapes.set(shapes.size() - 1, AffineTransform.getShearInstance(xShear, yShear).createTransformedShape(shapes.get(shapes.size() - 1))); - } else { - throw new IllegalStateException("Specify a shape before calling 'sheared'."); - } - return this; - } - - /** - * Scales (enlarges or reduces) the last-specified stroke shape. Throws an exception if invoked prior to - * specifying a shape. - * - * @param xScale The horizontal scale multiplier (1.0 means no scale) - * @param yScale The vertical scale of the multiplier (1.0 means no scale) - * @return The builder - */ - public ShapeStrokeBuilder scaled(double xScale, double yScale) { - if (!shapes.isEmpty()) { - shapes.set(shapes.size() - 1, AffineTransform.getScaleInstance(xScale, yScale).createTransformedShape(shapes.get(shapes.size() - 1))); - } else { - throw new IllegalStateException("Specify a shape before calling 'scaled'."); - } - return this; - } - - /** - * Transforms the last specified stroke shape from a filled (solid) shape into an outlined shape. - * - * @param width The width, in pixels, of the outline - * @return The builder - */ - public ShapeStrokeBuilder outlined(float width) { - return stroked(new BasicStroke(width)); - } - - /** - * Transforms the last specified shape from a filled (solid) shape into a stroked shape that is stroked by the - * given stroke. That is, this method produces a stroke from another stroke. - *

- * It is advised not to stroke a stroke with a {@link com.defano.jmonet.tools.brushes.StampStroke}, as doing - * so exponentially increases the drawing complexity of whatever shape is being stroked. - * - * @param stroke The stroke with which to stroke the current stroke. - * @return The builder. - */ - public ShapeStrokeBuilder stroked(Stroke stroke) { - if (!shapes.isEmpty()) { - shapes.set(shapes.size() - 1, stroke.createStrokedShape(shapes.get(shapes.size() - 1))); - } else { - throw new IllegalStateException("Specify a shape before calling 'stroked'."); - } - return this; - } - - /** - * When invoked, the stroke shape will be "stamped" only at each point where two paths join. - * - * @return The builder - */ - public ShapeStrokeBuilder withoutInterpolation() { - this.interpolation = 0; - return this; - } - - public ShapeStrokeBuilder interpolated(int resolution) { - this.interpolation = resolution; - return this; - } - - /** - * Creates the stroke as specified. - * - * @return The built stroke - */ - public Stroke build() { - ShapeStroke brush = new ShapeStroke(shapes); - brush.setInterpolationInterval(interpolation); - return brush; - } - } - - /** - * Builds {@link BasicStroke} objects. - */ - public static class BasicStrokeBuilder { - - private float width = 1; - private int cap = BasicStroke.CAP_ROUND; - private int join = BasicStroke.JOIN_ROUND; - private final ArrayList dash = new ArrayList<>(); - private float dashPhase = 0; - private float miterLimit = 1; - - /** - * Use {@link StrokeBuilder#withBasicStroke()} to get an instance of this class. - */ - private BasicStrokeBuilder() {} - - /** - * Specifies the width, in pixels, of the stroke. - * @param width The width - * @return The builder - */ - public BasicStrokeBuilder ofWidth(float width) { - this.width = width; - return this; - } - - /** - * Ends unclosed subpaths and dash segments with a round decoration that has a radius equal to half of the - * width of the pen. - * - * @return The builder - */ - public BasicStrokeBuilder withRoundCap() { - this.cap = BasicStroke.CAP_ROUND; - return this; - } - - /** - * Ends unclosed subpaths and dash segments with no added decoration. - * - * @return The builder - */ - public BasicStrokeBuilder withButtCap() { - this.cap = BasicStroke.CAP_BUTT; - return this; - } - - /** - * Ends unclosed subpaths and dash segments with a square projection that extends beyond the end of the segment - * to a distance equal to half of the line width. - * - * @return The builder - */ - public BasicStrokeBuilder withSquareCap() { - this.cap = BasicStroke.CAP_SQUARE; - return this; - } - - /** - * Joins path segments by rounding off the corner at a radius of half the line width. - * - * @return The builder - */ - public BasicStrokeBuilder withRoundJoin() { - this.join = BasicStroke.JOIN_ROUND; - return this; - } - - /** - * Joins path segments by extending their outside edges until they meet. - * - * @return The builder - */ - public BasicStrokeBuilder withMiterJoin() { - this.join = BasicStroke.JOIN_MITER; - return this; - } - - /** - * Joins path segments by connecting the outer corners of their wide outlines with a straight segment. - * - * @return The builder - */ - public BasicStrokeBuilder withBevelJoin() { - this.join = BasicStroke.JOIN_BEVEL; - return this; - } - - /** - * Adds a dash pattern element to the stroke. - * - * For example, specifying '5' as the argument to this method produces a dashed/dotted line where every 5 pixels - * are filled, and every 5 pixels are not. Invoking this method subsequent times appends values to the dash - * pattern. Calling a second time with '2' produces a line where every 5 pixels are filled followed by 2 that - * are not. - * - * @param dashLength The length of the dash pattern element to append - * @return The Builder - */ - public BasicStrokeBuilder withDash(float dashLength) { - this.dash.add(dashLength); - return this; - } - - /** - * The limit to trim the miter join; must be greater than or equal to 1.0f. - * - * @param miterLimit The miter limit - * @return The builder - */ - public BasicStrokeBuilder withMiterLimit(float miterLimit) { - this.miterLimit = miterLimit; - return this; - } - - /** - * The offset at which to start the dashing pattern. - * - * @param dashPhase The dash phase offset - * @return The builder - */ - public BasicStrokeBuilder withDashPhase(float dashPhase) { - this.dashPhase = dashPhase; - return this; - } - - /** - * Creates the {@link BasicStroke} as specified. - * - * @return The stroke - */ - public BasicStroke build() { - if (dash.isEmpty()) { - return new BasicStroke(width, cap, join, miterLimit); - } else { - float[] dashArray = new float[dash.size()]; - for (int index = 0; index < dash.size(); index++) { - dashArray[index] = dash.get(index); - } - return new BasicStroke(width, cap, join, miterLimit, dashArray, dashPhase); - } - } - } - } diff --git a/src/main/java/com/defano/jmonet/tools/util/CursorFactory.java b/src/main/java/com/defano/jmonet/tools/cursors/CursorFactory.java similarity index 91% rename from src/main/java/com/defano/jmonet/tools/util/CursorFactory.java rename to src/main/java/com/defano/jmonet/tools/cursors/CursorFactory.java index 598c29c9..5edfc963 100644 --- a/src/main/java/com/defano/jmonet/tools/util/CursorFactory.java +++ b/src/main/java/com/defano/jmonet/tools/cursors/CursorFactory.java @@ -1,13 +1,13 @@ -package com.defano.jmonet.tools.util; +package com.defano.jmonet.tools.cursors; -import com.defano.jmonet.algo.transform.image.ScaleTransform; +import com.defano.jmonet.transform.image.ScaleTransform; import java.awt.*; import java.awt.geom.Line2D; import java.awt.image.BufferedImage; /** - * A utility for creating custom tool cursors. + * A utility for creating custom JMonet tool cursors. */ public class CursorFactory { @@ -102,9 +102,12 @@ public static Cursor makeBrushCursor(Stroke stroke, Paint fill, double scale) { g.drawLine(strokedShape.getBounds().width / 2, strokedShape.getBounds().height / 2, strokedShape.getBounds().width / 2, strokedShape.getBounds().height / 2); g.dispose(); - BufferedImage scaledCursor = new ScaleTransform(new Dimension((int)(cursorImage.getWidth() * scale), (int)(cursorImage.getHeight() * scale))).apply(cursorImage); + BufferedImage scaledCursor = new ScaleTransform(new Dimension( + Math.max(1, (int)(cursorImage.getWidth() * scale)), + Math.max(1, (int)(cursorImage.getHeight() * scale))) + ).apply(cursorImage); - Point hotspot = new Point(scaledCursor.getWidth() / 2, scaledCursor.getHeight() / 2 - 1); + Point hotspot = new Point(scaledCursor.getWidth() / 2, scaledCursor.getHeight() / 2); return toolkit.createCustomCursor(scaledCursor, hotspot, stroke.toString()); } diff --git a/src/main/java/com/defano/jmonet/tools/cursors/CursorManager.java b/src/main/java/com/defano/jmonet/tools/cursors/CursorManager.java new file mode 100644 index 00000000..45d3b72d --- /dev/null +++ b/src/main/java/com/defano/jmonet/tools/cursors/CursorManager.java @@ -0,0 +1,29 @@ +package com.defano.jmonet.tools.cursors; + +import com.defano.jmonet.canvas.PaintCanvas; + +import java.awt.*; + +/** + * A class which has the ability to get and set the system's mouse cursor when the mouse is hovering over a specified + * {@link PaintCanvas}. + */ +public interface CursorManager { + + /** + * Gets the default mouse cursor used when painting with this tool. Note that specific tools may provide methods + * for getting and setting auxiliary cursors that are active during tool-specific states, too. + * + * @return The default tool cursor. + */ + Cursor getToolCursor(); + + /** + * Sets the default mouse cursor used when painting with this tool. + * + * @param toolCursor The default mouse cursor. + * @param canvas The canvas on which the cursor should be active. + */ + void setToolCursor(Cursor toolCursor, PaintCanvas canvas); + +} diff --git a/src/main/java/com/defano/jmonet/tools/cursors/SwingCursorManager.java b/src/main/java/com/defano/jmonet/tools/cursors/SwingCursorManager.java new file mode 100644 index 00000000..63588890 --- /dev/null +++ b/src/main/java/com/defano/jmonet/tools/cursors/SwingCursorManager.java @@ -0,0 +1,30 @@ +package com.defano.jmonet.tools.cursors; + +import com.defano.jmonet.canvas.PaintCanvas; + +import javax.swing.*; +import java.awt.*; + +/** + * A CursorManager that sets a canvas cursor on the Swing dispatch thread. + */ +public class SwingCursorManager implements CursorManager { + + private Cursor toolCursor; + + /** {@inheritDoc} */ + @Override + public Cursor getToolCursor() { + return toolCursor; + } + + /** {@inheritDoc} */ + @Override + public void setToolCursor(Cursor toolCursor, PaintCanvas canvas) { + this.toolCursor = toolCursor; + if (canvas != null) { + SwingUtilities.invokeLater(() -> canvas.setCursor(toolCursor)); + } + } + +} diff --git a/src/main/java/com/defano/jmonet/tools/selection/MutableSelection.java b/src/main/java/com/defano/jmonet/tools/selection/MutableSelection.java index aacf849b..ab30fc28 100644 --- a/src/main/java/com/defano/jmonet/tools/selection/MutableSelection.java +++ b/src/main/java/com/defano/jmonet/tools/selection/MutableSelection.java @@ -4,7 +4,7 @@ import java.awt.image.BufferedImage; /** - * Represents the state or context of an image selection that can be modified. + * A class which manages the state or context of a modifiable image selection. */ public interface MutableSelection extends Selection { @@ -22,7 +22,7 @@ public interface MutableSelection extends Selection { * * @param bounds The new selection bounds. */ - void setSelectionOutline(Rectangle bounds); + void setSelectionOutline(Shape bounds); /** * Marks the selection as having been mutated (either by transformation or movement). diff --git a/src/main/java/com/defano/jmonet/tools/selection/Selection.java b/src/main/java/com/defano/jmonet/tools/selection/Selection.java index 49079564..1312ffce 100644 --- a/src/main/java/com/defano/jmonet/tools/selection/Selection.java +++ b/src/main/java/com/defano/jmonet/tools/selection/Selection.java @@ -7,7 +7,7 @@ import java.awt.image.BufferedImage; /** - * Represents the state or context of an image selection made by the selection or lasso tools. + * A class that manages the state or context of an image selection made by the selection or lasso tools. */ public interface Selection { @@ -79,13 +79,13 @@ default Shape getIdentitySelectionFrame() { void redrawSelection(boolean includeFrame); /** - * Creates a new image in which every pixel not within the selection frame (i.e., bounded by marching ants) has been - * changed to fully transparent; the image produced is the same dimensions as the source image. + * Creates a new image in which every pixel of the input that is not within the selection frame (i.e., bounded by + * marching ants) has been changed to fully transparent; the image produced is the same dimensions as the source image. * * @param image The image to crop * @return A BufferedImage in which every pixel not within the selection has been made transparent */ - default BufferedImage crop(BufferedImage image) { + default BufferedImage getSelectionCroppedCopy(BufferedImage image) { Shape mask = getSelectionFrame(); BufferedImage maskedImage = new BufferedImage(image.getWidth(), image.getHeight(), BufferedImage.TYPE_INT_ARGB); diff --git a/src/main/java/com/defano/jmonet/tools/selection/TransformableCanvasSelection.java b/src/main/java/com/defano/jmonet/tools/selection/TransformableCanvasSelection.java index 0a9b2476..72a603da 100644 --- a/src/main/java/com/defano/jmonet/tools/selection/TransformableCanvasSelection.java +++ b/src/main/java/com/defano/jmonet/tools/selection/TransformableCanvasSelection.java @@ -4,7 +4,7 @@ import java.awt.image.BufferedImage; /** - * Represents a selection that can be transformed in a way that modifies the committed canvas image. + * A selection that can be transformed in a way that modifies the underlying committed canvas image. */ public interface TransformableCanvasSelection extends MutableSelection { @@ -34,7 +34,7 @@ default void pickupSelection() { redrawSelection(false); // Grab pixels from scratch and canvas that are bounded by the selection - BufferedImage maskedSelection = crop(getCanvas().render()); + BufferedImage maskedSelection = getSelectionCroppedCopy(getCanvas().render()); // Resize to smallest bounds for performance Shape selectionBounds = getSelectionFrame(); diff --git a/src/main/java/com/defano/jmonet/tools/selection/TransformableImageSelection.java b/src/main/java/com/defano/jmonet/tools/selection/TransformableImageSelection.java index ba052953..d2d41134 100644 --- a/src/main/java/com/defano/jmonet/tools/selection/TransformableImageSelection.java +++ b/src/main/java/com/defano/jmonet/tools/selection/TransformableImageSelection.java @@ -1,19 +1,18 @@ package com.defano.jmonet.tools.selection; -import com.defano.jmonet.algo.fill.FillFunction; -import com.defano.jmonet.algo.transform.image.PixelTransform; -import com.defano.jmonet.algo.transform.image.StaticImageTransform; -import com.defano.jmonet.algo.transform.image.Transformable; -import com.defano.jmonet.algo.transform.image.ApplyPixelTransform; -import com.defano.jmonet.algo.transform.image.FillTransform; +import com.defano.jmonet.tools.attributes.FillFunction; +import com.defano.jmonet.transform.image.PixelTransform; +import com.defano.jmonet.transform.image.StaticImageTransform; +import com.defano.jmonet.transform.image.Transformable; +import com.defano.jmonet.transform.image.ApplyPixelTransform; +import com.defano.jmonet.transform.image.FillTransform; import java.awt.*; /** - * Represents a selection in which the pixels of the selected image can be transformed (i.e., change of brightness, - * opacity, etc.). + * A selection in which the pixels of the selected image can be transformed (i.e., change of brightness, opacity, etc.). *

- * Differs from TransformableSelection in that these transforms do no change the selection shape (outline) or + * Differs from {@link TransformableSelection} in that these transforms do no change the selection shape (outline) or * location on the canvas; only the underlying selected image. */ public interface TransformableImageSelection extends MutableSelection, Transformable { diff --git a/src/main/java/com/defano/jmonet/tools/selection/TransformableSelection.java b/src/main/java/com/defano/jmonet/tools/selection/TransformableSelection.java index 436fbd8f..b1f74fba 100644 --- a/src/main/java/com/defano/jmonet/tools/selection/TransformableSelection.java +++ b/src/main/java/com/defano/jmonet/tools/selection/TransformableSelection.java @@ -1,17 +1,17 @@ package com.defano.jmonet.tools.selection; -import com.defano.jmonet.algo.transform.affine.FlipHorizontalTransform; -import com.defano.jmonet.algo.transform.affine.FlipVerticalTransform; -import com.defano.jmonet.algo.transform.affine.RotateLeftTransform; -import com.defano.jmonet.algo.transform.affine.RotateRightTransform; -import com.defano.jmonet.algo.transform.image.ApplyAffineTransform; +import com.defano.jmonet.transform.affine.FlipHorizontalTransform; +import com.defano.jmonet.transform.affine.FlipVerticalTransform; +import com.defano.jmonet.transform.affine.RotateLeftTransform; +import com.defano.jmonet.transform.affine.RotateRightTransform; +import com.defano.jmonet.transform.image.ApplyAffineTransform; import java.awt.*; import java.awt.geom.AffineTransform; /** - * Represents a selection that can be transformed using operations that affect both the pixels of the selected image - * (i.e., change of brightness or opacity) and/or the selection's shape and location (i.e., flip, rotate or translate). + * A selection that can be transformed using operations that affect both the pixels of the selected image (i.e., change + * of brightness or opacity) and/or the selection's shape and location (i.e., flip, rotate or translate). */ public interface TransformableSelection extends TransformableImageSelection { diff --git a/src/main/java/com/defano/jmonet/tools/util/ImageUtils.java b/src/main/java/com/defano/jmonet/tools/util/ImageUtils.java index 23d6e1d9..fd786b54 100644 --- a/src/main/java/com/defano/jmonet/tools/util/ImageUtils.java +++ b/src/main/java/com/defano/jmonet/tools/util/ImageUtils.java @@ -8,6 +8,11 @@ */ public class ImageUtils { + /** + * Library of static methods; cannot be instantiated. + */ + private ImageUtils() {} + /** * Makes a "deep" copy of the given image, returning a copy whose type is TYPE_INT_ARGB. * diff --git a/src/main/java/com/defano/jmonet/tools/util/MarchingAnts.java b/src/main/java/com/defano/jmonet/tools/util/MarchingAnts.java index 0fb7c766..7b9b2a5b 100644 --- a/src/main/java/com/defano/jmonet/tools/util/MarchingAnts.java +++ b/src/main/java/com/defano/jmonet/tools/util/MarchingAnts.java @@ -10,20 +10,27 @@ import java.util.concurrent.TimeUnit; /** - * A utility class for animating a dashed border stroke ("marching ants") commonly found in selection tools. + * A class for animating a dashed border stroke ("marching ants") commonly found in selection tools. + * + * This singleton class manages the ant animation by instantiating a single-threaded scheduled executor. Multiple + * "ants" paths can be drawn with this singleton by registering multiple listeners. */ public class MarchingAnts { - private static final int ANIMATION_PERIOD_MS = 50; private static final MarchingAnts instance = new MarchingAnts(); private static final ScheduledExecutorService antsAnimator = Executors.newSingleThreadScheduledExecutor(); private static final Set observers = new HashSet<>(); + private int animationPeriodMs = 50; // animation period + private int antLength = 5; // ant dash length, in pixels + private int antWidth = 1; // width of ant dash, in pixels + private Color antColor = Color.DARK_GRAY; + private Color pathColor = Color.WHITE; + private int antsPhase; private Future antsAnimation; - private MarchingAnts() { - } + private MarchingAnts() {} public static MarchingAnts getInstance() { return instance; @@ -35,7 +42,14 @@ public static MarchingAnts getInstance() { * @return The marching ants paint stroke. */ public Stroke getMarchingAnts() { - return new BasicStroke(1.0f, BasicStroke.CAP_BUTT, BasicStroke.JOIN_MITER, 5.0f, new float[]{5.0f}, antsPhase); + return new BasicStroke( + antWidth, + BasicStroke.CAP_BUTT, + BasicStroke.JOIN_MITER, + antLength, + new float[]{antLength}, + antsPhase + ); } /** @@ -59,13 +73,114 @@ public void removeObserver(MarchingAntsObserver observer) { } } + /** + * Gets the period of the ants animation, in milliseconds. That is, the number of milliseconds delayed before + * redrawing the position of the ants. Lower values means faster moving ants. + * + * @return The period of the ants animation, in milliseconds. + */ + @SuppressWarnings("unused") + public int getAnimationPeriodMs() { + return animationPeriodMs; + } + + /** + * Sets the period of the ants animation, in milliseconds. That is, the number of milliseconds delayed before + * redrawing the position of the ants. Lower values means faster moving ants. + * + * @param animationPeriodMs The period of the ants animation, in milliseconds. + */ + @SuppressWarnings("unused") + public void setAnimationPeriodMs(int animationPeriodMs) { + this.animationPeriodMs = animationPeriodMs; + } + + /** + * Gets the length of each ant, in pixels. Note that the length of the ant always is the same as the space between + * ants. + * + * @return The length of each ant, in pixels. + */ + @SuppressWarnings("unused") + public int getAntLength() { + return antLength; + } + + /** + * Sets the length of each ant, in pixels. Note that the length of the ant always is the same as the space between + * ants. + * @param antLength The length of each ant, in pixels. + */ + @SuppressWarnings("unused") + public void setAntLength(int antLength) { + this.antLength = antLength; + } + + /** + * Gets the width of each ant, in pixels. Note that when stroking a selection outline with marching ants, the + * ants are centered on the selected shape. Therefore, wide ants will partially obscure the selection being made. + * + * @return The width of each ant, in pixels. + */ + @SuppressWarnings("unused") + public int getAntWidth() { + return antWidth; + } + + /** + * Sets the width of each ant, in pixels. Note that when stroking a selection outline with marching ants, the + * ants are centered on the selected shape. Therefore, wide ants will partially obscure the selection being made. + * + * @param antWidth The width of each ant, in pixels. + */ + @SuppressWarnings("unused") + public void setAntWidth(int antWidth) { + this.antWidth = antWidth; + } + + /** + * Returns the color of each ant. + * @return The ant color + */ + @SuppressWarnings("unused") + public Color getAntColor() { + return antColor; + } + + /** + * Sets the color of each ant. + * @param antColor The ant color + */ + @SuppressWarnings("unused") + public void setAntColor(Color antColor) { + this.antColor = antColor; + } + + /** + * Gets the color of space between ants (the ant path). + * @return The ant path color. + */ + @SuppressWarnings("unused") + public Color getPathColor() { + return pathColor; + } + + /** + * Sets the color of the space between ants. + * @param pathColor The ant path color + */ + @SuppressWarnings("unused") + public void setPathColor(Color pathColor) { + this.pathColor = pathColor; + } + private void startMarching() { stopMarching(); antsAnimation = antsAnimator.scheduleAtFixedRate(() -> SwingUtilities.invokeLater(() -> { - antsPhase = antsPhase + 1 % 5; + antsPhase = antsPhase + 1 % antLength; fireMarchingAntsObservers(); - }), 0, ANIMATION_PERIOD_MS, TimeUnit.MILLISECONDS); + }), 0, animationPeriodMs, TimeUnit.MILLISECONDS); } private void stopMarching() { diff --git a/src/main/java/com/defano/jmonet/tools/util/MarchingAntsObserver.java b/src/main/java/com/defano/jmonet/tools/util/MarchingAntsObserver.java index cc7fd046..c6e7851c 100644 --- a/src/main/java/com/defano/jmonet/tools/util/MarchingAntsObserver.java +++ b/src/main/java/com/defano/jmonet/tools/util/MarchingAntsObserver.java @@ -10,7 +10,7 @@ public interface MarchingAntsObserver { * Called to indicate that ants have "marched" (the dotted line stroke phase has changed) and that observers should * re-paint using the stroke provided. * - * @param newAntsStroke The new paint stroke to re-draw. + * @param ants The new paint stroke to re-draw. */ - void onAntsMoved(Stroke newAntsStroke); + void onAntsMoved(Stroke ants); } diff --git a/src/main/java/com/defano/jmonet/tools/util/Geometry.java b/src/main/java/com/defano/jmonet/tools/util/MathUtils.java similarity index 92% rename from src/main/java/com/defano/jmonet/tools/util/Geometry.java rename to src/main/java/com/defano/jmonet/tools/util/MathUtils.java index c8e478aa..5559bd30 100644 --- a/src/main/java/com/defano/jmonet/tools/util/Geometry.java +++ b/src/main/java/com/defano/jmonet/tools/util/MathUtils.java @@ -7,9 +7,14 @@ import java.util.List; /** - * A utility class with geometric and trigonometric routines used by various tools. + * A utility class of geometric and trigonometric routines used by various tools. */ -public class Geometry { +public class MathUtils { + + /** + * Library of static methods; cannot be instantiated. + */ + private MathUtils() {} /** * Rounds a double value to the nearest provided integer multiple. For example rounding 24.3 to the nearest 10 @@ -19,28 +24,26 @@ public class Geometry { * @param toNearest The nearest integer multiple. * @return The nearest integer multiple */ - public static int round(Double value, int toNearest) { + public static int nearestRound(double value, int toNearest) { return (int) (toNearest * Math.round(value / toNearest)); } /** - * Rounds an integer value to the nearest provided integer multiple. For example rounding 24 to the nearest 10 - * yields 20. + * Rounds a value down to the nearest integer multiple. For example the floor of 24 to the nearest 10 + * yields 20; similarly flooring 29 to the nearest 10 also returns 20. * * @param value The value to round * @param toNearest The nearest integer multiple; if 0, no rounding occurs * @return The nearest integer multiple */ - @SuppressWarnings("IntegerDivisionInFloatingPointContext") - public static int round(int value, int toNearest) { + public static int nearestFloor(int value, int toNearest) { if (toNearest == 0) { toNearest = 1; } - return toNearest * Math.round(value / toNearest); + return toNearest * (value / toNearest); } - /** * Calculates the length of the line represented by two points. * @@ -63,7 +66,7 @@ public static List linearInterpolation(Point p1, Point p2, int interval) ArrayList interpolatedPoints = new ArrayList<>(); - double length = Geometry.length(p1, p2); + double length = MathUtils.length(p1, p2); double xStep = (p2.x - p1.x) / length; double yStep = (p2.y - p1.y) / length; @@ -124,7 +127,7 @@ public static Point2D line(Point2D origin, double length, double angle) { */ public static Point line(Point p1, Point p2, int toNearestAngle) { double length = distance(p1, p2); - double nearestAngle = round(Geometry.angle(p1.x, p1.y, p2.x, p2.y), toNearestAngle); + double nearestAngle = nearestRound(MathUtils.angle(p1.x, p1.y, p2.x, p2.y), toNearestAngle); return asPoint(line(p1, length, nearestAngle)); } @@ -149,8 +152,8 @@ public static double distance(Point2D p1, Point2D p2) { */ public static double theta(Point origin, Point p1, Point p2) { - double angle1 = Math.atan2(p1.y - origin.y, p1.x - origin.x); - double angle2 = Math.atan2(p2.y - origin.y, p2.x - origin.x); + double angle1 = Math.atan2(p1.getY() - origin.getY(), p1.getX() - origin.getX()); + double angle2 = Math.atan2(p2.getY() - origin.getY(), p2.getX() - origin.getX()); return angle1 - angle2; } @@ -299,8 +302,8 @@ public static Polygon polygon(Point location, int sides, double length, double r * @return A point that appears on a line parallel to angle and which is the same distance from anchor as reference. */ public static Point extrapolate(Line2D angle, Point anchor, Point reference) { - double degrees = Geometry.angle(angle); - return Geometry.asPoint(Geometry.line(anchor, Geometry.distance(anchor, reference), degrees)); + double degrees = MathUtils.angle(angle); + return MathUtils.asPoint(MathUtils.line(anchor, MathUtils.distance(anchor, reference), degrees)); } /** diff --git a/src/main/java/com/defano/jmonet/algo/transform/affine/FlipHorizontalTransform.java b/src/main/java/com/defano/jmonet/transform/affine/FlipHorizontalTransform.java similarity index 90% rename from src/main/java/com/defano/jmonet/algo/transform/affine/FlipHorizontalTransform.java rename to src/main/java/com/defano/jmonet/transform/affine/FlipHorizontalTransform.java index fa533a5f..6fc6c396 100644 --- a/src/main/java/com/defano/jmonet/algo/transform/affine/FlipHorizontalTransform.java +++ b/src/main/java/com/defano/jmonet/transform/affine/FlipHorizontalTransform.java @@ -1,4 +1,4 @@ -package com.defano.jmonet.algo.transform.affine; +package com.defano.jmonet.transform.affine; import java.awt.geom.AffineTransform; diff --git a/src/main/java/com/defano/jmonet/algo/transform/affine/FlipVerticalTransform.java b/src/main/java/com/defano/jmonet/transform/affine/FlipVerticalTransform.java similarity index 90% rename from src/main/java/com/defano/jmonet/algo/transform/affine/FlipVerticalTransform.java rename to src/main/java/com/defano/jmonet/transform/affine/FlipVerticalTransform.java index e621856b..5f8d03b4 100644 --- a/src/main/java/com/defano/jmonet/algo/transform/affine/FlipVerticalTransform.java +++ b/src/main/java/com/defano/jmonet/transform/affine/FlipVerticalTransform.java @@ -1,4 +1,4 @@ -package com.defano.jmonet.algo.transform.affine; +package com.defano.jmonet.transform.affine; import java.awt.geom.AffineTransform; diff --git a/src/main/java/com/defano/jmonet/algo/transform/affine/RotateLeftTransform.java b/src/main/java/com/defano/jmonet/transform/affine/RotateLeftTransform.java similarity index 92% rename from src/main/java/com/defano/jmonet/algo/transform/affine/RotateLeftTransform.java rename to src/main/java/com/defano/jmonet/transform/affine/RotateLeftTransform.java index c87162b8..5c75b349 100644 --- a/src/main/java/com/defano/jmonet/algo/transform/affine/RotateLeftTransform.java +++ b/src/main/java/com/defano/jmonet/transform/affine/RotateLeftTransform.java @@ -1,4 +1,4 @@ -package com.defano.jmonet.algo.transform.affine; +package com.defano.jmonet.transform.affine; import java.awt.geom.AffineTransform; diff --git a/src/main/java/com/defano/jmonet/algo/transform/affine/RotateRightTransform.java b/src/main/java/com/defano/jmonet/transform/affine/RotateRightTransform.java similarity index 92% rename from src/main/java/com/defano/jmonet/algo/transform/affine/RotateRightTransform.java rename to src/main/java/com/defano/jmonet/transform/affine/RotateRightTransform.java index aa2b49b6..b7e6af1c 100644 --- a/src/main/java/com/defano/jmonet/algo/transform/affine/RotateRightTransform.java +++ b/src/main/java/com/defano/jmonet/transform/affine/RotateRightTransform.java @@ -1,4 +1,4 @@ -package com.defano.jmonet.algo.transform.affine; +package com.defano.jmonet.transform.affine; import java.awt.geom.AffineTransform; diff --git a/src/main/java/com/defano/jmonet/algo/dither/AbstractDitherer.java b/src/main/java/com/defano/jmonet/transform/dither/AbstractDitherer.java similarity index 98% rename from src/main/java/com/defano/jmonet/algo/dither/AbstractDitherer.java rename to src/main/java/com/defano/jmonet/transform/dither/AbstractDitherer.java index e312224e..e13d34ec 100644 --- a/src/main/java/com/defano/jmonet/algo/dither/AbstractDitherer.java +++ b/src/main/java/com/defano/jmonet/transform/dither/AbstractDitherer.java @@ -1,6 +1,6 @@ -package com.defano.jmonet.algo.dither; +package com.defano.jmonet.transform.dither; -import com.defano.jmonet.algo.dither.quant.QuantizationFunction; +import com.defano.jmonet.transform.dither.quant.QuantizationFunction; import com.defano.jmonet.tools.util.ImageUtils; import java.awt.image.BufferedImage; diff --git a/src/main/java/com/defano/jmonet/algo/dither/AtkinsonDitherer.java b/src/main/java/com/defano/jmonet/transform/dither/AtkinsonDitherer.java similarity index 94% rename from src/main/java/com/defano/jmonet/algo/dither/AtkinsonDitherer.java rename to src/main/java/com/defano/jmonet/transform/dither/AtkinsonDitherer.java index d09b518d..7b661c37 100644 --- a/src/main/java/com/defano/jmonet/algo/dither/AtkinsonDitherer.java +++ b/src/main/java/com/defano/jmonet/transform/dither/AtkinsonDitherer.java @@ -1,4 +1,4 @@ -package com.defano.jmonet.algo.dither; +package com.defano.jmonet.transform.dither; /** * An implementation of Bill Atkinson's dithering algorithm. diff --git a/src/main/java/com/defano/jmonet/algo/dither/BurkesDitherer.java b/src/main/java/com/defano/jmonet/transform/dither/BurkesDitherer.java similarity index 94% rename from src/main/java/com/defano/jmonet/algo/dither/BurkesDitherer.java rename to src/main/java/com/defano/jmonet/transform/dither/BurkesDitherer.java index dd409b52..75b1aa41 100644 --- a/src/main/java/com/defano/jmonet/algo/dither/BurkesDitherer.java +++ b/src/main/java/com/defano/jmonet/transform/dither/BurkesDitherer.java @@ -1,4 +1,4 @@ -package com.defano.jmonet.algo.dither; +package com.defano.jmonet.transform.dither; /** * An implementation of the Burkes dithering algorithm. diff --git a/src/main/java/com/defano/jmonet/algo/dither/Ditherer.java b/src/main/java/com/defano/jmonet/transform/dither/Ditherer.java similarity index 86% rename from src/main/java/com/defano/jmonet/algo/dither/Ditherer.java rename to src/main/java/com/defano/jmonet/transform/dither/Ditherer.java index 688a383a..6d7c8131 100644 --- a/src/main/java/com/defano/jmonet/algo/dither/Ditherer.java +++ b/src/main/java/com/defano/jmonet/transform/dither/Ditherer.java @@ -1,6 +1,6 @@ -package com.defano.jmonet.algo.dither; +package com.defano.jmonet.transform.dither; -import com.defano.jmonet.algo.dither.quant.QuantizationFunction; +import com.defano.jmonet.transform.dither.quant.QuantizationFunction; import java.awt.image.BufferedImage; diff --git a/src/main/java/com/defano/jmonet/algo/dither/FloydSteinbergDitherer.java b/src/main/java/com/defano/jmonet/transform/dither/FloydSteinbergDitherer.java similarity index 93% rename from src/main/java/com/defano/jmonet/algo/dither/FloydSteinbergDitherer.java rename to src/main/java/com/defano/jmonet/transform/dither/FloydSteinbergDitherer.java index b7eb4be4..ae4067d5 100644 --- a/src/main/java/com/defano/jmonet/algo/dither/FloydSteinbergDitherer.java +++ b/src/main/java/com/defano/jmonet/transform/dither/FloydSteinbergDitherer.java @@ -1,4 +1,4 @@ -package com.defano.jmonet.algo.dither; +package com.defano.jmonet.transform.dither; /** * An implementation of the Floyd-Steinberg dithering algorithm. diff --git a/src/main/java/com/defano/jmonet/algo/dither/JarvisJudiceNinkeDitherer.java b/src/main/java/com/defano/jmonet/transform/dither/JarvisJudiceNinkeDitherer.java similarity index 96% rename from src/main/java/com/defano/jmonet/algo/dither/JarvisJudiceNinkeDitherer.java rename to src/main/java/com/defano/jmonet/transform/dither/JarvisJudiceNinkeDitherer.java index 47f1385e..3a014544 100644 --- a/src/main/java/com/defano/jmonet/algo/dither/JarvisJudiceNinkeDitherer.java +++ b/src/main/java/com/defano/jmonet/transform/dither/JarvisJudiceNinkeDitherer.java @@ -1,4 +1,4 @@ -package com.defano.jmonet.algo.dither; +package com.defano.jmonet.transform.dither; /** * An implementation of the Jarvis-Judice-Ninke dithering algorithm. diff --git a/src/main/java/com/defano/jmonet/algo/dither/NullDitherer.java b/src/main/java/com/defano/jmonet/transform/dither/NullDitherer.java similarity index 86% rename from src/main/java/com/defano/jmonet/algo/dither/NullDitherer.java rename to src/main/java/com/defano/jmonet/transform/dither/NullDitherer.java index c4c94777..2b2ffdf1 100644 --- a/src/main/java/com/defano/jmonet/algo/dither/NullDitherer.java +++ b/src/main/java/com/defano/jmonet/transform/dither/NullDitherer.java @@ -1,4 +1,4 @@ -package com.defano.jmonet.algo.dither; +package com.defano.jmonet.transform.dither; /** * A no-op dithering algorithm; provides no dithering whatsoever. diff --git a/src/main/java/com/defano/jmonet/algo/dither/SierraDitherer.java b/src/main/java/com/defano/jmonet/transform/dither/SierraDitherer.java similarity index 95% rename from src/main/java/com/defano/jmonet/algo/dither/SierraDitherer.java rename to src/main/java/com/defano/jmonet/transform/dither/SierraDitherer.java index b4f28875..4403c065 100644 --- a/src/main/java/com/defano/jmonet/algo/dither/SierraDitherer.java +++ b/src/main/java/com/defano/jmonet/transform/dither/SierraDitherer.java @@ -1,4 +1,4 @@ -package com.defano.jmonet.algo.dither; +package com.defano.jmonet.transform.dither; /** * An implementation of the Sierra-3 dithering algorithm. diff --git a/src/main/java/com/defano/jmonet/algo/dither/SierraLiteDitherer.java b/src/main/java/com/defano/jmonet/transform/dither/SierraLiteDitherer.java similarity index 92% rename from src/main/java/com/defano/jmonet/algo/dither/SierraLiteDitherer.java rename to src/main/java/com/defano/jmonet/transform/dither/SierraLiteDitherer.java index 7f3cf094..aaaef549 100644 --- a/src/main/java/com/defano/jmonet/algo/dither/SierraLiteDitherer.java +++ b/src/main/java/com/defano/jmonet/transform/dither/SierraLiteDitherer.java @@ -1,4 +1,4 @@ -package com.defano.jmonet.algo.dither; +package com.defano.jmonet.transform.dither; /** * An implementation of the Sierra-Lite dithering algorithm. diff --git a/src/main/java/com/defano/jmonet/algo/dither/SierraTwoDitherer.java b/src/main/java/com/defano/jmonet/transform/dither/SierraTwoDitherer.java similarity index 94% rename from src/main/java/com/defano/jmonet/algo/dither/SierraTwoDitherer.java rename to src/main/java/com/defano/jmonet/transform/dither/SierraTwoDitherer.java index 75cb4fd2..4e1d74f8 100644 --- a/src/main/java/com/defano/jmonet/algo/dither/SierraTwoDitherer.java +++ b/src/main/java/com/defano/jmonet/transform/dither/SierraTwoDitherer.java @@ -1,4 +1,4 @@ -package com.defano.jmonet.algo.dither; +package com.defano.jmonet.transform.dither; /** * An implementation of the Sierra-Two (row) dithering algorithm. diff --git a/src/main/java/com/defano/jmonet/algo/dither/StuckiDitherer.java b/src/main/java/com/defano/jmonet/transform/dither/StuckiDitherer.java similarity index 96% rename from src/main/java/com/defano/jmonet/algo/dither/StuckiDitherer.java rename to src/main/java/com/defano/jmonet/transform/dither/StuckiDitherer.java index a4f44bdf..1cbbb089 100644 --- a/src/main/java/com/defano/jmonet/algo/dither/StuckiDitherer.java +++ b/src/main/java/com/defano/jmonet/transform/dither/StuckiDitherer.java @@ -1,4 +1,4 @@ -package com.defano.jmonet.algo.dither; +package com.defano.jmonet.transform.dither; /** * An implementation of the Stucki dithering algorithm. diff --git a/src/main/java/com/defano/jmonet/algo/dither/quant/ColorReductionQuantizer.java b/src/main/java/com/defano/jmonet/transform/dither/quant/ColorReductionQuantizer.java similarity index 97% rename from src/main/java/com/defano/jmonet/algo/dither/quant/ColorReductionQuantizer.java rename to src/main/java/com/defano/jmonet/transform/dither/quant/ColorReductionQuantizer.java index fe1e97ac..c60f1388 100644 --- a/src/main/java/com/defano/jmonet/algo/dither/quant/ColorReductionQuantizer.java +++ b/src/main/java/com/defano/jmonet/transform/dither/quant/ColorReductionQuantizer.java @@ -1,4 +1,4 @@ -package com.defano.jmonet.algo.dither.quant; +package com.defano.jmonet.transform.dither.quant; /** * Quantizes (reduces) a 24-bit, RGB-encoded color value to a reduced palette where the total number diff --git a/src/main/java/com/defano/jmonet/algo/dither/quant/GrayscaleQuantizer.java b/src/main/java/com/defano/jmonet/transform/dither/quant/GrayscaleQuantizer.java similarity index 96% rename from src/main/java/com/defano/jmonet/algo/dither/quant/GrayscaleQuantizer.java rename to src/main/java/com/defano/jmonet/transform/dither/quant/GrayscaleQuantizer.java index 2343a241..a712946c 100644 --- a/src/main/java/com/defano/jmonet/algo/dither/quant/GrayscaleQuantizer.java +++ b/src/main/java/com/defano/jmonet/transform/dither/quant/GrayscaleQuantizer.java @@ -1,4 +1,4 @@ -package com.defano.jmonet.algo.dither.quant; +package com.defano.jmonet.transform.dither.quant; /** * Quantizes (reduces) a 24-bit, RGB-encoded color value to a gray-scale palette where the total number diff --git a/src/main/java/com/defano/jmonet/algo/dither/quant/MonochromaticQuantizer.java b/src/main/java/com/defano/jmonet/transform/dither/quant/MonochromaticQuantizer.java similarity index 93% rename from src/main/java/com/defano/jmonet/algo/dither/quant/MonochromaticQuantizer.java rename to src/main/java/com/defano/jmonet/transform/dither/quant/MonochromaticQuantizer.java index 44227a2d..70a6013d 100644 --- a/src/main/java/com/defano/jmonet/algo/dither/quant/MonochromaticQuantizer.java +++ b/src/main/java/com/defano/jmonet/transform/dither/quant/MonochromaticQuantizer.java @@ -1,4 +1,4 @@ -package com.defano.jmonet.algo.dither.quant; +package com.defano.jmonet.transform.dither.quant; /** * Quantizes (reduces) a 24-bit, RGB-encoded color value to a monochrome (black and white) palette diff --git a/src/main/java/com/defano/jmonet/algo/dither/quant/QuantizationFunction.java b/src/main/java/com/defano/jmonet/transform/dither/quant/QuantizationFunction.java similarity index 94% rename from src/main/java/com/defano/jmonet/algo/dither/quant/QuantizationFunction.java rename to src/main/java/com/defano/jmonet/transform/dither/quant/QuantizationFunction.java index d38b4e17..9a036695 100644 --- a/src/main/java/com/defano/jmonet/algo/dither/quant/QuantizationFunction.java +++ b/src/main/java/com/defano/jmonet/transform/dither/quant/QuantizationFunction.java @@ -1,4 +1,4 @@ -package com.defano.jmonet.algo.dither.quant; +package com.defano.jmonet.transform.dither.quant; /** * An object which performs a color quantization (reduction) function. diff --git a/src/main/java/com/defano/jmonet/algo/transform/image/ApplyAffineTransform.java b/src/main/java/com/defano/jmonet/transform/image/ApplyAffineTransform.java similarity index 97% rename from src/main/java/com/defano/jmonet/algo/transform/image/ApplyAffineTransform.java rename to src/main/java/com/defano/jmonet/transform/image/ApplyAffineTransform.java index 02213439..dfc85368 100644 --- a/src/main/java/com/defano/jmonet/algo/transform/image/ApplyAffineTransform.java +++ b/src/main/java/com/defano/jmonet/transform/image/ApplyAffineTransform.java @@ -1,4 +1,4 @@ -package com.defano.jmonet.algo.transform.image; +package com.defano.jmonet.transform.image; import com.defano.jmonet.model.Interpolation; diff --git a/src/main/java/com/defano/jmonet/algo/transform/image/ApplyPixelTransform.java b/src/main/java/com/defano/jmonet/transform/image/ApplyPixelTransform.java similarity index 91% rename from src/main/java/com/defano/jmonet/algo/transform/image/ApplyPixelTransform.java rename to src/main/java/com/defano/jmonet/transform/image/ApplyPixelTransform.java index 15b28bf0..029e5c55 100644 --- a/src/main/java/com/defano/jmonet/algo/transform/image/ApplyPixelTransform.java +++ b/src/main/java/com/defano/jmonet/transform/image/ApplyPixelTransform.java @@ -1,4 +1,4 @@ -package com.defano.jmonet.algo.transform.image; +package com.defano.jmonet.transform.image; import com.defano.jmonet.tools.util.ImageUtils; @@ -45,7 +45,7 @@ public BufferedImage apply(BufferedImage source) { for (int x = 0; x < transformed.getWidth(); x++) { for (int y = 0; y < transformed.getHeight(); y++) { if (mask == null || mask.contains(x, y)) { - transformed.setRGB(x, y, transform.apply(x, y, transformed.getRGB(x, y))); + transformed.setRGB(x, y, transform.apply(transformed.getRGB(x, y))); } } } diff --git a/src/main/java/com/defano/jmonet/algo/transform/image/BufferedImageOpTransform.java b/src/main/java/com/defano/jmonet/transform/image/BufferedImageOpTransform.java similarity index 93% rename from src/main/java/com/defano/jmonet/algo/transform/image/BufferedImageOpTransform.java rename to src/main/java/com/defano/jmonet/transform/image/BufferedImageOpTransform.java index a59e6dd2..b91a918f 100644 --- a/src/main/java/com/defano/jmonet/algo/transform/image/BufferedImageOpTransform.java +++ b/src/main/java/com/defano/jmonet/transform/image/BufferedImageOpTransform.java @@ -1,4 +1,4 @@ -package com.defano.jmonet.algo.transform.image; +package com.defano.jmonet.transform.image; import com.defano.jmonet.tools.util.ImageUtils; diff --git a/src/main/java/com/defano/jmonet/algo/transform/image/ColorReductionTransform.java b/src/main/java/com/defano/jmonet/transform/image/ColorReductionTransform.java similarity index 84% rename from src/main/java/com/defano/jmonet/algo/transform/image/ColorReductionTransform.java rename to src/main/java/com/defano/jmonet/transform/image/ColorReductionTransform.java index 274d3449..f9e25e0d 100644 --- a/src/main/java/com/defano/jmonet/algo/transform/image/ColorReductionTransform.java +++ b/src/main/java/com/defano/jmonet/transform/image/ColorReductionTransform.java @@ -1,9 +1,9 @@ -package com.defano.jmonet.algo.transform.image; +package com.defano.jmonet.transform.image; -import com.defano.jmonet.algo.dither.Ditherer; -import com.defano.jmonet.algo.dither.FloydSteinbergDitherer; -import com.defano.jmonet.algo.dither.quant.ColorReductionQuantizer; -import com.defano.jmonet.algo.dither.quant.MonochromaticQuantizer; +import com.defano.jmonet.transform.dither.Ditherer; +import com.defano.jmonet.transform.dither.FloydSteinbergDitherer; +import com.defano.jmonet.transform.dither.quant.ColorReductionQuantizer; +import com.defano.jmonet.transform.dither.quant.MonochromaticQuantizer; import java.awt.image.BufferedImage; diff --git a/src/main/java/com/defano/jmonet/algo/transform/image/ConvolutionTransform.java b/src/main/java/com/defano/jmonet/transform/image/ConvolutionTransform.java similarity index 94% rename from src/main/java/com/defano/jmonet/algo/transform/image/ConvolutionTransform.java rename to src/main/java/com/defano/jmonet/transform/image/ConvolutionTransform.java index e2b7af78..b705a21a 100644 --- a/src/main/java/com/defano/jmonet/algo/transform/image/ConvolutionTransform.java +++ b/src/main/java/com/defano/jmonet/transform/image/ConvolutionTransform.java @@ -1,4 +1,4 @@ -package com.defano.jmonet.algo.transform.image; +package com.defano.jmonet.transform.image; import com.defano.jmonet.tools.util.ImageUtils; diff --git a/src/main/java/com/defano/jmonet/algo/transform/image/FillTransform.java b/src/main/java/com/defano/jmonet/transform/image/FillTransform.java similarity index 55% rename from src/main/java/com/defano/jmonet/algo/transform/image/FillTransform.java rename to src/main/java/com/defano/jmonet/transform/image/FillTransform.java index 07e6d822..feb70a3e 100644 --- a/src/main/java/com/defano/jmonet/algo/transform/image/FillTransform.java +++ b/src/main/java/com/defano/jmonet/transform/image/FillTransform.java @@ -1,7 +1,7 @@ -package com.defano.jmonet.algo.transform.image; +package com.defano.jmonet.transform.image; -import com.defano.jmonet.algo.fill.DefaultFillFunction; -import com.defano.jmonet.algo.fill.FillFunction; +import com.defano.jmonet.tools.attributes.FillFunction; +import com.defano.jmonet.tools.attributes.MarkPredicate; import com.defano.jmonet.tools.util.ImageUtils; import java.awt.*; @@ -18,32 +18,50 @@ public class FillTransform implements StaticImageTransform { private final Shape mask; private final Paint paint; private final FillFunction fillFunction; + private final MarkPredicate markPredicate; /** - * Creates a fill transform that fills only pixels bounded by mask. + * Creates a fill transform that fills marked pixels bounded by a mask. + * + * @param mask The shape bounding pixels to be filled. + * @param paint The paint that will be used fill affected pixels + * @param fillFunction A function that applies the paint to each affected pixel + * @param markPredicate A predicate function that determines if each pixel in the bounded area is eligible to be + * filled. Only unmarked pixels will be filled. + */ + public FillTransform(Shape mask, Paint paint, FillFunction fillFunction, MarkPredicate markPredicate) { + this.mask = mask; + this.paint = paint; + this.fillFunction = fillFunction; + this.markPredicate = markPredicate; + } + + /** + * Creates a fill transform that fills only completely transparent pixels bounded by mask. * * @param mask The shape determining which transparent pixels to fill; null to adjust all transparent pixels. * @param paint The {@link Paint} with which to fill * @param fillFunction An function that applies the paint to each affected pixel. Most users should simply supply - * an instance of {@link DefaultFillFunction}. + * an instance of {@link FillFunction}. */ public FillTransform(Shape mask, Paint paint, FillFunction fillFunction) { - this.mask = mask; - this.paint = paint; - this.fillFunction = fillFunction; + this(mask, paint, fillFunction, new MarkPredicate() { + @Override + public boolean isMarked(Color pixel, Color eraseColor) { + return pixel.getAlpha() > 0; + } + }); } /** - * Creates a fill transform that fills all pixels in the raster. + * Creates a fill transform that fills all completely transparent pixels in the raster. * * @param paint The {@link Paint} with which to fill * @param fillFunction An function that applies the paint to each affected pixel. Most users should simply supply - * an instance of {@link DefaultFillFunction}. + * an instance of {@link FillFunction}. */ public FillTransform(Paint paint, FillFunction fillFunction) { - this.mask = null; - this.paint = paint; - this.fillFunction = fillFunction; + this(null, paint, fillFunction); } /** @@ -57,7 +75,7 @@ public BufferedImage apply(BufferedImage source) { for (int y = 0; y < transformed.getHeight(); y++) { if (mask == null || mask.contains(x, y)) { Color c = new Color(transformed.getRGB(x, y), true); - if (c.getAlpha() == 0) { + if (!markPredicate.isMarked(c, null)) { fillFunction.fill(transformed, x, y, paint); } } diff --git a/src/main/java/com/defano/jmonet/algo/transform/image/FloodFillTransform.java b/src/main/java/com/defano/jmonet/transform/image/FloodFillTransform.java similarity index 62% rename from src/main/java/com/defano/jmonet/algo/transform/image/FloodFillTransform.java rename to src/main/java/com/defano/jmonet/transform/image/FloodFillTransform.java index d1cc3fae..2b6ced41 100644 --- a/src/main/java/com/defano/jmonet/algo/transform/image/FloodFillTransform.java +++ b/src/main/java/com/defano/jmonet/transform/image/FloodFillTransform.java @@ -1,7 +1,7 @@ -package com.defano.jmonet.algo.transform.image; +package com.defano.jmonet.transform.image; -import com.defano.jmonet.algo.fill.BoundaryFunction; -import com.defano.jmonet.algo.fill.FillFunction; +import com.defano.jmonet.tools.attributes.BoundaryFunction; +import com.defano.jmonet.tools.attributes.FillFunction; import java.awt.*; import java.awt.image.BufferedImage; @@ -14,27 +14,13 @@ * Given an origin point in the image, this algorithm iteratively paints every adjacent pixel with the given color or * texture until it reaches a boundary pixel. */ +@SuppressWarnings("unused") public class FloodFillTransform implements ImageTransform { - private final BoundaryFunction boundary; - private final FillFunction fill; - private final Point origin; - private final Paint fillPaint; - - /** - * Creates a flood-fill transform. - * - * @param fillPaint The color or texture to flood-fill the image with - * @param origin The seed or origin point in the image where the flooding should begin - * @param fill A function that applies the fill paint to pixels identified for painting - * @param boundary A function that determines which pixels in the image "enclose" the paint - */ - public FloodFillTransform(Paint fillPaint, Point origin, FillFunction fill, BoundaryFunction boundary) { - this.fillPaint = fillPaint; - this.origin = origin; - this.fill = fill; - this.boundary = boundary; - } + private BoundaryFunction boundaryFunction; + private FillFunction fill; + private Point origin; + private Paint fillPaint; /** * {@inheritDoc} @@ -58,23 +44,55 @@ public BufferedImage apply(BufferedImage source) { fill.fill(transformed, thisPixelX, thisPixelY, fillPaint); - if (bounds.contains(thisPixelX + 1, thisPixelY) && !boundary.isBoundary(source, transformed, thisPixelX + 1, thisPixelY)) { + if (bounds.contains(thisPixelX + 1, thisPixelY) && !boundaryFunction.isBoundary(source, transformed, thisPixelX + 1, thisPixelY)) { fillPixels.add(new Point(thisPixelX + 1, thisPixelY)); } - if (bounds.contains(thisPixelX - 1, thisPixelY) && !boundary.isBoundary(source, transformed, thisPixelX - 1, thisPixelY)) { + if (bounds.contains(thisPixelX - 1, thisPixelY) && !boundaryFunction.isBoundary(source, transformed, thisPixelX - 1, thisPixelY)) { fillPixels.add(new Point(thisPixelX - 1, thisPixelY)); } - if (bounds.contains(thisPixelX, thisPixelY + 1) && !boundary.isBoundary(source, transformed, thisPixelX, thisPixelY + 1)) { + if (bounds.contains(thisPixelX, thisPixelY + 1) && !boundaryFunction.isBoundary(source, transformed, thisPixelX, thisPixelY + 1)) { fillPixels.add(new Point(thisPixelX, thisPixelY + 1)); } - if (bounds.contains(thisPixelX, thisPixelY - 1) && !boundary.isBoundary(source, transformed, thisPixelX, thisPixelY - 1)) { + if (bounds.contains(thisPixelX, thisPixelY - 1) && !boundaryFunction.isBoundary(source, transformed, thisPixelX, thisPixelY - 1)) { fillPixels.add(new Point(thisPixelX, thisPixelY - 1)); } } return transformed; } + + public BoundaryFunction getBoundaryFunction() { + return boundaryFunction; + } + + public void setBoundaryFunction(BoundaryFunction boundaryFunction) { + this.boundaryFunction = boundaryFunction; + } + + public FillFunction getFill() { + return fill; + } + + public void setFill(FillFunction fill) { + this.fill = fill; + } + + public Point getOrigin() { + return origin; + } + + public void setOrigin(Point origin) { + this.origin = origin; + } + + public Paint getFillPaint() { + return fillPaint; + } + + public void setFillPaint(Paint fillPaint) { + this.fillPaint = fillPaint; + } } diff --git a/src/main/java/com/defano/jmonet/algo/transform/image/GreyscaleReductionTransform.java b/src/main/java/com/defano/jmonet/transform/image/GreyscaleReductionTransform.java similarity index 78% rename from src/main/java/com/defano/jmonet/algo/transform/image/GreyscaleReductionTransform.java rename to src/main/java/com/defano/jmonet/transform/image/GreyscaleReductionTransform.java index e1e163ea..2faec1c5 100644 --- a/src/main/java/com/defano/jmonet/algo/transform/image/GreyscaleReductionTransform.java +++ b/src/main/java/com/defano/jmonet/transform/image/GreyscaleReductionTransform.java @@ -1,9 +1,9 @@ -package com.defano.jmonet.algo.transform.image; +package com.defano.jmonet.transform.image; -import com.defano.jmonet.algo.dither.Ditherer; -import com.defano.jmonet.algo.dither.FloydSteinbergDitherer; -import com.defano.jmonet.algo.dither.quant.GrayscaleQuantizer; -import com.defano.jmonet.algo.dither.quant.MonochromaticQuantizer; +import com.defano.jmonet.transform.dither.Ditherer; +import com.defano.jmonet.transform.dither.FloydSteinbergDitherer; +import com.defano.jmonet.transform.dither.quant.GrayscaleQuantizer; +import com.defano.jmonet.transform.dither.quant.MonochromaticQuantizer; import java.awt.image.BufferedImage; diff --git a/src/main/java/com/defano/jmonet/algo/transform/image/ImageTransform.java b/src/main/java/com/defano/jmonet/transform/image/ImageTransform.java similarity index 94% rename from src/main/java/com/defano/jmonet/algo/transform/image/ImageTransform.java rename to src/main/java/com/defano/jmonet/transform/image/ImageTransform.java index 85f60d46..612b7353 100644 --- a/src/main/java/com/defano/jmonet/algo/transform/image/ImageTransform.java +++ b/src/main/java/com/defano/jmonet/transform/image/ImageTransform.java @@ -1,4 +1,4 @@ -package com.defano.jmonet.algo.transform.image; +package com.defano.jmonet.transform.image; import java.awt.image.BufferedImage; diff --git a/src/main/java/com/defano/jmonet/algo/transform/image/PixelTransform.java b/src/main/java/com/defano/jmonet/transform/image/PixelTransform.java similarity index 60% rename from src/main/java/com/defano/jmonet/algo/transform/image/PixelTransform.java rename to src/main/java/com/defano/jmonet/transform/image/PixelTransform.java index 6284f5d8..3cc71a9a 100644 --- a/src/main/java/com/defano/jmonet/algo/transform/image/PixelTransform.java +++ b/src/main/java/com/defano/jmonet/transform/image/PixelTransform.java @@ -1,4 +1,4 @@ -package com.defano.jmonet.algo.transform.image; +package com.defano.jmonet.transform.image; /** * A transform that can be applied to an individual pixel, like invert, or brightness adjustment. @@ -8,10 +8,8 @@ public interface PixelTransform { /** * Performs a transformation on a pixel's RGB color value. * - * @param x The x-coordinate of the pixel being transformed - * @param y The y-coordinate of the pixel being transformed * @param rgb The pixel's color value * @return The transformed pixel's color value */ - int apply(int x, int y, int rgb); + int apply(int rgb); } diff --git a/src/main/java/com/defano/jmonet/algo/transform/image/ProjectionTransform.java b/src/main/java/com/defano/jmonet/transform/image/ProjectionTransform.java similarity index 50% rename from src/main/java/com/defano/jmonet/algo/transform/image/ProjectionTransform.java rename to src/main/java/com/defano/jmonet/transform/image/ProjectionTransform.java index 871a6823..f18f0420 100644 --- a/src/main/java/com/defano/jmonet/algo/transform/image/ProjectionTransform.java +++ b/src/main/java/com/defano/jmonet/transform/image/ProjectionTransform.java @@ -1,4 +1,4 @@ -package com.defano.jmonet.algo.transform.image; +package com.defano.jmonet.transform.image; import Jama.Matrix; import com.defano.jmonet.model.Quadrilateral; @@ -41,56 +41,56 @@ public BufferedImage apply(BufferedImage input) { // Scale the source image to match the dimensions of the projection geometry BufferedImage source = new ScaleTransform(new Dimension(projection.getWidth(), projection.getHeight())).apply(input); - double x1, y1, x2, y2, x3, y3, x4, y4, X1, Y1, X2, Y2, X3, Y3, X4, Y4; + double dx1, dy1, dx2, dy2, dx3, dy3, dx4, dy4, sx1, sy1, sx2, sy2, sx3, sy3, sx4, sy4; int imageWidth = source.getWidth(); int imageHeight = source.getHeight(); // Destination image geometry (defined by projection) - x1 = Math.abs(projection.getTopLeft().getX()); - y1 = Math.abs(projection.getTopLeft().getY()); - x2 = Math.abs(projection.getTopRight().getX()); - y2 = Math.abs(projection.getTopRight().getY()); - x3 = Math.abs(projection.getBottomRight().getX()); - y3 = Math.abs(projection.getBottomRight().getY()); - x4 = Math.abs(projection.getBottomLeft().getX()); - y4 = Math.abs(projection.getBottomLeft().getY()); + dx1 = Math.abs(projection.getTopLeft().getX()); + dy1 = Math.abs(projection.getTopLeft().getY()); + dx2 = Math.abs(projection.getTopRight().getX()); + dy2 = Math.abs(projection.getTopRight().getY()); + dx3 = Math.abs(projection.getBottomRight().getX()); + dy3 = Math.abs(projection.getBottomRight().getY()); + dx4 = Math.abs(projection.getBottomLeft().getX()); + dy4 = Math.abs(projection.getBottomLeft().getY()); // Source image geometry (assumed to be the bounds rect of projection) - X1 = 0; - Y1 = 0; - X2 = imageWidth - 1; - Y2 = 0; - X3 = imageWidth - 1; - Y3 = imageHeight - 1; - X4 = 0; - Y4 = imageHeight - 1; - - double[][] M_a = - {{x1, y1, 1, 0, 0, 0, -x1 * X1, -y1 * X1}, - {x2, y2, 1, 0, 0, 0, -x2 * X2, -y2 * X2}, - {x3, y3, 1, 0, 0, 0, -x3 * X3, -y3 * X3}, - {x4, y4, 1, 0, 0, 0, -x4 * X4, -y4 * X4}, - {0, 0, 0, x1, y1, 1, -x1 * Y1, -y1 * Y1}, - {0, 0, 0, x2, y2, 1, -x2 * Y2, -y2 * Y2}, - {0, 0, 0, x3, y3, 1, -x3 * Y3, -y3 * Y3}, - {0, 0, 0, x4, y4, 1, -x4 * Y4, -y4 * Y4} + sx1 = 0; + sy1 = 0; + sx2 = imageWidth - 1.0; + sy2 = 0; + sx3 = imageWidth - 1.0; + sy3 = imageHeight - 1.0; + sx4 = 0; + sy4 = imageHeight - 1.0; + + double[][] arrayA = + {{dx1, dy1, 1, 0, 0, 0, -dx1 * sx1, -dy1 * sx1}, + {dx2, dy2, 1, 0, 0, 0, -dx2 * sx2, -dy2 * sx2}, + {dx3, dy3, 1, 0, 0, 0, -dx3 * sx3, -dy3 * sx3}, + {dx4, dy4, 1, 0, 0, 0, -dx4 * sx4, -dy4 * sx4}, + {0, 0, 0, dx1, dy1, 1, -dx1 * sy1, -dy1 * sy1}, + {0, 0, 0, dx2, dy2, 1, -dx2 * sy2, -dy2 * sy2}, + {0, 0, 0, dx3, dy3, 1, -dx3 * sy3, -dy3 * sy3}, + {0, 0, 0, dx4, dy4, 1, -dx4 * sy4, -dy4 * sy4} }; - double[][] M_b = {{X1}, {X2}, {X3}, {X4}, {Y1}, {Y2}, {Y3}, {Y4}}; + double[][] arrayB = {{sx1}, {sx2}, {sx3}, {sx4}, {sy1}, {sy2}, {sy3}, {sy4}}; - Matrix A = new Matrix(M_a); - Matrix B = new Matrix(M_b); - Matrix C = A.solve(B); + Matrix matrixA = new Matrix(arrayA); + Matrix matrixB = new Matrix(arrayB); + Matrix solution = matrixA.solve(matrixB); - double a = C.get(0, 0); // fixed scale factor in X direction with scale Y unchanged - double b = C.get(1, 0); // scale factor in X direction proportional to Y distance from origin - double c = C.get(2, 0); // origin translation in X direction - double d = C.get(3, 0); // scale factor in Y direction proportional to X distance from origin - double e = C.get(4, 0); // fixed scale factor in Y direction with scale X unchanged - double f = C.get(5, 0); // origin translation in Y direction - double g = C.get(6, 0); // proportional scale factors X and Y in function of X - double h = C.get(7, 0); // proportional scale factors X and Y in function of Y + double a = solution.get(0, 0); // fixed scale factor in X direction with scale Y unchanged + double b = solution.get(1, 0); // scale factor in X direction proportional to Y distance from origin + double c = solution.get(2, 0); // origin translation in X direction + double d = solution.get(3, 0); // scale factor in Y direction proportional to X distance from origin + double e = solution.get(4, 0); // fixed scale factor in Y direction with scale X unchanged + double f = solution.get(5, 0); // origin translation in Y direction + double g = solution.get(6, 0); // proportional scale factors X and Y in function of X + double h = solution.get(7, 0); // proportional scale factors X and Y in function of Y BufferedImage output = new BufferedImage(imageWidth, imageHeight, BufferedImage.TYPE_INT_ARGB); diff --git a/src/main/java/com/defano/jmonet/algo/transform/image/RubbersheetTransform.java b/src/main/java/com/defano/jmonet/transform/image/RubbersheetTransform.java similarity index 50% rename from src/main/java/com/defano/jmonet/algo/transform/image/RubbersheetTransform.java rename to src/main/java/com/defano/jmonet/transform/image/RubbersheetTransform.java index 7163f2dd..e95a7b3a 100644 --- a/src/main/java/com/defano/jmonet/algo/transform/image/RubbersheetTransform.java +++ b/src/main/java/com/defano/jmonet/transform/image/RubbersheetTransform.java @@ -1,4 +1,4 @@ -package com.defano.jmonet.algo.transform.image; +package com.defano.jmonet.transform.image; import Jama.Matrix; import com.defano.jmonet.model.Quadrilateral; @@ -39,56 +39,56 @@ public BufferedImage apply(BufferedImage input) { // Scale the source image to match the dimensions of the projection geometry BufferedImage source = new ScaleTransform(new Dimension(projection.getWidth(), projection.getHeight())).apply(input); - double x1, y1, x2, y2, x3, y3, x4, y4, X1, Y1, X2, Y2, X3, Y3, X4, Y4; + double dx1, dy1, dx2, dy2, dx3, dy3, dx4, dy4, sx1, sy1, sx2, sy2, sx3, sy3, sx4, sy4; int sourceWidth = source.getWidth(); int sourceHeight = source.getHeight(); // Destination image geometry (defined by projection) - x1 = Math.abs(projection.getTopLeft().getX()); - y1 = Math.abs(projection.getTopLeft().getY()); - x2 = Math.abs(projection.getTopRight().getX()); - y2 = Math.abs(projection.getTopRight().getY()); - x3 = Math.abs(projection.getBottomRight().getX()); - y3 = Math.abs(projection.getBottomRight().getY()); - x4 = Math.abs(projection.getBottomLeft().getX()); - y4 = Math.abs(projection.getBottomLeft().getY()); + dx1 = Math.abs(projection.getTopLeft().getX()); + dy1 = Math.abs(projection.getTopLeft().getY()); + dx2 = Math.abs(projection.getTopRight().getX()); + dy2 = Math.abs(projection.getTopRight().getY()); + dx3 = Math.abs(projection.getBottomRight().getX()); + dy3 = Math.abs(projection.getBottomRight().getY()); + dx4 = Math.abs(projection.getBottomLeft().getX()); + dy4 = Math.abs(projection.getBottomLeft().getY()); // Source image geometry (assumed to be the bounds rect of projection) - X1 = 0; - Y1 = 0; - X2 = sourceWidth - 1; - Y2 = 0; - X3 = sourceWidth - 1; - Y3 = sourceHeight - 1; - X4 = 0; - Y4 = sourceHeight - 1; - - double[][] M_a = - {{x1 * y1, x1, y1, 1, 0, 0, 0, 0}, - {x2 * y2, x2, y2, 1, 0, 0, 0, 0}, - {x3 * y3, x3, y3, 1, 0, 0, 0, 0}, - {x4 * y4, x4, y4, 1, 0, 0, 0, 0}, - {0, 0, 0, 0, x1 * y1, x1, y1, 1}, - {0, 0, 0, 0, x2 * y2, x2, y2, 1}, - {0, 0, 0, 0, x3 * y3, x3, y3, 1}, - {0, 0, 0, 0, x4 * y4, x4, y4, 1} + sx1 = 0; + sy1 = 0; + sx2 = sourceWidth - 1.0; + sy2 = 0; + sx3 = sourceWidth - 1.0; + sy3 = sourceHeight - 1.0; + sx4 = 0; + sy4 = sourceHeight - 1.0; + + double[][] arrayA = + {{dx1 * dy1, dx1, dy1, 1, 0, 0, 0, 0}, + {dx2 * dy2, dx2, dy2, 1, 0, 0, 0, 0}, + {dx3 * dy3, dx3, dy3, 1, 0, 0, 0, 0}, + {dx4 * dy4, dx4, dy4, 1, 0, 0, 0, 0}, + {0, 0, 0, 0, dx1 * dy1, dx1, dy1, 1}, + {0, 0, 0, 0, dx2 * dy2, dx2, dy2, 1}, + {0, 0, 0, 0, dx3 * dy3, dx3, dy3, 1}, + {0, 0, 0, 0, dx4 * dy4, dx4, dy4, 1} }; - double[][] M_b = {{X1}, {X2}, {X3}, {X4}, {Y1}, {Y2}, {Y3}, {Y4}}; + double[][] arrayB = {{sx1}, {sx2}, {sx3}, {sx4}, {sy1}, {sy2}, {sy3}, {sy4}}; - Matrix A = new Matrix(M_a); - Matrix B = new Matrix(M_b); - Matrix C = A.solve(B); + Matrix matrixA = new Matrix(arrayA); + Matrix matrixB = new Matrix(arrayB); + Matrix solution = matrixA.solve(matrixB); - double a = C.get(0, 0); // scale factor in X direction proportional to the multiplication X * Y - double b = C.get(1, 0); // fixed scale factor in X direction with scale Y unchanged - double c = C.get(2, 0); // scale factor in X direction proportional to Y distance from origin - double d = C.get(3, 0); // origin translation in X direction - double e = C.get(4, 0); // scale factor in Y direction proportional to the multiplication X * Y - double f = C.get(5, 0); // fixed scale factor in Y direction with scale X unchanged - double g = C.get(6, 0); // scale factor in Y direction proportional to X distance from origin - double h = C.get(7, 0); // origin translation in Y direction + double a = solution.get(0, 0); // scale factor in X direction proportional to the multiplication X * Y + double b = solution.get(1, 0); // fixed scale factor in X direction with scale Y unchanged + double c = solution.get(2, 0); // scale factor in X direction proportional to Y distance from origin + double d = solution.get(3, 0); // origin translation in X direction + double e = solution.get(4, 0); // scale factor in Y direction proportional to the multiplication X * Y + double f = solution.get(5, 0); // fixed scale factor in Y direction with scale X unchanged + double g = solution.get(6, 0); // scale factor in Y direction proportional to X distance from origin + double h = solution.get(7, 0); // origin translation in Y direction BufferedImage output = new BufferedImage(sourceWidth, sourceHeight, BufferedImage.TYPE_INT_ARGB); diff --git a/src/main/java/com/defano/jmonet/algo/transform/image/ScaleTransform.java b/src/main/java/com/defano/jmonet/transform/image/ScaleTransform.java similarity index 94% rename from src/main/java/com/defano/jmonet/algo/transform/image/ScaleTransform.java rename to src/main/java/com/defano/jmonet/transform/image/ScaleTransform.java index c6c098e3..4f2bc12a 100644 --- a/src/main/java/com/defano/jmonet/algo/transform/image/ScaleTransform.java +++ b/src/main/java/com/defano/jmonet/transform/image/ScaleTransform.java @@ -1,4 +1,4 @@ -package com.defano.jmonet.algo.transform.image; +package com.defano.jmonet.transform.image; import java.awt.*; import java.awt.image.BufferedImage; diff --git a/src/main/java/com/defano/jmonet/algo/transform/image/SlantTransform.java b/src/main/java/com/defano/jmonet/transform/image/SlantTransform.java similarity index 96% rename from src/main/java/com/defano/jmonet/algo/transform/image/SlantTransform.java rename to src/main/java/com/defano/jmonet/transform/image/SlantTransform.java index 8f16d6ec..89326bbe 100644 --- a/src/main/java/com/defano/jmonet/algo/transform/image/SlantTransform.java +++ b/src/main/java/com/defano/jmonet/transform/image/SlantTransform.java @@ -1,4 +1,4 @@ -package com.defano.jmonet.algo.transform.image; +package com.defano.jmonet.transform.image; import java.awt.geom.AffineTransform; import java.awt.image.AffineTransformOp; diff --git a/src/main/java/com/defano/jmonet/algo/transform/image/StaticImageTransform.java b/src/main/java/com/defano/jmonet/transform/image/StaticImageTransform.java similarity index 94% rename from src/main/java/com/defano/jmonet/algo/transform/image/StaticImageTransform.java rename to src/main/java/com/defano/jmonet/transform/image/StaticImageTransform.java index b65ec247..273db05b 100644 --- a/src/main/java/com/defano/jmonet/algo/transform/image/StaticImageTransform.java +++ b/src/main/java/com/defano/jmonet/transform/image/StaticImageTransform.java @@ -1,4 +1,4 @@ -package com.defano.jmonet.algo.transform.image; +package com.defano.jmonet.transform.image; import java.awt.image.BufferedImage; diff --git a/src/main/java/com/defano/jmonet/algo/transform/image/StaticImageTransformable.java b/src/main/java/com/defano/jmonet/transform/image/StaticImageTransformable.java similarity index 93% rename from src/main/java/com/defano/jmonet/algo/transform/image/StaticImageTransformable.java rename to src/main/java/com/defano/jmonet/transform/image/StaticImageTransformable.java index d1c0c4b7..ec04ff02 100644 --- a/src/main/java/com/defano/jmonet/algo/transform/image/StaticImageTransformable.java +++ b/src/main/java/com/defano/jmonet/transform/image/StaticImageTransformable.java @@ -1,4 +1,4 @@ -package com.defano.jmonet.algo.transform.image; +package com.defano.jmonet.transform.image; /** * An image-containing object that can be transformed by a {@link StaticImageTransform}; that is, all the pixels of the diff --git a/src/main/java/com/defano/jmonet/algo/transform/image/Transformable.java b/src/main/java/com/defano/jmonet/transform/image/Transformable.java similarity index 91% rename from src/main/java/com/defano/jmonet/algo/transform/image/Transformable.java rename to src/main/java/com/defano/jmonet/transform/image/Transformable.java index 3a61a20a..56b2b3d1 100644 --- a/src/main/java/com/defano/jmonet/algo/transform/image/Transformable.java +++ b/src/main/java/com/defano/jmonet/transform/image/Transformable.java @@ -1,12 +1,12 @@ -package com.defano.jmonet.algo.transform.image; +package com.defano.jmonet.transform.image; -import com.defano.jmonet.algo.dither.Ditherer; -import com.defano.jmonet.algo.dither.FloydSteinbergDitherer; -import com.defano.jmonet.algo.fill.FillFunction; -import com.defano.jmonet.algo.transform.pixel.BrightnessPixelTransform; -import com.defano.jmonet.algo.transform.pixel.InvertPixelTransform; -import com.defano.jmonet.algo.transform.pixel.RemoveAlphaPixelTransform; -import com.defano.jmonet.algo.transform.pixel.TransparencyPixelTransform; +import com.defano.jmonet.transform.dither.Ditherer; +import com.defano.jmonet.transform.dither.FloydSteinbergDitherer; +import com.defano.jmonet.tools.attributes.FillFunction; +import com.defano.jmonet.transform.pixel.BrightnessPixelTransform; +import com.defano.jmonet.transform.pixel.InvertPixelTransform; +import com.defano.jmonet.transform.pixel.RemoveAlphaPixelTransform; +import com.defano.jmonet.transform.pixel.TransparencyPixelTransform; import java.awt.*; import java.awt.image.BufferedImageOp; diff --git a/src/main/java/com/defano/jmonet/algo/transform/pixel/BrightnessPixelTransform.java b/src/main/java/com/defano/jmonet/transform/pixel/BrightnessPixelTransform.java similarity index 87% rename from src/main/java/com/defano/jmonet/algo/transform/pixel/BrightnessPixelTransform.java rename to src/main/java/com/defano/jmonet/transform/pixel/BrightnessPixelTransform.java index 7ec7d9c0..348a68ce 100644 --- a/src/main/java/com/defano/jmonet/algo/transform/pixel/BrightnessPixelTransform.java +++ b/src/main/java/com/defano/jmonet/transform/pixel/BrightnessPixelTransform.java @@ -1,6 +1,6 @@ -package com.defano.jmonet.algo.transform.pixel; +package com.defano.jmonet.transform.pixel; -import com.defano.jmonet.algo.transform.image.PixelTransform; +import com.defano.jmonet.transform.image.PixelTransform; /** * Modifies the brightness (luminosity) of each affected pixel by adding/subtracting a delta value to each color channel @@ -24,7 +24,7 @@ public BrightnessPixelTransform(int delta) { * {@inheritDoc} */ @Override - public int apply(int x, int y, int rgb) { + public int apply(int rgb) { int alpha = 0xff000000 & rgb; int r = ((0xff0000 & rgb) >> 16) + delta; int g = ((0xff00 & rgb) >> 8) + delta; diff --git a/src/main/java/com/defano/jmonet/algo/transform/pixel/InvertPixelTransform.java b/src/main/java/com/defano/jmonet/transform/pixel/InvertPixelTransform.java similarity index 68% rename from src/main/java/com/defano/jmonet/algo/transform/pixel/InvertPixelTransform.java rename to src/main/java/com/defano/jmonet/transform/pixel/InvertPixelTransform.java index 70d9a350..d72aa3a7 100644 --- a/src/main/java/com/defano/jmonet/algo/transform/pixel/InvertPixelTransform.java +++ b/src/main/java/com/defano/jmonet/transform/pixel/InvertPixelTransform.java @@ -1,6 +1,6 @@ -package com.defano.jmonet.algo.transform.pixel; +package com.defano.jmonet.transform.pixel; -import com.defano.jmonet.algo.transform.image.PixelTransform; +import com.defano.jmonet.transform.image.PixelTransform; /** * Inverts the color value of each affected pixel. @@ -11,7 +11,7 @@ public class InvertPixelTransform implements PixelTransform { * {@inheritDoc} */ @Override - public int apply(int x, int y, int argb) { + public int apply(int argb) { int alpha = 0xff000000 & argb; int rgb = 0x00ffffff & argb; diff --git a/src/main/java/com/defano/jmonet/algo/transform/pixel/RemoveAlphaPixelTransform.java b/src/main/java/com/defano/jmonet/transform/pixel/RemoveAlphaPixelTransform.java similarity index 87% rename from src/main/java/com/defano/jmonet/algo/transform/pixel/RemoveAlphaPixelTransform.java rename to src/main/java/com/defano/jmonet/transform/pixel/RemoveAlphaPixelTransform.java index b734bc95..2bc3f291 100644 --- a/src/main/java/com/defano/jmonet/algo/transform/pixel/RemoveAlphaPixelTransform.java +++ b/src/main/java/com/defano/jmonet/transform/pixel/RemoveAlphaPixelTransform.java @@ -1,6 +1,6 @@ -package com.defano.jmonet.algo.transform.pixel; +package com.defano.jmonet.transform.pixel; -import com.defano.jmonet.algo.transform.image.PixelTransform; +import com.defano.jmonet.transform.image.PixelTransform; import java.awt.*; @@ -26,7 +26,7 @@ public RemoveAlphaPixelTransform(boolean makeTransparent) { * {@inheritDoc} */ @Override - public int apply(int x, int y, int rgb) { + public int apply(int rgb) { Color color = new Color(rgb, true); int alpha = color.getAlpha(); diff --git a/src/main/java/com/defano/jmonet/algo/transform/pixel/TransparencyPixelTransform.java b/src/main/java/com/defano/jmonet/transform/pixel/TransparencyPixelTransform.java similarity index 79% rename from src/main/java/com/defano/jmonet/algo/transform/pixel/TransparencyPixelTransform.java rename to src/main/java/com/defano/jmonet/transform/pixel/TransparencyPixelTransform.java index 34125574..05aee366 100644 --- a/src/main/java/com/defano/jmonet/algo/transform/pixel/TransparencyPixelTransform.java +++ b/src/main/java/com/defano/jmonet/transform/pixel/TransparencyPixelTransform.java @@ -1,6 +1,6 @@ -package com.defano.jmonet.algo.transform.pixel; +package com.defano.jmonet.transform.pixel; -import com.defano.jmonet.algo.transform.image.PixelTransform; +import com.defano.jmonet.transform.image.PixelTransform; import java.awt.*; @@ -15,7 +15,7 @@ public class TransparencyPixelTransform implements PixelTransform { * Creates a transparency-adjusting transform. * * @param delta The amount by which to adjust each affected pixel's alpha value. A value of +255 assures every pixel - * if fully opaque; a value of -255 assures every pixel is fully transparent. + * is fully opaque; a value of -255 assures every pixel is fully transparent. */ public TransparencyPixelTransform(int delta) { this.delta = delta; @@ -25,7 +25,7 @@ public TransparencyPixelTransform(int delta) { * {@inheritDoc} */ @Override - public int apply(int x, int y, int rgb) { + public int apply(int rgb) { Color color = new Color(rgb, true); int alpha = color.getAlpha() + delta; diff --git a/src/test/java/com/defano/jmonet/canvas/layer/ImageLayerTest.java b/src/test/java/com/defano/jmonet/canvas/layer/ImageLayerTest.java new file mode 100644 index 00000000..af65ee62 --- /dev/null +++ b/src/test/java/com/defano/jmonet/canvas/layer/ImageLayerTest.java @@ -0,0 +1,245 @@ +package com.defano.jmonet.canvas.layer; + +import com.defano.jmonet.context.GraphicsContext; +import com.defano.jmonet.tools.base.MockitoTest; +import org.junit.jupiter.api.Test; +import org.mockito.Mock; +import org.mockito.Mockito; + +import java.awt.*; +import java.awt.image.BufferedImage; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +public class ImageLayerTest extends MockitoTest { + + @Mock private BufferedImage mockImage; + @Mock private Composite mockComposite; + @Mock private Point mockLocation; + @Mock private GraphicsContext mockGraphics; + + @Test + void testThatConstructorLocatesImageAtOrigin() { + ImageLayer uut = new ImageLayer(mockImage); + + assertEquals(mockImage, uut.getImage()); + assertEquals(new Point(), uut.getLocation()); + assertEquals(AlphaComposite.getInstance(AlphaComposite.SRC_OVER, 1.0f), uut.getComposite()); + } + + @Test + void testThatConstructorDrawsImageWithSpecifiedComposite() { + ImageLayer uut = new ImageLayer(mockImage, mockComposite); + + assertEquals(mockImage, uut.getImage()); + assertEquals(new Point(), uut.getLocation()); + assertEquals(mockComposite, uut.getComposite()); + } + + @Test + void testThatConstructorDrawsImageWithSpecifiedCompositeAndLocation() { + ImageLayer uut = new ImageLayer(mockLocation, mockImage, mockComposite); + + assertEquals(mockImage, uut.getImage()); + assertEquals(mockLocation, uut.getLocation()); + assertEquals(mockComposite, uut.getComposite()); + } + + @Test + void testGetDisplayedSize() { + final int imageWidth = 100; + final int imageHeight = 200; + final Point location = new Point(10, 20); + + ImageLayer uut = new ImageLayer(location, mockImage, mockComposite); + + Mockito.when(mockImage.getWidth()).thenReturn(imageWidth); + Mockito.when(mockImage.getHeight()).thenReturn(imageHeight); + + assertEquals(new Dimension(location.x + imageWidth, location.y + imageHeight), uut.getDisplayedSize()); + } + + @Test + void testGetStoredSize() { + final int imageWidth = 100; + final int imageHeight = 200; + final Point location = new Point(10, 20); + + ImageLayer uut = new ImageLayer(location, mockImage, mockComposite); + + Mockito.when(mockImage.getWidth()).thenReturn(imageWidth); + Mockito.when(mockImage.getHeight()).thenReturn(imageHeight); + + assertEquals(new Dimension(imageWidth, imageHeight), uut.getStoredSize()); + } + + @Test + void testThatImagePaintsUnscaledWithNoClippingRect() { + final Dimension storedDimension = new Dimension(100, 200); + final Point imageLocation = new Point(10, 20); + + ImageLayer uut = new ImageLayer(imageLocation, mockImage, mockComposite); + Mockito.when(mockImage.getWidth()).thenReturn(storedDimension.width); + Mockito.when(mockImage.getHeight()).thenReturn(storedDimension.height); + + uut.paint(mockGraphics, 1.0, null); + + final Rectangle source = new Rectangle(0, 0, 100, 200); + final Rectangle projection = new Rectangle(10, 20, 100, 200); + + Mockito.verify(mockGraphics).setComposite(mockComposite); + Mockito.verify(mockGraphics).drawImage(mockImage, projection.x, projection.y, projection.x + projection.width, projection.y + projection.height, source.x, source.y, source.x + source.width, source.y + source.height, null); + } + + @Test + void testThatImagePaintsScaledWithNoClippingRect() { + final Dimension storedDimension = new Dimension(100, 200); + final Point imageLocation = new Point(10, 20); + final double scale = 16.0; + + ImageLayer uut = new ImageLayer(imageLocation, mockImage, mockComposite); + Mockito.when(mockImage.getWidth()).thenReturn(storedDimension.width); + Mockito.when(mockImage.getHeight()).thenReturn(storedDimension.height); + + uut.paint(mockGraphics, scale, null); + + final Rectangle source = new Rectangle(0, 0, 100, 200); + final Rectangle projection = new Rectangle(160, 320, 1600, 3200); + + Mockito.verify(mockGraphics).setComposite(mockComposite); + Mockito.verify(mockGraphics).drawImage(mockImage, projection.x, projection.y, projection.x + projection.width, projection.y + projection.height, source.x, source.y, source.x + source.width, source.y + source.height, null); + } + + @Test + void testThatImagePaintsScaledWithUnoccludedClippingRect() { + final Dimension storedDimension = new Dimension(100, 200); + final Point imageLocation = new Point(10, 20); + final double scale = 3.0; + + // Clipping rectangle does not drawn occlude image + final Rectangle clip = new Rectangle(0, 0, 1000, 1000); + + ImageLayer uut = new ImageLayer(imageLocation, mockImage, mockComposite); + Mockito.when(mockImage.getWidth()).thenReturn(storedDimension.width); + Mockito.when(mockImage.getHeight()).thenReturn(storedDimension.height); + + uut.paint(mockGraphics, scale, clip); + + final Rectangle source = new Rectangle(0, 0, 100, 200); + final Rectangle projection = new Rectangle(30, 60, 300, 600); + + Mockito.verify(mockGraphics).setComposite(mockComposite); + Mockito.verify(mockGraphics).drawImage(mockImage, projection.x, projection.y, projection.x + projection.width, projection.y + projection.height, source.x, source.y, source.x + source.width, source.y + source.height, null); + } + + @Test + void testThatImagePaintsScaledWithOccludedLeftClippingRgn() { + final Dimension storedDimension = new Dimension(100, 200); + final Point imageLocation = new Point(10, 20); + final double scale = 3.0; + + // Clipping rectangle occludes left portion of image + final Rectangle clip = new Rectangle(100, 0, 1000, 1000); + + ImageLayer uut = new ImageLayer(imageLocation, mockImage, mockComposite); + Mockito.when(mockImage.getWidth()).thenReturn(storedDimension.width); + Mockito.when(mockImage.getHeight()).thenReturn(storedDimension.height); + + uut.paint(mockGraphics, scale, clip); + + final Rectangle source = new Rectangle(23, 0, 100, 200); + final Rectangle projection = new Rectangle(0, 60, 300, 600); + + Mockito.verify(mockGraphics).setComposite(mockComposite); + Mockito.verify(mockGraphics).drawImage(mockImage, projection.x, projection.y, projection.x + projection.width, projection.y + projection.height, source.x, source.y, source.x + source.width, source.y + source.height, null); + } + + @Test + void testThatImagePaintsScaledWithOccludedTopClippingRgn() { + final Dimension storedDimension = new Dimension(100, 200); + final Point imageLocation = new Point(10, 20); + final double scale = 4.0; + + // Clipping rectangle occludes top portion of image + final Rectangle clip = new Rectangle(0, 100, 1000, 1000); + + ImageLayer uut = new ImageLayer(imageLocation, mockImage, mockComposite); + Mockito.when(mockImage.getWidth()).thenReturn(storedDimension.width); + Mockito.when(mockImage.getHeight()).thenReturn(storedDimension.height); + + uut.paint(mockGraphics, scale, clip); + + final Rectangle source = new Rectangle(0, 5, 100, 200); + final Rectangle projection = new Rectangle(40, 0, 400, 800); + + Mockito.verify(mockGraphics).setComposite(mockComposite); + Mockito.verify(mockGraphics).drawImage(mockImage, projection.x, projection.y, projection.x + projection.width, projection.y + projection.height, source.x, source.y, source.x + source.width, source.y + source.height, null); + } + + @Test + void testThatImagePaintsScaledWithOccludedRightClippingRgn() { + final Dimension storedDimension = new Dimension(100, 200); + final Point imageLocation = new Point(10, 20); + final double scale = 4.0; + + // Clipping rectangle occludes top portion of image + final Rectangle clip = new Rectangle(0, 0, 100, 1000); + + ImageLayer uut = new ImageLayer(imageLocation, mockImage, mockComposite); + Mockito.when(mockImage.getWidth()).thenReturn(storedDimension.width); + Mockito.when(mockImage.getHeight()).thenReturn(storedDimension.height); + + uut.paint(mockGraphics, scale, clip); + + final Rectangle source = new Rectangle(0, 0, 25, 200); + final Rectangle projection = new Rectangle(40, 80, 100, 800); + + Mockito.verify(mockGraphics).setComposite(mockComposite); + Mockito.verify(mockGraphics).drawImage(mockImage, projection.x, projection.y, projection.x + projection.width, projection.y + projection.height, source.x, source.y, source.x + source.width, source.y + source.height, null); + } + + @Test + void testThatImagePaintsScaledWithOccludedBottomClippingRgn() { + final Dimension storedDimension = new Dimension(100, 200); + final Point imageLocation = new Point(10, 20); + final double scale = 4.0; + + // Clipping rectangle occludes top portion of image + final Rectangle clip = new Rectangle(0, 0, 1000, 100); + + ImageLayer uut = new ImageLayer(imageLocation, mockImage, mockComposite); + Mockito.when(mockImage.getWidth()).thenReturn(storedDimension.width); + Mockito.when(mockImage.getHeight()).thenReturn(storedDimension.height); + + uut.paint(mockGraphics, scale, clip); + + final Rectangle source = new Rectangle(0, 0, 100, 25); + final Rectangle projection = new Rectangle(40, 80, 400, 100); + + Mockito.verify(mockGraphics).setComposite(mockComposite); + Mockito.verify(mockGraphics).drawImage(mockImage, projection.x, projection.y, projection.x + projection.width, projection.y + projection.height, source.x, source.y, source.x + source.width, source.y + source.height, null); + } + + @Test + void testThatImagePaintsScaledWithFullyOccludedClippingRgn() { + final Dimension storedDimension = new Dimension(100, 200); + final Point imageLocation = new Point(10, 20); + final double scale = 4.0; + + // No portion of the image appears within the clipping rect + final Rectangle clip = new Rectangle(0, 0, 1, 1); + + ImageLayer uut = new ImageLayer(imageLocation, mockImage, mockComposite); + Mockito.when(mockImage.getWidth()).thenReturn(storedDimension.width); + Mockito.when(mockImage.getHeight()).thenReturn(storedDimension.height); + + uut.paint(mockGraphics, scale, clip); + + final Rectangle source = new Rectangle(0, 0, 0, 0); + final Rectangle projection = new Rectangle(40, 80, 0, 0); + + Mockito.verify(mockGraphics).setComposite(mockComposite); + Mockito.verify(mockGraphics).drawImage(mockImage, projection.x, projection.y, projection.x + projection.width, projection.y + projection.height, source.x, source.y, source.x + source.width, source.y + source.height, null); + } + +} diff --git a/src/test/java/com/defano/jmonet/tools/AirbrushToolTest.java b/src/test/java/com/defano/jmonet/tools/AirbrushToolTest.java new file mode 100644 index 00000000..3573c97c --- /dev/null +++ b/src/test/java/com/defano/jmonet/tools/AirbrushToolTest.java @@ -0,0 +1,46 @@ +package com.defano.jmonet.tools; + +import com.defano.jmonet.tools.base.MockitoToolTest; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockito.Mockito; + +import java.awt.*; +import java.awt.geom.Line2D; + +import static org.mockito.Matchers.argThat; +import static org.mockito.Matchers.eq; + +public class AirbrushToolTest extends MockitoToolTest { + + @BeforeEach + public void setUp() { + initialize(new AirbrushTool()); + } + + @Test + public void testThatDefaultCursorIsSet() { + Mockito.verify(mockCursorManager).setToolCursor(argThat(matchesCursor(new Cursor(Cursor.CROSSHAIR_CURSOR))), eq(mockCanvas)); + } + + @Test + public void testThatPointInPathIsAdded() { + // Setup + final Stroke stroke = new BasicStroke(10); + final Paint fillPaint = Color.BLUE; + final Point lastPoint = new Point(10, 10); + final Point thisPoint = new Point(20, 20); + + Mockito.when(mockToolAttributes.getIntensity()).thenReturn(1.0); + + // Run the test + uut.addPoint(mockScratch, stroke, fillPaint, lastPoint, thisPoint); + + // Verify the results + Mockito.verify(mockAddScratchGraphics).setStroke(stroke); + Mockito.verify(mockAddScratchGraphics).setPaint(fillPaint); + Mockito.verify(mockAddScratchGraphics).setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_OVER, 1.0f)); + Mockito.verify(mockAddScratchGraphics).draw(argThat(matchesShape(new Line2D.Float(lastPoint, thisPoint)))); + } + +} diff --git a/src/test/java/com/defano/jmonet/tools/ArrowToolTest.java b/src/test/java/com/defano/jmonet/tools/ArrowToolTest.java new file mode 100644 index 00000000..ab9db5fe --- /dev/null +++ b/src/test/java/com/defano/jmonet/tools/ArrowToolTest.java @@ -0,0 +1,24 @@ +package com.defano.jmonet.tools; + +import com.defano.jmonet.tools.base.MockitoToolTest; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockito.Mockito; + +import java.awt.*; + +import static org.mockito.Matchers.argThat; +import static org.mockito.Matchers.eq; + +public class ArrowToolTest extends MockitoToolTest { + + @BeforeEach + public void setUp() { + initialize(new ArrowTool()); + } + + @Test + public void testThatDefaultCursorIsSet() { + Mockito.verify(mockCursorManager).setToolCursor(argThat(matchesCursor(Cursor.getDefaultCursor())), eq(mockCanvas)); + } +} diff --git a/src/test/java/com/defano/jmonet/tools/EraserToolTest.java b/src/test/java/com/defano/jmonet/tools/EraserToolTest.java new file mode 100644 index 00000000..06582b7b --- /dev/null +++ b/src/test/java/com/defano/jmonet/tools/EraserToolTest.java @@ -0,0 +1,46 @@ +package com.defano.jmonet.tools; + +import com.defano.jmonet.tools.base.MockitoToolTest; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockito.Mockito; + +import java.awt.*; +import java.awt.geom.Line2D; + +import static org.mockito.Matchers.argThat; +import static org.mockito.Matchers.eq; + +public class EraserToolTest extends MockitoToolTest { + + @BeforeEach + public void setUp() { + initialize(new EraserTool()); + } + + @Test + public void testThatInitialPathIsErased() { + Stroke stroke = new BasicStroke(1); + Point initialPoint = new Point(10, 10); + + uut.startPath(mockScratch, stroke, null, initialPoint); + Mockito.verify(mockScratch).erase(eq(uut), argThat(matchesShape(new Line2D.Float(initialPoint, initialPoint))), eq(stroke)); + } + + @Test + public void testThatSubsequentPointsOnPathAreErased() { + Stroke stroke = new BasicStroke(1); + Point initialPoint = new Point(10, 10); + Point thisPoint = new Point(20, 20); + + uut.addPoint(mockScratch, stroke, null, initialPoint, thisPoint); + + Mockito.verify(mockScratch).erase(eq(uut), argThat(matchesShape(new Line2D.Float(initialPoint, thisPoint))), eq(stroke)); + } + + @Test + public void testThatCompletePathDoesNothing() { + uut.completePath(mockScratch, null, null, null); + Mockito.verifyZeroInteractions(mockScratch); + } +} diff --git a/src/test/java/com/defano/jmonet/tools/FillToolTest.java b/src/test/java/com/defano/jmonet/tools/FillToolTest.java new file mode 100644 index 00000000..cf46be9b --- /dev/null +++ b/src/test/java/com/defano/jmonet/tools/FillToolTest.java @@ -0,0 +1,59 @@ +package com.defano.jmonet.tools; + +import com.defano.jmonet.tools.attributes.BoundaryFunction; +import com.defano.jmonet.tools.attributes.FillFunction; +import com.defano.jmonet.tools.base.MockitoToolTest; +import com.defano.jmonet.tools.cursors.CursorFactory; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockito.Mock; +import org.mockito.Mockito; + +import java.awt.*; +import java.awt.image.BufferedImage; +import java.util.Optional; + +import static org.mockito.Matchers.argThat; +import static org.mockito.Matchers.eq; + +public class FillToolTest extends MockitoToolTest { + + @Mock private BoundaryFunction mockBoundaryFunction; + @Mock private FillFunction mockFillFunction; + @Mock private Paint mockFillPaint; + @Mock private BufferedImage mockFilledImage; + + @BeforeEach + public void setUp() { + initialize(new FillTool()); + } + + @Test + public void testThatDefaultCursorIsSet() { + Mockito.verify(mockCursorManager).setToolCursor(argThat(matchesCursor(CursorFactory.makeBucketCursor())), eq(mockCanvas)); + } + + @Test + public void testThatMouseDownFloodFills() { + Point floodOrigin = new Point(); + Dimension canvasSize = new Dimension(); + + Mockito.when(mockToolAttributes.getBoundaryFunction()).thenReturn(mockBoundaryFunction); + Mockito.when(mockToolAttributes.getFillPaint()).thenReturn(Optional.of(mockFillPaint)); + Mockito.when(mockToolAttributes.getFillFunction()).thenReturn(mockFillFunction); + Mockito.when(mockFloodFillTransform.apply(mockCanvasImage)).thenReturn(mockFilledImage); + Mockito.when(mockCanvas.getCanvasSize()).thenReturn(canvasSize); + + uut.mousePressed(null, floodOrigin); + + Mockito.verify(mockFloodFillTransform).setBoundaryFunction(mockBoundaryFunction); + Mockito.verify(mockFloodFillTransform).setFillPaint(mockFillPaint); + Mockito.verify(mockFloodFillTransform).setFill(mockFillFunction); + Mockito.verify(mockFloodFillTransform).setOrigin(floodOrigin); + + Mockito.verify(mockScratch).setAddScratch(eq(mockFilledImage), argThat(matchesShape(new Rectangle(canvasSize)))); + + Mockito.verify(mockCanvas).commit(); + Mockito.verify(mockCanvas).repaint(); + } +} \ No newline at end of file diff --git a/src/test/java/com/defano/jmonet/tools/LineToolTest.java b/src/test/java/com/defano/jmonet/tools/LineToolTest.java new file mode 100644 index 00000000..1777442f --- /dev/null +++ b/src/test/java/com/defano/jmonet/tools/LineToolTest.java @@ -0,0 +1,37 @@ +package com.defano.jmonet.tools; + +import com.defano.jmonet.tools.base.MockitoToolTest; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockito.Mock; +import org.mockito.Mockito; + +import java.awt.*; +import java.awt.geom.Line2D; + +import static org.mockito.Matchers.argThat; +import static org.mockito.Matchers.eq; + +public class LineToolTest extends MockitoToolTest { + + @Mock private Stroke mockStroke; + @Mock private Paint mockPaint; + + @BeforeEach + public void setup() { + initialize(new LineTool()); + } + + @Test + public void testThatLineIsDrawn() { + Line2D.Float l = new Line2D.Float(10, 20, 30, 40); + + uut.drawLine(mockScratch, mockStroke, mockPaint, (int) l.x1, (int) l.y1, (int) l.x2, (int) l.y2); + + Mockito.verify(mockScratch).getAddScratchGraphics(eq(uut), eq(mockStroke), argThat(matchesShape(l))); + Mockito.verify(mockAddScratchGraphics).setPaint(mockPaint); + Mockito.verify(mockAddScratchGraphics).setStroke(mockStroke); + Mockito.verify(mockAddScratchGraphics).draw(argThat(matchesShape(l))); + } + +} diff --git a/src/test/java/com/defano/jmonet/tools/PencilToolTest.java b/src/test/java/com/defano/jmonet/tools/PencilToolTest.java new file mode 100644 index 00000000..d5b41115 --- /dev/null +++ b/src/test/java/com/defano/jmonet/tools/PencilToolTest.java @@ -0,0 +1,97 @@ +package com.defano.jmonet.tools; + +import com.defano.jmonet.tools.attributes.MarkPredicate; +import com.defano.jmonet.tools.base.MockitoToolTest; +import com.defano.jmonet.tools.cursors.CursorFactory; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockito.Mockito; + +import java.awt.*; +import java.awt.geom.Line2D; + +import static org.mockito.Matchers.argThat; +import static org.mockito.Matchers.eq; + +public class PencilToolTest extends MockitoToolTest { + + @BeforeEach + public void setUp() { + initialize(new PencilTool()); + } + + @Test + public void testDefaultCursor() { + Mockito.verify(mockCursorManager).setToolCursor(argThat(matchesCursor(CursorFactory.makePencilCursor())), eq(mockCanvas)); + } + + @Test + public void testThatInitialPointIsDrawn() { + // Setup + final Stroke stroke = new BasicStroke(1); + final Paint fillPaint = Color.blue; + final Point initialPoint = new Point(0,0); + final Point thisPoint = new Point(10,10); + + // 0-alpha puts pencil in draw mode + Mockito.when(mockCanvasImage.getRGB(initialPoint.x, initialPoint.y)).thenReturn(new Color(0, 0, 0, 0).getRGB()); + + // Run the test + uut.addPoint(mockScratch, stroke, fillPaint, initialPoint, thisPoint); + + // Verify the results + Mockito.verify(mockScratch).getAddScratchGraphics(eq(uut), eq(new BasicStroke(1)), argThat(matchesShape(new Line2D.Float(initialPoint, thisPoint)))); + Mockito.verify(mockAddScratchGraphics).setStroke(new BasicStroke(1)); + Mockito.verify(mockAddScratchGraphics).setPaint(fillPaint); + Mockito.verify(mockAddScratchGraphics).draw(argThat(matchesShape(new Line2D.Float(initialPoint, thisPoint)))); + } + + + @Test + public void testThatSubsequentPointsAreDrawn() { + // Setup + final Stroke stroke = new BasicStroke(1); + final Paint fillPaint = Color.blue; + final Point initialPoint = new Point(0,0); + + // Assume no pixels are marked + Mockito.when(mockToolAttributes.getMarkPredicate()).thenReturn(new MarkPredicate() { + @Override + public boolean isMarked(Color pixel, Color eraseColor) { + return false; + } + }); + + // Run the test + uut.startPath(mockScratch, stroke, fillPaint, initialPoint); + + // Verify the results + Mockito.verify(mockScratch).getAddScratchGraphics(eq(uut), eq(new BasicStroke(1)), argThat(matchesShape(new Line2D.Float(initialPoint, initialPoint)))); + Mockito.verify(mockAddScratchGraphics).setStroke(new BasicStroke(1)); + Mockito.verify(mockAddScratchGraphics).setPaint(fillPaint); + Mockito.verify(mockAddScratchGraphics).draw(argThat(matchesShape(new Line2D.Float(initialPoint, initialPoint)))); + } + + @Test + public void testThatInitialPointIsErased() { + // Setup + final Stroke stroke = new BasicStroke(1); + final Paint fillPaint = Color.blue; + final Point initialPoint = new Point(0,0); + + // Assume all pixels are marked + Mockito.when(mockToolAttributes.getMarkPredicate()).thenReturn(new MarkPredicate() { + @Override + public boolean isMarked(Color pixel, Color eraseColor) { + return true; + } + }); + + // Run the test + uut.startPath(mockScratch, stroke, fillPaint, initialPoint); + + // Verify the results + Mockito.verify(mockScratch).erase(eq(uut), argThat(matchesShape(new Line2D.Float(initialPoint, initialPoint))), eq(new BasicStroke(1))); + Mockito.verifyZeroInteractions(mockAddScratchGraphics); + } +} diff --git a/src/test/java/com/defano/jmonet/tools/RectangleToolTest.java b/src/test/java/com/defano/jmonet/tools/RectangleToolTest.java new file mode 100644 index 00000000..b3f7e932 --- /dev/null +++ b/src/test/java/com/defano/jmonet/tools/RectangleToolTest.java @@ -0,0 +1,50 @@ +package com.defano.jmonet.tools; + +import com.defano.jmonet.tools.base.MockitoToolTest; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockito.Mockito; + +import java.awt.*; + +import static org.mockito.Matchers.argThat; +import static org.mockito.Matchers.eq; + +public class RectangleToolTest extends MockitoToolTest { + + @BeforeEach + public void setUp() { + initialize(new RectangleTool()); + } + + @Test + public void testDefaultCursor() { + Mockito.verify(mockCursorManager).setToolCursor(argThat(matchesCursor(new Cursor(Cursor.CROSSHAIR_CURSOR))), eq(mockCanvas)); + } + + @Test + public void testThatRectangleIsStroked() { + Rectangle bounds = new Rectangle(1, 2, 3, 4); + Paint fill = Color.black; + Stroke stroke = new BasicStroke(1); + + uut.strokeBounds(mockScratch, stroke, fill, bounds, false); + + Mockito.verify(mockScratch).getAddScratchGraphics(eq(uut), eq(stroke), argThat(matchesShape(bounds))); + Mockito.verify(mockAddScratchGraphics).setStroke(stroke); + Mockito.verify(mockAddScratchGraphics).setPaint(fill); + Mockito.verify(mockAddScratchGraphics).draw(bounds); + } + + @Test + public void testThatRectangleIsFilled() { + Rectangle bounds = new Rectangle(1, 2, 3, 4); + Paint fill = Color.black; + + uut.fillBounds(mockScratch, fill, bounds, false); + + Mockito.verify(mockScratch).getAddScratchGraphics(eq(uut), argThat(matchesShape(bounds))); + Mockito.verify(mockAddScratchGraphics).setPaint(fill); + Mockito.verify(mockAddScratchGraphics).fill(bounds); + } +} diff --git a/src/test/java/com/defano/jmonet/tools/base/BaseToolTest.java b/src/test/java/com/defano/jmonet/tools/base/BaseToolTest.java new file mode 100644 index 00000000..f9cf175e --- /dev/null +++ b/src/test/java/com/defano/jmonet/tools/base/BaseToolTest.java @@ -0,0 +1,51 @@ +package com.defano.jmonet.tools.base; + +import com.defano.jmonet.canvas.PaintCanvas; +import com.defano.jmonet.canvas.Scratch; +import com.defano.jmonet.context.GraphicsContext; +import com.defano.jmonet.model.Interpolation; +import com.defano.jmonet.tools.attributes.ToolAttributes; +import com.defano.jmonet.tools.cursors.CursorManager; +import com.google.inject.AbstractModule; +import com.google.inject.Guice; +import org.mockito.Answers; +import org.mockito.Mock; +import org.mockito.Mockito; + +import java.awt.*; + +public class BaseToolTest extends MockitoTest { + + protected ToolType uut; + + @Mock(answer = Answers.RETURNS_DEEP_STUBS) protected CursorManager mockCursorManager; + @Mock(answer = Answers.RETURNS_DEEP_STUBS) protected ToolAttributes mockToolAttributes; + @Mock(answer = Answers.RETURNS_DEEP_STUBS) protected Cursor mockCursor; + @Mock(answer = Answers.RETURNS_DEEP_STUBS) protected PaintCanvas mockCanvas; + @Mock(answer = Answers.RETURNS_DEEP_STUBS) protected Scratch mockScratch; + @Mock(answer = Answers.RETURNS_DEEP_STUBS) protected GraphicsContext mockAddScratch; + @Mock(answer = Answers.RETURNS_DEEP_STUBS) protected GraphicsContext mockRemoveScratch; + + // Can't mock enums + protected Interpolation expectedInterpolation = Interpolation.BICUBIC; + + protected void setup(ToolType uut) { + super.setup(); + + this.uut = uut; + Guice.createInjector(new BaseToolTest.BasicToolAssembly()).injectMembers(this.uut); + + Mockito.when(mockToolAttributes.getAntiAliasing()).thenReturn(expectedInterpolation); + Mockito.when(mockCanvas.getScratch()).thenReturn(mockScratch); + Mockito.when(mockScratch.getAddScratchGraphics(uut, null)).thenReturn(mockAddScratch); + Mockito.when(mockScratch.getRemoveScratchGraphics(uut, null)).thenReturn(mockRemoveScratch); + } + + private class BasicToolAssembly extends AbstractModule { + @Override + protected void configure() { + bind(CursorManager.class).toInstance(mockCursorManager); + bind(ToolAttributes.class).toInstance(mockToolAttributes); + } + } +} diff --git a/src/test/java/com/defano/jmonet/tools/base/BasicToolTest.java b/src/test/java/com/defano/jmonet/tools/base/BasicToolTest.java new file mode 100644 index 00000000..8a24df47 --- /dev/null +++ b/src/test/java/com/defano/jmonet/tools/base/BasicToolTest.java @@ -0,0 +1,99 @@ +package com.defano.jmonet.tools.base; + +import com.defano.jmonet.model.PaintToolType; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockito.Mockito; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.Matchers.any; +import static org.mockito.Matchers.eq; + +class BasicToolTest extends BaseToolTest { + + @BeforeEach + protected void setup() { + super.setup(new BasicTool(null)); + } + + @Test + void testThatGetToolTypeReturnsToolType() { + for (PaintToolType thisType : PaintToolType.values()) { + assertEquals(thisType, new BasicTool(thisType).getPaintToolType()); + } + } + + @Test + void testThatNewToolIsNotActive() { + assertFalse(new BasicTool<>(null).isActive()); + } + + @Test + void testThatActivatedToolIsActive() { + uut.activate(mockCanvas); + assertTrue(uut.isActive()); + } + + @Test + void testThatDeactivatedToolIsNotActive() { + uut.activate(mockCanvas); + assertTrue(uut.isActive()); + + uut.deactivate(); + assertFalse(uut.isActive()); + } + + @Test + void testThatActivationSetsSurfaceInteractionListener() { + uut.activate(mockCanvas); + Mockito.verify(mockCanvas).addSurfaceInteractionObserver(uut); + } + + @Test + void testThatActivationSetsCursor() { + uut.activate(mockCanvas); + Mockito.verify(mockCursorManager).setToolCursor(any(), eq(mockCanvas)); + } + + @Test + void testThatSetCursorDelegatesToCursorManager() { + uut.activate(mockCanvas); + uut.setToolCursor(mockCursor); + + Mockito.verify(mockCursorManager).setToolCursor(mockCursor, mockCanvas); + } + + @Test + void testThatGetCursorDelegatesToCursorManager() { + Mockito.when(mockCursorManager.getToolCursor()).thenReturn(mockCursor); + assertEquals(mockCursor, uut.getToolCursor()); + Mockito.verify(mockCursorManager).getToolCursor(); + } + + @Test + void testThatGetCanvasThrowsWhenNotActive() { + assertThrows(IllegalStateException.class, () -> uut.getCanvas()); + } + + @Test + void testThatCanvasIsReturnedWhenActive() { + uut.activate(mockCanvas); + assertEquals(mockCanvas, uut.getCanvas()); + } + + @Test + void testThatInjectedAttributesAreReturned() { + assertEquals(mockToolAttributes, uut.getAttributes()); + } + + @Test + void testThatProperlyConfiguredScratchIsReturned() { + + uut.activate(mockCanvas); + assertEquals(mockScratch, uut.getScratch()); + + Mockito.verify(mockAddScratch).setAntialiasingMode(expectedInterpolation); + Mockito.verify(mockRemoveScratch).setAntialiasingMode(expectedInterpolation); + } + +} \ No newline at end of file diff --git a/src/test/java/com/defano/jmonet/tools/base/BoundsToolTest.java b/src/test/java/com/defano/jmonet/tools/base/BoundsToolTest.java new file mode 100644 index 00000000..3724271b --- /dev/null +++ b/src/test/java/com/defano/jmonet/tools/base/BoundsToolTest.java @@ -0,0 +1,171 @@ +package com.defano.jmonet.tools.base; + +import com.defano.jmonet.tools.util.MathUtils; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockito.Mock; +import org.mockito.Mockito; + +import java.awt.*; +import java.awt.event.MouseEvent; +import java.util.Optional; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +class BoundsToolTest extends BaseToolTest { + + @Mock private MouseEvent mockEvent; + @Mock private Point mockPoint; + @Mock private BoundsToolDelegate mockDelegate; + @Mock private Stroke mockStroke; + @Mock private Paint mockStrokePaint; + @Mock private Paint mockFillPaint; + + @BeforeEach + protected void setup() { + super.setup(new BoundsTool(null)); + } + + @Test + void testThatCrosshairIsDefaultCursor() { + assertTrue(new CursorMatcher(new Cursor(Cursor.CROSSHAIR_CURSOR)).matches(uut.getDefaultCursor())); + } + + @Test + void testThatMousePressedSetsInitialBound() { + uut.mousePressed(mockEvent, mockPoint); + assertEquals(mockPoint, uut.getInitialPoint()); + } + + @Test + void testThatMouseReleasedCommitsScratchToCanvas() { + uut.activate(mockCanvas); + uut.mouseReleased(mockEvent, mockPoint); + + Mockito.verify(mockCanvas).commit(); + } + + @Test + void testThatDrawMultipleDoesNotClearScratch() { + Mockito.when(mockToolAttributes.getFillPaint()).thenReturn(Optional.empty()); + Mockito.when(mockToolAttributes.isDrawMultiple()).thenReturn(true); + + uut.setDelegate(mockDelegate); + uut.activate(mockCanvas); + uut.mousePressed(mockEvent, mockPoint); + uut.mouseDragged(mockEvent, mockPoint); + + Mockito.verify(mockScratch, Mockito.never()).clear(); + } + + @Test + void thatMouseDraggedDrawsStrokedBounds() { + Point startPoint = new Point(10, 10); + Point endPoint = new Point(100, 100); + + Mockito.when(mockToolAttributes.getFillPaint()).thenReturn(Optional.empty()); + Mockito.when(mockToolAttributes.getStrokePaint()).thenReturn(mockStrokePaint); + Mockito.when(mockToolAttributes.getStroke()).thenReturn(mockStroke); + + uut.setDelegate(mockDelegate); + uut.activate(mockCanvas); + uut.mousePressed(mockEvent, startPoint); + uut.mouseDragged(mockEvent, endPoint); + + Mockito.verify(mockDelegate).strokeBounds( + mockScratch, + mockStroke, + mockStrokePaint, + new Rectangle(startPoint, new Dimension(endPoint.x - startPoint.x, endPoint.y - startPoint.y)), + false); + } + + @Test + void thatMouseDraggedDrawsSquareWhenShiftIsDown() { + Point startPoint = new Point(10, 10); + Point endPoint = new Point(100, 200); + + Mockito.when(mockToolAttributes.getFillPaint()).thenReturn(Optional.empty()); + Mockito.when(mockToolAttributes.getStrokePaint()).thenReturn(mockStrokePaint); + Mockito.when(mockToolAttributes.getStroke()).thenReturn(mockStroke); + Mockito.when(mockEvent.isShiftDown()).thenReturn(true); + + uut.setDelegate(mockDelegate); + uut.activate(mockCanvas); + uut.mousePressed(mockEvent, startPoint); + uut.mouseDragged(mockEvent, endPoint); + + Mockito.verify(mockDelegate).strokeBounds( + mockScratch, + mockStroke, + mockStrokePaint, + MathUtils.square(startPoint, endPoint), + true); + } + + @Test + void thatMouseDraggedDrawsFromCenter() { + Point startPoint = new Point(10, 10); + Point endPoint = new Point(20, 20); + + Mockito.when(mockToolAttributes.getFillPaint()).thenReturn(Optional.empty()); + Mockito.when(mockToolAttributes.getStrokePaint()).thenReturn(mockStrokePaint); + Mockito.when(mockToolAttributes.getStroke()).thenReturn(mockStroke); + Mockito.when(mockToolAttributes.isDrawCentered()).thenReturn(true); + + uut.setDelegate(mockDelegate); + uut.activate(mockCanvas); + uut.mousePressed(mockEvent, startPoint); + uut.mouseDragged(mockEvent, endPoint); + + Mockito.verify(mockDelegate).strokeBounds( + mockScratch, + mockStroke, + mockStrokePaint, + new Rectangle(5, 5, 15, 15), + false); + } + + @Test + void thatMouseDraggedDrawsFilledBounds() { + Point startPoint = new Point(10, 10); + Point endPoint = new Point(100, 100); + + Mockito.when(mockToolAttributes.getFillPaint()).thenReturn(Optional.of(mockFillPaint)); + Mockito.when(mockToolAttributes.getStrokePaint()).thenReturn(mockStrokePaint); + Mockito.when(mockToolAttributes.getStroke()).thenReturn(mockStroke); + + uut.setDelegate(mockDelegate); + uut.activate(mockCanvas); + uut.mousePressed(mockEvent, startPoint); + uut.mouseDragged(mockEvent, endPoint); + + Mockito.verify(mockDelegate).fillBounds( + mockScratch, + mockFillPaint, + new Rectangle(startPoint, new Dimension(endPoint.x - startPoint.x, endPoint.y - startPoint.y)), + false + ); + + Mockito.verify(mockDelegate).strokeBounds( + mockScratch, + mockStroke, + mockStrokePaint, + new Rectangle(startPoint, new Dimension(endPoint.x - startPoint.x, endPoint.y - startPoint.y)), + false + ); + + assertEquals(startPoint, uut.getInitialPoint()); + assertEquals(endPoint, uut.getCurrentPoint()); + } + + @Test + void testThatMouseMovedUpdatesCursor() { + Mockito.when(mockCursorManager.getToolCursor()).thenReturn(mockCursor); + uut.activate(mockCanvas); + uut.mouseMoved(mockEvent, mockPoint); + Mockito.verify(mockCursorManager).setToolCursor(mockCursor, mockCanvas); + } + +} \ No newline at end of file diff --git a/src/test/java/com/defano/jmonet/tools/base/ColorMatcher.java b/src/test/java/com/defano/jmonet/tools/base/ColorMatcher.java new file mode 100644 index 00000000..603b385a --- /dev/null +++ b/src/test/java/com/defano/jmonet/tools/base/ColorMatcher.java @@ -0,0 +1,41 @@ +package com.defano.jmonet.tools.base; + +import org.mockito.ArgumentMatcher; + +import java.awt.*; + +public class ColorMatcher extends ArgumentMatcher { + + private final Color c1; + private boolean ignoreAlpha = false; + + public ColorMatcher(Color c1) { + this.c1 = c1; + } + + public ColorMatcher ignoringAlpha() { + this.ignoreAlpha = true; + return this; + } + + @Override + public boolean matches(Object o) { + + if (o instanceof Color) { + Color c2 = (Color) o; + + if (ignoreAlpha) { + return c1.getRed() == c2.getRed() && + c1.getGreen() == c2.getGreen() && + c1.getBlue() == c2.getBlue(); + } else { + return c1.getRed() == c2.getRed() && + c1.getGreen() == c2.getGreen() && + c1.getBlue() == c2.getBlue() && + c1.getAlpha() == c2.getAlpha(); + } + } + + return false; + } +} diff --git a/src/test/java/com/defano/jmonet/tools/base/CursorMatcher.java b/src/test/java/com/defano/jmonet/tools/base/CursorMatcher.java new file mode 100644 index 00000000..ffe7cd1a --- /dev/null +++ b/src/test/java/com/defano/jmonet/tools/base/CursorMatcher.java @@ -0,0 +1,26 @@ +package com.defano.jmonet.tools.base; + +import org.mockito.ArgumentMatcher; + +import java.awt.*; + +public class CursorMatcher extends ArgumentMatcher { + + private final Cursor c1; + + public CursorMatcher(Cursor c1) { + this.c1 = c1; + } + + @Override + public boolean matches(Object o) { + + if (o instanceof Cursor) { + Cursor c2 = (Cursor) o; + + return c2.getName().equals(c1.getName()); + } + + return false; + } +} diff --git a/src/test/java/com/defano/jmonet/tools/base/LinearToolTest.java b/src/test/java/com/defano/jmonet/tools/base/LinearToolTest.java new file mode 100644 index 00000000..00215ccc --- /dev/null +++ b/src/test/java/com/defano/jmonet/tools/base/LinearToolTest.java @@ -0,0 +1,99 @@ +package com.defano.jmonet.tools.base; + +import com.defano.jmonet.tools.util.MathUtils; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockito.Mock; +import org.mockito.Mockito; + +import java.awt.*; +import java.awt.event.MouseEvent; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +class LinearToolTest extends BaseToolTest { + + @Mock private MouseEvent mockEvent; + @Mock private Point mockPoint; + @Mock private LinearToolDelegate mockDelegate; + @Mock private Stroke mockStroke; + @Mock private Paint mockPaint; + + @BeforeEach + protected void setup() { + super.setup(new LinearTool(null)); + } + + @Test + void testThatCrosshairIsDefaultCursor() { + assertTrue(new CursorMatcher(new Cursor(Cursor.CROSSHAIR_CURSOR)).matches(uut.getDefaultCursor())); + } + + @Test + void testThatMouseMovedUpdatesCursor() { + Mockito.when(mockCursorManager.getToolCursor()).thenReturn(mockCursor); + uut.activate(mockCanvas); + uut.mouseMoved(mockEvent, mockPoint); + Mockito.verify(mockCursorManager).setToolCursor(mockCursor, mockCanvas); + } + + @Test + void testThatMousePressedSetsInitialPoint() { + uut.mousePressed(mockEvent, mockPoint); + assertEquals(mockPoint, uut.getInitialPoint()); + } + + @Test + void testThatMouseReleasedCommitsLine() { + uut.activate(mockCanvas); + uut.mouseReleased(mockEvent, mockPoint); + + Mockito.verify(mockCanvas).commit(); + } + + @Test + void testThatMouseDraggedDrawsLine() { + Point start = new Point(10, 10); + Point end = new Point(100, 100); + + Mockito.when(mockToolAttributes.getStroke()).thenReturn(mockStroke); + Mockito.when(mockToolAttributes.getStrokePaint()).thenReturn(mockPaint); + + uut.setDelegate(mockDelegate); + uut.activate(mockCanvas); + uut.mousePressed(mockEvent, start); + uut.mouseDragged(mockEvent, end); + + Mockito.inOrder(mockScratch, mockDelegate, mockCanvas); + + Mockito.verify(mockScratch).clear(); + Mockito.verify(mockDelegate).drawLine(mockScratch, mockStroke, mockPaint, start.x, start.y, end.x, end.y); + Mockito.verify(mockCanvas).repaint(); + } + + @Test + void testThatMouseDraggedDrawsConstrainedLineWhenShiftIsDown() { + Point start = new Point(10, 10); + Point end = new Point(100, 100); + + Mockito.when(mockToolAttributes.getStroke()).thenReturn(mockStroke); + Mockito.when(mockToolAttributes.getStrokePaint()).thenReturn(mockPaint); + Mockito.when(mockEvent.isShiftDown()).thenReturn(true); + Mockito.when(mockToolAttributes.getConstrainedAngle()).thenReturn(17); + + uut.setDelegate(mockDelegate); + uut.activate(mockCanvas); + uut.mousePressed(mockEvent, start); + uut.mouseDragged(mockEvent, end); + + Point expectedEnd = MathUtils.line(start, end, mockToolAttributes.getConstrainedAngle()); + + Mockito.inOrder(mockScratch, mockDelegate, mockCanvas); + + Mockito.verify(mockScratch).clear(); + Mockito.verify(mockDelegate).drawLine(mockScratch, mockStroke, mockPaint, start.x, start.y, expectedEnd.x, expectedEnd.y); + Mockito.verify(mockCanvas).repaint(); + } + +} \ No newline at end of file diff --git a/src/test/java/com/defano/jmonet/tools/base/MockitoTest.java b/src/test/java/com/defano/jmonet/tools/base/MockitoTest.java new file mode 100644 index 00000000..0b4e9675 --- /dev/null +++ b/src/test/java/com/defano/jmonet/tools/base/MockitoTest.java @@ -0,0 +1,12 @@ +package com.defano.jmonet.tools.base; + +import org.junit.jupiter.api.BeforeEach; +import org.mockito.MockitoAnnotations; + +public class MockitoTest { + + @BeforeEach + protected void setup() { + MockitoAnnotations.initMocks(this); + } +} diff --git a/src/test/java/com/defano/jmonet/tools/base/MockitoToolTest.java b/src/test/java/com/defano/jmonet/tools/base/MockitoToolTest.java new file mode 100644 index 00000000..f88ce591 --- /dev/null +++ b/src/test/java/com/defano/jmonet/tools/base/MockitoToolTest.java @@ -0,0 +1,75 @@ +package com.defano.jmonet.tools.base; + +import com.defano.jmonet.canvas.JMonetCanvas; +import com.defano.jmonet.canvas.Scratch; +import com.defano.jmonet.context.GraphicsContext; +import com.defano.jmonet.model.Interpolation; +import com.defano.jmonet.tools.attributes.ToolAttributes; +import com.defano.jmonet.tools.cursors.CursorManager; +import com.defano.jmonet.transform.image.FloodFillTransform; +import com.google.inject.AbstractModule; +import com.google.inject.Guice; +import org.mockito.*; + +import java.awt.*; +import java.awt.image.BufferedImage; + +import static org.mockito.Matchers.*; + +public class MockitoToolTest extends MockitoTest { + + protected T uut; + + @Mock(answer=Answers.RETURNS_DEEP_STUBS) protected Scratch mockScratch; + @Mock(answer=Answers.RETURNS_DEEP_STUBS) protected GraphicsContext mockAddScratchGraphics; + @Mock(answer=Answers.RETURNS_DEEP_STUBS) protected GraphicsContext mockRemoveScratchGraphics; + @Mock(answer=Answers.RETURNS_DEEP_STUBS) protected JMonetCanvas mockCanvas; + @Mock(answer=Answers.RETURNS_DEEP_STUBS) protected BufferedImage mockCanvasImage; + @Mock(answer=Answers.RETURNS_DEEP_STUBS) protected ToolAttributes mockToolAttributes; + @Mock(answer=Answers.RETURNS_DEEP_STUBS) protected CursorManager mockCursorManager; + @Mock(answer=Answers.RETURNS_DEEP_STUBS) protected FloodFillTransform mockFloodFillTransform; + + public void initialize(T uut) { + MockitoAnnotations.initMocks(this); + + this.uut = uut; + Guice.createInjector(new MockToolAssembly()).injectMembers(uut); + this.uut.activate(mockCanvas); + + Mockito.when(mockCanvas.getCanvasImage()).thenReturn(mockCanvasImage); + Mockito.when(mockCanvas.getScratch()).thenReturn(mockScratch); + + // Provide mock add scratch + Mockito.when(mockScratch.getAddScratchGraphics(any(), any(), any())).thenReturn(mockAddScratchGraphics); + Mockito.when(mockScratch.getAddScratchGraphics(any(), any())).thenReturn(mockAddScratchGraphics); + + // Provide mock remove scratch + Mockito.when(mockScratch.getRemoveScratchGraphics(any(), any(), any())).thenReturn(mockRemoveScratchGraphics); + Mockito.when(mockScratch.getRemoveScratchGraphics(any(), any())).thenReturn(mockRemoveScratchGraphics); + Mockito.when(mockToolAttributes.getAntiAliasing()).thenReturn(Interpolation.NONE); + } + + protected static ShapeMatcher matchesShape(T t) { + return new ShapeMatcher<>(t); + } + + protected static CursorMatcher matchesCursor(Cursor c) { + return new CursorMatcher(c); + } + + protected static ColorMatcher matchesColor(Color c) { + return new ColorMatcher(c); + } + + private class MockToolAssembly extends AbstractModule { + + @Override + protected void configure() { + bind(ToolAttributes.class).toInstance(mockToolAttributes); + bind(CursorManager.class).toInstance(mockCursorManager); + bind(FloodFillTransform.class).toInstance(mockFloodFillTransform); + } + } + + +} diff --git a/src/test/java/com/defano/jmonet/tools/base/PathToolTest.java b/src/test/java/com/defano/jmonet/tools/base/PathToolTest.java new file mode 100644 index 00000000..e2e31473 --- /dev/null +++ b/src/test/java/com/defano/jmonet/tools/base/PathToolTest.java @@ -0,0 +1,76 @@ +package com.defano.jmonet.tools.base; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockito.Mock; +import org.mockito.Mockito; + +import java.awt.*; +import java.awt.event.MouseEvent; + +import static org.junit.jupiter.api.Assertions.*; + +class PathToolTest extends BaseToolTest { + + @Mock private MouseEvent mockEvent; + @Mock private Point mockPoint; + @Mock private PathToolDelegate mockDelegate; + @Mock private Stroke mockStroke; + @Mock private Paint mockPaint; + @Mock private Rectangle mockRegion; + + @BeforeEach + protected void setup() { + super.setup(new PathTool(null)); + } + + @Test + void testThatCrosshairIsDefaultCursor() { + assertTrue(new CursorMatcher(new Cursor(Cursor.CROSSHAIR_CURSOR)).matches(uut.getDefaultCursor())); + } + + @Test + void testThatMouseMovedUpdatesCursor() { + Mockito.when(mockCursorManager.getToolCursor()).thenReturn(mockCursor); + uut.activate(mockCanvas); + uut.mouseMoved(mockEvent, mockPoint); + Mockito.verify(mockCursorManager).setToolCursor(mockCursor, mockCanvas); + } + + @Test + void testThatMousePressedPaintsInitialPoint() { + uut.activate(mockCanvas); + uut.setDelegate(mockDelegate); + + Mockito.when(mockToolAttributes.getStroke()).thenReturn( mockStroke); + Mockito.when(mockToolAttributes.getStrokePaint()).thenReturn(mockPaint); + Mockito.when(mockScratch.getDirtyRegion()).thenReturn(mockRegion); + + uut.mousePressed(mockEvent, mockPoint); + + Mockito.verify(mockScratch).clear(); + Mockito.verify(mockDelegate).startPath(mockScratch, mockStroke, mockPaint, mockPoint); + Mockito.verify(mockCanvas).repaint(mockRegion); + } + + @Test + void testThatMouseDraggedPaintsPath() { + Point start = new Point(20, 20); + Point end = new Point(40, 30); + + Mockito.when(mockToolAttributes.getStroke()).thenReturn( mockStroke); + Mockito.when(mockToolAttributes.getStrokePaint()).thenReturn(mockPaint); + Mockito.when(mockScratch.getDirtyRegion()).thenReturn(mockRegion); + + uut.activate(mockCanvas); + uut.setDelegate(mockDelegate); + uut.mousePressed(mockEvent, start); + uut.mouseDragged(mockEvent, end); + + Mockito.inOrder(mockDelegate, mockCanvas); + + Mockito.verify(mockDelegate).addPoint(mockScratch, mockStroke, mockPaint, start, end); + Mockito.verify(mockCanvas, Mockito.times(2)).repaint(mockRegion); + + } +} \ No newline at end of file diff --git a/src/test/java/com/defano/jmonet/tools/base/PolylineToolTest.java b/src/test/java/com/defano/jmonet/tools/base/PolylineToolTest.java new file mode 100644 index 00000000..3d4670f0 --- /dev/null +++ b/src/test/java/com/defano/jmonet/tools/base/PolylineToolTest.java @@ -0,0 +1,102 @@ +package com.defano.jmonet.tools.base; + +import com.defano.jmonet.tools.util.MathUtils; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockito.Mock; +import org.mockito.Mockito; + +import java.awt.*; +import java.awt.event.MouseEvent; + +import static org.junit.jupiter.api.Assertions.*; + +class PolylineToolTest extends BaseToolTest { + + @Mock + private MouseEvent mockEvent; + @Mock private Point mockPoint; + @Mock private PolylineToolDelegate mockDelegate; + @Mock private Stroke mockStroke; + @Mock private Paint mockPaint; + @Mock private Rectangle mockRegion; + + @BeforeEach + protected void setup() { + super.setup(new PolylineTool(null)); + } + + @Test + void testThatCrosshairIsDefaultCursor() { + assertTrue(new CursorMatcher(new Cursor(Cursor.CROSSHAIR_CURSOR)).matches(uut.getDefaultCursor())); + } + + @Test + void testThatMouseMovedUpdatesCursor() { + Mockito.when(mockCursorManager.getToolCursor()).thenReturn(mockCursor); + uut.activate(mockCanvas); + uut.mouseMoved(mockEvent, mockPoint); + Mockito.verify(mockCursorManager).setToolCursor(mockCursor, mockCanvas); + } + + @Test + void testThatMouseMoveBeforeMousePressDoesNothing() { + uut.setDelegate(mockDelegate); + uut.mouseMoved(mockEvent, mockPoint); + + Mockito.verifyZeroInteractions(mockScratch); + Mockito.verifyZeroInteractions(mockDelegate); + Mockito.verifyZeroInteractions(mockCanvas); + } + + @Test + void testThatLineIsDrawnBetweenInitialPointAndMouseLoc() { + Point pressPoint = new Point(10, 10); + Point movePoint = new Point(20, 20); + + Mockito.when(mockToolAttributes.getStroke()).thenReturn(mockStroke); + Mockito.when(mockToolAttributes.getStrokePaint()).thenReturn(mockPaint); + + uut.activate(mockCanvas); + uut.setDelegate(mockDelegate); + uut.mousePressed(mockEvent, pressPoint); + uut.mouseMoved(mockEvent, movePoint); + + Mockito.verify(mockScratch).clear(); + Mockito.verify(mockDelegate).strokePolyline( + mockScratch, + mockStroke, + mockPaint, + new int[] {pressPoint.x, movePoint.x}, + new int[] {pressPoint.y, movePoint.y}); + Mockito.verify(mockCanvas).repaint(); + } + + @Test + void testThatLineIsDrawnConstrainedWhenShiftIsDown() { + Point pressPoint = new Point(10, 10); + Point movePoint = new Point(20, 20); + + Mockito.when(mockToolAttributes.getStroke()).thenReturn(mockStroke); + Mockito.when(mockToolAttributes.getStrokePaint()).thenReturn(mockPaint); + Mockito.when(mockEvent.isShiftDown()).thenReturn(true); + + uut.activate(mockCanvas); + uut.setDelegate(mockDelegate); + uut.mousePressed(mockEvent, pressPoint); + uut.mouseMoved(mockEvent, movePoint); + + Point constrainedPoint = MathUtils.line(pressPoint, movePoint, mockToolAttributes.getConstrainedAngle()); + + Mockito.verify(mockScratch).clear(); + Mockito.verify(mockDelegate).strokePolyline( + mockScratch, + mockStroke, + mockPaint, + new int[] {pressPoint.x, constrainedPoint.x}, + new int[] {pressPoint.y, constrainedPoint.y}); + Mockito.verify(mockCanvas).repaint(); + } + + +} \ No newline at end of file diff --git a/src/test/java/com/defano/jmonet/tools/base/ShapeMatcher.java b/src/test/java/com/defano/jmonet/tools/base/ShapeMatcher.java new file mode 100644 index 00000000..7746d326 --- /dev/null +++ b/src/test/java/com/defano/jmonet/tools/base/ShapeMatcher.java @@ -0,0 +1,73 @@ +package com.defano.jmonet.tools.base; + +import org.mockito.ArgumentMatcher; + +import java.awt.*; +import java.awt.geom.PathIterator; +import java.util.ArrayList; +import java.util.List; + +public class ShapeMatcher extends ArgumentMatcher { + + private final Shape s1; + private final double precision; + + public ShapeMatcher(Shape shape) { + this(shape, .0001); + } + + public ShapeMatcher(Shape shape, double precision) { + this.s1 = shape; + this.precision = precision; + } + + @Override + public boolean matches(Object o) { + if (o instanceof Shape) { + Shape s2 = (Shape) o; + + List s1Points = disassemble(s1); + List s2Points = disassemble(s2); + + return isEquivalent(s1Points, s2Points, precision); + } + + return false; + } + + private boolean isEquivalent(List l1, List l2, double precision) { + + if (l1.size() != l2.size()) { + return false; + } + + for (int i = 0; i < l1.size(); i++) { + double[] d1 = l1.get(i); + double[] d2 = l2.get(i); + + if (d1.length != d2.length) { + return false; + } + + for (int j = 0; j < d1.length; j++) { + if (Math.abs(d1[j] - d2[j]) > precision) { + return false; + } + } + } + + return true; + } + + private List disassemble(Shape s) { + ArrayList s1Points = new ArrayList<>(); + double[] coords = new double[6]; + + for (PathIterator pi = s.getPathIterator(null); !pi.isDone(); pi.next()) { + int type = pi.currentSegment(coords); + s1Points.add(new double[] {type, coords[0], coords[1]}); + } + + return s1Points; + } +} diff --git a/src/test/java/com/defano/jmonet/tools/util/MathUtilsTest.java b/src/test/java/com/defano/jmonet/tools/util/MathUtilsTest.java new file mode 100644 index 00000000..94ef6605 --- /dev/null +++ b/src/test/java/com/defano/jmonet/tools/util/MathUtilsTest.java @@ -0,0 +1,28 @@ +package com.defano.jmonet.tools.util; + +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.*; + +class MathUtilsTest { + + @Test + void testNearestFloor() { + assertEquals(30, MathUtils.nearestFloor(30, 10)); + assertEquals(30, MathUtils.nearestFloor(31, 10)); + assertEquals(30, MathUtils.nearestFloor(35, 10)); + assertEquals(40, MathUtils.nearestFloor(40, 10)); + } + + @Test + void testThatNearestFloorIgnoresZero() { + assertEquals(13, MathUtils.nearestFloor(13, 0)); + assertEquals(13, MathUtils.nearestFloor(13, 1)); + } + + @Test + void testThatNearestFloorHandlesNegatives() { + assertEquals(-10, MathUtils.nearestFloor(-15, 10)); + assertEquals(-10, MathUtils.nearestFloor(-15, -10)); + } +} \ No newline at end of file diff --git a/src/test/java/com/defano/jmonet/transform/pixel/BrightnessPixelTransformTest.java b/src/test/java/com/defano/jmonet/transform/pixel/BrightnessPixelTransformTest.java new file mode 100644 index 00000000..6eb74865 --- /dev/null +++ b/src/test/java/com/defano/jmonet/transform/pixel/BrightnessPixelTransformTest.java @@ -0,0 +1,45 @@ +package com.defano.jmonet.transform.pixel; + +import org.junit.jupiter.api.Test; + +import java.awt.*; + +import static org.junit.jupiter.api.Assertions.*; + +class BrightnessPixelTransformTest { + + @Test + void testThatZeroDeltaLeavesColorUnchanged() { + assertEquals(Color.black.getRGB(), + new BrightnessPixelTransform(0).apply(Color.black.getRGB())); + } + + @Test + void testThatUnsaturatedColorIsTransformed() { + final int delta = 10; + + assertEquals(new Color( + Color.black.getRed() + delta, + Color.black.getGreen() + delta, + Color.black.getBlue() + delta + ).getRGB(), new BrightnessPixelTransform(delta).apply(Color.black.getRGB())); + } + + @Test + void testThatFullyWhiteColorRemainUnchangedWhenBrightnessAdded() { + assertEquals(Color.white.getRGB(), new BrightnessPixelTransform(10).apply(Color.white.getRGB())); + } + + @Test + void testThatSaturatedColorComponentsAreNotModified() { + Color c = new Color(255, 100, 255, 20); + final int delta = 20; + + assertEquals(new Color( + c.getRed(), + c.getGreen() + delta, + c.getBlue(), + c.getAlpha() + ).getRGB(), new BrightnessPixelTransform(delta).apply(c.getRGB())); + } +} \ No newline at end of file diff --git a/src/test/java/com/defano/jmonet/transform/pixel/InvertPixelTransformTest.java b/src/test/java/com/defano/jmonet/transform/pixel/InvertPixelTransformTest.java new file mode 100644 index 00000000..fb6cbf4f --- /dev/null +++ b/src/test/java/com/defano/jmonet/transform/pixel/InvertPixelTransformTest.java @@ -0,0 +1,23 @@ +package com.defano.jmonet.transform.pixel; + +import org.junit.jupiter.api.Test; + +import java.awt.*; + +import static org.junit.jupiter.api.Assertions.*; + +class InvertPixelTransformTest { + + @Test + void testThatColorWithoutAlphaIsInverted() { + assertEquals(Color.white.getRGB(), new InvertPixelTransform().apply(Color.black.getRGB())); + } + + @Test + void testThatColorWithAlphaIsInvertedWithNoChangeToAlpha() { + Color transBlack = new Color(Color.black.getRed(), Color.black.getGreen(), Color.black.getBlue(), 20); + Color transWhite = new Color(Color.white.getRed(), Color.white.getGreen(), Color.white.getBlue(), 20); + + assertEquals(transWhite.getRGB(), new InvertPixelTransform().apply(transBlack.getRGB())); + } +} \ No newline at end of file diff --git a/src/test/java/com/defano/jmonet/transform/pixel/RemoveAlphaPixelTransformTest.java b/src/test/java/com/defano/jmonet/transform/pixel/RemoveAlphaPixelTransformTest.java new file mode 100644 index 00000000..e13fb0d8 --- /dev/null +++ b/src/test/java/com/defano/jmonet/transform/pixel/RemoveAlphaPixelTransformTest.java @@ -0,0 +1,43 @@ +package com.defano.jmonet.transform.pixel; + +import org.junit.jupiter.api.Test; + +import java.awt.*; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +class RemoveAlphaPixelTransformTest { + + @Test + void testThatTranslucentColorIsMadeTransparent() { + Color orig = new Color(Color.ORANGE.getRed(), Color.orange.getGreen(), Color.orange.getBlue(), 127); + Color transformed = new Color(orig.getRed(), orig.getGreen(), orig.getBlue(), 0); + + assertEquals(transformed.getRGB(), new RemoveAlphaPixelTransform(true).apply(orig.getRGB())); + } + + @Test + void testThatTranslucentColorIsMadeOpaque() { + Color orig = new Color(Color.ORANGE.getRed(), Color.orange.getGreen(), Color.orange.getBlue(), 127); + Color transformed = new Color(orig.getRed(), orig.getGreen(), orig.getBlue(), 255); + + assertEquals(transformed.getRGB(), new RemoveAlphaPixelTransform(false).apply(orig.getRGB())); + } + + @Test + void testThatOpaqueColorIsUnchanged() { + Color orig = new Color(Color.ORANGE.getRed(), Color.orange.getGreen(), Color.orange.getBlue(), 255); + + assertEquals(orig.getRGB(), new RemoveAlphaPixelTransform(false).apply(orig.getRGB())); + assertEquals(orig.getRGB(), new RemoveAlphaPixelTransform(true).apply(orig.getRGB())); + } + + @Test + void testThatTransparentColorIsUnchanged() { + Color orig = new Color(Color.ORANGE.getRed(), Color.orange.getGreen(), Color.orange.getBlue(), 0); + + assertEquals(orig.getRGB(), new RemoveAlphaPixelTransform(false).apply(orig.getRGB())); + assertEquals(orig.getRGB(), new RemoveAlphaPixelTransform(true).apply(orig.getRGB())); + } + +} \ No newline at end of file