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
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
import Foundation
|
|
2
2
|
|
|
3
|
-
///
|
|
4
|
-
///
|
|
5
|
-
///
|
|
6
|
-
///
|
|
3
|
+
/// Original, neutrally named preset filters (no third-party looks or names copied).
|
|
4
|
+
///
|
|
5
|
+
/// Every preset is expressed as the same four knobs the renderer already supports, so a preset can
|
|
6
|
+
/// never be a button that does nothing: 1) set saturation, 2) scale RGB, 3) add the per-channel
|
|
7
|
+
/// offset. `PhotoFilterPresets.kt` mirrors this table field-for-field — add presets to both or
|
|
8
|
+
/// neither, or the two platforms will render different looks for the same id.
|
|
7
9
|
struct PhotoFilterRecipe {
|
|
8
10
|
let scale: Double
|
|
9
11
|
let redOffset: Double
|
|
@@ -13,33 +15,61 @@ struct PhotoFilterRecipe {
|
|
|
13
15
|
}
|
|
14
16
|
|
|
15
17
|
enum PhotoFilterPresets {
|
|
16
|
-
|
|
18
|
+
/// Display order in the carousel. "Original" (no preset) is prepended by the UI, not listed here.
|
|
19
|
+
static let presetIDs = [
|
|
20
|
+
"vivid", "vividWarm", "vividCool",
|
|
21
|
+
"warmth", "cool",
|
|
22
|
+
"natural", "soft", "fade",
|
|
23
|
+
"mono", "noir", "silvertone",
|
|
24
|
+
"vintage", "sepia", "retro",
|
|
25
|
+
"dramatic", "dramaticWarm", "dramaticCool",
|
|
26
|
+
]
|
|
27
|
+
|
|
28
|
+
private static let recipes: [String: PhotoFilterRecipe] = [
|
|
29
|
+
"vivid": PhotoFilterRecipe(scale: 1.05, redOffset: 0, greenOffset: 0, blueOffset: 0, saturation: 1.45),
|
|
30
|
+
"vividWarm": PhotoFilterRecipe(scale: 1.05, redOffset: 12, greenOffset: 4, blueOffset: -10, saturation: 1.45),
|
|
31
|
+
"vividCool": PhotoFilterRecipe(scale: 1.05, redOffset: -8, greenOffset: 0, blueOffset: 14, saturation: 1.45),
|
|
32
|
+
"warmth": PhotoFilterRecipe(scale: 1, redOffset: 18, greenOffset: 6, blueOffset: -12, saturation: 1.05),
|
|
33
|
+
"cool": PhotoFilterRecipe(scale: 1, redOffset: -12, greenOffset: 0, blueOffset: 18, saturation: 1.05),
|
|
34
|
+
"natural": PhotoFilterRecipe(scale: 1.02, redOffset: 2, greenOffset: 2, blueOffset: 0, saturation: 1.15),
|
|
35
|
+
"soft": PhotoFilterRecipe(scale: 0.96, redOffset: 14, greenOffset: 12, blueOffset: 12, saturation: 0.9),
|
|
36
|
+
"fade": PhotoFilterRecipe(scale: 0.9, redOffset: 25, greenOffset: 25, blueOffset: 25, saturation: 1),
|
|
37
|
+
"mono": PhotoFilterRecipe(scale: 1, redOffset: 0, greenOffset: 0, blueOffset: 0, saturation: 0),
|
|
38
|
+
"noir": PhotoFilterRecipe(scale: 1.3, redOffset: -40, greenOffset: -40, blueOffset: -40, saturation: 0),
|
|
39
|
+
"silvertone": PhotoFilterRecipe(scale: 1.1, redOffset: -6, greenOffset: -2, blueOffset: 10, saturation: 0),
|
|
40
|
+
"vintage": PhotoFilterRecipe(scale: 0.95, redOffset: 24, greenOffset: 12, blueOffset: -6, saturation: 0.6),
|
|
41
|
+
"sepia": PhotoFilterRecipe(scale: 1, redOffset: 38, greenOffset: 20, blueOffset: -14, saturation: 0),
|
|
42
|
+
"retro": PhotoFilterRecipe(scale: 0.92, redOffset: 20, greenOffset: 6, blueOffset: 14, saturation: 0.75),
|
|
43
|
+
"dramatic": PhotoFilterRecipe(scale: 1.25, redOffset: -28, greenOffset: -28, blueOffset: -28, saturation: 1.2),
|
|
44
|
+
"dramaticWarm": PhotoFilterRecipe(scale: 1.25, redOffset: -14, greenOffset: -26, blueOffset: -38, saturation: 1.2),
|
|
45
|
+
"dramaticCool": PhotoFilterRecipe(scale: 1.25, redOffset: -38, greenOffset: -28, blueOffset: -12, saturation: 1.2),
|
|
46
|
+
// Retained so photos saved with an earlier preset id still render; not offered in the carousel.
|
|
47
|
+
"chrome": PhotoFilterRecipe(scale: 1.1, redOffset: 0, greenOffset: 0, blueOffset: 0, saturation: 1.4),
|
|
48
|
+
]
|
|
17
49
|
|
|
18
50
|
static func label(for id: String) -> String {
|
|
19
51
|
switch id {
|
|
52
|
+
case "vivid": return "Vivid"
|
|
53
|
+
case "vividWarm": return "Vivid Warm"
|
|
54
|
+
case "vividCool": return "Vivid Cool"
|
|
55
|
+
case "warmth": return "Warm"
|
|
56
|
+
case "cool": return "Cool"
|
|
57
|
+
case "natural": return "Natural"
|
|
58
|
+
case "soft": return "Soft"
|
|
59
|
+
case "fade": return "Fade"
|
|
20
60
|
case "mono": return "Mono"
|
|
21
61
|
case "noir": return "Noir"
|
|
22
|
-
case "
|
|
62
|
+
case "silvertone": return "Silvertone"
|
|
63
|
+
case "vintage": return "Vintage"
|
|
64
|
+
case "sepia": return "Sepia"
|
|
65
|
+
case "retro": return "Retro"
|
|
66
|
+
case "dramatic": return "Dramatic"
|
|
67
|
+
case "dramaticWarm": return "Dramatic Warm"
|
|
68
|
+
case "dramaticCool": return "Dramatic Cool"
|
|
23
69
|
case "chrome": return "Chrome"
|
|
24
|
-
case "warmth": return "Warmth"
|
|
25
70
|
default: return id
|
|
26
71
|
}
|
|
27
72
|
}
|
|
28
73
|
|
|
29
|
-
static func recipe(for id: String) -> PhotoFilterRecipe? {
|
|
30
|
-
switch id {
|
|
31
|
-
case "mono":
|
|
32
|
-
return PhotoFilterRecipe(scale: 1, redOffset: 0, greenOffset: 0, blueOffset: 0, saturation: 0)
|
|
33
|
-
case "noir":
|
|
34
|
-
return PhotoFilterRecipe(scale: 1.3, redOffset: -40, greenOffset: -40, blueOffset: -40, saturation: 0)
|
|
35
|
-
case "fade":
|
|
36
|
-
return PhotoFilterRecipe(scale: 0.9, redOffset: 25, greenOffset: 25, blueOffset: 25, saturation: 1)
|
|
37
|
-
case "chrome":
|
|
38
|
-
return PhotoFilterRecipe(scale: 1.1, redOffset: 0, greenOffset: 0, blueOffset: 0, saturation: 1.4)
|
|
39
|
-
case "warmth":
|
|
40
|
-
return PhotoFilterRecipe(scale: 1, redOffset: 18, greenOffset: 6, blueOffset: -12, saturation: 1)
|
|
41
|
-
default:
|
|
42
|
-
return nil
|
|
43
|
-
}
|
|
44
|
-
}
|
|
74
|
+
static func recipe(for id: String) -> PhotoFilterRecipe? { recipes[id] }
|
|
45
75
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import UIKit
|
|
2
2
|
|
|
3
3
|
enum LayerType {
|
|
4
|
-
case text, sticker, overlay
|
|
4
|
+
case text, sticker, overlay, drawing
|
|
5
5
|
}
|
|
6
6
|
|
|
7
7
|
/// A single non-destructive overlay layer (text, sticker, or a user-uploaded
|
|
@@ -11,7 +11,7 @@ enum LayerType {
|
|
|
11
11
|
///
|
|
12
12
|
/// `x`/`y` are the layer's center, normalized 0..1 against the image.
|
|
13
13
|
struct PhotoLayer {
|
|
14
|
-
|
|
14
|
+
var id: String
|
|
15
15
|
var type: LayerType
|
|
16
16
|
var x: CGFloat = 0.5
|
|
17
17
|
var y: CGFloat = 0.5
|
|
@@ -31,6 +31,10 @@ struct PhotoLayer {
|
|
|
31
31
|
// Overlay (user-uploaded image, placed as a fully generic movable/resizable/rotatable layer)
|
|
32
32
|
var overlayUri: String?
|
|
33
33
|
var overlayAspectRatio: CGFloat?
|
|
34
|
+
// Drawing (points are normalized offsets from x/y)
|
|
35
|
+
var drawColor: UIColor = .red
|
|
36
|
+
var drawStrokeWidth: CGFloat = 0.012
|
|
37
|
+
var drawPoints: [(CGFloat, CGFloat)] = []
|
|
34
38
|
// Timing (video overlays only; ignored for photo layers). endMs <= 0 means "to end of video".
|
|
35
39
|
var startMs: Int64 = 0
|
|
36
40
|
var endMs: Int64 = 0
|
|
@@ -40,6 +44,14 @@ struct PhotoLayer {
|
|
|
40
44
|
self.type = type
|
|
41
45
|
}
|
|
42
46
|
|
|
47
|
+
func duplicated() -> PhotoLayer {
|
|
48
|
+
var copy = self
|
|
49
|
+
copy.id = UUID().uuidString
|
|
50
|
+
copy.x = min(max(x + 0.04, 0), 1)
|
|
51
|
+
copy.y = min(max(y + 0.04, 0), 1)
|
|
52
|
+
return copy
|
|
53
|
+
}
|
|
54
|
+
|
|
43
55
|
/// True if this layer should be visible at `positionMs` against a video of `durationMs`. Always true for photo layers.
|
|
44
56
|
func effectiveEndMs(durationMs: Int64) -> Int64 {
|
|
45
57
|
(endMs > 0 && endMs <= durationMs) ? endMs : durationMs
|
|
@@ -29,6 +29,7 @@ enum PhotoLayerRenderer {
|
|
|
29
29
|
case .text: drawText(layer, shortSide: geometry.shortSide)
|
|
30
30
|
case .sticker: drawSticker(layer, shortSide: geometry.shortSide, resolver: imageResolver)
|
|
31
31
|
case .overlay: drawOverlay(layer, canvasWidth: base.size.width, resolver: imageResolver)
|
|
32
|
+
case .drawing: drawStroke(ctx, layer, width: base.size.width, height: base.size.height, shortSide: geometry.shortSide)
|
|
32
33
|
}
|
|
33
34
|
ctx.restoreGState()
|
|
34
35
|
}
|
|
@@ -53,8 +54,9 @@ enum PhotoLayerRenderer {
|
|
|
53
54
|
|
|
54
55
|
private static func drawSticker(_ layer: PhotoLayer, shortSide: CGFloat, resolver: (String) -> UIImage?) {
|
|
55
56
|
let size = shortSide * 0.18
|
|
56
|
-
let rect = CGRect(x: -size / 2, y: -size / 2, width: size, height: size)
|
|
57
57
|
if let uri = layer.stickerUri, let image = resolver(uri) {
|
|
58
|
+
let aspect = layer.overlayAspectRatio.flatMap { $0 > 0 ? $0 : nil } ?? (image.size.width / max(image.size.height, 1))
|
|
59
|
+
let rect = CGRect(x: -size / 2, y: -size / (2 * aspect), width: size, height: size / aspect)
|
|
58
60
|
image.draw(in: rect, blendMode: .normal, alpha: layer.opacity)
|
|
59
61
|
return
|
|
60
62
|
}
|
|
@@ -80,6 +82,19 @@ enum PhotoLayerRenderer {
|
|
|
80
82
|
image.draw(in: rect, blendMode: .normal, alpha: layer.opacity)
|
|
81
83
|
}
|
|
82
84
|
|
|
85
|
+
private static func drawStroke(_ ctx: CGContext, _ layer: PhotoLayer, width: CGFloat, height: CGFloat, shortSide: CGFloat) {
|
|
86
|
+
guard layer.drawPoints.count >= 2 else { return }
|
|
87
|
+
ctx.setAlpha(layer.opacity)
|
|
88
|
+
ctx.setStrokeColor(layer.drawColor.cgColor)
|
|
89
|
+
ctx.setLineWidth(layer.drawStrokeWidth * shortSide)
|
|
90
|
+
ctx.setLineCap(.round)
|
|
91
|
+
ctx.setLineJoin(.round)
|
|
92
|
+
let points = layer.drawPoints.map { CGPoint(x: $0.0 * width, y: $0.1 * height) }
|
|
93
|
+
ctx.move(to: points[0])
|
|
94
|
+
points.dropFirst().forEach { ctx.addLine(to: $0) }
|
|
95
|
+
ctx.strokePath()
|
|
96
|
+
}
|
|
97
|
+
|
|
83
98
|
}
|
|
84
99
|
|
|
85
100
|
private extension NSAttributedString {
|
|
@@ -12,6 +12,11 @@ struct PhotoTransformState: Equatable {
|
|
|
12
12
|
var rotationDegrees: Int = 0
|
|
13
13
|
var straightenDegrees: CGFloat = 0
|
|
14
14
|
var aspectRatio: CGFloat?
|
|
15
|
+
var aspectPreset = "Free"
|
|
16
|
+
// Zoom/pan are viewport metadata; normalized crop coordinates already encode them for export.
|
|
17
|
+
var zoom: CGFloat = 1
|
|
18
|
+
var panX: CGFloat = 0
|
|
19
|
+
var panY: CGFloat = 0
|
|
15
20
|
|
|
16
21
|
var isIdentity: Bool {
|
|
17
22
|
cropLeft == 0 && cropTop == 0 && cropRight == 1 && cropBottom == 1 &&
|
|
@@ -1,161 +1,125 @@
|
|
|
1
1
|
import UIKit
|
|
2
2
|
|
|
3
|
-
///
|
|
4
|
-
/// on-screen rect of the preview image, in this view's own coordinate space)
|
|
5
|
-
/// and reports the crop edges back normalized to that rect via `onCropChanged`.
|
|
3
|
+
/// Handle gestures resize the box; interior gestures pan/pinch the media, keeping the box fixed.
|
|
6
4
|
final class CropOverlayView: UIView {
|
|
7
|
-
var onCropChanged: ((
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
private var
|
|
5
|
+
var onCropChanged: ((CGFloat, CGFloat, CGFloat, CGFloat) -> Void)?
|
|
6
|
+
var onMediaBoundsChanged: ((CGRect) -> Void)?
|
|
7
|
+
var onViewportChanged: ((CGFloat, CGFloat, CGFloat) -> Void)?
|
|
8
|
+
private var imageBounds = CGRect.zero, fittedBounds = CGRect.zero, cropRect = CGRect.zero
|
|
11
9
|
private var aspectRatio: CGFloat?
|
|
12
|
-
|
|
13
|
-
private
|
|
14
|
-
private
|
|
15
|
-
private
|
|
16
|
-
private let minSize: CGFloat = 60
|
|
17
|
-
|
|
18
|
-
private enum Handle { case move, topLeft, topRight, bottomLeft, bottomRight }
|
|
19
|
-
private var activeHandle: Handle?
|
|
20
|
-
private var lastPoint: CGPoint = .zero
|
|
10
|
+
private var activeHandle: CropGeometry.Handle?
|
|
11
|
+
private var primary: UITouch?, secondary: UITouch?
|
|
12
|
+
private var lastPoint = CGPoint.zero
|
|
13
|
+
private var lastDistance: CGFloat = 0
|
|
21
14
|
|
|
22
15
|
override init(frame: CGRect) {
|
|
23
|
-
super.init(frame: frame)
|
|
24
|
-
backgroundColor = .clear
|
|
25
|
-
dimLayer.fillRule = .evenOdd
|
|
26
|
-
dimLayer.fillColor = UIColor.black.withAlphaComponent(0.6).cgColor
|
|
27
|
-
layer.addSublayer(dimLayer)
|
|
28
|
-
borderLayer.strokeColor = UIColor.white.cgColor
|
|
29
|
-
borderLayer.fillColor = UIColor.clear.cgColor
|
|
30
|
-
borderLayer.lineWidth = 2
|
|
31
|
-
layer.addSublayer(borderLayer)
|
|
16
|
+
super.init(frame: frame); backgroundColor = .clear; isOpaque = false; isMultipleTouchEnabled = true
|
|
32
17
|
}
|
|
33
|
-
|
|
34
18
|
required init?(coder: NSCoder) { nil }
|
|
35
19
|
|
|
36
|
-
|
|
20
|
+
func restore(bounds: CGRect, state: PhotoTransformState) {
|
|
21
|
+
guard bounds.width > 0, bounds.height > 0 else { return }
|
|
22
|
+
fittedBounds = bounds
|
|
23
|
+
let zoom = max(1, min(8, state.zoom))
|
|
24
|
+
let cx = bounds.midX + state.panX*bounds.width, cy = bounds.midY + state.panY*bounds.height
|
|
25
|
+
imageBounds = CGRect(x: cx-bounds.width*zoom/2, y: cy-bounds.height*zoom/2, width: bounds.width*zoom, height: bounds.height*zoom)
|
|
26
|
+
aspectRatio = state.aspectRatio
|
|
27
|
+
setCrop(left: state.cropLeft, top: state.cropTop, right: state.cropRight, bottom: state.cropBottom)
|
|
28
|
+
onMediaBoundsChanged?(imageBounds)
|
|
29
|
+
}
|
|
37
30
|
func setImageBounds(_ bounds: CGRect, resetCrop: Bool) {
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
31
|
+
let old = normalized()
|
|
32
|
+
imageBounds = bounds; fittedBounds = bounds
|
|
33
|
+
if resetCrop || cropRect.isEmpty { cropRect = bounds }
|
|
34
|
+
else { setCrop(left: old.minX, top: old.minY, right: old.maxX, bottom: old.maxY) }
|
|
35
|
+
setNeedsDisplay()
|
|
36
|
+
}
|
|
37
|
+
func setCrop(left: CGFloat, top: CGFloat, right: CGFloat, bottom: CGFloat) {
|
|
38
|
+
cropRect = CGRect(x: imageBounds.minX+left*imageBounds.width, y: imageBounds.minY+top*imageBounds.height,
|
|
39
|
+
width: (right-left)*imageBounds.width, height: (bottom-top)*imageBounds.height)
|
|
40
|
+
setNeedsDisplay()
|
|
44
41
|
}
|
|
45
|
-
|
|
46
42
|
func setAspectRatio(_ ratio: CGFloat?) {
|
|
47
43
|
aspectRatio = ratio
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
var width = cropRect.width
|
|
51
|
-
var height = width / ratio
|
|
52
|
-
if height > imageBounds.height {
|
|
53
|
-
height = cropRect.height
|
|
54
|
-
width = height * ratio
|
|
55
|
-
}
|
|
56
|
-
cropRect = CGRect(x: center.x - width / 2, y: center.y - height / 2, width: width, height: height)
|
|
57
|
-
clampToImageBounds()
|
|
58
|
-
setNeedsLayout()
|
|
59
|
-
}
|
|
60
|
-
reportCrop()
|
|
44
|
+
cropRect = CropGeometry.fit(imageBounds.intersection(bounds), ratio: ratio)
|
|
45
|
+
reportCrop(); setNeedsDisplay()
|
|
61
46
|
}
|
|
62
47
|
|
|
63
|
-
override func
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
48
|
+
override func draw(_ rect: CGRect) {
|
|
49
|
+
guard !cropRect.isEmpty, let context = UIGraphicsGetCurrentContext() else { return }
|
|
50
|
+
let shade = UIBezierPath(rect: bounds); shade.append(UIBezierPath(rect: cropRect)); shade.usesEvenOddFillRule = true
|
|
51
|
+
UIColor.black.withAlphaComponent(0.65).setFill(); shade.fill()
|
|
52
|
+
context.setStrokeColor(UIColor.white.cgColor); context.setLineWidth(1); context.stroke(cropRect)
|
|
53
|
+
context.setStrokeColor(UIColor.white.withAlphaComponent(0.33).cgColor); context.setLineWidth(0.5)
|
|
54
|
+
for i in 1...2 {
|
|
55
|
+
let x = cropRect.minX+cropRect.width*CGFloat(i)/3, y = cropRect.minY+cropRect.height*CGFloat(i)/3
|
|
56
|
+
context.move(to: CGPoint(x:x,y:cropRect.minY)); context.addLine(to:CGPoint(x:x,y:cropRect.maxY))
|
|
57
|
+
context.move(to: CGPoint(x:cropRect.minX,y:y)); context.addLine(to:CGPoint(x:cropRect.maxX,y:y))
|
|
58
|
+
}
|
|
59
|
+
context.strokePath(); context.setStrokeColor(UIColor.white.cgColor); context.setLineWidth(3); context.setLineCap(.round)
|
|
60
|
+
for x in [cropRect.minX,cropRect.maxX] {
|
|
61
|
+
for y in [cropRect.minY,cropRect.maxY] {
|
|
62
|
+
context.move(to: CGPoint(x:x+(x == cropRect.minX ? 14 : -14),y:y)); context.addLine(to:CGPoint(x:x,y:y))
|
|
63
|
+
context.addLine(to: CGPoint(x:x,y:y+(y == cropRect.minY ? 14 : -14)))
|
|
64
|
+
}
|
|
80
65
|
}
|
|
81
|
-
|
|
66
|
+
for y in [cropRect.minY,cropRect.maxY] { context.move(to:CGPoint(x:cropRect.midX-8,y:y)); context.addLine(to:CGPoint(x:cropRect.midX+8,y:y)) }
|
|
67
|
+
for x in [cropRect.minX,cropRect.maxX] { context.move(to:CGPoint(x:x,y:cropRect.midY-8)); context.addLine(to:CGPoint(x:x,y:cropRect.midY+8)) }
|
|
68
|
+
context.strokePath()
|
|
82
69
|
}
|
|
83
70
|
|
|
84
71
|
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
72
|
+
for touch in touches { if primary == nil { primary = touch } else if secondary == nil { secondary = touch } }
|
|
73
|
+
guard let primary else { return }
|
|
74
|
+
if secondary != nil { activeHandle = nil } else { activeHandle = hitHandle(primary.location(in:self)) }
|
|
75
|
+
let metrics = touchMetrics(); lastPoint = metrics.0; lastDistance = metrics.1
|
|
88
76
|
}
|
|
89
|
-
|
|
90
77
|
override func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent?) {
|
|
91
|
-
guard
|
|
92
|
-
let dx = point.x -
|
|
93
|
-
let
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
reportCrop()
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent?) { activeHandle = nil }
|
|
101
|
-
override func touchesCancelled(_ touches: Set<UITouch>, with event: UIEvent?) { activeHandle = nil }
|
|
102
|
-
|
|
103
|
-
private func hitTest(_ point: CGPoint) -> Handle? {
|
|
104
|
-
func near(_ target: CGPoint) -> Bool {
|
|
105
|
-
abs(point.x - target.x) < handleRadius && abs(point.y - target.y) < handleRadius
|
|
78
|
+
guard primary != nil, imageBounds.width > 0 else { return }
|
|
79
|
+
let (point,distance) = touchMetrics(), dx = point.x-lastPoint.x, dy = point.y-lastPoint.y
|
|
80
|
+
if let activeHandle {
|
|
81
|
+
cropRect = CropGeometry.resize(cropRect, bounds: imageBounds.intersection(bounds), handle: activeHandle, dx: dx, dy: dy, ratio: aspectRatio, minimum: 48)
|
|
82
|
+
} else {
|
|
83
|
+
moveMedia(dx:dx,dy:dy,factor:lastDistance > 0 && distance > 0 ? distance/lastDistance : 1,focus:point)
|
|
106
84
|
}
|
|
107
|
-
|
|
108
|
-
if near(CGPoint(x: cropRect.maxX, y: cropRect.minY)) { return .topRight }
|
|
109
|
-
if near(CGPoint(x: cropRect.minX, y: cropRect.maxY)) { return .bottomLeft }
|
|
110
|
-
if near(CGPoint(x: cropRect.maxX, y: cropRect.maxY)) { return .bottomRight }
|
|
111
|
-
if cropRect.contains(point) { return .move }
|
|
112
|
-
return nil
|
|
85
|
+
lastPoint = point; lastDistance = distance; reportCrop(); setNeedsDisplay()
|
|
113
86
|
}
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
rect = rect.offsetBy(dx: dx, dy: dy)
|
|
120
|
-
case .topLeft:
|
|
121
|
-
rect = CGRect(x: rect.minX + dx, y: rect.minY + dy, width: rect.width - dx, height: rect.height - dy)
|
|
122
|
-
case .topRight:
|
|
123
|
-
rect = CGRect(x: rect.minX, y: rect.minY + dy, width: rect.width + dx, height: rect.height - dy)
|
|
124
|
-
case .bottomLeft:
|
|
125
|
-
rect = CGRect(x: rect.minX + dx, y: rect.minY, width: rect.width - dx, height: rect.height + dy)
|
|
126
|
-
case .bottomRight:
|
|
127
|
-
rect = CGRect(x: rect.minX, y: rect.minY, width: rect.width + dx, height: rect.height + dy)
|
|
128
|
-
}
|
|
129
|
-
guard rect.width >= minSize, rect.height >= minSize else { return }
|
|
130
|
-
if let ratio = aspectRatio {
|
|
131
|
-
switch handle {
|
|
132
|
-
case .topLeft, .bottomLeft, .bottomRight:
|
|
133
|
-
rect.size.height = rect.width / ratio
|
|
134
|
-
case .topRight:
|
|
135
|
-
let newHeight = rect.width / ratio
|
|
136
|
-
rect.origin.y = cropRect.maxY - newHeight
|
|
137
|
-
rect.size.height = newHeight
|
|
138
|
-
case .move:
|
|
139
|
-
break
|
|
140
|
-
}
|
|
141
|
-
}
|
|
142
|
-
cropRect = rect
|
|
143
|
-
clampToImageBounds()
|
|
87
|
+
override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent?) {
|
|
88
|
+
if let secondary, touches.contains(secondary) { self.secondary = nil }
|
|
89
|
+
if let primary, touches.contains(primary) { self.primary = secondary; secondary = nil }
|
|
90
|
+
let metrics = touchMetrics(); lastPoint = metrics.0; lastDistance = metrics.1
|
|
91
|
+
if primary == nil { activeHandle = nil }
|
|
144
92
|
}
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
93
|
+
override func touchesCancelled(_ touches: Set<UITouch>, with event: UIEvent?) { primary = nil; secondary = nil; activeHandle = nil }
|
|
94
|
+
|
|
95
|
+
private func moveMedia(dx:CGFloat,dy:CGFloat,factor:CGFloat,focus:CGPoint) {
|
|
96
|
+
let minimum = max(1,max(cropRect.width/fittedBounds.width,cropRect.height/fittedBounds.height))
|
|
97
|
+
let zoom = imageBounds.width/fittedBounds.width
|
|
98
|
+
let scale = max(minimum,min(max(8,minimum),zoom*factor))/zoom
|
|
99
|
+
let w = imageBounds.width*scale, h = imageBounds.height*scale
|
|
100
|
+
let x = max(cropRect.maxX-w,min(cropRect.minX,focus.x+(imageBounds.minX-focus.x)*scale+dx))
|
|
101
|
+
let y = max(cropRect.maxY-h,min(cropRect.minY,focus.y+(imageBounds.minY-focus.y)*scale+dy))
|
|
102
|
+
imageBounds = CGRect(x:x,y:y,width:w,height:h)
|
|
103
|
+
onMediaBoundsChanged?(imageBounds)
|
|
104
|
+
onViewportChanged?(w/fittedBounds.width,(imageBounds.midX-fittedBounds.midX)/fittedBounds.width,(imageBounds.midY-fittedBounds.midY)/fittedBounds.height)
|
|
151
105
|
}
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
guard
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
let
|
|
159
|
-
|
|
106
|
+
private func touchMetrics() -> (CGPoint,CGFloat) {
|
|
107
|
+
guard let a = primary?.location(in:self) else { return (.zero,0) }
|
|
108
|
+
guard let b = secondary?.location(in:self) else { return (a,0) }
|
|
109
|
+
return (CGPoint(x:(a.x+b.x)/2,y:(a.y+b.y)/2),hypot(a.x-b.x,a.y-b.y))
|
|
110
|
+
}
|
|
111
|
+
private func hitHandle(_ p:CGPoint) -> CropGeometry.Handle? {
|
|
112
|
+
let points: [(CGPoint,CropGeometry.Handle)] = [
|
|
113
|
+
(CGPoint(x:cropRect.minX,y:cropRect.minY),.topLeft),(CGPoint(x:cropRect.maxX,y:cropRect.minY),.topRight),
|
|
114
|
+
(CGPoint(x:cropRect.minX,y:cropRect.maxY),.bottomLeft),(CGPoint(x:cropRect.maxX,y:cropRect.maxY),.bottomRight),
|
|
115
|
+
(CGPoint(x:cropRect.midX,y:cropRect.minY),.top),(CGPoint(x:cropRect.maxX,y:cropRect.midY),.right),
|
|
116
|
+
(CGPoint(x:cropRect.midX,y:cropRect.maxY),.bottom),(CGPoint(x:cropRect.minX,y:cropRect.midY),.left)]
|
|
117
|
+
guard let nearest = points.min(by: { hypot(p.x-$0.0.x,p.y-$0.0.y) < hypot(p.x-$1.0.x,p.y-$1.0.y) }), hypot(p.x-nearest.0.x,p.y-nearest.0.y) <= 24 else { return nil }
|
|
118
|
+
return nearest.1
|
|
119
|
+
}
|
|
120
|
+
private func normalized() -> CGRect {
|
|
121
|
+
guard imageBounds.width > 0,imageBounds.height > 0 else { return CGRect(x:0,y:0,width:1,height:1) }
|
|
122
|
+
return CGRect(x:(cropRect.minX-imageBounds.minX)/imageBounds.width,y:(cropRect.minY-imageBounds.minY)/imageBounds.height,width:cropRect.width/imageBounds.width,height:cropRect.height/imageBounds.height)
|
|
160
123
|
}
|
|
124
|
+
private func reportCrop() { let r = normalized(); onCropChanged?(max(0,r.minX),max(0,r.minY),min(1,r.maxX),min(1,r.maxY)) }
|
|
161
125
|
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import UIKit
|
|
2
|
+
|
|
3
|
+
struct DrawStroke {
|
|
4
|
+
var points: [CGPoint]
|
|
5
|
+
let color: UIColor
|
|
6
|
+
let widthPx: CGFloat
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
/** Captures freehand strokes over the visible image while Draw mode is active. */
|
|
10
|
+
final class DrawOverlayView: UIView {
|
|
11
|
+
private var imageBounds: CGRect = .zero
|
|
12
|
+
var strokeColor: UIColor = .red
|
|
13
|
+
var strokeWidthPx: CGFloat = 12
|
|
14
|
+
private var strokes: [DrawStroke] = []
|
|
15
|
+
private var currentIndex: Int?
|
|
16
|
+
|
|
17
|
+
override init(frame: CGRect) { super.init(frame: frame); backgroundColor = .clear; isOpaque = false }
|
|
18
|
+
required init?(coder: NSCoder) { nil }
|
|
19
|
+
func setImageBounds(_ bounds: CGRect) { imageBounds = bounds }
|
|
20
|
+
func clearStrokes() { strokes.removeAll(); currentIndex = nil; setNeedsDisplay() }
|
|
21
|
+
func undoLastStroke() { if !strokes.isEmpty { strokes.removeLast() }; setNeedsDisplay() }
|
|
22
|
+
var hasStrokes: Bool { !strokes.isEmpty }
|
|
23
|
+
func normalizedStrokes() -> [(points: [(CGFloat, CGFloat)], stroke: DrawStroke)] {
|
|
24
|
+
guard imageBounds.width > 0, imageBounds.height > 0 else { return [] }
|
|
25
|
+
return strokes.map { stroke in
|
|
26
|
+
(stroke.points.map { (($0.x - imageBounds.minX) / imageBounds.width, ($0.y - imageBounds.minY) / imageBounds.height) }, stroke)
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
override func draw(_ rect: CGRect) {
|
|
31
|
+
guard let ctx = UIGraphicsGetCurrentContext() else { return }
|
|
32
|
+
ctx.setLineCap(.round); ctx.setLineJoin(.round)
|
|
33
|
+
for stroke in strokes where stroke.points.count >= 2 {
|
|
34
|
+
ctx.setStrokeColor(stroke.color.cgColor); ctx.setLineWidth(stroke.widthPx)
|
|
35
|
+
ctx.move(to: stroke.points[0]); stroke.points.dropFirst().forEach { ctx.addLine(to: $0) }; ctx.strokePath()
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
|
|
39
|
+
guard let point = touches.first?.location(in: self), imageBounds.contains(point) else { return }
|
|
40
|
+
strokes.append(DrawStroke(points: [point], color: strokeColor, widthPx: strokeWidthPx)); currentIndex = strokes.count - 1; setNeedsDisplay()
|
|
41
|
+
}
|
|
42
|
+
override func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent?) {
|
|
43
|
+
guard let index = currentIndex, let point = touches.first?.location(in: self) else { return }
|
|
44
|
+
strokes[index].points.append(point); setNeedsDisplay()
|
|
45
|
+
}
|
|
46
|
+
override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent?) { currentIndex = nil }
|
|
47
|
+
override func touchesCancelled(_ touches: Set<UITouch>, with event: UIEvent?) { currentIndex = nil }
|
|
48
|
+
}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import UIKit
|
|
2
|
+
|
|
3
|
+
/// Loads filter-carousel previews off the main thread, mirroring `FilterThumbnailLoader.kt`.
|
|
4
|
+
///
|
|
5
|
+
/// The carousel offers a preset per look, so rendering each one from the full-resolution photo would
|
|
6
|
+
/// stall the panel every time it opens. Each preset is instead applied to one small square scaled
|
|
7
|
+
/// from the source, and the result is cached by preset id for the life of the editor.
|
|
8
|
+
final class FilterThumbnailLoader {
|
|
9
|
+
private let queue = DispatchQueue(label: "filter-thumbnail-loader", qos: .userInitiated)
|
|
10
|
+
private let cache = NSCache<NSString, UIImage>()
|
|
11
|
+
private var scaledSource: UIImage?
|
|
12
|
+
private var scaledSourceSize: CGFloat = 0
|
|
13
|
+
|
|
14
|
+
func load(source: UIImage?, presetID: String, size: CGFloat, deliver: @escaping (UIImage?) -> Void) {
|
|
15
|
+
guard let source else {
|
|
16
|
+
deliver(nil)
|
|
17
|
+
return
|
|
18
|
+
}
|
|
19
|
+
if let cached = cache.object(forKey: presetID as NSString) {
|
|
20
|
+
deliver(cached)
|
|
21
|
+
return
|
|
22
|
+
}
|
|
23
|
+
// Scale once and reuse for every preset rather than per thumbnail.
|
|
24
|
+
let square = squareSource(from: source, size: size)
|
|
25
|
+
queue.async { [weak self] in
|
|
26
|
+
let rendered = PhotoAdjustmentRenderer.apply(
|
|
27
|
+
square,
|
|
28
|
+
adjustments: PhotoAdjustments(filterPreset: presetID, filterStrength: 100)
|
|
29
|
+
)
|
|
30
|
+
self?.cache.setObject(rendered, forKey: presetID as NSString)
|
|
31
|
+
DispatchQueue.main.async { deliver(rendered) }
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
private func squareSource(from source: UIImage, size: CGFloat) -> UIImage {
|
|
36
|
+
if let scaledSource, scaledSourceSize == size { return scaledSource }
|
|
37
|
+
let scale = UIScreen.main.scale
|
|
38
|
+
let pixelSize = CGSize(width: size * scale, height: size * scale)
|
|
39
|
+
let renderer = PhotoEditSession.pixelRenderer(size: pixelSize)
|
|
40
|
+
// Aspect-fill into a square so each thumbnail frames the photo the same way.
|
|
41
|
+
let fitted = renderer.image { _ in
|
|
42
|
+
let sourceSize = source.size
|
|
43
|
+
let ratio = max(pixelSize.width / max(sourceSize.width, 1), pixelSize.height / max(sourceSize.height, 1))
|
|
44
|
+
let drawSize = CGSize(width: sourceSize.width * ratio, height: sourceSize.height * ratio)
|
|
45
|
+
source.draw(in: CGRect(
|
|
46
|
+
x: (pixelSize.width - drawSize.width) / 2,
|
|
47
|
+
y: (pixelSize.height - drawSize.height) / 2,
|
|
48
|
+
width: drawSize.width,
|
|
49
|
+
height: drawSize.height
|
|
50
|
+
))
|
|
51
|
+
}
|
|
52
|
+
scaledSource = fitted
|
|
53
|
+
scaledSourceSize = size
|
|
54
|
+
return fitted
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
func invalidate() {
|
|
58
|
+
cache.removeAllObjects()
|
|
59
|
+
scaledSource = nil
|
|
60
|
+
scaledSourceSize = 0
|
|
61
|
+
}
|
|
62
|
+
}
|