@imgly/editor-react-native 1.80.0-rc.1 → 1.81.0-rc.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (79) hide show
  1. package/android/build.gradle +1 -1
  2. package/android/src/main/assets/ly.img.memories.style/content.json +50 -0
  3. package/android/src/main/assets/ly.img.memories.style/thumbnails/bubblegum.png +0 -0
  4. package/android/src/main/assets/ly.img.memories.style/thumbnails/hologram.png +0 -0
  5. package/android/src/main/assets/ly.img.memories.style/thumbnails/noir.png +0 -0
  6. package/android/src/main/assets/ly.img.memories.style/videos/bubblegum.mp4 +0 -0
  7. package/android/src/main/assets/ly.img.memories.style/videos/hologram.mp4 +0 -0
  8. package/android/src/main/kotlin/ly/img/editor/configuration/memories/MemoriesApp.kt +77 -0
  9. package/android/src/main/kotlin/ly/img/editor/configuration/memories/MemoriesConfiguration.kt +38 -0
  10. package/android/src/main/kotlin/ly/img/editor/configuration/memories/MemoriesEditor.kt +79 -0
  11. package/android/src/main/kotlin/ly/img/editor/configuration/memories/MemoriesViewModel.kt +469 -0
  12. package/android/src/main/kotlin/ly/img/editor/configuration/memories/callback/OnCreate.kt +69 -0
  13. package/android/src/main/kotlin/ly/img/editor/configuration/memories/callback/OnExport.kt +46 -0
  14. package/android/src/main/kotlin/ly/img/editor/configuration/memories/component/BottomPanel.kt +151 -0
  15. package/android/src/main/kotlin/ly/img/editor/configuration/memories/component/Dock.kt +145 -0
  16. package/android/src/main/kotlin/ly/img/editor/configuration/memories/component/ExportOverlay.kt +229 -0
  17. package/android/src/main/kotlin/ly/img/editor/configuration/memories/component/NavigationBar.kt +80 -0
  18. package/android/src/main/kotlin/ly/img/editor/configuration/memories/component/Overlay.kt +138 -0
  19. package/android/src/main/kotlin/ly/img/editor/configuration/memories/extension/EditorScopeExt.kt +111 -0
  20. package/android/src/main/kotlin/ly/img/editor/configuration/memories/iconPack/AddPhotoAlternate.kt +79 -0
  21. package/android/src/main/kotlin/ly/img/editor/configuration/memories/iconPack/ArrowForward.kt +45 -0
  22. package/android/src/main/kotlin/ly/img/editor/configuration/memories/iconPack/Audiotrack.kt +43 -0
  23. package/android/src/main/kotlin/ly/img/editor/configuration/memories/iconPack/Check.kt +39 -0
  24. package/android/src/main/kotlin/ly/img/editor/configuration/memories/iconPack/CheckCircle.kt +44 -0
  25. package/android/src/main/kotlin/ly/img/editor/configuration/memories/iconPack/Close.kt +43 -0
  26. package/android/src/main/kotlin/ly/img/editor/configuration/memories/iconPack/Delete.kt +54 -0
  27. package/android/src/main/kotlin/ly/img/editor/configuration/memories/iconPack/DeleteOutline.kt +53 -0
  28. package/android/src/main/kotlin/ly/img/editor/configuration/memories/iconPack/Fullscreen.kt +74 -0
  29. package/android/src/main/kotlin/ly/img/editor/configuration/memories/iconPack/Image.kt +51 -0
  30. package/android/src/main/kotlin/ly/img/editor/configuration/memories/iconPack/Info.kt +48 -0
  31. package/android/src/main/kotlin/ly/img/editor/configuration/memories/iconPack/Pause.kt +46 -0
  32. package/android/src/main/kotlin/ly/img/editor/configuration/memories/iconPack/PlayArrow.kt +38 -0
  33. package/android/src/main/kotlin/ly/img/editor/configuration/memories/iconPack/RepeatOff.kt +80 -0
  34. package/android/src/main/kotlin/ly/img/editor/configuration/memories/iconPack/RepeatOn.kt +59 -0
  35. package/android/src/main/kotlin/ly/img/editor/configuration/memories/iconPack/SkipPrevious.kt +46 -0
  36. package/android/src/main/kotlin/ly/img/editor/configuration/memories/iconPack/Sort.kt +55 -0
  37. package/android/src/main/kotlin/ly/img/editor/configuration/memories/iconPack/Style.kt +59 -0
  38. package/android/src/main/kotlin/ly/img/editor/configuration/memories/iconPack/Title.kt +43 -0
  39. package/android/src/main/kotlin/ly/img/editor/configuration/memories/iconPack/TouchTriple.kt +134 -0
  40. package/android/src/main/kotlin/ly/img/editor/configuration/memories/iconPack/VolumeDown.kt +48 -0
  41. package/android/src/main/kotlin/ly/img/editor/configuration/memories/iconPack/VolumeUp.kt +60 -0
  42. package/android/src/main/kotlin/ly/img/editor/configuration/memories/model/ExportStatus.kt +20 -0
  43. package/android/src/main/kotlin/ly/img/editor/configuration/memories/model/ImageItem.kt +8 -0
  44. package/android/src/main/kotlin/ly/img/editor/configuration/memories/model/TimelineImage.kt +18 -0
  45. package/android/src/main/kotlin/ly/img/editor/configuration/memories/scene/Events.kt +59 -0
  46. package/android/src/main/kotlin/ly/img/editor/configuration/memories/scene/Playback.kt +29 -0
  47. package/android/src/main/kotlin/ly/img/editor/configuration/memories/scene/SceneSetup.kt +79 -0
  48. package/android/src/main/kotlin/ly/img/editor/configuration/memories/scene/Timeline.kt +27 -0
  49. package/android/src/main/kotlin/ly/img/editor/configuration/memories/scene/Title.kt +162 -0
  50. package/android/src/main/kotlin/ly/img/editor/configuration/memories/scene/TrackEditor.kt +740 -0
  51. package/android/src/main/kotlin/ly/img/editor/configuration/memories/screen/ImageSelectionScreen.kt +387 -0
  52. package/android/src/main/kotlin/ly/img/editor/configuration/memories/screen/LoadingScreen.kt +258 -0
  53. package/android/src/main/kotlin/ly/img/editor/configuration/memories/sheet/FilterStylesSheet.kt +167 -0
  54. package/android/src/main/kotlin/ly/img/editor/configuration/memories/sheet/FilterStylesSheetType.kt +21 -0
  55. package/android/src/main/kotlin/ly/img/editor/configuration/memories/sheet/VolumeSheet.kt +104 -0
  56. package/android/src/main/kotlin/ly/img/editor/configuration/memories/style/StyleApplier.kt +220 -0
  57. package/android/src/main/kotlin/ly/img/editor/configuration/memories/style/StyleAssetSource.kt +43 -0
  58. package/android/src/main/kotlin/ly/img/editor/configuration/memories/style/VideoStyle.kt +118 -0
  59. package/android/src/main/kotlin/ly/img/editor/configuration/memories/util/Animations.kt +58 -0
  60. package/android/src/main/kotlin/ly/img/editor/configuration/memories/util/Constants.kt +20 -0
  61. package/android/src/main/kotlin/ly/img/editor/configuration/memories/util/ImageDate.kt +81 -0
  62. package/android/src/main/kotlin/ly/img/editor/configuration/memories/util/Utils.kt +27 -0
  63. package/android/src/main/kotlin/ly/img/editor/configuration/memories/widget/CenteredAddImageButton.kt +82 -0
  64. package/android/src/main/kotlin/ly/img/editor/configuration/memories/widget/DraggableImageItem.kt +145 -0
  65. package/android/src/main/kotlin/ly/img/editor/configuration/memories/widget/FullscreenVideoControls.kt +136 -0
  66. package/android/src/main/kotlin/ly/img/editor/configuration/memories/widget/ImageGrid.kt +122 -0
  67. package/android/src/main/kotlin/ly/img/editor/configuration/memories/widget/ImageGridOverlay.kt +149 -0
  68. package/android/src/main/kotlin/ly/img/editor/configuration/memories/widget/ImageTimelineStrip.kt +323 -0
  69. package/android/src/main/kotlin/ly/img/editor/configuration/memories/widget/ImageToolbar.kt +401 -0
  70. package/android/src/main/kotlin/ly/img/editor/configuration/memories/widget/ReorderableGridState.kt +183 -0
  71. package/android/src/main/kotlin/ly/img/editor/configuration/memories/widget/VideoControlPanel.kt +180 -0
  72. package/android/src/main/kotlin/ly/img/editor/configuration/memories/widget/VideoThumbnail.kt +110 -0
  73. package/android/src/main/kotlin/ly/img/editor/configuration/video/callback/OnCreate.kt +0 -7
  74. package/android/src/main/kotlin/ly/img/editor/configuration/video/component/InspectorBar.kt +2 -0
  75. package/ios/StarterKits/Video/callbacks/OnCreate+Video.swift +1 -1
  76. package/ios/StarterKits/Video/components/Dock+Video.swift +1 -0
  77. package/ios/StarterKits/Video/components/InspectorBar+Video.swift +4 -0
  78. package/package.json +1 -1
  79. package/src/NativeIMGLYEditor.ts +1 -1
