using System.ComponentModel; using System.Globalization; using System.Runtime.CompilerServices; using System.Windows.Media; using System.Windows.Media.Imaging; namespace ytLive.Models; public enum BorderAnimation { None, Pulse, Chase, Rainbow, Shimmer, MarchingAnts, Glow, Electricity, Sparkles } /// /// A thing rendered in a scene: a multi-instance Source (image/background/text) /// or a singleton resource's per-scene config (webcam). Carries the shared /// layout + clip/mirror + border surface the preview pipeline renders from. /// public abstract class SceneElement : INotifyPropertyChanged { public event PropertyChangedEventHandler? PropertyChanged; protected void Raise([CallerMemberName] string? name = null) => PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(name)); protected bool Set(ref T field, T value, [CallerMemberName] string? name = null) { if (EqualityComparer.Default.Equals(field, value)) return false; field = value; Raise(name); return true; } public string Id { get; init; } = Guid.NewGuid().ToString(); private string _name = string.Empty; public string Name { get => _name; set => Set(ref _name, value); } /// True only for singleton resources (webcam). Gates webcam-only UI. public virtual bool IsWebcam => false; /// True only for image sources — the raw preview renderer's discriminator. public virtual bool IsImageSource => false; private bool _isVisible = true; public bool IsVisible { get => _isVisible; set => Set(ref _isVisible, value); } private ClipShape _clipShape = ClipShape.Traditional; public ClipShape ClipShape { get => _clipShape; set { if (Set(ref _clipShape, value)) Raise(nameof(ShapeButtonText)); } } // The rectangular dimensions before a Round toggle, so switching back restores // the aspect instead of staying locked to the square Round used. Persisted so // the restore survives a reload of a Round element. private double? _rectWidth; private double? _rectHeight; public double? RectWidth { get => _rectWidth; set => Set(ref _rectWidth, value); } public double? RectHeight { get => _rectHeight; set => Set(ref _rectHeight, value); } public void ToggleClipShape() { if (ClipShape == ClipShape.Traditional) { RectWidth = Width; RectHeight = Height; ClipShape = ClipShape.Round; } else { ClipShape = ClipShape.Traditional; if (RectWidth is { } rw && RectHeight is { } rh) { Width = rw; Height = rh; RectWidth = null; RectHeight = null; } } } private bool _isMirrored; public bool IsMirrored { get => _isMirrored; set { if (Set(ref _isMirrored, value)) { Raise(nameof(MirrorScale)); Raise(nameof(MirrorButtonText)); } } } public double MirrorScale => IsMirrored ? -1 : 1; public string MirrorButtonText => IsMirrored ? "Unmirror" : "Mirror"; public string ShapeButtonText => ClipShape == ClipShape.Round ? "Rect" : "Round"; // Live camera frames: the shared WriteableBitmap owned by CameraManager. private WriteableBitmap? _videoImageSource; public WriteableBitmap? VideoImageSource { get => _videoImageSource; set { if (Set(ref _videoImageSource, value)) Raise(nameof(DisplaySource)); } } /// What the preview shows: live frames (webcam) or a static image (sources). public abstract ImageSource? DisplaySource { get; } // Position/transform (per-scene usage) private double _x; public double X { get => _x; set => Set(ref _x, value); } private double _y; public double Y { get => _y; set => Set(ref _y, value); } private double _width; public double Width { get => _width; set { if (Set(ref _width, value)) Raise(nameof(RoundBorderSize)); } } private double _height; public double Height { get => _height; set { if (Set(ref _height, value)) Raise(nameof(RoundBorderSize)); } } private double _opacity = 1.0; public double Opacity { get => _opacity; set => Set(ref _opacity, value); } /// Round clip/border diameter = the shorter dimension (true circle). public double RoundBorderSize => Math.Min(Width, Height); // Border (OBS-style): #RRGGBB color, alpha opacity, pixel width. Off by default. private string _borderColor = string.Empty; public string BorderColor { get => _borderColor; set { if (Set(ref _borderColor, value)) { Raise(nameof(HasBorder)); Raise(nameof(BorderBrush)); } } } private double _borderOpacity = 1.0; public double BorderOpacity { get => _borderOpacity; set { if (Set(ref _borderOpacity, value)) { Raise(nameof(HasBorder)); Raise(nameof(BorderBrush)); } } } private int _borderWidth; public int BorderWidth { get => _borderWidth; set { if (Set(ref _borderWidth, value)) { Raise(nameof(HasBorder)); Raise(nameof(BorderBrush)); } } } private BorderAnimation _borderAnimation = BorderAnimation.None; public BorderAnimation BorderAnimation { get => _borderAnimation; set => Set(ref _borderAnimation, value); } public bool HasBorder { get { if (BorderWidth <= 0) return false; return TryGetBorderColor(out _, out _, out _); } } public Brush? BorderBrush { get { if (!HasBorder || !TryGetBorderColor(out var r, out var g, out var b)) return null; var alpha = (byte)Math.Round(Math.Clamp(BorderOpacity, 0, 1) * 255); return new SolidColorBrush(Color.FromArgb(alpha, r, g, b)); } } private bool TryGetBorderColor(out byte r, out byte g, out byte b) { r = g = b = 0; var hex = BorderColor.Trim().TrimStart('#'); if (hex.Length != 6) return false; return byte.TryParse(hex.AsSpan(0, 2), NumberStyles.HexNumber, CultureInfo.InvariantCulture, out r) && byte.TryParse(hex.AsSpan(2, 2), NumberStyles.HexNumber, CultureInfo.InvariantCulture, out g) && byte.TryParse(hex.AsSpan(4, 2), NumberStyles.HexNumber, CultureInfo.InvariantCulture, out b); } }