diff --git a/src/main/java/ch/njol/skript/Skript.java b/src/main/java/ch/njol/skript/Skript.java index 4f3afedccca..a427cba174a 100644 --- a/src/main/java/ch/njol/skript/Skript.java +++ b/src/main/java/ch/njol/skript/Skript.java @@ -80,6 +80,7 @@ import org.skriptlang.skript.bukkit.registration.BukkitSyntaxInfos; import org.skriptlang.skript.bukkit.tags.TagModule; import org.skriptlang.skript.common.CommonModule; +import org.skriptlang.skript.common.colors.ColorModule; import org.skriptlang.skript.docs.Origin; import org.skriptlang.skript.lang.comparator.Comparator; import org.skriptlang.skript.lang.comparator.Comparators; @@ -577,6 +578,7 @@ public void onEnable() { skript.loadModules( new CommonModule(), new BrewingModule(), + new ColorModule(), new EntityModule(), new DamageSourceModule(), new InteractionModule(), diff --git a/src/main/java/ch/njol/skript/classes/data/DefaultFunctions.java b/src/main/java/ch/njol/skript/classes/data/DefaultFunctions.java index 66cdda4336e..f87f9c9b926 100644 --- a/src/main/java/ch/njol/skript/classes/data/DefaultFunctions.java +++ b/src/main/java/ch/njol/skript/classes/data/DefaultFunctions.java @@ -9,8 +9,9 @@ import ch.njol.skript.lang.util.SimpleLiteral; import ch.njol.skript.registrations.Classes; import ch.njol.skript.registrations.DefaultClasses; -import ch.njol.skript.util.*; +import ch.njol.skript.util.Contract; import ch.njol.skript.util.Date; +import ch.njol.skript.util.Utils; import ch.njol.util.Math2; import ch.njol.util.StringUtils; import ch.njol.util.coll.CollectionUtils; @@ -355,7 +356,7 @@ public Class getReturnType(Expression... arguments) { Functions.register(DefaultFunction.builder(skript, "toBase", String[].class) .description(""" - Turns a number in a string using a specific base (decimal, hexadecimal, octal). + Turns a number into a string using a specific base (decimal, hexadecimal, octal). For example, converting 32 to hexadecimal (base 16) would be 'toBase(32, 16)', which would return "20". You can use any base between 2 and 36. """) @@ -592,29 +593,6 @@ public Long[] executeSimple(Object[][] params) { }.description("Calculates the total amount of experience needed to achieve given level from scratch in Minecraft.") .since("2.2-dev32")); - Functions.registerFunction(new SimpleJavaFunction("rgb", new Parameter[] { - new Parameter<>("red", DefaultClasses.LONG, true, null), - new Parameter<>("green", DefaultClasses.LONG, true, null), - new Parameter<>("blue", DefaultClasses.LONG, true, null), - new Parameter<>("alpha", DefaultClasses.LONG, true, new SimpleLiteral<>(255L,true)) - }, DefaultClasses.COLOR, true) { - @Override - public ColorRGB[] executeSimple(Object[][] params) { - Long red = (Long) params[0][0]; - Long green = (Long) params[1][0]; - Long blue = (Long) params[2][0]; - Long alpha = (Long) params[3][0]; - - return CollectionUtils.array(ColorRGB.fromRGBA(red.intValue(), green.intValue(), blue.intValue(), alpha.intValue())); - } - }).description("Returns a RGB color from the given red, green and blue parameters. Alpha values can be added optionally, " + - "but these only take affect in certain situations, like text display backgrounds.") - .examples( - "dye player's leggings rgb(120, 30, 45)", - "set the colour of a text display to rgb(10, 50, 100, 50)" - ) - .since("2.5, 2.10 (alpha)"); - Functions.register(DefaultFunction.builder(skript, "player", Player.class) .description( "Returns an online player from their name or UUID, if player is offline function will return nothing.", diff --git a/src/main/java/ch/njol/skript/classes/data/SkriptClasses.java b/src/main/java/ch/njol/skript/classes/data/SkriptClasses.java index 38d577ce2b1..974a6fb9218 100644 --- a/src/main/java/ch/njol/skript/classes/data/SkriptClasses.java +++ b/src/main/java/ch/njol/skript/classes/data/SkriptClasses.java @@ -351,37 +351,6 @@ public String toVariableNameString(final Direction o) { Classes.registerClass(new SlotClassInfo()); - Classes.registerClass(new ClassInfo<>(Color.class, "color") - .user("colou?rs?") - .name("Color") - .description("Wool, dye and chat colors.") - .usage("black, dark grey/dark gray, grey/light grey/gray/light gray/silver, white, blue/dark blue, cyan/aqua/dark cyan/dark aqua, light blue/light cyan/light aqua, green/dark green, light green/lime/lime green, yellow/light yellow, orange/gold/dark yellow, red/dark red, pink/light red, purple/dark purple, magenta/light purple, brown/indigo") - .examples("color of the sheep is red or black", - "set the color of the block to green", - "message \"You're holding a <%color of tool%>%color of tool% wool block\"") - .since("") - .supplier(SkriptColor.values()) - .parser(new Parser() { - @Override - @Nullable - public Color parse(String input, ParseContext context) { - Color rgbColor = ColorRGB.fromString(input); - if (rgbColor != null) - return rgbColor; - return SkriptColor.fromName(input); - } - - @Override - public String toString(Color c, int flags) { - return c.getName(); - } - - @Override - public String toVariableNameString(Color color) { - return "" + color.getName().toLowerCase(Locale.ENGLISH).replace('_', ' '); - } - })); - Classes.registerClass(new ClassInfo<>(StructureType.class, "structuretype") .user("tree ?types?", "trees?") .name("Tree Type") diff --git a/src/main/java/ch/njol/skript/expressions/ExprARGB.java b/src/main/java/ch/njol/skript/expressions/ExprARGB.java index d6d8a27276f..687a6204e77 100644 --- a/src/main/java/ch/njol/skript/expressions/ExprARGB.java +++ b/src/main/java/ch/njol/skript/expressions/ExprARGB.java @@ -1,6 +1,10 @@ package ch.njol.skript.expressions; -import ch.njol.skript.doc.*; +import ch.njol.skript.doc.Description; +import ch.njol.skript.doc.Example; +import ch.njol.skript.doc.Keywords; +import ch.njol.skript.doc.Name; +import ch.njol.skript.doc.Since; import ch.njol.skript.expressions.base.SimplePropertyExpression; import ch.njol.skript.lang.Expression; import ch.njol.skript.lang.Literal; @@ -24,7 +28,7 @@ public class ExprARGB extends SimplePropertyExpression { static { - register(ExprARGB.class, Integer.class, "(:alpha|:red|:green|:blue) (value|component)", "colors"); + register(ExprARGB.class, Integer.class, "(:alpha|:red|:green|:blue) (value|component|channel)", "colors"); } private RGB color; diff --git a/src/main/java/ch/njol/skript/registrations/DefaultClasses.java b/src/main/java/ch/njol/skript/registrations/DefaultClasses.java index 54f6842e44b..17b9b3c726d 100644 --- a/src/main/java/ch/njol/skript/registrations/DefaultClasses.java +++ b/src/main/java/ch/njol/skript/registrations/DefaultClasses.java @@ -8,7 +8,6 @@ import org.jetbrains.annotations.NotNull; import ch.njol.skript.classes.ClassInfo; -import ch.njol.skript.util.Color; import ch.njol.skript.util.Date; import ch.njol.skript.util.Timespan; @@ -31,7 +30,6 @@ public class DefaultClasses { public static ClassInfo WORLD = getClassInfo(World.class); // Skript - public static ClassInfo COLOR = getClassInfo(Color.class); public static ClassInfo DATE = getClassInfo(Date.class); public static ClassInfo TIMESPAN = getClassInfo(Timespan.class); diff --git a/src/main/java/ch/njol/skript/util/Color.java b/src/main/java/ch/njol/skript/util/Color.java index 3f6fcad6b5e..ed3643a60a9 100644 --- a/src/main/java/ch/njol/skript/util/Color.java +++ b/src/main/java/ch/njol/skript/util/Color.java @@ -1,43 +1,39 @@ package ch.njol.skript.util; -import ch.njol.yggdrasil.YggdrasilSerializable.YggdrasilExtendedSerializable; import org.bukkit.DyeColor; import org.jetbrains.annotations.Nullable; -public interface Color extends YggdrasilExtendedSerializable { +public interface Color { /** - * Gets Bukkit color representing this color. - * @return Bukkit color. + * @return The Bukkit color representing this color. */ org.bukkit.Color asBukkitColor(); /** - * @return The alpha component of this color. + * @return The alpha channel of this color. */ int getAlpha(); /** - * @return The red component of this color. + * @return The red channel of this color. */ int getRed(); /** - * @return The green component of this color. + * @return The green channel of this color. */ int getGreen(); /** - * @return The blue component of this color. + * @return The blue channel of this color. */ int getBlue(); /** - * Gets Bukkit dye color representing this color, if one exists. - * @return Dye color or null. + * @return The {@link DyeColor} representing this color if one exists, or null otherwise. */ - @Nullable - DyeColor asDyeColor(); + @Nullable DyeColor asDyeColor(); /** * @return Name of the color. @@ -45,17 +41,24 @@ public interface Color extends YggdrasilExtendedSerializable { String getName(); /** - * @return the color as an ARGB integer. + * @return The color as an ARGB integer. */ default int asARGB() { return asBukkitColor().asARGB(); } /** - * @return the colour as an RGB hex value: RRGGBB + * @return The color as an RGB hex value: RRGGBB */ default String toHexString() { return String.format("%02X%02X%02X", getRed(), getGreen(), getBlue()); } + /** + * @return The integer representing this color. Used for serialization. + */ + default int asInt() { + return (getAlpha() << 24) | (getRed() << 16) | (getGreen() << 8) | getBlue(); + } + } diff --git a/src/main/java/ch/njol/skript/util/ColorHSB.java b/src/main/java/ch/njol/skript/util/ColorHSB.java new file mode 100644 index 00000000000..dc99b51a22f --- /dev/null +++ b/src/main/java/ch/njol/skript/util/ColorHSB.java @@ -0,0 +1,101 @@ +package ch.njol.skript.util; + +import ch.njol.util.Math2; +import org.bukkit.DyeColor; +import org.jetbrains.annotations.ApiStatus; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; +import org.skriptlang.skript.common.colors.ColorUtils; + +/** +* Immutable representation of a color in the HSB/HSV color-space, with an alpha channel. +* Hue, saturation, and brightness are stored in normalized form: 0-1. +* Alpha is stored as a value from 0-255 (like the red, green, and blue channels). +*/ +public final class ColorHSB implements Color { + + private final float hue; + private final float saturation; + private final float brightness; + private final int alpha; + + private final ColorRGB rgb; + private final @Nullable DyeColor dye; + + private ColorHSB(float hue, float saturation, float brightness, int alpha) { + this.hue = hue; + this.saturation = saturation; + this.brightness = brightness; + this.alpha = alpha; + + this.rgb = ColorUtils.hsbToRgb(this); + this.dye = rgb.asDyeColor(); + } + + public static @NotNull ColorHSB fromHSBA(float hue, float saturation, float brightness, int alpha) { + return new ColorHSB( + Math2.fit(0f, hue, 1f), + Math2.fit(0f, saturation, 1f), + Math2.fit(0f, brightness, 1f), + Math2.fit(0, alpha, 255) + ); + } + + public static @NotNull ColorHSB fromHSB(float hue, float saturation, float brightness) { + return fromHSBA(hue, saturation, brightness, 255); + } + + @ApiStatus.Internal + public static @NotNull ColorHSB fromUncheckedHSBA(float hue, float saturation, float brightness, int alpha) { + return new ColorHSB(hue, saturation, brightness, alpha); + } + + public float getHue() { + return hue; + } + + public float getSaturation() { + return saturation; + } + + public float getBrightness() { + return brightness; + } + + @Override + public org.bukkit.Color asBukkitColor() { + return rgb.asBukkitColor(); + } + + @Override + public int getAlpha() { + return alpha; + } + + @Override + public int getRed() { + return rgb.getRed(); + } + + @Override + public int getGreen() { + return rgb.getGreen(); + } + + @Override + public int getBlue() { + return rgb.getBlue(); + } + + @Override + public @Nullable DyeColor asDyeColor() { + return dye; + } + + @Override + public String getName() { + String hsb = String.format("%.3f, %.3f, %.3f", hue, saturation, brightness); + return alpha == 255 ? "hsb " + hsb : "hsba " + hsb + ", " + alpha; + } + +} diff --git a/src/main/java/ch/njol/skript/util/ColorHSL.java b/src/main/java/ch/njol/skript/util/ColorHSL.java new file mode 100644 index 00000000000..2c238ff3130 --- /dev/null +++ b/src/main/java/ch/njol/skript/util/ColorHSL.java @@ -0,0 +1,103 @@ +package ch.njol.skript.util; + +import ch.njol.util.Math2; +import org.bukkit.DyeColor; +import org.jetbrains.annotations.ApiStatus; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; +import org.skriptlang.skript.common.colors.ColorUtils; + +/** + * Immutable representation of a color in the HSL color-space, with an alpha channel. + * Hue, saturation, and lightness are stored in normalized form: 0-1. + * Alpha is stored as a value from 0-255 (like the red, green, and blue channels). + */ +public class ColorHSL implements Color { + + private final float hue; + private final float saturation; + private final float lightness; + private final int alpha; + + private final ColorRGB rgb; + private final @Nullable DyeColor dye; + + private ColorHSL(float hue, float saturation, float lightness, int alpha) { + this.hue = hue; + this.saturation = saturation; + this.lightness = lightness; + this.alpha = alpha; + + this.rgb = ColorUtils.hslToRgb(this); + this.dye = rgb.asDyeColor(); + } + + public static @NotNull ColorHSL fromHSLA(float hue, float saturation, float lightness, int alpha) { + return new ColorHSL( + Math2.fit(0f, hue, 1f), + Math2.fit(0f, saturation, 1f), + Math2.fit(0f, lightness, 1f), + Math2.fit(0, alpha, 255) + ); + } + + public static @NotNull ColorHSL fromHSL(float hue, float saturation, float lightness) { + return fromHSLA(hue, saturation, lightness, 255); + } + + @ApiStatus.Internal + public static @NotNull ColorHSL fromUncheckedHSLA(float hue, float saturation, float lightness, int alpha) { + return new ColorHSL(hue, saturation, lightness, alpha); + } + + public float getHue() { + return hue; + } + + public float getSaturation() { + return saturation; + } + + public float getLightness() { + return lightness; + } + + @Override + public org.bukkit.Color asBukkitColor() { + return rgb.asBukkitColor(); + } + + @Override + public int getAlpha() { + return alpha; + } + + @Override + public int getRed() { + return rgb.getRed(); + } + + @Override + public int getGreen() { + return rgb.getGreen(); + } + + @Override + public int getBlue() { + return rgb.getBlue(); + } + + @Override + public @Nullable DyeColor asDyeColor() { + return dye; + } + + @Override + public String getName() { + String hslString = String.format("%.3f, %.3f, %.3f", hue, saturation, lightness); + if (alpha != 255) + return "hsla " + hslString + ", " + alpha; + return "hsl " + hslString; + } + +} diff --git a/src/main/java/ch/njol/skript/util/ColorRGB.java b/src/main/java/ch/njol/skript/util/ColorRGB.java index 8c589318f08..97cd223e541 100644 --- a/src/main/java/ch/njol/skript/util/ColorRGB.java +++ b/src/main/java/ch/njol/skript/util/ColorRGB.java @@ -1,8 +1,6 @@ package ch.njol.skript.util; -import ch.njol.skript.variables.Variables; import ch.njol.util.Math2; -import ch.njol.yggdrasil.Fields; import org.apache.commons.lang.math.NumberUtils; import org.bukkit.DyeColor; import org.jetbrains.annotations.ApiStatus; @@ -10,8 +8,6 @@ import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; -import java.io.NotSerializableException; -import java.io.StreamCorruptedException; import java.util.regex.Matcher; import java.util.regex.Pattern; @@ -19,9 +15,11 @@ public class ColorRGB implements Color { private static final Pattern RGB_PATTERN = Pattern.compile("(?>rgb|RGB) (\\d+), (\\d+), (\\d+)"); - private org.bukkit.Color bukkit; + private final org.bukkit.Color bukkit; - private @Nullable DyeColor dye; + private final @Nullable DyeColor dye; + + private final int alpha, red, green, blue; /** * Subject to being private in the future. Use {@link #fromRGB(int, int, int)} @@ -45,6 +43,10 @@ public ColorRGB(int red, int green, int blue) { public ColorRGB(org.bukkit.Color bukkit) { this.dye = DyeColor.getByColor(bukkit); this.bukkit = bukkit; + this.alpha = bukkit.getAlpha(); + this.red = bukkit.getRed(); + this.green = bukkit.getGreen(); + this.blue = bukkit.getBlue(); } /** @@ -87,22 +89,22 @@ public ColorRGB(org.bukkit.Color bukkit) { @Override public int getAlpha() { - return bukkit.getAlpha(); + return alpha; } @Override public int getRed() { - return bukkit.getRed(); + return red; } @Override public int getGreen() { - return bukkit.getGreen(); + return green; } @Override public int getBlue() { - return bukkit.getBlue(); + return blue; } @Override @@ -151,22 +153,4 @@ public String getName() { } } - @Override - public Fields serialize() throws NotSerializableException { - return new Fields(this, Variables.yggdrasil); - } - - @Override - public void deserialize(Fields fields) throws StreamCorruptedException, NotSerializableException { - org.bukkit.Color b = fields.getObject("bukkit", org.bukkit.Color.class); - DyeColor d = fields.getObject("dye", DyeColor.class); - if (b == null) - return; - if (d == null) - dye = DyeColor.getByColor(b); - else - dye = d; - bukkit = b; - } - } diff --git a/src/main/java/ch/njol/skript/util/SkriptColor.java b/src/main/java/ch/njol/skript/util/SkriptColor.java index 07ec28e845b..206de49ce17 100644 --- a/src/main/java/ch/njol/skript/util/SkriptColor.java +++ b/src/main/java/ch/njol/skript/util/SkriptColor.java @@ -3,14 +3,10 @@ import ch.njol.skript.localization.Adjective; import ch.njol.skript.localization.Language; import ch.njol.skript.variables.Variables; -import ch.njol.yggdrasil.Fields; import org.bukkit.ChatColor; import org.bukkit.DyeColor; -import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; -import java.io.NotSerializableException; -import java.io.StreamCorruptedException; import java.util.Arrays; import java.util.HashMap; import java.util.HashSet; @@ -55,7 +51,6 @@ public enum SkriptColor implements Color { names.clear(); for (SkriptColor color : values()) { String node = LANGUAGE_NODE + "." + color.name(); - color.setAdjective(new Adjective(node + ".adjective")); for (String name : Language.getList(node + ".names")) names.put(name.toLowerCase(Locale.ENGLISH), color); BY_CHAR.put(color.asChatColor().getChar(), color); @@ -66,14 +61,19 @@ public enum SkriptColor implements Color { Variables.yggdrasil.registerSingleClass(DyeColor.class, "DyeColor"); } - private ChatColor chat; - private DyeColor dye; - @Nullable - private Adjective adjective; + private final ChatColor chat; + private final DyeColor dye; + private final Adjective adjective; + private final int alpha, red, green, blue; SkriptColor(DyeColor dye, ChatColor chat) { this.chat = chat; this.dye = dye; + this.adjective = new Adjective(LANGUAGE_NODE + "." + name() + ".adjective"); + this.alpha = dye.getColor().getAlpha(); + this.red = dye.getColor().getRed(); + this.green = dye.getColor().getGreen(); + this.blue = dye.getColor().getBlue(); } @Override @@ -83,22 +83,22 @@ public org.bukkit.Color asBukkitColor() { @Override public int getAlpha() { - return dye.getColor().getAlpha(); + return alpha; } @Override public int getRed() { - return dye.getColor().getRed(); + return red; } @Override public int getGreen() { - return dye.getColor().getGreen(); + return green; } @Override public int getBlue() { - return dye.getColor().getBlue(); + return blue; } @Override @@ -112,25 +112,10 @@ public String getName() { return adjective.toString(); } - @Override - public Fields serialize() throws NotSerializableException { - return new Fields(this, Variables.yggdrasil); - } - - @Override - public void deserialize(@NotNull Fields fields) throws StreamCorruptedException { - dye = fields.getObject("dye", DyeColor.class); - chat = fields.getObject("chat", ChatColor.class); - try { - adjective = fields.getObject("adjective", Adjective.class); - } catch (StreamCorruptedException ignored) {} - } - public String getFormattedChat() { return "" + chat; } - - @Nullable + public Adjective getAdjective() { return adjective; } @@ -148,18 +133,12 @@ public byte getWoolData() { public byte getDyeData() { return (byte) (15 - dye.getWoolData()); } - - private void setAdjective(@Nullable Adjective adjective) { - this.adjective = adjective; - } - - + /** * @param name The String name of the color defined by Skript's .lang files. * @return Skript Color if matched up with the defined name */ - @Nullable - public static SkriptColor fromName(String name) { + public static @Nullable SkriptColor fromName(String name) { return names.get(name); } @@ -192,8 +171,7 @@ public static SkriptColor fromBukkitColor(org.bukkit.Color color) { * @return Skript Color if matched up with the defined short */ @Deprecated(since = "2.3.6", forRemoval = true) - @Nullable - public static SkriptColor fromDyeData(short data) { + public static @Nullable SkriptColor fromDyeData(short data) { if (data < 0 || data >= 16) return null; @@ -212,8 +190,7 @@ public static SkriptColor fromDyeData(short data) { * @return Skript Color if matched up with the defined short */ @Deprecated(since = "2.3.6", forRemoval = true) - @Nullable - public static SkriptColor fromWoolData(short data) { + public static @Nullable SkriptColor fromWoolData(short data) { if (data < 0 || data >= 16) return null; for (SkriptColor color : colors) { diff --git a/src/main/java/org/skriptlang/skript/common/colors/ColorModule.java b/src/main/java/org/skriptlang/skript/common/colors/ColorModule.java new file mode 100644 index 00000000000..ed485b9a83d --- /dev/null +++ b/src/main/java/org/skriptlang/skript/common/colors/ColorModule.java @@ -0,0 +1,278 @@ +package org.skriptlang.skript.common.colors; + +import ch.njol.skript.Skript; +import ch.njol.skript.classes.ClassInfo; +import ch.njol.skript.classes.Parser; +import ch.njol.skript.classes.Serializer; +import ch.njol.skript.lang.ParseContext; +import ch.njol.skript.lang.function.Functions; +import ch.njol.skript.registrations.Classes; +import ch.njol.skript.util.Color; +import ch.njol.skript.util.ColorRGB; +import ch.njol.skript.util.SkriptColor; +import ch.njol.yggdrasil.Fields; +import org.jetbrains.annotations.Nullable; +import org.skriptlang.skript.addon.AddonModule; +import org.skriptlang.skript.addon.SkriptAddon; +import org.skriptlang.skript.common.colors.elements.ExprBlend; +import org.skriptlang.skript.common.colors.elements.ExprComplementaryColor; +import org.skriptlang.skript.common.function.DefaultFunction; +import org.skriptlang.skript.common.function.Parameter.Modifier; +import org.skriptlang.skript.registration.SyntaxRegistry; + +import java.io.NotSerializableException; +import java.io.StreamCorruptedException; +import java.util.Arrays; +import java.util.Locale; +import java.util.function.Consumer; + +public class ColorModule implements AddonModule { + + @Override + public void init(SkriptAddon addon) { + Classes.registerClass(new ClassInfo<>(Color.class, "color") + .user("colou?rs?") + .name("Color") + .description("A color. Can be a wool, dye, or chat color, or a custom RGB color.") + .usage("black, dark grey/dark gray, grey/light grey/gray/light gray/silver, white, blue/dark blue, cyan/aqua/dark cyan/dark aqua, light blue/light cyan/light aqua, green/dark green, light green/lime/lime green, yellow/light yellow, orange/gold/dark yellow, red/dark red, pink/light red, purple/dark purple, magenta/light purple, brown/indigo") + .examples( + "color of the sheep is red or black", + "set the color of the block to green", + "message \"You're holding a <%color of tool%>%color of tool% wool block\"" + ) + .before("cattype", "gene", "wolfvariant") + .since("") + .supplier(SkriptColor.values()) + .parser(new Parser<>() { + @Override + public @Nullable Color parse(String input, ParseContext context) { + Color rgbColor = ColorRGB.fromString(input); + if (rgbColor != null) + return rgbColor; + return SkriptColor.fromName(input); + } + + @Override + public String toString(Color c, int flags) { + return c.getName(); + } + + @Override + public String toVariableNameString(Color color) { + return color.getName().toLowerCase(Locale.ENGLISH).replace('_', ' '); + } + }) + .serializer(new Serializer<>() { + @Override + public Fields serialize(Color color) throws NotSerializableException { + Fields f = new Fields(); + f.putPrimitive("asInt", color.asInt()); + return f; + } + + @Override + public void deserialize(Color o, Fields f) throws StreamCorruptedException { + assert false; + } + + @Override + protected Color deserialize(Fields fields) throws StreamCorruptedException { + int asInt = fields.getPrimitive("asInt", int.class); + return ColorUtils.fromInt(asInt); + } + + @Override + public boolean mustSyncDeserialization() { + return false; + } + + @Override + protected boolean canBeInstantiated() { + return false; + } + })); + } + + @Override + public void load(SkriptAddon addon) { + loadFunctions(); + register(addon.syntaxRegistry(), + ExprBlend::register, + ExprComplementaryColor::register + ); + } + + @Override + public String name() { + return "color"; + } + + public static void loadFunctions() { + SkriptAddon skript = Skript.instance(); + + Functions.register(DefaultFunction.builder(skript, "rgb", Color.class) + .description( + "Returns an RGB color from the given red, green and blue parameters.", + "Alpha values can be added optionally, but these only take affect in certain situations, like text display backgrounds." + ) + .examples( + "dye player's leggings rgb(120, 30, 45)", + "set the colour of a text display to rgb(10, 50, 100, 50)" + ) + .since("2.5, 2.10 (alpha)") + .parameter("red", Long.class) + .parameter("blue", Long.class) + .parameter("green", Long.class) + .parameter("alpha", Long.class, Modifier.OPTIONAL) + .build(args -> { + Long red = args.get("red"); + Long blue = args.get("blue"); + Long green = args.get("green"); + Long alpha = args.getOrDefault("alpha", 255L); + + return ColorRGB.fromRGBA(red.intValue(), blue.intValue(), green.intValue(), alpha.intValue()); + }) + ); + + Functions.register(DefaultFunction.builder(skript, "shade", Color.class) + .description( + "Shades a given color by a given amount, with optional HSL-based shading.", + "The amount parameter ranges from 1 to 100, with lower values closer to the original color and higher values closer to black.", + "Inputs below 1 will default to shading by 1%." + ) + .examples( + "set {_darkRed} to shade(red, 10)", + "set {_darkerRed} to shade(red, 20)", + "", + "function shadeExample(colour: colour, hsl: boolean = false):", + "\tloop 100 times:", + "\t\tset {_hex} to hex code of shade({_colour}, loop-value, {_hsl})", + "\t\tsend formatted \"%loop-value%: %{_hex}%████\" to all players", + "\t\twait 1 tick" + ) + .keywords("darken", "dim") + .since("INSERT VERSION") + .parameter("color", Color.class) + .parameter("amount", Long.class, Modifier.OPTIONAL) + .parameter("hsl", Boolean.class, Modifier.OPTIONAL) + .build(args -> { + Color color = args.get("color"); + Long amount = args.getOrDefault("amount", 1L); + boolean hsl = args.getOrDefault("hsl", false); + + return hsl ? ColorUtils.shadeColorHSL(color, amount.intValue()) + : ColorUtils.shadeColor(color, amount.intValue()); + }) + ); + + Functions.register(DefaultFunction.builder(skript, "tint", Color.class) + .description( + "Tints a given color by a given amount, with optional HSL-based shading.", + "The amount parameter ranges from 1 to 100, with lower values closer to the original color and higher values closer to white.", + "Inputs below 1 will default to tinting by 1%." + ) + .examples( + "set {_lightRed} to tint(red, 10)", + "set {_lighterRed} to tint(red, 20)", + "", + "function tintExample(colour: colour, hsl: boolean = false):", + "\tloop 100 times:", + "\t\tset {_hex} to hex code of tint({_colour}, loop-value, {_hsl})", + "\t\tsend formatted \"%loop-value%: %{_hex}%████\" to all players", + "\t\twait 1 tick" + ) + .keywords("lighten", "brighten") + .since("INSERT VERSION") + .parameter("color", Color.class) + .parameter("amount", Long.class, Modifier.OPTIONAL) + .parameter("hsl", Boolean.class, Modifier.OPTIONAL) + .build(args -> { + Color color = args.get("color"); + Long amount = args.getOrDefault("amount", 1L); + boolean hsl = args.getOrDefault("hsl", false); + + return hsl ? ColorUtils.tintColorHSL(color, amount.intValue()) + : ColorUtils.tintColor(color, amount.intValue()); + }) + ); + + Functions.register(DefaultFunction.builder(skript, "brightness", Color.class) + .description( + "Adjusts the brightness of a color by a specified amount, ranging from -100 to 100.", + "Positive values increase brightness, with higher values approaching white, and negative values decrease brightness, with lower values approaching black.", + "Inputs beyond the range will be clamped to the nearest valid value.", + "This is similar to shading and tinting, but is slightly different." + ) + .examples( + "set {_brighterRed} to colorBrightness(red, 10)", + "set {_darkerRed} to colorBrightness(red, -10)", + "", + "function brightnessExample(colour: colour):", + "\tloop integers from -100 to 100:", + "\t\tset {_hex} to hex code of colourBrightness({_colour}, loop-value)", + "\t\tsend formatted \"%loop-value%: %{_hex}%████\" to all players", + "\t\twait 1 tick" + ) + .keywords("brighten", "darken") + .since("INSERT VERSION") + .parameter("color", Color.class) + .parameter("amount", Long.class) + .build(args -> { + Color color = args.get("color"); + Long amount = args.get("amount"); + + return ColorUtils.adjustBrightness(color, amount.intValue()); + }) + ); + + Functions.register(DefaultFunction.builder(skript, "grayscale", Color.class) + .description( + "Converts a given color to its grayscale equivalent.", + "The resulting color retains its brightness but loses all hue, appearing as a shade of gray." + ) + .examples( + "set {_redButGrayscale} to grayscale(red)", + "", + "function grayscaleExample():", + "\tloop all colours:", + "\t\tset {_hex} to hex code of grayscale(loop-value)", + "\t\tsend formatted \"%loop-value%: %{_hex}%████\" to all players" + ) + .keywords("greyscale", "desaturate") + .since("INSERT VERSION") + .parameter("color", Color.class) + .build(args -> { + Color color = args.get("color"); + return ColorUtils.toGrayscale(color); + }) + ); + + Functions.register(DefaultFunction.builder(skript, "sepiatone", Color.class) + .description( + "Converts a given color to its sepiatone equivalent.", + "The resulting color mimics the warm, brownish look of vintage photographs." + ) + .examples( + "set {_redButSepiatone} to sepiatone(red)", + "", + "function sepiatoneExample():", + "\tloop all colours:", + "\t\tset {_hex} to hex code of sepiatone(loop-value)", + "\t\tsend formatted \"%loop-value%: %{_hex}%████\" to all players" + ) + .since("INSERT VERSION") + .parameter("color", Color.class) + .build(args -> { + Color color = args.get("color"); + return ColorUtils.toSepia(color); + }) + ); + + } + + @SafeVarargs + private void register(SyntaxRegistry registry, Consumer... consumers) { + Arrays.stream(consumers).forEach(consumer -> consumer.accept(registry)); + } + +} diff --git a/src/main/java/org/skriptlang/skript/common/colors/ColorUtils.java b/src/main/java/org/skriptlang/skript/common/colors/ColorUtils.java new file mode 100644 index 00000000000..1c8c4ec22e7 --- /dev/null +++ b/src/main/java/org/skriptlang/skript/common/colors/ColorUtils.java @@ -0,0 +1,417 @@ +package org.skriptlang.skript.common.colors; + +import ch.njol.skript.util.Color; +import ch.njol.skript.util.ColorHSB; +import ch.njol.skript.util.ColorHSL; +import ch.njol.skript.util.ColorRGB; +import ch.njol.skript.util.SkriptColor; +import ch.njol.util.Math2; +import org.jetbrains.annotations.NotNull; + +/** + * Utility class for color manipulation and conversion. + */ +public class ColorUtils { + + /** + * Converts an integer representation of a color to its {@link Color} equivalent. + * + * @param asInt an integer representing a color + * @return the {@link Color} represented by the integer + */ + public static Color fromInt(int asInt) { + for (SkriptColor preset : SkriptColor.values()) { + if (preset.asInt() == asInt) { + return preset; + } + } + int alpha = (asInt >> 24) & 0xFF; + int red = (asInt >> 16) & 0xFF; + int green = (asInt >> 8) & 0xFF; + int blue = asInt & 0xFF; + return ColorRGB.fromRGBA(red, green, blue, alpha); + } + + /** + * Converts a {@link Color} to it's HSL (hue, saturation, lightness) representation. + * + * @param color the {@link Color} to convert + * @return an immutable {@link ColorHSL} + */ + public static @NotNull ColorHSL rgbToHsl(@NotNull Color color) { + // normalize rgb values to between 0 and 1 + float red = color.getRed() / 255f; + float green = color.getGreen() / 255f; + float blue = color.getBlue() / 255f; + + float max = Math.max(red, Math.max(green, blue)); + float min = Math.min(red, Math.min(green, blue)); + + float hue, saturation, lightness; + lightness = (max + min) / 2f; // lightness = midpoint of max and min + + if (max == min) { + // achromatic (no hue or saturation) + hue = saturation = 0f; + } else { + float delta = max - min; + + // saturation depends on lightness (scales differently if lightness > 0.5) + saturation = lightness > 0.5f ? delta / (2f - max - min) : delta / (max + min); + + // determine hue by which channel is max + // normalize hue by converting from a 0-360 scale to a 0-1 scale by dividing by 6 + if (max == red) { + hue = ((green - blue) / delta + (green < blue ? 6f : 0f)) / 6f; + } else if (max == green) { + hue = ((blue - red) / delta + 2f) / 6f; + } else { + hue = ((red - green) / delta + 4f) / 6f; + } + } + return ColorHSL.fromHSLA(hue, saturation, lightness, color.getAlpha()); + } + + /** + * Converts a {@link ColorHSL} object to it's {@link ColorRGB} equivalent. + * + * @param hsl the {@link ColorHSL} to convert + * @return a {@link ColorRGB} object representing the same color + */ + public static @NotNull ColorRGB hslToRgb(@NotNull ColorHSL hsl) { + float hue = hsl.getHue(); + float saturation = hsl.getSaturation(); + float lightness = hsl.getLightness(); + + float red, green, blue; + + if (saturation == 0f) { + // achromatic i.e. gray (all channels equal to lightness) + red = green = blue = lightness; + } else { + // higherBound and lowerBound define two boundary colors + float lowerBound = lightness < 0.5f ? lightness * (1f + saturation) : (lightness + saturation) - (lightness * saturation); + float higherBound = 2f * lightness - lowerBound; + red = hueToRgb(higherBound, lowerBound, hue + 1f / 3f); + green = hueToRgb(higherBound, lowerBound, hue); + blue = hueToRgb(higherBound, lowerBound, hue - 1f / 3f); + } + int r = Math.round(red * 255f); + int g = Math.round(green * 255f); + int b = Math.round(blue * 255f); + return ColorRGB.fromRGBA(r, g, b, hsl.getAlpha()); + } + + /** + * Converts a {@link Color} to it's HSB (hue, saturation, brightness) representation. + * + * @param color the {@link Color} to convert + * @return an immutable {@link ColorHSB} + */ + public static @NotNull ColorHSB rgbToHsb(@NotNull Color color) { + float red = color.getRed() / 255f; + float green = color.getGreen() / 255f; + float blue = color.getBlue() / 255f; + + float max = Math.max(red, Math.max(green, blue)); + float min = Math.min(red, Math.min(green, blue)); + float delta = max - min; + + float hue = 0f; + if (delta != 0f) { + if (max == red) + hue = ((green - blue) / delta) % 6f; + else if (max == green) + hue = ((blue - red) / delta) + 2f; + else + hue = ((red - green) / delta) + 4f; + + hue /= 6f; + if (hue < 0f) + hue += 1f; + } + float saturation = max == 0f ? 0f : delta / max; + return ColorHSB.fromHSBA(hue, saturation, max, color.getAlpha()); + } + + /** + * Converts a {@link ColorHSB} object to it's {@link ColorRGB} equivalent. + * + * @param hsb the {@link ColorHSB} to convert + * @return a {@link ColorRGB} object representing the same color + */ + public static @NotNull ColorRGB hsbToRgb(@NotNull ColorHSB hsb) { + float hue = hsb.getHue(); + float saturation = hsb.getSaturation(); + float brightness = hsb.getBrightness(); + + // determine hue sector and offset within said sector + int sector = (int) (hue * 6f); + float remainder = hue * 6f - sector; + + // compute three intermediate values + float chromaLow = brightness * (1f - saturation); + float chromeHighSlope = brightness * (1f - remainder * saturation); + float chromaLowSlope = brightness * (1f - (1f - remainder) * saturation); + + float red = 0f, green = 0f, blue = 0f; + // pick the permutation of brightness, chroma low slope, and chroma lwo based on sector + switch (sector % 6) { + case 0 -> { + red = brightness; + green = chromaLowSlope; + blue = chromaLow; + } + case 1 -> { + red = chromeHighSlope; + green = brightness; + blue = chromaLow; + } + case 2 -> { + red = chromaLow; + green = brightness; + blue = chromaLowSlope; + } + case 3 -> { + red = chromaLow; + green = chromeHighSlope; + blue = brightness; + } + case 4 -> { + red = chromaLowSlope; + green = chromaLow; + blue = brightness; + } + case 5 -> { + red = brightness; + green = chromaLow; + blue = chromeHighSlope; + } + } + // scale to 0 - 255 range + return ColorRGB.fromRGBA(Math.round(red * 255f), Math.round(green * 255f), Math.round(blue * 255f), hsb.getAlpha()); + } + + /** + * Helper method to convert hue values to RGB. + * + * @param lowerBound intermediate value + * @param higherBound intermediate value + * @param hueOffset hue offset + * @return the calculated RGB value + */ + private static float hueToRgb(float lowerBound, float higherBound, float hueOffset) { + // wrap hueOffset if out of 0-1 range + if (hueOffset < 0f) + hueOffset += 1f; + if (hueOffset > 1f) + hueOffset -= 1f; + + // depending on hueOffset, interpolate between lowerBound and higherBound + if (hueOffset < 1f / 6f) + return lowerBound + (higherBound - lowerBound) * 6f * hueOffset; + if (hueOffset < 1f / 2f) + return higherBound; + if (hueOffset < 2f / 3f) + return lowerBound + (higherBound - lowerBound) * (2f / 3f - hueOffset) * 6f; + return lowerBound; + } + + /** + * Blends two {@link Color}s based on an amount from 0 to 100. + * + * @param c1 the first {@link Color} + * @param c2 the second {@link Color} + * @param amount the percentage amount to blend the colors (0 - 100) + * @return the blended color + */ + public static @NotNull Color blendColors(@NotNull Color c1, @NotNull Color c2, double amount) { + // amount is a percentage (clamp then normalize to between 0 and 1) + amount = Math2.fit(0, amount, 100) / 100.0; + + // linearly interpolate each channel + int red = (int) (c1.getRed() * (1 - amount) + c2.getRed() * amount); + int green = (int) (c1.getGreen() * (1 - amount) + c2.getGreen() * amount); + int blue = (int) (c1.getBlue() * (1 - amount) + c2.getBlue() * amount); + int alpha = (int) (c1.getAlpha() * (1 - amount) + c2.getAlpha() * amount); + return ColorRGB.fromRGBA(red, green, blue, alpha); + } + + /** + * Calculates the complement of a {@link Color}. + * + * @param color the {@link Color} to complement + * @return the complementary colour + */ + public static @NotNull Color complementColor(@NotNull Color color) { + // just invert each channel + int red = 255 - color.getRed(); + int green = 255 - color.getGreen(); + int blue = 255 - color.getBlue(); + return ColorRGB.fromRGBA(red, green, blue, color.getAlpha()); + } + + /** + * Calculates the complement of a {@link Color} using HSL adjustments. + * + * @param color the {@link Color} to complement + * @return the complementary colour + */ + public static @NotNull Color complementColorHSL(@NotNull Color color) { + ColorHSL hsl = rgbToHsl(color); + float newHue = (hsl.getHue() + 0.5f) % 1f; // 180 degree rotation + ColorHSL complemented = ColorHSL.fromHSLA(newHue, hsl.getSaturation(), hsl.getLightness(), color.getAlpha()); + return hslToRgb(complemented); + } + + /** + * Shades a {@link Color} by a given amount from 1 to 100. + * + * @param color the {@link Color} to shade + * @param amount the amount to shade the color by (1 - 100) + * @return the shaded color + */ + public static @NotNull ColorRGB shadeColor(@NotNull Color color, int amount) { + // reducing the channel values darkens the color + amount = Math2.fit(1, amount, 100); + double factor = (100 - amount) / 100.0; + int red = (int) (color.getRed() * factor); + int green = (int) (color.getGreen() * factor); + int blue = (int) (color.getBlue() * factor); + return ColorRGB.fromRGBA(red, green, blue, color.getAlpha()); + } + + /** + * Shades a {@link Color} by a given amount from 1 to 100 using HSL adjustments. + * + * @param color the {@link Color} to shade using HSL adjustments + * @param amount the amount to shade the color by (1 - 100) + * @return the shaded color + */ + public static @NotNull ColorRGB shadeColorHSL(@NotNull Color color, int amount) { + // reducing the lightness to shade + amount = Math2.fit(1, amount, 100); + ColorHSL hsl = rgbToHsl(color); + float newLightness = hsl.getLightness() * (100 - amount) / 100f; + ColorHSL shaded = ColorHSL.fromHSLA(hsl.getHue(), hsl.getSaturation(), newLightness, color.getAlpha()); + return hslToRgb(shaded); + } + + /** + * Tints a {@link Color} by a given amount from 1 to 100. + * + * @param color the {@link Color} to tint + * @param amount the amount to tint the color by (1 - 100) + * @return the tinted color + */ + public static @NotNull ColorRGB tintColor(@NotNull Color color, int amount) { + // move each channel closer to 255 to lighten the color + amount = Math2.fit(1, amount, 100); + double factor = amount / 100.0; + int red = (int) (color.getRed() + (255 - color.getRed()) * factor); + int green = (int) (color.getGreen() + (255 - color.getGreen()) * factor); + int blue = (int) (color.getBlue() + (255 - color.getBlue()) * factor); + return ColorRGB.fromRGBA(red, green, blue, color.getAlpha()); + } + + /** + * Tints a {@link Color} by a given amount from 1 to 100 using HSL adjustments. + * + * @param color the {@link Color} to tint using HSL adjustments + * @param amount the amount to tint the color by (1 - 100) + * @return the tinted color + */ + public static @NotNull ColorRGB tintColorHSL(@NotNull Color color, int amount) { + // increasing the lightness (towards 1) to tint + amount = Math2.fit(1, amount, 100); + ColorHSL hsl = rgbToHsl(color); + float newLightness = hsl.getLightness() + (1f - hsl.getLightness()) * (amount / 100f); + newLightness = Math.min(1f, newLightness); + ColorHSL tinted = ColorHSL.fromHSLA(hsl.getHue(), hsl.getSaturation(), newLightness, color.getAlpha()); + return hslToRgb(tinted); + } + + /** + * Rotates the hue of a {@link Color} by a given degree. + * + * @param color the {@link Color} to rotate the hue of + * @param degrees the number of degrees to rotate the hue by + * @return the hue-rotated color + */ + public static @NotNull Color rotateHue(@NotNull Color color, int degrees) { + // hue is a fraction of a circle, add (degrees/360) to rotate by that angle + ColorHSL hsl = rgbToHsl(color); + float newHue = (hsl.getHue() + degrees / 360f) % 1f; + if (newHue < 0f) + newHue += 1f; + ColorHSL rotated = ColorHSL.fromHSLA(newHue, hsl.getSaturation(), hsl.getLightness(), color.getAlpha()); + return hslToRgb(rotated); + } + + /** + * Adjusts the brightness of a {@link Color} by an amount from -100 to 100. + * This is similar to shading and tinting, but is slightly different. + * + * @param color the {@link Color} to adjust the brightness of + * @param amount the amount to adjust the brightness by (-100 - 100) + * @return the brightness-adjusted color + */ + public static @NotNull ColorRGB adjustBrightness(@NotNull Color color, int amount) { + // adjust brightness by scaling brightness directly (shocking stuff) + amount = Math2.fit(-100, amount, 100); + ColorHSB hsb = rgbToHsb(color); + float factor = amount / 100f; + float newBrightness = hsb.getBrightness() + hsb.getBrightness() * factor; + newBrightness = Math2.fit(0f, newBrightness, 1f); + return hsbToRgb(ColorHSB.fromHSBA(hsb.getHue(), hsb.getSaturation(), newBrightness, color.getAlpha())); + } + + /** + * Converts a {@link Color} to its grayscale equivalent. + * + * @param color the {@link Color} to convert to grayscale + * @return the colour's grayscale equivalent + */ + public static @NotNull ColorRGB toGrayscale(@NotNull Color color) { + // weighted average simulates human perception + int gray = (int) (0.299 * color.getRed() + 0.587 * color.getGreen() + 0.114 * color.getBlue()); + return ColorRGB.fromRGBA(gray, gray, gray, color.getAlpha()); + } + + /** + * Converts a {@link Color} to its sepiatone equivalent. + * + * @param color the {@link Color} to convert to sepiatone + * @return the colour's sepiatone equivalent + */ + public static @NotNull ColorRGB toSepia(@NotNull Color color) { + // standard sepia formula + int red = color.getRed(); + int green = color.getGreen(); + int blue = color.getBlue(); + int sepiaRed = (int) (0.393 * red + 0.769 * green + 0.189 * blue); + int sepiaGreen = (int) (0.349 * red + 0.686 * green + 0.168 * blue); + int sepiaBlue = (int) (0.272 * red + 0.534 * green + 0.131 * blue); + sepiaRed = Math.min(255, sepiaRed); + sepiaGreen = Math.min(255, sepiaGreen); + sepiaBlue = Math.min(255, sepiaBlue); + return ColorRGB.fromRGBA(sepiaRed, sepiaGreen, sepiaBlue, color.getAlpha()); + } + + /** + * Adjusts the temperature of a {@link Color} by changing the red and blue channel values. + * + * @param color the {@link Color} to adjust the temperature of + * @param amount the amount to adjust the temperature by (-255 - 255) + * @return the temperature-adjusted color + */ + public static @NotNull ColorRGB adjustTemperature(@NotNull Color color, int amount) { + // increasing red and decreasing blue 'warms' the color, opposite cools + int red = color.getRed() + amount; + int blue = color.getBlue() - amount; + red = Math2.fit(0, red, 255); + blue = Math2.fit(0, blue, 255); + return ColorRGB.fromRGBA(red, color.getGreen(), blue, color.getAlpha()); + } + +} diff --git a/src/main/java/org/skriptlang/skript/common/colors/elements/ExprBlend.java b/src/main/java/org/skriptlang/skript/common/colors/elements/ExprBlend.java new file mode 100644 index 00000000000..920e596f654 --- /dev/null +++ b/src/main/java/org/skriptlang/skript/common/colors/elements/ExprBlend.java @@ -0,0 +1,101 @@ +package org.skriptlang.skript.common.colors.elements; + +import ch.njol.skript.doc.Description; +import ch.njol.skript.doc.Examples; +import ch.njol.skript.doc.Name; +import ch.njol.skript.doc.Since; +import ch.njol.skript.lang.Expression; +import ch.njol.skript.lang.SkriptParser.ParseResult; +import ch.njol.skript.lang.SyntaxStringBuilder; +import ch.njol.skript.lang.util.SimpleExpression; +import ch.njol.skript.util.Color; +import ch.njol.util.Kleenean; +import org.bukkit.event.Event; +import org.jetbrains.annotations.Nullable; +import org.skriptlang.skript.common.colors.ColorUtils; +import org.skriptlang.skript.registration.SyntaxInfo; +import org.skriptlang.skript.registration.SyntaxRegistry; + +import java.util.ArrayList; +import java.util.List; + +@Name("Blended Colours") +@Description({ + "Returns the result of blending colours together. Optionally takes an amount to blend the colours by, which is" + + "a number from 0 to 100.", + "In that range, a 50 would be an expected equal blend of each colour (the default behaviour)." +}) +@Examples({ + "set {_purple} to red blended with blue", + "set {_goldyPurple} to {_purple} blended with gold", + "set {_morePurpleThanGold} to {_purple} blended with gold by an amount of 10", + "set {_aBunch} to red blended with all colours where [input is not red]" +}) +@Since("INSERT VERSION") +public class ExprBlend extends SimpleExpression { + + public static void register(SyntaxRegistry registry) { + registry.register(SyntaxRegistry.EXPRESSION, + SyntaxInfo.Expression.builder(ExprBlend.class, Color.class) + .addPatterns( + "%colors% (blended|mixed) with %colors% [by [(a factor|an amount) of] %-number%]", + "blend of %colors% (and|with) %colors% [by [(a factor|an amount) of] %-number%]", + "%colors% and %colors% blen(ded|t) together [by [(a factor|an amount) of] %-number%]" + ) + .supplier(ExprBlend::new) + .build() + ); + } + + private Expression colours, blendWith; + private Expression amount; + + @Override + @SuppressWarnings("unchecked") + public boolean init(Expression[] exprs, int matchedPattern, Kleenean isDelayed, ParseResult parseResult) { + this.colours = (Expression) exprs[0]; + this.blendWith = (Expression) exprs[1]; + this.amount = (Expression) exprs[2]; + return true; + } + + @Override + protected Color @Nullable [] get(Event event) { + Color[] colours = this.colours.getArray(event); + Color[] blendWiths = this.blendWith.getArray(event); + Number amount = this.amount == null ? 50 : this.amount.getOptionalSingle(event).orElse(50); + + List blendedColours = new ArrayList<>(); + for (Color colour : colours) { + Color blended = colour; + for (Color blendWith : blendWiths) { + blended = ColorUtils.blendColors(blended, blendWith, amount.doubleValue()); + } + blendedColours.add(blended); + } + + return blendedColours.toArray(new Color[0]); + } + + @Override + public boolean isSingle() { + return colours.isSingle(); + } + + @Override + public Class getReturnType() { + return Color.class; + } + + @Override + public String toString(@Nullable Event event, boolean debug) { + return new SyntaxStringBuilder(event, debug) + .append(colours) + .append("blended with") + .append(blendWith) + .append("by a factor of") + .append(amount) + .toString(); + } + +} diff --git a/src/main/java/org/skriptlang/skript/common/colors/elements/ExprComplementaryColor.java b/src/main/java/org/skriptlang/skript/common/colors/elements/ExprComplementaryColor.java new file mode 100644 index 00000000000..b003b89caa8 --- /dev/null +++ b/src/main/java/org/skriptlang/skript/common/colors/elements/ExprComplementaryColor.java @@ -0,0 +1,59 @@ +package org.skriptlang.skript.common.colors.elements; + +import ch.njol.skript.doc.Description; +import ch.njol.skript.doc.Examples; +import ch.njol.skript.doc.Name; +import ch.njol.skript.doc.Since; +import ch.njol.skript.expressions.base.SimplePropertyExpression; +import ch.njol.skript.lang.Expression; +import ch.njol.skript.lang.SkriptParser.ParseResult; +import ch.njol.skript.util.Color; +import ch.njol.util.Kleenean; +import org.jetbrains.annotations.Nullable; +import org.skriptlang.skript.common.colors.ColorUtils; +import org.skriptlang.skript.registration.SyntaxRegistry; + +@Name("Complementary Colours") +@Description({ + "Returns the complementary colour of a given colour(s).", + "Can optionally use a HSL-based approach if needed." +}) +@Examples({ + "set {_bluesComplement} to complement of blue", + "set {_allComplements::*} to complementary colours of all colours" +}) +@Since("INSERT VERSION") +public class ExprComplementaryColor extends SimplePropertyExpression { + + public static void register(SyntaxRegistry registry) { + registry.register(SyntaxRegistry.EXPRESSION, + infoBuilder( + ExprComplementaryColor.class, Color.class, + "[:hsl] complement[ary] [colo[u]r[s]]", "colors", false + ).build()); + } + + private boolean hsl; + + @Override + public boolean init(Expression[] expressions, int matchedPattern, Kleenean isDelayed, ParseResult parseResult) { + this.hsl = parseResult.hasTag("hsl"); + return super.init(expressions, matchedPattern, isDelayed, parseResult); + } + + @Override + public @Nullable Color convert(Color from) { + return hsl ? ColorUtils.complementColorHSL(from) : ColorUtils.complementColor(from); + } + + @Override + public Class getReturnType() { + return Color.class; + } + + @Override + protected String getPropertyName() { + return (hsl ? "hsl " : "") + "complementary color"; + } + +} diff --git a/src/test/skript/tests/syntaxes/expressions/ExprColorComplement.sk b/src/test/skript/tests/syntaxes/expressions/ExprColorComplement.sk new file mode 100644 index 00000000000..fcb9f3562ca --- /dev/null +++ b/src/test/skript/tests/syntaxes/expressions/ExprColorComplement.sk @@ -0,0 +1,21 @@ +test "colour complements": + + set {_bluesComplement} to complementary colour of blue + set {_bluesHslComplement} to hsl complementary colour of blue + set {_bluesComplementsComplement} to complementary colour of {_bluesComplement} + set {_bluesHslComplementsHslComplement} to hsl complementary colour of {_bluesHslComplement} + set {_trueBlacksComplement} to complementary colour of rgb(0, 0, 0) + set {_trueBlacksHslComplement} to hsl complementary colour of rgb(0, 0, 0) + set {_trueWhitesComplement} to complementary colour of rgb(255, 255, 255) + set {_trueWhitesHslComplement} to hsl complementary colour of rgb(255, 255, 255) + + assert hex code of {_bluesComplement} is "C3BB55" with "complement of blue was incorrect" + assert hex code of {_bluesHslComplement} is "AAA23C" with "hsl complement of blue was incorrect" + assert hex code of {_bluesComplementsComplement} is hex code of blue with "complement of blue's complement was incorrect" + assert hex code of {_bluesHslComplementsHslComplement} is hex code of blue with "hsl complement of blue's hsl complement was incorrect" + assert hex code of {_trueBlacksComplement} is "FFFFFF" with "complement of true black was incorrect" + assert hex code of {_trueBlacksHslComplement} is "000000" with "hsl complement of true black was incorrect" + assert hex code of {_trueWhitesComplement} is "000000" with "complement of true white was incorrect" + assert hex code of {_trueWhitesHslComplement} is "FFFFFF" with "hsl complement of true white was incorrect" + + assert complementary colour of {_x} is not set with "complement of an invalid type should return null" diff --git a/src/test/skript/tests/syntaxes/expressions/ExprColourBlend.sk b/src/test/skript/tests/syntaxes/expressions/ExprColourBlend.sk new file mode 100644 index 00000000000..642cd78e628 --- /dev/null +++ b/src/test/skript/tests/syntaxes/expressions/ExprColourBlend.sk @@ -0,0 +1,11 @@ +test "colour blends": + + set {_redAndBlue} to red blended with blue + set {_blackAndYellow} to black blended with yellow + set {_pinkAndPurple} to pink blended with purple by an amount of 20 + + assert hex code of {_redAndBlue} is "763968" with "hex of red and blue blended together was incorrect" + assert hex code of {_blackAndYellow} is "8D7A2F" with "hex of black and yellow blended together was incorrect" + assert hex code of {_pinkAndPurple} is "DD79AC" with "hex of pink and purple blended together by an amount of 20%% was incorrect" + + assert {_x} blended with {_y} is not set with "blending invalid types should return null" diff --git a/src/test/skript/tests/syntaxes/functions/colors/brightness.sk b/src/test/skript/tests/syntaxes/functions/colors/brightness.sk new file mode 100644 index 00000000000..5ae24ff16fa --- /dev/null +++ b/src/test/skript/tests/syntaxes/functions/colors/brightness.sk @@ -0,0 +1,17 @@ +test "colour brightness": + + set {_redBrightness-100} to brightness(red, -100) + set {_redBrightness-10} to brightness(red, -10) + set {_redBrightness10} to brightness(red, 10) + set {_redBrightness100} to brightness(red, 100) + set {_redBrightnessInfinity} to brightness(red, infinity value) + set {_redBrightnessNan} to brightness(red, nan value) + + assert hex code of {_redBrightness-100} is "000000" with "hex code of red brightened by -100 was incorrect" + assert hex code of {_redBrightness-10} is "9E2922" with "hex code of red brightened by -10 was incorrect" + assert hex code of {_redBrightness10} is "C2332A" with "hex code of red brightened by 10 was incorrect" + assert hex code of {_redBrightness100} is "FF4337" with "hex code of red brightened by 100 was incorrect" + assert hex code of {_redBrightnessInfinity} is "AE2E26" with "adjusting the brightness of a colour by infinity should clamp between -100 and 100" + assert hex code of {_redBrightnessNan} is "B02E26" with "adjusting the brightness of a colour by nan should clamp between -100 and 100" + + assert brightness({_x}, 4) is not set with "adjusting the brightness an invalid type shouldn't return anything" diff --git a/src/test/skript/tests/syntaxes/functions/colors/grayscale.sk b/src/test/skript/tests/syntaxes/functions/colors/grayscale.sk new file mode 100644 index 00000000000..f7579b4a8bb --- /dev/null +++ b/src/test/skript/tests/syntaxes/functions/colors/grayscale.sk @@ -0,0 +1,11 @@ +test "colour grayscales": + + set {_redGrayscale} to grayscale(red) + set {_redAndBlueGrayscale} to grayscale(red blended with blue) + set {_shadedRedGrayscale} to grayscale(shade(red, 50, true)) + + assert hex code of {_redGrayscale} is "535353" with "hex code of grayscale of red was incorrect" + assert hex code of {_redAndBlueGrayscale} is "505050" with "hex code of grayscale of red was incorrect" + assert hex code of {_shadedRedGrayscale} is "292929" with "hex code of grayscale of red was incorrect" + + assert grayscale({_x}) is not set with "getting the grayscale of an invalid type shouldn't return anything" diff --git a/src/test/skript/tests/syntaxes/functions/colors/sepiatone.sk b/src/test/skript/tests/syntaxes/functions/colors/sepiatone.sk new file mode 100644 index 00000000000..0376c6b7dc9 --- /dev/null +++ b/src/test/skript/tests/syntaxes/functions/colors/sepiatone.sk @@ -0,0 +1,11 @@ +test "colour sepiatones": + + set {_redSepiatone} to sepiatone(red) + set {_redAndBlueSepiatone} to sepiatone(red blended with blue) + set {_shadedRedSepiatone} to sepiatone(shade(red, 50, true)) + + assert hex code of {_redSepiatone} is "6F634D" with "hex code of sepiatone of red was incorrect" + assert hex code of {_redAndBlueSepiatone} is "6D614C" with "hex code of sepiatone of red was incorrect" + assert hex code of {_shadedRedSepiatone} is "373126" with "hex code of sepiatone of red was incorrect" + + assert sepiatone({_x}) is not set with "getting the sepiatone of an invalid type shouldn't return anything" diff --git a/src/test/skript/tests/syntaxes/functions/colors/shade.sk b/src/test/skript/tests/syntaxes/functions/colors/shade.sk new file mode 100644 index 00000000000..8f55113fafb --- /dev/null +++ b/src/test/skript/tests/syntaxes/functions/colors/shade.sk @@ -0,0 +1,19 @@ +test "colour shades": + + set {_redShade} to shade(red) + set {_redShade10} to shade(red, 10) + set {_redShade100} to shade(red, 100) + set {_redShadeHsl} to shade(red, 1, true) + set {_redShadeNegative} to shade(red, -4) + set {_redShadeInfinity} to shade(red, infinity value) + set {_redShadeNan} to shade(red, nan value) + + assert hex code of {_redShade} is "AE2D25" with "hex code of red shaded by 1 was incorrect" + assert hex code of {_redShade10} is "9E2922" with "hex code of red shaded by 10 was incorrect" + assert hex code of {_redShade100} is "000000" with "hex code of red shaded by 100 was incorrect" + assert hex code of {_redShadeHsl} is "AE2E26" with "hex code of red shaded by 1 using hsl methods was incorrect" + assert hex code of {_redShadeNegative} is "AE2D25" with "shading a colour by a negative amount should clamp between 1 and 100" + assert hex code of {_redShadeInfinity} is "AE2D25" with "shading a colour by infinity should clamp between 1 and 100" + assert hex code of {_redShadeNan} is "AE2D25" with "shading a colour by nan should clamp between 1 and 100" + + assert shade({_x}) is not set with "shading an invalid type shouldn't return anything" diff --git a/src/test/skript/tests/syntaxes/functions/colors/tint.sk b/src/test/skript/tests/syntaxes/functions/colors/tint.sk new file mode 100644 index 00000000000..8a398226562 --- /dev/null +++ b/src/test/skript/tests/syntaxes/functions/colors/tint.sk @@ -0,0 +1,19 @@ +test "colour tints": + + set {_redTint} to tint(red) + set {_redTint10} to tint(red, 10) + set {_redTint100} to tint(red, 100) + set {_redTintHsl} to tint(red, 1, true) + set {_redTintNegative} to tint(red, -4) + set {_redTintInfinity} to tint(red, infinity value) + set {_redTintNan} to tint(red, nan value) + + assert hex code of {_redTint} is "B03028" with "hex code of red tinted by 1 was incorrect" + assert hex code of {_redTint10} is "B7423B" with "hex code of red tinted by 10 was incorrect" + assert hex code of {_redTint100} is "FFFFFF" with "hex code of red tinted by 100 was incorrect" + assert hex code of {_redTintHsl} is "B22F27" with "hex code of red tinted by 1 using hsl methods was incorrect" + assert hex code of {_redTintNegative} is "B03028" with "tinting a colour by a negative amount should clamp between 1 and 100" + assert hex code of {_redTintInfinity} is "B03028" with "tinting a colour by infinity should clamp between 1 and 100" + assert hex code of {_redTintNan} is "B03028" with "tinting a colour by nan should clamp between 1 and 100" + + assert tint({_x}) is not set with "tinting an invalid type shouldn't return anything"