Skip to content

Commit b0f5fb1

Browse files
authored
Screen effects like Blur and CRT (#56)
* Implement screen effects: add Blur and CRT options to UI and ViewModel * Update README
1 parent a121f1a commit b0f5fb1

10 files changed

Lines changed: 197 additions & 112 deletions

File tree

README.md

Lines changed: 73 additions & 111 deletions
Large diffs are not rendered by default.

src/Spectron/Controls/MainMenu.axaml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
<converters:ValueEqualityConverter x:Key="ValueEqualityConverter"/>
2121
<converters:GreaterThanConverter x:Key="GreaterThanConverter"/>
2222
<converters:BoolToTextConverter x:Key="BoolToTextConverter"/>
23+
<converters:EnumHasValueConverter x:Key="EnumHasValueConverter"/>
2324
</UserControl.Resources>
2425

2526
<UserControl.Styles>
@@ -123,6 +124,11 @@
123124
<MenuItem Header="Large" ToggleType="Radio" Command="{Binding ChangeBorderSizeCommand}" CommandParameter="{x:Static screen:BorderSize.Large}" IsChecked="{Binding BorderSize, Converter={StaticResource ValueEqualityConverter}, ConverterParameter={x:Static screen:BorderSize.Large}}"/>
124125
<MenuItem Header="Full" ToggleType="Radio" Command="{Binding ChangeBorderSizeCommand}" CommandParameter="{x:Static screen:BorderSize.Full}" IsChecked="{Binding BorderSize, Converter={StaticResource ValueEqualityConverter}, ConverterParameter={x:Static screen:BorderSize.Full}}"/>
125126
</MenuItem>
127+
<MenuItem Header="Effects">
128+
<MenuItem Header="Blur" ToggleType="CheckBox" Command="{Binding ChangeScreenEffectCommand}" CommandParameter="{x:Static screen:ScreenEffect.Blur}" IsChecked="{Binding ScreenEffect, Converter={StaticResource EnumHasValueConverter}, ConverterParameter={x:Static screen:ScreenEffect.Blur}}"/>
129+
<MenuItem Header="CRT" ToggleType="CheckBox" Command="{Binding ChangeScreenEffectCommand}" CommandParameter="{x:Static screen:ScreenEffect.Crt}" IsChecked="{Binding ScreenEffect, Converter={StaticResource EnumHasValueConverter}, ConverterParameter={x:Static screen:ScreenEffect.Crt}}"/>
130+
</MenuItem>
131+
<MenuItem Header="-"/>
126132
<MenuItem Header="Trainers" Command="{Binding ShowTrainersCommand}"/>
127133
<MenuItem Header="Print Output" Command="{Binding ShowPrintOutputCommand}"/>
128134
<MenuItem Header="-"/>

src/Spectron/Controls/NativeMainMenu.cs

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ public sealed class NativeMainMenu
2727
private readonly Dictionary<MouseType, NativeMenuItem> _mouseTypes = new();
2828
private readonly Dictionary<string, NativeMenuItem> _emulationSpeeds = new();
2929
private readonly Dictionary<BorderSize, NativeMenuItem> _borderSizes = new();
30+
private readonly Dictionary<ScreenEffect, NativeMenuItem> _screenEffects = new();
3031
private readonly Dictionary<TapeSpeed, NativeMenuItem> _tapeLoadingSpeeds = new();
3132
private readonly Dictionary<MicrodriveId, NativeMenuItem> _microdrives = new();
3233
private readonly Dictionary<DriveId, NativeMenuItem> _diskDrives = new();
@@ -49,6 +50,7 @@ public NativeMainMenu(MainWindowViewModel viewModel)
4950
CreateMouseTypeMenu();
5051
CreateSpeedOptionMenu();
5152
CreateBorderSizeMenu();
53+
CreateScreenEffectMenu();
5254
CreateTapeLoadingSpeedMenu();
5355

5456
_viewModel.PropertyChanged += (_, e) => ViewModelPropertyChanged(e.PropertyName);
@@ -145,6 +147,14 @@ private void ViewModelPropertyChanged(string? propertyName)
145147

146148
break;
147149

150+
case nameof(MainWindowViewModel.ScreenEffect):
151+
foreach (var screenEffect in _screenEffects.Keys)
152+
{
153+
_screenEffects[screenEffect].IsChecked = _viewModel.ScreenEffect.HasFlag(screenEffect);
154+
}
155+
156+
break;
157+
148158
case nameof(MainWindowViewModel.IsFullScreen):
149159
_fullScreenMenuItem?.Header = _viewModel.IsFullScreen ? "Exit Full Screen" : "Full Screen";
150160

@@ -460,6 +470,17 @@ private NativeMenuItem CreateViewMenu()
460470
]
461471
},
462472

