Skip to content

Commit

Permalink
nonlinear transform in the Rose filter
Browse files Browse the repository at this point in the history
  • Loading branch information
lbalazscs committed Dec 15, 2024
1 parent 0f1d610 commit 2c124d3
Show file tree
Hide file tree
Showing 4 changed files with 252 additions and 199 deletions.
14 changes: 6 additions & 8 deletions src/main/java/pixelitor/filters/CurveFilter.java
Original file line number Diff line number Diff line change
Expand Up @@ -27,9 +27,8 @@
import pixelitor.filters.painters.AreaEffects;
import pixelitor.io.FileIO;
import pixelitor.utils.ImageUtils;
import pixelitor.utils.NonlinTransform;
import pixelitor.utils.Shapes;
import pixelitor.utils.Shapes.NonlinTransform;
import pixelitor.utils.Shapes.PointMapper;

import java.awt.*;
import java.awt.geom.AffineTransform;
Expand All @@ -48,7 +47,7 @@
import static pixelitor.colors.FgBgColors.getBGColor;
import static pixelitor.colors.FgBgColors.getFGColor;
import static pixelitor.filters.gui.RandomizePolicy.IGNORE_RANDOMIZE;
import static pixelitor.utils.Shapes.NonlinTransform.NONE;
import static pixelitor.utils.NonlinTransform.NONE;

