Merge branch 'master' into editor-song-end

This commit is contained in:
Bartłomiej Dach
2021-03-17 18:18:19 +01:00
2295 changed files with 98012 additions and 27524 deletions

View File

@ -12,14 +12,14 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline
{
public class CentreMarker : CompositeDrawable
{
private const float triangle_width = 20;
private const float triangle_width = 15;
private const float triangle_height = 10;
private const float bar_width = 2;
public CentreMarker()
{
RelativeSizeAxes = Axes.Y;
Size = new Vector2(20, 1);
Size = new Vector2(triangle_width, 1);
Anchor = Anchor.Centre;
Origin = Anchor.Centre;
@ -39,6 +39,12 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline
Origin = Anchor.BottomCentre,
Size = new Vector2(triangle_width, triangle_height),
Scale = new Vector2(1, -1)
},
new Triangle
{
Anchor = Anchor.BottomCentre,
Origin = Anchor.BottomCentre,
Size = new Vector2(triangle_width, triangle_height),
}
};
}
@ -46,7 +52,7 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline
[BackgroundDependencyLoader]
private void load(OsuColour colours)
{
Colour = colours.Red;
Colour = colours.RedDark;
}
}
}

View File

@ -0,0 +1,67 @@
// 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.Shapes;
using osu.Game.Beatmaps.ControlPoints;
using osu.Game.Graphics;
using osu.Game.Graphics.Sprites;
using osuTK.Graphics;
namespace osu.Game.Screens.Edit.Compose.Components.Timeline
{
public class DifficultyPointPiece : CompositeDrawable
{
private readonly DifficultyControlPoint difficultyPoint;
private OsuSpriteText speedMultiplierText;
private readonly BindableNumber<double> speedMultiplier;
public DifficultyPointPiece(DifficultyControlPoint difficultyPoint)
{
this.difficultyPoint = difficultyPoint;
speedMultiplier = difficultyPoint.SpeedMultiplierBindable.GetBoundCopy();
}
[BackgroundDependencyLoader]
private void load(OsuColour colours)
{
RelativeSizeAxes = Axes.Y;
AutoSizeAxes = Axes.X;
Color4 colour = difficultyPoint.GetRepresentingColour(colours);
InternalChildren = new Drawable[]
{
new Box
{
Colour = colour,
Width = 2,
RelativeSizeAxes = Axes.Y,
},
new Container
{
AutoSizeAxes = Axes.Both,
Children = new Drawable[]
{
new Box
{
Colour = colour,
RelativeSizeAxes = Axes.Both,
},
speedMultiplierText = new OsuSpriteText
{
Font = OsuFont.Default.With(weight: FontWeight.Bold),
Colour = Color4.White,
}
}
},
};
speedMultiplier.BindValueChanged(multiplier => speedMultiplierText.Text = $"{multiplier.NewValue:n2}x", true);
}
}
}

View File

@ -0,0 +1,85 @@
// 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.Extensions.Color4Extensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Colour;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes;
using osu.Game.Beatmaps.ControlPoints;
using osu.Game.Graphics;
using osu.Game.Graphics.Sprites;
using osuTK.Graphics;
namespace osu.Game.Screens.Edit.Compose.Components.Timeline
{
public class SamplePointPiece : CompositeDrawable
{
private readonly SampleControlPoint samplePoint;
private readonly Bindable<string> bank;
private readonly BindableNumber<int> volume;
private OsuSpriteText text;
private Box volumeBox;
public SamplePointPiece(SampleControlPoint samplePoint)
{
this.samplePoint = samplePoint;
volume = samplePoint.SampleVolumeBindable.GetBoundCopy();
bank = samplePoint.SampleBankBindable.GetBoundCopy();
}
[BackgroundDependencyLoader]
private void load(OsuColour colours)
{
Origin = Anchor.TopLeft;
Anchor = Anchor.TopLeft;
AutoSizeAxes = Axes.X;
RelativeSizeAxes = Axes.Y;
Color4 colour = samplePoint.GetRepresentingColour(colours);
InternalChildren = new Drawable[]
{
new Container
{
RelativeSizeAxes = Axes.Y,
Width = 20,
Children = new Drawable[]
{
volumeBox = new Box
{
X = 2,
Anchor = Anchor.BottomLeft,
Origin = Anchor.BottomLeft,
Colour = ColourInfo.GradientVertical(colour, Color4.Black),
RelativeSizeAxes = Axes.Both,
},
new Box
{
Colour = colour.Lighten(0.2f),
Width = 2,
RelativeSizeAxes = Axes.Y,
},
}
},
text = new OsuSpriteText
{
X = 2,
Y = -5,
Anchor = Anchor.BottomLeft,
Alpha = 0.9f,
Rotation = -90,
Font = OsuFont.Default.With(weight: FontWeight.SemiBold)
}
};
volume.BindValueChanged(volume => volumeBox.Height = volume.NewValue / 100f, true);
bank.BindValueChanged(bank => text.Text = bank.NewValue, true);
}
}
}

View File

