Skip to content

motionlib: add suite of RenderEffect-based MotionEffects#64

Merged
tejpratap46 merged 1 commit into
mainfrom
feat/render-effects
May 30, 2026
Merged

motionlib: add suite of RenderEffect-based MotionEffects#64
tejpratap46 merged 1 commit into
mainfrom
feat/render-effects

Conversation

@tejpratap46

@tejpratap46 tejpratap46 commented May 28, 2026

Copy link
Copy Markdown
Owner
  • motionlib: add Vignette and Pixelate effects using AGSL shaders
  • motionlib: add Grayscale, Sepia, Invert, and Brightness/Contrast effects using ColorMatrix
  • motionlib: add OffsetEffect and placeholder ChainEffect implementation
  • lyrics-maker: apply BlurEffect to background views in PopupLyricsTemplate

Summary by CodeRabbit

  • New Features

    • Added blur overlay to lyric templates and introduced a new vintage lyrics template.
    • Added multiple visual effects: brightness/contrast, grayscale, invert, offset, pixelate, sepia, vignette, vintage, and effect-chaining.
    • Added ML Kit subject-segmentation support with a new image-processor entry and background-removal plugin.
  • Chores

    • Added a new ML Kit extension module to the build and project settings; configured publishing.

Review Change Stack

@tejpratap46 tejpratap46 self-assigned this May 28, 2026
@cloudflare-workers-and-pages

cloudflare-workers-and-pages Bot commented May 28, 2026

Copy link
Copy Markdown
Contributor

Deploying with  Cloudflare Workers  Cloudflare Workers

The latest updates on your project. Learn more about integrating Git with Workers.

Status Name Latest Commit Preview URL Updated (UTC)
✅ Deployment successful!
View logs
sdui d035766 Commit Preview URL

Branch Preview URL
May 28 2026, 07:53 PM

@cloudflare-workers-and-pages

cloudflare-workers-and-pages Bot commented May 28, 2026

Copy link
Copy Markdown
Contributor

Deploying with  Cloudflare Workers  Cloudflare Workers

The latest updates on your project. Learn more about integrating Git with Workers.

Status Name Latest Commit Preview URL Updated (UTC)
✅ Deployment successful!
View logs
lyrics d035766 Commit Preview URL

Branch Preview URL
May 28 2026, 07:53 PM

@cloudflare-workers-and-pages

cloudflare-workers-and-pages Bot commented May 28, 2026

Copy link
Copy Markdown
Contributor

Deploying with  Cloudflare Workers  Cloudflare Workers

The latest updates on your project. Learn more about integrating Git with Workers.

Status Name Latest Commit Preview URL Updated (UTC)
✅ Deployment successful!
View logs
motion-lib d035766 Commit Preview URL

Branch Preview URL
May 28 2026, 07:52 PM

@coderabbitai

coderabbitai Bot commented May 28, 2026

Copy link
Copy Markdown
Contributor
📝 Walkthrough

Walkthrough

Adds many new MotionEffect implementations (brightness/contrast, grayscale, invert, sepia, offset, pixelate, vignette, vintage, chain), integrates BlurEffect into PopupLyricsTemplate, adds a new VintageLyricsTemplate and registry entry, and introduces an ml-kit-ext module with subject-segmentation plugin and runtime shader effect.

Changes

Motion Effects Library

Layer / File(s) Summary
Color matrix animation effects
modules/motionlib/src/main/java/.../BrightnessContrastEffect.kt, GrayscaleEffect.kt, InvertEffect.kt, SepiaEffect.kt
BrightnessContrast, Grayscale, Invert, and Sepia interpolate parameters across frame ranges and apply ColorMatrix-based RenderEffects (API-gated).
Offset & shader-based effects + VintageEffect
modules/motionlib/src/main/java/.../OffsetEffect.kt, PixelateEffect.kt, VignetteEffect.kt, VintageEffect.kt
OffsetEffect animates X/Y offset; Pixelate and Vignette use RuntimeShader AGSL uniforms; VintageEffect composes sepia + optional vignette.
Effect composition
modules/motionlib/src/main/java/.../ChainEffect.kt
ChainEffect forwards motionView to inner/outer effects and invokes them sequentially during the effect window.

Lyrics Templates

Layer / File(s) Summary
Popup template blur wiring
modules/lyrics-maker/.../PopupLyricsTemplate.kt
Imports BlurEffect and supplies it to translucentMotionView in both content and preview timelines spanning each lyric frame range.
Vintage lyrics template & registry
modules/lyrics-maker/.../VintageLyricsTemplate.kt, LyricsTemplateRegistry.kt
Adds VintageLyricsTemplate (content + preview) that renders image + translucent overlay + vintage effects and registers it in the templates list.

ML Kit Extension Module

Layer / File(s) Summary
Build & settings
settings.gradle, .idea/gradle.xml, gradle/libs.versions.toml
Registers :modules:ml-kit-ext, updates IDE linked modules, and adds version/library entries for ML Kit subject segmentation.
ml-kit-ext Gradle config
modules/ml-kit-ext/build.gradle
New Android library module with publishing configuration, SDK/Java settings, and dependencies (core, AppCompat, ML Kit segmentation).
ML Kit API & plugin
modules/ml-kit-ext/src/.../MLKitImageProcessor.kt, plugins/SubjectSegmentationPlugin.kt
Public MLKitImageProcessor singleton exposing a lazily-initialized SubjectSegmentationPlugin that applies foreground-confidence masks to bitmaps.
SubjectSegmentationEffect
modules/ml-kit-ext/src/.../effects/SubjectSegmentationEffect.kt
Per-frame mask generation (synchronous segmenter.process), FloatBuffer→Bitmap mask helper, and RuntimeShader RenderEffect installation on Views (API-gated).

SDUI Integration

Layer / File(s) Summary
VintageEffect registration
modules/sdui/src/.../MotionSduiInitializer.kt
Registers VintageEffect in SDUI initializer with JSON deserialization/serialization for fromIntensity/toIntensity.

