RgbZonePlateRenderer.java

package org.fresnel.optics;

import java.awt.image.BufferedImage;
import java.awt.image.WritableRaster;

/**
 * Renders a single zone-plate design at three wavelengths (R / G / B) and combines
 * them into one RGB image. Useful for visualising chromatic behaviour and for
 * fabricating colour-separated overlays.
 *
 * <p>The same geometric parameters (aperture, focal length, DPI, off-axis target)
 * are reused for all three channels; only {@code wavelengthNm} varies. The output
 * is always {@code TYPE_INT_RGB}.
 */
public final class RgbZonePlateRenderer {

    private RgbZonePlateRenderer() {}

    /**
     * Render an RGB zone-plate composite.
     *
     * @param base     base parameters; its wavelength is ignored for the channels
     * @param redNm    red channel design wavelength, nm (typical 630)
     * @param greenNm  green channel design wavelength, nm (typical 532)
     * @param blueNm   blue channel design wavelength, nm (typical 450)
     */
    public static RenderResult render(SingleZonePlateParameters base,
                                      double redNm, double greenNm, double blueNm) {
        if (redNm <= 0 || greenNm <= 0 || blueNm <= 0)
            throw new IllegalArgumentException("wavelengths must be > 0");
        RenderResult r = ZonePlateRenderer.render(withWavelength(base, redNm));
        RenderResult g = ZonePlateRenderer.render(withWavelength(base, greenNm));
        RenderResult b = ZonePlateRenderer.render(withWavelength(base, blueNm));
        BufferedImage rImg = r.image();
        BufferedImage gImg = g.image();
        BufferedImage bImg = b.image();
        int w = rImg.getWidth();
        int h = rImg.getHeight();
        if (gImg.getWidth() != w || bImg.getWidth() != w
                || gImg.getHeight() != h || bImg.getHeight() != h) {
            throw new IllegalStateException("channel sizes differ — should not happen");
        }
        BufferedImage rgb = new BufferedImage(w, h, BufferedImage.TYPE_INT_RGB);
        WritableRaster rrR = rImg.getRaster();
        WritableRaster rrG = gImg.getRaster();
        WritableRaster rrB = bImg.getRaster();
        int[] rRow = new int[w];
        int[] gRow = new int[w];
        int[] bRow = new int[w];
        int[] outRow = new int[w];
        for (int y = 0; y < h; y++) {
            rrR.getSamples(0, y, w, 1, 0, rRow);
            rrG.getSamples(0, y, w, 1, 0, gRow);
            rrB.getSamples(0, y, w, 1, 0, bRow);
            for (int x = 0; x < w; x++) {
                outRow[x] = (rRow[x] << 16) | (gRow[x] << 8) | bRow[x];
            }
            rgb.setRGB(0, y, w, 1, outRow, 0, w);
        }
        return new RenderResult(rgb, r.pixelSizeMm());
    }

    private static SingleZonePlateParameters withWavelength(SingleZonePlateParameters p, double nm) {
        return new SingleZonePlateParameters(
                p.apertureDiameterMm(), p.focalLengthMm(), nm, p.dpi(),
                p.targetOffsetXmm(), p.targetOffsetYmm(),
                p.maskType(), p.polarity());
    }
}