同步更新

This commit is contained in:
LA
2025-12-06 17:23:19 +08:00
parent a2ef94d6e5
commit 559b9cef88
3 changed files with 478 additions and 102 deletions

View 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);
}
}
}
}

View File

@@ -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)
}
};
}
}
}
}

View 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;
}
}
}