/*
* @(#)BristleStroke.java
*
* $Date: 2014-03-13 09:15:48 +0100 (Cs, 13 márc. 2014) $
*
* Copyright (c) 2011 by Jeremy Wood.
* All rights reserved.
*
* The copyright of this software is owned by Jeremy Wood.
* You may not use, copy or modify this software, except in
* accordance with the license agreement you entered into with
* Jeremy Wood. For details see accompanying license terms.
*
* This software is probably, but not necessarily, discussed here:
* https://javagraphics.java.net/
*
* That site should also contain the most recent official version
* of this software. (See the SVN repository for more details.)
*/
package com.bric.awt;
import com.bric.geom.MeasuredShape;
import net.jafama.FastMath;
import java.awt.Shape;
import java.awt.Stroke;
import java.awt.geom.GeneralPath;
import java.awt.geom.Point2D;
import java.util.Random;
/**
* This <code>Stroke</code> that resembles a bristle.
* <P>More specifically: this stroke splatters tiny triangles and dots over a path.
*/
public class BristleStroke implements Stroke {
private static final int SHAPE_TRIANGLE = 0;
private static final int SHAPE_SQUARE = 1;
private static final int SHAPE_STAR = 2;
private static final int SHAPE_TRIANGLE_OR_SQUARE = 3;
/**
* (I experimented with a few different shapes, but
* decided in the end that a simple mix of squares
* and triangles is sufficient.)
*/
private final int shape = SHAPE_TRIANGLE_OR_SQUARE;
public final float width;
public final float thickness;
private final int layers;
private final long randomSeed;
private final float grain;
private final float spacing;
/**
* Creates a new BristleStroke.
* <P>This constructor always uses a random seed of zero.
*
* @param width the width (in pixels) of this stroke.
* @param thickness a float between zero and one indicating how
* "thick" this stroke should be. (1 = "very thick", and 0 = "very thin")
*/
public BristleStroke(float width, float thickness) {
this(width, thickness, 0);
}
/**
* Creates a new BristleStroke.
*
* @param width the width (in pixels) of this stroke.
* @param thickness a float between zero and one indicating how
* "thick" this stroke should be. (1 = "very thick", and 0 = "very thin")
* @param randomSeed the random seed for this stroke.
*/
public BristleStroke(float width, float thickness, long randomSeed) {
if (width <= 0) {
throw new IllegalArgumentException("the width (" + width + ") must be positive");
}
if (thickness < 0) {
throw new IllegalArgumentException("the thickness (" + thickness + ") must be greater than 0");
}
this.width = width;
this.thickness = thickness;
this.grain = getGrain(width, thickness);
this.spacing = .5f + .5f * thickness;
this.randomSeed = randomSeed;
int l = (int) ((1 + 2 * thickness) * width) + 10;
if (l > 20) {
l = 20;
}
this.layers = 20;
}
private static float getGrain(float width, float thickness) {
double k = width;
if (width > 1) {
k = Math.pow(width, .5f);
if (k > 4) {
k = 4;
}
return (float) (k * (.75 + .25 * thickness));
} else {
return
Math.max(width, .1f);
}
}
/**
* @return the random seed used in this stroke
*/
public long getRandomSeed() {
return randomSeed;
}
/**
* @return the thickness of this stroke (a float between zero and one).
*/
public float getThickness() {
return thickness;
}
/**
* @return the width (in pixels) of this stroke.
*/
public float getWidth() {
return width;
}
public Shape createStrokedShape(Shape p) {
GeneralPath path = new GeneralPath();
Random r = new Random(randomSeed);
MeasuredShape[] paths = MeasuredShape.getSubpaths(p);
for (int a = 0; a < layers; a++) {
float k1 = ((float) a) / ((float) (layers - 1));
float k2 = (k1 - .5f) * 2; //range from [-1,1]
float k3 = thickness;
float minGapDistance = (4 + 10 * k3) / (1 + 9 * spacing);
float maxGapDistance = (40 + 10 * k3) / (1 + 9 * spacing);
Point2D p2 = new Point2D.Float();
float x, y;
for (int b = 0; b < paths.length; b++) {
r.setSeed(randomSeed + 1000 * a + 10000 * b);
float d = r.nextFloat() * (maxGapDistance - minGapDistance) + minGapDistance * (1 + 20 * (1 - thickness) * Math.abs(k2 * k2));
while (d < paths[b].getOriginalDistance()) {
float gapDistance = r.nextFloat() * (maxGapDistance - minGapDistance) + minGapDistance * (1 + 20 * (1 - thickness) * Math.abs(k2 * k2));
paths[b].getPoint(d, p2);
float angle = paths[b].getTangentSlope(d);
float dx = (float) (k2 * width * FastMath.cos(angle + Math.PI / 2) / 2.0);
float dy = (float) (k2 * width * FastMath.sin(angle + Math.PI / 2) / 2.0);
p2.setLocation(p2.getX() + dx, p2.getY() + dy);
x = (float) p2.getX();
y = (float) p2.getY();
float rotation = r.nextFloat() * 2 * 3.145f;
int thisShape = shape;
if (thisShape == SHAPE_TRIANGLE_OR_SQUARE) {
thisShape = r.nextInt(2);
}
if (thisShape == SHAPE_TRIANGLE) {
path.moveTo((float) (x + grain / 2.0 * FastMath.cos(rotation + 2 * Math.PI / 3)),
(float) (y + grain / 2.0 * FastMath.sin(rotation + 2 * Math.PI / 3)));
path.lineTo((float) (x + grain / 2.0 * FastMath.cos(rotation + 4 * Math.PI / 3)),
(float) (y + grain / 2.0 * FastMath.sin(rotation + 4 * Math.PI / 3)));
path.lineTo((float) (x + grain / 2.0 * FastMath.cos(rotation)),
(float) (y + grain / 2.0 * FastMath.sin(rotation)));
path.closePath();
} else if (thisShape == SHAPE_SQUARE) {
path.moveTo((float) (x + grain / 2.0 * FastMath.cos(rotation + 2 * Math.PI / 4)),
(float) (y + grain / 2.0 * FastMath.sin(rotation + 2 * Math.PI / 4)));
path.lineTo((float) (x + grain / 2.0 * FastMath.cos(rotation + 4 * Math.PI / 4)),
(float) (y + grain / 2.0 * FastMath.sin(rotation + 4 * Math.PI / 4)));
path.lineTo((float) (x + grain / 2.0 * FastMath.cos(rotation + 6 * Math.PI / 4)),
(float) (y + grain / 2.0 * FastMath.sin(rotation + 6 * Math.PI / 4)));
path.lineTo((float) (x + grain / 2.0 * FastMath.cos(rotation)),
(float) (y + grain / 2.0 * FastMath.sin(rotation)));
path.closePath();
} else if (thisShape == SHAPE_STAR) {
path.moveTo((float) (x + grain / (6.0 + 2 - 2 * thickness) * FastMath.cos(rotation)),
(float) (y + grain / (6.0 + 2 - 2 * thickness) * FastMath.sin(rotation)));
path.lineTo((float) (x + grain / 2.0 * FastMath.cos(rotation + 2 * Math.PI / 8.0)),
(float) (y + grain / 2.0 * FastMath.sin(rotation + 2 * Math.PI / 8.0)));
path.lineTo((float) (x + grain / (6.0 + 2 - 2 * thickness) * FastMath.cos(rotation + Math.PI / 2)),
(float) (y + grain / (6.0 + 2 - 2 * thickness) * FastMath.sin(rotation + Math.PI / 2)));
path.lineTo((float) (x + grain / 2.0 * FastMath.cos(rotation + Math.PI / 2 + 2 * Math.PI / 8.0)),
(float) (y + grain / 2.0 * FastMath.sin(rotation + Math.PI / 2 + 2 * Math.PI / 8.0)));
path.lineTo((float) (x + grain / (6.0 + 2 - 2 * thickness) * FastMath.cos(rotation + Math.PI)),
(float) (y + grain / (6.0 + 2 - 2 * thickness) * FastMath.sin(rotation + Math.PI)));
path.lineTo((float) (x + grain / 2.0 * FastMath.cos(rotation + Math.PI + 2 * Math.PI / 8.0)),
(float) (y + grain / 2.0 * FastMath.sin(rotation + Math.PI + 2 * Math.PI / 8.0)));
path.lineTo((float) (x + grain / (6.0 + 2 - 2 * thickness) * FastMath.cos(rotation + 3 * Math.PI / 2)),
(float) (y + grain / (6.0 + 2 - 2 * thickness) * FastMath.sin(rotation + 3 * Math.PI / 2)));
path.lineTo((float) (x + grain / 2.0 * FastMath.cos(rotation + 3 * Math.PI / 2 + 2 * Math.PI / 8.0)),
(float) (y + grain / 2.0 * FastMath.sin(rotation + 3 * Math.PI / 2 + 2 * Math.PI / 8.0)));
}
d = d + gapDistance;
}
}
}
return path;
}
}