Transforms

A transform is a gradual, predetermined change over time to a property of a Drawable. They are comparable to CSS transitions. osu!framework allows you to add transforms to Drawables and choose the easing function that is used to interpolate between the start and end value of the changed property.

The most common properties used by transitions are:

  • Color
  • Position and Scale
  • Opacity (to fade drawables in and out)
  • Rotation

So, some scenarios you can create with this are:

  • Expanding/coloring a button when it is being hovered
  • Fading out particles
  • Continuously rotating or bouncing UI elements
  • Moving an element out of the way after clicking it

Basic Usage

To use a single Transform on a drawable, simply call its respective extension method:

// instantly set scale to 1.5x
myBox.ScaleTo(1.5f);

This returns a TransformSequence, so you can chain multiple Transforms to execute them all at once:

// instantly set scale to 1.5x and rotate 90 degrees clockwise
myBox.ScaleTo(1.5f).RotateTo(90f);

By default, transforms will execute instantly, with no visible animation. To make a transform happen over time, pass in a time parameter:

// scale to 1.5x over 1000ms, with no/linear easing
myBox.ScaleTo(1.5f, 1000f);

When no other easing type is provided, the transform will happen linearly. This means that in this scaling example, the size of the box will be 1.125x after 250ms, 1.25x after 500ms and 1.375x after 750ms. Easing can be used to change the rate of change at which this happens and osu!framework supports many types of them. To get an idea of some of them, see easings.net.

// scale to 1.5x over 1000ms, with a sudden start and smooth ending
myBox.ScaleTo(1.5f, 1000f, Easing.OutQuint);

If you want one transition to happen after another, you can use Then. This will wait until all transforms before it are executed before running the next batch.

// scaling and rotation start at the same time
// since scaling takes longer, rotating finishes first
myBox.ScaleTo(1.5f, 1000f).RotateTo(90f, 500f);

// box first scales up, *then* rotates 90 degrees
myBox.ScaleTo(1.5f, 1000f).Then().RotateTo(90f, 500f);

// scale to 1.5x and rotate 90 degrees, *then* scale to 0.5x and rotate back.
myBox
    .ScaleTo(1.5f, 1000f).RotateTo(90f, 500f)
    .Then()
    .ScaleTo(0.5f, 250f).RotateTo(0f, 250f);

You can loop a series of transforms with Loop:

// fade between blue and red every 2 seconds
myBox
    .FadeColour(Color4.RoyalBlue, 1000f)
    .Then()
    .FadeColour(Color4.Tomato, 1000f)
    .Loop();

With Delay, you can delay the transforms after it. It is different from Then in that it won't wait for all transitions to finish. This allows you to start a transition while another one is still running:

// move downwards over 1s, and start fading out halfway
myBox.MoveToOffset(new Vector2(0f, 100f), 1000f).Delay(500f).FadeOut(500f);

If you just fade out a drawable, it will remain in the draw hierarchy and be active. You can make a drawable clean up after the transitions are done through Expire. This will queue the drawable for disposal, so it cannot be used after it has expired!

// fade out and mark for disposal
myBox.FadeOut(500f).Expire();

Example: osu!lazer menu buttons

This example shows how to recreate part of the main menu of osu!lazer, including bouncing icons.

Video of main menu recreation

		[BackgroundDependencyLoader]
		private void Load()
		{
			const int children = 4;
			Child = new FillFlowContainer
			{
				Direction = FillDirection.Horizontal,
				AutoSizeAxes = Axes.Both,
				Anchor = Anchor.Centre,
				Origin = Anchor.Centre,

				Children = Enumerable.Range(0, children).Select(i => new MenuButton(i)).ToList(),
			};
		}

		private class MenuButton : CompositeDrawable
		{
			private static Color4[] Colors = {Color4.Aqua, Color4.RoyalBlue, Color4.Tomato, Color4.HotPink};
			private readonly int _idx;
			private SpriteText _icon;

			public MenuButton(int idx)
			{
				_idx = idx;
			}

			[BackgroundDependencyLoader]
			private void Load()
			{
				Size = new Vector2(200f, 150f);

				InternalChildren = new Drawable[]
				{
					new Box
					{
						Name = "Background",
						RelativeSizeAxes = Axes.Both,
						Depth = 1,
						Colour = Colors[_idx % Colors.Length],
					},
					_icon = new SpriteText
					{
						Text = ((char) ('A' + _idx)).ToString(),
						Anchor = Anchor.Centre,
						Origin = Anchor.Centre,
						Font = FontUsage.Default.With(size: 36f),
						Shadow = true,
					},
				};
			}

			protected override bool OnHover(HoverEvent e)
			{
				const float durationStep = 250f;
				this.ResizeTo(new Vector2(300f, 150f), 500f, Easing.OutElastic);

				// using InCirc/OutCirc for Y movement because it appears similar to normal gravity
				// using InSine/OutSine for X movement and rotation because it appears fairly smooth
				_icon
					// move to left
					.MoveToX(-15f, durationStep, Easing.OutSine)
					.MoveToY(15f, durationStep, Easing.InCirc)
					.RotateTo(-30f, durationStep, Easing.OutSine)
					.Then()
					// move to top
					.MoveToX(0f, durationStep, Easing.InSine)
					.MoveToY(-35f, durationStep, Easing.OutCirc)
					.RotateTo(0f, durationStep, Easing.InSine)
					.Then()
					// move to right
					.MoveToX(15f, durationStep, Easing.OutSine)
					.MoveToY(15f, durationStep, Easing.InCirc)
					.RotateTo(30f, durationStep, Easing.OutSine)
					.Then()
					// move to top again
					.MoveToX(0f, durationStep, Easing.InSine)
					.MoveToY(-35f, durationStep, Easing.OutCirc)
					.RotateTo(0f, durationStep, Easing.InSine)
					// loop
					.Loop();

				// do not propagate hover event down
				return true;
			}

			protected override void OnHoverLost(HoverLostEvent e)
			{
				// the icon transition is a loop, we should stop it first
				_icon.ClearTransforms();

				this.ResizeTo(new Vector2(200f, 150f), 500f, Easing.OutElastic);
				_icon.MoveTo(Vector2.Zero, 250f, Easing.OutQuint).RotateTo(0f, 250f, Easing.OutQuint);

				base.OnHoverLost(e);
			}
		}