473+
new NativeMenuItem("Effect")
474+
{
475+
Menu =
476+
[
477+
_screenEffects[ScreenEffect.Blur],
478+
_screenEffects[ScreenEffect.Crt],
479+
]
480+
},
481+
482+
new NativeMenuItemSeparator(),
483+
463484
new NativeMenuItem("Trainers")
464485
{
465486
Command = _viewModel.ShowTrainersCommand,
@@ -923,6 +944,27 @@ private void CreateBorderSizeMenu()
923944
}
924945
}
925946

947+
private void CreateScreenEffectMenu()
948+
{
949+
var effects = new[]
950+
{
951+
new { Effect = ScreenEffect.Blur, DisplayName = "Blur" },
952+
new { Effect = ScreenEffect.Crt, DisplayName = "CRT" },
953+
};
954+
955+
foreach (var effect in effects)
956+
{
957+
_screenEffects[effect.Effect] = new NativeMenuItem(effect.DisplayName)
958+
{
959+
ToggleType = NativeMenuItemToggleType.CheckBox,
960+
Command = _viewModel.ChangeScreenEffectCommand,
961+
CommandParameter = effect.Effect,
962+
IsChecked = _viewModel.ScreenEffect.HasFlag(effect.Effect),
963+
IsEnabled = true
964+
};
965+
}
966+
}
967+
926968
private void CreateTapeLoadingSpeedMenu()
927969
{
928970
var speeds = new[]
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
using System;
2+
using System.Globalization;
3+
using Avalonia.Data.Converters;
4+
5+
namespace OldBit.Spectron.Converters;
6+
7+
public class EnumHasValueConverter : IValueConverter
8+
{
9+
public object? Convert(object? value, Type targetType, object? parameter, CultureInfo culture)
10+
{
11+
if (value is not Enum enumValue || parameter is not Enum parameterValue)
12+
{
13+
return false;
14+
}
15+
16+
return enumValue.HasFlag(parameterValue);
17+
}
18+
19+
public object? ConvertBack(object? value, Type targetType, object? parameter, CultureInfo culture) =>
20+
throw new NotImplementedException();
21+
}
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
using System;
2+
3+
namespace OldBit.Spectron.Screen;
4+
5+
[Flags]
6+
public enum ScreenEffect
7+
{
8+
None = 0x00,
9+
Blur = 0x01,
10+
Crt = 0x02,
11+
}

src/Spectron/Settings/Preferences.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,8 @@ public class Preferences
2323

2424
public BorderSize BorderSize { get; set; } = BorderSize.Medium;
2525

26+
public ScreenEffect ScreenEffect { get; set; } = ScreenEffect.None;
27+
2628
public ComputerType ComputerType { get; init; } = ComputerType.Spectrum48K;
2729

2830
public RomType RomType { get; init; } = RomType.Original;

src/Spectron/ViewModels/MainWindowViewModel.Display.cs

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,18 @@ private void HandleChangeBorderSize(BorderSize borderSize)
1717
SpectrumScreen = _frameBufferConverter.ScreenBitmap;
1818
}
1919

20+
private void HandleChangeScreenEffect(ScreenEffect screenEffect)
21+
{
22+
if (ScreenEffect.HasFlag(screenEffect))
23+
{
24+
ScreenEffect &= ~screenEffect;
25+
}
26+
else
27+
{
28+
ScreenEffect |= screenEffect;
29+
}
30+
}
31+
2032
private void HandleToggleFullScreen() =>
2133
WindowState = WindowState == WindowState.FullScreen ? WindowState.Normal : WindowState.FullScreen;
2234

src/Spectron/ViewModels/MainWindowViewModel.Window.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,7 @@ private async Task WindowOpenedAsync()
6161

6262
ConfigureShiftKeys(_preferences.Keyboard);
6363
HandleChangeBorderSize(CommandLineArgs?.BorderSize ?? _preferences.BorderSize);
64+
HandleChangeScreenEffect(_preferences.ScreenEffect);
6465

