From c0990027c8db1c33758d5f0748551e4aaad97ba0 Mon Sep 17 00:00:00 2001 From: Ilya Laktyushin Date: Tue, 9 Apr 2024 14:59:41 +0400 Subject: [PATCH] [WIP] Stickers editor --- .../Sources/ImageObjectSeparation.swift | 42 +++++++-- .../Sources/MediaCutoutScreen.swift | 2 +- .../Sources/MediaEditorDrafts.swift | 2 + .../Sources/MediaEditorScreen.swift | 6 +- .../Sources/StickerCutoutOutlineView.swift | 88 +++++-------------- 5 files changed, 69 insertions(+), 71 deletions(-) diff --git a/submodules/TelegramUI/Components/MediaEditor/Sources/ImageObjectSeparation.swift b/submodules/TelegramUI/Components/MediaEditor/Sources/ImageObjectSeparation.swift index ae5189ab7a..a69c64878f 100644 --- a/submodules/TelegramUI/Components/MediaEditor/Sources/ImageObjectSeparation.swift +++ b/submodules/TelegramUI/Components/MediaEditor/Sources/ImageObjectSeparation.swift @@ -64,6 +64,7 @@ public struct CutoutResult { public let index: Int public let extractedImage: Image? + public let edgesMaskImage: Image? public let maskImage: Image? public let backgroundImage: Image? } @@ -74,6 +75,25 @@ public enum CutoutTarget { case all } + +func refineEdges(_ maskImage: CIImage) -> CIImage? { + let maskImage = maskImage.clampedToExtent() + + let blurFilter = CIFilter(name: "CIGaussianBlur")! + blurFilter.setValue(maskImage, forKey: kCIInputImageKey) + blurFilter.setValue(11.4, forKey: kCIInputRadiusKey) + + let controlsFilter = CIFilter(name: "CIColorControls")! + controlsFilter.setValue(blurFilter.outputImage, forKey: kCIInputImageKey) + controlsFilter.setValue(6.61, forKey: kCIInputContrastKey) + + let sharpenFilter = CIFilter(name: "CISharpenLuminance")! + sharpenFilter.setValue(controlsFilter.outputImage, forKey: kCIInputImageKey) + sharpenFilter.setValue(250.0, forKey: kCIInputSharpnessKey) + + return sharpenFilter.outputImage?.cropped(to: maskImage.extent) +} + public func cutoutImage( from image: UIImage, editedImage: UIImage? = nil, @@ -153,19 +173,31 @@ public func cutoutImage( extractedImage = nil } + let whiteImage = CIImage(color: .white) + let blackImage = CIImage(color: .black) + let maskFilter = CIFilter.blendWithMask() - maskFilter.inputImage = CIImage(color: .white) - maskFilter.backgroundImage = CIImage(color: .black) + maskFilter.inputImage = whiteImage + maskFilter.backgroundImage = blackImage maskFilter.maskImage = CIImage(cvPixelBuffer: mask) + + let refinedMaskFilter = CIFilter.blendWithMask() + refinedMaskFilter.inputImage = whiteImage + refinedMaskFilter.backgroundImage = blackImage + refinedMaskFilter.maskImage = refineEdges(CIImage(cvPixelBuffer: mask)) + + let edgesMaskImage: CutoutResult.Image? let maskImage: CutoutResult.Image? - if let maskOutput = maskFilter.outputImage?.cropped(to: inputImage.extent), let maskCgImage = ciContext.createCGImage(maskOutput, from: inputImage.extent) { - maskImage = .image(UIImage(cgImage: maskCgImage), maskOutput) + if let maskOutput = maskFilter.outputImage?.cropped(to: inputImage.extent), let maskCgImage = ciContext.createCGImage(maskOutput, from: inputImage.extent), let refinedMaskOutput = refinedMaskFilter.outputImage?.cropped(to: inputImage.extent), let refinedMaskCgImage = ciContext.createCGImage(refinedMaskOutput, from: inputImage.extent) { + edgesMaskImage = .image(UIImage(cgImage: maskCgImage), maskOutput) + maskImage = .image(UIImage(cgImage: refinedMaskCgImage), refinedMaskOutput) } else { + edgesMaskImage = nil maskImage = nil } if extractedImage != nil || maskImage != nil { - results.append(CutoutResult(index: instance, extractedImage: extractedImage, maskImage: maskImage, backgroundImage: nil)) + results.append(CutoutResult(index: instance, extractedImage: extractedImage, edgesMaskImage: edgesMaskImage, maskImage: maskImage, backgroundImage: nil)) } } } diff --git a/submodules/TelegramUI/Components/MediaEditorScreen/Sources/MediaCutoutScreen.swift b/submodules/TelegramUI/Components/MediaEditorScreen/Sources/MediaCutoutScreen.swift index cf84864576..8d34248cc1 100644 --- a/submodules/TelegramUI/Components/MediaEditorScreen/Sources/MediaCutoutScreen.swift +++ b/submodules/TelegramUI/Components/MediaEditorScreen/Sources/MediaCutoutScreen.swift @@ -425,7 +425,7 @@ private final class MediaCutoutScreenComponent: Component { Queue.mainQueue().async { if !results.isEmpty { for result in results { - if let extractedImage = result.extractedImage, let maskImage = result.maskImage { + if let extractedImage = result.extractedImage, let maskImage = result.edgesMaskImage { if case let .image(image, _) = extractedImage, case let .image(_, mask) = maskImage { let outlineView = StickerCutoutOutlineView(frame: self.previewContainerView.frame) outlineView.update(image: image, maskImage: mask, size: self.previewContainerView.bounds.size, values: values) diff --git a/submodules/TelegramUI/Components/MediaEditorScreen/Sources/MediaEditorDrafts.swift b/submodules/TelegramUI/Components/MediaEditorScreen/Sources/MediaEditorDrafts.swift index 4435dd2956..b5c74027ad 100644 --- a/submodules/TelegramUI/Components/MediaEditorScreen/Sources/MediaEditorDrafts.swift +++ b/submodules/TelegramUI/Components/MediaEditorScreen/Sources/MediaEditorDrafts.swift @@ -39,6 +39,8 @@ extension MediaEditorScreen { return false } else if case .message = subject, !filteredValues.hasChanges && filteredEntities.isEmpty && caption.string.isEmpty { return false + } else if case .empty = subject, !self.node.hasAnyChanges && !self.node.drawingView.internalState.canUndo { + return false } } return true diff --git a/submodules/TelegramUI/Components/MediaEditorScreen/Sources/MediaEditorScreen.swift b/submodules/TelegramUI/Components/MediaEditorScreen/Sources/MediaEditorScreen.swift index 2152091942..af23bc10cb 100644 --- a/submodules/TelegramUI/Components/MediaEditorScreen/Sources/MediaEditorScreen.swift +++ b/submodules/TelegramUI/Components/MediaEditorScreen/Sources/MediaEditorScreen.swift @@ -2474,7 +2474,7 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate fileprivate let entitiesContainerView: UIView let entitiesView: DrawingEntitiesView fileprivate let selectionContainerView: DrawingSelectionContainerView - fileprivate let drawingView: DrawingView + let drawingView: DrawingView fileprivate let previewView: MediaEditorPreviewView fileprivate var stickerMaskWrapperView: UIView @@ -4063,6 +4063,10 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate }) self?.interaction?.insertEntity(entity, scale: 2.5) + + self?.hasAnyChanges = true + self?.controller?.isSavingAvailable = true + self?.controller?.requestLayout(transition: .immediate) } if let asset = result as? PHAsset { diff --git a/submodules/TelegramUI/Components/MediaEditorScreen/Sources/StickerCutoutOutlineView.swift b/submodules/TelegramUI/Components/MediaEditorScreen/Sources/StickerCutoutOutlineView.swift index 86fa9782ab..a35d388009 100644 --- a/submodules/TelegramUI/Components/MediaEditorScreen/Sources/StickerCutoutOutlineView.swift +++ b/submodules/TelegramUI/Components/MediaEditorScreen/Sources/StickerCutoutOutlineView.swift @@ -18,7 +18,6 @@ final class StickerCutoutOutlineView: UIView { let strokeLayer = SimpleShapeLayer() let imageLayer = SimpleLayer() var outlineLayer = CAEmitterLayer() - var outline2Layer = CAEmitterLayer() var glowLayer = CAEmitterLayer() override init(frame: CGRect) { @@ -56,24 +55,20 @@ 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.65 - self.outline2Layer = CAEmitterLayer() - self.outline2Layer.opacity = 0.65 + self.outlineLayer.opacity = 0.75 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(6.0, max(2.5, path.length / 2200.0)) + let duration = min(6.5, max(3.0, path.length / 100.0)) let outlineAnimation = CAKeyframeAnimation(keyPath: "emitterPosition") outlineAnimation.path = path.path.cgPath @@ -93,49 +88,14 @@ final class StickerCutoutOutlineView: UIView { lineEmitterCell.color = UIColor.white.cgColor lineEmitterCell.contents = UIImage(named: "Media Editor/ParticleDot")?.cgImage lineEmitterCell.lifetime = 2.2 - lineEmitterCell.birthRate = 1000 - lineEmitterCell.scale = 0.15 + lineEmitterCell.birthRate = 1700 + lineEmitterCell.scale = 0.18 lineEmitterCell.alphaSpeed = -0.4 self.outlineLayer.emitterCells = [lineEmitterCell] - self.outlineLayer.emitterMode = .points - 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) - 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 = 1000 - line2EmitterCell.scale = 0.15 - line2EmitterCell.alphaSpeed = -0.4 - - self.outline2Layer.emitterCells = [line2EmitterCell] - self.outline2Layer.emitterMode = .points - self.outline2Layer.emitterSize = CGSize(width: 1.33, height: 1.33) - self.outline2Layer.emitterShape = .rectangle - - - - + self.outlineLayer.emitterMode = .outline + self.outlineLayer.emitterSize = CGSize(width: 2.0, height: 2.0) + self.outlineLayer.emitterShape = .line let glowAnimation = CAKeyframeAnimation(keyPath: "emitterPosition") glowAnimation.path = path.path.cgPath @@ -166,7 +126,6 @@ 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) @@ -192,7 +151,6 @@ 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 } @@ -214,7 +172,7 @@ private func getPathFromMaskImage(_ image: CIImage, size: CGSize, values: MediaE } contour = simplify(contour, tolerance: 1.0) - let path = BezierPath(points: contour, smooth: true) + let path = BezierPath(points: contour, smooth: false) let contoursScale = min(size.width, size.height) / 256.0 let valuesScale = size.width / 1080.0 @@ -279,7 +237,7 @@ private func findContours(pixelBuffer: CVPixelBuffer) -> [CGPoint] { func isBlackPixel(_ point: Point) -> Bool { if point.x >= 0 && point.x < width && point.y >= 0 && point.y < height { let value = getPixelIntensity(point) - return value < 220 + return value < 225 } else { return false } @@ -296,18 +254,20 @@ private func findContours(pixelBuffer: CVPixelBuffer) -> [CGPoint] { repeat { var found = false for i in 0 ..< 8 { - let direction = (previousDirection + i) % 8 - let newX = currentPoint.x + dx[direction] - let newY = currentPoint.y + dy[direction] - let newPoint = Point(x: newX, y: newY) - - if isBlackPixel(newPoint) && !(visited[newPoint] == true) { - contour.append(newPoint) - previousDirection = (direction + 5) % 8 - currentPoint = newPoint - found = true - markVisited(newPoint) - break + for j in 1 ..< 2 { + let direction = (previousDirection + i) % 8 + let newX = currentPoint.x + dx[direction] * j + let newY = currentPoint.y + dy[direction] * j + let newPoint = Point(x: newX, y: newY) + + if isBlackPixel(newPoint) && !(visited[newPoint] == true) { + contour.append(newPoint) + previousDirection = (direction + 5) % 8 + currentPoint = newPoint + found = true + markVisited(newPoint) + break + } } } if !found { @@ -509,7 +469,7 @@ private extension CGPoint { func distanceFrom(_ otherPoint: CGPoint) -> CGFloat { let dx = self.x - otherPoint.x let dy = self.y - otherPoint.y - return (dx * dx) + (dy * dy) + return sqrt((dx * dx) + (dy * dy)) } func distanceToSegment(_ p1: CGPoint, _ p2: CGPoint) -> CGFloat {