You can view this example in the FrameworkExamples project.

Creating a transform for other properties

You can create transforms for any property whose type is supported by the framework or implements the IInterpolable interface.

The following types are supported by default in the framework:

  • all C# number types, such as byte, uint, float, decimal, etc.
  • ColourInfo
  • EdgeEffectParameters
  • SRGBColour
  • Color4
  • Colour4
  • Vector2
  • RectangleF

Additionally, the framework implements IInterpolable for the following classes:

  • MarginPadding

To transform a property, use TransformTo:

box.TransformTo(nameof(box.Shear), Vector2.Zero, 1000, Easing.OutCubic);

The first parameter to TransformTo should be the name of a property with a supported type. In this case the Shear property has type Vector2. It is recommended to use nameof to ensure the name is correct.

Transforming Bindables

Transforming Bindable properties of a Drawable is also possible, as long as its generic parameter is supported. Instead of using TransformTo, use TransformBindableTo:

// checkerboard.TileCountX is of type BindableInt
checkerboard.TransformBindableTo(checkerboard.TileCountX, 32, 1000, Easing.OutCubic);

Implementing IInterpolable

When types are not supported for transforms, the framework checks if they implement IInterpolable. IInterpolable contains a ValueAt method that will then be used to decide the value of that property at a certain point.

Important: While IInterpolable.ValueAt is an instance method, the class it exists in will be created with FormatterServices.GetSafeUninitializedObject meaning all instance members will be zeroed out. Accessing them can cause undefined behavior. You should program your ValueAt method as if it is static.

As an example, the following code implements a wrapper type around a string that allows interpolating the length of that string.

String example

		private const string LoremIpsum = "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod" +
		                                  "tempor incididunt ut labore et dolore magna aliqua.";

		private const string LoremIpsumShort = "Lorem ipsum dolor sit amet";

		[BackgroundDependencyLoader]
		private void Load()
		{
			InterpolatableSpriteText text;
			Child = text = new InterpolatableSpriteText
			{
				Anchor = Anchor.Centre,
				Origin = Anchor.Centre,
				InterpolableText = new InterpolableString(""),
			};

			AddStep("Interpolate to short lorem",
				() => text.TransformTo(nameof(text.InterpolableText), new InterpolableString(LoremIpsumShort),
					2000, Easing.OutQuint));

			AddStep("Interpolate to long lorem",
				() => text.TransformTo(nameof(text.InterpolableText), new InterpolableString(LoremIpsum),
					2000, Easing.OutQuint));
		}

		private class InterpolatableSpriteText : SpriteText
		{
			public InterpolableString InterpolableText
			{
				get => new(base.Text.ToString());
				set => base.Text = value.Value;
			}
		}

		/// <summary>
		/// A string wrapper that can interpolate between lengths. A better interpolation would
		/// </summary>
		private readonly struct InterpolableString : IInterpolable<InterpolableString>
		{
			public readonly string Value;

			public InterpolableString(string value)
			{
				Value = value;
			}

			public InterpolableString ValueAt<TEasing>(double time, InterpolableString startValue,
				InterpolableString endValue,
				double startTime, double endTime, in TEasing easing) where TEasing : IEasingFunction
			{
				var longestValue = startValue.Value.Length > endValue.Value.Length ? startValue : endValue;
				var shortestValue = startValue.Value.Length <= endValue.Value.Length ? startValue : endValue;

				// we can only go from "AAA" to "AAABBB" and vice versa, not from "AAABBB" to "CCC"
				if (!longestValue.Value.StartsWith(shortestValue.Value))
					throw new Exception("Cannot interpolate between strings with different start");

				var newLength = Interpolation.ValueAt(
					time, startValue.Value.Length, endValue.Value.Length,
					startTime, endTime, easing);

				newLength = Math.Clamp(newLength, 0, longestValue.Value.Length);

				return new InterpolableString(longestValue.Value[..newLength]);
			}
		}

You can view this example in the FrameworkExamples project.

Custom easing

If none of the existing easing functions fit your needs, you can define your own:

		/// <summary>
		/// Moves 450 degrees on a sine function.
		/// </summary>
		private class SineEasing : IEasingFunction
		{
			// static instance to use, to prevent unneeded allocations
			public static readonly SineEasing Instance = new();

			public double ApplyEasing(double time)
			{
				// Math.Sin takes radians, so convert 450 degrees to radians
				const double radian = 360.0 / Math.Tau;
				const double totalRadians = 450.0 / radian;

				// scale the 0-1 time to 0-450 degrees (in radians)
				return Math.Sin(time * totalRadians);
			}
		}

You can then pass an instance of this class to most easing functions:

				box.MoveToX(100f, 1500f, SineEasing.Instance)

You can view this example in the FrameworkExamples project.


Last verified against v2021.728.0