mirror of
https://github.com/Swiftgram/Telegram-iOS.git
synced 2025-06-16 05:55:20 +00:00
Camera and editor improvements
This commit is contained in:
parent
37e7a74e4e
commit
a2d7cfba4f
@ -13,6 +13,7 @@ import BlurredBackgroundComponent
|
|||||||
import SegmentedControlNode
|
import SegmentedControlNode
|
||||||
import MultilineTextComponent
|
import MultilineTextComponent
|
||||||
import HexColor
|
import HexColor
|
||||||
|
import MediaEditor
|
||||||
|
|
||||||
private let palleteColors: [UInt32] = [
|
private let palleteColors: [UInt32] = [
|
||||||
0xffffff, 0xebebeb, 0xd6d6d6, 0xc2c2c2, 0xadadad, 0x999999, 0x858585, 0x707070, 0x5c5c5c, 0x474747, 0x333333, 0x000000,
|
0xffffff, 0xebebeb, 0xd6d6d6, 0xc2c2c2, 0xadadad, 0x999999, 0x858585, 0x707070, 0x5c5c5c, 0x474747, 0x333333, 0x000000,
|
||||||
|
@ -2,119 +2,7 @@ import Foundation
|
|||||||
import UIKit
|
import UIKit
|
||||||
import Display
|
import Display
|
||||||
import AccountContext
|
import AccountContext
|
||||||
|
import MediaEditor
|
||||||
public final class DrawingBubbleEntity: DrawingEntity, Codable {
|
|
||||||
private enum CodingKeys: String, CodingKey {
|
|
||||||
case uuid
|
|
||||||
case drawType
|
|
||||||
case color
|
|
||||||
case lineWidth
|
|
||||||
case referenceDrawingSize
|
|
||||||
case position
|
|
||||||
case size
|
|
||||||
case rotation
|
|
||||||
case tailPosition
|
|
||||||
case renderImage
|
|
||||||
}
|
|
||||||
|
|
||||||
enum DrawType: Codable {
|
|
||||||
case fill
|
|
||||||
case stroke
|
|
||||||
}
|
|
||||||
|
|
||||||
public let uuid: UUID
|
|
||||||
public let isAnimated: Bool
|
|
||||||
|
|
||||||
var drawType: DrawType
|
|
||||||
public var color: DrawingColor
|
|
||||||
public var lineWidth: CGFloat
|
|
||||||
|
|
||||||
var referenceDrawingSize: CGSize
|
|
||||||
public var position: CGPoint
|
|
||||||
public var size: CGSize
|
|
||||||
public var rotation: CGFloat
|
|
||||||
var tailPosition: CGPoint
|
|
||||||
|
|
||||||
public var center: CGPoint {
|
|
||||||
return self.position
|
|
||||||
}
|
|
||||||
|
|
||||||
public var scale: CGFloat = 1.0
|
|
||||||
|
|
||||||
public var renderImage: UIImage?
|
|
||||||
|
|
||||||
public var isMedia: Bool {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
init(drawType: DrawType, color: DrawingColor, lineWidth: CGFloat) {
|
|
||||||
self.uuid = UUID()
|
|
||||||
self.isAnimated = false
|
|
||||||
|
|
||||||
self.drawType = drawType
|
|
||||||
self.color = color
|
|
||||||
self.lineWidth = lineWidth
|
|
||||||
|
|
||||||
self.referenceDrawingSize = .zero
|
|
||||||
self.position = .zero
|
|
||||||
self.size = CGSize(width: 1.0, height: 1.0)
|
|
||||||
self.rotation = 0.0
|
|
||||||
self.tailPosition = CGPoint(x: 0.16, y: 0.18)
|
|
||||||
}
|
|
||||||
|
|
||||||
public init(from decoder: Decoder) throws {
|
|
||||||
let container = try decoder.container(keyedBy: CodingKeys.self)
|
|
||||||
self.uuid = try container.decode(UUID.self, forKey: .uuid)
|
|
||||||
self.isAnimated = false
|
|
||||||
self.drawType = try container.decode(DrawType.self, forKey: .drawType)
|
|
||||||
self.color = try container.decode(DrawingColor.self, forKey: .color)
|
|
||||||
self.lineWidth = try container.decode(CGFloat.self, forKey: .lineWidth)
|
|
||||||
self.referenceDrawingSize = try container.decode(CGSize.self, forKey: .referenceDrawingSize)
|
|
||||||
self.position = try container.decode(CGPoint.self, forKey: .position)
|
|
||||||
self.size = try container.decode(CGSize.self, forKey: .size)
|
|
||||||
self.rotation = try container.decode(CGFloat.self, forKey: .rotation)
|
|
||||||
self.tailPosition = try container.decode(CGPoint.self, forKey: .tailPosition)
|
|
||||||
if let renderImageData = try? container.decodeIfPresent(Data.self, forKey: .renderImage) {
|
|
||||||
self.renderImage = UIImage(data: renderImageData)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public func encode(to encoder: Encoder) throws {
|
|
||||||
var container = encoder.container(keyedBy: CodingKeys.self)
|
|
||||||
try container.encode(self.uuid, forKey: .uuid)
|
|
||||||
try container.encode(self.drawType, forKey: .drawType)
|
|
||||||
try container.encode(self.color, forKey: .color)
|
|
||||||
try container.encode(self.lineWidth, forKey: .lineWidth)
|
|
||||||
try container.encode(self.referenceDrawingSize, forKey: .referenceDrawingSize)
|
|
||||||
try container.encode(self.position, forKey: .position)
|
|
||||||
try container.encode(self.size, forKey: .size)
|
|
||||||
try container.encode(self.rotation, forKey: .rotation)
|
|
||||||
try container.encode(self.tailPosition, forKey: .tailPosition)
|
|
||||||
if let renderImage, let data = renderImage.pngData() {
|
|
||||||
try container.encode(data, forKey: .renderImage)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public func duplicate() -> DrawingEntity {
|
|
||||||
let newEntity = DrawingBubbleEntity(drawType: self.drawType, color: self.color, lineWidth: self.lineWidth)
|
|
||||||
newEntity.referenceDrawingSize = self.referenceDrawingSize
|
|
||||||
newEntity.position = self.position
|
|
||||||
newEntity.size = self.size
|
|
||||||
newEntity.rotation = self.rotation
|
|
||||||
return newEntity
|
|
||||||
}
|
|
||||||
|
|
||||||
public weak var currentEntityView: DrawingEntityView?
|
|
||||||
public func makeView(context: AccountContext) -> DrawingEntityView {
|
|
||||||
let entityView = DrawingBubbleEntityView(context: context, entity: self)
|
|
||||||
self.currentEntityView = entityView
|
|
||||||
return entityView
|
|
||||||
}
|
|
||||||
|
|
||||||
public func prepareForRender() {
|
|
||||||
self.renderImage = (self.currentEntityView as? DrawingBubbleEntityView)?.getRenderImage()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
final class DrawingBubbleEntityView: DrawingEntityView {
|
final class DrawingBubbleEntityView: DrawingEntityView {
|
||||||
private var bubbleEntity: DrawingBubbleEntity {
|
private var bubbleEntity: DrawingBubbleEntity {
|
||||||
|
@ -3,66 +3,7 @@ import UIKit
|
|||||||
import Display
|
import Display
|
||||||
import LegacyComponents
|
import LegacyComponents
|
||||||
import AccountContext
|
import AccountContext
|
||||||
|
import MediaEditor
|
||||||
|
|
||||||
public protocol DrawingEntity: AnyObject {
|
|
||||||
var uuid: UUID { get }
|
|
||||||
var isAnimated: Bool { get }
|
|
||||||
var center: CGPoint { get }
|
|
||||||
|
|
||||||
var isMedia: Bool { get }
|
|
||||||
|
|
||||||
var lineWidth: CGFloat { get set }
|
|
||||||
var color: DrawingColor { get set }
|
|
||||||
|
|
||||||
var scale: CGFloat { get set }
|
|
||||||
|
|
||||||
func duplicate() -> DrawingEntity
|
|
||||||
|
|
||||||
var currentEntityView: DrawingEntityView? { get }
|
|
||||||
func makeView(context: AccountContext) -> DrawingEntityView
|
|
||||||
|
|
||||||
func prepareForRender()
|
|
||||||
}
|
|
||||||
|
|
||||||
enum CodableDrawingEntity {
|
|
||||||
case sticker(DrawingStickerEntity)
|
|
||||||
case text(DrawingTextEntity)
|
|
||||||
case simpleShape(DrawingSimpleShapeEntity)
|
|
||||||
case bubble(DrawingBubbleEntity)
|
|
||||||
case vector(DrawingVectorEntity)
|
|
||||||
|
|
||||||
init?(entity: DrawingEntity) {
|
|
||||||
if let entity = entity as? DrawingStickerEntity {
|
|
||||||
self = .sticker(entity)
|
|
||||||
} else if let entity = entity as? DrawingTextEntity {
|
|
||||||
self = .text(entity)
|
|
||||||
} else if let entity = entity as? DrawingSimpleShapeEntity {
|
|
||||||
self = .simpleShape(entity)
|
|
||||||
} else if let entity = entity as? DrawingBubbleEntity {
|
|
||||||
self = .bubble(entity)
|
|
||||||
} else if let entity = entity as? DrawingVectorEntity {
|
|
||||||
self = .vector(entity)
|
|
||||||
} else {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
var entity: DrawingEntity {
|
|
||||||
switch self {
|
|
||||||
case let .sticker(entity):
|
|
||||||
return entity
|
|
||||||
case let .text(entity):
|
|
||||||
return entity
|
|
||||||
case let .simpleShape(entity):
|
|
||||||
return entity
|
|
||||||
case let .bubble(entity):
|
|
||||||
return entity
|
|
||||||
case let .vector(entity):
|
|
||||||
return entity
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public func decodeDrawingEntities(data: Data) -> [DrawingEntity] {
|
public func decodeDrawingEntities(data: Data) -> [DrawingEntity] {
|
||||||
if let codableEntities = try? JSONDecoder().decode([CodableDrawingEntity].self, from: data) {
|
if let codableEntities = try? JSONDecoder().decode([CodableDrawingEntity].self, from: data) {
|
||||||
@ -71,56 +12,37 @@ public func decodeDrawingEntities(data: Data) -> [DrawingEntity] {
|
|||||||
return []
|
return []
|
||||||
}
|
}
|
||||||
|
|
||||||
extension CodableDrawingEntity: Codable {
|
private func makeEntityView(context: AccountContext, entity: DrawingEntity) -> DrawingEntityView? {
|
||||||
private enum CodingKeys: String, CodingKey {
|
if let entity = entity as? DrawingBubbleEntity {
|
||||||
case type
|
return DrawingBubbleEntityView(context: context, entity: entity)
|
||||||
case entity
|
} else if let entity = entity as? DrawingSimpleShapeEntity {
|
||||||
|
return DrawingSimpleShapeEntityView(context: context, entity: entity)
|
||||||
|
} else if let entity = entity as? DrawingStickerEntity {
|
||||||
|
return DrawingStickerEntityView(context: context, entity: entity)
|
||||||
|
} else if let entity = entity as? DrawingTextEntity {
|
||||||
|
return DrawingTextEntityView(context: context, entity: entity)
|
||||||
|
} else if let entity = entity as? DrawingVectorEntity {
|
||||||
|
return DrawingVectorEntityView(context: context, entity: entity)
|
||||||
|
} else if let entity = entity as? DrawingMediaEntity {
|
||||||
|
return DrawingMediaEntityView(context: context, entity: entity)
|
||||||
|
} else {
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private enum EntityType: Int, Codable {
|
private func prepareForRendering(entityView: DrawingEntityView) {
|
||||||
case sticker
|
if let entityView = entityView as? DrawingBubbleEntityView {
|
||||||
case text
|
entityView.entity.renderImage = entityView.getRenderImage()
|
||||||
case simpleShape
|
|
||||||
case bubble
|
|
||||||
case vector
|
|
||||||
}
|
}
|
||||||
|
if let entityView = entityView as? DrawingSimpleShapeEntityView {
|
||||||
init(from decoder: Decoder) throws {
|
entityView.entity.renderImage = entityView.getRenderImage()
|
||||||
let container = try decoder.container(keyedBy: CodingKeys.self)
|
|
||||||
let type = try container.decode(EntityType.self, forKey: .type)
|
|
||||||
switch type {
|
|
||||||
case .sticker:
|
|
||||||
self = .sticker(try container.decode(DrawingStickerEntity.self, forKey: .entity))
|
|
||||||
case .text:
|
|
||||||
self = .text(try container.decode(DrawingTextEntity.self, forKey: .entity))
|
|
||||||
case .simpleShape:
|
|
||||||
self = .simpleShape(try container.decode(DrawingSimpleShapeEntity.self, forKey: .entity))
|
|
||||||
case .bubble:
|
|
||||||
self = .bubble(try container.decode(DrawingBubbleEntity.self, forKey: .entity))
|
|
||||||
case .vector:
|
|
||||||
self = .vector(try container.decode(DrawingVectorEntity.self, forKey: .entity))
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
if let entityView = entityView as? DrawingTextEntityView {
|
||||||
func encode(to encoder: Encoder) throws {
|
entityView.entity.renderImage = entityView.getRenderImage()
|
||||||
var container = encoder.container(keyedBy: CodingKeys.self)
|
entityView.entity.renderSubEntities = entityView.getRenderSubEntities()
|
||||||
switch self {
|
}
|
||||||
case let .sticker(payload):
|
if let entityView = entityView as? DrawingVectorEntityView {
|
||||||
try container.encode(EntityType.sticker, forKey: .type)
|
entityView.entity.renderImage = entityView.getRenderImage()
|
||||||
try container.encode(payload, forKey: .entity)
|
|
||||||
case let .text(payload):
|
|
||||||
try container.encode(EntityType.text, forKey: .type)
|
|
||||||
try container.encode(payload, forKey: .entity)
|
|
||||||
case let .simpleShape(payload):
|
|
||||||
try container.encode(EntityType.simpleShape, forKey: .type)
|
|
||||||
try container.encode(payload, forKey: .entity)
|
|
||||||
case let .bubble(payload):
|
|
||||||
try container.encode(EntityType.bubble, forKey: .type)
|
|
||||||
try container.encode(payload, forKey: .entity)
|
|
||||||
case let .vector(payload):
|
|
||||||
try container.encode(EntityType.vector, forKey: .type)
|
|
||||||
try container.encode(payload, forKey: .entity)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -227,13 +149,17 @@ public final class DrawingEntitiesView: UIView, TGPhotoDrawingEntitiesView {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public static func encodeEntities(_ entities: [DrawingEntity]) -> Data? {
|
public static func encodeEntities(_ entities: [DrawingEntity], entitiesView: DrawingEntitiesView? = nil) -> Data? {
|
||||||
let entities = entities
|
let entities = entities
|
||||||
guard !entities.isEmpty else {
|
guard !entities.isEmpty else {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
for entity in entities {
|
if let entitiesView {
|
||||||
entity.prepareForRender()
|
for entity in entities {
|
||||||
|
if let entityView = entitiesView.getView(for: entity.uuid) {
|
||||||
|
prepareForRendering(entityView: entityView)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
let codableEntities = entities.compactMap({ CodableDrawingEntity(entity: $0) })
|
let codableEntities = entities.compactMap({ CodableDrawingEntity(entity: $0) })
|
||||||
if let data = try? JSONEncoder().encode(codableEntities) {
|
if let data = try? JSONEncoder().encode(codableEntities) {
|
||||||
@ -244,7 +170,7 @@ public final class DrawingEntitiesView: UIView, TGPhotoDrawingEntitiesView {
|
|||||||
}
|
}
|
||||||
|
|
||||||
var entitiesData: Data? {
|
var entitiesData: Data? {
|
||||||
return DrawingEntitiesView.encodeEntities(self.entities)
|
return DrawingEntitiesView.encodeEntities(self.entities, entitiesView: self)
|
||||||
}
|
}
|
||||||
|
|
||||||
var hasChanges: Bool {
|
var hasChanges: Bool {
|
||||||
@ -351,7 +277,9 @@ public final class DrawingEntitiesView: UIView, TGPhotoDrawingEntitiesView {
|
|||||||
|
|
||||||
@discardableResult
|
@discardableResult
|
||||||
public func add(_ entity: DrawingEntity, announce: Bool = true) -> DrawingEntityView {
|
public func add(_ entity: DrawingEntity, announce: Bool = true) -> DrawingEntityView {
|
||||||
let view = entity.makeView(context: self.context)
|
guard let view = makeEntityView(context: self.context, entity: entity) else {
|
||||||
|
fatalError()
|
||||||
|
}
|
||||||
view.containerView = self
|
view.containerView = self
|
||||||
|
|
||||||
view.onSnapToXAxis = { [weak self, weak view] snappedToX in
|
view.onSnapToXAxis = { [weak self, weak view] snappedToX in
|
||||||
@ -420,7 +348,9 @@ public final class DrawingEntitiesView: UIView, TGPhotoDrawingEntitiesView {
|
|||||||
let newEntity = entity.duplicate()
|
let newEntity = entity.duplicate()
|
||||||
self.prepareNewEntity(newEntity, setup: false, relativeTo: entity)
|
self.prepareNewEntity(newEntity, setup: false, relativeTo: entity)
|
||||||
|
|
||||||
let view = newEntity.makeView(context: self.context)
|
guard let view = makeEntityView(context: self.context, entity: entity) else {
|
||||||
|
fatalError()
|
||||||
|
}
|
||||||
view.containerView = self
|
view.containerView = self
|
||||||
view.update()
|
view.update()
|
||||||
self.addSubview(view)
|
self.addSubview(view)
|
||||||
|
@ -7,152 +7,6 @@ import AccountContext
|
|||||||
import MediaEditor
|
import MediaEditor
|
||||||
import Photos
|
import Photos
|
||||||
|
|
||||||
public final class DrawingMediaEntity: DrawingEntity, Codable {
|
|
||||||
public enum Content {
|
|
||||||
case image(UIImage, PixelDimensions)
|
|
||||||
case video(String, PixelDimensions)
|
|
||||||
case asset(PHAsset)
|
|
||||||
|
|
||||||
var dimensions: PixelDimensions {
|
|
||||||
switch self {
|
|
||||||
case let .image(_, dimensions), let .video(_, dimensions):
|
|
||||||
return dimensions
|
|
||||||
case let .asset(asset):
|
|
||||||
return PixelDimensions(width: Int32(asset.pixelWidth), height: Int32(asset.pixelHeight))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private enum CodingKeys: String, CodingKey {
|
|
||||||
case uuid
|
|
||||||
case image
|
|
||||||
case videoPath
|
|
||||||
case assetId
|
|
||||||
case size
|
|
||||||
case width
|
|
||||||
case height
|
|
||||||
case referenceDrawingSize
|
|
||||||
case position
|
|
||||||
case scale
|
|
||||||
case rotation
|
|
||||||
case mirrored
|
|
||||||
}
|
|
||||||
|
|
||||||
public let uuid: UUID
|
|
||||||
public let content: Content
|
|
||||||
public let size: CGSize
|
|
||||||
|
|
||||||
public var referenceDrawingSize: CGSize
|
|
||||||
public var position: CGPoint
|
|
||||||
public var scale: CGFloat
|
|
||||||
public var rotation: CGFloat
|
|
||||||
public var mirrored: Bool
|
|
||||||
|
|
||||||
public var color: DrawingColor = DrawingColor.clear
|
|
||||||
public var lineWidth: CGFloat = 0.0
|
|
||||||
|
|
||||||
public var center: CGPoint {
|
|
||||||
return self.position
|
|
||||||
}
|
|
||||||
|
|
||||||
public var baseSize: CGSize {
|
|
||||||
return self.size
|
|
||||||
}
|
|
||||||
|
|
||||||
public var isAnimated: Bool {
|
|
||||||
switch self.content {
|
|
||||||
case .image:
|
|
||||||
return false
|
|
||||||
case .video:
|
|
||||||
return true
|
|
||||||
case let .asset(asset):
|
|
||||||
return asset.mediaType == .video
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public var isMedia: Bool {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
public init(content: Content, size: CGSize) {
|
|
||||||
self.uuid = UUID()
|
|
||||||
self.content = content
|
|
||||||
self.size = size
|
|
||||||
|
|
||||||
self.referenceDrawingSize = .zero
|
|
||||||
self.position = CGPoint()
|
|
||||||
self.scale = 1.0
|
|
||||||
self.rotation = 0.0
|
|
||||||
self.mirrored = false
|
|
||||||
}
|
|
||||||
|
|
||||||
public init(from decoder: Decoder) throws {
|
|
||||||
let container = try decoder.container(keyedBy: CodingKeys.self)
|
|
||||||
self.uuid = try container.decode(UUID.self, forKey: .uuid)
|
|
||||||
self.size = try container.decode(CGSize.self, forKey: .size)
|
|
||||||
let width = try container.decode(Int32.self, forKey: .width)
|
|
||||||
let height = try container.decode(Int32.self, forKey: .height)
|
|
||||||
if let videoPath = try container.decodeIfPresent(String.self, forKey: .videoPath) {
|
|
||||||
self.content = .video(videoPath, PixelDimensions(width: width, height: height))
|
|
||||||
} else if let imageData = try container.decodeIfPresent(Data.self, forKey: .image), let image = UIImage(data: imageData) {
|
|
||||||
self.content = .image(image, PixelDimensions(width: width, height: height))
|
|
||||||
} else if let _ = try container.decodeIfPresent(String.self, forKey: .assetId) {
|
|
||||||
fatalError()
|
|
||||||
//self.content = .asset()
|
|
||||||
} else {
|
|
||||||
fatalError()
|
|
||||||
}
|
|
||||||
self.referenceDrawingSize = try container.decode(CGSize.self, forKey: .referenceDrawingSize)
|
|
||||||
self.position = try container.decode(CGPoint.self, forKey: .position)
|
|
||||||
self.scale = try container.decode(CGFloat.self, forKey: .scale)
|
|
||||||
self.rotation = try container.decode(CGFloat.self, forKey: .rotation)
|
|
||||||
self.mirrored = try container.decode(Bool.self, forKey: .mirrored)
|
|
||||||
}
|
|
||||||
|
|
||||||
public func encode(to encoder: Encoder) throws {
|
|
||||||
var container = encoder.container(keyedBy: CodingKeys.self)
|
|
||||||
try container.encode(self.uuid, forKey: .uuid)
|
|
||||||
switch self.content {
|
|
||||||
case let .video(videoPath, dimensions):
|
|
||||||
try container.encode(videoPath, forKey: .videoPath)
|
|
||||||
try container.encode(dimensions.width, forKey: .width)
|
|
||||||
try container.encode(dimensions.height, forKey: .height)
|
|
||||||
case let .image(image, dimensions):
|
|
||||||
try container.encodeIfPresent(image.jpegData(compressionQuality: 0.9), forKey: .image)
|
|
||||||
try container.encode(dimensions.width, forKey: .width)
|
|
||||||
try container.encode(dimensions.height, forKey: .height)
|
|
||||||
case let .asset(asset):
|
|
||||||
try container.encode(asset.localIdentifier, forKey: .assetId)
|
|
||||||
}
|
|
||||||
try container.encode(self.size, forKey: .size)
|
|
||||||
try container.encode(self.referenceDrawingSize, forKey: .referenceDrawingSize)
|
|
||||||
try container.encode(self.position, forKey: .position)
|
|
||||||
try container.encode(self.scale, forKey: .scale)
|
|
||||||
try container.encode(self.rotation, forKey: .rotation)
|
|
||||||
try container.encode(self.mirrored, forKey: .mirrored)
|
|
||||||
}
|
|
||||||
|
|
||||||
public func duplicate() -> DrawingEntity {
|
|
||||||
let newEntity = DrawingMediaEntity(content: self.content, size: self.size)
|
|
||||||
newEntity.referenceDrawingSize = self.referenceDrawingSize
|
|
||||||
newEntity.position = self.position
|
|
||||||
newEntity.scale = self.scale
|
|
||||||
newEntity.rotation = self.rotation
|
|
||||||
newEntity.mirrored = self.mirrored
|
|
||||||
return newEntity
|
|
||||||
}
|
|
||||||
|
|
||||||
public weak var currentEntityView: DrawingEntityView?
|
|
||||||
public func makeView(context: AccountContext) -> DrawingEntityView {
|
|
||||||
let entityView = DrawingMediaEntityView(context: context, entity: self)
|
|
||||||
self.currentEntityView = entityView
|
|
||||||
return entityView
|
|
||||||
}
|
|
||||||
|
|
||||||
public func prepareForRender() {
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public final class DrawingMediaEntityView: DrawingEntityView, DrawingEntityMediaView {
|
public final class DrawingMediaEntityView: DrawingEntityView, DrawingEntityMediaView {
|
||||||
private var mediaEntity: DrawingMediaEntity {
|
private var mediaEntity: DrawingMediaEntity {
|
||||||
return self.entity as! DrawingMediaEntity
|
return self.entity as! DrawingMediaEntity
|
||||||
|
@ -5,6 +5,7 @@ import MetalKit
|
|||||||
import Display
|
import Display
|
||||||
import SwiftSignalKit
|
import SwiftSignalKit
|
||||||
import AppBundle
|
import AppBundle
|
||||||
|
import MediaEditor
|
||||||
|
|
||||||
final class DrawingMetalView: MTKView {
|
final class DrawingMetalView: MTKView {
|
||||||
let size: CGSize
|
let size: CGSize
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
import Foundation
|
import Foundation
|
||||||
import UIKit
|
import UIKit
|
||||||
import Display
|
import Display
|
||||||
|
import MediaEditor
|
||||||
|
|
||||||
final class NeonTool: DrawingElement {
|
final class NeonTool: DrawingElement {
|
||||||
class RenderView: UIView, DrawingRenderView {
|
class RenderView: UIView, DrawingRenderView {
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
import Foundation
|
import Foundation
|
||||||
import UIKit
|
import UIKit
|
||||||
import Display
|
import Display
|
||||||
|
import MediaEditor
|
||||||
|
|
||||||
private let activeWidthFactor: CGFloat = 0.7
|
private let activeWidthFactor: CGFloat = 0.7
|
||||||
|
|
||||||
|
@ -21,6 +21,13 @@ import ChatEntityKeyboardInputNode
|
|||||||
import EntityKeyboard
|
import EntityKeyboard
|
||||||
import TelegramUIPreferences
|
import TelegramUIPreferences
|
||||||
import FastBlur
|
import FastBlur
|
||||||
|
import MediaEditor
|
||||||
|
|
||||||
|
public struct DrawingResultData {
|
||||||
|
public let data: Data?
|
||||||
|
public let drawingImage: UIImage?
|
||||||
|
public let entities: [CodableDrawingEntity]
|
||||||
|
}
|
||||||
|
|
||||||
enum DrawingToolState: Equatable, Codable {
|
enum DrawingToolState: Equatable, Codable {
|
||||||
private enum CodingKeys: String, CodingKey {
|
private enum CodingKeys: String, CodingKey {
|
||||||
@ -502,6 +509,8 @@ private final class DrawingScreenComponent: CombinedComponent {
|
|||||||
let toggleWithPreviousTool: ActionSlot<Void>
|
let toggleWithPreviousTool: ActionSlot<Void>
|
||||||
let insertSticker: ActionSlot<Void>
|
let insertSticker: ActionSlot<Void>
|
||||||
let insertText: ActionSlot<Void>
|
let insertText: ActionSlot<Void>
|
||||||
|
let updateEntityView: ActionSlot<(UUID, Bool)>
|
||||||
|
let endEditingTextEntityView: ActionSlot<(UUID, Bool)>
|
||||||
let apply: ActionSlot<Void>
|
let apply: ActionSlot<Void>
|
||||||
let dismiss: ActionSlot<Void>
|
let dismiss: ActionSlot<Void>
|
||||||
|
|
||||||
@ -533,6 +542,8 @@ private final class DrawingScreenComponent: CombinedComponent {
|
|||||||
toggleWithPreviousTool: ActionSlot<Void>,
|
toggleWithPreviousTool: ActionSlot<Void>,
|
||||||
insertSticker: ActionSlot<Void>,
|
insertSticker: ActionSlot<Void>,
|
||||||
insertText: ActionSlot<Void>,
|
insertText: ActionSlot<Void>,
|
||||||
|
updateEntityView: ActionSlot<(UUID, Bool)>,
|
||||||
|
endEditingTextEntityView: ActionSlot<(UUID, Bool)>,
|
||||||
apply: ActionSlot<Void>,
|
apply: ActionSlot<Void>,
|
||||||
dismiss: ActionSlot<Void>,
|
dismiss: ActionSlot<Void>,
|
||||||
presentColorPicker: @escaping (DrawingColor) -> Void,
|
presentColorPicker: @escaping (DrawingColor) -> Void,
|
||||||
@ -562,6 +573,8 @@ private final class DrawingScreenComponent: CombinedComponent {
|
|||||||
self.toggleWithPreviousTool = toggleWithPreviousTool
|
self.toggleWithPreviousTool = toggleWithPreviousTool
|
||||||
self.insertSticker = insertSticker
|
self.insertSticker = insertSticker
|
||||||
self.insertText = insertText
|
self.insertText = insertText
|
||||||
|
self.updateEntityView = updateEntityView
|
||||||
|
self.endEditingTextEntityView = endEditingTextEntityView
|
||||||
self.apply = apply
|
self.apply = apply
|
||||||
self.dismiss = dismiss
|
self.dismiss = dismiss
|
||||||
self.presentColorPicker = presentColorPicker
|
self.presentColorPicker = presentColorPicker
|
||||||
@ -637,6 +650,8 @@ private final class DrawingScreenComponent: CombinedComponent {
|
|||||||
private let toggleWithPreviousTool: ActionSlot<Void>
|
private let toggleWithPreviousTool: ActionSlot<Void>
|
||||||
private let insertSticker: ActionSlot<Void>
|
private let insertSticker: ActionSlot<Void>
|
||||||
private let insertText: ActionSlot<Void>
|
private let insertText: ActionSlot<Void>
|
||||||
|
private let updateEntityView: ActionSlot<(UUID, Bool)>
|
||||||
|
private let endEditingTextEntityView: ActionSlot<(UUID, Bool)>
|
||||||
private let present: (ViewController) -> Void
|
private let present: (ViewController) -> Void
|
||||||
|
|
||||||
var currentMode: Mode
|
var currentMode: Mode
|
||||||
@ -661,6 +676,8 @@ private final class DrawingScreenComponent: CombinedComponent {
|
|||||||
toggleWithPreviousTool: ActionSlot<Void>,
|
toggleWithPreviousTool: ActionSlot<Void>,
|
||||||
insertSticker: ActionSlot<Void>,
|
insertSticker: ActionSlot<Void>,
|
||||||
insertText: ActionSlot<Void>,
|
insertText: ActionSlot<Void>,
|
||||||
|
updateEntityView: ActionSlot<(UUID, Bool)>,
|
||||||
|
endEditingTextEntityView: ActionSlot<(UUID, Bool)>,
|
||||||
present: @escaping (ViewController) -> Void)
|
present: @escaping (ViewController) -> Void)
|
||||||
{
|
{
|
||||||
self.context = context
|
self.context = context
|
||||||
@ -673,6 +690,8 @@ private final class DrawingScreenComponent: CombinedComponent {
|
|||||||
self.toggleWithPreviousTool = toggleWithPreviousTool
|
self.toggleWithPreviousTool = toggleWithPreviousTool
|
||||||
self.insertSticker = insertSticker
|
self.insertSticker = insertSticker
|
||||||
self.insertText = insertText
|
self.insertText = insertText
|
||||||
|
self.updateEntityView = updateEntityView
|
||||||
|
self.endEditingTextEntityView = endEditingTextEntityView
|
||||||
self.present = present
|
self.present = present
|
||||||
|
|
||||||
self.currentMode = .drawing
|
self.currentMode = .drawing
|
||||||
@ -808,7 +827,7 @@ private final class DrawingScreenComponent: CombinedComponent {
|
|||||||
self.currentColor = color
|
self.currentColor = color
|
||||||
if let selectedEntity = self.selectedEntity {
|
if let selectedEntity = self.selectedEntity {
|
||||||
selectedEntity.color = color
|
selectedEntity.color = color
|
||||||
selectedEntity.currentEntityView?.update()
|
self.updateEntityView.invoke((selectedEntity.uuid, false))
|
||||||
} else {
|
} else {
|
||||||
self.drawingState = self.drawingState.withUpdatedColor(color)
|
self.drawingState = self.drawingState.withUpdatedColor(color)
|
||||||
self.updateToolState.invoke(self.drawingState.currentToolState)
|
self.updateToolState.invoke(self.drawingState.currentToolState)
|
||||||
@ -850,7 +869,7 @@ private final class DrawingScreenComponent: CombinedComponent {
|
|||||||
} else {
|
} else {
|
||||||
selectedEntity.lineWidth = size
|
selectedEntity.lineWidth = size
|
||||||
}
|
}
|
||||||
selectedEntity.currentEntityView?.update()
|
self.updateEntityView.invoke((selectedEntity.uuid, false))
|
||||||
} else {
|
} else {
|
||||||
self.drawingState = self.drawingState.withUpdatedSize(size)
|
self.drawingState = self.drawingState.withUpdatedSize(size)
|
||||||
self.updateToolState.invoke(self.drawingState.currentToolState)
|
self.updateToolState.invoke(self.drawingState.currentToolState)
|
||||||
@ -996,7 +1015,7 @@ private final class DrawingScreenComponent: CombinedComponent {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func makeState() -> State {
|
func makeState() -> State {
|
||||||
return State(context: self.context, existingStickerPickerInputData: self.existingStickerPickerInputData, updateToolState: self.updateToolState, insertEntity: self.insertEntity, deselectEntity: self.deselectEntity, updateEntitiesPlayback: self.updateEntitiesPlayback, dismissEyedropper: self.dismissEyedropper, toggleWithEraser: self.toggleWithEraser, toggleWithPreviousTool: self.toggleWithPreviousTool, insertSticker: self.insertSticker, insertText: self.insertText, present: self.present)
|
return State(context: self.context, existingStickerPickerInputData: self.existingStickerPickerInputData, updateToolState: self.updateToolState, insertEntity: self.insertEntity, deselectEntity: self.deselectEntity, updateEntitiesPlayback: self.updateEntitiesPlayback, dismissEyedropper: self.dismissEyedropper, toggleWithEraser: self.toggleWithEraser, toggleWithPreviousTool: self.toggleWithPreviousTool, insertSticker: self.insertSticker, insertText: self.insertText, updateEntityView: self.updateEntityView, endEditingTextEntityView: self.endEditingTextEntityView, present: self.present)
|
||||||
}
|
}
|
||||||
|
|
||||||
static var body: Body {
|
static var body: Body {
|
||||||
@ -1070,6 +1089,9 @@ private final class DrawingScreenComponent: CombinedComponent {
|
|||||||
let dismissFastColorPicker = component.dismissFastColorPicker
|
let dismissFastColorPicker = component.dismissFastColorPicker
|
||||||
let presentFontPicker = component.presentFontPicker
|
let presentFontPicker = component.presentFontPicker
|
||||||
|
|
||||||
|
let updateEntityView = component.updateEntityView
|
||||||
|
let endEditingTextEntityView = component.endEditingTextEntityView
|
||||||
|
|
||||||
component.updateState.connect { [weak state] updatedState in
|
component.updateState.connect { [weak state] updatedState in
|
||||||
state?.updateDrawingState(updatedState)
|
state?.updateDrawingState(updatedState)
|
||||||
}
|
}
|
||||||
@ -1162,9 +1184,7 @@ private final class DrawingScreenComponent: CombinedComponent {
|
|||||||
nextStyle = .regular
|
nextStyle = .regular
|
||||||
}
|
}
|
||||||
textEntity.style = nextStyle
|
textEntity.style = nextStyle
|
||||||
if let entityView = textEntity.currentEntityView {
|
updateEntityView.invoke((textEntity.uuid, false))
|
||||||
entityView.update()
|
|
||||||
}
|
|
||||||
state?.updated(transition: .easeInOut(duration: 0.2))
|
state?.updated(transition: .easeInOut(duration: 0.2))
|
||||||
},
|
},
|
||||||
toggleAnimation: { [weak state, weak textEntity] in
|
toggleAnimation: { [weak state, weak textEntity] in
|
||||||
@ -1183,9 +1203,7 @@ private final class DrawingScreenComponent: CombinedComponent {
|
|||||||
nextAnimation = .none
|
nextAnimation = .none
|
||||||
}
|
}
|
||||||
textEntity.animation = nextAnimation
|
textEntity.animation = nextAnimation
|
||||||
if let entityView = textEntity.currentEntityView {
|
updateEntityView.invoke((textEntity.uuid, false))
|
||||||
entityView.update()
|
|
||||||
}
|
|
||||||
state?.updated(transition: .easeInOut(duration: 0.2))
|
state?.updated(transition: .easeInOut(duration: 0.2))
|
||||||
},
|
},
|
||||||
toggleAlignment: { [weak state, weak textEntity] in
|
toggleAlignment: { [weak state, weak textEntity] in
|
||||||
@ -1202,9 +1220,7 @@ private final class DrawingScreenComponent: CombinedComponent {
|
|||||||
nextAlignment = .left
|
nextAlignment = .left
|
||||||
}
|
}
|
||||||
textEntity.alignment = nextAlignment
|
textEntity.alignment = nextAlignment
|
||||||
if let entityView = textEntity.currentEntityView {
|
updateEntityView.invoke((textEntity.uuid, false))
|
||||||
entityView.update()
|
|
||||||
}
|
|
||||||
state?.updated(transition: .easeInOut(duration: 0.2))
|
state?.updated(transition: .easeInOut(duration: 0.2))
|
||||||
},
|
},
|
||||||
presentFontPicker: {
|
presentFontPicker: {
|
||||||
@ -1549,14 +1565,14 @@ private final class DrawingScreenComponent: CombinedComponent {
|
|||||||
} else {
|
} else {
|
||||||
entity.drawType = .fill
|
entity.drawType = .fill
|
||||||
}
|
}
|
||||||
entity.currentEntityView?.update()
|
updateEntityView.invoke((entity.uuid, false))
|
||||||
} else if let entity = state.selectedEntity as? DrawingBubbleEntity {
|
} else if let entity = state.selectedEntity as? DrawingBubbleEntity {
|
||||||
if case .fill = entity.drawType {
|
if case .fill = entity.drawType {
|
||||||
entity.drawType = .stroke
|
entity.drawType = .stroke
|
||||||
} else {
|
} else {
|
||||||
entity.drawType = .fill
|
entity.drawType = .fill
|
||||||
}
|
}
|
||||||
entity.currentEntityView?.update()
|
updateEntityView.invoke((entity.uuid, false))
|
||||||
} else if let entity = state.selectedEntity as? DrawingVectorEntity {
|
} else if let entity = state.selectedEntity as? DrawingVectorEntity {
|
||||||
if case .oneSidedArrow = entity.type {
|
if case .oneSidedArrow = entity.type {
|
||||||
entity.type = .twoSidedArrow
|
entity.type = .twoSidedArrow
|
||||||
@ -1565,7 +1581,7 @@ private final class DrawingScreenComponent: CombinedComponent {
|
|||||||
} else {
|
} else {
|
||||||
entity.type = .oneSidedArrow
|
entity.type = .oneSidedArrow
|
||||||
}
|
}
|
||||||
entity.currentEntityView?.update()
|
updateEntityView.invoke((entity.uuid, false))
|
||||||
}
|
}
|
||||||
state.updated(transition: .easeInOut(duration: 0.2))
|
state.updated(transition: .easeInOut(duration: 0.2))
|
||||||
}
|
}
|
||||||
@ -1594,10 +1610,10 @@ private final class DrawingScreenComponent: CombinedComponent {
|
|||||||
var updatedTailPosition = entity.tailPosition
|
var updatedTailPosition = entity.tailPosition
|
||||||
updatedTailPosition.x = 1.0 - updatedTailPosition.x
|
updatedTailPosition.x = 1.0 - updatedTailPosition.x
|
||||||
entity.tailPosition = updatedTailPosition
|
entity.tailPosition = updatedTailPosition
|
||||||
entity.currentEntityView?.update()
|
updateEntityView.invoke((entity.uuid, false))
|
||||||
} else if let entity = state.selectedEntity as? DrawingStickerEntity {
|
} else if let entity = state.selectedEntity as? DrawingStickerEntity {
|
||||||
entity.mirrored = !entity.mirrored
|
entity.mirrored = !entity.mirrored
|
||||||
entity.currentEntityView?.update(animated: true)
|
updateEntityView.invoke((entity.uuid, true))
|
||||||
}
|
}
|
||||||
state.updated(transition: .easeInOut(duration: 0.2))
|
state.updated(transition: .easeInOut(duration: 0.2))
|
||||||
}
|
}
|
||||||
@ -1616,9 +1632,9 @@ private final class DrawingScreenComponent: CombinedComponent {
|
|||||||
var sizeSliderVisible = false
|
var sizeSliderVisible = false
|
||||||
var isEditingText = false
|
var isEditingText = false
|
||||||
var sizeValue: CGFloat?
|
var sizeValue: CGFloat?
|
||||||
if let textEntity = state.selectedEntity as? DrawingTextEntity, let entityView = textEntity.currentEntityView as? DrawingTextEntityView {
|
if let textEntity = state.selectedEntity as? DrawingTextEntity, !"".isEmpty {//} let entityView = textEntity.currentEntityView as? DrawingTextEntityView {
|
||||||
sizeSliderVisible = true
|
sizeSliderVisible = true
|
||||||
isEditingText = entityView.isEditing
|
isEditingText = false//entityView.isEditing
|
||||||
sizeValue = textEntity.fontSize
|
sizeValue = textEntity.fontSize
|
||||||
} else {
|
} else {
|
||||||
if state.selectedEntity == nil || !(state.selectedEntity is DrawingStickerEntity) {
|
if state.selectedEntity == nil || !(state.selectedEntity is DrawingStickerEntity) {
|
||||||
@ -1755,8 +1771,8 @@ private final class DrawingScreenComponent: CombinedComponent {
|
|||||||
Text(text: environment.strings.Common_Cancel, font: Font.regular(17.0), color: .white)
|
Text(text: environment.strings.Common_Cancel, font: Font.regular(17.0), color: .white)
|
||||||
),
|
),
|
||||||
action: { [weak state] in
|
action: { [weak state] in
|
||||||
if let entity = state?.selectedEntity as? DrawingTextEntity, let entityView = entity.currentEntityView as? DrawingTextEntityView {
|
if let entity = state?.selectedEntity as? DrawingTextEntity {
|
||||||
entityView.endEditing(reset: true)
|
endEditingTextEntityView.invoke((entity.uuid, true))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
),
|
),
|
||||||
@ -1775,8 +1791,8 @@ private final class DrawingScreenComponent: CombinedComponent {
|
|||||||
Text(text: environment.strings.Common_Done, font: Font.semibold(17.0), color: .white)
|
Text(text: environment.strings.Common_Done, font: Font.semibold(17.0), color: .white)
|
||||||
),
|
),
|
||||||
action: { [weak state] in
|
action: { [weak state] in
|
||||||
if let entity = state?.selectedEntity as? DrawingTextEntity, let entityView = entity.currentEntityView as? DrawingTextEntityView {
|
if let entity = state?.selectedEntity as? DrawingTextEntity {
|
||||||
entityView.endEditing()
|
endEditingTextEntityView.invoke((entity.uuid, false))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
),
|
),
|
||||||
@ -2041,6 +2057,8 @@ public class DrawingScreen: ViewController, TGPhotoDrawingInterfaceController, U
|
|||||||
private let toggleWithPreviousTool: ActionSlot<Void>
|
private let toggleWithPreviousTool: ActionSlot<Void>
|
||||||
fileprivate let insertSticker: ActionSlot<Void>
|
fileprivate let insertSticker: ActionSlot<Void>
|
||||||
fileprivate let insertText: ActionSlot<Void>
|
fileprivate let insertText: ActionSlot<Void>
|
||||||
|
private let updateEntityView: ActionSlot<(UUID, Bool)>
|
||||||
|
private let endEditingTextEntityView: ActionSlot<(UUID, Bool)>
|
||||||
|
|
||||||
private let apply: ActionSlot<Void>
|
private let apply: ActionSlot<Void>
|
||||||
private let dismiss: ActionSlot<Void>
|
private let dismiss: ActionSlot<Void>
|
||||||
@ -2274,6 +2292,18 @@ public class DrawingScreen: ViewController, TGPhotoDrawingInterfaceController, U
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
self.updateEntityView.connect { [weak self] uuid, animated in
|
||||||
|
if let strongSelf = self, let entitiesView = strongSelf._entitiesView {
|
||||||
|
entitiesView.getView(for: uuid)?.update(animated: animated)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
self.endEditingTextEntityView.connect { [weak self] uuid, reset in
|
||||||
|
if let strongSelf = self, let entitiesView = strongSelf._entitiesView {
|
||||||
|
if let textEntityView = entitiesView.getView(for: uuid) as? DrawingTextEntityView {
|
||||||
|
textEntityView.endEditing(reset: reset)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return self._entitiesView!
|
return self._entitiesView!
|
||||||
}
|
}
|
||||||
@ -2312,6 +2342,8 @@ public class DrawingScreen: ViewController, TGPhotoDrawingInterfaceController, U
|
|||||||
self.toggleWithPreviousTool = ActionSlot<Void>()
|
self.toggleWithPreviousTool = ActionSlot<Void>()
|
||||||
self.insertSticker = ActionSlot<Void>()
|
self.insertSticker = ActionSlot<Void>()
|
||||||
self.insertText = ActionSlot<Void>()
|
self.insertText = ActionSlot<Void>()
|
||||||
|
self.updateEntityView = ActionSlot<(UUID, Bool)>()
|
||||||
|
self.endEditingTextEntityView = ActionSlot<(UUID, Bool)>()
|
||||||
self.apply = ActionSlot<Void>()
|
self.apply = ActionSlot<Void>()
|
||||||
self.dismiss = ActionSlot<Void>()
|
self.dismiss = ActionSlot<Void>()
|
||||||
|
|
||||||
@ -2737,6 +2769,8 @@ public class DrawingScreen: ViewController, TGPhotoDrawingInterfaceController, U
|
|||||||
toggleWithPreviousTool: self.toggleWithPreviousTool,
|
toggleWithPreviousTool: self.toggleWithPreviousTool,
|
||||||
insertSticker: self.insertSticker,
|
insertSticker: self.insertSticker,
|
||||||
insertText: self.insertText,
|
insertText: self.insertText,
|
||||||
|
updateEntityView: self.updateEntityView,
|
||||||
|
endEditingTextEntityView: self.endEditingTextEntityView,
|
||||||
apply: self.apply,
|
apply: self.apply,
|
||||||
dismiss: self.dismiss,
|
dismiss: self.dismiss,
|
||||||
presentColorPicker: { [weak self] initialColor in
|
presentColorPicker: { [weak self] initialColor in
|
||||||
@ -3042,6 +3076,24 @@ public class DrawingScreen: ViewController, TGPhotoDrawingInterfaceController, U
|
|||||||
self.drawingView.addInteraction(dropInteraction)
|
self.drawingView.addInteraction(dropInteraction)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public func generateDrawingResultData() -> DrawingResultData? {
|
||||||
|
if self.drawingView.isEmpty && self.entitiesView.entities.isEmpty {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
let drawingImage = generateImage(self.drawingView.imageSize, contextGenerator: { size, context in
|
||||||
|
let bounds = CGRect(origin: .zero, size: size)
|
||||||
|
context.clear(bounds)
|
||||||
|
if let cgImage = self.drawingView.drawingImage?.cgImage {
|
||||||
|
context.draw(cgImage, in: bounds)
|
||||||
|
}
|
||||||
|
}, opaque: false, scale: 1.0)
|
||||||
|
|
||||||
|
let _ = self.entitiesView.entitiesData
|
||||||
|
let codableEntities = self.entitiesView.entities.filter { !($0 is DrawingMediaEntity) }.compactMap({ CodableDrawingEntity(entity: $0) })
|
||||||
|
return DrawingResultData(data: self.drawingView.drawingData, drawingImage: drawingImage, entities: codableEntities)
|
||||||
|
}
|
||||||
|
|
||||||
public func generateResultData() -> TGPaintingData? {
|
public func generateResultData() -> TGPaintingData? {
|
||||||
if self.drawingView.isEmpty && self.entitiesView.entities.isEmpty {
|
if self.drawingView.isEmpty && self.entitiesView.entities.isEmpty {
|
||||||
return nil
|
return nil
|
||||||
@ -3102,7 +3154,7 @@ public class DrawingScreen: ViewController, TGPhotoDrawingInterfaceController, U
|
|||||||
stickers.append(coder.makeData())
|
stickers.append(coder.makeData())
|
||||||
} else if let text = entity as? DrawingTextEntity, let subEntities = text.renderSubEntities {
|
} else if let text = entity as? DrawingTextEntity, let subEntities = text.renderSubEntities {
|
||||||
for sticker in subEntities {
|
for sticker in subEntities {
|
||||||
if case let .file(file) = sticker.content {
|
if let sticker = sticker as? DrawingStickerEntity, case let .file(file) = sticker.content {
|
||||||
let coder = PostboxEncoder()
|
let coder = PostboxEncoder()
|
||||||
coder.encodeRootObject(file)
|
coder.encodeRootObject(file)
|
||||||
stickers.append(coder.makeData())
|
stickers.append(coder.makeData())
|
||||||
|
@ -2,125 +2,7 @@ import Foundation
|
|||||||
import UIKit
|
import UIKit
|
||||||
import Display
|
import Display
|
||||||
import AccountContext
|
import AccountContext
|
||||||
|
import MediaEditor
|
||||||
public final class DrawingSimpleShapeEntity: DrawingEntity, Codable {
|
|
||||||
private enum CodingKeys: String, CodingKey {
|
|
||||||
case uuid
|
|
||||||
case shapeType
|
|
||||||
case drawType
|
|
||||||
case color
|
|
||||||
case lineWidth
|
|
||||||
case referenceDrawingSize
|
|
||||||
case position
|
|
||||||
case size
|
|
||||||
case rotation
|
|
||||||
case renderImage
|
|
||||||
}
|
|
||||||
|
|
||||||
public enum ShapeType: Codable {
|
|
||||||
case rectangle
|
|
||||||
case ellipse
|
|
||||||
case star
|
|
||||||
}
|
|
||||||
|
|
||||||
public enum DrawType: Codable {
|
|
||||||
case fill
|
|
||||||
case stroke
|
|
||||||
}
|
|
||||||
|
|
||||||
public let uuid: UUID
|
|
||||||
public let isAnimated: Bool
|
|
||||||
|
|
||||||
var shapeType: ShapeType
|
|
||||||
var drawType: DrawType
|
|
||||||
public var color: DrawingColor
|
|
||||||
public var lineWidth: CGFloat
|
|
||||||
|
|
||||||
var referenceDrawingSize: CGSize
|
|
||||||
public var position: CGPoint
|
|
||||||
public var size: CGSize
|
|
||||||
public var rotation: CGFloat
|
|
||||||
|
|
||||||
public var center: CGPoint {
|
|
||||||
return self.position
|
|
||||||
}
|
|
||||||
|
|
||||||
public var scale: CGFloat = 1.0
|
|
||||||
|
|
||||||
public var renderImage: UIImage?
|
|
||||||
|
|
||||||
public var isMedia: Bool {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
init(shapeType: ShapeType, drawType: DrawType, color: DrawingColor, lineWidth: CGFloat) {
|
|
||||||
self.uuid = UUID()
|
|
||||||
self.isAnimated = false
|
|
||||||
|
|
||||||
self.shapeType = shapeType
|
|
||||||
self.drawType = drawType
|
|
||||||
self.color = color
|
|
||||||
self.lineWidth = lineWidth
|
|
||||||
|
|
||||||
self.referenceDrawingSize = .zero
|
|
||||||
self.position = .zero
|
|
||||||
self.size = CGSize(width: 1.0, height: 1.0)
|
|
||||||
self.rotation = 0.0
|
|
||||||
}
|
|
||||||
|
|
||||||
public init(from decoder: Decoder) throws {
|
|
||||||
let container = try decoder.container(keyedBy: CodingKeys.self)
|
|
||||||
self.uuid = try container.decode(UUID.self, forKey: .uuid)
|
|
||||||
self.isAnimated = false
|
|
||||||
self.shapeType = try container.decode(ShapeType.self, forKey: .shapeType)
|
|
||||||
self.drawType = try container.decode(DrawType.self, forKey: .drawType)
|
|
||||||
self.color = try container.decode(DrawingColor.self, forKey: .color)
|
|
||||||
self.lineWidth = try container.decode(CGFloat.self, forKey: .lineWidth)
|
|
||||||
self.referenceDrawingSize = try container.decode(CGSize.self, forKey: .referenceDrawingSize)
|
|
||||||
self.position = try container.decode(CGPoint.self, forKey: .position)
|
|
||||||
self.size = try container.decode(CGSize.self, forKey: .size)
|
|
||||||
self.rotation = try container.decode(CGFloat.self, forKey: .rotation)
|
|
||||||
if let renderImageData = try? container.decodeIfPresent(Data.self, forKey: .renderImage) {
|
|
||||||
self.renderImage = UIImage(data: renderImageData)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public func encode(to encoder: Encoder) throws {
|
|
||||||
var container = encoder.container(keyedBy: CodingKeys.self)
|
|
||||||
try container.encode(self.uuid, forKey: .uuid)
|
|
||||||
try container.encode(self.shapeType, forKey: .shapeType)
|
|
||||||
try container.encode(self.drawType, forKey: .drawType)
|
|
||||||
try container.encode(self.color, forKey: .color)
|
|
||||||
try container.encode(self.lineWidth, forKey: .lineWidth)
|
|
||||||
try container.encode(self.referenceDrawingSize, forKey: .referenceDrawingSize)
|
|
||||||
try container.encode(self.position, forKey: .position)
|
|
||||||
try container.encode(self.size, forKey: .size)
|
|
||||||
try container.encode(self.rotation, forKey: .rotation)
|
|
||||||
if let renderImage, let data = renderImage.pngData() {
|
|
||||||
try container.encode(data, forKey: .renderImage)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public func duplicate() -> DrawingEntity {
|
|
||||||
let newEntity = DrawingSimpleShapeEntity(shapeType: self.shapeType, drawType: self.drawType, color: self.color, lineWidth: self.lineWidth)
|
|
||||||
newEntity.referenceDrawingSize = self.referenceDrawingSize
|
|
||||||
newEntity.position = self.position
|
|
||||||
newEntity.size = self.size
|
|
||||||
newEntity.rotation = self.rotation
|
|
||||||
return newEntity
|
|
||||||
}
|
|
||||||
|
|
||||||
public weak var currentEntityView: DrawingEntityView?
|
|
||||||
public func makeView(context: AccountContext) -> DrawingEntityView {
|
|
||||||
let entityView = DrawingSimpleShapeEntityView(context: context, entity: self)
|
|
||||||
self.currentEntityView = entityView
|
|
||||||
return entityView
|
|
||||||
}
|
|
||||||
|
|
||||||
public func prepareForRender() {
|
|
||||||
self.renderImage = (self.currentEntityView as? DrawingSimpleShapeEntityView)?.getRenderImage()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
final class DrawingSimpleShapeEntityView: DrawingEntityView {
|
final class DrawingSimpleShapeEntityView: DrawingEntityView {
|
||||||
private var shapeEntity: DrawingSimpleShapeEntity {
|
private var shapeEntity: DrawingSimpleShapeEntity {
|
||||||
|
@ -7,121 +7,7 @@ import AnimatedStickerNode
|
|||||||
import TelegramAnimatedStickerNode
|
import TelegramAnimatedStickerNode
|
||||||
import StickerResources
|
import StickerResources
|
||||||
import AccountContext
|
import AccountContext
|
||||||
|
import MediaEditor
|
||||||
public final class DrawingStickerEntity: DrawingEntity, Codable {
|
|
||||||
public enum Content {
|
|
||||||
case file(TelegramMediaFile)
|
|
||||||
case image(UIImage)
|
|
||||||
}
|
|
||||||
private enum CodingKeys: String, CodingKey {
|
|
||||||
case uuid
|
|
||||||
case file
|
|
||||||
case image
|
|
||||||
case referenceDrawingSize
|
|
||||||
case position
|
|
||||||
case scale
|
|
||||||
case rotation
|
|
||||||
case mirrored
|
|
||||||
}
|
|
||||||
|
|
||||||
public let uuid: UUID
|
|
||||||
public let content: Content
|
|
||||||
|
|
||||||
public var referenceDrawingSize: CGSize
|
|
||||||
public var position: CGPoint
|
|
||||||
public var scale: CGFloat
|
|
||||||
public var rotation: CGFloat
|
|
||||||
public var mirrored: Bool
|
|
||||||
|
|
||||||
public var color: DrawingColor = DrawingColor.clear
|
|
||||||
public var lineWidth: CGFloat = 0.0
|
|
||||||
|
|
||||||
public var center: CGPoint {
|
|
||||||
return self.position
|
|
||||||
}
|
|
||||||
|
|
||||||
public var baseSize: CGSize {
|
|
||||||
let size = max(10.0, min(self.referenceDrawingSize.width, self.referenceDrawingSize.height) * 0.2)
|
|
||||||
return CGSize(width: size, height: size)
|
|
||||||
}
|
|
||||||
|
|
||||||
public var isAnimated: Bool {
|
|
||||||
switch self.content {
|
|
||||||
case let .file(file):
|
|
||||||
return file.isAnimatedSticker || file.isVideoSticker || file.mimeType == "video/webm"
|
|
||||||
case .image:
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public var isMedia: Bool {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
public init(content: Content) {
|
|
||||||
self.uuid = UUID()
|
|
||||||
self.content = content
|
|
||||||
|
|
||||||
self.referenceDrawingSize = .zero
|
|
||||||
self.position = CGPoint()
|
|
||||||
self.scale = 1.0
|
|
||||||
self.rotation = 0.0
|
|
||||||
self.mirrored = false
|
|
||||||
}
|
|
||||||
|
|
||||||
public init(from decoder: Decoder) throws {
|
|
||||||
let container = try decoder.container(keyedBy: CodingKeys.self)
|
|
||||||
self.uuid = try container.decode(UUID.self, forKey: .uuid)
|
|
||||||
if let file = try container.decodeIfPresent(TelegramMediaFile.self, forKey: .file) {
|
|
||||||
self.content = .file(file)
|
|
||||||
} else if let imageData = try container.decodeIfPresent(Data.self, forKey: .image), let image = UIImage(data: imageData) {
|
|
||||||
self.content = .image(image)
|
|
||||||
} else {
|
|
||||||
fatalError()
|
|
||||||
}
|
|
||||||
self.referenceDrawingSize = try container.decode(CGSize.self, forKey: .referenceDrawingSize)
|
|
||||||
self.position = try container.decode(CGPoint.self, forKey: .position)
|
|
||||||
self.scale = try container.decode(CGFloat.self, forKey: .scale)
|
|
||||||
self.rotation = try container.decode(CGFloat.self, forKey: .rotation)
|
|
||||||
self.mirrored = try container.decode(Bool.self, forKey: .mirrored)
|
|
||||||
}
|
|
||||||
|
|
||||||
public func encode(to encoder: Encoder) throws {
|
|
||||||
var container = encoder.container(keyedBy: CodingKeys.self)
|
|
||||||
try container.encode(self.uuid, forKey: .uuid)
|
|
||||||
switch self.content {
|
|
||||||
case let .file(file):
|
|
||||||
try container.encode(file, forKey: .file)
|
|
||||||
case let .image(image):
|
|
||||||
try container.encodeIfPresent(image.pngData(), forKey: .image)
|
|
||||||
}
|
|
||||||
try container.encode(self.referenceDrawingSize, forKey: .referenceDrawingSize)
|
|
||||||
try container.encode(self.position, forKey: .position)
|
|
||||||
try container.encode(self.scale, forKey: .scale)
|
|
||||||
try container.encode(self.rotation, forKey: .rotation)
|
|
||||||
try container.encode(self.mirrored, forKey: .mirrored)
|
|
||||||
}
|
|
||||||
|
|
||||||
public func duplicate() -> DrawingEntity {
|
|
||||||
let newEntity = DrawingStickerEntity(content: self.content)
|
|
||||||
newEntity.referenceDrawingSize = self.referenceDrawingSize
|
|
||||||
newEntity.position = self.position
|
|
||||||
newEntity.scale = self.scale
|
|
||||||
newEntity.rotation = self.rotation
|
|
||||||
newEntity.mirrored = self.mirrored
|
|
||||||
return newEntity
|
|
||||||
}
|
|
||||||
|
|
||||||
public weak var currentEntityView: DrawingEntityView?
|
|
||||||
public func makeView(context: AccountContext) -> DrawingEntityView {
|
|
||||||
let entityView = DrawingStickerEntityView(context: context, entity: self)
|
|
||||||
self.currentEntityView = entityView
|
|
||||||
return entityView
|
|
||||||
}
|
|
||||||
|
|
||||||
public func prepareForRender() {
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
final class DrawingStickerEntityView: DrawingEntityView {
|
final class DrawingStickerEntityView: DrawingEntityView {
|
||||||
private var stickerEntity: DrawingStickerEntity {
|
private var stickerEntity: DrawingStickerEntity {
|
||||||
@ -274,7 +160,8 @@ final class DrawingStickerEntityView: DrawingEntityView {
|
|||||||
let dimensions = file.dimensions ?? PixelDimensions(width: 512, height: 512)
|
let dimensions = file.dimensions ?? PixelDimensions(width: 512, height: 512)
|
||||||
let fittedDimensions = dimensions.cgSize.aspectFitted(CGSize(width: 384.0, height: 384.0))
|
let fittedDimensions = dimensions.cgSize.aspectFitted(CGSize(width: 384.0, height: 384.0))
|
||||||
let source = AnimatedStickerResourceSource(account: self.context.account, resource: file.resource, isVideo: file.isVideoSticker || file.mimeType == "video/webm")
|
let source = AnimatedStickerResourceSource(account: self.context.account, resource: file.resource, isVideo: file.isVideoSticker || file.mimeType == "video/webm")
|
||||||
self.animationNode?.setup(source: source, width: Int(fittedDimensions.width), height: Int(fittedDimensions.height), playbackMode: .loop, mode: .direct(cachePathPrefix: nil))
|
let pathPrefix = self.context.account.postbox.mediaBox.shortLivedResourceCachePathPrefix(file.resource.id)
|
||||||
|
self.animationNode?.setup(source: source, width: Int(fittedDimensions.width), height: Int(fittedDimensions.height), playbackMode: .loop, mode: .direct(cachePathPrefix: pathPrefix))
|
||||||
|
|
||||||
self.cachedDisposable.set((source.cachedDataPath(width: 384, height: 384)
|
self.cachedDisposable.set((source.cachedDataPath(width: 384, height: 384)
|
||||||
|> deliverOn(Queue.concurrentDefaultQueue())).start())
|
|> deliverOn(Queue.concurrentDefaultQueue())).start())
|
||||||
|
@ -5,286 +5,17 @@ import SwiftSignalKit
|
|||||||
import AccountContext
|
import AccountContext
|
||||||
import TextFormat
|
import TextFormat
|
||||||
import EmojiTextAttachmentView
|
import EmojiTextAttachmentView
|
||||||
|
import MediaEditor
|
||||||
|
|
||||||
public final class DrawingTextEntity: DrawingEntity, Codable {
|
extension DrawingTextEntity.Alignment {
|
||||||
final class CustomEmojiAttribute: Codable {
|
var alignment: NSTextAlignment {
|
||||||
private enum CodingKeys: String, CodingKey {
|
switch self {
|
||||||
case attribute
|
case .left:
|
||||||
case rangeOrigin
|
return .left
|
||||||
case rangeLength
|
case .center:
|
||||||
}
|
return .center
|
||||||
let attribute: ChatTextInputTextCustomEmojiAttribute
|
case .right:
|
||||||
let range: NSRange
|
return .right
|
||||||
|
|
||||||
init(attribute: ChatTextInputTextCustomEmojiAttribute, range: NSRange) {
|
|
||||||
self.attribute = attribute
|
|
||||||
self.range = range
|
|
||||||
}
|
|
||||||
|
|
||||||
init(from decoder: Decoder) throws {
|
|
||||||
let container = try decoder.container(keyedBy: CodingKeys.self)
|
|
||||||
self.attribute = try container.decode(ChatTextInputTextCustomEmojiAttribute.self, forKey: .attribute)
|
|
||||||
|
|
||||||
let rangeOrigin = try container.decode(Int.self, forKey: .rangeOrigin)
|
|
||||||
let rangeLength = try container.decode(Int.self, forKey: .rangeLength)
|
|
||||||
self.range = NSMakeRange(rangeOrigin, rangeLength)
|
|
||||||
}
|
|
||||||
|
|
||||||
func encode(to encoder: Encoder) throws {
|
|
||||||
var container = encoder.container(keyedBy: CodingKeys.self)
|
|
||||||
try container.encode(self.attribute, forKey: .attribute)
|
|
||||||
try container.encode(self.range.location, forKey: .rangeOrigin)
|
|
||||||
try container.encode(self.range.length, forKey: .rangeLength)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private enum CodingKeys: String, CodingKey {
|
|
||||||
case uuid
|
|
||||||
case text
|
|
||||||
case textAttributes
|
|
||||||
case style
|
|
||||||
case animation
|
|
||||||
case font
|
|
||||||
case alignment
|
|
||||||
case fontSize
|
|
||||||
case color
|
|
||||||
case referenceDrawingSize
|
|
||||||
case position
|
|
||||||
case width
|
|
||||||
case scale
|
|
||||||
case rotation
|
|
||||||
case renderImage
|
|
||||||
case renderSubEntities
|
|
||||||
case renderAnimationFrames
|
|
||||||
}
|
|
||||||
|
|
||||||
enum Style: Codable {
|
|
||||||
case regular
|
|
||||||
case filled
|
|
||||||
case semi
|
|
||||||
case stroke
|
|
||||||
}
|
|
||||||
|
|
||||||
enum Animation: Codable {
|
|
||||||
case none
|
|
||||||
case typing
|
|
||||||
case wiggle
|
|
||||||
case zoomIn
|
|
||||||
}
|
|
||||||
|
|
||||||
enum Font: Codable {
|
|
||||||
case sanFrancisco
|
|
||||||
case other(String, String)
|
|
||||||
}
|
|
||||||
|
|
||||||
enum Alignment: Codable {
|
|
||||||
case left
|
|
||||||
case center
|
|
||||||
case right
|
|
||||||
|
|
||||||
var alignment: NSTextAlignment {
|
|
||||||
switch self {
|
|
||||||
case .left:
|
|
||||||
return .left
|
|
||||||
case .center:
|
|
||||||
return .center
|
|
||||||
case .right:
|
|
||||||
return .right
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public var uuid: UUID
|
|
||||||
public var isAnimated: Bool {
|
|
||||||
if self.animation != .none {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
var isAnimated = false
|
|
||||||
self.text.enumerateAttributes(in: NSMakeRange(0, self.text.length), options: [], using: { attributes, range, _ in
|
|
||||||
if let _ = attributes[ChatTextInputAttributes.customEmoji] as? ChatTextInputTextCustomEmojiAttribute {
|
|
||||||
isAnimated = true
|
|
||||||
}
|
|
||||||
})
|
|
||||||
return isAnimated
|
|
||||||
}
|
|
||||||
|
|
||||||
var text: NSAttributedString
|
|
||||||
var style: Style
|
|
||||||
var animation: Animation
|
|
||||||
var font: Font
|
|
||||||
var alignment: Alignment
|
|
||||||
var fontSize: CGFloat
|
|
||||||
public var color: DrawingColor
|
|
||||||
public var lineWidth: CGFloat = 0.0
|
|
||||||
|
|
||||||
var referenceDrawingSize: CGSize
|
|
||||||
public var position: CGPoint
|
|
||||||
var width: CGFloat
|
|
||||||
public var scale: CGFloat
|
|
||||||
public var rotation: CGFloat
|
|
||||||
|
|
||||||
public var center: CGPoint {
|
|
||||||
return self.position
|
|
||||||
}
|
|
||||||
|
|
||||||
public var renderImage: UIImage?
|
|
||||||
public var renderSubEntities: [DrawingStickerEntity]?
|
|
||||||
|
|
||||||
public var isMedia: Bool {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
public class AnimationFrame: Codable {
|
|
||||||
private enum CodingKeys: String, CodingKey {
|
|
||||||
case timestamp
|
|
||||||
case duration
|
|
||||||
case image
|
|
||||||
}
|
|
||||||
|
|
||||||
public let timestamp: Double
|
|
||||||
public let duration: Double
|
|
||||||
public let image: UIImage
|
|
||||||
|
|
||||||
public init(timestamp: Double, duration: Double, image: UIImage) {
|
|
||||||
self.timestamp = timestamp
|
|
||||||
self.duration = duration
|
|
||||||
self.image = image
|
|
||||||
}
|
|
||||||
|
|
||||||
required public init(from decoder: Decoder) throws {
|
|
||||||
let container = try decoder.container(keyedBy: CodingKeys.self)
|
|
||||||
self.timestamp = try container.decode(Double.self, forKey: .timestamp)
|
|
||||||
self.duration = try container.decode(Double.self, forKey: .duration)
|
|
||||||
if let renderImageData = try? container.decodeIfPresent(Data.self, forKey: .image) {
|
|
||||||
self.image = UIImage(data: renderImageData)!
|
|
||||||
} else {
|
|
||||||
fatalError()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public func encode(to encoder: Encoder) throws {
|
|
||||||
var container = encoder.container(keyedBy: CodingKeys.self)
|
|
||||||
|
|
||||||
try container.encode(self.timestamp, forKey: .timestamp)
|
|
||||||
try container.encode(self.duration, forKey: .duration)
|
|
||||||
if let data = self.image.pngData() {
|
|
||||||
try container.encode(data, forKey: .image)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
public var renderAnimationFrames: [AnimationFrame]?
|
|
||||||
|
|
||||||
init(text: NSAttributedString, style: Style, animation: Animation, font: Font, alignment: Alignment, fontSize: CGFloat, color: DrawingColor) {
|
|
||||||
self.uuid = UUID()
|
|
||||||
|
|
||||||
self.text = text
|
|
||||||
self.style = style
|
|
||||||
self.animation = animation
|
|
||||||
self.font = font
|
|
||||||
self.alignment = alignment
|
|
||||||
self.fontSize = fontSize
|
|
||||||
self.color = color
|
|
||||||
|
|
||||||
self.referenceDrawingSize = .zero
|
|
||||||
self.position = .zero
|
|
||||||
self.width = 100.0
|
|
||||||
self.scale = 1.0
|
|
||||||
self.rotation = 0.0
|
|
||||||
}
|
|
||||||
|
|
||||||
public init(from decoder: Decoder) throws {
|
|
||||||
let container = try decoder.container(keyedBy: CodingKeys.self)
|
|
||||||
self.uuid = try container.decode(UUID.self, forKey: .uuid)
|
|
||||||
let text = try container.decode(String.self, forKey: .text)
|
|
||||||
|
|
||||||
let attributedString = NSMutableAttributedString(string: text)
|
|
||||||
let textAttributes = try container.decode([CustomEmojiAttribute].self, forKey: .textAttributes)
|
|
||||||
for attribute in textAttributes {
|
|
||||||
attributedString.addAttribute(ChatTextInputAttributes.customEmoji, value: attribute.attribute, range: attribute.range)
|
|
||||||
}
|
|
||||||
self.text = attributedString
|
|
||||||
|
|
||||||
self.style = try container.decode(Style.self, forKey: .style)
|
|
||||||
self.animation = try container.decode(Animation.self, forKey: .animation)
|
|
||||||
self.font = try container.decode(Font.self, forKey: .font)
|
|
||||||
self.alignment = try container.decode(Alignment.self, forKey: .alignment)
|
|
||||||
self.fontSize = try container.decode(CGFloat.self, forKey: .fontSize)
|
|
||||||
self.color = try container.decode(DrawingColor.self, forKey: .color)
|
|
||||||
self.referenceDrawingSize = try container.decode(CGSize.self, forKey: .referenceDrawingSize)
|
|
||||||
self.position = try container.decode(CGPoint.self, forKey: .position)
|
|
||||||
self.width = try container.decode(CGFloat.self, forKey: .width)
|
|
||||||
self.scale = try container.decode(CGFloat.self, forKey: .scale)
|
|
||||||
self.rotation = try container.decode(CGFloat.self, forKey: .rotation)
|
|
||||||
if let renderImageData = try? container.decodeIfPresent(Data.self, forKey: .renderImage) {
|
|
||||||
self.renderImage = UIImage(data: renderImageData)
|
|
||||||
}
|
|
||||||
if let renderSubEntities = try? container.decodeIfPresent([CodableDrawingEntity].self, forKey: .renderSubEntities) {
|
|
||||||
self.renderSubEntities = renderSubEntities.compactMap { $0.entity as? DrawingStickerEntity }
|
|
||||||
}
|
|
||||||
self.renderAnimationFrames = try container.decodeIfPresent([AnimationFrame].self, forKey: .renderAnimationFrames)
|
|
||||||
}
|
|
||||||
|
|
||||||
public func encode(to encoder: Encoder) throws {
|
|
||||||
var container = encoder.container(keyedBy: CodingKeys.self)
|
|
||||||
try container.encode(self.uuid, forKey: .uuid)
|
|
||||||
try container.encode(self.text.string, forKey: .text)
|
|
||||||
|
|
||||||
var textAttributes: [CustomEmojiAttribute] = []
|
|
||||||
self.text.enumerateAttributes(in: NSMakeRange(0, self.text.length), options: [], using: { attributes, range, _ in
|
|
||||||
if let value = attributes[ChatTextInputAttributes.customEmoji] as? ChatTextInputTextCustomEmojiAttribute {
|
|
||||||
textAttributes.append(CustomEmojiAttribute(attribute: value, range: range))
|
|
||||||
}
|
|
||||||
})
|
|
||||||
try container.encode(textAttributes, forKey: .textAttributes)
|
|
||||||
|
|
||||||
try container.encode(self.style, forKey: .style)
|
|
||||||
try container.encode(self.animation, forKey: .animation)
|
|
||||||
try container.encode(self.font, forKey: .font)
|
|
||||||
try container.encode(self.alignment, forKey: .alignment)
|
|
||||||
try container.encode(self.fontSize, forKey: .fontSize)
|
|
||||||
try container.encode(self.color, forKey: .color)
|
|
||||||
try container.encode(self.referenceDrawingSize, forKey: .referenceDrawingSize)
|
|
||||||
try container.encode(self.position, forKey: .position)
|
|
||||||
try container.encode(self.width, forKey: .width)
|
|
||||||
try container.encode(self.scale, forKey: .scale)
|
|
||||||
try container.encode(self.rotation, forKey: .rotation)
|
|
||||||
if let renderImage, let data = renderImage.pngData() {
|
|
||||||
try container.encode(data, forKey: .renderImage)
|
|
||||||
}
|
|
||||||
if let renderSubEntities = self.renderSubEntities {
|
|
||||||
let codableEntities: [CodableDrawingEntity] = renderSubEntities.map { .sticker($0) }
|
|
||||||
try container.encode(codableEntities, forKey: .renderSubEntities)
|
|
||||||
}
|
|
||||||
if let renderAnimationFrames = self.renderAnimationFrames {
|
|
||||||
try container.encode(renderAnimationFrames, forKey: .renderAnimationFrames)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public func duplicate() -> DrawingEntity {
|
|
||||||
let newEntity = DrawingTextEntity(text: self.text, style: self.style, animation: self.animation, font: self.font, alignment: self.alignment, fontSize: self.fontSize, color: self.color)
|
|
||||||
newEntity.referenceDrawingSize = self.referenceDrawingSize
|
|
||||||
newEntity.position = self.position
|
|
||||||
newEntity.width = self.width
|
|
||||||
newEntity.scale = self.scale
|
|
||||||
newEntity.rotation = self.rotation
|
|
||||||
return newEntity
|
|
||||||
}
|
|
||||||
|
|
||||||
public weak var currentEntityView: DrawingEntityView?
|
|
||||||
public func makeView(context: AccountContext) -> DrawingEntityView {
|
|
||||||
let entityView = DrawingTextEntityView(context: context, entity: self)
|
|
||||||
self.currentEntityView = entityView
|
|
||||||
return entityView
|
|
||||||
}
|
|
||||||
|
|
||||||
public func prepareForRender() {
|
|
||||||
self.renderImage = (self.currentEntityView as? DrawingTextEntityView)?.getRenderImage()
|
|
||||||
self.renderSubEntities = (self.currentEntityView as? DrawingTextEntityView)?.getRenderSubEntities()
|
|
||||||
|
|
||||||
if case .none = self.animation {
|
|
||||||
self.renderAnimationFrames = nil
|
|
||||||
} else {
|
|
||||||
self.renderAnimationFrames = (self.currentEntityView as? DrawingTextEntityView)?.getRenderAnimationFrames()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -892,7 +623,7 @@ final class DrawingTextEntityView: DrawingEntityView, UITextViewDelegate {
|
|||||||
return image
|
return image
|
||||||
}
|
}
|
||||||
|
|
||||||
func getRenderSubEntities() -> [DrawingStickerEntity] {
|
func getRenderSubEntities() -> [DrawingEntity] {
|
||||||
let textSize = self.textView.bounds.size
|
let textSize = self.textView.bounds.size
|
||||||
let textPosition = self.textEntity.position
|
let textPosition = self.textEntity.position
|
||||||
let scale = self.textEntity.scale
|
let scale = self.textEntity.scale
|
||||||
@ -900,7 +631,7 @@ final class DrawingTextEntityView: DrawingEntityView, UITextViewDelegate {
|
|||||||
|
|
||||||
let itemSize: CGFloat = floor(24.0 * self.displayFontSize * 0.78 / 17.0)
|
let itemSize: CGFloat = floor(24.0 * self.displayFontSize * 0.78 / 17.0)
|
||||||
|
|
||||||
var entities: [DrawingStickerEntity] = []
|
var entities: [DrawingEntity] = []
|
||||||
for (emojiRect, emojiAttribute) in self.emojiRects {
|
for (emojiRect, emojiAttribute) in self.emojiRects {
|
||||||
guard let file = emojiAttribute.file else {
|
guard let file = emojiAttribute.file else {
|
||||||
continue
|
continue
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
import Foundation
|
import Foundation
|
||||||
import UIKit
|
import UIKit
|
||||||
import Display
|
import Display
|
||||||
|
import MediaEditor
|
||||||
|
|
||||||
final class MarkerTool: DrawingElement {
|
final class MarkerTool: DrawingElement {
|
||||||
let uuid: UUID
|
let uuid: UUID
|
||||||
|
@ -2,164 +2,7 @@ import Foundation
|
|||||||
import UIKit
|
import UIKit
|
||||||
import QuartzCore
|
import QuartzCore
|
||||||
import simd
|
import simd
|
||||||
|
import MediaEditor
|
||||||
public struct DrawingColor: Equatable, Codable {
|
|
||||||
private enum CodingKeys: String, CodingKey {
|
|
||||||
case red
|
|
||||||
case green
|
|
||||||
case blue
|
|
||||||
case alpha
|
|
||||||
case position
|
|
||||||
}
|
|
||||||
|
|
||||||
public static var clear = DrawingColor(red: 0.0, green: 0.0, blue: 0.0, alpha: 0.0)
|
|
||||||
|
|
||||||
public var red: CGFloat
|
|
||||||
public var green: CGFloat
|
|
||||||
public var blue: CGFloat
|
|
||||||
public var alpha: CGFloat
|
|
||||||
|
|
||||||
public var position: CGPoint?
|
|
||||||
|
|
||||||
var isClear: Bool {
|
|
||||||
return self.red.isZero && self.green.isZero && self.blue.isZero && self.alpha.isZero
|
|
||||||
}
|
|
||||||
|
|
||||||
public init(
|
|
||||||
red: CGFloat,
|
|
||||||
green: CGFloat,
|
|
||||||
blue: CGFloat,
|
|
||||||
alpha: CGFloat = 1.0,
|
|
||||||
position: CGPoint? = nil
|
|
||||||
) {
|
|
||||||
self.red = red
|
|
||||||
self.green = green
|
|
||||||
self.blue = blue
|
|
||||||
self.alpha = alpha
|
|
||||||
self.position = position
|
|
||||||
}
|
|
||||||
|
|
||||||
public init(color: UIColor) {
|
|
||||||
var red: CGFloat = 0.0
|
|
||||||
var green: CGFloat = 0.0
|
|
||||||
var blue: CGFloat = 0.0
|
|
||||||
var alpha: CGFloat = 1.0
|
|
||||||
if color.getRed(&red, green: &green, blue: &blue, alpha: &alpha) {
|
|
||||||
self.init(red: red, green: green, blue: blue, alpha: alpha)
|
|
||||||
} else if color.getWhite(&red, alpha: &alpha) {
|
|
||||||
self.init(red: red, green: red, blue: red, alpha: alpha)
|
|
||||||
} else {
|
|
||||||
self.init(red: 0.0, green: 0.0, blue: 0.0)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public init(rgb: UInt32) {
|
|
||||||
self.init(color: UIColor(rgb: rgb))
|
|
||||||
}
|
|
||||||
|
|
||||||
public init(from decoder: Decoder) throws {
|
|
||||||
let container = try decoder.container(keyedBy: CodingKeys.self)
|
|
||||||
self.red = try container.decode(CGFloat.self, forKey: .red)
|
|
||||||
self.green = try container.decode(CGFloat.self, forKey: .green)
|
|
||||||
self.blue = try container.decode(CGFloat.self, forKey: .blue)
|
|
||||||
self.alpha = try container.decode(CGFloat.self, forKey: .alpha)
|
|
||||||
self.position = try container.decodeIfPresent(CGPoint.self, forKey: .position)
|
|
||||||
}
|
|
||||||
|
|
||||||
public func encode(to encoder: Encoder) throws {
|
|
||||||
var container = encoder.container(keyedBy: CodingKeys.self)
|
|
||||||
try container.encode(self.red, forKey: .red)
|
|
||||||
try container.encode(self.green, forKey: .green)
|
|
||||||
try container.encode(self.blue, forKey: .blue)
|
|
||||||
try container.encode(self.alpha, forKey: .alpha)
|
|
||||||
try container.encodeIfPresent(self.position, forKey: .position)
|
|
||||||
}
|
|
||||||
|
|
||||||
func withUpdatedRed(_ red: CGFloat) -> DrawingColor {
|
|
||||||
return DrawingColor(
|
|
||||||
red: red,
|
|
||||||
green: self.green,
|
|
||||||
blue: self.blue,
|
|
||||||
alpha: self.alpha
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
func withUpdatedGreen(_ green: CGFloat) -> DrawingColor {
|
|
||||||
return DrawingColor(
|
|
||||||
red: self.red,
|
|
||||||
green: green,
|
|
||||||
blue: self.blue,
|
|
||||||
alpha: self.alpha
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
func withUpdatedBlue(_ blue: CGFloat) -> DrawingColor {
|
|
||||||
return DrawingColor(
|
|
||||||
red: self.red,
|
|
||||||
green: self.green,
|
|
||||||
blue: blue,
|
|
||||||
alpha: self.alpha
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
func withUpdatedAlpha(_ alpha: CGFloat) -> DrawingColor {
|
|
||||||
return DrawingColor(
|
|
||||||
red: self.red,
|
|
||||||
green: self.green,
|
|
||||||
blue: self.blue,
|
|
||||||
alpha: alpha,
|
|
||||||
position: self.position
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
func withUpdatedPosition(_ position: CGPoint) -> DrawingColor {
|
|
||||||
return DrawingColor(
|
|
||||||
red: self.red,
|
|
||||||
green: self.green,
|
|
||||||
blue: self.blue,
|
|
||||||
alpha: self.alpha,
|
|
||||||
position: position
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
func toUIColor() -> UIColor {
|
|
||||||
return UIColor(
|
|
||||||
red: self.red,
|
|
||||||
green: self.green,
|
|
||||||
blue: self.blue,
|
|
||||||
alpha: self.alpha
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
func toCGColor() -> CGColor {
|
|
||||||
return self.toUIColor().cgColor
|
|
||||||
}
|
|
||||||
|
|
||||||
func toFloat4() -> vector_float4 {
|
|
||||||
return [
|
|
||||||
simd_float1(self.red),
|
|
||||||
simd_float1(self.green),
|
|
||||||
simd_float1(self.blue),
|
|
||||||
simd_float1(self.alpha)
|
|
||||||
]
|
|
||||||
}
|
|
||||||
|
|
||||||
public static func ==(lhs: DrawingColor, rhs: DrawingColor) -> Bool {
|
|
||||||
if lhs.red != rhs.red {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
if lhs.green != rhs.green {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
if lhs.blue != rhs.blue {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
if lhs.alpha != rhs.alpha {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
extension UIBezierPath {
|
extension UIBezierPath {
|
||||||
convenience init(roundRect rect: CGRect, topLeftRadius: CGFloat = 0.0, topRightRadius: CGFloat = 0.0, bottomLeftRadius: CGFloat = 0.0, bottomRightRadius: CGFloat = 0.0) {
|
convenience init(roundRect rect: CGRect, topLeftRadius: CGFloat = 0.0, topRightRadius: CGFloat = 0.0, bottomLeftRadius: CGFloat = 0.0, bottomRightRadius: CGFloat = 0.0) {
|
||||||
|
@ -2,133 +2,7 @@ import Foundation
|
|||||||
import UIKit
|
import UIKit
|
||||||
import Display
|
import Display
|
||||||
import AccountContext
|
import AccountContext
|
||||||
|
import MediaEditor
|
||||||
public final class DrawingVectorEntity: DrawingEntity, Codable {
|
|
||||||
private enum CodingKeys: String, CodingKey {
|
|
||||||
case uuid
|
|
||||||
case type
|
|
||||||
case color
|
|
||||||
case lineWidth
|
|
||||||
case drawingSize
|
|
||||||
case referenceDrawingSize
|
|
||||||
case start
|
|
||||||
case mid
|
|
||||||
case end
|
|
||||||
case renderImage
|
|
||||||
}
|
|
||||||
|
|
||||||
public enum VectorType: Codable {
|
|
||||||
case line
|
|
||||||
case oneSidedArrow
|
|
||||||
case twoSidedArrow
|
|
||||||
}
|
|
||||||
|
|
||||||
public let uuid: UUID
|
|
||||||
public let isAnimated: Bool
|
|
||||||
|
|
||||||
var type: VectorType
|
|
||||||
public var color: DrawingColor
|
|
||||||
public var lineWidth: CGFloat
|
|
||||||
|
|
||||||
public var drawingSize: CGSize
|
|
||||||
var referenceDrawingSize: CGSize
|
|
||||||
var start: CGPoint
|
|
||||||
var mid: (CGFloat, CGFloat)
|
|
||||||
var end: CGPoint
|
|
||||||
|
|
||||||
var _cachedMidPoint: (start: CGPoint, end: CGPoint, midLength: CGFloat, midHeight: CGFloat, midPoint: CGPoint)?
|
|
||||||
var midPoint: CGPoint {
|
|
||||||
if let (start, end, midLength, midHeight, midPoint) = self._cachedMidPoint, start == self.start, end == self.end, midLength == self.mid.0, midHeight == self.mid.1 {
|
|
||||||
return midPoint
|
|
||||||
} else {
|
|
||||||
let midPoint = midPointPositionFor(start: self.start, end: self.end, length: self.mid.0, height: self.mid.1)
|
|
||||||
self._cachedMidPoint = (self.start, self.end, self.mid.0, self.mid.1, midPoint)
|
|
||||||
return midPoint
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public var center: CGPoint {
|
|
||||||
return self.start
|
|
||||||
}
|
|
||||||
|
|
||||||
public var scale: CGFloat = 1.0
|
|
||||||
|
|
||||||
public var renderImage: UIImage?
|
|
||||||
|
|
||||||
public var isMedia: Bool {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
init(type: VectorType, color: DrawingColor, lineWidth: CGFloat) {
|
|
||||||
self.uuid = UUID()
|
|
||||||
self.isAnimated = false
|
|
||||||
|
|
||||||
self.type = type
|
|
||||||
self.color = color
|
|
||||||
self.lineWidth = lineWidth
|
|
||||||
|
|
||||||
self.drawingSize = .zero
|
|
||||||
self.referenceDrawingSize = .zero
|
|
||||||
self.start = CGPoint()
|
|
||||||
self.mid = (0.5, 0.0)
|
|
||||||
self.end = CGPoint()
|
|
||||||
}
|
|
||||||
|
|
||||||
public init(from decoder: Decoder) throws {
|
|
||||||
let container = try decoder.container(keyedBy: CodingKeys.self)
|
|
||||||
self.uuid = try container.decode(UUID.self, forKey: .uuid)
|
|
||||||
self.isAnimated = false
|
|
||||||
self.type = try container.decode(VectorType.self, forKey: .type)
|
|
||||||
self.color = try container.decode(DrawingColor.self, forKey: .color)
|
|
||||||
self.lineWidth = try container.decode(CGFloat.self, forKey: .lineWidth)
|
|
||||||
self.drawingSize = try container.decode(CGSize.self, forKey: .drawingSize)
|
|
||||||
self.referenceDrawingSize = try container.decode(CGSize.self, forKey: .referenceDrawingSize)
|
|
||||||
self.start = try container.decode(CGPoint.self, forKey: .start)
|
|
||||||
let mid = try container.decode(CGPoint.self, forKey: .mid)
|
|
||||||
self.mid = (mid.x, mid.y)
|
|
||||||
self.end = try container.decode(CGPoint.self, forKey: .end)
|
|
||||||
if let renderImageData = try? container.decodeIfPresent(Data.self, forKey: .renderImage) {
|
|
||||||
self.renderImage = UIImage(data: renderImageData)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public func encode(to encoder: Encoder) throws {
|
|
||||||
var container = encoder.container(keyedBy: CodingKeys.self)
|
|
||||||
try container.encode(self.uuid, forKey: .uuid)
|
|
||||||
try container.encode(self.type, forKey: .type)
|
|
||||||
try container.encode(self.color, forKey: .color)
|
|
||||||
try container.encode(self.lineWidth, forKey: .lineWidth)
|
|
||||||
try container.encode(self.drawingSize, forKey: .drawingSize)
|
|
||||||
try container.encode(self.referenceDrawingSize, forKey: .referenceDrawingSize)
|
|
||||||
try container.encode(self.start, forKey: .start)
|
|
||||||
try container.encode(CGPoint(x: self.mid.0, y: self.mid.1), forKey: .mid)
|
|
||||||
try container.encode(self.end, forKey: .end)
|
|
||||||
if let renderImage, let data = renderImage.pngData() {
|
|
||||||
try container.encode(data, forKey: .renderImage)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public func duplicate() -> DrawingEntity {
|
|
||||||
let newEntity = DrawingVectorEntity(type: self.type, color: self.color, lineWidth: self.lineWidth)
|
|
||||||
newEntity.drawingSize = self.drawingSize
|
|
||||||
newEntity.referenceDrawingSize = self.referenceDrawingSize
|
|
||||||
newEntity.start = self.start
|
|
||||||
newEntity.mid = self.mid
|
|
||||||
newEntity.end = self.end
|
|
||||||
return newEntity
|
|
||||||
}
|
|
||||||
|
|
||||||
public weak var currentEntityView: DrawingEntityView?
|
|
||||||
public func makeView(context: AccountContext) -> DrawingEntityView {
|
|
||||||
let entityView = DrawingVectorEntityView(context: context, entity: self)
|
|
||||||
self.currentEntityView = entityView
|
|
||||||
return entityView
|
|
||||||
}
|
|
||||||
|
|
||||||
public func prepareForRender() {
|
|
||||||
self.renderImage = (self.currentEntityView as? DrawingVectorEntityView)?.getRenderImage()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
final class DrawingVectorEntityView: DrawingEntityView {
|
final class DrawingVectorEntityView: DrawingEntityView {
|
||||||
private var vectorEntity: DrawingVectorEntity {
|
private var vectorEntity: DrawingVectorEntity {
|
||||||
@ -166,7 +40,7 @@ final class DrawingVectorEntityView: DrawingEntityView {
|
|||||||
self.shapeLayer.path = CGPath.curve(
|
self.shapeLayer.path = CGPath.curve(
|
||||||
start: self.vectorEntity.start,
|
start: self.vectorEntity.start,
|
||||||
end: self.vectorEntity.end,
|
end: self.vectorEntity.end,
|
||||||
mid: self.vectorEntity.midPoint,
|
mid: self.midPoint,
|
||||||
lineWidth: lineWidth,
|
lineWidth: lineWidth,
|
||||||
arrowSize: self.vectorEntity.type == .line ? nil : CGSize(width: lineWidth * 1.5, height: lineWidth * 3.0),
|
arrowSize: self.vectorEntity.type == .line ? nil : CGSize(width: lineWidth * 1.5, height: lineWidth * 3.0),
|
||||||
twoSided: self.vectorEntity.type == .twoSidedArrow
|
twoSided: self.vectorEntity.type == .twoSidedArrow
|
||||||
@ -198,7 +72,7 @@ final class DrawingVectorEntityView: DrawingEntityView {
|
|||||||
let expandedPath = CGPath.curve(
|
let expandedPath = CGPath.curve(
|
||||||
start: self.vectorEntity.start,
|
start: self.vectorEntity.start,
|
||||||
end: self.vectorEntity.end,
|
end: self.vectorEntity.end,
|
||||||
mid: self.vectorEntity.midPoint,
|
mid: self.midPoint,
|
||||||
lineWidth: self.maxLineWidth * 0.8,
|
lineWidth: self.maxLineWidth * 0.8,
|
||||||
arrowSize: nil,
|
arrowSize: nil,
|
||||||
twoSided: false
|
twoSided: false
|
||||||
@ -227,6 +101,18 @@ final class DrawingVectorEntityView: DrawingEntityView {
|
|||||||
UIGraphicsEndImageContext()
|
UIGraphicsEndImageContext()
|
||||||
return image
|
return image
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var _cachedMidPoint: (start: CGPoint, end: CGPoint, midLength: CGFloat, midHeight: CGFloat, midPoint: CGPoint)?
|
||||||
|
var midPoint: CGPoint {
|
||||||
|
let entity = self.vectorEntity
|
||||||
|
if let (start, end, midLength, midHeight, midPoint) = self._cachedMidPoint, start == entity.start, end == entity.end, midLength == entity.mid.0, midHeight == entity.mid.1 {
|
||||||
|
return midPoint
|
||||||
|
} else {
|
||||||
|
let midPoint = midPointPositionFor(start: entity.start, end: entity.end, length: entity.mid.0, height: entity.mid.1)
|
||||||
|
self._cachedMidPoint = (entity.start, entity.end, entity.mid.0, entity.mid.1, midPoint)
|
||||||
|
return midPoint
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private func midPointPositionFor(start: CGPoint, end: CGPoint, length: CGFloat, height: CGFloat) -> CGPoint {
|
private func midPointPositionFor(start: CGPoint, end: CGPoint, length: CGFloat, height: CGFloat) -> CGPoint {
|
||||||
@ -295,7 +181,7 @@ final class DrawingVectorEntititySelectionView: DrawingEntitySelectionView, UIGe
|
|||||||
|
|
||||||
private var currentHandle: CALayer?
|
private var currentHandle: CALayer?
|
||||||
@objc private func handlePan(_ gestureRecognizer: UIPanGestureRecognizer) {
|
@objc private func handlePan(_ gestureRecognizer: UIPanGestureRecognizer) {
|
||||||
guard let entityView = self.entityView, let entity = entityView.entity as? DrawingVectorEntity else {
|
guard let entityView = self.entityView as? DrawingVectorEntityView, let entity = entityView.entity as? DrawingVectorEntity else {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
let location = gestureRecognizer.location(in: self)
|
let location = gestureRecognizer.location(in: self)
|
||||||
@ -325,7 +211,7 @@ final class DrawingVectorEntititySelectionView: DrawingEntitySelectionView, UIGe
|
|||||||
updatedEnd.x += delta.x
|
updatedEnd.x += delta.x
|
||||||
updatedEnd.y += delta.y
|
updatedEnd.y += delta.y
|
||||||
} else if self.currentHandle === self.midHandle {
|
} else if self.currentHandle === self.midHandle {
|
||||||
var updatedMidPoint = entity.midPoint
|
var updatedMidPoint = entityView.midPoint
|
||||||
updatedMidPoint.x += delta.x
|
updatedMidPoint.x += delta.x
|
||||||
updatedMidPoint.y += delta.y
|
updatedMidPoint.y += delta.y
|
||||||
|
|
||||||
@ -356,7 +242,7 @@ final class DrawingVectorEntititySelectionView: DrawingEntitySelectionView, UIGe
|
|||||||
entity.start = updatedStart
|
entity.start = updatedStart
|
||||||
entity.mid = updatedMid
|
entity.mid = updatedMid
|
||||||
entity.end = updatedEnd
|
entity.end = updatedEnd
|
||||||
entityView.update()
|
entityView.update(animated: false)
|
||||||
|
|
||||||
gestureRecognizer.setTranslation(.zero, in: entityView)
|
gestureRecognizer.setTranslation(.zero, in: entityView)
|
||||||
case .ended:
|
case .ended:
|
||||||
@ -391,7 +277,7 @@ final class DrawingVectorEntititySelectionView: DrawingEntitySelectionView, UIGe
|
|||||||
self.startHandle.lineWidth = lineWidth
|
self.startHandle.lineWidth = lineWidth
|
||||||
|
|
||||||
self.midHandle.path = handlePath
|
self.midHandle.path = handlePath
|
||||||
self.midHandle.position = entity.midPoint
|
self.midHandle.position = entityView.midPoint
|
||||||
self.midHandle.bounds = bounds
|
self.midHandle.bounds = bounds
|
||||||
self.midHandle.lineWidth = lineWidth
|
self.midHandle.lineWidth = lineWidth
|
||||||
|
|
||||||
|
@ -6,6 +6,7 @@ import ComponentFlow
|
|||||||
import LegacyComponents
|
import LegacyComponents
|
||||||
import AppBundle
|
import AppBundle
|
||||||
import ImageBlur
|
import ImageBlur
|
||||||
|
import MediaEditor
|
||||||
|
|
||||||
protocol DrawingRenderLayer: CALayer {
|
protocol DrawingRenderLayer: CALayer {
|
||||||
|
|
||||||
|
@ -2,6 +2,7 @@ import Foundation
|
|||||||
import UIKit
|
import UIKit
|
||||||
import Display
|
import Display
|
||||||
import SwiftSignalKit
|
import SwiftSignalKit
|
||||||
|
import MediaEditor
|
||||||
|
|
||||||
private let size = CGSize(width: 148.0, height: 148.0)
|
private let size = CGSize(width: 148.0, height: 148.0)
|
||||||
private let outerWidth: CGFloat = 12.0
|
private let outerWidth: CGFloat = 12.0
|
||||||
|
@ -5,6 +5,7 @@ import ComponentFlow
|
|||||||
import LegacyComponents
|
import LegacyComponents
|
||||||
import TelegramCore
|
import TelegramCore
|
||||||
import LottieAnimationComponent
|
import LottieAnimationComponent
|
||||||
|
import MediaEditor
|
||||||
|
|
||||||
enum DrawingTextStyle: Equatable {
|
enum DrawingTextStyle: Equatable {
|
||||||
case regular
|
case regular
|
||||||
|
@ -30,6 +30,7 @@ swift_library(
|
|||||||
"//submodules/AttachmentUI:AttachmentUI",
|
"//submodules/AttachmentUI:AttachmentUI",
|
||||||
"//submodules/DrawingUI:DrawingUI",
|
"//submodules/DrawingUI:DrawingUI",
|
||||||
"//submodules/SolidRoundedButtonNode:SolidRoundedButtonNode",
|
"//submodules/SolidRoundedButtonNode:SolidRoundedButtonNode",
|
||||||
|
"//submodules/TelegramUI/Components/MediaEditor",
|
||||||
],
|
],
|
||||||
visibility = [
|
visibility = [
|
||||||
"//visibility:public",
|
"//visibility:public",
|
||||||
|
@ -10,6 +10,7 @@ import YuvConversion
|
|||||||
import StickerResources
|
import StickerResources
|
||||||
import DrawingUI
|
import DrawingUI
|
||||||
import SolidRoundedButtonNode
|
import SolidRoundedButtonNode
|
||||||
|
import MediaEditor
|
||||||
|
|
||||||
protocol LegacyPaintEntity {
|
protocol LegacyPaintEntity {
|
||||||
var position: CGPoint { get }
|
var position: CGPoint { get }
|
||||||
@ -432,7 +433,9 @@ public final class LegacyPaintEntityRenderer: NSObject, TGPhotoPaintEntityRender
|
|||||||
renderEntities.append(LegacyPaintTextEntity(entity: text))
|
renderEntities.append(LegacyPaintTextEntity(entity: text))
|
||||||
if let renderSubEntities = text.renderSubEntities, let account {
|
if let renderSubEntities = text.renderSubEntities, let account {
|
||||||
for entity in renderSubEntities {
|
for entity in renderSubEntities {
|
||||||
renderEntities.append(LegacyPaintStickerEntity(account: account, entity: entity))
|
if let entity = entity as? DrawingStickerEntity {
|
||||||
|
renderEntities.append(LegacyPaintStickerEntity(account: account, entity: entity))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else if let simpleShape = entity as? DrawingSimpleShapeEntity {
|
} else if let simpleShape = entity as? DrawingSimpleShapeEntity {
|
||||||
|
@ -497,9 +497,25 @@ public func peersNearbyController(context: AccountContext) -> ViewController {
|
|||||||
})
|
})
|
||||||
|
|
||||||
let dataSignal: Signal<PeersNearbyData?, NoError> = coordinatePromise.get()
|
let dataSignal: Signal<PeersNearbyData?, NoError> = coordinatePromise.get()
|
||||||
|
|> distinctUntilChanged(isEqual: { lhs, rhs in
|
||||||
|
return lhs?.latitude == rhs?.latitude && lhs?.longitude == rhs?.longitude
|
||||||
|
})
|
||||||
|> mapToSignal { coordinate -> Signal<PeersNearbyData?, NoError> in
|
|> mapToSignal { coordinate -> Signal<PeersNearbyData?, NoError> in
|
||||||
guard let coordinate = coordinate else {
|
guard let coordinate = coordinate else {
|
||||||
return .single(nil)
|
let peersNearbyContext = PeersNearbyContext(network: context.account.network, stateManager: context.account.stateManager, coordinate: nil)
|
||||||
|
return peersNearbyContext.get()
|
||||||
|
|> map { peersNearby -> PeersNearbyData in
|
||||||
|
var isVisible = false
|
||||||
|
if let peersNearby {
|
||||||
|
for peer in peersNearby {
|
||||||
|
if case .selfPeer = peer {
|
||||||
|
isVisible = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return PeersNearbyData(latitude: 0.0, longitude: 0.0, address: nil, visible: isVisible, accountPeerId: context.account.peerId, users: [], groups: [], channels: [])
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return Signal { subscriber in
|
return Signal { subscriber in
|
||||||
|
@ -14,7 +14,7 @@ func _internal_requestAccountPrivacySettings(account: Account) -> Signal<Account
|
|||||||
let phoneNumberPrivacy = account.network.request(Api.functions.account.getPrivacy(key: .inputPrivacyKeyPhoneNumber))
|
let phoneNumberPrivacy = account.network.request(Api.functions.account.getPrivacy(key: .inputPrivacyKeyPhoneNumber))
|
||||||
let phoneDiscoveryPrivacy = account.network.request(Api.functions.account.getPrivacy(key: .inputPrivacyKeyAddedByPhone))
|
let phoneDiscoveryPrivacy = account.network.request(Api.functions.account.getPrivacy(key: .inputPrivacyKeyAddedByPhone))
|
||||||
let voiceMessagesPrivacy = account.network.request(Api.functions.account.getPrivacy(key: .inputPrivacyKeyVoiceMessages))
|
let voiceMessagesPrivacy = account.network.request(Api.functions.account.getPrivacy(key: .inputPrivacyKeyVoiceMessages))
|
||||||
let bioPrivacy = account.network.request(Api.functions.account.getPrivacy(key: .inputPrivacyKeyProfilePhoto))
|
let bioPrivacy = account.network.request(Api.functions.account.getPrivacy(key: .inputPrivacyKeyAbout))
|
||||||
let autoremoveTimeout = account.network.request(Api.functions.account.getAccountTTL())
|
let autoremoveTimeout = account.network.request(Api.functions.account.getAccountTTL())
|
||||||
let globalPrivacySettings = account.network.request(Api.functions.account.getGlobalPrivacySettings())
|
let globalPrivacySettings = account.network.request(Api.functions.account.getGlobalPrivacySettings())
|
||||||
let messageAutoremoveTimeout = account.network.request(Api.functions.messages.getDefaultHistoryTTL())
|
let messageAutoremoveTimeout = account.network.request(Api.functions.messages.getDefaultHistoryTTL())
|
||||||
@ -253,7 +253,7 @@ public enum UpdateSelectiveAccountPrivacySettingsType {
|
|||||||
case .voiceMessages:
|
case .voiceMessages:
|
||||||
return .inputPrivacyKeyVoiceMessages
|
return .inputPrivacyKeyVoiceMessages
|
||||||
case .bio:
|
case .bio:
|
||||||
return .inputPrivacyKeyProfilePhoto
|
return .inputPrivacyKeyAbout
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -38,6 +38,7 @@ swift_library(
|
|||||||
"//submodules/StickerResources:StickerResources",
|
"//submodules/StickerResources:StickerResources",
|
||||||
"//submodules/TelegramUI/Components/AnimationCache:AnimationCache",
|
"//submodules/TelegramUI/Components/AnimationCache:AnimationCache",
|
||||||
"//submodules/TelegramUI/Components/EmojiTextAttachmentView:EmojiTextAttachmentView",
|
"//submodules/TelegramUI/Components/EmojiTextAttachmentView:EmojiTextAttachmentView",
|
||||||
|
"//submodules/TelegramUI/Components/MediaEditor",
|
||||||
],
|
],
|
||||||
visibility = [
|
visibility = [
|
||||||
"//visibility:public",
|
"//visibility:public",
|
||||||
|
@ -23,6 +23,7 @@ import DrawingUI
|
|||||||
import SolidRoundedButtonComponent
|
import SolidRoundedButtonComponent
|
||||||
import AnimationCache
|
import AnimationCache
|
||||||
import EmojiTextAttachmentView
|
import EmojiTextAttachmentView
|
||||||
|
import MediaEditor
|
||||||
|
|
||||||
enum AvatarBackground: Equatable {
|
enum AvatarBackground: Equatable {
|
||||||
case gradient([UInt32])
|
case gradient([UInt32])
|
||||||
|
@ -61,6 +61,11 @@ swift_library(
|
|||||||
"//submodules/TelegramPresentationData:TelegramPresentationData",
|
"//submodules/TelegramPresentationData:TelegramPresentationData",
|
||||||
"//submodules/AccountContext:AccountContext",
|
"//submodules/AccountContext:AccountContext",
|
||||||
"//submodules/AppBundle:AppBundle",
|
"//submodules/AppBundle:AppBundle",
|
||||||
|
"//submodules/TextFormat:TextFormat",
|
||||||
|
"//submodules/AnimatedStickerNode:AnimatedStickerNode",
|
||||||
|
"//submodules/TelegramAnimatedStickerNode:TelegramAnimatedStickerNode",
|
||||||
|
"//submodules/StickerResources:StickerResources",
|
||||||
|
"//submodules/YuvConversion:YuvConversion",
|
||||||
],
|
],
|
||||||
visibility = [
|
visibility = [
|
||||||
"//visibility:public",
|
"//visibility:public",
|
||||||
|
@ -0,0 +1,93 @@
|
|||||||
|
import Foundation
|
||||||
|
|
||||||
|
public enum CodableDrawingEntity {
|
||||||
|
case sticker(DrawingStickerEntity)
|
||||||
|
case text(DrawingTextEntity)
|
||||||
|
case simpleShape(DrawingSimpleShapeEntity)
|
||||||
|
case bubble(DrawingBubbleEntity)
|
||||||
|
case vector(DrawingVectorEntity)
|
||||||
|
|
||||||
|
public init?(entity: DrawingEntity) {
|
||||||
|
if let entity = entity as? DrawingStickerEntity {
|
||||||
|
self = .sticker(entity)
|
||||||
|
} else if let entity = entity as? DrawingTextEntity {
|
||||||
|
self = .text(entity)
|
||||||
|
} else if let entity = entity as? DrawingSimpleShapeEntity {
|
||||||
|
self = .simpleShape(entity)
|
||||||
|
} else if let entity = entity as? DrawingBubbleEntity {
|
||||||
|
self = .bubble(entity)
|
||||||
|
} else if let entity = entity as? DrawingVectorEntity {
|
||||||
|
self = .vector(entity)
|
||||||
|
} else {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public var entity: DrawingEntity {
|
||||||
|
switch self {
|
||||||
|
case let .sticker(entity):
|
||||||
|
return entity
|
||||||
|
case let .text(entity):
|
||||||
|
return entity
|
||||||
|
case let .simpleShape(entity):
|
||||||
|
return entity
|
||||||
|
case let .bubble(entity):
|
||||||
|
return entity
|
||||||
|
case let .vector(entity):
|
||||||
|
return entity
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension CodableDrawingEntity: Codable {
|
||||||
|
private enum CodingKeys: String, CodingKey {
|
||||||
|
case type
|
||||||
|
case entity
|
||||||
|
}
|
||||||
|
|
||||||
|
private enum EntityType: Int, Codable {
|
||||||
|
case sticker
|
||||||
|
case text
|
||||||
|
case simpleShape
|
||||||
|
case bubble
|
||||||
|
case vector
|
||||||
|
}
|
||||||
|
|
||||||
|
public init(from decoder: Decoder) throws {
|
||||||
|
let container = try decoder.container(keyedBy: CodingKeys.self)
|
||||||
|
let type = try container.decode(EntityType.self, forKey: .type)
|
||||||
|
switch type {
|
||||||
|
case .sticker:
|
||||||
|
self = .sticker(try container.decode(DrawingStickerEntity.self, forKey: .entity))
|
||||||
|
case .text:
|
||||||
|
self = .text(try container.decode(DrawingTextEntity.self, forKey: .entity))
|
||||||
|
case .simpleShape:
|
||||||
|
self = .simpleShape(try container.decode(DrawingSimpleShapeEntity.self, forKey: .entity))
|
||||||
|
case .bubble:
|
||||||
|
self = .bubble(try container.decode(DrawingBubbleEntity.self, forKey: .entity))
|
||||||
|
case .vector:
|
||||||
|
self = .vector(try container.decode(DrawingVectorEntity.self, forKey: .entity))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public func encode(to encoder: Encoder) throws {
|
||||||
|
var container = encoder.container(keyedBy: CodingKeys.self)
|
||||||
|
switch self {
|
||||||
|
case let .sticker(payload):
|
||||||
|
try container.encode(EntityType.sticker, forKey: .type)
|
||||||
|
try container.encode(payload, forKey: .entity)
|
||||||
|
case let .text(payload):
|
||||||
|
try container.encode(EntityType.text, forKey: .type)
|
||||||
|
try container.encode(payload, forKey: .entity)
|
||||||
|
case let .simpleShape(payload):
|
||||||
|
try container.encode(EntityType.simpleShape, forKey: .type)
|
||||||
|
try container.encode(payload, forKey: .entity)
|
||||||
|
case let .bubble(payload):
|
||||||
|
try container.encode(EntityType.bubble, forKey: .type)
|
||||||
|
try container.encode(payload, forKey: .entity)
|
||||||
|
case let .vector(payload):
|
||||||
|
try container.encode(EntityType.vector, forKey: .type)
|
||||||
|
try container.encode(payload, forKey: .entity)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,107 @@
|
|||||||
|
import Foundation
|
||||||
|
import UIKit
|
||||||
|
import Display
|
||||||
|
import AccountContext
|
||||||
|
|
||||||
|
public final class DrawingBubbleEntity: DrawingEntity, Codable {
|
||||||
|
private enum CodingKeys: String, CodingKey {
|
||||||
|
case uuid
|
||||||
|
case drawType
|
||||||
|
case color
|
||||||
|
case lineWidth
|
||||||
|
case referenceDrawingSize
|
||||||
|
case position
|
||||||
|
case size
|
||||||
|
case rotation
|
||||||
|
case tailPosition
|
||||||
|
case renderImage
|
||||||
|
}
|
||||||
|
|
||||||
|
public enum DrawType: Codable {
|
||||||
|
case fill
|
||||||
|
case stroke
|
||||||
|
}
|
||||||
|
|
||||||
|
public let uuid: UUID
|
||||||
|
public let isAnimated: Bool
|
||||||
|
|
||||||
|
public var drawType: DrawType
|
||||||
|
public var color: DrawingColor
|
||||||
|
public var lineWidth: CGFloat
|
||||||
|
|
||||||
|
public var referenceDrawingSize: CGSize
|
||||||
|
public var position: CGPoint
|
||||||
|
public var size: CGSize
|
||||||
|
public var rotation: CGFloat
|
||||||
|
public var tailPosition: CGPoint
|
||||||
|
|
||||||
|
public var center: CGPoint {
|
||||||
|
return self.position
|
||||||
|
}
|
||||||
|
|
||||||
|
public var scale: CGFloat = 1.0
|
||||||
|
|
||||||
|
public var renderImage: UIImage?
|
||||||
|
public var renderSubEntities: [DrawingEntity]?
|
||||||
|
|
||||||
|
public var isMedia: Bool {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
public init(drawType: DrawType, color: DrawingColor, lineWidth: CGFloat) {
|
||||||
|
self.uuid = UUID()
|
||||||
|
self.isAnimated = false
|
||||||
|
|
||||||
|
self.drawType = drawType
|
||||||
|
self.color = color
|
||||||
|
self.lineWidth = lineWidth
|
||||||
|
|
||||||
|
self.referenceDrawingSize = .zero
|
||||||
|
self.position = .zero
|
||||||
|
self.size = CGSize(width: 1.0, height: 1.0)
|
||||||
|
self.rotation = 0.0
|
||||||
|
self.tailPosition = CGPoint(x: 0.16, y: 0.18)
|
||||||
|
}
|
||||||
|
|
||||||
|
public init(from decoder: Decoder) throws {
|
||||||
|
let container = try decoder.container(keyedBy: CodingKeys.self)
|
||||||
|
self.uuid = try container.decode(UUID.self, forKey: .uuid)
|
||||||
|
self.isAnimated = false
|
||||||
|
self.drawType = try container.decode(DrawType.self, forKey: .drawType)
|
||||||
|
self.color = try container.decode(DrawingColor.self, forKey: .color)
|
||||||
|
self.lineWidth = try container.decode(CGFloat.self, forKey: .lineWidth)
|
||||||
|
self.referenceDrawingSize = try container.decode(CGSize.self, forKey: .referenceDrawingSize)
|
||||||
|
self.position = try container.decode(CGPoint.self, forKey: .position)
|
||||||
|
self.size = try container.decode(CGSize.self, forKey: .size)
|
||||||
|
self.rotation = try container.decode(CGFloat.self, forKey: .rotation)
|
||||||
|
self.tailPosition = try container.decode(CGPoint.self, forKey: .tailPosition)
|
||||||
|
if let renderImageData = try? container.decodeIfPresent(Data.self, forKey: .renderImage) {
|
||||||
|
self.renderImage = UIImage(data: renderImageData)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public func encode(to encoder: Encoder) throws {
|
||||||
|
var container = encoder.container(keyedBy: CodingKeys.self)
|
||||||
|
try container.encode(self.uuid, forKey: .uuid)
|
||||||
|
try container.encode(self.drawType, forKey: .drawType)
|
||||||
|
try container.encode(self.color, forKey: .color)
|
||||||
|
try container.encode(self.lineWidth, forKey: .lineWidth)
|
||||||
|
try container.encode(self.referenceDrawingSize, forKey: .referenceDrawingSize)
|
||||||
|
try container.encode(self.position, forKey: .position)
|
||||||
|
try container.encode(self.size, forKey: .size)
|
||||||
|
try container.encode(self.rotation, forKey: .rotation)
|
||||||
|
try container.encode(self.tailPosition, forKey: .tailPosition)
|
||||||
|
if let renderImage, let data = renderImage.pngData() {
|
||||||
|
try container.encode(data, forKey: .renderImage)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public func duplicate() -> DrawingEntity {
|
||||||
|
let newEntity = DrawingBubbleEntity(drawType: self.drawType, color: self.color, lineWidth: self.lineWidth)
|
||||||
|
newEntity.referenceDrawingSize = self.referenceDrawingSize
|
||||||
|
newEntity.position = self.position
|
||||||
|
newEntity.size = self.size
|
||||||
|
newEntity.rotation = self.rotation
|
||||||
|
return newEntity
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,161 @@
|
|||||||
|
import Foundation
|
||||||
|
import UIKit
|
||||||
|
import simd
|
||||||
|
|
||||||
|
public struct DrawingColor: Equatable, Codable {
|
||||||
|
private enum CodingKeys: String, CodingKey {
|
||||||
|
case red
|
||||||
|
case green
|
||||||
|
case blue
|
||||||
|
case alpha
|
||||||
|
case position
|
||||||
|
}
|
||||||
|
|
||||||
|
public static var clear = DrawingColor(red: 0.0, green: 0.0, blue: 0.0, alpha: 0.0)
|
||||||
|
|
||||||
|
public var red: CGFloat
|
||||||
|
public var green: CGFloat
|
||||||
|
public var blue: CGFloat
|
||||||
|
public var alpha: CGFloat
|
||||||
|
|
||||||
|
public var position: CGPoint?
|
||||||
|
|
||||||
|
public var isClear: Bool {
|
||||||
|
return self.red.isZero && self.green.isZero && self.blue.isZero && self.alpha.isZero
|
||||||
|
}
|
||||||
|
|
||||||
|
public init(
|
||||||
|
red: CGFloat,
|
||||||
|
green: CGFloat,
|
||||||
|
blue: CGFloat,
|
||||||
|
alpha: CGFloat = 1.0,
|
||||||
|
position: CGPoint? = nil
|
||||||
|
) {
|
||||||
|
self.red = red
|
||||||
|
self.green = green
|
||||||
|
self.blue = blue
|
||||||
|
self.alpha = alpha
|
||||||
|
self.position = position
|
||||||
|
}
|
||||||
|
|
||||||
|
public init(color: UIColor) {
|
||||||
|
var red: CGFloat = 0.0
|
||||||
|
var green: CGFloat = 0.0
|
||||||
|
var blue: CGFloat = 0.0
|
||||||
|
var alpha: CGFloat = 1.0
|
||||||
|
if color.getRed(&red, green: &green, blue: &blue, alpha: &alpha) {
|
||||||
|
self.init(red: red, green: green, blue: blue, alpha: alpha)
|
||||||
|
} else if color.getWhite(&red, alpha: &alpha) {
|
||||||
|
self.init(red: red, green: red, blue: red, alpha: alpha)
|
||||||
|
} else {
|
||||||
|
self.init(red: 0.0, green: 0.0, blue: 0.0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public init(rgb: UInt32) {
|
||||||
|
self.init(color: UIColor(rgb: rgb))
|
||||||
|
}
|
||||||
|
|
||||||
|
public init(from decoder: Decoder) throws {
|
||||||
|
let container = try decoder.container(keyedBy: CodingKeys.self)
|
||||||
|
self.red = try container.decode(CGFloat.self, forKey: .red)
|
||||||
|
self.green = try container.decode(CGFloat.self, forKey: .green)
|
||||||
|
self.blue = try container.decode(CGFloat.self, forKey: .blue)
|
||||||
|
self.alpha = try container.decode(CGFloat.self, forKey: .alpha)
|
||||||
|
self.position = try container.decodeIfPresent(CGPoint.self, forKey: .position)
|
||||||
|
}
|
||||||
|
|
||||||
|
public func encode(to encoder: Encoder) throws {
|
||||||
|
var container = encoder.container(keyedBy: CodingKeys.self)
|
||||||
|
try container.encode(self.red, forKey: .red)
|
||||||
|
try container.encode(self.green, forKey: .green)
|
||||||
|
try container.encode(self.blue, forKey: .blue)
|
||||||
|
try container.encode(self.alpha, forKey: .alpha)
|
||||||
|
try container.encodeIfPresent(self.position, forKey: .position)
|
||||||
|
}
|
||||||
|
|
||||||
|
public func withUpdatedRed(_ red: CGFloat) -> DrawingColor {
|
||||||
|
return DrawingColor(
|
||||||
|
red: red,
|
||||||
|
green: self.green,
|
||||||
|
blue: self.blue,
|
||||||
|
alpha: self.alpha
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
public func withUpdatedGreen(_ green: CGFloat) -> DrawingColor {
|
||||||
|
return DrawingColor(
|
||||||
|
red: self.red,
|
||||||
|
green: green,
|
||||||
|
blue: self.blue,
|
||||||
|
alpha: self.alpha
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
public func withUpdatedBlue(_ blue: CGFloat) -> DrawingColor {
|
||||||
|
return DrawingColor(
|
||||||
|
red: self.red,
|
||||||
|
green: self.green,
|
||||||
|
blue: blue,
|
||||||
|
alpha: self.alpha
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
public func withUpdatedAlpha(_ alpha: CGFloat) -> DrawingColor {
|
||||||
|
return DrawingColor(
|
||||||
|
red: self.red,
|
||||||
|
green: self.green,
|
||||||
|
blue: self.blue,
|
||||||
|
alpha: alpha,
|
||||||
|
position: self.position
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
public func withUpdatedPosition(_ position: CGPoint) -> DrawingColor {
|
||||||
|
return DrawingColor(
|
||||||
|
red: self.red,
|
||||||
|
green: self.green,
|
||||||
|
blue: self.blue,
|
||||||
|
alpha: self.alpha,
|
||||||
|
position: position
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
public func toUIColor() -> UIColor {
|
||||||
|
return UIColor(
|
||||||
|
red: self.red,
|
||||||
|
green: self.green,
|
||||||
|
blue: self.blue,
|
||||||
|
alpha: self.alpha
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
public func toCGColor() -> CGColor {
|
||||||
|
return self.toUIColor().cgColor
|
||||||
|
}
|
||||||
|
|
||||||
|
public func toFloat4() -> vector_float4 {
|
||||||
|
return [
|
||||||
|
simd_float1(self.red),
|
||||||
|
simd_float1(self.green),
|
||||||
|
simd_float1(self.blue),
|
||||||
|
simd_float1(self.alpha)
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
public static func ==(lhs: DrawingColor, rhs: DrawingColor) -> Bool {
|
||||||
|
if lhs.red != rhs.red {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if lhs.green != rhs.green {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if lhs.blue != rhs.blue {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if lhs.alpha != rhs.alpha {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,20 @@
|
|||||||
|
import Foundation
|
||||||
|
import UIKit
|
||||||
|
|
||||||
|
public protocol DrawingEntity: AnyObject {
|
||||||
|
var uuid: UUID { get }
|
||||||
|
var isAnimated: Bool { get }
|
||||||
|
var center: CGPoint { get }
|
||||||
|
|
||||||
|
var isMedia: Bool { get }
|
||||||
|
|
||||||
|
var lineWidth: CGFloat { get set }
|
||||||
|
var color: DrawingColor { get set }
|
||||||
|
|
||||||
|
var scale: CGFloat { get set }
|
||||||
|
|
||||||
|
func duplicate() -> DrawingEntity
|
||||||
|
|
||||||
|
var renderImage: UIImage? { get set }
|
||||||
|
var renderSubEntities: [DrawingEntity]? { get set }
|
||||||
|
}
|
@ -0,0 +1,146 @@
|
|||||||
|
import Foundation
|
||||||
|
import UIKit
|
||||||
|
import Display
|
||||||
|
import SwiftSignalKit
|
||||||
|
import TelegramCore
|
||||||
|
import AccountContext
|
||||||
|
import Photos
|
||||||
|
|
||||||
|
public final class DrawingMediaEntity: DrawingEntity, Codable {
|
||||||
|
public enum Content {
|
||||||
|
case image(UIImage, PixelDimensions)
|
||||||
|
case video(String, PixelDimensions)
|
||||||
|
case asset(PHAsset)
|
||||||
|
|
||||||
|
var dimensions: PixelDimensions {
|
||||||
|
switch self {
|
||||||
|
case let .image(_, dimensions), let .video(_, dimensions):
|
||||||
|
return dimensions
|
||||||
|
case let .asset(asset):
|
||||||
|
return PixelDimensions(width: Int32(asset.pixelWidth), height: Int32(asset.pixelHeight))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private enum CodingKeys: String, CodingKey {
|
||||||
|
case uuid
|
||||||
|
case image
|
||||||
|
case videoPath
|
||||||
|
case assetId
|
||||||
|
case size
|
||||||
|
case width
|
||||||
|
case height
|
||||||
|
case referenceDrawingSize
|
||||||
|
case position
|
||||||
|
case scale
|
||||||
|
case rotation
|
||||||
|
case mirrored
|
||||||
|
}
|
||||||
|
|
||||||
|
public let uuid: UUID
|
||||||
|
public let content: Content
|
||||||
|
public let size: CGSize
|
||||||
|
|
||||||
|
public var referenceDrawingSize: CGSize
|
||||||
|
public var position: CGPoint
|
||||||
|
public var scale: CGFloat
|
||||||
|
public var rotation: CGFloat
|
||||||
|
public var mirrored: Bool
|
||||||
|
|
||||||
|
public var color: DrawingColor = DrawingColor.clear
|
||||||
|
public var lineWidth: CGFloat = 0.0
|
||||||
|
|
||||||
|
public var center: CGPoint {
|
||||||
|
return self.position
|
||||||
|
}
|
||||||
|
|
||||||
|
public var baseSize: CGSize {
|
||||||
|
return self.size
|
||||||
|
}
|
||||||
|
|
||||||
|
public var isAnimated: Bool {
|
||||||
|
switch self.content {
|
||||||
|
case .image:
|
||||||
|
return false
|
||||||
|
case .video:
|
||||||
|
return true
|
||||||
|
case let .asset(asset):
|
||||||
|
return asset.mediaType == .video
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public var isMedia: Bool {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
public var renderImage: UIImage?
|
||||||
|
public var renderSubEntities: [DrawingEntity]?
|
||||||
|
|
||||||
|
public init(content: Content, size: CGSize) {
|
||||||
|
self.uuid = UUID()
|
||||||
|
self.content = content
|
||||||
|
self.size = size
|
||||||
|
|
||||||
|
self.referenceDrawingSize = .zero
|
||||||
|
self.position = CGPoint()
|
||||||
|
self.scale = 1.0
|
||||||
|
self.rotation = 0.0
|
||||||
|
self.mirrored = false
|
||||||
|
}
|
||||||
|
|
||||||
|
public init(from decoder: Decoder) throws {
|
||||||
|
let container = try decoder.container(keyedBy: CodingKeys.self)
|
||||||
|
self.uuid = try container.decode(UUID.self, forKey: .uuid)
|
||||||
|
self.size = try container.decode(CGSize.self, forKey: .size)
|
||||||
|
let width = try container.decode(Int32.self, forKey: .width)
|
||||||
|
let height = try container.decode(Int32.self, forKey: .height)
|
||||||
|
if let videoPath = try container.decodeIfPresent(String.self, forKey: .videoPath) {
|
||||||
|
self.content = .video(videoPath, PixelDimensions(width: width, height: height))
|
||||||
|
} else if let imageData = try container.decodeIfPresent(Data.self, forKey: .image), let image = UIImage(data: imageData) {
|
||||||
|
self.content = .image(image, PixelDimensions(width: width, height: height))
|
||||||
|
} else if let _ = try container.decodeIfPresent(String.self, forKey: .assetId) {
|
||||||
|
fatalError()
|
||||||
|
//self.content = .asset()
|
||||||
|
} else {
|
||||||
|
fatalError()
|
||||||
|
}
|
||||||
|
self.referenceDrawingSize = try container.decode(CGSize.self, forKey: .referenceDrawingSize)
|
||||||
|
self.position = try container.decode(CGPoint.self, forKey: .position)
|
||||||
|
self.scale = try container.decode(CGFloat.self, forKey: .scale)
|
||||||
|
self.rotation = try container.decode(CGFloat.self, forKey: .rotation)
|
||||||
|
self.mirrored = try container.decode(Bool.self, forKey: .mirrored)
|
||||||
|
}
|
||||||
|
|
||||||
|
public func encode(to encoder: Encoder) throws {
|
||||||
|
var container = encoder.container(keyedBy: CodingKeys.self)
|
||||||
|
try container.encode(self.uuid, forKey: .uuid)
|
||||||
|
switch self.content {
|
||||||
|
case let .video(videoPath, dimensions):
|
||||||
|
try container.encode(videoPath, forKey: .videoPath)
|
||||||
|
try container.encode(dimensions.width, forKey: .width)
|
||||||
|
try container.encode(dimensions.height, forKey: .height)
|
||||||
|
case let .image(image, dimensions):
|
||||||
|
try container.encodeIfPresent(image.jpegData(compressionQuality: 0.9), forKey: .image)
|
||||||
|
try container.encode(dimensions.width, forKey: .width)
|
||||||
|
try container.encode(dimensions.height, forKey: .height)
|
||||||
|
case let .asset(asset):
|
||||||
|
try container.encode(asset.localIdentifier, forKey: .assetId)
|
||||||
|
}
|
||||||
|
try container.encode(self.size, forKey: .size)
|
||||||
|
try container.encode(self.referenceDrawingSize, forKey: .referenceDrawingSize)
|
||||||
|
try container.encode(self.position, forKey: .position)
|
||||||
|
try container.encode(self.scale, forKey: .scale)
|
||||||
|
try container.encode(self.rotation, forKey: .rotation)
|
||||||
|
try container.encode(self.mirrored, forKey: .mirrored)
|
||||||
|
}
|
||||||
|
|
||||||
|
public func duplicate() -> DrawingEntity {
|
||||||
|
let newEntity = DrawingMediaEntity(content: self.content, size: self.size)
|
||||||
|
newEntity.referenceDrawingSize = self.referenceDrawingSize
|
||||||
|
newEntity.position = self.position
|
||||||
|
newEntity.scale = self.scale
|
||||||
|
newEntity.rotation = self.rotation
|
||||||
|
newEntity.mirrored = self.mirrored
|
||||||
|
return newEntity
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,113 @@
|
|||||||
|
import Foundation
|
||||||
|
import UIKit
|
||||||
|
import Display
|
||||||
|
import AccountContext
|
||||||
|
|
||||||
|
public final class DrawingSimpleShapeEntity: DrawingEntity, Codable {
|
||||||
|
private enum CodingKeys: String, CodingKey {
|
||||||
|
case uuid
|
||||||
|
case shapeType
|
||||||
|
case drawType
|
||||||
|
case color
|
||||||
|
case lineWidth
|
||||||
|
case referenceDrawingSize
|
||||||
|
case position
|
||||||
|
case size
|
||||||
|
case rotation
|
||||||
|
case renderImage
|
||||||
|
}
|
||||||
|
|
||||||
|
public enum ShapeType: Codable {
|
||||||
|
case rectangle
|
||||||
|
case ellipse
|
||||||
|
case star
|
||||||
|
}
|
||||||
|
|
||||||
|
public enum DrawType: Codable {
|
||||||
|
case fill
|
||||||
|
case stroke
|
||||||
|
}
|
||||||
|
|
||||||
|
public let uuid: UUID
|
||||||
|
public let isAnimated: Bool
|
||||||
|
|
||||||
|
public var shapeType: ShapeType
|
||||||
|
public var drawType: DrawType
|
||||||
|
public var color: DrawingColor
|
||||||
|
public var lineWidth: CGFloat
|
||||||
|
|
||||||
|
public var referenceDrawingSize: CGSize
|
||||||
|
public var position: CGPoint
|
||||||
|
public var size: CGSize
|
||||||
|
public var rotation: CGFloat
|
||||||
|
|
||||||
|
public var center: CGPoint {
|
||||||
|
return self.position
|
||||||
|
}
|
||||||
|
|
||||||
|
public var scale: CGFloat = 1.0
|
||||||
|
|
||||||
|
public var renderImage: UIImage?
|
||||||
|
public var renderSubEntities: [DrawingEntity]?
|
||||||
|
|
||||||
|
public var isMedia: Bool {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
public init(shapeType: ShapeType, drawType: DrawType, color: DrawingColor, lineWidth: CGFloat) {
|
||||||
|
self.uuid = UUID()
|
||||||
|
self.isAnimated = false
|
||||||
|
|
||||||
|
self.shapeType = shapeType
|
||||||
|
self.drawType = drawType
|
||||||
|
self.color = color
|
||||||
|
self.lineWidth = lineWidth
|
||||||
|
|
||||||
|
self.referenceDrawingSize = .zero
|
||||||
|
self.position = .zero
|
||||||
|
self.size = CGSize(width: 1.0, height: 1.0)
|
||||||
|
self.rotation = 0.0
|
||||||
|
}
|
||||||
|
|
||||||
|
public init(from decoder: Decoder) throws {
|
||||||
|
let container = try decoder.container(keyedBy: CodingKeys.self)
|
||||||
|
self.uuid = try container.decode(UUID.self, forKey: .uuid)
|
||||||
|
self.isAnimated = false
|
||||||
|
self.shapeType = try container.decode(ShapeType.self, forKey: .shapeType)
|
||||||
|
self.drawType = try container.decode(DrawType.self, forKey: .drawType)
|
||||||
|
self.color = try container.decode(DrawingColor.self, forKey: .color)
|
||||||
|
self.lineWidth = try container.decode(CGFloat.self, forKey: .lineWidth)
|
||||||
|
self.referenceDrawingSize = try container.decode(CGSize.self, forKey: .referenceDrawingSize)
|
||||||
|
self.position = try container.decode(CGPoint.self, forKey: .position)
|
||||||
|
self.size = try container.decode(CGSize.self, forKey: .size)
|
||||||
|
self.rotation = try container.decode(CGFloat.self, forKey: .rotation)
|
||||||
|
if let renderImageData = try? container.decodeIfPresent(Data.self, forKey: .renderImage) {
|
||||||
|
self.renderImage = UIImage(data: renderImageData)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public func encode(to encoder: Encoder) throws {
|
||||||
|
var container = encoder.container(keyedBy: CodingKeys.self)
|
||||||
|
try container.encode(self.uuid, forKey: .uuid)
|
||||||
|
try container.encode(self.shapeType, forKey: .shapeType)
|
||||||
|
try container.encode(self.drawType, forKey: .drawType)
|
||||||
|
try container.encode(self.color, forKey: .color)
|
||||||
|
try container.encode(self.lineWidth, forKey: .lineWidth)
|
||||||
|
try container.encode(self.referenceDrawingSize, forKey: .referenceDrawingSize)
|
||||||
|
try container.encode(self.position, forKey: .position)
|
||||||
|
try container.encode(self.size, forKey: .size)
|
||||||
|
try container.encode(self.rotation, forKey: .rotation)
|
||||||
|
if let renderImage, let data = renderImage.pngData() {
|
||||||
|
try container.encode(data, forKey: .renderImage)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public func duplicate() -> DrawingEntity {
|
||||||
|
let newEntity = DrawingSimpleShapeEntity(shapeType: self.shapeType, drawType: self.drawType, color: self.color, lineWidth: self.lineWidth)
|
||||||
|
newEntity.referenceDrawingSize = self.referenceDrawingSize
|
||||||
|
newEntity.position = self.position
|
||||||
|
newEntity.size = self.size
|
||||||
|
newEntity.rotation = self.rotation
|
||||||
|
return newEntity
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,113 @@
|
|||||||
|
import Foundation
|
||||||
|
import UIKit
|
||||||
|
import Display
|
||||||
|
import AccountContext
|
||||||
|
import TelegramCore
|
||||||
|
|
||||||
|
public final class DrawingStickerEntity: DrawingEntity, Codable {
|
||||||
|
public enum Content {
|
||||||
|
case file(TelegramMediaFile)
|
||||||
|
case image(UIImage)
|
||||||
|
}
|
||||||
|
private enum CodingKeys: String, CodingKey {
|
||||||
|
case uuid
|
||||||
|
case file
|
||||||
|
case image
|
||||||
|
case referenceDrawingSize
|
||||||
|
case position
|
||||||
|
case scale
|
||||||
|
case rotation
|
||||||
|
case mirrored
|
||||||
|
}
|
||||||
|
|
||||||
|
public let uuid: UUID
|
||||||
|
public let content: Content
|
||||||
|
|
||||||
|
public var referenceDrawingSize: CGSize
|
||||||
|
public var position: CGPoint
|
||||||
|
public var scale: CGFloat
|
||||||
|
public var rotation: CGFloat
|
||||||
|
public var mirrored: Bool
|
||||||
|
|
||||||
|
public var color: DrawingColor = DrawingColor.clear
|
||||||
|
public var lineWidth: CGFloat = 0.0
|
||||||
|
|
||||||
|
public var center: CGPoint {
|
||||||
|
return self.position
|
||||||
|
}
|
||||||
|
|
||||||
|
public var baseSize: CGSize {
|
||||||
|
let size = max(10.0, min(self.referenceDrawingSize.width, self.referenceDrawingSize.height) * 0.2)
|
||||||
|
return CGSize(width: size, height: size)
|
||||||
|
}
|
||||||
|
|
||||||
|
public var isAnimated: Bool {
|
||||||
|
switch self.content {
|
||||||
|
case let .file(file):
|
||||||
|
return file.isAnimatedSticker || file.isVideoSticker || file.mimeType == "video/webm"
|
||||||
|
case .image:
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public var isMedia: Bool {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
public var renderImage: UIImage?
|
||||||
|
public var renderSubEntities: [DrawingEntity]?
|
||||||
|
|
||||||
|
public init(content: Content) {
|
||||||
|
self.uuid = UUID()
|
||||||
|
self.content = content
|
||||||
|
|
||||||
|
self.referenceDrawingSize = .zero
|
||||||
|
self.position = CGPoint()
|
||||||
|
self.scale = 1.0
|
||||||
|
self.rotation = 0.0
|
||||||
|
self.mirrored = false
|
||||||
|
}
|
||||||
|
|
||||||
|
public init(from decoder: Decoder) throws {
|
||||||
|
let container = try decoder.container(keyedBy: CodingKeys.self)
|
||||||
|
self.uuid = try container.decode(UUID.self, forKey: .uuid)
|
||||||
|
if let file = try container.decodeIfPresent(TelegramMediaFile.self, forKey: .file) {
|
||||||
|
self.content = .file(file)
|
||||||
|
} else if let imageData = try container.decodeIfPresent(Data.self, forKey: .image), let image = UIImage(data: imageData) {
|
||||||
|
self.content = .image(image)
|
||||||
|
} else {
|
||||||
|
fatalError()
|
||||||
|
}
|
||||||
|
self.referenceDrawingSize = try container.decode(CGSize.self, forKey: .referenceDrawingSize)
|
||||||
|
self.position = try container.decode(CGPoint.self, forKey: .position)
|
||||||
|
self.scale = try container.decode(CGFloat.self, forKey: .scale)
|
||||||
|
self.rotation = try container.decode(CGFloat.self, forKey: .rotation)
|
||||||
|
self.mirrored = try container.decode(Bool.self, forKey: .mirrored)
|
||||||
|
}
|
||||||
|
|
||||||
|
public func encode(to encoder: Encoder) throws {
|
||||||
|
var container = encoder.container(keyedBy: CodingKeys.self)
|
||||||
|
try container.encode(self.uuid, forKey: .uuid)
|
||||||
|
switch self.content {
|
||||||
|
case let .file(file):
|
||||||
|
try container.encode(file, forKey: .file)
|
||||||
|
case let .image(image):
|
||||||
|
try container.encodeIfPresent(image.pngData(), forKey: .image)
|
||||||
|
}
|
||||||
|
try container.encode(self.referenceDrawingSize, forKey: .referenceDrawingSize)
|
||||||
|
try container.encode(self.position, forKey: .position)
|
||||||
|
try container.encode(self.scale, forKey: .scale)
|
||||||
|
try container.encode(self.rotation, forKey: .rotation)
|
||||||
|
try container.encode(self.mirrored, forKey: .mirrored)
|
||||||
|
}
|
||||||
|
|
||||||
|
public func duplicate() -> DrawingEntity {
|
||||||
|
let newEntity = DrawingStickerEntity(content: self.content)
|
||||||
|
newEntity.referenceDrawingSize = self.referenceDrawingSize
|
||||||
|
newEntity.position = self.position
|
||||||
|
newEntity.scale = self.scale
|
||||||
|
newEntity.rotation = self.rotation
|
||||||
|
newEntity.mirrored = self.mirrored
|
||||||
|
return newEntity
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,277 @@
|
|||||||
|
import Foundation
|
||||||
|
import UIKit
|
||||||
|
import Display
|
||||||
|
import AccountContext
|
||||||
|
import TextFormat
|
||||||
|
|
||||||
|
public final class DrawingTextEntity: DrawingEntity, Codable {
|
||||||
|
final class CustomEmojiAttribute: Codable {
|
||||||
|
private enum CodingKeys: String, CodingKey {
|
||||||
|
case attribute
|
||||||
|
case rangeOrigin
|
||||||
|
case rangeLength
|
||||||
|
}
|
||||||
|
let attribute: ChatTextInputTextCustomEmojiAttribute
|
||||||
|
let range: NSRange
|
||||||
|
|
||||||
|
init(attribute: ChatTextInputTextCustomEmojiAttribute, range: NSRange) {
|
||||||
|
self.attribute = attribute
|
||||||
|
self.range = range
|
||||||
|
}
|
||||||
|
|
||||||
|
init(from decoder: Decoder) throws {
|
||||||
|
let container = try decoder.container(keyedBy: CodingKeys.self)
|
||||||
|
self.attribute = try container.decode(ChatTextInputTextCustomEmojiAttribute.self, forKey: .attribute)
|
||||||
|
|
||||||
|
let rangeOrigin = try container.decode(Int.self, forKey: .rangeOrigin)
|
||||||
|
let rangeLength = try container.decode(Int.self, forKey: .rangeLength)
|
||||||
|
self.range = NSMakeRange(rangeOrigin, rangeLength)
|
||||||
|
}
|
||||||
|
|
||||||
|
func encode(to encoder: Encoder) throws {
|
||||||
|
var container = encoder.container(keyedBy: CodingKeys.self)
|
||||||
|
try container.encode(self.attribute, forKey: .attribute)
|
||||||
|
try container.encode(self.range.location, forKey: .rangeOrigin)
|
||||||
|
try container.encode(self.range.length, forKey: .rangeLength)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private enum CodingKeys: String, CodingKey {
|
||||||
|
case uuid
|
||||||
|
case text
|
||||||
|
case textAttributes
|
||||||
|
case style
|
||||||
|
case animation
|
||||||
|
case font
|
||||||
|
case alignment
|
||||||
|
case fontSize
|
||||||
|
case color
|
||||||
|
case referenceDrawingSize
|
||||||
|
case position
|
||||||
|
case width
|
||||||
|
case scale
|
||||||
|
case rotation
|
||||||
|
case renderImage
|
||||||
|
case renderSubEntities
|
||||||
|
case renderAnimationFrames
|
||||||
|
}
|
||||||
|
|
||||||
|
public enum Style: Codable {
|
||||||
|
case regular
|
||||||
|
case filled
|
||||||
|
case semi
|
||||||
|
case stroke
|
||||||
|
}
|
||||||
|
|
||||||
|
public enum Animation: Codable {
|
||||||
|
case none
|
||||||
|
case typing
|
||||||
|
case wiggle
|
||||||
|
case zoomIn
|
||||||
|
}
|
||||||
|
|
||||||
|
public enum Font: Codable {
|
||||||
|
case sanFrancisco
|
||||||
|
case other(String, String)
|
||||||
|
}
|
||||||
|
|
||||||
|
public enum Alignment: Codable {
|
||||||
|
case left
|
||||||
|
case center
|
||||||
|
case right
|
||||||
|
}
|
||||||
|
|
||||||
|
public var uuid: UUID
|
||||||
|
public var isAnimated: Bool {
|
||||||
|
if self.animation != .none {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
var isAnimated = false
|
||||||
|
self.text.enumerateAttributes(in: NSMakeRange(0, self.text.length), options: [], using: { attributes, range, _ in
|
||||||
|
if let _ = attributes[ChatTextInputAttributes.customEmoji] as? ChatTextInputTextCustomEmojiAttribute {
|
||||||
|
isAnimated = true
|
||||||
|
}
|
||||||
|
})
|
||||||
|
return isAnimated
|
||||||
|
}
|
||||||
|
|
||||||
|
public var text: NSAttributedString
|
||||||
|
public var style: Style
|
||||||
|
public var animation: Animation
|
||||||
|
public var font: Font
|
||||||
|
public var alignment: Alignment
|
||||||
|
public var fontSize: CGFloat
|
||||||
|
public var color: DrawingColor
|
||||||
|
public var lineWidth: CGFloat = 0.0
|
||||||
|
|
||||||
|
public var referenceDrawingSize: CGSize
|
||||||
|
public var position: CGPoint
|
||||||
|
public var width: CGFloat
|
||||||
|
public var scale: CGFloat
|
||||||
|
public var rotation: CGFloat
|
||||||
|
|
||||||
|
public var center: CGPoint {
|
||||||
|
return self.position
|
||||||
|
}
|
||||||
|
|
||||||
|
public var renderImage: UIImage?
|
||||||
|
public var renderSubEntities: [DrawingEntity]?
|
||||||
|
|
||||||
|
public var isMedia: Bool {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
public class AnimationFrame: Codable {
|
||||||
|
private enum CodingKeys: String, CodingKey {
|
||||||
|
case timestamp
|
||||||
|
case duration
|
||||||
|
case image
|
||||||
|
}
|
||||||
|
|
||||||
|
public let timestamp: Double
|
||||||
|
public let duration: Double
|
||||||
|
public let image: UIImage
|
||||||
|
|
||||||
|
public init(timestamp: Double, duration: Double, image: UIImage) {
|
||||||
|
self.timestamp = timestamp
|
||||||
|
self.duration = duration
|
||||||
|
self.image = image
|
||||||
|
}
|
||||||
|
|
||||||
|
required public init(from decoder: Decoder) throws {
|
||||||
|
let container = try decoder.container(keyedBy: CodingKeys.self)
|
||||||
|
self.timestamp = try container.decode(Double.self, forKey: .timestamp)
|
||||||
|
self.duration = try container.decode(Double.self, forKey: .duration)
|
||||||
|
if let renderImageData = try? container.decodeIfPresent(Data.self, forKey: .image) {
|
||||||
|
self.image = UIImage(data: renderImageData)!
|
||||||
|
} else {
|
||||||
|
fatalError()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public func encode(to encoder: Encoder) throws {
|
||||||
|
var container = encoder.container(keyedBy: CodingKeys.self)
|
||||||
|
|
||||||
|
try container.encode(self.timestamp, forKey: .timestamp)
|
||||||
|
try container.encode(self.duration, forKey: .duration)
|
||||||
|
if let data = self.image.pngData() {
|
||||||
|
try container.encode(data, forKey: .image)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
public var renderAnimationFrames: [AnimationFrame]?
|
||||||
|
|
||||||
|
public init(text: NSAttributedString, style: Style, animation: Animation, font: Font, alignment: Alignment, fontSize: CGFloat, color: DrawingColor) {
|
||||||
|
self.uuid = UUID()
|
||||||
|
|
||||||
|
self.text = text
|
||||||
|
self.style = style
|
||||||
|
self.animation = animation
|
||||||
|
self.font = font
|
||||||
|
self.alignment = alignment
|
||||||
|
self.fontSize = fontSize
|
||||||
|
self.color = color
|
||||||
|
|
||||||
|
self.referenceDrawingSize = .zero
|
||||||
|
self.position = .zero
|
||||||
|
self.width = 100.0
|
||||||
|
self.scale = 1.0
|
||||||
|
self.rotation = 0.0
|
||||||
|
}
|
||||||
|
|
||||||
|
public init(from decoder: Decoder) throws {
|
||||||
|
let container = try decoder.container(keyedBy: CodingKeys.self)
|
||||||
|
self.uuid = try container.decode(UUID.self, forKey: .uuid)
|
||||||
|
let text = try container.decode(String.self, forKey: .text)
|
||||||
|
|
||||||
|
let attributedString = NSMutableAttributedString(string: text)
|
||||||
|
let textAttributes = try container.decode([CustomEmojiAttribute].self, forKey: .textAttributes)
|
||||||
|
for attribute in textAttributes {
|
||||||
|
attributedString.addAttribute(ChatTextInputAttributes.customEmoji, value: attribute.attribute, range: attribute.range)
|
||||||
|
}
|
||||||
|
self.text = attributedString
|
||||||
|
|
||||||
|
self.style = try container.decode(Style.self, forKey: .style)
|
||||||
|
self.animation = try container.decode(Animation.self, forKey: .animation)
|
||||||
|
self.font = try container.decode(Font.self, forKey: .font)
|
||||||
|
self.alignment = try container.decode(Alignment.self, forKey: .alignment)
|
||||||
|
self.fontSize = try container.decode(CGFloat.self, forKey: .fontSize)
|
||||||
|
self.color = try container.decode(DrawingColor.self, forKey: .color)
|
||||||
|
self.referenceDrawingSize = try container.decode(CGSize.self, forKey: .referenceDrawingSize)
|
||||||
|
self.position = try container.decode(CGPoint.self, forKey: .position)
|
||||||
|
self.width = try container.decode(CGFloat.self, forKey: .width)
|
||||||
|
self.scale = try container.decode(CGFloat.self, forKey: .scale)
|
||||||
|
self.rotation = try container.decode(CGFloat.self, forKey: .rotation)
|
||||||
|
if let renderImageData = try? container.decodeIfPresent(Data.self, forKey: .renderImage) {
|
||||||
|
self.renderImage = UIImage(data: renderImageData)
|
||||||
|
}
|
||||||
|
if let renderSubEntities = try? container.decodeIfPresent([CodableDrawingEntity].self, forKey: .renderSubEntities) {
|
||||||
|
self.renderSubEntities = renderSubEntities.compactMap { $0.entity as? DrawingStickerEntity }
|
||||||
|
}
|
||||||
|
self.renderAnimationFrames = try container.decodeIfPresent([AnimationFrame].self, forKey: .renderAnimationFrames)
|
||||||
|
}
|
||||||
|
|
||||||
|
public func encode(to encoder: Encoder) throws {
|
||||||
|
var container = encoder.container(keyedBy: CodingKeys.self)
|
||||||
|
try container.encode(self.uuid, forKey: .uuid)
|
||||||
|
try container.encode(self.text.string, forKey: .text)
|
||||||
|
|
||||||
|
var textAttributes: [CustomEmojiAttribute] = []
|
||||||
|
self.text.enumerateAttributes(in: NSMakeRange(0, self.text.length), options: [], using: { attributes, range, _ in
|
||||||
|
if let value = attributes[ChatTextInputAttributes.customEmoji] as? ChatTextInputTextCustomEmojiAttribute {
|
||||||
|
textAttributes.append(CustomEmojiAttribute(attribute: value, range: range))
|
||||||
|
}
|
||||||
|
})
|
||||||
|
try container.encode(textAttributes, forKey: .textAttributes)
|
||||||
|
|
||||||
|
try container.encode(self.style, forKey: .style)
|
||||||
|
try container.encode(self.animation, forKey: .animation)
|
||||||
|
try container.encode(self.font, forKey: .font)
|
||||||
|
try container.encode(self.alignment, forKey: .alignment)
|
||||||
|
try container.encode(self.fontSize, forKey: .fontSize)
|
||||||
|
try container.encode(self.color, forKey: .color)
|
||||||
|
try container.encode(self.referenceDrawingSize, forKey: .referenceDrawingSize)
|
||||||
|
try container.encode(self.position, forKey: .position)
|
||||||
|
try container.encode(self.width, forKey: .width)
|
||||||
|
try container.encode(self.scale, forKey: .scale)
|
||||||
|
try container.encode(self.rotation, forKey: .rotation)
|
||||||
|
if let renderImage, let data = renderImage.pngData() {
|
||||||
|
try container.encode(data, forKey: .renderImage)
|
||||||
|
}
|
||||||
|
if let renderSubEntities = self.renderSubEntities {
|
||||||
|
let codableEntities: [CodableDrawingEntity] = renderSubEntities.compactMap { CodableDrawingEntity(entity: $0) }
|
||||||
|
try container.encode(codableEntities, forKey: .renderSubEntities)
|
||||||
|
}
|
||||||
|
if let renderAnimationFrames = self.renderAnimationFrames {
|
||||||
|
try container.encode(renderAnimationFrames, forKey: .renderAnimationFrames)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public func duplicate() -> DrawingEntity {
|
||||||
|
let newEntity = DrawingTextEntity(text: self.text, style: self.style, animation: self.animation, font: self.font, alignment: self.alignment, fontSize: self.fontSize, color: self.color)
|
||||||
|
newEntity.referenceDrawingSize = self.referenceDrawingSize
|
||||||
|
newEntity.position = self.position
|
||||||
|
newEntity.width = self.width
|
||||||
|
newEntity.scale = self.scale
|
||||||
|
newEntity.rotation = self.rotation
|
||||||
|
return newEntity
|
||||||
|
}
|
||||||
|
|
||||||
|
// public weak var currentEntityView: DrawingEntityView?
|
||||||
|
// public func makeView(context: AccountContext) -> DrawingEntityView {
|
||||||
|
// let entityView = DrawingTextEntityView(context: context, entity: self)
|
||||||
|
// self.currentEntityView = entityView
|
||||||
|
// return entityView
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// public func prepareForRender() {
|
||||||
|
// self.renderImage = (self.currentEntityView as? DrawingTextEntityView)?.getRenderImage()
|
||||||
|
// self.renderSubEntities = (self.currentEntityView as? DrawingTextEntityView)?.getRenderSubEntities()
|
||||||
|
//
|
||||||
|
// if case .none = self.animation {
|
||||||
|
// self.renderAnimationFrames = nil
|
||||||
|
// } else {
|
||||||
|
// self.renderAnimationFrames = (self.currentEntityView as? DrawingTextEntityView)?.getRenderAnimationFrames()
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
}
|
@ -0,0 +1,110 @@
|
|||||||
|
import Foundation
|
||||||
|
import UIKit
|
||||||
|
import Display
|
||||||
|
import AccountContext
|
||||||
|
|
||||||
|
public final class DrawingVectorEntity: DrawingEntity, Codable {
|
||||||
|
private enum CodingKeys: String, CodingKey {
|
||||||
|
case uuid
|
||||||
|
case type
|
||||||
|
case color
|
||||||
|
case lineWidth
|
||||||
|
case drawingSize
|
||||||
|
case referenceDrawingSize
|
||||||
|
case start
|
||||||
|
case mid
|
||||||
|
case end
|
||||||
|
case renderImage
|
||||||
|
}
|
||||||
|
|
||||||
|
public enum VectorType: Codable {
|
||||||
|
case line
|
||||||
|
case oneSidedArrow
|
||||||
|
case twoSidedArrow
|
||||||
|
}
|
||||||
|
|
||||||
|
public let uuid: UUID
|
||||||
|
public let isAnimated: Bool
|
||||||
|
|
||||||
|
public var type: VectorType
|
||||||
|
public var color: DrawingColor
|
||||||
|
public var lineWidth: CGFloat
|
||||||
|
|
||||||
|
public var drawingSize: CGSize
|
||||||
|
public var referenceDrawingSize: CGSize
|
||||||
|
public var start: CGPoint
|
||||||
|
public var mid: (CGFloat, CGFloat)
|
||||||
|
public var end: CGPoint
|
||||||
|
|
||||||
|
public var center: CGPoint {
|
||||||
|
return self.start
|
||||||
|
}
|
||||||
|
|
||||||
|
public var scale: CGFloat = 1.0
|
||||||
|
|
||||||
|
public var renderImage: UIImage?
|
||||||
|
public var renderSubEntities: [DrawingEntity]?
|
||||||
|
|
||||||
|
public var isMedia: Bool {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
public init(type: VectorType, color: DrawingColor, lineWidth: CGFloat) {
|
||||||
|
self.uuid = UUID()
|
||||||
|
self.isAnimated = false
|
||||||
|
|
||||||
|
self.type = type
|
||||||
|
self.color = color
|
||||||
|
self.lineWidth = lineWidth
|
||||||
|
|
||||||
|
self.drawingSize = .zero
|
||||||
|
self.referenceDrawingSize = .zero
|
||||||
|
self.start = CGPoint()
|
||||||
|
self.mid = (0.5, 0.0)
|
||||||
|
self.end = CGPoint()
|
||||||
|
}
|
||||||
|
|
||||||
|
public init(from decoder: Decoder) throws {
|
||||||
|
let container = try decoder.container(keyedBy: CodingKeys.self)
|
||||||
|
self.uuid = try container.decode(UUID.self, forKey: .uuid)
|
||||||
|
self.isAnimated = false
|
||||||
|
self.type = try container.decode(VectorType.self, forKey: .type)
|
||||||
|
self.color = try container.decode(DrawingColor.self, forKey: .color)
|
||||||
|
self.lineWidth = try container.decode(CGFloat.self, forKey: .lineWidth)
|
||||||
|
self.drawingSize = try container.decode(CGSize.self, forKey: .drawingSize)
|
||||||
|
self.referenceDrawingSize = try container.decode(CGSize.self, forKey: .referenceDrawingSize)
|
||||||
|
self.start = try container.decode(CGPoint.self, forKey: .start)
|
||||||
|
let mid = try container.decode(CGPoint.self, forKey: .mid)
|
||||||
|
self.mid = (mid.x, mid.y)
|
||||||
|
self.end = try container.decode(CGPoint.self, forKey: .end)
|
||||||
|
if let renderImageData = try? container.decodeIfPresent(Data.self, forKey: .renderImage) {
|
||||||
|
self.renderImage = UIImage(data: renderImageData)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public func encode(to encoder: Encoder) throws {
|
||||||
|
var container = encoder.container(keyedBy: CodingKeys.self)
|
||||||
|
try container.encode(self.uuid, forKey: .uuid)
|
||||||
|
try container.encode(self.type, forKey: .type)
|
||||||
|
try container.encode(self.color, forKey: .color)
|
||||||
|
try container.encode(self.lineWidth, forKey: .lineWidth)
|
||||||
|
try container.encode(self.drawingSize, forKey: .drawingSize)
|
||||||
|
try container.encode(self.referenceDrawingSize, forKey: .referenceDrawingSize)
|
||||||
|
try container.encode(self.start, forKey: .start)
|
||||||
|
try container.encode(CGPoint(x: self.mid.0, y: self.mid.1), forKey: .mid)
|
||||||
|
try container.encode(self.end, forKey: .end)
|
||||||
|
if let renderImage, let data = renderImage.pngData() {
|
||||||
|
try container.encode(data, forKey: .renderImage)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public func duplicate() -> DrawingEntity {
|
||||||
|
let newEntity = DrawingVectorEntity(type: self.type, color: self.color, lineWidth: self.lineWidth)
|
||||||
|
newEntity.drawingSize = self.drawingSize
|
||||||
|
newEntity.referenceDrawingSize = self.referenceDrawingSize
|
||||||
|
newEntity.start = self.start
|
||||||
|
newEntity.mid = self.mid
|
||||||
|
newEntity.end = self.end
|
||||||
|
return newEntity
|
||||||
|
}
|
||||||
|
}
|
@ -89,6 +89,7 @@ public final class MediaEditor {
|
|||||||
videoTrimRange: nil,
|
videoTrimRange: nil,
|
||||||
videoIsMuted: false,
|
videoIsMuted: false,
|
||||||
drawing: nil,
|
drawing: nil,
|
||||||
|
entities: [],
|
||||||
toolValues: [:]
|
toolValues: [:]
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@ -267,6 +268,10 @@ public final class MediaEditor {
|
|||||||
self.values = self.values.withUpdatedVideoIsMuted(videoIsMuted)
|
self.values = self.values.withUpdatedVideoIsMuted(videoIsMuted)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public func setDrawingAndEntities(data: Data?, image: UIImage?, entities: [CodableDrawingEntity]) {
|
||||||
|
self.values = self.values.withUpdatedDrawingAndEntities(drawing: image, entities: entities)
|
||||||
|
}
|
||||||
|
|
||||||
public func setGradientColors(_ gradientColors: [UIColor]) {
|
public func setGradientColors(_ gradientColors: [UIColor]) {
|
||||||
self.values = self.values.withUpdatedGradientColors(gradientColors: gradientColors)
|
self.values = self.values.withUpdatedGradientColors(gradientColors: gradientColors)
|
||||||
}
|
}
|
||||||
|
@ -4,6 +4,13 @@ import UIKit
|
|||||||
import CoreImage
|
import CoreImage
|
||||||
import Metal
|
import Metal
|
||||||
import Display
|
import Display
|
||||||
|
import SwiftSignalKit
|
||||||
|
import TelegramCore
|
||||||
|
import AnimatedStickerNode
|
||||||
|
import TelegramAnimatedStickerNode
|
||||||
|
import YuvConversion
|
||||||
|
import StickerResources
|
||||||
|
import AccountContext
|
||||||
|
|
||||||
final class MediaEditorComposer {
|
final class MediaEditorComposer {
|
||||||
let device: MTLDevice?
|
let device: MTLDevice?
|
||||||
@ -17,15 +24,14 @@ final class MediaEditorComposer {
|
|||||||
private let renderer = MediaEditorRenderer()
|
private let renderer = MediaEditorRenderer()
|
||||||
private let renderChain = MediaEditorRenderChain()
|
private let renderChain = MediaEditorRenderChain()
|
||||||
|
|
||||||
private var gradientImage: CIImage
|
private let gradientImage: CIImage
|
||||||
|
private let drawingImage: CIImage?
|
||||||
let semaphore = DispatchSemaphore(value: 1)
|
private var entities: [MediaEditorComposerEntity]
|
||||||
|
|
||||||
init(values: MediaEditorValues, dimensions: CGSize) {
|
init(context: AccountContext, values: MediaEditorValues, dimensions: CGSize) {
|
||||||
self.values = values
|
self.values = values
|
||||||
self.dimensions = dimensions
|
self.dimensions = dimensions
|
||||||
|
|
||||||
self.renderer.externalSemaphore = self.semaphore
|
|
||||||
self.renderer.addRenderChain(self.renderChain)
|
self.renderer.addRenderChain(self.renderChain)
|
||||||
self.renderer.addRenderPass(ComposerRenderPass())
|
self.renderer.addRenderPass(ComposerRenderPass())
|
||||||
|
|
||||||
@ -36,6 +42,14 @@ final class MediaEditorComposer {
|
|||||||
self.gradientImage = CIImage(color: .black)
|
self.gradientImage = CIImage(color: .black)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if let drawing = values.drawing, let drawingImage = CIImage(image: drawing) {
|
||||||
|
self.drawingImage = drawingImage.transformed(by: CGAffineTransform(translationX: -dimensions.width / 2.0, y: -dimensions.height / 2.0))
|
||||||
|
} else {
|
||||||
|
self.drawingImage = nil
|
||||||
|
}
|
||||||
|
|
||||||
|
self.entities = values.entities.map { $0.entity } .compactMap { composerEntityForDrawingEntity(context: context, entity: $0) }
|
||||||
|
|
||||||
self.device = MTLCreateSystemDefaultDevice()
|
self.device = MTLCreateSystemDefaultDevice()
|
||||||
if let device = self.device {
|
if let device = self.device {
|
||||||
self.ciContext = CIContext(mtlDevice: device, options: [.workingColorSpace : NSNull()])
|
self.ciContext = CIContext(mtlDevice: device, options: [.workingColorSpace : NSNull()])
|
||||||
@ -49,10 +63,13 @@ final class MediaEditorComposer {
|
|||||||
self.renderChain.update(values: self.values)
|
self.renderChain.update(values: self.values)
|
||||||
}
|
}
|
||||||
|
|
||||||
func processSampleBuffer(_ sampleBuffer: CMSampleBuffer, pool: CVPixelBufferPool?) -> CVPixelBuffer? {
|
func processSampleBuffer(_ sampleBuffer: CMSampleBuffer, pool: CVPixelBufferPool?, completion: @escaping (CVPixelBuffer?) -> Void) {
|
||||||
guard let textureCache = self.textureCache, let imageBuffer = CMSampleBufferGetImageBuffer(sampleBuffer), let pool = pool else {
|
guard let textureCache = self.textureCache, let imageBuffer = CMSampleBufferGetImageBuffer(sampleBuffer), let pool = pool else {
|
||||||
return nil
|
completion(nil)
|
||||||
|
return
|
||||||
}
|
}
|
||||||
|
let time = CMSampleBufferGetPresentationTimeStamp(sampleBuffer)
|
||||||
|
|
||||||
let width = CVPixelBufferGetWidth(imageBuffer)
|
let width = CVPixelBufferGetWidth(imageBuffer)
|
||||||
let height = CVPixelBufferGetHeight(imageBuffer)
|
let height = CVPixelBufferGetHeight(imageBuffer)
|
||||||
let format: MTLPixelFormat = .bgra8Unorm
|
let format: MTLPixelFormat = .bgra8Unorm
|
||||||
@ -62,7 +79,6 @@ final class MediaEditorComposer {
|
|||||||
if status == kCVReturnSuccess {
|
if status == kCVReturnSuccess {
|
||||||
texture = CVMetalTextureGetTexture(textureRef!)
|
texture = CVMetalTextureGetTexture(textureRef!)
|
||||||
}
|
}
|
||||||
|
|
||||||
if let texture {
|
if let texture {
|
||||||
self.renderer.consumeTexture(texture, rotation: .rotate90Degrees)
|
self.renderer.consumeTexture(texture, rotation: .rotate90Degrees)
|
||||||
self.renderer.renderFrame()
|
self.renderer.renderFrame()
|
||||||
@ -73,45 +89,59 @@ final class MediaEditorComposer {
|
|||||||
var pixelBuffer: CVPixelBuffer?
|
var pixelBuffer: CVPixelBuffer?
|
||||||
CVPixelBufferPoolCreatePixelBuffer(kCFAllocatorDefault, pool, &pixelBuffer)
|
CVPixelBufferPoolCreatePixelBuffer(kCFAllocatorDefault, pool, &pixelBuffer)
|
||||||
|
|
||||||
if let composition = processImage(inputImage: ciImage), let pixelBuffer {
|
if let pixelBuffer {
|
||||||
self.ciContext?.render(composition, to: pixelBuffer)
|
processImage(inputImage: ciImage, time: time, completion: { compositedImage in
|
||||||
|
if let compositedImage {
|
||||||
return pixelBuffer
|
self.ciContext?.render(compositedImage, to: pixelBuffer)
|
||||||
} else {
|
completion(pixelBuffer)
|
||||||
return nil
|
} else {
|
||||||
|
completion(nil)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
return
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
return nil
|
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
return nil
|
|
||||||
}
|
}
|
||||||
|
completion(nil)
|
||||||
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
func processImage(inputImage: CIImage) -> CIImage? {
|
func processImage(inputImage: CIImage, time: CMTime, completion: @escaping (CIImage?) -> Void) {
|
||||||
return makeEditorImageFrameComposition(inputImage: inputImage, gradientImage: self.gradientImage, dimensions: self.dimensions, values: self.values)
|
return makeEditorImageFrameComposition(inputImage: inputImage, gradientImage: self.gradientImage, drawingImage: self.drawingImage, dimensions: self.dimensions, values: self.values, entities: self.entities, time: time, completion: completion)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public func makeEditorImageComposition(inputImage: UIImage, dimensions: CGSize, values: MediaEditorValues) -> UIImage? {
|
public func makeEditorImageComposition(context: AccountContext, inputImage: UIImage, dimensions: CGSize, values: MediaEditorValues, time: CMTime, completion: @escaping (UIImage?) -> Void) {
|
||||||
let inputImage = CIImage(image: inputImage)!
|
let inputImage = CIImage(image: inputImage)!
|
||||||
let gradientImage: CIImage
|
let gradientImage: CIImage
|
||||||
|
var drawingImage: CIImage?
|
||||||
if let gradientColors = values.gradientColors {
|
if let gradientColors = values.gradientColors {
|
||||||
let image = generateGradientImage(size: dimensions, scale: 1.0, colors: gradientColors, locations: [0.0, 1.0])!
|
let image = generateGradientImage(size: dimensions, scale: 1.0, colors: gradientColors, locations: [0.0, 1.0])!
|
||||||
gradientImage = CIImage(image: image)!.transformed(by: CGAffineTransform(translationX: -dimensions.width / 2.0, y: -dimensions.height / 2.0))
|
gradientImage = CIImage(image: image)!.transformed(by: CGAffineTransform(translationX: -dimensions.width / 2.0, y: -dimensions.height / 2.0))
|
||||||
} else {
|
} else {
|
||||||
gradientImage = CIImage(color: .black)
|
gradientImage = CIImage(color: .black)
|
||||||
}
|
}
|
||||||
if let ciImage = makeEditorImageFrameComposition(inputImage: inputImage, gradientImage: gradientImage, dimensions: dimensions, values: values) {
|
|
||||||
let context = CIContext(options: [.workingColorSpace : NSNull()])
|
if let drawing = values.drawing, let image = CIImage(image: drawing) {
|
||||||
if let cgImage = context.createCGImage(ciImage, from: CGRect(origin: .zero, size: ciImage.extent.size)) {
|
drawingImage = image.transformed(by: CGAffineTransform(translationX: -dimensions.width / 2.0, y: -dimensions.height / 2.0))
|
||||||
return UIImage(cgImage: cgImage)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
return nil
|
|
||||||
|
let entities: [MediaEditorComposerEntity] = values.entities.map { $0.entity }.compactMap { composerEntityForDrawingEntity(context: context, entity: $0) }
|
||||||
|
makeEditorImageFrameComposition(inputImage: inputImage, gradientImage: gradientImage, drawingImage: drawingImage, dimensions: dimensions, values: values, entities: entities, time: time, completion: { ciImage in
|
||||||
|
if let ciImage {
|
||||||
|
let context = CIContext(options: [.workingColorSpace : NSNull()])
|
||||||
|
if let cgImage = context.createCGImage(ciImage, from: CGRect(origin: .zero, size: ciImage.extent.size)) {
|
||||||
|
Queue.mainQueue().async {
|
||||||
|
completion(UIImage(cgImage: cgImage))
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
completion(nil)
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
private func makeEditorImageFrameComposition(inputImage: CIImage, gradientImage: CIImage, dimensions: CGSize, values: MediaEditorValues) -> CIImage? {
|
private func makeEditorImageFrameComposition(inputImage: CIImage, gradientImage: CIImage, drawingImage: CIImage?, dimensions: CGSize, values: MediaEditorValues, entities: [MediaEditorComposerEntity], time: CMTime, completion: @escaping (CIImage?) -> Void) {
|
||||||
var resultImage = CIImage(color: .black).cropped(to: CGRect(origin: .zero, size: dimensions)).transformed(by: CGAffineTransform(translationX: -dimensions.width / 2.0, y: -dimensions.height / 2.0))
|
var resultImage = CIImage(color: .black).cropped(to: CGRect(origin: .zero, size: dimensions)).transformed(by: CGAffineTransform(translationX: -dimensions.width / 2.0, y: -dimensions.height / 2.0))
|
||||||
resultImage = gradientImage.composited(over: resultImage)
|
resultImage = gradientImage.composited(over: resultImage)
|
||||||
|
|
||||||
@ -121,32 +151,377 @@ private func makeEditorImageFrameComposition(inputImage: CIImage, gradientImage:
|
|||||||
cropTransform = cropTransform.rotated(by: -values.cropRotation)
|
cropTransform = cropTransform.rotated(by: -values.cropRotation)
|
||||||
cropTransform = cropTransform.scaledBy(x: values.cropScale, y: values.cropScale)
|
cropTransform = cropTransform.scaledBy(x: values.cropScale, y: values.cropScale)
|
||||||
mediaImage = mediaImage.transformed(by: cropTransform)
|
mediaImage = mediaImage.transformed(by: cropTransform)
|
||||||
|
|
||||||
resultImage = mediaImage.composited(over: resultImage)
|
resultImage = mediaImage.composited(over: resultImage)
|
||||||
|
|
||||||
return resultImage.transformed(by: CGAffineTransform(translationX: dimensions.width / 2.0, y: dimensions.height / 2.0))
|
if let drawingImage {
|
||||||
|
resultImage = drawingImage.composited(over: resultImage)
|
||||||
|
}
|
||||||
|
|
||||||
|
let frameRate: Float = 60.0
|
||||||
|
|
||||||
|
let entitiesCount = Atomic<Int>(value: 1)
|
||||||
|
let entitiesImages = Atomic<[(CIImage, Int)]>(value: [])
|
||||||
|
let maybeFinalize = {
|
||||||
|
let count = entitiesCount.modify { current -> Int in
|
||||||
|
return current - 1
|
||||||
|
}
|
||||||
|
if count == 0 {
|
||||||
|
let sortedImages = entitiesImages.with({ $0 }).sorted(by: { $0.1 < $1.1 }).map({ $0.0 })
|
||||||
|
for image in sortedImages {
|
||||||
|
resultImage = image.composited(over: resultImage)
|
||||||
|
}
|
||||||
|
|
||||||
|
resultImage = resultImage.transformed(by: CGAffineTransform(translationX: dimensions.width / 2.0, y: dimensions.height / 2.0))
|
||||||
|
completion(resultImage)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
var i = 0
|
||||||
|
for entity in entities {
|
||||||
|
let _ = entitiesCount.modify { current -> Int in
|
||||||
|
return current + 1
|
||||||
|
}
|
||||||
|
let index = i
|
||||||
|
entity.image(for: time, frameRate: frameRate, completion: { image in
|
||||||
|
if var image = image {
|
||||||
|
var transform = CGAffineTransform(translationX: -image.extent.midX, y: -image.extent.midY)
|
||||||
|
image = image.transformed(by: transform)
|
||||||
|
|
||||||
|
var scale = entity.scale * 1.0
|
||||||
|
if let baseSize = entity.baseSize {
|
||||||
|
scale *= baseSize.width / image.extent.size.width
|
||||||
|
}
|
||||||
|
|
||||||
|
transform = CGAffineTransform(translationX: entity.position.x, y: dimensions.height - entity.position.y)
|
||||||
|
transform = transform.rotated(by: CGFloat.pi * 2.0 - entity.rotation)
|
||||||
|
transform = transform.scaledBy(x: scale, y: scale)
|
||||||
|
if entity.mirrored {
|
||||||
|
transform = transform.scaledBy(x: -1.0, y: 1.0)
|
||||||
|
}
|
||||||
|
|
||||||
|
image = image.transformed(by: transform)
|
||||||
|
let _ = entitiesImages.modify { current in
|
||||||
|
var updated = current
|
||||||
|
updated.append((image, index))
|
||||||
|
return updated
|
||||||
|
}
|
||||||
|
}
|
||||||
|
maybeFinalize()
|
||||||
|
})
|
||||||
|
i += 1
|
||||||
|
}
|
||||||
|
maybeFinalize()
|
||||||
}
|
}
|
||||||
|
|
||||||
extension CMSampleBuffer {
|
private func composerEntityForDrawingEntity(context: AccountContext, entity: DrawingEntity) -> MediaEditorComposerEntity? {
|
||||||
func newSampleBufferWithReplacedImageBuffer(_ imageBuffer: CVImageBuffer) -> CMSampleBuffer? {
|
if let entity = entity as? DrawingStickerEntity {
|
||||||
guard let _ = CMSampleBufferGetImageBuffer(self) else {
|
let content: MediaEditorComposerStickerEntity.Content
|
||||||
return nil
|
switch entity.content {
|
||||||
|
case let .file(file):
|
||||||
|
content = .file(file)
|
||||||
|
case let .image(image):
|
||||||
|
content = .image(image)
|
||||||
}
|
}
|
||||||
var timingInfo = CMSampleTimingInfo()
|
return MediaEditorComposerStickerEntity(context: context, content: content, position: entity.position, scale: entity.scale, rotation: entity.rotation, baseSize: entity.baseSize, mirrored: entity.mirrored)
|
||||||
guard CMSampleBufferGetSampleTimingInfo(self, at: 0, timingInfoOut: &timingInfo) == 0 else {
|
} else if let renderImage = entity.renderImage, let image = CIImage(image: renderImage) {
|
||||||
return nil
|
if let entity = entity as? DrawingBubbleEntity {
|
||||||
|
return MediaEditorComposerStaticEntity(image: image, position: entity.position, scale: 1.0, rotation: entity.rotation, baseSize: entity.size, mirrored: false)
|
||||||
|
} else if let entity = entity as? DrawingSimpleShapeEntity {
|
||||||
|
return MediaEditorComposerStaticEntity(image: image, position: entity.position, scale: 1.0, rotation: entity.rotation, baseSize: entity.size, mirrored: false)
|
||||||
|
} else if let entity = entity as? DrawingVectorEntity {
|
||||||
|
return MediaEditorComposerStaticEntity(image: image, position: CGPoint(x: entity.drawingSize.width * 0.5, y: entity.drawingSize.height * 0.5), scale: 1.0, rotation: 0.0, baseSize: entity.drawingSize, mirrored: false)
|
||||||
|
} else if let entity = entity as? DrawingTextEntity {
|
||||||
|
return MediaEditorComposerStaticEntity(image: image, position: entity.position, scale: entity.scale, rotation: entity.rotation, baseSize: nil, mirrored: false)
|
||||||
}
|
}
|
||||||
var outputSampleBuffer: CMSampleBuffer?
|
}
|
||||||
var newFormatDescription: CMFormatDescription?
|
return nil
|
||||||
CMVideoFormatDescriptionCreateForImageBuffer(allocator: nil, imageBuffer: imageBuffer, formatDescriptionOut: &newFormatDescription)
|
}
|
||||||
guard let formatDescription = newFormatDescription else {
|
|
||||||
return nil
|
private class MediaEditorComposerStaticEntity: MediaEditorComposerEntity {
|
||||||
}
|
let image: CIImage
|
||||||
CMSampleBufferCreateReadyWithImageBuffer(allocator: nil, imageBuffer: imageBuffer, formatDescription: formatDescription, sampleTiming: &timingInfo, sampleBufferOut: &outputSampleBuffer)
|
let position: CGPoint
|
||||||
return outputSampleBuffer
|
let scale: CGFloat
|
||||||
|
let rotation: CGFloat
|
||||||
|
let baseSize: CGSize?
|
||||||
|
let mirrored: Bool
|
||||||
|
|
||||||
|
init(image: CIImage, position: CGPoint, scale: CGFloat, rotation: CGFloat, baseSize: CGSize?, mirrored: Bool) {
|
||||||
|
self.image = image
|
||||||
|
self.position = position
|
||||||
|
self.scale = scale
|
||||||
|
self.rotation = rotation
|
||||||
|
self.baseSize = baseSize
|
||||||
|
self.mirrored = mirrored
|
||||||
|
}
|
||||||
|
|
||||||
|
func image(for time: CMTime, frameRate: Float, completion: @escaping (CIImage?) -> Void) {
|
||||||
|
completion(self.image)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private class MediaEditorComposerStickerEntity: MediaEditorComposerEntity {
|
||||||
|
public enum Content {
|
||||||
|
case file(TelegramMediaFile)
|
||||||
|
case image(UIImage)
|
||||||
|
|
||||||
|
var file: TelegramMediaFile? {
|
||||||
|
if case let .file(file) = self {
|
||||||
|
return file
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let content: Content
|
||||||
|
let position: CGPoint
|
||||||
|
let scale: CGFloat
|
||||||
|
let rotation: CGFloat
|
||||||
|
let baseSize: CGSize?
|
||||||
|
let mirrored: Bool
|
||||||
|
|
||||||
|
var isAnimated: Bool
|
||||||
|
var source: AnimatedStickerNodeSource?
|
||||||
|
var frameSource = Promise<QueueLocalObject<AnimatedStickerDirectFrameSource>?>()
|
||||||
|
|
||||||
|
var frameCount: Int?
|
||||||
|
var frameRate: Int?
|
||||||
|
var currentFrameIndex: Int?
|
||||||
|
var totalDuration: Double?
|
||||||
|
let durationPromise = Promise<Double>()
|
||||||
|
|
||||||
|
let queue = Queue()
|
||||||
|
let disposables = DisposableSet()
|
||||||
|
|
||||||
|
var image: CIImage?
|
||||||
|
var imagePixelBuffer: CVPixelBuffer?
|
||||||
|
let imagePromise = Promise<UIImage>()
|
||||||
|
|
||||||
|
init(context: AccountContext, content: Content, position: CGPoint, scale: CGFloat, rotation: CGFloat, baseSize: CGSize, mirrored: Bool) {
|
||||||
|
self.content = content
|
||||||
|
self.position = position
|
||||||
|
self.scale = scale
|
||||||
|
self.rotation = rotation
|
||||||
|
self.baseSize = baseSize
|
||||||
|
self.mirrored = mirrored
|
||||||
|
|
||||||
|
switch content {
|
||||||
|
case let .file(file):
|
||||||
|
if file.isAnimatedSticker || file.isVideoSticker || file.mimeType == "video/webm" {
|
||||||
|
self.isAnimated = true
|
||||||
|
self.source = AnimatedStickerResourceSource(account: context.account, resource: file.resource, isVideo: file.isVideoSticker || file.mimeType == "video/webm")
|
||||||
|
let pathPrefix = context.account.postbox.mediaBox.shortLivedResourceCachePathPrefix(file.resource.id)
|
||||||
|
if let source = self.source {
|
||||||
|
let dimensions = file.dimensions ?? PixelDimensions(width: 512, height: 512)
|
||||||
|
let fittedDimensions = dimensions.cgSize.aspectFitted(CGSize(width: 384, height: 384))
|
||||||
|
self.disposables.add((source.directDataPath(attemptSynchronously: true)
|
||||||
|
|> deliverOn(self.queue)).start(next: { [weak self] path in
|
||||||
|
if let strongSelf = self, let path {
|
||||||
|
if let data = try? Data(contentsOf: URL(fileURLWithPath: path), options: [.mappedRead]) {
|
||||||
|
let queue = strongSelf.queue
|
||||||
|
let frameSource = QueueLocalObject<AnimatedStickerDirectFrameSource>(queue: queue, generate: {
|
||||||
|
return AnimatedStickerDirectFrameSource(queue: queue, data: data, width: Int(fittedDimensions.width), height: Int(fittedDimensions.height), cachePathPrefix: pathPrefix, useMetalCache: false, fitzModifier: nil)!
|
||||||
|
//return AnimatedStickerCachedFrameSource(queue: queue, data: data, complete: complete, notifyUpdated: {})!
|
||||||
|
})
|
||||||
|
frameSource.syncWith { frameSource in
|
||||||
|
strongSelf.frameCount = frameSource.frameCount
|
||||||
|
strongSelf.frameRate = frameSource.frameRate
|
||||||
|
|
||||||
|
let duration = Double(frameSource.frameCount) / Double(frameSource.frameRate)
|
||||||
|
strongSelf.totalDuration = duration
|
||||||
|
strongSelf.durationPromise.set(.single(duration))
|
||||||
|
}
|
||||||
|
|
||||||
|
strongSelf.frameSource.set(.single(frameSource))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
self.isAnimated = false
|
||||||
|
self.disposables.add((chatMessageSticker(account: context.account, userLocation: .other, file: file, small: false, fetched: true, onlyFullSize: true, thumbnail: false, synchronousLoad: false)
|
||||||
|
|> deliverOn(self.queue)).start(next: { [weak self] generator in
|
||||||
|
if let strongSelf = self {
|
||||||
|
let context = generator(TransformImageArguments(corners: ImageCorners(), imageSize: baseSize, boundingSize: baseSize, intrinsicInsets: UIEdgeInsets()))
|
||||||
|
let image = context?.generateImage()
|
||||||
|
if let image = image {
|
||||||
|
strongSelf.imagePromise.set(.single(image))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
case let .image(image):
|
||||||
|
self.isAnimated = false
|
||||||
|
self.imagePromise.set(.single(image))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
deinit {
|
||||||
|
self.disposables.dispose()
|
||||||
|
}
|
||||||
|
|
||||||
|
var tested = false
|
||||||
|
func image(for time: CMTime, frameRate: Float, completion: @escaping (CIImage?) -> Void) {
|
||||||
|
if self.isAnimated {
|
||||||
|
let currentTime = CMTimeGetSeconds(time)
|
||||||
|
|
||||||
|
var tintColor: UIColor?
|
||||||
|
if let file = self.content.file, file.isCustomTemplateEmoji {
|
||||||
|
tintColor = .white
|
||||||
|
}
|
||||||
|
|
||||||
|
// let start = CACurrentMediaTime()
|
||||||
|
self.disposables.add((self.frameSource.get()
|
||||||
|
|> take(1)
|
||||||
|
|> deliverOn(self.queue)).start(next: { [weak self] frameSource in
|
||||||
|
guard let strongSelf = self else {
|
||||||
|
completion(nil)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
guard let frameSource, let duration = strongSelf.totalDuration, let frameCount = strongSelf.frameCount else {
|
||||||
|
completion(nil)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// if !strongSelf.tested {
|
||||||
|
// frameSource.syncWith { frameSource in
|
||||||
|
// for _ in 0 ..< 60 * 3 {
|
||||||
|
// let _ = frameSource.takeFrame(draw: true)
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// strongSelf.tested = true
|
||||||
|
// print("180 frames in \(CACurrentMediaTime() - start)")
|
||||||
|
// }
|
||||||
|
|
||||||
|
let relativeTime = currentTime - floor(currentTime / duration) * duration
|
||||||
|
var t = relativeTime / duration
|
||||||
|
t = max(0.0, t)
|
||||||
|
t = min(1.0, t)
|
||||||
|
|
||||||
|
let startFrame: Double = 0
|
||||||
|
let endFrame = Double(frameCount)
|
||||||
|
|
||||||
|
let frameOffset = Int(Double(startFrame) * (1.0 - t) + Double(endFrame - 1) * t)
|
||||||
|
let lowerBound: Int = 0
|
||||||
|
let upperBound = frameCount - 1
|
||||||
|
let frameIndex = max(lowerBound, min(upperBound, frameOffset))
|
||||||
|
|
||||||
|
let currentFrameIndex = strongSelf.currentFrameIndex
|
||||||
|
if currentFrameIndex != frameIndex {
|
||||||
|
let previousFrameIndex = currentFrameIndex
|
||||||
|
strongSelf.currentFrameIndex = frameIndex
|
||||||
|
|
||||||
|
var delta = 1
|
||||||
|
if let previousFrameIndex = previousFrameIndex {
|
||||||
|
delta = max(1, frameIndex - previousFrameIndex)
|
||||||
|
}
|
||||||
|
|
||||||
|
//print("skipping: \(delta) frames")
|
||||||
|
|
||||||
|
|
||||||
|
var frame: AnimatedStickerFrame?
|
||||||
|
frameSource.syncWith { frameSource in
|
||||||
|
for i in 0 ..< delta {
|
||||||
|
frame = frameSource.takeFrame(draw: i == delta - 1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if let frame {
|
||||||
|
//print("has frame: \(CACurrentMediaTime() - start)")
|
||||||
|
|
||||||
|
var imagePixelBuffer: CVPixelBuffer?
|
||||||
|
if let pixelBuffer = strongSelf.imagePixelBuffer {
|
||||||
|
imagePixelBuffer = pixelBuffer
|
||||||
|
} else {
|
||||||
|
let ioSurfaceProperties = NSMutableDictionary()
|
||||||
|
let options = NSMutableDictionary()
|
||||||
|
options.setObject(ioSurfaceProperties, forKey: kCVPixelBufferIOSurfacePropertiesKey as NSString)
|
||||||
|
|
||||||
|
var pixelBuffer: CVPixelBuffer?
|
||||||
|
CVPixelBufferCreate(
|
||||||
|
kCFAllocatorDefault,
|
||||||
|
frame.width,
|
||||||
|
frame.height,
|
||||||
|
kCVPixelFormatType_32BGRA,
|
||||||
|
options,
|
||||||
|
&pixelBuffer
|
||||||
|
)
|
||||||
|
|
||||||
|
imagePixelBuffer = pixelBuffer
|
||||||
|
strongSelf.imagePixelBuffer = pixelBuffer
|
||||||
|
}
|
||||||
|
|
||||||
|
if let imagePixelBuffer {
|
||||||
|
let image = render(width: frame.width, height: frame.height, bytesPerRow: frame.bytesPerRow, data: frame.data, type: frame.type, pixelBuffer: imagePixelBuffer, tintColor: tintColor)
|
||||||
|
//print("image loaded in: \(CACurrentMediaTime() - start)")
|
||||||
|
strongSelf.image = image
|
||||||
|
}
|
||||||
|
completion(strongSelf.image)
|
||||||
|
} else {
|
||||||
|
completion(nil)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
completion(strongSelf.image)
|
||||||
|
}
|
||||||
|
}))
|
||||||
|
} else {
|
||||||
|
var image: CIImage?
|
||||||
|
if let cachedImage = self.image {
|
||||||
|
image = cachedImage
|
||||||
|
completion(image)
|
||||||
|
} else {
|
||||||
|
let _ = (self.imagePromise.get()
|
||||||
|
|> take(1)
|
||||||
|
|> deliverOn(self.queue)).start(next: { [weak self] image in
|
||||||
|
if let strongSelf = self {
|
||||||
|
strongSelf.image = CIImage(image: image)
|
||||||
|
completion(strongSelf.image)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protocol MediaEditorComposerEntity {
|
||||||
|
var position: CGPoint { get }
|
||||||
|
var scale: CGFloat { get }
|
||||||
|
var rotation: CGFloat { get }
|
||||||
|
var baseSize: CGSize? { get }
|
||||||
|
var mirrored: Bool { get }
|
||||||
|
|
||||||
|
func image(for time: CMTime, frameRate: Float, completion: @escaping (CIImage?) -> Void)
|
||||||
|
}
|
||||||
|
|
||||||
|
private func render(width: Int, height: Int, bytesPerRow: Int, data: Data, type: AnimationRendererFrameType, pixelBuffer: CVPixelBuffer, tintColor: UIColor?) -> CIImage? {
|
||||||
|
//let calculatedBytesPerRow = (4 * Int(width) + 31) & (~31)
|
||||||
|
//assert(bytesPerRow == calculatedBytesPerRow)
|
||||||
|
|
||||||
|
|
||||||
|
CVPixelBufferLockBaseAddress(pixelBuffer, CVPixelBufferLockFlags(rawValue: 0))
|
||||||
|
let dest = CVPixelBufferGetBaseAddress(pixelBuffer)
|
||||||
|
|
||||||
|
switch type {
|
||||||
|
case .yuva:
|
||||||
|
data.withUnsafeBytes { buffer -> Void in
|
||||||
|
guard let bytes = buffer.baseAddress?.assumingMemoryBound(to: UInt8.self) else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
decodeYUVAToRGBA(bytes, dest, Int32(width), Int32(height), Int32(bytesPerRow))
|
||||||
|
}
|
||||||
|
case .argb:
|
||||||
|
data.withUnsafeBytes { buffer -> Void in
|
||||||
|
guard let bytes = buffer.baseAddress?.assumingMemoryBound(to: UInt8.self) else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
memcpy(dest, bytes, data.count)
|
||||||
|
}
|
||||||
|
case .dct:
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
CVPixelBufferUnlockBaseAddress(pixelBuffer, CVPixelBufferLockFlags(rawValue: 0))
|
||||||
|
|
||||||
|
return CIImage(cvPixelBuffer: pixelBuffer)
|
||||||
|
}
|
||||||
|
|
||||||
final class ComposerRenderPass: DefaultRenderPass {
|
final class ComposerRenderPass: DefaultRenderPass {
|
||||||
fileprivate var cachedTexture: MTLTexture?
|
fileprivate var cachedTexture: MTLTexture?
|
||||||
|
@ -65,9 +65,7 @@ final class MediaEditorRenderer: TextureConsumer {
|
|||||||
private var library: MTLLibrary?
|
private var library: MTLLibrary?
|
||||||
|
|
||||||
var finalTexture: MTLTexture?
|
var finalTexture: MTLTexture?
|
||||||
|
|
||||||
var externalSemaphore: DispatchSemaphore?
|
|
||||||
|
|
||||||
public init() {
|
public init() {
|
||||||
|
|
||||||
}
|
}
|
||||||
@ -175,7 +173,6 @@ final class MediaEditorRenderer: TextureConsumer {
|
|||||||
|
|
||||||
commandBuffer.addCompletedHandler { [weak self] _ in
|
commandBuffer.addCompletedHandler { [weak self] _ in
|
||||||
self?.semaphore.signal()
|
self?.semaphore.signal()
|
||||||
self?.externalSemaphore?.signal()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if let _ = self.renderTarget {
|
if let _ = self.renderTarget {
|
||||||
|
@ -34,6 +34,72 @@ private let adjustmentToolsKeys: [EditorToolKey] = [
|
|||||||
.sharpen
|
.sharpen
|
||||||
]
|
]
|
||||||
|
|
||||||
|
public class MediaEditorValues {
|
||||||
|
public let originalDimensions: PixelDimensions
|
||||||
|
public let cropOffset: CGPoint
|
||||||
|
public let cropSize: CGSize?
|
||||||
|
public let cropScale: CGFloat
|
||||||
|
public let cropRotation: CGFloat
|
||||||
|
public let cropMirroring: Bool
|
||||||
|
|
||||||
|
public let gradientColors: [UIColor]?
|
||||||
|
|
||||||
|
public let videoTrimRange: Range<Double>?
|
||||||
|
public let videoIsMuted: Bool
|
||||||
|
|
||||||
|
public let drawing: UIImage?
|
||||||
|
public let entities: [CodableDrawingEntity]
|
||||||
|
public let toolValues: [EditorToolKey: Any]
|
||||||
|
|
||||||
|
init(
|
||||||
|
originalDimensions: PixelDimensions,
|
||||||
|
cropOffset: CGPoint,
|
||||||
|
cropSize: CGSize?,
|
||||||
|
cropScale: CGFloat,
|
||||||
|
cropRotation: CGFloat,
|
||||||
|
cropMirroring: Bool,
|
||||||
|
gradientColors: [UIColor]?,
|
||||||
|
videoTrimRange: Range<Double>?,
|
||||||
|
videoIsMuted: Bool,
|
||||||
|
drawing: UIImage?,
|
||||||
|
entities: [CodableDrawingEntity],
|
||||||
|
toolValues: [EditorToolKey: Any]
|
||||||
|
) {
|
||||||
|
self.originalDimensions = originalDimensions
|
||||||
|
self.cropOffset = cropOffset
|
||||||
|
self.cropSize = cropSize
|
||||||
|
self.cropScale = cropScale
|
||||||
|
self.cropRotation = cropRotation
|
||||||
|
self.cropMirroring = cropMirroring
|
||||||
|
self.gradientColors = gradientColors
|
||||||
|
self.videoTrimRange = videoTrimRange
|
||||||
|
self.videoIsMuted = videoIsMuted
|
||||||
|
self.drawing = drawing
|
||||||
|
self.entities = entities
|
||||||
|
self.toolValues = toolValues
|
||||||
|
}
|
||||||
|
|
||||||
|
func withUpdatedCrop(offset: CGPoint, scale: CGFloat, rotation: CGFloat, mirroring: Bool) -> MediaEditorValues {
|
||||||
|
return MediaEditorValues(originalDimensions: self.originalDimensions, cropOffset: offset, cropSize: self.cropSize, cropScale: scale, cropRotation: rotation, cropMirroring: mirroring, gradientColors: self.gradientColors, videoTrimRange: self.videoTrimRange, videoIsMuted: self.videoIsMuted, drawing: self.drawing, entities: self.entities, toolValues: self.toolValues)
|
||||||
|
}
|
||||||
|
|
||||||
|
func withUpdatedGradientColors(gradientColors: [UIColor]) -> MediaEditorValues {
|
||||||
|
return MediaEditorValues(originalDimensions: self.originalDimensions, cropOffset: self.cropOffset, cropSize: self.cropSize, cropScale: self.cropScale, cropRotation: self.cropRotation, cropMirroring: self.cropMirroring, gradientColors: gradientColors, videoTrimRange: self.videoTrimRange, videoIsMuted: self.videoIsMuted, drawing: self.drawing, entities: self.entities, toolValues: self.toolValues)
|
||||||
|
}
|
||||||
|
|
||||||
|
func withUpdatedVideoIsMuted(_ videoIsMuted: Bool) -> MediaEditorValues {
|
||||||
|
return MediaEditorValues(originalDimensions: self.originalDimensions, cropOffset: self.cropOffset, cropSize: self.cropSize, cropScale: self.cropScale, cropRotation: self.cropRotation, cropMirroring: self.cropMirroring, gradientColors: self.gradientColors, videoTrimRange: self.videoTrimRange, videoIsMuted: videoIsMuted, drawing: self.drawing, entities: self.entities, toolValues: self.toolValues)
|
||||||
|
}
|
||||||
|
|
||||||
|
func withUpdatedDrawingAndEntities(drawing: UIImage?, entities: [CodableDrawingEntity]) -> MediaEditorValues {
|
||||||
|
return MediaEditorValues(originalDimensions: self.originalDimensions, cropOffset: self.cropOffset, cropSize: self.cropSize, cropScale: self.cropScale, cropRotation: self.cropRotation, cropMirroring: self.cropMirroring, gradientColors: self.gradientColors, videoTrimRange: self.videoTrimRange, videoIsMuted: self.videoIsMuted, drawing: drawing, entities: entities, toolValues: self.toolValues)
|
||||||
|
}
|
||||||
|
|
||||||
|
func withUpdatedToolValues(_ toolValues: [EditorToolKey: Any]) -> MediaEditorValues {
|
||||||
|
return MediaEditorValues(originalDimensions: self.originalDimensions, cropOffset: self.cropOffset, cropSize: self.cropSize, cropScale: self.cropScale, cropRotation: self.cropRotation, cropMirroring: self.cropMirroring, gradientColors: self.gradientColors, videoTrimRange: self.videoTrimRange, videoIsMuted: self.videoIsMuted, drawing: self.drawing, entities: self.entities, toolValues: toolValues)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public struct TintValue: Equatable {
|
public struct TintValue: Equatable {
|
||||||
public static let initial = TintValue(
|
public static let initial = TintValue(
|
||||||
color: .clear,
|
color: .clear,
|
||||||
@ -300,64 +366,6 @@ public struct CurvesValue: Equatable {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public class MediaEditorValues {
|
|
||||||
public let originalDimensions: PixelDimensions
|
|
||||||
public let cropOffset: CGPoint
|
|
||||||
public let cropSize: CGSize?
|
|
||||||
public let cropScale: CGFloat
|
|
||||||
public let cropRotation: CGFloat
|
|
||||||
public let cropMirroring: Bool
|
|
||||||
|
|
||||||
public let gradientColors: [UIColor]?
|
|
||||||
|
|
||||||
public let videoTrimRange: Range<Double>?
|
|
||||||
public let videoIsMuted: Bool
|
|
||||||
|
|
||||||
public let drawing: UIImage?
|
|
||||||
public let toolValues: [EditorToolKey: Any]
|
|
||||||
|
|
||||||
init(
|
|
||||||
originalDimensions: PixelDimensions,
|
|
||||||
cropOffset: CGPoint,
|
|
||||||
cropSize: CGSize?,
|
|
||||||
cropScale: CGFloat,
|
|
||||||
cropRotation: CGFloat,
|
|
||||||
cropMirroring: Bool,
|
|
||||||
gradientColors: [UIColor]?,
|
|
||||||
videoTrimRange: Range<Double>?,
|
|
||||||
videoIsMuted: Bool,
|
|
||||||
drawing: UIImage?,
|
|
||||||
toolValues: [EditorToolKey: Any]
|
|
||||||
) {
|
|
||||||
self.originalDimensions = originalDimensions
|
|
||||||
self.cropOffset = cropOffset
|
|
||||||
self.cropSize = cropSize
|
|
||||||
self.cropScale = cropScale
|
|
||||||
self.cropRotation = cropRotation
|
|
||||||
self.cropMirroring = cropMirroring
|
|
||||||
self.gradientColors = gradientColors
|
|
||||||
self.videoTrimRange = videoTrimRange
|
|
||||||
self.videoIsMuted = videoIsMuted
|
|
||||||
self.drawing = drawing
|
|
||||||
self.toolValues = toolValues
|
|
||||||
}
|
|
||||||
|
|
||||||
func withUpdatedCrop(offset: CGPoint, scale: CGFloat, rotation: CGFloat, mirroring: Bool) -> MediaEditorValues {
|
|
||||||
return MediaEditorValues(originalDimensions: self.originalDimensions, cropOffset: offset, cropSize: self.cropSize, cropScale: scale, cropRotation: rotation, cropMirroring: mirroring, gradientColors: self.gradientColors, videoTrimRange: self.videoTrimRange, videoIsMuted: self.videoIsMuted, drawing: self.drawing, toolValues: self.toolValues)
|
|
||||||
}
|
|
||||||
|
|
||||||
func withUpdatedGradientColors(gradientColors: [UIColor]) -> MediaEditorValues {
|
|
||||||
return MediaEditorValues(originalDimensions: self.originalDimensions, cropOffset: self.cropOffset, cropSize: self.cropSize, cropScale: self.cropScale, cropRotation: self.cropRotation, cropMirroring: self.cropMirroring, gradientColors: gradientColors, videoTrimRange: self.videoTrimRange, videoIsMuted: self.videoIsMuted, drawing: self.drawing, toolValues: self.toolValues)
|
|
||||||
}
|
|
||||||
|
|
||||||
func withUpdatedVideoIsMuted(_ videoIsMuted: Bool) -> MediaEditorValues {
|
|
||||||
return MediaEditorValues(originalDimensions: self.originalDimensions, cropOffset: self.cropOffset, cropSize: self.cropSize, cropScale: self.cropScale, cropRotation: self.cropRotation, cropMirroring: self.cropMirroring, gradientColors: self.gradientColors, videoTrimRange: self.videoTrimRange, videoIsMuted: videoIsMuted, drawing: self.drawing, toolValues: self.toolValues)
|
|
||||||
}
|
|
||||||
|
|
||||||
func withUpdatedToolValues(_ toolValues: [EditorToolKey: Any]) -> MediaEditorValues {
|
|
||||||
return MediaEditorValues(originalDimensions: self.originalDimensions, cropOffset: self.cropOffset, cropSize: self.cropSize, cropScale: self.cropScale, cropRotation: self.cropRotation, cropMirroring: self.cropMirroring, gradientColors: self.gradientColors, videoTrimRange: self.videoTrimRange, videoIsMuted: self.videoIsMuted, drawing: self.drawing, toolValues: toolValues)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private let toolEpsilon: Float = 0.005
|
private let toolEpsilon: Float = 0.005
|
||||||
public extension MediaEditorValues {
|
public extension MediaEditorValues {
|
||||||
|
@ -2,6 +2,7 @@ import Foundation
|
|||||||
import AVFoundation
|
import AVFoundation
|
||||||
import MetalKit
|
import MetalKit
|
||||||
import SwiftSignalKit
|
import SwiftSignalKit
|
||||||
|
import AccountContext
|
||||||
|
|
||||||
enum ExportWriterStatus {
|
enum ExportWriterStatus {
|
||||||
case unknown
|
case unknown
|
||||||
@ -239,6 +240,7 @@ public final class MediaEditorVideoExport {
|
|||||||
|
|
||||||
public private(set) var internalStatus: Status = .idle
|
public private(set) var internalStatus: Status = .idle
|
||||||
|
|
||||||
|
private let context: AccountContext
|
||||||
private let subject: Subject
|
private let subject: Subject
|
||||||
private let configuration: Configuration
|
private let configuration: Configuration
|
||||||
private let outputPath: String
|
private let outputPath: String
|
||||||
@ -262,7 +264,10 @@ public final class MediaEditorVideoExport {
|
|||||||
|
|
||||||
private var startTimestamp = CACurrentMediaTime()
|
private var startTimestamp = CACurrentMediaTime()
|
||||||
|
|
||||||
public init(subject: Subject, configuration: Configuration, outputPath: String) {
|
private let semaphore = DispatchSemaphore(value: 0)
|
||||||
|
|
||||||
|
public init(context: AccountContext, subject: Subject, configuration: Configuration, outputPath: String) {
|
||||||
|
self.context = context
|
||||||
self.subject = subject
|
self.subject = subject
|
||||||
self.configuration = configuration
|
self.configuration = configuration
|
||||||
self.outputPath = outputPath
|
self.outputPath = outputPath
|
||||||
@ -284,7 +289,7 @@ public final class MediaEditorVideoExport {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if self.configuration.values.requiresComposing {
|
if self.configuration.values.requiresComposing {
|
||||||
self.composer = MediaEditorComposer(values: self.configuration.values, dimensions: self.configuration.dimensions)
|
self.composer = MediaEditorComposer(context: self.context, values: self.configuration.values, dimensions: self.configuration.dimensions)
|
||||||
}
|
}
|
||||||
self.setupVideoInput()
|
self.setupVideoInput()
|
||||||
}
|
}
|
||||||
@ -336,7 +341,7 @@ public final class MediaEditorVideoExport {
|
|||||||
}
|
}
|
||||||
|
|
||||||
let audioTracks = asset.tracks(withMediaType: .audio)
|
let audioTracks = asset.tracks(withMediaType: .audio)
|
||||||
if audioTracks.count > 0 {
|
if audioTracks.count > 0, !self.configuration.values.videoIsMuted {
|
||||||
let audioOutput = AVAssetReaderAudioMixOutput(audioTracks: audioTracks, audioSettings: nil)
|
let audioOutput = AVAssetReaderAudioMixOutput(audioTracks: audioTracks, audioSettings: nil)
|
||||||
audioOutput.alwaysCopiesSampleData = false
|
audioOutput.alwaysCopiesSampleData = false
|
||||||
if reader.canAdd(audioOutput) {
|
if reader.canAdd(audioOutput) {
|
||||||
@ -420,28 +425,40 @@ public final class MediaEditorVideoExport {
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var appendFailed = false
|
||||||
while writer.isReadyForMoreVideoData {
|
while writer.isReadyForMoreVideoData {
|
||||||
let _ = self.composer?.semaphore.wait(timeout: .distantFuture)
|
if appendFailed {
|
||||||
|
return false
|
||||||
|
}
|
||||||
if reader.status != .reading || writer.status != .writing {
|
if reader.status != .reading || writer.status != .writing {
|
||||||
writer.markVideoAsFinished()
|
writer.markVideoAsFinished()
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
self.pauseDispatchGroup.wait()
|
self.pauseDispatchGroup.wait()
|
||||||
if let buffer = output.copyNextSampleBuffer() {
|
if let buffer = output.copyNextSampleBuffer() {
|
||||||
if let pixelBuffer = self.composer?.processSampleBuffer(buffer, pool: writer.pixelBufferPool) {
|
if let composer = self.composer {
|
||||||
let timestamp = CMSampleBufferGetPresentationTimeStamp(buffer)
|
let timestamp = CMSampleBufferGetPresentationTimeStamp(buffer)
|
||||||
if !writer.appendPixelBuffer(pixelBuffer, at: timestamp) {
|
composer.processSampleBuffer(buffer, pool: writer.pixelBufferPool, completion: { pixelBuffer in
|
||||||
writer.markVideoAsFinished()
|
if let pixelBuffer {
|
||||||
return false
|
if !writer.appendPixelBuffer(pixelBuffer, at: timestamp) {
|
||||||
}
|
writer.markVideoAsFinished()
|
||||||
|
appendFailed = true
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if !writer.appendVideoBuffer(buffer) {
|
||||||
|
writer.markVideoAsFinished()
|
||||||
|
appendFailed = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
self.semaphore.signal()
|
||||||
|
})
|
||||||
|
self.semaphore.wait()
|
||||||
} else {
|
} else {
|
||||||
if !writer.appendVideoBuffer(buffer) {
|
if !writer.appendVideoBuffer(buffer) {
|
||||||
writer.markVideoAsFinished()
|
writer.markVideoAsFinished()
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// let progress = (CMSampleBufferGetPresentationTimeStamp(buffer) - self.configuration.timeRange.start).seconds/self.duration.seconds
|
// let progress = (CMSampleBufferGetPresentationTimeStamp(buffer) - self.configuration.timeRange.start).seconds/self.duration.seconds
|
||||||
// if self.videoOutput === output {
|
// if self.videoOutput === output {
|
||||||
// self.dispatchProgressCallback { $0.updateVideoEncodingProgress(fractionCompleted: progress) }
|
// self.dispatchProgressCallback { $0.updateVideoEncodingProgress(fractionCompleted: progress) }
|
||||||
|
@ -1044,6 +1044,12 @@ public final class MediaEditorScreen: ViewController {
|
|||||||
self?.drawingView.isUserInteractionEnabled = false
|
self?.drawingView.isUserInteractionEnabled = false
|
||||||
self?.animateInFromTool()
|
self?.animateInFromTool()
|
||||||
|
|
||||||
|
if let result = controller?.generateDrawingResultData() {
|
||||||
|
self?.mediaEditor?.setDrawingAndEntities(data: result.data, image: result.drawingImage, entities: result.entities)
|
||||||
|
} else {
|
||||||
|
self?.mediaEditor?.setDrawingAndEntities(data: nil, image: nil, entities: [])
|
||||||
|
}
|
||||||
|
|
||||||
selectionContainerView?.removeFromSuperview()
|
selectionContainerView?.removeFromSuperview()
|
||||||
}
|
}
|
||||||
self.controller?.present(controller, in: .current)
|
self.controller?.present(controller, in: .current)
|
||||||
@ -1217,13 +1223,15 @@ public final class MediaEditorScreen: ViewController {
|
|||||||
})
|
})
|
||||||
} else {
|
} else {
|
||||||
if let image = mediaEditor.resultImage {
|
if let image = mediaEditor.resultImage {
|
||||||
if let resultImage = makeEditorImageComposition(inputImage: image, dimensions: storyDimensions, values: mediaEditor.values) {
|
makeEditorImageComposition(context: self.context, inputImage: image, dimensions: storyDimensions, values: mediaEditor.values, time: .zero, completion: { resultImage in
|
||||||
self.completion(.image(resultImage, nil), { [weak self] in
|
if let resultImage {
|
||||||
self?.node.animateOut(completion: { [weak self] in
|
self.completion(.image(resultImage, nil), { [weak self] in
|
||||||
self?.dismiss()
|
self?.node.animateOut(completion: { [weak self] in
|
||||||
|
self?.dismiss()
|
||||||
|
})
|
||||||
})
|
})
|
||||||
})
|
}
|
||||||
}
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -1267,7 +1275,7 @@ public final class MediaEditorScreen: ViewController {
|
|||||||
|
|
||||||
let configuration = recommendedExportConfiguration(mediaEditor: mediaEditor)
|
let configuration = recommendedExportConfiguration(mediaEditor: mediaEditor)
|
||||||
let outputPath = NSTemporaryDirectory() + "\(Int64.random(in: 0 ..< .max)).mp4"
|
let outputPath = NSTemporaryDirectory() + "\(Int64.random(in: 0 ..< .max)).mp4"
|
||||||
let export = MediaEditorVideoExport(subject: exportSubject, configuration: configuration, outputPath: outputPath)
|
let export = MediaEditorVideoExport(context: self.context, subject: exportSubject, configuration: configuration, outputPath: outputPath)
|
||||||
self.export = export
|
self.export = export
|
||||||
|
|
||||||
export.startExport()
|
export.startExport()
|
||||||
@ -1283,12 +1291,13 @@ public final class MediaEditorScreen: ViewController {
|
|||||||
})
|
})
|
||||||
} else {
|
} else {
|
||||||
if let image = mediaEditor.resultImage {
|
if let image = mediaEditor.resultImage {
|
||||||
let resultImage = makeEditorImageComposition(inputImage: image, dimensions: storyDimensions, values: mediaEditor.values)
|
makeEditorImageComposition(context: self.context, inputImage: image, dimensions: storyDimensions, values: mediaEditor.values, time: .zero, completion: { resultImage in
|
||||||
if let data = resultImage?.jpegData(compressionQuality: 0.8) {
|
if let data = resultImage?.jpegData(compressionQuality: 0.8) {
|
||||||
let outputPath = NSTemporaryDirectory() + "\(Int64.random(in: 0 ..< .max)).jpg"
|
let outputPath = NSTemporaryDirectory() + "\(Int64.random(in: 0 ..< .max)).jpg"
|
||||||
try? data.write(to: URL(fileURLWithPath: outputPath))
|
try? data.write(to: URL(fileURLWithPath: outputPath))
|
||||||
saveToPhotos(outputPath, false)
|
saveToPhotos(outputPath, false)
|
||||||
}
|
}
|
||||||
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user