A game framework written with osu! in mind.
1// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
2// See the LICENCE file in the repository root for full licence text.
3
4using System.Collections.Generic;
5using System.Linq;
6using osu.Framework.Bindables;
7using osu.Framework.Extensions.Color4Extensions;
8using osu.Framework.Graphics;
9using osu.Framework.Graphics.Containers;
10using osu.Framework.Graphics.Lines;
11using osu.Framework.Graphics.Shapes;
12using osu.Framework.Graphics.Sprites;
13using osu.Framework.Graphics.Transforms;
14using osu.Framework.Input.Events;
15using osu.Framework.Utils;
16using osuTK;
17using osuTK.Graphics;
18
19namespace osu.Framework.Tests.Visual.Drawables
20{
21 public class TestSceneCustomEasingCurve : FrameworkTestScene
22 {
23 public TestSceneCustomEasingCurve()
24 {
25 Add(new CurveVisualiser
26 {
27 Anchor = Anchor.Centre,
28 Origin = Anchor.Centre,
29 Size = new Vector2(400),
30 });
31 }
32
33 private class CurveVisualiser : CompositeDrawable
34 {
35 private readonly BindableList<Vector2> easingVertices = new BindableList<Vector2>();
36
37 private readonly SmoothPath path;
38 private readonly Container<ControlPointVisualiser> controlPointContainer;
39 private readonly SpriteIcon sideTracker;
40 private readonly Box verticalTracker;
41 private readonly Box horizontalTracker;
42
43 private readonly CustomEasingFunction easingFunction;
44
45 public CurveVisualiser()
46 {
47 easingFunction = new CustomEasingFunction { EasingVertices = { BindTarget = easingVertices } };
48
49 Container gridContainer;
50
51 InternalChildren = new Drawable[]
52 {
53 new Container
54 {
55 RelativeSizeAxes = Axes.Both,
56 Masking = true,
57 BorderColour = Color4.White,
58 BorderThickness = 2,
59 Child = new Box
60 {
61 RelativeSizeAxes = Axes.Both,
62 Alpha = 0,
63 AlwaysPresent = true
64 }
65 },
66 gridContainer = new Container { RelativeSizeAxes = Axes.Both },
67 path = new SmoothPath
68 {
69 PathRadius = 1
70 },
71 controlPointContainer = new Container<ControlPointVisualiser> { RelativeSizeAxes = Axes.Both },
72 sideTracker = new SpriteIcon
73 {
74 Anchor = Anchor.TopRight,
75 Origin = Anchor.BottomCentre,
76 RelativePositionAxes = Axes.Y,
77 Size = new Vector2(10),
78 X = 2,
79 Colour = Color4.SkyBlue,
80 Rotation = 90,
81 Icon = FontAwesome.Solid.MapMarker,
82 },
83 verticalTracker = new Box
84 {
85 Origin = Anchor.CentreLeft,
86 RelativeSizeAxes = Axes.X,
87 RelativePositionAxes = Axes.Y,
88 Height = 1,
89 Colour = Color4.SkyBlue
90 },
91 horizontalTracker = new Box
92 {
93 Origin = Anchor.TopCentre,
94 RelativeSizeAxes = Axes.Y,
95 RelativePositionAxes = Axes.X,
96 Width = 1,
97 Colour = Color4.SkyBlue
98 }
99 };
100
101 for (int i = 0; i <= 10; i++)
102 {
103 gridContainer.Add(new Box
104 {
105 Origin = Anchor.CentreLeft,
106 RelativeSizeAxes = Axes.X,
107 RelativePositionAxes = Axes.Y,
108 Height = 2,
109 Y = 0.1f * i,
110 Colour = Color4.White.Opacity(0.1f)
111 });
112
113 gridContainer.Add(new Box
114 {
115 Origin = Anchor.TopCentre,
116 RelativeSizeAxes = Axes.Y,
117 RelativePositionAxes = Axes.X,
118 Width = 2,
119 X = 0.1f * i,
120 Colour = Color4.White.Opacity(0.1f)
121 });
122 }
123
124 controlPointContainer.Add(new ControlPointVisualiser
125 {
126 PointPosition = { Value = new Vector2(100, 100) }
127 });
128 }
129
130 protected override void LoadComplete()
131 {
132 base.LoadComplete();
133
134 sideTracker.MoveToY(1)
135 .Then().MoveToY(0, 2000, easingFunction)
136 .Then().Delay(200)
137 .Loop();
138
139 verticalTracker.MoveToY(1)
140 .Then().MoveToY(0, 2000, easingFunction)
141 .Then().Delay(200)
142 .Loop();
143
144 horizontalTracker.MoveToX(0)
145 .Then().MoveToX(1, 2000)
146 .Then().Delay(200)
147 .Loop();
148 }
149
150 protected override void Update()
151 {
152 base.Update();
153
154 ControlPointVisualiser[] ordered = controlPointContainer.OrderBy(p => p.PointPosition.Value.X).ToArray();
155
156 for (int i = 0; i < ordered.Length; i++)
157 {
158 ordered[i].Last = i > 0 ? ordered[i - 1] : null;
159 ordered[i].Next = i < ordered.Length - 1 ? ordered[i + 1] : null;
160 }
161
162 var vectorPath = new List<Vector2> { new Vector2(0, DrawHeight) };
163 vectorPath.AddRange(ordered.Select(p => p.PointPosition.Value));
164 vectorPath.Add(new Vector2(DrawWidth, 0));
165
166 var bezierPath = PathApproximator.ApproximateBezier(vectorPath.ToArray());
167 path.Vertices = bezierPath;
168 path.Position = -path.PositionInBoundingBox(Vector2.Zero);
169
170 easingVertices.Clear();
171 easingVertices.AddRange(bezierPath.Select(p => Vector2.Divide(p, DrawSize)).Select(p => new Vector2(p.X, 1 - p.Y)));
172 }
173
174 protected override bool OnMouseDown(MouseDownEvent e)
175 {
176 controlPointContainer.Add(new ControlPointVisualiser
177 {
178 PointPosition = { Value = ToLocalSpace(e.ScreenSpaceMousePosition) }
179 });
180
181 return true;
182 }
183 }
184
185 private class ControlPointVisualiser : CompositeDrawable
186 {
187 public readonly Bindable<Vector2> PointPosition = new Bindable<Vector2>();
188
189 public ControlPointVisualiser Last;
190 public ControlPointVisualiser Next;
191
192 private readonly SmoothPath path;
193
194 public ControlPointVisualiser()
195 {
196 RelativeSizeAxes = Axes.Both;
197
198 InternalChildren = new Drawable[]
199 {
200 path = new SmoothPath
201 {
202 PathRadius = 1,
203 Colour = Color4.Yellow.Opacity(0.5f)
204 },
205 new PointHandle
206 {
207 PointPosition = { BindTarget = PointPosition }
208 }
209 };
210 }
211
212 protected override void Update()
213 {
214 base.Update();
215
216 path.ClearVertices();
217
218 path.AddVertex(Last?.PointPosition.Value ?? new Vector2(0, DrawHeight));
219 path.AddVertex(PointPosition.Value);
220
221 if (Next == null)
222 path.AddVertex(new Vector2(DrawWidth, 0));
223
224 path.Position = -path.PositionInBoundingBox(Vector2.Zero);
225 }
226 }
227
228 private class PointHandle : Circle
229 {
230 public readonly Bindable<Vector2> PointPosition = new Bindable<Vector2>();
231
232 public PointHandle()
233 {
234 Origin = Anchor.Centre;
235 Size = new Vector2(10);
236
237 Colour = Color4.Yellow;
238 Alpha = 0.5f;
239 }
240
241 protected override void Update()
242 {
243 base.Update();
244
245 Position = PointPosition.Value;
246 }
247
248 private bool isDragging;
249
250 protected override bool OnHover(HoverEvent e)
251 {
252 updateColour();
253 return true;
254 }
255
256 protected override void OnHoverLost(HoverLostEvent e) => updateColour();
257
258 protected override bool OnMouseDown(MouseDownEvent e)
259 {
260 isDragging = true;
261 return true;
262 }
263
264 protected override void OnMouseUp(MouseUpEvent e)
265 {
266 isDragging = false;
267 updateColour();
268 }
269
270 protected override bool OnDragStart(DragStartEvent e) => true;
271
272 protected override void OnDrag(DragEvent e) => PointPosition.Value += e.Delta;
273
274 private void updateColour() => Alpha = IsHovered || isDragging ? 1f : 0.5f;
275 }
276
277 private class CustomEasingFunction : IEasingFunction
278 {
279 public readonly BindableList<Vector2> EasingVertices = new BindableList<Vector2>();
280
281 public double ApplyEasing(double time)
282 {
283 for (int i = 0; i < EasingVertices.Count; i++)
284 {
285 if (EasingVertices[i].X < time)
286 continue;
287
288 Vector2 last = EasingVertices[i - 1];
289 Vector2 next = EasingVertices[i];
290
291 return Interpolation.ValueAt(time, last.Y, next.Y, last.X, next.X);
292 }
293
294 return 0;
295 }
296 }
297 }
298}