Skip to content

feat(compositor): add circular crop for Loom-style webcam PIP overlays#176

Merged
streamer45 merged 6 commits intomainfrom
devin/1773952178-compositor-circle-crop
Mar 20, 2026
Merged

feat(compositor): add circular crop for Loom-style webcam PIP overlays#176
streamer45 merged 6 commits intomainfrom
devin/1773952178-compositor-circle-crop

Conversation

@staging-devin-ai-integration
Copy link
Contributor

@staging-devin-ai-integration staging-devin-ai-integration bot commented Mar 19, 2026

Summary

Add circular cropping to the compositor node for Loom-style webcam PIP overlays, using an extensible CropShape enum instead of a boolean.

Backend (Rust)

  • CropShape enum (Rect | Circle) with serde(rename_all = "snake_case") and #[default] Rect — extensible to RoundedRect, Hexagon, etc.
  • Ellipse-masked blit with anti-aliased edges (smoothstep band ~1.5px) in both axis-aligned and rotated paths
  • skip_clear regression fix — disabled when crop_shape != Rect so pooled buffer data doesn't leak in ellipse corners
  • 4 new Rust tests including a pooled-buffer regression test

Frontend (React/TypeScript)

  • Segmented control (▭ Rect / ● Circle) replaces the boolean On/Off toggle — matches existing MirrorButton design patterns
  • cropShape: 'rect' | 'circle' throughout LayerState, parsers, atoms, overlays, server sync, canvas layers
  • CSS borderRadius: 50% canvas preview for instant WYSIWYG feedback
  • mergeOverlayState comparator includes cropShape (fixes silent drop of remote-only changes)

Demo pipeline

  • samples/pipelines/dynamic/video_moq_webcam_circle_pip.yml — screen-share + circular webcam PIP

Files changed (22 files, ~600 additions)

Backend: config.rs, kernel.rs, mod.rs, blit.rs, tests.rs, compositor_only.rs (bench)
Frontend: compositor-types.ts, compositorConstants.ts, compositorLayerParsers.ts, compositorAtoms.ts, compositorOverlays.ts, useCompositorLayers.ts, compositorNodeWidgets.tsx, compositorNodeInspector.tsx, compositorCanvasLayers.tsx, compositorServerSync.ts
Tests: compositorDragResize.test.ts, useCompositorLayers.monitor-flow.test.ts
Pipeline: video_moq_webcam_circle_pip.yml

Review & Testing Checklist for Human

  • Ellipse AA quality: Load the demo pipeline with a webcam source and verify the circle crop edge looks smooth at various zoom levels — the smoothstep band is ~1.5px which may need tuning for very small or very large layers
  • Segmented control UX: Select a video layer in the compositor inspector → verify the ▭ Rect / ● Circle toggle in Crop & Zoom section works, resets correctly, and matches the existing MirrorButton style
  • Canvas preview matches backend: Toggle circle crop on a video layer → verify the CSS borderRadius: 50% preview matches the actual composited output in Stream view
  • Backwards compatibility: Load an existing pipeline YAML that has no crop_shape field → verify it defaults to rect (no regression)
  • Skip-clear correctness: With a single full-canvas circle-cropped layer, verify corners show transparent (black) not stale buffer data

Recommended test plan

  1. Start backend + UI locally (just skit + just ui)
  2. Load video_moq_webcam_circle_pip.yml pipeline
  3. In the compositor, select the PIP layer → toggle between Rect/Circle → verify preview and stream output
  4. Resize the PIP layer to non-square → verify ellipse (not circle) clipping
  5. Apply rotation + circle crop → verify rotated ellipse renders correctly

Notes

  • The blit functions (blit.rs) still use crop_circle: bool internally — the kernel converts enum→bool at the call site to avoid touching the complex SIMD code
  • Three bugs caught and fixed from automated review: skip_clear, mergeOverlayState comparator, overflow:hidden clipping resize handles
  • CropShape derives PartialEq, Eq for comparison in skip_clear and blit branching

Link to Devin session: https://staging.itsdev.in/sessions/9615e2650647434e84e4147265a8f38c
Requested by: @streamer45


Staging: Open in Devin

Add a crop_circle boolean field to compositor LayerConfig that clips
the composited layer to an ellipse inscribed in the destination rect.
When the rect is square this produces a perfect circle — ideal for
webcam PIP overlays in the style of Loom.

Backend:
- config.rs, kernel.rs, mod.rs: thread crop_circle through LayerConfig,
  ResolvedLayer, LayerSnapshot, CompositeItem, and resolve_scene
- blit.rs: add ellipse-masked blit path with anti-aliased edges (~1.5px
  smoothstep band) to both scale_blit_rgba and scale_blit_rgba_rotated;
  disable SIMD fast-forward when crop_circle is active
- tests.rs: add 3 new tests for axis-aligned, rotated, and full
  composite_frame circular crop

