Skip to content

Commit 1040217

Browse files
author
devpossible
committed
feat(services): add transition registry and default implementations
feat(cli): implement sensor commands for listing, reading, and watching feat(cli): create CLI context and router for command handling feat(plugins): add hardware sensor provider for system metrics
1 parent 69301f5 commit 1040217

38 files changed

Lines changed: 2875 additions & 348 deletions
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
using LCDPossible.Core.Services;
2+
using SixLabors.ImageSharp;
3+
using SixLabors.ImageSharp.PixelFormats;
4+
5+
namespace LCDPossible.Core.Effects;
6+
7+
/// <summary>
8+
/// Abstract base class for visual post-processing effects.
9+
/// </summary>
10+
public abstract class BaseEffect : IVisualEffect
11+
{
12+
/// <inheritdoc />
13+
public abstract string EffectId { get; }
14+
15+
/// <inheritdoc />
16+
public abstract string DisplayName { get; }
17+
18+
/// <inheritdoc />
19+
public bool IsEnabled { get; set; } = true;
20+
21+
/// <inheritdoc />
22+
public float Intensity { get; set; } = 1.0f;
23+
24+
/// <inheritdoc />
25+
public abstract void Apply(Image<Rgba32> image);
26+
27+
/// <summary>
28+
/// Linear interpolation between two values based on intensity.
29+
/// </summary>
30+
protected float Lerp(float a, float b, float t) => a + (b - a) * t;
31+
32+
/// <summary>
33+
/// Clamps a value between 0 and 255.
34+
/// </summary>
35+
protected byte ClampByte(float value) => (byte)Math.Clamp(value, 0, 255);
36+
37+
/// <inheritdoc />
38+
public virtual void Dispose()
39+
{
40+
GC.SuppressFinalize(this);
41+
}
42+
}
Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
using SixLabors.ImageSharp;
2+
using SixLabors.ImageSharp.PixelFormats;
3+
4+
namespace LCDPossible.Core.Effects;
5+
6+
/// <summary>
7+
/// Applies a color tint/overlay to the image.
8+
/// </summary>
9+
public sealed class ColorTintEffect : BaseEffect
10+
{
11+
/// <inheritdoc />
12+
public override string EffectId => "color-tint";
13+
14+
/// <inheritdoc />
15+
public override string DisplayName => "Color Tint";
16+
17+
/// <summary>
18+
/// The tint color. Default is cyan (0, 212, 255).
19+
/// </summary>
20+
public Rgba32 TintColor { get; set; } = new(0, 212, 255);
21+
22+
/// <summary>
23+
/// Blend mode: "multiply", "overlay", or "screen". Default is "multiply".
24+
/// </summary>
25+
public string BlendMode { get; set; } = "multiply";
26+
27+
/// <inheritdoc />
28+
public override void Apply(Image<Rgba32> image)
29+
{
30+
if (!IsEnabled || Intensity <= 0) return;
31+
32+
var tintR = TintColor.R / 255f;
33+
var tintG = TintColor.G / 255f;
34+
var tintB = TintColor.B / 255f;
35+
36+
image.ProcessPixelRows(accessor =>
37+
{
38+
for (var y = 0; y < accessor.Height; y++)
39+
{
40+
var row = accessor.GetRowSpan(y);
41+
for (var x = 0; x < row.Length; x++)
42+
{
43+
ref var pixel = ref row[x];
44+
var r = pixel.R / 255f;
45+
var g = pixel.G / 255f;
46+
var b = pixel.B / 255f;
47+
48+
float newR, newG, newB;
49+
50+
switch (BlendMode.ToLowerInvariant())
51+
{
52+
case "screen":
53+
newR = 1 - (1 - r) * (1 - tintR);
54+
newG = 1 - (1 - g) * (1 - tintG);
55+
newB = 1 - (1 - b) * (1 - tintB);
56+
break;
57+
58+
case "overlay":
59+
newR = r < 0.5f ? 2 * r * tintR : 1 - 2 * (1 - r) * (1 - tintR);
60+
newG = g < 0.5f ? 2 * g * tintG : 1 - 2 * (1 - g) * (1 - tintG);
61+
newB = b < 0.5f ? 2 * b * tintB : 1 - 2 * (1 - b) * (1 - tintB);
62+
break;
63+
64+
case "multiply":
65+
default:
66+
newR = r * tintR;
67+
newG = g * tintG;
68+
newB = b * tintB;
69+
break;
70+
}
71+
72+
// Blend with original based on intensity
73+
pixel.R = ClampByte(Lerp(pixel.R, newR * 255f, Intensity));
74+
pixel.G = ClampByte(Lerp(pixel.G, newG * 255f, Intensity));
75+
pixel.B = ClampByte(Lerp(pixel.B, newB * 255f, Intensity));
76+
}
77+
}
78+
});
79+
}
80+
}
Lines changed: 115 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,115 @@
1+
using LCDPossible.Core.Services;
2+
3+
namespace LCDPossible.Core.Effects;
4+
5+
/// <summary>
6+
/// Default implementation of the effect registry with built-in effects.
7+
/// </summary>
8+
public sealed class EffectRegistry : IEffectRegistry, IDisposable
9+
{
10+
private readonly Dictionary<string, Func<IVisualEffect>> _effectFactories = new(StringComparer.OrdinalIgnoreCase);
11+
private readonly List<IVisualEffect> _activeEffects = [];
12+
private bool _disposed;
13+
14+
/// <summary>
15+
/// Creates a new effect registry with built-in effects registered.
16+
/// </summary>
17+
public EffectRegistry()
18+
{
19+
// Register built-in effects
20+
RegisterEffect("vignette", () => new VignetteEffect());
21+
RegisterEffect("scanlines", () => new ScanlinesEffect());
22+
RegisterEffect("noise", () => new NoiseEffect());
23+
RegisterEffect("glow", () => new GlowEffect());
24+
RegisterEffect("color-tint", () => new ColorTintEffect());
25+
}
26+
27+
/// <summary>
28+
/// Registers an effect factory.
29+
/// </summary>
30+
/// <param name="effectId">Unique effect identifier.</param>
31+
/// <param name="factory">Factory function to create effect instances.</param>
32+
public void RegisterEffect(string effectId, Func<IVisualEffect> factory)
33+
{
34+
_effectFactories[effectId] = factory;
35+
}
36+
37+
/// <inheritdoc />
38+
public IReadOnlyList<EffectTypeInfo> GetEffectTypes()
39+
{
40+
var types = new List<EffectTypeInfo>();
41+
42+
foreach (var (id, factory) in _effectFactories)
43+
{
44+
using var effect = factory();
45+
types.Add(new EffectTypeInfo(id, effect.DisplayName));
46+
}
47+
48+
return types;
49+
}
50+
51+
/// <inheritdoc />
52+
public IVisualEffect? CreateEffect(string effectId)
53+
{
54+
return _effectFactories.TryGetValue(effectId, out var factory) ? factory() : null;
55+
}
56+
57+
/// <inheritdoc />
58+
public IReadOnlyList<IVisualEffect> ActiveEffects => _activeEffects.AsReadOnly();
59+
60+
/// <inheritdoc />
61+
public void AddEffect(string effectId)
62+
{
63+
var effect = CreateEffect(effectId);
64+
if (effect != null)
65+
{
66+
_activeEffects.Add(effect);
67+
}
68+
}
69+
70+
/// <inheritdoc />
71+
public void RemoveEffect(string effectId)
72+
{
73+
var effect = _activeEffects.FirstOrDefault(e => e.EffectId.Equals(effectId, StringComparison.OrdinalIgnoreCase));
74+
if (effect != null)
75+
{
76+
_activeEffects.Remove(effect);
77+
effect.Dispose();
78+
}
79+
}
80+
81+
/// <inheritdoc />
82+
public void ClearEffects()
83+
{
84+
foreach (var effect in _activeEffects)
85+
{
86+
effect.Dispose();
87+
}
88+
_activeEffects.Clear();
89+
}
90+
91+
/// <summary>
92+
/// Applies all active effects to an image in order.
93+
/// </summary>
94+
/// <param name="image">The image to apply effects to.</param>
95+
public void ApplyAll(SixLabors.ImageSharp.Image<SixLabors.ImageSharp.PixelFormats.Rgba32> image)
96+
{
97+
foreach (var effect in _activeEffects)
98+
{
99+
if (effect.IsEnabled)
100+
{
101+
effect.Apply(image);
102+
}
103+
}
104+
}
105+
106+
/// <inheritdoc />
107+
public void Dispose()
108+
{
109+
if (_disposed) return;
110+
_disposed = true;
111+
112+
ClearEffects();
113+
GC.SuppressFinalize(this);
114+
}
115+
}
Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
using SixLabors.ImageSharp;
2+
using SixLabors.ImageSharp.PixelFormats;
3+
using SixLabors.ImageSharp.Processing;
4+
5+
namespace LCDPossible.Core.Effects;
6+
7+
/// <summary>
8+
/// Applies a bloom/glow effect to bright areas of the image.
9+
/// </summary>
10+
public sealed class GlowEffect : BaseEffect
11+
{
12+
/// <inheritdoc />
13+
public override string EffectId => "glow";
14+
15+
/// <inheritdoc />
16+
public override string DisplayName => "Glow";
17+
18+
/// <summary>
19+
/// Blur radius for the glow effect. Default is 10.
20+
/// </summary>
21+
public float BlurRadius { get; set; } = 10f;
22+
23+
/// <summary>
24+
/// Brightness threshold for glow (0-255). Pixels brighter than this will glow. Default is 180.
25+
/// </summary>
26+
public byte Threshold { get; set; } = 180;
27+
28+
/// <inheritdoc />
29+
public override void Apply(Image<Rgba32> image)
30+
{
31+
if (!IsEnabled || Intensity <= 0) return;
32+
33+
// Create a copy for the glow layer
34+
using var glowLayer = image.Clone();
35+
36+
// Extract bright areas
37+
glowLayer.ProcessPixelRows(accessor =>
38+
{
39+
for (var y = 0; y < accessor.Height; y++)
40+
{
41+
var row = accessor.GetRowSpan(y);
42+
for (var x = 0; x < row.Length; x++)
43+
{
44+
ref var pixel = ref row[x];
45+
var brightness = (pixel.R + pixel.G + pixel.B) / 3;
46+
47+
if (brightness < Threshold)
48+
{
49+
pixel = new Rgba32(0, 0, 0, 0);
50+
}
51+
}
52+
}
53+
});
54+
55+
// Apply blur to the glow layer
56+
glowLayer.Mutate(ctx => ctx.GaussianBlur(BlurRadius * Intensity));
57+
58+
// Blend the glow layer back onto the original using additive blending
59+
image.ProcessPixelRows(glowLayer, (originalAccessor, glowAccessor) =>
60+
{
61+
for (var y = 0; y < originalAccessor.Height; y++)
62+
{
63+
var originalRow = originalAccessor.GetRowSpan(y);
64+
var glowRow = glowAccessor.GetRowSpan(y);
65+
66+
for (var x = 0; x < originalRow.Length; x++)
67+
{
68+
ref var original = ref originalRow[x];
69+
var glow = glowRow[x];
70+
71+
// Additive blend with intensity
72+
original.R = ClampByte(original.R + glow.R * Intensity);
73+
original.G = ClampByte(original.G + glow.G * Intensity);
74+
original.B = ClampByte(original.B + glow.B * Intensity);
75+
}
76+
}
77+
});
78+
}
79+
}
Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
using SixLabors.ImageSharp;
2+
using SixLabors.ImageSharp.PixelFormats;
3+
4+
namespace LCDPossible.Core.Effects;
5+
6+
/// <summary>
7+
/// Applies film grain/noise effect to the image.
8+
/// </summary>
9+
public sealed class NoiseEffect : BaseEffect
10+
{
11+
private readonly Random _random = new();
12+
13+
/// <inheritdoc />
14+
public override string EffectId => "noise";
15+
16+
/// <inheritdoc />
17+
public override string DisplayName => "Film Grain";
18+
19+
/// <summary>
20+
/// Amount of noise to apply (0.0 to 1.0). Default is 0.1.
21+
/// </summary>
22+
public float Amount { get; set; } = 0.1f;
23+
24+
/// <summary>
25+
/// Whether to use monochrome noise (true) or color noise (false). Default is true.
26+
/// </summary>
27+
public bool Monochrome { get; set; } = true;
28+
29+
/// <inheritdoc />
30+
public override void Apply(Image<Rgba32> image)
31+
{
32+
if (!IsEnabled || Intensity <= 0) return;
33+
34+
var effectiveAmount = Amount * Intensity * 255f;
35+
36+
image.ProcessPixelRows(accessor =>
37+
{
38+
for (var y = 0; y < accessor.Height; y++)
39+
{
40+
var row = accessor.GetRowSpan(y);
41+
for (var x = 0; x < row.Length; x++)
42+
{
43+
ref var pixel = ref row[x];
44+
45+
if (Monochrome)
46+
{
47+
var noise = (float)(_random.NextDouble() * 2 - 1) * effectiveAmount;
48+
pixel.R = ClampByte(pixel.R + noise);
49+
pixel.G = ClampByte(pixel.G + noise);
50+
pixel.B = ClampByte(pixel.B + noise);
51+
}
52+
else
53+
{
54+
pixel.R = ClampByte(pixel.R + (float)(_random.NextDouble() * 2 - 1) * effectiveAmount);
55+
pixel.G = ClampByte(pixel.G + (float)(_random.NextDouble() * 2 - 1) * effectiveAmount);
56+
pixel.B = ClampByte(pixel.B + (float)(_random.NextDouble() * 2 - 1) * effectiveAmount);
57+
}
58+
}
59+
}
60+
});
61+
}
62+
}

0 commit comments

Comments
 (0)