From a51c65e26877f525b7a44a6cbd79cea75cde3ee0 Mon Sep 17 00:00:00 2001 From: Ilya Laktyushin Date: Sun, 7 Apr 2024 18:39:05 +0400 Subject: [PATCH] [WIP] Stickers editor --- .../Telegram-iOS/en.lproj/Localizable.strings | 3 + ...nchronizeRecentlyUsedMediaOperations.swift | 7 ++ .../Stickers/TelegramEngineStickers.swift | 10 +- .../Sources/ChatEntityKeyboardInputNode.swift | 21 +++- .../Sources/StickerOutlineRenderPass.swift | 3 +- .../Sources/MediaCutoutScreen.swift | 6 +- .../Sources/MediaEditorScreen.swift | 4 +- .../Sources/StickerCutoutOutlineView.swift | 116 ++++++++++++++++-- .../TelegramUI/Sources/ChatController.swift | 2 +- 9 files changed, 151 insertions(+), 21 deletions(-) diff --git a/Telegram/Telegram-iOS/en.lproj/Localizable.strings b/Telegram/Telegram-iOS/en.lproj/Localizable.strings index d649e756bf..d7f77c8498 100644 --- a/Telegram/Telegram-iOS/en.lproj/Localizable.strings +++ b/Telegram/Telegram-iOS/en.lproj/Localizable.strings @@ -11898,3 +11898,6 @@ Sorry for the inconvenience."; "ChatList.PremiumGraceTitle" = "⚠️ Don't lose access to Telegram Premium!"; "ChatList.PremiumGraceText" = "Your exclusive benefits are about to expire."; + +"Stickers.RemoveFromRecent" = "Remove from Recents"; +"Conversation.StickerRemovedFromRecent" = "Sticker was removed from Recents."; diff --git a/submodules/TelegramCore/Sources/State/SynchronizeRecentlyUsedMediaOperations.swift b/submodules/TelegramCore/Sources/State/SynchronizeRecentlyUsedMediaOperations.swift index fd0ef9b6aa..fe1b5734c2 100644 --- a/submodules/TelegramCore/Sources/State/SynchronizeRecentlyUsedMediaOperations.swift +++ b/submodules/TelegramCore/Sources/State/SynchronizeRecentlyUsedMediaOperations.swift @@ -46,6 +46,13 @@ func addRecentlyUsedSticker(transaction: Transaction, fileReference: FileMediaRe } } +func _internal_removeRecentlyUsedSticker(transaction: Transaction, fileReference: FileMediaReference) { + if let resource = fileReference.media.resource as? CloudDocumentMediaResource { + transaction.removeOrderedItemListItem(collectionId: Namespaces.OrderedItemList.CloudRecentStickers, itemId: RecentMediaItemId(fileReference.media.fileId).rawValue) + addSynchronizeRecentlyUsedMediaOperation(transaction: transaction, category: .stickers, operation: .remove(id: resource.fileId, accessHash: resource.accessHash)) + } +} + func _internal_clearRecentlyUsedStickers(transaction: Transaction) { transaction.replaceOrderedItemListItems(collectionId: Namespaces.OrderedItemList.CloudRecentStickers, items: []) addSynchronizeRecentlyUsedMediaOperation(transaction: transaction, category: .stickers, operation: .clear) diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Stickers/TelegramEngineStickers.swift b/submodules/TelegramCore/Sources/TelegramEngine/Stickers/TelegramEngineStickers.swift index 4300d76c83..2f2005e228 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Stickers/TelegramEngineStickers.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Stickers/TelegramEngineStickers.swift @@ -286,9 +286,15 @@ public extension TelegramEngine { } } - public func addRecentlyUsedSticker(file: TelegramMediaFile) { + public func addRecentlyUsedSticker(fileReference: FileMediaReference) { let _ = self.account.postbox.transaction({ transaction -> Void in - TelegramCore.addRecentlyUsedSticker(transaction: transaction, fileReference: .standalone(media: file)) + TelegramCore.addRecentlyUsedSticker(transaction: transaction, fileReference: fileReference) + }).start() + } + + public func removeRecentlyUsedSticker(fileReference: FileMediaReference) { + let _ = self.account.postbox.transaction({ transaction -> Void in + _internal_removeRecentlyUsedSticker(transaction: transaction, fileReference: fileReference) }).start() } } diff --git a/submodules/TelegramUI/Components/ChatEntityKeyboardInputNode/Sources/ChatEntityKeyboardInputNode.swift b/submodules/TelegramUI/Components/ChatEntityKeyboardInputNode/Sources/ChatEntityKeyboardInputNode.swift index 5f187d2a3f..477b16b599 100644 --- a/submodules/TelegramUI/Components/ChatEntityKeyboardInputNode/Sources/ChatEntityKeyboardInputNode.swift +++ b/submodules/TelegramUI/Components/ChatEntityKeyboardInputNode/Sources/ChatEntityKeyboardInputNode.swift @@ -1346,7 +1346,7 @@ public final class ChatEntityKeyboardInputNode: ChatInputNode { stickerPacks: [packReference], loadedStickerPacks: [], isEditing: true, - expandIfNeeded: false, + expandIfNeeded: true, parentNavigationController: interaction.getNavigationController(), sendSticker: { [weak interaction] fileReference, sourceView, sourceRect in return interaction?.sendSticker(fileReference, false, false, nil, false, sourceView, sourceRect, nil, []) ?? false @@ -2845,6 +2845,25 @@ public final class EmojiContentPeekBehaviorImpl: EmojiContentPeekBehavior { break } } + + if groupId == AnyHashable("recent") { + menuItems.append( + .action(ContextMenuActionItem(text: presentationData.strings.Stickers_RemoveFromRecent, textColor: .destructive, icon: { theme in + return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Delete"), color: theme.actionSheet.destructiveActionTextColor) + }, action: { _, f in + f(.default) + + guard let strongSelf = self else { + return + } + + let presentationData = strongSelf.context.sharedContext.currentPresentationData.with { $0 } + interaction.presentGlobalOverlayController(UndoOverlayController(presentationData: presentationData, content: .sticker(context: context, file: file, loop: true, title: nil, text: presentationData.strings.Conversation_StickerRemovedFromRecent, undoText: nil, customAction: nil), elevatedLayout: false, action: { _ in return false }), nil) + + strongSelf.context.engine.stickers.removeRecentlyUsedSticker(fileReference: .recentSticker(media: file)) + })) + ) + } } guard let view = view else { diff --git a/submodules/TelegramUI/Components/MediaEditor/Sources/StickerOutlineRenderPass.swift b/submodules/TelegramUI/Components/MediaEditor/Sources/StickerOutlineRenderPass.swift index 68dbff32ac..610e25f56e 100644 --- a/submodules/TelegramUI/Components/MediaEditor/Sources/StickerOutlineRenderPass.swift +++ b/submodules/TelegramUI/Components/MediaEditor/Sources/StickerOutlineRenderPass.swift @@ -95,7 +95,8 @@ final class StickerOutlineRenderPass: RenderPass { return input } - let resultImage = outline.composited(over: image) + var resultImage = outline.composited(over: image) + resultImage = outline.composited(over: resultImage) if self.outputTexture == nil { let textureDescriptor = MTLTextureDescriptor() diff --git a/submodules/TelegramUI/Components/MediaEditorScreen/Sources/MediaCutoutScreen.swift b/submodules/TelegramUI/Components/MediaEditorScreen/Sources/MediaCutoutScreen.swift index 9f79ec454c..f30ac1668d 100644 --- a/submodules/TelegramUI/Components/MediaEditorScreen/Sources/MediaCutoutScreen.swift +++ b/submodules/TelegramUI/Components/MediaEditorScreen/Sources/MediaCutoutScreen.swift @@ -141,11 +141,7 @@ private final class MediaCutoutScreenComponent: Component { self.initialOutlineValue = mediaEditor.getToolValue(.stickerOutline) as? Float mediaEditor.setToolValue(.stickerOutline, value: nil) mediaEditor.isSegmentationMaskEnabled = false - mediaEditor.setOnNextDisplay { [weak controller] in - if let controller { - controller.previewView.mask = controller.maskWrapperView - } - } + controller.previewView.mask = controller.maskWrapperView self.buttonsBackgroundView.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2) self.label.view?.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2) diff --git a/submodules/TelegramUI/Components/MediaEditorScreen/Sources/MediaEditorScreen.swift b/submodules/TelegramUI/Components/MediaEditorScreen/Sources/MediaEditorScreen.swift index 8de0b1a540..9c99b90398 100644 --- a/submodules/TelegramUI/Components/MediaEditorScreen/Sources/MediaEditorScreen.swift +++ b/submodules/TelegramUI/Components/MediaEditorScreen/Sources/MediaEditorScreen.swift @@ -2866,8 +2866,8 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate self.stickerMaskWrapperView.frame = CGRect(origin: .zero, size: previewSize) self.stickerMaskPreviewView.frame = CGRect(origin: .zero, size: previewSize) - let filledSize = maskDrawingSize.aspectFitted(previewSize) - let maskScale = filledSize.width / maskDrawingSize.width +// let filledSize = maskDrawingSize.aspectFitted(previewSize) + let maskScale = previewSize.width / min(maskDrawingSize.width, maskDrawingSize.height) initialMaskScale = maskScale initialMaskPosition = CGPoint(x: previewSize.width / 2.0, y: previewSize.height / 2.0) stickerMaskDrawingView.bounds = CGRect(origin: .zero, size: maskDrawingSize) diff --git a/submodules/TelegramUI/Components/MediaEditorScreen/Sources/StickerCutoutOutlineView.swift b/submodules/TelegramUI/Components/MediaEditorScreen/Sources/StickerCutoutOutlineView.swift index 413b61c9d3..02396e6e58 100644 --- a/submodules/TelegramUI/Components/MediaEditorScreen/Sources/StickerCutoutOutlineView.swift +++ b/submodules/TelegramUI/Components/MediaEditorScreen/Sources/StickerCutoutOutlineView.swift @@ -18,6 +18,7 @@ final class StickerCutoutOutlineView: UIView { let strokeLayer = SimpleShapeLayer() let imageLayer = SimpleLayer() var outlineLayer = CAEmitterLayer() + var outline2Layer = CAEmitterLayer() var glowLayer = CAEmitterLayer() override init(frame: CGRect) { @@ -56,23 +57,32 @@ final class StickerCutoutOutlineView: UIView { } private func setupAnimation(path: BezierPath) { + self.outlineLayer.removeFromSuperlayer() + self.outline2Layer.removeFromSuperlayer() + self.glowLayer.removeFromSuperlayer() + self.outlineLayer = CAEmitterLayer() - self.outlineLayer.opacity = 0.7 + self.outlineLayer.opacity = 0.77 + self.outline2Layer = CAEmitterLayer() + self.outline2Layer.opacity = 0.7 + self.glowLayer = CAEmitterLayer() self.layer.addSublayer(self.outlineLayer) +// self.layer.addSublayer(self.outline2Layer) self.layer.addSublayer(self.glowLayer) let randomBeginTime = (previousBeginTime + 4) % 6 previousBeginTime = randomBeginTime - let duration = min(5.0, max(2.0, path.length / 2200.0)) + let duration = min(6.0, max(2.5, path.length / 2200.0)) let outlineAnimation = CAKeyframeAnimation(keyPath: "emitterPosition") outlineAnimation.path = path.path.cgPath outlineAnimation.duration = duration outlineAnimation.repeatCount = .infinity outlineAnimation.calculationMode = .paced + outlineAnimation.fillMode = .forwards outlineAnimation.beginTime = Double(randomBeginTime) self.outlineLayer.add(outlineAnimation, forKey: "emitterPosition") @@ -85,14 +95,49 @@ final class StickerCutoutOutlineView: UIView { lineEmitterCell.color = UIColor.white.cgColor lineEmitterCell.contents = UIImage(named: "Media Editor/ParticleDot")?.cgImage lineEmitterCell.lifetime = 2.2 - lineEmitterCell.birthRate = 120 - lineEmitterCell.scale = 0.14 + lineEmitterCell.birthRate = 700 + lineEmitterCell.scale = 0.17 lineEmitterCell.alphaSpeed = -0.4 self.outlineLayer.emitterCells = [lineEmitterCell] self.outlineLayer.emitterMode = .points - self.outlineLayer.emitterSize = CGSize(width: 1.0, height: 1.0) - self.outlineLayer.emitterShape = .point + self.outlineLayer.emitterSize = CGSize(width: 1.33, height: 1.33) + self.outlineLayer.emitterShape = .rectangle + + + + + + let outline2Animation = CAKeyframeAnimation(keyPath: "emitterPosition") + outline2Animation.path = path.path.cgPath + outline2Animation.duration = duration + outline2Animation.repeatCount = .infinity + outline2Animation.calculationMode = .paced + outline2Animation.fillMode = .forwards + outline2Animation.beginTime = Double(randomBeginTime) + 0.02 + self.outline2Layer.add(outline2Animation, forKey: "emitterPosition") + + let line2EmitterCell = CAEmitterCell() + line2EmitterCell.beginTime = CACurrentMediaTime() + let line2AlphaBehavior = createEmitterBehavior(type: "valueOverLife") + line2AlphaBehavior.setValue("color.alpha", forKey: "keyPath") + line2AlphaBehavior.setValue([0.0, 0.5, 0.8, 0.5, 0.0], forKey: "values") + line2EmitterCell.setValue([line2AlphaBehavior], forKey: "emitterBehaviors") + line2EmitterCell.color = UIColor.white.cgColor + line2EmitterCell.contents = UIImage(named: "Media Editor/ParticleDot")?.cgImage + line2EmitterCell.lifetime = 2.2 + line2EmitterCell.birthRate = 500 + line2EmitterCell.scale = 0.14 + line2EmitterCell.alphaSpeed = -0.4 + + self.outline2Layer.emitterCells = [line2EmitterCell] + self.outline2Layer.emitterMode = .points + self.outline2Layer.emitterSize = CGSize(width: 1.5, height: 1.5) + self.outline2Layer.emitterShape = .circle + + + + let glowAnimation = CAKeyframeAnimation(keyPath: "emitterPosition") glowAnimation.path = path.path.cgPath @@ -123,6 +168,7 @@ final class StickerCutoutOutlineView: UIView { self.strokeLayer.animateAlpha(from: 0.0, to: CGFloat(self.strokeLayer.opacity), duration: 0.4) self.outlineLayer.animateAlpha(from: 0.0, to: CGFloat(self.outlineLayer.opacity), duration: 0.4, delay: 0.0) + self.outline2Layer.animateAlpha(from: 0.0, to: CGFloat(self.outline2Layer.opacity), duration: 0.4, delay: 0.0) self.glowLayer.animateAlpha(from: 0.0, to: CGFloat(self.glowLayer.opacity), duration: 0.4, delay: 0.0) let values = [1.0, 1.07, 1.0] @@ -133,6 +179,7 @@ final class StickerCutoutOutlineView: UIView { override func layoutSubviews() { self.strokeLayer.frame = self.bounds.offsetBy(dx: 0.0, dy: 1.0) self.outlineLayer.frame = self.bounds + self.outline2Layer.frame = self.bounds self.imageLayer.frame = self.bounds self.glowLayer.frame = self.bounds } @@ -152,8 +199,8 @@ private func getPathFromMaskImage(_ image: CIImage, size: CGSize, values: MediaE return nil } - contour = simplify(contour, tolerance: 1.4) - let path = BezierPath(points: contour, smooth: false) + contour = simplify(contour, tolerance: 1.0) + let path = BezierPath(points: contour, smooth: true) let firstScale = min(size.width, size.height) / 256.0 let secondScale = size.width / 1080.0 @@ -494,8 +541,59 @@ private class BezierPath { init(points: [CGPoint], smooth: Bool) { self.path = UIBezierPath() - + if smooth { + if points.count < 3 { + self.path.move(to: points.first ?? CGPoint.zero) + self.path.addLine(to: points[1]) + self.length = points[1].distanceFrom(points[0]) + return + } else { + self.path.move(to: points.first!) + + let n = points.count - 1 + let tension = 0.5 + + for i in 0 ..< n { + let currentPoint = points[i] + var nextIndex = (i + 1) % points.count + var prevIndex = i == 0 ? points.count - 1 : i - 1 + var nextNextIndex = (nextIndex + 1) % points.count + let prevPoint = points[prevIndex] + let nextPoint = points[nextIndex] + let nextNextPoint = points[nextNextIndex] + + let d1 = sqrt(pow(currentPoint.x - prevPoint.x, 2) + pow(currentPoint.y - prevPoint.y, 2)) + let d2 = sqrt(pow(nextPoint.x - currentPoint.x, 2) + pow(nextPoint.y - currentPoint.y, 2)) + let d3 = sqrt(pow(nextNextPoint.x - nextPoint.x, 2) + pow(nextNextPoint.y - nextPoint.y, 2)) + + var controlPoint1: CGPoint + if d1 < 0.0001 { + controlPoint1 = currentPoint + } else { + controlPoint1 = CGPoint(x: currentPoint.x + (tension * d2 / (d2 + d3)) * (nextPoint.x - prevPoint.x), + y: currentPoint.y + (tension * d2 / (d2 + d3)) * (nextPoint.y - prevPoint.y)) + } + + prevIndex = i + nextIndex = (i + 1) % points.count + nextNextIndex = (nextIndex + 1) % points.count + + let controlPoint2: CGPoint + if d3 < 0.0001 { + controlPoint2 = nextPoint + } else { + controlPoint2 = CGPoint(x: nextPoint.x - (tension * d2 / (d1 + d2)) * (nextNextPoint.x - currentPoint.x), + y: nextPoint.y - (tension * d2 / (d1 + d2)) * (nextNextPoint.y - currentPoint.y)) + } + + self.path.addCurve(to: nextPoint, controlPoint1: controlPoint1, controlPoint2: controlPoint2) + self.length += nextPoint.distanceFrom(currentPoint) + } + + self.path.close() + } + } else if smooth { let K: CGFloat = 0.2 var c1 = [Int: CGPoint]() var c2 = [Int: CGPoint]() diff --git a/submodules/TelegramUI/Sources/ChatController.swift b/submodules/TelegramUI/Sources/ChatController.swift index bec990963e..b2304b60c9 100644 --- a/submodules/TelegramUI/Sources/ChatController.swift +++ b/submodules/TelegramUI/Sources/ChatController.swift @@ -14362,7 +14362,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G Queue.mainQueue().after(3.0) { if let message = self.chatDisplayNode.historyNode.lastVisbleMesssage(), let file = message.media.first(where: { $0 is TelegramMediaFile }) as? TelegramMediaFile, file.isSticker { - self.context.engine.stickers.addRecentlyUsedSticker(file: file) + self.context.engine.stickers.addRecentlyUsedSticker(fileReference: .message(message: MessageReference(message), media: file)) } } }