From 147e050e6919bfb1ef02c034ae67810512078507 Mon Sep 17 00:00:00 2001 From: Robert Date: Fri, 21 Oct 2022 10:37:02 +0200 Subject: [PATCH 1/2] Nodes - Added Pressed Key Position node --- .../Nodes/Input/PressedKeyPositionNode.cs | 73 ++++++++++++++ .../Input/PressedKeyPositionNodeEntity.cs | 29 ++++++ .../PressedKeyPositionNodeCustomView.axaml | 29 ++++++ .../PressedKeyPositionNodeCustomView.axaml.cs | 19 ++++ .../PressedKeyPositionNodeCustomViewModel.cs | 94 +++++++++++++++++++ 5 files changed, 244 insertions(+) create mode 100644 src/Artemis.VisualScripting/Nodes/Input/PressedKeyPositionNode.cs create mode 100644 src/Artemis.VisualScripting/Nodes/Input/PressedKeyPositionNodeEntity.cs create mode 100644 src/Artemis.VisualScripting/Nodes/Input/Screens/PressedKeyPositionNodeCustomView.axaml create mode 100644 src/Artemis.VisualScripting/Nodes/Input/Screens/PressedKeyPositionNodeCustomView.axaml.cs create mode 100644 src/Artemis.VisualScripting/Nodes/Input/Screens/PressedKeyPositionNodeCustomViewModel.cs diff --git a/src/Artemis.VisualScripting/Nodes/Input/PressedKeyPositionNode.cs b/src/Artemis.VisualScripting/Nodes/Input/PressedKeyPositionNode.cs new file mode 100644 index 000000000..e08b05fab --- /dev/null +++ b/src/Artemis.VisualScripting/Nodes/Input/PressedKeyPositionNode.cs @@ -0,0 +1,73 @@ +using Artemis.Core; +using Artemis.Core.Services; +using Artemis.VisualScripting.Nodes.Input.Screens; +using SkiaSharp; +using static Artemis.VisualScripting.Nodes.Input.PressedKeyPositionNodeEntity; + +namespace Artemis.VisualScripting.Nodes.Input; + +[Node("Pressed Key Position", "Outputs the position of a pressed key relative to a layer", "Input", OutputType = typeof(Numeric))] +public class PressedKeyPositionNode : Node, IDisposable +{ + private readonly IInputService _inputService; + private Layer? _layer; + private SKPoint _ledPosition; + private Profile? _profile; + + public PressedKeyPositionNode(IInputService inputService) + { + _inputService = inputService; + XPosition = CreateOutputPin("X"); + YPosition = CreateOutputPin("Y"); + + StorageModified += OnStorageModified; + _inputService.KeyboardKeyDown += InputServiceOnKeyboardKeyDown; + _inputService.KeyboardKeyUp += InputServiceOnKeyboardKeyUp; + } + + public OutputPin XPosition { get; } + public OutputPin YPosition { get; } + + public override void Initialize(INodeScript script) + { + Storage ??= new PressedKeyPositionNodeEntity(); + + _profile = script.Context as Profile; + _layer = _profile?.GetAllLayers().FirstOrDefault(l => l.EntityId == Storage.LayerId); + } + + public override void Evaluate() + { + XPosition.Value = _ledPosition.X; + YPosition.Value = _ledPosition.Y; + } + + private void InputServiceOnKeyboardKeyDown(object? sender, ArtemisKeyboardKeyEventArgs e) + { + if (Storage?.RespondTo is KeyPressType.Down or KeyPressType.UpDown) + SetLedPosition(e.Led); + } + + private void InputServiceOnKeyboardKeyUp(object? sender, ArtemisKeyboardKeyEventArgs e) + { + if (Storage?.RespondTo is KeyPressType.Up or KeyPressType.UpDown) + SetLedPosition(e.Led); + } + + private void SetLedPosition(ArtemisLed? led) + { + if (_layer != null && led != null) + _ledPosition = new SKPoint((led.AbsoluteRectangle.MidX - _layer.Bounds.Left) / _layer.Bounds.Width, (led.AbsoluteRectangle.MidY - _layer.Bounds.Top) / _layer.Bounds.Height); + } + + private void OnStorageModified(object? sender, EventArgs e) + { + _layer = _profile?.GetAllLayers().FirstOrDefault(l => l.EntityId == Storage?.LayerId); + } + + public void Dispose() + { + _inputService.KeyboardKeyDown -= InputServiceOnKeyboardKeyDown; + _inputService.KeyboardKeyUp -= InputServiceOnKeyboardKeyUp; + } +} \ No newline at end of file diff --git a/src/Artemis.VisualScripting/Nodes/Input/PressedKeyPositionNodeEntity.cs b/src/Artemis.VisualScripting/Nodes/Input/PressedKeyPositionNodeEntity.cs new file mode 100644 index 000000000..1c74a9899 --- /dev/null +++ b/src/Artemis.VisualScripting/Nodes/Input/PressedKeyPositionNodeEntity.cs @@ -0,0 +1,29 @@ +using System.ComponentModel; + +namespace Artemis.VisualScripting.Nodes.Input; + +public class PressedKeyPositionNodeEntity +{ + public PressedKeyPositionNodeEntity() + { + } + + public PressedKeyPositionNodeEntity(Guid layerId, KeyPressType respondTo) + { + LayerId = layerId; + RespondTo = respondTo; + } + + public Guid LayerId { get; set; } + public KeyPressType RespondTo { get; set; } + + public enum KeyPressType + { + [Description("Up")] + Up, + [Description("Down")] + Down, + [Description("Up/down")] + UpDown + } +} \ No newline at end of file diff --git a/src/Artemis.VisualScripting/Nodes/Input/Screens/PressedKeyPositionNodeCustomView.axaml b/src/Artemis.VisualScripting/Nodes/Input/Screens/PressedKeyPositionNodeCustomView.axaml new file mode 100644 index 000000000..f9a87a72c --- /dev/null +++ b/src/Artemis.VisualScripting/Nodes/Input/Screens/PressedKeyPositionNodeCustomView.axaml @@ -0,0 +1,29 @@ + + + Layer + + + + + + + + + + + + Respond to + + + + \ No newline at end of file diff --git a/src/Artemis.VisualScripting/Nodes/Input/Screens/PressedKeyPositionNodeCustomView.axaml.cs b/src/Artemis.VisualScripting/Nodes/Input/Screens/PressedKeyPositionNodeCustomView.axaml.cs new file mode 100644 index 000000000..0896825e0 --- /dev/null +++ b/src/Artemis.VisualScripting/Nodes/Input/Screens/PressedKeyPositionNodeCustomView.axaml.cs @@ -0,0 +1,19 @@ +using Avalonia; +using Avalonia.Controls; +using Avalonia.Markup.Xaml; +using Avalonia.ReactiveUI; + +namespace Artemis.VisualScripting.Nodes.Input.Screens; + +public partial class PressedKeyPositionNodeCustomView : ReactiveUserControl +{ + public PressedKeyPositionNodeCustomView() + { + InitializeComponent(); + } + + private void InitializeComponent() + { + AvaloniaXamlLoader.Load(this); + } +} \ No newline at end of file diff --git a/src/Artemis.VisualScripting/Nodes/Input/Screens/PressedKeyPositionNodeCustomViewModel.cs b/src/Artemis.VisualScripting/Nodes/Input/Screens/PressedKeyPositionNodeCustomViewModel.cs new file mode 100644 index 000000000..fce2c13a3 --- /dev/null +++ b/src/Artemis.VisualScripting/Nodes/Input/Screens/PressedKeyPositionNodeCustomViewModel.cs @@ -0,0 +1,94 @@ +using System.Collections.ObjectModel; +using System.Reactive.Linq; +using Artemis.Core; +using Artemis.UI.Shared.Services.NodeEditor; +using Artemis.UI.Shared.Services.NodeEditor.Commands; +using Artemis.UI.Shared.VisualScripting; +using Avalonia.Controls.Mixins; +using ReactiveUI; +using static Artemis.VisualScripting.Nodes.Input.PressedKeyPositionNodeEntity; + +namespace Artemis.VisualScripting.Nodes.Input.Screens; + +public class PressedKeyPositionNodeCustomViewModel : CustomNodeViewModel +{ + private readonly INodeEditorService _nodeEditorService; + private readonly Profile? _profile; + private readonly PressedKeyPositionNode _node; + private Layer? _selectedLayer; + private KeyPressType _respondTo; + + public PressedKeyPositionNodeCustomViewModel(PressedKeyPositionNode node, INodeScript script, INodeEditorService nodeEditorService) : base(node, script) + { + _nodeEditorService = nodeEditorService; + _profile = script.Context as Profile; + _node = node; + + Layers = new ObservableCollection(); + + this.WhenActivated(d => + { + if (_profile == null) + return; + + Observable.FromEventPattern(x => _profile.DescendentAdded += x, x => _profile.DescendentAdded -= x).Subscribe(_ => GetLayers()).DisposeWith(d); + Observable.FromEventPattern(x => _profile.DescendentRemoved += x, x => _profile.DescendentRemoved -= x).Subscribe(_ => GetLayers()).DisposeWith(d); + Observable.FromEventPattern(x => _node.StorageModified += x, x => _node.StorageModified -= x).Subscribe(_ => Update()).DisposeWith(d); + + GetLayers(); + }); + + this.WhenAnyValue(vm => vm.SelectedLayer).Subscribe(UpdateSelectedLayer); + this.WhenAnyValue(vm => vm.RespondTo).Subscribe(UpdateSelectedRespondTo); + } + + public ObservableCollection Layers { get; } + + public Layer? SelectedLayer + { + get => _selectedLayer; + set => this.RaiseAndSetIfChanged(ref _selectedLayer, value); + } + + public KeyPressType RespondTo + { + get => _respondTo; + set => this.RaiseAndSetIfChanged(ref _respondTo, value); + } + + private void GetLayers() + { + Layers.Clear(); + if (_profile == null) + return; + foreach (Layer layer in _profile.GetAllLayers()) + Layers.Add(layer); + + Update(); + } + + private void Update() + { + SelectedLayer = Layers.FirstOrDefault(l => l.EntityId == _node.Storage?.LayerId); + RespondTo = _node.Storage?.RespondTo ?? KeyPressType.Up; + } + + private void UpdateSelectedLayer(Layer? layer) + { + if (layer == null || _node.Storage?.LayerId == layer.EntityId) + return; + + _nodeEditorService.ExecuteCommand( + Script, + new UpdateStorage(_node, new PressedKeyPositionNodeEntity(layer.EntityId, _node.Storage?.RespondTo ?? KeyPressType.Up), "layer") + ); + } + + private void UpdateSelectedRespondTo(KeyPressType respondTo) + { + if (_node.Storage?.RespondTo == respondTo) + return; + + _nodeEditorService.ExecuteCommand(Script, new UpdateStorage(_node, new PressedKeyPositionNodeEntity(_node.Storage?.LayerId ?? Guid.Empty, respondTo), "layer")); + } +} \ No newline at end of file From cbf2cd17367b6a2afbcf9c09218512fc3b2091ba Mon Sep 17 00:00:00 2001 From: Robert Date: Sun, 30 Oct 2022 18:13:47 +0100 Subject: [PATCH 2/2] Nodes - Added color gradient easing node Color ramp node - Fixed 1 and multiples of 1 being treated as 0 --- .../Models/Profile/Colors/ColorGradient.cs | 34 +++++++++ .../Profile/Colors/ColorGradientStop.cs | 6 ++ .../Profile/LayerProperties/LayerProperty.cs | 2 +- .../Nodes/Color/RampSKColorNode.cs | 8 ++- .../Nodes/Easing/ColorGradientEasingNode.cs | 71 +++++++++++++++++++ 5 files changed, 119 insertions(+), 2 deletions(-) create mode 100644 src/Artemis.VisualScripting/Nodes/Easing/ColorGradientEasingNode.cs diff --git a/src/Artemis.Core/Models/Profile/Colors/ColorGradient.cs b/src/Artemis.Core/Models/Profile/Colors/ColorGradient.cs index 18c570c9a..eeb5c3938 100644 --- a/src/Artemis.Core/Models/Profile/Colors/ColorGradient.cs +++ b/src/Artemis.Core/Models/Profile/Colors/ColorGradient.cs @@ -468,6 +468,8 @@ public class ColorGradient : IList, IList, INotifyCollectionC { _stops.Add(item); item.ColorGradient = this; + // Update the position, reapplying the overlap-check in Position's setter + item.Position = item.Position; item.PropertyChanged += ItemOnPropertyChanged; OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Add, item, _stops.IndexOf(item))); @@ -624,4 +626,36 @@ public class ColorGradient : IList, IList, INotifyCollectionC } #endregion + + public ColorGradient Interpolate(ColorGradient targetValue, float progress) + { + ColorGradient interpolated = new(this); + + // Add new stops + if (targetValue.Count > interpolated.Count) + { + // Prefer the stops on a vacant position + foreach (ColorGradientStop stop in targetValue.Take(targetValue.Count - interpolated.Count)) + interpolated.Add(new ColorGradientStop(GetColor(stop.Position), stop.Position)); + } + // Interpolate stops + int index = 0; + foreach (ColorGradientStop stop in interpolated.ToList()) + { + if (index < targetValue.Count) + { + ColorGradientStop targetStop = targetValue[index]; + stop.Interpolate(targetStop, progress); + } + // Interpolate stops not on the target gradient + else + { + stop.Color = stop.Color.Interpolate(targetValue.GetColor(stop.Position), progress); + } + + index++; + } + + return interpolated; + } } \ No newline at end of file diff --git a/src/Artemis.Core/Models/Profile/Colors/ColorGradientStop.cs b/src/Artemis.Core/Models/Profile/Colors/ColorGradientStop.cs index 96af9f2b6..d38fe4598 100644 --- a/src/Artemis.Core/Models/Profile/Colors/ColorGradientStop.cs +++ b/src/Artemis.Core/Models/Profile/Colors/ColorGradientStop.cs @@ -95,4 +95,10 @@ public class ColorGradientStop : CorePropertyChanged } #endregion + + public void Interpolate(ColorGradientStop targetValue, float progress) + { + Color = Color.Interpolate(targetValue.Color, progress); + Position = Position + ((targetValue.Position - Position) * progress); + } } \ No newline at end of file diff --git a/src/Artemis.Core/Models/Profile/LayerProperties/LayerProperty.cs b/src/Artemis.Core/Models/Profile/LayerProperties/LayerProperty.cs index 2772c3821..ff9a7d514 100644 --- a/src/Artemis.Core/Models/Profile/LayerProperties/LayerProperty.cs +++ b/src/Artemis.Core/Models/Profile/LayerProperties/LayerProperty.cs @@ -237,7 +237,7 @@ public class LayerProperty : CorePropertyChanged, ILayerProperty } /// - /// Gets or sets the base value of this layer property without any keyframes applied + /// Gets or sets the base value of this layer property without any keyframes or data bindings applied /// public T BaseValue { diff --git a/src/Artemis.VisualScripting/Nodes/Color/RampSKColorNode.cs b/src/Artemis.VisualScripting/Nodes/Color/RampSKColorNode.cs index b1cc9b183..c70e1489d 100644 --- a/src/Artemis.VisualScripting/Nodes/Color/RampSKColorNode.cs +++ b/src/Artemis.VisualScripting/Nodes/Color/RampSKColorNode.cs @@ -22,7 +22,13 @@ public class RampSKColorNode : Node(); + EasingTime = CreateInputPin("delay"); + EasingFunction = CreateInputPin("function"); + + Output = CreateOutputPin(); + } + + public InputPin Input { get; set; } + public InputPin EasingTime { get; set; } + public InputPin EasingFunction { get; set; } + + public OutputPin Output { get; set; } + + public override void Evaluate() + { + DateTime now = DateTime.Now; + + if (Input.Value == null) + return; + + // If the value changed reset progress + if (!Equals(_targetValue, Input.Value)) + { + _sourceValue = _currentValue ?? new ColorGradient(Input.Value); + _targetValue = new ColorGradient(Input.Value); + _progress = 0f; + } + + // Update until finished + if (_progress < 1f) + { + Update(); + Output.Value = _currentValue; + } + // Stop updating past 1 and use the target value + else + { + Output.Value = _targetValue; + } + + _lastEvaluate = now; + } + + private void Update() + { + if (_sourceValue == null || _targetValue == null) + return; + + float easingTime = EasingTime.Value != 0f ? EasingTime.Value : 1f; + TimeSpan delta = DateTime.Now - _lastEvaluate; + + // In case of odd delta's, keep progress between 0f and 1f + _progress = Math.Clamp(_progress + (float) delta.TotalMilliseconds / easingTime, 0f, 1f); + _currentValue = _sourceValue.Interpolate(_targetValue, (float) Easings.Interpolate(_progress, EasingFunction.Value)); + } +} \ No newline at end of file