react-native-photo-video-editor 0.1.0 → 0.2.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.
- package/CHANGELOG.md +20 -0
- package/PhotoVideoEditor.podspec +1 -0
- package/README.md +10 -6
- package/THIRD_PARTY_NOTICES.md +20 -0
- package/android/build.gradle +1 -1
- package/android/src/main/AndroidManifest.xml +1 -1
- package/android/src/main/assets/Stickers/AUTUMN_1.png +0 -0
- package/android/src/main/assets/Stickers/AUTUMN_10.png +0 -0
- package/android/src/main/assets/Stickers/AUTUMN_11.png +0 -0
- package/android/src/main/assets/Stickers/AUTUMN_12.png +0 -0
- package/android/src/main/assets/Stickers/AUTUMN_13.png +0 -0
- package/android/src/main/assets/Stickers/AUTUMN_14.png +0 -0
- package/android/src/main/assets/Stickers/AUTUMN_15.png +0 -0
- package/android/src/main/assets/Stickers/AUTUMN_16.png +0 -0
- package/android/src/main/assets/Stickers/AUTUMN_17.png +0 -0
- package/android/src/main/assets/Stickers/AUTUMN_18.png +0 -0
- package/android/src/main/assets/Stickers/AUTUMN_2.png +0 -0
- package/android/src/main/assets/Stickers/AUTUMN_3.png +0 -0
- package/android/src/main/assets/Stickers/AUTUMN_4.png +0 -0
- package/android/src/main/assets/Stickers/AUTUMN_5.png +0 -0
- package/android/src/main/assets/Stickers/AUTUMN_6.png +0 -0
- package/android/src/main/assets/Stickers/AUTUMN_7.png +0 -0
- package/android/src/main/assets/Stickers/AUTUMN_8.png +0 -0
- package/android/src/main/assets/Stickers/AUTUMN_9.png +0 -0
- package/android/src/main/assets/Stickers/LOL_1.png +0 -0
- package/android/src/main/assets/Stickers/LOL_10.png +0 -0
- package/android/src/main/assets/Stickers/LOL_11.png +0 -0
- package/android/src/main/assets/Stickers/LOL_12.png +0 -0
- package/android/src/main/assets/Stickers/LOL_13.png +0 -0
- package/android/src/main/assets/Stickers/LOL_14.png +0 -0
- package/android/src/main/assets/Stickers/LOL_15.png +0 -0
- package/android/src/main/assets/Stickers/LOL_16.png +0 -0
- package/android/src/main/assets/Stickers/LOL_17.png +0 -0
- package/android/src/main/assets/Stickers/LOL_18.png +0 -0
- package/android/src/main/assets/Stickers/LOL_19.png +0 -0
- package/android/src/main/assets/Stickers/LOL_2.png +0 -0
- package/android/src/main/assets/Stickers/LOL_3.png +0 -0
- package/android/src/main/assets/Stickers/LOL_4.png +0 -0
- package/android/src/main/assets/Stickers/LOL_5.png +0 -0
- package/android/src/main/assets/Stickers/LOL_6.png +0 -0
- package/android/src/main/assets/Stickers/LOL_7.png +0 -0
- package/android/src/main/assets/Stickers/LOL_8.png +0 -0
- package/android/src/main/assets/Stickers/LOL_9.png +0 -0
- package/android/src/main/assets/Stickers/SPRING_1.png +0 -0
- package/android/src/main/assets/Stickers/SPRING_2.png +0 -0
- package/android/src/main/assets/Stickers/SPRING_3.png +0 -0
- package/android/src/main/assets/Stickers/SPRING_4.png +0 -0
- package/android/src/main/assets/Stickers/SPRING_5.png +0 -0
- package/android/src/main/assets/Stickers/SPRING_6.png +0 -0
- package/android/src/main/assets/Stickers/SUMMER_1.png +0 -0
- package/android/src/main/assets/Stickers/SUMMER_10.png +0 -0
- package/android/src/main/assets/Stickers/SUMMER_11.png +0 -0
- package/android/src/main/assets/Stickers/SUMMER_12.png +0 -0
- package/android/src/main/assets/Stickers/SUMMER_13.png +0 -0
- package/android/src/main/assets/Stickers/SUMMER_14.png +0 -0
- package/android/src/main/assets/Stickers/SUMMER_15.png +0 -0
- package/android/src/main/assets/Stickers/SUMMER_16.png +0 -0
- package/android/src/main/assets/Stickers/SUMMER_17.png +0 -0
- package/android/src/main/assets/Stickers/SUMMER_18.png +0 -0
- package/android/src/main/assets/Stickers/SUMMER_19.png +0 -0
- package/android/src/main/assets/Stickers/SUMMER_2.png +0 -0
- package/android/src/main/assets/Stickers/SUMMER_20.png +0 -0
- package/android/src/main/assets/Stickers/SUMMER_21.png +0 -0
- package/android/src/main/assets/Stickers/SUMMER_3.png +0 -0
- package/android/src/main/assets/Stickers/SUMMER_4.png +0 -0
- package/android/src/main/assets/Stickers/SUMMER_5.png +0 -0
- package/android/src/main/assets/Stickers/SUMMER_6.png +0 -0
- package/android/src/main/assets/Stickers/SUMMER_7.png +0 -0
- package/android/src/main/assets/Stickers/SUMMER_8.png +0 -0
- package/android/src/main/assets/Stickers/SUMMER_9.png +0 -0
- package/android/src/main/assets/Stickers/TEXT_1.png +0 -0
- package/android/src/main/assets/Stickers/TEXT_12.png +0 -0
- package/android/src/main/assets/Stickers/TEXT_13.png +0 -0
- package/android/src/main/assets/Stickers/TEXT_14.png +0 -0
- package/android/src/main/assets/Stickers/TEXT_15.png +0 -0
- package/android/src/main/assets/Stickers/TEXT_16.png +0 -0
- package/android/src/main/assets/Stickers/TEXT_2.png +0 -0
- package/android/src/main/assets/Stickers/TEXT_25.png +0 -0
- package/android/src/main/assets/Stickers/TEXT_26.png +0 -0
- package/android/src/main/assets/Stickers/TEXT_27.png +0 -0
- package/android/src/main/assets/Stickers/TEXT_28.png +0 -0
- package/android/src/main/assets/Stickers/TEXT_29.png +0 -0
- package/android/src/main/assets/Stickers/TEXT_3.png +0 -0
- package/android/src/main/assets/Stickers/TEXT_30.png +0 -0
- package/android/src/main/assets/Stickers/TEXT_31.png +0 -0
- package/android/src/main/assets/Stickers/TEXT_32.png +0 -0
- package/android/src/main/assets/Stickers/TEXT_7.png +0 -0
- package/android/src/main/assets/Stickers/TEXT_8.png +0 -0
- package/android/src/main/assets/Stickers/TEXT_9.png +0 -0
- package/android/src/main/java/com/photovideoeditor/editor/PhotoVideoEditorActivity.kt +901 -651
- package/android/src/main/java/com/photovideoeditor/photo/export/PhotoExporter.kt +8 -23
- package/android/src/main/java/com/photovideoeditor/photo/render/CropGeometry.kt +71 -0
- package/android/src/main/java/com/photovideoeditor/photo/render/OverlayGeometry.kt +36 -0
- package/android/src/main/java/com/photovideoeditor/photo/render/PhotoEditSession.kt +5 -7
- package/android/src/main/java/com/photovideoeditor/photo/render/PhotoFilterPresets.kt +71 -41
- package/android/src/main/java/com/photovideoeditor/photo/render/PhotoLayer.kt +5 -1
- package/android/src/main/java/com/photovideoeditor/photo/render/PhotoLayerRenderer.kt +24 -1
- package/android/src/main/java/com/photovideoeditor/photo/render/PhotoTransformRenderer.kt +33 -0
- package/android/src/main/java/com/photovideoeditor/photo/render/PhotoTransformState.kt +6 -1
- package/android/src/main/java/com/photovideoeditor/photo/ui/CropOverlayView.kt +112 -122
- package/android/src/main/java/com/photovideoeditor/photo/ui/DrawOverlayView.kt +66 -0
- package/android/src/main/java/com/photovideoeditor/photo/ui/LayerOverlayView.kt +63 -14
- package/android/src/main/java/com/photovideoeditor/photo/ui/ZoomableImageView.kt +10 -0
- package/android/src/main/java/com/photovideoeditor/stickers/OnlineStickerSheet.kt +135 -104
- package/android/src/main/java/com/photovideoeditor/ui/components/CropPanel.kt +122 -0
- package/android/src/main/java/com/photovideoeditor/ui/components/EditorPanel.kt +250 -0
- package/android/src/main/java/com/photovideoeditor/ui/components/EditorToolSlider.kt +103 -0
- package/android/src/main/java/com/photovideoeditor/video/export/VideoExporter.kt +38 -11
- package/android/src/main/java/com/photovideoeditor/video/render/VideoCropEffects.kt +35 -0
- package/android/src/main/java/com/photovideoeditor/video/render/VideoEditSession.kt +10 -0
- package/android/src/main/java/com/photovideoeditor/video/render/VideoTransformState.kt +1 -0
- package/android/src/main/res/drawable/ic_draw.xml +6 -0
- package/android/src/main/res/drawable/ic_pause.xml +7 -0
- package/android/src/main/res/drawable/ic_play.xml +7 -0
- package/docs/api.md +1 -1
- package/docs/editor-ui-validation.md +22 -0
- package/docs/photo-editor.md +5 -6
- package/ios/Editor/PhotoVideoEditorViewController.swift +918 -494
- package/ios/Editor/UI/CropPanel.swift +97 -0
- package/ios/Editor/UI/EditorPanel.swift +334 -0
- package/ios/Editor/UI/EditorToolSlider.swift +22 -0
- package/ios/Editor/UI/TextEditorSheet.swift +84 -0
- package/ios/Photo/Export/PhotoExporter.swift +3 -3
- package/ios/Photo/Rendering/CropGeometry.swift +48 -0
- package/ios/Photo/Rendering/PhotoEditSession.swift +21 -10
- package/ios/Photo/Rendering/PhotoFilterPresets.swift +53 -23
- package/ios/Photo/Rendering/PhotoLayer.swift +14 -2
- package/ios/Photo/Rendering/PhotoLayerRenderer.swift +16 -1
- package/ios/Photo/Rendering/PhotoTransformState.swift +5 -0
- package/ios/Photo/UI/CropOverlayView.swift +98 -134
- package/ios/Photo/UI/DrawOverlayView.swift +48 -0
- package/ios/Photo/UI/FilterThumbnailLoader.swift +62 -0
- package/ios/Photo/UI/LayerOverlayView.swift +36 -11
- package/ios/Photo/UI/ZoomableImageView.swift +19 -1
- package/ios/Resources/Stickers/AUTUMN_1.png +0 -0
- package/ios/Resources/Stickers/AUTUMN_10.png +0 -0
- package/ios/Resources/Stickers/AUTUMN_11.png +0 -0
- package/ios/Resources/Stickers/AUTUMN_12.png +0 -0
- package/ios/Resources/Stickers/AUTUMN_13.png +0 -0
- package/ios/Resources/Stickers/AUTUMN_14.png +0 -0
- package/ios/Resources/Stickers/AUTUMN_15.png +0 -0
- package/ios/Resources/Stickers/AUTUMN_16.png +0 -0
- package/ios/Resources/Stickers/AUTUMN_17.png +0 -0
- package/ios/Resources/Stickers/AUTUMN_18.png +0 -0
- package/ios/Resources/Stickers/AUTUMN_2.png +0 -0
- package/ios/Resources/Stickers/AUTUMN_3.png +0 -0
- package/ios/Resources/Stickers/AUTUMN_4.png +0 -0
- package/ios/Resources/Stickers/AUTUMN_5.png +0 -0
- package/ios/Resources/Stickers/AUTUMN_6.png +0 -0
- package/ios/Resources/Stickers/AUTUMN_7.png +0 -0
- package/ios/Resources/Stickers/AUTUMN_8.png +0 -0
- package/ios/Resources/Stickers/AUTUMN_9.png +0 -0
- package/ios/Resources/Stickers/LOL_1.png +0 -0
- package/ios/Resources/Stickers/LOL_10.png +0 -0
- package/ios/Resources/Stickers/LOL_11.png +0 -0
- package/ios/Resources/Stickers/LOL_12.png +0 -0
- package/ios/Resources/Stickers/LOL_13.png +0 -0
- package/ios/Resources/Stickers/LOL_14.png +0 -0
- package/ios/Resources/Stickers/LOL_15.png +0 -0
- package/ios/Resources/Stickers/LOL_16.png +0 -0
- package/ios/Resources/Stickers/LOL_17.png +0 -0
- package/ios/Resources/Stickers/LOL_18.png +0 -0
- package/ios/Resources/Stickers/LOL_19.png +0 -0
- package/ios/Resources/Stickers/LOL_2.png +0 -0
- package/ios/Resources/Stickers/LOL_3.png +0 -0
- package/ios/Resources/Stickers/LOL_4.png +0 -0
- package/ios/Resources/Stickers/LOL_5.png +0 -0
- package/ios/Resources/Stickers/LOL_6.png +0 -0
- package/ios/Resources/Stickers/LOL_7.png +0 -0
- package/ios/Resources/Stickers/LOL_8.png +0 -0
- package/ios/Resources/Stickers/LOL_9.png +0 -0
- package/ios/Resources/Stickers/SPRING_1.png +0 -0
- package/ios/Resources/Stickers/SPRING_2.png +0 -0
- package/ios/Resources/Stickers/SPRING_3.png +0 -0
- package/ios/Resources/Stickers/SPRING_4.png +0 -0
- package/ios/Resources/Stickers/SPRING_5.png +0 -0
- package/ios/Resources/Stickers/SPRING_6.png +0 -0
- package/ios/Resources/Stickers/SUMMER_1.png +0 -0
- package/ios/Resources/Stickers/SUMMER_10.png +0 -0
- package/ios/Resources/Stickers/SUMMER_11.png +0 -0
- package/ios/Resources/Stickers/SUMMER_12.png +0 -0
- package/ios/Resources/Stickers/SUMMER_13.png +0 -0
- package/ios/Resources/Stickers/SUMMER_14.png +0 -0
- package/ios/Resources/Stickers/SUMMER_15.png +0 -0
- package/ios/Resources/Stickers/SUMMER_16.png +0 -0
- package/ios/Resources/Stickers/SUMMER_17.png +0 -0
- package/ios/Resources/Stickers/SUMMER_18.png +0 -0
- package/ios/Resources/Stickers/SUMMER_19.png +0 -0
- package/ios/Resources/Stickers/SUMMER_2.png +0 -0
- package/ios/Resources/Stickers/SUMMER_20.png +0 -0
- package/ios/Resources/Stickers/SUMMER_21.png +0 -0
- package/ios/Resources/Stickers/SUMMER_3.png +0 -0
- package/ios/Resources/Stickers/SUMMER_4.png +0 -0
- package/ios/Resources/Stickers/SUMMER_5.png +0 -0
- package/ios/Resources/Stickers/SUMMER_6.png +0 -0
- package/ios/Resources/Stickers/SUMMER_7.png +0 -0
- package/ios/Resources/Stickers/SUMMER_8.png +0 -0
- package/ios/Resources/Stickers/SUMMER_9.png +0 -0
- package/ios/Resources/Stickers/TEXT_1.png +0 -0
- package/ios/Resources/Stickers/TEXT_12.png +0 -0
- package/ios/Resources/Stickers/TEXT_13.png +0 -0
- package/ios/Resources/Stickers/TEXT_14.png +0 -0
- package/ios/Resources/Stickers/TEXT_15.png +0 -0
- package/ios/Resources/Stickers/TEXT_16.png +0 -0
- package/ios/Resources/Stickers/TEXT_2.png +0 -0
- package/ios/Resources/Stickers/TEXT_25.png +0 -0
- package/ios/Resources/Stickers/TEXT_26.png +0 -0
- package/ios/Resources/Stickers/TEXT_27.png +0 -0
- package/ios/Resources/Stickers/TEXT_28.png +0 -0
- package/ios/Resources/Stickers/TEXT_29.png +0 -0
- package/ios/Resources/Stickers/TEXT_3.png +0 -0
- package/ios/Resources/Stickers/TEXT_30.png +0 -0
- package/ios/Resources/Stickers/TEXT_31.png +0 -0
- package/ios/Resources/Stickers/TEXT_32.png +0 -0
- package/ios/Resources/Stickers/TEXT_7.png +0 -0
- package/ios/Resources/Stickers/TEXT_8.png +0 -0
- package/ios/Resources/Stickers/TEXT_9.png +0 -0
- package/ios/Resources/Stickers/a_pety_icon.png +0 -0
- package/ios/Stickers/OnlineStickerSheetViewController.swift +102 -159
- package/ios/Video/Export/VideoExporter.swift +10 -1
- package/ios/Video/Render/VideoCropGeometry.swift +48 -0
- package/ios/Video/Render/VideoTransformState.swift +1 -0
- package/lib/module/index.js +2 -2
- package/lib/module/index.js.map +1 -1
- package/lib/typescript/src/types.d.ts +2 -2
- package/lib/typescript/src/types.d.ts.map +1 -1
- package/package.json +2 -1
- package/src/index.ts +1 -1
- package/src/types.ts +2 -2
- package/android/src/main/java/com/photovideoeditor/stickers/OpenMojiCatalog.kt +0 -172
- package/android/src/main/java/com/photovideoeditor/stickers/RemoteImageLoader.kt +0 -110
- package/ios/Stickers/OpenMojiCatalog.swift +0 -188
- package/ios/Stickers/RemoteImageLoader.swift +0 -127
|
@@ -12,6 +12,7 @@ import com.photovideoeditor.photo.render.PhotoAdjustmentRenderer
|
|
|
12
12
|
import com.photovideoeditor.photo.render.PhotoAdjustments
|
|
13
13
|
import com.photovideoeditor.photo.render.PhotoLayer
|
|
14
14
|
import com.photovideoeditor.photo.render.PhotoLayerRenderer
|
|
15
|
+
import com.photovideoeditor.photo.render.PhotoTransformRenderer
|
|
15
16
|
import com.photovideoeditor.photo.render.PhotoTransformState
|
|
16
17
|
import org.json.JSONObject
|
|
17
18
|
import java.io.File
|
|
@@ -68,11 +69,11 @@ object PhotoExporter {
|
|
|
68
69
|
?: throw PhotoExportException("E_OUT_OF_MEMORY", "The photo is too large to process on this device.")
|
|
69
70
|
|
|
70
71
|
val upright = applyExifOrientation(decoded, exifOrientation)
|
|
71
|
-
val transformed =
|
|
72
|
+
val transformed = PhotoTransformRenderer.transform(upright, transform)
|
|
72
73
|
val adjusted = PhotoAdjustmentRenderer.apply(transformed, adjustments)
|
|
73
|
-
val
|
|
74
|
-
val
|
|
75
|
-
val resized = resizeToFit(
|
|
74
|
+
val cropped = PhotoTransformRenderer.crop(adjusted, transform)
|
|
75
|
+
val layered = PhotoLayerRenderer.render(cropped, layers) { uri -> resolveImageUri(context, uri) }
|
|
76
|
+
val resized = resizeToFit(layered, maxWidth, maxHeight)
|
|
76
77
|
|
|
77
78
|
val format = exportOptions?.optString("imageFormat", "jpeg")?.lowercase() ?: "jpeg"
|
|
78
79
|
val quality = qualityFor(exportOptions?.optString("quality", "high") ?: "high")
|
|
@@ -93,9 +94,9 @@ object PhotoExporter {
|
|
|
93
94
|
if (decoded !== upright) decoded.recycle()
|
|
94
95
|
if (upright !== transformed) upright.recycle()
|
|
95
96
|
if (transformed !== adjusted) transformed.recycle()
|
|
96
|
-
if (adjusted !==
|
|
97
|
-
if (
|
|
98
|
-
if (
|
|
97
|
+
if (adjusted !== cropped) adjusted.recycle()
|
|
98
|
+
if (cropped !== layered) cropped.recycle()
|
|
99
|
+
if (layered !== resized) layered.recycle()
|
|
99
100
|
|
|
100
101
|
return PhotoExportResult(
|
|
101
102
|
uri = Uri.fromFile(outputFile).toString(),
|
|
@@ -130,22 +131,6 @@ object PhotoExporter {
|
|
|
130
131
|
return Bitmap.createBitmap(bitmap, 0, 0, bitmap.width, bitmap.height, matrix, true)
|
|
131
132
|
}
|
|
132
133
|
|
|
133
|
-
private fun applyTransform(bitmap: Bitmap, transform: PhotoTransformState): Bitmap {
|
|
134
|
-
if (transform.rotationDegrees == 0 && transform.straightenDegrees == 0f) return bitmap
|
|
135
|
-
return Bitmap.createBitmap(bitmap, 0, 0, bitmap.width, bitmap.height, transform.toMatrix(), true)
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
private fun cropBitmap(bitmap: Bitmap, transform: PhotoTransformState): Bitmap {
|
|
139
|
-
if (transform.cropLeft <= 0f && transform.cropTop <= 0f && transform.cropRight >= 1f && transform.cropBottom >= 1f) {
|
|
140
|
-
return bitmap
|
|
141
|
-
}
|
|
142
|
-
val left = (transform.cropLeft * bitmap.width).roundToInt().coerceIn(0, bitmap.width - 1)
|
|
143
|
-
val top = (transform.cropTop * bitmap.height).roundToInt().coerceIn(0, bitmap.height - 1)
|
|
144
|
-
val right = (transform.cropRight * bitmap.width).roundToInt().coerceIn(left + 1, bitmap.width)
|
|
145
|
-
val bottom = (transform.cropBottom * bitmap.height).roundToInt().coerceIn(top + 1, bitmap.height)
|
|
146
|
-
return Bitmap.createBitmap(bitmap, left, top, right - left, bottom - top)
|
|
147
|
-
}
|
|
148
|
-
|
|
149
134
|
private fun resizeToFit(bitmap: Bitmap, maxWidth: Int?, maxHeight: Int?): Bitmap {
|
|
150
135
|
if (maxWidth == null && maxHeight == null) return bitmap
|
|
151
136
|
val widthLimit = maxWidth ?: bitmap.width
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
package com.photovideoeditor.photo.render
|
|
2
|
+
|
|
3
|
+
import kotlin.math.abs
|
|
4
|
+
import kotlin.math.cos
|
|
5
|
+
import kotlin.math.sin
|
|
6
|
+
|
|
7
|
+
/** Resolution-independent crop constraints shared by gestures and rendering. */
|
|
8
|
+
object CropGeometry {
|
|
9
|
+
data class Rect(val left: Float, val top: Float, val right: Float, val bottom: Float) {
|
|
10
|
+
val width get() = right - left
|
|
11
|
+
val height get() = bottom - top
|
|
12
|
+
}
|
|
13
|
+
enum class Handle { MOVE, TOP, RIGHT, BOTTOM, LEFT, TOP_LEFT, TOP_RIGHT, BOTTOM_LEFT, BOTTOM_RIGHT }
|
|
14
|
+
|
|
15
|
+
fun fillScale(width: Float, height: Float, degrees: Float): Float {
|
|
16
|
+
val radians = Math.toRadians(degrees.toDouble())
|
|
17
|
+
val c = abs(cos(radians)).toFloat(); val s = abs(sin(radians)).toFloat()
|
|
18
|
+
return maxOf(c + height / width * s, c + width / height * s)
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
fun fit(bounds: Rect, ratio: Float?): Rect {
|
|
22
|
+
if (ratio == null || ratio <= 0) return bounds
|
|
23
|
+
val width = minOf(bounds.width, bounds.height * ratio)
|
|
24
|
+
val height = width / ratio
|
|
25
|
+
val x = (bounds.left + bounds.right - width) / 2
|
|
26
|
+
val y = (bounds.top + bounds.bottom - height) / 2
|
|
27
|
+
return Rect(x, y, x + width, y + height)
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/** Keeps the opposite edge/corner anchored; clamps motion instead of shifting a resized box. */
|
|
31
|
+
fun resize(r: Rect, bounds: Rect, handle: Handle, dx: Float, dy: Float, ratio: Float?, minimum: Float): Rect {
|
|
32
|
+
if (handle == Handle.MOVE) {
|
|
33
|
+
val x = dx.coerceIn(bounds.left - r.left, bounds.right - r.right)
|
|
34
|
+
val y = dy.coerceIn(bounds.top - r.top, bounds.bottom - r.bottom)
|
|
35
|
+
return Rect(r.left + x, r.top + y, r.right + x, r.bottom + y)
|
|
36
|
+
}
|
|
37
|
+
val left = handle in listOf(Handle.LEFT, Handle.TOP_LEFT, Handle.BOTTOM_LEFT)
|
|
38
|
+
val right = handle in listOf(Handle.RIGHT, Handle.TOP_RIGHT, Handle.BOTTOM_RIGHT)
|
|
39
|
+
val top = handle in listOf(Handle.TOP, Handle.TOP_LEFT, Handle.TOP_RIGHT)
|
|
40
|
+
val bottom = handle in listOf(Handle.BOTTOM, Handle.BOTTOM_LEFT, Handle.BOTTOM_RIGHT)
|
|
41
|
+
var l = r.left + if (left) dx else 0f
|
|
42
|
+
var t = r.top + if (top) dy else 0f
|
|
43
|
+
var rr = r.right + if (right) dx else 0f
|
|
44
|
+
var b = r.bottom + if (bottom) dy else 0f
|
|
45
|
+
if (ratio != null && ratio > 0) {
|
|
46
|
+
if ((left || right) && (top || bottom)) {
|
|
47
|
+
val deltaW = if (left) -dx else dx
|
|
48
|
+
val deltaH = if (top) -dy else dy
|
|
49
|
+
val width = r.width + if (abs(deltaW) > abs(deltaH * ratio)) deltaW else deltaH * ratio
|
|
50
|
+
if (left) l = r.right - width else rr = r.left + width
|
|
51
|
+
if (top) t = r.bottom - width / ratio else b = r.top + width / ratio
|
|
52
|
+
} else if (left || right) {
|
|
53
|
+
val height = (rr - l) / ratio
|
|
54
|
+
t = (r.top + r.bottom - height) / 2; b = t + height
|
|
55
|
+
} else {
|
|
56
|
+
val width = (b - t) * ratio
|
|
57
|
+
l = (r.left + r.right - width) / 2; rr = l + width
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
// All constraints are linear along the drag; interpolation preserves the ratio and anchor.
|
|
61
|
+
var fraction = 1f
|
|
62
|
+
fun limit(before: Float, after: Float, minimumValue: Float) {
|
|
63
|
+
if (after < minimumValue && after < before) fraction = minOf(fraction, ((before - minimumValue) / (before - after)).coerceIn(0f, 1f))
|
|
64
|
+
}
|
|
65
|
+
limit(r.left, l, bounds.left); limit(r.top, t, bounds.top)
|
|
66
|
+
limit(-r.right, -rr, -bounds.right); limit(-r.bottom, -b, -bounds.bottom)
|
|
67
|
+
limit(r.width, rr - l, minOf(minimum, r.width)); limit(r.height, b - t, minOf(minimum, r.height))
|
|
68
|
+
return Rect(r.left + (l - r.left) * fraction, r.top + (t - r.top) * fraction,
|
|
69
|
+
r.right + (rr - r.right) * fraction, r.bottom + (b - r.bottom) * fraction)
|
|
70
|
+
}
|
|
71
|
+
}
|
|
@@ -2,6 +2,16 @@ package com.photovideoeditor.photo.render
|
|
|
2
2
|
|
|
3
3
|
/** Pure coordinate mapping shared by preview and export renderers. */
|
|
4
4
|
object OverlayGeometry {
|
|
5
|
+
/** New sticker width is 30% of media width; persisted layers keep their existing scale. */
|
|
6
|
+
fun initialStickerScale(width: Float, height: Float): Float =
|
|
7
|
+
if (width > 0 && height > 0) 0.3f * width / (0.18f * minOf(width, height)) else 0.3f / 0.18f
|
|
8
|
+
|
|
9
|
+
fun stickerSize(shortSide: Float, aspectRatio: Float): Pair<Float, Float> {
|
|
10
|
+
val width = shortSide * 0.18f
|
|
11
|
+
val aspect = aspectRatio.takeIf { it.isFinite() && it > 0f } ?: 1f
|
|
12
|
+
return width to (width / aspect)
|
|
13
|
+
}
|
|
14
|
+
|
|
5
15
|
data class Transform(
|
|
6
16
|
val centerX: Float,
|
|
7
17
|
val centerY: Float,
|
|
@@ -11,6 +21,32 @@ object OverlayGeometry {
|
|
|
11
21
|
val shortSide: Float,
|
|
12
22
|
)
|
|
13
23
|
|
|
24
|
+
/**
|
|
25
|
+
* How to size the bitmap that [PhotoLayerRenderer] composites overlays onto for a
|
|
26
|
+
* [frameWidth]x[frameHeight] output frame, plus the scale a compositor must apply to that bitmap
|
|
27
|
+
* for it to cover the frame exactly.
|
|
28
|
+
*
|
|
29
|
+
* [PhotoLayerRenderer] works in coordinates normalized to the bitmap it is handed, so the invariant
|
|
30
|
+
* every caller must preserve is `width * scaleX == frameWidth` (and likewise for height): a bitmap
|
|
31
|
+
* that ends up covering less than the full frame shrinks every layer and pulls it toward the frame
|
|
32
|
+
* centre, which is precisely how exports came to disagree with the preview.
|
|
33
|
+
*
|
|
34
|
+
* [maxEdgePx] bounds the allocation for high-resolution outputs; the returned scale compensates for
|
|
35
|
+
* whatever downscale that forced, so geometry is identical at every resolution and only sharpness
|
|
36
|
+
* varies.
|
|
37
|
+
*/
|
|
38
|
+
data class CanvasPlan(val width: Int, val height: Int, val scaleX: Float, val scaleY: Float)
|
|
39
|
+
|
|
40
|
+
fun canvasPlan(frameWidth: Int, frameHeight: Int, maxEdgePx: Float): CanvasPlan {
|
|
41
|
+
val safeWidth = frameWidth.coerceAtLeast(2)
|
|
42
|
+
val safeHeight = frameHeight.coerceAtLeast(2)
|
|
43
|
+
val fit = minOf(1f, maxEdgePx / maxOf(safeWidth, safeHeight))
|
|
44
|
+
val width = (safeWidth * fit).toInt().coerceAtLeast(2)
|
|
45
|
+
val height = (safeHeight * fit).toInt().coerceAtLeast(2)
|
|
46
|
+
// Divide by the rounded size actually allocated, not `fit`, so rounding cannot reintroduce drift.
|
|
47
|
+
return CanvasPlan(width, height, safeWidth.toFloat() / width, safeHeight.toFloat() / height)
|
|
48
|
+
}
|
|
49
|
+
|
|
14
50
|
fun map(layer: PhotoLayer, frameWidth: Int, frameHeight: Int): Transform = Transform(
|
|
15
51
|
centerX = layer.x * frameWidth,
|
|
16
52
|
centerY = layer.y * frameHeight,
|
|
@@ -42,11 +42,7 @@ class PhotoEditSession(private val context: Context, sourceUri: String, maxPrevi
|
|
|
42
42
|
val base = baseBitmap ?: return null
|
|
43
43
|
val key = Pair(state, adjustments)
|
|
44
44
|
cachedBaseBitmap?.let { if (cachedKey == key) return it }
|
|
45
|
-
val transformed =
|
|
46
|
-
base
|
|
47
|
-
} else {
|
|
48
|
-
Bitmap.createBitmap(base, 0, 0, base.width, base.height, state.toMatrix(), true)
|
|
49
|
-
}
|
|
45
|
+
val transformed = PhotoTransformRenderer.transform(base, state)
|
|
50
46
|
val adjusted = PhotoAdjustmentRenderer.apply(transformed, adjustments)
|
|
51
47
|
cachedBaseBitmap = adjusted
|
|
52
48
|
cachedKey = key
|
|
@@ -54,8 +50,10 @@ class PhotoEditSession(private val context: Context, sourceUri: String, maxPrevi
|
|
|
54
50
|
}
|
|
55
51
|
|
|
56
52
|
/** Renders rotate/straighten + adjustments + layers — the crop overlay draws the crop live on top of this. */
|
|
57
|
-
fun renderPreview(): Bitmap? {
|
|
58
|
-
val
|
|
53
|
+
fun renderPreview(cropping: Boolean = false): Bitmap? {
|
|
54
|
+
val transformed = computeBase() ?: return null
|
|
55
|
+
if (cropping) return transformed
|
|
56
|
+
val base = PhotoTransformRenderer.crop(transformed, state)
|
|
59
57
|
val layers = layerStack.layers
|
|
60
58
|
cachedPreviewBitmap?.let { cached ->
|
|
61
59
|
if (cachedPreviewBase === base && cachedPreviewLayerRevision == layerStack.revision) return cached
|
|
@@ -2,63 +2,93 @@ package com.photovideoeditor.photo.render
|
|
|
2
2
|
|
|
3
3
|
import android.graphics.ColorMatrix
|
|
4
4
|
|
|
5
|
-
/**
|
|
5
|
+
/**
|
|
6
|
+
* Original, neutrally named preset filters (no third-party looks or names copied).
|
|
7
|
+
*
|
|
8
|
+
* Every preset is expressed as the same four knobs the renderer already supports, so a preset can
|
|
9
|
+
* never be a button that does nothing: set [saturation], scale RGB by [scale], then add the
|
|
10
|
+
* per-channel offsets. `PhotoFilterPresets.swift` mirrors this table field-for-field — add presets to
|
|
11
|
+
* both or neither, or the two platforms will render different looks for the same id.
|
|
12
|
+
*/
|
|
13
|
+
data class PhotoFilterRecipe(
|
|
14
|
+
val saturation: Float,
|
|
15
|
+
val scale: Float,
|
|
16
|
+
val redOffset: Float,
|
|
17
|
+
val greenOffset: Float,
|
|
18
|
+
val blueOffset: Float
|
|
19
|
+
)
|
|
20
|
+
|
|
6
21
|
object PhotoFilterPresets {
|
|
7
|
-
|
|
22
|
+
/** Display order in the carousel. "Original" (no preset) is prepended by the UI, not listed here. */
|
|
23
|
+
val PRESET_IDS = listOf(
|
|
24
|
+
"vivid", "vividWarm", "vividCool",
|
|
25
|
+
"warmth", "cool",
|
|
26
|
+
"natural", "soft", "fade",
|
|
27
|
+
"mono", "noir", "silvertone",
|
|
28
|
+
"vintage", "sepia", "retro",
|
|
29
|
+
"dramatic", "dramaticWarm", "dramaticCool"
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
private val recipes = mapOf(
|
|
33
|
+
"vivid" to PhotoFilterRecipe(1.45f, 1.05f, 0f, 0f, 0f),
|
|
34
|
+
"vividWarm" to PhotoFilterRecipe(1.45f, 1.05f, 12f, 4f, -10f),
|
|
35
|
+
"vividCool" to PhotoFilterRecipe(1.45f, 1.05f, -8f, 0f, 14f),
|
|
36
|
+
"warmth" to PhotoFilterRecipe(1.05f, 1f, 18f, 6f, -12f),
|
|
37
|
+
"cool" to PhotoFilterRecipe(1.05f, 1f, -12f, 0f, 18f),
|
|
38
|
+
"natural" to PhotoFilterRecipe(1.15f, 1.02f, 2f, 2f, 0f),
|
|
39
|
+
"soft" to PhotoFilterRecipe(0.9f, 0.96f, 14f, 12f, 12f),
|
|
40
|
+
"fade" to PhotoFilterRecipe(1f, 0.9f, 25f, 25f, 25f),
|
|
41
|
+
"mono" to PhotoFilterRecipe(0f, 1f, 0f, 0f, 0f),
|
|
42
|
+
"noir" to PhotoFilterRecipe(0f, 1.3f, -40f, -40f, -40f),
|
|
43
|
+
"silvertone" to PhotoFilterRecipe(0f, 1.1f, -6f, -2f, 10f),
|
|
44
|
+
"vintage" to PhotoFilterRecipe(0.6f, 0.95f, 24f, 12f, -6f),
|
|
45
|
+
"sepia" to PhotoFilterRecipe(0f, 1f, 38f, 20f, -14f),
|
|
46
|
+
"retro" to PhotoFilterRecipe(0.75f, 0.92f, 20f, 6f, 14f),
|
|
47
|
+
"dramatic" to PhotoFilterRecipe(1.2f, 1.25f, -28f, -28f, -28f),
|
|
48
|
+
"dramaticWarm" to PhotoFilterRecipe(1.2f, 1.25f, -14f, -26f, -38f),
|
|
49
|
+
"dramaticCool" to PhotoFilterRecipe(1.2f, 1.25f, -38f, -28f, -12f),
|
|
50
|
+
// Retained so photos saved with an earlier preset id still render; not offered in the carousel.
|
|
51
|
+
"chrome" to PhotoFilterRecipe(1.4f, 1.1f, 0f, 0f, 0f)
|
|
52
|
+
)
|
|
8
53
|
|
|
9
54
|
fun labelFor(id: String): String = when (id) {
|
|
55
|
+
"vivid" -> "Vivid"
|
|
56
|
+
"vividWarm" -> "Vivid Warm"
|
|
57
|
+
"vividCool" -> "Vivid Cool"
|
|
58
|
+
"warmth" -> "Warm"
|
|
59
|
+
"cool" -> "Cool"
|
|
60
|
+
"natural" -> "Natural"
|
|
61
|
+
"soft" -> "Soft"
|
|
62
|
+
"fade" -> "Fade"
|
|
10
63
|
"mono" -> "Mono"
|
|
11
64
|
"noir" -> "Noir"
|
|
12
|
-
"
|
|
65
|
+
"silvertone" -> "Silvertone"
|
|
66
|
+
"vintage" -> "Vintage"
|
|
67
|
+
"sepia" -> "Sepia"
|
|
68
|
+
"retro" -> "Retro"
|
|
69
|
+
"dramatic" -> "Dramatic"
|
|
70
|
+
"dramaticWarm" -> "Dramatic Warm"
|
|
71
|
+
"dramaticCool" -> "Dramatic Cool"
|
|
13
72
|
"chrome" -> "Chrome"
|
|
14
|
-
"warmth" -> "Warmth"
|
|
15
73
|
else -> id
|
|
16
74
|
}
|
|
17
75
|
|
|
18
|
-
fun
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
floatArrayOf(
|
|
25
|
-
1.3f, 0f, 0f, 0f, -40f,
|
|
26
|
-
0f, 1.3f, 0f, 0f, -40f,
|
|
27
|
-
0f, 0f, 1.3f, 0f, -40f,
|
|
28
|
-
0f, 0f, 0f, 1f, 0f
|
|
29
|
-
)
|
|
30
|
-
)
|
|
31
|
-
)
|
|
32
|
-
}
|
|
33
|
-
"fade" -> ColorMatrix(
|
|
34
|
-
floatArrayOf(
|
|
35
|
-
0.9f, 0f, 0f, 0f, 25f,
|
|
36
|
-
0f, 0.9f, 0f, 0f, 25f,
|
|
37
|
-
0f, 0f, 0.9f, 0f, 25f,
|
|
38
|
-
0f, 0f, 0f, 1f, 0f
|
|
39
|
-
)
|
|
40
|
-
)
|
|
41
|
-
"chrome" -> ColorMatrix().apply {
|
|
42
|
-
setSaturation(1.4f)
|
|
76
|
+
fun recipeFor(id: String): PhotoFilterRecipe? = recipes[id]
|
|
77
|
+
|
|
78
|
+
fun matrixFor(id: String): ColorMatrix? {
|
|
79
|
+
val recipe = recipes[id] ?: return null
|
|
80
|
+
return ColorMatrix().apply {
|
|
81
|
+
setSaturation(recipe.saturation)
|
|
43
82
|
postConcat(
|
|
44
83
|
ColorMatrix(
|
|
45
84
|
floatArrayOf(
|
|
46
|
-
|
|
47
|
-
0f,
|
|
48
|
-
0f, 0f,
|
|
85
|
+
recipe.scale, 0f, 0f, 0f, recipe.redOffset,
|
|
86
|
+
0f, recipe.scale, 0f, 0f, recipe.greenOffset,
|
|
87
|
+
0f, 0f, recipe.scale, 0f, recipe.blueOffset,
|
|
49
88
|
0f, 0f, 0f, 1f, 0f
|
|
50
89
|
)
|
|
51
90
|
)
|
|
52
91
|
)
|
|
53
92
|
}
|
|
54
|
-
"warmth" -> ColorMatrix(
|
|
55
|
-
floatArrayOf(
|
|
56
|
-
1f, 0f, 0f, 0f, 18f,
|
|
57
|
-
0f, 1f, 0f, 0f, 6f,
|
|
58
|
-
0f, 0f, 1f, 0f, -12f,
|
|
59
|
-
0f, 0f, 0f, 1f, 0f
|
|
60
|
-
)
|
|
61
|
-
)
|
|
62
|
-
else -> null
|
|
63
93
|
}
|
|
64
94
|
}
|
|
@@ -3,7 +3,7 @@ package com.photovideoeditor.photo.render
|
|
|
3
3
|
import android.graphics.Color
|
|
4
4
|
import java.util.UUID
|
|
5
5
|
|
|
6
|
-
enum class LayerType { TEXT, STICKER, OVERLAY }
|
|
6
|
+
enum class LayerType { TEXT, STICKER, OVERLAY, DRAWING }
|
|
7
7
|
|
|
8
8
|
/**
|
|
9
9
|
* A single non-destructive overlay layer (text, sticker, or uploaded image
|
|
@@ -33,6 +33,10 @@ data class PhotoLayer(
|
|
|
33
33
|
// Overlay (uploaded image)
|
|
34
34
|
var overlayUri: String? = null,
|
|
35
35
|
var overlayAspectRatio: Float? = null,
|
|
36
|
+
// Drawing (points are normalized offsets from x/y)
|
|
37
|
+
var drawColor: Int = Color.RED,
|
|
38
|
+
var drawStrokeWidth: Float = 0.012f,
|
|
39
|
+
var drawPoints: List<Pair<Float, Float>> = emptyList(),
|
|
36
40
|
// Timing (video overlays only; ignored for photo layers). endMs <= 0 means "to end of video".
|
|
37
41
|
var startMs: Long = 0L,
|
|
38
42
|
var endMs: Long = 0L
|
|
@@ -3,6 +3,7 @@ package com.photovideoeditor.photo.render
|
|
|
3
3
|
import android.graphics.Bitmap
|
|
4
4
|
import android.graphics.Canvas
|
|
5
5
|
import android.graphics.Paint
|
|
6
|
+
import android.graphics.Path
|
|
6
7
|
import android.graphics.RectF
|
|
7
8
|
import android.text.TextPaint
|
|
8
9
|
|
|
@@ -41,6 +42,7 @@ object PhotoLayerRenderer {
|
|
|
41
42
|
LayerType.TEXT -> drawText(canvas, layer, geometry.shortSide, alpha)
|
|
42
43
|
LayerType.STICKER -> drawSticker(canvas, layer, geometry.shortSide, alpha, bitmapResolver)
|
|
43
44
|
LayerType.OVERLAY -> drawOverlay(canvas, layer, geometry.shortSide, alpha, bitmapResolver)
|
|
45
|
+
LayerType.DRAWING -> drawStroke(canvas, layer, result.width.toFloat(), result.height.toFloat(), geometry.shortSide, alpha)
|
|
44
46
|
}
|
|
45
47
|
canvas.restore()
|
|
46
48
|
}
|
|
@@ -75,7 +77,9 @@ object PhotoLayerRenderer {
|
|
|
75
77
|
if (uri != null) {
|
|
76
78
|
resolver(uri)?.let { bitmap ->
|
|
77
79
|
val paint = Paint(Paint.ANTI_ALIAS_FLAG).apply { this.alpha = alpha }
|
|
78
|
-
|
|
80
|
+
val aspect = layer.overlayAspectRatio?.takeIf { it > 0 } ?: (bitmap.width.toFloat() / bitmap.height)
|
|
81
|
+
val (width, height) = OverlayGeometry.stickerSize(shortSide, aspect)
|
|
82
|
+
canvas.drawBitmap(bitmap, null, RectF(-width / 2, -height / 2, width / 2, height / 2), paint)
|
|
79
83
|
return
|
|
80
84
|
}
|
|
81
85
|
}
|
|
@@ -105,4 +109,23 @@ object PhotoLayerRenderer {
|
|
|
105
109
|
val paint = Paint(Paint.ANTI_ALIAS_FLAG).apply { this.alpha = alpha }
|
|
106
110
|
canvas.drawBitmap(bitmap, null, RectF(-width / 2, -height / 2, width / 2, height / 2), paint)
|
|
107
111
|
}
|
|
112
|
+
|
|
113
|
+
private fun drawStroke(canvas: Canvas, layer: PhotoLayer, width: Float, height: Float, shortSide: Float, alpha: Int) {
|
|
114
|
+
if (layer.drawPoints.size < 2) return
|
|
115
|
+
val paint = Paint(Paint.ANTI_ALIAS_FLAG).apply {
|
|
116
|
+
color = layer.drawColor
|
|
117
|
+
this.alpha = alpha
|
|
118
|
+
style = Paint.Style.STROKE
|
|
119
|
+
strokeWidth = layer.drawStrokeWidth * shortSide
|
|
120
|
+
strokeCap = Paint.Cap.ROUND
|
|
121
|
+
strokeJoin = Paint.Join.ROUND
|
|
122
|
+
}
|
|
123
|
+
val path = Path()
|
|
124
|
+
layer.drawPoints.forEachIndexed { index, (dx, dy) ->
|
|
125
|
+
val px = dx * width
|
|
126
|
+
val py = dy * height
|
|
127
|
+
if (index == 0) path.moveTo(px, py) else path.lineTo(px, py)
|
|
128
|
+
}
|
|
129
|
+
canvas.drawPath(path, paint)
|
|
130
|
+
}
|
|
108
131
|
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
package com.photovideoeditor.photo.render
|
|
2
|
+
|
|
3
|
+
import android.graphics.Bitmap
|
|
4
|
+
import android.graphics.Canvas
|
|
5
|
+
import android.graphics.Matrix
|
|
6
|
+
import android.graphics.Paint
|
|
7
|
+
import kotlin.math.roundToInt
|
|
8
|
+
|
|
9
|
+
/** The same upright, auto-filled straighten and normalized crop pipeline for preview and export. */
|
|
10
|
+
object PhotoTransformRenderer {
|
|
11
|
+
fun transform(source: Bitmap, state: PhotoTransformState): Bitmap {
|
|
12
|
+
val oriented = if (state.rotationDegrees % 360 == 0) source else Bitmap.createBitmap(
|
|
13
|
+
source, 0, 0, source.width, source.height, Matrix().apply { postRotate(state.rotationDegrees.toFloat()) }, true)
|
|
14
|
+
if (state.straightenDegrees == 0f) return oriented
|
|
15
|
+
val output = Bitmap.createBitmap(oriented.width, oriented.height, Bitmap.Config.ARGB_8888)
|
|
16
|
+
val canvas = Canvas(output)
|
|
17
|
+
val fill = CropGeometry.fillScale(oriented.width.toFloat(), oriented.height.toFloat(), state.straightenDegrees)
|
|
18
|
+
canvas.translate(oriented.width / 2f, oriented.height / 2f)
|
|
19
|
+
canvas.rotate(state.straightenDegrees)
|
|
20
|
+
canvas.scale(fill, fill)
|
|
21
|
+
canvas.drawBitmap(oriented, -oriented.width / 2f, -oriented.height / 2f, Paint(Paint.ANTI_ALIAS_FLAG or Paint.FILTER_BITMAP_FLAG))
|
|
22
|
+
if (oriented !== source) oriented.recycle()
|
|
23
|
+
return output
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
fun crop(bitmap: Bitmap, state: PhotoTransformState): Bitmap {
|
|
27
|
+
val left = (state.cropLeft * bitmap.width).roundToInt().coerceIn(0, bitmap.width - 1)
|
|
28
|
+
val top = (state.cropTop * bitmap.height).roundToInt().coerceIn(0, bitmap.height - 1)
|
|
29
|
+
val right = (state.cropRight * bitmap.width).roundToInt().coerceIn(left + 1, bitmap.width)
|
|
30
|
+
val bottom = (state.cropBottom * bitmap.height).roundToInt().coerceIn(top + 1, bitmap.height)
|
|
31
|
+
return Bitmap.createBitmap(bitmap, left, top, right - left, bottom - top)
|
|
32
|
+
}
|
|
33
|
+
}
|
|
@@ -15,7 +15,12 @@ data class PhotoTransformState(
|
|
|
15
15
|
val cropBottom: Float = 1f,
|
|
16
16
|
val rotationDegrees: Int = 0,
|
|
17
17
|
val straightenDegrees: Float = 0f,
|
|
18
|
-
val aspectRatio: Float? = null
|
|
18
|
+
val aspectRatio: Float? = null,
|
|
19
|
+
val aspectPreset: String = "Free",
|
|
20
|
+
// Viewport metadata only: zoom/pan are already encoded in normalized crop coordinates for export.
|
|
21
|
+
val zoom: Float = 1f,
|
|
22
|
+
val panX: Float = 0f,
|
|
23
|
+
val panY: Float = 0f
|
|
19
24
|
) {
|
|
20
25
|
val isIdentity: Boolean
|
|
21
26
|
get() = cropLeft == 0f && cropTop == 0f && cropRight == 1f && cropBottom == 1f &&
|