From f1406b876ae089610387961e45d7e925d59f7442 Mon Sep 17 00:00:00 2001 From: Ilya Laktyushin Date: Tue, 9 Apr 2024 20:57:17 +0400 Subject: [PATCH] [WIP] Stickers editor --- .../Sources/MediaEditorScreen.swift | 606 +++++++++--------- .../Sources/StickerCutoutOutlineView.swift | 90 ++- 2 files changed, 389 insertions(+), 307 deletions(-) diff --git a/submodules/TelegramUI/Components/MediaEditorScreen/Sources/MediaEditorScreen.swift b/submodules/TelegramUI/Components/MediaEditorScreen/Sources/MediaEditorScreen.swift index af23bc10cb..433df9df8d 100644 --- a/submodules/TelegramUI/Components/MediaEditorScreen/Sources/MediaEditorScreen.swift +++ b/submodules/TelegramUI/Components/MediaEditorScreen/Sources/MediaEditorScreen.swift @@ -1028,306 +1028,6 @@ final class MediaEditorScreenComponent: Component { } } - let mediaEditor = controller.node.mediaEditor - var isOutlineActive = false - if let value = mediaEditor?.values.toolValues[.stickerOutline] as? Float, value > 0.0 { - isOutlineActive = true - } - - if case .stickerEditor = controller.mode { - var stickerButtonsHidden = buttonsAreHidden - if let displayingTool = component.isDisplayingTool, [.cutoutErase, .cutoutRestore].contains(displayingTool) { - stickerButtonsHidden = false - } - let stickerButtonsAlpha = stickerButtonsHidden ? 0.0 : bottomButtonsAlpha - - let stickerFrameWidth = floorToScreenPixels(previewSize.width * 0.97) - let stickerFrameRect = CGRect(origin: CGPoint(x: previewFrame.minX + floorToScreenPixels((previewSize.width - stickerFrameWidth) / 2.0), y: previewFrame.minY + floorToScreenPixels((previewSize.height - stickerFrameWidth) / 2.0)), size: CGSize(width: stickerFrameWidth, height: stickerFrameWidth)) - - var hasCutoutButton = false - var hasUndoButton = false - var hasEraseButton = false - var hasRestoreButton = false - var hasOutlineButton = false - - if let canCutout = controller.node.canCutout { - if controller.node.isCutout || controller.node.stickerMaskDrawingView?.internalState.canUndo == true { - hasUndoButton = true - } - if canCutout && !controller.node.isCutout { - hasCutoutButton = true - } else { - hasEraseButton = true - if hasUndoButton { - hasRestoreButton = true - } - } - if hasUndoButton || controller.node.hasTransparency { - hasOutlineButton = true - } - } - - if hasUndoButton { - let undoButtonSize = self.undoButton.update( - transition: transition, - component: AnyComponent(PlainButtonComponent( - content: AnyComponent(CutoutButtonContentComponent( - backgroundColor: UIColor(rgb: 0xffffff, alpha: 0.18), - icon: state.image(.undo), - title: "Undo" - )), - effectAlignment: .center, - action: { - cutoutUndo() - } - )), - environment: {}, - containerSize: CGSize(width: availableSize.width, height: 44.0) - ) - let undoButtonFrame = CGRect( - origin: CGPoint(x: floorToScreenPixels((availableSize.width - undoButtonSize.width) / 2.0), y: stickerFrameRect.minY - 35.0 - undoButtonSize.height), - size: undoButtonSize - ) - if let undoButtonView = self.undoButton.view { - var positionTransition = transition - if undoButtonView.superview == nil { - self.addSubview(undoButtonView) - - undoButtonView.alpha = stickerButtonsAlpha - undoButtonView.layer.animateAlpha(from: 0.0, to: stickerButtonsAlpha, duration: 0.2, delay: 0.0) - undoButtonView.layer.animateScale(from: 0.1, to: 1.0, duration: 0.2, delay: 0.0) - positionTransition = .immediate - } - positionTransition.setPosition(view: undoButtonView, position: undoButtonFrame.center) - undoButtonView.bounds = CGRect(origin: .zero, size: undoButtonFrame.size) - transition.setAlpha(view: undoButtonView, alpha: stickerButtonsAlpha) - } - } else { - if let undoButtonView = self.undoButton.view, undoButtonView.superview != nil { - undoButtonView.alpha = 0.0 - undoButtonView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, delay: 0.0, completion: { _ in - undoButtonView.removeFromSuperview() - }) - undoButtonView.layer.animateScale(from: 1.0, to: 0.1, duration: 0.2, delay: 0.0) - } - } - - if hasCutoutButton { - let cutoutButtonSize = self.cutoutButton.update( - transition: transition, - component: AnyComponent(PlainButtonComponent( - content: AnyComponent(CutoutButtonContentComponent( - backgroundColor: UIColor(rgb: 0xffffff, alpha: 0.18), - icon: state.image(.cutout), - title: "Cut Out an Object" - )), - effectAlignment: .center, - action: { - openDrawing(.cutout) - } - )), - environment: {}, - containerSize: CGSize(width: availableSize.width, height: 44.0) - ) - let cutoutButtonFrame = CGRect( - origin: CGPoint(x: floorToScreenPixels((availableSize.width - cutoutButtonSize.width) / 2.0), y: stickerFrameRect.maxY + 35.0), - size: cutoutButtonSize - ) - if let cutoutButtonView = self.cutoutButton.view { - var positionTransition = transition - if cutoutButtonView.superview == nil { - self.addSubview(cutoutButtonView) - - cutoutButtonView.layer.animatePosition(from: CGPoint(x: 0.0, y: 64.0), to: .zero, duration: 0.3, delay: 0.0, timingFunction: kCAMediaTimingFunctionSpring, additive: true) - cutoutButtonView.layer.animateAlpha(from: 0.0, to: stickerButtonsAlpha, duration: 0.2, delay: 0.0) - cutoutButtonView.layer.animateScale(from: 0.1, to: 1.0, duration: 0.2, delay: 0.0) - positionTransition = .immediate - } - positionTransition.setPosition(view: cutoutButtonView, position: cutoutButtonFrame.center) - cutoutButtonView.bounds = CGRect(origin: .zero, size: cutoutButtonFrame.size) - transition.setAlpha(view: cutoutButtonView, alpha: stickerButtonsAlpha) - } - } else { - if let cutoutButtonView = self.cutoutButton.view, cutoutButtonView.superview != nil { - cutoutButtonView.alpha = 0.0 - if transition.animation.isImmediate { - cutoutButtonView.removeFromSuperview() - } 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) - } - } - } - - if hasEraseButton { - let buttonSpacing: CGFloat = hasRestoreButton ? 10.0 : 0.0 - var totalButtonsWidth = buttonSpacing - - let eraseButtonSize = self.eraseButton.update( - transition: transition, - component: AnyComponent(PlainButtonComponent( - content: AnyComponent(CutoutButtonContentComponent( - backgroundColor: UIColor(rgb: 0xffffff, alpha: 0.18), - icon: state.image(.erase), - title: "Erase", - minWidth: 160.0, - selected: component.isDisplayingTool == .cutoutErase - )), - effectAlignment: .center, - action: { - openDrawing(.cutoutErase) - } - )), - environment: {}, - containerSize: CGSize(width: availableSize.width, height: 44.0) - ) - totalButtonsWidth += eraseButtonSize.width - - var buttonOriginX = floorToScreenPixels((availableSize.width - totalButtonsWidth) / 2.0) - - if hasRestoreButton { - let restoreButtonSize = self.restoreButton.update( - transition: transition, - component: AnyComponent(PlainButtonComponent( - content: AnyComponent(CutoutButtonContentComponent( - backgroundColor: UIColor(rgb: 0xffffff, alpha: 0.18), - icon: state.image(.restore), - title: "Restore", - minWidth: 160.0, - selected: component.isDisplayingTool == .cutoutRestore - )), - effectAlignment: .center, - action: { - openDrawing(.cutoutRestore) - } - )), - environment: {}, - containerSize: CGSize(width: availableSize.width, height: 44.0) - ) - totalButtonsWidth += restoreButtonSize.width - - buttonOriginX = floorToScreenPixels((availableSize.width - totalButtonsWidth) / 2.0) - let restoreButtonFrame = CGRect( - origin: CGPoint(x: buttonOriginX + eraseButtonSize.width + buttonSpacing, y: stickerFrameRect.maxY + 35.0), - size: restoreButtonSize - ) - if let restoreButtonView = self.restoreButton.view { - var positionTransition = transition - if restoreButtonView.superview == nil { - self.addSubview(restoreButtonView) - - restoreButtonView.alpha = stickerButtonsAlpha - restoreButtonView.layer.animateAlpha(from: 0.0, to: stickerButtonsAlpha, duration: 0.2, delay: 0.0) - restoreButtonView.layer.animateScale(from: 0.1, to: 1.0, duration: 0.2, delay: 0.0) - restoreButtonView.layer.animatePosition(from: CGPoint(x: 0.0, y: 64.0), to: .zero, duration: 0.3, delay: 0.0, timingFunction: kCAMediaTimingFunctionSpring, additive: true) - positionTransition = .immediate - } - positionTransition.setPosition(view: restoreButtonView, position: restoreButtonFrame.center) - restoreButtonView.bounds = CGRect(origin: .zero, size: restoreButtonFrame.size) - transition.setAlpha(view: restoreButtonView, alpha: stickerButtonsAlpha) - } - } - - let eraseButtonFrame = CGRect( - origin: CGPoint(x: buttonOriginX, y: stickerFrameRect.maxY + 35.0), - size: eraseButtonSize - ) - if let eraseButtonView = self.eraseButton.view { - var positionTransition = transition - if eraseButtonView.superview == nil { - self.addSubview(eraseButtonView) - - eraseButtonView.alpha = stickerButtonsAlpha - eraseButtonView.layer.animatePosition(from: CGPoint(x: 0.0, y: 64.0), to: .zero, duration: 0.3, delay: 0.0, timingFunction: kCAMediaTimingFunctionSpring, additive: true) - eraseButtonView.layer.animateAlpha(from: 0.0, to: stickerButtonsAlpha, duration: 0.2, delay: 0.0) - eraseButtonView.layer.animateScale(from: 0.1, to: 1.0, duration: 0.2, delay: 0.0) - positionTransition = .immediate - } - positionTransition.setPosition(view: eraseButtonView, position: eraseButtonFrame.center) - eraseButtonView.bounds = CGRect(origin: .zero, size: eraseButtonFrame.size) - transition.setAlpha(view: eraseButtonView, alpha: stickerButtonsAlpha) - } - } else { - if let eraseButtonView = self.eraseButton.view, eraseButtonView.superview != nil { - eraseButtonView.alpha = 0.0 - eraseButtonView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, delay: 0.0, completion: { _ in - eraseButtonView.removeFromSuperview() - }) - eraseButtonView.layer.animateScale(from: 1.0, to: 0.1, duration: 0.2, delay: 0.0) - } - } - if !hasRestoreButton { - if let restoreButtonView = self.restoreButton.view, restoreButtonView.superview != nil { - restoreButtonView.alpha = 0.0 - restoreButtonView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, delay: 0.0, completion: { _ in - restoreButtonView.removeFromSuperview() - }) - restoreButtonView.layer.animateScale(from: 1.0, to: 0.1, duration: 0.2, delay: 0.0) - } - } - - if hasOutlineButton { - let outlineButtonSize = self.outlineButton.update( - transition: transition, - component: AnyComponent(PlainButtonComponent( - content: AnyComponent(CutoutButtonContentComponent( - backgroundColor: UIColor(rgb: 0xffffff, alpha: 0.18), - icon: state.image(.outline), - title: "Add Outline", - minWidth: 160.0, - selected: isOutlineActive - )), - effectAlignment: .center, - action: { [weak self, weak controller] in - guard let self, let mediaEditor = controller?.node.mediaEditor else { - return - } - if let value = mediaEditor.values.toolValues[.stickerOutline] as? Float, value > 0.0 { - mediaEditor.setToolValue(.stickerOutline, value: Float(0.0)) - } else { - mediaEditor.setToolValue(.stickerOutline, value: Float(0.5)) - } - self.state?.updated(transition: .easeInOut(duration: 0.25)) - } - )), - environment: {}, - containerSize: CGSize(width: availableSize.width, height: 44.0) - ) - - let outlineButtonFrame = CGRect( - origin: CGPoint(x: floorToScreenPixels((availableSize.width - outlineButtonSize.width) / 2.0), y: stickerFrameRect.maxY + 35.0 + 40.0 + 16.0), - size: outlineButtonSize - ) - if let outlineButtonView = self.outlineButton.view { - let outlineButtonAlpha = buttonsAreHidden ? 0.0 : bottomButtonsAlpha - var positionTransition = transition - if outlineButtonView.superview == nil { - self.addSubview(outlineButtonView) - - outlineButtonView.alpha = outlineButtonAlpha - outlineButtonView.layer.animateAlpha(from: 0.0, to: outlineButtonAlpha, duration: 0.2, delay: 0.0) - outlineButtonView.layer.animateScale(from: 0.1, to: 1.0, duration: 0.2, delay: 0.0) - outlineButtonView.layer.animatePosition(from: CGPoint(x: 0.0, y: 64.0), to: .zero, duration: 0.3, delay: 0.0, timingFunction: kCAMediaTimingFunctionSpring, additive: true) - - positionTransition = .immediate - } - positionTransition.setPosition(view: outlineButtonView, position: outlineButtonFrame.center) - outlineButtonView.bounds = CGRect(origin: .zero, size: outlineButtonFrame.size) - transition.setAlpha(view: outlineButtonView, alpha: outlineButtonAlpha) - } - } else { - if let outlineButtonView = self.outlineButton.view, outlineButtonView.superview != nil { - outlineButtonView.alpha = 0.0 - outlineButtonView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, delay: 0.0, completion: { _ in - outlineButtonView.removeFromSuperview() - }) - outlineButtonView.layer.animateScale(from: 1.0, to: 0.1, duration: 0.2, delay: 0.0) - } - } - } - var timeoutValue: String switch component.privacy.timeout { case 21600: @@ -1460,6 +1160,12 @@ final class MediaEditorScreenComponent: Component { ) } + let mediaEditor = controller.node.mediaEditor + var isOutlineActive = false + if let value = mediaEditor?.values.toolValues[.stickerOutline] as? Float, value > 0.0 { + isOutlineActive = true + } + var isEditingTextEntity = false var sizeSliderVisible = false var sizeValue: CGFloat? @@ -1479,6 +1185,8 @@ final class MediaEditorScreenComponent: Component { } } + let displayTopButtons = !(self.inputPanelExternalState.isEditing || isEditingTextEntity || component.isDisplayingTool != nil) + if case .storyEditor = controller.mode { let nextInputMode: MessageInputPanelComponent.InputMode switch self.currentInputMode { @@ -1895,9 +1603,7 @@ final class MediaEditorScreenComponent: Component { } } } - - let displayTopButtons = !(self.inputPanelExternalState.isEditing || isEditingTextEntity || component.isDisplayingTool != nil) - + let saveContentComponent: AnyComponentWithIdentity if component.hasAppeared { saveContentComponent = AnyComponentWithIdentity( @@ -2256,6 +1962,300 @@ final class MediaEditorScreenComponent: Component { } } + if case .stickerEditor = controller.mode { + var stickerButtonsHidden = buttonsAreHidden + if let displayingTool = component.isDisplayingTool, [.cutoutErase, .cutoutRestore].contains(displayingTool) { + stickerButtonsHidden = false + } + let stickerButtonsAlpha = stickerButtonsHidden ? 0.0 : bottomButtonsAlpha + + let stickerFrameWidth = floorToScreenPixels(previewSize.width * 0.97) + let stickerFrameRect = CGRect(origin: CGPoint(x: previewFrame.minX + floorToScreenPixels((previewSize.width - stickerFrameWidth) / 2.0), y: previewFrame.minY + floorToScreenPixels((previewSize.height - stickerFrameWidth) / 2.0)), size: CGSize(width: stickerFrameWidth, height: stickerFrameWidth)) + + var hasCutoutButton = false + var hasUndoButton = false + var hasEraseButton = false + var hasRestoreButton = false + var hasOutlineButton = false + + if let canCutout = controller.node.canCutout { + if controller.node.isCutout || controller.node.stickerMaskDrawingView?.internalState.canUndo == true { + hasUndoButton = true + } + if canCutout && !controller.node.isCutout { + hasCutoutButton = true + } else { + hasEraseButton = true + if hasUndoButton { + hasRestoreButton = true + } + } + if hasUndoButton || controller.node.hasTransparency { + hasOutlineButton = true + } + } + + if hasUndoButton { + let undoButtonSize = self.undoButton.update( + transition: transition, + component: AnyComponent(PlainButtonComponent( + content: AnyComponent(CutoutButtonContentComponent( + backgroundColor: UIColor(rgb: 0xffffff, alpha: 0.18), + icon: state.image(.undo), + title: "Undo" + )), + effectAlignment: .center, + action: { + cutoutUndo() + } + )), + environment: {}, + containerSize: CGSize(width: availableSize.width, height: 44.0) + ) + let undoButtonFrame = CGRect( + origin: CGPoint(x: floorToScreenPixels((availableSize.width - undoButtonSize.width) / 2.0), y: stickerFrameRect.minY - 35.0 - undoButtonSize.height), + size: undoButtonSize + ) + if let undoButtonView = self.undoButton.view { + var positionTransition = transition + if undoButtonView.superview == nil { + self.addSubview(undoButtonView) + + undoButtonView.alpha = stickerButtonsAlpha + undoButtonView.layer.animateAlpha(from: 0.0, to: stickerButtonsAlpha, duration: 0.2, delay: 0.0) + undoButtonView.layer.animateScale(from: 0.1, to: 1.0, duration: 0.2, delay: 0.0) + positionTransition = .immediate + } + positionTransition.setPosition(view: undoButtonView, position: undoButtonFrame.center) + undoButtonView.bounds = CGRect(origin: .zero, size: undoButtonFrame.size) + transition.setAlpha(view: undoButtonView, alpha: displayTopButtons && !component.isDismissing ? stickerButtonsAlpha : 0.0) + transition.setScale(view: undoButtonView, scale: displayTopButtons ? 1.0 : 0.01) + } + } else { + if let undoButtonView = self.undoButton.view, undoButtonView.superview != nil { + undoButtonView.alpha = 0.0 + undoButtonView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, delay: 0.0, completion: { _ in + undoButtonView.removeFromSuperview() + }) + undoButtonView.layer.animateScale(from: 1.0, to: 0.1, duration: 0.2, delay: 0.0) + } + } + + if hasCutoutButton { + let cutoutButtonSize = self.cutoutButton.update( + transition: transition, + component: AnyComponent(PlainButtonComponent( + content: AnyComponent(CutoutButtonContentComponent( + backgroundColor: UIColor(rgb: 0xffffff, alpha: 0.18), + icon: state.image(.cutout), + title: "Cut Out an Object" + )), + effectAlignment: .center, + action: { + openDrawing(.cutout) + } + )), + environment: {}, + containerSize: CGSize(width: availableSize.width, height: 44.0) + ) + let cutoutButtonFrame = CGRect( + origin: CGPoint(x: floorToScreenPixels((availableSize.width - cutoutButtonSize.width) / 2.0), y: stickerFrameRect.maxY + 35.0), + size: cutoutButtonSize + ) + if let cutoutButtonView = self.cutoutButton.view { + var positionTransition = transition + if cutoutButtonView.superview == nil { + self.addSubview(cutoutButtonView) + + cutoutButtonView.layer.animatePosition(from: CGPoint(x: 0.0, y: 64.0), to: .zero, duration: 0.3, delay: 0.0, timingFunction: kCAMediaTimingFunctionSpring, additive: true) + cutoutButtonView.layer.animateAlpha(from: 0.0, to: stickerButtonsAlpha, duration: 0.2, delay: 0.0) + cutoutButtonView.layer.animateScale(from: 0.1, to: 1.0, duration: 0.2, delay: 0.0) + positionTransition = .immediate + } + positionTransition.setPosition(view: cutoutButtonView, position: cutoutButtonFrame.center) + cutoutButtonView.bounds = CGRect(origin: .zero, size: cutoutButtonFrame.size) + transition.setAlpha(view: cutoutButtonView, alpha: stickerButtonsAlpha) + } + } else { + if let cutoutButtonView = self.cutoutButton.view, cutoutButtonView.superview != nil { + cutoutButtonView.alpha = 0.0 + if transition.animation.isImmediate { + cutoutButtonView.removeFromSuperview() + } 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) + } + } + } + + if hasEraseButton { + let buttonSpacing: CGFloat = hasRestoreButton ? 10.0 : 0.0 + var totalButtonsWidth = buttonSpacing + + let eraseButtonSize = self.eraseButton.update( + transition: transition, + component: AnyComponent(PlainButtonComponent( + content: AnyComponent(CutoutButtonContentComponent( + backgroundColor: UIColor(rgb: 0xffffff, alpha: 0.18), + icon: state.image(.erase), + title: "Erase", + minWidth: 160.0, + selected: component.isDisplayingTool == .cutoutErase + )), + effectAlignment: .center, + action: { + openDrawing(.cutoutErase) + } + )), + environment: {}, + containerSize: CGSize(width: availableSize.width, height: 44.0) + ) + totalButtonsWidth += eraseButtonSize.width + + var buttonOriginX = floorToScreenPixels((availableSize.width - totalButtonsWidth) / 2.0) + + if hasRestoreButton { + let restoreButtonSize = self.restoreButton.update( + transition: transition, + component: AnyComponent(PlainButtonComponent( + content: AnyComponent(CutoutButtonContentComponent( + backgroundColor: UIColor(rgb: 0xffffff, alpha: 0.18), + icon: state.image(.restore), + title: "Restore", + minWidth: 160.0, + selected: component.isDisplayingTool == .cutoutRestore + )), + effectAlignment: .center, + action: { + openDrawing(.cutoutRestore) + } + )), + environment: {}, + containerSize: CGSize(width: availableSize.width, height: 44.0) + ) + totalButtonsWidth += restoreButtonSize.width + + buttonOriginX = floorToScreenPixels((availableSize.width - totalButtonsWidth) / 2.0) + let restoreButtonFrame = CGRect( + origin: CGPoint(x: buttonOriginX + eraseButtonSize.width + buttonSpacing, y: stickerFrameRect.maxY + 35.0), + size: restoreButtonSize + ) + if let restoreButtonView = self.restoreButton.view { + var positionTransition = transition + if restoreButtonView.superview == nil { + self.addSubview(restoreButtonView) + + restoreButtonView.alpha = stickerButtonsAlpha + restoreButtonView.layer.animateAlpha(from: 0.0, to: stickerButtonsAlpha, duration: 0.2, delay: 0.0) + restoreButtonView.layer.animateScale(from: 0.1, to: 1.0, duration: 0.2, delay: 0.0) + restoreButtonView.layer.animatePosition(from: CGPoint(x: 0.0, y: 64.0), to: .zero, duration: 0.3, delay: 0.0, timingFunction: kCAMediaTimingFunctionSpring, additive: true) + positionTransition = .immediate + } + positionTransition.setPosition(view: restoreButtonView, position: restoreButtonFrame.center) + restoreButtonView.bounds = CGRect(origin: .zero, size: restoreButtonFrame.size) + transition.setAlpha(view: restoreButtonView, alpha: stickerButtonsAlpha) + } + } + + let eraseButtonFrame = CGRect( + origin: CGPoint(x: buttonOriginX, y: stickerFrameRect.maxY + 35.0), + size: eraseButtonSize + ) + if let eraseButtonView = self.eraseButton.view { + var positionTransition = transition + if eraseButtonView.superview == nil { + self.addSubview(eraseButtonView) + + eraseButtonView.alpha = stickerButtonsAlpha + eraseButtonView.layer.animatePosition(from: CGPoint(x: 0.0, y: 64.0), to: .zero, duration: 0.3, delay: 0.0, timingFunction: kCAMediaTimingFunctionSpring, additive: true) + eraseButtonView.layer.animateAlpha(from: 0.0, to: stickerButtonsAlpha, duration: 0.2, delay: 0.0) + eraseButtonView.layer.animateScale(from: 0.1, to: 1.0, duration: 0.2, delay: 0.0) + positionTransition = .immediate + } + positionTransition.setPosition(view: eraseButtonView, position: eraseButtonFrame.center) + eraseButtonView.bounds = CGRect(origin: .zero, size: eraseButtonFrame.size) + transition.setAlpha(view: eraseButtonView, alpha: stickerButtonsAlpha) + } + } else { + if let eraseButtonView = self.eraseButton.view, eraseButtonView.superview != nil { + eraseButtonView.alpha = 0.0 + eraseButtonView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, delay: 0.0, completion: { _ in + eraseButtonView.removeFromSuperview() + }) + eraseButtonView.layer.animateScale(from: 1.0, to: 0.1, duration: 0.2, delay: 0.0) + } + } + if !hasRestoreButton { + if let restoreButtonView = self.restoreButton.view, restoreButtonView.superview != nil { + restoreButtonView.alpha = 0.0 + restoreButtonView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, delay: 0.0, completion: { _ in + restoreButtonView.removeFromSuperview() + }) + restoreButtonView.layer.animateScale(from: 1.0, to: 0.1, duration: 0.2, delay: 0.0) + } + } + + if hasOutlineButton { + let outlineButtonSize = self.outlineButton.update( + transition: transition, + component: AnyComponent(PlainButtonComponent( + content: AnyComponent(CutoutButtonContentComponent( + backgroundColor: UIColor(rgb: 0xffffff, alpha: 0.18), + icon: state.image(.outline), + title: "Add Outline", + minWidth: 160.0, + selected: isOutlineActive + )), + effectAlignment: .center, + action: { [weak self, weak controller] in + guard let self, let mediaEditor = controller?.node.mediaEditor else { + return + } + if let value = mediaEditor.values.toolValues[.stickerOutline] as? Float, value > 0.0 { + mediaEditor.setToolValue(.stickerOutline, value: Float(0.0)) + } else { + mediaEditor.setToolValue(.stickerOutline, value: Float(0.5)) + } + self.state?.updated(transition: .easeInOut(duration: 0.25)) + } + )), + environment: {}, + containerSize: CGSize(width: availableSize.width, height: 44.0) + ) + + let outlineButtonFrame = CGRect( + origin: CGPoint(x: floorToScreenPixels((availableSize.width - outlineButtonSize.width) / 2.0), y: stickerFrameRect.maxY + 35.0 + 40.0 + 16.0), + size: outlineButtonSize + ) + if let outlineButtonView = self.outlineButton.view { + let outlineButtonAlpha = buttonsAreHidden ? 0.0 : bottomButtonsAlpha + var positionTransition = transition + if outlineButtonView.superview == nil { + self.addSubview(outlineButtonView) + + outlineButtonView.alpha = outlineButtonAlpha + outlineButtonView.layer.animateAlpha(from: 0.0, to: outlineButtonAlpha, duration: 0.2, delay: 0.0) + outlineButtonView.layer.animateScale(from: 0.1, to: 1.0, duration: 0.2, delay: 0.0) + outlineButtonView.layer.animatePosition(from: CGPoint(x: 0.0, y: 64.0), to: .zero, duration: 0.3, delay: 0.0, timingFunction: kCAMediaTimingFunctionSpring, additive: true) + + positionTransition = .immediate + } + positionTransition.setPosition(view: outlineButtonView, position: outlineButtonFrame.center) + outlineButtonView.bounds = CGRect(origin: .zero, size: outlineButtonFrame.size) + transition.setAlpha(view: outlineButtonView, alpha: outlineButtonAlpha) + } + } else { + if let outlineButtonView = self.outlineButton.view, outlineButtonView.superview != nil { + outlineButtonView.alpha = 0.0 + outlineButtonView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, delay: 0.0, completion: { _ in + outlineButtonView.removeFromSuperview() + }) + outlineButtonView.layer.animateScale(from: 1.0, to: 0.1, duration: 0.2, delay: 0.0) + } + } + } let textCancelButtonSize = self.textCancelButton.update( transition: transition, diff --git a/submodules/TelegramUI/Components/MediaEditorScreen/Sources/StickerCutoutOutlineView.swift b/submodules/TelegramUI/Components/MediaEditorScreen/Sources/StickerCutoutOutlineView.swift index ea9f67a45b..5aab1c46b5 100644 --- a/submodules/TelegramUI/Components/MediaEditorScreen/Sources/StickerCutoutOutlineView.swift +++ b/submodules/TelegramUI/Components/MediaEditorScreen/Sources/StickerCutoutOutlineView.swift @@ -68,7 +68,7 @@ final class StickerCutoutOutlineView: UIView { let randomBeginTime = (previousBeginTime + 4) % 6 previousBeginTime = randomBeginTime - let duration = min(6.5, max(3.0, path.length / 100.0)) + let duration = min(8.0, max(3.0, path.length / 135.0)) let outlineAnimation = CAKeyframeAnimation(keyPath: "emitterPosition") outlineAnimation.path = path.path.cgPath @@ -157,16 +157,18 @@ final class StickerCutoutOutlineView: UIView { } private func getPathFromMaskImage(_ image: CIImage, size: CGSize, values: MediaEditorValues) -> BezierPath? { - let edges = image.applyingFilter("CILineOverlay", parameters: ["inputEdgeIntensity": 0.1]) +// let edges = image.applyingFilter("CILineOverlay", parameters: ["inputEdgeIntensity": 0.1]) - guard let pixelBuffer = getEdgesBitmap(edges) else { + guard let pixelBuffer = getEdgesBitmap(image) else { return nil } let minSide = min(size.width, size.height) let scaledImageSize = image.extent.size.aspectFilled(CGSize(width: minSide, height: minSide)) let contourImageSize = image.extent.size.aspectFilled(CGSize(width: 256.0, height: 256.0)) - var contour = findContours(pixelBuffer: pixelBuffer) +// var contour = findContours(pixelBuffer: pixelBuffer) + + var contour = findEdgePoints(in: pixelBuffer) guard !contour.isEmpty else { return nil } @@ -203,6 +205,86 @@ private func getPathFromMaskImage(_ image: CIImage, size: CGSize, values: MediaE return nil } +func findEdgePoints(in pixelBuffer: CVPixelBuffer) -> [CGPoint] { + struct Point: Hashable { + let x: Int + let y: Int + + var cgPoint: CGPoint { + return CGPoint(x: x, y: y) + } + } + + let width = CVPixelBufferGetWidth(pixelBuffer) + let height = CVPixelBufferGetHeight(pixelBuffer) + var edgePoints: Set = [] + var edgePath: [Point] = [] + + CVPixelBufferLockBaseAddress(pixelBuffer, .readOnly) + let baseAddress = CVPixelBufferGetBaseAddress(pixelBuffer) + let bytesPerRow = CVPixelBufferGetBytesPerRow(pixelBuffer) + + func isPixelWhiteAt(x: Int, y: Int) -> Bool { + let pixelOffset = y * bytesPerRow + x + let pixelPtr = baseAddress?.advanced(by: pixelOffset) + let pixel = pixelPtr?.load(as: UInt8.self) ?? 0 + return pixel >= 235 + } + + let directions = [(1, 0), (1, 1), (0, 1), (-1, 1), (-1, 0), (-1, -1), (0, -1), (1, -1)] + var lastDirectionIndex = 0 + + var startPoint: Point? = nil +outerLoop: for y in 0.. Bool { + return abs(point.x - startPoint.x) <= tolerance && abs(point.y - startPoint.y) <= tolerance + } + + repeat { + var foundNextPoint = false + for i in 0..= 0, nextX < width, nextY >= 0, nextY < height, isPixelWhiteAt(x: nextX, y: nextY) { + let nextPoint = Point(x: nextX, y: nextY) + if !edgePoints.contains(nextPoint) { + edgePoints.insert(nextPoint) + edgePath.append(nextPoint) + currentPoint = nextPoint + lastDirectionIndex = (directionIndex + 6) % directions.count + foundNextPoint = true + break + } + } + } + + if !foundNextPoint || (edgePath.count > 3 && isCloseEnough(currentPoint, to: startingPoint)) { + break + } + } while true + + CVPixelBufferUnlockBaseAddress(pixelBuffer, .readOnly) + + return Array(edgePath.map { $0.cgPoint }) +} + private func findContours(pixelBuffer: CVPixelBuffer) -> [CGPoint] { struct Point: Hashable { let x: Int