@@ -53,6 +53,9 @@ public class JHDotsHalftone extends ParametrizedFilter {
5353 private static final int GRID_TRIANGLE = 0 ;
5454 private static final int GRID_SQUARE = 1 ;
5555
56+ private static final double SQRT_2 = 1.4142135623730951 ;
57+ private static final double SQRT_3 = 1.7320508075688772 ;
58+
5659 private final RangeParam dotRadius = new RangeParam ("Dot Radius" , 1 , 10 , 100 );
5760 private final IntChoiceParam shapeParam = new IntChoiceParam ("Dot Shape" , new Item []{
5861 new Item ("Circle" , SHAPE_CIRCLE ),
@@ -109,27 +112,35 @@ public BufferedImage transform(BufferedImage src, BufferedImage dest) {
109112 return filter .filter (src , dest );
110113 }
111114
115+ /**
116+ * Creates a mask image from the clustered dot matrix.
117+ */
112118 private BufferedImage createMaskImage (BufferedImage src ) {
113- int matrixSize = 2 * dotRadius .getValue ();
114- int [][] matrix = genClusteredDotMatrix (
115- matrixSize , 0.5 , shapeParam .getValue ());
116- BufferedImage maskImage = ImageUtils .createImageWithSameCM (src , matrixSize , matrixSize );
119+ int maskSize = 2 * dotRadius .getValue ();
120+ int [][] matrix = genClusteredDotMatrix (maskSize , shapeParam .getValue ());
121+ BufferedImage maskImage = ImageUtils .createImageWithSameCM (src , maskSize , maskSize );
117122
118123 int [] maskPixels = ImageUtils .getPixels (maskImage );
119- for (int y = 0 ; y < matrixSize ; y ++) {
120- for (int x = 0 ; x < matrixSize ; x ++) {
124+ for (int y = 0 ; y < maskSize ; y ++) {
125+ for (int x = 0 ; x < maskSize ; x ++) {
121126 int threshold = matrix [x ][y ];
122- maskPixels [x + y * matrixSize ] = 0xFF_00_00_00 | threshold << 16 | threshold << 8 | threshold ;
127+ maskPixels [x + y * maskSize ] = 0xFF_00_00_00 | threshold << 16 | threshold << 8 | threshold ;
123128 }
124129 }
125130
126131 return maskImage ;
127132 }
128133
129- private static int [][] genClusteredDotMatrix (int matrixSize , double dotSize , int shape ) {
134+ /**
135+ * Creates a square matrix where dots are clustered together
136+ * to represent different intensity thresholds (priority orders).
137+ * As brightness increases, dots will appear in that order,
138+ * expanding outward, and growing into recognizable shapes.
139+ */
140+ private static int [][] genClusteredDotMatrix (int matrixSize , int shape ) {
130141 assert matrixSize % 2 == 0 : "matrixSize = " + matrixSize ;
131- assert dotSize > 0 && dotSize <= 1 : "dotSize = " + dotSize ;
132142
143+ // stores a pixel's coordinates and its distance from the center
133144 record MPoint (int x , int y , double dist ) {
134145 }
135146
@@ -149,54 +160,62 @@ record MPoint(int x, int y, double dist) {
149160 // sort the points by the distance to the center of the shape
150161 points .sort (Comparator .comparingDouble (p -> p .dist ));
151162
152- // assign threshold values
153- for (int i = 0 ; i < matrixSize * matrixSize ; i ++) {
163+ // assign 0-255 threshold values
164+ int total = matrixSize * matrixSize ;
165+ for (int i = 0 ; i < total ; i ++) {
154166 MPoint p = points .get (i );
155- matrix [p .x ][p .y ] = (int ) Math .round ((double ) i / ( matrixSize * matrixSize ) * 255 );
167+ matrix [p .x ][p .y ] = (int ) Math .round ((double ) i / total * 255 );
156168 }
157169
158170 return matrix ;
159171 }
160172
161173 private static double distanceToCenter (int shape , double dx , double dy ) {
162174 return switch (shape ) {
163- case SHAPE_CIRCLE -> Math .sqrt (dx * dx + dy * dy );
164- case SHAPE_SQUARE -> Math .max (Math .abs (dx ), Math .abs (dy )); // Manhattan distance
165- case SHAPE_DIAMOND -> Math .abs (dx ) + Math .abs (dy ); // Manhattan distance
175+ case SHAPE_CIRCLE -> Math .hypot (dx , dy );
176+ case SHAPE_SQUARE -> Math .max (Math .abs (dx ), Math .abs (dy ));
177+ case SHAPE_DIAMOND -> Math .abs (dx ) + Math .abs (dy );
166178 case SHAPE_CROSS -> Math .min (Math .abs (dx ), Math .abs (dy ));
167179 case SHAPE_X -> {
168- double distanceToFirstDiagonal = Math .abs (dx - dy ) / Math . sqrt ( 2 ) ; // Distance to y = x
169- double distanceToSecondDiagonal = Math .abs (dx + dy ) / Math . sqrt ( 2 ) ; // Distance to y = -x
180+ double distanceToFirstDiagonal = Math .abs (dx - dy ) / SQRT_2 ; // distance to y = x
181+ double distanceToSecondDiagonal = Math .abs (dx + dy ) / SQRT_2 ; // distance to y = -x
170182 yield Math .min (distanceToFirstDiagonal , distanceToSecondDiagonal );
171183 }
172- case SHAPE_TRIANGLE -> {
173- // Equilateral triangle pointing upwards
174- double a = 2.0 / Math .sqrt (3 ); // Scaling factor for unit triangle
175- double dist1 = (Math .sqrt (3 ) * dx - dy ) / 2 ;
176- double dist2 = (-Math .sqrt (3 ) * dx - dy ) / 2 ;
184+ case SHAPE_TRIANGLE -> { // equilateral triangle pointing upwards
185+ // distances to the triangle’s edges
186+ double dist1 = (SQRT_3 * dx - dy ) / 2 ;
187+ double dist2 = (-SQRT_3 * dx - dy ) / 2 ;
177188 double dist3 = dy ;
178189
179- yield Math .max (Math .max (dist1 , dist2 ), dist3 ) / a ;
190+ double scale = 2.0 / SQRT_3 ; // scaling factor for unit triangle
191+ yield Math .max (Math .max (dist1 , dist2 ), dist3 ) / scale ;
180192 }
181193 case SHAPE_HEXAGON -> {
182- // Distance to center of a regular hexagon
183- double q = (Math . sqrt ( 3 ) / 3 * dx - 1.0 / 3 * dy );
194+ // uses axial coordinates to calculate distance from the center of a regular hexagon
195+ double q = (SQRT_3 / 3 * dx - 1.0 / 3 * dy );
184196 double r = (2.0 / 3 * dy );
185197 double s = -q - r ;
186198 yield (Math .abs (q ) + Math .abs (r ) + Math .abs (s )) / 2 ;
187199 }
188200 case SHAPE_OCTAGON -> {
189201 double absX = Math .abs (dx );
190202 double absY = Math .abs (dy );
191- yield Math .max (absX , absY ) + (Math . sqrt ( 2 ) - 1 ) * Math .min (absX , absY );
203+ yield Math .max (absX , absY ) + (SQRT_2 - 1 ) * Math .min (absX , absY );
192204 }
193205 case SHAPE_STAR -> {
194- double angle = Math .atan2 (dy , dx ) + 3 * Math .PI / 2 ;
195- double radius = Math .sqrt (dx * dx + dy * dy );
206+ // uses polar coordinates to create a 5-pointed star
207+ double angle = Math .atan2 (dy , dx ) + 3 * Math .PI / 2 ; // orient the star to point up
208+ double radius = Math .hypot (dx , dy );
209+ // modulates the radius based on the angle to form the star's points
196210 double modifiedRadius = radius * (1 + 0.25 * Math .cos (5 * angle ));
197211 yield modifiedRadius ;
198212 }
199213 default -> throw new IllegalArgumentException ("Invalid shape: " + shape );
200214 };
201215 }
216+
217+ @ Override
218+ public boolean supportsGray () {
219+ return false ;
220+ }
202221}
0 commit comments