/*
JWildfire - an image and animation processor written in Java
Copyright (C) 1995-2011 Andreas Maschke
This is free software; you can redistribute it and/or modify it under the terms of the GNU Lesser
General Public License as published by the Free Software Foundation; either version 2.1 of the
License, or (at your option) any later version.
This software 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
Lesser General Public License for more details.
You should have received a copy of the GNU Lesser General Public License along with this software;
if not, write to the Free Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
02110-1301 USA, or see the FSF site: http://www.fsf.org.
*/
package org.jwildfire.create.tina.variation;
import static java.lang.Math.abs;
import static java.lang.Math.ceil;
import static org.jwildfire.base.mathlib.MathLib.M_PI;
import static org.jwildfire.base.mathlib.MathLib.atan2;
import static org.jwildfire.base.mathlib.MathLib.cos;
import static org.jwildfire.base.mathlib.MathLib.floor;
import static org.jwildfire.base.mathlib.MathLib.sin;
import static org.jwildfire.base.mathlib.MathLib.sqrt;
import java.math.BigInteger;
import org.jwildfire.create.tina.base.Layer;
import org.jwildfire.create.tina.base.XForm;
import org.jwildfire.create.tina.base.XYZPoint;
/**
Rhodonea curves (also known as rose curves)
Implemented by CozyG, March 2015
For references, see http://en.wikipedia.org/wiki/Rose_%28mathematics%29
There are other JWildfire variations (RoseWF, PRose3D), and an Apophysis rose plugin (http://fardareismai.deviantart.com/art/Apophysis-Plugin-Rose-246324281)
that implement the class of rose curves where k is an integer and cycles = 1,
but Rhodonea implements a fuller range of possibilities (at least within the 2D plane)
From reference literature:
Rhodonea curves were studied and named by the Italian mathematician Guido Grandi between the year 1723 and 1728
These curves can all be expressed by a polar equation of the form
r = cos(k * theta)
or, alternatively, as a pair of Cartesian parametric equations of the form
x = cos(kt)cos(t)
y = cos(kt)sin(t)
If k is an integer, the curve will be rose-shaped with:
2k petals if k is even, and
k petals if k is odd.
When k is even, the entire graph of the rose will be traced out exactly once when the value of θ changes from 0 to 2π.
When k is odd, this will happen on the interval between 0 and π.
(More generally, this will happen on any interval of length 2π for k even, and π for k odd.)
If k is a half-integer (e.g. 1/2, 3/2, 5/2), the curve will be rose-shaped with 4k petals.
If k can be expressed as n±1/6, where n is a nonzero integer, the curve will be rose-shaped with 12k petals.
If k can be expressed as n/3, where n is an integer not divisible by 3,
the curve will be rose-shaped with n petals if n is odd and 2n petals if n is even.
If k is rational, then the curve is closed and has finite length.
If k is irrational, then it is not closed and has infinite length.
Furthermore, the graph of the rose in this case forms a dense set
(i.e., it comes arbitrarily close to every point in the unit disk).
Adding an offset parameter c, so the polar equation becomes
r = cos(k * theta) + c
In the case where the parameter k is an odd integer, the two overlapping halves of the curve separate as the offset changes from zero.
*/
public class RhodoneaFunc extends VariationFunc {
private static final long serialVersionUID = 1L;
private static final String PARAM_KNUMER = "knumer";
private static final String PARAM_KDENOM = "kdenom";
private static final String PARAM_INNER_MODE = "inner_mode";
private static final String PARAM_OUTER_MODE = "outer_mode";
private static final String PARAM_INNER_SPREAD = "inner_spread";
private static final String PARAM_OUTER_SPREAD = "outer_spread";
private static final String PARAM_INNER_SPREAD_RATIO = "inner_spread_ratio";
private static final String PARAM_OUTER_SPREAD_RATIO = "outer_spread_ratio";
private static final String PARAM_SPREAD_SPLIT = "spread_split";
private static final String PARAM_FILL = "fill";
private static final String PARAM_RADIAL_OFFSET = "radial_offset";
// if cycles is set to 0, function will make best effort to calculate minimum number of cycles needed
// to close curve, or a somewhat arbitrary number if cannot
private static final String PARAM_CYCLES = "cycles";
private static final String PARAM_CYCLE_OFFSET = "cycle_offset";
private static final String PARAM_METACYCLES = "metacycles"; // only in effect when cycles = 0 (automatic cycle calculations in effect)
private static final String PARAM_METACYCLE_EXPANSION = "metacycle_expansion";
private static final String[] paramNames = { PARAM_KNUMER, PARAM_KDENOM, PARAM_INNER_MODE, PARAM_OUTER_MODE, PARAM_INNER_SPREAD, PARAM_OUTER_SPREAD, PARAM_INNER_SPREAD_RATIO, PARAM_OUTER_SPREAD_RATIO, PARAM_SPREAD_SPLIT, PARAM_FILL, PARAM_RADIAL_OFFSET, PARAM_CYCLES, PARAM_CYCLE_OFFSET, PARAM_METACYCLES, PARAM_METACYCLE_EXPANSION };
private double knumer = 3; // numerator of k in rose curve equations, k = kn/kd
private double kdenom = 4; // denominator of k in rose curve equations, k = kn/kd
private double radial_offset = 0; // often called "c" in rose curve modifier equations
private int inner_mode = 1; // transform mode to use for incoming points within or on the curve
private int outer_mode = 1; // transform mode to use for incoming points outside the curve
private double inner_spread = 0; // amount to spread away from rose curve within or on the curve, based on incoming point x/y coords
private double outer_spread = 0; // amount to spread away from rose curve outside the curve, based on incoming point x/y coords
private double inner_spread_ratio = 1; // how much inner spread applies to x relative to y
private double outer_spread_ratio = 1; // how much outer spread applies to x relative to y
private double spread_split = 1; // scaling of input point radius to determine inner/outer threshold
private double cycles_param = 0; // number of cycles (roughly circle loops?), if set to 0 then number of cycles is calculated automatically to close curve (if possible)
private double cycle_offset = 0; // radians to offset cycle for incoming points
private double metacycle_expansion = 0; // expansion factor for each metacycle
private double metacycles = 1; // if cycles is calculated automatically to close the curve, metacycles is number of times to loop over closed curve
private double fill = 0; // amount to thicken curve by randomizing input
private double kn, kd;
private double k; // k = kn/kd
private double cycles; // 1 cycle = 2*PI
private double cycles_to_close;
// want to figure out (when possible):
// number of cycles(radians) to close the curve (radians = 2*PI*cycles)
// number of petals in the curve
// given those, can also calculate: for a given input x and y, which petal(s) the point will map to.
//
@Override
public void init(FlameTransformationContext pContext, Layer pLayer, XForm pXForm, double pAmount) {
kn = knumer;
kd = kdenom;
k = kn / kd;
// attempt to calculate minimum cycles manually, or reasonable upper bound if unsure
if ((k % 1) == 0) { // k is integer
if ((k % 2) == 0) { // k is even integer, will have 2k petals
cycles_to_close = 1; // (2PI)
}
else { // k is odd integer, will have k petals (or sometimes 2k with offset)
if (radial_offset != 0 || inner_spread != 0 || outer_spread != 0 || fill != 0) {
cycles_to_close = 1;
} // if adding an offset or spread, need a full cycle
else {
cycles_to_close = 0.5;
} // (1PI)
}
}
else if ((kn % 1 == 0) && (kd % 1 == 0)) {
// if kn and kd are integers,
// determine if kn and kd are relatively prime (their greatest common denominator is 1)
// using builtin gcd() function for BigIntegers in Java
// and if they're not, make them
BigInteger bigkn = BigInteger.valueOf((long) kn);
BigInteger bigkd = BigInteger.valueOf((int) kd);
int gcd = bigkn.gcd(bigkd).intValue();
if (gcd != 1) {
kn = kn / gcd;
kd = kd / gcd;
}
// paraphrased from http://www.encyclopediaofmath.org/index.php/Roses_%28curves%29:
// If kn and kd are relatively prime, then the rose consists of 2*kn petals if either kn or kd are even, and kn petals if both kn and kd are odd
//
// paraphrased from http://mathworld.wolfram.com/Rose.html:
// If k=kn/kd is a rational number, then the curve closes at a polar angle of theta = PI * kd if (kn * kd) is odd, and 2 * PI * kd if (kn * kd) is even
if ((kn % 2 == 0) || (kd % 2 == 0)) {
cycles_to_close = kd; // 2 * PI * kd
}
else {
cycles_to_close = kd / 2; // PI * kd
}
}
// additional special cases:
/*
// superceded by relative prime conditional?
else if (((k * 2) % 1) == 0) { // k is a half-integer (1/2, 3/2, 5/2, etc.), will have 4k petals
cycles_to_close = 2; // (4PI)
petal_count = 4*k;
}
*/
/*
// superceded by relative prime conditional?
// If k can be expressed as kn/3, where n is an integer not divisible by 3, the curve will be rose-shaped with n petals if n is odd and 2n petals if n is even.
// (case where kn is integer divisible by three is already handled above, where k is integer)
else if (((k * 3) % 1) == 0) {
double basekn = k * 3;
if ((basekn % 2) == 0) {
cycles_to_close = 3;
petal_count = 2 * basekn;
}
else {
cycles_to_close = 1.5;
petal_count = basekn;
}
}
*/
/*
// superceded by relative prime conditional????
If k can be expressed as n±1/6, where n is a nonzero integer, the curve will be rose-shaped with 12k petals.
else if {
}
*/
else {
// if one or both of kn and kd are non-integers, then the above may still be true (k may still be [effectively] rational) but haven't
// figured out a way to determine this.
// could restrict kn and kd to integers to simplify, but that would exclude a huge space of interesting patterns
// could set cycles extremely high, but if k is truly irrational this will just approarch a uniform distribution across a circle,
// and also exclude a large space of interesting patterns with non-closed curves
// so for now keep kn and kd as continuous doubles, and just pick a large but not huge number for cycles
//
// realistically in this case it is better for user to fiddle with manual cycles setting to get a pattern they like
//
cycles_to_close = 2 * kn * kd;
if (cycles < 16) {
cycles_to_close = 16;
} // ??? just want to keep number higher if kn and kd are small but potentially irrational
}
if (cycles_param == 0) {
// use auto-calculation of cycles (2*PI*radians) to close the curve,
// and metacycles for how many closed curves to cycle through
cycles = cycles_to_close * metacycles;
}
else {
// manually set number of cycles (cycles are specified in 2*PI*radians)
cycles = cycles_param;
}
// System.out.println("cycles to close: " + cycles_to_close);
// System.out.println("metacycles: " + metacycles);
// System.out.println("total cycles: " + cycles);
}
@Override
public void transform(FlameTransformationContext pContext, XForm pXForm, XYZPoint pAffineTP, XYZPoint pVarTP, double pAmount) {
/*
k = kn/kd
r = cos(k * theta)
or, alternatively, as a pair of Cartesian parametric equations of the form
t = atan2(y,x)
x = cos(kt)cos(t)
y = cos(kt)sin(t)
*/
double rin = spread_split * sqrt((pAffineTP.x * pAffineTP.x) + (pAffineTP.y * pAffineTP.y));
// atan2 range is [-PI, PI], so tin covers 2PI, or 1 cycle (from -0.5 to 0.5 cycle)
double tin = atan2(pAffineTP.y, pAffineTP.x); // polar coordinate angle (theta in radians) of incoming point
double t = cycles * (tin + (cycle_offset * 2 * M_PI)); // angle of rose curve
double r = cos(k * t) + radial_offset; // radius of rose curve
// double r = sin(k * t) + radial_offset; // can use sin instead of cos to rotate by PI/(2*k) radians
if (fill != 0) {
r = r + (fill * (pContext.random() - 0.5));
}
double x = r * cos(t);
double y = r * sin(t);
double expansion = floor((cycles * (tin + M_PI)) / (cycles_to_close * 2 * M_PI));
double adjustedAmount = pAmount + (expansion * metacycle_expansion);
double xin, yin;
double rinx, riny;
if (abs(rin) > abs(r)) { // incoming point lies outside of current petal of rose curve
switch (outer_mode) {
case 0: // no spread
pVarTP.x += adjustedAmount * x;
pVarTP.y += adjustedAmount * y;
break;
case 1:
rinx = (rin * outer_spread * outer_spread_ratio) - (outer_spread * outer_spread_ratio) + 1;
riny = (rin * outer_spread) - outer_spread + 1;
pVarTP.x += adjustedAmount * rinx * x;
pVarTP.y += adjustedAmount * riny * y;
if (pVarTP.y == 0) {
pVarTP.x = 0;
}
break;
case 2:
xin = Math.abs(pAffineTP.x);
yin = Math.abs(pAffineTP.y);
if (x < 0) {
xin = xin * -1;
}
if (y < 0) {
yin = yin * -1;
}
//pVarTP.x += adjustedAmount * x + ((rin - r) * outer_spread * outer_spread_ratio);
pVarTP.x += adjustedAmount * (x + (outer_spread * outer_spread_ratio * (xin - x)));
pVarTP.y += adjustedAmount * (y + (outer_spread * (yin - y)));
break;
case 3:
xin = Math.abs(pAffineTP.x);
yin = Math.abs(pAffineTP.y);
if (x < 0) {
xin = xin * -1;
}
if (y < 0) {
yin = yin * -1;
}
pVarTP.x += adjustedAmount * (x + (outer_spread * outer_spread_ratio * xin));
pVarTP.y += adjustedAmount * (y + (outer_spread * yin));
break;
case 4:
rinx = (0.5 * rin) + (outer_spread * outer_spread_ratio);
riny = (0.5 * rin) + outer_spread;
pVarTP.x += adjustedAmount * rinx * x;
pVarTP.y += adjustedAmount * riny * y;
break;
case 5: // mask "inside" the curve
pVarTP.x += pAffineTP.x;
pVarTP.y += pAffineTP.y;
break;
case 6: // mask "outside" the curve
pVarTP.doHide = true;
break;
default:
pVarTP.x += adjustedAmount * x;
pVarTP.y += adjustedAmount * y;
break;
}
}
else { // incoming point lies on or inside current petal of rose curve
switch (inner_mode) {
case 0: // no spread, all points map to rose curve
pVarTP.x += adjustedAmount * x;
pVarTP.y += adjustedAmount * y;
break;
case 1:
// rin = (rin * inner_spread) - inner_spread + 1;
rinx = (rin * inner_spread * inner_spread_ratio) - (inner_spread * inner_spread_ratio) + 1;
riny = (rin * inner_spread) - inner_spread + 1;
pVarTP.x += adjustedAmount * rinx * x;
pVarTP.y += adjustedAmount * riny * y;
if (pVarTP.y == 0) {
pVarTP.x = 0;
}
break;
case 2:
xin = Math.abs(pAffineTP.x);
yin = Math.abs(pAffineTP.y);
if (x < 0) {
xin = xin * -1;
}
if (y < 0) {
yin = yin * -1;
}
pVarTP.x += adjustedAmount * (x - (inner_spread * inner_spread_ratio * (x - xin)));
pVarTP.y += adjustedAmount * (y - (inner_spread * (y - yin)));
break;
case 3:
xin = Math.abs(pAffineTP.x);
yin = Math.abs(pAffineTP.y);
if (x < 0) {
xin = xin * -1;
}
if (y < 0) {
yin = yin * -1;
}
pVarTP.x += adjustedAmount * (x - (inner_spread * inner_spread_ratio * xin));
pVarTP.y += adjustedAmount * (y - (inner_spread * yin));
break;
case 4:
// rin = (0.5 * rin) + inner_spread;
rinx = (0.5 * rin) + (inner_spread * inner_spread_ratio);
riny = (0.5 * rin) + inner_spread;
pVarTP.x += adjustedAmount * rinx * x;
pVarTP.y += adjustedAmount * riny * y;
break;
case 5: // mask "inside" the curve
pVarTP.doHide = true;
break;
case 6: // mask "outside" the curve
pVarTP.x += pAffineTP.x;
pVarTP.y += pAffineTP.y;
break;
default: // shouldn't reach here (inner_mode is constrained to be 0-6), but just in case...
pVarTP.x += adjustedAmount * x;
pVarTP.y += adjustedAmount * y;
break;
}
}
boolean draw_diagnostics = false;
if (draw_diagnostics) {
double diagnostic = pContext.random() * 100;
// draw diagnostic unit circles
if (diagnostic == 0) {
// ignore zero
}
if (diagnostic <= 3) { // diagnostic = (0-3]
double radius = ceil(diagnostic); // radius = 1, 2, 3
double angle = diagnostic * 2 * M_PI; // in radians, ensures coverage of unit circles
pVarTP.x = radius * cos(angle);
pVarTP.y = radius * sin(angle);
}
// draw diagnostic unit squares
else if (diagnostic <= 6) { // diagnostic = (3-6]
double unit = ceil(diagnostic) - 3; // unit = 1, 2, 3
int side = (int) ceil(4 * (ceil(diagnostic) - diagnostic)); // side = 1, 2, 3, 4
double varpos = (pContext.random() * unit * 2) - unit;
double sx = 0, sy = 0;
if (side == 1) {
sx = unit;
sy = varpos;
}
else if (side == 2) {
sx = varpos;
sy = unit;
}
else if (side == 3) {
sx = -1 * unit;
sy = varpos;
}
else if (side == 4) {
sx = varpos;
sy = -1 * unit;
}
pVarTP.x = sx;
pVarTP.y = sy;
}
}
if (pContext.isPreserveZCoordinate()) {
pVarTP.z += pAmount * pAffineTP.z;
}
}
@Override
public String[] getParameterNames() {
return paramNames;
}
@Override
public Object[] getParameterValues() {
return new Object[] { knumer, kdenom,
inner_mode, outer_mode, inner_spread, outer_spread, inner_spread_ratio, outer_spread_ratio, spread_split,
fill, radial_offset,
cycles_param, cycle_offset, metacycles, metacycle_expansion };
}
@Override
public void setParameter(String pName, double pValue) {
if (PARAM_KNUMER.equalsIgnoreCase(pName))
knumer = pValue;
else if (PARAM_KDENOM.equalsIgnoreCase(pName))
kdenom = pValue;
else if (PARAM_RADIAL_OFFSET.equalsIgnoreCase(pName))
radial_offset = pValue;
else if (PARAM_INNER_MODE.equalsIgnoreCase(pName)) {
inner_mode = (int) floor(pValue);
if (inner_mode > 6 || inner_mode < 0) {
inner_mode = 0;
}
}
else if (PARAM_OUTER_MODE.equalsIgnoreCase(pName)) {
outer_mode = (int) floor(pValue);
if (outer_mode > 6 || outer_mode < 0) {
outer_mode = 0;
}
}
else if (PARAM_INNER_SPREAD.equalsIgnoreCase(pName))
inner_spread = pValue;
else if (PARAM_OUTER_SPREAD.equalsIgnoreCase(pName))
outer_spread = pValue;
else if (PARAM_INNER_SPREAD_RATIO.equalsIgnoreCase(pName))
inner_spread_ratio = pValue;
else if (PARAM_OUTER_SPREAD_RATIO.equalsIgnoreCase(pName))
outer_spread_ratio = pValue;
else if (PARAM_SPREAD_SPLIT.equalsIgnoreCase(pName))
spread_split = pValue;
else if (PARAM_CYCLES.equalsIgnoreCase(pName))
cycles_param = pValue;
else if (PARAM_CYCLE_OFFSET.equalsIgnoreCase(pName))
cycle_offset = pValue;
else if (PARAM_METACYCLES.equalsIgnoreCase(pName))
metacycles = pValue;
else if (PARAM_METACYCLE_EXPANSION.equalsIgnoreCase(pName))
metacycle_expansion = pValue;
else if (PARAM_FILL.equalsIgnoreCase(pName))
fill = pValue;
else
throw new IllegalArgumentException(pName);
}
@Override
public String getName() {
return "rhodonea";
}
}