diff --git a/Telegram/Telegram-iOS/en.lproj/Localizable.strings b/Telegram/Telegram-iOS/en.lproj/Localizable.strings index 9f6cbd776e..06999e3f6a 100644 --- a/Telegram/Telegram-iOS/en.lproj/Localizable.strings +++ b/Telegram/Telegram-iOS/en.lproj/Localizable.strings @@ -9345,3 +9345,11 @@ Sorry for the inconvenience."; "ChatList.PremiumRestoreDiscountText" = "Your Telegram Premium has recently expired. Tap here to extend it."; "Login.ErrorAppOutdated" = "Please update Telegram to the latest version to log in."; + +"Login.GetCodeViaFragment" = "Get a code via Fragment"; + +"Privacy.Bio" = "Bio"; +"Privacy.Bio.WhoCanSeeMyBio" = "WHO CAN SEE MY BIO"; +"Privacy.Bio.CustomHelp" = "You can restrict who can see your profile bio with granular precision."; +"Privacy.Bio.AlwaysShareWith.Title" = "Always Share With"; +"Privacy.Bio.NeverShareWith.Title" = "Never Share With"; diff --git a/submodules/AuthorizationUtils/Sources/AuthorizationOptionText.swift b/submodules/AuthorizationUtils/Sources/AuthorizationOptionText.swift index dc26d51911..fe2248742a 100644 --- a/submodules/AuthorizationUtils/Sources/AuthorizationOptionText.swift +++ b/submodules/AuthorizationUtils/Sources/AuthorizationOptionText.swift @@ -87,7 +87,7 @@ public func authorizationNextOptionText(currentType: SentAuthorizationCodeType, case .flashCall, .missedCall: return (NSAttributedString(string: strings.Login_SendCodeViaFlashCall, font: Font.regular(16.0), textColor: accentColor, paragraphAlignment: .center), true) case .fragment: - return (NSAttributedString(string: "Send code via fragment", font: Font.regular(16.0), textColor: accentColor, paragraphAlignment: .center), true) + return (NSAttributedString(string: strings.Login_GetCodeViaFragment, font: Font.regular(16.0), textColor: accentColor, paragraphAlignment: .center), true) case .none: return (NSAttributedString(string: strings.Login_HaveNotReceivedCodeInternal, font: Font.regular(16.0), textColor: accentColor, paragraphAlignment: .center), true) } @@ -100,7 +100,7 @@ public func authorizationNextOptionText(currentType: SentAuthorizationCodeType, case .flashCall, .missedCall: return (NSAttributedString(string: strings.Login_SendCodeViaFlashCall, font: Font.regular(16.0), textColor: accentColor, paragraphAlignment: .center), true) case .fragment: - return (NSAttributedString(string: "Send code via fragment", font: Font.regular(16.0), textColor: accentColor, paragraphAlignment: .center), true) + return (NSAttributedString(string: strings.Login_GetCodeViaFragment, font: Font.regular(16.0), textColor: accentColor, paragraphAlignment: .center), true) case .none: return (NSAttributedString(string: strings.Login_HaveNotReceivedCodeInternal, font: Font.regular(16.0), textColor: accentColor, paragraphAlignment: .center), true) } diff --git a/submodules/Camera/Sources/Camera.swift b/submodules/Camera/Sources/Camera.swift index e0e7f1e294..a205b8221c 100644 --- a/submodules/Camera/Sources/Camera.swift +++ b/submodules/Camera/Sources/Camera.swift @@ -4,7 +4,7 @@ import AVFoundation private final class CameraContext { private let queue: Queue - private let session = AVCaptureSession() + private let session: AVCaptureSession private let device: CameraDevice private let input = CameraInput() private let output = CameraOutput() @@ -27,20 +27,32 @@ private final class CameraContext { } } - private let filter = CameraTestFilter() + var simplePreviewView: CameraSimplePreviewView? { + didSet { + if let oldValue { + Queue.mainQueue().async { + oldValue.session = nil + self.simplePreviewView?.session = self.session + } + } + } + } private var videoOrientation: AVCaptureVideoOrientation? - init(queue: Queue, configuration: Camera.Configuration, metrics: Camera.Metrics) { + init(queue: Queue, session: AVCaptureSession, configuration: Camera.Configuration, metrics: Camera.Metrics, previewView: CameraSimplePreviewView?) { self.queue = queue + self.session = session self.initialConfiguration = configuration + self.simplePreviewView = previewView self.device = CameraDevice() - self.device.configure(for: self.session, position: configuration.position) - self.configure { - self.session.sessionPreset = configuration.preset + self.device.configure(for: self.session, position: configuration.position) self.input.configure(for: self.session, device: self.device, audio: configuration.audio) self.output.configure(for: self.session, configuration: configuration) + + self.device.configureDeviceFormat(maxDimensions: CMVideoDimensions(width: 1920, height: 1080), maxFramerate: 60) + self.output.configureVideoStabilization() } self.output.processSampleBuffer = { [weak self] pixelBuffer, connection in @@ -126,7 +138,9 @@ private final class CameraContext { self.changingPosition = true self.device.configure(for: self.session, position: targetPosition) self.input.configure(for: self.session, device: self.device, audio: self.initialConfiguration.audio) - self.queue.after(0.7) { + self.device.configureDeviceFormat(maxDimensions: CMVideoDimensions(width: 1920, height: 1080), maxFramerate: 60) + self.output.configureVideoStabilization() + self.queue.after(0.5) { self.changingPosition = false } } @@ -137,6 +151,8 @@ private final class CameraContext { self.input.invalidate(for: self.session) self.device.configure(for: self.session, position: position) self.input.configure(for: self.session, device: self.device, audio: self.initialConfiguration.audio) + self.device.configureDeviceFormat(maxDimensions: CMVideoDimensions(width: 1920, height: 1080), maxFramerate: 60) + self.output.configureVideoStabilization() } } @@ -170,14 +186,6 @@ private final class CameraContext { func setFlashMode(_ mode: Camera.FlashMode) { self._flashMode = mode - -// if mode == .on { -// self.output.faceLandmarks = true -// //self.output.activeFilter = self.filter -// } else if mode == .off { -// self.output.faceLandmarks = false -// //self.output.activeFilter = nil -// } } func setZoomLevel(_ zoomLevel: CGFloat) { @@ -185,7 +193,7 @@ private final class CameraContext { } func takePhoto() -> Signal { - return self.output.takePhoto(orientation: self.videoOrientation ?? .portrait, flashMode: .off) //self._flashMode) + return self.output.takePhoto(orientation: self.videoOrientation ?? .portrait, flashMode: self._flashMode) } public func startRecording() -> Signal { @@ -214,13 +222,15 @@ public final class Camera { let audio: Bool let photo: Bool let metadata: Bool + let preferredFps: Double - public init(preset: Preset, position: Position, audio: Bool, photo: Bool, metadata: Bool) { + public init(preset: Preset, position: Position, audio: Bool, photo: Bool, metadata: Bool, preferredFps: Double) { self.preset = preset self.position = position self.audio = audio self.photo = photo self.metadata = metadata + self.preferredFps = preferredFps } } @@ -231,11 +241,16 @@ public final class Camera { public let metrics: Camera.Metrics - public init(configuration: Camera.Configuration = Configuration(preset: .hd1920x1080, position: .back, audio: true, photo: false, metadata: false)) { + public init(configuration: Camera.Configuration = Configuration(preset: .hd1920x1080, position: .back, audio: true, photo: false, metadata: false, preferredFps: 60.0), previewView: CameraSimplePreviewView? = nil) { self.metrics = Camera.Metrics(model: DeviceModel.current) + let session = AVCaptureSession() + if let previewView { + previewView.session = session + } + self.queue.async { - let context = CameraContext(queue: self.queue, configuration: configuration, metrics: self.metrics) + let context = CameraContext(queue: self.queue, session: session, configuration: configuration, metrics: self.metrics, previewView: previewView) self.contextRef = Unmanaged.passRetained(context) } } @@ -443,7 +458,21 @@ public final class Camera { } } } - + + public func attachSimplePreviewView(_ view: CameraSimplePreviewView) { + let viewRef: Unmanaged = Unmanaged.passRetained(view) + self.queue.async { + if let context = self.contextRef?.takeUnretainedValue() { + context.simplePreviewView = viewRef.takeUnretainedValue() + viewRef.release() + } else { + Queue.mainQueue().async { + viewRef.release() + } + } + } + } + public var detectedCodes: Signal<[CameraCode], NoError> { return Signal { subscriber in let disposable = MetaDisposable() diff --git a/submodules/Camera/Sources/CameraDevice.swift b/submodules/Camera/Sources/CameraDevice.swift index a994590ff7..182017f167 100644 --- a/submodules/Camera/Sources/CameraDevice.swift +++ b/submodules/Camera/Sources/CameraDevice.swift @@ -25,6 +25,58 @@ final class CameraDevice { self.audioDevice = AVCaptureDevice.default(for: .audio) } + func configureDeviceFormat(maxDimensions: CMVideoDimensions, maxFramerate: Double) { + guard let device = self.videoDevice else { + return + } + self.transaction(device) { device in + var maxWidth: Int32 = 0 + var maxHeight: Int32 = 0 + var hasSecondaryZoomLevels = false + var candidates: [AVCaptureDevice.Format] = [] + outer: for format in device.formats { + if format.mediaType != .video || format.value(forKey: "isPhotoFormat") as? Bool == true { + continue + } + + let dimensions = CMVideoFormatDescriptionGetDimensions(format.formatDescription) + if dimensions.width >= maxWidth && dimensions.width <= maxDimensions.width && dimensions.height >= maxHeight && dimensions.height <= maxDimensions.height { + if dimensions.width > maxWidth { + hasSecondaryZoomLevels = false + candidates.removeAll() + } + let subtype = CMFormatDescriptionGetMediaSubType(format.formatDescription) + if subtype == kCVPixelFormatType_420YpCbCr8BiPlanarVideoRange { + for range in format.videoSupportedFrameRateRanges { + if range.maxFrameRate > maxFramerate { + continue outer + } + } + + maxWidth = dimensions.width + maxHeight = dimensions.height + + if #available(iOS 16.0, *), !format.secondaryNativeResolutionZoomFactors.isEmpty { + hasSecondaryZoomLevels = true + candidates.append(format) + } else if !hasSecondaryZoomLevels { + candidates.append(format) + } + } + } + } + + if let bestFormat = candidates.last { + device.activeFormat = bestFormat + } + + if let targetFPS = device.actualFPS(maxFramerate) { + device.activeVideoMinFrameDuration = targetFPS.duration + device.activeVideoMaxFrameDuration = targetFPS.duration + } + } + } + func transaction(_ device: AVCaptureDevice, update: (AVCaptureDevice) -> Void) { if let _ = try? device.lockForConfiguration() { update(device) diff --git a/submodules/Camera/Sources/CameraFilter.swift b/submodules/Camera/Sources/CameraFilter.swift index 6c9fb92386..ab6a518268 100644 --- a/submodules/Camera/Sources/CameraFilter.swift +++ b/submodules/Camera/Sources/CameraFilter.swift @@ -113,7 +113,6 @@ class CameraTestFilter: CameraFilter { private(set) var inputFormatDescription: CMFormatDescription? - /// - Tag: FilterCoreImageRosy func prepare(with formatDescription: CMFormatDescription, outputRetainedBufferCountHint: Int) { reset() @@ -149,7 +148,6 @@ class CameraTestFilter: CameraFilter { guard let rosyFilter = rosyFilter, let ciContext = ciContext, isPrepared else { - assertionFailure("Invalid state: Not prepared") return nil } @@ -157,18 +155,15 @@ class CameraTestFilter: CameraFilter { rosyFilter.setValue(sourceImage, forKey: kCIInputImageKey) guard let filteredImage = rosyFilter.value(forKey: kCIOutputImageKey) as? CIImage else { - print("CIFilter failed to render image") return nil } var pbuf: CVPixelBuffer? CVPixelBufferPoolCreatePixelBuffer(kCFAllocatorDefault, outputPixelBufferPool!, &pbuf) guard let outputPixelBuffer = pbuf else { - print("Allocation failure") return nil } - // Render the filtered image out to a pixel buffer (no locking needed, as CIContext's render method will do that) ciContext.render(filteredImage, to: outputPixelBuffer, bounds: filteredImage.extent, colorSpace: outputColorSpace) return outputPixelBuffer } diff --git a/submodules/Camera/Sources/CameraOutput.swift b/submodules/Camera/Sources/CameraOutput.swift index 85a78c1f10..6372cc0bb3 100644 --- a/submodules/Camera/Sources/CameraOutput.swift +++ b/submodules/Camera/Sources/CameraOutput.swift @@ -64,9 +64,9 @@ final class CameraOutput: NSObject { override init() { super.init() - self.videoOutput.alwaysDiscardsLateVideoFrames = true; - self.videoOutput.videoSettings = [kCVPixelBufferPixelFormatTypeKey: kCVPixelFormatType_32BGRA] as [String : Any] - //[kCVPixelBufferPixelFormatTypeKey: kCVPixelFormatType_420YpCbCr8BiPlanarVideoRange] as [String : Any] + self.videoOutput.alwaysDiscardsLateVideoFrames = false + //self.videoOutput.videoSettings = [kCVPixelBufferPixelFormatTypeKey: kCVPixelFormatType_32BGRA] as [String : Any] + self.videoOutput.videoSettings = [kCVPixelBufferPixelFormatTypeKey: kCVPixelFormatType_420YpCbCr8BiPlanarVideoRange] as [String : Any] self.faceLandmarksOutput.outputFaceObservations = { [weak self] observations in if let self { @@ -108,6 +108,16 @@ final class CameraOutput: NSObject { } } + func configureVideoStabilization() { + if let videoDataOutputConnection = self.videoOutput.connection(with: .video), videoDataOutputConnection.isVideoStabilizationSupported { + if #available(iOS 13.0, *) { + videoDataOutputConnection.preferredVideoStabilizationMode = .cinematicExtended + } else { + videoDataOutputConnection.preferredVideoStabilizationMode = .cinematic + } + } + } + var isFlashActive: Signal { return Signal { [weak self] subscriber in guard let self else { @@ -215,26 +225,26 @@ extension CameraOutput: AVCaptureVideoDataOutputSampleBufferDelegate, AVCaptureA } } - let finalSampleBuffer: CMSampleBuffer = sampleBuffer - if let videoPixelBuffer = CMSampleBufferGetImageBuffer(sampleBuffer), let formatDescription = CMSampleBufferGetFormatDescription(sampleBuffer) { - var finalVideoPixelBuffer = videoPixelBuffer - if let filter = self.activeFilter { - if !filter.isPrepared { - filter.prepare(with: formatDescription, outputRetainedBufferCountHint: 3) - } - - guard let filteredBuffer = filter.render(pixelBuffer: finalVideoPixelBuffer) else { - return - } - finalVideoPixelBuffer = filteredBuffer - } - self.processSampleBuffer?(finalVideoPixelBuffer, connection) - } +// let finalSampleBuffer: CMSampleBuffer = sampleBuffer +// if let videoPixelBuffer = CMSampleBufferGetImageBuffer(sampleBuffer), let formatDescription = CMSampleBufferGetFormatDescription(sampleBuffer) { +// var finalVideoPixelBuffer = videoPixelBuffer +// if let filter = self.activeFilter { +// if !filter.isPrepared { +// filter.prepare(with: formatDescription, outputRetainedBufferCountHint: 3) +// } +// +// guard let filteredBuffer = filter.render(pixelBuffer: finalVideoPixelBuffer) else { +// return +// } +// finalVideoPixelBuffer = filteredBuffer +// } +// self.processSampleBuffer?(finalVideoPixelBuffer, connection) +// } if let videoRecorder = self.videoRecorder, videoRecorder.isRecording || videoRecorder.isStopping { let mediaType = sampleBuffer.type if mediaType == kCMMediaType_Video { - videoRecorder.appendVideo(sampleBuffer: finalSampleBuffer) + videoRecorder.appendVideo(sampleBuffer: sampleBuffer) } else if mediaType == kCMMediaType_Audio { videoRecorder.appendAudio(sampleBuffer: sampleBuffer) } diff --git a/submodules/Camera/Sources/CameraPreviewView.swift b/submodules/Camera/Sources/CameraPreviewView.swift index 4183f23271..0c326a8c64 100644 --- a/submodules/Camera/Sources/CameraPreviewView.swift +++ b/submodules/Camera/Sources/CameraPreviewView.swift @@ -8,6 +8,51 @@ import MetalKit import CoreMedia import Vision +public class CameraSimplePreviewView: UIView { + var videoPreviewLayer: AVCaptureVideoPreviewLayer { + guard let layer = layer as? AVCaptureVideoPreviewLayer else { + fatalError() + } + return layer + } + + var session: AVCaptureSession? { + get { + return self.videoPreviewLayer.session + } + set { + self.videoPreviewLayer.session = newValue + } + } + + public var isEnabled: Bool = true { + didSet { + self.videoPreviewLayer.connection?.isEnabled = self.isEnabled + } + } + + public override class var layerClass: AnyClass { + return AVCaptureVideoPreviewLayer.self + } + + @available(iOS 13.0, *) + public var isPreviewing: Signal { + return Signal { [weak self] subscriber in + guard let self else { + return EmptyDisposable + } + subscriber.putNext(self.videoPreviewLayer.isPreviewing) + let observer = self.videoPreviewLayer.observe(\.isPreviewing, options: [.new], changeHandler: { view, _ in + subscriber.putNext(view.isPreviewing) + }) + return ActionDisposable { + observer.invalidate() + } + } + |> distinctUntilChanged + } +} + public class CameraPreviewView: MTKView { private let queue = DispatchQueue(label: "CameraPreview", qos: .userInitiated, attributes: [], autoreleaseFrequency: .workItem) private let commandQueue: MTLCommandQueue diff --git a/submodules/Camera/Sources/VideoRecorder.swift b/submodules/Camera/Sources/VideoRecorder.swift index 9d9b14e79d..fb9eab210a 100644 --- a/submodules/Camera/Sources/VideoRecorder.swift +++ b/submodules/Camera/Sources/VideoRecorder.swift @@ -12,7 +12,8 @@ struct MediaPreset { } var hasAudio: Bool { - return !self.audioSettings.isEmpty + return false + //return !self.audioSettings.isEmpty } } @@ -132,7 +133,12 @@ final class VideoRecorder { } } + private var skippedCount = 0 func appendVideo(sampleBuffer: CMSampleBuffer) { + if self.skippedCount < 2 { + self.skippedCount += 1 + return + } self.queue.async { guard let assetWriter = self.assetWriter, let videoInput = self.videoInput, (self.isRecording || self.isStopping) && !self.finishedWriting else { return diff --git a/submodules/ComponentFlow/Source/Base/Transition.swift b/submodules/ComponentFlow/Source/Base/Transition.swift index 16dbadebcb..9c68843c30 100644 --- a/submodules/ComponentFlow/Source/Base/Transition.swift +++ b/submodules/ComponentFlow/Source/Base/Transition.swift @@ -739,6 +739,34 @@ public struct Transition { } } + public func setShapeLayerFillColor(layer: CAShapeLayer, color: UIColor, completion: ((Bool) -> Void)? = nil) { + if let current = layer.layerTintColor, current == color.cgColor { + completion?(true) + return + } + + switch self.animation { + case .none: + layer.fillColor = color.cgColor + completion?(true) + case let .curve(duration, curve): + let previousColor: CGColor = layer.fillColor ?? UIColor.clear.cgColor + layer.fillColor = color.cgColor + + layer.animate( + from: previousColor, + to: color.cgColor, + keyPath: "fillColor", + duration: duration, + delay: 0.0, + curve: curve, + removeOnCompletion: true, + additive: false, + completion: completion + ) + } + } + public func setBackgroundColor(view: UIView, color: UIColor, completion: ((Bool) -> Void)? = nil) { self.setBackgroundColor(layer: view.layer, color: color, completion: completion) } diff --git a/submodules/ComponentFlow/Source/Components/Button.swift b/submodules/ComponentFlow/Source/Components/Button.swift index 774d274b89..dcf4a880b8 100644 --- a/submodules/ComponentFlow/Source/Components/Button.swift +++ b/submodules/ComponentFlow/Source/Components/Button.swift @@ -149,6 +149,7 @@ public final class Button: Component { override init(frame: CGRect) { self.contentView = ComponentHostView() self.contentView.isUserInteractionEnabled = false + self.contentView.layer.allowsGroupOpacity = true super.init(frame: frame) diff --git a/submodules/ComponentFlow/Source/Components/Image.swift b/submodules/ComponentFlow/Source/Components/Image.swift index ec4dc81e6c..6f32ccab3d 100644 --- a/submodules/ComponentFlow/Source/Components/Image.swift +++ b/submodules/ComponentFlow/Source/Components/Image.swift @@ -5,15 +5,18 @@ public final class Image: Component { public let image: UIImage? public let tintColor: UIColor? public let size: CGSize? + public let contentMode: UIImageView.ContentMode public init( image: UIImage?, tintColor: UIColor? = nil, - size: CGSize? = nil + size: CGSize? = nil, + contentMode: UIImageView.ContentMode = .scaleToFill ) { self.image = image self.tintColor = tintColor self.size = size + self.contentMode = contentMode } public static func ==(lhs: Image, rhs: Image) -> Bool { @@ -26,6 +29,9 @@ public final class Image: Component { if lhs.size != rhs.size { return false } + if lhs.contentMode != rhs.contentMode { + return false + } return true } @@ -41,6 +47,7 @@ public final class Image: Component { func update(component: Image, availableSize: CGSize, environment: Environment, transition: Transition) -> CGSize { self.image = component.image self.tintColor = component.tintColor + self.contentMode = component.contentMode return component.size ?? availableSize } diff --git a/submodules/Display/Source/UIKitUtils.swift b/submodules/Display/Source/UIKitUtils.swift index 1830677525..bd9bff677a 100644 --- a/submodules/Display/Source/UIKitUtils.swift +++ b/submodules/Display/Source/UIKitUtils.swift @@ -145,6 +145,20 @@ public extension UIColor { } } + var components: (r: CGFloat, g: CGFloat, b: CGFloat, a: CGFloat) { + var red: CGFloat = 0.0 + var green: CGFloat = 0.0 + var blue: CGFloat = 0.0 + var alpha: CGFloat = 0.0 + if self.getRed(&red, green: &green, blue: &blue, alpha: &alpha) { + return (red, green, blue, alpha) + } else if self.getWhite(&red, alpha: &alpha) { + return (red, red, red, alpha) + } else { + return (0.0, 0.0, 0.0, 0.0) + } + } + var lightness: CGFloat { var red: CGFloat = 0.0 var green: CGFloat = 0.0 diff --git a/submodules/DrawingUI/BUILD b/submodules/DrawingUI/BUILD index fdde9b283e..f0540b9073 100644 --- a/submodules/DrawingUI/BUILD +++ b/submodules/DrawingUI/BUILD @@ -93,6 +93,7 @@ swift_library( "//submodules/FeaturedStickersScreen:FeaturedStickersScreen", "//submodules/TelegramNotices:TelegramNotices", "//submodules/FastBlur:FastBlur", + "//submodules/TelegramUI/Components/MediaEditor", ], visibility = [ "//visibility:public", diff --git a/submodules/DrawingUI/Sources/DrawingBubbleEntity.swift b/submodules/DrawingUI/Sources/DrawingBubbleEntity.swift index a182caee13..7c100b5ab9 100644 --- a/submodules/DrawingUI/Sources/DrawingBubbleEntity.swift +++ b/submodules/DrawingUI/Sources/DrawingBubbleEntity.swift @@ -214,7 +214,7 @@ final class DrawingBubbleEntityView: DrawingEntityView { selectionView.setNeedsLayout() } - override func makeSelectionView() -> DrawingEntitySelectionView { + override func makeSelectionView() -> DrawingEntitySelectionView? { if let selectionView = self.selectionView { return selectionView } diff --git a/submodules/DrawingUI/Sources/DrawingEntitiesView.swift b/submodules/DrawingUI/Sources/DrawingEntitiesView.swift index 893c583bf2..32a9ff9f5a 100644 --- a/submodules/DrawingUI/Sources/DrawingEntitiesView.swift +++ b/submodules/DrawingUI/Sources/DrawingEntitiesView.swift @@ -250,7 +250,8 @@ public final class DrawingEntitiesView: UIView, TGPhotoDrawingEntitiesView { let entitiesData = self.entitiesData return entitiesData != initialEntitiesData } else { - return !self.entities.isEmpty + let filteredEntities = self.entities.filter { !($0 is DrawingMediaEntity) } + return !filteredEntities.isEmpty } } @@ -266,6 +267,9 @@ public final class DrawingEntitiesView: UIView, TGPhotoDrawingEntitiesView { while true { var occupied = false for case let view as DrawingEntityView in self.subviews { + if view is DrawingMediaEntityView { + continue + } let location = view.entity.center let distance = sqrt(pow(location.x - position.x, 2) + pow(location.y - position.y, 2)) if distance < minimalDistance { @@ -344,7 +348,7 @@ public final class DrawingEntitiesView: UIView, TGPhotoDrawingEntitiesView { } @discardableResult - func add(_ entity: DrawingEntity, announce: Bool = true) -> DrawingEntityView { + public func add(_ entity: DrawingEntity, announce: Bool = true) -> DrawingEntityView { let view = entity.makeView(context: self.context) view.containerView = self @@ -462,6 +466,9 @@ public final class DrawingEntitiesView: UIView, TGPhotoDrawingEntitiesView { private func clear(animated: Bool = false) { if animated { for case let view as DrawingEntityView in self.subviews { + if view is DrawingMediaEntityView { + continue + } if let selectionView = view.selectionView { selectionView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.1, removeOnCompletion: false, completion: { [weak selectionView] _ in selectionView?.removeFromSuperview() @@ -477,6 +484,9 @@ public final class DrawingEntitiesView: UIView, TGPhotoDrawingEntitiesView { } else { for case let view as DrawingEntityView in self.subviews { + if view is DrawingMediaEntityView { + continue + } view.selectionView?.removeFromSuperview() view.removeFromSuperview() } @@ -489,7 +499,7 @@ public final class DrawingEntitiesView: UIView, TGPhotoDrawingEntitiesView { } } - func getView(for uuid: UUID) -> DrawingEntityView? { + public func getView(for uuid: UUID) -> DrawingEntityView? { for case let view as DrawingEntityView in self.subviews { if view.entity.uuid == uuid { return view @@ -544,6 +554,9 @@ public final class DrawingEntitiesView: UIView, TGPhotoDrawingEntitiesView { } func selectEntity(_ entity: DrawingEntity?) { + if entity is DrawingMediaEntity { + return + } if entity !== self.selectedEntityView?.entity { if let selectedEntityView = self.selectedEntityView { if let textEntityView = selectedEntityView as? DrawingTextEntityView, textEntityView.isEditing { @@ -565,14 +578,15 @@ public final class DrawingEntitiesView: UIView, TGPhotoDrawingEntitiesView { if let entity = entity, let entityView = self.getView(for: entity.uuid) { self.selectedEntityView = entityView - let selectionView = entityView.makeSelectionView() - selectionView.tapped = { [weak self, weak entityView] in - if let strongSelf = self, let entityView = entityView { - strongSelf.requestedMenuForEntityView(entityView, strongSelf.subviews.last === entityView) + if let selectionView = entityView.makeSelectionView() { + selectionView.tapped = { [weak self, weak entityView] in + if let strongSelf = self, let entityView = entityView { + strongSelf.requestedMenuForEntityView(entityView, strongSelf.subviews.last === entityView) + } } + entityView.selectionView = selectionView + self.selectionContainerView?.addSubview(selectionView) } - entityView.selectionView = selectionView - self.selectionContainerView?.addSubview(selectionView) entityView.update() } @@ -616,14 +630,24 @@ public final class DrawingEntitiesView: UIView, TGPhotoDrawingEntitiesView { return self.selectedEntityView != nil } + public func handlePan(_ gestureRecognizer: UIPanGestureRecognizer) { + if !self.hasSelection, let mediaEntityView = self.subviews.first(where: { $0 is DrawingMediaEntityView }) as? DrawingMediaEntityView { + mediaEntityView.handlePan(gestureRecognizer) + } + } + public func handlePinch(_ gestureRecognizer: UIPinchGestureRecognizer) { - if let selectedEntityView = self.selectedEntityView, let selectionView = selectedEntityView.selectionView { + if !self.hasSelection, let mediaEntityView = self.subviews.first(where: { $0 is DrawingMediaEntityView }) as? DrawingMediaEntityView { + mediaEntityView.handlePinch(gestureRecognizer) + } else if let selectedEntityView = self.selectedEntityView, let selectionView = selectedEntityView.selectionView { selectionView.handlePinch(gestureRecognizer) } } public func handleRotate(_ gestureRecognizer: UIRotationGestureRecognizer) { - if let selectedEntityView = self.selectedEntityView, let selectionView = selectedEntityView.selectionView { + if !self.hasSelection, let mediaEntityView = self.subviews.first(where: { $0 is DrawingMediaEntityView }) as? DrawingMediaEntityView { + mediaEntityView.handleRotate(gestureRecognizer) + } else if let selectedEntityView = self.selectedEntityView, let selectionView = selectedEntityView.selectionView { selectionView.handleRotate(gestureRecognizer) } } @@ -725,7 +749,7 @@ public class DrawingEntityView: UIView { return self.point(inside: point, with: nil) } - func makeSelectionView() -> DrawingEntitySelectionView { + func makeSelectionView() -> DrawingEntitySelectionView? { if let selectionView = self.selectionView { return selectionView } diff --git a/submodules/DrawingUI/Sources/DrawingMediaEntity.swift b/submodules/DrawingUI/Sources/DrawingMediaEntity.swift new file mode 100644 index 0000000000..24c796548a --- /dev/null +++ b/submodules/DrawingUI/Sources/DrawingMediaEntity.swift @@ -0,0 +1,560 @@ +import Foundation +import UIKit +import Display +import SwiftSignalKit +import TelegramCore +import AccountContext +import MediaEditor +import Photos + +public final class DrawingMediaEntity: DrawingEntity, Codable { + public enum Content { + case image(UIImage, PixelDimensions) + case video(String, PixelDimensions) + case asset(PHAsset) + + var dimensions: PixelDimensions { + switch self { + case let .image(_, dimensions), let .video(_, dimensions): + return dimensions + case let .asset(asset): + return PixelDimensions(width: Int32(asset.pixelWidth), height: Int32(asset.pixelHeight)) + } + } + } + + private enum CodingKeys: String, CodingKey { + case uuid + case image + case videoPath + case assetId + case size + case width + case height + case referenceDrawingSize + case position + case scale + case rotation + case mirrored + } + + public let uuid: UUID + public let content: Content + public let size: CGSize + + public var referenceDrawingSize: CGSize + public var position: CGPoint + public var scale: CGFloat + public var rotation: CGFloat + public var mirrored: Bool + + public var color: DrawingColor = DrawingColor.clear + public var lineWidth: CGFloat = 0.0 + + public var center: CGPoint { + return self.position + } + + public var baseSize: CGSize { + return self.size + } + + public var isAnimated: Bool { + switch self.content { + case .image: + return false + case .video: + return true + case let .asset(asset): + return asset.mediaType == .video + } + } + + public init(content: Content, size: CGSize) { + self.uuid = UUID() + self.content = content + self.size = size + + self.referenceDrawingSize = .zero + self.position = CGPoint() + self.scale = 1.0 + self.rotation = 0.0 + self.mirrored = false + } + + public init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + self.uuid = try container.decode(UUID.self, forKey: .uuid) + self.size = try container.decode(CGSize.self, forKey: .size) + let width = try container.decode(Int32.self, forKey: .width) + let height = try container.decode(Int32.self, forKey: .height) + if let videoPath = try container.decodeIfPresent(String.self, forKey: .videoPath) { + self.content = .video(videoPath, PixelDimensions(width: width, height: height)) + } else if let imageData = try container.decodeIfPresent(Data.self, forKey: .image), let image = UIImage(data: imageData) { + self.content = .image(image, PixelDimensions(width: width, height: height)) + } else if let _ = try container.decodeIfPresent(String.self, forKey: .assetId) { + fatalError() + //self.content = .asset() + } else { + fatalError() + } + self.referenceDrawingSize = try container.decode(CGSize.self, forKey: .referenceDrawingSize) + self.position = try container.decode(CGPoint.self, forKey: .position) + self.scale = try container.decode(CGFloat.self, forKey: .scale) + self.rotation = try container.decode(CGFloat.self, forKey: .rotation) + self.mirrored = try container.decode(Bool.self, forKey: .mirrored) + } + + public func encode(to encoder: Encoder) throws { + var container = encoder.container(keyedBy: CodingKeys.self) + try container.encode(self.uuid, forKey: .uuid) + switch self.content { + case let .video(videoPath, dimensions): + try container.encode(videoPath, forKey: .videoPath) + try container.encode(dimensions.width, forKey: .width) + try container.encode(dimensions.height, forKey: .height) + case let .image(image, dimensions): + try container.encodeIfPresent(image.jpegData(compressionQuality: 0.9), forKey: .image) + try container.encode(dimensions.width, forKey: .width) + try container.encode(dimensions.height, forKey: .height) + case let .asset(asset): + try container.encode(asset.localIdentifier, forKey: .assetId) + } + try container.encode(self.size, forKey: .size) + try container.encode(self.referenceDrawingSize, forKey: .referenceDrawingSize) + try container.encode(self.position, forKey: .position) + try container.encode(self.scale, forKey: .scale) + try container.encode(self.rotation, forKey: .rotation) + try container.encode(self.mirrored, forKey: .mirrored) + } + + public func duplicate() -> DrawingEntity { + let newEntity = DrawingMediaEntity(content: self.content, size: self.size) + newEntity.referenceDrawingSize = self.referenceDrawingSize + newEntity.position = self.position + newEntity.scale = self.scale + newEntity.rotation = self.rotation + newEntity.mirrored = self.mirrored + return newEntity + } + + public weak var currentEntityView: DrawingEntityView? + public func makeView(context: AccountContext) -> DrawingEntityView { + let entityView = DrawingMediaEntityView(context: context, entity: self) + self.currentEntityView = entityView + return entityView + } + + public func prepareForRender() { + } +} + +public final class DrawingMediaEntityView: DrawingEntityView { + private var mediaEntity: DrawingMediaEntity { + return self.entity as! DrawingMediaEntity + } + + var started: ((Double) -> Void)? + + private var currentSize: CGSize? + private var isVisible = true + private var isPlaying = false + + public var previewView: MediaEditorPreviewView? { + didSet { + if let previewView = self.previewView { + previewView.isUserInteractionEnabled = false + self.addSubview(previewView) + } + } + } + + init(context: AccountContext, entity: DrawingMediaEntity) { + super.init(context: context, entity: entity) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + deinit { + + } + + override func play() { + self.isVisible = true + self.applyVisibility() + } + + override func pause() { + self.isVisible = false + self.applyVisibility() + } + + override func seek(to timestamp: Double) { + self.isVisible = false + self.isPlaying = false + + } + + override func resetToStart() { + self.isVisible = false + self.isPlaying = false + } + + override func updateVisibility(_ visibility: Bool) { + self.isVisible = visibility + self.applyVisibility() + } + + private func applyVisibility() { + let isPlaying = self.isVisible + if self.isPlaying != isPlaying { + self.isPlaying = isPlaying + + } + } + + private var didApplyVisibility = false + public override func layoutSubviews() { + super.layoutSubviews() + + let size = self.bounds.size + + if size.width > 0 && self.currentSize != size { + self.currentSize = size + self.previewView?.frame = CGRect(origin: .zero, size: size) +// let sideSize: CGFloat = size.width +// let boundingSize = CGSize(width: sideSize, height: sideSize) +// +// let imageSize = self.dimensions.aspectFitted(boundingSize) +// self.imageNode.asyncLayout()(TransformImageArguments(corners: ImageCorners(), imageSize: imageSize, boundingSize: imageSize, intrinsicInsets: UIEdgeInsets()))() +// self.imageNode.frame = CGRect(origin: CGPoint(x: floor((size.width - imageSize.width) / 2.0), y: (size.height - imageSize.height) / 2.0), size: imageSize) +// if let animationNode = self.animationNode { +// animationNode.frame = CGRect(origin: CGPoint(x: floor((size.width - imageSize.width) / 2.0), y: (size.height - imageSize.height) / 2.0), size: imageSize) +// animationNode.updateLayout(size: imageSize) +// +// if !self.didApplyVisibility { +// self.didApplyVisibility = true +// self.applyVisibility() +// } +// } + self.update(animated: false) + } + } + + override func update(animated: Bool) { + self.center = self.mediaEntity.position + + let size = self.mediaEntity.baseSize + let scale = self.mediaEntity.scale + + self.bounds = CGRect(origin: .zero, size: size) + self.transform = CGAffineTransformScale(CGAffineTransformMakeRotation(self.mediaEntity.rotation), scale, scale) + + self.previewView?.layer.transform = CATransform3DMakeScale(self.mediaEntity.mirrored ? -1.0 : 1.0, 1.0, 1.0) + self.previewView?.frame = self.bounds + + super.update(animated: animated) + } + + override func updateSelectionView() { + + } + + override func makeSelectionView() -> DrawingEntitySelectionView? { + return nil + } + + @objc func handlePan(_ gestureRecognizer: UIPanGestureRecognizer) { + let delta = gestureRecognizer.translation(in: self.superview) + var updatedPosition = self.mediaEntity.position + + switch gestureRecognizer.state { + case .began, .changed: + updatedPosition.x += delta.x + updatedPosition.y += delta.y + + gestureRecognizer.setTranslation(.zero, in: self.superview) + default: + break + } + + self.mediaEntity.position = updatedPosition + self.update(animated: false) + } + + @objc func handlePinch(_ gestureRecognizer: UIPinchGestureRecognizer) { + switch gestureRecognizer.state { + case .began, .changed: + let scale = gestureRecognizer.scale + self.mediaEntity.scale = self.mediaEntity.scale * scale + self.update(animated: false) + + gestureRecognizer.scale = 1.0 + default: + break + } + } + + @objc func handleRotate(_ gestureRecognizer: UIRotationGestureRecognizer) { + var updatedRotation = self.mediaEntity.rotation + var rotation: CGFloat = 0.0 + + switch gestureRecognizer.state { + case .began: + break + case .changed: + rotation = gestureRecognizer.rotation + updatedRotation += rotation + + gestureRecognizer.rotation = 0.0 + case .ended, .cancelled: + break + default: + break + } + + self.mediaEntity.rotation = updatedRotation + self.update(animated: false) + } +} + +//final class DrawingStickerEntititySelectionView: DrawingEntitySelectionView, UIGestureRecognizerDelegate { +// private let border = SimpleShapeLayer() +// private let leftHandle = SimpleShapeLayer() +// private let rightHandle = SimpleShapeLayer() +// +// private var panGestureRecognizer: UIPanGestureRecognizer! +// +// override init(frame: CGRect) { +// let handleBounds = CGRect(origin: .zero, size: entitySelectionViewHandleSize) +// let handles = [ +// self.leftHandle, +// self.rightHandle +// ] +// +// super.init(frame: frame) +// +// self.backgroundColor = .clear +// self.isOpaque = false +// +// self.border.lineCap = .round +// self.border.fillColor = UIColor.clear.cgColor +// self.border.strokeColor = UIColor(rgb: 0xffffff, alpha: 0.5).cgColor +// self.border.shadowColor = UIColor.black.cgColor +// self.border.shadowRadius = 1.0 +// self.border.shadowOpacity = 0.5 +// self.border.shadowOffset = CGSize() +// self.layer.addSublayer(self.border) +// +// for handle in handles { +// handle.bounds = handleBounds +// handle.fillColor = UIColor(rgb: 0x0a60ff).cgColor +// handle.strokeColor = UIColor(rgb: 0xffffff).cgColor +// handle.rasterizationScale = UIScreen.main.scale +// handle.shouldRasterize = true +// +// self.layer.addSublayer(handle) +// } +// +// let panGestureRecognizer = UIPanGestureRecognizer(target: self, action: #selector(self.handlePan(_:))) +// panGestureRecognizer.delegate = self +// self.addGestureRecognizer(panGestureRecognizer) +// self.panGestureRecognizer = panGestureRecognizer +// +// self.snapTool.onSnapXUpdated = { [weak self] snapped in +// if let strongSelf = self, let entityView = strongSelf.entityView { +// entityView.onSnapToXAxis(snapped) +// } +// } +// +// self.snapTool.onSnapYUpdated = { [weak self] snapped in +// if let strongSelf = self, let entityView = strongSelf.entityView { +// entityView.onSnapToYAxis(snapped) +// } +// } +// +// self.snapTool.onSnapRotationUpdated = { [weak self] snappedAngle in +// if let strongSelf = self, let entityView = strongSelf.entityView { +// entityView.onSnapToAngle(snappedAngle) +// } +// } +// } +// +// required init?(coder: NSCoder) { +// fatalError("init(coder:) has not been implemented") +// } +// +// var scale: CGFloat = 1.0 { +// didSet { +// self.setNeedsLayout() +// } +// } +// +// override var selectionInset: CGFloat { +// return 18.0 +// } +// +// override func gestureRecognizerShouldBegin(_ gestureRecognizer: UIGestureRecognizer) -> Bool { +// return true +// } +// +// private let snapTool = DrawingEntitySnapTool() +// +// private var currentHandle: CALayer? +// @objc private func handlePan(_ gestureRecognizer: UIPanGestureRecognizer) { +// guard let entityView = self.entityView, let entity = entityView.entity as? DrawingStickerEntity else { +// return +// } +// let location = gestureRecognizer.location(in: self) +// +// switch gestureRecognizer.state { +// case .began: +// self.snapTool.maybeSkipFromStart(entityView: entityView, position: entity.position) +// +// if let sublayers = self.layer.sublayers { +// for layer in sublayers { +// if layer.frame.contains(location) { +// self.currentHandle = layer +// self.snapTool.maybeSkipFromStart(entityView: entityView, rotation: entity.rotation) +// return +// } +// } +// } +// self.currentHandle = self.layer +// case .changed: +// let delta = gestureRecognizer.translation(in: entityView.superview) +// let parentLocation = gestureRecognizer.location(in: self.superview) +// let velocity = gestureRecognizer.velocity(in: entityView.superview) +// +// var updatedPosition = entity.position +// var updatedScale = entity.scale +// var updatedRotation = entity.rotation +// if self.currentHandle === self.leftHandle || self.currentHandle === self.rightHandle { +// var deltaX = gestureRecognizer.translation(in: self).x +// if self.currentHandle === self.leftHandle { +// deltaX *= -1.0 +// } +// let scaleDelta = (self.bounds.size.width + deltaX * 2.0) / self.bounds.size.width +// updatedScale *= scaleDelta +// +// let newAngle: CGFloat +// if self.currentHandle === self.leftHandle { +// newAngle = atan2(self.center.y - parentLocation.y, self.center.x - parentLocation.x) +// } else { +// newAngle = atan2(parentLocation.y - self.center.y, parentLocation.x - self.center.x) +// } +// +// // let delta = newAngle - updatedRotation +// updatedRotation = newAngle// self.snapTool.update(entityView: entityView, velocity: 0.0, delta: delta, updatedRotation: newAngle) +// } else if self.currentHandle === self.layer { +// updatedPosition.x += delta.x +// updatedPosition.y += delta.y +// +// updatedPosition = self.snapTool.update(entityView: entityView, velocity: velocity, delta: delta, updatedPosition: updatedPosition) +// } +// +// entity.position = updatedPosition +// entity.scale = updatedScale +// entity.rotation = updatedRotation +// entityView.update() +// +// gestureRecognizer.setTranslation(.zero, in: entityView) +// case .ended, .cancelled: +// self.snapTool.reset() +// if self.currentHandle != nil { +// self.snapTool.rotationReset() +// } +// default: +// break +// } +// +// entityView.onPositionUpdated(entity.position) +// } +// +// override func handlePinch(_ gestureRecognizer: UIPinchGestureRecognizer) { +// guard let entityView = self.entityView, let entity = entityView.entity as? DrawingStickerEntity else { +// return +// } +// +// switch gestureRecognizer.state { +// case .began, .changed: +// let scale = gestureRecognizer.scale +// entity.scale = entity.scale * scale +// entityView.update() +// +// gestureRecognizer.scale = 1.0 +// default: +// break +// } +// } +// +// override func handleRotate(_ gestureRecognizer: UIRotationGestureRecognizer) { +// guard let entityView = self.entityView, let entity = entityView.entity as? DrawingStickerEntity else { +// return +// } +// +// let velocity = gestureRecognizer.velocity +// var updatedRotation = entity.rotation +// var rotation: CGFloat = 0.0 +// +// switch gestureRecognizer.state { +// case .began: +// self.snapTool.maybeSkipFromStart(entityView: entityView, rotation: entity.rotation) +// case .changed: +// rotation = gestureRecognizer.rotation +// updatedRotation += rotation +// +// gestureRecognizer.rotation = 0.0 +// case .ended, .cancelled: +// self.snapTool.rotationReset() +// default: +// break +// } +// +// updatedRotation = self.snapTool.update(entityView: entityView, velocity: velocity, delta: rotation, updatedRotation: updatedRotation) +// entity.rotation = updatedRotation +// entityView.update() +// +// entityView.onPositionUpdated(entity.position) +// } +// +// override func point(inside point: CGPoint, with event: UIEvent?) -> Bool { +// return self.bounds.insetBy(dx: -22.0, dy: -22.0).contains(point) +// } +// +// override func layoutSubviews() { +// let inset = self.selectionInset - 10.0 +// +// let bounds = CGRect(origin: .zero, size: CGSize(width: entitySelectionViewHandleSize.width / self.scale, height: entitySelectionViewHandleSize.height / self.scale)) +// let handleSize = CGSize(width: 9.0 / self.scale, height: 9.0 / self.scale) +// let handlePath = CGPath(ellipseIn: CGRect(origin: CGPoint(x: (bounds.width - handleSize.width) / 2.0, y: (bounds.height - handleSize.height) / 2.0), size: handleSize), transform: nil) +// let lineWidth = (1.0 + UIScreenPixel) / self.scale +// +// let handles = [ +// self.leftHandle, +// self.rightHandle +// ] +// +// for handle in handles { +// handle.path = handlePath +// handle.bounds = bounds +// handle.lineWidth = lineWidth +// } +// +// self.leftHandle.position = CGPoint(x: inset, y: self.bounds.midY) +// self.rightHandle.position = CGPoint(x: self.bounds.maxX - inset, y: self.bounds.midY) +// +// +// let radius = (self.bounds.width - inset * 2.0) / 2.0 +// let circumference: CGFloat = 2.0 * .pi * radius +// let count = 10 +// let relativeDashLength: CGFloat = 0.25 +// let dashLength = circumference / CGFloat(count) +// self.border.lineDashPattern = [dashLength * relativeDashLength, dashLength * relativeDashLength] as [NSNumber] +// +// self.border.lineWidth = 2.0 / self.scale +// self.border.path = UIBezierPath(ovalIn: CGRect(origin: CGPoint(x: inset, y: inset), size: CGSize(width: self.bounds.width - inset * 2.0, height: self.bounds.height - inset * 2.0))).cgPath +// } +//} diff --git a/submodules/DrawingUI/Sources/DrawingScreen.swift b/submodules/DrawingUI/Sources/DrawingScreen.swift index 0a9e719b8d..9b4b294fc9 100644 --- a/submodules/DrawingUI/Sources/DrawingScreen.swift +++ b/submodules/DrawingUI/Sources/DrawingScreen.swift @@ -482,6 +482,8 @@ private final class DrawingScreenComponent: CombinedComponent { typealias EnvironmentType = ViewControllerComponentContainer.Environment let context: AccountContext + let sourceHint: DrawingScreen.SourceHint? + let existingStickerPickerInputData: Promise? let isVideo: Bool let isAvatar: Bool let present: (ViewController) -> Void @@ -498,6 +500,8 @@ private final class DrawingScreenComponent: CombinedComponent { let requestPresentColorPicker: ActionSlot let toggleWithEraser: ActionSlot let toggleWithPreviousTool: ActionSlot + let insertSticker: ActionSlot + let insertText: ActionSlot let apply: ActionSlot let dismiss: ActionSlot @@ -509,6 +513,8 @@ private final class DrawingScreenComponent: CombinedComponent { init( context: AccountContext, + sourceHint: DrawingScreen.SourceHint?, + existingStickerPickerInputData: Promise?, isVideo: Bool, isAvatar: Bool, present: @escaping (ViewController) -> Void, @@ -525,6 +531,8 @@ private final class DrawingScreenComponent: CombinedComponent { requestPresentColorPicker: ActionSlot, toggleWithEraser: ActionSlot, toggleWithPreviousTool: ActionSlot, + insertSticker: ActionSlot, + insertText: ActionSlot, apply: ActionSlot, dismiss: ActionSlot, presentColorPicker: @escaping (DrawingColor) -> Void, @@ -534,6 +542,8 @@ private final class DrawingScreenComponent: CombinedComponent { presentFontPicker: @escaping (UIView) -> Void ) { self.context = context + self.sourceHint = sourceHint + self.existingStickerPickerInputData = existingStickerPickerInputData self.isVideo = isVideo self.isAvatar = isAvatar self.present = present @@ -550,6 +560,8 @@ private final class DrawingScreenComponent: CombinedComponent { self.requestPresentColorPicker = requestPresentColorPicker self.toggleWithEraser = toggleWithEraser self.toggleWithPreviousTool = toggleWithPreviousTool + self.insertSticker = insertSticker + self.insertText = insertText self.apply = apply self.dismiss = dismiss self.presentColorPicker = presentColorPicker @@ -623,6 +635,8 @@ private final class DrawingScreenComponent: CombinedComponent { private let dismissEyedropper: ActionSlot private let toggleWithEraser: ActionSlot private let toggleWithPreviousTool: ActionSlot + private let insertSticker: ActionSlot + private let insertText: ActionSlot private let present: (ViewController) -> Void var currentMode: Mode @@ -633,9 +647,22 @@ private final class DrawingScreenComponent: CombinedComponent { var lastSize: CGFloat = 0.5 - private let stickerPickerInputData = Promise() + private let stickerPickerInputData: Promise - init(context: AccountContext, updateToolState: ActionSlot, insertEntity: ActionSlot, deselectEntity: ActionSlot, updateEntitiesPlayback: ActionSlot, dismissEyedropper: ActionSlot, toggleWithEraser: ActionSlot, toggleWithPreviousTool: ActionSlot, present: @escaping (ViewController) -> Void) { + init( + context: AccountContext, + existingStickerPickerInputData: Promise?, + updateToolState: ActionSlot, + insertEntity: ActionSlot, + deselectEntity: ActionSlot, + updateEntitiesPlayback: ActionSlot, + dismissEyedropper: ActionSlot, + toggleWithEraser: ActionSlot, + toggleWithPreviousTool: ActionSlot, + insertSticker: ActionSlot, + insertText: ActionSlot, + present: @escaping (ViewController) -> Void) + { self.context = context self.updateToolState = updateToolState self.insertEntity = insertEntity @@ -644,6 +671,8 @@ private final class DrawingScreenComponent: CombinedComponent { self.dismissEyedropper = dismissEyedropper self.toggleWithEraser = toggleWithEraser self.toggleWithPreviousTool = toggleWithPreviousTool + self.insertSticker = insertSticker + self.insertText = insertText self.present = present self.currentMode = .drawing @@ -653,77 +682,95 @@ private final class DrawingScreenComponent: CombinedComponent { self.updateToolState.invoke(self.drawingState.currentToolState) - let stickerPickerInputData = self.stickerPickerInputData - Queue.concurrentDefaultQueue().after(0.5, { - let emojiItems = EmojiPagerContentComponent.emojiInputData( - context: context, - animationCache: context.animationCache, - animationRenderer: context.animationRenderer, - isStandalone: false, - isStatusSelection: false, - isReactionSelection: false, - isEmojiSelection: true, - hasTrending: false, - topReactionItems: [], - areUnicodeEmojiEnabled: true, - areCustomEmojiEnabled: true, - chatPeerId: context.account.peerId, - hasSearch: false, - forceHasPremium: true - ) + if let existingStickerPickerInputData { + self.stickerPickerInputData = existingStickerPickerInputData + } else { + self.stickerPickerInputData = Promise() - let stickerItems = EmojiPagerContentComponent.stickerInputData( - context: context, - animationCache: context.animationCache, - animationRenderer: context.animationRenderer, - stickerNamespaces: [Namespaces.ItemCollection.CloudStickerPacks], - stickerOrderedItemListCollectionIds: [Namespaces.OrderedItemList.CloudSavedStickers, Namespaces.OrderedItemList.CloudRecentStickers, Namespaces.OrderedItemList.CloudAllPremiumStickers], - chatPeerId: context.account.peerId, - hasSearch: false, - hasTrending: true, - forceHasPremium: true - ) - - let maskItems = EmojiPagerContentComponent.stickerInputData( - context: context, - animationCache: context.animationCache, - animationRenderer: context.animationRenderer, - stickerNamespaces: [Namespaces.ItemCollection.CloudMaskPacks], - stickerOrderedItemListCollectionIds: [], - chatPeerId: context.account.peerId, - hasSearch: false, - hasTrending: false, - forceHasPremium: true - ) - - let signal = combineLatest(queue: .mainQueue(), - emojiItems, - stickerItems, - maskItems - ) |> map { emoji, stickers, masks -> StickerPickerInputData in - return StickerPickerInputData(emoji: emoji, stickers: stickers, masks: masks) - } - - stickerPickerInputData.set(signal) - }) - + let stickerPickerInputData = self.stickerPickerInputData + Queue.concurrentDefaultQueue().after(0.5, { + let emojiItems = EmojiPagerContentComponent.emojiInputData( + context: context, + animationCache: context.animationCache, + animationRenderer: context.animationRenderer, + isStandalone: false, + isStatusSelection: false, + isReactionSelection: false, + isEmojiSelection: true, + hasTrending: false, + topReactionItems: [], + areUnicodeEmojiEnabled: true, + areCustomEmojiEnabled: true, + chatPeerId: context.account.peerId, + hasSearch: false, + forceHasPremium: true + ) + + let stickerItems = EmojiPagerContentComponent.stickerInputData( + context: context, + animationCache: context.animationCache, + animationRenderer: context.animationRenderer, + stickerNamespaces: [Namespaces.ItemCollection.CloudStickerPacks], + stickerOrderedItemListCollectionIds: [Namespaces.OrderedItemList.CloudSavedStickers, Namespaces.OrderedItemList.CloudRecentStickers, Namespaces.OrderedItemList.CloudAllPremiumStickers], + chatPeerId: context.account.peerId, + hasSearch: false, + hasTrending: true, + forceHasPremium: true + ) + + let maskItems = EmojiPagerContentComponent.stickerInputData( + context: context, + animationCache: context.animationCache, + animationRenderer: context.animationRenderer, + stickerNamespaces: [Namespaces.ItemCollection.CloudMaskPacks], + stickerOrderedItemListCollectionIds: [], + chatPeerId: context.account.peerId, + hasSearch: false, + hasTrending: false, + forceHasPremium: true + ) + + let signal = combineLatest(queue: .mainQueue(), + emojiItems, + stickerItems, + maskItems + ) |> map { emoji, stickers, masks -> StickerPickerInputData in + return StickerPickerInputData(emoji: emoji, stickers: stickers, masks: masks) + } + + stickerPickerInputData.set(signal) + }) + } + super.init() self.loadToolState() self.toggleWithEraser.connect { [weak self] _ in - if let strongSelf = self { - if strongSelf.drawingState.selectedTool == .eraser { - strongSelf.updateSelectedTool(strongSelf.nextToEraserTool) + if let self { + if self.drawingState.selectedTool == .eraser { + self.updateSelectedTool(self.nextToEraserTool) } else { - strongSelf.updateSelectedTool(.eraser) + self.updateSelectedTool(.eraser) } } } self.toggleWithPreviousTool.connect { [weak self] _ in - if let strongSelf = self { - strongSelf.updateSelectedTool(strongSelf.previousTool) + if let self { + self.updateSelectedTool(self.previousTool) + } + } + + self.insertText.connect { [weak self] _ in + if let self { + self.addTextEntity() + } + } + + self.insertSticker.connect { [weak self] _ in + if let self { + self.presentStickerPicker() } } } @@ -949,7 +996,7 @@ private final class DrawingScreenComponent: CombinedComponent { } func makeState() -> State { - return State(context: self.context, updateToolState: self.updateToolState, insertEntity: self.insertEntity, deselectEntity: self.deselectEntity, updateEntitiesPlayback: self.updateEntitiesPlayback, dismissEyedropper: self.dismissEyedropper, toggleWithEraser: self.toggleWithEraser, toggleWithPreviousTool: self.toggleWithPreviousTool, present: self.present) + return State(context: self.context, existingStickerPickerInputData: self.existingStickerPickerInputData, updateToolState: self.updateToolState, insertEntity: self.insertEntity, deselectEntity: self.deselectEntity, updateEntitiesPlayback: self.updateEntitiesPlayback, dismissEyedropper: self.dismissEyedropper, toggleWithEraser: self.toggleWithEraser, toggleWithPreviousTool: self.toggleWithPreviousTool, insertSticker: self.insertSticker, insertText: self.insertText, present: self.present) } static var body: Body { @@ -1046,7 +1093,10 @@ private final class DrawingScreenComponent: CombinedComponent { } } - let topInset = environment.safeInsets.top + 31.0 + var topInset = environment.safeInsets.top + 31.0 + if component.sourceHint == .storyEditor { + topInset += 75.0 + } let bottomInset: CGFloat = environment.inputHeight > 0.0 ? environment.inputHeight : 145.0 var leftEdge: CGFloat = environment.safeInsets.left @@ -1058,17 +1108,19 @@ private final class DrawingScreenComponent: CombinedComponent { rightEdge = floorToScreenPixels((context.availableSize.width - availableWidth) / 2.0) + availableWidth } - let topGradient = topGradient.update( - component: BlurredGradientComponent( - position: .top, - tag: topGradientTag - ), - availableSize: CGSize(width: context.availableSize.width, height: topInset + 15.0), - transition: .immediate - ) - context.add(topGradient - .position(CGPoint(x: context.availableSize.width / 2.0, y: topGradient.size.height / 2.0)) - ) + if component.sourceHint != .storyEditor { + let topGradient = topGradient.update( + component: BlurredGradientComponent( + position: .top, + tag: topGradientTag + ), + availableSize: CGSize(width: context.availableSize.width, height: topInset + 15.0), + transition: .immediate + ) + context.add(topGradient + .position(CGPoint(x: context.availableSize.width / 2.0, y: topGradient.size.height / 2.0)) + ) + } let bottomGradient = bottomGradient.update( component: BlurredGradientComponent( @@ -1844,8 +1896,13 @@ private final class DrawingScreenComponent: CombinedComponent { availableSize: CGSize(width: 33.0, height: 33.0), transition: .immediate ) + + var doneButtonPosition = CGPoint(x: context.availableSize.width - environment.safeInsets.right - doneButton.size.width / 2.0 - 3.0, y: context.availableSize.height - environment.safeInsets.bottom - doneButton.size.height / 2.0 - 2.0 - UIScreenPixel) + if component.sourceHint == .storyEditor { + doneButtonPosition = doneButtonPosition.offsetBy(dx: -2.0, dy: 0.0) + } context.add(doneButton - .position(CGPoint(x: context.availableSize.width - environment.safeInsets.right - doneButton.size.width / 2.0 - 3.0, y: context.availableSize.height - environment.safeInsets.bottom - doneButton.size.height / 2.0 - 2.0 - UIScreenPixel)) + .position(doneButtonPosition) .appear(Transition.Appear { _, view, transition in transition.animateScale(view: view, from: 0.1, to: 1.0) transition.animateAlpha(view: view, from: 0.0, to: 1.0) @@ -1917,8 +1974,9 @@ private final class DrawingScreenComponent: CombinedComponent { availableSize: CGSize(width: availableWidth - 57.0 - modeRightInset, height: context.availableSize.height), transition: context.transition ) + let modeAndSizePosition = CGPoint(x: context.availableSize.width / 2.0 - (modeRightInset - 57.0) / 2.0, y: context.availableSize.height - environment.safeInsets.bottom - modeAndSize.size.height / 2.0 - 9.0) context.add(modeAndSize - .position(CGPoint(x: context.availableSize.width / 2.0 - (modeRightInset - 57.0) / 2.0, y: context.availableSize.height - environment.safeInsets.bottom - modeAndSize.size.height / 2.0 - 9.0)) + .position(modeAndSizePosition) ) var animatingOut = false @@ -1950,8 +2008,12 @@ private final class DrawingScreenComponent: CombinedComponent { availableSize: CGSize(width: 33.0, height: 33.0), transition: .immediate ) + var backButtonPosition = CGPoint(x: environment.safeInsets.left + backButton.size.width / 2.0 + 3.0, y: context.availableSize.height - environment.safeInsets.bottom - backButton.size.height / 2.0 - 2.0 - UIScreenPixel) + if component.sourceHint == .storyEditor { + backButtonPosition = backButtonPosition.offsetBy(dx: 2.0, dy: 0.0) + } context.add(backButton - .position(CGPoint(x: environment.safeInsets.left + backButton.size.width / 2.0 + 3.0, y: context.availableSize.height - environment.safeInsets.bottom - backButton.size.height / 2.0 - 2.0 - UIScreenPixel)) + .position(backButtonPosition) ) return context.availableSize @@ -1977,6 +2039,8 @@ public class DrawingScreen: ViewController, TGPhotoDrawingInterfaceController, U private let requestPresentColorPicker: ActionSlot private let toggleWithEraser: ActionSlot private let toggleWithPreviousTool: ActionSlot + fileprivate let insertSticker: ActionSlot + fileprivate let insertText: ActionSlot private let apply: ActionSlot private let dismiss: ActionSlot @@ -1990,10 +2054,14 @@ public class DrawingScreen: ViewController, TGPhotoDrawingInterfaceController, U private let hapticFeedback = HapticFeedback() private var validLayout: (ContainerViewLayout, UIInterfaceOrientation?)? - private var _drawingView: DrawingView? + var _drawingView: DrawingView? var drawingView: DrawingView { if self._drawingView == nil, let controller = self.controller { - self._drawingView = DrawingView(size: controller.size) + if let externalDrawingView = controller.externalDrawingView { + self._drawingView = externalDrawingView + } else { + self._drawingView = DrawingView(size: controller.size) + } self._drawingView?.animationsEnabled = self.context.sharedContext.energyUsageSettings.fullTranslucency self._drawingView?.shouldBegin = { [weak self] _ in if let strongSelf = self { @@ -2074,14 +2142,13 @@ public class DrawingScreen: ViewController, TGPhotoDrawingInterfaceController, U } private weak var currentMenuController: ContextMenuController? - private var _entitiesView: DrawingEntitiesView? + var _entitiesView: DrawingEntitiesView? var entitiesView: DrawingEntitiesView { if self._entitiesView == nil, let controller = self.controller { if let externalEntitiesView = controller.externalEntitiesView { self._entitiesView = externalEntitiesView } else { self._entitiesView = DrawingEntitiesView(context: self.context, size: controller.size) - //self._entitiesView = DrawingEntitiesView(context: self.context, size: controller.originalSize) } self._drawingView?.entitiesView = self._entitiesView self._entitiesView?.drawingView = self._drawingView @@ -2243,6 +2310,8 @@ public class DrawingScreen: ViewController, TGPhotoDrawingInterfaceController, U self.requestPresentColorPicker = ActionSlot() self.toggleWithEraser = ActionSlot() self.toggleWithPreviousTool = ActionSlot() + self.insertSticker = ActionSlot() + self.insertText = ActionSlot() self.apply = ActionSlot() self.dismiss = ActionSlot() @@ -2646,6 +2715,8 @@ public class DrawingScreen: ViewController, TGPhotoDrawingInterfaceController, U component: AnyComponent( DrawingScreenComponent( context: self.context, + sourceHint: controller.sourceHint, + existingStickerPickerInputData: controller.existingStickerPickerInputData, isVideo: controller.isVideo, isAvatar: controller.isAvatar, present: { [weak self] c in @@ -2664,6 +2735,8 @@ public class DrawingScreen: ViewController, TGPhotoDrawingInterfaceController, U requestPresentColorPicker: self.requestPresentColorPicker, toggleWithEraser: self.toggleWithEraser, toggleWithPreviousTool: self.toggleWithPreviousTool, + insertSticker: self.insertSticker, + insertText: self.insertText, apply: self.apply, dismiss: self.dismiss, presentColorPicker: { [weak self] initialColor in @@ -2894,24 +2967,39 @@ public class DrawingScreen: ViewController, TGPhotoDrawingInterfaceController, U return self.displayNode as! Node } + public enum SourceHint { + case storyEditor + } + private let context: AccountContext + private let sourceHint: SourceHint? private let size: CGSize private let originalSize: CGSize private let isVideo: Bool private let isAvatar: Bool + private let externalDrawingView: DrawingView? private let externalEntitiesView: DrawingEntitiesView? + private let existingStickerPickerInputData: Promise? public var requestDismiss: () -> Void = {} public var requestApply: () -> Void = {} public var getCurrentImage: () -> UIImage? = { return nil } public var updateVideoPlayback: (Bool) -> Void = { _ in } - public init(context: AccountContext, size: CGSize, originalSize: CGSize, isVideo: Bool, isAvatar: Bool, entitiesView: (UIView & TGPhotoDrawingEntitiesView)?) { + public init(context: AccountContext, sourceHint: SourceHint? = nil, size: CGSize, originalSize: CGSize, isVideo: Bool, isAvatar: Bool, drawingView: DrawingView?, entitiesView: (UIView & TGPhotoDrawingEntitiesView)?, existingStickerPickerInputData: Promise? = nil) { self.context = context + self.sourceHint = sourceHint self.size = size self.originalSize = originalSize self.isVideo = isVideo self.isAvatar = isAvatar + self.existingStickerPickerInputData = existingStickerPickerInputData + + if let drawingView { + self.externalDrawingView = drawingView + } else { + self.externalDrawingView = nil + } if let entitiesView = entitiesView as? DrawingEntitiesView { self.externalEntitiesView = entitiesView @@ -3029,7 +3117,36 @@ public class DrawingScreen: ViewController, TGPhotoDrawingInterfaceController, U public func animateOut(_ completion: @escaping (() -> Void)) { self.selectionContainerView.alpha = 0.0 - self.node.animateOut(completion: completion) + self.node.animateOut(completion: { + completion() + }) + + Queue.mainQueue().after(0.4) { + self.node.isHidden = true + } + } + + public override func dismiss(animated flag: Bool, completion: (() -> Void)? = nil) { + super.dismiss(animated: flag, completion: completion) + + self.node._drawingView?.entitiesView = nil + self.node._entitiesView?.drawingView = nil + self.node._entitiesView?.entityAdded = { _ in } + self.node._entitiesView?.entityRemoved = { _ in } + self.node._drawingView?.getFullImage = { return nil } + self.node._entitiesView?.selectionContainerView = nil + self.node._entitiesView?.selectionChanged = { _ in } + self.node._entitiesView?.requestedMenuForEntityView = { _, _ in } + self.node._drawingView = nil + self.node._entitiesView = nil + } + + public func presentStickerSelection() { + self.node.insertSticker.invoke(Void()) + } + + public func addTextEntity() { + self.node.insertText.invoke(Void()) } private var orientation: UIInterfaceOrientation? diff --git a/submodules/DrawingUI/Sources/DrawingSimpleShapeEntity.swift b/submodules/DrawingUI/Sources/DrawingSimpleShapeEntity.swift index 49cfd20c03..2730975f87 100644 --- a/submodules/DrawingUI/Sources/DrawingSimpleShapeEntity.swift +++ b/submodules/DrawingUI/Sources/DrawingSimpleShapeEntity.swift @@ -227,7 +227,7 @@ final class DrawingSimpleShapeEntityView: DrawingEntityView { selectionView.transform = CGAffineTransformMakeRotation(self.shapeEntity.rotation) } - override func makeSelectionView() -> DrawingEntitySelectionView { + override func makeSelectionView() -> DrawingEntitySelectionView? { if let selectionView = self.selectionView { return selectionView } diff --git a/submodules/DrawingUI/Sources/DrawingStickerEntity.swift b/submodules/DrawingUI/Sources/DrawingStickerEntity.swift index e2d18a2079..3e7adffde8 100644 --- a/submodules/DrawingUI/Sources/DrawingStickerEntity.swift +++ b/submodules/DrawingUI/Sources/DrawingStickerEntity.swift @@ -366,7 +366,7 @@ final class DrawingStickerEntityView: DrawingEntityView { self.popIdentityTransformForMeasurement() } - override func makeSelectionView() -> DrawingEntitySelectionView { + override func makeSelectionView() -> DrawingEntitySelectionView? { if let selectionView = self.selectionView { return selectionView } diff --git a/submodules/DrawingUI/Sources/DrawingTextEntity.swift b/submodules/DrawingUI/Sources/DrawingTextEntity.swift index c14555de0d..3b07c2530d 100644 --- a/submodules/DrawingUI/Sources/DrawingTextEntity.swift +++ b/submodules/DrawingUI/Sources/DrawingTextEntity.swift @@ -55,6 +55,7 @@ public final class DrawingTextEntity: DrawingEntity, Codable { case rotation case renderImage case renderSubEntities + case renderAnimationFrames } enum Style: Codable { @@ -129,6 +130,46 @@ public final class DrawingTextEntity: DrawingEntity, Codable { public var renderImage: UIImage? public var renderSubEntities: [DrawingStickerEntity]? + public class AnimationFrame: Codable { + private enum CodingKeys: String, CodingKey { + case timestamp + case duration + case image + } + + public let timestamp: Double + public let duration: Double + public let image: UIImage + + public init(timestamp: Double, duration: Double, image: UIImage) { + self.timestamp = timestamp + self.duration = duration + self.image = image + } + + required public init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + self.timestamp = try container.decode(Double.self, forKey: .timestamp) + self.duration = try container.decode(Double.self, forKey: .duration) + if let renderImageData = try? container.decodeIfPresent(Data.self, forKey: .image) { + self.image = UIImage(data: renderImageData)! + } else { + fatalError() + } + } + + public func encode(to encoder: Encoder) throws { + var container = encoder.container(keyedBy: CodingKeys.self) + + try container.encode(self.timestamp, forKey: .timestamp) + try container.encode(self.duration, forKey: .duration) + if let data = self.image.pngData() { + try container.encode(data, forKey: .image) + } + } + } + public var renderAnimationFrames: [AnimationFrame]? + init(text: NSAttributedString, style: Style, animation: Animation, font: Font, alignment: Alignment, fontSize: CGFloat, color: DrawingColor) { self.uuid = UUID() @@ -176,6 +217,7 @@ public final class DrawingTextEntity: DrawingEntity, Codable { if let renderSubEntities = try? container.decodeIfPresent([CodableDrawingEntity].self, forKey: .renderSubEntities) { self.renderSubEntities = renderSubEntities.compactMap { $0.entity as? DrawingStickerEntity } } + self.renderAnimationFrames = try container.decodeIfPresent([AnimationFrame].self, forKey: .renderAnimationFrames) } public func encode(to encoder: Encoder) throws { @@ -209,6 +251,9 @@ public final class DrawingTextEntity: DrawingEntity, Codable { let codableEntities: [CodableDrawingEntity] = renderSubEntities.map { .sticker($0) } try container.encode(codableEntities, forKey: .renderSubEntities) } + if let renderAnimationFrames = self.renderAnimationFrames { + try container.encode(renderAnimationFrames, forKey: .renderAnimationFrames) + } } public func duplicate() -> DrawingEntity { @@ -231,6 +276,12 @@ public final class DrawingTextEntity: DrawingEntity, Codable { public func prepareForRender() { self.renderImage = (self.currentEntityView as? DrawingTextEntityView)?.getRenderImage() self.renderSubEntities = (self.currentEntityView as? DrawingTextEntityView)?.getRenderSubEntities() + + if case .none = self.animation { + self.renderAnimationFrames = nil + } else { + self.renderAnimationFrames = (self.currentEntityView as? DrawingTextEntityView)?.getRenderAnimationFrames() + } } } @@ -285,6 +336,10 @@ final class DrawingTextEntityView: DrawingEntityView, UITextViewDelegate { fatalError("init(coder:) has not been implemented") } + deinit { + self.displayLink?.invalidate() + } + private var isSuspended = false private var _isEditing = false var isEditing: Bool { @@ -512,6 +567,7 @@ final class DrawingTextEntityView: DrawingEntityView, UITextViewDelegate { } override func sizeThatFits(_ size: CGSize) -> CGSize { + self.textView.setNeedsLayersUpdate() var result = self.textView.sizeThatFits(CGSize(width: self.textEntity.width, height: .greatestFiniteMagnitude)) result.width = max(224.0, ceil(result.width) + 20.0) result.height = ceil(result.height) //+ 20.0 + (self.textView.font?.pointSize ?? 0.0) // * _font.sizeCorrection; @@ -549,6 +605,7 @@ final class DrawingTextEntityView: DrawingEntityView, UITextViewDelegate { let range = NSMakeRange(0, text.length) let fontSize = self.displayFontSize + self.textView.hasTextLayers = [.typing, .wiggle].contains(self.textEntity.animation) self.textView.drawingLayoutManager.textContainers.first?.lineFragmentPadding = floor(fontSize * 0.24) if let (font, name) = availableFonts[text.string.lowercased()] { @@ -566,6 +623,10 @@ final class DrawingTextEntityView: DrawingEntityView, UITextViewDelegate { text.addAttribute(.font, value: font, range: range) self.textView.font = font + let paragraphStyle = NSMutableParagraphStyle() + paragraphStyle.alignment = self.textEntity.alignment.alignment + text.addAttribute(.paragraphStyle, value: paragraphStyle, range: range) + let color = self.textEntity.color.toUIColor() let textColor: UIColor switch self.textEntity.style { @@ -578,23 +639,136 @@ final class DrawingTextEntityView: DrawingEntityView, UITextViewDelegate { case .stroke: textColor = color.lightness > 0.99 ? UIColor.black : UIColor.white } - text.addAttribute(.foregroundColor, value: textColor, range: range) + + guard let visualText = text.mutableCopy() as? NSMutableAttributedString else { + return + } + if self.textView.hasTextLayers { + text.addAttribute(.foregroundColor, value: UIColor.clear, range: range) + } else { + text.addAttribute(.foregroundColor, value: textColor, range: range) + } + visualText.addAttribute(.foregroundColor, value: textColor, range: range) text.enumerateAttributes(in: range) { attributes, subrange, _ in if let _ = attributes[ChatTextInputAttributes.customEmoji] { text.addAttribute(.foregroundColor, value: UIColor.clear, range: subrange) + visualText.addAttribute(.foregroundColor, value: UIColor.clear, range: subrange) } } - - let paragraphStyle = NSMutableParagraphStyle() - paragraphStyle.alignment = self.textEntity.alignment.alignment - text.addAttribute(.paragraphStyle, value: paragraphStyle, range: range) let previousRange = self.textView.selectedRange self.textView.attributedText = text + self.textView.visualText = visualText + if keepSelectedRange { self.textView.selectedRange = previousRange } + + if self.textView.hasTextLayers { + self.textView.onLayersUpdate = { [weak self] in + self?.updateTextAnimations() + } + } else { + self.updateTextAnimations() + } + } + + private var previousDisplayLinkTime: Double? + + private var displayLinkStart: Double? + private var displayLink: SharedDisplayLinkDriver.Link? + + private var pendingImage: (Double, UIImage)? + private var cachedFrames: [DrawingTextEntity.AnimationFrame] = [] + + private func setupRecorder(delta: Double, duration: Double) { + self.cachedFrames.removeAll() + + self.displayLink?.invalidate() + self.displayLink = nil + + self.previousDisplayLinkTime = nil + let displayLinkStart = CACurrentMediaTime() + self.displayLinkStart = displayLinkStart + + self.displayLink = SharedDisplayLinkDriver.shared.add { [weak self] in + if let strongSelf = self { + let currentTime = CACurrentMediaTime() + if let previousDisplayLinkTime = strongSelf.previousDisplayLinkTime, currentTime < previousDisplayLinkTime + delta { + return + } + if currentTime >= displayLinkStart + duration { + strongSelf.displayLink?.invalidate() + strongSelf.displayLink = nil + } + if let (timestamp, image) = strongSelf.pendingImage, let previousDisplayLinkTime = strongSelf.previousDisplayLinkTime { + strongSelf.cachedFrames.append(DrawingTextEntity.AnimationFrame(timestamp: timestamp - displayLinkStart, duration: currentTime - previousDisplayLinkTime, image: image)) + } + if let image = strongSelf.getPresentationRenderImage() { + strongSelf.pendingImage = (currentTime, image) + } + if strongSelf.previousDisplayLinkTime == nil { + strongSelf.previousDisplayLinkTime = displayLinkStart + } else { + strongSelf.previousDisplayLinkTime = currentTime + } + } + } + self.displayLink?.isPaused = false + } + + func updateTextAnimations() { + for layer in self.textView.characterLayers { + layer.removeAllAnimations() + } + self.textView.layer.removeAllAnimations() + + guard self.textView.characterLayers.count > 0 || self.textEntity.animation == .zoomIn else { + return + } + + switch self.textEntity.animation { + case .typing: + let delta: CGFloat = 1.0 / CGFloat(self.textView.characterLayers.count + 3) + let duration = Double(self.textView.characterLayers.count + 3) * 0.28 + var offset = delta + for layer in self.textView.characterLayers { + let animation = CAKeyframeAnimation(keyPath: "opacity") + animation.calculationMode = .discrete + animation.values = [0.0, 1.0] + animation.keyTimes = [0.0, offset as NSNumber, 1.0] + animation.timingFunction = CAMediaTimingFunction(name: .easeInEaseOut) + animation.duration = duration + animation.repeatCount = .infinity + layer.add(animation, forKey: "opacity") + offset += delta + } + self.setupRecorder(delta: delta, duration: duration) + case .wiggle: + for layer in self.textView.characterLayers { + let animation = CABasicAnimation(keyPath: "transform.rotation.z") + animation.fromValue = (-.pi / 10.0) as NSNumber + animation.toValue = (.pi / 10.0) as NSNumber + animation.autoreverses = true + animation.timingFunction = CAMediaTimingFunction(name: .easeInEaseOut) + animation.duration = 0.6 + animation.repeatCount = .infinity + layer.add(animation, forKey: "transform.rotation.z") + } + self.setupRecorder(delta: 0.033, duration: 1.2) + case .zoomIn: + let animation = CABasicAnimation(keyPath: "transform.scale") + animation.fromValue = 0.001 as NSNumber + animation.toValue = 1.0 as NSNumber + animation.autoreverses = true + animation.timingFunction = CAMediaTimingFunction(name: .easeInEaseOut) + animation.duration = 0.8 + animation.repeatCount = .infinity + self.textView.layer.add(animation, forKey: "transform.scale") + default: + break + } } override func update(animated: Bool = false) { @@ -643,9 +817,12 @@ final class DrawingTextEntityView: DrawingEntityView, UITextViewDelegate { self.sizeToFit() - Queue.mainQueue().after(afterAppendingEmoji ? 0.01 : 0.001) { + self.textView.onLayoutUpdate = { self.updateEntities() } +// Queue.mainQueue().after(afterAppendingEmoji ? 0.01 : 0.001) { +// self.updateEntities() +// } super.update(animated: animated) } @@ -669,7 +846,7 @@ final class DrawingTextEntityView: DrawingEntityView, UITextViewDelegate { self.popIdentityTransformForMeasurement() } - override func makeSelectionView() -> DrawingEntitySelectionView { + override func makeSelectionView() -> DrawingEntitySelectionView? { if let selectionView = self.selectionView { return selectionView } @@ -687,6 +864,30 @@ final class DrawingTextEntityView: DrawingEntityView, UITextViewDelegate { return image } + func getPresentationRenderImage() -> UIImage? { + let rect = self.bounds + UIGraphicsBeginImageContextWithOptions(rect.size, false, 1.0) + if let context = UIGraphicsGetCurrentContext() { + for layer in self.textView.characterLayers { + if let presentation = layer.presentation() { + context.saveGState() + context.translateBy(x: presentation.position.x - presentation.bounds.width / 2.0, y: 0.0) + if let rotation = (presentation.value(forKeyPath: "transform.rotation.z") as? NSNumber)?.floatValue { + context.translateBy(x: presentation.bounds.width / 2.0, y: presentation.bounds.height) + context.rotate(by: CGFloat(rotation)) + context.translateBy(x: -presentation.bounds.width / 2.0, y: -presentation.bounds.height) + } + presentation.render(in: context) + context.restoreGState() + } + } + } + //self.textView.drawHierarchy(in: rect, afterScreenUpdates: true) + let image = UIGraphicsGetImageFromCurrentImageContext() + UIGraphicsEndImageContext() + return image + } + func getRenderSubEntities() -> [DrawingStickerEntity] { let textSize = self.textView.bounds.size let textPosition = self.textEntity.position @@ -714,6 +915,10 @@ final class DrawingTextEntityView: DrawingEntityView, UITextViewDelegate { } return entities } + + func getRenderAnimationFrames() -> [DrawingTextEntity.AnimationFrame]? { + return self.cachedFrames + } } final class DrawingTextEntititySelectionView: DrawingEntitySelectionView, UIGestureRecognizerDelegate { @@ -1185,7 +1390,15 @@ private class DrawingTextStorage: NSTextStorage { } } -class DrawingTextView: UITextView { +final class SimpleTextLayer: CATextLayer { + override func action(forKey event: String) -> CAAction? { + return nullAction + } +} + +final class DrawingTextView: UITextView, NSLayoutManagerDelegate { + var characterLayers: [CALayer] = [] + fileprivate var drawingLayoutManager: DrawingTextLayoutManager { return self.layoutManager as! DrawingTextLayoutManager } @@ -1221,16 +1434,33 @@ class DrawingTextView: UITextView { } } + override var font: UIFont? { + get { + return super.font + } + set { + if self.font != newValue { + super.font = newValue + self.fixTypingAttributes() + } + } + } + override var textColor: UIColor? { get { return super.textColor } set { - super.textColor = newValue - self.fixTypingAttributes() + if self.textColor != newValue { + super.textColor = newValue + self.fixTypingAttributes() + } } } + var hasTextLayers = false + var visualText: NSAttributedString? + init(frame: CGRect) { let textStorage = DrawingTextStorage() let layoutManager = DrawingTextLayoutManager() @@ -1243,6 +1473,8 @@ class DrawingTextView: UITextView { super.init(frame: frame, textContainer: textContainer) self.tintColor = UIColor.white + + layoutManager.delegate = self } required init?(coder: NSCoder) { @@ -1280,6 +1512,64 @@ class DrawingTextView: UITextView { attributes[NSAttributedString.Key.paragraphStyle] = paragraphStyle self.typingAttributes = attributes } + + var onLayoutUpdate: (() -> Void)? + var onLayersUpdate: (() -> Void)? + + private var needsLayersUpdate = false + func setNeedsLayersUpdate() { + self.needsLayersUpdate = true + } + + func layoutManager(_ layoutManager: NSLayoutManager, didCompleteLayoutFor textContainer: NSTextContainer?, atEnd layoutFinishedFlag: Bool) { + self.updateCharLayers() + if layoutFinishedFlag { +// if self.needsLayersUpdate { +// self.needsLayersUpdate = false +// self.updateCharLayers() +// } + if let onLayoutUpdate = self.onLayoutUpdate { + self.onLayoutUpdate = nil + onLayoutUpdate() + } + } + } + + func updateCharLayers() { + for layer in self.characterLayers { + layer.removeFromSuperlayer() + } + self.characterLayers = [] + + guard let attributedString = self.visualText, self.hasTextLayers else { + return + } + + let wordRange = NSMakeRange(0, attributedString.length) + + var index = wordRange.location + while index < wordRange.location + wordRange.length { + let glyphRange = NSMakeRange(index, 1) + let characterRange = self.layoutManager.characterRange(forGlyphRange: glyphRange, actualGlyphRange:nil) + var glyphRect = self.layoutManager.boundingRect(forGlyphRange: glyphRange, in: self.textContainer) + //let location = self.layoutManager.location(forGlyphAt: index) + + glyphRect.origin.y += glyphRect.height / 2.0 //location.y - (glyphRect.height / 2.0); + let textLayer = SimpleTextLayer() + textLayer.contentsScale = 1.0 + textLayer.frame = glyphRect + textLayer.string = attributedString.attributedSubstring(from: characterRange) + textLayer.anchorPoint = CGPoint(x: 0.5, y: 1.0) + + self.layer.addSublayer(textLayer) + self.characterLayers.append(textLayer) + + let stepGlyphRange = self.layoutManager.glyphRange(forCharacterRange: characterRange, actualCharacterRange:nil) + index += stepGlyphRange.length + } + + self.onLayersUpdate?() + } } private var availableFonts: [String: (String, String)] = { diff --git a/submodules/DrawingUI/Sources/DrawingVectorEntity.swift b/submodules/DrawingUI/Sources/DrawingVectorEntity.swift index 226f2b9d11..6fcac34040 100644 --- a/submodules/DrawingUI/Sources/DrawingVectorEntity.swift +++ b/submodules/DrawingUI/Sources/DrawingVectorEntity.swift @@ -206,7 +206,7 @@ final class DrawingVectorEntityView: DrawingEntityView { } } - override func makeSelectionView() -> DrawingEntitySelectionView { + override func makeSelectionView() -> DrawingEntitySelectionView? { if let selectionView = self.selectionView { return selectionView } diff --git a/submodules/DrawingUI/Sources/DrawingView.swift b/submodules/DrawingUI/Sources/DrawingView.swift index 58a57dd239..5cb9fdcec1 100644 --- a/submodules/DrawingUI/Sources/DrawingView.swift +++ b/submodules/DrawingUI/Sources/DrawingView.swift @@ -137,7 +137,7 @@ public final class DrawingView: UIView, UIGestureRecognizerDelegate, UIPencilInt private let pencilInteraction: UIInteraction? - init(size: CGSize) { + public init(size: CGSize) { self.imageSize = size self.screenSize = size @@ -175,10 +175,6 @@ public final class DrawingView: UIView, UIGestureRecognizerDelegate, UIPencilInt super.init(frame: CGRect(origin: .zero, size: size)) - Queue.mainQueue().async { - self.loadTemplates() - } - if #available(iOS 12.1, *), let pencilInteraction = self.pencilInteraction as? UIPencilInteraction { pencilInteraction.delegate = self self.addInteraction(pencilInteraction) diff --git a/submodules/DrawingUI/Sources/StickerPickerScreen.swift b/submodules/DrawingUI/Sources/StickerPickerScreen.swift index 85e9b74f03..47ca066839 100644 --- a/submodules/DrawingUI/Sources/StickerPickerScreen.swift +++ b/submodules/DrawingUI/Sources/StickerPickerScreen.swift @@ -16,12 +16,12 @@ import TelegramNotices import ChatEntityKeyboardInputNode import ContextUI -struct StickerPickerInputData: Equatable { +public struct StickerPickerInputData: Equatable { var emoji: EmojiPagerContentComponent var stickers: EmojiPagerContentComponent? var masks: EmojiPagerContentComponent? - init( + public init( emoji: EmojiPagerContentComponent, stickers: EmojiPagerContentComponent?, masks: EmojiPagerContentComponent? diff --git a/submodules/LegacyComponents/LegacyImages.xcassets/Editor/BrushMarker.imageset/ic_menu_brush2.pdf b/submodules/LegacyComponents/LegacyImages.xcassets/Editor/BrushMarker.imageset/ic_menu_brush2.pdf deleted file mode 100644 index 326fa48931..0000000000 Binary files a/submodules/LegacyComponents/LegacyImages.xcassets/Editor/BrushMarker.imageset/ic_menu_brush2.pdf and /dev/null differ diff --git a/submodules/LegacyComponents/LegacyImages.xcassets/Editor/BrushNeon.imageset/ic_menu_brush3.pdf b/submodules/LegacyComponents/LegacyImages.xcassets/Editor/BrushNeon.imageset/ic_menu_brush3.pdf deleted file mode 100644 index ab17d8361f..0000000000 Binary files a/submodules/LegacyComponents/LegacyImages.xcassets/Editor/BrushNeon.imageset/ic_menu_brush3.pdf and /dev/null differ diff --git a/submodules/LegacyComponents/LegacyImages.xcassets/Editor/BrushSelectedArrow.imageset/Contents.json b/submodules/LegacyComponents/LegacyImages.xcassets/Editor/BrushSelectedArrow.imageset/Contents.json deleted file mode 100644 index 2c4b2648b1..0000000000 --- a/submodules/LegacyComponents/LegacyImages.xcassets/Editor/BrushSelectedArrow.imageset/Contents.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "images" : [ - { - "filename" : "ic_editor_brush4.pdf", - "idiom" : "universal" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/submodules/LegacyComponents/LegacyImages.xcassets/Editor/BrushSelectedMarker.imageset/Contents.json b/submodules/LegacyComponents/LegacyImages.xcassets/Editor/BrushSelectedMarker.imageset/Contents.json deleted file mode 100644 index 8ab8127274..0000000000 --- a/submodules/LegacyComponents/LegacyImages.xcassets/Editor/BrushSelectedMarker.imageset/Contents.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "images" : [ - { - "filename" : "ic_editor_brush2.pdf", - "idiom" : "universal" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/submodules/LegacyComponents/LegacyImages.xcassets/Editor/BrushSelectedNeon.imageset/Contents.json b/submodules/LegacyComponents/LegacyImages.xcassets/Editor/BrushSelectedNeon.imageset/Contents.json deleted file mode 100644 index c94485170a..0000000000 --- a/submodules/LegacyComponents/LegacyImages.xcassets/Editor/BrushSelectedNeon.imageset/Contents.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "images" : [ - { - "filename" : "ic_editor_brush3.pdf", - "idiom" : "universal" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/submodules/LegacyComponents/LegacyImages.xcassets/Editor/BrushSelectedPen.imageset/Contents.json b/submodules/LegacyComponents/LegacyImages.xcassets/Editor/BrushSelectedPen.imageset/Contents.json deleted file mode 100644 index 0402056ad7..0000000000 --- a/submodules/LegacyComponents/LegacyImages.xcassets/Editor/BrushSelectedPen.imageset/Contents.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "images" : [ - { - "filename" : "ic_editor_brush1.pdf", - "idiom" : "universal" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/submodules/LegacyComponents/LegacyImages.xcassets/Editor/Drawing.imageset/Contents.json b/submodules/LegacyComponents/LegacyImages.xcassets/Editor/Drawing.imageset/Contents.json deleted file mode 100644 index 3076fdf67b..0000000000 --- a/submodules/LegacyComponents/LegacyImages.xcassets/Editor/Drawing.imageset/Contents.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "images" : [ - { - "filename" : "ic_editor_brush.pdf", - "idiom" : "universal" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/submodules/LegacyComponents/Sources/TGPhotoEditorBlurView.h b/submodules/LegacyComponents/Sources/TGPhotoEditorBlurView.h deleted file mode 100644 index 42a6065ad6..0000000000 --- a/submodules/LegacyComponents/Sources/TGPhotoEditorBlurView.h +++ /dev/null @@ -1,8 +0,0 @@ -#import -#import "PGBlurTool.h" - -@interface TGPhotoEditorBlurView : UIView - -- (instancetype)initWithType:(PGBlurToolType)type; - -@end diff --git a/submodules/LegacyComponents/Sources/TGPhotoEditorBlurView.m b/submodules/LegacyComponents/Sources/TGPhotoEditorBlurView.m deleted file mode 100644 index 1200ef22ee..0000000000 --- a/submodules/LegacyComponents/Sources/TGPhotoEditorBlurView.m +++ /dev/null @@ -1,167 +0,0 @@ -#import "TGPhotoEditorBlurView.h" - -#import "LegacyComponentsInternal.h" - -const CGFloat TGPhotoEditorBlurViewOverscreenSize = 3000; - -@interface TGPhotoEditorBlurView () -{ - UIImageView *_maskView; - UIView *_leftView; - UIView *_topView; - UIView *_rightView; - UIView *_bottomView; -} -@end - -@implementation TGPhotoEditorBlurView - -- (instancetype)initWithType:(PGBlurToolType)type -{ - self = [super initWithFrame:CGRectZero]; - if (self != nil) - { - UIColor *overlayColor = UIColorRGBA(0xffffff, 0.75f); - UIColor *transparentColor = UIColorRGBA(0xffffff, 0.0f); - - static UIImage *radialBlurImage = nil; - static UIImage *linearBlurImage = nil; - static dispatch_once_t radialBlurOnceToken; - static dispatch_once_t linearBlurOnceToken; - - UIImage *blurImage = nil; - - switch (type) - { - case PGBlurToolTypeRadial: - { - dispatch_once(&radialBlurOnceToken, ^ - { - UIGraphicsBeginImageContextWithOptions(CGSizeMake(100.0f, 100.0f), false, 0.0f); - CGContextRef context = UIGraphicsGetCurrentContext(); - - CGColorRef colors[3] = { - CGColorRetain(transparentColor.CGColor), - CGColorRetain(overlayColor.CGColor) - }; - - CFArrayRef colorsArray = CFArrayCreate(kCFAllocatorDefault, (const void **)&colors, 2, NULL); - CGFloat locations[2] = {0.3f, 0.9f}; - - CGColorSpaceRef colorSpace = CGColorSpaceCreateDeviceRGB(); - CGGradientRef gradient = CGGradientCreateWithColors(colorSpace, colorsArray, (CGFloat const *)&locations); - - CFRelease(colorsArray); - CFRelease(colors[0]); - CFRelease(colors[1]); - - CGColorSpaceRelease(colorSpace); - - CGContextDrawRadialGradient(context, gradient, CGPointMake(50.0f, 50.0f), 0, CGPointMake(50.0f, 50.0f), 50.0f, kCGGradientDrawsAfterEndLocation); - - CFRelease(gradient); - - radialBlurImage = UIGraphicsGetImageFromCurrentImageContext(); - UIGraphicsEndImageContext(); - }); - - blurImage = radialBlurImage; - } - break; - - case PGBlurToolTypeLinear: - { - dispatch_once(&linearBlurOnceToken, ^ - { - UIGraphicsBeginImageContextWithOptions(CGSizeMake(1.0f, 100.0f), false, 0.0f); - CGContextRef context = UIGraphicsGetCurrentContext(); - - CGColorRef colors[4] = { - CGColorRetain(overlayColor.CGColor), - CGColorRetain(transparentColor.CGColor), - CGColorRetain(transparentColor.CGColor), - CGColorRetain(overlayColor.CGColor) - }; - - CFArrayRef colorsArray = CFArrayCreate(kCFAllocatorDefault, (const void **)&colors, 4, NULL); - CGFloat locations[4] = {0.0f, 0.3f, 0.7f, 1.0f}; - - CGColorSpaceRef colorSpace = CGColorSpaceCreateDeviceRGB(); - CGGradientRef gradient = CGGradientCreateWithColors(colorSpace, colorsArray, (CGFloat const *)&locations); - - CFRelease(colorsArray); - CFRelease(colors[0]); - CFRelease(colors[1]); - CFRelease(colors[2]); - CFRelease(colors[3]); - - CGColorSpaceRelease(colorSpace); - - CGContextDrawLinearGradient(context, gradient, CGPointMake(0.0f, 0.0f), CGPointMake(0.0f, 100.0f), 0); - - CFRelease(gradient); - - linearBlurImage = UIGraphicsGetImageFromCurrentImageContext(); - UIGraphicsEndImageContext(); - }); - - blurImage = linearBlurImage; - } - break; - - default: - break; - } - - _maskView = [[UIImageView alloc] initWithImage:blurImage]; - [self addSubview:_maskView]; - - switch (type) - { - case PGBlurToolTypeRadial: - { - _leftView = [[UIView alloc] initWithFrame:CGRectZero]; - _leftView.backgroundColor = overlayColor; - [self addSubview:_leftView]; - - _rightView = [[UIView alloc] initWithFrame:CGRectZero]; - _rightView.backgroundColor = overlayColor; - [self addSubview:_rightView]; - } - case PGBlurToolTypeLinear: - { - _topView = [[UIView alloc] initWithFrame:CGRectZero]; - _topView.backgroundColor = overlayColor; - [self addSubview:_topView]; - - _bottomView = [[UIView alloc] initWithFrame:CGRectZero]; - _bottomView.backgroundColor = overlayColor; - [self addSubview:_bottomView]; - } - break; - default: - break; - } - } - return self; -} - -- (void)layoutSubviews -{ - if (_leftView != nil && _rightView != nil) - { - _maskView.frame = self.bounds; - _topView.frame = CGRectMake(0, -TGPhotoEditorBlurViewOverscreenSize, _maskView.bounds.size.width, TGPhotoEditorBlurViewOverscreenSize); - _bottomView.frame = CGRectMake(0, _maskView.bounds.size.height, _maskView.bounds.size.width, TGPhotoEditorBlurViewOverscreenSize); - _leftView.frame = CGRectMake(-TGPhotoEditorBlurViewOverscreenSize, -TGPhotoEditorBlurViewOverscreenSize, TGPhotoEditorBlurViewOverscreenSize, _maskView.bounds.size.height + TGPhotoEditorBlurViewOverscreenSize * 2); - _rightView.frame = CGRectMake(_maskView.bounds.size.width, -TGPhotoEditorBlurViewOverscreenSize, TGPhotoEditorBlurViewOverscreenSize, _maskView.bounds.size.height + TGPhotoEditorBlurViewOverscreenSize * 2); - } - else - { - _maskView.frame = CGRectMake(-TGPhotoEditorBlurViewOverscreenSize, 0, self.bounds.size.width + TGPhotoEditorBlurViewOverscreenSize * 2, self.bounds.size.height); - _topView.frame = CGRectMake(_maskView.frame.origin.x, -TGPhotoEditorBlurViewOverscreenSize, _maskView.bounds.size.width, TGPhotoEditorBlurViewOverscreenSize); - _bottomView.frame = CGRectMake(_maskView.frame.origin.x, _maskView.bounds.size.height, _maskView.bounds.size.width, TGPhotoEditorBlurViewOverscreenSize); - } -} - -@end diff --git a/submodules/LegacyComponents/Sources/TGPhotoEditorSliderView.m b/submodules/LegacyComponents/Sources/TGPhotoEditorSliderView.m index 11250f7b87..5a2e49f5c9 100644 --- a/submodules/LegacyComponents/Sources/TGPhotoEditorSliderView.m +++ b/submodules/LegacyComponents/Sources/TGPhotoEditorSliderView.m @@ -396,7 +396,9 @@ const CGFloat TGPhotoEditorSliderViewInternalMargin = 7.0f; if (_minimumValue < 0) { CGFloat knob = knobSize; - if ((NSInteger)value == 0) + if (fabs(_minimumValue) > 1.0 && (NSInteger)value == 0) { + return totalLength / 2; + } else if (fabs(value) < 0.01) { return totalLength / 2; } diff --git a/submodules/LegacyMediaPickerUI/Sources/LegacyPaintStickersContext.swift b/submodules/LegacyMediaPickerUI/Sources/LegacyPaintStickersContext.swift index 05cabcdd74..1ab174a644 100644 --- a/submodules/LegacyMediaPickerUI/Sources/LegacyPaintStickersContext.swift +++ b/submodules/LegacyMediaPickerUI/Sources/LegacyPaintStickersContext.swift @@ -576,7 +576,7 @@ public final class LegacyPaintStickersContext: NSObject, TGPhotoPaintStickersCon let interfaceController: TGPhotoDrawingInterfaceController init(context: AccountContext, size: CGSize, originalSize: CGSize, isVideo: Bool, isAvatar: Bool, entitiesView: (UIView & TGPhotoDrawingEntitiesView)?) { - let interfaceController = DrawingScreen(context: context, size: size, originalSize: originalSize, isVideo: isVideo, isAvatar: isAvatar, entitiesView: entitiesView) + let interfaceController = DrawingScreen(context: context, size: size, originalSize: originalSize, isVideo: isVideo, isAvatar: isAvatar, drawingView: nil, entitiesView: entitiesView) self.interfaceController = interfaceController self.drawingView = interfaceController.drawingView self.drawingEntitiesView = interfaceController.entitiesView diff --git a/submodules/MediaPickerUI/Sources/MediaPickerGridItem.swift b/submodules/MediaPickerUI/Sources/MediaPickerGridItem.swift index 6d65780560..830f09d8f9 100644 --- a/submodules/MediaPickerUI/Sources/MediaPickerGridItem.swift +++ b/submodules/MediaPickerUI/Sources/MediaPickerGridItem.swift @@ -129,6 +129,8 @@ final class MediaPickerGridItemNode: GridItemNode { super.init() + self.clipsToBounds = true + self.addSubnode(self.imageNode) self.addSubnode(self.activateAreaNode) @@ -396,7 +398,7 @@ final class MediaPickerGridItemNode: GridItemNode { override func layout() { super.layout() - self.imageNode.frame = self.bounds + self.imageNode.frame = self.bounds.insetBy(dx: -1.0 + UIScreenPixel, dy: -1.0 + UIScreenPixel) self.gradientNode.frame = CGRect(x: 0.0, y: self.bounds.height - 24.0, width: self.bounds.width, height: 24.0) self.typeIconNode.frame = CGRect(x: 0.0, y: self.bounds.height - 20.0, width: 19.0, height: 19.0) self.activateAreaNode.frame = self.bounds diff --git a/submodules/QrCodeUI/Sources/QrCodeScanScreen.swift b/submodules/QrCodeUI/Sources/QrCodeScanScreen.swift index 1a24b6e5f1..72a262891b 100644 --- a/submodules/QrCodeUI/Sources/QrCodeScanScreen.swift +++ b/submodules/QrCodeUI/Sources/QrCodeScanScreen.swift @@ -513,7 +513,7 @@ private final class QrCodeScanScreenNode: ViewControllerTracingNode, UIScrollVie self.errorTextNode.textAlignment = .center self.errorTextNode.isHidden = true - self.camera = Camera(configuration: .init(preset: .hd1920x1080, position: .back, audio: false, photo: true, metadata: false)) + self.camera = Camera(configuration: .init(preset: .hd1920x1080, position: .back, audio: false, photo: true, metadata: false, preferredFps: 60)) super.init() diff --git a/submodules/SettingsUI/Sources/Privacy and Security/PrivacyAndSecurityController.swift b/submodules/SettingsUI/Sources/Privacy and Security/PrivacyAndSecurityController.swift index 0b2baf2d02..d5282ccca1 100644 --- a/submodules/SettingsUI/Sources/Privacy and Security/PrivacyAndSecurityController.swift +++ b/submodules/SettingsUI/Sources/Privacy and Security/PrivacyAndSecurityController.swift @@ -30,6 +30,7 @@ private final class PrivacyAndSecurityControllerArguments { let openForwardPrivacy: () -> Void let openPhoneNumberPrivacy: () -> Void let openVoiceMessagePrivacy: () -> Void + let openBioPrivacy: () -> Void let openPasscode: () -> Void let openTwoStepVerification: (TwoStepVerificationAccessConfiguration?) -> Void let openActiveSessions: () -> Void @@ -39,7 +40,7 @@ private final class PrivacyAndSecurityControllerArguments { let openDataSettings: () -> Void let openEmailSettings: (String?) -> Void - init(account: Account, openBlockedUsers: @escaping () -> Void, openLastSeenPrivacy: @escaping () -> Void, openGroupsPrivacy: @escaping () -> Void, openVoiceCallPrivacy: @escaping () -> Void, openProfilePhotoPrivacy: @escaping () -> Void, openForwardPrivacy: @escaping () -> Void, openPhoneNumberPrivacy: @escaping () -> Void, openVoiceMessagePrivacy: @escaping () -> Void, openPasscode: @escaping () -> Void, openTwoStepVerification: @escaping (TwoStepVerificationAccessConfiguration?) -> Void, openActiveSessions: @escaping () -> Void, toggleArchiveAndMuteNonContacts: @escaping (Bool) -> Void, setupAccountAutoremove: @escaping () -> Void, setupMessageAutoremove: @escaping () -> Void, openDataSettings: @escaping () -> Void, openEmailSettings: @escaping (String?) -> Void) { + init(account: Account, openBlockedUsers: @escaping () -> Void, openLastSeenPrivacy: @escaping () -> Void, openGroupsPrivacy: @escaping () -> Void, openVoiceCallPrivacy: @escaping () -> Void, openProfilePhotoPrivacy: @escaping () -> Void, openForwardPrivacy: @escaping () -> Void, openPhoneNumberPrivacy: @escaping () -> Void, openVoiceMessagePrivacy: @escaping () -> Void, openBioPrivacy: @escaping () -> Void, openPasscode: @escaping () -> Void, openTwoStepVerification: @escaping (TwoStepVerificationAccessConfiguration?) -> Void, openActiveSessions: @escaping () -> Void, toggleArchiveAndMuteNonContacts: @escaping (Bool) -> Void, setupAccountAutoremove: @escaping () -> Void, setupMessageAutoremove: @escaping () -> Void, openDataSettings: @escaping () -> Void, openEmailSettings: @escaping (String?) -> Void) { self.account = account self.openBlockedUsers = openBlockedUsers self.openLastSeenPrivacy = openLastSeenPrivacy @@ -49,6 +50,7 @@ private final class PrivacyAndSecurityControllerArguments { self.openForwardPrivacy = openForwardPrivacy self.openPhoneNumberPrivacy = openPhoneNumberPrivacy self.openVoiceMessagePrivacy = openVoiceMessagePrivacy + self.openBioPrivacy = openBioPrivacy self.openPasscode = openPasscode self.openTwoStepVerification = openTwoStepVerification self.openActiveSessions = openActiveSessions @@ -93,6 +95,7 @@ private enum PrivacyAndSecurityEntry: ItemListNodeEntry { case forwardPrivacy(PresentationTheme, String, String) case groupPrivacy(PresentationTheme, String, String) case voiceMessagePrivacy(PresentationTheme, String, String, Bool) + case bioPrivacy(PresentationTheme, String, String) case selectivePrivacyInfo(PresentationTheme, String) case passcode(PresentationTheme, String, Bool, String) case twoStepVerification(PresentationTheme, String, String, TwoStepVerificationAccessConfiguration?) @@ -114,7 +117,7 @@ private enum PrivacyAndSecurityEntry: ItemListNodeEntry { switch self { case .blockedPeers, .activeSessions, .passcode, .twoStepVerification, .loginEmail, .loginEmailInfo, .messageAutoremoveTimeout, .messageAutoremoveInfo: return PrivacyAndSecuritySection.general.rawValue - case .privacyHeader, .phoneNumberPrivacy, .lastSeenPrivacy, .profilePhotoPrivacy, .forwardPrivacy, .groupPrivacy, .voiceCallPrivacy, .voiceMessagePrivacy, .selectivePrivacyInfo: + case .privacyHeader, .phoneNumberPrivacy, .lastSeenPrivacy, .profilePhotoPrivacy, .forwardPrivacy, .groupPrivacy, .voiceCallPrivacy, .voiceMessagePrivacy, .bioPrivacy, .selectivePrivacyInfo: return PrivacyAndSecuritySection.privacy.rawValue case .autoArchiveHeader, .autoArchive, .autoArchiveInfo: return PrivacyAndSecuritySection.autoArchive.rawValue @@ -159,24 +162,26 @@ private enum PrivacyAndSecurityEntry: ItemListNodeEntry { return 15 case .groupPrivacy: return 16 - case .selectivePrivacyInfo: + case .bioPrivacy: return 17 - case .autoArchiveHeader: + case .selectivePrivacyInfo: return 18 - case .autoArchive: + case .autoArchiveHeader: return 19 - case .autoArchiveInfo: + case .autoArchive: return 20 - case .accountHeader: + case .autoArchiveInfo: return 21 - case .accountTimeout: + case .accountHeader: return 22 - case .accountInfo: + case .accountTimeout: return 23 - case .dataSettings: + case .accountInfo: return 24 - case .dataSettingsInfo: + case .dataSettings: return 25 + case .dataSettingsInfo: + return 26 } } @@ -236,6 +241,12 @@ private enum PrivacyAndSecurityEntry: ItemListNodeEntry { } else { return false } + case let .bioPrivacy(lhsTheme, lhsText, lhsValue): + if case let .bioPrivacy(rhsTheme, rhsText, rhsValue) = rhs, lhsTheme === rhsTheme, lhsText == rhsText, lhsValue == rhsValue { + return true + } else { + return false + } case let .selectivePrivacyInfo(lhsTheme, lhsText): if case let .selectivePrivacyInfo(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText { return true @@ -372,6 +383,10 @@ private enum PrivacyAndSecurityEntry: ItemListNodeEntry { return ItemListDisclosureItem(presentationData: presentationData, title: text, label: value, labelStyle: locked ? .textWithIcon(UIImage(bundleImageName: "Chat/Input/Accessory Panels/TextLockIcon")!.precomposed()) : .text, sectionId: self.section, style: .blocks, action: { arguments.openVoiceMessagePrivacy() }) + case let .bioPrivacy(_, text, value): + return ItemListDisclosureItem(presentationData: presentationData, title: text, label: value, sectionId: self.section, style: .blocks, action: { + arguments.openBioPrivacy() + }) case let .selectivePrivacyInfo(_, text): return ItemListTextItem(presentationData: presentationData, text: .plain(text), sectionId: self.section) case let .voiceCallPrivacy(_, text, value): @@ -549,6 +564,7 @@ private func privacyAndSecurityControllerEntries( } entries.append(.forwardPrivacy(presentationData.theme, presentationData.strings.Privacy_Forwards, stringForSelectiveSettings(strings: presentationData.strings, settings: privacySettings.forwards))) entries.append(.groupPrivacy(presentationData.theme, presentationData.strings.Privacy_GroupsAndChannels, stringForSelectiveSettings(strings: presentationData.strings, settings: privacySettings.groupInvitations))) + entries.append(.bioPrivacy(presentationData.theme, presentationData.strings.Privacy_Bio, stringForSelectiveSettings(strings: presentationData.strings, settings: privacySettings.bio))) entries.append(.selectivePrivacyInfo(presentationData.theme, presentationData.strings.PrivacyLastSeenSettings_GroupsAndChannelsHelp)) } else { @@ -560,6 +576,7 @@ private func privacyAndSecurityControllerEntries( } entries.append(.forwardPrivacy(presentationData.theme, presentationData.strings.Privacy_Forwards, presentationData.strings.Channel_NotificationLoading)) entries.append(.groupPrivacy(presentationData.theme, presentationData.strings.Privacy_GroupsAndChannels, presentationData.strings.Channel_NotificationLoading)) + entries.append(.bioPrivacy(presentationData.theme, presentationData.strings.Privacy_GroupsAndChannels, presentationData.strings.Channel_NotificationLoading)) entries.append(.selectivePrivacyInfo(presentationData.theme, presentationData.strings.PrivacyLastSeenSettings_GroupsAndChannelsHelp)) } @@ -744,7 +761,7 @@ public func privacyAndSecurityController( |> deliverOnMainQueue |> mapToSignal { value -> Signal in if let value = value { - privacySettingsPromise.set(.single(AccountPrivacySettings(presence: updated, groupInvitations: value.groupInvitations, voiceCalls: value.voiceCalls, voiceCallsP2P: value.voiceCallsP2P, profilePhoto: value.profilePhoto, forwards: value.forwards, phoneNumber: value.phoneNumber, phoneDiscoveryEnabled: value.phoneDiscoveryEnabled, voiceMessages: value.voiceMessages, automaticallyArchiveAndMuteNonContacts: value.automaticallyArchiveAndMuteNonContacts, accountRemovalTimeout: value.accountRemovalTimeout, messageAutoremoveTimeout: value.messageAutoremoveTimeout))) + privacySettingsPromise.set(.single(AccountPrivacySettings(presence: updated, groupInvitations: value.groupInvitations, voiceCalls: value.voiceCalls, voiceCallsP2P: value.voiceCallsP2P, profilePhoto: value.profilePhoto, forwards: value.forwards, phoneNumber: value.phoneNumber, phoneDiscoveryEnabled: value.phoneDiscoveryEnabled, voiceMessages: value.voiceMessages, bio: value.bio, automaticallyArchiveAndMuteNonContacts: value.automaticallyArchiveAndMuteNonContacts, accountRemovalTimeout: value.accountRemovalTimeout, messageAutoremoveTimeout: value.messageAutoremoveTimeout))) } return .complete() } @@ -767,7 +784,7 @@ public func privacyAndSecurityController( |> deliverOnMainQueue |> mapToSignal { value -> Signal in if let value = value { - privacySettingsPromise.set(.single(AccountPrivacySettings(presence: value.presence, groupInvitations: updated, voiceCalls: value.voiceCalls, voiceCallsP2P: value.voiceCallsP2P, profilePhoto: value.profilePhoto, forwards: value.forwards, phoneNumber: value.phoneNumber, phoneDiscoveryEnabled: value.phoneDiscoveryEnabled, voiceMessages: value.voiceMessages, automaticallyArchiveAndMuteNonContacts: value.automaticallyArchiveAndMuteNonContacts, accountRemovalTimeout: value.accountRemovalTimeout, messageAutoremoveTimeout: value.messageAutoremoveTimeout))) + privacySettingsPromise.set(.single(AccountPrivacySettings(presence: value.presence, groupInvitations: updated, voiceCalls: value.voiceCalls, voiceCallsP2P: value.voiceCallsP2P, profilePhoto: value.profilePhoto, forwards: value.forwards, phoneNumber: value.phoneNumber, phoneDiscoveryEnabled: value.phoneDiscoveryEnabled, voiceMessages: value.voiceMessages, bio: value.bio, automaticallyArchiveAndMuteNonContacts: value.automaticallyArchiveAndMuteNonContacts, accountRemovalTimeout: value.accountRemovalTimeout, messageAutoremoveTimeout: value.messageAutoremoveTimeout))) } return .complete() } @@ -804,7 +821,7 @@ public func privacyAndSecurityController( |> deliverOnMainQueue |> mapToSignal { value -> Signal in if let value = value { - privacySettingsPromise.set(.single(AccountPrivacySettings(presence: value.presence, groupInvitations: value.groupInvitations, voiceCalls: updated, voiceCallsP2P: updatedCallsPrivacy, profilePhoto: value.profilePhoto, forwards: value.forwards, phoneNumber: value.phoneNumber, phoneDiscoveryEnabled: value.phoneDiscoveryEnabled, voiceMessages: value.voiceMessages, automaticallyArchiveAndMuteNonContacts: value.automaticallyArchiveAndMuteNonContacts, accountRemovalTimeout: value.accountRemovalTimeout, messageAutoremoveTimeout: value.messageAutoremoveTimeout))) + privacySettingsPromise.set(.single(AccountPrivacySettings(presence: value.presence, groupInvitations: value.groupInvitations, voiceCalls: updated, voiceCallsP2P: updatedCallsPrivacy, profilePhoto: value.profilePhoto, forwards: value.forwards, phoneNumber: value.phoneNumber, phoneDiscoveryEnabled: value.phoneDiscoveryEnabled, voiceMessages: value.voiceMessages, bio: value.bio, automaticallyArchiveAndMuteNonContacts: value.automaticallyArchiveAndMuteNonContacts, accountRemovalTimeout: value.accountRemovalTimeout, messageAutoremoveTimeout: value.messageAutoremoveTimeout))) } return .complete() } @@ -831,7 +848,7 @@ public func privacyAndSecurityController( |> deliverOnMainQueue |> mapToSignal { value -> Signal in if let value = value { - privacySettingsPromise.set(.single(AccountPrivacySettings(presence: value.presence, groupInvitations: value.groupInvitations, voiceCalls: value.voiceCalls, voiceCallsP2P: value.voiceCallsP2P, profilePhoto: updated, forwards: value.forwards, phoneNumber: value.phoneNumber, phoneDiscoveryEnabled: value.phoneDiscoveryEnabled, voiceMessages: value.voiceMessages, automaticallyArchiveAndMuteNonContacts: value.automaticallyArchiveAndMuteNonContacts, accountRemovalTimeout: value.accountRemovalTimeout, messageAutoremoveTimeout: value.messageAutoremoveTimeout))) + privacySettingsPromise.set(.single(AccountPrivacySettings(presence: value.presence, groupInvitations: value.groupInvitations, voiceCalls: value.voiceCalls, voiceCallsP2P: value.voiceCallsP2P, profilePhoto: updated, forwards: value.forwards, phoneNumber: value.phoneNumber, phoneDiscoveryEnabled: value.phoneDiscoveryEnabled, voiceMessages: value.voiceMessages, bio: value.bio, automaticallyArchiveAndMuteNonContacts: value.automaticallyArchiveAndMuteNonContacts, accountRemovalTimeout: value.accountRemovalTimeout, messageAutoremoveTimeout: value.messageAutoremoveTimeout))) } return .complete() } @@ -854,7 +871,7 @@ public func privacyAndSecurityController( |> deliverOnMainQueue |> mapToSignal { value -> Signal in if let value = value { - privacySettingsPromise.set(.single(AccountPrivacySettings(presence: value.presence, groupInvitations: value.groupInvitations, voiceCalls: value.voiceCalls, voiceCallsP2P: value.voiceCallsP2P, profilePhoto: value.profilePhoto, forwards: updated, phoneNumber: value.phoneNumber, phoneDiscoveryEnabled: value.phoneDiscoveryEnabled, voiceMessages: value.voiceMessages, automaticallyArchiveAndMuteNonContacts: value.automaticallyArchiveAndMuteNonContacts, accountRemovalTimeout: value.accountRemovalTimeout, messageAutoremoveTimeout: value.messageAutoremoveTimeout))) + privacySettingsPromise.set(.single(AccountPrivacySettings(presence: value.presence, groupInvitations: value.groupInvitations, voiceCalls: value.voiceCalls, voiceCallsP2P: value.voiceCallsP2P, profilePhoto: value.profilePhoto, forwards: updated, phoneNumber: value.phoneNumber, phoneDiscoveryEnabled: value.phoneDiscoveryEnabled, voiceMessages: value.voiceMessages, bio: value.bio, automaticallyArchiveAndMuteNonContacts: value.automaticallyArchiveAndMuteNonContacts, accountRemovalTimeout: value.accountRemovalTimeout, messageAutoremoveTimeout: value.messageAutoremoveTimeout))) } return .complete() } @@ -877,7 +894,7 @@ public func privacyAndSecurityController( |> deliverOnMainQueue |> mapToSignal { value -> Signal in if let value = value { - privacySettingsPromise.set(.single(AccountPrivacySettings(presence: value.presence, groupInvitations: value.groupInvitations, voiceCalls: value.voiceCalls, voiceCallsP2P: value.voiceCallsP2P, profilePhoto: value.profilePhoto, forwards: value.forwards, phoneNumber: updated, phoneDiscoveryEnabled: updatedDiscoveryEnabled ?? value.phoneDiscoveryEnabled, voiceMessages: value.voiceMessages, automaticallyArchiveAndMuteNonContacts: value.automaticallyArchiveAndMuteNonContacts, accountRemovalTimeout: value.accountRemovalTimeout, messageAutoremoveTimeout: value.messageAutoremoveTimeout))) + privacySettingsPromise.set(.single(AccountPrivacySettings(presence: value.presence, groupInvitations: value.groupInvitations, voiceCalls: value.voiceCalls, voiceCallsP2P: value.voiceCallsP2P, profilePhoto: value.profilePhoto, forwards: value.forwards, phoneNumber: updated, phoneDiscoveryEnabled: updatedDiscoveryEnabled ?? value.phoneDiscoveryEnabled, voiceMessages: value.voiceMessages, bio: value.bio, automaticallyArchiveAndMuteNonContacts: value.automaticallyArchiveAndMuteNonContacts, accountRemovalTimeout: value.accountRemovalTimeout, messageAutoremoveTimeout: value.messageAutoremoveTimeout))) } return .complete() } @@ -907,7 +924,7 @@ public func privacyAndSecurityController( |> deliverOnMainQueue |> mapToSignal { value -> Signal in if let value = value { - privacySettingsPromise.set(.single(AccountPrivacySettings(presence: value.presence, groupInvitations: value.groupInvitations, voiceCalls: value.voiceCalls, voiceCallsP2P: value.voiceCallsP2P, profilePhoto: value.profilePhoto, forwards: value.forwards, phoneNumber: value.phoneNumber, phoneDiscoveryEnabled: value.phoneDiscoveryEnabled, voiceMessages: updated, automaticallyArchiveAndMuteNonContacts: value.automaticallyArchiveAndMuteNonContacts, accountRemovalTimeout: value.accountRemovalTimeout, messageAutoremoveTimeout: value.messageAutoremoveTimeout))) + privacySettingsPromise.set(.single(AccountPrivacySettings(presence: value.presence, groupInvitations: value.groupInvitations, voiceCalls: value.voiceCalls, voiceCallsP2P: value.voiceCallsP2P, profilePhoto: value.profilePhoto, forwards: value.forwards, phoneNumber: value.phoneNumber, phoneDiscoveryEnabled: value.phoneDiscoveryEnabled, voiceMessages: updated, bio: value.bio, automaticallyArchiveAndMuteNonContacts: value.automaticallyArchiveAndMuteNonContacts, accountRemovalTimeout: value.accountRemovalTimeout, messageAutoremoveTimeout: value.messageAutoremoveTimeout))) } return .complete() } @@ -933,6 +950,29 @@ public func privacyAndSecurityController( })) } })) + }, openBioPrivacy: { + let signal = privacySettingsPromise.get() + |> take(1) + |> deliverOnMainQueue + currentInfoDisposable.set(signal.start(next: { [weak currentInfoDisposable] info in + if let info = info { + pushControllerImpl?(selectivePrivacySettingsController(context: context, kind: .bio, current: info.bio, updated: { updated, _, updatedDiscoveryEnabled in + if let currentInfoDisposable = currentInfoDisposable { + let applySetting: Signal = privacySettingsPromise.get() + |> filter { $0 != nil } + |> take(1) + |> deliverOnMainQueue + |> mapToSignal { value -> Signal in + if let value = value { + privacySettingsPromise.set(.single(AccountPrivacySettings(presence: value.presence, groupInvitations: value.groupInvitations, voiceCalls: value.voiceCalls, voiceCallsP2P: value.voiceCallsP2P, profilePhoto: value.profilePhoto, forwards: value.forwards, phoneNumber: value.phoneNumber, phoneDiscoveryEnabled: value.phoneDiscoveryEnabled, voiceMessages: value.voiceMessages, bio: updated, automaticallyArchiveAndMuteNonContacts: value.automaticallyArchiveAndMuteNonContacts, accountRemovalTimeout: value.accountRemovalTimeout, messageAutoremoveTimeout: value.messageAutoremoveTimeout))) + } + return .complete() + } + currentInfoDisposable.set(applySetting.start()) + } + }), true) + } + })) }, openPasscode: { let _ = passcodeOptionsAccessController(context: context, pushController: { controller in replaceTopControllerImpl?(controller) @@ -989,7 +1029,7 @@ public func privacyAndSecurityController( |> deliverOnMainQueue |> mapToSignal { value -> Signal in if let value = value { - privacySettingsPromise.set(.single(AccountPrivacySettings(presence: value.presence, groupInvitations: value.groupInvitations, voiceCalls: value.voiceCalls, voiceCallsP2P: value.voiceCallsP2P, profilePhoto: value.profilePhoto, forwards: value.forwards, phoneNumber: value.phoneNumber, phoneDiscoveryEnabled: value.phoneDiscoveryEnabled, voiceMessages: value.voiceMessages, automaticallyArchiveAndMuteNonContacts: archiveValue, accountRemovalTimeout: value.accountRemovalTimeout, messageAutoremoveTimeout: value.messageAutoremoveTimeout))) + privacySettingsPromise.set(.single(AccountPrivacySettings(presence: value.presence, groupInvitations: value.groupInvitations, voiceCalls: value.voiceCalls, voiceCallsP2P: value.voiceCallsP2P, profilePhoto: value.profilePhoto, forwards: value.forwards, phoneNumber: value.phoneNumber, phoneDiscoveryEnabled: value.phoneDiscoveryEnabled, voiceMessages: value.voiceMessages, bio: value.bio, automaticallyArchiveAndMuteNonContacts: archiveValue, accountRemovalTimeout: value.accountRemovalTimeout, messageAutoremoveTimeout: value.messageAutoremoveTimeout))) } return .complete() } @@ -1028,7 +1068,7 @@ public func privacyAndSecurityController( |> deliverOnMainQueue |> mapToSignal { value -> Signal in if let value = value { - privacySettingsPromise.set(.single(AccountPrivacySettings(presence: value.presence, groupInvitations: value.groupInvitations, voiceCalls: value.voiceCalls, voiceCallsP2P: value.voiceCallsP2P, profilePhoto: value.profilePhoto, forwards: value.forwards, phoneNumber: value.phoneNumber, phoneDiscoveryEnabled: value.phoneDiscoveryEnabled, voiceMessages: value.voiceMessages, automaticallyArchiveAndMuteNonContacts: value.automaticallyArchiveAndMuteNonContacts, accountRemovalTimeout: timeout, messageAutoremoveTimeout: value.messageAutoremoveTimeout))) + privacySettingsPromise.set(.single(AccountPrivacySettings(presence: value.presence, groupInvitations: value.groupInvitations, voiceCalls: value.voiceCalls, voiceCallsP2P: value.voiceCallsP2P, profilePhoto: value.profilePhoto, forwards: value.forwards, phoneNumber: value.phoneNumber, phoneDiscoveryEnabled: value.phoneDiscoveryEnabled, voiceMessages: value.voiceMessages, bio: value.bio, automaticallyArchiveAndMuteNonContacts: value.automaticallyArchiveAndMuteNonContacts, accountRemovalTimeout: timeout, messageAutoremoveTimeout: value.messageAutoremoveTimeout))) } return .complete() } @@ -1089,7 +1129,7 @@ public func privacyAndSecurityController( |> deliverOnMainQueue |> mapToSignal { value -> Signal in if let value = value { - privacySettingsPromise.set(.single(AccountPrivacySettings(presence: value.presence, groupInvitations: value.groupInvitations, voiceCalls: value.voiceCalls, voiceCallsP2P: value.voiceCallsP2P, profilePhoto: value.profilePhoto, forwards: value.forwards, phoneNumber: value.phoneNumber, phoneDiscoveryEnabled: value.phoneDiscoveryEnabled, voiceMessages: value.voiceMessages, automaticallyArchiveAndMuteNonContacts: value.automaticallyArchiveAndMuteNonContacts, accountRemovalTimeout: value.accountRemovalTimeout, messageAutoremoveTimeout: updatedValue == 0 ? nil : updatedValue))) + privacySettingsPromise.set(.single(AccountPrivacySettings(presence: value.presence, groupInvitations: value.groupInvitations, voiceCalls: value.voiceCalls, voiceCallsP2P: value.voiceCallsP2P, profilePhoto: value.profilePhoto, forwards: value.forwards, phoneNumber: value.phoneNumber, phoneDiscoveryEnabled: value.phoneDiscoveryEnabled, voiceMessages: value.voiceMessages, bio: value.bio, automaticallyArchiveAndMuteNonContacts: value.automaticallyArchiveAndMuteNonContacts, accountRemovalTimeout: value.accountRemovalTimeout, messageAutoremoveTimeout: updatedValue == 0 ? nil : updatedValue))) } return .complete() } diff --git a/submodules/SettingsUI/Sources/Privacy and Security/SelectivePrivacySettingsController.swift b/submodules/SettingsUI/Sources/Privacy and Security/SelectivePrivacySettingsController.swift index f1287de315..8f49fa2baa 100644 --- a/submodules/SettingsUI/Sources/Privacy and Security/SelectivePrivacySettingsController.swift +++ b/submodules/SettingsUI/Sources/Privacy and Security/SelectivePrivacySettingsController.swift @@ -20,6 +20,7 @@ enum SelectivePrivacySettingsKind { case forwards case phoneNumber case voiceMessages + case bio } private enum SelectivePrivacySettingType { @@ -644,6 +645,11 @@ private func selectivePrivacySettingsControllerEntries(presentationData: Present settingInfoText = presentationData.strings.Privacy_VoiceMessages_CustomHelp disableForText = presentationData.strings.Privacy_GroupsAndChannels_NeverAllow enableForText = presentationData.strings.Privacy_GroupsAndChannels_AlwaysAllow + case .bio: + settingTitle = presentationData.strings.Privacy_Bio_WhoCanSeeMyBio + settingInfoText = presentationData.strings.Privacy_Bio_CustomHelp + disableForText = presentationData.strings.PrivacyLastSeenSettings_NeverShareWith + enableForText = presentationData.strings.PrivacyLastSeenSettings_AlwaysShareWith } if case .forwards = kind { @@ -827,6 +833,8 @@ func selectivePrivacySettingsController( title = strings.PrivacyLastSeenSettings_AlwaysShareWith_Title case .voiceMessages: title = strings.Privacy_VoiceMessages_AlwaysAllow_Title + case .bio: + title = strings.Privacy_Bio_AlwaysShareWith_Title } } else { switch kind { @@ -844,6 +852,8 @@ func selectivePrivacySettingsController( title = strings.PrivacyLastSeenSettings_NeverShareWith_Title case .voiceMessages: title = strings.Privacy_VoiceMessages_NeverAllow_Title + case .bio: + title = strings.Privacy_Bio_NeverShareWith_Title } } var peerIds: [EnginePeer.Id: SelectivePrivacyPeer] = [:] @@ -1080,6 +1090,8 @@ func selectivePrivacySettingsController( title = presentationData.strings.Privacy_PhoneNumber case .voiceMessages: title = presentationData.strings.Privacy_VoiceMessages + case .bio: + title = presentationData.strings.Privacy_Bio } let controllerState = ItemListControllerState(presentationData: ItemListPresentationData(presentationData), title: .text(title), leftNavigationButton: nil, rightNavigationButton: nil, backNavigationButton: ItemListBackButton(title: presentationData.strings.Common_Back), animateChanges: false) let listState = ItemListNodeState(presentationData: ItemListPresentationData(presentationData), entries: selectivePrivacySettingsControllerEntries(presentationData: presentationData, kind: kind, state: state, peerName: peerName ?? "", phoneNumber: phoneNumber, peer: peer, publicPhoto: publicPhoto), style: .blocks, animateChanges: true) @@ -1160,6 +1172,8 @@ func selectivePrivacySettingsController( type = .phoneNumber case .voiceMessages: type = .voiceMessages + case .bio: + type = .bio } let updateSettingsSignal = context.engine.privacy.updateSelectiveAccountPrivacySettings(type: type, settings: settings) diff --git a/submodules/SettingsUI/Sources/Search/SettingsSearchableItems.swift b/submodules/SettingsUI/Sources/Search/SettingsSearchableItems.swift index bd3b7a722c..fbed9c2c50 100644 --- a/submodules/SettingsUI/Sources/Search/SettingsSearchableItems.swift +++ b/submodules/SettingsUI/Sources/Search/SettingsSearchableItems.swift @@ -566,6 +566,8 @@ private func privacySearchableItems(context: AccountContext, privacySettings: Ac current = info.phoneNumber case .voiceMessages: current = info.voiceMessages + case .bio: + current = info.bio } present(.push, selectivePrivacySettingsController(context: context, kind: kind, current: current, callSettings: callSettings != nil ? (info.voiceCallsP2P, callSettings!.0) : nil, voipConfiguration: callSettings?.1, callIntegrationAvailable: CallKitIntegration.isAvailable, updated: { updated, updatedCallSettings, _ in @@ -740,24 +742,12 @@ private func dataSearchableItems(context: AccountContext) -> [SettingsSearchable SettingsSearchableItem(id: .data(7), title: strings.ChatSettings_AutoDownloadReset, alternate: synonyms(strings.SettingsSearch_Synonyms_Data_AutoDownloadReset), icon: icon, breadcrumbs: [strings.Settings_ChatSettings], present: { context, _, present in presentDataSettings(context, present, .automaticDownloadReset) }), - /*SettingsSearchableItem(id: .data(8), title: strings.ChatSettings_AutoPlayGifs, alternate: synonyms(strings.SettingsSearch_Synonyms_Data_AutoplayGifs), icon: icon, breadcrumbs: [strings.Settings_ChatSettings, strings.ChatSettings_AutoPlayTitle], present: { context, _, present in - presentDataSettings(context, present, .autoplayGifs) - }),*/ - /*SettingsSearchableItem(id: .data(9), title: strings.ChatSettings_AutoPlayVideos, alternate: synonyms(strings.SettingsSearch_Synonyms_Data_AutoplayVideos), icon: icon, breadcrumbs: [strings.Settings_ChatSettings, strings.ChatSettings_AutoPlayTitle], present: { context, _, present in - presentDataSettings(context, present, .autoplayVideos) - }),*/ SettingsSearchableItem(id: .data(10), title: strings.CallSettings_UseLessData, alternate: synonyms(strings.SettingsSearch_Synonyms_Data_CallsUseLessData), icon: icon, breadcrumbs: [strings.Settings_ChatSettings, strings.Settings_CallSettings], present: { context, _, present in present(.push, voiceCallDataSavingController(context: context)) }), - /*SettingsSearchableItem(id: .data(11), title: strings.Settings_SaveIncomingPhotos, alternate: synonyms(strings.SettingsSearch_Synonyms_Data_SaveIncomingPhotos), icon: icon, breadcrumbs: [strings.Settings_ChatSettings], present: { context, _, present in - present(.push, saveIncomingMediaController(context: context)) - }),*/ SettingsSearchableItem(id: .data(12), title: strings.Settings_SaveEditedPhotos, alternate: synonyms(strings.SettingsSearch_Synonyms_Data_SaveEditedPhotos), icon: icon, breadcrumbs: [strings.Settings_ChatSettings], present: { context, _, present in presentDataSettings(context, present, .saveEditedPhotos) }), - SettingsSearchableItem(id: .data(13), title: strings.ChatSettings_DownloadInBackground, alternate: synonyms(strings.SettingsSearch_Synonyms_Data_DownloadInBackground), icon: icon, breadcrumbs: [strings.Settings_ChatSettings], present: { context, _, present in - presentDataSettings(context, present, .downloadInBackground) - }), SettingsSearchableItem(id: .data(14), title: strings.ChatSettings_OpenLinksIn, alternate: synonyms(strings.SettingsSearch_Synonyms_ChatSettings_OpenLinksIn), icon: icon, breadcrumbs: [strings.Settings_ChatSettings], present: { context, _, present in present(.push, webBrowserSettingsController(context: context)) }), @@ -831,9 +821,6 @@ private func appearanceSearchableItems(context: AccountContext) -> [SettingsSear }), SettingsSearchableItem(id: .appearance(6), title: strings.Appearance_ColorTheme, alternate: synonyms(strings.SettingsSearch_Synonyms_Appearance_ColorTheme), icon: icon, breadcrumbs: [strings.Settings_Appearance], present: { context, _, present in presentAppearanceSettings(context, present, .accentColor) - }), - SettingsSearchableItem(id: .appearance(8), title: strings.Appearance_ReduceMotion, alternate: synonyms(strings.SettingsSearch_Synonyms_Appearance_Animations), icon: icon, breadcrumbs: [strings.Settings_Appearance, strings.Appearance_Other], present: { context, _, present in - presentAppearanceSettings(context, present, .animations) }) ] } diff --git a/submodules/TelegramCore/Sources/Settings/PrivacySettings.swift b/submodules/TelegramCore/Sources/Settings/PrivacySettings.swift index ddaea62247..61dc05ae6f 100644 --- a/submodules/TelegramCore/Sources/Settings/PrivacySettings.swift +++ b/submodules/TelegramCore/Sources/Settings/PrivacySettings.swift @@ -94,12 +94,13 @@ public struct AccountPrivacySettings: Equatable { public let phoneNumber: SelectivePrivacySettings public let phoneDiscoveryEnabled: Bool public let voiceMessages: SelectivePrivacySettings + public let bio: SelectivePrivacySettings public let automaticallyArchiveAndMuteNonContacts: Bool public let accountRemovalTimeout: Int32 public let messageAutoremoveTimeout: Int32? - public init(presence: SelectivePrivacySettings, groupInvitations: SelectivePrivacySettings, voiceCalls: SelectivePrivacySettings, voiceCallsP2P: SelectivePrivacySettings, profilePhoto: SelectivePrivacySettings, forwards: SelectivePrivacySettings, phoneNumber: SelectivePrivacySettings, phoneDiscoveryEnabled: Bool, voiceMessages: SelectivePrivacySettings, automaticallyArchiveAndMuteNonContacts: Bool, accountRemovalTimeout: Int32, messageAutoremoveTimeout: Int32?) { + public init(presence: SelectivePrivacySettings, groupInvitations: SelectivePrivacySettings, voiceCalls: SelectivePrivacySettings, voiceCallsP2P: SelectivePrivacySettings, profilePhoto: SelectivePrivacySettings, forwards: SelectivePrivacySettings, phoneNumber: SelectivePrivacySettings, phoneDiscoveryEnabled: Bool, voiceMessages: SelectivePrivacySettings, bio: SelectivePrivacySettings, automaticallyArchiveAndMuteNonContacts: Bool, accountRemovalTimeout: Int32, messageAutoremoveTimeout: Int32?) { self.presence = presence self.groupInvitations = groupInvitations self.voiceCalls = voiceCalls @@ -109,6 +110,7 @@ public struct AccountPrivacySettings: Equatable { self.phoneNumber = phoneNumber self.phoneDiscoveryEnabled = phoneDiscoveryEnabled self.voiceMessages = voiceMessages + self.bio = bio self.automaticallyArchiveAndMuteNonContacts = automaticallyArchiveAndMuteNonContacts self.accountRemovalTimeout = accountRemovalTimeout self.messageAutoremoveTimeout = messageAutoremoveTimeout @@ -142,6 +144,9 @@ public struct AccountPrivacySettings: Equatable { if lhs.voiceMessages != rhs.voiceMessages { return false } + if lhs.bio != rhs.bio { + return false + } if lhs.automaticallyArchiveAndMuteNonContacts != rhs.automaticallyArchiveAndMuteNonContacts { return false } diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Privacy/UpdatedAccountPrivacySettings.swift b/submodules/TelegramCore/Sources/TelegramEngine/Privacy/UpdatedAccountPrivacySettings.swift index d06849d902..b5158fc87f 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Privacy/UpdatedAccountPrivacySettings.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Privacy/UpdatedAccountPrivacySettings.swift @@ -14,15 +14,16 @@ func _internal_requestAccountPrivacySettings(account: Account) -> Signal `catch` { _ in return .complete() } - |> mapToSignal { lastSeenPrivacy, groupPrivacy, voiceCallPrivacy, voiceCallP2P, profilePhotoPrivacy, forwardPrivacy, phoneNumberPrivacy, phoneDiscoveryPrivacy, voiceMessagesPrivacy, autoremoveTimeout, globalPrivacySettings, messageAutoremoveTimeout -> Signal in + |> mapToSignal { lastSeenPrivacy, groupPrivacy, voiceCallPrivacy, voiceCallP2P, profilePhotoPrivacy, forwardPrivacy, phoneNumberPrivacy, phoneDiscoveryPrivacy, voiceMessagesPrivacy, bioPrivacy, autoremoveTimeout, globalPrivacySettings, messageAutoremoveTimeout -> Signal in let accountTimeoutSeconds: Int32 switch autoremoveTimeout { case let .accountDaysTTL(days): @@ -47,6 +48,7 @@ func _internal_requestAccountPrivacySettings(account: Account) -> Signal Signal Signal + let minSize: CGSize? + let tag: AnyObject? + let isEnabled: Bool + let action: () -> Void + + init( + content: AnyComponent, + minSize: CGSize? = nil, + tag: AnyObject? = nil, + isEnabled: Bool = true, + action: @escaping () -> Void + ) { + self.content = content + self.minSize = minSize + self.tag = tag + self.isEnabled = isEnabled + self.action = action + } + + func tagged(_ tag: AnyObject) -> CameraButton { + return CameraButton( + content: self.content, + minSize: self.minSize, + tag: tag, + isEnabled: self.isEnabled, + action: self.action + ) + } + + static func ==(lhs: CameraButton, rhs: CameraButton) -> Bool { + if lhs.content != rhs.content { + return false + } + if lhs.minSize != rhs.minSize { + return false + } + if lhs.tag !== rhs.tag { + return false + } + if lhs.isEnabled != rhs.isEnabled { + return false + } + return true + } + + final class View: UIButton, ComponentTaggedView { + private let contentView: ComponentHostView + + private var component: CameraButton? + private var currentIsHighlighted: Bool = false { + didSet { + if self.currentIsHighlighted != oldValue { + self.updateScale(transition: .easeInOut(duration: 0.3)) + } + } + } + + private func updateScale(transition: Transition) { + guard let component = self.component else { + return + } + let scale: CGFloat + if component.isEnabled { + scale = self.currentIsHighlighted ? 0.8 : 1.0 + } else { + scale = 1.0 + } + transition.setScale(view: self, scale: scale) + } + + override init(frame: CGRect) { + self.contentView = ComponentHostView() + self.contentView.isUserInteractionEnabled = false + self.contentView.layer.allowsGroupOpacity = true + + super.init(frame: frame) + + self.addSubview(self.contentView) + + self.addTarget(self, action: #selector(self.pressed), for: .touchUpInside) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + public func matches(tag: Any) -> Bool { + if let component = self.component, let componentTag = component.tag { + let tag = tag as AnyObject + if componentTag === tag { + return true + } + } + return false + } + + @objc private func pressed() { + self.component?.action() + } + + override func beginTracking(_ touch: UITouch, with event: UIEvent?) -> Bool { + self.currentIsHighlighted = true + + return super.beginTracking(touch, with: event) + } + + override func endTracking(_ touch: UITouch?, with event: UIEvent?) { + self.currentIsHighlighted = false + + super.endTracking(touch, with: event) + } + + override func cancelTracking(with event: UIEvent?) { + self.currentIsHighlighted = false + + super.cancelTracking(with: event) + } + + func update(component: CameraButton, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + let contentSize = self.contentView.update( + transition: transition, + component: component.content, + environment: {}, + containerSize: availableSize + ) + + var size = contentSize + if let minSize = component.minSize { + size.width = max(size.width, minSize.width) + size.height = max(size.height, minSize.height) + } + + self.component = component + + self.updateScale(transition: transition) + self.isEnabled = component.isEnabled + + transition.setFrame(view: self.contentView, frame: CGRect(origin: CGPoint(x: floor((size.width - contentSize.width) / 2.0), y: floor((size.height - contentSize.height) / 2.0)), size: contentSize), completion: nil) + + return size + } + } + + func makeView() -> View { + return View(frame: CGRect()) + } + + func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition) + } +} diff --git a/submodules/TelegramUI/Components/CameraScreen/Sources/CameraScreen.swift b/submodules/TelegramUI/Components/CameraScreen/Sources/CameraScreen.swift index c3841cdf53..e25bd3af56 100644 --- a/submodules/TelegramUI/Components/CameraScreen/Sources/CameraScreen.swift +++ b/submodules/TelegramUI/Components/CameraScreen/Sources/CameraScreen.swift @@ -57,9 +57,9 @@ enum CameraScreenTransition { private let cancelButtonTag = GenericComponentViewTag() private let flashButtonTag = GenericComponentViewTag() -private let shutterButtonTag = GenericComponentViewTag() -private let flipButtonTag = GenericComponentViewTag() private let zoomControlTag = GenericComponentViewTag() +private let captureControlsTag = GenericComponentViewTag() +private let modeControlTag = GenericComponentViewTag() private final class CameraScreenComponent: CombinedComponent { typealias EnvironmentType = ViewControllerComponentContainer.Environment @@ -68,23 +68,20 @@ private final class CameraScreenComponent: CombinedComponent { let camera: Camera let present: (ViewController) -> Void let push: (ViewController) -> Void - let completion: (CameraScreen.Result) -> Void - let shootAction: ActionSlot + let completion: ActionSlot> init( context: AccountContext, camera: Camera, present: @escaping (ViewController) -> Void, push: @escaping (ViewController) -> Void, - completion: @escaping (CameraScreen.Result) -> Void, - shootAction: ActionSlot + completion: ActionSlot> ) { self.context = context self.camera = camera self.present = present self.push = push self.completion = completion - self.shootAction = shootAction } static func ==(lhs: CameraScreenComponent, rhs: CameraScreenComponent) -> Bool { @@ -122,8 +119,7 @@ private final class CameraScreenComponent: CombinedComponent { private let context: AccountContext fileprivate let camera: Camera private let present: (ViewController) -> Void - private let completion: (CameraScreen.Result) -> Void - private let shootAction: ActionSlot + private let completion: ActionSlot> private var cameraStateDisposable: Disposable? private var resultDisposable = MetaDisposable() @@ -135,12 +131,11 @@ private final class CameraScreenComponent: CombinedComponent { var cameraState = CameraState(mode: .photo, flashMode: .off, recording: .none, duration: 0.0) var swipeHint: CaptureControlsComponent.SwipeHint = .none - init(context: AccountContext, camera: Camera, present: @escaping (ViewController) -> Void, completion: @escaping (CameraScreen.Result) -> Void, shootAction: ActionSlot) { + init(context: AccountContext, camera: Camera, present: @escaping (ViewController) -> Void, completion: ActionSlot>) { self.context = context self.camera = camera self.present = present self.completion = completion - self.shootAction = shootAction self.mediaAssetsContext = MediaAssetsContext() @@ -185,19 +180,18 @@ private final class CameraScreenComponent: CombinedComponent { } func takePhoto() { - self.resultDisposable.set((self.camera.takePhoto() - |> deliverOnMainQueue).start(next: { [weak self] value in - if let self { - switch value { - case .began: - self.shootAction.invoke(Void()) - case let .finished(image): - self.completion(.image(image)) - case .failed: - break - } + let takePhoto = self.camera.takePhoto() + |> mapToSignal { value -> Signal in + switch value { + case .began: + return .single(.pendingImage) + case let .finished(image): + return .single(.image(image)) + case .failed: + return .complete() } - })) + } + self.completion.invoke(takePhoto) } func startVideoRecording(pressing: Bool) { @@ -217,7 +211,7 @@ private final class CameraScreenComponent: CombinedComponent { self.resultDisposable.set((self.camera.stopRecording() |> deliverOnMainQueue).start(next: { [weak self] path in if let self, let path { - self.completion(.video(path)) + self.completion.invoke(.single(.video(path, PixelDimensions(width: 1080, height: 1920)))) } })) self.updated(transition: .spring(duration: 0.4)) @@ -230,14 +224,14 @@ private final class CameraScreenComponent: CombinedComponent { } func makeState() -> State { - return State(context: self.context, camera: self.camera, present: self.present, completion: self.completion, shootAction: self.shootAction) + return State(context: self.context, camera: self.camera, present: self.present, completion: self.completion) } static var body: Body { - let cancelButton = Child(Button.self) + let cancelButton = Child(CameraButton.self) let captureControls = Child(CaptureControlsComponent.self) let zoomControl = Child(ZoomComponent.self) - let flashButton = Child(Button.self) + let flashButton = Child(CameraButton.self) let modeControl = Child(ModeComponent.self) let hintLabel = Child(MultilineTextComponent.self) @@ -259,7 +253,7 @@ private final class CameraScreenComponent: CombinedComponent { if case .none = state.cameraState.recording { let cancelButton = cancelButton.update( - component: Button( + component: CameraButton( content: AnyComponent(Image( image: state.image(.cancel), size: CGSize(width: 40.0, height: 40.0) @@ -282,8 +276,10 @@ private final class CameraScreenComponent: CombinedComponent { ) let flashButton = flashButton.update( - component: Button( - content: AnyComponent(Image(image: state.image(.flash))), + component: CameraButton( + content: AnyComponent(Image( + image: state.image(.flash) + )), action: { [weak state] in guard let state else { return @@ -345,7 +341,7 @@ private final class CameraScreenComponent: CombinedComponent { component: CaptureControlsComponent( shutterState: shutterState, lastGalleryAsset: state.lastGalleryAsset, - tag: shutterButtonTag, + tag: captureControlsTag, shutterTapped: { [weak state] in guard let state else { return @@ -388,10 +384,10 @@ private final class CameraScreenComponent: CombinedComponent { var dismissGalleryControllerImpl: (() -> Void)? let controller = accountContext.sharedContext.makeMediaPickerScreen(context: accountContext, completion: { asset in dismissGalleryControllerImpl?() - completion(.asset(asset)) + completion.invoke(.single(.asset(asset))) }) dismissGalleryControllerImpl = { [weak controller] in - controller?.dismiss(animated: false) + controller?.dismiss(animated: true) } push(controller) }, @@ -399,11 +395,11 @@ private final class CameraScreenComponent: CombinedComponent { state.updateSwipeHint(hint) } ), - availableSize: context.availableSize, + availableSize: availableSize, transition: context.transition ) context.add(captureControls - .position(CGPoint(x: context.availableSize.width / 2.0, y: context.availableSize.height - captureControls.size.height / 2.0 - 77.0 - environment.safeInsets.bottom)) + .position(CGPoint(x: availableSize.width / 2.0, y: availableSize.height - captureControls.size.height / 2.0 - environment.safeInsets.bottom - 5.0)) ) var isVideoRecording = false @@ -433,14 +429,14 @@ private final class CameraScreenComponent: CombinedComponent { transition: context.transition ) context.add(timeBackground - .position(CGPoint(x: context.availableSize.width / 2.0, y: environment.safeInsets.top + 40.0)) + .position(CGPoint(x: availableSize.width / 2.0, y: environment.safeInsets.top + 40.0)) .appear(.default(alpha: true)) .disappear(.default(alpha: true)) ) } context.add(timeLabel - .position(CGPoint(x: context.availableSize.width / 2.0, y: environment.safeInsets.top + 40.0)) + .position(CGPoint(x: availableSize.width / 2.0, y: environment.safeInsets.top + 40.0)) .appear(.default(alpha: true)) .disappear(.default(alpha: true)) ) @@ -465,11 +461,11 @@ private final class CameraScreenComponent: CombinedComponent { text: .plain(NSAttributedString(string: hintText.uppercased(), font: Font.with(size: 14.0, design: .camera, weight: .semibold), textColor: .white)), horizontalAlignment: .center ), - availableSize: context.availableSize, + availableSize: availableSize, transition: .immediate ) context.add(hintLabel - .position(CGPoint(x: context.availableSize.width / 2.0, y: context.availableSize.height - 35.0 - hintLabel.size.height - environment.safeInsets.bottom)) + .position(CGPoint(x: availableSize.width / 2.0, y: availableSize.height - environment.safeInsets.bottom + 14.0 + hintLabel.size.height / 2.0)) .appear(.default(alpha: true)) .disappear(.default(alpha: true)) ) @@ -486,22 +482,69 @@ private final class CameraScreenComponent: CombinedComponent { if let state { state.updateCameraMode(mode) } - } + }, + tag: modeControlTag ), - availableSize: context.availableSize, + availableSize: availableSize, transition: context.transition ) context.add(modeControl - .position(CGPoint(x: context.availableSize.width / 2.0, y: context.availableSize.height - 7.0 - modeControl.size.height - environment.safeInsets.bottom)) + .clipsToBounds(true) + .position(CGPoint(x: availableSize.width / 2.0, y: availableSize.height - environment.safeInsets.bottom + modeControl.size.height / 2.0)) .appear(.default(alpha: true)) .disappear(.default(alpha: true)) ) } - return context.availableSize + return availableSize } } } +private let useSimplePreviewView = true + +private class BlurView: UIVisualEffectView { + private func setup() { + for subview in self.subviews { + if subview.description.contains("VisualEffectSubview") { + subview.isHidden = true + } + } + + if let sublayer = self.layer.sublayers?[0], let filters = sublayer.filters { + sublayer.backgroundColor = nil + sublayer.isOpaque = false + let allowedKeys: [String] = [ + "gaussianBlur" + ] + sublayer.filters = filters.filter { filter in + guard let filter = filter as? NSObject else { + return true + } + let filterName = String(describing: filter) + if !allowedKeys.contains(filterName) { + return false + } + return true + } + } + } + + override var effect: UIVisualEffect? { + get { + return super.effect + } + set { + super.effect = newValue + self.setup() + } + } + + override func didAddSubview(_ subview: UIView) { + super.didAddSubview(subview) + self.setup() + } +} + public class CameraScreen: ViewController { public enum Mode { case generic @@ -510,8 +553,9 @@ public class CameraScreen: ViewController { } public enum Result { + case pendingImage case image(UIImage) - case video(String) + case video(String, PixelDimensions) case asset(PHAsset) } @@ -524,8 +568,9 @@ public class CameraScreen: ViewController { private let backgroundDimView: UIView fileprivate let componentHost: ComponentView private let previewContainerView: UIView - fileprivate let previewView: CameraPreviewView - fileprivate let previewBlurView: UIVisualEffectView + fileprivate let previewView: CameraPreviewView? + fileprivate let simplePreviewView: CameraSimplePreviewView? + fileprivate let previewBlurView: BlurView fileprivate let camera: Camera private var presentationData: PresentationData @@ -534,13 +579,24 @@ public class CameraScreen: ViewController { private var changingPositionDisposable: Disposable? - private let shootAction: ActionSlot + private let completion = ActionSlot>() + + private var effectivePreviewView: UIView { + if let simplePreviewView = self.simplePreviewView { + return simplePreviewView + } else if let previewView = self.previewView { + return previewView + } else { + fatalError() + } + } + + private var previewBlurPromise = ValuePromise(false) init(controller: CameraScreen) { self.controller = controller self.context = controller.context self.updateState = ActionSlot() - self.shootAction = ActionSlot() self.presentationData = self.context.sharedContext.currentPresentationData.with { $0 } @@ -553,19 +609,30 @@ public class CameraScreen: ViewController { self.previewContainerView = UIView() self.previewContainerView.clipsToBounds = true self.previewContainerView.layer.cornerRadius = 12.0 + if #available(iOS 13.0, *) { + self.previewContainerView.layer.cornerCurve = .continuous + } - self.previewBlurView = UIVisualEffectView(effect: nil) + self.previewBlurView = BlurView() self.previewBlurView.isUserInteractionEnabled = false if let holder = controller.holder { + self.simplePreviewView = nil self.previewView = holder.previewView self.camera = holder.camera } else { - self.previewView = CameraPreviewView(test: false)! - self.camera = Camera(configuration: Camera.Configuration(preset: .hd1920x1080, position: .back, audio: true, photo: true, metadata: false)) - self.camera.attachPreviewView(self.previewView) + if useSimplePreviewView { + self.simplePreviewView = CameraSimplePreviewView() + self.previewView = nil + } else { + self.previewView = CameraPreviewView(test: false)! + self.simplePreviewView = nil + } + self.camera = Camera(configuration: Camera.Configuration(preset: .hd1920x1080, position: .back, audio: true, photo: true, metadata: false, preferredFps: 60.0), previewView: self.simplePreviewView) + if !useSimplePreviewView { + self.camera.attachPreviewView(self.previewView!) + } } - self.previewView.clipsToBounds = true super.init() @@ -575,26 +642,57 @@ public class CameraScreen: ViewController { self.view.addSubview(self.backgroundDimView) self.view.addSubview(self.previewContainerView) - self.previewContainerView.addSubview(self.previewView) + self.previewContainerView.addSubview(self.effectivePreviewView) self.previewContainerView.addSubview(self.previewBlurView) - self.changingPositionDisposable = (self.camera.changingPosition - |> deliverOnMainQueue).start(next: { [weak self] value in + self.changingPositionDisposable = combineLatest( + queue: Queue.mainQueue(), + self.camera.changingPosition, + self.previewBlurPromise.get() + ).start(next: { [weak self] changingPosition, forceBlur in if let self { - UIView.animate(withDuration: 0.5) { - if value { - if #available(iOS 13.0, *) { - self.previewBlurView.effect = UIBlurEffect(style: .systemThinMaterialDark) - } - } else { + if changingPosition { + UIView.transition(with: self.previewContainerView, duration: 0.4, options: [.transitionFlipFromLeft, .curveEaseOut], animations: { + self.previewBlurView.effect = UIBlurEffect(style: .dark) + }) + } else if forceBlur { + self.previewBlurView.effect = UIBlurEffect(style: .dark) + } else { + UIView.animate(withDuration: 0.4) { self.previewBlurView.effect = nil } } } }) - self.shootAction.connect { [weak self] _ in - self?.previewView.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.15) + self.completion.connect { [weak self] result in + if let self { + self.animateOutToEditor() + self.controller?.completion( + result + |> beforeNext { [weak self] value in + guard let self else { + return + } + if case .pendingImage = value { + Queue.mainQueue().async { + self.effectivePreviewView.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.25) + self.simplePreviewView?.isEnabled = false + } + } else { + Queue.mainQueue().async { + if case .image = value { + Queue.mainQueue().after(0.3) { + self.previewBlurPromise.set(true) + } + } + self.simplePreviewView?.isEnabled = false + self.camera.stopCapture() + } + } + } + ) + } } } @@ -609,10 +707,13 @@ public class CameraScreen: ViewController { self.view.disablesInteractiveKeyboardGestureRecognizer = true let pinchGestureRecognizer = UIPinchGestureRecognizer(target: self, action: #selector(self.handlePinch(_:))) - self.previewView.addGestureRecognizer(pinchGestureRecognizer) + self.effectivePreviewView.addGestureRecognizer(pinchGestureRecognizer) let panGestureRecognizer = UIPanGestureRecognizer(target: self, action: #selector(self.handlePan(_:))) - self.previewView.addGestureRecognizer(panGestureRecognizer) + self.effectivePreviewView.addGestureRecognizer(panGestureRecognizer) + + self.camera.focus(at: CGPoint(x: 0.5, y: 0.5)) + self.camera.startCapture() } @objc private func handlePinch(_ gestureRecognizer: UIPinchGestureRecognizer) { @@ -656,9 +757,6 @@ public class CameraScreen: ViewController { }, completion: { _ in self.backgroundEffectView.isHidden = true }) - - self.camera.focus(at: CGPoint(x: 0.5, y: 0.5)) - self.camera.startCapture() self.previewContainerView.layer.animatePosition(from: CGPoint(x: 0.0, y: layout.size.height / 2.0 - layout.intrinsicInsets.bottom - 22.0), to: .zero, duration: 0.4, timingFunction: kCAMediaTimingFunctionSpring, additive: true) self.componentHost.view?.layer.animatePosition(from: CGPoint(x: 0.0, y: layout.size.height / 2.0 - layout.intrinsicInsets.bottom - 22.0), to: .zero, duration: 0.4, timingFunction: kCAMediaTimingFunctionSpring, additive: true) @@ -689,23 +787,86 @@ public class CameraScreen: ViewController { self.componentHost.view?.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.15, removeOnCompletion: false) self.previewContainerView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.35, removeOnCompletion: false) } + + func animateOutToEditor() { + let transition = Transition(animation: .curve(duration: 0.2, curve: .easeInOut)) + if let view = self.componentHost.findTaggedView(tag: cancelButtonTag) { + transition.setScale(view: view, scale: 0.1) + transition.setAlpha(view: view, alpha: 0.0) + } + if let view = self.componentHost.findTaggedView(tag: flashButtonTag) { + transition.setScale(view: view, scale: 0.1) + transition.setAlpha(view: view, alpha: 0.0) + } + if let view = self.componentHost.findTaggedView(tag: zoomControlTag) { + transition.setAlpha(view: view, alpha: 0.0) + } + if let view = self.componentHost.findTaggedView(tag: captureControlsTag) as? CaptureControlsComponent.View { + view.animateOutToEditor(transition: transition) + } + if let view = self.componentHost.findTaggedView(tag: modeControlTag) as? ModeComponent.View { + view.animateOutToEditor(transition: transition) + } + } + + func animateInFromEditor() { + self.simplePreviewView?.isEnabled = true + self.camera.startCapture() + + if #available(iOS 13.0, *), let isPreviewing = self.simplePreviewView?.isPreviewing { + let _ = (isPreviewing + |> filter { + $0 + } + |> take(1)).start(next: { [weak self] _ in + if let self { + self.previewBlurPromise.set(false) + } + }) + } else { + Queue.mainQueue().after(1.0) { + self.previewBlurPromise.set(false) + } + } + + let transition = Transition(animation: .curve(duration: 0.2, curve: .easeInOut)) + if let view = self.componentHost.findTaggedView(tag: cancelButtonTag) { + transition.setScale(view: view, scale: 1.0) + transition.setAlpha(view: view, alpha: 1.0) + } + if let view = self.componentHost.findTaggedView(tag: flashButtonTag) { + transition.setScale(view: view, scale: 1.0) + transition.setAlpha(view: view, alpha: 1.0) + } + if let view = self.componentHost.findTaggedView(tag: zoomControlTag) { + transition.setScale(view: view, scale: 1.0) + transition.setAlpha(view: view, alpha: 1.0) + } + if let view = self.componentHost.findTaggedView(tag: captureControlsTag) as? CaptureControlsComponent.View { + view.animateInFromEditor(transition: transition) + } + if let view = self.componentHost.findTaggedView(tag: modeControlTag) as? ModeComponent.View { + view.animateInFromEditor(transition: transition) + } + } override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? { let result = super.hitTest(point, with: event) if result == self.componentHost.view { - return self.previewView + return self.effectivePreviewView } return result } func containerLayoutUpdated(layout: ContainerViewLayout, forceUpdate: Bool = false, animateOut: Bool = false, transition: Transition) { - guard let controller = self.controller else { + guard let _ = self.controller else { return } let isFirstTime = self.validLayout == nil self.validLayout = layout - - let topInset: CGFloat = 60.0 //layout.intrinsicInsets.top + layout.safeInsets.top + + let previewSize = CGSize(width: layout.size.width, height: floorToScreenPixels(layout.size.width * 1.77778)) + let topInset: CGFloat = floor(layout.size.height - previewSize.height) / 2.0 let environment = ViewControllerComponentContainer.Environment( statusBarHeight: layout.statusBarHeight ?? 0.0, @@ -713,7 +874,7 @@ public class CameraScreen: ViewController { safeInsets: UIEdgeInsets( top: topInset, left: layout.safeInsets.left, - bottom: layout.intrinsicInsets.bottom + layout.safeInsets.bottom, + bottom: topInset, right: layout.safeInsets.right ), inputHeight: layout.inputHeight ?? 0.0, @@ -748,8 +909,7 @@ public class CameraScreen: ViewController { push: { [weak self] c in self?.controller?.push(c) }, - completion: controller.completion, - shootAction: self.shootAction + completion: self.completion ) ), environment: { @@ -775,10 +935,9 @@ public class CameraScreen: ViewController { transition.setFrame(view: self.backgroundDimView, frame: CGRect(origin: .zero, size: layout.size)) transition.setFrame(view: self.backgroundEffectView, frame: CGRect(origin: .zero, size: layout.size)) - let previewSize = CGSize(width: layout.size.width, height: floorToScreenPixels(layout.size.width * 1.77778)) - let previewFrame = CGRect(origin: CGPoint(x: 0.0, y: 60.0), size: previewSize) + let previewFrame = CGRect(origin: CGPoint(x: 0.0, y: topInset), size: previewSize) transition.setFrame(view: self.previewContainerView, frame: previewFrame) - transition.setFrame(view: self.previewView, frame: CGRect(origin: .zero, size: previewFrame.size)) + transition.setFrame(view: self.effectivePreviewView, frame: CGRect(origin: .zero, size: previewFrame.size)) transition.setFrame(view: self.previewBlurView, frame: CGRect(origin: .zero, size: previewFrame.size)) } } @@ -790,9 +949,9 @@ public class CameraScreen: ViewController { private let context: AccountContext fileprivate let mode: Mode fileprivate let holder: CameraHolder? - fileprivate let completion: (CameraScreen.Result) -> Void + fileprivate let completion: (Signal) -> Void - public init(context: AccountContext, mode: Mode, holder: CameraHolder? = nil, completion: @escaping (CameraScreen.Result) -> Void) { + public init(context: AccountContext, mode: Mode, holder: CameraHolder? = nil, completion: @escaping (Signal) -> Void) { self.context = context self.mode = mode self.holder = holder @@ -815,6 +974,10 @@ public class CameraScreen: ViewController { super.displayNodeDidLoad() } + + public func returnFromEditor() { + self.node.animateInFromEditor() + } private var isDismissed = false fileprivate func requestDismiss(animated: Bool) { diff --git a/submodules/TelegramUI/Components/CameraScreen/Sources/CaptureControlsComponent.swift b/submodules/TelegramUI/Components/CameraScreen/Sources/CaptureControlsComponent.swift index 2da3986ebd..34fc1b3b31 100644 --- a/submodules/TelegramUI/Components/CameraScreen/Sources/CaptureControlsComponent.swift +++ b/submodules/TelegramUI/Components/CameraScreen/Sources/CaptureControlsComponent.swift @@ -392,6 +392,7 @@ final class CaptureControlsComponent: Component { let location = gestureRecognizer.location(in: self) switch gestureRecognizer.state { case .began: + self.hapticFeedback.impact(.click05) self.component?.shutterPressed() self.component?.swipeHintUpdated(.zoom) self.shutterUpdateOffset.invoke((0.0, .immediate)) @@ -405,6 +406,7 @@ final class CaptureControlsComponent: Component { } self.shutterUpdateOffset.invoke((blobOffset, .spring(duration: 0.5))) } else { + self.hapticFeedback.impact(.light) self.component?.shutterReleased() self.shutterUpdateOffset.invoke((0.0, .spring(duration: 0.3))) } @@ -416,7 +418,6 @@ final class CaptureControlsComponent: Component { private let hapticFeedback = HapticFeedback() private var didFlip = false - private var wasBanding: Bool? private var panBlobState: ShutterBlobView.BlobState? @objc private func handlePan(_ gestureRecognizer: UIPanGestureRecognizer) { @@ -498,12 +499,43 @@ final class CaptureControlsComponent: Component { return true } - func animateIn() { - + private var animatedOut = false + func animateOutToEditor(transition: Transition) { + self.animatedOut = true + + if let view = self.galleryButtonView.view { + transition.setScale(view: view, scale: 0.1) + transition.setAlpha(view: view, alpha: 0.0) + } + + if let view = self.flipButtonView.view { + transition.setScale(view: view, scale: 0.1) + transition.setAlpha(view: view, alpha: 0.0) + } + + if let view = self.shutterButtonView.view { + transition.setScale(view: view, scale: 0.1) + transition.setAlpha(view: view, alpha: 0.0) + } } - func animateOut() { + func animateInFromEditor(transition: Transition) { + self.animatedOut = false + if let view = self.galleryButtonView.view { + transition.setScale(view: view, scale: 1.0) + transition.setAlpha(view: view, alpha: 1.0) + } + + if let view = self.flipButtonView.view { + transition.setScale(view: view, scale: 1.0) + transition.setAlpha(view: view, alpha: 1.0) + } + + if let view = self.shutterButtonView.view { + transition.setScale(view: view, scale: 1.0) + transition.setAlpha(view: view, alpha: 1.0) + } } func update(component: CaptureControlsComponent, state: State, availableSize: CGSize, transition: Transition) -> CGSize { @@ -529,11 +561,12 @@ final class CaptureControlsComponent: Component { let galleryButtonSize = self.galleryButtonView.update( transition: .immediate, component: AnyComponent( - Button( + CameraButton( content: AnyComponent( Image( image: state.cachedAssetImage?.1, - size: CGSize(width: 50.0, height: 50.0) + size: CGSize(width: 50.0, height: 50.0), + contentMode: .scaleAspectFill ) ), action: { @@ -585,15 +618,16 @@ final class CaptureControlsComponent: Component { let flipButtonSize = self.flipButtonView.update( transition: .immediate, component: AnyComponent( - Button( + CameraButton( content: AnyComponent( FlipButtonContentComponent(action: flipAnimationAction) ), + minSize: CGSize(width: 44.0, height: 44.0), action: { component.flipTapped() flipAnimationAction.invoke(Void()) } - ).minSize(CGSize(width: 44.0, height: 44.0)) + ) ), environment: {}, containerSize: availableSize @@ -679,15 +713,6 @@ final class CaptureControlsComponent: Component { self.leftGuide.cornerRadius = guideSize.height / 2.0 self.rightGuide.cornerRadius = guideSize.height / 2.0 - if let screenTransition = transition.userData(CameraScreenTransition.self) { - switch screenTransition { - case .animateIn: - self.animateIn() - case .animateOut: - self.animateOut() - } - } - return size } } diff --git a/submodules/TelegramUI/Components/CameraScreen/Sources/ModeComponent.swift b/submodules/TelegramUI/Components/CameraScreen/Sources/ModeComponent.swift index d6aa83ecf9..106cb5f70a 100644 --- a/submodules/TelegramUI/Components/CameraScreen/Sources/ModeComponent.swift +++ b/submodules/TelegramUI/Components/CameraScreen/Sources/ModeComponent.swift @@ -14,19 +14,24 @@ extension CameraMode { } } +private let buttonSize = CGSize(width: 55.0, height: 44.0) + final class ModeComponent: Component { let availableModes: [CameraMode] let currentMode: CameraMode let updatedMode: (CameraMode) -> Void + let tag: AnyObject? init( availableModes: [CameraMode], currentMode: CameraMode, - updatedMode: @escaping (CameraMode) -> Void + updatedMode: @escaping (CameraMode) -> Void, + tag: AnyObject? ) { self.availableModes = availableModes self.currentMode = currentMode self.updatedMode = updatedMode + self.tag = tag } static func ==(lhs: ModeComponent, rhs: ModeComponent) -> Bool { @@ -39,7 +44,7 @@ final class ModeComponent: Component { return true } - final class View: UIView { + final class View: UIView, ComponentTaggedView { private var component: ModeComponent? final class ItemView: HighlightTrackingButton { @@ -69,6 +74,16 @@ final class ModeComponent: Component { private var containerView = UIView() private var itemViews: [ItemView] = [] + public func matches(tag: Any) -> Bool { + if let component = self.component, let componentTag = component.tag { + let tag = tag as AnyObject + if componentTag === tag { + return true + } + } + return false + } + init() { super.init(frame: CGRect()) @@ -80,6 +95,21 @@ final class ModeComponent: Component { required init?(coder aDecoder: NSCoder) { preconditionFailure() } + + private var animatedOut = false + func animateOutToEditor(transition: Transition) { + self.animatedOut = true + + transition.setAlpha(view: self.containerView, alpha: 0.0) + transition.setSublayerTransform(view: self.containerView, transform: CATransform3DMakeTranslation(0.0, -buttonSize.height, 0.0)) + } + + func animateInFromEditor(transition: Transition) { + self.animatedOut = false + + transition.setAlpha(view: self.containerView, alpha: 1.0) + transition.setSublayerTransform(view: self.containerView, transform: CATransform3DIdentity) + } func update(component: ModeComponent, availableSize: CGSize, transition: Transition) -> CGSize { self.component = component @@ -87,7 +117,6 @@ final class ModeComponent: Component { let updatedMode = component.updatedMode let spacing: CGFloat = 14.0 - let buttonSize = CGSize(width: 55.0, height: 44.0) var i = 0 var itemFrame = CGRect(origin: .zero, size: buttonSize) diff --git a/submodules/TelegramUI/Components/MediaEditor/BUILD b/submodules/TelegramUI/Components/MediaEditor/BUILD new file mode 100644 index 0000000000..408877794f --- /dev/null +++ b/submodules/TelegramUI/Components/MediaEditor/BUILD @@ -0,0 +1,68 @@ +load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library") +load( + "@build_bazel_rules_apple//apple:resources.bzl", + "apple_resource_bundle", + "apple_resource_group", +) +load("//build-system/bazel-utils:plist_fragment.bzl", + "plist_fragment", +) + +filegroup( + name = "MediaEditorMetalResources", + srcs = glob([ + "MetalResources/**/*.*", + ]), + visibility = ["//visibility:public"], +) + +plist_fragment( + name = "MediaEditorBundleInfoPlist", + extension = "plist", + template = + """ + CFBundleIdentifier + org.telegram.MediaEditor + CFBundleDevelopmentRegion + en + CFBundleName + MediaEditor + """ +) + +apple_resource_bundle( + name = "MediaEditorBundle", + infoplists = [ + ":MediaEditorBundleInfoPlist", + ], + resources = [ + ":MediaEditorMetalResources", + ], +) + +swift_library( + name = "MediaEditor", + module_name = "MediaEditor", + srcs = glob([ + "Sources/**/*.swift", + ]), + copts = [ + "-warnings-as-errors", + ], + data = [ + ":MediaEditorBundle", + ], + deps = [ + "//submodules/AsyncDisplayKit:AsyncDisplayKit", + "//submodules/Display:Display", + "//submodules/Postbox:Postbox", + "//submodules/TelegramCore:TelegramCore", + "//submodules/SSignalKit/SwiftSignalKit:SwiftSignalKit", + "//submodules/TelegramPresentationData:TelegramPresentationData", + "//submodules/AccountContext:AccountContext", + "//submodules/AppBundle:AppBundle", + ], + visibility = [ + "//visibility:public", + ], +) diff --git a/submodules/TelegramUI/Components/MediaEditor/MetalResources/EditorAdjustments.metal b/submodules/TelegramUI/Components/MediaEditor/MetalResources/EditorAdjustments.metal new file mode 100644 index 0000000000..ebb9bc18fa --- /dev/null +++ b/submodules/TelegramUI/Components/MediaEditor/MetalResources/EditorAdjustments.metal @@ -0,0 +1,185 @@ +#include +#include "EditorCommon.h" +#include "EditorUtils.h" + +using namespace metal; + +typedef struct { + float2 dimensions; + float aspectRatio; + float shadows; + float highlights; + float contrast; + float fade; + float saturation; + float shadowsTintIntensity; + float3 shadowsTintColor; + float highlightsTintIntensity; + float3 highlightsTintColor; + float exposure; + float warmth; + float grain; + float vignette; +} MediaEditorAdjustments; + +half3 fade(half3 color, float fadeAmount) { + half3 comp1 = half3(-0.9772) * half3(pow(float3(color), float3(3.0))); + half3 comp2 = half3(1.708) * half3(pow(float3(color), float3(2.0))); + half3 comp3 = half3(-0.1603) * color; + half3 comp4 = half3(0.2878); + half3 finalComponent = comp1 + comp2 + comp3 + comp4; + half3 difference = finalComponent - color; + half3 scalingValue = half3(0.9); + half3 faded = color + (difference * scalingValue); + return (color * (1.0 - fadeAmount)) + (faded * fadeAmount); +} + +float3 tintRaiseShadowsCurve(half3 color) { + float3 comp1 = float3(-0.003671) * pow(float3(color), float3(3.0)); + float3 comp2 = float3(0.3842) * pow(float3(color), float3(2.0)); + float3 comp3 = float3(0.3764) * float3(color); + float3 comp4 = float3(0.2515); + return comp1 + comp2 + comp3 + comp4; +} + +half3 tintShadows(half3 color, float3 tintColor, float tintAmount) { + float3 raisedShadows = tintRaiseShadowsCurve(color); + float3 tintedShadows = mix(float3(color), raisedShadows, tintColor); + float3 tintedShadowsWithAmount = mix(float3(color), tintedShadows, tintAmount); + return half3(clamp(tintedShadowsWithAmount, 0.0, 1.0)); +} + +half3 tintHighlights(half3 color, float3 tintColor, float tintAmount) { + float3 loweredHighlights = float3(1.0) - tintRaiseShadowsCurve(half3(1.0) - color); + float3 tintedHighlights = mix(float3(color), loweredHighlights, (float3(1.0) - tintColor)); + float3 tintedHighlightsWithAmount = mix(float3(color), tintedHighlights, tintAmount); + return half3(clamp(tintedHighlightsWithAmount, 0.0, 1.0)); +} + +half3 applyLuminanceCurve(half3 pixel, constant float allCurve[200]) { + int index = int(clamp(pixel.z / (1.0 / 200.0), 0.0, 199.0)); + float value = allCurve[index]; + + float grayscale = (smoothstep(0.0, 0.1, float(pixel.z)) * (1.0 - smoothstep(0.8, 1.0, float(pixel.z)))); + half saturation = mix(0.0, float(pixel.y), grayscale); + pixel.y = saturation; + pixel.z = value; + return pixel; +} + +half3 applyRGBCurve(half3 pixel, constant float redCurve[200], constant float greenCurve[200], constant float blueCurve[200]) { + int index = int(clamp(pixel.r / (1.0 / 200.0), 0.0, 199.0)); + float value = redCurve[index]; + pixel.r = value; + + index = int(clamp(pixel.g / (1.0 / 200.0), 0.0, 199.0)); + value = greenCurve[index]; + pixel.g = clamp(value, 0.0, 1.0); + + index = int(clamp(pixel.b / (1.0 / 200.0), 0.0, 199.0)); + value = blueCurve[index]; + pixel.b = clamp(value, 0.0, 1.0); + + return pixel; +} + +fragment half4 adjustmentsFragmentShader(RasterizerData in [[stage_in]], + texture2d sourceImage [[texture(0)]], + constant MediaEditorAdjustments& adjustments [[buffer(0)]], + constant float allCurve [[buffer(1)]][200], + constant float redCurve [[buffer(2)]][200], + constant float greenCurve [[buffer(3)]][200], + constant float blueCurve [[buffer(4)]][200] + ) { + constexpr sampler samplr(filter::linear, mag_filter::linear, min_filter::linear); + const float epsilon = 0.005; + + half4 source = sourceImage.sample(samplr, float2(in.texCoord.x, in.texCoord.y)); + half4 result = source; + + //result = half4(applyRGBCurve(hslToRgb(applyLuminanceCurve(rgbToHsl(result.rgb), allCurve)), redCurve, greenCurve, blueCurve), result.a); + + if (abs(adjustments.highlights) > epsilon || abs(adjustments.shadows) > epsilon) { + const float3 hsLuminanceWeighting = float3(0.3, 0.3, 0.3); + float mappedHighlights = adjustments.highlights * 0.75 + 1.0; + float mappedShadows = adjustments.shadows * 0.55 + 1.0; + + float hsLuminance = dot(float3(result.rgb), hsLuminanceWeighting); + float shadow = clamp((pow(hsLuminance, 1.0 / mappedShadows) - 0.76 * pow(hsLuminance, 2.0 / mappedShadows)) - hsLuminance, 0.0, 1.0); + float highlight = clamp((1.0 - (pow(1.0 - hsLuminance, 1.0 / (2.0 - mappedHighlights)) - 0.8 * pow(1.0 - hsLuminance, 2.0 / (2.0 - mappedHighlights)))) - hsLuminance, -1.0, 0.0); + float3 hsResult = float3(0.0, 0.0, 0.0) + ((hsLuminance + shadow + highlight) - 0.0) * ((float3(result.rgb) - float3(0.0, 0.0, 0.0)) / (hsLuminance - 0.0)); + + float contrastedLuminance = ((hsLuminance - 0.5) * 1.5) + 0.5; + float whiteInterp = contrastedLuminance * contrastedLuminance * contrastedLuminance; + half whiteTarget = clamp(mappedHighlights, 1.0, 2.0) - 1.0; + hsResult = mix(hsResult, float3(1.0), whiteInterp * whiteTarget); + float invContrastedLuminance = 1.0 - contrastedLuminance; + float blackInterp = invContrastedLuminance * invContrastedLuminance * invContrastedLuminance; + half blackTarget = 1.0 - clamp(mappedShadows, 0.0, 1.0); + + result.rgb = half3(mix(hsResult, float3(0.0), blackInterp * blackTarget)); + } + + if (abs(adjustments.contrast) > epsilon) { + half mappedContrast = half(adjustments.contrast) * 0.3 + 1.0; + result.rgb = clamp(((result.rgb - half3(0.5)) * mappedContrast + half3(0.5)), 0.0, 1.0); + } + + if (abs(adjustments.fade) > epsilon) { + result.rgb = fade(result.rgb, adjustments.fade); + } + + if (abs(adjustments.saturation) > epsilon) { + float mappedSaturation = adjustments.saturation; + if (mappedSaturation > 0.0) { + mappedSaturation *= 1.05; + } + mappedSaturation += 1.0; + half satLuminance = dot(result.rgb, half3(0.2126, 0.7152, 0.0722)); + half3 greyScaleColor = half3(satLuminance); + result.rgb = clamp(mix(greyScaleColor, result.rgb, mappedSaturation), 0.0, 1.0); + } + + if (abs(adjustments.shadowsTintIntensity) > epsilon) { + result.rgb = tintShadows(result.rgb, adjustments.shadowsTintColor, adjustments.shadowsTintIntensity * 2.0); + } + + if (abs(adjustments.highlightsTintIntensity) > epsilon) { + result.rgb = tintHighlights(result.rgb, adjustments.highlightsTintColor, adjustments.highlightsTintIntensity * 2.0); + } + + if (abs(adjustments.exposure) > epsilon) { + float mag = adjustments.exposure * 1.045; + float power = 1.0 + abs(mag); + if (mag < 0.0) { + power = 1.0 / power; + } + result.r = 1.0 - pow((1.0 - result.r), power); + result.g = 1.0 - pow((1.0 - result.g), power); + result.b = 1.0 - pow((1.0 - result.b), power); + } + + if (abs(adjustments.warmth) > epsilon) { + half3 yuvVector; + if (adjustments.warmth > 0.0) { + yuvVector = half3(0.1765, -0.1255, 0.0902); + } else { + yuvVector = -half3(0.0588, 0.1569, -0.1255); + } + half3 yuvColor = rgbToYuv(result.rgb); + half luma = yuvColor.r; + half curveScale = sin(luma * 3.14159); + yuvColor += 0.375 * adjustments.warmth * curveScale * yuvVector; + result.rgb = yuvToRgb(yuvColor); + } + + if (abs(adjustments.vignette) > epsilon) { + const float midpoint = 0.7; + const float fuzziness = 0.62; + float radDist = length(in.texCoord - 0.5) / sqrt(0.5); + float mag = easeInOutSigmoid(radDist * midpoint, fuzziness) * adjustments.vignette * 0.645; + result.rgb = half3(mix(pow(float3(result.rgb), float3(1.0 / (1.0 - mag))), float3(0.0), mag * mag)); + } + + return result; +} diff --git a/submodules/TelegramUI/Components/MediaEditor/MetalResources/EditorBlur.metal b/submodules/TelegramUI/Components/MediaEditor/MetalResources/EditorBlur.metal new file mode 100644 index 0000000000..f1079efb05 --- /dev/null +++ b/submodules/TelegramUI/Components/MediaEditor/MetalResources/EditorBlur.metal @@ -0,0 +1,72 @@ +#include +#include "EditorCommon.h" +#include "EditorUtils.h" + +using namespace metal; + +typedef struct { + float2 dimensions; + float2 position; + float aspectRatio; + float size; + float falloff; + float rotation; +} MediaEditorBlur; + +fragment half4 blurRadialFragmentShader(RasterizerData in [[stage_in]], + texture2d sourceTexture [[texture(0)]], + texture2d blurTexture [[texture(1)]], + constant MediaEditorBlur& values [[ buffer(0) ]] + ) +{ + constexpr sampler sourceSampler(min_filter::linear, mag_filter::linear, address::clamp_to_zero); + constexpr sampler blurSampler(min_filter::linear, mag_filter::linear, address::clamp_to_zero); + + half4 sourceColor = sourceTexture.sample(sourceSampler, in.texCoord); + half4 blurredColor = blurTexture.sample(blurSampler, in.texCoord); + + float2 texCoord = float2(in.texCoord.x, (in.texCoord.y * values.aspectRatio)); + half distanceFromCenter = distance(values.position, texCoord); + + half3 result = mix(blurredColor.rgb, sourceColor.rgb, smoothstep(1.0, values.falloff, clamp(distanceFromCenter / values.size, 0.0, 1.0))); + return half4(result, sourceColor.a); +} + + +fragment half4 blurLinearFragmentShader(RasterizerData in [[stage_in]], + texture2d sourceTexture [[texture(0)]], + texture2d blurTexture [[texture(1)]], + constant MediaEditorBlur& values [[ buffer(0) ]] + ) +{ + constexpr sampler sourceSampler(min_filter::linear, mag_filter::linear, address::clamp_to_zero); + constexpr sampler blurSampler(min_filter::linear, mag_filter::linear, address::clamp_to_zero); + + half4 sourceColor = sourceTexture.sample(sourceSampler, in.texCoord); + half4 blurredColor = blurTexture.sample(blurSampler, in.texCoord); + + float2 texCoord = float2(in.texCoord.x, (in.texCoord.y * values.aspectRatio)); + half distanceFromCenter = abs((texCoord.x - values.position.x) * sin(-values.rotation) + (texCoord.y - values.position.y) * cos(-values.rotation)); + + half3 result = mix(blurredColor.rgb, sourceColor.rgb, smoothstep(1.0, values.falloff, clamp(distanceFromCenter / values.size, 0.0, 1.0))); + return half4(result, sourceColor.a); +} + +fragment half4 blurPortraitFragmentShader(RasterizerData in [[stage_in]], + texture2d sourceTexture [[texture(0)]], + texture2d blurTexture [[texture(1)]], + texture2d maskTexture [[texture(2)]], + constant MediaEditorBlur& values [[ buffer(0) ]] + ) +{ + constexpr sampler sourceSampler(min_filter::linear, mag_filter::linear, address::clamp_to_zero); + constexpr sampler blurSampler(min_filter::linear, mag_filter::linear, address::clamp_to_zero); + constexpr sampler maskSampler(min_filter::linear, mag_filter::linear, address::clamp_to_zero); + + half4 sourceColor = sourceTexture.sample(sourceSampler, in.texCoord); + half4 blurredColor = blurTexture.sample(blurSampler, in.texCoord); + half4 maskColor = maskTexture.sample(maskSampler, in.texCoord); + + half3 result = mix(blurredColor.rgb, sourceColor.rgb, maskColor.r); + return half4(result, sourceColor.a); +} diff --git a/submodules/TelegramUI/Components/MediaEditor/MetalResources/EditorCommon.h b/submodules/TelegramUI/Components/MediaEditor/MetalResources/EditorCommon.h new file mode 100644 index 0000000000..687d1fecdd --- /dev/null +++ b/submodules/TelegramUI/Components/MediaEditor/MetalResources/EditorCommon.h @@ -0,0 +1,8 @@ +#include + +#pragma once + +typedef struct { + float4 pos [[position]]; + float2 texCoord; +} RasterizerData; diff --git a/submodules/TelegramUI/Components/MediaEditor/MetalResources/EditorDefault.metal b/submodules/TelegramUI/Components/MediaEditor/MetalResources/EditorDefault.metal new file mode 100644 index 0000000000..278e09edda --- /dev/null +++ b/submodules/TelegramUI/Components/MediaEditor/MetalResources/EditorDefault.metal @@ -0,0 +1,37 @@ +#include +#include "EditorCommon.h" + +using namespace metal; + +typedef struct { + float4 pos; + float2 texCoord; +} VertexData; + +vertex RasterizerData defaultVertexShader(uint vertexID [[vertex_id]], + constant VertexData *vertices [[buffer(0)]]) { + RasterizerData out; + + out.pos = vector_float4(0.0, 0.0, 0.0, 1.0); + out.pos.xy = vertices[vertexID].pos.xy; + + out.texCoord = vertices[vertexID].texCoord; + + return out; +} + +fragment half4 defaultFragmentShader(RasterizerData in [[stage_in]], + constant float2 &texCoordScales [[buffer(0)]], + texture2d texture [[texture(0)]]) { + constexpr sampler samplr(filter::linear, mag_filter::linear, min_filter::linear); + + float scaleX = texCoordScales.x; + float scaleY = texCoordScales.y; + float x = (in.texCoord.x - (1.0 - scaleX) / 2.0) / scaleX; + float y = (in.texCoord.y - (1.0 - scaleY) / 2.0) / scaleY; + if (x < 0 || x > 1 || y < 0 || y > 1) { + return half4(0.0, 0.0, 0.0, 1.0); + } + half3 color = texture.sample(samplr, float2(x, y)).rgb; + return half4(color, 1.0); +} diff --git a/submodules/TelegramUI/Components/MediaEditor/MetalResources/EditorEnhance.metal b/submodules/TelegramUI/Components/MediaEditor/MetalResources/EditorEnhance.metal new file mode 100644 index 0000000000..37f45ec976 --- /dev/null +++ b/submodules/TelegramUI/Components/MediaEditor/MetalResources/EditorEnhance.metal @@ -0,0 +1,143 @@ +#include +#include "EditorCommon.h" +#include "EditorUtils.h" + +using namespace metal; + +typedef struct { + uint histogramBins; + uint clipLimit; + uint totalPixelCountPerTile; + uint numberOfLUTs; +} MediaEditorEnhanceLUTGeneratorParameters; + +METAL_FUNC half3 rgb2hsl(half3 inputColor) { + half3 color = saturate(inputColor); + + //Compute min and max component values + half MAX = max(color.r, max(color.g, color.b)); + half MIN = min(color.r, min(color.g, color.b)); + + //Make sure MAX > MIN to avoid division by zero later + MAX = max(MIN + 1e-6h, MAX); + + //Compute luminosity + half l = (MIN + MAX) / 2.0h; + + //Compute saturation + half s = (l < 0.5h ? (MAX - MIN) / (MIN + MAX) : (MAX - MIN) / (2.0h - MAX - MIN)); + + //Compute hue + half h = (MAX == color.r ? (color.g - color.b) / (MAX - MIN) : (MAX == color.g ? 2.0h + (color.b - color.r) / (MAX - MIN) : 4.0h + (color.r - color.g) / (MAX - MIN))); + h /= 6.0h; + h = (h < 0.0h ? 1.0h + h : h); + + return half3(h, s, l); +} + +fragment half rgbToLightnessFragmentShader(RasterizerData in [[ stage_in ]], + texture2d sourceTexture [[ texture(0) ]], + sampler colorSampler [[ sampler(0) ]], + constant float2 & scale [[buffer(0)]]) +{ + half4 color = sourceTexture.sample(colorSampler, in.texCoord * scale); + half3 hsl = rgbToHsl(color.rgb); + return hsl.b; +} + +kernel void CLAHEGenerateLUT(texture2d outTexture [[texture(0)]], + device uint * histogramBuffer [[buffer(0)]], + constant MediaEditorEnhanceLUTGeneratorParameters & parameters [[buffer(1)]], + uint gid [[thread_position_in_grid]]) +{ + if (gid >= parameters.numberOfLUTs) { + return; + } + + device uint *l = histogramBuffer + gid * parameters.histogramBins; + const uint histSize = parameters.histogramBins; + + uint clipped = 0; + for (uint i = 0; i < histSize; ++i) { + if(l[i] > parameters.clipLimit) { + clipped += (l[i] - parameters.clipLimit); + l[i] = parameters.clipLimit; + } + } + + const uint redistBatch = clipped / histSize; + uint residual = clipped - redistBatch * histSize; + + for (uint i = 0; i < histSize; ++i) { + l[i] += redistBatch; + } + + if (residual != 0) { + const uint residualStep = max(histSize / residual, (uint)1); + for (uint i = 0; i < histSize && residual > 0; i += residualStep, residual--) { + l[i]++; + } + } + + uint sum = 0; + const float lutScale = (histSize - 1) / float(parameters.totalPixelCountPerTile); + for (uint index = 0; index < histSize; ++index) { + sum += l[index]; + outTexture.write(round(sum * lutScale)/255.0, uint2(index, gid)); + } +} + +half CLAHELookup(texture2d lutTexture, sampler lutSamper, float index, float x) { + return lutTexture.sample(lutSamper, float2(x, (index + 0.5)/lutTexture.get_height())).r; +} + +fragment half4 enhanceColorLookupFragmentShader(RasterizerData in [[stage_in]], + texture2d sourceTexture [[texture(0)]], + texture2d lutTexture [[texture(1)]], + constant float2 & tileGridSize [[ buffer(0) ]], + constant float & intensity [[ buffer(1) ]] + ) +{ + constexpr sampler colorSampler(min_filter::linear, mag_filter::linear, address::clamp_to_zero); + constexpr sampler lutSampler(min_filter::linear, mag_filter::linear, address::clamp_to_zero); + + float2 sourceCoord = in.texCoord; + half4 color = sourceTexture.sample(colorSampler,sourceCoord); + half3 hslColor = rgbToHsl(color.rgb); + + float txf = sourceCoord.x * tileGridSize.x - 0.5; + + float tx1 = floor(txf); + float tx2 = tx1 + 1.0; + + float xa_p = txf - tx1; + float xa1_p = 1.0 - xa_p; + + tx1 = max(tx1, 0.0); + tx2 = min(tx2, tileGridSize.x - 1.0); + + float tyf = sourceCoord.y * tileGridSize.y - 0.5; + + float ty1 = floor(tyf); + float ty2 = ty1 + 1.0; + + float ya = tyf - ty1; + float ya1 = 1.0 - ya; + + ty1 = max(ty1, 0.0); + ty2 = min(ty2, tileGridSize.y - 1.0); + + float srcVal = hslColor.b; + float x = (srcVal * 255.0 + 0.5) / lutTexture.get_width(); + + half lutPlane1_ind1 = CLAHELookup(lutTexture, lutSampler, ty1 * tileGridSize.x + tx1, x); + half lutPlane1_ind2 = CLAHELookup(lutTexture, lutSampler, ty1 * tileGridSize.x + tx2, x); + half lutPlane2_ind1 = CLAHELookup(lutTexture, lutSampler, ty2 * tileGridSize.x + tx1, x); + half lutPlane2_ind2 = CLAHELookup(lutTexture, lutSampler, ty2 * tileGridSize.x + tx2, x); + + half res = (lutPlane1_ind1 * xa1_p + lutPlane1_ind2 * xa_p) * ya1 + (lutPlane2_ind1 * xa1_p + lutPlane2_ind2 * xa_p) * ya; + + half3 r = half3(hslColor.r, min(1.0, hslColor.g * 1.25), min(1.0, res * 1.1)); + half3 rgbResult = hslToRgb(r); + return half4(mix(color.rgb, rgbResult, half(intensity)), color.a); +} diff --git a/submodules/TelegramUI/Components/MediaEditor/MetalResources/EditorUtils.h b/submodules/TelegramUI/Components/MediaEditor/MetalResources/EditorUtils.h new file mode 100644 index 0000000000..811a23d326 --- /dev/null +++ b/submodules/TelegramUI/Components/MediaEditor/MetalResources/EditorUtils.h @@ -0,0 +1,23 @@ +#include + +#pragma once + +half getLuma(half3 color); + +half3 rgbToHsv(half3 c); + +half3 hsvToRgb(half3 c); + +half3 rgbToHsl(half3 color); + +half hueToRgb(half f1, half f2, half hue); + +half3 hslToRgb(half3 hsl); + +half3 rgbToYuv(half3 inP); + +half3 yuvToRgb(half3 inP); + +half easeInOutSigmoid(half value, half strength); + +half powerCurve(half inVal, half mag); diff --git a/submodules/TelegramUI/Components/MediaEditor/MetalResources/EditorUtils.metal b/submodules/TelegramUI/Components/MediaEditor/MetalResources/EditorUtils.metal new file mode 100644 index 0000000000..2a0763eee6 --- /dev/null +++ b/submodules/TelegramUI/Components/MediaEditor/MetalResources/EditorUtils.metal @@ -0,0 +1,134 @@ +#include +#include "EditorUtils.h" + +using namespace metal; + +half getLuma(half3 color) { + return (0.299 * color.r) + (0.587 * color.g) + (0.114 * color.b); +} + +half3 rgbToHsv(half3 c) { + half4 K = half4(0.0, -1.0 / 3.0, 2.0 / 3.0, -1.0); + half4 p = c.g < c.b ? half4(c.bg, K.wz) : half4(c.gb, K.xy); + half4 q = c.r < p.x ? half4(p.xyw, c.r) : half4(c.r, p.yzx); + half d = q.x - min(q.w, q.y); + half e = 1.0e-10; + return half3(abs(q.z + (q.w - q.y) / (6.0 * d + e)), d / (q.x + e), q.x); +} + +half3 hsvToRgb(half3 c) { + half4 K = half4(1.0, 2.0 / 3.0, 1.0 / 3.0, 3.0); + half3 p = abs(fract(c.xxx + K.xyz) * 6.0 - K.www); + return c.z * mix(K.xxx, clamp(p - K.xxx, 0.0, 1.0), c.y); +} + +half3 rgbToHsl(half3 color) { + half3 hsl; + half fMin = min(min(color.r, color.g), color.b); + half fMax = max(max(color.r, color.g), color.b); + half delta = fMax - fMin; + hsl.z = (fMax + fMin) / 2.0; + if (delta == 0.0) { + hsl.x = 0.0; + hsl.y = 0.0; + } else { + if (hsl.z < 0.5) { + hsl.y = delta / (fMax + fMin); + } else { + hsl.y = delta / (2.0 - fMax - fMin); + } + half deltaR = (((fMax - color.r) / 6.0) + (delta / 2.0)) / delta; + half deltaG = (((fMax - color.g) / 6.0) + (delta / 2.0)) / delta; + half deltaB = (((fMax - color.b) / 6.0) + (delta / 2.0)) / delta; + if (color.r == fMax) { + hsl.x = deltaB - deltaG; + } else if (color.g == fMax) { + hsl.x = (1.0 / 3.0) + deltaR - deltaB; + } else if (color.b == fMax) { + hsl.x = (2.0 / 3.0) + deltaG - deltaR; + } + if (hsl.x < 0.0) { + hsl.x += 1.0; + } else if (hsl.x > 1.0) { + hsl.x -= 1.0; + } + } + return hsl; +} + +half hueToRgb(half f1, half f2, half hue) { + if (hue < 0.0) { + hue += 1.0; + } else if (hue > 1.0) { + hue -= 1.0; + } + half res; + if ((6.0 * hue) < 1.0) { + res = f1 + (f2 - f1) * 6.0 * hue; + } else if ((2.0 * hue) < 1.0) { + res = f2; + } else if ((3.0 * hue) < 2.0) { + res = f1 + (f2 - f1) * ((2.0 / 3.0) - hue) * 6.0; + } else { + res = f1; + } + return res; +} + +half3 hslToRgb(half3 hsl) { + half3 rgb; + if (hsl.y == 0.0) { + rgb = half3(hsl.z); + } else { + half f2; + if (hsl.z < 0.5) { + f2 = hsl.z * (1.0 + hsl.y); + } else { + f2 = (hsl.z + hsl.y) - (hsl.y * hsl.z); + } + half f1 = 2.0 * hsl.z - f2; + rgb.r = hueToRgb(f1, f2, hsl.x + (1.0 / 3.0)); + rgb.g = hueToRgb(f1, f2, hsl.x); + rgb.b = hueToRgb(f1, f2, hsl.x - (1.0 / 3.0)); + } + return rgb; +} + +half3 rgbToYuv(half3 inP) { + half3 outP; + outP.r = getLuma(inP); + outP.g = (1.0 / 1.772) * (inP.b - outP.r); + outP.b = (1.0 / 1.402) * (inP.r - outP.r); + return outP; +} + +half3 yuvToRgb(half3 inP) { + float y = inP.r; + float u = inP.g; + float v = inP.b; + half3 outP; + outP.r = 1.402 * v + y; + outP.g = (y - (0.299 * 1.402 / 0.587) * v - (0.114 * 1.772 / 0.587) * u); + outP.b = 1.772 * u + y; + return outP; +} + +half easeInOutSigmoid(half value, half strength) { + float t = 1.0 / (1.0 - strength); + if (value > 0.5) { + return 1.0 - pow(2.0 - 2.0 * value, t) * 0.5; + } else { + return pow(2.0 * value, t) * 0.5; + } +} + +half powerCurve(half inVal, half mag) { + half outVal; + float power = 1.0 + abs(mag); + if (mag > 0.0) { + power = 1.0 / power; + } + inVal = 1.0 - inVal; + outVal = pow((1.0 - inVal), power); + return outVal; +} diff --git a/submodules/TelegramUI/Components/MediaEditor/Sources/AdjustmentsRenderPass.swift b/submodules/TelegramUI/Components/MediaEditor/Sources/AdjustmentsRenderPass.swift new file mode 100644 index 0000000000..d10be35fe2 --- /dev/null +++ b/submodules/TelegramUI/Components/MediaEditor/Sources/AdjustmentsRenderPass.swift @@ -0,0 +1,110 @@ +import Foundation +import Metal +import simd + +struct MediaEditorAdjustments { + var dimensions: simd_float2 + var aspectRatio: simd_float1 + var shadows: simd_float1 + var highlights: simd_float1 + var contrast: simd_float1 + var fade: simd_float1 + var saturation: simd_float1 + var shadowsTintIntensity: simd_float1 + var shadowsTintColor: simd_float3 + var highlightsTintIntensity: simd_float1 + var highlightsTintColor: simd_float3 + var exposure: simd_float1 + var warmth: simd_float1 + var grain: simd_float1 + var vignette: simd_float1 +} + +final class AdjustmentsRenderPass: DefaultRenderPass { + fileprivate var cachedTexture: MTLTexture? + + var adjustments = MediaEditorAdjustments( + dimensions: simd_float2(1.0, 1.0), + aspectRatio: 0.0, + shadows: 0.0, + highlights: 0.0, + contrast: 0.0, + fade: 0.0, + saturation: 0.0, + shadowsTintIntensity: 0.0, + shadowsTintColor: simd_float3(0.0, 0.0, 0.0), + highlightsTintIntensity: 0.0, + highlightsTintColor: simd_float3(0.0, 0.0, 0.0), + exposure: 0.0, + warmth: 0.0, + grain: 0.0, + vignette: 0.0 + ) + + var allCurve: [Float] = Array(repeating: 0, count: 200) + var redCurve: [Float] = Array(repeating: 0, count: 200) + var greenCurve: [Float] = Array(repeating: 0, count: 200) + var blueCurve: [Float] = Array(repeating: 0, count: 200) + + override var fragmentShaderFunctionName: String { + return "adjustmentsFragmentShader" + } + + override func process(input: MTLTexture, rotation: TextureRotation, device: MTLDevice, commandBuffer: MTLCommandBuffer) -> MTLTexture? { + self.setupVerticesBuffer(device: device, rotation: rotation) + + let (width, height) = textureDimensionsForRotation(texture: input, rotation: rotation) + + if self.cachedTexture == nil || self.cachedTexture?.width != width || self.cachedTexture?.height != height { + self.adjustments.dimensions = simd_float2(Float(width), Float(height)) + self.adjustments.aspectRatio = Float(width) / Float(height) + + let textureDescriptor = MTLTextureDescriptor() + textureDescriptor.textureType = .type2D + textureDescriptor.width = width + textureDescriptor.height = height + textureDescriptor.pixelFormat = input.pixelFormat + textureDescriptor.storageMode = .private + textureDescriptor.usage = [.shaderRead, .shaderWrite, .renderTarget] + guard let texture = device.makeTexture(descriptor: textureDescriptor) else { + return input + } + self.cachedTexture = texture + texture.label = "adjustmentsTexture" + } + + let renderPassDescriptor = MTLRenderPassDescriptor() + renderPassDescriptor.colorAttachments[0].texture = self.cachedTexture! + renderPassDescriptor.colorAttachments[0].loadAction = .dontCare + renderPassDescriptor.colorAttachments[0].storeAction = .store + renderPassDescriptor.colorAttachments[0].clearColor = MTLClearColor(red: 0.0, green: 0.0, blue: 0.0, alpha: 1.0) + guard let renderCommandEncoder = commandBuffer.makeRenderCommandEncoder(descriptor: renderPassDescriptor) else { + return input + } + + renderCommandEncoder.setViewport(MTLViewport( + originX: 0, originY: 0, + width: Double(width), height: Double(height), + znear: -1.0, zfar: 1.0) + ) + + renderCommandEncoder.setFragmentTexture(input, index: 0) + renderCommandEncoder.setFragmentBytes(&self.adjustments, length: MemoryLayout.size, index: 0) + + let allCurve = self.allCurve + let redCurve = self.redCurve + let greenCurve = self.greenCurve + let blueCurve = self.blueCurve + + renderCommandEncoder.setFragmentBytes(allCurve, length: MemoryLayout.size * 200, index: 1) + renderCommandEncoder.setFragmentBytes(redCurve, length: MemoryLayout.size * 200, index: 2) + renderCommandEncoder.setFragmentBytes(greenCurve, length: MemoryLayout.size * 200, index: 3) + renderCommandEncoder.setFragmentBytes(blueCurve, length: MemoryLayout.size * 200, index: 4) + + self.encodeDefaultCommands(using: renderCommandEncoder) + + renderCommandEncoder.endEncoding() + + return self.cachedTexture! + } +} diff --git a/submodules/TelegramUI/Components/MediaEditor/Sources/BlurRenderPass.swift b/submodules/TelegramUI/Components/MediaEditor/Sources/BlurRenderPass.swift new file mode 100644 index 0000000000..2682b98b94 --- /dev/null +++ b/submodules/TelegramUI/Components/MediaEditor/Sources/BlurRenderPass.swift @@ -0,0 +1,256 @@ +import Foundation +import Metal +import MetalPerformanceShaders +import simd + +enum MediaEditorBlurMode { + case off + case radial + case linear + case portrait +} + +struct MediaEditorBlur { + var dimensions: simd_float2 + var position: simd_float2 + var aspectRatio: simd_float1 + var size: simd_float1 + var falloff: simd_float1 + var rotation: simd_float1 +} + +private final class BlurGaussianPass: RenderPass { + private var cachedTexture: MTLTexture? + fileprivate var blur: MPSImageGaussianBlur? + + var updated: ((Data) -> Void)? + + func setup(device: MTLDevice, library: MTLLibrary) { + + } + + func process(input: MTLTexture, rotation: TextureRotation, device: MTLDevice, commandBuffer: MTLCommandBuffer) -> MTLTexture? { + return nil + } + + func process(input: MTLTexture, intensity: Float, rotation: TextureRotation, device: MTLDevice, commandBuffer: MTLCommandBuffer) -> MTLTexture? { + let radius = round(4.0 + intensity * 26.0) + if self.blur?.sigma != radius { + self.blur = MPSImageGaussianBlur(device: device, sigma: radius) + } + + if self.cachedTexture == nil { + let textureDescriptor = MTLTextureDescriptor() + textureDescriptor.textureType = .type2D + textureDescriptor.width = input.width + textureDescriptor.height = input.height + textureDescriptor.pixelFormat = input.pixelFormat + textureDescriptor.storageMode = .private + textureDescriptor.usage = [.shaderRead, .shaderWrite, .renderTarget] + guard let texture = device.makeTexture(descriptor: textureDescriptor) else { + return input + } + self.cachedTexture = texture + } + + if let blur = self.blur, let destinationTexture = self.cachedTexture { + blur.encode(commandBuffer: commandBuffer, sourceTexture: input, destinationTexture: destinationTexture) + } + + return self.cachedTexture + } +} + +private final class BlurLinearPass: DefaultRenderPass { + override var fragmentShaderFunctionName: String { + return "blurLinearFragmentShader" + } + + func process(input: MTLTexture, blurredTexture: MTLTexture, values: MediaEditorBlur, output: MTLTexture, rotation: TextureRotation, device: MTLDevice, commandBuffer: MTLCommandBuffer) -> MTLTexture? { + self.setupVerticesBuffer(device: device, rotation: rotation) + + let (width, height) = textureDimensionsForRotation(texture: input, rotation: rotation) + + let renderPassDescriptor = MTLRenderPassDescriptor() + renderPassDescriptor.colorAttachments[0].texture = output + renderPassDescriptor.colorAttachments[0].loadAction = .dontCare + renderPassDescriptor.colorAttachments[0].storeAction = .store + renderPassDescriptor.colorAttachments[0].clearColor = MTLClearColor(red: 0.0, green: 0.0, blue: 0.0, alpha: 0.0) + guard let renderCommandEncoder = commandBuffer.makeRenderCommandEncoder(descriptor: renderPassDescriptor) else { + return input + } + + renderCommandEncoder.setViewport(MTLViewport( + originX: 0, originY: 0, + width: Double(width), height: Double(height), + znear: -1.0, zfar: 1.0) + ) + + var values = values + renderCommandEncoder.setFragmentTexture(input, index: 0) + renderCommandEncoder.setFragmentTexture(blurredTexture, index: 1) + renderCommandEncoder.setFragmentBytes(&values, length: MemoryLayout.size, index: 0) + + self.encodeDefaultCommands(using: renderCommandEncoder) + + renderCommandEncoder.endEncoding() + + return output + } +} + +private final class BlurRadialPass: DefaultRenderPass { + override var fragmentShaderFunctionName: String { + return "blurRadialFragmentShader" + } + + func process(input: MTLTexture, blurredTexture: MTLTexture, values: MediaEditorBlur, output: MTLTexture, rotation: TextureRotation, device: MTLDevice, commandBuffer: MTLCommandBuffer) -> MTLTexture? { + self.setupVerticesBuffer(device: device, rotation: rotation) + + let (width, height) = textureDimensionsForRotation(texture: input, rotation: rotation) + + let renderPassDescriptor = MTLRenderPassDescriptor() + renderPassDescriptor.colorAttachments[0].texture = output + renderPassDescriptor.colorAttachments[0].loadAction = .dontCare + renderPassDescriptor.colorAttachments[0].storeAction = .store + renderPassDescriptor.colorAttachments[0].clearColor = MTLClearColor(red: 0.0, green: 0.0, blue: 0.0, alpha: 0.0) + guard let renderCommandEncoder = commandBuffer.makeRenderCommandEncoder(descriptor: renderPassDescriptor) else { + return input + } + + renderCommandEncoder.setViewport(MTLViewport( + originX: 0, originY: 0, + width: Double(width), height: Double(height), + znear: -1.0, zfar: 1.0) + ) + + var values = values + renderCommandEncoder.setFragmentTexture(input, index: 0) + renderCommandEncoder.setFragmentTexture(blurredTexture, index: 1) + renderCommandEncoder.setFragmentBytes(&values, length: MemoryLayout.size, index: 0) + + self.encodeDefaultCommands(using: renderCommandEncoder) + + renderCommandEncoder.endEncoding() + + return output + } +} + +private final class BlurPortraitPass: DefaultRenderPass { + fileprivate var cachedTexture: MTLTexture? + + override var fragmentShaderFunctionName: String { + return "blurPortraitFragmentShader" + } + + func process(input: MTLTexture, blurredTexture: MTLTexture, maskTexture: MTLTexture, values: MediaEditorBlur, output: MTLTexture, rotation: TextureRotation, device: MTLDevice, commandBuffer: MTLCommandBuffer) -> MTLTexture? { + self.setupVerticesBuffer(device: device, rotation: rotation) + + let (width, height) = textureDimensionsForRotation(texture: input, rotation: rotation) + + let renderPassDescriptor = MTLRenderPassDescriptor() + renderPassDescriptor.colorAttachments[0].texture = output + renderPassDescriptor.colorAttachments[0].loadAction = .dontCare + renderPassDescriptor.colorAttachments[0].storeAction = .store + renderPassDescriptor.colorAttachments[0].clearColor = MTLClearColor(red: 0.0, green: 0.0, blue: 0.0, alpha: 0.0) + guard let renderCommandEncoder = commandBuffer.makeRenderCommandEncoder(descriptor: renderPassDescriptor) else { + return input + } + + renderCommandEncoder.setViewport(MTLViewport( + originX: 0, originY: 0, + width: Double(width), height: Double(height), + znear: -1.0, zfar: 1.0) + ) + + var values = values + renderCommandEncoder.setFragmentTexture(input, index: 0) + renderCommandEncoder.setFragmentTexture(blurredTexture, index: 1) + renderCommandEncoder.setFragmentTexture(maskTexture, index: 2) + renderCommandEncoder.setFragmentBytes(&values, length: MemoryLayout.size, index: 0) + + self.encodeDefaultCommands(using: renderCommandEncoder) + + renderCommandEncoder.endEncoding() + + return output + } +} + + +final class BlurRenderPass: RenderPass { + fileprivate var cachedTexture: MTLTexture? + + var maskTexture: MTLTexture? + + private let blurPass = BlurGaussianPass() + private let linearPass = BlurLinearPass() + private let radialPass = BlurRadialPass() + private let portraitPass = BlurPortraitPass() + + var value = MediaEditorBlur( + dimensions: simd_float2(0.0, 0.0), + position: simd_float2(0.5, 0.5), + aspectRatio: 1.0, + size: 0.2, + falloff: 0.2, + rotation: 0.0 + ) + var intensity: simd_float1 = 0.0 + var mode: MediaEditorBlurMode = .off + + func setup(device: MTLDevice, library: MTLLibrary) { + self.blurPass.setup(device: device, library: library) + self.linearPass.setup(device: device, library: library) + self.radialPass.setup(device: device, library: library) + self.portraitPass.setup(device: device, library: library) + } + + func process(input: MTLTexture, rotation: TextureRotation, device: MTLDevice, commandBuffer: MTLCommandBuffer) -> MTLTexture? { + self.process(input: input, maskTexture: self.maskTexture, rotation: rotation, device: device, commandBuffer: commandBuffer) + } + + func process(input: MTLTexture, maskTexture: MTLTexture?, rotation: TextureRotation, device: MTLDevice, commandBuffer: MTLCommandBuffer) -> MTLTexture? { + guard self.intensity > 0.005 && self.mode != .off else { + return input + } + + let (width, height) = textureDimensionsForRotation(texture: input, rotation: rotation) + + if self.cachedTexture == nil { + self.value.aspectRatio = Float(height) / Float(width) + + let textureDescriptor = MTLTextureDescriptor() + textureDescriptor.textureType = .type2D + textureDescriptor.width = width + textureDescriptor.height = height + textureDescriptor.pixelFormat = input.pixelFormat + textureDescriptor.storageMode = .private + textureDescriptor.usage = [.shaderRead, .shaderWrite, .renderTarget] + guard let texture = device.makeTexture(descriptor: textureDescriptor) else { + return input + } + self.cachedTexture = texture + } + + guard let blurredTexture = self.blurPass.process(input: input, intensity: self.intensity, rotation: rotation, device: device, commandBuffer: commandBuffer), let output = self.cachedTexture else { + return input + } + + switch self.mode { + case .linear: + return self.linearPass.process(input: input, blurredTexture: blurredTexture, values: self.value, output: output, rotation: rotation, device: device, commandBuffer: commandBuffer) + case .radial: + return self.radialPass.process(input: input, blurredTexture: blurredTexture, values: self.value, output: output, rotation: rotation, device: device, commandBuffer: commandBuffer) + case .portrait: + if let maskTexture { + return self.portraitPass.process(input: input, blurredTexture: blurredTexture, maskTexture: maskTexture, values: self.value, output: output, rotation: rotation, device: device, commandBuffer: commandBuffer) + } else { + return input + } + default: + return input + } + } +} diff --git a/submodules/TelegramUI/Components/MediaEditor/Sources/EnhanceRenderPass.swift b/submodules/TelegramUI/Components/MediaEditor/Sources/EnhanceRenderPass.swift new file mode 100644 index 0000000000..8479418a52 --- /dev/null +++ b/submodules/TelegramUI/Components/MediaEditor/Sources/EnhanceRenderPass.swift @@ -0,0 +1,293 @@ +import Foundation +import UIKit +import Metal +import MetalPerformanceShaders +import simd +import CoreImage + +struct TextureSize { + let width: Int + let height: Int +} + +private final class EnhanceLightnessPass: DefaultRenderPass { + fileprivate var cachedTexture: MTLTexture? + + override var fragmentShaderFunctionName: String { + return "rgbToLightnessFragmentShader" + } + + override var pixelFormat: MTLPixelFormat { + return .r8Unorm + } + + func process(input: MTLTexture, size: TextureSize, scale: simd_float2, rotation: TextureRotation, device: MTLDevice, commandBuffer: MTLCommandBuffer) -> MTLTexture? { + self.setupVerticesBuffer(device: device, rotation: rotation) + + let width: Int + let height: Int + switch rotation { + case .rotate90Degrees, .rotate270Degrees: + width = size.height + height = size.width + default: + width = size.width + height = size.height + } + + if self.cachedTexture == nil { + let textureDescriptor = MTLTextureDescriptor() + textureDescriptor.textureType = .type2D + textureDescriptor.width = width + textureDescriptor.height = height + textureDescriptor.pixelFormat = .r8Unorm + textureDescriptor.storageMode = .private + textureDescriptor.usage = [.shaderRead, .shaderWrite, .renderTarget] + guard let texture = device.makeTexture(descriptor: textureDescriptor) else { + return nil + } + texture.label = "lightnessTexture" + self.cachedTexture = texture + } + + let renderPassDescriptor = MTLRenderPassDescriptor() + renderPassDescriptor.colorAttachments[0].texture = self.cachedTexture! + renderPassDescriptor.colorAttachments[0].loadAction = .dontCare + renderPassDescriptor.colorAttachments[0].storeAction = .store + renderPassDescriptor.colorAttachments[0].clearColor = MTLClearColor(red: 0.0, green: 0.0, blue: 0.0, alpha: 0.0) + guard let renderCommandEncoder = commandBuffer.makeRenderCommandEncoder(descriptor: renderPassDescriptor) else { + return nil + } + + renderCommandEncoder.setViewport(MTLViewport( + originX: 0, originY: 0, + width: Double(size.width), height: Double(size.height), + znear: -1.0, zfar: 1.0) + ) + + let samplerDescriptor = MTLSamplerDescriptor() + samplerDescriptor.minFilter = .linear + samplerDescriptor.magFilter = .linear + samplerDescriptor.sAddressMode = .mirrorRepeat + samplerDescriptor.tAddressMode = .mirrorRepeat + samplerDescriptor.rAddressMode = .mirrorRepeat + + guard let samplerState = device.makeSamplerState(descriptor: samplerDescriptor) else { + return nil + } + + var scale = scale + renderCommandEncoder.setFragmentTexture(input, index: 0) + renderCommandEncoder.setFragmentBytes(&scale, length: MemoryLayout.size, index: 0) + renderCommandEncoder.setFragmentSamplerState(samplerState, index: 0) + + self.encodeDefaultCommands(using: renderCommandEncoder) + + renderCommandEncoder.endEncoding() + + //saveTexture(self.cachedTexture!, name: "lightness", device: device) + + return self.cachedTexture! + } +} + +private let binCount = 256 +struct MediaEditorEnhanceLUTGeneratorParameters { + var histogramBins: simd_uint1 + var clipLimit: simd_uint1 + var totalPixelCountPerTile: simd_uint1 + var numberOfLUTs: simd_uint1 +} + +private final class EnhanceLUTGeneratorPass: RenderPass { + fileprivate var pipelineState: MTLComputePipelineState? + fileprivate var histogramBuffer: MTLBuffer? + fileprivate var calculation: MPSImageHistogram? + + private var lutTexture: MTLTexture? + + func setup(device: MTLDevice, library: MTLLibrary) { + + } + + func setup(gridSize: TextureSize, device: MTLDevice, library: MTLLibrary) { + var histogramInfo = MPSImageHistogramInfo( + numberOfHistogramEntries: binCount, + histogramForAlpha: false, + minPixelValue: vector_float4(0,0,0,0), + maxPixelValue: vector_float4(1,1,1,1) + ) + + let calculation = MPSImageHistogram(device: device, histogramInfo: &histogramInfo) + calculation.zeroHistogram = false + self.calculation = calculation + + let pipelineDescriptor = MTLComputePipelineDescriptor() + pipelineDescriptor.computeFunction = library.makeFunction(name: "CLAHEGenerateLUT") + + do { + self.pipelineState = try device.makeComputePipelineState(descriptor: pipelineDescriptor, options: .argumentInfo, reflection: nil) + } catch { + print(error.localizedDescription) + } + } + + func process(input: MTLTexture, rotation: TextureRotation, device: MTLDevice, commandBuffer: MTLCommandBuffer) -> MTLTexture? { + return nil + } + + func process(input: MTLTexture, gridSize: TextureSize, clipLimit: Float, device: MTLDevice, commandBuffer: MTLCommandBuffer) -> MTLTexture? { + let lutCount = gridSize.width * gridSize.height + let tileSize = TextureSize(width: input.width / gridSize.width, height: input.height / gridSize.height); + let clipLimitValue = max(1, clipLimit * Float(tileSize.width * tileSize.height) / Float(binCount)) + + if self.lutTexture == nil { + let textureDescriptor = MTLTextureDescriptor() + textureDescriptor.textureType = .type2D + textureDescriptor.width = binCount + textureDescriptor.height = lutCount + textureDescriptor.pixelFormat = .r8Unorm + textureDescriptor.storageMode = .private + textureDescriptor.usage = [.shaderRead, .shaderWrite, .renderTarget] + guard let texture = device.makeTexture(descriptor: textureDescriptor) else { + return nil + } + self.lutTexture = texture + texture.label = "lutTexture" + } + + guard let calculation = self.calculation, let histogramBuffer = device.makeBuffer(length: calculation.histogramSize(forSourceFormat: .r8Unorm) * lutCount, options: [.storageModePrivate]) else { + return nil + } + + let histogramSize = calculation.histogramSize(forSourceFormat: input.pixelFormat) + for i in 0 ..< lutCount { + let col = i % gridSize.width + let row = i / gridSize.width + calculation.clipRectSource = MTLRegionMake2D(col * tileSize.width, row * tileSize.height, tileSize.width, tileSize.height) + + calculation.encode(to: commandBuffer, sourceTexture: input, histogram: histogramBuffer, histogramOffset: i * histogramSize) + } + + guard let computeCommandEncoder = commandBuffer.makeComputeCommandEncoder() else { + return nil + } + + guard let pipelineState = self.pipelineState else { + return nil + } + + var parameters = MediaEditorEnhanceLUTGeneratorParameters( + histogramBins: UInt32(binCount), + clipLimit: UInt32(clipLimitValue), + totalPixelCountPerTile: UInt32(tileSize.width * tileSize.height), + numberOfLUTs: UInt32(lutCount) + ) + + computeCommandEncoder.setComputePipelineState(pipelineState) + computeCommandEncoder.setBuffer(histogramBuffer, offset: 0, index: 0) + computeCommandEncoder.setBytes(¶meters, length: MemoryLayout.size, index: 1) + computeCommandEncoder.setTexture(self.lutTexture, index: 0) + + let w = pipelineState.threadExecutionWidth + let threadsPerThreadgroup = MTLSize(width: w, height: 1, depth: 1) + let threadgroupsPerGrid = MTLSize(width: (lutCount + w - 1) / w, height: 1, depth: 1) + computeCommandEncoder.dispatchThreadgroups(threadgroupsPerGrid, threadsPerThreadgroup: threadsPerThreadgroup) + computeCommandEncoder.endEncoding() + + return self.lutTexture! + } +} + +private final class EnhanceLookupPass: DefaultRenderPass { + fileprivate var cachedTexture: MTLTexture? + + override var fragmentShaderFunctionName: String { + return "enhanceColorLookupFragmentShader" + } + + func process(input: MTLTexture, lookupTexture: MTLTexture, value: simd_float1, gridSize: simd_float2, rotation: TextureRotation, device: MTLDevice, commandBuffer: MTLCommandBuffer) -> MTLTexture? { + self.setupVerticesBuffer(device: device, rotation: rotation) + + let (width, height) = textureDimensionsForRotation(texture: input, rotation: rotation) + + if self.cachedTexture == nil { + let textureDescriptor = MTLTextureDescriptor() + textureDescriptor.textureType = .type2D + textureDescriptor.width = width + textureDescriptor.height = height + textureDescriptor.pixelFormat = input.pixelFormat + textureDescriptor.storageMode = .private + textureDescriptor.usage = [.shaderRead, .shaderWrite, .renderTarget] + guard let texture = device.makeTexture(descriptor: textureDescriptor) else { + return input + } + self.cachedTexture = texture + } + + let renderPassDescriptor = MTLRenderPassDescriptor() + renderPassDescriptor.colorAttachments[0].texture = self.cachedTexture! + renderPassDescriptor.colorAttachments[0].loadAction = .dontCare + renderPassDescriptor.colorAttachments[0].storeAction = .store + renderPassDescriptor.colorAttachments[0].clearColor = MTLClearColor(red: 0.0, green: 0.0, blue: 0.0, alpha: 0.0) + guard let renderCommandEncoder = commandBuffer.makeRenderCommandEncoder(descriptor: renderPassDescriptor) else { + return input + } + + renderCommandEncoder.setViewport(MTLViewport( + originX: 0, originY: 0, + width: Double(width), height: Double(height), + znear: -1.0, zfar: 1.0) + ) + + var gridSize = gridSize + var value = value + renderCommandEncoder.setFragmentTexture(input, index: 0) + renderCommandEncoder.setFragmentTexture(lookupTexture, index: 1) + renderCommandEncoder.setFragmentBytes(&gridSize, length: MemoryLayout.size, index: 0) + renderCommandEncoder.setFragmentBytes(&value, length: MemoryLayout.size, index: 1) + + self.encodeDefaultCommands(using: renderCommandEncoder) + + renderCommandEncoder.endEncoding() + + return self.cachedTexture! + } +} + +final class EnhanceRenderPass: RenderPass { + private let lightnessPass = EnhanceLightnessPass() + private let lutGeneratorPass = EnhanceLUTGeneratorPass() + private let lookupPass = EnhanceLookupPass() + + var value: simd_float1 = 0.0 + + let clipLimit: Float = 1.25 + let tileGridSize: TextureSize = TextureSize(width: 4, height: 4) + + func setup(device: MTLDevice, library: MTLLibrary) { + self.lightnessPass.setup(device: device, library: library) + self.lutGeneratorPass.setup(gridSize: self.tileGridSize, device: device, library: library) + self.lookupPass.setup(device: device, library: library) + } + + func process(input: MTLTexture, rotation: TextureRotation, device: MTLDevice, commandBuffer: MTLCommandBuffer) -> MTLTexture? { + guard self.value > 0.005 else { + return input + } + let dY = (self.tileGridSize.height - (input.height % self.tileGridSize.height)) % self.tileGridSize.height + let dX = (self.tileGridSize.width - (input.width % self.tileGridSize.width)) % self.tileGridSize.width + + let lightnessSize = TextureSize(width: input.width + dX, height: input.height + dY) + let lightnessScale = simd_float2(Float(input.width + dX) / Float(input.width), Float(input.height + dY) / Float(input.height)) + + let lightness = self.lightnessPass.process(input: input, size: lightnessSize, scale: lightnessScale, rotation: rotation, device: device, commandBuffer: commandBuffer) + + let lookupTexture = self.lutGeneratorPass.process(input: lightness!, gridSize: self.tileGridSize, clipLimit: self.clipLimit, device: device, commandBuffer: commandBuffer) + + let gridSize = simd_float2(Float(self.tileGridSize.width), Float(self.tileGridSize.height)) + let output = self.lookupPass.process(input: input, lookupTexture: lookupTexture!, value: self.value, gridSize: gridSize, rotation: rotation, device: device, commandBuffer: commandBuffer) + + return output + } +} diff --git a/submodules/TelegramUI/Components/MediaEditor/Sources/HistogramCalculationPass.swift b/submodules/TelegramUI/Components/MediaEditor/Sources/HistogramCalculationPass.swift new file mode 100644 index 0000000000..09df3d5229 --- /dev/null +++ b/submodules/TelegramUI/Components/MediaEditor/Sources/HistogramCalculationPass.swift @@ -0,0 +1,41 @@ +import Foundation +import Metal +import simd +import MetalPerformanceShaders + +final class HistogramCalculationPass: RenderPass { + fileprivate var histogramInfoBuffer: MTLBuffer? + fileprivate var calculation: MPSImageHistogram? + + var updated: ((Data) -> Void)? + + func setup(device: MTLDevice, library: MTLLibrary) { + var histogramInfo = MPSImageHistogramInfo( + numberOfHistogramEntries: 256, + histogramForAlpha: false, + minPixelValue: vector_float4(0,0,0,0), + maxPixelValue: vector_float4(1,1,1,1) + ) + + let calculation = MPSImageHistogram(device: device, histogramInfo: &histogramInfo) + calculation.zeroHistogram = false + + let bufferLength = calculation.histogramSize(forSourceFormat: .bgra8Unorm) + + if let histogramInfoBuffer = device.makeBuffer(length: bufferLength, options: [.storageModeShared]) { + self.calculation = calculation + self.histogramInfoBuffer = histogramInfoBuffer + } + } + + func process(input: MTLTexture, rotation: TextureRotation, device: MTLDevice, commandBuffer: MTLCommandBuffer) -> MTLTexture? { + if let histogramInfoBuffer = self.histogramInfoBuffer, let calculation = self.calculation { + calculation.encode(to: commandBuffer, sourceTexture: input, histogram: histogramInfoBuffer, histogramOffset: 0) + + let data = Data(bytes: histogramInfoBuffer.contents(), count: histogramInfoBuffer.length) + self.updated?(data) + } + + return input + } +} diff --git a/submodules/TelegramUI/Components/MediaEditor/Sources/ImageTextureSource.swift b/submodules/TelegramUI/Components/MediaEditor/Sources/ImageTextureSource.swift new file mode 100644 index 0000000000..40dc7823b4 --- /dev/null +++ b/submodules/TelegramUI/Components/MediaEditor/Sources/ImageTextureSource.swift @@ -0,0 +1,52 @@ +import Foundation +import AVFoundation +import Metal +import MetalKit + +final class ImageTextureSource: TextureSource { + weak var output: TextureConsumer? + + var textureLoader: MTKTextureLoader? + var texture: MTLTexture? + + init(image: UIImage, renderTarget: RenderTarget) { + guard let device = renderTarget.mtlDevice, let cgImage = image.cgImage else { + return + } + let textureLoader = MTKTextureLoader(device: device) + self.textureLoader = textureLoader + + self.texture = try? textureLoader.newTexture(cgImage: cgImage, options: nil) + } + + func start() { + + } + + func pause() { + + } + + func connect(to consumer: TextureConsumer) { + self.output = consumer + + if let texture = self.texture { + self.output?.consumeTexture(texture, rotation: .rotate0Degrees) + } + } +} + +func pixelBufferToMTLTexture(pixelBuffer:CVPixelBuffer, textureCache: CVMetalTextureCache) -> MTLTexture? +{ + let width = CVPixelBufferGetWidth(pixelBuffer) + let height = CVPixelBufferGetHeight(pixelBuffer) + + let format: MTLPixelFormat = .r8Unorm + var textureRef : CVMetalTexture? + let status = CVMetalTextureCacheCreateTextureFromImage(nil, textureCache, pixelBuffer, nil, format, width, height, 0, &textureRef) + if status == kCVReturnSuccess { + return CVMetalTextureGetTexture(textureRef!) + } + + return nil +} diff --git a/submodules/TelegramUI/Components/MediaEditor/Sources/MediaEditor.swift b/submodules/TelegramUI/Components/MediaEditor/Sources/MediaEditor.swift new file mode 100644 index 0000000000..9939635185 --- /dev/null +++ b/submodules/TelegramUI/Components/MediaEditor/Sources/MediaEditor.swift @@ -0,0 +1,407 @@ +import Foundation +import UIKit +import Metal +import MetalKit +import Vision +import Photos +import SwiftSignalKit +import Display +import TelegramCore +import AccountContext +import TelegramPresentationData + +public final class MediaEditor { + public enum Subject { + case image(UIImage, PixelDimensions) + case video(String, PixelDimensions) + case asset(PHAsset) + + var dimensions: PixelDimensions { + switch self { + case let .image(_, dimensions), let .video(_, dimensions): + return dimensions + case let .asset(asset): + return PixelDimensions(width: Int32(asset.pixelWidth), height: Int32(asset.pixelHeight)) + } + } + } + + private let context: AccountContext + private let subject: Subject + private let renderer: MediaEditorRenderer + private var player: AVPlayer? + private var didPlayToEndTimeObserver: NSObjectProtocol? + + private weak var previewView: MediaEditorPreviewView? + + public var values: MediaEditorValues { + didSet { + self.updateRenderValues() + } + } + + private let enhancePass = EnhanceRenderPass() + private let sharpenPass = SharpenRenderPass() + private let blurPass = BlurRenderPass() + private let adjustmentsPass = AdjustmentsRenderPass() + private let histogramCalculationPass = HistogramCalculationPass() + + private var textureSourceDisposable: Disposable? + + private let gradientColorsPromise = Promise<(UIColor, UIColor)?>() + public var gradientColors: Signal<(UIColor, UIColor)?, NoError> { + return self.gradientColorsPromise.get() + } + private var gradientColorsValue: (UIColor, UIColor)? { + didSet { + self.gradientColorsPromise.set(.single(self.gradientColorsValue)) + } + } + + public let histogramPipe = ValuePipe() + public var histogram: Signal { + return self.histogramPipe.signal() + } + + var textureCache: CVMetalTextureCache! + + public var hasPortraitMask: Bool { + return self.blurPass.maskTexture != nil + } + + public var resultIsVideo: Bool { + let hasAnimatedEntities = false + return self.player != nil || hasAnimatedEntities + } + + public var resultImage: UIImage? { + return self.renderer.finalRenderedImage() + } + + public init(context: AccountContext, subject: Subject, values: MediaEditorValues? = nil) { + self.context = context + self.subject = subject + if let values { + self.values = values + } else { + self.values = MediaEditorValues( + originalDimensions: subject.dimensions, + cropOffset: .zero, + cropSize: nil, + cropScale: 1.0, + cropRotation: 0.0, + cropMirroring: false, + videoTrimRange: nil, + drawing: nil, + toolValues: [:] + ) + } + + self.renderer = MediaEditorRenderer() + self.renderer.addRenderPass(self.enhancePass) + //self.renderer.addRenderPass(self.sharpenPass) + self.renderer.addRenderPass(self.blurPass) + self.renderer.addRenderPass(self.adjustmentsPass) + self.renderer.addRenderPass(self.histogramCalculationPass) + + self.histogramCalculationPass.updated = { [weak self] data in + if let self { + self.histogramPipe.putNext(data) + } + } + } + + deinit { + self.textureSourceDisposable?.dispose() + + if let didPlayToEndTimeObserver = self.didPlayToEndTimeObserver { + NotificationCenter.default.removeObserver(didPlayToEndTimeObserver) + } + } + + private func setupSource() { + guard let renderTarget = self.previewView else { + return + } + + if let device = renderTarget.mtlDevice, CVMetalTextureCacheCreate(nil, nil, device, nil, &self.textureCache) != kCVReturnSuccess { + print("error") + } + + func gradientColors(from image: UIImage) -> (UIColor, UIColor) { + let context = DrawingContext(size: CGSize(width: 1.0, height: 4.0), scale: 1.0, clear: false)! + context.withFlippedContext({ context in + if let cgImage = image.cgImage { + context.draw(cgImage, in: CGRect(x: 0.0, y: 0.0, width: 1.0, height: 4.0)) + } + }) + return (context.colorAt(CGPoint(x: 0.0, y: 0.0)), context.colorAt(CGPoint(x: 0.0, y: 3.0))) + } + + let textureSource: Signal<(TextureSource, UIImage?, AVPlayer?, UIColor, UIColor), NoError> + switch subject { + case let .image(image, _): + let colors = gradientColors(from: image) + textureSource = .single((ImageTextureSource(image: image, renderTarget: renderTarget), image, nil, colors.0, colors.1)) + case let .video(path, _): + textureSource = Signal { subscriber in + let url = URL(fileURLWithPath: path) + let asset = AVURLAsset(url: url) + let imageGenerator = AVAssetImageGenerator(asset: asset) + imageGenerator.appliesPreferredTrackTransform = true + imageGenerator.generateCGImagesAsynchronously(forTimes: [NSValue(time: CMTime(seconds: 0, preferredTimescale: CMTimeScale(30.0)))]) { _, image, _, _, _ in + let playerItem = AVPlayerItem(asset: asset) + let player = AVPlayer(playerItem: playerItem) + if let image { + let colors = gradientColors(from: UIImage(cgImage: image)) + subscriber.putNext((VideoTextureSource(player: player, renderTarget: renderTarget), nil, player, colors.0, colors.1)) + } else { + subscriber.putNext((VideoTextureSource(player: player, renderTarget: renderTarget), nil, player, .black, .black)) + } + } + return ActionDisposable { + imageGenerator.cancelAllCGImageGeneration() + } + } + case let .asset(asset): + textureSource = Signal { subscriber in + if asset.mediaType == .video { + let requestId = PHImageManager.default().requestImage(for: asset, targetSize: CGSize(width: 128.0, height: 128.0), contentMode: .aspectFit, options: nil, resultHandler: { image, info in + if let image { + var degraded = false + if let info { + if let cancelled = info[PHImageCancelledKey] as? Bool, cancelled { + return + } + if let degradedValue = info[PHImageResultIsDegradedKey] as? Bool, degradedValue { + degraded = true + } + } + if !degraded { + let colors = gradientColors(from: image) + PHImageManager.default().requestAVAsset(forVideo: asset, options: nil, resultHandler: { asset, _, _ in + if let asset { + let playerItem = AVPlayerItem(asset: asset) + let player = AVPlayer(playerItem: playerItem) + subscriber.putNext((VideoTextureSource(player: player, renderTarget: renderTarget), nil, player, colors.0, colors.1)) + subscriber.putCompletion() + } + }) + } + } + }) + return ActionDisposable { + PHImageManager.default().cancelImageRequest(requestId) + } + } else { + let requestId = PHImageManager.default().requestImage(for: asset, targetSize: CGSize(width: 1920.0, height: 1920.0), contentMode: .aspectFit, options: nil, resultHandler: { image, info in + if let image { + var degraded = false + if let info { + if let cancelled = info[PHImageCancelledKey] as? Bool, cancelled { + return + } + if let degradedValue = info[PHImageResultIsDegradedKey] as? Bool, degradedValue { + degraded = true + } + } + if !degraded { + let colors = gradientColors(from: image) + subscriber.putNext((ImageTextureSource(image: image, renderTarget: renderTarget), image, nil, colors.0, colors.1)) + subscriber.putCompletion() + } + } + }) + return ActionDisposable { + PHImageManager.default().cancelImageRequest(requestId) + } + } + } + } + + self.textureSourceDisposable = (textureSource + |> deliverOnMainQueue).start(next: { [weak self] sourceAndColors in + if let self { + let (source, image, player, topColor, bottomColor) = sourceAndColors + self.renderer.textureSource = source + self.player = player + self.gradientColorsValue = (topColor, bottomColor) + + self.maybeGeneratePersonSegmentation(image) + + if let player { + self.didPlayToEndTimeObserver = NotificationCenter.default.addObserver(forName: NSNotification.Name.AVPlayerItemDidPlayToEndTime, object: player.currentItem, queue: nil, using: { [weak self] notification in + if let strongSelf = self { + strongSelf.player?.seek(to: CMTime(seconds: 0.0, preferredTimescale: 30)) + strongSelf.player?.play() + } + }) + } else { + self.didPlayToEndTimeObserver = nil + } + } + }) + } + + public func attachPreviewView(_ previewView: MediaEditorPreviewView) { + self.previewView?.renderer = nil + + self.previewView = previewView + previewView.renderer = self.renderer + + self.setupSource() + } + + public func getToolValue(_ key: EditorToolKey) -> Any? { + return self.values.toolValues[key] + } + + public func setToolValue(_ key: EditorToolKey, value: Any) { + var updatedToolValues = self.values.toolValues + updatedToolValues[key] = value + self.values = self.values.withUpdatedToolValues(updatedToolValues) + self.updateRenderValues() + } + + func updateRenderValues() { + for (key, value) in self.values.toolValues { + switch key { + case .enhance: + if let value = value as? Float { + self.enhancePass.value = value + } else { + self.enhancePass.value = 0.0 + } + case .brightness: + if let value = value as? Float { + self.adjustmentsPass.adjustments.exposure = value + } else { + self.adjustmentsPass.adjustments.exposure = 0.0 + } + case .contrast: + if let value = value as? Float { + self.adjustmentsPass.adjustments.contrast = value + } else { + self.adjustmentsPass.adjustments.contrast = 0.0 + } + case .saturation: + if let value = value as? Float { + self.adjustmentsPass.adjustments.saturation = value + } else { + self.adjustmentsPass.adjustments.saturation = 0.0 + } + case .warmth: + if let value = value as? Float { + self.adjustmentsPass.adjustments.warmth = value + } else { + self.adjustmentsPass.adjustments.warmth = 0.0 + } + case .fade: + if let value = value as? Float { + self.adjustmentsPass.adjustments.fade = value + } else { + self.adjustmentsPass.adjustments.fade = 0.0 + } + case .highlights: + if let value = value as? Float { + self.adjustmentsPass.adjustments.highlights = value + } else { + self.adjustmentsPass.adjustments.highlights = 0.0 + } + case .shadows: + if let value = value as? Float { + self.adjustmentsPass.adjustments.shadows = value + } else { + self.adjustmentsPass.adjustments.shadows = 0.0 + } + case .vignette: + if let value = value as? Float { + self.adjustmentsPass.adjustments.vignette = value + } else { + self.adjustmentsPass.adjustments.vignette = 0.0 + } + case .grain: + break + case .sharpen: + if let value = value as? Float { + self.sharpenPass.value = value + } else { + self.sharpenPass.value = 0.0 + } + case .shadowsTint: + if let value = value as? TintValue { + let (red, green, blue, _) = value.color.components + self.adjustmentsPass.adjustments.shadowsTintColor = simd_float3(Float(red), Float(green), Float(blue)) + self.adjustmentsPass.adjustments.shadowsTintIntensity = value.intensity + } + case .highlightsTint: + if let value = value as? TintValue { + let (red, green, blue, _) = value.color.components + self.adjustmentsPass.adjustments.shadowsTintColor = simd_float3(Float(red), Float(green), Float(blue)) + self.adjustmentsPass.adjustments.highlightsTintIntensity = value.intensity + } + case .blur: + if let value = value as? BlurValue { + switch value.mode { + case .off: + self.blurPass.mode = .off + case .linear: + self.blurPass.mode = .linear + case .radial: + self.blurPass.mode = .radial + case .portrait: + self.blurPass.mode = .portrait + } + self.blurPass.intensity = value.intensity + self.blurPass.value.size = Float(value.size) + self.blurPass.value.position = simd_float2(Float(value.position.x), Float(value.position.y)) + self.blurPass.value.falloff = Float(value.falloff) + self.blurPass.value.rotation = Float(value.rotation) + } + case .curves: + var value = (value as? CurvesValue) ?? CurvesValue.initial + let allDataPoints = value.all.dataPoints + let redDataPoints = value.red.dataPoints + let greenDataPoints = value.green.dataPoints + let blueDataPoints = value.blue.dataPoints + + self.adjustmentsPass.allCurve = allDataPoints + self.adjustmentsPass.redCurve = redDataPoints + self.adjustmentsPass.greenCurve = greenDataPoints + self.adjustmentsPass.blueCurve = blueDataPoints + } + } + self.previewView?.scheduleFrame() + } + + private func maybeGeneratePersonSegmentation(_ image: UIImage?) { + if #available(iOS 15.0, *), let cgImage = image?.cgImage { + let faceRequest = VNDetectFaceRectanglesRequest { [weak self] request, _ in + guard let _ = request.results?.first as? VNFaceObservation else { return } + + let personRequest = VNGeneratePersonSegmentationRequest(completionHandler: { [weak self] request, error in + if let self, let result = (request as? VNGeneratePersonSegmentationRequest)?.results?.first { + Queue.mainQueue().async { + self.blurPass.maskTexture = pixelBufferToMTLTexture(pixelBuffer: result.pixelBuffer, textureCache: self.textureCache) + } + } + }) + personRequest.qualityLevel = .accurate + personRequest.outputPixelFormat = kCVPixelFormatType_OneComponent8 + + let handler = VNImageRequestHandler(cgImage: cgImage, options: [:]) + do { + try handler.perform([personRequest]) + } catch { + print(error) + } + } + let handler = VNImageRequestHandler(cgImage: cgImage, options: [:]) + do { + try handler.perform([faceRequest]) + } catch { + print(error) + } + } + } +} diff --git a/submodules/TelegramUI/Components/MediaEditor/Sources/MediaEditorPreviewView.swift b/submodules/TelegramUI/Components/MediaEditor/Sources/MediaEditorPreviewView.swift new file mode 100644 index 0000000000..adf68aefbe --- /dev/null +++ b/submodules/TelegramUI/Components/MediaEditor/Sources/MediaEditorPreviewView.swift @@ -0,0 +1,70 @@ +import Foundation +import UIKit +import Metal +import MetalKit +import SwiftSignalKit + +public final class MediaEditorPreviewView: MTKView, MTKViewDelegate, RenderTarget { + var renderer: MediaEditorRenderer? { + didSet { + if let renderer = self.renderer { + renderer.renderTargetDidChange(self) + } + } + } + + var drawable: MTLDrawable? { + return self.currentDrawable + } + + var renderPassDescriptor: MTLRenderPassDescriptor? { + return self.currentRenderPassDescriptor + } + + var mtlDevice: MTLDevice? { + return self.device + } + + public override init(frame frameRect: CGRect, device: MTLDevice?) { + super.init(frame: frameRect, device: device) + + self.setup() + } + + required init(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + private func setup() { + guard let device = MTLCreateSystemDefaultDevice() else { + return + } + + self.device = device + self.delegate = self + + self.colorPixelFormat = .bgra8Unorm + + self.isPaused = true + self.enableSetNeedsDisplay = false + } + + func scheduleFrame() { + Queue.mainQueue().async { + self.draw() + } + } + + public func mtkView(_ view: MTKView, drawableSizeWillChange size: CGSize) { + Queue.mainQueue().justDispatch { + self.renderer?.renderTargetDrawableSizeDidChange(size) + } + } + + public func draw(in view: MTKView) { + guard self.frame.width > 0.0 else { + return + } + self.renderer?.renderFrame() + } +} diff --git a/submodules/TelegramUI/Components/MediaEditor/Sources/MediaEditorRenderer.swift b/submodules/TelegramUI/Components/MediaEditor/Sources/MediaEditorRenderer.swift new file mode 100644 index 0000000000..354e3003a7 --- /dev/null +++ b/submodules/TelegramUI/Components/MediaEditor/Sources/MediaEditorRenderer.swift @@ -0,0 +1,185 @@ +import Foundation +import UIKit +import Metal +import MetalKit +import Photos +import SwiftSignalKit + +protocol TextureConsumer: AnyObject { + func consumeTexture(_ texture: MTLTexture, rotation: TextureRotation) +} + +final class RenderingContext { + let device: MTLDevice + let commandBuffer: MTLCommandBuffer + + init( + device: MTLDevice, + commandBuffer: MTLCommandBuffer + ) { + self.device = device + self.commandBuffer = commandBuffer + } +} + +protocol RenderPass: AnyObject { + func setup(device: MTLDevice, library: MTLLibrary) + func process(input: MTLTexture, rotation: TextureRotation, device: MTLDevice, commandBuffer: MTLCommandBuffer) -> MTLTexture? +} + +protocol TextureSource { + func pause() + func start() + func connect(to: TextureConsumer) +} + +protocol RenderTarget: AnyObject { + var mtlDevice: MTLDevice? { get } + + var drawableSize: CGSize { get } + var colorPixelFormat: MTLPixelFormat { get } + var drawable: MTLDrawable? { get } + var renderPassDescriptor: MTLRenderPassDescriptor? { get } + + func scheduleFrame() +} + +final class MediaEditorRenderer: TextureConsumer { + var textureSource: TextureSource? { + willSet { + self.textureSource?.pause() + } + didSet { + self.textureSource?.connect(to: self) + self.textureSource?.start() + } + } + + var semaphore = DispatchSemaphore(value: 3) + private var renderPasses: [RenderPass] = [] + private var outputRenderPass = OutputRenderPass() + private weak var renderTarget: RenderTarget? { + didSet { + self.outputRenderPass.renderTarget = self.renderTarget + } + } + + private var commandQueue: MTLCommandQueue? + private var currentTexture: MTLTexture? + private var currentRotation: TextureRotation = .rotate0Degrees + private var library: MTLLibrary? + + private weak var finalTexture: MTLTexture? + + public init() { + + } + + deinit { + for _ in 0 ..< 3 { + self.semaphore.signal() + } + } + + func addRenderPass(_ renderPass: RenderPass) { + self.renderPasses.append(renderPass) + if let device = self.renderTarget?.mtlDevice, let library = self.library { + renderPass.setup(device: device, library: library) + } + } + + func setup() { + guard let device = self.renderTarget?.mtlDevice else { + return + } + + let mainBundle = Bundle(for: MediaEditorRenderer.self) + guard let path = mainBundle.path(forResource: "MediaEditorBundle", ofType: "bundle") else { + return + } + guard let bundle = Bundle(path: path) else { + return + } + + guard let defaultLibrary = try? device.makeDefaultLibrary(bundle: bundle) else { + return + } + self.library = defaultLibrary + + self.commandQueue = device.makeCommandQueue() + self.commandQueue?.label = "Media Editor Command Queue" + self.renderPasses.forEach { $0.setup(device: device, library: defaultLibrary) } + self.outputRenderPass.setup(device: device, library: defaultLibrary) + } + + func renderFrame() { + guard let renderTarget = self.renderTarget, + let device = renderTarget.mtlDevice, + let commandQueue = self.commandQueue, + var texture = self.currentTexture else { + return + } + + guard let commandBuffer = commandQueue.makeCommandBuffer() else { + return + } + + var rotation: TextureRotation = self.currentRotation + for renderPass in self.renderPasses { + if let nextTexture = renderPass.process(input: texture, rotation: rotation, device: device, commandBuffer: commandBuffer) { + if nextTexture !== texture { + rotation = .rotate0Degrees + } + texture = nextTexture + } + } + let _ = self.outputRenderPass.process(input: texture, rotation: rotation, device: device, commandBuffer: commandBuffer) + self.finalTexture = texture + + commandBuffer.addCompletedHandler { [weak self] _ in + self?.semaphore.signal() + } + commandBuffer.commit() + } + + func consumeTexture(_ texture: MTLTexture, rotation: TextureRotation) { + self.semaphore.wait() + + self.currentTexture = texture + self.currentRotation = rotation + self.renderTarget?.scheduleFrame() + } + + func renderTargetDidChange(_ target: RenderTarget?) { + self.renderTarget = target + self.setup() + } + + func renderTargetDrawableSizeDidChange(_ size: CGSize) { + self.renderTarget?.scheduleFrame() + } + + func finalRenderedImage() -> UIImage? { + if let finalTexture = self.finalTexture { + return getTextureImage(finalTexture) + } else { + return nil + } + } + + private func getTextureImage(_ texture: MTLTexture) -> UIImage? { + guard let device = self.renderTarget?.mtlDevice else { + return nil + } + let context = CIContext(mtlDevice: device) + guard var ciImage = CIImage(mtlTexture: texture) else { + return nil + } + let transform = CGAffineTransform(1.0, 0.0, 0.0, -1.0, 0.0, ciImage.extent.height) + ciImage = ciImage.transformed(by: transform) + guard let cgImage = context.createCGImage(ciImage, from: CGRect(origin: .zero, size: CGSize(width: ciImage.extent.width, height: ciImage.extent.height))) else { + return nil + } + return UIImage(cgImage: cgImage) + } +} diff --git a/submodules/TelegramUI/Components/MediaEditor/Sources/MediaEditorValues.swift b/submodules/TelegramUI/Components/MediaEditor/Sources/MediaEditorValues.swift new file mode 100644 index 0000000000..dbc172a0ac --- /dev/null +++ b/submodules/TelegramUI/Components/MediaEditor/Sources/MediaEditorValues.swift @@ -0,0 +1,495 @@ +import Foundation +import UIKit +import Display +import TelegramCore + +public enum EditorToolKey { + case enhance + case brightness + case contrast + case saturation + case warmth + case fade + case highlights + case shadows + case vignette + case grain + case sharpen + case shadowsTint + case highlightsTint + case blur + case curves +} + +public struct TintValue: Equatable { + public static let initial = TintValue( + color: .clear, + intensity: 0.5 + ) + + public let color: UIColor + public let intensity: Float + + public init( + color: UIColor, + intensity: Float + ) { + self.color = color + self.intensity = intensity + } + + public func withUpdatedColor(_ color: UIColor) -> TintValue { + return TintValue(color: color, intensity: self.intensity) + } + + public func withUpdatedIntensity(_ intensity: Float) -> TintValue { + return TintValue(color: self.color, intensity: intensity) + } +} + +public struct BlurValue: Equatable { + public static let initial = BlurValue( + mode: .off, + intensity: 0.5, + position: CGPoint(x: 0.5, y: 0.5), + size: 0.24, + falloff: 0.12, + rotation: 0.0 + ) + + public enum Mode: Equatable { + case off + case radial + case linear + case portrait + } + + public let mode: Mode + public let intensity: Float + public let position: CGPoint + public let size: Float + public let falloff: Float + public let rotation: Float + + public init( + mode: Mode, + intensity: Float, + position: CGPoint, + size: Float, + falloff: Float, + rotation: Float + ) { + self.mode = mode + self.intensity = intensity + self.position = position + self.size = size + self.falloff = falloff + self.rotation = rotation + } + + public func withUpdatedMode(_ mode: Mode) -> BlurValue { + return BlurValue( + mode: mode, + intensity: self.intensity, + position: self.position, + size: self.size, + falloff: self.falloff, + rotation: self.rotation + ) + } + + public func withUpdatedIntensity(_ intensity: Float) -> BlurValue { + return BlurValue( + mode: self.mode, + intensity: intensity, + position: self.position, + size: self.size, + falloff: self.falloff, + rotation: self.rotation + ) + } + + public func withUpdatedPosition(_ position: CGPoint) -> BlurValue { + return BlurValue( + mode: self.mode, + intensity: self.intensity, + position: position, + size: self.size, + falloff: self.falloff, + rotation: self.rotation + ) + } + + public func withUpdatedSize(_ size: Float) -> BlurValue { + return BlurValue( + mode: self.mode, + intensity: self.intensity, + position: self.position, + size: size, + falloff: self.falloff, + rotation: self.rotation + ) + } + + public func withUpdatedFalloff(_ falloff: Float) -> BlurValue { + return BlurValue( + mode: self.mode, + intensity: self.intensity, + position: self.position, + size: self.size, + falloff: falloff, + rotation: self.rotation + ) + } + + public func withUpdatedRotation(_ rotation: Float) -> BlurValue { + return BlurValue( + mode: self.mode, + intensity: self.intensity, + position: self.position, + size: self.size, + falloff: self.falloff, + rotation: rotation + ) + } +} + +public struct CurvesValue: Equatable { + public struct CurveValue: Equatable { + public static let initial = CurveValue( + blacks: 0.0, + shadows: 0.25, + midtones: 0.5, + highlights: 0.75, + whites: 1.0 + ) + + public let blacks: Float + public let shadows: Float + public let midtones: Float + public let highlights: Float + public let whites: Float + + lazy var dataPoints: [Float] = { + let points: [Float] = [ + self.blacks, + self.blacks, + self.shadows, + self.midtones, + self.highlights, + self.whites, + self.whites + ] + + let (_, dataPoints) = curveThroughPoints( + count: points.count, + valueAtIndex: { index in + return points[index] + }, + positionAtIndex: { index, _ in + switch index { + case 0: + return -0.001 + case 1: + return 0.0 + case 2: + return 0.25 + case 3: + return 0.5 + case 4: + return 0.75 + case 5: + return 1.0 + default: + return 1.001 + } + }, + size: CGSize(width: 1.0, height: 1.0), + type: .line, + granularity: 100 + ) + return dataPoints + }() + + public init( + blacks: Float, + shadows: Float, + midtones: Float, + highlights: Float, + whites: Float + ) { + self.blacks = blacks + self.shadows = shadows + self.midtones = midtones + self.highlights = highlights + self.whites = whites + } + + public func withUpdatedBlacks(_ blacks: Float) -> CurveValue { + return CurveValue(blacks: blacks, shadows: self.shadows, midtones: self.midtones, highlights: self.highlights, whites: self.whites) + } + + public func withUpdatedShadows(_ shadows: Float) -> CurveValue { + return CurveValue(blacks: self.blacks, shadows: shadows, midtones: self.midtones, highlights: self.highlights, whites: self.whites) + } + + public func withUpdatedMidtones(_ midtones: Float) -> CurveValue { + return CurveValue(blacks: self.blacks, shadows: self.shadows, midtones: midtones, highlights: self.highlights, whites: self.whites) + } + + public func withUpdatedHighlights(_ highlights: Float) -> CurveValue { + return CurveValue(blacks: self.blacks, shadows: self.shadows, midtones: self.midtones, highlights: highlights, whites: self.whites) + } + + public func withUpdatedWhites(_ whites: Float) -> CurveValue { + return CurveValue(blacks: self.blacks, shadows: self.shadows, midtones: self.midtones, highlights: self.highlights, whites: whites) + } + } + + public static let initial = CurvesValue( + all: CurveValue.initial, + red: CurveValue.initial, + green: CurveValue.initial, + blue: CurveValue.initial + ) + + public var all: CurveValue + public var red: CurveValue + public var green: CurveValue + public var blue: CurveValue + + public init( + all: CurveValue, + red: CurveValue, + green: CurveValue, + blue: CurveValue + ) { + self.all = all + self.red = red + self.green = green + self.blue = blue + } + + public func withUpdatedAll(_ all: CurveValue) -> CurvesValue { + return CurvesValue(all: all, red: self.red, green: self.green, blue: self.blue) + } + + public func withUpdatedRed(_ red: CurveValue) -> CurvesValue { + return CurvesValue(all: self.all, red: red, green: self.green, blue: self.blue) + } + + public func withUpdatedGreen(_ green: CurveValue) -> CurvesValue { + return CurvesValue(all: self.all, red: self.red, green: green, blue: self.blue) + } + + public func withUpdatedBlue(_ blue: CurveValue) -> CurvesValue { + return CurvesValue(all: self.all, red: self.red, green: self.green, blue: blue) + } +} + +public class MediaEditorValues { + let originalDimensions: PixelDimensions + let cropOffset: CGPoint + let cropSize: CGSize? + let cropScale: CGFloat + let cropRotation: CGFloat + let cropMirroring: Bool + + let videoTrimRange: Range? + + let drawing: UIImage? + let toolValues: [EditorToolKey: Any] + + init(originalDimensions: PixelDimensions, cropOffset: CGPoint, cropSize: CGSize?, cropScale: CGFloat, cropRotation: CGFloat, cropMirroring: Bool, videoTrimRange: Range?, drawing: UIImage?, toolValues: [EditorToolKey: Any]) { + self.originalDimensions = originalDimensions + self.cropOffset = cropOffset + self.cropSize = cropSize + self.cropScale = cropScale + self.cropRotation = cropRotation + self.cropMirroring = cropMirroring + self.videoTrimRange = videoTrimRange + self.drawing = drawing + self.toolValues = toolValues + } + + func withUpdatedToolValues(_ toolValues: [EditorToolKey: Any]) -> MediaEditorValues { + return MediaEditorValues(originalDimensions: self.originalDimensions, cropOffset: self.cropOffset, cropSize: self.cropSize, cropScale: self.cropScale, cropRotation: self.cropRotation, cropMirroring: self.cropMirroring, videoTrimRange: self.videoTrimRange, drawing: self.drawing, toolValues: toolValues) + } +} + +public class MediaEditorHistogram: Equatable { + public class HistogramBins: Equatable { + public static func == (lhs: HistogramBins, rhs: HistogramBins) -> Bool { + if lhs.count != rhs.count { + return false + } + if lhs.max != rhs.max { + return false + } + if lhs.values != rhs.values { + return false + } + return true + } + + let values: [UInt32] + let max: UInt32 + + public var count: Int { + return self.values.count + } + + init(values: [UInt32], max: UInt32) { + self.values = values + self.max = max + } + + public func valueAtIndex(_ index: Int, mirrored: Bool = false) -> Float { + if index >= 0 && index < values.count, self.max > 0 { + let value = Float(self.values[index]) / Float(self.max) + return mirrored ? 1.0 - value : value + } else { + return 0.0 + } + } + } + + public static func == (lhs: MediaEditorHistogram, rhs: MediaEditorHistogram) -> Bool { + if lhs.luminance != rhs.luminance { + return false + } + if lhs.red != rhs.red { + return false + } + if lhs.green != rhs.green { + return false + } + if lhs.blue != rhs.blue { + return false + } + return true + } + + public let luminance: HistogramBins + public let red: HistogramBins + public let green: HistogramBins + public let blue: HistogramBins + + public init(data: Data) { + let count = 256 + + var maxRed: UInt32 = 0 + var redValues: [UInt32] = [] + var maxGreen: UInt32 = 0 + var greenValues: [UInt32] = [] + var maxBlue: UInt32 = 0 + var blueValues: [UInt32] = [] + + data.withUnsafeBytes { pointer in + if let red = pointer.baseAddress?.assumingMemoryBound(to: UInt32.self) { + for i in 0 ..< count { + redValues.append(red[i]) + if red[i] > maxRed { + maxRed = red[i] + } + } + } + + if let green = pointer.baseAddress?.assumingMemoryBound(to: UInt32.self).advanced(by: count) { + for i in 0 ..< count { + greenValues.append(green[i]) + if green[i] > maxGreen { + maxGreen = green[i] + } + } + } + + if let blue = pointer.baseAddress?.assumingMemoryBound(to: UInt32.self).advanced(by: count * 2) { + for i in 0 ..< count { + blueValues.append(blue[i]) + if blue[i] > maxBlue { + maxBlue = blue[i] + } + } + } + } + + self.luminance = HistogramBins(values: [], max: 0) + self.red = HistogramBins(values: redValues, max: maxRed) + self.green = HistogramBins(values: greenValues, max: maxGreen) + self.blue = HistogramBins(values: blueValues, max: maxBlue) + } + + init( + luminance: HistogramBins, + red: HistogramBins, + green: HistogramBins, + blue: HistogramBins + ) { + self.luminance = luminance + self.red = red + self.green = green + self.blue = blue + } +} + +public enum MediaEditorCurveType { + case filled + case line +} + +public func curveThroughPoints(count: Int, valueAtIndex: (Int) -> Float, positionAtIndex: (Int, CGFloat) -> CGFloat, size: CGSize, type: MediaEditorCurveType, granularity: Int) -> (UIBezierPath, [Float]) { + let path = UIBezierPath() + var dataPoints: [Float] = [] + + let firstValue = valueAtIndex(0) + switch type { + case .filled: + path.move(to: CGPoint(x: -1.0, y: size.height)) + path.addLine(to: CGPoint(x: -1.0, y: CGFloat(firstValue) * size.height)) + case .line: + path.move(to: CGPoint(x: -1.0, y: CGFloat(firstValue) * size.height)) + } + + let step = size.width / CGFloat(count) + func pointAtIndex(_ index: Int) -> CGPoint { + return CGPoint(x: floorToScreenPixels(positionAtIndex(index, step)), y: floorToScreenPixels(CGFloat(valueAtIndex(index)) * size.height)) + } + + for index in 1 ..< count - 2 { + let point0 = pointAtIndex(index - 1) + let point1 = pointAtIndex(index) + let point2 = pointAtIndex(index + 1) + let point3 = pointAtIndex(index + 2) + + for j in 1 ..< granularity { + let t = CGFloat(j) * (1.0 / CGFloat(granularity)) + let tt = t * t + let ttt = tt * t + + var point = CGPoint( + x: 0.5 * (2 * point1.x + (point2.x - point0.x) * t + (2 * point0.x - 5 * point1.x + 4 * point2.x - point3.x) * tt + (3 * point1.x - point0.x - 3 * point2.x + point3.x) * ttt), + y: 0.5 * (2 * point1.y + (point2.y - point0.y) * t + (2 * point0.y - 5 * point1.y + 4 * point2.y - point3.y) * tt + (3 * point1.y - point0.y - 3 * point2.y + point3.y) * ttt) + ) + point.y = max(0.0, min(size.height, point.y)) + if point.x > point0.x { + path.addLine(to: point) + } + + if ((index - 1) % 2 == 0) { + dataPoints.append(Float(point.y)) + } + } + path.addLine(to: point2) + } + + let lastValue = valueAtIndex(count - 1) + path.addLine(to: CGPoint(x: size.width + 1.0, y: CGFloat(lastValue) * size.height)) + + if case .filled = type { + path.addLine(to: CGPoint(x: size.width + 1.0, y: size.height)) + path.close() + } + + return (path, dataPoints) +} diff --git a/submodules/TelegramUI/Components/MediaEditor/Sources/RenderPass.swift b/submodules/TelegramUI/Components/MediaEditor/Sources/RenderPass.swift new file mode 100644 index 0000000000..7301dabbd2 --- /dev/null +++ b/submodules/TelegramUI/Components/MediaEditor/Sources/RenderPass.swift @@ -0,0 +1,178 @@ +import Foundation +import Metal +import simd + +fileprivate struct VertexData { + let pos: simd_float4 + let texCoord: simd_float2 +} + +enum TextureRotation: Int { + case rotate0Degrees + case rotate90Degrees + case rotate180Degrees + case rotate270Degrees +} + +private func verticesDataForRotation(_ rotation: TextureRotation) -> [VertexData] { + let topLeft: simd_float2 + let topRight: simd_float2 + let bottomLeft: simd_float2 + let bottomRight: simd_float2 + + switch rotation { + case .rotate0Degrees: + topLeft = simd_float2(0.0, 1.0) + topRight = simd_float2(1.0, 1.0) + bottomLeft = simd_float2(0.0, 0.0) + bottomRight = simd_float2(1.0, 0.0) + case .rotate180Degrees: + topLeft = simd_float2(1.0, 0.0) + topRight = simd_float2(0.0, 0.0) + bottomLeft = simd_float2(1.0, 1.0) + bottomRight = simd_float2(0.0, 1.0) + case .rotate90Degrees: + topLeft = simd_float2(1.0, 1.0) + topRight = simd_float2(1.0, 0.0) + bottomLeft = simd_float2(0.0, 1.0) + bottomRight = simd_float2(0.0, 0.0) + case .rotate270Degrees: + topLeft = simd_float2(0.0, 0.0) + topRight = simd_float2(0.0, 1.0) + bottomLeft = simd_float2(1.0, 0.0) + bottomRight = simd_float2(1.0, 1.0) + } + + return [ + VertexData( + pos: simd_float4(x: -1, y: -1, z: 0, w: 1), + texCoord: topLeft + ), + VertexData( + pos: simd_float4(x: 1, y: -1, z: 0, w: 1), + texCoord: topRight + ), + VertexData( + pos: simd_float4(x: -1, y: 1, z: 0, w: 1), + texCoord: bottomLeft + ), + VertexData( + pos: simd_float4(x: 1, y: 1, z: 0, w: 1), + texCoord: bottomRight + ), + ] +} + +func textureDimensionsForRotation(texture: MTLTexture, rotation: TextureRotation) -> (width: Int, height: Int) { + switch rotation { + case .rotate90Degrees, .rotate270Degrees: + return (texture.height, texture.width) + default: + return (texture.width, texture.height) + } +} + +class DefaultRenderPass: RenderPass { + fileprivate var pipelineState: MTLRenderPipelineState? + fileprivate var verticesBuffer: MTLBuffer? + fileprivate var textureRotation: TextureRotation = .rotate0Degrees + + var vertexShaderFunctionName: String { + return "defaultVertexShader" + } + + var fragmentShaderFunctionName: String { + return "defaultFragmentShader" + } + + var pixelFormat: MTLPixelFormat { + return .bgra8Unorm + } + + func setup(device: MTLDevice, library: MTLLibrary) { + let pipelineDescriptor = MTLRenderPipelineDescriptor() + pipelineDescriptor.vertexFunction = library.makeFunction(name: self.vertexShaderFunctionName) + pipelineDescriptor.fragmentFunction = library.makeFunction(name: self.fragmentShaderFunctionName) + pipelineDescriptor.colorAttachments[0].pixelFormat = self.pixelFormat + + do { + self.pipelineState = try device.makeRenderPipelineState(descriptor: pipelineDescriptor) + } catch { + print(error.localizedDescription) + } + } + + func setupVerticesBuffer(device: MTLDevice, rotation: TextureRotation) { + if self.verticesBuffer == nil || rotation != self.textureRotation { + self.textureRotation = rotation + let vertices = verticesDataForRotation(rotation) + self.verticesBuffer = device.makeBuffer( + bytes: vertices, + length: MemoryLayout.stride * vertices.count, + options: []) + } + } + + func process(input: MTLTexture, rotation: TextureRotation, device: MTLDevice, commandBuffer: MTLCommandBuffer) -> MTLTexture? { + self.setupVerticesBuffer(device: device, rotation: rotation) + return nil + } + + func encodeDefaultCommands(using encoder: MTLRenderCommandEncoder) { + guard let pipelineState = self.pipelineState, let verticesBuffer = self.verticesBuffer else { + return + } + encoder.setRenderPipelineState(pipelineState) + encoder.setVertexBuffer(verticesBuffer, offset: 0, index: 0) + encoder.drawPrimitives(type: .triangleStrip, vertexStart: 0, vertexCount: 4) + } +} + +final class OutputRenderPass: DefaultRenderPass { + weak var renderTarget: RenderTarget? + + override func process(input: MTLTexture, rotation: TextureRotation, device: MTLDevice, commandBuffer: MTLCommandBuffer) -> MTLTexture? { + guard let renderTarget = self.renderTarget, let renderPassDescriptor = renderTarget.renderPassDescriptor else { + return nil + } + self.setupVerticesBuffer(device: device, rotation: rotation) + + let drawableSize = renderTarget.drawableSize + + let renderCommandEncoder = commandBuffer.makeRenderCommandEncoder( + descriptor: renderPassDescriptor)! + + renderCommandEncoder.setViewport(MTLViewport( + originX: 0.0, originY: 0.0, + width: Double(drawableSize.width), height: Double(drawableSize.height), + znear: -1.0, zfar: 1.0)) + + do { + var texCoordScales = simd_float2(x: 1.0, y: 1.0) + var scaleFactor = drawableSize.width / CGFloat(input.width) + let textureFitHeight = CGFloat(input.height) * scaleFactor + if textureFitHeight > drawableSize.height { + scaleFactor = drawableSize.height / CGFloat(input.height) + let textureFitWidth = CGFloat(input.width) * scaleFactor + let texCoordsScaleX = textureFitWidth / drawableSize.width + texCoordScales.x = Float(texCoordsScaleX) + } else { + let texCoordsScaleY = textureFitHeight / drawableSize.height + texCoordScales.y = Float(texCoordsScaleY) + } + + renderCommandEncoder.setFragmentBytes(&texCoordScales, length: MemoryLayout.stride, index: 0) + renderCommandEncoder.setFragmentTexture(input, index: 0) + } + + self.encodeDefaultCommands(using: renderCommandEncoder) + + renderCommandEncoder.endEncoding() + + if let drawable = renderTarget.drawable { + commandBuffer.present(drawable) + } + + return nil + } +} diff --git a/submodules/TelegramUI/Components/MediaEditor/Sources/SharpenRenderPass.swift b/submodules/TelegramUI/Components/MediaEditor/Sources/SharpenRenderPass.swift new file mode 100644 index 0000000000..07e0e8e651 --- /dev/null +++ b/submodules/TelegramUI/Components/MediaEditor/Sources/SharpenRenderPass.swift @@ -0,0 +1,17 @@ +import Foundation +import Metal +import simd + +final class SharpenRenderPass: RenderPass { + fileprivate var cachedTexture: MTLTexture? + + var value: simd_float1 = 0.0 + + func setup(device: MTLDevice, library: MTLLibrary) { + + } + + func process(input: MTLTexture, rotation: TextureRotation, device: MTLDevice, commandBuffer: MTLCommandBuffer) -> MTLTexture? { + return input + } +} diff --git a/submodules/TelegramUI/Components/MediaEditor/Sources/VideoTextureSource.swift b/submodules/TelegramUI/Components/MediaEditor/Sources/VideoTextureSource.swift new file mode 100644 index 0000000000..d735506a46 --- /dev/null +++ b/submodules/TelegramUI/Components/MediaEditor/Sources/VideoTextureSource.swift @@ -0,0 +1,204 @@ +import Foundation +import AVFoundation +import Metal +import MetalKit + +final class VideoTextureSource: NSObject, TextureSource, AVPlayerItemOutputPullDelegate { + private let player: AVPlayer + private var playerItem: AVPlayerItem? + private var playerItemOutput: AVPlayerItemVideoOutput? + + private var playerItemStatusObservation: NSKeyValueObservation? + private var playerItemObservation: NSKeyValueObservation? + + private var displayLink: CADisplayLink? + + private var preferredVideoTransform: CGAffineTransform = .identity + + private var forceUpdate: Bool = false + + weak var output: TextureConsumer? + var textureCache: CVMetalTextureCache! + var queue: DispatchQueue! + var started: Bool = false + + init(player: AVPlayer, renderTarget: RenderTarget) { + self.player = player + + if let device = renderTarget.mtlDevice, CVMetalTextureCacheCreate(nil, nil, device, nil, &self.textureCache) != kCVReturnSuccess { + print("error") + } + + self.queue = DispatchQueue( + label: "VideoTextureSource Queue", + qos: .userInteractive, + attributes: [], + autoreleaseFrequency: .workItem, + target: nil) + + super.init() + + self.playerItemObservation = self.player.observe(\.currentItem, options: [.initial, .new], changeHandler: { [weak self] (player, change) in + guard let strongSelf = self, strongSelf.player == player else { + return + } + strongSelf.updatePlayerItem(strongSelf.player.currentItem) + }) + } + + deinit { + print() + } + + private func updatePlayerItem(_ playerItem: AVPlayerItem?) { + self.displayLink?.invalidate() + self.displayLink = nil + if let output = self.playerItemOutput, let item = self.playerItem { + if item.outputs.contains(output) { + item.remove(output) + } + } + self.playerItemOutput = nil + self.playerItemStatusObservation?.invalidate() + self.playerItemStatusObservation = nil + + self.playerItem = playerItem + self.playerItemStatusObservation = self.playerItem?.observe(\.status, options: [.initial,.new], changeHandler: { [weak self] item, change in + guard let strongSelf = self else { + return + } + if strongSelf.playerItem == item, item.status == .readyToPlay { + strongSelf.handleReadyToPlay() + } + }) + } + + private func handleReadyToPlay() { + guard let playerItem = self.playerItem else { + return + } + + var hasVideoTrack: Bool = false + for track in playerItem.asset.tracks { + if track.mediaType == .video { + hasVideoTrack = true + self.preferredVideoTransform = track.preferredTransform + break + } + } + if !hasVideoTrack { + assertionFailure("No video track found.") + return + } + + let output = AVPlayerItemVideoOutput(pixelBufferAttributes: [kCVPixelBufferPixelFormatTypeKey as NSString as String: kCVPixelFormatType_32BGRA]) + output.setDelegate(self, queue: self.queue) + playerItem.add(output) + self.playerItemOutput = output + + self.setupDisplayLink() + } + + private class DisplayLinkTarget { + private let handler: () -> Void + init(_ handler: @escaping () -> Void) { + self.handler = handler + } + @objc func handleDisplayLinkUpdate(sender: CADisplayLink) { + self.handler() + } + } + + private func setupDisplayLink() { + self.displayLink?.invalidate() + self.displayLink = nil + + if self.playerItemOutput != nil { + let displayLink = CADisplayLink(target: DisplayLinkTarget({ [weak self] in + self?.handleUpdate() + }), selector: #selector(DisplayLinkTarget.handleDisplayLinkUpdate(sender:))) + displayLink.preferredFramesPerSecond = 30 + displayLink.add(to: .main, forMode: .common) + self.displayLink = displayLink + } + } + + private func handleUpdate() { + if self.player.rate != 0 { + self.forceUpdate = true + } + self.update(forced: self.forceUpdate) + self.forceUpdate = false + } + + private let advanceInterval: TimeInterval = 1.0 / 60.0 + private func update(forced: Bool) { + guard let output = self.playerItemOutput else { + return + } + + let requestTime = output.itemTime(forHostTime: CACurrentMediaTime()) + if requestTime < .zero { + return + } + + if !forced && !output.hasNewPixelBuffer(forItemTime: requestTime) { + self.displayLink?.isPaused = true + output.requestNotificationOfMediaDataChange(withAdvanceInterval: self.advanceInterval) + return + } + + var presentationTime: CMTime = .zero + if let pixelBuffer = output.copyPixelBuffer(forItemTime: requestTime, itemTimeForDisplay: &presentationTime) { + if let texture = self.pixelBufferToMTLTexture(pixelBuffer: pixelBuffer) { + self.output?.consumeTexture(texture, rotation: .rotate90Degrees) + } +// +// self.handler(VideoFrame(preferredTrackTransform: self.preferredVideoTransform, +// presentationTimestamp: presentationTime, +// playerTimestamp: player.currentTime(), +// pixelBuffer: pixelBuffer)) + } + } + + func setNeedsUpdate() { + self.displayLink?.isPaused = false + self.forceUpdate = true + } + + func updateIfNeeded() { + if self.forceUpdate { + self.update(forced: true) + self.forceUpdate = false + } + } + + func start() { + + } + + func pause() { + + } + + func connect(to consumer: TextureConsumer) { + self.output = consumer + } + + private func pixelBufferToMTLTexture(pixelBuffer: CVPixelBuffer) -> MTLTexture? { + let width = CVPixelBufferGetWidth(pixelBuffer) + let height = CVPixelBufferGetHeight(pixelBuffer) + let format: MTLPixelFormat = .bgra8Unorm + var textureRef : CVMetalTexture? + let status = CVMetalTextureCacheCreateTextureFromImage(nil, self.textureCache, pixelBuffer, nil, format, width, height, 0, &textureRef) + if status == kCVReturnSuccess { + return CVMetalTextureGetTexture(textureRef!) + } + return nil + } + + public func outputMediaDataWillChange(_ sender: AVPlayerItemOutput) { + self.displayLink?.isPaused = false + self.player.play() + } +} diff --git a/submodules/TelegramUI/Components/MediaEditorScreen/BUILD b/submodules/TelegramUI/Components/MediaEditorScreen/BUILD new file mode 100644 index 0000000000..2c499ab040 --- /dev/null +++ b/submodules/TelegramUI/Components/MediaEditorScreen/BUILD @@ -0,0 +1,38 @@ +load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library") + +swift_library( + name = "MediaEditorScreen", + module_name = "MediaEditorScreen", + srcs = glob([ + "Sources/**/*.swift", + ]), + copts = [ + "-warnings-as-errors", + ], + deps = [ + "//submodules/AsyncDisplayKit:AsyncDisplayKit", + "//submodules/Display:Display", + "//submodules/Postbox:Postbox", + "//submodules/TelegramCore:TelegramCore", + "//submodules/SSignalKit/SwiftSignalKit:SwiftSignalKit", + "//submodules/ComponentFlow:ComponentFlow", + "//submodules/Components/ViewControllerComponent:ViewControllerComponent", + "//submodules/Components/ComponentDisplayAdapters:ComponentDisplayAdapters", + "//submodules/Components/MultilineTextComponent:MultilineTextComponent", + "//submodules/TelegramPresentationData:TelegramPresentationData", + "//submodules/AccountContext:AccountContext", + "//submodules/AppBundle:AppBundle", + "//submodules/TelegramStringFormatting:TelegramStringFormatting", + "//submodules/PresentationDataUtils:PresentationDataUtils", + "//submodules/ContextUI", + "//submodules/LegacyComponents:LegacyComponents", + "//submodules/TelegramUI/Components/MediaEditor", + "//submodules/DrawingUI:DrawingUI", + "//submodules/Components/LottieAnimationComponent:LottieAnimationComponent", + "//submodules/TelegramUI/Components/MessageInputPanelComponent", + "//submodules/TelegramUI/Components/ChatEntityKeyboardInputNode", + ], + visibility = [ + "//visibility:public", + ], +) diff --git a/submodules/TelegramUI/Components/MediaEditorScreen/Sources/AdjustmentsComponent.swift b/submodules/TelegramUI/Components/MediaEditorScreen/Sources/AdjustmentsComponent.swift new file mode 100644 index 0000000000..934bda3a10 --- /dev/null +++ b/submodules/TelegramUI/Components/MediaEditorScreen/Sources/AdjustmentsComponent.swift @@ -0,0 +1,275 @@ +import Foundation +import UIKit +import Display +import ComponentFlow +import LegacyComponents +import MediaEditor + +final class AdjustmentSliderComponent: Component { + typealias EnvironmentType = Empty + + let title: String + let value: Float + let minValue: Float + let maxValue: Float + let startValue: Float + let isEnabled: Bool + let trackColor: UIColor? + let updateValue: (Float) -> Void + + init( + title: String, + value: Float, + minValue: Float, + maxValue: Float, + startValue: Float, + isEnabled: Bool, + trackColor: UIColor?, + updateValue: @escaping (Float) -> Void + ) { + self.title = title + self.value = value + self.minValue = minValue + self.maxValue = maxValue + self.startValue = startValue + self.isEnabled = isEnabled + self.trackColor = trackColor + self.updateValue = updateValue + } + + static func ==(lhs: AdjustmentSliderComponent, rhs: AdjustmentSliderComponent) -> Bool { + if lhs.title != rhs.title { + return false + } + if lhs.value != rhs.value { + return false + } + if lhs.minValue != rhs.minValue { + return false + } + if lhs.maxValue != rhs.maxValue { + return false + } + if lhs.startValue != rhs.startValue { + return false + } + if lhs.isEnabled != rhs.isEnabled { + return false + } + if lhs.trackColor != rhs.trackColor { + return false + } + return true + } + + final class View: UIView, UITextFieldDelegate { + private let title = ComponentView() + private let value = ComponentView() + private var sliderView: TGPhotoEditorSliderView? + + private var component: AdjustmentSliderComponent? + private weak var state: EmptyComponentState? + + override init(frame: CGRect) { + super.init(frame: frame) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + func update(component: AdjustmentSliderComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + self.component = component + self.state = state + + let sliderView: TGPhotoEditorSliderView + if let current = self.sliderView { + sliderView = current + sliderView.value = CGFloat(component.value) + } else { + sliderView = TGPhotoEditorSliderView() + sliderView.backgroundColor = .clear + sliderView.enablePanHandling = true + sliderView.trackCornerRadius = 1.0 + sliderView.lineSize = 2.0 + sliderView.minimumValue = CGFloat(component.minValue) + sliderView.maximumValue = CGFloat(component.maxValue) + sliderView.startValue = CGFloat(component.startValue) + sliderView.value = CGFloat(component.value) + sliderView.disablesInteractiveTransitionGestureRecognizer = true + sliderView.addTarget(self, action: #selector(self.sliderValueChanged), for: .valueChanged) + sliderView.layer.allowsGroupOpacity = true + self.sliderView = sliderView + self.addSubview(sliderView) + } + + if component.isEnabled { + sliderView.alpha = 1.3 + sliderView.trackColor = component.trackColor ?? UIColor(rgb: 0xffffff) + sliderView.isUserInteractionEnabled = true + } else { + sliderView.trackColor = UIColor(rgb: 0xffffff) + sliderView.alpha = 0.3 + sliderView.isUserInteractionEnabled = false + } + + transition.setFrame(view: sliderView, frame: CGRect(origin: CGPoint(x: 22.0, y: 7.0), size: CGSize(width: availableSize.width - 22.0 * 2.0, height: 44.0))) + sliderView.hitTestEdgeInsets = UIEdgeInsets(top: -sliderView.frame.minX, left: 0.0, bottom: 0.0, right: -sliderView.frame.minX) + + let titleSize = self.title.update( + transition: .immediate, + component: AnyComponent( + Text(text: component.title, font: Font.regular(14.0), color: UIColor(rgb: 0x808080)) + ), + environment: {}, + containerSize: CGSize(width: 100.0, height: 100.0) + ) + if let titleView = self.title.view { + if titleView.superview == nil { + self.addSubview(titleView) + } + transition.setFrame(view: titleView, frame: CGRect(origin: CGPoint(x: 21.0, y: 0.0), size: titleSize)) + } + + return CGSize(width: availableSize.width, height: 52.0) + } + + @objc private func sliderValueChanged() { + guard let component = self.component, let sliderView = self.sliderView else { + return + } + component.updateValue(Float(sliderView.value)) + } + } + + public func makeView() -> View { + return View(frame: CGRect()) + } + + public func update(view: View, availableSize: CGSize, state: State, environment: Environment, transition: Transition) -> CGSize { + return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition) + } +} + +struct AdjustmentTool: Equatable { + let key: EditorToolKey + let title: String + let value: Float + let minValue: Float + let maxValue: Float + let startValue: Float +} + +final class AdjustmentsComponent: Component { + typealias EnvironmentType = Empty + + let tools: [AdjustmentTool] + let valueUpdated: (EditorToolKey, Float) -> Void + + init( + tools: [AdjustmentTool], + valueUpdated: @escaping (EditorToolKey, Float) -> Void + ) { + self.tools = tools + self.valueUpdated = valueUpdated + } + + static func ==(lhs: AdjustmentsComponent, rhs: AdjustmentsComponent) -> Bool { + if lhs.tools != rhs.tools { + return false + } + return true + } + + final class View: UIView { + private let scrollView = UIScrollView() + private var toolViews: [ComponentView] = [] + + private var component: AdjustmentsComponent? + private weak var state: EmptyComponentState? + + override init(frame: CGRect) { + self.scrollView.showsVerticalScrollIndicator = false + + super.init(frame: frame) + + self.addSubview(self.scrollView) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + func update(component: AdjustmentsComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + self.component = component + self.state = state + + let valueUpdated = component.valueUpdated + + var sizes: [CGSize] = [] + for i in 0 ..< component.tools.count { + let tool = component.tools[i] + let componentView: ComponentView + if i >= self.toolViews.count { + componentView = ComponentView() + self.toolViews.append(componentView) + } else { + componentView = self.toolViews[i] + } + + let size = componentView.update( + transition: transition, + component: AnyComponent( + AdjustmentSliderComponent( + title: tool.title, + value: tool.value, + minValue: tool.minValue, + maxValue: tool.maxValue, + startValue: tool.startValue, + isEnabled: true, + trackColor: nil, + updateValue: { value in + valueUpdated(tool.key, value) + } + ) + ), + environment: {}, + containerSize: availableSize + ) + sizes.append(size) + } + + var origin: CGPoint = CGPoint(x: 0.0, y: 11.0) + for i in 0 ..< component.tools.count { + let size = sizes[i] + let componentView = self.toolViews[i] + + if let view = componentView.view { + if view.superview == nil { + self.scrollView.addSubview(view) + } + transition.setFrame(view: view, frame: CGRect(origin: origin, size: size)) + } + origin = origin.offsetBy(dx: 0.0, dy: size.height) + } + + let size = CGSize(width: availableSize.width, height: 180.0) + let contentSize = CGSize(width: availableSize.width, height: origin.y) + if contentSize != self.scrollView.contentSize { + self.scrollView.contentSize = contentSize + } + transition.setFrame(view: self.scrollView, frame: CGRect(origin: .zero, size: size)) + + return size + } + } + + public func makeView() -> View { + return View(frame: CGRect()) + } + + public func update(view: View, availableSize: CGSize, state: State, environment: Environment, transition: Transition) -> CGSize { + return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition) + } +} + diff --git a/submodules/TelegramUI/Components/MediaEditorScreen/Sources/BlurComponent.swift b/submodules/TelegramUI/Components/MediaEditorScreen/Sources/BlurComponent.swift new file mode 100644 index 0000000000..ad47244fad --- /dev/null +++ b/submodules/TelegramUI/Components/MediaEditorScreen/Sources/BlurComponent.swift @@ -0,0 +1,814 @@ +import Foundation +import UIKit +import Display +import ComponentFlow +import LegacyComponents +import MediaEditor + +private final class BlurModeComponent: Component { + typealias EnvironmentType = Empty + + let title: String + let icon: UIImage? + let isSelected: Bool + + init( + title: String, + icon: UIImage?, + isSelected: Bool + ) { + self.title = title + self.icon = icon + self.isSelected = isSelected + } + + static func ==(lhs: BlurModeComponent, rhs: BlurModeComponent) -> Bool { + if lhs.title != rhs.title { + return false + } + if lhs.icon !== rhs.icon { + return false + } + if lhs.isSelected != rhs.isSelected { + return false + } + return true + } + + final class View: UIView { + private let icon = ComponentView() + private let title = ComponentView() + + private var component: BlurModeComponent? + private weak var state: EmptyComponentState? + + override init(frame: CGRect) { + super.init(frame: frame) + + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + func update(component: BlurModeComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + self.component = component + self.state = state + + let iconSize = self.icon.update( + transition: transition, + component: AnyComponent( + Image( + image: component.icon, + tintColor: component.isSelected ? UIColor(rgb: 0xf8d74a) : .white, + size: CGSize(width: 30.0, height: 30.0) + ) + ), + environment: {}, + containerSize: availableSize + ) + let titleSize = self.title.update( + transition: transition, + component: AnyComponent( + Text( + text: component.title, + font: Font.regular(14.0), + color: component.isSelected ? UIColor(rgb: 0xf8d74a) : UIColor(rgb: 0x808080) + ) + ), + environment: {}, + containerSize: availableSize + ) + + let spacing: CGFloat = 3.0 + let size = CGSize(width: 66.0, height: iconSize.height + spacing + titleSize.height) + + let iconFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((size.width - iconSize.width) / 2.0), y: 0.0), size: iconSize) + if let view = self.icon.view { + if view.superview == nil { + self.addSubview(view) + } + transition.setFrame(view: view, frame: iconFrame) + } + + let titleFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((size.width - titleSize.width) / 2.0), y: iconSize.height + spacing), size: titleSize) + if let view = self.title.view { + if view.superview == nil { + self.addSubview(view) + } + transition.setFrame(view: view, frame: titleFrame) + } + + return size + } + } + + public func makeView() -> View { + return View(frame: CGRect()) + } + + public func update(view: View, availableSize: CGSize, state: State, environment: Environment, transition: Transition) -> CGSize { + return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition) + } +} + +final class BlurComponent: Component { + typealias EnvironmentType = Empty + + let value: BlurValue + let hasPortrait: Bool + let valueUpdated: (BlurValue) -> Void + + init( + value: BlurValue, + hasPortrait: Bool, + valueUpdated: @escaping (BlurValue) -> Void + ) { + self.value = value + self.hasPortrait = hasPortrait + self.valueUpdated = valueUpdated + } + + static func ==(lhs: BlurComponent, rhs: BlurComponent) -> Bool { + if lhs.value != rhs.value { + return false + } + if lhs.hasPortrait != rhs.hasPortrait { + return false + } + return true + } + + func makeState() -> State { + return State(value: self.value) + } + + final class State: ComponentState { + var value: BlurValue + + init(value: BlurValue) { + self.value = value + } + } + + final class View: UIView { + private let title = ComponentView() + private let offButton = ComponentView() + private let radialButton = ComponentView() + private let linearButton = ComponentView() + private let portraitButton = ComponentView() + + private let slider = ComponentView() + + private var component: BlurComponent? + private weak var state: State? + + private let offImage = UIImage(bundleImageName: "Media Editor/BlurOff") + private let radialImage = UIImage(bundleImageName: "Media Editor/BlurRadial") + private let linearImage = UIImage(bundleImageName: "Media Editor/BlurLinear") + private let portraitImage = UIImage(bundleImageName: "Media Editor/BlurPortrait") + + override init(frame: CGRect) { + super.init(frame: frame) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + func update(component: BlurComponent, availableSize: CGSize, state: State, environment: Environment, transition: Transition) -> CGSize { + self.component = component + self.state = state + state.value = component.value + + let valueUpdated = component.valueUpdated + + let titleSize = self.title.update( + transition: transition, + component: AnyComponent( + Text( + text: "Blur", + font: Font.regular(14.0), + color: UIColor(rgb: 0x808080) + ) + ), + environment: {}, + containerSize: availableSize + ) + let titleFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((availableSize.width - titleSize.width) / 2.0), y: 11.0), size: titleSize) + if let view = self.title.view { + if view.superview == nil { + self.addSubview(view) + } + transition.setFrame(view: view, frame: titleFrame) + } + + let offButtonSize = self.offButton.update( + transition: transition, + component: AnyComponent( + Button( + content: AnyComponent( + BlurModeComponent( + title: "Off", + icon: self.offImage, + isSelected: state.value.mode == .off + ) + ), + action: { [weak state] in + if let state { + valueUpdated(state.value.withUpdatedMode(.off)) + } + } + ) + ), + environment: {}, + containerSize: availableSize + ) + let _ = self.radialButton.update( + transition: transition, + component: AnyComponent( + Button( + content: AnyComponent( + BlurModeComponent( + title: "Radial", + icon: self.radialImage, + isSelected: state.value.mode == .radial + ) + ), + action: { [weak state] in + if let state { + valueUpdated(state.value.withUpdatedMode(.radial)) + } + } + ) + ), + environment: {}, + containerSize: availableSize + ) + let _ = self.linearButton.update( + transition: transition, + component: AnyComponent( + Button( + content: AnyComponent( + BlurModeComponent( + title: "Linear", + icon: self.linearImage, + isSelected: state.value.mode == .linear + ) + ), + action: { [weak state] in + if let state { + valueUpdated(state.value.withUpdatedMode(.linear)) + } + } + ) + ), + environment: {}, + containerSize: availableSize + ) + let _ = self.portraitButton.update( + transition: transition, + component: AnyComponent( + Button( + content: AnyComponent( + BlurModeComponent( + title: "Portrait", + icon: self.portraitImage, + isSelected: state.value.mode == .portrait + ) + ), + action: { [weak state] in + if let state { + valueUpdated(state.value.withUpdatedMode(.portrait)) + } + } + ) + ), + environment: {}, + containerSize: availableSize + ) + + let sliderSize = self.slider.update( + transition: transition, + component: AnyComponent( + AdjustmentSliderComponent( + title: "", + value: state.value.intensity, + minValue: 0.0, + maxValue: 1.0, + startValue: 0.0, + isEnabled: state.value.mode != .off, + trackColor: nil, + updateValue: { [weak state] value in + if let state { + valueUpdated(state.value.withUpdatedIntensity(value)) + } + } + ) + ), + environment: {}, + containerSize: availableSize + ) + + var buttons = [self.offButton, self.radialButton, self.linearButton] + if component.hasPortrait { + buttons.append(self.portraitButton) + } + + let topInset: CGFloat = 34.0 + let horizontalSpacing: CGFloat = 24.0 + let width: CGFloat = CGFloat(buttons.count) * offButtonSize.width + (CGFloat(buttons.count - 1) * horizontalSpacing) + let commonX = floorToScreenPixels((availableSize.width - width) / 2.0) + var offsetX: CGFloat = commonX + for button in buttons { + if let view = button.view { + if view.superview == nil { + self.addSubview(view) + } + transition.setFrame(view: view, frame: CGRect(origin: CGPoint(x: offsetX, y: topInset), size: offButtonSize)) + } + offsetX += offButtonSize.width + horizontalSpacing + } + + let verticalSpacing: CGFloat = -5.0 + let sliderFrame = CGRect(origin: CGPoint(x: 0.0, y: topInset + offButtonSize.height + verticalSpacing), size: sliderSize) + if let view = self.slider.view { + if view.superview == nil { + self.addSubview(view) + } + transition.setFrame(view: view, frame: sliderFrame) + } + + return CGSize(width: availableSize.width, height: topInset + offButtonSize.height + verticalSpacing + sliderSize.height + 6.0) + } + } + + public func makeView() -> View { + return View(frame: CGRect()) + } + + public func update(view: View, availableSize: CGSize, state: State, environment: Environment, transition: Transition) -> CGSize { + return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition) + } +} + +private let blurInsetProximity: CGFloat = 20.0 +private let blurMinimumFalloff: Float = 0.1 +private let blurMinimumDifference: Float = 0.02 +private let blurViewCenterInset: CGFloat = 30.0 +private let blurViewRadiusInset: CGFloat = 30.0 + +final class BlurScreenComponent: Component { + typealias EnvironmentType = Empty + + let value: BlurValue + let valueUpdated: (BlurValue) -> Void + + init( + value: BlurValue, + valueUpdated: @escaping (BlurValue) -> Void + + ) { + self.value = value + self.valueUpdated = valueUpdated + } + + static func ==(lhs: BlurScreenComponent, rhs: BlurScreenComponent) -> Bool { + if lhs.value != rhs.value { + return false + } + return true + } + + final class View: UIView, UIGestureRecognizerDelegate { + enum Control { + case center + case innerRadius + case outerRadius + case rotation + case wholeArea + } + private var component: BlurScreenComponent? + private weak var state: EmptyComponentState? + + override init(frame: CGRect) { + super.init(frame: frame) + + self.backgroundColor = .clear + self.contentMode = .redraw + self.isOpaque = false + + let panGestureRecognizer = UIPanGestureRecognizer(target: self, action: #selector(self.handlePan(_:))) + panGestureRecognizer.delegate = self + self.addGestureRecognizer(panGestureRecognizer) + + let pinchGestureRecognizer = UIPinchGestureRecognizer(target: self, action: #selector(self.handlePinch(_:))) + pinchGestureRecognizer.delegate = self + self.addGestureRecognizer(pinchGestureRecognizer) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + private var activeControl: Control? + private var startCenterPoint: CGPoint? + private var startDistance: CGFloat? + private var startRadius: CGFloat? + @objc func handlePan(_ gestureRecognizer: UIPanGestureRecognizer) { + guard let component = self.component else { + return + } + + let location = gestureRecognizer.location(in: gestureRecognizer.view) + let centerPoint = CGPoint( + x: component.value.position.x * self.frame.width, + y: component.value.position.y * self.frame.height + ) + let delta = CGPoint(x: location.x - centerPoint.x, y: location.y - centerPoint.y) + let shorterSide = min(self.frame.width, self.frame.height) + let innerRadius = shorterSide * CGFloat(component.value.falloff) + let outerRadius = shorterSide * CGFloat(component.value.size) + + switch gestureRecognizer.state { + case .began: + switch component.value.mode { + case .radial: + let distance = sqrt(delta.x * delta.x + delta.y * delta.y) + + let close = abs(outerRadius - innerRadius) < blurInsetProximity + let innerRadiusOuterInset = close ? 0 : blurViewRadiusInset + let outerRadiusInnerInset = close ? 0 : blurViewRadiusInset + + if distance < blurViewCenterInset { + self.activeControl = .center + self.startCenterPoint = centerPoint + } + else if distance > innerRadius - blurViewRadiusInset && distance < innerRadius + innerRadiusOuterInset { + self.activeControl = .innerRadius + self.startDistance = distance + self.startRadius = innerRadius + } else if distance > outerRadius - outerRadiusInnerInset && distance < outerRadius + blurViewRadiusInset { + self.activeControl = .outerRadius + self.startDistance = distance + self.startRadius = outerRadius + } + case .linear: + let radialDistance = sqrt(delta.x * delta.x + delta.y * delta.y) + let distance = abs(delta.x * cos(CGFloat(component.value.rotation) + .pi / 2.0) + delta.y * sin(CGFloat(component.value.rotation) + .pi / 2.0)) + + let close = abs(outerRadius - innerRadius) < blurInsetProximity + let innerRadiusOuterInset = close ? 0 : blurViewRadiusInset + let outerRadiusInnerInset = close ? 0 : blurViewRadiusInset + + if radialDistance < blurViewCenterInset { + self.activeControl = .center + self.startCenterPoint = centerPoint + } else if distance > innerRadius - blurViewRadiusInset && distance < innerRadius + innerRadiusOuterInset { + self.activeControl = .innerRadius + self.startDistance = distance + self.startRadius = innerRadius + } else if distance > outerRadius - outerRadiusInnerInset && distance < outerRadius + blurViewRadiusInset { + self.activeControl = .outerRadius; + self.startDistance = distance + self.startRadius = outerRadius + } else if distance <= innerRadius - blurViewRadiusInset || distance >= outerRadius + blurViewRadiusInset { + self.activeControl = .rotation + } + default: + break + } + case .changed: + switch component.value.mode { + case .radial: + guard let activeControl = self.activeControl else { + return + } + let distance = sqrt(delta.x * delta.x + delta.y * delta.y) + + switch activeControl { + case .center: + guard let startCenterPoint = self.startCenterPoint else { + return + } + let translation = gestureRecognizer.translation(in: gestureRecognizer.view) + let centerPoint = CGPoint( + x: max(0.0, min(self.frame.width, startCenterPoint.x + translation.x)), + y: max(0.0, min(self.frame.height, startCenterPoint.y + translation.y)) + ) + let position = CGPoint( + x: centerPoint.x / self.frame.width, + y: centerPoint.y / self.frame.height + ) + component.valueUpdated(component.value.withUpdatedPosition(position)) + case .innerRadius: + guard let startDistance = self.startDistance, let startRadius = self.startRadius else { + return + } + let delta = distance - startDistance + let falloff = min(max(blurMinimumFalloff, Float((startRadius + delta) / shorterSide)), component.value.size - blurMinimumDifference) + component.valueUpdated(component.value.withUpdatedFalloff(falloff)) + case .outerRadius: + guard let startDistance = self.startDistance, let startRadius = self.startRadius else { + return + } + let delta = distance - startDistance + let size = max(component.value.falloff + blurMinimumDifference, Float((startRadius + delta) / shorterSide)) + component.valueUpdated(component.value.withUpdatedSize(size)) + default: + break + } + case .linear: + guard let activeControl = self.activeControl else { + return + } + let distance = sqrt(delta.x * delta.x + delta.y * delta.y) + + switch activeControl { + case .center: + guard let startCenterPoint = self.startCenterPoint else { + return + } + let translation = gestureRecognizer.translation(in: gestureRecognizer.view) + let centerPoint = CGPoint( + x: max(0.0, min(self.frame.width, startCenterPoint.x + translation.x)), + y: max(0.0, min(self.frame.height, startCenterPoint.y + translation.y)) + ) + let position = CGPoint( + x: centerPoint.x / self.frame.width, + y: centerPoint.y / self.frame.height + ) + component.valueUpdated(component.value.withUpdatedPosition(position)) + case .innerRadius: + guard let startDistance = self.startDistance, let startRadius = self.startRadius else { + return + } + let delta = distance - startDistance + let falloff = min(max(blurMinimumFalloff, Float((startRadius + delta) / shorterSide)), component.value.size - blurMinimumDifference) + component.valueUpdated(component.value.withUpdatedFalloff(falloff)) + case .outerRadius: + guard let startDistance = self.startDistance, let startRadius = self.startRadius else { + return + } + let delta = distance - startDistance + let size = max(component.value.falloff + blurMinimumDifference, Float((startRadius + delta) / shorterSide)) + component.valueUpdated(component.value.withUpdatedSize(size)) + case .rotation: + let translation = gestureRecognizer.translation(in: gestureRecognizer.view) + var clockwise = false + let right = location.x > centerPoint.x + let bottom = location.y > centerPoint.y + + if !right && !bottom { + if abs(translation.y) > abs(translation.x) { + if translation.y < 0.0 { + clockwise = true + } + } else { + if translation.x > 0.0 { + clockwise = true + } + } + } else if right && !bottom { + if abs(translation.y) > abs(translation.x) { + if translation.y > 0.0 { + clockwise = true + } + } else + { + if translation.x > 0.0 { + clockwise = true + } + } + } else if right && bottom { + if abs(translation.y) > abs(translation.x) { + if translation.y > 0 { + clockwise = true + } + } else { + if translation.x < 0 { + clockwise = true + } + } + } else { + if abs(translation.y) > abs(translation.x) { + if translation.y < 0 { + clockwise = true + } + } else { + if translation.x < 0 { + clockwise = true + } + } + } + + let delta = sqrt(translation.x * translation.x + translation.y * translation.y) + + let angleInDegress = radiansToDegrees(radians: CGFloat(component.value.rotation)) + let updatedAngle = angleInDegress + delta * (clockwise ? 1.0 : -1.0) / .pi / 1.15 + component.valueUpdated(component.value.withUpdatedRotation(Float(degreesToRadians(degrees: updatedAngle)))) + + gestureRecognizer.setTranslation(.zero, in: gestureRecognizer.view) + default: + break + } + default: + break + } + default: + self.activeControl = nil + self.startCenterPoint = nil + self.startDistance = nil + self.startRadius = nil + } + } + + @objc func handlePinch(_ gestureRecognizer: UIPinchGestureRecognizer) { + guard let component = self.component else { + return + } + switch gestureRecognizer.state { + case .began: + self.activeControl = .wholeArea + case .changed: + let scale = Float(gestureRecognizer.scale) + + let size = max(component.value.falloff + blurMinimumDifference, component.value.size * scale) + let falloff = max(blurMinimumFalloff, component.value.falloff * scale) + component.valueUpdated(component.value.withUpdatedSize(size).withUpdatedFalloff(falloff)) + + gestureRecognizer.scale = 1.0 + default: + self.activeControl = nil + } + } + + override func gestureRecognizerShouldBegin(_ gestureRecognizer: UIGestureRecognizer) -> Bool { + guard let component = self.component else { + return false + } + + let location = gestureRecognizer.location(in: gestureRecognizer.view) + let centerPoint = CGPoint( + x: component.value.position.x * self.frame.width, + y: component.value.position.y * self.frame.height + ) + let delta = CGPoint(x: location.x - centerPoint.x, y: location.y - centerPoint.y) + let innerRadius = min(self.frame.width, self.frame.height) * CGFloat(component.value.falloff) + let outerRadius = min(self.frame.width, self.frame.height) * CGFloat(component.value.size) + + switch component.value.mode { + case .radial: + let distance = sqrt(delta.x * delta.x + delta.y * delta.y) + + let close = abs(outerRadius - innerRadius) < blurInsetProximity + let innerRadiusOuterInset = close ? 0.0 : blurViewRadiusInset + let outerRadiusInnerInset = close ? 0.0 : blurViewRadiusInset + + if distance < blurViewCenterInset && gestureRecognizer is UIPanGestureRecognizer { + return true + } else if distance > innerRadius - blurViewRadiusInset && distance < innerRadius + innerRadiusOuterInset { + return true + } else if distance > outerRadius - outerRadiusInnerInset && distance < outerRadius + blurViewRadiusInset { + return true + } + case .linear: + let radialDistance = sqrt(delta.x * delta.x + delta.y * delta.y) + let distance = abs(delta.x * cos(CGFloat(component.value.rotation) + .pi / 2.0) + delta.y * sin(CGFloat(component.value.rotation) + .pi / 2.0)) + + let close = abs(outerRadius - innerRadius) < blurInsetProximity + let innerRadiusOuterInset = close ? 0.0 : blurViewRadiusInset + let outerRadiusInnerInset = close ? 0.0 : blurViewRadiusInset + + if radialDistance < blurViewCenterInset && gestureRecognizer is UIPanGestureRecognizer { + return true + } else if distance > innerRadius - blurViewRadiusInset && distance < innerRadius + innerRadiusOuterInset { + return true + } else if distance > outerRadius - outerRadiusInnerInset && distance < outerRadius + blurViewRadiusInset { + return true + } else if distance <= innerRadius - blurViewRadiusInset || distance >= outerRadius + blurViewRadiusInset { + return true + } + default: + break + } + return false + } + + func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool { + return true + } + + func update(component: BlurScreenComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + self.component = component + self.state = state + + self.setNeedsDisplay() + + return availableSize + } + + override func draw(_ rect: CGRect) { + guard let context = UIGraphicsGetCurrentContext(), let component = self.component else { + return + } + guard ![.off, .portrait].contains(component.value.mode) else { + return + } + + let centerPoint = CGPoint( + x: component.value.position.x * rect.size.width, + y: component.value.position.y * rect.size.height + ) + let innerRadius = min(rect.size.width, rect.size.height) * CGFloat(component.value.falloff) + let outerRadius = min(rect.size.width, rect.size.height) * CGFloat(component.value.size) + + context.setFillColor(UIColor.white.cgColor) + context.setShadow(offset: .zero, blur: 2.5, color: UIColor(rgb: 0x000000, alpha: 0.3).cgColor) + + let knobSize = CGSize(width: 16.0, height: 16.0) + switch component.value.mode { + case .radial: + var radSpace = degreesToRadians(degrees: 6.15) + var radLen = degreesToRadians(degrees: 10.2) + for i in 0 ..< 22 { + let cgPath = CGMutablePath() + cgPath.addArc( + center: centerPoint, + radius: innerRadius, + startAngle: CGFloat(i) * (radSpace + radLen), + endAngle: CGFloat(i) * (radSpace + radLen) + radLen, + clockwise: false + ) + let strokedArc = cgPath.copy(strokingWithWidth: 1.5, lineCap: .butt, lineJoin: .miter, miterLimit: 10.0) + context.addPath(strokedArc) + } + + radSpace = degreesToRadians(degrees: 2.02) + radLen = degreesToRadians(degrees: 3.6) + for i in 0 ..< 64 { + let cgPath = CGMutablePath() + cgPath.addArc( + center: centerPoint, + radius: outerRadius, + startAngle: CGFloat(i) * (radSpace + radLen), + endAngle: CGFloat(i) * (radSpace + radLen) + radLen, + clockwise: false + ) + let strokedArc = cgPath.copy(strokingWithWidth: 1.5, lineCap: .butt, lineJoin: .miter, miterLimit: 10.0) + context.addPath(strokedArc) + } + context.fillPath() + + context.fillEllipse(in: CGRect(origin: CGPoint(x: centerPoint.x - knobSize.width / 2.0, y: centerPoint.y - knobSize.height / 2.0), size: knobSize)) + case .linear: + context.translateBy(x: centerPoint.x, y: centerPoint.y) + context.rotate(by: CGFloat(component.value.rotation)) + + let space: CGFloat = 6.0 + var length: CGFloat = 12.0 + let thickness: CGFloat = 1.5 + + for i in 0 ..< 30 { + context.addRect(CGRect(x: CGFloat(i) * (length + space), y: -innerRadius, width: length, height: thickness)) + context.addRect(CGRect(x: CGFloat(-i) * (length + space) - space - length, y: -innerRadius, width: length, height: thickness)) + + context.addRect(CGRect(x: CGFloat(i) * (length + space), y: innerRadius, width: length, height: thickness)) + context.addRect(CGRect(x: CGFloat(-i) * (length + space) - space - length, y: innerRadius, width: length, height: thickness)) + } + + length = 6.0 + + for i in 0 ..< 64 { + context.addRect(CGRect(x: CGFloat(i) * (length + space), y: -outerRadius, width: length, height: thickness)) + context.addRect(CGRect(x: CGFloat(-i) * (length + space) - space - length, y: -outerRadius, width: length, height: thickness)) + + context.addRect(CGRect(x: CGFloat(i) * (length + space), y: outerRadius, width: length, height: thickness)) + context.addRect(CGRect(x: CGFloat(-i) * (length + space) - space - length, y: outerRadius, width: length, height: thickness)) + } + + context.fillPath() + + context.fillEllipse(in: CGRect(origin: CGPoint(x: -knobSize.width / 2.0, y: -knobSize.height / 2.0), size: knobSize)) + default: + break + } + } + } + + public func makeView() -> View { + return View(frame: CGRect()) + } + + public func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition) + } +} + +private func degreesToRadians(degrees: CGFloat) -> CGFloat { + return degrees * .pi / 180.0 +} + +private func radiansToDegrees(radians: CGFloat) -> CGFloat { + return radians * 180.0 / .pi +} diff --git a/submodules/TelegramUI/Components/MediaEditorScreen/Sources/CurvesComponent.swift b/submodules/TelegramUI/Components/MediaEditorScreen/Sources/CurvesComponent.swift new file mode 100644 index 0000000000..399d30dc23 --- /dev/null +++ b/submodules/TelegramUI/Components/MediaEditorScreen/Sources/CurvesComponent.swift @@ -0,0 +1,662 @@ +import Foundation +import UIKit +import Display +import ComponentFlow +import LegacyComponents +import MediaEditor + +private class HistogramView: UIView { + private var size: CGSize? + private var histogramBins: MediaEditorHistogram.HistogramBins? + private var color: UIColor? + + private let shapeLayer = SimpleShapeLayer() + + var dataPointsUpdated: (([Float]) -> Void)? + + init() { + super.init(frame: .zero) + + self.layer.addSublayer(self.shapeLayer) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + func updateSize(size: CGSize, histogramBins: MediaEditorHistogram.HistogramBins?, color: UIColor, transition: Transition) { + guard self.size != size || self.color != color || self.histogramBins != histogramBins else { + return + } + self.size = size + self.histogramBins = histogramBins + self.color = color + self.update(transition: transition) + } + + func update(transition: Transition) { + guard let size = self.size, let histogramBins = self.histogramBins, histogramBins.count > 0, let color = self.color else { + self.shapeLayer.path = nil + return + } + + transition.setShapeLayerFillColor(layer: self.shapeLayer, color: color) + + let (path, _) = curveThroughPoints( + count: histogramBins.count, + valueAtIndex: { index in + return histogramBins.valueAtIndex(index, mirrored: true) + }, + positionAtIndex: { index, step in + return CGFloat(index) * step + }, + size: size, + type: .filled, + granularity: 200 + ) + + transition.setShapeLayerPath(layer: self.shapeLayer, path: path.cgPath) + } +} + +enum CurvesSection { + case all + case red + case green + case blue +} + +class CurvesInternalState { + var section: CurvesSection = .all +} + +final class CurvesComponent: Component { + typealias EnvironmentType = Empty + + let histogram: MediaEditorHistogram? + let internalState: CurvesInternalState + + init( + histogram: MediaEditorHistogram?, + internalState: CurvesInternalState + ) { + self.histogram = histogram + self.internalState = internalState + } + + static func ==(lhs: CurvesComponent, rhs: CurvesComponent) -> Bool { + if lhs.histogram != rhs.histogram { + return false + } + return true + } + + final class State: ComponentState { + let internalState: CurvesInternalState + + init(internalState: CurvesInternalState) { + self.internalState = internalState + } + + var section: CurvesSection { + get { + return self.internalState.section + } + set { + self.internalState.section = newValue + } + } + } + + func makeState() -> State { + return State(internalState: self.internalState) + } + + final class View: UIView { + private var allButton = ComponentView() + private var redButton = ComponentView() + private var greenButton = ComponentView() + private var blueButton = ComponentView() + private let histogramView = HistogramView() + + private var component: CurvesComponent? + private weak var state: State? + + override init(frame: CGRect) { + super.init(frame: frame) + + self.addSubview(self.histogramView) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + func update(component: CurvesComponent, availableSize: CGSize, state: State, environment: Environment, transition: Transition) -> CGSize { + self.component = component + self.state = state + + let topInset: CGFloat = 11.0 + let allButtonSize = self.allButton.update( + transition: transition, + component: AnyComponent( + Button( + content: AnyComponent( + Text( + text: "All", + font: Font.regular(14.0), + color: state.section == .all ? .white : UIColor(rgb: 0x808080) + ) + ), + action: { [weak state] in + state?.section = .all + state?.updated(transition: Transition(animation: .curve(duration: 0.2, curve: .easeInOut))) + } + ) + ), + environment: {}, + containerSize: availableSize + ) + let allButtonFrame = CGRect(origin: CGPoint(x: floorToScreenPixels(availableSize.width / 5.0 - allButtonSize.width / 2.0), y: topInset), size: allButtonSize) + if let view = self.allButton.view { + if view.superview == nil { + self.addSubview(view) + } + transition.setFrame(view: view, frame: allButtonFrame) + } + + let redButtonSize = self.redButton.update( + transition: transition, + component: AnyComponent( + Button( + content: AnyComponent( + Text( + text: "Red", + font: Font.regular(14.0), + color: state.section == .red ? .white : UIColor(rgb: 0x808080) + ) + ), + action: { [weak state] in + state?.section = .red + state?.updated(transition: Transition(animation: .curve(duration: 0.2, curve: .easeInOut))) + } + ) + ), + environment: {}, + containerSize: availableSize + ) + let redButtonFrame = CGRect(origin: CGPoint(x: floorToScreenPixels(availableSize.width / 5.0 * 2.0 - redButtonSize.width / 2.0), y: topInset), size: redButtonSize) + if let view = self.redButton.view { + if view.superview == nil { + self.addSubview(view) + } + transition.setFrame(view: view, frame: redButtonFrame) + } + + let greenButtonSize = self.greenButton.update( + transition: transition, + component: AnyComponent( + Button( + content: AnyComponent( + Text( + text: "Green", + font: Font.regular(14.0), + color: state.section == .green ? .white : UIColor(rgb: 0x808080) + ) + ), + action: { [weak state] in + state?.section = .green + state?.updated(transition: Transition(animation: .curve(duration: 0.2, curve: .easeInOut))) + } + ) + ), + environment: {}, + containerSize: availableSize + ) + let greenButtonFrame = CGRect(origin: CGPoint(x: floorToScreenPixels(availableSize.width / 5.0 * 3.0 - greenButtonSize.width / 2.0), y: topInset), size: greenButtonSize) + if let view = self.greenButton.view { + if view.superview == nil { + self.addSubview(view) + } + transition.setFrame(view: view, frame: greenButtonFrame) + } + + let blueButtonSize = self.blueButton.update( + transition: transition, + component: AnyComponent( + Button( + content: AnyComponent( + Text( + text: "Blue", + font: Font.regular(14.0), + color: state.section == .blue ? .white : UIColor(rgb: 0x808080) + ) + ), + action: { [weak state] in + state?.section = .blue + state?.updated(transition: Transition(animation: .curve(duration: 0.2, curve: .easeInOut))) + } + ) + ), + environment: {}, + containerSize: availableSize + ) + let blueButtonFrame = CGRect(origin: CGPoint(x: floorToScreenPixels(availableSize.width / 5.0 * 4.0 - blueButtonSize.width / 2.0), y: topInset), size: blueButtonSize) + if let view = self.blueButton.view { + if view.superview == nil { + self.addSubview(view) + } + transition.setFrame(view: view, frame: blueButtonFrame) + } + + let histogramHeight: CGFloat = 85.0 + let histogramColor: UIColor + let histogramBins: MediaEditorHistogram.HistogramBins? + switch state.section { + case .all: + histogramColor = .white + histogramBins = component.histogram?.green + case .red: + histogramColor = UIColor(rgb: 0xed3d4c) + histogramBins = component.histogram?.red + case .green: + histogramColor = UIColor(rgb: 0x10ee9d) + histogramBins = component.histogram?.green + case .blue: + histogramColor = UIColor(rgb: 0x3377fb) + histogramBins = component.histogram?.blue + } + let histogramSize = CGSize(width: availableSize.width, height: histogramHeight) + let verticalSpacing: CGFloat = 3.0 + + self.histogramView.updateSize(size: histogramSize, histogramBins: histogramBins, color: histogramColor, transition: transition) + transition.setFrame(view: self.histogramView, frame: CGRect(origin: CGPoint(x: 0.0, y: topInset + allButtonSize.height + verticalSpacing), size: histogramSize)) + return CGSize(width: availableSize.width, height: topInset + allButtonSize.height + verticalSpacing + histogramHeight) + } + } + + public func makeView() -> View { + return View(frame: CGRect()) + } + + public func update(view: View, availableSize: CGSize, state: State, environment: Environment, transition: Transition) -> CGSize { + return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition) + } +} + +final class CurvesScreenComponent: Component { + typealias EnvironmentType = Empty + + let value: CurvesValue + let section: CurvesSection + let valueUpdated: (CurvesValue) -> Void + + init( + value: CurvesValue, + section: CurvesSection, + valueUpdated: @escaping (CurvesValue) -> Void + + ) { + self.value = value + self.section = section + self.valueUpdated = valueUpdated + } + + static func ==(lhs: CurvesScreenComponent, rhs: CurvesScreenComponent) -> Bool { + if lhs.value != rhs.value { + return false + } + if lhs.section != rhs.section { + return false + } + return true + } + + final class View: UIView { + enum Field { + case blacks + case shadows + case midtones + case highlights + case whites + } + + private var blacks = ComponentView() + private var shadows = ComponentView() + private var midtones = ComponentView() + private var highlights = ComponentView() + private let whites = ComponentView() + + private let line1 = SimpleLayer() + private let line2 = SimpleLayer() + private let line3 = SimpleLayer() + private let line4 = SimpleLayer() + + private let guideLayer = SimpleShapeLayer() + private let curveLayer = SimpleShapeLayer() + + private var component: CurvesScreenComponent? + private weak var state: EmptyComponentState? + + override init(frame: CGRect) { + super.init(frame: frame) + + self.layer.addSublayer(self.line1) + self.layer.addSublayer(self.line2) + self.layer.addSublayer(self.line3) + self.layer.addSublayer(self.line4) + + self.layer.addSublayer(self.guideLayer) + self.layer.addSublayer(self.curveLayer) + + self.line1.backgroundColor = UIColor(rgb: 0xffffff, alpha: 0.5).cgColor + self.line2.backgroundColor = UIColor(rgb: 0xffffff, alpha: 0.5).cgColor + self.line3.backgroundColor = UIColor(rgb: 0xffffff, alpha: 0.5).cgColor + self.line4.backgroundColor = UIColor(rgb: 0xffffff, alpha: 0.5).cgColor + + self.guideLayer.lineWidth = 1.5 + self.guideLayer.lineDashPattern = [7, 4] + self.guideLayer.strokeColor = UIColor(rgb: 0xffffff, alpha: 0.5).cgColor + + self.curveLayer.lineWidth = 2.0 + self.curveLayer.fillColor = UIColor.clear.cgColor + + let panGestureRecognizer = UIPanGestureRecognizer(target: self, action: #selector(self.handlePan(_:))) + self.addGestureRecognizer(panGestureRecognizer) + + let doubleTapGestureRecognizer = UITapGestureRecognizer(target: self, action: #selector(self.handleDoubleTap(_:))) + doubleTapGestureRecognizer.numberOfTapsRequired = 2 + self.addGestureRecognizer(doubleTapGestureRecognizer) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + private var selectedField: Field? + @objc func handlePan(_ gestureRecognizer: UIPanGestureRecognizer) { + guard let component = self.component else { + return + } + + let fieldWidth = self.frame.width / 5.0 + + switch gestureRecognizer.state { + case .began: + let location = gestureRecognizer.location(in: gestureRecognizer.view).x + let index = floor(location / fieldWidth) + switch index { + case 0: + self.selectedField = .blacks + case 1: + self.selectedField = .shadows + case 2: + self.selectedField = .midtones + case 3: + self.selectedField = .highlights + case 4: + self.selectedField = .whites + default: + break + } + case .changed: + guard let selectedField = self.selectedField else { + return + } + let translation = gestureRecognizer.translation(in: gestureRecognizer.view).y + let delta = Float(min(2.0, -1.0 * translation / 8.0) / 100.0) + + var updatedValue = component.value + + var curve: CurvesValue.CurveValue + switch component.section { + case .all: + curve = updatedValue.all + case .red: + curve = updatedValue.red + case .green: + curve = updatedValue.green + case .blue: + curve = updatedValue.blue + } + + switch selectedField { + case .blacks: + curve = curve.withUpdatedBlacks(max(0.0, min(1.0, curve.blacks + delta))) + case .shadows: + curve = curve.withUpdatedShadows(max(0.0, min(1.0, curve.shadows + delta))) + case .midtones: + curve = curve.withUpdatedMidtones(max(0.0, min(1.0, curve.midtones + delta))) + case .highlights: + curve = curve.withUpdatedHighlights(max(0.0, min(1.0, curve.highlights + delta))) + case .whites: + curve = curve.withUpdatedWhites(max(0.0, min(1.0, curve.whites + delta))) + } + + switch component.section { + case .all: + updatedValue = updatedValue.withUpdatedAll(curve) + case .red: + updatedValue = updatedValue.withUpdatedRed(curve) + case .green: + updatedValue = updatedValue.withUpdatedGreen(curve) + case .blue: + updatedValue = updatedValue.withUpdatedBlue(curve) + } + + component.valueUpdated(updatedValue) + + gestureRecognizer.setTranslation(.zero, in: gestureRecognizer.view) + default: + self.selectedField = nil + } + } + + @objc func handleDoubleTap(_ gestureRecognizer: UITapGestureRecognizer) { + guard let component = self.component else { + return + } + switch component.section { + case .all: + component.valueUpdated(component.value.withUpdatedAll(.initial)) + case .red: + component.valueUpdated(component.value.withUpdatedRed(.initial)) + case .green: + component.valueUpdated(component.value.withUpdatedGreen(.initial)) + case .blue: + component.valueUpdated(component.value.withUpdatedBlue(.initial)) + } + } + + func update(component: CurvesScreenComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + self.component = component + self.state = state + + let value: CurvesValue.CurveValue + let lineColor: UIColor + switch component.section { + case .all: + lineColor = UIColor.white + value = component.value.all + case .red: + lineColor = UIColor(rgb: 0xed3d4c) + value = component.value.red + case .green: + lineColor = UIColor(rgb: 0x10ee9d) + value = component.value.green + case .blue: + lineColor = UIColor(rgb: 0x3377fb) + value = component.value.blue + } + + let fieldWidth = availableSize.width / 5.0 + let bottomInset: CGFloat = 5.0 + + let blacksSize = self.blacks.update( + transition: transition, + component: AnyComponent( + Text( + text: String(format: "%.2f", value.blacks), + font: Font.regular(14.0), + color: UIColor(rgb: 0xffffff, alpha: 0.75) + ) + ), + environment: {}, + containerSize: availableSize + ) + let blacksFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((fieldWidth - blacksSize.width) / 2.0), y: availableSize.height - blacksSize.height - bottomInset), size: blacksSize) + if let view = self.blacks.view { + if view.superview == nil { + self.addSubview(view) + } + transition.setFrame(view: view, frame: blacksFrame) + } + + let shadowsSize = self.shadows.update( + transition: transition, + component: AnyComponent( + Text( + text: String(format: "%.2f", value.shadows), + font: Font.regular(14.0), + color: UIColor(rgb: 0xffffff, alpha: 0.75) + ) + ), + environment: {}, + containerSize: availableSize + ) + let shadowsFrame = CGRect(origin: CGPoint(x: floorToScreenPixels(fieldWidth + (fieldWidth - blacksSize.width) / 2.0), y: availableSize.height - shadowsSize.height - bottomInset), size: shadowsSize) + if let view = self.shadows.view { + if view.superview == nil { + self.addSubview(view) + } + transition.setFrame(view: view, frame: shadowsFrame) + } + + let midtonesSize = self.midtones.update( + transition: transition, + component: AnyComponent( + Text( + text: String(format: "%.2f", value.midtones), + font: Font.regular(14.0), + color: UIColor(rgb: 0xffffff, alpha: 0.75) + ) + ), + environment: {}, + containerSize: availableSize + ) + let midtonesFrame = CGRect(origin: CGPoint(x: floorToScreenPixels(fieldWidth * 2.0 + (fieldWidth - blacksSize.width) / 2.0), y: availableSize.height - midtonesSize.height - bottomInset), size: midtonesSize) + if let view = self.midtones.view { + if view.superview == nil { + self.addSubview(view) + } + transition.setFrame(view: view, frame: midtonesFrame) + } + + let highlightsSize = self.highlights.update( + transition: transition, + component: AnyComponent( + Text( + text: String(format: "%.2f", value.highlights), + font: Font.regular(14.0), + color: UIColor(rgb: 0xffffff, alpha: 0.75) + ) + ), + environment: {}, + containerSize: availableSize + ) + let highlightsFrame = CGRect(origin: CGPoint(x: floorToScreenPixels(fieldWidth * 3.0 + (fieldWidth - blacksSize.width) / 2.0), y: availableSize.height - highlightsSize.height - bottomInset), size: highlightsSize) + if let view = self.highlights.view { + if view.superview == nil { + self.addSubview(view) + } + transition.setFrame(view: view, frame: highlightsFrame) + } + + let whitesSize = self.whites.update( + transition: transition, + component: AnyComponent( + Text( + text: String(format: "%.2f", value.whites), + font: Font.regular(14.0), + color: UIColor(rgb: 0xffffff, alpha: 0.75) + ) + ), + environment: {}, + containerSize: availableSize + ) + let whitesFrame = CGRect(origin: CGPoint(x: floorToScreenPixels(fieldWidth * 4.0 + (fieldWidth - blacksSize.width) / 2.0), y: availableSize.height - whitesSize.height - bottomInset), size: whitesSize) + if let view = self.whites.view { + if view.superview == nil { + self.addSubview(view) + } + transition.setFrame(view: view, frame: whitesFrame) + } + + let lineWidth: CGFloat = 1.0 - UIScreenPixel + self.line1.frame = CGRect(x: fieldWidth, y: 0.0, width: lineWidth, height: availableSize.height) + self.line2.frame = CGRect(x: fieldWidth * 2.0, y: 0.0, width: lineWidth, height: availableSize.height) + self.line3.frame = CGRect(x: fieldWidth * 3.0, y: 0.0, width: lineWidth, height: availableSize.height) + self.line4.frame = CGRect(x: fieldWidth * 4.0, y: 0.0, width: lineWidth, height: availableSize.height) + + let guidePath = UIBezierPath() + guidePath.move(to: CGPoint(x: 0.0, y: availableSize.height)) + guidePath.addLine(to: CGPoint(x: availableSize.width, y: 0.0)) + + self.guideLayer.frame = CGRect(origin: .zero, size: availableSize) + self.guideLayer.path = guidePath.cgPath + + self.curveLayer.strokeColor = lineColor.cgColor + self.curveLayer.frame = CGRect(origin: .zero, size: availableSize) + + let points: [Float] = [ + value.blacks, + value.blacks, + value.shadows, + value.midtones, + value.highlights, + value.whites, + value.whites + ] + + let (curvePath, _) = curveThroughPoints( + count: points.count, + valueAtIndex: { index in + return 1.0 - points[index] + }, + positionAtIndex: { index, _ in + switch index { + case 0: + return -1.0 + case 1: + return 0.0 + case 2: + return 0.25 * availableSize.width + case 3: + return 0.5 * availableSize.width + case 4: + return 0.75 * availableSize.width + case 5: + return availableSize.width + default: + return availableSize.width + 1.0 + } + }, + size: availableSize, + type: .line, + granularity: 100 + ) + self.curveLayer.path = curvePath.cgPath + + return availableSize + } + } + + public func makeView() -> View { + return View(frame: CGRect()) + } + + public func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition) + } +} + diff --git a/submodules/TelegramUI/Components/MediaEditorScreen/Sources/MediaEditorScreen.swift b/submodules/TelegramUI/Components/MediaEditorScreen/Sources/MediaEditorScreen.swift new file mode 100644 index 0000000000..46a663c39c --- /dev/null +++ b/submodules/TelegramUI/Components/MediaEditorScreen/Sources/MediaEditorScreen.swift @@ -0,0 +1,1191 @@ +import Foundation +import UIKit +import Display +import AsyncDisplayKit +import ComponentFlow +import SwiftSignalKit +import ViewControllerComponent +import ComponentDisplayAdapters +import TelegramPresentationData +import AccountContext +import TelegramCore +import MultilineTextComponent +import DrawingUI +import MediaEditor +import Photos +import LottieAnimationComponent +import MessageInputPanelComponent +import EntityKeyboard + +enum DrawingScreenType { + case drawing + case text + case sticker +} + +final class MediaEditorScreenComponent: Component { + typealias EnvironmentType = ViewControllerComponentContainer.Environment + + let context: AccountContext + let openDrawing: (DrawingScreenType) -> Void + let openTools: () -> Void + + init( + context: AccountContext, + openDrawing: @escaping (DrawingScreenType) -> Void, + openTools: @escaping () -> Void + ) { + self.context = context + self.openDrawing = openDrawing + self.openTools = openTools + } + + static func ==(lhs: MediaEditorScreenComponent, rhs: MediaEditorScreenComponent) -> Bool { + if lhs.context !== rhs.context { + return false + } + return true + } + + final class State: ComponentState { + enum ImageKey: Hashable { + case draw + case text + case sticker + case tools + case done + } + private var cachedImages: [ImageKey: UIImage] = [:] + func image(_ key: ImageKey) -> UIImage { + if let image = self.cachedImages[key] { + return image + } else { + var image: UIImage + switch key { + case .draw: + image = generateTintedImage(image: UIImage(bundleImageName: "Media Editor/Pencil"), color: .white)! + case .text: + image = generateTintedImage(image: UIImage(bundleImageName: "Media Editor/AddText"), color: .white)! + case .sticker: + image = generateTintedImage(image: UIImage(bundleImageName: "Media Editor/AddSticker"), color: .white)! + case .tools: + image = generateTintedImage(image: UIImage(bundleImageName: "Media Editor/Tools"), color: .white)! + case .done: + let accentColor = self.context.sharedContext.currentPresentationData.with { $0 }.theme.chat.inputPanel.panelControlAccentColor + image = generateImage(CGSize(width: 33.0, height: 33.0), rotatedContext: { size, context in + context.clear(CGRect(origin: CGPoint(), size: size)) + context.setFillColor(accentColor.cgColor) + context.fillEllipse(in: CGRect(origin: CGPoint(), size: size)) + context.setBlendMode(.copy) + context.setStrokeColor(UIColor.white.cgColor) + context.setLineWidth(2.0) + context.setLineCap(.round) + context.setLineJoin(.round) + + context.translateBy(x: 5.45, y: 4.0) + + context.saveGState() + context.translateBy(x: 4.0, y: 4.0) + let _ = try? drawSvgPath(context, path: "M1,7 L7,1 L13,7 S ") + context.restoreGState() + + context.saveGState() + context.translateBy(x: 10.0, y: 4.0) + let _ = try? drawSvgPath(context, path: "M1,16 V1 S ") + context.restoreGState() + })! + } + cachedImages[key] = image + return image + } + } + + let context: AccountContext + + init(context: AccountContext) { + self.context = context + + super.init() + + } + + deinit { + + } + } + + func makeState() -> State { + return State( + context: self.context + ) + } + + public final class View: UIView { + private let cancelButton = ComponentView() + private let drawButton = ComponentView() + private let textButton = ComponentView() + private let stickerButton = ComponentView() + private let toolsButton = ComponentView() + private let doneButton = ComponentView() + + private let inputPanel = ComponentView() + private let inputPanelExternalState = MessageInputPanelComponent.ExternalState() + + private let scrubber = ComponentView() + + private let saveButton = ComponentView() + private let muteButton = ComponentView() + + private var component: MediaEditorScreenComponent? + private weak var state: State? + private var environment: ViewControllerComponentContainer.Environment? + + override init(frame: CGRect) { + super.init(frame: frame) + + self.backgroundColor = .clear + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + func animateInFromCamera() { + if let view = self.cancelButton.view { + view.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2) + view.layer.animateScale(from: 0.1, to: 1.0, duration: 0.2) + } + + let buttons = [ + self.drawButton, + self.textButton, + self.stickerButton, + self.toolsButton + ] + + var delay: Double = 0.0 + for button in buttons { + if let view = button.view { + view.layer.animatePosition(from: CGPoint(x: 0.0, y: 64.0), to: .zero, duration: 0.3, delay: delay, timingFunction: kCAMediaTimingFunctionSpring, additive: true) + view.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2, delay: delay) + view.layer.animateScale(from: 0.1, to: 1.0, duration: 0.2, delay: delay) + delay += 0.05 + } + } + + if let view = self.doneButton.view { + view.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2) + view.layer.animateScale(from: 0.1, to: 1.0, duration: 0.2) + } + + if let view = self.inputPanel.view { + view.layer.animatePosition(from: CGPoint(x: 0.0, y: 44.0), to: .zero, duration: 0.3, timingFunction: kCAMediaTimingFunctionSpring, additive: true) + view.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2) + view.layer.animateScale(from: 0.1, to: 1.0, duration: 0.2) + } + + if let view = self.saveButton.view { + view.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2) + view.layer.animateScale(from: 0.1, to: 1.0, duration: 0.2) + } + + if let view = self.muteButton.view { + view.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2) + view.layer.animateScale(from: 0.1, to: 1.0, duration: 0.2) + } + } + + func animateOutToCamera() { + let transition = Transition(animation: .curve(duration: 0.2, curve: .easeInOut)) + if let view = self.cancelButton.view { + transition.setAlpha(view: view, alpha: 0.0) + transition.setScale(view: view, scale: 0.1) + } + + let buttons = [ + self.drawButton, + self.textButton, + self.stickerButton, + self.toolsButton + ] + + for button in buttons { + if let view = button.view { + view.layer.animatePosition(from: .zero, to: CGPoint(x: 0.0, y: 64.0), duration: 0.3, timingFunction: kCAMediaTimingFunctionSpring, removeOnCompletion: false, additive: true) + view.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false) + view.layer.animateScale(from: 1.0, to: 0.1, duration: 0.2) + } + } + + if let view = self.doneButton.view { + transition.setAlpha(view: view, alpha: 0.0) + transition.setScale(view: view, scale: 0.1) + } + + if let view = self.inputPanel.view { + view.layer.animatePosition(from: .zero, to: CGPoint(x: 0.0, y: 44.0), duration: 0.3, timingFunction: kCAMediaTimingFunctionSpring, removeOnCompletion: false, additive: true) + view.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false) + view.layer.animateScale(from: 1.0, to: 0.1, duration: 0.2) + } + + if let view = self.saveButton.view { + transition.setAlpha(view: view, alpha: 0.0) + transition.setScale(view: view, scale: 0.1) + } + + if let view = self.muteButton.view { + transition.setAlpha(view: view, alpha: 0.0) + transition.setScale(view: view, scale: 0.1) + } + } + + func animateOutToTool() { + let transition = Transition(animation: .curve(duration: 0.2, curve: .easeInOut)) + if let view = self.cancelButton.view { + view.alpha = 0.0 + } + + let buttons = [ + self.drawButton, + self.textButton, + self.stickerButton, + self.toolsButton + ] + + for button in buttons { + if let view = button.view { + view.layer.animatePosition(from: .zero, to: CGPoint(x: 0.0, y: -44.0), duration: 0.3, timingFunction: kCAMediaTimingFunctionSpring, additive: true) + transition.setAlpha(view: view, alpha: 0.0) + view.layer.animateScale(from: 1.0, to: 0.1, duration: 0.2) + } + } + + if let view = self.doneButton.view { + transition.setAlpha(view: view, alpha: 0.0) + transition.setScale(view: view, scale: 0.1) + } + + if let view = self.inputPanel.view { + transition.setAlpha(view: view, alpha: 0.0) + view.layer.animateScale(from: 1.0, to: 0.1, duration: 0.2) + } + + if let view = self.saveButton.view { + transition.setAlpha(view: view, alpha: 0.0) + transition.setScale(view: view, scale: 0.1) + } + + if let view = self.muteButton.view { + transition.setAlpha(view: view, alpha: 0.0) + transition.setScale(view: view, scale: 0.1) + } + } + + func animateInFromTool() { + let transition = Transition(animation: .curve(duration: 0.2, curve: .easeInOut)) + if let view = self.cancelButton.view { + view.alpha = 1.0 + } + + let buttons = [ + self.drawButton, + self.textButton, + self.stickerButton, + self.toolsButton + ] + + for button in buttons { + if let view = button.view { + view.layer.animatePosition(from: CGPoint(x: 0.0, y: -44.0), to: .zero, duration: 0.3, timingFunction: kCAMediaTimingFunctionSpring, additive: true) + transition.setAlpha(view: view, alpha: 1.0) + view.layer.animateScale(from: 0.1, to: 1.0, duration: 0.2) + } + } + + if let view = self.doneButton.view { + transition.setAlpha(view: view, alpha: 1.0) + transition.setScale(view: view, scale: 1.0) + } + + if let view = self.inputPanel.view { + transition.setAlpha(view: view, alpha: 1.0) + view.layer.animateScale(from: 0.0, to: 1.0, duration: 0.2) + } + + if let view = self.saveButton.view { + transition.setAlpha(view: view, alpha: 1.0) + transition.setScale(view: view, scale: 1.0) + } + + if let view = self.muteButton.view { + transition.setAlpha(view: view, alpha: 1.0) + transition.setScale(view: view, scale: 1.0) + } + } + + func update(component: MediaEditorScreenComponent, availableSize: CGSize, state: State, environment: Environment, transition: Transition) -> CGSize { + let environment = environment[ViewControllerComponentContainer.Environment.self].value + self.environment = environment + + self.component = component + self.state = state + + let openDrawing = component.openDrawing + let openTools = component.openTools + + let buttonSideInset: CGFloat = 10.0 + let buttonBottomInset: CGFloat = 8.0 + + let cancelButtonSize = self.cancelButton.update( + transition: transition, + component: AnyComponent(Button( + content: AnyComponent( + LottieAnimationComponent( + animation: LottieAnimationComponent.AnimationItem( + name: "media_backToCancel", + mode: .still(position: .begin), + range: nil + ), + colors: ["__allcolors__": .white], + size: CGSize(width: 33.0, height: 33.0) + ) + ), + action: { + guard let controller = environment.controller() as? MediaEditorScreen else { + return + } + controller.requestDismiss(animated: true) + } + )), + environment: {}, + containerSize: CGSize(width: 44.0, height: 44.0) + ) + let cancelButtonFrame = CGRect( + origin: CGPoint(x: buttonSideInset, y: availableSize.height - environment.safeInsets.bottom + buttonBottomInset), + size: cancelButtonSize + ) + if let cancelButtonView = self.cancelButton.view { + if cancelButtonView.superview == nil { + self.addSubview(cancelButtonView) + } + transition.setPosition(view: cancelButtonView, position: cancelButtonFrame.center) + transition.setBounds(view: cancelButtonView, bounds: CGRect(origin: .zero, size: cancelButtonFrame.size)) + } + + let doneButtonSize = self.doneButton.update( + transition: transition, + component: AnyComponent(Button( + content: AnyComponent(Image( + image: state.image(.done), + size: CGSize(width: 33.0, height: 33.0) + )), + action: { + guard let controller = environment.controller() as? MediaEditorScreen else { + return + } + controller.requestCompletion(animated: true) + } + )), + environment: {}, + containerSize: CGSize(width: 44.0, height: 44.0) + ) + let doneButtonFrame = CGRect( + origin: CGPoint(x: availableSize.width - buttonSideInset - doneButtonSize.width, y: availableSize.height - environment.safeInsets.bottom + buttonBottomInset), + size: doneButtonSize + ) + if let doneButtonView = self.doneButton.view { + if doneButtonView.superview == nil { + self.addSubview(doneButtonView) + } + transition.setPosition(view: doneButtonView, position: doneButtonFrame.center) + transition.setBounds(view: doneButtonView, bounds: CGRect(origin: .zero, size: doneButtonFrame.size)) + } + + let drawButtonSize = self.drawButton.update( + transition: transition, + component: AnyComponent(Button( + content: AnyComponent(Image( + image: state.image(.draw), + size: CGSize(width: 30.0, height: 30.0) + )), + action: { + openDrawing(.drawing) + } + )), + environment: {}, + containerSize: CGSize(width: 40.0, height: 40.0) + ) + let drawButtonFrame = CGRect( + origin: CGPoint(x: floorToScreenPixels(availableSize.width / 4.0 - 3.0 - drawButtonSize.width / 2.0), y: availableSize.height - environment.safeInsets.bottom + buttonBottomInset), + size: drawButtonSize + ) + if let drawButtonView = self.drawButton.view { + if drawButtonView.superview == nil { + self.addSubview(drawButtonView) + } + transition.setFrame(view: drawButtonView, frame: drawButtonFrame) + } + + let textButtonSize = self.textButton.update( + transition: transition, + component: AnyComponent(Button( + content: AnyComponent(Image( + image: state.image(.text), + size: CGSize(width: 30.0, height: 30.0) + )), + action: { + openDrawing(.text) + } + )), + environment: {}, + containerSize: CGSize(width: 40.0, height: 40.0) + ) + let textButtonFrame = CGRect( + origin: CGPoint(x: floorToScreenPixels(availableSize.width / 2.5 + 5.0 - textButtonSize.width / 2.0), y: availableSize.height - environment.safeInsets.bottom + buttonBottomInset), + size: textButtonSize + ) + if let textButtonView = self.textButton.view { + if textButtonView.superview == nil { + self.addSubview(textButtonView) + } + transition.setFrame(view: textButtonView, frame: textButtonFrame) + } + + let stickerButtonSize = self.stickerButton.update( + transition: transition, + component: AnyComponent(Button( + content: AnyComponent(Image( + image: state.image(.sticker), + size: CGSize(width: 30.0, height: 30.0) + )), + action: { + openDrawing(.sticker) + } + )), + environment: {}, + containerSize: CGSize(width: 40.0, height: 40.0) + ) + let stickerButtonFrame = CGRect( + origin: CGPoint(x: floorToScreenPixels(availableSize.width - availableSize.width / 2.5 - 5.0 - stickerButtonSize.width / 2.0), y: availableSize.height - environment.safeInsets.bottom + buttonBottomInset), + size: stickerButtonSize + ) + if let stickerButtonView = self.stickerButton.view { + if stickerButtonView.superview == nil { + self.addSubview(stickerButtonView) + } + transition.setFrame(view: stickerButtonView, frame: stickerButtonFrame) + } + + let toolsButtonSize = self.toolsButton.update( + transition: transition, + component: AnyComponent(Button( + content: AnyComponent(Image( + image: state.image(.tools), + size: CGSize(width: 30.0, height: 30.0) + )), + action: { + openTools() + } + )), + environment: {}, + containerSize: CGSize(width: 40.0, height: 40.0) + ) + let toolsButtonFrame = CGRect( + origin: CGPoint(x: floorToScreenPixels(availableSize.width / 4.0 * 3.0 + 3.0 - toolsButtonSize.width / 2.0), y: availableSize.height - environment.safeInsets.bottom + buttonBottomInset), + size: toolsButtonSize + ) + if let toolsButtonView = self.toolsButton.view { + if toolsButtonView.superview == nil { + self.addSubview(toolsButtonView) + } + transition.setFrame(view: toolsButtonView, frame: toolsButtonFrame) + } + + var scrubberBottomInset: CGFloat = 0.0 + if !"".isEmpty { + let scrubberInset: CGFloat = 9.0 + let scrubberSize = self.scrubber.update( + transition: transition, + component: AnyComponent(VideoScrubberComponent( + context: component.context, + duration: 1.0, + startPosition: 0.0, + endPosition: 1.0 + )), + environment: {}, + containerSize: CGSize(width: availableSize.width - scrubberInset * 2.0, height: availableSize.height) + ) + + let scrubberFrame = CGRect(origin: CGPoint(x: scrubberInset, y: availableSize.height - environment.safeInsets.bottom - scrubberSize.height - 8.0), size: scrubberSize) + if let scrubberView = self.scrubber.view { + if scrubberView.superview == nil { + self.addSubview(scrubberView) + } + transition.setFrame(view: scrubberView, frame: scrubberFrame) + } + + scrubberBottomInset = scrubberSize.height + 10.0 + } else { + + } + + self.inputPanel.parentState = state + let inputPanelSize = self.inputPanel.update( + transition: transition, + component: AnyComponent(MessageInputPanelComponent( + externalState: self.inputPanelExternalState, + context: component.context, + theme: environment.theme, + strings: environment.strings, + style: .editor, + placeholder: "Add a caption...", + presentController: { [weak self] c in + guard let self, let _ = self.component else { + return + } + //component.presentController(c) + }, + sendMessageAction: { [weak self] in + guard let _ = self else { + return + } + //self.performSendMessageAction() + }, + setMediaRecordingActive: nil, + attachmentAction: nil, + reactionAction: nil, + audioRecorder: nil, + videoRecordingStatus: nil + )), + environment: {}, + containerSize: CGSize(width: availableSize.width, height: 200.0) + ) + + var inputPanelBottomInset: CGFloat = scrubberBottomInset + if environment.inputHeight > 0.0 { + inputPanelBottomInset = environment.inputHeight - environment.safeInsets.bottom + } + let inputPanelFrame = CGRect(origin: CGPoint(x: 0.0, y: availableSize.height - environment.safeInsets.bottom - inputPanelBottomInset - inputPanelSize.height - 3.0), size: inputPanelSize) + if let inputPanelView = self.inputPanel.view { + if inputPanelView.superview == nil { + self.addSubview(inputPanelView) + } + transition.setFrame(view: inputPanelView, frame: inputPanelFrame) + } + + let saveButtonSize = self.saveButton.update( + transition: transition, + component: AnyComponent(Button( + content: AnyComponent( + LottieAnimationComponent( + animation: LottieAnimationComponent.AnimationItem( + name: "anim_storysave", + mode: .still(position: .begin), + range: nil + ), + colors: ["__allcolors__": .white], + size: CGSize(width: 33.0, height: 33.0) + ) + ), + action: { + guard let controller = environment.controller() as? MediaEditorScreen else { + return + } + controller.requestDismiss(animated: true) + } + )), + environment: {}, + containerSize: CGSize(width: 44.0, height: 44.0) + ) + let saveButtonFrame = CGRect( + origin: CGPoint(x: availableSize.width - 20.0 - saveButtonSize.width, y: environment.safeInsets.top + 20.0), + size: saveButtonSize + ) + if let saveButtonView = self.saveButton.view { + if saveButtonView.superview == nil { + self.addSubview(saveButtonView) + } + transition.setPosition(view: saveButtonView, position: saveButtonFrame.center) + transition.setBounds(view: saveButtonView, bounds: CGRect(origin: .zero, size: saveButtonFrame.size)) + } + + let muteButtonSize = self.muteButton.update( + transition: transition, + component: AnyComponent(Button( + content: AnyComponent( + LottieAnimationComponent( + animation: LottieAnimationComponent.AnimationItem( + name: "anim_storymute", + mode: .still(position: .begin), + range: nil + ), + colors: ["__allcolors__": .white], + size: CGSize(width: 33.0, height: 33.0) + ) + ), + action: { + guard let controller = environment.controller() as? MediaEditorScreen else { + return + } + controller.requestDismiss(animated: true) + } + )), + environment: {}, + containerSize: CGSize(width: 44.0, height: 44.0) + ) + let muteButtonFrame = CGRect( + origin: CGPoint(x: availableSize.width - 20.0 - muteButtonSize.width - 50.0, y: environment.safeInsets.top + 20.0), + size: muteButtonSize + ) + if let muteButtonView = self.muteButton.view { + if muteButtonView.superview == nil { + self.addSubview(muteButtonView) + } + transition.setPosition(view: muteButtonView, position: muteButtonFrame.center) + transition.setBounds(view: muteButtonView, bounds: CGRect(origin: .zero, size: muteButtonFrame.size)) + } + + return availableSize + } + } + + func makeView() -> View { + return View() + } + + public func update(view: View, availableSize: CGSize, state: State, environment: Environment, transition: Transition) -> CGSize { + return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition) + } +} + +private let storyDimensions = CGSize(width: 1080.0, height: 1920.0) + +public final class MediaEditorScreen: ViewController { + fileprivate final class Node: ViewControllerTracingNode, UIGestureRecognizerDelegate { + private weak var controller: MediaEditorScreen? + private let context: AccountContext + private let initializationTimestamp = CACurrentMediaTime() + + private var subject: MediaEditorScreen.Subject? + private var subjectDisposable: Disposable? + + private let backgroundDimView: UIView + fileprivate let componentHost: ComponentView + + private let previewContainerView: UIView + + private let gradientView: UIImageView + private var gradientColorsDisposable: Disposable? + + fileprivate let entitiesContainerView: UIView + fileprivate let entitiesView: DrawingEntitiesView + fileprivate let drawingView: DrawingView + fileprivate let previewView: MediaEditorPreviewView + fileprivate var mediaEditor: MediaEditor? + + private let stickerPickerInputData = Promise() + + private var presentationData: PresentationData + private let hapticFeedback = HapticFeedback() + private var validLayout: ContainerViewLayout? + + init(controller: MediaEditorScreen) { + self.controller = controller + self.context = controller.context + + self.presentationData = self.context.sharedContext.currentPresentationData.with { $0 } + + self.backgroundDimView = UIView() + self.backgroundDimView.alpha = 0.0 + self.backgroundDimView.backgroundColor = .black + + self.componentHost = ComponentView() + + self.previewContainerView = UIView() + self.previewContainerView.alpha = 0.0 + self.previewContainerView.clipsToBounds = true + self.previewContainerView.layer.cornerRadius = 12.0 + if #available(iOS 13.0, *) { + self.previewContainerView.layer.cornerCurve = .continuous + } + + self.gradientView = UIImageView() + + self.entitiesContainerView = UIView(frame: CGRect(origin: .zero, size: storyDimensions)) + self.entitiesView = DrawingEntitiesView(context: controller.context, size: storyDimensions) + self.entitiesView.getEntityCenterPosition = { + return CGPoint(x: storyDimensions.width / 2.0, y: storyDimensions.height / 2.0) + } + self.previewView = MediaEditorPreviewView(frame: .zero) + self.drawingView = DrawingView(size: storyDimensions) + self.drawingView.isUserInteractionEnabled = false + + super.init() + + self.backgroundColor = .clear + + //self.view.addSubview(self.backgroundDimView) + self.view.addSubview(self.previewContainerView) + self.previewContainerView.addSubview(self.gradientView) + self.previewContainerView.addSubview(self.entitiesContainerView) + self.entitiesContainerView.addSubview(self.entitiesView) + self.previewContainerView.addSubview(self.drawingView) + + self.subjectDisposable = ( + controller.subject + |> filter { + $0 != nil + } + |> take(1) + |> deliverOnMainQueue + ).start(next: { [weak self] subject in + if let self, let subject { + self.setup(with: subject) + } + }) + + let stickerPickerInputData = self.stickerPickerInputData + Queue.concurrentDefaultQueue().after(0.5, { + let emojiItems = EmojiPagerContentComponent.emojiInputData( + context: controller.context, + animationCache: controller.context.animationCache, + animationRenderer: controller.context.animationRenderer, + isStandalone: false, + isStatusSelection: false, + isReactionSelection: false, + isEmojiSelection: true, + hasTrending: false, + topReactionItems: [], + areUnicodeEmojiEnabled: true, + areCustomEmojiEnabled: true, + chatPeerId: controller.context.account.peerId, + hasSearch: false, + forceHasPremium: true + ) + + let stickerItems = EmojiPagerContentComponent.stickerInputData( + context: controller.context, + animationCache: controller.context.animationCache, + animationRenderer: controller.context.animationRenderer, + stickerNamespaces: [Namespaces.ItemCollection.CloudStickerPacks], + stickerOrderedItemListCollectionIds: [Namespaces.OrderedItemList.CloudSavedStickers, Namespaces.OrderedItemList.CloudRecentStickers, Namespaces.OrderedItemList.CloudAllPremiumStickers], + chatPeerId: controller.context.account.peerId, + hasSearch: false, + hasTrending: true, + forceHasPremium: true + ) + + let maskItems = EmojiPagerContentComponent.stickerInputData( + context: controller.context, + animationCache: controller.context.animationCache, + animationRenderer: controller.context.animationRenderer, + stickerNamespaces: [Namespaces.ItemCollection.CloudMaskPacks], + stickerOrderedItemListCollectionIds: [], + chatPeerId: controller.context.account.peerId, + hasSearch: false, + hasTrending: false, + forceHasPremium: true + ) + + let signal = combineLatest(queue: .mainQueue(), + emojiItems, + stickerItems, + maskItems + ) |> map { emoji, stickers, masks -> StickerPickerInputData in + return StickerPickerInputData(emoji: emoji, stickers: stickers, masks: masks) + } + + stickerPickerInputData.set(signal) + }) + } + + deinit { + self.subjectDisposable?.dispose() + self.gradientColorsDisposable?.dispose() + } + + private func setup(with subject: MediaEditorScreen.Subject) { + self.subject = subject + guard let controller = self.controller else { + return + } + + let mediaDimensions = subject.dimensions + + let maxSide: CGFloat = 1920.0 / UIScreen.main.scale + let fittedSize = mediaDimensions.cgSize.fitted(CGSize(width: maxSide, height: maxSide)) + let mediaEntity = DrawingMediaEntity(content: subject.mediaContent, size: fittedSize) + mediaEntity.position = CGPoint(x: storyDimensions.width / 2.0, y: storyDimensions.height / 2.0) + if fittedSize.height > fittedSize.width { + mediaEntity.scale = storyDimensions.height / fittedSize.height + } else { + mediaEntity.scale = storyDimensions.width / fittedSize.width + } + self.entitiesView.add(mediaEntity, announce: false) + if let entityView = self.entitiesView.getView(for: mediaEntity.uuid) as? DrawingMediaEntityView { + entityView.previewView = self.previewView + } + + let mediaEditor = MediaEditor(context: controller.context, subject: subject.editorSubject) + mediaEditor.attachPreviewView(self.previewView) + + self.gradientColorsDisposable = mediaEditor.gradientColors.start(next: { [weak self] colors in + if let self, let colors { + let (topColor, bottomColor) = colors + let gradientImage = generateGradientImage(size: CGSize(width: 5.0, height: 640.0), colors: [topColor, bottomColor], locations: [0.0, 1.0]) + Queue.mainQueue().async { + self.gradientView.image = gradientImage + + self.previewContainerView.alpha = 1.0 + if CACurrentMediaTime() - self.initializationTimestamp > 0.2 { + self.previewContainerView.layer.allowsGroupOpacity = true + self.previewContainerView.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.25, completion: { _ in + self.previewContainerView.layer.allowsGroupOpacity = false + }) + } + } + } + }) + self.mediaEditor = mediaEditor + } + + override func didLoad() { + super.didLoad() + + self.view.disablesInteractiveModalDismiss = true + self.view.disablesInteractiveKeyboardGestureRecognizer = true + + let panGestureRecognizer = UIPanGestureRecognizer(target: self, action: #selector(self.handlePan(_:))) + panGestureRecognizer.delegate = self + panGestureRecognizer.minimumNumberOfTouches = 2 + panGestureRecognizer.maximumNumberOfTouches = 2 + self.previewContainerView.addGestureRecognizer(panGestureRecognizer) + + let pinchGestureRecognizer = UIPinchGestureRecognizer(target: self, action: #selector(self.handlePinch(_:))) + pinchGestureRecognizer.delegate = self + self.previewContainerView.addGestureRecognizer(pinchGestureRecognizer) + + let rotateGestureRecognizer = UIRotationGestureRecognizer(target: self, action: #selector(self.handleRotate(_:))) + rotateGestureRecognizer.delegate = self + self.previewContainerView.addGestureRecognizer(rotateGestureRecognizer) + } + + @objc func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool { + return true + } + + @objc func handlePan(_ gestureRecognizer: UIPanGestureRecognizer) { + self.entitiesView.handlePan(gestureRecognizer) + } + + @objc func handlePinch(_ gestureRecognizer: UIPinchGestureRecognizer) { + self.entitiesView.handlePinch(gestureRecognizer) + } + + @objc func handleRotate(_ gestureRecognizer: UIRotationGestureRecognizer) { + self.entitiesView.handleRotate(gestureRecognizer) + } + + func animateIn() { + if let sourceHint = self.controller?.sourceHint { + switch sourceHint { + case .camera: + if let view = self.componentHost.view as? MediaEditorScreenComponent.View { + view.animateInFromCamera() + } + } + } + } + + func animateOut(completion: @escaping () -> Void) { + guard let controller = self.controller else { + return + } + if let sourceHint = controller.sourceHint { + switch sourceHint { + case .camera: + if let view = self.componentHost.view as? MediaEditorScreenComponent.View { + view.animateOutToCamera() + } + let transition = Transition(animation: .curve(duration: 0.25, curve: .easeInOut)) + transition.setAlpha(view: self.previewContainerView, alpha: 0.0, completion: { _ in + completion() + }) + } + } else { + completion() + } + } + + func animateOutToTool() { + if let view = self.componentHost.view as? MediaEditorScreenComponent.View { + view.animateOutToTool() + } + } + + func animateInFromTool() { + if let view = self.componentHost.view as? MediaEditorScreenComponent.View { + view.animateInFromTool() + } + } + + override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? { + let result = super.hitTest(point, with: event) + if result == self.componentHost.view { + self.controller?.view.endEditing(true) + let point = self.view.convert(point, to: self.previewContainerView) + return self.previewContainerView.hitTest(point, with: event) + } + return result + } + + private var drawingScreen: DrawingScreen? + func containerLayoutUpdated(layout: ContainerViewLayout, forceUpdate: Bool = false, animateOut: Bool = false, transition: Transition) { + guard let _ = self.controller else { + return + } + let isFirstTime = self.validLayout == nil + self.validLayout = layout + + let previewSize = CGSize(width: layout.size.width, height: floorToScreenPixels(layout.size.width * 1.77778)) + let topInset: CGFloat = floor(layout.size.height - previewSize.height) / 2.0 + + let environment = ViewControllerComponentContainer.Environment( + statusBarHeight: layout.statusBarHeight ?? 0.0, + navigationHeight: 0.0, + safeInsets: UIEdgeInsets( + top: topInset, + left: layout.safeInsets.left, + bottom: topInset, + right: layout.safeInsets.right + ), + inputHeight: layout.inputHeight ?? 0.0, + metrics: layout.metrics, + deviceMetrics: layout.deviceMetrics, + orientation: nil, + isVisible: true, + theme: self.presentationData.theme, + strings: self.presentationData.strings, + dateTimeFormat: self.presentationData.dateTimeFormat, + controller: { [weak self] in + return self?.controller + } + ) + + let componentSize = self.componentHost.update( + transition: transition, + component: AnyComponent( + MediaEditorScreenComponent( + context: self.context, + openDrawing: { [weak self] mode in + if let self { + let controller = DrawingScreen(context: self.context, sourceHint: .storyEditor, size: self.previewContainerView.frame.size, originalSize: storyDimensions, isVideo: false, isAvatar: false, drawingView: self.drawingView, entitiesView: self.entitiesView, existingStickerPickerInputData: self.stickerPickerInputData) + self.drawingScreen = controller + self.drawingView.isUserInteractionEnabled = true + + let selectionContainerView = controller.selectionContainerView + selectionContainerView.frame = self.previewContainerView.bounds + self.previewContainerView.addSubview(selectionContainerView) + + controller.requestDismiss = { [weak controller, weak self, weak selectionContainerView] in + self?.drawingScreen = nil + controller?.animateOut({ + controller?.dismiss() + }) + self?.drawingView.isUserInteractionEnabled = false + self?.animateInFromTool() + + selectionContainerView?.removeFromSuperview() + } + controller.requestApply = { [weak controller, weak self, weak selectionContainerView] in + self?.drawingScreen = nil + controller?.animateOut({ + controller?.dismiss() + }) + self?.drawingView.isUserInteractionEnabled = false + self?.animateInFromTool() + + selectionContainerView?.removeFromSuperview() + } + self.controller?.present(controller, in: .current) + + switch mode { + case .sticker: + controller.presentStickerSelection() + case .text: + Queue.mainQueue().after(0.05, { + controller.addTextEntity() + }) + default: + break + } + + self.animateOutToTool() + } + }, + openTools: { [weak self] in + if let self, let mediaEditor = self.mediaEditor { + let controller = MediaToolsScreen(context: self.context, mediaEditor: mediaEditor) + controller.dismissed = { [weak self] in + if let self { + self.animateInFromTool() + } + } + self.controller?.present(controller, in: .current) + self.animateOutToTool() + } + } + ) + ), + environment: { + environment + }, + forceUpdate: forceUpdate || animateOut, + containerSize: layout.size + ) + if let componentView = self.componentHost.view { + if componentView.superview == nil { + self.view.insertSubview(componentView, at: 3) + componentView.clipsToBounds = true + } + let componentFrame = CGRect(origin: .zero, size: componentSize) + transition.setFrame(view: componentView, frame: CGRect(origin: componentFrame.origin, size: CGSize(width: componentFrame.width, height: componentFrame.height))) + } + + var bottomInputOffset: CGFloat = 0.0 + if let inputHeight = layout.inputHeight, inputHeight > 0.0 { + bottomInputOffset = inputHeight - topInset + } + + transition.setFrame(view: self.backgroundDimView, frame: CGRect(origin: .zero, size: layout.size)) + + var previewFrame = CGRect(origin: CGPoint(x: 0.0, y: topInset - bottomInputOffset), size: previewSize) + if let inputHeight = layout.inputHeight, inputHeight > 0.0, self.drawingScreen != nil { + previewFrame = previewFrame.offsetBy(dx: 0.0, dy: inputHeight / 2.0) + } + + transition.setFrame(view: self.previewContainerView, frame: previewFrame) + let entitiesViewScale = previewSize.width / storyDimensions.width + self.entitiesContainerView.transform = CGAffineTransformMakeScale(entitiesViewScale, entitiesViewScale) + transition.setFrame(view: self.entitiesContainerView, frame: CGRect(origin: .zero, size: previewFrame.size)) + transition.setFrame(view: self.gradientView, frame: CGRect(origin: .zero, size: previewFrame.size)) + transition.setFrame(view: self.drawingView, frame: CGRect(origin: .zero, size: previewFrame.size)) + + if isFirstTime { + self.animateIn() + } + } + } + + fileprivate var node: Node { + return self.displayNode as! Node + } + + public enum Subject { + case image(UIImage, PixelDimensions) + case video(String, PixelDimensions) + case asset(PHAsset) + + var dimensions: PixelDimensions { + switch self { + case let .image(_, dimensions), let .video(_, dimensions): + return dimensions + case let .asset(asset): + return PixelDimensions(width: Int32(asset.pixelWidth), height: Int32(asset.pixelHeight)) + } + } + + var editorSubject: MediaEditor.Subject { + switch self { + case let .image(image, dimensions): + return .image(image, dimensions) + case let .video(videoPath, dimensions): + return .video(videoPath, dimensions) + case let .asset(asset): + return .asset(asset) + } + } + + var mediaContent: DrawingMediaEntity.Content { + switch self { + case let .image(image, dimensions): + return .image(image, dimensions) + case let .video(videoPath, dimensions): + return .video(videoPath, dimensions) + case let .asset(asset): + return .asset(asset) + } + } + } + + public enum Result { + case image(UIImage) + case video(String, UIImage?, MediaEditorValues) + } + + fileprivate let context: AccountContext + fileprivate let subject: Signal + + public enum SourceHint { + case camera + } + public var sourceHint: SourceHint? + + public var cancelled: () -> Void = {} + public var completion: (MediaEditorScreen.Result) -> Void = { _ in } + + public init(context: AccountContext, subject: Signal, completion: @escaping (MediaEditorScreen.Result) -> Void) { + self.context = context + self.subject = subject + self.completion = completion + + super.init(navigationBarPresentationData: nil) + self.navigationPresentation = .flatModal + + self.supportedOrientations = ViewControllerSupportedOrientations(regularSize: .all, compactSize: .portrait) + + self.statusBar.statusBarStyle = .White + } + + required public init(coder aDecoder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + override public func loadDisplayNode() { + self.displayNode = Node(controller: self) + + super.displayNodeDidLoad() + } + + func requestDismiss(animated: Bool) { + self.cancelled() + + self.node.animateOut(completion: { + self.dismiss() + }) + } + + func requestCompletion(animated: Bool) { + guard let mediaEditor = self.node.mediaEditor else { + return + } + + if mediaEditor.resultIsVideo { + + } else { + if let image = mediaEditor.resultImage { + self.completion(.image(image)) + } + } + + self.node.animateOut(completion: { + self.dismiss() + }) + } + + override public func containerLayoutUpdated(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) { + super.containerLayoutUpdated(layout, transition: transition) + + (self.displayNode as! Node).containerLayoutUpdated(layout: layout, transition: Transition(transition)) + } +} diff --git a/submodules/TelegramUI/Components/MediaEditorScreen/Sources/MediaToolsScreen.swift b/submodules/TelegramUI/Components/MediaEditorScreen/Sources/MediaToolsScreen.swift new file mode 100644 index 0000000000..df5a8638f7 --- /dev/null +++ b/submodules/TelegramUI/Components/MediaEditorScreen/Sources/MediaToolsScreen.swift @@ -0,0 +1,979 @@ +import Foundation +import UIKit +import Display +import AsyncDisplayKit +import ComponentFlow +import SwiftSignalKit +import ViewControllerComponent +import ComponentDisplayAdapters +import TelegramPresentationData +import AccountContext +import TelegramCore +import MultilineTextComponent +import DrawingUI +import MediaEditor +import Photos +import LottieAnimationComponent +import MessageInputPanelComponent + +private enum MediaToolsSection: Equatable { + case adjustments + case highlights + case blur + case curves +} + +private final class ToolIconComponent: Component { + typealias EnvironmentType = Empty + + let icon: UIImage? + let isActive: Bool + let isSelected: Bool + + init( + icon: UIImage?, + isActive: Bool, + isSelected: Bool + ) { + self.icon = icon + self.isActive = isActive + self.isSelected = isSelected + } + + static func ==(lhs: ToolIconComponent, rhs: ToolIconComponent) -> Bool { + if lhs.icon !== rhs.icon { + return false + } + if lhs.isActive != rhs.isActive { + return false + } + if lhs.isSelected != rhs.isSelected { + return false + } + return true + } + + final class View: UIView { + private let selection = SimpleShapeLayer() + private let icon = ComponentView() + + private var component: ToolIconComponent? + private weak var state: EmptyComponentState? + + override init(frame: CGRect) { + super.init(frame: frame) + + self.selection.path = UIBezierPath(roundedRect: CGRect(origin: .zero, size: CGSize(width: 33.0, height: 33.0)), cornerRadius: 10.0).cgPath + self.selection.fillColor = UIColor(rgb: 0xd1d1d1).cgColor + self.layer.addSublayer(self.selection) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + func update(component: ToolIconComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + self.component = component + self.state = state + + let iconColor: UIColor + if component.isSelected { + iconColor = .black + } else { + iconColor = component.isActive ? UIColor(rgb: 0xf8d74a) : .white + } + + let iconSize = self.icon.update( + transition: transition, + component: AnyComponent( + Image( + image: component.icon, + tintColor: iconColor, + size: CGSize(width: 30.0, height: 30.0) + ) + ), + environment: {}, + containerSize: availableSize + ) + + let size = CGSize(width: 33.0, height: 33.0) + let iconFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((size.width - iconSize.width) / 2.0), y: floorToScreenPixels((size.height - iconSize.height) / 2.0)), size: iconSize) + if let view = self.icon.view { + if view.superview == nil { + self.addSubview(view) + } + transition.setFrame(view: view, frame: iconFrame) + } + + self.selection.isHidden = !component.isSelected + + return size + } + } + + public func makeView() -> View { + return View(frame: CGRect()) + } + + public func update(view: View, availableSize: CGSize, state: State, environment: Environment, transition: Transition) -> CGSize { + return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition) + } +} + + +private final class MediaToolsScreenComponent: Component { + typealias EnvironmentType = ViewControllerComponentContainer.Environment + + let context: AccountContext + let mediaEditor: MediaEditor + let section: MediaToolsSection + let sectionUpdated: (MediaToolsSection) -> Void + + init( + context: AccountContext, + mediaEditor: MediaEditor, + section: MediaToolsSection, + sectionUpdated: @escaping (MediaToolsSection) -> Void + ) { + self.context = context + self.mediaEditor = mediaEditor + self.section = section + self.sectionUpdated = sectionUpdated + } + + static func ==(lhs: MediaToolsScreenComponent, rhs: MediaToolsScreenComponent) -> Bool { + if lhs.context !== rhs.context { + return false + } + if lhs.section != rhs.section { + return false + } + return true + } + + final class State: ComponentState { + enum ImageKey: Hashable { + case adjustments + case highlights + case blur + case curves + case done + } + private var cachedImages: [ImageKey: UIImage] = [:] + func image(_ key: ImageKey) -> UIImage { + if let image = self.cachedImages[key] { + return image + } else { + var image: UIImage + switch key { + case .adjustments: + image = UIImage(bundleImageName: "Media Editor/Tools")! + case .highlights: + image = UIImage(bundleImageName: "Media Editor/Tint")! + case .blur: + image = UIImage(bundleImageName: "Media Editor/Blur")! + case .curves: + image = UIImage(bundleImageName: "Media Editor/Curves")! + case .done: + image = generateTintedImage(image: UIImage(bundleImageName: "Media Editor/Done"), color: .white)! + } + cachedImages[key] = image + return image + } + } + + let context: AccountContext + var histogram: MediaEditorHistogram? + private var histogramDisposable: Disposable? + + init(context: AccountContext, mediaEditor: MediaEditor) { + self.context = context + + super.init() + + self.histogramDisposable = (mediaEditor.histogram + |> deliverOnMainQueue).start(next: { [weak self] data in + if let self { + self.histogram = MediaEditorHistogram(data: data) + self.updated() + } + }) + } + + deinit { + self.histogramDisposable?.dispose() + } + } + + func makeState() -> State { + return State( + context: self.context, + mediaEditor: self.mediaEditor + ) + } + + public final class View: UIView { + private let buttonsContainerView = UIView() + private let cancelButton = ComponentView() + private let adjustmentsButton = ComponentView() + private let highlightsButton = ComponentView() + private let blurButton = ComponentView() + private let curvesButton = ComponentView() + private let doneButton = ComponentView() + + private let previewContainerView = UIView() + private var optionsContainerView = UIView() + private var toolOptions = ComponentView() + private var toolScreen: ComponentView? + + private var curvesState: CurvesInternalState? + + private var component: MediaToolsScreenComponent? + private weak var state: State? + private var environment: ViewControllerComponentContainer.Environment? + + override init(frame: CGRect) { + self.buttonsContainerView.clipsToBounds = true + self.previewContainerView.clipsToBounds = true + + self.optionsContainerView.clipsToBounds = true + self.optionsContainerView.backgroundColor = UIColor(rgb: 0x000000, alpha: 0.9) + + super.init(frame: frame) + + self.backgroundColor = .clear + + self.addSubview(self.buttonsContainerView) + self.addSubview(self.previewContainerView) + self.previewContainerView.addSubview(self.optionsContainerView) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + func animateInFromEditor() { + let buttons = [ + self.adjustmentsButton, + self.highlightsButton, + self.blurButton, + self.curvesButton + ] + + var delay: Double = 0.0 + for button in buttons { + if let view = button.view { + view.layer.animatePosition(from: CGPoint(x: 0.0, y: 64.0), to: .zero, duration: 0.3, delay: delay, timingFunction: kCAMediaTimingFunctionSpring, additive: true) + view.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2, delay: delay) + view.layer.animateScale(from: 0.1, to: 1.0, duration: 0.2, delay: delay) + delay += 0.05 + } + } + + if let view = self.doneButton.view { + view.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2) + view.layer.animateScale(from: 0.1, to: 1.0, duration: 0.2) + } + + self.optionsContainerView.layer.animatePosition(from: CGPoint(x: 0.0, y: self.optionsContainerView.frame.height), to: .zero, duration: 0.3, timingFunction: kCAMediaTimingFunctionSpring, removeOnCompletion: false, additive: true) + } + + private var animatingOut = false + func animateOutToEditor(completion: @escaping () -> Void) { + self.animatingOut = true + + let buttons = [ + self.adjustmentsButton, + self.highlightsButton, + self.blurButton, + self.curvesButton + ] + + for button in buttons { + if let view = button.view { + view.layer.animatePosition(from: .zero, to: CGPoint(x: 0.0, y: 64.0), duration: 0.3, timingFunction: kCAMediaTimingFunctionSpring, additive: true, completion: { _ in + completion() + }) + view.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false) + view.layer.animateScale(from: 1.0, to: 0.1, duration: 0.2) + } + } + + if let view = self.doneButton.view { + view.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false) + view.layer.animateScale(from: 1.0, to: 0.1, duration: 0.2) + } + + if let view = self.toolScreen?.view { + view.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false) + } + + self.optionsContainerView.layer.animatePosition(from: .zero, to: CGPoint(x: 0.0, y: self.optionsContainerView.frame.height), duration: 0.3, timingFunction: kCAMediaTimingFunctionSpring, removeOnCompletion: false, additive: true) + + self.state?.updated() + } + + func update(component: MediaToolsScreenComponent, availableSize: CGSize, state: State, environment: Environment, transition: Transition) -> CGSize { + let environment = environment[ViewControllerComponentContainer.Environment.self].value + self.environment = environment + + let previousSection = self.component?.section + self.component = component + self.state = state + + let mediaEditor = (environment.controller() as? MediaToolsScreen)?.mediaEditor + + let sectionUpdated = component.sectionUpdated + + let previewContainerFrame = CGRect(origin: CGPoint(x: 0.0, y: environment.safeInsets.top), size: CGSize(width: availableSize.width, height: availableSize.height - environment.safeInsets.top - environment.safeInsets.bottom)) + let buttonsContainerFrame = CGRect(origin: CGPoint(x: 0.0, y: availableSize.height - environment.safeInsets.bottom), size: CGSize(width: availableSize.width, height: environment.safeInsets.bottom)) + + let buttonSideInset: CGFloat = 10.0 + let buttonBottomInset: CGFloat = 8.0 + + let cancelButtonSize = self.cancelButton.update( + transition: transition, + component: AnyComponent(Button( + content: AnyComponent( + LottieAnimationComponent( + animation: LottieAnimationComponent.AnimationItem( + name: "media_backToCancel", + mode: .animating(loop: false), + range: self.animatingOut ? (0.5, 1.0) : (0.0, 0.5) + ), + colors: ["__allcolors__": .white], + size: CGSize(width: 33.0, height: 33.0) + ) + ), + action: { + guard let controller = environment.controller() as? MediaToolsScreen else { + return + } + controller.requestDismiss(animated: true) + } + )), + environment: {}, + containerSize: CGSize(width: 44.0, height: 44.0) + ) + let cancelButtonFrame = CGRect( + origin: CGPoint(x: buttonSideInset, y: buttonBottomInset), + size: cancelButtonSize + ) + if let cancelButtonView = self.cancelButton.view { + if cancelButtonView.superview == nil { + self.buttonsContainerView.addSubview(cancelButtonView) + } + transition.setFrame(view: cancelButtonView, frame: cancelButtonFrame) + } + + let doneButtonSize = self.doneButton.update( + transition: transition, + component: AnyComponent(Button( + content: AnyComponent(Image( + image: state.image(.done), + size: CGSize(width: 33.0, height: 33.0) + )), + action: { + guard let controller = environment.controller() as? MediaToolsScreen else { + return + } + controller.requestDismiss(animated: true) + } + )), + environment: {}, + containerSize: CGSize(width: 44.0, height: 44.0) + ) + let doneButtonFrame = CGRect( + origin: CGPoint(x: availableSize.width - buttonSideInset - doneButtonSize.width, y: buttonBottomInset), + size: doneButtonSize + ) + if let doneButtonView = self.doneButton.view { + if doneButtonView.superview == nil { + self.buttonsContainerView.addSubview(doneButtonView) + } + transition.setFrame(view: doneButtonView, frame: doneButtonFrame) + } + + let adjustmentsButtonSize = self.adjustmentsButton.update( + transition: transition, + component: AnyComponent(Button( + content: AnyComponent(ToolIconComponent( + icon: state.image(.adjustments), + isActive: false, + isSelected: component.section == .adjustments + )), + action: { + sectionUpdated(.adjustments) + } + )), + environment: {}, + containerSize: CGSize(width: 40.0, height: 40.0) + ) + let adjustmentsButtonFrame = CGRect( + origin: CGPoint(x: floorToScreenPixels(availableSize.width / 4.0 - 3.0 - adjustmentsButtonSize.width / 2.0), y: buttonBottomInset), + size: adjustmentsButtonSize + ) + if let adjustmentsButtonView = self.adjustmentsButton.view { + if adjustmentsButtonView.superview == nil { + self.buttonsContainerView.addSubview(adjustmentsButtonView) + } + transition.setFrame(view: adjustmentsButtonView, frame: adjustmentsButtonFrame) + } + + let highlightsButtonSize = self.highlightsButton.update( + transition: transition, + component: AnyComponent(Button( + content: AnyComponent(ToolIconComponent( + icon: state.image(.highlights), + isActive: false, + isSelected: component.section == .highlights + )), + action: { + sectionUpdated(.highlights) + } + )), + environment: {}, + containerSize: CGSize(width: 40.0, height: 40.0) + ) + let highlightsButtonFrame = CGRect( + origin: CGPoint(x: floorToScreenPixels(availableSize.width / 2.5 + 5.0 - highlightsButtonSize.width / 2.0), y: buttonBottomInset), + size: highlightsButtonSize + ) + if let highlightsButtonView = self.highlightsButton.view { + if highlightsButtonView.superview == nil { + self.buttonsContainerView.addSubview(highlightsButtonView) + } + transition.setFrame(view: highlightsButtonView, frame: highlightsButtonFrame) + } + + let blurButtonSize = self.blurButton.update( + transition: transition, + component: AnyComponent(Button( + content: AnyComponent(ToolIconComponent( + icon: state.image(.blur), + isActive: false, + isSelected: component.section == .blur + )), + action: { + sectionUpdated(.blur) + } + )), + environment: {}, + containerSize: CGSize(width: 40.0, height: 40.0) + ) + let blurButtonFrame = CGRect( + origin: CGPoint(x: floorToScreenPixels(availableSize.width - availableSize.width / 2.5 - 5.0 - blurButtonSize.width / 2.0), y: buttonBottomInset), + size: blurButtonSize + ) + if let blurButtonView = self.blurButton.view { + if blurButtonView.superview == nil { + self.buttonsContainerView.addSubview(blurButtonView) + } + transition.setFrame(view: blurButtonView, frame: blurButtonFrame) + } + + let curvesButtonSize = self.curvesButton.update( + transition: transition, + component: AnyComponent(Button( + content: AnyComponent(ToolIconComponent( + icon: state.image(.curves), + isActive: false, + isSelected: component.section == .curves + )), + action: { + sectionUpdated(.curves) + } + )), + environment: {}, + containerSize: CGSize(width: 40.0, height: 40.0) + ) + let curvesButtonFrame = CGRect( + origin: CGPoint(x: floorToScreenPixels(availableSize.width / 4.0 * 3.0 + 3.0 - curvesButtonSize.width / 2.0), y: buttonBottomInset), + size: curvesButtonSize + ) + if let curvesButtonView = self.curvesButton.view { + if curvesButtonView.superview == nil { + self.buttonsContainerView.addSubview(curvesButtonView) + } + transition.setFrame(view: curvesButtonView, frame: curvesButtonFrame) + } + + var sectionChanged = false + if previousSection != component.section { + sectionChanged = true + if let previousView = self.toolOptions.view { + previousView.layer.allowsGroupOpacity = true + previousView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.25, removeOnCompletion: false, completion: { [weak previousView] _ in + previousView?.removeFromSuperview() + }) + } + self.toolOptions = ComponentView() + } + + var toolScreen: ComponentView? + + if sectionChanged && previousSection != nil, let view = self.toolScreen?.view { + view.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.25, removeOnCompletion: false, completion: { [weak view] _ in + view?.removeFromSuperview() + }) + } + + let screenSize: CGSize + let optionsSize: CGSize + let optionsTransition: Transition = sectionChanged ? .immediate : transition + switch component.section { + case .adjustments: + self.curvesState = nil + let tools: [AdjustmentTool] = [ + AdjustmentTool( + key: .enhance, + title: "Enhance", + value: mediaEditor?.getToolValue(.enhance) as? Float ?? 0.0, + minValue: 0.0, + maxValue: 1.0, + startValue: 0.0 + ), + AdjustmentTool( + key: .brightness, + title: "Brightness", + value: mediaEditor?.getToolValue(.brightness) as? Float ?? 0.0, + minValue: -1.0, + maxValue: 1.0, + startValue: 0.0 + ), + AdjustmentTool( + key: .contrast, + title: "Contrast", + value: mediaEditor?.getToolValue(.contrast) as? Float ?? 0.0, + minValue: -1.0, + maxValue: 1.0, + startValue: 0.0 + ), + AdjustmentTool( + key: .saturation, + title: "Saturation", + value: mediaEditor?.getToolValue(.saturation) as? Float ?? 0.0, + minValue: -1.0, + maxValue: 1.0, + startValue: 0.0 + ), + AdjustmentTool( + key: .warmth, + title: "Warmth", + value: mediaEditor?.getToolValue(.warmth) as? Float ?? 0.0, + minValue: -1.0, + maxValue: 1.0, + startValue: 0.0 + ), + AdjustmentTool( + key: .fade, + title: "Fade", + value: mediaEditor?.getToolValue(.fade) as? Float ?? 0.0, + minValue: 0.0, + maxValue: 1.0, + startValue: 0.0 + ), + AdjustmentTool( + key: .highlights, + title: "Highlights", + value: mediaEditor?.getToolValue(.highlights) as? Float ?? 0.0, + minValue: -1.0, + maxValue: 1.0, + startValue: 0.0 + ), + AdjustmentTool( + key: .shadows, + title: "Shadows", + value: mediaEditor?.getToolValue(.shadows) as? Float ?? 0.0, + minValue: -1.0, + maxValue: 1.0, + startValue: 0.0 + ), + AdjustmentTool( + key: .vignette, + title: "Vignette", + value: mediaEditor?.getToolValue(.vignette) as? Float ?? 0.0, + minValue: 0.0, + maxValue: 1.0, + startValue: 0.0 + ), + AdjustmentTool( + key: .grain, + title: "Grain", + value: mediaEditor?.getToolValue(.grain) as? Float ?? 0.0, + minValue: 0.0, + maxValue: 1.0, + startValue: 0.0 + ), + AdjustmentTool( + key: .sharpen, + title: "Sharpen", + value: mediaEditor?.getToolValue(.sharpen) as? Float ?? 0.0, + minValue: 0.0, + maxValue: 1.0, + startValue: 0.0 + ) + ] + optionsSize = self.toolOptions.update( + transition: optionsTransition, + component: AnyComponent(AdjustmentsComponent( + tools: tools, + valueUpdated: { [weak state] key, value in + if let controller = environment.controller() as? MediaToolsScreen { + controller.mediaEditor.setToolValue(key, value: value) + state?.updated() + } + } + )), + environment: {}, + containerSize: availableSize + ) + screenSize = previewContainerFrame.size + self.toolScreen = nil + case .highlights: + self.curvesState = nil + optionsSize = self.toolOptions.update( + transition: optionsTransition, + component: AnyComponent(TintComponent( + shadowsValue: mediaEditor?.getToolValue(.shadowsTint) as? TintValue ?? TintValue.initial, + highlightsValue: mediaEditor?.getToolValue(.highlightsTint) as? TintValue ?? TintValue.initial, + shadowsValueUpdated: { [weak state] value in + if let controller = environment.controller() as? MediaToolsScreen { + controller.mediaEditor.setToolValue(.shadowsTint, value: value) + state?.updated() + } + }, + highlightsValueUpdated: { [weak state] value in + if let controller = environment.controller() as? MediaToolsScreen { + controller.mediaEditor.setToolValue(.highlightsTint, value: value) + state?.updated() + } + } + )), + environment: {}, + containerSize: availableSize + ) + screenSize = previewContainerFrame.size + self.toolScreen = nil + case .blur: + self.curvesState = nil + optionsSize = self.toolOptions.update( + transition: optionsTransition, + component: AnyComponent(BlurComponent( + value: mediaEditor?.getToolValue(.blur) as? BlurValue ?? BlurValue.initial, + hasPortrait: mediaEditor?.hasPortraitMask ?? false, + valueUpdated: { [weak state] value in + if let controller = environment.controller() as? MediaToolsScreen { + controller.mediaEditor.setToolValue(.blur, value: value) + state?.updated() + } + } + )), + environment: {}, + containerSize: availableSize + ) + + let blurToolScreen: ComponentView + if let current = self.toolScreen, !sectionChanged { + blurToolScreen = current + } else { + blurToolScreen = ComponentView() + self.toolScreen = blurToolScreen + } + toolScreen = blurToolScreen + screenSize = blurToolScreen.update( + transition: optionsTransition, + component: AnyComponent( + BlurScreenComponent( + value: mediaEditor?.getToolValue(.blur) as? BlurValue ?? BlurValue.initial, + valueUpdated: { [weak state] value in + if let controller = environment.controller() as? MediaToolsScreen { + controller.mediaEditor.setToolValue(.blur, value: value) + state?.updated() + } + } + ) + ), + environment: {}, + containerSize: CGSize(width: previewContainerFrame.width, height: previewContainerFrame.height - optionsSize.height) + ) + case .curves: + let internalState: CurvesInternalState + if let current = self.curvesState { + internalState = current + } else { + internalState = CurvesInternalState() + self.curvesState = internalState + } + self.toolOptions.parentState = state + optionsSize = self.toolOptions.update( + transition: optionsTransition, + component: AnyComponent(CurvesComponent( + histogram: state.histogram, + internalState: internalState + )), + environment: {}, + containerSize: availableSize + ) + + let curvesToolScreen: ComponentView + if let current = self.toolScreen, !sectionChanged { + curvesToolScreen = current + } else { + curvesToolScreen = ComponentView() + self.toolScreen = curvesToolScreen + } + toolScreen = curvesToolScreen + screenSize = curvesToolScreen.update( + transition: optionsTransition, + component: AnyComponent( + CurvesScreenComponent( + value: mediaEditor?.getToolValue(.curves) as? CurvesValue ?? CurvesValue.initial, + section: internalState.section, + valueUpdated: { [weak state] value in + if let controller = environment.controller() as? MediaToolsScreen { + controller.mediaEditor.setToolValue(.curves, value: value) + state?.updated() + } + } + ) + ), + environment: {}, + containerSize: CGSize(width: previewContainerFrame.width, height: previewContainerFrame.height - optionsSize.height) + ) + } + + let optionsFrame = CGRect(origin: .zero, size: optionsSize) + if let optionsView = self.toolOptions.view { + if optionsView.superview == nil { + self.optionsContainerView.addSubview(optionsView) + } + optionsTransition.setFrame(view: optionsView, frame: optionsFrame) + + if sectionChanged && previousSection != nil { + optionsView.layer.allowsGroupOpacity = true + optionsView.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.25, completion: { _ in + optionsView.layer.allowsGroupOpacity = false + }) + } + } + + let optionsBackgroundFrame = CGRect( + origin: CGPoint(x: 0.0, y: previewContainerFrame.height - optionsSize.height), + size: optionsSize + ) + transition.setFrame(view: self.optionsContainerView, frame: optionsBackgroundFrame) + + if let toolScreen = toolScreen { + let screenFrame = CGRect(origin: .zero, size: screenSize) + if let screenView = toolScreen.view { + if screenView.superview == nil { + self.previewContainerView.insertSubview(screenView, at: 0) + + screenView.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.25) + } + optionsTransition.setFrame(view: screenView, frame: screenFrame) + } + } + + transition.setFrame(view: self.previewContainerView, frame: previewContainerFrame) + transition.setFrame(view: self.buttonsContainerView, frame: buttonsContainerFrame) + + return availableSize + } + } + + func makeView() -> View { + return View() + } + + public func update(view: View, availableSize: CGSize, state: State, environment: Environment, transition: Transition) -> CGSize { + return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition) + } +} + +private let storyDimensions = CGSize(width: 1080.0, height: 1920.0) + +public final class MediaToolsScreen: ViewController { + fileprivate final class Node: ViewControllerTracingNode, UIGestureRecognizerDelegate { + private weak var controller: MediaToolsScreen? + private let context: AccountContext + + fileprivate let componentHost: ComponentView + private var currentSection: MediaToolsSection = .adjustments + + private var presentationData: PresentationData + private var validLayout: ContainerViewLayout? + + init(controller: MediaToolsScreen) { + self.controller = controller + self.context = controller.context + + self.presentationData = self.context.sharedContext.currentPresentationData.with { $0 } + + self.componentHost = ComponentView() + + super.init() + + self.backgroundColor = .clear + } + + override func didLoad() { + super.didLoad() + + self.view.disablesInteractiveModalDismiss = true + self.view.disablesInteractiveKeyboardGestureRecognizer = true + +// let panGestureRecognizer = UIPanGestureRecognizer(target: self, action: #selector(self.handlePan(_:))) +// panGestureRecognizer.delegate = self +// panGestureRecognizer.minimumNumberOfTouches = 2 +// panGestureRecognizer.maximumNumberOfTouches = 2 +// self.previewContainerView.addGestureRecognizer(panGestureRecognizer) +// +// let pinchGestureRecognizer = UIPinchGestureRecognizer(target: self, action: #selector(self.handlePinch(_:))) +// pinchGestureRecognizer.delegate = self +// self.previewContainerView.addGestureRecognizer(pinchGestureRecognizer) +// +// let rotateGestureRecognizer = UIRotationGestureRecognizer(target: self, action: #selector(self.handleRotate(_:))) +// rotateGestureRecognizer.delegate = self +// self.previewContainerView.addGestureRecognizer(rotateGestureRecognizer) + } + + @objc func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool { + return true + } + + func animateInFromEditor() { + if let view = self.componentHost.view as? MediaToolsScreenComponent.View { + view.animateInFromEditor() + } + } + + func animateOutToEditor(completion: @escaping () -> Void) { + if let view = self.componentHost.view as? MediaToolsScreenComponent.View { + view.animateOutToEditor(completion: completion) + } + } + + func containerLayoutUpdated(layout: ContainerViewLayout, forceUpdate: Bool = false, animateOut: Bool = false, transition: Transition) { + guard let controller = self.controller else { + return + } + let isFirstTime = self.validLayout == nil + self.validLayout = layout + + let previewSize = CGSize(width: layout.size.width, height: floorToScreenPixels(layout.size.width * 1.77778)) + let topInset: CGFloat = floor(layout.size.height - previewSize.height) / 2.0 + + let environment = ViewControllerComponentContainer.Environment( + statusBarHeight: layout.statusBarHeight ?? 0.0, + navigationHeight: 0.0, + safeInsets: UIEdgeInsets( + top: topInset, + left: layout.safeInsets.left, + bottom: topInset, + right: layout.safeInsets.right + ), + inputHeight: layout.inputHeight ?? 0.0, + metrics: layout.metrics, + deviceMetrics: layout.deviceMetrics, + orientation: nil, + isVisible: true, + theme: self.presentationData.theme, + strings: self.presentationData.strings, + dateTimeFormat: self.presentationData.dateTimeFormat, + controller: { [weak self] in + return self?.controller + } + ) + +// var transition = transition +// if isFirstTime { +// transition = transition.withUserData(CameraScreenTransition.animateIn) +// } else if animateOut { +// transition = transition.withUserData(CameraScreenTransition.animateOut) +// } + + let componentSize = self.componentHost.update( + transition: transition, + component: AnyComponent( + MediaToolsScreenComponent( + context: self.context, + mediaEditor: controller.mediaEditor, + section: self.currentSection, + sectionUpdated: { [weak self] section in + if let self { + self.currentSection = section + if let layout = self.validLayout { + self.containerLayoutUpdated(layout: layout, transition: Transition(animation: .curve(duration: 0.3, curve: .spring))) + } + } + } + ) + ), + environment: { + environment + }, + forceUpdate: forceUpdate || animateOut, + containerSize: layout.size + ) + if let componentView = self.componentHost.view { + if componentView.superview == nil { + self.view.insertSubview(componentView, at: 3) + componentView.clipsToBounds = true + } + let componentFrame = CGRect(origin: .zero, size: componentSize) + transition.setFrame(view: componentView, frame: CGRect(origin: componentFrame.origin, size: CGSize(width: componentFrame.width, height: componentFrame.height))) + } + + if isFirstTime { + self.animateInFromEditor() + } + } + } + + fileprivate var node: Node { + return self.displayNode as! Node + } + + fileprivate let context: AccountContext + fileprivate let mediaEditor: MediaEditor + + public var dismissed: () -> Void = {} + + public init(context: AccountContext, mediaEditor: MediaEditor) { + self.context = context + self.mediaEditor = mediaEditor + + super.init(navigationBarPresentationData: nil) + self.navigationPresentation = .flatModal + + self.supportedOrientations = ViewControllerSupportedOrientations(regularSize: .all, compactSize: .portrait) + + self.statusBar.statusBarStyle = .White + } + + required public init(coder aDecoder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + override public func loadDisplayNode() { + self.displayNode = Node(controller: self) + + super.displayNodeDidLoad() + } + + func requestDismiss(animated: Bool) { + self.dismissed() + + self.node.animateOutToEditor(completion: { + self.dismiss() + }) + } + + override public func containerLayoutUpdated(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) { + super.containerLayoutUpdated(layout, transition: transition) + + (self.displayNode as! Node).containerLayoutUpdated(layout: layout, transition: Transition(transition)) + } +} diff --git a/submodules/TelegramUI/Components/MediaEditorScreen/Sources/TintComponent.swift b/submodules/TelegramUI/Components/MediaEditorScreen/Sources/TintComponent.swift new file mode 100644 index 0000000000..8276e9f3d1 --- /dev/null +++ b/submodules/TelegramUI/Components/MediaEditorScreen/Sources/TintComponent.swift @@ -0,0 +1,370 @@ +import Foundation +import UIKit +import Display +import ComponentFlow +import LegacyComponents +import MediaEditor + +private final class TintColorComponent: Component { + typealias EnvironmentType = Empty + + let color: UIColor + let isSelected: Bool + + init( + color: UIColor, + isSelected: Bool + ) { + self.color = color + self.isSelected = isSelected + } + + static func ==(lhs: TintColorComponent, rhs: TintColorComponent) -> Bool { + if lhs.color != rhs.color { + return false + } + if lhs.isSelected != rhs.isSelected { + return false + } + return true + } + + final class View: UIView { + private var background = SimpleShapeLayer() + private var selection = SimpleShapeLayer() + + private var component: TintColorComponent? + private weak var state: EmptyComponentState? + + private let size = CGSize(width: 24.0, height: 24.0) + + override init(frame: CGRect) { + super.init(frame: frame) + + self.background.path = CGPath(ellipseIn: CGRect(origin: .zero, size: size).insetBy(dx: 3.0, dy: 3.0), transform: nil) + + let lineWidth = 1.0 + UIScreenPixel + self.selection.lineWidth = lineWidth + self.selection.path = CGPath(ellipseIn: CGRect(origin: .zero, size: size).insetBy(dx: lineWidth / 2.0, dy: lineWidth / 2.0), transform: nil) + + self.layer.addSublayer(self.selection) + self.layer.addSublayer(self.background) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + func update(component: TintColorComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + self.component = component + self.state = state + + let size = CGSize(width: 24.0, height: 24.0) + let bounds = CGRect(origin: .zero, size: size) + + let color: UIColor + let selectionColor: UIColor + if component.color == .clear { + if component.isSelected { + color = UIColor(rgb: 0x000000) + } else { + color = UIColor(rgb: 0x1c1f22) + } + selectionColor = UIColor(rgb: 0x808080) + } else { + color = component.color + selectionColor = component.color + } + + self.background.fillColor = color.cgColor + self.selection.strokeColor = selectionColor.cgColor + self.selection.fillColor = UIColor.clear.cgColor + + self.background.frame = bounds + self.selection.frame = bounds + + self.selection.isHidden = !component.isSelected + + return size + } + } + + public func makeView() -> View { + return View(frame: CGRect()) + } + + public func update(view: View, availableSize: CGSize, state: State, environment: Environment, transition: Transition) -> CGSize { + return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition) + } +} + +final class TintComponent: Component { + enum Section { + case shadows + case highlights + } + + typealias EnvironmentType = Empty + + let shadowsValue: TintValue + let highlightsValue: TintValue + let shadowsValueUpdated: (TintValue) -> Void + let highlightsValueUpdated: (TintValue) -> Void + + init( + shadowsValue: TintValue, + highlightsValue: TintValue, + shadowsValueUpdated: @escaping (TintValue) -> Void, + highlightsValueUpdated: @escaping (TintValue) -> Void + ) { + self.shadowsValue = shadowsValue + self.highlightsValue = highlightsValue + self.shadowsValueUpdated = shadowsValueUpdated + self.highlightsValueUpdated = highlightsValueUpdated + } + + static func ==(lhs: TintComponent, rhs: TintComponent) -> Bool { + if lhs.highlightsValue != rhs.highlightsValue { + return false + } + if lhs.shadowsValue != rhs.shadowsValue { + return false + } + return true + } + + final class State: ComponentState { + var section: Section + var shadowsValue: TintValue + var highlightsValue: TintValue + + init(section: Section, shadowsValue: TintValue, highlightsValue: TintValue) { + self.section = section + self.shadowsValue = shadowsValue + self.highlightsValue = highlightsValue + } + } + + func makeState() -> State { + return State(section: .shadows, shadowsValue: self.shadowsValue, highlightsValue: self.highlightsValue) + } + + final class View: UIView { + private var shadowsButton = ComponentView() + private var highlightsButton = ComponentView() + private var colorViews: [ComponentView] = [] + private var slider = ComponentView() + + private var component: TintComponent? + private weak var state: State? + + override init(frame: CGRect) { + super.init(frame: frame) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + func update(component: TintComponent, availableSize: CGSize, state: State, environment: Environment, transition: Transition) -> CGSize { + self.component = component + self.state = state + state.shadowsValue = component.shadowsValue + state.highlightsValue = component.highlightsValue + + let shadowsValueUpdated = component.shadowsValueUpdated + let highlightsValueUpdated = component.highlightsValueUpdated + + let topInset: CGFloat = 11.0 + let shadowsButtonSize = self.shadowsButton.update( + transition: transition, + component: AnyComponent( + Button( + content: AnyComponent( + Text( + text: "Shadows", + font: Font.regular(14.0), + color: state.section == .shadows ? .white : UIColor(rgb: 0x808080) + ) + ), + action: { [weak state] in + state?.section = .shadows + state?.updated() + } + ) + ), + environment: {}, + containerSize: availableSize + ) + let shadowsButtonFrame = CGRect(origin: CGPoint(x: floorToScreenPixels(availableSize.width / 3.0 - shadowsButtonSize.width / 2.0), y: topInset), size: shadowsButtonSize) + if let view = self.shadowsButton.view { + if view.superview == nil { + self.addSubview(view) + } + transition.setFrame(view: view, frame: shadowsButtonFrame) + } + + let highlightsButtonSize = self.highlightsButton.update( + transition: transition, + component: AnyComponent( + Button( + content: AnyComponent( + Text( + text: "Highlights", + font: Font.regular(14.0), + color: state.section == .highlights ? .white : UIColor(rgb: 0x808080) + ) + ), + action: { [weak state] in + state?.section = .highlights + state?.updated() + } + ) + ), + environment: {}, + containerSize: availableSize + ) + let highlightsButtonFrame = CGRect(origin: CGPoint(x: floorToScreenPixels(availableSize.width / 3.0 * 2.0 - highlightsButtonSize.width / 2.0), y: topInset), size: highlightsButtonSize) + if let view = self.highlightsButton.view { + if view.superview == nil { + self.addSubview(view) + } + transition.setFrame(view: view, frame: highlightsButtonFrame) + } + + let currentColor: UIColor + let colors: [UIColor] + switch state.section { + case .shadows: + currentColor = component.shadowsValue.color + colors = [ + UIColor.clear, + UIColor(rgb: 0xff4d4d), + UIColor(rgb: 0xf48022), + UIColor(rgb: 0xffcd00), + UIColor(rgb: 0x81d281), + UIColor(rgb: 0x71c5d6), + UIColor(rgb: 0x0072bc), + UIColor(rgb: 0x662d91) + ] + case .highlights: + currentColor = component.highlightsValue.color + colors = [ + UIColor.clear, + UIColor(rgb: 0xef9286), + UIColor(rgb: 0xeacea2), + UIColor(rgb: 0xf2e17c), + UIColor(rgb: 0xa4edae), + UIColor(rgb: 0x89dce5), + UIColor(rgb: 0x2e8bc8), + UIColor(rgb: 0xcd98e5) + ] + } + + var sizes: [CGSize] = [] + for i in 0 ..< colors.count { + let color = colors[i] + let componentView: ComponentView + if i >= self.colorViews.count { + componentView = ComponentView() + self.colorViews.append(componentView) + } else { + componentView = self.colorViews[i] + } + + let size = componentView.update( + transition: transition, + component: AnyComponent( + Button( + content: AnyComponent( + TintColorComponent( + color: color, + isSelected: color == currentColor + ) + ), + action: { [weak state] in + if let state { + switch state.section { + case .shadows: + shadowsValueUpdated(state.shadowsValue.withUpdatedColor(color)) + case .highlights: + highlightsValueUpdated(state.highlightsValue.withUpdatedColor(color)) + } + } + } + ) + ), + environment: {}, + containerSize: availableSize + ) + sizes.append(size) + } + + let sliderSize = self.slider.update( + transition: transition, + component: AnyComponent( + AdjustmentSliderComponent( + title: "", + value: state.section == .shadows ? component.shadowsValue.intensity : component.highlightsValue.intensity, + minValue: 0.0, + maxValue: 1.0, + startValue: 0.0, + isEnabled: currentColor != .clear, + trackColor: currentColor != .clear ? currentColor : .white, + updateValue: { [weak state] value in + if let state { + switch state.section { + case .shadows: + shadowsValueUpdated(state.shadowsValue.withUpdatedIntensity(value)) + case .highlights: + highlightsValueUpdated(state.highlightsValue.withUpdatedIntensity(value)) + } + } + } + ) + ), + environment: {}, + containerSize: availableSize + ) + + let colorsVerticalSpacing: CGFloat = 9.0 + let leftInset: CGFloat = 30.0 + let itemSpacing = min(33.0, floorToScreenPixels((availableSize.width - leftInset * 2.0 - sizes.first!.width * CGFloat(colors.count)) / CGFloat(colors.count - 1))) + let finalLeftInset: CGFloat = floorToScreenPixels((availableSize.width - ((sizes.first!.width + itemSpacing) * CGFloat(colors.count) - itemSpacing)) / 2.0) + + var origin: CGPoint = CGPoint(x: finalLeftInset, y: topInset + highlightsButtonSize.height + colorsVerticalSpacing) + for i in 0 ..< colors.count { + let size = sizes[i] + let componentView = self.colorViews[i] + + if let view = componentView.view { + if view.superview == nil { + self.addSubview(view) + } + transition.setFrame(view: view, frame: CGRect(origin: origin, size: size)) + } + origin = origin.offsetBy(dx: size.width + itemSpacing, dy: 0.0) + } + + let verticalSpacing: CGFloat = 3.0 + let sliderFrame = CGRect(origin: CGPoint(x: 0.0, y: topInset + highlightsButtonSize.height + verticalSpacing + sizes.first!.height + verticalSpacing), size: sliderSize) + if let view = self.slider.view { + if view.superview == nil { + self.addSubview(view) + } + transition.setFrame(view: view, frame: sliderFrame) + } + + return CGSize(width: availableSize.width, height: topInset + highlightsButtonSize.height + colorsVerticalSpacing + sizes.first!.height + verticalSpacing + sliderSize.height) + } + } + + public func makeView() -> View { + return View(frame: CGRect()) + } + + public func update(view: View, availableSize: CGSize, state: State, environment: Environment, transition: Transition) -> CGSize { + return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition) + } +} + diff --git a/submodules/TelegramUI/Components/MediaEditorScreen/Sources/VideoScrubberComponent.swift b/submodules/TelegramUI/Components/MediaEditorScreen/Sources/VideoScrubberComponent.swift new file mode 100644 index 0000000000..af8e921231 --- /dev/null +++ b/submodules/TelegramUI/Components/MediaEditorScreen/Sources/VideoScrubberComponent.swift @@ -0,0 +1,130 @@ +import Foundation +import UIKit +import Display +import AsyncDisplayKit +import ComponentFlow +import SwiftSignalKit +import ViewControllerComponent +import ComponentDisplayAdapters +import TelegramPresentationData +import AccountContext + +private let handleWidth: CGFloat = 14.0 +private let scrubberHeight: CGFloat = 39.0 +private let borderHeight: CGFloat = 1.0 + UIScreenPixel +private let frameWidth: CGFloat = 24.0 + +final class VideoScrubberComponent: Component { + typealias EnvironmentType = Empty + + let context: AccountContext + let duration: Double + let startPosition: Double + let endPosition: Double + + init( + context: AccountContext, + duration: Double, + startPosition: Double, + endPosition: Double + ) { + self.context = context + self.duration = duration + self.startPosition = startPosition + self.endPosition = endPosition + } + + static func ==(lhs: VideoScrubberComponent, rhs: VideoScrubberComponent) -> Bool { + if lhs.context !== rhs.context { + return false + } + return true + } + + final class View: UIView, UITextFieldDelegate { + private let containerView = UIView() + private let leftHandleView = UIImageView() + private let rightHandleView = UIImageView() + private let borderView = UIImageView() + private let cursorView = UIImageView() + + private var component: VideoScrubberComponent? + private weak var state: EmptyComponentState? + + override init(frame: CGRect) { + super.init(frame: frame) + + self.disablesInteractiveModalDismiss = true + self.disablesInteractiveKeyboardGestureRecognizer = true + + let handleImage = generateImage(CGSize(width: handleWidth, height: scrubberHeight), rotatedContext: { size, context in + context.clear(CGRect(origin: .zero, size: size)) + context.setFillColor(UIColor.white.cgColor) + + let path = UIBezierPath(roundedRect: CGRect(origin: .zero, size: CGSize(width: size.width * 2.0, height: size.height)), cornerRadius: 9.0) + context.addPath(path.cgPath) + context.fillPath() + + context.setBlendMode(.clear) + let innerPath = UIBezierPath(roundedRect: CGRect(origin: CGPoint(x: handleWidth - 3.0, y: borderHeight), size: CGSize(width: handleWidth, height: size.height - borderHeight * 2.0)), cornerRadius: 2.0) + context.addPath(innerPath.cgPath) + context.fillPath() + + let holeSize = CGSize(width: 2.0, height: 11.0) + let holePath = UIBezierPath(roundedRect: CGRect(origin: CGPoint(x: 5.0 - UIScreenPixel, y: (size.height - holeSize.height) / 2.0), size: holeSize), cornerRadius: holeSize.width / 2.0) + context.addPath(holePath.cgPath) + context.fillPath() + }) + + self.leftHandleView.image = handleImage + self.rightHandleView.image = handleImage + self.rightHandleView.transform = CGAffineTransform(scaleX: -1.0, y: 1.0) + + self.borderView.image = generateImage(CGSize(width: 1.0, height: scrubberHeight), rotatedContext: { size, context in + context.clear(CGRect(origin: .zero, size: size)) + context.setFillColor(UIColor.white.cgColor) + context.fill(CGRect(origin: .zero, size: CGSize(width: size.width, height: borderHeight))) + context.fill(CGRect(origin: CGPoint(x: 0.0, y: size.height - borderHeight), size: CGSize(width: size.width, height: scrubberHeight))) + }) + + self.addSubview(self.containerView) + self.addSubview(self.leftHandleView) + self.addSubview(self.rightHandleView) + self.addSubview(self.borderView) + self.addSubview(self.cursorView) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + func update(component: VideoScrubberComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + self.component = component + self.state = state + + let scrubberSize = CGSize(width: availableSize.width, height: scrubberHeight) + let bounds = CGRect(origin: .zero, size: scrubberSize) + + transition.setFrame(view: self.containerView, frame: bounds) + + let leftHandleFrame = CGRect(origin: .zero, size: CGSize(width: handleWidth, height: scrubberSize.height)) + transition.setFrame(view: self.leftHandleView, frame: leftHandleFrame) + + let rightHandleFrame = CGRect(origin: CGPoint(x: scrubberSize.width - handleWidth, y: 0.0), size: CGSize(width: handleWidth, height: scrubberSize.height)) + transition.setFrame(view: self.rightHandleView, frame: rightHandleFrame) + + let borderFrame = CGRect(origin: CGPoint(x: leftHandleFrame.maxX, y: 0.0), size: CGSize(width: rightHandleFrame.minX - leftHandleFrame.maxX, height: scrubberSize.height)) + transition.setFrame(view: self.borderView, frame: borderFrame) + + return scrubberSize + } + } + + public func makeView() -> View { + return View(frame: CGRect()) + } + + public func update(view: View, availableSize: CGSize, state: State, environment: Environment, transition: Transition) -> CGSize { + return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition) + } +} diff --git a/submodules/TelegramUI/Components/MessageInputPanelComponent/Sources/MessageInputActionButtonComponent.swift b/submodules/TelegramUI/Components/MessageInputPanelComponent/Sources/MessageInputActionButtonComponent.swift index 7b8a35c899..81d4549624 100644 --- a/submodules/TelegramUI/Components/MessageInputPanelComponent/Sources/MessageInputActionButtonComponent.swift +++ b/submodules/TelegramUI/Components/MessageInputPanelComponent/Sources/MessageInputActionButtonComponent.swift @@ -10,7 +10,9 @@ import ChatPresentationInterfaceState public final class MessageInputActionButtonComponent: Component { public enum Mode { + case none case send + case apply case voiceInput case videoInput } @@ -214,7 +216,9 @@ public final class MessageInputActionButtonComponent: Component { var microphoneAlpha: CGFloat = 0.0 switch component.mode { - case .send: + case .none: + break + case .send, .apply: sendAlpha = 1.0 case .videoInput, .voiceInput: microphoneAlpha = 1.0 @@ -244,7 +248,7 @@ public final class MessageInputActionButtonComponent: Component { if previousComponent?.mode != component.mode { switch component.mode { - case .send, .voiceInput: + case .none, .send, .apply, .voiceInput: micButton.updateMode(mode: .audio, animated: !transition.animation.isImmediate) case .videoInput: micButton.updateMode(mode: .video, animated: !transition.animation.isImmediate) diff --git a/submodules/TelegramUI/Components/MessageInputPanelComponent/Sources/MessageInputPanelComponent.swift b/submodules/TelegramUI/Components/MessageInputPanelComponent/Sources/MessageInputPanelComponent.swift index 6e596d91e2..06f2ba1cb4 100644 --- a/submodules/TelegramUI/Components/MessageInputPanelComponent/Sources/MessageInputPanelComponent.swift +++ b/submodules/TelegramUI/Components/MessageInputPanelComponent/Sources/MessageInputPanelComponent.swift @@ -10,6 +10,10 @@ import TelegramPresentationData import ChatPresentationInterfaceState public final class MessageInputPanelComponent: Component { + public enum Style { + case story + case editor + } public final class ExternalState { public fileprivate(set) var isEditing: Bool = false public fileprivate(set) var hasText: Bool = false @@ -22,11 +26,13 @@ public final class MessageInputPanelComponent: Component { public let context: AccountContext public let theme: PresentationTheme public let strings: PresentationStrings + public let style: Style + public let placeholder: String public let presentController: (ViewController) -> Void public let sendMessageAction: () -> Void - public let setMediaRecordingActive: (Bool, Bool, Bool) -> Void - public let attachmentAction: () -> Void - public let reactionAction: (UIView) -> Void + public let setMediaRecordingActive: ((Bool, Bool, Bool) -> Void)? + public let attachmentAction: (() -> Void)? + public let reactionAction: ((UIView) -> Void)? public let audioRecorder: ManagedAudioRecorder? public let videoRecordingStatus: InstantVideoControllerRecordingStatus? @@ -35,11 +41,13 @@ public final class MessageInputPanelComponent: Component { context: AccountContext, theme: PresentationTheme, strings: PresentationStrings, + style: Style, + placeholder: String, presentController: @escaping (ViewController) -> Void, sendMessageAction: @escaping () -> Void, - setMediaRecordingActive: @escaping (Bool, Bool, Bool) -> Void, - attachmentAction: @escaping () -> Void, - reactionAction: @escaping (UIView) -> Void, + setMediaRecordingActive: ((Bool, Bool, Bool) -> Void)?, + attachmentAction: (() -> Void)?, + reactionAction: ((UIView) -> Void)?, audioRecorder: ManagedAudioRecorder?, videoRecordingStatus: InstantVideoControllerRecordingStatus? ) { @@ -47,6 +55,8 @@ public final class MessageInputPanelComponent: Component { self.context = context self.theme = theme self.strings = strings + self.style = style + self.placeholder = placeholder self.presentController = presentController self.sendMessageAction = sendMessageAction self.setMediaRecordingActive = setMediaRecordingActive @@ -69,6 +79,12 @@ public final class MessageInputPanelComponent: Component { if lhs.strings !== rhs.strings { return false } + if lhs.style != rhs.style { + return false + } + if lhs.placeholder != rhs.placeholder { + return false + } if lhs.audioRecorder !== rhs.audioRecorder { return false } @@ -84,6 +100,7 @@ public final class MessageInputPanelComponent: Component { public final class View: UIView { private let fieldBackgroundView: UIImageView + private let fieldBackgroundEffectView: UIVisualEffectView private let textField = ComponentView() private let textFieldExternalState = TextFieldComponent.ExternalState() @@ -104,10 +121,9 @@ public final class MessageInputPanelComponent: Component { override init(frame: CGRect) { self.fieldBackgroundView = UIImageView() + self.fieldBackgroundEffectView = UIVisualEffectView() super.init(frame: frame) - - self.addSubview(self.fieldBackgroundView) } required init?(coder: NSCoder) { @@ -137,14 +153,34 @@ public final class MessageInputPanelComponent: Component { func update(component: MessageInputPanelComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { let baseHeight: CGFloat = 44.0 - let insets = UIEdgeInsets(top: 5.0, left: 41.0, bottom: 5.0, right: 41.0) + var insets = UIEdgeInsets(top: 5.0, left: 7.0, bottom: 5.0, right: 7.0) + if let _ = component.attachmentAction { + insets.left = 41.0 + } + if let _ = component.setMediaRecordingActive { + insets.right = 41.0 + } let fieldCornerRadius: CGFloat = 16.0 self.component = component self.state = state - if self.fieldBackgroundView.image == nil { - self.fieldBackgroundView.image = generateStretchableFilledCircleImage(diameter: fieldCornerRadius * 2.0, color: nil, strokeColor: UIColor(white: 1.0, alpha: 0.16), strokeWidth: 1.0, backgroundColor: nil) + var placeholderAlignment: NSTextAlignment + switch component.style { + case .story: + if self.fieldBackgroundView.superview == nil { + self.fieldBackgroundView.image = generateStretchableFilledCircleImage(diameter: fieldCornerRadius * 2.0, color: nil, strokeColor: UIColor(white: 1.0, alpha: 0.16), strokeWidth: 1.0, backgroundColor: nil) + self.insertSubview(self.fieldBackgroundView, at: 0) + } + placeholderAlignment = .natural + case .editor: + if self.fieldBackgroundEffectView.superview == nil { + self.fieldBackgroundEffectView.clipsToBounds = true + self.fieldBackgroundEffectView.layer.cornerRadius = fieldCornerRadius + self.fieldBackgroundEffectView.effect = UIBlurEffect(style: .dark) + self.insertSubview(self.fieldBackgroundEffectView, at: 0) + } + placeholderAlignment = .center } let availableTextFieldSize = CGSize(width: availableSize.width - insets.left - insets.right, height: availableSize.height - insets.top - insets.bottom) @@ -154,16 +190,23 @@ public final class MessageInputPanelComponent: Component { transition: .immediate, component: AnyComponent(TextFieldComponent( externalState: self.textFieldExternalState, - placeholder: "Reply Privately..." + placeholder: component.placeholder, + placeholderAlignment: placeholderAlignment )), environment: {}, containerSize: availableTextFieldSize ) + if self.textFieldExternalState.isEditing { + insets.right = 41.0 + } let fieldFrame = CGRect(origin: CGPoint(x: insets.left, y: insets.top), size: CGSize(width: availableSize.width - insets.left - insets.right, height: textFieldSize.height)) transition.setFrame(view: self.fieldBackgroundView, frame: fieldFrame) transition.setAlpha(view: self.fieldBackgroundView, alpha: (component.audioRecorder != nil || component.videoRecordingStatus != nil) ? 0.0 : 1.0) + transition.setFrame(view: self.fieldBackgroundEffectView, frame: fieldFrame) + transition.setAlpha(view: self.fieldBackgroundEffectView, alpha: (component.audioRecorder != nil || component.videoRecordingStatus != nil) ? 0.0 : 1.0) + //let rightFieldInset: CGFloat = 34.0 let size = CGSize(width: availableSize.width, height: textFieldSize.height + insets.top + insets.bottom) @@ -176,40 +219,48 @@ public final class MessageInputPanelComponent: Component { transition.setAlpha(view: textFieldView, alpha: (component.audioRecorder != nil || component.videoRecordingStatus != nil) ? 0.0 : 1.0) } - let attachmentButtonSize = self.attachmentButton.update( - transition: transition, - component: AnyComponent(Button( - content: AnyComponent(BundleIconComponent( - name: "Chat/Input/Text/IconAttachment", - tintColor: .white - )), - action: { [weak self] in - guard let self else { - return + if let attachmentAction = component.attachmentAction { + let attachmentButtonSize = self.attachmentButton.update( + transition: transition, + component: AnyComponent(Button( + content: AnyComponent(BundleIconComponent( + name: "Chat/Input/Text/IconAttachment", + tintColor: .white + )), + action: { + attachmentAction() } - self.component?.attachmentAction() + ).minSize(CGSize(width: 41.0, height: baseHeight))), + environment: {}, + containerSize: CGSize(width: 41.0, height: baseHeight) + ) + if let attachmentButtonView = self.attachmentButton.view { + if attachmentButtonView.superview == nil { + self.addSubview(attachmentButtonView) } - ).minSize(CGSize(width: 41.0, height: baseHeight))), - environment: {}, - containerSize: CGSize(width: 41.0, height: baseHeight) - ) - if let attachmentButtonView = self.attachmentButton.view { - if attachmentButtonView.superview == nil { - self.addSubview(attachmentButtonView) + transition.setFrame(view: attachmentButtonView, frame: CGRect(origin: CGPoint(x: floor((insets.left - attachmentButtonSize.width) * 0.5), y: size.height - baseHeight + floor((baseHeight - attachmentButtonSize.height) * 0.5)), size: attachmentButtonSize)) } - transition.setFrame(view: attachmentButtonView, frame: CGRect(origin: CGPoint(x: floor((insets.left - attachmentButtonSize.width) * 0.5), y: size.height - baseHeight + floor((baseHeight - attachmentButtonSize.height) * 0.5)), size: attachmentButtonSize)) } + + let inputActionButtonMode: MessageInputActionButtonComponent.Mode + if case .editor = component.style { + inputActionButtonMode = self.textFieldExternalState.isEditing ? .apply : .none + } else { + inputActionButtonMode = self.textFieldExternalState.hasText ? .send : (self.currentMediaInputIsVoice ? .voiceInput : .videoInput) + } let inputActionButtonSize = self.inputActionButton.update( transition: transition, component: AnyComponent(MessageInputActionButtonComponent( - mode: self.textFieldExternalState.hasText ? .send : (self.currentMediaInputIsVoice ? .voiceInput : .videoInput), + mode: inputActionButtonMode, action: { [weak self] mode, action, sendAction in guard let self else { return } switch mode { + case .none: + break case .send: if case .up = action { if case .text("") = self.getSendMessageInput() { @@ -217,8 +268,12 @@ public final class MessageInputPanelComponent: Component { self.component?.sendMessageAction() } } + case .apply: + if case .up = action { + self.component?.sendMessageAction() + } case .voiceInput, .videoInput: - self.component?.setMediaRecordingActive(action == .down, mode == .videoInput, sendAction) + self.component?.setMediaRecordingActive?(action == .down, mode == .videoInput, sendAction) } }, switchMediaInputMode: { [weak self] in @@ -251,69 +306,80 @@ public final class MessageInputPanelComponent: Component { if inputActionButtonView.superview == nil { self.addSubview(inputActionButtonView) } - transition.setFrame(view: inputActionButtonView, frame: CGRect(origin: CGPoint(x: size.width - insets.right + floorToScreenPixels((insets.right - inputActionButtonSize.width) * 0.5), y: size.height - baseHeight + floorToScreenPixels((baseHeight - inputActionButtonSize.height) * 0.5)), size: inputActionButtonSize)) - } - var fieldIconNextX = fieldFrame.maxX - 2.0 - let stickerButtonSize = self.stickerButton.update( - transition: transition, - component: AnyComponent(Button( - content: AnyComponent(BundleIconComponent( - name: "Chat/Input/Text/AccessoryIconStickers", - tintColor: .white - )), - action: { [weak self] in - guard let self else { - return - } - self.component?.attachmentAction() - } - ).minSize(CGSize(width: 32.0, height: 32.0))), - environment: {}, - containerSize: CGSize(width: 32.0, height: 32.0) - ) - if let stickerButtonView = self.stickerButton.view { - if stickerButtonView.superview == nil { - self.addSubview(stickerButtonView) + let inputActionButtonOriginX: CGFloat + if component.setMediaRecordingActive != nil || self.textFieldExternalState.isEditing { + inputActionButtonOriginX = size.width - insets.right + floorToScreenPixels((insets.right - inputActionButtonSize.width) * 0.5) + } else { + inputActionButtonOriginX = size.width + } + transition.setFrame(view: inputActionButtonView, frame: CGRect(origin: CGPoint(x: inputActionButtonOriginX, y: size.height - baseHeight + floorToScreenPixels((baseHeight - inputActionButtonSize.height) * 0.5)), size: inputActionButtonSize)) + } + + var fieldIconNextX = fieldFrame.maxX - 2.0 + if case .story = component.style { + let stickerButtonSize = self.stickerButton.update( + transition: transition, + component: AnyComponent(Button( + content: AnyComponent(BundleIconComponent( + name: "Chat/Input/Text/AccessoryIconStickers", + tintColor: .white + )), + action: { [weak self] in + guard let self else { + return + } + self.component?.attachmentAction?() + } + ).minSize(CGSize(width: 32.0, height: 32.0))), + environment: {}, + containerSize: CGSize(width: 32.0, height: 32.0) + ) + if let stickerButtonView = self.stickerButton.view { + if stickerButtonView.superview == nil { + self.addSubview(stickerButtonView) + } + let stickerIconFrame = CGRect(origin: CGPoint(x: fieldIconNextX - stickerButtonSize.width, y: fieldFrame.minY + floor((fieldFrame.height - stickerButtonSize.height) * 0.5)), size: stickerButtonSize) + transition.setPosition(view: stickerButtonView, position: stickerIconFrame.center) + transition.setBounds(view: stickerButtonView, bounds: CGRect(origin: CGPoint(), size: stickerIconFrame.size)) + + transition.setAlpha(view: stickerButtonView, alpha: self.textFieldExternalState.hasText ? 0.0 : 1.0) + transition.setScale(view: stickerButtonView, scale: self.textFieldExternalState.hasText ? 0.1 : 1.0) + + fieldIconNextX -= stickerButtonSize.width + 2.0 } - let stickerIconFrame = CGRect(origin: CGPoint(x: fieldIconNextX - stickerButtonSize.width, y: fieldFrame.minY + floor((fieldFrame.height - stickerButtonSize.height) * 0.5)), size: stickerButtonSize) - transition.setPosition(view: stickerButtonView, position: stickerIconFrame.center) - transition.setBounds(view: stickerButtonView, bounds: CGRect(origin: CGPoint(), size: stickerIconFrame.size)) - - transition.setAlpha(view: stickerButtonView, alpha: self.textFieldExternalState.hasText ? 0.0 : 1.0) - transition.setScale(view: stickerButtonView, scale: self.textFieldExternalState.hasText ? 0.1 : 1.0) - - fieldIconNextX -= stickerButtonSize.width + 2.0 } - let reactionButtonSize = self.reactionButton.update( - transition: transition, - component: AnyComponent(Button( - content: AnyComponent(BundleIconComponent( - name: "Chat/Input/Text/AccessoryIconReaction", - tintColor: .white - )), - action: { [weak self] in - guard let self, let reactionButtonView = self.reactionButton.view else { - return + if let reactionAction = component.reactionAction { + let reactionButtonSize = self.reactionButton.update( + transition: transition, + component: AnyComponent(Button( + content: AnyComponent(BundleIconComponent( + name: "Chat/Input/Text/AccessoryIconReaction", + tintColor: .white + )), + action: { [weak self] in + guard let self, let reactionButtonView = self.reactionButton.view else { + return + } + reactionAction(reactionButtonView) } - self.component?.reactionAction(reactionButtonView) + ).minSize(CGSize(width: 32.0, height: 32.0))), + environment: {}, + containerSize: CGSize(width: 32.0, height: 32.0) + ) + if let reactionButtonView = self.reactionButton.view { + if reactionButtonView.superview == nil { + self.addSubview(reactionButtonView) } - ).minSize(CGSize(width: 32.0, height: 32.0))), - environment: {}, - containerSize: CGSize(width: 32.0, height: 32.0) - ) - if let reactionButtonView = self.reactionButton.view { - if reactionButtonView.superview == nil { - self.addSubview(reactionButtonView) + let reactionIconFrame = CGRect(origin: CGPoint(x: fieldIconNextX - reactionButtonSize.width, y: fieldFrame.minY + 1.0 + floor((fieldFrame.height - reactionButtonSize.height) * 0.5)), size: reactionButtonSize) + transition.setPosition(view: reactionButtonView, position: reactionIconFrame.center) + transition.setBounds(view: reactionButtonView, bounds: CGRect(origin: CGPoint(), size: reactionIconFrame.size)) + + transition.setAlpha(view: reactionButtonView, alpha: self.textFieldExternalState.hasText ? 0.0 : 1.0) + transition.setScale(view: reactionButtonView, scale: self.textFieldExternalState.hasText ? 0.1 : 1.0) + + fieldIconNextX -= reactionButtonSize.width + 2.0 } - let reactionIconFrame = CGRect(origin: CGPoint(x: fieldIconNextX - reactionButtonSize.width, y: fieldFrame.minY + 1.0 + floor((fieldFrame.height - reactionButtonSize.height) * 0.5)), size: reactionButtonSize) - transition.setPosition(view: reactionButtonView, position: reactionIconFrame.center) - transition.setBounds(view: reactionButtonView, bounds: CGRect(origin: CGPoint(), size: reactionIconFrame.size)) - - transition.setAlpha(view: reactionButtonView, alpha: self.textFieldExternalState.hasText ? 0.0 : 1.0) - transition.setScale(view: reactionButtonView, scale: self.textFieldExternalState.hasText ? 0.1 : 1.0) - - fieldIconNextX -= reactionButtonSize.width + 2.0 } /*if let image = self.reactionIconView.image { diff --git a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetContainerComponent.swift b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetContainerComponent.swift index 46f11ea7b9..f126ef5672 100644 --- a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetContainerComponent.swift +++ b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetContainerComponent.swift @@ -516,6 +516,8 @@ public final class StoryItemSetContainerComponent: Component { context: component.context, theme: component.theme, strings: component.strings, + style: .story, + placeholder: "Reply Privately...", presentController: { [weak self] c in guard let self, let component = self.component else { return diff --git a/submodules/TelegramUI/Components/TextFieldComponent/Sources/TextFieldComponent.swift b/submodules/TelegramUI/Components/TextFieldComponent/Sources/TextFieldComponent.swift index 317bd8ec69..e1799bbba1 100644 --- a/submodules/TelegramUI/Components/TextFieldComponent/Sources/TextFieldComponent.swift +++ b/submodules/TelegramUI/Components/TextFieldComponent/Sources/TextFieldComponent.swift @@ -27,13 +27,16 @@ public final class TextFieldComponent: Component { public let externalState: ExternalState public let placeholder: String + public let placeholderAlignment: NSTextAlignment public init( externalState: ExternalState, - placeholder: String + placeholder: String, + placeholderAlignment: NSTextAlignment ) { self.externalState = externalState self.placeholder = placeholder + self.placeholderAlignment = placeholderAlignment } public static func ==(lhs: TextFieldComponent, rhs: TextFieldComponent) -> Bool { @@ -43,6 +46,9 @@ public final class TextFieldComponent: Component { if lhs.placeholder != rhs.placeholder { return false } + if lhs.placeholderAlignment != rhs.placeholderAlignment { + return false + } return true } @@ -145,7 +151,7 @@ public final class TextFieldComponent: Component { let placeholderSize = self.placeholder.update( transition: .immediate, - component: AnyComponent(Text(text: component.placeholder, font: Font.regular(17.0), color: UIColor(white: 1.0, alpha: 0.25))), + component: AnyComponent(Text(text: component.placeholder, font: Font.regular(17.0), color: UIColor(white: 1.0, alpha: 0.4))), environment: {}, containerSize: availableSize ) @@ -156,7 +162,22 @@ public final class TextFieldComponent: Component { self.insertSubview(placeholderView, belowSubview: self.textView) } - let placeholderFrame = CGRect(origin: CGPoint(x: self.textView.textContainerInset.left + 5.0, y: self.textView.textContainerInset.top), size: placeholderSize) + var placeholderAlignment = component.placeholderAlignment + if self.textView.isFirstResponder { + placeholderAlignment = .natural + } + let placeholderOriginX: CGFloat + switch placeholderAlignment { + case .left, .natural: + placeholderOriginX = self.textView.textContainerInset.left + 5.0 + case .center, .justified: + placeholderOriginX = floor((size.width - placeholderSize.width) / 2.0) + case .right: + placeholderOriginX = availableSize.width - self.textView.textContainerInset.left - 5.0 - placeholderSize.width + @unknown default: + placeholderOriginX = self.textView.textContainerInset.left + 5.0 + } + let placeholderFrame = CGRect(origin: CGPoint(x: placeholderOriginX, y: self.textView.textContainerInset.top), size: placeholderSize) placeholderView.bounds = CGRect(origin: CGPoint(), size: placeholderFrame.size) transition.setPosition(view: placeholderView, position: placeholderFrame.origin) diff --git a/submodules/LegacyComponents/LegacyImages.xcassets/Editor/BrushMarker.imageset/Contents.json b/submodules/TelegramUI/Images.xcassets/Media Editor/AddSticker.imageset/Contents.json similarity index 72% rename from submodules/LegacyComponents/LegacyImages.xcassets/Editor/BrushMarker.imageset/Contents.json rename to submodules/TelegramUI/Images.xcassets/Media Editor/AddSticker.imageset/Contents.json index e4d1a3e45b..1c0f9b65ca 100644 --- a/submodules/LegacyComponents/LegacyImages.xcassets/Editor/BrushMarker.imageset/Contents.json +++ b/submodules/TelegramUI/Images.xcassets/Media Editor/AddSticker.imageset/Contents.json @@ -1,7 +1,7 @@ { "images" : [ { - "filename" : "ic_menu_brush2.pdf", + "filename" : "ic_editor_addsticker.pdf", "idiom" : "universal" } ], diff --git a/submodules/LegacyComponents/LegacyImages.xcassets/Editor/Drawing.imageset/ic_editor_brush.pdf b/submodules/TelegramUI/Images.xcassets/Media Editor/AddSticker.imageset/ic_editor_addsticker.pdf similarity index 67% rename from submodules/LegacyComponents/LegacyImages.xcassets/Editor/Drawing.imageset/ic_editor_brush.pdf rename to submodules/TelegramUI/Images.xcassets/Media Editor/AddSticker.imageset/ic_editor_addsticker.pdf index f79859caaa..4a0e3ff1bd 100644 Binary files a/submodules/LegacyComponents/LegacyImages.xcassets/Editor/Drawing.imageset/ic_editor_brush.pdf and b/submodules/TelegramUI/Images.xcassets/Media Editor/AddSticker.imageset/ic_editor_addsticker.pdf differ diff --git a/submodules/LegacyComponents/LegacyImages.xcassets/Editor/BrushNeon.imageset/Contents.json b/submodules/TelegramUI/Images.xcassets/Media Editor/AddText.imageset/Contents.json similarity index 73% rename from submodules/LegacyComponents/LegacyImages.xcassets/Editor/BrushNeon.imageset/Contents.json rename to submodules/TelegramUI/Images.xcassets/Media Editor/AddText.imageset/Contents.json index 7c10dc82bd..94e17b900c 100644 --- a/submodules/LegacyComponents/LegacyImages.xcassets/Editor/BrushNeon.imageset/Contents.json +++ b/submodules/TelegramUI/Images.xcassets/Media Editor/AddText.imageset/Contents.json @@ -1,7 +1,7 @@ { "images" : [ { - "filename" : "ic_menu_brush3.pdf", + "filename" : "ic_editor_addtext.pdf", "idiom" : "universal" } ], diff --git a/submodules/TelegramUI/Images.xcassets/Media Editor/AddText.imageset/ic_editor_addtext.pdf b/submodules/TelegramUI/Images.xcassets/Media Editor/AddText.imageset/ic_editor_addtext.pdf new file mode 100644 index 0000000000..2c18f3d469 Binary files /dev/null and b/submodules/TelegramUI/Images.xcassets/Media Editor/AddText.imageset/ic_editor_addtext.pdf differ diff --git a/submodules/LegacyComponents/LegacyImages.xcassets/Editor/BrushPen.imageset/Contents.json b/submodules/TelegramUI/Images.xcassets/Media Editor/Blur.imageset/Contents.json similarity index 52% rename from submodules/LegacyComponents/LegacyImages.xcassets/Editor/BrushPen.imageset/Contents.json rename to submodules/TelegramUI/Images.xcassets/Media Editor/Blur.imageset/Contents.json index e6a3712dd8..13fc345efc 100644 --- a/submodules/LegacyComponents/LegacyImages.xcassets/Editor/BrushPen.imageset/Contents.json +++ b/submodules/TelegramUI/Images.xcassets/Media Editor/Blur.imageset/Contents.json @@ -1,12 +1,15 @@ { "images" : [ { - "filename" : "ic_menu_brush1.pdf", + "filename" : "ic_editor_blur.pdf", "idiom" : "universal" } ], "info" : { "author" : "xcode", "version" : 1 + }, + "properties" : { + "template-rendering-intent" : "template" } } diff --git a/submodules/LegacyComponents/LegacyImages.xcassets/Editor/BrushArrow.imageset/ic_menu_brush4.pdf b/submodules/TelegramUI/Images.xcassets/Media Editor/Blur.imageset/ic_editor_blur.pdf similarity index 76% rename from submodules/LegacyComponents/LegacyImages.xcassets/Editor/BrushArrow.imageset/ic_menu_brush4.pdf rename to submodules/TelegramUI/Images.xcassets/Media Editor/Blur.imageset/ic_editor_blur.pdf index d09005a3e7..0dd29d3021 100644 Binary files a/submodules/LegacyComponents/LegacyImages.xcassets/Editor/BrushArrow.imageset/ic_menu_brush4.pdf and b/submodules/TelegramUI/Images.xcassets/Media Editor/Blur.imageset/ic_editor_blur.pdf differ diff --git a/submodules/TelegramUI/Images.xcassets/Media Editor/BlurLinear.imageset/Contents.json b/submodules/TelegramUI/Images.xcassets/Media Editor/BlurLinear.imageset/Contents.json new file mode 100644 index 0000000000..a09a11234e --- /dev/null +++ b/submodules/TelegramUI/Images.xcassets/Media Editor/BlurLinear.imageset/Contents.json @@ -0,0 +1,15 @@ +{ + "images" : [ + { + "filename" : "ic_editor_blurlinear.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "template-rendering-intent" : "template" + } +} diff --git a/submodules/LegacyComponents/LegacyImages.xcassets/Editor/BrushSelectedMarker.imageset/ic_editor_brush2.pdf b/submodules/TelegramUI/Images.xcassets/Media Editor/BlurLinear.imageset/ic_editor_blurlinear.pdf similarity index 76% rename from submodules/LegacyComponents/LegacyImages.xcassets/Editor/BrushSelectedMarker.imageset/ic_editor_brush2.pdf rename to submodules/TelegramUI/Images.xcassets/Media Editor/BlurLinear.imageset/ic_editor_blurlinear.pdf index 88964822ea..bc05e822e0 100644 Binary files a/submodules/LegacyComponents/LegacyImages.xcassets/Editor/BrushSelectedMarker.imageset/ic_editor_brush2.pdf and b/submodules/TelegramUI/Images.xcassets/Media Editor/BlurLinear.imageset/ic_editor_blurlinear.pdf differ diff --git a/submodules/TelegramUI/Images.xcassets/Media Editor/BlurOff.imageset/Contents.json b/submodules/TelegramUI/Images.xcassets/Media Editor/BlurOff.imageset/Contents.json new file mode 100644 index 0000000000..04aad23b36 --- /dev/null +++ b/submodules/TelegramUI/Images.xcassets/Media Editor/BlurOff.imageset/Contents.json @@ -0,0 +1,15 @@ +{ + "images" : [ + { + "filename" : "ic_editor_blurnope.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "template-rendering-intent" : "template" + } +} diff --git a/submodules/LegacyComponents/LegacyImages.xcassets/Editor/BrushSelectedArrow.imageset/ic_editor_brush4.pdf b/submodules/TelegramUI/Images.xcassets/Media Editor/BlurOff.imageset/ic_editor_blurnope.pdf similarity index 80% rename from submodules/LegacyComponents/LegacyImages.xcassets/Editor/BrushSelectedArrow.imageset/ic_editor_brush4.pdf rename to submodules/TelegramUI/Images.xcassets/Media Editor/BlurOff.imageset/ic_editor_blurnope.pdf index abf261fda2..aa499cb862 100644 Binary files a/submodules/LegacyComponents/LegacyImages.xcassets/Editor/BrushSelectedArrow.imageset/ic_editor_brush4.pdf and b/submodules/TelegramUI/Images.xcassets/Media Editor/BlurOff.imageset/ic_editor_blurnope.pdf differ diff --git a/submodules/TelegramUI/Images.xcassets/Media Editor/BlurPortrait.imageset/Contents.json b/submodules/TelegramUI/Images.xcassets/Media Editor/BlurPortrait.imageset/Contents.json new file mode 100644 index 0000000000..aabf80753e --- /dev/null +++ b/submodules/TelegramUI/Images.xcassets/Media Editor/BlurPortrait.imageset/Contents.json @@ -0,0 +1,15 @@ +{ + "images" : [ + { + "filename" : "ic_editor_blurportrait.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "template-rendering-intent" : "template" + } +} diff --git a/submodules/LegacyComponents/LegacyImages.xcassets/Editor/BrushSelectedNeon.imageset/ic_editor_brush3.pdf b/submodules/TelegramUI/Images.xcassets/Media Editor/BlurPortrait.imageset/ic_editor_blurportrait.pdf similarity index 76% rename from submodules/LegacyComponents/LegacyImages.xcassets/Editor/BrushSelectedNeon.imageset/ic_editor_brush3.pdf rename to submodules/TelegramUI/Images.xcassets/Media Editor/BlurPortrait.imageset/ic_editor_blurportrait.pdf index 0eaaefca86..9dd6e79f01 100644 Binary files a/submodules/LegacyComponents/LegacyImages.xcassets/Editor/BrushSelectedNeon.imageset/ic_editor_brush3.pdf and b/submodules/TelegramUI/Images.xcassets/Media Editor/BlurPortrait.imageset/ic_editor_blurportrait.pdf differ diff --git a/submodules/TelegramUI/Images.xcassets/Media Editor/BlurRadial.imageset/Contents.json b/submodules/TelegramUI/Images.xcassets/Media Editor/BlurRadial.imageset/Contents.json new file mode 100644 index 0000000000..338c457c5a --- /dev/null +++ b/submodules/TelegramUI/Images.xcassets/Media Editor/BlurRadial.imageset/Contents.json @@ -0,0 +1,15 @@ +{ + "images" : [ + { + "filename" : "ic_editor_blurradian.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "template-rendering-intent" : "template" + } +} diff --git a/submodules/LegacyComponents/LegacyImages.xcassets/Editor/BrushSelectedPen.imageset/ic_editor_brush1.pdf b/submodules/TelegramUI/Images.xcassets/Media Editor/BlurRadial.imageset/ic_editor_blurradian.pdf similarity index 78% rename from submodules/LegacyComponents/LegacyImages.xcassets/Editor/BrushSelectedPen.imageset/ic_editor_brush1.pdf rename to submodules/TelegramUI/Images.xcassets/Media Editor/BlurRadial.imageset/ic_editor_blurradian.pdf index cafe10024a..ebac9e89c5 100644 Binary files a/submodules/LegacyComponents/LegacyImages.xcassets/Editor/BrushSelectedPen.imageset/ic_editor_brush1.pdf and b/submodules/TelegramUI/Images.xcassets/Media Editor/BlurRadial.imageset/ic_editor_blurradian.pdf differ diff --git a/submodules/TelegramUI/Images.xcassets/Media Editor/Curves.imageset/Contents.json b/submodules/TelegramUI/Images.xcassets/Media Editor/Curves.imageset/Contents.json new file mode 100644 index 0000000000..5f76e6669c --- /dev/null +++ b/submodules/TelegramUI/Images.xcassets/Media Editor/Curves.imageset/Contents.json @@ -0,0 +1,15 @@ +{ + "images" : [ + { + "filename" : "ic_editor_curves.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "template-rendering-intent" : "template" + } +} diff --git a/submodules/TelegramUI/Images.xcassets/Media Editor/Curves.imageset/ic_editor_curves.pdf b/submodules/TelegramUI/Images.xcassets/Media Editor/Curves.imageset/ic_editor_curves.pdf new file mode 100644 index 0000000000..ce7be3acd2 Binary files /dev/null and b/submodules/TelegramUI/Images.xcassets/Media Editor/Curves.imageset/ic_editor_curves.pdf differ diff --git a/submodules/LegacyComponents/LegacyImages.xcassets/Editor/BrushArrow.imageset/Contents.json b/submodules/TelegramUI/Images.xcassets/Media Editor/Pencil.imageset/Contents.json similarity index 75% rename from submodules/LegacyComponents/LegacyImages.xcassets/Editor/BrushArrow.imageset/Contents.json rename to submodules/TelegramUI/Images.xcassets/Media Editor/Pencil.imageset/Contents.json index 84eb1bf387..6074e4a7f0 100644 --- a/submodules/LegacyComponents/LegacyImages.xcassets/Editor/BrushArrow.imageset/Contents.json +++ b/submodules/TelegramUI/Images.xcassets/Media Editor/Pencil.imageset/Contents.json @@ -1,7 +1,7 @@ { "images" : [ { - "filename" : "ic_menu_brush4.pdf", + "filename" : "pencil_30.pdf", "idiom" : "universal" } ], diff --git a/submodules/TelegramUI/Images.xcassets/Media Editor/Pencil.imageset/pencil_30.pdf b/submodules/TelegramUI/Images.xcassets/Media Editor/Pencil.imageset/pencil_30.pdf new file mode 100644 index 0000000000..9cb855fa62 --- /dev/null +++ b/submodules/TelegramUI/Images.xcassets/Media Editor/Pencil.imageset/pencil_30.pdf @@ -0,0 +1,93 @@ +%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 4.335022 4.334961 cm +1.000000 1.000000 1.000000 scn +1.330000 10.665017 m +1.330000 15.820595 5.509422 20.000017 10.665000 20.000017 c +15.820578 20.000017 20.000000 15.820595 20.000000 10.665017 c +20.000000 7.511573 18.436378 4.723331 16.042154 3.033316 c +14.314164 10.809276 l +14.246551 11.113539 13.976685 11.330017 13.665000 11.330017 c +13.443334 11.330017 l +11.613684 16.818966 l +11.309785 17.730663 10.020216 17.730665 9.716317 16.818970 c +7.886667 11.330017 l +7.665000 11.330017 l +7.353315 11.330017 7.083449 11.113539 7.015836 10.809276 c +5.287845 3.033316 l +2.893622 4.723331 1.330000 7.511574 1.330000 10.665017 c +h +13.131556 10.000017 m +14.839726 2.313250 l +13.583508 1.684090 12.165603 1.330017 10.665000 1.330017 c +9.164397 1.330017 7.746492 1.684090 6.490273 2.313250 c +8.198444 10.000017 l +13.131556 10.000017 l +h +10.665000 21.330017 m +4.774883 21.330017 0.000000 16.555134 0.000000 10.665017 c +0.000000 4.774900 4.774883 0.000015 10.665000 0.000015 c +16.555117 0.000015 21.330002 4.774900 21.330002 10.665017 c +21.330002 16.555134 16.555117 21.330017 10.665000 21.330017 c +h +f* +n +Q + +endstream +endobj + +3 0 obj + 1162 +endobj + +4 0 obj + << /Annots [] + /Type /Page + /MediaBox [ 0.000000 0.000000 30.000000 30.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 +0000001252 00000 n +0000001275 00000 n +0000001448 00000 n +0000001522 00000 n +trailer +<< /ID [ (some) (id) ] + /Root 6 0 R + /Size 7 +>> +startxref +1581 +%%EOF \ No newline at end of file diff --git a/submodules/TelegramUI/Images.xcassets/Media Editor/Tint.imageset/Contents.json b/submodules/TelegramUI/Images.xcassets/Media Editor/Tint.imageset/Contents.json new file mode 100644 index 0000000000..d3b5ee8e9b --- /dev/null +++ b/submodules/TelegramUI/Images.xcassets/Media Editor/Tint.imageset/Contents.json @@ -0,0 +1,15 @@ +{ + "images" : [ + { + "filename" : "ic_editor_tint.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "template-rendering-intent" : "template" + } +} diff --git a/submodules/TelegramUI/Images.xcassets/Media Editor/Tint.imageset/ic_editor_tint.pdf b/submodules/TelegramUI/Images.xcassets/Media Editor/Tint.imageset/ic_editor_tint.pdf new file mode 100644 index 0000000000..1da45fcd21 Binary files /dev/null and b/submodules/TelegramUI/Images.xcassets/Media Editor/Tint.imageset/ic_editor_tint.pdf differ diff --git a/submodules/TelegramUI/Images.xcassets/Media Editor/Tools.imageset/Contents.json b/submodules/TelegramUI/Images.xcassets/Media Editor/Tools.imageset/Contents.json new file mode 100644 index 0000000000..3dbd2aa827 --- /dev/null +++ b/submodules/TelegramUI/Images.xcassets/Media Editor/Tools.imageset/Contents.json @@ -0,0 +1,15 @@ +{ + "images" : [ + { + "filename" : "ic_editor_tools.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "template-rendering-intent" : "template" + } +} diff --git a/submodules/LegacyComponents/LegacyImages.xcassets/Editor/BrushPen.imageset/ic_menu_brush1.pdf b/submodules/TelegramUI/Images.xcassets/Media Editor/Tools.imageset/ic_editor_tools.pdf similarity index 80% rename from submodules/LegacyComponents/LegacyImages.xcassets/Editor/BrushPen.imageset/ic_menu_brush1.pdf rename to submodules/TelegramUI/Images.xcassets/Media Editor/Tools.imageset/ic_editor_tools.pdf index 7e189831a4..e7db29f833 100644 Binary files a/submodules/LegacyComponents/LegacyImages.xcassets/Editor/BrushPen.imageset/ic_menu_brush1.pdf and b/submodules/TelegramUI/Images.xcassets/Media Editor/Tools.imageset/ic_editor_tools.pdf differ diff --git a/submodules/TelegramUI/Resources/Animations/anim_storymute.json b/submodules/TelegramUI/Resources/Animations/anim_storymute.json new file mode 100644 index 0000000000..97b2bd49d3 --- /dev/null +++ b/submodules/TelegramUI/Resources/Animations/anim_storymute.json @@ -0,0 +1 @@ +{"v":"5.10.1","fr":60,"ip":0,"op":60,"w":90,"h":90,"nm":"Comp 2","ddd":0,"assets":[],"layers":[{"ddd":0,"ind":1,"ty":4,"nm":"Artboard Outlines","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":1,"k":[{"i":{"x":[0.833],"y":[1.02]},"o":{"x":[0.167],"y":[0]},"t":0,"s":[0]},{"i":{"x":[0.833],"y":[0.707]},"o":{"x":[0.167],"y":[0.083]},"t":1,"s":[0]},{"i":{"x":[0.833],"y":[0.671]},"o":{"x":[0.167],"y":[0.116]},"t":2,"s":[-0.245]},{"i":{"x":[0.833],"y":[0.823]},"o":{"x":[0.167],"y":[0.112]},"t":3,"s":[-0.862]},{"i":{"x":[0.833],"y":[0.838]},"o":{"x":[0.167],"y":[0.157]},"t":4,"s":[-2.677]},{"i":{"x":[0.833],"y":[0.879]},"o":{"x":[0.167],"y":[0.172]},"t":5,"s":[-4.723]},{"i":{"x":[0.833],"y":[0.771]},"o":{"x":[0.167],"y":[0.268]},"t":6,"s":[-6.651]},{"i":{"x":[0.833],"y":[0.881]},"o":{"x":[0.167],"y":[0.131]},"t":7,"s":[-7.52]},{"i":{"x":[0.833],"y":[0.809]},"o":{"x":[0.167],"y":[0.279]},"t":8,"s":[-9.036]},{"i":{"x":[0.833],"y":[0.979]},"o":{"x":[0.167],"y":[0.148]},"t":9,"s":[-9.682]},{"i":{"x":[0.833],"y":[0.832]},"o":{"x":[0.167],"y":[-0.029]},"t":10,"s":[-10.52]},{"i":{"x":[0.833],"y":[0.719]},"o":{"x":[0.167],"y":[0.166]},"t":11,"s":[-9.897]},{"i":{"x":[0.833],"y":[0.832]},"o":{"x":[0.167],"y":[0.118]},"t":12,"s":[-9.267]},{"i":{"x":[0.833],"y":[0.878]},"o":{"x":[0.167],"y":[0.165]},"t":13,"s":[-7.773]},{"i":{"x":[0.833],"y":[0.768]},"o":{"x":[0.167],"y":[0.262]},"t":14,"s":[-6.257]},{"i":{"x":[0.833],"y":[0.848]},"o":{"x":[0.167],"y":[0.13]},"t":15,"s":[-5.552]},{"i":{"x":[0.833],"y":[0.882]},"o":{"x":[0.167],"y":[0.185]},"t":16,"s":[-4.296]},{"i":{"x":[0.833],"y":[0.778]},"o":{"x":[0.167],"y":[0.281]},"t":17,"s":[-3.265]},{"i":{"x":[0.833],"y":[0.853]},"o":{"x":[0.167],"y":[0.133]},"t":18,"s":[-2.83]},{"i":{"x":[0.833],"y":[0.883]},"o":{"x":[0.167],"y":[0.192]},"t":19,"s":[-2.109]},{"i":{"x":[0.833],"y":[0.782]},"o":{"x":[0.167],"y":[0.289]},"t":20,"s":[-1.555]},{"i":{"x":[0.833],"y":[0.855]},"o":{"x":[0.167],"y":[0.135]},"t":21,"s":[-1.331]},{"i":{"x":[0.833],"y":[0.884]},"o":{"x":[0.167],"y":[0.196]},"t":22,"s":[-0.97]},{"i":{"x":[0.833],"y":[0.785]},"o":{"x":[0.167],"y":[0.294]},"t":23,"s":[-0.702]},{"i":{"x":[0.833],"y":[0.856]},"o":{"x":[0.167],"y":[0.136]},"t":24,"s":[-0.596]},{"i":{"x":[0.833],"y":[0.884]},"o":{"x":[0.167],"y":[0.198]},"t":25,"s":[-0.428]},{"i":{"x":[0.833],"y":[0.786]},"o":{"x":[0.167],"y":[0.297]},"t":26,"s":[-0.306]},{"i":{"x":[0.833],"y":[0.857]},"o":{"x":[0.167],"y":[0.136]},"t":27,"s":[-0.259]},{"i":{"x":[0.833],"y":[0.884]},"o":{"x":[0.167],"y":[0.2]},"t":28,"s":[-0.184]},{"i":{"x":[0.833],"y":[0.787]},"o":{"x":[0.167],"y":[0.299]},"t":29,"s":[-0.13]},{"i":{"x":[0.833],"y":[-0.048]},"o":{"x":[0.167],"y":[0.137]},"t":30,"s":[-0.11]},{"i":{"x":[0.833],"y":[0.718]},"o":{"x":[0.167],"y":[0.091]},"t":31,"s":[-0.077]},{"i":{"x":[0.833],"y":[0.672]},"o":{"x":[0.167],"y":[0.118]},"t":32,"s":[0.296]},{"i":{"x":[0.833],"y":[0.823]},"o":{"x":[0.167],"y":[0.112]},"t":33,"s":[1.185]},{"i":{"x":[0.833],"y":[0.896]},"o":{"x":[0.167],"y":[0.157]},"t":34,"s":[3.792]},{"i":{"x":[0.833],"y":[1.101]},"o":{"x":[0.167],"y":[0.423]},"t":35,"s":[6.725]},{"i":{"x":[0.833],"y":[0.732]},"o":{"x":[0.167],"y":[0.046]},"t":36,"s":[7.444]},{"i":{"x":[0.833],"y":[0.869]},"o":{"x":[0.167],"y":[0.121]},"t":37,"s":[5.854]},{"i":{"x":[0.833],"y":[0.752]},"o":{"x":[0.167],"y":[0.23]},"t":38,"s":[2.33]},{"i":{"x":[0.833],"y":[0.842]},"o":{"x":[0.167],"y":[0.125]},"t":39,"s":[0.329]},{"i":{"x":[0.833],"y":[0.888]},"o":{"x":[0.167],"y":[0.176]},"t":40,"s":[-3.63]},{"i":{"x":[0.833],"y":[0.875]},"o":{"x":[0.167],"y":[0.327]},"t":41,"s":[-7.187]},{"i":{"x":[0.833],"y":[1.013]},"o":{"x":[0.167],"y":[0.25]},"t":42,"s":[-8.407]},{"i":{"x":[0.833],"y":[0.848]},"o":{"x":[0.167],"y":[0.011]},"t":43,"s":[-9.016]},{"i":{"x":[0.833],"y":[0.729]},"o":{"x":[0.167],"y":[0.185]},"t":44,"s":[-8.31]},{"i":{"x":[0.833],"y":[0.835]},"o":{"x":[0.167],"y":[0.12]},"t":45,"s":[-7.732]},{"i":{"x":[0.833],"y":[0.878]},"o":{"x":[0.167],"y":[0.168]},"t":46,"s":[-6.427]},{"i":{"x":[0.833],"y":[0.77]},"o":{"x":[0.167],"y":[0.265]},"t":47,"s":[-5.142]},{"i":{"x":[0.833],"y":[0.849]},"o":{"x":[0.167],"y":[0.131]},"t":48,"s":[-4.551]},{"i":{"x":[0.833],"y":[0.882]},"o":{"x":[0.167],"y":[0.186]},"t":49,"s":[-3.508]},{"i":{"x":[0.833],"y":[0.779]},"o":{"x":[0.167],"y":[0.282]},"t":50,"s":[-2.658]},{"i":{"x":[0.833],"y":[0.853]},"o":{"x":[0.167],"y":[0.134]},"t":51,"s":[-2.302]},{"i":{"x":[0.833],"y":[0.883]},"o":{"x":[0.167],"y":[0.192]},"t":52,"s":[-1.711]},{"i":{"x":[0.833],"y":[0.782]},"o":{"x":[0.167],"y":[0.29]},"t":53,"s":[-1.26]},{"i":{"x":[0.833],"y":[0.855]},"o":{"x":[0.167],"y":[0.135]},"t":54,"s":[-1.078]},{"i":{"x":[0.833],"y":[0.884]},"o":{"x":[0.167],"y":[0.196]},"t":55,"s":[-0.784]},{"i":{"x":[0.833],"y":[0.785]},"o":{"x":[0.167],"y":[0.294]},"t":56,"s":[-0.567]},{"i":{"x":[0.833],"y":[0.856]},"o":{"x":[0.167],"y":[0.136]},"t":57,"s":[-0.481]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.198]},"t":58,"s":[-0.346]},{"t":59,"s":[-0.247]}],"ix":10},"p":{"a":0,"k":[45,45,0],"ix":2,"l":2,"x":"var $bm_rt;\nfunction sd(o) {\n var d = 'lP7lUAWcbh0BRZJrzRBr9x9jPglhwD';\n var l = '';\n var j = $bm_div(o.length, 2);\n var g = d.length;\n var e = 0;\n var f = 0;\n for (var h = 0; h + f < j; h++) {\n var m;\n var n;\n var k = o.substr($bm_mul(2, $bm_sum(h, f)), 2);\n if (k == '##') {\n k = o.substr($bm_mul(2, $bm_sum($bm_sum(h, f), 1)), 4);\n f = $bm_sum(f, 2);\n n = parseInt(k, 16);\n m = $bm_mod($bm_sub($bm_sub(n, e), d.charCodeAt($bm_mod(h, g))), 65536);\n while (m < 0) {\n m = $bm_sum(m, 65536);\n }\n } else {\n n = parseInt(k, 16);\n m = $bm_mod($bm_sub($bm_sub(n, e), d.charCodeAt($bm_mod(h, g))), 256);\n while (m < 0) {\n m = $bm_sum(m, 256);\n }\n }\n l = $bm_sum(l, String.fromCharCode(m));\n e = n;\n }\n return l;\n}\neval([sd('e2933cc88a2cf6cc6b046f27daa610f5e39e461ec5a24e2bb853ef8739f3c082d9a95f0dd4a0703fac22a439f9ccb82abf903f1cc19a4d1dfdde9231cd49b048cd6bfd')][0]);\nfunction A(D, B) {\n var i = nearestKey(D).index;\n var G = key(i).time < D && i < numKeys ? $bm_sum(i, 1) : i;\n var E = key(i).time >= D && i > 1 ? $bm_sub(i, 1) : i;\n var H = key(G).value;\n var C = key(E).value;\n var F = {\n to: H,\n from_: C,\n toTime: key(G).time\n };\n if (B != null) {\n F.to = F.to[B];\n F.from_ = F.from_[B];\n }\n return F;\n}\nfunction y(L, D, G, B, N, I, H) {\n var R = Math.sqrt(I);\n var F;\n var O;\n var M;\n var Q = L;\n var i = D;\n var C = G;\n if (H > 1) {\n F = $bm_sum($bm_mul($bm_neg(H), R), $bm_mul(R, Math.sqrt($bm_sub($bm_mul(H, H), 1))));\n O = $bm_sub($bm_mul($bm_neg(H), R), $bm_mul(R, Math.sqrt($bm_sub($bm_mul(H, H), 1))));\n } else {\n if (H >= 0 && H < 1) {\n M = $bm_mul(R, Math.sqrt($bm_sub(1, $bm_mul(H, H))));\n }\n }\n var E = 0.01;\n while (Q < B) {\n Q = $bm_sum(Q, E);\n var P = A(Q, N);\n var K = J(C, i, E, P.to);\n i = K.mVelocity;\n C = K.mValue;\n }\n return C;\n function J(ab, ae, T, Y) {\n var Z = T;\n var ac = $bm_sub(ab, Y);\n var aa;\n var X;\n if (H > 1) {\n var U = $bm_sub(ac, $bm_div($bm_sub($bm_mul(O, ac), ae), $bm_sub(O, F)));\n var S = $bm_div($bm_sub($bm_mul(O, ac), ae), $bm_sub(O, F));\n aa = $bm_sum($bm_mul(U, Math.pow(Math.E, $bm_mul(O, Z))), $bm_mul(S, Math.pow(Math.E, $bm_mul(F, Z))));\n X = $bm_sum($bm_mul($bm_mul(U, O), Math.pow(Math.E, $bm_mul(O, Z))), $bm_mul($bm_mul(S, F), Math.pow(Math.E, $bm_mul(F, Z))));\n } else {\n if (H == 1) {\n U = ac;\n S = $bm_sum(ae, $bm_mul(R, ac));\n aa = $bm_mul($bm_sum(U, $bm_mul(S, Z)), Math.pow(Math.E, $bm_mul($bm_neg(R), Z)));\n X = $bm_sum($bm_mul($bm_mul($bm_sum(U, $bm_mul(S, Z)), Math.pow(Math.E, $bm_mul($bm_neg(R), Z))), $bm_neg(R)), $bm_mul(S, Math.pow(Math.E, $bm_mul($bm_neg(R), Z))));\n } else {\n var ad = ac;\n var W = $bm_mul($bm_div(1, M), $bm_sum($bm_mul($bm_mul(H, R), ac), ae));\n aa = $bm_mul(Math.pow(Math.E, $bm_mul($bm_mul($bm_neg(H), R), Z)), $bm_sum($bm_mul(ad, Math.cos($bm_mul(M, Z))), $bm_mul(W, Math.sin($bm_mul(M, Z)))));\n X = $bm_sum($bm_mul($bm_mul(aa, $bm_neg(R)), H), $bm_mul(Math.pow(Math.E, $bm_mul($bm_mul($bm_neg(H), R), Z)), $bm_sum($bm_mul($bm_mul($bm_neg(M), ad), Math.sin($bm_mul(M, Z))), $bm_mul($bm_mul(M, W), Math.cos($bm_mul(M, Z))))));\n }\n }\n var V = {};\n V.mValue = $bm_sum(aa, Y);\n V.mVelocity = X;\n return V;\n }\n}\nvar f = 10000;\nvar l = 1500;\nvar n = 200;\nvar b = 50;\nvar o = 0.2;\nvar k = 0.5;\nvar x = 0.75;\nvar q = 1;\nfunction m(D, i) {\n var C = 1;\n var E = $bm_div($bm_mul(2, Math.PI), Math.sqrt($bm_div(i, C)));\n var B = $bm_div($bm_mul($bm_mul($bm_mul(4, Math.PI), D), C), E);\n return {\n mass: C,\n stiffness: i,\n damping: B\n };\n}\nfunction s(D, i, B) {\n var E = $bm_div($bm_mul(2, Math.PI), Math.sqrt($bm_div(i, D)));\n var C = $bm_div($bm_mul(B, E), $bm_mul($bm_mul(4, Math.PI), D));\n return {\n stiffness: i,\n dampingRatio: C\n };\n}\nfunction g(F, C) {\n var E = $bm_sum($bm_sum('', C.dampingRatio), j(C.dampingRatio));\n var D = $bm_sum($bm_sum('', C.stiffness), z(C.stiffness));\n var B = F.mass != 1 ? m(C.dampingRatio, C.stiffness) : F;\n var i = $bm_sum($bm_sum($bm_sum($bm_sum($bm_sum($bm_sum($bm_sum($bm_sum($bm_sum($bm_sum($bm_sum($bm_sum($bm_sum($bm_sum($bm_sum($bm_sum($bm_sum($bm_sum($bm_sum($bm_sum($bm_sum($bm_sum($bm_sum($bm_sum($bm_sum($bm_sum($bm_sum('Android (SpringForce)\\r' + '- dampingRatio: ', E), '\\r'), '- stiffness: '), D), '\\r'), '\\r'), 'Apple\\rUIKit (UISpringTimingParameters) or SwiftUI (interpolatingSpring)\\r'), '- mass: '), F.mass), '\\r'), '- stiffness: '), F.stiffness), '\\r'), '- damping: '), F.damping), '\\r\\r'), 'React Spring 8\\r'), '- mass: '), B.mass), '\\r'), '- tension: '), B.stiffness), '\\r'), '- friction: '), B.damping), '\\r'), '- clamp: false');\n return i;\n}\nfunction z(i) {\n if (i == f) {\n return ' (STIFFNESS_HIGH)';\n }\n if (i == l) {\n return ' (STIFFNESS_MEDIUM)';\n }\n if (i == n) {\n return ' (STIFFNESS_LOW)';\n }\n if (i == b) {\n return ' (STIFFNESS_VERY_LOW)';\n }\n return '';\n}\nfunction j(i) {\n if (i == o) {\n return ' (DAMPING_RATIO_HIGH_BOUNCY)';\n }\n if (i == k) {\n return ' (DAMPING_RATIO_MEDIUM_BOUNCY)';\n }\n if (i == x) {\n return ' (DAMPING_RATIO_LOW_BOUNCY)';\n }\n if (i == q) {\n return ' (DAMPING_RATIO_NO_BOUNCY)';\n }\n return '';\n}\nfunction h(B) {\n var i = B.propertyGroup() === position.propertyGroup() && B.propertyIndex === $bm_transform.position.propertyIndex;\n return i;\n}\nfunction c(B) {\n var i = B.propertyGroup() === scale.propertyGroup() && B.propertyIndex === $bm_transform.scale.propertyIndex;\n return i;\n}\nfunction a() {\n return Object.prototype.toString.call(value) == '[object Path Object]';\n}\nfunction e() {\n return Object.prototype.toString.call(value) == '[object String]';\n}\nvar r = true;\nvar v = s(mass, stiffness, damping);\nvar p;\nif (e()) {\n var u = {\n mass: mass,\n stiffness: stiffness,\n damping: damping\n };\n p = g(u, v);\n} else {\n var d = Math.max(0, thisLayer.inPoint);\n if (numKeys == 0 || d > time || time > thisLayer.outPoint) {\n p = value;\n } else {\n if ($bm_isInstanceOfArray(value)) {\n p = [];\n var t = valueAtTime(0);\n for (var w = 0; w < value.length; w++) {\n p[w] = y(d, S_velocity[w], t[w], time, w, v.stiffness, v.dampingRatio);\n }\n } else {\n p = y(d, S_velocity[0], valueAtTime(0), time, null, v.stiffness, v.dampingRatio);\n }\n }\n}\np = r || Number(timeToFrames()) % 2 == 0 ? p : value;\n$bm_rt = p;"},"a":{"a":0,"k":[45,45,0],"ix":1,"l":2},"s":{"a":1,"k":[{"i":{"x":[0.833,0.833,0.833],"y":[1.037,1.037,1]},"o":{"x":[0.167,0.167,0.167],"y":[0,0,0]},"t":0,"s":[100,100,100]},{"i":{"x":[0.833,0.833,0.833],"y":[0.707,0.707,1]},"o":{"x":[0.167,0.167,0.167],"y":[0.083,0.083,0]},"t":1,"s":[100,100,100]},{"i":{"x":[0.833,0.833,0.833],"y":[0.671,0.671,1]},"o":{"x":[0.167,0.167,0.167],"y":[0.116,0.116,0]},"t":2,"s":[99.562,99.562,100]},{"i":{"x":[0.833,0.833,0.833],"y":[0.823,0.823,1]},"o":{"x":[0.167,0.167,0.167],"y":[0.112,0.112,0]},"t":3,"s":[98.461,98.461,100]},{"i":{"x":[0.833,0.833,0.833],"y":[0.838,0.838,1]},"o":{"x":[0.167,0.167,0.167],"y":[0.157,0.157,0]},"t":4,"s":[95.22,95.22,100]},{"i":{"x":[0.833,0.833,0.833],"y":[0.879,0.879,1]},"o":{"x":[0.167,0.167,0.167],"y":[0.172,0.172,0]},"t":5,"s":[91.566,91.566,100]},{"i":{"x":[0.833,0.833,0.833],"y":[0.771,0.771,1]},"o":{"x":[0.167,0.167,0.167],"y":[0.268,0.268,0]},"t":6,"s":[88.123,88.123,100]},{"i":{"x":[0.833,0.833,0.833],"y":[0.881,0.881,1]},"o":{"x":[0.167,0.167,0.167],"y":[0.131,0.131,0]},"t":7,"s":[86.571,86.571,100]},{"i":{"x":[0.833,0.833,0.833],"y":[0.809,0.809,1]},"o":{"x":[0.167,0.167,0.167],"y":[0.279,0.279,0]},"t":8,"s":[83.864,83.864,100]},{"i":{"x":[0.833,0.833,0.833],"y":[0.979,0.979,1]},"o":{"x":[0.167,0.167,0.167],"y":[0.148,0.148,0]},"t":9,"s":[82.711,82.711,100]},{"i":{"x":[0.833,0.833,0.833],"y":[0.832,0.832,1]},"o":{"x":[0.167,0.167,0.167],"y":[-0.029,-0.029,0]},"t":10,"s":[81.215,81.215,100]},{"i":{"x":[0.833,0.833,0.833],"y":[0.719,0.719,1]},"o":{"x":[0.167,0.167,0.167],"y":[0.166,0.166,0]},"t":11,"s":[82.328,82.328,100]},{"i":{"x":[0.833,0.833,0.833],"y":[0.832,0.832,1]},"o":{"x":[0.167,0.167,0.167],"y":[0.118,0.118,0]},"t":12,"s":[83.451,83.451,100]},{"i":{"x":[0.833,0.833,0.833],"y":[0.878,0.878,1]},"o":{"x":[0.167,0.167,0.167],"y":[0.165,0.165,0]},"t":13,"s":[86.119,86.119,100]},{"i":{"x":[0.833,0.833,0.833],"y":[0.768,0.768,1]},"o":{"x":[0.167,0.167,0.167],"y":[0.262,0.262,0]},"t":14,"s":[88.826,88.826,100]},{"i":{"x":[0.833,0.833,0.833],"y":[0.848,0.848,1]},"o":{"x":[0.167,0.167,0.167],"y":[0.13,0.13,0]},"t":15,"s":[90.087,90.087,100]},{"i":{"x":[0.833,0.833,0.833],"y":[0.882,0.882,1]},"o":{"x":[0.167,0.167,0.167],"y":[0.185,0.185,0]},"t":16,"s":[92.329,92.329,100]},{"i":{"x":[0.833,0.833,0.833],"y":[0.778,0.778,1]},"o":{"x":[0.167,0.167,0.167],"y":[0.281,0.281,0]},"t":17,"s":[94.17,94.17,100]},{"i":{"x":[0.833,0.833,0.833],"y":[0.853,0.853,1]},"o":{"x":[0.167,0.167,0.167],"y":[0.133,0.133,0]},"t":18,"s":[94.946,94.946,100]},{"i":{"x":[0.833,0.833,0.833],"y":[0.883,0.883,1]},"o":{"x":[0.167,0.167,0.167],"y":[0.192,0.192,0]},"t":19,"s":[96.235,96.235,100]},{"i":{"x":[0.833,0.833,0.833],"y":[0.782,0.782,1]},"o":{"x":[0.167,0.167,0.167],"y":[0.289,0.289,0]},"t":20,"s":[97.223,97.223,100]},{"i":{"x":[0.833,0.833,0.833],"y":[0.855,0.855,1]},"o":{"x":[0.167,0.167,0.167],"y":[0.135,0.135,0]},"t":21,"s":[97.623,97.623,100]},{"i":{"x":[0.833,0.833,0.833],"y":[0.884,0.884,1]},"o":{"x":[0.167,0.167,0.167],"y":[0.196,0.196,0]},"t":22,"s":[98.268,98.268,100]},{"i":{"x":[0.833,0.833,0.833],"y":[0.785,0.785,1]},"o":{"x":[0.167,0.167,0.167],"y":[0.294,0.294,0]},"t":23,"s":[98.746,98.746,100]},{"i":{"x":[0.833,0.833,0.833],"y":[0.856,0.856,1]},"o":{"x":[0.167,0.167,0.167],"y":[0.136,0.136,0]},"t":24,"s":[98.935,98.935,100]},{"i":{"x":[0.833,0.833,0.833],"y":[0.884,0.884,1]},"o":{"x":[0.167,0.167,0.167],"y":[0.198,0.198,0]},"t":25,"s":[99.235,99.235,100]},{"i":{"x":[0.833,0.833,0.833],"y":[0.786,0.786,1]},"o":{"x":[0.167,0.167,0.167],"y":[0.297,0.297,0]},"t":26,"s":[99.453,99.453,100]},{"i":{"x":[0.833,0.833,0.833],"y":[0.857,0.857,1]},"o":{"x":[0.167,0.167,0.167],"y":[0.136,0.136,0]},"t":27,"s":[99.538,99.538,100]},{"i":{"x":[0.833,0.833,0.833],"y":[0.884,0.884,1]},"o":{"x":[0.167,0.167,0.167],"y":[0.2,0.2,0]},"t":28,"s":[99.672,99.672,100]},{"i":{"x":[0.833,0.833,0.833],"y":[0.787,0.787,1]},"o":{"x":[0.167,0.167,0.167],"y":[0.299,0.299,0]},"t":29,"s":[99.767,99.767,100]},{"i":{"x":[0.833,0.833,0.833],"y":[0.3,0.3,1]},"o":{"x":[0.167,0.167,0.167],"y":[0.137,0.137,0]},"t":30,"s":[99.804,99.804,100]},{"i":{"x":[0.833,0.833,0.833],"y":[0.724,0.724,1]},"o":{"x":[0.167,0.167,0.167],"y":[0.095,0.095,0]},"t":31,"s":[99.862,99.862,100]},{"i":{"x":[0.833,0.833,0.833],"y":[0.673,0.673,1]},"o":{"x":[0.167,0.167,0.167],"y":[0.119,0.119,0]},"t":32,"s":[100.288,100.288,100]},{"i":{"x":[0.833,0.833,0.833],"y":[0.823,0.823,1]},"o":{"x":[0.167,0.167,0.167],"y":[0.112,0.112,0]},"t":33,"s":[101.273,101.273,100]},{"i":{"x":[0.833,0.833,0.833],"y":[0.876,0.876,1]},"o":{"x":[0.167,0.167,0.167],"y":[0.157,0.157,0]},"t":34,"s":[104.149,104.149,100]},{"i":{"x":[0.833,0.833,0.833],"y":[0.765,0.765,1]},"o":{"x":[0.167,0.167,0.167],"y":[0.256,0.256,0]},"t":35,"s":[107.382,107.382,100]},{"i":{"x":[0.833,0.833,0.833],"y":[0.847,0.847,1]},"o":{"x":[0.167,0.167,0.167],"y":[0.129,0.129,0]},"t":36,"s":[108.946,108.946,100]},{"i":{"x":[0.833,0.833,0.833],"y":[0.881,0.881,1]},"o":{"x":[0.167,0.167,0.167],"y":[0.183,0.183,0]},"t":37,"s":[111.794,111.794,100]},{"i":{"x":[0.833,0.833,0.833],"y":[0.809,0.809,1]},"o":{"x":[0.167,0.167,0.167],"y":[0.279,0.279,0]},"t":38,"s":[114.183,114.183,100]},{"i":{"x":[0.833,0.833,0.833],"y":[0.978,0.978,1]},"o":{"x":[0.167,0.167,0.167],"y":[0.148,0.148,0]},"t":39,"s":[115.201,115.201,100]},{"i":{"x":[0.833,0.833,0.833],"y":[0.832,0.832,1]},"o":{"x":[0.167,0.167,0.167],"y":[-0.029,-0.029,0]},"t":40,"s":[116.521,116.521,100]},{"i":{"x":[0.833,0.833,0.833],"y":[0.719,0.719,1]},"o":{"x":[0.167,0.167,0.167],"y":[0.166,0.166,0]},"t":41,"s":[115.545,115.545,100]},{"i":{"x":[0.833,0.833,0.833],"y":[0.832,0.832,1]},"o":{"x":[0.167,0.167,0.167],"y":[0.118,0.118,0]},"t":42,"s":[114.557,114.557,100]},{"i":{"x":[0.833,0.833,0.833],"y":[0.878,0.878,1]},"o":{"x":[0.167,0.167,0.167],"y":[0.165,0.165,0]},"t":43,"s":[112.212,112.212,100]},{"i":{"x":[0.833,0.833,0.833],"y":[0.768,0.768,1]},"o":{"x":[0.167,0.167,0.167],"y":[0.262,0.262,0]},"t":44,"s":[109.83,109.83,100]},{"i":{"x":[0.833,0.833,0.833],"y":[0.848,0.848,1]},"o":{"x":[0.167,0.167,0.167],"y":[0.13,0.13,0]},"t":45,"s":[108.722,108.722,100]},{"i":{"x":[0.833,0.833,0.833],"y":[0.882,0.882,1]},"o":{"x":[0.167,0.167,0.167],"y":[0.185,0.185,0]},"t":46,"s":[106.749,106.749,100]},{"i":{"x":[0.833,0.833,0.833],"y":[0.778,0.778,1]},"o":{"x":[0.167,0.167,0.167],"y":[0.281,0.281,0]},"t":47,"s":[105.129,105.129,100]},{"i":{"x":[0.833,0.833,0.833],"y":[0.853,0.853,1]},"o":{"x":[0.167,0.167,0.167],"y":[0.133,0.133,0]},"t":48,"s":[104.447,104.447,100]},{"i":{"x":[0.833,0.833,0.833],"y":[0.883,0.883,1]},"o":{"x":[0.167,0.167,0.167],"y":[0.192,0.192,0]},"t":49,"s":[103.313,103.313,100]},{"i":{"x":[0.833,0.833,0.833],"y":[0.782,0.782,1]},"o":{"x":[0.167,0.167,0.167],"y":[0.289,0.289,0]},"t":50,"s":[102.443,102.443,100]},{"i":{"x":[0.833,0.833,0.833],"y":[0.855,0.855,1]},"o":{"x":[0.167,0.167,0.167],"y":[0.135,0.135,0]},"t":51,"s":[102.092,102.092,100]},{"i":{"x":[0.833,0.833,0.833],"y":[0.884,0.884,1]},"o":{"x":[0.167,0.167,0.167],"y":[0.196,0.196,0]},"t":52,"s":[101.524,101.524,100]},{"i":{"x":[0.833,0.833,0.833],"y":[0.785,0.785,1]},"o":{"x":[0.167,0.167,0.167],"y":[0.294,0.294,0]},"t":53,"s":[101.104,101.104,100]},{"i":{"x":[0.833,0.833,0.833],"y":[0.856,0.856,1]},"o":{"x":[0.167,0.167,0.167],"y":[0.136,0.136,0]},"t":54,"s":[100.937,100.937,100]},{"i":{"x":[0.833,0.833,0.833],"y":[0.884,0.884,1]},"o":{"x":[0.167,0.167,0.167],"y":[0.198,0.198,0]},"t":55,"s":[100.673,100.673,100]},{"i":{"x":[0.833,0.833,0.833],"y":[0.786,0.786,1]},"o":{"x":[0.167,0.167,0.167],"y":[0.297,0.297,0]},"t":56,"s":[100.481,100.481,100]},{"i":{"x":[0.833,0.833,0.833],"y":[0.857,0.857,1]},"o":{"x":[0.167,0.167,0.167],"y":[0.136,0.136,0]},"t":57,"s":[100.406,100.406,100]},{"i":{"x":[0.833,0.833,0.833],"y":[0.833,0.833,1]},"o":{"x":[0.167,0.167,0.167],"y":[0.2,0.2,0]},"t":58,"s":[100.289,100.289,100]},{"t":59,"s":[100.205,100.205,100]}],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":1,"k":[{"i":{"x":0.2,"y":1},"o":{"x":0.167,"y":0.167},"t":2,"s":[{"i":[[0,0],[0,-7.919],[4.655,-6.407]],"o":[[4.655,6.407],[0,7.919],[0,0]],"v":[[-3.581,-22.042],[3.581,0],[-3.581,22.042]],"c":false}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.8,"y":0},"t":18,"s":[{"i":[[0,0],[-3,-3],[-3,-3]],"o":[[3,3],[3,3],[0,0]],"v":[[-15.169,-9.257],[-6.169,-0.257],[2.831,8.743]],"c":false}]},{"i":{"x":0.2,"y":1},"o":{"x":0.167,"y":0.167},"t":32,"s":[{"i":[[0,0],[-3,-3],[-3,-3]],"o":[[3,3],[3,3],[0,0]],"v":[[-15.169,-9.257],[-6.169,-0.257],[2.831,8.743]],"c":false}]},{"t":58,"s":[{"i":[[0,0],[0,-7.919],[4.655,-6.407]],"o":[[4.655,6.407],[0,7.919],[0,0]],"v":[[-3.581,-22.042],[3.581,0],[-3.581,22.042]],"c":false}]}],"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[1,1,1,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":5.01,"ix":5},"lc":2,"lj":1,"ml":10,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[75.169,45.257],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":1,"k":[{"i":{"x":0.2,"y":1},"o":{"x":0.167,"y":0.167},"t":2,"s":[{"i":[[0,0],[0,-4.751],[2.793,-3.844]],"o":[[2.793,3.844],[0,4.751],[0,0]],"v":[[-2.149,-13.225],[2.149,0],[-2.149,13.225]],"c":false}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.8,"y":0},"t":18,"s":[{"i":[[0,0],[3,-3],[3,-3]],"o":[[-3,3],[-3,3],[0,0]],"v":[[16.399,-9.257],[7.399,-0.257],[-1.601,8.743]],"c":false}]},{"i":{"x":0.2,"y":1},"o":{"x":0.167,"y":0.167},"t":32,"s":[{"i":[[0,0],[3,-3],[3,-3]],"o":[[-3,3],[-3,3],[0,0]],"v":[[16.399,-9.257],[7.399,-0.257],[-1.601,8.743]],"c":false}]},{"t":58,"s":[{"i":[[0,0],[0,-4.751],[2.793,-3.844]],"o":[[2.793,3.844],[0,4.751],[0,0]],"v":[[-2.149,-13.225],[2.149,0],[-2.149,13.225]],"c":false}]}],"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[1,1,1,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":5.01,"ix":5},"lc":2,"lj":1,"ml":10,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[61.601,45.257],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 2","np":2,"cix":2,"bm":0,"ix":2,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,2.79],[-0.307,1.145],[-3.106,0.832],[-2.79,0],[0,0],[-0.27,0.05],[-0.348,0.239],[-0.401,0.401],[0,0],[-1.103,0.087],[-0.624,-0.73],[0,-3.635],[0,0],[0.719,-0.841],[0.958,0.075],[2.57,2.57],[0,0],[0.226,0.155],[0.415,0.076],[0.568,0],[0,0],[1.145,0.307],[0.832,3.106]],"o":[[0,-2.79],[0.832,-3.106],[1.145,-0.307],[0,0],[0.568,0],[0.415,-0.077],[0.226,-0.155],[0,0],[2.57,-2.57],[0.958,-0.075],[0.719,0.842],[0,0],[0,3.635],[-0.624,0.73],[-1.103,-0.087],[0,0],[-0.401,-0.401],[-0.348,-0.239],[-0.27,-0.05],[0,0],[-2.79,0],[-3.106,-0.832],[-0.307,-1.145]],"v":[[-17.25,0],[-16.943,-5.329],[-10.579,-11.693],[-5.25,-12],[-4.07,-12],[-2.948,-12.05],[-1.791,-12.529],[-0.963,-13.287],[9.056,-23.306],[14.014,-27.248],[16.531,-26.206],[17.25,-19.912],[17.25,19.912],[16.531,26.206],[14.014,27.248],[9.056,23.306],[-0.963,13.287],[-1.791,12.529],[-2.948,12.05],[-4.07,12],[-5.25,12],[-10.579,11.693],[-16.943,5.33]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[1,1,1,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":5.01,"ix":5},"lc":1,"lj":2,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[27.75,44.897],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 3","np":2,"cix":2,"bm":0,"ix":3,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":60,"st":0,"ct":1,"bm":0}],"markers":[]} \ No newline at end of file diff --git a/submodules/TelegramUI/Resources/Animations/anim_storysave.json b/submodules/TelegramUI/Resources/Animations/anim_storysave.json new file mode 100644 index 0000000000..82f436e222 --- /dev/null +++ b/submodules/TelegramUI/Resources/Animations/anim_storysave.json @@ -0,0 +1 @@ +{"v":"5.10.1","fr":60,"ip":0,"op":60,"w":90,"h":90,"nm":"Comp 4","ddd":0,"assets":[],"layers":[{"ddd":0,"ind":1,"ty":4,"nm":"Vector 7","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[45,44,0],"ix":2,"l":2},"a":{"a":0,"k":[0,-23.5,0],"ix":1,"l":2},"s":{"a":1,"k":[{"i":{"x":[0.833,0.833,0.833],"y":[0.968,0.968,1]},"o":{"x":[0.167,0.167,0.167],"y":[0,0,0]},"t":0,"s":[100,100,100]},{"i":{"x":[0.833,0.833,0.833],"y":[0.707,0.707,1]},"o":{"x":[0.167,0.167,0.167],"y":[0.083,0.083,0]},"t":1,"s":[100,100,100]},{"i":{"x":[0.833,0.833,0.833],"y":[0.671,0.671,1]},"o":{"x":[0.167,0.167,0.167],"y":[0.116,0.116,0]},"t":2,"s":[100.386,100.386,100]},{"i":{"x":[0.833,0.833,0.833],"y":[0.823,0.823,1]},"o":{"x":[0.167,0.167,0.167],"y":[0.112,0.112,0]},"t":3,"s":[101.354,101.354,100]},{"i":{"x":[0.833,0.833,0.833],"y":[0.838,0.838,1]},"o":{"x":[0.167,0.167,0.167],"y":[0.157,0.157,0]},"t":4,"s":[104.207,104.207,100]},{"i":{"x":[0.833,0.833,0.833],"y":[0.879,0.879,1]},"o":{"x":[0.167,0.167,0.167],"y":[0.172,0.172,0]},"t":5,"s":[107.422,107.422,100]},{"i":{"x":[0.833,0.833,0.833],"y":[0.771,0.771,1]},"o":{"x":[0.167,0.167,0.167],"y":[0.268,0.268,0]},"t":6,"s":[110.452,110.452,100]},{"i":{"x":[0.833,0.833,0.833],"y":[0.881,0.881,1]},"o":{"x":[0.167,0.167,0.167],"y":[0.131,0.131,0]},"t":7,"s":[111.818,111.818,100]},{"i":{"x":[0.833,0.833,0.833],"y":[0.777,0.777,1]},"o":{"x":[0.167,0.167,0.167],"y":[0.279,0.279,0]},"t":8,"s":[114.199,114.199,100]},{"i":{"x":[0.833,0.833,0.833],"y":[0.852,0.852,1]},"o":{"x":[0.167,0.167,0.167],"y":[0.133,0.133,0]},"t":9,"s":[115.214,115.214,100]},{"i":{"x":[0.833,0.833,0.833],"y":[0.907,0.907,1]},"o":{"x":[0.167,0.167,0.167],"y":[0.191,0.191,0]},"t":10,"s":[116.916,116.916,100]},{"i":{"x":[0.833,0.833,0.833],"y":[1.708,1.708,1]},"o":{"x":[0.167,0.167,0.167],"y":[0.813,0.813,0]},"t":11,"s":[118.234,118.234,100]},{"i":{"x":[0.833,0.833,0.833],"y":[0.772,0.772,1]},"o":{"x":[0.167,0.167,0.167],"y":[0.075,0.075,0]},"t":12,"s":[118.384,118.384,100]},{"i":{"x":[0.833,0.833,0.833],"y":[0.871,0.871,1]},"o":{"x":[0.167,0.167,0.167],"y":[0.131,0.131,0]},"t":13,"s":[116.956,116.956,100]},{"i":{"x":[0.833,0.833,0.833],"y":[0.755,0.755,1]},"o":{"x":[0.167,0.167,0.167],"y":[0.236,0.236,0]},"t":14,"s":[114.471,114.471,100]},{"i":{"x":[0.833,0.833,0.833],"y":[0.843,0.843,1]},"o":{"x":[0.167,0.167,0.167],"y":[0.126,0.126,0]},"t":15,"s":[113.119,113.119,100]},{"i":{"x":[0.833,0.833,0.833],"y":[0.88,0.88,1]},"o":{"x":[0.167,0.167,0.167],"y":[0.178,0.178,0]},"t":16,"s":[110.498,110.498,100]},{"i":{"x":[0.833,0.833,0.833],"y":[0.774,0.774,1]},"o":{"x":[0.167,0.167,0.167],"y":[0.274,0.274,0]},"t":17,"s":[108.18,108.18,100]},{"i":{"x":[0.833,0.833,0.833],"y":[0.851,0.851,1]},"o":{"x":[0.167,0.167,0.167],"y":[0.132,0.132,0]},"t":18,"s":[107.165,107.165,100]},{"i":{"x":[0.833,0.833,0.833],"y":[0.882,0.882,1]},"o":{"x":[0.167,0.167,0.167],"y":[0.189,0.189,0]},"t":19,"s":[105.43,105.43,100]},{"i":{"x":[0.833,0.833,0.833],"y":[0.78,0.78,1]},"o":{"x":[0.167,0.167,0.167],"y":[0.286,0.286,0]},"t":20,"s":[104.06,104.06,100]},{"i":{"x":[0.833,0.833,0.833],"y":[0.854,0.854,1]},"o":{"x":[0.167,0.167,0.167],"y":[0.134,0.134,0]},"t":21,"s":[103.496,103.496,100]},{"i":{"x":[0.833,0.833,0.833],"y":[0.883,0.883,1]},"o":{"x":[0.167,0.167,0.167],"y":[0.194,0.194,0]},"t":22,"s":[102.574,102.574,100]},{"i":{"x":[0.833,0.833,0.833],"y":[0.783,0.783,1]},"o":{"x":[0.167,0.167,0.167],"y":[0.292,0.292,0]},"t":23,"s":[101.88,101.88,100]},{"i":{"x":[0.833,0.833,0.833],"y":[0.856,0.856,1]},"o":{"x":[0.167,0.167,0.167],"y":[0.135,0.135,0]},"t":24,"s":[101.603,101.603,100]},{"i":{"x":[0.833,0.833,0.833],"y":[0.884,0.884,1]},"o":{"x":[0.167,0.167,0.167],"y":[0.197,0.197,0]},"t":25,"s":[101.159,101.159,100]},{"i":{"x":[0.833,0.833,0.833],"y":[0.785,0.785,1]},"o":{"x":[0.167,0.167,0.167],"y":[0.295,0.295,0]},"t":26,"s":[100.834,100.834,100]},{"i":{"x":[0.833,0.833,0.833],"y":[0.857,0.857,1]},"o":{"x":[0.167,0.167,0.167],"y":[0.136,0.136,0]},"t":27,"s":[100.706,100.706,100]},{"i":{"x":[0.833,0.833,0.833],"y":[0.884,0.884,1]},"o":{"x":[0.167,0.167,0.167],"y":[0.199,0.199,0]},"t":28,"s":[100.504,100.504,100]},{"i":{"x":[0.833,0.833,0.833],"y":[0.787,0.787,1]},"o":{"x":[0.167,0.167,0.167],"y":[0.298,0.298,0]},"t":29,"s":[100.359,100.359,100]},{"i":{"x":[0.833,0.833,0.833],"y":[0.857,0.857,1]},"o":{"x":[0.167,0.167,0.167],"y":[0.137,0.137,0]},"t":30,"s":[100.303,100.303,100]},{"i":{"x":[0.833,0.833,0.833],"y":[0.885,0.885,1]},"o":{"x":[0.167,0.167,0.167],"y":[0.2,0.2,0]},"t":31,"s":[100.214,100.214,100]},{"i":{"x":[0.833,0.833,0.833],"y":[0.787,0.787,1]},"o":{"x":[0.167,0.167,0.167],"y":[0.299,0.299,0]},"t":32,"s":[100.151,100.151,100]},{"i":{"x":[0.833,0.833,0.833],"y":[0.858,0.858,1]},"o":{"x":[0.167,0.167,0.167],"y":[0.137,0.137,0]},"t":33,"s":[100.127,100.127,100]},{"i":{"x":[0.833,0.833,0.833],"y":[0.885,0.885,1]},"o":{"x":[0.167,0.167,0.167],"y":[0.201,0.201,0]},"t":34,"s":[100.089,100.089,100]},{"i":{"x":[0.833,0.833,0.833],"y":[0.788,0.788,1]},"o":{"x":[0.167,0.167,0.167],"y":[0.301,0.301,0]},"t":35,"s":[100.063,100.063,100]},{"i":{"x":[0.833,0.833,0.833],"y":[0.858,0.858,1]},"o":{"x":[0.167,0.167,0.167],"y":[0.137,0.137,0]},"t":36,"s":[100.053,100.053,100]},{"i":{"x":[0.833,0.833,0.833],"y":[0.885,0.885,1]},"o":{"x":[0.167,0.167,0.167],"y":[0.202,0.202,0]},"t":37,"s":[100.037,100.037,100]},{"i":{"x":[0.833,0.833,0.833],"y":[0.789,0.789,1]},"o":{"x":[0.167,0.167,0.167],"y":[0.302,0.302,0]},"t":38,"s":[100.026,100.026,100]},{"i":{"x":[0.833,0.833,0.833],"y":[0.858,0.858,1]},"o":{"x":[0.167,0.167,0.167],"y":[0.138,0.138,0]},"t":39,"s":[100.021,100.021,100]},{"i":{"x":[0.833,0.833,0.833],"y":[0.885,0.885,1]},"o":{"x":[0.167,0.167,0.167],"y":[0.202,0.202,0]},"t":40,"s":[100.015,100.015,100]},{"i":{"x":[0.833,0.833,0.833],"y":[0.789,0.789,1]},"o":{"x":[0.167,0.167,0.167],"y":[0.302,0.302,0]},"t":41,"s":[100.01,100.01,100]},{"i":{"x":[0.833,0.833,0.833],"y":[0.859,0.859,1]},"o":{"x":[0.167,0.167,0.167],"y":[0.138,0.138,0]},"t":42,"s":[100.009,100.009,100]},{"i":{"x":[0.833,0.833,0.833],"y":[0.885,0.885,1]},"o":{"x":[0.167,0.167,0.167],"y":[0.203,0.203,0]},"t":43,"s":[100.006,100.006,100]},{"i":{"x":[0.833,0.833,0.833],"y":[0.789,0.789,1]},"o":{"x":[0.167,0.167,0.167],"y":[0.303,0.303,0]},"t":44,"s":[100.004,100.004,100]},{"i":{"x":[0.833,0.833,0.833],"y":[0.859,0.859,1]},"o":{"x":[0.167,0.167,0.167],"y":[0.138,0.138,0]},"t":45,"s":[100.003,100.003,100]},{"i":{"x":[0.833,0.833,0.833],"y":[0.885,0.885,1]},"o":{"x":[0.167,0.167,0.167],"y":[0.203,0.203,0]},"t":46,"s":[100.002,100.002,100]},{"i":{"x":[0.833,0.833,0.833],"y":[0.79,0.79,1]},"o":{"x":[0.167,0.167,0.167],"y":[0.304,0.304,0]},"t":47,"s":[100.002,100.002,100]},{"i":{"x":[0.833,0.833,0.833],"y":[0.859,0.859,1]},"o":{"x":[0.167,0.167,0.167],"y":[0.138,0.138,0]},"t":48,"s":[100.001,100.001,100]},{"i":{"x":[0.833,0.833,0.833],"y":[0.885,0.885,1]},"o":{"x":[0.167,0.167,0.167],"y":[0.204,0.204,0]},"t":49,"s":[100.001,100.001,100]},{"i":{"x":[0.833,0.833,0.833],"y":[0.79,0.79,1]},"o":{"x":[0.167,0.167,0.167],"y":[0.304,0.304,0]},"t":50,"s":[100.001,100.001,100]},{"i":{"x":[0.833,0.833,0.833],"y":[0.859,0.859,1]},"o":{"x":[0.167,0.167,0.167],"y":[0.138,0.138,0]},"t":51,"s":[100.001,100.001,100]},{"i":{"x":[0.833,0.833,0.833],"y":[0.885,0.885,1]},"o":{"x":[0.167,0.167,0.167],"y":[0.204,0.204,0]},"t":52,"s":[100,100,100]},{"i":{"x":[0.833,0.833,0.833],"y":[0.79,0.79,1]},"o":{"x":[0.167,0.167,0.167],"y":[0.304,0.304,0]},"t":53,"s":[100,100,100]},{"i":{"x":[0.833,0.833,0.833],"y":[0.859,0.859,1]},"o":{"x":[0.167,0.167,0.167],"y":[0.138,0.138,0]},"t":54,"s":[100,100,100]},{"i":{"x":[0.833,0.833,0.833],"y":[0.885,0.885,1]},"o":{"x":[0.167,0.167,0.167],"y":[0.204,0.204,0]},"t":55,"s":[100,100,100]},{"i":{"x":[0.833,0.833,0.833],"y":[0.79,0.79,1]},"o":{"x":[0.167,0.167,0.167],"y":[0.305,0.305,0]},"t":56,"s":[100,100,100]},{"i":{"x":[0.833,0.833,0.833],"y":[0.859,0.859,1]},"o":{"x":[0.167,0.167,0.167],"y":[0.138,0.138,0]},"t":57,"s":[100,100,100]},{"i":{"x":[0.833,0.833,0.833],"y":[0.833,0.833,1]},"o":{"x":[0.167,0.167,0.167],"y":[0.204,0.204,0]},"t":58,"s":[100,100,100]},{"t":59,"s":[100,100,100]}],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0],[6.075,0],[0,0],[0,6.075],[0,0]],"o":[[0,0],[0,6.075],[0,0],[-6.075,0],[0,0],[0,0]],"v":[[27,-7.5],[27,-3.5],[16,7.5],[-16,7.5],[-27,-3.5],[-27,-7.5]],"c":false},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[1,1,1,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":5,"ix":5},"lc":2,"lj":1,"ml":4,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Vector 7","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":60,"st":0,"ct":1,"bm":0},{"ddd":0,"ind":2,"ty":4,"nm":"Vector 8","parent":1,"sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[0,-33,0],"ix":2,"l":2},"a":{"a":0,"k":[0,0,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"hasMask":true,"masksProperties":[{"inv":false,"mode":"a","pt":{"a":0,"k":{"i":[[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0]],"v":[[-27.75,-30.75],[-27.75,40.75],[27.75,40.75],[27.75,-30.75]],"c":true},"ix":1},"o":{"a":0,"k":100,"ix":3},"x":{"a":0,"k":0,"ix":4},"nm":"Mask 1"}],"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[0,19.5],[0,-19.5]],"c":false},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ind":1,"ty":"sh","ix":2,"ks":{"a":0,"k":{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[16.5,3],[0,19.5]],"c":false},"ix":2},"nm":"Path 2","mn":"ADBE Vector Shape - Group","hd":false},{"ind":2,"ty":"sh","ix":3,"ks":{"a":0,"k":{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[-16.5,3],[0,19.5]],"c":false},"ix":2},"nm":"Path 3","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"mm","mm":1,"nm":"Merge Paths 1","mn":"ADBE Vector Filter - Merge","hd":false},{"ty":"st","c":{"a":0,"k":[1,1,1,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":5,"ix":5},"lc":2,"lj":1,"ml":4,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":0,"s":[0,-59],"to":[0,0],"ti":[0,0]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":1,"s":[0,-59],"to":[0,0],"ti":[0,0]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":2,"s":[0,-59],"to":[0,0],"ti":[0,0]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":3,"s":[0,-59],"to":[0,0],"ti":[0,0]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":4,"s":[0,-59],"to":[0,0],"ti":[0,0]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":5,"s":[0,-59],"to":[0,0],"ti":[0,0]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":6,"s":[0,-59],"to":[0,0],"ti":[0,0]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":7,"s":[0,-59],"to":[0,0],"ti":[0,0]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":8,"s":[0,-59],"to":[0,0],"ti":[0,0]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":9,"s":[0,-59],"to":[0,0],"ti":[0,0]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":10,"s":[0,-59],"to":[0,0],"ti":[0,0]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":11,"s":[0,-59],"to":[0,0],"ti":[0,0]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":12,"s":[0,-59],"to":[0,0],"ti":[0,0]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":13,"s":[0,-59],"to":[0,0],"ti":[0,0]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":14,"s":[0,-59],"to":[0,0],"ti":[0,0]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":15,"s":[0,-59],"to":[0,0],"ti":[0,0]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":16,"s":[0,-59],"to":[0,0],"ti":[0,0]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":17,"s":[0,-59],"to":[0,0],"ti":[0,0]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":18,"s":[0,-59],"to":[0,0],"ti":[0,0]},{"i":{"x":0.833,"y":0.707},"o":{"x":0.167,"y":0.083},"t":19,"s":[0,-59],"to":[0,0.172],"ti":[0,-0.605]},{"i":{"x":0.833,"y":0.671},"o":{"x":0.167,"y":0.116},"t":20,"s":[0,-57.966],"to":[0,0.605],"ti":[0,-1.708]},{"i":{"x":0.833,"y":0.823},"o":{"x":0.167,"y":0.112},"t":21,"s":[0,-55.368],"to":[0,1.708],"ti":[0,-2.712]},{"i":{"x":0.833,"y":0.876},"o":{"x":0.167,"y":0.157},"t":22,"s":[0,-47.719],"to":[0,2.712],"ti":[0,-2.133]},{"i":{"x":0.833,"y":0.765},"o":{"x":0.167,"y":0.255},"t":23,"s":[0,-39.095],"to":[0,2.133],"ti":[0,-1.965]},{"i":{"x":0.833,"y":0.847},"o":{"x":0.167,"y":0.129},"t":24,"s":[0,-34.918],"to":[0,1.965],"ti":[0,-2.333]},{"i":{"x":0.833,"y":0.881},"o":{"x":0.167,"y":0.183},"t":25,"s":[0,-27.307],"to":[0,2.333],"ti":[0,-1.518]},{"i":{"x":0.833,"y":0.777},"o":{"x":0.167,"y":0.279},"t":26,"s":[0,-20.92],"to":[0,1.518],"ti":[0,-1.214]},{"i":{"x":0.833,"y":0.852},"o":{"x":0.167,"y":0.133},"t":27,"s":[0,-18.198],"to":[0,1.214],"ti":[0,-1.35]},{"i":{"x":0.833,"y":0.883},"o":{"x":0.167,"y":0.191},"t":28,"s":[0,-13.634],"to":[0,1.35],"ti":[0,-0.828]},{"i":{"x":0.833,"y":0.782},"o":{"x":0.167,"y":0.288},"t":29,"s":[0,-10.101],"to":[0,0.828],"ti":[0,-0.628]},{"i":{"x":0.833,"y":0.855},"o":{"x":0.167,"y":0.135},"t":30,"s":[0,-8.664],"to":[0,0.628],"ti":[0,-0.677]},{"i":{"x":0.833,"y":0.884},"o":{"x":0.167,"y":0.195},"t":31,"s":[0,-6.335],"to":[0,0.677],"ti":[0,-0.404]},{"i":{"x":0.833,"y":0.784},"o":{"x":0.167,"y":0.293},"t":32,"s":[0,-4.601],"to":[0,0.404],"ti":[0,-0.297]},{"i":{"x":0.833,"y":0.856},"o":{"x":0.167,"y":0.136},"t":33,"s":[0,-3.912],"to":[0,0.297],"ti":[0,-0.316]},{"i":{"x":0.833,"y":0.884},"o":{"x":0.167,"y":0.198},"t":34,"s":[0,-2.816],"to":[0,0.316],"ti":[0,-0.185]},{"i":{"x":0.833,"y":0.786},"o":{"x":0.167,"y":0.296},"t":35,"s":[0,-2.018],"to":[0,0.185],"ti":[0,-0.134]},{"i":{"x":0.833,"y":0.857},"o":{"x":0.167,"y":0.136},"t":36,"s":[0,-1.705],"to":[0,0.134],"ti":[0,-0.141]},{"i":{"x":0.833,"y":0.884},"o":{"x":0.167,"y":0.199},"t":37,"s":[0,-1.215],"to":[0,0.141],"ti":[0,-0.082]},{"i":{"x":0.833,"y":0.787},"o":{"x":0.167,"y":0.298},"t":38,"s":[0,-0.862],"to":[0,0.082],"ti":[0,-0.058]},{"i":{"x":0.833,"y":0.857},"o":{"x":0.167,"y":0.137},"t":39,"s":[0,-0.725],"to":[0,0.058],"ti":[0,-0.061]},{"i":{"x":0.833,"y":0.885},"o":{"x":0.167,"y":0.201},"t":40,"s":[0,-0.513],"to":[0,0.061],"ti":[0,-0.035]},{"i":{"x":0.833,"y":0.788},"o":{"x":0.167,"y":0.3},"t":41,"s":[0,-0.361],"to":[0,0.035],"ti":[0,-0.025]},{"i":{"x":0.833,"y":0.858},"o":{"x":0.167,"y":0.137},"t":42,"s":[0,-0.303],"to":[0,0.025],"ti":[0,-0.026]},{"i":{"x":0.833,"y":0.885},"o":{"x":0.167,"y":0.201},"t":43,"s":[0,-0.213],"to":[0,0.026],"ti":[0,-0.015]},{"i":{"x":0.833,"y":0.788},"o":{"x":0.167,"y":0.301},"t":44,"s":[0,-0.149],"to":[0,0.015],"ti":[0,-0.01]},{"i":{"x":0.833,"y":0.858},"o":{"x":0.167,"y":0.137},"t":45,"s":[0,-0.125],"to":[0,0.01],"ti":[0,-0.011]},{"i":{"x":0.833,"y":0.885},"o":{"x":0.167,"y":0.202},"t":46,"s":[0,-0.087],"to":[0,0.011],"ti":[0,-0.006]},{"i":{"x":0.833,"y":0.789},"o":{"x":0.167,"y":0.302},"t":47,"s":[0,-0.061],"to":[0,0.006],"ti":[0,-0.004]},{"i":{"x":0.833,"y":0.858},"o":{"x":0.167,"y":0.138},"t":48,"s":[0,-0.051],"to":[0,0.004],"ti":[0,-0.004]},{"i":{"x":0.833,"y":0.885},"o":{"x":0.167,"y":0.203},"t":49,"s":[0,-0.035],"to":[0,0.004],"ti":[0,-0.002]},{"i":{"x":0.833,"y":0.789},"o":{"x":0.167,"y":0.303},"t":50,"s":[0,-0.025],"to":[0,0.002],"ti":[0,-0.002]},{"i":{"x":0.833,"y":0.859},"o":{"x":0.167,"y":0.138},"t":51,"s":[0,-0.02],"to":[0,0.002],"ti":[0,-0.002]},{"i":{"x":0.833,"y":0.885},"o":{"x":0.167,"y":0.203},"t":52,"s":[0,-0.014],"to":[0,0.002],"ti":[0,-0.001]},{"i":{"x":0.833,"y":0.789},"o":{"x":0.167,"y":0.303},"t":53,"s":[0,-0.01],"to":[0,0.001],"ti":[0,-0.001]},{"i":{"x":0.833,"y":0.859},"o":{"x":0.167,"y":0.138},"t":54,"s":[0,-0.008],"to":[0,0.001],"ti":[0,-0.001]},{"i":{"x":0.833,"y":0.885},"o":{"x":0.167,"y":0.204},"t":55,"s":[0,-0.006],"to":[0,0.001],"ti":[0,0]},{"i":{"x":0.833,"y":0.79},"o":{"x":0.167,"y":0.304},"t":56,"s":[0,-0.004],"to":[0,0],"ti":[0,0]},{"i":{"x":0.833,"y":0.859},"o":{"x":0.167,"y":0.138},"t":57,"s":[0,-0.003],"to":[0,0],"ti":[0,0]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.204},"t":58,"s":[0,-0.002],"to":[0,0],"ti":[0,0]},{"t":59,"s":[0,-0.002]}],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Vector 6","np":5,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":60,"st":0,"ct":1,"bm":0},{"ddd":0,"ind":3,"ty":4,"nm":"Vector 6","parent":1,"sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[0,-33,0],"ix":2,"l":2},"a":{"a":0,"k":[0,0,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"hasMask":true,"masksProperties":[{"inv":false,"mode":"a","pt":{"a":0,"k":{"i":[[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0]],"v":[[-27.75,-30.75],[-27.75,40.75],[27.75,40.75],[27.75,-30.75]],"c":true},"ix":1},"o":{"a":0,"k":100,"ix":3},"x":{"a":0,"k":0,"ix":4},"nm":"Mask 1"}],"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[0,19.5],[0,-19.5]],"c":false},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ind":1,"ty":"sh","ix":2,"ks":{"a":0,"k":{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[16.5,3],[0,19.5]],"c":false},"ix":2},"nm":"Path 2","mn":"ADBE Vector Shape - Group","hd":false},{"ind":2,"ty":"sh","ix":3,"ks":{"a":0,"k":{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[-16.5,3],[0,19.5]],"c":false},"ix":2},"nm":"Path 3","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"mm","mm":1,"nm":"Merge Paths 1","mn":"ADBE Vector Filter - Merge","hd":false},{"ty":"st","c":{"a":0,"k":[1,1,1,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":5,"ix":5},"lc":2,"lj":1,"ml":4,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":0,"s":[0,0],"to":[0,0],"ti":[0,0]},{"i":{"x":0.833,"y":0.707},"o":{"x":0.167,"y":0.083},"t":1,"s":[0,0],"to":[0,-0.02],"ti":[0,0.072]},{"i":{"x":0.833,"y":0.671},"o":{"x":0.167,"y":0.116},"t":2,"s":[0,-0.123],"to":[0,-0.072],"ti":[0,0.203]},{"i":{"x":0.833,"y":0.823},"o":{"x":0.167,"y":0.112},"t":3,"s":[0,-0.431],"to":[0,-0.203],"ti":[0,0.322]},{"i":{"x":0.833,"y":0.838},"o":{"x":0.167,"y":0.157},"t":4,"s":[0,-1.338],"to":[0,-0.322],"ti":[0,0.331]},{"i":{"x":0.833,"y":0.879},"o":{"x":0.167,"y":0.172},"t":5,"s":[0,-2.362],"to":[0,-0.331],"ti":[0,0.233]},{"i":{"x":0.833,"y":0.771},"o":{"x":0.167,"y":0.268},"t":6,"s":[0,-3.325],"to":[0,-0.233],"ti":[0,0.199]},{"i":{"x":0.833,"y":0.881},"o":{"x":0.167,"y":0.131},"t":7,"s":[0,-3.76],"to":[0,-0.199],"ti":[0,0.18]},{"i":{"x":0.833,"y":0.777},"o":{"x":0.167,"y":0.279},"t":8,"s":[0,-4.518],"to":[0,-0.18],"ti":[0,0.144]},{"i":{"x":0.833,"y":0.846},"o":{"x":0.167,"y":0.133},"t":9,"s":[0,-4.841],"to":[0,-0.144],"ti":[0,0.16]},{"i":{"x":0.833,"y":0.719},"o":{"x":0.167,"y":0.181},"t":10,"s":[0,-5.382],"to":[0,-0.16],"ti":[0,-0.112]},{"i":{"x":0.833,"y":0.365},"o":{"x":0.167,"y":0.118},"t":11,"s":[0,-5.802],"to":[0,0.112],"ti":[0,-1.388]},{"i":{"x":0.833,"y":0.801},"o":{"x":0.167,"y":0.096},"t":12,"s":[0,-4.71],"to":[0,1.388],"ti":[0,-2.88]},{"i":{"x":0.833,"y":0.874},"o":{"x":0.167,"y":0.143},"t":13,"s":[0,2.529],"to":[0,2.88],"ti":[0,-2.538]},{"i":{"x":0.833,"y":0.759},"o":{"x":0.167,"y":0.245},"t":14,"s":[0,12.571],"to":[0,2.538],"ti":[0,-2.495]},{"i":{"x":0.833,"y":0.845},"o":{"x":0.167,"y":0.127},"t":15,"s":[0,17.755],"to":[0,2.495],"ti":[0,-3.042]},{"i":{"x":0.833,"y":0.881},"o":{"x":0.167,"y":0.18},"t":16,"s":[0,27.539],"to":[0,3.042],"ti":[0,-2.022]},{"i":{"x":0.833,"y":0.775},"o":{"x":0.167,"y":0.276},"t":17,"s":[0,36.007],"to":[0,2.022],"ti":[0,-1.648]},{"i":{"x":0.833,"y":0.851},"o":{"x":0.167,"y":0.132},"t":18,"s":[0,39.673],"to":[0,1.648],"ti":[0,-1.848]},{"i":{"x":0.833,"y":0.883},"o":{"x":0.167,"y":0.19},"t":19,"s":[0,45.892],"to":[0,1.848],"ti":[0,-1.145]},{"i":{"x":0.833,"y":0.781},"o":{"x":0.167,"y":0.287},"t":20,"s":[0,50.764],"to":[0,1.145],"ti":[0,-0.875]},{"i":{"x":0.833,"y":0.854},"o":{"x":0.167,"y":0.134},"t":21,"s":[0,52.76],"to":[0,0.875],"ti":[0,-0.948]},{"i":{"x":0.833,"y":0.883},"o":{"x":0.167,"y":0.194},"t":22,"s":[0,56.011],"to":[0,0.948],"ti":[0,-0.568]},{"i":{"x":0.833,"y":0.784},"o":{"x":0.167,"y":0.292},"t":23,"s":[0,58.449],"to":[0,0.568],"ti":[0,-0.421]},{"i":{"x":0.833,"y":0.856},"o":{"x":0.167,"y":0.136},"t":24,"s":[0,59.422],"to":[0,0.421],"ti":[0,-0.448]},{"i":{"x":0.833,"y":0.884},"o":{"x":0.167,"y":0.197},"t":25,"s":[0,60.972],"to":[0,0.448],"ti":[0,-0.263]},{"i":{"x":0.833,"y":0.786},"o":{"x":0.167,"y":0.296},"t":26,"s":[0,62.107],"to":[0,0.263],"ti":[0,-0.191]},{"i":{"x":0.833,"y":0.857},"o":{"x":0.167,"y":0.136},"t":27,"s":[0,62.552],"to":[0,0.191],"ti":[0,-0.201]},{"i":{"x":0.833,"y":0.884},"o":{"x":0.167,"y":0.199},"t":28,"s":[0,63.253],"to":[0,0.201],"ti":[0,-0.117]},{"i":{"x":0.833,"y":0.787},"o":{"x":0.167,"y":0.298},"t":29,"s":[0,63.758],"to":[0,0.117],"ti":[0,-0.084]},{"i":{"x":0.833,"y":0.857},"o":{"x":0.167,"y":0.137},"t":30,"s":[0,63.954],"to":[0,0.084],"ti":[0,-0.087]},{"i":{"x":0.833,"y":0.885},"o":{"x":0.167,"y":0.2},"t":31,"s":[0,64.26],"to":[0,0.087],"ti":[0,-0.05]},{"i":{"x":0.833,"y":0.787},"o":{"x":0.167,"y":0.3},"t":32,"s":[0,64.478],"to":[0,0.05],"ti":[0,-0.036]},{"i":{"x":0.833,"y":0.858},"o":{"x":0.167,"y":0.137},"t":33,"s":[0,64.562],"to":[0,0.036],"ti":[0,-0.037]},{"i":{"x":0.833,"y":0.885},"o":{"x":0.167,"y":0.201},"t":34,"s":[0,64.692],"to":[0,0.037],"ti":[0,-0.021]},{"i":{"x":0.833,"y":0.788},"o":{"x":0.167,"y":0.301},"t":35,"s":[0,64.784],"to":[0,0.021],"ti":[0,-0.015]},{"i":{"x":0.833,"y":0.858},"o":{"x":0.167,"y":0.137},"t":36,"s":[0,64.819],"to":[0,0.015],"ti":[0,-0.015]},{"i":{"x":0.833,"y":0.885},"o":{"x":0.167,"y":0.202},"t":37,"s":[0,64.873],"to":[0,0.015],"ti":[0,-0.009]},{"i":{"x":0.833,"y":0.789},"o":{"x":0.167,"y":0.302},"t":38,"s":[0,64.912],"to":[0,0.009],"ti":[0,-0.006]},{"i":{"x":0.833,"y":0.858},"o":{"x":0.167,"y":0.138},"t":39,"s":[0,64.926],"to":[0,0.006],"ti":[0,-0.006]},{"i":{"x":0.833,"y":0.885},"o":{"x":0.167,"y":0.203},"t":40,"s":[0,64.949],"to":[0,0.006],"ti":[0,-0.004]},{"i":{"x":0.833,"y":0.789},"o":{"x":0.167,"y":0.302},"t":41,"s":[0,64.964],"to":[0,0.004],"ti":[0,-0.003]},{"i":{"x":0.833,"y":0.859},"o":{"x":0.167,"y":0.138},"t":42,"s":[0,64.97],"to":[0,0.003],"ti":[0,-0.003]},{"i":{"x":0.833,"y":0.885},"o":{"x":0.167,"y":0.203},"t":43,"s":[0,64.979],"to":[0,0.003],"ti":[0,-0.001]},{"i":{"x":0.833,"y":0.789},"o":{"x":0.167,"y":0.303},"t":44,"s":[0,64.986],"to":[0,0.001],"ti":[0,-0.001]},{"i":{"x":0.833,"y":0.859},"o":{"x":0.167,"y":0.138},"t":45,"s":[0,64.988],"to":[0,0.001],"ti":[0,-0.001]},{"i":{"x":0.833,"y":0.885},"o":{"x":0.167,"y":0.203},"t":46,"s":[0,64.992],"to":[0,0.001],"ti":[0,-0.001]},{"i":{"x":0.833,"y":0.79},"o":{"x":0.167,"y":0.304},"t":47,"s":[0,64.994],"to":[0,0.001],"ti":[0,0]},{"i":{"x":0.833,"y":0.859},"o":{"x":0.167,"y":0.138},"t":48,"s":[0,64.995],"to":[0,0],"ti":[0,0]},{"i":{"x":0.833,"y":0.885},"o":{"x":0.167,"y":0.204},"t":49,"s":[0,64.997],"to":[0,0],"ti":[0,0]},{"i":{"x":0.833,"y":0.79},"o":{"x":0.167,"y":0.304},"t":50,"s":[0,64.998],"to":[0,0],"ti":[0,0]},{"i":{"x":0.833,"y":0.859},"o":{"x":0.167,"y":0.138},"t":51,"s":[0,64.998],"to":[0,0],"ti":[0,0]},{"i":{"x":0.833,"y":0.885},"o":{"x":0.167,"y":0.204},"t":52,"s":[0,64.999],"to":[0,0],"ti":[0,0]},{"i":{"x":0.833,"y":0.79},"o":{"x":0.167,"y":0.304},"t":53,"s":[0,64.999],"to":[0,0],"ti":[0,0]},{"i":{"x":0.833,"y":0.859},"o":{"x":0.167,"y":0.138},"t":54,"s":[0,64.999],"to":[0,0],"ti":[0,0]},{"i":{"x":0.833,"y":0.885},"o":{"x":0.167,"y":0.204},"t":55,"s":[0,64.999],"to":[0,0],"ti":[0,0]},{"i":{"x":0.833,"y":0.79},"o":{"x":0.167,"y":0.305},"t":56,"s":[0,65],"to":[0,0],"ti":[0,0]},{"i":{"x":0.833,"y":0.859},"o":{"x":0.167,"y":0.138},"t":57,"s":[0,65],"to":[0,0],"ti":[0,0]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.204},"t":58,"s":[0,65],"to":[0,0],"ti":[0,0]},{"t":59,"s":[0,65]}],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Vector 6","np":5,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":60,"st":0,"ct":1,"bm":0}],"markers":[]} \ No newline at end of file diff --git a/submodules/TelegramUI/Sources/ChatTextInputPanelNode.swift b/submodules/TelegramUI/Sources/ChatTextInputPanelNode.swift index ef76f3db6f..e8e3acba4b 100644 --- a/submodules/TelegramUI/Sources/ChatTextInputPanelNode.swift +++ b/submodules/TelegramUI/Sources/ChatTextInputPanelNode.swift @@ -3853,7 +3853,7 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate { return false } let aspectRatio = min(image.size.width, image.size.height) / maxSide - if isMemoji || (imageHasTransparency(cgImage) && aspectRatio > 0.85) { + if isMemoji || (imageHasTransparency(cgImage) && aspectRatio > 0.2) { self.paste(.sticker(image, isMemoji)) return true } diff --git a/submodules/TelegramUI/Sources/TelegramRootController.swift b/submodules/TelegramUI/Sources/TelegramRootController.swift index d98b628fad..d3142a7524 100644 --- a/submodules/TelegramUI/Sources/TelegramRootController.swift +++ b/submodules/TelegramUI/Sources/TelegramRootController.swift @@ -18,6 +18,7 @@ import TabBarUI import WallpaperBackgroundNode import ChatPresentationInterfaceState import CameraScreen +import MediaEditorScreen import LegacyComponents import LegacyMediaPickerUI import LegacyCamera @@ -255,51 +256,61 @@ public final class TelegramRootController: NavigationController { } controller.view.endEditing(true) + let context = self.context + var presentImpl: ((ViewController) -> Void)? + var returnToCameraImpl: (() -> Void)? var dismissCameraImpl: (() -> Void)? - let cameraController = CameraScreen(context: self.context, mode: .story, completion: { [weak self] result in - if let self { - let item: TGMediaEditableItem & TGMediaSelectableItem + let _ = presentImpl + let _ = returnToCameraImpl + let _ = dismissCameraImpl + let cameraController = CameraScreen(context: context, mode: .story, completion: { result in + let subject: Signal = result + |> map { value -> MediaEditorScreen.Subject? in + switch value { + case .pendingImage: + return nil + case let .image(image): + return .image(image, PixelDimensions(image.size)) + case let .video(path, dimensions): + return .video(path, dimensions) + case let .asset(asset): + return .asset(asset) + } + } + let controller = MediaEditorScreen(context: context, subject: subject, completion: { result in switch result { case let .image(image): - item = TGCameraCapturedPhoto(existing: image) - case let .video(path): - item = TGCameraCapturedVideo(url: URL(fileURLWithPath: path)) - case let .asset(asset): - item = TGMediaAsset(phAsset: asset) - } - let context = self.context - legacyStoryMediaEditor(context: self.context, item: item, getCaptionPanelView: { return nil }, completion: { result in - dismissCameraImpl?() - switch result { - case let .image(image): - _ = image - case let .video(path): - _ = path - case let .asset(asset): - let options = PHImageRequestOptions() - options.deliveryMode = .highQualityFormat - options.isNetworkAccessAllowed = true - PHImageManager.default().requestImageData(for: asset, options:options, resultHandler: { data, _, _, _ in - if let data, let image = UIImage(data: data) { - Queue.mainQueue().async { - let _ = context.engine.messages.uploadStory(media: .image(dimensions: PixelDimensions(image.size), data: data)).start() - } - } - }) + if let data = image.jpegData(compressionQuality: 0.8) { + let _ = context.engine.messages.uploadStory(media: .image(dimensions: PixelDimensions(image.size), data: data)).start() } - }, present: { c, a in - presentImpl?(c) - }) + case .video: + break + } + dismissCameraImpl?() + }) + controller.sourceHint = .camera + controller.cancelled = { + returnToCameraImpl?() } + presentImpl?(controller) }) controller.push(cameraController) presentImpl = { [weak cameraController] c in - cameraController?.present(c, in: .window(.root)) + if let navigationController = cameraController?.navigationController as? NavigationController { + var controllers = navigationController.viewControllers + controllers.append(c) + navigationController.setViewControllers(controllers, animated: false) + } } dismissCameraImpl = { [weak cameraController] in cameraController?.dismiss(animated: false) } + returnToCameraImpl = { [weak cameraController] in + if let cameraController { + cameraController.returnFromEditor() + } + } } public func openSettings() {