@ -8,65 +8,30 @@ using osu.Framework.Bindables;
using osu.Framework.Extensions.Color4Extensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Audio;
using osu.Framework.Graphics.Containers;
using osu.Framework.Input.Events;
using osu.Framework.Timing;
using osu.Game.Beatmaps;
using osu.Game.Configuration;
using osu.Game.Graphics;
using osu.Game.Rulesets.Edit;
using osuTK;
namespace osu.Game.Screens.Edit.Compose.Components.Timeline
{
[Cached(typeof(IDistanceSnapProvider))]
[Cached(typeof(IPositionSnapProvider))]
[Cached]
public class Timeline : ZoomableScrollContainer, IDistanceSnapProvider
public class Timeline : ZoomableScrollContainer, IPositionSnapProvider
{
public readonly Bindable<bool> WaveformVisible = new Bindable<bool>();
public readonly Bindable<bool> ControlPointsVisible = new Bindable<bool>();
public readonly Bindable<bool> TicksVisible = new Bindable<bool>();
public readonly IBindable<WorkingBeatmap> Beatmap = new Bindable<WorkingBeatmap>();
[Resolved]
private IAdjustableClock adjustableClock { get; set; }
public Timeline()
{
ZoomDuration = 200;
ZoomEasing = Easing.OutQuint;
ScrollbarVisible = false;
}
private WaveformGraph waveform;
[BackgroundDependencyLoader]
private void load(IBindable<WorkingBeatmap> beatmap, OsuColour colours)
{
Add(waveform = new WaveformGraph
{
RelativeSizeAxes = Axes.Both,
Colour = colours.Blue.Opacity(0.2f),
LowColour = colours.BlueLighter,
MidColour = colours.BlueDark,
HighColour = colours.BlueDarker,
Depth = float.MaxValue
});
// We don't want the centre marker to scroll
AddInternal(new CentreMarker());
WaveformVisible.ValueChanged += visible => waveform.FadeTo(visible.NewValue ? 1 : 0, 200, Easing.OutQuint);
Beatmap.BindTo(beatmap);
Beatmap.BindValueChanged(b =>
{
waveform.Waveform = b.NewValue.Waveform;
track = b.NewValue.Track;
MinZoom = getZoomLevelForVisibleMilliseconds(10000);
MaxZoom = getZoomLevelForVisibleMilliseconds(500);
Zoom = getZoomLevelForVisibleMilliseconds(2000);
}, true);
}
private float getZoomLevelForVisibleMilliseconds(double milliseconds) => (float)(track.Length / milliseconds);
private EditorClock editorClock { get; set; }
/// <summary>
/// The timeline's scroll position in the last frame.
@ -90,6 +55,77 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline
private Track track;
public Timeline()
{
ZoomDuration = 200;
ZoomEasing = Easing.OutQuint;
ScrollbarVisible = false;
}
private WaveformGraph waveform;
private TimelineTickDisplay ticks;
private TimelineControlPointDisplay controlPoints;
private Bindable<float> waveformOpacity;
[BackgroundDependencyLoader]
private void load(IBindable<WorkingBeatmap> beatmap, OsuColour colours, OsuConfigManager config)
{
AddRange(new Drawable[]
{
new Container
{
RelativeSizeAxes = Axes.Both,
Depth = float.MaxValue,
Children = new Drawable[]
{
waveform = new WaveformGraph
{
RelativeSizeAxes = Axes.Both,
BaseColour = colours.Blue.Opacity(0.2f),
LowColour = colours.BlueLighter,
MidColour = colours.BlueDark,
HighColour = colours.BlueDarker,
},
ticks = new TimelineTickDisplay(),
controlPoints = new TimelineControlPointDisplay(),
}
},
});
// We don't want the centre marker to scroll
AddInternal(new CentreMarker { Depth = float.MaxValue });
waveformOpacity = config.GetBindable<float>(OsuSetting.EditorWaveformOpacity);
waveformOpacity.BindValueChanged(_ => updateWaveformOpacity(), true);
WaveformVisible.ValueChanged += _ => updateWaveformOpacity();
ControlPointsVisible.ValueChanged += visible => controlPoints.FadeTo(visible.NewValue ? 1 : 0, 200, Easing.OutQuint);
TicksVisible.ValueChanged += visible => ticks.FadeTo(visible.NewValue ? 1 : 0, 200, Easing.OutQuint);
Beatmap.BindTo(beatmap);
Beatmap.BindValueChanged(b =>
{
waveform.Waveform = b.NewValue.Waveform;
track = b.NewValue.Track;
// todo: i don't think this is safe, the track may not be loaded yet.
if (track.Length > 0)
{
MaxZoom = getZoomLevelForVisibleMilliseconds(500);
MinZoom = getZoomLevelForVisibleMilliseconds(10000);
Zoom = getZoomLevelForVisibleMilliseconds(2000);
}
}, true);
}
private void updateWaveformOpacity() =>
waveform.FadeTo(WaveformVisible.Value ? waveformOpacity.Value : 0, 200, Easing.OutQuint);
private float getZoomLevelForVisibleMilliseconds(double milliseconds) => Math.Max(1, (float)(track.Length / milliseconds));
protected override void Update()
{
base.Update();
@ -98,31 +134,42 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline
Content.Margin = new MarginPadding { Horizontal = DrawWidth / 2 };
// This needs to happen after transforms are updated, but before the scroll position is updated in base.UpdateAfterChildren
if (adjustableClock.IsRunning)
if (editorClock.IsRunning)
scrollToTrackTime();
}
protected override bool OnScroll(ScrollEvent e)
{
// if this is not a precision scroll event, let the editor handle the seek itself (for snapping support)
if (!e.AltPressed && !e.IsPrecise)
return false;
return base.OnScroll(e);
}
protected override void UpdateAfterChildren()
{
base.UpdateAfterChildren();
if (handlingDragInput)
seekTrackToCurrent();
else if (!adjustableClock.IsRunning)
else if (!editorClock.IsRunning)
{
// The track isn't running. There are two cases we have to be wary of:
// 1) The user flick-drags on this timeline: We want the track to follow us
// 2) The user changes the track time through some other means (scrolling in the editor or overview timeline): We want to follow the track time
// The track isn't running. There are three cases we have to be wary of:
// 1) The user flick-drags on this timeline and we are applying an interpolated seek on the clock, until interrupted by 2 or 3.
// 2) The user changes the track time through some other means (scrolling in the editor or overview timeline; clicking a hitobject etc.). We want the timeline to track the clock's time.
// 3) An ongoing seek transform is running from an external seek. We want the timeline to track the clock's time.
// The simplest way to cover both cases is by checking whether the scroll position has changed and the audio hasn't been changed externally
if (Current != lastScrollPosition && adjustableClock.CurrentTime == lastTrackTime)
// The simplest way to cover the first two cases is by checking whether the scroll position has changed and the audio hasn't been changed externally
// Checking IsSeeking covers the third case, where the transform may not have been applied yet.
if (Current != lastScrollPosition && editorClock.CurrentTime == lastTrackTime && !editorClock.IsSeeking)
seekTrackToCurrent();
else
scrollToTrackTime();
}
lastScrollPosition = Current;
lastTrackTime = adjustableClock.CurrentTime;
lastTrackTime = editorClock.CurrentTime;
}
private void seekTrackToCurrent()
@ -131,16 +178,20 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline
return;
double target = Current / Content.DrawWidth * track.Length;
adjustableClock.Seek(Math.Min(track.Length, target));
editorClock.Seek(Math.Min(track.Length, target));
}
private void scrollToTrackTime()
{
if (!track.IsLoaded)
if (!track.IsLoaded || track.Length == 0)
return;
ScrollTo((float)(adjustableClock.CurrentTime / track.Length) * Content.DrawWidth, false);
// covers the case where the user starts playback after a drag is in progress.
// we want to ensure the clock is always stopped during drags to avoid weird audio playback.
if (handlingDragInput)
editorClock.Stop();
ScrollTo((float)(editorClock.CurrentTime / track.Length) * Content.DrawWidth, false);
}
protected override bool OnMouseDown(MouseDownEvent e)
@ -163,15 +214,15 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline
private void beginUserDrag()
{
handlingDragInput = true;
trackWasPlaying = adjustableClock.IsRunning;
adjustableClock.Stop();
trackWasPlaying = editorClock.IsRunning;
editorClock.Stop();
}
private void endUserDrag()
{
handlingDragInput = false;
if (trackWasPlaying)
adjustableClock.Start();
editorClock.Start();
}
[Resolved]
@ -180,11 +231,16 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline
[Resolved]
private IBeatSnapProvider beatSnapProvider { get; set; }
public double GetTimeFromScreenSpacePosition(Vector2 position)
=> getTimeFromPosition(Content.ToLocalSpace(position));
/// <summary>
/// The total amount of time visible on the timeline.
/// </summary>
public double VisibleRange => track.Length / Zoom;
public (Vector2 position, double time) GetSnappedPosition(Vector2 position, double time) =>
(position, beatSnapProvider.SnapTime(getTimeFromPosition(position)));
public SnapResult SnapScreenSpacePositionToValidPosition(Vector2 screenSpacePosition) =>
new SnapResult(screenSpacePosition, null);
public SnapResult SnapScreenSpacePositionToValidTime(Vector2 screenSpacePosition) =>
new SnapResult(screenSpacePosition, beatSnapProvider.SnapTime(getTimeFromPosition(Content.ToLocalSpace(screenSpacePosition))));
private double getTimeFromPosition(Vector2 localPosition) =>
(localPosition.X / Content.DrawWidth) * track.Length;

View File

@ -14,9 +14,9 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline
{
public class TimelineArea : Container
{
private readonly Timeline timeline = new Timeline { RelativeSizeAxes = Axes.Both };
public readonly Timeline Timeline = new Timeline { RelativeSizeAxes = Axes.Both };
protected override Container<Drawable> Content => timeline;
protected override Container<Drawable> Content => Timeline;
[BackgroundDependencyLoader]
private void load()
@ -25,6 +25,8 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline
CornerRadius = 5;
OsuCheckbox waveformCheckbox;
OsuCheckbox controlPointsCheckbox;
OsuCheckbox ticksCheckbox;
InternalChildren = new Drawable[]
{
@ -57,12 +59,26 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline
Origin = Anchor.CentreLeft,
AutoSizeAxes = Axes.Y,
Width = 160,
Padding = new MarginPadding { Horizontal = 15 },
Padding = new MarginPadding { Horizontal = 10 },
Direction = FillDirection.Vertical,
Spacing = new Vector2(0, 4),
Children = new[]
{
waveformCheckbox = new OsuCheckbox { LabelText = "Waveform" }
waveformCheckbox = new OsuCheckbox
{
LabelText = "Waveform",
Current = { Value = true },
},
controlPointsCheckbox = new OsuCheckbox
{
LabelText = "Control Points",
Current = { Value = true },
},
ticksCheckbox = new OsuCheckbox
{
LabelText = "Ticks",
Current = { Value = true },
}
}
}
}
@ -107,7 +123,7 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline
}
}
},
timeline
Timeline
},
},
ColumnDimensions = new[]
@ -119,11 +135,11 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline
}
};
waveformCheckbox.Current.Value = true;
timeline.WaveformVisible.BindTo(waveformCheckbox.Current);
Timeline.WaveformVisible.BindTo(waveformCheckbox.Current);
Timeline.ControlPointsVisible.BindTo(controlPointsCheckbox.Current);
Timeline.TicksVisible.BindTo(ticksCheckbox.Current);
}
private void changeZoom(float change) => timeline.Zoom += change;
private void changeZoom(float change) => Timeline.Zoom += change;
}
}

