├── .gitignore ├── .travis.yml ├── .travis └── push-javadocs.sh ├── LICENSE.txt ├── README.md ├── icons ├── arrow.png ├── button.png ├── curve.png ├── distort.png ├── eraser.png ├── field.png ├── fill.png ├── finger.png ├── freeform.png ├── lasso.png ├── line.png ├── magnifier.png ├── oval.png ├── paintbrush.png ├── pencil.png ├── perspective.png ├── polygon.png ├── rectangle.png ├── rotate.png ├── roundrect.png ├── scale.png ├── selection.png ├── shape.png ├── slant.png ├── spraypaint.png └── text.png ├── pom.xml └── src ├── main ├── java │ └── com │ │ └── defano │ │ └── jmonet │ │ ├── canvas │ │ ├── AbstractPaintCanvas.java │ │ ├── JFXPaintCanvasNode.java │ │ ├── JMonetCanvas.java │ │ ├── PaintCanvas.java │ │ ├── Scratch.java │ │ ├── Undoable.java │ │ ├── layer │ │ │ ├── ImageLayer.java │ │ │ ├── ImageLayerSet.java │ │ │ ├── LayeredImage.java │ │ │ └── ScaledLayeredImage.java │ │ ├── observable │ │ │ ├── CanvasCommitObserver.java │ │ │ ├── LayerSetObserver.java │ │ │ ├── ObservableSurface.java │ │ │ └── SurfaceInteractionObserver.java │ │ ├── paint │ │ │ └── PaintFactory.java │ │ └── surface │ │ │ ├── AbstractPaintSurface.java │ │ │ ├── DefaultSurfaceScrollController.java │ │ │ ├── Disposable.java │ │ │ ├── GridSurface.java │ │ │ ├── PaintSurface.java │ │ │ ├── ScalableSurface.java │ │ │ ├── ScanlineSurface.java │ │ │ ├── ScrollableSurface.java │ │ │ ├── SurfaceScrollController.java │ │ │ └── SwingSurface.java │ │ ├── clipboard │ │ ├── CanvasClipboardActionListener.java │ │ ├── CanvasFocusDelegate.java │ │ ├── CanvasTransferDelegate.java │ │ ├── CanvasTransferHandler.java │ │ ├── JMonetCanvasFocusDelegate.java │ │ └── TransferableImage.java │ │ ├── context │ │ ├── AwtGraphicsContext.java │ │ └── GraphicsContext.java │ │ ├── model │ │ ├── FixedQuadrilateral.java │ │ ├── FlexQuadrilateral.java │ │ ├── Interpolation.java │ │ ├── PaintToolType.java │ │ └── Quadrilateral.java │ │ ├── tools │ │ ├── AirbrushTool.java │ │ ├── ArrowTool.java │ │ ├── CurveTool.java │ │ ├── EraserTool.java │ │ ├── FillTool.java │ │ ├── FreeformShapeTool.java │ │ ├── LassoTool.java │ │ ├── LineTool.java │ │ ├── MagnifierTool.java │ │ ├── MarqueeTool.java │ │ ├── OvalTool.java │ │ ├── PaintbrushTool.java │ │ ├── PencilTool.java │ │ ├── PerspectiveTool.java │ │ ├── PolygonTool.java │ │ ├── ProjectionTool.java │ │ ├── RectangleTool.java │ │ ├── RotateTool.java │ │ ├── RoundRectangleTool.java │ │ ├── RubberSheetTool.java │ │ ├── ScaleTool.java │ │ ├── ShapeTool.java │ │ ├── SlantTool.java │ │ ├── TextTool.java │ │ ├── attributes │ │ │ ├── BoundaryFunction.java │ │ │ ├── FillFunction.java │ │ │ ├── MarkPredicate.java │ │ │ ├── ObservableToolAttributes.java │ │ │ ├── RxToolAttributes.java │ │ │ └── ToolAttributes.java │ │ ├── base │ │ │ ├── BasicTool.java │ │ │ ├── BoundsTool.java │ │ │ ├── BoundsToolDelegate.java │ │ │ ├── LinearTool.java │ │ │ ├── LinearToolDelegate.java │ │ │ ├── PathTool.java │ │ │ ├── PathToolDelegate.java │ │ │ ├── PolylineTool.java │ │ │ ├── PolylineToolDelegate.java │ │ │ ├── SelectionTool.java │ │ │ ├── SelectionToolDelegate.java │ │ │ ├── StrokedCursorPathTool.java │ │ │ ├── Tool.java │ │ │ ├── TransformTool.java │ │ │ └── TransformToolDelegate.java │ │ ├── brushes │ │ │ ├── ShapeStroke.java │ │ │ └── StampStroke.java │ │ ├── builder │ │ │ ├── BasicStrokeBuilder.java │ │ │ ├── PaintToolBuilder.java │ │ │ ├── ShapeStrokeBuilder.java │ │ │ └── StrokeBuilder.java │ │ ├── cursors │ │ │ ├── CursorFactory.java │ │ │ ├── CursorManager.java │ │ │ └── SwingCursorManager.java │ │ ├── selection │ │ │ ├── MutableSelection.java │ │ │ ├── Selection.java │ │ │ ├── TransformableCanvasSelection.java │ │ │ ├── TransformableImageSelection.java │ │ │ └── TransformableSelection.java │ │ └── util │ │ │ ├── ImageUtils.java │ │ │ ├── MarchingAnts.java │ │ │ ├── MarchingAntsObserver.java │ │ │ └── MathUtils.java │ │ └── transform │ │ ├── affine │ │ ├── FlipHorizontalTransform.java │ │ ├── FlipVerticalTransform.java │ │ ├── RotateLeftTransform.java │ │ └── RotateRightTransform.java │ │ ├── dither │ │ ├── AbstractDitherer.java │ │ ├── AtkinsonDitherer.java │ │ ├── BurkesDitherer.java │ │ ├── Ditherer.java │ │ ├── FloydSteinbergDitherer.java │ │ ├── JarvisJudiceNinkeDitherer.java │ │ ├── NullDitherer.java │ │ ├── SierraDitherer.java │ │ ├── SierraLiteDitherer.java │ │ ├── SierraTwoDitherer.java │ │ ├── StuckiDitherer.java │ │ └── quant │ │ │ ├── ColorReductionQuantizer.java │ │ │ ├── GrayscaleQuantizer.java │ │ │ ├── MonochromaticQuantizer.java │ │ │ └── QuantizationFunction.java │ │ ├── image │ │ ├── ApplyAffineTransform.java │ │ ├── ApplyPixelTransform.java │ │ ├── BufferedImageOpTransform.java │ │ ├── ColorReductionTransform.java │ │ ├── ConvolutionTransform.java │ │ ├── FillTransform.java │ │ ├── FloodFillTransform.java │ │ ├── GreyscaleReductionTransform.java │ │ ├── ImageTransform.java │ │ ├── PixelTransform.java │ │ ├── ProjectionTransform.java │ │ ├── RubbersheetTransform.java │ │ ├── ScaleTransform.java │ │ ├── SlantTransform.java │ │ ├── StaticImageTransform.java │ │ ├── StaticImageTransformable.java │ │ └── Transformable.java │ │ └── pixel │ │ ├── BrightnessPixelTransform.java │ │ ├── InvertPixelTransform.java │ │ ├── RemoveAlphaPixelTransform.java │ │ └── TransparencyPixelTransform.java └── resources │ └── cursors │ ├── fill.png │ ├── lasso.png │ ├── magnifier.png │ ├── magnifier_minus.png │ ├── magnifier_plus.png │ └── pencil.png └── test └── java └── com └── defano └── jmonet ├── canvas └── layer │ └── ImageLayerTest.java ├── tools ├── AirbrushToolTest.java ├── ArrowToolTest.java ├── EraserToolTest.java ├── FillToolTest.java ├── LineToolTest.java ├── PencilToolTest.java ├── RectangleToolTest.java ├── base │ ├── BaseToolTest.java │ ├── BasicToolTest.java │ ├── BoundsToolTest.java │ ├── ColorMatcher.java │ ├── CursorMatcher.java │ ├── LinearToolTest.java │ ├── MockitoTest.java │ ├── MockitoToolTest.java │ ├── PathToolTest.java │ ├── PolylineToolTest.java │ └── ShapeMatcher.java └── util │ └── MathUtilsTest.java └── transform └── pixel ├── BrightnessPixelTransformTest.java ├── InvertPixelTransformTest.java └── RemoveAlphaPixelTransformTest.java /.gitignore: -------------------------------------------------------------------------------- 1 | *.class 2 | 3 | # Package Files # 4 | *.jar 5 | *.war 6 | *.ear 7 | 8 | .idea/* 9 | *.iml 10 | target/* 11 | *.bin 12 | .gradle/* 13 | /src/main/java/com/defano/jmonet/Tester.java 14 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | dist: trusty 2 | language: java 3 | 4 | before_install: 5 | - "export DISPLAY=:99.0" 6 | - "sh -e /etc/init.d/xvfb start" 7 | 8 | jdk: 9 | - oraclejdk8 10 | 11 | install: true 12 | 13 | env: 14 | global: 15 | secure: aQxSSvrwAOkfK3A7XsQN4rQherQJgZ13WDT3EInOnZHdLu0ap6E4M8s6s8QniNZssvusp9gFUoxcUif1p45OYXqZcpxSfwyw83N7ycQgsMBx5ZJfDIVMDv8zEzyK0SBeJVeD4HCwzkbYQkNkfoJE5XkbxJsdfE31WdQoNaHOurJEqoFlsZpfZpTRU7FvxToesc7gQMu0NULpJ1xpz4RmxzDHeTpcMj16aDVGh8G6nzZLWf0TxllgA0AfsQIvU/5s8aYr4h6FhujhBhJcFB+kD+wzOXTgTjzscB6moLGOsLhr5GLORDSGkash7n/ki9G1vV3LPCOKEEVKd++NJNkfJTBbSoWRzd7h6ApT4zloWb22aTsZDZ5GC+u6zop69MAZ+BOcF2eUbFN47XtqzEn8EFEJFzj899SI0HrS6Loh8Ac/6nKsD7G/B70nZrDe9stLDdwDkM5wah0l4tVBeYh00x7COxrE+RQvf3sOomZpGLe9dEGQ3s910IZmVC9GA+2mikC7Dri22VWLrOfgFRbj20UMe7bXD4iyXUOHZlimUhCR9kvLx126cGNeelFcdKs5XCazwVmogDbcSMdP/zZoZuxxhRJcwtKAsrpaFVttMsW2oIC6A/IsDAbdJ+55HRf2mSswh/DOSSMMzLS6k+nmQgsmSQqj+gBjKZTFoMRfD+0= 16 | 17 | script: mvn test javadoc:javadoc 18 | 19 | after_success: 20 | - chmod +x .travis/push-javadocs.sh 21 | - .travis/push-javadocs.sh 22 | - mvn sonar:sonar -Dsonar.organization=defano-github -Dsonar.host.url=https://sonarcloud.io -Dsonar.login=$SONAR_KEY -Dsonar.branch.name=$TRAVIS_BRANCH 23 | -------------------------------------------------------------------------------- /.travis/push-javadocs.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | if [ "$TRAVIS_REPO_SLUG" == "defano/jmonet" ] && [ "$TRAVIS_JDK_VERSION" == "oraclejdk8" ] && [ "$TRAVIS_PULL_REQUEST" == "false" ] && [ "$TRAVIS_BRANCH" == "master" ]; then 4 | 5 | echo -e "Publishing JavaDocs...\n" 6 | 7 | cp -R target/site/apidocs $HOME/javadoc-latest 8 | 9 | cd $HOME 10 | git config --global user.email "travis@travis-ci.org" 11 | git config --global user.name "travis-ci" 12 | git clone --quiet --branch=gh-pages https://${GH_TOKEN}@github.com/defano/jmonet gh-pages > /dev/null 13 | 14 | cd gh-pages 15 | git rm -rf ./javadoc 16 | cp -Rf $HOME/javadoc-latest ./javadoc 17 | git add -f . 18 | git commit -m "Javadoc publication by Travis build $TRAVIS_BUILD_NUMBER" 19 | git push -fq origin gh-pages > /dev/null 20 | 21 | echo -e "Published Javadoc to gh-pages.\n" 22 | 23 | fi -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 Matt DeFano 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /icons/arrow.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/defano/jmonet/21333cbfccdc89ae9c1db0b93b2b760439c5cd59/icons/arrow.png -------------------------------------------------------------------------------- /icons/button.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/defano/jmonet/21333cbfccdc89ae9c1db0b93b2b760439c5cd59/icons/button.png -------------------------------------------------------------------------------- /icons/curve.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/defano/jmonet/21333cbfccdc89ae9c1db0b93b2b760439c5cd59/icons/curve.png -------------------------------------------------------------------------------- /icons/distort.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/defano/jmonet/21333cbfccdc89ae9c1db0b93b2b760439c5cd59/icons/distort.png -------------------------------------------------------------------------------- /icons/eraser.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/defano/jmonet/21333cbfccdc89ae9c1db0b93b2b760439c5cd59/icons/eraser.png -------------------------------------------------------------------------------- /icons/field.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/defano/jmonet/21333cbfccdc89ae9c1db0b93b2b760439c5cd59/icons/field.png -------------------------------------------------------------------------------- /icons/fill.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/defano/jmonet/21333cbfccdc89ae9c1db0b93b2b760439c5cd59/icons/fill.png -------------------------------------------------------------------------------- /icons/finger.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/defano/jmonet/21333cbfccdc89ae9c1db0b93b2b760439c5cd59/icons/finger.png -------------------------------------------------------------------------------- /icons/freeform.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/defano/jmonet/21333cbfccdc89ae9c1db0b93b2b760439c5cd59/icons/freeform.png -------------------------------------------------------------------------------- /icons/lasso.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/defano/jmonet/21333cbfccdc89ae9c1db0b93b2b760439c5cd59/icons/lasso.png -------------------------------------------------------------------------------- /icons/line.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/defano/jmonet/21333cbfccdc89ae9c1db0b93b2b760439c5cd59/icons/line.png -------------------------------------------------------------------------------- /icons/magnifier.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/defano/jmonet/21333cbfccdc89ae9c1db0b93b2b760439c5cd59/icons/magnifier.png -------------------------------------------------------------------------------- /icons/oval.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/defano/jmonet/21333cbfccdc89ae9c1db0b93b2b760439c5cd59/icons/oval.png -------------------------------------------------------------------------------- /icons/paintbrush.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/defano/jmonet/21333cbfccdc89ae9c1db0b93b2b760439c5cd59/icons/paintbrush.png -------------------------------------------------------------------------------- /icons/pencil.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/defano/jmonet/21333cbfccdc89ae9c1db0b93b2b760439c5cd59/icons/pencil.png -------------------------------------------------------------------------------- /icons/perspective.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/defano/jmonet/21333cbfccdc89ae9c1db0b93b2b760439c5cd59/icons/perspective.png -------------------------------------------------------------------------------- /icons/polygon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/defano/jmonet/21333cbfccdc89ae9c1db0b93b2b760439c5cd59/icons/polygon.png -------------------------------------------------------------------------------- /icons/rectangle.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/defano/jmonet/21333cbfccdc89ae9c1db0b93b2b760439c5cd59/icons/rectangle.png -------------------------------------------------------------------------------- /icons/rotate.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/defano/jmonet/21333cbfccdc89ae9c1db0b93b2b760439c5cd59/icons/rotate.png -------------------------------------------------------------------------------- /icons/roundrect.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/defano/jmonet/21333cbfccdc89ae9c1db0b93b2b760439c5cd59/icons/roundrect.png -------------------------------------------------------------------------------- /icons/scale.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/defano/jmonet/21333cbfccdc89ae9c1db0b93b2b760439c5cd59/icons/scale.png -------------------------------------------------------------------------------- /icons/selection.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/defano/jmonet/21333cbfccdc89ae9c1db0b93b2b760439c5cd59/icons/selection.png -------------------------------------------------------------------------------- /icons/shape.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/defano/jmonet/21333cbfccdc89ae9c1db0b93b2b760439c5cd59/icons/shape.png -------------------------------------------------------------------------------- /icons/slant.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/defano/jmonet/21333cbfccdc89ae9c1db0b93b2b760439c5cd59/icons/slant.png -------------------------------------------------------------------------------- /icons/spraypaint.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/defano/jmonet/21333cbfccdc89ae9c1db0b93b2b760439c5cd59/icons/spraypaint.png -------------------------------------------------------------------------------- /icons/text.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/defano/jmonet/21333cbfccdc89ae9c1db0b93b2b760439c5cd59/icons/text.png -------------------------------------------------------------------------------- /src/main/java/com/defano/jmonet/canvas/JFXPaintCanvasNode.java: -------------------------------------------------------------------------------- 1 | package com.defano.jmonet.canvas; 2 | 3 | import com.defano.jmonet.canvas.surface.Disposable; 4 | import javafx.application.Platform; 5 | import javafx.embed.swing.SwingNode; 6 | 7 | /** 8 | * A trivial wrapper making a PaintCanvas available to JavaFX applications. 9 | */ 10 | public class JFXPaintCanvasNode extends SwingNode implements Disposable { 11 | 12 | private final AbstractPaintCanvas canvas; 13 | 14 | public JFXPaintCanvasNode(AbstractPaintCanvas canvas) { 15 | this.canvas = canvas; 16 | Platform.runLater(() -> JFXPaintCanvasNode.super.setContent(canvas)); 17 | } 18 | 19 | public PaintCanvas getCanvas() { 20 | return canvas; 21 | } 22 | 23 | @Override 24 | public void dispose() { 25 | canvas.dispose(); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/main/java/com/defano/jmonet/canvas/Undoable.java: -------------------------------------------------------------------------------- 1 | package com.defano.jmonet.canvas; 2 | 3 | import com.defano.jmonet.canvas.layer.ImageLayerSet; 4 | 5 | public interface Undoable { 6 | 7 | /** 8 | * Undoes the previous committed change. May be called successively to revert committed changes one-by-one until 9 | * the undo buffer is exhausted. 10 | * 11 | * @return The ChangeSet that was undone by this operation, or null if there were no undoable changes. 12 | */ 13 | ImageLayerSet undo(); 14 | 15 | /** 16 | * Reverts the previous undo; has no effect if a commit was made following the previous undo. 17 | * 18 | * @return True if the redo was successful, false if there is no undo available to revert. 19 | */ 20 | boolean redo(); 21 | 22 | /** 23 | * Determines if a commit is available to be undone. 24 | * 25 | * @return True if {@link #undo()} will succeed; false otherwise. 26 | */ 27 | boolean hasUndoableChanges(); 28 | 29 | /** 30 | * Determines if an undo is available to revert. 31 | * 32 | * @return True if {@link #redo()} will succeed; false otherwise. 33 | */ 34 | boolean hasRedoableChanges(); 35 | 36 | /** 37 | * Gets the maximum depth of the undo buffer. 38 | * 39 | * @return The maximum number of undos that are supported by this PaintCanvas. 40 | */ 41 | int getMaxUndoBufferDepth(); 42 | 43 | /** 44 | * Gets the number of changes that can be "undone". 45 | * 46 | * @return The depth of undo buffer. 47 | */ 48 | int getUndoBufferDepth(); 49 | 50 | /** 51 | * Gets the number of changes that can be "redone". 52 | * 53 | * @return The depth of the redo buffer. 54 | */ 55 | int getRedoBufferDepth(); 56 | 57 | } 58 | -------------------------------------------------------------------------------- /src/main/java/com/defano/jmonet/canvas/layer/LayeredImage.java: -------------------------------------------------------------------------------- 1 | package com.defano.jmonet.canvas.layer; 2 | 3 | import com.defano.jmonet.context.AwtGraphicsContext; 4 | import com.defano.jmonet.context.GraphicsContext; 5 | 6 | import java.awt.*; 7 | import java.awt.image.BufferedImage; 8 | 9 | /** 10 | * An image that is composed of multiple layers rendered one atop another. 11 | */ 12 | public interface LayeredImage { 13 | 14 | /** 15 | * Gets the layers of this image in the order in which they should be drawn. 16 | * 17 | * @return The image layers 18 | */ 19 | ImageLayer[] getImageLayers(); 20 | 21 | /** 22 | * Produces a new BufferedImage containing each of the layers of this image rendered atop one another. 23 | * 24 | * @return A rendering of this image. 25 | */ 26 | default BufferedImage render() { 27 | Dimension size = getSize(); 28 | BufferedImage rendering = new BufferedImage(size.width, size.height, BufferedImage.TYPE_INT_ARGB); 29 | 30 | GraphicsContext g = new AwtGraphicsContext(rendering.createGraphics()); 31 | paint(g, 1.0, null); 32 | g.dispose(); 33 | 34 | return rendering; 35 | } 36 | 37 | /** 38 | * Draws this layered image onto the given graphics context. 39 | * 40 | * @param g The graphics context on which to draw 41 | * @param scale The scale at which to draw the image 42 | * @param clip The clipping rectangle describing the bounds of the graphics context that should be painted 43 | */ 44 | default void paint(GraphicsContext g, Double scale, Rectangle clip) { 45 | for (ImageLayer thisLayer : getImageLayers()) { 46 | if (thisLayer != null) { 47 | thisLayer.paint(g, scale, clip); 48 | } 49 | } 50 | } 51 | 52 | /** 53 | * Calculates the size of this image; equal to the dimensions of the largest layer in the image. 54 | * 55 | * @return The size of this image. 56 | */ 57 | default Dimension getSize() { 58 | int height = 0; 59 | int width = 0; 60 | 61 | for (ImageLayer thisLayer : getImageLayers()) { 62 | Dimension layerDimension = thisLayer.getDisplayedSize(); 63 | height = Math.max(height, layerDimension.height); 64 | width = Math.max(width, layerDimension.width); 65 | } 66 | 67 | return new Dimension(width, height); 68 | } 69 | 70 | } 71 | -------------------------------------------------------------------------------- /src/main/java/com/defano/jmonet/canvas/layer/ScaledLayeredImage.java: -------------------------------------------------------------------------------- 1 | package com.defano.jmonet.canvas.layer; 2 | 3 | /** 4 | * An image with multiple layers that can be drawn at scale. 5 | */ 6 | public interface ScaledLayeredImage extends LayeredImage { 7 | 8 | /** 9 | * Gets the displayed scale factor of this image. A scale of 1.0 means no scaling; a value of 2.0 means the image 10 | * should be drawn at a 2:1 scale. 11 | * 12 | * @return The displayed scale factor of this image. 13 | */ 14 | double getScale(); 15 | } 16 | -------------------------------------------------------------------------------- /src/main/java/com/defano/jmonet/canvas/observable/CanvasCommitObserver.java: -------------------------------------------------------------------------------- 1 | package com.defano.jmonet.canvas.observable; 2 | 3 | import com.defano.jmonet.canvas.layer.ImageLayerSet; 4 | import com.defano.jmonet.canvas.PaintCanvas; 5 | 6 | import java.awt.image.BufferedImage; 7 | 8 | /** 9 | * An object which listens to changes being committed to a canvas. 10 | */ 11 | public interface CanvasCommitObserver { 12 | /** 13 | * Fires when an new shape or image is committed from scratch onto the canvas. 14 | * 15 | * @param canvas The canvas on which the commit is occurring. 16 | * @param imageLayerSet The set of changes being committed. 17 | * @param canvasImage The resulting canvas image (including the committed change) 18 | */ 19 | void onCommit(PaintCanvas canvas, ImageLayerSet imageLayerSet, BufferedImage canvasImage); 20 | } 21 | -------------------------------------------------------------------------------- /src/main/java/com/defano/jmonet/canvas/observable/LayerSetObserver.java: -------------------------------------------------------------------------------- 1 | package com.defano.jmonet.canvas.observable; 2 | 3 | import com.defano.jmonet.canvas.layer.ImageLayerSet; 4 | 5 | /** 6 | * An observer of modifications to a {@link ImageLayerSet}. 7 | */ 8 | public interface LayerSetObserver { 9 | 10 | /** 11 | * Fired to indicate a {@link ImageLayerSet} was modified. 12 | * @param modified The ChangeSet that was modified. 13 | */ 14 | void onLayerSetModified(ImageLayerSet modified); 15 | } 16 | -------------------------------------------------------------------------------- /src/main/java/com/defano/jmonet/canvas/observable/ObservableSurface.java: -------------------------------------------------------------------------------- 1 | package com.defano.jmonet.canvas.observable; 2 | 3 | /** 4 | * Represents an AWT component that can contain other components and report on keyboard and mouse interactions taking 5 | * place inside of it. 6 | * 7 | * A surface has no layout manager, so elements added to are displayed in a fixed location based on their bounds. 8 | */ 9 | public interface ObservableSurface { 10 | 11 | /** 12 | * Adds an observer to mouse and keyboard events taking place within this surface. 13 | * @param listener The observer to add 14 | */ 15 | void addSurfaceInteractionObserver(SurfaceInteractionObserver listener); 16 | 17 | /** 18 | * Removes an observer that was previously listening for UI events. 19 | * @param listener The observer to be removed 20 | * @return True if the listener was previously registered as an observer and was successfully removed 21 | */ 22 | @SuppressWarnings("UnusedReturnValue") 23 | boolean removeSurfaceInteractionObserver(SurfaceInteractionObserver listener); 24 | 25 | } 26 | -------------------------------------------------------------------------------- /src/main/java/com/defano/jmonet/canvas/paint/PaintFactory.java: -------------------------------------------------------------------------------- 1 | package com.defano.jmonet.canvas.paint; 2 | 3 | import javax.swing.*; 4 | import java.awt.*; 5 | import java.awt.image.BufferedImage; 6 | 7 | /** 8 | * A utility for producing {@link Paint} in commonly-needed colors and textures. 9 | */ 10 | @SuppressWarnings("unused") 11 | public class PaintFactory { 12 | 13 | private PaintFactory() {} 14 | 15 | /** 16 | * Produces {@link Paint} of the same background color of a {@link JPanel} for the current look and feel, or 17 | * {@link Color#WHITE} if no such color is defined. 18 | * 19 | * @return Paint matching the panel background color. 20 | */ 21 | public static Paint makePanelColor() { 22 | Color panel = (Color) UIManager.getLookAndFeelDefaults().get("Panel.background"); 23 | return panel == null ? Color.WHITE : panel; 24 | } 25 | 26 | /** 27 | * Produces {@link Paint} that's fully transparent (otherwise black). 28 | * 29 | * @return Fully transparent paint. 30 | */ 31 | public static Paint makeTransparent() { 32 | return new Color(0, 0, 0, 0); 33 | } 34 | 35 | /** 36 | * Produces a light-grey and white checkerboard paint pattern, often used as a canvas background to denote areas of 37 | * an image that are transparent. 38 | * 39 | * @param checkSize The square dimension of the checks, in pixels. 40 | * @return Checkerboard paint 41 | */ 42 | public static Paint makeCheckerboard(int checkSize) { 43 | return makeCheckerboard(checkSize, Color.WHITE, Color.LIGHT_GRAY); 44 | } 45 | 46 | /** 47 | * Produces a checkerboard paint pattern of specified size and color. 48 | * 49 | * @param checkSize The square dimension of the checks, in pixels 50 | * @param c1 First check's color 51 | * @param c2 Second check's color 52 | * @return Checkerboard paint 53 | */ 54 | public static Paint makeCheckerboard(int checkSize, Color c1, Color c2) { 55 | BufferedImage checks = new BufferedImage(2, 2, BufferedImage.TYPE_INT_ARGB); 56 | Graphics2D g = checks.createGraphics(); 57 | 58 | // Fill the field 59 | g.setPaint(c1); 60 | g.fillRect(0, 0, 2, 2); 61 | 62 | // Fill the checks 63 | g.setPaint(c2); 64 | g.fillRect(0, 0, 1, 1); 65 | g.fillRect(1, 1, 2, 2); 66 | g.dispose(); 67 | 68 | return new TexturePaint(checks, new Rectangle(0, 0, checkSize * 2, checkSize * 2)); 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /src/main/java/com/defano/jmonet/canvas/surface/DefaultSurfaceScrollController.java: -------------------------------------------------------------------------------- 1 | package com.defano.jmonet.canvas.surface; 2 | 3 | import com.defano.jmonet.tools.MagnifierTool; 4 | 5 | import javax.swing.*; 6 | import java.awt.*; 7 | 8 | import static javax.swing.ScrollPaneConstants.HORIZONTAL_SCROLLBAR_AS_NEEDED; 9 | import static javax.swing.ScrollPaneConstants.VERTICAL_SCROLLBAR_AS_NEEDED; 10 | 11 | /** 12 | * Provides basic scroll behavior when an AbstractPaintSurface is the viewport of a {@link JScrollPane}. This is 13 | * required so that tools (like {@link MagnifierTool} can query and change the scroll position 14 | * of the JScrollPane containing the canvas the tool is painting on. 15 | *