Frontend:
- compositor-types.ts: add crop_circle to LayerConfig and ResolvedLayer
- compositorConstants.ts: DEFAULT_CROP_CIRCLE = false
- compositorLayerParsers.ts: parse/serialize cropCircle <-> crop_circle
- compositorAtoms.ts: include cropCircle in layerEqual comparison
- compositorOverlays.ts, useCompositorLayers.ts: thread cropCircle
- compositorNodeWidgets.tsx: add Circle toggle (MirrorButton) in
  CropZoomControl; include in reset
- compositorNodeInspector.tsx: pass cropCircle prop
- compositorCanvasLayers.tsx: CSS border-radius: 50% preview
- compositorServerSync.ts: sync cropCircle from server state

Demo:
- samples/pipelines/dynamic/video_moq_webcam_circle_pip.yml

Signed-off-by: devin-ai-integration[bot] <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Signed-off-by: StreamKit Devin <devin@streamkit.dev>
Co-Authored-By: Claudio Costa <cstcld91@gmail.com>
@staging-devin-ai-integration
Copy link
Contributor Author

🤖 Devin AI Engineer

I'll be helping with this pull request! Here's what you should know:

✅ I will automatically:

  • Address comments on this PR. Add '(aside)' to your comment to have me ignore it.
  • Look at CI failures and help fix them

Note: I can only respond to comments from users who have write access to this repository.

⚙️ Control Options:

  • Disable automatic comment and CI monitoring

staging-devin-ai-integration[bot]

This comment was marked as resolved.

…State comparator

Address two bugs found by Devin Review:

1. kernel.rs: The skip_clear optimization now checks crop_circle — when
   true, pixels outside the ellipse must be cleared to transparent black.
   Without this, pooled buffers would leak stale frame data in the corners.
   Added a regression test using VideoFramePool with pre-filled garbage.

2. useCompositorLayers.ts: The hasExtraChanges comparator passed to
   mergeOverlayState now includes cropCircle, so remote config updates
   that only change cropCircle are no longer silently discarded.

Signed-off-by: devin-ai-integration[bot] <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Signed-off-by: StreamKit Devin <devin@streamkit.dev>
Co-Authored-By: Claudio Costa <cstcld91@gmail.com>
staging-devin-ai-integration[bot]

This comment was marked as resolved.

streamkit-devin and others added 2 commits March 19, 2026 21:14
Remove overflow:hidden from the VideoLayer container when crop_circle
is enabled. The overflow:hidden was clipping resize handles positioned
at corners since they fall outside the inscribed ellipse. The circular
outline (via borderRadius: 50%) alone provides sufficient visual
preview of the crop circle shape.

Signed-off-by: devin-ai-integration[bot] <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Signed-off-by: StreamKit Devin <devin@streamkit.dev>
Co-Authored-By: Claudio Costa <cstcld91@gmail.com>
…tack

Replace the boolean crop_circle field with an extensible CropShape enum
(Rect | Circle) throughout backend and frontend. This enables future
shape variants (RoundedRect, Hexagon, etc.) without breaking changes.

Backend:
- Add CropShape enum with serde rename_all snake_case
- Replace crop_circle: bool with crop_shape: CropShape in LayerConfig,
  ResolvedLayer, LayerSnapshot, CompositeItem, ResolvedSlotConfig
- Kernel converts enum to bool at blit call site
- Update all tests and benchmarks

Frontend:
- Replace cropCircle: boolean with cropShape: 'rect' | 'circle' in
  LayerState, parsers, atoms, overlays, server sync, drag resize
- Replace On/Off MirrorButton toggle with segmented control
  (▭ Rect / ● Circle) matching existing design patterns
- Update DEFAULT_CROP_CIRCLE to DEFAULT_CROP_SHAPE
- Update all test files

Pipeline:
- Update demo pipeline YAML to use crop_shape: rect/circle

Signed-off-by: Devin AI <devin@devin.ai>
Signed-off-by: StreamKit Devin <devin@streamkit.dev>
Co-Authored-By: Claudio Costa <cstcld91@gmail.com>
staging-devin-ai-integration[bot]

This comment was marked as resolved.

streamkit-devin and others added 2 commits March 20, 2026 08:14
The doc comment claimed unknown values deserialize as Rect, but there
was no #[serde(other)] to back that up. Corrected the comment to
accurately describe the actual behavior: field-level #[serde(default)]
handles missing keys, but unknown variant strings will error.

Signed-off-by: Devin AI <devin@devin.ai>
Signed-off-by: StreamKit Devin <devin@streamkit.dev>
Co-Authored-By: Claudio Costa <cstcld91@gmail.com>
…loop

ellipse_sx, ellipse_sy, aa_band, and aa_inner only depend on the
destination rect dimensions (rw/rh) which are loop-invariant. Move
them before the per-row closure, matching the axis-aligned path which
already hoists these correctly.

Signed-off-by: Devin AI <devin@devin.ai>
Signed-off-by: StreamKit Devin <devin@streamkit.dev>
Co-Authored-By: Claudio Costa <cstcld91@gmail.com>
@streamer45 streamer45 merged commit 7d3af2f into main Mar 20, 2026
15 of 16 checks passed
@streamer45 streamer45 deleted the devin/1773952178-compositor-circle-crop branch March 20, 2026 19:59
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants