diff --git a/src/main/java/com/laytonsmith/core/Easings.java b/src/main/java/com/laytonsmith/core/Easings.java new file mode 100644 index 000000000..4eba89e14 --- /dev/null +++ b/src/main/java/com/laytonsmith/core/Easings.java @@ -0,0 +1,172 @@ +package com.laytonsmith.core; + +import com.laytonsmith.annotations.MEnum; + +/** + * A utility class that contains various easing algorithms. + */ +public final class Easings { + + private Easings() { + } + + @MEnum("com.methodscript.EasingType") + public static enum EasingType { + EASE_IN_SINE, + EASE_OUT_SINE, + EASE_IN_OUT_SINE, + EASE_IN_CUBIC, + EASE_OUT_CUBIC, + EASE_IN_OUT_CUBIC, + EASE_IN_QUINT, + EASE_OUT_QUINT, + EASE_IN_OUT_QUINT, + EASE_IN_CIRC, + EASE_OUT_CIRC, + EASE_IN_OUT_CIRC, + EASE_IN_ELASTIC, + EASE_OUT_ELASTIC, + EASE_IN_OUT_ELASTIC, + EASE_IN_QUAD, + EASE_OUT_QUAD, + EASE_IN_OUT_QUAD, + EASE_IN_QUART, + EASE_OUT_QUART, + EASE_IN_OUT_QUART, + EASE_IN_EXPO, + EASE_OUT_EXPO, + EASE_IN_OUT_EXPO, + EASE_IN_BACK, + EASE_OUT_BACK, + EASE_IN_OUT_BACK, + EASE_IN_BOUNCE, + EASE_OUT_BOUNCE, + EASE_IN_OUT_BOUNCE, + LINEAR, + } + + private static final double C1 = 1.70158; + private static final double C2 = C1 * 1.525; + private static final double C3 = C1 + 1; + private static final double C4 = (2 * Math.PI) / 3; + private static final double C5 = (2 * Math.PI) / 4.5; + private static final double D1 = 2.75; + private static final double N1 = 7.5625; + + /** + * Returns the resultant percentage of travel given the overall time percentage, based on the given easing type. For + * instance, for {@code LINEAR} easings, the total distance is the same as the input, but for others, the value will + * differ based on the specific easing selected. + * + * @param type The easing type to use. Based on the easings listed at https://easings.net/ + * @param x The overall time percentage, a value between 0 and 1. If the value is outside the range, it is clamped + * to 0 or 1. + * @return The resultant percentage of travel. + */ + public static double GetEasing(EasingType type, double x) { + if(x < 0) { + x = 0; + } else if(x > 1) { + x = 1; + } + + switch(type) { + case EASE_IN_SINE: + return 1 - Math.cos((x * Math.PI) / 2); + case EASE_OUT_SINE: + return Math.sin((x * Math.PI) / 2); + case EASE_IN_OUT_SINE: + return -(Math.cos(Math.PI * x) - 1) / 2; + case EASE_IN_CUBIC: + return x * x * x; + case EASE_OUT_CUBIC: + return 1 - Math.pow(1 - x, 3); + case EASE_IN_OUT_CUBIC: + return x < 0.5 ? 4 * x * x * x : 1 - Math.pow(-2 * x + 2, 3) / 2; + case EASE_IN_QUINT: + return x * x * x * x * x; + case EASE_OUT_QUINT: + return 1 - Math.pow(1 - x, 5); + case EASE_IN_OUT_QUINT: + return x < 0.5 ? 16 * x * x * x * x * x : 1 - Math.pow(-2 * x + 2, 5) / 2; + case EASE_IN_CIRC: + return 1 - Math.sqrt(1 - Math.pow(x, 2)); + case EASE_OUT_CIRC: + return Math.sqrt(1 - Math.pow(x - 1, 2)); + case EASE_IN_OUT_CIRC: + return x < 0.5 + ? (1 - Math.sqrt(1 - Math.pow(2 * x, 2))) / 2 + : (Math.sqrt(1 - Math.pow(-2 * x + 2, 2)) + 1) / 2; + case EASE_IN_ELASTIC: + return (x - 0.000001) < 0.0 + ? 0 + : (x + 0.000001) > 1.0 + ? 1 + : -Math.pow(2, 10 * x - 10) * Math.sin((x * 10 - 10.75) * C4); + case EASE_OUT_ELASTIC: + return (x - 0.000001) < 0.0 + ? 0 + : (x + 0.000001) > 1.0 + ? 1 + : Math.pow(2, -10 * x) * Math.sin((x * 10 - 0.75) * C4) + 1; + case EASE_IN_OUT_ELASTIC: + return (x - 0.000001) < 0.0 + ? 0 + : (x + 0.000001) > 1.0 + ? 1 + : x < 0.5 + ? -(Math.pow(2, 20 * x - 10) * Math.sin((20 * x - 11.125) * C5)) / 2 + : (Math.pow(2, -20 * x + 10) * Math.sin((20 * x - 11.125) * C5)) / 2 + 1; + case EASE_IN_QUAD: + return x * x; + case EASE_OUT_QUAD: + return 1 - (1 - x) * (1 - x); + case EASE_IN_OUT_QUAD: + return x < 0.5 ? 2 * x * x : 1 - Math.pow(-2 * x + 2, 2) / 2; + case EASE_IN_QUART: + return x * x * x * x; + case EASE_OUT_QUART: + return 1 - Math.pow(1 - x, 4); + case EASE_IN_OUT_QUART: + return x < 0.5 ? 8 * x * x * x * x : 1 - Math.pow(-2 * x + 2, 4) / 2; + case EASE_IN_EXPO: + return (x - 0.000001) < 0.0 ? 0 : Math.pow(2, 10 * x - 10); + case EASE_OUT_EXPO: + return (x + 0.000001) > 1.0 ? 1 : 1 - Math.pow(2, -10 * x); + case EASE_IN_OUT_EXPO: + return (x - 0.000001) < 0.0 + ? 0 + : (x + 0.000001) > 1.0 + ? 1 + : x < 0.5 ? Math.pow(2, 20 * x - 10) / 2 + : (2 - Math.pow(2, -20 * x + 10)) / 2; + case EASE_IN_BACK: + return C3 * x * x * x - C1 * x * x; + case EASE_OUT_BACK: + return 1 + C3 * Math.pow(x - 1, 3) + C1 * Math.pow(x - 1, 2); + case EASE_IN_OUT_BACK: + return x < 0.5 + ? (Math.pow(2 * x, 2) * ((C2 + 1) * 2 * x - C2)) / 2 + : (Math.pow(2 * x - 2, 2) * ((C2 + 1) * (x * 2 - 2) + C2) + 2) / 2; + case EASE_IN_BOUNCE: + return 1 - GetEasing(EasingType.EASE_OUT_BOUNCE, 1 - x); + case EASE_OUT_BOUNCE: + if(x < 1 / D1) { + return N1 * x * x; + } else if(x < 2 / D1) { + return N1 * (x -= 1.5 / D1) * x + 0.75; + } else if(x < 2.5 / D1) { + return N1 * (x -= 2.25 / D1) * x + 0.9375; + } else { + return N1 * (x -= 2.625 / D1) * x + 0.984375; + } + case EASE_IN_OUT_BOUNCE: + return x < 0.5 + ? (1 - GetEasing(EasingType.EASE_OUT_BOUNCE, 1 - 2 * x)) / 2 + : (1 + GetEasing(EasingType.EASE_OUT_BOUNCE, 2 * x - 1)) / 2; + case LINEAR: + return x; + } + throw new RuntimeException("Missing easing implementation."); + } +} diff --git a/src/main/java/com/laytonsmith/core/constructs/CArray.java b/src/main/java/com/laytonsmith/core/constructs/CArray.java index 3cf46553b..755859157 100644 --- a/src/main/java/com/laytonsmith/core/constructs/CArray.java +++ b/src/main/java/com/laytonsmith/core/constructs/CArray.java @@ -396,11 +396,11 @@ public final void set(String index, String value) { set(index, value, Target.UNKNOWN); } - public final void set(String index, float value) { + public final void set(String index, double value) { set(index, new CDouble(value, Target.UNKNOWN), Target.UNKNOWN); } - public final void set(String index, int value) { + public final void set(String index, long value) { set(index, new CInt(value, Target.UNKNOWN), Target.UNKNOWN); } diff --git a/src/main/java/com/laytonsmith/core/constructs/CClassType.java b/src/main/java/com/laytonsmith/core/constructs/CClassType.java index 1d560f2fa..c069eb62c 100644 --- a/src/main/java/com/laytonsmith/core/constructs/CClassType.java +++ b/src/main/java/com/laytonsmith/core/constructs/CClassType.java @@ -194,6 +194,20 @@ public static CClassType get(CClassType... types) throws ClassNotFoundException .toArray(new FullyQualifiedClassName[types.length])); } + /** + * Returns the CClassType for a native enum. This will fail with a runtime error if this + * class is not tagged with MEnum. + * @param menum + * @return + */ + public static CClassType getForEnum(Class> menum) { + try { + return get(FullyQualifiedClassName.forNativeEnum(menum)); + } catch(ClassNotFoundException ex) { + throw new RuntimeException(ex); + } + } + /** * This function defines a brand new class type. This should exclusively be used in a class * definition scenario, and never when simply looking up an existing class. The created diff --git a/src/main/java/com/laytonsmith/core/functions/Easings.java b/src/main/java/com/laytonsmith/core/functions/Easings.java new file mode 100644 index 000000000..3a0a93ff1 --- /dev/null +++ b/src/main/java/com/laytonsmith/core/functions/Easings.java @@ -0,0 +1,319 @@ +package com.laytonsmith.core.functions; + +import com.laytonsmith.PureUtilities.Common.StringUtils; +import com.laytonsmith.PureUtilities.Version; +import com.laytonsmith.annotations.api; +import com.laytonsmith.annotations.seealso; +import com.laytonsmith.core.ArgumentValidation; +import com.laytonsmith.core.MSVersion; +import com.laytonsmith.core.Optimizable; +import com.laytonsmith.core.compiler.signature.FunctionSignatures; +import com.laytonsmith.core.compiler.signature.SignatureBuilder; +import com.laytonsmith.core.constructs.CArray; +import com.laytonsmith.core.constructs.CClassType; +import com.laytonsmith.core.constructs.CDouble; +import com.laytonsmith.core.constructs.Target; +import com.laytonsmith.core.environments.Environment; +import com.laytonsmith.core.exceptions.CRE.CRECastException; +import com.laytonsmith.core.exceptions.CRE.CREThrowable; +import com.laytonsmith.core.exceptions.ConfigCompileException; +import com.laytonsmith.core.exceptions.ConfigRuntimeException; +import com.laytonsmith.core.natives.interfaces.Mixed; +import java.util.EnumSet; +import java.util.Set; + +/** + * + */ +public class Easings { + public static String docs() { + return "Easing related functions. Easings are based on the easings listed at http://easings.net, with" + + " the addition of the LINEAR easing, which just returns x."; + } + + @api + public static class easing extends AbstractFunction implements Optimizable { + + @Override + public Class[] thrown() { + return new Class[]{ + CRECastException.class, + }; + } + + @Override + public boolean isRestricted() { + return false; + } + + @Override + public Boolean runAsync() { + return null; + } + + @Override + public Mixed exec(Target t, Environment env, Mixed... args) throws ConfigRuntimeException { + com.laytonsmith.core.Easings.EasingType type + = ArgumentValidation.getEnum(args[0], com.laytonsmith.core.Easings.EasingType.class, t); + double x = ArgumentValidation.getDouble(args[1], t); + double ret = com.laytonsmith.core.Easings.GetEasing(type, x); + return new CDouble(ret, Target.UNKNOWN); + } + + @Override + public String getName() { + return "easing"; + } + + @Override + public Integer[] numArgs() { + return new Integer[]{2}; + } + + @Override + public String docs() { + return "double {EasingType type, double x} Given an easing type, and a duration percentage x, returns the" + + " given resulting interpolation value. ----" + + " Easing type may be one of " + StringUtils.Join(com.laytonsmith.core.Easings.EasingType.values(), ", ", ", or ") + + " and x must be a double between 0 and 1, or it is clamped. The return value may be less than" + + " zero or above one, depending on the easing algorithm. See http://easings.net/ for visual" + + " examples."; + } + + @Override + public Version since() { + return MSVersion.V3_3_5; + } + + @Override + public Set optimizationOptions() { + return EnumSet.of(OptimizationOption.CACHE_RETURN, + OptimizationOption.NO_SIDE_EFFECTS, + OptimizationOption.CONSTANT_OFFLINE); + } + + @Override + public FunctionSignatures getSignatures() { + return new SignatureBuilder(CDouble.TYPE) + .param(CClassType.getForEnum(com.laytonsmith.core.Easings.EasingType.class), + "type", "The easing type.") + .param(CDouble.TYPE, "x", "The duration percentage.") + .build(); + } + + @Override + public ExampleScript[] examples() throws ConfigCompileException { + return new ExampleScript[]{ + new ExampleScript("Usage with LINEAR interpolation.", "for(@x = 0, @x <= 1, @x += 0.1) {\n" + + "\tmsg(@x . ': ' . easing('LINEAR', @x));\n" + + "}"), + new ExampleScript("Usage with EASE_IN_SINE interpolation.", "for(@x = 0, @x <= 1, @x += 0.1) {\n" + + "\tmsg(@x . ': ' . easing('EASE_IN_SINE', @x));\n" + + "}"), + }; + } + } + + @api + @seealso({easing.class}) + public static class ease_between_loc extends AbstractFunction { + + @Override + public Class[] thrown() { + return new Class[]{ + CRECastException.class, + }; + } + + @Override + public boolean isRestricted() { + return false; + } + + @Override + public Boolean runAsync() { + return null; + } + + @Override + public Mixed exec(Target t, Environment env, Mixed... args) throws ConfigRuntimeException { + CArray start = ArgumentValidation.getArray(args[0], t); + double startX = ArgumentValidation.getDouble(start.get("x", t), t); + double startY = ArgumentValidation.getDouble(start.get("y", t), t); + double startZ = ArgumentValidation.getDouble(start.get("z", t), t); + CArray finish = ArgumentValidation.getArray(args[1], t); + double finishX = ArgumentValidation.getDouble(finish.get("x", t), t); + double finishY = ArgumentValidation.getDouble(finish.get("y", t), t); + double finishZ = ArgumentValidation.getDouble(finish.get("z", t), t); + com.laytonsmith.core.Easings.EasingType type + = ArgumentValidation.getEnum(args[2], com.laytonsmith.core.Easings.EasingType.class, t); + double x = ArgumentValidation.getDouble(args[3], t); + double percentage = com.laytonsmith.core.Easings.GetEasing(type, x); + CArray result = new CArray(Target.UNKNOWN); + result.set("x", startX + (finishX - startX) * percentage); + result.set("y", startY + (finishY - startY) * percentage); + result.set("z", startZ + (finishZ - startZ) * percentage); + return result; + } + + @Override + public String getName() { + return "ease_between_loc"; + } + + @Override + public Integer[] numArgs() { + return new Integer[]{4}; + } + + @Override + public String docs() { + return "array {array start, array finish, EasingType type, double x} Given an easing type, and a" + + " duration percentage x, returns the" + + " given resulting interpolated distance. ----" + + " For instance, given the location arrays representing x: 0 and x: 100, with a LINEAR" + + " easing and 0.25 duration, a location array of x: 25 would be returned." + + " Easing type may be one of " + StringUtils.Join(com.laytonsmith.core.Easings.EasingType.values(), ", ", ", or ") + + " and x must be a double between 0 and 1, or it is clamped. The return value may be less than" + + " zero or above one, depending on the easing algorithm. See http://easings.net/ for visual" + + " examples. The start and finish arrays must contain x, y, and z parameters, but don't necessarily" + + " have to represent locations."; + } + + @Override + public Version since() { + return MSVersion.V3_3_5; + } + + @Override + public FunctionSignatures getSignatures() { + return new SignatureBuilder(CArray.TYPE) + .param(CArray.TYPE, "start", "The starting position.") + .param(CArray.TYPE, "finish", "The ending position.") + .param(CClassType.getForEnum(com.laytonsmith.core.Easings.EasingType.class), "type", "The easing type.") + .param(CDouble.TYPE, "x", "The duration percentage.") + .build(); + } + + @Override + public ExampleScript[] examples() throws ConfigCompileException { + return new ExampleScript[] { + new ExampleScript("Usage with a LINEAR interpolation.", "@start = array(x: 0, y: 0, z: 0);\n" + + "@finish = array(x: 0, y: 100, z: 0);\n" + + "for(@i = 0, @i <= 1, @i += 0.1) {\n" + + "\tmsg(ease_between_loc(@start, @finish, 'LINEAR', @i);\n" + + "}"), + new ExampleScript("Usage with a EASE_IN_CUBIC interpolation.", "@start = array(x: 0, y: 0, z: 0);\n" + + "@finish = array(x: 0, y: 100, z: 0);\n" + + "for(@i = 0, @i <= 1, @i += 0.1) {\n" + + "\tmsg(ease_between_loc(@start, @finish, 'EASE_IN_CUBIC', @i);\n" + + "}") + }; + } + } + +// @api +// public static class ease_between_time extends AbstractFunction { +// +// @Override +// public Class[] thrown() { +// return new Class[]{ +// CRECastException.class, +// }; +// } +// +// @Override +// public boolean isRestricted() { +// return false; +// } +// +// @Override +// public Boolean runAsync() { +// return null; +// } +// +// @Override +// public Mixed exec(Target t, Environment env, Mixed... args) throws ConfigRuntimeException { +// CArray start = ArgumentValidation.getArray(args[0], t); +// double startX = ArgumentValidation.getDouble(start.get("x", t), t); +// double startY = ArgumentValidation.getDouble(start.get("y", t), t); +// double startZ = ArgumentValidation.getDouble(start.get("z", t), t); +// CArray finish = ArgumentValidation.getArray(args[1], t); +// double finishX = ArgumentValidation.getDouble(finish.get("x", t), t); +// double finishY = ArgumentValidation.getDouble(finish.get("y", t), t); +// double finishZ = ArgumentValidation.getDouble(finish.get("z", t), t); +// com.laytonsmith.core.Easings.EasingType type +// = ArgumentValidation.getEnum(args[2], com.laytonsmith.core.Easings.EasingType.class, t); +// long timeMs = ArgumentValidation.getInt(args[3], t); +// long framerateMs = ArgumentValidation.getInt(args[4], t); +// Callable callback = ArgumentValidation.getObject(args[5], t, Callable.class); +// if(framerateMs <= 0) { +// throw new CRERangeException("framerateMs must be greater than 0.", t); +// } +// if(framerateMs > timeMs) { +// throw new CRERangeException("framerateMs must be less than or equal to timeMS.", t); +// } +// long startTime = System.currentTimeMillis(); +// final AtomicInteger step = new AtomicInteger(0); +// final MutableObject r = new MutableObject<>(); +// +// r.setObject(() -> { +// int s = step.getAndIncrement(); +// long expectedTime = ((s + 1) * framerateMs) + startTime; +// +// StaticLayer.SetFutureRunnable(env.getEnv(StaticRuntimeEnv.class).GetDaemonManager(), 0, r.getObject()); +// }); +// StaticLayer.SetFutureRunnable(env.getEnv(StaticRuntimeEnv.class).GetDaemonManager(), 0, r.getObject()); +// return CVoid.VOID; +// } +// +// @Override +// public String getName() { +// return "ease_between_time"; +// } +// +// @Override +// public Integer[] numArgs() { +// return new Integer[]{6}; +// } +// +// @Override +// public String docs() { +// return "void {array start, array finish, EasingType type, int timeMs, int framerateMs, Callable callback}" +// + " Runs the callback at the appropriate time with the given easing, such that the animation" +// + " completes in the given number of ms. ----" +// + " Easing type may be one of " + StringUtils.Join(com.laytonsmith.core.Easings.EasingType.values(), ", ", ", or ") +// + ". See http://easings.net/ for visual" +// + " examples. The start and finish arrays must contain x, y, and z parameters, but don't necessarily" +// + " have to represent locations. timeMs represents the total time the process should run for," +// + " and framerateMs represents how often it should \"tick\". The callback will receive the" +// + " interpolated location array. If the callback returns -1, the animation will halt. Note that" +// + " the framerate is lower priority than the total time, that is, if the animation is running" +// + " behind, the framerate will be shortened, to ensure that the total time of the animation is" +// + " completed by the time timeMs has elapsed. framerateMs must be greater than 0, and less than" +// + " timeMs. If framerateMs is not a multiple of timeMs, the last frame is truncated."; +// } +// +// @Override +// public Version since() { +// return MSVersion.V3_3_5; +// } +// +// @Override +// public FunctionSignatures getSignatures() { +// return new SignatureBuilder(CVoid.TYPE) +// .param(CArray.TYPE, "start", "The starting position.") +// .param(CArray.TYPE, "finish", "The ending position.") +// .param(CClassType.getForEnum(com.laytonsmith.core.Easings.EasingType.class), "type", "The easing type.") +// .param(CInt.TYPE, "timeMs", "The total time of the animation.") +// .param(CInt.TYPE, "framerateMs", "The time between frames.") +// .param(Callable.TYPE, "callback", "The callback that receives the interpolated location.") +// .build(); +// } +// +// @Override +// public ExampleScript[] examples() throws ConfigCompileException { +// return super.examples(); //To change body of generated methods, choose Tools | Templates. +// } +// } +}