Skip to content

Commit

Permalink
feat(minimessage): super quick & dirty implementation of shadow gradi…
Browse files Browse the repository at this point in the history
…ent and rainbow tags
  • Loading branch information
zml2008 committed Dec 23, 2024
1 parent b3159e3 commit 354bc30
Show file tree
Hide file tree
Showing 6 changed files with 434 additions and 18 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -58,9 +61,10 @@
* <li>(color()? advanceColor())*</li>
* </ol>
*
* @param <S> the style attribute type
* @since 4.10.0
*/
abstract class AbstractColorChangingTag implements Modifying, Examinable {
abstract class AbstractAttributeChangingTag<S> 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
Expand Down Expand Up @@ -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;
}
Expand Down Expand Up @@ -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;
}

Expand All @@ -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();
}
}

Expand All @@ -165,15 +169,21 @@ 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.
*
* @return the current color
* @since 4.10.0
*/
protected abstract TextColor color();
protected abstract S attribute();

protected abstract <T extends StyleSetter<T>> T apply(final T style, final S attribute);

protected abstract <T extends StyleSetter<T>> 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.
Expand Down Expand Up @@ -241,4 +251,38 @@ public void emit(final @NotNull TokenEmitter emitter) {

return (TagInfoHolder) holder;
}

static abstract class OfColor extends AbstractAttributeChangingTag<TextColor> {
@Override
protected <T extends StyleSetter<T>> T apply(final T style, final TextColor attribute) {
return style.color(attribute);
}

@Override
protected <T extends StyleSetter<T>> 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<ShadowColor> {
@Override
protected <T extends StyleSetter<T>> T apply(final T style, final ShadowColor attribute) {
return style.shadowColor(attribute);
}

@Override
protected <T extends StyleSetter<T>> T applyIfAbsent(final T style, final ShadowColor attribute) {
return style.shadowColorIfAbsent(attribute);
}

@Override
protected @Nullable ShadowColor query(final Style style) {
return style.shadowColor();
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -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);
Expand Down
Original file line number Diff line number Diff line change
@@ -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<ShadowColor> 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<ShadowColor> 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<TokenEmitter> 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<? extends ExaminableProperty> 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;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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;
Expand All @@ -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));
Expand Down
Loading

0 comments on commit 354bc30

Please sign in to comment.