From 52b8d96b9611040c88b512f6a932e5f753a361db Mon Sep 17 00:00:00 2001
From: Michael Carleton
Date: Fri, 3 Jan 2025 16:33:19 +0000
Subject: [PATCH] fix subregion rendering for all gradient types
---
.../peasygradients/PeasyGradients.java | 99 ++++++++++++-------
.../peasygradients/PeasyGradientsTests.java | 88 ++++++++++++++++-
2 files changed, 150 insertions(+), 37 deletions(-)
diff --git a/src/main/micycle/peasygradients/PeasyGradients.java b/src/main/micycle/peasygradients/PeasyGradients.java
index e3d58d2..2e4b2ce 100644
--- a/src/main/micycle/peasygradients/PeasyGradients.java
+++ b/src/main/micycle/peasygradients/PeasyGradients.java
@@ -47,13 +47,15 @@
* with multiple color stops and custom center offsets)
*
*
- * By default, renders directly to the Processing sketch. Use
+ *
+ * By default, renders directly to the Processing sketch. Use
* {@code .setRenderTarget()} to specify a custom {@code PGraphics} output
- * buffer.
+ * buffer.
+ *
*
*
- * Linear, radial & conic sampling algorithms adapted from Jeremy
- * Behreandt. Additional sampling patterns are original implementations.
+ * Linear, radial & conic sampling algorithms adapted from Jeremy Behreandt.
+ * Additional sampling patterns are original implementations.
*
*
* @author Michael Carleton
@@ -107,9 +109,13 @@ public final class PeasyGradients {
/**
* Number of horizontal strips the plane is paritioned into for threaded
- * rendering
+ * rendering.
*/
- private int renderStrips = (int) Math.max(cpuThreads * 0.75, 1);
+ private int renderStrips = (int) Math.min(Math.max(cpuThreads * 0.75, 1), 10); // 1..10 strips
+
+ void setRenderStrips(int renderStrips) {
+ this.renderStrips = renderStrips;
+ }
/**
* Constructs a new PeasyGradients renderer from a running Processing sketch;
@@ -194,22 +200,20 @@ public void setRenderTarget(PImage g) {
* @param height height of region to render gradients into
*/
public void setRenderTarget(PImage g, int offSetX, int offSetY, int width, int height) {
- if (offSetX < 0 || offSetY < 0 || (width + offSetX) > g.width || (offSetY + height) > g.height) {
- System.err.println("Invalid parameters.");
- return;
- }
+ renderOffsetX = Math.max(0, offSetX); // Offset X cannot be less than 0
+ renderOffsetX = Math.min(renderOffsetX, g.width - 1); // Offset X cannot be beyond (image width - 1)
+ renderOffsetY = Math.max(0, offSetY); // Offset Y cannot be less than 0
+ renderOffsetY = Math.min(renderOffsetY, g.height - 1); // Offset Y cannot be beyond (image height - 1)
- final int actualWidth = width - offSetX;
- final int actualHeight = height - offSetY;
+ // 2. Constrain Width and Height based on Constrained Offsets:
+ renderWidth = Math.max(0, width); // Width cannot be negative
+ renderWidth = Math.min(renderWidth, g.width - renderOffsetX); // Width cannot extend beyond the image's right edge
+ renderHeight = Math.max(0, height); // Height cannot be negative
+ renderHeight = Math.min(renderHeight, g.height - renderOffsetY); // Height cannot extend beyond the image's bottom edge
scaleX = g.width / (double) width; // used for correct rendering increment for some gradients
scaleY = g.height / (double) height; // used for correct rendering increment for some gradients
- renderWidth = width;
- renderHeight = height;
- renderOffsetX = offSetX;
- renderOffsetY = offSetY;
-
if (!g.isLoaded()) { // load pixel array if not already done
if (g instanceof PGraphics) {
((PGraphics) g).beginDraw();
@@ -220,7 +224,7 @@ public void setRenderTarget(PImage g, int offSetX, int offSetY, int width, int h
gradientPG = g;
- gradientCacheSize = (3 * Math.max(actualWidth, actualHeight));
+ gradientCacheSize = (3 * Math.max(renderWidth, renderHeight));
gradientCache = new int[gradientCacheSize];
}
@@ -403,7 +407,6 @@ public void linearGradient(Gradient gradient, PVector controlPoint1, PVector con
double odY = controlPoint2.y - controlPoint1.y; // Rise and run of line.
final double odSqInverse = 1 / (odX * odX + odY * odY); // Distance-squared of line.
double opXod = -controlPoint1.x * odX + -controlPoint1.y * odY;
-
makeThreadPool(gradient, renderStrips, LinearThread.class, odX, odY, odSqInverse, opXod);
gradientPG.updatePixels();
@@ -849,8 +852,8 @@ public void hourglassGradient(Gradient gradient, PVector centerPoint, double ang
/**
* Creates a pool of threads to split the rendering work for the given gradient
- * type (each thread works on a portion of the pixels array). This method starts
- * the threads and returns when all threads have completed.
+ * type (each thread works on a horizontal strip portion of the pixels array).
+ * This method starts the threads and returns when all threads have completed.
*
* @param gradient TODO
* @param partitionsY
@@ -879,14 +882,15 @@ private void makeThreadPool(Gradient gradient, final int partitionsY, final Clas
// division)
int rows = renderHeight / partitionsY;
for (int strip = 0; strip < partitionsY - 1; strip++) {
- fullArgs[1] = rows * strip;
- fullArgs[2] = rows;
+ fullArgs[1] = rows * strip; // row vertical offset (y coord to start rendering at)
+ fullArgs[2] = rows; // row count (height of horizontal strip)
RenderThread thread = constructor.newInstance(fullArgs);
taskList.add(thread);
}
fullArgs[1] = rows * (partitionsY - 1);
fullArgs[2] = renderHeight - rows * (partitionsY - 1);
+
RenderThread thread = constructor.newInstance(fullArgs);
taskList.add(thread);
@@ -941,7 +945,7 @@ private double interleavedGradientNoise(final int x, final int y) {
}
/**
- * Threads operate on a portion of the pixels grid.
+ * Threads operate on a portion (horizontal strip) of the pixels grid.
*
* RenderThread child classes will implement call(); here the parallel gradient
* rendering work is done.
@@ -963,7 +967,7 @@ private abstract class RenderThread implements Callable {
RenderThread(int rowOffset, int rows) {
this.rowOffset = rowOffset;
this.rows = rows;
- pixel = rowOffset * renderWidth;
+ pixel = (rowOffset + renderOffsetY) * gradientPG.width; // Start at the correct global row, only considering renderOffsetY here.
}
}
@@ -987,19 +991,20 @@ public Boolean call() {
* Usually we'd call Functions.linearProject() to calculate step at each pixel,
* but the function is inlined here to optimise speed.
*/
- opXod += rowOffset * odY * scaleY; // offset for thread
+ opXod += rowOffset * odY * scaleY;
for (int y = rowOffset; y < rowOffset + rows; y++) {
opXod += odY * scaleY;
- double xOff = 0; // set partition x offset to correct amount
+ // Add renderOffsetX only at the start of each row.
+ pixel += renderOffsetX;
for (int x = 0; x < renderWidth; x++) {
- double step = (opXod + xOff) * odSqInverse; // get position of point on 1D gradient and normalise
- xOff += odX * scaleX;
+ double step = (opXod + x * odX * scaleX) * odSqInverse;
int stepInt = clampAndDither(step, x, y);
gradientPG.pixels[pixel++] = gradientCache[stepInt];
}
+ // After rendering a row, jump to the beginning of the next row.
+ pixel += gradientPG.width - (renderWidth + renderOffsetX);
}
-
return true;
}
@@ -1022,6 +1027,7 @@ public Boolean call() {
for (int y = rowOffset; y < rowOffset + rows; y++) {
double rise = renderMidpointY - y;
rise *= rise;
+ pixel += renderOffsetX;
for (int x = 0; x < renderWidth; x++) {
double run = renderMidpointX - x;
@@ -1033,6 +1039,7 @@ public Boolean call() {
int stepInt = clampAndDither(dist, x, y);
gradientPG.pixels[pixel++] = gradientCache[stepInt];
}
+ pixel += gradientPG.width - (renderWidth + renderOffsetX);
}
return true;
}
@@ -1059,7 +1066,8 @@ public Boolean call() {
for (int y = rowOffset; y < rowOffset + rows; y++) {
rise = renderMidpointY - y;
- for (int x = 0; x < gradientPG.width; x++) { // FULL WIDTH
+ pixel += renderOffsetX;
+ for (int x = 0; x < renderWidth; x++) { // FULL WIDTH
run = renderMidpointX - x;
t = Functions.fastAtan2b(rise, run) + Math.PI - angle; // + PI to align bump with angle
t *= INV_TWO_PI; // normalise
@@ -1069,6 +1077,7 @@ public Boolean call() {
gradientPG.pixels[pixel++] = gradientCache[stepInt];
}
+ pixel += gradientPG.width - (renderWidth + renderOffsetX);
}
return true;
@@ -1107,15 +1116,16 @@ public Boolean call() throws Exception {
double t;
double spiralOffset = 0;
for (int y = rowOffset; y < rowOffset + rows; y++) {
+ pixel += renderOffsetX;
double rise = renderMidpointY - y;
final double riseSquared = rise * rise;
- for (int x = 0; x < gradientPG.width; x++) { // FULL WIDTH
+ for (int x = 0; x < renderWidth; x++) { // FULL WIDTH
double run = renderMidpointX - x;
t = Functions.fastAtan2b(rise, run) - angle; // -PI...PI
spiralOffset = curviness == 0.5f ? Math.sqrt((riseSquared + run * run) * curveDenominator)
: FastPow.fastPow((riseSquared + run * run) * curveDenominator, curviness);
- spiralOffset*=curveCount;
+ spiralOffset *= curveCount;
t += spiralOffset;
t *= INV_TWO_PI; // normalise
@@ -1125,6 +1135,7 @@ public Boolean call() throws Exception {
gradientPG.pixels[pixel++] = gradientCache[stepInt];
}
+ pixel += gradientPG.width - (renderWidth + renderOffsetX);
}
return true;
@@ -1152,6 +1163,7 @@ public Boolean call() {
double xDist; // x distance between midpoint and a given pixel
for (int y = rowOffset; y < rowOffset + rows; y++) {
+ pixel += renderOffsetX;
yDist = (renderMidpointY - y);
xDist = renderMidpointX;
for (int x = 0; x < renderWidth; x++) {
@@ -1167,6 +1179,7 @@ public Boolean call() {
final int stepInt = clampAndDither(dist, x, y);
gradientPG.pixels[pixel++] = gradientCache[stepInt];
}
+ pixel += gradientPG.width - (renderWidth + renderOffsetX);
}
return true;
@@ -1193,6 +1206,7 @@ private final class CrossThread extends RenderThread {
public Boolean call() {
for (int y = rowOffset; y < rowOffset + rows; y++) {
+ pixel += renderOffsetX;
final double yTranslate = (y - renderMidpointY);
for (int x = 0; x < renderWidth; x++) {
final double newXpos = (x - renderMidpointX) * cos - yTranslate * sin + renderMidpointX; // rotate x about midpoint
@@ -1203,6 +1217,7 @@ public Boolean call() {
final int stepInt = clampAndDither(dist, x, y);
gradientPG.pixels[pixel++] = gradientCache[stepInt];
}
+ pixel += gradientPG.width - (renderWidth + renderOffsetX);
}
return true;
@@ -1229,6 +1244,7 @@ private final class DiamondThread extends RenderThread {
public Boolean call() {
for (int y = rowOffset; y < rowOffset + rows; y++) {
+ pixel += renderOffsetX;
final double yTranslate = (y - renderMidpointY);
for (int x = 0; x < renderWidth; x++) {
final double newXpos = (x - renderMidpointX) * cos - yTranslate * sin + renderMidpointX; // rotate x about midpoint
@@ -1239,6 +1255,7 @@ public Boolean call() {
final int stepInt = clampAndDither(dist, x, y);
gradientPG.pixels[pixel++] = gradientCache[stepInt];
}
+ pixel += gradientPG.width - (renderWidth + renderOffsetX);
}
return true;
@@ -1262,8 +1279,9 @@ private class NoiseThread extends RenderThread {
public Boolean call() {
for (int y = rowOffset; y < rowOffset + rows; y++) {
+ pixel += renderOffsetX;
final double yTranslate = (y - centerPoint.y);
- for (int x = 0; x < gradientPG.width; x++) {
+ for (int x = 0; x < renderWidth; x++) {
double newXpos = (x - centerPoint.x) * cos - yTranslate * sin + centerPoint.x; // rotate x about midpoint
double newYpos = yTranslate * cos + (x - centerPoint.x) * sin + centerPoint.y; // rotate y about midpoint
@@ -1272,6 +1290,7 @@ public Boolean call() {
final int stepInt = clampAndDither(step, x, y);
gradientPG.pixels[pixel++] = gradientCache[stepInt];
}
+ pixel += gradientPG.width - (renderWidth + renderOffsetX);
}
return true;
@@ -1294,8 +1313,9 @@ private final class UniformNoiseThread extends NoiseThread {
public Boolean call() {
for (int y = rowOffset; y < rowOffset + rows; y++) {
+ pixel += renderOffsetX;
final double yTranslate = (y - centerPoint.y);
- for (int x = 0; x < gradientPG.width; x++) {
+ for (int x = 0; x < renderWidth; x++) {
double newXpos = (x - centerPoint.x) * cos - yTranslate * sin + centerPoint.x; // rotate x about midpoint
double newYpos = yTranslate * cos + (x - centerPoint.x) * sin + centerPoint.y; // rotate y about midpoint
@@ -1304,6 +1324,7 @@ public Boolean call() {
final int stepInt = clampAndDither(step, x, y);
gradientPG.pixels[pixel++] = gradientCache[stepInt];
}
+ pixel += gradientPG.width - (renderWidth + renderOffsetX);
}
return true;
@@ -1330,8 +1351,9 @@ private final class FractalNoiseThread extends RenderThread {
public Boolean call() {
for (int y = rowOffset; y < rowOffset + rows; y++) {
+ pixel += renderOffsetX;
final double yTranslate = (y - centerPoint.y);
- for (int x = 0; x < gradientPG.width; x++) {
+ for (int x = 0; x < renderWidth; x++) {
double newXpos = (x - centerPoint.x) * cos - yTranslate * sin + centerPoint.x; // rotate x about midpoint
double newYpos = yTranslate * cos + (x - centerPoint.x) * sin + centerPoint.y; // rotate y about midpoint
@@ -1341,6 +1363,7 @@ public Boolean call() {
final int stepInt = clampAndDither(step, x, y);
gradientPG.pixels[pixel++] = gradientCache[stepInt];
}
+ pixel += gradientPG.width - (renderWidth + renderOffsetX);
}
return true;
@@ -1368,6 +1391,7 @@ private final class SpotlightThread extends RenderThread {
public Boolean call() {
for (int y = rowOffset; y < rowOffset + rows; y++) {
+ pixel += renderOffsetX;
final double yTranslate = (y - originPoint.y);
for (int x = 0; x < renderWidth; x++) {
double newXpos = (x - originPoint.x) * cos - yTranslate * sin + originPoint.x; // rotate x about midpoint
@@ -1392,6 +1416,7 @@ public Boolean call() {
int stepInt = clampAndDither(step, x, y);
gradientPG.pixels[pixel++] = gradientCache[stepInt];
}
+ pixel += gradientPG.width - (renderWidth + renderOffsetX);
}
return true;
@@ -1428,6 +1453,7 @@ public Boolean call() {
double xDist;
for (int y = rowOffset; y < rowOffset + rows; y++) {
+ pixel += renderOffsetX;
yDist = (renderMidpointY - y) * (renderMidpointY - y);
final double yTranslate = (y - renderMidpointY);
for (int x = 0; x < renderWidth; x++) {
@@ -1452,6 +1478,7 @@ public Boolean call() {
final int stepInt = clampAndDither(dist, x, y);
gradientPG.pixels[pixel++] = gradientCache[stepInt];
}
+ pixel += gradientPG.width - (renderWidth + renderOffsetX);
}
return true;
diff --git a/src/test/micycle/peasygradients/PeasyGradientsTests.java b/src/test/micycle/peasygradients/PeasyGradientsTests.java
index 24b4245..c3365c2 100644
--- a/src/test/micycle/peasygradients/PeasyGradientsTests.java
+++ b/src/test/micycle/peasygradients/PeasyGradientsTests.java
@@ -9,12 +9,14 @@
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.ValueSource;
-import micycle.peasygradients.PeasyGradients;
import micycle.peasygradients.gradient.Gradient;
import micycle.peasygradients.gradient.Palette;
import micycle.peasygradients.utilities.ColorUtils;
+import micycle.peasygradients.utilities.FastNoiseLite.FractalType;
+import micycle.peasygradients.utilities.FastNoiseLite.NoiseType;
import processing.core.PConstants;
import processing.core.PImage;
+import processing.core.PVector;
/**
* Tests to ensure 2D gradients are rendered as expected.
@@ -25,6 +27,90 @@ class PeasyGradientsTests {
private static final int GREY = ColorUtils.RGB255ToRGB255(128, 128, 128);
private static final int BLACK = ColorUtils.RGB255ToRGB255(0, 0, 0);
+ @Test
+ void testSubregionRendering() {
+ PeasyGradients pg = new PeasyGradients();
+
+ PImage i = new PImage(1000, 1000);
+ i.loadPixels();
+ Arrays.fill(i.pixels, WHITE);
+ i.updatePixels();
+ for (int value : i.pixels) {
+ assertEquals(WHITE, value);
+ }
+
+ int offsetX = 250;
+ int offsetY = 250;
+ int regionWidth = 500;
+ int regionHeight = 500;
+ pg.setRenderTarget(i, offsetX, offsetY, regionWidth, regionHeight); // offSetX, offSetY, width, height
+ pg.setRenderStrips(4);
+
+ PVector v = new PVector(500, 500);
+ Gradient g = new Gradient(BLACK, BLACK);
+
+ pg.linearGradient(g, 0);
+ assertRegion(i, offsetX, offsetY, regionWidth, regionHeight);
+ Arrays.fill(i.pixels, WHITE);
+
+ pg.radialGradient(g, v, 1);
+ assertRegion(i, offsetX, offsetY, regionWidth, regionHeight);
+ Arrays.fill(i.pixels, WHITE);
+
+ pg.conicGradient(g, new PVector(500, 500), 0);
+ assertRegion(i, offsetX, offsetY, regionWidth, regionHeight);
+ Arrays.fill(i.pixels, WHITE);
+
+ pg.spiralGradient(g, new PVector(500, 500), 0, 0, 5);
+ assertRegion(i, offsetX, offsetY, regionWidth, regionHeight);
+ Arrays.fill(i.pixels, WHITE);
+
+ pg.polygonGradient(g, v, 0, 0, 5);
+ assertRegion(i, offsetX, offsetY, regionWidth, regionHeight);
+ Arrays.fill(i.pixels, WHITE);
+
+ pg.crossGradient(g, v, 0, 1);
+ assertRegion(i, offsetX, offsetY, regionWidth, regionHeight);
+ Arrays.fill(i.pixels, WHITE);
+
+ pg.diamondGradient(g, v, 0, 1);
+ assertRegion(i, offsetX, offsetY, regionWidth, regionHeight);
+ Arrays.fill(i.pixels, WHITE);
+
+ pg.noiseGradient(g, v, 0, 1);
+ assertRegion(i, offsetX, offsetY, regionWidth, regionHeight);
+ Arrays.fill(i.pixels, WHITE);
+
+ pg.uniformNoiseGradient(g, v, 0, 0, 1);
+ assertRegion(i, offsetX, offsetY, regionWidth, regionHeight);
+ Arrays.fill(i.pixels, WHITE);
+
+ pg.fractalNoiseGradient(g, v, 0, 1, NoiseType.Perlin, FractalType.None, 0, 0, 0);
+ assertRegion(i, offsetX, offsetY, regionWidth, regionHeight);
+ Arrays.fill(i.pixels, WHITE);
+
+ pg.spotlightGradient(g, v, v.copy().add(100, 100));
+ assertRegion(i, offsetX, offsetY, regionWidth, regionHeight);
+ Arrays.fill(i.pixels, WHITE);
+
+ pg.hourglassGradient(g, v, 0, 1);
+ assertRegion(i, offsetX, offsetY, regionWidth, regionHeight);
+ Arrays.fill(i.pixels, WHITE);
+ }
+
+ private void assertRegion(PImage image, int offsetX, int offsetY, int regionWidth, int regionHeight) {
+ for (int y = 0; y < image.height; y++) {
+ for (int x = 0; x < image.width; x++) {
+ int index = y * image.width + x;
+ if (x >= offsetX && x < offsetX + regionWidth && y >= offsetY && y < offsetY + regionHeight) {
+ assertEquals(BLACK, image.pixels[index], "Pixel at (" + x + ", " + y + ") should be black");
+ } else {
+ assertEquals(WHITE, image.pixels[index], "Pixel at (" + x + ", " + y + ") should be white");
+ }
+ }
+ }
+ }
+
@Test
void testLinearGradient() {
PImage g = new PImage(100, 1);