├── .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
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
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
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 | ArrayListActionMap
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 extends Tool> toolClass;
36 |
37 | PaintToolType(Class extends Tool> clazz) {
38 | this.toolClass = clazz;
39 | }
40 |
41 | public Class extends Tool> 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