Skip to content

feat(compositor): text color, font selection, draggable layers, clipping fix#80

Merged
streamer45 merged 10 commits intovideofrom
devin/1772554047-compositor-text-improvements
Mar 3, 2026
Merged

feat(compositor): text color, font selection, draggable layers, clipping fix#80
streamer45 merged 10 commits intovideofrom
devin/1772554047-compositor-text-improvements

Conversation

@staging-devin-ai-integration
Copy link
Copy Markdown
Contributor

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

Summary

Four compositor UI/backend improvements on the video branch:

1. Text Color Support

  • Added RGB color picker (<input type="color">) for text overlay color
  • Color is sent to the backend as [R, G, B, 255] — text opacity is controlled via the existing layer opacity slider

2. Font Selection

  • Backend: Added font_name: Option<String> field to TextOverlayConfig with a curated map of 12 system fonts (DejaVu, Liberation, FreeFonts families)
  • load_font() resolution order: font_data_base64font_namefont_path → system default
  • Logs a warning when a named font file is not found on disk
  • UI: Added fontName field to TextOverlayState, font dropdown in text overlay controls, round-trip serialization
  • Canvas preview maps font names to CSS font-family with web-safe fallbacks (Verdana/Georgia/Courier New) for visual distinction

3. Draggable Layer List

  • Replaced ▲/▼ z-index buttons and numeric input with drag-to-reorder using Reorder from motion/react
  • Drag handle (grip icon) on each layer list item
  • On reorder, z-index values are reassigned atomically via reorderLayers() which batches all layer type updates into a single config commit (prevents race conditions from stale refs)
  • New overlays use maxZIndex() + 1 instead of fixed bands so they always stack on top after reordering

4. Text Layer Clipping Fix

  • Root cause: When font size increases, the bitmap allocated in rasterize_text_overlay used the fixed rect.width/rect.height — text was clipped both horizontally and vertically
  • Backend fix: Added measure_text() that computes exact pixel dimensions from font metrics. Bitmap is now expanded in both width and height to fit the full rendered string
  • UI fix: updateTextOverlay auto-expands the rect width (~0.6× font size per character) and height (~1.4× font size) when text/fontSize changes would overflow

Bug fixes (post-review)

  • Added missing color array comparison to mergeOverlayState's hasExtraChanges comparator
  • Replaced per-layer handleReorder calls with batched reorderLayers() function
  • Fixed reorderLayers onParamChange path to use immediate commit instead of throttled
  • Fixed font_name doc comment to list all 12 available font names
  • Fixed FontSelect contrast: use var(--sk-panel-bg) with color-scheme hint and explicit option styling

Review & Testing Checklist for Human

  • Font selection end-to-end: Add a text overlay, change the font dropdown, verify the backend renders with the selected font (requires system fonts: fonts-dejavu-core, fonts-liberation, fonts-freefont-ttf)
  • Text color: Change color via the picker, verify the rendered text on the output canvas matches
  • Drag-to-reorder layers: Add multiple overlays (text + image + video inputs), drag items in the layer list, verify z-index changes are reflected on the canvas preview and in the YAML config
  • Text clipping fix: Increase font size to 48+ on a text overlay and verify the full text (including descenders like "g", "y", "p") is visible without clipping
  • Regression: Verify existing compositor features (drag/resize on canvas, inline text editing, image overlays, opacity/rotation sliders) still work

Notes

  • Font selection relies on system font paths (/usr/share/fonts/truetype/...). Missing fonts produce a warning and fall back to DejaVu Sans
  • The canvas preview uses web-safe CSS font fallbacks — exact font rendering is only in the backend output
  • The motion/react Reorder component is already available since motion v12 is a project dependency
  • Requested by Claudio

streamkit-devin and others added 2 commits March 3, 2026 16:12
…ing fix

- Text color: add color picker (RGB + alpha slider) for text overlays
- Font selection: add font_name field to TextOverlayConfig with 12
  curated system fonts (DejaVu, Liberation, FreeFonts), dropdown in UI,
  warning when named font file is missing
