/*
* @(#)CharcoalEffect.java
*
* $Date: 2014-04-06 05:02:15 +0200 (V, 06 ápr. 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.GeneralPathWriter;
import com.bric.geom.MeasuredShape;
import com.bric.geom.PathWriter;
import net.jafama.FastMath;
import java.awt.Shape;
import java.awt.geom.GeneralPath;
import java.awt.geom.Point2D;
import java.util.Random;
/**
* This applies a charcoal effect to a shape.
* <p>
* This basically takes a shape and applies several "cracks" of varying
* depth at a fixed angle.
* <p>
* (The implementation is pretty simple, and there are a few interesting
* code snippets commented out that change how this renders.)
*/
public class CharcoalEffect {
public final PathWriter writer;
public final int seed;
public final float size;
public final float angle;
public final float maxDepth;
/**
* Creates a new <code>CharcoalEffect</code>.
*
* @param dest the destination to write the new shape to.
* @param size the size of the cracks. This is float from [0,1], where
* "0" means "no crack depth" and "1" means "high depth". The depth is
* always relative to the <i>possible</i> depth.
* @param angle the angle of the cracks.
* @param randomSeed the random seed.
*/
public CharcoalEffect(PathWriter dest, float size, float angle, int randomSeed) {
this(dest, size, angle, randomSeed, Float.MAX_VALUE);
}
/**
* Creates a new <code>CharcoalEffect</code>.
*
* @param dest the destination to write the new shape to.
* @param size the size of the cracks. This is float from [0,1], where
* "0" means "no crack depth" and "1" means "high depth". The depth is
* always relative to the <i>possible</i> depth.
* @param angle the angle of the cracks.
* @param randomSeed the random seed.
* @param maxDepth this is the maximum crack depth. If this is zero, then no
* cracks will be added. If this is 5, then cracks will be at most 5 pixels.
* If you aren't sure what to make this value, use <code>Float.MAX_VALUE</code>.
*/
public CharcoalEffect(PathWriter dest, float size, float angle, int randomSeed, float maxDepth) {
if (size < 0 || size > 1) {
throw new IllegalArgumentException("size (" + size + ") must be between 0 and 1.");
}
writer = dest;
seed = randomSeed;
this.size = size;
this.angle = angle;
this.maxDepth = maxDepth;
}
/**
* Applies the <code>CharcoalEffect</code> to a shape.
*
* @param shape the shape to apply the effect to.
* @param size the size of the cracks. This is float from [0,1], where
* "0" means "no crack depth" and "1" means "high depth". The depth is
* always relative to the <i>possible</i> depth.
* @param angle the angle of the cracks.
* @param randomSeed the random seed.
* @param maxDepth this is the maximum crack depth. If this is zero, then no
* cracks will be added. If this is 5, then cracks will be at most 5 pixels.
* If you aren't sure what to make this value, use <code>Float.MAX_VALUE</code>.
* @return a new filtered path.
*/
public static GeneralPath filter(Shape shape, float size, float angle, int randomSeed, float maxDepth) {
GeneralPath path = new GeneralPath();
GeneralPathWriter writer = new GeneralPathWriter(path);
CharcoalEffect effect = new CharcoalEffect(writer, size, angle, randomSeed, maxDepth);
effect.write(shape);
return path;
}
/**
* Applies the <code>CharcoalEffect</code> to a shape.
*
* @param shape the shape to apply the effect to.
* @param size the size of the cracks. This is float from [0,1], where
* "0" means "no crack depth" and "1" means "high depth". The depth is
* always relative to the <i>possible</i> depth.
* @param angle the angle of the cracks.
* @param randomSeed the random seed.
* @return a new filtered path.
*/
public static GeneralPath filter(Shape shape, float size, float angle, int randomSeed) {
return filter(shape, size, angle, randomSeed, Float.MAX_VALUE);
}
/**
* Applies this effect to the shape provided.
*
* @param s the shape to write.
*/
public void write(Shape s) {
Random random = new Random(seed);
Point2D center = new Point2D.Float();
Point2D rightSide = new Point2D.Float();
MeasuredShape[] m = MeasuredShape.getSubpaths(s, .05f);
subpathIterator:
for (int a = 0; a < m.length; a++) {
float orig = m[a].getOriginalDistance();
float total = m[a].getClosedDistance();
float distance = 0;
float pendingGap = 0;
writer.moveTo(m[a].getMoveToX(), m[a].getMoveToY());
while (distance < orig) {
pendingGap += (.05f + .95f * random.nextFloat()) * 20 * (.05f + .95f * (1 - .9f * size));
if (distance + pendingGap >= orig) {
//we're overflowing:
float remaining = orig - distance;
if (remaining > 2) {
m[a].writeShape(distance / total, remaining / total, writer, false);
} else {
writer.closePath();
}
continue subpathIterator;
} else if (distance + pendingGap < orig) {
//see if we can add a crack here:
m[a].getPoint(distance + pendingGap, center);
/** Don't add a crack if this point is completely inside the guiding shape.
* And don't trust shape.contains(x,y,width,height). Although that is in
* theory what we want, it doesn't always return correct results!
* This test isn't quite the same, but gets us what we need. And accurately:
*/
boolean addCrack = !(s.contains(center.getX() - .5, center.getY() - .5) &&
s.contains(center.getX() + .5, center.getY() - .5) &&
s.contains(center.getX() - .5, center.getY() + .5) &&
s.contains(center.getX() + .5, center.getY() + .5));
if (addCrack) {
for (int mult = -1; mult <= 1; mult += 2) { //try both the clockwise and the counterclockwise side
float width = .05f;
//yes, you can also try this:
//float angle = m[a].getTangentSlope(distance+pendingGap);
//or
//float angle = m[a].getTangentSlope(distance+pendingGap)-(float)Math.PI/4;
while (s.contains(center.getX() + width * FastMath.cos(angle + mult * Math.PI / 2),
center.getY() + width * FastMath.sin(angle + mult * Math.PI / 2)) && width < maxDepth) {
width++;
}
//or to make something spikey, try:
//if(guide.contains(center.getX()+.1*FastMath.cos(angle-mult*Math.PI/2),
// center.getY()+.1*FastMath.sin(angle-mult*Math.PI/2))) {
// width = random.nextFloat()*8+1;
//}
if (width > 1) {
//width will be > 1 when we're on the correct side AND we have
//a fleshy material to cut into
float crackWidth, depth;
//now define a constant to multiply the depth of the crack by
//when width is 5, multiply by 1. when width is 15, multiply by .5
float k = -.05f * width + 1.25f;
if (k > 1) {
k = 1; //cap at these values
}
if (k < .4f) {
k = .4f;
}
depth = width * k * (.5f + .5f * size) * (.25f + .75f * random.nextFloat());
crackWidth = depth * depth / 150;
if (crackWidth < 1f) {
crackWidth = 1f;
}
if (crackWidth > 2) {
crackWidth = 2;
}
if (distance + pendingGap - crackWidth / 2 > 0 && distance + pendingGap + crackWidth / 2 < orig) {
m[a].getPoint(distance + pendingGap + crackWidth / 2, rightSide);
m[a].writeShape(distance / total, (pendingGap - crackWidth / 2) / total, writer, false);
writer.lineTo(
(float) (center.getX() + depth * FastMath.cos(angle + mult * Math.PI / 2)),
(float) (center.getY() + depth * FastMath.sin(angle + mult * Math.PI / 2))
);
writer.lineTo((float) rightSide.getX(), (float) rightSide.getY());
distance += pendingGap + crackWidth / 2;
pendingGap = 0;
}
break;
}
}
}
}
}
writer.closePath();
}
}
}