diff --git a/submodules/DrawingUI/Sources/DrawingEntitiesView.swift b/submodules/DrawingUI/Sources/DrawingEntitiesView.swift index 5d6719096e..34253dde77 100644 --- a/submodules/DrawingUI/Sources/DrawingEntitiesView.swift +++ b/submodules/DrawingUI/Sources/DrawingEntitiesView.swift @@ -885,7 +885,7 @@ public final class DrawingEntitiesView: UIView, TGPhotoDrawingEntitiesView { } else if self.autoSelectEntities, gestureRecognizer.numberOfTouches == 1, let viewToSelect = self.entity(at: location) { self.selectEntity(viewToSelect.entity, animate: false) self.onInteractionUpdated(true) - } else if gestureRecognizer.numberOfTouches == 2 || self.isStickerEditor, let mediaEntityView = self.subviews.first(where: { $0 is DrawingEntityMediaView }) as? DrawingEntityMediaView { + } else if gestureRecognizer.numberOfTouches == 2 || (self.isStickerEditor && self.autoSelectEntities), let mediaEntityView = self.subviews.first(where: { $0 is DrawingEntityMediaView }) as? DrawingEntityMediaView { mediaEntityView.handlePan(gestureRecognizer) } } diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Stickers/TelegramEngineStickers.swift b/submodules/TelegramCore/Sources/TelegramEngine/Stickers/TelegramEngineStickers.swift index 8e607e3dce..acc8702807 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Stickers/TelegramEngineStickers.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Stickers/TelegramEngineStickers.swift @@ -257,6 +257,12 @@ public extension TelegramEngine { return (items.map(\.file), isFinalResult) } } + + public func addRecentlyUsedSticker(file: TelegramMediaFile) { + let _ = self.account.postbox.transaction({ transaction -> Void in + TelegramCore.addRecentlyUsedSticker(transaction: transaction, fileReference: .standalone(media: file)) + }).start() + } } } diff --git a/submodules/TelegramUI/Components/MediaEditor/MetalResources/EditorDual.metal b/submodules/TelegramUI/Components/MediaEditor/MetalResources/EditorDual.metal index c45b5ec9ff..a4fa037c62 100644 --- a/submodules/TelegramUI/Components/MediaEditor/MetalResources/EditorDual.metal +++ b/submodules/TelegramUI/Components/MediaEditor/MetalResources/EditorDual.metal @@ -3,6 +3,14 @@ using namespace metal; +typedef struct { + float2 dimensions; + float roundness; + float alpha; + float isOpaque; + float empty; +} VideoEncodeParameters; + typedef struct { float4 pos; float2 texCoord; @@ -17,11 +25,10 @@ float sdfRoundedRectangle(float2 uv, float2 position, float2 size, float radius) fragment half4 dualFragmentShader(RasterizerData in [[stage_in]], texture2d texture [[texture(0)]], - constant uint2 &resolution[[buffer(0)]], - constant float &roundness[[buffer(1)]], - constant float &alpha[[buffer(2)]] + texture2d mask [[texture(1)]], + constant VideoEncodeParameters& adjustments [[buffer(0)]] ) { - float2 R = float2(resolution.x, resolution.y); + float2 R = float2(adjustments.dimensions.x, adjustments.dimensions.y); float2 uv = (in.localPos - float2(0.5, 0.5)) * 2.0; if (R.x > R.y) { @@ -33,10 +40,11 @@ fragment half4 dualFragmentShader(RasterizerData in [[stage_in]], constexpr sampler samplr(filter::linear, mag_filter::linear, min_filter::linear); half3 color = texture.sample(samplr, in.texCoord).rgb; + float colorAlpha = min(1.0, adjustments.isOpaque + mask.sample(samplr, in.texCoord).r); - float t = 1.0 / resolution.y; + float t = 1.0 / adjustments.dimensions.y; float side = 1.0 * aspectRatio; - float distance = smoothstep(t, -t, sdfRoundedRectangle(uv, float2(0.0, 0.0), float2(side, mix(1.0, side, roundness)), side * roundness)); + float distance = smoothstep(t, -t, sdfRoundedRectangle(uv, float2(0.0, 0.0), float2(side, mix(1.0, side, adjustments.roundness)), side * adjustments.roundness)); - return mix(half4(color, 0.0), half4(color, 1.0 * alpha), distance); + return mix(half4(color, 0.0), half4(color, colorAlpha * adjustments.alpha), distance); } diff --git a/submodules/TelegramUI/Components/MediaEditor/Sources/ImageObjectSeparation.swift b/submodules/TelegramUI/Components/MediaEditor/Sources/ImageObjectSeparation.swift index 9ecd12429b..fe2f1100f0 100644 --- a/submodules/TelegramUI/Components/MediaEditor/Sources/ImageObjectSeparation.swift +++ b/submodules/TelegramUI/Components/MediaEditor/Sources/ImageObjectSeparation.swift @@ -55,6 +55,70 @@ public func cutoutStickerImage(from image: UIImage, onlyCheck: Bool = false) -> } } +public enum CutoutResult { + case image(UIImage) + case pixelBuffer(CVPixelBuffer) +} + +public func cutoutImage(from image: UIImage, atPoint point: CGPoint?, asImage: Bool) -> Signal { + if #available(iOS 17.0, *) { + guard let cgImage = image.cgImage else { + return .single(nil) + } + return Signal { subscriber in + let ciContext = CIContext(options: nil) + let inputImage = CIImage(cgImage: cgImage) + let handler = VNImageRequestHandler(cgImage: cgImage, options: [:]) + let request = VNGenerateForegroundInstanceMaskRequest { [weak handler] request, error in + guard let handler, let result = request.results?.first as? VNInstanceMaskObservation else { + subscriber.putNext(nil) + subscriber.putCompletion() + return + } + + let instances = IndexSet(instances(atPoint: point, inObservation: result).prefix(1)) + if let mask = try? result.generateScaledMaskForImage(forInstances: instances, from: handler) { + if asImage { + let filter = CIFilter.blendWithMask() + filter.inputImage = inputImage + filter.backgroundImage = CIImage(color: .clear) + filter.maskImage = CIImage(cvPixelBuffer: mask) + if let output = filter.outputImage, let cgImage = ciContext.createCGImage(output, from: inputImage.extent) { + let image = UIImage(cgImage: cgImage) + subscriber.putNext(.image(image)) + subscriber.putCompletion() + return + } + } else { + let filter = CIFilter.blendWithMask() + filter.inputImage = CIImage(color: .white) + filter.backgroundImage = CIImage(color: .black) + filter.maskImage = CIImage(cvPixelBuffer: mask) + if let output = filter.outputImage, let cgImage = ciContext.createCGImage(output, from: inputImage.extent) { + let image = UIImage(cgImage: cgImage) + subscriber.putNext(.image(image)) + subscriber.putCompletion() + return + } +// subscriber.putNext(.pixelBuffer(mask)) +// subscriber.putCompletion() + } + } + subscriber.putNext(nil) + subscriber.putCompletion() + } + + try? handler.perform([request]) + return ActionDisposable { + request.cancel() + } + } + |> runOn(queue) + } else { + return .single(nil) + } +} + @available(iOS 17.0, *) private func instances(atPoint maybePoint: CGPoint?, inObservation observation: VNInstanceMaskObservation) -> IndexSet { guard let point = maybePoint else { diff --git a/submodules/TelegramUI/Components/MediaEditor/Sources/MediaEditor.swift b/submodules/TelegramUI/Components/MediaEditor/Sources/MediaEditor.swift index fe03b681b0..183b2405e3 100644 --- a/submodules/TelegramUI/Components/MediaEditor/Sources/MediaEditor.swift +++ b/submodules/TelegramUI/Components/MediaEditor/Sources/MediaEditor.swift @@ -190,6 +190,7 @@ public final class MediaEditor { public private(set) var canCutout: Bool = false public var canCutoutUpdated: (Bool) -> Void = { _ in } + public var isCutoutUpdated: (Bool) -> Void = { _ in } private var textureCache: CVMetalTextureCache! @@ -1682,6 +1683,51 @@ public final class MediaEditor { self.renderer.renderFrame() } + public func getSeparatedImage(point: CGPoint?) -> Signal { + guard let textureSource = self.renderer.textureSource as? UniversalTextureSource, let image = textureSource.mainImage else { + return .single(nil) + } + return cutoutImage(from: image, atPoint: point, asImage: true) + |> map { result in + if let result, case let .image(image) = result { + return image + } else { + return nil + } + } + } + + public func removeSeparationMask() { + self.isCutoutUpdated(false) + + self.renderer.currentMainInputMask = nil + if !self.skipRendering { + self.updateRenderChain() + } + } + + public func setSeparationMask(point: CGPoint?) { + guard let renderTarget = self.previewView, let device = renderTarget.mtlDevice else { + return + } + guard let textureSource = self.renderer.textureSource as? UniversalTextureSource, let image = textureSource.mainImage else { + return + } + self.isCutoutUpdated(true) + + let _ = (cutoutImage(from: image, atPoint: point, asImage: false) + |> deliverOnMainQueue).start(next: { [weak self] result in + guard let self, let result, case let .image(image) = result else { + return + } + //TODO:replace with pixelbuffer + self.renderer.currentMainInputMask = loadTexture(image: image, device: device) + if !self.skipRendering { + self.updateRenderChain() + } + }) + } + private func maybeGeneratePersonSegmentation(_ image: UIImage?) { if #available(iOS 15.0, *), let cgImage = image?.cgImage { let faceRequest = VNDetectFaceRectanglesRequest { [weak self] request, _ in diff --git a/submodules/TelegramUI/Components/MediaEditor/Sources/MediaEditorRenderer.swift b/submodules/TelegramUI/Components/MediaEditor/Sources/MediaEditorRenderer.swift index 0242d8dc45..c4bddd4725 100644 --- a/submodules/TelegramUI/Components/MediaEditor/Sources/MediaEditorRenderer.swift +++ b/submodules/TelegramUI/Components/MediaEditor/Sources/MediaEditorRenderer.swift @@ -98,6 +98,7 @@ final class MediaEditorRenderer { } private var currentMainInput: Input? + var currentMainInputMask: MTLTexture? private var currentAdditionalInput: Input? private(set) var resultTexture: MTLTexture? @@ -202,7 +203,7 @@ final class MediaEditorRenderer { } if let mainTexture { - return self.videoFinishPass.process(input: mainTexture, secondInput: additionalTexture, timestamp: mainInput.timestamp, device: device, commandBuffer: commandBuffer) + return self.videoFinishPass.process(input: mainTexture, inputMask: self.currentMainInputMask, secondInput: additionalTexture, timestamp: mainInput.timestamp, device: device, commandBuffer: commandBuffer) } else { return nil } diff --git a/submodules/TelegramUI/Components/MediaEditor/Sources/UniversalTextureSource.swift b/submodules/TelegramUI/Components/MediaEditor/Sources/UniversalTextureSource.swift index 71f84cdfa1..4e7a97f844 100644 --- a/submodules/TelegramUI/Components/MediaEditor/Sources/UniversalTextureSource.swift +++ b/submodules/TelegramUI/Components/MediaEditor/Sources/UniversalTextureSource.swift @@ -42,6 +42,13 @@ final class UniversalTextureSource: TextureSource { ) } + var mainImage: UIImage? { + if let mainInput = self.mainInputContext?.input, case let .image(image) = mainInput { + return image + } + return nil + } + func setMainInput(_ input: Input) { guard let renderTarget = self.renderTarget else { return diff --git a/submodules/TelegramUI/Components/MediaEditor/Sources/VideoFinishPass.swift b/submodules/TelegramUI/Components/MediaEditor/Sources/VideoFinishPass.swift index bd96115295..d6e3fcbe43 100644 --- a/submodules/TelegramUI/Components/MediaEditor/Sources/VideoFinishPass.swift +++ b/submodules/TelegramUI/Components/MediaEditor/Sources/VideoFinishPass.swift @@ -144,6 +144,14 @@ private var transitionDuration = 0.5 private var apperanceDuration = 0.2 private var videoRemovalDuration: Double = 0.2 +struct VideoEncodeParameters { + var dimensions: simd_float2 + var roundness: simd_float1 + var alpha: simd_float1 + var isOpaque: simd_float1 + var empty: simd_float1 +} + final class VideoFinishPass: RenderPass { private var cachedTexture: MTLTexture? @@ -195,6 +203,7 @@ final class VideoFinishPass: RenderPass { containerSize: CGSize, texture: MTLTexture, textureRotation: TextureRotation, + maskTexture: MTLTexture?, position: VideoPosition, roundness: Float, alpha: Float, @@ -202,6 +211,11 @@ final class VideoFinishPass: RenderPass { device: MTLDevice ) { encoder.setFragmentTexture(texture, index: 0) + if let maskTexture { + encoder.setFragmentTexture(maskTexture, index: 1) + } else { + encoder.setFragmentTexture(texture, index: 1) + } let center = CGPoint( x: position.position.x - containerSize.width / 2.0, @@ -220,14 +234,25 @@ final class VideoFinishPass: RenderPass { options: []) encoder.setVertexBuffer(buffer, offset: 0, index: 0) - var resolution = simd_uint2(UInt32(size.width), UInt32(size.height)) - encoder.setFragmentBytes(&resolution, length: MemoryLayout.size * 2, index: 0) - - var roundness = roundness - encoder.setFragmentBytes(&roundness, length: MemoryLayout.size, index: 1) - - var alpha = alpha - encoder.setFragmentBytes(&alpha, length: MemoryLayout.size, index: 2) + var parameters = VideoEncodeParameters( + dimensions: simd_float2(Float(size.width), Float(size.height)), + roundness: roundness, + alpha: alpha, + isOpaque: maskTexture == nil ? 1.0 : 0.0, + empty: 0 + ) + encoder.setFragmentBytes(¶meters, length: MemoryLayout.size, index: 0) +// var resolution = simd_uint2(UInt32(size.width), UInt32(size.height)) +// encoder.setFragmentBytes(&resolution, length: MemoryLayout.size * 2, index: 0) +// +// var roundness = roundness +// encoder.setFragmentBytes(&roundness, length: MemoryLayout.size, index: 1) +// +// var alpha = alpha +// encoder.setFragmentBytes(&alpha, length: MemoryLayout.size, index: 2) +// +// var isOpaque = maskTexture == nil ? 1.0 : 0.0 +// encoder.setFragmentBytes(&isOpaque, length: MemoryLayout.size, index: 3) encoder.drawPrimitives(type: .triangleStrip, vertexStart: 0, vertexCount: 4) } @@ -478,7 +503,14 @@ final class VideoFinishPass: RenderPass { return (backgroundVideoState, foregroundVideoState, disappearingVideoState) } - func process(input: MTLTexture, secondInput: MTLTexture?, timestamp: CMTime, device: MTLDevice, commandBuffer: MTLCommandBuffer) -> MTLTexture? { + func process( + input: MTLTexture, + inputMask: MTLTexture?, + secondInput: MTLTexture?, + timestamp: CMTime, + device: MTLDevice, + commandBuffer: MTLCommandBuffer + ) -> MTLTexture? { if !self.isStory { return input } @@ -536,7 +568,6 @@ final class VideoFinishPass: RenderPass { ) if self.gradientColors.topColor.w > 0.0 { - renderCommandEncoder.setRenderPipelineState(self.gradientPipelineState!) self.encodeGradient( using: renderCommandEncoder, containerSize: containerSize, @@ -554,6 +585,7 @@ final class VideoFinishPass: RenderPass { containerSize: containerSize, texture: transitionVideoState.texture, textureRotation: transitionVideoState.textureRotation, + maskTexture: nil, position: transitionVideoState.position, roundness: transitionVideoState.roundness, alpha: transitionVideoState.alpha, @@ -567,6 +599,7 @@ final class VideoFinishPass: RenderPass { containerSize: containerSize, texture: mainVideoState.texture, textureRotation: mainVideoState.textureRotation, + maskTexture: inputMask, position: mainVideoState.position, roundness: mainVideoState.roundness, alpha: mainVideoState.alpha, @@ -580,6 +613,7 @@ final class VideoFinishPass: RenderPass { containerSize: containerSize, texture: additionalVideoState.texture, textureRotation: additionalVideoState.textureRotation, + maskTexture: nil, position: additionalVideoState.position, roundness: additionalVideoState.roundness, alpha: additionalVideoState.alpha, @@ -603,6 +637,7 @@ final class VideoFinishPass: RenderPass { containerSize: CGSize, device: MTLDevice ) { + encoder.setRenderPipelineState(self.gradientPipelineState!) let vertices = verticesDataForRotation(.rotate0Degrees) let buffer = device.makeBuffer( diff --git a/submodules/TelegramUI/Components/MediaEditorScreen/BUILD b/submodules/TelegramUI/Components/MediaEditorScreen/BUILD index 728aedc52c..88ab5ba5ef 100644 --- a/submodules/TelegramUI/Components/MediaEditorScreen/BUILD +++ b/submodules/TelegramUI/Components/MediaEditorScreen/BUILD @@ -51,6 +51,7 @@ swift_library( "//submodules/TelegramUI/Components/ContextReferenceButtonComponent", "//submodules/TelegramUI/Components/MediaScrubberComponent", "//submodules/Components/BlurredBackgroundComponent", + "//submodules/TelegramUI/Components/DustEffect", ], visibility = [ "//visibility:public", diff --git a/submodules/TelegramUI/Components/MediaEditorScreen/Sources/MediaCutoutScreen.swift b/submodules/TelegramUI/Components/MediaEditorScreen/Sources/MediaCutoutScreen.swift index 2e411f9cf7..e1b3c046cc 100644 --- a/submodules/TelegramUI/Components/MediaEditorScreen/Sources/MediaCutoutScreen.swift +++ b/submodules/TelegramUI/Components/MediaEditorScreen/Sources/MediaCutoutScreen.swift @@ -15,6 +15,7 @@ import MediaEditor import Photos import LottieAnimationComponent import MessageInputPanelComponent +import DustEffect private final class MediaCutoutScreenComponent: Component { typealias EnvironmentType = ViewControllerComponentContainer.Environment @@ -40,9 +41,13 @@ private final class MediaCutoutScreenComponent: Component { public final class View: UIView { private let buttonsContainerView = UIView() private let buttonsBackgroundView = UIView() + private let previewContainerView = UIView() private let cancelButton = ComponentView() private let label = ComponentView() private let doneButton = ComponentView() + + private let fadeView = UIView() + private let separatedImageView = UIImageView() private var component: MediaCutoutScreenComponent? private weak var state: State? @@ -51,18 +56,44 @@ private final class MediaCutoutScreenComponent: Component { override init(frame: CGRect) { self.buttonsContainerView.clipsToBounds = true + self.fadeView.alpha = 0.0 + self.fadeView.backgroundColor = UIColor(rgb: 0x000000, alpha: 0.6) + + self.separatedImageView.contentMode = .scaleAspectFit + super.init(frame: frame) self.backgroundColor = .clear self.addSubview(self.buttonsContainerView) self.buttonsContainerView.addSubview(self.buttonsBackgroundView) + + self.addSubview(self.fadeView) + self.addSubview(self.separatedImageView) + self.addSubview(self.previewContainerView) + + self.previewContainerView.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.previewTap(_:)))) } required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } + @objc private func previewTap(_ gestureRecognizer: UITapGestureRecognizer) { + guard let component = self.component else { + return + } + let location = gestureRecognizer.location(in: gestureRecognizer.view) + + let point = CGPoint( + x: location.x / self.previewContainerView.frame.width, + y: location.y / self.previewContainerView.frame.height + ) + component.mediaEditor.setSeparationMask(point: point) + + self.playDissolveAnimation() + } + func animateInFromEditor() { self.buttonsBackgroundView.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2) self.label.view?.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2) @@ -74,6 +105,7 @@ private final class MediaCutoutScreenComponent: Component { self.cancelButton.view?.isHidden = true + self.fadeView.layer.animateAlpha(from: self.fadeView.alpha, to: 0.0, duration: 0.2, removeOnCompletion: false) self.buttonsBackgroundView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false, completion: { _ in completion() }) @@ -82,14 +114,35 @@ private final class MediaCutoutScreenComponent: Component { self.state?.updated() } + public func playDissolveAnimation() { + guard let component = self.component, let resultImage = component.mediaEditor.resultImage, let environment = self.environment, let controller = environment.controller() as? MediaCutoutScreen else { + return + } + let previewView = controller.previewView + + let dustEffectLayer = DustEffectLayer() + dustEffectLayer.position = previewView.center + dustEffectLayer.bounds = previewView.bounds + previewView.superview?.layer.insertSublayer(dustEffectLayer, below: previewView.layer) + + dustEffectLayer.animationSpeed = 2.2 + dustEffectLayer.becameEmpty = { [weak dustEffectLayer] in + dustEffectLayer?.removeFromSuperlayer() + } + + dustEffectLayer.addItem(frame: previewView.bounds, image: resultImage) + + controller.requestDismiss(animated: true) + } + func update(component: MediaCutoutScreenComponent, availableSize: CGSize, state: State, environment: Environment, transition: Transition) -> CGSize { let environment = environment[ViewControllerComponentContainer.Environment.self].value self.environment = environment + let isFirstTime = self.component == nil self.component = component self.state = state -// let presentationData = component.context.sharedContext.currentPresentationData.with { $0 } let isTablet: Bool if case .regular = environment.metrics.widthClass { isTablet = true @@ -97,8 +150,6 @@ private final class MediaCutoutScreenComponent: Component { isTablet = false } -// let mediaEditor = (environment.controller() as? MediaCutoutScreen)?.mediaEditor - let buttonSideInset: CGFloat let buttonBottomInset: CGFloat = 8.0 var controlsBottomInset: CGFloat = 0.0 @@ -119,7 +170,7 @@ private final class MediaCutoutScreenComponent: Component { } } -// var previewContainerFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((availableSize.width - previewSize.width) / 2.0), y: environment.safeInsets.top), size: CGSize(width: previewSize.width, height: availableSize.height - environment.safeInsets.top - environment.safeInsets.bottom + controlsBottomInset)) + let previewContainerFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((availableSize.width - previewSize.width) / 2.0), y: environment.safeInsets.top), size: CGSize(width: previewSize.width, height: availableSize.height - environment.safeInsets.top - environment.safeInsets.bottom + controlsBottomInset)) let buttonsContainerFrame = CGRect(origin: CGPoint(x: 0.0, y: availableSize.height - environment.safeInsets.bottom + controlsBottomInset), size: CGSize(width: availableSize.width, height: environment.safeInsets.bottom - controlsBottomInset)) let cancelButtonSize = self.cancelButton.update( @@ -140,7 +191,7 @@ private final class MediaCutoutScreenComponent: Component { guard let controller = environment.controller() as? MediaCutoutScreen else { return } - controller.requestDismiss(reset: true, animated: true) + controller.requestDismiss(animated: true) } )), environment: {}, @@ -177,6 +228,30 @@ private final class MediaCutoutScreenComponent: Component { transition.setFrame(view: self.buttonsContainerView, frame: buttonsContainerFrame) transition.setFrame(view: self.buttonsBackgroundView, frame: CGRect(origin: .zero, size: buttonsContainerFrame.size)) + transition.setFrame(view: self.previewContainerView, frame: previewContainerFrame) + transition.setFrame(view: self.separatedImageView, frame: previewContainerFrame) + + let frameWidth = floor(previewContainerFrame.width * 0.97) + + self.fadeView.frame = CGRect(x: floorToScreenPixels((previewContainerFrame.width - frameWidth) / 2.0), y: previewContainerFrame.minY + floorToScreenPixels((previewContainerFrame.height - frameWidth) / 2.0), width: frameWidth, height: frameWidth) + self.fadeView.layer.cornerRadius = frameWidth / 8.0 + + if isFirstTime { + let _ = (component.mediaEditor.getSeparatedImage(point: nil) + |> deliverOnMainQueue).start(next: { [weak self] image in + guard let self else { + return + } + self.separatedImageView.image = image + self.state?.updated(transition: .easeInOut(duration: 0.2)) + }) + } else { + if let _ = self.separatedImageView.image { + transition.setAlpha(view: self.fadeView, alpha: 1.0) + } else { + transition.setAlpha(view: self.fadeView, alpha: 0.0) + } + } return availableSize } } @@ -315,14 +390,16 @@ public final class MediaCutoutScreen: ViewController { fileprivate let context: AccountContext fileprivate let mediaEditor: MediaEditor + fileprivate let previewView: MediaEditorPreviewView public var dismissed: () -> Void = {} private var initialValues: MediaEditorValues - public init(context: AccountContext, mediaEditor: MediaEditor) { + public init(context: AccountContext, mediaEditor: MediaEditor, previewView: MediaEditorPreviewView) { self.context = context self.mediaEditor = mediaEditor + self.previewView = previewView self.initialValues = mediaEditor.values.makeCopy() super.init(navigationBarPresentationData: nil) @@ -343,11 +420,7 @@ public final class MediaCutoutScreen: ViewController { super.displayNodeDidLoad() } - func requestDismiss(reset: Bool, animated: Bool) { - if reset { - self.mediaEditor.values = self.initialValues - } - + func requestDismiss(animated: Bool) { self.dismissed() self.node.animateOutToEditor(completion: { diff --git a/submodules/TelegramUI/Components/MediaEditorScreen/Sources/MediaEditorScreen.swift b/submodules/TelegramUI/Components/MediaEditorScreen/Sources/MediaEditorScreen.swift index 2f078dad8e..abae6ccac6 100644 --- a/submodules/TelegramUI/Components/MediaEditorScreen/Sources/MediaEditorScreen.swift +++ b/submodules/TelegramUI/Components/MediaEditorScreen/Sources/MediaEditorScreen.swift @@ -154,6 +154,7 @@ final class MediaEditorScreenComponent: Component { case tools case done case cutout + case undo } private var cachedImages: [ImageKey: UIImage] = [:] func image(_ key: ImageKey) -> UIImage { @@ -172,6 +173,8 @@ final class MediaEditorScreenComponent: Component { image = generateTintedImage(image: UIImage(bundleImageName: "Media Editor/Tools"), color: .white)! case .cutout: image = generateTintedImage(image: UIImage(bundleImageName: "Media Editor/Cutout"), color: .white)! + case .undo: + image = generateTintedImage(image: UIImage(bundleImageName: "Media Editor/CutoutUndo"), color: .white)! case .done: image = generateImage(CGSize(width: 33.0, height: 33.0), rotatedContext: { size, context in context.clear(CGRect(origin: CGPoint(), size: size)) @@ -981,13 +984,14 @@ final class MediaEditorScreenComponent: Component { } if controller.node.canCutout { + let isCutout = controller.node.isCutout let cutoutButtonSize = self.cutoutButton.update( transition: transition, component: AnyComponent(PlainButtonComponent( content: AnyComponent(CutoutButtonContentComponent( backgroundColor: UIColor(rgb: 0xffffff, alpha: 0.18), - icon: state.image(.cutout), - title: "Cut Out an Object" + icon: state.image(isCutout ? .undo : .cutout), + title: isCutout ? "Undo Cut Out" : "Cut Out an Object" )), effectAlignment: .center, action: { @@ -2161,6 +2165,7 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate private var isDismissBySwipeSuppressed = false fileprivate var canCutout = false + fileprivate var isCutout = false private (set) var hasAnyChanges = false @@ -2513,7 +2518,13 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate self.canCutout = canCutout controller.requestLayout(transition: .animated(duration: 0.25, curve: .easeInOut)) } - + mediaEditor.isCutoutUpdated = { [weak self] isCutout in + guard let self else { + return + } + self.isCutout = isCutout + self.requestLayout(forceUpdate: true, transition: .immediate) + } if case .message = effectiveSubject { } else { @@ -4231,14 +4242,25 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate self.entitiesView.selectEntity(nil) } - let controller = MediaCutoutScreen(context: self.context, mediaEditor: mediaEditor) - controller.dismissed = { [weak self] in - if let self { - self.animateInFromTool(inPlace: true) + if controller.node.isCutout { + let snapshotView = self.previewView.snapshotView(afterScreenUpdates: false) + if let snapshotView { + self.previewView.superview?.addSubview(snapshotView) } + self.previewView.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.3, completion: { _ in + snapshotView?.removeFromSuperview() + }) + mediaEditor.removeSeparationMask() + } else { + let controller = MediaCutoutScreen(context: self.context, mediaEditor: mediaEditor, previewView: self.previewView) + controller.dismissed = { [weak self] in + if let self { + self.animateInFromTool(inPlace: true) + } + } + self.controller?.present(controller, in: .window(.root)) + self.animateOutToTool(inPlace: true) } - self.controller?.present(controller, in: .window(.root)) - self.animateOutToTool(inPlace: true) } } ) @@ -5084,35 +5106,46 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate } let presentationData = self.context.sharedContext.currentPresentationData.with { $0 } - let title: String - let save: String - if case .draft = self.node.actualSubject { - title = presentationData.strings.Story_Editor_DraftDiscardDraft - save = presentationData.strings.Story_Editor_DraftKeepDraft - } else { + var title: String + var text: String + var save: String? + switch self.mode { + case .storyEditor: + if case .draft = self.node.actualSubject { + title = presentationData.strings.Story_Editor_DraftDiscardDraft + save = presentationData.strings.Story_Editor_DraftKeepDraft + } else { + title = presentationData.strings.Story_Editor_DraftDiscardMedia + save = presentationData.strings.Story_Editor_DraftKeepMedia + } + text = presentationData.strings.Story_Editor_DraftDiscaedText + case .stickerEditor: title = presentationData.strings.Story_Editor_DraftDiscardMedia - save = presentationData.strings.Story_Editor_DraftKeepMedia + text = presentationData.strings.Story_Editor_DiscardText } + + var actions: [TextAlertAction] = [] + actions.append(TextAlertAction(type: .destructiveAction, title: presentationData.strings.Story_Editor_DraftDiscard, action: { [weak self] in + if let self { + self.requestDismiss(saveDraft: false, animated: true) + } + })) + if let save { + actions.append(TextAlertAction(type: .genericAction, title: save, action: { [weak self] in + if let self { + self.requestDismiss(saveDraft: true, animated: true) + } + })) + } + actions.append(TextAlertAction(type: .genericAction, title: presentationData.strings.Common_Cancel, action: { + + })) let controller = textAlertController( context: self.context, forceTheme: defaultDarkPresentationTheme, title: title, - text: presentationData.strings.Story_Editor_DraftDiscaedText, - actions: [ - TextAlertAction(type: .destructiveAction, title: presentationData.strings.Story_Editor_DraftDiscard, action: { [weak self] in - if let self { - self.requestDismiss(saveDraft: false, animated: true) - } - }), - TextAlertAction(type: .genericAction, title: save, action: { [weak self] in - if let self { - self.requestDismiss(saveDraft: true, animated: true) - } - }), - TextAlertAction(type: .genericAction, title: presentationData.strings.Common_Cancel, action: { - - }) - ], + text: text, + actions: actions, actionLayout: .vertical ) self.present(controller, in: .window(.root)) diff --git a/submodules/TelegramUI/Images.xcassets/Media Editor/CutoutUndo.imageset/Contents.json b/submodules/TelegramUI/Images.xcassets/Media Editor/CutoutUndo.imageset/Contents.json new file mode 100644 index 0000000000..4a8e6450d0 --- /dev/null +++ b/submodules/TelegramUI/Images.xcassets/Media Editor/CutoutUndo.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "undo2_30.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/submodules/TelegramUI/Images.xcassets/Media Editor/CutoutUndo.imageset/undo2_30.pdf b/submodules/TelegramUI/Images.xcassets/Media Editor/CutoutUndo.imageset/undo2_30.pdf new file mode 100644 index 0000000000..e814c8183d --- /dev/null +++ b/submodules/TelegramUI/Images.xcassets/Media Editor/CutoutUndo.imageset/undo2_30.pdf @@ -0,0 +1,126 @@ +%PDF-1.7 + +1 0 obj + << >> +endobj + +2 0 obj + << /Length 3 0 R >> +stream +/DeviceRGB CS +/DeviceRGB cs +q +1.000000 0.000000 -0.000000 1.000000 5.000000 4.258789 cm +0.000000 0.000000 0.000000 scn +1.000000 2.571211 m +0.541604 2.571211 0.170000 2.199607 0.170000 1.741211 c +0.170000 1.282815 0.541604 0.911211 1.000000 0.911211 c +1.000000 2.571211 l +h +1.000000 13.571211 m +0.541604 13.571211 0.170000 13.199608 0.170000 12.741211 c +0.170000 12.282814 0.541604 11.911211 1.000000 11.911211 c +1.000000 13.571211 l +h +3.413101 8.154312 m +3.737236 7.830177 4.262764 7.830177 4.586899 8.154312 c +4.911034 8.478448 4.911034 9.003975 4.586899 9.328110 c +3.413101 8.154312 l +h +0.000000 12.741211 m +-0.586899 13.328110 l +-0.911034 13.003975 -0.911034 12.478447 -0.586899 12.154312 c +0.000000 12.741211 l +h +4.586899 16.154312 m +4.911034 16.478447 4.911034 17.003975 4.586899 17.328110 c +4.262764 17.652245 3.737236 17.652245 3.413101 17.328110 c +4.586899 16.154312 l +h +1.000000 0.911211 m +8.500000 0.911211 l +8.500000 2.571211 l +1.000000 2.571211 l +1.000000 0.911211 l +h +8.500000 13.571211 m +1.000000 13.571211 l +1.000000 11.911211 l +8.500000 11.911211 l +8.500000 13.571211 l +h +4.586899 9.328110 m +0.586899 13.328110 l +-0.586899 12.154312 l +3.413101 8.154312 l +4.586899 9.328110 l +h +0.586899 12.154312 m +4.586899 16.154312 l +3.413101 17.328110 l +-0.586899 13.328110 l +0.586899 12.154312 l +h +14.830000 7.241211 m +14.830000 10.737173 11.995962 13.571211 8.500000 13.571211 c +8.500000 11.911211 l +11.079169 11.911211 13.170000 9.820381 13.170000 7.241211 c +14.830000 7.241211 l +h +8.500000 0.911211 m +11.995962 0.911211 14.830000 3.745249 14.830000 7.241211 c +13.170000 7.241211 l +13.170000 4.662042 11.079169 2.571211 8.500000 2.571211 c +8.500000 0.911211 l +h +f +n +Q + +endstream +endobj + +3 0 obj + 1673 +endobj + +4 0 obj + << /Annots [] + /Type /Page + /MediaBox [ 0.000000 0.000000 24.000000 24.000000 ] + /Resources 1 0 R + /Contents 2 0 R + /Parent 5 0 R + >> +endobj + +5 0 obj + << /Kids [ 4 0 R ] + /Count 1 + /Type /Pages + >> +endobj + +6 0 obj + << /Pages 5 0 R + /Type /Catalog + >> +endobj + +xref +0 7 +0000000000 65535 f +0000000010 00000 n +0000000034 00000 n +0000001763 00000 n +0000001786 00000 n +0000001959 00000 n +0000002033 00000 n +trailer +<< /ID [ (some) (id) ] + /Root 6 0 R + /Size 7 +>> +startxref +2092 +%%EOF \ No newline at end of file