mirror of
https://github.com/Swiftgram/Telegram-iOS.git
synced 2025-06-16 05:55:20 +00:00
Various improvements
This commit is contained in:
parent
5ce85e7b75
commit
64eaaae4fd
@ -885,7 +885,7 @@ public final class DrawingEntitiesView: UIView, TGPhotoDrawingEntitiesView {
|
||||
} else if self.autoSelectEntities, gestureRecognizer.numberOfTouches == 1, let viewToSelect = self.entity(at: location) {
|
||||
self.selectEntity(viewToSelect.entity, animate: false)
|
||||
self.onInteractionUpdated(true)
|
||||
} else if gestureRecognizer.numberOfTouches == 2 || self.isStickerEditor, let mediaEntityView = self.subviews.first(where: { $0 is DrawingEntityMediaView }) as? DrawingEntityMediaView {
|
||||
} else if gestureRecognizer.numberOfTouches == 2 || (self.isStickerEditor && self.autoSelectEntities), let mediaEntityView = self.subviews.first(where: { $0 is DrawingEntityMediaView }) as? DrawingEntityMediaView {
|
||||
mediaEntityView.handlePan(gestureRecognizer)
|
||||
}
|
||||
}
|
||||
|
@ -257,6 +257,12 @@ public extension TelegramEngine {
|
||||
return (items.map(\.file), isFinalResult)
|
||||
}
|
||||
}
|
||||
|
||||
public func addRecentlyUsedSticker(file: TelegramMediaFile) {
|
||||
let _ = self.account.postbox.transaction({ transaction -> Void in
|
||||
TelegramCore.addRecentlyUsedSticker(transaction: transaction, fileReference: .standalone(media: file))
|
||||
}).start()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -3,6 +3,14 @@
|
||||
|
||||
using namespace metal;
|
||||
|
||||
typedef struct {
|
||||
float2 dimensions;
|
||||
float roundness;
|
||||
float alpha;
|
||||
float isOpaque;
|
||||
float empty;
|
||||
} VideoEncodeParameters;
|
||||
|
||||
typedef struct {
|
||||
float4 pos;
|
||||
float2 texCoord;
|
||||
@ -17,11 +25,10 @@ float sdfRoundedRectangle(float2 uv, float2 position, float2 size, float radius)
|
||||
|
||||
fragment half4 dualFragmentShader(RasterizerData in [[stage_in]],
|
||||
texture2d<half, access::sample> texture [[texture(0)]],
|
||||
constant uint2 &resolution[[buffer(0)]],
|
||||
constant float &roundness[[buffer(1)]],
|
||||
constant float &alpha[[buffer(2)]]
|
||||
texture2d<half, access::sample> mask [[texture(1)]],
|
||||
constant VideoEncodeParameters& adjustments [[buffer(0)]]
|
||||
) {
|
||||
float2 R = float2(resolution.x, resolution.y);
|
||||
float2 R = float2(adjustments.dimensions.x, adjustments.dimensions.y);
|
||||
|
||||
float2 uv = (in.localPos - float2(0.5, 0.5)) * 2.0;
|
||||
if (R.x > R.y) {
|
||||
@ -33,10 +40,11 @@ fragment half4 dualFragmentShader(RasterizerData in [[stage_in]],
|
||||
|
||||
constexpr sampler samplr(filter::linear, mag_filter::linear, min_filter::linear);
|
||||
half3 color = texture.sample(samplr, in.texCoord).rgb;
|
||||
float colorAlpha = min(1.0, adjustments.isOpaque + mask.sample(samplr, in.texCoord).r);
|
||||
|
||||
float t = 1.0 / resolution.y;
|
||||
float t = 1.0 / adjustments.dimensions.y;
|
||||
float side = 1.0 * aspectRatio;
|
||||
float distance = smoothstep(t, -t, sdfRoundedRectangle(uv, float2(0.0, 0.0), float2(side, mix(1.0, side, roundness)), side * roundness));
|
||||
float distance = smoothstep(t, -t, sdfRoundedRectangle(uv, float2(0.0, 0.0), float2(side, mix(1.0, side, adjustments.roundness)), side * adjustments.roundness));
|
||||
|
||||
return mix(half4(color, 0.0), half4(color, 1.0 * alpha), distance);
|
||||
return mix(half4(color, 0.0), half4(color, colorAlpha * adjustments.alpha), distance);
|
||||
}
|
||||
|
@ -55,6 +55,70 @@ public func cutoutStickerImage(from image: UIImage, onlyCheck: Bool = false) ->
|
||||
}
|
||||
}
|
||||
|
||||
public enum CutoutResult {
|
||||
case image(UIImage)
|
||||
case pixelBuffer(CVPixelBuffer)
|
||||
}
|
||||
|
||||
public func cutoutImage(from image: UIImage, atPoint point: CGPoint?, asImage: Bool) -> Signal<CutoutResult?, NoError> {
|
||||
if #available(iOS 17.0, *) {
|
||||
guard let cgImage = image.cgImage else {
|
||||
return .single(nil)
|
||||
}
|
||||
return Signal { subscriber in
|
||||
let ciContext = CIContext(options: nil)
|
||||
let inputImage = CIImage(cgImage: cgImage)
|
||||
let handler = VNImageRequestHandler(cgImage: cgImage, options: [:])
|
||||
let request = VNGenerateForegroundInstanceMaskRequest { [weak handler] request, error in
|
||||
guard let handler, let result = request.results?.first as? VNInstanceMaskObservation else {
|
||||
subscriber.putNext(nil)
|
||||
subscriber.putCompletion()
|
||||
return
|
||||
}
|
||||
|
||||
let instances = IndexSet(instances(atPoint: point, inObservation: result).prefix(1))
|
||||
if let mask = try? result.generateScaledMaskForImage(forInstances: instances, from: handler) {
|
||||
if asImage {
|
||||
let filter = CIFilter.blendWithMask()
|
||||
filter.inputImage = inputImage
|
||||
filter.backgroundImage = CIImage(color: .clear)
|
||||
filter.maskImage = CIImage(cvPixelBuffer: mask)
|
||||
if let output = filter.outputImage, let cgImage = ciContext.createCGImage(output, from: inputImage.extent) {
|
||||
let image = UIImage(cgImage: cgImage)
|
||||
subscriber.putNext(.image(image))
|
||||
subscriber.putCompletion()
|
||||
return
|
||||
}
|
||||
} else {
|
||||
let filter = CIFilter.blendWithMask()
|
||||
filter.inputImage = CIImage(color: .white)
|
||||
filter.backgroundImage = CIImage(color: .black)
|
||||
filter.maskImage = CIImage(cvPixelBuffer: mask)
|
||||
if let output = filter.outputImage, let cgImage = ciContext.createCGImage(output, from: inputImage.extent) {
|
||||
let image = UIImage(cgImage: cgImage)
|
||||
subscriber.putNext(.image(image))
|
||||
subscriber.putCompletion()
|
||||
return
|
||||
}
|
||||
// subscriber.putNext(.pixelBuffer(mask))
|
||||
// subscriber.putCompletion()
|
||||
}
|
||||
}
|
||||
subscriber.putNext(nil)
|
||||
subscriber.putCompletion()
|
||||
}
|
||||
|
||||
try? handler.perform([request])
|
||||
return ActionDisposable {
|
||||
request.cancel()
|
||||
}
|
||||
}
|
||||
|> runOn(queue)
|
||||
} else {
|
||||
return .single(nil)
|
||||
}
|
||||
}
|
||||
|
||||
@available(iOS 17.0, *)
|
||||
private func instances(atPoint maybePoint: CGPoint?, inObservation observation: VNInstanceMaskObservation) -> IndexSet {
|
||||
guard let point = maybePoint else {
|
||||
|
@ -190,6 +190,7 @@ public final class MediaEditor {
|
||||
|
||||
public private(set) var canCutout: Bool = false
|
||||
public var canCutoutUpdated: (Bool) -> Void = { _ in }
|
||||
public var isCutoutUpdated: (Bool) -> Void = { _ in }
|
||||
|
||||
private var textureCache: CVMetalTextureCache!
|
||||
|
||||
@ -1682,6 +1683,51 @@ public final class MediaEditor {
|
||||
self.renderer.renderFrame()
|
||||
}
|
||||
|
||||
public func getSeparatedImage(point: CGPoint?) -> Signal<UIImage?, NoError> {
|
||||
guard let textureSource = self.renderer.textureSource as? UniversalTextureSource, let image = textureSource.mainImage else {
|
||||
return .single(nil)
|
||||
}
|
||||
return cutoutImage(from: image, atPoint: point, asImage: true)
|
||||
|> map { result in
|
||||
if let result, case let .image(image) = result {
|
||||
return image
|
||||
} else {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public func removeSeparationMask() {
|
||||
self.isCutoutUpdated(false)
|
||||
|
||||
self.renderer.currentMainInputMask = nil
|
||||
if !self.skipRendering {
|
||||
self.updateRenderChain()
|
||||
}
|
||||
}
|
||||
|
||||
public func setSeparationMask(point: CGPoint?) {
|
||||
guard let renderTarget = self.previewView, let device = renderTarget.mtlDevice else {
|
||||
return
|
||||
}
|
||||
guard let textureSource = self.renderer.textureSource as? UniversalTextureSource, let image = textureSource.mainImage else {
|
||||
return
|
||||
}
|
||||
self.isCutoutUpdated(true)
|
||||
|
||||
let _ = (cutoutImage(from: image, atPoint: point, asImage: false)
|
||||
|> deliverOnMainQueue).start(next: { [weak self] result in
|
||||
guard let self, let result, case let .image(image) = result else {
|
||||
return
|
||||
}
|
||||
//TODO:replace with pixelbuffer
|
||||
self.renderer.currentMainInputMask = loadTexture(image: image, device: device)
|
||||
if !self.skipRendering {
|
||||
self.updateRenderChain()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
private func maybeGeneratePersonSegmentation(_ image: UIImage?) {
|
||||
if #available(iOS 15.0, *), let cgImage = image?.cgImage {
|
||||
let faceRequest = VNDetectFaceRectanglesRequest { [weak self] request, _ in
|
||||
|
@ -98,6 +98,7 @@ final class MediaEditorRenderer {
|
||||
}
|
||||
|
||||
private var currentMainInput: Input?
|
||||
var currentMainInputMask: MTLTexture?
|
||||
private var currentAdditionalInput: Input?
|
||||
private(set) var resultTexture: MTLTexture?
|
||||
|
||||
@ -202,7 +203,7 @@ final class MediaEditorRenderer {
|
||||
}
|
||||
|
||||
if let mainTexture {
|
||||
return self.videoFinishPass.process(input: mainTexture, secondInput: additionalTexture, timestamp: mainInput.timestamp, device: device, commandBuffer: commandBuffer)
|
||||
return self.videoFinishPass.process(input: mainTexture, inputMask: self.currentMainInputMask, secondInput: additionalTexture, timestamp: mainInput.timestamp, device: device, commandBuffer: commandBuffer)
|
||||
} else {
|
||||
return nil
|
||||
}
|
||||
|
@ -42,6 +42,13 @@ final class UniversalTextureSource: TextureSource {
|
||||
)
|
||||
}
|
||||
|
||||
var mainImage: UIImage? {
|
||||
if let mainInput = self.mainInputContext?.input, case let .image(image) = mainInput {
|
||||
return image
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func setMainInput(_ input: Input) {
|
||||
guard let renderTarget = self.renderTarget else {
|
||||
return
|
||||
|
@ -144,6 +144,14 @@ private var transitionDuration = 0.5
|
||||
private var apperanceDuration = 0.2
|
||||
private var videoRemovalDuration: Double = 0.2
|
||||
|
||||
struct VideoEncodeParameters {
|
||||
var dimensions: simd_float2
|
||||
var roundness: simd_float1
|
||||
var alpha: simd_float1
|
||||
var isOpaque: simd_float1
|
||||
var empty: simd_float1
|
||||
}
|
||||
|
||||
final class VideoFinishPass: RenderPass {
|
||||
private var cachedTexture: MTLTexture?
|
||||
|
||||
@ -195,6 +203,7 @@ final class VideoFinishPass: RenderPass {
|
||||
containerSize: CGSize,
|
||||
texture: MTLTexture,
|
||||
textureRotation: TextureRotation,
|
||||
maskTexture: MTLTexture?,
|
||||
position: VideoPosition,
|
||||
roundness: Float,
|
||||
alpha: Float,
|
||||
@ -202,6 +211,11 @@ final class VideoFinishPass: RenderPass {
|
||||
device: MTLDevice
|
||||
) {
|
||||
encoder.setFragmentTexture(texture, index: 0)
|
||||
if let maskTexture {
|
||||
encoder.setFragmentTexture(maskTexture, index: 1)
|
||||
} else {
|
||||
encoder.setFragmentTexture(texture, index: 1)
|
||||
}
|
||||
|
||||
let center = CGPoint(
|
||||
x: position.position.x - containerSize.width / 2.0,
|
||||
@ -220,14 +234,25 @@ final class VideoFinishPass: RenderPass {
|
||||
options: [])
|
||||
encoder.setVertexBuffer(buffer, offset: 0, index: 0)
|
||||
|
||||
var resolution = simd_uint2(UInt32(size.width), UInt32(size.height))
|
||||
encoder.setFragmentBytes(&resolution, length: MemoryLayout<simd_uint2>.size * 2, index: 0)
|
||||
|
||||
var roundness = roundness
|
||||
encoder.setFragmentBytes(&roundness, length: MemoryLayout<simd_float1>.size, index: 1)
|
||||
|
||||
var alpha = alpha
|
||||
encoder.setFragmentBytes(&alpha, length: MemoryLayout<simd_float1>.size, index: 2)
|
||||
var parameters = VideoEncodeParameters(
|
||||
dimensions: simd_float2(Float(size.width), Float(size.height)),
|
||||
roundness: roundness,
|
||||
alpha: alpha,
|
||||
isOpaque: maskTexture == nil ? 1.0 : 0.0,
|
||||
empty: 0
|
||||
)
|
||||
encoder.setFragmentBytes(¶meters, length: MemoryLayout<VideoEncodeParameters>.size, index: 0)
|
||||
// var resolution = simd_uint2(UInt32(size.width), UInt32(size.height))
|
||||
// encoder.setFragmentBytes(&resolution, length: MemoryLayout<simd_uint2>.size * 2, index: 0)
|
||||
//
|
||||
// var roundness = roundness
|
||||
// encoder.setFragmentBytes(&roundness, length: MemoryLayout<simd_float1>.size, index: 1)
|
||||
//
|
||||
// var alpha = alpha
|
||||
// encoder.setFragmentBytes(&alpha, length: MemoryLayout<simd_float1>.size, index: 2)
|
||||
//
|
||||
// var isOpaque = maskTexture == nil ? 1.0 : 0.0
|
||||
// encoder.setFragmentBytes(&isOpaque, length: MemoryLayout<simd_float1>.size, index: 3)
|
||||
|
||||
encoder.drawPrimitives(type: .triangleStrip, vertexStart: 0, vertexCount: 4)
|
||||
}
|
||||
@ -478,7 +503,14 @@ final class VideoFinishPass: RenderPass {
|
||||
return (backgroundVideoState, foregroundVideoState, disappearingVideoState)
|
||||
}
|
||||
|
||||
func process(input: MTLTexture, secondInput: MTLTexture?, timestamp: CMTime, device: MTLDevice, commandBuffer: MTLCommandBuffer) -> MTLTexture? {
|
||||
func process(
|
||||
input: MTLTexture,
|
||||
inputMask: MTLTexture?,
|
||||
secondInput: MTLTexture?,
|
||||
timestamp: CMTime,
|
||||
device: MTLDevice,
|
||||
commandBuffer: MTLCommandBuffer
|
||||
) -> MTLTexture? {
|
||||
if !self.isStory {
|
||||
return input
|
||||
}
|
||||
@ -536,7 +568,6 @@ final class VideoFinishPass: RenderPass {
|
||||
)
|
||||
|
||||
if self.gradientColors.topColor.w > 0.0 {
|
||||
renderCommandEncoder.setRenderPipelineState(self.gradientPipelineState!)
|
||||
self.encodeGradient(
|
||||
using: renderCommandEncoder,
|
||||
containerSize: containerSize,
|
||||
@ -554,6 +585,7 @@ final class VideoFinishPass: RenderPass {
|
||||
containerSize: containerSize,
|
||||
texture: transitionVideoState.texture,
|
||||
textureRotation: transitionVideoState.textureRotation,
|
||||
maskTexture: nil,
|
||||
position: transitionVideoState.position,
|
||||
roundness: transitionVideoState.roundness,
|
||||
alpha: transitionVideoState.alpha,
|
||||
@ -567,6 +599,7 @@ final class VideoFinishPass: RenderPass {
|
||||
containerSize: containerSize,
|
||||
texture: mainVideoState.texture,
|
||||
textureRotation: mainVideoState.textureRotation,
|
||||
maskTexture: inputMask,
|
||||
position: mainVideoState.position,
|
||||
roundness: mainVideoState.roundness,
|
||||
alpha: mainVideoState.alpha,
|
||||
@ -580,6 +613,7 @@ final class VideoFinishPass: RenderPass {
|
||||
containerSize: containerSize,
|
||||
texture: additionalVideoState.texture,
|
||||
textureRotation: additionalVideoState.textureRotation,
|
||||
maskTexture: nil,
|
||||
position: additionalVideoState.position,
|
||||
roundness: additionalVideoState.roundness,
|
||||
alpha: additionalVideoState.alpha,
|
||||
@ -603,6 +637,7 @@ final class VideoFinishPass: RenderPass {
|
||||
containerSize: CGSize,
|
||||
device: MTLDevice
|
||||
) {
|
||||
encoder.setRenderPipelineState(self.gradientPipelineState!)
|
||||
|
||||
let vertices = verticesDataForRotation(.rotate0Degrees)
|
||||
let buffer = device.makeBuffer(
|
||||
|
@ -51,6 +51,7 @@ swift_library(
|
||||
"//submodules/TelegramUI/Components/ContextReferenceButtonComponent",
|
||||
"//submodules/TelegramUI/Components/MediaScrubberComponent",
|
||||
"//submodules/Components/BlurredBackgroundComponent",
|
||||
"//submodules/TelegramUI/Components/DustEffect",
|
||||
],
|
||||
visibility = [
|
||||
"//visibility:public",
|
||||
|
@ -15,6 +15,7 @@ import MediaEditor
|
||||
import Photos
|
||||
import LottieAnimationComponent
|
||||
import MessageInputPanelComponent
|
||||
import DustEffect
|
||||
|
||||
private final class MediaCutoutScreenComponent: Component {
|
||||
typealias EnvironmentType = ViewControllerComponentContainer.Environment
|
||||
@ -40,10 +41,14 @@ private final class MediaCutoutScreenComponent: Component {
|
||||
public final class View: UIView {
|
||||
private let buttonsContainerView = UIView()
|
||||
private let buttonsBackgroundView = UIView()
|
||||
private let previewContainerView = UIView()
|
||||
private let cancelButton = ComponentView<Empty>()
|
||||
private let label = ComponentView<Empty>()
|
||||
private let doneButton = ComponentView<Empty>()
|
||||
|
||||
private let fadeView = UIView()
|
||||
private let separatedImageView = UIImageView()
|
||||
|
||||
private var component: MediaCutoutScreenComponent?
|
||||
private weak var state: State?
|
||||
private var environment: ViewControllerComponentContainer.Environment?
|
||||
@ -51,18 +56,44 @@ private final class MediaCutoutScreenComponent: Component {
|
||||
override init(frame: CGRect) {
|
||||
self.buttonsContainerView.clipsToBounds = true
|
||||
|
||||
self.fadeView.alpha = 0.0
|
||||
self.fadeView.backgroundColor = UIColor(rgb: 0x000000, alpha: 0.6)
|
||||
|
||||
self.separatedImageView.contentMode = .scaleAspectFit
|
||||
|
||||
super.init(frame: frame)
|
||||
|
||||
self.backgroundColor = .clear
|
||||
|
||||
self.addSubview(self.buttonsContainerView)
|
||||
self.buttonsContainerView.addSubview(self.buttonsBackgroundView)
|
||||
|
||||
self.addSubview(self.fadeView)
|
||||
self.addSubview(self.separatedImageView)
|
||||
self.addSubview(self.previewContainerView)
|
||||
|
||||
self.previewContainerView.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.previewTap(_:))))
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
@objc private func previewTap(_ gestureRecognizer: UITapGestureRecognizer) {
|
||||
guard let component = self.component else {
|
||||
return
|
||||
}
|
||||
let location = gestureRecognizer.location(in: gestureRecognizer.view)
|
||||
|
||||
let point = CGPoint(
|
||||
x: location.x / self.previewContainerView.frame.width,
|
||||
y: location.y / self.previewContainerView.frame.height
|
||||
)
|
||||
component.mediaEditor.setSeparationMask(point: point)
|
||||
|
||||
self.playDissolveAnimation()
|
||||
}
|
||||
|
||||
func animateInFromEditor() {
|
||||
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)
|
||||
@ -74,6 +105,7 @@ private final class MediaCutoutScreenComponent: Component {
|
||||
|
||||
self.cancelButton.view?.isHidden = true
|
||||
|
||||
self.fadeView.layer.animateAlpha(from: self.fadeView.alpha, to: 0.0, duration: 0.2, removeOnCompletion: false)
|
||||
self.buttonsBackgroundView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false, completion: { _ in
|
||||
completion()
|
||||
})
|
||||
@ -82,14 +114,35 @@ private final class MediaCutoutScreenComponent: Component {
|
||||
self.state?.updated()
|
||||
}
|
||||
|
||||
public func playDissolveAnimation() {
|
||||
guard let component = self.component, let resultImage = component.mediaEditor.resultImage, let environment = self.environment, let controller = environment.controller() as? MediaCutoutScreen else {
|
||||
return
|
||||
}
|
||||
let previewView = controller.previewView
|
||||
|
||||
let dustEffectLayer = DustEffectLayer()
|
||||
dustEffectLayer.position = previewView.center
|
||||
dustEffectLayer.bounds = previewView.bounds
|
||||
previewView.superview?.layer.insertSublayer(dustEffectLayer, below: previewView.layer)
|
||||
|
||||
dustEffectLayer.animationSpeed = 2.2
|
||||
dustEffectLayer.becameEmpty = { [weak dustEffectLayer] in
|
||||
dustEffectLayer?.removeFromSuperlayer()
|
||||
}
|
||||
|
||||
dustEffectLayer.addItem(frame: previewView.bounds, image: resultImage)
|
||||
|
||||
controller.requestDismiss(animated: true)
|
||||
}
|
||||
|
||||
func update(component: MediaCutoutScreenComponent, availableSize: CGSize, state: State, environment: Environment<ViewControllerComponentContainer.Environment>, transition: Transition) -> CGSize {
|
||||
let environment = environment[ViewControllerComponentContainer.Environment.self].value
|
||||
self.environment = environment
|
||||
|
||||
let isFirstTime = self.component == nil
|
||||
self.component = component
|
||||
self.state = state
|
||||
|
||||
// let presentationData = component.context.sharedContext.currentPresentationData.with { $0 }
|
||||
let isTablet: Bool
|
||||
if case .regular = environment.metrics.widthClass {
|
||||
isTablet = true
|
||||
@ -97,8 +150,6 @@ private final class MediaCutoutScreenComponent: Component {
|
||||
isTablet = false
|
||||
}
|
||||
|
||||
// let mediaEditor = (environment.controller() as? MediaCutoutScreen)?.mediaEditor
|
||||
|
||||
let buttonSideInset: CGFloat
|
||||
let buttonBottomInset: CGFloat = 8.0
|
||||
var controlsBottomInset: CGFloat = 0.0
|
||||
@ -119,7 +170,7 @@ private final class MediaCutoutScreenComponent: Component {
|
||||
}
|
||||
}
|
||||
|
||||
// var previewContainerFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((availableSize.width - previewSize.width) / 2.0), y: environment.safeInsets.top), size: CGSize(width: previewSize.width, height: availableSize.height - environment.safeInsets.top - environment.safeInsets.bottom + controlsBottomInset))
|
||||
let previewContainerFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((availableSize.width - previewSize.width) / 2.0), y: environment.safeInsets.top), size: CGSize(width: previewSize.width, height: availableSize.height - environment.safeInsets.top - environment.safeInsets.bottom + controlsBottomInset))
|
||||
let buttonsContainerFrame = CGRect(origin: CGPoint(x: 0.0, y: availableSize.height - environment.safeInsets.bottom + controlsBottomInset), size: CGSize(width: availableSize.width, height: environment.safeInsets.bottom - controlsBottomInset))
|
||||
|
||||
let cancelButtonSize = self.cancelButton.update(
|
||||
@ -140,7 +191,7 @@ private final class MediaCutoutScreenComponent: Component {
|
||||
guard let controller = environment.controller() as? MediaCutoutScreen else {
|
||||
return
|
||||
}
|
||||
controller.requestDismiss(reset: true, animated: true)
|
||||
controller.requestDismiss(animated: true)
|
||||
}
|
||||
)),
|
||||
environment: {},
|
||||
@ -177,6 +228,30 @@ private final class MediaCutoutScreenComponent: Component {
|
||||
transition.setFrame(view: self.buttonsContainerView, frame: buttonsContainerFrame)
|
||||
transition.setFrame(view: self.buttonsBackgroundView, frame: CGRect(origin: .zero, size: buttonsContainerFrame.size))
|
||||
|
||||
transition.setFrame(view: self.previewContainerView, frame: previewContainerFrame)
|
||||
transition.setFrame(view: self.separatedImageView, frame: previewContainerFrame)
|
||||
|
||||
let frameWidth = floor(previewContainerFrame.width * 0.97)
|
||||
|
||||
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 _ = (component.mediaEditor.getSeparatedImage(point: nil)
|
||||
|> deliverOnMainQueue).start(next: { [weak self] image in
|
||||
guard let self else {
|
||||
return
|
||||
}
|
||||
self.separatedImageView.image = image
|
||||
self.state?.updated(transition: .easeInOut(duration: 0.2))
|
||||
})
|
||||
} else {
|
||||
if let _ = self.separatedImageView.image {
|
||||
transition.setAlpha(view: self.fadeView, alpha: 1.0)
|
||||
} else {
|
||||
transition.setAlpha(view: self.fadeView, alpha: 0.0)
|
||||
}
|
||||
}
|
||||
return availableSize
|
||||
}
|
||||
}
|
||||
@ -315,14 +390,16 @@ public final class MediaCutoutScreen: ViewController {
|
||||
|
||||
fileprivate let context: AccountContext
|
||||
fileprivate let mediaEditor: MediaEditor
|
||||
fileprivate let previewView: MediaEditorPreviewView
|
||||
|
||||
public var dismissed: () -> Void = {}
|
||||
|
||||
private var initialValues: MediaEditorValues
|
||||
|
||||
public init(context: AccountContext, mediaEditor: MediaEditor) {
|
||||
public init(context: AccountContext, mediaEditor: MediaEditor, previewView: MediaEditorPreviewView) {
|
||||
self.context = context
|
||||
self.mediaEditor = mediaEditor
|
||||
self.previewView = previewView
|
||||
self.initialValues = mediaEditor.values.makeCopy()
|
||||
|
||||
super.init(navigationBarPresentationData: nil)
|
||||
@ -343,11 +420,7 @@ public final class MediaCutoutScreen: ViewController {
|
||||
super.displayNodeDidLoad()
|
||||
}
|
||||
|
||||
func requestDismiss(reset: Bool, animated: Bool) {
|
||||
if reset {
|
||||
self.mediaEditor.values = self.initialValues
|
||||
}
|
||||
|
||||
func requestDismiss(animated: Bool) {
|
||||
self.dismissed()
|
||||
|
||||
self.node.animateOutToEditor(completion: {
|
||||
|
@ -154,6 +154,7 @@ final class MediaEditorScreenComponent: Component {
|
||||
case tools
|
||||
case done
|
||||
case cutout
|
||||
case undo
|
||||
}
|
||||
private var cachedImages: [ImageKey: UIImage] = [:]
|
||||
func image(_ key: ImageKey) -> UIImage {
|
||||
@ -172,6 +173,8 @@ final class MediaEditorScreenComponent: Component {
|
||||
image = generateTintedImage(image: UIImage(bundleImageName: "Media Editor/Tools"), color: .white)!
|
||||
case .cutout:
|
||||
image = generateTintedImage(image: UIImage(bundleImageName: "Media Editor/Cutout"), color: .white)!
|
||||
case .undo:
|
||||
image = generateTintedImage(image: UIImage(bundleImageName: "Media Editor/CutoutUndo"), color: .white)!
|
||||
case .done:
|
||||
image = generateImage(CGSize(width: 33.0, height: 33.0), rotatedContext: { size, context in
|
||||
context.clear(CGRect(origin: CGPoint(), size: size))
|
||||
@ -981,13 +984,14 @@ final class MediaEditorScreenComponent: Component {
|
||||
}
|
||||
|
||||
if controller.node.canCutout {
|
||||
let isCutout = controller.node.isCutout
|
||||
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"
|
||||
icon: state.image(isCutout ? .undo : .cutout),
|
||||
title: isCutout ? "Undo Cut Out" : "Cut Out an Object"
|
||||
)),
|
||||
effectAlignment: .center,
|
||||
action: {
|
||||
@ -2161,6 +2165,7 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate
|
||||
private var isDismissBySwipeSuppressed = false
|
||||
|
||||
fileprivate var canCutout = false
|
||||
fileprivate var isCutout = false
|
||||
|
||||
private (set) var hasAnyChanges = false
|
||||
|
||||
@ -2513,7 +2518,13 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate
|
||||
self.canCutout = canCutout
|
||||
controller.requestLayout(transition: .animated(duration: 0.25, curve: .easeInOut))
|
||||
}
|
||||
|
||||
mediaEditor.isCutoutUpdated = { [weak self] isCutout in
|
||||
guard let self else {
|
||||
return
|
||||
}
|
||||
self.isCutout = isCutout
|
||||
self.requestLayout(forceUpdate: true, transition: .immediate)
|
||||
}
|
||||
|
||||
if case .message = effectiveSubject {
|
||||
} else {
|
||||
@ -4231,7 +4242,17 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate
|
||||
self.entitiesView.selectEntity(nil)
|
||||
}
|
||||
|
||||
let controller = MediaCutoutScreen(context: self.context, mediaEditor: mediaEditor)
|
||||
if controller.node.isCutout {
|
||||
let snapshotView = self.previewView.snapshotView(afterScreenUpdates: false)
|
||||
if let snapshotView {
|
||||
self.previewView.superview?.addSubview(snapshotView)
|
||||
}
|
||||
self.previewView.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.3, completion: { _ in
|
||||
snapshotView?.removeFromSuperview()
|
||||
})
|
||||
mediaEditor.removeSeparationMask()
|
||||
} else {
|
||||
let controller = MediaCutoutScreen(context: self.context, mediaEditor: mediaEditor, previewView: self.previewView)
|
||||
controller.dismissed = { [weak self] in
|
||||
if let self {
|
||||
self.animateInFromTool(inPlace: true)
|
||||
@ -4241,6 +4262,7 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate
|
||||
self.animateOutToTool(inPlace: true)
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
),
|
||||
environment: {
|
||||
@ -5084,8 +5106,11 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate
|
||||
}
|
||||
|
||||
let presentationData = self.context.sharedContext.currentPresentationData.with { $0 }
|
||||
let title: String
|
||||
let save: String
|
||||
var title: String
|
||||
var text: String
|
||||
var save: String?
|
||||
switch self.mode {
|
||||
case .storyEditor:
|
||||
if case .draft = self.node.actualSubject {
|
||||
title = presentationData.strings.Story_Editor_DraftDiscardDraft
|
||||
save = presentationData.strings.Story_Editor_DraftKeepDraft
|
||||
@ -5093,26 +5118,34 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate
|
||||
title = presentationData.strings.Story_Editor_DraftDiscardMedia
|
||||
save = presentationData.strings.Story_Editor_DraftKeepMedia
|
||||
}
|
||||
text = presentationData.strings.Story_Editor_DraftDiscaedText
|
||||
case .stickerEditor:
|
||||
title = presentationData.strings.Story_Editor_DraftDiscardMedia
|
||||
text = presentationData.strings.Story_Editor_DiscardText
|
||||
}
|
||||
|
||||
var actions: [TextAlertAction] = []
|
||||
actions.append(TextAlertAction(type: .destructiveAction, title: presentationData.strings.Story_Editor_DraftDiscard, action: { [weak self] in
|
||||
if let self {
|
||||
self.requestDismiss(saveDraft: false, animated: true)
|
||||
}
|
||||
}))
|
||||
if let save {
|
||||
actions.append(TextAlertAction(type: .genericAction, title: save, action: { [weak self] in
|
||||
if let self {
|
||||
self.requestDismiss(saveDraft: true, animated: true)
|
||||
}
|
||||
}))
|
||||
}
|
||||
actions.append(TextAlertAction(type: .genericAction, title: presentationData.strings.Common_Cancel, action: {
|
||||
|
||||
}))
|
||||
let controller = textAlertController(
|
||||
context: self.context,
|
||||
forceTheme: defaultDarkPresentationTheme,
|
||||
title: title,
|
||||
text: presentationData.strings.Story_Editor_DraftDiscaedText,
|
||||
actions: [
|
||||
TextAlertAction(type: .destructiveAction, title: presentationData.strings.Story_Editor_DraftDiscard, action: { [weak self] in
|
||||
if let self {
|
||||
self.requestDismiss(saveDraft: false, animated: true)
|
||||
}
|
||||
}),
|
||||
TextAlertAction(type: .genericAction, title: save, action: { [weak self] in
|
||||
if let self {
|
||||
self.requestDismiss(saveDraft: true, animated: true)
|
||||
}
|
||||
}),
|
||||
TextAlertAction(type: .genericAction, title: presentationData.strings.Common_Cancel, action: {
|
||||
|
||||
})
|
||||
],
|
||||
text: text,
|
||||
actions: actions,
|
||||
actionLayout: .vertical
|
||||
)
|
||||
self.present(controller, in: .window(.root))
|
||||
|
12
submodules/TelegramUI/Images.xcassets/Media Editor/CutoutUndo.imageset/Contents.json
vendored
Normal file
12
submodules/TelegramUI/Images.xcassets/Media Editor/CutoutUndo.imageset/Contents.json
vendored
Normal file
@ -0,0 +1,12 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"filename" : "undo2_30.pdf",
|
||||
"idiom" : "universal"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
126
submodules/TelegramUI/Images.xcassets/Media Editor/CutoutUndo.imageset/undo2_30.pdf
vendored
Normal file
126
submodules/TelegramUI/Images.xcassets/Media Editor/CutoutUndo.imageset/undo2_30.pdf
vendored
Normal file
@ -0,0 +1,126 @@
|
||||
%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 5.000000 4.258789 cm
|
||||
0.000000 0.000000 0.000000 scn
|
||||
1.000000 2.571211 m
|
||||
0.541604 2.571211 0.170000 2.199607 0.170000 1.741211 c
|
||||
0.170000 1.282815 0.541604 0.911211 1.000000 0.911211 c
|
||||
1.000000 2.571211 l
|
||||
h
|
||||
1.000000 13.571211 m
|
||||
0.541604 13.571211 0.170000 13.199608 0.170000 12.741211 c
|
||||
0.170000 12.282814 0.541604 11.911211 1.000000 11.911211 c
|
||||
1.000000 13.571211 l
|
||||
h
|
||||
3.413101 8.154312 m
|
||||
3.737236 7.830177 4.262764 7.830177 4.586899 8.154312 c
|
||||
4.911034 8.478448 4.911034 9.003975 4.586899 9.328110 c
|
||||
3.413101 8.154312 l
|
||||
h
|
||||
0.000000 12.741211 m
|
||||
-0.586899 13.328110 l
|
||||
-0.911034 13.003975 -0.911034 12.478447 -0.586899 12.154312 c
|
||||
0.000000 12.741211 l
|
||||
h
|
||||
4.586899 16.154312 m
|
||||
4.911034 16.478447 4.911034 17.003975 4.586899 17.328110 c
|
||||
4.262764 17.652245 3.737236 17.652245 3.413101 17.328110 c
|
||||
4.586899 16.154312 l
|
||||
h
|
||||
1.000000 0.911211 m
|
||||
8.500000 0.911211 l
|
||||
8.500000 2.571211 l
|
||||
1.000000 2.571211 l
|
||||
1.000000 0.911211 l
|
||||
h
|
||||
8.500000 13.571211 m
|
||||
1.000000 13.571211 l
|
||||
1.000000 11.911211 l
|
||||
8.500000 11.911211 l
|
||||
8.500000 13.571211 l
|
||||
h
|
||||
4.586899 9.328110 m
|
||||
0.586899 13.328110 l
|
||||
-0.586899 12.154312 l
|
||||
3.413101 8.154312 l
|
||||
4.586899 9.328110 l
|
||||
h
|
||||
0.586899 12.154312 m
|
||||
4.586899 16.154312 l
|
||||
3.413101 17.328110 l
|
||||
-0.586899 13.328110 l
|
||||
0.586899 12.154312 l
|
||||
h
|
||||
14.830000 7.241211 m
|
||||
14.830000 10.737173 11.995962 13.571211 8.500000 13.571211 c
|
||||
8.500000 11.911211 l
|
||||
11.079169 11.911211 13.170000 9.820381 13.170000 7.241211 c
|
||||
14.830000 7.241211 l
|
||||
h
|
||||
8.500000 0.911211 m
|
||||
11.995962 0.911211 14.830000 3.745249 14.830000 7.241211 c
|
||||
13.170000 7.241211 l
|
||||
13.170000 4.662042 11.079169 2.571211 8.500000 2.571211 c
|
||||
8.500000 0.911211 l
|
||||
h
|
||||
f
|
||||
n
|
||||
Q
|
||||
|
||||
endstream
|
||||
endobj
|
||||
|
||||
3 0 obj
|
||||
1673
|
||||
endobj
|
||||
|
||||
4 0 obj
|
||||
<< /Annots []
|
||||
/Type /Page
|
||||
/MediaBox [ 0.000000 0.000000 24.000000 24.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
|
||||
0000001763 00000 n
|
||||
0000001786 00000 n
|
||||
0000001959 00000 n
|
||||
0000002033 00000 n
|
||||
trailer
|
||||
<< /ID [ (some) (id) ]
|
||||
/Root 6 0 R
|
||||
/Size 7
|
||||
>>
|
||||
startxref
|
||||
2092
|
||||
%%EOF
|
Loading…
x
Reference in New Issue
Block a user