From 7184b40765d27e48338325bf4d5b521e3e209c65 Mon Sep 17 00:00:00 2001 From: MiniDigger Date: Mon, 14 Feb 2022 18:29:50 +0100 Subject: [PATCH] Implement TransitionTag --- .../tag/standard/StandardTags.java | 13 +- .../tag/standard/TransitionTag.java | 163 ++++++++++++++++++ .../tag/standard/TransitionTagTest.java | 48 ++++++ 3 files changed, 223 insertions(+), 1 deletion(-) create mode 100644 text-minimessage/src/main/java/net/kyori/adventure/text/minimessage/tag/standard/TransitionTag.java create mode 100644 text-minimessage/src/test/java/net/kyori/adventure/text/minimessage/tag/standard/TransitionTagTest.java diff --git a/text-minimessage/src/main/java/net/kyori/adventure/text/minimessage/tag/standard/StandardTags.java b/text-minimessage/src/main/java/net/kyori/adventure/text/minimessage/tag/standard/StandardTags.java index f2ed77848..3674d5802 100644 --- a/text-minimessage/src/main/java/net/kyori/adventure/text/minimessage/tag/standard/StandardTags.java +++ b/text-minimessage/src/main/java/net/kyori/adventure/text/minimessage/tag/standard/StandardTags.java @@ -58,7 +58,8 @@ private StandardTags() { GradientTag.RESOLVER, RainbowTag.RESOLVER, ResetTag.RESOLVER, - NewlineTag.RESOLVER + NewlineTag.RESOLVER, + TransitionTag.RESOLVER ) .build(); @@ -181,6 +182,16 @@ private StandardTags() { return RainbowTag.RESOLVER; } + /** + * Get a resolver for the {@value TransitionTag#TRANSITION} tag. + * + * @return a resolver for the {@value TransitionTag#TRANSITION} tag + * @since 4.10.0 + */ + public static TagResolver transition() { + return TransitionTag.RESOLVER; + } + /** * Get a resolver for the {@value ResetTag#RESET} tag. * diff --git a/text-minimessage/src/main/java/net/kyori/adventure/text/minimessage/tag/standard/TransitionTag.java b/text-minimessage/src/main/java/net/kyori/adventure/text/minimessage/tag/standard/TransitionTag.java new file mode 100644 index 000000000..6e7dafe68 --- /dev/null +++ b/text-minimessage/src/main/java/net/kyori/adventure/text/minimessage/tag/standard/TransitionTag.java @@ -0,0 +1,163 @@ +/* + * This file is part of adventure, licensed under the MIT License. + * + * Copyright (c) 2017-2022 KyoriPowered + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ +package net.kyori.adventure.text.minimessage.tag.standard; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.Objects; +import java.util.OptionalDouble; +import java.util.stream.Stream; +import net.kyori.adventure.text.Component; +import net.kyori.adventure.text.format.NamedTextColor; +import net.kyori.adventure.text.format.TextColor; +import net.kyori.adventure.text.minimessage.Context; +import net.kyori.adventure.text.minimessage.tag.Inserting; +import net.kyori.adventure.text.minimessage.tag.Tag; +import net.kyori.adventure.text.minimessage.tag.resolver.ArgumentQueue; +import net.kyori.adventure.text.minimessage.tag.resolver.TagResolver; +import net.kyori.examination.Examinable; +import net.kyori.examination.ExaminableProperty; +import org.jetbrains.annotations.NotNull; + +/** + * Changes the color based on a phase param. + * + * @since 4.10.0 + */ +public final class TransitionTag implements Inserting, Examinable { + public static final String TRANSITION = "transition"; + + private final TextColor[] colors; + private final float phase; + private final boolean negativePhase; + + static final TagResolver RESOLVER = TagResolver.resolver(TransitionTag.TRANSITION, TransitionTag::create); + + static Tag create(final ArgumentQueue args, final Context ctx) { + float phase = 0; + final List textColors; + if (args.hasNext()) { + textColors = new ArrayList<>(); + while (args.hasNext()) { + final Tag.Argument arg = args.pop(); + // last argument? maybe this is the phase? + if (!args.hasNext()) { + final OptionalDouble possiblePhase = arg.asDouble(); + if (possiblePhase.isPresent()) { + phase = (float) possiblePhase.getAsDouble(); + if (phase < -1f || phase > 1f) { + throw ctx.newException(String.format("Gradient phase is out of range (%s). Must be in the range [-1.0f, 1.0f] (inclusive).", phase), args); + } + break; + } + } + + final String argValue = arg.value(); + final TextColor parsedColor; + if (argValue.charAt(0) == '#') { + parsedColor = TextColor.fromHexString(argValue); + } else { + parsedColor = NamedTextColor.NAMES.value(arg.lowerValue()); + } + if (parsedColor == null) { + throw ctx.newException(String.format("Unable to parse a color from '%s'. Please use named colors or hex (#RRGGBB) colors.", argValue), args); + } + textColors.add(parsedColor); + } + + if (textColors.size() < 2) { + throw ctx.newException("Invalid transition, not enough colors. Transitions must have at least two colors.", args); + } + } else { + textColors = Collections.emptyList(); + } + + return new TransitionTag(phase, textColors); + } + + private TransitionTag(final float phase, final List colors) { + if (phase < 0) { + this.negativePhase = true; + this.phase = 1 + phase; + Collections.reverse(colors); + } else { + this.negativePhase = false; + this.phase = phase; + } + + if (colors.isEmpty()) { + this.colors = new TextColor[]{TextColor.color(0xffffff), TextColor.color(0x000000)}; + } else { + this.colors = colors.toArray(new TextColor[0]); + } + } + + @Override + public @NotNull Component value() { + return Component.text("", this.color()); + } + + private TextColor color() { + final float steps = 1f / (this.colors.length - 1); + for (int colorIndex = 1; colorIndex < this.colors.length; colorIndex++) { + final float val = colorIndex * steps; + if (val >= this.phase) { + final float factor = 1 + (this.phase - val) * (this.colors.length - 1); + + if (this.negativePhase) { + // flip the gradient segment for to allow for looping phase -1 through 1 + return TextColor.lerp(1 - factor, this.colors[colorIndex], this.colors[colorIndex - 1]); + } else { + return TextColor.lerp(factor, this.colors[colorIndex - 1], this.colors[colorIndex]); + } + } + } + return this.colors[0]; + } + + @Override + public @NotNull Stream examinableProperties() { + return Stream.of( + ExaminableProperty.of("phase", this.phase), + ExaminableProperty.of("colors", this.colors) + ); + } + + @Override + public boolean equals(final Object other) { + if (this == other) return true; + if (other == null || this.getClass() != other.getClass()) return false; + final TransitionTag that = (TransitionTag) other; + return this.phase == that.phase && Arrays.equals(this.colors, that.colors); + } + + @Override + public int hashCode() { + int result = Objects.hash(this.phase); + result = 31 * result + Arrays.hashCode(this.colors); + return result; + } +} diff --git a/text-minimessage/src/test/java/net/kyori/adventure/text/minimessage/tag/standard/TransitionTagTest.java b/text-minimessage/src/test/java/net/kyori/adventure/text/minimessage/tag/standard/TransitionTagTest.java new file mode 100644 index 000000000..a123f0d75 --- /dev/null +++ b/text-minimessage/src/test/java/net/kyori/adventure/text/minimessage/tag/standard/TransitionTagTest.java @@ -0,0 +1,48 @@ +/* + * This file is part of adventure, licensed under the MIT License. + * + * Copyright (c) 2017-2022 KyoriPowered + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ +package net.kyori.adventure.text.minimessage.tag.standard; + +import net.kyori.adventure.text.Component; +import net.kyori.adventure.text.format.TextColor; +import net.kyori.adventure.text.minimessage.AbstractTest; +import org.junit.jupiter.api.Test; + +import static net.kyori.adventure.text.Component.text; +import static net.kyori.adventure.text.format.NamedTextColor.BLACK; +import static net.kyori.adventure.text.format.NamedTextColor.WHITE; +import static net.kyori.adventure.text.format.TextColor.color; + +class TransitionTagTest extends AbstractTest { + + @Test + void testTransition() { + final TextColor[] colors = new TextColor[] {BLACK, color(0x808080), WHITE, color(0x808080), BLACK}; + final float[] phases = new float[] {-1, 0.5f, 0, 0.5f, 1}; + for (int i = 0; i < colors.length; i++) { + final String input = "Hello World"; + final Component expected = text("", colors[i]).append(text("Hello World")); + this.assertParsedEquals(expected, input); + } + } +}