[WIP] Stickers editor

This commit is contained in:
Ilya Laktyushin
2024-04-08 03:35:16 +04:00
parent 1e155f3243
commit f79bbc3eba
7 changed files with 98 additions and 40 deletions

View File

@@ -330,11 +330,7 @@ private func opportunisticallyTransformOutgoingMedia(network: Network, postbox:
if let mediaReference = mediaReference {
signals.append(opportunisticallyTransformMessageWithMedia(network: network, postbox: postbox, transformOutgoingMessageMedia: transformOutgoingMessageMedia, mediaReference: mediaReference, userInteractive: userInteractive)
|> map { result -> (Bool, EnqueueMessage) in
if let result = result {
return (true, .message(text: text, attributes: attributes, inlineStickers: inlineStickers, mediaReference: .standalone(media: result.media), threadId: threadId, replyToMessageId: replyToMessageId, replyToStoryId: replyToStoryId, localGroupingKey: localGroupingKey, correlationId: correlationId, bubbleUpEmojiOrStickersets: bubbleUpEmojiOrStickersets))
} else {
return (false, .message(text: text, attributes: attributes, inlineStickers: inlineStickers, mediaReference: mediaReference, threadId: threadId, replyToMessageId: replyToMessageId, replyToStoryId: replyToStoryId, localGroupingKey: localGroupingKey, correlationId: correlationId, bubbleUpEmojiOrStickersets: bubbleUpEmojiOrStickersets))
}
return (result != nil, .message(text: text, attributes: attributes, inlineStickers: inlineStickers, mediaReference: result ?? mediaReference, threadId: threadId, replyToMessageId: replyToMessageId, replyToStoryId: replyToStoryId, localGroupingKey: localGroupingKey, correlationId: correlationId, bubbleUpEmojiOrStickersets: bubbleUpEmojiOrStickersets))
})
} else {
signals.append(.single((false, message)))

View File

@@ -444,6 +444,33 @@ public enum AnyMediaReference: Equatable {
}
}
public func withUpdatedMedia(_ media: Media) -> AnyMediaReference {
switch self {
case .standalone:
return .standalone(media: media)
case let .message(message, _):
return .message(message: message, media: media)
case let .webPage(webPage, _):
return .webPage(webPage: webPage, media: media)
case let .stickerPack(stickerPack, _):
return .stickerPack(stickerPack: stickerPack, media: media)
case .savedGif:
return .savedGif(media: media)
case .savedSticker:
return .savedSticker(media: media)
case .recentSticker:
return .recentSticker(media: media)
case let .avatarList(peer, _):
return .avatarList(peer: peer, media: media)
case let .attachBot(peer, _):
return .attachBot(peer: peer, media: media)
case .customEmoji:
return .customEmoji(media: media)
case let .story(peer, id, _):
return .story(peer: peer, id: id, media: media)
}
}
public func resourceReference(_ resource: MediaResource) -> MediaResourceReference {
return .media(media: self, resource: resource)
}

View File

@@ -194,7 +194,6 @@ public final class MediaEditor {
public private(set) var canCutout: Bool = false
public var canCutoutUpdated: (Bool, Bool) -> Void = { _, _ in }
public var isCutoutUpdated: (Bool) -> Void = { _ in }
public var maskUpdated: (UIImage) -> Void = { _ in }
public var classificationUpdated: ([(String, Float)]) -> Void = { _ in }
@@ -1721,9 +1720,7 @@ public final class MediaEditor {
}
private var mainInputMask: MTLTexture?
public func removeSegmentationMask() {
self.isCutoutUpdated(false)
public func removeSegmentationMask() {
self.mainInputMask = nil
self.renderer.currentMainInputMask = nil
if !self.skipRendering {
@@ -1731,15 +1728,11 @@ public final class MediaEditor {
}
}
public func setSegmentationMask(_ image: UIImage, andEnable enable: Bool = false, updateCutout: Bool = false) {
public func setSegmentationMask(_ image: UIImage, andEnable enable: Bool = false) {
guard let renderTarget = self.previewView, let device = renderTarget.mtlDevice else {
return
}
if updateCutout {
self.isCutoutUpdated(true)
}
//TODO:replace with pixelbuffer?
self.mainInputMask = loadTexture(image: image, device: device)
if enable {

View File

@@ -102,14 +102,15 @@ private final class MediaCutoutScreenComponent: Component {
}
@objc private func previewTap(_ gestureRecognizer: UITapGestureRecognizer) {
guard let component = self.component else {
guard let component = self.component, let controller = self.environment?.controller() as? MediaCutoutScreen else {
return
}
let location = gestureRecognizer.location(in: gestureRecognizer.view)
let location = gestureRecognizer.location(in: controller.drawingView)
let point = CGPoint(
x: location.x / self.previewContainerView.frame.width,
y: location.y / self.previewContainerView.frame.height
x: location.x / controller.drawingView.bounds.width,
y: location.y / controller.drawingView.bounds.height
)
component.mediaEditor.processImage { [weak self] originalImage, _ in
@@ -118,7 +119,7 @@ private final class MediaCutoutScreenComponent: Component {
if let self, let _ = self.component, let result = results.first, let maskImage = result.maskImage, let controller = self.environment?.controller() as? MediaCutoutScreen {
if case let .image(mask, _) = maskImage {
self.playDissolveAnimation()
component.mediaEditor.setSegmentationMask(mask, updateCutout: true)
component.mediaEditor.setSegmentationMask(mask)
if let maskData = mask.pngData() {
controller.drawingView.setup(withDrawing: maskData)
}
@@ -189,7 +190,7 @@ private final class MediaCutoutScreenComponent: Component {
let mediaEditor = controller.mediaEditor
if let drawingImage = controller.drawingView.drawingImage {
mediaEditor.setSegmentationMask(drawingImage, andEnable: true, updateCutout: false)
mediaEditor.setSegmentationMask(drawingImage, andEnable: true)
}
let initialOutlineValue = self.initialOutlineValue
mediaEditor.setOnNextDisplay { [weak controller, weak mediaEditor] in
@@ -197,6 +198,7 @@ private final class MediaCutoutScreenComponent: Component {
if let initialOutlineValue {
mediaEditor?.setToolValue(.stickerOutline, value: initialOutlineValue)
}
controller?.completed()
}
self.animatingOut = true
@@ -249,6 +251,7 @@ private final class MediaCutoutScreenComponent: Component {
dustEffectLayer.addItem(frame: previewView.bounds, image: resultImage)
controller.completedWithCutout()
controller.requestDismiss(animated: true)
}
@@ -386,7 +389,19 @@ private final class MediaCutoutScreenComponent: Component {
if labelView.superview == nil {
self.buttonsContainerView.addSubview(labelView)
}
transition.setFrame(view: labelView, frame: labelFrame)
if labelView.bounds.width > 0.0 && labelFrame.width != labelView.bounds.width {
if let snapshotView = labelView.snapshotView(afterScreenUpdates: false) {
snapshotView.center = labelView.center
self.buttonsContainerView.addSubview(snapshotView)
labelView.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.25)
snapshotView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.25, removeOnCompletion: false, completion: { _ in
snapshotView.removeFromSuperview()
})
}
}
labelView.bounds = CGRect(origin: .zero, size: labelFrame.size)
transition.setPosition(view: labelView, position: labelFrame.center)
}
transition.setFrame(view: self.buttonsContainerView, frame: buttonsContainerFrame)
@@ -403,7 +418,6 @@ private final class MediaCutoutScreenComponent: Component {
self.fadeView.frame = CGRect(x: floorToScreenPixels((previewContainerFrame.width - frameWidth) / 2.0), y: previewContainerFrame.minY + floorToScreenPixels((previewContainerFrame.height - frameWidth) / 2.0), width: frameWidth, height: frameWidth)
self.fadeView.layer.cornerRadius = frameWidth / 8.0
if isFirstTime {
let values = component.mediaEditor.values
component.mediaEditor.processImage { originalImage, editedImage in
@@ -598,6 +612,8 @@ final class MediaCutoutScreen: ViewController {
fileprivate let overlayView: UIView
fileprivate let backgroundView: UIView
var completed: () -> Void = {}
var completedWithCutout: () -> Void = {}
var dismissed: () -> Void = {}
private var initialValues: MediaEditorValues

View File

@@ -1132,10 +1132,14 @@ final class MediaEditorScreenComponent: Component {
} else {
if let cutoutButtonView = self.cutoutButton.view, cutoutButtonView.superview != nil {
cutoutButtonView.alpha = 0.0
cutoutButtonView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, delay: 0.0, completion: { _ in
if transition.animation.isImmediate {
cutoutButtonView.removeFromSuperview()
})
cutoutButtonView.layer.animateScale(from: 1.0, to: 0.1, duration: 0.2, delay: 0.0)
} else {
cutoutButtonView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, delay: 0.0, completion: { _ in
cutoutButtonView.removeFromSuperview()
})
cutoutButtonView.layer.animateScale(from: 1.0, to: 0.1, duration: 0.2, delay: 0.0)
}
}
}
@@ -2927,13 +2931,6 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate
self.hasTransparency = hasTransparency
self.requestLayout(forceUpdate: true, transition: .easeInOut(duration: 0.25))
}
mediaEditor.isCutoutUpdated = { [weak self] isCutout in
guard let self else {
return
}
self.isCutout = isCutout
self.requestLayout(forceUpdate: true, transition: .immediate)
}
mediaEditor.maskUpdated = { [weak self] mask in
guard let self else {
return
@@ -4736,6 +4733,17 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate
overlayView: self.stickerMaskPreviewView,
backgroundView: stickerBackgroundView
)
cutoutController.completedWithCutout = { [weak self] in
if let self {
self.isCutout = true
self.requestLayout(forceUpdate: true, transition: .immediate)
}
}
cutoutController.completed = { [weak self] in
if let self {
self.requestLayout(forceUpdate: true, transition: .easeInOut(duration: 0.25))
}
}
cutoutController.dismissed = { [weak self] in
if let self {
self.animateInFromTool(inPlace: true)
@@ -4802,6 +4810,9 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate
})
mediaEditor.removeSegmentationMask()
self.stickerMaskDrawingView?.clearWithEmptyColor()
self.isCutout = false
self.requestLayout(forceUpdate: true, transition: .easeInOut(duration: 0.25))
}
if let value = mediaEditor.getToolValue(.stickerOutline) as? Float, value > 0.0 {

View File

@@ -169,6 +169,21 @@ final class StickerCutoutOutlineView: UIView {
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)
self.animateBump(path: path)
}
private func animateBump(path: BezierPath) {
let boundingBox = path.path.cgPath.boundingBox
let pathCenter = CGPoint(x: boundingBox.midX, y: boundingBox.midY)
// let originalPosition = self.imageLayer.position
// let originalAnchorPoint = self.imageLayer.anchorPoint
let layerPathCenter = self.imageLayer.convert(pathCenter, from: self.imageLayer.superlayer)
self.imageLayer.anchorPoint = CGPoint(x: layerPathCenter.x / layer.bounds.width, y: layerPathCenter.y / layer.bounds.height)
self.imageLayer.position = layerPathCenter
let values = [1.0, 1.07, 1.0]
let keyTimes = [0.0, 0.67, 1.0]
self.imageLayer.animateKeyframes(values: values as [NSNumber], keyTimes: keyTimes as [NSNumber], duration: 0.4, keyPath: "transform.scale", timingFunction: CAMediaTimingFunctionName.easeInEaseOut.rawValue)

View File

@@ -68,21 +68,21 @@ public func transformOutgoingMessageMedia(postbox: Postbox, network: Network, me
}
attributes.append(.ImageSize(size: PixelDimensions(imageDimensions)))
let updatedFile = file.withUpdatedSize(data.size).withUpdatedPreviewRepresentations([TelegramMediaImageRepresentation(dimensions: PixelDimensions(scaledImageSize), resource: thumbnailResource, progressiveSizes: [], immediateThumbnailData: nil, hasVideo: false, isPersonal: false)]).withUpdatedAttributes(attributes)
subscriber.putNext(.standalone(media: updatedFile))
subscriber.putNext(media.withUpdatedMedia(updatedFile))
subscriber.putCompletion()
} else {
let updatedFile = file.withUpdatedSize(data.size)
subscriber.putNext(.standalone(media: updatedFile))
subscriber.putNext(media.withUpdatedMedia(updatedFile))
subscriber.putCompletion()
}
} else {
let updatedFile = file.withUpdatedSize(data.size)
subscriber.putNext(.standalone(media: updatedFile))
subscriber.putNext(media.withUpdatedMedia(updatedFile))
subscriber.putCompletion()
}
} else {
let updatedFile = file.withUpdatedSize(data.size)
subscriber.putNext(.standalone(media: updatedFile))
subscriber.putNext(media.withUpdatedMedia(updatedFile))
subscriber.putCompletion()
}
@@ -97,11 +97,11 @@ public func transformOutgoingMessageMedia(postbox: Postbox, network: Network, me
let scaledImageSize = CGSize(width: scaledImage.size.width * scaledImage.scale, height: scaledImage.size.height * scaledImage.scale)
let updatedFile = file.withUpdatedSize(data.size).withUpdatedPreviewRepresentations([TelegramMediaImageRepresentation(dimensions: PixelDimensions(scaledImageSize), resource: thumbnailResource, progressiveSizes: [], immediateThumbnailData: nil, hasVideo: false, isPersonal: false)])
subscriber.putNext(.standalone(media: updatedFile))
subscriber.putNext(media.withUpdatedMedia(updatedFile))
subscriber.putCompletion()
} else {
let updatedFile = file.withUpdatedSize(data.size)
subscriber.putNext(.standalone(media: updatedFile))
subscriber.putNext(media.withUpdatedMedia(updatedFile))
subscriber.putCompletion()
}
@@ -109,7 +109,7 @@ public func transformOutgoingMessageMedia(postbox: Postbox, network: Network, me
} |> runOn(opportunistic ? Queue.mainQueue() : Queue.concurrentDefaultQueue())
} else {
let updatedFile = file.withUpdatedSize(data.size)
return .single(.standalone(media: updatedFile))
return .single(media.withUpdatedMedia(updatedFile))
}
} else if opportunistic {
return .single(nil)
@@ -158,7 +158,7 @@ public func transformOutgoingMessageMedia(postbox: Postbox, network: Network, me
postbox.mediaBox.storeResourceData(thumbnailResource.id, data: smallestData)
representations.append(TelegramMediaImageRepresentation(dimensions: PixelDimensions(smallestSize), resource: thumbnailResource, progressiveSizes: [], immediateThumbnailData: nil, hasVideo: false, isPersonal: false))
let updatedImage = TelegramMediaImage(imageId: image.imageId, representations: representations, immediateThumbnailData: image.immediateThumbnailData, reference: image.reference, partialReference: image.partialReference, flags: [])
return .single(.standalone(media: updatedImage))
return .single(media.withUpdatedMedia(updatedImage))
}
}