16 | * This default implementation is sufficient for cases when the surface is added to a {@link JScrollPane} as the view 17 | * port. If the surface (paint canvas) is embedded is some container with additional components or decorations then a 18 | * custom implementation may be required to properly adjust viewport scroll position taking into account the size and 19 | * layout of the surface's sibling components. 20 | */ 21 | public class DefaultSurfaceScrollController implements SurfaceScrollController { 22 | 23 | private final AbstractPaintSurface surface; 24 | 25 | public DefaultSurfaceScrollController(AbstractPaintSurface surface) { 26 | this.surface = surface; 27 | } 28 | 29 | /** 30 | * {@inheritDoc} 31 | */ 32 | @Override 33 | public void setScrollPosition(Point position) { 34 | JScrollPane scrollPane = getScrollPane(); 35 | 36 | if (scrollPane != null) { 37 | scrollPane.getViewport().setViewPosition(position); 38 | scrollPane.setVerticalScrollBarPolicy(VERTICAL_SCROLLBAR_AS_NEEDED); 39 | scrollPane.setHorizontalScrollBarPolicy(HORIZONTAL_SCROLLBAR_AS_NEEDED); 40 | 41 | if (scrollPane.getTopLevelAncestor() != null) { 42 | scrollPane.getTopLevelAncestor().revalidate(); 43 | } 44 | 45 | scrollPane.revalidate(); 46 | scrollPane.repaint(); 47 | } 48 | } 49 | 50 | /** 51 | * {@inheritDoc} 52 | */ 53 | @Override 54 | public Rectangle getScrollRect() { 55 | JScrollPane scrollPane = getScrollPane(); 56 | return scrollPane == null ? new Rectangle() : scrollPane.getViewport().getViewRect(); 57 | } 58 | 59 | /** 60 | * Returns the {@link JScrollPane} that is the parent of the surface, or null, if the surface is not the viewport 61 | * of a scroll pane (or is otherwise embedded in a container that is the viewport). 62 | * 63 | * @return The scroll pane object the surface is directly embedded in, or null. 64 | */ 65 | private JScrollPane getScrollPane() { 66 | if (surface.getParent() instanceof JViewport && 67 | surface.getParent().getParent() != null && 68 | surface.getParent().getParent() instanceof JScrollPane) { 69 | 70 | return (JScrollPane) surface.getParent().getParent(); 71 | } 72 | 73 | return null; 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /src/main/java/com/defano/jmonet/canvas/surface/Disposable.java: -------------------------------------------------------------------------------- 1 | package com.defano.jmonet.canvas.surface; 2 | 3 | /** 4 | * An object that holds resources/memory that cannot be automatically garbage collected by the VM. 5 | */ 6 | public interface Disposable { 7 | 8 | /** 9 | * Releases memory-holding resources and unregisters any listeners or observables. The disposed object should 10 | * not be used after this method has been invoked. 11 | */ 12 | void dispose(); 13 | } 14 | -------------------------------------------------------------------------------- /src/main/java/com/defano/jmonet/canvas/surface/GridSurface.java: -------------------------------------------------------------------------------- 1 | package com.defano.jmonet.canvas.surface; 2 | 3 | /** 4 | * A surface that supports a snap-to-grid property. 5 | */ 6 | @SuppressWarnings("unused") 7 | public interface GridSurface { 8 | 9 | /** 10 | * Sets a grid spacing on which the mouse coordinates provided to the paint tools will be "snapped to". 11 | * 12 | * @param grid The grid spacing 13 | */ 14 | void setGridSpacing(int grid); 15 | 16 | /** 17 | * Gets the grid spacing property. 18 | * 19 | * @return The grid spacing 20 | */ 21 | int getGridSpacing(); 22 | 23 | } 24 | -------------------------------------------------------------------------------- /src/main/java/com/defano/jmonet/canvas/surface/PaintSurface.java: -------------------------------------------------------------------------------- 1 | package com.defano.jmonet.canvas.surface; 2 | 3 | import com.defano.jmonet.canvas.observable.ObservableSurface; 4 | 5 | import java.awt.*; 6 | 7 | /** 8 | * A component that can be painted and that provides methods for drawing at scale, snap-to-grid behavior, scrolling, 9 | * observables, and embedding Swing components. 10 | */ 11 | @SuppressWarnings("unused") 12 | public interface PaintSurface 13 | extends ScanlineSurface, GridSurface, SwingSurface, ObservableSurface, ScrollableSurface, Disposable 14 | { 15 | /** 16 | * Specifies the un-scaled size of this painting surface. This determines the size of the image (document) that can 17 | * be painted by a user. This does not specify the size of the Swing component or otherwise adjust layout or 18 | * presentation of the canvas. 19 | * 20 | * @param dimension The dimensions of the painting surface 21 | */ 22 | void setSurfaceDimension(Dimension dimension); 23 | 24 | /** 25 | * Gets the un-scaled size of this painting surface (that is, the size of the image being painted). 26 | * 27 | * @return The dimensions of the image being painted 28 | */ 29 | Dimension getSurfaceDimension(); 30 | 31 | /** 32 | * Causes the entire surface to be repainted by Swing. Note that repainting large regions is computationally 33 | * expensive, whenever possible tools should repaint the smallest sub-region possible using the 34 | * {@link #repaint(Rectangle)} method. 35 | */ 36 | void repaint(); 37 | 38 | /** 39 | * Causes a section of the surface to be repainted by Swing. This is the minimum rectangle that will be repainted; 40 | * there is no 41 | * 42 | * @param r The region of this surface to be repainted. 43 | */ 44 | void repaint(Rectangle r); 45 | 46 | /** 47 | * Determines if the canvas is visible. 48 | * 49 | * @return True if visible; false otherwise. 50 | */ 51 | boolean isVisible(); 52 | 53 | /** 54 | * Sets whether the canvas is visible. When invisible, the component hierarchy will be drawn as though this 55 | * component does not exist. 56 | * 57 | * @param visible True to make this canvas invisible; false for visible. 58 | */ 59 | void setVisible(boolean visible); 60 | 61 | /** 62 | * Gets the mouse cursor that is displayed when the mouse is within the bounds of this component. 63 | * 64 | * @return The active cursor 65 | */ 66 | Cursor getCursor(); 67 | 68 | /** 69 | * Sets the mouse cursor that is displayed when the mouse is within the bounds of this component. 70 | * 71 | * @param cursor The active cursor to display 72 | */ 73 | void setCursor(Cursor cursor); 74 | } 75 | -------------------------------------------------------------------------------- /src/main/java/com/defano/jmonet/canvas/surface/ScrollableSurface.java: -------------------------------------------------------------------------------- 1 | package com.defano.jmonet.canvas.surface; 2 | 3 | import java.awt.*; 4 | 5 | /** 6 | * A surface that can control and monitor its scroll position as a viewport view inside of a 7 | * {@link javax.swing.JScrollPane}. 8 | *

