From 1416548f2495fcefb8d8275325df7b5b7eb495c5 Mon Sep 17 00:00:00 2001 From: gramps Date: Sun, 1 Nov 2026 13:12:27 -0800 Subject: [PATCH] =?UTF-8?q?feat(blur):=20the=20editor=20shows=20the=20blur?= =?UTF-8?q?=20=E2=80=94=20whatever=20is=20beneath=20it=20is=20blurred,=20i?= =?UTF-8?q?n=20the=20preview?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The preview canvas is WPF visuals; a blur layer contributes no visual of its own, so until now the creator saw nothing where the blur was and could not tell what it was hiding. Each visible blur now draws a rectangle (or ellipse) filled with a VisualBrush of the whole BackdropLayer — the desktop capture plus the background art — sampled at the blur's own rect and carrying a BlurEffect at the same strength the compositor uses. The host canvas sits after the backdrop and before the element layers, which is the model's own z-rule, so the preview shows exactly what the output will: desktop blurred, brand and everything above it untouched. Rebuilt on scene change, on add/remove, and on any blur property change (position, size, shape, strength) — the brush samples the live backdrop, so the picture keeps moving underneath. --- Controls/PreviewPane.xaml | 9 ++++ Controls/PreviewPane.xaml.cs | 92 ++++++++++++++++++++++++++++++++++++ 2 files changed, 101 insertions(+) diff --git a/Controls/PreviewPane.xaml b/Controls/PreviewPane.xaml index 98b20fc..9f96054 100644 --- a/Controls/PreviewPane.xaml +++ b/Controls/PreviewPane.xaml @@ -71,6 +71,15 @@ Visibility="{Binding BackgroundVisible, Converter={StaticResource BoolToVis}}"/> + + + diff --git a/Controls/PreviewPane.xaml.cs b/Controls/PreviewPane.xaml.cs index a8a480c..b501f7d 100644 --- a/Controls/PreviewPane.xaml.cs +++ b/Controls/PreviewPane.xaml.cs @@ -47,6 +47,7 @@ public partial class PreviewPane : UserControl newVm.PreviewTransition += PlayPreviewTransition; UpdateZoomBox(); UpdateSelectedLayerZOrder(); + HookBlurPreview(); } } @@ -737,4 +738,95 @@ public partial class PreviewPane : UserControl if (DataContext is MainViewModel vm) vm.ToggleSettingsDrawer(); e.Handled = true; } + + // ─── Privacy blur preview (2026-11-01) ─── + // The editor has to show the blur it is about to bake into the output: "whatever is beneath the + // blur layer is blurred". Each blur becomes a rectangle/ellipse filled with a VisualBrush of the + // whole BackdropLayer (desktop + background art) sampled at that rect, with a BlurEffect at the + // same strength the compositor uses. The host canvas sits between the backdrop and the element + // layers, which is the model's own z-rule. + private MainViewModel? _blurVm; + private Scene? _blurScene; + + private void HookBlurPreview() + { + if (_blurVm != null) _blurVm.PropertyChanged -= BlurViewModelChanged; + _blurVm = DataContext as MainViewModel; + if (_blurVm != null) _blurVm.PropertyChanged += BlurViewModelChanged; + HookBlurScene(); + } + + private void BlurViewModelChanged(object? sender, System.ComponentModel.PropertyChangedEventArgs e) + { + if (e.PropertyName is nameof(MainViewModel.StagedScene)) HookBlurScene(); + else if (e.PropertyName is nameof(MainViewModel.ActiveBackgroundImage) + or nameof(MainViewModel.BackgroundImage)) RebuildBlurPreview(); + } + + private void HookBlurScene() + { + if (_blurScene != null) + { + _blurScene.Elements.CollectionChanged -= BlurElementsChanged; + foreach (var element in _blurScene.Elements) element.PropertyChanged -= BlurElementChanged; + } + + _blurScene = _blurVm?.StagedScene; + + if (_blurScene != null) + { + _blurScene.Elements.CollectionChanged += BlurElementsChanged; + foreach (var element in _blurScene.Elements) element.PropertyChanged += BlurElementChanged; + } + + RebuildBlurPreview(); + } + + private void BlurElementsChanged(object? sender, System.Collections.Specialized.NotifyCollectionChangedEventArgs e) + { + foreach (var element in e.OldItems?.OfType() ?? System.Linq.Enumerable.Empty()) + element.PropertyChanged -= BlurElementChanged; + foreach (var element in e.NewItems?.OfType() ?? System.Linq.Enumerable.Empty()) + element.PropertyChanged += BlurElementChanged; + RebuildBlurPreview(); + } + + private void BlurElementChanged(object? sender, System.ComponentModel.PropertyChangedEventArgs e) + { + if (sender is Source { IsBlur: true }) RebuildBlurPreview(); + } + + /// Rebuild one overlay shape per visible blur. Cheap by design: a handful of shapes, + /// and the VisualBrush samples the live backdrop so the preview keeps moving underneath. + private void RebuildBlurPreview() + { + if (BlurPreviewHost == null) return; + BlurPreviewHost.Children.Clear(); + if (_blurScene == null) return; + + foreach (var blur in _blurScene.Elements.OfType() + .Where(s => s.IsBlur && s.IsVisible && s.Width > 0 && s.Height > 0)) + { + var shape = new Rectangle + { + Width = blur.Width, + Height = blur.Height, + Fill = new VisualBrush(BackdropLayer) + { + Viewbox = new Rect(blur.X, blur.Y, blur.Width, blur.Height), + ViewboxUnits = BrushMappingMode.Absolute, + Stretch = Stretch.Fill, + }, + // 4..24 px at master scale, the same shape of curve the compositor's radius uses. + Effect = new BlurEffect { Radius = 4 + 20 * Math.Clamp(blur.BlurAmount, 0, 1), KernelType = KernelType.Gaussian }, + }; + + if (blur.ClipShape == ClipShape.Round) + shape.Clip = new EllipseGeometry(new Rect(0, 0, blur.Width, blur.Height)); + + Canvas.SetLeft(shape, blur.X); + Canvas.SetTop(shape, blur.Y); + BlurPreviewHost.Children.Add(shape); + } + } }