Skip to content

feat(layers): blend a layer with what is beneath it - #900

Merged
hm21 merged 2 commits into
stablefrom
feat/layer-blend-mode
Oct 22, 2026
Merged

hm21 merged 2 commits into
stablefrom
feat/layer-blend-mode

Conversation

@hm21

@hm21 hm21 commented Oct 22, 2026 •

Copy link
Copy Markdown
Owner

Description

Problem. A layer can only be drawn over the image and the layers before it. Opacity makes it see-through, but it never interacts with what is beneath: a host cannot tint the picture with a colored shape, add only the light of a flame or a light leak shot against black, or overlay a texture, which are standard tools in other editors.

Approach. Layer.blendMode is a Flutter BlendMode the layer is drawn onto what is beneath it with; BlendMode.srcOver (the default) is the old behaviour.

  • The canvas draws it through LayerBlendModeBox. The obvious way, a saveLayer with a blend-mode paint, cannot reach a video Texture, which is a compositing layer of its own, so a host previewing a video under its layers would see the blend only over images. A BackdropFilter is the one compositing layer that composites its content onto the backdrop with a blend mode; its filter here clears the backdrop, so the child alone is blended. Verified pixel for pixel against the W3C formulas on Skia and Impeller.
  • The box wraps the layer outside its opacity, its timeline fades and its keyframe opacity. Inside them it would blend only with the content of their offscreen layer, i.e. with nothing. The selection overlay is drawn through an OverlayPortal, so it is not blended.
  • The mode survives copies, text edits, history steps and import/export (minified key bm), and removing it in a later history step is recorded too (toMapFromReference writes srcOver).
  • A censor layer is never blended: it blurs or pixelates its backdrop, which inside the box is cleared, so blended it would hide nothing. A mode that changes the destination where the layer is transparent, such as dstIn, acts across the whole canvas, as a saveLayer would; the docs say so.
  • The generated image honors it. captureAsPng leaves it out: the captured image is the layer's own content, which a renderer that composites the layers itself (for example pro_video_editor, whose LayerBlendMode maps onto these modes) draws with the mode.

Not changed. Layers without a blend mode paint, capture and export exactly as before. A blended drawing is neither merged with other drawings nor raster-cached, since both draw their members over each other rather than blended onto the canvas.

CHANGELOG 14.16.0.

Related Issue: Closes #none; requested in divinevideo/divine-mobile#10256.

Verification

  • New tests: serialisation on every layer type, a map without or with an unknown mode, the history diff, copyWith and equality; copies and text edits keep the mode; import/export across history steps, minified and not; merge and raster-cache exclusion; a censor layer stays unblended; LayerBlendModeBox pixels for multiply and for a child faded inside it; the editor places the box outside the layer's opacity; the generated image is blended (multiply comes out at 48/40/24 over 0x8040C0) while captureAsPng stays unblended.
  • flutter test: 1074 passed on Flutter 3.47.2 and 1072 on 3.44.0 before the generated-image test was added; flutter analyze clean on both; dart format clean.
  • Used in the Divine video editor on a Samsung Galaxy S26 (debug): every mode on text, stickers, drawings and a video texture, live while it is picked.

Type of Change

  • ✨ New feature (non-breaking change which adds functionality)
  • 🛠️ Bug fix (non-breaking change which fixes an issue)
  • ❌ Breaking change (fix or feature that would cause existing functionality to change)
  • 🧹 Code refactor
  • ✅ Build configuration change
  • 📝 Documentation
  • 🗑️ Chore

Adds Layer.blendMode, a Flutter BlendMode the layer is drawn onto the image
and the layers before it with: multiply to tint them with a colored layer,
screen to add only the light of a layer shot against black, and so on.

The canvas draws it through LayerBlendModeBox, a BackdropFilter whose filter
clears the backdrop and whose blendMode composites the child alone onto what
is beneath. A save layer with a blend mode cannot do that over a video
Texture, which is a compositing layer of its own. The box sits outside the
layer's opacity and timeline fades, so it blends with the canvas rather than
inside their offscreen layers; the selection overlay is drawn in an overlay
of its own and stays unblended.

The mode survives copies, text edits, history steps and import/export
(minified key bm), and a later step that removes it is recorded too. The
generated image honors it. captureAsPng leaves it out: the captured image is
the layer's own content, which a renderer that composites the layers itself,
such as a video exporter, draws with the mode. A blended drawing is neither
merged with other drawings nor raster-cached, since both draw their members
over each other.
A censor area blurs or pixelates its backdrop, and inside a
LayerBlendModeBox that backdrop is cleared, so a censor layer with a blend
mode hid nothing. LayerWidget now leaves censor areas unblended.

Documents that modes which change the destination where the layer is
transparent, such as dstIn, act across the whole canvas, and shortens the
changelog entry.
@hm21
hm21 marked this pull request as ready for review October 22, 2026 10:05
@hm21
hm21 merged commit 7568a08 into stable Oct 22, 2026
3 checks passed
saif-ellafi added a commit to saif-ellafi/pro_image_editor that referenced this pull request Oct 23, 2026
Resolve conflicts with Layer.blendMode (hm21#900): keep both CHANGELOG
entries and copy blendMode in _keepTextLayerPlacement. Inline edits
also carry blendMode in buildResult, with a test that typing on a
layer keeps its blend mode.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@saif-ellafi saif-ellafi mentioned this pull request Oct 23, 2026
1 of 7 tasks
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.

1 participant