/**
* Abstract superclass for the "Render/Curves" filters.
Expand Down Expand Up @@ -152,12 +151,11 @@ public BufferedImage transform(BufferedImage src, BufferedImage dest) {
return dest;
}

NonlinTransform transform = nonlinType.getSelected();
if (transform != NONE) {
NonlinTransform nonlin = nonlinType.getSelected();
if (nonlin != NONE) {
double amount = nonlinTuning.getValueAsDouble();
Point2D mapCenter = center.getAbsolutePoint(src);
PointMapper mapper = transform.createMapper(mapCenter, amount, srcWidth, srcHeight);
shape = Shapes.transformShape(shape, mapper);
Point2D pivotPoint = center.getAbsolutePoint(src);
shape = nonlin.transform(shape, pivotPoint, amount, srcWidth, srcHeight);
}

double scaleX = scale.getPercentage(0);
Expand Down
19 changes: 16 additions & 3 deletions src/main/java/pixelitor/filters/Rose.java
Original file line number Diff line number Diff line change
Expand Up @@ -20,11 +20,13 @@
import pixelitor.colors.Colors;
import pixelitor.filters.gui.*;
import pixelitor.utils.ImageUtils;
import pixelitor.utils.NonlinTransform;

import java.awt.Graphics2D;
import java.awt.Shape;
import java.awt.geom.AffineTransform;
import java.awt.geom.Path2D;
import java.awt.geom.Point2D;
import java.awt.image.BufferedImage;
import java.io.Serial;

Expand All @@ -33,6 +35,7 @@
import static java.awt.RenderingHints.KEY_ANTIALIASING;
import static java.awt.RenderingHints.VALUE_ANTIALIAS_ON;
import static pixelitor.filters.gui.TransparencyPolicy.USER_ONLY_TRANSPARENCY;
import static pixelitor.utils.NonlinTransform.NONE;

/**
* The Render/Geometry/Rose filter, generating a polar rose curve.
Expand All @@ -50,18 +53,21 @@ public class Rose extends ParametrizedFilter {
private final ColorParam fgColor = new ColorParam("Foreground Color", WHITE, USER_ONLY_TRANSPARENCY);
private final GroupedRangeParam scale = new GroupedRangeParam("Scale (%)", 1, 100, 500);
private final AngleParam rotate = new AngleParam("Rotate", 0);
private final EnumParam<NonlinTransform> nonlinType = NonlinTransform.asParam();
private final RangeParam nonlinTuning = new RangeParam("Nonlinear Tuning", -100, 0, 100);

public Rose() {
super(false);

nonlinType.setupEnableOtherIf(nonlinTuning, NonlinTransform::hasTuning);

setParams(
nParam,
dParam,
bgColor,
fgColor,
center,
scale,
rotate
new DialogParam("Transform",
nonlinType, nonlinTuning, center, rotate, scale)
);

helpURL = "https://en.wikipedia.org/wiki/Rose_(mathematics)";
Expand Down Expand Up @@ -104,6 +110,13 @@ public BufferedImage transform(BufferedImage src, BufferedImage dest) {
}
path.closePath();

NonlinTransform nonlin = nonlinType.getSelected();
if (nonlin != NONE) {
double amount = nonlinTuning.getValueAsDouble();
Point2D pivotPoint = new Point2D.Double(cx, cy);
path = nonlin.transform(path, pivotPoint, amount, width, height);
}

int scaleX = scale.getValue(0);
int scaleY = scale.getValue(1);
Shape shape;
Expand Down
230 changes: 230 additions & 0 deletions src/main/java/pixelitor/utils/NonlinTransform.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,230 @@
/*
* Copyright 2024 Laszlo Balazs-Csiki and Contributors
*
* 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.utils;

import pixelitor.filters.gui.EnumParam;

import java.awt.Shape;
import java.awt.geom.Path2D;
import java.awt.geom.PathIterator;
import java.awt.geom.Point2D;

import static java.awt.geom.PathIterator.SEG_CLOSE;
import static java.awt.geom.PathIterator.SEG_CUBICTO;
import static java.awt.geom.PathIterator.SEG_LINETO;
import static java.awt.geom.PathIterator.SEG_MOVETO;
import static java.awt.geom.PathIterator.SEG_QUADTO;
import static java.lang.Math.PI;
import static net.jafama.FastMath.atan2;
import static net.jafama.FastMath.cos;
import static net.jafama.FastMath.sin;

/**
* Nonlinear transformations that can be applied to points.
*/
public enum NonlinTransform {
NONE("None", false) {
@Override
public PointMapper createMapper(Point2D center, double tuning, int width, int height) {
throw new UnsupportedOperationException();
}
}, INVERT("Circle Inversion", true) {
@Override
public PointMapper createMapper(Point2D center, double tuning, int width, int height) {
double circleRadius2 = (width * width + height * height) / 20.0;
double tuningOffset = tuning * width / 500.0;
return (x, y) -> {
x -= tuningOffset;
double r = center.distance(x, y);
double cx = center.getX();
double cy = center.getY();
double angle = atan2(y - cy, x - cx);
double invertedR;
if (r > 1) { // the normal case
invertedR = circleRadius2 / r;
} else {
// points that are too far away can cause problems with
// some strokes, not not mention the infinitely distant points.
invertedR = circleRadius2;
}

// inverted point: same angle, but inverted distance
double newX = cx + invertedR * cos(angle);
double newY = cy + invertedR * sin(angle);
return new Point2D.Double(newX, newY);
};
}
}, SWIRL("Swirl", true) {
@Override
public PointMapper createMapper(Point2D center, double tuning, int width, int height) {
return (x, y) -> {
double r = center.distance(x, y);
double cx = center.getX();
double cy = center.getY();
double angle = atan2(y - cy, x - cx);
double newAngle = angle + tuning * r / 20_000;

double newX = cx + r * cos(newAngle);
double newY = cy + r * sin(newAngle);
return new Point2D.Double(newX, newY);
};
}
}, BULGE("Pinch-Bulge", true) {
@Override
public PointMapper createMapper(Point2D center, double tuning, int width, int height) {
double maxR = Math.sqrt(width * width + height * height) / 2.0;
return (x, y) -> {
double r = center.distance(x, y) / maxR;
double cx = center.getX();
double cy = center.getY();
double angle = atan2(y - cy, x - cx);
double newRadius = maxR * Math.pow(r, -tuning / 100 + 1);

double newX = cx + newRadius * cos(angle);
double newY = cy + newRadius * sin(angle);
return new Point2D.Double(newX, newY);
};
}
}, RECT_TO_POLAR("Rectangular to Polar", false) {
@Override
public PointMapper createMapper(Point2D center, double tuning, int width, int height) {
double maxR = Math.min(width, height) / 2.0;
return (x, y) -> {
double r = x * maxR / width;
double angle = y * 2 * PI / height;

double newX = center.getX() + r * cos(angle);
double newY = center.getY() + r * sin(angle);
return new Point2D.Double(newX, newY);
};
}
}, POLAR_TO_RECT("Polar to Rectangular", true) {
@Override
public PointMapper createMapper(Point2D center, double tuning, int width, int height) {
double maxR = Math.sqrt(width * width + height * height) / 2.0;
return (x, y) -> {
double r = center.distance(x, y) / maxR;
double cx = center.getX();
double cy = center.getY();

// atan2 is in the range -pi..pi, angle will be 0..2*pi
double angle = atan2(y - cy, x - cx) + PI;

// in the range 0..1
double normalizedAngle = angle / (2 * PI);
normalizedAngle += tuning / 100.0;
if (normalizedAngle > 1) {
normalizedAngle -= 1;
} else if (normalizedAngle < 0) {
normalizedAngle += 1;
}

double newX = normalizedAngle * width;
double newY = r * height;

return new Point2D.Double(newX, newY);
};
}
};

private final String displayName;
private final boolean hasTuning;

NonlinTransform(String displayName, boolean hasTuning) {
this.displayName = displayName;
this.hasTuning = hasTuning;
}

/**
* Transforms the given {@link Shape} into another
* {@link Shape} using the given {@link PointMapper}.
*/
private static Path2D transformShape(Shape shape, PointMapper mapper) {
Path2D transformedShape = shape instanceof Path2D inputPath
? new Path2D.Double(inputPath.getWindingRule())
: new Path2D.Double();

double[] coords = new double[6];
Point2D target;
Point2D cp1;
Point2D cp2;
PathIterator pathIterator = shape.getPathIterator(null);
while (!pathIterator.isDone()) {
int type = pathIterator.currentSegment(coords);
switch (type) {
case SEG_MOVETO:
target = mapper.map(coords[0], coords[1]);
transformedShape.moveTo(target.getX(), target.getY());
break;
case SEG_LINETO:
target = mapper.map(coords[0], coords[1]);
transformedShape.lineTo(target.getX(), target.getY());
break;
case SEG_QUADTO:
cp1 = mapper.map(coords[0], coords[1]);
target = mapper.map(coords[2], coords[3]);
transformedShape.quadTo(cp1.getX(), cp1.getY(),
target.getX(), target.getY());
break;
case SEG_CUBICTO:
cp1 = mapper.map(coords[0], coords[1]);
cp2 = mapper.map(coords[2], coords[3]);
target = mapper.map(coords[4], coords[5]);
transformedShape.curveTo(cp1.getX(), cp1.getY(),
cp2.getX(), cp2.getY(),
target.getX(), target.getY());
break;
case SEG_CLOSE:
transformedShape.closePath();
break;
}
pathIterator.next();
}
return transformedShape;
}

public Path2D transform(Shape shape, Point2D pivotPoint, double amount, int width, int height) {
PointMapper mapper = createMapper(pivotPoint, amount, width, height);
return transformShape(shape, mapper);
}

/**
* Creates a point mapper for this transformation.
*/
public abstract PointMapper createMapper(Point2D center, double tuning, int width, int height);

public static EnumParam<NonlinTransform> asParam() {
return new EnumParam<>("Nonlinear Transform", NonlinTransform.class);
}

public boolean hasTuning() {
return hasTuning;
}

@Override
public String toString() {
return displayName;
}

/**
* Maps a coordinate to another coordinate.
*/
public interface PointMapper {
Point2D map(double x, double y);
}
}
Loading

0 comments on commit 2c124d3

Please sign in to comment.