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 MultilineTextComponent
|
||||
import HexColor
|
||||
import MediaEditor
|
||||
|
||||
private let palleteColors: [UInt32] = [
|
||||
0xffffff, 0xebebeb, 0xd6d6d6, 0xc2c2c2, 0xadadad, 0x999999, 0x858585, 0x707070, 0x5c5c5c, 0x474747, 0x333333, 0x000000,
|
||||
|
@ -2,119 +2,7 @@ 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
|
||||
}
|
||||
|
||||
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()
|
||||
}
|
||||
}
|
||||
import MediaEditor
|
||||
|
||||
final class DrawingBubbleEntityView: DrawingEntityView {
|
||||
private var bubbleEntity: DrawingBubbleEntity {
|
||||
|
@ -3,66 +3,7 @@ import UIKit
|
||||
import Display
|
||||
import LegacyComponents
|
||||
import AccountContext
|
||||
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
import MediaEditor
|
||||
|
||||
public func decodeDrawingEntities(data: Data) -> [DrawingEntity] {
|
||||
if let codableEntities = try? JSONDecoder().decode([CodableDrawingEntity].self, from: data) {
|
||||
@ -71,56 +12,37 @@ public func decodeDrawingEntities(data: Data) -> [DrawingEntity] {
|
||||
return []
|
||||
}
|
||||
|
||||
extension CodableDrawingEntity: Codable {
|
||||
private enum CodingKeys: String, CodingKey {
|
||||
case type
|
||||
case entity
|
||||
private func makeEntityView(context: AccountContext, entity: DrawingEntity) -> DrawingEntityView? {
|
||||
if let entity = entity as? DrawingBubbleEntity {
|
||||
return DrawingBubbleEntityView(context: context, entity: 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 {
|
||||
case sticker
|
||||
case text
|
||||
case simpleShape
|
||||
case bubble
|
||||
case vector
|
||||
private func prepareForRendering(entityView: DrawingEntityView) {
|
||||
if let entityView = entityView as? DrawingBubbleEntityView {
|
||||
entityView.entity.renderImage = entityView.getRenderImage()
|
||||
}
|
||||
|
||||
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))
|
||||
}
|
||||
if let entityView = entityView as? DrawingSimpleShapeEntityView {
|
||||
entityView.entity.renderImage = entityView.getRenderImage()
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
if let entityView = entityView as? DrawingTextEntityView {
|
||||
entityView.entity.renderImage = entityView.getRenderImage()
|
||||
entityView.entity.renderSubEntities = entityView.getRenderSubEntities()
|
||||
}
|
||||
if let entityView = entityView as? DrawingVectorEntityView {
|
||||
entityView.entity.renderImage = entityView.getRenderImage()
|
||||
}
|
||||
}
|
||||
|
||||
@ -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
|
||||
guard !entities.isEmpty else {
|
||||
return nil
|
||||
}
|
||||
for entity in entities {
|
||||
entity.prepareForRender()
|
||||
if let entitiesView {
|
||||
for entity in entities {
|
||||
if let entityView = entitiesView.getView(for: entity.uuid) {
|
||||
prepareForRendering(entityView: entityView)
|
||||
}
|
||||
}
|
||||
}
|
||||
let codableEntities = entities.compactMap({ CodableDrawingEntity(entity: $0) })
|
||||
if let data = try? JSONEncoder().encode(codableEntities) {
|
||||
@ -244,7 +170,7 @@ public final class DrawingEntitiesView: UIView, TGPhotoDrawingEntitiesView {
|
||||
}
|
||||
|
||||
var entitiesData: Data? {
|
||||
return DrawingEntitiesView.encodeEntities(self.entities)
|
||||
return DrawingEntitiesView.encodeEntities(self.entities, entitiesView: self)
|
||||
}
|
||||
|
||||
var hasChanges: Bool {
|
||||
@ -351,7 +277,9 @@ public final class DrawingEntitiesView: UIView, TGPhotoDrawingEntitiesView {
|
||||
|
||||
@discardableResult
|
||||
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.onSnapToXAxis = { [weak self, weak view] snappedToX in
|
||||
@ -420,7 +348,9 @@ public final class DrawingEntitiesView: UIView, TGPhotoDrawingEntitiesView {
|
||||
let newEntity = entity.duplicate()
|
||||
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.update()
|
||||
self.addSubview(view)
|
||||
|
@ -7,152 +7,6 @@ import AccountContext
|
||||
import MediaEditor
|
||||
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 {
|
||||
private var mediaEntity: DrawingMediaEntity {
|
||||
return self.entity as! DrawingMediaEntity
|
||||
|
@ -5,6 +5,7 @@ import MetalKit
|
||||
import Display
|
||||
import SwiftSignalKit
|
||||
import AppBundle
|
||||
import MediaEditor
|
||||
|
||||
final class DrawingMetalView: MTKView {
|
||||
let size: CGSize
|
||||
|
@ -1,6 +1,7 @@
|
||||
import Foundation
|
||||
import UIKit
|
||||
import Display
|
||||
import MediaEditor
|
||||
|
||||
final class NeonTool: DrawingElement {
|
||||
class RenderView: UIView, DrawingRenderView {
|
||||
|
@ -1,6 +1,7 @@
|
||||
import Foundation
|
||||
import UIKit
|
||||
import Display
|
||||
import MediaEditor
|
||||
|
||||
private let activeWidthFactor: CGFloat = 0.7
|
||||
|
||||
|
@ -21,6 +21,13 @@ import ChatEntityKeyboardInputNode
|
||||
import EntityKeyboard
|
||||
import TelegramUIPreferences
|
||||
import FastBlur
|
||||
import MediaEditor
|
||||
|
||||
public struct DrawingResultData {
|
||||
public let data: Data?
|
||||
public let drawingImage: UIImage?
|
||||
public let entities: [CodableDrawingEntity]
|
||||
}
|
||||
|
||||
enum DrawingToolState: Equatable, Codable {
|
||||
private enum CodingKeys: String, CodingKey {
|
||||
@ -502,6 +509,8 @@ private final class DrawingScreenComponent: CombinedComponent {
|
||||
let toggleWithPreviousTool: ActionSlot<Void>
|
||||
let insertSticker: ActionSlot<Void>
|
||||
let insertText: ActionSlot<Void>
|
||||
let updateEntityView: ActionSlot<(UUID, Bool)>
|
||||
let endEditingTextEntityView: ActionSlot<(UUID, Bool)>
|
||||
let apply: ActionSlot<Void>
|
||||
let dismiss: ActionSlot<Void>
|
||||
|
||||
@ -533,6 +542,8 @@ private final class DrawingScreenComponent: CombinedComponent {
|
||||
toggleWithPreviousTool: ActionSlot<Void>,
|
||||
insertSticker: ActionSlot<Void>,
|
||||
insertText: ActionSlot<Void>,
|
||||
updateEntityView: ActionSlot<(UUID, Bool)>,
|
||||
endEditingTextEntityView: ActionSlot<(UUID, Bool)>,
|
||||
apply: ActionSlot<Void>,
|
||||
dismiss: ActionSlot<Void>,
|
||||
presentColorPicker: @escaping (DrawingColor) -> Void,
|
||||
@ -562,6 +573,8 @@ private final class DrawingScreenComponent: CombinedComponent {
|
||||
self.toggleWithPreviousTool = toggleWithPreviousTool
|
||||
self.insertSticker = insertSticker
|
||||
self.insertText = insertText
|
||||
self.updateEntityView = updateEntityView
|
||||
self.endEditingTextEntityView = endEditingTextEntityView
|
||||
self.apply = apply
|
||||
self.dismiss = dismiss
|
||||
self.presentColorPicker = presentColorPicker
|
||||
@ -637,6 +650,8 @@ private final class DrawingScreenComponent: CombinedComponent {
|
||||
private let toggleWithPreviousTool: ActionSlot<Void>
|
||||
private let insertSticker: ActionSlot<Void>
|
||||
private let insertText: ActionSlot<Void>
|
||||
private let updateEntityView: ActionSlot<(UUID, Bool)>
|
||||
private let endEditingTextEntityView: ActionSlot<(UUID, Bool)>
|
||||
private let present: (ViewController) -> Void
|
||||
|
||||
var currentMode: Mode
|
||||
@ -661,6 +676,8 @@ private final class DrawingScreenComponent: CombinedComponent {
|
||||
toggleWithPreviousTool: ActionSlot<Void>,
|
||||
insertSticker: ActionSlot<Void>,
|
||||
insertText: ActionSlot<Void>,
|
||||
updateEntityView: ActionSlot<(UUID, Bool)>,
|
||||
endEditingTextEntityView: ActionSlot<(UUID, Bool)>,
|
||||
present: @escaping (ViewController) -> Void)
|
||||
{
|
||||
self.context = context
|
||||
@ -673,6 +690,8 @@ private final class DrawingScreenComponent: CombinedComponent {
|
||||
self.toggleWithPreviousTool = toggleWithPreviousTool
|
||||
self.insertSticker = insertSticker
|
||||
self.insertText = insertText
|
||||
self.updateEntityView = updateEntityView
|
||||
self.endEditingTextEntityView = endEditingTextEntityView
|
||||
self.present = present
|
||||
|
||||
self.currentMode = .drawing
|
||||
@ -808,7 +827,7 @@ private final class DrawingScreenComponent: CombinedComponent {
|
||||
self.currentColor = color
|
||||
if let selectedEntity = self.selectedEntity {
|
||||
selectedEntity.color = color
|
||||
selectedEntity.currentEntityView?.update()
|
||||
self.updateEntityView.invoke((selectedEntity.uuid, false))
|
||||
} else {
|
||||
self.drawingState = self.drawingState.withUpdatedColor(color)
|
||||
self.updateToolState.invoke(self.drawingState.currentToolState)
|
||||
@ -850,7 +869,7 @@ private final class DrawingScreenComponent: CombinedComponent {
|
||||
} else {
|
||||
selectedEntity.lineWidth = size
|
||||
}
|
||||
selectedEntity.currentEntityView?.update()
|
||||
self.updateEntityView.invoke((selectedEntity.uuid, false))
|
||||
} else {
|
||||
self.drawingState = self.drawingState.withUpdatedSize(size)
|
||||
self.updateToolState.invoke(self.drawingState.currentToolState)
|
||||
@ -996,7 +1015,7 @@ private final class DrawingScreenComponent: CombinedComponent {
|
||||
}
|
||||
|
||||
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 {
|
||||
@ -1070,6 +1089,9 @@ private final class DrawingScreenComponent: CombinedComponent {
|
||||
let dismissFastColorPicker = component.dismissFastColorPicker
|
||||
let presentFontPicker = component.presentFontPicker
|
||||
|
||||
let updateEntityView = component.updateEntityView
|
||||
let endEditingTextEntityView = component.endEditingTextEntityView
|
||||
|
||||
component.updateState.connect { [weak state] updatedState in
|
||||
state?.updateDrawingState(updatedState)
|
||||
}
|
||||
@ -1162,9 +1184,7 @@ private final class DrawingScreenComponent: CombinedComponent {
|
||||
nextStyle = .regular
|
||||
}
|
||||
textEntity.style = nextStyle
|
||||
if let entityView = textEntity.currentEntityView {
|
||||
entityView.update()
|
||||
}
|
||||
updateEntityView.invoke((textEntity.uuid, false))
|
||||
state?.updated(transition: .easeInOut(duration: 0.2))
|
||||
},
|
||||
toggleAnimation: { [weak state, weak textEntity] in
|
||||
@ -1183,9 +1203,7 @@ private final class DrawingScreenComponent: CombinedComponent {
|
||||
nextAnimation = .none
|
||||
}
|
||||
textEntity.animation = nextAnimation
|
||||
if let entityView = textEntity.currentEntityView {
|
||||
entityView.update()
|
||||
}
|
||||
updateEntityView.invoke((textEntity.uuid, false))
|
||||
state?.updated(transition: .easeInOut(duration: 0.2))
|
||||
},
|
||||
toggleAlignment: { [weak state, weak textEntity] in
|
||||
@ -1202,9 +1220,7 @@ private final class DrawingScreenComponent: CombinedComponent {
|
||||
nextAlignment = .left
|
||||
}
|
||||
textEntity.alignment = nextAlignment
|
||||
if let entityView = textEntity.currentEntityView {
|
||||
entityView.update()
|
||||
}
|
||||
updateEntityView.invoke((textEntity.uuid, false))
|
||||
state?.updated(transition: .easeInOut(duration: 0.2))
|
||||
},
|
||||
presentFontPicker: {
|
||||
@ -1549,14 +1565,14 @@ private final class DrawingScreenComponent: CombinedComponent {
|
||||
} else {
|
||||
entity.drawType = .fill
|
||||
}
|
||||
entity.currentEntityView?.update()
|
||||
updateEntityView.invoke((entity.uuid, false))
|
||||
} else if let entity = state.selectedEntity as? DrawingBubbleEntity {
|
||||
if case .fill = entity.drawType {
|
||||
entity.drawType = .stroke
|
||||
} else {
|
||||
entity.drawType = .fill
|
||||
}
|
||||
entity.currentEntityView?.update()
|
||||
updateEntityView.invoke((entity.uuid, false))
|
||||
} else if let entity = state.selectedEntity as? DrawingVectorEntity {
|
||||
if case .oneSidedArrow = entity.type {
|
||||
entity.type = .twoSidedArrow
|
||||
@ -1565,7 +1581,7 @@ private final class DrawingScreenComponent: CombinedComponent {
|
||||
} else {
|
||||
entity.type = .oneSidedArrow
|
||||
}
|
||||
entity.currentEntityView?.update()
|
||||
updateEntityView.invoke((entity.uuid, false))
|
||||
}
|
||||
state.updated(transition: .easeInOut(duration: 0.2))
|
||||
}
|
||||
@ -1594,10 +1610,10 @@ private final class DrawingScreenComponent: CombinedComponent {
|
||||
var updatedTailPosition = entity.tailPosition
|
||||
updatedTailPosition.x = 1.0 - updatedTailPosition.x
|
||||
entity.tailPosition = updatedTailPosition
|
||||
entity.currentEntityView?.update()
|
||||
updateEntityView.invoke((entity.uuid, false))
|
||||
} else if let entity = state.selectedEntity as? DrawingStickerEntity {
|
||||
entity.mirrored = !entity.mirrored
|
||||
entity.currentEntityView?.update(animated: true)
|
||||
updateEntityView.invoke((entity.uuid, true))
|
||||
}
|
||||
state.updated(transition: .easeInOut(duration: 0.2))
|
||||
}
|
||||
@ -1616,9 +1632,9 @@ private final class DrawingScreenComponent: CombinedComponent {
|
||||
var sizeSliderVisible = false
|
||||
var isEditingText = false
|
||||
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
|
||||
isEditingText = entityView.isEditing
|
||||
isEditingText = false//entityView.isEditing
|
||||
sizeValue = textEntity.fontSize
|
||||
} else {
|
||||
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)
|
||||
),
|
||||
action: { [weak state] in
|
||||
if let entity = state?.selectedEntity as? DrawingTextEntity, let entityView = entity.currentEntityView as? DrawingTextEntityView {
|
||||
entityView.endEditing(reset: true)
|
||||
if let entity = state?.selectedEntity as? DrawingTextEntity {
|
||||
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)
|
||||
),
|
||||
action: { [weak state] in
|
||||
if let entity = state?.selectedEntity as? DrawingTextEntity, let entityView = entity.currentEntityView as? DrawingTextEntityView {
|
||||
entityView.endEditing()
|
||||
if let entity = state?.selectedEntity as? DrawingTextEntity {
|
||||
endEditingTextEntityView.invoke((entity.uuid, false))
|
||||
}
|
||||
}
|
||||
),
|
||||
@ -2041,6 +2057,8 @@ public class DrawingScreen: ViewController, TGPhotoDrawingInterfaceController, U
|
||||
private let toggleWithPreviousTool: ActionSlot<Void>
|
||||
fileprivate let insertSticker: 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 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!
|
||||
}
|
||||
@ -2312,6 +2342,8 @@ public class DrawingScreen: ViewController, TGPhotoDrawingInterfaceController, U
|
||||
self.toggleWithPreviousTool = ActionSlot<Void>()
|
||||
self.insertSticker = ActionSlot<Void>()
|
||||
self.insertText = ActionSlot<Void>()
|
||||
self.updateEntityView = ActionSlot<(UUID, Bool)>()
|
||||
self.endEditingTextEntityView = ActionSlot<(UUID, Bool)>()
|
||||
self.apply = ActionSlot<Void>()
|
||||
self.dismiss = ActionSlot<Void>()
|
||||
|
||||
@ -2737,6 +2769,8 @@ public class DrawingScreen: ViewController, TGPhotoDrawingInterfaceController, U
|
||||
toggleWithPreviousTool: self.toggleWithPreviousTool,
|
||||
insertSticker: self.insertSticker,
|
||||
insertText: self.insertText,
|
||||
updateEntityView: self.updateEntityView,
|
||||
endEditingTextEntityView: self.endEditingTextEntityView,
|
||||
apply: self.apply,
|
||||
dismiss: self.dismiss,
|
||||
presentColorPicker: { [weak self] initialColor in
|
||||
@ -3042,6 +3076,24 @@ public class DrawingScreen: ViewController, TGPhotoDrawingInterfaceController, U
|
||||
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? {
|
||||
if self.drawingView.isEmpty && self.entitiesView.entities.isEmpty {
|
||||
return nil
|
||||
@ -3102,7 +3154,7 @@ public class DrawingScreen: ViewController, TGPhotoDrawingInterfaceController, U
|
||||
stickers.append(coder.makeData())
|
||||
} else if let text = entity as? DrawingTextEntity, let subEntities = text.renderSubEntities {
|
||||
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()
|
||||
coder.encodeRootObject(file)
|
||||
stickers.append(coder.makeData())
|
||||
|
@ -2,125 +2,7 @@ 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
|
||||
|
||||
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()
|
||||
}
|
||||
}
|
||||
import MediaEditor
|
||||
|
||||
final class DrawingSimpleShapeEntityView: DrawingEntityView {
|
||||
private var shapeEntity: DrawingSimpleShapeEntity {
|
||||
|
@ -7,121 +7,7 @@ import AnimatedStickerNode
|
||||
import TelegramAnimatedStickerNode
|
||||
import StickerResources
|
||||
import AccountContext
|
||||
|
||||
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() {
|
||||
}
|
||||
}
|
||||
import MediaEditor
|
||||
|
||||
final class DrawingStickerEntityView: DrawingEntityView {
|
||||
private var stickerEntity: DrawingStickerEntity {
|
||||
@ -274,7 +160,8 @@ final class DrawingStickerEntityView: DrawingEntityView {
|
||||
let dimensions = file.dimensions ?? PixelDimensions(width: 512, height: 512)
|
||||
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")
|
||||
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)
|
||||
|> deliverOn(Queue.concurrentDefaultQueue())).start())
|
||||
|
@ -5,286 +5,17 @@ import SwiftSignalKit
|
||||
import AccountContext
|
||||
import TextFormat
|
||||
import EmojiTextAttachmentView
|
||||
import MediaEditor
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
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()
|
||||
extension DrawingTextEntity.Alignment {
|
||||
var alignment: NSTextAlignment {
|
||||
switch self {
|
||||
case .left:
|
||||
return .left
|
||||
case .center:
|
||||
return .center
|
||||
case .right:
|
||||
return .right
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -892,7 +623,7 @@ final class DrawingTextEntityView: DrawingEntityView, UITextViewDelegate {
|
||||
return image
|
||||
}
|
||||
|
||||
func getRenderSubEntities() -> [DrawingStickerEntity] {
|
||||
func getRenderSubEntities() -> [DrawingEntity] {
|
||||
let textSize = self.textView.bounds.size
|
||||
let textPosition = self.textEntity.position
|
||||
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)
|
||||
|
||||
var entities: [DrawingStickerEntity] = []
|
||||
var entities: [DrawingEntity] = []
|
||||
for (emojiRect, emojiAttribute) in self.emojiRects {
|
||||
guard let file = emojiAttribute.file else {
|
||||
continue
|
||||
|
@ -1,6 +1,7 @@
|
||||
import Foundation
|
||||
import UIKit
|
||||
import Display
|
||||
import MediaEditor
|
||||
|
||||
final class MarkerTool: DrawingElement {
|
||||
let uuid: UUID
|
||||
|
@ -2,164 +2,7 @@ import Foundation
|
||||
import UIKit
|
||||
import QuartzCore
|
||||
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?
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
import MediaEditor
|
||||
|
||||
extension UIBezierPath {
|
||||
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 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
|
||||
|
||||
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()
|
||||
}
|
||||
}
|
||||
import MediaEditor
|
||||
|
||||
final class DrawingVectorEntityView: DrawingEntityView {
|
||||
private var vectorEntity: DrawingVectorEntity {
|
||||
@ -166,7 +40,7 @@ final class DrawingVectorEntityView: DrawingEntityView {
|
||||
self.shapeLayer.path = CGPath.curve(
|
||||
start: self.vectorEntity.start,
|
||||
end: self.vectorEntity.end,
|
||||
mid: self.vectorEntity.midPoint,
|
||||
mid: self.midPoint,
|
||||
lineWidth: lineWidth,
|
||||
arrowSize: self.vectorEntity.type == .line ? nil : CGSize(width: lineWidth * 1.5, height: lineWidth * 3.0),
|
||||
twoSided: self.vectorEntity.type == .twoSidedArrow
|
||||
@ -198,7 +72,7 @@ final class DrawingVectorEntityView: DrawingEntityView {
|
||||
let expandedPath = CGPath.curve(
|
||||
start: self.vectorEntity.start,
|
||||
end: self.vectorEntity.end,
|
||||
mid: self.vectorEntity.midPoint,
|
||||
mid: self.midPoint,
|
||||
lineWidth: self.maxLineWidth * 0.8,
|
||||
arrowSize: nil,
|
||||
twoSided: false
|
||||
@ -227,6 +101,18 @@ final class DrawingVectorEntityView: DrawingEntityView {
|
||||
UIGraphicsEndImageContext()
|
||||
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 {
|
||||
@ -295,7 +181,7 @@ final class DrawingVectorEntititySelectionView: DrawingEntitySelectionView, UIGe
|
||||
|
||||
private var currentHandle: CALayer?
|
||||
@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
|
||||
}
|
||||
let location = gestureRecognizer.location(in: self)
|
||||
@ -325,7 +211,7 @@ final class DrawingVectorEntititySelectionView: DrawingEntitySelectionView, UIGe
|
||||
updatedEnd.x += delta.x
|
||||
updatedEnd.y += delta.y
|
||||
} else if self.currentHandle === self.midHandle {
|
||||
var updatedMidPoint = entity.midPoint
|
||||
var updatedMidPoint = entityView.midPoint
|
||||
updatedMidPoint.x += delta.x
|
||||
updatedMidPoint.y += delta.y
|
||||
|
||||
@ -356,7 +242,7 @@ final class DrawingVectorEntititySelectionView: DrawingEntitySelectionView, UIGe
|
||||
entity.start = updatedStart
|
||||
entity.mid = updatedMid
|
||||
entity.end = updatedEnd
|
||||
entityView.update()
|
||||
entityView.update(animated: false)
|
||||
|
||||
gestureRecognizer.setTranslation(.zero, in: entityView)
|
||||
case .ended:
|
||||
@ -391,7 +277,7 @@ final class DrawingVectorEntititySelectionView: DrawingEntitySelectionView, UIGe
|
||||
self.startHandle.lineWidth = lineWidth
|
||||
|
||||
self.midHandle.path = handlePath
|
||||
self.midHandle.position = entity.midPoint
|
||||
self.midHandle.position = entityView.midPoint
|
||||
self.midHandle.bounds = bounds
|
||||
self.midHandle.lineWidth = lineWidth
|
||||
|
||||
|
@ -6,6 +6,7 @@ import ComponentFlow
|
||||
import LegacyComponents
|
||||
import AppBundle
|
||||
import ImageBlur
|
||||
import MediaEditor
|
||||
|
||||
protocol DrawingRenderLayer: CALayer {
|
||||
|
||||
|
@ -2,6 +2,7 @@ import Foundation
|
||||
import UIKit
|
||||
import Display
|
||||
import SwiftSignalKit
|
||||
import MediaEditor
|
||||
|
||||
private let size = CGSize(width: 148.0, height: 148.0)
|
||||
private let outerWidth: CGFloat = 12.0
|
||||
|
@ -5,6 +5,7 @@ import ComponentFlow
|
||||
import LegacyComponents
|
||||
import TelegramCore
|
||||
import LottieAnimationComponent
|
||||
import MediaEditor
|
||||
|
||||
enum DrawingTextStyle: Equatable {
|
||||
case regular
|
||||
|
@ -30,6 +30,7 @@ swift_library(
|
||||
"//submodules/AttachmentUI:AttachmentUI",
|
||||
"//submodules/DrawingUI:DrawingUI",
|
||||
"//submodules/SolidRoundedButtonNode:SolidRoundedButtonNode",
|
||||
"//submodules/TelegramUI/Components/MediaEditor",
|
||||
],
|
||||
visibility = [
|
||||
"//visibility:public",
|
||||
|
@ -10,6 +10,7 @@ import YuvConversion
|
||||
import StickerResources
|
||||
import DrawingUI
|
||||
import SolidRoundedButtonNode
|
||||
import MediaEditor
|
||||
|
||||
protocol LegacyPaintEntity {
|
||||
var position: CGPoint { get }
|
||||
@ -432,7 +433,9 @@ public final class LegacyPaintEntityRenderer: NSObject, TGPhotoPaintEntityRender
|
||||
renderEntities.append(LegacyPaintTextEntity(entity: text))
|
||||
if let renderSubEntities = text.renderSubEntities, let account {
|
||||
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 {
|
||||
|
@ -497,9 +497,25 @@ public func peersNearbyController(context: AccountContext) -> ViewController {
|
||||
})
|
||||
|
||||
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
|
||||
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
|
||||
|
@ -14,7 +14,7 @@ func _internal_requestAccountPrivacySettings(account: Account) -> Signal<Account
|
||||
let phoneNumberPrivacy = account.network.request(Api.functions.account.getPrivacy(key: .inputPrivacyKeyPhoneNumber))
|
||||
let phoneDiscoveryPrivacy = account.network.request(Api.functions.account.getPrivacy(key: .inputPrivacyKeyAddedByPhone))
|
||||
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 globalPrivacySettings = account.network.request(Api.functions.account.getGlobalPrivacySettings())
|
||||
let messageAutoremoveTimeout = account.network.request(Api.functions.messages.getDefaultHistoryTTL())
|
||||
@ -253,7 +253,7 @@ public enum UpdateSelectiveAccountPrivacySettingsType {
|
||||
case .voiceMessages:
|
||||
return .inputPrivacyKeyVoiceMessages
|
||||
case .bio:
|
||||
return .inputPrivacyKeyProfilePhoto
|
||||
return .inputPrivacyKeyAbout
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -38,6 +38,7 @@ swift_library(
|
||||
"//submodules/StickerResources:StickerResources",
|
||||
"//submodules/TelegramUI/Components/AnimationCache:AnimationCache",
|
||||
"//submodules/TelegramUI/Components/EmojiTextAttachmentView:EmojiTextAttachmentView",
|
||||
"//submodules/TelegramUI/Components/MediaEditor",
|
||||
],
|
||||
visibility = [
|
||||
"//visibility:public",
|
||||
|
@ -23,6 +23,7 @@ import DrawingUI
|
||||
import SolidRoundedButtonComponent
|
||||
import AnimationCache
|
||||
import EmojiTextAttachmentView
|
||||
import MediaEditor
|
||||
|
||||
enum AvatarBackground: Equatable {
|
||||
case gradient([UInt32])
|
||||
|
@ -61,6 +61,11 @@ swift_library(
|
||||
"//submodules/TelegramPresentationData:TelegramPresentationData",
|
||||
"//submodules/AccountContext:AccountContext",
|
||||
"//submodules/AppBundle:AppBundle",
|
||||
"//submodules/TextFormat:TextFormat",
|
||||
"//submodules/AnimatedStickerNode:AnimatedStickerNode",
|
||||
"//submodules/TelegramAnimatedStickerNode:TelegramAnimatedStickerNode",
|
||||
"//submodules/StickerResources:StickerResources",
|
||||
"//submodules/YuvConversion:YuvConversion",
|
||||
],
|
||||
visibility = [
|
||||
"//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,
|
||||
videoIsMuted: false,
|
||||
drawing: nil,
|
||||
entities: [],
|
||||
toolValues: [:]
|
||||
)
|
||||
}
|
||||
@ -267,6 +268,10 @@ public final class MediaEditor {
|
||||
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]) {
|
||||
self.values = self.values.withUpdatedGradientColors(gradientColors: gradientColors)
|
||||
}
|
||||
|
@ -4,6 +4,13 @@ import UIKit
|
||||
import CoreImage
|
||||
import Metal
|
||||
import Display
|
||||
import SwiftSignalKit
|
||||
import TelegramCore
|
||||
import AnimatedStickerNode
|
||||
import TelegramAnimatedStickerNode
|
||||
import YuvConversion
|
||||
import StickerResources
|
||||
import AccountContext
|
||||
|
||||
final class MediaEditorComposer {
|
||||
let device: MTLDevice?
|
||||
@ -17,15 +24,14 @@ final class MediaEditorComposer {
|
||||
private let renderer = MediaEditorRenderer()
|
||||
private let renderChain = MediaEditorRenderChain()
|
||||
|
||||
private var gradientImage: CIImage
|
||||
|
||||
let semaphore = DispatchSemaphore(value: 1)
|
||||
private let gradientImage: CIImage
|
||||
private let drawingImage: CIImage?
|
||||
private var entities: [MediaEditorComposerEntity]
|
||||
|
||||
init(values: MediaEditorValues, dimensions: CGSize) {
|
||||
init(context: AccountContext, values: MediaEditorValues, dimensions: CGSize) {
|
||||
self.values = values
|
||||
self.dimensions = dimensions
|
||||
|
||||
self.renderer.externalSemaphore = self.semaphore
|
||||
self.renderer.addRenderChain(self.renderChain)
|
||||
self.renderer.addRenderPass(ComposerRenderPass())
|
||||
|
||||
@ -36,6 +42,14 @@ final class MediaEditorComposer {
|
||||
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()
|
||||
if let device = self.device {
|
||||
self.ciContext = CIContext(mtlDevice: device, options: [.workingColorSpace : NSNull()])
|
||||
@ -49,10 +63,13 @@ final class MediaEditorComposer {
|
||||
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 {
|
||||
return nil
|
||||
completion(nil)
|
||||
return
|
||||
}
|
||||
let time = CMSampleBufferGetPresentationTimeStamp(sampleBuffer)
|
||||
|
||||
let width = CVPixelBufferGetWidth(imageBuffer)
|
||||
let height = CVPixelBufferGetHeight(imageBuffer)
|
||||
let format: MTLPixelFormat = .bgra8Unorm
|
||||
@ -62,7 +79,6 @@ final class MediaEditorComposer {
|
||||
if status == kCVReturnSuccess {
|
||||
texture = CVMetalTextureGetTexture(textureRef!)
|
||||
}
|
||||
|
||||
if let texture {
|
||||
self.renderer.consumeTexture(texture, rotation: .rotate90Degrees)
|
||||
self.renderer.renderFrame()
|
||||
@ -73,45 +89,59 @@ final class MediaEditorComposer {
|
||||
var pixelBuffer: CVPixelBuffer?
|
||||
CVPixelBufferPoolCreatePixelBuffer(kCFAllocatorDefault, pool, &pixelBuffer)
|
||||
|
||||
if let composition = processImage(inputImage: ciImage), let pixelBuffer {
|
||||
self.ciContext?.render(composition, to: pixelBuffer)
|
||||
|
||||
return pixelBuffer
|
||||
} else {
|
||||
return nil
|
||||
if let pixelBuffer {
|
||||
processImage(inputImage: ciImage, time: time, completion: { compositedImage in
|
||||
if let compositedImage {
|
||||
self.ciContext?.render(compositedImage, to: pixelBuffer)
|
||||
completion(pixelBuffer)
|
||||
} else {
|
||||
completion(nil)
|
||||
}
|
||||
})
|
||||
return
|
||||
}
|
||||
} else {
|
||||
return nil
|
||||
}
|
||||
} else {
|
||||
return nil
|
||||
}
|
||||
completion(nil)
|
||||
return
|
||||
}
|
||||
|
||||
func processImage(inputImage: CIImage) -> CIImage? {
|
||||
return makeEditorImageFrameComposition(inputImage: inputImage, gradientImage: self.gradientImage, dimensions: self.dimensions, values: self.values)
|
||||
func processImage(inputImage: CIImage, time: CMTime, completion: @escaping (CIImage?) -> Void) {
|
||||
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 gradientImage: CIImage
|
||||
var drawingImage: CIImage?
|
||||
if let gradientColors = values.gradientColors {
|
||||
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))
|
||||
} else {
|
||||
gradientImage = CIImage(color: .black)
|
||||
}
|
||||
if let ciImage = makeEditorImageFrameComposition(inputImage: inputImage, gradientImage: gradientImage, dimensions: dimensions, values: values) {
|
||||
let context = CIContext(options: [.workingColorSpace : NSNull()])
|
||||
if let cgImage = context.createCGImage(ciImage, from: CGRect(origin: .zero, size: ciImage.extent.size)) {
|
||||
return UIImage(cgImage: cgImage)
|
||||
}
|
||||
|
||||
if let drawing = values.drawing, let image = CIImage(image: drawing) {
|
||||
drawingImage = image.transformed(by: CGAffineTransform(translationX: -dimensions.width / 2.0, y: -dimensions.height / 2.0))
|
||||
}
|
||||
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))
|
||||
resultImage = gradientImage.composited(over: resultImage)
|
||||
|
||||
@ -121,32 +151,377 @@ private func makeEditorImageFrameComposition(inputImage: CIImage, gradientImage:
|
||||
cropTransform = cropTransform.rotated(by: -values.cropRotation)
|
||||
cropTransform = cropTransform.scaledBy(x: values.cropScale, y: values.cropScale)
|
||||
mediaImage = mediaImage.transformed(by: cropTransform)
|
||||
|
||||
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 {
|
||||
func newSampleBufferWithReplacedImageBuffer(_ imageBuffer: CVImageBuffer) -> CMSampleBuffer? {
|
||||
guard let _ = CMSampleBufferGetImageBuffer(self) else {
|
||||
return nil
|
||||
private func composerEntityForDrawingEntity(context: AccountContext, entity: DrawingEntity) -> MediaEditorComposerEntity? {
|
||||
if let entity = entity as? DrawingStickerEntity {
|
||||
let content: MediaEditorComposerStickerEntity.Content
|
||||
switch entity.content {
|
||||
case let .file(file):
|
||||
content = .file(file)
|
||||
case let .image(image):
|
||||
content = .image(image)
|
||||
}
|
||||
var timingInfo = CMSampleTimingInfo()
|
||||
guard CMSampleBufferGetSampleTimingInfo(self, at: 0, timingInfoOut: &timingInfo) == 0 else {
|
||||
return nil
|
||||
return MediaEditorComposerStickerEntity(context: context, content: content, position: entity.position, scale: entity.scale, rotation: entity.rotation, baseSize: entity.baseSize, mirrored: entity.mirrored)
|
||||
} else if let renderImage = entity.renderImage, let image = CIImage(image: renderImage) {
|
||||
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?
|
||||
CMVideoFormatDescriptionCreateForImageBuffer(allocator: nil, imageBuffer: imageBuffer, formatDescriptionOut: &newFormatDescription)
|
||||
guard let formatDescription = newFormatDescription else {
|
||||
return nil
|
||||
}
|
||||
CMSampleBufferCreateReadyWithImageBuffer(allocator: nil, imageBuffer: imageBuffer, formatDescription: formatDescription, sampleTiming: &timingInfo, sampleBufferOut: &outputSampleBuffer)
|
||||
return outputSampleBuffer
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
private class MediaEditorComposerStaticEntity: MediaEditorComposerEntity {
|
||||
let image: CIImage
|
||||
let position: CGPoint
|
||||
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 {
|
||||
fileprivate var cachedTexture: MTLTexture?
|
||||
|
@ -65,9 +65,7 @@ final class MediaEditorRenderer: TextureConsumer {
|
||||
private var library: MTLLibrary?
|
||||
|
||||
var finalTexture: MTLTexture?
|
||||
|
||||
var externalSemaphore: DispatchSemaphore?
|
||||
|
||||
|
||||
public init() {
|
||||
|
||||
}
|
||||
@ -175,7 +173,6 @@ final class MediaEditorRenderer: TextureConsumer {
|
||||
|
||||
commandBuffer.addCompletedHandler { [weak self] _ in
|
||||
self?.semaphore.signal()
|
||||
self?.externalSemaphore?.signal()
|
||||
}
|
||||
|
||||
if let _ = self.renderTarget {
|
||||
|
@ -34,6 +34,72 @@ private let adjustmentToolsKeys: [EditorToolKey] = [
|
||||
.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 static let initial = TintValue(
|
||||
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
|
||||
public extension MediaEditorValues {
|
||||
|
@ -2,6 +2,7 @@ import Foundation
|
||||
import AVFoundation
|
||||
import MetalKit
|
||||
import SwiftSignalKit
|
||||
import AccountContext
|
||||
|
||||
enum ExportWriterStatus {
|
||||
case unknown
|
||||
@ -239,6 +240,7 @@ public final class MediaEditorVideoExport {
|
||||
|
||||
public private(set) var internalStatus: Status = .idle
|
||||
|
||||
private let context: AccountContext
|
||||
private let subject: Subject
|
||||
private let configuration: Configuration
|
||||
private let outputPath: String
|
||||
@ -262,7 +264,10 @@ public final class MediaEditorVideoExport {
|
||||
|
||||
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.configuration = configuration
|
||||
self.outputPath = outputPath
|
||||
@ -284,7 +289,7 @@ public final class MediaEditorVideoExport {
|
||||
}
|
||||
|
||||
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()
|
||||
}
|
||||
@ -336,7 +341,7 @@ public final class MediaEditorVideoExport {
|
||||
}
|
||||
|
||||
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)
|
||||
audioOutput.alwaysCopiesSampleData = false
|
||||
if reader.canAdd(audioOutput) {
|
||||
@ -420,28 +425,40 @@ public final class MediaEditorVideoExport {
|
||||
return false
|
||||
}
|
||||
|
||||
var appendFailed = false
|
||||
while writer.isReadyForMoreVideoData {
|
||||
let _ = self.composer?.semaphore.wait(timeout: .distantFuture)
|
||||
|
||||
if appendFailed {
|
||||
return false
|
||||
}
|
||||
if reader.status != .reading || writer.status != .writing {
|
||||
writer.markVideoAsFinished()
|
||||
return false
|
||||
}
|
||||
self.pauseDispatchGroup.wait()
|
||||
if let buffer = output.copyNextSampleBuffer() {
|
||||
if let pixelBuffer = self.composer?.processSampleBuffer(buffer, pool: writer.pixelBufferPool) {
|
||||
if let composer = self.composer {
|
||||
let timestamp = CMSampleBufferGetPresentationTimeStamp(buffer)
|
||||
if !writer.appendPixelBuffer(pixelBuffer, at: timestamp) {
|
||||
writer.markVideoAsFinished()
|
||||
return false
|
||||
}
|
||||
composer.processSampleBuffer(buffer, pool: writer.pixelBufferPool, completion: { pixelBuffer in
|
||||
if let pixelBuffer {
|
||||
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 {
|
||||
if !writer.appendVideoBuffer(buffer) {
|
||||
writer.markVideoAsFinished()
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
// let progress = (CMSampleBufferGetPresentationTimeStamp(buffer) - self.configuration.timeRange.start).seconds/self.duration.seconds
|
||||
// if self.videoOutput === output {
|
||||
// self.dispatchProgressCallback { $0.updateVideoEncodingProgress(fractionCompleted: progress) }
|
||||
|
@ -1044,6 +1044,12 @@ public final class MediaEditorScreen: ViewController {
|
||||
self?.drawingView.isUserInteractionEnabled = false
|
||||
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()
|
||||
}
|
||||
self.controller?.present(controller, in: .current)
|
||||
@ -1217,13 +1223,15 @@ public final class MediaEditorScreen: ViewController {
|
||||
})
|
||||
} else {
|
||||
if let image = mediaEditor.resultImage {
|
||||
if let resultImage = makeEditorImageComposition(inputImage: image, dimensions: storyDimensions, values: mediaEditor.values) {
|
||||
self.completion(.image(resultImage, nil), { [weak self] in
|
||||
self?.node.animateOut(completion: { [weak self] in
|
||||
self?.dismiss()
|
||||
makeEditorImageComposition(context: self.context, inputImage: image, dimensions: storyDimensions, values: mediaEditor.values, time: .zero, completion: { resultImage in
|
||||
if let resultImage {
|
||||
self.completion(.image(resultImage, nil), { [weak self] in
|
||||
self?.node.animateOut(completion: { [weak self] in
|
||||
self?.dismiss()
|
||||
})
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1267,7 +1275,7 @@ public final class MediaEditorScreen: ViewController {
|
||||
|
||||
let configuration = recommendedExportConfiguration(mediaEditor: mediaEditor)
|
||||
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
|
||||
|
||||
export.startExport()
|
||||
@ -1283,12 +1291,13 @@ public final class MediaEditorScreen: ViewController {
|
||||
})
|
||||
} else {
|
||||
if let image = mediaEditor.resultImage {
|
||||
let resultImage = makeEditorImageComposition(inputImage: image, dimensions: storyDimensions, values: mediaEditor.values)
|
||||
if let data = resultImage?.jpegData(compressionQuality: 0.8) {
|
||||
let outputPath = NSTemporaryDirectory() + "\(Int64.random(in: 0 ..< .max)).jpg"
|
||||
try? data.write(to: URL(fileURLWithPath: outputPath))
|
||||
saveToPhotos(outputPath, false)
|
||||
}
|
||||
makeEditorImageComposition(context: self.context, inputImage: image, dimensions: storyDimensions, values: mediaEditor.values, time: .zero, completion: { resultImage in
|
||||
if let data = resultImage?.jpegData(compressionQuality: 0.8) {
|
||||
let outputPath = NSTemporaryDirectory() + "\(Int64.random(in: 0 ..< .max)).jpg"
|
||||
try? data.write(to: URL(fileURLWithPath: outputPath))
|
||||
saveToPhotos(outputPath, false)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user