mirror of
https://github.com/SK-la/osu-framework.git
synced 2026-03-13 11:20:31 +00:00
同步更新
This commit is contained in:
327
osu.Framework.Tests/Visual/Drawables/TestSceneSpring.cs
Normal file
327
osu.Framework.Tests/Visual/Drawables/TestSceneSpring.cs
Normal file
@@ -0,0 +1,327 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Graphics.Lines;
|
||||
using osu.Framework.Graphics.Shapes;
|
||||
using osu.Framework.Graphics.Sprites;
|
||||
using osu.Framework.Graphics.Transforms;
|
||||
using osu.Framework.Graphics.UserInterface;
|
||||
using osu.Framework.Input.Events;
|
||||
using osu.Framework.Layout;
|
||||
using osu.Framework.Testing;
|
||||
using osuTK;
|
||||
using osuTK.Graphics;
|
||||
|
||||
namespace osu.Framework.Tests.Visual.Drawables
|
||||
{
|
||||
public partial class TestSceneSpring : TestScene
|
||||
{
|
||||
private readonly BindableFloat naturalFrequency = new BindableFloat(2)
|
||||
{
|
||||
MinValue = 0.1f,
|
||||
MaxValue = 8f,
|
||||
Precision = 0.01f,
|
||||
};
|
||||
|
||||
private readonly BindableFloat damping = new BindableFloat(1)
|
||||
{
|
||||
MinValue = 0f,
|
||||
MaxValue = 6f,
|
||||
Precision = 0.01f,
|
||||
};
|
||||
|
||||
private readonly BindableFloat response = new BindableFloat(0)
|
||||
{
|
||||
MinValue = -5f,
|
||||
MaxValue = 5f,
|
||||
Precision = 0.01f,
|
||||
};
|
||||
|
||||
private SpringTimeline timeline = null!;
|
||||
private FollowingCircle followingCircle = null!;
|
||||
private DraggableCircle targetCircle = null!;
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load()
|
||||
{
|
||||
Child = new FillFlowContainer
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Direction = FillDirection.Vertical,
|
||||
Children =
|
||||
[
|
||||
new GridContainer
|
||||
{
|
||||
RelativeSizeAxes = Axes.X,
|
||||
AutoSizeAxes = Axes.Y,
|
||||
ColumnDimensions = [new Dimension(GridSizeMode.Absolute, 300), new Dimension()],
|
||||
RowDimensions = [new Dimension(GridSizeMode.AutoSize)],
|
||||
Padding = new MarginPadding { Vertical = 150 },
|
||||
Content = new Drawable[][]
|
||||
{
|
||||
[
|
||||
new FillFlowContainer
|
||||
{
|
||||
AutoSizeAxes = Axes.Both,
|
||||
Direction = FillDirection.Vertical,
|
||||
Children =
|
||||
[
|
||||
new LabelledSliderBar("Frequency")
|
||||
{
|
||||
Size = new Vector2(300, 30),
|
||||
Current = naturalFrequency,
|
||||
},
|
||||
new LabelledSliderBar("Damping")
|
||||
{
|
||||
Size = new Vector2(300, 30),
|
||||
Current = damping,
|
||||
},
|
||||
new LabelledSliderBar("Response")
|
||||
{
|
||||
Size = new Vector2(300, 30),
|
||||
Current = response,
|
||||
},
|
||||
]
|
||||
},
|
||||
timeline = new SpringTimeline
|
||||
{
|
||||
RelativeSizeAxes = Axes.X,
|
||||
Height = 150,
|
||||
},
|
||||
]
|
||||
}
|
||||
},
|
||||
new Container
|
||||
{
|
||||
RelativeSizeAxes = Axes.X,
|
||||
Height = 300,
|
||||
Children =
|
||||
[
|
||||
targetCircle = new DraggableCircle
|
||||
{
|
||||
Anchor = Anchor.Centre,
|
||||
},
|
||||
followingCircle = new FollowingCircle(targetCircle)
|
||||
{
|
||||
Anchor = Anchor.Centre,
|
||||
Depth = 1,
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
};
|
||||
}
|
||||
|
||||
protected override void LoadComplete()
|
||||
{
|
||||
base.LoadComplete();
|
||||
|
||||
naturalFrequency.BindValueChanged(_ => Scheduler.AddOnce(updateSpring));
|
||||
damping.BindValueChanged(_ => Scheduler.AddOnce(updateSpring));
|
||||
response.BindValueChanged(_ => Scheduler.AddOnce(updateSpring));
|
||||
updateSpring();
|
||||
}
|
||||
|
||||
private void updateSpring()
|
||||
{
|
||||
var springParameters = new SpringParameters
|
||||
{
|
||||
NaturalFrequency = naturalFrequency.Value,
|
||||
Damping = damping.Value,
|
||||
Response = response.Value,
|
||||
};
|
||||
|
||||
followingCircle.SpringParameters = springParameters;
|
||||
|
||||
timeline.SetSpringParameters(springParameters);
|
||||
}
|
||||
|
||||
private partial class LabelledSliderBar : CompositeDrawable
|
||||
{
|
||||
private readonly BasicSliderBar<float> sliderBar;
|
||||
private readonly SpriteText label;
|
||||
private readonly string labelText;
|
||||
|
||||
public Bindable<float> Current
|
||||
{
|
||||
get => sliderBar.Current;
|
||||
set => sliderBar.Current = value;
|
||||
}
|
||||
|
||||
public LabelledSliderBar(string labelText)
|
||||
{
|
||||
this.labelText = labelText;
|
||||
|
||||
InternalChildren =
|
||||
[
|
||||
sliderBar = new BasicSliderBar<float>
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
},
|
||||
label = new SpriteText
|
||||
{
|
||||
Anchor = Anchor.CentreLeft,
|
||||
Origin = Anchor.CentreLeft,
|
||||
Margin = new MarginPadding { Left = 5 },
|
||||
Font = new FontUsage(size: 15f),
|
||||
Colour = Color4.Black
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
protected override void LoadComplete()
|
||||
{
|
||||
base.LoadComplete();
|
||||
|
||||
Current.BindValueChanged(e =>
|
||||
{
|
||||
label.Text = $"{labelText}: {e.NewValue:F2}";
|
||||
}, true);
|
||||
}
|
||||
}
|
||||
|
||||
private partial class SpringTimeline : CompositeDrawable
|
||||
{
|
||||
private const double graph_duration = 3_000;
|
||||
|
||||
private readonly SmoothPath graph;
|
||||
|
||||
private readonly FloatSpring spring = new FloatSpring();
|
||||
|
||||
private readonly LayoutValue drawSizeBacking = new LayoutValue(Invalidation.DrawSize);
|
||||
|
||||
public SpringTimeline()
|
||||
{
|
||||
AddLayout(drawSizeBacking);
|
||||
|
||||
InternalChildren =
|
||||
[
|
||||
new Box
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Alpha = 0.1f
|
||||
},
|
||||
graph = new SmoothPath
|
||||
{
|
||||
PathRadius = 1
|
||||
}
|
||||
];
|
||||
|
||||
for (int i = 0; i <= graph_duration; i += 1000)
|
||||
{
|
||||
AddInternal(new Box
|
||||
{
|
||||
RelativePositionAxes = Axes.X,
|
||||
X = i / (float)graph_duration,
|
||||
RelativeSizeAxes = Axes.Y,
|
||||
Width = 1,
|
||||
Origin = Anchor.TopCentre,
|
||||
Alpha = 0.2f,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
public void SetSpringParameters(SpringParameters parameters)
|
||||
{
|
||||
spring.Parameters = parameters;
|
||||
updateGraph();
|
||||
}
|
||||
|
||||
protected override void Update()
|
||||
{
|
||||
base.Update();
|
||||
|
||||
if (!drawSizeBacking.IsValid)
|
||||
{
|
||||
updateGraph();
|
||||
drawSizeBacking.Validate();
|
||||
}
|
||||
}
|
||||
|
||||
private void updateGraph()
|
||||
{
|
||||
spring.Current = 0;
|
||||
spring.Velocity = 0;
|
||||
spring.PreviousTarget = 0;
|
||||
|
||||
int numSteps = (int)DrawWidth;
|
||||
double timestep = graph_duration / numSteps;
|
||||
|
||||
var vertices = new Vector2[numSteps];
|
||||
|
||||
for (int i = 0; i < numSteps; i++)
|
||||
{
|
||||
vertices[i] = new Vector2(i, (1 - spring.Current) * DrawHeight);
|
||||
|
||||
spring.Update(timestep, 1);
|
||||
}
|
||||
|
||||
graph.Vertices = vertices;
|
||||
|
||||
graph.OriginPosition = graph.PositionInBoundingBox(new Vector2());
|
||||
}
|
||||
}
|
||||
|
||||
private partial class DraggableCircle : Circle
|
||||
{
|
||||
public DraggableCircle()
|
||||
{
|
||||
Size = new Vector2(20);
|
||||
Colour = FrameworkColour.Green;
|
||||
Origin = Anchor.Centre;
|
||||
}
|
||||
|
||||
protected override bool OnHover(HoverEvent e)
|
||||
{
|
||||
Scale = new Vector2(1.2f);
|
||||
|
||||
return base.OnHover(e);
|
||||
}
|
||||
|
||||
protected override void OnHoverLost(HoverLostEvent e)
|
||||
{
|
||||
Scale = new Vector2(1);
|
||||
|
||||
base.OnHoverLost(e);
|
||||
}
|
||||
|
||||
protected override bool OnDragStart(DragStartEvent e) => true;
|
||||
|
||||
protected override void OnDrag(DragEvent e)
|
||||
{
|
||||
Position += e.Delta;
|
||||
}
|
||||
}
|
||||
|
||||
private partial class FollowingCircle : Circle
|
||||
{
|
||||
private readonly Drawable target;
|
||||
|
||||
public FollowingCircle(Drawable target)
|
||||
{
|
||||
this.target = target;
|
||||
Size = new Vector2(30);
|
||||
Colour = FrameworkColour.Yellow;
|
||||
Origin = Anchor.Centre;
|
||||
}
|
||||
|
||||
private readonly Vector2Spring position = new Vector2Spring();
|
||||
|
||||
public SpringParameters SpringParameters
|
||||
{
|
||||
set => position.Parameters = value;
|
||||
}
|
||||
|
||||
protected override void Update()
|
||||
{
|
||||
base.Update();
|
||||
|
||||
Position = position.Update(Time.Elapsed, target.Position);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,102 +0,0 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Graphics.Shapes;
|
||||
using osu.Framework.Graphics.Sprites;
|
||||
using osu.Framework.Graphics.Textures;
|
||||
using osu.Framework.Graphics.UserInterface;
|
||||
using osuTK;
|
||||
using osuTK.Graphics;
|
||||
|
||||
namespace osu.Framework.Tests.Visual.Graphics
|
||||
{
|
||||
public partial class TestSceneFrostedGlass : FrameworkTestScene
|
||||
{
|
||||
[BackgroundDependencyLoader]
|
||||
private void load(TextureStore textures)
|
||||
{
|
||||
Container backgroundCircles;
|
||||
|
||||
Children = new Drawable[]
|
||||
{
|
||||
// Background with animated circles
|
||||
backgroundCircles = new Container
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
},
|
||||
// Frosted glass container overlaying part of the screen
|
||||
// new FrostedGlassContainer
|
||||
// {
|
||||
// RelativeSizeAxes = Axes.Both,
|
||||
// Width = 0.5f,
|
||||
// BlurSigma = new Vector2(10),
|
||||
// Children = new Drawable[]
|
||||
// {
|
||||
// new Box
|
||||
// {
|
||||
// RelativeSizeAxes = Axes.Both,
|
||||
// Colour = new Color4(1, 1, 1, 0.5f),
|
||||
// },
|
||||
// new SpriteText
|
||||
// {
|
||||
// Text = "Frosted Glass Effect",
|
||||
// Anchor = Anchor.Centre,
|
||||
// Origin = Anchor.Centre,
|
||||
// Colour = Color4.Black,
|
||||
// }
|
||||
// }
|
||||
// },
|
||||
new Label("Background"),
|
||||
new Label("FrostedGlassContainer")
|
||||
{
|
||||
Anchor = Anchor.TopRight,
|
||||
Origin = Anchor.TopRight
|
||||
}
|
||||
};
|
||||
|
||||
const float circle_radius = 0.05f;
|
||||
const float spacing = 0.01f;
|
||||
|
||||
for (float xPos = 0; xPos < 1; xPos += circle_radius + spacing)
|
||||
{
|
||||
for (float yPos = 0; yPos < 1; yPos += circle_radius + spacing)
|
||||
{
|
||||
backgroundCircles.Add(new CircularProgress
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Size = new Vector2(circle_radius),
|
||||
RelativePositionAxes = Axes.Both,
|
||||
Position = new Vector2(xPos, yPos),
|
||||
Progress = 1,
|
||||
Colour = Color4.HotPink,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private partial class Label : Container
|
||||
{
|
||||
public Label(string text)
|
||||
{
|
||||
AutoSizeAxes = Axes.Both;
|
||||
Margin = new MarginPadding(10);
|
||||
Children = new Drawable[]
|
||||
{
|
||||
new Box
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Colour = Color4.Black
|
||||
},
|
||||
new SpriteText
|
||||
{
|
||||
Text = text,
|
||||
Margin = new MarginPadding(10)
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
151
osu.Framework/Graphics/Transforms/Spring.cs
Normal file
151
osu.Framework/Graphics/Transforms/Spring.cs
Normal file
@@ -0,0 +1,151 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using System;
|
||||
using osuTK;
|
||||
|
||||
namespace osu.Framework.Graphics.Transforms
|
||||
{
|
||||
public readonly record struct SpringParameters(
|
||||
float NaturalFrequency = 1,
|
||||
float Damping = 1,
|
||||
float Response = 1
|
||||
);
|
||||
|
||||
/// <summary>
|
||||
/// Simulates a value following a target value over time using spring physics.
|
||||
/// See TestSceneSpring for a visualization of the spring parameters.
|
||||
/// </summary>
|
||||
public abstract class Spring<T>
|
||||
where T : struct
|
||||
{
|
||||
/// <summary>
|
||||
/// The current value of the spring.
|
||||
/// </summary>
|
||||
public T Current;
|
||||
|
||||
/// <summary>
|
||||
/// The current velocity of the spring.
|
||||
/// </summary>
|
||||
public T Velocity;
|
||||
|
||||
/// <summary>
|
||||
/// The target value of the previous frame.
|
||||
/// </summary>
|
||||
public T PreviousTarget;
|
||||
|
||||
private SpringParameters parameters;
|
||||
|
||||
public SpringParameters Parameters
|
||||
{
|
||||
get => parameters;
|
||||
set
|
||||
{
|
||||
parameters = value;
|
||||
|
||||
k1 = Damping / (MathF.PI * NaturalFrequency);
|
||||
k2 = 1 / ((2 * MathF.PI * NaturalFrequency) * (2 * MathF.PI * NaturalFrequency));
|
||||
k3 = Response * Damping / (2 * MathF.PI * NaturalFrequency);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Controls the overall movement speed of the spring and the frequency (in hertz) that the spring will tend to vibrate at.
|
||||
/// </summary>
|
||||
public float NaturalFrequency
|
||||
{
|
||||
get => Parameters.NaturalFrequency;
|
||||
set => Parameters = Parameters with { NaturalFrequency = value };
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Rate at which the spring looses energy over time.
|
||||
/// If the value is 0, the spring will vibrate indefinitely.
|
||||
/// If the value is between 0 and 1, the vibration will settle over time.
|
||||
/// If the value is greater than or equal to 1 the spring will not vibrate, and will approach the target value at decreasing speeds as damping is increased.
|
||||
/// </summary>
|
||||
public float Damping
|
||||
{
|
||||
get => Parameters.Damping;
|
||||
set => Parameters = Parameters with { Damping = value };
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Controls the initial response to target value changes.
|
||||
/// If the value is 0, the system will take time to begin moving towards the target value.
|
||||
/// If the value is positive, the spring will react immediately to value changes.
|
||||
/// If the value is negative, the spring will anticipate value changes by moving in the opposite direction at first.
|
||||
/// If the value is greater than 1, the spring will overshoot the target value before it settles down.
|
||||
/// </summary>
|
||||
public float Response
|
||||
{
|
||||
get => Parameters.Response;
|
||||
set => Parameters = Parameters with { Response = value };
|
||||
}
|
||||
|
||||
private float k1, k2, k3;
|
||||
|
||||
protected Spring(T initialValue = default, float naturalFrequency = 1, float damping = 1, float response = 0)
|
||||
{
|
||||
Current = initialValue;
|
||||
PreviousTarget = initialValue;
|
||||
|
||||
Parameters = new SpringParameters
|
||||
{
|
||||
NaturalFrequency = naturalFrequency,
|
||||
Damping = damping,
|
||||
Response = response,
|
||||
};
|
||||
}
|
||||
|
||||
protected abstract T GetTargetVelocity(T target, T previousTarget, float dt);
|
||||
|
||||
public T Update(double elapsed, T target, T? targetVelocity = null)
|
||||
{
|
||||
float dt = (float)(elapsed / 1000);
|
||||
|
||||
if (targetVelocity == null)
|
||||
{
|
||||
targetVelocity = GetTargetVelocity(target, PreviousTarget, dt);
|
||||
PreviousTarget = target;
|
||||
}
|
||||
|
||||
return ComputeNextValue(dt, target, targetVelocity.Value);
|
||||
}
|
||||
|
||||
protected abstract T ComputeNextValue(float dt, T target, T targetVelocity);
|
||||
|
||||
protected void ComputeSingleValue(float dt, ref float current, ref float velocity, float target, float targetVelocity)
|
||||
{
|
||||
float k2Stable = MathF.Max(MathF.Max(k2, dt * dt / 2 + dt * k1 / 2), dt * k1);
|
||||
|
||||
current += dt * velocity;
|
||||
velocity += (dt * (target + k3 * targetVelocity - current - k1 * velocity)) / k2Stable;
|
||||
}
|
||||
}
|
||||
|
||||
public class FloatSpring : Spring<float>
|
||||
{
|
||||
protected override float GetTargetVelocity(float target, float previousTarget, float dt) => (target - previousTarget) / dt;
|
||||
|
||||
protected override float ComputeNextValue(float dt, float target, float targetVelocity)
|
||||
{
|
||||
ComputeSingleValue(dt, ref Current, ref Velocity, target, targetVelocity);
|
||||
|
||||
return Current;
|
||||
}
|
||||
}
|
||||
|
||||
public class Vector2Spring : Spring<Vector2>
|
||||
{
|
||||
protected override Vector2 GetTargetVelocity(Vector2 target, Vector2 previousTarget, float dt) => (target - previousTarget) / dt;
|
||||
|
||||
protected override Vector2 ComputeNextValue(float dt, Vector2 target, Vector2 targetVelocity)
|
||||
{
|
||||
ComputeSingleValue(dt, ref Current.X, ref Velocity.X, target.X, targetVelocity.X);
|
||||
ComputeSingleValue(dt, ref Current.Y, ref Velocity.Y, target.Y, targetVelocity.Y);
|
||||
|
||||
return Current;
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user