Skip to content

Instantly share code, notes, and snippets.

@maluoi
Last active June 4, 2024 04:35
Show Gist options
  • Save maluoi/d65f35a90c1129cac2856e25f745f9a2 to your computer and use it in GitHub Desktop.
Save maluoi/d65f35a90c1129cac2856e25f745f9a2 to your computer and use it in GitHub Desktop.
A basic tweening / lerping library for StereoKit.
// SPDX-License-Identifier: MIT
// The authors below grant copyright rights under the MIT license:
// Copyright (c) 2024 Nick Klingensmith
using System;
namespace StereoKit.Framework
{
/// <summary> Don't know which to use? Try SoftOut! It's a great default!
/// This site is a great reference for how these functions look:
/// https://easings.net/
/// </summary>
public static class DynEase
{
/// <summary>A constant motion the whole way through. Stops and starts
/// hard, doesn't look all that great in most cases.</summary>
public static float Linear (float t) => t;
/// <summary>Quadratic soft start with a hard stop.</summary>
public static float SoftIn (float t) => t * t;
/// <summary>Quadratic fast start with a soft stop, a very good default
/// for most cases as it feels very responsive and looks good.</summary>
public static float SoftOut (float t) => 1 - (1 - t) * (1 - t);
/// <summary>Quadratic smooth start, fast middle, and smooth stop.
/// </summary>
public static float SoftInOut (float t) => t < 0.5f ? 2 * t * t : 1 - (float)Math.Pow(-2 * t + 2, 2) / 2;
/// <summary>Quartic soft start with a hard stop.</summary>
public static float FastIn (float t) => t * t * t * t;
/// <summary>Quartic fast start with a soft stop, a pretty good default
/// for most cases as it feels very responsive and looks good.</summary>
public static float FastOut (float t) => 1 - (float)Math.Pow(1 - t, 4);
/// <summary>Quartic smooth start, fast middle, and smooth stop.
/// </summary>
public static float FastInOut (float t) => t < 0.5 ? 8 * t * t * t * t : 1 - (float)Math.Pow(-2 * t + 2, 4) / 2;
const float overshoot = 1.70158f;
const float overshoot2 = overshoot + 1;
/// <summary>Soft start with a hard stop, overshoots its destination a
/// bit before arriving at it.</summary>
public static float OvershootIn (float t) => overshoot2 * t * t * t - overshoot * t * t;
/// <summary>Fast start with a soft stop, backs off a bit before moving
/// to its destination.</summary>
public static float OvershootOut (float t) => 1 + overshoot2 * (float)Math.Pow(t - 1, 3) + overshoot * (float)Math.Pow(t - 1, 2);
/// <summary>Smooth start, fast middle, and smooth stop. Overshoots on
/// both the start and the end.</summary>
public static float OvershootInOut(float t) => t < 0.5
? ((float)Math.Pow(2 * t, 2) * ((overshoot2 + 1) * 2 * t - overshoot2)) / 2
: ((float)Math.Pow(2 * t - 2, 2) * ((overshoot2 + 1) * (t * 2 - 2) + overshoot2) + 2) / 2;
}
public delegate float EaseFn(float t);
public class DynVec3
{
Vec3 begin, end;
float start, duration;
EaseFn ease = DynEase.Linear;
public bool IsFinished => Time.Totalf - start >= duration;
public DynVec3(Vec3 pt) => SetTo(pt);
public DynVec3(float x, float y, float z) => SetTo(new Vec3(x,y,z));
public Vec3 Resolve()
{
float t = Math.Min(1, (Time.Totalf - start) / duration);
return Vec3.Lerp(begin, end, ease(t));
}
public void AnimTo(float x, float y, float z, float duration, EaseFn easeFn) => AnimTo(new Vec3(x,y,z), duration, easeFn );
public void AnimTo(Vec3 pt, float duration, EaseFn easeFn) { begin = Resolve(); start = Time.Totalf; end = pt; ease = easeFn; this.duration = duration; }
public void SetTo (Vec3 pt) { begin = pt; start = 0; end = pt; ease = DynEase.Linear; this.duration = 1; }
public static implicit operator Vec3(DynVec3 v) => v.Resolve();
}
public class DynColor
{
Color begin, end;
float start, duration;
EaseFn ease = DynEase.Linear;
public bool IsFinished => Time.Totalf - start >= duration;
public DynColor(Color color) => SetTo(color);
public DynColor(float r, float g, float b, float a=1) => SetTo(new Color(r,g,b,a));
public Color Resolve()
{
float t = Math.Min(1, (Time.Totalf - start) / duration);
return Color.Lerp(begin, end, ease(t));
}
public void AnimTo(Color color, float duration, EaseFn easeFn) { begin = Resolve(); start = Time.Totalf; end = color; ease = easeFn; this.duration = duration; }
public void SetTo (Color color) { begin = color; start = 0; end = color; ease = DynEase.Linear; this.duration = 1; }
public static implicit operator Color(DynColor color) => color.Resolve();
}
public class DynPose
{
Pose begin, end;
float start, duration;
EaseFn ease = DynEase.Linear;
public bool IsFinished => Time.Totalf - start >= duration;
public DynPose(Pose pose) => SetTo(pose);
public DynPose(Vec3 pos, Quat rot) => SetTo(new Pose(pos, rot));
public Pose Resolve()
{
float t = Math.Min(1, (Time.Totalf - start) / duration);
return Pose.Lerp(begin, end, ease(t));
}
public void AnimTo(Pose pose, float duration, EaseFn easeFn) { begin = Resolve(); start = Time.Totalf; end = pose; ease = easeFn; this.duration = duration; }
public void SetTo (Pose pose) { begin = pose; start = 0; end = pose; ease = DynEase.Linear; this.duration = 1; }
public static implicit operator Pose(DynPose pose) => pose.Resolve();
}
}
if (!SK.Initialize())
return;
Random r = new ();
DynVec3 pos = new (0,0,-0.5f);
DynColor col = new (Color.White);
EaseFn[] eases = new EaseFn[] {
DynEase.SoftOut,
DynEase.FastOut,
DynEase.SoftInOut,
DynEase.OvershootInOut };
SK.Run(() =>
{
Mesh.Sphere.Draw(Material.Default, Matrix.TS(pos, 0.1f), col);
if (Input.Key(Key.Space).IsJustActive())
{
float x = (r.NextSingle() - 0.5f) * 0.5f;
float y = (r.NextSingle() - 0.5f) * 0.5f;
float z = -0.5f - (r.NextSingle() - 0.5f) * 0.5f;
Color color = Color.HSV(r.NextSingle(), 0.8f, 1);
EaseFn ease = eases[r.Next(eases.Length)];
pos.AnimTo(x, y, z, 0.5f, ease);
col.AnimTo(color, 0.5f, ease);
}
});
@maluoi
Copy link
Author

maluoi commented May 9, 2024

Example code while running looks like this!

StereoKitDynAnim

Usage

The core concept of this code is to create some basic data types that animate themselves over time. There's no system involved here, these objects can take care of their own timing and evaluation! In many cases, you can treat them quite similarly to their base types.

DynVec3 position = new (0,0,0);

// To animate this to a new location over the next half a second:
position.AnimTo(new Vec3(1,0,0), 0.5f, DynEase.SoftOut);

// To assign it to a specific location (WITHOUT animation)
position.SetTo(new Vec3(1,0,0));

// DynVec3 and friends have implicit cast operators that resolve their type to a normal Vec3
Vec3 currentPosition = position;
// Alternatively, you can explicitly Resolve the value
currentPosition = position.Resolve();

@maluoi
Copy link
Author

maluoi commented Jun 4, 2024

This can now be found in StereoKit.Framework with a slightly different name. Ease, EaseVec3, EaseColor, and EasePose.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment