react-native-photo-video-editor 0.1.0 → 0.1.1
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 +9 -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 +188 -108
- package/android/src/main/java/com/photovideoeditor/photo/render/PhotoLayer.kt +5 -1
- package/android/src/main/java/com/photovideoeditor/photo/render/PhotoLayerRenderer.kt +21 -0
- package/android/src/main/java/com/photovideoeditor/photo/ui/CropOverlayView.kt +34 -2
- package/android/src/main/java/com/photovideoeditor/photo/ui/DrawOverlayView.kt +66 -0
- package/android/src/main/java/com/photovideoeditor/photo/ui/LayerOverlayView.kt +21 -1
- package/android/src/main/java/com/photovideoeditor/stickers/OnlineStickerSheet.kt +135 -104
- 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/photo-editor.md +5 -6
- package/ios/Editor/PhotoVideoEditorViewController.swift +166 -77
- package/ios/Photo/Rendering/PhotoLayer.swift +5 -1
- package/ios/Photo/Rendering/PhotoLayerRenderer.swift +14 -0
- package/ios/Photo/UI/CropOverlayView.swift +38 -1
- package/ios/Photo/UI/DrawOverlayView.swift +48 -0
- package/ios/Photo/UI/LayerOverlayView.swift +9 -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/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
|
@@ -20,7 +20,9 @@ final class PhotoVideoEditorViewController: UIViewController {
|
|
|
20
20
|
|
|
21
21
|
private var photoSession: PhotoEditSession?
|
|
22
22
|
private var cropMode = false
|
|
23
|
+
private var cropEntryState: PhotoTransformState?
|
|
23
24
|
private var filtersMode = false
|
|
25
|
+
private var drawMode = false
|
|
24
26
|
private var stickersMode = false
|
|
25
27
|
private var pendingPickPurpose: ImagePickPurpose?
|
|
26
28
|
private var activeAdjustmentKey = "brightness"
|
|
@@ -30,11 +32,13 @@ final class PhotoVideoEditorViewController: UIViewController {
|
|
|
30
32
|
private var photoImageView: ZoomableImageView?
|
|
31
33
|
private var cropOverlay: CropOverlayView?
|
|
32
34
|
private var layerOverlay: LayerOverlayView?
|
|
35
|
+
private var drawOverlay: DrawOverlayView?
|
|
33
36
|
private var straightenSlider: UISlider?
|
|
34
37
|
private var cropSubBar: UIView?
|
|
35
38
|
private var adjustmentSlider: UISlider?
|
|
36
39
|
private var filtersSubBar: UIView?
|
|
37
40
|
private var stickersSubBar: UIView?
|
|
41
|
+
private var drawSubBar: UIView?
|
|
38
42
|
private var layerToolBar: UIView?
|
|
39
43
|
private var layerPropertySlider: UISlider?
|
|
40
44
|
private var layerColorSwatchRow: UIView?
|
|
@@ -186,30 +190,32 @@ final class PhotoVideoEditorViewController: UIViewController {
|
|
|
186
190
|
layers.bottomAnchor.constraint(equalTo: preview.bottomAnchor),
|
|
187
191
|
])
|
|
188
192
|
|
|
189
|
-
let
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
metadataBadge.layer.cornerRadius = DesignTokens.radiusLg
|
|
195
|
-
metadataBadge.clipsToBounds = true
|
|
196
|
-
preview.addSubview(metadataBadge)
|
|
197
|
-
metadataBadge.translatesAutoresizingMaskIntoConstraints = false
|
|
193
|
+
let drawing = DrawOverlayView()
|
|
194
|
+
drawing.isHidden = true
|
|
195
|
+
drawOverlay = drawing
|
|
196
|
+
preview.addSubview(drawing)
|
|
197
|
+
drawing.translatesAutoresizingMaskIntoConstraints = false
|
|
198
198
|
NSLayoutConstraint.activate([
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
199
|
+
drawing.leadingAnchor.constraint(equalTo: preview.leadingAnchor),
|
|
200
|
+
drawing.trailingAnchor.constraint(equalTo: preview.trailingAnchor),
|
|
201
|
+
drawing.topAnchor.constraint(equalTo: preview.topAnchor),
|
|
202
|
+
drawing.bottomAnchor.constraint(equalTo: preview.bottomAnchor),
|
|
202
203
|
])
|
|
203
204
|
|
|
204
205
|
imageView.onBoundsChanged = { [weak self] bounds in
|
|
205
206
|
guard let self else { return }
|
|
206
207
|
if self.cropMode { overlay.setImageBounds(bounds, resetCrop: false) }
|
|
207
208
|
layers.setImageBounds(bounds)
|
|
209
|
+
drawing.setImageBounds(bounds)
|
|
208
210
|
}
|
|
209
211
|
overlay.onCropChanged = { left, top, right, bottom in
|
|
210
212
|
session.update { $0.setCrop(left: left, top: top, right: right, bottom: bottom) }
|
|
211
213
|
}
|
|
212
214
|
layers.onLayerTapped = { [weak self] id in self?.selectLayer(id) }
|
|
215
|
+
layers.onLayerDoubleTapped = { [weak self] id in
|
|
216
|
+
guard let self, let layer = session.layerStack.layers.first(where: { $0.id == id && $0.type == .text }) else { return }
|
|
217
|
+
self.showTextInputDialog(session: session, editingLayer: layer)
|
|
218
|
+
}
|
|
213
219
|
layers.onLayerTransformChanged = { [weak self] id, x, y, scale, rotation in
|
|
214
220
|
guard let self, let session = self.photoSession else { return }
|
|
215
221
|
if self.dragStartSnapshot == nil { self.dragStartSnapshot = session.layerStack.layers }
|
|
@@ -251,6 +257,10 @@ final class PhotoVideoEditorViewController: UIViewController {
|
|
|
251
257
|
stickersBar.isHidden = true
|
|
252
258
|
stickersSubBar = stickersBar
|
|
253
259
|
|
|
260
|
+
let drawBar = makeDrawSubBar(textColor: textColor, primaryColor: primaryColor, toolbarColor: toolbarColor)
|
|
261
|
+
drawBar.isHidden = true
|
|
262
|
+
drawSubBar = drawBar
|
|
263
|
+
|
|
254
264
|
let layerSlider = UISlider()
|
|
255
265
|
layerSlider.isHidden = true
|
|
256
266
|
layerSlider.addTarget(self, action: #selector(layerPropertyChanged(_:)), for: .valueChanged)
|
|
@@ -267,7 +277,7 @@ final class PhotoVideoEditorViewController: UIViewController {
|
|
|
267
277
|
let toolbar = makePhotoToolBar(textColor: textColor, toolbarColor: toolbarColor)
|
|
268
278
|
mainToolBar = toolbar
|
|
269
279
|
|
|
270
|
-
let root = UIStackView(arrangedSubviews: [header, preview, straighten, cropBar, adjustmentSlider, filtersBar, stickersBar, layerSlider, colorSwatchRow, layerBar, toolbar])
|
|
280
|
+
let root = UIStackView(arrangedSubviews: [header, preview, straighten, cropBar, adjustmentSlider, filtersBar, stickersBar, drawBar, layerSlider, colorSwatchRow, layerBar, toolbar])
|
|
271
281
|
root.axis = .vertical
|
|
272
282
|
root.translatesAutoresizingMaskIntoConstraints = false
|
|
273
283
|
view.addSubview(root)
|
|
@@ -281,6 +291,7 @@ final class PhotoVideoEditorViewController: UIViewController {
|
|
|
281
291
|
cropBar.heightAnchor.constraint(equalToConstant: 100), // rotate row (44pt) + aspect chip row (56pt)
|
|
282
292
|
filtersBar.heightAnchor.constraint(equalToConstant: 56),
|
|
283
293
|
stickersBar.heightAnchor.constraint(equalToConstant: 56),
|
|
294
|
+
drawBar.heightAnchor.constraint(equalToConstant: 56),
|
|
284
295
|
layerBar.heightAnchor.constraint(equalToConstant: 56),
|
|
285
296
|
colorSwatchRow.heightAnchor.constraint(equalToConstant: 56),
|
|
286
297
|
])
|
|
@@ -377,9 +388,7 @@ final class PhotoVideoEditorViewController: UIViewController {
|
|
|
377
388
|
("filters", "Filters", "camera.filters"),
|
|
378
389
|
("text", "Text", "textformat"),
|
|
379
390
|
("stickers", "Stickers", "face.smiling"),
|
|
380
|
-
("
|
|
381
|
-
("retouch", "Retouch", "wand.and.rays"),
|
|
382
|
-
("layers", "Layers", "square.3.layers.3d"),
|
|
391
|
+
("draw", "Draw", "pencil.tip"),
|
|
383
392
|
("resize", "Resize", "aspectratio"),
|
|
384
393
|
]
|
|
385
394
|
let scroll = UIScrollView(); scroll.backgroundColor = toolbarColor
|
|
@@ -435,6 +444,7 @@ final class PhotoVideoEditorViewController: UIViewController {
|
|
|
435
444
|
outer.addArrangedSubview(transformRow)
|
|
436
445
|
|
|
437
446
|
let bar = UIStackView(); bar.axis = .horizontal; bar.alignment = .center
|
|
447
|
+
bar.addArrangedSubview(button("Cancel", color: textColor) { [weak self] in self?.cancelCropMode() })
|
|
438
448
|
let scroll = UIScrollView()
|
|
439
449
|
let presetStack = UIStackView(); presetStack.axis = .horizontal; presetStack.spacing = 4
|
|
440
450
|
presets.forEach { label, ratio in
|
|
@@ -470,19 +480,18 @@ final class PhotoVideoEditorViewController: UIViewController {
|
|
|
470
480
|
switch key {
|
|
471
481
|
case "crop":
|
|
472
482
|
setCropMode(true)
|
|
473
|
-
case "adjust", "filters"
|
|
483
|
+
case "adjust", "filters":
|
|
474
484
|
setFiltersMode(true, session: session)
|
|
475
485
|
case "text":
|
|
476
486
|
showTextInputDialog(session: session)
|
|
477
487
|
case "stickers":
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
488
|
+
stickersMode = true
|
|
489
|
+
refreshPhotoToolSelection()
|
|
490
|
+
presentOnlineStickerSheet(purpose: .photoSticker)
|
|
491
|
+
case "draw":
|
|
492
|
+
setDrawMode(true, session: session)
|
|
481
493
|
case "resize":
|
|
482
494
|
setCropMode(true)
|
|
483
|
-
case "layers":
|
|
484
|
-
if let top = session.layerStack.layers.last { selectLayer(top.id) }
|
|
485
|
-
else { showToast("Add content to start a layer stack.") }
|
|
486
495
|
default:
|
|
487
496
|
showToast("\(key) is coming in a later milestone.")
|
|
488
497
|
}
|
|
@@ -491,15 +500,15 @@ final class PhotoVideoEditorViewController: UIViewController {
|
|
|
491
500
|
// MARK: - Layers: text, selection, and transform/duplicate/reorder/lock/hide/delete
|
|
492
501
|
// with undo/redo (Milestone 4)
|
|
493
502
|
|
|
494
|
-
private func showTextInputDialog(session: PhotoEditSession) {
|
|
495
|
-
let alert = UIAlertController(title: "Add text", message:
|
|
496
|
-
alert.addTextField { $0.placeholder = "Text" }
|
|
503
|
+
private func showTextInputDialog(session: PhotoEditSession, editingLayer: PhotoLayer? = nil) {
|
|
504
|
+
let alert = UIAlertController(title: editingLayer == nil ? "Add text" : "Edit text", message: "Pinch to resize and rotate after adding.", preferredStyle: .alert)
|
|
505
|
+
alert.addTextField { $0.placeholder = "Text"; $0.text = editingLayer?.text; $0.textColor = editingLayer?.textColor ?? .white }
|
|
497
506
|
alert.addAction(UIAlertAction(title: "Cancel", style: .cancel))
|
|
498
507
|
alert.addAction(UIAlertAction(title: "Add", style: .default) { [weak self, weak alert] _ in
|
|
499
508
|
guard let text = alert?.textFields?.first?.text, !text.trimmingCharacters(in: .whitespaces).isEmpty else { return }
|
|
500
|
-
var layer = PhotoLayer(type: .text)
|
|
509
|
+
var layer = editingLayer ?? PhotoLayer(type: .text)
|
|
501
510
|
layer.text = text
|
|
502
|
-
session.layerStack.commit {
|
|
511
|
+
session.layerStack.commit { list in editingLayer == nil ? list + [layer] : list.map { $0.id == layer.id ? layer : $0 } }
|
|
503
512
|
self?.selectLayer(layer.id)
|
|
504
513
|
})
|
|
505
514
|
present(alert, animated: true)
|
|
@@ -537,11 +546,6 @@ final class PhotoVideoEditorViewController: UIViewController {
|
|
|
537
546
|
chipStack.addArrangedSubview(button("Upload", color: textColor) { [weak self] in
|
|
538
547
|
self?.presentImagePicker(purpose: .photoSticker)
|
|
539
548
|
})
|
|
540
|
-
if features["onlineStickers"] as? Bool != false {
|
|
541
|
-
chipStack.addArrangedSubview(button("Browse", color: textColor) { [weak self] in
|
|
542
|
-
self?.presentOnlineStickerSheet(purpose: .photoSticker)
|
|
543
|
-
})
|
|
544
|
-
}
|
|
545
549
|
scroll.addSubview(chipStack)
|
|
546
550
|
chipStack.translatesAutoresizingMaskIntoConstraints = false
|
|
547
551
|
NSLayoutConstraint.activate([
|
|
@@ -559,6 +563,71 @@ final class PhotoVideoEditorViewController: UIViewController {
|
|
|
559
563
|
return bar
|
|
560
564
|
}
|
|
561
565
|
|
|
566
|
+
private func setDrawMode(_ enabled: Bool, session: PhotoEditSession) {
|
|
567
|
+
drawMode = enabled
|
|
568
|
+
drawOverlay?.isHidden = !enabled
|
|
569
|
+
drawSubBar?.isHidden = !enabled
|
|
570
|
+
mainToolBar?.isHidden = enabled
|
|
571
|
+
layerOverlay?.isHidden = enabled
|
|
572
|
+
photoImageView?.panZoomEnabled = !enabled
|
|
573
|
+
if enabled {
|
|
574
|
+
drawOverlay?.strokeColor = .red
|
|
575
|
+
drawOverlay?.strokeWidthPx = 4
|
|
576
|
+
if let bounds = photoImageView?.currentImageBounds() { drawOverlay?.setImageBounds(bounds) }
|
|
577
|
+
} else {
|
|
578
|
+
commitDrawStrokes(session: session)
|
|
579
|
+
drawOverlay?.clearStrokes()
|
|
580
|
+
}
|
|
581
|
+
refreshPhotoToolSelection()
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
private func commitDrawStrokes(session: PhotoEditSession) {
|
|
585
|
+
guard let drawOverlay, drawOverlay.hasStrokes, let bounds = photoImageView?.currentImageBounds(), bounds.width > 0 else { return }
|
|
586
|
+
let shortSide = min(bounds.width, bounds.height)
|
|
587
|
+
let newLayers: [PhotoLayer] = drawOverlay.normalizedStrokes().compactMap { points, stroke in
|
|
588
|
+
guard points.count >= 2 else { return nil }
|
|
589
|
+
let centerX = points.reduce(0) { $0 + $1.0 } / CGFloat(points.count)
|
|
590
|
+
let centerY = points.reduce(0) { $0 + $1.1 } / CGFloat(points.count)
|
|
591
|
+
var layer = PhotoLayer(type: .drawing)
|
|
592
|
+
layer.x = centerX; layer.y = centerY
|
|
593
|
+
layer.drawColor = stroke.color
|
|
594
|
+
layer.drawStrokeWidth = stroke.widthPx / shortSide
|
|
595
|
+
layer.drawPoints = points.map { ($0.0 - centerX, $0.1 - centerY) }
|
|
596
|
+
return layer
|
|
597
|
+
}
|
|
598
|
+
if !newLayers.isEmpty { session.layerStack.commit { $0 + newLayers } }
|
|
599
|
+
onLayerStackChanged()
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
private func makeDrawSubBar(textColor: UIColor, primaryColor: UIColor, toolbarColor: UIColor) -> UIView {
|
|
603
|
+
let colors: [(String, UIColor)] = [("Red", .red), ("Yellow", .yellow), ("Green", .green), ("Blue", .systemBlue), ("White", .white), ("Black", .black)]
|
|
604
|
+
let bar = UIStackView(); bar.axis = .horizontal; bar.alignment = .center
|
|
605
|
+
let scroll = UIScrollView()
|
|
606
|
+
let controls = UIStackView(); controls.axis = .horizontal; controls.spacing = 4
|
|
607
|
+
colors.forEach { label, value in controls.addArrangedSubview(button(label, color: value) { [weak self] in self?.drawOverlay?.strokeColor = value }) }
|
|
608
|
+
controls.addArrangedSubview(button("Thin", color: textColor) { [weak self] in self?.drawOverlay?.strokeWidthPx = 2 })
|
|
609
|
+
controls.addArrangedSubview(button("Thick", color: textColor) { [weak self] in self?.drawOverlay?.strokeWidthPx = 10 })
|
|
610
|
+
controls.addArrangedSubview(button("Undo", color: textColor) { [weak self] in self?.drawOverlay?.undoLastStroke() })
|
|
611
|
+
controls.addArrangedSubview(button("Clear", color: textColor) { [weak self] in self?.drawOverlay?.clearStrokes() })
|
|
612
|
+
scroll.addSubview(controls); controls.translatesAutoresizingMaskIntoConstraints = false
|
|
613
|
+
NSLayoutConstraint.activate([
|
|
614
|
+
controls.leadingAnchor.constraint(equalTo: scroll.contentLayoutGuide.leadingAnchor, constant: 8),
|
|
615
|
+
controls.trailingAnchor.constraint(equalTo: scroll.contentLayoutGuide.trailingAnchor, constant: -8),
|
|
616
|
+
controls.topAnchor.constraint(equalTo: scroll.contentLayoutGuide.topAnchor),
|
|
617
|
+
controls.bottomAnchor.constraint(equalTo: scroll.contentLayoutGuide.bottomAnchor),
|
|
618
|
+
controls.heightAnchor.constraint(equalTo: scroll.frameLayoutGuide.heightAnchor),
|
|
619
|
+
])
|
|
620
|
+
bar.addArrangedSubview(scroll)
|
|
621
|
+
bar.addArrangedSubview(button("Done", color: primaryColor) { [weak self] in
|
|
622
|
+
guard let self, let session = self.photoSession else { return }
|
|
623
|
+
self.setDrawMode(false, session: session)
|
|
624
|
+
})
|
|
625
|
+
bar.backgroundColor = toolbarColor
|
|
626
|
+
bar.isLayoutMarginsRelativeArrangement = true
|
|
627
|
+
bar.layoutMargins = UIEdgeInsets(top: 0, left: 12, bottom: 0, right: 12)
|
|
628
|
+
return bar
|
|
629
|
+
}
|
|
630
|
+
|
|
562
631
|
private func consumerStickerAssets() -> [(id: String, uri: String)] {
|
|
563
632
|
stickerAssets.compactMap { asset in
|
|
564
633
|
guard let uri = asset["uri"] as? String, !uri.isEmpty else { return nil }
|
|
@@ -578,14 +647,24 @@ final class PhotoVideoEditorViewController: UIViewController {
|
|
|
578
647
|
present(picker, animated: true)
|
|
579
648
|
}
|
|
580
649
|
|
|
581
|
-
/// Presents the
|
|
582
|
-
/// insertion point once the user picks a sticker — same shared path the Upload flow uses.
|
|
650
|
+
/// Presents the bundled sticker sheet, reusing `handlePickedImage` as the insertion point.
|
|
583
651
|
private func presentOnlineStickerSheet(purpose: ImagePickPurpose) {
|
|
584
|
-
let
|
|
652
|
+
let runtimeStickers = consumerStickerAssets().map { OnlineStickerSheetViewController.RuntimeSticker(id: $0.id, uri: $0.uri) }
|
|
653
|
+
let sheet = OnlineStickerSheetViewController(runtimeStickers: runtimeStickers)
|
|
654
|
+
sheet.isModalInPresentation = false
|
|
655
|
+
sheet.onDismiss = { [weak self] in
|
|
656
|
+
if case .photoSticker = purpose {
|
|
657
|
+
self?.stickersMode = false
|
|
658
|
+
self?.refreshPhotoToolSelection()
|
|
659
|
+
}
|
|
660
|
+
}
|
|
585
661
|
sheet.onStickerPicked = { [weak self] fileURL, image in
|
|
586
662
|
self?.handlePickedImage(fileURL, image: image, purpose: purpose)
|
|
587
663
|
}
|
|
588
664
|
sheet.sheetPresentationController?.detents = [.medium(), .large()]
|
|
665
|
+
sheet.sheetPresentationController?.selectedDetentIdentifier = .large
|
|
666
|
+
sheet.sheetPresentationController?.prefersGrabberVisible = true
|
|
667
|
+
sheet.sheetPresentationController?.preferredCornerRadius = DesignTokens.radiusSheet
|
|
589
668
|
present(sheet, animated: true)
|
|
590
669
|
}
|
|
591
670
|
|
|
@@ -648,6 +727,7 @@ final class PhotoVideoEditorViewController: UIViewController {
|
|
|
648
727
|
case "crop": selected = cropMode
|
|
649
728
|
case "adjust", "filters": selected = filtersMode
|
|
650
729
|
case "stickers": selected = stickersMode
|
|
730
|
+
case "draw": selected = drawMode
|
|
651
731
|
default: selected = false
|
|
652
732
|
}
|
|
653
733
|
setToolNodeSelected(node, selected: selected, textColor: textColor)
|
|
@@ -1003,6 +1083,7 @@ final class PhotoVideoEditorViewController: UIViewController {
|
|
|
1003
1083
|
|
|
1004
1084
|
private func setCropMode(_ enabled: Bool) {
|
|
1005
1085
|
guard let imageView = photoImageView, let overlay = cropOverlay, let session = photoSession else { return }
|
|
1086
|
+
if enabled && !cropMode { cropEntryState = session.state }
|
|
1006
1087
|
cropMode = enabled
|
|
1007
1088
|
imageView.panZoomEnabled = !enabled
|
|
1008
1089
|
overlay.isHidden = !enabled
|
|
@@ -1013,6 +1094,10 @@ final class PhotoVideoEditorViewController: UIViewController {
|
|
|
1013
1094
|
if enabled {
|
|
1014
1095
|
imageView.resetToFit()
|
|
1015
1096
|
overlay.setImageBounds(imageView.currentImageBounds(), resetCrop: session.state.isIdentity)
|
|
1097
|
+
overlay.setCrop(left: session.state.cropLeft, top: session.state.cropTop, right: session.state.cropRight, bottom: session.state.cropBottom)
|
|
1098
|
+
overlay.setAspectRatio(session.state.aspectRatio)
|
|
1099
|
+
} else {
|
|
1100
|
+
cropEntryState = nil
|
|
1016
1101
|
}
|
|
1017
1102
|
refreshPhotoToolSelection()
|
|
1018
1103
|
let textColor = color("textColor") ?? DesignTokens.onSurface
|
|
@@ -1020,6 +1105,15 @@ final class PhotoVideoEditorViewController: UIViewController {
|
|
|
1020
1105
|
refreshSelection(cropAspectButtons, activeKey: aspectRatioLabel(session.state.aspectRatio), textColor: textColor, primaryColor: primaryColor)
|
|
1021
1106
|
}
|
|
1022
1107
|
|
|
1108
|
+
private func cancelCropMode() {
|
|
1109
|
+
guard let session = photoSession else { return }
|
|
1110
|
+
if let snapshot = cropEntryState { session.update { $0 = snapshot } }
|
|
1111
|
+
straightenSlider?.value = Float(cropEntryState?.straightenDegrees ?? 0)
|
|
1112
|
+
cropEntryState = nil
|
|
1113
|
+
schedulePhotoPreviewRender()
|
|
1114
|
+
setCropMode(false)
|
|
1115
|
+
}
|
|
1116
|
+
|
|
1023
1117
|
/// Maps an aspect ratio back to its preset label (matching the presets defined in `makeCropSubBar`/`makeVideoAspectSubBar`) so the correct chip can be highlighted.
|
|
1024
1118
|
private func aspectRatioLabel(_ ratio: CGFloat?) -> String {
|
|
1025
1119
|
guard let ratio else { return "Free" }
|
|
@@ -1051,6 +1145,7 @@ final class PhotoVideoEditorViewController: UIViewController {
|
|
|
1051
1145
|
completion?(.failure(code: "E_INTERNAL", message: "Nothing to export."))
|
|
1052
1146
|
return
|
|
1053
1147
|
}
|
|
1148
|
+
if drawMode { setDrawMode(false, session: session) }
|
|
1054
1149
|
progressOverlay?.isHidden = false
|
|
1055
1150
|
let state = session.state
|
|
1056
1151
|
let adjustments = session.adjustments
|
|
@@ -1141,11 +1236,26 @@ final class PhotoVideoEditorViewController: UIViewController {
|
|
|
1141
1236
|
self.onVideoLayerStackChanged()
|
|
1142
1237
|
}
|
|
1143
1238
|
|
|
1144
|
-
let playPause =
|
|
1239
|
+
let playPause = UIButton(type: .system)
|
|
1240
|
+
playPause.tintColor = textColor
|
|
1241
|
+
playPause.setImage(UIImage(systemName: "play.fill"), for: .normal)
|
|
1242
|
+
playPause.accessibilityLabel = "Play video"
|
|
1243
|
+
playPause.addAction(UIAction { [weak self] _ in
|
|
1145
1244
|
guard let self, let session = self.videoSession else { return }
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
|
|
1245
|
+
if session.player.timeControlStatus == .playing {
|
|
1246
|
+
session.player.pause()
|
|
1247
|
+
} else {
|
|
1248
|
+
let current = CMTimeGetSeconds(session.player.currentTime())
|
|
1249
|
+
let duration = Double(session.durationMs) / 1000
|
|
1250
|
+
if current.isFinite, duration > 0, current >= duration - 0.05 {
|
|
1251
|
+
session.player.seek(to: .zero) { finished in
|
|
1252
|
+
if finished { session.player.rate = session.state.speed }
|
|
1253
|
+
}
|
|
1254
|
+
} else {
|
|
1255
|
+
session.player.rate = session.state.speed
|
|
1256
|
+
}
|
|
1257
|
+
}
|
|
1258
|
+
}, for: .touchUpInside)
|
|
1149
1259
|
playPauseButton = playPause
|
|
1150
1260
|
let time = UILabel()
|
|
1151
1261
|
time.text = "0:00 / 0:00"
|
|
@@ -1229,7 +1339,9 @@ final class PhotoVideoEditorViewController: UIViewController {
|
|
|
1229
1339
|
let toolbar = makeVideoToolBar(session: session, playerController: playerController, textColor: textColor, toolbarColor: toolbarColor)
|
|
1230
1340
|
videoToolBar = toolbar
|
|
1231
1341
|
|
|
1232
|
-
|
|
1342
|
+
// The editor is intentionally single-video: omit the multi-clip strip and
|
|
1343
|
+
// its Split/Copy/Delete/Reorder controls from the visible layout.
|
|
1344
|
+
let root = UIStackView(arrangedSubviews: [header, preview, transport, position, aspectBar, layerSlider, layerBar, toolbar])
|
|
1233
1345
|
root.axis = .vertical
|
|
1234
1346
|
root.translatesAutoresizingMaskIntoConstraints = false
|
|
1235
1347
|
view.addSubview(root)
|
|
@@ -1283,7 +1395,9 @@ final class PhotoVideoEditorViewController: UIViewController {
|
|
|
1283
1395
|
self.positionSlider?.value = Float(min(max(Double(currentMs) / Double(session.durationMs), 0), 1)) * 1000
|
|
1284
1396
|
self.updateTimeLabel()
|
|
1285
1397
|
}
|
|
1286
|
-
|
|
1398
|
+
let playing = session.player.timeControlStatus == .playing
|
|
1399
|
+
self.playPauseButton?.setImage(UIImage(systemName: playing ? "pause.fill" : "play.fill"), for: .normal)
|
|
1400
|
+
self.playPauseButton?.accessibilityLabel = playing ? "Pause video" : "Play video"
|
|
1287
1401
|
self.refreshVideoOverlayPreview(session: session)
|
|
1288
1402
|
}
|
|
1289
1403
|
}
|
|
@@ -1382,7 +1496,13 @@ final class PhotoVideoEditorViewController: UIViewController {
|
|
|
1382
1496
|
videoLayerToolBar?.isHidden = !visible
|
|
1383
1497
|
videoLayerPropertySlider?.isHidden = !visible
|
|
1384
1498
|
videoToolBar?.isHidden = visible
|
|
1385
|
-
if visible {
|
|
1499
|
+
if visible {
|
|
1500
|
+
if !["rotation", "opacity"].contains(activeVideoLayerPropertyKey) { activeVideoLayerPropertyKey = "rotation" }
|
|
1501
|
+
syncVideoLayerPropertySlider(session: session)
|
|
1502
|
+
let textColor = color("textColor") ?? DesignTokens.onSurface
|
|
1503
|
+
let primaryColor = color("primaryColor") ?? DesignTokens.primaryContainer
|
|
1504
|
+
refreshSelection(videoLayerPropertyButtons, activeKey: activeVideoLayerPropertyKey, textColor: textColor, primaryColor: primaryColor)
|
|
1505
|
+
}
|
|
1386
1506
|
}
|
|
1387
1507
|
|
|
1388
1508
|
private func currentSelectedVideoLayer(session: VideoEditSession) -> PhotoLayer? {
|
|
@@ -1428,7 +1548,7 @@ final class PhotoVideoEditorViewController: UIViewController {
|
|
|
1428
1548
|
}
|
|
1429
1549
|
|
|
1430
1550
|
private func makeVideoLayerToolBar(session: VideoEditSession, textColor: UIColor, primaryColor: UIColor, toolbarColor: UIColor) -> UIView {
|
|
1431
|
-
let properties: [(String, String)] = [("
|
|
1551
|
+
let properties: [(String, String)] = [("rotation", "Rotate"), ("opacity", "Opacity")]
|
|
1432
1552
|
let bar = UIStackView(); bar.axis = .horizontal; bar.alignment = .center
|
|
1433
1553
|
let scroll = UIScrollView()
|
|
1434
1554
|
let chipStack = UIStackView(); chipStack.axis = .horizontal; chipStack.spacing = 4
|
|
@@ -1496,8 +1616,7 @@ final class PhotoVideoEditorViewController: UIViewController {
|
|
|
1496
1616
|
|
|
1497
1617
|
private func makeVideoToolBar(session: VideoEditSession, playerController: AVPlayerViewController, textColor: UIColor, toolbarColor: UIColor) -> UIView {
|
|
1498
1618
|
let tools: [(String, String)] = [
|
|
1499
|
-
("
|
|
1500
|
-
("filters", "Filters"), ("text", "Text"), ("stickers", "Stickers"), ("overlay", "Overlay"),
|
|
1619
|
+
("text", "Text"), ("stickers", "Stickers"),
|
|
1501
1620
|
]
|
|
1502
1621
|
let scroll = UIScrollView(); scroll.backgroundColor = toolbarColor
|
|
1503
1622
|
let stack = UIStackView(); stack.axis = .horizontal; stack.spacing = 8
|
|
@@ -1560,37 +1679,7 @@ final class PhotoVideoEditorViewController: UIViewController {
|
|
|
1560
1679
|
}
|
|
1561
1680
|
|
|
1562
1681
|
private func showVideoStickerPicker(session: VideoEditSession) {
|
|
1563
|
-
|
|
1564
|
-
PhotoLayerRenderer.builtinStickerIDs().forEach { id in
|
|
1565
|
-
alert.addAction(UIAlertAction(title: PhotoLayerRenderer.glyph(for: id), style: .default) { [weak self] _ in
|
|
1566
|
-
var layer = PhotoLayer(type: .sticker)
|
|
1567
|
-
layer.stickerId = id
|
|
1568
|
-
session.layerStack.commit { $0 + [layer] }
|
|
1569
|
-
self?.onVideoLayerStackChanged()
|
|
1570
|
-
})
|
|
1571
|
-
}
|
|
1572
|
-
consumerStickerAssets().forEach { asset in
|
|
1573
|
-
alert.addAction(UIAlertAction(title: asset.id, style: .default) { [weak self] _ in
|
|
1574
|
-
var layer = PhotoLayer(type: .sticker)
|
|
1575
|
-
layer.stickerUri = asset.uri
|
|
1576
|
-
session.layerStack.commit { $0 + [layer] }
|
|
1577
|
-
self?.onVideoLayerStackChanged()
|
|
1578
|
-
})
|
|
1579
|
-
}
|
|
1580
|
-
alert.addAction(UIAlertAction(title: "Upload", style: .default) { [weak self] _ in
|
|
1581
|
-
self?.presentImagePicker(purpose: .videoSticker)
|
|
1582
|
-
})
|
|
1583
|
-
if features["onlineStickers"] as? Bool != false {
|
|
1584
|
-
alert.addAction(UIAlertAction(title: "Browse online", style: .default) { [weak self] _ in
|
|
1585
|
-
self?.presentOnlineStickerSheet(purpose: .videoSticker)
|
|
1586
|
-
})
|
|
1587
|
-
}
|
|
1588
|
-
alert.addAction(UIAlertAction(title: "Cancel", style: .cancel))
|
|
1589
|
-
if let popover = alert.popoverPresentationController {
|
|
1590
|
-
popover.sourceView = view
|
|
1591
|
-
popover.sourceRect = CGRect(x: view.bounds.midX, y: view.bounds.maxY - 1, width: 1, height: 1)
|
|
1592
|
-
}
|
|
1593
|
-
present(alert, animated: true)
|
|
1682
|
+
presentOnlineStickerSheet(purpose: .videoSticker)
|
|
1594
1683
|
}
|
|
1595
1684
|
|
|
1596
1685
|
private func formatSpeedLabel(_ speed: Float) -> String {
|
|
@@ -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
|
|
@@ -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
|
|
@@ -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
|
}
|
|
@@ -80,6 +81,19 @@ enum PhotoLayerRenderer {
|
|
|
80
81
|
image.draw(in: rect, blendMode: .normal, alpha: layer.opacity)
|
|
81
82
|
}
|
|
82
83
|
|
|
84
|
+
private static func drawStroke(_ ctx: CGContext, _ layer: PhotoLayer, width: CGFloat, height: CGFloat, shortSide: CGFloat) {
|
|
85
|
+
guard layer.drawPoints.count >= 2 else { return }
|
|
86
|
+
ctx.setAlpha(layer.opacity)
|
|
87
|
+
ctx.setStrokeColor(layer.drawColor.cgColor)
|
|
88
|
+
ctx.setLineWidth(layer.drawStrokeWidth * shortSide)
|
|
89
|
+
ctx.setLineCap(.round)
|
|
90
|
+
ctx.setLineJoin(.round)
|
|
91
|
+
let points = layer.drawPoints.map { CGPoint(x: $0.0 * width, y: $0.1 * height) }
|
|
92
|
+
ctx.move(to: points[0])
|
|
93
|
+
points.dropFirst().forEach { ctx.addLine(to: $0) }
|
|
94
|
+
ctx.strokePath()
|
|
95
|
+
}
|
|
96
|
+
|
|
83
97
|
}
|
|
84
98
|
|
|
85
99
|
private extension NSAttributedString {
|
|
@@ -12,10 +12,11 @@ final class CropOverlayView: UIView {
|
|
|
12
12
|
|
|
13
13
|
private let dimLayer = CAShapeLayer()
|
|
14
14
|
private let borderLayer = CAShapeLayer()
|
|
15
|
+
private let handlesLayer = CAShapeLayer()
|
|
15
16
|
private let handleRadius: CGFloat = 32
|
|
16
17
|
private let minSize: CGFloat = 60
|
|
17
18
|
|
|
18
|
-
private enum Handle { case move, topLeft, topRight, bottomLeft, bottomRight }
|
|
19
|
+
private enum Handle { case move, top, right, bottom, left, topLeft, topRight, bottomLeft, bottomRight }
|
|
19
20
|
private var activeHandle: Handle?
|
|
20
21
|
private var lastPoint: CGPoint = .zero
|
|
21
22
|
|
|
@@ -29,6 +30,8 @@ final class CropOverlayView: UIView {
|
|
|
29
30
|
borderLayer.fillColor = UIColor.clear.cgColor
|
|
30
31
|
borderLayer.lineWidth = 2
|
|
31
32
|
layer.addSublayer(borderLayer)
|
|
33
|
+
handlesLayer.fillColor = UIColor.white.cgColor
|
|
34
|
+
layer.addSublayer(handlesLayer)
|
|
32
35
|
}
|
|
33
36
|
|
|
34
37
|
required init?(coder: NSCoder) { nil }
|
|
@@ -43,6 +46,18 @@ final class CropOverlayView: UIView {
|
|
|
43
46
|
setNeedsLayout()
|
|
44
47
|
}
|
|
45
48
|
|
|
49
|
+
/// Restores a normalized crop rectangle, used when reopening or cancelling crop mode.
|
|
50
|
+
func setCrop(left: CGFloat, top: CGFloat, right: CGFloat, bottom: CGFloat) {
|
|
51
|
+
guard imageBounds.width > 0, imageBounds.height > 0 else { return }
|
|
52
|
+
cropRect = CGRect(
|
|
53
|
+
x: imageBounds.minX + left * imageBounds.width,
|
|
54
|
+
y: imageBounds.minY + top * imageBounds.height,
|
|
55
|
+
width: (right - left) * imageBounds.width,
|
|
56
|
+
height: (bottom - top) * imageBounds.height
|
|
57
|
+
)
|
|
58
|
+
clampToImageBounds(); setNeedsLayout()
|
|
59
|
+
}
|
|
60
|
+
|
|
46
61
|
func setAspectRatio(_ ratio: CGFloat?) {
|
|
47
62
|
aspectRatio = ratio
|
|
48
63
|
if let ratio, imageBounds.width > 0, imageBounds.height > 0 {
|
|
@@ -79,6 +94,16 @@ final class CropOverlayView: UIView {
|
|
|
79
94
|
borderPath.addLine(to: CGPoint(x: cropRect.maxX, y: y))
|
|
80
95
|
}
|
|
81
96
|
borderLayer.path = borderPath.cgPath
|
|
97
|
+
|
|
98
|
+
let handlesPath = UIBezierPath()
|
|
99
|
+
let points = [
|
|
100
|
+
CGPoint(x: cropRect.minX, y: cropRect.minY), CGPoint(x: cropRect.midX, y: cropRect.minY),
|
|
101
|
+
CGPoint(x: cropRect.maxX, y: cropRect.minY), CGPoint(x: cropRect.maxX, y: cropRect.midY),
|
|
102
|
+
CGPoint(x: cropRect.maxX, y: cropRect.maxY), CGPoint(x: cropRect.midX, y: cropRect.maxY),
|
|
103
|
+
CGPoint(x: cropRect.minX, y: cropRect.maxY), CGPoint(x: cropRect.minX, y: cropRect.midY),
|
|
104
|
+
]
|
|
105
|
+
points.forEach { point in handlesPath.append(UIBezierPath(ovalIn: CGRect(x: point.x - 4, y: point.y - 4, width: 8, height: 8))) }
|
|
106
|
+
handlesLayer.path = handlesPath.cgPath
|
|
82
107
|
}
|
|
83
108
|
|
|
84
109
|
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
|
|
@@ -108,6 +133,10 @@ final class CropOverlayView: UIView {
|
|
|
108
133
|
if near(CGPoint(x: cropRect.maxX, y: cropRect.minY)) { return .topRight }
|
|
109
134
|
if near(CGPoint(x: cropRect.minX, y: cropRect.maxY)) { return .bottomLeft }
|
|
110
135
|
if near(CGPoint(x: cropRect.maxX, y: cropRect.maxY)) { return .bottomRight }
|
|
136
|
+
if near(CGPoint(x: cropRect.midX, y: cropRect.minY)) { return .top }
|
|
137
|
+
if near(CGPoint(x: cropRect.maxX, y: cropRect.midY)) { return .right }
|
|
138
|
+
if near(CGPoint(x: cropRect.midX, y: cropRect.maxY)) { return .bottom }
|
|
139
|
+
if near(CGPoint(x: cropRect.minX, y: cropRect.midY)) { return .left }
|
|
111
140
|
if cropRect.contains(point) { return .move }
|
|
112
141
|
return nil
|
|
113
142
|
}
|
|
@@ -125,6 +154,10 @@ final class CropOverlayView: UIView {
|
|
|
125
154
|
rect = CGRect(x: rect.minX + dx, y: rect.minY, width: rect.width - dx, height: rect.height + dy)
|
|
126
155
|
case .bottomRight:
|
|
127
156
|
rect = CGRect(x: rect.minX, y: rect.minY, width: rect.width + dx, height: rect.height + dy)
|
|
157
|
+
case .top: rect = CGRect(x: rect.minX, y: rect.minY + dy, width: rect.width, height: rect.height - dy)
|
|
158
|
+
case .right: rect.size.width += dx
|
|
159
|
+
case .bottom: rect.size.height += dy
|
|
160
|
+
case .left: rect = CGRect(x: rect.minX + dx, y: rect.minY, width: rect.width - dx, height: rect.height)
|
|
128
161
|
}
|
|
129
162
|
guard rect.width >= minSize, rect.height >= minSize else { return }
|
|
130
163
|
if let ratio = aspectRatio {
|
|
@@ -137,6 +170,10 @@ final class CropOverlayView: UIView {
|
|
|
137
170
|
rect.size.height = newHeight
|
|
138
171
|
case .move:
|
|
139
172
|
break
|
|
173
|
+
case .left, .right:
|
|
174
|
+
let centerY = cropRect.midY; rect.size.height = rect.width / ratio; rect.origin.y = centerY - rect.height / 2
|
|
175
|
+
case .top, .bottom:
|
|
176
|
+
let centerX = cropRect.midX; rect.size.width = rect.height * ratio; rect.origin.x = centerX - rect.width / 2
|
|
140
177
|
}
|
|
141
178
|
}
|
|
142
179
|
cropRect = rect
|
|
@@ -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
|
+
}
|
|
@@ -9,6 +9,7 @@ final class LayerOverlayView: UIView {
|
|
|
9
9
|
var layers: [PhotoLayer] = [] { didSet { setNeedsDisplay() } }
|
|
10
10
|
var selectedLayerID: String? { didSet { setNeedsDisplay() } }
|
|
11
11
|
var onLayerTapped: ((String?) -> Void)?
|
|
12
|
+
var onLayerDoubleTapped: ((String) -> Void)?
|
|
12
13
|
var onLayerTransformChanged: ((_ id: String, _ x: CGFloat, _ y: CGFloat, _ scale: CGFloat, _ rotationDegrees: CGFloat) -> Void)?
|
|
13
14
|
var onLayerTransformEnded: ((String) -> Void)?
|
|
14
15
|
|
|
@@ -107,7 +108,10 @@ final class LayerOverlayView: UIView {
|
|
|
107
108
|
}
|
|
108
109
|
}
|
|
109
110
|
|
|
110
|
-
override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent?) {
|
|
111
|
+
override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent?) {
|
|
112
|
+
if let touch = touches.first, touch.tapCount >= 2, let id = gestureID { onLayerDoubleTapped?(id) }
|
|
113
|
+
endTouches(touches)
|
|
114
|
+
}
|
|
111
115
|
override func touchesCancelled(_ touches: Set<UITouch>, with event: UIEvent?) { endTouches(touches) }
|
|
112
116
|
|
|
113
117
|
private func endTouches(_ touches: Set<UITouch>) {
|
|
@@ -146,6 +150,10 @@ final class LayerOverlayView: UIView {
|
|
|
146
150
|
// Mirrors `PhotoLayerRenderer.drawOverlay`'s box: full canvas width, height from aspect ratio.
|
|
147
151
|
let aspectRatio = layer.overlayAspectRatio.flatMap { $0 > 0 ? $0 : nil } ?? 1
|
|
148
152
|
base = CGSize(width: imageBounds.width / 2, height: imageBounds.width / (2 * aspectRatio))
|
|
153
|
+
case .drawing:
|
|
154
|
+
let maxX = layer.drawPoints.map { abs($0.0) }.max() ?? 0.08
|
|
155
|
+
let maxY = layer.drawPoints.map { abs($0.1) }.max() ?? 0.08
|
|
156
|
+
base = CGSize(width: maxX * imageBounds.width + handleRadius, height: maxY * imageBounds.height + handleRadius)
|
|
149
157
|
}
|
|
150
158
|
return CGSize(width: max(handleRadius, base.width * layer.scale), height: max(handleRadius, base.height * layer.scale))
|
|
151
159
|
}
|
|
Binary file
|