瀏覽代碼

Improved Scrolling

Danielshe 5 年之前
父節點
當前提交
689b520433

+ 1 - 1
gradle.properties

@@ -2,5 +2,5 @@ minecraft_version=19w41a
 yarn_version=19w41a+build.3
 fabric_loader_version=0.6.3+build.167
 fabric_version=0.4.4+build.248-1.15
-mod_version=2.0.0-unstable
+mod_version=2.0.1-unstable
 modmenu_version=1.7.6+build.115

+ 97 - 43
src/main/java/me/shedaniel/clothconfig2/ClothConfigInitializer.java

@@ -1,10 +1,11 @@
 package me.shedaniel.clothconfig2;
 
-import com.google.common.collect.ImmutableList;
 import me.shedaniel.clothconfig2.api.ConfigBuilder;
 import me.shedaniel.clothconfig2.api.ConfigCategory;
 import me.shedaniel.clothconfig2.api.ConfigEntryBuilder;
-import me.shedaniel.clothconfig2.impl.builders.SubCategoryBuilder;
+import me.shedaniel.clothconfig2.impl.EasingMethod;
+import me.shedaniel.clothconfig2.impl.EasingMethod.EasingMethodImpl;
+import me.shedaniel.clothconfig2.impl.EasingMethods;
 import net.fabricmc.api.ClientModInitializer;
 import net.fabricmc.loader.api.FabricLoader;
 import net.minecraft.client.MinecraftClient;
@@ -12,46 +13,116 @@ import net.minecraft.util.Identifier;
 import org.apache.logging.log4j.LogManager;
 import org.apache.logging.log4j.Logger;
 
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileWriter;
 import java.lang.reflect.Method;
