Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

shadow gradient and rainbow tags #1147

Draft
wants to merge 1 commit into
base: main/4
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading