├── .gitignore ├── LICENSE ├── README.md ├── build.zig ├── build.zig.zon └── src ├── ease.zig ├── interp.zig └── root.zig /.gitignore: -------------------------------------------------------------------------------- 1 | .zig-cache 2 | zig-out 3 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) contributors 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Tween 2 | 3 | Rigid motion often appears unnatural. This library provides easing and interpolation functions widely used in game development to create natural looking or stylized tweens ("inbetweens"). 4 | 5 | Example usage: 6 | ```zig 7 | const tween = @import("tween"); 8 | const lerp = tween.interp.lerp; 9 | const ease = tween.ease; 10 | 11 | // ... 12 | 13 | pos = lerp(start, end, ease.bounceOut(t)); 14 | ``` 15 | 16 | ## Interpolation 17 | 18 | The following interpolation functions are provided: 19 | * `lerp` 20 | * `ilerp` 21 | * `remap` 22 | * `clamp01` 23 | * `damp` 24 | 25 | Linear interpolation differs from the implementation in the standard library in that it returns exact results at 0 and 1. 26 | 27 | Additionally, it accepts a larger variety of types: 28 | * Floats 29 | * Vectors 30 | * Arrays 31 | * Structures containing other supported types 32 | 33 | `ilerp` is the inverse of `lerp`, and only accepts floats. `remap` uses a combination of `ilerp` and `lerp` to remap a value from one range to another. 34 | 35 | `clamp01` is what it sounds like, and is provided for convenience. You can apply it to a `t` value getting passed to `lerp` to create a clamped lerp. 36 | 37 | `damp` is useful for [framerate independent lerping](https://www.rorydriscoll.com/2016/03/07/frame-rate-independent-damping-using-lerp/). 38 | 39 | If you're unfamiliar with this class of functions, I recommend viewing [The Simple Yet Powerful Math We Don't Talk About](https://www.youtube.com/watch?v=NzjF1pdlK7Y) by [Freya Holmer](https://www.acegikmo.com/). 40 | 41 | ## Ease 42 | 43 | The following easing functions are supported, most of these originated with [Robert Penner](http://robertpenner.com/easing/): 44 | 45 | * linear 46 | * [sine](https://easings.net/#easeInSine) 47 | * [quad](https://easings.net/#easeInQuad) 48 | * [cubic](https://easings.net/#easeInCubic) 49 | * [quart](https://easings.net/#easeInQuart) 50 | * [quint](https://easings.net/#easeInQuint) 51 | * [exp](https://easings.net/#easeInExpo) 52 | * [circ](https://easings.net/#easeInCirc) 53 | * [back](https://easings.net/#easeInBack) 54 | * [elastic](https://easings.net/#easeInElastic) 55 | * [bounce](https://easings.net/#easeInBounce) 56 | * step 57 | * [smoothstep](https://en.wikipedia.org/wiki/Smoothstep) 58 | * [smootherstep](https://en.wikipedia.org/wiki/Smoothstep#Variations) 59 | 60 | I've opted to not include GIFs demonstrating the easing styles here, as [easings.net](https://easings.net) already has a great visualizer for almost all of these. 61 | 62 | (Note that the links above are provided for convenient reference, the actual implementations may not be 100% identical or may have slightly different parameters.) 63 | 64 | In, out, and in-out variations of each are provided, functions are exact at 0 and 1 for 32 bit floats unless otherwise noted. 65 | 66 | Easing functions operate on the `t` value given to `lerp`. For example: 67 | ```zig 68 | const result = lerp(a, b, ease.smootherstep(t)); 69 | ``` 70 | 71 | If you're new to easing and not sure which to use, `smootherstep` is a reasonable default to slap on everything to start. 72 | 73 | You can adapt easing functions with `mix`, `combine`, `reflect`, and `reverse`. 74 | 75 | The provided easing functions are exact at 0 and 1 for 32 bit floats unless otherwise noted. 76 | 77 | When designing your own easing functions, I highly recommend testing them in [Desmos](https://www.desmos.com/calculator). 78 | 79 | ## Build Configuration 80 | 81 | If you're shipping binaries, you probably have a min spec CPU in mind. I recommend making sure `muladd` is enabled for your baseline so it doesn't end up getting emulated in software, [more info here](https://gamesbymason.com/devlog/2025/#muladd). On x86 this means enabling `fma`, on arm/aarch64 this means enabling either `neon` or `vfp4`. 82 | -------------------------------------------------------------------------------- /build.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | 3 | pub fn build(b: *std.Build) void { 4 | const target = b.standardTargetOptions(.{}); 5 | const optimize = b.standardOptimizeOption(.{}); 6 | const test_filters = b.option( 7 | []const []const u8, 8 | "test-filter", 9 | "Skip tests that do not match the specified filters.", 10 | ) orelse &.{}; 11 | 12 | const tween = b.addModule("tween", .{ 13 | .root_source_file = b.path("src/root.zig"), 14 | .target = target, 15 | .optimize = optimize, 16 | }); 17 | 18 | const lib_unit_tests = b.addTest(.{ 19 | .root_module = tween, 20 | .filters = test_filters, 21 | }); 22 | const run_lib_unit_tests = b.addRunArtifact(lib_unit_tests); 23 | const test_step = b.step("test", "Run unit tests"); 24 | test_step.dependOn(&run_lib_unit_tests.step); 25 | } 26 | -------------------------------------------------------------------------------- /build.zig.zon: -------------------------------------------------------------------------------- 1 | .{ 2 | .name = .tween, 3 | .fingerprint = 0xc004f341948175c6, 4 | .version = "0.0.0", 5 | .minimum_zig_version = "0.14.0", 6 | .dependencies = .{}, 7 | .paths = .{ 8 | "build.zig", 9 | "build.zig.zon", 10 | "src", 11 | "LICENSE", 12 | "README.md", 13 | }, 14 | } 15 | -------------------------------------------------------------------------------- /src/ease.zig: -------------------------------------------------------------------------------- 1 | //! Commonly used easing styles, to be applied to the `t` value of a linear interpolation. 2 | 3 | const std = @import("std"); 4 | const tween = @import("root.zig"); 5 | 6 | const lerp = tween.interp.lerp; 7 | const pi = std.math.pi; 8 | 9 | /// Linear easing does not change the time value. Provided for convenience when writing generic 10 | /// code. 11 | pub fn linear(t: anytype) @TypeOf(t) { 12 | return t; 13 | } 14 | 15 | test linear { 16 | try std.testing.expectEqual(0.0, linear(@as(f32, 0.0))); 17 | try std.testing.expectEqual(0.5, linear(@as(f32, 0.5))); 18 | try std.testing.expectEqual(1.0, linear(@as(f32, 1.0))); 19 | } 20 | 21 | /// Easing that follows a sine curve. 22 | /// 23 | /// Sinusoidal easing can't guarantee exact results at 0 and 1 since it's up to your sin 24 | /// implementation/hardware, but in practice should always be exact. 25 | pub fn sinIn(t: anytype) @TypeOf(t) { 26 | return 1.0 - @cos(t * pi / 2.0); 27 | } 28 | 29 | test sinIn { 30 | try std.testing.expectEqual(@as(f32, 0.0), sinIn(@as(f32, 0.0))); 31 | try std.testing.expectEqual(1.0 - @cos(pi / 4.0), sinIn(@as(f32, 0.5))); 32 | try std.testing.expectEqual(@as(f32, 1.0), sinIn(@as(f32, 1.0))); 33 | } 34 | 35 | /// See `sinIn`. 36 | pub fn sinOut(t: anytype) @TypeOf(t) { 37 | return @sin(t * pi / 2.0); 38 | } 39 | 40 | test sinOut { 41 | try std.testing.expectEqual(0.0, sinOut(@as(f32, 0.0))); 42 | try std.testing.expectEqual(@sin(pi / 4.0), sinOut(@as(f32, 0.5))); 43 | try std.testing.expectEqual(1.0, sinOut(@as(f32, 1.0))); 44 | } 45 | 46 | /// Eases in and out using a sine wave. 47 | pub fn sinInOut(t: anytype) @TypeOf(t) { 48 | return -(@cos(pi * t) - 1.0) / 2.0; 49 | } 50 | 51 | test sinInOut { 52 | try std.testing.expectEqual(0.0, sinInOut(@as(f32, 0.0))); 53 | try std.testing.expectApproxEqAbs(@as(f32, 0.146447), sinInOut(@as(f32, 0.25)), 0.001); 54 | try std.testing.expectEqual(0.5, sinInOut(@as(f32, 0.5))); 55 | try std.testing.expectApproxEqAbs(@as(f32, 0.853553), sinInOut(@as(f32, 0.75)), 0.001); 56 | try std.testing.expectEqual(1.0, sinInOut(@as(f32, 1.0))); 57 | } 58 | 59 | /// Quadratic easing. 60 | pub fn quadIn(t: anytype) @TypeOf(t) { 61 | return t * t; 62 | } 63 | 64 | test quadIn { 65 | try std.testing.expectEqual(0.0, quadIn(@as(f32, 0.0))); 66 | try std.testing.expectEqual(0.25, quadIn(@as(f32, 0.5))); 67 | try std.testing.expectEqual(1.0, quadIn(@as(f32, 1.0))); 68 | } 69 | 70 | /// See `quadIn`. 71 | pub fn quadOut(t: anytype) @TypeOf(t) { 72 | const T = @TypeOf(t); 73 | const inv = 1.0 - t; 74 | return @mulAdd(T, -inv, inv, 1.0); 75 | } 76 | 77 | test quadOut { 78 | try std.testing.expectEqual(0.0, quadOut(@as(f32, 0.0))); 79 | try std.testing.expectEqual(0.75, quadOut(@as(f32, 0.5))); 80 | try std.testing.expectEqual(1.0, quadOut(@as(f32, 1.0))); 81 | } 82 | 83 | /// Quadratic ease in followed by quadratic ease out. 84 | pub fn quadInOut(t: anytype) @TypeOf(t) { 85 | const T = @TypeOf(t); 86 | if (t < 0.5) { 87 | return 2 * t * t; 88 | } else { 89 | return @mulAdd(T, 4, t, -1) - 2 * t * t; 90 | } 91 | } 92 | 93 | test quadInOut { 94 | try std.testing.expectEqual(0.0, quadInOut(@as(f32, 0.0))); 95 | try std.testing.expectApproxEqAbs(@as(f32, 0.125), quadInOut(@as(f32, 0.25)), 0.001); 96 | try std.testing.expectEqual(0.5, quadInOut(@as(f32, 0.5))); 97 | try std.testing.expectApproxEqAbs(@as(f32, 0.875), quadInOut(@as(f32, 0.75)), 0.001); 98 | try std.testing.expectEqual(1.0, quadInOut(@as(f32, 1.0))); 99 | } 100 | 101 | /// Cubic easing. 102 | pub fn cubicIn(t: anytype) @TypeOf(t) { 103 | return t * t * t; 104 | } 105 | 106 | test cubicIn { 107 | try std.testing.expectEqual(0.0, cubicIn(@as(f32, 0.0))); 108 | try std.testing.expectEqual(0.125, cubicIn(@as(f32, 0.5))); 109 | try std.testing.expectEqual(1.0, cubicIn(@as(f32, 1.0))); 110 | } 111 | 112 | /// See `cubicIn`. 113 | pub fn cubicOut(t: anytype) @TypeOf(t) { 114 | const T = @TypeOf(t); 115 | const inv = 1.0 - t; 116 | return @mulAdd(T, inv * inv, -inv, 1.0); 117 | } 118 | 119 | test cubicOut { 120 | try std.testing.expectEqual(0.0, cubicOut(@as(f32, 0.0))); 121 | try std.testing.expectEqual(0.875, cubicOut(@as(f32, 0.5))); 122 | try std.testing.expectEqual(1.0, cubicOut(@as(f32, 1.0))); 123 | } 124 | 125 | /// Cubic ease in followed by cubic ease out. 126 | pub fn cubicInOut(t: anytype) @TypeOf(t) { 127 | const T = @TypeOf(t); 128 | if (t < 0.5) { 129 | return 4 * t * t * t; 130 | } else { 131 | const c = @mulAdd(T, -2.0, t, 2.0); 132 | return @mulAdd(T, -c / 2.0, c * c, 1.0); 133 | } 134 | } 135 | 136 | test cubicInOut { 137 | try std.testing.expectEqual(0.0, cubicInOut(@as(f32, 0.0))); 138 | try std.testing.expectApproxEqAbs(@as(f32, 0.0625), cubicInOut(@as(f32, 0.25)), 0.001); 139 | try std.testing.expectEqual(0.5, cubicInOut(@as(f32, 0.5))); 140 | try std.testing.expectApproxEqAbs(@as(f32, 0.9375), cubicInOut(@as(f32, 0.75)), 0.001); 141 | try std.testing.expectEqual(1.0, cubicInOut(@as(f32, 1.0))); 142 | } 143 | 144 | /// Quartic easing. 145 | pub fn quartIn(t: anytype) @TypeOf(t) { 146 | return t * t * t * t; 147 | } 148 | 149 | test quartIn { 150 | try std.testing.expectEqual(0.0, quartIn(@as(f32, 0.0))); 151 | try std.testing.expectEqual(0.0625, quartIn(@as(f32, 0.5))); 152 | try std.testing.expectEqual(1.0, quartIn(@as(f32, 1.0))); 153 | } 154 | 155 | /// See `quartIn`. 156 | pub fn quartOut(t: anytype) @TypeOf(t) { 157 | const T = @TypeOf(t); 158 | const inv = 1.0 - t; 159 | const squared = inv * inv; 160 | return @mulAdd(T, -squared, squared, 1.0); 161 | } 162 | 163 | test quartOut { 164 | try std.testing.expectEqual(0.0, quartOut(@as(f32, 0.0))); 165 | try std.testing.expectEqual(0.9375, quartOut(@as(f32, 0.5))); 166 | try std.testing.expectEqual(1.0, quartOut(@as(f32, 1.0))); 167 | } 168 | 169 | /// Quartic ease in followed by quartic ease out. 170 | pub fn quartInOut(t: anytype) @TypeOf(t) { 171 | const T = @TypeOf(t); 172 | if (t < 0.5) { 173 | return 8 * t * t * t * t; 174 | } else { 175 | const q = @mulAdd(T, -2.0, t, 2.0); 176 | return @mulAdd(T, -q / 2.0, q * q * q, 1.0); 177 | } 178 | } 179 | 180 | test quartInOut { 181 | try std.testing.expectEqual(0.0, quartInOut(@as(f32, 0.0))); 182 | try std.testing.expectApproxEqAbs(@as(f32, 0.03125), quartInOut(@as(f32, 0.25)), 0.001); 183 | try std.testing.expectEqual(0.5, quartInOut(@as(f32, 0.5))); 184 | try std.testing.expectApproxEqAbs(@as(f32, 0.96875), quartInOut(@as(f32, 0.75)), 0.001); 185 | try std.testing.expectEqual(1.0, quartInOut(@as(f32, 1.0))); 186 | } 187 | 188 | /// Quintic easing. 189 | pub fn quintIn(t: anytype) @TypeOf(t) { 190 | return t * t * t * t * t; 191 | } 192 | 193 | test quintIn { 194 | try std.testing.expectEqual(0.0, quintIn(@as(f32, 0.0))); 195 | try std.testing.expectEqual(0.03125, quintIn(@as(f32, 0.5))); 196 | try std.testing.expectEqual(1.0, quintIn(@as(f32, 1.0))); 197 | } 198 | 199 | /// See `quintIn`. 200 | pub fn quintOut(t: anytype) @TypeOf(t) { 201 | const T = @TypeOf(t); 202 | const inv = 1.0 - t; 203 | const squared = inv * inv; 204 | return @mulAdd(T, -squared, squared * inv, 1.0); 205 | } 206 | 207 | test quintOut { 208 | try std.testing.expectEqual(0.0, quintOut(@as(f32, 0.0))); 209 | try std.testing.expectEqual(0.96875, quintOut(@as(f32, 0.5))); 210 | try std.testing.expectEqual(1.0, quintOut(@as(f32, 1.0))); 211 | } 212 | 213 | /// Quintic ease in followed by quintic ease out. 214 | pub fn quintInOut(t: anytype) @TypeOf(t) { 215 | const T = @TypeOf(t); 216 | if (t < 0.5) { 217 | return 16 * t * t * t * t * t; 218 | } else { 219 | const q = @mulAdd(T, -2.0, t, 2.0); 220 | return @mulAdd(T, -q * q / 2.0, q * q * q, 1.0); 221 | } 222 | } 223 | 224 | test quintInOut { 225 | try std.testing.expectEqual(0.0, quintInOut(@as(f32, 0.0))); 226 | try std.testing.expectApproxEqAbs(@as(f32, 0.015625), quintInOut(@as(f32, 0.25)), 0.001); 227 | try std.testing.expectEqual(0.5, quintInOut(@as(f32, 0.5))); 228 | try std.testing.expectApproxEqAbs(@as(f32, 0.984375), quintInOut(@as(f32, 0.75)), 0.001); 229 | try std.testing.expectEqual(1.0, quintInOut(@as(f32, 1.0))); 230 | } 231 | 232 | /// Robert Penner's widely used exponential easing function. 233 | pub fn expIn(t: anytype) @TypeOf(t) { 234 | const T = @TypeOf(t); 235 | if (t <= 0.0) return 0; 236 | return std.math.pow(f32, 2, @mulAdd(T, 10, t, -10)); 237 | } 238 | 239 | test expIn { 240 | try std.testing.expectEqual(0.0, expIn(@as(f32, 0.0))); 241 | try std.testing.expectEqual(0.03125, expIn(@as(f32, 0.5))); 242 | try std.testing.expectEqual(1.0, expIn(@as(f32, 1.0))); 243 | } 244 | 245 | /// See `expIn`. 246 | pub fn expOut(t: anytype) @TypeOf(t) { 247 | return reverse(expIn, t, .{}); 248 | } 249 | 250 | test expOut { 251 | try std.testing.expectEqual(0.0, expOut(@as(f32, 0.0))); 252 | try std.testing.expectEqual(0.96875, expOut(@as(f32, 0.5))); 253 | try std.testing.expectEqual(1.0, expOut(@as(f32, 1.0))); 254 | } 255 | 256 | /// Exponential ease in followed by exponential ease out. 257 | pub fn expInOut(t: anytype) @TypeOf(t) { 258 | const T = @TypeOf(t); 259 | if (t >= 1.0) return 1.0; 260 | if (t <= 0.0) return 0.0; 261 | if (t < 0.5) { 262 | return std.math.pow(f32, 2, @mulAdd(T, 20, t, -10)) / 2.0; 263 | } else { 264 | return 1.0 - std.math.pow(T, 2.0, @mulAdd(T, -20.0, t, 10.0)) / 2.0; 265 | } 266 | } 267 | 268 | test expInOut { 269 | try std.testing.expectEqual(0.0, expInOut(@as(f32, 0.0))); 270 | try std.testing.expectApproxEqAbs(@as(f32, 0.015625), expInOut(@as(f32, 0.25)), 0.001); 271 | try std.testing.expectEqual(0.5, expInOut(@as(f32, 0.5))); 272 | try std.testing.expectApproxEqAbs(@as(f32, 0.984375), expInOut(@as(f32, 0.75)), 0.001); 273 | try std.testing.expectEqual(1.0, expInOut(@as(f32, 1.0))); 274 | } 275 | 276 | /// Circular easing. 277 | pub fn circIn(t: anytype) @TypeOf(t) { 278 | const T = @TypeOf(t); 279 | return 1.0 - @sqrt(@mulAdd(T, -t, t, 1.0)); 280 | } 281 | 282 | test circIn { 283 | try std.testing.expectEqual(0.0, circIn(@as(f32, 0.0))); 284 | try std.testing.expectApproxEqAbs(@as(f32, 0.13397459621556), circIn(@as(f32, 0.5)), 0.001); 285 | try std.testing.expectEqual(1.0, circIn(@as(f32, 1.0))); 286 | } 287 | 288 | /// See `circIn`. 289 | pub fn circOut(t: anytype) @TypeOf(t) { 290 | const T = @TypeOf(t); 291 | const inv = t - 1.0; 292 | return @sqrt(@mulAdd(T, -inv, inv, 1.0)); 293 | } 294 | 295 | test circOut { 296 | try std.testing.expectEqual(0.0, circOut(@as(f32, 0.0))); 297 | try std.testing.expectEqual(0.8660254037844385965883020617184229, circOut(@as(f32, 0.5))); 298 | try std.testing.expectEqual(1.0, circOut(@as(f32, 1.0))); 299 | } 300 | 301 | /// Circular ease in followed by circular ease out. 302 | pub fn circInOut(t: anytype) @TypeOf(t) { 303 | const T = @TypeOf(t); 304 | if (t < 0.5) { 305 | return (1.0 - @sqrt(@mulAdd(T, -4.0 * t, t, 1.0))) / 2.0; 306 | } else { 307 | const s = @mulAdd(T, -2.0, t, 2.0); 308 | return (@sqrt(@mulAdd(T, -s, s, 1)) + 1.0) / 2.0; 309 | } 310 | } 311 | 312 | test circInOut { 313 | try std.testing.expectEqual(0.0, circInOut(@as(f32, 0.0))); 314 | try std.testing.expectApproxEqAbs(@as(f32, 0.066987306), circInOut(@as(f32, 0.25)), 0.001); 315 | try std.testing.expectEqual(0.5, circInOut(@as(f32, 0.5))); 316 | try std.testing.expectApproxEqAbs(@as(f32, 0.9330127), circInOut(@as(f32, 0.75)), 0.001); 317 | try std.testing.expectEqual(1.0, circInOut(@as(f32, 1.0))); 318 | } 319 | 320 | pub const BackOptions = struct { 321 | /// Increasing this value increases the amount that the moves backwards before proceeding. 322 | back: f32 = 1.70158, 323 | }; 324 | 325 | /// Easing that moves backwards slightly before moving in the correct direction. 326 | /// 327 | /// One of Robert Penner's widely used easing functions. 328 | pub fn backIn(t: anytype, opt: BackOptions) @TypeOf(t) { 329 | const T = @TypeOf(t); 330 | const t2 = t * t; 331 | const t3 = t2 * t; 332 | return @mulAdd(T, t3, opt.back, @mulAdd(T, -t2, opt.back, t3)); 333 | } 334 | 335 | test backIn { 336 | try std.testing.expectEqual(0.0, backIn(@as(f32, 0.0), .{})); 337 | try std.testing.expectApproxEqAbs(@as(f32, -0.0641365625), backIn(@as(f32, 0.25), .{}), 0.001); 338 | try std.testing.expectApproxEqAbs(@as(f32, -0.0876975), backIn(@as(f32, 0.5), .{}), 0.001); 339 | try std.testing.expectApproxEqAbs(@as(f32, 0.1825903125), backIn(@as(f32, 0.75), .{}), 0.001); 340 | try std.testing.expectEqual(1.0, backIn(@as(f32, 1.0), .{})); 341 | } 342 | 343 | /// See `backIn`. 344 | pub fn backOut(t: anytype, opt: BackOptions) @TypeOf(t) { 345 | const T = @TypeOf(t); 346 | const inv = t - 1; 347 | const inv2 = inv * inv; 348 | const inv3 = inv2 * inv; 349 | return @mulAdd(T, opt.back, inv3, @mulAdd(T, opt.back, inv2, inv3 + 1)); 350 | } 351 | 352 | test backOut { 353 | try std.testing.expectEqual(0.0, backOut(@as(f32, 0.0), .{})); 354 | try std.testing.expectApproxEqAbs(@as(f32, 0.8174096875), backOut(@as(f32, 0.25), .{}), 0.001); 355 | try std.testing.expectApproxEqAbs(@as(f32, 1.0876975), backOut(@as(f32, 0.5), .{}), 0.001); 356 | try std.testing.expectApproxEqAbs(@as(f32, 1.0641365624999999), backOut(@as(f32, 0.75), .{}), 0.001); 357 | try std.testing.expectEqual(1.0, backOut(@as(f32, 1.0), .{})); 358 | } 359 | 360 | /// Ease in back followed by ease out back. 361 | pub fn backInOut(t: anytype, opt: BackOptions) @TypeOf(t) { 362 | const T = @TypeOf(t); 363 | const overshoot_adjusted = opt.back * 1.525; 364 | 365 | if (t < 0.5) { 366 | const a = 2 * t; 367 | const b = @mulAdd(T, overshoot_adjusted + 1, 2 * t, -overshoot_adjusted); 368 | return (a * a * b) / 2; 369 | } else { 370 | const a = @mulAdd(T, 2, t, -2); 371 | const b = @mulAdd(T, overshoot_adjusted + 1, @mulAdd(T, t, 2, -2), overshoot_adjusted); 372 | return @mulAdd(T, a * a, b, 2) / 2; 373 | } 374 | } 375 | 376 | test backInOut { 377 | try std.testing.expectEqual(0.0, backInOut(@as(f32, 0.0), .{})); 378 | try std.testing.expectApproxEqAbs(@as(f32, -0.09968184), backInOut(@as(f32, 0.25), .{}), 0.001); 379 | try std.testing.expectEqual(@as(f32, 0.5), backInOut(@as(f32, 0.5), .{})); 380 | try std.testing.expectApproxEqAbs(@as(f32, 1.0996819), backInOut(@as(f32, 0.75), .{}), 0.001); 381 | try std.testing.expectEqual(1.0, backInOut(@as(f32, 1.0), .{})); 382 | } 383 | 384 | pub const ElasticOptions = struct { 385 | /// If less than one, overshoots before arriving. Less than one has no effect. 386 | amplitude: f32 = 1.0, 387 | /// The period of the oscillation. 388 | period: f32 = 0.3, 389 | }; 390 | 391 | /// Elastic easing. 392 | /// 393 | /// One of Robert Penner's widely used elastic easing function. 394 | pub fn elasticIn(t: anytype, opt: ElasticOptions) @TypeOf(t) { 395 | if (t >= 1.0) return 1.0; 396 | if (t <= 0.0) return 0.0; 397 | 398 | var a = opt.amplitude; 399 | var m: f32 = pi / 4.0; 400 | if (a <= 1.0) { 401 | a = 1.0; 402 | m = opt.period / 4; 403 | } else { 404 | m = opt.period / (2 * pi) * std.math.asin(1 / a); 405 | } 406 | 407 | return -a * std.math.pow(@TypeOf(t), 2, @mulAdd(@TypeOf(t), 10, t, -10)) * @sin( 408 | (t - 1 - m) * 2 * pi / opt.period, 409 | ); 410 | } 411 | 412 | test elasticIn { 413 | try std.testing.expectEqual(0.0, elasticIn(@as(f32, 0.0), .{})); 414 | try std.testing.expectApproxEqAbs(@as(f32, -0.005524), elasticIn(@as(f32, 0.25), .{}), 0.001); 415 | try std.testing.expectApproxEqAbs(@as(f32, -0.015625), elasticIn(@as(f32, 0.5), .{}), 0.001); 416 | try std.testing.expectApproxEqAbs(@as(f32, 0.0883882), elasticIn(@as(f32, 0.75), .{}), 0.001); 417 | try std.testing.expectEqual(1.0, elasticIn(@as(f32, 1.0), .{})); 418 | } 419 | 420 | /// See `elasticIn`. 421 | pub fn elasticOut(t: anytype, opt: ElasticOptions) @TypeOf(t) { 422 | return reverse(elasticIn, t, .{opt}); 423 | } 424 | 425 | test elasticOut { 426 | try std.testing.expectEqual(0.0, elasticOut(@as(f32, 0.0), .{})); 427 | try std.testing.expectApproxEqAbs(@as(f32, 0.911611), elasticOut(@as(f32, 0.25), .{}), 0.001); 428 | try std.testing.expectApproxEqAbs(@as(f32, 1.015625), elasticOut(@as(f32, 0.5), .{}), 0.001); 429 | try std.testing.expectApproxEqAbs(@as(f32, 1.005524), elasticOut(@as(f32, 0.75), .{}), 0.001); 430 | try std.testing.expectEqual(1.0, elasticOut(@as(f32, 1.0), .{})); 431 | } 432 | 433 | /// Elastic ease in followed by elastic ease out. 434 | pub fn elasticInOut(t: anytype, opt: ElasticOptions) @TypeOf(t) { 435 | return mirror(elasticIn, t, .{opt}); 436 | } 437 | 438 | test elasticInOut { 439 | try std.testing.expectEqual(0.0, elasticInOut(@as(f32, 0.0), .{})); 440 | try std.testing.expectApproxEqAbs(@as(f32, -0.007812), elasticInOut(@as(f32, 0.25), .{}), 0.001); 441 | try std.testing.expectEqual(@as(f32, 0.5), elasticInOut(@as(f32, 0.5), .{})); 442 | try std.testing.expectApproxEqAbs(@as(f32, 1.0078125), elasticInOut(@as(f32, 0.75), .{}), 0.001); 443 | try std.testing.expectEqual(1.0, elasticInOut(@as(f32, 1.0), .{})); 444 | } 445 | 446 | /// Bounces with increasing magnitude until reaching the target. 447 | /// 448 | /// One of Robert Penner's widely used easing functions. 449 | pub fn bounceIn(t: anytype) @TypeOf(t) { 450 | return reverse(bounceOut, t, .{}); 451 | } 452 | 453 | test bounceIn { 454 | try std.testing.expectEqual(0.0, bounceIn(@as(f32, 0.0))); 455 | try std.testing.expectApproxEqAbs(@as(f32, 0.02734375), bounceIn(@as(f32, 0.25)), 0.001); 456 | try std.testing.expectApproxEqAbs(@as(f32, 0.234375), bounceIn(@as(f32, 0.5)), 0.001); 457 | try std.testing.expectApproxEqAbs(@as(f32, 0.52734375), bounceIn(@as(f32, 0.75)), 0.001); 458 | try std.testing.expectEqual(1.0, bounceIn(@as(f32, 1.0))); 459 | } 460 | 461 | /// See `bounceIn`. 462 | pub fn bounceOut(t: anytype) @TypeOf(t) { 463 | const T = @TypeOf(t); 464 | const a = 7.5625; 465 | const b = 2.75; 466 | 467 | if (t < 1.0 / b) { 468 | return a * t * t; 469 | } else if (t < 2.0 / b) { 470 | const t2 = t - 1.5 / b; 471 | return @mulAdd(T, a, t2 * t2, 0.75); 472 | } else if (t < 2.5 / b) { 473 | const t2 = t - 2.25 / b; 474 | return @mulAdd(T, a, t2 * t2, 0.9375); 475 | } else { 476 | const t2 = t - 2.625 / b; 477 | return @mulAdd(T, a, t2 * t2, 0.984375); 478 | } 479 | } 480 | 481 | test bounceOut { 482 | try std.testing.expectEqual(0.0, bounceOut(@as(f32, 0.0))); 483 | try std.testing.expectApproxEqAbs(@as(f32, 0.47265625), bounceOut(@as(f32, 0.25)), 0.001); 484 | try std.testing.expectApproxEqAbs(@as(f32, 0.765625), bounceOut(@as(f32, 0.5)), 0.001); 485 | try std.testing.expectApproxEqAbs(@as(f32, 0.97265625), bounceOut(@as(f32, 0.75)), 0.001); 486 | try std.testing.expectEqual(1.0, bounceOut(@as(f32, 1.0))); 487 | } 488 | 489 | /// Bounce in followed by bounce out. 490 | pub fn bounceInOut(t: anytype) @TypeOf(t) { 491 | return mirror(bounceIn, t, .{}); 492 | } 493 | 494 | test bounceInOut { 495 | try std.testing.expectEqual(0.0, bounceInOut(@as(f32, 0.0))); 496 | try std.testing.expectApproxEqAbs(@as(f32, 0.1171875), bounceInOut(@as(f32, 0.25)), 0.001); 497 | try std.testing.expectEqual(@as(f32, 0.5), bounceInOut(@as(f32, 0.5))); 498 | try std.testing.expectApproxEqAbs(@as(f32, 0.8828125), bounceInOut(@as(f32, 0.75)), 0.001); 499 | try std.testing.expectEqual(1.0, bounceInOut(@as(f32, 1.0))); 500 | } 501 | 502 | /// Smoothstep is a popular default easing function that eases in and out. 503 | /// 504 | /// It can be derived by solving for the coefficients of a cubic equation that passes through (0, 0) 505 | /// and (1, 1) with first derivatives of 0 at both points. 506 | /// 507 | /// Mathematically equivalent to `mix(cubicIn, cubicOut, t)`, but slightly more performant. 508 | pub fn smoothstep(t: anytype) @TypeOf(t) { 509 | const T = @TypeOf(t); 510 | return t * t * @mulAdd(T, -2.0, t, 3.0); 511 | } 512 | 513 | test smoothstep { 514 | try std.testing.expectEqual(0.0, smoothstep(@as(f32, 0.0))); 515 | try std.testing.expectEqual(mix(cubicIn, cubicOut, 0.25), smoothstep(@as(f32, 0.25))); 516 | try std.testing.expectEqual(0.5, smoothstep(@as(f32, 0.5))); 517 | try std.testing.expectEqual(mix(cubicIn, cubicOut, 0.75), smoothstep(@as(f32, 0.75))); 518 | try std.testing.expectEqual(1.0, smoothstep(@as(f32, 1.0))); 519 | } 520 | 521 | /// Smootherstep is a popular improvement to smoothstep popularized by Ken Perlin, and a slightly 522 | /// better default if you don't mind mildly heavier computation. 523 | /// 524 | /// It can be derived the same way as smoothstep, but you start with a quintic equation and require 525 | /// both the first *and second* derivatives to 0 at the start and end points, creating a smoother 526 | /// curve. 527 | pub fn smootherstep(t: anytype) @TypeOf(t) { 528 | const T = @TypeOf(t); 529 | const t3 = t * t * t; 530 | const t4 = t3 * t; 531 | const t5 = t4 * t; 532 | return @mulAdd(T, 6, t5, @mulAdd(T, -15.0, t4, 10.0 * t3)); 533 | } 534 | 535 | test smootherstep { 536 | try std.testing.expectEqual(0.0, smootherstep(@as(f32, 0.0))); 537 | try std.testing.expectEqual(0.103515625, smootherstep(@as(f32, 0.25))); 538 | try std.testing.expectEqual(0.5, smootherstep(@as(f32, 0.5))); 539 | try std.testing.expectEqual(0.8964844, smootherstep(@as(f32, 0.75))); 540 | try std.testing.expectEqual(1.0, smootherstep(@as(f32, 1.0))); 541 | } 542 | 543 | pub const StepOptions = struct { 544 | /// The number of in between steps to take. 545 | count: f32, 546 | }; 547 | 548 | /// An ease function that steps by fixed amounts. Starts on the first step which is already past 549 | /// zero, ends at 1 which is immediately after the last step. 550 | pub fn step(t: anytype, opt: StepOptions) @TypeOf(t) { 551 | return @floor(opt.count * t + 1) / (opt.count + 1); 552 | } 553 | 554 | test step { 555 | const exc: StepOptions = .{ .count = 2 }; 556 | try std.testing.expectEqual(@as(f32, 0.0), step(@as(f32, -0.1), exc)); 557 | try std.testing.expectEqual(@as(f32, 1.0 / 3.0), step(@as(f32, 0.0), exc)); 558 | try std.testing.expectEqual(@as(f32, 1.0 / 3.0), step(@as(f32, 0.1), exc)); 559 | try std.testing.expectEqual(@as(f32, 1.0 / 3.0), step(@as(f32, 0.4), exc)); 560 | try std.testing.expectEqual(@as(f32, 2.0 / 3.0), step(@as(f32, 0.5), exc)); 561 | try std.testing.expectEqual(@as(f32, 2.0 / 3.0), step(@as(f32, 0.7), exc)); 562 | try std.testing.expectEqual(@as(f32, 1.0), step(@as(f32, 1.0), exc)); 563 | } 564 | 565 | /// Mixes two easing functions by linear interpolation. This is an alternative to `mirror`. 566 | pub fn mix(in: anytype, out: anytype, t: anytype) @TypeOf(t) { 567 | return lerp(in(t), out(t), t); 568 | } 569 | 570 | test mix { 571 | try std.testing.expectEqual(0.0, mix(quadIn, quadOut, 0.0)); 572 | try std.testing.expectEqual(0.15625, mix(quadIn, quadOut, 0.25)); 573 | try std.testing.expectEqual(0.5, mix(quadIn, quadOut, 0.5)); 574 | try std.testing.expectEqual(0.84375, mix(quadIn, quadOut, 0.75)); 575 | try std.testing.expectEqual(1.0, mix(quadIn, quadOut, 1.0)); 576 | } 577 | 578 | /// Reverses an easing function. Ease in functions become ease out functions, ease out functions 579 | /// become ease in functions. 580 | pub fn reverse(f: anytype, t: anytype, args: anytype) @TypeOf(t) { 581 | return 1.0 - @call(.auto, f, .{1.0 - t} ++ args); 582 | } 583 | 584 | test reverse { 585 | try std.testing.expectEqual(quadOut(@as(f32, 0.0)), reverse(quadIn, @as(f32, 0.0), .{})); 586 | try std.testing.expectEqual(quadOut(@as(f32, 0.25)), reverse(quadIn, @as(f32, 0.25), .{})); 587 | try std.testing.expectEqual(quadOut(@as(f32, 0.5)), reverse(quadIn, @as(f32, 0.5), .{})); 588 | try std.testing.expectEqual(quadOut(@as(f32, 0.75)), reverse(quadIn, @as(f32, 0.75), .{})); 589 | try std.testing.expectEqual(quadOut(@as(f32, 1.0)), reverse(quadIn, @as(f32, 1.0), .{})); 590 | } 591 | 592 | /// Mirrors an easing in function to create an in out function. This is an alternative to `mix`. 593 | pub fn mirror(f: anytype, t: anytype, args: anytype) @TypeOf(t) { 594 | if (t < 0.5) { 595 | return @call(.auto, f, .{2 * t} ++ args) / 2.0; 596 | } else { 597 | return 1 - @call(.auto, f, .{@mulAdd(@TypeOf(t), -2, t, 2)} ++ args) / 2.0; 598 | } 599 | } 600 | 601 | test mirror { 602 | try std.testing.expectEqual(cubicInOut(@as(f32, 0.0)), mirror(cubicIn, @as(f32, 0.0), .{})); 603 | try std.testing.expectEqual(cubicInOut(@as(f32, 0.25)), mirror(cubicIn, @as(f32, 0.25), .{})); 604 | try std.testing.expectEqual(cubicInOut(@as(f32, 0.5)), mirror(cubicIn, @as(f32, 0.5), .{})); 605 | try std.testing.expectEqual(cubicInOut(@as(f32, 0.75)), mirror(cubicIn, @as(f32, 0.75), .{})); 606 | try std.testing.expectEqual(cubicInOut(@as(f32, 1.0)), mirror(cubicIn, @as(f32, 1.0), .{})); 607 | } 608 | 609 | /// Accelerates the `in` and `out` easing functions by two times, follows the first with the second. 610 | pub fn combine(in: anytype, in_args: anytype, out: anytype, out_args: anytype, t: f32) @TypeOf(t) { 611 | if (t < 0.5) { 612 | return @call(.auto, in, .{2 * t} ++ in_args) / 2.0; 613 | } else { 614 | return (1.0 + @call(.auto, out, .{2 * t - 1} ++ out_args)) / 2.0; 615 | } 616 | } 617 | 618 | test combine { 619 | try std.testing.expectEqual( 620 | cubicInOut(@as(f32, 0.0)), 621 | combine(cubicIn, .{}, cubicOut, .{}, @as(f32, 0.0)), 622 | ); 623 | try std.testing.expectEqual( 624 | cubicInOut(@as(f32, 0.25)), 625 | combine(cubicIn, .{}, cubicOut, .{}, @as(f32, 0.25)), 626 | ); 627 | try std.testing.expectEqual( 628 | cubicInOut(@as(f32, 0.5)), 629 | combine(cubicIn, .{}, cubicOut, .{}, @as(f32, 0.5)), 630 | ); 631 | try std.testing.expectEqual( 632 | cubicInOut(@as(f32, 0.75)), 633 | combine(cubicIn, .{}, cubicOut, .{}, @as(f32, 0.75)), 634 | ); 635 | try std.testing.expectEqual( 636 | cubicInOut(@as(f32, 1.0)), 637 | combine(cubicIn, .{}, cubicOut, .{}, @as(f32, 1.0)), 638 | ); 639 | } 640 | -------------------------------------------------------------------------------- /src/interp.zig: -------------------------------------------------------------------------------- 1 | //! Helpers for working with various interpolation schemes. 2 | 3 | const std = @import("std"); 4 | const geom = @import("root.zig"); 5 | 6 | const assert = std.debug.assert; 7 | 8 | /// Coerce to a float type if possible. 9 | fn PreferFloats(T: type) type { 10 | switch (@typeInfo(T)) { 11 | .float, .comptime_float => return T, 12 | .comptime_int => return comptime_float, 13 | else => return T, 14 | } 15 | } 16 | 17 | /// The type returned by `lerp`. 18 | fn Lerp(Start: type, End: type, T: type) type { 19 | const start: PreferFloats(Start) = undefined; 20 | const end: PreferFloats(End) = undefined; 21 | const t: PreferFloats(T) = undefined; 22 | switch (@typeInfo(@TypeOf(start, end))) { 23 | .float, .comptime_float => return @TypeOf(start, end, t), 24 | else => return @TypeOf(start, end), 25 | } 26 | } 27 | 28 | /// Linear interpolation, exact results at `0` and `1`. 29 | /// 30 | /// Supports floats, vectors of floats, and structs or arrays that only contain other supported 31 | /// types. Comptime ints are coerced to comptime floats. 32 | pub fn lerp( 33 | start: anytype, 34 | end: anytype, 35 | t: anytype, 36 | ) Lerp(@TypeOf(start), @TypeOf(end), @TypeOf(t)) { 37 | const Type = Lerp(@TypeOf(start), @TypeOf(end), @TypeOf(t)); 38 | switch (@typeInfo(Type)) { 39 | .float, .comptime_float => return @mulAdd(Type, start, 1.0 - t, end * t), 40 | .vector => return @mulAdd( 41 | Type, 42 | start, 43 | @splat(1.0 - t), 44 | @as(Type, end) * @as(Type, @splat(@floatCast(t))), 45 | ), 46 | .@"struct" => |info| { 47 | var result: Type = undefined; 48 | inline for (info.fields) |field| { 49 | @field(result, field.name) = lerp( 50 | @field(start, field.name), 51 | @field(end, field.name), 52 | t, 53 | ); 54 | } 55 | return result; 56 | }, 57 | .array => { 58 | var result: Type = undefined; 59 | for (&result, start, end) |*dest, a, b| { 60 | dest.* = lerp(a, b, t); 61 | } 62 | return result; 63 | }, 64 | else => comptime unreachable, 65 | } 66 | } 67 | 68 | test lerp { 69 | const expectEqual = std.testing.expectEqual; 70 | const ctf = comptime_float; 71 | const cti = comptime_int; 72 | 73 | // Float values 74 | { 75 | try expectEqual(100.0, lerp(100.0, 200.0, 0.0)); 76 | try expectEqual(200.0, lerp(100.0, 200.0, 1.0)); 77 | try expectEqual(150.0, lerp(100.0, 200.0, @as(f32, 0.5))); 78 | } 79 | 80 | // Float types 81 | { 82 | try expectEqual(@as(f32, 0), lerp(@as(f32, 0), @as(f32, 0), @as(f32, 0))); 83 | 84 | try expectEqual(@as(f64, 0), lerp(@as(f64, 0), @as(f32, 0), @as(f32, 0))); 85 | try expectEqual(@as(f64, 0), lerp(@as(f32, 0), @as(f64, 0), @as(f32, 0))); 86 | try expectEqual(@as(f64, 0), lerp(@as(f32, 0), @as(f32, 0), @as(f64, 0))); 87 | 88 | try expectEqual(@as(f64, 0), lerp(@as(f64, 0), @as(f64, 0), @as(f32, 0))); 89 | try expectEqual(@as(f64, 0), lerp(@as(f64, 0), @as(f32, 0), @as(f64, 0))); 90 | try expectEqual(@as(f64, 0), lerp(@as(f32, 0), @as(f64, 0), @as(f64, 0))); 91 | 92 | try expectEqual(@as(f64, 0), lerp(@as(f64, 0), @as(f64, 0), @as(f64, 0))); 93 | } 94 | 95 | // Comptime float types 96 | { 97 | try expectEqual(@as(f32, 0), lerp(@as(f32, 0), @as(f32, 0), @as(f32, 0))); 98 | 99 | try expectEqual(@as(f32, 0), lerp(@as(ctf, 0), @as(f32, 0), @as(f32, 0))); 100 | try expectEqual(@as(f32, 0), lerp(@as(f32, 0), @as(ctf, 0), @as(f32, 0))); 101 | try expectEqual(@as(f32, 0), lerp(@as(f32, 0), @as(f32, 0), @as(ctf, 0))); 102 | 103 | try expectEqual(@as(f32, 0), lerp(@as(ctf, 0), @as(ctf, 0), @as(f32, 0))); 104 | try expectEqual(@as(f32, 0), lerp(@as(ctf, 0), @as(f32, 0), @as(ctf, 0))); 105 | try expectEqual(@as(f32, 0), lerp(@as(f32, 0), @as(ctf, 0), @as(ctf, 0))); 106 | 107 | try expectEqual(@as(ctf, 0), lerp(@as(ctf, 0), @as(ctf, 0), @as(ctf, 0))); 108 | } 109 | 110 | // Comptime int types 111 | { 112 | try expectEqual(@as(f32, 0), lerp(@as(f32, 0), @as(f32, 0), @as(f32, 0))); 113 | 114 | try expectEqual(@as(f32, 0), lerp(@as(cti, 0), @as(f32, 0), @as(f32, 0))); 115 | try expectEqual(@as(f32, 0), lerp(@as(f32, 0), @as(cti, 0), @as(f32, 0))); 116 | try expectEqual(@as(f32, 0), lerp(@as(f32, 0), @as(f32, 0), @as(cti, 0))); 117 | 118 | try expectEqual(@as(f32, 0), lerp(@as(cti, 0), @as(cti, 0), @as(f32, 0))); 119 | try expectEqual(@as(f32, 0), lerp(@as(cti, 0), @as(f32, 0), @as(cti, 0))); 120 | try expectEqual(@as(f32, 0), lerp(@as(f32, 0), @as(cti, 0), @as(cti, 0))); 121 | 122 | try expectEqual(@as(cti, 0), lerp(@as(cti, 0), @as(cti, 0), @as(cti, 0))); 123 | } 124 | 125 | // Vectors, and checking for lack of clamping 126 | { 127 | const a: @Vector(3, f32) = .{ 0.0, 50.0, 100.0 }; 128 | const b: @Vector(3, f32) = .{ 100.0, 0.0, 200.0 }; 129 | try expectEqual(@Vector(3, f32){ -100.0, 100.0, 0.0 }, lerp(a, b, @as(ctf, -1.0))); 130 | try expectEqual(@Vector(3, f32){ 0.0, 50.0, 100.0 }, lerp(a, b, @as(ctf, 0.0))); 131 | try expectEqual(@Vector(3, f32){ 50.0, 25.0, 150.0 }, lerp(a, b, @as(f32, 0.5))); 132 | try expectEqual(@Vector(3, f32){ 100.0, 0.0, 200.0 }, lerp(a, b, @as(f32, 1))); 133 | try expectEqual(@Vector(3, f32){ 200.0, -50.0, 300.0 }, lerp(a, b, @as(ctf, 2.0))); 134 | } 135 | 136 | // Vector types 137 | { 138 | try expectEqual(@Vector(3, f32), @TypeOf(lerp( 139 | @Vector(3, f32){ 0.0, 0.0, 0.0 }, 140 | @Vector(3, f32){ 0.0, 0.0, 0.0 }, 141 | @as(f32, 0.0), 142 | ))); 143 | try expectEqual(@Vector(3, f32), @TypeOf(lerp( 144 | .{ 0.0, 0.0, 0.0 }, 145 | @Vector(3, f32){ 0.0, 0.0, 0.0 }, 146 | @as(f32, 0.0), 147 | ))); 148 | try expectEqual(@Vector(3, f32), @TypeOf(lerp( 149 | @Vector(3, f32){ 0.0, 0.0, 0.0 }, 150 | .{ 0.0, 0.0, 0.0 }, 151 | @as(f32, 0.0), 152 | ))); 153 | } 154 | 155 | // Structs 156 | { 157 | const Vec3 = struct { x: f32, y: f32, z: f32 }; 158 | const a: Vec3 = .{ .x = 0.0, .y = 50.0, .z = 100.0 }; 159 | const b: Vec3 = .{ .x = 100.0, .y = 0.0, .z = 200.0 }; 160 | try std.testing.expectEqual(Vec3{ .x = 0.0, .y = 50.0, .z = 100.0 }, lerp(a, b, 0.0)); 161 | try std.testing.expectEqual(Vec3{ .x = 50.0, .y = 25.0, .z = 150.0 }, lerp(a, b, 0.5)); 162 | try std.testing.expectEqual(Vec3{ .x = 100.0, .y = 0.0, .z = 200.0 }, lerp(a, b, 1.0)); 163 | } 164 | 165 | // Tuples 166 | { 167 | const a = .{ 0.0, 50.0, 100.0 }; 168 | const b = .{ 100.0, 0.0, 200.0 }; 169 | try std.testing.expectEqual(.{ 0.0, 50.0, 100.0 }, lerp(a, b, 0.0)); 170 | try std.testing.expectEqual(.{ 50.0, 25.0, 150.0 }, lerp(a, b, 0.5)); 171 | try std.testing.expectEqual(.{ 100.0, 0.0, 200.0 }, lerp(a, b, 1.0)); 172 | } 173 | 174 | // Array 175 | { 176 | const a: [3]f32 = .{ 0, 50, 100 }; 177 | const b: [3]f32 = .{ 100, 0, 200 }; 178 | try expectEqual([3]f32{ 0, 50, 100 }, lerp(a, b, @as(ctf, 0))); 179 | try expectEqual([3]f32{ 50, 25, 150 }, lerp(a, b, @as(f32, 0.5))); 180 | try expectEqual([3]f32{ 100, 0, 200 }, lerp(a, b, @as(f32, 1))); 181 | } 182 | } 183 | 184 | /// Similar to `lerp`, but `t` is clamped to [0, 1]. 185 | pub fn lerpClamped( 186 | start: anytype, 187 | end: anytype, 188 | t: anytype, 189 | ) @TypeOf(lerp(start, end, t)) { 190 | return lerp(start, end, clamp01(t)); 191 | } 192 | 193 | test lerpClamped { 194 | const expectEqual = std.testing.expectEqual; 195 | const ctf = comptime_float; 196 | const a: @Vector(3, f32) = .{ 0.0, 50.0, 100.0 }; 197 | const b: @Vector(3, f32) = .{ 100.0, 0.0, 200.0 }; 198 | try expectEqual(@Vector(3, f32){ 0.0, 50.0, 100.0 }, lerpClamped(a, b, @as(ctf, -1.0))); 199 | try expectEqual(@Vector(3, f32){ 0.0, 50.0, 100.0 }, lerpClamped(a, b, @as(ctf, 0.0))); 200 | try expectEqual(@Vector(3, f32){ 50.0, 25.0, 150.0 }, lerpClamped(a, b, @as(f32, 0.5))); 201 | try expectEqual(@Vector(3, f32){ 100.0, 0.0, 200.0 }, lerpClamped(a, b, @as(f32, 1))); 202 | try expectEqual(@Vector(3, f32){ 100.0, 0.0, 200.0 }, lerpClamped(a, b, @as(f32, 2))); 203 | } 204 | 205 | /// The type returned by `ilerp`. 206 | fn Ilerp(Start: type, End: type, Val: type) type { 207 | const start: PreferFloats(Start) = undefined; 208 | const end: PreferFloats(End) = undefined; 209 | const val: PreferFloats(Val) = undefined; 210 | return @TypeOf(start, end, val); 211 | } 212 | 213 | /// Inverse linear interpolation, gives exact results at 0 and 1. 214 | /// 215 | /// Only supports floats, and comptime ints which are coerced to comptime floats. 216 | pub fn ilerp( 217 | start: anytype, 218 | end: anytype, 219 | val: anytype, 220 | ) Ilerp(@TypeOf(start), @TypeOf(end), @TypeOf(val)) { 221 | const Type = Ilerp(@TypeOf(start), @TypeOf(end), @TypeOf(val)); 222 | comptime assert(@typeInfo(Type) == .float or @typeInfo(Type) == .comptime_float); 223 | return (@as(Type, val) - @as(Type, start)) / (@as(Type, end) - @as(Type, start)); 224 | } 225 | 226 | test ilerp { 227 | try std.testing.expectEqual(-1.0, ilerp(50.0, 100.0, 0.0)); 228 | try std.testing.expectEqual(0.0, ilerp(50.0, 100.0, 50.0)); 229 | try std.testing.expectEqual(0.5, ilerp(50.0, 100.0, 75.0)); 230 | try std.testing.expectEqual(1.0, ilerp(50.0, 100.0, 100.0)); 231 | try std.testing.expectEqual(2.0, ilerp(50.0, 100.0, 150.0)); 232 | 233 | // Make sure comptime ints can coerce to comptime floats 234 | try std.testing.expectEqual(-1.0, ilerp(50, 100.0, 0.0)); 235 | try std.testing.expectEqual(-1.0, ilerp(50.0, 100, 0.0)); 236 | try std.testing.expectEqual(-1.0, ilerp(50.0, 100.0, 0)); 237 | try std.testing.expectEqual(-1.0, ilerp(50, 100, 0.0)); 238 | try std.testing.expectEqual(-1.0, ilerp(50, 100.0, 0)); 239 | try std.testing.expectEqual(-1.0, ilerp(50.0, 100, 0)); 240 | try std.testing.expectEqual(-1.0, ilerp(50, 100, 0)); 241 | } 242 | 243 | /// Similar to `ilerp`, but clamps the result to [0, 1]. 244 | pub fn ilerpClamped(start: anytype, end: anytype, val: anytype) @TypeOf(ilerp(start, end, val)) { 245 | return clamp01(ilerp(start, end, val)); 246 | } 247 | 248 | test ilerpClamped { 249 | try std.testing.expectEqual(0.0, ilerpClamped(50.0, 100.0, 0.0)); 250 | try std.testing.expectEqual(0.0, ilerpClamped(50.0, 100.0, 50.0)); 251 | try std.testing.expectEqual(0.5, ilerpClamped(50.0, 100.0, 75.0)); 252 | try std.testing.expectEqual(1.0, ilerpClamped(50.0, 100.0, 100.0)); 253 | try std.testing.expectEqual(1.0, ilerpClamped(50.0, 100.0, 150.0)); 254 | } 255 | 256 | fn Remap( 257 | InStart: type, 258 | InEnd: type, 259 | OutStart: type, 260 | OutEnd: type, 261 | Val: type, 262 | ) type { 263 | const T = Ilerp(InStart, InEnd, Val); 264 | return Lerp(OutStart, OutEnd, T); 265 | } 266 | 267 | /// Remaps a value from the start range into the end range. 268 | /// 269 | /// Only supports floats. 270 | pub fn remap( 271 | in_start: anytype, 272 | in_end: anytype, 273 | out_start: anytype, 274 | out_end: anytype, 275 | val: anytype, 276 | ) Remap( 277 | @TypeOf(in_start), 278 | @TypeOf(in_end), 279 | @TypeOf(out_start), 280 | @TypeOf(out_end), 281 | @TypeOf(val), 282 | ) { 283 | const t = ilerp(in_start, in_end, val); 284 | return lerp(out_start, out_end, t); 285 | } 286 | 287 | test remap { 288 | try std.testing.expectEqual(0.0, remap(10.0, 20.0, 50.0, 100.0, 0.0)); 289 | try std.testing.expectEqual(150.0, remap(10.0, 20.0, 50.0, 100.0, 30.0)); 290 | 291 | try std.testing.expectEqual(50.0, remap(10.0, 20.0, 50.0, 100.0, 10.0)); 292 | try std.testing.expectEqual(50.0, remap(10.0, 20.0, 50.0, 100.0, 10.0)); 293 | try std.testing.expectEqual(100.0, remap(10.0, 20.0, 50.0, 100.0, 20.0)); 294 | try std.testing.expectEqual(75.0, remap(10.0, 20.0, 50.0, 100.0, 15.0)); 295 | 296 | // Check types 297 | const f32_0: f32 = 0; 298 | const f64_0: f64 = 0; 299 | const ctf_0: comptime_float = 0; 300 | const cti_0: comptime_int = 0; 301 | 302 | try std.testing.expectEqual(f32, @TypeOf(remap(f32_0, f32_0, f32_0, f32_0, f32_0))); 303 | try std.testing.expectEqual(f64, @TypeOf(remap(f64_0, f32_0, f32_0, f32_0, f32_0))); 304 | try std.testing.expectEqual(f64, @TypeOf(remap(f32_0, f64_0, f32_0, f32_0, f32_0))); 305 | try std.testing.expectEqual(f64, @TypeOf(remap(f32_0, f32_0, f64_0, f32_0, f32_0))); 306 | try std.testing.expectEqual(f64, @TypeOf(remap(f32_0, f32_0, f32_0, f64_0, f32_0))); 307 | try std.testing.expectEqual(f64, @TypeOf(remap(f32_0, f32_0, f32_0, f32_0, f64_0))); 308 | 309 | try std.testing.expectEqual(f32, @TypeOf(remap(ctf_0, f32_0, f32_0, f32_0, f32_0))); 310 | try std.testing.expectEqual(f32, @TypeOf(remap(f32_0, ctf_0, f32_0, f32_0, f32_0))); 311 | try std.testing.expectEqual(f32, @TypeOf(remap(f32_0, f32_0, ctf_0, f32_0, f32_0))); 312 | try std.testing.expectEqual(f32, @TypeOf(remap(f32_0, f32_0, f32_0, ctf_0, f32_0))); 313 | try std.testing.expectEqual(f32, @TypeOf(remap(f32_0, f32_0, f32_0, f32_0, ctf_0))); 314 | try std.testing.expectEqual(comptime_float, @TypeOf(remap(ctf_0, ctf_0, ctf_0, ctf_0, ctf_0))); 315 | 316 | try std.testing.expectEqual(f32, @TypeOf(remap(cti_0, f32_0, f32_0, f32_0, f32_0))); 317 | try std.testing.expectEqual(f32, @TypeOf(remap(f32_0, cti_0, f32_0, f32_0, f32_0))); 318 | try std.testing.expectEqual(f32, @TypeOf(remap(f32_0, f32_0, cti_0, f32_0, f32_0))); 319 | try std.testing.expectEqual(f32, @TypeOf(remap(f32_0, f32_0, f32_0, cti_0, f32_0))); 320 | try std.testing.expectEqual(f32, @TypeOf(remap(f32_0, f32_0, f32_0, f32_0, cti_0))); 321 | } 322 | 323 | /// Similar to `remap`, but the results are clamped to [start, end]. 324 | pub fn remapClamped( 325 | in_start: anytype, 326 | in_end: anytype, 327 | out_start: anytype, 328 | out_end: anytype, 329 | val: anytype, 330 | ) @TypeOf(in_start, in_end, out_start, out_end, val) { 331 | const t = ilerp(in_start, in_end, val); 332 | return lerpClamped(out_start, out_end, t); 333 | } 334 | 335 | test remapClamped { 336 | try std.testing.expectEqual(50.0, remapClamped(10.0, 20.0, 50.0, 100.0, 0.0)); 337 | try std.testing.expectEqual(100.0, remapClamped(10.0, 20.0, 50.0, 100.0, 30.0)); 338 | 339 | try std.testing.expectEqual(50.0, remapClamped(10.0, 20.0, 50.0, 100.0, 10.0)); 340 | try std.testing.expectEqual(50.0, remapClamped(10.0, 20.0, 50.0, 100.0, 10.0)); 341 | try std.testing.expectEqual(100.0, remapClamped(10.0, 20.0, 50.0, 100.0, 20.0)); 342 | try std.testing.expectEqual(75.0, remapClamped(10.0, 20.0, 50.0, 100.0, 15.0)); 343 | } 344 | 345 | /// Clamps a value between 0 and 1. 346 | pub fn clamp01(val: anytype) @TypeOf(val) { 347 | return @max(0.0, @min(1.0, val)); 348 | } 349 | 350 | test clamp01 { 351 | try std.testing.expectEqual(0.0, clamp01(-1.0)); 352 | try std.testing.expectEqual(1.0, clamp01(10.0)); 353 | try std.testing.expectEqual(0.5, clamp01(0.5)); 354 | } 355 | 356 | /// Processes a delta time value to make it usable as the `t` argument to lerp. 357 | /// 358 | /// It's often tempting to pass delta time into lerp to create smooth motion, e.g. to smoothly move 359 | /// a follow camera towards a position behind the player each frame, but this is not framerate 360 | /// independent. 361 | /// 362 | /// This function processes a delta time value, returning a `t` value that can be used for framerate 363 | /// independent lerp. 364 | /// 365 | /// The smoothing parameter ranges from zero to one, higher values result in more smoothing. In 366 | /// particular, the smoothing value is appropriately equivalent to the distance from t = 1 the 367 | /// return value will be for a delta time of one. 368 | /// 369 | /// A nice write up elaborating on this concept can be found here: 370 | /// https://www.rorydriscoll.com/2016/03/07/frame-rate-independent-damping-using-lerp/ 371 | pub fn damp(smoothing: anytype, dt: anytype) @TypeOf(dt) { 372 | return 1.0 - std.math.pow(@TypeOf(dt), @floatCast(smoothing), dt); 373 | } 374 | 375 | test damp { 376 | try std.testing.expectEqual(1.0, damp(0.0, @as(f32, 1.0))); 377 | try std.testing.expectEqual(0.8, damp(0.2, @as(f32, 1.0))); 378 | try std.testing.expectEqual(0.5, damp(0.5, @as(f32, 1.0))); 379 | } 380 | -------------------------------------------------------------------------------- /src/root.zig: -------------------------------------------------------------------------------- 1 | //! Composable easing an interpolation functions. 2 | 3 | const std = @import("std"); 4 | const builtin = @import("builtin"); 5 | 6 | pub const ease = @import("ease.zig"); 7 | pub const interp = @import("interp.zig"); 8 | 9 | test { 10 | std.testing.refAllDecls(@This()); 11 | } 12 | --------------------------------------------------------------------------------