-import java.util.Arrays;
-import java.util.Optional;
+import java.util.Properties;
 
 public class ClothConfigInitializer implements ClientModInitializer {
     
     public static final Logger LOGGER = LogManager.getFormatterLogger("ClothConfig");
+    private static EasingMethod easingMethod = EasingMethodImpl.QUART;
+    private static long scrollDuration = 1000;
+    private static double scrollStep = 16;
+    private static double bounceBackMultiplier = .93;
+    
+    public static EasingMethod getEasingMethod() {
+        return easingMethod;
+    }
+    
+    public static long getScrollDuration() {
+        return scrollDuration;
+    }
+    
+    public static double getScrollStep() {
+        return scrollStep;
+    }
+    
+    public static double getBounceBackMultiplier() {
+        return bounceBackMultiplier;
+    }
+    
+    private static void loadConfig() {
+        File file = new File(FabricLoader.getInstance().getConfigDirectory(), "cloth-config2/config.properties");
+        try {
+            file.getParentFile().mkdirs();
+            easingMethod = EasingMethodImpl.QUART;
+            scrollDuration = 1000;
+            scrollStep = 16;
+            bounceBackMultiplier = .93;
+            if (!file.exists()) {
+                saveConfig();
+            }
+            Properties properties = new Properties();
+            properties.load(new FileInputStream(file));
+            String easing = properties.getProperty("easingMethod", "QUART");
+            for(EasingMethod value : EasingMethods.getMethods()) {
+                if (value.toString().equalsIgnoreCase(easing)) {
+                    easingMethod = value;
+                    break;
+                }
+            }
+            scrollDuration = Long.parseLong(properties.getProperty("scrollDuration", "1000"));
+            scrollStep = Double.parseDouble(properties.getProperty("scrollStep", "16"));
+            bounceBackMultiplier = Double.parseDouble(properties.getProperty("bounceBackMultiplier", "0.93"));
+        } catch (Exception e) {
+            e.printStackTrace();
+            easingMethod = EasingMethodImpl.QUART;
+            scrollDuration = 1000;
+            scrollStep = 16;
+            bounceBackMultiplier = .93;
+            try {
+                if (file.exists())
+                    file.delete();
+            } catch (Exception ignored) {
+            }
+            saveConfig();
+        }
+    }
+    
+    private static void saveConfig() {
+        File file = new File(FabricLoader.getInstance().getConfigDirectory(), "cloth-config2/config.properties");
+        try {
+            FileWriter writer = new FileWriter(file, false);
+            Properties properties = new Properties();
+            properties.setProperty("easingMethod", easingMethod.toString());
+            properties.setProperty("scrollDuration", scrollDuration + "");
+            properties.setProperty("scrollStep", scrollDuration + "");
+            properties.setProperty("bounceBackMultiplier", scrollDuration + "");
+            properties.store(writer, null);
+            writer.close();
+        } catch (Exception e) {
+            e.printStackTrace();
+            easingMethod = EasingMethodImpl.QUART;
+            scrollDuration = 1000;
+            scrollStep = 16;
+            bounceBackMultiplier = .93;
+        }
+    }
     
     @Override
     public void onInitializeClient() {
+        loadConfig();
         if (FabricLoader.getInstance().isModLoaded("modmenu")) {
             try {
                 Class<?> clazz = Class.forName("io.github.prospector.modmenu.api.ModMenuApi");
                 Method method = clazz.getMethod("addConfigOverride", String.class, Runnable.class);
                 method.invoke(null, "cloth-config2", (Runnable) () -> {
                     try {
-                        ConfigBuilder builder = ConfigBuilder.create().setParentScreen(MinecraftClient.getInstance().currentScreen).setTitle("Cloth Mod Config Demo");
+                        ConfigBuilder builder = ConfigBuilder.create().setParentScreen(MinecraftClient.getInstance().currentScreen).setTitle("Cloth Mod Config Config");
                         builder.setDefaultBackgroundTexture(new Identifier("minecraft:textures/block/oak_planks.png"));
-                        ConfigCategory playZone = builder.getOrCreateCategory("Play Zone");
+                        ConfigCategory scrolling = builder.getOrCreateCategory("Scrolling");
                         ConfigEntryBuilder entryBuilder = ConfigEntryBuilder.create();
-                        playZone.addEntry(entryBuilder.startBooleanToggle("Simple Boolean", false).build());
-                        playZone.addEntry(entryBuilder.startStrField("Simple String", "ab").setDefaultValue(() -> "ab").build());
-                        playZone.addEntry(entryBuilder.startLongSlider("Long Slider", 0, -10, 10).setDefaultValue(() -> 0l).build());
-                        playZone.addEntry(entryBuilder.startDoubleList("Double List", Arrays.asList(1d, 6d, 14d, 1414d)).setTooltip("this is a bad tooltip").setSaveConsumer(integers -> integers.forEach(System.out::println)).setCellErrorSupplier(aDouble -> aDouble == 2 ? Optional.of("2 is not accepted!") : Optional.empty()).setDefaultValue(Arrays.asList(1d, 6d, 14d, 1414d)).build());
-                        playZone.addEntry(entryBuilder.startStrList("Party Member List", Arrays.asList("Tim", "Daniel", "John")).setTooltip("A list of party members.").setDefaultValue(Arrays.asList("Tim", "Daniel", "John")).build());
-                        playZone.addEntry(entryBuilder.startIntField("Integer Field", 2).setErrorSupplier(integer -> {
-                            if (integer == 4)
-                                return Optional.of("I hate the number 4 please stop");
-                            return Optional.empty();
-                        }).setDefaultValue(() -> 2).setMin(2).setMax(99).build());
-                        SubCategoryBuilder randomCategory = entryBuilder.startSubCategory("Random Sub-Category");
-                        randomCategory.add(entryBuilder.startTextDescription("§7This is a promotional message brought to you by Danielshe. Shop your favorite Lil Tater at store.liltater.com!").setTooltipSupplier(() -> Optional.of(new String[]{"This is an example tooltip."})).build());
-                        randomCategory.add(entryBuilder.startSubCategory("Sub-Sub-Category", ImmutableList.of(entryBuilder.startEnumSelector("Enum Field No. 1", DemoEnum.class, DemoEnum.CONSTANT_2).setDefaultValue(() -> DemoEnum.CONSTANT_1).build(), entryBuilder.startEnumSelector("Enum Field No. 2", DemoEnum.class, DemoEnum.CONSTANT_2).setDefaultValue(() -> DemoEnum.CONSTANT_1).build())).build());
-                        for(int i = 0; i < 10; i++)
-                            randomCategory.add(entryBuilder.startIntSlider("Integer Slider No. " + (i + 1), 0, -99, 99).build());
-                        playZone.addEntry(randomCategory.build());
-                        ConfigCategory enumZone = builder.getOrCreateCategory("Enum Zone");
-                        enumZone.setCategoryBackground(new Identifier("minecraft:textures/block/stone.png"));
-                        enumZone.addEntry(entryBuilder.startEnumSelector("Enum Field", DemoEnum.class, DemoEnum.CONSTANT_2).setDefaultValue(() -> DemoEnum.CONSTANT_1).build());
-                        builder.setFallbackCategory(enumZone);
+                        scrolling.addEntry(entryBuilder.startEnumSelector("Easing Method", EasingMethodImpl.class, easingMethod instanceof EasingMethodImpl ? (EasingMethodImpl) easingMethod : EasingMethodImpl.QUART).setDefaultValue(EasingMethodImpl.QUART).setSaveConsumer(o -> easingMethod = (EasingMethod) o).build());
+                        scrolling.addEntry(entryBuilder.startLongSlider("Scroll Duration", scrollDuration, 0, 5000).setTextGetter(integer -> {
+                            return integer <= 0 ? "Value: Disabled" : (integer > 1500 ? String.format("Value: %.1fs", integer / 1000f) : "Value: " + integer + "ms");
+                        }).setDefaultValue(1000).setSaveConsumer(i -> scrollDuration = i).build());
+                        scrolling.addEntry(entryBuilder.startDoubleField("Scroll Step", scrollStep).setDefaultValue(16).setSaveConsumer(i -> scrollStep = i).build());
+                        scrolling.addEntry(entryBuilder.startDoubleField("Bounce Multiplier", bounceBackMultiplier).setDefaultValue(0.93).setSaveConsumer(i -> bounceBackMultiplier = i).build());
+                        builder.setSavingRunnable(() -> {
+                            saveConfig();
+                        });
                         MinecraftClient.getInstance().openScreen(builder.build());
                     } catch (Throwable throwable) {
                         throwable.printStackTrace();
@@ -63,21 +134,4 @@ public class ClothConfigInitializer implements ClientModInitializer {
         }
     }
     
-    private static enum DemoEnum {
-        CONSTANT_1("Constant 1"),
-        CONSTANT_2("Constant 2"),
-        CONSTANT_3("Constant 3");
-        
-        private final String key;
-        
-        private DemoEnum(String key) {
-            this.key = key;
-        }
-        
-        @Override
-        public String toString() {
-            return this.key;
-        }
-    }
-    
 }

+ 0 - 8
src/main/java/me/shedaniel/clothconfig2/gui/ClothConfigScreen.java

@@ -474,14 +474,6 @@ public abstract class ClothConfigScreen extends Screen {
         protected final void clearStuff() {
             this.clearItems();
         }
-        
-        @Override
-        public boolean mouseClicked(double double_1, double double_2, int int_1) {
-            boolean b = super.mouseClicked(double_1, double_2, int_1);
-            if (!scroller.isRegistered())
-                scroller.registerTick();
-            return b;
-        }
     }
     
 }

+ 1 - 1
src/main/java/me/shedaniel/clothconfig2/gui/widget/DynamicElementListWidget.java

@@ -8,7 +8,7 @@ import net.minecraft.client.gui.ParentElement;
 import net.minecraft.util.Identifier;
 
 @Environment(EnvType.CLIENT)
-public abstract class DynamicElementListWidget<E extends DynamicElementListWidget.ElementEntry<E>> extends DynamicSmoothScrollingEntryListWidget<E> {
+public abstract class DynamicElementListWidget<E extends DynamicElementListWidget.ElementEntry<E>> extends DynamicNewSmoothScrollingEntryListWidget<E> {
     
     public DynamicElementListWidget(MinecraftClient client, int width, int height, int top, int bottom, Identifier backgroundLocation) {
         super(client, width, height, top, bottom, backgroundLocation);

+ 9 - 5
src/main/java/me/shedaniel/clothconfig2/gui/widget/DynamicEntryListWidget.java

@@ -20,7 +20,6 @@ import java.util.AbstractList;
 import java.util.ArrayList;
 import java.util.List;
 import java.util.Objects;
-import java.util.concurrent.atomic.AtomicInteger;
 
 @SuppressWarnings("deprecation")
 @Environment(EnvType.CLIENT)
@@ -43,7 +42,6 @@ public abstract class DynamicEntryListWidget<E extends DynamicEntryListWidget.En
     protected boolean scrolling;
     protected E selectedItem;
     protected Identifier backgroundLocation;
-    
     public DynamicEntryListWidget(MinecraftClient client, int width, int height, int top, int bottom, Identifier backgroundLocation) {
         this.client = client;
         this.width = width;
@@ -140,9 +138,10 @@ public abstract class DynamicEntryListWidget<E extends DynamicEntryListWidget.En
     }
     
     protected int getMaxScrollPosition() {
-        AtomicInteger integer = new AtomicInteger(headerHeight);
-        entries.forEach(item -> integer.addAndGet(item.getItemHeight()));
-        return integer.get();
+        int i = headerHeight;
+        for(E entry : entries)
+            i += entry.getItemHeight();
+        return i;
     }
     
     protected void clickedHeader(int int_1, int int_2) {
@@ -457,6 +456,11 @@ public abstract class DynamicEntryListWidget<E extends DynamicEntryListWidget.En
         return boolean_1;
     }
     
+    public static final class SmoothScrollingSettings {
+        private SmoothScrollingSettings() {}
+        public static final double CLAMP_EXTENSION = 200;
+    }
+    
     @SuppressWarnings("deprecation")
     @Environment(EnvType.CLIENT)
     public abstract static class Entry<E extends Entry<E>> extends DrawableHelper implements Element {

+ 183 - 0
src/main/java/me/shedaniel/clothconfig2/gui/widget/DynamicNewSmoothScrollingEntryListWidget.java

@@ -0,0 +1,183 @@
+package me.shedaniel.clothconfig2.gui.widget;
+
+import me.shedaniel.clothconfig2.ClothConfigInitializer;
+import me.shedaniel.math.api.Rectangle;
+import me.shedaniel.math.impl.PointHelper;
+import net.minecraft.client.MinecraftClient;
+import net.minecraft.client.render.BufferBuilder;
+import net.minecraft.client.render.Tessellator;
+import net.minecraft.client.render.VertexFormats;
+import net.minecraft.util.Identifier;
+import net.minecraft.util.math.MathHelper;
+
+import static me.shedaniel.clothconfig2.ClothConfigInitializer.getBounceBackMultiplier;
+
+public abstract class DynamicNewSmoothScrollingEntryListWidget<E extends DynamicEntryListWidget.Entry<E>> extends DynamicEntryListWidget<E> {
+    
+    protected double target;
+    protected boolean smoothScrolling = true;
+    protected long start;
+    protected long duration;
+    
+    public DynamicNewSmoothScrollingEntryListWidget(MinecraftClient client, int width, int height, int top, int bottom, Identifier backgroundLocation) {
+        super(client, width, height, top, bottom, backgroundLocation);
+    }
+    
+    public final double clamp(double v) {
+        return clamp(v, SmoothScrollingSettings.CLAMP_EXTENSION);
+    }
+    
+    public final double clamp(double v, double clampExtension) {
+        return MathHelper.clamp(v, -clampExtension, getMaxScroll() + clampExtension);
+    }
+    
+    public boolean isSmoothScrolling() {
+        return smoothScrolling;
+    }
+    
+    public void setSmoothScrolling(boolean smoothScrolling) {
+        this.smoothScrolling = smoothScrolling;
+    }
+    
+    @Override
+    public void capYPosition(double double_1) {
+        if (!smoothScrolling)
+            this.scroll = MathHelper.clamp(double_1, 0.0D, (double) this.getMaxScroll());
+        else {
+            scroll = clamp(double_1);
+            target = clamp(double_1);
+        }
+    }
+    
+    @Override
+    public boolean mouseDragged(double double_1, double double_2, int int_1, double double_3, double double_4) {
+        if (!smoothScrolling)
+            return super.mouseDragged(double_1, double_2, int_1, double_3, double_4);
+        if (this.getFocused() != null && this.isDragging() && int_1 == 0 ? this.getFocused().mouseDragged(double_1, double_2, int_1, double_3, double_4) : false) {
+            return true;
+        } else if (int_1 == 0 && this.scrolling) {
+            if (double_2 < (double) this.top) {
+                this.capYPosition(0.0D);
+            } else if (double_2 > (double) this.bottom) {
+                this.capYPosition((double) this.getMaxScroll());
+            } else {
+                double double_5 = (double) Math.max(1, this.getMaxScroll());
+                int int_2 = this.bottom - this.top;
+                int int_3 = MathHelper.clamp((int) ((float) (int_2 * int_2) / (float) this.getMaxScrollPosition()), 32, int_2 - 8);
+                double double_6 = Math.max(1.0D, double_5 / (double) (int_2 - int_3));
+                this.capYPosition(MathHelper.clamp(this.getScroll() + double_4 * double_6, 0, getMaxScroll()));
+            }
+            return true;
+        }
+        return false;
+    }
+    
+    @Override
+    public boolean mouseScrolled(double double_1, double double_2, double double_3) {
+        if (!smoothScrolling) {
+            scroll += 16 * -double_3;
+            this.scroll = MathHelper.clamp(double_1, 0.0D, (double) this.getMaxScroll());
+            return true;
+        }
+        offset(ClothConfigInitializer.getScrollStep() * -double_3, true);
+        return true;
+    }
+    
+    public void offset(double value, boolean animated) {
+        scrollTo(target + value, animated);
+    }
+    
+    public void scrollTo(double value, boolean animated) {
+        scrollTo(value, animated, ClothConfigInitializer.getScrollDuration());
+    }
+    
+    public void scrollTo(double value, boolean animated, long duration) {
+        target = clamp(value);
+        
+        if (animated) {
+            start = System.currentTimeMillis();
+            this.duration = duration;
+        } else
+            scroll = target;
+    }
+    
+    @Override
+    public void render(int mouseX, int mouseY, float delta) {
+        updatePosition(delta);
+        super.render(mouseX, mouseY, delta);
+    }
+    
+    private void updatePosition(float delta) {
+        target = clamp(target);
+        if (target < 0) {
+            target = target * getBounceBackMultiplier();
+        } else if (target > getMaxScroll()) {
+            target = (target - getMaxScroll()) * getBounceBackMultiplier() + getMaxScroll();
+        }
+        if (!Precision.almostEquals(scroll, target, Precision.FLOAT_EPSILON))
+            scroll = (float) Interpolation.expoEase(scroll, target, Math.min((System.currentTimeMillis() - start) / ((double) duration), 1));
+        else
+            scroll = target;
+    }
+    
+    @SuppressWarnings("deprecation")
+    @Override
+    protected void renderScrollBar(Tessellator tessellator, BufferBuilder buffer, int maxScroll, int scrollbarPositionMinX, int scrollbarPositionMaxX) {
+        if (!smoothScrolling)
+            super.renderScrollBar(tessellator, buffer, maxScroll, scrollbarPositionMinX, scrollbarPositionMaxX);
+        else if (maxScroll > 0) {
+            int height = (int) (((this.bottom - this.top) * (this.bottom - this.top)) / this.getMaxScrollPosition());
+            height = MathHelper.clamp(height, 32, this.bottom - this.top - 8);
+            height -= Math.min((scroll < 0 ? (int) -scroll : scroll > getMaxScroll() ? (int) scroll - getMaxScroll() : 0), height * .95);
+            height = Math.max(10, height);
+            int minY = Math.min(Math.max((int) this.getScroll() * (this.bottom - this.top - height) / maxScroll + this.top, this.top), this.bottom - height);
+            
+            int bottomc = new Rectangle(scrollbarPositionMinX, minY, scrollbarPositionMaxX - scrollbarPositionMinX, height).contains(PointHelper.fromMouse()) ? 168 : 128;
+            int topc = new Rectangle(scrollbarPositionMinX, minY, scrollbarPositionMaxX - scrollbarPositionMinX, height).contains(PointHelper.fromMouse()) ? 222 : 172;
+            
+            // Black Bar
+            buffer.begin(7, VertexFormats.POSITION_UV_COLOR);
+            buffer.vertex(scrollbarPositionMinX, this.bottom, 0.0D).texture(0, 1).color(0, 0, 0, 255).next();
+            buffer.vertex(scrollbarPositionMaxX, this.bottom, 0.0D).texture(1, 1).color(0, 0, 0, 255).next();
+            buffer.vertex(scrollbarPositionMaxX, this.top, 0.0D).texture(1, 0).color(0, 0, 0, 255).next();
+            buffer.vertex(scrollbarPositionMinX, this.top, 0.0D).texture(0, 0).color(0, 0, 0, 255).next();
+            tessellator.draw();
+            
+            // Bottom
+            buffer.begin(7, VertexFormats.POSITION_UV_COLOR);
+            buffer.vertex(scrollbarPositionMinX, minY + height, 0.0D).texture(0, 1).color(bottomc, bottomc, bottomc, 255).next();
+            buffer.vertex(scrollbarPositionMaxX, minY + height, 0.0D).texture(1, 1).color(bottomc, bottomc, bottomc, 255).next();
+            buffer.vertex(scrollbarPositionMaxX, minY, 0.0D).texture(1, 0).color(bottomc, bottomc, bottomc, 255).next();
+            buffer.vertex(scrollbarPositionMinX, minY, 0.0D).texture(0, 0).color(bottomc, bottomc, bottomc, 255).next();
+            tessellator.draw();
+            
+            // Top
+            buffer.begin(7, VertexFormats.POSITION_UV_COLOR);
+            buffer.vertex(scrollbarPositionMinX, (minY + height - 1), 0.0D).texture(0, 1).color(topc, topc, topc, 255).next();
+            buffer.vertex((scrollbarPositionMaxX - 1), (minY + height - 1), 0.0D).texture(1, 1).color(topc, topc, topc, 255).next();
+            buffer.vertex((scrollbarPositionMaxX - 1), minY, 0.0D).texture(1, 0).color(topc, topc, topc, 255).next();
+            buffer.vertex(scrollbarPositionMinX, minY, 0.0D).texture(0, 0).color(topc, topc, topc, 255).next();
+            tessellator.draw();
+        }
+    }
+    
+    public static class Interpolation {
+        public static double expoEase(double start, double end, double amount) {
+            return start + (end - start) * ClothConfigInitializer.getEasingMethod().apply(amount);
+        }
+    }
+    
+    public static class Precision {
+        public static final float FLOAT_EPSILON = 1e-3f;
+        public static final double DOUBLE_EPSILON = 1e-7;
+        
+        public static boolean almostEquals(float value1, float value2, float acceptableDifference) {
+            return Math.abs(value1 - value2) <= acceptableDifference;
+        }
+        
+        public static boolean almostEquals(double value1, double value2, double acceptableDifference) {
+            return Math.abs(value1 - value2) <= acceptableDifference;
+        }
+    }
+    
+}

+ 3 - 6
src/main/java/me/shedaniel/clothconfig2/gui/widget/DynamicSmoothScrollingEntryListWidget.java

@@ -103,17 +103,14 @@ public abstract class DynamicSmoothScrollingEntryListWidget<E extends DynamicEnt
     public boolean mouseScrolled(double double_1, double double_2, double double_3) {
         if (!smoothScrolling) {
             this.scrollVelocity = 0d;
-            if (double_3 < 0)
-                scroll += 16;
-            if (double_3 > 0)
-                scroll -= 16;
+            scroll += 16 * -double_3;
             this.scroll = MathHelper.clamp(double_1, 0.0D, (double) this.getMaxScroll());
             return true;
         }
         if (scroll <= getMaxScroll() && double_3 < 0)
-            scrollVelocity += 16;
+            scrollVelocity += 16 * -double_3;
         if (scroll >= 0 && double_3 > 0)
-            scrollVelocity -= 16;
+            scrollVelocity += 16 * -double_3;
         if (!scroller.isRegistered())
             scroller.registerTick();
         return true;

+ 36 - 0
src/main/java/me/shedaniel/clothconfig2/impl/EasingMethod.java

@@ -0,0 +1,36 @@
+package me.shedaniel.clothconfig2.impl;
+
+import java.util.function.Function;
+
+public interface EasingMethod {
+    
+    double apply(double v);
+    
+    public enum EasingMethodImpl implements EasingMethod {
+        NONE(v -> 1.0),
+        LINEAR(v -> v),
+        EXPO(v -> ((v == 1.0) ? 1 : 1 * (-Math.pow(2, -10 * v) + 1))),
+        QUAD(v -> -1 * (v /= 1) * (v - 2)),
+        QUART(v -> ((v == 1.0) ? 1 : 1 * (-1 * ((v = v - 1) * v * v * v - 1)))),
+        SINE(v -> Math.sin(v * (Math.PI / 2))),
+        CUBIC(v -> ((v = v - 1) * v * v + 1)),
+        QUINTIC(v -> ((v = v - 1) * v * v * v * v + 1)),
+        CIRC(v -> Math.sqrt(1 - (v = v - 1) * v));
+        
+        private Function<Double, Double> function;
+        
+        EasingMethodImpl(Function<Double, Double> function) {
+            this.function = function;
+        }
+        
+        @Override
+        public double apply(double v) {
+            return function.apply(v);
+        }
+        
+        @Override
+        public String toString() {
+            return name();
+        }
+    }
+}

+ 23 - 0
src/main/java/me/shedaniel/clothconfig2/impl/EasingMethods.java

@@ -0,0 +1,23 @@
+package me.shedaniel.clothconfig2.impl;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+
+public class EasingMethods {
+    private static final List<EasingMethod> METHODS;
+    
+    static {
+        METHODS = new ArrayList<>();
+        METHODS.addAll(Arrays.asList(EasingMethod.EasingMethodImpl.values()));
+    }
+    
+    public static void register(EasingMethod easingMethod) {
+        METHODS.add(easingMethod);
+    }
+    
+    public static List<EasingMethod> getMethods() {
+        return Collections.unmodifiableList(new ArrayList<>(METHODS));
+    }
+}