9 | * Typically, when scrolling a component, the component being scrolled does not need to be aware of its positioning 10 | * inside the scroll pane. However, some tools (like the magnifier) need to be able to request scroll position changes 11 | * to be effective. This layer of indirection allows that to occur without having to embed the 12 | * {@link javax.swing.JScrollPane} in the canvas itself. 13 | */ 14 | @SuppressWarnings("unused") 15 | public interface ScrollableSurface { 16 | 17 | /** 18 | * Gets the delegate responsible for the changing the scroll position of the entity (i.e., 19 | * {@link javax.swing.JScrollPane} that holds this surface as its viewport. 20 | * 21 | * @return The current scroll controller 22 | */ 23 | SurfaceScrollController getSurfaceScrollController(); 24 | 25 | /** 26 | * Sets the delegate responsible for changing the scroll position of the entity (i.e., 27 | * {@link javax.swing.JScrollPane} that holds this surface as its viewport. 28 | *

29 | * Some layouts may require a custom scroll controller to account for the sizing and layout of the surface relative 30 | * to other components placed alongside it in the viewport. 31 | * 32 | * @param surfaceScrollController The scroll controller to use with this surface. 33 | */ 34 | void setSurfaceScrollController(SurfaceScrollController surfaceScrollController); 35 | 36 | /** 37 | * Gets the number of pixels horizontal and vertical that have scrolled in the viewport but which have not affected 38 | * the canvas image. 39 | *

40 | * A surface is always drawn such that a full row and column of pixels are aligned with the top-left corner of 41 | * the surface component. When scale is less than or equal to 1.0, this limitation has no impact on scroll. However, 42 | * with larger scales this implies that the user can change the scroll position some amount before the image is 43 | * redrawn. 44 | *

45 | * For example, if scale = 64 the first row of pixels will remain aligned to the top of the component when the 46 | * vertical scroll position is any value less than 64. Thus, if the user scrolls 16 pixels down, the image has not 47 | * changed but the view coordinate space has. This method provides a mechanism for taking this "error" into account. 48 | * 49 | * @return The number of pixels that have scrolled in the viewport but which have not impacted the canvas image 50 | * rendering. Always returns (0,0) when scale is less than or equal to 1.0. 51 | */ 52 | Point getScrollError(); 53 | } 54 | -------------------------------------------------------------------------------- /src/main/java/com/defano/jmonet/canvas/surface/SurfaceScrollController.java: -------------------------------------------------------------------------------- 1 | package com.defano.jmonet.canvas.surface; 2 | 3 | import javax.swing.*; 4 | import java.awt.*; 5 | 6 | /** 7 | * A mechanism enabling a surface to request that its scrolled position (within a {@link javax.swing.JScrollPane}) be 8 | * changed. 9 | * 10 | * This interface allows tools to advise scroll position changes to the {@link javax.swing.JScrollPane} (or other 11 | * container) holding the surface. This enables changes to scale to intelligently "zoom in" on the area being scaled and 12 | * for tools like the {@link com.defano.jmonet.tools.MagnifierTool} to recenter the scroll position. 13 | */ 14 | public interface SurfaceScrollController { 15 | 16 | /** 17 | * Sets the top-left position of the surface that appears in the viewport of the controlled {@link JScrollPane}. 18 | * This coordinate is specified in scaled dimensions (that is, the point (20,20) makes pixel (10,10) of the 19 | * displayed surface visible in the top-left corner when scale is 2.0). Has no effect if the surface is not 20 | * embedded in a {@link JScrollPane} managed by this surface controller. 21 | * 22 | * @param position The position of the surface image to be displayed in the top-left corner of the viewport, in 23 | * scaled dimensions. 24 | */ 25 | void setScrollPosition(Point position); 26 | 27 | /** 28 | * Gets the visible rectangle of the surface in the scroll pane's viewport, represented in scaled coordinates. 29 | * Returns an empty rectangle (0, 0, 0, 0) if this surface is not within a {@link JScrollPane} or is not being 30 | * managed by this scroll controller. 31 | * 32 | * @return The visible rectangle of the scroll pane's view port. 33 | */ 34 | Rectangle getScrollRect(); 35 | 36 | } 37 | -------------------------------------------------------------------------------- /src/main/java/com/defano/jmonet/canvas/surface/SwingSurface.java: -------------------------------------------------------------------------------- 1 | package com.defano.jmonet.canvas.surface; 2 | 3 | import javax.swing.*; 4 | import java.awt.*; 5 | 6 | /** 7 | * A surface to which Swing components can be added and removed. 8 | */ 9 | public interface SwingSurface { 10 | 11 | /** 12 | * Adds a component to the surface. 13 | * 14 | * @param component The component to be added. 15 | */ 16 | void addComponent(Component component); 17 | 18 | /** 19 | * Removes a component from the surface; has no effect if the given component is not a child of this surface. 20 | * 21 | * @param component The component to remove 22 | */ 23 | void removeComponent(Component component); 24 | 25 | /** 26 | * Returns the ActionMap used to determine what 27 | * Action to fire for particular KeyStroke 28 | * binding. The returned ActionMap, unless otherwise 29 | * set, will have the ActionMap from the UI set as the parent. 30 | * 31 | * @return the ActionMap containing the key/action bindings 32 | */ 33 | ActionMap getActionMap(); 34 | 35 | /** 36 | * Gets the Swing component representing the surface itself. 37 | * 38 | * @return The surface component. 39 | */ 40 | Component getComponent(); 41 | } 42 | -------------------------------------------------------------------------------- /src/main/java/com/defano/jmonet/clipboard/CanvasClipboardActionListener.java: -------------------------------------------------------------------------------- 1 | package com.defano.jmonet.clipboard; 2 | 3 | import com.defano.jmonet.canvas.PaintCanvas; 4 | 5 | import javax.swing.*; 6 | import java.awt.event.ActionEvent; 7 | import java.awt.event.ActionListener; 8 | 9 | /** 10 | * A listener of clipboard-related actions (cut, copy and paste). 11 | */ 12 | @SuppressWarnings("unused") 13 | public class CanvasClipboardActionListener implements ActionListener { 14 | 15 | private final CanvasFocusDelegate delegate; 16 | 17 | public CanvasClipboardActionListener(CanvasFocusDelegate delegate) { 18 | this.delegate = delegate; 19 | } 20 | 21 | public CanvasClipboardActionListener() { 22 | this(new JMonetCanvasFocusDelegate()); 23 | } 24 | 25 | /** {@inheritDoc} */ 26 | @Override 27 | public void actionPerformed(ActionEvent e) { 28 | PaintCanvas focusedCanvas = delegate == null ? null : delegate.getCanvasInFocus(); 29 | 30 | if (focusedCanvas != null) { 31 | Action a = focusedCanvas.getActionMap().get(e.getActionCommand()); 32 | if (a != null) { 33 | try { 34 | a.actionPerformed(new ActionEvent(focusedCanvas, ActionEvent.ACTION_PERFORMED, null)); 35 | } catch (Exception ignored) { 36 | // Nothing to do 37 | } 38 | } 39 | } 40 | } 41 | 42 | } 43 | -------------------------------------------------------------------------------- /src/main/java/com/defano/jmonet/clipboard/CanvasFocusDelegate.java: -------------------------------------------------------------------------------- 1 | package com.defano.jmonet.clipboard; 2 | 3 | import com.defano.jmonet.canvas.PaintCanvas; 4 | 5 | /** 6 | * A determiner of which JMonet canvas presently has focus; used by the {@link CanvasClipboardActionListener} to 7 | * indicate which canvas (if any) should receive actions. 8 | */ 9 | public interface CanvasFocusDelegate { 10 | 11 | /** 12 | * Invoked to retrieve the canvas currently in focus, which should receive cut, copy and paste actions. 13 | * 14 | * @return The canvas that should receive cut, copy and paste commands, or null if no canvas is currently in 15 | * focus. 16 | */ 17 | PaintCanvas getCanvasInFocus(); 18 | } 19 | -------------------------------------------------------------------------------- /src/main/java/com/defano/jmonet/clipboard/CanvasTransferDelegate.java: -------------------------------------------------------------------------------- 1 | package com.defano.jmonet.clipboard; 2 | 3 | import com.defano.jmonet.tools.MarqueeTool; 4 | 5 | import java.awt.*; 6 | import java.awt.image.BufferedImage; 7 | 8 | /** 9 | * A provider of clipboard image transfers. 10 | */ 11 | public interface CanvasTransferDelegate { 12 | 13 | /** 14 | * Invoked when the user issued to "Copy" command. A typical implementation of this method will delegate 15 | * to {@link com.defano.jmonet.tools.base.SelectionTool#getSelectedImage()}. 16 | * 17 | * @return The image to be copied to the clipboard, or null if there is no selection to be copied. 18 | */ 19 | BufferedImage copySelection(); 20 | 21 | /** 22 | * Invoked to delete the current selection as a result of completing a "Cut" command. A typical implementation 23 | * of this method will delegate to {@link com.defano.jmonet.tools.base.SelectionTool#deleteSelection()}. 24 | * 25 | * Note that a "Cut" command is comprised of a {@link #copySelection()} this method. 26 | */ 27 | void deleteSelection(); 28 | 29 | /** 30 | * Invoked to paste the given image onto the canvas. A typical implementation of this method might activate the 31 | * {@link MarqueeTool} on the canvas, then invoke 32 | * {@link MarqueeTool#createSelection(BufferedImage, Point)} to make the pasted image 33 | * the current selection. 34 | * 35 | * @param image The image to paste onto the focused canvas. 36 | */ 37 | void pasteSelection(BufferedImage image); 38 | } 39 | -------------------------------------------------------------------------------- /src/main/java/com/defano/jmonet/clipboard/CanvasTransferHandler.java: -------------------------------------------------------------------------------- 1 | package com.defano.jmonet.clipboard; 2 | 3 | import com.defano.jmonet.canvas.AbstractPaintCanvas; 4 | 5 | import javax.swing.*; 6 | import java.awt.*; 7 | import java.awt.datatransfer.DataFlavor; 8 | import java.awt.datatransfer.Transferable; 9 | import java.awt.datatransfer.UnsupportedFlavorException; 10 | import java.awt.image.BufferedImage; 11 | import java.io.IOException; 12 | 13 | /** 14 | * A {@link TransferHandler} for JMonet canvas images. 15 | */ 16 | public class CanvasTransferHandler extends TransferHandler { 17 | 18 | private final CanvasTransferDelegate delegate; 19 | 20 | public CanvasTransferHandler(AbstractPaintCanvas canvas, CanvasTransferDelegate delegate) { 21 | this.delegate = delegate; 22 | 23 | // Register canvas for cut/copy/paste actions 24 | ActionMap map = canvas.getActionMap(); 25 | map.put(TransferHandler.getCutAction().getValue(Action.NAME), TransferHandler.getCutAction()); 26 | map.put(TransferHandler.getCopyAction().getValue(Action.NAME), TransferHandler.getCopyAction()); 27 | map.put(TransferHandler.getPasteAction().getValue(Action.NAME), TransferHandler.getPasteAction()); 28 | } 29 | 30 | /** {@inheritDoc} */ 31 | @Override 32 | protected Transferable createTransferable(JComponent c) { 33 | return TransferableImage.from(delegate.copySelection()); 34 | } 35 | 36 | /** {@inheritDoc} */ 37 | @Override 38 | public int getSourceActions(JComponent c) { 39 | return COPY_OR_MOVE; 40 | } 41 | 42 | /** {@inheritDoc} */ 43 | @Override 44 | protected void exportDone(JComponent c, Transferable data, int action) { 45 | if (action == MOVE) { 46 | delegate.deleteSelection(); 47 | } 48 | } 49 | 50 | /** {@inheritDoc} */ 51 | @Override 52 | public boolean importData(TransferHandler.TransferSupport info) { 53 | try { 54 | delegate.pasteSelection(toBufferedImage((Image) info.getTransferable().getTransferData(DataFlavor.imageFlavor))); 55 | return true; 56 | } catch (UnsupportedFlavorException | IOException ignored) { 57 | // Nothing to do 58 | } 59 | return false; 60 | } 61 | 62 | /** 63 | * Converts an image to a BufferedImage of type ARGB. 64 | * @param source The image to convert 65 | * @return The resulting BufferedImage 66 | */ 67 | private BufferedImage toBufferedImage(Image source) { 68 | if (source instanceof BufferedImage) { 69 | return (BufferedImage) source; 70 | } 71 | 72 | // Create a buffered image with transparency 73 | BufferedImage dest = new BufferedImage(source.getWidth(null), source.getHeight(null), BufferedImage.TYPE_INT_ARGB); 74 | 75 | // Draw the image on to the buffered image 76 | Graphics2D g = dest.createGraphics(); 77 | g.drawImage(source, 0, 0, null); 78 | g.dispose(); 79 | 80 | return dest; 81 | } 82 | 83 | 84 | } 85 | -------------------------------------------------------------------------------- /src/main/java/com/defano/jmonet/clipboard/JMonetCanvasFocusDelegate.java: -------------------------------------------------------------------------------- 1 | package com.defano.jmonet.clipboard; 2 | 3 | import com.defano.jmonet.canvas.PaintCanvas; 4 | 5 | import javax.swing.FocusManager; 6 | import java.awt.*; 7 | 8 | /** 9 | * A basic implementation of {@link CanvasFocusDelegate} that returns {@link PaintCanvas} component currently in focus 10 | * (according to the {@link javax.swing.FocusManager}, or null, if no canvas currently has focus. 11 | */ 12 | public class JMonetCanvasFocusDelegate implements CanvasFocusDelegate { 13 | 14 | @Override 15 | public PaintCanvas getCanvasInFocus() { 16 | Component focusOwner = FocusManager.getCurrentManager().getFocusOwner(); 17 | 18 | if (focusOwner instanceof PaintCanvas) { 19 | return (PaintCanvas) focusOwner; 20 | } 21 | 22 | return null; 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/main/java/com/defano/jmonet/clipboard/TransferableImage.java: -------------------------------------------------------------------------------- 1 | package com.defano.jmonet.clipboard; 2 | 3 | import java.awt.datatransfer.DataFlavor; 4 | import java.awt.datatransfer.Transferable; 5 | import java.awt.datatransfer.UnsupportedFlavorException; 6 | import java.awt.image.BufferedImage; 7 | 8 | /** 9 | * A {@link Transferable} wrapper for BufferedImages, used to facilitate cut-copy-paste operations on JMonet images. 10 | */ 11 | class TransferableImage implements Transferable { 12 | 13 | private final BufferedImage image; 14 | 15 | private TransferableImage(BufferedImage image) { 16 | this.image = image; 17 | } 18 | 19 | /** 20 | * Creates a TransferableImage from a standard BufferedImage. 21 | * 22 | * @param image The BufferedImage for transfer. 23 | * @return The TransferableImage 24 | */ 25 | public static TransferableImage from(BufferedImage image) { 26 | return image == null ? null : new TransferableImage(image); 27 | } 28 | 29 | /** {@inheritDoc} */ 30 | @Override 31 | public DataFlavor[] getTransferDataFlavors() { 32 | return new DataFlavor[]{DataFlavor.imageFlavor}; 33 | } 34 | 35 | /** {@inheritDoc} */ 36 | @Override 37 | public boolean isDataFlavorSupported(DataFlavor flavor) { 38 | return flavor == DataFlavor.imageFlavor; 39 | } 40 | 41 | /** {@inheritDoc} */ 42 | @Override 43 | public Object getTransferData(DataFlavor flavor) throws UnsupportedFlavorException { 44 | if (flavor == DataFlavor.imageFlavor) { 45 | return image; 46 | } 47 | 48 | throw new UnsupportedFlavorException(flavor); 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/main/java/com/defano/jmonet/model/FixedQuadrilateral.java: -------------------------------------------------------------------------------- 1 | package com.defano.jmonet.model; 2 | 3 | import java.awt.*; 4 | 5 | /** 6 | * A model of a quadrilateral with fixed dimensions. 7 | */ 8 | @SuppressWarnings("unused") 9 | public class FixedQuadrilateral implements Quadrilateral { 10 | 11 | private final Point topLeft; 12 | private final Point topRight; 13 | private final Point bottomLeft; 14 | private final Point bottomRight; 15 | 16 | public FixedQuadrilateral(Point topLeft, Point topRight, Point bottomLeft, Point bottomRight) { 17 | this.topLeft = topLeft; 18 | this.topRight = topRight; 19 | this.bottomLeft = bottomLeft; 20 | this.bottomRight = bottomRight; 21 | } 22 | 23 | /** 24 | * {@inheritDoc} 25 | */ 26 | @Override 27 | public Point getTopLeft() { 28 | return topLeft; 29 | } 30 | 31 | /** 32 | * {@inheritDoc} 33 | */ 34 | @Override 35 | public Point getTopRight() { 36 | return topRight; 37 | } 38 | 39 | /** 40 | * {@inheritDoc} 41 | */ 42 | @Override 43 | public Point getBottomLeft() { 44 | return bottomLeft; 45 | } 46 | 47 | /** 48 | * {@inheritDoc} 49 | */ 50 | @Override 51 | public Point getBottomRight() { 52 | return bottomRight; 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /src/main/java/com/defano/jmonet/model/Interpolation.java: -------------------------------------------------------------------------------- 1 | package com.defano.jmonet.model; 2 | 3 | /** 4 | * An enumeration of interpolation algorithms. 5 | */ 6 | public enum Interpolation { 7 | 8 | /** 9 | * Specifies that no interpolation should be used. 10 | */ 11 | NONE, 12 | 13 | /** 14 | * Specifies the platform-default interpolation mode should be used. 15 | */ 16 | DEFAULT, 17 | 18 | /** 19 | * Specifies the nearest neighbor interpolation algorithm. 20 | */ 21 | NEAREST_NEIGHBOR, 22 | 23 | /** 24 | * Specifies the bi-cubic interpolation algorithm. 25 | */ 26 | BICUBIC, 27 | 28 | /** 29 | * Specifies the bi-linear interpolation algorithm. 30 | */ 31 | BILINEAR 32 | } 33 | -------------------------------------------------------------------------------- /src/main/java/com/defano/jmonet/model/PaintToolType.java: -------------------------------------------------------------------------------- 1 | package com.defano.jmonet.model; 2 | 3 | import com.defano.jmonet.tools.*; 4 | import com.defano.jmonet.tools.base.Tool; 5 | 6 | /** 7 | * An enumeration of paint tools provided by this library. 8 | */ 9 | public enum PaintToolType { 10 | ARROW(ArrowTool.class), 11 | PENCIL(PencilTool.class), 12 | RECTANGLE(RectangleTool.class), 13 | ROUND_RECTANGLE(RoundRectangleTool.class), 14 | OVAL(OvalTool.class), 15 | PAINTBRUSH(PaintbrushTool.class), 16 | ERASER(EraserTool.class), 17 | LINE(LineTool.class), 18 | POLYGON(PolygonTool.class), 19 | SHAPE(ShapeTool.class), 20 | FREEFORM(FreeformShapeTool.class), 21 | SELECTION(MarqueeTool.class), 22 | LASSO(LassoTool.class), 23 | TEXT(TextTool.class), 24 | FILL(FillTool.class), 25 | AIRBRUSH(AirbrushTool.class), 26 | CURVE(CurveTool.class), 27 | SLANT(SlantTool.class), 28 | ROTATE(RotateTool.class), 29 | SCALE(ScaleTool.class), 30 | MAGNIFIER(MagnifierTool.class), 31 | PROJECTION(ProjectionTool.class), 32 | PERSPECTIVE(PerspectiveTool.class), 33 | RUBBERSHEET(RubberSheetTool.class); 34 | 35 | private final Class toolClass; 36 | 37 | PaintToolType(Class clazz) { 38 | this.toolClass = clazz; 39 | } 40 | 41 | public Class getToolClass() { 42 | return toolClass; 43 | } 44 | 45 | /** 46 | * Determines if the given tool type draws closed-path "shapes" (i.e., elements which can be 47 | * filled with paint. 48 | * 49 | * @return True if the tool draws shapes; false otherwise. 50 | */ 51 | public boolean isShapeTool() { 52 | return this == RECTANGLE || 53 | this == ROUND_RECTANGLE || 54 | this == OVAL || 55 | this == POLYGON || 56 | this == SHAPE || 57 | this == FREEFORM; 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /src/main/java/com/defano/jmonet/tools/AirbrushTool.java: -------------------------------------------------------------------------------- 1 | package com.defano.jmonet.tools; 2 | 3 | import com.defano.jmonet.canvas.Scratch; 4 | import com.defano.jmonet.context.GraphicsContext; 5 | import com.defano.jmonet.model.PaintToolType; 6 | import com.defano.jmonet.tools.base.PathToolDelegate; 7 | import com.defano.jmonet.tools.base.StrokedCursorPathTool; 8 | import com.defano.jmonet.tools.util.MathUtils; 9 | 10 | import java.awt.*; 11 | import java.awt.geom.Line2D; 12 | 13 | /** 14 | * A tool that paints translucent textured paint on the canvas. 15 | */ 16 | public class AirbrushTool extends StrokedCursorPathTool implements PathToolDelegate { 17 | 18 | /** 19 | * Tool must be constructed via {@link com.defano.jmonet.tools.builder.PaintToolBuilder} to handle dependency 20 | * injection. 21 | */ 22 | AirbrushTool() { 23 | super(PaintToolType.AIRBRUSH); 24 | setDelegate(this); 25 | } 26 | 27 | /** {@inheritDoc} */ 28 | @Override 29 | public void startPath(Scratch scratch, Stroke stroke, Paint strokePaint, Point initialPoint) { 30 | // Nothing to do 31 | } 32 | 33 | /** {@inheritDoc} */ 34 | @Override 35 | public void addPoint(Scratch scratch, Stroke stroke, Paint strokePaint, Point lastPoint, Point thisPoint) { 36 | Line2D line = new Line2D.Float(lastPoint, thisPoint); 37 | 38 | GraphicsContext g = scratch.getAddScratchGraphics(this, stroke, line); 39 | g.setStroke(stroke); 40 | g.setPaint(strokePaint); 41 | 42 | if (getAttributes().isPathInterpolated()) { 43 | g.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_OVER, (float) getAttributes().getIntensity() / 10.0f)); 44 | for (Point p : MathUtils.linearInterpolation(lastPoint, thisPoint, 1)) { 45 | g.draw(new Line2D.Float(p, p)); 46 | } 47 | } else { 48 | g.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_OVER, (float) getAttributes().getIntensity())); 49 | g.draw(line); 50 | } 51 | 52 | } 53 | 54 | @Override 55 | public void completePath(Scratch scratch, Stroke stroke, Paint strokePaint, Paint fillPaint) { 56 | // Nothing to do 57 | } 58 | 59 | } 60 | -------------------------------------------------------------------------------- /src/main/java/com/defano/jmonet/tools/ArrowTool.java: -------------------------------------------------------------------------------- 1 | package com.defano.jmonet.tools; 2 | 3 | import com.defano.jmonet.model.PaintToolType; 4 | import com.defano.jmonet.tools.base.BasicTool; 5 | 6 | import java.awt.*; 7 | 8 | /** 9 | * A no-op tool; does not modify the canvas in any way. 10 | */ 11 | public class ArrowTool extends BasicTool { 12 | 13 | /** 14 | * Tool must be constructed via {@link com.defano.jmonet.tools.builder.PaintToolBuilder} to handle dependency 15 | * injection. 16 | */ 17 | ArrowTool() { 18 | super(PaintToolType.ARROW); 19 | } 20 | 21 | @Override 22 | public Cursor getDefaultCursor() { 23 | return Cursor.getDefaultCursor(); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/main/java/com/defano/jmonet/tools/CurveTool.java: -------------------------------------------------------------------------------- 1 | package com.defano.jmonet.tools; 2 | 3 | import com.defano.jmonet.canvas.Scratch; 4 | import com.defano.jmonet.context.GraphicsContext; 5 | import com.defano.jmonet.model.PaintToolType; 6 | import com.defano.jmonet.tools.base.PolylineTool; 7 | import com.defano.jmonet.tools.base.PolylineToolDelegate; 8 | 9 | import java.awt.*; 10 | import java.awt.geom.Path2D; 11 | 12 | /** 13 | * A tool for drawing quadratic (Bezier) curves on the canvas. 14 | */ 15 | public class CurveTool extends PolylineTool implements PolylineToolDelegate { 16 | 17 | /** 18 | * Tool must be constructed via {@link com.defano.jmonet.tools.builder.PaintToolBuilder} to handle dependency 19 | * injection. 20 | */ 21 | CurveTool() { 22 | super(PaintToolType.CURVE); 23 | setDelegate(this); 24 | } 25 | 26 | /** {@inheritDoc} */ 27 | @Override 28 | public void strokePolyline(Scratch scratch, Stroke stroke, Paint paint, int[] xPoints, int[] yPoints) { 29 | Shape curve = renderCurvePath(xPoints, yPoints); 30 | 31 | GraphicsContext g = scratch.getAddScratchGraphics(this, stroke, curve); 32 | g.setPaint(paint); 33 | g.setStroke(stroke); 34 | g.draw(curve); 35 | } 36 | 37 | /** {@inheritDoc} */ 38 | @Override 39 | public void strokePolygon(Scratch scratch, Stroke stroke, Paint strokePaint, int[] xPoints, int[] yPoints) { 40 | Shape curve = renderCurvePath(xPoints, yPoints); 41 | 42 | GraphicsContext g = scratch.getAddScratchGraphics(this, curve); 43 | g.setPaint(strokePaint); 44 | g.setStroke(stroke); 45 | g.draw(renderCurvePath(xPoints, yPoints)); 46 | } 47 | 48 | /** {@inheritDoc} */ 49 | @Override 50 | public void fillPolygon(Scratch scratch, Paint fillPaint, int[] xPoints, int[] yPoints) { 51 | // Not fillable; nothing to do 52 | } 53 | 54 | private Shape renderCurvePath(int[] xPoints, int[] yPoints) { 55 | Path2D path = new Path2D.Double(); 56 | path.moveTo(xPoints[0], yPoints[0]); 57 | 58 | int curveIndex; 59 | for (curveIndex = 0; curveIndex <= xPoints.length - 3; curveIndex += 3) { 60 | path.curveTo(xPoints[curveIndex], yPoints[curveIndex], xPoints[curveIndex + 1], yPoints[curveIndex + 1], xPoints[curveIndex + 2], yPoints[curveIndex + 2]); 61 | } 62 | 63 | if (xPoints.length >= 3 && xPoints.length - curveIndex == 2) { 64 | path.curveTo(xPoints[xPoints.length - 3], yPoints[xPoints.length - 3], xPoints[xPoints.length - 2], yPoints[xPoints.length - 2], xPoints[xPoints.length - 1], yPoints[xPoints.length - 1]); 65 | } else { 66 | path.lineTo(xPoints[xPoints.length - 1], yPoints[xPoints.length - 1]); 67 | } 68 | 69 | return path; 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /src/main/java/com/defano/jmonet/tools/EraserTool.java: -------------------------------------------------------------------------------- 1 | package com.defano.jmonet.tools; 2 | 3 | import com.defano.jmonet.canvas.Scratch; 4 | import com.defano.jmonet.model.PaintToolType; 5 | import com.defano.jmonet.tools.base.PathToolDelegate; 6 | import com.defano.jmonet.tools.base.StrokedCursorPathTool; 7 | 8 | import java.awt.*; 9 | import java.awt.geom.Line2D; 10 | 11 | /** 12 | * Tool that erases pixels from the canvas by turning them back to fully transparent. 13 | */ 14 | public class EraserTool extends StrokedCursorPathTool implements PathToolDelegate { 15 | 16 | /** 17 | * Tool must be constructed via {@link com.defano.jmonet.tools.builder.PaintToolBuilder} to handle dependency 18 | * injection. 19 | */ 20 | EraserTool() { 21 | super(PaintToolType.ERASER); 22 | setDelegate(this); 23 | } 24 | 25 | /** {@inheritDoc} */ 26 | @Override 27 | public void startPath(Scratch scratch, Stroke stroke, Paint strokePaint, Point initialPoint) { 28 | scratch.erase(this, new Line2D.Float(initialPoint, initialPoint), stroke); 29 | } 30 | 31 | /** {@inheritDoc} */ 32 | @Override 33 | public void addPoint(Scratch scratch, Stroke stroke, Paint strokePaint, Point lastPoint, Point thisPoint) { 34 | scratch.erase(this, new Line2D.Float(lastPoint, thisPoint), stroke); 35 | } 36 | 37 | @Override 38 | public void completePath(Scratch scratch, Stroke stroke, Paint strokePaint, Paint fillPaint) { 39 | // Nothing to do 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/main/java/com/defano/jmonet/tools/FillTool.java: -------------------------------------------------------------------------------- 1 | package com.defano.jmonet.tools; 2 | 3 | import com.defano.jmonet.model.PaintToolType; 4 | import com.defano.jmonet.tools.attributes.ToolAttributes; 5 | import com.defano.jmonet.tools.base.BasicTool; 6 | import com.defano.jmonet.tools.builder.PaintToolBuilder; 7 | import com.defano.jmonet.tools.cursors.CursorFactory; 8 | import com.defano.jmonet.transform.image.FloodFillTransform; 9 | import com.google.inject.Inject; 10 | 11 | import java.awt.*; 12 | import java.awt.event.MouseEvent; 13 | import java.util.Optional; 14 | 15 | /** 16 | * Tool that performs a flood-fill of all transparent pixels. 17 | */ 18 | @SuppressWarnings("unused") 19 | public class FillTool extends BasicTool { 20 | 21 | @Inject 22 | private FloodFillTransform floodFill; 23 | 24 | /** 25 | * Tool must be constructed via {@link PaintToolBuilder} to handle dependency 26 | * injection. 27 | */ 28 | FillTool() { 29 | super(PaintToolType.FILL); 30 | } 31 | 32 | @Override 33 | public Cursor getDefaultCursor() { 34 | return CursorFactory.makeBucketCursor(); 35 | } 36 | 37 | /** {@inheritDoc} */ 38 | @Override 39 | public void mousePressed(MouseEvent e, Point imageLocation) { 40 | ToolAttributes attributes = getAttributes(); 41 | Optional fillPaint = attributes.getFillPaint(); 42 | 43 | // Nothing to do if no fill paint is specified 44 | if (fillPaint.isPresent()) { 45 | getScratch().clear(); 46 | 47 | floodFill.setFillPaint(fillPaint.get()); 48 | floodFill.setOrigin(imageLocation); 49 | floodFill.setFill(attributes.getFillFunction()); 50 | floodFill.setBoundaryFunction(attributes.getBoundaryFunction()); 51 | 52 | getScratch().setAddScratch(floodFill.apply(getCanvas().getCanvasImage()), new Rectangle(getCanvas().getCanvasSize())); 53 | 54 | getCanvas().commit(); 55 | getCanvas().repaint(); 56 | } 57 | } 58 | 59 | } 60 | -------------------------------------------------------------------------------- /src/main/java/com/defano/jmonet/tools/FreeformShapeTool.java: -------------------------------------------------------------------------------- 1 | package com.defano.jmonet.tools; 2 | 3 | import com.defano.jmonet.canvas.Scratch; 4 | import com.defano.jmonet.context.GraphicsContext; 5 | import com.defano.jmonet.model.PaintToolType; 6 | import com.defano.jmonet.tools.base.PathTool; 7 | import com.defano.jmonet.tools.base.PathToolDelegate; 8 | import com.defano.jmonet.tools.builder.PaintToolBuilder; 9 | 10 | import java.awt.*; 11 | import java.awt.geom.Path2D; 12 | 13 | /** 14 | * Tool allowing user to draw a free-form path (like a paintbrush) that is closed upon completion 15 | * and can thusly be filled with paint. 16 | */ 17 | public class FreeformShapeTool extends PathTool implements PathToolDelegate { 18 | 19 | private Path2D path; 20 | 21 | /** 22 | * Tool must be constructed via {@link PaintToolBuilder} to handle dependency 23 | * injection. 24 | */ 25 | FreeformShapeTool() { 26 | super(PaintToolType.FREEFORM); 27 | setDelegate(this); 28 | } 29 | 30 | /** {@inheritDoc} */ 31 | @Override 32 | public void startPath(Scratch scratch, Stroke stroke, Paint strokePaint, Point initialPoint) { 33 | path = new Path2D.Double(); 34 | path.moveTo(initialPoint.getX(), initialPoint.getY()); 35 | } 36 | 37 | /** {@inheritDoc} */ 38 | @Override 39 | public void addPoint(Scratch scratch, Stroke stroke, Paint strokePaint, Point lastPoint, Point thisPoint) { 40 | path.lineTo(thisPoint.getX(), thisPoint.getY()); 41 | 42 | GraphicsContext g = scratch.getAddScratchGraphics(this, stroke, path); 43 | g.setStroke(stroke); 44 | g.setPaint(getAttributes().getStrokePaint()); 45 | g.draw(path); 46 | } 47 | 48 | /** {@inheritDoc} */ 49 | @Override 50 | public void completePath(Scratch scratch, Stroke stroke, Paint strokePaint, Paint fillPaint) { 51 | path.closePath(); 52 | 53 | GraphicsContext g = scratch.getAddScratchGraphics(this, stroke, path); 54 | 55 | if (fillPaint != null) { 56 | g.setPaint(fillPaint); 57 | g.fill(path); 58 | } 59 | 60 | g.setStroke(stroke); 61 | g.setPaint(strokePaint); 62 | g.draw(path); 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /src/main/java/com/defano/jmonet/tools/LassoTool.java: -------------------------------------------------------------------------------- 1 | package com.defano.jmonet.tools; 2 | 3 | import com.defano.jmonet.model.PaintToolType; 4 | import com.defano.jmonet.tools.base.SelectionTool; 5 | import com.defano.jmonet.tools.base.SelectionToolDelegate; 6 | import com.defano.jmonet.tools.selection.TransformableCanvasSelection; 7 | import com.defano.jmonet.tools.selection.TransformableSelection; 8 | import com.defano.jmonet.tools.cursors.CursorFactory; 9 | 10 | import java.awt.*; 11 | import java.awt.geom.AffineTransform; 12 | import java.awt.geom.Path2D; 13 | 14 | /** 15 | * Selection tool allowing the user to draw a free-form selection path on the canvas. 16 | */ 17 | public class LassoTool extends SelectionTool implements TransformableSelection, TransformableCanvasSelection, SelectionToolDelegate { 18 | 19 | private Path2D selectionBounds; 20 | 21 | /** 22 | * Tool must be constructed via {@link com.defano.jmonet.tools.builder.PaintToolBuilder} to handle dependency 23 | * injection. 24 | */ 25 | LassoTool() { 26 | super(PaintToolType.LASSO); 27 | 28 | setDelegate(this); 29 | setBoundaryCursor(CursorFactory.makeLassoCursor()); 30 | } 31 | 32 | /** {@inheritDoc} */ 33 | @Override 34 | public void clearSelectionFrame() { 35 | selectionBounds = null; 36 | } 37 | 38 | /** {@inheritDoc} */ 39 | @Override 40 | public void setSelectionFrame(Shape bounds) { 41 | selectionBounds = new Path2D.Double(bounds); 42 | } 43 | 44 | /** {@inheritDoc} */ 45 | @Override 46 | public void addPointToSelectionFrame(Point initialPoint, Point newPoint, boolean isShiftKeyDown) { 47 | if (selectionBounds == null) { 48 | selectionBounds = new Path2D.Double(); 49 | selectionBounds.moveTo(initialPoint.getX(), initialPoint.getY()); 50 | } 51 | 52 | selectionBounds.lineTo(newPoint.x, newPoint.y); 53 | } 54 | 55 | /** {@inheritDoc} */ 56 | @Override 57 | public void closeSelectionFrame(Point finalPoint) { 58 | selectionBounds.closePath(); 59 | } 60 | 61 | /** {@inheritDoc} */ 62 | @Override 63 | public Shape getSelectionFrame() { 64 | return selectionBounds; 65 | } 66 | 67 | /** {@inheritDoc} */ 68 | @Override 69 | public void translateSelectionFrame(int xDelta, int yDelta) { 70 | selectionBounds.transform(AffineTransform.getTranslateInstance(xDelta, yDelta)); 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /src/main/java/com/defano/jmonet/tools/LineTool.java: -------------------------------------------------------------------------------- 1 | package com.defano.jmonet.tools; 2 | 3 | import com.defano.jmonet.canvas.Scratch; 4 | import com.defano.jmonet.context.GraphicsContext; 5 | import com.defano.jmonet.model.PaintToolType; 6 | import com.defano.jmonet.tools.base.LinearTool; 7 | import com.defano.jmonet.tools.base.LinearToolDelegate; 8 | 9 | import java.awt.*; 10 | import java.awt.geom.Line2D; 11 | 12 | /** 13 | * Tool that draws straight lines on the canvas. 14 | */ 15 | public class LineTool extends LinearTool implements LinearToolDelegate { 16 | 17 | /** 18 | * Tool must be constructed via {@link com.defano.jmonet.tools.builder.PaintToolBuilder} to handle dependency 19 | * injection. 20 | */ 21 | LineTool() { 22 | super(PaintToolType.LINE); 23 | setDelegate(this); 24 | } 25 | 26 | /** {@inheritDoc} */ 27 | @Override 28 | public void drawLine(Scratch scratch, Stroke stroke, Paint paint, int x1, int y1, int x2, int y2) { 29 | Line2D line = new Line2D.Float(x1, y1, x2, y2); 30 | 31 | GraphicsContext g = scratch.getAddScratchGraphics(this, stroke, line); 32 | g.setPaint(paint); 33 | g.setStroke(stroke); 34 | g.draw(line); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/main/java/com/defano/jmonet/tools/MarqueeTool.java: -------------------------------------------------------------------------------- 1 | package com.defano.jmonet.tools; 2 | 3 | import com.defano.jmonet.model.PaintToolType; 4 | import com.defano.jmonet.tools.base.SelectionTool; 5 | import com.defano.jmonet.tools.base.SelectionToolDelegate; 6 | import com.defano.jmonet.tools.selection.TransformableCanvasSelection; 7 | import com.defano.jmonet.tools.selection.TransformableSelection; 8 | import com.defano.jmonet.tools.util.MathUtils; 9 | 10 | import java.awt.*; 11 | 12 | /** 13 | * A tool for drawing a rectangular selection on the canvas. 14 | */ 15 | public class MarqueeTool extends SelectionTool implements TransformableSelection, TransformableCanvasSelection, SelectionToolDelegate { 16 | 17 | private Rectangle selectionBounds; 18 | 19 | /** 20 | * Tool must be constructed via {@link com.defano.jmonet.tools.builder.PaintToolBuilder} to handle dependency 21 | * injection. 22 | */ 23 | MarqueeTool() { 24 | super(PaintToolType.SELECTION); 25 | setDelegate(this); 26 | } 27 | 28 | /** {@inheritDoc} */ 29 | @Override 30 | public Shape getSelectionFrame() { 31 | return selectionBounds; 32 | } 33 | 34 | /** {@inheritDoc} */ 35 | @Override 36 | public void addPointToSelectionFrame(Point initialPoint, Point newPoint, boolean isShiftKeyDown) { 37 | selectionBounds = new Rectangle(initialPoint); 38 | selectionBounds.add(newPoint); 39 | 40 | // #24: Disallow constraint if it results in selection outside canvas bounds 41 | Rectangle canvasBounds = new Rectangle(0, 0, getCanvas().getCanvasSize().width, getCanvas().getCanvasSize().height); 42 | if (isShiftKeyDown && canvasBounds.contains(MathUtils.square(initialPoint, newPoint))) { 43 | selectionBounds = MathUtils.square(initialPoint, newPoint); 44 | } else { 45 | selectionBounds = MathUtils.rectangle(initialPoint, newPoint); 46 | } 47 | } 48 | 49 | /** {@inheritDoc} */ 50 | @Override 51 | public void closeSelectionFrame(Point finalPoint) { 52 | // Nothing to do 53 | } 54 | 55 | /** {@inheritDoc} */ 56 | @Override 57 | public void clearSelectionFrame() { 58 | selectionBounds = null; 59 | } 60 | 61 | /** {@inheritDoc} */ 62 | @Override 63 | public void setSelectionFrame(Shape bounds) { 64 | selectionBounds = bounds.getBounds(); 65 | } 66 | 67 | /** {@inheritDoc} */ 68 | @Override 69 | public void translateSelectionFrame(int xDelta, int yDelta) { 70 | selectionBounds.setLocation(selectionBounds.x + xDelta, selectionBounds.y + yDelta); 71 | } 72 | 73 | } 74 | -------------------------------------------------------------------------------- /src/main/java/com/defano/jmonet/tools/OvalTool.java: -------------------------------------------------------------------------------- 1 | package com.defano.jmonet.tools; 2 | 3 | import com.defano.jmonet.canvas.Scratch; 4 | import com.defano.jmonet.context.GraphicsContext; 5 | import com.defano.jmonet.model.PaintToolType; 6 | import com.defano.jmonet.tools.base.BoundsTool; 7 | import com.defano.jmonet.tools.base.BoundsToolDelegate; 8 | 9 | import java.awt.*; 10 | import java.awt.geom.Ellipse2D; 11 | 12 | /** 13 | * Tool for drawing outlined or filled ovals/circles on the canvas. 14 | */ 15 | public class OvalTool extends BoundsTool implements BoundsToolDelegate { 16 | 17 | /** 18 | * Tool must be constructed via {@link com.defano.jmonet.tools.builder.PaintToolBuilder} to handle dependency 19 | * injection. 20 | */ 21 | OvalTool() { 22 | super(PaintToolType.OVAL); 23 | setDelegate(this); 24 | } 25 | 26 | /** {@inheritDoc} */ 27 | @Override 28 | public void strokeBounds(Scratch scratch, Stroke stroke, Paint paint, Rectangle bounds, boolean isShiftDown) { 29 | Ellipse2D oval = new Ellipse2D.Float(bounds.x, bounds.y, bounds.width, bounds.height); 30 | 31 | GraphicsContext g = scratch.getAddScratchGraphics(this, stroke, oval); 32 | g.setStroke(stroke); 33 | g.setPaint(paint); 34 | g.draw(oval); 35 | } 36 | 37 | /** {@inheritDoc} */ 38 | @Override 39 | public void fillBounds(Scratch scratch, Paint fill, Rectangle bounds, boolean isShiftDown) { 40 | GraphicsContext g = scratch.getAddScratchGraphics(this, null); 41 | g.setPaint(fill); 42 | g.fillOval(bounds.x, bounds.y, bounds.width, bounds.height); 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/main/java/com/defano/jmonet/tools/PaintbrushTool.java: -------------------------------------------------------------------------------- 1 | package com.defano.jmonet.tools; 2 | 3 | import com.defano.jmonet.canvas.Scratch; 4 | import com.defano.jmonet.context.GraphicsContext; 5 | import com.defano.jmonet.model.PaintToolType; 6 | import com.defano.jmonet.tools.base.PathToolDelegate; 7 | import com.defano.jmonet.tools.base.StrokedCursorPathTool; 8 | 9 | import java.awt.*; 10 | import java.awt.geom.Line2D; 11 | 12 | /** 13 | * Tool for drawing free-form, textured paths on the canvas. 14 | */ 15 | public class PaintbrushTool extends StrokedCursorPathTool implements PathToolDelegate { 16 | 17 | /** 18 | * Tool must be constructed via {@link com.defano.jmonet.tools.builder.PaintToolBuilder} to handle dependency 19 | * injection. 20 | */ 21 | PaintbrushTool() { 22 | super(PaintToolType.PAINTBRUSH); 23 | setDelegate(this); 24 | } 25 | 26 | /** {@inheritDoc} */ 27 | @Override 28 | public void startPath(Scratch scratch, Stroke stroke, Paint strokePaint, Point initialPoint) { 29 | Line2D line = new Line2D.Float(initialPoint, initialPoint); 30 | 31 | GraphicsContext g = scratch.getAddScratchGraphics(this, stroke, line); 32 | g.setStroke(stroke); 33 | g.setPaint(strokePaint); 34 | g.draw(line); 35 | 36 | } 37 | 38 | /** {@inheritDoc} */ 39 | @Override 40 | public void addPoint(Scratch scratch, Stroke stroke, Paint strokePaint, Point lastPoint, Point thisPoint) { 41 | Line2D line = new Line2D.Float(lastPoint, thisPoint); 42 | 43 | GraphicsContext g = scratch.getAddScratchGraphics(this, stroke, line); 44 | g.setStroke(stroke); 45 | g.setPaint(strokePaint); 46 | g.draw(line); 47 | } 48 | 49 | @Override 50 | public void completePath(Scratch scratch, Stroke stroke, Paint strokePaint, Paint fillPaint) { 51 | // Nothing to do 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /src/main/java/com/defano/jmonet/tools/PencilTool.java: -------------------------------------------------------------------------------- 1 | package com.defano.jmonet.tools; 2 | 3 | import com.defano.jmonet.canvas.Scratch; 4 | import com.defano.jmonet.context.GraphicsContext; 5 | import com.defano.jmonet.model.PaintToolType; 6 | import com.defano.jmonet.tools.base.PathTool; 7 | import com.defano.jmonet.tools.attributes.ToolAttributes; 8 | import com.defano.jmonet.tools.base.PathToolDelegate; 9 | import com.defano.jmonet.tools.cursors.CursorFactory; 10 | 11 | import java.awt.*; 12 | import java.awt.geom.Line2D; 13 | 14 | /** 15 | * Tool for drawing or erasing a single-pixel, free-form path on the canvas. 16 | */ 17 | public class PencilTool extends PathTool implements PathToolDelegate { 18 | 19 | // Flag indicating whether pencil is operating in eraser mode 20 | private boolean isErasing = false; 21 | 22 | /** 23 | * Tool must be constructed via {@link com.defano.jmonet.tools.builder.PaintToolBuilder} to handle dependency 24 | * injection. 25 | */ 26 | PencilTool() { 27 | super(PaintToolType.PENCIL); 28 | setDelegate(this); 29 | } 30 | 31 | @Override 32 | public Cursor getDefaultCursor() { 33 | return CursorFactory.makePencilCursor(); 34 | } 35 | 36 | /** {@inheritDoc} */ 37 | @Override 38 | public void startPath(Scratch scratch, Stroke stroke, Paint strokePaint, Point initialPoint) { 39 | ToolAttributes attributes = getAttributes(); 40 | 41 | Color pixel = new Color(getCanvas().getCanvasImage().getRGB(initialPoint.x, initialPoint.y), true); 42 | 43 | // Pencil erases when user begins stoke over a "marked" pixel, otherwise pencil marks canvas 44 | isErasing = attributes.getMarkPredicate().isMarked(pixel, attributes.getEraseColor()); 45 | 46 | renderStroke(scratch, strokePaint, new Line2D.Float(initialPoint, initialPoint)); 47 | } 48 | 49 | /** {@inheritDoc} */ 50 | @Override 51 | public void addPoint(Scratch scratch, Stroke stroke, Paint strokePaint, Point lastPoint, Point thisPoint) { 52 | renderStroke(scratch, strokePaint, new Line2D.Float(lastPoint, thisPoint)); 53 | } 54 | 55 | @Override 56 | public void completePath(Scratch scratch, Stroke stroke, Paint strokePaint, Paint fillPaint) { 57 | // Nothing to do 58 | } 59 | 60 | private void renderStroke(Scratch scratch, Paint fillPaint, Line2D line) { 61 | if (isErasing) { 62 | scratch.erase(this, line, new BasicStroke(1)); 63 | } 64 | 65 | else { 66 | GraphicsContext g = scratch.getAddScratchGraphics(this, new BasicStroke(1), line); 67 | g.setStroke(new BasicStroke(1)); 68 | g.setPaint(fillPaint); 69 | g.draw(line); 70 | } 71 | } 72 | 73 | } 74 | -------------------------------------------------------------------------------- /src/main/java/com/defano/jmonet/tools/PerspectiveTool.java: -------------------------------------------------------------------------------- 1 | package com.defano.jmonet.tools; 2 | 3 | import com.defano.jmonet.tools.base.TransformToolDelegate; 4 | import com.defano.jmonet.transform.image.ProjectionTransform; 5 | import com.defano.jmonet.model.FlexQuadrilateral; 6 | import com.defano.jmonet.model.PaintToolType; 7 | import com.defano.jmonet.tools.base.TransformTool; 8 | 9 | import java.awt.*; 10 | 11 | /** 12 | * Tool for making either the left or right side appear closer/further away than the other. 13 | */ 14 | public class PerspectiveTool extends TransformTool implements TransformToolDelegate { 15 | 16 | /** 17 | * Tool must be constructed via {@link com.defano.jmonet.tools.builder.PaintToolBuilder} to handle dependency 18 | * injection. 19 | */ 20 | PerspectiveTool() { 21 | super(PaintToolType.PERSPECTIVE); 22 | setTransformToolDelegate(this); 23 | } 24 | 25 | /** {@inheritDoc} */ 26 | @Override 27 | public void moveTopLeft(FlexQuadrilateral quadrilateral, Point newPosition, boolean isShiftDown) { 28 | int bottomLeft = quadrilateral.getBottomLeft().y - (newPosition.y - quadrilateral.getTopLeft().y); 29 | 30 | quadrilateral.setBottomLeft(new Point(quadrilateral.getBottomLeft().x, bottomLeft)); 31 | quadrilateral.setTopLeft(new Point(quadrilateral.getTopLeft().x, newPosition.y)); 32 | 33 | setSelectedImage(new ProjectionTransform(quadrilateral.translate(getSelectedImageLocation().x, getSelectedImageLocation().y)).apply(getOriginalImage())); 34 | } 35 | 36 | /** {@inheritDoc} */ 37 | @Override 38 | public void moveTopRight(FlexQuadrilateral quadrilateral, Point newPosition, boolean isShiftDown) { 39 | int bottomRight = quadrilateral.getBottomRight().y - (newPosition.y - quadrilateral.getTopRight().y); 40 | 41 | quadrilateral.setBottomRight(new Point(quadrilateral.getBottomRight().x, bottomRight)); 42 | quadrilateral.setTopRight(new Point(quadrilateral.getTopRight().x, newPosition.y)); 43 | 44 | setSelectedImage(new ProjectionTransform(quadrilateral.translate(getSelectedImageLocation().x, getSelectedImageLocation().y)).apply(getOriginalImage())); 45 | } 46 | 47 | /** {@inheritDoc} */ 48 | @Override 49 | public void moveBottomLeft(FlexQuadrilateral quadrilateral, Point newPosition, boolean isShiftDown) { 50 | int topLeft = quadrilateral.getTopLeft().y - (newPosition.y - quadrilateral.getBottomLeft().y); 51 | 52 | quadrilateral.setTopLeft(new Point(quadrilateral.getTopLeft().x, topLeft)); 53 | quadrilateral.setBottomLeft(new Point(quadrilateral.getBottomLeft().x, newPosition.y)); 54 | 55 | setSelectedImage(new ProjectionTransform(quadrilateral.translate(getSelectedImageLocation().x, getSelectedImageLocation().y)).apply(getOriginalImage())); 56 | } 57 | 58 | /** {@inheritDoc} */ 59 | @Override 60 | public void moveBottomRight(FlexQuadrilateral quadrilateral, Point newPosition, boolean isShiftDown) { 61 | int topRight = quadrilateral.getTopRight().y - (newPosition.y - quadrilateral.getBottomRight().y); 62 | 63 | quadrilateral.setTopRight(new Point(quadrilateral.getTopRight().x, topRight)); 64 | quadrilateral.setBottomRight(new Point(quadrilateral.getBottomRight().x, newPosition.y)); 65 | 66 | setSelectedImage(new ProjectionTransform(quadrilateral.translate(getSelectedImageLocation().x, getSelectedImageLocation().y)).apply(getOriginalImage())); 67 | } 68 | 69 | } 70 | -------------------------------------------------------------------------------- /src/main/java/com/defano/jmonet/tools/PolygonTool.java: -------------------------------------------------------------------------------- 1 | package com.defano.jmonet.tools; 2 | 3 | import com.defano.jmonet.canvas.Scratch; 4 | import com.defano.jmonet.context.GraphicsContext; 5 | import com.defano.jmonet.model.PaintToolType; 6 | import com.defano.jmonet.tools.base.PolylineTool; 7 | import com.defano.jmonet.tools.base.PolylineToolDelegate; 8 | 9 | import java.awt.*; 10 | import java.awt.geom.Path2D; 11 | 12 | /** 13 | * Tool to draw outlined or filled irregular polygons on the canvas. 14 | */ 15 | public class PolygonTool extends PolylineTool implements PolylineToolDelegate { 16 | 17 | /** 18 | * Tool must be constructed via {@link com.defano.jmonet.tools.builder.PaintToolBuilder} to handle dependency 19 | * injection. 20 | */ 21 | PolygonTool() { 22 | super(PaintToolType.POLYGON); 23 | setDelegate(this); 24 | } 25 | 26 | /** {@inheritDoc} */ 27 | @Override 28 | public void strokePolyline(Scratch scratch, Stroke stroke, Paint paint, int[] xPoints, int[] yPoints) { 29 | Path2D poly = getPolylineShape(xPoints, yPoints, xPoints.length); 30 | 31 | GraphicsContext g = scratch.getAddScratchGraphics(this, stroke, poly); 32 | g.setPaint(paint); 33 | g.setStroke(stroke); 34 | g.draw(poly); 35 | } 36 | 37 | /** {@inheritDoc} */ 38 | @Override 39 | public void strokePolygon(Scratch scratch, Stroke stroke, Paint strokePaint, int[] xPoints, int[] yPoints) { 40 | Path2D polygon = getPolygonShape(xPoints, yPoints, xPoints.length); 41 | 42 | GraphicsContext g = scratch.getAddScratchGraphics(this, stroke, polygon); 43 | g.setStroke(stroke); 44 | g.setPaint(strokePaint); 45 | g.draw(polygon); 46 | } 47 | 48 | /** {@inheritDoc} */ 49 | @Override 50 | public void fillPolygon(Scratch scratch, Paint fillPaint, int[] xPoints, int[] yPoints) { 51 | GraphicsContext g = scratch.getAddScratchGraphics(this, null); 52 | g.setPaint(fillPaint); 53 | g.fillPolygon(xPoints, yPoints, xPoints.length); 54 | } 55 | 56 | private Path2D getPolygonShape(int[] xPoints, int[] yPoints, int points) { 57 | Path2D poly = getPolylineShape(xPoints, yPoints, points); 58 | poly.closePath(); 59 | return poly; 60 | } 61 | 62 | private Path2D getPolylineShape(int[] xPoints, int[] yPoints, int points) { 63 | Path2D poly = new Path2D.Double(); 64 | 65 | if (points > 0) { 66 | poly.moveTo(xPoints[0], yPoints[0]); 67 | 68 | for (int index = 1; index < points; index++) { 69 | poly.lineTo(xPoints[index], yPoints[index]); 70 | } 71 | } 72 | 73 | return poly; 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /src/main/java/com/defano/jmonet/tools/ProjectionTool.java: -------------------------------------------------------------------------------- 1 | package com.defano.jmonet.tools; 2 | 3 | import com.defano.jmonet.tools.base.TransformToolDelegate; 4 | import com.defano.jmonet.transform.image.ProjectionTransform; 5 | import com.defano.jmonet.model.FlexQuadrilateral; 6 | import com.defano.jmonet.model.PaintToolType; 7 | import com.defano.jmonet.tools.base.TransformTool; 8 | 9 | import java.awt.*; 10 | 11 | /** 12 | * Tool for performing a projection of a selected image onto an arbitrary quadrilateral. 13 | */ 14 | public class ProjectionTool extends TransformTool implements TransformToolDelegate { 15 | 16 | /** 17 | * Tool must be constructed via {@link com.defano.jmonet.tools.builder.PaintToolBuilder} to handle dependency 18 | * injection. 19 | */ 20 | ProjectionTool() { 21 | super(PaintToolType.PROJECTION); 22 | setTransformToolDelegate(this); 23 | } 24 | 25 | /** {@inheritDoc} */ 26 | @Override 27 | public void moveTopLeft(FlexQuadrilateral quadrilateral, Point newPosition, boolean isShiftDown) { 28 | quadrilateral.setTopLeft(newPosition); 29 | setSelectedImage(new ProjectionTransform(quadrilateral.translate(getSelectedImageLocation().x, getSelectedImageLocation().y)).apply(getOriginalImage())); 30 | } 31 | 32 | /** {@inheritDoc} */ 33 | @Override 34 | public void moveTopRight(FlexQuadrilateral quadrilateral, Point newPosition, boolean isShiftDown) { 35 | quadrilateral.setTopRight(newPosition); 36 | setSelectedImage(new ProjectionTransform(quadrilateral.translate(getSelectedImageLocation().x, getSelectedImageLocation().y)).apply(getOriginalImage())); 37 | } 38 | 39 | /** {@inheritDoc} */ 40 | @Override 41 | public void moveBottomLeft(FlexQuadrilateral quadrilateral, Point newPosition, boolean isShiftDown) { 42 | quadrilateral.setBottomLeft(newPosition); 43 | setSelectedImage(new ProjectionTransform(quadrilateral.translate(getSelectedImageLocation().x, getSelectedImageLocation().y)).apply(getOriginalImage())); 44 | } 45 | 46 | /** {@inheritDoc} */ 47 | @Override 48 | public void moveBottomRight(FlexQuadrilateral quadrilateral, Point newPosition, boolean isShiftDown) { 49 | quadrilateral.setBottomRight(newPosition); 50 | setSelectedImage(new ProjectionTransform(quadrilateral.translate(getSelectedImageLocation().x, getSelectedImageLocation().y)).apply(getOriginalImage())); 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/main/java/com/defano/jmonet/tools/RectangleTool.java: -------------------------------------------------------------------------------- 1 | package com.defano.jmonet.tools; 2 | 3 | import com.defano.jmonet.canvas.Scratch; 4 | import com.defano.jmonet.context.GraphicsContext; 5 | import com.defano.jmonet.model.PaintToolType; 6 | import com.defano.jmonet.tools.base.BoundsTool; 7 | import com.defano.jmonet.tools.base.BoundsToolDelegate; 8 | 9 | import java.awt.*; 10 | 11 | /** 12 | * Draws outlined or filled rectangles/squares on the canvas. 13 | */ 14 | public class RectangleTool extends BoundsTool implements BoundsToolDelegate { 15 | 16 | /** 17 | * Tool must be constructed via {@link com.defano.jmonet.tools.builder.PaintToolBuilder} to handle dependency 18 | * injection. 19 | */ 20 | RectangleTool() { 21 | super(PaintToolType.RECTANGLE); 22 | setDelegate(this); 23 | } 24 | 25 | /** {@inheritDoc} */ 26 | @Override 27 | public void strokeBounds(Scratch scratch, Stroke stroke, Paint paint, Rectangle bounds, boolean isShiftDown) { 28 | Rectangle rectangle = new Rectangle(bounds.x, bounds.y, bounds.width, bounds.height); 29 | 30 | GraphicsContext g = scratch.getAddScratchGraphics(this, stroke, rectangle); 31 | g.setStroke(stroke); 32 | g.setPaint(paint); 33 | g.draw(rectangle); 34 | } 35 | 36 | /** {@inheritDoc} */ 37 | @Override 38 | public void fillBounds(Scratch scratch, Paint fill, Rectangle bounds, boolean isShiftDown) { 39 | Rectangle rectangle = new Rectangle(bounds.x, bounds.y, bounds.width, bounds.height); 40 | 41 | GraphicsContext g = scratch.getAddScratchGraphics(this, rectangle); 42 | g.setPaint(fill); 43 | g.fill(rectangle); 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/main/java/com/defano/jmonet/tools/RoundRectangleTool.java: -------------------------------------------------------------------------------- 1 | package com.defano.jmonet.tools; 2 | 3 | import com.defano.jmonet.canvas.Scratch; 4 | import com.defano.jmonet.context.GraphicsContext; 5 | import com.defano.jmonet.model.PaintToolType; 6 | import com.defano.jmonet.tools.base.BoundsTool; 7 | import com.defano.jmonet.tools.base.BoundsToolDelegate; 8 | 9 | import java.awt.*; 10 | import java.awt.geom.RoundRectangle2D; 11 | 12 | /** 13 | * Tool for drawing outlined or filled rounded-rectangles on the canvas. 14 | */ 15 | public class RoundRectangleTool extends BoundsTool implements BoundsToolDelegate { 16 | 17 | /** 18 | * Tool must be constructed via {@link com.defano.jmonet.tools.builder.PaintToolBuilder} to handle dependency 19 | * injection. 20 | */ 21 | RoundRectangleTool() { 22 | super(PaintToolType.ROUND_RECTANGLE); 23 | setDelegate(this); 24 | } 25 | 26 | /** {@inheritDoc} */ 27 | @Override 28 | public void strokeBounds(Scratch scratch, Stroke stroke, Paint paint, Rectangle bounds, boolean isShiftDown) { 29 | int cornerRadius = getAttributes().getCornerRadius(); 30 | RoundRectangle2D roundRect = new RoundRectangle2D.Double(bounds.x, bounds.y, bounds.width, bounds.height, cornerRadius, cornerRadius); 31 | 32 | GraphicsContext g = scratch.getAddScratchGraphics(this, stroke, roundRect); 33 | g.setPaint(paint); 34 | g.setStroke(stroke); 35 | g.draw(roundRect); 36 | } 37 | 38 | /** {@inheritDoc} */ 39 | @Override 40 | public void fillBounds(Scratch scratch, Paint fill, Rectangle bounds, boolean isShiftDown) { 41 | int cornerRadius = getAttributes().getCornerRadius(); 42 | RoundRectangle2D roundRect = new RoundRectangle2D.Double(bounds.x, bounds.y, bounds.width, bounds.height, cornerRadius, cornerRadius); 43 | 44 | GraphicsContext g = scratch.getAddScratchGraphics(this, roundRect); 45 | g.setPaint(fill); 46 | g.fillRoundRect(bounds.x, bounds.y, bounds.width, bounds.height, cornerRadius, cornerRadius); 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/main/java/com/defano/jmonet/tools/RubberSheetTool.java: -------------------------------------------------------------------------------- 1 | package com.defano.jmonet.tools; 2 | 3 | import com.defano.jmonet.tools.base.TransformToolDelegate; 4 | import com.defano.jmonet.transform.image.RubbersheetTransform; 5 | import com.defano.jmonet.model.FlexQuadrilateral; 6 | import com.defano.jmonet.model.PaintToolType; 7 | import com.defano.jmonet.tools.base.TransformTool; 8 | 9 | import java.awt.*; 10 | 11 | /** 12 | * Tool for performing a rubber sheet projection of the image. 13 | */ 14 | public class RubberSheetTool extends TransformTool implements TransformToolDelegate { 15 | 16 | /** 17 | * Tool must be constructed via {@link com.defano.jmonet.tools.builder.PaintToolBuilder} to handle dependency 18 | * injection. 19 | */ 20 | RubberSheetTool() { 21 | super(PaintToolType.RUBBERSHEET); 22 | setTransformToolDelegate(this); 23 | } 24 | 25 | /** {@inheritDoc} */ 26 | @Override 27 | public void moveTopLeft(FlexQuadrilateral quadrilateral, Point newPosition, boolean isShiftDown) { 28 | quadrilateral.setTopLeft(newPosition); 29 | setSelectedImage(new RubbersheetTransform(quadrilateral.translate(getSelectedImageLocation().x, getSelectedImageLocation().y)).apply(getOriginalImage())); 30 | } 31 | 32 | /** {@inheritDoc} */ 33 | @Override 34 | public void moveTopRight(FlexQuadrilateral quadrilateral, Point newPosition, boolean isShiftDown) { 35 | quadrilateral.setTopRight(newPosition); 36 | setSelectedImage(new RubbersheetTransform(quadrilateral.translate(getSelectedImageLocation().x, getSelectedImageLocation().y)).apply(getOriginalImage())); 37 | } 38 | 39 | /** {@inheritDoc} */ 40 | @Override 41 | public void moveBottomLeft(FlexQuadrilateral quadrilateral, Point newPosition, boolean isShiftDown) { 42 | quadrilateral.setBottomLeft(newPosition); 43 | setSelectedImage(new RubbersheetTransform(quadrilateral.translate(getSelectedImageLocation().x, getSelectedImageLocation().y)).apply(getOriginalImage())); 44 | } 45 | 46 | /** {@inheritDoc} */ 47 | @Override 48 | public void moveBottomRight(FlexQuadrilateral quadrilateral, Point newPosition, boolean isShiftDown) { 49 | quadrilateral.setBottomRight(newPosition); 50 | setSelectedImage(new RubbersheetTransform(quadrilateral.translate(getSelectedImageLocation().x, getSelectedImageLocation().y)).apply(getOriginalImage())); 51 | } 52 | 53 | } 54 | -------------------------------------------------------------------------------- /src/main/java/com/defano/jmonet/tools/ShapeTool.java: -------------------------------------------------------------------------------- 1 | package com.defano.jmonet.tools; 2 | 3 | import com.defano.jmonet.canvas.Scratch; 4 | import com.defano.jmonet.context.GraphicsContext; 5 | import com.defano.jmonet.model.PaintToolType; 6 | import com.defano.jmonet.tools.base.BoundsTool; 7 | import com.defano.jmonet.tools.base.BoundsToolDelegate; 8 | import com.defano.jmonet.tools.util.MathUtils; 9 | 10 | import java.awt.*; 11 | 12 | /** 13 | * Tool for drawing regular polygons ("shapes") based on a configurable number of sides. For example, triangles, 14 | * squares, pentagons, hexagons, etc. 15 | */ 16 | public class ShapeTool extends BoundsTool implements BoundsToolDelegate { 17 | 18 | /** 19 | * Tool must be constructed via {@link com.defano.jmonet.tools.builder.PaintToolBuilder} to handle dependency 20 | * injection. 21 | */ 22 | ShapeTool() { 23 | super(PaintToolType.SHAPE); 24 | setDelegate(this); 25 | } 26 | 27 | /** {@inheritDoc} */ 28 | @Override 29 | public void strokeBounds(Scratch scratch, Stroke stroke, Paint paint, Rectangle bounds, boolean isShiftDown) { 30 | Polygon poly = MathUtils.polygon(getInitialPoint(), getAttributes().getShapeSides(), getRadius(), getRotationAngle(isShiftDown)); 31 | 32 | GraphicsContext g = scratch.getAddScratchGraphics(this, stroke, poly); 33 | g.setStroke(stroke); 34 | g.setPaint(paint); 35 | g.draw(poly); 36 | } 37 | 38 | /** {@inheritDoc} */ 39 | @Override 40 | public void fillBounds(Scratch scratch, Paint fill, Rectangle bounds, boolean isShiftDown) { 41 | GraphicsContext g = scratch.getAddScratchGraphics(this, null); 42 | g.setPaint(fill); 43 | g.fill(MathUtils.polygon(getInitialPoint(), getAttributes().getShapeSides(), getRadius(), getRotationAngle(isShiftDown))); 44 | } 45 | 46 | private double getRadius() { 47 | return MathUtils.distance(getInitialPoint(), getCurrentPoint()); 48 | } 49 | 50 | private double getRotationAngle(boolean isShiftDown) { 51 | double degrees = MathUtils.angle(getInitialPoint().x, getInitialPoint().y, getCurrentPoint().x, getCurrentPoint().y); 52 | 53 | if (isShiftDown) { 54 | degrees = MathUtils.nearestRound(degrees, getAttributes().getConstrainedAngle()); 55 | } 56 | 57 | return Math.toRadians(degrees); 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /src/main/java/com/defano/jmonet/tools/SlantTool.java: -------------------------------------------------------------------------------- 1 | package com.defano.jmonet.tools; 2 | 3 | import com.defano.jmonet.tools.base.TransformToolDelegate; 4 | import com.defano.jmonet.transform.image.SlantTransform; 5 | import com.defano.jmonet.model.PaintToolType; 6 | import com.defano.jmonet.model.FlexQuadrilateral; 7 | import com.defano.jmonet.tools.base.TransformTool; 8 | import com.defano.jmonet.tools.util.MathUtils; 9 | 10 | import java.awt.*; 11 | 12 | /** 13 | * Tool for drawing a rectangular selection boundary with drag-handles to shear/slant the image from the top or bottom. 14 | */ 15 | public class SlantTool extends TransformTool implements TransformToolDelegate { 16 | 17 | /** 18 | * Tool must be constructed via {@link com.defano.jmonet.tools.builder.PaintToolBuilder} to handle dependency 19 | * injection. 20 | */ 21 | SlantTool() { 22 | super(PaintToolType.SLANT); 23 | setTransformToolDelegate(this); 24 | } 25 | 26 | /** {@inheritDoc} */ 27 | @Override 28 | public void moveTopLeft(FlexQuadrilateral quadrilateral, Point newPosition, boolean isShiftDown) { 29 | quadrilateral.getTopRight().x += newPosition.x - quadrilateral.getTopLeft().x; 30 | quadrilateral.getTopLeft().x = newPosition.x; 31 | 32 | int xTranslation = (quadrilateral.getTopLeft().x - getSelectionFrame().getBounds().x) / 2; 33 | setSelectedImage(new SlantTransform(getTheta(quadrilateral), xTranslation).apply(getOriginalImage())); 34 | } 35 | 36 | /** {@inheritDoc} */ 37 | @Override 38 | public void moveTopRight(FlexQuadrilateral quadrilateral, Point newPosition, boolean isShiftDown) { 39 | quadrilateral.getTopLeft().x += newPosition.x - quadrilateral.getTopRight().x; 40 | quadrilateral.getTopRight().x = newPosition.x; 41 | 42 | int xTranslation = (quadrilateral.getTopLeft().x - getSelectionFrame().getBounds().x) / 2; 43 | setSelectedImage(new SlantTransform(getTheta(quadrilateral), xTranslation).apply(getOriginalImage())); 44 | } 45 | 46 | /** {@inheritDoc} */ 47 | @Override 48 | public void moveBottomLeft(FlexQuadrilateral quadrilateral, Point newPosition, boolean isShiftDown) { 49 | quadrilateral.getBottomRight().x += newPosition.x - quadrilateral.getBottomLeft().x; 50 | quadrilateral.getBottomLeft().x = newPosition.x; 51 | 52 | int xTranslation = ((getSelectionFrame().getBounds().x + getSelectionFrame().getBounds().width) - quadrilateral.getBottomRight().x) / 2; 53 | setSelectedImage(new SlantTransform(getTheta(quadrilateral), xTranslation).apply(getOriginalImage())); 54 | } 55 | 56 | /** {@inheritDoc} */ 57 | @Override 58 | public void moveBottomRight(FlexQuadrilateral quadrilateral, Point newPosition, boolean isShiftDown) { 59 | quadrilateral.getBottomLeft().x += newPosition.x - quadrilateral.getBottomRight().x; 60 | quadrilateral.getBottomRight().x = newPosition.x; 61 | 62 | int xTranslation = ((getSelectionFrame().getBounds().x + getSelectionFrame().getBounds().width) - quadrilateral.getBottomRight().x) / 2; 63 | setSelectedImage(new SlantTransform(getTheta(quadrilateral), xTranslation).apply(getOriginalImage())); 64 | } 65 | 66 | private double getTheta(FlexQuadrilateral quadrilateral) { 67 | Point p = new Point(quadrilateral.getBottomLeft().x, quadrilateral.getTopLeft().y); 68 | return MathUtils.theta(quadrilateral.getBottomLeft(), p, quadrilateral.getTopLeft()); 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /src/main/java/com/defano/jmonet/tools/attributes/BoundaryFunction.java: -------------------------------------------------------------------------------- 1 | package com.defano.jmonet.tools.attributes; 2 | 3 | import java.awt.*; 4 | import java.awt.image.BufferedImage; 5 | 6 | /** 7 | * A function for determining boundaries when flood-filling a raster. 8 | */ 9 | public interface BoundaryFunction { 10 | 11 | /** 12 | * Determines if a given pixel on the canvas should be flood-filled. This method will be invoked repeatedly until 13 | * the flood fill algorithm has filled all available pixels, thus, this method should execute quickly. 14 | *

15 | * When flood-filling an image, all pixels adjacent to the selected pixel will attempt to be recursively "filled" 16 | * with paint. This method will be invoked with each point to determine when the flood fill has reached a pixel or 17 | * image boundary that should not be filled. 18 | *

19 | * NOTE: This method may be called more than once with the same point. To prevent an infinite loop, this method 20 | * must return 'false' for any point that previously returned true (i.e., any flood-filled pixel must constitute a 21 | * boundary). 22 | * 23 | * @param canvas The existing canvas image to be filled (that is, the user's image prior to any fill-related 24 | * changes). 25 | * @param scratch The scratch buffer containing just the flood-fill changes (does not contain any of the user's 26 | * image committed to the canvas). Note the bounds of this image are equal to that of the canvas 27 | * image. 28 | * @param x The x coordinate of the pixel in the image to boundary check 29 | * @param y The y coordinate of the pixel in the image to boundary check 30 | * @return True if the given point in the image should be filled, false otherwise (i.e., the pixel represents a 31 | * boundary) 32 | */ 33 | @SuppressWarnings("BooleanMethodIsAlwaysInverted") 34 | default boolean isBoundary(BufferedImage canvas, BufferedImage scratch, int x, int y) { 35 | Color canvasPixel = new Color(canvas.getRGB(x, y), true); 36 | Color scratchPixel = new Color(scratch.getRGB(x, y), true); 37 | return canvasPixel.getAlpha() != 0 || scratchPixel.getAlpha() != 0; 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/main/java/com/defano/jmonet/tools/attributes/FillFunction.java: -------------------------------------------------------------------------------- 1 | package com.defano.jmonet.tools.attributes; 2 | 3 | import java.awt.*; 4 | import java.awt.image.BufferedImage; 5 | 6 | /** 7 | * A function for filling a single pixel on the canvas. 8 | */ 9 | public interface FillFunction { 10 | 11 | /** 12 | * Fills a single pixel in an image with a given paint or texture. 13 | * 14 | * @param image The image whose pixel should be filled. 15 | * @param x The x coordinate of the point / pixel to fill 16 | * @param y The y coordinate of the point / pixel to fill 17 | * @param fillPaint The paint to apply to the given pixel. 18 | */ 19 | default void fill(BufferedImage image, int x, int y, Paint fillPaint) { 20 | if (fillPaint instanceof Color) { 21 | image.setRGB (x, y, ((Color) fillPaint).getRGB()); 22 | } else if (fillPaint instanceof TexturePaint) { 23 | BufferedImage texture = ((TexturePaint) fillPaint).getImage(); 24 | image.setRGB (x, y, texture.getRGB(x % texture.getWidth(), y % texture.getHeight())); 25 | } else { 26 | throw new IllegalArgumentException("Don't know how to fill using this kind of paint: " + fillPaint); 27 | } 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/main/java/com/defano/jmonet/tools/attributes/MarkPredicate.java: -------------------------------------------------------------------------------- 1 | package com.defano.jmonet.tools.attributes; 2 | 3 | import java.awt.*; 4 | 5 | /** 6 | * An interface for determining if a given pixel constitutes a "mark" on a canvas. Used, for example, when determining 7 | * if the pencil tool should draw or erase (when starting on a marked pixel the pencil erases, when starting on an 8 | * unmarked or blank pixel, it draws). 9 | */ 10 | public interface MarkPredicate { 11 | 12 | /** 13 | * Determines if a pixel of a given color is considered to be a mark on the canvas in contrast to an "erase color" 14 | * that may optionally define the color that erased pixels are changed to. 15 | * 16 | * @param pixel A non-null value, indicating the present color value of the pixel being queried. 17 | * @param eraseColor An nullable value, indicating the present erase color in the context of this query. 18 | * @return True if this pixel should be treated as a mark; false otherwise. 19 | */ 20 | default boolean isMarked(Color pixel, Color eraseColor) { 21 | if (eraseColor == null) { 22 | return pixel.getAlpha() >= 128; 23 | } else { 24 | return eraseColor.getRed() != pixel.getRed() || eraseColor.getBlue() != pixel.getBlue() || eraseColor.getGreen() != pixel.getGreen(); 25 | } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/main/java/com/defano/jmonet/tools/attributes/ObservableToolAttributes.java: -------------------------------------------------------------------------------- 1 | package com.defano.jmonet.tools.attributes; 2 | 3 | import com.defano.jmonet.model.Interpolation; 4 | import io.reactivex.Observable; 5 | 6 | import java.awt.*; 7 | import java.util.Optional; 8 | 9 | public interface ObservableToolAttributes { 10 | 11 | void setEraseColorObservable(Observable> observable); 12 | Observable> getEraseColorObservable(); 13 | 14 | void setFillPaintObservable(Observable> observable); 15 | Observable> getFillPaintObservable(); 16 | 17 | void setStrokeObservable(Observable observable); 18 | Observable getStrokeObservable(); 19 | 20 | void setStrokePaintObservable(Observable observable); 21 | Observable getStrokePaintObservable(); 22 | 23 | void setShapeSidesObservable(Observable observable); 24 | Observable getShapeSidesObservable(); 25 | 26 | void setFontObservable(Observable observable); 27 | Observable getFontObservable(); 28 | 29 | void setFontColorObservable(Observable observable); 30 | Observable getFontColorObservable(); 31 | 32 | void setIntensityObservable(Observable observable); 33 | Observable getIntensityObservable(); 34 | 35 | void setDrawCenteredObservable(Observable observable); 36 | Observable getDrawCenteredObservable(); 37 | 38 | void setDrawMultipleObservable(Observable observable); 39 | Observable getDrawMultipleObservable(); 40 | 41 | void setCornerRadiusObservable(Observable observable); 42 | Observable getCornerRadiusObservable(); 43 | 44 | void setConstrainedAngleObservable(Observable observable); 45 | Observable getConstrainedAngleObservable(); 46 | 47 | void setAntiAliasingObservable(Observable observable); 48 | Observable getAntiAliasingObservable(); 49 | 50 | void setMinimumScaleObservable(Observable observable); 51 | Observable getMinimumScaleObservable(); 52 | 53 | void setMaximumScaleObservable(Observable observable); 54 | Observable getMaximumScaleObservable(); 55 | 56 | void setMagnificationStepObservable(Observable observable); 57 | Observable getMagnificationStepObservable(); 58 | 59 | void setRecenterOnMagnifyObservable(Observable observable); 60 | Observable getRecenterOnMagnifyObservable(); 61 | 62 | void setPathInterpolationObservable(Observable observable); 63 | Observable getPathInterpolationObservable(); 64 | } 65 | -------------------------------------------------------------------------------- /src/main/java/com/defano/jmonet/tools/base/BoundsTool.java: -------------------------------------------------------------------------------- 1 | package com.defano.jmonet.tools.base; 2 | 3 | import com.defano.jmonet.canvas.observable.SurfaceInteractionObserver; 4 | import com.defano.jmonet.model.PaintToolType; 5 | import com.defano.jmonet.tools.util.MathUtils; 6 | 7 | import java.awt.*; 8 | import java.awt.event.MouseEvent; 9 | 10 | /** 11 | * A tool that draws shapes by defined by a bounding box, like rectangles, round-rectangles, and ovals. 12 | * 13 | * Click to define the first point in the bounding box, then drag to define the second. There are no restrictions 14 | * on the relative location of the two points. 15 | */ 16 | public class BoundsTool extends BasicTool implements SurfaceInteractionObserver { 17 | 18 | private Point initialPoint; 19 | private Point currentPoint; 20 | 21 | public BoundsTool(PaintToolType type) { 22 | super(type); 23 | } 24 | 25 | /** {@inheritDoc} */ 26 | @Override 27 | public Cursor getDefaultCursor() { 28 | return new Cursor(Cursor.CROSSHAIR_CURSOR); 29 | } 30 | 31 | /** {@inheritDoc} */ 32 | @Override 33 | public void mousePressed(MouseEvent e, Point imageLocation) { 34 | initialPoint = imageLocation; 35 | } 36 | 37 | /** {@inheritDoc} */ 38 | @Override 39 | public void mouseDragged(MouseEvent e, Point canvasLoc) { 40 | currentPoint = canvasLoc; 41 | 42 | if (!getAttributes().isDrawMultiple()) { 43 | getScratch().clear(); 44 | } 45 | 46 | Point originPoint = new Point(initialPoint); 47 | 48 | if (getAttributes().isDrawCentered()) { 49 | int height = currentPoint.y - initialPoint.y; 50 | int width = currentPoint.x - initialPoint.x; 51 | 52 | originPoint.x = initialPoint.x - width / 2; 53 | originPoint.y = initialPoint.y - height / 2; 54 | } 55 | 56 | Rectangle bounds = e.isShiftDown() ? 57 | MathUtils.square(originPoint, currentPoint) : 58 | MathUtils.rectangle(originPoint, currentPoint); 59 | 60 | getAttributes().getFillPaint().ifPresent(paint -> 61 | getDelegate().fillBounds(getScratch(), paint, new Rectangle(bounds.x, bounds.y, bounds.width, bounds.height), e.isShiftDown())); 62 | 63 | getDelegate().strokeBounds(getScratch(), getAttributes().getStroke(), getAttributes().getStrokePaint(), new Rectangle(bounds.x, bounds.y, bounds.width, bounds.height), e.isShiftDown()); 64 | getCanvas().repaint(); 65 | } 66 | 67 | /** {@inheritDoc} */ 68 | @Override 69 | public void mouseReleased(MouseEvent e, Point canvasLoc) { 70 | getCanvas().commit(); 71 | } 72 | 73 | /** {@inheritDoc} */ 74 | @Override 75 | public void mouseMoved(MouseEvent e, Point canvasLoc) { 76 | setToolCursor(getToolCursor()); 77 | } 78 | 79 | /** 80 | * Gets the initial point defined by this tool, or null if no point has yet been defined. 81 | * @return The initial point 82 | */ 83 | public Point getInitialPoint() { 84 | return initialPoint; 85 | } 86 | 87 | /** 88 | * Gets the last point defined by this tool, or null if no point has yet been defined. 89 | * @return The current point. 90 | */ 91 | public Point getCurrentPoint() { 92 | return currentPoint; 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /src/main/java/com/defano/jmonet/tools/base/BoundsToolDelegate.java: -------------------------------------------------------------------------------- 1 | package com.defano.jmonet.tools.base; 2 | 3 | import com.defano.jmonet.canvas.Scratch; 4 | 5 | import java.awt.*; 6 | 7 | /** 8 | * A delegate class responsible for rendering the shape drawn by a {@link BoundsTool}. 9 | */ 10 | public interface BoundsToolDelegate { 11 | 12 | /** 13 | * Draws the stroke (outline) of a shape described by a rectangular boundary. 14 | * 15 | * @param scratch The scratch buffer on which to draw 16 | * @param stroke The stroke with which to draw 17 | * @param paint The paint with which to draw 18 | * @param bounds The bounds of the shape to draw 19 | * @param isShiftDown True to indicate that the user is holding the shift key; implementers may use this flag to 20 | * constrain the bounds or otherwise modify the tool behavior. 21 | */ 22 | void strokeBounds(Scratch scratch, Stroke stroke, Paint paint, Rectangle bounds, boolean isShiftDown); 23 | 24 | /** 25 | * Fills a shape described by a rectangular boundary. 26 | * 27 | * @param scratch The scratch buffer on which to draw 28 | * @param fill The paint with which to fill the shape 29 | * @param bounds The bounds of the shape to draw 30 | * @param isShiftDown True to indicate that the user is holding the shift key; implementers may use this flag to 31 | * constrain the bounds or otherwise modify the tool behavior. 32 | */ 33 | void fillBounds(Scratch scratch, Paint fill, Rectangle bounds, boolean isShiftDown); 34 | 35 | } 36 | -------------------------------------------------------------------------------- /src/main/java/com/defano/jmonet/tools/base/LinearTool.java: -------------------------------------------------------------------------------- 1 | package com.defano.jmonet.tools.base; 2 | 3 | import com.defano.jmonet.canvas.observable.SurfaceInteractionObserver; 4 | import com.defano.jmonet.model.PaintToolType; 5 | import com.defano.jmonet.tools.util.MathUtils; 6 | 7 | import java.awt.*; 8 | import java.awt.event.MouseEvent; 9 | 10 | /** 11 | * A tool for drawing shapes defined by two points on the canvas, like lines. 12 | * 13 | * Click the mouse to define the first point, then drag to define the second point. 14 | */ 15 | public class LinearTool extends BasicTool implements SurfaceInteractionObserver { 16 | 17 | private Point initialPoint; 18 | 19 | public LinearTool(PaintToolType type) { 20 | super(type); 21 | } 22 | 23 | /** {@inheritDoc} */ 24 | @Override 25 | public Cursor getDefaultCursor() { 26 | return new Cursor(Cursor.CROSSHAIR_CURSOR); 27 | } 28 | 29 | /** {@inheritDoc} */ 30 | @Override 31 | public void mouseMoved(MouseEvent e, Point canvasLoc) { 32 | setToolCursor(getToolCursor()); 33 | } 34 | 35 | /** {@inheritDoc} */ 36 | @Override 37 | public void mousePressed(MouseEvent e, Point imageLocation) { 38 | initialPoint = imageLocation; 39 | } 40 | 41 | /** {@inheritDoc} */ 42 | @Override 43 | public void mouseDragged(MouseEvent e, Point canvasLoc) { 44 | getScratch().clear(); 45 | 46 | Point currentLoc = canvasLoc; 47 | 48 | if (e.isShiftDown()) { 49 | currentLoc = MathUtils.line(initialPoint, currentLoc, getAttributes().getConstrainedAngle()); 50 | } 51 | 52 | getDelegate().drawLine(getScratch(), getAttributes().getStroke(), getAttributes().getStrokePaint(), initialPoint.x, initialPoint.y, currentLoc.x, currentLoc.y); 53 | getCanvas().repaint(); 54 | } 55 | 56 | /** {@inheritDoc} */ 57 | @Override 58 | public void mouseReleased(MouseEvent e, Point canvasLoc) { 59 | getCanvas().commit(); 60 | } 61 | 62 | public Point getInitialPoint() { 63 | return this.initialPoint; 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /src/main/java/com/defano/jmonet/tools/base/LinearToolDelegate.java: -------------------------------------------------------------------------------- 1 | package com.defano.jmonet.tools.base; 2 | 3 | import com.defano.jmonet.canvas.Scratch; 4 | 5 | import java.awt.*; 6 | 7 | /** 8 | * A delegate class responsible for rendering shapes drawn by the {@link LinearTool}. 9 | */ 10 | public interface LinearToolDelegate { 11 | 12 | /** 13 | * Draws a line from (x1, y1) to (x2, y2) on the given graphics context. 14 | * 15 | * @param scratch The scratch buffer on which to draw 16 | * @param stroke The stroke with which to draw 17 | * @param paint The paint with which to draw 18 | * @param x1 First x coordinate of the line 19 | * @param y1 First y coordinate of the line 20 | * @param x2 Second x coordinate of the line 21 | * @param y2 Second y coordinate of the line 22 | */ 23 | void drawLine(Scratch scratch, Stroke stroke, Paint paint, int x1, int y1, int x2, int y2); 24 | 25 | } 26 | -------------------------------------------------------------------------------- /src/main/java/com/defano/jmonet/tools/base/PathTool.java: -------------------------------------------------------------------------------- 1 | package com.defano.jmonet.tools.base; 2 | 3 | import com.defano.jmonet.canvas.observable.SurfaceInteractionObserver; 4 | import com.defano.jmonet.model.PaintToolType; 5 | 6 | import java.awt.*; 7 | import java.awt.event.MouseEvent; 8 | 9 | /** 10 | * A tool for drawing shapes defined by an arbitrary path (series of points) drawn on the canvas, like a paintbrush, 11 | * eraser, or pencil. 12 | * 13 | * Click and drag the mouse around the canvas to define the path of the shape to be rendered. 14 | */ 15 | public class PathTool extends BasicTool implements SurfaceInteractionObserver { 16 | 17 | private Point lastPoint; 18 | 19 | public PathTool(PaintToolType type) { 20 | super(type); 21 | } 22 | 23 | /** {@inheritDoc} */ 24 | @Override 25 | public Cursor getDefaultCursor() { 26 | return new Cursor(Cursor.CROSSHAIR_CURSOR); 27 | } 28 | 29 | /** {@inheritDoc} */ 30 | @Override 31 | public void mouseMoved(MouseEvent e, Point canvasLoc) { 32 | setToolCursor(getToolCursor()); 33 | } 34 | 35 | /** {@inheritDoc} */ 36 | @Override 37 | public void mousePressed(MouseEvent e, Point imageLocation) { 38 | getScratch().clear(); 39 | 40 | getDelegate().startPath(getScratch(), getAttributes().getStroke(), getAttributes().getStrokePaint(), imageLocation); 41 | lastPoint = imageLocation; 42 | 43 | getCanvas().repaint(getScratch().getDirtyRegion()); 44 | } 45 | 46 | /** {@inheritDoc} */ 47 | @Override 48 | public void mouseDragged(MouseEvent e, Point canvasLoc) { 49 | getDelegate().addPoint(getScratch(), getAttributes().getStroke(), getAttributes().getStrokePaint(), lastPoint, canvasLoc); 50 | lastPoint = canvasLoc; 51 | 52 | // While mouse is down, only repaint the modified area of the canvas 53 | getCanvas().repaint(getScratch().getDirtyRegion()); 54 | } 55 | 56 | /** {@inheritDoc} */ 57 | @Override 58 | public void mouseReleased(MouseEvent e, Point canvasLoc) { 59 | getDelegate().completePath(getScratch(), getAttributes().getStroke(), getAttributes().getStrokePaint(), getAttributes().getFillPaint().orElse(null)); 60 | getCanvas().commit(getScratch().getLayerSet()); 61 | } 62 | 63 | } 64 | -------------------------------------------------------------------------------- /src/main/java/com/defano/jmonet/tools/base/PathToolDelegate.java: -------------------------------------------------------------------------------- 1 | package com.defano.jmonet.tools.base; 2 | 3 | import com.defano.jmonet.canvas.Scratch; 4 | 5 | import java.awt.*; 6 | 7 | /** 8 | * A delegate class responsible for rendering shapes drawn by the {@link PathToolDelegate}. 9 | */ 10 | public interface PathToolDelegate { 11 | 12 | /** 13 | * Begins drawing a path on the given graphics context. 14 | * 15 | * @param scratch The scratch buffer on which to draw. 16 | * @param stroke The stroke with which to draw 17 | * @param strokePaint The paint with which to draw 18 | * @param initialPoint The first point defined on the path 19 | */ 20 | void startPath(Scratch scratch, Stroke stroke, Paint strokePaint, Point initialPoint); 21 | 22 | /** 23 | * Adds a point to the path begun via a call to {@link #startPath(Scratch, Stroke, Paint, Point)}. 24 | * 25 | * @param scratch The scratch buffer on which to draw. 26 | * @param stroke The stroke with which to draw 27 | * @param strokePaint The paint with which to draw 28 | * @param lastPoint The last point added to the current path 29 | * @param thisPoint The new point to add to the current path 30 | */ 31 | void addPoint(Scratch scratch, Stroke stroke, Paint strokePaint, Point lastPoint, Point thisPoint); 32 | 33 | /** 34 | * Completes the path begun via a call to {@link #startPath(Scratch, Stroke, Paint, Point)}. 35 | * 36 | * @param scratch The scratch buffer on which to draw. 37 | * @param stroke The stroke with which to draw 38 | * @param strokePaint The paint with which to render the stroke 39 | * @param fillPaint The paint with which to fill the shape, null to indicate shape should not be filled 40 | */ 41 | void completePath(Scratch scratch, Stroke stroke, Paint strokePaint, Paint fillPaint); 42 | 43 | } 44 | -------------------------------------------------------------------------------- /src/main/java/com/defano/jmonet/tools/base/PolylineToolDelegate.java: -------------------------------------------------------------------------------- 1 | package com.defano.jmonet.tools.base; 2 | 3 | import com.defano.jmonet.canvas.Scratch; 4 | 5 | import java.awt.*; 6 | 7 | /** 8 | * A delegate class responsible for rendering shapes drawn by the {@link PolylineTool}. 9 | */ 10 | public interface PolylineToolDelegate { 11 | 12 | /** 13 | * Draws one or more sides (edges) of a polygon which is not filled and may not be closed. 14 | * 15 | * @param scratch The scratch buffer on which to draw. 16 | * @param stroke The current stroke context. 17 | * @param strokePaint The current paint context. 18 | * @param xPoints An array of x points, see {@link Graphics2D#drawPolyline(int[], int[], int)} 19 | * @param yPoints An array of y points, see {@link Graphics2D#drawPolyline(int[], int[], int)} 20 | */ 21 | void strokePolyline(Scratch scratch, Stroke stroke, Paint strokePaint, int[] xPoints, int[] yPoints); 22 | 23 | /** 24 | * Draws one or more sides (edges) of a polygon, closing the shape as needed. 25 | * 26 | * @param scratch The scratch buffer on which to draw. 27 | * @param stroke The current stroke context. 28 | * @param strokePaint The current paint context. 29 | * @param xPoints An array of x points, see {@link Graphics2D#drawPolygon(int[], int[], int)} (int[], int[], int)} 30 | * @param yPoints An array of y points, see {@link Graphics2D#drawPolygon(int[], int[], int)} (int[], int[], int)} 31 | */ 32 | void strokePolygon(Scratch scratch, Stroke stroke, Paint strokePaint, int[] xPoints, int[] yPoints); 33 | 34 | /** 35 | * Draws a filled polygon. 36 | * 37 | * @param scratch The scratch buffer on which to draw. 38 | * @param fillPaint The paint with which to fill the polyfon 39 | * @param xPoints An array of x points, see {@link Graphics2D#fillPolygon(int[], int[], int)} (int[], int[], int)} 40 | * @param yPoints An array of y points, see {@link Graphics2D#fillPolygon(int[], int[], int)} (int[], int[], int)} 41 | */ 42 | void fillPolygon(Scratch scratch, Paint fillPaint, int[] xPoints, int[] yPoints); 43 | 44 | } 45 | -------------------------------------------------------------------------------- /src/main/java/com/defano/jmonet/tools/base/SelectionToolDelegate.java: -------------------------------------------------------------------------------- 1 | package com.defano.jmonet.tools.base; 2 | 3 | import java.awt.*; 4 | 5 | /** 6 | * A delegate class responsible for rendering selections drawn by the {@link SelectionTool}. 7 | */ 8 | public interface SelectionToolDelegate { 9 | 10 | /** 11 | * Gets the current selection frame, or null, if there is no active selection. 12 | * 13 | * @return The shape of the active selection frame, or null if there is no selection. 14 | */ 15 | Shape getSelectionFrame(); 16 | 17 | /** 18 | * Resets the selection frame such that subsequent calls to {@link #getSelectionFrame()} return null. 19 | */ 20 | void clearSelectionFrame(); 21 | 22 | /** 23 | * Creates or replaces the selection frame with the given shape. Note that tools that do not support arbitrary 24 | * selection shapes (like the marquee tool) will use the bounds of the given shape instead. 25 | * 26 | * @param bounds The shape or bounds of the new selection frame. 27 | */ 28 | void setSelectionFrame(Shape bounds); 29 | 30 | /** 31 | * Translates the location of the current selection frame, if one exists. 32 | * 33 | * @param xDelta The number of pixels to translate in the x-direction 34 | * @param yDelta The number of pixels to translate in the y-direction 35 | */ 36 | void translateSelectionFrame(int xDelta, int yDelta); 37 | 38 | /** 39 | * Invoked to indicate that the user has defined a new point on the selection path. 40 | * 41 | * @param initialPoint The first point defined by the user (i.e., where the mouse was initially pressed) 42 | * @param newPoint A new point to append to the selection path (i.e., where the mouse is now) 43 | * @param isShiftKeyDown When true, indicates user is holding the shift key down 44 | */ 45 | void addPointToSelectionFrame(Point initialPoint, Point newPoint, boolean isShiftKeyDown); 46 | 47 | /** 48 | * Invoked to indicate that the given point should be considered the last point in the selection path, and the 49 | * path shape should be closed. 50 | * 51 | * @param finalPoint The final point on the selection path. 52 | */ 53 | void closeSelectionFrame(Point finalPoint); 54 | 55 | } 56 | -------------------------------------------------------------------------------- /src/main/java/com/defano/jmonet/tools/base/Tool.java: -------------------------------------------------------------------------------- 1 | package com.defano.jmonet.tools.base; 2 | 3 | import com.defano.jmonet.canvas.PaintCanvas; 4 | import com.defano.jmonet.canvas.Scratch; 5 | import com.defano.jmonet.model.PaintToolType; 6 | import com.defano.jmonet.tools.attributes.ToolAttributes; 7 | 8 | import java.awt.*; 9 | 10 | public interface Tool { 11 | /** 12 | * Activates this tool on a given canvas. 13 | *

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

19 | * Use {@link #deactivate()} to stop this tool from painting on the canvas. 20 | * 21 | * @param canvas The paint canvas on which to activate the tool. 22 | */ 23 | void activate(PaintCanvas canvas); 24 | 25 | /** 26 | * Deactivates the tool on the canvas. Invoking this method on a tool this is not presently activated has no effect. 27 | * A deactivated tool no longer affects the canvas and all listeners / observers are un-subscribed making the tool 28 | * available for garbage collection. 29 | */ 30 | void deactivate(); 31 | 32 | /** 33 | * Determines if this tool is presently active on a canvas. A tool is considered active after a call to 34 | * {@link #activate(PaintCanvas)} has been made, but before a call to {@link #deactivate()}. 35 | * 36 | * @return True if the tool is active, false otherwise. 37 | */ 38 | boolean isActive(); 39 | 40 | /** 41 | * Gets the default mouse cursor used when painting with this tool. Note that specific tools may provide methods 42 | * for getting and setting auxiliary cursors that are active during tool-specific states, too. 43 | * 44 | * @return The default tool cursor. 45 | */ 46 | Cursor getToolCursor(); 47 | 48 | /** 49 | * Sets the default mouse cursor used when painting with this tool. 50 | * 51 | * @param toolCursor The default mouse cursor. 52 | */ 53 | void setToolCursor(Cursor toolCursor); 54 | 55 | /** 56 | * Gets the canvas on which this tool is currently painting, or null, if the tool has not been activated (via a 57 | * call to {@link #activate(PaintCanvas)}). 58 | * 59 | * @return The canvas this tool is painting on, or null 60 | */ 61 | PaintCanvas getCanvas(); 62 | 63 | /** 64 | * Gets the scratch buffer of the canvas that this tool is presently activated on, applying this tool's anti- 65 | * aliasing mode to the scratch buffer's graphics context. 66 | * 67 | * @return The active scratch buffer. 68 | */ 69 | Scratch getScratch(); 70 | 71 | /** 72 | * Gets the type of this tool. 73 | * 74 | * @return The tool type. 75 | */ 76 | PaintToolType getPaintToolType(); 77 | 78 | /** 79 | * Gets the set of tool attributes bound to this tool (like paint color, fill mode, line size, etc.) 80 | * 81 | * @return The set of observable tool attributes. 82 | */ 83 | ToolAttributes getAttributes(); 84 | 85 | } 86 | -------------------------------------------------------------------------------- /src/main/java/com/defano/jmonet/tools/base/TransformToolDelegate.java: -------------------------------------------------------------------------------- 1 | package com.defano.jmonet.tools.base; 2 | 3 | import com.defano.jmonet.model.FlexQuadrilateral; 4 | 5 | import java.awt.*; 6 | 7 | /** 8 | * A delegate responsible for handling changes made to the transformed selection frame. 9 | */ 10 | public interface TransformToolDelegate { 11 | 12 | /** 13 | * Invoked to indicate that the user has dragged/moved the top-left handle of the transform quadrilateral to a 14 | * new position. Transforms the selection frame and bounded pixels accordingly. 15 | * 16 | * @param quadrilateral The quadrilateral representing the transform bounds. 17 | * @param newPosition The new location of the affected drag handle. 18 | * @param isShiftDown True to indicate user is holding shift down; implementers may optionally use this flag 19 | * to constrain drag movement or apply some other feature of the transform. 20 | */ 21 | void moveTopLeft(FlexQuadrilateral quadrilateral, Point newPosition, boolean isShiftDown); 22 | 23 | /** 24 | * Invoked to indicate that the user has dragged/moved the top-right handle of the transform quadrilateral to a 25 | * new position. Transforms the selection frame and bounded pixels accordingly. 26 | * 27 | * @param quadrilateral The quadrilateral representing the transform bounds. 28 | * @param newPosition The new location of the affected drag handle. 29 | * @param isShiftDown True to indicate user is holding shift down; implementers may optionally use this flag 30 | * to constrain drag movement or apply some other feature of the transform. 31 | */ 32 | void moveTopRight(FlexQuadrilateral quadrilateral, Point newPosition, boolean isShiftDown); 33 | 34 | /** 35 | * Invoked to indicate that the user has dragged/moved the bottom-left handle of the transform quadrilateral to a 36 | * new position. Transforms the selection frame and bounded pixels accordingly. 37 | * 38 | * @param quadrilateral The quadrilateral representing the transform bounds. 39 | * @param newPosition The new location of the affected drag handle. 40 | * @param isShiftDown True to indicate user is holding shift down; implementers may optionally use this flag 41 | * to constrain drag movement or apply some other feature of the transform. 42 | */ 43 | void moveBottomLeft(FlexQuadrilateral quadrilateral, Point newPosition, boolean isShiftDown); 44 | 45 | /** 46 | * Invoked to indicate that the user has dragged/moved the bottom-right handle of the transform quadrilateral to a 47 | * new position. Transforms the selection frame and bounded pixels accordingly. 48 | * 49 | * @param quadrilateral The quadrilateral representing the transform bounds. 50 | * @param newPosition The new location of the affected drag handle. 51 | * @param isShiftDown True to indicate user is holding shift down; implementers may optionally use this flag 52 | * to constrain drag movement or apply some other feature of the transform. 53 | */ 54 | void moveBottomRight(FlexQuadrilateral quadrilateral, Point newPosition, boolean isShiftDown); 55 | } 56 | -------------------------------------------------------------------------------- /src/main/java/com/defano/jmonet/tools/brushes/ShapeStroke.java: -------------------------------------------------------------------------------- 1 | package com.defano.jmonet.tools.brushes; 2 | 3 | import java.awt.*; 4 | import java.awt.geom.AffineTransform; 5 | import java.awt.geom.GeneralPath; 6 | import java.util.ArrayList; 7 | import java.util.Collection; 8 | import java.util.List; 9 | 10 | /** 11 | * A brush whose tip is a user-provided shape. Produces a stroke where every point on the stroked path is "stamped" with a 12 | * filled shape. 13 | */ 14 | public class ShapeStroke extends StampStroke { 15 | 16 | private final List shapes = new ArrayList<>(); 17 | 18 | /** 19 | * Produces a brush of given shape. 20 | * 21 | * @param shape The shape of the brush 22 | */ 23 | @SuppressWarnings("unused") 24 | public ShapeStroke(Shape shape) { 25 | this.shapes.add(shape); 26 | } 27 | 28 | public ShapeStroke(Collection shapes) { 29 | this.shapes.addAll(shapes); 30 | } 31 | 32 | /** 33 | * {@inheritDoc} 34 | */ 35 | @Override 36 | public void stampPoint(GeneralPath path, Point point) { 37 | for (Shape shape : shapes) { 38 | Shape stamp = AffineTransform 39 | .getTranslateInstance(point.x - (shape.getBounds().width / 2.0) - shape.getBounds().x, point.y - (shape.getBounds().height / 2.0) - shape.getBounds().y) 40 | .createTransformedShape(shape); 41 | 42 | path.append(stamp, false); 43 | } 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/main/java/com/defano/jmonet/tools/builder/StrokeBuilder.java: -------------------------------------------------------------------------------- 1 | package com.defano.jmonet.tools.builder; 2 | 3 | import com.defano.jmonet.tools.brushes.ShapeStroke; 4 | 5 | import java.awt.*; 6 | 7 | /** 8 | * A utility for building strokes, both {@link BasicStroke} and {@link ShapeStroke}. 9 | */ 10 | public class StrokeBuilder { 11 | 12 | private StrokeBuilder() {} 13 | 14 | /** 15 | * Builds a stroke in which a shape is "stamped" along each point of the stroked path. Note that the stamped shape 16 | * (the pen) is not automatically rotated to match the angular perpendicular to the path (as occurs with a 17 | * {@link #withBasicStroke()}. 18 | * 19 | * @return The builder. 20 | */ 21 | public static ShapeStrokeBuilder withShape() { 22 | return new ShapeStrokeBuilder(); 23 | } 24 | 25 | /** 26 | * Builds a basic stroke. 27 | * 28 | * @return The builder 29 | */ 30 | public static BasicStrokeBuilder withBasicStroke() { 31 | return new BasicStrokeBuilder(); 32 | } 33 | 34 | } 35 | -------------------------------------------------------------------------------- /src/main/java/com/defano/jmonet/tools/cursors/CursorManager.java: -------------------------------------------------------------------------------- 1 | package com.defano.jmonet.tools.cursors; 2 | 3 | import com.defano.jmonet.canvas.PaintCanvas; 4 | 5 | import java.awt.*; 6 | 7 | /** 8 | * A class which has the ability to get and set the system's mouse cursor when the mouse is hovering over a specified 9 | * {@link PaintCanvas}. 10 | */ 11 | public interface CursorManager { 12 | 13 | /** 14 | * Gets the default mouse cursor used when painting with this tool. Note that specific tools may provide methods 15 | * for getting and setting auxiliary cursors that are active during tool-specific states, too. 16 | * 17 | * @return The default tool cursor. 18 | */ 19 | Cursor getToolCursor(); 20 | 21 | /** 22 | * Sets the default mouse cursor used when painting with this tool. 23 | * 24 | * @param toolCursor The default mouse cursor. 25 | * @param canvas The canvas on which the cursor should be active. 26 | */ 27 | void setToolCursor(Cursor toolCursor, PaintCanvas canvas); 28 | 29 | } 30 | -------------------------------------------------------------------------------- /src/main/java/com/defano/jmonet/tools/cursors/SwingCursorManager.java: -------------------------------------------------------------------------------- 1 | package com.defano.jmonet.tools.cursors; 2 | 3 | import com.defano.jmonet.canvas.PaintCanvas; 4 | 5 | import javax.swing.*; 6 | import java.awt.*; 7 | 8 | /** 9 | * A CursorManager that sets a canvas cursor on the Swing dispatch thread. 10 | */ 11 | public class SwingCursorManager implements CursorManager { 12 | 13 | private Cursor toolCursor; 14 | 15 | /** {@inheritDoc} */ 16 | @Override 17 | public Cursor getToolCursor() { 18 | return toolCursor; 19 | } 20 | 21 | /** {@inheritDoc} */ 22 | @Override 23 | public void setToolCursor(Cursor toolCursor, PaintCanvas canvas) { 24 | this.toolCursor = toolCursor; 25 | if (canvas != null) { 26 | SwingUtilities.invokeLater(() -> canvas.setCursor(toolCursor)); 27 | } 28 | } 29 | 30 | } 31 | -------------------------------------------------------------------------------- /src/main/java/com/defano/jmonet/tools/selection/MutableSelection.java: -------------------------------------------------------------------------------- 1 | package com.defano.jmonet.tools.selection; 2 | 3 | import java.awt.*; 4 | import java.awt.image.BufferedImage; 5 | 6 | /** 7 | * A class which manages the state or context of a modifiable image selection. 8 | */ 9 | public interface MutableSelection extends Selection { 10 | 11 | /** 12 | * Transitions to an un-selected state. 13 | * 14 | * Reset the selection boundary to its initial, no-selection state. {@link #getSelectionFrame()} should return 15 | * null following a selection reset, but prior to defining a new selection. 16 | */ 17 | void resetSelection(); 18 | 19 | /** 20 | * Specifies the selection rectangle (bounds) of the current selection, i.e., the rectangular path on which 21 | * "marching ants" will be drawn. 22 | * 23 | * @param bounds The new selection bounds. 24 | */ 25 | void setSelectionOutline(Shape bounds); 26 | 27 | /** 28 | * Marks the selection as having been mutated (either by transformation or movement). 29 | */ 30 | void setDirty(); 31 | 32 | /** 33 | * Replaces the current selected image with the given image. It is the caller's responsibility to mask the given 34 | * image to assure it does not exceed the selection bounds. 35 | * 36 | * @param image The image with which to replace the selection. 37 | */ 38 | void setSelectedImage(BufferedImage image); 39 | 40 | /** 41 | * Invoked to indicate that the selection should be moved on the canvas and therefore the selection shape's 42 | * coordinates should be translated accordingly. 43 | * 44 | * @param xDelta Number of pixels to move selection bounds horizontally. 45 | * @param yDelta Number of pixels to move selection bounds vertically. 46 | */ 47 | void translateSelection(int xDelta, int yDelta); 48 | 49 | } 50 | -------------------------------------------------------------------------------- /src/main/java/com/defano/jmonet/tools/selection/TransformableCanvasSelection.java: -------------------------------------------------------------------------------- 1 | package com.defano.jmonet.tools.selection; 2 | 3 | import java.awt.*; 4 | import java.awt.image.BufferedImage; 5 | 6 | /** 7 | * A selection that can be transformed in a way that modifies the underlying committed canvas image. 8 | */ 9 | public interface TransformableCanvasSelection extends MutableSelection { 10 | 11 | /** 12 | * Makes image bounded by the selection frame appear to have been deleted by filling the selection frame with paint 13 | * in the remove-scratch buffer. 14 | *

15 | * This method does not mark the selection as dirty, nor does it commit this change to the canvas. Callers are 16 | * expected to invoke {@link #setDirty()} or commit the scratch buffer to the canvas if this change is intended to 17 | * be permanent. 18 | */ 19 | void eraseSelectedPixelsFromCanvas(); 20 | 21 | /** 22 | * Add the graphics underneath the selection to the currently selected image. Has no effect if a selection is not 23 | * active or has not been dirtied. 24 | *

25 | * This method "picks up" the paint underneath the selection (that wasn't part of the paint initially 26 | * picked up when the selection bounds were defined). 27 | */ 28 | default void pickupSelection() { 29 | 30 | if (hasSelection()) { 31 | setDirty(); 32 | 33 | // Draw current selection without marching ants 34 | redrawSelection(false); 35 | 36 | // Grab pixels from scratch and canvas that are bounded by the selection 37 | BufferedImage maskedSelection = getSelectionCroppedCopy(getCanvas().render()); 38 | 39 | // Resize to smallest bounds for performance 40 | Shape selectionBounds = getSelectionFrame(); 41 | BufferedImage trimmedSelection = maskedSelection.getSubimage( 42 | Math.max(0, selectionBounds.getBounds().x), 43 | Math.max(0, selectionBounds.getBounds().y), 44 | Math.min(selectionBounds.getBounds().width, maskedSelection.getWidth() - selectionBounds.getBounds().x), 45 | Math.min(selectionBounds.getBounds().height, maskedSelection.getHeight() - selectionBounds.getBounds().y) 46 | ); 47 | 48 | // Update the current selection 49 | setSelectedImage(trimmedSelection); 50 | eraseSelectedPixelsFromCanvas(); 51 | 52 | // And redraw once more, this time with ants 53 | redrawSelection(true); 54 | } 55 | } 56 | 57 | } 58 | -------------------------------------------------------------------------------- /src/main/java/com/defano/jmonet/tools/selection/TransformableImageSelection.java: -------------------------------------------------------------------------------- 1 | package com.defano.jmonet.tools.selection; 2 | 3 | import com.defano.jmonet.tools.attributes.FillFunction; 4 | import com.defano.jmonet.transform.image.PixelTransform; 5 | import com.defano.jmonet.transform.image.StaticImageTransform; 6 | import com.defano.jmonet.transform.image.Transformable; 7 | import com.defano.jmonet.transform.image.ApplyPixelTransform; 8 | import com.defano.jmonet.transform.image.FillTransform; 9 | 10 | import java.awt.*; 11 | 12 | /** 13 | * A selection in which the pixels of the selected image can be transformed (i.e., change of brightness, opacity, etc.). 14 | *

15 | * Differs from {@link TransformableSelection} in that these transforms do no change the selection shape (outline) or 16 | * location on the canvas; only the underlying selected image. 17 | */ 18 | public interface TransformableImageSelection extends MutableSelection, Transformable { 19 | 20 | /** 21 | * Performs a transformation on the selected image that does not effect the dimensions, bounds or location of the 22 | * selection. 23 | * 24 | * @param transform The transform to perform. 25 | */ 26 | default void transform(StaticImageTransform transform) { 27 | if (hasSelection()) { 28 | setSelectedImage(transform.apply(getSelectedImage())); 29 | setDirty(); 30 | } 31 | } 32 | 33 | /** 34 | * Performs a per-pixel transformation on all pixels bound by the selection. 35 | * 36 | * @param transform The transform operation to apply 37 | */ 38 | default void transform(PixelTransform transform) { 39 | transform(new ApplyPixelTransform(transform, getIdentitySelectionFrame())); 40 | } 41 | 42 | /** 43 | * Fills all transparent pixels in the selection with the given fill paint. 44 | * 45 | * @param fillPaint The paint to fill with. 46 | * @param fillFunction A method to fill pixels in the selected image 47 | */ 48 | @Override 49 | default void fill(Paint fillPaint, FillFunction fillFunction) { 50 | transform(new FillTransform(getIdentitySelectionFrame(), fillPaint, fillFunction)); 51 | } 52 | 53 | } 54 | -------------------------------------------------------------------------------- /src/main/java/com/defano/jmonet/tools/selection/TransformableSelection.java: -------------------------------------------------------------------------------- 1 | package com.defano.jmonet.tools.selection; 2 | 3 | import com.defano.jmonet.transform.affine.FlipHorizontalTransform; 4 | import com.defano.jmonet.transform.affine.FlipVerticalTransform; 5 | import com.defano.jmonet.transform.affine.RotateLeftTransform; 6 | import com.defano.jmonet.transform.affine.RotateRightTransform; 7 | import com.defano.jmonet.transform.image.ApplyAffineTransform; 8 | 9 | import java.awt.*; 10 | import java.awt.geom.AffineTransform; 11 | 12 | /** 13 | * A selection that can be transformed using operations that affect both the pixels of the selected image (i.e., change 14 | * of brightness or opacity) and/or the selection's shape and location (i.e., flip, rotate or translate). 15 | */ 16 | public interface TransformableSelection extends TransformableImageSelection { 17 | 18 | /** 19 | * Rotates the image 90 degrees counter-clockwise. 20 | */ 21 | default void rotateLeft() { 22 | if (hasSelection()) { 23 | int width = getSelectedImage().getWidth(); 24 | int height = getSelectedImage().getHeight(); 25 | 26 | applyTransform(new RotateLeftTransform(width, height)); 27 | translateSelection((width - height) / 2, -(width - height) / 2); 28 | redrawSelection(true); 29 | } 30 | } 31 | 32 | /** 33 | * Rotates the image 90 degrees clockwise. 34 | */ 35 | default void rotateRight() { 36 | if (hasSelection()) { 37 | int width = getSelectedImage().getWidth(); 38 | int height = getSelectedImage().getHeight(); 39 | 40 | applyTransform(new RotateRightTransform(width, height)); 41 | translateSelection((width - height) / 2, -(width - height) / 2); 42 | redrawSelection(true); 43 | } 44 | } 45 | 46 | /** 47 | * Flips or mirrors the image along its vertical axis. That is, all pixels on the left side of the image 48 | * will move the right side and vice versa. 49 | */ 50 | default void flipHorizontal() { 51 | if (hasSelection()) { 52 | int width = getSelectedImage().getWidth(); 53 | applyTransform(new FlipHorizontalTransform(width)); 54 | redrawSelection(true); 55 | } 56 | } 57 | 58 | /** 59 | * Flips or mirrors the image along its horizontal axis. That is, all pixels on the top of the image will 60 | * move to the bottom and vice versa. 61 | */ 62 | default void flipVertical() { 63 | if (hasSelection()) { 64 | int height = getSelectedImage().getHeight(); 65 | applyTransform(new FlipVerticalTransform(height)); 66 | redrawSelection(true); 67 | } 68 | } 69 | 70 | /** 71 | * Applies an AffineTransform the current image. 72 | * @param transform The transform to apply. 73 | */ 74 | default void applyTransform(AffineTransform transform) { 75 | if (hasSelection()) { 76 | setDirty(); 77 | 78 | // Get the original location of the selection 79 | Point originalLocation = getSelectionLocation(); 80 | 81 | // Transform the selected image 82 | setSelectedImage(new ApplyAffineTransform(transform).apply(getSelectedImage())); 83 | 84 | // Relocate the image to its original location 85 | Rectangle newBounds = getSelectedImage().getRaster().getBounds(); 86 | newBounds.setLocation(originalLocation); 87 | setSelectionOutline(newBounds); 88 | } 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /src/main/java/com/defano/jmonet/tools/util/ImageUtils.java: -------------------------------------------------------------------------------- 1 | package com.defano.jmonet.tools.util; 2 | 3 | import java.awt.*; 4 | import java.awt.image.BufferedImage; 5 | 6 | /** 7 | * A utility providing image-related routines common to many classes. 8 | */ 9 | public class ImageUtils { 10 | 11 | /** 12 | * Library of static methods; cannot be instantiated. 13 | */ 14 | private ImageUtils() {} 15 | 16 | /** 17 | * Makes a "deep" copy of the given image, returning a copy whose type is TYPE_INT_ARGB. 18 | * 19 | * @param src The image to copy. 20 | * @return A copy of the source, in ARGB mode. 21 | */ 22 | public static BufferedImage argbCopy(BufferedImage src) { 23 | BufferedImage copy = new BufferedImage(src.getWidth(), src.getHeight(), BufferedImage.TYPE_INT_ARGB); 24 | Graphics g = copy.getGraphics(); 25 | g.drawImage(src, 0, 0, null); 26 | g.dispose(); 27 | return copy; 28 | } 29 | 30 | /** 31 | * Creates a new ARGB BufferedImage of the same dimensions as the given source. 32 | * @param src The image whose dimensions should be used to create the new image. 33 | * @return A new, empty BufferedImage with the same dimensions as src. 34 | */ 35 | public static BufferedImage newArgbOfSize(BufferedImage src) { 36 | return new BufferedImage( 37 | src.getWidth(), 38 | src.getHeight(), 39 | BufferedImage.TYPE_INT_ARGB 40 | ); 41 | } 42 | 43 | /** 44 | * Calculates the smallest bounding rectangle that completely frames all pixels in the source image that are not 45 | * fully transparent. This bounding rectangle can be used to retrieve a sub-image 46 | * ({@link BufferedImage#getSubimage(int, int, int, int)}) that fully encapsulates all visible pixels. 47 | * 48 | * @param src A source image to analyze 49 | * @return The minimum bounding rectangle. 50 | */ 51 | public static Rectangle getMinimumBounds(BufferedImage src) { 52 | 53 | int minX = Integer.MAX_VALUE; 54 | int minY = Integer.MAX_VALUE; 55 | int maxX = 0; 56 | int maxY = 0; 57 | 58 | for (int x = 0; x < src.getWidth(); x++) { 59 | for (int y = 0; y < src.getHeight(); y++) { 60 | Color c = new Color(src.getRGB(x, y), true); 61 | if (c.getAlpha() != 0) { 62 | minX = Math.min(minX, x); 63 | minY = Math.min(minY, y); 64 | maxX = Math.max(maxX, x); 65 | maxY = Math.max(maxY, y); 66 | } 67 | } 68 | } 69 | 70 | // Special case: Zero-sized image 71 | if (minX == Integer.MAX_VALUE) { 72 | minX = 0; 73 | } 74 | 75 | if (minY == Integer.MAX_VALUE) { 76 | minY = 0; 77 | } 78 | 79 | return new Rectangle(minX, minY, maxX - minX, maxY - minY); 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /src/main/java/com/defano/jmonet/tools/util/MarchingAntsObserver.java: -------------------------------------------------------------------------------- 1 | package com.defano.jmonet.tools.util; 2 | 3 | import java.awt.*; 4 | 5 | /** 6 | * An observer of movement of the ants (i.e., change in dashed line phase) 7 | */ 8 | public interface MarchingAntsObserver { 9 | /** 10 | * Called to indicate that ants have "marched" (the dotted line stroke phase has changed) and that observers should 11 | * re-paint using the stroke provided. 12 | * 13 | * @param ants The new paint stroke to re-draw. 14 | */ 15 | void onAntsMoved(Stroke ants); 16 | } 17 | -------------------------------------------------------------------------------- /src/main/java/com/defano/jmonet/transform/affine/FlipHorizontalTransform.java: -------------------------------------------------------------------------------- 1 | package com.defano.jmonet.transform.affine; 2 | 3 | import java.awt.geom.AffineTransform; 4 | 5 | /** 6 | * An {@link AffineTransform} that performs a horizontal mirroring of a shape or image about its vertical center-line. 7 | */ 8 | public class FlipHorizontalTransform extends AffineTransform { 9 | 10 | /** 11 | * Creates a horizontal flip transformation. 12 | * 13 | * @param width The width of the image/shape being flipped. 14 | */ 15 | public FlipHorizontalTransform(int width) { 16 | setToScale(-1, 1); 17 | translate(-width, 0); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/main/java/com/defano/jmonet/transform/affine/FlipVerticalTransform.java: -------------------------------------------------------------------------------- 1 | package com.defano.jmonet.transform.affine; 2 | 3 | import java.awt.geom.AffineTransform; 4 | 5 | /** 6 | * An {@link AffineTransform} that performs a vertical mirroring of a shape or image about its horizontal center-line. 7 | */ 8 | public class FlipVerticalTransform extends AffineTransform { 9 | 10 | /** 11 | * Creates a vertical flip transform. 12 | * 13 | * @param height The height of the image/shape being flipped. 14 | */ 15 | public FlipVerticalTransform(int height) { 16 | setToScale(1, -1); 17 | translate(0, -height); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/main/java/com/defano/jmonet/transform/affine/RotateLeftTransform.java: -------------------------------------------------------------------------------- 1 | package com.defano.jmonet.transform.affine; 2 | 3 | import java.awt.geom.AffineTransform; 4 | 5 | /** 6 | * An {@link AffineTransform} that performs a 90-degree counter-clockwise rotation around an object's center point. 7 | */ 8 | public class RotateLeftTransform extends AffineTransform { 9 | 10 | /** 11 | * Creates a rotate-left transform. 12 | * 13 | * @param width The width of the shape or image to be rotated 14 | * @param height The height of the shape or image to be rotated 15 | */ 16 | public RotateLeftTransform(int width, int height) { 17 | setToTranslation(height / 2.0, width / 2.0); 18 | quadrantRotate(3); 19 | translate(-width / 2.0, -height / 2.0); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/main/java/com/defano/jmonet/transform/affine/RotateRightTransform.java: -------------------------------------------------------------------------------- 1 | package com.defano.jmonet.transform.affine; 2 | 3 | import java.awt.geom.AffineTransform; 4 | 5 | /** 6 | * An {@link AffineTransform} that performs a 90-degree counter-clockwise rotation around an object's center point. 7 | */ 8 | public class RotateRightTransform extends AffineTransform { 9 | 10 | /** 11 | * Creates a rotate-right transform. 12 | * 13 | * @param width The width of the shape or image to be rotated 14 | * @param height The height of the shape or image to be rotated 15 | */ 16 | public RotateRightTransform(int width, int height) { 17 | setToTranslation(height / 2.0, width / 2.0); 18 | quadrantRotate(1); 19 | translate(-width / 2.0, -height / 2.0); 20 | } 21 | 22 | } 23 | -------------------------------------------------------------------------------- /src/main/java/com/defano/jmonet/transform/dither/AtkinsonDitherer.java: -------------------------------------------------------------------------------- 1 | package com.defano.jmonet.transform.dither; 2 | 3 | /** 4 | * An implementation of Bill Atkinson's dithering algorithm. 5 | */ 6 | @SuppressWarnings("PointlessArithmeticExpression") 7 | public class AtkinsonDitherer extends AbstractDitherer { 8 | 9 | /** 10 | * {@inheritDoc} 11 | */ 12 | @Override 13 | public void ditherPixel(int x, int y, double qer, double qeg, double qeb) { 14 | distributeError(x + 1, y + 0, qer, qeg, qeb, 1.0 / 8.0); 15 | distributeError(x + 2, y + 0, qer, qeg, qeb, 1.0 / 8.0); 16 | 17 | distributeError(x - 1, y + 1, qer, qeg, qeb, 1.0 / 8.0); 18 | distributeError(x + 0, y + 1, qer, qeg, qeb, 1.0 / 8.0); 19 | distributeError(x + 1, y + 1, qer, qeg, qeb, 1.0 / 8.0); 20 | 21 | distributeError(x + 0, y + 1, qer, qeg, qeb, 1.0 / 8.0); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/main/java/com/defano/jmonet/transform/dither/BurkesDitherer.java: -------------------------------------------------------------------------------- 1 | package com.defano.jmonet.transform.dither; 2 | 3 | /** 4 | * An implementation of the Burkes dithering algorithm. 5 | */ 6 | @SuppressWarnings("PointlessArithmeticExpression") 7 | public class BurkesDitherer extends AbstractDitherer { 8 | 9 | /** 10 | * {@inheritDoc} 11 | */ 12 | @Override 13 | public void ditherPixel(int x, int y, double qer, double qeg, double qeb) { 14 | distributeError(x + 1, y + 0, qer, qeg, qeb, 8.0 / 32.0); 15 | distributeError(x + 2, y + 0, qer, qeg, qeb, 4.0 / 32.0); 16 | 17 | distributeError(x - 2, y + 1, qer, qeg, qeb, 2.0 / 32.0); 18 | distributeError(x - 1, y + 1, qer, qeg, qeb, 4.0 / 32.0); 19 | distributeError(x + 0, y + 1, qer, qeg, qeb, 8.0 / 32.0); 20 | distributeError(x + 1, y + 1, qer, qeg, qeb, 4.0 / 32.0); 21 | distributeError(x + 2, y + 1, qer, qeg, qeb, 2.0 / 32.0); 22 | 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/main/java/com/defano/jmonet/transform/dither/Ditherer.java: -------------------------------------------------------------------------------- 1 | package com.defano.jmonet.transform.dither; 2 | 3 | import com.defano.jmonet.transform.dither.quant.QuantizationFunction; 4 | 5 | import java.awt.image.BufferedImage; 6 | 7 | /** 8 | * An object that applies a {@link QuantizationFunction} to a {@link BufferedImage} and dithers (diffuses) 9 | * the resulting quantization error. 10 | */ 11 | public interface Ditherer { 12 | 13 | /** 14 | * Applies a {@link QuantizationFunction} to each pixel in the source image and dithers (diffuses) the 15 | * quantization error. 16 | * 17 | * @param source The source image to be quantized and dithered; unmodified by this operation. 18 | * @param quantizer The quantization function to use. 19 | * @return A copy of the source image with the quantization/dithering function applied. 20 | */ 21 | BufferedImage dither(BufferedImage source, QuantizationFunction quantizer); 22 | } 23 | -------------------------------------------------------------------------------- /src/main/java/com/defano/jmonet/transform/dither/FloydSteinbergDitherer.java: -------------------------------------------------------------------------------- 1 | package com.defano.jmonet.transform.dither; 2 | 3 | /** 4 | * An implementation of the Floyd-Steinberg dithering algorithm. 5 | */ 6 | @SuppressWarnings("PointlessArithmeticExpression") 7 | public class FloydSteinbergDitherer extends AbstractDitherer { 8 | 9 | /** 10 | * {@inheritDoc} 11 | */ 12 | @Override 13 | public void ditherPixel(int x, int y, double qer, double qeg, double qeb) { 14 | distributeError(x + 1, y + 0, qer, qeg, qeb, 7.0 / 16.0); 15 | 16 | distributeError(x - 1, y + 1, qer, qeg, qeb, 3.0 / 16.0); 17 | distributeError(x + 0, y + 1, qer, qeg, qeb, 5.0 / 16.0); 18 | distributeError(x + 1, y + 1, qer, qeg, qeb, 1.0 / 16.0); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/main/java/com/defano/jmonet/transform/dither/JarvisJudiceNinkeDitherer.java: -------------------------------------------------------------------------------- 1 | package com.defano.jmonet.transform.dither; 2 | 3 | /** 4 | * An implementation of the Jarvis-Judice-Ninke dithering algorithm. 5 | */ 6 | @SuppressWarnings({"PointlessArithmeticExpression", "unused"}) 7 | public class JarvisJudiceNinkeDitherer extends AbstractDitherer { 8 | 9 | /** 10 | * {@inheritDoc} 11 | */ 12 | @Override 13 | public void ditherPixel(int x, int y, double qer, double qeg, double qeb) { 14 | distributeError(x + 1, y + 0, qer, qeg, qeb, 7.0 / 48.0); 15 | distributeError(x + 2, y + 0, qer, qeg, qeb, 5.0 / 48.0); 16 | 17 | distributeError(x - 2, y + 1, qer, qeg, qeb, 3.0 / 48.0); 18 | distributeError(x - 1, y + 1, qer, qeg, qeb, 5.0 / 48.0); 19 | distributeError(x + 0, y + 1, qer, qeg, qeb, 7.0 / 48.0); 20 | distributeError(x + 1, y + 1, qer, qeg, qeb, 5.0 / 48.0); 21 | distributeError(x + 2, y + 1, qer, qeg, qeb, 3.0 / 48.0); 22 | 23 | distributeError(x - 2, y + 2, qer, qeg, qeb, 1.0 / 48.0); 24 | distributeError(x - 1, y + 2, qer, qeg, qeb, 3.0 / 48.0); 25 | distributeError(x + 0, y + 2, qer, qeg, qeb, 5.0 / 48.0); 26 | distributeError(x + 1, y + 2, qer, qeg, qeb, 3.0 / 48.0); 27 | distributeError(x + 2, y + 2, qer, qeg, qeb, 1.0 / 48.0); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/main/java/com/defano/jmonet/transform/dither/NullDitherer.java: -------------------------------------------------------------------------------- 1 | package com.defano.jmonet.transform.dither; 2 | 3 | /** 4 | * A no-op dithering algorithm; provides no dithering whatsoever. 5 | */ 6 | public class NullDitherer extends AbstractDitherer { 7 | 8 | /** 9 | * {@inheritDoc} 10 | */ 11 | @Override 12 | public void ditherPixel(int x, int y, double qer, double qeg, double qeb) { 13 | // Nothing to do 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/main/java/com/defano/jmonet/transform/dither/SierraDitherer.java: -------------------------------------------------------------------------------- 1 | package com.defano.jmonet.transform.dither; 2 | 3 | /** 4 | * An implementation of the Sierra-3 dithering algorithm. 5 | */ 6 | @SuppressWarnings("PointlessArithmeticExpression") 7 | public class SierraDitherer extends AbstractDitherer { 8 | 9 | /** 10 | * {@inheritDoc} 11 | */ 12 | @Override 13 | public void ditherPixel(int x, int y, double qer, double qeg, double qeb) { 14 | distributeError(x + 1, y + 0, qer, qeg, qeb, 5.0 / 32.0); 15 | distributeError(x + 2, y + 0, qer, qeg, qeb, 3.0 / 32.0); 16 | 17 | distributeError(x - 2, y + 1, qer, qeg, qeb, 2.0 / 32.0); 18 | distributeError(x - 1, y + 1, qer, qeg, qeb, 4.0 / 32.0); 19 | distributeError(x + 0, y + 1, qer, qeg, qeb, 5.0 / 32.0); 20 | distributeError(x + 1, y + 1, qer, qeg, qeb, 4.0 / 32.0); 21 | distributeError(x + 2, y + 1, qer, qeg, qeb, 2.0 / 32.0); 22 | 23 | distributeError(x - 1, y + 2, qer, qeg, qeb, 2.0 / 32.0); 24 | distributeError(x + 0, y + 2, qer, qeg, qeb, 3.0 / 32.0); 25 | distributeError(x + 1, y + 2, qer, qeg, qeb, 2.0 / 32.0); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/main/java/com/defano/jmonet/transform/dither/SierraLiteDitherer.java: -------------------------------------------------------------------------------- 1 | package com.defano.jmonet.transform.dither; 2 | 3 | /** 4 | * An implementation of the Sierra-Lite dithering algorithm. 5 | */ 6 | @SuppressWarnings("PointlessArithmeticExpression") 7 | public class SierraLiteDitherer extends AbstractDitherer { 8 | 9 | /** 10 | * {@inheritDoc} 11 | */ 12 | @Override 13 | public void ditherPixel(int x, int y, double qer, double qeg, double qeb) { 14 | distributeError(x + 1, y + 0, qer, qeg, qeb, 2.0 / 4.0); 15 | 16 | distributeError(x - 1, y + 1, qer, qeg, qeb, 1.0 / 4.0); 17 | distributeError(x + 0, y + 1, qer, qeg, qeb, 1.0 / 4.0); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/main/java/com/defano/jmonet/transform/dither/SierraTwoDitherer.java: -------------------------------------------------------------------------------- 1 | package com.defano.jmonet.transform.dither; 2 | 3 | /** 4 | * An implementation of the Sierra-Two (row) dithering algorithm. 5 | */ 6 | @SuppressWarnings("PointlessArithmeticExpression") 7 | public class SierraTwoDitherer extends AbstractDitherer { 8 | 9 | /** 10 | * {@inheritDoc} 11 | */ 12 | @Override 13 | public void ditherPixel(int x, int y, double qer, double qeg, double qeb) { 14 | distributeError(x+1, y+0, qer, qeg, qeb, 4.0/16.0); 15 | distributeError(x+2, y+0, qer, qeg, qeb, 3.0/16.0); 16 | 17 | distributeError(x-2, y+1, qer, qeg, qeb, 1.0/16.0); 18 | distributeError(x-1, y+1, qer, qeg, qeb, 2.0/16.0); 19 | distributeError(x+0, y+1, qer, qeg, qeb, 3.0/16.0); 20 | distributeError(x+1, y+1, qer, qeg, qeb, 1.0/16.0); 21 | distributeError(x+2, y+1, qer, qeg, qeb, 1.0/16.0); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/main/java/com/defano/jmonet/transform/dither/StuckiDitherer.java: -------------------------------------------------------------------------------- 1 | package com.defano.jmonet.transform.dither; 2 | 3 | /** 4 | * An implementation of the Stucki dithering algorithm. 5 | */ 6 | @SuppressWarnings({"PointlessArithmeticExpression", "unused"}) 7 | public class StuckiDitherer extends AbstractDitherer { 8 | 9 | /** 10 | * {@inheritDoc} 11 | */ 12 | @Override 13 | public void ditherPixel(int x, int y, double qer, double qeg, double qeb) { 14 | distributeError(x + 1, y + 0, qer, qeg, qeb, 8.0 / 42.0); 15 | distributeError(x + 2, y + 0, qer, qeg, qeb, 4.0 / 42.0); 16 | 17 | distributeError(x - 2, y + 1, qer, qeg, qeb, 2.0 / 42.0); 18 | distributeError(x - 1, y + 1, qer, qeg, qeb, 4.0 / 42.0); 19 | distributeError(x + 0, y + 1, qer, qeg, qeb, 8.0 / 42.0); 20 | distributeError(x + 1, y + 1, qer, qeg, qeb, 4.0 / 42.0); 21 | distributeError(x + 2, y + 1, qer, qeg, qeb, 2.0 / 42.0); 22 | 23 | distributeError(x - 2, y + 2, qer, qeg, qeb, 1.0 / 42.0); 24 | distributeError(x - 1, y + 2, qer, qeg, qeb, 2.0 / 42.0); 25 | distributeError(x + 0, y + 2, qer, qeg, qeb, 4.0 / 42.0); 26 | distributeError(x + 1, y + 2, qer, qeg, qeb, 2.0 / 42.0); 27 | distributeError(x + 2, y + 2, qer, qeg, qeb, 1.0 / 42.0); 28 | } 29 | } -------------------------------------------------------------------------------- /src/main/java/com/defano/jmonet/transform/dither/quant/ColorReductionQuantizer.java: -------------------------------------------------------------------------------- 1 | package com.defano.jmonet.transform.dither.quant; 2 | 3 | /** 4 | * Quantizes (reduces) a 24-bit, RGB-encoded color value to a reduced palette where the total number 5 | * of unique color values cannot not exceed a specified count. 6 | *

7 | * This class implements a naive color quantization algorithm that does not optimize the reduced color palette for the 8 | * given image; it simply produces a new palette with colors evenly distributed in the color space, and maps colors 9 | * in the input to the nearest color in the reduced palette. 10 | */ 11 | public class ColorReductionQuantizer implements QuantizationFunction { 12 | 13 | private final int channelColorCount; 14 | 15 | /** 16 | * Creates a {@link ColorReductionQuantizer} that reduces the color palette such that the resulting 17 | * image has no more than the given number of values in each color channel--red, green, and blue. 18 | *

19 | * Note that the specified color depth is on a per-channel basis. The total number of colors 20 | * in the palette will be the cube of this value. For example, a channelColorCount of 2 produces 21 | * an image with 8 unique colors--2 unique reds * 2 unique greens * 2 unique blues. 22 | * 23 | * @param channelColorCount The number of unique color values in each red, green and blue color 24 | * channel. 25 | */ 26 | public ColorReductionQuantizer(int channelColorCount) { 27 | this.channelColorCount = channelColorCount - 1; 28 | } 29 | 30 | /** 31 | * {@inheritDoc} 32 | */ 33 | @Override 34 | public double[] quantize(double[] input) { 35 | double[] reduced = new double[4]; 36 | 37 | reduced[0] = Math.round(input[0] * (double) channelColorCount) / (double) channelColorCount; 38 | reduced[1] = Math.round(input[1] * (double) channelColorCount) / (double) channelColorCount; 39 | reduced[2] = Math.round(input[2] * (double) channelColorCount) / (double) channelColorCount; 40 | reduced[3] = input[3]; 41 | 42 | return reduced; 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/main/java/com/defano/jmonet/transform/dither/quant/GrayscaleQuantizer.java: -------------------------------------------------------------------------------- 1 | package com.defano.jmonet.transform.dither.quant; 2 | 3 | /** 4 | * Quantizes (reduces) a 24-bit, RGB-encoded color value to a gray-scale palette where the total number 5 | * of unique grays cannot not exceed a specified count. 6 | *

7 | * This class implements a naive quantization algorithm that does not optimize the palette of grays for the 8 | * given input; it simply produces a new palette with grays evenly distributed in the color space, and maps colors 9 | * in the input to the nearest gray in the reduced palette. 10 | */ 11 | public class GrayscaleQuantizer implements QuantizationFunction { 12 | 13 | private final int graysCount; 14 | 15 | /** 16 | * Creates a {@link GrayscaleQuantizer} that reduces the color palette to a series of "true" grays 17 | * not exceeding the specified count. 18 | *

19 | * Note that 24-bit RGB color images support only 256 "true" grays (including full white and full 20 | * black); specifying any value greater than this will have the same effect as 256. 21 | * 22 | * @param graysCount The maximum number of unique grays to be present in the output. 23 | */ 24 | public GrayscaleQuantizer(int graysCount) { 25 | this.graysCount = graysCount; 26 | } 27 | 28 | /** 29 | * {@inheritDoc} 30 | */ 31 | @Override 32 | public double[] quantize(double[] input) { 33 | double[] reduced = new double[4]; 34 | 35 | double luminosity = (input[0] + input[1] + input[2]) / 3.0; 36 | luminosity = Math.round(luminosity * (double) graysCount) / (double) graysCount; 37 | 38 | reduced[0] = luminosity; 39 | reduced[1] = luminosity; 40 | reduced[2] = luminosity; 41 | reduced[3] = input[3]; // No change to alpha channel 42 | 43 | return reduced; 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/main/java/com/defano/jmonet/transform/dither/quant/MonochromaticQuantizer.java: -------------------------------------------------------------------------------- 1 | package com.defano.jmonet.transform.dither.quant; 2 | 3 | /** 4 | * Quantizes (reduces) a 24-bit, RGB-encoded color value to a monochrome (black and white) palette 5 | * where each pixel is either white (0xffffff) or black (0x000000). 6 | */ 7 | public class MonochromaticQuantizer implements QuantizationFunction { 8 | 9 | /** 10 | * {@inheritDoc} 11 | */ 12 | @Override 13 | public double[] quantize(double[] input) { 14 | double[] reduced = new double[4]; 15 | double luminosity = (input[0] + input[1] + input[2]) / 3.0; 16 | 17 | reduced[0] = luminosity > .5 ? 1.0 : 0.0; 18 | reduced[1] = luminosity > .5 ? 1.0 : 0.0; 19 | reduced[2] = luminosity > .5 ? 1.0 : 0.0; 20 | reduced[3] = input[3]; // No change to alpha channel 21 | 22 | return reduced; 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/main/java/com/defano/jmonet/transform/dither/quant/QuantizationFunction.java: -------------------------------------------------------------------------------- 1 | package com.defano.jmonet.transform.dither.quant; 2 | 3 | /** 4 | * An object which performs a color quantization (reduction) function. 5 | */ 6 | public interface QuantizationFunction { 7 | 8 | /** 9 | * Given a color value whose red, green, blue and alpha values (in that order) are represented 10 | * as double in the range 0.0..1.0, this function performs a quantization of the color, returning 11 | * a reduced set of red, green, blue and alpha values. 12 | * 13 | * @param input A color value where input[0] is the red channel, input[1] is the green channel 14 | * input[2] is the blue channel and input[3] in the alpha channel. 15 | * @return A quantized color where element[0] is the red channel, element[1] is the green channel 16 | * element[2] is the blue channel and element[3] is the alpha channel 17 | */ 18 | double[] quantize(double[] input); 19 | } 20 | -------------------------------------------------------------------------------- /src/main/java/com/defano/jmonet/transform/image/ApplyAffineTransform.java: -------------------------------------------------------------------------------- 1 | package com.defano.jmonet.transform.image; 2 | 3 | import com.defano.jmonet.model.Interpolation; 4 | 5 | import java.awt.geom.AffineTransform; 6 | import java.awt.image.AffineTransformOp; 7 | import java.awt.image.BufferedImage; 8 | 9 | /** 10 | * Applies an {@link AffineTransform} to a given image. 11 | */ 12 | public class ApplyAffineTransform implements ImageTransform { 13 | 14 | private final AffineTransform transform; 15 | private final Interpolation interpolation; 16 | 17 | /** 18 | * Creates an image transform that applies a given affine transform using bi-cubic interpolation. 19 | * 20 | * @param transform The affine transform to apply 21 | */ 22 | public ApplyAffineTransform(AffineTransform transform) { 23 | this(transform, Interpolation.BICUBIC); 24 | } 25 | 26 | /** 27 | * Creates an image transform that applies a given affine transform to the image using a specified interpolation 28 | * method. 29 | * 30 | * @param transform The affine transform to apply 31 | * @param interpolation The interpolation method to use 32 | */ 33 | public ApplyAffineTransform(AffineTransform transform, Interpolation interpolation) { 34 | this.transform = transform; 35 | this.interpolation = interpolation; 36 | } 37 | 38 | /** 39 | * {@inheritDoc} 40 | */ 41 | @Override 42 | public BufferedImage apply(BufferedImage source) { 43 | return new AffineTransformOp(transform, toAffineTransformOp(interpolation)).filter(source, null); 44 | } 45 | 46 | private int toAffineTransformOp(Interpolation interpolation) { 47 | switch (interpolation) { 48 | case NONE: 49 | throw new IllegalArgumentException("Interpolation 'none' not supported."); 50 | case NEAREST_NEIGHBOR: 51 | return AffineTransformOp.TYPE_NEAREST_NEIGHBOR; 52 | case BILINEAR: 53 | return AffineTransformOp.TYPE_BILINEAR; 54 | 55 | default: 56 | return AffineTransformOp.TYPE_BICUBIC; 57 | } 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /src/main/java/com/defano/jmonet/transform/image/ApplyPixelTransform.java: -------------------------------------------------------------------------------- 1 | package com.defano.jmonet.transform.image; 2 | 3 | import com.defano.jmonet.tools.util.ImageUtils; 4 | 5 | import java.awt.*; 6 | import java.awt.image.BufferedImage; 7 | 8 | /** 9 | * Applies a {@link PixelTransform} to every pixel of a given image that's contained within a masking shape. 10 | */ 11 | public class ApplyPixelTransform implements StaticImageTransform { 12 | 13 | private final PixelTransform transform; 14 | private final Shape mask; 15 | 16 | /** 17 | * Creates a transform that applies a {@link PixelTransform} to each pixel that is contained within the bounds of 18 | * the given shape. 19 | * 20 | * @param transform The pixel transform to apply 21 | * @param mask The shape whose bounds determine which pixels the transform should be applied to 22 | */ 23 | public ApplyPixelTransform(PixelTransform transform, Shape mask) { 24 | this.transform = transform; 25 | this.mask = mask; 26 | } 27 | 28 | /** 29 | * Creates a transform that applies a {@link PixelTransform} to each pixel in the image. 30 | * 31 | * @param transform The pixel transform to apply 32 | */ 33 | public ApplyPixelTransform(PixelTransform transform) { 34 | this.transform = transform; 35 | this.mask = null; 36 | } 37 | 38 | /** 39 | * {@inheritDoc} 40 | */ 41 | @Override 42 | public BufferedImage apply(BufferedImage source) { 43 | BufferedImage transformed = ImageUtils.argbCopy(source); 44 | 45 | for (int x = 0; x < transformed.getWidth(); x++) { 46 | for (int y = 0; y < transformed.getHeight(); y++) { 47 | if (mask == null || mask.contains(x, y)) { 48 | transformed.setRGB(x, y, transform.apply(transformed.getRGB(x, y))); 49 | } 50 | } 51 | } 52 | 53 | return transformed; 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /src/main/java/com/defano/jmonet/transform/image/BufferedImageOpTransform.java: -------------------------------------------------------------------------------- 1 | package com.defano.jmonet.transform.image; 2 | 3 | import com.defano.jmonet.tools.util.ImageUtils; 4 | 5 | import java.awt.image.BufferedImage; 6 | import java.awt.image.BufferedImageOp; 7 | 8 | /** 9 | * Transforms an image by applying a {@link BufferedImageOp} to it. 10 | */ 11 | public class BufferedImageOpTransform implements StaticImageTransform { 12 | 13 | private final BufferedImageOp operation; 14 | 15 | public BufferedImageOpTransform(BufferedImageOp operation) { 16 | this.operation = operation; 17 | } 18 | 19 | @Override 20 | public BufferedImage apply(BufferedImage source) { 21 | BufferedImage destination = ImageUtils.newArgbOfSize(source); 22 | operation.filter(source, destination); 23 | return destination; 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/main/java/com/defano/jmonet/transform/image/ColorReductionTransform.java: -------------------------------------------------------------------------------- 1 | package com.defano.jmonet.transform.image; 2 | 3 | import com.defano.jmonet.transform.dither.Ditherer; 4 | import com.defano.jmonet.transform.dither.FloydSteinbergDitherer; 5 | import com.defano.jmonet.transform.dither.quant.ColorReductionQuantizer; 6 | import com.defano.jmonet.transform.dither.quant.MonochromaticQuantizer; 7 | 8 | import java.awt.image.BufferedImage; 9 | 10 | /** 11 | * Converts the color space of an image to a reduced palette containing no more than a specified number of colors. 12 | *

13 | * Note that this transform merely adjusts the look of the image and does not affect the image storage 14 | * in any way (all images are always stored in 24-bit "true color" irrespective of whether they have been reduced 15 | * via this method). Thus, reducing colors will not reduce memory usage or affect how the image is exported or saved. 16 | */ 17 | public class ColorReductionTransform implements StaticImageTransform { 18 | 19 | private final Ditherer ditherer; 20 | private final int colorDepth; 21 | 22 | /** 23 | * Constructs a color reduction transform. 24 | * 25 | * @param colorDepth The maximum number of unique colors that should appear in the resultant selection image; zero 26 | * produces a black and white (monochrome) image. Note that color depth should be cubic; if 27 | * the cubed root of colorDepth is not an integer, the the floor of the cubed root 28 | * will be assumed. 29 | * @param ditherer The dithering algorithm to use, for example, {@link FloydSteinbergDitherer}. 30 | */ 31 | public ColorReductionTransform(Ditherer ditherer, int colorDepth) { 32 | this.ditherer = ditherer; 33 | this.colorDepth = colorDepth; 34 | } 35 | 36 | /** 37 | * {@inheritDoc} 38 | */ 39 | @Override 40 | public BufferedImage apply(BufferedImage source) { 41 | int channelDepth = (int) Math.floor(Math.cbrt(colorDepth)); 42 | 43 | return colorDepth == 0 ? 44 | ditherer.dither(source, new MonochromaticQuantizer()) : 45 | ditherer.dither(source, new ColorReductionQuantizer(channelDepth)); 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/main/java/com/defano/jmonet/transform/image/ConvolutionTransform.java: -------------------------------------------------------------------------------- 1 | package com.defano.jmonet.transform.image; 2 | 3 | import com.defano.jmonet.tools.util.ImageUtils; 4 | 5 | import java.awt.image.BufferedImage; 6 | import java.awt.image.BufferedImageOp; 7 | import java.awt.image.ConvolveOp; 8 | import java.awt.image.Kernel; 9 | 10 | /** 11 | * Convolves an image by applying an image {@link Kernel} to each pixel. 12 | */ 13 | public class ConvolutionTransform implements StaticImageTransform { 14 | 15 | private final Kernel kernel; 16 | 17 | public ConvolutionTransform(Kernel kernel) { 18 | this.kernel = kernel; 19 | } 20 | 21 | @Override 22 | public BufferedImage apply(BufferedImage source) { 23 | BufferedImage destination = ImageUtils.newArgbOfSize(source); 24 | 25 | BufferedImageOp convolution = new ConvolveOp(kernel, ConvolveOp.EDGE_ZERO_FILL, null); 26 | convolution.filter(source, destination); 27 | 28 | return destination; 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/main/java/com/defano/jmonet/transform/image/FloodFillTransform.java: -------------------------------------------------------------------------------- 1 | package com.defano.jmonet.transform.image; 2 | 3 | import com.defano.jmonet.tools.attributes.BoundaryFunction; 4 | import com.defano.jmonet.tools.attributes.FillFunction; 5 | 6 | import java.awt.*; 7 | import java.awt.image.BufferedImage; 8 | import java.util.ArrayList; 9 | 10 | /** 11 | * Performs a "flood fill" (sometimes called "seed fill" or "spill paint") of the image with a provided paint or 12 | * texture. Terrible performance on very large fill surfaces. 13 | * 14 | * Given an origin point in the image, this algorithm iteratively paints every adjacent pixel with the given color or 15 | * texture until it reaches a boundary pixel. 16 | */ 17 | @SuppressWarnings("unused") 18 | public class FloodFillTransform implements ImageTransform { 19 | 20 | private BoundaryFunction boundaryFunction; 21 | private FillFunction fill; 22 | private Point origin; 23 | private Paint fillPaint; 24 | 25 | /** 26 | * {@inheritDoc} 27 | */ 28 | @Override 29 | public BufferedImage apply(BufferedImage source) { 30 | 31 | ArrayList fillPixels = new ArrayList<>(); 32 | 33 | Rectangle bounds = new Rectangle(0, 0, source.getWidth(), source.getHeight()); 34 | BufferedImage transformed = new BufferedImage(source.getWidth(), source.getHeight(), BufferedImage.TYPE_INT_ARGB); 35 | 36 | // Start by filling origin pixel (i.e., clicked pixel) 37 | fillPixels.add(origin); 38 | 39 | while (!fillPixels.isEmpty()) { 40 | 41 | Point popped = fillPixels.remove(fillPixels.size() - 1); 42 | int thisPixelX = popped.x; 43 | int thisPixelY = popped.y; 44 | 45 | fill.fill(transformed, thisPixelX, thisPixelY, fillPaint); 46 | 47 | if (bounds.contains(thisPixelX + 1, thisPixelY) && !boundaryFunction.isBoundary(source, transformed, thisPixelX + 1, thisPixelY)) { 48 | fillPixels.add(new Point(thisPixelX + 1, thisPixelY)); 49 | } 50 | 51 | if (bounds.contains(thisPixelX - 1, thisPixelY) && !boundaryFunction.isBoundary(source, transformed, thisPixelX - 1, thisPixelY)) { 52 | fillPixels.add(new Point(thisPixelX - 1, thisPixelY)); 53 | } 54 | 55 | if (bounds.contains(thisPixelX, thisPixelY + 1) && !boundaryFunction.isBoundary(source, transformed, thisPixelX, thisPixelY + 1)) { 56 | fillPixels.add(new Point(thisPixelX, thisPixelY + 1)); 57 | } 58 | 59 | if (bounds.contains(thisPixelX, thisPixelY - 1) && !boundaryFunction.isBoundary(source, transformed, thisPixelX, thisPixelY - 1)) { 60 | fillPixels.add(new Point(thisPixelX, thisPixelY - 1)); 61 | } 62 | } 63 | 64 | return transformed; 65 | } 66 | 67 | public BoundaryFunction getBoundaryFunction() { 68 | return boundaryFunction; 69 | } 70 | 71 | public void setBoundaryFunction(BoundaryFunction boundaryFunction) { 72 | this.boundaryFunction = boundaryFunction; 73 | } 74 | 75 | public FillFunction getFill() { 76 | return fill; 77 | } 78 | 79 | public void setFill(FillFunction fill) { 80 | this.fill = fill; 81 | } 82 | 83 | public Point getOrigin() { 84 | return origin; 85 | } 86 | 87 | public void setOrigin(Point origin) { 88 | this.origin = origin; 89 | } 90 | 91 | public Paint getFillPaint() { 92 | return fillPaint; 93 | } 94 | 95 | public void setFillPaint(Paint fillPaint) { 96 | this.fillPaint = fillPaint; 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /src/main/java/com/defano/jmonet/transform/image/GreyscaleReductionTransform.java: -------------------------------------------------------------------------------- 1 | package com.defano.jmonet.transform.image; 2 | 3 | import com.defano.jmonet.transform.dither.Ditherer; 4 | import com.defano.jmonet.transform.dither.FloydSteinbergDitherer; 5 | import com.defano.jmonet.transform.dither.quant.GrayscaleQuantizer; 6 | import com.defano.jmonet.transform.dither.quant.MonochromaticQuantizer; 7 | 8 | import java.awt.image.BufferedImage; 9 | 10 | /** 11 | * Transform an image to a gray-scale containing no more than the specified number of gray shades. 12 | */ 13 | public class GreyscaleReductionTransform implements StaticImageTransform { 14 | 15 | private final Ditherer ditherer; 16 | private final int grayDepth; 17 | 18 | /** 19 | * Creates a greyscale color reduction transform. 20 | * 21 | * @param grayDepth The maximum number of unique shades of gray in which to render the given image; zero produces 22 | * a black and white (monochrome) image. 23 | * @param ditherer The dithering algorithm to use, for example {@link FloydSteinbergDitherer}. 24 | */ 25 | public GreyscaleReductionTransform(Ditherer ditherer, int grayDepth) { 26 | this.ditherer = ditherer; 27 | this.grayDepth = grayDepth; 28 | } 29 | 30 | /** 31 | * {@inheritDoc} 32 | */ 33 | @Override 34 | public BufferedImage apply(BufferedImage source) { 35 | return grayDepth == 0 ? 36 | ditherer.dither(source, new MonochromaticQuantizer()) : 37 | ditherer.dither(source, new GrayscaleQuantizer(grayDepth)); 38 | 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/main/java/com/defano/jmonet/transform/image/ImageTransform.java: -------------------------------------------------------------------------------- 1 | package com.defano.jmonet.transform.image; 2 | 3 | import java.awt.image.BufferedImage; 4 | 5 | /** 6 | * A transform that can be applied to a {@link BufferedImage} as a whole, like rotate, flip, and scale operations. 7 | */ 8 | public interface ImageTransform { 9 | 10 | /** 11 | * Applies a transform operation to the given image, producing a new, transformed image. This method does not 12 | * destroy or otherwise mutate the source image. 13 | * 14 | * Note that this operation may return an image whose dimensions differ from the source image. See 15 | * {@link StaticImageTransform} for a transform which maintains source dimensions. 16 | * 17 | * @param source The source image to which the transform should be applied. 18 | * @return A new, transformed image. 19 | */ 20 | BufferedImage apply(BufferedImage source); 21 | } 22 | -------------------------------------------------------------------------------- /src/main/java/com/defano/jmonet/transform/image/PixelTransform.java: -------------------------------------------------------------------------------- 1 | package com.defano.jmonet.transform.image; 2 | 3 | /** 4 | * A transform that can be applied to an individual pixel, like invert, or brightness adjustment. 5 | */ 6 | public interface PixelTransform { 7 | 8 | /** 9 | * Performs a transformation on a pixel's RGB color value. 10 | * 11 | * @param rgb The pixel's color value 12 | * @return The transformed pixel's color value 13 | */ 14 | int apply(int rgb); 15 | } 16 | -------------------------------------------------------------------------------- /src/main/java/com/defano/jmonet/transform/image/ScaleTransform.java: -------------------------------------------------------------------------------- 1 | package com.defano.jmonet.transform.image; 2 | 3 | import java.awt.*; 4 | import java.awt.image.BufferedImage; 5 | 6 | /** 7 | * Transforms an image by scaling it to a specified dimension. 8 | */ 9 | public class ScaleTransform implements ImageTransform { 10 | 11 | private final Dimension size; 12 | 13 | /** 14 | * Creates a scale transform. 15 | * 16 | * @param size The dimension to which the image should be resized/scaled 17 | */ 18 | public ScaleTransform(Dimension size) { 19 | this.size = size; 20 | } 21 | 22 | /** 23 | * {@inheritDoc} 24 | */ 25 | @Override 26 | public BufferedImage apply(BufferedImage source) { 27 | BufferedImage resized = new BufferedImage(size.width, size.height, BufferedImage.TYPE_INT_ARGB); 28 | Graphics2D g = (Graphics2D) resized.getGraphics(); 29 | g.drawImage(source, 0, 0, size.width, size.height, null); 30 | g.dispose(); 31 | 32 | return resized; 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/main/java/com/defano/jmonet/transform/image/SlantTransform.java: -------------------------------------------------------------------------------- 1 | package com.defano.jmonet.transform.image; 2 | 3 | import java.awt.geom.AffineTransform; 4 | import java.awt.image.AffineTransformOp; 5 | import java.awt.image.BufferedImage; 6 | 7 | /** 8 | * Vertically slants (shears) an image by a given angle and applies an x-translation to compensate for the change 9 | * in position implied by the slant (allows the vertical center-line to remain constant, if desired). 10 | */ 11 | public class SlantTransform implements ImageTransform { 12 | 13 | private final double theta; 14 | private final int xTranslation; 15 | 16 | /** 17 | * Creates a slant image transform. 18 | * 19 | * @param theta The angle, in radians, to shear the image 20 | * @param xTranslation The number of pixels to translate the image, typically this is calculated by determining the 21 | * number of pixels left or right the image has been sheared (delta between top-left corner and 22 | * bottom-left corner), then dividing this value in half. 23 | */ 24 | public SlantTransform(double theta, int xTranslation) { 25 | this.theta = theta; 26 | this.xTranslation = xTranslation; 27 | } 28 | 29 | /** 30 | * {@inheritDoc} 31 | */ 32 | @Override 33 | public BufferedImage apply(BufferedImage source) { 34 | AffineTransform transform = AffineTransform.getTranslateInstance(xTranslation, 0); 35 | transform.shear(Math.tan(theta), 0); 36 | 37 | transform.translate(xTranslation, 0); 38 | AffineTransformOp op = new AffineTransformOp(transform, AffineTransformOp.TYPE_BICUBIC); 39 | return op.filter(source, null); 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/main/java/com/defano/jmonet/transform/image/StaticImageTransform.java: -------------------------------------------------------------------------------- 1 | package com.defano.jmonet.transform.image; 2 | 3 | import java.awt.image.BufferedImage; 4 | 5 | /** 6 | * A transformation that can be applied to a {@link BufferedImage} as a whole, but which does not modify the dimensions 7 | * of the image (like color reduction or fill). 8 | * 9 | * See {@link ImageTransform} for a transform that can modify image dimensions. 10 | */ 11 | public interface StaticImageTransform extends ImageTransform { 12 | 13 | /** 14 | * Applies a transform operation to the given image, producing a new, transformed image of the same dimensions as 15 | * the source image. This method does not destroy or otherwise mutate the source image. 16 | * 17 | * @param source The source image to which the transform should be applied. 18 | * @return A new, transformed image. 19 | */ 20 | BufferedImage apply(BufferedImage source); 21 | } 22 | -------------------------------------------------------------------------------- /src/main/java/com/defano/jmonet/transform/image/StaticImageTransformable.java: -------------------------------------------------------------------------------- 1 | package com.defano.jmonet.transform.image; 2 | 3 | /** 4 | * An image-containing object that can be transformed by a {@link StaticImageTransform}; that is, all the pixels of the 5 | * image may be transformed, but the dimensions or bounds of the image can not. 6 | */ 7 | public interface StaticImageTransformable { 8 | 9 | /** 10 | * Performs a transformation on the image that does not effect the dimensions, bounds or location of the 11 | * affected image. 12 | * 13 | * @param transform The transform to perform. 14 | */ 15 | void transform(StaticImageTransform transform); 16 | 17 | /** 18 | * Applies a {@link PixelTransform} on all the pixels of the image. 19 | * @param transform The pixel transform to apply 20 | */ 21 | void transform(PixelTransform transform); 22 | } 23 | -------------------------------------------------------------------------------- /src/main/java/com/defano/jmonet/transform/pixel/BrightnessPixelTransform.java: -------------------------------------------------------------------------------- 1 | package com.defano.jmonet.transform.pixel; 2 | 3 | import com.defano.jmonet.transform.image.PixelTransform; 4 | 5 | /** 6 | * Modifies the brightness (luminosity) of each affected pixel by adding/subtracting a delta value to each color channel 7 | * of an affected pixel. 8 | */ 9 | public class BrightnessPixelTransform implements PixelTransform { 10 | 11 | private final int delta; 12 | 13 | /** 14 | * Creates a brightness-adjusting transform. 15 | * 16 | * @param delta The amount by which to adjust brightness; a value of -255 assures that every pixel is completely 17 | * black; a value of +255 assures that every pixel is completely white. 18 | */ 19 | public BrightnessPixelTransform(int delta) { 20 | this.delta = delta; 21 | } 22 | 23 | /** 24 | * {@inheritDoc} 25 | */ 26 | @SuppressWarnings("squid:S3358") 27 | @Override 28 | public int apply(int rgb) { 29 | int alpha = 0xff000000 & rgb; 30 | int r = ((0xff0000 & rgb) >> 16) + delta; 31 | int g = ((0xff00 & rgb) >> 8) + delta; 32 | int b = (0xff & rgb) + delta; 33 | 34 | // Saturate at 0 and 256 35 | r = r > 0xff ? 0xff : r < 0 ? 0 : r; 36 | g = g > 0xff ? 0xff : g < 0 ? 0 : g; 37 | b = b > 0xff ? 0xff : b < 0 ? 0 : b; 38 | 39 | // Adjust preserving alpha channel 40 | return alpha | (r << 16) | (g << 8) | b; 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/main/java/com/defano/jmonet/transform/pixel/InvertPixelTransform.java: -------------------------------------------------------------------------------- 1 | package com.defano.jmonet.transform.pixel; 2 | 3 | import com.defano.jmonet.transform.image.PixelTransform; 4 | 5 | /** 6 | * Inverts the color value of each affected pixel. 7 | */ 8 | public class InvertPixelTransform implements PixelTransform { 9 | 10 | /** 11 | * {@inheritDoc} 12 | */ 13 | @Override 14 | public int apply(int argb) { 15 | int alpha = 0xff000000 & argb; 16 | int rgb = 0x00ffffff & argb; 17 | 18 | // Invert preserving alpha channel 19 | return alpha | (~rgb & 0x00ffffff); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/main/java/com/defano/jmonet/transform/pixel/RemoveAlphaPixelTransform.java: -------------------------------------------------------------------------------- 1 | package com.defano.jmonet.transform.pixel; 2 | 3 | import com.defano.jmonet.transform.image.PixelTransform; 4 | 5 | import java.awt.*; 6 | 7 | /** 8 | * Saturates the alpha channel of any pixel that is not fully transparent or fully opaque (has no effect on fully opaque 9 | * or fully transparent pixels). 10 | */ 11 | public class RemoveAlphaPixelTransform implements PixelTransform { 12 | 13 | private final boolean makeTransparent; 14 | 15 | /** 16 | * Creates an alpha-removing transform. 17 | * 18 | * @param makeTransparent When true, translucent (but not opaque) pixels will be made fully transparent; when false 19 | * they will be made opaque. 20 | */ 21 | public RemoveAlphaPixelTransform(boolean makeTransparent) { 22 | this.makeTransparent = makeTransparent; 23 | } 24 | 25 | /** 26 | * {@inheritDoc} 27 | */ 28 | @Override 29 | public int apply(int rgb) { 30 | Color color = new Color(rgb, true); 31 | 32 | int alpha = color.getAlpha(); 33 | if (alpha != 0 && alpha != 0xff) { 34 | alpha = makeTransparent ? 0x00 : 0xff; 35 | } 36 | 37 | // Adjust alpha preserving color channel 38 | return new Color(color.getRed(), color.getGreen(), color.getBlue(), alpha).getRGB(); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/main/java/com/defano/jmonet/transform/pixel/TransparencyPixelTransform.java: -------------------------------------------------------------------------------- 1 | package com.defano.jmonet.transform.pixel; 2 | 3 | import com.defano.jmonet.transform.image.PixelTransform; 4 | 5 | import java.awt.*; 6 | 7 | /** 8 | * Adjusts the level of transparency (alpha) in each affected pixel. 9 | */ 10 | public class TransparencyPixelTransform implements PixelTransform { 11 | 12 | private final int delta; 13 | 14 | /** 15 | * Creates a transparency-adjusting transform. 16 | * 17 | * @param delta The amount by which to adjust each affected pixel's alpha value. A value of +255 assures every pixel 18 | * is fully opaque; a value of -255 assures every pixel is fully transparent. 19 | */ 20 | public TransparencyPixelTransform(int delta) { 21 | this.delta = delta; 22 | } 23 | 24 | /** 25 | * {@inheritDoc} 26 | */ 27 | @SuppressWarnings("squid:S3358") 28 | @Override 29 | public int apply(int rgb) { 30 | Color color = new Color(rgb, true); 31 | 32 | int alpha = color.getAlpha() + delta; 33 | alpha = alpha > 0xff ? 0xff : alpha < 0 ? 0 : alpha; 34 | 35 | // Adjust alpha preserving color channel 36 | return new Color(color.getRed(), color.getGreen(), color.getBlue(), alpha).getRGB(); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/main/resources/cursors/fill.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/defano/jmonet/21333cbfccdc89ae9c1db0b93b2b760439c5cd59/src/main/resources/cursors/fill.png -------------------------------------------------------------------------------- /src/main/resources/cursors/lasso.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/defano/jmonet/21333cbfccdc89ae9c1db0b93b2b760439c5cd59/src/main/resources/cursors/lasso.png -------------------------------------------------------------------------------- /src/main/resources/cursors/magnifier.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/defano/jmonet/21333cbfccdc89ae9c1db0b93b2b760439c5cd59/src/main/resources/cursors/magnifier.png -------------------------------------------------------------------------------- /src/main/resources/cursors/magnifier_minus.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/defano/jmonet/21333cbfccdc89ae9c1db0b93b2b760439c5cd59/src/main/resources/cursors/magnifier_minus.png -------------------------------------------------------------------------------- /src/main/resources/cursors/magnifier_plus.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/defano/jmonet/21333cbfccdc89ae9c1db0b93b2b760439c5cd59/src/main/resources/cursors/magnifier_plus.png -------------------------------------------------------------------------------- /src/main/resources/cursors/pencil.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/defano/jmonet/21333cbfccdc89ae9c1db0b93b2b760439c5cd59/src/main/resources/cursors/pencil.png -------------------------------------------------------------------------------- /src/test/java/com/defano/jmonet/tools/AirbrushToolTest.java: -------------------------------------------------------------------------------- 1 | package com.defano.jmonet.tools; 2 | 3 | import com.defano.jmonet.tools.base.MockitoToolTest; 4 | import org.junit.jupiter.api.BeforeEach; 5 | import org.junit.jupiter.api.Test; 6 | import org.mockito.Mockito; 7 | 8 | import java.awt.*; 9 | import java.awt.geom.Line2D; 10 | 11 | import static org.mockito.Matchers.argThat; 12 | import static org.mockito.Matchers.eq; 13 | 14 | public class AirbrushToolTest extends MockitoToolTest { 15 | 16 | @BeforeEach 17 | public void setUp() { 18 | initialize(new AirbrushTool()); 19 | } 20 | 21 | @Test 22 | public void testThatDefaultCursorIsSet() { 23 | Mockito.verify(mockCursorManager).setToolCursor(argThat(matchesCursor(new Cursor(Cursor.CROSSHAIR_CURSOR))), eq(mockCanvas)); 24 | } 25 | 26 | @Test 27 | public void testThatPointInPathIsAdded() { 28 | // Setup 29 | final Stroke stroke = new BasicStroke(10); 30 | final Paint fillPaint = Color.BLUE; 31 | final Point lastPoint = new Point(10, 10); 32 | final Point thisPoint = new Point(20, 20); 33 | 34 | Mockito.when(mockToolAttributes.getIntensity()).thenReturn(1.0); 35 | 36 | // Run the test 37 | uut.addPoint(mockScratch, stroke, fillPaint, lastPoint, thisPoint); 38 | 39 | // Verify the results 40 | Mockito.verify(mockAddScratchGraphics).setStroke(stroke); 41 | Mockito.verify(mockAddScratchGraphics).setPaint(fillPaint); 42 | Mockito.verify(mockAddScratchGraphics).setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_OVER, 1.0f)); 43 | Mockito.verify(mockAddScratchGraphics).draw(argThat(matchesShape(new Line2D.Float(lastPoint, thisPoint)))); 44 | } 45 | 46 | } 47 | -------------------------------------------------------------------------------- /src/test/java/com/defano/jmonet/tools/ArrowToolTest.java: -------------------------------------------------------------------------------- 1 | package com.defano.jmonet.tools; 2 | 3 | import com.defano.jmonet.tools.base.MockitoToolTest; 4 | import org.junit.jupiter.api.BeforeEach; 5 | import org.junit.jupiter.api.Test; 6 | import org.mockito.Mockito; 7 | 8 | import java.awt.*; 9 | 10 | import static org.mockito.Matchers.argThat; 11 | import static org.mockito.Matchers.eq; 12 | 13 | public class ArrowToolTest extends MockitoToolTest { 14 | 15 | @BeforeEach 16 | public void setUp() { 17 | initialize(new ArrowTool()); 18 | } 19 | 20 | @Test 21 | public void testThatDefaultCursorIsSet() { 22 | Mockito.verify(mockCursorManager).setToolCursor(argThat(matchesCursor(Cursor.getDefaultCursor())), eq(mockCanvas)); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/test/java/com/defano/jmonet/tools/EraserToolTest.java: -------------------------------------------------------------------------------- 1 | package com.defano.jmonet.tools; 2 | 3 | import com.defano.jmonet.tools.base.MockitoToolTest; 4 | import org.junit.jupiter.api.BeforeEach; 5 | import org.junit.jupiter.api.Test; 6 | import org.mockito.Mockito; 7 | 8 | import java.awt.*; 9 | import java.awt.geom.Line2D; 10 | 11 | import static org.mockito.Matchers.argThat; 12 | import static org.mockito.Matchers.eq; 13 | 14 | public class EraserToolTest extends MockitoToolTest { 15 | 16 | @BeforeEach 17 | public void setUp() { 18 | initialize(new EraserTool()); 19 | } 20 | 21 | @Test 22 | public void testThatInitialPathIsErased() { 23 | Stroke stroke = new BasicStroke(1); 24 | Point initialPoint = new Point(10, 10); 25 | 26 | uut.startPath(mockScratch, stroke, null, initialPoint); 27 | Mockito.verify(mockScratch).erase(eq(uut), argThat(matchesShape(new Line2D.Float(initialPoint, initialPoint))), eq(stroke)); 28 | } 29 | 30 | @Test 31 | public void testThatSubsequentPointsOnPathAreErased() { 32 | Stroke stroke = new BasicStroke(1); 33 | Point initialPoint = new Point(10, 10); 34 | Point thisPoint = new Point(20, 20); 35 | 36 | uut.addPoint(mockScratch, stroke, null, initialPoint, thisPoint); 37 | 38 | Mockito.verify(mockScratch).erase(eq(uut), argThat(matchesShape(new Line2D.Float(initialPoint, thisPoint))), eq(stroke)); 39 | } 40 | 41 | @Test 42 | public void testThatCompletePathDoesNothing() { 43 | uut.completePath(mockScratch, null, null, null); 44 | Mockito.verifyZeroInteractions(mockScratch); 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/test/java/com/defano/jmonet/tools/FillToolTest.java: -------------------------------------------------------------------------------- 1 | package com.defano.jmonet.tools; 2 | 3 | import com.defano.jmonet.tools.attributes.BoundaryFunction; 4 | import com.defano.jmonet.tools.attributes.FillFunction; 5 | import com.defano.jmonet.tools.base.MockitoToolTest; 6 | import com.defano.jmonet.tools.cursors.CursorFactory; 7 | import org.junit.jupiter.api.BeforeEach; 8 | import org.junit.jupiter.api.Test; 9 | import org.mockito.Mock; 10 | import org.mockito.Mockito; 11 | 12 | import java.awt.*; 13 | import java.awt.image.BufferedImage; 14 | import java.util.Optional; 15 | 16 | import static org.mockito.Matchers.argThat; 17 | import static org.mockito.Matchers.eq; 18 | 19 | public class FillToolTest extends MockitoToolTest { 20 | 21 | @Mock private BoundaryFunction mockBoundaryFunction; 22 | @Mock private FillFunction mockFillFunction; 23 | @Mock private Paint mockFillPaint; 24 | @Mock private BufferedImage mockFilledImage; 25 | 26 | @BeforeEach 27 | public void setUp() { 28 | initialize(new FillTool()); 29 | } 30 | 31 | @Test 32 | public void testThatDefaultCursorIsSet() { 33 | Mockito.verify(mockCursorManager).setToolCursor(argThat(matchesCursor(CursorFactory.makeBucketCursor())), eq(mockCanvas)); 34 | } 35 | 36 | @Test 37 | public void testThatMouseDownFloodFills() { 38 | Point floodOrigin = new Point(); 39 | Dimension canvasSize = new Dimension(); 40 | 41 | Mockito.when(mockToolAttributes.getBoundaryFunction()).thenReturn(mockBoundaryFunction); 42 | Mockito.when(mockToolAttributes.getFillPaint()).thenReturn(Optional.of(mockFillPaint)); 43 | Mockito.when(mockToolAttributes.getFillFunction()).thenReturn(mockFillFunction); 44 | Mockito.when(mockFloodFillTransform.apply(mockCanvasImage)).thenReturn(mockFilledImage); 45 | Mockito.when(mockCanvas.getCanvasSize()).thenReturn(canvasSize); 46 | 47 | uut.mousePressed(null, floodOrigin); 48 | 49 | Mockito.verify(mockFloodFillTransform).setBoundaryFunction(mockBoundaryFunction); 50 | Mockito.verify(mockFloodFillTransform).setFillPaint(mockFillPaint); 51 | Mockito.verify(mockFloodFillTransform).setFill(mockFillFunction); 52 | Mockito.verify(mockFloodFillTransform).setOrigin(floodOrigin); 53 | 54 | Mockito.verify(mockScratch).setAddScratch(eq(mockFilledImage), argThat(matchesShape(new Rectangle(canvasSize)))); 55 | 56 | Mockito.verify(mockCanvas).commit(); 57 | Mockito.verify(mockCanvas).repaint(); 58 | } 59 | } -------------------------------------------------------------------------------- /src/test/java/com/defano/jmonet/tools/LineToolTest.java: -------------------------------------------------------------------------------- 1 | package com.defano.jmonet.tools; 2 | 3 | import com.defano.jmonet.tools.base.MockitoToolTest; 4 | import org.junit.jupiter.api.BeforeEach; 5 | import org.junit.jupiter.api.Test; 6 | import org.mockito.Mock; 7 | import org.mockito.Mockito; 8 | 9 | import java.awt.*; 10 | import java.awt.geom.Line2D; 11 | 12 | import static org.mockito.Matchers.argThat; 13 | import static org.mockito.Matchers.eq; 14 | 15 | public class LineToolTest extends MockitoToolTest { 16 | 17 | @Mock private Stroke mockStroke; 18 | @Mock private Paint mockPaint; 19 | 20 | @BeforeEach 21 | public void setup() { 22 | initialize(new LineTool()); 23 | } 24 | 25 | @Test 26 | public void testThatLineIsDrawn() { 27 | Line2D.Float l = new Line2D.Float(10, 20, 30, 40); 28 | 29 | uut.drawLine(mockScratch, mockStroke, mockPaint, (int) l.x1, (int) l.y1, (int) l.x2, (int) l.y2); 30 | 31 | Mockito.verify(mockScratch).getAddScratchGraphics(eq(uut), eq(mockStroke), argThat(matchesShape(l))); 32 | Mockito.verify(mockAddScratchGraphics).setPaint(mockPaint); 33 | Mockito.verify(mockAddScratchGraphics).setStroke(mockStroke); 34 | Mockito.verify(mockAddScratchGraphics).draw(argThat(matchesShape(l))); 35 | } 36 | 37 | } 38 | -------------------------------------------------------------------------------- /src/test/java/com/defano/jmonet/tools/RectangleToolTest.java: -------------------------------------------------------------------------------- 1 | package com.defano.jmonet.tools; 2 | 3 | import com.defano.jmonet.tools.base.MockitoToolTest; 4 | import org.junit.jupiter.api.BeforeEach; 5 | import org.junit.jupiter.api.Test; 6 | import org.mockito.Mockito; 7 | 8 | import java.awt.*; 9 | 10 | import static org.mockito.Matchers.argThat; 11 | import static org.mockito.Matchers.eq; 12 | 13 | public class RectangleToolTest extends MockitoToolTest { 14 | 15 | @BeforeEach 16 | public void setUp() { 17 | initialize(new RectangleTool()); 18 | } 19 | 20 | @Test 21 | public void testDefaultCursor() { 22 | Mockito.verify(mockCursorManager).setToolCursor(argThat(matchesCursor(new Cursor(Cursor.CROSSHAIR_CURSOR))), eq(mockCanvas)); 23 | } 24 | 25 | @Test 26 | public void testThatRectangleIsStroked() { 27 | Rectangle bounds = new Rectangle(1, 2, 3, 4); 28 | Paint fill = Color.black; 29 | Stroke stroke = new BasicStroke(1); 30 | 31 | uut.strokeBounds(mockScratch, stroke, fill, bounds, false); 32 | 33 | Mockito.verify(mockScratch).getAddScratchGraphics(eq(uut), eq(stroke), argThat(matchesShape(bounds))); 34 | Mockito.verify(mockAddScratchGraphics).setStroke(stroke); 35 | Mockito.verify(mockAddScratchGraphics).setPaint(fill); 36 | Mockito.verify(mockAddScratchGraphics).draw(bounds); 37 | } 38 | 39 | @Test 40 | public void testThatRectangleIsFilled() { 41 | Rectangle bounds = new Rectangle(1, 2, 3, 4); 42 | Paint fill = Color.black; 43 | 44 | uut.fillBounds(mockScratch, fill, bounds, false); 45 | 46 | Mockito.verify(mockScratch).getAddScratchGraphics(eq(uut), argThat(matchesShape(bounds))); 47 | Mockito.verify(mockAddScratchGraphics).setPaint(fill); 48 | Mockito.verify(mockAddScratchGraphics).fill(bounds); 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/test/java/com/defano/jmonet/tools/base/BaseToolTest.java: -------------------------------------------------------------------------------- 1 | package com.defano.jmonet.tools.base; 2 | 3 | import com.defano.jmonet.canvas.PaintCanvas; 4 | import com.defano.jmonet.canvas.Scratch; 5 | import com.defano.jmonet.context.GraphicsContext; 6 | import com.defano.jmonet.model.Interpolation; 7 | import com.defano.jmonet.tools.attributes.ToolAttributes; 8 | import com.defano.jmonet.tools.cursors.CursorManager; 9 | import com.google.inject.AbstractModule; 10 | import com.google.inject.Guice; 11 | import org.mockito.Answers; 12 | import org.mockito.Mock; 13 | import org.mockito.Mockito; 14 | 15 | import java.awt.*; 16 | 17 | abstract public class BaseToolTest extends MockitoTest { 18 | 19 | protected ToolType uut; 20 | 21 | @Mock(answer = Answers.RETURNS_DEEP_STUBS) protected CursorManager mockCursorManager; 22 | @Mock(answer = Answers.RETURNS_DEEP_STUBS) protected ToolAttributes mockToolAttributes; 23 | @Mock(answer = Answers.RETURNS_DEEP_STUBS) protected Cursor mockCursor; 24 | @Mock(answer = Answers.RETURNS_DEEP_STUBS) protected PaintCanvas mockCanvas; 25 | @Mock(answer = Answers.RETURNS_DEEP_STUBS) protected Scratch mockScratch; 26 | @Mock(answer = Answers.RETURNS_DEEP_STUBS) protected GraphicsContext mockAddScratch; 27 | @Mock(answer = Answers.RETURNS_DEEP_STUBS) protected GraphicsContext mockRemoveScratch; 28 | 29 | // Can't mock enums 30 | protected Interpolation expectedInterpolation = Interpolation.BICUBIC; 31 | 32 | protected void setup(ToolType uut) { 33 | super.setup(); 34 | 35 | this.uut = uut; 36 | Guice.createInjector(new BaseToolTest.BasicToolAssembly()).injectMembers(this.uut); 37 | 38 | Mockito.when(mockToolAttributes.getAntiAliasing()).thenReturn(expectedInterpolation); 39 | Mockito.when(mockCanvas.getScratch()).thenReturn(mockScratch); 40 | Mockito.when(mockScratch.getAddScratchGraphics(uut, null)).thenReturn(mockAddScratch); 41 | Mockito.when(mockScratch.getRemoveScratchGraphics(uut, null)).thenReturn(mockRemoveScratch); 42 | } 43 | 44 | private class BasicToolAssembly extends AbstractModule { 45 | @Override 46 | protected void configure() { 47 | bind(CursorManager.class).toInstance(mockCursorManager); 48 | bind(ToolAttributes.class).toInstance(mockToolAttributes); 49 | } 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/test/java/com/defano/jmonet/tools/base/BasicToolTest.java: -------------------------------------------------------------------------------- 1 | package com.defano.jmonet.tools.base; 2 | 3 | import com.defano.jmonet.model.PaintToolType; 4 | import org.junit.jupiter.api.BeforeEach; 5 | import org.junit.jupiter.api.Test; 6 | import org.mockito.Mockito; 7 | 8 | import static org.junit.jupiter.api.Assertions.*; 9 | import static org.mockito.Matchers.any; 10 | import static org.mockito.Matchers.eq; 11 | 12 | class BasicToolTest extends BaseToolTest { 13 | 14 | @BeforeEach 15 | protected void setup() { 16 | super.setup(new BasicTool(null)); 17 | } 18 | 19 | @Test 20 | void testThatGetToolTypeReturnsToolType() { 21 | for (PaintToolType thisType : PaintToolType.values()) { 22 | assertEquals(thisType, new BasicTool(thisType).getPaintToolType()); 23 | } 24 | } 25 | 26 | @Test 27 | void testThatNewToolIsNotActive() { 28 | assertFalse(new BasicTool<>(null).isActive()); 29 | } 30 | 31 | @Test 32 | void testThatActivatedToolIsActive() { 33 | uut.activate(mockCanvas); 34 | assertTrue(uut.isActive()); 35 | } 36 | 37 | @Test 38 | void testThatDeactivatedToolIsNotActive() { 39 | uut.activate(mockCanvas); 40 | assertTrue(uut.isActive()); 41 | 42 | uut.deactivate(); 43 | assertFalse(uut.isActive()); 44 | } 45 | 46 | @Test 47 | void testThatActivationSetsSurfaceInteractionListener() { 48 | uut.activate(mockCanvas); 49 | Mockito.verify(mockCanvas).addSurfaceInteractionObserver(uut); 50 | } 51 | 52 | @Test 53 | void testThatActivationSetsCursor() { 54 | uut.activate(mockCanvas); 55 | Mockito.verify(mockCursorManager).setToolCursor(any(), eq(mockCanvas)); 56 | } 57 | 58 | @Test 59 | void testThatSetCursorDelegatesToCursorManager() { 60 | uut.activate(mockCanvas); 61 | uut.setToolCursor(mockCursor); 62 | 63 | Mockito.verify(mockCursorManager).setToolCursor(mockCursor, mockCanvas); 64 | } 65 | 66 | @Test 67 | void testThatGetCursorDelegatesToCursorManager() { 68 | Mockito.when(mockCursorManager.getToolCursor()).thenReturn(mockCursor); 69 | assertEquals(mockCursor, uut.getToolCursor()); 70 | Mockito.verify(mockCursorManager).getToolCursor(); 71 | } 72 | 73 | @Test 74 | void testThatGetCanvasThrowsWhenNotActive() { 75 | assertThrows(IllegalStateException.class, () -> uut.getCanvas()); 76 | } 77 | 78 | @Test 79 | void testThatCanvasIsReturnedWhenActive() { 80 | uut.activate(mockCanvas); 81 | assertEquals(mockCanvas, uut.getCanvas()); 82 | } 83 | 84 | @Test 85 | void testThatInjectedAttributesAreReturned() { 86 | assertEquals(mockToolAttributes, uut.getAttributes()); 87 | } 88 | 89 | @Test 90 | void testThatProperlyConfiguredScratchIsReturned() { 91 | 92 | uut.activate(mockCanvas); 93 | assertEquals(mockScratch, uut.getScratch()); 94 | 95 | Mockito.verify(mockAddScratch).setAntialiasingMode(expectedInterpolation); 96 | Mockito.verify(mockRemoveScratch).setAntialiasingMode(expectedInterpolation); 97 | } 98 | 99 | } -------------------------------------------------------------------------------- /src/test/java/com/defano/jmonet/tools/base/ColorMatcher.java: -------------------------------------------------------------------------------- 1 | package com.defano.jmonet.tools.base; 2 | 3 | import org.mockito.ArgumentMatcher; 4 | 5 | import java.awt.*; 6 | 7 | public class ColorMatcher extends ArgumentMatcher { 8 | 9 | private final Color c1; 10 | private boolean ignoreAlpha = false; 11 | 12 | public ColorMatcher(Color c1) { 13 | this.c1 = c1; 14 | } 15 | 16 | public ColorMatcher ignoringAlpha() { 17 | this.ignoreAlpha = true; 18 | return this; 19 | } 20 | 21 | @Override 22 | public boolean matches(Object o) { 23 | 24 | if (o instanceof Color) { 25 | Color c2 = (Color) o; 26 | 27 | if (ignoreAlpha) { 28 | return c1.getRed() == c2.getRed() && 29 | c1.getGreen() == c2.getGreen() && 30 | c1.getBlue() == c2.getBlue(); 31 | } else { 32 | return c1.getRed() == c2.getRed() && 33 | c1.getGreen() == c2.getGreen() && 34 | c1.getBlue() == c2.getBlue() && 35 | c1.getAlpha() == c2.getAlpha(); 36 | } 37 | } 38 | 39 | return false; 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/test/java/com/defano/jmonet/tools/base/CursorMatcher.java: -------------------------------------------------------------------------------- 1 | package com.defano.jmonet.tools.base; 2 | 3 | import org.mockito.ArgumentMatcher; 4 | 5 | import java.awt.*; 6 | 7 | public class CursorMatcher extends ArgumentMatcher { 8 | 9 | private final Cursor c1; 10 | 11 | public CursorMatcher(Cursor c1) { 12 | this.c1 = c1; 13 | } 14 | 15 | @Override 16 | public boolean matches(Object o) { 17 | 18 | if (o instanceof Cursor) { 19 | Cursor c2 = (Cursor) o; 20 | 21 | return c2.getName().equals(c1.getName()); 22 | } 23 | 24 | return false; 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/test/java/com/defano/jmonet/tools/base/MockitoTest.java: -------------------------------------------------------------------------------- 1 | package com.defano.jmonet.tools.base; 2 | 3 | import org.junit.jupiter.api.BeforeEach; 4 | import org.mockito.MockitoAnnotations; 5 | 6 | public abstract class MockitoTest { 7 | 8 | @BeforeEach 9 | protected void setup() { 10 | MockitoAnnotations.initMocks(this); 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /src/test/java/com/defano/jmonet/tools/base/MockitoToolTest.java: -------------------------------------------------------------------------------- 1 | package com.defano.jmonet.tools.base; 2 | 3 | import com.defano.jmonet.canvas.JMonetCanvas; 4 | import com.defano.jmonet.canvas.Scratch; 5 | import com.defano.jmonet.context.GraphicsContext; 6 | import com.defano.jmonet.model.Interpolation; 7 | import com.defano.jmonet.tools.attributes.ToolAttributes; 8 | import com.defano.jmonet.tools.cursors.CursorManager; 9 | import com.defano.jmonet.transform.image.FloodFillTransform; 10 | import com.google.inject.AbstractModule; 11 | import com.google.inject.Guice; 12 | import org.mockito.*; 13 | 14 | import java.awt.*; 15 | import java.awt.image.BufferedImage; 16 | 17 | import static org.mockito.Matchers.*; 18 | 19 | public abstract class MockitoToolTest extends MockitoTest { 20 | 21 | protected T uut; 22 | 23 | @Mock(answer=Answers.RETURNS_DEEP_STUBS) protected Scratch mockScratch; 24 | @Mock(answer=Answers.RETURNS_DEEP_STUBS) protected GraphicsContext mockAddScratchGraphics; 25 | @Mock(answer=Answers.RETURNS_DEEP_STUBS) protected GraphicsContext mockRemoveScratchGraphics; 26 | @Mock(answer=Answers.RETURNS_DEEP_STUBS) protected JMonetCanvas mockCanvas; 27 | @Mock(answer=Answers.RETURNS_DEEP_STUBS) protected BufferedImage mockCanvasImage; 28 | @Mock(answer=Answers.RETURNS_DEEP_STUBS) protected ToolAttributes mockToolAttributes; 29 | @Mock(answer=Answers.RETURNS_DEEP_STUBS) protected CursorManager mockCursorManager; 30 | @Mock(answer=Answers.RETURNS_DEEP_STUBS) protected FloodFillTransform mockFloodFillTransform; 31 | 32 | public void initialize(T uut) { 33 | MockitoAnnotations.initMocks(this); 34 | 35 | this.uut = uut; 36 | Guice.createInjector(new MockToolAssembly()).injectMembers(uut); 37 | this.uut.activate(mockCanvas); 38 | 39 | Mockito.when(mockCanvas.getCanvasImage()).thenReturn(mockCanvasImage); 40 | Mockito.when(mockCanvas.getScratch()).thenReturn(mockScratch); 41 | 42 | // Provide mock add scratch 43 | Mockito.when(mockScratch.getAddScratchGraphics(any(), any(), any())).thenReturn(mockAddScratchGraphics); 44 | Mockito.when(mockScratch.getAddScratchGraphics(any(), any())).thenReturn(mockAddScratchGraphics); 45 | 46 | // Provide mock remove scratch 47 | Mockito.when(mockScratch.getRemoveScratchGraphics(any(), any(), any())).thenReturn(mockRemoveScratchGraphics); 48 | Mockito.when(mockScratch.getRemoveScratchGraphics(any(), any())).thenReturn(mockRemoveScratchGraphics); 49 | Mockito.when(mockToolAttributes.getAntiAliasing()).thenReturn(Interpolation.NONE); 50 | } 51 | 52 | protected static ShapeMatcher matchesShape(T t) { 53 | return new ShapeMatcher<>(t); 54 | } 55 | 56 | protected static CursorMatcher matchesCursor(Cursor c) { 57 | return new CursorMatcher(c); 58 | } 59 | 60 | protected static ColorMatcher matchesColor(Color c) { 61 | return new ColorMatcher(c); 62 | } 63 | 64 | private class MockToolAssembly extends AbstractModule { 65 | 66 | @Override 67 | protected void configure() { 68 | bind(ToolAttributes.class).toInstance(mockToolAttributes); 69 | bind(CursorManager.class).toInstance(mockCursorManager); 70 | bind(FloodFillTransform.class).toInstance(mockFloodFillTransform); 71 | } 72 | } 73 | 74 | 75 | } 76 | -------------------------------------------------------------------------------- /src/test/java/com/defano/jmonet/tools/base/PathToolTest.java: -------------------------------------------------------------------------------- 1 | package com.defano.jmonet.tools.base; 2 | 3 | import org.junit.jupiter.api.BeforeEach; 4 | import org.junit.jupiter.api.Test; 5 | import org.mockito.Mock; 6 | import org.mockito.Mockito; 7 | 8 | import java.awt.*; 9 | import java.awt.event.MouseEvent; 10 | 11 | import static org.junit.jupiter.api.Assertions.*; 12 | 13 | class PathToolTest extends BaseToolTest { 14 | 15 | @Mock private MouseEvent mockEvent; 16 | @Mock private Point mockPoint; 17 | @Mock private PathToolDelegate mockDelegate; 18 | @Mock private Stroke mockStroke; 19 | @Mock private Paint mockPaint; 20 | @Mock private Rectangle mockRegion; 21 | 22 | @BeforeEach 23 | protected void setup() { 24 | super.setup(new PathTool(null)); 25 | } 26 | 27 | @Test 28 | void testThatCrosshairIsDefaultCursor() { 29 | assertTrue(new CursorMatcher(new Cursor(Cursor.CROSSHAIR_CURSOR)).matches(uut.getDefaultCursor())); 30 | } 31 | 32 | @Test 33 | void testThatMouseMovedUpdatesCursor() { 34 | Mockito.when(mockCursorManager.getToolCursor()).thenReturn(mockCursor); 35 | uut.activate(mockCanvas); 36 | uut.mouseMoved(mockEvent, mockPoint); 37 | Mockito.verify(mockCursorManager).setToolCursor(mockCursor, mockCanvas); 38 | } 39 | 40 | @Test 41 | void testThatMousePressedPaintsInitialPoint() { 42 | uut.activate(mockCanvas); 43 | uut.setDelegate(mockDelegate); 44 | 45 | Mockito.when(mockToolAttributes.getStroke()).thenReturn( mockStroke); 46 | Mockito.when(mockToolAttributes.getStrokePaint()).thenReturn(mockPaint); 47 | Mockito.when(mockScratch.getDirtyRegion()).thenReturn(mockRegion); 48 | 49 | uut.mousePressed(mockEvent, mockPoint); 50 | 51 | Mockito.verify(mockScratch).clear(); 52 | Mockito.verify(mockDelegate).startPath(mockScratch, mockStroke, mockPaint, mockPoint); 53 | Mockito.verify(mockCanvas).repaint(mockRegion); 54 | } 55 | 56 | @Test 57 | void testThatMouseDraggedPaintsPath() { 58 | Point start = new Point(20, 20); 59 | Point end = new Point(40, 30); 60 | 61 | Mockito.when(mockToolAttributes.getStroke()).thenReturn( mockStroke); 62 | Mockito.when(mockToolAttributes.getStrokePaint()).thenReturn(mockPaint); 63 | Mockito.when(mockScratch.getDirtyRegion()).thenReturn(mockRegion); 64 | 65 | uut.activate(mockCanvas); 66 | uut.setDelegate(mockDelegate); 67 | uut.mousePressed(mockEvent, start); 68 | uut.mouseDragged(mockEvent, end); 69 | 70 | Mockito.inOrder(mockDelegate, mockCanvas); 71 | 72 | Mockito.verify(mockDelegate).addPoint(mockScratch, mockStroke, mockPaint, start, end); 73 | Mockito.verify(mockCanvas, Mockito.times(2)).repaint(mockRegion); 74 | 75 | } 76 | } -------------------------------------------------------------------------------- /src/test/java/com/defano/jmonet/tools/base/ShapeMatcher.java: -------------------------------------------------------------------------------- 1 | package com.defano.jmonet.tools.base; 2 | 3 | import org.mockito.ArgumentMatcher; 4 | 5 | import java.awt.*; 6 | import java.awt.geom.PathIterator; 7 | import java.util.ArrayList; 8 | import java.util.List; 9 | 10 | public class ShapeMatcher extends ArgumentMatcher { 11 | 12 | private final Shape s1; 13 | private final double precision; 14 | 15 | public ShapeMatcher(Shape shape) { 16 | this(shape, .0001); 17 | } 18 | 19 | public ShapeMatcher(Shape shape, double precision) { 20 | this.s1 = shape; 21 | this.precision = precision; 22 | } 23 | 24 | @Override 25 | public boolean matches(Object o) { 26 | if (o instanceof Shape) { 27 | Shape s2 = (Shape) o; 28 | 29 | List s1Points = disassemble(s1); 30 | List s2Points = disassemble(s2); 31 | 32 | return isEquivalent(s1Points, s2Points, precision); 33 | } 34 | 35 | return false; 36 | } 37 | 38 | private boolean isEquivalent(List l1, List l2, double precision) { 39 | 40 | if (l1.size() != l2.size()) { 41 | return false; 42 | } 43 | 44 | for (int i = 0; i < l1.size(); i++) { 45 | double[] d1 = l1.get(i); 46 | double[] d2 = l2.get(i); 47 | 48 | if (d1.length != d2.length) { 49 | return false; 50 | } 51 | 52 | for (int j = 0; j < d1.length; j++) { 53 | if (Math.abs(d1[j] - d2[j]) > precision) { 54 | return false; 55 | } 56 | } 57 | } 58 | 59 | return true; 60 | } 61 | 62 | private List disassemble(Shape s) { 63 | ArrayList s1Points = new ArrayList<>(); 64 | double[] coords = new double[6]; 65 | 66 | for (PathIterator pi = s.getPathIterator(null); !pi.isDone(); pi.next()) { 67 | int type = pi.currentSegment(coords); 68 | s1Points.add(new double[] {type, coords[0], coords[1]}); 69 | } 70 | 71 | return s1Points; 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /src/test/java/com/defano/jmonet/tools/util/MathUtilsTest.java: -------------------------------------------------------------------------------- 1 | package com.defano.jmonet.tools.util; 2 | 3 | import org.junit.jupiter.api.Test; 4 | 5 | import static org.junit.jupiter.api.Assertions.*; 6 | 7 | class MathUtilsTest { 8 | 9 | @Test 10 | void testNearestFloor() { 11 | assertEquals(30, MathUtils.nearestFloor(30, 10)); 12 | assertEquals(30, MathUtils.nearestFloor(31, 10)); 13 | assertEquals(30, MathUtils.nearestFloor(35, 10)); 14 | assertEquals(40, MathUtils.nearestFloor(40, 10)); 15 | } 16 | 17 | @Test 18 | void testThatNearestFloorIgnoresZero() { 19 | assertEquals(13, MathUtils.nearestFloor(13, 0)); 20 | assertEquals(13, MathUtils.nearestFloor(13, 1)); 21 | } 22 | 23 | @Test 24 | void testThatNearestFloorHandlesNegatives() { 25 | assertEquals(-10, MathUtils.nearestFloor(-15, 10)); 26 | assertEquals(-10, MathUtils.nearestFloor(-15, -10)); 27 | } 28 | } -------------------------------------------------------------------------------- /src/test/java/com/defano/jmonet/transform/pixel/BrightnessPixelTransformTest.java: -------------------------------------------------------------------------------- 1 | package com.defano.jmonet.transform.pixel; 2 | 3 | import org.junit.jupiter.api.Test; 4 | 5 | import java.awt.*; 6 | 7 | import static org.junit.jupiter.api.Assertions.*; 8 | 9 | class BrightnessPixelTransformTest { 10 | 11 | @Test 12 | void testThatZeroDeltaLeavesColorUnchanged() { 13 | assertEquals(Color.black.getRGB(), 14 | new BrightnessPixelTransform(0).apply(Color.black.getRGB())); 15 | } 16 | 17 | @Test 18 | void testThatUnsaturatedColorIsTransformed() { 19 | final int delta = 10; 20 | 21 | assertEquals(new Color( 22 | Color.black.getRed() + delta, 23 | Color.black.getGreen() + delta, 24 | Color.black.getBlue() + delta 25 | ).getRGB(), new BrightnessPixelTransform(delta).apply(Color.black.getRGB())); 26 | } 27 | 28 | @Test 29 | void testThatFullyWhiteColorRemainUnchangedWhenBrightnessAdded() { 30 | assertEquals(Color.white.getRGB(), new BrightnessPixelTransform(10).apply(Color.white.getRGB())); 31 | } 32 | 33 | @Test 34 | void testThatSaturatedColorComponentsAreNotModified() { 35 | Color c = new Color(255, 100, 255, 20); 36 | final int delta = 20; 37 | 38 | assertEquals(new Color( 39 | c.getRed(), 40 | c.getGreen() + delta, 41 | c.getBlue(), 42 | c.getAlpha() 43 | ).getRGB(), new BrightnessPixelTransform(delta).apply(c.getRGB())); 44 | } 45 | } -------------------------------------------------------------------------------- /src/test/java/com/defano/jmonet/transform/pixel/InvertPixelTransformTest.java: -------------------------------------------------------------------------------- 1 | package com.defano.jmonet.transform.pixel; 2 | 3 | import org.junit.jupiter.api.Test; 4 | 5 | import java.awt.*; 6 | 7 | import static org.junit.jupiter.api.Assertions.*; 8 | 9 | class InvertPixelTransformTest { 10 | 11 | @Test 12 | void testThatColorWithoutAlphaIsInverted() { 13 | assertEquals(Color.white.getRGB(), new InvertPixelTransform().apply(Color.black.getRGB())); 14 | } 15 | 16 | @Test 17 | void testThatColorWithAlphaIsInvertedWithNoChangeToAlpha() { 18 | Color transBlack = new Color(Color.black.getRed(), Color.black.getGreen(), Color.black.getBlue(), 20); 19 | Color transWhite = new Color(Color.white.getRed(), Color.white.getGreen(), Color.white.getBlue(), 20); 20 | 21 | assertEquals(transWhite.getRGB(), new InvertPixelTransform().apply(transBlack.getRGB())); 22 | } 23 | } -------------------------------------------------------------------------------- /src/test/java/com/defano/jmonet/transform/pixel/RemoveAlphaPixelTransformTest.java: -------------------------------------------------------------------------------- 1 | package com.defano.jmonet.transform.pixel; 2 | 3 | import org.junit.jupiter.api.Test; 4 | 5 | import java.awt.*; 6 | 7 | import static org.junit.jupiter.api.Assertions.assertEquals; 8 | 9 | class RemoveAlphaPixelTransformTest { 10 | 11 | @Test 12 | void testThatTranslucentColorIsMadeTransparent() { 13 | Color orig = new Color(Color.ORANGE.getRed(), Color.orange.getGreen(), Color.orange.getBlue(), 127); 14 | Color transformed = new Color(orig.getRed(), orig.getGreen(), orig.getBlue(), 0); 15 | 16 | assertEquals(transformed.getRGB(), new RemoveAlphaPixelTransform(true).apply(orig.getRGB())); 17 | } 18 | 19 | @Test 20 | void testThatTranslucentColorIsMadeOpaque() { 21 | Color orig = new Color(Color.ORANGE.getRed(), Color.orange.getGreen(), Color.orange.getBlue(), 127); 22 | Color transformed = new Color(orig.getRed(), orig.getGreen(), orig.getBlue(), 255); 23 | 24 | assertEquals(transformed.getRGB(), new RemoveAlphaPixelTransform(false).apply(orig.getRGB())); 25 | } 26 | 27 | @Test 28 | void testThatOpaqueColorIsUnchanged() { 29 | Color orig = new Color(Color.ORANGE.getRed(), Color.orange.getGreen(), Color.orange.getBlue(), 255); 30 | 31 | assertEquals(orig.getRGB(), new RemoveAlphaPixelTransform(false).apply(orig.getRGB())); 32 | assertEquals(orig.getRGB(), new RemoveAlphaPixelTransform(true).apply(orig.getRGB())); 33 | } 34 | 35 | @Test 36 | void testThatTransparentColorIsUnchanged() { 37 | Color orig = new Color(Color.ORANGE.getRed(), Color.orange.getGreen(), Color.orange.getBlue(), 0); 38 | 39 | assertEquals(orig.getRGB(), new RemoveAlphaPixelTransform(false).apply(orig.getRGB())); 40 | assertEquals(orig.getRGB(), new RemoveAlphaPixelTransform(true).apply(orig.getRGB())); 41 | } 42 | 43 | } --------------------------------------------------------------------------------