- Draggable layer list: replace z-index ▲/▼ buttons with drag-to-reorder
  using motion/react Reorder, reassigns z-indices on drop
- Text clipping fix: expand bitmap height to ceil(font_size * 1.4) in
  rasterize_text_overlay and auto-expand UI rect height when font size
  increases

Signed-off-by: StreamKit Devin <devin@streamkit.dev>
Co-Authored-By: Claudio Costa <cstcld91@gmail.com>
Signed-off-by: StreamKit Devin <devin@streamkit.dev>
Co-Authored-By: Claudio Costa <cstcld91@gmail.com>
@staging-devin-ai-integration
Copy link
Copy Markdown
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

Copy link
Copy Markdown
Contributor Author

@staging-devin-ai-integration staging-devin-ai-integration bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Devin Review found 3 potential issues.

View 4 additional findings in Devin Review.

Staging: Open in Devin
Debug

Playground

Comment on lines +730 to +743
<Reorder.Group
axis="y"
values={entries}
onReorder={handleReorder}
as="div"
style={{ listStyle: 'none', padding: 0, margin: 0 }}
>
{entries.map((entry) => (
<Reorder.Item
key={entry.id}
value={entry}
as="div"
style={{ listStyle: 'none' }}
dragListener={!disabled}
Copy link
Copy Markdown
Contributor Author

@staging-devin-ai-integration staging-devin-ai-integration bot Mar 3, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📝 Info: Reorder.Group receives useMemo-derived values instead of direct state

The Reorder.Group component at ui/src/nodes/CompositorNode.tsx:731 receives values={entries} where entries is computed via useMemo (line 593) rather than held in direct useState. Motion's Reorder typically expects values to be state that onReorder updates directly (e.g. [items, setItems] = useState(...) with onReorder={setItems}). Here, handleReorder goes through onReorderLayersreorderLayers → multiple setState calls → useMemo recomputation. This round-trip works correctly because React batches the state updates into a single render, and the entries memo recomputes with the new z-indices. During drag, motion manages visual positions internally and only uses values for the committed order. No data loss or incorrect behavior observed, but this pattern is worth noting for future maintainers since it deviates from motion's typical usage pattern.

Staging: Open in Devin

Was this helpful? React with 👍 or 👎 to provide feedback.

Debug

Playground

@staging-devin-ai-integration
Copy link
Copy Markdown
Contributor Author

Both the color sync bug (#2) and the handleReorder race condition (#1) are valid findings. The Reorder.Group object identity concern (#3) is not a real issue since key={entry.id} is what motion uses for identity tracking.

I'll fix both bugs now.

- Add reorderLayers() to useCompositorLayers that atomically updates
  z-index for all layer types (video, text, image) in a single commit,
  preventing race conditions from stale refs when handleReorder fired
  N individual update calls.

- Add missing color array comparison to mergeOverlayState's
  hasExtraChanges comparator so server-echoed color changes are
  correctly detected.

- Remove unused onZIndexChange prop from UnifiedLayerList since
  reorderLayers now handles all z-index mutations.

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 3 commits March 3, 2026 16:32
Replace fixed z-index bands (text: 100+, image: 200+) with
maxZIndex() + 1 so that new overlays always stack on top even after
drag-to-reorder has normalized z-indices to [0, n-1].

Signed-off-by: Devin AI <devin@devin.ai>
Signed-off-by: StreamKit Devin <devin@streamkit.dev>
Co-Authored-By: Claudio Costa <cstcld91@gmail.com>
Add measure_text() that computes actual pixel width/height from font
metrics, and use it in rasterize_text_overlay to expand the bitmap to
fit the full rendered string. Previously only height was expanded via a
1.4× heuristic; now both width and height use exact font measurements.

On the UI side, updateTextOverlay now auto-expands the rect width
(~0.6× font_size per character) in addition to height when the text
would overflow the current box.

Signed-off-by: Devin AI <devin@devin.ai>
Signed-off-by: StreamKit Devin <devin@streamkit.dev>
Co-Authored-By: Claudio Costa <cstcld91@gmail.com>
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 3, 2026 16:55
- Replace throttledConfigChange with immediate onParamChange in
  reorderLayers' onParamChange path so video layer and overlay z-index
  updates commit atomically in the same tick.

- Extract serializeLayers() helper to avoid duplicating layer
  serialization logic between buildConfig and reorderLayers.

- Add missing dejavu-serif-bold and dejavu-sans-mono-bold to the
  font_name doc comment in TextOverlayConfig.

Signed-off-by: Devin AI <devin@devin.ai>
Signed-off-by: StreamKit Devin <devin@streamkit.dev>
Co-Authored-By: Claudio Costa <cstcld91@gmail.com>
- Add FONT_FAMILY_MAP to CompositorCanvas that maps backend font names
  to CSS font-family values. Text overlays now preview the selected font
  in both display and edit mode on the canvas.

- Bold font variants (e.g. dejavu-sans-bold) render with fontWeight 700.

- Fix FontSelect contrast: use var(--sk-panel-bg) instead of undefined
  var(--sk-input-bg), add color-scheme hint, and style option elements
  explicitly for dark mode compatibility.

Signed-off-by: Devin AI <devin@devin.ai>
Signed-off-by: StreamKit Devin <devin@streamkit.dev>
Co-Authored-By: Claudio Costa <cstcld91@gmail.com>
Copy link
Copy Markdown
Contributor Author

@staging-devin-ai-integration staging-devin-ai-integration bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Devin Review found 1 new potential issue.

View 13 additional findings in Devin Review.

Staging: Open in Devin
Debug

Playground

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 measure_text height includes above-origin pixels that blit_text_rgba clips, causing top-of-glyph clipping for tall ascenders

The new measure_text function (crates/nodes/src/video/mod.rs:56-89) computes height as max_bottom - max_top, where max_top can be negative for glyphs taller than 'A' (e.g. accented capitals like Á, É). However, rasterize_text_overlay (crates/nodes/src/video/compositor/overlay.rs:231-236) calls blit_text_rgba with origin_y: 0. In blit_text_rgba, any pixel at dst_y < 0 is clipped (crates/nodes/src/video/mod.rs:134). This means: for 'Á' with gy = -3, the top 3 pixel rows (containing the accent mark) are lost, while 3 unused rows are allocated at the bottom of the bitmap. The fix is to pass origin_y = -max_top (i.e., the absolute value of the most-negative gy) to shift rendering down so all glyphs fit. Since measure_text only returns (width, height), it would need to also return the y-offset.

(Refers to lines 231-236)

Prompt for agents
In crates/nodes/src/video/mod.rs, change measure_text to return (u32, u32, i32) where the third element is the y-offset (the negative of max_top, i.e. how many pixels above origin the tallest glyph extends). Then in crates/nodes/src/video/compositor/overlay.rs rasterize_text_overlay, use that offset as origin_y when calling blit_text_rgba (lines 231-236), replacing the hardcoded 0. This ensures glyphs with tall ascenders (like accented capitals) are rendered fully within the bitmap instead of being clipped at the top.
Staging: Open in Devin

Was this helpful? React with 👍 or 👎 to provide feedback.

Debug

Playground

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good catch on the ascender clipping for accented characters. This is a pre-existing limitation (blit_text_rgba always used origin_y: 0), but now that we have measure_text it would be straightforward to return the y-offset and pass it through. Will address if requested.

- Add web-safe intermediate fonts (Verdana, Georgia, Arial, Times New
  Roman, Courier New) to the CSS font-family fallback stacks so the
  canvas preview shows a visible difference between sans-serif, serif,
  and monospace font groups even when the exact system fonts are not
  installed in the browser.

- Remove the alpha slider from text color controls. Text opacity is
  already covered by the layer opacity slider, and a standalone alpha
  slider for a single channel was confusing. The color picker now
  always sends alpha=255.

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.

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 b8d9051 into video Mar 3, 2026
1 check passed
@streamer45 streamer45 deleted the devin/1772554047-compositor-text-improvements branch March 3, 2026 17: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