Sequence Diagram(s)

(omitted — changes are library additions and wiring; no multi-component sequential control flow diagram required)

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

Poem

🐰 Soft hops and code that shimmers bright,
Vintage hues and pixels dance in light,
Blurry verses hum and scenes take flight,
ML masks peel back the background’s night,
A rabbit cheers — effects stitched just right!

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately describes the main change: a suite of RenderEffect-based MotionEffects is added to motionlib, which is the primary focus across 8 new effect classes.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/render-effects

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@amazon-q-developer amazon-q-developer Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Review Summary

This PR adds a comprehensive suite of RenderEffect-based motion effects including Vignette, Pixelate, Grayscale, Sepia, Invert, BrightnessContrast, and Offset effects. The implementation follows Android's RenderEffect API and integrates well with the existing MotionEffect framework.

Critical Issues (Must Fix Before Merge)

  • ChainEffect: Non-null assertion crash risk on uninitialized motionView
  • VignetteEffect: Potential division by zero crash when view dimensions are 0

Summary

The implementation is solid overall, but the two crash risks identified above must be addressed before merging to ensure stability. Once fixed, this will be a valuable addition to the motion effects library.


You can now have the agent implement changes and create commits directly on your pull request's source branch. Simply comment with /q followed by your request in natural language to ask the agent to make changes.

) : MotionEffect {
private var _motionView: MotionView? = null
override var motionView: MotionView
get() = _motionView!!

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🛑 Crash Risk: Non-null assertion on lateinit property can crash if motionView is accessed before initialization. Add null check before returning.

Suggested change
get() = _motionView!!
get() = _motionView ?: throw IllegalStateException("motionView must be initialized before use")

Comment on lines +63 to +65
val shader = RuntimeShader(VIGNETTE_SHADER)
shader.setFloatUniform("size", view.width.toFloat(), view.height.toFloat())
shader.setFloatUniform("intensity", intensity)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🛑 Crash Risk: Division by zero will occur if view width or height is 0 during initial layout, causing the shader to compute incorrect UV coordinates and potentially crash. Add dimension validation before shader creation.

Suggested change
val shader = RuntimeShader(VIGNETTE_SHADER)
shader.setFloatUniform("size", view.width.toFloat(), view.height.toFloat())
shader.setFloatUniform("intensity", intensity)
if (view.width == 0 || view.height == 0) return motionView
val shader = RuntimeShader(VIGNETTE_SHADER)
shader.setFloatUniform("size", view.width.toFloat(), view.height.toFloat())
shader.setFloatUniform("intensity", intensity)

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Code Review

This pull request introduces several new motion effects using Android's RenderEffect and AGSL shaders, and integrates BlurEffect into PopupLyricsTemplate. Key feedback focuses on performance optimizations, such as caching RuntimeShader instances in PixelateEffect and VignetteEffect to prevent rendering jank. Additionally, multiple effects need to clear their RenderEffect when the frame is below the startFrame to handle backward scrubbing correctly. Finally, ChainEffect should be refactored to properly chain effects using RenderEffect.createChainEffect and to use idiomatic lateinit var properties instead of unsafe nullable casts.

Comment on lines +42 to +49
// Chaining is tricky with the current side-effect based architecture.
// For now, we just call the inner and outer effects, which will
// each try to set the RenderEffect on the view, with the last one winning.
// To properly support chaining, we would need to refactor MotionEffect
// to return a RenderEffect instead of applying it.
innerEffect.forFrame(frame)
outerEffect.forFrame(frame)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

high

The current placeholder implementation does not actually chain the effects; the outer effect completely overwrites the inner effect. Since we are on API 31+ (guaranteed by the SDK check), we can retrieve the applied RenderEffect from the view after running each effect and chain them properly using RenderEffect.createChainEffect.

        innerEffect.forFrame(frame)
        val innerRenderEffect = view.getRenderEffect()

        outerEffect.forFrame(frame)
        val outerRenderEffect = view.getRenderEffect()

        if (innerRenderEffect != null && outerRenderEffect != null) {
            view.setRenderEffect(RenderEffect.createChainEffect(outerRenderEffect, innerRenderEffect))
        } else if (innerRenderEffect != null) {
            view.setRenderEffect(innerRenderEffect)
        } else if (outerRenderEffect != null) {
            view.setRenderEffect(outerRenderEffect)
        } else {
            view.setRenderEffect(null)
        }

Comment on lines +38 to +64
override fun forFrame(frame: Int): MotionView {
if (motionView !is View) return motionView
val view = motionView as View

if (Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU) return motionView

if (frame !in startFrame..endFrame) {
if (frame > endFrame) {
view.setRenderEffect(null)
}
return motionView
}

val pixelSize = MotionInterpolator.interpolateForRange(
interpolator = Interpolators(Easings.LINEAR),
currentFrame = frame,
frameRange = Pair(startFrame, endFrame),
valueRange = Pair(fromPixelSize, toPixelSize),
)

val shader = RuntimeShader(PIXELATE_SHADER)
shader.setFloatUniform("pixelSize", pixelSize)

view.setRenderEffect(RenderEffect.createRuntimeShaderEffect(shader, "content"))

return motionView
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

high

Recreating the RuntimeShader on every frame is extremely expensive because it compiles the AGSL shader on each call, causing rendering stutters (jank). We should cache the RuntimeShader instance as a class property and reuse it, only updating the uniforms. Additionally, the render effect should be cleared when frame < startFrame.

    private var cachedShader: RuntimeShader? = null

    override fun forFrame(frame: Int): MotionView {
        if (motionView !is View) return motionView
        val view = motionView as View

        if (Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU) return motionView

        if (frame !in startFrame..endFrame) {
            view.setRenderEffect(null)
            return motionView
        }

        val pixelSize = MotionInterpolator.interpolateForRange(
            interpolator = Interpolators(Easings.LINEAR),
            currentFrame = frame,
            frameRange = Pair(startFrame, endFrame),
            valueRange = Pair(fromPixelSize, toPixelSize),
        )

        val shader = cachedShader ?: RuntimeShader(PIXELATE_SHADER).also { cachedShader = it }
        shader.setFloatUniform("pixelSize", pixelSize)
        
        view.setRenderEffect(RenderEffect.createRuntimeShaderEffect(shader, "content"))

        return motionView
    }

Comment on lines +42 to +70
override fun forFrame(frame: Int): MotionView {
if (motionView !is View) return motionView
val view = motionView as View

if (Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU) return motionView

if (frame !in startFrame..endFrame) {
if (frame > endFrame) {
view.setRenderEffect(null)
}
return motionView
}

val intensity =
MotionInterpolator.interpolateForRange(
interpolator = Interpolators(Easings.LINEAR),
currentFrame = frame,
frameRange = Pair(startFrame, endFrame),
valueRange = Pair(fromIntensity, toIntensity),
)

val shader = RuntimeShader(VIGNETTE_SHADER)
shader.setFloatUniform("size", view.width.toFloat(), view.height.toFloat())
shader.setFloatUniform("intensity", intensity)

view.setRenderEffect(RenderEffect.createRuntimeShaderEffect(shader, "content"))

return motionView
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

high

Recreating the RuntimeShader on every frame is extremely expensive because it compiles the AGSL shader on each call, causing rendering stutters (jank). We should cache the RuntimeShader instance as a class property and reuse it, only updating the uniforms. Additionally, the render effect should be cleared when frame < startFrame.

    private var cachedShader: RuntimeShader? = null

    override fun forFrame(frame: Int): MotionView {
        if (motionView !is View) return motionView
        val view = motionView as View

        if (Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU) return motionView

        if (frame !in startFrame..endFrame) {
            view.setRenderEffect(null)
            return motionView
        }

        val intensity =
            MotionInterpolator.interpolateForRange(
                interpolator = Interpolators(Easings.LINEAR),
                currentFrame = frame,
                frameRange = Pair(startFrame, endFrame),
                valueRange = Pair(fromIntensity, toIntensity),
            )

        val shader = cachedShader ?: RuntimeShader(VIGNETTE_SHADER).also { cachedShader = it }
        shader.setFloatUniform("size", view.width.toFloat(), view.height.toFloat())
        shader.setFloatUniform("intensity", intensity)

        view.setRenderEffect(RenderEffect.createRuntimeShaderEffect(shader, "content"))

        return motionView
    }

Comment on lines +34 to +39
if (frame !in startFrame..endFrame) {
if (frame > endFrame) {
view.setRenderEffect(null)
}
return motionView
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

medium

The render effect is only cleared when frame > endFrame. If the animation is scrubbed backwards or jumps to a frame before startFrame, the effect will remain applied to the view. It should be cleared whenever the frame is outside the active range.

        if (frame !in startFrame..endFrame) {
            view.setRenderEffect(null)
            return motionView
        }

Comment on lines +20 to +27
private var _motionView: MotionView? = null
override var motionView: MotionView
get() = _motionView!!
set(value) {
_motionView = value
outerEffect.motionView = value
innerEffect.motionView = value
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

medium

Using a nullable backing property with the double-bang operator (!!) is not idiomatic in Kotlin. Since motionView is guaranteed to be initialized, we can use lateinit var for the backing property to avoid nullability and unsafe casts.

Suggested change
private var _motionView: MotionView? = null
override var motionView: MotionView
get() = _motionView!!
set(value) {
_motionView = value
outerEffect.motionView = value
innerEffect.motionView = value
}
private lateinit var _motionView: MotionView
override var motionView: MotionView
get() = _motionView
set(value) {
_motionView = value
outerEffect.motionView = value
innerEffect.motionView = value
}

Comment on lines +35 to +40
if (frame !in startFrame..endFrame) {
if (frame > endFrame) {
view.setRenderEffect(null)
}
return motionView
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

medium

The render effect is only cleared when frame > endFrame. If the animation is scrubbed backwards or jumps to a frame before startFrame, the effect will remain applied to the view. It should be cleared whenever the frame is outside the active range.

        if (frame !in startFrame..endFrame) {
            view.setRenderEffect(null)
            return motionView
        }

Comment on lines +33 to +42
if (frame !in startFrame..endFrame) {
if (frame > endFrame) {
// Keep the final state if needed, or clear it.
// Typically transitions might want to stay at final state if it's the end of visibility.
// But for generic effects, we might want to clear them when out of range.
// BlurEffect clears it, so we follow that pattern.
view.setRenderEffect(null)
}
return motionView
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

medium

The render effect is only cleared when frame > endFrame. If the animation is scrubbed backwards or jumps to a frame before startFrame, the effect will remain applied to the view. It should be cleared whenever the frame is outside the active range.

        if (frame !in startFrame..endFrame) {
            view.setRenderEffect(null)
            return motionView
        }

Comment on lines +31 to +36
if (frame !in startFrame..endFrame) {
if (frame > endFrame) {
view.setRenderEffect(null)
}
return motionView
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

medium

The render effect is only cleared when frame > endFrame. If the animation is scrubbed backwards or jumps to a frame before startFrame, the effect will remain applied to the view. It should be cleared whenever the frame is outside the active range.

        if (frame !in startFrame..endFrame) {
            view.setRenderEffect(null)
            return motionView
        }

Comment on lines +32 to +37
if (frame !in startFrame..endFrame) {
if (frame > endFrame) {
view.setRenderEffect(null)
}
return motionView
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

medium

The render effect is only cleared when frame > endFrame. If the animation is scrubbed backwards or jumps to a frame before startFrame, the effect will remain applied to the view. It should be cleared whenever the frame is outside the active range.

        if (frame !in startFrame..endFrame) {
            view.setRenderEffect(null)
            return motionView
        }

Comment on lines +32 to +37
if (frame !in startFrame..endFrame) {
if (frame > endFrame) {
view.setRenderEffect(null)
}
return motionView
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

medium

The render effect is only cleared when frame > endFrame. If the animation is scrubbed backwards or jumps to a frame before startFrame, the effect will remain applied to the view. It should be cleared whenever the frame is outside the active range.

        if (frame !in startFrame..endFrame) {
            view.setRenderEffect(null)
            return motionView
        }

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 5

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In
`@modules/motionlib/src/main/java/com/tejpratapsingh/motionlib/ui/effects/ChainEffect.kt`:
- Around line 42-49: ChainEffect currently calls innerEffect.forFrame(frame)
then outerEffect.forFrame(frame) which means the second call overwrites the
RenderEffect set by the first; update ChainEffect to fail fast by throwing an
UnsupportedOperationException (or rename to SequentialOverwriteEffect) until
proper chaining is implemented: replace the current sequential calls in
ChainEffect.forFrame with code that throws an UnsupportedOperationException
(message: chaining not supported) so callers of ChainEffect learn chaining is
unsupported rather than silently overwritten; reference innerEffect,
outerEffect, forFrame, MotionEffect, and setRenderEffect when making the change.

In
`@modules/motionlib/src/main/java/com/tejpratapsingh/motionlib/ui/effects/PixelateEffect.kt`:
- Around line 51-56: The computed pixelSize from
MotionInterpolator.interpolateForRange can be zero or negative, which breaks the
shader (fragCoord / pixelSize); clamp/validate it immediately after the
interpolation in PixelateEffect (after the pixelSize assignment) by replacing it
with a positive minimum (e.g., max(pixelSize, 1f) or max(pixelSize,
Float.MIN_VALUE)) so downstream shader math uses a safe non-zero value; update
any references to pixelSize in this scope (the PixelateEffect interpolation that
uses fromPixelSize, toPixelSize, startFrame, endFrame, frame, and the
Interpolators(Easings.LINEAR) call) to use the clamped value.
- Around line 58-61: Currently a new RuntimeShader is created every frame in
PixelateEffect.kt; instead make RuntimeShader (constructed with PIXELATE_SHADER)
a cached field on the PixelateEffect instance and reuse it across frames—update
only the uniform via shader.setFloatUniform("pixelSize", pixelSize) each frame;
likewise create the RenderEffect once with
RenderEffect.createRuntimeShaderEffect(cachedShader, "content") and reuse or
recreate it only if the shader instance changes, ensuring you avoid per-frame
allocations of RuntimeShader and RenderEffect.

In
`@modules/motionlib/src/main/java/com/tejpratapsingh/motionlib/ui/effects/VignetteEffect.kt`:
- Around line 63-67: The code currently creates a new RuntimeShader every frame;
change VignetteEffect to cache a single RuntimeShader (from VIGNETTE_SHADER) as
a property (and optionally cache the RenderEffect created from it) and only call
shader.setFloatUniform("size", width, height) and
shader.setFloatUniform("intensity", intensity) each frame. Ensure the cached
shader/RenderEffect is created once (e.g., in the VignetteEffect constructor or
an init block), update uniforms when view size or intensity changes, and reuse
the same shader when calling
view.setRenderEffect(RenderEffect.createRuntimeShaderEffect(shader, "content"))
to avoid re-instantiation on the hot animation path.
- Line 64: Guard against zero-sized views before calling
shader.setFloatUniform("size", ...) in VignetteEffect: check view.width and
view.height and if either is zero, either return/skip setting uniforms or use a
safe non-zero fallback (e.g., 1f) before computing view.width.toFloat() and
view.height.toFloat(); update the code around the shader.setFloatUniform("size",
view.width.toFloat(), view.height.toFloat()) call so the shader never receives a
zero component for size.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: c827b2d9-1b16-4693-9772-8ae3069bea83

📥 Commits

Reviewing files that changed from the base of the PR and between fc5d0c3 and 03863d1.

📒 Files selected for processing (9)
  • modules/lyrics-maker/src/main/java/com/tejpratapsingh/lyricsmaker/presentation/templates/PopupLyricsTemplate.kt
  • modules/motionlib/src/main/java/com/tejpratapsingh/motionlib/ui/effects/BrightnessContrastEffect.kt
  • modules/motionlib/src/main/java/com/tejpratapsingh/motionlib/ui/effects/ChainEffect.kt
  • modules/motionlib/src/main/java/com/tejpratapsingh/motionlib/ui/effects/GrayscaleEffect.kt
  • modules/motionlib/src/main/java/com/tejpratapsingh/motionlib/ui/effects/InvertEffect.kt
  • modules/motionlib/src/main/java/com/tejpratapsingh/motionlib/ui/effects/OffsetEffect.kt
  • modules/motionlib/src/main/java/com/tejpratapsingh/motionlib/ui/effects/PixelateEffect.kt
  • modules/motionlib/src/main/java/com/tejpratapsingh/motionlib/ui/effects/SepiaEffect.kt
  • modules/motionlib/src/main/java/com/tejpratapsingh/motionlib/ui/effects/VignetteEffect.kt

Comment on lines +42 to +49
// Chaining is tricky with the current side-effect based architecture.
// For now, we just call the inner and outer effects, which will
// each try to set the RenderEffect on the view, with the last one winning.
// To properly support chaining, we would need to refactor MotionEffect
// to return a RenderEffect instead of applying it.
innerEffect.forFrame(frame)
outerEffect.forFrame(frame)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

ChainEffect does not actually chain effects; it overwrites.

Current logic applies two side-effecting effects sequentially, so only the last setRenderEffect survives. That breaks the class contract for callers expecting composition.

At minimum, fail fast (e.g., throw UnsupportedOperationException) until true chaining is implemented, or rename this class to reflect sequential overwrite semantics.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@modules/motionlib/src/main/java/com/tejpratapsingh/motionlib/ui/effects/ChainEffect.kt`
around lines 42 - 49, ChainEffect currently calls innerEffect.forFrame(frame)
then outerEffect.forFrame(frame) which means the second call overwrites the
RenderEffect set by the first; update ChainEffect to fail fast by throwing an
UnsupportedOperationException (or rename to SequentialOverwriteEffect) until
proper chaining is implemented: replace the current sequential calls in
ChainEffect.forFrame with code that throws an UnsupportedOperationException
(message: chaining not supported) so callers of ChainEffect learn chaining is
unsupported rather than silently overwritten; reference innerEffect,
outerEffect, forFrame, MotionEffect, and setRenderEffect when making the change.

Comment on lines +58 to +61
val shader = RuntimeShader(PIXELATE_SHADER)
shader.setFloatUniform("pixelSize", pixelSize)

view.setRenderEffect(RenderEffect.createRuntimeShaderEffect(shader, "content"))

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Avoid allocating RuntimeShader every frame.

Creating a new shader on each frame adds avoidable churn on the render path. Cache one shader instance and only update uniforms per frame.

Proposed fix
 class PixelateEffect(
@@
 ) : MotionEffect {
     override lateinit var motionView: MotionView
+    private val shader by lazy { RuntimeShader(PIXELATE_SHADER) }
@@
-        val shader = RuntimeShader(PIXELATE_SHADER)
         shader.setFloatUniform("pixelSize", pixelSize)
         
         view.setRenderEffect(RenderEffect.createRuntimeShaderEffect(shader, "content"))
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@modules/motionlib/src/main/java/com/tejpratapsingh/motionlib/ui/effects/PixelateEffect.kt`
around lines 58 - 61, Currently a new RuntimeShader is created every frame in
PixelateEffect.kt; instead make RuntimeShader (constructed with PIXELATE_SHADER)
a cached field on the PixelateEffect instance and reuse it across frames—update
only the uniform via shader.setFloatUniform("pixelSize", pixelSize) each frame;
likewise create the RenderEffect once with
RenderEffect.createRuntimeShaderEffect(cachedShader, "content") and reuse or
recreate it only if the shader instance changes, ensuring you avoid per-frame
allocations of RuntimeShader and RenderEffect.

Comment on lines +63 to +67
val shader = RuntimeShader(VIGNETTE_SHADER)
shader.setFloatUniform("size", view.width.toFloat(), view.height.toFloat())
shader.setFloatUniform("intensity", intensity)

view.setRenderEffect(RenderEffect.createRuntimeShaderEffect(shader, "content"))

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Reuse a cached shader instead of recreating it each frame.

Re-instantiating RuntimeShader on each frame is expensive on a hot animation path. Keep one shader instance and only update uniforms.

Proposed fix
 ) : MotionEffect {
     override lateinit var motionView: MotionView
+    private val shader by lazy { RuntimeShader(VIGNETTE_SHADER) }
@@
-        val shader = RuntimeShader(VIGNETTE_SHADER)
         shader.setFloatUniform("size", view.width.toFloat(), view.height.toFloat())
         shader.setFloatUniform("intensity", intensity)
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
val shader = RuntimeShader(VIGNETTE_SHADER)
shader.setFloatUniform("size", view.width.toFloat(), view.height.toFloat())
shader.setFloatUniform("intensity", intensity)
view.setRenderEffect(RenderEffect.createRuntimeShaderEffect(shader, "content"))
) : MotionEffect {
override lateinit var motionView: MotionView
private val shader by lazy { RuntimeShader(VIGNETTE_SHADER) }
// ... other code ...
// In the forFrame method or equivalent:
shader.setFloatUniform("size", view.width.toFloat(), view.height.toFloat())
shader.setFloatUniform("intensity", intensity)
view.setRenderEffect(RenderEffect.createRuntimeShaderEffect(shader, "content"))
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@modules/motionlib/src/main/java/com/tejpratapsingh/motionlib/ui/effects/VignetteEffect.kt`
around lines 63 - 67, The code currently creates a new RuntimeShader every
frame; change VignetteEffect to cache a single RuntimeShader (from
VIGNETTE_SHADER) as a property (and optionally cache the RenderEffect created
from it) and only call shader.setFloatUniform("size", width, height) and
shader.setFloatUniform("intensity", intensity) each frame. Ensure the cached
shader/RenderEffect is created once (e.g., in the VignetteEffect constructor or
an init block), update uniforms when view size or intensity changes, and reuse
the same shader when calling
view.setRenderEffect(RenderEffect.createRuntimeShaderEffect(shader, "content"))
to avoid re-instantiation on the hot animation path.

)

val shader = RuntimeShader(VIGNETTE_SHADER)
shader.setFloatUniform("size", view.width.toFloat(), view.height.toFloat())

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Guard against zero-sized views before setting shader uniforms.

When width or height is 0, the shader computes fragCoord / size, which can generate invalid output.

Proposed fix
-        shader.setFloatUniform("size", view.width.toFloat(), view.height.toFloat())
+        if (view.width <= 0 || view.height <= 0) return motionView
+        shader.setFloatUniform("size", view.width.toFloat(), view.height.toFloat())
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@modules/motionlib/src/main/java/com/tejpratapsingh/motionlib/ui/effects/VignetteEffect.kt`
at line 64, Guard against zero-sized views before calling
shader.setFloatUniform("size", ...) in VignetteEffect: check view.width and
view.height and if either is zero, either return/skip setting uniforms or use a
safe non-zero fallback (e.g., 1f) before computing view.width.toFloat() and
view.height.toFloat(); update the code around the shader.setFloatUniform("size",
view.width.toFloat(), view.height.toFloat()) call so the shader never receives a
zero component for size.

* motionlib: add Vignette and Pixelate effects using AGSL shaders
* motionlib: add Grayscale, Sepia, Invert, and Brightness/Contrast effects using ColorMatrix
* motionlib: add OffsetEffect and placeholder ChainEffect implementation
* lyrics-maker: apply BlurEffect to background views in PopupLyricsTemplate
@tejpratap46 tejpratap46 force-pushed the feat/render-effects branch from 03863d1 to d035766 Compare May 28, 2026 19:51

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 5

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In
`@modules/lyrics-maker/src/main/java/com/tejpratapsingh/lyricsmaker/presentation/templates/VintageLyricsTemplate.kt`:
- Around line 53-69: The current loop using lyrics.zipWithNext() in
VintageLyricsTemplate.kt (inside the block that calls wordWriterTextView and
transition with SlideTransition/SlideDirection) omits the final lyric, causing
the last frame (and single-frame cases) to be skipped; update the rendering to
iterate all items (e.g., use forEachIndexed or a for loop over lyrics) and after
processing each pair call wordWriterTextView with current.text/current.frame and
next.frame for timing, then separately handle the final lyric element by calling
wordWriterTextView with its startFrame and an appropriate endFrame (or duration)
so the last segment is rendered; apply the same fix to the second occurrence of
zipWithNext() at the other block (lines referenced in the comment).

In
`@modules/ml-kit-ext/src/main/java/com/tejpratapsingh/motionlib/mlkit/effects/SubjectSegmentationEffect.kt`:
- Around line 38-39: The maskCache currently (in SubjectSegmentationEffect) is
an unbounded mutableMap and can retain one full-size Bitmap per frame; replace
it with a bounded cache (e.g., an LruCache<Int, Bitmap> or a LinkedHashMap with
access-order eviction) keyed the same way as maskCache so entries are evicted
when size exceeds a configured max (bytes or entry count), and ensure evicted
Bitmaps are recycled/closed to free memory; update all uses of maskCache (where
put/replace happens in the class) to use the new cache API and make the cache
access thread-safe if methods like produceMask() or onFrameProcessed() access it
from different threads.
- Around line 60-65: The code only clears the RenderEffect when frame > endFrame
and when maskBitmap is null it leaves the prior effect in place; update the
logic in SubjectSegmentationEffect (the method that checks frame against
startFrame..endFrame and the branch that handles maskBitmap) to always clear the
prior effect via view.setRenderEffect(null) whenever the segmentation is
inactive (i.e., frame not in startFrame..endFrame, including frame < startFrame)
and also when maskBitmap is null, then return motionView; ensure you reference
the existing calls to view.setRenderEffect(null), the startFrame and endFrame
bounds check, and the maskBitmap null-check while making this change.

In
`@modules/motionlib/src/main/java/com/tejpratapsingh/motionlib/ui/effects/VintageEffect.kt`:
- Around line 49-53: The code fails to clear the render effect when frame <
startFrame, leaving stale effects; update the out-of-window handling in
VintageEffect (the block that checks frame against startFrame..endFrame) to call
view.setRenderEffect(null) for any frame not in the range (both before
startFrame and after endFrame) before returning motionView so previously applied
effects are always cleared; locate the conditional using startFrame, endFrame,
view.setRenderEffect(null), and motionView and ensure the nulling happens
whenever frame !in startFrame..endFrame.
- Around line 56-62: The interpolated intensity value returned from
MotionInterpolator.interpolateForRange (assigned to intensity in
VintageEffect.kt) must be clamped to a safe range (e.g., 0f..1f) to avoid
invalid/excessive sepia/vignette parameters; after computing intensity, apply a
clamp (Kotlin's coerceIn or equivalent) and use the clamped value for subsequent
processing (replace usages of intensity with the clamped variable) so external
fromIntensity/toIntensity cannot produce out-of-range effects.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: d3c618d2-de1c-4c03-97ad-2467441ec339

📥 Commits

Reviewing files that changed from the base of the PR and between 03863d1 and d035766.

📒 Files selected for processing (22)
  • .idea/gradle.xml
  • gradle/libs.versions.toml
  • modules/lyrics-maker/src/main/java/com/tejpratapsingh/lyricsmaker/presentation/templates/LyricsTemplateRegistry.kt
  • modules/lyrics-maker/src/main/java/com/tejpratapsingh/lyricsmaker/presentation/templates/PopupLyricsTemplate.kt
  • modules/lyrics-maker/src/main/java/com/tejpratapsingh/lyricsmaker/presentation/templates/VintageLyricsTemplate.kt
  • modules/ml-kit-ext/build.gradle
  • modules/ml-kit-ext/consumer-rules.pro
  • modules/ml-kit-ext/proguard-rules.pro
  • modules/ml-kit-ext/src/main/java/com/tejpratapsingh/motionlib/mlkit/MLKitImageProcessor.kt
  • modules/ml-kit-ext/src/main/java/com/tejpratapsingh/motionlib/mlkit/effects/SubjectSegmentationEffect.kt
  • modules/ml-kit-ext/src/main/java/com/tejpratapsingh/motionlib/mlkit/plugins/SubjectSegmentationPlugin.kt
  • modules/motionlib/src/main/java/com/tejpratapsingh/motionlib/ui/effects/BrightnessContrastEffect.kt
  • modules/motionlib/src/main/java/com/tejpratapsingh/motionlib/ui/effects/ChainEffect.kt
  • modules/motionlib/src/main/java/com/tejpratapsingh/motionlib/ui/effects/GrayscaleEffect.kt
  • modules/motionlib/src/main/java/com/tejpratapsingh/motionlib/ui/effects/InvertEffect.kt
  • modules/motionlib/src/main/java/com/tejpratapsingh/motionlib/ui/effects/OffsetEffect.kt
  • modules/motionlib/src/main/java/com/tejpratapsingh/motionlib/ui/effects/PixelateEffect.kt
  • modules/motionlib/src/main/java/com/tejpratapsingh/motionlib/ui/effects/SepiaEffect.kt
  • modules/motionlib/src/main/java/com/tejpratapsingh/motionlib/ui/effects/VignetteEffect.kt
  • modules/motionlib/src/main/java/com/tejpratapsingh/motionlib/ui/effects/VintageEffect.kt
  • modules/sdui/src/main/java/com/tejpratapsingh/motion/sdui/infra/MotionSduiInitializer.kt
  • settings.gradle
✅ Files skipped from review due to trivial changes (2)
  • gradle/libs.versions.toml
  • .idea/gradle.xml
🚧 Files skipped from review as they are similar to previous changes (9)
  • modules/motionlib/src/main/java/com/tejpratapsingh/motionlib/ui/effects/OffsetEffect.kt
  • modules/motionlib/src/main/java/com/tejpratapsingh/motionlib/ui/effects/SepiaEffect.kt
  • modules/motionlib/src/main/java/com/tejpratapsingh/motionlib/ui/effects/GrayscaleEffect.kt
  • modules/motionlib/src/main/java/com/tejpratapsingh/motionlib/ui/effects/InvertEffect.kt
  • modules/lyrics-maker/src/main/java/com/tejpratapsingh/lyricsmaker/presentation/templates/PopupLyricsTemplate.kt
  • modules/motionlib/src/main/java/com/tejpratapsingh/motionlib/ui/effects/BrightnessContrastEffect.kt
  • modules/motionlib/src/main/java/com/tejpratapsingh/motionlib/ui/effects/VignetteEffect.kt
  • modules/motionlib/src/main/java/com/tejpratapsingh/motionlib/ui/effects/ChainEffect.kt
  • modules/motionlib/src/main/java/com/tejpratapsingh/motionlib/ui/effects/PixelateEffect.kt

Comment on lines +53 to +69
lyrics.zipWithNext().forEach { (current, next) ->
wordWriterTextView(
text = current.text,
startFrame = current.frame,
endFrame = next.frame,
textSizeVariant = MotionTextVariant.H1,
textColor = "#FFFFFF",
writingSpeed = 1.0f,
textView =
AppCompatTextView(context).apply {
setPadding(32, 32, 32, 32)
textAlignment = AppCompatTextView.TEXT_ALIGNMENT_CENTER
gravity = Gravity.CENTER
},
)
transition(SlideTransition(SlideDirection.RIGHT_TO_LEFT), duration = 15)
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

zipWithNext() drops the last lyric segment (and all text when only one frame exists).

Both loops miss rendering the final lyric frame because zipWithNext() emits n-1 pairs only.

Suggested fix
-                lyrics.zipWithNext().forEach { (current, next) ->
+                lyrics.forEachIndexed { index, current ->
+                    val segmentEndFrame = lyrics.getOrNull(index + 1)?.frame ?: endFrame
                     wordWriterTextView(
                         text = current.text,
                         startFrame = current.frame,
-                        endFrame = next.frame,
+                        endFrame = segmentEndFrame,
                         textSizeVariant = MotionTextVariant.H1,
                         textColor = "`#FFFFFF`",
                         writingSpeed = 1.0f,
                         textView =
                             AppCompatTextView(context).apply {
                                 setPadding(32, 32, 32, 32)
                                 textAlignment = AppCompatTextView.TEXT_ALIGNMENT_CENTER
                                 gravity = Gravity.CENTER
                             },
                     )
                     transition(SlideTransition(SlideDirection.RIGHT_TO_LEFT), duration = 15)
                 }
@@
-                previewLyrics.zipWithNext().forEach { (current, next) ->
+                previewLyrics.forEachIndexed { index, current ->
+                    val segmentEndFrame = previewLyrics.getOrNull(index + 1)?.frame ?: endFrame
                     wordWriterTextView(
                         text = current.text,
                         startFrame = current.frame,
-                        endFrame = next.frame,
+                        endFrame = segmentEndFrame,
                         textSizeVariant = MotionTextVariant.H1,
                         textColor = "`#FFFFFF`",
                         writingSpeed = 1.0f,
                         textView =
                             AppCompatTextView(context).apply {
                                 setPadding(32, 32, 32, 32)
                                 textAlignment = AppCompatTextView.TEXT_ALIGNMENT_CENTER
                                 gravity = Gravity.CENTER
                             },
                     )
                     transition(SlideTransition(SlideDirection.RIGHT_TO_LEFT), duration = 15)
                 }

Also applies to: 102-118

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@modules/lyrics-maker/src/main/java/com/tejpratapsingh/lyricsmaker/presentation/templates/VintageLyricsTemplate.kt`
around lines 53 - 69, The current loop using lyrics.zipWithNext() in
VintageLyricsTemplate.kt (inside the block that calls wordWriterTextView and
transition with SlideTransition/SlideDirection) omits the final lyric, causing
the last frame (and single-frame cases) to be skipped; update the rendering to
iterate all items (e.g., use forEachIndexed or a for loop over lyrics) and after
processing each pair call wordWriterTextView with current.text/current.frame and
next.frame for timing, then separately handle the final lyric element by calling
wordWriterTextView with its startFrame and an appropriate endFrame (or duration)
so the last segment is rendered; apply the same fix to the second occurrence of
zipWithNext() at the other block (lines referenced in the comment).

Comment on lines +38 to +39
private val maskCache = mutableMapOf<Int, Bitmap>()

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Bound maskCache growth to avoid frame-count-sized bitmap retention.

At Line 38 and Line 69–79, one full-size bitmap can be retained per frame with no eviction. On long timelines this can escalate memory usage and trigger OOMs.

Suggested fix (bounded cache)
+import android.util.LruCache
...
-    private val maskCache = mutableMapOf<Int, Bitmap>()
+    private val maskCache = LruCache<Int, Bitmap>(16)
...
-        val maskBitmap =
-            maskCache[frame] ?: run {
+        val maskBitmap =
+            maskCache.get(frame) ?: run {
...
-                        maskCache[frame] = createdMask
+                        maskCache.put(frame, createdMask)
                         createdMask

Also applies to: 68-80

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@modules/ml-kit-ext/src/main/java/com/tejpratapsingh/motionlib/mlkit/effects/SubjectSegmentationEffect.kt`
around lines 38 - 39, The maskCache currently (in SubjectSegmentationEffect) is
an unbounded mutableMap and can retain one full-size Bitmap per frame; replace
it with a bounded cache (e.g., an LruCache<Int, Bitmap> or a LinkedHashMap with
access-order eviction) keyed the same way as maskCache so entries are evicted
when size exceeds a configured max (bytes or entry count), and ensure evicted
Bitmaps are recycled/closed to free memory; update all uses of maskCache (where
put/replace happens in the class) to use the new cache API and make the cache
access thread-safe if methods like produceMask() or onFrameProcessed() access it
from different threads.

Comment on lines +60 to +65
if (frame !in startFrame..endFrame) {
// If we are past the end frame, clear the effect
if (frame > endFrame) {
view.setRenderEffect(null)
}
return motionView

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Clear stale RenderEffect whenever segmentation is inactive.

At Line 60 and Line 62, the effect is cleared only when frame > endFrame. If playback seeks to a frame before startFrame, the previously applied effect can persist. Also at Line 89–95, when maskBitmap is null, the prior effect is not reset.

Suggested fix
-        if (frame !in startFrame..endFrame) {
-            // If we are past the end frame, clear the effect
-            if (frame > endFrame) {
-                view.setRenderEffect(null)
-            }
+        if (frame !in startFrame..endFrame) {
+            view.setRenderEffect(null)
             return motionView
         }
...
-        if (maskBitmap != null) {
+        if (maskBitmap != null) {
             val shader = RuntimeShader(MASK_SHADER)
             val maskShader = BitmapShader(maskBitmap, Shader.TileMode.CLAMP, Shader.TileMode.CLAMP)
             shader.setInputShader("mask", maskShader)
 
             view.setRenderEffect(RenderEffect.createRuntimeShaderEffect(shader, "content"))
+        } else {
+            view.setRenderEffect(null)
         }

Also applies to: 89-95

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@modules/ml-kit-ext/src/main/java/com/tejpratapsingh/motionlib/mlkit/effects/SubjectSegmentationEffect.kt`
around lines 60 - 65, The code only clears the RenderEffect when frame >
endFrame and when maskBitmap is null it leaves the prior effect in place; update
the logic in SubjectSegmentationEffect (the method that checks frame against
startFrame..endFrame and the branch that handles maskBitmap) to always clear the
prior effect via view.setRenderEffect(null) whenever the segmentation is
inactive (i.e., frame not in startFrame..endFrame, including frame < startFrame)
and also when maskBitmap is null, then return motionView; ensure you reference
the existing calls to view.setRenderEffect(null), the startFrame and endFrame
bounds check, and the maskBitmap null-check while making this change.

Comment on lines +49 to +53
if (frame !in startFrame..endFrame) {
if (frame > endFrame) {
view.setRenderEffect(null)
}
return motionView

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Clear the effect for all out-of-window frames.

At Line 49, frames before startFrame return without clearing any previously applied render effect, so seeking backward can leave stale vintage visuals on screen.

Suggested fix
-        if (frame !in startFrame..endFrame) {
-            if (frame > endFrame) {
-                view.setRenderEffect(null)
-            }
-            return motionView
-        }
+        if (frame !in startFrame..endFrame) {
+            view.setRenderEffect(null)
+            return motionView
+        }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if (frame !in startFrame..endFrame) {
if (frame > endFrame) {
view.setRenderEffect(null)
}
return motionView
if (frame !in startFrame..endFrame) {
view.setRenderEffect(null)
return motionView
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@modules/motionlib/src/main/java/com/tejpratapsingh/motionlib/ui/effects/VintageEffect.kt`
around lines 49 - 53, The code fails to clear the render effect when frame <
startFrame, leaving stale effects; update the out-of-window handling in
VintageEffect (the block that checks frame against startFrame..endFrame) to call
view.setRenderEffect(null) for any frame not in the range (both before
startFrame and after endFrame) before returning motionView so previously applied
effects are always cleared; locate the conditional using startFrame, endFrame,
view.setRenderEffect(null), and motionView and ensure the nulling happens
whenever frame !in startFrame..endFrame.

Comment on lines +56 to +62
val intensity =
MotionInterpolator.interpolateForRange(
interpolator = Interpolators(Easings.LINEAR),
currentFrame = frame,
frameRange = Pair(startFrame, endFrame),
valueRange = Pair(fromIntensity, toIntensity),
)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Clamp interpolated intensity to a safe range.

fromIntensity/toIntensity are externally configurable; clamping prevents invalid/excessive values from producing unstable sepia/vignette output.

Suggested fix
-        val intensity =
+        val intensity =
             MotionInterpolator.interpolateForRange(
                 interpolator = Interpolators(Easings.LINEAR),
                 currentFrame = frame,
                 frameRange = Pair(startFrame, endFrame),
                 valueRange = Pair(fromIntensity, toIntensity),
-            )
+            ).coerceIn(0f, 1f)
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
val intensity =
MotionInterpolator.interpolateForRange(
interpolator = Interpolators(Easings.LINEAR),
currentFrame = frame,
frameRange = Pair(startFrame, endFrame),
valueRange = Pair(fromIntensity, toIntensity),
)
val intensity =
MotionInterpolator.interpolateForRange(
interpolator = Interpolators(Easings.LINEAR),
currentFrame = frame,
frameRange = Pair(startFrame, endFrame),
valueRange = Pair(fromIntensity, toIntensity),
).coerceIn(0f, 1f)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@modules/motionlib/src/main/java/com/tejpratapsingh/motionlib/ui/effects/VintageEffect.kt`
around lines 56 - 62, The interpolated intensity value returned from
MotionInterpolator.interpolateForRange (assigned to intensity in
VintageEffect.kt) must be clamped to a safe range (e.g., 0f..1f) to avoid
invalid/excessive sepia/vignette parameters; after computing intensity, apply a
clamp (Kotlin's coerceIn or equivalent) and use the clamped value for subsequent
processing (replace usages of intensity with the clamped variable) so external
fromIntensity/toIntensity cannot produce out-of-range effects.

@tejpratap46 tejpratap46 merged commit f1976e0 into main May 30, 2026
8 of 10 checks passed
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