rename library-image to api-image

This commit is contained in:
dfsek committed 2026-01-02 22:24:42 -07:00
1 parent b6d42fdcc1
commit ac1258d233
46 files changed
+6 -6

No files matched your search

+7
View File
@@ -0,0 +1,7 @@
version = version("1.1.0")
dependencies {
compileOnlyApi(project(":common:addons:addon-loader-manifest"))
}
@@ -0,0 +1,89 @@
package com.dfsek.terra.addons.image;
import com.dfsek.seismic.type.sampler.Sampler;
import com.dfsek.tectonic.api.config.template.object.ObjectTemplate;
import java.util.function.Supplier;
import com.dfsek.terra.addons.image.colorsampler.ColorSampler;
import com.dfsek.terra.addons.image.config.ColorLoader;
import com.dfsek.terra.addons.image.config.ColorLoader.ColorString;
import com.dfsek.terra.addons.image.config.ImageLibraryPackConfigTemplate;
import com.dfsek.terra.addons.image.config.colorsampler.ConstantColorSamplerTemplate;
import com.dfsek.terra.addons.image.config.colorsampler.image.SingleImageColorSamplerTemplate;
import com.dfsek.terra.addons.image.config.colorsampler.image.TileImageColorSamplerTemplate;
import com.dfsek.terra.addons.image.config.colorsampler.mutate.RotateColorSamplerTemplate;
import com.dfsek.terra.addons.image.config.colorsampler.mutate.TranslateColorSamplerTemplate;
import com.dfsek.terra.addons.image.config.image.ImageTemplate;
import com.dfsek.terra.addons.image.config.image.StitchedImageTemplate;
import com.dfsek.terra.addons.image.config.sampler.ChannelSamplerTemplate;
import com.dfsek.terra.addons.image.config.sampler.DistanceTransformSamplerTemplate;
import com.dfsek.terra.addons.image.image.Image;
import com.dfsek.terra.addons.image.operator.DistanceTransform;
import com.dfsek.terra.addons.manifest.api.AddonInitializer;
import com.dfsek.terra.api.Platform;
import com.dfsek.terra.api.addon.BaseAddon;
import com.dfsek.terra.api.config.ConfigPack;
import com.dfsek.terra.api.event.events.config.pack.ConfigPackPreLoadEvent;
import com.dfsek.terra.api.event.functional.FunctionalEventHandler;
import com.dfsek.terra.api.inject.annotations.Inject;
import com.dfsek.terra.api.registry.CheckedRegistry;
import com.dfsek.terra.api.util.reflection.TypeKey;
public class ImageLibraryAddon implements AddonInitializer {
public static final TypeKey<Supplier<ObjectTemplate<Image>>> IMAGE_REGISTRY_KEY = new TypeKey<>() {
};
public static final TypeKey<Supplier<ObjectTemplate<ColorSampler>>> COLOR_PICKER_REGISTRY_KEY = new TypeKey<>() {
};
public static final TypeKey<Supplier<ObjectTemplate<Sampler>>> NOISE_SAMPLER_TOKEN = new TypeKey<>() {
};
@Inject
private Platform platform;
@Inject
private BaseAddon addon;
@Override
public void initialize() {
platform.getEventManager()
.getHandler(FunctionalEventHandler.class)
.register(addon, ConfigPackPreLoadEvent.class)
.priority(10)
.then(event -> {
ImageLibraryPackConfigTemplate config = event.loadTemplate(new ImageLibraryPackConfigTemplate());
event.getPack().getContext().put(config);
})
.then(event -> {
ConfigPack pack = event.getPack();
CheckedRegistry<Supplier<ObjectTemplate<Image>>> imageRegistry = pack.getOrCreateRegistry(IMAGE_REGISTRY_KEY);
imageRegistry.register(addon.key("BITMAP"), () -> new ImageTemplate(pack));
imageRegistry.register(addon.key("STITCHED_BITMAP"), () -> new StitchedImageTemplate(pack));
})
.then(event -> {
event.getPack()
.applyLoader(DistanceTransform.CostFunction.class,
(type, o, loader, depthTracker) -> DistanceTransform.CostFunction.valueOf((String) o))
.applyLoader(DistanceTransform.Normalization.class,
(type, o, loader, depthTracker) -> DistanceTransform.Normalization.valueOf((String) o))
.applyLoader(ColorString.class, new ColorLoader());
CheckedRegistry<Supplier<ObjectTemplate<Sampler>>> noiseRegistry = event.getPack().getOrCreateRegistry(
NOISE_SAMPLER_TOKEN);
noiseRegistry.register(addon.key("DISTANCE_TRANSFORM"), DistanceTransformSamplerTemplate::new);
noiseRegistry.register(addon.key("CHANNEL"), ChannelSamplerTemplate::new);
})
.then(event -> {
CheckedRegistry<Supplier<ObjectTemplate<ColorSampler>>> colorSamplerRegistry = event.getPack().getOrCreateRegistry(
COLOR_PICKER_REGISTRY_KEY);
colorSamplerRegistry.register(addon.key("SINGLE_IMAGE"), SingleImageColorSamplerTemplate::new);
colorSamplerRegistry.register(addon.key("TILED_IMAGE"), TileImageColorSamplerTemplate::new);
colorSamplerRegistry.register(addon.key("COLOR"), ConstantColorSamplerTemplate::new);
colorSamplerRegistry.register(addon.key("ROTATE"), RotateColorSamplerTemplate::new);
colorSamplerRegistry.register(addon.key("TRANSLATE"), TranslateColorSamplerTemplate::new);
});
}
}
@@ -0,0 +1,13 @@
package com.dfsek.terra.addons.image.colorsampler;
@FunctionalInterface
public interface ColorSampler {
/**
* @param x World x coordinate
* @param z World z coordinate
*
* @return Integer representing a web color
*/
int apply(int x, int z);
}
@@ -0,0 +1,29 @@
package com.dfsek.terra.addons.image.colorsampler.image;
import com.dfsek.terra.addons.image.colorsampler.ColorSampler;
import com.dfsek.terra.addons.image.colorsampler.image.transform.ImageTransformation;
import com.dfsek.terra.addons.image.image.Image;
public class SingleImageColorSampler implements ColorSampler {
private final Image image;
private final ColorSampler fallback;
private final ImageTransformation transformation;
public SingleImageColorSampler(Image image, ColorSampler fallback, ImageTransformation transformation) {
this.image = image;
this.fallback = fallback;
this.transformation = transformation;
}
@Override
public int apply(int x, int z) {
var nx = transformation.transformX(image, x);
var nz = transformation.transformZ(image, z);
if(nx < 0 || nz < 0 || nx >= image.getWidth() || nz >= image.getHeight()) return fallback.apply(x, z);
return image.getRGB(nx, nz);
}
}
@@ -0,0 +1,25 @@
package com.dfsek.terra.addons.image.colorsampler.image;
import com.dfsek.terra.addons.image.colorsampler.ColorSampler;
import com.dfsek.terra.addons.image.colorsampler.image.transform.ImageTransformation;
import com.dfsek.terra.addons.image.image.Image;
public class TileImageColorSampler implements ColorSampler {
private final Image image;
private final ImageTransformation transformation;
public TileImageColorSampler(Image image, ImageTransformation transformation) {
this.image = image;
this.transformation = transformation;
}
@Override
public int apply(int x, int z) {
x = transformation.transformX(image, x);
z = transformation.transformZ(image, z);
return image.getRGB(Math.floorMod(x, image.getWidth()), Math.floorMod(z, image.getHeight()));
}
}
@@ -0,0 +1,30 @@
package com.dfsek.terra.addons.image.colorsampler.image.transform;
import com.dfsek.terra.addons.image.image.Image;
public enum Alignment implements ImageTransformation {
NONE() {
@Override
public int transformX(Image image, int x) {
return x;
}
@Override
public int transformZ(Image image, int z) {
return z;
}
},
CENTER {
@Override
public int transformX(Image image, int x) {
return x + image.getWidth() / 2;
}
@Override
public int transformZ(Image image, int z) {
return z + image.getHeight() / 2;
}
};
}
@@ -0,0 +1,11 @@
package com.dfsek.terra.addons.image.colorsampler.image.transform;
import com.dfsek.terra.addons.image.image.Image;
public interface ImageTransformation {
int transformX(Image image, int x);
int transformZ(Image image, int z);
}
@@ -0,0 +1,62 @@
package com.dfsek.terra.addons.image.colorsampler.mutate;
import com.dfsek.seismic.math.trigonometry.TrigonometryFunctions;
import com.dfsek.terra.addons.image.colorsampler.ColorSampler;
public class RotateColorSampler implements ColorSampler {
private final ColorSampler sampler;
private final double radians;
private final RotationMethod rotationMethod;
public RotateColorSampler(ColorSampler sampler, double degrees) {
this.sampler = sampler;
double normalizedDegrees = degrees % 360.0;
if(normalizedDegrees < 0) normalizedDegrees += 360.0;
if(normalizedDegrees == 0.0)
rotationMethod = RotationMethod.DEG_0;
else if(normalizedDegrees == 90.0)
rotationMethod = RotationMethod.DEG_90;
else if(normalizedDegrees == 180.0)
rotationMethod = RotationMethod.DEG_180;
else if(normalizedDegrees == 270.0)
rotationMethod = RotationMethod.DEG_270;
else
rotationMethod = RotationMethod.RAD_ANY;
this.radians = Math.toRadians(degrees);
}
@Override
public int apply(int x, int z) {
int rx = switch(rotationMethod) {
case DEG_0 -> x;
case DEG_90 -> -z;
case DEG_180 -> -x;
case DEG_270 -> z;
case RAD_ANY -> (int) (x * TrigonometryFunctions.cos(radians) - z * TrigonometryFunctions.sin(radians));
};
int rz = switch(rotationMethod) {
case DEG_0 -> z;
case DEG_90 -> x;
case DEG_180 -> -z;
case DEG_270 -> -x;
case RAD_ANY -> (int) (z * TrigonometryFunctions.cos(radians) + x * TrigonometryFunctions.sin(radians));
};
return sampler.apply(rx, rz);
}
private enum RotationMethod {
DEG_0,
DEG_90,
DEG_180,
DEG_270,
RAD_ANY,
}
}
@@ -0,0 +1,21 @@
package com.dfsek.terra.addons.image.colorsampler.mutate;
import com.dfsek.terra.addons.image.colorsampler.ColorSampler;
public class TranslateColorSampler implements ColorSampler {
private final ColorSampler sampler;
private final int translateX, translateZ;
public TranslateColorSampler(ColorSampler sampler, int translateX, int translateZ) {
this.sampler = sampler;
this.translateX = translateX;
this.translateZ = translateZ;
}
@Override
public int apply(int x, int z) {
return sampler.apply(x - translateX, z - translateZ);
}
}
@@ -0,0 +1,92 @@
package com.dfsek.terra.addons.image.config;
import com.dfsek.tectonic.api.depth.DepthTracker;
import com.dfsek.tectonic.api.exception.LoadException;
import com.dfsek.tectonic.api.loader.ConfigLoader;
import com.dfsek.tectonic.api.loader.type.TypeLoader;
import org.jetbrains.annotations.NotNull;
import java.lang.reflect.AnnotatedType;
import com.dfsek.terra.addons.image.config.ColorLoader.ColorString;
import com.dfsek.terra.addons.image.util.ColorUtil;
public class ColorLoader implements TypeLoader<ColorString> {
@Override
public ColorString load(@NotNull AnnotatedType annotatedType, @NotNull Object o, @NotNull ConfigLoader configLoader,
DepthTracker depthTracker) throws LoadException {
return new ColorString((String) o);
}
public static class ColorString {
private final int argb;
public ColorString(String string) throws IllegalArgumentException {
this.argb = parse(string);
}
private static int parse(String string) throws IllegalArgumentException {
if(string.length() == 0)
throw new IllegalArgumentException("Empty string cannot be parsed as a valid color");
String[] split = string.split(",");
if(split.length == 1)
return parseHex(string);
else if(split.length == 3)
return parseChannels("255", split[0], split[1], split[2]);
else if(split.length == 4)
return parseChannels(split[0], split[1], split[2], split[3]);
else
throw new IllegalArgumentException("Invalid channels provided, required format RED,GREEN,BLUE or ALPHA,RED,GREEN,BLUE");
}
private static int parseHex(String hex) throws IllegalArgumentException {
if(hex.startsWith("#"))
hex = hex.substring(1);
int alpha = 255;
int red = 0;
int green = 0;
int blue = 0;
try {
if(hex.length() == 8) {
alpha = Integer.parseInt(hex.substring(0, 2), 16);
hex = hex.substring(2);
}
if(hex.length() != 6)
throw new IllegalArgumentException("Invalid color channels, required format AARRGGBB or RRGGBB");
red = Integer.parseInt(hex.substring(0, 2), 16);
green = Integer.parseInt(hex.substring(2, 4), 16);
blue = Integer.parseInt(hex.substring(4, 6), 16);
return ColorUtil.argbValidated(alpha, red, green, blue);
} catch(NumberFormatException e) {
throw new IllegalArgumentException("Failed to parse hex color", e);
}
}
private static int parseChannels(String alpha, String red, String green, String blue) throws IllegalArgumentException {
try {
int a = Integer.decode(alpha);
int r = Integer.decode(red);
int g = Integer.decode(green);
int b = Integer.decode(blue);
return ColorUtil.argbValidated(a, r, g, b);
} catch(NumberFormatException e) {
throw new IllegalArgumentException("Invalid channel value", e);
}
}
public int getColor() {
return argb;
}
}
}
@@ -0,0 +1,39 @@
package com.dfsek.terra.addons.image.config;
import com.dfsek.tectonic.api.config.template.ConfigTemplate;
import com.dfsek.tectonic.api.config.template.annotations.Default;
import com.dfsek.tectonic.api.config.template.annotations.Description;
import com.dfsek.tectonic.api.config.template.annotations.Value;
import com.dfsek.terra.api.properties.Properties;
public class ImageLibraryPackConfigTemplate implements ConfigTemplate, Properties {
// TODO - These would be better as plugin wide config parameters in config.yml
@Value("images.cache.load-on-use")
@Description("If set to true, images will load into memory upon use rather than on pack load.")
@Default
private boolean loadOnUse = false;
@Value("images.cache.timeout")
@Description("How many seconds to keep images loaded in the image cache for. " +
"If set to a number greater than 0, images will be removed from memory if not used after the timeout, otherwise images " +
"will stay loaded in memory. " +
"Setting the timeout to greater than 0 will trade decreased memory consumption when not performing any image reads for a" +
" period of time for extra processing time required to perform cache lookups.")
@Default
private int cacheTimeout = 0;
public boolean loadOnUse() {
return loadOnUse;
}
public boolean unloadOnTimeout() {
return cacheTimeout > 0;
}
public int getCacheTimeout() {
return cacheTimeout;
}
}
@@ -0,0 +1,19 @@
package com.dfsek.terra.addons.image.config.colorsampler;
import com.dfsek.tectonic.api.config.template.annotations.Value;
import com.dfsek.tectonic.api.config.template.object.ObjectTemplate;
import com.dfsek.terra.addons.image.colorsampler.ColorSampler;
import com.dfsek.terra.addons.image.config.ColorLoader.ColorString;
public class ConstantColorSamplerTemplate implements ObjectTemplate<ColorSampler> {
@Value("color")
private ColorString color;
@Override
public ColorSampler get() {
return ((x, z) -> color.getColor());
}
}
@@ -0,0 +1,21 @@
package com.dfsek.terra.addons.image.config.colorsampler.image;
import com.dfsek.tectonic.api.config.template.annotations.Default;
import com.dfsek.tectonic.api.config.template.annotations.Value;
import com.dfsek.tectonic.api.config.template.object.ObjectTemplate;
import com.dfsek.terra.addons.image.colorsampler.ColorSampler;
import com.dfsek.terra.addons.image.colorsampler.image.transform.Alignment;
import com.dfsek.terra.addons.image.image.Image;
public abstract class ImageColorSamplerTemplate implements ObjectTemplate<ColorSampler> {
@Value("image")
protected Image image;
@Value("align")
@Default
protected Alignment alignment = Alignment.NONE;
}
@@ -0,0 +1,17 @@
package com.dfsek.terra.addons.image.config.colorsampler.image;
import com.dfsek.tectonic.api.config.template.annotations.Value;
import com.dfsek.terra.addons.image.colorsampler.ColorSampler;
import com.dfsek.terra.addons.image.colorsampler.image.SingleImageColorSampler;
public class SingleImageColorSamplerTemplate extends ImageColorSamplerTemplate {
@Value("outside-sampler")
private ColorSampler fallback;
@Override
public ColorSampler get() {
return new SingleImageColorSampler(image, fallback, alignment);
}
}
@@ -0,0 +1,13 @@
package com.dfsek.terra.addons.image.config.colorsampler.image;
import com.dfsek.terra.addons.image.colorsampler.ColorSampler;
import com.dfsek.terra.addons.image.colorsampler.image.TileImageColorSampler;
public class TileImageColorSamplerTemplate extends ImageColorSamplerTemplate {
@Override
public ColorSampler get() {
return new TileImageColorSampler(image, alignment);
}
}
@@ -0,0 +1,13 @@
package com.dfsek.terra.addons.image.config.colorsampler.mutate;
import com.dfsek.tectonic.api.config.template.annotations.Value;
import com.dfsek.tectonic.api.config.template.object.ObjectTemplate;
import com.dfsek.terra.addons.image.colorsampler.ColorSampler;
public abstract class MutateColorSamplerTemplate implements ObjectTemplate<ColorSampler> {
@Value("color-sampler")
protected ColorSampler sampler;
}
@@ -0,0 +1,18 @@
package com.dfsek.terra.addons.image.config.colorsampler.mutate;
import com.dfsek.tectonic.api.config.template.annotations.Value;
import com.dfsek.terra.addons.image.colorsampler.ColorSampler;
import com.dfsek.terra.addons.image.colorsampler.mutate.RotateColorSampler;
public class RotateColorSamplerTemplate extends MutateColorSamplerTemplate {
@Value("angle")
private double degrees;
@Override
public ColorSampler get() {
return new RotateColorSampler(sampler, degrees);
}
}
@@ -0,0 +1,21 @@
package com.dfsek.terra.addons.image.config.colorsampler.mutate;
import com.dfsek.tectonic.api.config.template.annotations.Value;
import com.dfsek.terra.addons.image.colorsampler.ColorSampler;
import com.dfsek.terra.addons.image.colorsampler.mutate.TranslateColorSampler;
public class TranslateColorSamplerTemplate extends MutateColorSamplerTemplate {
@Value("x")
private int translateX;
@Value("z")
private int translateZ;
@Override
public ColorSampler get() {
return new TranslateColorSampler(sampler, translateX, translateZ);
}
}
@@ -0,0 +1,16 @@
package com.dfsek.terra.addons.image.config.converter;
import com.dfsek.terra.addons.image.converter.ClosestMatchColorConverter;
import com.dfsek.terra.addons.image.converter.ColorConverter;
import com.dfsek.terra.addons.image.converter.mapping.ColorMapping;
public abstract class ClosestColorConverterTemplate<T> implements ColorConverterTemplate<T> {
protected abstract ColorMapping<T> getMapping();
@Override
public ColorConverter<T> get() {
return new ClosestMatchColorConverter<T>(getMapping().get());
}
}
@@ -0,0 +1,9 @@
package com.dfsek.terra.addons.image.config.converter;
import com.dfsek.tectonic.api.config.template.object.ObjectTemplate;
import com.dfsek.terra.addons.image.converter.ColorConverter;
public interface ColorConverterTemplate<T> extends ObjectTemplate<ColorConverter<T>> {
}
@@ -0,0 +1,20 @@
package com.dfsek.terra.addons.image.config.converter;
import com.dfsek.terra.addons.image.converter.ColorConverter;
import com.dfsek.terra.addons.image.converter.ExactColorConverter;
import com.dfsek.terra.addons.image.converter.mapping.ColorMapping;
public abstract class ExactColorConverterTemplate<T> implements ColorConverterTemplate<T> {
protected abstract ColorMapping<T> getMapping();
protected abstract T getFallback();
protected abstract boolean ignoreAlpha();
@Override
public ColorConverter<T> get() {
return new ExactColorConverter<T>(getMapping().get(), getFallback(), ignoreAlpha());
}
}
@@ -0,0 +1,56 @@
package com.dfsek.terra.addons.image.config.image;
import com.github.benmanes.caffeine.cache.Caffeine;
import com.github.benmanes.caffeine.cache.LoadingCache;
import com.github.benmanes.caffeine.cache.Scheduler;
import javax.imageio.ImageIO;
import java.io.IOException;
import java.io.InputStream;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.concurrent.TimeUnit;
import com.dfsek.terra.addons.image.config.ImageLibraryPackConfigTemplate;
import com.dfsek.terra.addons.image.image.BufferedImageWrapper;
import com.dfsek.terra.addons.image.image.Image;
import com.dfsek.terra.addons.image.image.SuppliedImage;
import com.dfsek.terra.api.config.ConfigPack;
import com.dfsek.terra.api.properties.Properties;
import com.dfsek.terra.api.util.generic.Memo;
import static com.dfsek.terra.api.util.cache.CacheUtils.CACHE_EXECUTOR;
/*
* Cache prevents configs from loading the same image multiple times into memory
*/
record ImageCache(LoadingCache<String, Image> cache) implements Properties {
public static Image load(String path, ConfigPack pack) throws IOException {
ImageLibraryPackConfigTemplate config = pack.getContext().get(ImageLibraryPackConfigTemplate.class);
ImageCache images;
if(!pack.getContext().has(ImageCache.class)) {
var cacheBuilder = Caffeine.newBuilder().executor(CACHE_EXECUTOR).scheduler(Scheduler.systemScheduler());
if(config.unloadOnTimeout()) cacheBuilder.expireAfterAccess(config.getCacheTimeout(), TimeUnit.SECONDS);
images = new ImageCache(cacheBuilder.build(s -> loadImage(s, pack.getRootPath())));
pack.getContext().put(images);
} else images = pack.getContext().get(ImageCache.class);
if(config.loadOnUse()) {
if(config.unloadOnTimeout()) { // Grab directly from cache if images are to unload on timeout
return new SuppliedImage(() -> images.cache.get(path));
} else {
// If images do not time out, image can be lazily loaded once instead of performing cache lookups for each image operation
Memo<Image> lazyImage = Memo.lazy(() -> images.cache.get(path));
return new SuppliedImage(lazyImage::value);
}
}
return images.cache.get(path);
}
private static Image loadImage(String path, Path directory) throws IOException {
InputStream is = Files.newInputStream(directory.resolve(path));
return new BufferedImageWrapper(ImageIO.read(is));
}
}
@@ -0,0 +1,30 @@
package com.dfsek.terra.addons.image.config.image;
import com.dfsek.tectonic.api.config.template.annotations.Value;
import com.dfsek.tectonic.api.config.template.object.ObjectTemplate;
import java.io.IOException;
import com.dfsek.terra.addons.image.image.Image;
import com.dfsek.terra.api.config.ConfigPack;
public class ImageTemplate implements ObjectTemplate<Image> {
private final ConfigPack pack;
@Value("path")
private String path;
public ImageTemplate(ConfigPack pack) {
this.pack = pack;
}
@Override
public Image get() {
try {
return ImageCache.load(path, pack);
} catch(IOException e) {
throw new RuntimeException(e);
}
}
}
@@ -0,0 +1,68 @@
package com.dfsek.terra.addons.image.config.image;
import com.dfsek.tectonic.api.config.template.ValidatedConfigTemplate;
import com.dfsek.tectonic.api.config.template.annotations.Default;
import com.dfsek.tectonic.api.config.template.annotations.Value;
import com.dfsek.tectonic.api.config.template.object.ObjectTemplate;
import com.dfsek.tectonic.api.exception.ValidationException;
import java.io.IOException;
import com.dfsek.terra.addons.image.image.Image;
import com.dfsek.terra.addons.image.image.StitchedImage;
import com.dfsek.terra.api.config.ConfigPack;
public class StitchedImageTemplate implements ObjectTemplate<Image>, ValidatedConfigTemplate {
private final ConfigPack pack;
@Value("path-format")
private String path;
@Value("rows")
private int rows;
@Value("columns")
private int cols;
@Value("zero-indexed")
@Default
private boolean zeroIndexed = false;
public StitchedImageTemplate(ConfigPack pack) {
this.pack = pack;
}
@Override
public Image get() {
Image[][] grid = new Image[rows][cols];
for(int i = 0; i < rows; i++) {
for(int j = 0; j < cols; j++) {
try {
grid[i][j] = ImageCache.load(getFormattedPath(i, j), pack);
} catch(IOException e) {
throw new RuntimeException(e);
}
}
}
return new StitchedImage(grid, zeroIndexed);
}
private String getFormattedPath(int row, int column) {
if(!zeroIndexed) {
row++;
column++;
}
return path.replaceFirst("\\{row}", String.valueOf(row)).replaceFirst("\\{column}", String.valueOf(column));
}
@Override
public boolean validate() throws ValidationException {
if(!path.contains("{row}"))
throw new ValidationException("Path format does not contain sequence '{row}'");
if(!path.contains("{column}"))
throw new ValidationException("Path format does not contain sequence '{column}'");
if(rows < 1)
throw new ValidationException("Must have at least one row");
if(cols < 1)
throw new ValidationException("Must have at least one column");
return true;
}
}
@@ -0,0 +1,41 @@
package com.dfsek.terra.addons.image.config.sampler;
import com.dfsek.seismic.type.sampler.Sampler;
import com.dfsek.tectonic.api.config.template.annotations.Default;
import com.dfsek.tectonic.api.config.template.annotations.Value;
import com.dfsek.tectonic.api.config.template.object.ObjectTemplate;
import com.dfsek.terra.addons.image.colorsampler.ColorSampler;
import com.dfsek.terra.addons.image.sampler.ChannelSampler;
import com.dfsek.terra.addons.image.util.ColorUtil.Channel;
public class ChannelSamplerTemplate implements ObjectTemplate<Sampler> {
@Value("color-sampler")
private ColorSampler colorSampler;
@Value("channel")
private Channel channel;
/*
* If the channel should be normalized to range [-1, 1] or not
*/
@Value("normalize")
@Default
private boolean normalize = true;
/*
* Whether to multiply color channels by the alpha channel or not. If users
* are expecting pixel transparency to reduce the output value then this should
* be set to true.
*/
@Value("premultiply")
@Default
private boolean premultiply = false;
@Override
public Sampler get() {
return new ChannelSampler(colorSampler, channel, normalize, premultiply);
}
}
@@ -0,0 +1,75 @@
package com.dfsek.terra.addons.image.config.sampler;
import com.dfsek.seismic.type.sampler.Sampler;
import com.dfsek.tectonic.api.config.template.annotations.Default;
import com.dfsek.tectonic.api.config.template.annotations.Value;
import com.dfsek.tectonic.api.config.template.object.ObjectTemplate;
import com.dfsek.terra.addons.image.image.Image;
import com.dfsek.terra.addons.image.operator.DistanceTransform;
import com.dfsek.terra.addons.image.operator.DistanceTransform.CostFunction;
import com.dfsek.terra.addons.image.operator.DistanceTransform.Normalization;
import com.dfsek.terra.addons.image.util.ColorUtil.Channel;
public class DistanceTransformSamplerTemplate implements ObjectTemplate<Sampler> {
@Value("image")
private Image image;
/**
* The threshold value applied to the channel specified in the 'channel' parameter that splits
* the image into a binary image. This parameter is only used for cost functions that utilize
* a binary image.
*/
@Value("threshold")
@Default
private int threshold = 127;
/**
* If set to true, distances calculated will be clamped to stay above the largest
* distance calculated on the edges of the image. This ensures output values do not
* appear to be cut off at the image boundaries. It is recommended to leave padding
* around the image if this is in use, such that larger evaluated distances do not
* get cut out by smaller evaluated distances close to borders. Doing so will yield
* better results.
*/
@Value("clamp-to-max-edge")
@Default
private boolean clampToEdge = false;
/**
* The target channel to run distance calculations on.
*/
@Value("channel")
@Default
private Channel channel = Channel.GRAYSCALE;
/**
* The method of image processing applied to the specified image prior to calculating
* distances.
*/
@Value("cost-function")
@Default
private CostFunction costFunction = CostFunction.Channel;
/**
* Inverts the resulting binary image that may be used as a cost function.
*/
@Value("invert-threshold")
@Default
private boolean invertThreshold = false;
/**
* How the final distance calculation should be redistributed.
*/
@Value("normalization")
@Default
private Normalization normalization = Normalization.None;
@Override
public Sampler get() {
return new DistanceTransform.Noise(new DistanceTransform(image, channel, threshold, clampToEdge, costFunction, invertThreshold),
normalization);
}
}
@@ -0,0 +1,41 @@
package com.dfsek.terra.addons.image.converter;
import java.util.Map;
import com.dfsek.terra.addons.image.util.ColorUtil;
public class ClosestMatchColorConverter<T> implements ColorConverter<T> {
private final Map<Integer, T> map;
private final Integer[] colors;
public ClosestMatchColorConverter(Map<Integer, T> map) {
this.map = map;
this.colors = map.keySet().toArray(new Integer[0]);
}
@Override
public T apply(int color) {
int closest = 0;
int smallestDistance = Integer.MAX_VALUE;
for(int compare : colors) {
if(color == compare) {
closest = compare;
break;
}
int distance = ColorUtil.distance(color, compare);
if(distance < smallestDistance) {
smallestDistance = distance;
closest = compare;
}
}
return map.get(closest);
}
@Override
public Iterable<T> getEntries() {
return map.values();
}
}
@@ -0,0 +1,8 @@
package com.dfsek.terra.addons.image.converter;
public interface ColorConverter<T> {
T apply(int color);
Iterable<T> getEntries();
}
@@ -0,0 +1,42 @@
package com.dfsek.terra.addons.image.converter;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import com.dfsek.terra.addons.image.util.ColorUtil;
import com.dfsek.terra.addons.image.util.MapUtil;
public class ExactColorConverter<T> implements ColorConverter<T> {
private final Map<Integer, T> map;
private final T fallback;
private final boolean ignoreAlpha;
public ExactColorConverter(Map<Integer, T> map, T fallback, boolean ignoreAlpha) {
if(ignoreAlpha) {
map = MapUtil.mapKeys(map, ColorUtil::zeroAlpha);
}
this.map = map;
this.fallback = fallback;
this.ignoreAlpha = ignoreAlpha;
}
@Override
public T apply(int color) {
if(ignoreAlpha) {
color = ColorUtil.zeroAlpha(color);
}
T lookup = map.get(color);
return lookup != null ? lookup : fallback;
}
@Override
public Iterable<T> getEntries() {
Set<T> entries = new HashSet<>(map.values());
entries.add(fallback);
return entries;
}
}
@@ -0,0 +1,39 @@
package com.dfsek.terra.addons.image.converter.mapping;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Map.Entry;
import java.util.function.Function;
import java.util.stream.Collectors;
import com.dfsek.terra.api.registry.Registry;
import com.dfsek.terra.api.world.biome.Biome;
public class BiomeDefinedColorMapping<T> implements ColorMapping<T> {
Registry<Biome> biomeRegistry;
Function<Biome, T> converter;
public BiomeDefinedColorMapping(Registry<Biome> biomeRegistry, Function<Biome, T> converter) {
this.biomeRegistry = biomeRegistry;
this.converter = converter;
}
@Override
public Map<Integer, T> get() {
Map<Biome, Integer> colorMap = new HashSet<>(biomeRegistry.entries()).stream().collect(Collectors.toMap(b -> b, Biome::getColor));
Map<Integer, Biome> output = new HashMap<>();
colorMap.forEach(((biome, color) -> {
if(!output.containsKey(color)) {
output.put(color, biome);
} else {
throw new IllegalArgumentException(
String.format("Biome %s has same color as %s: %x", biome.getID(), output.get(color).getID(), color));
}
}));
return output.entrySet().stream().collect(Collectors.toMap(Entry::getKey, e -> converter.apply(e.getValue())));
}
}
@@ -0,0 +1,8 @@
package com.dfsek.terra.addons.image.converter.mapping;
import java.util.Map;
import java.util.function.Supplier;
public interface ColorMapping<T> extends Supplier<Map<Integer, T>> {
}
@@ -0,0 +1,28 @@
package com.dfsek.terra.addons.image.image;
import java.awt.image.BufferedImage;
public class BufferedImageWrapper implements Image {
private final BufferedImage image;
public BufferedImageWrapper(BufferedImage image) {
this.image = image;
}
@Override
public int getRGB(int x, int y) {
return image.getRGB(x, y);
}
@Override
public int getWidth() {
return image.getWidth();
}
@Override
public int getHeight() {
return image.getHeight();
}
}
@@ -0,0 +1,9 @@
package com.dfsek.terra.addons.image.image;
public interface Image {
int getRGB(int x, int y);
int getWidth();
int getHeight();
}
@@ -0,0 +1,74 @@
package com.dfsek.terra.addons.image.image;
public class StitchedImage implements Image {
private final Image[][] images;
private final int[] rowOffsets, columnOffsets;
private final int width, height;
public StitchedImage(Image[][] images, boolean zeroIndexed) throws IllegalArgumentException {
int width = 0;
int height = 0;
int rows = images.length;
int columns = images[0].length;
this.rowOffsets = new int[rows];
this.columnOffsets = new int[columns];
for(int i = 0; i < rows; i++) {
int rowHeight = images[i][0].getHeight();
rowOffsets[i] = height;
height += rowHeight;
for(int j = 1; j < columns; j++) {
if(images[i][j].getHeight() != rowHeight)
throw new IllegalArgumentException("Image heights in row " + (i + (zeroIndexed ? 0 : 1)) + " do not match");
}
}
for(int i = 0; i < columns; i++) {
int columnWidth = images[0][i].getWidth();
columnOffsets[i] = width;
width += columnWidth;
for(int j = 1; j < rows; j++) {
if(images[i][j].getWidth() != columnWidth)
throw new IllegalArgumentException("Image widths in column " + (i + (zeroIndexed ? 0 : 1)) + " do not match");
}
}
this.width = width;
this.height = height;
this.images = images;
}
private int getColumn(int x) {
for(int i = columnOffsets.length - 1; i > 0; i--) {
if(x >= columnOffsets[i])
return i;
}
return 0;
}
private int getRow(int y) {
for(int i = rowOffsets.length - 1; i > 0; i--) {
if(y >= rowOffsets[i])
return i;
}
return 0;
}
@Override
public int getRGB(int x, int y) {
int row = getRow(y);
int column = getColumn(x);
return images[row][column].getRGB(x - columnOffsets[column], y - rowOffsets[row]);
}
@Override
public int getWidth() {
return width;
}
@Override
public int getHeight() {
return height;
}
}
@@ -0,0 +1,28 @@
package com.dfsek.terra.addons.image.image;
import java.util.function.Supplier;
public class SuppliedImage implements Image {
private final Supplier<Image> imageSupplier;
public SuppliedImage(Supplier<Image> imageSupplier) {
this.imageSupplier = imageSupplier;
}
@Override
public int getRGB(int x, int y) {
return imageSupplier.get().getRGB(x, y);
}
@Override
public int getWidth() {
return imageSupplier.get().getWidth();
}
@Override
public int getHeight() {
return imageSupplier.get().getHeight();
}
}
@@ -0,0 +1,245 @@
package com.dfsek.terra.addons.image.operator;
import com.dfsek.seismic.math.floatingpoint.FloatingPointFunctions;
import com.dfsek.seismic.type.sampler.Sampler;
import com.dfsek.terra.addons.image.image.Image;
import com.dfsek.terra.addons.image.util.ColorUtil;
import com.dfsek.terra.addons.image.util.ColorUtil.Channel;
import static com.dfsek.terra.addons.image.util.MathUtil.lerp;
/**
* Computes a 2D distance transform of a given image and stores the result in a 2D array of distances.
* Implementation based on the algorithm described in the paper
* <a href="https://cs.brown.edu/people/pfelzens/papers/dt-final.pdf">Distance Transforms of Sampled Functions</a>
* by Pedro F. Felzenszwalb and Daniel P. Huttenlocher.
*/
public class DistanceTransform {
private static final double MAX_DISTANCE_CAP = 10_000_000; // Arbitrarily large value, doubtful someone would
private final double[][] distances;
/**
* Size bounds matching the provided image.
*/
private final int width, height;
/**
* Min and max distances of the distance computation. These may change after {@link #normalize(Normalization)} calls.
*/
private double minDistance, maxDistance;
// ever use an image large enough to exceed this.
public DistanceTransform(Image image, Channel channel, int threshold, boolean clampToMaxEdgeDistance, CostFunction costFunction,
boolean invertThreshold) {
// Construct binary image based on threshold value
boolean[][] binaryImage = new boolean[image.getWidth()][image.getHeight()];
for(int x = 0; x < image.getWidth(); x++) {
for(int y = 0; y < image.getHeight(); y++) {
binaryImage[x][y] = ColorUtil.getChannel(image.getRGB(x, y), channel) > threshold ^ invertThreshold;
}
}
// Get edges of binary image
boolean[][] binaryImageEdge = new boolean[image.getWidth()][image.getHeight()];
for(int x = 0; x < image.getWidth(); x++) {
for(int y = 0; y < image.getHeight(); y++) {
if(!binaryImage[x][y])
binaryImageEdge[x][y] = false;
else
// If cell borders any false cell
binaryImageEdge[x][y] = x > 0 && !binaryImage[x - 1][y] ||
y > 0 && !binaryImage[x][y - 1] ||
x < image.getWidth() - 1 && !binaryImage[x + 1][y] ||
y < image.getHeight() - 1 && !binaryImage[x][y + 1];
}
}
double[][] function = new double[image.getWidth()][image.getHeight()];
for(int x = 0; x < image.getWidth(); x++) {
for(int y = 0; y < image.getHeight(); y++) {
function[x][y] = switch(costFunction) {
case Channel -> ColorUtil.getChannel(image.getRGB(x, y), channel);
case Threshold -> binaryImage[x][y] ? MAX_DISTANCE_CAP : 0;
case ThresholdEdge, ThresholdEdgeSigned -> binaryImageEdge[x][y] ? 0 : MAX_DISTANCE_CAP;
};
}
}
distances = calculateDistance2D(function);
if(costFunction == CostFunction.ThresholdEdgeSigned) {
for(int x = 0; x < image.getWidth(); x++) {
for(int y = 0; y < image.getHeight(); y++) {
distances[x][y] *= binaryImage[x][y] ? 1 : -1;
}
}
}
if(clampToMaxEdgeDistance) {
// Find the largest value on the edge of the image
double max = Double.NEGATIVE_INFINITY;
for(int x = 0; x < image.getWidth(); x++) {
max = Math.max(max, distances[x][0]);
max = Math.max(max, distances[x][image.getHeight() - 1]);
}
for(int y = 0; y < image.getHeight(); y++) {
max = Math.max(max, distances[0][y]);
max = Math.max(max, distances[image.getWidth() - 1][y]);
}
// Clamp to that largest value
for(int x = 0; x < image.getWidth(); x++) {
for(int y = 0; y < image.getHeight(); y++) {
distances[x][y] = Math.max(max, distances[x][y]);
}
}
}
this.width = image.getWidth();
this.height = image.getHeight();
setOutputRange();
}
private double[][] calculateDistance2D(double[][] f) {
double[][] d = new double[f.length][f[0].length];
// Distance pass for each column
for(int x = 0; x < f.length; x++) {
d[x] = calculateDistance1D(f[x]);
}
// Distance pass for each row
double[] row = new double[f.length];
for(int y = 0; y < f[0].length; y++) {
for(int x = 0; x < f[0].length; x++)
row[x] = d[x][y];
row = calculateDistance1D(row);
for(int x = 0; x < f[0].length; x++) {
d[x][y] = Math.sqrt(row[x]);
}
}
return d;
}
private double[] calculateDistance1D(double[] f) {
double[] d = new double[f.length];
int[] v = new int[f.length];
double[] z = new double[f.length + 1];
int k = 0;
v[0] = 0;
z[0] = Integer.MIN_VALUE;
z[1] = Integer.MAX_VALUE;
for(int q = 1; q <= f.length - 1; q++) {
double fqPlusQ2 = (f[q] + Math.pow(q, 2));
double twoQ = 2 * q;
double s = (fqPlusQ2 - (f[v[k]] + Math.pow(v[k], 2))) / (twoQ - 2 * v[k]);
while(s <= z[k]) {
k--;
s = (fqPlusQ2 - (f[v[k]] + Math.pow(v[k], 2))) / (twoQ - 2 * v[k]);
}
k++;
v[k] = q;
z[k] = s;
z[k + 1] = Integer.MAX_VALUE;
}
k = 0;
for(int q = 0; q <= f.length - 1; q++) {
while(z[k + 1] < q)
k++;
d[q] = Math.pow(q - v[k], 2) + f[v[k]];
}
return d;
}
/**
* Redistributes the stored distance computation according to the provided {@link Normalization} method.
*/
private void normalize(Normalization normalization) {
for(int x = 0; x < width; x++) {
for(int y = 0; y < height; y++) {
double d = distances[x][y];
distances[x][y] = switch(normalization) {
case None -> distances[x][y];
case Linear -> lerp(d, minDistance, -1, maxDistance, 1);
case SmoothPreserveZero -> {
if(minDistance > 0 || maxDistance < 0) {
// Can't preserve zero if it is not contained in range so just lerp
yield lerp(distances[x][y], minDistance, -1, maxDistance, 1);
} else {
if(d > 0) {
yield Math.pow(d / maxDistance, 2);
} else if(d < 0) {
yield -Math.pow(d / minDistance, 2);
} else {
yield 0;
}
}
}
};
}
}
setOutputRange();
}
private void setOutputRange() {
double minDistance = Double.POSITIVE_INFINITY;
double maxDistance = Double.NEGATIVE_INFINITY;
for(int x = 0; x < width; x++) {
for(int y = 0; y < height; y++) {
minDistance = Math.min(minDistance, distances[x][y]);
maxDistance = Math.max(maxDistance, distances[x][y]);
}
}
this.minDistance = minDistance;
this.maxDistance = maxDistance;
}
public enum CostFunction {
Channel,
Threshold,
ThresholdEdge,
ThresholdEdgeSigned,
}
public enum Normalization {
/**
* Return the raw calculated distances.
*/
None,
/**
* Redistribute the output values to fit in the range [-1, 1]
*/
Linear,
/**
* Redistributes smoothly to the range [-1, 1], such that areas where distance = 0 stay 0.
* This is only really applicable to signed distance calculations, and will fall back to linear
* redistribution if the input range does not contain both positive and negative values.
*/
SmoothPreserveZero,
}
public static class Noise implements Sampler {
private final DistanceTransform transform;
public Noise(DistanceTransform transform, Normalization normalization) {
this.transform = transform;
transform.normalize(normalization);
}
@Override
public double getSample(long seed, double x, double y) {
if(x < 0 || y < 0 || x >= transform.width || y >= transform.height) return transform.minDistance;
return transform.distances[FloatingPointFunctions.floor(x)][FloatingPointFunctions.floor(y)];
}
@Override
public double getSample(long seed, double x, double y, double z) {
return getSample(seed, x, z);
}
}
}
@@ -0,0 +1,41 @@
package com.dfsek.terra.addons.image.sampler;
import com.dfsek.seismic.type.sampler.Sampler;
import com.dfsek.terra.addons.image.colorsampler.ColorSampler;
import com.dfsek.terra.addons.image.util.ColorUtil;
import com.dfsek.terra.addons.image.util.ColorUtil.Channel;
import static com.dfsek.terra.addons.image.util.MathUtil.lerp;
public class ChannelSampler implements Sampler {
private final ColorSampler colorSampler;
private final Channel channel;
private final boolean normalize;
private final boolean premultiply;
public ChannelSampler(ColorSampler colorSampler, Channel channel, boolean normalize, boolean premultiply) {
this.colorSampler = colorSampler;
this.channel = channel;
this.normalize = normalize;
this.premultiply = premultiply;
}
@Override
public double getSample(long seed, double x, double y) {
int sample = colorSampler.apply((int) x, (int) y);
int premultiplied = premultiply ? ColorUtil.premultiply(sample) : sample;
double channelValue = channel.from(premultiplied);
return normalize ? lerp(channelValue, 0, -1, 255, 1) : channelValue;
}
@Override
public double getSample(long seed, double x, double y, double z) {
return getSample(seed, x, z);
}
}
@@ -0,0 +1,340 @@
package com.dfsek.terra.addons.image.util;
/**
* Utility class for manipulating 8 bit ARGB colors
*/
public class ColorUtil {
private ColorUtil() { }
public static int distance(int a, int b) {
return Math.abs(getRed(a) - getRed(b)) +
Math.abs(getGreen(a) - getGreen(b)) +
Math.abs(getBlue(a) - getBlue(b));
}
/**
* Returns the red channel value of a given ARGB color value.
*
* @param argb the ARGB color value to extract the red channel value from
*
* @return the red channel value of the given ARGB color value, in the range 0-255
*/
public static int getRed(int argb) {
return argb >> 16 & 255;
}
/**
* Returns the green channel value of a given ARGB color value.
*
* @param argb the ARGB color value to extract the green channel value from
*
* @return the green channel value of the given ARGB color value, in the range 0-255
*/
public static int getGreen(int argb) {
return argb >> 8 & 255;
}
/**
* Returns the blue channel value of a given ARGB color value.
*
* @param argb the ARGB color value to extract the blue channel value from
*
* @return the blue channel value of the given ARGB color value, in the range 0-255
*/
public static int getBlue(int argb) {
return argb & 255;
}
/**
* Returns the alpha channel value of a given ARGB color value.
*
* @param argb the ARGB color value to extract the blue channel value from
*
* @return the alpha channel value of the given ARGB color value, in the range 0-255
*/
public static int getAlpha(int argb) {
return argb >> 24 & 255;
}
/**
* Returns the grayscale value of a given ARGB color value.
*
* @param argb the ARGB color value to convert to grayscale
*
* @return the grayscale value of the given ARGB color value, in the range 0-255
*/
public static int getGrayscale(int argb) {
return (getRed(argb) + getGreen(argb) + getBlue(argb)) / 3;
}
/**
* Returns the value of the specified channel for a given ARGB color value.
*
* @param argb the ARGB color value to extract the channel value from
* @param channel the channel to extract the value from
*
* @return the value of the specified channel for the given ARGB color value, in the range 0-255
*/
public static int getChannel(int argb, Channel channel) {
return channel.from(argb);
}
/**
* Sets the red channel value of a given ARGB color value to zero.
*
* @param argb the ARGB color value to zero the red channel of
*
* @return the resulting ARGB color value with the red channel set to zero
*/
public static int zeroRed(int argb) {
return argb & ~0x00FF0000;
}
/**
* Sets the green channel value of a given ARGB color value to zero.
*
* @param argb the ARGB color value to zero the green channel of
*
* @return the resulting ARGB color value with the green channel set to zero
*/
public static int zeroGreen(int argb) {
return argb & ~0x0000FF00;
}
/**
* Sets the blue channel value of a given ARGB color value to zero.
*
* @param argb the ARGB color value to zero the blue channel of
*
* @return the resulting ARGB color value with the blue channel set to zero
*/
public static int zeroBlue(int argb) {
return argb & ~0x000000FF;
}
/**
* Sets the alpha channel value of a given ARGB color value to zero.
* This is the same as setting the color to fully transparent.
*
* @param argb the ARGB color value to zero the alpha channel of
*
* @return the resulting ARGB color value with the alpha channel set to zero
*/
public static int zeroAlpha(int argb) {
return argb & ~0xFF000000;
}
/**
* Sets the color channels of a given ARGB color value to zero.
* This is the same as setting the color to black, while preserving the alpha.
*
* @param argb the ARGB color value to zero the color channel of
*
* @return the resulting ARGB color value with the color channels set to zero
*/
public static int zeroGrayscale(int argb) {
return argb & ~0x00FFFFFF;
}
/**
* Sets the specified channel value of a given ARGB color value to zero.
*
* @param argb the ARGB color value to zero the specified channel of
* @param channel the channel to zero the value of
*
* @return the resulting ARGB color value with the specified channel value set to zero
*/
public static int zeroChannel(int argb, Channel channel) {
return channel.zero(argb);
}
/**
* Multiply the RGB channels of a given ARGB color value by its alpha channel value.
*
* @param argb the ARGB color value to premultiply the RGB channels of
*
* @return the resulting premultiplied ARGB color value
*/
public static int premultiply(int argb) {
int alpha = getAlpha(argb);
int red = (getRed(argb) * alpha + 127) / 255;
int green = (getGreen(argb) * alpha + 127) / 255;
int blue = (getBlue(argb) * alpha + 127) / 255;
return argb(alpha, red, green, blue);
}
/**
* Returns an ARGB color value with the specified values for alpha, red, green, and blue channels.
*
* @param alpha the alpha value, between 0 and 255, to set in the ARGB color value
* @param red the red value, between 0 and 255, to set in the ARGB color value
* @param green the green value, between 0 and 255, to set in the ARGB color value
* @param blue the blue value, between 0 and 255, to set in the ARGB color value
*
* @return the resulting ARGB color value with the specified values for alpha, red, green, and blue channels
*/
public static int argb(int alpha, int red, int green, int blue) {
return argbAlpha(alpha) | argbRed(red) | argbGreen(green) | argbBlue(blue);
}
/**
* Returns an ARGB color value with the specified values for alpha, red, green, and blue channels,
* after validating that each channel value is in the range 0-255.
*
* @param alpha the alpha value, between 0 and 255, to set in the ARGB color value
* @param red the red value, between 0 and 255, to set in the ARGB color value
* @param green the green value, between 0 and 255, to set in the ARGB color value
* @param blue the blue value, between 0 and 255, to set in the ARGB color value
*
* @return the resulting ARGB color value with the specified values for alpha, red, green, and blue channels
*
* @throws IllegalArgumentException if any channel value is outside the range 0-255
*/
public static int argbValidated(int alpha, int red, int green, int blue) throws IllegalArgumentException {
if(alpha < 0 || alpha > 255 ||
red < 0 || red > 255 ||
green < 0 || green > 255 ||
blue < 0 || blue > 255
) throw new IllegalArgumentException("Channel values must be in range 0-255");
return argb(alpha, red, green, blue);
}
/**
* Returns the ARGB color value with the specified alpha channel value and zero
* for the red, green, and blue channels.
*
* @param alpha the alpha channel value to set in the ARGB color value
*
* @return the resulting ARGB color value
*/
public static int argbAlpha(int alpha) { return alpha << 24; }
/**
* Returns the ARGB color value with the specified red channel value and zero
* for the alpha, green, and blue channels.
*
* @param red the red channel value to set in the ARGB color value
*
* @return the resulting ARGB color value
*/
public static int argbRed(int red) { return red << 16; }
/**
* Returns the ARGB color value with the specified red channel value and zero
* for the alpha, red, and blue channels.
*
* @param green the green channel value to set in the ARGB color value
*
* @return the resulting ARGB color value
*/
public static int argbGreen(int green) { return green << 8; }
/**
* Returns the ARGB color value with the specified blue channel value and zero
* for the alpha, red, and green channels.
*
* @param blue the blue channel value to set in the ARGB color value
*
* @return the resulting ARGB color value
*/
public static int argbBlue(int blue) { return blue; }
/**
* Returns an ARGB color value with the specified grayscale value for all four channels.
*
* @param value the grayscale value to set in all four channels of the ARGB color value
*
* @return the resulting ARGB color value with the specified grayscale value for all four channels
*/
public static int argbGrayscale(int value) { return argb(value, value, value, value); }
public enum Channel {
RED {
@Override
public int from(int argb) {
return getRed(argb);
}
@Override
public int zero(int argb) {
return zeroRed(argb);
}
@Override
public int argb(int value) {
return argbRed(value);
}
},
GREEN {
@Override
public int from(int argb) {
return getGreen(argb);
}
@Override
public int zero(int argb) {
return zeroGreen(argb);
}
@Override
public int argb(int value) {
return argbGreen(value);
}
},
BLUE {
@Override
public int from(int argb) {
return getBlue(argb);
}
@Override
public int zero(int argb) {
return zeroBlue(argb);
}
@Override
public int argb(int value) {
return argbBlue(value);
}
},
GRAYSCALE {
@Override
public int from(int argb) {
return getGrayscale(argb);
}
@Override
public int zero(int argb) {
return zeroGrayscale(argb);
}
@Override
public int argb(int value) {
return argbAlpha(value);
}
},
ALPHA {
@Override
public int from(int argb) {
return getAlpha(argb);
}
@Override
public int zero(int argb) {
return zeroAlpha(argb);
}
@Override
public int argb(int value) {
return argbAlpha(value);
}
};
public abstract int from(int argb);
public abstract int zero(int argb);
public abstract int argb(int value);
}
}
@@ -0,0 +1,25 @@
package com.dfsek.terra.addons.image.util;
import java.util.Map;
import java.util.Map.Entry;
import java.util.function.Function;
import java.util.stream.Collectors;
public class MapUtil {
private MapUtil() { }
/**
* Utility method for applying transformations on a map's keys.
*/
public static <O, N, T> Map<N, T> mapKeys(Map<O, T> map, Function<O, N> mappingFunction) {
return map
.entrySet()
.stream()
.collect(Collectors.toMap(
e -> mappingFunction.apply(e.getKey()),
Entry::getValue
));
}
}
@@ -0,0 +1,9 @@
package com.dfsek.terra.addons.image.util;
public class MathUtil {
private MathUtil() { }
public static double lerp(double x, double x1, double y1, double x2, double y2) {
return (((y1 - y2) * (x - x1)) / (x1 - x2)) + y1;
}
}
@@ -0,0 +1,12 @@
schema-version: 1
contributors:
- Terra contributors
id: api-image
version: @VERSION@
entrypoints:
- "com.dfsek.terra.addons.image.ImageLibraryAddon"
website:
issues: https://github.com/PolyhedralDev/Terra/issues
source: https://github.com/PolyhedralDev/Terra
docs: https://terra.polydev.org
license: MIT License