diff --git a/src/Avalonia/Artemis.UI.Shared/Services/ProfileEditor/IProfileEditorService.cs b/src/Avalonia/Artemis.UI.Shared/Services/ProfileEditor/IProfileEditorService.cs
index bef5cab0d..40cb889a5 100644
--- a/src/Avalonia/Artemis.UI.Shared/Services/ProfileEditor/IProfileEditorService.cs
+++ b/src/Avalonia/Artemis.UI.Shared/Services/ProfileEditor/IProfileEditorService.cs
@@ -39,9 +39,8 @@ public interface IProfileEditorService : IArtemisSharedUIService
///
/// Gets an observable of the zoom level.
///
- IObservable PixelsPerSecond { get; }
-
-
+ IObservable PixelsPerSecond { get; }
+
///
/// Changes the selected profile by its .
///
@@ -61,7 +60,14 @@ public interface IProfileEditorService : IArtemisSharedUIService
void ChangeTime(TimeSpan time);
///
- /// Snaps the given time to the closest relevant element in the timeline, this can be the cursor, a keyframe or a segment end.
+ /// Changes the current pixels per second
+ ///
+ /// The new pixels per second.
+ void ChangePixelsPerSecond(int pixelsPerSecond);
+
+ ///
+ /// Snaps the given time to the closest relevant element in the timeline, this can be the cursor, a keyframe or a
+ /// segment end.
///
/// The time to snap.
/// How close the time must be to snap.
diff --git a/src/Avalonia/Artemis.UI.Shared/Services/ProfileEditor/ProfileEditorService.cs b/src/Avalonia/Artemis.UI.Shared/Services/ProfileEditor/ProfileEditorService.cs
index bd3663872..54de02885 100644
--- a/src/Avalonia/Artemis.UI.Shared/Services/ProfileEditor/ProfileEditorService.cs
+++ b/src/Avalonia/Artemis.UI.Shared/Services/ProfileEditor/ProfileEditorService.cs
@@ -19,7 +19,7 @@ internal class ProfileEditorService : IProfileEditorService
private readonly BehaviorSubject _timeSubject = new(TimeSpan.Zero);
private readonly BehaviorSubject _playingSubject = new(false);
private readonly BehaviorSubject _suspendedEditingSubject = new(false);
- private readonly BehaviorSubject _pixelsPerSecondSubject = new(300);
+ private readonly BehaviorSubject _pixelsPerSecondSubject = new(120);
private readonly ILogger _logger;
private readonly IProfileService _profileService;
private readonly IModuleService _moduleService;
@@ -59,7 +59,7 @@ internal class ProfileEditorService : IProfileEditorService
public IObservable Time { get; }
public IObservable Playing { get; }
public IObservable SuspendedEditing { get; }
- public IObservable PixelsPerSecond { get; }
+ public IObservable PixelsPerSecond { get; }
public void ChangeCurrentProfileConfiguration(ProfileConfiguration? profileConfiguration)
{
@@ -147,7 +147,7 @@ internal class ProfileEditorService : IProfileEditorService
return time;
}
- public void ChangePixelsPerSecond(double pixelsPerSecond)
+ public void ChangePixelsPerSecond(int pixelsPerSecond)
{
_pixelsPerSecondSubject.OnNext(pixelsPerSecond);
}
diff --git a/src/Avalonia/Artemis.UI/Controls/TimelineHeader.cs b/src/Avalonia/Artemis.UI/Controls/TimelineHeader.cs
new file mode 100644
index 000000000..4c17ef671
--- /dev/null
+++ b/src/Avalonia/Artemis.UI/Controls/TimelineHeader.cs
@@ -0,0 +1,186 @@
+using System;
+using Avalonia;
+using Avalonia.Controls;
+using Avalonia.Media;
+
+namespace Artemis.UI.Controls;
+
+public class TimelineHeader : Control
+{
+ public static readonly StyledProperty ForegroundProperty = AvaloniaProperty.Register(nameof(Foreground), new SolidColorBrush(Colors.Black));
+ public static readonly StyledProperty BackgroundProperty = AvaloniaProperty.Register(nameof(Background), new SolidColorBrush(Colors.Transparent));
+ public static readonly StyledProperty FontFamilyProperty = AvaloniaProperty.Register(nameof(FontFamily), FontFamily.Default);
+ public static readonly StyledProperty PixelsPerSecondProperty = AvaloniaProperty.Register(nameof(PixelsPerSecond));
+ public static readonly StyledProperty HorizontalOffsetProperty = AvaloniaProperty.Register(nameof(HorizontalOffset));
+ public static readonly StyledProperty VisibleWidthProperty = AvaloniaProperty.Register(nameof(VisibleWidth));
+ public static readonly StyledProperty OffsetFirstValueProperty = AvaloniaProperty.Register(nameof(OffsetFirstValue));
+
+ ///
+ static TimelineHeader()
+ {
+ AffectsRender(
+ ForegroundProperty,
+ BackgroundProperty,
+ FontFamilyProperty,
+ PixelsPerSecondProperty,
+ HorizontalOffsetProperty,
+ VisibleWidthProperty,
+ OffsetFirstValueProperty
+ );
+ }
+
+ private double _subd1;
+ private double _subd2;
+ private double _subd3;
+
+ public Brush Foreground
+ {
+ get => GetValue(ForegroundProperty);
+ set => SetValue(ForegroundProperty, value);
+ }
+
+ public Brush Background
+ {
+ get => GetValue(BackgroundProperty);
+ set => SetValue(BackgroundProperty, value);
+ }
+
+ public FontFamily FontFamily
+ {
+ get => GetValue(FontFamilyProperty);
+ set => SetValue(FontFamilyProperty, value);
+ }
+
+ public int PixelsPerSecond
+ {
+ get => GetValue(PixelsPerSecondProperty);
+ set => SetValue(PixelsPerSecondProperty, value);
+ }
+
+ public double HorizontalOffset
+ {
+ get => GetValue(HorizontalOffsetProperty);
+ set => SetValue(HorizontalOffsetProperty, value);
+ }
+
+ public double VisibleWidth
+ {
+ get => GetValue(VisibleWidthProperty);
+ set => SetValue(VisibleWidthProperty, value);
+ }
+
+ public bool OffsetFirstValue
+ {
+ get => GetValue(OffsetFirstValueProperty);
+ set => SetValue(OffsetFirstValueProperty, value);
+ }
+
+ public override void Render(DrawingContext drawingContext)
+ {
+ UpdateTimeScale();
+
+ drawingContext.DrawRectangle(Background, null, new Rect(0, 0, Bounds.Width, 30));
+
+ Pen linePen = new(Foreground);
+ double width = HorizontalOffset + VisibleWidth;
+ int frameStart = 0;
+
+ double units = PixelsPerSecond / _subd1;
+ double offsetUnits = frameStart * PixelsPerSecond % units;
+
+ // Labels
+ double count = (width + offsetUnits) / units;
+ for (int i = 0; i < count; i++)
+ {
+ double x = i * units - offsetUnits;
+ // Add a 100px margin to allow the text to partially render when needed
+ if (x < HorizontalOffset - 100 || x > HorizontalOffset + width)
+ continue;
+
+ TimeSpan t = TimeSpan.FromSeconds((i * units - offsetUnits) / PixelsPerSecond + frameStart);
+ // 0.00 is always formatted as 0.00
+ if (t == TimeSpan.Zero)
+ RenderLabel(drawingContext, "0.00", x);
+ else if (PixelsPerSecond > 200)
+ RenderLabel(drawingContext, $"{Math.Floor(t.TotalSeconds):00}.{t.Milliseconds:000}", x);
+ else if (PixelsPerSecond > 60)
+ RenderLabel(drawingContext, $"{Math.Floor(t.TotalSeconds):00}.{t.Milliseconds:000}", x);
+ else
+ RenderLabel(drawingContext, $"{Math.Floor(t.TotalMinutes):0}:{t.Seconds:00}", x);
+ }
+
+ // Large ticks
+ units = PixelsPerSecond / _subd2;
+ count = (width + offsetUnits) / units;
+ for (int i = 0; i < count; i++)
+ {
+ double x = i * units - offsetUnits;
+ if (x == 0 && OffsetFirstValue)
+ drawingContext.DrawLine(linePen, new Point(1, 20), new Point(1, 30));
+ else if (x > HorizontalOffset && x < HorizontalOffset + width)
+ drawingContext.DrawLine(linePen, new Point(x, 20), new Point(x, 30));
+ }
+
+ // Small ticks
+ double mul = _subd3 / _subd2;
+ units = PixelsPerSecond / _subd3;
+ count = (width + offsetUnits) / units;
+ for (int i = 0; i < count; i++)
+ {
+ if (Math.Abs(i % mul) < 0.001) continue;
+ double x = i * units - offsetUnits;
+ if (x > HorizontalOffset && x < HorizontalOffset + width)
+ drawingContext.DrawLine(linePen, new Point(x, 25), new Point(x, 30));
+ }
+ }
+
+ private void RenderLabel(DrawingContext drawingContext, string text, double x)
+ {
+ Typeface typeFace = new(FontFamily);
+ FormattedText formattedText = new(text, typeFace, 9, TextAlignment.Left, TextWrapping.NoWrap, Bounds.Size);
+ if (x == 0 && OffsetFirstValue)
+ drawingContext.DrawText(Foreground, new Point(2, 5), formattedText);
+ else
+ drawingContext.DrawText(Foreground, new Point(x - formattedText.Bounds.Width / 2, 5), formattedText);
+ }
+
+ private void UpdateTimeScale()
+ {
+ object[] subds;
+ if (PixelsPerSecond > 350)
+ subds = new object[] {12d, 12d, 60d};
+ else if (PixelsPerSecond > 250)
+ subds = new object[] {6d, 12d, 60d};
+ else if (PixelsPerSecond > 200)
+ subds = new object[] {6d, 6d, 30d};
+ else if (PixelsPerSecond > 150)
+ subds = new object[] {4d, 4d, 20d};
+ else if (PixelsPerSecond > 140)
+ subds = new object[] {4d, 4d, 20d};
+ else if (PixelsPerSecond > 90)
+ subds = new object[] {2d, 4d, 20d};
+ else if (PixelsPerSecond > 60)
+ subds = new object[] {2d, 4d, 8d};
+ else if (PixelsPerSecond > 40)
+ subds = new object[] {1d, 2d, 10d};
+ else if (PixelsPerSecond > 30)
+ subds = new object[] {1d, 2d, 10d};
+ else if (PixelsPerSecond > 10)
+ subds = new object[] {1d / 2d, 1d / 2d, 1d / 2d};
+ else if (PixelsPerSecond > 4)
+ subds = new object[] {1d / 5d, 1d / 5d, 1d / 5d};
+ else if (PixelsPerSecond > 3)
+ subds = new object[] {1d / 10d, 1d / 10d, 1d / 5d};
+ else if (PixelsPerSecond > 1)
+ subds = new object[] {1d / 20d, 1d / 20d, 1d / 10d};
+ else if (PixelsPerSecond >= 1)
+ subds = new object[] {1d / 30d, 1d / 30d, 1d / 15d};
+ else
+ // 1s per pixel
+ subds = new object[] {1d / 60d, 1d / 60d, 1d / 15d};
+
+ _subd1 = (double) subds[0]; // big ticks / labels
+ _subd2 = (double) subds[1]; // medium ticks
+ _subd3 = (double) subds[2]; // small ticks
+ }
+}
\ No newline at end of file
diff --git a/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/ProfileElementProperties/ProfileElementPropertiesView.axaml b/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/ProfileElementProperties/ProfileElementPropertiesView.axaml
index 8e22176a4..bd8454752 100644
--- a/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/ProfileElementProperties/ProfileElementPropertiesView.axaml
+++ b/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/ProfileElementProperties/ProfileElementPropertiesView.axaml
@@ -4,6 +4,7 @@
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:local="clr-namespace:Artemis.UI.Screens.ProfileEditor.ProfileElementProperties"
xmlns:avalonia="clr-namespace:Material.Icons.Avalonia;assembly=Material.Icons.Avalonia"
+ xmlns:controls="clr-namespace:Artemis.UI.Controls"
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
x:Class="Artemis.UI.Screens.ProfileEditor.ProfileElementProperties.ProfileElementPropertiesView">
@@ -26,12 +27,24 @@
+ Background="Transparent"
+ Margin="0 0 -5 0" />
+
+