feat(blur): universal — the blur covers every layer, webcam included
The creator found the use case that reframes the feature: a moment of privacy while the webcam is on. A blur that can only cover the desktop cannot do that. - SceneCompositor applies the blur group AFTER every element is composited (still before the flash / social bar / ticker / zoom overlays), so it covers webcam, chat, alerts — anything. Its position in the layer list no longer affects the output, so it is added at the top and the reorder clamp is gone. - Supersedes the earlier "blur sits below the brand" rule: a universal blur does obscure the logo when it covers it. Called out in frame-pipeline.md so it is not re-litigated. - Preview mirrors it exactly: the blur host moved OUT of CanvasGrid to a sibling above it, so the VisualBrush samples every layer (and cannot sample itself). It used to sample only the backdrop, which is why a circle over the webcam would have shown sharp pixels in the editor and blurred ones in the stream.
This commit is contained in:
@@ -20,6 +20,11 @@
|
||||
PreviewMouseRightButtonUp="Preview_MouseRightButtonUp">
|
||||
<Viewbox Stretch="Uniform">
|
||||
<Canvas x:Name="PreviewCanvas" Width="1920" Height="1080" ClipToBounds="True">
|
||||
<!-- Privacy blur preview (2026-11-01): the blur is UNIVERSAL — it covers
|
||||
every layer beneath it, so this host sits ABOVE the whole CanvasGrid
|
||||
rather than inside it. Each child samples CanvasGrid (all layers, no
|
||||
recursion because this canvas is a sibling) at the blur's rect and
|
||||
carries a BlurEffect at the output's strength. -->
|
||||
<Grid x:Name="CanvasGrid" Width="1920" Height="1080" ClipToBounds="True" Background="Transparent">
|
||||
<Grid.RenderTransform>
|
||||
<TransformGroup>
|
||||
@@ -71,15 +76,6 @@
|
||||
Visibility="{Binding BackgroundVisible, Converter={StaticResource BoolToVis}}"/>
|
||||
<Image Source="{Binding ActiveBackgroundImage}" Stretch="UniformToFill"/>
|
||||
</Grid>
|
||||
<!-- Privacy blur preview (2026-11-01): whatever is BENEATH a blur layer
|
||||
is blurred, right here in the editor — not a hatched placeholder. Each
|
||||
child is a rectangle/ellipse filled with a VisualBrush of the whole
|
||||
BackdropLayer (the desktop + background art), clipped to the blur's
|
||||
shape and blurred at the same strength the output uses. Hosted AFTER
|
||||
the backdrop and BEFORE the element layers, which is the model's own
|
||||
z-rule: the blur sits above the desktop and below the brand. -->
|
||||
<Canvas x:Name="BlurPreviewHost" Width="1920" Height="1080" IsHitTestVisible="False"/>
|
||||
|
||||
<Image x:Name="TransitionOverlay" Width="1920" Height="1080" Stretch="Fill"
|
||||
IsHitTestVisible="False" Visibility="Collapsed"/>
|
||||
<Canvas x:Name="OverlayCanvas" Width="1920" Height="1080">
|
||||
@@ -474,6 +470,7 @@
|
||||
<VisualBrush x:Name="ZoomVisualBrush" ViewboxUnits="Absolute" Stretch="Fill"/>
|
||||
</Grid.Background>
|
||||
</Grid>
|
||||
<Canvas x:Name="BlurPreviewHost" Width="1920" Height="1080" IsHitTestVisible="False"/>
|
||||
</Canvas>
|
||||
</Viewbox>
|
||||
<Border HorizontalAlignment="Left" VerticalAlignment="Top" Margin="8,8,0,0"
|
||||
|
||||
@@ -740,11 +740,11 @@ public partial class PreviewPane : UserControl
|
||||
}
|
||||
|
||||
// ─── 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.
|
||||
// The editor has to show the blur it is about to bake into the output, and the blur is
|
||||
// UNIVERSAL: it covers every layer, webcam and chat included. Each blur becomes a
|
||||
// rectangle/ellipse filled with a VisualBrush of the whole CanvasGrid sampled at that rect,
|
||||
// with a BlurEffect at the same strength the compositor uses. The host canvas is a SIBLING
|
||||
// above CanvasGrid, which is what stops the brush from sampling itself.
|
||||
private MainViewModel? _blurVm;
|
||||
private Scene? _blurScene;
|
||||
|
||||
@@ -811,7 +811,7 @@ public partial class PreviewPane : UserControl
|
||||
{
|
||||
Width = blur.Width,
|
||||
Height = blur.Height,
|
||||
Fill = new VisualBrush(BackdropLayer)
|
||||
Fill = new VisualBrush(CanvasGrid)
|
||||
{
|
||||
Viewbox = new Rect(blur.X, blur.Y, blur.Width, blur.Height),
|
||||
ViewboxUnits = BrushMappingMode.Absolute,
|
||||
|
||||
@@ -174,25 +174,13 @@ public sealed class SceneCompositor
|
||||
}
|
||||
|
||||
// Composite the above-split layers (dynamic + any static above them) per frame.
|
||||
var blurApplied = false;
|
||||
for (var i = Math.Max(0, split); i < scene.Elements.Count; i++)
|
||||
{
|
||||
var element = scene.Elements[i];
|
||||
if (!element.IsVisible) continue;
|
||||
|
||||
// Privacy blurs are handled as ONE group the first time one is reached: overlapping
|
||||
// boxes must resolve to the strongest single blur, never to a blurred blur. The group
|
||||
// runs at the lowest blur's z-position, which is where the creator put it — directly
|
||||
// above the background and below the brand logo.
|
||||
if (element is Source { Type: SourceType.Blur })
|
||||
{
|
||||
if (!blurApplied)
|
||||
{
|
||||
blurApplied = true;
|
||||
ApplyBlurLayers(scene, buffer, cropW, cropH, options, timings);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
// Blurs are applied once, after every layer (see below) — they are universal.
|
||||
if (element is Source { Type: SourceType.Blur }) continue;
|
||||
|
||||
switch (element)
|
||||
{
|
||||
@@ -207,6 +195,12 @@ public sealed class SceneCompositor
|
||||
BlitBakedElement(buffer, cropW, cropH, options, element, frame, timings, $"#{i} {element.Name}");
|
||||
}
|
||||
|
||||
// ⛔ Privacy blur is UNIVERSAL (creator ruling 2026-11-01): it is applied after every
|
||||
// layer has been composited, so it can cover the webcam, chat, alerts, anything — the
|
||||
// creator's "moment of privacy" case. It is the only layer that ignores its z-position;
|
||||
// overlapping blurs still resolve to the strongest single one.
|
||||
ApplyBlurLayers(scene, buffer, cropW, cropH, options, timings);
|
||||
|
||||
if (flashFrame != null)
|
||||
{
|
||||
var tFlash = System.Diagnostics.Stopwatch.GetTimestamp();
|
||||
|
||||
@@ -81,18 +81,10 @@ public partial class MainViewModel
|
||||
: sourceType == SourceType.Blur ? 160 : 0,
|
||||
};
|
||||
|
||||
if (sourceType == SourceType.Blur)
|
||||
{
|
||||
// ⛔ Z-order is policy, not preference: the blur sits directly above the background and
|
||||
// BELOW the brand logo, which can never be obscured (creator ruling 2026-11-01).
|
||||
_sceneGraph.AddElement(scene, source);
|
||||
var blurIndex = scene.Elements.IndexOf(source);
|
||||
if (blurIndex >= 0) scene.Elements.Move(blurIndex, BlurInsertIndex(scene));
|
||||
}
|
||||
else
|
||||
{
|
||||
_sceneGraph.AddElement(scene, source);
|
||||
}
|
||||
// The blur is universal (creator ruling 2026-11-01): the compositor applies it over every
|
||||
// layer, so its position in the list does not affect the output. It is added at the top so
|
||||
// the layer list reads "this covers everything", and no reorder clamp is needed.
|
||||
_sceneGraph.AddElement(scene, source);
|
||||
|
||||
if (sourceType == SourceType.WebSource)
|
||||
{
|
||||
@@ -133,38 +125,10 @@ public partial class MainViewModel
|
||||
/// X/Y/W/H) in the same Source rows.</summary>
|
||||
public void OnSceneElementsReordered()
|
||||
{
|
||||
// A blur may never end up above the brand logo (creator ruling 2026-11-01). Only offending
|
||||
// blurs move — a legitimate reorder inside the layer list is left exactly as dropped.
|
||||
if (StagedScene is { } scene)
|
||||
{
|
||||
var brand = scene.Elements.OfType<Source>().FirstOrDefault(s => s.IsBrandLogo);
|
||||
if (brand != null)
|
||||
{
|
||||
foreach (var blur in scene.Elements.OfType<Source>().Where(s => s.IsBlur).ToList())
|
||||
{
|
||||
var brandIndex = scene.Elements.IndexOf(brand);
|
||||
var blurIndex = scene.Elements.IndexOf(blur);
|
||||
if (blurIndex > brandIndex) scene.Elements.Move(blurIndex, brandIndex);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (StagedScene != null) _sceneGraph.InvalidateBake(StagedScene);
|
||||
SaveLayoutNow();
|
||||
}
|
||||
|
||||
/// <summary>The slot a privacy blur belongs in: immediately after the background, which puts it
|
||||
/// below the brand logo (the logo is inserted directly above the background too, so the blur
|
||||
/// must land before it).</summary>
|
||||
private static int BlurInsertIndex(Scene scene)
|
||||
{
|
||||
var background = scene.Elements.OfType<Source>().FirstOrDefault(s => s.IsBackground);
|
||||
if (background != null) return scene.Elements.IndexOf(background) + 1;
|
||||
|
||||
var brand = scene.Elements.OfType<Source>().FirstOrDefault(s => s.IsBrandLogo);
|
||||
return brand != null ? scene.Elements.IndexOf(brand) : 0;
|
||||
}
|
||||
|
||||
// Duplicate resource names get an incrementing suffix with no space: Image,
|
||||
// Image2, Image3… The next free number is derived from the names actually in
|
||||
// the scene, so deleting a middle resource never collides with a survivor.
|
||||
|
||||
@@ -788,15 +788,16 @@ Full suite 290/291 passing, the sole failure the pre-existing compositor pixel t
|
||||
bottom) in master space. `MainViewModel` owns the frame (`_socialBarFrame`, rebuilt by `RenderSocialBarFrame`
|
||||
on load/save/`NotifySocialsChanged`).
|
||||
- **Privacy blur layer (2026-11-01):** `SourceType.Blur` is a Live-scene element that blurs the
|
||||
composed-so-far buffer inside a rect or ellipse — it hides part of the creator's desktop without
|
||||
hiding the brand. `SceneCompositor` treats all visible blurs as ONE group the first time one is
|
||||
composed frame inside a rect or ellipse. **It is universal**: `SceneCompositor` applies the blur
|
||||
group AFTER every layer has been composited (before the flash/social/ticker overlays), so it can
|
||||
cover the webcam, chat, alerts — anything — which is the creator's "moment of privacy" case. Its
|
||||
position in the layer list is cosmetic for that reason (it is added at the top), and the earlier
|
||||
"never above the brand" rule is superseded for blurs: a blur over the logo does obscure it. `SceneCompositor` treats all visible blurs as ONE group the first time one is
|
||||
reached in the z-order (`ApplyBlurLayers`), sorts them **strongest first** and passes a shared
|
||||
coverage map to `RegionBlur.ApplyInPlace`, so an overlap resolves to the strongest single blur
|
||||
rather than a blurred blur. `RegionBlur` shares the webcam's radius curve and constants
|
||||
(`Downscale 4`, `MinRadius 1`, `MaxRadius 6`, `RadiusFor(strength)`), so a strength means the
|
||||
same thing in both features; the webcam's `BackgroundEffectProcessor` delegates to it.
|
||||
⛔ Z-order is policy: `AddSource` inserts the blur directly above the background (below the brand
|
||||
logo), and `OnSceneElementsReordered` moves any blur that ends up above the brand back down.
|
||||
Blur is `ElementKind.Dynamic`, so it is never baked into the static base and every render path
|
||||
(pump, preview, thumbnails, scene dump) gets it from the one compositor.
|
||||
- Known consideration: the pump reads the active scene on a background thread while the UI can still edit
|
||||
|
||||
Reference in New Issue
Block a user