1
0
mirror of https://github.com/ppy/osu.git synced 2026-05-16 16:23:39 +08:00
Files
osu-lazer/osu.Game/Screens/SelectV2/Panel.cs
T
Dean Herbert c83ebe3a25 Adjust panel flashing to feel more in time
Especially on higher BPM tracks.

Rather than delaying time wise, higher level panels will now just flash
less often.

Addresses https://github.com/ppy/osu/discussions/34396 maybe.
2025-07-28 15:40:22 +09:00

393 lines
12 KiB
C#

// 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 osu.Framework.Allocation;
using osu.Framework.Audio.Track;
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.Cursor;
using osu.Framework.Graphics.Effects;
using osu.Framework.Graphics.Pooling;
using osu.Framework.Graphics.Shapes;
using osu.Framework.Graphics.UserInterface;
using osu.Framework.Input.Events;
using osu.Game.Beatmaps.ControlPoints;
using osu.Game.Graphics;
using osu.Game.Graphics.Carousel;
using osu.Game.Graphics.Containers;
using osu.Game.Graphics.UserInterface;
using osu.Game.Overlays;
using osuTK;
using osuTK.Graphics;
namespace osu.Game.Screens.SelectV2
{
public abstract partial class Panel : PoolableDrawable, ICarouselPanel, IHasContextMenu
{
public const float CORNER_RADIUS = 10;
private const float active_x_offset = 25f;
protected const float DURATION = 400;
protected float PanelXOffset { get; init; }
private Container backgroundContainer = null!;
private Container iconContainer = null!;
private Drawable activationFlash = null!;
private Drawable hoverLayer = null!;
private Drawable keyboardSelectionLayer = null!;
private PulsatingBox selectionLayer = null!;
public Container TopLevelContent { get; private set; } = null!;
private Container contentPaddingContainer = null!;
protected Container Content { get; private set; } = null!;
public Drawable Background
{
set => backgroundContainer.Child = value;
}
public Drawable Icon
{
set => iconContainer.Child = value;
}
private Color4? accentColour;
public Color4? AccentColour
{
get => accentColour;
set
{
if (value == accentColour)
return;
accentColour = value;
updateAccentColour();
}
}
public sealed override bool ReceivePositionalInputAt(Vector2 screenSpacePos)
{
if (item == null)
return TopLevelContent.ReceivePositionalInputAt(screenSpacePos);
var inputRectangle = TopLevelContent.DrawRectangle;
// Cover the gaps introduced by the spacing between panels so that user mis-aims don't result in no-ops.
inputRectangle = inputRectangle.Inflate(new MarginPadding
{
Top = item.CarouselInputLenienceAbove,
Bottom = item.CarouselInputLenienceBelow,
});
return inputRectangle.Contains(TopLevelContent.ToLocalSpace(screenSpacePos));
}
[Resolved]
private BeatmapCarousel? carousel { get; set; }
[BackgroundDependencyLoader]
private void load(OverlayColourProvider colourProvider, OsuColour colours)
{
Anchor = Anchor.TopRight;
Origin = Anchor.TopRight;
RelativeSizeAxes = Axes.X;
Height = CarouselItem.DEFAULT_HEIGHT;
InternalChild = TopLevelContent = new Container
{
Masking = true,
CornerRadius = CORNER_RADIUS,
RelativeSizeAxes = Axes.Both,
X = CORNER_RADIUS,
Children = new[]
{
backgroundContainer = new Container
{
RelativeSizeAxes = Axes.Both,
},
iconContainer = new Container
{
Anchor = Anchor.CentreLeft,
Origin = Anchor.CentreLeft,
AutoSizeAxes = Axes.Both,
},
contentPaddingContainer = new Container
{
RelativeSizeAxes = Axes.Both,
Child = Content = new Container
{
RelativeSizeAxes = Axes.Both,
CornerRadius = CORNER_RADIUS,
Masking = true,
},
},
hoverLayer = new Box
{
Alpha = 0,
Colour = colours.Blue.Opacity(0.1f),
Blending = BlendingParameters.Additive,
RelativeSizeAxes = Axes.Both,
},
selectionLayer = new PulsatingBox
{
Alpha = 0,
RelativeSizeAxes = Axes.Both,
Width = 0.8f,
Blending = BlendingParameters.Additive,
Anchor = Anchor.TopRight,
Origin = Anchor.TopRight,
},
keyboardSelectionLayer = new Box
{
Alpha = 0,
Colour = ColourInfo.GradientHorizontal(colourProvider.Highlight1.Opacity(0.1f), colourProvider.Highlight1.Opacity(0.4f)),
Blending = BlendingParameters.Additive,
RelativeSizeAxes = Axes.Both,
},
activationFlash = new Box
{
Colour = Color4.White.Opacity(0.4f),
Blending = BlendingParameters.Additive,
Alpha = 0f,
RelativeSizeAxes = Axes.Both,
},
new HoverSounds(),
}
};
}
public partial class PulsatingBox : BeatSyncedContainer
{
public int FlashOffset;
private readonly Box box;
public PulsatingBox()
{
EarlyActivationMilliseconds = 40;
InternalChildren = new Drawable[]
{
box = new Box
{
RelativeSizeAxes = Axes.Both,
},
};
}
protected override void OnNewBeat(int beatIndex, TimingControlPoint timingPoint, EffectControlPoint effectPoint, ChannelAmplitudes amplitudes)
{
base.OnNewBeat(beatIndex, timingPoint, effectPoint, amplitudes);
if (beatIndex % Math.Pow(2, FlashOffset) != 0)
return;
double length = timingPoint.BeatLength;
while (length < 250)
length *= 2;
box
.FadeTo(0.8f, 40, Easing.Out)
.Then()
.FadeTo(0.4f, length, Easing.Out);
}
}
protected override void LoadComplete()
{
base.LoadComplete();
Expanded.BindValueChanged(_ =>
{
updateSelectedState();
updateXOffset();
});
Selected.BindValueChanged(_ =>
{
updateSelectedState();
updateXOffset();
}, true);
KeyboardSelected.BindValueChanged(selected =>
{
if (selected.NewValue)
{
keyboardSelectionLayer.FadeIn(80, Easing.Out)
.Then()
.FadeTo(0.5f, 2000, Easing.OutQuint);
}
else
keyboardSelectionLayer.FadeOut(1000, Easing.OutQuint);
updateXOffset();
}, true);
}
protected override void PrepareForUse()
{
base.PrepareForUse();
// Slightly offset the flash animation based on the panel depth.
// This assumes a minimum depth of -2 (groups).
selectionLayer.FlashOffset = -Item!.DepthLayer;
updateAccentColour();
updateXOffset(animated: false);
updateSelectedState(animated: false);
this.FadeIn(DURATION, Easing.OutQuint);
}
protected override void FreeAfterUse()
{
base.FreeAfterUse();
Hide();
// Important to set this to null to handle reuse scenarios correctly, see `Item` implementation.
item = null;
}
protected override bool OnClick(ClickEvent e)
{
carousel?.Activate(Item!);
return true;
}
private void updateAccentColour()
{
var backgroundColour = accentColour ?? Color4.White;
selectionLayer.Colour = ColourInfo.GradientHorizontal(backgroundColour.Opacity(0), backgroundColour.Opacity(0.5f));
updateSelectedState(animated: false);
}
private void updateSelectedState(bool animated = true)
{
bool selectedOrExpanded = Expanded.Value || Selected.Value;
var edgeEffectColour = accentColour ?? Color4Extensions.FromHex(@"4EBFFF");
if (selectedOrExpanded)
{
TopLevelContent.EdgeEffect = new EdgeEffectParameters
{
Type = EdgeEffectType.Shadow,
Radius = 2f,
Hollow = true,
};
}
else
{
TopLevelContent.EdgeEffect = new EdgeEffectParameters
{
Type = EdgeEffectType.Shadow,
Radius = 4f,
Hollow = true,
Offset = new Vector2(0f, 1f),
};
}
TopLevelContent.FadeEdgeEffectTo(selectedOrExpanded ? edgeEffectColour.Opacity(0.8f) : Color4.Black.Opacity(0.2f), animated ? DURATION : 0, Easing.OutQuint);
if (selectedOrExpanded)
selectionLayer.FadeIn(100, Easing.OutQuint);
else
selectionLayer.FadeOut(200, Easing.OutQuint);
}
private void updateXOffset(bool animated = true)
{
float x = PanelXOffset + CORNER_RADIUS;
if (!Expanded.Value && !Selected.Value)
{
if (this is PanelBeatmap || this is PanelBeatmapStandalone)
x += active_x_offset * 2;
else
x += active_x_offset * 4;
}
if (!KeyboardSelected.Value)
x += active_x_offset;
TopLevelContent.MoveToX(x, animated ? DURATION : 0, Easing.OutQuint);
}
protected override bool OnHover(HoverEvent e)
{
hoverLayer.FadeIn(100, Easing.OutQuint);
return true;
}
protected override void OnHoverLost(HoverLostEvent e)
{
hoverLayer.FadeOut(1000, Easing.OutQuint);
base.OnHoverLost(e);
}
protected override void Update()
{
base.Update();
contentPaddingContainer.Padding = contentPaddingContainer.Padding with { Left = iconContainer.DrawWidth };
}
public abstract MenuItem[]? ContextMenuItems { get; }
#region ICarouselPanel
private CarouselItem? item;
public CarouselItem? Item
{
get => item;
set
{
if (ReferenceEquals(item, value))
return;
// If a new item is set and we already have an item, this is a case of reuse.
// To keep things simple, assume that we need to do a full refresh.
//
// In the future, this could be more contextual and check whether the associated model has actually changed.
if (item != null && value != null)
{
item = value;
PrepareForUse();
}
else
item = value;
}
}
public BindableBool Selected { get; } = new BindableBool();
public BindableBool Expanded { get; } = new BindableBool();
public BindableBool KeyboardSelected { get; } = new BindableBool();
public double DrawYPosition { get; set; }
public virtual void Activated()
{
activationFlash.FadeOutFromOne(1000, Easing.OutQuint);
}
#endregion
}
}