@@ -0,0 +1,167 @@
1
+ package ly.img.editor.configuration.memories.sheet
2
+
3
+ import androidx.compose.foundation.background
4
+ import androidx.compose.foundation.border
5
+ import androidx.compose.foundation.clickable
6
+ import androidx.compose.foundation.layout.Arrangement
7
+ import androidx.compose.foundation.layout.Box
8
+ import androidx.compose.foundation.layout.Column
9
+ import androidx.compose.foundation.layout.PaddingValues
10
+ import androidx.compose.foundation.layout.Spacer
11
+ import androidx.compose.foundation.layout.fillMaxSize
12
+ import androidx.compose.foundation.layout.fillMaxWidth
13
+ import androidx.compose.foundation.layout.height
14
+ import androidx.compose.foundation.layout.padding
15
+ import androidx.compose.foundation.layout.size
16
+ import androidx.compose.foundation.lazy.LazyRow
17
+ import androidx.compose.foundation.lazy.items
18
+ import androidx.compose.foundation.shape.RoundedCornerShape
19
+ import androidx.compose.material3.Icon
20
+ import androidx.compose.material3.MaterialTheme
21
+ import androidx.compose.material3.Text
22
+ import androidx.compose.runtime.Composable
23
+ import androidx.compose.runtime.getValue
24
+ import androidx.compose.runtime.mutableStateOf
25
+ import androidx.compose.runtime.remember
26
+ import androidx.compose.runtime.setValue
27
+ import androidx.compose.ui.Alignment
28
+ import androidx.compose.ui.Modifier
29
+ import androidx.compose.ui.draw.clip
30
+ import androidx.compose.ui.graphics.Color
31
+ import androidx.compose.ui.layout.ContentScale
32
+ import androidx.compose.ui.text.font.FontWeight
33
+ import androidx.compose.ui.text.style.TextAlign
34
+ import androidx.compose.ui.unit.dp
35
+ import coil.compose.AsyncImage
36
+ import ly.img.editor.configuration.memories.iconPack.Image
37
+ import ly.img.editor.configuration.memories.style.VideoStyle
38
+ import ly.img.editor.configuration.memories.style.VideoStyles
39
+ import ly.img.editor.core.iconpack.IconPack
40
+
41
+ @Composable
42
+ fun FilterStylesSheet(
43
+ onFilterSelected: (VideoStyle) -> Unit,
44
+ thumbnails: Map<String, String>,
45
+ initialSelectedId: String = VideoStyles.DEFAULT.id,
46
+ modifier: Modifier = Modifier,
47
+ ) {
48
+ // Seed from the currently-applied style so reopening the sheet keeps it highlighted.
49
+ var selectedStyleId by remember(initialSelectedId) { mutableStateOf(initialSelectedId) }
50
+
51
+ Column(
52
+ modifier = modifier
53
+ .fillMaxWidth()
54
+ .padding(16.dp),
55
+ horizontalAlignment = Alignment.CenterHorizontally,
56
+ ) {
57
+ // Title
58
+ Text(
59
+ text = "Styles",
60
+ style = MaterialTheme.typography.headlineSmall,
61
+ fontWeight = FontWeight.Medium,
62
+ color = MaterialTheme.colorScheme.onSurface,
63
+ modifier = Modifier.padding(bottom = 24.dp),
64
+ )
65
+
66
+ // Filter styles row — centered when it fits, horizontally scrollable on smaller screens.
67
+ LazyRow(
68
+ horizontalArrangement = Arrangement.spacedBy(12.dp, Alignment.CenterHorizontally),
69
+ contentPadding = PaddingValues(horizontal = 16.dp),
70
+ modifier = Modifier.fillMaxWidth(),
71
+ ) {
72
+ items(VideoStyles.ALL) { style ->
73
+ FilterStyleItem(
74
+ style = style,
75
+ thumbnailUri = thumbnails[style.id],
76
+ isSelected = style.id == selectedStyleId,
77
+ onClick = {
78
+ selectedStyleId = style.id
79
+ onFilterSelected(style)
80
+ },
81
+ )
82
+ }
83
+ }
84
+
85
+ Spacer(modifier = Modifier.height(24.dp))
86
+ }
87
+ }
88
+
89
+ @Composable
90
+ private fun FilterStyleItem(
91
+ style: VideoStyle,
92
+ thumbnailUri: String?,
93
+ isSelected: Boolean,
94
+ onClick: () -> Unit,
95
+ modifier: Modifier = Modifier,
96
+ ) {
97
+ Column(
98
+ horizontalAlignment = Alignment.CenterHorizontally,
99
+ modifier = modifier.clickable { onClick() },
100
+ ) {
101
+ // Filter preview tile
102
+ Box(
103
+ modifier = Modifier
104
+ .size(72.dp)
105
+ .clip(RoundedCornerShape(16.dp))
106
+ .background(
107
+ if (isSelected) {
108
+ MaterialTheme.colorScheme.primary.copy(alpha = 0.1f)
109
+ } else {
110
+ Color.Transparent
111
+ },
112
+ )
113
+ .border(
114
+ width = if (isSelected) 3.dp else 0.dp,
115
+ color = if (isSelected) {
116
+ MaterialTheme.colorScheme.primary
117
+ } else {
118
+ Color.Transparent
119
+ },
120
+ shape = RoundedCornerShape(16.dp),
121
+ )
122
+ .padding(if (isSelected) 4.dp else 0.dp),
123
+ contentAlignment = Alignment.Center,
124
+ ) {
125
+ Box(
126
+ modifier = Modifier
127
+ .fillMaxSize()
128
+ .clip(RoundedCornerShape(12.dp))
129
+ .background(Color(style.previewBackground)),
130
+ contentAlignment = Alignment.Center,
131
+ ) {
132
+ if (thumbnailUri != null) {
133
+ // Thumbnail supplied by the custom style asset source, loaded with Coil.
134
+ AsyncImage(
135
+ model = thumbnailUri,
136
+ contentDescription = style.displayName,
137
+ contentScale = ContentScale.Crop,
138
+ modifier = Modifier.fillMaxSize(),
139
+ )
140
+ } else {
141
+ // Default style: an icon on the tile color (no filter / original look).
142
+ Icon(
143
+ imageVector = IconPack.Image,
144
+ contentDescription = style.displayName,
145
+ tint = MaterialTheme.colorScheme.onSurfaceVariant,
146
+ modifier = Modifier.size(32.dp),
147
+ )
148
+ }
149
+ }
150
+ }
151
+
152
+ Spacer(modifier = Modifier.height(8.dp))
153
+
154
+ // Style name
155
+ Text(
156
+ text = style.displayName,
157
+ style = MaterialTheme.typography.bodyMedium,
158
+ fontWeight = if (isSelected) FontWeight.SemiBold else FontWeight.Normal,
159
+ color = if (isSelected) {
160
+ MaterialTheme.colorScheme.primary
161
+ } else {
162
+ MaterialTheme.colorScheme.onSurfaceVariant
163
+ },
164
+ textAlign = TextAlign.Center,
165
+ )
166
+ }
167
+ }
@@ -0,0 +1,21 @@
1
+ package ly.img.editor.configuration.memories.sheet
2
+
3
+ import ly.img.editor.configuration.memories.style.VideoStyle
4
+ import ly.img.editor.configuration.memories.style.VideoStyles
5
+ import ly.img.editor.core.sheet.SheetStyle
6
+ import ly.img.editor.core.sheet.SheetType
7
+
8
+ fun createFilterStylesSheetType(
9
+ thumbnails: Map<String, String>,
10
+ initialSelectedId: String = VideoStyles.DEFAULT.id,
11
+ onFilterSelected: (VideoStyle) -> Unit,
12
+ ): SheetType.Custom = SheetType.Custom(
13
+ style = SheetStyle(),
14
+ content = {
15
+ FilterStylesSheet(
16
+ onFilterSelected = onFilterSelected,
17
+ thumbnails = thumbnails,
18
+ initialSelectedId = initialSelectedId,
19
+ )
20
+ },
21
+ )
@@ -0,0 +1,104 @@
1
+ package ly.img.editor.configuration.memories.sheet
2
+
3
+ import androidx.compose.foundation.layout.Arrangement
4
+ import androidx.compose.foundation.layout.Column
5
+ import androidx.compose.foundation.layout.Row
6
+ import androidx.compose.foundation.layout.fillMaxWidth
7
+ import androidx.compose.foundation.layout.padding
8
+ import androidx.compose.foundation.layout.size
9
+ import androidx.compose.material3.ExperimentalMaterial3Api
10
+ import androidx.compose.material3.Icon
11
+ import androidx.compose.material3.MaterialTheme
12
+ import androidx.compose.material3.Slider
13
+ import androidx.compose.material3.Text
14
+ import androidx.compose.runtime.Composable
15
+ import androidx.compose.runtime.getValue
16
+ import androidx.compose.runtime.mutableFloatStateOf
17
+ import androidx.compose.runtime.remember
18
+ import androidx.compose.runtime.setValue
19
+ import androidx.compose.ui.Alignment
20
+ import androidx.compose.ui.Modifier
21
+ import androidx.compose.ui.text.font.FontWeight
22
+ import androidx.compose.ui.unit.dp
23
+ import ly.img.editor.configuration.memories.iconPack.VolumeDown
24
+ import ly.img.editor.configuration.memories.iconPack.VolumeUp
25
+ import ly.img.editor.core.iconpack.IconPack
26
+ import ly.img.editor.core.sheet.SheetStyle
27
+ import ly.img.editor.core.sheet.SheetType
28
+
29
+ @OptIn(ExperimentalMaterial3Api::class)
30
+ @Composable
31
+ fun VolumeSheet(
32
+ volume: Float,
33
+ onVolumeChange: (Float) -> Unit,
34
+ modifier: Modifier = Modifier,
35
+ ) {
36
+ Column(
37
+ modifier = modifier
38
+ .fillMaxWidth()
39
+ .padding(24.dp),
40
+ verticalArrangement = Arrangement.spacedBy(16.dp),
41
+ ) {
42
+ // Title
43
+ Text(
44
+ text = "Volume",
45
+ style = MaterialTheme.typography.headlineSmall,
46
+ fontWeight = FontWeight.Medium,
47
+ color = MaterialTheme.colorScheme.onSurface,
48
+ )
49
+
50
+ // Volume control
51
+ Row(
52
+ modifier = Modifier.fillMaxWidth(),
53
+ verticalAlignment = Alignment.CenterVertically,
54
+ horizontalArrangement = Arrangement.spacedBy(12.dp),
55
+ ) {
56
+ Icon(
57
+ imageVector = IconPack.VolumeDown,
58
+ contentDescription = "Volume Down",
59
+ tint = MaterialTheme.colorScheme.onSurfaceVariant,
60
+ modifier = Modifier.size(24.dp),
61
+ )
62
+
63
+ Slider(
64
+ value = volume,
65
+ onValueChange = onVolumeChange,
66
+ valueRange = 0f..1f,
67
+ modifier = Modifier.weight(1f),
68
+ )
69
+
70
+ Icon(
71
+ imageVector = IconPack.VolumeUp,
72
+ contentDescription = "Volume Up",
73
+ tint = MaterialTheme.colorScheme.onSurfaceVariant,
74
+ modifier = Modifier.size(24.dp),
75
+ )
76
+ }
77
+
78
+ // Volume percentage
79
+ Text(
80
+ text = "${(volume * 100).toInt()}%",
81
+ style = MaterialTheme.typography.bodyMedium,
82
+ color = MaterialTheme.colorScheme.onSurfaceVariant,
83
+ modifier = Modifier.align(Alignment.CenterHorizontally),
84
+ )
85
+ }
86
+ }
87
+
88
+ fun createVolumeSheetType(
89
+ initialVolume: Float,
90
+ onVolumeChange: (Float) -> Unit,
91
+ ): SheetType.Custom = SheetType.Custom(
92
+ style = SheetStyle(),
93
+ content = {
94
+ var volume by remember { mutableFloatStateOf(initialVolume) }
95
+
96
+ VolumeSheet(
97
+ volume = volume,
98
+ onVolumeChange = { newVolume ->
99
+ volume = newVolume
100
+ onVolumeChange(newVolume)
101
+ },
102
+ )
103
+ },
104
+ )
@@ -0,0 +1,220 @@
1
+ package ly.img.editor.configuration.memories.style
2
+
3
+ import android.util.Log
4
+ import ly.img.editor.configuration.memories.scene.backgroundBlock
5
+ import ly.img.editor.configuration.memories.scene.findTracks
6
+ import ly.img.editor.configuration.memories.scene.imageBlocksInOrder
7
+ import ly.img.editor.configuration.memories.scene.matteBlock
8
+ import ly.img.engine.Color
9
+ import ly.img.engine.EffectType
10
+ import ly.img.engine.Engine
11
+ import ly.img.engine.FillType
12
+ import ly.img.engine.FindAssetsQuery
13
+ import ly.img.engine.Font
14
+ import ly.img.engine.Typeface
15
+ import kotlin.math.abs
16
+
17
+ private const val TAG = "MemoriesStyles"
18
+
19
+ /**
20
+ * Generic, data-driven application of a [VideoStyle] to the engine. These functions read
21
+ * everything they need from the style, so they never have to change when styles are added,
22
+ * removed, or tuned in [VideoStyles].
23
+ */
24
+
25
+ /**
26
+ * Apply [style] to the whole slideshow: filter + scale every media clip, paint the backdrop and
27
+ * matte, restyle the title, and checkpoint it as one undo step. The tracks are the source of truth,
28
+ * so this reads the clips back from them rather than holding block ids.
29
+ */
30
+ suspend fun applyStyleToSlideshow(
31
+ engine: Engine,
32
+ page: Int,
33
+ style: VideoStyle,
34
+ ) {
35
+ val tracks = findTracks(engine, page) ?: return
36
+ val duration = engine.block.getDuration(page)
37
+
38
+ imageBlocksInOrder(engine, tracks).forEach { block ->
39
+ if (engine.block.supportsEffects(block)) {
40
+ engine.block.getEffects(block).indices.reversed().forEach { engine.block.removeEffect(block, it) }
41
+ applyStyleEffects(engine, block, style)
42
+ }
43
+ applyMediaScale(engine, block, page, style)
44
+ }
45
+
46
+ backgroundBlock(engine, page)?.let { applyStyleBackground(engine, it, style, duration) }
47
+ matteBlock(engine, page)?.let {
48
+ applyMediaScale(engine, it, page, style)
49
+ engine.block.setDuration(it, duration)
50
+ }
51
+
52
+ tracks.textTrack?.let { textTrack ->
53
+ engine.block.getChildren(textTrack)
54
+ .filter { runCatching { engine.block.getMetadata(it, "blockType") }.getOrNull() == "title" }
55
+ .forEach { updateTitleTypeface(engine, it, style) }
56
+ }
57
+
58
+ engine.editor.addUndoStep()
59
+ }
60
+
61
+ /** Set the title's font + color for [style] (Noir reads black on its white backdrop; others white). */
62
+ private suspend fun updateTitleTypeface(
63
+ engine: Engine,
64
+ textBlock: Int,
65
+ style: VideoStyle,
66
+ ) {
67
+ try {
68
+ engine.block.setTextColor(textBlock, Color.fromHex(style.titleTextColorHex))
69
+ val assets = engine.asset.findAssets(
70
+ sourceId = "ly.img.typeface",
71
+ query = FindAssetsQuery(query = style.typeface, page = 0, perPage = 100),
72
+ )
73
+ val typeface = assets.assets.firstOrNull()?.payload?.typeface ?: return
74
+ val font = resolveFont(typeface, style) ?: return
75
+ engine.block.setFont(textBlock, font.uri, typeface)
76
+ } catch (e: Exception) {
77
+ Log.w(TAG, "Could not restyle title for '${style.id}'", e)
78
+ }
79
+ }
80
+
81
+ private const val VIDEO_FILL_URI = "fill/video/fileURI"
82
+
83
+ /** Applies a style's image-filter adjustments to a single media [block]. */
84
+ fun applyStyleEffects(
85
+ engine: Engine,
86
+ block: Int,
87
+ style: VideoStyle,
88
+ ) {
89
+ if (style.adjustments.isNotEmpty()) {
90
+ val adjustments = engine.block.createEffect(EffectType.Adjustments)
91
+ engine.block.appendEffect(block, adjustments)
92
+ style.adjustments.forEach { (property, value) ->
93
+ engine.block.setFloat(adjustments, property, value)
94
+ }
95
+ }
96
+ }
97
+
98
+ /**
99
+ * Size + center a media [block] within the [page] for the style's [VideoStyle.mediaScale]:
100
+ * full-bleed at 1.0, or scaled down and centered so the [VideoStyle.background] shows around it.
101
+ *
102
+ * This sets the block's **size and position** directly (rather than an `engine.block.scale` transform)
103
+ * so it cooperates with the slideshow's Ken Burns pan/zoom. The Ken Burns code pre-scales zoom-out
104
+ * clips (via `scale`, which mutates width/height) so the pan never reveals their edges; resetting the
105
+ * size here would wipe that out, so after re-establishing the clean centered base frame we re-apply
106
+ * the very same compensation on top. The base is reset first, so re-applying a style is idempotent
107
+ * and never compounds the scale. Blocks without an in-animation (videos, the matte) just get sized.
108
+ */
109
+ fun applyMediaScale(
110
+ engine: Engine,
111
+ block: Int,
112
+ page: Int,
113
+ style: VideoStyle,
114
+ ) {
115
+ val pageWidth = engine.block.getWidth(page)
116
+ val pageHeight = engine.block.getHeight(page)
117
+ val scale = style.mediaScale.coerceIn(0.1f, 1f)
118
+
119
+ // Clean, centered base frame at the target size.
120
+ engine.block.setWidth(block, pageWidth * scale)
121
+ engine.block.setHeight(block, pageHeight * scale)
122
+ engine.block.setPositionX(block, pageWidth * (1f - scale) / 2f)
123
+ engine.block.setPositionY(block, pageHeight * (1f - scale) / 2f)
124
+
125
+ // Re-coordinate Ken Burns: a zoom-out clip is pre-scaled around its center so its pan stays
126
+ // framed within the new size instead of revealing the backdrop at its edges.
127
+ val inAnimation = runCatching { engine.block.getInAnimation(block) }.getOrDefault(-1)
128
+ if (inAnimation != -1) {
129
+ val zoomIntensity = runCatching {
130
+ engine.block.getFloat(inAnimation, "animation/ken_burns/zoomIntensity")
131
+ }.getOrDefault(0f)
132
+ if (zoomIntensity < 0f) {
133
+ engine.block.scale(block, 1f + abs(zoomIntensity), anchorX = 0.5f, anchorY = 0.5f)
134
+ }
135
+ }
136
+ }
137
+
138
+ /**
139
+ * Paint the persistent [backgroundBlock] for the style's [VideoStyle.background]: hidden for
140
+ * [StyleBackground.None], a flat color for [StyleBackground.Solid], or a muted, looping video
141
+ * spanning [pageDuration] for [StyleBackground.Video].
142
+ */
143
+ suspend fun applyStyleBackground(
144
+ engine: Engine,
145
+ backgroundBlock: Int,
146
+ style: VideoStyle,
147
+ pageDuration: Double,
148
+ ) {
149
+ when (val background = style.background) {
150
+ StyleBackground.None -> {
151
+ engine.block.setVisible(backgroundBlock, false)
152
+ }
153
+
154
+ is StyleBackground.Solid -> {
155
+ val fill = ensureFill(engine, backgroundBlock, FillType.Color)
156
+ engine.block.setColor(fill, "fill/color/value", Color.fromHex(background.colorHex))
157
+ engine.block.setVisible(backgroundBlock, true)
158
+ }
159
+
160
+ is StyleBackground.Video -> {
161
+ // Resolve the backdrop clip from the custom local asset source rather than a hard-coded
162
+ // path; if the asset is missing, hide the backdrop instead of setting an empty fill.
163
+ val uri = engine.styleBackgroundVideoUri(background.assetId) ?: run {
164
+ engine.block.setVisible(backgroundBlock, false)
165
+ return
166
+ }
167
+ val fill = ensureFill(engine, backgroundBlock, FillType.Video)
168
+ // Only (re)set the source + reload when it actually changed; re-applying a style (e.g.
169
+ // after a track rebuild) shouldn't pay the decode cost again for the same backdrop.
170
+ val current = runCatching { engine.block.getString(fill, VIDEO_FILL_URI) }.getOrNull()
171
+ if (current != uri) {
172
+ engine.block.setString(fill, VIDEO_FILL_URI, uri)
173
+ engine.block.forceLoadAVResource(fill)
174
+ }
175
+ val sourceDuration = runCatching { engine.block.getAVResourceTotalDuration(fill) }.getOrDefault(0.0)
176
+ engine.block.setLooping(fill, true)
177
+ engine.block.setMuted(fill, true)
178
+ engine.block.setTrimOffset(fill, 0.0)
179
+ if (sourceDuration > 0.0) engine.block.setTrimLength(fill, sourceDuration)
180
+ // Keep the backdrop the full length of the slideshow so the loop always covers it.
181
+ engine.block.setDuration(backgroundBlock, pageDuration)
182
+ engine.block.setVisible(backgroundBlock, true)
183
+ }
184
+ }
185
+ }
186
+
187
+ /**
188
+ * Return the [block]'s fill, reusing it when it is already the requested [type], otherwise creating
189
+ * a fresh fill of that type (and destroying the previous one). Lets us flip the persistent
190
+ * background block between a color and a video without leaking the discarded fill.
191
+ */
192
+ private fun ensureFill(
193
+ engine: Engine,
194
+ block: Int,
195
+ type: FillType,
196
+ ): Int {
197
+ val existing = runCatching { engine.block.getFill(block) }.getOrDefault(-1)
198
+ val existingValid = existing != -1 && runCatching { engine.block.isValid(existing) }.getOrDefault(false)
199
+ if (existingValid && runCatching { engine.block.getType(existing) }.getOrNull() == type.key) {
200
+ return existing
201
+ }
202
+ val fill = engine.block.createFill(type)
203
+ engine.block.setFill(block, fill)
204
+ if (existingValid) runCatching { engine.block.destroy(existing) }
205
+ return fill
206
+ }
207
+
208
+ /**
209
+ * Picks the best font for [style] from [typeface]: the first available sub-family in the
210
+ * style's preference order, falling back to the typeface's first font.
211
+ */
212
+ fun resolveFont(
213
+ typeface: Typeface,
214
+ style: VideoStyle,
215
+ ): Font? {
216
+ for (weight in style.fontWeights) {
217
+ typeface.fonts.firstOrNull { it.subFamily == weight }?.let { return it }
218
+ }
219
+ return typeface.fonts.firstOrNull()
220
+ }
@@ -0,0 +1,43 @@
1
+ package ly.img.editor.configuration.memories.style
2
+
3
+ import androidx.core.net.toUri
4
+ import ly.img.engine.Engine
5
+ import ly.img.engine.FindAssetsQuery
6
+
7
+ /**
8
+ * The custom **local** asset source that supplies the bundled style assets — the looping backdrop
9
+ * videos and every style's picker thumbnail. The files stay local (in `src/main/assets`) and are
10
+ * described by `assets/ly.img.memories.style/content.json`; the engine loads them through
11
+ * [addLocalSourceFromJSON][ly.img.engine.AssetApi.addLocalSourceFromJSON], exactly like the default
12
+ * IMG.LY sources. Keeping them behind an asset source means the style catalog references assets by
13
+ * id ([VideoStyle]) instead of hard-coding `file:///android_asset/...` paths across the kit.
14
+ */
15
+ const val STYLE_SOURCE_ID = "ly.img.memories.style"
16
+
17
+ /** The bundled `content.json` describing [STYLE_SOURCE_ID], resolved as an `android_asset` URI. */
18
+ private const val STYLE_SOURCE_CONTENT_URI = "file:///android_asset/$STYLE_SOURCE_ID/content.json"
19
+
20
+ /**
21
+ * Register [STYLE_SOURCE_ID] from its bundled `content.json` (idempotent). Call once while loading
22
+ * the other asset sources, before any style is applied or the Styles picker is shown.
23
+ */
24
+ suspend fun Engine.registerStyleAssetSource() {
25
+ if (STYLE_SOURCE_ID !in asset.findAllSources()) {
26
+ asset.addLocalSourceFromJSON(contentUri = STYLE_SOURCE_CONTENT_URI.toUri())
27
+ }
28
+ }
29
+
30
+ /**
31
+ * The picker thumbnail URI for each style, keyed by style id, read from [STYLE_SOURCE_ID]. Styles
32
+ * without a bundled asset (e.g. the unstyled default) are simply absent from the map.
33
+ */
34
+ suspend fun Engine.loadStyleThumbnails(): Map<String, String> = asset.findAssets(
35
+ sourceId = STYLE_SOURCE_ID,
36
+ query = FindAssetsQuery(page = 0, perPage = 100),
37
+ ).assets.mapNotNull { asset ->
38
+ asset.meta?.get("thumbUri")?.let { asset.id to it }
39
+ }.toMap()
40
+
41
+ /** The backdrop video URI for a style, read from its [STYLE_SOURCE_ID] asset (null if absent). */
42
+ suspend fun Engine.styleBackgroundVideoUri(assetId: String): String? =
43
+ asset.fetchAsset(sourceId = STYLE_SOURCE_ID, assetId = assetId)?.meta?.get("uri")
@@ -0,0 +1,118 @@
1
+ package ly.img.editor.configuration.memories.style
2
+
3
+ /**
4
+ * Catalog of the video styles offered in the Styles sheet.
5
+ *
6
+ * This is the single source of truth for a style: its name, typeface, preferred font
7
+ * weights, the image-filter adjustments, the scene background and media scale, and the title
8
+ * text color all live here. To add, remove, or tune a style, edit [VideoStyles.ALL] below —
9
+ * nothing else needs to change. The generic application logic lives in [StyleApplier].
10
+ *
11
+ * The bundled files a style needs — its looping backdrop [StyleBackground.Video] and its picker
12
+ * thumbnail — are supplied by the [STYLE_SOURCE_ID] custom local asset source (see
13
+ * [StyleAssetSource]). A style references them by [id], never by a hard-coded `file://` path.
14
+ */
15
+ data class VideoStyle(
16
+ /** Stable lowercase identifier used as the key everywhere (e.g. "noir"). */
17
+ val id: String,
18
+ /** Human-facing name shown in the sheet (e.g. "Noir"). */
19
+ val displayName: String,
20
+ /** Typeface name as it appears in the "ly.img.typeface" asset source. */
21
+ val typeface: String,
22
+ /** Preferred font sub-families, most-preferred first; falls back to the first available. */
23
+ val fontWeights: List<String>,
24
+ /** Adjustment effect properties, e.g. ("effect/adjustments/saturation", -1.0f). Empty = no adjustments. */
25
+ val adjustments: List<Pair<String, Float>> = emptyList(),
26
+ /** How much of the page the media fills. 1.0 = full-bleed; < 1.0 reveals the [background]. */
27
+ val mediaScale: Float = 1f,
28
+ /** The backdrop shown behind the (scaled-down) media. */
29
+ val background: StyleBackground = StyleBackground.None,
30
+ /** Title text color for this style (hex). White reads on the dark/video backdrops; Noir uses black. */
31
+ val titleTextColorHex: String = "#FFFFFF",
32
+ /** Opaque ARGB color for the picker tile (shown behind the icon / as a placeholder). */
33
+ val previewBackground: Long,
34
+ )
35
+
36
+ /** The backdrop a style paints behind the media once the media is scaled below full-bleed. */
37
+ sealed interface StyleBackground {
38
+ /** No backdrop: the media fills the whole page (used by the unstyled default). */
39
+ object None : StyleBackground
40
+
41
+ /** A flat color fill behind the media (e.g. white for Noir). [colorHex] is an "#RRGGBB" string. */
42
+ data class Solid(
43
+ val colorHex: String,
44
+ ) : StyleBackground
45
+
46
+ /**
47
+ * A looping video behind the media, supplied by the [STYLE_SOURCE_ID] asset source.
48
+ * [assetId] is the id of the backdrop asset in that source (e.g. "hologram").
49
+ */
50
+ data class Video(
51
+ val assetId: String,
52
+ ) : StyleBackground
53
+ }
54
+
55
+ object VideoStyles {
56
+ /** Neutral, unstyled: clean modern sans, full-bleed media, no filter or backdrop. */
57
+ val DEFAULT = VideoStyle(
58
+ id = "default",
59
+ displayName = "Default",
60
+ typeface = "Montserrat",
61
+ fontWeights = listOf("SemiBold", "Medium", "Regular"),
62
+ previewBackground = 0xFFEFEBE9,
63
+ )
64
+
65
+ /** Professional black & white on a clean white backdrop, with black title type. */
66
+ val NOIR = VideoStyle(
67
+ id = "noir",
68
+ displayName = "Noir",
69
+ typeface = "Playfair Display",
70
+ fontWeights = listOf("Bold", "SemiBold"),
71
+ adjustments = listOf(
72
+ "effect/adjustments/saturation" to -1.0f,
73
+ "effect/adjustments/contrast" to 0.15f,
74
+ "effect/adjustments/clarity" to 0.1f,
75
+ ),
76
+ mediaScale = 0.8f,
77
+ background = StyleBackground.Solid(colorHex = "#FFFFFF"),
78
+ titleTextColorHex = "#000000",
79
+ previewBackground = 0xFF222222,
80
+ )
81
+
82
+ /** Futuristic cool cast over a looping hologram backdrop. A blue temperature shift cools the media. */
83
+ val HOLOGRAM = VideoStyle(
84
+ id = "hologram",
85
+ displayName = "Hologram",
86
+ typeface = "VT323",
87
+ fontWeights = listOf("Regular"),
88
+ adjustments = listOf(
89
+ "effect/adjustments/temperature" to -0.4f,
90
+ "effect/adjustments/contrast" to 0.1f,
91
+ ),
92
+ mediaScale = 0.8f,
93
+ background = StyleBackground.Video(assetId = "hologram"),
94
+ previewBackground = 0xFFE1F5FE,
95
+ )
96
+
97
+ /** Playful, poppy filter over a looping bubblegum backdrop: punchy saturation, bright tones. */
98
+ val BUBBLEGUM = VideoStyle(
99
+ id = "bubblegum",
100
+ displayName = "Bubblegum",
101
+ typeface = "Lobster Two",
102
+ fontWeights = listOf("Bold", "Regular"),
103
+ adjustments = listOf(
104
+ "effect/adjustments/saturation" to 0.5f,
105
+ "effect/adjustments/brightness" to 0.05f,
106
+ "effect/adjustments/contrast" to 0.1f,
107
+ ),
108
+ mediaScale = 0.8f,
109
+ background = StyleBackground.Video(assetId = "bubblegum"),
110
+ previewBackground = 0xFFFCE4EC,
111
+ )
112
+
113
+ /** All styles, in the order they appear in the Styles sheet. */
114
+ val ALL = listOf(DEFAULT, NOIR, HOLOGRAM, BUBBLEGUM)
115
+
116
+ /** Resolves a style by its [id] (case-insensitive), falling back to [DEFAULT]. */
117
+ fun byId(id: String): VideoStyle = ALL.firstOrNull { it.id.equals(id, ignoreCase = true) } ?: DEFAULT
118
+ }