View File

@ -9,10 +9,11 @@ using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Primitives;
using osu.Framework.Graphics.Shapes;
using osu.Framework.Input.Events;
using osu.Framework.Utils;
using osu.Game.Graphics;
using osu.Game.Rulesets.Edit;
using osu.Game.Rulesets.Objects;
using osu.Game.Screens.Edit.Components.Timelines.Summary.Parts;
using osuTK;
using osuTK.Graphics;
namespace osu.Game.Screens.Edit.Compose.Components.Timeline
@ -25,21 +26,25 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline
[Resolved]
private EditorBeatmap beatmap { get; set; }
[Resolved]
private OsuColour colours { get; set; }
private DragEvent lastDragEvent;
private Bindable<HitObject> placement;
private SelectionBlueprint placementBlueprint;
public TimelineBlueprintContainer()
private readonly Box backgroundBox;
public TimelineBlueprintContainer(HitObjectComposer composer)
: base(composer)
{
RelativeSizeAxes = Axes.Both;
Anchor = Anchor.Centre;
Origin = Anchor.Centre;
Height = 0.4f;
Height = 0.6f;
AddInternal(new Box
AddInternal(backgroundBox = new Box
{
Colour = Color4.Black,
RelativeSizeAxes = Axes.Both,
@ -78,6 +83,18 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline
protected override Container<SelectionBlueprint> CreateSelectionBlueprintContainer() => new TimelineSelectionBlueprintContainer { RelativeSizeAxes = Axes.Both };
protected override bool OnHover(HoverEvent e)
{
backgroundBox.FadeColour(colours.BlueLighter, 120, Easing.OutQuint);
return base.OnHover(e);
}
protected override void OnHoverLost(HoverLostEvent e)
{
backgroundBox.FadeColour(Color4.Black, 600, Easing.OutQuint);
base.OnHoverLost(e);
}
protected override void OnDrag(DragEvent e)
{
handleScrollViaDrag(e);
@ -97,6 +114,12 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline
if (lastDragEvent != null)
OnDrag(lastDragEvent);
if (Composer != null && timeline != null)
{
Composer.Playfield.PastLifetimeExtension = timeline.VisibleRange / 2;
Composer.Playfield.FutureLifetimeExtension = timeline.VisibleRange / 2;
}
base.Update();
}
@ -137,8 +160,14 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline
private class TimelineDragBox : DragBox
{
private Vector2 lastMouseDown;
private float localMouseDown;
// the following values hold the start and end X positions of the drag box in the timeline's local space,
// but with zoom unapplied in order to be able to compensate for positional changes
// while the timeline is being zoomed in/out.
private float? selectionStart;
private float selectionEnd;
[Resolved]
private Timeline timeline { get; set; }
public TimelineDragBox(Action<RectangleF> performSelect)
: base(performSelect)
@ -153,21 +182,34 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline
public override bool HandleDrag(MouseButtonEvent e)
{
// store the original position of the mouse down, as we may be scrolled during selection.
if (lastMouseDown != e.ScreenSpaceMouseDownPosition)
{
lastMouseDown = e.ScreenSpaceMouseDownPosition;
localMouseDown = e.MouseDownPosition.X;
}
selectionStart ??= e.MouseDownPosition.X / timeline.CurrentZoom;
float selection1 = localMouseDown;
float selection2 = e.MousePosition.X;
// only calculate end when a transition is not in progress to avoid bouncing.
if (Precision.AlmostEquals(timeline.CurrentZoom, timeline.Zoom))
selectionEnd = e.MousePosition.X / timeline.CurrentZoom;
Box.X = Math.Min(selection1, selection2);
Box.Width = Math.Abs(selection1 - selection2);
updateDragBoxPosition();
return true;
}
private void updateDragBoxPosition()
{
if (selectionStart == null)
return;
float rescaledStart = selectionStart.Value * timeline.CurrentZoom;
float rescaledEnd = selectionEnd * timeline.CurrentZoom;
Box.X = Math.Min(rescaledStart, rescaledEnd);
Box.Width = Math.Abs(rescaledStart - rescaledEnd);
PerformSelection?.Invoke(Box.ScreenSpaceDrawQuad.AABBFloat);
return true;
}
public override void Hide()
{
base.Hide();
selectionStart = null;
}
}
@ -177,7 +219,7 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline
public TimelineSelectionBlueprintContainer()
{
AddInternal(new TimelinePart<SelectionBlueprint>(Content = new Container<SelectionBlueprint> { RelativeSizeAxes = Axes.Both }) { RelativeSizeAxes = Axes.Both });
AddInternal(new TimelinePart<SelectionBlueprint>(Content = new HitObjectOrderedSelectionContainer { RelativeSizeAxes = Axes.Both }) { RelativeSizeAxes = Axes.Both });
}
}
}

View File

@ -0,0 +1,57 @@
// 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.Collections.Specialized;
using System.Linq;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Game.Beatmaps.ControlPoints;
using osu.Game.Screens.Edit.Components.Timelines.Summary.Parts;
namespace osu.Game.Screens.Edit.Compose.Components.Timeline
{
/// <summary>
/// The part of the timeline that displays the control points.
/// </summary>
public class TimelineControlPointDisplay : TimelinePart<TimelineControlPointGroup>
{
private readonly IBindableList<ControlPointGroup> controlPointGroups = new BindableList<ControlPointGroup>();
public TimelineControlPointDisplay()
{
RelativeSizeAxes = Axes.Both;
}
protected override void LoadBeatmap(EditorBeatmap beatmap)
{
base.LoadBeatmap(beatmap);
controlPointGroups.UnbindAll();
controlPointGroups.BindTo(beatmap.ControlPointInfo.Groups);
controlPointGroups.BindCollectionChanged((sender, args) =>
{
switch (args.Action)
{
case NotifyCollectionChangedAction.Reset:
Clear();
break;
case NotifyCollectionChangedAction.Add:
foreach (var group in args.NewItems.OfType<ControlPointGroup>())
Add(new TimelineControlPointGroup(group));
break;
case NotifyCollectionChangedAction.Remove:
foreach (var group in args.OldItems.OfType<ControlPointGroup>())
{
var matching = Children.SingleOrDefault(gv => gv.Group == group);
matching?.Expire();
}
break;
}
}, true);
}
}
}

View File

@ -0,0 +1,62 @@
// 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.Game.Beatmaps.ControlPoints;
using osu.Game.Graphics;
namespace osu.Game.Screens.Edit.Compose.Components.Timeline
{
public class TimelineControlPointGroup : CompositeDrawable
{
public readonly ControlPointGroup Group;
private readonly IBindableList<ControlPoint> controlPoints = new BindableList<ControlPoint>();
[Resolved]
private OsuColour colours { get; set; }
public TimelineControlPointGroup(ControlPointGroup group)
{
Group = group;
RelativePositionAxes = Axes.X;
RelativeSizeAxes = Axes.Y;
AutoSizeAxes = Axes.X;
X = (float)group.Time;
}
protected override void LoadComplete()
{
base.LoadComplete();
controlPoints.BindTo(Group.ControlPoints);
controlPoints.BindCollectionChanged((_, __) =>
{
ClearInternal();
foreach (var point in controlPoints)
{
switch (point)
{
case DifficultyControlPoint difficultyPoint:
AddInternal(new DifficultyPointPiece(difficultyPoint) { Depth = -2 });
break;
case TimingControlPoint timingPoint:
AddInternal(new TimingPointPiece(timingPoint));
break;
case SampleControlPoint samplePoint:
AddInternal(new SamplePointPiece(samplePoint) { Depth = -1 });
break;
}
}
}, true);
}
}
}

View File

@ -7,15 +7,20 @@ using JetBrains.Annotations;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Colour;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Effects;
using osu.Framework.Graphics.Primitives;
using osu.Framework.Graphics.Shapes;
using osu.Framework.Input.Events;
using osu.Framework.Utils;
using osu.Game.Graphics;
using osu.Game.Graphics.Sprites;
using osu.Game.Graphics.UserInterface;
using osu.Game.Rulesets.Edit;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Objects.Types;
using osu.Game.Skinning;
using osuTK;
using osuTK.Graphics;
@ -23,22 +28,26 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline
{
public class TimelineHitObjectBlueprint : SelectionBlueprint
{
private readonly Circle circle;
private const float thickness = 5;
private const float shadow_radius = 5;
private const float circle_size = 34;
public Action<DragEvent> OnDragHandled;
[UsedImplicitly]
private readonly Bindable<double> startTime;
public Action<DragEvent> OnDragHandled;
private Bindable<int> indexInCurrentComboBindable;
private Bindable<int> comboIndexBindable;
private readonly Circle circle;
private readonly DragBar dragBar;
private readonly List<Container> shadowComponents = new List<Container>();
private readonly Container mainComponents;
private readonly OsuSpriteText comboIndexText;
private const float thickness = 5;
private const float shadow_radius = 5;
private const float circle_size = 16;
[Resolved]
private ISkinSource skin { get; set; }
public TimelineHitObjectBlueprint(HitObject hitObject)
: base(hitObject)
@ -54,14 +63,28 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline
RelativeSizeAxes = Axes.X;
AutoSizeAxes = Axes.Y;
AddRangeInternal(new Drawable[]
{
mainComponents = new Container
{
Anchor = Anchor.CentreLeft,
Origin = Anchor.CentreLeft,
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
},
comboIndexText = new OsuSpriteText
{
Anchor = Anchor.CentreLeft,
Origin = Anchor.Centre,
Font = OsuFont.Numeric.With(size: circle_size / 2, weight: FontWeight.Black),
},
});
circle = new Circle
{
Size = new Vector2(circle_size),
Anchor = Anchor.CentreLeft,
Origin = Anchor.Centre,
RelativePositionAxes = Axes.X,
AlwaysPresent = true,
Colour = Color4.White,
EdgeEffect = new EdgeEffectParameters
{
Type = EdgeEffectType.Shadow,
@ -72,12 +95,12 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline
shadowComponents.Add(circle);
if (hitObject is IHasEndTime)
if (hitObject is IHasDuration)
{
DragBar dragBarUnderlay;
Container extensionBar;
AddRangeInternal(new Drawable[]
mainComponents.AddRange(new Drawable[]
{
extensionBar = new Container
{
@ -117,18 +140,89 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline
}
else
{
AddInternal(circle);
mainComponents.Add(circle);
}
updateShadows();
}
protected override void LoadComplete()
{
base.LoadComplete();
if (HitObject is IHasComboInformation comboInfo)
{
indexInCurrentComboBindable = comboInfo.IndexInCurrentComboBindable.GetBoundCopy();
indexInCurrentComboBindable.BindValueChanged(_ => updateComboIndex(), true);
comboIndexBindable = comboInfo.ComboIndexBindable.GetBoundCopy();
comboIndexBindable.BindValueChanged(_ => updateComboColour(), true);
skin.SourceChanged += updateComboColour;
}
}
private void updateComboIndex() => comboIndexText.Text = (indexInCurrentComboBindable.Value + 1).ToString();
private void updateComboColour()
{
if (!(HitObject is IHasComboInformation combo))
return;
var comboColours = skin.GetConfig<GlobalSkinColours, IReadOnlyList<Color4>>(GlobalSkinColours.ComboColours)?.Value ?? Array.Empty<Color4>();
var comboColour = combo.GetComboColour(comboColours);
if (HitObject is IHasDuration)
mainComponents.Colour = ColourInfo.GradientHorizontal(comboColour, Color4.White);
else
mainComponents.Colour = comboColour;
var col = mainComponents.Colour.TopLeft.Linear;
float brightness = col.R + col.G + col.B;
// decide the combo index colour based on brightness?
comboIndexText.Colour = brightness > 0.5f ? Color4.Black : Color4.White;
}
protected override void Update()
{
base.Update();
// no bindable so we perform this every update
Width = (float)(HitObject.GetEndTime() - HitObject.StartTime);
float duration = (float)(HitObject.GetEndTime() - HitObject.StartTime);
if (Width != duration)
{
Width = duration;
// kind of haphazard but yeah, no bindables.
if (HitObject is IHasRepeats repeats)
updateRepeats(repeats);
}
}
private Container repeatsContainer;
private void updateRepeats(IHasRepeats repeats)
{
repeatsContainer?.Expire();
mainComponents.Add(repeatsContainer = new Container
{
RelativeSizeAxes = Axes.Both,
});
for (int i = 0; i < repeats.RepeatCount; i++)
{
repeatsContainer.Add(new Circle
{
Size = new Vector2(circle_size / 2),
Anchor = Anchor.CentreLeft,
Origin = Anchor.Centre,
RelativePositionAxes = Axes.X,
X = (float)(i + 1) / (repeats.RepeatCount + 1),
});
}
}
protected override bool ShouldBeConsideredForInput(Drawable child) => true;
@ -186,7 +280,7 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline
}
}
public override Vector2 SelectionPoint => ScreenSpaceDrawQuad.TopLeft;
public override Vector2 ScreenSpaceSelectionPoint => ScreenSpaceDrawQuad.TopLeft;
public class DragBar : Container
{
@ -254,46 +348,54 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline
Colour = IsHovered || hasMouseDown ? Color4.OrangeRed : Color4.White;
}
protected override bool OnDragStart(DragStartEvent e) => true;
[Resolved]
private EditorBeatmap beatmap { get; set; }
[Resolved]
private IBeatSnapProvider beatSnapProvider { get; set; }
[Resolved(CanBeNull = true)]
private IEditorChangeHandler changeHandler { get; set; }
protected override bool OnDragStart(DragStartEvent e)
{
changeHandler?.BeginChange();
return true;
}
protected override void OnDrag(DragEvent e)
{
base.OnDrag(e);
OnDragHandled?.Invoke(e);
var time = timeline.GetTimeFromScreenSpacePosition(e.ScreenSpaceMousePosition);
switch (hitObject)
if (timeline.SnapScreenSpacePositionToValidTime(e.ScreenSpaceMousePosition).Time is double time)
{
case IHasRepeats repeatHitObject:
// find the number of repeats which can fit in the requested time.
var lengthOfOneRepeat = repeatHitObject.Duration / (repeatHitObject.RepeatCount + 1);
var proposedCount = Math.Max(0, (int)((time - hitObject.StartTime) / lengthOfOneRepeat) - 1);
switch (hitObject)
{
case IHasRepeats repeatHitObject:
// find the number of repeats which can fit in the requested time.
var lengthOfOneRepeat = repeatHitObject.Duration / (repeatHitObject.RepeatCount + 1);
var proposedCount = Math.Max(0, (int)Math.Round((time - hitObject.StartTime) / lengthOfOneRepeat) - 1);
if (proposedCount == repeatHitObject.RepeatCount)
return;
if (proposedCount == repeatHitObject.RepeatCount)
return;
repeatHitObject.RepeatCount = proposedCount;
break;
repeatHitObject.RepeatCount = proposedCount;
beatmap.Update(hitObject);
break;
case IHasEndTime endTimeHitObject:
var snappedTime = Math.Max(hitObject.StartTime, beatSnapProvider.SnapTime(time));
case IHasDuration endTimeHitObject:
var snappedTime = Math.Max(hitObject.StartTime, beatSnapProvider.SnapTime(time));
if (endTimeHitObject.EndTime == snappedTime)
return;
if (endTimeHitObject.EndTime == snappedTime || Precision.AlmostEquals(snappedTime, hitObject.StartTime, beatmap.GetBeatLengthAtTime(snappedTime)))
return;
endTimeHitObject.EndTime = snappedTime;
break;
endTimeHitObject.Duration = snappedTime - hitObject.StartTime;
beatmap.Update(hitObject);
break;
}
}
beatmap.UpdateHitObject(hitObject);
}
protected override void OnDragEnd(DragEndEvent e)
@ -301,6 +403,7 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline
base.OnDragEnd(e);
OnDragHandled?.Invoke(null);
changeHandler?.EndChange();
}
}
}

View File

@ -1,9 +1,11 @@
// 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 System.Linq;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Caching;
using osu.Framework.Graphics;
using osu.Game.Beatmaps;
using osu.Game.Graphics;
@ -12,7 +14,7 @@ using osu.Game.Screens.Edit.Components.Timelines.Summary.Visualisations;
namespace osu.Game.Screens.Edit.Compose.Components.Timeline
{
public class TimelineTickDisplay : TimelinePart
public class TimelineTickDisplay : TimelinePart<PointVisualisation>
{
[Resolved]
private EditorBeatmap beatmap { get; set; }
@ -23,6 +25,9 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline
[Resolved]
private BindableBeatDivisor beatDivisor { get; set; }
[Resolved(CanBeNull = true)]
private IEditorChangeHandler changeHandler { get; set; }
[Resolved]
private OsuColour colours { get; set; }
@ -31,15 +36,72 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline
RelativeSizeAxes = Axes.Both;
}
private readonly Cached tickCache = new Cached();
[BackgroundDependencyLoader]
private void load()
{
beatDivisor.BindValueChanged(_ => createLines(), true);
beatDivisor.BindValueChanged(_ => invalidateTicks());
if (changeHandler != null)
// currently this is the best way to handle any kind of timing changes.
changeHandler.OnStateChange += invalidateTicks;
}
private void createLines()
private void invalidateTicks()
{
Clear();
tickCache.Invalidate();
}
/// <summary>
/// The visible time/position range of the timeline.
/// </summary>
private (float min, float max) visibleRange = (float.MinValue, float.MaxValue);
/// <summary>
/// The next time/position value to the left of the display when tick regeneration needs to be run.
/// </summary>
private float? nextMinTick;
/// <summary>
/// The next time/position value to the right of the display when tick regeneration needs to be run.
/// </summary>
private float? nextMaxTick;
[Resolved(canBeNull: true)]
private Timeline timeline { get; set; }
protected override void Update()
{
base.Update();
if (timeline != null)
{
var newRange = (
(ToLocalSpace(timeline.ScreenSpaceDrawQuad.TopLeft).X - PointVisualisation.WIDTH * 2) / DrawWidth * Content.RelativeChildSize.X,
(ToLocalSpace(timeline.ScreenSpaceDrawQuad.TopRight).X + PointVisualisation.WIDTH * 2) / DrawWidth * Content.RelativeChildSize.X);
if (visibleRange != newRange)
{
visibleRange = newRange;
// actual regeneration only needs to occur if we've passed one of the known next min/max tick boundaries.
if (nextMinTick == null || nextMaxTick == null || (visibleRange.min < nextMinTick || visibleRange.max > nextMaxTick))
tickCache.Invalidate();
}
}
if (!tickCache.IsValid)
createTicks();
}
private void createTicks()
{
int drawableIndex = 0;
int highestDivisor = BindableBeatDivisor.VALID_DIVISORS.Last();
nextMinTick = null;
nextMaxTick = null;
for (var i = 0; i < beatmap.ControlPointInfo.TimingPoints.Count; i++)
{
@ -50,41 +112,78 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline
for (double t = point.Time; t < until; t += point.BeatLength / beatDivisor.Value)
{
var indexInBeat = beat % beatDivisor.Value;
float xPos = (float)t;
if (indexInBeat == 0)
{
Add(new PointVisualisation(t)
{
Colour = BindableBeatDivisor.GetColourFor(1, colours),
Origin = Anchor.TopCentre,
});
}
if (t < visibleRange.min)
nextMinTick = xPos;
else if (t > visibleRange.max)
nextMaxTick ??= xPos;
else
{
// if this is the first beat in the beatmap, there is no next min tick
if (beat == 0 && i == 0)
nextMinTick = float.MinValue;
var indexInBar = beat % ((int)point.TimeSignature * beatDivisor.Value);
var divisor = BindableBeatDivisor.GetDivisorForBeatIndex(beat, beatDivisor.Value);
var colour = BindableBeatDivisor.GetColourFor(divisor, colours);
var height = 0.1f - (float)divisor / BindableBeatDivisor.VALID_DIVISORS.Last() * 0.08f;
Add(new PointVisualisation(t)
{
Colour = colour,
Height = height,
Origin = Anchor.TopCentre,
});
// even though "bar lines" take up the full vertical space, we render them in two pieces because it allows for less anchor/origin churn.
var height = indexInBar == 0 ? 0.5f : 0.1f - (float)divisor / highestDivisor * 0.08f;
Add(new PointVisualisation(t)
{
Colour = colour,
Anchor = Anchor.BottomLeft,
Origin = Anchor.BottomCentre,
Height = height,
});
var topPoint = getNextUsablePoint();
topPoint.X = xPos;
topPoint.Colour = colour;
topPoint.Height = height;
topPoint.Anchor = Anchor.TopLeft;
topPoint.Origin = Anchor.TopCentre;
var bottomPoint = getNextUsablePoint();
bottomPoint.X = xPos;
bottomPoint.Colour = colour;
bottomPoint.Anchor = Anchor.BottomLeft;
bottomPoint.Origin = Anchor.BottomCentre;
bottomPoint.Height = height;
}
beat++;
}
}
int usedDrawables = drawableIndex;
// save a few drawables beyond the currently used for edge cases.
while (drawableIndex < Math.Min(usedDrawables + 16, Count))
Children[drawableIndex++].Hide();
// expire any excess
while (drawableIndex < Count)
Children[drawableIndex++].Expire();
tickCache.Validate();
Drawable getNextUsablePoint()
{
PointVisualisation point;
if (drawableIndex >= Count)
Add(point = new PointVisualisation());
else
point = Children[drawableIndex];
drawableIndex++;
point.Show();
return point;
}
}
protected override void Dispose(bool isDisposing)
{
base.Dispose(isDisposing);
if (changeHandler != null)
changeHandler.OnStateChange -= invalidateTicks;
}
}
}

View File

@ -0,0 +1,63 @@
// 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.Extensions.Color4Extensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Colour;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes;
using osu.Game.Beatmaps.ControlPoints;
using osu.Game.Graphics;
using osu.Game.Graphics.Sprites;
using osuTK.Graphics;
namespace osu.Game.Screens.Edit.Compose.Components.Timeline
{
public class TimingPointPiece : CompositeDrawable
{
private readonly TimingControlPoint point;
private readonly BindableNumber<double> beatLength;
private OsuSpriteText bpmText;
public TimingPointPiece(TimingControlPoint point)
{
this.point = point;
beatLength = point.BeatLengthBindable.GetBoundCopy();
}
[BackgroundDependencyLoader]
private void load(OsuColour colours)
{
Origin = Anchor.CentreLeft;
Anchor = Anchor.CentreLeft;
AutoSizeAxes = Axes.Both;
Color4 colour = point.GetRepresentingColour(colours);
InternalChildren = new Drawable[]
{
new Box
{
Alpha = 0.9f,
Colour = ColourInfo.GradientHorizontal(colour, colour.Opacity(0.5f)),
RelativeSizeAxes = Axes.Both,
},
bpmText = new OsuSpriteText
{
Alpha = 0.9f,
Padding = new MarginPadding(3),
Font = OsuFont.Default.With(size: 40)
}
};
beatLength.BindValueChanged(beatLength =>
{
bpmText.Text = $"{60000 / beatLength.NewValue:n1} BPM";
}, true);
}
}
}

View File

@ -29,9 +29,14 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline
private readonly Container zoomedContent;
protected override Container<Drawable> Content => zoomedContent;
private float currentZoom = 1;
/// <summary>
/// The current zoom level of <see cref="ZoomableScrollContainer" />.
/// It may differ from <see cref="Zoom" /> during transitions.
/// </summary>
public float CurrentZoom => currentZoom;
[Resolved(canBeNull: true)]
private IFrameBasedClock editorClock { get; set; }
@ -108,19 +113,19 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline
protected override bool OnScroll(ScrollEvent e)
{
if (e.IsPrecise)
if (e.AltPressed)
{
// can't handle scroll correctly while playing.
// the editor will handle this case for us.
if (editorClock?.IsRunning == true)
return false;
// for now, we don't support zoom when using a precision scroll device. this needs gesture support.
return base.OnScroll(e);
// zoom when holding alt.
setZoomTarget(zoomTarget + e.ScrollDelta.Y, zoomedContent.ToLocalSpace(e.ScreenSpaceMousePosition).X);
return true;
}
setZoomTarget(zoomTarget + e.ScrollDelta.Y, zoomedContent.ToLocalSpace(e.ScreenSpaceMousePosition).X);
return true;
// can't handle scroll correctly while playing.
// the editor will handle this case for us.
if (editorClock?.IsRunning == true)
return false;
return base.OnScroll(e);
}
private void updateZoomedContentWidth() => zoomedContent.Width = DrawWidth * currentZoom;