/*
* Copyright 2017 Laszlo Balazs-Csiki
*
* This file is part of Pixelitor. Pixelitor is free software: you
* can redistribute it and/or modify it under the terms of the GNU
* General Public License, version 3 as published by the Free
* Software Foundation.
*
* Pixelitor is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with Pixelitor. If not, see <http://www.gnu.org/licenses/>.
*/
package pixelitor.filters;
import pixelitor.ThreadPool;
import pixelitor.filters.gui.ColorParam;
import pixelitor.filters.gui.ParamSet;
import pixelitor.filters.gui.RangeParam;
import pixelitor.filters.gui.ReseedNoiseFilterAction;
import pixelitor.filters.gui.ShowOriginal;
import pixelitor.utils.BasicProgressTracker;
import pixelitor.utils.ImageUtils;
import pixelitor.utils.ProgressTracker;
import java.awt.Color;
import java.awt.image.BufferedImage;
import java.util.Random;
import java.util.concurrent.Future;
import static java.awt.Color.BLACK;
import static java.awt.Color.WHITE;
import static pixelitor.filters.gui.ColorParam.OpacitySetting.USER_ONLY_OPACITY;
/**
* Renders value noise
*/
public class ValueNoise extends FilterWithParametrizedGUI {
public static final String NAME = "Value Noise";
private static final Random rand = new Random();
private static int r1;
private static int r2;
private static int r3;
static {
reseed();
}
private final RangeParam scale = new RangeParam("Zoom", 5, 100, 300);
private final RangeParam details = new RangeParam("Octaves (Details)", 1, 5, 8);
private final ColorParam color1 = new ColorParam("Color 1", BLACK, USER_ONLY_OPACITY);
private final ColorParam color2 = new ColorParam("Color 2", WHITE, USER_ONLY_OPACITY);
public ValueNoise() {
super(ShowOriginal.NO);
setParamSet(new ParamSet(
scale.withAdjustedRange(0.3),
details,
color1,
color2
).withAction(new ReseedNoiseFilterAction(e -> reseed())));
}
@Override
public BufferedImage doTransform(BufferedImage src, BufferedImage dest) {
int[] lookupTable = new int[256];
Color c1 = color1.getColor();
Color c2 = color2.getColor();
int[] colorArray1 = {c1.getAlpha(), c1.getRed(), c1.getGreen(), c1.getBlue()};
int[] colorArray2 = {c2.getAlpha(), c2.getRed(), c2.getGreen(), c2.getBlue()};
for (int i = 0, lookupTableLength = lookupTable.length; i < lookupTableLength; i++) {
lookupTable[i] = ImageUtils.lerpAndPremultiplyColorWithAlpha(i / 255.0f, colorArray1, colorArray2);
}
int[] destData = ImageUtils.getPixelsAsArray(dest);
int width = dest.getWidth();
int height = dest.getHeight();
float frequency = 1.0f / scale.getValueAsFloat();
float persistence = 0.6f;
float amplitude = 1.0f;
ProgressTracker pt = new BasicProgressTracker(NAME, height);
Future<?>[] futures = new Future[height];
for (int y = 0; y < height; y++) {
int finalY = y;
Runnable lineTask = () -> calculateLine(lookupTable, destData, width, frequency, persistence, amplitude, finalY);
futures[y] = ThreadPool.submit(lineTask);
}
ThreadPool.waitForFutures(futures, pt);
pt.finish();
return dest;
}
private void calculateLine(int[] lookupTable, int[] destData, int width, float frequency, float persistence, float amplitude, int y) {
for (int x = 0; x < width; x++) {
int octaves = details.getValue();
int noise = (int) (255 * generateValueNoise(x, y, octaves, frequency, persistence, amplitude));
int value = lookupTable[noise];
destData[x + y * width] = value;
}
}
/**
* Returns a float between 0 and 1
*/
@SuppressWarnings("WeakerAccess")
public static float generateValueNoise(int x, int y, int octaves, float frequency, float persistence, float amplitude) {
float total = 0.0f;
for (int lcv = 0; lcv < octaves; lcv++) {
total += smooth(x * frequency, y * frequency) * amplitude;
frequency *= 2;
amplitude *= persistence;
}
if (total < 0) {
total = 0.0f;
}
if (total > 1) {
total = 1.0f;
}
return total;
}
private static float smooth(float x, float y) {
float n1 = noise((int) x, (int) y);
float n2 = noise((int) x + 1, (int) y);
float n3 = noise((int) x, (int) y + 1);
float n4 = noise((int) x + 1, (int) y + 1);
float i1 = interpolate(n1, n2, x - (int) x);
float i2 = interpolate(n3, n4, x - (int) x);
return interpolate(i1, i2, y - (int) y);
}
public static void reseed() {
r1 = 1000 + rand.nextInt(90000);
r2 = 10000 + rand.nextInt(900000);
r3 = 100000 + rand.nextInt(1000000000);
}
private static float noise(int x, int y) {
int n = x + y * 57;
n = (n << 13) ^ n;
return (1.0f - ((n * (n * n * r1 + r2) + r3) & 0x7fffffff) / 1073741824.0f);
}
private static float interpolate(float x, float y, float a) {
// float val = (float) ((1 - FastMath.cos(a * Math.PI)) * 0.5);
// the smooth step is very similar but much faster than the cosine interpolation
// http://en.wikipedia.org/wiki/Smoothstep
// http://www.wolframalpha.com/input/?i=Plot[{3+*+a+*+a+-+2+*+a+*+a+*a%2C+%281+-+Cos[a+*+Pi]%29+*+0.5}%2C+{a%2C+0%2C+1}]
float val = a * a * (3 - 2 * a);
return x * (1 - val) + y * val;
}
public void setDetails(int newDetails) {
details.setValue(newDetails);
}
@Override
public boolean supportsGray() {
return false;
}
}