From 354bc30a7f579c93be2ebb02a1a9dd3f407fe2b0 Mon Sep 17 00:00:00 2001 From: zml Date: Sun, 22 Dec 2024 16:06:05 -0800 Subject: [PATCH] feat(minimessage): super quick & dirty implementation of shadow gradient and rainbow tags --- ...java => AbstractAttributeChangingTag.java} | 62 +++++- .../minimessage/tag/standard/GradientTag.java | 8 +- .../minimessage/tag/standard/GradowTag.java | 204 ++++++++++++++++++ .../minimessage/tag/standard/RainbowTag.java | 8 +- .../minimessage/tag/standard/ShainbowTag.java | 146 +++++++++++++ .../tag/standard/StandardTags.java | 24 ++- 6 files changed, 434 insertions(+), 18 deletions(-) rename text-minimessage/src/main/java/net/kyori/adventure/text/minimessage/tag/standard/{AbstractColorChangingTag.java => AbstractAttributeChangingTag.java} (80%) create mode 100644 text-minimessage/src/main/java/net/kyori/adventure/text/minimessage/tag/standard/GradowTag.java create mode 100644 text-minimessage/src/main/java/net/kyori/adventure/text/minimessage/tag/standard/ShainbowTag.java diff --git a/text-minimessage/src/main/java/net/kyori/adventure/text/minimessage/tag/standard/AbstractColorChangingTag.java b/text-minimessage/src/main/java/net/kyori/adventure/text/minimessage/tag/standard/AbstractAttributeChangingTag.java similarity index 80% rename from text-minimessage/src/main/java/net/kyori/adventure/text/minimessage/tag/standard/AbstractColorChangingTag.java rename to text-minimessage/src/main/java/net/kyori/adventure/text/minimessage/tag/standard/AbstractAttributeChangingTag.java index 2639981ac..ad48736e9 100644 --- a/text-minimessage/src/main/java/net/kyori/adventure/text/minimessage/tag/standard/AbstractColorChangingTag.java +++ b/text-minimessage/src/main/java/net/kyori/adventure/text/minimessage/tag/standard/AbstractAttributeChangingTag.java @@ -34,6 +34,9 @@ import net.kyori.adventure.text.VirtualComponent; import net.kyori.adventure.text.VirtualComponentRenderer; import net.kyori.adventure.text.flattener.ComponentFlattener; +import net.kyori.adventure.text.format.ShadowColor; +import net.kyori.adventure.text.format.Style; +import net.kyori.adventure.text.format.StyleSetter; import net.kyori.adventure.text.format.TextColor; import net.kyori.adventure.text.minimessage.internal.parser.node.TagNode; import net.kyori.adventure.text.minimessage.internal.parser.node.ValueNode; @@ -58,9 +61,10 @@ *
  • (color()? advanceColor())*
  • * * + * @param the style attribute type * @since 4.10.0 */ -abstract class AbstractColorChangingTag implements Modifying, Examinable { +abstract class AbstractAttributeChangingTag implements Modifying, Examinable { private static final ComponentFlattener LENGTH_CALCULATOR = ComponentFlattener.builder() .mapper(TextComponent.class, TextComponent::content) .unknownMapper(x -> "_") // every unknown component gets a single colour @@ -106,7 +110,7 @@ public final Component apply(final @NotNull Component current, final int depth) return Component.virtual(Void.class, new TagInfoHolder(this.preserveData(), current), current.style()); } - if ((this.disableApplyingColorDepth != -1 && depth > this.disableApplyingColorDepth) || current.style().color() != null) { + if ((this.disableApplyingColorDepth != -1 && depth > this.disableApplyingColorDepth) || this.query(current.style()) != null) { if (this.disableApplyingColorDepth == -1 || depth < this.disableApplyingColorDepth) { this.disableApplyingColorDepth = depth; } @@ -135,15 +139,15 @@ public final Component apply(final @NotNull Component current, final int depth) final int[] holder = new int[1]; for (final PrimitiveIterator.OfInt it = content.codePoints().iterator(); it.hasNext();) { holder[0] = it.nextInt(); - final Component comp = Component.text(new String(holder, 0, 1), current.style().color(this.color())); - this.advanceColor(); + final Component comp = Component.text(new String(holder, 0, 1), this.apply(current.style(), this.attribute())); + this.advanceAttribute(); parent.append(comp); } return parent.build(); } else if (!(current instanceof TextComponent)) { - final Component ret = current.children(Collections.emptyList()).colorIfAbsent(this.color()); - this.advanceColor(); + final Component ret = this.applyIfAbsent(current.children(Collections.emptyList()), this.attribute()); + this.advanceAttribute(); return ret; } @@ -154,7 +158,7 @@ private void skipColorForLengthOf(final String content) { final int len = content.codePointCount(0, content.length()); for (int i = 0; i < len; i++) { // increment our color index - this.advanceColor(); + this.advanceAttribute(); } } @@ -165,7 +169,7 @@ private void skipColorForLengthOf(final String content) { /** * Advance the active color. */ - protected abstract void advanceColor(); + protected abstract void advanceAttribute(); /** * Get the current color, without side-effects. @@ -173,7 +177,13 @@ private void skipColorForLengthOf(final String content) { * @return the current color * @since 4.10.0 */ - protected abstract TextColor color(); + protected abstract S attribute(); + + protected abstract > T apply(final T style, final S attribute); + + protected abstract > T applyIfAbsent(final T style, final S attribute); + + protected abstract @Nullable S query(final Style style); /** * Return an emitable that will accurately reserialize the provided input data. @@ -241,4 +251,38 @@ public void emit(final @NotNull TokenEmitter emitter) { return (TagInfoHolder) holder; } + + static abstract class OfColor extends AbstractAttributeChangingTag { + @Override + protected > T apply(final T style, final TextColor attribute) { + return style.color(attribute); + } + + @Override + protected > T applyIfAbsent(final T style, final TextColor attribute) { + return style.colorIfAbsent(attribute); + } + + @Override + protected @Nullable TextColor query(final Style style) { + return style.color(); + } + } + + static abstract class OfShadowColor extends AbstractAttributeChangingTag { + @Override + protected > T apply(final T style, final ShadowColor attribute) { + return style.shadowColor(attribute); + } + + @Override + protected > T applyIfAbsent(final T style, final ShadowColor attribute) { + return style.shadowColorIfAbsent(attribute); + } + + @Override + protected @Nullable ShadowColor query(final Style style) { + return style.shadowColor(); + } + } } diff --git a/text-minimessage/src/main/java/net/kyori/adventure/text/minimessage/tag/standard/GradientTag.java b/text-minimessage/src/main/java/net/kyori/adventure/text/minimessage/tag/standard/GradientTag.java index fa191ba3b..312f3bcc4 100644 --- a/text-minimessage/src/main/java/net/kyori/adventure/text/minimessage/tag/standard/GradientTag.java +++ b/text-minimessage/src/main/java/net/kyori/adventure/text/minimessage/tag/standard/GradientTag.java @@ -49,12 +49,12 @@ * * @since 4.10.0 */ -class GradientTag extends AbstractColorChangingTag { +class GradientTag extends AbstractAttributeChangingTag.OfColor { private static final String GRADIENT = "gradient"; private static final TextColor DEFAULT_WHITE = TextColor.color(0xffffff); private static final TextColor DEFAULT_BLACK = TextColor.color(0x000000); - static final TagResolver RESOLVER = SerializableResolver.claimingComponent(GRADIENT, GradientTag::create, AbstractColorChangingTag::claimComponent); + static final TagResolver RESOLVER = SerializableResolver.claimingComponent(GRADIENT, GradientTag::create, AbstractAttributeChangingTag::claimComponent); private int index = 0; @@ -125,12 +125,12 @@ protected void init() { } @Override - protected void advanceColor() { + protected void advanceAttribute() { this.index++; } @Override - protected TextColor color() { + protected TextColor attribute() { // from [0, this.colors.length - 1], select the position in the gradient // we will wrap around in order to preserve an even cycle as would be seen with non-zero phases final double position = ((this.index * this.multiplier) + this.phase); diff --git a/text-minimessage/src/main/java/net/kyori/adventure/text/minimessage/tag/standard/GradowTag.java b/text-minimessage/src/main/java/net/kyori/adventure/text/minimessage/tag/standard/GradowTag.java new file mode 100644 index 000000000..eea2143e4 --- /dev/null +++ b/text-minimessage/src/main/java/net/kyori/adventure/text/minimessage/tag/standard/GradowTag.java @@ -0,0 +1,204 @@ +/* + * This file is part of adventure, licensed under the MIT License. + * + * Copyright (c) 2017-2024 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.function.Consumer; +import java.util.stream.Stream; +import net.kyori.adventure.text.format.NamedTextColor; +import net.kyori.adventure.text.format.ShadowColor; +import net.kyori.adventure.text.format.TextColor; +import net.kyori.adventure.text.minimessage.Context; +import net.kyori.adventure.text.minimessage.internal.serializer.SerializableResolver; +import net.kyori.adventure.text.minimessage.internal.serializer.TokenEmitter; +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.ExaminableProperty; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; +import org.jetbrains.annotations.Range; + +/** + * A transformation that applies a colour gradient. + * + * @since 4.19.0 + */ +class GradowTag extends AbstractAttributeChangingTag.OfShadowColor { + private static final String GRADOW = "gradow"; + private static final int SHADOW_ALPHA = 0xcc; + private static final ShadowColor DEFAULT_WHITE = ShadowColor.shadowColor(0xccffffff); + private static final ShadowColor DEFAULT_BLACK = ShadowColor.shadowColor(0xcc000000); + + static final TagResolver RESOLVER = SerializableResolver.claimingComponent(GRADOW, GradowTag::create, AbstractAttributeChangingTag::claimComponent); + + private int index = 0; + + private double multiplier = 1; + + private final ShadowColor[] colors; + @Range(from = -1, to = 1) double phase; + + private final boolean negativePhase; + + static Tag create(final ArgumentQueue args, final Context ctx) { + double phase = 0; + final List textColors; + if (args.hasNext()) { + textColors = new ArrayList<>(); + while (args.hasNext()) { + final Argument arg = args.pop(); + // last argument? maybe this is the phase? + if (!args.hasNext()) { + final OptionalDouble possiblePhase = arg.asDouble(); + if (possiblePhase.isPresent()) { + phase = possiblePhase.getAsDouble(); + if (phase < -1d || phase > 1d) { + throw ctx.newException(String.format("Shadow gradient phase is out of range (%s). Must be in the range [-1.0, 1.0] (inclusive).", phase), args); + } + break; + } + } + + final ShadowColor parsedColor = ShadowColor.shadowColor(ColorTagResolver.resolveColor(arg.value(), ctx), SHADOW_ALPHA); + textColors.add(parsedColor); + } + + if (textColors.size() == 1) { + throw ctx.newException("Invalid gradient, not enough colors. Gradients must have at least two colors.", args); + } + } else { + textColors = Collections.emptyList(); + } + + return new GradowTag(phase, textColors); + } + + GradowTag(final double phase, final List colors) { + if (colors.isEmpty()) { + this.colors = new ShadowColor[]{DEFAULT_WHITE, DEFAULT_BLACK}; + } else { + this.colors = colors.toArray(new ShadowColor[0]); + } + + if (phase < 0) { + this.negativePhase = true; + this.phase = 1 + phase; // [-1, 0) -> [0, 1) + Collections.reverse(Arrays.asList(this.colors)); + } else { + this.negativePhase = false; + this.phase = phase; + } + } + + @Override + protected void init() { + // Set a scaling factor for character indices, so that the colours in a gradient are evenly spread across the original text + // make it so the max character index maps to the maximum colour + this.multiplier = this.size() == 1 ? 0 : (double) (this.colors.length - 1) / (this.size() - 1); + this.phase *= this.colors.length - 1; + this.index = 0; + } + + @Override + protected void advanceAttribute() { + this.index++; + } + + @Override + protected ShadowColor attribute() { + // from [0, this.colors.length - 1], select the position in the gradient + // we will wrap around in order to preserve an even cycle as would be seen with non-zero phases + final double position = ((this.index * this.multiplier) + this.phase); + final int lowUnclamped = (int) Math.floor(position); + + final int high = (int) Math.ceil(position) % this.colors.length; + final int low = lowUnclamped % this.colors.length; + + return ShadowColor.lerp((float) position - lowUnclamped, this.colors[low], this.colors[high]); + } + + @Override + protected @NotNull Consumer preserveData() { + final ShadowColor[] colors; + final double phase; + + if (this.negativePhase) { + colors = Arrays.copyOf(this.colors, this.colors.length); + Collections.reverse(Arrays.asList(colors)); + phase = this.phase - 1; + } else { + colors = this.colors; + phase = this.phase; + } + + return emit -> { + emit.tag(GRADOW); + if (colors.length != 2 || !colors[0].equals(DEFAULT_WHITE) || !colors[1].equals(DEFAULT_BLACK)) { // non-default params + for (final ShadowColor shadow : colors) { + final TextColor color = TextColor.color(shadow); + if (color instanceof NamedTextColor) { + emit.argument(NamedTextColor.NAMES.keyOrThrow((NamedTextColor) color)); + } else { + emit.argument(color.asHexString()); + } + } + } + + if (phase != 0) { + emit.argument(Double.toString(phase)); + } + }; + } + + @Override + public @NotNull Stream examinableProperties() { + return Stream.of( + ExaminableProperty.of("phase", this.phase), + ExaminableProperty.of("colors", this.colors) + ); + } + + @Override + public boolean equals(final @Nullable Object other) { + if (this == other) return true; + if (other == null || this.getClass() != other.getClass()) return false; + final GradowTag that = (GradowTag) other; + return this.index == that.index + && this.phase == that.phase + && Arrays.equals(this.colors, that.colors); + } + + @Override + public int hashCode() { + int result = Objects.hash(this.index, this.phase); + result = 31 * result + Arrays.hashCode(this.colors); + return result; + } +} diff --git a/text-minimessage/src/main/java/net/kyori/adventure/text/minimessage/tag/standard/RainbowTag.java b/text-minimessage/src/main/java/net/kyori/adventure/text/minimessage/tag/standard/RainbowTag.java index 17018ee90..cc5e30f69 100644 --- a/text-minimessage/src/main/java/net/kyori/adventure/text/minimessage/tag/standard/RainbowTag.java +++ b/text-minimessage/src/main/java/net/kyori/adventure/text/minimessage/tag/standard/RainbowTag.java @@ -43,11 +43,11 @@ * * @since 4.10.0 */ -final class RainbowTag extends AbstractColorChangingTag { +final class RainbowTag extends AbstractAttributeChangingTag.OfColor { private static final String REVERSE = "!"; private static final String RAINBOW = "rainbow"; - static final TagResolver RESOLVER = SerializableResolver.claimingComponent(RAINBOW, RainbowTag::create, AbstractColorChangingTag::claimComponent); + static final TagResolver RESOLVER = SerializableResolver.claimingComponent(RAINBOW, RainbowTag::create, AbstractAttributeChangingTag::claimComponent); private final boolean reversed; private final double dividedPhase; @@ -89,7 +89,7 @@ protected void init() { } @Override - protected void advanceColor() { + protected void advanceAttribute() { if (this.reversed) { if (this.colorIndex == 0) { this.colorIndex = this.size() - 1; @@ -102,7 +102,7 @@ protected void advanceColor() { } @Override - protected TextColor color() { + protected TextColor attribute() { final float index = this.colorIndex; final float hue = (float) ((index / this.size() + this.dividedPhase) % 1f); return TextColor.color(HSVLike.hsvLike(hue, 1f, 1f)); diff --git a/text-minimessage/src/main/java/net/kyori/adventure/text/minimessage/tag/standard/ShainbowTag.java b/text-minimessage/src/main/java/net/kyori/adventure/text/minimessage/tag/standard/ShainbowTag.java new file mode 100644 index 000000000..b3ca51988 --- /dev/null +++ b/text-minimessage/src/main/java/net/kyori/adventure/text/minimessage/tag/standard/ShainbowTag.java @@ -0,0 +1,146 @@ +/* + * This file is part of adventure, licensed under the MIT License. + * + * Copyright (c) 2017-2024 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.Objects; +import java.util.function.Consumer; +import java.util.stream.Stream; +import net.kyori.adventure.text.format.ShadowColor; +import net.kyori.adventure.text.format.TextColor; +import net.kyori.adventure.text.minimessage.Context; +import net.kyori.adventure.text.minimessage.internal.serializer.SerializableResolver; +import net.kyori.adventure.text.minimessage.internal.serializer.TokenEmitter; +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.adventure.util.HSVLike; +import net.kyori.examination.ExaminableProperty; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +/** + * Applies rainbow color to a component. + * + * @since 4.19.0 + */ +final class ShainbowTag extends AbstractAttributeChangingTag.OfShadowColor { + private static final String REVERSE = "!"; + private static final String RAINBOW = "shainbow"; + private static final int SHADOW_ALPHA = 0xcc; + + static final TagResolver RESOLVER = SerializableResolver.claimingComponent(RAINBOW, ShainbowTag::create, AbstractAttributeChangingTag::claimComponent); + + private final boolean reversed; + private final double dividedPhase; + + private int colorIndex = 0; + + static Tag create(final ArgumentQueue args, final Context ctx) { + boolean reversed = false; + int phase = 0; + + if (args.hasNext()) { + String value = args.pop().value(); + if (value.startsWith(REVERSE)) { + reversed = true; + value = value.substring(REVERSE.length()); + } + if (value.length() > 0) { + try { + phase = Integer.parseInt(value); + } catch (final NumberFormatException ex) { + throw ctx.newException("Expected phase, got " + value); + } + } + } + + return new ShainbowTag(reversed, phase); + } + + private ShainbowTag(final boolean reversed, final int phase) { + this.reversed = reversed; + this.dividedPhase = ((double) phase) / 10d; + } + + @Override + protected void init() { + if (this.reversed) { + this.colorIndex = this.size() - 1; + } + } + + @Override + protected void advanceAttribute() { + if (this.reversed) { + if (this.colorIndex == 0) { + this.colorIndex = this.size() - 1; + } else { + this.colorIndex--; + } + } else { + this.colorIndex++; + } + } + + @Override + protected ShadowColor attribute() { + final float index = this.colorIndex; + final float hue = (float) ((index / this.size() + this.dividedPhase) % 1f); + return ShadowColor.shadowColor(TextColor.color(HSVLike.hsvLike(hue, 1f, 1f)), SHADOW_ALPHA); + } + + @Override + protected @NotNull Consumer preserveData() { + final boolean reversed = this.reversed; + final int phase = (int) Math.round(this.dividedPhase * 10); + return emit -> { + emit.tag(RAINBOW); + if (reversed && phase != 0) { + emit.argument(REVERSE + phase); + } else if (reversed) { + emit.argument(REVERSE); + } else if (phase != 0) { + emit.argument(Integer.toString(phase)); + } + }; + } + + @Override + public @NotNull Stream examinableProperties() { + return Stream.of(ExaminableProperty.of("phase", this.dividedPhase)); + } + + @Override + public boolean equals(final @Nullable Object other) { + if (this == other) return true; + if (other == null || this.getClass() != other.getClass()) return false; + final ShainbowTag that = (ShainbowTag) other; + return this.colorIndex == that.colorIndex && this.dividedPhase == that.dividedPhase; + } + + @Override + public int hashCode() { + return Objects.hash(this.colorIndex, this.dividedPhase); + } +} 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 3b36e851f..9f7991ba7 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 @@ -65,7 +65,9 @@ private StandardTags() { ScoreTag.RESOLVER, NbtTag.RESOLVER, PrideTag.RESOLVER, - ShadowColorTag.RESOLVER + ShadowColorTag.RESOLVER, + GradowTag.RESOLVER, + ShainbowTag.RESOLVER ) .build(); @@ -288,6 +290,26 @@ public static TagResolver transition() { return ShadowColorTag.RESOLVER; } + /** + * Get a resolver for the {@value ShainbowTag#RAINBOW} tags. + * + * @return a resolver for the {@value ShainbowTag#RAINBOW} tags + * @since 4.18.0 + */ + public static @NotNull TagResolver shainhow() { + return ShainbowTag.RESOLVER; + } + + /** + * Get a resolver for the {@value GradowTag#GRADOW} tags. + * + * @return a resolver for the {@value GradowTag#GRADOW} tags + * @since 4.18.0 + */ + public static @NotNull TagResolver gradow() { + return GradowTag.RESOLVER; + } + /** * Get a resolver that handles all default standard tags. *