diff --git a/.idea/gradle.xml b/.idea/gradle.xml
index 4f00e00a..88d99e07 100644
--- a/.idea/gradle.xml
+++ b/.idea/gradle.xml
@@ -21,6 +21,7 @@
+
diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml
index 47b7e964..84a25455 100644
--- a/gradle/libs.versions.toml
+++ b/gradle/libs.versions.toml
@@ -34,6 +34,7 @@ runtimeVersion = "1.10.6"
crashlyticsVersion = "3.0.7"
googleServicesVersion = "4.4.4"
media3Version = "1.10.0"
+mlkitSubjectSegmentationVersion = "16.0.0-beta1"
[libraries]
androidx-constraintlayout = { module = "androidx.constraintlayout:constraintlayout", version.ref = "constraintlayoutVersion" }
@@ -84,6 +85,7 @@ androidx-ui-test-junit4 = { group = "androidx.compose.ui", name = "ui-test-junit
androidx-material3 = { group = "androidx.compose.material3", name = "material3" }
androidx-navigation-compose = { group = "androidx.navigation", name = "navigation-compose", version.ref = "navigationComposeVersion" }
androidx-compose-runtime = { group = "androidx.compose.runtime", name = "runtime", version.ref = "runtimeVersion" }
+mlkit-subject-segmentation = { module = "com.google.android.gms:play-services-mlkit-subject-segmentation", version.ref = "mlkitSubjectSegmentationVersion" }
compose-material-icons-extended = { module = "androidx.compose.material:material-icons-extended" }
supabase-bom = { module = "io.github.jan-tennert.supabase:bom", version.ref = "supabaseBomVersion" }
diff --git a/modules/lyrics-maker/src/main/java/com/tejpratapsingh/lyricsmaker/presentation/templates/LyricsTemplateRegistry.kt b/modules/lyrics-maker/src/main/java/com/tejpratapsingh/lyricsmaker/presentation/templates/LyricsTemplateRegistry.kt
index 1bb55337..ec52423b 100644
--- a/modules/lyrics-maker/src/main/java/com/tejpratapsingh/lyricsmaker/presentation/templates/LyricsTemplateRegistry.kt
+++ b/modules/lyrics-maker/src/main/java/com/tejpratapsingh/lyricsmaker/presentation/templates/LyricsTemplateRegistry.kt
@@ -14,6 +14,7 @@ object LyricsTemplateRegistry {
GradientLyricsTemplate,
RainbowLyricsTemplate,
AccentLyricsTemplate,
+ VintageLyricsTemplate,
)
fun getTemplate(name: String?): MotionTemplate = templates.find { it.name == name } ?: PopupLyricsTemplate
diff --git a/modules/lyrics-maker/src/main/java/com/tejpratapsingh/lyricsmaker/presentation/templates/PopupLyricsTemplate.kt b/modules/lyrics-maker/src/main/java/com/tejpratapsingh/lyricsmaker/presentation/templates/PopupLyricsTemplate.kt
index 3cba644e..dff8c77a 100644
--- a/modules/lyrics-maker/src/main/java/com/tejpratapsingh/lyricsmaker/presentation/templates/PopupLyricsTemplate.kt
+++ b/modules/lyrics-maker/src/main/java/com/tejpratapsingh/lyricsmaker/presentation/templates/PopupLyricsTemplate.kt
@@ -12,6 +12,7 @@ import com.tejpratapsingh.motionlib.templates.extensions.motionImageView
import com.tejpratapsingh.motionlib.templates.extensions.popUpTextView
import com.tejpratapsingh.motionlib.templates.extensions.translucentMotionView
import com.tejpratapsingh.motionlib.templates.model.MotionTemplate
+import com.tejpratapsingh.motionlib.ui.effects.BlurEffect
val PopupLyricsTemplate: MotionTemplate =
motionTemplate("Multi Lyrics Template") {
@@ -41,6 +42,13 @@ val PopupLyricsTemplate: MotionTemplate =
alpha = 0.4f,
startFrame = lyrics.first().frame,
endFrame = lyrics.last().frame,
+ effects =
+ listOf(
+ BlurEffect(
+ lyrics.first().frame,
+ lyrics.last().frame,
+ ),
+ ),
)
lyrics.zipWithNext().forEach { (current, next) ->
@@ -86,6 +94,13 @@ val PopupLyricsTemplate: MotionTemplate =
alpha = 0.4f,
startFrame = previewLyrics.first().frame,
endFrame = previewLyrics.last().frame,
+ effects =
+ listOf(
+ BlurEffect(
+ previewLyrics.first().frame,
+ previewLyrics.last().frame,
+ ),
+ ),
)
previewLyrics.zipWithNext().forEach { (current, next) ->
diff --git a/modules/lyrics-maker/src/main/java/com/tejpratapsingh/lyricsmaker/presentation/templates/VintageLyricsTemplate.kt b/modules/lyrics-maker/src/main/java/com/tejpratapsingh/lyricsmaker/presentation/templates/VintageLyricsTemplate.kt
new file mode 100644
index 00000000..3a474209
--- /dev/null
+++ b/modules/lyrics-maker/src/main/java/com/tejpratapsingh/lyricsmaker/presentation/templates/VintageLyricsTemplate.kt
@@ -0,0 +1,121 @@
+package com.tejpratapsingh.lyricsmaker.presentation.templates
+
+import android.view.Gravity
+import androidx.appcompat.widget.AppCompatTextView
+import androidx.core.net.toUri
+import com.tejpratapsingh.lyricsmaker.data.lrc.SyncedLyricFrame
+import com.tejpratapsingh.motionlib.core.MotionTextVariant
+import com.tejpratapsingh.motionlib.core.motion.transitions.SlideDirection
+import com.tejpratapsingh.motionlib.core.motion.transitions.SlideTransition
+import com.tejpratapsingh.motionlib.templates.dsl.motionTemplate
+import com.tejpratapsingh.motionlib.templates.extensions.motionImageView
+import com.tejpratapsingh.motionlib.templates.extensions.translucentMotionView
+import com.tejpratapsingh.motionlib.templates.extensions.wordWriterTextView
+import com.tejpratapsingh.motionlib.templates.model.MotionTemplate
+import com.tejpratapsingh.motionlib.ui.effects.VibrateEffect
+import com.tejpratapsingh.motionlib.ui.effects.VintageEffect
+
+val VintageLyricsTemplate: MotionTemplate =
+ motionTemplate("Vintage Lyrics Template") {
+ parameters {
+ string("songName")
+ string("image", defaultValue = null)
+ }
+
+ content {
+ val image = data.getString("image")
+ val lyrics = data.get>("lyrics") ?: emptyList()
+
+ if (lyrics.isNotEmpty()) {
+ val startFrame = lyrics.first().frame
+ val endFrame = lyrics.last().frame
+
+ image?.let {
+ motionImageView(
+ startFrame = startFrame,
+ endFrame = endFrame,
+ imageUri = image.toUri(),
+ effects =
+ listOf(
+ VibrateEffect(startFrame, endFrame, amplitude = 10f, frequency = 0.05f),
+ VintageEffect(startFrame, endFrame, fromIntensity = 0.8f, toIntensity = 1.0f),
+ ),
+ )
+ }
+
+ translucentMotionView(
+ color = "#000000",
+ alpha = 0.3f,
+ startFrame = startFrame,
+ endFrame = endFrame,
+ )
+
+ 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)
+ }
+ }
+ }
+
+ preview {
+ val image = data.getString("image")
+ val lyrics = data.get>("lyrics") ?: emptyList()
+
+ if (lyrics.isNotEmpty()) {
+ val previewLyrics = lyrics.take(3)
+ val startFrame = previewLyrics.first().frame
+ val endFrame = previewLyrics.last().frame
+
+ image?.let {
+ motionImageView(
+ startFrame = startFrame,
+ endFrame = endFrame,
+ imageUri = image.toUri(),
+ effects =
+ listOf(
+ VibrateEffect(startFrame, endFrame, amplitude = 10f, frequency = 0.05f),
+ VintageEffect(startFrame, endFrame, fromIntensity = 0.8f, toIntensity = 1.0f),
+ ),
+ )
+ }
+
+ translucentMotionView(
+ color = "#000000",
+ alpha = 0.3f,
+ startFrame = startFrame,
+ endFrame = endFrame,
+ )
+
+ previewLyrics.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)
+ }
+ }
+ }
+ }
diff --git a/modules/ml-kit-ext/build.gradle b/modules/ml-kit-ext/build.gradle
new file mode 100644
index 00000000..e62b2ec6
--- /dev/null
+++ b/modules/ml-kit-ext/build.gradle
@@ -0,0 +1,55 @@
+plugins {
+ alias(libs.plugins.android.library)
+ id 'maven-publish'
+}
+
+android {
+ compileSdk 36
+
+ defaultConfig {
+ minSdk 25
+
+ testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
+ consumerProguardFiles "consumer-rules.pro"
+ }
+
+ buildTypes {
+ release {
+ minifyEnabled false
+ proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
+ }
+ }
+ compileOptions {
+ sourceCompatibility JavaVersion.VERSION_11
+ targetCompatibility JavaVersion.VERSION_11
+ }
+
+ namespace 'com.tejpratapsingh.motionlib.mlkit'
+ lint {
+ targetSdk 36
+ }
+ testOptions {
+ targetSdk 36
+ }
+ publishing {
+ singleVariant("release")
+ }
+}
+
+afterEvaluate {
+ publishing {
+ publications {
+ release(MavenPublication) {
+ from components.release
+ }
+ }
+ }
+}
+
+dependencies {
+ api project(path: ':modules:core')
+ implementation libs.androidx.appcompat
+ implementation libs.mlkit.subject.segmentation
+
+ testImplementation libs.junit
+}
diff --git a/modules/ml-kit-ext/consumer-rules.pro b/modules/ml-kit-ext/consumer-rules.pro
new file mode 100644
index 00000000..e69de29b
diff --git a/modules/ml-kit-ext/proguard-rules.pro b/modules/ml-kit-ext/proguard-rules.pro
new file mode 100644
index 00000000..e69de29b
diff --git a/modules/ml-kit-ext/src/main/java/com/tejpratapsingh/motionlib/mlkit/MLKitImageProcessor.kt b/modules/ml-kit-ext/src/main/java/com/tejpratapsingh/motionlib/mlkit/MLKitImageProcessor.kt
new file mode 100644
index 00000000..332da1fb
--- /dev/null
+++ b/modules/ml-kit-ext/src/main/java/com/tejpratapsingh/motionlib/mlkit/MLKitImageProcessor.kt
@@ -0,0 +1,16 @@
+package com.tejpratapsingh.motionlib.mlkit
+
+import com.tejpratapsingh.motionlib.core.MotionPlugin
+import com.tejpratapsingh.motionlib.mlkit.plugins.SubjectSegmentationPlugin
+
+/**
+ * Entry point for ML Kit based image processing plugins.
+ */
+object MLKitImageProcessor {
+ /**
+ * Plugin for background removal using ML Kit Subject Segmentation.
+ */
+ val subjectSegmentationPlugin: MotionPlugin by lazy {
+ SubjectSegmentationPlugin()
+ }
+}
diff --git a/modules/ml-kit-ext/src/main/java/com/tejpratapsingh/motionlib/mlkit/effects/SubjectSegmentationEffect.kt b/modules/ml-kit-ext/src/main/java/com/tejpratapsingh/motionlib/mlkit/effects/SubjectSegmentationEffect.kt
new file mode 100644
index 00000000..c54b17ce
--- /dev/null
+++ b/modules/ml-kit-ext/src/main/java/com/tejpratapsingh/motionlib/mlkit/effects/SubjectSegmentationEffect.kt
@@ -0,0 +1,117 @@
+package com.tejpratapsingh.motionlib.mlkit.effects
+
+import android.graphics.Bitmap
+import android.graphics.BitmapShader
+import android.graphics.Color
+import android.graphics.RenderEffect
+import android.graphics.RuntimeShader
+import android.graphics.Shader
+import android.os.Build
+import android.view.View
+import androidx.core.graphics.createBitmap
+import com.google.android.gms.tasks.Tasks
+import com.google.mlkit.vision.common.InputImage
+import com.google.mlkit.vision.segmentation.subject.SubjectSegmentation
+import com.google.mlkit.vision.segmentation.subject.SubjectSegmenterOptions
+import com.tejpratapsingh.motionlib.core.MotionEffect
+import com.tejpratapsingh.motionlib.core.MotionView
+import java.nio.FloatBuffer
+
+/**
+ * A [MotionEffect] that uses ML Kit Subject Segmentation to remove background.
+ * Works only on Android T (API 33) and above due to [RenderEffect] and [RuntimeShader].
+ */
+class SubjectSegmentationEffect(
+ override val startFrame: Int,
+ override val endFrame: Int,
+) : MotionEffect {
+ override lateinit var motionView: MotionView
+
+ private val options =
+ SubjectSegmenterOptions
+ .Builder()
+ .enableForegroundConfidenceMask()
+ .build()
+
+ private val segmenter = SubjectSegmentation.getClient(options)
+
+ private val maskCache = mutableMapOf()
+
+ @Suppress("ktlint:standard:property-naming")
+ private val MASK_SHADER =
+ """
+ uniform shader content;
+ uniform shader mask;
+
+ half4 main(float2 fragCoord) {
+ half4 color = content.eval(fragCoord);
+ half4 maskColor = mask.eval(fragCoord);
+ // Use alpha channel of the mask for transparency
+ return color * maskColor.a;
+ }
+ """.trimIndent()
+
+ 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 we are past the end frame, clear the effect
+ if (frame > endFrame) {
+ view.setRenderEffect(null)
+ }
+ return motionView
+ }
+
+ val maskBitmap =
+ maskCache[frame] ?: run {
+ val bitmap = motionView.getViewBitmap()
+ val image = InputImage.fromBitmap(bitmap, 0)
+ try {
+ // This is synchronous and can be slow.
+ // Ideally, this should be pre-processed or run on a background thread.
+ val result = Tasks.await(segmenter.process(image))
+ val mask = result.foregroundConfidenceMask // This is a FloatBuffer
+ if (mask != null) {
+ val createdMask = createBitmapFromMask(mask, bitmap.width, bitmap.height)
+ maskCache[frame] = createdMask
+ createdMask
+ } else {
+ null
+ }
+ } catch (_: Exception) {
+ 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"))
+ }
+
+ return motionView
+ }
+
+ private fun createBitmapFromMask(
+ mask: FloatBuffer,
+ width: Int,
+ height: Int,
+ ): Bitmap {
+ val bitmap = createBitmap(width, height)
+ mask.rewind()
+ val pixels = IntArray(width * height)
+ for (i in 0 until width * height) {
+ val confidence = if (mask.hasRemaining()) mask.get() else 0.0f
+ val alpha = (confidence * 255).toInt()
+ // Set alpha for the mask bitmap, color doesn't strictly matter if we only use alpha in shader
+ pixels[i] = Color.argb(alpha, 255, 255, 255)
+ }
+ bitmap.setPixels(pixels, 0, width, 0, 0, width, height)
+ return bitmap
+ }
+}
diff --git a/modules/ml-kit-ext/src/main/java/com/tejpratapsingh/motionlib/mlkit/plugins/SubjectSegmentationPlugin.kt b/modules/ml-kit-ext/src/main/java/com/tejpratapsingh/motionlib/mlkit/plugins/SubjectSegmentationPlugin.kt
new file mode 100644
index 00000000..d58fd280
--- /dev/null
+++ b/modules/ml-kit-ext/src/main/java/com/tejpratapsingh/motionlib/mlkit/plugins/SubjectSegmentationPlugin.kt
@@ -0,0 +1,67 @@
+package com.tejpratapsingh.motionlib.mlkit.plugins
+
+import android.graphics.Bitmap
+import android.graphics.Color
+import com.google.android.gms.tasks.Tasks
+import com.google.mlkit.vision.common.InputImage
+import com.google.mlkit.vision.segmentation.subject.SubjectSegmentation
+import com.google.mlkit.vision.segmentation.subject.SubjectSegmenterOptions
+import com.tejpratapsingh.motionlib.core.MotionPlugin
+import java.nio.FloatBuffer
+
+/**
+ * A [MotionPlugin] that uses ML Kit Subject Segmentation to remove background from a [Bitmap].
+ * This processes the bitmap directly and returns a new bitmap with the background removed.
+ */
+class SubjectSegmentationPlugin : MotionPlugin {
+
+ private val options =
+ SubjectSegmenterOptions.Builder()
+ .enableForegroundConfidenceMask()
+ .build()
+
+ private val segmenter = SubjectSegmentation.getClient(options)
+
+ override fun apply(input: Bitmap): Bitmap {
+ val image = InputImage.fromBitmap(input, 0)
+ return try {
+ // This is synchronous and can be slow.
+ val result = Tasks.await(segmenter.process(image))
+ val mask = result.foregroundConfidenceMask
+ if (mask != null) {
+ applyMaskToBitmap(input, mask)
+ } else {
+ input
+ }
+ } catch (e: Exception) {
+ input
+ }
+ }
+
+ private fun applyMaskToBitmap(
+ source: Bitmap,
+ mask: FloatBuffer
+ ): Bitmap {
+ val width = source.width
+ val height = source.height
+ val result = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888)
+
+ mask.rewind()
+ val pixels = IntArray(width * height)
+ source.getPixels(pixels, 0, width, 0, 0, width, height)
+
+ for (i in 0 until width * height) {
+ val confidence = if (mask.hasRemaining()) mask.get() else 0.0f
+ val alpha = (Color.alpha(pixels[i]) * confidence).toInt()
+ pixels[i] = Color.argb(
+ alpha,
+ Color.red(pixels[i]),
+ Color.green(pixels[i]),
+ Color.blue(pixels[i])
+ )
+ }
+
+ result.setPixels(pixels, 0, width, 0, 0, width, height)
+ return result
+ }
+}
diff --git a/modules/motionlib/src/main/java/com/tejpratapsingh/motionlib/ui/effects/BrightnessContrastEffect.kt b/modules/motionlib/src/main/java/com/tejpratapsingh/motionlib/ui/effects/BrightnessContrastEffect.kt
new file mode 100644
index 00000000..60491a67
--- /dev/null
+++ b/modules/motionlib/src/main/java/com/tejpratapsingh/motionlib/ui/effects/BrightnessContrastEffect.kt
@@ -0,0 +1,72 @@
+package com.tejpratapsingh.motionlib.ui.effects
+
+import android.graphics.ColorMatrixColorFilter
+import android.graphics.RenderEffect
+import android.os.Build
+import android.view.View
+import com.tejpratapsingh.motionlib.core.MotionEffect
+import com.tejpratapsingh.motionlib.core.MotionView
+import com.tejpratapsingh.motionlib.core.animation.Easings
+import com.tejpratapsingh.motionlib.core.animation.Interpolators
+import com.tejpratapsingh.motionlib.core.animation.MotionInterpolator
+
+/**
+ * A [MotionEffect] that adjusts brightness and contrast using [RenderEffect].
+ * Brightness: 0.0 is identity, negative is darker, positive is brighter.
+ * Contrast: 1.0 is identity, higher is more contrast.
+ */
+class BrightnessContrastEffect(
+ override val startFrame: Int,
+ override val endFrame: Int,
+ val fromBrightness: Float = 0.0f,
+ val toBrightness: Float = 0.0f,
+ val fromContrast: Float = 1.0f,
+ val toContrast: Float = 1.0f,
+) : MotionEffect {
+ override lateinit var motionView: MotionView
+
+ 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.S) return motionView
+
+ if (frame !in startFrame..endFrame) {
+ if (frame > endFrame) {
+ view.setRenderEffect(null)
+ }
+ return motionView
+ }
+
+ val brightness = MotionInterpolator.interpolateForRange(
+ interpolator = Interpolators(Easings.LINEAR),
+ currentFrame = frame,
+ frameRange = Pair(startFrame, endFrame),
+ valueRange = Pair(fromBrightness, toBrightness),
+ )
+
+ val contrast = MotionInterpolator.interpolateForRange(
+ interpolator = Interpolators(Easings.LINEAR),
+ currentFrame = frame,
+ frameRange = Pair(startFrame, endFrame),
+ valueRange = Pair(fromContrast, toContrast),
+ )
+
+ // Matrix for contrast and brightness
+ // contrast * (channel - 0.5) + 0.5 + brightness
+ // = contrast * channel - 0.5 * contrast + 0.5 + brightness
+ val t = (1.0f - contrast) / 2.0f * 255.0f + brightness * 255.0f
+
+ val matrix = floatArrayOf(
+ contrast, 0f, 0f, 0f, t,
+ 0f, contrast, 0f, 0f, t,
+ 0f, 0f, contrast, 0f, t,
+ 0f, 0f, 0f, 1f, 0f
+ )
+
+ val colorFilter = ColorMatrixColorFilter(matrix)
+ view.setRenderEffect(RenderEffect.createColorFilterEffect(colorFilter))
+
+ return motionView
+ }
+}
diff --git a/modules/motionlib/src/main/java/com/tejpratapsingh/motionlib/ui/effects/ChainEffect.kt b/modules/motionlib/src/main/java/com/tejpratapsingh/motionlib/ui/effects/ChainEffect.kt
new file mode 100644
index 00000000..dde977a6
--- /dev/null
+++ b/modules/motionlib/src/main/java/com/tejpratapsingh/motionlib/ui/effects/ChainEffect.kt
@@ -0,0 +1,52 @@
+package com.tejpratapsingh.motionlib.ui.effects
+
+import android.graphics.RenderEffect
+import android.os.Build
+import android.view.View
+import com.tejpratapsingh.motionlib.core.MotionEffect
+import com.tejpratapsingh.motionlib.core.MotionView
+
+/**
+ * A [MotionEffect] that chains two other [MotionEffect]s together using [RenderEffect.createChainEffect].
+ * Note: This implementation is a placeholder to show how chaining could work.
+ * In the current architecture, effects apply themselves directly to the view.
+ */
+class ChainEffect(
+ override val startFrame: Int,
+ override val endFrame: Int,
+ val outerEffect: MotionEffect,
+ val innerEffect: MotionEffect,
+) : MotionEffect {
+ private var _motionView: MotionView? = null
+ override var motionView: MotionView
+ get() = _motionView!!
+ set(value) {
+ _motionView = value
+ outerEffect.motionView = value
+ innerEffect.motionView = value
+ }
+
+ 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.S) return motionView
+
+ if (frame !in startFrame..endFrame) {
+ if (frame > endFrame) {
+ view.setRenderEffect(null)
+ }
+ return motionView
+ }
+
+ // 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)
+
+ return motionView
+ }
+}
diff --git a/modules/motionlib/src/main/java/com/tejpratapsingh/motionlib/ui/effects/GrayscaleEffect.kt b/modules/motionlib/src/main/java/com/tejpratapsingh/motionlib/ui/effects/GrayscaleEffect.kt
new file mode 100644
index 00000000..e796d305
--- /dev/null
+++ b/modules/motionlib/src/main/java/com/tejpratapsingh/motionlib/ui/effects/GrayscaleEffect.kt
@@ -0,0 +1,60 @@
+package com.tejpratapsingh.motionlib.ui.effects
+
+import android.graphics.ColorMatrix
+import android.graphics.ColorMatrixColorFilter
+import android.graphics.RenderEffect
+import android.os.Build
+import android.view.View
+import com.tejpratapsingh.motionlib.core.MotionEffect
+import com.tejpratapsingh.motionlib.core.MotionView
+import com.tejpratapsingh.motionlib.core.animation.Easings
+import com.tejpratapsingh.motionlib.core.animation.Interpolators
+import com.tejpratapsingh.motionlib.core.animation.MotionInterpolator
+
+/**
+ * A [MotionEffect] that applies a grayscale filter using [RenderEffect].
+ * Animates saturation from [fromSaturation] to [toSaturation].
+ * Saturation 1.0 is full color, 0.0 is grayscale.
+ */
+class GrayscaleEffect(
+ override val startFrame: Int,
+ override val endFrame: Int,
+ val fromSaturation: Float = 1.0f,
+ val toSaturation: Float = 0.0f,
+) : MotionEffect {
+ override lateinit var motionView: MotionView
+
+ 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.S) return motionView
+
+ 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
+ }
+
+ val saturation = MotionInterpolator.interpolateForRange(
+ interpolator = Interpolators(Easings.LINEAR),
+ currentFrame = frame,
+ frameRange = Pair(startFrame, endFrame),
+ valueRange = Pair(fromSaturation, toSaturation),
+ )
+
+ val matrix = ColorMatrix().apply {
+ setSaturation(saturation)
+ }
+
+ val colorFilter = ColorMatrixColorFilter(matrix)
+ view.setRenderEffect(RenderEffect.createColorFilterEffect(colorFilter))
+
+ return motionView
+ }
+}
diff --git a/modules/motionlib/src/main/java/com/tejpratapsingh/motionlib/ui/effects/InvertEffect.kt b/modules/motionlib/src/main/java/com/tejpratapsingh/motionlib/ui/effects/InvertEffect.kt
new file mode 100644
index 00000000..c8c21157
--- /dev/null
+++ b/modules/motionlib/src/main/java/com/tejpratapsingh/motionlib/ui/effects/InvertEffect.kt
@@ -0,0 +1,76 @@
+package com.tejpratapsingh.motionlib.ui.effects
+
+import android.graphics.ColorMatrixColorFilter
+import android.graphics.RenderEffect
+import android.os.Build
+import android.view.View
+import com.tejpratapsingh.motionlib.core.MotionEffect
+import com.tejpratapsingh.motionlib.core.MotionView
+import com.tejpratapsingh.motionlib.core.animation.Easings
+import com.tejpratapsingh.motionlib.core.animation.Interpolators
+import com.tejpratapsingh.motionlib.core.animation.MotionInterpolator
+
+/**
+ * A [MotionEffect] that inverts the colors of the view using [RenderEffect].
+ * Animates intensity from [fromIntensity] to [toIntensity].
+ */
+class InvertEffect(
+ override val startFrame: Int,
+ override val endFrame: Int,
+ val fromIntensity: Float = 0.0f,
+ val toIntensity: Float = 1.0f,
+) : MotionEffect {
+ override lateinit var motionView: MotionView
+
+ 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.S) 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),
+ )
+
+ // Invert matrix:
+ // R' = -1*R + 255
+ // G' = -1*G + 255
+ // B' = -1*B + 255
+ // Scaled to 0-1 for ColorMatrix:
+ // R' = -1*R + 1
+
+ val invertMatrix = floatArrayOf(
+ -1f, 0f, 0f, 0f, 255f,
+ 0f, -1f, 0f, 0f, 255f,
+ 0f, 0f, -1f, 0f, 255f,
+ 0f, 0f, 0f, 1f, 0f
+ )
+
+ val identityMatrix = floatArrayOf(
+ 1f, 0f, 0f, 0f, 0f,
+ 0f, 1f, 0f, 0f, 0f,
+ 0f, 0f, 1f, 0f, 0f,
+ 0f, 0f, 0f, 1f, 0f
+ )
+
+ val resultMatrix = FloatArray(20)
+ for (i in 0 until 20) {
+ resultMatrix[i] = identityMatrix[i] + (invertMatrix[i] - identityMatrix[i]) * intensity
+ }
+
+ val colorFilter = ColorMatrixColorFilter(resultMatrix)
+ view.setRenderEffect(RenderEffect.createColorFilterEffect(colorFilter))
+
+ return motionView
+ }
+}
diff --git a/modules/motionlib/src/main/java/com/tejpratapsingh/motionlib/ui/effects/OffsetEffect.kt b/modules/motionlib/src/main/java/com/tejpratapsingh/motionlib/ui/effects/OffsetEffect.kt
new file mode 100644
index 00000000..e8906213
--- /dev/null
+++ b/modules/motionlib/src/main/java/com/tejpratapsingh/motionlib/ui/effects/OffsetEffect.kt
@@ -0,0 +1,57 @@
+package com.tejpratapsingh.motionlib.ui.effects
+
+import android.graphics.RenderEffect
+import android.os.Build
+import android.view.View
+import com.tejpratapsingh.motionlib.core.MotionEffect
+import com.tejpratapsingh.motionlib.core.MotionView
+import com.tejpratapsingh.motionlib.core.animation.Easings
+import com.tejpratapsingh.motionlib.core.animation.Interpolators
+import com.tejpratapsingh.motionlib.core.animation.MotionInterpolator
+
+/**
+ * A [MotionEffect] that applies a rendering offset using [RenderEffect.createOffsetEffect].
+ * This offsets the content without changing the layout bounds.
+ */
+class OffsetEffect(
+ override val startFrame: Int,
+ override val endFrame: Int,
+ val fromOffsetX: Float = 0f,
+ val toOffsetX: Float = 0f,
+ val fromOffsetY: Float = 0f,
+ val toOffsetY: Float = 0f,
+) : MotionEffect {
+ override lateinit var motionView: MotionView
+
+ 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.S) return motionView
+
+ if (frame !in startFrame..endFrame) {
+ if (frame > endFrame) {
+ view.setRenderEffect(null)
+ }
+ return motionView
+ }
+
+ val offsetX = MotionInterpolator.interpolateForRange(
+ interpolator = Interpolators(Easings.LINEAR),
+ currentFrame = frame,
+ frameRange = Pair(startFrame, endFrame),
+ valueRange = Pair(fromOffsetX, toOffsetX),
+ )
+
+ val offsetY = MotionInterpolator.interpolateForRange(
+ interpolator = Interpolators(Easings.LINEAR),
+ currentFrame = frame,
+ frameRange = Pair(startFrame, endFrame),
+ valueRange = Pair(fromOffsetY, toOffsetY),
+ )
+
+ view.setRenderEffect(RenderEffect.createOffsetEffect(offsetX, offsetY))
+
+ return motionView
+ }
+}
diff --git a/modules/motionlib/src/main/java/com/tejpratapsingh/motionlib/ui/effects/PixelateEffect.kt b/modules/motionlib/src/main/java/com/tejpratapsingh/motionlib/ui/effects/PixelateEffect.kt
new file mode 100644
index 00000000..da4c3914
--- /dev/null
+++ b/modules/motionlib/src/main/java/com/tejpratapsingh/motionlib/ui/effects/PixelateEffect.kt
@@ -0,0 +1,65 @@
+package com.tejpratapsingh.motionlib.ui.effects
+
+import android.graphics.RenderEffect
+import android.graphics.RuntimeShader
+import android.os.Build
+import android.view.View
+import com.tejpratapsingh.motionlib.core.MotionEffect
+import com.tejpratapsingh.motionlib.core.MotionView
+import com.tejpratapsingh.motionlib.core.animation.Easings
+import com.tejpratapsingh.motionlib.core.animation.Interpolators
+import com.tejpratapsingh.motionlib.core.animation.MotionInterpolator
+
+/**
+ * A [MotionEffect] that applies a pixelation effect using AGSL.
+ * Animates pixel size from [fromPixelSize] to [toPixelSize].
+ */
+class PixelateEffect(
+ override val startFrame: Int,
+ override val endFrame: Int,
+ val fromPixelSize: Float = 1f,
+ val toPixelSize: Float = 20f,
+) : MotionEffect {
+ override lateinit var motionView: MotionView
+
+ private val PIXELATE_SHADER = """
+ uniform shader content;
+ uniform float pixelSize;
+
+ half4 main(float2 fragCoord) {
+ if (pixelSize <= 1.0) {
+ return content.eval(fragCoord);
+ }
+ float2 p = floor(fragCoord / pixelSize) * pixelSize;
+ return content.eval(p);
+ }
+ """.trimIndent()
+
+ 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
+ }
+}
diff --git a/modules/motionlib/src/main/java/com/tejpratapsingh/motionlib/ui/effects/SepiaEffect.kt b/modules/motionlib/src/main/java/com/tejpratapsingh/motionlib/ui/effects/SepiaEffect.kt
new file mode 100644
index 00000000..510af887
--- /dev/null
+++ b/modules/motionlib/src/main/java/com/tejpratapsingh/motionlib/ui/effects/SepiaEffect.kt
@@ -0,0 +1,76 @@
+package com.tejpratapsingh.motionlib.ui.effects
+
+import android.graphics.ColorMatrix
+import android.graphics.ColorMatrixColorFilter
+import android.graphics.RenderEffect
+import android.os.Build
+import android.view.View
+import com.tejpratapsingh.motionlib.core.MotionEffect
+import com.tejpratapsingh.motionlib.core.MotionView
+import com.tejpratapsingh.motionlib.core.animation.Easings
+import com.tejpratapsingh.motionlib.core.animation.Interpolators
+import com.tejpratapsingh.motionlib.core.animation.MotionInterpolator
+
+/**
+ * A [MotionEffect] that applies a sepia filter using [RenderEffect].
+ * Animates intensity from [fromIntensity] to [toIntensity].
+ */
+class SepiaEffect(
+ override val startFrame: Int,
+ override val endFrame: Int,
+ val fromIntensity: Float = 0.0f,
+ val toIntensity: Float = 1.0f,
+) : MotionEffect {
+ override lateinit var motionView: MotionView
+
+ 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.S) 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),
+ )
+
+ // Sepia matrix (standard)
+ // R' = (R * .393) + (G * .769) + (B * .189)
+ // G' = (R * .349) + (G * .686) + (B * .168)
+ // B' = (R * .272) + (G * .534) + (B * .131)
+
+ val sepiaMatrix = floatArrayOf(
+ 0.393f, 0.769f, 0.189f, 0f, 0f,
+ 0.349f, 0.686f, 0.168f, 0f, 0f,
+ 0.272f, 0.534f, 0.131f, 0f, 0f,
+ 0f, 0f, 0f, 1f, 0f
+ )
+
+ // We can interpolate between identity and sepia
+ val identityMatrix = floatArrayOf(
+ 1f, 0f, 0f, 0f, 0f,
+ 0f, 1f, 0f, 0f, 0f,
+ 0f, 0f, 1f, 0f, 0f,
+ 0f, 0f, 0f, 1f, 0f
+ )
+
+ val resultMatrix = FloatArray(20)
+ for (i in 0 until 20) {
+ resultMatrix[i] = identityMatrix[i] + (sepiaMatrix[i] - identityMatrix[i]) * intensity
+ }
+
+ val colorFilter = ColorMatrixColorFilter(resultMatrix)
+ view.setRenderEffect(RenderEffect.createColorFilterEffect(colorFilter))
+
+ return motionView
+ }
+}
diff --git a/modules/motionlib/src/main/java/com/tejpratapsingh/motionlib/ui/effects/VignetteEffect.kt b/modules/motionlib/src/main/java/com/tejpratapsingh/motionlib/ui/effects/VignetteEffect.kt
new file mode 100644
index 00000000..6681930b
--- /dev/null
+++ b/modules/motionlib/src/main/java/com/tejpratapsingh/motionlib/ui/effects/VignetteEffect.kt
@@ -0,0 +1,71 @@
+package com.tejpratapsingh.motionlib.ui.effects
+
+import android.graphics.RenderEffect
+import android.graphics.RuntimeShader
+import android.os.Build
+import android.view.View
+import com.tejpratapsingh.motionlib.core.MotionEffect
+import com.tejpratapsingh.motionlib.core.MotionView
+import com.tejpratapsingh.motionlib.core.animation.Easings
+import com.tejpratapsingh.motionlib.core.animation.Interpolators
+import com.tejpratapsingh.motionlib.core.animation.MotionInterpolator
+
+/**
+ * A [MotionEffect] that applies a vignette effect using AGSL.
+ * Animates intensity from [fromIntensity] to [toIntensity].
+ */
+class VignetteEffect(
+ override val startFrame: Int,
+ override val endFrame: Int,
+ val fromIntensity: Float = 0.0f,
+ val toIntensity: Float = 0.5f,
+) : MotionEffect {
+ override lateinit var motionView: MotionView
+
+ @Suppress("ktlint:standard:property-naming")
+ private val VIGNETTE_SHADER =
+ """
+ uniform shader content;
+ uniform float2 size;
+ uniform float intensity;
+
+ half4 main(float2 fragCoord) {
+ half4 color = content.eval(fragCoord);
+ float2 uv = fragCoord / size;
+ uv *= 1.0 - uv.yx;
+ float vig = uv.x*uv.y * 15.0;
+ vig = pow(vig, intensity);
+ return color * vig;
+ }
+ """.trimIndent()
+
+ 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
+ }
+}
diff --git a/modules/motionlib/src/main/java/com/tejpratapsingh/motionlib/ui/effects/VintageEffect.kt b/modules/motionlib/src/main/java/com/tejpratapsingh/motionlib/ui/effects/VintageEffect.kt
new file mode 100644
index 00000000..3d6223ec
--- /dev/null
+++ b/modules/motionlib/src/main/java/com/tejpratapsingh/motionlib/ui/effects/VintageEffect.kt
@@ -0,0 +1,102 @@
+package com.tejpratapsingh.motionlib.ui.effects
+
+import android.graphics.ColorMatrixColorFilter
+import android.graphics.RenderEffect
+import android.graphics.RuntimeShader
+import android.os.Build
+import android.view.View
+import com.tejpratapsingh.motionlib.core.MotionEffect
+import com.tejpratapsingh.motionlib.core.MotionView
+import com.tejpratapsingh.motionlib.core.animation.Easings
+import com.tejpratapsingh.motionlib.core.animation.Interpolators
+import com.tejpratapsingh.motionlib.core.animation.MotionInterpolator
+
+/**
+ * A [MotionEffect] that applies a vintage filter by combining Sepia and Vignette.
+ * Animates intensity from [fromIntensity] to [toIntensity].
+ */
+class VintageEffect(
+ override val startFrame: Int,
+ override val endFrame: Int,
+ val fromIntensity: Float = 0.0f,
+ val toIntensity: Float = 1.0f,
+) : MotionEffect {
+ override lateinit var motionView: MotionView
+
+ @Suppress("ktlint:standard:property-naming")
+ private val VIGNETTE_SHADER =
+ """
+ uniform shader content;
+ uniform float2 size;
+ uniform float intensity;
+
+ half4 main(float2 fragCoord) {
+ half4 color = content.eval(fragCoord);
+ float2 uv = fragCoord / size;
+ uv *= 1.0 - uv.yx;
+ float vig = uv.x*uv.y * 15.0;
+ vig = pow(vig, intensity);
+ return color * vig;
+ }
+ """.trimIndent()
+
+ 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.S) 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),
+ )
+
+ // Sepia matrix (standard)
+ val sepiaMatrix =
+ floatArrayOf(
+ 0.393f, 0.769f, 0.189f, 0f, 0f,
+ 0.349f, 0.686f, 0.168f, 0f, 0f,
+ 0.272f, 0.534f, 0.131f, 0f, 0f,
+ 0f, 0f, 0f, 1f, 0f,
+ )
+
+ // We can interpolate between identity and sepia
+ val identityMatrix =
+ floatArrayOf(
+ 1f, 0f, 0f, 0f, 0f,
+ 0f, 1f, 0f, 0f, 0f,
+ 0f, 0f, 1f, 0f, 0f,
+ 0f, 0f, 0f, 1f, 0f,
+ )
+
+ val resultMatrix = FloatArray(20)
+ for (i in 0 until 20) {
+ resultMatrix[i] = identityMatrix[i] + (sepiaMatrix[i] - identityMatrix[i]) * intensity
+ }
+
+ val sepiaEffect = RenderEffect.createColorFilterEffect(ColorMatrixColorFilter(resultMatrix))
+
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
+ val shader = RuntimeShader(VIGNETTE_SHADER)
+ shader.setFloatUniform("size", view.width.toFloat(), view.height.toFloat())
+ shader.setFloatUniform("intensity", intensity * 0.5f) // Scale vignette intensity
+
+ val vignetteEffect = RenderEffect.createRuntimeShaderEffect(shader, "content")
+ view.setRenderEffect(RenderEffect.createChainEffect(vignetteEffect, sepiaEffect))
+ } else {
+ view.setRenderEffect(sepiaEffect)
+ }
+
+ return motionView
+ }
+}
diff --git a/modules/sdui/src/main/java/com/tejpratapsingh/motion/sdui/infra/MotionSduiInitializer.kt b/modules/sdui/src/main/java/com/tejpratapsingh/motion/sdui/infra/MotionSduiInitializer.kt
index a68f288d..2f5ce556 100644
--- a/modules/sdui/src/main/java/com/tejpratapsingh/motion/sdui/infra/MotionSduiInitializer.kt
+++ b/modules/sdui/src/main/java/com/tejpratapsingh/motion/sdui/infra/MotionSduiInitializer.kt
@@ -34,6 +34,7 @@ import com.tejpratapsingh.motionlib.ui.effects.SlideLeftToRightEffect
import com.tejpratapsingh.motionlib.ui.effects.SlideRightToLeftEffect
import com.tejpratapsingh.motionlib.ui.effects.SlideTopToBottomEffect
import com.tejpratapsingh.motionlib.ui.effects.VibrateEffect
+import com.tejpratapsingh.motionlib.ui.effects.VintageEffect
import com.tejpratapsingh.motionlib.ui.effects.ZoomInEffect
import com.tejpratapsingh.motionlib.ui.effects.ZoomOutEffect
@@ -581,6 +582,25 @@ object MotionSduiInitializer {
json.addProperty("amplitude", effect.amplitude)
json.addProperty("frequency", effect.frequency)
}
+
+ // Register VintageEffect
+ MotionSdui.registerEffect(VintageEffect::class.java.simpleName) { json ->
+ val props = json.parseMotionEffectProps()
+ val fromIntensity = json.get("fromIntensity")?.asFloat ?: 0.0f
+ val toIntensity = json.get("toIntensity")?.asFloat ?: 1.0f
+ VintageEffect(
+ startFrame = props.startFrame,
+ endFrame = props.endFrame,
+ fromIntensity = fromIntensity,
+ toIntensity = toIntensity,
+ )
+ }
+ MotionSdui.registerEffectSerializer(VintageEffect::class.java) { effect, json ->
+ json.addProperty("type", effect.javaClass.simpleName)
+ json.addProperty("fromIntensity", effect.fromIntensity)
+ json.addProperty("toIntensity", effect.toIntensity)
+ }
+
// Register SlideEffect
MotionSdui.registerEffect(SlideEffect::class.java.simpleName) { json ->
val props = json.parseMotionEffectProps()
diff --git a/settings.gradle b/settings.gradle
index 3bc1dde0..0cadf0b4 100644
--- a/settings.gradle
+++ b/settings.gradle
@@ -34,3 +34,4 @@ include ':modules:metadata-extractor'
include ':modules:motion-store'
include ':modules:motion-video-player'
include ':modules:motion-video-editor'
+include ':modules:ml-kit-ext'