6566
TapeLoadSpeed = CommandLineArgs?.TapeLoadSpeed ?? _preferences.Tape.LoadSpeed;
6667

@@ -191,6 +192,7 @@ private async Task WindowClosingAsync(WindowClosingEventArgs args)
191192

192193
_preferences.Audio.IsMuted = IsAudioMuted;
193194
_preferences.BorderSize = BorderSize;
195+
_preferences.ScreenEffect = ScreenEffect;
194196

195197
await Task.WhenAll(
196198
_preferencesService.SaveAsync(_preferences),

src/Spectron/ViewModels/MainWindowViewModel.cs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -104,6 +104,9 @@ public partial class MainWindowViewModel : ObservableObject
104104
[ObservableProperty]
105105
public partial BorderSize BorderSize { get; set; } = BorderSize.Medium;
106106

107+
[ObservableProperty]
108+
public partial ScreenEffect ScreenEffect { get; set; } = ScreenEffect.None;
109+
107110
[ObservableProperty]
108111
public partial RomType RomType { get; set; } = RomType.Original;
109112

@@ -298,6 +301,9 @@ private async Task TakeScreenshot()
298301
[RelayCommand]
299302
private void ChangeBorderSize(BorderSize borderSize) => HandleChangeBorderSize(borderSize);
300303

304+
[RelayCommand]
305+
private void ChangeScreenEffect(ScreenEffect screenEffect) => HandleChangeScreenEffect(screenEffect);
306+
301307
[RelayCommand]
302308
private void ShowTrainers() => OpenTrainersWindow();
303309

src/Spectron/Views/MainWindow.axaml

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
xmlns:viewModels="clr-namespace:OldBit.Spectron.ViewModels"
77
xmlns:converters="clr-namespace:OldBit.Spectron.Converters"
88
xmlns:controls="clr-namespace:OldBit.Spectron.Controls"
9+
xmlns:screen="clr-namespace:OldBit.Spectron.Screen"
910
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
1011
x:Class="OldBit.Spectron.Views.MainWindow"
1112
x:DataType="viewModels:MainWindowViewModel"
@@ -35,6 +36,8 @@
3536

3637
<Window.Resources>
3738
<converters:ValueInequalityConverter x:Key="ValueInequalityConverter"/>
39+
<converters:ValueEqualityConverter x:Key="ValueEqualityConverter"/>
40+
<converters:EnumHasValueConverter x:Key="EnumHasValueConverter"/>
3841
</Window.Resources>
3942

4043
<Window.KeyBindings>
@@ -91,15 +94,33 @@
9194
PointerMoved="Screen_OnPointerMoved"
9295
PointerPressed="Screen_OnPointerPressed"
9396
PointerReleased="Screen_OnPointerReleased"
94-
Classes.IsPaused="{Binding IsPauseOverlayVisible}">
97+
Classes.IsPaused="{Binding IsPauseOverlayVisible}"
98+
Classes.Blur="{Binding ScreenEffect, Converter={StaticResource EnumHasValueConverter}, ConverterParameter={x:Static screen:ScreenEffect.Blur}}">
9599
<Image.Styles>
100+
<Style Selector="Image.Blur">
101+
<Setter Property="Effect">
102+
<BlurEffect Radius="3"/>
103+
</Setter>
104+
</Style>
96105
<Style Selector="Image.IsPaused">
97106
<Setter Property="Effect">
98107
<BlurEffect Radius="7"/>
99108
</Setter>
100109
</Style>
101110
</Image.Styles>
102111
</Image>
112+
113+
<Border IsVisible="{Binding ScreenEffect, Converter={StaticResource EnumHasValueConverter}, ConverterParameter={x:Static screen:ScreenEffect.Crt}}">
114+
<Border.Background>
115+
<VisualBrush TileMode="Tile" SourceRect="0,0,1,2" DestinationRect="0,0,1,2">
116+
<VisualBrush.Visual>
117+
<Canvas Width="1" Height="2">
118+
<Rectangle Width="1" Height="1" Fill="Black" Opacity=".3"/>
119+
</Canvas>
120+
</VisualBrush.Visual>
121+
</VisualBrush>
122+
</Border.Background>
123+
</Border>
103124
</Grid>
104125
</Border>
105126

0 commit comments

Comments
 (0)