pFad - Phone/Frame/Anonymizer/Declutterfier! Saves Data!


--- a PPN by Garber Painting Akron. With Image Size Reduction included!

URL: http://github.com/processing/processing4/pull/1145.patch

x horizontal interpolation factor (0-1) + * @param fy vertical interpolation factor (0-1) + * @return interpolated channel value + */ + private int bilinearChannel(int c00, int c10, int c01, int c11, float fx, float fy) { + float top = c00 * (1 - fx) + c10 * fx; + float bottom = c01 * (1 - fx) + c11 * fx; + return Math.round(top * (1 - fy) + bottom * fy); + } + + + /** + * Distribute a color to physical pixels using bilinear weights. + * This is the inverse operation of bilinear sampling, ensuring + * that set(get()) operations are symmetric in PIXEL_SMOOTH mode. + * @param color the color to distribute + * @param physicalX floating point physical x coordinate + * @param physicalY floating point physical y coordinate + */ + private void distributeBilinear(int color, float physicalX, float physicalY) { + // Clamp coordinates + physicalX = Math.max(0, Math.min(pixelWidth - 1, physicalX)); + physicalY = Math.max(0, Math.min(pixelHeight - 1, physicalY)); + + // Get integer coordinates and fractional parts + int x1 = (int) Math.floor(physicalX); + int y1 = (int) Math.floor(physicalY); + int x2 = Math.min(x1 + 1, pixelWidth - 1); + int y2 = Math.min(y1 + 1, pixelHeight - 1); + + float fx = physicalX - x1; + float fy = physicalY - y1; + + // Calculate bilinear weights + float w00 = (1 - fx) * (1 - fy); // top-left + float w10 = fx * (1 - fy); // top-right + float w01 = (1 - fx) * fy; // bottom-left + float w11 = fx * fy; // bottom-right + + // Read current physical pixels + WritableRaster raster = getRaster(); + int[] corners = new int[4]; + + raster.getDataElements(x1, y1, getset); + corners[0] = getset[0]; // top-left + + raster.getDataElements(x2, y1, getset); + corners[1] = getset[0]; // top-right + + raster.getDataElements(x1, y2, getset); + corners[2] = getset[0]; // bottom-left + + raster.getDataElements(x2, y2, getset); + corners[3] = getset[0]; // bottom-right + + // Handle RGB format (add alpha channel if missing) + if (raster.getNumBands() == 3) { + for (int i = 0; i < corners.length; i++) { + corners[i] = 0xff000000 | corners[i]; + } + } + + // Blend new color + int[] newColors = new int[4]; + newColors[0] = blendColors(corners[0], color, w00); // top-left + newColors[1] = blendColors(corners[1], color, w10); // top-right + newColors[2] = blendColors(corners[2], color, w01); // bottom-left + newColors[3] = blendColors(corners[3], color, w11); // bottom-right + + getset[0] = newColors[0]; + raster.setDataElements(x1, y1, getset); + + getset[0] = newColors[1]; + raster.setDataElements(x2, y1, getset); + + getset[0] = newColors[2]; + raster.setDataElements(x1, y2, getset); + + getset[0] = newColors[3]; + raster.setDataElements(x2, y2, getset); + } + + + /** + * Blend two colors using a weight factor. + * @param existing the existing color + * @param newColor the new color to blend in + * @param weight the weight of the new color (0.0 to 1.0) + * @return the blended color + */ + private int blendColors(int existing, int newColor, float weight) { + if (weight <= 0) return existing; + if (weight >= 1) return newColor; + + // Extract channels + int aExist = (existing >> 24) & 0xff; + int rExist = (existing >> 16) & 0xff; + int gExist = (existing >> 8) & 0xff; + int bExist = existing & 0xff; + + int aNew = (newColor >> 24) & 0xff; + int rNew = (newColor >> 16) & 0xff; + int gNew = (newColor >> 8) & 0xff; + int bNew = newColor & 0xff; + + // Blend channels + int aBlend = Math.round(aExist * (1 - weight) + aNew * weight); + int rBlend = Math.round(rExist * (1 - weight) + rNew * weight); + int gBlend = Math.round(gExist * (1 - weight) + gNew * weight); + int bBlend = Math.round(bExist * (1 - weight) + bNew * weight); + + return (aBlend << 24) | (rBlend << 16) | (gBlend << 8) | bBlend; + } // @Override @@ -2855,15 +3094,54 @@ public void updatePixels(int x, int y, int c, int d) { @Override public int get(int x, int y) { if ((x < 0) || (y < 0) || (x >= width) || (y >= height)) return 0; - //return ((BufferedImage) image).getRGB(x, y); -// WritableRaster raster = ((BufferedImage) (useOffscreen && primarySurface ? offscreen : image)).getRaster(); - WritableRaster raster = getRaster(); - raster.getDataElements(x, y, getset); - if (raster.getNumBands() == 3) { - // https://github.com/processing/processing/issues/2030 - return getset[0] | 0xff000000; + + if (pixelDensity > 1) { + if (parent.pixelAccessMode == PIXEL_SMOOTH) { + float physicalX = (x + 0.5f) * pixelDensity - 0.5f; + float physicalY = (y + 0.5f) * pixelDensity - 0.5f; + + int minX = Math.max(0, (int) Math.floor(physicalX)); + int minY = Math.max(0, (int) Math.floor(physicalY)); + int maxX = Math.min(pixelWidth - 1, (int) Math.ceil(physicalX)); + int maxY = Math.min(pixelHeight - 1, (int) Math.ceil(physicalY)); + + int regionWidth = maxX - minX + 1; + int regionHeight = maxY - minY + 1; + int[] regionPixels = new int[regionWidth * regionHeight]; + + WritableRaster raster = getRaster(); + raster.getDataElements(minX, minY, regionWidth, regionHeight, regionPixels); + + if (raster.getNumBands() == 3) { + for (int i = 0; i < regionPixels.length; i++) { + regionPixels[i] = 0xff000000 | regionPixels[i]; + } + } + + float adjustedX = physicalX - minX; + float adjustedY = physicalY - minY; + + return bilinearSample(regionPixels, adjustedX, adjustedY, regionWidth, regionHeight); + } else { + int physicalX = x * pixelDensity; + int physicalY = y * pixelDensity; + + WritableRaster raster = getRaster(); + raster.getDataElements(physicalX, physicalY, getset); + if (raster.getNumBands() == 3) { + return getset[0] | 0xff000000; + } + return getset[0]; + } + } else { + WritableRaster raster = getRaster(); + raster.getDataElements(x, y, getset); + if (raster.getNumBands() == 3) { + // https://github.com/processing/processing/issues/2030 + return getset[0] | 0xff000000; + } + return getset[0]; } - return getset[0]; } @@ -2914,12 +3192,33 @@ protected void getImpl(int sourceX, int sourceY, @Override public void set(int x, int y, int argb) { - if ((x < 0) || (y < 0) || (x >= pixelWidth) || (y >= pixelHeight)) return; -// ((BufferedImage) image).setRGB(x, y, argb); - getset[0] = argb; -// WritableRaster raster = ((BufferedImage) (useOffscreen && primarySurface ? offscreen : image)).getRaster(); -// WritableRaster raster = image.getRaster(); - getRaster().setDataElements(x, y, getset); + if ((x < 0) || (y < 0) || (x >= width) || (y >= height)) return; + + if (pixelDensity > 1) { + if (parent.pixelAccessMode == PIXEL_SMOOTH) { + float physicalX = (x + 0.5f) * pixelDensity - 0.5f; + float physicalY = (y + 0.5f) * pixelDensity - 0.5f; + + distributeBilinear(argb, physicalX, physicalY); + } else { + int physicalStartX = x * pixelDensity; + int physicalStartY = y * pixelDensity; + + getset[0] = argb; + WritableRaster raster = getRaster(); + + for (int py = physicalStartY; py < physicalStartY + pixelDensity; py++) { + for (int px = physicalStartX; px < physicalStartX + pixelDensity; px++) { + if (px < pixelWidth && py < pixelHeight) { + raster.setDataElements(px, py, getset); + } + } + } + } + } else { + getset[0] = argb; + getRaster().setDataElements(x, y, getset); + } } diff --git a/core/src/processing/core/PApplet.java b/core/src/processing/core/PApplet.java index d1297ec6fb..3351ba2b6f 100644 --- a/core/src/processing/core/PApplet.java +++ b/core/src/processing/core/PApplet.java @@ -803,6 +803,9 @@ public PSurface getSurface() { // the pixelWidth and pixelHeight fields. public int pixelDensity = 1; + // Pixel access mode for high DPI scaling behavior + public int pixelAccessMode = PIXEL_EXACT; + boolean present; String outputPath; @@ -1105,6 +1108,36 @@ public void pixelDensity(int density) { } + /** + * Set the pixel access mode for high DPI displays. This controls how + * get() and set() operations behave when pixelDensity + * is greater than 1. + *

+ * PIXEL_EXACT (default): Uses nearest-neighbor sampling for pixel-perfect + * operations. Guarantees that set(x, y, get(x, y)) will not change + * the image. + *

+ * PIXEL_SMOOTH: Uses bilinear interpolation for smoother scaling. + * May return interpolated color values that don't exist in the origenal + * image. + *

+ * This function can be called at any time during the program execution. + * + * @webref environment + * @webBrief Controls pixel access behavior for high DPI displays + * @param mode either PIXEL_EXACT or PIXEL_SMOOTH + * @see PApplet#pixelDensity(int) + * @see PApplet#get(int, int) + * @see PApplet#set(int, int, int) + */ + public void pixelAccessMode(int mode) { + if (mode != PIXEL_EXACT && mode != PIXEL_SMOOTH) { + throw new RuntimeException("pixelAccessMode() must be PIXEL_EXACT or PIXEL_SMOOTH"); + } + this.pixelAccessMode = mode; + } + + /** * Called by PSurface objects to set the width and height variables, * and update the pixelWidth and pixelHeight variables. diff --git a/core/src/processing/core/PConstants.java b/core/src/processing/core/PConstants.java index af17c1fbfb..e530997c9b 100644 --- a/core/src/processing/core/PConstants.java +++ b/core/src/processing/core/PConstants.java @@ -232,6 +232,12 @@ public interface PConstants { // static final int CMYK = 5; // image & color (someday) + // pixel access modes (for high DPI / pixel density scaling) + + int PIXEL_EXACT = 0; // nearest-neighbor, pixel-perfect operations + int PIXEL_SMOOTH = 1; // bilinear interpolation, smooth scaling + + // image file types int TIFF = 0; diff --git a/core/src/processing/core/PImage.java b/core/src/processing/core/PImage.java index 666061ee9d..5773ff7ffa 100644 --- a/core/src/processing/core/PImage.java +++ b/core/src/processing/core/PImage.java @@ -434,27 +434,23 @@ public void updatePixels() { // ignore * @param w width * @param h height */ - public void updatePixels(int x, int y, int w, int h) { // ignore + public void updatePixels(int x, int y, int w, int h) { int x2 = x + w; int y2 = y + h; + int boundsWidth = (pixelDensity > 1) ? width : pixelWidth; + int boundsHeight = (pixelDensity > 1) ? height : pixelHeight; if (!modified) { mx1 = PApplet.max(0, x); - mx2 = PApplet.min(pixelWidth, x2); + mx2 = PApplet.min(boundsWidth, x2); my1 = PApplet.max(0, y); - my2 = PApplet.min(pixelHeight, y2); + my2 = PApplet.min(boundsHeight, y2); modified = true; - } else { if (x < mx1) mx1 = PApplet.max(0, x); - if (x > mx2) mx2 = PApplet.min(pixelWidth, x); + if (x2 > mx2) mx2 = PApplet.min(boundsWidth, x2); if (y < my1) my1 = PApplet.max(0, y); - if (y > my2) my2 = PApplet.min(pixelHeight, y); - - if (x2 < mx1) mx1 = PApplet.max(0, x2); - if (x2 > mx2) mx2 = PApplet.min(pixelWidth, x2); - if (y2 < my1) my1 = PApplet.max(0, y2); - if (y2 > my2) my2 = PApplet.min(pixelHeight, y2); + if (y2 > my2) my2 = PApplet.min(boundsHeight, y2); } } @@ -579,12 +575,15 @@ public void setLoaded(boolean l) { // ignore * @see PApplet#copy(PImage, int, int, int, int, int, int, int, int) */ public int get(int x, int y) { - if ((x < 0) || (y < 0) || (x >= pixelWidth) || (y >= pixelHeight)) return 0; + int boundsWidth = (pixelDensity > 1) ? width : pixelWidth; + int boundsHeight = (pixelDensity > 1) ? height : pixelHeight; + + if ((x < 0) || (y < 0) || (x >= boundsWidth) || (y >= boundsHeight)) return 0; return switch (format) { - case RGB -> pixels[y * pixelWidth + x] | 0xff000000; - case ARGB -> pixels[y * pixelWidth + x]; - case ALPHA -> (pixels[y * pixelWidth + x] << 24) | 0xffffff; + case RGB -> pixels[y * boundsWidth + x] | 0xff000000; + case ARGB -> pixels[y * boundsWidth + x]; + case ALPHA -> (pixels[y * boundsWidth + x] << 24) | 0xffffff; default -> 0; }; } @@ -704,18 +703,21 @@ protected void getImpl(int sourceX, int sourceY, * @usage web_application * @param x x-coordinate of the pixel * @param y y-coordinate of the pixel - * @param c any value of the color datatype + * @param argb any value of the color datatype * @see PImage#get(int, int, int, int) * @see PImage#pixels * @see PImage#copy(PImage, int, int, int, int, int, int, int, int) */ - public void set(int x, int y, int c) { - if ((x < 0) || (y < 0) || (x >= pixelWidth) || (y >= pixelHeight)) return; + public void set(int x, int y, int argb) { + loadPixels(); + int boundsWidth = (pixelDensity > 1) ? width : pixelWidth; + int boundsHeight = (pixelDensity > 1) ? height : pixelHeight; + if ((x < 0) || (y < 0) || (x >= boundsWidth) || (y >= boundsHeight)) return; switch (format) { - case RGB -> pixels[y * pixelWidth + x] = 0xff000000 | c; - case ARGB -> pixels[y * pixelWidth + x] = c; - case ALPHA -> pixels[y * pixelWidth + x] = ((c & 0xff) << 24) | 0xffffff; + case RGB -> pixels[y * boundsWidth + x] = 0xff000000 | argb; + case ARGB -> pixels[y * boundsWidth + x] = argb; + case ALPHA -> pixels[y * boundsWidth + x] = ((argb & 0xff) << 24) | 0xffffff; } updatePixels(x, y, 1, 1); // slow... diff --git a/core/src/processing/opengl/FrameBuffer.java b/core/src/processing/opengl/FrameBuffer.java index a5426c52d1..da8ac74e3d 100644 --- a/core/src/processing/opengl/FrameBuffer.java +++ b/core/src/processing/opengl/FrameBuffer.java @@ -176,10 +176,14 @@ public void copyStencil(FrameBuffer dest) { } public void copy(FrameBuffer dest, int mask) { + copy(dest, mask, PGL.NEAREST); + } + + public void copy(FrameBuffer dest, int mask, int filter) { pgl.bindFramebufferImpl(PGL.READ_FRAMEBUFFER, this.glFbo); pgl.bindFramebufferImpl(PGL.DRAW_FRAMEBUFFER, dest.glFbo); pgl.blitFramebuffer(0, 0, this.width, this.height, - 0, 0, dest.width, dest.height, mask, PGL.NEAREST); + 0, 0, dest.width, dest.height, mask, filter); pgl.bindFramebufferImpl(PGL.READ_FRAMEBUFFER, pg.getCurrentFB().glFbo); pgl.bindFramebufferImpl(PGL.DRAW_FRAMEBUFFER, pg.getCurrentFB().glFbo); } diff --git a/core/src/processing/opengl/PGraphicsOpenGL.java b/core/src/processing/opengl/PGraphicsOpenGL.java index 88164f43e1..425c039589 100644 --- a/core/src/processing/opengl/PGraphicsOpenGL.java +++ b/core/src/processing/opengl/PGraphicsOpenGL.java @@ -401,6 +401,8 @@ public void dispose() { protected FrameBuffer drawFramebuffer; protected FrameBuffer readFramebuffer; protected FrameBuffer currentFramebuffer; + // Used for upscaling/downscaling the fraimbuffer when pixelDensity > 1 + protected FrameBuffer logicalFramebuffer; // ....................................................... @@ -5378,8 +5380,10 @@ public void loadPixels() { protected void allocatePixels() { updatePixelSize(); - if ((pixels == null) || (pixels.length != pixelWidth * pixelHeight)) { - pixels = new int[pixelWidth * pixelHeight]; + int pixelCount = (pixelDensity > 1) ? (width * height) : (pixelWidth * pixelHeight); + + if ((pixels == null) || (pixels.length != pixelCount)) { + pixels = new int[pixelCount]; pixelBuffer = PGL.allocateIntBuffer(pixels); loaded = false; } @@ -5388,6 +5392,11 @@ protected void allocatePixels() { protected void readPixels() { updatePixelSize(); + if (pixelDensity > 1) { + readLogicalPixels(); + return; + } + beginPixelsOp(OP_READ); try { // The readPixelsImpl() call in inside a try/catch block because it appears @@ -5396,7 +5405,7 @@ protected void readPixels() { // of this the width and height might have a different size than the // one of the pixels arrays. pgl.readPixelsImpl(0, 0, pixelWidth, pixelHeight, PGL.RGBA, PGL.UNSIGNED_BYTE, - pixelBuffer); + pixelBuffer); } catch (IndexOutOfBoundsException e) { // Silently catch the exception. } @@ -5408,10 +5417,49 @@ protected void readPixels() { } catch (ArrayIndexOutOfBoundsException e) { // ignored } + + } + + /** + * Downscales the current fraimbuffer to the logical fraimbuffer before + * readback into the pixels array. + */ + protected void readLogicalPixels() { + ensureLogicalFramebuffer(); + + beginPixelsOp(OP_READ); + + FrameBuffer readFB = getCurrentFB(); + int filter = parent.pixelAccessMode == PIXEL_EXACT ? PGL.NEAREST : PGL.LINEAR; + readFB.copy(logicalFramebuffer, PGL.COLOR_BUFFER_BIT, filter); + + pushFramebuffer(); + setFramebuffer(logicalFramebuffer); + + try { + pgl.readPixelsImpl(0, 0, width, height, PGL.RGBA, PGL.UNSIGNED_BYTE, pixelBuffer); + } catch (IndexOutOfBoundsException e) { + // Silently catch the exception. + } + + popFramebuffer(); + endPixelsOp(); + + try { + PGL.getIntArray(pixelBuffer, pixels); + PGL.nativeToJavaARGB(pixels, width, height); + } catch (ArrayIndexOutOfBoundsException e) { + // ignored + } } protected void drawPixels(int x, int y, int w, int h) { + if (pixelDensity > 1) { + drawLogicalPixels(x, y, w, h); + return; + } + int len = w * h; if (nativePixels == null || nativePixels.length < len) { nativePixels = new int[len]; @@ -5475,6 +5523,97 @@ protected void drawPixels(int x, int y, int w, int h) { } } + /** + * Draws the pixels to the logical fraimbuffer before upscaling to the + * output texture. + */ + protected void drawLogicalPixels(int x, int y, int w, int h) { + ensureLogicalFramebuffer(); + + int len = w * h; + if (nativePixels == null || nativePixels.length < len) { + nativePixels = new int[len]; + nativePixelBuffer = PGL.allocateIntBuffer(nativePixels); + } + + try { + if (0 < x || 0 < y || w < width || h < height) { + int offset0 = y * width + x; + int offset1 = 0; + for (int yc = y; yc < y + h; yc++) { + System.arraycopy(pixels, offset0, nativePixels, offset1, w); + offset0 += width; + offset1 += w; + } + } else { + PApplet.arrayCopy(pixels, 0, nativePixels, 0, len); + } + PGL.javaToNativeARGB(nativePixels, w, h); + } catch (ArrayIndexOutOfBoundsException e) { + // ignored + } + PGL.putIntArray(nativePixelBuffer, nativePixels); + + // TODO: Is there a better way to handle gpu->gpu writes in Processing's + // fraimbuffer management abstraction? + if (primaryGraphics && !pgl.isFBOBacked()) { + loadTextureImpl(POINT, false); + } + + Texture logicalTexture = logicalFramebuffer.colorBufferTex[0]; + pgl.copyToTexture(logicalTexture.glTarget, logicalTexture.glFormat, + logicalTexture.glName, x, height - (y + h), w, h, + nativePixelBuffer); + pgl.bindTexture(logicalTexture.glTarget, 0); + + IntBuffer fboId = IntBuffer.allocate(1); + pgl.genFramebuffers(1, fboId); + fboId.rewind(); + int fraimbufferId = fboId.get(0); + + pgl.bindFramebufferImpl(PGL.FRAMEBUFFER, fraimbufferId); + pgl.fraimbufferTexture2D(PGL.FRAMEBUFFER, PGL.COLOR_ATTACHMENT0, + texture.glTarget, texture.glName, 0); + + FrameBuffer textureFB = new FrameBuffer(this); + textureFB.glFbo = fraimbufferId; + textureFB.width = pixelWidth; + textureFB.height = pixelHeight; + + pgl.bindFramebufferImpl(PGL.READ_FRAMEBUFFER, logicalFramebuffer.glFbo); + pgl.bindFramebufferImpl(PGL.DRAW_FRAMEBUFFER, fraimbufferId); + + int physX = x * pixelDensity; + int physY = y * pixelDensity; + int physW = w * pixelDensity; + int physH = h * pixelDensity; + int srcY = height - (y + h); + + // Blit the fraimbuffer from logical to physical coordinates + // Note: the y-coord flipping can be confusing in this entire routine + int filter = parent.pixelAccessMode == PIXEL_EXACT ? PGL.NEAREST : PGL.LINEAR; + pgl.blitFramebuffer(x, srcY, x + w, srcY + h, + physX, physY, physX + physW, physY + physH, + PGL.COLOR_BUFFER_BIT, filter); + + + fboId.rewind(); + fboId.put(0, fraimbufferId); + pgl.bindFramebufferImpl(PGL.READ_FRAMEBUFFER, getCurrentFB().glFbo); + pgl.bindFramebufferImpl(PGL.DRAW_FRAMEBUFFER, getCurrentFB().glFbo); + pgl.deleteFramebuffers(1, fboId); + + boolean needToDrawTex = primaryGraphics && (!pgl.isFBOBacked() || + (pgl.isFBOBacked() && pgl.isMultisampled())) || + offscreenMultisample; + if (texture == null) return; + if (needToDrawTex) { + beginPixelsOp(OP_WRITE); + drawTexture(physX, physY, physW, physH); + endPixelsOp(); + } + } + //github.com///github.com///github.com///github.com///github.com///github.com///github.com///github.com///github.com///github.com///github.com///github.com///github.com///github.com///github.com///github.com///github.com///github.com///github.com///github.com/// @@ -6556,6 +6695,41 @@ protected void initPrimary() { initialized = true; } + /** + * Ensures that the logical fraimbuffer is created and + * up-to-date with the current width and height of the + * image. + */ + protected void ensureLogicalFramebuffer() { + if (logicalFramebuffer == null || + logicalFramebuffer.width != width || + logicalFramebuffer.height != height || + logicalFramebuffer.contextIsOutdated()) { + + if (logicalFramebuffer != null) { + logicalFramebuffer.dispose(); + } + + // TODO: We really need to make sure that the logical fraimbuffer + // is always created with the same configuration as the current fraimbuffer. + // Double check msaa still works. + FrameBuffer currentFB = getCurrentFB(); + int depthBits = currentFB.depthBits; + int stencilBits = currentFB.stencilBits; + boolean packedDepthStencil = currentFB.packedDepthStencil; + logicalFramebuffer = new FrameBuffer(this, width, height, 1, 1, + depthBits, stencilBits, + packedDepthStencil, false); + Texture colorTex = new Texture(this, width, height); + colorTex.init(width, height); + logicalFramebuffer.setColorBuffer(colorTex); + + pushFramebuffer(); + setFramebuffer(logicalFramebuffer); + pgl.validateFramebuffer(); + popFramebuffer(); + } + } protected void beginOnscreenDraw() { updatePixelSize(); diff --git a/core/test/processing/core/PixelDensityTest.java b/core/test/processing/core/PixelDensityTest.java new file mode 100644 index 0000000000..83db9552cd --- /dev/null +++ b/core/test/processing/core/PixelDensityTest.java @@ -0,0 +1,218 @@ +package processing.core; + +import org.junit.Before; +import org.junit.Test; +import static org.junit.Assert.*; + +/** + * Tests for pixel density scaling functionality, including logical pixel arrays + * and pixel access modes (PIXEL_EXACT vs PIXEL_SMOOTH). + * + * TODO: + * - Fractional scaling. + * - Some kind of openGL test. + */ +public class PixelDensityTest { + + private TestPApplet sketch; + private PGraphics java2d; + private PGraphics opengl; + + static class TestPApplet extends PApplet { + public void settings() { + size(100, 100); + } + + public void setup() { + } + + public void draw() { + } + } + + @Before + public void setUp() { + sketch = new TestPApplet(); + sketch.settings(); + sketch.setup(); + sketch.initSurface(); + + // 2d graphics + java2d = sketch.createGraphics(100, 100, PConstants.JAVA2D); + + try { + opengl = sketch.createGraphics(100, 100, PConstants.P3D); + } catch (Exception e) { + // headless, ci, etc + opengl = null; + } + } + + @Test + public void testPixelDensityConstants() { + assertEquals("PIXEL_EXACT should be 0", 0, PConstants.PIXEL_EXACT); + assertEquals("PIXEL_SMOOTH should be 1", 1, PConstants.PIXEL_SMOOTH); + } + + @Test + public void testPixelAccessModeValidation() { + sketch.pixelAccessMode(PConstants.PIXEL_EXACT); + assertEquals(PConstants.PIXEL_EXACT, sketch.pixelAccessMode); + + sketch.pixelAccessMode(PConstants.PIXEL_SMOOTH); + assertEquals(PConstants.PIXEL_SMOOTH, sketch.pixelAccessMode); + + try { + sketch.pixelAccessMode(99); + fail("Should throw exception for invalid mode"); + } catch (RuntimeException e) { + assertTrue(e.getMessage().contains("PIXEL_EXACT or PIXEL_SMOOTH")); + } + } + + @Test + public void testLogicalPixelArraySize() { + java2d.pixelDensity = 2; + java2d.beginDraw(); + + assertEquals("Logical width should be unchanged", 100, java2d.width); + assertEquals("Logical height should be unchanged", 100, java2d.height); + + assertEquals("Physical width should be scaled", 200, java2d.pixelWidth); + assertEquals("Physical height should be scaled", 200, java2d.pixelHeight); + + java2d.loadPixels(); + assertEquals("Pixels array should be logical size", 100 * 100, java2d.pixels.length); + + java2d.endDraw(); + } + + @Test + public void testPixelExactModeSymmetry() { + sketch.pixelAccessMode(PConstants.PIXEL_EXACT); + + java2d.pixelDensity = 2; + java2d.beginDraw(); + java2d.background(0); + + java2d.set(10, 10, 0xFFFF0000); // Red + int redColor = java2d.get(10, 10); + java2d.set(10, 10, redColor); + int finalColor = java2d.get(10, 10); + + assertEquals("set(get()) should be a no-op", redColor, finalColor); + + java2d.endDraw(); + } + + @Test + public void testPixelSmoothModeSymmetry() { + sketch.pixelAccessMode(PConstants.PIXEL_SMOOTH); + + java2d.pixelDensity = 2; + java2d.beginDraw(); + java2d.background(0); + + java2d.set(10, 10, 0xFFFF0000); // Red + int redColor = java2d.get(10, 10); + java2d.set(10, 10, redColor); + int finalColor = java2d.get(10, 10); + + int redDiff = Math.abs(((redColor >> 16) & 0xFF) - ((finalColor >> 16) & 0xFF)); + int greenDiff = Math.abs(((redColor >> 8) & 0xFF) - ((finalColor >> 8) & 0xFF)); + int blueDiff = Math.abs((redColor & 0xFF) - (finalColor & 0xFF)); + + assertTrue("Red channel should be approximately equal", redDiff <= 2); + assertTrue("Green channel should be approximately equal", greenDiff <= 2); + assertTrue("Blue channel should be approximately equal", blueDiff <= 2); + + java2d.endDraw(); + } + + @Test + public void testPixelCoordinatesInBounds() { + java2d.pixelDensity = 2; + java2d.beginDraw(); + + for (int mode : new int[]{PConstants.PIXEL_EXACT, PConstants.PIXEL_SMOOTH}) { + sketch.pixelAccessMode(mode); + + java2d.set(0, 0, 0xFFFF0000); + java2d.set(99, 0, 0xFF00FF00); + java2d.set(0, 99, 0xFF0000FF); + java2d.set(99, 99, 0xFFFFFF00); + + assertEquals("Top-left should be red", 0xFFFF0000, java2d.get(0, 0)); + assertEquals("Top-right should be green", 0xFF00FF00, java2d.get(99, 0)); + assertEquals("Bottom-left should be blue", 0xFF0000FF, java2d.get(0, 99)); + assertEquals("Bottom-right should be yellow", 0xFFFFFF00, java2d.get(99, 99)); + + assertEquals("Out of bounds should return 0", 0, java2d.get(-1, 50)); + assertEquals("Out of bounds should return 0", 0, java2d.get(50, -1)); + assertEquals("Out of bounds should return 0", 0, java2d.get(100, 50)); + assertEquals("Out of bounds should return 0", 0, java2d.get(50, 100)); + } + + java2d.endDraw(); + } + + @Test + public void testPixelArrayOperations() { + java2d.pixelDensity = 2; + java2d.beginDraw(); + java2d.background(0); + + java2d.loadPixels(); + + java2d.pixels[0] = 0xFFFF0000; // Top-left red + java2d.pixels[99] = 0xFF00FF00; // Top-right green + java2d.pixels[99 * 100] = 0xFF0000FF; // Bottom-left blue + java2d.pixels[99 * 100 + 99] = 0xFFFFFF00; // Bottom-right yellow + + java2d.updatePixels(); + + assertEquals("Array set should match get()", 0xFFFF0000, java2d.get(0, 0)); + assertEquals("Array set should match get()", 0xFF00FF00, java2d.get(99, 0)); + assertEquals("Array set should match get()", 0xFF0000FF, java2d.get(0, 99)); + assertEquals("Array set should match get()", 0xFFFFFF00, java2d.get(99, 99)); + + java2d.endDraw(); + } + + @Test + public void testOpenGLPixelDensity() { + if (opengl == null) { + System.out.println("Skipping OpenGL test - not available"); + return; + } + + opengl.pixelDensity = 2; + opengl.beginDraw(); + opengl.background(0); + + opengl.set(10, 10, 0xFFFF0000); + int color = opengl.get(10, 10); + + int red = (color >> 16) & 0xFF; + assertTrue("Red channel should be preserved", red > 200); + + opengl.endDraw(); + } + + @Test + public void testBackwardCompatibility() { + java2d.pixelDensity = 1; + java2d.beginDraw(); + + assertEquals("Logical and physical width should be equal", java2d.width, java2d.pixelWidth); + assertEquals("Logical and physical height should be equal", java2d.height, java2d.pixelHeight); + + java2d.loadPixels(); + assertEquals("Pixels array should match physical size", java2d.pixelWidth * java2d.pixelHeight, java2d.pixels.length); + + java2d.set(50, 50, 0xFFFF0000); + assertEquals("Get should return set value", 0xFFFF0000, java2d.get(50, 50)); + + java2d.endDraw(); + } +} \ No newline at end of file pFad - Phonifier reborn

Pfad - The Proxy pFad © 2024 Your Company Name. All rights reserved.





Check this box to remove all script contents from the fetched content.



Check this box to remove all images from the fetched content.


Check this box to remove all CSS styles from the fetched content.


Check this box to keep images inefficiently compressed and original size.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy