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:
2026-11-01 13:29:15 -08:00
parent bb80ec8370
commit bb51164f2f
5 changed files with 29 additions and 73 deletions
+4 -40
View File
@@ -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.